Can数据C8T6发送代码(1)

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

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 → HSECrystal/Ceramic Resonator使用开发板外部晶体;“CRIS OL”是错误转录
SYS → DebugSerial Wire保留 SWD 调试下载功能,PA13/PA14 分别用于 SWDIO/SWCLK
系统时钟72 MHz按板上 8 MHz HSE 与 PLL×9 配置
APB136 MHzCAN 和 USART2 所在外设时钟域
CANActivated、Normal启用真实 CAN 总线收发
CAN 时序Prescaler=4、BS1=9 TQ、BS2=8 TQ与 ZET6 的 500 kbit/s 配置一致
USART1Asynchronous115200、8N1,用于日志
USART2Asynchronous115200、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 00AF 01 44 00 00 00 00 0020% 与 68% 交替,即约 50° 与 170°
水温AF 02 01 00 00 00 00 00AF 02 00 00 00 00 00 00红色与白色交替
安全带AF 03 01 00 00 00 00 00AF 03 00 00 00 00 00 00红色与白色交替
灯光AF 04 01 00 00 00 00 00AF 04 02 00 00 00 00 00黄色与红色交替
转向灯AF 05 01 00 00 00 00 00AF 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 代码编写步骤

课堂采用的重定向步骤为:

  1. Keil Options for Target → Target 中勾选 Use MicroLIB,不是“make lab”。视频使用 Arm Compiler 5 环境,应与课程工具链匹配。
  2. 在 usart.c 的用户区包含 <stdio.h>,获得 FILE 等声明。
  3. 在 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、外设与供电类别。