CAN数据收发实验

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

184.1.7 CAN数据收发实验

#include "string.h" /* 声明 memset();原工程使用引号形式,这里依照课程源码保留。 */

/* main.c:以下语句位于 while (1) 中原有的 #if 1 接收分支,仅摘录本课最终展示的接收片段。 */
CAN_RxHeaderTypeDef RxMessage; /* 在本轮循环中定义局部接收帧头,用来承载 ID、IDE、RTR、DLC 等信息。 */

HAL_CAN_GetRxMessage(&hcan, CAN_FILTER_FIFO0,&RxMessage, Rx_Data); /* 尝试从 FIFO0 取一帧;原代码没有保存或检查返回值。 */

printf("recv id 0x%x:\r\n", RxMessage.StdId); /* 按十六进制打印标准标识符;字符串中的 0x 是人工加入的前缀。 */
printf("recv length %u:\r\n", RxMessage.DLC); /* 按无符号十进制打印 DLC;本实验发送值为 8。 */
printf("recv:"); /* 打印接收数据行的提示文字。 */
for(int i=0;i<8;i++) /* 固定遍历 8 次;原代码没有按本帧 RxMessage.DLC 限定长度。 */
        printf(" %u ",Rx_Data[i]); /* 将第 i 个接收字节以无符号十进制输出。 */
printf("\r\n"); /* 一帧的 8 个数据字节打印完后回车并换行。 */

memset(Rx_Data, 0, 8); /* 把 Rx_Data 起始地址后的 8 字节全部写为 0;这只清空数据缓冲区。 */
        printf("recv memset:"); /* 输出清零验证行的提示文字;原源码中这一行的缩进比相邻语句更深。 */
for(int i=0;i<8;i++) /* 再遍历 8 个字节,仅用来观察 memset() 之后的缓冲区。 */
        printf(" %u ",Rx_Data[i]); /* 正常应连续打印 8 个 0。 */
printf("\r\n"); /* 清零验证行输出完成后换行。 */

源码范围说明:上面只收录本课在 main.c 中实际新增并测试的 string.h、ID/DLC 打印、memset() 及其验证输出,同时保留必要的接收上下文。来自第 183 课的全局数组、CAN 启动/过滤器、串口打印宏和发送分支不重复抄录,也没有把 HAL 库函数实现强行加进来。业务语句按课程配套源码保留,它们的限制只在注释和正文中说明,未擅自修复。

一、CAN数据收发实验

本课把前几课已经写好的过滤器、CAN 启动、发送帧和轮询接收代码放到两块实物板上运行:C8T6 作为发送节点,ZET6 作为接收节点,两端再各用一路 USART1 日志观察结果。

C8T6 发送分支
  │ PA12/CAN_TX,PA11/CAN_RX
  ▼
CAN 收发器 ══ CANH/CANL 双绞线 ══ CAN 收发器
                                           ▲
                                           │ PA12/CAN_TX,PA11/CAN_RX
                                      ZET6 接收分支

C8T6 USART1 ── CH340 ── 串口助手①
ZET6 USART1 ── 课程板载 USB 串口 ── 串口助手②

这个实验要同时证明三条独立链路:发送端程序在运行、CAN 报文确实经过物理总线到达另一节点、接收端又能把帧头和数据经串口输出。某个串口窗口有文字,不能单独证明整条 CAN 链路都已正常。

1.1 硬件连接

课程中的“两块单片机都接 5 V”必须理解为“向开发板的 USB/5V 输入端供电,再由板上稳压器产生 3.3 V”,不是给 STM32 的 VDD 或 GPIO 直接加 5 V。STM32F103 芯片的正常应用电源范围是 2.0~3.6 V。

CAN 一侧按下表连接:

