GD32H759 + RT-Thread 工控实战–第0篇 环境搭建及点灯实验



前言

笔者从2006年开始工作到现在刚好20年,这期间从工控到驱动开发再到各种SaaS以及各种没什么用的信息系统甚至游戏开发,以及各种嵌入式盒子。可谓涉猎广泛,爱好繁多,精力十分过剩。期间一直从社区抄写各种代码,做了20年码抄公,从未回馈任何东西。刚好最近失业了,40多岁了,找工作也十分困难。每天除了接送孩子上下学,无事可做,呆在家里十分无聊。于是就萌生了写点文章打发时间的想法。于是就有本文以及接下来可能会有的数篇拙作


一、工控板选型

我选择了兆易创新的GD32H759I的全功能评估板。选它的理由主要还是符合国产化大趋势,以及我手头刚好有一块别人送的GD32H759I-EVAL 😄。以下资料来自于网络:

GD32H7 是兆易创新(GigaDevice)于 2023 年 5 月推出的超高性能 MCU 系列,也是中国首款基于 Arm Cortex-M7 内核的 MCU 产品,首批包括 GD32H737/757/759 三个子系列共 27 个型号。
核心规格:

  • 内核:600MHz Cortex-M7(Armv7E-M,6 级超标量流水线 + 分支预测),性能 1552 DMIPS / CoreMark 2888 分,同频代码效率较同类产品提升约 10%;
  • 存储:1024KB~3840KB 片上 Flash + 1024KB SRAM(含 512KB 可配置紧耦合内存 ITCM/DTCM),64KB L1 Cache;
  • 加速器:DSP 指令、双精度 FPU、三角函数加速器(TMU)、滤波算法加速器(FAC);
  • 连接外设:3 路 CAN-FD、2 路以太网、USB 2.0 FS/HS OTG、多路 USART/SPI/I2C、TLI LCD 控制器与图形加速器 IPA;
  • 模拟:14 位 ADC(4MSPS)+ 12 位 ADC(5.3MSPS)、比较器、DAC,面向电机控制等场景。
    本系列实战使用的 GD32H759IMK6 是该家族顶配型号(3840KB Flash / 1024KB SRAM),搭载于官方全功能评估板 GD32H759I-EVAL——板载双路 CAN 收发器、以太网 PHY(DP83848)、按键/LED 等,是工控通信类练手的理想平台。

二、编译工具/套件的选取、目标机系统选取

2.1 编译工具/套件

兆易官网以及各种社区笔记给出了诸多可选项。我对它们进行了详细的考察评估。选择如下:

方案费用特点是否采用
Keil MDK收费,商业授权不便宜,有社区版免费国产 MCU 生态支持最好,厂商例程标配
IAR收费,贼贵同上
GD32 Embedded Builder免费兆易官方 Eclipse 系 IDE,开箱即用否,我不喜欢用Eclipse
SEGGER Embedded Studio个人免费体验感挺好,但对GD32新芯片支持滞后;授权条款对商用有限制
VS Code + Cortex-Debug免费编辑体验最好没有之一是,作为编辑前端
GNU GCC Toolchain + Scons + KConfig免费RT-Thread 原生构建体系

2.2 目标机系统

对于做产品且功能单一的产品,我个人并不推荐在目标板上使用任何系统。纯裸C代码直接开跑。
但是对于功能较为复杂,任务链条较长的产品,也就是说当发现目标机的while(1)里的逻辑内容有点过长时,我个人是十分推荐选取一个操作系统的。无论是对代码工程化管理、目标机硬件的有效利用,还是减少复杂任务中的重复工作以及避免不必要的bug等。
这里我采用了rt-thread v5.2.2,选取理由如下:

  1. 设备驱动框架是最大的工程杠杆。 裸机开发里,每换一个外设,初始化、中断、收发缓冲、阻塞/超时语义都要重新发明一遍;RT-Thread 的设备框架把这些收敛成统一契约(rt_device_open/read/write/control + 中断事件上报)。驱动写一次,所有应用复用——本系列后面会看到,CAN、以太网、串口在应用层长一个样。
  2. 组件生态是第二杠杆。 lwIP 协议栈、finsh shell、CAN 子系统,全是 menuconfig 里勾选即用的成熟组件。裸机攒齐这些,即使是有ai agent的辅助,工作量和心智负担也不容小觑。
  3. 硬实时与确定性。 抢占式调度、微秒级中断响应、毫秒级启动——工控场景的底线要求,也是它和 Linux 的本质分野(下文还会简单从驱动与线程2个角度来阐述一下与linux的区别)。
  4. GD32系列支持度高:目前v5.2.2版本已经有了较为完整的bsp级别的支撑(当然有各种坑,后续文章中涉及到时会提出来)
