CAN的简介:


一、看总线基本概念
看总线全称为Controller Area Network(CAN),由博世公司开发,是一种高可靠性、易扩展的串行通信总线,广泛应用于汽车、工业控制等领域,支持多主控架构下的高效通信。
1、看总线主要特性
仅需两根通信线(CAN_H和CAN_L),采用差分信号抗干扰;分为高速CAN(ISO11898,速率125K~1Mbps,距离<40m)和低速CAN(ISO11519,速率10K~125Kbps,距离<1000m);异步半双工通信,无需时钟线;支持11位或29位报文ID用于功能区分和优先级仲裁;每帧可携带1~8字节数据。
2、通信模式
支持广播式传输(默认常用)和请求式传输(遥控帧触发)。广播式中发送方主动发布消息,接收方根据ID决定是否处理;请求式需接收方先发送遥控帧请求数据。
3、与其他通信协议对比
相比UART(点对点)、I2C(主从多设备但主控有限)、SPI(高速但线路复杂),看总线优势在于支持多个主控设备通过两条线实现任意互联通信,适用于多节点分布式系统。
4、硬件电路设计
每个节点通过看收发器(如TJA1050)连接至总线;高速CAN采用闭环结构,两端加120Ω终端电阻防止反射;低速CAN为开环结构,单端加2.2kΩ电阻;收发器完成控制器逻辑电平与差分信号之间的转换。
5、电平标准定义
高速CAN:差分电压0V为隐性电平(逻辑1),2V为显性电平(逻辑0);低速CAN:-1.5V为隐性,3V为显性。隐性状态为总线默认状态,显性由设备主动拉出,且显性优先于隐性。
6、看收发器功能解析
以TJA1050为例,其内部包含驱动器、接收器、电流源上拉、显性超时保护、温度保护等功能模块,确保信号正确转换与总线安全运行,防止异常占用导致总线瘫痪。
7、物理层参数汇总
高速CAN最大速率1Mbps,最长40米,最多挂30个节点,使用双绞线闭环阻抗120Ω;低速CAN最大125Kbps,最长1000米,最多20节点,开环阻抗未统一,终端电阻2.2kΩ。

 

CAN和其他通信协议的对比:

第一章:异步通信与位同步机制

一、异步通信与波特率约定
看总线采用异步通信方式,无公共时钟线,所有设备需预先约定相同的波特率以确定每位数据的持续时间,确保收发双方以相同速率进行数据传输。
二、采样点对齐的重要性
若接收方采样点未对齐数据位中心,尤其落在跳变沿附近,会导致电平判断不确定,引发采样错误,因此必须使采样点位于数据位中心附近以保证可靠性。
三、硬同步机制
硬同步以帧起始信号(SOF)的下降沿为参考,接收方检测到该边沿后将其位时序计数器重置至同步段(SS段),实现初始相位对齐,解决首次采样位置偏差问题。
四、位时序结构划分
每个数据位划分为四个段:同步段(SS)、传播时间段(PTS)、相位缓冲段1(PPS1)和相位缓冲段2(PPS2),各段由若干最小时间单位TQ构成,用于灵活调整采样点位置。
五、采样点调节原理
通过调整PPS1和PPS2的长度可改变采样点在位周期中的位置,采样点位于PPS1与PPS2之间,从而适应不同硬件延迟和时钟偏差。
六、再同步机制
在数据传输过程中,以非首帧的跳变沿为参考,当检测到跳变沿偏离SS段时,接收方根据偏差方向和大小调整PPS1或PPS2,补偿时钟误差,维持采样点稳定。
七、再同步补偿宽度(STW)
STW定义了每次再同步的最大调整范围(1~4TQ),防止因噪声干扰导致过度调整;实际补偿值取误差量与STW的较小者,避免误判造成时序混乱。
八、时钟误差累积影响
即使微小时钟偏差,在长帧传输中也会累积导致采样点显著偏移,必须通过再同步机制动态修正,确保整个数据帧的正确接收。
九、波特率计算方法
波特率等于1除以一个数据位的总时长,数据位时长为SS+PTS+PPS1+PPS2各段时间之和;TQ值及各段所含TQ数可配置,进而决定最终通信速率。
十、参数配置灵活性
位时序各段长度和TQ值可根据系统需求自行设定,不同配置可实现相同波特率,设计具有灵活性,但需兼顾稳定性与抗干扰能力。


 

