目录
一、四种通信方式区别
首先介绍一下四种通信方式的区别,在面试中也常见这些问题,方便大家理解记忆。
- SPI(Serial Peripheral Interface)是一种同步的串行通信协议,需要4根线:MISO、MOSI、SCLK和CS。SPI通信速度快,但只能在短距离内通信,且只能支持单主设备和多从设备的通信方式。
- IIC(Inter-Integrated Circuit)是一种同步的串行通信协议,需要2根线:SCL和SDA。IIC通信速度较慢,但可以在长距离内通信,且可以支持多主设备和多从设备的通信方式。
- UART(Universal Asynchronous Receiver/Transmitter)是一种异步的串行通信协议,需要2根线:TX和RX。UART通信速度较慢,但可以在长距离内通信,且可以支持点对点的通信方式。
- CAN:为了保证传输的安全性,相较于前面三种方法,使用了大量的手段保证数据及时、准确的传输。同时使用识别码,解决总线冲突的问题。这保证的优先级高的设备,先通讯。这也是为什么CAN能够用于工业、汽车等需要安全性高的领域。
二、SPI协议(全双工、速度快)
SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola(摩托罗拉)首先在其MC68HCXX系列处理器上定义的。SPI是一种高速的,全双工,同步的通信总线,可以在同一时间发送和接收数据,并且没有定义速度限制,通常能达到甚至超过10M/bps。在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。
1.SPI特性
SPI总线包括4条逻辑线,定义如下:
MISO:Master input slave output 主机输入,从机输出(数据来自从机);
MOSI:Master output slave input 主机输出,从机输入(数据来自主机);
SCLK :Serial Clock 串行时钟信号,由主机产生发送给从机;
SS:Slave Select 片选信号,由主机发送,以控制与哪个从机通信,通常是低电平有效信号。
其他制造商可能会遵循其他命名规则,但是最终他们指的相同的含义。以下是一些常用术语;
MISO也可以是SDI,DIN或DI;
MOSI也可以是SDO,DOUT或DO;
NSS也可以是CE,CS或SSEL;
SCLK也可以是SCK;
本文将按照以下命名进行讲解[MISO, MOSI, SCK,CS]
2.SPI工作流程
SPI是一个同步的数据总线,它是用一条单独的数据线和一条单独的时钟信号来保证发送端和接收端的完美同步。
时钟是一个振荡信号,它告诉接收端在确切的时机对数据线上的信号进行采样。
SPI有主、从两种模式,通常由一个主模块(主机)和一个或多个从模块(丛机)组成(SPI不支持多主机),主模块选择一个从模块进行同步通信,从而完成数据的交换。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起,当存在多个从设备时,通过各自的片选信号进行管理。
数据的采集时机可能是时钟信号的上升沿(从低到高)或下降沿(从高到低)。
整体的传输大概可以分为以下几个过程:
1、主设备发起信号,将CS/SS拉低,启动通信。
2、主设备通过发送时钟信号,来告诉从设备进行写数据或者读数据操作(采集时机可能是时钟信号的上升沿(从低到高)或下降沿(从高到低),因为SPI有四种模式,后面会讲到),它将立即读取数据线上的信号,这样就得到了一位数据(1bit)。
3、主机(Master)将要发送的数据写到发送数据缓存区(Menory),缓存区经过移位寄存器(缓存长度不一定,看单片机配置),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区。
4、从机(Slave)也将自己的串行移位寄存器(缓存长度不一定,看单片机配置)中的内容通过MISO信号线返回给主机。同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。
例如,下图示例中简单模拟SPI通信流程,主机拉低CS片选信号,启动通信,并且产生时钟信号,上升沿触发边沿信号,主机在MOSI线路一位一位发送数据0X53,在MISO线路一位一位接收数据0X46,如下图所示:
SPI是“全双工”(具有单独的发送和接收线路), SPI只有主模式和从模式之分,没有读和写的说法,可以在同一时间发送和接收数据。另外SPI的接收硬件可以是一个简单的移位寄存器。这比异步串行通信所需的完整UART要简单得多,并且更加便宜;
如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。
数据在传输中,高位在先还是低位在先,SPI协议并无明确规定,但是数据要在主从机中正确传输,自然双方要先约定好,一般会采用高位在先(MSB)方式传输。
3.SPI四种工作模式
除了配置串行时钟速率(频率)外,SPI主设备还需要配置时钟极性和时钟相位。
时钟极性 CKP/Clock Polarit
根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据;
CKP可以配置为1或0。这意味着您可以根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现。您必须参考设备的数据手册才能正确设置CKP和CKE。CKP = 0:时钟空闲IDLE为低电平 0;
CKP = 1:时钟空闲IDLE为高电平1;
时钟相位 CKE /Clock Phase (Edge)
除配置串行时钟速率和极性外,SPI主设备还应配置时钟相位(或边沿)。根据硬件制造商的不同,时钟相位通常写为CKE或CPHA;
顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿;
CKE = 0:在时钟信号SCK的第一个跳变沿采样;
CKE = 1:在时钟信号SCK的第二个跳变沿采样;
此处附上一组软件模拟SPI 通信传输数据的代码,方便各位理解其实现的方法。
//SPI发送数据,val为要发送的数据
void SPI_Send(u8 val)
{
u8 recv_data = 0, i = 0;//将接受数据清零方便直接或运算接受数据
SCK = 0;//时钟线拉低
for(i=0; i<8; i++) //传输数据,先发高位
{
//准备数据
if(val & (1<<(7-i))) //通过1左移7位然后相与,判断数据最高位是1还是0
{
MOSI = 1; //数据为1
}
else
{
MOSI = 0; //数据为0
}
delay_us(5);
SCK = 1; //时钟线拉高准备接收数据
delay_us(5);
//高电平区间接收数据,此处不重要但必须有
if(MISO == 1) //收到的为1
{
recv_data |= (1<<(7-i));//将数据位通过或运算保存。先保持高位
}
SCK = 0;//时钟线拉低
}
}
//SPI接收数据
u8 SPI_Receive(void)
{
u8 recv_data = 0, i = 0;
// u8 val = 0x00;
RC522_SCK = 0;
for(i=0; i<8; i++) //发送数据,此处不重要但必须有
{
//准备数据
// if(val & (1<<(7-i))) //1
// {
// MOSI = 1; //数据为1
// }
// else
// {
// MOSI = 0; //数据为0
// }
MOSI = 0; //输出线清零
delay_us(5);
SCK = 1; //时钟线拉高准备接收数据
delay_us(5);
//高电平区间接收数据
if(MISO == 1) //收到的为1
{
recv_data |= (1<<(7-i));//将数据位通过或运算保存。先保持高位
}
SCK = 0;
}
return recv_data; //返回接收到的数据
}
数据传输流程
首先主机和从机都选择同一传输模式。然后主机片选拉低,选中从机。接着在时钟的驱动下, MOSI发送数据,同时MISO读取接收数据。最后完成传输,取消片选。
/*
* 函数名: void SPI_WriteByte(uint8_t data)
* 输入参数: data -> 要写的数据
* 输出参数:无
* 返回值:无
* 函数作用:模拟 SPI 写一个字节
*/ SPI写1 Byte,循环8次,每次发送1 Bit;
void SPI_WriteByte(uint8_t data) {
uint8_t i = 0;
uint8_t temp = 0;
for(i=0; i<8; i++) {
temp = ((data&0x80)==0x80)? 1:0; //将data最高位保存到temp;
data = data<<1; //data左移一位,将次高位变为最高位,用于下次取最高位;
SPI_CLK(0); //CPOL=0 //拉低时钟,即空闲时钟为低电平, CPOL=0;
SPI_MOSI(temp); //根据temp值,设置MOSI引脚的电平;
SPI_Delay(); //简单延时,可以定时器或延时函数实现
SPI_CLK(1); //CPHA=0 //拉高时钟, W25Q64只支持SPI模式0或1,即会在时钟上升沿采样MOSI数据;
SPI_Delay();
}
SPI_CLK(0); //最后SPI发送完后,拉低时钟,进入空闲状态;
}
/*
* 函数名: uint8_t SPI_ReadByte(void)
* 输入参数:
* 输出参数:无
* 返回值:读到的数据
* 函数作用:模拟 SPI 读一个字节
*/ SPI读1 Byte,循环8次,每次接收1 Bit;
uint8_t SPI_ReadByte(void) {
uint8_t i = 0;
uint8_t read_data = 0xFF;
for(i=0; i<8; i++) {
read_data = read_data << 1; //“腾空” read_data最低位,8次循环后,read_data将高位在前;
SPI_CLK(0); //拉低时钟,即空闲时钟为低电平;
SPI_Delay();
SPI_CLK(1);
SPI_Delay();
if(SPI_MISO()==1) {
read_data = read_data + 1;
}
}
SPI_CLK(0); //最后SPI读取完后,拉低时钟,进入空闲状态
return read_data;
}
前面提到SPI传输可以看作一个虚拟的环形拓扑结构,即输入和输出同时进行。在前面“ SPI_WriteByte()”函数里,发送了1 Byte,也应该接收1 Byte,只是代码中忽略了接收引脚MISO的状态; 在前面“ SPI_ReadByte()”函数里,接收了1 Byte,也应该发送1 Byte,只是代码中忽略了发送引脚MOSI的内容。有些场景, SPI需要同时读写,因此还需要编写SPI同时读写函数。
/*
* 函数名: uint8_t SPI_WriteReadByte(uint8_t data)
* 输入参数: data -> 要写的一个字节数据
* 输出参数:无
* 返回值:读到的数据
* 函数作用:模拟 SPI 读写一个字节
*/SPI读和写1 Byte,循环8次,每次发送和接收1 Bit;
uint8_t SPI_WriteReadByte(uint8_t data) {
uint8_t i = 0;
uint8_t temp = 0;
uint8_t read_data = 0xFF;
for(i=0;i<8;i++) {
temp = ((data&0x80)==0x80)? 1:0; //将data最高位保存到temp;
data = data<<1; //data左移一位,将次高位变为最高位,用于下次取最高位;
read_data = read_data<<1; //“腾空” read_data最低位,8次循环后,read_data将高位在前;
SPI_CLK(0);
SPI_MOSI(temp);
SPI_Delay();
SPI_CLK(1);
SPI_Delay();
if(SPI_MISO()==1) { //读取MISO上的数据,保存到当前read_data最低位;
read_data = read_data + 1;
}
}
SPI_CLK(0);
return read_data;
}
三、IIC协议(半双工、速度中等)
IIC(Inter-Integrated Circuit)总线是一种由NXP(原PHILIPS)公司开发的两线式串行总线,用于连接微控制器及其外围设备。多用于主控制器和从器件间的主从通信,在小数据量场合使用,传输距离短,任意时刻只能有一个主机等特性。
在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送,高速 IIC 总线一般可达 400kbps 以上。IIC是为了与低速设备通信而发明的,所以IIC的传输速率比不上SPI
1.IIC数据接口
IIC只有两个总线: 一条是双向的串行数据线SDA,一条是串行时钟线SCL
SDA(Serial data)是数据线,D代表Data也就是数据,Send Data也就是用来传输数据。
SCL(Serial clock line)是时钟线,C代表Clock也就是时钟,用来控制数据发送的时序。
所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。I2C总线上的每个设备都自己一个唯一的地址,来确保不同设备之间访问的准确性。
2.IIC主要特点
通常我们为了方便把IIC设备分为主设备和从设备,基本上谁控制时钟线(即控制SCL的电平高低变换)谁就是主设备。
IIC主设备功能:主要产生时钟,产生起始信号和停止信号
IIC从设备功能:可编程的IIC地址检测,停止位检测
IIC的一个优点是它支持多主控(multimastering), 其中任何一个能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。支持不同速率的通讯速度,标准速度(最高速度100kHZ),快速(最高400kHZ)。
SCL和SDA都需要接上拉电阻 (大小由速度和容性负载决定一般在3.3K-10K之间) 保证数据的稳定性,减少干扰。
IIC是半双工,而不是全双工 ,同一时间只可以单向通信
为了避免总线信号的混乱,要求各设备连接到总线的输出端时必须是漏极开路(OD)输出或集电极开路(OC)输出。
IIC的高阻态
漏极开路(Open Drain)即高阻状态,适用于输入/输出,其可独立输入/输出低电平和高阻状态,若需要产生高电平,则需使用外部上拉电阻
高阻状态:高阻状态是三态门电路的一种状态。逻辑门的输出除有高、低电平两种状态外,还有第三种状态——高阻状态的门电路。电路分析时高阻态可做开路理解。
我们知道IIC的所有设备是接在一根总线上的,那么我们进行通信的时候往往只是几个设备进行通信,那么这时候其余的空闲设备可能会受到总线干扰,或者干扰到总线,怎么办呢?
为了避免总线信号的混乱,IIC的空闲状态只能有外部上拉, 而此时空闲设备被拉到了高阻态,也就是相当于断路, 整个IIC总线只有开启了的设备才会正常进行通信,而不会干扰到其他设备。
3.工作流程介绍
(1)主机向从机写入数据过程
Start: IIC开始信号,表示开始传输。
DEVICE_ADDRESS:: 从设备地址,就是7位从机地址。
R/W: W(write)为写,R(read)为读。
ACK: 应答信号。
WORD_ADDRESS : 从机中对应的寄存器地址 比方说访问 OLED中的 某个寄存器。
DATA: 发送的数据。
STOP: 停止信号。结束IIC通信。
流程介绍:
1、主机首先产生START信号;
2、然后发送一个从机地址,这个地址共有7位,紧接着的第8位是数据方向位(R/W),“0”表示主机向从机发送数据(写),“1”表示主机从从机接收数据(读);
3、主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,若相同,则认为自己正在被主机寻址,根据R/W位将自己确定为发送器和接收器;
4、这时候主机等待从机的应答信号(ACK);
5、当主机收到应答信号时,发送要访问从机的那个地址, 继续等待从机的应答信号;
6、当主机收到应答信号时,发送N个字节的数据,继续等待从机的N次应答信号;
7、主机产生停止信号,结束传输过程。还有一种情况是从机发送非应答信号表示接受完毕后主机产生停止信号结束传输过程。
(2)主机读取从机数据过程
1、主机首先产生START信号;
2、然后紧跟着发送一个从机设备地址,注意此时该地址的第8位为0,表明是向从机写命令;
3、这时候主机等待从机的应答信号(ACK);
4、当主机收到应答信号时,发送要访问的寄存器地址,继续等待从机的应答信号;
5、当主机收到应答信号后,主机要改变通信模式(主机将由发送变为接收,从机将由接收变为发送)所以主机重新发送一个开始start信号,然后紧跟着发送一个从机地址,注意此时该地址的第8位为1,表明将主机设置成接收模式开始读取数据;
6、这时候主机等待从机的应答信号,当主机收到应答信号时,就可以接收1个字节的数据,当接收完成后,主机发送非应答信号(NACK),表示不再接收数据;
7、主机进而产生停止信号,结束传送过程。
4.IIC常见问题
(1)为什么Open-Drain开漏输出需要上拉电阻
开漏Pin不连接外部的上拉电阻,则只能输出低电平。当输出电平为低时,N沟道MOS管是导通的,这样在Vcc和GND之间有一个持续的电流流过上拉电阻R和三极管Q1。这会影响整个系统的功耗。采用较大值的上拉电阻可以减小电流。但是大的阻值会使输出信号的上升时间变慢。即上拉电阻R pull-up的阻值 决定了逻辑电平转换的沿的速度。阻值越大,速度越低功耗越小。反之亦然。
(2)为什么IIC需要漏极开路
防止短路
如果不设为开漏,而设为推挽,几个设备连在同一条总线上,这时某一设备的某个IO输出高电平,另有一台设备的某一个IO输出低电平,这时你会发现这两个IO的VCC和GND短路了;但是开漏就不会有这个问题。
增强端口扇出能力、降低功耗
IC为增强端口扇出能力而设计为漏极开路样式,使用时将该端口设为低电平有效的灌电流模式,能得到最大输出电流同时IC功耗最低。此类端口当输出高电平则需要外接上拉电阻。
利用“线与”判断总线占用状态
可以将多个开漏输出的Pin脚,连接到一条线上,形成“与逻辑”关系,即“线与”功能,任意一个变低后,开漏线上的逻辑就为0了。这也是I2C,SMBus等总线判断总线占用状态的原理。
如果总线上的一个A设备将SDA拉高,这时总线上另一个B设备已将SDA拉低,这时由于1&0=0,所以A设备检查SDA的时候会发现不是高电平而是低电平,这就表明总线上已经有其他设备占用总线了,A只好放弃,如果检测是高电平那就可以使用。
增加驱动能力
如果在漏极drain_output接上拉电阻,则可以进行电平转换,且驱动能力较强。
利用外部电路的驱动能力,减少IC内部的驱动。当IC内部MOSFET导通时,驱动电流是从外部的VCC流经R pull-up ,MOSFET到GND。IC内部仅需很下的栅极驱动电流。
控制输出高电平大小
可以利用改变上拉电源的电压,改变传输电平。
(3)软件IIC和硬件IIC的区别
软件IIC:软件IIC通信指的是用单片机的两个I/O端口模拟出来的IIC,用软件控制管脚状态以模拟I2C通信波形,软件模拟寄存器的工作方式。
硬件IIC:一块硬件电路,硬件I2C对应芯片上的I2C外设,有相应I2C驱动电路,其所使用的I2C管脚也是专用的,硬件(固件)I2C是直接调用内部寄存器进行配置。
硬件I2C的效率要远高于软件的,而软件I2C由于不受管脚限制,接口比较灵活。
STM32硬件I2C与软件模拟I2C超详解_stm32 i2c-优快云博客
四、URAT协议(全双工、速度慢)
1.UART协议概念
UART的全称是通用异步收发器(Universal Asynchronous Receiver/Transmitter)
Universal 通用性体现在UART使用范围广上,作为一个通用的接口协议,UART广泛的应用在各类MCU和SOC产品上。
Asynchronous 异步性体现在“不需要额外的时钟线进行数据的同步传输”即只要信号拉低,即可开始传送数据,而另一些通讯协议,需要引入时钟信号来进行操作,如SPI需要在时钟的边沿发送数据。
Receiver/Transmitter 收发器则更好理解,即一个数据的发送方和一个数据的接收方,也意味着在数字IC设计中需要分别设计Receiver和Transmitter。
2.UART的帧格式
UART的一帧由起始位、数据位、校验位和停止位组成。数据逐位传输,如下图所示
(1)为什么UART的传输需要起始位?
因为UART没有控制线,要让接收方知道什么时候开始接收数据,需要一些手段,UART数据的传输中,只有一根线,所以在发送数据之前,先发一位逻辑“0”作为数据发送的起始标志,接收方在空闲时,当检测到有一个低电平,则开始接逐位接收数据。
(2)UART基本的数据形式
- 默认无传输数据时,为高电平
- 当信号拉低,传输线上的电平拉低,意味着开始进行数据传输
- 紧接着起始位的是数据位,它可以是5、6、7或8位。
- UART的“校验位”紧挨着“数据位”,采用奇偶校验方式,根据设置,校验位可以存在也可以不存在。
- UART将停止位作为停止标志,是在数据位(没有校验位)和校验位(有校验位)之后发送1~2位的逻辑“1”高电平。当发送完停止位之后,UART总线进入空闲。
(3)为什么UART的数据位可变?
因为UART是一种低速总线,每多发一位都占用不少的时间(由传输波特率决定),所以可以根据传输数据的特点,采用不同位宽以节约数据传输的时间。
3.UART的波特率
如果从更高的level审视UART传输协议,如嵌入式开发者的角度,我们会发现,在使用具体的UART协议前,我们需要对发送端和接收端进行波特率的同步,以此来确保发射端的数据可以在接收端得到正确的采集。常用的波特率可以是300,1200,2400,9600,19200,38400,115200,这些数意味着什么呢?别着急,我们接下来要讨论这个内容。
(1)什么是波特率
波特率等于每秒钟传输的数据位数,假如我们的全局时钟频率为100MHz,波特率设置为9600,那么意味着每秒该UART传输协议可以传输9600bits的数据,换句话说传输1比特需时间约为:10^9(ns)/9600=104166(ns)。
(2)如何换算波特率
书接上文,时钟频率假如为100MHz,这意味着我们的时钟周期为10ns,因此10416个时钟周期我们就可以传输1bit数据,换言之我们需要一个大小为10416的分频电路来对100MHz时钟进行处理,因此在设计UART的过程中,我们需要使用分频电路依据波特率处理全局时钟,依据分频后的时钟节奏来发送数据和接收数据。
4.UART的缺点
(1)电气接口不统一
UART只是对信号的时序进行了定义,而未定义接口的电气特性;
UART通信时一般直接使用处理器使用的电平,即TTL电平,但不同的处理器使用的电平存在差异,所以不同的处理器使用UART通信时一般不能直接相连;
UART没有规定不同器件连接时连接器的标准,所以不同器件间通过UART通信时连接很不方便。
(2)抗干扰能力差
UART一般直接使用TTL信号来表示0和1,但TTL信号的抗干扰能力较差,数据在传输过程中很容易出错。
(3)通信距离极短
因为TTL信号的抗干扰能力较差,所以其通信距离也很短,一船只能用于一个电路板上的两个不芯片之间的通信。
5.RS232标准
缺点:
接口的信号电平值较高,易损坏接口电路的芯片,又因为与TTL电平不兼容,所以需要使用电平转换芯片才能与TTL电路连接
通信速度较低。
易产生共模干扰,抗噪声干扰性弱。
传输距离较短(15m)。
五、CAN通信(半双工)
1.CAN通信概念
CAN通信是一种多节点通信协议,最早由Bosch公司开发并在1986年首次推出。它被广泛应用于汽车电子控制系统、工业自动化领域以及其他需求多节点通信的应用场中。
CAN通信的特点之一是支持多节点之间的高速数据传输,适用于需要高实时性和高可靠性的系统。CAN总线由两根线组成,分别是CAN_H(CAN High)和CAN_L(CAN Low)。CAN总线使用不同的电压电平来表示0和1,并通过差分信号传输来抗干扰。
2.CAN通信概念
CAN通信采用CSMA/CD
(Carrier Sense Multiple Access with Collision Detection)的工作原理。这意味着每个节点都可以在总线上发送消息,但在发送之前需要先监听总线上的通信情况。
当一个节点要发送消息时,首先会监听总线,如果没有其他节点正在发送消息,它就可以开始发送。如果同时有多个节点尝试发送消息,就会发生冲突。在CAN总线上使用的是非毁坏性冲突检测机制,冲突的节点会立即停止发送,并在发送完自己的消息后再次来检测冲突。
CAN通信中还使用了位定时传输方式,即总线上的每个位都有固定的时间段。发送节点将每个位的电平保持一段时间,接收节点则在相应的时间段内检测位的电平。这种位定时传输方式确保了数据的同步和准确性。
此外,CAN通信还通过帧的优先级
来管理消息的传输。较低优先级的帧会在总线上等待较高优先级的帧发送完毕后再发送,确保重要消息的及时传输。
3. CAN通信的应用领域
CAN通信被广泛应用于各种领域,特别是在汽车和工业控制系统中。
在汽车领域,CAN通信用于连接汽车的各个控制单元,如发动机控制单元(ECU)、刹车系统、仪表盘等。CAN总线提供了高速、实时的数据传输,使得这些控制单元能够相互通信和协调工作,实现车辆的高效控制和监测。
在工业控制系统中,CAN通信被用于连接各种设备和传感器,例如机器人、PLC(可编程逻辑控制器)、传感器网络等。通过CAN总线,这些设备可以实现实时数据交换和远程控制,从而提高生产效率和系统的可靠性。
除了汽车和工业控制,CAN通信还应用于其他领域,包括航空航天、医疗设备、能源管理等。CAN通信的高可靠性和抗干扰能力使其成为处理实时数据和多节点通信的理想选择。
4. CAN帧格式与标识符
CAN通信使用帧格式来传输数据。CAN帧分为标准帧
和扩展帧
两种格式。
标准帧由11位标识符、数据域、控制域和CRC
(循环冗余校验)组成。标识符用于标识消息的优先级和内容,数据域用于传输实际的数据,控制域包含帧的控制信息,而CRC用于发送节点计算校验和,接收节点用于验证数据的完整性。
扩展帧使用29位标识符,其他组成部分与标准帧相同。扩展帧的使用使得CAN网络能够处理更多的节点和更大的数据量。
标识符的选择对于CAN通信至关重要。帧使用的标识符决定了其在总线上的优先级,较低的标识符意味着较高的优先级。在设计和配置CAN网络时,需要合理设置标识符以确保系统的正确运行。
5. CAN通信模式
CAN通信有几种不同的模式,可以通过配置CAN控制器的寄存器来选择适合应用需求的模式。
常见的CAN通信模式包括:
- 正常模式(Normal Mode):用于实际通信,节点能够发送和接收数据。
- 监听模式(Listen-Only Mode):节点只能监听总线上的通信,但不能发送消息。
- 回环模式(Loopback Mode):发送的帧会回环到本地接收,用于自测和调试。
- 静默模式(Silent Mode):节点只能监听总线上的通信,不能发送消息,并向其他节点传递错误状态。
6. 使用HAL库实现CAN通信
对于使用HAL(Hardware Abstraction Layer)库进行STM32开发的用户,HAL库提供了高级的抽象接口和简洁的函数调用,方便编写和管理CAN通信。
6.1 STM32 CAN通信配置:
a. 使能CAN外设:在初始化阶段,需要使能所选择的CAN外设,并配置相应的时钟。
b. 配置CAN控制器:通过设置CAN控制器的特殊寄存器,配置CAN通信的参数,包括波特率、工作模式、过滤器等。
c. 配置GPIO引脚:将相关的GPIO引脚配置为CAN模式,以实现CAN数据的收发。
d. 初始化CAN通信:通过初始化CAN控制器的寄存器,准备CAN通信的环境,包括清除任何悬空状态和错误标志。
e. 开始CAN通信:使能CAN控制器的接收和发送功能,使其处于工作状态。
6.2 整体代码
// 包含所需的头文件
#include "stm32f4xx_hal.h"
// CAN消息结构体
CAN_TxHeaderTypeDef TxHeader;
CAN_RxHeaderTypeDef RxHeader;
uint8_t TxData[8];
uint8_t RxData[8];
void CAN_Configuration(void)
{
// 初始化CAN控制器
hcan.Instance = CAN1;
hcan.Init.Prescaler = 10;
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_8TQ;
hcan.Init.TimeSeg2 = CAN_BS2_7TQ;
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = ENABLE;
hcan.Init.AutoWakeUp = DISABLE;
hcan.Init.AutoRetransmission = ENABLE;
hcan.Init.ReceiveFifoLocked = DISABLE;
HAL_CAN_Init(&hcan);
// 配置CAN过滤器
CAN_FilterTypeDef sFilterConfig;
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000;
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000;
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.SlaveStartFilterBank = 14;
HAL_CAN_ConfigFilter(&hcan, &sFilterConfig);
// 启动CAN通信
HAL_CAN_Start(&hcan);
// 配置CAN消息头
TxHeader.StdId = 0x123;
TxHeader.ExtId = 0x00;
TxHeader.RTR = CAN_RTR_DATA;
TxHeader.IDE = CAN_ID_STD;
TxHeader.DLC = 8;
TxHeader.TransmitGlobalTime = DISABLE;
}
void CAN_SendData(uint8_t* data, uint32_t length)
{
// 填充发送消息的数据
for (uint8_t i = 0; i < length; i++) {
TxData[i] = data[i];
}
// 发送消息
if (HAL_CAN_AddTxMessage(&hcan, &TxHeader, TxData, &TxMailbox) == HAL_OK) {
HAL_CAN_Transmit(&hcan, 100);
}
}
void CAN_ReceiveData(void)
{
// 接收消息
if (HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_RX_FIFO0) > 0) {
if (HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK) {
// 处理接收到的数据
// …
}
}
}
int main(void)
{
// 初始化HAL库和其他外设
// 配置CAN通信
CAN_Configuration();
while (1) {
// 发送数据
uint8_t sendData[] = {0x01, 0x02, 0x03};
CAN_SendData(sendData, sizeof(sendData));
// 接收数据
CAN_ReceiveData();
}
}