CANN通信
CAN总线是“两根线连接所有设备,按ID优先级仲裁,用数据帧传输数据,嵌入式中通过FDCAN外设+中断+FreeRTOS队列实现‘高效接收-安全传数据-任务处理’的通信方案”。到这里,你已经掌握了CAN总线的底层原理、嵌入式实战流程,以及你项目中FDCAN相关代码的逻辑——接下来再看你项目的fdcan.c、freertos.c,就能明白“每一行代码为什么这么写,和其他模块怎么配合”了。如果后续看具
CAN总线
CAN总线(Controller Area Network,控制器局域网)是嵌入式领域最常用的“工业级通信总线”——简单说,就是给设备之间(比如汽车里的发动机、仪表盘、车灯,工业设备里的传感器、控制器)搭建的“高速、可靠、抗干扰的对话通道”。
一、为什么需要CAN总线?
在CAN总线出现前,设备间通信是“点对点”的——比如一个系统里有5个设备要互相通信,就得拉5×4=20根线,还得处理每个设备的通信协议,不仅布线复杂、成本高,还容易出故障(一根线断了,两个设备就没法通信)。
CAN总线解决了这个问题,核心优势是:
- 总线结构:所有设备都挂在“两根线”(CAN_H、CAN_L)上,像大家共用一条“高速公路”,不用点对点拉线;
- 多主通信:没有“老大”(比如不是只有一个设备能发数据),任何设备都能主动发数据,适合分布式系统;
- 抗干扰强:CAN_H和CAN_L是差分信号
- 容错性好
- 远距离+高速
二、CAN总线的核心
1. 物理层:
CAN总线的物理层核心是“两根差分线”:
- CAN_H:高电平线(正常通信时电压约3.5V);
- CAN_L:低电平线(正常通信时电压约1.5V);
- 差分电压:CAN_H - CAN_L = 2V(表示“显性电平”,对应逻辑0),CAN_H - CAN_L = 0V(表示“隐性电平”,对应逻辑1);
- 终端电阻:在总线的两端必须接一个120Ω的电阻(相当于“匹配负载”),防止信号反射导致通信出错(新手容易忘接,导致总线不稳定)。
2. 数据帧:CAN总线的“数据包”(相当于“快递包裹”)
设备之间通信,不能乱发数据,必须把数据打包成“标准格式的数据包”,这就是CAN数据帧。新手重点记“标准帧”(最常用),结构如下(用比喻拆解):
| 字段 | 作用(比喻:快递包裹的组成) | 通俗解释 |
|---|---|---|
| 起始位 | 包裹的“开头标记” | 1个显性电平(0),告诉所有设备:“我要发数据了,大家注意接收” |
| ID(11位) | 包裹的“地址+优先级”(核心!) | 11位二进制数(0~2047),既代表“数据要发给谁”,也代表“数据的优先级”——ID越小,优先级越高 |
| RTR位 | 包裹的“类型标记”(数据帧/远程帧) | 0=数据帧(带实际数据,比如“转速1000转”);1=远程帧(请求数据,比如“请告诉我你的转速”) |
| DLC位(4位) | 包裹的“快递单”:说明里面有多少数据 | 0~8,表示数据段有多少个字节(比如DLC=2,就是2个字节数据) |
| 数据段(0-8B) | 包裹里的“实际货物” | 要传输的真实数据(比如发动机转速是0x03E8,就是1000转;温度是0x28,就是40℃) |
| CRC段 | 包裹的“校验码” | 对前面的字段做校验,接收方用来判断数据有没有传错(比如路上干扰导致字节变了) |
| ACK段 | 包裹的“签收确认” | 接收方收到正确数据后,发一个“确认信号”,告诉发送方“我收到了,没问题” |
| 结束位 | 包裹的“结尾标记” | 7个隐性电平(1),告诉所有设备:“这次数据发完了” |
关键提醒:CAN总线的ID是“核心中的核心”——不仅是地址,还是优先级。比如汽车里“刹车信号”的ID是0x001(优先级最高),“空调温度”的ID是0x100(优先级低),当两者同时发数据时,刹车信号会“插队”先传,这就是后面要讲的“总线仲裁”。
3. 总线仲裁:解决“多设备同时发数据”的冲突(相当于“交通规则”)
CAN总线是“多主通信”——所有设备都能主动发数据,那如果两个设备同时发数据,会不会像路上两辆车撞在一起?
不会!因为CAN有“总线仲裁”机制,核心逻辑是:按ID优先级“抢答”,优先级高的先发,优先级低的自动避让,全程无冲突。
用比喻解释仲裁过程:
- 总线平时是“隐性电平”(相当于道路空着);
- 设备A(ID=0x001,优先级高)和设备B(ID=0x002,优先级低)同时想发数据,都开始往总线上发“起始位”(显性电平);
- 接下来发ID:设备A发第1位是0,设备B发第1位也是0(一致,继续);… 直到发ID的最后一位,设备A发0,设备B发1;
- 此时总线的电平是“显性电平”(因为A发的0是显性,B发的1是隐性,显性电平会“覆盖”隐性电平);
- 设备B发现:“我发的是1(隐性),但总线上实际是0(显性),说明有优先级比我高的设备在发数据”——于是设备B立刻停止发送,退到一边,等A发完再试;
- 设备A没发现冲突,继续把剩下的数据发完;
- A发完后,总线回到隐性电平,设备B再重新尝试发送自己的数据。
这个过程全程很快(微秒级),不会导致数据丢失,也不用像其他总线那样“等待”,这就是CAN总线适合“实时系统”(比如汽车刹车、工业控制)的关键——优先级高的紧急数据能立刻传输,不耽误。
第一步:硬件准备(CubeMX配置的核心)
STM32H7的FDCAN外设需要连接CAN总线,CubeMX里的配置,主要是这几点:
- 引脚配置:指定STM32的哪两个引脚作为CAN_H和CAN_L——这是硬件连接的基础,必须和实际接线一致;
- 波特率配置:设置CAN总线的通信速率(比如500Kbps、1Mbps)——所有挂在总线上的设备,波特率必须一致,否则没法通信(比如A设备发数据是1Mbps,B设备收数据是250Kbps,B会把数据当成乱码);
- 波特率怎么来?STM32的FDCAN外设由“时钟源”(比如APB时钟)分频得到,CubeMX会自动计算“预分频器、时间段1、时间段2”这三个参数,不用你手动算。
- 模式配置:设置FDCAN是“正常模式”(能发能收)、“只收模式”(只接收不发送)还是“环回模式”(自己发的数据自己收,用来调试);
- 过滤器配置:CAN总线所有设备都会收到数据,但我们只关心自己需要的数据(比如只接收ID=0x123的数据)——过滤器就是“筛选器”,只让符合条件的数据进入接收缓冲区,其他数据直接丢弃(减少CPU负担);
第二步:初始化FDCAN
这部分是CubeMX生成+可能手动修改的代码,核心作用是“告诉STM32的FDCAN外设:怎么通信、怎么筛选数据、怎么处理接收”,步骤拆解:
// 示例:初始化函数核心逻辑(HAL库生成)
static void MX_FDCAN1_Init(void)
{
hfdcan1.Instance = FDCAN1; // 选择FDCAN1外设(STM32H7可能有多个FDCAN)
hfdcan1.Init.ClockDivider = FDCAN_CLOCK_DIV1; // 时钟分频(1分频,用原始时钟)
hfdcan1.Init.FrameFormat = FDCAN_FRAME_CLASSIC; // 帧格式:传统CAN帧(11位ID)
hfdcan1.Init.Mode = FDCAN_MODE_NORMAL; // 正常模式(能发能收)
hfdcan1.Init.NominalPrescaler = 16; // 波特率预分频器(和时钟一起决定波特率)
hfdcan1.Init.NominalSyncJumpWidth = FDCAN_BT1_BITTIME_1; // 同步跳转宽度(时钟同步用)
hfdcan1.Init.NominalTimeSeg1 = FDCAN_BT1_BITTIME_13; // 时间段1(波特率计算参数)
hfdcan1.Init.NominalTimeSeg2 = FDCAN_BT2_BITTIME_2; // 时间段2(波特率计算参数)
// 其他配置(比如自动重发、唤醒模式等)
if (HAL_FDCAN_Init(&hfdcan1) != HAL_OK) // 初始化FDCAN外设
{
Error_Handler(); // 初始化失败就报错(比如硬件故障、配置错误)
}
// 配置过滤器:只接收ID=0x123的标准帧
FDCAN_FilterTypeDef sFilterConfig = {0};
sFilterConfig.FilterActivation = ENABLE; // 启用过滤器
sFilterConfig.FilterBank = 0; // 用第0个过滤器(STM32有多个过滤器)
sFilterConfig.FilterMode = FDCAN_FILTER_IDMASK; // 过滤模式:ID掩码模式(按ID+掩码筛选)
sFilterConfig.FilterScale = FDCAN_FILTER_SCALE_32BIT; // 过滤器宽度:32位
sFilterConfig.FilterId1 = 0x123 << 21; // 要筛选的ID(11位ID左移21位,符合FDCAN寄存器格式)
sFilterConfig.FilterMask1 = 0x1FFFFFFF; // 掩码(全1表示严格匹配ID=0x123)
sFilterConfig.FilterFIFOAssignment = FDCAN_FILTER_FIFO0; // 符合条件的数据放进FIFO0缓冲区
if (HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig) != HAL_OK) // 应用过滤器配置
{
Error_Handler();
}
// 启动FDCAN接收:开启FIFO0的接收中断(数据来了触发中断)
if (HAL_FDCAN_Start(&hfdcan1) != HAL_OK)
{
Error_Handler();
}
if (HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0) != HAL_OK)
{
Error_Handler();
}
}
逐行解释核心逻辑:
hfdcan1是FDCAN的“句柄”:相当于FDCAN1外设的“身份证”,后续所有操作(发数据、收数据、配置)都要通过这个句柄告诉HAL库“操作哪个FDCAN”;- 波特率配置参数:不用手动算,CubeMX会根据你设置的波特率(比如500Kbps)自动生成
NominalPrescaler等参数,只要知道“这些参数决定了通信速率,所有设备必须一致”; - 过滤器配置:为什么要配置?如果不配置,STM32会接收总线上所有数据,CPU要处理大量无用数据,导致负担过重。配置后只接收需要的数据,效率更高;
HAL_FDCAN_ActivateNotification():启用“接收中断”——当FIFO0缓冲区有新数据时,会触发中断,CPU会暂停当前任务(比如FreeRTOS任务),去执行中断服务函数(这是嵌入式里“及时接收数据”的常用方式)。
第三步:发送CAN数据
设备要发数据时,需要把数据打包成CAN数据帧,通过FDCAN外设发送到总线上,HAL库提供了HAL_FDCAN_AddMessageToTxFifoQ()函数,核心逻辑:
// 示例:手动编写的CAN发送函数
uint8_t fdcan_send_data(uint32_t id, uint8_t *data, uint8_t len)
{
FDCAN_TxHeaderTypeDef tx_header; // 发送帧的“头部配置”(ID、DLC等)
uint32_t tx_mailbox; // 发送邮箱(STM32FDCAN有多个发送邮箱,用来缓存要发的数据)
// 配置发送帧头部
tx_header.Identifier = id; // 要发送的ID(比如0x123)
tx_header.IdType = FDCAN_STANDARD_ID; // 标准帧(11位ID)
tx_header.TxFrameType = FDCAN_DATA_FRAME; // 数据帧(带实际数据)
tx_header.DataLength = len; // 数据长度(0~8字节)
tx_header.TransmitGlobalTime = DISABLE; // 不包含发送时间戳
// 把数据帧加入发送邮箱,发送到总线上
if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &tx_header, data, &tx_mailbox) == HAL_OK)
{
return 0; // 发送成功
}
else
{
return 1; // 发送失败(比如发送邮箱满了、总线忙)
}
}
关键提醒:
- 发送数据时,必须指定
Identifier(ID)和DataLength(数据长度),否则接收方没法解析; tx_mailbox是“发送邮箱”:STM32FDCAN有3个发送邮箱,当总线忙时,要发送的数据会存在邮箱里,等总线空闲了再发——如果3个邮箱都满了,HAL_FDCAN_AddMessageToTxFifoQ()会返回失败,所以要判断返回值,避免数据丢失;- 发送成功不代表接收方收到了:CAN总线的“ACK段”只是“接收方收到正确数据后确认”,但发送方不知道是哪个接收方确认的——如果需要“一对一确认”,需要在应用层自己加协议(比如接收方收到数据后,发一个“确认帧”)。
第四步:接收CAN数据
接收数据有两种方式:“查询方式”(CPU一直问“有没有数据?”,浪费CPU)和“中断方式”(数据来了主动通知CPU,高效),核心流程:
1. 中断触发条件
当FDCAN接收到符合过滤器条件的数据,并且放进了FIFO0缓冲区,就会触发FDCAN1_IRQn中断(FDCAN1的中断号),CPU会跳转到中断服务函数FDCAN1_IRQHandler()。
2. 中断服务函数
// 示例
void FDCAN1_IRQHandler(void)
{
/* USER CODE BEGIN FDCAN1_IRQn 0 */
/* USER CODE END FDCAN1_IRQn 0 */
HAL_FDCAN_IRQHandler(&hfdcan1); // HAL库的中断处理入口(自动判断中断类型)
/* USER CODE BEGIN FDCAN1_IRQn 1 */
/* USER CODE END FDCAN1_IRQn 1 */
}
这行代码的作用:HAL_FDCAN_IRQHandler(&hfdcan1)会自动读取FDCAN的中断状态寄存器,判断是“接收中断”“发送完成中断”还是“错误中断”,然后调用对应的回调函数——这是HAL库的“封装优势”,不用我们手动操作寄存器判断中断类型。
3. 接收回调函数
HAL库会在中断处理完成后,调用回调函数HAL_FDCAN_RxFifo0Callback(),我们在这个函数里处理接收的数据(比如把数据放进FreeRTOS队列,交给任务处理):
// 示例:FDCAN接收回调函数(数据来了会自动调用)
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
if ((RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) != RESET) // 确认是“新数据接收”中断
{
FDCAN_RxHeaderTypeDef rx_header; // 接收帧的头部(存储接收数据的ID、DLC等)
uint8_t rx_data[8] = {0}; // 接收数据的缓冲区(最多8字节)
// 从FIFO0缓冲区读取数据和头部信息
if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &rx_header, rx_data) == HAL_OK)
{
// 关键:中断里不能做耗时操作(比如解析数据、打印日志),要快进快出
// 所以把数据放进FreeRTOS队列,交给任务处理
xQueueSendFromISR(can_data_queue, rx_data, NULL); // 中断安全的队列发送函数
}
}
}
关键逻辑:
- 为什么用回调函数?因为HAL库把“判断中断类型、读取数据”的复杂操作封装了,我们只需要在回调函数里写“数据收到后该做什么”,不用修改中断服务函数(符合“分层设计”,方便维护);
- 为什么中断里要把数据放进队列?中断服务函数要“快进快出”——如果在中断里解析数据(比如判断数据是转速还是温度)、打印日志(USART打印很慢),会导致中断响应时间变长,其他中断(比如USART、TIM)没法及时响应,甚至导致系统崩溃;
- 为什么用
xQueueSendFromISR()而不是xQueueSend()?xQueueSend()是普通的队列发送函数,只能在FreeRTOS任务中使用;xQueueSendFromISR()是“中断安全”的队列发送函数,专门用于中断服务函数/回调函数中,能避免中断和任务同时操作队列导致的数据混乱(容易用错,导致系统不稳定)。
第五步:FreeRTOS任务处理CAN数据
核心逻辑:中断把数据放进队列后,FreeRTOS的“CAN数据处理任务”从队列中取出数据,进行解析、处理(比如控制LED、通过USART打印、下发指令给其他外设),示例代码:
//FreeRTOS CAN数据处理任务
void can_process_task(void *argument)
{
uint8_t received_data[8] = {0}; // 存储从队列取出的数据
for (;;) // 任务是“无限循环”(FreeRTOS任务不能返回,否则会崩溃)
{
// 从队列中读取数据:如果队列有数据,就取出;如果没有,任务进入阻塞态(不占用CPU)
if (xQueueReceive(can_data_queue, received_data, portMAX_DELAY) == pdPASS)
{
uint8_t temp_int = received_data[0];
uint8_t temp_dec = received_data[1];
float temperature = temp_int + temp_dec / 100.0f;
// 处理数据
char log_buf[32];
sprintf(log_buf, "Received temperature: %.2f℃\r\n", temperature);
HAL_UART_Transmit(&huart1, (uint8_t *)log_buf, strlen(log_buf), 100);
// 或者控制LED:温度超过50℃,LED亮;否则灭
if (temperature > 50.0f)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
}
else
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
}
}
}
}
关键逻辑解释:
- 任务是“无限循环”:FreeRTOS的任务是“最小执行单元”,一旦创建就会一直运行,所以用
for(;;)循环——如果任务返回,会导致FreeRTOS调度器出错; xQueueReceive()的阻塞态:portMAX_DELAY表示“一直等,直到队列有数据”,此时任务会进入“阻塞态”,CPU会去执行其他任务(比如LED任务),不会浪费CPU资源(新手容易写成while(1)查询队列,导致CPU占用100%);- 数据解析的灵活性:CAN数据帧的数据段是“原始字节”,具体代表什么意思(比如是温度、转速、指令),需要你和总线上的其他设备约定好“应用层协议”——比如约定“ID=0x123的数据,第0-1字节是温度,第2-3字节是转速”,这就是“自定义协议”,新手要注意:CAN总线只负责“传输数据”,不负责“解释数据”,解释数据需要应用层协议。
六、CAN总线开发中容易犯的错误
- 波特率不一致:总线上的所有设备(比如STM32、传感器、控制器)波特率必须一样(比如都是500Kbps),否则会出现“发得出但收不到”“收到乱码”——容易只改STM32的波特率,忘了改其他设备;
- 终端电阻没接或接多了:总线两端必须各接一个120Ω电阻,接少了(比如只接一端)会导致信号反射,通信不稳定;接多了(比如中间设备也接)会导致总线电平异常,没法通信;
- 中断里做耗时操作:比如在HAL_FDCAN_RxFifo0Callback()里用HAL_UART_Transmit()打印数据(USART打印很慢,耗时毫秒级),会导致中断响应超时,其他中断丢失——正确做法是用队列把数据传给任务处理;
- 队列操作函数用错:中断里用xQueueSend()而不是xQueueSendFromISR(),会导致FreeRTOS内核崩溃(因为xQueueSend()不是中断安全的);
- 过滤器配置错误:比如ID左移位数不对(FDCAN的ID存储格式要求11位ID左移21位)、掩码设置错误(比如想匹配ID=0x123,掩码设为0x00000000,会接收所有数据)——可以先禁用过滤器(接收所有数据),调试通了再配置过滤器。
昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链
更多推荐


所有评论(0)