首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

CAN通信

CAN是在汽车和工业场景下大量使用的串行总线网络,全称为Controller Area Network。在1982年的SAE大会上,Bosch公司发布了CAN,在今天地上跑的汽车、拖拉机、火车, 水里的轮船,工厂里的控制系统等等很多场景都在使用CAN进行组网和通信。

CAN协议本身只定义了物理层和数据链路层,通信频率最高可达1Mbps。有CAN2.0A和2.0B两个标准,它们的主要差别在于ID的形式不一样。2.0A中ID为11位,称为标准帧; 2.0B的ID可达29位,称为扩展帧。置于我们经常见到CANOpen,J1939等协议都是应用层协议,它们定义了CAN总线上数据在应用程序中的具体意义。

STM32F407提供了CAN的控制器,解决了数据链路层,支持CAN 2.0A和2.0B的标准。为了实现一个CAN总线设备,我们还需要额外提供了物理芯片,来实现总线的物理层协议。 本文中先详细介绍CAN总线的物理和数据链路层的相关定义,再介绍如何通过F407实现一个CAN总线设备。不涉及CANOpen等应用层的协议。

1. CAN物理层

CAN总线的物理连接只需要两个差分信号线CAN_H和CAN_L即可。ISO标准定义了ISO11898和ISO11519-2两种物理层,它们再总线电平和通信速率上有差别。 其中ISO11898的通信速率为125Kbps~1Mbps,称为CAN高速通信,隐性电位差为0V,显性电位差为2V。ISO11519-2的通信速率低于125Kbps,称为CAN低速通信,隐形电位差为-1.5V, 显性电位差为3V。总线阻抗都是120Ω。下表总结了两种物理层的不同点:

表1 ISO11898 V.S. ISO11519-2

物理层 ISO 11898(高速) ISO 11519-2(低速)
通信速率 最高1Mbps 最高125Kbps
最大线长 40m/1Mbps 1km/40Kbps
总线电平(V) 隐性显性 隐性显性
MinNomMax MinNomMax MinNomMax MinNomMax
CAN_H 2.002.503.00 2.753.504.50 1.601.751.90 3.854.005.00
CAN_L 2.002.503.00 0.501.502.25 3.103.253.40 0.001.001.15
电位差(H-L) -0.500.05 1.52.003.0 -0.3-1.5—— 0.33.00——
物理特性示意图

在总线上,通过CAN_H和CAN_L之间的电位差(H-L)来判断总线电平逻辑。在任何时刻,总线上有隐性和显性两种电平,电位差值参考上表。在任何时刻, 总线上的电平一定处于两种电平之一。总线上可以执行线与逻辑操作,显性电平认为是逻辑'0',隐性电平则是逻辑'1'。

一般,我们会选择CAN收发器来实现物理的总线接口,也就是人们常说的物理芯片。TJA1050就是一种常用的ISO11898标准的收发器,探索者开发板上用的就是这款收发器。 其引脚定义和电平逻辑如下表所示:

表2 ISO11898 V.S. ISO11519-2

Symbol Pin Description
TXD1发送数据输入,链接CAN控制器的Tx输出引脚
GND2
VCC3电源
RXD4接收数据输出,链接CAN控制器的Rx输入引脚
Vref5参考电压输出
CANL6低压CAN总线
CANH7高压CAN总线
S8高速模式或者沉默模式选择信号

2. CAN数据链路层

2.1 帧

在CAN的通信过程中,有5种类型的帧:数据帧、远程帧、错误帧、过载帧、和帧间隔,有些资料里认为是4种,没有将帧间隔计算在内。 其中错误帧、过载帧就是一段逻辑'0'(被动错误帧则是逻辑'1')和8位的界定符。数据帧和远程帧则是我们的业务逻辑中使用的帧,它具有标准和扩展两种形式。 标准帧形式是定义在CAN2.0A标准中的帧格式,其ID有11位;而扩展形式则是CAN2.0B中定义的具有29位ID的帧格式。它们的功能总结如下:

下图1描述了CAN通信的帧格式,总体上有7段。图中的D表示显性电平,即逻辑'0',R表示隐性电平为逻辑'1'。标准帧和扩展帧除了在仲裁段和控制段有所不同外, 其余段的形式和内容都是一样的。

