Can数据C8T6串口代码优化

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

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 FA2 / 5类型 01、百分比 0x11=17,不是 11%
水温AF 02 02 00 FA / AF 02 02 01 FA2 / 5类型 02、状态 0 白色/1 红色
安全带AF 02 03 00 FA / AF 02 03 01 FA2 / 5类型 03、状态 0 白色/1 红色
灯光AF 02 04 00 FA / AF 02 04 01 FA / AF 02 04 02 FA2 / 5类型 04、状态 0 白色/1 黄色/2 红色
转向灯AF 02 05 00 FA / AF 02 05 01 FA2 / 5类型 05、状态 0 白色/1 红色
日期AF 05 06 14 19 07 14 FA5 / 8类型 06,加世纪、年、月、日四个字节
时间AF 04 07 10 08 21 FA4 / 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函数

按原回调的执行顺序逐层理解:

  1. huart->Instance == USART2 限制只处理串口二。将新字节写入 rx_buf[rx_cnt] 后执行 rx_cnt++,因此写入第一个字节后计数为 1,有效下标是 0。
  2. 如果缓存下标 0 不是 AF,立即将计数归零。下一字节覆盖下标 0,等待可能的帧头;此失败分支没有打印,所以噪声字节可能看不到日志。
  3. 如果帧头正确且计数达到 9,归零并打印 data len error。正常协议最大 L=5,总包最多 8 字节,9 字节已经超限。
  4. 未超限时,必须 rx_cnt > 2 才读长度。视频口头说“第二个字节也收到了”,代码实际等到 第三个字节 到达;计数等于 2 时暂不校验。保留课堂条件,不悄悄改成 >=2。
  5. (data_len>1)&&(data_len<6) 判断的是 2~5,数学上也包括 3;课件使用 2、4、5 三种长度,代码却不是精确枚举这三种。
  6. rx_cnt >= data_len+3 表示整包已到,再检查 rx_buf[data_len+2]==0xFA。对于 L=2,需要五字节,尾在下标 4;对于日期 L=5,需要八字节,尾在下标 7。
  7. 尾正确才打印 uart recv:、复制业务区并发 CAN。尾错误则归零并打印 data tail error;长度非法也归零打印长度错误。未达到整包长度则继续保留计数,等待下一字节。
  8. 完整合法包处理完,将计数归零,即使 CAN 发送返回失败也没有保留该包重发;最后再提交下一字节接收。

计数归零是“此前字节不再作为有效候选帧”,不是将数组十个元素逐个清零。下一包写入会覆盖旧内容;本课在满足对应长度后才访问尾部,不能用旧数组内容推断已经接收了新帧。

当前逻辑先写数组,再检查计数上限。根据初值 0 及所有路径,正常进入回调时计数最多 8,最多写下标 8 后加到 9 就被重置,处于 10 元素数组范围内;不能把它误解成“任意损坏的计数也保证安全”,因为没有写入前的通用边界检查。课程原样保留这个顺序。

日期接收过程实例:

新到字节保存下标写后计数本轮处理
AF01头正确,尚未进入长度检查
0512长度字段已在缓冲,但原条件 >2 暂不检查
0623读出 L=5,合法;总长不足 8
1434继续等待
1945继续等待
0756继续等待
1467继续等待
FA78足够 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 FAAF 01 11 00 00 00 00 00
AF 05 06 14 19 07 14 FAAF 06 14 19 07 14 00 00
AF 04 07 10 08 21 FAAF 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 噪声及恢复。正文的异常单次输入结果是对原代码的推导,课堂实机结果只按视频描述,本轮没有声称在两块实际开发板上重跑。