CAN数据收发实验
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/TXD | MCU 内部 CAN 控制器把待发位流送给收发器 |
ZET6 PA11/CAN_RX | 本端收发器 CRX/RXD | 收发器把总线实际状态返回控制器 |
C8T6 PA12/CAN_TX | 本端收发器 CTX/TXD | 这是发送节点的 CAN 输出 |
C8T6 PA11/CAN_RX | 本端收发器 CRX/RXD | 即使主要作为发送端也必须接 RX,控制器需要观察仲裁、ACK 和错误 |
两个收发器 CANH | CANH ↔ CANH | 不能与 CANL 交叉 |
两个收发器 CANL | CANL ↔ 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 最小系统板都自带 |
| C8T6 | PA9/USART1_TX → CH340 RX | TX 接对方 RX,由 MCU 向电脑发日志 |
| C8T6 | PA10/USART1_RX ← CH340 TX | RX 接对方 TX;本课主要打印,但仍按完整串口接法连接 |
| C8T6 | GND ↔ 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 中的发送分支;它不是芯片在运行时自动识别自己应该收还是发。
推荐的烧录顺序是:
- 先断电核对 ZET6 的
SWDIO、SWCLK、GND和目标电压参考线,确认 ST-Link 接口方向; - 把条件编译开关设为 1,确认本次编译的是接收分支;
- 使用与 STM32F103ZE 匹配的 Device、512 KB Flash 地址范围、高密度启动文件和 Flash 算法,然后重新 Build;
- 在
Options for Target → Debug选择 ST-Link,再到Settings → Flash Download检查烧录算法; - 勾选
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 | 对应密度/启动文件 |
|---|---|---|---|
| STM32F103C8T6 | 64 KB | 20 KB | 中密度,Cube HAL 工程通常用 startup_stm32f103xb.s |
| STM32F103ZET6 | 512 KB | 64 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”:
HAL_CAN_GetRxMessage()仍然没有检查返回值。本项目的 HAL 在 FIFO0 为空时会返回HAL_ERROR,并在写入帧头和数据之前就退出。RxMessage是每轮循环中未初始化的局部变量。接收失败后仍读取StdId和DLC属于读取未确定值,即使它在某次运行中“看起来还是上一帧”,也不能依赖。- 打印数据的循环仍固定执行 8 次。如果以后接收 DLC 小于 8 的帧,有效负载只有前
DLC个字节。 - 代码无条件打印 ID/DLC/数据/清零数据,而项目的
printf又是 115200 波特率的阻塞式 UART 输出。报文速率较高时,大量日志会拖慢接收循环,甚至增加 FIFO 溢出风险。 - 接收函数第二个参数使用了
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 消息数。
如果发送端有日志而接收端没有,先不要改数据数组,按以下顺序排查:
- 按一次 ZET6 复位键,并检查
Reset and Run; - 确认两个串口助手选中了正确的 COM 口与 115200-8-N-1;
- 确认两端都为正常 CAN 模式、相同位率;本工程 APB1 的 CAN 时钟为 36 MHz,预分频为 4,一位为
1 + 9 + 8 = 18 TQ,因而目标位率为36 MHz ÷ (4 × 18) = 500 kbit/s; - 检查
CANH↔CANH、CANL↔CANL、共地、收发器供电和终端电阻; - 确认 C8T6 真正烧入了
#if 0的发送构建,ZET6 真正烧入了#if 1的接收构建; - 最后再查看 CAN 错误状态、ACK 错误、Bus-Off 和 FIFO 溢出,不要把一条无条件
Send ok!当作最终诊断结论。
1.7 结构体变量讲解
CAN_RxHeaderTypeDef RxMessage; 中,CAN_RxHeaderTypeDef 是结构体类型,RxMessage 是该类型的一个变量。可以把它理解成一张“接收帧信息表”,各成员保存同一帧的不同属性:
| 成员 | 含义 | 本实验的理解 |
|---|---|---|
StdId | 11 位标准标识符 | 在 IDE == CAN_ID_STD 时有效,本课收到 0x12 |
ExtId | 29 位扩展标识符 | 只在扩展帧时作为报文 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 符合预期之后进行。
参考资料
- 本课配套 main.c、usart.c、CubeMX 配置 与 Keil 工程:核对本课最终代码、USART1 参数、引脚与当前目标配置的混合状态。
- 本项目 HAL stm32f1xx_hal_can.h 与 stm32f1xx_hal_can.c:核对
CAN_RxHeaderTypeDef、FIFO 空时的返回路径以及成功接收时的帧头/数据填充过程。 - 课程配套 STM32F103ZET6 芯片手册:核对高密度 STM32F103xE 的 Flash/SRAM 范围、应用供电与 CAN 外设。
- ST 官方 STM32F103C8 产品页 与 STM32F103ZE 产品页:核对 C8 的 64 KB Flash/20 KB SRAM 和 ZE 的 512 KB Flash/64 KB SRAM。
- ST 官方 STM32F1 HAL CAN 驱动:对照当前 HAL 中先查 FIFO、再填充帧头与数据、最后释放 FIFO 的接收逻辑。
- TI 官方 SN65HVD23x 数据手册:核对
VP230顶面标记、3.3 V 供电与 CANH/CANL 引脚。 - Keil 官方 Download Function 与 Programming Algorithm:核对
Reset and Run的作用,以及 Flash 算法的起始地址和容量配置意义。