1 STM32中printf使用

1.1 选择use MicroLIB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
*@brief 使用microLib的方法
*/
int fputc(int ch, FILE *f)
{
USART_SendData(USART1, (uint8_t) ch);

while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) {}

return ch;
}
int GetKey (void) {

while (!(USART1->SR & USART_FLAG_RXNE));

return ((int)(USART1->DR & 0x1FF));
}

1.2 不选择use MicroLIB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
*@brief 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB
*/
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;

};

FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
_sys_exit(int x) // 若报错,则可以添加返回类型void
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif

2 CAN通信协议

2.1 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,用于区分消息功能,同时决定优先级(ID号小的优先发送)
    • 可配置1~8字节的有效载荷(强于串口通信一次只能发送一个1个字节)
    • 可实现广播式请求式两种传输方式
    • 应答、 CRC 校验、位填充、位同步、错误处理等特性

    虽然差分信号各个设备不共地可以使用,但是这可能会让收发器承受较高的共模电压,因此实际应用中为了安全和稳定,CAN通信一般还是会共地的(即再额外使用一根线,把各个设备的GND连接在一起)。

    此外,如果除了传递信号,还需要为设备提供供电电源,那么就需要再添加一根电源正的线路。

    电源正、电源负(GND)、CAN_H、CAN_L这4根线一起构成完整的供电和通信线路。

2.2 CAN相关的硬件电路

2.2.1 CAN连接线路框图

如上图所示,每个设备通过CAN收发器挂载在CAN总线网络上,CAN控制器引出的TX和RX与CAN收发器相连(注意这里CAN控制器引出的TX与CAN收发器的TX相连,CAN控制器引出的RX与CAN收发器的RX相连,无需交叉),CAN收发器引出的CAN_H和CAN_L分别与总线的CAN_H和CAN_L相连。

高速CAN使用闭环网络,CAN_H和CAN_L(双绞线)两端添加120Ω的终端电阻,这两个终端电阻的作用有2个:① 防止回波反射,尤其是高频信号、远距离传输的场景,② 在没有设备操作CAN总线的时候,将两根差分线的电压“收紧”,使其电压一致(简单说就是在没有设备操作CAN总线的时候,这两个终端电阻就像“弹簧”一样,会将两根线的电压拉到同一水平)。

关于“回波反射”:若两端不加终端电阻,信号波形会在终端反射,进而影响原信号,产生“回波反射”的原因可以学习《传输线理论》,

低速CAN使用开环网络,CAN_H和CAN_L其中一端添加2.2kΩ的终端电阻。

2.2.2 电平标准

CAN总线采用差分信号,即两线电压差(VCAN_H-VCAN_L)传输数据位。

  1. 高速CAN规定:电压差为0V时表示逻辑1(隐性电平),电压差为2V时表示逻辑0(显性电平);
  2. 低速CAN规定:电压差为-1.5V时表示逻辑1(隐性电平),电压差为3V时表示逻辑0(显性电平);

2.2.3 CAN收发器-TJA1050 (高速CAN)

说明:

  1. VCC接5V
  2. Vref是参考电压,可以不用
  3. S用于选择高速模式或者是静默模式,也可以不用
  4. 其他Pin按2.2.1 CAN连接线路框图来连接
  5. 内部框图说明:
    • a) RECEIVER时刻检测压差,并输出给两个场效应管。如果有压差,即RECEIVER==1,下管导通,RXD=0; 如果无压差,即RECEIVER == 0,上管导通,RXD = 1;
      • 注意:这里其实有一个别扭的地方:对于CAN收发器来说,CAN的压差是输入,Pin4是输出,那么输出为什么不叫TXD反而叫RXD呢(RXD不是接收吗?真他么别扭)? 估计是因为连MCU时没有交叉——RX接RX,TX接TX,所以对于MCU来说,收发器的RXD确实是MCU的接收,倒也没毛病;
    • b) 再看TXD,如果TXD == 1,与DRIVER相连的上下管都断开,也就是不干预CAN_H和CAN_L。因此Pin6、Pin7在外部“收紧电阻”作用下呈现隐性,逻辑1(按上图的画法,下管应该是导通的,但是江科大说这种情况会是断开,所以我猜DRIVER与下管相连经过了一个反相器,猜的没空研究),如果TXD == 0,与DRIVER相连的上下管都导通(同样的我猜下管有反相器),因此Pin6 == GND,pin7 == VCC,显性,逻辑0(有人说这里又有一个问题:此时压差是VCC-GND = 5V,与图4定义不符。你别忘了那两个25KΩ电阻)
    • c) 200uA电流源作用:使得TXD默认是上拉的,因此TXD浮空时与TXD = 1时效果一样。

小结

2.3 CAN总线帧格式

帧格式是人为规定的,也就是当我们发送一串0101…的数据流,需要规定每一位的作用是什么,要不然接收方只能收到这个数据流而无法进行解析。

CAN总线协议规定了5种数据帧格式:

帧格式 说明
数据帧 发送设备主动发送数据(广播式)
遥控帧 接受设备主动请求数据(请求式)
错误帧 某个设备检测出错误时向其他设备通知错误
过载帧 接受设备通知其尚未做好接受准备
帧间隔 用于将数据帧及遥控帧与前面的帧分离开

2.3.1 数据帧

如上图所示,数据帧有标准格式和扩展格式两种,CAN总线标准刚指定的时候只有标准格式,后来才新增的扩展格式,扩展格式增加了ID位数,能承载更多种类的ID。

注意,上图示例发送的电平的高低是通过颜色区分的,图中标注的1、11、1、1、1、4、0~64、15等指的是这块占据的比特位位数。

下面结合上图简单分析一下数据帧的格式。

空闲状态:首先,在开始发送数据帧之前,总线必须处于空闲状态(总线是隐性电平,两根线收紧,表示高电平);

SOF位:数据帧第一位,表示CAN总线从静默状态隐性1变为需要干活显性0(两线张开),同时也告诉接收方,接下来一段时间里,发送方再释放总线(总线处于隐性1)那就不代表空闲,二是发送方发送的数据1;

报文ID:SOF之后紧接着发送ID,标准格式ID是11位,既可用于表示后面要发送的数据的功能(因为总线上各种报文消息都有,若没有ID加以区分,就混乱了),又可以表示优先级,当多个设备同时要向总线上发送数据时,可根据CAN的仲裁规则,ID小的报文优先发送,ID大的报文等待下一次总线空闲的时候再发松,不同功能的数据帧其ID都不同,否则两个设备同时发送两个相同ID的数据帧就无法仲裁了;

RTR位:远程请求标志位,用于区分数据帧还是遥控帧的,数据帧固定为显性0,遥控帧固定为隐性1,报文ID+RTR位称为仲裁段,主要由报文ID仲裁,相同报文ID的数据帧和遥控帧,数据帧的优先级大于遥控帧(也就是数据帧和遥控帧的ID是可以相同的);

其实仲裁段这一段和I2C通信里面的7位从机地址+1位读写位相类似。I2C有多主机模式,可以把7位从机地址+1位读写位视为I2C的仲裁段。

CAN总线的设计取消了从机地址和读写位的概念,也即CAN不对从机地址进行分配,而是对报文分配不同ID,这里11位报文ID表示报文功能,1位RTR表示数据帧(类似写入)还是遥控帧(类似读取)。这样做的好处是同一条报文可以被多个设备同时接收。如果规定每个设备只能接收一个固定ID的消息,那这里的报文ID就和从机地址是一样的了。

IDE位:以前是保留位,叫r1,现在叫ID扩展标志位,用于区分标准格式还是扩展格式。标准格式固定为显性0,扩展模式固定为隐性1;

r0:保留位,默认给显性0;

DLC:表示后面数据段DATA的长度,单位字节。上面提到CAN总线一帧数据可以有1~8个字节,DLC就是具体指定有几个字节,DLC与DATA长度必须对应,DLC发0001,DATA就只能发1个字节,不能发其他数量的字节;

CRC:高效的校验算法(循环冗余校验)。从SOF到DATA这些数据位进行CRC校验;

CRC界定符:必须是隐性1,表示数据位已经传输完毕;