从驱动的角度与Linux对比

RT-Thread 驱动和 Linux 驱动哲学相同,规模悬殊

维度Linux 驱动RT-Thread 驱动
核心思想注册 ops 表(file_operations),框架管通用逻辑注册 ops 表(rt_can_ops / eth_device),框架管通用逻辑——完全一致
设备模型bus/device/driver 三元组 + 设备树匹配扁平的设备注册表,rt_device_find("can1") 按名取用
用户/内核边界有,syscall + copy_to_user没有,应用和驱动同处内核态,直接函数调用
加载方式模块 insmod,运行时加载编译期 INIT_BOARD_EXPORT 自动初始化
配置系统Kconfig + Kbuild + 设备树Kconfig + SCons,静态编译进固件
中断上下文上半部/下半部(tasklet/工作队列)ISR + 事件上报(rt_hw_can_isr),框架转交线程处理
从线程角度与Linux对比

做传统工控的同行大多出身前后台系统(while(1) 大循环 + 中断),听到"多线程"本能警惕:切换开销多大?时序还可控吗?在我个人的经验+D老师的帮助下,我总结成如下2条:

Linux 的线程是用户态实体:一次线程切换要进出内核态、过调度器、可能伴随页表切换和 TLB/缓存污染,开销以微秒到数十微秒计,且调度器(CFS)以"公平"为目标,不承诺谁一定先跑。

RT-Thread 的线程是内核态轻量任务:没有用户/内核分界,所有线程共享同一地址空间。一次切换的全部工作就是"保存当前任务的寄存器现场 → 换栈指针 → 恢复新任务现场",在 Cortex-M 上还借力硬件自动压栈(异常进入时硬件自动保存 8 个寄存器,配合 PendSV 尾链优化)。总开销是几十到一百多条指令——在 600MHz 的 H759 上是百纳秒级、亚微秒。 单看切换开销,已经接近协程切换的量级;但调度方式上它仍是彻底的抢占式,确定性远高于协程。

以我使用的这块板子上来评估的话,一次线程切换的耗时,比一次 rt_kprintf 打印一个字符还要略微快点。

时序可控性反而比大循环更好。 这是最容易被误解的一点:

  • 前后台系统的响应延迟 = 最坏情况下大循环跑完一圈的时间。功能越加越多,这个延迟只涨不跌,还藏在代码里看不见;
  • RT-Thread 是抢占式优先级调度:任何时候,就绪的最高优先级线程立即获得 CPU,延迟 = 固定的切换开销,O(1),与系统里有多少功能无关。高优先级的 CAN 接收任务来了就抢,不排队。

还要注意一个常见误解:系统 tick(默认 1ms)不是切换的节拍器。抢占是事件驱动的——中断里收到 CAN 帧、上报事件、唤醒接收线程,这条链路和 tick 无关,微秒级完成;tick 只管超时等待和同优先级任务的时间片轮转。

真实的代价在内存,不在时间。 每个线程要独立栈(本系列文章后续实操里涉及的一般给 2KB,printf 用得多的线程给大些)。这是 MCU 资源规划的工程问题,除非为了节约成本,目标产品内存干到几KB。否则线程栈的内存开销在我看来几乎可以忽略不计。


三、为何要采用GCC工具链、其他边角料工具清单、工作环境

3.1 为何采用GCC工具链

作为开发了很多嵌入式产品的本人来说,gcc这一套用起来已经成了习惯。开源社区信息、支持都比较多和及时。而我个人最喜欢的vs code + 官方提供的 Cortex插件,如果要使用完全体,则仍需要keil的支持。当然对于企业工作来说,我个人仍会支持keil 更多些。

3.2 其他工具清单

目前用到了串口读写工具以及GD-Link 烧写工具

名称功能
putty或MobaXterm串口读写、交互
GD-LinkUtilityProgrammer_win_v2.xGD-Link 烧写工具

