仪表盘Can数据接收(1)

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

228.4.2 仪表盘Can数据接收(1)

/* 本节目的:让 ZET6 从 CAN FIFO0 取出一帧,按 ID、数据头和功能码判断后更新仪表指针数据。 */
/* 课程阶段:下面先收录视频本节末态的 can_handle.c;ui_rpm 此时是普通 uint32_t 变量。 */
/* 配套 26Can_Recv 已含后续课程的结构体与刷新标志;本节按视频实际写到的位置抄录。 */
#include "main.h"                         /* 引入工程公共头文件,间接取得 uint32_t、uint8_t 等类型声明。 */
#include "canif.h"                        /* 引入课程封装的 CAN 接口,下面使用 Can_Recv_Msg()。 */
#include "can.h"                          /* 引入 CubeMX 生成的 CAN 外设声明,与视频包含顺序一致。 */
#include "can_handle.h"                   /* 引入本文件对应的接口声明,使定义与对外声明保持一致。 */
uint32_t ui_rpm;                          /* 定义全局仪表数据;文件作用域未显式初始化的变量启动时置零,此处单位是百分比。 */
void Can_Handle(void)                     /* 定义无参数、无返回值的 CAN 应用处理函数,每轮主循环调用一次。 */
{                                        /* CAN 应用处理函数体开始。 */
    uint32_t recv_len;                    /* 保存接收接口返回的数据长度;只有接口返回后才使用它。 */
    uint32_t recv_id;                     /* 保存接收到的标准 CAN 标识符,由接收接口通过指针写入。 */
    uint8_t rx_data[8] = {0};             /* 建立 8 字节接收数组,{0} 使全部元素初始为零,对应 Byte0~Byte7。 */
    recv_len = Can_Recv_Msg(&recv_id, rx_data); /* 传入 ID 变量地址和数组首地址,让接口填入一帧的 ID 与数据并返回 DLC。 */
    if(recv_len > 0)                      /* 本课程把非零长度当作有数据;这里只检查非零,尚未验证协议要求的 8 字节。 */
    {                                    /* 开始处理一帧非零长度的数据。 */
        if(recv_id == 0x0B4)              /* 只处理双方约定的标准 CAN ID 0x0B4;该 ID 位于 CAN 帧头而非数据数组。 */
        {                                /* 标识符符合仪表业务约定,继续检查应用数据。 */
            if(rx_data[0] == 0xAF)        /* 检查第一个数据字节是否为课程自定义的应用层数据头 0xAF。 */
            {                            /* 数据头正确,继续读取功能码。 */
                if(rx_data[1] == 0x1)     /* 检查第二个字节是否为指针指令;0x1 与 0x01 数值相同。 */
                {                        /* 当前帧属于仪表指针控制指令。 */
                    ui_rpm = rx_data[2]; /* 把第三个字节的 0~100 百分比存入全局变量,交给 main.c 显示。 */
                }                        /* 指针指令处理结束;本节还没有其他功能码的处理分支。 */
            }                            /* 应用层数据头检查结束。 */
        }                                /* 标准 CAN 标识符检查结束。 */
    }                                    /* 非零数据长度检查结束。 */
}                                        /* 本次只尝试取一帧并处理,然后返回主循环。 */

can_handle.h——本节对外接口:

#ifndef __CAN_HANDLE_H__                  /* 第一次包含本头文件时进入,防止同一翻译单元重复展开声明。 */
#define __CAN_HANDLE_H__                  /* 标记本头文件已经展开;课程沿用这个宏名。 */
#include "main.h"                         /* 提供 uint32_t 等工程基础类型。 */
extern uint32_t ui_rpm;                   /* 声明变量实际定义在别的 .c 文件中,让 main.c 访问同一个 ui_rpm。 */
void Can_Handle(void);                    /* 声明 CAN 应用处理函数,末尾分号表示这是声明而不是函数体。 */
#endif                                   /* 结束头文件重复包含保护。 */

canif.c——本节从前面 CAN 工程复制、并回顾的滤波器与接收封装:

#include "canif.h"                        /* 引入该封装模块自身的公开接口声明。 */
#include "can.h"                          /* 引入 hcan 以及 CAN HAL 的类型、宏和函数声明。 */
#include "usart.h"                        /* 保留配套课程文件的包含项;下列滤波和接收函数并不使用串口。 */
void Can_Filter_Init(void)                /* 初始化接收滤波器,并在函数末尾启动 CAN 外设。 */
{                                        /* 滤波器初始化函数开始。 */
    CAN_FilterTypeDef Can_FilterConfig;   /* 建立滤波器配置结构体,下面逐项填写所需字段。 */
    Can_FilterConfig.FilterBank = 0;      /* 使用编号为 0 的硬件滤波器组。 */
    Can_FilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; /* 选择掩码模式,掩码位决定哪些标识符位必须匹配。 */
    Can_FilterConfig.FilterScale = CAN_FILTERSCALE_16BIT; /* 选择 16 位滤波器宽度;这不是 CAN 数据区长度。 */
    Can_FilterConfig.FilterIdHigh = 0x0;   /* 填写该滤波组的一个 16 位 ID 值;本例配合零掩码实现全通。 */
    Can_FilterConfig.FilterIdLow = 0x0;    /* 填写该滤波组的另一个 16 位 ID 值。 */
    Can_FilterConfig.FilterMaskIdHigh = 0x0; /* 掩码各位全为零,表示这些位置都不要求匹配。 */
    Can_FilterConfig.FilterMaskIdLow = 0x0; /* 另一组掩码同样全零,使该滤波器接收所有 ID。 */
    Can_FilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0; /* 把通过本滤波器的帧存入接收 FIFO0。 */
    Can_FilterConfig.FilterActivation = CAN_FILTER_ENABLE; /* 激活这一组滤波器,否则配置存在也不会参与接收。 */
    Can_FilterConfig.SlaveStartFilterBank = 14; /* 保留课程值;单 CAN 的 STM32F103 不用它划分 CAN1/CAN2 滤波器。 */
    if (HAL_CAN_ConfigFilter(&hcan, &Can_FilterConfig) != HAL_OK) /* 把配置写入硬件,并判断配置操作是否成功。 */
    {                                    /* 配置失败时进入错误处理路径。 */
        Error_Handler();                 /* 调用工程错误处理函数;本节不展开其库/模板实现。 */
    }                                    /* 滤波器配置结果判断结束。 */
    HAL_CAN_Start(&hcan);                 /* 让 CAN 退出初始化状态并开始运行;课程这里未检查启动返回值。 */
}                                        /* 滤波配置及 CAN 启动结束。 */
uint32_t Can_Recv_Msg(uint32_t *id, uint8_t *buf) /* 接收一帧:通过两个指针输出 ID 和数据,通过返回值输出数据长度。 */
{                                        /* 课程接收封装函数开始。 */
    CAN_RxHeaderTypeDef RxMessage;        /* 保存 HAL 读出的 CAN 帧头,包括标识符、帧类型和 DLC。 */
    if(HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_FILTER_FIFO0) == 0) /* 查询 FIFO0 待取帧数;零表示目前没有帧可取。 */
        return 0;                        /* FIFO 为空立即返回,避免主循环在这里等待总线数据。 */
    HAL_CAN_GetRxMessage(&hcan, CAN_FILTER_FIFO0, &RxMessage, buf); /* 从 FIFO0 取出一帧,把帧头和最多 8 字节数据分别写入。 */
    *id = RxMessage.StdId;                /* 对 id 指向的变量赋值,调用者的 recv_id 因此得到标准标识符。 */
    return RxMessage.DLC;                /* 返回该帧的数据长度;课程此处未检查 GetRxMessage 的 HAL 返回状态。 */
}                                        /* 接收接口结束,控制权返回 Can_Handle()。 */

canif.h——课堂查看的三个封装接口:

#ifndef __CANIF_H__                       /* 防止同一 .c 文件通过多条包含路径重复展开此头文件。 */
#define __CANIF_H__                       /* 设置本头文件的包含标记。 */
#include "main.h"                         /* 提供 uint32_t、uint8_t 和基础 HAL 声明。 */
void Can_Filter_Init(void);               /* 声明配置滤波器并启动 CAN 的接口。 */
uint32_t Can_Send_Msg(uint32_t id, uint32_t len, uint8_t *buf); /* 声明发送接口;发送实现本节略过,后续 C8T6 课程使用。 */
uint32_t Can_Recv_Msg(uint32_t *id, uint8_t *buf); /* 声明接收接口,两个指针接收 ID 和数据,返回值是长度。 */
#endif                                   /* 结束头文件包含保护。 */

main.c——本节新增/调整的局部代码,分别放在对应位置:

#include "canif.h"                        /* 放在文件顶部包含区,使 main.c 能调用 Can_Filter_Init()。 */
#include "can_handle.h"                   /* 同样放在包含区,取得 Can_Handle() 和 extern ui_rpm 的声明。 */
MX_CAN_Init();                           /* 位于外设初始化区,由 CubeMX 生成,先建立 CAN 时序、模式和引脚配置。 */
Can_Filter_Init();                       /* 位于 USER CODE BEGIN 2 区,完成硬件滤波设置并启动 CAN。 */
Can_Handle();                            /* 位于 while (1) 循环内部,每轮读取并解析一次 CAN 接收 FIFO。 */
lv_img_set_angle(ui_ImageNeedle, ui_rpm*2500/100); /* 紧接处理调用之后,用百分比计算指针角度;2500 表示 250.0°。 */

上方代码块分别属于多个文件;main.c 仅摘录本节涉及的语句,不能把所有块拼在一起当成一个完整 C 文件。源码保留课堂写法及其当时的不足,改进点在正文说明。本节没有加入刷新标志、其他功能码分支、串口转发或发送端实现。

一、任务信息资源导图

本节先解决“拿到一个协议后,怎样开始写接收代码”。原笔记的“窗口”“看”“IVGL”等转录词分别应为串口、CAN、LVGL。

完整链路是:串口助手 → C8T6 USART2 → C8T6 CAN 发送 → ZET6 CAN 接收 → LVGL 显示。需要分别编写 C8T6 与 ZET6 两个工程,因为两块 MCU 执行不同角色的程序。

课堂采用分步验证:先让 C8T6 发送固定的 CAN 测试数据,让 ZET6 收到并显示;确认这段通路后,再接入电脑串口助手。这能让一次调试涉及的接口数量减少,便于定位问题。

动手前先列清楚协议,而不是直接堆代码:

信息课程约定对应代码思路
数据长度8 字节,Byte0~Byte7建立 uint8_t rx_data[8]
CAN ID标准 ID 0x0B4判断 recv_id
应用数据头Byte0=0xAF判断 rx_data[0]
功能码Byte1=0x01~0x07逐个判断 rx_data[1]
指针参数Byte2=0~100存入仪表数据变量并映射到角度

功能码依次为:0x01 指针,0x02 水温,0x03 安全带,0x04 灯光,0x05 转向灯,0x06 日期,0x07 时间。原笔记说“0~7 分别代表功能”不准确;Byte0~Byte7 是八个位置编号,不能把位置编号与功能码混为一谈。

学习时可以先根据协议自己写出“取数据 → 判断 ID → 判断数据头 → 判断功能码 → 取参数 → 更新界面”的草稿,再与视频对照。遇到无从下手时,优先找一个可以独立验证的最小步骤,例如“先读出 FIFO 中的一帧及其 ID”,而不是要求一次完成整条链路。

二、接收与发送代码

2.1 写ZET6代码

2.1.1 复制修改工程名称
  • 本节实际修改的是 ZET6 仪表接收工程。转录在开头几处把 ZET6/C8T6 混淆了,应结合被复制的 LCD/LVGL 工程和接收任务判断。
  • 以第 25 个工程 25Lvgl_Dashboard _Flashok 为基础复制,得到配套目录 26Can_Recv。该基础工程已经完成仪表盘及图片 Flash 占用优化。
  • “接收”英文是 receive,配套工程缩写采用 Recv。原笔记中的 26_CAN_Receive 是概括性写法,实际文件路径应以配套目录为准。
  • 复制后打开新目录内的 .ioc 和 Keil 工程,避免修改旧工程。配套目录内部仍可保留 12LCD_Display.ioc、12LCD_Display.uvprojx 等旧名称;文件名不自动等于目录名。
  • 视频右键 main.c 标签,使用 Close All Other Files,仅保留当前文件,减少查找干扰。
2.1.2 接入数据

原工程还没有初始化 CAN,需要用 STM32CubeMX 配置:在 Connectivity 中激活 CAN,采用正常模式,默认引脚为 PA11=CAN_RX、PA12=CAN_TX。

参数视频设置含义
CAN 时钟APB1=36 MHz由工程时钟树确认,不是直接使用 CPU 的 72 MHz
Prescaler4CAN 时钟分频值
Sync Segment固定 1 TQ一个 CAN 位开始的同步段
Time Segment 1(BS1)9 TQ采样前时间段
Time Segment 2(BS2)8 TQ采样后时间段
SJW1 TQ重同步时允许调整的最大时间宽度
ModeNormal通过外接收发器与真实总线通信