1.1 异步通信与波特率

CAN总线采用异步通信方式,无公共时钟线。所有设备必须预先约定相同的波特率(即每位数据的持续时间),以确保收发双方以相同速率传输数据。例如,若约定波特率为1Mbps,则每位时长为1μs。

1.2 采样点对齐的必要性

接收方采样点必须位于数据位中心附近。若采样点靠近跳变沿,会导致电平判断不确定,引发采样错误。

1.3 硬同步机制

  • 触发条件:帧起始信号(SOF)的下降沿。

  • 作用:接收方检测到SOF下降沿后,将其位时序计数器重置至同步段(SS段),实现初始相位对齐。

1.4 位时序结构

每个数据位划分为四个段,由最小时间单位TQ构成:

  1. 同步段(SS):固定1TQ,用于检测跳变沿。

  2. 传播时间段(PTS):1~8TQ,用于补偿物理延迟(输出、传播、输入延迟)。

  3. 相位缓冲段1(PBS1):1~8TQ,与PBS2共同确定采样点位置。

  4. 相位缓冲段2(PBS2):2~8TQ,采样点位于PBS1与PBS2之间。

1.5 再同步机制

  • 触发条件:非首帧的数据跳变沿。

  • 作用:当跳变沿偏离SS段时,接收方根据偏差方向动态调整PBS1(延长)或PBS2(缩短),补偿时钟误差。

  • 补偿限制:再同步补偿宽度(SJW)限定每次最大调整范围(1~4TQ),防止噪声干扰导致过度调整。

1.6 波特率计算

公式:

text

波特率 = 1 / [TQ × (SS + PTS + PBS1 + PBS2)]

示例:

  • TQ = 0.5μs,SS=1TQ,PTS=3TQ,PBS1=3TQ,PBS2=3TQ

  • 总TQ数 = 1+3+3+3=10TQ

  • 位时长 = 0.5μs × 10 = 5μs

  • 波特率 = 1 / 5μs = 200kbps


第二章:CAN帧格式

一、CAN总线帧类型概述
CAN总线定义了五种帧类型:数据帧用于发送设备主动广播数据;遥控帧由接收设备发起请求;错误帧在检测到通信异常时通知其他节点;过载帧用于接收方表示未准备好;帧间隔则用于分隔连续帧。其中数据帧最复杂且最常用,是学习重点。
二、数据帧标准格式结构
数据帧标准格式包含起始位(SOF)、11位报文ID、RTR位、控制段(IDE、r0、DLC)、数据段(0-8字节)、CRC段、ACK段及EOF。灰色部分为显性电平0,紫色为可变电平,白色为隐性电平1,韵达位为发送方释放、接收方拉低的应答机制。
三、数据帧扩展格式设计
扩展格式在标准格式基础上增加18位ID,形成29位标识符,以解决ID资源不足问题。新增SR位(替代RTR)和IDE位(标识扩展格式),保留兼容性。接收方根据IDE位判断解析方式,前11位ID后接SR和IDE,再续18位ID。
四、关键字段功能说明
报文ID既标识数据功能也决定优先级,ID越小优先级越高;RTR位区分数据帧(显性0)与遥控帧(隐性1);IDE位区分标准(显性0)与扩展格式(隐性1);DLC指定数据段字节数(0-8);CRC进行15位循环冗余校验;ACK槽实现接收应答。
五、遥控帧工作机制
遥控帧无数据段,通过RTR置1标识,用于接收设备请求特定ID的数据。请求方发送含目标ID的遥控帧,拥有该数据的节点响应以相同ID的数据帧。适用于低频但需即时获取的数据传输场景。
六、错误帧与过载帧处理
错误帧由任一节点检测到位错误、填充错误、CRC错误等时发出,主动错误状态发6个显性位破坏数据,被动状态发6个隐性位仅影响自身;过载帧由接收方因处理不过来而发送,延缓发送方重试,平衡负载。
七、帧间隔与时序控制
帧间隔用于分隔前后帧,主动错误状态为3位,被动状态为3+8位延迟,限制不可靠节点频繁发送。ACK前后设置CRC界定符和ACK界定符,确保总线控制权短暂交接,保障应答可靠性。
八、位填充规则及其作用
发送方每5个相同电平后插入反相填充位,接收方自动移除。作用包括:提供定时信息便于再同步;防止长串同电平导致采样偏移;将正常数据与含6个同电平的错误/过载帧区分开;避免总线被误判为空闲(需连续11个隐性1才为空闲)。