3.3 工作环境

  • 编译环境:WSL
  • 编辑:vscode + wsl remote
  • 串口工具以及gd-link烧写均在windows下进行

3.4 其他边角料

环境全景

Windows 主机
├── WSL2(Ubuntu)—— 全部开发工作在这里
│   └── ~/gd32/
│       ├── toolchain/    arm-gnu-toolchain-15.3.rel1(arm-none-eabi-gcc)
│       ├── vendor/       厂商的 demo suites(仅作参考/对照编译,代码不进工程)
│       └── rt-thread/    RT-Thread v5.2.2 源码(menuconfig + scons 构建)
│           └── bsp/gd32/arm/
│               ├── gd32h759i-eval/
│               │   ├── applications/     ★ 所有测试性代码(can_test.c、tcp_echo.c、
│               │   │                              mb_core/mb_tcp/mb_app 等)
│               │   └── board/            板级配置(Kconfig 菜单、link.ld 链接脚本)
│               └── libraries/
│                   └── gd32_drivers/     ★ 驱动开发目录(drv_can_h7.c、drv_enet.c,
│                                            与官方 drv_*.c 并列,遵循同一套 BSP 惯例)
│
└── Windows 侧
    ├── GD-Link Utility Programmer —— 烧录(GD-Link 的 Windows 工具最稳)
    └── 串口终端(Putty/MobaXterm)—— 控制台

评估板串口驱动下载地址:
https://www.wch.cn/downloads/CH341SER_EXE.html


四、配置

4.1 gcc下载安装及与配置

wget https://gitlab.arm.com/api/v4/projects/tooling%2Fgnu-toolchains-for-arm/packages/generic/gnu-toolchain/15.3.rel1/arm-gnu-toolchain-15.3.rel1-x86_64-arm-none-eabi.tar.xz
tar xJf arm-gnu-toolchain-15.3.rel1-x86_64-arm-none-eabi.tar.xz -C $HOME/gd32/toolchain
echo 'export PATH="$PATH:$HOME/gd32/toolchain/arm-gnu-toolchain-15.3.rel1-x86_64-arm-none-eabi/bin"' >> ~/.bashrc
source ~/.bashrc

4.2 rt-thread下载及配置

  1. 先克隆代码并切换到v5.2.2
cd ~/gd32
git clone https://github.com/RT-Thread/rt-thread.git
git checkout v5.2.2
  1. WSL 安装必要的软件
sudo apt update
sudo apt install -y git scons python3 python3-pip libncurses-dev build-essential
pip install requests        # 或 sudo apt install python3-requests

注意,尽量不要使用conda或者venv,因为后续操作中,rt-thread会使用自己的venv。尽量将requests安装在非conda或venv的全局python3环境中。

  1. 初始化源码仓库
cd ~/gd32/rt-thread/bsp/gd32/arm/gd32h759i-eval
scons --menuconfig

弹出的menuconfig跟linux内核一样的kconfig界面可对rt-thread进行深度定制。此处我们先修改一个参数:

(Top)
RT-Thread Kernel ->
(1024) The stack size of idle thread

(默认 256B 偏小:main 线程退出时是在 idle 线程上下文里销毁的,256B 不够会报 tidle0 stack overflow。提前改掉,免得后面莫名其妙踩坑。)

按ESC看到提示后按Y保存退出

source ~/.env/env.sh   # 首次执行 scons --menuconfig 时 Env 工具会自动安装到 ~/.env/ 并生成此脚本;建议写入 ~/.bashrc 免每次手动 source
pkgs --update

上述操作完毕后,脚本会在 ~/gd32/rt-thread/bsp/gd32/arm/gd32h759i-eval/packages 目录下拉取最新的固件代码库,需要注意的是,这些代码的维护也不一定可靠,事实上也有一些坑,在本系列文章的后续篇章中遇到了会提出来。

五、基础按键及点灯实验

5.1 固件代码库bug修正

修复共两处(bsp/gd32/arm/libraries/gd32_drivers/drv_gpio.c):

第一处(约 601 行,gd32_pin_irq_enable 内):

#if defined SOC_SERIES_GD32H7xx
    exti_init((exti_line_enum)bit2bitno(index->pin), EXTI_INTERRUPT, trigger_mode);
    exti_interrupt_flag_clear((exti_line_enum)bit2bitno(index->pin));