先计算一个时间量子:

[ t_q=\frac{Prescaler}{f_{PCLK1}}=\frac{4}{36\times10^6}\approx111.11\ ns ]

一个位包含 1+9+8=18 TQ,因此:

[ Bitrate=\frac{36\times10^6}{4\times(1+9+8)}=500000\ bit/s=500\ kbit/s ]

这是 500 kbit/s,不是 500 kB/s。APB1 的定时器倍频规则不能套用到 CAN,CAN 按 PCLK1 计算。CubeMX 显示的位时间应约为 2000 ns=2 μs,可与公式互相核对。

这组时序的采样点为 (1+9)/18≈55.56%。它是课程实际参数;更换线长、收发器、时钟或网络后,应重新评估采样点、传播延迟与时钟容差,不能只因算出 500 kbit/s 就认定全部时序条件均合适。

点击 Generate Code 生成代码,在 Keil 中确认 Reload 工程,然后编译。视频中的“帧”“IMAX”“but to no”等 OCR 表述应纠正为 CubeMX、Generate Code、Reload、Build/Rebuild 等操作,不是新的接收功能。

硬件仍按第 227 篇连接:两端 PA12 接各自收发器 TXD,PA11 接 RXD;两只收发器之间 CANH 对 CANH、CANL 对 CANL,并检查参考地和两端终端电阻。CubeMX 激活的是 MCU 内部 CAN 控制器,它不会代替外部收发器。

2.1.3 添加文件

从前面第 4 个 CAN 工程复制 Canif 文件夹到新工程的 BSP 目录。文件夹内的 canif.c/.h 是课程此前写的接口封装,负责滤波器配置、发送、接收;不是 STM32 官方库的全部实现。

文件复制到硬盘只是第一步,还要让 Keil 构建系统编译 canif.c,让编译器找到 canif.h。这分别对应“添加源文件”和“设置头文件搜索路径”,不能用其中一项代替另一项。

2.1.4 右键点击bsp

在 Keil 工程树的 BSP 组上右键,选择 Add Existing Files to Group 'BSP',浏览到第 26 个工程的 BSP/Canif,选择 canif.c,点击 Add 后关闭窗口。

不要只记“右键菜单第三项”:菜单排序可能随版本变化,应认准功能名称。路径也要指向第 26 个工程自己的副本,避免工程引用旧目录,之后修改一个文件却影响另一个项目。

2.1.5 填写头文件

在 Options for Target → C/C++ → Include Paths 中加入新工程的 BSP/Canif 路径;使用相对工程的路径更便于整体移动工程。然后在调用接口的 .c 文件中写 #include "canif.h"。

  • Include Paths 告诉编译器“去哪里找”;
  • #include 告诉预处理器“把哪个头文件的声明展开进来”;
  • 把 .c 加入工程告诉构建系统“编译哪份实现”。

完成后先编译。头文件不必像 .c 一样加入源文件编译列表;如果为了浏览把它显示在工程树里,也不会改变这三个步骤的职责。

2.1.6 之前工程复制

canif.c 主要包含三个公开接口:Can_Filter_Init()、Can_Send_Msg()、Can_Recv_Msg()。课堂通过对应头文件快速查看公开函数,再定位实现。

头文件只能帮助查看公开声明,不保证列出 .c 中的所有内部函数,例如 static 函数通常只属于该模块。复制旧接口后仍要编译,确认它使用的外设句柄和头文件与新工程匹配。

2.1.7 查看canif.h
接口本节作用
Can_Filter_Init(void)设置接收过滤条件,并启动 CAN
Can_Send_Msg(id, len, buf)发送一帧;本节接收端暂不使用
Can_Recv_Msg(id, buf)轮询 FIFO0 并取出一帧;接收端的核心入口

Keil 可通过右键 Open Document 打开头文件,通过 Go To Definition 查看函数定义。理解一个已有模块时,先读接口参数、返回值,再读具体实现,比直接翻全部库文件更容易把握用途。

2.1.8 接收到数据处理

MX_CAN_Init() 只建立外设时序、模式和 GPIO 配置。还要调用 Can_Filter_Init() 配置并激活滤波器,然后执行其内部的 HAL_CAN_Start()。