位置连接作用与注意事项
ZET6 PA12/CAN_TX本端收发器 CTX/TXDMCU 内部 CAN 控制器把待发位流送给收发器
ZET6 PA11/CAN_RX本端收发器 CRX/RXD收发器把总线实际状态返回控制器
C8T6 PA12/CAN_TX本端收发器 CTX/TXD这是发送节点的 CAN 输出
C8T6 PA11/CAN_RX本端收发器 CRX/RXD即使主要作为发送端也必须接 RX,控制器需要观察仲裁、ACK 和错误
两个收发器 CANHCANH ↔ CANH不能与 CANL 交叉
两个收发器 CANLCANL ↔ CANL与 CANH 组成一对双绞线
课程的两个 VP230 收发器 VCC各接本端稳定 3.3 V这是收发器芯片供电,与开发板的 5 V 输入不是一回事
两块板及两个收发器 GND共地非隔离台架实验应提供共同参考地,只接 CANH/CANL 不够稳妥

课程实物的收发器芯片丝印在前课确认为 VP230,对应 SN65HVD230,它是 3.3 V 收发器;其模块 VCC 不应因为开发板用 5 V 输入就跟着接 5 V。如果手边是其他芯片,必须以丝印、模块原理图和对应数据手册重新确认供电。

两节点处于总线两端时,应在两端各放一个 120 Ω 终端电阻。模块可能已集成电阻或用跳帽控制,不要盲目重复并联;断电后在 CANH 与 CANL 之间测到约 60 Ω,通常表示两只 120 Ω 电阻已并联在线。桌面短线即使偶尔在终端不完善时也能通信,也不能据此认为长线或高速系统可以不加终端。

1.2 代码分析

两块板都需要一条串口观测通道,但两个串口不参与 CAN 报文传输,它们只是把各自程序的日志发到电脑:

节点USART1 接线说明
课程的 ZET6 开发板使用该板板载 USB-串口数据线这是课程板级功能,不是所有 ZET6 最小系统板都自带
C8T6PA9/USART1_TX → CH340 RXTX 接对方 RX,由 MCU 向电脑发日志
C8T6PA10/USART1_RX ← CH340 TXRX 接对方 TX;本课主要打印,但仍按完整串口接法连接
C8T6GND ↔ CH340 GND逐字稿的“D 线”是识别错误,这里指共地线

配套 usart.c 配置为 115200 波特率、8 位数据位、1 位停止位、无校验、无硬件流控,串口助手应选择同样的 115200-8-N-1。CH340 模块的逻辑电平和供电跳帽存在多种版本,应优先使用与 3.3 V MCU 兼容的 TX/RX 电平,不要仅根据 USB 供电是 5 V 就推断信号电平也必须是 5 V。同时使用 USB、CH340 和 ST-Link 时,还要避免多路电源互相反馈。

循环中的两条日志含义不同:

  • 发送分支里的 Send ok! 位于 HAL_CAN_AddTxMessage() 后,却没有检查该函数的返回值,因而只能证明程序执行到了这里,不能单独证明邮箱成功入队、帧已发出或已收到 ACK。
  • 接收分支里的 recv: 也是无条件执行。只有在 HAL_CAN_GetRxMessage() 返回 HAL_OK 时,RxMessage 和 Rx_Data 才是刚刚取出的新报文。

1.3 测试接收端代码

接收端使用 ZET6,课程把条件编译开关设为 #if 1。“1”的意义是预处理器保留接收分支,并在编译前排除 #else 中的发送分支;它不是芯片在运行时自动识别自己应该收还是发。

推荐的烧录顺序是:

  1. 先断电核对 ZET6 的 SWDIO、SWCLK、GND和目标电压参考线,确认 ST-Link 接口方向;
  2. 把条件编译开关设为 1,确认本次编译的是接收分支;
  3. 使用与 STM32F103ZE 匹配的 Device、512 KB Flash 地址范围、高密度启动文件和 Flash 算法,然后重新 Build;
  4. 在 Options for Target → Debug 选择 ST-Link,再到 Settings → Flash Download 检查烧录算法;
  5. 勾选 Reset and Run,让下载与校验完成后自动复位并运行;若未勾选,可以手动按开发板复位键。

逐字稿中“Pack 那个选项不能勾”无法对应到一个清楚的 Keil 复选框,而 Pack 通常是配置窗口的标签页。不应按这句识别结果去取消一个不明选项;下载后不运行时,应优先确认 Reset and Run、复位脚/BOOT 状态、正确的 Device/启动文件/内存范围以及烧录日志。

