Can数据C8T6串口代码优化
236.4.10 Can数据C8T6串口代码优化
/* 以下是本节 usart.c 的业务用户区;旧版整段注释回调不再重复收录,见 231。 */
#include <stdio.h> /* 引入 printf 和 FILE 声明,用串口一打印调试日志。 */
#include "canif.h" /* 引入课程 CAN 发送业务接口;usart.c 原有 usart.h 包含关系保持不变。 */
uint8_t uart_rx_data; /* 把旧的 8 字节数组改为单字节接收变量;HAL 每次把一个新字节写到这个地址。 */
static UART_RX_typeDef struct_uart2_rx_data; /* 建立仅本源文件可直接访问的持续接收缓存,静态存储使数组和计数在启动时清零。 */
void uart2_recv_it_enable(void) /* 定义已有的串口二接收启动函数,本课把单次接收长度改为 1。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
HAL_UART_Receive_IT(&huart2, &uart_rx_data, 1); /* 向 HAL 提交串口二的一字节中断接收;回调末尾还要重新提交下一次。 */
} /* 结束当前作用域,回到外层后续处理。 */
int fputc(int ch, FILE *f) /* 沿用课堂 printf 字符输出重定向;ch 是待输出字符,f 参数未在函数体使用。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); /* 沿用阻塞发送一个字符到串口一,超时参数原样保留。 */
return ch; /* 把原字符返回给标准输出;本课没有检查上述 HAL 发送是否失败。 */
} /* 结束当前作用域,回到外层后续处理。 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) /* 定义接收完成回调;此时一个字节已接收完,当前处于 UART 中断调用链。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
if(huart ->Instance == USART2) /* 只处理串口二;箭头读取句柄所指结构体内的硬件实例字段。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
struct_uart2_rx_data.rx_buf[struct_uart2_rx_data.rx_cnt] = uart_rx_data; /* 用当前计数作下标,将刚收到的字节保存到组帧数组。 */
struct_uart2_rx_data.rx_cnt ++; /* 保存后计数加一;现在计数表示已保存多少字节,不是最后一个有效下标。 */
if(struct_uart2_rx_data.rx_buf[0] == 0xAF) /* 检查本批缓存第一个字节是否为 AF 帧头;不是每遇到 AF 都立即重启组帧。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
if(struct_uart2_rx_data.rx_cnt >= 9) /* 达到 9 字节时认定当前包过长;正常最大串口帧是 8 字节。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
struct_uart2_rx_data.rx_cnt = 0; /* 把有效计数归零,下次从下标 0 保存;没有清空数组或把当前字节重新当帧头。 */
printf("data len error\r\n"); /* 串口一输出长度错误日志,便于观察失败分支。 */
} /* 结束当前作用域,回到外层后续处理。 */
else /* 与对应上方 if 配对,在该条件不满足时走这里;具体失败原因见下方处理。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
if(struct_uart2_rx_data.rx_cnt > 2) /* 计数必须大于 2,实际从第三字节到达后才读取并检查长度。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
uint8_t data_len = struct_uart2_rx_data.rx_buf[1]; /* 取数组下标 1 的长度字段,即帧头后第二个字节。 */
if((data_len > 1) && (data_len < 6)) /* 允许长度 2、3、4、5;课堂协议使用 2、4、5,但这条条件也放行 3。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
if(struct_uart2_rx_data.rx_cnt >= (data_len + 3)) /* 比较总已接收字节数与长度加 3,额外 3 字节为帧头、长度字段和帧尾。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
if(struct_uart2_rx_data.rx_buf[data_len + 2] == 0xFA) /* 按长度计算尾字节的零基下标,必须等于 FA 才转发;它不是 CRC 校验。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
uint8_t can_tx_data[8] = {0}; /* 在完整有效串口帧分支创建 8 字节 CAN 缓冲,全部先清零。 */
can_tx_data[0] = 0xAF; /* CAN 数据第一字节仍沿用 AF,不将串口长度字段放在这里。 */
printf("uart recv:"); /* 打印原串口帧日志的提示文字,输出走串口一。 */
printf("%X ",struct_uart2_rx_data.rx_buf[0]); /* 以大写十六进制打印帧头,%X 不保证补齐两位。 */
printf("%X ",struct_uart2_rx_data.rx_buf[1]); /* 打印串口长度字段,仅供日志观察,不复制到 CAN 数据中。 */
for(int i=0; i<data_len; i++) /* 遍历 data_len 个业务字节,包含类型和它后面的业务字段。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
can_tx_data[i + 1] = struct_uart2_rx_data.rx_buf[i + 2]; /* 串口下标 2 起复制到 CAN 下标 1 起,移除长度字节,帧尾也不复制。 */
printf("%X ",struct_uart2_rx_data.rx_buf[i + 2]); /* 打印当前复制的业务字节,使串口原帧日志完整。 */
} /* 结束当前作用域,回到外层后续处理。 */
printf("%X ",struct_uart2_rx_data.rx_buf[data_len + 2]); /* 打印串口 FA 帧尾;CAN 缓冲中不含这一串口帧尾。 */
printf("\r\n"); /* 输出回车换行,结束当前串口帧日志。 */
uint32_t ret = Can_Send_Msg(0x0B4, 8,can_tx_data); /* 以原 CAN 标准 ID 0x0B4、固定 DLC 8 发送重组的数据,返回状态存入 ret。 */
if(ret == 0) /* 课程发送封装返回 0 时打印 CAN 数据;此返回值不证明仪表盘业务已处理。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
printf("can send:"); /* 打印转换后的 CAN 数据提示,与 uart recv 日志分开核对。 */
for(int x=0; x<8; x++) /* 依次遍历全部 8 字节,含清零后未被业务覆盖的填充字节。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
printf("%X ",can_tx_data[x]); /* 以十六进制打印这一字节 CAN 数据,零值打印为 0。 */
} /* 结束当前作用域,回到外层后续处理。 */
printf("\r\n\r\n"); /* 连续两次回车换行,用空行隔开调试数据组。 */
} /* 结束当前作用域,回到外层后续处理。 */
struct_uart2_rx_data.rx_cnt = 0; /* 把有效计数归零,下次从下标 0 保存;没有清空数组或把当前字节重新当帧头。 */
} /* 结束当前作用域,回到外层后续处理。 */
else /* 与对应上方 if 配对,在该条件不满足时走这里;具体失败原因见下方处理。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
struct_uart2_rx_data.rx_cnt = 0; /* 把有效计数归零,下次从下标 0 保存;没有清空数组或把当前字节重新当帧头。 */
printf("data tail error\r\n"); /* 输出帧尾错误日志;本批数据不会经此分支转发。 */
} /* 结束当前作用域,回到外层后续处理。 */
} /* 结束当前作用域,回到外层后续处理。 */
} /* 结束当前作用域,回到外层后续处理。 */
else /* 与对应上方 if 配对,在该条件不满足时走这里;具体失败原因见下方处理。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
struct_uart2_rx_data.rx_cnt = 0; /* 把有效计数归零,下次从下标 0 保存;没有清空数组或把当前字节重新当帧头。 */
printf("data len error\r\n"); /* 串口一输出长度错误日志,便于观察失败分支。 */
} /* 结束当前作用域,回到外层后续处理。 */
} /* 结束当前作用域,回到外层后续处理。 */
} /* 结束当前作用域,回到外层后续处理。 */
} /* 结束当前作用域,回到外层后续处理。 */
else /* 与对应上方 if 配对,在该条件不满足时走这里;具体失败原因见下方处理。 */
{ /* 开始当前函数、条件分支或循环的作用域。 */
struct_uart2_rx_data.rx_cnt = 0; /* 把有效计数归零,下次从下标 0 保存;没有清空数组或把当前字节重新当帧头。 */
} /* 结束当前作用域,回到外层后续处理。 */
HAL_UART_Receive_IT(&huart2, &uart_rx_data, 1); /* 向 HAL 提交串口二的一字节中断接收;回调末尾还要重新提交下一次。 */
} /* 结束当前作用域,回到外层后续处理。 */
} /* 结束当前作用域,回到外层后续处理。 */
**本节源码范围:**代码来自配套 29C8T6_Send_OK/Core/Src/usart.c 的业务用户区。教师直接展示已经改好的 29 工程并讲解完整回调,不是下一节才完成,所以本节收录单字节变量、接收缓存实例、接收启动、已有 fputc 和整个新回调。保留原计数条件、协议转换、日志和发送方式,仅调整排版、替换逐行注释,不补超时、CRC、DMA、错误回调或更强的同步逻辑。已注释的旧八字节回调见 231,不重复粘贴。
**Core/Inc/usart.h:本节新增的缓存类型。**此类型必须先于 usart.c 中的缓存实例声明可见;它位于已有 usart.h 的用户包含区,头文件原有包含和保护结构不需重写。
typedef struct{ /* 定义接收缓存结构体的类型,类型声明本身不创建接收对象。 */
uint8_t rx_buf[10]; /* 提供 10 个单字节元素,合法下标为 0~9,存放整个串口应用帧。 */
uint8_t rx_cnt; /* 保存当前帧已接收字节数,用作下一次写入下标。 */
}UART_RX_typeDef; /* 把该结构体命名为 UART_RX_typeDef,供 usart.c 声明缓存实例。 */
一、C8T6串口代码优化
1.1 任务资源
前面 233~235 已整理 ZET6 仪表盘的 CAN 接收和显示,本课回到 STM32F103C8T6 发送端。原笔记的“z1t6”“看线”“收音器”分别应核对为 ZET6、CAN 线和烧录器;教师先打开旧 27C8T6_Send 复现问题,再用已修改的 29C8T6_Send_OK 讲解和调试。“27/29”是配套工程目录编号,不是要优化 27 个工程。原转录约 00:51 先说烧到 ZET6,随后立即更正为 C8T6。
硬件链路与 231 相同:电脑串口助手 → USB 转串口模块 → C8T6 USART2 → C8T6 CAN 控制器及 CAN 收发器 → 总线 → ZET6 CAN 接收端 → 屏幕。C8T6 的 USART1 是调试日志端,USART2 是电脑发命令的接收端;两路串口都连到电脑,烧录器另用于下载 C8T6 程序,ZET6 屏幕保持连接。
配套 29 工程 usart.c 核实两路串口均为 115200、8 数据位、1 停止位、无校验、无硬件流控。GPIO 配置对应 PA9/PA10 为 USART1 TX/RX、PA2/PA3 为 USART2 TX/RX;连接时模块 TX 接 MCU RX,模块 RX 接 MCU TX,并有共同参考地。PA11/PA12 是 CAN RX/TX 的逻辑引脚,不能直接把它们当总线 CANH/CANL;差分总线侧仍由前几课的 CAN 收发器连接。此处沿用课程既有接线,不臆测视频未标明的转接模块供电或终端电阻位置。
本课配套 .ioc 的 CAN 参数仍是预分频 4、BS1 9 TQ、BS2 8 TQ,配置显示 500000 bit/s;UART 改接收方式没有改变两块 MCU 间 CAN 位速率、ID 或仪表盘解析协议。
1.2 之前代码存在的问题
旧版 HAL_UART_Receive_IT(&huart2,uart_rx_data,8) 请求凑满 8 字节后调用接收完成回调。正常发送 AF 06 14 19 07 14 AF AF 时,C8T6 将同样的 8 字节经 CAN 发出,ZET6 显示 2025 年 7 月 20 日;把月份 07 改成 08,显示变为 8 月 20 日,说明正常对齐时链路可用。
日期问题发生在 ZET6 接收工程,不能按原笔记写成“修改 C8T6 的日期参数”。STM32F1 这一 RTC 外设核心是计数器,配套 HAL 软件维护日期,接收工程没有实现完整的日期跨复位恢复。复位后日期不正确时,电脑发送日期包,经 C8T6 转 CAN 再让 ZET6 设置 RTC。不是所有 STM32F103 RTC 状态都必然在普通复位后丢失:保持条件还涉及备份域、供电和软件初始化,详见 232 核对的本地 rtc.c 及 HAL 日期实现。
课堂约 02:32 单独发送一个 AF,它先被旧版接收请求存入缓冲。随后发送完整 8 字节日期指令,HAL 把这个旧 AF 加上新指令前 7 字节凑成一组,剩下的新指令最后一字节又进入下一组。屏幕仍停在 8 月 20 日,并不意味着 CPU 一定死循环或 RTC 停止走时;实际看到的是 应用指令边界持续错位、正确业务没有生效。
1.3 问题优化
UART 把字符组成连续字节流,串口软件一次“发送”的界限不会天然传成 MCU 应用层的一包;HAL 的长度 8 只知道数到八,不知道这八个字节是否是一条正确命令。用户少发、多发一个字节,或数据在链路中发生异常,都会暴露旧版缺少重新定位包边界的问题。课堂是主动发错数据复现,并没有测量或证实实际环境中的电磁干扰来源。
教师约 04:19 复位 C8T6 后重发,恢复日期更新。这说明重置接收状态可以暂时重新对齐,但产品不能要求每次输入异常都人工复位。上位机、C8T6 桥接端和 ZET6 显示端可能由不同人员维护;边界错位会表现为屏幕不变,排查需要核对每一段的原始数据,不能看到显示错误就只修改 UI。
本课增加帧头、长度、帧尾,按字节接收并由应用代码检查格式,比旧版固定分组更容易丢弃错误候选包。不过“HAL 不再保存错误数据”“单字节接收就保证恢复”是口头简化:HAL 仍会保存刚收到的字节,应用程序决定哪些字节组成一包、什么时候丢弃和重新开始。
1.3.1 优化思路
从一次接收 8 字节改为一次接收 1 字节,完成回调的频率也从“每八字节一次”变为“每字节一次”。每次回调保存当前字节,更新计数,依据候选帧头、长度及期望尾部位置判断;回调最后再次调用 HAL_UART_Receive_IT,继续接收下一字节。
这里修改的是 usart.c,不是原笔记说的“RTOS 的 c 文件”;本课工程没有因此新增 RTOS。库函数内部字节接收和完成回调的关系要分清:长度 8 时,不是前七个字节都没有 UART 硬件中断,而是 HAL 内部逐次处理收到的字节,凑满八个才调用用户完成回调;长度 1 时,一个字节就完成这次请求。ST 官方 STM32F1 HAL UART 源码
固定长度接收 API 本身没有错,也不是所有使用场景都应该禁用;旧程序的缺陷是把固定取八字节当成已经识别出协议包,且没有处理错位。逐字节方式让应用能更早检查边界,但同时增加中断处理次数和重新使能次数,是否适合其他应用需要结合实际吞吐量。
1.3.1.1 例题: 错误数据接收处理
先假设旧缓存计数为零,发送一个孤立 AF,再发正确日期帧:
| 到达的数据 | 旧版接收状态/结果 |
|---|---|
孤立 AF | 缓冲中已有 1 字节,还差 7 字节完成本次 8 字节请求 |
正确帧的前七字节 AF 06 14 19 07 14 AF | 拼成 AF AF 06 14 19 07 14 AF,这组虽然首字节是 AF,第二字节却变成 AF,不是有效业务类型 |
正确帧最后一个 AF | 若被下一次接收请求收到,就成为下一组的第一个字节 |
| 之后重复正确的八字节命令 | 若持续未丢字节,错误偏移会一直延续,屏幕不按预期更新 |
原笔记把原因说成“未清除异常 AF 包”不够准确:这个单字节根本还没有组成应用包,HAL 是按请求保留一个未完成的数据片段。复位清零软件接收状态后,才重新从完整帧开始计数。
新协议减少串口填充字节:原指针、水温等旧 CAN 数据实际只用类型和一个状态/百分比,后面的 AF 填充没有参与解析;新的串口短包只发有用内容,桥接代码再重组为固定 8 字节 CAN 帧。删除冗余串口填充不等于可以删除所有 AF,也不等于 CAN 的 DLC 改成可变长度。
1.3.1.2 新指令格式分析
原笔记“针头/针尾”应理解为帧头/帧尾。新 串口应用帧 格式为:AF | L | L 个业务字节 | FA。帧头是 AF,尾是 FA,不能颠倒。L 只统计业务区,其中第一个业务字节是 类型,不包含帧头、长度字节或帧尾。
[ N_{\text{串口总字节}}=L+3,\qquad I_{\text{帧尾}}=L+2 ]
总字节数是长度,帧尾位置是数组从零开始的下标,两者差 1。类型的位置从旧 CAN 数据的下标 1 移到了新串口帧的下标 2。
| 业务 | 新串口帧(HEX) | L / 总长 | 解释 |
|---|---|---|---|
| 指针 | AF 02 01 11 FA | 2 / 5 | 类型 01、百分比 0x11=17,不是 11% |
| 水温 | AF 02 02 00 FA / AF 02 02 01 FA | 2 / 5 | 类型 02、状态 0 白色/1 红色 |
| 安全带 | AF 02 03 00 FA / AF 02 03 01 FA | 2 / 5 | 类型 03、状态 0 白色/1 红色 |
| 灯光 | AF 02 04 00 FA / AF 02 04 01 FA / AF 02 04 02 FA | 2 / 5 | 类型 04、状态 0 白色/1 黄色/2 红色 |
| 转向灯 | AF 02 05 00 FA / AF 02 05 01 FA | 2 / 5 | 类型 05、状态 0 白色/1 红色 |
| 日期 | AF 05 06 14 19 07 14 FA | 5 / 8 | 类型 06,加世纪、年、月、日四个字节 |
| 时间 | AF 04 07 10 08 21 FA | 4 / 7 | 类型 07,加时、分、秒三个字节,16:08:33 |
日期业务区 06 14 19 07 14 一共五字节:06 是日期类型,14=20、19=25,表示世纪 20、两位年份 25;并非将连续两字节 0x1419 直接转成十进制 2025。ZET6 现有程序仍只用年/月/日,显示年份的 20 前缀仍固定,不应据此声称世纪字段已经全面支持。
收到预期位置的 FA 只证明尾标记符合格式,不能证明中间数据都正确。长度字段与类型也可能不相配,本课代码并未逐个类型检查准确的 L。有效业务字段中即使出现 AF 或 FA,程序也按长度确定它是否为尾部;不是在整个缓冲里搜索第一个 FA 就结束。
1.3.1.3 代码具体修改
1.3.1.3.1 更改接收一个字节的变量
将原 uint8_t uart_rx_data[8] 改为 uint8_t uart_rx_data,作为 HAL 单字节接收地址。但组帧仍需一个数组,因此另定义 static UART_RX_typeDef struct_uart2_rx_data;不能理解为“全部接收缓冲都只剩一个字节”。static 文件级对象具有持续存储期,回调返回后已经保存的字节不会丢失,也限制了这个名字在本源文件外的直接可见性。
**1.3.1.3.1.1 代码修改步骤:**先在 usart.h 加上缓存类型,再在 usart.c 使用它。教师通过 Go To 跳到头文件核对类型:rx_buf[10] 存整个候选帧,rx_cnt 存已收字节数。类型名 UART_RX_typeDef 与实例名 struct_uart2_rx_data 是两件事,声明一个类型不会自动创建一份存储。
1.3.1.3.2 修改串口二中断接收函数
初始化接收和回调末尾均改成 HAL_UART_Receive_IT(&huart2,&uart_rx_data,1),这里原数组名曾能自动转成地址,新变量是单个 uint8_t,所以必须使用 &uart_rx_data。长度 1 指这一轮接收的字节数量,不是波特率、CAN 数据长度或缓存数组容量。
该函数通常在主初始化后调用一次,之后每个完成回调重新开启下一次接收;本课没有改 main.c 的启动调用,也没有把已有 uart2_recv_it_enable() 改名。若忘记回调末尾重新提交,第一次字节完成后接收不会按这段代码持续运行。源码没有检查提交函数的 HAL 返回值,笔记不擅自加入重试。
1.3.1.3.3 修改串口callback函数
按原回调的执行顺序逐层理解:
huart->Instance == USART2限制只处理串口二。将新字节写入rx_buf[rx_cnt]后执行rx_cnt++,因此写入第一个字节后计数为 1,有效下标是 0。- 如果缓存下标 0 不是
AF,立即将计数归零。下一字节覆盖下标 0,等待可能的帧头;此失败分支没有打印,所以噪声字节可能看不到日志。 - 如果帧头正确且计数达到 9,归零并打印
data len error。正常协议最大 L=5,总包最多 8 字节,9 字节已经超限。 - 未超限时,必须
rx_cnt > 2才读长度。视频口头说“第二个字节也收到了”,代码实际等到 第三个字节 到达;计数等于 2 时暂不校验。保留课堂条件,不悄悄改成>=2。 (data_len>1)&&(data_len<6)判断的是 2~5,数学上也包括 3;课件使用 2、4、5 三种长度,代码却不是精确枚举这三种。rx_cnt >= data_len+3表示整包已到,再检查rx_buf[data_len+2]==0xFA。对于 L=2,需要五字节,尾在下标 4;对于日期 L=5,需要八字节,尾在下标 7。- 尾正确才打印
uart recv:、复制业务区并发 CAN。尾错误则归零并打印data tail error;长度非法也归零打印长度错误。未达到整包长度则继续保留计数,等待下一字节。 - 完整合法包处理完,将计数归零,即使 CAN 发送返回失败也没有保留该包重发;最后再提交下一字节接收。
计数归零是“此前字节不再作为有效候选帧”,不是将数组十个元素逐个清零。下一包写入会覆盖旧内容;本课在满足对应长度后才访问尾部,不能用旧数组内容推断已经接收了新帧。
当前逻辑先写数组,再检查计数上限。根据初值 0 及所有路径,正常进入回调时计数最多 8,最多写下标 8 后加到 9 就被重置,处于 10 元素数组范围内;不能把它误解成“任意损坏的计数也保证安全”,因为没有写入前的通用边界检查。课程原样保留这个顺序。
日期接收过程实例:
| 新到字节 | 保存下标 | 写后计数 | 本轮处理 |
|---|---|---|---|
| AF | 0 | 1 | 头正确,尚未进入长度检查 |
| 05 | 1 | 2 | 长度字段已在缓冲,但原条件 >2 暂不检查 |
| 06 | 2 | 3 | 读出 L=5,合法;总长不足 8 |
| 14 | 3 | 4 | 继续等待 |
| 19 | 4 | 5 | 继续等待 |
| 07 | 5 | 6 | 继续等待 |
| 14 | 6 | 7 | 继续等待 |
| FA | 7 | 8 | 足够 L+3,检查下标 L+2=7 的 FA,打印、转换、发送、归零 |
串口帧到 CAN 帧的转换:can_tx_data[8]={0},先填 can_tx_data[0]=AF,然后用 can_tx_data[i+1]=rx_buf[i+2] 复制 L 个业务字节。串口长度字段被跳过、FA 帧尾不复制,剩余位置保持 00。所以:
| 输入串口帧 | 输出 CAN 的八字节数据 |
|---|---|
AF 02 01 11 FA | AF 01 11 00 00 00 00 00 |
AF 05 06 14 19 07 14 FA | AF 06 14 19 07 14 00 00 |
AF 04 07 10 08 21 FA | AF 07 10 08 21 00 00 00 |
CAN 调用仍为 Can_Send_Msg(0x0B4,8,can_tx_data)。**串口应用帧可变长,CAN 数据仍固定八字节。**ZET6 不需要改成按新串口帧解析,否则会误将 L 当成类型。旧版填充为 AF,新版填充为 00 能继续工作,是因为相关尾部填充不参与现有业务解析。
printf 的 %X 会把 05 打印为 5、00 打印为 0,这只是日志宽度,不是实际线上少发了字节。uart recv: 日志显示原串口包;can send: 日志显示转换后的 CAN 数据。后者只在课程封装返回 0 时出现;该封装等待邮箱空闲,不能凭打印就断言 CAN 的 TXOK、对端收到或显示已更新,需结合接收端观察。
**本课视频的测试和它证明的范围:**约 20:23 新日期包能设置屏幕日期,日志明确显示 uart recv:AF 5 6 14 19 7 14 FA 和 can send:AF 6 14 19 7 14 0 0。约 21:09~21:17 教师说“发一个 AF”,但画面发送记录实际上有 连续多次 AF,随后出现多条 data len error。之后发送正确日期包,可改成 8 月 20 日;再发送 AA AA 等无帧头数据,不转发、不改变日期;最后再次正确发送,日期改为 3 月 20 日。这支持“演示中的错误序列后能够继续接收”,不能把多次 AF 的演示写成任意一个孤立 AF 后的首个合法包必定成功。
按源码推导仍需理解的边界(不改课堂源码):
- 如果只发一次孤立 AF,再紧接一包
AF 05 06 14 19 07 14 FA,收到后者 AF 时计数为 2,收到后者 05 时计数为 3,长度下标 1 实际是旧/新衔接处的 AF=175,判非法归零;05 也已被消费,接下来的 06 等字节没有帧头,因此 这一包会丢弃。再发送一包完整正确命令才可能正常处理。它能回到等待帧头,但不是零丢包恢复。 - 本课没有超时。只发
AF 05 06后暂停,计数仍为 3;之后来的新包字节会先被当成旧包后续内容,可能到尾检查才丢弃,也可能继续损失一包。软件没有自动知道两次“点击发送”之间是新命令。 - 长度检查没有绑定业务类型。比如
AF 02 06 14 FA格式检查通过后,CAN 转出AF 06 14 00 00 00 00 00;日期业务会按缺失字段的零值解析。说明格式通过不等于日期业务有效,也不能因为 L 在范围内就断言类型正确。 - 错包归零时没有重新把当前字节当新帧头处理;若当前字节恰好是下一条正确命令的 AF,也会一起丢弃。进一步的重同步、超时、类型长度一致性属于后续设计议题,本课未实现,不能擅自补入源码。
本节仍在中断回调内执行大量阻塞 printf 和 Can_Send_Msg 等待,并且最后才重新开启接收;期间硬件可能继续来字节,不能据“单字节接收”推断永不丢字节。29 工程 USART2 中断优先级为 0、SysTick 为 15,若在高优先级回调中阻塞等待依赖低优先级节拍的超时,节拍不能抢占该回调;这延续 231 已指出的限制。本课没有把这些代码迁到主循环,也没有实现 UART 硬件错误后的完整恢复。ST 官方 HAL UART 实现
**CRC 与帧尾判断的区别:**教师约 22:02 提到可进一步增加校验,现有源码没有校验字段。固定 FA 只能检查期望位置是否为尾标记,业务数值从 11 变成 12 而尾仍是 FA 时照样通过。教师口头把 CRC 概括为“把数据加起来”,需要精校:简单字节求和是加和校验,CRC 使用约定生成多项式的运算,不能直接等同于普通加法。若以后扩展,双方还需约定计算范围、多项式、初值、位序、结果处理和字节顺序,本课未指定具体算法,因此不编造 CRC 参数或代码。ST AN4187:CRC 原理与实现
短距离、低速度是教师在演示中简化协议的背景,不是无校验必然可靠的证明;本串口 8N1 配置也没有硬件奇偶校验。CAN 自身的链路检错不能反过来验证串口收到的业务是否已损坏,因为桥接端可能把错误业务重新封成一帧 CAN 发出。
教师建议理解后把这段代码作为小设计模板,前提是能自己走通每一层条件,而不是直接复制便宣称协议健壮。学习时应能说清:L 是否含类型、总长为何加 3、帧尾下标为何加 2、计数何时归零、为何新串口包不能原样发给 ZET6、单个 AF 后首个合法包可能怎样丢失、帧尾正确为何不等于数据正确。
**核对依据:**本节完整转录及视频中协议和代码画面;27C8T6_Send 旧业务、29C8T6_Send_OK 的 usart.c/.h、canif.c、.ioc;28Can_Recv_Ok 的 CAN 解析与 RTC 处理。重点画面约 10:50 新协议、14:00 缓存类型、16:00 帧头和计数、19:17 长度与尾判断、20:27 日期转发日志、21:17 连续 AF 测试、21:40 噪声及恢复。正文的异常单次输入结果是对原代码的推导,课堂实机结果只按视频描述,本轮没有声称在两块实际开发板上重跑。