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 位数据字长。

  1. 接收和发送数据都是存在USART_DR

USART_DR 包含了已发送的数据或者接收到的数据。 USART_DR 实际是包含了两个寄存器,一个专门用于发送的可写 TDR,一个专门用于接收的可读 RDR。当进行发送操作时,往 USART_DR 写入数据会自动存储在 TDR 内;当进行读取操作时,向 USART_DR读取数据会自动提取 RDR 数据。

3.3.3 ③控制器

(1)发送器

名称 描述
TE 发送使能
TXE 发送寄存器为空,发送单个字节的时候使用
TC 发送完成,发送多个字节数据的时候使用
TXIE 发送完成中断使能 

  1. 接收器

名称 描述
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进行初始化

  1. void NVIC_Config()
  2. {
  3.     NVIC_InitTypeDef NVIC_InitStructure;
  4.     NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
  5.     
  6.    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x03;
  7.     NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
  8.     NVIC_InitStructure.NVIC_IRQChannel  = USART1_IRQn;
  9.     NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  10.         
  11.     NVIC_Init(&NVIC_InitStructure);
  12. }

第二步:配置引脚功能,因为我的板子上PA9被用来驱动LED了,所以只能将将串口映射到PB6,PB7。这个设置跟F1系列的有点差别。首先初始化端口时钟,第二配置端口引脚功能,第三不设置功能映射将串口连接到引脚。

  1. void USART_Gpio_Config(void)
  2. {
  3.     GPIO_InitTypeDef GPIO_InitStructure;
  4.     
  5.     RCC_AHB1PeriphClockCmd( RCC_AHB1Periph_GPIOB  , ENABLE);
  6.     
  7.     //PB6->TX  PB7->Rx
  8.     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
  9.     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  10.     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  11.     GPIO_Init(GPIOB, &GPIO_InitStructure);
  12.     
  13.     GPIO_PinAFConfig(GPIOB,GPIO_PinSource6,GPIO_AF_USART1);
  14.     GPIO_PinAFConfig(GPIOB,GPIO_PinSource7,GPIO_AF_USART1);
  15. }

第三步:配置串口工作方式。步骤也差不多:打开时钟,用Init函数初始化串口,设置串口的接收中断,最后别忘了使能串口。

  1. void USART_Config(void)
  2. {
  3.         USART_Gpio_Config();
  4.     RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
  5.     
  6.     USART_InitStructure.USART_BaudRate = 115200;
  7.     USART_InitStructure.USART_WordLength = USART_WordLength_8b;
  8.     USART_InitStructure.USART_StopBits = USART_StopBits_1;
  9.     USART_InitStructure.USART_Parity = USART_Parity_No;
  10.     USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  11.     USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  12.     
  13.     USART_Init(USART1,&USART_InitStructure);   
  14.     USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
  15.     
  16.     USART_Cmd(USART1,ENABLE);
  17.     
  18. }

第四步:添加串口中断函数,函数名是固定的:void USART1_IRQHandler(void)。中断进入时候,先判断接收寄存器是否有数据,有数据时候就接收,然后使用USART_SendData()将数据发回

  1. void USART1_IRQHandler(void)
  2. {
  3.     char c;
  4.     if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
  5.     {
  6.         c = USART_ReceiveData(USART1);
  7.         USART_SendData(USART1,c);
  8.     }
  9.         //while(1);
  10. }

最后就是main了,没啥可说的

  1. int main(void)
  2. {
  3.     NVIC_Config();
  4.     USART_Config();
  5.     while(1)
  6.     {
  7.         while(RESET == USART_GetFlagStatus(USART1,USART_FLAG_TXE));
  8.         USART_SendData(USART1,'b');
  9.         while(RESET == USART_GetFlagStatus(USART1,USART_FLAG_TXE));
  10.         USART_SendData(USART1,'a');
  11.         delay_ms(1000);
  12.     }
  1. 我们在来谈谈SPI吧

学习SPI最快的方式就是先先搞清外设的各个引脚即物理结构,在就是搞清通信协议即时序。但是这里我们稍微扎实点,我们得先研究SPI的架构。

    1. 简介:

SPI是高速串行全双工同步通信;

他的优势在于速度块、能够实现一对多的通信

    1. 物理接口

一个4个引脚;

SS(CS):从设备选择(如果多个从设备的话,也就是靠这个引脚来选择的)

CLK:时钟信号线

SDI(MISO):主机输入,从机输出

SDO(MOSI):主机输出,从机输入

    1. 时序

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. 库函数结构体讲解

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 2468163264128256 分频。

SPI_FirstBit :所有串行的通讯协议都会有 MSB 先行(高位数据在前)还是 LSB 先行(低位数据在前)的问题,而 STM32 SPI 模块可以通过这个结构体成员,对这个特性编程控制。 

SPI_CRCPolynomial :这是 SPI CRC 校验中的多项式,若我们使用 CRC 校验时,就使用这个成员的参数(多项式),来计算 CRC 的值   

    1. 编程

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);

}

  1. 我们来简单的谈谈IIC

同样还是从两个方面来描述,物理层和协议层

    1. 简介:

他的有点就是引脚少,占用IO资源少,硬件实现也简单不少

串行半双工同步通信

    1. 物理层

 

  1. 他是总线,即它可以接入多个从设备
  2. 它只有两根线,即时钟线(SCL),数据线(SDA),但是它同一时间只有做发送或者接收线
  3. 每个连接到总线的设备,都要支持IIC,即有自己的设备地址,主机就是靠这个来区别不同的设备的
  4. 总线通过上拉电阻接到电源,即当空闲时,是高电平
  5. 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。 
  6. 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式 
    1. 协议层

 

这个就是IIC的通信协议,我们分开来解析下

(1)首先,起始信号:L 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。 起始信号开始后,所以的设备都在那个等待

 

 

  1. 然后,主机发送7位的地址,来选择从机。
  2. 然后,会有一位的读写位来控制是读数据还是写数据
  3. 在然后呢,会有一位的响应信号,然后主机会等待从机的响应,也就是从机把电平拉低才会开始接收数据,然后开始读取数据,并产生个应答或者非应答信号,最后来个结束信号
    1. 模拟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;

}