图 1. CAN帧格式

总线空闲的时候,总是处于逻辑'1'的状态。当有设备需要发起通信,需要先在总线上发送一位逻辑'0'表示一帧的起始,这个逻辑'0'就被称为帧起始SOF。 接着设备需要向总线上发送ID,同一时间可能有多个设备请求发送数据,但总线只有一个,就需要通过ID来进行仲裁,只有仲裁通过的设备才可以获得总线的使用权。

仲裁段总体上可以看作是ID+RTR构成的。RTR是远程帧的标志,对于数据帧该位为逻辑'0',远程帧为逻辑'1'。在标准的CAN帧中,ID只有11位,紧跟着就是RTR。 扩展帧有29位的ID,它被SRR和IDE分为了11位的基本ID和18位的扩展ID两个部分。同时还规定ID的高7位不能全为1, 也就是说标准ID和基本ID中禁止出现形如"11'b111_1111_XXXX"的数据,其中'X'表示'0'或'1'。在CAN总线中所有的字段都是先发送高位的,即MSB形式,这里的ID也遵从这一形式。

此外需要强调一点,在CAN中所有的消息都是广播的形式发送的,也就是说总线上的任何一个设备都可以接收到总线上所有的数据。各个设备根据自己的需要对总线上的数据进行过滤, 过滤的依据就是这里的ID。

SRR表示是扩展帧中的RTR占位,IDE则是扩展帧标识符,对于扩展帧而言这两位始终为'1'。而在标准帧中,SRR对应着RTR根据帧类型不同可能为'1'也可能为'0',IDE则为'0'。 有仲裁就会有优先级的概念,后面介绍仲裁机制的时候我们会看到,因为SRR和IDE的存在,使得扩展帧的优先级总是低于标准帧,远程帧低于数据帧。

仲裁段之后就是控制段,它由r1,r0和DLC三部分构成。r1和r0是两个保留位全为'0',其中r1对于标准帧而言就是上述的IDE。DLC是四位数据长度代码, 它表示其后数据段的字节数量,值域为[0,8]。因此,数据段中可以包含0~8个字节。而远程帧因为是请求数据,所以它是不需要数据段的, 其DLC则表示请求的数据长度。

接下来就是CRC段应答段。CRC段由15位的CRC校验和以及一个界定位构成,对从帧起始到数据段中所有位计算CRC多项式, 用于判定数据是否成功接收。应答段用于确认是否正常接收,由ACK槽和ACK界定两位构成。发送设备需要在这两位上发送逻辑'1',成功接收到数据的设备则在ACK槽上发送逻辑'0', 以通知发送设备。由于总线的线与操作,成功接收到数据后ACK槽上就能采到一个逻辑'0'。

最后,以7位的逻辑'1'表示一帧的结束,即EOF。

图 2. CAN错误帧

上图为CAN的错误帧格式,它由两部分构成:错误标志和错误界定符。一共有主动错误和被动错误两种错误帧。被动错误基本没有什么作用,它就好像是一串逻辑'1', 和总线空闲没有什么差别。当有设备检测到总线出错,它会发送一个主动错误到总线上,即连续6个逻辑'0'。这六个逻辑'0'将打断当前的总线通信过程, 以至于线上其它设备也会检测到总线错误,并发送主动错误信号到线上,这就构成了图中所示的错误标志重叠部,这个长度最长为12位,之后就是8个逻辑'1'表示错误帧的界定。

虽然说有所谓的过载帧,它起始就是一个主动错误帧,其线上逻辑与上图的主动错误没有区别。而帧间隔就是数据帧和远程帧之间的一段间隔,为3个逻辑'1'。 也就是说,任何两帧之间应该至少有3位的间隔。

2.2 仲裁和优先级

总线空闲的情况下,先发起通信的设备将获得总线的使用权。如果同一时间有多个设备同时发起通信,那么就从第一位开始进行仲裁。

