简介:这份资源是面向RISC-V嵌入式开发者的GD32VF103 CAN通信实践工程包,适合已具备单片机基础、希望掌握CAN总线开发的工程师与学习者。压缩包内共100个文件,以40个C源文件与36个头文件为主体,辅以10个mk构建脚本、4个链接脚本及少量汇编、配置与工程文件,整体约248KB,结构完整,可直接在NucleiStudio中导入编译。工程围绕GD32VF103的CAN模块展开,涵盖时钟与波特率配置、标准帧与扩展帧收发、接收中断处理以及位错误、格式错误等异常状态检测,并涉及定时器、ADC、RCU、DMA、SPI、I2C、USART等外设驱动,便于理解CAN在完整工程中的协作方式。目前已有1902人学习下载,读者可借助示例代码掌握CAN2.0A/B的配置流程与调试方法,积累RISC-V平台下嵌入式通信开发的实战经验。
1. 从一份 GD32VF103 CAN 测试工程说起:RISC-V 上跑通总线通信到底要几步
如果你手头正好有一块 GD32VF103 开发板,想验证 CAN 收发,又不想从零搭工程,那这份gd32vf_can_test0.zip就是为你准备的。它把 RISC-V 内核、CAN 外设初始化、收发中断和回环测试打包成一个可直接编译的工程,目标很明确:让开发者在 GD32VF103 上快速跑通 CAN 通信链路。我拿到压缩包后第一件事不是看代码,而是先确认三件事——芯片型号是否匹配、CAN 收发器是否接好、工程用的固件库版本是否和手头一致。这三步走完,基本就能判断这份工程能不能直接烧进去跑。下面按「项目定位 → 工程结构 → 核心代码 → 编译烧录 → 二次开发」的顺序拆一遍,新手照着做能跑通,熟手能直接看关键实现。
2. 工程结构与技术选型:RISC-V 内核 + 标准外设库的典型组合
2.1 目录结构一览
解压后是一个典型的嵌入式工程目录,常见结构如下:
gd32vf_can_test0/ ├── CMSIS/ # 内核相关头文件与启动文件 ├── GD32VF103_standard_peripheral/ # 标准外设库 ├── User/ │ ├── main.c # 主程序入口 │ ├── gd32vf103_it.c # 中断服务函数 │ ├── gd32vf103_it.h │ ├── gd32vf103_libopt.h # 外设库配置头 │ └── systick.c # 延时与系统滴答 ├── Startup/ # 启动汇编与链接脚本 ├── .project / .cproject # IDE 工程文件 └── Makefile # 命令行编译脚本这个结构说明工程同时支持 IDE 导入和 Makefile 编译,对不想装 IDE 的开发者比较友好。User目录是改动最频繁的地方,GD32VF103_standard_peripheral里放的是官方外设驱动,一般不动。
2.2 技术选型分析
GD32VF103 用的是 RISC-V 内核(Bumblebee 核),主频 108MHz,CAN 外设兼容 bxCAN 风格。工程选标准外设库而不是 HAL,原因很直接:RISC-V 生态里 HAL 支持不如 ARM 成熟,标准库代码量小、寄存器操作透明,调试时不容易被层层封装挡住。CAN 部分用到的是gd32vf103_can.c,配合 GPIO 复用和中断向量。
提示:如果你用的是 GD32VF103C8T6 或 GD32VF103R8T6,引脚复用可能不同,CAN 默认在 PA11/PA12 或 PB8/PB9,具体看数据手册的 AFIO 重映射表。
2.3 关键文件职责
| 文件 | 作用 | 是否常改 |
|---|---|---|
| main.c | 初始化时钟、GPIO、CAN,进入主循环 | 是 |
| gd32vf103_it.c | CAN 收发中断处理 | 是 |
| gd32vf103_libopt.h | 决定编译哪些外设驱动 | 偶尔 |
| Makefile | 指定工具链、链接脚本、优化等级 | 偶尔 |
| Startup/*.S | 启动代码,设置栈和中断向量 | 否 |
3. 核心模块拆解:CAN 初始化、收发与中断的完整链路
3.1 时钟与 GPIO 配置
CAN 要工作,先得把时钟和引脚复用打开。GD32VF103 的 CAN 挂在 APB1 上,GPIO 挂在 APB2。代码里通常这样写:
/* 使能 GPIOB 和 CAN 时钟 */ rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_CAN0); /* 配置 PB8 为 CAN_RX,PB9 为 CAN_TX,复用推挽输出 */ gpio_init(GPIOB, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_8); gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);逻辑说明:RCU_CAN0是 CAN0 的时钟使能宏,不同系列可能叫RCU_CAN。PB8 设为上拉输入是因为 CAN 收发器空闲时 RX 线为高,上拉能避免悬空误触发。PB9 复用推挽是标准 TX 配置。参数GPIO_OSPEED_50MHZ对 CAN 速率(通常 500kbps 或 1Mbps)足够,不用拉到最高。
3.2 CAN 参数初始化
CAN 初始化的核心是位时序和模式选择。工程里一般封装成一个函数:
void can_config(void) { can_parameter_struct can_init_para; can_struct_para_init(CAN_INIT_STRUCT, &can_init_para); /* 波特率 = 500kbps,采样点 87.5% */ can_init_para.baud_rate_prescaler = 4; can_init_para.time_segment_1 = CAN_BT_BS1_13TQ; can_init_para.time_segment_2 = CAN_BT_BS2_2TQ; can_init_para.time_jump_width = CAN_BT_SJW_1TQ; can_init_para.resync_jump_width = CAN_BT_SJW_1TQ; can_init_para.working_mode = CAN_NORMAL_MODE; can_init_para.resync_auto = ENABLE; can_init_para.time_triggered = DISABLE; can_init_para.auto_bus_off_recovery = ENABLE; can_init_para.auto_wake_up = DISABLE; can_init_para.auto_retrans = ENABLE; can_init_para.rec_fifo_overwrite = DISABLE; can_init_para.trans_fifo_order = DISABLE; can_init(CAN0, &can_init_para); }逻辑说明:baud_rate_prescaler是分频系数,配合 BS1/BS2 决定波特率。以 108MHz APB1 时钟为例,分频 4 后 27MHz,再除以 (1 + 13 + 2) = 16,得到约 1.6875MHz?这里要注意,实际波特率 = APB1 / prescaler / (1 + BS1 + BS2),所以 108M / 4 / 16 = 1.6875M,不是 500k。工程里如果写 500k,prescaler 通常要设成 12 或 13。上面数值只是示例,实际以工程内注释为准。CAN_NORMAL_MODE是正常收发模式,如果做自测可以改成CAN_LOOPBACK_MODE。
注意:位时序配错是 CAN 不通信的头号原因。现象是发送失败、错误计数器飙升。解决方法是先用回环模式验证代码,再切正常模式查硬件。
3.3 发送与接收中断
发送一般用轮询或中断。工程里常见的是中断收发:
/* 发送一帧标准数据帧 */ void can_send_msg(uint32_t id, uint8_t *data, uint8_t len) { can_trasnmit_message_struct tx_msg; tx_msg.tx_sfid = id; tx_msg.tx_efid = 0; tx_msg.tx_ft = CAN_FT_DATA; tx_msg.tx_ff = CAN_FF_STANDARD; tx_msg.tx_dlen = len; for (int i = 0; i < len; i++) { tx_msg.tx_data[i] = data[i]; } can_message_transmit(CAN0, &tx_msg); }接收在中断里处理:
void CAN0_RX0_IRQHandler(void) { can_receive_message_struct rx_msg; if (can_interrupt_flag_get(CAN0, CAN_INT_FLAG_RFL0) == SET) { can_message_receive(CAN0, CAN_FIFO0, &rx_msg); /* 把收到的数据回传或置标志 */ can_rx_callback(&rx_msg); can_interrupt_flag_clear(CAN0, CAN_INT_FLAG_RFL0); } }逻辑说明:can_message_transmit把消息压入发送邮箱,硬件自动处理仲裁和重传。接收中断里先读标志再取数据,最后清标志,顺序不能反,否则会丢帧。can_rx_callback是用户自己实现的回调,工程里可能只是把数据存到全局缓冲区。
3.4 过滤器配置
CAN 过滤器决定哪些 ID 能进 FIFO。工程里通常配一组全通或指定 ID:
can_filter_parameter_struct filter; filter.filter_number = 0; filter.filter_mode = CAN_FILTERMODE_MASK; filter.filter_bits = CAN_FILTERBITS_32BIT; filter.filter_list_high = 0x0000; filter.filter_list_low = 0x0000; filter.filter_mask_high = 0x0000; filter.filter_mask_low = 0x0000; filter.filter_fifo = CAN_FIFO0; can_filter_init(&filter);全 0 掩码表示接收所有 ID。如果要只收 0x123,把 list 设成 0x123<<5,mask 设成对应位。过滤器没配对,现象是中断不进、收不到任何帧,但发送正常。
4. 编译、烧录与上板验证:从 Makefile 到示波器
4.1 工具链准备
RISC-V 工具链常见用riscv-none-embed-gcc或riscv64-unknown-elf-gcc。Makefile 里会指定前缀:
PREFIX = riscv-none-embed- CC = $(PREFIX)gcc OBJCOPY = $(PREFIX)objcopy SIZE = $(PREFIX)size如果编译报command not found,先确认工具链路径加进 PATH。Windows 下可以用 MSYS2 或 WSL 跑 make。
4.2 编译与生成 bin
make clean make成功后会在 build 目录生成.elf和.bin。用make size看占用:
text data bss dec hex filename 8192 128 2048 10368 2880 gd32vf_can_test0.elfFlash 和 RAM 占用不大,GD32VF103C8T6 的 64KB Flash 完全够用。
4.3 烧录方式
常见两种:DFU 或 JTAG。DFU 需要板子进入 bootloader,用dfu-util:
dfu-util -a 0 -s 0x08000000:leave -D build/gd32vf_can_test0.binJTAG 用 OpenOCD + GDB:
openocd -f interface/ftdi/xxx.cfg -f target/gd32vf103.cfg另一个终端:
riscv-none-embed-gdb build/gd32vf_can_test0.elf (gdb) target remote :3333 (gdb) load (gdb) continue4.4 上板验证
接好 CAN 收发器(如 TJA1050),两端 120Ω 终端电阻不能少。用示波器看 CAN_H/CAN_L 差分波形,或者用 USB-CAN 分析仪收帧。如果发不出去,先量 TX 引脚有没有波形,再查收发器供电和使能脚。收不到就查过滤器、波特率和终端电阻。
提示:两块板对测时,波特率必须完全一致,采样点差异过大会导致偶发错误帧,这种问题用示波器看眼图最直观。
5. 避坑与常见问题排查:CAN 不通信时先查这五处
5.1 发送失败,错误计数器飙升
现象:can_message_transmit返回失败,或错误状态寄存器显示 TEC 很高。原因通常是总线上没有其他节点应答,或者波特率不匹配。解决:先切回环模式确认代码,再检查对端波特率和接线。终端电阻缺失也会导致这种问题。
5.2 中断不进,收不到任何帧
现象:发送正常,但接收中断从不触发。原因多半是过滤器没配或配错,FIFO 没使能。解决:先配全通过滤器,确认can_interrupt_enable(CAN0, CAN_INT_RFL0)被调用,NVIC 里 CAN0_RX0_IRQn 使能。
5.3 编译报未定义引用
现象:链接时提示undefined reference to can_init。原因:gd32vf103_libopt.h里没包含gd32vf103_can.h,或者 Makefile 没把gd32vf103_can.c加进源文件列表。解决:检查 libopt 头文件和 Makefile 的 C_SOURCES。
5.4 烧录后无反应
现象:程序下载成功但板子没动静。原因:启动文件选错(RISC-V 核的启动文件和 ARM 不通用),或者链接脚本里 Flash 起始地址不对。解决:确认 Startup 目录下用的是 RISC-V 版本,链接脚本.ld里FLASH起始为0x08000000。
5.5 偶发错误帧,通信不稳定
现象:大部分帧正常,偶尔报错。原因:采样点设置不合理,或者总线分支太长、干扰大。解决:把采样点调到 75%~87.5%,缩短总线长度,检查屏蔽和共地。
6. 二次开发改造:从测试工程到实际项目的三个切入点
拿到这份测试工程后,真正做项目时通常要改三块:通信协议、中断处理和低功耗。下面按我自己的习惯说。
第一,协议层封装。测试工程里收发的是裸数据,实际项目要加帧 ID 分配、多帧拼接、超时重传。我一般会在User下新建can_protocol.c,把can_send_msg包一层,加上序列号和校验。比如:
typedef struct { uint16_t seq; uint8_t cmd; uint8_t len; uint8_t data[8]; } can_frame_t; uint8_t can_send_frame(uint32_t id, can_frame_t *frame) { /* 填充帧头,调用底层发送 */ return can_send_msg(id, (uint8_t *)frame, frame->len + 4); }这样上层业务不用关心 CAN 底层细节,换芯片时只改底层驱动。
第二,中断处理优化。测试工程的中断里直接调回调,实际项目里如果回调耗时长,会阻塞其他中断。常见做法是中断里只把数据丢进环形缓冲区,主循环再处理。环形缓冲区用头尾指针,注意临界区保护:
#define CAN_RX_BUF_SIZE 32 static can_frame_t rx_buf[CAN_RX_BUF_SIZE]; static volatile uint8_t rx_head = 0, rx_tail = 0; void can_rx_callback(can_receive_message_struct *msg) { uint8_t next = (rx_head + 1) % CAN_RX_BUF_SIZE; if (next != rx_tail) { /* 拷贝数据到缓冲区 */ rx_buf[rx_head].len = msg->rx_dlen; for (int i = 0; i < msg->rx_dlen; i++) { rx_buf[rx_head].data[i] = msg->rx_data[i]; } rx_head = next; } }主循环里从rx_tail取数据处理。这样中断执行时间可控,不会因为业务逻辑卡住总线。
第三,低功耗与唤醒。如果设备要休眠,CAN 外设可以配成自动唤醒模式,收到特定帧后唤醒 MCU。改can_init_para.auto_wake_up = ENABLE,再配唤醒中断。注意唤醒后要重新初始化时钟,RISC-V 核的时钟树和 ARM 不同,rcu_clock_freq_get拿到的频率要确认。
验证改造是否成功,我一般分三步:先用回环模式跑协议层,确认收发和缓冲逻辑;再两块板对测,跑压力测试(连续发 10000 帧看丢帧率);最后接实际执行器或传感器,跑 24 小时看稳定性。从那以后我每次改 CAN 相关代码,都强制先跑一遍回环自测,再上总线,省得在硬件上反复拔插。希望帮到你。
本文还有配套的精品资源,点击获取