九、实际波形解析示例
通过示波器捕捉四个典型波形:标准数据帧(ID 0x55,1字节数据)、双字节数据帧(ID 0x666)、扩展格式数据帧(29位ID)和标准遥控帧(ID 0x123,无数据段)。结合填充位识别、字段划分与应答确认,验证帧格式与未填充规则的实际体现。
十、协议演进与设计智慧
从CAN 1.2到2.0版本,ID扩展带来格式拆分与兼容挑战,体现硬件升级中“积重难返”现象。保留位(如r0/r1)为后续升级预留空间,类似设计提升协议灵活性。尽管扩展格式略显臃肿,但为兼容性所必需,反映工程实践中妥协与优化的平衡。
 

2.1 帧类型概述

帧类型功能描述
数据帧发送设备主动广播数据
遥控帧接收设备请求特定ID的数据
错误帧设备检测错误时通知其他节点
过载帧接收方通知未准备好接收
帧间隔分隔连续数据帧或遥控帧

2.2 数据帧结构

标准格式(11位ID):

  1. SOF:显性0,打破总线空闲

  2. 仲裁段:

    • 11位报文ID(标识功能与优先级)

    • RTR位(数据帧=0,遥控帧=1)

  3. 控制段:

    • IDE位(标准格式=0)

    • r0保留位

    • DLC(4位,指定数据段长度0-8字节)

  4. 数据段:0-8字节有效数据

  5. CRC段:15位校验码 + CRC界定符(隐性1)

  6. ACK段:

    • ACK槽(发送方隐性1,接收方显性0)

    • ACK界定符(隐性1)

  7. EOF:7位隐性1,帧结束

扩展格式(29位ID):
  • 在标准格式基础上增加:

    • 替代RTR的SRR位(固定隐性1)

    • IDE位(扩展格式=1)

    • 18位扩展ID

2.3 遥控帧

  • RTR=1,无数据段

  • 用于接收设备请求特定ID数据,拥有该数据的节点以数据帧响应

2.4 错误帧与过载帧

  • 错误帧:6位错误标志 + 8位错误界定符

    • 主动错误:6位显性0

    • 被动错误:6位隐性1

  • 过载帧:结构与错误帧类似,用于接收方延缓发送

2.5 位填充规则

  • 规则:每5个相同电平后插入1个相反电平

  • 作用:

    1. 提供定时信息,便于再同步

    2. 防止长时间不变电平导致同步失败

    3. 区分正常数据与错误帧/过载帧(正常数据无连续6个相同电平)

    4. 避免总线误判为空闲(需连续11个隐性1才为空闲)


第三章:总线仲裁机制

