Can数据C8T6发送代码(2)串口二接收
231.4.5 Can数据C8T6发送代码(2)串口二接收
/* 整体目的:电脑把 8 字节仪表指令送到 C8T6 的 USART2,C8T6 再用 CAN 转发给 ZET6。 */
/* 本块取自 27C8T6_Send/Core/Src/usart.c 的用户代码区,与本节视频最终写法一致。 */
/* 设计流程:启动接收 → 收满八字节进入中断回调 → 复制数据 → 检查 AF → 转发并打印 → 再启动接收。 */
/* 下方不抄录 CubeMX 外设初始化及 HAL 库实现;已有 USART1 重定向一并保留,方便理解日志路径。 */
#include <stdio.h> /* 提供 FILE、printf 等标准输入输出声明。 */
#include "canif.h" /* 提供课程 Can_Send_Msg 的声明,使串口模块能调用 CAN 发送接口。 */
uint8_t uart_rx_data[8] = {0}; /* 八个 8 位无符号元素作为长期存在的接收缓冲;HAL 直接把收到的字节写入这里。 */
void uart2_recv_it_enable(void) /* 对外提供“启动 USART2 中断接收”的接口,不要求 main.c 直接操作数组。 */
{ /* 启动接口函数体开始;void 参数表表示不接收参数。 */
HAL_UART_Receive_IT(&huart2, uart_rx_data, 8); /* 把串口二句柄地址、缓冲区首地址和长度 8 交给 HAL;本节未检查返回状态。 */
} /* 接收被安排后立即返回,正常情况下不会在此等待八字节全部到达。 */
int fputc(int ch, FILE *f) /* 沿用上一课的 MicroLIB 字符输出接口,printf 的日志经它送往 USART1。 */
{ /* 重定向函数体开始,f 在课程代码中未使用。 */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); /* 阻塞发送字符的低位字节;0xFFFF 为超时 tick 数。 */
return ch; /* 按课堂写法返回字符,未把 HAL 发送错误转换成标准输出错误。 */
} /* 单字符输出结束,printf 继续输出后续字符。 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) /* 定义与 HAL 弱实现同名的完成回调;本节由 USART2 中断路径调用。 */
{ /* 完成回调开始,不是主循环主动轮询调用的函数。 */
if(huart == &huart2) /* 比较句柄指针,只处理通过串口二完成的这一批接收。 */
{ /* USART2 分支开始,其他串口进入回调时不执行下面操作。 */
uint8_t can_tx_data[8] = {0}; /* 建立本次回调的局部数组,用作本批 CAN 数据的副本。 */
for(int i=0; i<8; i++) /* 从下标 0 到 7,共执行八次复制;i==8 时退出。 */
can_tx_data[i] = uart_rx_data[i]; /* 把对应接收字节复制到局部数组,不能用普通 C 数组整体赋值代替。 */
if(can_tx_data[0] == 0xaf) /* 仅检查数据区首字节是否为应用头 AF;这不是完整性校验或 CAN 的帧标识符。 */
{ /* 应用头正确时执行转发,后七字节不在此逐项解析。 */
uint32_t ret = Can_Send_Msg(0x0B4, 8, can_tx_data); /* 用标准 ID 0x0B4 转发八字节;课程封装还会等待邮箱全空。 */
if(ret == 0) /* 按课程封装的零返回值决定是否打印,不等于确认仪表已完成显示。 */
{ /* 调试日志分支开始。 */
printf("can send"); /* 先打印固定前缀;原字符串后没有空格或冒号,笔记保持原样。 */
for(int x=0; x<8; x++) /* 遍历本次转发的八个数据字节,方便与串口助手发送内容对照。 */
{ /* 打印循环体开始。 */
printf("%X ", can_tx_data[x]); /* 按大写十六进制输出,再加一个空格;没有指定两位补零宽度。 */
} /* 一个字节打印结束,再输出下一个。 */
printf("\r\n"); /* 输出回车加换行,让下一批日志从新行开始。 */
} /* 返回零时的日志分支结束,非零时本节没有错误日志。 */
} /* 首字节不为 AF 时跳过 CAN 转发,但仍需执行下面的接收重启。 */
HAL_UART_Receive_IT(&huart2, uart_rx_data, 8); /* 为下一批八字节重新启动接收,目标仍是长期存在的 uart_rx_data。 */
} /* 串口二分支结束。 */
} /* 回调返回 HAL,再退出中断;局部 can_tx_data 的生命周期在此结束。 */
usart.h——本节添加到 USER CODE BEGIN Prototypes 内的声明:
void uart2_recv_it_enable(void); /* 告诉包含 usart.h 的调用文件该接口的名字、参数和返回类型。 */
main.c——本节保留/新增的初始化用户代码:
Can_Filter_Init(); /* 放在外设初始化之后,沿用上一课的滤波配置与 CAN 启动。 */
uart2_recv_it_enable(); /* 首次启动 USART2 的八字节中断接收;后续接收由回调末尾续接。 */
printf("C8T6 running\r\n"); /* 经 USART1 输出运行标志,用于辨认日志端口和程序启动状态。 */
三块代码分别放回对应文件的用户区,不是拼成一个新文件。
usart.c已由 CubeMX 包含usart.h,main.c也已有usart.h;外设初始化先完成,才执行第三块。视频开头删除了上一课while(1)中的固定发送和延时业务,本节主循环不再周期性发测试帧。配套main.c仍留有不再使用的tx_data和ret,不能误认为它们仍承担转发任务。这里仅收录本节业务及必要上下文,没有添加后续课的解析或优化代码。
一、C8T6串口二接收代码
1.1 窗口二中断接收
“窗口二”实际指 USART2(串口二)。上一课 C8T6 在主循环中按固定数值发送;这一课把数据来源改成电脑串口助手,形成下面的链路:
电脑 HEX 指令 → USB 转 TTL → C8T6 USART2 → 八字节完成回调 → CAN → ZET6 → 仪表指针/图标
中断接收启动接口安排好接收任务便返回,主循环可以继续做其他事。此后每个接收字节仍需要 CPU 在中断里处理,并不是完全不占 CPU,也不是 DMA。阻塞接收则在调用处等待达到长度、发生错误或超时。
本节先删除上一课固定发送循环中的十帧测试和两个 HAL_Delay(1000)。保留 CAN、USART1、USART2 初始化和启动日志,让新指令成为仪表状态的来源。如果仍保留固定发送,手动设置的状态会不断被旧测试数据覆盖。
1.2 找到have库
标题中的“have库”应理解为 HAL 库。视频通过 stm32f1xx_hal_uart.h 找到 HAL_UART_Receive_IT(),而非在 CubeMX 的 usart.h 中寻找 HAL 的全部接口。
| 本节参数 | 具体含义 | 初学者容易混淆的地方 |
|---|---|---|
&huart2 | USART2 句柄的地址,HAL 通过它访问配置、状态和硬件实例 | huart2 是结构体,&huart2 才是指向它的指针 |
uart_rx_data | 数组首元素地址,作为 HAL 接收写入位置 | 数组不是收到后才“返回”的;HAL 逐字节填入传进去的存储区 |
8 | 本次需要接收八个字节 | 不是八个十六进制字符,也不是每收一字节便回调一次 |
启动接口一般返回 HAL_OK、HAL_BUSY 或 HAL_ERROR。课程未接住这个状态,所以仅执行这一行不能证明接收已经成功启动。中断模式保存了缓冲区地址,接收缓冲必须一直有效;不能在启动函数里临时定义一个局部数组,函数返回后再让 HAL 往那个失效地址接收。课程使用文件范围数组,解决了存储生命周期问题。
还须具备上一课生成的 NVIC 配置和中断入口:USART2 global interrupt 已启用,stm32f1xx_it.c 的 USART2_IRQHandler() 调用 HAL_UART_IRQHandler(&huart2)。原笔记的 UART2_IRQHandler 拼写不准确,F103 这里是 USART2。
1.3 help库
“help库”仍是 HAL,不是另一套库。定位流程是:在头文件找公开接口,沿中断入口进入 HAL,再找接收完成回调;库文件用于理解调用关系,不直接改库文件来写业务。
1.3.1 接收IT函数接口
依据配套 HAL,完整过程为:
HAL_UART_Receive_IT()检查串口接收状态和参数,设置本次标准接收。- 内部启动函数保存缓冲地址、总长度和剩余计数,设置忙状态并打开接收相关中断。
- 字节到达触发 USART2 中断,HAL 读取数据寄存器,把数据写入数组,移动写指针并递减计数。
- 收满八字节后关闭这一批的接收相关中断,把接收状态恢复为 READY,然后调用完成回调。
- 用户回调处理完后再次调用接收接口,才能继续收下一批。
所以 NVIC 勾选、接收接口启动、完成回调和再次启动接收是四个不同环节。只定义回调或只勾选 NVIC 都不够。这里只是定长接收,没有靠串口空闲检测划分报文,也没有自动寻找 AF 再凑八字节。
如果只送七字节,正常情况下剩余计数不会归零,完成回调就不会发生;下一次送来的第一个字节会补进这批的第八位。若先混入多余字符,再连续送合法指令,八字节分组可能整体错位。本节没有超时清理、循环缓冲、帧头搜索或错误恢复逻辑,实验应每次准确发送八字节。串口 overrun 等错误还可能使接收提前终止,不能把异常接收也描述为永远等到八字节。
1.3.2 填充回调函数
HAL 库已有 __weak 的 HAL_UART_RxCpltCallback() 默认实现。视频把同名函数定义放进 usart.c 的最后用户区,去掉 __weak,由用户实现覆盖弱默认实现。也可以放进 main.c 的用户区,但工程只应有一份对应的强定义,不能两个文件各抄一份。
huart 是 HAL 传入的句柄指针。if(huart == &huart2) 确认当前完成的是 USART2;这不是把一个结构体与串口号 2 相比较。通过判断后,先复制八字节,再检查副本的 Byte0 是否为 0xAF,符合时调用 Can_Send_Msg(0x0B4, 8, can_tx_data)。
AF 是应用数据头,0x0B4 是 CAN 标识符,二者属于不同层次。C8T6 本节只检查首字节,没有检查功能码、取值范围、校验和或日期是否合法,因此“头对了代表数据都正确”的口述需要收窄为“通过了本节唯一的应用头检查”。真正选择指针、灯光等功能由 ZET6 解析。
最后一条接收重启放在 if(can_tx_data[0] == 0xaf) 外面、USART2 分支里面。这样一批数据头错误也会丢弃后继续接收,而不会永久停止。重启仍传 uart_rx_data,不能改成马上离开作用域的 can_tx_data。
1.3.3 定义数组
视频先在 main.c 定义接收数组,随后发现 usart.c 看不到它,于是将定义移入 usart.c 的用户区。这涉及定义与声明:一个数组应有一份实际存储定义;其他文件若直接访问,可用相应 extern 声明,不能复制定义来消除红线,否则可能发生链接重复定义。
课堂最终没有把数组写进 usart.h 的 extern 接口,而是声明 uart2_recv_it_enable()。main.c 只调用启动函数,函数内部知道用哪个缓冲区。这样减少的是跨文件直接访问数组的耦合,不是把数组变成局部变量或减少它的八字节存储。现有定义也没有 static,不能说它已具有 C 语言强制的文件私有链接属性。
回调里的 can_tx_data 才是局部数组。for 循环产生本批副本,用它做头判断、CAN 发送和日志输出,可让这三步针对同一份数据。在本节接收流程中,下一批软件接收是在这些操作之后才启动,因此不能声称 USART2 必然在这段代码运行期间自动重写原数组。高优先级代码或 RTOS 其他执行路径可能改变共享数据,但也不能把“复制到局部数组”当成通用并发保护:复制过程本身若被其他写入打断,也可能得到前后两批混合的数据;本节没有加锁或双缓冲。
局部数组能用于这次发送,是因为配套 HAL_CAN_AddTxMessage() 在调用中同步把字节写入 CAN 硬件邮箱,不把这个地址留给 DMA 后续读取。函数返回、回调结束后数组消失,不会影响已复制到硬件邮箱的那批字节;这项判断仅适用于已核对的接口,不能推广到所有异步发送 API。
1.3.4 返回值判断
ret 是课程 Can_Send_Msg() 的自定义返回值:HAL 入队失败返回 1,等待三个发送邮箱都空后返回 0。邮箱空闲不直接等同于 CAN 已成功送达,更不等同于 ZET6 已完成业务更新;本节没有检查 TXOK,也没有业务应答。这个封装的帧头还漏设 TransmitGlobalTime,详见第 230 篇,不因加日志而消失。
视频约 14:13 的编译输出是 0 Errors、2 Warnings,不是“完全没有诊断”。其中 main.c 的 printf 被隐式声明,另一个是上一课保留的 ret 未使用。实际复现时应在调用文件中提供 <stdio.h> 声明并理解未使用变量的来源;为了忠实保留课堂源码,顶部没有擅自替讲师新增该包含或删除遗留声明。
成功分支先输出 can send,再输出八个 %X ,最后 \r\n。比如指针示例可能看到:
can sendAF 1 11 3B 30 1 AF AF
这里的 1 就是十六进制 01;%X 不保证两位宽度。原笔记“百分号xx”和“每次循环结束加换行”均不准确,实际是每个字节后空格、整批输出结束后回车换行;固定前缀与首字节之间没有额外空格。日志显示的是局部副本,不是直接读取接收端仪表的显示状态。
本节回调运行在中断上下文,内部 CAN 等待和 USART1 阻塞打印都可能延长中断。115200、8N1 时八字节连续到达仅约需 8×10/115200≈0.694 ms,而一行二十多个字符的打印就需要数毫秒;本节直到打印结束才重启接收。其间下一批数据仍可能到达硬件,发生接收溢出或丢字节。“采用中断接收”不等于整段程序都无阻塞,也不保证高速连续指令可靠。 CAN 等待没有超时,UART 阻塞输出的超时又依赖 HAL tick;配套 USART2 优先级为 0、SysTick 为 15,SysTick 不能抢占该中断,异常情况下超时也可能不能按预期推进。这里解释原代码限制,后续课的改动应留到对应课收录。
二、串口助手使用
本节使用两个 USB 转 TTL 通道连接 C8T6。两窗口的位置和 COM 号只是讲师电脑的分配,辨认方法比记住“左/右窗口”更可靠。
| 通道 | C8T6 默认引脚(配套工程已核对) | 电脑端用途 |
|---|---|---|
| USART1 | PA9=TX,PA10=RX | 接收 C8T6 running 和 can send... 文本日志 |
| USART2 | PA2=TX,PA3=RX | 向 C8T6 发送八字节二进制仪表指令 |
电脑发送通道的模块 TX 接 PA3(MCU RX),模块 RX 可接 PA2;日志通道模块 RX 接 PA9(MCU TX),模块 TX 可接 PA10。各模块 GND 接板 GND,电平按板和模块配置采用兼容的 3.3 V TTL 信号。TX/RX 交叉是为了把一端输出送进另一端输入,共地提供电压比较参考。不能使用没有电平转换的 RS-232 正负电压接口直接接 MCU 脚。USB 转 TTL 是电脑 USB 与 MCU 串口之间的转换器,USB 数据线和 UART 的 TX/RX 不是同一种接口。
CAN 链路继续沿用上一课:C8T6 的 PA12/CAN_TX 和 PA11/CAN_RX 经过 CAN 收发器连接总线,另一端也须有收发器,CANH 对 CANH、CANL 对 CANL,位速率 500 kbit/s,终端电阻与参考地按已有实验接线检查。USB 转 TTL 负责串口侧,CAN 收发器负责总线侧,两个模块不能互相替代。
两个串口窗口都设置 115200、8 数据位、1 停止位、无校验、无流控。USART1 日志以 ASCII/文本显示,USART2 指令必须选择 HEX 发送。输入 AF 在 HEX 模式是一个数值为 175 的字节;在 ASCII 模式会成为字符 A、F 两个字节,分别为 41 46,会破坏定长接收。关闭自动附加回车、换行等发送附加项,确保一次恰好八字节。
视频约 14:50~15:42 因 USB HUB 接触问题,COM 端口无法打开。讲师重新插接 HUB 后端口恢复;排查顺序可为查看设备是否枚举、刷新 COM 列表、确认选中实际模块、关闭占用端口的软件、检查连接后再打开。COM1 或以前用过的 COM5 不一定是当前目标端口。
把本节工程烧入 C8T6,保留 ZET6 的上节接收固件。复位 ZET6 后图标恢复初始状态;打开两个串口再复位 C8T6,通过启动文本识别 USART1。USART2 本节没有回显实现,它负责收电脑指令,不能期待两个串口都出现同样的 MCU 日志。串口助手自己的发送记录也不等于 MCU 已经收到。
三、发送指令
视频先从思维导图用 Ctrl+A 全选、Ctrl+C 复制指令,再粘贴到发送框,最后点击发送。Ctrl+A/Ctrl+C 是编辑操作,不是协议发送指令。
| 操作/示例(HEX,共八字节) | 字节意义 | 预期仪表现象 |
|---|---|---|
AF 01 11 3B 30 01 AF AF | 01 指针,11 是 0x11=17;后三类尾部数据不参与当前指针处理 | 指针到 17% 的位置 |
AF 02 01 AF AF AF AF AF | 02 水温图标,状态 01 | 水温红色 |
AF 02 00 AF AF AF AF AF | 水温状态 00 | 水温白色 |
AF 03 01 AF AF AF AF AF | 03 安全带,状态 01 | 安全带红色 |
AF 04 02 AF AF AF AF AF | 04 灯光,状态 02 | 灯光红色 |
AF 04 01 AF AF AF AF AF | 仅把灯光状态从 02 改为 01 | 灯光黄色 |
AF 05 01 AF AF AF AF AF | 把功能码改为 05,保持状态 01 | 转向灯红色 |
原笔记的“11 对应百分号17”“27%”“242”均应按画面中的字节位置理解:0x11=1×16+1=17,是 17%;AF 04 02... 的 04 是功能码、02 是状态,绝不是一个十进制数 242。把功能码 04 改为 05 才会控制转向灯;只改状态字节不能把灯光指令变成转向灯指令。
Byte0=AF 固定,Byte1 选择功能,Byte2 提供状态/百分比;当前这些功能不使用后五字节,但本节仍完整转发八字节,不能把尾部直接省掉。指针百分比也不是已经换算好的实际发动机转速:ZET6 当前把 0~100 的百分比映射到仪表角度,17% 对应 250° 量程的 42.5°,课程 17*2500/100=425 按 LVGL 的 0.1° 单位传入,这个例子并未截成 42°。灯图标表示协议指定的状态,课程没有连接实际水温传感器或车辆灯具。
日期 06、时间 07 虽然已在思维导图中出现,本节 ZET6 还没有对应更新逻辑。C8T6 对 AF 开头的数据仍会转发,不能误写成发送端“不能接收日期数据”;准确结论是本节不以日期/时间作为已实现功能的测试项。
验证时从一条指针指令开始,然后逐个试图标:USART2 发送记录应为八字节,USART1 日志应与这八字节对应,ZET6 显示应与功能码和状态相符。若只有日志没有界面变化,检查 CAN 总线及 ZET6;若没有日志,分别检查接收未收满、AF 不匹配、发送返回非零、CAN 等待未退出、日志口选错等环节。不要只根据某一窗口判断整条链路。
四、核对依据与自查
- 课程视频/转录
231.4.5 Can数据C8T6发送代码(2)串口二接收:00:00~09:34 核对接收启动、回调、数组迁移及函数声明;09:35~14:17 核对局部副本、日志、编译警告;14:18~17:49 核对下载、串口故障和指令示范。 - 配套
源码/3.1 CAN、LVGL、IAP固件升级专题/uint3code/27C8T6_Send的Core/Src/usart.c、Core/Inc/usart.h、Core/Src/main.c、.ioc、BSP/Canif/canif.c和本地 HAL UART/CAN:核对原样业务代码、引脚、NVIC、缓冲写入、完成后关中断及发送邮箱复制行为。 - ST 官方 UART HAL 源码与CAN HAL 源码:用于对照接口语义,课程所用版本以本地配套文件为准。
学完应能说明:为何启动接口不等于完成回调、为何数组不能是启动函数的临时局部变量、为何坏头数据后仍要重启接收、为何 HEX 模式与八字节长度缺一不可,以及为何“中断接收”仍可能被回调里的阻塞业务拖慢。这些解释都对应本节真实代码,而非提前补进下一课的优化实现。