ACK槽:作用是应答,发送方发送完一帧数据,到底有没有设备收到呢?这就靠ACK位实现,这里的应答设计和I2C的应答类似。具体逻辑是:当发送方发完1帧数据,在ACK槽这一位发送方释放总线,总线回到默认状态隐性1,如果接收方收到数据了,那么就会在ACK槽这一位时把总线拉开,总线变成显性0。发送方释放总线后在ACK槽这一位时会读取总线状态,如果发送方读取到0,发送方就知道有接收方接收到了数据,如果发送方读取到1,发送方就知道接收方没有接收数据,这时候发送方选择可以不管,也可以重新发送直到接收方应答。总之,ACK槽这一位,操作总线的控制权是有一个短暂交换的。为了给交接流程给足时间,因此设计了CRC界定符和ACK界定符。CRC界定符必须是1,理由:首先是分隔,其次是ACK槽之前,总线必须被释放,所以CRC界定符只能是隐性1。

ACK界定符:接收方如果收到了,但是不能一直占着总线,所以在ACK界定符就得松手,松手就是隐性1。

2个注意事项:
① ACK槽可以有多个接收方应答,反正多少个应答都会显示0
② 发送方不是发完一整帧数据后才被接收方接收的,而是发送方每发1位,接收方就立马收到一位

EOF:发送方发7个隐性1,作为帧结束,表示数据位已经传输完毕。

  • 扩展格式的不同点:
    • 在标准格式的11位ID后新增18位ID;
    • SRR:类似RTR,固定给1
    • IDE:本身就是用来区分标准帧还是扩展帧,扩展帧就给1

2.3.2 遥控帧

遥控帧无数据段,RTR为隐性电平1,其他部分与数据帧相同(也即遥控帧其实和数据帧非常类似,只是遥控帧没有数据段有效载荷这部分)。接收方/请求方发出遥控帧,遥控帧的ID表示要请求的数据,响应请求的一方,通过相同ID的数据帧反馈数据。当请求和反馈数据同时发生时,数据帧拥有更高的优先级。

目前我们了解到,CAN总线的数据主要靠发送方自觉广播出来的,一般发送方会定一个周期,定时广播自己的数据,但如果发送方没有及时发出数据,或者这个数据的使用频率太低了、广播太频繁了,大家都用不着,浪费总线资源,又或者广播太慢了,偶尔有用的话,又及时拿不到。

这样我们就可以规定,发送方就不要主动广播这个数据了,而是如果有设备需要的话,首先接收方发出一个遥控帧,遥控包含报文ID(其实遥控帧也是广播出来的),CAN总线上每个设备都能收到遥控帧,如果其中某个设备有这个ID的数据,该设备就会再通过数据帧广播出来,这样接收方就能及时获取这个数据了。

所以,遥控帧实现请求式数据传输。请求式传输,每传输一次数据都需要一问一答(一来一回)两个过程,适合那种使用频率低,但偶尔又需要集中用几次的数据。如果数据使用频率高,再用遥控帧请求式就不合适了,因为请求产生的遥控帧本身也在占用总线资源,所以使用频率高的数据,可以直接用广播式,发送方无脑发就行了,反正接收方也要经常使用,不是很浪费。

2.3.3 错误帧

总线上所有设备都会监督总线的数据,一旦发现“位错误”或“填充错误”或“CRC错误”或“格式错误”或“应答错误” ,这些设备便会发出错误帧,错误帧可以叠加在数据帧上,可以破坏数据帧的数据,同时终止当前的发送设备。

错误分为主动错误和被动错误,设备默认处于主动错误状态。处于主动错误状态的设备,检测出错误时会连续发6个显性0(发送显性位就是拉开总线,只要有一个设备拉开了总线,总线就必然处于显性状态,即0和1相遇时总线总是处于0状态,这就是“线与”特性),所以主动错误标志的6个显性位,必然会破坏正常传输的数据,其他设备检测到错误标志就会抛弃这个数据。

如果主动错误产生太频繁了,说明这个设备不太可靠,设备就会进入被动错误状态,处于被动错误状态的设备,检测出错误时会连续发6个隐性1(发送隐性位就是不去碰总线,不碰总线,就不会破坏总线上别人发的数据,但是会破坏自己发的数据,自己的数据有问题,自己破坏掉,不会影响到别的设备发的数据)。

发完6位错误标志后,再跟8个隐性1,作为错误界定符,这就是错误帧的定义。但是图中会发现在错误标志和错误界定符中间还有0~6位的延长时间呢?这是因为一个设备发出的错误标志可能会引发其他设备连带产生错误标志,多个设备的错误标志叠加起来,这个标志位的长度就可能不止6位了,所以后面这里画的是错误标志可能会延长0~6位的时间。

2.3.4 过载帧

当接收方收到大量数据而无法处理时,其可以发出过载帧,延缓发送方的数据发送,以平衡总线负载,避免数据丢失。

过载帧格式和错误帧类似,只是过载帧不是在错误的时候产生,而是发送方发太快,接收方处理不了的时候,由接收方发送过载帧。因为数据是发送方主动发出的,接收方无法直接调整发送方的发送频率,那怎么办,只能产生一个和错误帧类似的过载帧了,将数据破坏掉,发送方发不出去就会重试。在这个破坏和重试的过程中,发送数据就被延误了(相当于降低了发送方发送数据的频率),同时也间接告诉发送方,接收方收不了。如果发送方有相应的处理逻辑,它就会降低一点发送频率。

2.3.5 帧间隔

用途是将数据帧及遥控帧与前面的分离开。这个你知道连续发送数据帧时,其中间会有一小段帧间隔就行。

帧间隔也分为主动错误状态和被动错误状态。主动错误状态的帧间隔是3位,被动错误状态的帧间隔是3位加8位延迟传输(被动状态表示设备不太可靠,不可靠你就别那么着急发数据了),延迟传送,会将设备置于仲裁不利的处境,尽量减少此设备干扰总线,这就是帧间隔。

2.4 位填充

上面讲的时序波形,数据帧和遥控帧,这两个在最终发送到总线之前,还要经过位填充的处理。

2.4.1 位填充规则

位填充规则:发送方每发送5个相同电平后,自动追加一个相反电平的填充位,接收方检测到填充位时,会自动移除填充位,恢复原始数据。例如:

2.4.2 位填充的作用

  1. 增加波形的定时信息,利于接收方执行“再同步”,防止波形长时间无变化,导致接收方不能精确掌握数据采样时机;
  2. 将正常数据流与“错误帧”和“过载帧”区分开,标志“错误帧”和“过载帧”的特异性(“错误帧”和“过载帧”会有6个以上的相同电平,但是位填充后的正常数据不会出现6个连续一样的电平);
  3. 保持CAN总线在发送正常数据流时的活跃状态,防止被误认为总线空闲(因为CAN总线规定,当总线出现连续11个隐性1后即认为总线空闲,所以如果没有位填充,数据段就有可能出现连续64个隐性1,别的设备就可能认为当前是总线是空闲的,如果别的设备也发数据,那就产生冲突了);

2.5 CAN波形实例

几个细节:

  1. 填充位会在波形上真实体现,也就是黄颜色的数字1;
  2. 想发数据0xAA,二进制是1010 1010,实际波形也是1010 1010,可知CAN是高位先行
  3. CRC是由CAN控制器自动生成,接收时由CAN控制器自动校验,CRC计算时是会剔除填充位的;
  4. EOF为什么不加填充位?
    • 江科大回答:这一块没必要,不需要填充。 因为手册里特别规定了:数据帧或远程帧(CRC 界定符、应答场和帧末尾)的剩余位场形式相同,不填充。

2.6 接收方数据采样

通过之前的章节,我们了解到,挂载在CAN总线上的所有设备初始都默认为接收方,当某一个设备想要广播自己的数据时,它就会主动出击,变成发送方,拉开或释放总线,使总线产生一段波形。那这个发送方产生这样一段波形后,其他的所有接收方该如何准确地采样,得到每一个数据位是1还是0呢

  1. CAN总线是一种异步通信,没有时钟线,总线上的所有设备通过约定波特率的方式确定每一个数据位的时长
  2. 发送方以约定的位时长每隔固定时间输出一个数据位
  3. 接收方以约定的位时长每隔固定时间采样总线的电平,输入一个数据位
  4. 理想状态下,接收方能依次采样到发送方发出的每个数据位,且采样点位于数据位中心附近

2.6.1 接收方遇到的问题

① 接收方以约定的位时长进行采样,但是采样点没有对齐数据位中心附近:

解决方式:位同步

② 接收方刚开始采样正确,但是时钟有误差,随着误差积累,采样点逐渐偏离

解决方式:再同步

2.6.2 位时序

