参考资料
文档资料
- STM32F103参考手册:rm0008-stm32f101xx-stm32f102xx-stm32f103xx-stm32f105xx-and-stm32f107xx
- STM32103ZE数据手册:STM32F103xC, STM32F103xD, STM32F103xE
- CAN Specification
- 瑞萨CAN入门书
视频教程
江协科技CAN总线教程
原理解析
CAN简介
-
CAN总线(Controller Area Network Bus)控制器局域网总线
-
CAN总线是由BOSCH公司开发的一种简洁易用、传输速度快、易扩展、可靠性高的串行通信总线,广泛应用于汽车、嵌入式、工业控制等领域
-
CAN总线特征:
-
两根通信线(CAN_H、CAN_L),线路少
-
差分信号通信,抗干扰能力强
-
高速CAN(ISO11898):125k~1Mbps, <40m
-
低速CAN(ISO11519):10k~125kbps, <1km
-
异步,无需时钟线,通信速率由设备各自约定
-
半双工,可挂载多设备,多设备同时发送数据时通过仲裁判断先后顺序
-
11位/29位报文ID,用于区分消息功能,同时决定优先级
-
可配置1~8字节的有效载荷
-
可实现广播式和请求式两种传输方式
-
应答、CRC校验、位填充、位同步、错误处理等特性
-
物理层
主流通信协议对比
CAN硬件电路
- 每个设备通过CAN收发器挂载在CAN总线网络上
- CAN控制器引出的TX和RX与CAN收发器相连,CAN收发器引出的CAN_H和CAN_L分别与总线的CAN_H和CAN_L相连
- 高速CAN使用闭环网络,CAN_H和CAN_L两端添加120Ω的终端电阻
- 低速CAN使用开环网络,CAN_H和CAN_L其中一端添加2.2kΩ的终端电阻
CAN电平标准
- CAN总线采用差分信号,即两线电压差(VCAN_H-VCAN_L)传输数据位
- 高速CAN规定:
- 电压差为0V时表示逻辑1(隐性电平)
- 电压差为2V时表示逻辑0(显性电平)
- 低速CAN规定:
- 电压差为-1.5V时表示逻辑1(隐性电平)
- 电压差为3V时表示逻辑0(显性电平)
CAN收发器 – TJA1050(高速CAN)
CAN物理层特性
CAN总线帧格式
CAN协议规定了以下5种类型的帧:
数据帧
[!NOTE]
图例说明:D表示对应颜色bit只能为显性电平(Dominant),R则表示只能为隐形电平(Recessive);ACK位槽,该bit周期,发送方发送隐形电平,接收方如果要回复ACK则发送显性电平
- SOF(Start of Frame):帧起始,表示后面一段波形为传输的数据位
- ID(Identify):标识符,区分功能,同时决定优先级
- RTR(Remote Transmission Request ):远程请求位,区分数据帧和遥控帧
- IDE(Identifier Extension):扩展标志位,区分标准格式和扩展格式
- SRR(Substitute Remote Request):替代RTR,协议升级时留下的无意义位
- r0/r1(Reserve):保留位,为后续协议升级留下空间
- DLC(Data Length Code):数据长度,指示数据段有几个字节
- Data:数据段的1~8个字节有效数据
- CRC(Cyclic Redundancy Check):循环冗余校验,校验数据是否正确
- ACK(Acknowledgement):应答位,判断数据有没有被接收方接收
- CRC/ACK界定符:为应答位前后发送方和接收方释放总线留下时间
- EOF(End of Frame ):帧结束,表示数据位已经传输完毕
数据帧的发展历史
CAN 1.2时期,仅存在标准格式,IDE位当时仍为保留位r1
CAN 2.0时期,ID不够用,出现了扩展格式,增加了ID的位数,为了区分标准格式与扩展格式,协议将标准格式中的r1赋予了新功能—IDE
遥控帧
遥控帧无数据段,RTR为隐性电平1,其他部分与数据帧相同
错误帧
总线上所有设备都会监督总线的数据,一旦发现“位错误”或“填充错误”或“CRC错误”或“格式错误”或“应答错误” ,这些设备便会发出错误帧来破坏数据,同时终止当前的发送设备
过载帧
当接收方收到大量数据而无法处理时,其可以发出过载帧,延缓发送方的数据发送,以平衡总线负载,避免数据丢失
帧间隔
将数据帧和遥控帧与前面的帧分离开
位填充
波形实例
标准数据帧,报文ID为0x555,数据长度1字节,数据内容为0xAA
[!NOTE]
标准格式下,r0默认为显性电平,这样设计是为了让标准格式比扩展格式有更高的优先级。
黄色高亮表示该bit为5个相同电平后的一个位填充。
CRC界定符使得发送方能够在ACK槽前提前释放总线,这样接收方能够在ACK槽处发送显性电平。
ACK界定符给接收方发送ACK后交回总线控制权留出时间,发送方随后能够发送EOF。
标准数据帧,报文ID为0x666,数据长度2字节,数据内容为0x12, 0x34
扩展数据帧,报文ID为0x0789ABCD,数据长度1字节,数据内容为0x56
标准遥控帧,报文ID为0x088,数据长度1字节,无数据内容
接收方数据采样和同步问题
- CAN总线没有时钟线,总线上的所有设备通过约定波特率的方式确定每一个数据位的时长
- 发送方以约定的位时长每隔固定时间输出一个数据位
- 接收方以约定的位时长每隔固定时间采样总线的电平,输入一个数据位
- 理想状态下,接收方能依次采样到发送方发出的每个数据位,且采样点位于数据位中心附近
数据采样可能遇到的问题
接收方以约定的位时长进行采样,但是采样点没有对齐数据位中心附近
接收方刚开始采样正确,但是时钟有误差,随着误差积累,采样点逐渐偏离
[!NOTE]
发送方和接收方的时钟都可能因自身的物理特性或环境因素导致不准的情况。
位时序
为了灵活调整每个采样点的位置,使采样点对齐数据位中心附近,CAN总线对每一个数据位的时长进行了更细的划分,分为同步段(SS)、传播时间段(PTS)、相位缓冲段1(PBS1)和相位缓冲段2(PBS2),每个段又由若干个最小时间单位(Tq,Time Quantum)构成
硬件同步(Hard Synchronization)
- 每个设备都有一个位时序计时周期,当某个设备(发送方)率先发送报文,其他所有设备(接收方)收到SOF的下降沿时,接收方会将自己的位时序计时周期拨到SS段的位置,与发送方的位时序计时周期保持同步
- 硬同步只在帧的第一个下降沿(SOF下降沿)有效
- 经过硬同步后,若发送方和接收方的时钟没有误差,则后续所有数据位的采样点必然都会对齐数据位中心附近
再同步(Resynchronization)
- 若发送方或接收方的时钟有误差,随着误差积累,数据位边沿逐渐偏离SS段,则此时接收方根据再同步补偿宽度值(SJW)通过加长PBS1段,或缩短PBS2段,以调整同步
- 再同步可以发生在第一个下降沿(SOF)之后的每个数据位跳变边沿
调整同步的规则
波特率计算
多设备同时发送遇到的问题
-
CAN总线只有一对差分信号线,同一时间只能有一个设备操作总线发送数据,若多个设备同时有发送需求,该如何分配总线资源?
-
解决问题的思路:制定资源分配规则,依次满足多个设备的发送需求,确保同一时间只有一个设备操作总线
资源分配规则一:先到先得
- 若当前已经有设备正在操作总线发送数据帧/遥控帧,则其他任何设备不能再同时发送数据帧/遥控帧(可以发送错误帧/过载帧破坏当前数据)
- 任何设备检测到连续11个隐性电平,即认为总线空闲,只有在总线空闲时,设备才能发送数据帧/遥控帧
- 一旦有设备正在发送数据帧/遥控帧,总线就会变为活跃状态,必然不会出现连续11个隐性电平,其他设备自然也不会破坏当前发送
- 若总线活跃状态其他设备有发送需求,则需要等待总线变为空闲,才能执行发送需求
资源分配规则二:非破坏性仲裁
- 若多个设备的发送需求同时到来或因等待而同时到来,则CAN总线协议会根据ID号(仲裁段)进行非破坏性仲裁,ID号小的(优先级高)取到总线控制权,ID号大的(优先级低)仲裁失利后将转入接收状态,等待下一次总线空闲时再尝试发送
- 实现非破坏性仲裁需要两个要求:
- 线与特性:总线上任何一个设备发送显性电平0时,总线就会呈现显性电平0状态,只有当所有设备都发送隐性电平1时,总线才呈现隐性电平1状态,即:
0 & X & X = 0
,1 & 1 & 1 = 1
- 回读机制:每个设备发出一个数据位后,都会读回总线当前的电平状态,以确认自己发出的电平是否被真实地发送出去了,根据线与特性,发出0读回必然是0,发出1读回不一定是1
- 线与特性:总线上任何一个设备发送显性电平0时,总线就会呈现显性电平0状态,只有当所有设备都发送隐性电平1时,总线才呈现隐性电平1状态,即:
ID号大的仲裁失利
数据位从前到后依次比较,出现差异且数据位为1的设备仲裁失利:
数据帧和遥控帧的优先级
数据帧和遥控帧ID号一样时,数据帧的优先级高于遥控帧
标准格式和扩展格式的优先级
标准格式11位ID号和扩展格式29位ID号的高11位一样时,标准格式的优先级高于扩展格式(SRR必须始终为1,以保证此要求)
错误处理
再看错误帧和过载帧
错误的种类
错误共有 5 种。多种错误可能同时发生。
- 位错误
- 填充错误
- CRC 错误
- 格式错误
- ACK 错误
错误的种类、错误的内容、错误检测帧和检测单元如表 9 所示。
错误状态的种类
- 主动错误状态的设备正常参与通信并在检测到错误时发出主动错误帧
- 被动错误状态的设备正常参与通信但检测到错误时只能发出被动错误帧
- 总线关闭状态的设备不能参与通信
- 每个设备内部管理一个TEC和REC,根据TEC和REC的值确定自己的状态
[!NOTE]
TEC:Transmit Error Count,REC:Receive Error Count
错误计数值
- 发送错误计数值和接收错误计数值根据一定的条件发生变化。错误计数值的变动条件如表 2 所示。
- 一次数据的接收和发送可能同时满足多个条件。
- 错误计数器在错误标志的第一个位出现的时间点上开始计数
波形示例
设备处于主动错误状态,发送标准数据帧,正常传输
设备处于主动错误状态,发送标准数据帧,检测到ACK错误
设备处于被动错误状态,发送标准数据帧,检测到ACK错误
STM32的CAN外设(以STM32F103为例)
STM32的芯片中具有bxCAN控制器(Basic Extended CAN),它支持CAN协议2.0A 和2.0B Active标准。(CAN2.0A只能处理标准数据帧且扩展帧的内容会织别错误。而CAN2.0 B Active可以处理标准数据帧和扩展数据帧。CAN2.0 B Passive只能处理标准数据帧而扩展帧的内容会被忽略)。
- 该CAN控制器支持最高的通讯速率为1Mb/s;
- 可以自动地接收和发送CAN报文,支持使用标准ID和扩展ID的报文;
- 外设中具有3个发送邮箱,发送报文的优先级可以使用软件控制,还可以记录发送的时间;
- 具有2个3级深度的接收FIFO,可使用过滤功能只接收或不接收某些ID号的报文;
- 可配置成自动重发;
- 不支持使用DMA进行数据收发。
CAN控制器的3种工作模式
CAN控制器有3种工作模式:初始化模式,正常模式,睡眠模式。
上电复位后CAN控制器默认会进入睡眠模式,作用是降低功耗。当需要将进行初始的时候(配置寄存器),会进入初始化模式。当需要通讯的时候,就进入正常模式。
CAN控制器的3种测试模式
有3种测试模式:静默模式、环回模式、环回静默模式。当控制器进入初始化模式的时候才可以配置测试模式。
功能框图
主动内核
含各种控制/状态/配置寄存器,可以配置模式、波特率等。在STM32CubeMx中可以非常方便的配置。
发送邮箱
用来缓存待发送的报文,最多可以缓存3个报文。发送调度决定报文的发送顺序。
接收FIFO
共有2个接收FIFO,每个FIFO都可以存放3个完整的报文。它们完全由硬件来管理。从而节省了CPU的处理负荷,简化了软件并保证了数据的一致性。应用程序只能通过读取FIFO输出邮箱,来读取FIFO中最先收到的报文。
接收过滤器
作用:对接到的报文进行过滤。最后放入FIFO 0或FIFO 1。
当总线上报文数据量很大时,总线上的设备会频繁获取报文,占用CPU。过滤器的存在,选择性接收有效报文,减轻系统负担。
有2种过滤模式:
- 标识符列表模式,它把要接收报文的ID列成一个表,要求报文ID与列表中的某一个标识符完全相同才可以接收,可以理解为白名单管理。
- 屏蔽位模式,允许将特定的bit设置为屏蔽模式,即不关心该bit是0还是1。
如果使能了过滤器,且报文的ID与所有过滤器的配置都不匹配,CAN外设会丢弃该报文,不存入接收FIFO。
每个CAN提供了14个位宽可变的、可配置的过滤器组(13~0)。每个过滤器组x由2个32位寄存器,CAN_FxR1和 CAN_FxR2组成。
说明:
(1)当工作于32位屏蔽位模式时,FR1保存标识符,FR2保存屏蔽位。FR2某位是1表示来的ID的这位必须和FR1中对应的位一致,FR2某位是0,表示ID的这位不关心。
(2)当工作于32位标识符模式时。FR1和FR2分别保存两个标识符。这意味着将来只有两个ID会匹配成功。
位时序
[!NOTE]
在协议标准的位时序基础上,STM32的CAN将PROP_SEG(传播缓冲段)和PHASE_SEG1(相位缓冲段1)合并为了一个BS1:
实验1-回环静默模式测试
硬件连线
RCC-时钟配置
/* 开启CAN外设、CAN引脚GPIO、引脚重定义AFIO时钟 */
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
RCC->APB2ENR |= (RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN);
AFIO-外设引脚复用重映射
/* CAN引脚复用重映射 */
// REMAP=10
AFIO->MAPR &= ~AFIO_MAPR_CAN_REMAP_0;
AFIO->MAPR |= AFIO_MAPR_CAN_REMAP_1;
GPIO-外设引脚工作模式
/* 配置CAN引脚的工作模式 PB8-RX:浮空输入 PB9-TX:复用推挽 */
// PB8 MODE=00 CNF=01
GPIOB->CRH &= ~GPIO_CRH_MODE8;
GPIOB->CRH |= GPIO_CRH_CNF8_0;
GPIOB->CRH &= ~GPIO_CRH_CNF8_1;
// PB9 MODE=11 CNF=10
GPIOB->CRH |= GPIO_CRH_MODE9;
GPIOB->CRH &= ~GPIO_CRH_CNF9_0;
GPIOB->CRH |= GPIO_CRH_CNF9_1;
CAN外设初始化
CAN寄存器访问保护
[!NOTE]
- CAN时序配置寄存器只能在CAN外设初始化模式下修改
- 发送邮箱只能在其状态为空时被修改
- 过滤器值只能在其被停用时修改(FINIT被置位或对应的过滤器组在CAN_FA1R中的状态为停用状态);此外,过滤器配置(过滤值位宽、过滤模式、关联FIFO)只能在过滤器初始化模式下完成(FINIT为置位状态)
CAN外设初始化流程分析
[!NOTE]
- CAN外设硬件复位后,默认处于睡眠模式以降低功耗
- 该模式下通过置位INRQ或SLEEP可以进入初始化/睡眠模式
- 一旦模式切换完成,INAK或SLAK会被硬件置位
- 软件初始化CAN外设需要在其处于初始化模式时完成
- 软件可以通过置位INRQ进入CAN初始化模式,并需要等待INAK被硬件置位来确保硬件已进入该模式
- 软件可以通过清除INRQ来退出初始化模式,退出完成时INAK会被硬件清除
- 在该模式下,软件需要配置CAN参数寄存器(MCR)和位时序寄存器(BTR)来初始化CAN外设
[!NOTE]
- 为了初始化CAN过滤器组相关的寄存器(过滤模式、过滤器位宽、关联FIFO、过滤器激活、过滤值),软件必须置位FINIT
- 单个过滤器组中过滤器值的初始化可以在CAN初始化模式中完成,也可以在CAN初始化模式之外操作
- 注意
- FINIT置位时,CAN外设接收功能是不工作的。
- 如果想修改某个过滤器组的过滤值,可以通过CAN_FA1R先将其失活,然后进行修改。(FINIT是针对整个过滤器模块的全局状态,而CAN_FA1R则是针对某个单独的过滤器组配置激活/停用)
- 如果不想使用某个过滤器组了,也建议通过CAN_FA1R将其设置为停用状态。
[!NOTE]
- 一旦完成了CAN外设的初始化,软件必须请求外设进入正常模式,以在CAN总线上取得同步,并开始接收和发送报文。
- 通过清除INRQ可以请求外设进入正常模式。请求发出后,当CAN外设和CAN总线上传输的数据取得同步后(等待11个连续的隐形电平,即总线空闲状态),外设会进入正常模式并准备好参与总线活动。外设切换到正常模式后,会清除INAK。
- 过滤器值的初始化不依赖CAN外设的初始化模式,但是必须在过滤器停用(见CAN_FA1R)时操作。
- 过滤器位宽和过滤模式必须在进入正常模式之前配置。
请求CAN外设进入初始化模式
/* CAN主要参数配置 */
// 复位后CAN外设默认处于睡眠模式
CAN1->MCR |= CAN_MCR_INRQ; // 请求CAN外设进入初始化模式
while ((CAN1->MSR & CAN_MSR_INAK) == 0) { // 轮询等待CAN外设确认
}
CAN主要控制参数配置-MCR
// 发送FIFO队列中报文发送的优先级 0=>ID小的先发 1=>时间顺序先入队的先发
CAN1->MCR |= CAN_MCR_TXFP; // 先入队先发
// 接收FIFO队列满后入队报文处理:0=>覆盖先前的 1=>丢弃
CAN1->MCR |= CAN_MCR_RFLM; // 丢弃
// 是否停用自动重传(根据CAN协议规范,发生仲裁失利、错误处理等需要重传报文)
CAN1->MCR |= CAN_MCR_NART; // 测试回环静默模式,不需要自动重传
// 是否启用自动唤醒模式(当检测到CAN报文时自动退出睡眠模式)
CAN1->MCR |= CAN_MCR_AWUM; // 启用自动唤醒模式
// 是否启用自动离线管理(根据CAN标准,TEC超过255后节点会进入离线模式,当检测128次连续的11位隐形电平后可以恢复到主动错误状态)
CAN1->MCR |= CAN_MCR_ABOM; // 启用自动离线管理(符合条件后,自动从离线状态恢复到主动错误状态)
CAN时序配置
/* CAN时序配置 */
// 配置1Tq时间 => (BRP + 1)*Tpclk,CAN1时钟36M,BRP配置为35,则1Tq=1us
CAN1->BTR &= ~CAN_BTR_BRP;
CAN1->BTR |= (36 - 1) << 0;
// 位时序配置
// 同步段固定为1Tq,TS1配置为3Tq,TS2配置6Tq,1bit时间为10Tq=10us,波特率为100kbps
CAN1->BTR &= ~CAN_BTR_TS1;
CAN1->BTR |= (3 - 1) << 16;
CAN1->BTR &= ~CAN_BTR_TS2;
CAN1->BTR |= (6 - 1) << 20;
// 再同步时,需要增加或减少Tq来调整bit时间(调整值称为跳跃宽度),该参数表示允许CAN外设执行再同步时跳跃宽度的最大值(限制过度补偿)
CAN1->BTR &= ~CAN_BTR_SJW;
CAN1->BTR |= (2 - 1) << 24; // 允许的最大跳跃宽度为2Tq
// 开启测试模式:回环模式+静默模式,即不发到总线上,也不从总线接收,仅自己发给自己
CAN1->BTR |= CAN_BTR_LBKM;
CAN1->BTR |= CAN_BTR_SILM;
CAN过滤器配置
/* 配置过滤器 */
// 过滤模式 0=>掩码模式 1=>清单模式(白名单)
CAN1->FM1R &= ~CAN_FM1R_FBM0; // 过滤器1配置为掩码模式
// 过滤器位宽配置
CAN1->FS1R |= CAN_FS1R_FSC0; // 过滤值配置为32位(可以过滤11位ID和29位ID)
// 分配FIFO,过滤器匹配后递交给那个接收FIFO
CAN1->FFA1R &= ~CAN_FFA1R_FFA0; // 过滤器1分配给FIFO0
// 过滤器1的过滤值和屏蔽位
CAN1->sFilterRegister[0].FR1 = 0x00000000; // 过滤值为0
CAN1->sFilterRegister[0].FR2 =
0x00000000; // 屏蔽为0表示该bit不关心,不需要和过滤值相应bit进行比较
// 激活过滤器1
CAN1->FA1R |= CAN_FA1R_FACT0;
// 进入过滤器激活模式
CAN1->FMR &= ~CAN_FMR_FINIT;
请求CAN外设进入正常模式
// 请求CAN外设进入正常模式
CAN1->MCR &= ~CAN_MCR_INRQ;
// 轮询等待外设确认
while ((CAN1->MSR & CAN_MSR_INAK) == 1) {
}
完整代码
static void CAN_GPIOConfig(void) {
/* CAN引脚复用重映射 */
// REMAP=10
AFIO->MAPR &= ~AFIO_MAPR_CAN_REMAP_0;
AFIO->MAPR |= AFIO_MAPR_CAN_REMAP_1;
/* 配置CAN引脚的工作模式 PB8-RX:浮空输入 PB9-TX:复用推挽 */
// PB8 MODE=00 CNF=01
GPIOB->CRH &= ~GPIO_CRH_MODE8;
GPIOB->CRH |= GPIO_CRH_CNF8_0;
GPIOB->CRH &= ~GPIO_CRH_CNF8_1;
// PB9 MODE=11 CNF=10
GPIOB->CRH |= GPIO_CRH_MODE9;
GPIOB->CRH &= ~GPIO_CRH_CNF9_0;
GPIOB->CRH |= GPIO_CRH_CNF9_1;
}
void CAN_Init(void) {
/* 开启CAN外设、CAN引脚GPIO、引脚重定义AFIO时钟 */
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
RCC->APB2ENR |= (RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN);
CAN_GPIOConfig();
/* CAN主要参数配置 */
// 复位后CAN外设默认处于睡眠模式
CAN1->MCR |= CAN_MCR_INRQ; // 请求CAN外设进入初始化模式
while ((CAN1->MSR & CAN_MSR_INAK) == 0) { // 轮询等待CAN外设确认
}
// 发送FIFO队列中报文发送的优先级 0=>ID小的先发 1=>时间顺序先入队的先发
CAN1->MCR |= CAN_MCR_TXFP; // 先入队先发
// 接收FIFO队列满后入队报文处理:0=>覆盖先前的 1=>丢弃
CAN1->MCR |= CAN_MCR_RFLM; // 丢弃
// 是否停用自动重传(根据CAN协议规范,发生仲裁失利、错误处理等需要重传报文)
CAN1->MCR |= CAN_MCR_NART; // 测试回环静默模式,不需要自动重传
// 是否启用自动唤醒模式(当检测到CAN报文时自动退出睡眠模式)
CAN1->MCR |= CAN_MCR_AWUM; // 启用自动唤醒模式
// 是否启用自动离线管理(根据CAN标准,TEC超过255后节点会进入离线模式,当检测128次连续的11位隐形电平后可以恢复到主动错误状态)
CAN1->MCR |= CAN_MCR_ABOM; // 启用自动离线管理(符合条件后,自动从离线状态恢复到主动错误状态)
/* CAN时序配置 */
// 配置1Tq时间 => (BRP + 1)*Tpclk,CAN1时钟36M,BRP配置为35,则1Tq=1us
CAN1->BTR &= ~CAN_BTR_BRP;
CAN1->BTR |= (36 - 1) << 0;
// 位时序配置
// 同步段固定为1Tq,TS1配置为3Tq,TS2配置6Tq,1bit时间为10Tq=10us,波特率为100kbps
CAN1->BTR &= ~CAN_BTR_TS1;
CAN1->BTR |= (3 - 1) << 16;
CAN1->BTR &= ~CAN_BTR_TS2;
CAN1->BTR |= (6 - 1) << 20;
// 再同步时,需要增加或减少Tq来调整bit时间(调整值称为跳跃宽度),该参数表示允许CAN外设执行再同步时跳跃宽度的最大值(限制过度补偿)
CAN1->BTR &= ~CAN_BTR_SJW;
CAN1->BTR |= (2 - 1) << 24; // 允许的最大跳跃宽度为2Tq
// 开启测试模式:回环模式+静默模式,即不发到总线上,也不从总线接收,仅自己发给自己
CAN1->BTR |= CAN_BTR_LBKM;
CAN1->BTR |= CAN_BTR_SILM;
/* 配置过滤器 */
// 过滤模式 0=>掩码模式 1=>清单模式(白名单)
CAN1->FM1R &= ~CAN_FM1R_FBM0; // 过滤器1配置为掩码模式
// 过滤器位宽配置
CAN1->FS1R |= CAN_FS1R_FSC0; // 过滤值配置为32位(可以过滤11位ID和29位ID)
// 分配FIFO,过滤器匹配后递交给那个接收FIFO
CAN1->FFA1R &= ~CAN_FFA1R_FFA0; // 过滤器1分配给FIFO0
// 过滤器1的过滤值和屏蔽位
CAN1->sFilterRegister[0].FR1 = 0x00000000; // 过滤值为0
CAN1->sFilterRegister[0].FR2 =
0x00000000; // 屏蔽为0表示该bit不关心,不需要和过滤值相应bit进行比较
// 激活过滤器1
CAN1->FA1R |= CAN_FA1R_FACT0;
// 进入过滤器激活模式
CAN1->FMR &= ~CAN_FMR_FINIT;
// 请求CAN外设进入正常模式
CAN1->MCR &= ~CAN_MCR_INRQ;
// 轮询等待外设确认
while ((CAN1->MSR & CAN_MSR_INAK) == 1) {
}
}
发送报文
void CAN_SendMsg(uint16_t stdId, uint8_t *data, uint8_t len) {
if (len > 8) {
LOG_ERROR("最多只能发送8个字节")
return;
}
// 如果发送邮箱0不为空,则轮询等待
while ((CAN1->TSR & CAN_TSR_TME0) == 0) {
}
/* 配置发送邮箱0 */
// ID配置
// 标准格式,扩展格式
CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_IDE;
// 数据帧,远程帧配置
CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_RTR;
// 设置ID
CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_STID;
CAN1->sTxMailBox[0].TIR |= (stdId << 21);
// 设置数据长度
CAN1->sTxMailBox[0].TDTR &= ~CAN_TDT0R_DLC;
CAN1->sTxMailBox[0].TDTR |= ((len & 0x0F) << 0);
// 设置数据字节
CAN1->sTxMailBox[0].TDLR = 0;
CAN1->sTxMailBox[0].TDHR = 0;
for (uint8_t i = 0; i < len; i++) {
if (i < 4) {
CAN1->sTxMailBox[0].TDLR |= (data[i] << (i * 8));
} else {
CAN1->sTxMailBox[0].TDHR |= (data[i] << ((i - 4) * 8));
}
}
// 请求发送数据
CAN1->sTxMailBox[0].TIR |= CAN_TI0R_TXRQ;
}
接收报文
[!NOTE]
接收时使用的是封装好的FIFO模型,虽然我们知道FIFO中有3个邮箱,但是在STM32的API设计中,我们只能通过
CAN_TypeDef#sFIFOMailBox[0]
访问FIFO0中的队首报文,并通过CAN1->RF0R |= CAN_RF0R_RFOM0
来释放(出队)FIFO0的队首报文。
void CAN_ReceiveMsg(CAN_RxMsgStruct *msgBuf, uint8_t *msgSize) {
// 获取接收FIFO0中的报文数量
*msgSize = (CAN1->RF0R & CAN_RF0R_FMP0) >> 0;
for (uint8_t i = 0; i < *msgSize; i++) {
CAN_RxMsgStruct *buf = msgBuf + i;
// 获取FIFO0队首报文
CAN_FIFOMailBox_TypeDef *msg = &(CAN1->sFIFOMailBox[0]);
// 解析报文
buf->stdId = (msg->RIR & CAN_RI0R_STID) >> 21;
buf->len = (msg->RDTR & CAN_RDT0R_DLC) >> 0;
for (uint8_t j = 0; j < buf->len; j++) {
if (j < 4) {
buf->data[j] = (msg->RDLR >> (j * 8)) & 0xFF;
} else {
buf->data[j] = (msg->RDHR >> ((j - 4) * 8)) & 0xFF;
}
}
// 释放FIFO0队首报文(出队)
CAN1->RF0R |= CAN_RF0R_RFOM0;
}
}
构建/解析报文优化(位运算)
[!NOTE]
⚠️值得注意的是:通过掩码和按位与从寄存器中取某个字段的值时,经常容易忘记右移以让其最低有效位和bit0对齐。可以封装一个从寄存器取字段值的函数,并通过入参强制传入移位参数。
位运算封装,规范必要入参
#define GET_REG_FIELD(regVal, fieldMask, shift) ((regVal & fieldMask) >> shift)
#define GET_REG_BYTE(regVal, byteIndex) ((regVal >> (byteIndex * 8)) & 0xFF)
#define SET_REG_BYTE(reg, byteIndex, byteData) (reg |= (byteData << ((byteIndex) * 8)))
构建报文
// 设置数据字节
CAN1->sTxMailBox[0].TDLR = 0;
CAN1->sTxMailBox[0].TDHR = 0;
for (uint8_t i = 0; i < len; i++) {
if (i < 4) {
SET_REG_BYTE(CAN1->sTxMailBox[0].TDLR, i, data[i]);
} else {
SET_REG_BYTE(CAN1->sTxMailBox[0].TDHR, i - 4, data[i]);
}
}
解析报文
// 解析报文
buf->stdId = GET_REG_FIELD(msg->RIR, CAN_RI0R_STID, 21);
buf->len = GET_REG_FIELD(msg->RDTR, CAN_RDT0R_DLC, 0);
for (uint8_t j = 0; j < buf->len; j++) {
if (j < 4) {
buf->data[j] = GET_REG_BYTE(msg->RDLR, j);
} else {
buf->data[j] = GET_REG_BYTE(msg->RDHR, (j - 4));
}
}
示例代码
can.h
#ifndef __CAN_H__
#define __CAN_H__
#include "stm32f10x.h"
#define GET_REG_FIELD(regVal, fieldMask, shift) ((regVal & fieldMask) >> shift)
#define GET_REG_BYTE(regVal, byteIndex) ((regVal >> (byteIndex * 8)) & 0xFF)
#define SET_REG_BYTE(reg, byteIndex, byteData) (reg |= (byteData << ((byteIndex) * 8)))
typedef struct {
uint16_t stdId;
uint8_t len;
uint8_t data[8];
} CAN_RxMsgStruct;
void CAN_Init(void);
void CAN_SendMsg(uint16_t stdId, uint8_t *data, uint8_t len);
void CAN_ReceiveMsg(CAN_RxMsgStruct *msgBuf, uint8_t *msgSize);
#endif /* __CAN_H__ */
can.c
#include "can.h"
#include "logger.h"
static void CAN_GPIOConfig(void) {
/* CAN引脚复用重映射 */
// REMAP=10
AFIO->MAPR &= ~AFIO_MAPR_CAN_REMAP_0;
AFIO->MAPR |= AFIO_MAPR_CAN_REMAP_1;
/* 配置CAN引脚的工作模式 PB8-RX:浮空输入 PB9-TX:复用推挽 */
// PB8 MODE=00 CNF=01
GPIOB->CRH &= ~GPIO_CRH_MODE8;
GPIOB->CRH |= GPIO_CRH_CNF8_0;
GPIOB->CRH &= ~GPIO_CRH_CNF8_1;
// PB9 MODE=11 CNF=10
GPIOB->CRH |= GPIO_CRH_MODE9;
GPIOB->CRH &= ~GPIO_CRH_CNF9_0;
GPIOB->CRH |= GPIO_CRH_CNF9_1;
}
void CAN_Init(void) {
/* 开启CAN外设、CAN引脚GPIO、引脚重定义AFIO时钟 */
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
RCC->APB2ENR |= (RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN);
CAN_GPIOConfig();
/* CAN主要参数配置 */
// 复位后CAN外设默认处于睡眠模式
CAN1->MCR |= CAN_MCR_INRQ; // 请求CAN外设进入初始化模式
while ((CAN1->MSR & CAN_MSR_INAK) == 0) { // 轮询等待CAN外设确认
}
// 发送FIFO队列中报文发送的优先级 0=>ID小的先发 1=>时间顺序先入队的先发
CAN1->MCR |= CAN_MCR_TXFP; // 先入队先发
// 接收FIFO队列满后入队报文处理:0=>覆盖先前的 1=>丢弃
CAN1->MCR |= CAN_MCR_RFLM; // 丢弃
// 是否停用自动重传(根据CAN协议规范,发生仲裁失利、错误处理等需要重传报文)
CAN1->MCR |= CAN_MCR_NART; // 测试回环静默模式,不需要自动重传
// 是否启用自动唤醒模式(当检测到CAN报文时自动退出睡眠模式)
CAN1->MCR |= CAN_MCR_AWUM; // 启用自动唤醒模式
// 是否启用自动离线管理(根据CAN标准,TEC超过255后节点会进入离线模式,当检测128次连续的11位隐形电平后可以恢复到主动错误状态)
CAN1->MCR |=
CAN_MCR_ABOM; // 启用自动离线管理(符合条件后,自动从离线状态恢复到主动错误状态)
/* CAN时序配置 */
// 配置1Tq时间 => (BRP + 1)*Tpclk,CAN1时钟36M,BRP配置为35,则1Tq=1us
CAN1->BTR &= ~CAN_BTR_BRP;
CAN1->BTR |= (36 - 1) << 0;
// 位时序配置
// 同步段固定为1Tq,TS1配置为3Tq,TS2配置6Tq,1bit时间为10Tq=10us,波特率为100kbps
CAN1->BTR &= ~CAN_BTR_TS1;
CAN1->BTR |= (3 - 1) << 16;
CAN1->BTR &= ~CAN_BTR_TS2;
CAN1->BTR |= (6 - 1) << 20;
// 再同步时,需要增加或减少Tq来调整bit时间(调整值称为跳跃宽度),该参数表示允许CAN外设执行再同步时跳跃宽度的最大值(限制过度补偿)
CAN1->BTR &= ~CAN_BTR_SJW;
CAN1->BTR |= (2 - 1) << 24; // 允许的最大跳跃宽度为2Tq
// 开启测试模式:回环模式+静默模式,即不发到总线上,也不从总线接收,仅自己发给自己
CAN1->BTR |= CAN_BTR_LBKM;
CAN1->BTR |= CAN_BTR_SILM;
/* 配置过滤器 */
// 过滤模式 0=>掩码模式 1=>清单模式(白名单)
CAN1->FM1R &= ~CAN_FM1R_FBM0; // 过滤器1配置为掩码模式
// 过滤器位宽配置
CAN1->FS1R |= CAN_FS1R_FSC0; // 过滤值配置为32位(可以过滤11位ID和29位ID)
// 分配FIFO,过滤器匹配后递交给那个接收FIFO
CAN1->FFA1R &= ~CAN_FFA1R_FFA0; // 过滤器1分配给FIFO0
// 过滤器1的过滤值和屏蔽位
CAN1->sFilterRegister[0].FR1 = 0x00000000; // 过滤值为0
CAN1->sFilterRegister[0].FR2 =
0x00000000; // 屏蔽为0表示该bit不关心,不需要和过滤值相应bit进行比较
// 激活过滤器1
CAN1->FA1R |= CAN_FA1R_FACT0;
// 进入过滤器激活模式
CAN1->FMR &= ~CAN_FMR_FINIT;
// 请求CAN外设进入正常模式
CAN1->MCR &= ~CAN_MCR_INRQ;
// 轮询等待外设确认
while ((CAN1->MSR & CAN_MSR_INAK) == 1) {
}
}
void CAN_SendMsg(uint16_t stdId, uint8_t *data, uint8_t len) {
if (len > 8) {
LOG_ERROR("最多只能发送8个字节")
return;
}
// 如果发送邮箱0不为空,则轮询等待
while ((CAN1->TSR & CAN_TSR_TME0) == 0) {
}
/* 配置发送邮箱0 */
// ID配置
// 标准格式,扩展格式
CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_IDE;
// 数据帧,远程帧配置
CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_RTR;
// 设置ID
CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_STID;
CAN1->sTxMailBox[0].TIR |= (stdId << 21);
// 设置数据长度
CAN1->sTxMailBox[0].TDTR &= ~CAN_TDT0R_DLC;
CAN1->sTxMailBox[0].TDTR |= ((len & 0x0F) << 0);
// 设置数据字节
CAN1->sTxMailBox[0].TDLR = 0;
CAN1->sTxMailBox[0].TDHR = 0;
for (uint8_t i = 0; i < len; i++) {
if (i < 4) {
SET_REG_BYTE(CAN1->sTxMailBox[0].TDLR, i, data[i]);
} else {
SET_REG_BYTE(CAN1->sTxMailBox[0].TDHR, i - 4, data[i]);
}
}
// 请求发送数据
CAN1->sTxMailBox[0].TIR |= CAN_TI0R_TXRQ;
}
void CAN_ReceiveMsg(CAN_RxMsgStruct *msgBuf, uint8_t *msgSize) {
// 获取接收FIFO0中的报文数量
*msgSize = GET_REG_FIELD(CAN1->RF0R, CAN_RF0R_FMP0, 0);
for (uint8_t i = 0; i < *msgSize; i++) {
CAN_RxMsgStruct *buf = msgBuf + i;
// 获取FIFO0队首报文句柄
CAN_FIFOMailBox_TypeDef *msg = &(CAN1->sFIFOMailBox[0]);
// 解析报文
buf->stdId = GET_REG_FIELD(msg->RIR, CAN_RI0R_STID, 21);
buf->len = GET_REG_FIELD(msg->RDTR, CAN_RDT0R_DLC, 0);
for (uint8_t j = 0; j < buf->len; j++) {
if (j < 4) {
buf->data[j] = GET_REG_BYTE(msg->RDLR, j);
} else {
buf->data[j] = GET_REG_BYTE(msg->RDHR, (j - 4));
}
}
// 释放FIFO0队首报文(出队)
CAN1->RF0R |= CAN_RF0R_RFOM0;
}
}
main.c
#include "can.h"
#include "delay.h"
#include "key.h"
#include "led.h"
#include "logger.h"
#include "stdio.h"
#include "stm32f10x.h"
#include "string.h"
#include "uart.h"
void uart1_received_callback(uint8_t buf[], uint8_t size) {
uart_send_bytes(buf, size);
}
int main() {
uart_init();
CAN_Init();
LOG_DEBUG("main start")
CAN_SendMsg(0x01, "abcd", 5);
CAN_SendMsg(0x02, "hello", 6);
LOG_DEBUG("send msg done")
delay_ms(1000); // 等待发送到CAN的接收FIFO
CAN_RxMsgStruct msgBuf[2] = {0};
uint8_t msgSize = 0;
CAN_ReceiveMsg(msgBuf, &msgSize);
LOG_DEBUG("msgSize = %d", msgSize)
for (uint8_t i = 0; i < msgSize; i++) {
LOG_DEBUG("id = %#x, msg = %s", msgBuf[i].stdId, msgBuf[i].data)
}
while (1) {
}
}
过滤器测试
过滤出ID为1的报文
CAN1->sFilterRegister[0].FR1 = 1 << 21;
CAN1->sFilterRegister[0].FR2 = 1 << 21;
过滤出ID为2的报文
CAN1->sFilterRegister[0].FR1 = 2 << 21;
CAN1->sFilterRegister[0].FR2 = 2 << 21;
寄存器总结
实验1的HAL库实现
Cube配置
示例代码
can.h
/* USER CODE BEGIN Prototypes */
void CAN_Init(void);
void CAN_SendMsg(uint16_t stdId, uint8_t *data, uint8_t len);
void CAN_ReceiveMsg(CAN_RxMsgStruct *msgBuf, uint8_t *msgSize);
/* USER CODE END Prototypes */
can.c
/* USER CODE BEGIN 1 */
static void CAN_FilterConfig() {
CAN_FilterTypeDef filterConfig;
// 初始化 CAN_FilterTypeDef 结构体
filterConfig.FilterBank = 0; // 选择过滤器组 0
filterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 掩码模式
filterConfig.FilterScale = 1; // 32 位过滤器
filterConfig.FilterIdHigh = 0x0000; // 高 16 位标识符,设置为 0x0000
filterConfig.FilterIdLow = 0x0000; // 低 16 位标识符,设置为 0x0000
filterConfig.FilterMaskIdHigh =
0x0000; // 高 16 位掩码,设置为 0xFFFF(全屏蔽)
filterConfig.FilterMaskIdLow =
0x0000; // 低 16 位掩码,设置为 0xFFFF(全屏蔽)
filterConfig.FilterFIFOAssignment = 0; // 分配到 FIFO 0
filterConfig.FilterActivation = 1; // 启用过滤器
HAL_CAN_ConfigFilter(&hcan, &filterConfig);
}
void CAN_Init(void) {
// 配置CAN外设主要参数
MX_CAN_Init();
// 配置自定义过滤规则
CAN_FilterConfig();
// 请求CAN外设退出初始化模式,进入正常模式
HAL_CAN_Start(&hcan);
}
void CAN_SendMsg(uint16_t stdId, uint8_t *data, uint8_t len) {
// 等待发送邮箱空闲
while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0) {
}
// 将报文添加到发送邮箱
CAN_TxHeaderTypeDef header;
uint32_t txMailBox = 0;
header.IDE = CAN_ID_STD; // 标准格式ID
header.StdId = stdId;
header.DLC = len;
header.RTR = CAN_RTR_DATA; // 数据帧
HAL_CAN_AddTxMessage(&hcan, &header, data, &txMailBox);
}
void CAN_ReceiveMsg(CAN_RxMsgStruct *msgBuf, uint8_t *msgSize) {
// 获取接收队列(FIFO0)待处理报文数量
*msgSize = HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_RX_FIFO0);
for (uint8_t i = 0; i < *msgSize; i++) {
CAN_RxHeaderTypeDef header;
// 从FIFO0中出队报文
HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &header, msgBuf[i].data);
msgBuf[i].stdId = header.StdId;
msgBuf[i].len = header.DLC;
}
}
/* USER CODE END 1 */
main.c
/* Initialize all configured peripherals */
MX_GPIO_Init();
//MX_CAN_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
CAN_Init();
printf("main start\n");
CAN_SendMsg(0x01, "abcd", 5);
CAN_SendMsg(0x02, "hello", 6);
CAN_SendMsg(0x03, "world", 6);
printf("send msg done\n");
HAL_Delay(1000); // 等待发送到CAN的接收FIFO
CAN_RxMsgStruct msgBuf[3] = {0};
uint8_t msgSize = 0;
CAN_ReceiveMsg(msgBuf, &msgSize);
printf("msgSize = %d\n", msgSize);
for (uint8_t i = 0; i < msgSize; i++) {
printf("id = %#x, msg = %s\n", msgBuf[i].stdId, msgBuf[i].data);
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
实验2-两个CAN节点一收一发
关闭测试模式
// 关闭测试模式
CAN1->BTR &= ~CAN_BTR_LBKM;
CAN1->BTR &= ~CAN_BTR_SILM;
节点A轮询发送
uint32_t seq = 0;
uint8_t buf[10] = {0};
while (1) {
sprintf(buf, "hello msg, seq = %d", seq);
CAN_SendMsg(0x01, buf, strlen(buf));
seq++;
delay_ms(1000);
}
节点B轮询接收
CAN_RxMsgStruct msgBuf[3] = {0};
uint8_t msgSize = 0;
while (1) {
CAN_ReceiveMsg(msgBuf, &msgSize);
for (uint8_t i = 0; i < msgSize; i++) {
LOG_DEBUG("id = %#x, msg = %.*s", msgBuf[i].stdId, msgBuf[i].len, msgBuf[i].data)
}
}
接收报文结构体数据残留问题
[!NOTE]
CAN_RxMsgStruct#data
大小为8字节,可能残留了上次接收的数据,可以根据业务需要来清除。但最好还是根据CAN_RxMsgStruct#len
指示的字节数量来处理报文数据
void CAN_ReceiveMsg(CAN_RxMsgStruct *msgBuf, uint8_t *msgSize) {
// 获取接收FIFO0中的报文数量
*msgSize = GET_REG_FIELD(CAN1->RF0R, CAN_RF0R_FMP0, 0);
for (uint8_t i = 0; i < *msgSize; i++) {
CAN_RxMsgStruct *buf = msgBuf + i;
// 获取FIFO0队首报文句柄
CAN_FIFOMailBox_TypeDef *msg = &(CAN1->sFIFOMailBox[0]);
// 解析报文
buf->stdId = GET_REG_FIELD(msg->RIR, CAN_RI0R_STID, 21);
buf->len = GET_REG_FIELD(msg->RDTR, CAN_RDT0R_DLC, 0);
// buf.data可能残留了上次接收到的数据,可以按需清除
memset(buf->data, 0, sizeof(buf->data));
for (uint8_t j = 0; j < buf->len; j++) {
if (j < 4) {
buf->data[j] = GET_REG_BYTE(msg->RDLR, j);
} else {
buf->data[j] = GET_REG_BYTE(msg->RDHR, (j - 4));
}
}
// 释放FIFO0队首报文(出队)
CAN1->RF0R |= CAN_RF0R_RFOM0;
}
}