1.4 测试发送端代码

发送端使用 C8T6,课程把同一处条件编译开关改为 #if 0,使 #else 发送分支参与编译。改完必须重新 Build,否则烧入的仍可能是上一次构建的接收版固件。

切换目标板时先断电,再把 ST-Link 从 ZET6 移到 C8T6,不要只按“红黑线或正负极”记忆。不同开发板的四针排列可能不同,应逐针核对 SWDIO、SWCLK、GND 和目标电压参考;把电源与地接反可能损坏开发板或调试器。

这里必须纠正课程中的 Flash 容量说法:

芯片官方 Flash常用 SRAM对应密度/启动文件
STM32F103C8T664 KB20 KB中密度,Cube HAL 工程通常用 startup_stm32f103xb.s
STM32F103ZET6512 KB64 KB高密度,使用 startup_stm32f103xe.s

STM32F103x8/xB 数据手册覆盖的家族上限是 128 KB,Keil 里的 STM32F10x 128kB Flash 也是一个算法/家族范围名称;它们都不能把型号中容量码为 8 的正规 C8T6 变成 128 KB 器件。某些开发板上的非原厂或不同批次芯片可能表现出额外地址可读写,也不应当作设计依据。

当前归档的 01Can_Test.uvprojx 反映了课程现场切换后的混合状态:

  • Device 已改为 STM32F103C8;
  • IROM 为 0x08000000 + 0x10000,即 64 KB,IRAM 为 20 KB;
  • Flash Driver 名称中是 STM32F10x_128;
  • 工程组里仍然是 ZET6 的高密度 startup_stm32f103xe.s;
  • .ioc 仍然把目标芯片记为 STM32F103ZET6。

因此,课程中“小容量工程可烧进大容量芯片,反之不行”最多只能描述当时那个小固件恰好占用低地址且烧录器能写入的现象,不是完整的兼容性规则。Device、IROM/IRAM、启动文件和中断向量、宏定义、Flash 算法、引脚封装与 CubeMX 目标都应同时匹配。实验时最稳妥的做法是建立 ZET6_Recv 和 C8T6_Send 两个独立 Target,而不是每次覆盖同一份配置。

1.5 代码bug与优化

课程准确发现了一个现象:Rx_Data 是全局数组,某次接收成功后会保留该帧的 8 个字节;下一次调用在 FIFO 为空时失败,HAL 不会自动把数组改成“无数据”,因此旧值可能继续留在缓冲区中。

本课在打印后加入 memset(Rx_Data, 0, 8),它的精确含义是:从 Rx_Data 的起始地址开始,把连续 8 个字节都写入位模式 0。Rx_Data 恰好是 8 个 uint8_t,所以本次参数没有越界。增加 #include "string.h" 是为了让编译器看到 memset() 的标准函数声明。

但这一改动没有修复根因,只是把“误把旧数据当新数据打印”变成了“接收失败时大概打印一行 0”:

  1. HAL_CAN_GetRxMessage() 仍然没有检查返回值。本项目的 HAL 在 FIFO0 为空时会返回 HAL_ERROR,并在写入帧头和数据之前就退出。
  2. RxMessage 是每轮循环中未初始化的局部变量。接收失败后仍读取 StdId 和 DLC 属于读取未确定值,即使它在某次运行中“看起来还是上一帧”,也不能依赖。
  3. 打印数据的循环仍固定执行 8 次。如果以后接收 DLC 小于 8 的帧,有效负载只有前 DLC 个字节。
  4. 代码无条件打印 ID/DLC/数据/清零数据,而项目的 printf 又是 115200 波特率的阻塞式 UART 输出。报文速率较高时,大量日志会拖慢接收循环,甚至增加 FIFO 溢出风险。
  5. 接收函数第二个参数使用了 CAN_FILTER_FIFO0,这个名称语义上是“把过滤器分配给 FIFO0”;接收 API 对应的宏应是 CAN_RX_FIFO0。本 HAL 版本中二者数值恰好都是 0,所以课程代码能运行,但读代码时不应把两个概念混为一谈。