#else
    exti_init((exti_line_enum)(index->pin), EXTI_INTERRUPT, trigger_mode);
    exti_interrupt_flag_clear((exti_line_enum)(index->pin));
#endif

第二处(约 652 行,GD32_GPIO_EXTI_IRQHandler):

void GD32_GPIO_EXTI_IRQHandler(rt_int8_t exti_line)
{
#if defined SOC_SERIES_GD32H7xx
    if (RESET != exti_interrupt_flag_get((exti_line_enum)exti_line)) {
        pin_irq_hdr(exti_line);
        exti_interrupt_flag_clear((exti_line_enum)exti_line);
    }
#else
    if (RESET != exti_interrupt_flag_get((exti_line_enum)(1 << exti_line))) {
        pin_irq_hdr(exti_line);
        exti_interrupt_flag_clear((exti_line_enum)(1 << exti_line));
    }
#endif
}

5.2 编写代码

~/gd32/rt-thread/bsp/gd32/arm/gd32h759i-eval/applications/led_key_test.c

#include <stdio.h>
#include <string.h>
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>

/* ========== LED(注意跳线:JP50(2,3) 接 LED1,JP66(2,3) 接 LED2)========== */
#define LED1_PIN   GET_PIN(F, 10)
#define LED2_PIN   GET_PIN(A, 6)

/* ========== 按键(全部为实测极性)========== */
struct key_desc {
    rt_int32_t  pin;
    rt_uint8_t  active;      /* 按下时的电平 */
    rt_uint8_t  irq_mode;
    rt_uint8_t  pull_mode;
    const char *name;
    rt_uint32_t led_mask;    /* 按下时要点亮的灯:bit0=LED1, bit1=LED2 */
    rt_uint32_t count;
};

static struct key_desc keys[] = {
    { GET_PIN(A, 0),  PIN_HIGH, PIN_IRQ_MODE_RISING,  PIN_MODE_INPUT_PULLDOWN, "WAKEUP(PA0) ", 0x1, 0 },
    { GET_PIN(C, 13), PIN_LOW,  PIN_IRQ_MODE_FALLING, PIN_MODE_INPUT_PULLUP,   "TAMPER(PC13)", 0x2, 0 },
    { GET_PIN(F, 8),  PIN_LOW,  PIN_IRQ_MODE_FALLING, PIN_MODE_INPUT_PULLUP,   "USER  (PF8) ", 0x3, 0 },
};
#define KEY_NUM  (sizeof(keys) / sizeof(keys[0]))

static struct rt_semaphore key_sem;
static struct rt_mailbox   led_mb;
static rt_uint32_t         led_mb_pool[4];

/* ISR:只发信号量 */
static void key_isr(void *args)
{
    rt_sem_release(&key_sem);
}

/* LED 线程:收邮件,按掩码快闪两遍 */
static void led_thread_entry(void *param)
{
    rt_uint32_t mask;

    rt_pin_mode(LED1_PIN, PIN_MODE_OUTPUT);
    rt_pin_mode(LED2_PIN, PIN_MODE_OUTPUT);
    rt_pin_write(LED1_PIN, PIN_LOW);
    rt_pin_write(LED2_PIN, PIN_LOW);

    while (1) {
        if (rt_mb_recv(&led_mb, &mask, RT_WAITING_FOREVER) == RT_EOK) {
            for (int i = 0; i < 2; i++) {
                if (mask & 0x1) rt_pin_write(LED1_PIN, PIN_HIGH);
                if (mask & 0x2) rt_pin_write(LED2_PIN, PIN_HIGH);
                rt_thread_mdelay(120);
                rt_pin_write(LED1_PIN, PIN_LOW);
                rt_pin_write(LED2_PIN, PIN_LOW);
                rt_thread_mdelay(120);
            }
        }
    }
}

/* 按键线程:消抖、计时、上报、派灯效 */
static void key_thread_entry(void *param)
{
    while (1) {
        rt_sem_take(&key_sem, RT_WAITING_FOREVER);
        rt_thread_mdelay(20);

        for (int i = 0; i < KEY_NUM; i++) {
            struct key_desc *k = &keys[i];
            if (rt_pin_read(k->pin) == k->active) {
                rt_uint32_t t0 = rt_tick_get_millisecond();
                while (rt_pin_read(k->pin) == k->active)
                    rt_thread_mdelay(10);
                rt_uint32_t held = rt_tick_get_millisecond() - t0;
                k->count++;

                rt_kprintf("[KEY] %s %s, held %ums, total #%u\n",
                           k->name, held > 1000 ? "LONG " : "short",
                           held, k->count);
                rt_mb_send(&led_mb, k->led_mask);   /* 通知 LED 线程 */
            }
        }
    }
}

