在简单的学习过了STM32中的简单外设以及中断系统后,在本章节中开始介绍STM32芯片中各个通信接口的配置。在计算机中,按数据传输方式可分为串行通信以及并行通信;按数据同步方式可分为异步通信和同步通信;按数据传输方向课分为单工、半双工和全双工通信。
串行通信: 在一条数据线上,将数据按照二进制位依次传输,传输一位数据占据一个固定的时间长度。适用于计算机之间、计算机与外设之间的远距离通信,其具备占用传输线数量少、长距离传输时成本低的优点,但数据传输控制相比于并行通信复杂。
并行通信: 在多条数据线上,一个数据的多位同时进行传输,通常是8位、16位或32位数据一起进行传输。其具备控制简单、传输速度快的特点,但由于其占用数据线过多,长距离传输数据时成本较高,且接受设备出同时接收数据时容易出现错位即抗干扰能力弱。
异步通信: 通信双方(发送和接收端)使用各自的时钟控制数据的发送和接收,但为了双方的收发协调,时钟需要尽可能的一致。其具备实现容易、成本低的特点,但通信过程中每个字符需要添加2~3位数据作为起止位,各帧之间需有间隔导致了传输效率不高。
同步通信: 通信时需要建立发送方时钟对接收方时钟的直接控制,使得双方达到完全的同步。传输数据的位之间距离为“位间隔”的整数倍,且传送的字符间不留间隙,保持位同步关系。
单工通信: 数据的传输仅能够沿着一个方向,不能反向传输。
半双工通信: 数据的传输可以沿着两个方向,但需要分时进行。
全双工通信: 数据的传输可以同时双向的进行。
通信速率: 衡量通信性能的参数,以波特率来表示,代表着每秒钟传输二进制编码的位数,单位为:位/秒。
串口通信(Serial communication)是外设和计算机之间经过数据线、地线以及电源线进行数据传输的一种通信方式,属于串行通信。串口是常用的接口标准,规定了电气标准,但没有规定接口插件电缆以及使用的协议。
A. 接口标准
串口通信的接口标准包含了RS-232C、ES-232、RS-422A、RS-485等等,常用的是RS-232C标准,定义了数据终端设备(DTE)与数据通信设备(DCE)之间的物理接口标准。而在RS-232C标准下又有DB25(25针连接器)和DB9(9针连接器)之分,DB9的管脚说明如下表中所示:
管脚序号 | 管脚定义 | 功能 | 信号方向 |
---|---|---|---|
1 | PGND | 保护接地 | |
2 | TXD | 发送数据(串行输出) | DTE→DCE |
3 | RXD | 接收数据(串行输入) | DTE←DCE |
4 | RTS | 发送请求 | DTE→DCE |
5 | CTS | 发送允许 | DTE←DCE |
6 | DSR | 数据建立就绪 | DTE←DCE |
7 | SGND | 信号接地 | |
8 | DCD | 载波检测 | DTE←DCE |
9 | DTR | 数据终端准备就绪 | DTE→DCE |
10 | RI | 振铃指示 | DTE←DCEddd |
B. 通信协议
RS23的通信协议遵循96-N-8-1格式。其中“96”表示通信波特率为9600;“N”表示无校验位;“8”表述数据位数为8位;“1”表示有1位停止位。
在STM32中的USART(通用同步异步收发器),可以与外部设备进行全双工数据交换,UART(通用异步收发器)是在其基础上剪裁掉了同步通信功能。USART在STM32中应用最多的是对printf重定向,通过输出函数将信息打印到串口助手上显示出来。USART的内部结构框图如下所示:
引脚功能: TX(发送数据输出引脚),RX(接收数据输入引脚),SW_RX(数据接收引脚,内部引脚),nRTS(请求以发送,n表示低电平有效,仅适用于硬件流控制),nCTS(清除以发送,n表示低电平有效,仅适用于硬件流控制)。
数据寄存器: USART_DR,低9位有效,第9位数据是否有效取决于USART控制寄存器1(USART_CR1)的M位设置,当M位为0时表示8位数据字长,M位为1时表示9位数据字长,通常情况下使用8位数据字长。
控制器: USART中有专门控制发送的发送器、控制接收的接收器,以及唤醒单元、中断控制等。
波特率生成: 波特率越大,传输速度越快。
计算公式: 波特率= USART的时钟频率 / (16*USARTDIV)
其中USARTDIV是一个存放在波特率寄存器(USART_BRR)的一个无符号定点数。串口通信常用的波特率为4800、9600、115200等。
在对STM32F1的USART进行配置时,需要使用到USART库,包含stm32f10x_usart.c和stm32f10x_usart.h文件。
使能串口时钟和GPIO端口时钟;
STM32F103ZET6芯片中共有5个串口通信资源,其中串口1挂载在APB1总线上,串口2~5挂载在APB2总线上。使用串口时,分别使能对应总线上的串口以及GPIO的时钟即可。调用函数:RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOx, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USARTx, ENABLE);
配置GPIO端口,设置串口引脚为复用功能;
使用引脚的USART功能需要将GPIO设置为复用功能,并将串口的Tx引脚配置为复用推挽输出,Rx引脚配置为浮空输入,调用函数:
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
typedef struct
{
uint32_t USART_BaudRate; //波特率,4800 / 9600 / 115200
uint16_t USART_WordLength; //字长,8 / 9
uint16_t USART_StopBits; //停止位,0.5 / 1 / 1.5 / 2
uint16_t USART_Parity; //校验位 USART_Parity_NO(无校验)、USART_Parity_Even(偶校验)以及USART_Parity_Odd(奇校验)
uint16_t USART_Mode; //USART模式 USART_Mode_Rx / USART_Mode_Tx
uint16_t USART_HardwareFlowControl; //硬件流控制 只有在硬件流模式下有效,常用无硬件流模式 USART_HardwareFlowControl_None
} USART_InitTypeDef;
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
USART_ITConfig(USART1, USART_IT_TC, ENABLE);
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
uint16_t USART_ReceiveData(USART_TYpeDef* USARTx);
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
使用STM32的USART1实现单片机与电脑端串口助手之间的通信,详细的代码如下:
usart.h
#ifndef _usart_H
#define _usart_H
#include "system.h"
void USART1_Init(u32 bound);
#endif
usart.c
#include "usart.h"
int fputc(int ch,FILE *p)
{
USART_SendData(USART1, (u8)ch);
while(USART_GetFlagStatus(USART1,USART_FLAG_TC) != SET);
return ch;
}
void USART1_Init(u32 bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
// 配置GPIO端口
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;//TX
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;//RX
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//USART1 初始化配置
USART_InitStructure.USART_BaudRate = bound;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//ÎÞÓ²¼þÊý¾ÝÁ÷¿ØÖÆ
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
USART_ClearFlag(USART1, USART_FLAG_TC);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void USART1_IRQHandler(void)
{
u8 r;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
r =USART_ReceiveData(USART1);
USART_SendData(USART1,r);
while(USART_GetFlagStatus(USART1,USART_FLAG_TC) != SET);
}
USART_ClearFlag(USART1,USART_FLAG_TC);
}
main.c
#include "system.h"
#include "led.h"
#include "SysTick.h"
#include "usart.h"
int main()
{
u8 i=0;
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
LED_Init();
USART1_Init(9600);
while(1)
{
i++;
if(i%20==0)
{
led1=!led1;
}
delay_ms(10);
}
}
I2C(Inter-Intergrated Circuit)是PHILIPS公司开发的串行总线,用于连接微控制器及其外围设备,属于同步通信。其具有控制方式简单,通信速率较高的优点。I2C总线由两根双向信号线组成,一根是数据线SDA,一根是时钟线SCL。使用I2C通信设备的连接如下图所示:
I2C的物理层具备如下特点:
(1) 支持多设备,总线上可连接多个I2C设备,支持多主机和多从机设备;
(2) I2C总线只是用SDA和SCL两根线路,SDA传输数据,SCL用于数据收发同步时钟;
(3) 连接在I2C总线上的设备都拥有一个独一无二的地址,主机可使用改地址对不同设备之间进行访问;
(4) I2C总线通过上拉电阻,当I2C设备空闲时,输出高阻态,当所有设备都空闲时,由上拉电阻将总线拉升为高电平;
(5) 多主机设备同时使用I2C总线时,为了防止数据冲突,会采用仲裁方式决定占用情况;
(6) 三种传输模式:标准模式(速率100kbit/s)、快速模式(400kbit/s)以及高速模式(3.4Mbit/s);
(7) 连接到相同I2C总线上的设备数量受到总线最大电容400pF的限制。
I2C的协议层包含了:
(1) 数据有效性规定;
I2C总线在传输数据时,SCL时钟信号为高电平的过程中,数据线上的数据需要保持稳定;在时钟信号为低电平时,数据线上允许有高/低电平的变化。
(2) 起始和停止信号;
SCL时钟线为高电平是,SDA数据线由高电平向低电平的变化表示起始信号;SCL为高电平是,SDA有低电平向高电平的变化表示终止信号。
(3) 应答信号;
当主机发送器件传输一个字节的数据后,其尾部需跟一个校验位,检验位是接收端通过控制SDA来实现,可以提示发送端数据已被接收完成。校验位就是数据或地址传输过程中的响应,包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端,设备接收到数据后,如果要继续接收数据,则需要向对方发送ACK信号即特定的低电平脉冲;若不要继续接收数据,则需要向对方发送NACK信号即特定的高电平脉冲。
(4) 总线寻址方式;
I2C总线寻址方式按照从机地址位数可分为:7位寻址和10位寻址。
D7~D0组成从机的地址,D0为数据传输方向位,置0时表示主机向从机写数据,置1时表示主机由从机读取数据。
(5) 数据传输。
I2C总线上传输的数据比较广泛,包含有地址信号以及数据信号,在一个起始信号发出后,紧接着需要传输一个从机地址,每一次数据的传输由主机产生的终止信号结束。在总线的一次数据传输中,有下以下的方式:
A. 主机向从机发送数据,数据传输方向在整个过程中不变;
B. 主机在第一个字节后,变为从从机读取数据;
C. 传输过程中,需要改变数据流动方向时,起始信号和从机地址被重复产生一次,但两次读/写方向位相反。
AT24C02是一个2K位串行CMOS,内部含有256个8位字节,有一个16字节页写缓冲器。AT24C02通过I2C总线进行操作,拥有写保护功能,芯片内部存储的数据可以保证在掉电的情况下不丢失,一般用来存储一些比较重要的数据。
AT24C02芯片的地址有7位,高4位为1010,低3位由A0/A1/A2信号线的高/低电平决定。I2C通信中传输地址或数据都是以字节为单位的,在传输地址时,芯片的地址占据7位,还有1位用来选择读/写方向。
24cxx.h
#ifndef _24cxx_H
#define _24cxx_H
#include "system.h"
#include "iic.h"
#define AT24C01 127
#define AT24C02 255
#define AT24C04 511
#define AT24C08 1023
#define AT24C16 2047
#define AT24C32 4095
#define AT24C64 8191
#define AT24C128 16383
#define AT24C256 32767
#define EE_TYPE AT24C02
void AT24CXX_Init(void);
u8 AT24CXX_ReadOneByte(u16 ReadAddr);
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite);
void AT24CXX_WritelenByte(u16 WriteAddr,u8 DataToWrite,u8 len);
u32 AT24CXX_readLenByte(u16 ReadAddr,u8 Len);
u8 AT24CXX_Check(void);
void AT24CXX_read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead);
void AT24CXX_write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite);
#endif
24cxx.c
#include "24cxx.h"
#include "SysTick.h"
void AT24CXX_Init(void)
{
IIC_Init();
}
u8 AT24CXX_ReadOneByte(u16 ReadAddr) //从从机AT24Cxx读取一个字节数据
{
u8 temp=0;
IIC_start();
if(EE_TYPE>AT24C16)
{
IIC_send_byte(0xA0); //发送写命令
IIC_wait_ack(); //等待应答
IIC_send_byte(ReadAddr>>8); //发送高地址
}
else
{
IIC_send_byte(0xA0+((ReadAddr/256)<<1)); //发送器件地址,写数据
}
IIC_wait_ack();
IIC_send_byte(ReadAddr%256); //发送低地址
IIC_wait_ack();
IIC_start();
IIC_send_byte(0xA1); //进入接收模式
IIC_wait_ack();
temp=IIC_read_byte(0);
IIC_stop();
return temp;
}
//在AT24Cxx指定地址写入数据
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
IIC_start();
if(EE_TYPE>AT24C16)
{
IIC_send_byte(0xA0); //发送写命令
IIC_wait_ack(); //等待应答
IIC_send_byte(WriteAddr>>8); //发送高地址
}
else
{
IIC_send_byte(0xA0+((WriteAddr/256)<<1)); //发送器件地址,写数据
}
IIC_wait_ack();
IIC_send_byte(WriteAddr%256);
IIC_wait_ack();
IIC_send_byte(DataToWrite);
IIC_wait_ack();
IIC_stop();
delay_ms(10);
}
//向AT24CXX指定位置写入长度为len的数据
void AT24CXX_WritelenByte(u16 WriteAddr,u8 DataToWrite,u8 len)
{
u8 t;
for(t=0;t<len;t++)
{
AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);
}
}
//从ATC24XX中指定地址开始读长度为len的数据
u32 AT24CXX_readLenByte(u16 ReadAddr,u8 Len)
{
u8 t;
u32 temp=0;
for(t=0;t<Len;t++)
{
temp<<=8;
temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1);
}
return temp;
}
//检查ATC24XX工作是否正常,0正常,1不正常
u8 AT24CXX_Check(void)
{
u8 temp;
temp=AT24CXX_ReadOneByte(255);
if (temp==0x36)
return 0;
else
{
AT24CXX_WriteOneByte(255,0x36);
temp=AT24CXX_ReadOneByte(255);
if (temp==0x36)
return 0;
}
return 1;
}
//从指定地址开始读出指定额数据,pBuffer存放数据的数组首地址,NumToRead需要读出数据的个数
void AT24CXX_read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
while(NumToRead)
{
*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);
NumToRead--;
}
}
//指定地址写入指定个数的数据
void AT24CXX_write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
WriteAddr++;
pBuffer++;
}
}
iic.h
#ifndef _iic_H
#define _iic_H
#include "system.h"
/*IIC_SCL时钟端口、引脚定义*/
#define IIC_SCL_PORT GPIOB
#define IIC_SCL_PIN (GPIO_Pin_10)
#define IIC_SCL_PORT_RCC RCC_APB2Periph_GPIOB
/*IIC_SDA数据端口、引脚定义*/
#define IIC_SDA_PORT GPIOB
#define IIC_SDA_PIN (GPIO_Pin_11)
#define IIC_SDA_PORT_RCC RCC_APB2Periph_GPIOB
/*IO操作*/
#define IIC_SCL PBout(10)//SCL
#define IIC_SDA PBout(11)//SDA
#define READ_SDA PBin(11)//ÊäÈëSDA
//IIC操作函数
u8 IIC_read_byte(u8 ack);
void IIC_send_byte(u8 byte);
void IIC_noack(void);
void IIC_ack(void);
u8 IIC_wait_ack(void);
void IIC_stop(void);//Í£Ö¹ÐźÅ
void IIC_start(void);
void SDA_IN(void);
void SDA_OUT(void);
void IIC_Init(void);
#endif
iic.c
#include "iic.h"
#include "SysTick.h"
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(IIC_SCL_PORT_RCC|IIC_SDA_PORT_RCC,ENABLE);
GPIO_InitStructure.GPIO_Pin=IIC_SCL_PIN;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_Init(IIC_SCL_PORT,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin=IIC_SDA_PIN;
GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
IIC_SCL=1;
IIC_SDA=1;
}
/*模拟IIC工作时序*/
void SDA_OUT(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin=IIC_SDA_PIN;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
}
void SDA_IN(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin=IIC_SDA_PIN;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
}
void IIC_start(void) //起始信号
{
SDA_OUT();
IIC_SDA=1;
IIC_SCL=1;
delay_us(5);
IIC_SDA=0;
delay_us(6);
IIC_SCL=0;
}
void IIC_stop(void) //终止信号
{
SDA_OUT();
IIC_SCL=0;
IIC_SDA=0;
IIC_SCL=1;
delay_us(6);
IIC_SDA=1;
delay_us(6);
}
//等待应答信号的到来,1接收应答失败,0接收应答成功
u8 IIC_wait_ack(void)
{
u8 tempTime=0;
IIC_SDA=1;
delay_us(1);
SDA_IN(); //SDA设置输入
IIC_SCL=1;
delay_us(1);
while(READ_SDA)
{
tempTime++;
if(tempTime>250) //接收应答失败
{
IIC_stop();
return 1;
}
}
IIC_SCL=0;
return 0;
}
//产生应答信号
void IIC_ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(5);
IIC_SCL=0;
}
//产生非应答信号
void IIC_noack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(5);
IIC_SCL=0;
}
//发送单字节信息
void IIC_send_byte(u8 byte)
{
u8 t;
SDA_OUT();
IIC_SCL=0; //开始数据传输
for(t=0;t<8;t++)
{
if((byte&0x80)>0) //每一次发送最高位
IIC_SDA=1;
else
IIC_SDA=0;
byte<<=1; //发送位左移到最高位
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
//读取单字节信息
u8 IIC_read_byte(u8 ack)
{
u8 i,receive=0;
SDA_IN(); //设置为输入
for(i=0;i<8;i++)
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)
receive++;
delay_us(1);
}
//读取完成后,发送应答信息
if(!ack)
{
IIC_noack();
}
else
{
IIC_ack();
}
return receive;
}
main.c
#include "system.h"
#include "led.h"
#include "SysTick.h"
#include "usart.h"
#include "24cxx.h"
#include "key.h"
int main()
{
u8 i=0;
u8 key;
u8 k=0;
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
LED_Init();
USART1_Init(9600);
KEY_Init();
AT24CXX_Init();
while(AT24CXX_Check())
{
printf("AT24C02检测不正常\r\n");
delay_ms(500);
}
printf("AT24C02检测正常\r\n");
while(1)
{
key=KEY_Scan(0);
if(key==KEY_UP)
{
k++;
if(k>255)
{
k=255;
}
AT24CXX_WriteOneByte(0,k);
printf("写入的数据为:%d\r\n",k);
}
if(key==KEY_DOWN)
{
k=AT24CXX_ReadOneByte(0);
printf("读取的数据为:%d\r\n",k);
}
i++;
if(i%20==0)
{
led1=!led1;
}
delay_ms(10);
}
}
CAN(Controller Area Network),控制器局域网络,是ISO国际标准化串行通信协议。为了减少线束的数量、通过多个LAN进行大量数据的高速通信的实际需要,1986年德国电气商博世公司开发了面向汽车的CAN通信协议。CAN是国际上应用最广泛的现场总线之一,为分布式系统实现各节点之间实时、可靠的数据通信提供了有利的技术支持。
CAN通信只需要两个信号线,CAN_H和CAN_L,CAN控制器根据两根信号线上的电位差来判断总线电平。总线电平被分为显性电平和隐性电平,发送方通过使总线电平发生变化,从而将信息发送给接收方。
CAN通信具备以下特点:
1、 多主控制;
总线空闲时,所有单元均可发生消息,当两个以上的单元同时发生消息时,依据标识符(ID)决定优先级。ID并不是表示发送的目的地址,而是表示访问总线消息的优先级。两个以上的单元开始发送消息时,对消息ID的每个位进行逐个仲裁比较。仲裁获胜的单元继续发送消息,仲裁失败的单元停止发送进行接收工作。
2、 系统柔软性;
与总线相连的单元没有类似“地址”的信息,因此在总线上增加单元时,总线上的其它单元软硬件及应用层不需要被改变。
3、 通信速度快、距离远;
最高1Mbps(距离小于40m),最远10km(速率低于5Kbps)。
4、 错误检测、错误通知和错误恢复功能;
总线上的所有单元都可检测错误,出现错误的单元会立即通知其它单元,正在发送消息的单元检测出错误后,会强制结束当前的发送。强制结束发送的单元会不断重新发送此消息直到成功发送为止。
5、 故障封闭功能;
可以判断出错的类型是总线上暂时的数据错误(外部噪声)或持续的数据错误(单元内部故障、驱动器故障、断线等)。当总线持续数据错误时,可将引起此故障的单元从总线上隔离。
6、 可连接节点多;
可以同时连接多个单元设备,连接总数理论上无限制,但实际上可连接的单元数受到总线时延和电气负载的限制。降低通信速度,可连接的单元数增加;提高通信速度,可连接单元数减少。
对于CAN通信的两种标准:ISO11898(高速)可以组建一个高速、短距离的闭环网络,该标准下总线最大长度40m,通信速率最高1Mbps,总线两端需各有一个120欧姆的电阻作为阻抗匹配功能,以减少回波反射。ISO11519-2(低速)可以组建一个低速、远距离的开环网络,该标准下总线最大长度1km,通信速率最高125kbps,两根总线独立不形成闭环,两根总线上各自串联一个2.2千欧的电阻。
在IS01898标准下,显性电平对应逻辑0,CAN_H和CAN_L压差为2.5V左右;隐性电平对应逻辑1,CAN_H和CAN_L压差为0V。在总线上显性电平具有优先权,只要有一个单元输出显性电平,总线上即为显性电平。隐性电平具有包容性,只有所有的单元都输出隐性电平,总线上才为隐性电平。
CAN总线和CAN控制器之间需要一个CAN收发器将信号进行转换,通信主要通过5种类型的帧进行,分别为数据帧、遥控帧、错误帧、过载帧以及帧间隔。其中数据帧由7个段构成,分别为帧起始(数据帧的开始段)、仲裁段(帧优先级的段)、控制段(数据的字节数及保留位的段)、数据段(数据内容,0~8字节数据)、CRC段(检查帧错误的段)、ACK段(确认正常接收段)以及帧结束段(数据帧结束的段)。
在STM32F1芯片中自带bxCAN控制器(Basic Extended CAN),基本扩展CAN,支持2.0A和B版本的CAN协议。目的是以最小的CPU负载,高效的管理大量传入消息,并按需要的优先级实现消息发送。
使用库函数对CAN进行配置需要使用到库文件stm32f10x_can.h和stm32f10x_can.c,详细的步骤如下:
1、 使能CAN时钟,将对应的引脚复用映射为CAN功能;
CAN1和CAN2都是挂载在APB1总线上的设备,其发送和接收引脚对应不同的IO口,因此在使能时钟后,还需要使能对应IO口的时钟,并将其设置为复用功能。配置代码如下:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, EANBLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, EANBLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入模式
GPIO_Init(GPIOA, & GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speede = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, & GPIO_InitStructure);
2、 设置CAN工作模式、波特率等;
调用函数:uint8_t CAN_Init(CAN_TypeDef* CANx, CAN_InitTypeDef* CAN_InitStruct);
参数CAN_InitStruct为结构体指针变量,其成员变量包含CAN工作模式以及波特率初始化的变量:
typedef struct
{
uint16_t CAN_prescaler; //设置CAN外设时钟分频
uint8_t CAN_Mode; //正常模式(CAN_Mode_Normal)、回环模式(CAN_Mode_LoopBack)、静默模式(CAN_Mode_Silent)以及回环静默模式(CAN_Mode_Silent_LoopBack)
uint8_t CAN_SJW; //设置CAN重新同步时单次可增加或缩短的最大长度,可配置为1-4tp(CAN_SJW_1/2/3/4tp)
uint8_t CAN_BS1; //设置位时序BS1长度,CAN_BS1_1/2/3/…/16tp
uint8_t CAN_BS2; //设置位时序BS2长度,CAN_BS1_1/2/3/…/8tp
FunctionalState CAN_TTCM; //是否使用时间触发功能
FunctionalState CAN_ABOM; //是否使用自动离线管理
FunctionalState CAN_AWUM; //是否使用自动唤醒功能
FunctionalState CAN_NART; //是否使用自动重传功能
FunctionalState CAN_RFLM; //是否使用锁定接收FIFO
FunctionalState CAN_TXFP; //设置发送报文的优先级判断方法,使能时以报文存入发送邮箱的先后顺序发送,失能时按照报文ID优先级发送
}
CAN的波特率 = Fpclk1 / ((CAN_BS1+CAN_BS2+1)*CAN_Prescaler)
3、 设置CAN筛选器(过滤器);
调用函数:void CAN_FilterInit(CAN_FilterInitTypeDef* CAN_FilterInitStruct);
其中结构体CAN_FilterInitTypeDef的成员变量为:
typeder struct
{
uint16_t CAN_FilterIdHigh; //存储需要筛选的ID高16位
uint16_t CAN_FilterIdLow; //存储需要筛选的ID低16位
uint16_t CAN_FilterMaskIdHigh; //存储需要筛选ID或掩码
uint16_t CAN_FilterMaskIdLow; //存储需要筛选ID或掩码
uint16_t CAN_FilterFIFOAssignment; //设置报文被存储到那一个接收FIFO,FIFO
0或FIFO1
uint8_t CAN_FilterNumber; //设置筛选器编号,0~27
uint8_t CAN_FilterMode; //设置筛选器工作模式,列表模式(CAN_FilterMode_IdList)和掩码模式(CAN_FilterMode_IdMask)
uint8_t CAN_FilterScale; //设置筛选器位宽,CAN_FilterScale_32bit及CAN_FilterScale_64bit
}
4、 选择CAN中断类型,开启中断;
调用函数:void CAN_ITConfig(CAN_TypeDef* CANx, uint32_t CAN_IT, FunctionalState NewState);
5、 CAN发送和接收消息;
发送消息函数:uint8_t CAN_Transmit(CAN_TypeDef* CANx, CanTxMsg* TxMessage);
其中结构体变量CanTxMsg包含的成员变量为:
typedef struct
{
uint32_t StdId; //报文11位标准标识符,0~0x7FF
uint32_t ExtId; //报文29位扩展标识符,0~0x1FFFFFFF
uint8_t IDE; //扩展标志位,CAN_ID_STD(标准帧)和CAN_ID_EXT(扩展帧)
uint8_t RTR; //报文类型标志位,CAN_RTR_Data(数据帧)和CAN_RTR_Remote(遥控帧)
uint8_t DLC; //数据帧的长度,0~8,当报文是遥控帧是DLC=0
uint8_t Data[8]; //数据帧中数据段的数据
}
接收消息函数:void CAN_Receive(CAN_TypeDef* CANx, uint8_t FIFONumber, CanRxMsg* RxMessage);
其中结构体变量CanRxMsg包含的成员变量为:
typedef struct
{
uint32_t StdId; //报文11位标准标识符,0~0x7FF
uint32_t ExtId; //报文29位扩展标识符,0~0x1FFFFFFF
uint8_t IDE; //扩展标志位,CAN_ID_STD(标准帧)和CAN_ID_EXT(扩展帧)
uint8_t RTR; //报文类型标志位,CAN_RTR_Data(数据帧)和CAN_RTR_Remote(遥控帧)
uint8_t DLC; //数据帧的长度,0~8,当报文是遥控帧是DLC=0
uint8_t Data[8]; //数据帧中数据段的数据
uint8_t FMI; //存储筛选器编号,表示经过哪一个筛选器存储进接收FIFO的
}
6、 CAN状态的获取
调用函数:CAN_TransmitStatus(); CAN_MessagePending(); CAN_GetFlagStatus();
STM32使用按键切换CAN1的通信模式(数据发送、接收),将数据通过串口打印出,详细的代码模块如下:
can.h
#ifndef _can_H
#define _can_H
#include "system.h"
#define CAN_RX0_INT_ENABLE 0 //使能中断
void CAN_Mode_Init(u8 tsjw,u8 tbs2,u8 tbs1,u16 brp,u8 mode); //CAN初始化
u8 CAN_Send_Msg(u8* msg,u8 len); //发送数据
u8 CAN_Receive_Msg(u8 *buf); //接收数据
#endif
can.c
#include "can.h"
#include "usart.h"
void CAN_Mode_Init(u8 tsjw,u8 tbs2,u8 tbs1,u16 brp,u8 mode)
{
GPIO_InitTypeDef GPIO_InitStructure;
CAN_InitTypeDef CAN_InitStructure;
CAN_FilterInitTypeDef CAN_FilterInitStructure;
#if CAN_RX0_INT_ENABLE
NVIC_InitTypeDef NVIC_InitStructure;
#endif
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
CAN_InitStructure.CAN_TTCM=DISABLE;
CAN_InitStructure.CAN_ABOM=DISABLE;
CAN_InitStructure.CAN_AWUM=DISABLE;
CAN_InitStructure.CAN_NART=ENABLE;
CAN_InitStructure.CAN_RFLM=DISABLE;
CAN_InitStructure.CAN_TXFP=DISABLE;
CAN_InitStructure.CAN_Mode= mode;
CAN_InitStructure.CAN_SJW=tsjw;
CAN_InitStructure.CAN_BS1=tbs1;
CAN_InitStructure.CAN_BS2=tbs2;
CAN_InitStructure.CAN_Prescaler=brp;
CAN_Init(CAN1, &CAN_InitStructure);
//ÅäÖùýÂËÆ÷
CAN_FilterInitStructure.CAN_FilterNumber=0;
CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit;
CAN_FilterInitStructure.CAN_FilterIdHigh=0x0000;
CAN_FilterInitStructure.CAN_FilterIdLow=0x0000;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh=0x0000;
CAN_FilterInitStructure.CAN_FilterMaskIdLow=0x0000;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation=ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
#if CAN_RX0_INT_ENABLE
CAN_ITConfig(CAN1,CAN_IT_FMP0,ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
#endif
}
#if CAN_RX0_INT_ENABLE
void USB_LP_CAN1_RX0_IRQHandler(void)
{
CanRxMsg RxMessage;
int i=0;
CAN_Receive(CAN1, 0, &RxMessage);
for(i=0;i<8;i++)
printf("rxbuf[%d]:%d\r\n",i,RxMessage.Data[i]);
}
#endif
u8 CAN_Send_Msg(u8* msg,u8 len)
{
u8 mbox;
u16 i=0;
CanTxMsg TxMessage;
TxMessage.StdId=0x12;
TxMessage.ExtId=0x12;
TxMessage.IDE=0;
TxMessage.RTR=0;
TxMessage.DLC=len;
for(i=0;i<len;i++)
TxMessage.Data[i]=msg[i];
mbox= CAN_Transmit(CAN1, &TxMessage);
i=0;
while((CAN_TransmitStatus(CAN1, mbox)==CAN_TxStatus_Failed)&&(i<0XFFF)) i++;
if(i>=0XFFF) return 1;
return 0;
}
u8 CAN_Receive_Msg(u8 *buf)
{
u32 i;
CanRxMsg RxMessage;
if( CAN_MessagePending(CAN1,CAN_FIFO0)==0)return 0;
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
for(i=0;i<RxMessage.DLC;i++)
buf[i]=RxMessage.Data[i];
return RxMessage.DLC;
}
main.c
#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "usart.h"
#include "key.h"
#include "can.h"
int main()
{
u8 i=0,j=0;
u8 key;
u8 mode=0;
u8 res;
u8 tbuf[8],char_buf[8];
u8 rbuf[8];
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
LED_Init();
USART1_Init(9600);
KEY_Init();
CAN_Mode_Init(CAN_SJW_1tq,CAN_BS2_8tq,CAN_BS1_9tq,4,CAN_Mode_Normal);/
while(1)
{
key=KEY_Scan(0);
if(key==KEY_UP)
{
mode=!mode;
CAN_Mode_Init(CAN_SJW_1tq,CAN_BS2_8tq,CAN_BS1_9tq,4,mode);
if(mode==0)
{
printf("Normal Mode\r\n");
}
else
{
printf("LoopBack Mode\r\n");
}
}
if(key==KEY_DOWN)
{
for(j=0;j<8;j++)
{
tbuf[j]=j;
char_buf[j]=tbuf[j]+0x30;
}
res=CAN_Send_Msg(tbuf,8);
if(res)
{
printf("Send Failed!\r\n");
}
else
{
printf("发送数据:%s\r\n",char_buf);
}
}
res=CAN_Receive_Msg(rbuf);
if(res)
{
for(j=0;j<res;j++)
{
char_buf[j]=rbuf[j]+0x30;
}
printf("接收数据:%s\r\n",char_buf);
}
i++;
if (i%20==0)
{
led1=!led1;
}
delay_ms(10);
}
}