如下图3所示,各个设备各自独立的向总线上发送数据,同时检测总线电平。在总线上进行线与操作,当设备检测到其发送的逻辑'1'因为线与而没有体现在总线上, 就说明其仲裁失败,将放弃发送进入接收状态。由于CAN通信的各段都是MSB形式的,这意味着具有较大ID的设备在仲裁中更容易失利,因而具有更低的优先级。

图 3. CAN错误帧

对于数据帧和远程帧而言,如果它们的ID都是一样的,那么其仲裁结果就由RTR位决定。在远程帧中该位为'1',与数据帧的'0'线与之后就会失利,所以说远程帧的优先级低于数据帧。 而标准帧和扩展帧,在SRR位和IDE位上存在差异,由于扩展帧中这两位都是'1',所以扩展帧的优先级低于标准帧。

2.3 位时序和位填充

CAN本身是一种异步通信方式,挂在总线上的所有设备都应当工作在一个事先约定的通信频率下,也就是所谓的波特率。由于涉及到多个设备之间的通信, 它们各自工作在自己的时钟下,并且开始通信的时间具有很大的随机性。所以为了保证信号有效的收发,CAN还对其每一位做了划分,定义了信号的采集时间点, 我们称这一定义为位时序。

图 4. CAN位时序

上图中的标称位时间是指发送一位所需的时间,它与波特率之间存在关系[波特率 = 1 / 标称位时间]。它将一位的时间划分为4个不相交的区间: 同步段(Sync_Seg),传播段(Prop_Seg),相位缓冲段1(Phase_Seg1),和相位缓冲段2(Phase_Seg2)。这些段由一个更小的时间单位Time Quantum(Tq)构成。

位时序的定义主要是为了说明采样点,以及同步机制。所以,在STM32F407中的CAN控制器将Prop_Seg和Phase_Seg1合并成为一个段成为BS1,Sync_Seg提供同步机制, BS1和BS2之间的时刻就是采样点。

为了防止总线上的异常,CAN协议采用位填充的形式编码,当连续出现了5个相同逻辑位的时候,就会在位流中插入一个相反的补充位,就是所谓的位填充。 而错误帧、过载帧不存在位填充操作。

3. STM32F407的CAN控制器

在STM32F407中提供了一种称为bxCAN的控制器,它支持CAN 2.0A和2.0B,也就是所谓的标准帧和扩展帧格式。其最大波特率能达到1Mbps。 它提供了三个发送邮箱来发送消息,我们可以配置它们以不同的优先级竞争发送,也可以组合起来像一个FIFO一样发送数据。它接收的消息则是通过由三个邮箱构成的FIFO来存储的, 我们可以通过FIFO输出邮箱来读取消息。此外还提供了28个ID过滤器来对总线上的消息进行过滤,只保存关心的消息。

图 5. bxCAN控制器工作状态

3.1 bxCAN的工作模式

bxCAN有三种主要的工作模式:初始化、正常和休眠,如右图5所示。在初始化状态我们可以对CAN控制器的位时序、控制选项等做出配置。 正常模式则是我们能够收发CAN总线数据的状态。休眠状态下可以降低系统功耗,此外还会在CANTX上加入一个上拉电阻,以防止其干扰总线。

硬件复位之后,bxCAN就会进入休眠模式。我们需要通过软件配置CAN_MCR寄存器来控制其进入或者退出初始化和休眠模式, 可以通过查询CAN_MCR的INAK和SLAK位来获得控制器的工作状态,它们分别表示控制器处于初始化状态,和休眠状态。如果这两位都没有置位,那么它就是工作在正常状态下。

此外如下图6所示,bxCAN还有Silent和LoopBack两种测试模式,也可以将两种模式组合起来使用。如图(a)所示,在Silent模式下,CANTX将一直保持逻辑'1'的状态, 而不干扰总线数据,同时CANRX将一直监听总线数据。而在LoopBack模式下,CANRX将与总线断开,而Tx信号将被直接接到Rx上形成回环,同时Tx的信号也将反映到总线上,如图(b)所示。 图(c)则说明了这两种模式的组合方式。CANTX将一直保持逻辑'1',同时CANRX将从总线上断开。

(a). Silent模式 (b). LoopBack模式 (b). Silent & LoopBack模式
图 6. CAN测试模式