int main(void)
{
    rt_kprintf("build marker: v9 - keys + leds\n");

    rt_sem_init(&key_sem, "key", 0, RT_IPC_FLAG_PRIO);
    rt_mb_init(&led_mb, "led", led_mb_pool, 4, RT_IPC_FLAG_PRIO);

    for (int i = 0; i < KEY_NUM; i++) {
        rt_pin_mode(keys[i].pin, keys[i].pull_mode);
        rt_pin_attach_irq(keys[i].pin, keys[i].irq_mode, key_isr, RT_NULL);
        rt_pin_irq_enable(keys[i].pin, PIN_IRQ_ENABLE);
    }

    rt_thread_t tid;
    tid = rt_thread_create("key", key_thread_entry, RT_NULL, 2048, 20, 10);
    if (tid) rt_thread_startup(tid);
    tid = rt_thread_create("led", led_thread_entry, RT_NULL, 1024, 21, 10);
    if (tid) rt_thread_startup(tid);

    rt_kprintf("ready: WAKEUP->LED1, TAMPER->LED2, USER->both\n");
    while (1) rt_thread_mdelay(60000);
    return RT_EOK;
}

5.3 编译及下载烧写以及测试结果

cd ~/gd32/rt-thread/bsp/gd32/arm/gd32h759i-eval
scons -j`nproc`

顺利的话会在当前目录得到一个名字rtthread.bin的文件。将它拷贝到windows目录去,我这里是 D:\work\gd32\bin

cp rtthread.bin /mnt/d/work/gd32/bin/

接下来就可以在windows上打开 GD-Link Utility Programmer,板子上好电,插好gd-link线和uart线(2条都是typec usb线).跳好跳线 注意看丝印,跳线分别是:JP50(2,3)、JP66(2,3);另外 USER 键还需要 JP42(2,3),控制台需要 JP68(2,3)

gd-link 选择 File -> Open File -> 选中刚刚拷贝出来的 rtthread.bin文件。gd-link选择 Target->Connect。
当看到gd-link日志窗口出现类似如下消息,就代表 GD-Link 连接成功:

[09:53:44] Connected successfully! 
[09:53:44] Getting option successfully! 
[09:53:44] D0 AA C6 01 D0 AA C6 01 FF FF FF FF FF 00 00 00 

gd-link选择Target->Program。等在日志窗口出现类似如下消息即代表下载烧写成功:

[09:53:48] Starting programming: 
[09:53:48] Erasing.... 
[09:53:51] -----Erasing complete! 
[09:53:51] -----Erasing time: 3.217 s 
[09:53:51] Reset MCU.... 
[09:53:51] -----Reset MCU complete! 
[09:53:51] Downloading data.... 
[09:53:57] -----Downloading data complete! 
[09:53:57] -----Programming time: 5.636 s 
[09:53:57] -----Programming speed: 38.76 KB/s 
[09:53:57] Programming and Verification Successfully! 

gd-link 选择Target->Disconnect

putty 打开评估板的串口(115200-8-1-N,流控 None)
然后按一下评估板的reset按键
如果能看到类似如下控制消息则代表成功:

 \ | /
- RT -     Thread Operating System
 / | \     5.2.2 build Sep  9 2026 08:29:26
 2006 - 2024 Copyright by RT-Thread team
build marker: v9 - keys + leds
ready: WAKEUP->LED1, TAMPER->LED2, USER->both
msh />

此时可以分别按下 wake up,TAMPER,USER 三个按钮,可以看到串口控制台有对应的输出,以及LED1和LED2按程序中的规律在闪烁。

六、问题排查及资料检索

1,串口无任何东西输出 ,检查跳线JP68是否跳到了2,3位置,检查驱动是否装好
2,官方提供的GD32H7xx_Demo_Suites_V2.1.0\GD32H759I_EVAL_Demo_Suites\Docs\User_Guide目录下有非常详细的文档资料。关于硬件的任何疑问都应该先去文档里查原理图。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