【江科大CAN】3. 代码实战
本文介绍了STM32 CAN总线通信的实现方法,重点讲解了单个设备环回测试和三个设备间通信的配置流程。主要内容包括:1)CAN外设初始化步骤(时钟配置、GPIO设置、CAN控制器参数配置、过滤器初始化);2)报文发送流程(使用CanTxMsg结构体和CAN_Transmit函数);3)报文接收流程(通过CAN_MessagePending检查状态,使用CAN_Receive读取数据)。文中还详细列
代码实战
01-CAN总线单个设备环回测试

在开始编写本节代码之前,我们先回顾PPT内容,规划代码实现思路。
整体程序结构:
本程序主要分为三大部分:
- CAN外设初始化: 配置CAN控制器和相关硬件。
- 报文发送: 实现CAN报文的发送功能。
- 报文接收: 实现CAN报文的接收功能。
得益于库函数良好的封装性,这三个部分的实际代码实现并不复杂。核心流程是:定义并填充相应的结构体参数,然后调用对应的库函数。
详细初始化步骤:
CAN外设初始化通常包含以下关键步骤:
- RCC时钟初始化:
- 启用CAN所用GPIO引脚对应的GPIO端口时钟(例如,
GPIOA)。 - 启用CAN控制器本身的时钟(例如,
CAN1)。
- 启用CAN所用GPIO引脚对应的GPIO端口时钟(例如,
- GPIO初始化:
- 将CAN_TX引脚配置为复用推挽输出模式。
- 将CAN_RX引脚配置为上拉输入模式。
- CAN外设基本初始化:
- 使用
CAN_InitTypeDef结构体配置CAN的核心参数(例如:工作模式、波特率、采样点、各种功能开关等)。 - 调用
CAN_Init()函数应用这些配置。
- 使用
- CAN过滤器初始化:
- 由于过滤器配置相对复杂且参数众多,需单独初始化。
- 使用
CAN_FilterInitTypeDef结构体配置过滤器参数(例如:位宽、模式、过滤器ID、掩码ID、关联的FIFO、激活状态等)。 - 调用
CAN_FilterInit()函数应用过滤器配置。
- (可选)中断配置:
- 如果需要使用中断(如接收中断),需调用
CAN_ITConfig()函数使能相应的CAN中断。 - 配置NVIC(嵌套向量中断控制器),设置中断优先级。
- 编写对应的中断服务函数(ISR)。
- (注:我们当前的初步代码暂不使用中断,后续会演示中断用法。)
- 如果需要使用中断(如接收中断),需调用
至此,CAN外设初始化完成。
报文发送流程:
库函数提供了专门的发送函数CAN_Transmit():
- 定义一个
CanTxMsg(或库对应版本的结构体)结构体变量。 - 将待发送报文的ID、类型(标准/扩展)、数据长度(DLC)、数据内容等填充到该结构体中。
- 调用
CAN_Transmit()函数。该函数会将报文放入发送邮箱,由硬件自动发送。 - 可通过
CAN_TransmitStatus()函数查询发送邮箱的状态(如发送成功、挂起、失败)。
报文接收流程:
库函数提供了检查接收状态和读取报文的函数:
- 使用
CAN_MessagePending()函数(或检查相应标志位)检查指定的接收FIFO(FIFO0或FIFO1)中是否有报文在排队。 - 如果FIFO中有报文:
- 定义一个
CanRxMsg(或库对应版本的结构体)结构体变量。 - 调用
CAN_Receive()函数。该函数会从指定的FIFO中读取最早到达的报文,并将其数据(ID、类型、DLC、数据等)填充到传入的结构体变量中。 - 重要提示:
CAN_Receive()函数在成功读取报文后,通常会自动释放该FIFO邮箱(相当于将报文从队列头部移除)。因此,在调用CAN_Receive()之后,通常不需要再额外调用CAN_FIFORelease()函数,除非有特殊需求(如丢弃报文而不读取数据)。
- 定义一个
库函数概览 (stm32f10x_can.c):
现在,我们回到工程,查看Library目录下的stm32f10x_can.c文件(滑至文件末尾),了解关键库函数:
-
初始化和配置函数:
CAN_DeInit(): 将CAN外设恢复到默认(复位)状态。CAN_Init(): (核心) 初始化CAN外设基本参数(模式、波特率等),使用CAN_InitTypeDef结构体。CAN_FilterInit(): (核心) 初始化CAN过滤器,使用CAN_FilterInitTypeDef结构体。CAN_StructInit(): 为CAN_InitTypeDef结构体成员设置默认值。CAN_DBGFreeze(): (调试用) 配置调试时的冻结模式。CAN_TTComModeCmd(): (特定模式) 使能时间触发通信模式(TTCAN)中的TGT位。(互联型设备专用,通常不需要)CAN_SlaveStartBank(): (互联型设备专用) 配置CAN2的起始过滤器组。(通常不需要)
-
报文发送函数:
CAN_Transmit(): (核心) 请求发送一个报文,使用CanTxMsg结构体。返回值为报文存放的发送邮箱编号。CAN_TransmitStatus(): (核心) 获取指定发送邮箱的状态(成功、挂起、失败)。CAN_CancelTransmit(): 取消指定邮箱中挂起或待发送的报文(设置ABRQ位)。
-
报文接收函数:
CAN_Receive(): (核心) 从指定的接收FIFO(0或1)中读取一个报文,数据存入CanRxMsg结构体。读取后通常会自动释放邮箱。CAN_FIFORelease(): 手动释放指定FIFO的邮箱(将FMP计数器减1)。通常在仅调用CAN_Receive()读取数据时不需要此函数。CAN_MessagePending(): (核心) 获取指定接收FIFO(0或1)中排队的报文数量(即FMP寄存器的值)。
-
工作模式管理函数:
CAN_OperatingModeRequest(): 请求切换工作模式(初始化模式、正常模式、睡眠模式)。CAN_Sleep(): 使CAN进入睡眠模式(设置SLEEP位)。CAN_WakeUp(): 唤醒CAN(清除SLEEP位),唤醒后默认进入正常模式。
-
错误管理函数:
CAN_GetLastErrorCode(): 获取最近一次的错误代码。CAN_GetReceiveErrorCounter(): 获取接收错误计数器(REC)的值。CAN_GetLSBTransmitErrorCounter(): 获取发送错误计数器(TEC)的低8位值。(如需完整TEC,需结合其他方法)
-
中断和标志管理函数: (老朋友)
CAN_ITConfig(): 使能/失能指定的CAN中断源。CAN_GetFlagStatus(): 获取指定标志位的状态。CAN_ClearFlag(): 清除指定的挂起标志位。CAN_GetITStatus(): 检查指定的中断是否发生。CAN_ClearITPendingBit(): 清除指定的中断挂起位。
总结流程与关键函数:
- 初始化核心:
CAN_Init(),CAN_FilterInit() - 发送核心:
CAN_Transmit(),CAN_TransmitStatus() - 接收核心:
CAN_MessagePending(),CAN_Receive()
现在流程和可用函数已清晰,我们回到主函数开始编写代码。
STM32 CAN 外设初始化代码
步骤 1:创建模块文件
- 在项目
hardware文件夹上右键,选择“添加新文件”。 - 添加 C 文件:命名为
my_can.c,保存路径选择hardware文件夹。 - 添加 H 文件:命名为
my_can.h,保存路径选择hardware文件夹。 - 在
my_can.h头文件中,添加防止重复包含的宏:#ifndef __MY_CAN_H #define __MY_CAN_H // ... 后续代码 ... #endif /* __MY_CAN_H */ - 在
my_can.c源文件中,包含必要的头文件:#include "stm32f10x.h" // 根据实际使用的STM32系列修改,如 stm32f4xx.h #include "my_can.h"
步骤 2:编写 CAN 初始化函数
在 my_can.c 中定义初始化函数 void MY_CAN_Init(void),用于集中配置 CAN 外设(此处以 CAN1 为例)。
初始化流程:
-
开启时钟 (RCC Configuration):
- GPIO 时钟: CAN 默认使用 PA11 (CAN_RX) 和 PA12 (CAN_TX)。这些引脚属于 GPIOA,挂载在 APB2 总线上。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启 GPIOA 时钟- 注意: CAN 引脚可重映射到 PB8 (CAN_RX) 和 PB9 (CAN_TX)。如需重映射,需开启 GPIOB 时钟 (
RCC_APB2Periph_GPIOB) 并配置重映射寄存器 (GPIO_PinRemapConfig()),此处使用默认 PA11/PA12。
- 注意: CAN 引脚可重映射到 PB8 (CAN_RX) 和 PB9 (CAN_TX)。如需重映射,需开启 GPIOB 时钟 (
- CAN 外设时钟: CAN1 (和 CAN2,如果存在) 挂载在 APB1 总线上。
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); // 开启 CAN1 时钟
- GPIO 时钟: CAN 默认使用 PA11 (CAN_RX) 和 PA12 (CAN_TX)。这些引脚属于 GPIOA,挂载在 APB2 总线上。
-
初始化 GPIO (GPIO Configuration):
- CAN_TX (PA12): 配置为复用推挽输出模式。
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; // PA12 (TX) GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 (控制权交给CAN外设) GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度可选 GPIO_Init(GPIOA, &GPIO_InitStructure); - CAN_RX (PA11): 配置为上拉输入模式。
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; // PA11 (RX) GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式 (引脚默认状态是高电平) // GPIO_Speed 对输入模式通常无效或不需设置 GPIO_Init(GPIOA, &GPIO_InitStructure);
- CAN_TX (PA12): 配置为复用推挽输出模式。
-
初始化 CAN 外设参数 (CAN Peripheral Initialization):
- 使用
CAN_InitTypeDef结构体配置 CAN 工作模式和通信参数。 - 调用
CAN_Init(CANx, &CAN_InitStructure)应用配置。
CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack; // 模式:回环测试 (自发自收)。测试成功后改为 CAN_Mode_Normal // 位时序配置 (波特率计算关键参数) CAN_InitStructure.CAN_SJW = CAN_SJW_2tq; // 再同步跳转宽度 = 2 Tq (范围 1-4 Tq) CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq; // TSeg1 (BS1) = 2 Tq (范围 1-16 Tq) CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq; // TSeg2 (BS2) = 3 Tq (范围 1-8 Tq) CAN_InitStructure.CAN_Prescaler = 48; // 分频系数 Prescaler (BRP) = 48 (范围 1-1024) // 波特率计算: Fpclk1 / (Prescaler * (1 + BS1 + BS2)) // 假设 APB1 时钟 Fpclk1 = 36MHz: 36,000,000 / (48 * (1 + 3 + 2)) = 36,000,000 / 288) = 125,000 bps (125kbps) // 其他功能配置 (根据需求选择) CAN_InitStructure.CAN_TTCM = DISABLE; // 时间触发通信模式: 禁用 CAN_InitStructure.CAN_ABOM = DISABLE; // 自动离线管理: 禁用 (需手动恢复) CAN_InitStructure.CAN_AWUM = DISABLE; // 自动唤醒模式: 禁用 (需手动唤醒) CAN_InitStructure.CAN_NART = DISABLE; // 非自动重传: 禁用 (即启用自动重传) CAN_InitStructure.CAN_RFLM = DISABLE; // 接收 FIFO 锁定模式: 禁用 (溢出时覆盖旧报文);启用:溢出时候,新报文丢弃 CAN_InitStructure.CAN_TXFP = DISABLE; // 发送 FIFO 优先级: 禁用 (按标识符优先级发送),启用:先请求先发送 // 应用 CAN1 配置 CAN_Init(CAN1, &CAN_InitStructure);- 库函数内部工作模式处理:
CAN_Init()函数内部已处理工作模式切换(请求进入初始化模式 -> 配置寄存器 -> 退出初始化模式进入正常/回环模式)。用户无需手动操作INRQ或SLEEP位。
- 使用
注:这里分频不需要我们手动-1,因为其内部已经-1了。
- 配置 CAN 过滤器 (Filter Configuration):
- 使用
CAN_FilterInitTypeDef结构体配置过滤器。 - 调用
CAN_FilterInit(&CAN_FilterInitStructure)应用配置(过滤器独立于具体 CAN 实例,CAN1/CAN2 共用)。 - 配置全通过滤器示例 (允许接收所有报文):
CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; // 使用过滤器 0 (范围 0-13) CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; // 模式: 标识符屏蔽位模式 CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; // 位宽: 32位 // 配置为全通: 屏蔽码 (MASK) 全设为 0 (不检查任何位) CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; // ID 高 16 位 (可任意值, 因MASK=0) CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; // ID 低 16 位 (可任意值) CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; // MASK 高 16 位 = 0 (不屏蔽) CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; // MASK 低 16 位 = 0 (不屏蔽) CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; // 匹配报文存入 FIFO0 CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; // 激活过滤器 0 // 应用过滤器配置 CAN_FilterInit(&CAN_FilterInitStructure); - 过滤器工作说明: 此全通过滤器配置后,任何接收到的报文都会通过过滤器 0 并存入 FIFO0 队列。后续可通过检查 FIFO0 状态 (
CAN_MessagePending(CAN1, CAN_FIFO0) > 0) 和读取 FIFO0 (CAN_Receive(CAN1, CAN_FIFO0, &RxMessage)) 来获取报文。
- 使用
CAN外设初始化细节补充说明
1、工作模式切换的自动化机制
-
PPT理论流程
- 复位后默认进入睡眠模式
- 初始化需先切换到初始化模式
- 配置完成后进入正常模式
-
代码实现隐藏逻辑
- 模式切换过程被封装在
CAN_Init()库函数内部:// 伪代码逻辑 CAN->MCR &= ~CAN_MCR_SLEEP; // 清除SLEEP位(退出睡眠) CAN->MCR |= CAN_MCR_INRQ; // 置位INRQ(请求初始化模式) while(!(CAN->MSR & CAN_MSR_INAK)); // 等待INAK应答 // 配置寄存器参数... CAN->MCR &= ~CAN_MCR_INRQ; // 清除INRQ(退出初始化) while(CAN->MSR & CAN_MSR_INAK); // 等待退出应答 - 关键结论:开发者无需手动操作模式切换,库函数已实现完整状态机控制。
- 模式切换过程被封装在
2、过滤器初始化的时序问题
-
疑问点
- 在
CAN_Init()后(已进入正常模式)再初始化过滤器是否可行?没问题
- 在
-
技术原理
- 过滤器通过独立控制位
FINIT(Filter Initialization)管理:- 置位
FINIT:进入过滤器专属配置模式 CAN_FilterInit()函数内部自动操作:CAN->FMR |= CAN_FMR_FINIT; // 进入过滤器配置模式 // 配置过滤器参数... CAN->FMR &= ~CAN_FMR_FINIT; // 退出配置模式
- 置位
- 结论:过滤器初始化与CAN主状态机解耦,可在正常模式下独立配置。
- 过滤器通过独立控制位
总结版本的核心流程:
// my_can.c
#include "stm32f10x.h"
#include "my_can.h"
void MY_CAN_Init(void) {
// 1. 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // CAN GPIO (PA11, PA12)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); // CAN1 Peripheral
// 2. 初始化GPIO
GPIO_InitTypeDef GPIO_InitStructure;
// CAN_TX (PA12) - 复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// CAN_RX (PA11) - 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 初始化CAN外设参数 (回环模式, 125kbps)
CAN_InitTypeDef CAN_InitStructure;
CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
CAN_InitStructure.CAN_SJW = CAN_SJW_1tq;
CAN_InitStructure.CAN_BS1 = CAN_BS1_3tq;
CAN_InitStructure.CAN_BS2 = CAN_BS2_2tq;
CAN_InitStructure.CAN_Prescaler = 48;
CAN_InitStructure.CAN_TTCM = DISABLE;
CAN_InitStructure.CAN_ABOM = DISABLE;
CAN_InitStructure.CAN_AWUM = DISABLE;
CAN_InitStructure.CAN_NART = DISABLE;
CAN_InitStructure.CAN_RFLM = DISABLE;
CAN_InitStructure.CAN_TXFP = DISABLE;
CAN_Init(CAN1, &CAN_InitStructure); // 函数内部处理模式切换
// 4. 配置CAN过滤器 (全通, 存FIFO0)
CAN_FilterInitTypeDef CAN_FilterInitStructure;
CAN_FilterInitStructure.CAN_FilterNumber = 0;
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; // ID High (任意)
CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; // ID Low (任意)
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; // MASK High = 0 (通)
CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; // MASK Low = 0 (通)
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
}
// my_can.h
#ifndef __MY_CAN_H
#define __MY_CAN_H
#include "stm32f10x.h" // Or appropriate header
void MY_CAN_Init(void);
#endif /* __MY_CAN_H */
STM32 CAN 报文发送函数封装
目标: 封装一个简化版的 CAN 报文发送函数 void MyCAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *Data),用于发送标准数据帧。
函数原型设计:
void MyCAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *Data);
id: 报文的标识符 (ID)。虽然定义为uint32_t以兼容未来可能的扩展 ID,但在此简化函数中仅使用标准 ID (11位,范围 0x000 - 0x7FF)。Length: 数据段的长度 (DLC - Data Lengthgth Code),有效范围 0 - 8。Data: 指向要发送的数据内容的指针。数据长度必须至少为Length字节。
函数实现步骤 (在 my_can.c 中):
-
定义并填充发送报文结构体 (
CAN_TransmitMailbox_TypeDef):CAN_TransmitMailbox_TypeDef TxMessage; // 定义发送邮箱结构体变量 // 配置报文标识符 (ID) 和格式 TxMessage.StdId = ID; // 将传入的 ID赋值给标准 ID 成员 (StdId) TxMessage.ExtId = 0; // 扩展 ID (ExtId) 在此函数中不使用,设为 0 TxMessage.IDE = CAN_Id_Standard; // 指定使用标准标识符格式 (11位 ID) // 配置帧类型 TxMessage.RTR = CAN_RTR_Data; // 指定为数据帧 (非远程帧) // 配置数据长度 TxMessage.DLC = Length; // 将传入的数据长度 Length 赋值给 DLC // 复制数据内容 uint8_t i; for (i = 0; i < Length; i++) { TxMessage.Data[i] = Data[i]; // 将传入数据数组的内容逐字节复制到结构体的 Data 数组 } // 注意: Data 数组有 8 个元素,但只复制前 Length 个字节- 关键点说明:
IDE = CAN_Id_Standard: 强制使用标准 ID 格式。传入的id值应确保在 0x000 到 0x7FF 范围内。RTR = CAN_RTR_Data: 强制发送数据帧。DLC = Length: 设置数据长度。库函数/硬件会自动处理 DLC 值范围 (0-8)。- 数据复制: 使用
for循环将Data指向的数据复制到TxMessage.Data[]数组中。CRC 由硬件自动计算和添加,无需用户干预。
- 关键点说明:
-
调用库函数发送报文 (
CAN_Transmit):uint8_t TransmitMailbox = CAN_Transmit(CAN1, &TxMessage); // 尝试将报文放入发送邮箱
- 库函数内部工作 (
CAN_Transmit):- 函数会轮询检查三个发送邮箱 (Mailbox 0, 1, 2) 的
TME(Transmit Mailbox Empty) 标志位。 - 找到第一个
TME = 1(邮箱为空) 的邮箱。 - 将
TxMessage结构体中的配置 (ID, RTR, DLC, Data) 写入该邮箱的寄存器。 - 将该邮箱的
TXRQ(Transmit Request) 位置 1,请求发送。此时,邮箱状态从空置变为挂起 (Pending),后续的发送过程,由硬件管理员自动操作。 - 返回被使用的邮箱号 (
TransmitMailbox)。如果所有邮箱都忙 (TME = 0),则返回CAN_TxStatus_NoMailBox。
- 函数会轮询检查三个发送邮箱 (Mailbox 0, 1, 2) 的
- 库函数内部工作 (
-
等待发送完成 (可选但推荐):
uint32_t Timeout= 0; while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok) { Timeout++; if (Timeout> 100000) { // 设置一个合理的超时计数值 (例如 100,000) // 超时处理 (例如: break, 记录错误, 返回错误码等) break; } }注意:这里的 timeout不用清零,因为定义的是局部变量退出后栈回回收的,直接就等于0
- 目的: 确保报文成功发送出去,避免函数返回后立即进行其他操作可能干扰发送。
- 工作原理 (
CAN_TransmitStatus):- 检查指定邮箱 (
TransmitMailbox) 的状态。 - 通过检查
RQCP(Request Completed) 和TXRQ位来判断状态 (参考手册状态图)。 CAN_TxStatus_Ok: 表示报文已成功发送 (状态变为 空)。这是等待的目标状态。CAN_TxStatus_Failed: 表示发送失败 (例如仲裁丢失或错误)。CAN_TxStatus_Pending: 表示报文仍在挂起或传输中。
- 检查指定邮箱 (
- 超时处理:
- 使用循环计数器
Timeout防止因发送失败导致程序永久卡死。 - 超时值
100000是一个示例,需要根据系统时钟频率和预期最大发送时间进行调整。更优的方法是使用基于系统滴答定时器 (SysTick) 的绝对超时时间。 - 超时后应跳出循环。可以在此处添加错误处理逻辑 (如设置错误标志、返回错误码等)。
- 使用循环计数器
完整 MyCAN_Transmit 函数示例:
void MyCAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *Data) {
// 1. 填充发送报文结构体
CAN_TransmitMailbox_TypeDef TxMessage;
TxMessage.StdId = id; // 使用标准ID (确保id <= 0x7FF)
TxMessage.ExtId = 0; // 扩展ID未使用
TxMessage.IDE = CAN_Id_Standard; // 标准帧格式
TxMessage.RTR = CAN_RTR_Data; // 数据帧
TxMessage.DLC = Length; // 数据长度 (0-8)
// 复制数据
uint8_t i;
for (i = 0; i < Length; i++) {
TxMessage.Data[i] = Data[i];
}
// 2. 发送报文 (请求放入发送邮箱)
uint8_t TransmitMailbox = CAN_Transmit(CAN1, &TxMessage);
// 3. (推荐) 等待发送完成或超时
uint32_t Timeout= 0;
while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok) {
timeout++;
if (Timeout> 100000) { // 超时设定 (需根据实际情况调整)
// 超时处理 (例如: 记录错误、设置标志位等)
break;
}
}
// 函数结束,无论成功或超时都返回
}
STM32 CAN 接收功能实现 - 查询方式
目标: 实现两个函数,用于查询方式接收 CAN 报文(当前配置使用 FIFO 0):
- 检查 FIFO 中是否有报文:
uint8_t MyCAN_ReceiveFlag(void) - 从 FIFO 读取报文数据:
void MyCAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *Data)
函数 1:检查接收 FIFO 状态 (MyCAN_ReceiveFlag)
uint8_t MyCAN_ReceiveFlag(void) {
// 调用库函数获取 FIFO0 中挂起报文的数量
// 判断是否有报文 (数量 > 0)
if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0)
{
return 1;
}
return 0;
}
- 功能说明:
- 此函数用于查询 FIFO 0 中是否有待处理的报文。
- 调用标准库函数
CAN_MessagePending(CANx, FIFO_Number)。CAN1: 指定 CAN 外设实例。CAN_FIFO0: 指定要检查的 FIFO 队列)。注意: 在之前的过滤器配置中,我们指定了匹配的报文存入 FIFO 0。
CAN_MessagePending函数返回 FIFO 0 中当前排队的报文数量 (FMP寄存器的值),范围是 0 到 3。- 如果返回的数量
numPending > 0,表示 FIFO 0 中有报文,函数返回1。 - 如果返回的数量
numPending == 0,表示 FIFO 0 为空,函数返回0。
函数 2:从接收 FIFO 读取报文 (MyCAN_Receive)
void MyCAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *Data) {
// 定义接收报文结构体
CanRxMsg RxMessage; // 标准库中接收结构体通常名为 CanRxMsg
// 调用库函数从 FIFO0 读取一个报文
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
// 处理接收到的报文标识符 (ID) 和格式
if (RxMessage.IDE == CAN_Id_Standard) {
*ID = RxMessage.StdId; // 标准 ID (11位)
} else { // CAN_Id_Extended
*ID = RxMessage.ExtId; // 扩展 ID (29位)
}
// 处理帧类型 (数据帧 or 远程帧)
if (RxMessage.RTR == CAN_RTR_Data) {
// 数据帧: 提取数据长度和数据内容
*Length = RxMessage.DLC; // 数据长度 (0-8)
// 复制数据到输出缓冲区
uint8_t i;
for (i = 0; i < *Length; i++) {
pData[i] = RxMessage.Data[i];
}
} else { // CAN_RTR_Remote
// 远程帧: 无数据负载
*Length = 0; // 数据长度为 0
// 注意: pData 指向的缓冲区内容在此情况下不会被修改
// (可选: 可根据应用需求处理远程帧请求)
}
// 注意: RxMessage.FMI (过滤器匹配索引) 在此函数中未使用
}
注意:返回结构体,要分配堆空间,到时候还要释放,不如直接传一个结构体参数进来
- 功能说明:
- 此函数用于从 FIFO 0 中读取一个报文,并提取其 ID、数据长度和数据内容。
- 参数 (输出参数):
ID: 指向uint32_t的指针,用于输出接收到的报文 ID (标准 ID 或扩展 ID)。Length: 指向uint8_t的指针,用于输出接收到的数据长度 (DLC, 0-8)。如果是远程帧,输出 0。Data: 指向uint8_t数组的指针,用于输出接收到的数据内容。调用者需确保该缓冲区至少有 8 字节空间。如果是远程帧,此缓冲区内容不会被修改。
- 内部流程:
- 定义接收结构体 (
CanRxMsg): 用于存储库函数读取的报文信息。 - 调用库函数读取报文 (
CAN_Receive):CAN1: 指定 CAN 外设实例。CAN_FIFO0: 指定从哪个 FIFO 读取 (与检查函数和过滤器配置一致)。&RxMessage: 接收结构体的地址,库函数会将读取到的报文信息填充到此结构体中。- 重要: 此函数调用会从 FIFO 0 中移除该报文 (类似出队操作)。
- 处理标识符 (ID) 和格式 (
IDE):if (RxMessage.IDE == CAN_Id_Standard): 报文是标准格式 (11位 ID)。*ID = RxMessage.StdId;将标准 ID 赋值给输出参数*ID。
else: 报文是扩展格式 (29位 ID)。*ID = RxMessage.ExtId;将扩展 ID 赋值给输出参数*ID。
- 处理帧类型 (
RTR):if (RxMessage.RTR == CAN_RTR_Data): 报文是数据帧。*Length = RxMessage.DLC;将数据长度 (DLC) 赋值给输出参数*Length。- 使用
for循环将RxMessage.Data[]数组中的前*Length个字节复制到pData指向的调用者缓冲区。
else: 报文是远程帧。*Length = 0;远程帧没有数据负载,数据长度输出为 0。Data指向的缓冲区不会被修改。应用层可根据需要响应此远程帧请求 (当前函数内未实现)。
- 过滤器匹配索引 (
FMI): 结构体中的FMI成员指示了是哪个过滤器匹配了此报文。当前函数未使用此信息,但它在更复杂的过滤场景中很有用。
- 定义接收结构体 (
使用流程示例 (在主循环中):
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"
uint8_t KeyNum;
uint32_t TxID = 0x555;
uint8_t TxLength = 4;
uint8_t TxData[8] = {0x11, 0x22, 0x33, 0x44};
uint32_t RxID;
uint8_t RxLength;
uint8_t RxData[8];
int main(void)
{
OLED_Init();
Key_Init();
MyCAN_Init();
OLED_ShowString(1, 1, "TxID:");
OLED_ShowHexNum(1, 6, TxID, 3);
OLED_ShowString(2, 1, "RxID:");
OLED_ShowString(3, 1, "Leng:");
OLED_ShowString(4, 1, "Data:");
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
TxData[0] ++;
TxData[1] ++;
TxData[2] ++;
TxData[3] ++;
MyCAN_Transmit(TxID, TxLength, TxData);
}
if (MyCAN_ReceiveFlag())
{
MyCAN_Receive(&RxID, &RxLength, RxData);
OLED_ShowHexNum(2, 6, RxID, 3);
OLED_ShowHexNum(3, 6, RxLength, 1);
OLED_ShowHexNum(4, 6, RxData[0], 2);
OLED_ShowHexNum(4, 9, RxData[1], 2);
OLED_ShowHexNum(4, 12, RxData[2], 2);
OLED_ShowHexNum(4, 15, RxData[3], 2);
}
}
}
02-CAN总线三个设备互相通信