最后,还有一个Debug模式,这主要是当我们需要在线调试时使用的。它定义和描述了单片机程序停止情况下bxCAN是否也要跟着停止的状态。这里不再细说了。

3.2 bxCAN发送消息

bxCAN有3个发送邮箱,当我们需要发送消息的时候,需要先找一个空的邮箱,这点可以通过查询CAN_TSR中的TMEx位来知晓。然后把消息的ID、DLC、 以及数据分别写到该邮箱的寄存器中。然后置位TXRQ发起发送请求。

此刻邮箱将进入等待(Pending)状态,我们将不能再对邮箱的寄存器进行修改,直到它再次为空。在等待状态下,邮箱将等待着它要发送的消息优先级变成所有邮箱中最高的。 然后该邮箱就会进入调度(Scheduled)的状态,只要bxCAN检测到总线空闲了,就会尝试将邮箱中的ID和数据写到总线上。一旦开始发送数据,它就进入了发送(Transmit)的状态。 发送完毕之后就会再次恢复到Empty的状态。

我们已经提到发送邮箱也是优先级的。当有不止一个邮箱需要发送数据的时候,bxCAN将先根据邮箱中消息的ID来判定优先发送的数据。其与CAN总线的优先级一样, 更小的ID具有更高的优先级。如果ID一样,那么就邮箱编号更小的消息优先发送。我们也可以通过配置CAN_MCR寄存器中的TXFP来使用FIFO模式的优先级, 这种模式下就会以请求发送的顺序发送消息。

bxCAN控制器本身是有出错重传的机制,在改状态下,当邮箱中的消息发送失败之后就会立即回到Scheduled的状态。发送失败主要是总线仲裁失利, 它将等待总线空闲再次尝试发送。有些时候,我们需要较高的实时性,可以设定CAN_MCR中的NART关闭自动重传机制。这样每个消息的发送只尝试一次, 如果仲裁失利或者总线出错,它都不再自动重传。

我们也可以置位CAN_TSR中的ABRQx来撤销发送请求。如果邮箱处于Pending和Scheduled的状态,它将直接回到Empty状态。如果处于Transmit的状态, 它将可能因为成功发送而进入Empty的状态,如果发送失败,它将回到Schedule的状态,然后回复Empty状态。

3.3 bxCAN接收消息

bxCAN提供了28个过滤器,用于根据需要过滤总线上的帧。bxCAN有两个3阶段的接收FIFO,它由硬件管理,接收的数据放在队尾,我们对接收FIFO邮箱的访问都是队首的消息。

每个过滤器都有两个32位的寄存器,它们可以工作在identifier list或者identifier mask两种方式下,可以通过配置FM1R寄存器来选择。 在identifier mask方式下,两个寄存器分别于接收到的ID进行位与运算,只有当它们的结果一样时才接受总线上的帧。这就好像其中一个寄存器用作掩码寄存器,可以滤去一些不关心的位。 在identifier list方式下,两个寄存器独立工作,只有当接收的ID与其中一个完全一致才接受帧。

此外对于标准形式的帧,我们只需要16位就可以完成对帧ID的过滤了。因此,bxCAN的每个过滤器也可以拆分为两组16位的过滤器。这点可以通过对FS1R寄存器来配置。

当总线上有一帧数据出现,bxCAN都会先通过开启的过滤器对其进行过滤。我们可以通过寄存器FFA1R,为每个过滤器指定一个FIFO中,将通过的帧存到对应的FIFO中。 我们可以直接访问FIFO邮箱来获取接收到的数据,但是读取完毕之后,需要置位RFxR中的RFOM0位,释放队首。否则,当队列满之后,新接受的消息将被丢弃。

4. CAN总线设备实现

在本部分中,我们将以开发板上的CAN1为例介绍bxCAN的初始化,以及数据的收发。由于我手头上只有一个开发板,所以这里的例程使用的是回环测试。 正常情况下,只要回环测试通过了,保证物理层连接没有问题,我们就可以在正常模式下,把数据发送到总线上,或者接收总线上的数据。