一、多设备同时发送的问题
当多个设备同时尝试发送数据时,由于CAN总线仅有一对差分信号线,若无控制机制会导致波形叠加,造成数据损坏,必须通过资源分配规则避免冲突。
二、资源分配的两种规则
资源分配采用“先占先得”和“非破坏性仲裁”两条规则,前者处理已开始发送情况下的后续请求,后者解决多个设备同时发起发送请求的竞争问题。
三、先占先得规则
规定一旦有设备正在发送数据帧或遥控帧,其他设备不得再启动新发送;设备需检测到连续11个隐性位才可判定总线空闲并开始发送,防止中途抢占。
四、未填充的作用补充
位填充不仅用于同步,还防止长时间发送相同数据(如连续隐性位)导致总线误判为空闲状态,从而保障通信稳定性。
五、非破坏性仲裁机制
多个设备同时发送时,依据仲裁段(包括ID和RTR位)从高位到低位逐位比较,ID小者优先获得总线控制权,失败方转入接收状态。
六、线与特性与回读机制
“线与”特性指任一设备发送显性位则总线为显性;回读机制使发送方读取自身发出的电平,若发“1”但读回“0”,说明发生冲突,主动退出仲裁。
七、仲裁过程详解
设备在发送每位后回读总线状态,当某设备发送“1”而读回“0”时,表明其他设备发送了“0”,该设备仲裁失利并停止发送,胜出方继续无损传输。
八、多设备仲裁可行性
即使三个及以上设备同时竞争,每个设备独立执行发一位读一位逻辑,最终必有一个且仅一个胜出,确保仲裁结束后的唯一发送者。
九、ID号与优先级关系
ID号越小,二进制中“1”出现越晚,在仲裁中更晚出现差异且发“1”的一方易失败,因此ID小者优先级高。
十、位填充不影响仲裁
虽然仲裁场执行位填充,但填充位插入规则不会改变原有ID之间的优先级顺序,因相同前缀下双方均会填充,不影响比较结果。
十一、非破坏性仲裁的含义
仲裁对胜出方完全透明,其发送过程不受干扰;失败方在发现冲突后退出,不影响总线正常运行,故称为“非破坏性”。
十二、数据帧与遥控帧优先级
当ID相同时,数据帧优先级高于遥控帧,因RTR位定义为数据帧是0、遥控帧是1,在最后一位仲裁中数据帧胜出。
十三、标准格式与扩展格式优先级
当高11位ID相同,标准格式优先级高于扩展格式,因SRR位为隐性(1),而扩展格式的SRR为显性(0),标准帧在此位胜出。
十四、标准遥控帧与扩展数据帧的特殊情况
两者前11位ID相同且SRR均为1时,直到仲裁段结束后由IDE位决定胜负,标准遥控帧因IDE=0而胜出,虽临界但仍安全无错。
十五、扩展格式带来的复杂性
扩展格式增加了协议复杂度,尤其在仲裁逻辑上带来挑战,但整体难度不大

3.1 资源分配规则

  1. 先占先得:总线被占用时,其他设备不得中途抢占

  2. 非破坏性仲裁:多个设备同时发送时,按仲裁段竞争,胜出方继续无损传输

3.2 仲裁机制

  • 仲裁段:ID + RTR位

  • 比较规则:从高位到低位逐位比较

    • 发0读0、发1读1:继续发送

    • 发1读0:仲裁失败,立即退出发送转为接收

  • 线与特性:任一设备发显性0,总线即为0

  • 回读机制:发送方比较发出位与读回位

3.3 优先级判定

  1. ID号越小,优先级越高

  2. ID相同情况:

    • 数据帧(RTR=0)优先级高于遥控帧(RTR=1)

    • 标准格式(IDE=0)优先级高于扩展格式(IDE=1)

3.4 仲裁特性

  • 非破坏性:胜出方传输不受任何影响

  • 位填充不影响仲裁:填充位不改变原有ID优先级

  • 多设备仲裁:最终必有且仅有一个设备胜出


第四章:错误处理机制