执行顺序为:外设初始化 → 配置并启用滤波器 → 启动 CAN → 在主循环中取帧。

本节使用“全通”硬件滤波器:掩码为零,表示所有对应位都不参与比较,通过的帧进入 FIFO0。随后 Can_Handle() 再用软件筛选 0x0B4。硬件全通不等于无需配置滤波器,也不等于界面会处理所有 ID。

FilterScale=16BIT 表示滤波寄存器组织方式,与 8 字节应用数据长度无关。SlaveStartFilterBank=14 是旧封装保留项;STM32F103ZET6 只有一个 CAN 实例,这个字段不用于给不存在的 CAN2 分配滤波器。

视频从头文件复制函数声明到 main.c 后,将其变成 Can_Filter_Init(); 调用语句。声明有返回类型,调用没有返回类型。若编辑器出现红线,可能是未包含头文件,也可能是符号拼写、路径或索引问题,应以编译器报错为准。

2.1.9 包含之前工程头文件

调用前包含准确文件名 canif.h;不要使用 OCR 生成的 can_if.h。调用业务处理时还需包含 can_handle.h。

Can_Recv_Msg(&recv_id, rx_data) 的三个输出如下:

形式输出内容为什么这样传
&recv_idCAN 标识符& 取得变量地址,接口通过 *id=... 修改调用者的变量
rx_data接收字节数组数组名在此转换为首元素指针,接口向该内存写入数据
返回值赋给 recv_lenDLC 数据长度返回值单独表达帧内有效数据字节数

接收数组必须容纳经典 CAN 最大 8 字节。recv_len 的 C 类型是 uint32_t,并不表示一帧有 32 字节;类型宽度与数据长度是两件事。

课程接口 FIFO 为空时返回零,不会等待帧到来;FIFO 非空时一次取一帧。这里是主循环轮询接收,没有启用 CAN 接收中断,不需要凭空增加中断回调。

2.2 重新封装CAN接收文件

把应用协议解析从 main.c 抽到 can_handle.c/.h,与 canif 放在同一文件夹:

文件职责
canif.c/.h接近硬件的接口:滤波、取帧、发帧
can_handle.c/.h仪表业务:识别 ID、数据头、功能码并保存参数
main.c调用接收处理并把数据交给 LVGL 显示

视频通过新建文件、改扩展名得到 .c/.h。实际应创建纯文本源码文件;已有 Word 文档仅改后缀不会转换成 C 源码。Windows 资源管理器可在“查看 → 显示 → 文件扩展名”中显示真实后缀,防止生成 can_handle.c.txt。

将 can_handle.c 加入 BSP 组。因为 .h 与 canif.h 位于同一个已加入搜索路径的目录,不必另加一个重复路径。新函数签名是 void Can_Handle(void):前一个 void 表示无返回值,括号内 void 表示无参数;原笔记的“返回值为 0”应纠正。

2.3 添加处理函数

函数先定义三个局部变量,再调用 Can_Recv_Msg()。rx_data[8]={0} 将八个元素全部置零;recv_len 在调用返回时赋值,recv_id 只有取到帧才被接口填写,所以课程把后续判断放在 recv_len>0 内。

can_handle.h 复制头文件基本框架后,将包含保护宏改为 __CAN_HANDLE_H__,删除旧模块的接口声明,再加入 Can_Handle() 声明。如果两个不同头文件错误地沿用同一个保护宏,先被包含的文件会阻止另一个展开,造成声明丢失。

在 main.c 的 while(1) 中调用 Can_Handle(),每轮都尝试处理一帧。原笔记中的 receive_message、receive_ID 等是概述,视频的实际标识符是 Can_Recv_Msg、recv_id、recv_len、rx_data;抄写时大小写和下划线必须一致。

三、协议详解

本节判断顺序可读成:长度非零 → ID 为 0x0B4 → Byte0 为 0xAF → Byte1 为 0x01 → 从 Byte2 取得指针参数。

数组元素协议位置本节意义
rx_data[0]第 1 字节数据头 0xAF
rx_data[1]第 2 字节指针功能码 0x01
rx_data[2]第 3 字节0~100 指针位置百分比

数组从零下标开始,所以“第二个字节”对应 [1],不是 [2]。0xAF 不匹配只能说明这帧不符合本应用协议,不能直接说明 CAN 硬件传输错误。