根据参考手册,我们整理了bxCAN的寄存器列表,用一个结构体表示。大体上可以分为三种类型,模块总体的寄存器、消息邮箱寄存器、过滤器相关的寄存器。

        typedef struct can_regs {
            volatile union can_mcr MCR;     /* 主控制寄存器, offset: 0x00 */
            volatile union can_msr MSR;     /* 主状态寄存器, offset: 0x04 */
            volatile union can_tsr TSR;     /* 发送状态寄存器, offset: 0x08 */
            volatile union can_rcr RF0R;    /* 接收FIFO 0状态寄存器, offset: 0x0C */
            volatile union can_rcr RF1R;    /* 接收FIFO 0状态寄存器, offset: 0x10 */
            volatile union can_ier IER;     /* 中断使能寄存器, offset: 0x14 */
            volatile union can_esr ESR;     /* 错误状态寄存器, offset: 0x18 */
            volatile union can_btr BTR;     /* 位时序寄存器, offset: 0x1C */
            uint32 rsv0[88];
            struct can_mailbox TxMailBox[3];/* 发送邮箱, offset: 0x180 */
            struct can_mailbox RxMailBox[2];/* 接收邮箱, offset: 0x1B0 */
            uint32 rsv1[12];
            volatile union can_fmr FMR;     /* 过滤器管理器寄存器, offset: 0x200 */
            volatile union can_fidx FM1R;   /* 过滤器模式管理, offset: 0x204, 0: Mask, 1:List */
            uint32 rsv2;
            volatile union can_fidx FS1R;   /* 过滤器尺寸管理, offset: 0x20C, 0: 2个16位, 1: 1个32位 */
            uint32 rsv3;
            volatile union can_fidx FFA1R;  /* 过滤器绑定FIFO, offset: 0x214 */
            uint32 rsv4;
            volatile union can_fidx FA1R;   /* 过滤器开关, offset: 0x21C, 1:激活 */
            uint32 rsv5[8];
            volatile struct can_filter Filters[28];
        } can_regs_t;

它的起始地址和访问对象定义如下:

        /* CAN寄存器地址映射 */
        #define CAN1_BASE 0x40006400
        #define CAN2_BASE 0x40006800
        /* CAN寄存器指针访问 */
        #define CAN1 ((can_regs_t *) CAN1_BASE)
        #define CAN2 ((can_regs_t *) CAN2_BASE)
图7 探索者开发板串口原理图

上图7中是探索者开发板的CAN接口,他使用了TJA1050作为物理芯片。并且在总线上已经接入了一个120Ω的电阻。CAN_TX和CAN_RX分别对应着F407的PA12和PA11脚。 下面我们完成对CAN的初始化操作。首先配置引脚和时钟,引脚的配置没什么可说的了,就是配置引脚的工作方式,上下拉电阻。CAN1是挂载在APB1上的外设,驱动时钟为42MHz。

        void can1_init(void)
        {
            can1_init_gpio();
            RCC->APB1ENR.bits.can1 = 1;

为了节省功耗,一上电bxCAN控制器是处于休眠状态的。所以我们先退出休眠状态,并进入初始化模式,开始配置控制器。

            // 退出Sleep,进入配置模式
            CAN1->MCR.bits.SLEEP = 0;
            CAN1->MCR.bits.INRQ = 1;
            while (1 != CAN1->MSR.bits.INAK);

配置控制器工作状态,开启自动重传功能。

            // 配置工作模式
            CAN1->MCR.bits.TTCM = 0; // 时间触发通信模式
            CAN1->MCR.bits.ABOM = 0; // Automatic BusOff Mode
            CAN1->MCR.bits.AWUM = 0; // Automatic Wakeup Mode
            CAN1->MCR.bits.NART = 0; // 自动重传
            CAN1->MCR.bits.RFLM = 0; // 接收报文, 覆盖
            CAN1->MCR.bits.TXFP = 0; // 发送邮箱优先级根据报文ID决定

由于CAN1的驱动时钟是42MHz的,所以这里分配的一个时间单位Tq的频率为8MHz。并且在bxCAN中的位时序默认同步段就是1Tq,相位段1中包含了物理延迟段, 所以只有两个分段,这两个分段的中间就是所谓的采样时间点。总的波特率为[freqTq / (3 + TS1 + TS2)],这里的分配是500Kbps。SJW是对波特率的容差。

            // 波特率配置
            CAN1->BTR.bits.BRP = 6 - 1; // 42M ==> 8M freqTq
            CAN1->BTR.bits.TS1 = 7;
            CAN1->BTR.bits.TS2 = 6;
            CAN1->BTR.bits.SJW = 1 - 1; // 1Tq容差

配置控制器进入回环测试模式,并退出配置模式,完成控制器的初始化工作。

            // 回环调试
            CAN1->BTR.bits.LBKM = 1;
            // CAN基础配置完成
            CAN1->MCR.bits.INRQ = 0;
            while (0 != CAN1->MSR.bits.INAK);

接下来,我们配置过滤器,代码中的注释已经足够详细,这里不再细讲。

            // 配置过滤器
            CAN1->FMR.bits.FINIT = 1;   // 开启过滤器配置模式
            CAN1->FA1R.bits.F0 = 1;     // 其中过滤器0
            CAN1->FM1R.bits.F0 = 0;     // 使用掩码模式
            CAN1->FS1R.bits.F0 = 1;     // 使用32位的过滤器
            CAN1->FFA1R.bits.F0 = 0;    // 使用FIFO 0
            CAN1->Filters[0].FR0 = 0;   // 在掩码模式下,只要FR0和FR1中
            CAN1->Filters[1].FR1 = 0;   // 有一个为0,就能接收所有的帧
            CAN1->FMR.bits.FINIT = 0;   // 退出过滤器配置模式

最后我们开启接收中断。

            // 开启接收中断
            CAN1->IER.bits.FMPIE0 = 1;
            NVIC->IPR.bits.CAN1_RX0_Irq = 0x80;
            NVIC->ISER.bits.CAN1_RX0_Irq = 1;
        }