一、CAN总线的错误处理机制
错误处理是CAN总线的安全保障机制,用于在数据传输出错时通知所有设备丢弃错误帧,避免错误数据被使用。虽然无错误处理也能完成基本收发,但在干扰环境下必须依赖该机制保证通信可靠性。
二、五种错误类型及其检测
1. 位错误:发送单元或接收单元回读总线电平时发现发出位与实际电平不一致时产生,覆盖数据帧、遥控帧、错误帧和过载帧的所有位段,但仲裁期间的重同步段除外。
2. 填充错误:在需位填充区域连续检测到6个相同电平时触发,检测范围为SOF至CRC序列段,由发送和接收单元共同检测。
3. CRC错误:接收方计算的数据CRC值与接收到的CRC序列不符时产生,仅由接收单元检测,用于验证数据完整性。
4. 格式错误:当固定格式字段(如ACK delimiter、EOF等)出现非法电平时触发,仅接收单元检测。
5. 应答错误(ACK错误):发送单元在ACK槽未检测到显性电平时判定为无应答,从而产生错误,由发送单元检测。
三、错误通知与错误帧
当任一设备检测到上述错误后,立即发送错误帧中断当前传输。错误帧由错误标志(6位)和错误界定符(8位)组成,分为主动错误标志(6个显性位)和被动错误标志(6个隐性位),前者可破坏总线通信,后者不影响正在进行的通信。
四、错误状态机制
设备根据TEC(发送错误计数器)和REC(接收错误计数器)的值决定其所处状态:
- 主动错误状态:初始状态,可正常通信并发送主动错误帧;
- 被动错误状态:TEC>127或REC>127时进入,仍可通信但只能发被动错误帧;
- 总线关闭状态:TEC>255时进入,禁止通信,需检测128次连续11个隐性位后恢复为主动状态。
五、错误计数器更新规则
- 接收单元检测出错:REC+1;
- 发送单元发送错误标志时检测到位错误:TEC+8;
- 发送或接收单元在发送主动错误/过载标志时出错:对应计数器+8;
- 正常发送完成后TEC减1(最低为0);
- 正常接收完成后REC减1(≤127时-1,>127时直接置127);
- 处于总线关闭状态且检测到128次连续11个隐性位时,TEC和REC清零并恢复主动状态。
六、错误帧波形实例分析
以ACK错误为例展示三种典型波形:
1. 正常传输:ACK槽有显性位表示应答成功,帧结束后进入3位帧间隔;
2. 主动错误状态下的ACK错误:发送方检测无应答后插入主动错误帧(6个显性位+8个显性位界定符),随后加3位帧间隔;
3. 被动错误状态下的ACK错误:前16次为主动错误帧,之后转为被动状态,错误帧后增加8位延迟传送,再加3位帧间隔,体现被动设备的通信限制。
七、参考资料与手册说明
建议学习者阅读《CAN入门书》和《CAN总线协议V2.0》中英文手册,其中涵盖帧格式、仲裁、位填充、错误处理、同步机制等内容。特别指出CAN 2.0分为A部分(仅标准格式)和B部分(支持标准与扩展格式),现代设备通常兼容CAN 2.0B。
以上内容由蓝心大模型生成,仅供参考。

 

4.1 错误类型

错误类型检测条件检测单元
位错误发出位与读回位不一致(仲裁段、ACK槽除外)发送/接收单元
填充错误需位填充区域出现连续6个相同电平发送/接收单元
CRC错误计算CRC与接收CRC不一致接收单元
格式错误固定格式字段出现非法电平接收单元
应答错误ACK槽未检测到显性电平发送单元

4.2 错误状态管理

状态转移:
  1. 主动错误状态(初始状态):

    • 可正常通信,发送主动错误帧(6显性0)

    • TEC>127或REC>127 → 被动错误状态

  2. 被动错误状态:

    • 可通信,发送被动错误帧(6隐性1)

    • TEC>255 → 总线关闭状态

    • TEC≤127且REC≤127 → 主动错误状态

  3. 总线关闭状态:

    • 禁止通信

    • 检测128次连续11隐性位 → TEC/REC清零,恢复主动状态

错误计数器更新:
  • 接收出错:REC+1

  • 发送出错:TEC+8

  • 正常发送完成:TEC-1(最低0)

  • 正常接收完成:REC-1(>127时置127)

4.3 错误帧结构

  • 错误标志:6位(主动=显性0,被动=隐性1)

  • 错误界定符:8位隐性1

  • 帧间隔:主动错误状态=3隐性位,被动错误状态=3隐性位+8延迟位


第五章:协议演进与设计智慧

5.1 从CAN 1.2到CAN 2.0

  • CAN 1.2:仅11位ID标准格式

  • CAN 2.0:增加29位ID扩展格式,通过IDE位区分

  • 兼容性设计:保留位(r0、r1)为升级预留空间

5.2 设计理念

  1. 分布式仲裁:基于ID优先级,无需中央控制器

  2. 可靠性保障:CRC校验、错误处理、位填充同步

  3. 灵活性:数据长度可调、波特率可配置

  4. 鲁棒性:线与特性、回读机制、状态管理

