CAN 总线最初由博世于1980年代为汽车行业开发,能够简化复杂的布线网络,还确保可靠和安全的数据传输。
1.CAN技术解释
CAN网络中的每个节点,都是平等的,没有主次之分,这一点和SPI和I2C不同。每个节点都可以在需要的时候收发数据,同时也在监听来自其他节点的数据传输。CAN还具有如下优势:
- 降低网络复杂性:单个 CAN 网络可以取代多条通信线路,从而降低复杂性和成本;
- 抗干扰性强:CAN 总线采用抗电磁干扰设计,即使在高干扰环境中也能确保可靠和稳定的通信。
1.1 CAN总线的拓扑物理结构
CAN分为高速和低速,如下图所示:
CAN总线拓扑结构就像一个地铁,每个站点就是节点。每个节点可以和线路上的其他节点进行通信,有如下物理特点:
双绞线结构
成对的电线相互缠绕,可最大限度地减少电磁干扰。想象一下这样一个场景,CAN 电缆安装在电源附近,例如车辆中的交流发电机,这会产生明显的电磁噪声。通常这种噪声可能会干扰和扭曲通信信号。然而,CAN 总线在 CAN 高电平和 CAN 低电平线路之间使用了巧妙的差分信号系统。这意味着任何影响一条线路的干扰信号也会以类似的方式影响另一条线路,从而允许系统相互抵消。因此,即使存在大量噪声,传输的数据也能保持清晰且未损坏。此特性与以太网等系统相同。
CAN High和CAN Low线
根据ISO 11898(高速CAN)和ISO 11519(低速CAN)标准,CAN_H和CAN_L的电压和显/隐性(逻辑0和逻辑1)是不一样的。
类型 | 状态 | CAN_H电压范围 | CAN_L电压范围 | 差分电压 | 传输速率 |
---|---|---|---|---|---|
高速CAN | 显性(逻辑0) | 2.75V ~ 4.5V | 0.5V ~ 2.75V | ≥1.5V | 1Mbps |
隐性(逻辑1) | 2.0V ~ 3.0V | 2.0V ~ 3.0V | ≈0V (±0.05V) | ||
低速CAN | 显性(逻辑0) | ≥3.0V | ≤2.0V | ≥1.5V | 10-125Kbps |
隐性 (逻辑1) | ≤1.0V | ≥4.0V | ≤-2.0V |
1.2 CAN总线终端电阻作用
高速CAN总线需在物理两端各接一个120Ω电阻,形成总电阻60Ω的网络,具有如下作用:
- 提高抗干扰能力。若无终端电阻,隐性无压差时,外部很小的干扰就可能导致进入显性。
- 快速进入隐性状态。加个终端电阻,加速寄生电容放电。
- 提高信号质量。
一、提高抗干扰能力
CAN总线有“显性”和“隐性”两种状态,“显性”有压差,“隐性”无压差,由CAN收发器决定。下图是一个CAN收发器的典型内部结构图,CANH、CANL连接总线。
总线显性时,收发器内部Q1、Q2导通,CANH、CANL之间产生压差;隐性时,Q1、Q2截止,CANH、CANL处于无源状态,压差为0。
总线若无负载,隐性时差分电阻阻值很大,内部的MOS管属于高阻态,外部的干扰只需要极小的能量即可令总线进入显性(一般的收发器显性门限最小电压仅500mV)。这个时候如果有差模干扰过来,总线上就会有明显的波动,而这些波动没有地方能够吸收掉他们,就会在总线上创造一个显性位出来。所以为提升总线隐性时的抗干扰能力,可以增加一个差分负载电阻,且阻值尽可能小,以杜绝大部分噪声能量的影响。然而,为了避免需要过大的电流总线才能进入显性,阻值也不能过小。
二、快速进入隐性状态
加电阻前,可以看到由高变低时,不够快:
加上终端电阻:
三、提高信号质量
高频信号在传输线末端会因阻抗突变产生反射波,反射与原始信号叠加形成振铃(信号振荡),导致电平失真。终端电阻通过匹配传输线特征阻抗(约120Ω),吸收反射能量,避免波形畸变。
带振铃:
加上电阻后:
为何120Ω?
什么是阻抗?在电学中,常把对电路中电流所起的阻碍作用叫做阻抗。阻抗单位为欧姆,常用Z表示,是一个复数Z= R+i( ωL–1/(ωC))。具体说来阻抗可分为两个部分,电阻(实部)和电抗(虚部)。其中电抗又包括容抗和感抗,由电容引起的电流阻碍称为容抗,由电感引起的电流阻碍称为感抗。这里的阻抗是指Z的模。
任何一根线缆的特征阻抗都可以通过实验的方式得出。线缆的一端接方波发生器,另一端接一个可调电阻,并通过示波器观察电阻上的波形。调整电阻阻值的大小,直到电阻上的信号是一个良好的无振铃的方波,此时的电阻值可以认为与线缆的特征阻抗一致。
采用两根汽车使用的典型线缆,将它们扭制成双绞线,就可根据上述方法得到特征阻抗大约为120Ω,这也是CAN标准推荐的终端电阻阻值,所以这个120Ω是测出来的,不是算出来的,都是根据实际的线束特性进行计算得到的。当然在ISO 11898-2这个标准里面也是有定义的。
不接120Ω后果?
不接终端电阻的后果
- 通信故障,信号反射导致电平波动,可能误触发显性位,引发CRC校验错误或数据帧丢失
- 波形异常,示波器观测显示信号下降沿变缓、振铃明显,隐性状态恢复时间延长,影响高速通信(如CAN FD)的时序容限
- 抗干扰能力下降,隐性状态易受外部噪声干扰,增加误码率。
为什么功率还要选0.25W?
这个就要结合一些故障状态也计算,汽车ECU的所有接口都需要考虑短路到电源和短路到地的情况,所以我们也需要考虑CAN总线的节点短路到电源的情况,根据标准需要考虑短路到18V的情况,假设CANH短路到18V,电流会通过终端电阻流到CANL上,而CANL内部由于限流的原因,最大注入电流为50mA(TJA1145的规格书上标注),这时候120Ω电阻的功率就是50mA50mA120Ω=0.3W。考虑到高温情况下的降额,终端电阻的功率就是0.5W。
1.3 CAN帧结构
总的来说CAN协议帧有5种类型:
整体的结构如下:
- 帧开始 (SOF): 单bit表示帧开始。
- 仲裁域(Arbitration):包括确定帧优先级的消息标识符 (ID) 和远程请求的远程传输请求 (RTR) 位。
- 控制域 :包括 DLC(数据长度代码),指示数据的字节数。
- 数据字段:包含帧的实际数据,最多 8 个字节。
- CRC字段 :用于错误检查,包括 CRC 序列和 CRC 分隔符。
- ACK: 包括确认位和确认分隔符。
- EOF: 由 7 个隐性位(1)组成,标记帧的结束。
- 间歇场:帧间隔专用,由3个隐性位组成。
- 用于错误或过载的其他字段。
1.3.1 数据帧 - 标准帧(11位ID)
1.3.2 数据帧 - 扩展帧(29位ID)
1.3.2 扩展帧与标准帧使用场景
使用场景 | ID类型 | ID设计思路 |
---|---|---|
简单网络 | 标准帧 | 预留固定ID段给不同节点,如0x100-0x1FF |
复杂网络 | 扩展帧 | 设计ID高位为设备类型/功能码,低位为节点编号 |
多厂家系统 | 扩展帧 | 高位区分厂家ID,低位区分设备和功能 |
广播与点对点混合 | 标准或扩展帧 | 广播使用固定ID的标准帧,点对点用不同ID的扩展帧 |
1.3.3 实际场景举例
比如有多个板卡,每个板卡带有板卡类型+板卡ID,他们之间通过CAN总线连接到一起,板卡可以进行广播发送和点对点发送,此时CAN帧消息结构可以这样定义:
标准帧用来发送广播消息,一个板子可以给其他板子广播发送数据。消息类别中可以用来区分消息的优先级,可以支持4种优先级、16种板卡类型和32个板子互联:
点对点使用扩展帧,ID的前11bit和标准帧一样,但后18bit中可以描述目标板卡的信息:
这样设计很方便的进行消息的优先级控制以及板卡的消息过滤功能,比如只过滤指定板卡类型的消息,甚至是特定的消息!
1.4 总线仲裁机制
当多个节点可能同时尝试发送数据时,需要一种机制,保证总线上只有一个节点能继续发送,其他节点延迟发送。
仲裁保证高优先级消息优先传输,且总线无碰撞。
【核心原理】
- 基于CAN报文的ID字段进行仲裁。 CAN总线采用差分信号,逻辑“0”(Dominant)比逻辑“1”(Recessive)电平优先。即ID数值越小,优先级越高(ID低 = 优先级高)
- 节点在发送ID位时,同时监听总线上的实际电平。
- 如果节点发送“1”,但读总线实际是“0”,说明有高优先级节点发送“0”,当前节点立即停止发送(放弃仲裁)。如果发送1,读总线是1,继续发送。直到只剩下一个节点继续发送,该节点获得总线控制权。
- CAN控制器自动完成仲裁,无需软件干预
1.5 总线标识符ID过滤
CAN 总线使用独特的过滤机制来处理消息。这种方法增强了其效率和灵活性。过滤在多个节点同时通信的复杂系统中特别有用。它确保重要信息到达适当的接收者,而不会让其他节点因非必要数据而变得混乱。
正如前面所述,可以配置过滤器,指定匹配ID中的部分字段,比如该字段可能描述目标设备ID,实现当目标设备ID配置为自身时,只接收发往自己的单点通信的功能。
基本概念:
- 滤波器(Filter):硬件模块,负责检测报文ID是否匹配。
- 掩码(Mask):决定ID中哪些位参与匹配,哪些位忽略。
- FIFO0 和 FIFO1:两个独立的接收FIFO,过滤后的报文存放位置。
【注意】CAN控制器的滤波器硬件设计只支持对ID的匹配,不支持根据数据位内容过滤。
两种模式:掩码模式和列表模式。
- 掩码模式
过滤器ID与掩码组合,匹配过滤器ID中掩码指定的位。即报文ID 与滤波器ID 按位进行比较,掩码为1的位置必须匹配,掩码为0的位置忽略。
公式如下:
(Received_ID & Mask) == (Filter_ID & Mask)
例如,掩码为0x7FF(0111 1111 1111),过滤器ID为0x100(0001 0000 0000 ),只能匹配0x100;
掩码为0x700(0111 0000 0000),过滤器ID为0x100(0001 0000 0000 ),对应上后,001和掩码匹配上了,只能匹配(001 0000 0000 ~ 001 1111 1111)即0x100~0x1FF。
- 列表模式
最简单,就是和指定的ID直接匹配。
1.6 CAN FD
CAN FD(CAN with Flexible Data-Rate) 是 CAN 总线协议的增强版本,由 Bosch 公司在 2012 年提出,意在突破传统 CAN 总线的数据传输速率和数据长度限制。
特性 | 传统 CAN | CAN FD |
---|---|---|
最大数据长度 | 8 字节 | 最多 64 字节 |
数据传输速率 | 通常最高 1 Mbps | 数据段最高可达 8 Mbps(甚至更高) |
传输效率 | 较低(受限于帧长和速率) | 更高,支持更大数据量和更高速率 |
兼容性 | 传统设备 | 兼容传统 CAN,需支持 FD 的设备 |
STM32F7、STM32H7等部分型号MCU内置CAN FD模块,适合嵌入式开发。
Jetson AGX Orin支持CAN FD。
2.STM32中的CAN通信
2.1 CAN外设介绍
2.1.1 CAN 框图
CAN2的(只有互联型设备才有CAN2)。
本次使用的STM32F103C8T6只有CAN1。
简单看一下,发送的数据会进入3个发送邮箱进行数据发送,接收的数据首先进过滤器,然后放到FIFO中,每个FIFO有3个接收邮箱。
2.1.2 发送过程
发送的数据通过CPU写入到邮箱中,然后给出请求发送的命令,然后发送和接收控制器就会等待总线空闲,然后自动把这个报文广播到总线上。
为何需要3个邮箱?
简单来说就是3级缓存,减少发送的CPU等待。如果全满了那就没办法了。
当两个或者三个邮箱都有数据要发送,那么会先发哪一个呢?手册中是这么写的:
实际上,如果 hcan.Init.TransmitFifoPrioritys (即MX中Transmit Fifo Priority选项)设置为了Enable,表示第二种方法,按发送请求次序确定。
如果是Disable,则按消息的ID优先级来确认,当ID相同时,按邮箱号确定。
2.1.3 接收过程
接收到的报文,首先会进入过滤器,根据过滤规则,被存储在过滤器指定的3级邮箱深度的FIFO中(即过滤器绑定的FIFIO0还是FIFO1中。一个过滤器都没有则会进入FIFO0。)。FIFO完全由硬件来管理,从而节省了CPU的处理负荷,简化了软件并保证了数据的一致性。应用程序只能通过读取FIFO输出邮箱,来读取FIFO中最先收到的报文。
FIFO的3个邮箱都是满的,再收到一个数据会怎样?
- 如果禁用了FIFO锁定功能(CAN_MCR寄存器的RFLM位被清’0’),那么FIFO中最后收到的报文就被新报文所覆盖。这样,最新收到的报文不会被丢弃掉(但实际上被覆盖的报文是丢掉的),此时发送端应该不会重传。对应ReceiveFifoLocked 为DISABLE;
- 如果启用了FIFO锁定功能(CAN_MCR寄存器的RFLM位被置’1’),那么新收到的报文就被丢弃,软件可以读到FIFO中最早收到的3个报文。此时发送端应该会重传吧(不确定)。对应ReceiveFifoLocked 为ENABLE。
接收相关的中断
- 一旦往FIFO存入一个报文,会产生一个中断请求。
- 当FIFO变满时(即第3个报文被存入),会产生一个满中断请求。
- 在溢出的情况下,会产生一个溢出中断请求。
2.2 MX配置
基本配置:
- 波特率计算
Prescaler Time Quanta in Bit Segment 1和2是为了计算波特率使用的。
CAN挂在APB1总线上,这里配置的是36MHz。
位段1(BS1,Bit Segment 1):采样点之前的时间段
位段2(BS2,Bit Segment 2):采样点之后的时间段
最终的波特率是下面公式计算出来的:
36M/分频系数/(BS1 + BS2 + 1)
我们目标是配置出1Mbit/s的速率,那么可以使用小工具进行计算,CAN波特率计算工具:
gpt得知,采样点在75%-87.5% 左右(靠近比特末尾)可确保数据稳定,这里选择了BS1:14 BS2:3这一条。
-
Basic Parameters(基本参数)
- Automatic Retransmission 是否允许自动重传错误帧,建议开启
- Transmit Fifo Priority 发送 FIFO 优先级模式,启用后按 FIFO 顺序发送,禁用则按报文标识符优先级发送。建议启用,保证发送的顺序。
- Receive Fifo Locked 设置为ENABLE,接收FIFO满了,会将报文丢弃(对端应该会重传吧)。建议启用。
-
Test Mode
有四种选择:
- Normal 正常模式
生产环境或实际应用中的标准工作模式,多机通信。 - Loop Back 环回模式
软件调试,验证发送与接收功能。CAN 控制器内部环回发送和接收数据,不发送到总线。在环回模式下,bxCAN在内部把Tx输出回馈到Rx输入上,而完全忽略CANRX引脚的实际状态。在环回模式下CAN内核忽略确认错误(在数据/远程帧的确认位时刻,不检测是否有显性位),但会进过滤器。 - Silent 静默模式
控制器只接收总线数据,但不发送任何信号或应答。监听模式,用于被动监听总线数据。 - Silent Loop Back 静默环回模式
软件调试,验证接收功能,无干扰总线。结合环回和静默,内部环回数据,不送出总线,也不应答
- Normal 正常模式
中断配置:
- CAN1 TX interrupts
触发条件:发送完成中断。
用途:确认报文已成功发送,释放发送缓冲区,可以继续发送新的数据。
应用:在中断服务函数中通常清标志位,通知上层发送完成。 - CAN1 RX0 interrupts
触发条件:接收 FIFO 0 有新报文到达。
用途:读取 FIFO 0 中的 CAN 报文,进行数据处理。
特点:FIFO 0 常用于普通消息接收,响应速度较快。 - CAN1 RX1 interrupt
触发条件:接收 FIFO 1 有新报文到达。
用途:读取 FIFO 1 中的 CAN 报文,适合区分不同消息队列。
特点:可以用来区分高优先级和低优先级消息,或不同来源的消息。 - CAN1 SCE interrupt
触发条件:CAN 状态变化或错误事件。
包含事件:
错误计数器变化(Error Passive / Active 状态)
总线错误(Bit Error、Stuff Error 等)
总线关闭(Bus-Off)
唤醒事件
用途:监控 CAN 总线健康状态,及时响应和处理错误,保证通信可靠。
【创建代码】
创建了can.c,其中的初始化:
void MX_CAN_Init(void)
{hcan.Instance = CAN1;hcan.Init.Prescaler = 2;hcan.Init.Mode = CAN_MODE_NORMAL;hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;hcan.Init.TimeSeg1 = CAN_BS1_14TQ;hcan.Init.TimeSeg2 = CAN_BS2_3TQ;hcan.Init.TimeTriggeredMode = DISABLE;hcan.Init.AutoBusOff = DISABLE;hcan.Init.AutoWakeUp = DISABLE;hcan.Init.AutoRetransmission = ENABLE;hcan.Init.ReceiveFifoLocked = DISABLE;hcan.Init.TransmitFifoPriority = ENABLE;if (HAL_CAN_Init(&hcan) != HAL_OK){Error_Handler();}
}
2.3 过滤器详解
接收到的数据,首先会经过过滤器,符合条件的才会进入FIFO。
还得掏出下面这个图:
中文版:
英文版:
【这里推荐英文版的,中文版的过滤器编号和过滤器组把人搞懵逼】 中文版中的过滤器编号并不和程序中的FilterBank
字段对应,和FilterBank
字段对应的中文版中是过滤器组!
简单看一下,前面提到了,过滤器有两种模式:掩码
和纯ID
,这张图中,按照FilterScale
的不同,还得分16位还是32位,所有总共有4种寄存器配置方式:
- 1个32位的针对标识符屏蔽过滤器(即掩码模式)
- 2个32位的只针对标识符的过滤器(即纯ID模式),也就是你只能配置2个ID列表方式的过滤器,只能过滤2个ID号
- 2个16位的针对标识符屏蔽过滤器(即掩码模式)
- 4个32位的只针对标识符的过滤器(即纯ID模式)
**【注意-易错点】**这里的1个、2个和4个针对的是一个过滤器编号中对应了几个。比如当使用HAL_CAN_ConfigFilter
设置一个过滤器时,可以给它配置2个32为列表标识的过滤器。这样推断,STM32F1X只有一个CAN,一共可配置14个过滤器,每个过滤器可以按16位还是32位进行 1个、2个和4个这样的配置!
2.3.1 32位掩码模式的过滤器配置
注意STF1/4/7都支持32bit配置!
这里的Mapping实际就是我们需要配置的位。为了简化配置,我们可以提炼为一个结构体,可支持对掩码和ID的配置:
typedef struct CanFilterCfg
{uint32_t b1Reserve : 1; //最低位未用 uint32_t b1RTR : 1; //RTR过滤位 uint32_t b1IDE : 1; //扩展/标准帧标识 可用来过滤扩展帧还标准帧 uint32_t b18EXIDL : 18;//扩展帧ID,可以根据实际业务再进行拆分uint32_t b11STID : 11;//标准帧ID,可以根据实际业务再进行拆分
}TCanFilterCfg; //正好32位typedef union CanFilterDesc
{TCanFilterCfg Cfg;uint32_t Val;
}UCanFilterDesc;
比如只过滤标准帧:
CAN_FilterTypeDef sFilterConfig;UCanFilterDesc uFilterMask; //过滤掩码选定UCanFilterDesc uFilterId; //过滤目标选定//掩码,只对扩展帧/标准帧b1IDE进行标记,这里对只关心的字段按bit赋值1即可uFilterMask.Val = 0; //初始化uFilterMask.Cfg.b1IDE = 1; //实际ID,只设置标准帧标记uFilterId.Val = 0;uFilterId.Cfg.b1IDE = 0; //标准帧,1是扩展帧sFilterConfig.FilterBank = 0; sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; //32位sFilterConfig.FilterIdHigh = ((uFilterId.Val >> 16) & 0x0000FFFF); //高16bitsFilterConfig.FilterIdLow = (uFilterId.Val & 0x0000FFFF);sFilterConfig.FilterMaskIdHigh = ((uFilterMask.Val >> 16) & 0x0000FFFF);sFilterConfig.FilterMaskIdLow = (uFilterMask.Val & 0x0000FFFF);sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;sFilterConfig.FilterActivation = ENABLE;sFilterConfig.SlaveStartFilterBank = 14; //固定配置,指定第二个CAN的Bank从哪个编号开始if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK){return -1;}
2.3.2 16位掩码模式的过滤器配置(踩坑)
和程序中配置字段的对应关系:
同样的,Mapping抽象为一个位域结构。不同的是,因为是16位的,一个过滤器Bank可对应两个,FilterIdHigh对应一个,FilterIdLow 对应另一个。
typedef struct CanFilterCfg
{uint32_t b3EXID : 3; //扩展帧ID的高[17:15]位 uint32_t b1IDE : 1; //扩展/标准帧标识 可用来过滤扩展帧还标准帧 uint32_t b1RTR : 1; //RTR过滤位 uint32_t b11STID : 11;//标准帧ID,可以根据实际业务再进行拆分
}TCanFilterCfg16; typedef union CanFilterDesc
{TCanFilterCfg16 Cfg;uint32_t Val;
}UCanFilterDesc16;
这里我们也是只过滤标准帧,扩展帧收不到了:
//CAN初始化过滤器设置
int CAN_Init()
{//过滤器配置 - 只接收标准帧CAN_FilterTypeDef sFilterConfig;UCanFilterDesc16 uFilterMask; //过滤掩码选定UCanFilterDesc16 uFilterId; //过滤目标选定//掩码,只对扩展帧/标准帧b1IDE进行标记,这里对只关心的字段按bit赋值1即可uFilterMask.Val = 0; //初始化uFilterMask.Cfg.b1IDE = 1; //实际ID,只设置标准帧标记uFilterId.Val = 0;uFilterId.Cfg.b1IDE = 0; //标准帧sFilterConfig.FilterBank = 0; sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;sFilterConfig.FilterScale = CAN_FILTERSCALE_16BIT; sFilterConfig.FilterIdHigh = uFilterId.Val; //注意这里直接赋值即可sFilterConfig.FilterIdLow = uFilterId.Val //0; 注意千万别配置为0sFilterConfig.FilterMaskIdHigh = uFilterMask.Val;sFilterConfig.FilterMaskIdLow = uFilterMask.Val;//0;sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;sFilterConfig.FilterActivation = ENABLE;//sFilterConfig.SlaveStartFilterBank = 14; //F1中这个配置也无效if (HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK){return -1;}return 0;
}
【巨坑】
下面这段代码一开始,Low都赋值为0,结果发现过滤器根本不起作用。这里还是自己理解有误,手册说的也不清楚。
像16bit的配置模式,对应两个过滤器,这其中有一个为0的话,实际上是什么都不过滤的!。
High和Low配置成一样的就好了!
建议直接用32bit的!
sFilterConfig.FilterIdHigh = uFilterId.Val; //注意这里直接赋值即可sFilterConfig.FilterIdLow = uFilterId.Val //0; 注意千万别配置为0sFilterConfig.FilterMaskIdHigh = uFilterMask.Val;sFilterConfig.FilterMaskIdLow = uFilterMask.Val;//0;
2.4 回环模式测试
mx配置中,Test Mode配置为lookback模式,进行单机测试。
初始化时,启用中断和CAN
//CAN初始化设置int CAN_Init(){//过滤器配置 - 只接收标准帧CAN_FilterTypeDef sFilterConfig;UCanFilterDesc16 uFilterMask; //过滤掩码选定UCanFilterDesc16 uFilterId; //过滤目标选定//掩码,只对扩展帧/标准帧b1IDE进行标记,这里对只关心的字段按bit赋值1即可uFilterMask.Val = 0; //初始化uFilterMask.Cfg.b1IDE = 1; //实际ID,只设置标准帧标记uFilterId.Val = 0;uFilterId.Cfg.b1IDE = 0; //标准帧sFilterConfig.FilterBank = 0; sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;sFilterConfig.FilterScale = CAN_FILTERSCALE_16BIT; sFilterConfig.FilterIdHigh = uFilterId.Val; sFilterConfig.FilterIdLow = 0; //这个不配置了sFilterConfig.FilterMaskIdHigh = uFilterMask.Val;sFilterConfig.FilterMaskIdLow = 0;sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;sFilterConfig.FilterActivation = ENABLE;//sFilterConfig.SlaveStartFilterBank = 14; //F1中这个配置也无效if (HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK){return -1;}//激活接收中断 FIFO0中只要收到消息就进中断if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK){return -1;}//使能发送MailBox if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_TX_MAILBOX_EMPTY) != HAL_OK){return -1;}//使能CAN节点收发if (HAL_CAN_Start(&hcan) != HAL_OK){return -1;}return 0;
}
2.4.1 各种中断
/* Transmit Interrupt /
#define CAN_IT_TX_MAILBOX_EMPTY ((uint32_t)CAN_IER_TMEIE) /!< 用来驱动发送流程,比如发送完成后通知程序准备加载下一帧数据。必须确保发送邮箱确实空后才写数据,否则可能导致数据覆盖。*/
/* Receive Interrupts /
#define CAN_IT_RX_FIFO0_MSG_PENDING ((uint32_t)CAN_IER_FMPIE0) /!< FIFO 0 中有新的消息进入且等待处理时触发。 中断服务程序要尽快读取消息,防止 FIFO 堆积导致溢出。/
#define CAN_IT_RX_FIFO0_FULL ((uint32_t)CAN_IER_FFIE0) /!<FIFO 0 缓冲区满,无法再接收新消息时触发。 /
#define CAN_IT_RX_FIFO0_OVERRUN ((uint32_t)CAN_IER_FOVIE0) /!< FIFO 0 出现溢出,丢失一条或多条消息时触发。 /
#define CAN_IT_RX_FIFO1_MSG_PENDING ((uint32_t)CAN_IER_FMPIE1) /!< FIFO 1 message pending interrupt /
#define CAN_IT_RX_FIFO1_FULL ((uint32_t)CAN_IER_FFIE1) /!< FIFO 1 full interrupt /
#define CAN_IT_RX_FIFO1_OVERRUN ((uint32_t)CAN_IER_FOVIE1) /!< FIFO 1 overrun interrupt */
/* Operating Mode Interrupts /
#define CAN_IT_WAKEUP ((uint32_t)CAN_IER_WKUIE) /!< CAN 控制器从休眠模式(sleep)被唤醒时触发。适用于低功耗应用,确保唤醒后重新初始化通信。 /
#define CAN_IT_SLEEP_ACK ((uint32_t)CAN_IER_SLKIE) /!< CAN 控制器进入休眠模式时触发,表示睡眠模式已被确认。确认进入低功耗状态,系统可进入节能状态。Sleep acknowledge interrupt */
/* Error Interrupts /
#define CAN_IT_ERROR_WARNING ((uint32_t)CAN_IER_EWGIE) /!< CAN 控制器检测到错误计数器达到警告阈值(如接收或发送错误计数器超过某值)。 提示总线错误增加,需关注通信质量。 /
#define CAN_IT_ERROR_PASSIVE ((uint32_t)CAN_IER_EPVIE) /!< CAN 控制器进入错误被动状态,即总线错误严重,但尚未完全失效。 /
#define CAN_IT_BUSOFF ((uint32_t)CAN_IER_BOFIE) /!< CAN 控制器进入总线关闭状态,因错误计数器超过阈值,停止总线通信。 /
#define CAN_IT_LAST_ERROR_CODE ((uint32_t)CAN_IER_LECIE) /!< 总线错误发生时触发,记录最后一次错误代码。 /
#define CAN_IT_ERR OR ((uint32_t)CAN_IER_ERRIE) /!< 总线出现任意错误时触发。 需要结合错误状态寄存器分析具体错误类型。*/
2.4.2 数据发送 - 中断方式
需使能中断:
//使能发送MailBox if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_TX_MAILBOX_EMPTY) != HAL_OK){return -1;}
发送测试消息:
//CAN初始化CAN_Init();uint8_t u8Data[8] = {1,2,3,4,5,6,7,8};//发送uint8_t u8TxBoxesNum = HAL_CAN_GetTxMailboxesFreeLevel(&hcan); //空闲发送邮箱的个数if (u8TxBoxesNum > 0){CAN_Send_STD_Frame(u8Data); //发送标准帧}u8TxBoxesNum = HAL_CAN_GetTxMailboxesFreeLevel(&hcan); //空闲发送邮箱的个数if (u8TxBoxesNum > 0){CAN_Send_EXT_Frame(u8Data); //发送扩展帧}
发送邮箱空闲中断,该中断只要有一个邮箱空闲出来,就会触发中断:
//CAN 发送邮箱发送成功中断回调
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan)
{// 投递消息到消息队列,可以发送下个消息了。。TODO
}
void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef *hcan)
{// 投递消息到消息队列,可以发送下个消息了。。TODO
}
void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef *hcan)
{// 投递消息到消息队列,可以发送下个消息了。。TODO
}
2.4.4 数据接收 - 中断方式
配置了过滤器只接标准帧后,这个回调中扩展帧就收不到了!
//CAN FIFO0的接收中断处理
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{CAN_RxHeaderTypeDef rxHeader;uint8_t rxData[8]; //接收8字节数据// 读取 FIFO0 中的一条消息if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK){// 打印ID和数据printf("Received CAN message: ID=0x%X DLC=%d\n", rxHeader.StdId, rxHeader.DLC);// 把数据放入你的消息队列。。TODO}else{// 读取失败处理,比如错误计数或重启CAN等}
}
2.5 使用经验总结
-
CAN通信还需考虑重传吗?
- CAN 硬件自带重传机制(将开关打开),大多数不需要重传。
- 广播情况下,发送时不考虑重传,数据压入发送邮箱即可认为发送成功;发送完后,由对端回复丢包情况再进行重传。(此种丢包应该是应用层丢包,比如队列溢出等等。)
-
发送邮箱状态检查很重要
- 发送前应检查发送邮箱是否空闲,避免覆盖未发送完成的报文。
- 使用
HAL_CAN_GetTxMailboxesFreeLevel()
函数可以方便获取空闲邮箱数量。
-
中断中投递消息到任务中处理
- 发送空闲中断或者接收中断中,不要直接处理数据,投递消息到任务中进行处理。
-
其他待补充
3.SocketCan
待完善。
4.CanOpen协议
待完善。