真正的处理原则应是:先确认 FIFO 有帧,或直接判断 HAL_CAN_GetRxMessage() 是否返回 HAL_OK;只在成功时使用帧头和数据,并按 DLC 处理有效字节。这是后续优化方向,本课配套源码并未实现,因此上方代码块不强行补写。

1.6 查看接收数据

两块板都上电、两个固件都已运行后,发送端会持续打印 Send ok!,接收端应观察到与下面类似的内容:

recv id 0x12:
recv length 8:
recv: 7 8 5 24 11 2 13 9
recv memset: 0 0 0 0 0 0 0 0

这里有三个显示细节:

  • 发送数组在 C 源码中是 {7,8,5,24,11,2,13,9},%u 以十进制显示,所以数据行里的 24、11、13 不是十六进制字符串。
  • 标准 ID 源码中是 0x12,如用 %u 显示就是十进制 18;课程改成 %x 并在格式字符串里加上 0x,才显示为 0x12。%x 本身不会自动加前缀。
  • recv memset: 的八个 0 只证明紧接着前一行执行的 memset() 已把缓冲区清零,不能证明“每个 CAN 报文都已成功接收”。要证明每行对应新帧,仍要检查 HAL 返回值或 FIFO 消息数。

如果发送端有日志而接收端没有,先不要改数据数组,按以下顺序排查:

  1. 按一次 ZET6 复位键,并检查 Reset and Run;
  2. 确认两个串口助手选中了正确的 COM 口与 115200-8-N-1;
  3. 确认两端都为正常 CAN 模式、相同位率;本工程 APB1 的 CAN 时钟为 36 MHz,预分频为 4,一位为 1 + 9 + 8 = 18 TQ,因而目标位率为 36 MHz ÷ (4 × 18) = 500 kbit/s;
  4. 检查 CANH↔CANH、CANL↔CANL、共地、收发器供电和终端电阻;
  5. 确认 C8T6 真正烧入了 #if 0 的发送构建,ZET6 真正烧入了 #if 1 的接收构建;
  6. 最后再查看 CAN 错误状态、ACK 错误、Bus-Off 和 FIFO 溢出,不要把一条无条件 Send ok! 当作最终诊断结论。

1.7 结构体变量讲解

CAN_RxHeaderTypeDef RxMessage; 中,CAN_RxHeaderTypeDef 是结构体类型,RxMessage 是该类型的一个变量。可以把它理解成一张“接收帧信息表”,各成员保存同一帧的不同属性:

成员含义本实验的理解
StdId11 位标准标识符在 IDE == CAN_ID_STD 时有效,本课收到 0x12
ExtId29 位扩展标识符只在扩展帧时作为报文 ID
IDE标准帧/扩展帧类型决定应读 StdId 还是 ExtId
RTR数据帧/远程帧标志本课发送的是数据帧
DLC数据段长度Classical CAN 为 0~8,本课为 8
Timestamp接收起始时捕获的时间戳只在相关时间触发模式开启时具有对应用途
FilterMatchIndex命中的过滤器索引可用来判断该帧由哪个过滤器接收

RxMessage.StdId 中的点运算符表示“取出结构体变量 RxMessage 的 StdId 成员”。若手里是指向该结构体的指针,才会使用 ->;本课变量本身不是指针,因此用 .。

发送时,程序先把 ID、IDE、RTR 和 DLC 等写入 CAN_TxHeaderTypeDef,再把帧头与 Tx_Data 一起交给 HAL;接收时,HAL 反过来把寄存器中的帧头填入 CAN_RxHeaderTypeDef,把负载填入 Rx_Data。所以“收到一帧”不只是得到 8 个字节,还应同时知道该帧的标识符、格式、帧类型和长度。

本课直接打印 StdId 是因为发送端明确构造了标准数据帧。如果将来的总线也可能出现扩展帧,程序应先检查 IDE,而不能对所有报文都无条件解释 StdId。同理,业务数据的解析应在接收成功并确认 ID、RTR 和 DLC 符合预期之后进行。

参考资料