以下是对该段视频讲解内容的逻辑重构与专业校正,优化了技术表述的准确性和流程逻辑性:
多设备CAN总线通信实验指南
一、硬件搭建规范流程
-
单节点电路搭建
-
接线步骤:
收发器引脚 STM32连接点 电源连接 TXD PA12 - RXD PA11 - GND 面包板负极 共地 VCC - 5V供电 -
供电方案:
- ST-Link的5V引脚 → 面包板电源列
- 所有收发器VCC → 面包板同一电源列
-
-
多节点扩展
- 复制上述单节点电路×3
- 总线互联关键:
- 所有CAN_H引脚并联
- 所有CAN_L引脚并联
- 建议使用带螺丝端子的总线连接器
-
调试设备连接
- 各节点STM32通过ST-Link连接电脑
- USB不足解决方案:
- 使用USB扩展坞
- 非调试节点ST-Link接充电宝供电
二、软件配置核心修改
-
工程迁移
- 复制回环测试工程 → 重命名为
02_CAN_Multi_Device
- 复制回环测试工程 → 重命名为
-
关键参数修改
// 修改工作模式(单个节点代码) CAN_InitStructureure.CAN_Mode = CAN_Mode_Normal; // 回环→正常模式 -
节点ID分配原则
节点 发送ID 配置要点 1 0x555 必须保证ID全网唯一 2 0x666 避免仲裁冲突 3 0x777 范围需符合CAN标准帧格式
验证要点:
- 各设备按键可发送专属ID报文
- 所有设备屏幕实时显示总线数据
- 无ID冲突导致的仲裁失败
四、故障排查路径
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 回环成功但组网失败 | 物理层连接错误 | 检查CAN_H/CAN_L并联 |
| 特定节点收不到数据 | 终端电阻缺失 | 总线两端加120Ω电阻 |
| 数据随机丢失 | 波特率不一致 | 统一各节点波特率配置 |
| 发送阻塞 | ID冲突 | 检查ID唯一性 |
调试箴言:
始终遵循"回环测试→单点测试→组网验证"的递进策略,避免直接组网调试的复杂性。
03-标准格式-扩展格式-数据帧-遥控帧