为了灵活调整每个采样点的位置,使采样点对齐数据位中心附近,CAN总线对每一个数据位的时长进行了更细的划分,分为同步段(SS)、传播时间段(PTS)、相位缓冲段1(PBS1)和相位缓冲段2(PBS2),每个段又由若干个最小时间单位(Tq)构成。

正常的时候,波形跳变沿要出现在同步段SS,采样点设置在PBS1和PBS2之间(可通过调节PBS1和PBS2的时长灵活调整采样点)。

2.6.3 硬同步(硬件同步)

作用:使得接收方的第一个采样点和波形的第一位对齐。

  1. 每个设备都有一个位时序计时周期,当某个设备(发送方)率先发送报文,其他所有设备(接收方)收到SOF的逻辑下降沿时,接收方会将自己的位时序计时周期拨到SS段的位置,与发送方的位时序计时周期保持同步
  2. 硬同步只在帧的第一个下降沿(SOF下降沿)有效
  3. 经过硬同步后,若发送方和接收方的时钟没有误差,则后续所有数据位的采样点必然都会对齐数据位中心附近

图中显示,因为发送方是一定会在自己的SS段开始发送数据的,因此当接收方检测到SOF下降沿不在自己的SS段内部的时候,会立刻调整自己的小时钟(上图中右侧)把自己的SS段与波形的下降沿对齐(也就是从上图的上子图变成下子图)。

2.6.4 再同步(重新同步)

作用:补偿累积时钟误差。

  1. 若发送方或接收方的时钟有误差,随着误差积累,数据位边沿逐渐偏离SS段,则此时接收方根据再同步补偿宽度值(SJW,宽度可自己指定,一半配置在1∽4Tq)通过加长PBS1段,或缩短PBS2段,以调整同步;
  2. 再同步可以发生在第一个下降沿之后的每个数据位跳变边沿(也即普通的数据跳变边沿是再同步的参考);

这个就跟我们平时用的钟表一样,里面可能有个时间补偿选项,如果钟表有误差,可以选择+10s或者-10s。这就是一个简单的补偿方法。

这里SJW也是一样,当位时序周期这个秒表有误差时,可以选择加1∽4个Tq或者减1∽4个Tq。

注意:

SJW是再同步补偿的最大宽度值,比如时序误差了2个Tp,SJW配置的是4Tp,实际上只会补偿2个Tp。所以结论是:如果误差值小于或等于SJM指定值则误差几个Tq,就补偿几个Tq,这种情况下的位时序调整和硬同步差不多;如果误差值大于SJW指定值,则这时无论误差多大就都只会补偿SJW指定的Tq数(这样能避免接收方因为噪声干扰而补偿过度)。

再同步,本来就是用来补偿时钟的微小误差,误差本来就不大,所以,补偿值必须也要有个最大限制,以免由于噪声干扰,补偿过度。

2.6.5 波特率的计算

波特率就是数据位的传输速率,单位是bps,意为每s传输多少个bit位。PS:波特率本实的单位是波特(Baud)
意为码元/S,bps是比特率的单位,但是在二进制调制下,波特率的值=比特率的值,所以波特率和比特率经常会混为一谈。

2.7 仲裁

仲裁主要是指总线的资源分配规则,也就是多个设备想同时发送波形时,总线如何进行资源分配。

  • 问题:CAN总线只有一对差分信号线,同一时间只能有一个设备操作总线发送数据,若多个设备同时有发送需求,该如何分配总线资源?
  • 解决问题的思路:制定资源分配规则,依次满足多个设备的发送需求,确保同一时间只有一个设备操作总线。

2.7.1 仲裁规则1 - 先占先得

如上图所示的情况,在一个设备发送波形的中途,另一个设备想开始一个新的发送,这种情况比较好处理,简单说就是先占先得,具体仲裁规则如下:

  1. 若当前已经有设备正在操作总线发送数据帧/遥控帧,则其他任何设备不能再同时发送数据帧/遥控帧(可以发送错误帧/过载帧破坏当前数据);
  2. 任何设备检测到连续11个隐性电平,即认为总线空闲,只有在总线空闲时,设备才能发送数据帧/遥控帧;
  3. 一旦有设备正在发送数据帧/遥控帧,总线就会变为活跃状态,必然不会出现连续11个隐性电平,其他设备自然也不会破坏当前发送;
  4. 若总线活跃状态其他设备有发送需求,则需要等待总线变为空闲,才能执行发送需求;

2.7.2 仲裁规则2 - 非破坏性仲裁

  1. 若多个设备的发送需求同时到来或因等待而同时到来,则CAN总线协议会根据ID号(仲裁段)进行非破坏性仲裁,ID号小的(优先级高)取到总线控制权,ID号大的(优先级低)仲裁失利后将转入接收状态,等待下一次总线空闲时再尝试发送;
  2. 实现非破坏性仲裁需要两个要求:
    • 线与特性:总线上任何一个设备发送显性电平0时,总线就会呈现显性电平0状态,只有当所有设备都发送隐性电平1时,总线才呈现隐性电平1状态,即:0 & X & X = 01 & 1 & 1 = 1
    • 回读机制:每个设备发出一个数据位后,都会读回总线当前的电平状态,以确认自己发出的电平是否被真实地发送出去了,根据线与特性,发出0读回必然是0,发出1读回不一定是1

非破坏性仲裁过程

数据位从前到后依次比较,出现差异且数据位为1的设备仲裁失利

位填充会不会影响仲裁?

首先说明,位填充的作用范围是:帧起始、仲裁场、控制场、数据场以及CRC序列,所以在仲裁场里,会执行位填充,这就有个问题需要我们考虑:位填充加入一些相反位,会不会导致仲裁优先级的变化?

答案就是不会的,可以自己试着举几个例子看看。

注意点1:数据帧和遥控帧的优先级

数据帧和遥控帧ID号一样时,数据帧的优先级高于遥控帧。

注意点2:标准格式和扩展格式的优先级

标准格式11位ID号和扩展格式29位ID号的高11位一样时,标准格式的优先级高于扩展格式(SRR必须始终为1,以保证此要求)。

2.8 错误处理

2.8.1 错误类型

错误共有5种:位错误、填充错误、CRC错误、格式错误、应答错误。

错误类型 错误内容 错误的检测帧 检测单元
位错误 比较输出电平和总线电平(不含填充
位),当两电平不一样时所检测到的
• 数据帧(SOF∼EOF)
• 遥控帧(SOF∼EOF)
• 错误帧
• 过载帧
发送单元
接收单元

填充错误
在需要位填充的段内,连续检测到
6位相同的电平时所检测到的错误。
• 数据帧(SOF∼CRC顺序)
• 遥控帧(SOF∼CRC顺序)
发送单元
接收单元

CRC错误
从接收到的数据计算出的 CRC 结果
与接收到的CRC顺序不同时所检测到
的错误。
• 数据帧(CRC顺序)
• 遥控帧(CRC顺序)
接收单元

格式错误
检测出与固定格式的位段相
反的格式时所检测到的错误。
• 数据帧(CRC界定符、ACK界定符、EOF)
• 遥控帧(CRC界定符、ACK界定符、EOF)
• 错误界定符
• 过载界定符
接收单元

应答错误
发送单元在ACK槽(ACK Slot)中
检测出隐性电平时所检测到的
错误(ACK没被传送过来时所检
测到的错误)。
• 数据帧(ACK 槽)
• 遥控帧(ACK 槽)
发送单元

当这些发送单元或者接收单元检测到对应的错误时,它们就会紧跟着主动出击发出错误帧,破坏当前总线上的数据。发出错误帧的行为,就叫做错误通知,意思是不管你们其他设备有没有检测到这个错误,反正我现在检测到了错误,我要破坏这帧数据,大家都别收了。错误帧一旦产生这帧数据就作废了,数据传输也会终止,等错误帧结束后,总线就回归空闲,大家谁想发数据,谁再开始一个新的帧。

有了错误检测和错误通知的设计,CAN总线上的每个设备都变成了“检察官”,一旦有错误出现,一个或多个设备,就会发出错误帧进行通知,即便有的设备没检测到错误,但它收到错误帧之后,也会得知有错误发生,这样就避兔了错误数据被别的设备使用。

但是,这个设计目前有一个风险:就是错误通知赋予了每个设备破坏传输的能力,如果有个设备抽风了,它无论收到啥都认为是错的,本来没问题它也认为是错的,那这样它就会不断破坏数据,总线上正常的数据传输,都无法进行了。所以,错误通知的设计,还要加一些限制措施,防止某一个设备,因为自己的问题而干扰了正常的数据传输。这个限制措施,就是错误状态

