GD32H759 + RT-Thread 工控实战--第0篇 环境搭建及点灯实验
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,选取理由如下:
- 设备驱动框架是最大的工程杠杆。 裸机开发里,每换一个外设,初始化、中断、收发缓冲、阻塞/超时语义都要重新发明一遍;RT-Thread 的设备框架把这些收敛成统一契约(
rt_device_open/read/write/control+ 中断事件上报)。驱动写一次,所有应用复用——本系列后面会看到,CAN、以太网、串口在应用层长一个样。 - 组件生态是第二杠杆。 lwIP 协议栈、finsh shell、CAN 子系统,全是 menuconfig 里勾选即用的成熟组件。裸机攒齐这些,即使是有ai agent的辅助,工作量和心智负担也不容小觑。
- 硬实时与确定性。 抢占式调度、微秒级中断响应、毫秒级启动——工控场景的底线要求,也是它和 Linux 的本质分野(下文还会简单从驱动与线程2个角度来阐述一下与linux的区别)。
- 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.x | GD-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下载及配置
- 先克隆代码并切换到v5.2.2
cd ~/gd32
git clone https://github.com/RT-Thread/rt-thread.git
git checkout v5.2.2
- 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环境中。
- 初始化源码仓库
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目录下有非常详细的文档资料。关于硬件的任何疑问都应该先去文档里查原理图。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)