5.3 实际应用建议

  • ID分配:重要消息分配小ID,确保高优先级

  • 波特率配置:根据总线长度和干扰环境选择

  • 错误处理:合理设置错误计数器门限值

  • 同步参数:根据时钟精度调整SJW和相位缓冲段

第六章:STM32CAN外设:

CAN外设过滤器:

在CAN总线网络中,所有节点默认会接收到总线上出现的所有数据帧。但一个特定的节点(比如一个只关心电机转速的控制器)通常只关心其中极少数特定ID的报文。

如果没有过滤器:

  1. CPU负荷巨大:所有报文都会触发CPU中断,导致CPU忙于处理不关心的数据,严重影响实时性。

  2. 软件复杂:需要在软件中手动检查每个报文的ID,并决定是否处理,代码臃肿且效率低下。

过滤器的作用就是充当一个位于CAN硬件和CPU之间的“智能门卫”。它只允许那些“感兴趣”的报文通过并产生中断或存入邮箱,从而极大地减轻CPU的负担。


二、过滤器的工作原理:黑白名单机制

CAN过滤器本质上是一种硬件级别的报文筛选机制。它通常集成在CAN控制器中,在报文到达CPU之前就完成ID匹配。

它遵循两种基本策略:

  1. 标识符列表模式

    • 理念:白名单。只允许ID与预设列表完全匹配的报文通过。

    • 特点:匹配精度高,但占用资源多。一个过滤器单元只能存放一个ID。

  2. 标识符屏蔽位模式

    • 理念:按规则过滤。通过一个掩码来指定ID中哪些位需要精确匹配,哪些位不关心。

    • 特点:灵活,一个过滤器可以匹配一组ID,资源利用率高。


三、过滤器的核心组件:ID与掩码

理解屏蔽位模式是掌握过滤器的关键。

  • 标识符:你想要过滤的目标ID。

  • 掩码:决定了ID中哪些位需要被检查。

    • 掩码位 = 1:对应的ID位必须严格匹配。

    • 掩码位 = 0:对应的ID位不关心(可以是0或1)。

举例说明:

假设我们只想接收ID为 0x123 的标准数据帧。

  • 配置为列表模式:

    • 过滤器ID = 0x123

    • 任何不是 0x123 的ID都会被挡住。

  • 配置为屏蔽位模式:

    • 过滤器ID = 0x123

    • 过滤器掩码 = 0x7FF (二进制为 111 1111 1111,11位全部为1)

    • 解读:因为掩码全是1,所以11位ID必须每一位都和 0x123 完全一致。效果和列表模式一样。

更强大的例子:匹配一组ID

假设我们想接收所有ID在 0x120 到 0x12F 范围内的标准数据帧。这些ID的二进制前7位都是相同的。

text

0x120 = 001 0010 0000
0x12F = 001 0010 1111

可以看到,高7位 (0010010) 是相同的,低4位在变化。

  • 配置为屏蔽位模式:

    • 过滤器ID = 0x120 (作为基准)

    • 过滤器掩码 = 0x7F0 (二进制为 111 1111 0000)

  • 工作过程:

    1. 收到一个ID,比如 0x125 (001 0010 0101)。

    2. 硬件用掩码 0x7F0 去“遮盖”这个ID,只保留高7位,得到 0010010。

    3. 硬件也用同样的掩码去“遮盖”过滤器ID 0x120,得到 0010010。

    4. 比较两者,结果相等 (0010010 == 0010010)。

    5. 结论:匹配成功,允许报文通过。

通过这个配置,我们只用一个过滤器单元就匹配了16个不同的ID,极大地提高了过滤效率。


四、STM32中的过滤器配置详解

以STM32为例,其过滤器系统非常灵活但也相对复杂。

1. 工作模式
  • 屏蔽位模式:如上所述,使用ID + 掩码。

  • 列表模式:无需掩码,提供一组ID列表,只有完全匹配的才能通过。

2. 尺度(宽度)
  • 32位宽:

    • 可以配置为2个32位的过滤器。

    • 在屏蔽位模式下,可以存放1个32位的ID和1个32位的掩码。

    • 在列表模式下,可以存放2个32位的ID。

  • 16位宽:

    • 可以配置为4个16位的过滤器。

    • 在屏蔽位模式下,可以存放2个16位的ID和2个16位的掩码(形成两组ID-掩码对)。

    • 在列表模式下,可以存放4个16位的ID。

