Can数据C8T6发送代码(1)
230.4.4 Can数据C8T6发送代码(1)
/* 本节目的:让 C8T6 主动发送两组固定 CAN 数据,验证 ZET6 的指针与四个状态图标接收逻辑。 */
/* 本块是 main.c 中本节编写的局部代码;声明、初始化与循环分别放回各自 USER CODE 区。 */
/* 配套 27C8T6_Send 已改成后续串口中断转发版本,下面固定发送循环依据本节视频原样收录。 */
#include "canif.h" /* 放入 main.c 的 Includes 用户区,取得 CAN 封装函数声明。 */
uint8_t tx_data[8] = {0}; /* 放入全局 PV 用户区,建立 8 字节发送数组,所有元素启动时为零。 */
uint32_t ret; /* 放入 main() 的 USER CODE BEGIN 1,接住第一条发送调用的返回值。 */
Can_Filter_Init(); /* 放在外设初始化之后的 USER CODE BEGIN 2,配置滤波器并启动 CAN。 */
printf("C8T6 running\r\n"); /* 初始化后通过 USART1 输出启动文本,\r\n 表示回车加换行。 */
while (1) /* 已有主循环框架,下面两组固定数据在循环内交替发送。 */
{ /* 每轮发送第一组、等待,再发送第二组、等待。 */
tx_data[0] = 0xAF; /* 设置应用数据头;它是 Byte0,不是 CAN 帧的标识符。 */
tx_data[1] = 0x1; /* 第一组第 1 帧:功能码 1,控制指针。 */
tx_data[2] = 0x14; /* 0x14 等于十进制 20,表示指针 20% 位置。 */
ret = Can_Send_Msg(0x0B4, 8, tx_data); /* 发送标准 ID 0x0B4、DLC=8 的帧,记录课程封装的返回值。 */
if(ret == 0) /* 只有这次调用返回零时输出提示;本节没有逐帧打印全部发送结果。 */
printf("can send ok!\r\n"); /* 打印课程成功提示,不能仅凭文字确认接收端业务一定更新成功。 */
tx_data[1] = 0x2; /* 第一组第 2 帧:功能码 2,水温报警图标。 */
tx_data[2] = 0x1; /* 状态 1 让接收端把水温图标显示为红色。 */
Can_Send_Msg(0x0B4, 8, tx_data); /* 使用同一数组发送一帧水温状态数据。 */
tx_data[1] = 0x3; /* 第一组第 3 帧:功能码 3,安全带图标。 */
tx_data[2] = 0x1; /* 状态 1 让安全带图标显示为红色。 */
Can_Send_Msg(0x0B4, 8, tx_data); /* 发送安全带状态帧;本次返回值没有保存。 */
tx_data[1] = 0x4; /* 第一组第 4 帧:功能码 4,灯光图标。 */
tx_data[2] = 0x1; /* 灯光的状态 1 表示黄色,不是红色。 */
Can_Send_Msg(0x0B4, 8, tx_data); /* 发送黄色灯光状态帧。 */
tx_data[1] = 0x5; /* 第一组第 5 帧:功能码 5,转向灯图标。 */
tx_data[2] = 0x1; /* 状态 1 使接收端把转向灯显示为红色。 */
Can_Send_Msg(0x0B4, 8, tx_data); /* 发送转向灯状态帧,第一组五帧发送结束。 */
HAL_Delay(1000); /* 主循环阻塞等待约 1000 ms,让人眼有时间观察第一组显示。 */
tx_data[1] = 0x1; /* 第二组第 1 帧:再次选择指针功能,Byte0 保留 0xAF。 */
tx_data[2] = 0x44; /* 0x44=4×16+4=68,改成指针 68% 位置。 */
Can_Send_Msg(0x0B4, 8, tx_data); /* 发送第二组指针数据,本节没有给这一帧添加日志判断。 */
tx_data[1] = 0x2; /* 第二组第 2 帧:水温图标。 */
tx_data[2] = 0x0; /* 状态 0 使水温图标变为白色。 */
Can_Send_Msg(0x0B4, 8, tx_data); /* 发送水温白色状态帧。 */
tx_data[1] = 0x3; /* 第二组第 3 帧:安全带图标。 */
tx_data[2] = 0x0; /* 状态 0 使安全带图标变为白色。 */
Can_Send_Msg(0x0B4, 8, tx_data); /* 发送安全带白色状态帧。 */
tx_data[1] = 0x4; /* 第二组第 4 帧:灯光图标。 */
tx_data[2] = 0x2; /* 状态 2 表示红色,因而灯光在黄色与红色之间切换。 */
Can_Send_Msg(0x0B4, 8, tx_data); /* 发送灯光红色状态帧。 */
tx_data[1] = 0x5; /* 第二组第 5 帧:转向灯图标。 */
tx_data[2] = 0x0; /* 状态 0 使转向灯变为白色;本节不是自动闪烁控制。 */
Can_Send_Msg(0x0B4, 8, tx_data); /* 发送转向灯白色状态帧。 */
HAL_Delay(1000); /* 再等待约 1000 ms,下一轮循环重新发送第一组。 */
} /* 主循环一轮结束;总周期约 2 s 加发送和打印所用时间。 */
canif.c——本节查看、调用的已有发送封装:
uint32_t Can_Send_Msg(uint32_t id, uint32_t len, uint8_t *buf) /* 参数分别为标准 ID、数据长度和发送数组首地址。 */
{ /* 发送封装函数开始,hcan 由 can.h 声明。 */
CAN_TxHeaderTypeDef TxMessage; /* 建立发送帧头;课程未整体初始化,遗漏字段的限制在正文说明。 */
uint32_t Tx_Mail = CAN_TX_MAILBOX0; /* 建立保存邮箱掩码的变量,HAL 会通过它返回实际选中的邮箱。 */
TxMessage.StdId = id; /* 填写 11 位标准 CAN 标识符。 */
TxMessage.IDE = CAN_ID_STD; /* 指定标准帧标识符格式,未使用扩展 ID。 */
TxMessage.DLC = len; /* 填写数据长度,本节调用时传入 8。 */
TxMessage.RTR = CAN_RTR_DATA; /* 指定数据帧,区别于请求数据的远程帧。 */
if(HAL_CAN_AddTxMessage(&hcan, &TxMessage, buf, &Tx_Mail) != HAL_OK) /* 尝试将帧头及数据写入一个空发送邮箱并提出发送请求。 */
return 1; /* 请求入队失败时返回 1,跳过后面的等待。 */
while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) != 3); /* 空循环等待三个邮箱全空;没有超时,也未检查本帧 TXOK。 */
return 0; /* 邮箱全空后返回零,这是课程封装的完成判据。 */
} /* 返回到 main.c 继续发送下一帧。 */
usart.c——本节添加的 printf 输出重定向:
#include <stdio.h> /* 放在 usart.c 的用户包含区,提供 FILE 与标准输出函数声明。 */
int fputc(int ch, FILE *f) /* 定义 MicroLIB 所用的字符输出接口;ch 是待输出字符,f 本节不使用。 */
{ /* 字符重定向函数体开始。 */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); /* 经已初始化的 USART1 阻塞发送一个字节,超时值为 65535 个 HAL tick。 */
return ch; /* 按课程写法返回字符;未根据 UART 的 HAL 状态返回输出错误。 */
} /* 输出一个字符后回到 printf,继续处理下一个字符。 */
第一块中包含原文件的多个局部位置,
while(1)应替换已有循环里的业务内容,不能嵌套再建第二个循环。源码保持本节视频写法,仅加注释。CubeMX 生成的初始化和 HAL 库实现未重复收录。配套工程中的uart2_recv_it_enable()及串口接收回调属于下一课,此处没有提前补入。
一、C8T6代码编写
1.1 上节工程烧写
先把第 229 篇完成的 26Can_Recv 接收程序下载到 ZET6 仪表开发板,再创建 C8T6 发送工程。这样接收端先准备好,后面发送端运行时能立即观察仪表变化。
下载前保存并编译对应工程,确认下载器连接到 ZET6;创建发送程序后,下载器要切换到 C8T6。视频后段转录把这两个板名颠倒了,应以工程目标芯片与屏幕演示判断,不能把 C8T6 固件烧到 ZET6。
程序烧入 Flash 后,MCU 还需要复位并运行。Keil 的下载、调试和运行是不同步骤,不能仅凭“编译无错”判断硬件已经执行新程序。
理解两块板的分工:C8T6 是生成测试指令的发送端,ZET6 是运行 LVGL 仪表的接收端。程序、下载器连接和观察对象要分别对应;C8T6 不会因为自己发出了数据就在自己的板上显示仪表。
1.2 新建C8T6工程
1.2.1 选择MCU
在 STM32CubeMX 的 MCU Selector 中搜索并选择 STM32F103C8T6(LQFP48)。这个芯片是 C8T6 发送端,不是仪表使用的 ZET6。
| 配置项 | 本节实际设置 | 作用 |
|---|---|---|
| RCC → HSE | Crystal/Ceramic Resonator | 使用开发板外部晶体;“CRIS OL”是错误转录 |
| SYS → Debug | Serial Wire | 保留 SWD 调试下载功能,PA13/PA14 分别用于 SWDIO/SWCLK |
| 系统时钟 | 72 MHz | 按板上 8 MHz HSE 与 PLL×9 配置 |
| APB1 | 36 MHz | CAN 和 USART2 所在外设时钟域 |
| CAN | Activated、Normal | 启用真实 CAN 总线收发 |
| CAN 时序 | Prescaler=4、BS1=9 TQ、BS2=8 TQ | 与 ZET6 的 500 kbit/s 配置一致 |
| USART1 | Asynchronous | 115200、8N1,用于日志 |
| USART2 | Asynchronous | 115200、8N1,为下一节接收测试指令做准备 |
CAN 位速率再次核对为 36 MHz/[4×(1+9+8)]=500 kbit/s。发送端和接收端需使用一致位速率,并满足总线时序条件。不能根据主频 72 MHz 直接套算,CAN 不使用 APB 定时器的倍频规则。
1.2.2 配置串口二中断
原笔记“窗口二”应为 USART2 串口二。在 USART2 的 NVIC Settings 中勾选 USART2 global interrupt,为下一课中断接收生成 IRQ 配置。
勾选 NVIC 只是允许 USART2 中断进入 CPU;还必须调用 HAL 接收接口启动具体接收操作,才会形成相应完成回调。本节尚未调用接收启动函数,USART2 也尚未参与 CAN 数据来源,因此打开其串口助手没有日志是正常现象。
1.2.3 配置工程路径
Project Manager 中将 Project Name 设为 27C8T6_Send,Project Location 选课程 uint3code 目录,Toolchain/IDE 选 MDK-ARM。确认新工程位于独立目录,避免覆盖第 26 个 ZET6 工程。
“unit 3 code”“27c8t六”“send OK”等转录应结合界面理解为目录名、工程名和确认操作,并非 C 接口或代码语句。
1.2.4 配置代码生成器
在 Code Generator 中勾选 Generate peripheral initialization as a pair of '.c/.h' files per peripheral,点击 Generate Code。这样 CAN、USART 等外设配置分别生成到 can.c/.h、usart.c/.h,便于本节在 usart.c 添加重定向代码。
打开新工程 MDK-ARM 目录下的 Keil .uvprojx。业务代码应放在对应 USER CODE BEGIN/END 之间,这些区域用于让 CubeMX 重新生成代码时保留用户内容。仍应保存工程副本并检查生成结果,而不能把“放在用户区”理解为所有操作都绝对不会影响代码。
1.3 C8T6通信代码编写
1.3.1 找到bsp文件夹
新建 27C8T6_Send/BSP 文件夹,从第 4 个 CAN 工程复制 Canif 模块进去。实际文件是 canif.c、canif.h,不是 OCR 里的“看if”“can if”。
这个文件夹装的是课程封装接口。发送端复用已有 Can_Send_Msg(),无需再次从 CAN 寄存器或 HAL 库底层开始编写。
1.3.2 添加bsp组
在 Keil 的 Manage Project Items 窗口中新建 BSP Group。视频使用工具栏三个小方框图标进入;应认准管理工程组/文件功能,而不是只记图标形状。
Keil 工程树中的 Group 是源码组织分类,硬盘上的 BSP 是实际目录。两者并不自动互相创建或同步,因此创建组后仍要添加文件。
1.3.3 添加c文件
在 BSP 组内使用 Add Existing Files 添加当前第 27 个工程副本中的 BSP/Canif/canif.c。添加后确认文件路径,不能仍引用第 4 个旧工程的实现。
1.3.4 添加路径
在 Options for Target → C/C++ → Include Paths 中加入 BSP/Canif。.c 加入工程负责让实现被编译,Include Paths 负责让头文件被找到,#include "canif.h" 负责把声明带入调用文件。这三项缺一都可能导致构建问题。
1.3.5 初始化代码分析
在 main.c 顶部用户包含区加入 canif.h,在所有外设初始化后的 USER CODE BEGIN 2 调用 Can_Filter_Init()。
该接口名字虽然只提到滤波器,实际上还调用了 HAL_CAN_Start(&hcan)。因此发送端也复用它来完成 CAN 启动;调用它本身不会发送一帧数据,后续 Can_Send_Msg() 才是发送请求。
顺序应为 MX_CAN_Init() 建立外设配置 → Can_Filter_Init() 配置滤波器并启动 CAN → 循环内调用发送接口。遇到红线先检查头文件、拼写和路径,再看编译器实际诊断。
1.3.6 发送消息函数分析
Can_Send_Msg(0x0B4, 8, tx_data) 的参数分别是:
| 参数 | 意义 |
|---|---|
0x0B4 | 标准 CAN 标识符,应与 ZET6 软件筛选值一致 |
8 | 数据区长度 DLC;每次仍发送八个字节 |
tx_data | 待发送数组的首地址,HAL 读取数组中的字节 |
发送封装设置标准 ID、标准帧格式、DLC 和数据帧类型,交给 HAL 选择空发送邮箱。本外设有三个发送邮箱;Tx_Mail 初始赋为 MAILBOX0,不代表 HAL 只能使用邮箱 0,它是输出参数,HAL 会写回实际邮箱掩码。
原封装存在几个需要理解的限制,笔记保留源码而不擅改:
HAL_CAN_AddTxMessage()成功表示发送请求已加入邮箱,不是远端仪表已经完成显示。- 接着等待三个邮箱全空,没有超时,可能占住主循环;也没有检查本帧的
TXOK等实际发送结果。 - 邮箱空闲不能单独证明发送成功。尤其课程禁用了 AutoRetransmission,一次失败的请求也可能结束并释放邮箱。
TxMessage未整体初始化,也未设置TransmitGlobalTime,而配套 HAL 会读取该字段。这是旧封装实际遗漏,不能描述成已经完整初始化的帧头。id、len的合法性也未由这个封装显式校验,本实验固定合法标准 ID 和长度 8。
因此返回值为 0 是课程封装的判据,其含义比日志里的“send ok”更弱。可靠确认还需结合 CAN 状态与接收端解析/显示结果。
1.3.7 发送数据分析
发送数组有 8 字节,但前五类指令只用前三字节。Byte0=AF,Byte1 为功能码,Byte2 为状态或百分比,Byte3~Byte7 从初始化后保持为零。本节填充零,与第 227 篇串口助手填 AF 的示例都能用于当前接收端,因为这些指令忽略尾部填充。
讲师说“先发这三个字节”应理解为先填写三个有业务意义的字节;实际传给接口的 DLC 仍是 8,不是只发送三字节。
| 项目 | 第一组 | 约等待 1 s 后第二组 | ZET6 现象 |
|---|---|---|---|
| 指针 | AF 01 14 00 00 00 00 00 | AF 01 44 00 00 00 00 00 | 20% 与 68% 交替,即约 50° 与 170° |
| 水温 | AF 02 01 00 00 00 00 00 | AF 02 00 00 00 00 00 00 | 红色与白色交替 |
| 安全带 | AF 03 01 00 00 00 00 00 | AF 03 00 00 00 00 00 00 | 红色与白色交替 |
| 灯光 | AF 04 01 00 00 00 00 00 | AF 04 02 00 00 00 00 00 | 黄色与红色交替 |
| 转向灯 | AF 05 01 00 00 00 00 00 | AF 05 00 00 00 00 00 00 | 红色与白色交替 |
这里 0x14=20,0x44=68。第一组“图标都改红”的口述不适用于灯光,它的状态 1 是黄色,随后讲师已按协议把第二组改为 2,使黄色/红色交替。
五帧按顺序发送,每组发完阻塞延时 1000 ms,再切换下一组;完整循环约 2 s,另加 CAN 发送与日志耗时。HAL_Delay() 只阻塞当前主循环,已开启的中断仍可运行;本节没有 UART 接收业务,所以暂用这种易观察的实验节奏。
1.3.8 串口打印分析
本节在烧写前加入串口日志,使用 USART1。原笔记的“充电线”“充电方式”应为重定向方式:将 printf 的字符输出送到指定 UART。
USART1 发出的字符经 PA9 → USB 转 TTL 的 RX → USB → 电脑串口助手。TTL 模块 GND 与板 GND 相连,模块 TX 则接 PA10;两端参数统一为 115200、8N1、无流控。USB 转 TTL 的 USB 插头是电脑接口,不能把 MCU 的 UART 脚直接当成 USB 数据脚。
CAN 接线仍为两块板各自 PA12→收发器 TXD、PA11←RXD,CANH 对 CANH、CANL 对 CANL,两端终端电阻与参考地按第 227 篇检查。两块板要分别正常供电;不要通过不明电源接法并联多个 USB 5V 输出。
PA12/PA11 是 MCU 的逻辑收发信号,CANH/CANL 是收发器转换后的差分总线,不能把两组引脚直接混接。发送时 MCU 交给收发器的是逻辑电平,收发器在总线两线上产生相应电压差,另一端再把电压差转换成接收逻辑电平。终端电阻放在总线两端,用来匹配传输线路、减轻信号反射;每块板的电源地还要有适当参考连接,不能把 CANL 当作 GND。课堂实验的具体模块供电和电阻开关应按所用模块原理图及第 227 篇连接执行。
二、总结
2.1 代码编写步骤
课堂采用的重定向步骤为:
- Keil
Options for Target → Target中勾选 Use MicroLIB,不是“make lab”。视频使用 Arm Compiler 5 环境,应与课程工具链匹配。 - 在
usart.c的用户区包含<stdio.h>,获得FILE等声明。 - 在
usart.c最后用户代码区定义fputc(),把单字符发送到huart1。
MicroLIB 是面向小型嵌入式程序的 C 运行库选项;课程利用其低层字符输出接口完成串口输出。单独勾选它不会自动把 printf 连接到 USART1,实际连接由 fputc() 的 UART 调用建立。
2.2 串口重定向与打印测试
printf("C8T6 running\r\n"); 依次输出文本字符。\r 是回车,\n 是换行,组合有助于串口终端显示新的一行。
fputc(int ch, FILE *f) 每次用 HAL_UART_Transmit() 发送一个字节。C8T6 使用小端布局,取 ch 的地址按字节读能得到低位字符。这里的 f 参数不参与区分设备,所有字符都走 USART1。
发送方式为阻塞式,UART 以 115200、8N1 发送一个字节约需 10/115200≈86.8 μs,长日志会消耗相应时间。0xFFFF 是超时参数而非无穷等待;在默认 1 ms tick 下约为 65.535 s。课程代码也没有检查 UART 输出状态,返回 ch 不等于已经处理好所有输出错误。
还有一个文件范围问题:在 usart.c 包含 <stdio.h> 不会自动使 main.c 获得 printf 声明。视频的 main.c 未展示补入该头文件的步骤,顶部源码因此未擅自添加;若复现时出现函数未声明诊断,应在实际调用 printf 的源文件中提供正确声明,不能靠隐式声明蒙混过去。
启动文本出现说明 MCU 至少运行到了这个打印位置且日志路径可用;不能只据此判断 CAN 接收端已经正常。
2.3 例题1: 判断CAN消息发送状态
课堂用 ret 接收第一组指针帧的函数返回值,判断 ret==0 后打印 can send ok!。其余九次发送仍直接调用,没有逐次保存或检查返回值。
ret 是自定义封装返回值,不是原封不动的 HAL_StatusTypeDef。封装在 HAL 入队失败时返回 1,在三个邮箱都空后返回 0。本节日志只是辅助定位:有提示时说明程序越过第一条发送等待;若没有提示,可以检查是否入队失败、等待未退出、串口选错或打印路径不通。
要确认业务链路,应同时观察 ZET6 指针和图标与两组测试数据是否一致。CAN ACK 是总线级确认,不代表接收端已按 0xAF 和功能码解析并更新 LVGL。
2.4 代码测试与现象观察
新工程需要设置正确下载器和下载后运行方式。视频在 Flash Download 中勾选 Reset and Run,其作用是下载与校验后复位并运行,不是改变 MCU 正常上电启动机制。
原笔记“取消音频相关勾选”是误识别。视频操作的是下载器设置 Pack 页的 Enable 选项(约 13:40),与音频无关;这是授课环境中的调试配置处理,不能推广成所有 Keil 工程一律关闭 Pack 支持。
确认目标为 C8T6、下载完成且程序开始运行后,观察两组状态大约每秒交替。若无变化,分段检查:
| 检查位置 | 可用证据 |
|---|---|
| C8T6 是否运行 | 复位后 USART1 出现 C8T6 running |
| 固定发送是否继续 | 约每轮出现第一帧的 can send ok!,结合发送封装限制判断 |
| CAN 物理链路 | 两板位速率相同、收发器供电、CANH/CANL、参考地、终端电阻 |
| ZET6 是否运行正确固件 | 已下载第 229 篇接收程序,LVGL 仪表界面正常 |
| 应用协议是否一致 | ID=0x0B4、Byte0=AF、Byte1=01~05、Byte2 对应合法状态 |
不能因为一个 log 看似成功就跳过后面链路;也不能把“没有串口打印”直接归咎于 CAN。
2.5 例题2: 串口调试助手的使用
打开 USART1 所接模块对应的 COM 口,按文本显示接收数据,波特率设为 115200、8N1。能看到 can send ok!;按 C8T6 复位键后,再出现 C8T6 running。
另一个窗口对应 USART2,本节没有发送日志、也没有启动接收,空白是预期现象。COM 号由电脑分配,不能照搬视频端口号;可通过插拔模块和复位日志确定窗口对应关系。
本节完成“C8T6 固定 CAN 发送 → ZET6 协议解析 → 五项界面更新”验证。下一节再让 C8T6 从 USART2 接收电脑指令并转发,固定测试循环会因此被替换;当前源码与配套工程最终版本有差异正是课程连续演进的结果。
三、资料依据
- 课程视频与转录,重点核对约 06:00~09:28 的固定发送循环、09:39~12:58 的串口重定向/日志、13:30~14:24 的下载设置与现象。
- 本轮重新核对视频保存的固定发送第一组、第二组与 Pack 下载设置画面,保留课程中灯光状态 1/2 的黄色/红色含义;未用后续课的串口回调覆盖本课循环。
- 配套
27C8T6_Send.ioc、Core/Src/usart.c与BSP/Canif/canif.c:核对 MCU、时钟、引脚、UART 参数及发送封装;固定发送循环以视频为准。 - ST 官方 STM32F1 CAN HAL:核对发送邮箱选择、空闲查询、帧头字段和入队语义。
- Keil:MicroLIB及MDK5 入门手册:核对 MicroLIB 的用途与低层输出重定向关系。
- Keil:Download Function:核对 Reset and Run 是下载后复位运行设置。
- ST:STM32F103C8:核对芯片 CPU、Flash、外设与供电类别。