2.8.2 错误状态

  • 主动错误状态的设备正常参与通信并在检测到错误时发出主动错误帧,每个设备初始时都是主动错误状态;

  • 被动错误状态的设备正常参与通信但检测到错误时只能发出被动错误帧;

  • 总线关闭状态的设备不能参与通信;

  • 每个设备内部管理一个TEC和REC,根据TEC和REC的值确定自己的状态
    • TEC(Transmit Error Counter)是发送错误计数器,设备在发送时,每发现一个错误,TEC就会增加一次,同时,每进行一次正常的发送后,TEC也会减小一次;
    • REC(Receive Error Counter)是接收错误计数器,设备在接收时,每发现一个错误,REC就会增加一次,同时,进行正常接收后,REC也会减小一次;

错误计数器的细节

2.8.3 错误示例:ACK错误

  • 正常传输,没有错误的情况

在开始下一帧数据帧之前必须还要加入3位的帧间隔,这3位的帧间隔,主要是给过载帧准备的,意思是如果这个数据帧的接收方过载了,想要延迟发送方的数据传送,那么接收方就会在3位帧间隔的第一位拉开总线产生过载帧

  • 检测到ACK错误的情况:设备处于主动错误状态

上图中,到ACK槽这一位时,发送方释放总线,但是没有接收方拉开总线,所以ACK槽表现出隐性电平,意思就是没有设备应答,这时发送方就检测到了一个ACK应答错误,所以发送方会在ACK槽的下一位发出一个错误帧。因为目前发送方还是主动错误状态,所以它会发出主动错误帧,也就是6个显性0(主动错误标志)再跟8个隐性1(错误界定符)。

可以看到,错误帧是特殊的,正常的波形不会出现连续6个0,所以这里一旦出现连续6个0,就知道它是错误帧,看加了错误帧的波形,都是作废的,不会再使用了。这就是错误通知的功能。

另外我们还可以了解到,为什么正常传送的EOF要给7个隐性1,正常结束给1位隐性1不就够了吗?从这个ACK错误的现象,大概推测可能这7位EOF是为错误标志位的叠加,留下空间,要不然如果你EOF太短了,错误标志还没检测到,数据帧直接结束了,那接收方就不太好处理了,对吧。然后这里,错误界定符结束后。也要再跟3位帧间隔,才能开始下一帧。

同时我们也发现,错误界定符8位+帧间隔3位正好也是11位,所以设备检测到连续11个隐性电平,就到了总线空闲状态,下一个帧就可以开始了。

  • 检测到ACK错误的情况:设备处于被动错误状态

看一下这个被动错误状态的波形,ACK槽是没有应答的,接着该设备输出被动错误标志:6个隐性位1+8个错误界定符,再之后是3位帧间隔,帧闻隔结束后,该设备不能立刻开始下一帧,因为它是被动错误状态,不太可靠。所以,它的帧间隔后,还要加8位延迟传送,之后,才可以开始下一帧。

所以,如果一个主动错误状态和一个被动错误状态的设备都想发数据,它们同时检测到了11个隐性位,那么主动错误状态将直接取得总线控制权,不会和被动错误的设备进行仲裁,因为被动错误的设备,有8位延迟传送,所以它根本就赶不上主动错误设备的脚步。

2.9 STM32的CAN外设

2.9.1 CAN外设介绍

STM32内置bxCAN外设(CAN控制器),支持CAN2.0A和2.0B,可以自动发送CAN报文和按照过滤器自动接收指定CAN报文,程序只需处理报文数据而无需关注总线的电平细节。

  • CAN外设的部分参数
    • 波特率最高可达1兆位/秒:高速CAN的波特率范围是125k~1Mbps,所以STM32可支持高速CAN
    • 3个可配置优先级的发送邮箱:发送时有3个缓存区,可以存入3个待发报文
    • 2个3级深度的接收FIFO:接收时有3个缓存区,总共可以缓存2*3=6个报文
    • 14个过滤器组(互联型28个):用来过滤接收报文ID的,让CAN外设只接收指定的报文,无关报文过滤不看
    • 时间触发通信、自动离线恢复、自动唤醒、禁止自动重传、接收FIFO溢出处理方式可配置、发送优先级可配置、双CAN模式

2.9.2 CAN网拓扑结构

每个CAN节点都挂载在CAN总线上,对于其中一个CAN节点,都由CAN控制器和CAN收发器组成,CAN控制器一般集成在MCU也就是单片机芯片里面(我们本节学的CAN外设就是集成在STM32芯片内的CAN控制器),然后STM32引出CAN_RX和CAN_TX引脚与CAN收发器连接,之后CAN收发器引出CAN_High和CAN_Low与CAN总线相连。

2.9.3 CAN收发器电路

上面几小节简单看过TJA1050这个收发器芯片的内部框图了,现在我们看一下这个模块的电路。上图左上角是商家资料里提供的模块原理图,左下角是TJA1050芯片手册里给的应用参考电路,右边,就是我们本视频使用的收发器模块。

先看一下TJA1050芯片手册里给的参考电路:CANH和CANL直接连在CAN总线上,总线两端各加120Ω终端电阻,GND和S接地,VCC接5V供电,然后加一大一小两个电源滤波电容(注意这里VCC供电必须接5V,不能接3.3V),然后左边Vref引脚一般不用,可直接悬空,TXD和RXD两个引脚和左边的位于单片机芯片内的CAN控制器相连,注意这里和串口不一样,不用交叉连接

PS:注意一些低端的单片机内部没有集成CAN控制器,那么就可以通过外置上图中的SJA1000这个外置的独立CAN控制器,实现CAN通信。

2.9.4 CAN外设框图

(一)CAN外设内部设计

接下来,我们来看一下STM32内部的设计。

首先,我们看这个CAN外设的框图,这个框图就是手册里给出的CAN整体结构了,整个框图可以分为两个部分,蓝色虚线以上是主CAN,也就是CAN1,蓝色虚线以下是从CAN,也就是CAN2,但是只有互联型设备才有CAN2,STM32F103C8T6这款芯片只有CAN1。

最左边这一块是CAN2.0B主动核心,也就是核心电路都在这一块,核心里面有很多寄存器,我们可以读写这些寄存器来对CAN电路进行配置,或者获取电路的各种状态,简单说程序通过读写寄存器来操纵电路的运行。这些寄存器,在手册寄存器描述里都有详细解释。

中间这一块是主发送邮箱,有3个,每个邮箱可以存入一个CAN报文,如果我们想发出一个报文,那我们就把这个报文写入到其中一个空置邮箱,之后设置寄存器请求发送,就完事了。剩下的所有步骤,比如等待总线空闲,操作引脚输出波形,进行位同步和仲裁等等,这些步骤,全都由硬件电路自动执行,所以使用起来,其实非常简单。

最右边这一块是接收部分,包括接收过滤器和2个FIFO,当CAN总线上出现一个数据帧或者遥控帧时,CAN硬件电路都会把这个报文缓存下来,至于是不是要保留这个报文,那得看它能不能通过过滤器,过滤器内,我们可以设置过滤规则,告诉硬件,我们想要什么ID的报文,如果硬件收到了这些ID报文,就可以把它存入FIFO,如果收到的报文,无法通过任何一个过滤器,说明这个ID的报文我们并不想要。通过过滤器的报文会自动存入主接收FIFO-0或者主接收FIFO-1(FlFO的意思是先进先出寄存器,也可以把它称为队列)等待CPU读取。这里设计了两个队伍,每个队伍有3个邮箱,也就是最大存入3个报文,如果接收报文很快,CPU无法及时读走,那报文就可以在FIFO-0或者FIFO-1里面排队,这样在一定程度上,可以避免报文丢失。

(二)CAN外设的抽象结构图

上图是江科大总结的CAN外设抽象结构,编写程序初始化用到的各个外设模块就按照这个图来

首先,我们要配置GPIO外设,CAN_TX是输出,引脚控制权在CAN外设,需要配置为复用推挽输出模式,CAN_RX是输入,需要配置为上拉输入模式。

引脚进来之后,由发送和接收控制器全权管理,你想发什么报文,只需要把报文内容告诉它,之后它就自动给你发出去;同样,当它接收到一个报文后,它也会自动和你配置的过滤器进行比对,符合过滤器的报文,它自动帮你存入到FIFO的队列之中,CPU直接读取FIFO就行了。所以说,操作CAN总线绝大多数的工作,这个“管理员”已经帮我们完成了,程序只需要完成告诉它想发出什么报文和读取它帮我们存入到FIFO里的接收报文就行了