0x0B4 是双方人为约定的 CAN 标识符,发送端必须一致。标准标识符有 11 位,范围 0x000~0x7FF,它并非每个节点天然自带的地址;在 CAN 中通常用于区分消息及参与仲裁。本节接收封装取 StdId,以标准数据帧为实验前提。

四、转速数据接收并处理

ui_rpm 在 can_handle.c 定义,在 can_handle.h 用 extern uint32_t ui_rpm; 声明,再由 main.c 包含该头文件访问同一变量。定义负责建立变量,extern 声明告诉其他文件“变量在别处存在”,不能在每个 .c 中再建立一个独立副本。

RPM 通常是 revolutions per minute,即每分钟转数。但课程变量名 ui_rpm 此时实际存储的是 0~100 的百分比,并没有真实转速单位和比例标定。收到 0x11 就得到十进制 17,即 17% 指针位置。

视频把原来定时自增的指针演示语句屏蔽,改为在主循环中读取接收值。学习时要理解:过去的“指针自动扫动”由 MCU 自己生成角度;现在角度来源变为 CAN 报文。若旧自增逻辑还在更新同一个对象,两个数据来源会互相覆盖。

本节没有检查参数是否超过 100,也没有严格检查 DLC 是否为 8。保留原代码用于理解课程步骤,但应认识其边界:

  • 短帧不能仅因 recv_len>0 就被当成完整应用报文;数组清零会让不存在的字节表现为零,可能产生错误的默认参数。
  • 零长度 CAN 数据帧在 CAN 标准中允许,但课程接口返回零同时表示“FIFO 空”与“帧 DLC 为零”,不适合区分这两种情况。本应用固定 8 字节,暂不依赖零长度帧。
  • 接收封装还没有检查 HAL 取帧状态、标准/扩展类型和数据/远程帧类型。扩展/远程帧不能简单当作本协议标准数据帧。
  • 本节 ui_rpm 更新后,界面语句每轮循环都被调用。下一篇将引入刷新标志来控制调用时机。

这些是源码实际限制,不能通过注释把它描述成已经完成校验的健壮接收器,也不能偷偷改写为后续版本。

五、投屏角度计算

本节投屏用于观察实体仪表指针,核心知识是百分比到 LVGL 角度单位的映射。LVGL 8 的 lv_img_set_angle() 使用 0.1° 单位:2500 对应 250.0°,不是 2500°。

[ angle_{0.1°}=\frac{ui_rpm\times2500}{100} ]

收到的百分比计算结果真实旋转角
000°
1742542.5°
491225122.5°
812025202.5°
1002500250°

数学上可以理解为“百分比乘全量程”,但 C 整数表达式必须按视频的 ui_rpm*2500/100 执行。ui_rpm/100*2500 会先做整数除法,17/100、49/100、81/100 全得到零。这正是原笔记需要纠正的计算顺序。

这里最大合理中间值为 100×2500=250000,uint32_t 足够容纳。旋转中心仍沿用前面仪表盘建立时的 pivot;lv_img_set_angle() 设置对象属性后,需要主循环中的 lv_timer_handler() 等既有 LVGL 调度来完成实际绘制。

本节到此只完成接收端的一个指令分支,发送端程序尚待后续课程实现。视频末尾提出的“重复调用显示函数”问题由下一篇接着处理。

六、复习与资料依据

学习检查:能否不看视频写出接收判断的顺序;能否解释 &recv_id 与 rx_data 为什么不同;能否从 36 MHz 时钟算出 500 kbit/s;能否指出整数先除法导致的角度错误;能否说明一个新 .c/.h 文件进入工程需要哪些配置。

  • 课堂视频与转录:重点核对约 04:18~09:43 的 CubeMX/接口回顾、12:28~18:20 的函数封装,以及 18:25~23:04 的协议与角度代码。
  • 配套 26Can_Recv/BSP/Canif/canif.c/.h、Core/Src/can.c 和 12LCD_Display.ioc:核对滤波器、接收封装和 PCLK1/CAN 时序。can_handle 的本节普通变量版本依据视频,而非后续合并完成的配套版本。
  • ST RM0008:STM32F1 参考手册:bxCAN 时序、标识符过滤和接收 FIFO 原理。
  • ST 官方 STM32F1 CAN HAL 源码:核对单 CAN 滤波器行为以及启动、FIFO 查询、取帧 API;笔记没有抄录库实现。
  • LVGL 8.3 Image 文档:核对角度单位为 0.1°、旋转中心和图像旋转要求。