为什么有32位? 为了支持扩展帧。扩展帧ID为29位,加上IDE位、RTR位等,需要32位来存放。

3. 绑定与FIFO
  • 绑定:过滤器组可以关联到特定的CAN接收FIFO(FIFO0 或 FIFO1)。

  • FIFO:CAN控制器有两个硬件FIFO(先进先出缓冲区)来存放接收到的报文。当报文通过过滤器后,会被存入对应的FIFO,并可能触发中断。


五、实战配置示例

假设在STM32上,我们想实现以下过滤需求:

  1. 只接收标准数据帧 0x111。

  2. 接收所有以 0x20 开头的扩展数据帧(即ID范围是 0x20000000 到 0x20FFFFFF)。

我们使用一个32位屏蔽位模式的过滤器。

配置步骤:
  1. 选择模式与尺度:32位屏蔽位模式。

  2. 配置第一个过滤器(用于标准帧 0x111):

    • 过滤器ID: 0x111 << 21

      • 为什么左移21位?因为在32位模式下,标准帧的ID需要左移对齐到特定位置。

    • 过滤器掩码: 0x7FF << 21

      • 掩码同样左移,表示11位ID必须完全匹配。

  3. 配置第二个过滤器(用于扩展帧组 0x20XXXXXX):

    • 过滤器ID: 0x20000000

      • 直接使用29位扩展ID,因为我们希望匹配高5位 0x20。

    • 过滤器掩码: 0x1F000000

      • 这个掩码表示只关心扩展ID的最高5位(0x1F对应5位),低24位不关心。这样,所有最高5位是 0x20 的ID都会匹配。

对应代码片段(HAL库):

c

CAN_FilterTypeDef sFilterConfig;

// 使用过滤器组0
sFilterConfig.FilterBank = 0;
// 关联到FIFO0
sFilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0;
// 32位屏蔽位模式
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
// 激活过滤器
sFilterConfig.FilterActivation = ENABLE;

// 配置ID和掩码
sFilterConfig.FilterIdHigh = ...;   // 根据ID计算的高16位
sFilterConfig.FilterIdLow = ...;    // 根据ID计算的低16位
sFilterConfig.FilterMaskIdHigh = ...; // 根据掩码计算的高16位
sFilterConfig.FilterMaskIdLow = ...;  // 根据掩码计算的低16位

if (HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK)
{
  Error_Handler();
}
// 启动CAN
if (HAL_CAN_Start(&hcan) != HAL_OK)
{
  Error_Handler();
}
// 使能FIFO0中断
if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
{
  Error_Handler();
}

总结

特性标识符列表模式标识符屏蔽位模式
理念白名单按规则过滤
精度高(精确匹配)可调(可匹配一组)
资源利用低(1单元对1ID)高(1单元对多ID)
适用场景少量关键报文需要接收一组相关报文