代码结构预览:
-
主程序 (03 - 标准/扩展格式 & 数据/远程帧):
- 定义了一个结构体数组 (
txMsgArray),其中每一行代表一个待发送帧的参数,分别对应:- 标准格式数据帧
- 扩展格式数据帧
- 标准格式远程帧
- 扩展格式远程帧
- 主循环扫描按键:当按键按下时,程序依次从帧数组中取出一个帧发送出去。
- 接收处理逻辑:
- 接收到帧后,首先判断帧格式:
- 如果是标准格式 (IDE = CANId_Standard):第一行显示
STD,第二行显示标准ID (stdId)。 - 如果是扩展格式 (IDE = CANId_Extended):第一行显示
EXT,第二行显示扩展ID (extId)。
- 如果是标准格式 (IDE = CANId_Standard):第一行显示
- 接着判断帧类型:
- 如果是数据帧 (RTR = CANRTR_Data):第一行显示
DATA字符串,第三行显示数据长度DLC,第四行显示数据内容 (data)。 - 如果是远程帧 (RTR = CANRTR_Remote):第一行显示
REMOTE字符串,第三行显示数据长度DLC。由于远程帧没有数据段,第四行直接显示00000000(注意:虽然DLC被显示出来,但在远程帧中通常无实际意义)。
- 如果是数据帧 (RTR = CANRTR_Data):第一行显示
- 接收到帧后,首先判断帧格式:
- 重要配置: 此程序配置为回环模式,仅需一个设备即可观察到自发自收的现象。标识符过滤器配置为全通模式,任何帧都能被接收。
实验现象演示 (回环模式 - 单设备):
- 编译下载程序到设备。
- 按下按键,发送帧数组中的第一帧(自发自收)。
- 屏幕上显示接收到的帧信息:
- 第一行:
STD DATA(标准格式数据帧) - 第二行:
ID: 0x055(与发送帧指定的标准ID0x55一致) - 第三行:
DLC: 4 - 第四行:
Data: 11 22 33 44(与发送数据一致)
- 第一行:
- 继续按键:
- 发送并收到第二帧:
EXT DATA,ID: 0x12345678,DLC: 4,Data: AA BB CC DD(扩展格式数据帧)。 - 发送并收到第三帧:
STD REMOTE,ID: 0x666,DLC: 0,Data: 00000000(标准格式远程帧)。 - 发送并收到第四帧:
EXT REMOTE,ID: 0x0789ABCD,DLC: 0,Data: 00000000(扩展格式远程帧)。
- 发送并收到第二帧:
- 再次按键,循环发送第一帧。
- 定义了一个结构体数组 (
代码编写步骤 (以03为例):
- 复制工程
01_单个设备回环测试,重命名为03_标准格式_扩展格式_数据帧_远程帧。 - 改造
MyCAN模块 (mycan.c):- 发送函数 (
MyCAN_SendMsg): 将参数改为传递CAN_TxMessage结构体指针,函数内部直接使用该结构体调用CAN_Transmit。简化了参数传递。 - 接收函数 (
MyCAN_ReceiveMsg): 将参数改为传递CAN_RxMessage结构体指针,函数内部直接调用CAN_Receive并将结果存入该结构体。本质是包装了库函数,简化调用。
原代码:
- 发送函数 (
uint8_t MyCAN_ReceiveFlag(void)
{
if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0)
{
return 1;
}
return 0;
}
void MyCAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *Data)
{
CanRxMsg RxMessage;
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
if (RxMessage.IDE == CAN_Id_Standard)
{
*ID = RxMessage.StdId;
}
else
{
*ID = RxMessage.ExtId;
}
if (RxMessage.RTR == CAN_RTR_Data)
{
*Length = RxMessage.DLC;
for (uint8_t i = 0; i < *Length; i ++)
{
Data[i] = RxMessage.Data[i];
}
}
else
{
//...
}
}
改之后:
void MyCAN_Receive(CanRxMsg *RxMessage)
{
CAN_Receive(CAN1, CAN_FIFO0, RxMessage);
}
void MyCAN_Transmit(CanTxMsg *TxMessage)
{
uint8_t TransmitMailbox = CAN_Transmit(CAN1, TxMessage);
uint32_t Timeout = 0;
while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok)
{
Timeout ++;
if (Timeout > 100000)
{
break;
}
}
}
- 更新函数声明 (
mycan.h): 匹配修改后的参数类型。 - 修改主程序 (
main.c):-
定义帧数组: 创建
CAN_TxMessage txMsgArray[4]并初始化四个测试帧(标准数据、扩展数据、标准远程、扩展远程)。
-
发送逻辑: 使用索引变量
pTxMsgArray跟踪当前发送位置。按键按下时,发送txMsgArray[pTxMsgArray]指向的帧,然后索引递增并回绕(使用sizeof计算数组大小实现自适应)。
-
接收显示逻辑: 如前所述,根据接收到的
rxMsg结构体中的IDE和RTR字段,在OLED上清晰显示帧类型(STD/EXT, DATA/REMOTE)、ID、DLC和数据(远程帧数据区显示0)。
-
- 模式配置: 确保CAN工作在 **回环模式 (
CAN_Mode_Loopback) ** 以便单设备测试。 - 过滤器配置: 在03代码中配置为 全通模式 (
CAN_FilterMode_IdMask,CAN_FilterScale_32bit, Mask 设置为0x00000000)。
部署到多设备:
- 将03代码中的CAN模式改为 **正常模式 (
CAN_Mode_Normal) **。 - 发送设备: 注释掉接收处理代码,只保留发送逻辑(按键发送
txMsgArray中的帧)。 - 接收设备: 注释掉发送逻辑,只保留接收和显示逻辑。可配置特定的过滤器模式(如04代码)进行验证。
- 注意: 正常模式下,总线至少需要两个设备(一个发送,一个或多个接收/应答)。设备发送的帧ID和格式应避免冲突。
(04及后续代码编写思路简述):
- 复制03工程,按需重命名(如04)。
- 修改待发送帧数组: 包含需要测试过滤效果的ID帧。
- 关键:按PPT示例配置特定的过滤器模式(列表/掩码)和参数(ID值、掩码值)。
- 主循环逻辑不变(按键发送数组中的帧)。
- 观察接收设备是否只接收到预期ID的帧。
基于环回测试模式总代码:
#include "stm32f10x.h" // Device header
void MyCAN_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
CAN_InitTypeDef CAN_InitStructure;
CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;
CAN_InitStructure.CAN_Prescaler = 48; //波特率 = 36M / 48 / (1 + 2 + 3) = 125K
CAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;
CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;
CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;
CAN_InitStructure.CAN_NART = DISABLE;
CAN_InitStructure.CAN_TXFP = DISABLE;
CAN_InitStructure.CAN_RFLM = DISABLE;
CAN_InitStructure.CAN_AWUM = DISABLE;
CAN_InitStructure.CAN_TTCM = DISABLE;
CAN_InitStructure.CAN_ABOM = DISABLE;
CAN_Init(CAN1, &CAN_InitStructure);
CAN_FilterInitTypeDef CAN_FilterInitStructure;
CAN_FilterInitStructure.CAN_FilterNumber = 0;
CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;
CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000;
CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
}
void MyCAN_Transmit(CanTxMsg *TxMessage)
{
uint8_t TransmitMailbox = CAN_Transmit(CAN1, TxMessage);
uint32_t Timeout = 0;
while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok)
{
Timeout ++;
if (Timeout > 100000)
{
break;
}
}
}
uint8_t MyCAN_ReceiveFlag(void)
{
if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0)
{
return 1;
}
return 0;
}
void MyCAN_Receive(CanRxMsg *RxMessage)
{
CAN_Receive(CAN1, CAN_FIFO0, RxMessage);
}
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"
uint8_t KeyNum;
CanTxMsg TxMsgArray[] = {
/* StdId ExtId IDE RTR DLC Data[8] */
{0x555, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x12345678, CAN_Id_Extended, CAN_RTR_Data, 4, {0xAA, 0xBB, 0xCC, 0xDD}},
{0x666, 0x00000000, CAN_Id_Standard, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
{0x000, 0x0789ABCD, CAN_Id_Extended, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
};
uint8_t pTxMsgArray = 0;
CanRxMsg RxMsg;
int main(void)
{
OLED_Init();
Key_Init();
MyCAN_Init();
OLED_ShowString(1, 1, " Rx :");
OLED_ShowString(2, 1, "RxID:");
OLED_ShowString(3, 1, "Leng:");
OLED_ShowString(4, 1, "Data:");
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
MyCAN_Transmit(&TxMsgArray[pTxMsgArray]);
pTxMsgArray ++;
if (pTxMsgArray >= sizeof(TxMsgArray) / sizeof(CanTxMsg))
{
pTxMsgArray = 0;
}
}
if (MyCAN_ReceiveFlag())
{
MyCAN_Receive(&RxMsg);
if (RxMsg.IDE == CAN_Id_Standard)
{
OLED_ShowString(1, 6, "Std");
OLED_ShowHexNum(2, 6, RxMsg.StdId, 8);
}
else if (RxMsg.IDE == CAN_Id_Extended)
{
OLED_ShowString(1, 6, "Ext");
OLED_ShowHexNum(2, 6, RxMsg.ExtId, 8);
}
if (RxMsg.RTR == CAN_RTR_Data)
{
OLED_ShowString(1, 10, "Data ");
OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);
OLED_ShowHexNum(4, 6, RxMsg.Data[0], 2);
OLED_ShowHexNum(4, 9, RxMsg.Data[1], 2);
OLED_ShowHexNum(4, 12, RxMsg.Data[2], 2);
OLED_ShowHexNum(4, 15, RxMsg.Data[3], 2);
}
else if (RxMsg.RTR == CAN_RTR_Remote)
{
OLED_ShowString(1, 10, "Remote");
OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);
OLED_ShowHexNum(4, 6, 0x00, 2);
OLED_ShowHexNum(4, 9, 0x00, 2);
OLED_ShowHexNum(4, 12, 0x00, 2);
OLED_ShowHexNum(4, 15, 0x00, 2);
}
}
}
}