下面的发送部分,当我们想发出一个报文时,只需把报文的各个参数(比如ID、Data、IDE、RTR等)写入到其中一个发送邮箱,然后给一个请求发送的命令,之后这个管理员就会等待总线空闲,然后自动把这个报文广播到总线上。STM32设计了3个发送邮箱是为了防止总线繁忙造成发送拥堵,3个邮箱可以设置发送策略,比如先进入邮箱等待的报文先发送或者ID号小的报文先发送。

再看右边的接收部分,当CAN总线出现任何一个数据或遥控的报文波形时,这个控制器管理员就会把这个报文收下来,但是总线上的报文各种各样,这个报文并不一定是我们这个设备需要的,所以报文收下来之后,按照箭头往后传递到过滤器,接收过滤器可以根据ID号对报文进行过滤,如果这个ID号并不是我们想要的,那么报文就通过不了过滤器,通过不了过滤器,这个报文就会直接丢弃,不会再向后传递了。这里过滤器总共有14个,在程序中我们可以对任意一个过滤器进行配置,把我们想要接收的报文ID规则写入到过滤器中,这样的话总线上一旦出现我们想要的报文,那么它就会通过其中某一个过滤器,进入到后续的FIFO之中,进而被CPU读取。如果我们只需要几种类型的报文ID,那么使用一两个过滤器就能够满足要求了,其他没有使用到的过滤器,我们不去配置它,它就默认处于失能的状态,不会起作用。如果我们需要很多种类型的报文ID,那么就可以使用很多的过滤器,配置各种各样的过滤规则,来满是我们的需求。有了硬件设计这14个的过滤器,就节省了我们使用软件手动编程进行报文ID比对的工作,通过过滤器的报文就都是该设备所需要的报文。

通过过滤器的报文会存储在下面两个接收FIFO中(刚才说过FIFO就是先进先出寄存器)。这里STM32设计了两个接收FIFO,也就是有两个队伍,每个队伍都有3个邮箱(可以容纳3个报文排队)。上面配置过滤器的时候,有参数可以指定,通过这个过滤器的报文它该进哪一个FIFO队伍,也就是下面两个箭头是一个二选一的通道。比如说举个例子,可以指定过滤器0出来的报文进FIFO-0排队、过滤器1出来的报文进FIFO-0排队、过滤器2出来的报文进FIFO-1排队等等。当FIFO中3个邮箱都填满了但里面的数据还是没有被CPU读走,那么如果这时候再过来一个数据,这时STM32可以配置FIFO的锁定状态,来处理FIFO满之后新的报文该怎么存。如果配置了FIFO锁定,那么FIFO满之后,新的报文会直接丢弃,如果配置FIFO不锁定,FIFO满之后新的报文会把最后一个邮箱2的数据踢出去,自己占据邮箱2的位置。这就是队伍排满的处理方式,队伍排满后再来新的报文,必然会有报文丢失,要么是新来的丢失,要么是队伍末尾的丢失。当邮箱0的数据被CPU读走了,那么邮箱1的数据就会进入邮箱0、邮箱2的数据会进入邮箱1。

(三)CAN外设发送过程

上面这个流程图表示了发送邮箱在发送数据时的各种状态流转。基本流程:选择一个空置邮箱 → 写入报文 → 请求发送。

当请求发送之后,这个邮箱会经历什么状态呢,这个图表示的是邮箱0、1、2其中一个的状态。首先,邮箱默认的状态是左上角的空置状态,这里有3个状态寄存器的位,像这种大写字母一般都是寄存器里的各种位,具体意思得查手册里的寄存器描述,具体而言:

  • RQCP(Request completed)请求完成:图中RQCP=X,表示任意值
  • TXOK(Transmission Ok)发送成功:图中TXOK=X,表示任意值
  • TME(Transmit mailbox empty)是发送邮箱空:图中TME=1,表示当前邮箱是空置状态

当我们指定这个空置状态邮箱后,首先写入报文,然后请求发送,TXRQ(Transmit mailbox reguest)是发送请求控制位,TXRQ=1表示产生这个发送请求。所以该邮箱就进入到挂号状态,挂号状态,请求完成RQCP=0,发送成功TXOK=0,发送邮箱空TME=0,挂号状态表示此邮箱的数据已经准备好了,但是邮箱有3个,可能别的邮箱也有数据准备好了,所以该邮箱得排队,等到该邮箱的优先级已经是最高优先级了,说明下一次发送就轮到我了,那该邮箱就进入预定状态,这里的优先级有两种决定方式,一种是按照请求次序先来后到,一种是按照报文ID的大小。当然,在预定状态如果该邮箱优先级又被其他邮箱比下去了,那该邮箱得退回到挂号状态(只有按照报文ID大小决定的优先级,才有被比下去的可能)。

处于预定状态的报文,只要出现了CAN总线=IDLE空闲,那么管理员就会把该报文发送到CAN总线,该邮箱的状态,也正式进入发送状态,发送状态持续一段时间,如果报文正常发送成功,那么该邮箱就完成了全部使命,回到空置状态,并且置状态位,请求完成RQCP=1,发送成功TXOK=1,发送邮箱空TME=1。如果报文发送失败,那这里有两种走向,取决于NART(No automatic retransmission)是禁止自动重传,NART=0表示使用自动重传,那么发送失败的报文就回到预定状态,下一次总线空闲了会再次发送,直到发送成功为止,NART=1表示禁止自动重传,发送失败后直接回到空置状态,并且置状态位,请求完成RQCP=1,发送成功TXOK=0,发送邮箱空TME=1

最后,如果邮箱处于预定或挂号状态,当前的报文其实还没发出去,这时我们可以置ABRQ=1(Abort request, 中止发送标志位)中止该发送,中止发送后的各个标志位状态和发送失败是一样的。

(四)CAN外设接收过程

基本流程是,接收到一个报文→匹配过滤器后进入FIFO-0或FIFO-1→CPU读取。

上图是其中一个FIFO接收的流程图。

首先,最初状态FIFO是空状态,此时FMP(FlFO message pending, 报文数目)=0000,FOVR(FIFO overrun
FIFO溢出)=0,当收到一个有效报文时,FIFO队伍里就排队一个报文,对吧,这时FIFO处于挂号1状态,报文数目FMP=1,FFO溢出FOVR=0。如果又收到有效报文了呢,FIFO队伍里排队两个报文,这时进入挂号2状态,报文数目FMP=0010,FIFO溢出FOVR=0,然后挂号2状态,如果又收到有效报文就进入挂号3状态,报文数目FMP=0011,FIFO溢出FOVR=0,此时FIFO已经满了,标志位FULL=1(该标志位图中没有画出)。之后,如果FIFO存满了又收到有效报文,那么当前FIFO就进入溢出状态,报文数目仍然为FMP=0011,同时FIFO溢出标志位FOVR=1,FIFO溢出,肯定有报文会丢失,溢出后,再收到有效报文,那状态一直都是溢出。这是FIFO一直存的情况。

接着我们看FIFO的读取和释放。首先,如果FIFO是溢出状态,我们设置RFOM(Release FlFO output mailbox)为1,释放邮箱,FIFO就进入挂号3状态,挂号3状态,我们读取FIFO后,也释放邮箱,这时FIFO进入挂号2状态,队伍变短了,如果继续读取FIFO,释放邮箱,FIFO就进入挂号1状态,队伍更短,再读取FIFO,释放邮箱,队伍就会回到空状态。

(五)发送和接收的3个配置位

  • NART
    • 置1,关闭自动重传,CAN报文只被发送1次,不管发送的结果如何(成功、出错或仲裁丢失);
    • 置0,自动重传,CAN硬件在发送报文失败时会一直自动重传直到发送成功
  • TXFP
    • 置1,优先级由发送请求的顺序来决定,先请求的先发送;
    • 置0,优先级由报文标识符来决定,标识符值小的先发送(标识符值相等时,邮箱号小的报文先发送)
  • RFLM
    • 置1,接收FIFO锁定,FIFO溢出时,新收到的报文会被丢弃;
    • 置0,禁用FIFO锁定,FIFO溢出时,FIFO中最后收到的报文被新报文覆盖

2.9.5 标识符过滤器