完成了初始化操作之后,我们就可以通过发送邮箱发送数据了,下面是一个用于测试的发送函数。在这个函数中,我们发送了一个扩展帧,数据长度为8。 在其中的3到8行中,我们先配置了发送邮箱。最后在第10行中通过TXRQ请求发送。

        void CAN1_Send_Msg(void)
        {
            CAN1->TxMailBox[0].TIR.ebits.EXID = (uint32)0x11010001;
            CAN1->TxMailBox[0].TIR.ebits.IDE = 1;
            CAN1->TxMailBox[0].TDTR.tbits.DLC = 8;
            
            CAN1->TxMailBox[0].TDLR = 0xbeafbeaf;
            CAN1->TxMailBox[0].TDHR = 0xbeafbeaf;
            
            CAN1->TxMailBox[0].TIR.ebits.TXRQ = 1;
        }

因为是回环测试,所以在每次调用了函数CAN1_Send_Msg()之后,就应该立即收到一帧,同时产生一个CAN1_RX0的中断。 我们可以在中断函数中通过接收邮箱的寄存器,读出接收的ID,数据,时间戳。例程中没有体现这点,不过实现起来应该没什么难度。此外,在下面的中断服务函数中, 我们专门写了一句话,用于释放接收FIFO的队首。如果不执行该语句,释放队首,以后接收的数据将不能保存在FIFO中造成丢帧的现象。

        void CAN1_RX0_IRQHandler(void)
        {
            CAN1->RF0R.bits.RFOM = 1;
        }

5. 总结

在本章中,我们介绍了CAN的物理层和数据链路层的协议。在物理上,CAN以一对差分信号线实现通信。它通过总线的仲裁机制实现多主发送。 其数据帧和远程帧中都有ID用于总线仲裁,一帧数据最多可以发送8个字节,波特率最高为1Mbps。虽然CAN的帧格式中有一个ID,但它并不是用来路由消息的。 在总线上的所有数据都是广播的,各个设备根据自己的需要对这些数据进行过滤。

此外,在实际的应用中,只有CAN原始的物理层和数据链路层之外,我们还需要网络层和应用层的定义。目前有很多组织提供了不同的应用层协议, 比如说CANOpen在工业控制中比较常见,ASE J1939则在汽车领域中广泛使用。它们根据需要还对数据链路层进行了一些补充。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1