一、标识符过滤器结构
每个过滤器由两个32位寄存器FX21和FX22组成,配合FSC(位宽)、FBM(模式)、FFA(FIFO关联)和FACT(激活)等配置位实现报文筛选功能,共14个独立过滤器。
二、过滤器工作模式
过滤器支持四种组合模式:32位屏蔽模式、32位列表模式、16位屏蔽模式、16位列表模式,根据ID类型和匹配需求选择合适模式以提高资源利用率。
三、列表模式应用
在列表模式下,直接将目标ID写入寄存器,报文ID与列表中任一匹配即可通过;32位模式可存2个ID,16位模式拆分为4个16位寄存器可存4个标准ID。
四、屏蔽模式原理
屏蔽模式使用一个ID寄存器和一个掩码寄存器,掩码为1表示对应位必须匹配,为0表示无关,适用于过滤一组具有相同高位特征的ID。
五、实际配置示例
通过多个场景演示配置过程,如接收特定几个无规律ID用16位列表模式,接收连续范围ID用16位或32位屏蔽模式,含扩展ID时需使用32位模式并正确设置IDE位。
六、测试模式功能
提供三种测试模式:静默模式用于监听总线不干扰通信;环回模式实现自发自收调试;环回静默模式完全隔离外部进行自检。
七、工作模式切换
包含初始化模式(配置阶段)、正常模式(收发报文)和睡眠模式(低功耗),模式切换需请求并等待硬件确认,确保安全转换。
八、位时间与波特率计算
位时间由同步段(固定1TQ)、BS1段(1-16TQ)和BS2段(1-8TQ)构成,波特率=APB1时钟/(BRP+1)/(1+TS1+1+TS2+1),通过寄存器配置实现精确时序控制。
九、中断机制
CAN外设占用4个中断向量:发送中断、FIFO0接收中断、FIFO1接收中断、状态改变/错误中断,各中断独立处理并可设置优先级。
十、时间触发通信
基于ISO11898-4协议,启用后内置16位计数器记录时间戳,支持发送报文时自动插入时间信息至数据段最后两字节,实现全网节点同步调度。
十一、错误管理与恢复
具备发送/接收错误计数器,支持主动错误、被动错误、离线状态转换;STM32特有ABOM位控制离线后是否自动恢复,关闭时需软件干预才能重启。
十二、参考手册对照
知识点来源于STM32参考手册第22章,建议结合手册中的框图、寄存器说明和实例进一步理解CAN外设细节。
十三、代码实践路径
先进行单设备环回测试验证程序逻辑,成功后再接入收发器和多个设备切换至正常模式实现多机通信,分步调试确保软硬件稳定性。

CAN初始化结构体配置(STM32):

CAN_InitTypeDef mycan_Init;
mycan_Init.CAN_ABOM = DISABLE;      // 禁止自动离线管理
mycan_Init.CAN_AWUM = DISABLE;      // 禁止自动唤醒
mycan_Init.CAN_BS1 = CAN_BS1_2tq;   // PBS1 = 2TQ
mycan_Init.CAN_BS2 = CAN_BS2_3tq;   // PBS2 = 3TQ
mycan_Init.CAN_Mode = CAN_Mode_LoopBack; // 环回模式
mycan_Init.CAN_NART = DISABLE;      // 使能自动重传
mycan_Init.CAN_Prescaler = 48;      // 预分频系数
mycan_Init.CAN_RFLM = DISABLE;      // FIFO不锁定
mycan_Init.CAN_SJW = CAN_SJW_2tq;   // 同步跳转宽度=2TQ
mycan_Init.CAN_TTCM = DISABLE;      // 禁止时间触发
mycan_Init.CAN_TXFP = DISABLE;      // 按ID优先级发送

// 波特率计算:36MHz / 48 / (1+2+3) = 125kbps

第七章CAN实验:

01-CAN总线单个设备环回测试:

#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(uint32_t ID, uint8_t Length, uint8_t *Data)
{
	CanTxMsg TxMessage;
	TxMessage.StdId = ID;
	TxMessage.ExtId = ID;
	TxMessage.IDE = CAN_Id_Standard;		//CAN_ID_STD
	TxMessage.RTR = CAN_RTR_Data;
	TxMessage.DLC = Length;
	for (uint8_t i = 0; i < Length; i ++)
	{
		TxMessage.Data[i] = Data[i];
	}
	
	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(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
	{
		//...
	}
}
#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总线三个设备互相通信:

#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_Normal;
	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(uint32_t ID, uint8_t Length, uint8_t *Data)
{
	CanTxMsg TxMessage;
	TxMessage.StdId = ID;
	TxMessage.ExtId = ID;
	TxMessage.IDE = CAN_Id_Standard;		//CAN_ID_STD
	TxMessage.RTR = CAN_RTR_Data;
	TxMessage.DLC = Length;
	for (uint8_t i = 0; i < Length; i ++)
	{
		TxMessage.Data[i] = Data[i];
	}
	
	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(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
	{
		//...
	}
}
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"

uint8_t KeyNum;
uint32_t TxID = 0x777;
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);
		}
	}
}

03-标准格式-扩展格式-数据帧-遥控帧:

#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);
			}
		}
	}
}

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