前面讲过,当管理员收到一个报文后,会依次和每个过滤器进行比较,如果报文符合过滤器里配置的规则,则通过过滤器,存入后续的FIFO之中,如果报文无法匹配任何一个过滤器,则说明此报文并不是我们关心的,这时这个报文就直接丢弃,不会被存储。

那现在的问题是,怎么样配置这个过滤器的过滤规则来让它过滤出我想要的报文,这就得我们来详细地分析一下标识待过滤器的设计了。

上图是每一个过滤器的构成,一共14个过滤器,每个都这样互相不影响,x可以是0~13。

(一)四种过滤器模式配置

根据FSCFBM两位的配置,这个过滤器可以工作在4种状态下:

  1. FSC=1, FBM=0表示32位过滤器屏蔽模式
  2. FSC=1, FBM=1表示32位过滤器列表模式
  3. FSC=0, FBM=0表示16位过滤器屏蔽模式
  4. FSC=0, FBM=1表示16位过滤器列表模式

为什么要区分这么多的正作状态呢?这是因为实际的应用场景是多样的,在不同场景下我们要选择不同的状态,这样才能更好地满足我们的需求。

列表模式:直接指定要过滤的ID号,所以能过滤的报文ID非常少

屏蔽模式:使用模糊匹配的测量过滤ID号,比如有100个温度传感器测量的温度,我们给这些温度报文ID编号为0100、0101、0102、0103、… … 、0199,如果使用列表的方法,那么14个过滤器是远远不够的,这时候就可以使用屏蔽模式,将以01xx开头的数据过滤进来,这样的话这100个温度就都能接收了。

  • 32位过滤器列表模式

首先,我们先看第二种工作状态,这里写的是2个32位过滤器,标识待列表模式,这个是最直接、最简单的模式。在这个模式下,R1和R2两个寄存器,都写入的是目标ID,我们可以直接把想要的ID号写入到R1或R2寄存器中,总共可以写两个目标ID。当管理员收到一个报文时,它就和R1、R2寄存器的目标ID对比,如果有一样的就通过过滤器,如果都不一样就通不过过滤器。这种把我们想要过滤的目标ID直接写入到R1和R2寄存器的模式就是列表模式。

那我们看,怎么写入目标ID号呢?图中下面这里指明了存储映像(也就是每一位表示什么意思),其中这个32位寄存器的高11位需要存入的是标准格式的ID号,STID(Standard-lD),然后标准ID后面跟的这18位需要存入的是扩展格式的ID号,EXID(Extended-lD)。简单来说,如果你想写入一个标准ID号,那直接写入到前面11位的STID空间里就行,后面18位EXID就用不了了,全部写0就行。然后,过滤器咋知道你写入的是标准ID还是扩展ID呢?这就要继续看后面几位,IDE是扩展标志位,如果你想要过滤扩展ID,就在前面29位写入一个目标扩展ID,然后IDE位写1;如果你想过滤标准ID,就在最高的11位写入一个目标标准ID,然后IDE位写0。

注意你写入的ID类型,一定要和IDE位保持对应关系,如果你写入了扩展ID,IDE却写入的是0,那它就只会把你扩展ID的高11位当作标准ID来看。

然后后面,还有一个RTR位,RTR是遥控帧标志位,如果你想过滤数据帧,RTR位就写0,如果你想过滤遥控帧,RTR位就写1。

最后,这个32位寄存器还多出一位没有用,默认保持为0即可。

总上,这种模式下一个过滤器最多只能过滤两个ID号。

  • 16位过滤器列表模式

在16位列表模式下,R1和R2都会被拆开,拆成4个16位的寄存器,每个寄存器写入一个标准格式的目标ID,这样,一个过滤器就可以写入4个目标ID,就能最大化利用资源了。

我们看一下这种模式下的寄存器存储映像,在这种模式下,拆分开的4个16位寄存器是高11位,写入的都是标准ID。然后,标准ID后跟的是RTR,需要过滤遥控帧,RTR就写1,需要过滤数据帧,RTR就写0。再之后,是IDE,在这种模式下,IDE一般固定写0,也就是指定过滤标准格式ID。

最后,后面还多出来了3位,这里映像定义的是扩展ID的位17~位15,因为16位位宽一般不写扩展ID,所以这后面即使多出来了3位扩展ID,一般也没啥用,写0即可。

  • 32位过滤器屏蔽模式

在这种模式下,R1寄存器要写入一个ID号,R2寄存器写入的就是屏蔽位了,这个屏蔽位,英文是Mask,更常见的翻译是叫“掩码”。

假设我们的需求是过滤出0x1开头的所有标准ID号,那我们首先需要把这个ID号写入到R1寄存器里,然后IDE一定要给0,我们需要标准ID,RTR位这个根据需求来,给0表示需要数据帧,这样R1寄存器就写好了,如果现在是列表模式那当前仅能过滤一个ID,但是现在是屏蔽模式,我们可以在下面R2寄存器里,表明在过滤时,哪些位是必须匹配的,而哪些位是无关的。

在R2写入掩码,写1表示R1里的ID对应位必须匹配一致,写0表示R1里的ID对应位1和0均可。现在我们需要标准ID的高3位必须匹配,而剩下的位1或0均可,所以我们可以这样填,R2寄存器的高3位都给1,告诉过滤器ID的高3位必须为001才给过,然后后面都给0,告诉过滤器后面这些位,不需要和目标ID一样,1或0都给过,这样,通过过滤器的ID就是001 xxxx xxxx。满足我们过滤一组ID的需求,继续后面的屏蔽位。EXID的屏蔽位,没用,可以都给0,最后,IDE的屏蔽位必须给1,表示上面R1寄存器的IDE必须匹配,如果IDE的屏蔽位给了0,那就表示标准格式和扩展格式是无所谓的,这样可能会导致某些扩展ID误入进来,就不符合需求了。然后RTR的屏蔽位,看情况,如果你只需要数据帧,那么R1的RTR给0,R2的RTR给1,表示必须匹配数据帧,如果只需要遥控帧,那么上面给1,下面给1,表示必须匹配遥控帧,如果你数据帧和遥控帧都想要,那上面无所谓,下面给0,就表示RTR位,1和0都给过,这样就可以了。

  • 16位过滤器屏蔽模式

就是屏蔽模式,只需要过滤标准I的话,一个寄存器拆开两个来用,这样就是R1的低位写一个目标ID,R1的高位是对应的屏蔽位;R2的低位再写一个目标ID,R2的高位是对应的屏蔽位,每一位的映像在下面有写,拆分之后,一个过滤器就可以设置两组屏蔽位模式的ID。

那到这里,我们就学完了这个过滤器的所有模式。

(二)过滤器配置示例

2.9.6 测试模式

测试模式,顾名思义,就是方便测试的,在这个CAN外设中,设计的有3种测试模式,这些测试模式,在调试程序的时候非常有用,尤其是发送端和接收端程序都需要调试的时候

  1. 静默模式:用于分析CAN总线的活动,不会对总线造成影响
  2. 环回模式:用于自测试,同时发送的报文可以在CAN_TX引脚上检测到
  3. 环回静默模式:用于热自测试,自测的同时不会影响CAN总线

在静默模式下,发送和接收的线路会在内部进行变更,TX引脚始终发送1,发送1其实就相当于啥也没发,对吧(因为总线默认就是逻辑1高电平),然后自己的发送端,直接接到自己的接收端(也就是自己发自己收),同时,RX引脚收到的报文,也可以进入接收端。

可以想到,这个模式,除了可以自发自收测试,还可以默默地监测CAN总线的报文数据,比如你想做一个CAN报文分析仪,只想看一下总线有哪些报文,而不想输出任何电平,就可以配置为这个模式,静静地看着总线。

在环回模式下,RX引脚直接断开,不接收任何报文,自己的发送端接到TX,可以发出报文,同时,自己发的数据自己也可以收回来,注意这个并不是之前我们说的回读机制,这个自发自收侧重点是在报文层面上的自发自收,也就是自己发的报文,自己也会接收回来,不是底层的回读电平。

那在环回模式下,就可以进行自测了,同时发送的报文可以在CAN_TX引脚上检测到,就是在程序上可以读取自己发出的报文,在引脚上也可以检测到自己的输出波形,我们之后写代码的时候,就会用到环回模式,到时候再给大家演示用法。

