USART 、SPI、IIC、常用的通信方式详解
1.我们首先呢,我们谈谈要通信几大要点
1.1串行和并行
串行通信就是设备之间通过少量的线,进行一位一位的数据传输
并行通信就是使用多根数据线同时进行数据传输
1.2 全双工、半双工、单工
全双工:在同一时刻,两个设备之间可以同时收发数据
半双工:两个设备之间可以收发数据,但不能在同一时刻进行
单工:在任何时候都只能进行同一个方向的通信,即一个固定位发送设备,另一固定位接收设备
1.3同步和异步
同步:以时钟线来传输数据的,就是说,发送和接收的时序相同(iic、SPI)
异步:没有时钟线,以数据帧格式来传输数据的(RS485、串口)
1.4 传输速度
这个我认为比较重要的,首先,我们我们提到传输速度,就会想到比特率(Bit/s)和波特率,比特率是从单位看就可以知道是指每秒传输二进制的位数,而波特率是指每秒传输的码元;这里的码元我们给具体化,大家都知道串口,他的数据以帧格式传输的,即一个起始位+8位数据位+1位奇偶位+1位结束位,那么这里时候,这11位加在一起就是一个码元,就是说多位算一个单位。
2 串口通讯
在我看来,通讯都分为物理接口和软件时序(物理层和协议层),掌握这两,就可以基本掌握一种通讯方式,物理层主要还是电压特性,数据位高低的电压;协议层主要就是数据传输的规则
2.1 物理层
串口通信的物理层有很多标准,我们一般有两种电平标准,TTL标准和RS485标准
这里呢,我画个简易图吧,来表示电平的转换,其中设备时STM32,设备B是内一个STM32
USB转串口
当然,这里也可以用CH340
电平标准
2.2 协议层
2.2.1串口的数据传输的帧格式
1位起始位+6/7/8位数据位+1位奇偶位+1位结束位(可以不要校验位)
也就是说数据就按照这种格式发送出去的,这样比直接发送数据位更加的准确和安全
但是在两个设备进行串口通信的时候,要注意的就是他们的波特率要相同,因为他是异步的没有时钟,所以两个设备发送接收之前必须约定好波特率
3 USART
3.1 USART的简单介绍
(1)串口是串行异步全双工的以这种通信方式
(2)串口的数据传输是通过两根数据线传输,即RX、TX,发送和接收
(3)USART 支持使用 DMA
(4)我们在用USART最多的还是printf的作用,即用来上位机的调试
3.2 USART的功能框图
3.3.2 ①功能引脚
我们能看到的就是外部引脚
TX:发送数据输出引脚。
RX:接收数据输入引脚。
STM32F103VET6 系统控制器有三个 USART 和两个 UART,其中 USART1 和时钟来源于 APB2 总线时钟,其最大频率为 72MHz,其他四个的时钟来源于 APB1 总线时钟,其最大频率为 36MHz。
3.3.3 ②数据寄存器
(1)数据字长
USART数据寄存器USART_DR只有低9位有效,其他位数保留,并且第 9 位数据是否有效要取决于USART 控制寄存器 1(USART_CR1)的 M 位设置,当 M 位为 0 时表示 8 位数据字长,当 M位为 1 表示 9 位数据字长,我们一般使用 8 位数据字长。
- 接收和发送数据都是存在USART_DR
USART_DR 包含了已发送的数据或者接收到的数据。 USART_DR 实际是包含了两个寄存器,一个专门用于发送的可写 TDR,一个专门用于接收的可读 RDR。当进行发送操作时,往 USART_DR 写入数据会自动存储在 TDR 内;当进行读取操作时,向 USART_DR读取数据会自动提取 RDR 数据。
3.3.3 ③控制器
(1)发送器
名称 描述
TE 发送使能
TXE 发送寄存器为空,发送单个字节的时候使用
TC 发送完成,发送多个字节数据的时候使用
TXIE 发送完成中断使能
- 接收器
名称 描述
RE 接收使能
RXNE 读数据寄存器非空
RXNEIE 发送完成中断使能
3.3.4 ④小数波特率生成
我们在设置波特率的时候,其实也就是计算出USARTDIV然后把它放到波特率寄存器中(BRR),这个寄存器一共是32位,其中高16位保留了,低16位中,最低4位是保存小数的,其他12位是保存整数部分。那么我们举个例子,我们要设置115200,
解 得 USARTDIV=39.0625 , 可 算 得 DIV_Fraction=0.0625*16=1=0x01 ,
DIV_Mantissa=39=0x17,即应该设置 USART_BRR 的值为 0x171。
float usartdiv=72000000/(brr*16);
mantissa=(u32)usartdiv;
fraction=(usartdiv-mantissa)*16+0.5f;
USART1->BRR =mantissa<<4|fraction;
3.3.5 校验控制
STM32F103 系列控制器 USART 支持奇偶校验。当使用校验位时,串口传输的长度将是 8 位的数据帧加上 1 位的校验位总共 9 位,此时 USART_CR1 寄存器的 M 位需要设置为1,即 9 数据位。将 USART_CR1 寄存器的 PCE 位置 1 就可以启动奇偶校验控制,奇偶校验由硬件自动完成。启动了奇偶校验控制之后,在发送数据帧时会自动添加校验位,接收数据时自动验证校验位。接收数据时如果出现奇偶校验位验证失败,会见 USART_SR 寄存器的 PE 位置 1,并可以产生奇偶校验中断。
3.3.6 中断控制
3.4 库函数结构体分析
typedef struct {
2 uint32_t USART_BaudRate; // 波特率
3 uint16_t USART_WordLength; // 字长
4 uint16_t USART_StopBits; // 停止位
5 uint16_t USART_Parity; // 校验位
6 uint16_t USART_Mode; // USART 模式
7 uint16_t USART_HardwareFlowControl; // 硬件流控制
8 } USART_InitTypeDef;
这里就不多说了,大家自己跳进去就知道选择什么了
3.5 编程(F4)
使用库函数操作
首先,配置NVIC使用NVIC_PriorityGroupConfig()设置优先级分组,使用NVIC_Init()对NVIC进行初始化
- void NVIC_Config()
- {
- NVIC_InitTypeDef NVIC_InitStructure;
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x03;
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
- NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
- NVIC_Init(&NVIC_InitStructure);
- }
第二步:配置引脚功能,因为我的板子上PA9被用来驱动LED了,所以只能将将串口映射到PB6,PB7。这个设置跟F1系列的有点差别。首先初始化端口时钟,第二配置端口引脚功能,第三不设置功能映射将串口连接到引脚。
- void USART_Gpio_Config(void)
- {
- GPIO_InitTypeDef GPIO_InitStructure;
- RCC_AHB1PeriphClockCmd( RCC_AHB1Periph_GPIOB , ENABLE);
- //PB6->TX PB7->Rx
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init(GPIOB, &GPIO_InitStructure);
- GPIO_PinAFConfig(GPIOB,GPIO_PinSource6,GPIO_AF_USART1);
- GPIO_PinAFConfig(GPIOB,GPIO_PinSource7,GPIO_AF_USART1);
- }
第三步:配置串口工作方式。步骤也差不多:打开时钟,用Init函数初始化串口,设置串口的接收中断,最后别忘了使能串口。
- void USART_Config(void)
- {
- USART_Gpio_Config();
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
- USART_InitStructure.USART_BaudRate = 115200;
- 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_ITConfig(USART1,USART_IT_RXNE,ENABLE);
- USART_Cmd(USART1,ENABLE);
- }
第四步:添加串口中断函数,函数名是固定的:void USART1_IRQHandler(void)。中断进入时候,先判断接收寄存器是否有数据,有数据时候就接收,然后使用USART_SendData()将数据发回
- void USART1_IRQHandler(void)
- {
- char c;
- if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
- {
- c = USART_ReceiveData(USART1);
- USART_SendData(USART1,c);
- }
- //while(1);
- }
最后就是main了,没啥可说的
- int main(void)
- {
- NVIC_Config();
- USART_Config();
- while(1)
- {
- while(RESET == USART_GetFlagStatus(USART1,USART_FLAG_TXE));
- USART_SendData(USART1,'b');
- while(RESET == USART_GetFlagStatus(USART1,USART_FLAG_TXE));
- USART_SendData(USART1,'a');
- delay_ms(1000);
- }
- }
- 我们在来谈谈SPI吧
学习SPI最快的方式就是先先搞清外设的各个引脚即物理结构,在就是搞清通信协议即时序。但是这里我们稍微扎实点,我们得先研究SPI的架构。
-
- 简介:
SPI是高速串行全双工同步通信;
他的优势在于速度块、能够实现一对多的通信
-
- 物理接口
一个4个引脚;
SS(CS):从设备选择(如果多个从设备的话,也就是靠这个引脚来选择的)
CLK:时钟信号线
SDI(MISO):主机输入,从机输出
SDO(MOSI):主机输出,从机输入
-
- 时序
SPI 工作方式:
SPI0:CPOL=0;CPHA=0;
空闲状态低电平, 上升沿数据被采样, 下降沿数据输出
SPI1:CPOL=0;CPHA=1;
空闲状态低电平, 下降沿数据被采样, 上升沿数据输出
SPI2:CPOL=1;CPHA=0;
空闲状态高电平, 下降沿数据被采样, 上升沿数据输出
SPI3:CPOL=1;CPHA=1;
空闲状态高电平, 上升沿数据被采样, 下降沿数据输出
SPI0 和 SPI3 时序相同的
SPI1 和 SPI2 时序相同的
工作方式的选择: 取决于从设备
在时钟的上升沿开始1位数据的采集,在下降沿结束1位数据的采集,但是这里有个另外,就是对于开始的第一位数据的开始,在第一个上升沿来临之前就开始采集。
这里对于主机来说,发送和接收数据时同步的,在同一个时钟下
-
- 库函数结构体讲解
1 typedef struct
2 {
3 uint16_t SPI_Direction; /*设置 SPI 的单双向模式 */
4 uint16_t SPI_Mode; /*设置 SPI 的主/从机端模式 */
5 uint16_t SPI_DataSize; /*设置 SPI 的数据帧长度,可选 8/16 位 */
6 uint16_t SPI_CPOL; /*设置时钟极性 CPOL,可选高/低电平*/
7 uint16_t SPI_CPHA; /*设置时钟相位,可选奇/偶数边沿采样 */
8 uint16_t SPI_NSS; /*设置 NSS 引脚由 SPI 硬件控制还是软件控制*/
9 uint16_t SPI_BaudRatePrescaler; /*设置时钟分频因子, fpclk/分频数=fSCK */
10 uint16_t SPI_FirstBit; /*设置 MSB/LSB 先行 */
11 uint16_t SPI_CRCPolynomial; /*设置 CRC 校验的表达式 */
12 } SPI_InitTypeDef;
SPI_Direction :本成员设置 SPI 的通讯方向,可设置为双线全双工(SPI_Direction_2Lines_FullDuplex),双线只接收(SPI_Direction_2Lines_RxOnly),单线只接收(SPI_Direction_1Line_Rx)、单线只发送模式(SPI_Direction_1Line_Tx)。 这里我们一般设置位双线全双工
SPI_Mode :本成员设置 SPI 工作在主机模式(SPI_Mode_Master)或从机模式(SPI_Mode_Slave ),这两个模式的最大区别为 SPI 的 SCK 信号线的时序, SCK 的时序是由通讯中的主机产生的。若被配置为从机模式, STM32 的 SPI 外设将接受外来的 SCK 信号。
SPI_DataSize :本成员可以选择 SPI 通讯的数据帧大小是为 8 位 (SPI_DataSize_8b)还是 16 位
SPI_CPOL 和 SPI_CPHA :时钟极性 CPOL 成员,可设置为高电平(SPI_CPOL_High)或低电平(SPI_CPOL_Low )。
时钟相位 CPHA 则可以设置为 SPI_CPHA_1Edge(在 SCK 的奇数边沿采集数据) 或SPI_CPHA_2Edge (在 SCK 的偶数边沿采集数据) 。
SPI_NSS :本成员配置 NSS 引脚的使用模式,可以选择为硬件模式(SPI_NSS_Hard )与软件模式
(SPI_NSS_Soft ),在硬件模式中的 SPI 片选信号由 SPI 硬件自动产生,而软件模式则需要我们亲自把相应的 GPIO 端口拉高或置低产生非片选和片选信号。实际中软件模式应用比较多。
SPI_BaudRatePrescaler :本成员设置波特率分频因子,分频后的时钟即为 SPI 的 SCK 信号线的时钟频率。这个成员参数可设置为 fpclk 的 2、 4、 6、 8、 16、 32、 64、 128、 256 分频。
SPI_FirstBit :所有串行的通讯协议都会有 MSB 先行(高位数据在前)还是 LSB 先行(低位数据在前)的问题,而 STM32 的 SPI 模块可以通过这个结构体成员,对这个特性编程控制。
SPI_CRCPolynomial :这是 SPI 的 CRC 校验中的多项式,若我们使用 CRC 校验时,就使用这个成员的参数(多项式),来计算 CRC 的值
-
- 编程
SPI是一种通信方式,主要就是读写数据的,我们我们需要先配置好SPI模式,然后在整个单字节的读写函数,然后,这个基本工作做过后后面就是看从设备自己时序了(也就是自己的规则)
void spi_Config(void)
{
GPIO_InitTypeDef GPIOInitStructures;
SPI_InitTypeDef SPIInitStructures;
//打开时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_SPI1,ENABLE);
//配置时钟--PA5 MOSI--PA7
GPIOInitStructures.GPIO_Pin=GPIO_Pin_5|GPIO_Pin_7;
GPIOInitStructures.GPIO_Mode=GPIO_Mode_AF_PP;
GPIOInitStructures.GPIO_Speed=GPIO_Speed_10MHz;
GPIO_Init(GPIOA,&GPIOInitStructures);
//配置miso--pa6
GPIOInitStructures.GPIO_Pin=GPIO_Pin_6;
GPIOInitStructures.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA,&GPIOInitStructures);
//配置片选 PB0
GPIOInitStructures.GPIO_Pin=GPIO_Pin_0;
GPIOInitStructures.GPIO_Mode=GPIO_Mode_Out_PP;
GPIOInitStructures.GPIO_Speed=GPIO_Speed_10MHz;
GPIO_Init(GPIOB,&GPIOInitStructures);
SPI1_CS(1);
//配置SPI1
SPIInitStructures.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_128;
SPIInitStructures.SPI_CPHA=SPI_CPHA_2Edge;
SPIInitStructures.SPI_CPOL=SPI_CPOL_High;
SPIInitStructures.SPI_CRCPolynomial=7;
SPIInitStructures.SPI_DataSize=SPI_DataSize_8b;
SPIInitStructures.SPI_Direction=SPI_Direction_2Lines_FullDuplex;
SPIInitStructures.SPI_FirstBit=SPI_FirstBit_MSB;
SPIInitStructures.SPI_Mode=SPI_Mode_Master;
SPIInitStructures.SPI_NSS=SPI_NSS_Soft;
SPI_Init(SPI1,&SPIInitStructures);
SPI_Cmd(SPI1,ENABLE);
}
/**************************************
函数名:
函数功能:单字节读写函数
备注:
日期:
**************************************/
u8 spi_ReadorWrite(u8 data)
{
while(!SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE));//等待发送缓存区为空
SPI_I2S_SendData(SPI1,data);
while(!SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE));
return SPI_I2S_ReceiveData(SPI1);
}
- 我们来简单的谈谈IIC
同样还是从两个方面来描述,物理层和协议层
-
- 简介:
他的有点就是引脚少,占用IO资源少,硬件实现也简单不少
串行半双工同步通信
-
- 物理层
- 物理层
- 他是总线,即它可以接入多个从设备
- 它只有两根线,即时钟线(SCL),数据线(SDA),但是它同一时间只有做发送或者接收线
- 每个连接到总线的设备,都要支持IIC,即有自己的设备地址,主机就是靠这个来区别不同的设备的
- 总线通过上拉电阻接到电源,即当空闲时,是高电平
- 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
- 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式
- 协议层
- 协议层
这个就是IIC的通信协议,我们分开来解析下
(1)首先,起始信号:L 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。 起始信号开始后,所以的设备都在那个等待
- 然后,主机发送7位的地址,来选择从机。
- 然后,会有一位的读写位来控制是读数据还是写数据
- 在然后呢,会有一位的响应信号,然后主机会等待从机的响应,也就是从机把电平拉低才会开始接收数据,然后开始读取数据,并产生个应答或者非应答信号,最后来个结束信号
- 模拟IIC
下面是模拟的IIC配置,改改SCL和SDL引脚就可以用啦
#include "iic.h"
//PB8 SCL PB9 SDA
void IIC_Init(void)
{
RCC->AHB1ENR|=1<<1; //使能PORTB时钟
//PB8输出 PB9推挽
GPIOB->MODER &=~ (0xf<<16);
GPIOB->MODER |= (0x5<<16);//通用
GPIOB->OTYPER &=~ (3<<8);//推挽
GPIOB->OSPEEDR &=~ (0xf<<16);
GPIOB->OSPEEDR |= (0xf<<16);//高速
GPIOB->PUPDR &=~ (0xf<<16);
GPIOB->PUPDR |= (0x5<<16);//上拉
IIC_SCL(1);
IIC_SDA(1);
}
//产生IIC起始信号
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
IIC_SDA(1);
IIC_SCL(1);
Delay_us(4);
IIC_SDA(0);//时钟线高,数据线拉低
Delay_us(4);
IIC_SCL(0);//钳住I2C总线,准备发送或接收数据
}
//产生IIC停止信号
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SCL(0);
IIC_SDA(0);//时钟线高,数据线拉高
Delay_us(4);
IIC_SCL(1);
IIC_SDA(1);//发送I2C总线结束信号
Delay_us(4);
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA设置为输入
IIC_SDA(1);Delay_us(1);
IIC_SCL(1);Delay_us(1);
while(READ_SDA())
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL(0);//时钟输出0
return 0;
}
//主机产生ACK应答
void IIC_Ack(void)
{
IIC_SCL(0);
SDA_OUT();
IIC_SDA(0);
Delay_us(2);
IIC_SCL(1);
Delay_us(2);
IIC_SCL(0);
}
//主机不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL(0);
SDA_OUT();
IIC_SDA(1);
Delay_us(2);
IIC_SCL(1);
Delay_us(2);
IIC_SCL(0);
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL(0);//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
IIC_SDA((txd&0x80)>>7); //先发高位
txd<<=1;
Delay_us(2); //对TEA5767这三个延时都是必须的
IIC_SCL(1);
Delay_us(2);
IIC_SCL(0);
Delay_us(2);
}
}
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
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_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}