04-标识符过滤器-16位列表
核心代码:
修改待发送报文列表:
CanTxMsg TxMsgArray[] = {
/* StdId ExtId IDE RTR DLC Data[8] */
{0x123, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x234, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x345, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x456, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x567, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x678, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
};
配置过滤器:
CAN_FilterInitTypeDef CAN_FilterInitStructure;
CAN_FilterInitStructure.CAN_FilterNumber = 0;
CAN_FilterInitStructure.CAN_FilterIdHigh = 0x234 << 5;
CAN_FilterInitStructure.CAN_FilterIdLow = 0x345 << 5;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x567 << 5;
CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x000 << 5;
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit;
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInitStructure.CAN_FilterNumber = 1;
CAN_FilterInitStructure.CAN_FilterIdHigh = 0x123 << 5;
CAN_FilterInitStructure.CAN_FilterIdLow = 0x678 << 5;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x000 << 5;
CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x000 << 5;
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit;
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
05-标识符过滤器-16位屏蔽
核心代码:
修改待发送报文列表:
CanTxMsg TxMsgArray[] = {
/* StdId ExtId IDE RTR DLC Data[8] */
{0x100, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x101, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x1FE, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x1FF, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x200, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x201, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x2FE, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x2FF, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x310, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x311, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x31E, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x31F, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x320, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x321, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x32E, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x32F, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
};
配置过滤器:
CAN_FilterInitTypeDef CAN_FilterInitStructure;
CAN_FilterInitStructure.CAN_FilterNumber = 0;
CAN_FilterInitStructure.CAN_FilterIdHigh = 0x200 << 5;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = (0x700 << 5) | 0x10 | 0x8;
CAN_FilterInitStructure.CAN_FilterIdLow = 0x320 << 5;
CAN_FilterInitStructure.CAN_FilterMaskIdLow = (0x7F0 << 5) | 0x10 | 0x8;
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit;
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
06-标识符过滤器-32位列表
核心代码:
修改待发送报文列表:
CanTxMsg TxMsgArray[] = {
/* StdId ExtId IDE RTR DLC Data[8] */
{0x123, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x234, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x345, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x456, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x12345678, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x0789ABCD, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
};
配置过滤器:
CAN_FilterInitTypeDef CAN_FilterInitStructure;
CAN_FilterInitStructure.CAN_FilterNumber = 0;
uint32_t ID1 = 0x123 << 21;
CAN_FilterInitStructure.CAN_FilterIdHigh = ID1 >> 16;
CAN_FilterInitStructure.CAN_FilterIdLow = ID1;
uint32_t ID2 = (0x12345678u << 3) | 0x4;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = ID2 >> 16;
CAN_FilterInitStructure.CAN_FilterMaskIdLow = ID2;
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
07-标识符过滤器-32位屏蔽
核心代码:
修改待发送报文列表:
CanTxMsg TxMsgArray[] = {
/* StdId ExtId IDE RTR DLC Data[8] */
{0x000, 0x12345600, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x12345601, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x123456FE, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x123456FF, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x0789AB00, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x0789AB01, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x0789ABFE, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x0789ABFF, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
};
配置过滤器:
CAN_FilterInitTypeDef CAN_FilterInitStructure;
CAN_FilterInitStructure.CAN_FilterNumber = 0;
uint32_t ID = (0x12345600u << 3) | 0x4;
CAN_FilterInitStructure.CAN_FilterIdHigh = ID >> 16;
CAN_FilterInitStructure.CAN_FilterIdLow = ID;
uint32_t Mask = (0x1FFFFF00u << 3) | 0x4 | 0x2;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = Mask >> 16;
CAN_FilterInitStructure.CAN_FilterMaskIdLow = Mask;
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
08-标识符过滤器-只要遥控帧
核心代码:
修改待发送报文列表:
CanTxMsg TxMsgArray[] = {
/* StdId ExtId IDE RTR DLC Data[8] */
{0x123, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x234, 0x00000000, CAN_Id_Standard, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
{0x345, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x456, 0x00000000, CAN_Id_Standard, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
{0x000, 0x12345600, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x12345601, CAN_Id_Extended, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
{0x000, 0x123456FE, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x123456FF, CAN_Id_Extended, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
{0x000, 0x0789AB00, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x0789AB01, CAN_Id_Extended, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
{0x000, 0x0789ABFE, CAN_Id_Extended, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}},
{0x000, 0x0789ABFF, CAN_Id_Extended, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
};
配置过滤器:
CAN_FilterInitTypeDef CAN_FilterInitStructure;
CAN_FilterInitStructure.CAN_FilterNumber = 0;
uint32_t ID = 0x2;
CAN_FilterInitStructure.CAN_FilterIdHigh = ID >> 16;
CAN_FilterInitStructure.CAN_FilterIdLow = ID;
uint32_t Mask = 0x2;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = Mask >> 16;
CAN_FilterInitStructure.CAN_FilterMaskIdLow = Mask;
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
本节课我们继续学习CAN通信的代码实现。首先,让我们预览一下最终的程序现象。
本节目标: 编写两个程序:
- 09_中断接收: 功能与
03_标准格式_扩展格式_数据帧_远程帧相同,但将接收方式由查询改为中断。 - 10_数据传输策略: 演示三种不同的CAN数据传输策略。该程序需要两个设备配合运行,一个运行发送方代码,一个运行接收方代码。
程序一:09_中断接收
-
代码基础:
- 基于
03_标准格式_扩展格式_数据帧_远程帧工程修改。 main.c核心逻辑与03代码基本一致,实现自发自收测试四种帧类型。
- 基于
-
关键修改 - 中断配置:
- 在CAN初始化配置中,启用了中断。
- 中断源:
CAN_IT_FMP0(当接收FIFO 0中有新报文时触发中断)。 - 中断服务程序(ISR): 位于文件底部。当中断发生时:
- 调用
MyCAN_ReceiveMsg(&myCanRxMsg)接收报文到结构体myCanRxMsg中。 - 置位标志位
canRxFlag = 1,表示有新报文到达。 - (注:也可直接在ISR中处理数据,但通常ISR应尽量简短,置标志位在主循环处理是常见做法)
- 调用
- 主循环处理: 检测
canRxFlag是否置位。若置位,则判断并显示myCanRxMsg结构体内容,最后清除标志位。
-
关于中断接收的说明:
- 本示例局限性: 对于单帧接收,使用中断方式与查询方式在效率上差异不大,且中断方式增加了复杂度。本示例的主要目的是演示CAN中断的配置和使用方法。
- 中断接收的优势场景: 当需要接收多帧组成的数据包时,中断接收的优势显现。例如:
- 在中断服务程序(ISR)中,每收到一帧数据就将其暂存到缓冲区。
- 当收到完整数据包的所有帧后,再置位标志位通知主程序处理。
- 这种方式类似于STM32串口接收不定长数据包,避免了主程序轮询的开销,提高了系统响应效率。
-
实验现象: 与03代码完全相同。按下按键,设备自发自收四种测试帧(标准/扩展格式,数据/远程帧),并在OLED上正确显示接收到的帧信息。
程序二:10_数据传输策略
本程序演示三种常见的CAN数据传输策略,需要两个设备配合:
- 设备A (发送方): 运行
10_发送方_数据传输策略代码。 - 设备B (接收方): 运行
10_接收方_数据传输策略代码。
重要配置: 两个设备的CAN外设均工作在正常模式 (CAN_Mode_Normal)。
发送方代码 (10_发送方_数据传输策略) - 功能
-
策略一:周期性发送
- 初始化一个定时器,定时周期为100毫秒。
- 在定时器中断服务程序(ISR)中置位标志位
Timing = 1。 - 主循环检测
Timing:- 若置位,则更新待发送数据
txMsgTiming.data[]。 - 调用
MyCAN_Transmit(&txMsgTiming)发送该数据帧 (预设ID为0x100)。 - 在OLED第二行显示发送的数据。
- 清除
Timing。
- 若置位,则更新待发送数据
-
策略二:事件触发发送
- 使用按键模拟触发事件。按键按下时置位标志位
triggerFlag = 1。 - 主循环检测
triggerFlag:- 若置位,则更新待发送数据
txMsgTrigger.data[]。 - 调用
MyCAN_Transmit(&txMsgTrigger)发送该数据帧 (预设ID为0x200)。 - 在OLED第三行显示发送的数据。
- 清除
triggerFlag。
- 若置位,则更新待发送数据
- 使用按键模拟触发事件。按键按下时置位标志位
-
策略三:远程请求/响应
- 发送方不会主动发送该数据帧。
- 发送方持续检查是否收到请求:
- 请求方式一 (远程帧): 如果接收到ID为
0x300的远程帧,置位标志位requestFlag = 1。 - 请求方式二 (数据帧): 如果接收到ID为
0x3FF的数据帧,也置位标志位requestFlag = 1(此方式非标准,但可灵活实现)。
- 请求方式一 (远程帧): 如果接收到ID为
- 主循环检测
requestFlag:- 若置位,则更新待发送数据
txMsgRequest.data[]。 - 调用
MyCAN_Transmit(&txMsgRequest)发送该数据帧 (预设ID为0x300)。 - 在OLED第四行显示发送的数据。
- 清除
requestFlag。
- 若置位,则更新待发送数据
接收方代码 (10_接收方_数据传输策略) - 功能
-
发送请求:
- 按键1 (K1) 按下: 发送一个ID为
0x300的远程帧 (请求ID0x300对应的数据)。 - 按键2 (K2) 按下: 发送一个ID为
0x3FF的数据帧 (模拟非标准请求信号)。
- 按键1 (K1) 按下: 发送一个ID为
-
接收与显示:
- 接收周期性数据: 如果收到ID为
0x100的数据帧,在OLED第二行显示接收到的数据。 - 接收触发数据: 如果收到ID为
0x200的数据帧,在OLED第三行显示接收到的数据。 - 接收响应数据: 如果收到ID为
0x300的数据帧 (即对请求的响应),在OLED第四行显示接收到的数据。
- 接收周期性数据: 如果收到ID为
实验现象演示 (双设备)
- 设备A (发送方) 和 设备B (接收方) 上电,连接在同一个CAN总线上。
- 周期性发送 (第二行):
- 发送方设备A每100毫秒自动发送ID
0x100数据帧。 - 接收方设备B第二行持续刷新显示接收到的
0x100数据。
- 发送方设备A每100毫秒自动发送ID
- 事件触发发送 (第三行):
- 按下发送方设备A的按键1。
- 发送方设备A立即发送ID
0x200数据帧。 - 接收方设备B第三行显示接收到的
0x200数据。
- 远程请求/响应 (第四行):
- 方式一 (远程帧请求):
- 按下接收方设备B的按键1 (K1)。
- 接收方设备B发送ID
0x300远程帧。 - 发送方设备A收到此远程帧 (
0x300),触发响应,发送ID0x300数据帧。 - 接收方设备B第四行显示接收到的
0x300响应数据。
- 方式二 (数据帧请求):
- 按下接收方设备B的按键2 (K2)。
- 接收方设备B发送ID
0x3FF数据帧 (模拟请求)。 - 发送方设备A收到此数据帧 (
0x3FF),触发响应,发送ID0x300数据帧。 - 接收方设备B第四行显示接收到的
0x300响应数据。
- 方式一 (远程帧请求):
09-中断式接收
STM32 CAN 接收功能改造指南 (查询 -> 中断)
目标: 将现有基于查询的 CAN 接收功能改造为基于中断的接收方式,使用 FIFO 0 接收中断 (CAN_RX0_IRQn)。演示中断配置流程和注意事项。
重要说明:
- 对于当前简单的单报文接收需求,查询方式已足够高效且更简单。中断方式在此场景下反而增加了复杂性。
- 本改造的主要目的是演示 STM32 CAN 中断接收的配置方法和流程。
- 中断接收的优势在以下场景更明显:
- 数据帧种类和数量繁多。
- 需要处理组合数据包。
- 要求极低的接收延迟。
- 系统负载高,需要及时响应 CAN 报文。
步骤 1:创建工程副本
步骤 2:修改 my_can.c 模块 (核心改造)
-
添加全局变量 (存储接收数据和标志位):
// 在文件顶部合适位置 (通常在 include 之后,函数定义之前) CanRxMsg MY_CAN_RxMessage; // 全局变量,用于在中断服务程序 (ISR) 中存储接收到的报文 volatile uint8_t MY_CAN_RxFlag = 0; // 全局标志位,用于主循环检测是否有新报文到达 (volatile 防止编译器优化) -
修改初始化函数 (
MY_CAN_Init): 添加中断配置- 在
CAN_FilterInit(...);之后,但在CAN_Init(...);使能 CAN 工作之前 (避免中断过早触发),添加中断配置代码:
// ... 之前的初始化代码 (时钟, GPIO, CAN参数, 过滤器) ... // ===== 步骤 1: 配置 CAN 外设中断源 (IER 寄存器) ===== // 使能 FIFO0 报文挂起中断 (当 FIFO0 接收到新报文时触发) CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); // 参数: CANx, Interrupt, NewState // ===== 步骤 2: 配置 NVIC (嵌套向量中断控制器) ===== // 2.1 设置中断优先级分组 (如果项目中其他地方未设置) NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 示例分组 (2位抢占, 2位响应) // 2.2 配置特定中断通道 (USB_LP_CAN1_RX0_IRQn) NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn; // 指定中断通道: CAN1 接收 FIFO0 中断 NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级 (根据系统需求设置) NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; // 子优先级 (响应优先级) NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能此中断通道 NVIC_Init(&NVIC_InitStruct); // ... 其他代码 (如果有) ...- 关键点:
CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE): 使能 CAN1 的 FIFO 0 报文挂起中断 (FMP0)。当 FIFO 0 接收到新报文 (FMP > 0) 时,此中断信号会发送给 NVIC。NVIC_PriorityGroupConfig: 设置中断优先级分组方案。确保项目中只设置一次。如果其他地方已设置,可省略此行。NVIC_Init: 配置 NVIC 中USB_LP_CAN1_RX0_IRQn通道的优先级和使能状态。中断通道名称USB_LP_CAN1_RX0_IRQn必须严格匹配 (从启动文件或设备头文件复制,不要手写或修改数字后缀)。
- 在
-
编写中断服务程序 (ISR):
- 在
my_can.c文件底部添加中断服务程序:
// CAN1 接收 FIFO0 中断服务程序 (函数名必须与启动文件中的向量表一致!) void USB_LP_CAN1_RX0_IRQHandler(void) { // 对于 STM32F103 等设备常用此名 // 1. 检查中断源标志位 (确保是 FMP0 中断触发) if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) == SET) { // 2. 从 FIFO0 读取报文到全局变量 CAN_Receive(CAN1, CAN_FIFO0, &MY_CAN_RxMessage); // 3. 设置接收完成标志位 (通知主循环) MY_CAN_RxFlag = 1; // 4. 注意: 不需要手动清除 FMP0 中断标志位! // CAN_Receive() 函数内部读取 FIFO 后,硬件会自动更新 FMP 值。 // 当 FMP 值变化 (通常减少) 时,FMP0 中断标志位状态也会相应变化。 // 显式清除 CAN_IT_FMP0 标志位是不必要的,且可能导致问题。 } // (可选: 可在此处检查其他 CAN 中断标志位,如错误中断) }- 关键点:
- 函数名必须精确匹配: 中断服务程序的名称 (
USB_LP_CAN1_RX0_IRQHandler) 必须与启动文件 (startup_stm32fxxx.s) 中为USB_LP_CAN1_RX0_IRQHandler中断向量定义的名称完全一致。务必从启动文件中复制粘贴,避免错误。 - 检查中断源 (
CAN_GetITStatus): 进入 ISR 后,首先检查是否是预期的CAN_IT_FMP0中断触发的。这是良好的实践,尤其当多个中断源共享同一个 ISR 时。 - 读取报文 (
CAN_Receive): 使用CAN_Receive从CAN_FIFO0读取一个报文,并将其存储到全局变量MY_CAN_RxMessage中。此操作会从 FIFO 中移除该报文并使FMP值减 1。 - 设置标志位 (
MY_CAN_RxFlag = 1): 设置全局标志位MY_CAN_RxFlag为 1,通知主循环有新报文需要处理。volatile关键字确保主循环能及时看到此标志位的改变。 - 无需手动清除
FMP0标志位: 重要!CAN_IT_FMP0中断标志位反映的是FMP(FIFO 报文数量) 的状态。当FMP > 0时,该标志位为SET。调用CAN_Receive读取报文后,FMP值会减少。如果FMP减到 0,FMP0标志位会自动变为RESET。如果FMP仍大于 0 (FIFO 中还有报文),该标志位保持SET,会再次触发中断,直到 FIFO 被读空。因此,绝对不要在 ISR 中使用CAN_ClearITPendingBit来清除FMP0标志位,这会破坏硬件状态机逻辑,导致报文丢失或重复中断。
- 函数名必须精确匹配: 中断服务程序的名称 (
- 在
步骤 3:修改 my_can.h 头文件
- 声明在
my_can.c中定义的全局变量,以便main.c访问:#ifndef __MY_CAN_H #define __MY_CAN_H #include "stm32f10x.h" // 或实际使用的头文件 #include "stm32f10x_can.h" // 函数声明 void MY_CAN_Init(void); void MY_CAN_Transmit(uint32_t id, uint8_t len, uint8_t *pData); // 声明中断接收使用的全局变量 (供 main.c 使用) extern CanRxMsg MY_CAN_RxMessage; extern volatile uint8_t MY_CAN_RxFlag; #endif /* __MY_CAN_H */
步骤 4:修改 main.c 主循环 (处理接收)
- 移除原有的查询接收函数调用 (
MY_CAN_ReceiveFlag和MY_CAN_Receive)。 - 改为检查全局标志位
MY_CAN_RxFlag,并在其为 1 时处理存储在MY_CAN_RxMessage中的报文:// 在主循环中 while (1) { // ... 其他代码 (例如按键检测, 发送等) ... // 检查 CAN 接收标志位 (由中断设置) if (MY_CAN_RxFlag == 1) { // 1. 清除标志位 (准备接收下一个报文) MY_CAN_RxFlag = 0; // 2. 处理接收到的报文 (使用全局变量 MY_CAN_RxMessage) // 例如: 解析 ID, 长度, 数据, 更新显示等 uint32_t receivedId; uint8_t receivedLen = MY_CAN_RxMessage.DLC; uint8_t receivedData[8]; // 提取 ID (标准或扩展) if (MY_CAN_RxMessage.IDE == CAN_Id_Standard) { receivedId = MY_CAN_RxMessage.StdId; } else { receivedId = MY_CAN_RxMessage.ExtId; } // 如果是数据帧, 复制数据 if (MY_CAN_RxMessage.RTR == CAN_RTR_Data) { for (uint8_t i = 0; i < receivedLen; i++) { receivedData[i] = MY_CAN_RxMessage.Data[i]; } } else { // 远程帧处理 (receivedLen 为 0) // ... } // 3. 根据 receivedId, receivedLen, receivedData 执行应用逻辑 // (例如: 在 LCD 上显示接收到的数据) // ... 你的处理代码 ... } // ... 其他代码 ... }
步骤 5:验证与调试
- 编译下载: 编译工程并下载到开发板。
- 测试中断触发:
- 在
USB_LP_CAN1_RX0_IRQHandler函数入口处设置断点。 - 进入调试模式,全速运行。
- 触发 CAN 报文发送 (例如按下发送按钮进行自发自收)。
- 观察调试器是否在报文到达时停在断点处。是则证明中断配置正确。
- 在
- 测试标志位与数据处理:
- 在
main.c中处理接收报文的代码处设置断点。 - 全速运行,触发报文发送。
- 观察程序是否在中断 ISR 执行后,进入主循环
if (MY_CAN_RxFlag == 1)内的处理代码。是则证明标志位传递正确。 - 检查
receivedId,receivedLen,receivedData的值是否正确。是则证明报文解析正确。
- 在
- 注意 FIFO 行为: 如果连续快速发送多个报文,由于 FIFO 深度为 3,
FMP0中断会在收到第一个报文时触发一次。ISR 读取一个报文 (FMP减 1)。如果此时 FIFO 中还有报文 (FMP > 0),FMP0标志位仍为SET,会立即再次触发中断,直到 FIFO 被读空。这是正常行为。
注意:如果在步骤 2中,中断通道USB_LP_CAN1_RX0_IRQn显示灰色,
在这里宏定义STM32F10X_MD,就解决了

10-数据传输测试_发送部分
实验目标: 本节将通过实验观察数据传输现象。以下是第一个代码的全部内容。接下来,请回到工程文件夹。
实验内容: 我们将开始本节的第二个程序——数据传输策略。该程序分为发送部分和接收部分。
工程准备: 如前所述,我们将复制工程“03”并将其重命名为“10_数据传输策略_发送部分”。首先编写发送部分代码,请打开该工程。
数据传输策略概述:
在CAN通信中,数据传输策略是指如何协调发送方和接收方之间数据交换的规则。考虑到CAN总线采用广播式发送模型(任何节点需要发送数据时都可操作总线进行广播),无论是数据帧还是远程帧均由发送方主动广播。这引出了两个关键问题:
- 发送方如何确定广播数据的时机?
- 如果接收方需要特定数据,如何向发送方发出请求?程序逻辑又应如何协调此过程?
基于CAN总线的特性,我们可以设计三种主要的数据传输策略(对应本节最初的实验现象):
-
定时传输 (Periodic Transmission):
- 描述: 数据的提供方(例如传感器)定期将数据放入数据帧中广播出去。
- 发送时机: 发送频率取决于传感器数据更新速率以及接收节点对该数据的需求频率(例如:10ms, 20ms, 100ms, 1s)。
- 接收方行为: 接收方无需主动请求,只需配置好过滤器直接接收并缓存该数据即可。
- 适用场景: 适合需要频繁使用的实时数据(如发动机转速、车轮速度)。发送方以固定频率持续发送即可。
-
触发传输 (Event-Triggered Transmission):
- 描述: 发送方仅在内部满足特定条件时才广播数据帧。
- 发送时机: 数据发送频率不固定,完全由发送方内部事件(如状态变化、报警触发)决定。例如:
- 设备检测到故障时发送报警帧,无故障则不发送。
- 数据内容发生变化时才发送,内容未变则不发送(如之前演示的按键按下触发发送)。
- 适用场景: 适合报警、通知或状态变化信息。
-
请求传输 (Request-Response Transmission):
- 描述: 由接收方主导。接收方需要数据时,先广播一个远程帧进行请求。
- 流程:
- 接收方广播远程帧(包含所需数据的ID等信息)。
- 数据提供方(发送方)收到该远程帧。
- 如果发送方拥有被请求的数据,它应随后广播对应的数据帧。
- 接收方接收该数据帧,完成请求过程。
- 适用场景: 适合接收方偶尔需要且数据量较大的情况(如调试接口请求行车电脑的配置参数)。
- 关键点与扩展:
- 远程帧本身并无特殊硬件机制。收到远程帧后回复数据帧需要在软件逻辑中实现,这是一种约定(软件层协议),而非总线强制规定。发送方可以选择不回复。
- 数据请求不一定必须使用远程帧。完全可以用数据帧来模拟请求/响应过程。例如:
- 规定ID为100的数据帧是对ID为101的数据帧的请求。
- 提供方收到ID=100的数据帧后,回复ID=101的数据帧。
- 数据帧请求的优势: 数据帧可以在其数据段中携带具体的请求参数(例如,请求特定配置项),而远程帧不具备此能力。因此,在实际应用中,远程帧使用相对较少,数据帧请求更为灵活。
- 多请求问题:
- 场景:多个节点同时请求同一数据(发送帧ID和类型相同的远程帧或数据帧请求帧)。
- 仲裁机制: CAN总线仲裁基于ID优先级。如果多个节点发送完全相同的帧(ID和所有位都相同),在仲裁域它们会同步发送且不会检测到冲突(因为回读的位与发送的位一致),因此可以成功发送。但是:
- 问题与建议: 虽然理论上允许,但让多个节点发送完全相同的请求帧会造成总线带宽浪费,且所有请求节点会同时收到回复数据帧(可能并非必要)。更优的设计是避免这种情况。如果节点都需要同一数据,应考虑采用定时传输(所有节点被动接收)或设计一个主协调节点来管理请求。
总结:
以上三种传输策略的流程和适用场景已明确。接下来,我们将通过代码实现这些功能。
定时传输
实现定时传输策略(发送端)
接下来,我们将通过代码实现三种数据传输策略的功能。首先实现第一种策略:定时传输。
工程准备:
- 由于本工程是发送端,主要任务为定时发送数据帧。
- 为保持思路清晰,我们在发送端工程中清空之前的测试代码(保留必要的初始化部分)。
- 修改OLED的静态显示字符串:
- 第一行显示
TX: 标识此设备为发送方。 - 第二行显示
Tim: 用于显示定时传输的数据。 - 第三行显示
Tri: 用于显示触发传输的数据(后续实现)。 - 第四行显示
Req: 用于显示请求传输的数据(后续实现)。
- 第一行显示
代码实现(发送端):
- 定义定时发送的数据帧结构体:
CAN_TxMessageTypeDef TxMsg_Timing = { .StdId = 0x100, // 定时传输数据帧ID (标准帧,0x100,优先级可调整) .ExtId = 0x00, // 扩展ID (标准帧未使用) .IDE = CAN_Id_Standard, // 标识符类型:标准帧 .RTR = CAN_RTR_DATA, // 帧类型:数据帧 .DLC = 4, // 数据长度:4字节 .Data = {0x11, 0x22, 0x33, 0x44} // 初始测试数据 }; - 主循环发送逻辑 (阻塞版 - 待优化):
while (1) { // 变换测试数据 (实际应用应读取传感器数据) TxMsg_Timing.Data[0]++; TxMsg_Timing.Data[1]++; TxMsg_Timing.Data[2]++; TxMsg_Timing.Data[3]++; // 发送定时数据帧 if (HAL_CAN_AddTxMessage(&hcan, &txHeader_timing, TxMsg_Timing.Data, &txMailbox) != HAL_OK) { // 发送错误处理 } // 在OLED第二行显示发送的数据 (Data[0]-Data[3]) OLED_ShowHexNum(2, 5, TxMsg_Timing.Data[0], 2); // 行2, 列5, 显示2位Hex OLED_ShowHexNum(2, 8, TxMsg_Timing.Data[1], 2); // 行2, 列8 OLED_ShowHexNum(2, 11, TxMsg_Timing.Data[2], 2); // 行2, 列11 OLED_ShowHexNum(2, 14, TxMsg_Timing.Data[3], 2); // 行2, 列14 // 注意:屏幕空间有限,未显示帧ID Delay_ms(100); // 阻塞延时100ms (确定发送频率,但会阻塞主循环) } - 解决阻塞问题 (使用定时器中断):
- 添加定时器模块: 从提供的
6-1_定时器定时中断工程中复制timer.c和timer.h文件到本工程的SYSTEM文件夹。在IDE中添加这些文件到工程。 - 包含头文件并初始化定时器: 在
main.c中包含timer.h,并在main函数初始化部分调用Timer_Init()。 - 修改定时器配置 (在
timer.c中): 调整TIM_Prescaler和TIM_Period参数,使定时器溢出时间 = 100ms (例如,原72MHz/7200/10000 = 1s,改为 72MHz/7200/1000 ≈ 100ms)。 - 定义标志位: 在
main.c全局变量区域定义volatile uint8_t TimingFlag= 0;。 - 修改定时器中断服务函数 (在
timer.c中):void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) { TimingFlag = 1; TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } } - 优化主循环 (非阻塞版):
while (1) { if (TimingFlag== 1) { // 检查定时发送标志 TimingFlag= 0; // 清除标志 // 变换测试数据 TxMsg_Timing.Data[0]++; TxMsg_Timing.Data[1]++; TxMsg_Timing.Data[2]++; TxMsg_Timing.Data[3]++; MyCAN_Transmit(&TxMsg_Request); // 在OLED第二行显示发送的数据 OLED_ShowHexNum(2, 5, TxMsg_Timing.Data[0], 2); OLED_ShowHexNum(2, 8, TxMsg_Timing.Data[1], 2); OLED_ShowHexNum(2, 11, TxMsg_Timing.Data[2], 2); OLED_ShowHexNum(2, 14, TxMsg_Timing.Data[3], 2); } // 此处可添加其他任务(如处理触发传输、请求传输、接收等) }
- 添加定时器模块: 从提供的
- 配置CAN模式: 确保发送端CAN控制器工作模式设置为
CAN_MODE_NORMAL(正常模式),以便与接收端物理通信。
编译、下载与初步测试 (发送端):
- 编译发送端工程。
- 使用ST-Link将程序下载到设备一。
- 观察设备一的OLED屏幕,第二行 (
Tim) 应持续刷新递增的十六进制数据,表明数据帧正以约100ms间隔发送。
实现定时传输策略(接收端)
- 工程准备:
- 回到工程文件夹,复制
03_基础工程(或类似基础工程),重命名为100_数据传输策略_接收部分。 - 打开接收端工程。
- 清空之前的测试代码。
- 修改OLED静态显示字符串:
- 第一行显示
RX: 标识此设备为接收方。 - 第二行显示
Tim: 用于显示接收到的定时传输数据。 - 第三行显示
Tri: (预留) - 第四行显示
Req: (预留)
- 第一行显示
- 回到工程文件夹,复制
- 代码实现 (接收端主循环):
CanRxMsg RxMsg; while (1) { if (MyCAN_ReceiveFlag()) { MyCAN_Receive(&RxMsg); // 检查是否为数据帧 (RTR == DATA) if (RxMsg.RTR == CAN_RTR_Data) { /*收到定时数据帧*/ if (RxMsg.StdId == 0x100 && RxMsg.IDE == CAN_Id_Standard) { OLED_ShowHexNum(2, 5, RxMsg.Data[0], 2); OLED_ShowHexNum(2, 8, RxMsg.Data[1], 2); OLED_ShowHexNum(2, 11, RxMsg.Data[2], 2); OLED_ShowHexNum(2, 14, RxMsg.Data[3], 2); } } // 可在此处添加对其他帧(触发帧、远程请求帧)的过滤和处理 } // 此处可添加其他任务 } - 配置CAN模式: 确保接收端CAN控制器工作模式也设置为
CAN_MODE_NORMAL。
编译、下载与联合测试:
- 编译接收端工程。
- 使用另一个ST-Link将程序下载到设备二。
- 将发送端设备(设备一)和接收端设备(设备二)连接到同一物理CAN总线上(注意终端电阻)。
- 给两个设备上电。
- 观察现象:
- 发送端设备一OLED第二行 (
TX Tim) 持续刷新递增的十六进制数据。 - 接收端设备二OLED第二行 (
RX Tim) 应显示与发送端完全一致的十六进制数据,且刷新频率相同。 - 这表明定时传输策略成功实现:发送端以固定间隔(100ms)广播数据帧,接收端成功接收并显示该数据。
- 发送端设备一OLED第二行 (
优化定时频率与总线资源说明
- 调整发送频率: 若要改变定时传输的频率,只需修改发送端定时器初始化代码中的
TIM_Prescaler和TIM_Period参数。例如,将定时器中断周期设置为1000ms,则发送频率变为1Hz。 - 总线资源占用提醒: 定时传输会持续占用总线带宽。发送频率越高、数据帧越长,占用带宽越多。
- 优先级设计的重要性:
- 帧ID决定了其在CAN总线仲裁中的优先级(ID值越小,优先级越高)。
- 关键问题: 如果最高优先级(最小ID)的帧配置了过高的发送频率并持续发送,理论上会完全阻塞低优先级帧的发送机会,因为低优先级帧在仲裁阶段始终会退让。
- 解决方案: 这不是CAN总线设计的缺陷,而是系统设计者的责任:
- 仔细规划不同数据帧的ID(优先级)和发送频率。
- 确保高优先级帧的发送间隔足够大,为低优先级帧留出总线空闲时间窗口。
- 采用合理的带宽分配方案,避免单一高优先级帧垄断总线。
总结(定时传输部分):
至此,我们完成了定时传输策略在发送端和接收端的代码实现、测试,并解决了主循环阻塞问题。同时,强调了合理设计帧优先级和发送频率对于保证CAN总线公平性的重要性。接下来,可以继续实现触发传输和请求传输策略。
触发传输
实现触发传输策略
代码位置: 在发送端工程中,位于定时传输代码段之后。
核心概念: 触发传输策略的核心是事件驱动。发送方仅在检测到特定事件(如按键按下、传感器报警、状态变化)时才广播数据帧。这与定时传输的周期性发送形成鲜明对比。我们将使用按键模拟触发事件。
发送端实现:
-
定义触发传输数据结构与变量:
// 定义触发传输数据帧结构体 (位于全局变量区域) CAN_TxMessageTypeDef txMessage_trigger = { .StdId = 0x200, // 触发传输数据帧ID (标准帧,0x200) .ExtId = 0x00, .IDE = CAN_Id_Standard, .RTR = CAN_RTR_DATA, .DLC = 4, .Data = {0x55, 0x66, 0x77, 0x88} // 初始测试数据 }; volatile uint8_t TriggerFlag= 0; // 触发发送标志位 (volatile 确保中断/主循环可见性) uint8_t key_num = 0; // 存储按键键值 -
设置触发源 (按键检测):
// 在主循环开始处或适当位置添加按键检测 while (1) { // ... (定时传输标志位检查和处理代码) // 1. 检测按键 (假设 Key_GetNum() 返回按下的键值,0 表示无按键) key_num = Key_GetNum(); // 2. 判断按键按下 (例如 KEY1 按下) if (key_num == 1) { // 假设 KEY1 键值为 1 TriggerFlag= 1; // 设置触发发送标志 } // ... (后续触发发送处理代码) } -
触发发送处理逻辑:
// ... (按键检测代码之后) // 3. 检查触发发送标志 if (TriggerFlag== 1) { TriggerFlag= 0; // 清除标志,准备下一次触发 // 4. 更新测试数据 (实际应用应基于触发事件更新数据) txMessage_trigger.Data[0]++; txMessage_trigger.Data[1]++; txMessage_trigger.Data[2]++; txMessage_trigger.Data[3]++; // 5. 发送触发数据帧 // if (HAL_CAN_AddTxMessage(&hcan, &txHeader_trigger, txMessage_trigger.Data, &txMailbox) != HAL_OK) { // 发送错误处理 // } // 6. 在OLED第三行 (Tri) 显示发送的数据 OLED_ShowHexNum(3, 5, txMessage_trigger.Data[0], 2); OLED_ShowHexNum(3, 8, txMessage_trigger.Data[1], 2); OLED_ShowHexNum(3, 11, txMessage_trigger.Data[2], 2); OLED_ShowHexNum(3, 14, txMessage_trigger.Data[3], 2); }
编译、下载与初步测试 (仅发送端):
- 编译并下载更新后的发送端代码到设备一。
- 观察设备一OLED:
- 第二行 (
TX Tim):应持续刷新(定时传输数据)。 - 第三行 (
TX Tri):初始显示55 66 77 88。
- 第二行 (
- 按下设备一的按键 (KEY1)。
- 观察现象:
- 第三行 (
TX Tri) 显示的数据应递增一次(如变为56 67 78 89)。 - 每次按下按键,第三行数据应递增更新一次。
- 注意: 此时接收设备二尚未更新代码,故不会显示触发帧数据。同时,长时间按住按键会导致主循环阻塞,影响定时传输的显示刷新(此问题后续视频专门解决)。
- 第三行 (
接收端实现:
- 修改接收端代码 (在接收端工程的主循环中):
while (1) { //、、、 // 新增:检查是否为触发传输数据帧 (ID 0x200 的数据帧) if (RxMsg.StdId == 0x200 && RxMsg.IDE == CAN_Id_Standard) { // 接收到触发传输数据帧 // 在OLED第三行 (RX Tri) 显示接收到的数据 (rxData[0]-rxData[3]) OLED_ShowHexNum(3, 5, rxData[0], 2); OLED_ShowHexNum(3, 8, rxData[1], 2); OLED_ShowHexNum(3, 11, rxData[2], 2); OLED_ShowHexNum(3, 14, rxData[3], 2); } // 可在此处添加对其他帧(如后续的请求帧)的处理 } }
联合测试 (发送端 + 接收端):
- 编译并下载更新后的接收端代码到设备二。
- 确保两个设备连接在同一CAN总线上并上电。
- 观察现象:
- 第二行 (
RX Tim/TX Tim): 两端持续同步刷新(定时传输正常)。 - 第三行初始状态 (
RX Tri/TX Tri): 两端初始显示55 66 77 88(或上次触发后的值)。
- 第二行 (
- 按下发送端 (设备一) 的按键 (KEY1)。
- 观察现象:
- 发送端 (设备一) 第三行 (
TX Tri): 显示的数据递增一次。 - 接收端 (设备二) 第三行 (
RX Tri): 几乎同时 显示与发送端 完全一致 的递增后数据。 - 每次按键按下,两端第三行数据同步更新一次。
- 发送端 (设备一) 第三行 (
定时传输 vs. 触发传输总结:
-
定时传输 (Periodic):
- 发送方行为: 严格按固定时间间隔(如 100ms)持续、自动广播数据帧。
- 接收方行为: 被动接收并更新数据。数据刷新率由发送方频率决定。
- 适用场景: 需要高刷新率、持续监控的数据(如转速、速度)。
- 实验现象: OLED第二行数据持续、规律地变化。
-
触发传输 (Event-Triggered):
- 发送方行为: 仅在特定事件发生(如按键按下)时广播数据帧。发送时刻不确定,取决于事件发生时间。
- 接收方行为: 被动接收。仅在事件发生时收到新数据并更新显示。
- 适用场景: 报警、状态变化通知、用户指令响应等非周期性事件。
- 实验现象: OLED第三行数据仅在按键按下时变化一次。两次按键之间数据保持静止。
关键区别: 定时传输是时间驱动(到时间就发),重在规律性;触发传输是事件驱动(有事发生才发),重在及时响应事件。两者在数据更新频率和驱动源上本质不同。
请求传输
实现请求传输策略
请求传输策略的核心是请求/响应模型,由接收端(数据请求方)主导。整个过程分为两个主要阶段:
- 请求阶段 (接收端发起): 接收端广播一个请求帧(可以是远程帧或特定数据帧)。
- 响应阶段 (发送端执行): 发送端(数据提供方)接收到有效的请求帧后,广播对应的数据帧作为响应。
接下来,我们将分别在发送端(响应方)和接收端(请求方)实现此策略。
发送端实现 (响应方 - 设备一)
发送端的主要任务是:
-
监听并识别接收端发来的请求帧(远程帧或特定数据帧)。
-
根据识别到的请求,设置标志位。
-
在主循环中检查标志位,若置位则发送对应的响应数据帧。
-
定义数据结构与变量:
// 定义响应数据帧结构体 (位于全局变量区域) CAN_TxMessageTypeDef TxMsg_Request= { .StdId = 0x300, // 响应数据帧ID (标准帧,0x300) .ExtId = 0x00, .IDE = CAN_Id_Standard, .RTR = CAN_RTR_DATA, .DLC = 4, .Data = {0xAA, 0xBB, 0xCC, 0xDD} // 初始响应数据 }; volatile uint8_t RequestFlag= 0; // 请求响应标志位 (volatile 确保中断/主循环可见性) CAN_RxHeaderTypeDef rxHeader; // 接收帧头 uint8_t rxData[8]; // 接收数据缓冲区 (可选,用于读取请求帧数据段) -
接收请求帧并设置标志位 (在主循环中):
while (1) { /*请求发送*/ if (MyCAN_ReceiveFlag()) { MyCAN_Receive(&RxMsg); if (RxMsg.IDE == CAN_Id_Standard && RxMsg.RTR == CAN_RTR_Remote && RxMsg.StdId == 0x300) { RequestFlag = 1; } if (RxMsg.IDE == CAN_Id_Standard && RxMsg.RTR == CAN_RTR_Data && RxMsg.StdId == 0x3FF) { RequestFlag = 1; } } } }- 关键点: 接收端发送的请求帧(无论是远程帧
0x300还是数据帧0x3FF)都会导致发送端设置RequestFlag= 1,触发对0x300数据帧的响应。数据帧请求的优势在于其数据段 (rxData) 可携带请求参数。
- 关键点: 接收端发送的请求帧(无论是远程帧
-
发送响应数据帧 (在主循环中):
// ... (接收请求帧代码之后) // 3. 检查请求响应标志 if (RequestFlag== 1) { RequestFlag= 0; // 清除标志,准备下一次响应 // 4. (可选) 更新响应数据 (例如递增、基于请求参数生成等) txMessage_request.Data[0]++; txMessage_request.Data[1]++; txMessage_request.Data[2]++; txMessage_request.Data[3]++; // 5. 在OLED第四行 (Req) 显示发送的响应数据 OLED_ShowHexNum(4, 5, txMessage_request.Data[0], 2); OLED_ShowHexNum(4, 8, txMessage_request.Data[1], 2); OLED_ShowHexNum(4, 11, txMessage_request.Data[2], 2); OLED_ShowHexNum(4, 14, txMessage_request.Data[3], 2); }
编译、下载与初步测试 (仅发送端):
- 编译并下载更新后的发送端代码到设备一。
- 观察设备一OLED第四行 (
TX Req):初始显示AA BB CC DD。此时不会有变化,因为尚未收到请求帧。
接收端实现 (请求方 - 设备二)
接收端的主要任务是:
-
提供用户接口(如按键)来发起请求。
-
根据用户操作,发送预先定义好的请求帧(远程帧
0x300或数据帧0x3FF)。 -
接收并显示发送端响应的数据帧 (
ID=0x300)。 -
定义请求帧结构体 (位于全局变量区域):
// 定义远程请求帧结构体 (ID=0x300) CAN_TxMessageTypeDef txMessage_request_remote = { .StdId = 0x300, // 远程请求帧ID (标准帧) .ExtId = 0x00, .IDE = CAN_ID_STD, .RTR = CAN_RTR_REMOTE, // 帧类型: 远程帧 .DLC = 0, // 远程帧DLC通常为0 (但CAN标准允许>0) .Data = {0} // 数据段通常不使用 }; // 定义数据请求帧结构体 (ID=0x3FF) CAN_TxMessageTypeDef txMessage_request_data = { .StdId = 0x3FF, // 数据请求帧ID (标准帧) .ExtId = 0x00, .IDE = CAN_ID_STD, .RTR = CAN_RTR_DATA, // 帧类型: 数据帧 .DLC = 4, // 可携带请求参数 (示例长度) .Data = {0x01, 0x02, 0x03, 0x04} // 示例请求参数 (可根据需要修改) }; uint8_t key_num = 0; // 存储按键键值 -
发起请求 (按键扫描与发送):
while (1) { // ... (接收并显示定时、触发、请求响应帧的代码 - 见下一步) // 1. 检测按键 (假设 Key_GetNum() 返回按下的键值,0 表示无按键) key_num = Key_GetNum(); // 2. 根据按键发送不同的请求帧 keyNum = Key_GetNum(); if (KeyNum == 1) { MyCAN_Transmit(&TxMsg_Request_Remote); } if (KeyNum == 2) { MyCAN_Transmit(&TxMsg_Request_Data); } // ... (其他任务) } -
接收并显示响应数据帧:
CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; while (1) { // ... (按键扫描和发送请求帧代码) /*接收部分*/ if (MyCAN_ReceiveFlag()) { MyCAN_Receive(&RxMsg); if (RxMsg.RTR == CAN_RTR_Data) { /*收到定时数据帧*/ if (RxMsg.StdId == 0x100 && RxMsg.IDE == CAN_Id_Standard) { OLED_ShowHexNum(2, 5, RxMsg.Data[0], 2); OLED_ShowHexNum(2, 8, RxMsg.Data[1], 2); OLED_ShowHexNum(2, 11, RxMsg.Data[2], 2); OLED_ShowHexNum(2, 14, RxMsg.Data[3], 2); } /*收到触发数据帧*/ if (RxMsg.StdId == 0x200 && RxMsg.IDE == CAN_Id_Standard) { OLED_ShowHexNum(3, 5, RxMsg.Data[0], 2); OLED_ShowHexNum(3, 8, RxMsg.Data[1], 2); OLED_ShowHexNum(3, 11, RxMsg.Data[2], 2); OLED_ShowHexNum(3, 14, RxMsg.Data[3], 2); } /*收到请求数据帧*/ if (RxMsg.StdId == 0x300 && RxMsg.IDE == CAN_Id_Standard) { OLED_ShowHexNum(4, 5, RxMsg.Data[0], 2); OLED_ShowHexNum(4, 8, RxMsg.Data[1], 2); OLED_ShowHexNum(4, 11, RxMsg.Data[2], 2); OLED_ShowHexNum(4, 14, RxMsg.Data[3], 2); } } } }
联合测试 (发送端 + 接收端):
- 编译并下载更新后的接收端代码到设备二。
- 确保两个设备连接在同一CAN总线上并上电。
- 观察初始状态:
- 设备一 (发送端/响应方) OLED:
- 第二行 (
TX Tim):持续刷新 (定时传输)。 - 第三行 (
TX Tri):静止或显示上次触发值。 - 第四行 (
TX Req):显示AA BB CC DD(初始响应数据)。
- 第二行 (
- 设备二 (接收端/请求方) OLED:
- 第二行 (
RX Tim):持续刷新 (与发送端同步)。 - 第三行 (
RX Tri):静止或显示上次触发值。 - 第四行 (
RX Req):显示00 00 00 00或上次响应值。
- 第二行 (
- 设备一 (发送端/响应方) OLED:
- 测试请求传输 (按键在接收端 - 设备二):
- 按下设备二的 KEY1:
- 设备二发送一个 ID=
0x300的远程帧(请求)。 - 设备一收到此远程帧,设置
request_flag=1。 - 设备一主循环检测到
request_flag=1,发送 ID=0x300的数据帧(响应),并递增其数据,在第四行 (TX Req) 显示新值 (如AB BC CD DE)。 - 设备二收到 ID=
0x300的数据帧,在其第四行 (RX Req) 显示与设备一相同的新数据。
- 设备二发送一个 ID=
- 按下设备二的 KEY2:
- 设备二发送一个 ID=
0x3FF的数据帧(请求,携带参数01 02 03 04)。 - 设备一收到此数据帧,设置
request_flag=1(并可选择解析参数01 02 03 04来定制响应)。 - 设备一主循环检测到
request_flag=1,发送 ID=0x300的数据帧(响应),再次递增其数据,在第四行 (TX Req) 显示更新值 (如AC BD CE DF)。 - 设备二收到 ID=
0x300的数据帧,在其第四行 (RX Req) 显示设备一发送的更新数据。
- 设备二发送一个 ID=
- 按下设备二的 KEY1:
三种传输策略现象总结:
| 策略 | 驱动源 | 发送端行为 | 接收端行为 | OLED 行 (示例) | 实验现象 |
|---|---|---|---|---|---|
| 定时传输 | 时间 (定时器中断) | 周期性地、自动广播数据帧 (ID=0x100)。无视接收端状态。 |
被动接收并显示。数据按固定频率刷新。 | 第二行 (Tim) | 两端第二行持续、同步刷新 (无需用户干预)。 |
| 触发传输 | 事件 (发送端按键按下) | 仅在事件发生时广播数据帧 (ID=0x200)。(如按键按下)。 |
被动接收并显示。数据仅在事件发生时刷新。 | 第三行 (Tri) | 按下发送端按键时,两端第三行同步刷新一次。按键间数据静止。 |
| 请求传输 | 请求 (接收端按键按下) | 仅在收到有效请求帧后广播响应数据帧 (ID=0x300)。 |
主动发起请求 (按KEY1/KEY2),被动接收并显示响应数据帧。 | 第四行 (Req) | 按下接收端按键 (KEY1或KEY2) 时,接收端第四行随后刷新一次 (显示响应数据)。发送端第四行在发送响应时刷新一次。 |
关键区别:
- 定时传输: 由时间驱动,发送方主导,周期性发送,接收方被动持续接收。
- 触发传输: 由发送方内部事件驱动,发送方主导,非周期性发送,接收方被动事件性接收。
- 请求传输: 由接收方需求驱动 (请求),交互式过程 (一去一回),发送方按需响应,接收方主动请求、被动接收响应。
至此,三种主要的CAN总线数据传输策略(定时、触发、请求)均已实现并通过实验验证。
数据传输部分_发送端总代码:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"
#include "Timer.h"
uint8_t KeyNum;
uint8_t TimingFlag;
uint8_t TriggerFlag;
uint8_t RequestFlag;
CanTxMsg TxMsg_Timing = {
.StdId = 0x100,
.ExtId = 0x00000000,
.IDE = CAN_Id_Standard,
.RTR = CAN_RTR_Data,
.DLC = 4,
.Data = {0x11, 0x22, 0x33, 0x44}
};
CanTxMsg TxMsg_Trigger = {
.StdId = 0x200,
.ExtId = 0x00000000,
.IDE = CAN_Id_Standard,
.RTR = CAN_RTR_Data,
.DLC = 4,
.Data = {0x11, 0x22, 0x33, 0x44}
};
CanTxMsg TxMsg_Request = {
.StdId = 0x300,
.ExtId = 0x00000000,
.IDE = CAN_Id_Standard,
.RTR = CAN_RTR_Data,
.DLC = 4,
.Data = {0x11, 0x22, 0x33, 0x44}
};
CanRxMsg RxMsg;
int main(void)
{
OLED_Init();
Key_Init();
MyCAN_Init();
Timer_Init();
OLED_ShowString(1, 1, "Tx");
OLED_ShowString(2, 1, "Tim:");
OLED_ShowString(3, 1, "Tri:");
OLED_ShowString(4, 1, "Req:");
while (1)
{
/*定时发送*/
if (TimingFlag == 1)
{
TimingFlag = 0;
TxMsg_Timing.Data[0] ++;
TxMsg_Timing.Data[1] ++;
TxMsg_Timing.Data[2] ++;
TxMsg_Timing.Data[3] ++;
MyCAN_Transmit(&TxMsg_Timing);
OLED_ShowHexNum(2, 5, TxMsg_Timing.Data[0], 2);
OLED_ShowHexNum(2, 8, TxMsg_Timing.Data[1], 2);
OLED_ShowHexNum(2, 11, TxMsg_Timing.Data[2], 2);
OLED_ShowHexNum(2, 14, TxMsg_Timing.Data[3], 2);
}
/*触发发送*/
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
TriggerFlag = 1;
}
if (TriggerFlag == 1)
{
TriggerFlag = 0;
TxMsg_Trigger.Data[0] ++;
TxMsg_Trigger.Data[1] ++;
TxMsg_Trigger.Data[2] ++;
TxMsg_Trigger.Data[3] ++;
MyCAN_Transmit(&TxMsg_Trigger);
OLED_ShowHexNum(3, 5, TxMsg_Trigger.Data[0], 2);
OLED_ShowHexNum(3, 8, TxMsg_Trigger.Data[1], 2);
OLED_ShowHexNum(3, 11, TxMsg_Trigger.Data[2], 2);
OLED_ShowHexNum(3, 14, TxMsg_Trigger.Data[3], 2);
}
/*请求发送*/
if (MyCAN_ReceiveFlag())
{
MyCAN_Receive(&RxMsg);
if (RxMsg.IDE == CAN_Id_Standard &&
RxMsg.RTR == CAN_RTR_Remote &&
RxMsg.StdId == 0x300)
{
RequestFlag = 1;
}
if (RxMsg.IDE == CAN_Id_Standard &&
RxMsg.RTR == CAN_RTR_Data &&
RxMsg.StdId == 0x3FF)
{
RequestFlag = 1;
}
}
if (RequestFlag == 1)
{
RequestFlag = 0;
TxMsg_Request.Data[0] ++;
TxMsg_Request.Data[1] ++;
TxMsg_Request.Data[2] ++;
TxMsg_Request.Data[3] ++;
MyCAN_Transmit(&TxMsg_Request);
OLED_ShowHexNum(4, 5, TxMsg_Request.Data[0], 2);
OLED_ShowHexNum(4, 8, TxMsg_Request.Data[1], 2);
OLED_ShowHexNum(4, 11, TxMsg_Request.Data[2], 2);
OLED_ShowHexNum(4, 14, TxMsg_Request.Data[3], 2);
}
}
}
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TimingFlag = 1;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
数据传输部分_发接收端总代码:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"
uint8_t KeyNum;
CanTxMsg TxMsg_Request_Remote = {
.StdId = 0x300,
.ExtId = 0x00000000,
.IDE = CAN_Id_Standard,
.RTR = CAN_RTR_Remote,
.DLC = 0,
.Data = {0x00}
};
CanTxMsg TxMsg_Request_Data = {
.StdId = 0x3FF,
.ExtId = 0x00000000,
.IDE = CAN_Id_Standard,
.RTR = CAN_RTR_Data,
.DLC = 0,
.Data = {0x00}
};
CanRxMsg RxMsg;
int main(void)
{
OLED_Init();
Key_Init();
MyCAN_Init();
OLED_ShowString(1, 1, "Rx");
OLED_ShowString(2, 1, "Tim:");
OLED_ShowString(3, 1, "Tri:");
OLED_ShowString(4, 1, "Req:");
while (1)
{
/*请求部分*/
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
MyCAN_Transmit(&TxMsg_Request_Remote);
}
if (KeyNum == 2)
{
MyCAN_Transmit(&TxMsg_Request_Data);
}
/*接收部分*/
if (MyCAN_ReceiveFlag())
{
MyCAN_Receive(&RxMsg);
if (RxMsg.RTR == CAN_RTR_Data)
{
/*收到定时数据帧*/
if (RxMsg.StdId == 0x100 && RxMsg.IDE == CAN_Id_Standard)
{
OLED_ShowHexNum(2, 5, RxMsg.Data[0], 2);
OLED_ShowHexNum(2, 8, RxMsg.Data[1], 2);
OLED_ShowHexNum(2, 11, RxMsg.Data[2], 2);
OLED_ShowHexNum(2, 14, RxMsg.Data[3], 2);
}
/*收到触发数据帧*/
if (RxMsg.StdId == 0x200 && RxMsg.IDE == CAN_Id_Standard)
{
OLED_ShowHexNum(3, 5, RxMsg.Data[0], 2);
OLED_ShowHexNum(3, 8, RxMsg.Data[1], 2);
OLED_ShowHexNum(3, 11, RxMsg.Data[2], 2);
OLED_ShowHexNum(3, 14, RxMsg.Data[3], 2);
}
/*收到请求数据帧*/
if (RxMsg.StdId == 0x300 && RxMsg.IDE == CAN_Id_Standard)
{
OLED_ShowHexNum(4, 5, RxMsg.Data[0], 2);
OLED_ShowHexNum(4, 8, RxMsg.Data[1], 2);
OLED_ShowHexNum(4, 11, RxMsg.Data[2], 2);
OLED_ShowHexNum(4, 14, RxMsg.Data[3], 2);
}
}
}
}
}
数据传输策略与上层协议
本课程所探讨的数据传输策略(定时、触发、请求)本质上是**CAN通信的上层协议(Higher-Layer Protocol, HLP)**的重要组成部分。上层协议负责定义和规范以下核心内容:
- 标识符(ID)分配: 如何分配帧ID?不同ID代表何种信息或设备?ID的优先级如何设定?
- 数据场(Data Field)格式: 数据段中每个字节(甚至每个位)的具体含义是什么?数据如何编码(如数值、状态、命令)?
- 通信机制: 帧何时应被发送?采用何种传输策略(定时、触发、请求)?节点间如何交互(如请求/响应)?错误处理与恢复机制?
- 网络管理: 节点如何加入/退出网络?如何检测节点状态(在线/离线)?
自定义协议 vs. 标准化协议:
- 小型/个人项目: 在独立或小型项目中,开发者可以完全自主设计上层协议。你可以自由定义ID含义、数据格式和通信规则(如同本课程示例),以满足特定需求。
- 大型/多厂商系统: 当项目涉及多个设备供应商或需要集成到现有复杂系统(如汽车、工业自动化)时,各自为政的自定义协议会带来严重的兼容性问题。不同厂商按自身规则设计的设备将无法有效协同工作。
标准化协议的价值:
为了解决兼容性问题,业界制定了标准化的上层协议(如目前主流的 CANopen, J1939, DeviceNet 等)。这些协议:
- 规范通信基础:
- 严格定义ID分配策略(如预定义PDO/SDO ID范围)。
- 规定数据场格式(如定义对象字典索引、子索引、数据类型)。
- 定义标准通信模型: 提供成熟的、可互操作的通信机制,例如:
- SDO (Service Data Object): 客户端/服务器模型,用于可靠的点对点参数配置和读取(类似强大的“请求/响应”)。
- PDO (Process Data Object): 生产者/消费者模型,用于高效、实时地传输过程数据(融合了“定时”和“事件触发”策略)。
- 网络管理 (NMT): 提供统一的节点状态控制与监控。
- 确保互操作性: 遵循同一标准的设备,无论来自哪个厂商,都能无缝集成并协同工作。
- 促进开发与应用:
- 设备制造商: 遵循标准使其设备易于集成到主流系统中。
- 系统集成商/开发者: 了解并应用这些标准协议是在现有平台(如汽车、工业控制网络)上进行二次开发和功能扩展的必备基础。
总结与展望:
作为CAN总线的入门教程,我们通过实践详细讲解了物理层、数据链路层核心机制以及基础的数据传输策略(上层协议的关键部分)。我们演示了如何从零开始构建简单的自定义通信逻辑。
理解这些基础概念是至关重要的第一步。然而,要深入工业或汽车电子等实际应用领域,掌握 CANopen、J1939 等主流标准化上层协议将是您下一步学习的核心方向。它们提供了构建健壮、可互操作、可维护的复杂分布式系统的框架和工具。
更多推荐



所有评论(0)