在环回静默模式下,内部把发送端和接收端连接起来,外面的TX引脚和RX引脚全都是断的,这就是自发自收,同时隔绝外部的联系,比如说你有一个CAN设备,刚上电,为了确保自己的硬件是没问题的,设备可以进行自测,就是自己随便发一个报文,看看自己是不是能常地接收,同时外面的CAN总线,其他设备正在进行正常的通信,我不想我的自测干扰了它们,也不想它们的通信干扰我自测,这时,我就可以配置成环回静默模式,大门紧闭,自己测自己。测试没问题了,再配置成常模式,大门再敞开。因此,这个自测试就是热自测试,也就是外面总线正常工作,我把自己隔绝开,进行自测试,不会影响别的设备,也不会被别的设备影响。

在进行设备自测时,可以在代码上进行灵活地切换,并且这个切换,只需要修改代码,而不需要实际地去接线,使用起来非常方便。

2.9.7 工作模式

接着我们继续看下一个知识点,工作模式,这个工作模式总共有3个:

  1. 初始化模式:用于配置CAN外设,禁止报文的接收和发送
    • 一般我们在配置CAN外设的时候就会让它处于初始化模式,等配置完成后,再切换到正常模式,否则如果在正常模式初始化,那可能参数刚配置一半,还没配置完整,CAN外设就在错误的参数下运行起来了
  2. 正常模式:配置CAN外设后进入正常模式,以便正常接收和发送报文
  3. 睡眠模式:低功耗,CAN外设时钟停止,可使用软件唤醒或者硬件自动唤醒
    • AWUM
      • 置1,自动唤醒,一旦检测到CAN总线活动,硬件就自动清零SLEEP,唤醒CAN外设;
      • 置0,手动唤醒,软件清零SLEEP,唤醒CAN外设

上图是CAN外设的工作模式切换图,图中大写字母,是寄存器的各种状态位或配置位。

首先,复位后,CAN外设默认是睡眠模式,SLAK(Sleep ack)是睡眠确认状态位,SLAK=1表示,硬件已经确认进入睡眠模式了,INAK(lnit ack)是初始化确认位,INAK=0表示,硬件目前没有确认进入初始化模式(因为现在是睡眠模式)。

然后看下面两个模式的状态位,初始化模式时,SLAK=0INAK=1表示硬件确认进入初始化模式了,正常模式,SLAK=0INAK=0,表示硬件既没有进睡眠模式,也没进初始化模式,那就是正常模式。

之后各个模式的转移,可以通过配置SLEEP位和INRQ位实现,SLEEP置1就是请求进入睡眠,SLEEP置0就是请求退出睡眠(图中SLEEP头上一横线就表示置0)。然后INRQ(Init request)置1,表示请求进入初始化,INRQ置0,表示请求退出初始化。当然这些配置位都只是请求,可能不会立刻生效,因为还有可能需要等待ACK应答位或者SYNC信号(总线空闲),只有在总线空闲时,才能进入正常模式,总线不空闲进入正常模式,可能会导致误操作。这就是这3个工作模式以及各种工作模式之间的转换。

2.9.8 位时间特性

这部分介绍位时间特性,也就是位时序,位时序决定了采样点的位置和一位的时长(也就是波特率)。

位时序我们之前CAN协议的地方也讲过,但是在STM32中这个位时序稍有不同。首先,STM32 CAN外设规定的位时序只有3个段:

  1. 第一个是SS,同步段,固定1Tq
  2. 第二个是BS1段,可以配置1~16Tq
  3. 第三个是BS2段,可以配置1~8Tq
  4. SJW,可以配置1~4Tq

采样点在BS1和BS2段之间,这和我们之前CAN协议里讲的有点不同,CAN协议规定的位时序是一位有4个段,分别是SS,PTS,PBS1和PBS2,其中SS固定1Tq,PTS、PBS1和PBS2都可以配置为者干Tq,采样点在PBS1和PBS2之间。但是STM32的设计者可能想你这个PTS和PBS1都是可配置的,但是中间也没啥事要干,那我干脆把这两个段合在一起算了,这样还能少设计个参数。所以,在STM32这里PTS和PBS1两个段是合在起的,统一叫作BS1段,可以配置的范围也比较大是1~16Tq,就这个地方和CAN协议有些不同,其他地方和CAN协议里的规定都一样。

然后看下面波特率的计算。对于C8T6这款芯片,CAN外设挂载在APB1总线(36MHz)上,那么tPCLK就等于1/36MHz,BRP就是时钟分频,这个36M的时钟经过BRP+1分频,得到的就是tq的时钟,然后再代入图中的公式计算就能得到波特率了。

2.9.9 CAN中断

中断就是当CAN外设内部发生一些重要的事件时,程序可以自动跳转到中断函数执行。STM32的CAN外设占用4个专用的中断向量,也就是有4个中断函数可以使用:

  1. 发送中断:发送邮箱空时产生
  2. FIFO-0中断:收到一个报文/FIFO 0满/FIFO 0溢出时产生
  3. FIFO-1中断:收到一个报文/FIFO 1满/FIFO 1溢出时产生
  4. 状态改变错误中断:出错/唤醒/进入睡眠时产生

上图是中断信号流向图,图中大写字母是各种寄存器、中断标志位,比如发送邮箱0空了RQCP0(Request completed 0)就自动置1,FIFO-0收到一个报文,FMP0不再是00的时候就可以触发中断,FIFO-0满了,FULL置1,FIFO-0溢出了,FOVR(FIFO Overrun)置1等等,剩下的都是类似的。此外CAN_IER对应的是中断允许位,里面都是各种(lnterrupt enable)寄存器,当对应的中断允许位为1时,允许中断信号通过与门,当中断允许位为0,与门输出固定为0,中断信号无效。最后,各个中断信号通过或门(图中右侧的圆形加号)输出到NVIC,去请求中断,在NVIC中,与CAN相关的有4个中断向量,对应的就是4个中断函数。

2.9.10 时间触发通信

该功能初学了解即可,它是ISO11898-4协议里规定的更高级的功能,简单来说就是这个功能可以对所有节点进行同步调度,也就是每个节点只在一个固定的时间段内发送报文,这样可以避免优先级仲裁。

这里简单介绍一下它的硬件设计,首先,TTCM(Time triggered communication mode)位控制这个功能的开关

2.9.11 错误处理和离线恢复

这个功能在CAN总线协议那里也介绍过,基本是一样的,但是STM32对此有个性化的设计。

首先,TEC发送错误计数器和REC接收错误计数器,根据错误的情况增加或减少。然后下面主动错误、被动错误和离线状态之间的转换和CAN协议规定的一样,初始是这个错误主动,当TEC或REC>127时,转入错误被动,当TEC>255时,转入离线,也就是总线关闭态,当出现128次11个隐性位时,恢复到错误主动。这个流程和2.8.2 错误状态的转移是一样的。

但是,STM32这里,额外设计了一个小功能,就是ABOM(Automatic bus-off management)自动离线管理

  • 置1,开启离线自动恢复,进入离线状态后,就自动开启恢复过程;
  • 置0,关闭离线自动恢复,软件必须先请求进入然后再退出初始化模式,随后恢复过程才被开启 o

CAN协议规定的是,当出现128次11个隐性位自动就恢复到错误主动了。但是,STM32在这个恢复的箭头加了个开关——ABOM,如果ABOM=1,那进入离线状态后,就自动开启恢复过程,跟CAN协议要求是一样的。但是,如果ABOM=0,进入离线状态后,设备无法直接恢复为错误主动,而是只有软件先请求进入然后再退出初始化模式,就是INRQ位先置1再清0。之后,检测到128次11个隐性位才能恢复到错误主动。

2.9.12 STM32的CAN编程

根据2.9.4 CAN外设框图的(二)CAN外设的抽象结构图部分的图像,可根据此图进行编写程序:

  • 代码第一部分:CAN外设初始化
    1. 第一步,是RCC时钟初始化,这里要开启CAN引脚的GPIO时钟和CAN1的时钟
    2. 第二步,是GPIO初始化,把CAN_TX引脚初始化为复用推挽输出,CAN_RX引脚初始化为上拉输入;
    3. 第三步,是整个CAN外设的初始化,编程中用一个结构体,可以配置很多参数,比如CAN外设的模式、波特率、各种小功能等等,这些参数用一个结构体和CANInit函数就可以配置了;
    4. 第四步,是单独对14个过滤器进行初始化,因为过滤器比较复杂,里面参数也很多,所以要单独初始化,也是用结构体配置、有过滤器位宽、模式、R1 R2的值、FIFO关联、使能的参数;
    5. 第五步:如果需要开启中断的话,就再加上ITConfig函数,使能中断输出,然后再加上NVIC和中断函数。
  • 代码第二部分:报文的发送
    1. 库函数封装了一个发送结构体和发送函数,把报文数据写入发送结构体,再调用发送函数,就完成了。
  • 代码第三部分:报文的接收
    1. 库函数也有检查接收FIFO状态的函数,如果FIFO里有报文就调用接收函数,这样报文数据就会存入到接收的结构体中,读取接收结构体的内容,就可以获取接收到的数据了,这就是代码整体的流程

