Can数据C8T6发送代码(2)串口二接收

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

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 的全部接口。

本节参数具体含义初学者容易混淆的地方
&huart2USART2 句柄的地址,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,完整过程为:

  1. HAL_UART_Receive_IT() 检查串口接收状态和参数,设置本次标准接收。
  2. 内部启动函数保存缓冲地址、总长度和剩余计数,设置忙状态并打开接收相关中断。
  3. 字节到达触发 USART2 中断,HAL 读取数据寄存器,把数据写入数组,移动写指针并递减计数。
  4. 收满八字节后关闭这一批的接收相关中断,把接收状态恢复为 READY,然后调用完成回调。
  5. 用户回调处理完后再次调用接收接口,才能继续收下一批。

所以 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 默认引脚(配套工程已核对)电脑端用途
USART1PA9=TX,PA10=RX接收 C8T6 running 和 can send... 文本日志
USART2PA2=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 AF01 指针,11 是 0x11=17;后三类尾部数据不参与当前指针处理指针到 17% 的位置
AF 02 01 AF AF AF AF AF02 水温图标,状态 01水温红色
AF 02 00 AF AF AF AF AF水温状态 00水温白色
AF 03 01 AF AF AF AF AF03 安全带,状态 01安全带红色
AF 04 02 AF AF AF AF AF04 灯光,状态 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 模式与八字节长度缺一不可,以及为何“中断接收”仍可能被回调里的阻塞业务拖慢。这些解释都对应本节真实代码,而非提前补进下一课的优化实现。