库函数概览 (stm32f10x_can.c): 现在,我们回到工程,查看Library目录下的stm32f10x_can.c文件(滑至文件末尾),了解关键库函数:

  1. 初始化和配置函数
    • 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的起始过滤器组。(通常不需要)
  2. 报文发送函数:
    • CAN_Transmit(): (核心) 请求发送一个报文,使用CanTxMsg结构体。返回值为报文存放的发送邮箱编号。
    • CAN_TransmitStatus(): (核心) 获取指定发送邮箱的状态(成功、挂起、失败)。
    • CAN_CancelTransmit(): 取消指定邮箱中挂起或待发送的报文(就是设置ABRQ位,取消报文发送)。
  3. 报文接收函数
    • CAN_Receive(): (核心) 从指定的接收FIFO(0或1)中读取一个报文,数据存入CanRxMsg结构体。读取后通常会自动释放邮箱,无需手动调用下面的CAN_FIFORelease()进行释放。
    • CAN_FIFORelease(): 手动释放指定FIFO的邮箱(置RFOM=1,将FMP计数器减1)。通常在仅调用CAN_Receive()读取数据时不需要此函数。
    • CAN_MessagePending(): (核心) 获取指定接收FIFO(0或1)中排队的报文数量(即FMP寄存器的值)。
  4. 工作模式管理函数
    • CAN_OperatingModeRequest(): 请求切换工作模式(参数可指定:初始化模式、正常模式、睡眠模式)。
    • CAN_Sleep(): 使CAN进入睡眠模式(设置SLEEP位)。
    • CAN_WakeUp(): 使CAN退出睡眠模式,唤醒CAN(清除SLEEP位),唤醒后默认进入正常模式。
  5. 错误管理函数:
    • CAN_GetLastErrorCode(): 获取最近一次的错误代码。
    • CAN_GetReceiveErrorCounter(): 获取接收错误计数器(REC)的值。
    • CAN_GetLSBTransmitErrorCounter(): 获取发送错误计数器(TEC)的低8位值。(如需完整TEC,需结合其他方法)
  6. 中断和标志管理函数: (老朋友)
    • CAN_ITConfig(): 使能/失能指定的CAN中断源。
    • CAN_GetFlagStatus(): 获取指定标志位的状态。
    • CAN_ClearFlag(): 清除指定的挂起标志位。
    • CAN_GetITStatus(): 检查指定的中断是否发生。
    • CAN_ClearITPendingBit(): 清除指定的中断挂起位。

总结流程与关键函数:

  • 初始化核心: CAN_Init(), CAN_FilterInit()
  • 发送核心: CAN_Transmit(), CAN_TransmitStatus()
  • 接收核心: CAN_MessagePending(), CAN_Receive()

2.10 示例程序

2.10.1 CAN总线单个设备环回测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include "MyCAN.h"

/**
*@brief 初始化CAN外设
*@note 工作模式为:环回测试模式
*/
void MyCAN_Init(void)
{
// 1. 开启CAN外设和GPIO时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

// 2. 初始化GPIO
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入,因为CAN默认情况就是高电平
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);

// 3. CAN外设初始化
CAN_InitTypeDef CAN_InitStruct;
CAN_InitStruct.CAN_Mode = CAN_Mode_LoopBack; // 环回测试模式
CAN_InitStruct.CAN_Prescaler = 48; // 分频系数,BRP寄存器的值
CAN_InitStruct.CAN_BS1 = CAN_BS1_2tq; // BS1段的Tq数
CAN_InitStruct.CAN_BS2 = CAN_BS2_3tq;; // BS2段的Tq数
CAN_InitStruct.CAN_SJW = CAN_SJW_2tq; // 再同步最大补偿宽度
CAN_InitStruct.CAN_NART = DISABLE; // 不开启不自动重传功能,也即自动重传
CAN_InitStruct.CAN_TXFP = DISABLE; // ID号小的先发送
CAN_InitStruct.CAN_RFLM = DISABLE; // FIFO溢出时,最后收到的报文被新报文覆盖
CAN_InitStruct.CAN_AWUM = DISABLE; // 手动唤醒
CAN_InitStruct.CAN_TTCM = DISABLE; // 关闭时间触发通信
CAN_InitStruct.CAN_ABOM = DISABLE; // 不使用离线自动恢复,也即手动恢复

CAN_Init(CAN1, &CAN_InitStruct);

// 4. 过滤器初始化
CAN_FilterInitTypeDef CAN_FilterInitStruct;
CAN_FilterInitStruct.CAN_FilterNumber = 0; // 初始化14个过滤器中的第0个过滤器
CAN_FilterInitStruct.CAN_FilterIdHigh = 0;
CAN_FilterInitStruct.CAN_FilterIdLow = 0;
CAN_FilterInitStruct.CAN_FilterMaskIdHigh = 0;
CAN_FilterInitStruct.CAN_FilterMaskIdLow = 0; // 所有报文全通
CAN_FilterInitStruct.CAN_FilterScale = CAN_FilterScale_32bit; // 选取32位
CAN_FilterInitStruct.CAN_FilterMode = CAN_FilterMode_IdMask; // 选择屏蔽模式
CAN_FilterInitStruct.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; // 通过此过滤器的报文进入FIFI-0
CAN_FilterInitStruct.CAN_FilterActivation = ENABLE; // 使能过滤器

CAN_FilterInit(&CAN_FilterInitStruct);

}

/**
*@brief 使用CAN外设发送报文数据
*@param Id 报文Id号
*@param Length 数据段长度
*@param Data 实际数据
*/
void MyCAN_Transmit(uint32_t Id, uint8_t Length, uint8_t* Data)
{
uint8_t i = 0;
CanTxMsg TxMessage;
TxMessage.StdId = Id; // 标准ID
TxMessage.ExtId = Id; // 扩展ID
TxMessage.IDE = CAN_Id_Standard; // 扩展标志位
TxMessage.RTR = CAN_RTR_Data; // 遥控标志位
TxMessage.DLC = Length; // 指定数据段长度

for(i=0; i<Length; i++)
TxMessage.Data[i] = Data[i]; // 发送的实际数据

// 使用CAN外设发送数据
uint8_t Mail_Num = CAN_Transmit(CAN1, &TxMessage);
// 等待数据发送完成
while(CAN_TransmitStatus(CAN1, Mail_Num) != CAN_TxStatus_Ok);
}

/**
*@brief 使用CAN外设接收报文数据
*@note 暂时使用循环轮询的方法查询是否收到报文数据,进阶方法使用中断
*@param Id 报文Id号
*@param Length 数据段长度
*@param Data 实际数据
*/
void MyCAN_Receive(uint32_t* Id, uint8_t* Length, uint8_t* Data)
{
uint8_t i = 0;
CanRxMsg RxMessage;
// 1. 判断接收FIFO里是否有报文
uint8_t mark = CAN_MessagePending(CAN1, CAN_FIFO0);
// 2. 读取接收FIFO,把报文内容取出来
while(mark > 0)
{
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);

// 判断报文Id是标准格式还是扩展格式
if(RxMessage.IDE == CAN_Id_Standard)
*Id = RxMessage.StdId;
else
*Id = RxMessage.ExtId;

// 判断是遥控帧还是数据帧
if(RxMessage.RTR == CAN_RTR_DATA)
{
*Length = RxMessage.DLC;
for(i=0; i<RxMessage.DLC; i++)
Data[i] = RxMessage.Data[i];
}
else
{
// ...
}
mark--;
}
}

2.10.2 中断方式接收

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
*@brief 接收FIFI-0的中断函数,从startup_stm32f10x_md.s起始文件中查看中断函数名
*@note 使用标志位的方法
*/
void USB_LP_CAN1_RX0_IRQHandler(void)
{
if(CAN_GetITStatus(CAN1, CAN_IT_FMP0) == SET)
{
MyCAN_Receive(&RxMesg);
MyCAN_RxFlag = 1;
}

}

参考链接:【江科大CAN】2. STM32 CAN外设 - CSDN【江科大CAN】3. 代码实战 - CSND