目录
基本理论知识:
1、并行通信/串行通信
2、异步通信/同步通信
3、半双工通信/全双工通信
并行通信/串行通信:
并行通信:传输速度快,一次传输8bit,但是通信成本高,需要8个独立的通道,另外不支持长距离传输。用于打印机和扫描仪等设备,例如DB-25接口。
串行通信:传输速度慢,成本低,支持长距离传输,是计算机通信的主要方式,例如DB-接口。
异步通信/同步通信:
异步通信:用于低速设备,会有更高的误码率。
同步通信:用于高速设备传输,同步传输有同步时钟为节拍进行传输数据。
半双工通信/全双工通信:
单工通信:发送机只能给接收机发送数据,不允许从接收机发送给发送机。
半双工通信:发送机和接收机可以相互读写通信,但不能同时读写。
全双工工信:发送机和接收机可以相互读写通信,且能同时读写。
UART串口:
UART(Universal Asynchronous Receiver Transmitter),具有串行通信 、异步通信 、全双工通信的特点,两线制(TX, RX),传输速度慢,点对点的异步通信,一般用于RJ45 Console、打印机等。
UART工作原理:
发送器UART1从发送端数据总线接收到并行数据,将起始位、校验位和停止位添加到数据帧中,打包成数据包;然后将数据包以串行方式发送给接收器UART2;UART2以预配置的波特率对数据进行采样,将数据包还原成数据帧;最后UART2将数据帧串行转并传输给接收端的数据总线。
I2C串口:
I2C(Inter-Integrated Circuit),具有串行通信 、同步通信 、半双工通信的特点,两线制(SDA, SCLK),用于监控、存储和数字信号处理器等。
I2C是两线制(SDA, SCLK),通过上拉电阻接到电源线,总线空闲时,SDA,SCLK都保持高电平。
I2C的数据传输过程:
Step1: I2C总线空闲时,上拉电阻使SDA, SCLK处于高电平。
Step2:Master发送start后,将SDA由高电平切换成低电平,然后SCLK线也由高电平切换成低电平。
Step3: Master在发送start后,再发送 slave的地址和读/写的命令,其中write是0,read是1,slave收到地址和读写命令后,向master回复ASK。
Step4:Master收到ASK后,再发送特定寄存器的地址,slave收到后回复master ASK。
Step5:Master再次收到ASK后,再像特定的寄存器发送8bit数据,slave收到数据后回复ASK,重复这一动作直至数据发完。
Step6:Master收到stop,SCLK由低电平切换成高电平,随后SDA也从低电平切换成高电平。
SPI串口:
SPI(Serial Peripheral Interface),具有串行通信 、同步通信 、全双工通信的特点,四线制(CS, SCLK, MOSI, MISO),传输速度快,时许同步准确,一般用于存储器、数字信号处理、传感器和语音识别等。
SPI 通讯使用 3 条总线及片选线, 3 条总线分别为 SCK 、 MOSI 、 MISO ,片选线为 SS ,它们的作
用介绍如下:
(1) SS ( Slave Select) :从设备选择信号线,常称为片选信号线,也称为 NSS 、 CS ,以下用 NSS 表示。当有多个 SPI 从设备与 SPI 主机相连时,设备的其它信号线 SCK 、 MOSI 及 MISO 同时并联到相同的SPI 总线上,即无论有多少个从设备,都共同只使用这 3 条总线;而每个从设备都有独 立的这一条 NSS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C 协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而 SPI 协议中没有设备地址,它使用 NSS 信号线来寻址,当主机要选择从设备时,把该从设备的 NSS 信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通讯。所以 SPI 通讯以 NSS 线置低电平为开始信号,以 NSS 线被拉高作为结束信号。
(2) SCK (Serial Clock) :时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率, 不同的设备支持的最高时钟频率不一样,如 STM32 的 SPI 时钟频率最大为 f pclk /2 ,两个设备之间 通讯时,通讯速率受限于低速设备。
(3) MOSI (Master Output , Slave Input) :主设备输出 / 从设备输入引脚。主机的数据从这条信号线输 出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。
(4) MISO (Master Input, , Slave Output) :主设备输入 / 从设备输出引脚。主机从这条信号线读入数
据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。
I2C在单片机中的应用:
软件模拟:
51单片机:
cpp
/*******************************************************************************
* 函 数 名 : iic_start
* 函数功能 : 产生IIC起始信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_start(void)
{
IIC_SDA=1;//如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=0; //当SCL为高电平时,SDA由高变为低
delay_10us(1);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
delay_10us(1);
}
void iic_start(void)
{
IIC_SDA=1;//如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=0; //当SCL为高电平时,SDA由高变为低
delay_10us(1);
IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
delay_10us(1);
}
/*******************************************************************************
* 函 数 名 : iic_stop
* 函数功能 : 产生IIC停止信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_stop(void)
{
IIC_SDA=0;//如果把该条语句放在SCL后面,第二次读写会出现问题
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SDA=1; //当SCL为高电平时,SDA由低变为高
delay_10us(1);
}
/*******************************************************************************
* 函 数 名 : iic_ack
* 函数功能 : 产生ACK应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_ack(void)
{
IIC_SCL=0;
IIC_SDA=0; //SDA为低电平
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
/*******************************************************************************
* 函 数 名 : iic_nack
* 函数功能 : 产生NACK非应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_nack(void)
{
IIC_SCL=0;
IIC_SDA=1; //SDA为高电平
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
}
/*******************************************************************************
* 函 数 名 : iic_wait_ack
* 函数功能 : 等待应答信号到来
* 输 入 : 无
* 输 出 : 1,接收应答失败
0,接收应答成功
*******************************************************************************/
u8 iic_wait_ack(void)
{
u8 time_temp=0;
IIC_SCL=1;
delay_10us(1);
while(IIC_SDA) //等待SDA为低电平
{
time_temp++;
if(time_temp>100)//超时则强制结束IIC通信
{
iic_stop();
return 1;
}
}
IIC_SCL=0;
return 0;
}
/*******************************************************************************
* 函 数 名 : iic_write_byte
* 函数功能 : IIC发送一个字节
* 输 入 : dat:发送一个字节
* 输 出 : 无
*******************************************************************************/
void iic_write_byte(u8 dat)
{
u8 i=0;
IIC_SCL=0;
for(i=0;i<8;i++) //循环8次将一个字节传出,先传高再传低位
{
if((dat&0x80)>0)
IIC_SDA=1;
else
IIC_SDA=0;
dat<<=1;
delay_10us(1);
IIC_SCL=1;
delay_10us(1);
IIC_SCL=0;
delay_10us(1);
}
}
/*******************************************************************************
* 函 数 名 : iic_read_byte
* 函数功能 : IIC读一个字节
* 输 入 : ack=1时,发送ACK,ack=0,发送nACK
* 输 出 : 应答或非应答
*******************************************************************************/
u8 iic_read_byte(u8 ack)
{
u8 i=0,receive=0;
for(i=0;i<8;i++ ) //循环8次将一个字节读出,先读高再传低位
{
IIC_SCL=0;
delay_10us(1);
IIC_SCL=1;
receive<<=1;
if(IIC_SDA)receive++;
delay_10us(1);
}
if (!ack)
iic_nack();
else
iic_ack();
return receive;
}
STM32:
PA4和PA2分别作为SCL和SDA。
cpp
void MyI2C_Init(void)
{
//开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PB10和PB11引脚初始化为开漏输出
/*设置默认电平*/
GPIO_SetBits(GPIOA, GPIO_Pin_4 | GPIO_Pin_2); //设置PB10和PB11引脚初始化后默
}
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_2, (BitAction)BitValue);
Delay_us(10);
}
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
void MyI2C_Start(void)
{
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for(i=0;i<8;i++)
{
MyI2C_W_SDA(Byte&0x80>>i);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i,Byte=0x00;
MyI2C_W_SDA(1);
for(i=0;i<8;i++)
{
MyI2C_W_SCL(1);
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
MyI2C_W_SCL(0);
}
return Byte;
}
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
AckBit= MyI2C_R_SDA();
MyI2C_W_SCL(0);
return AckBit;
}
硬件I2C:
51单片机中没有硬件I2C的功能,只有STM32中有,这里只讲解STM32中的I2C功能。
STM32 的 I2C 片上外设专门负责实现 I2C 通讯协议,只要配 置好该外设,它就会自动根据协议求产生通讯信号,收发数据并缓存起来, CPU 只要检测该外设的状态和访问数据寄存器 ,就能完成数据收发。这种由硬件外设处理 I2C 协议的方式减轻了 CPU 的工作,且使软件设计更加简 STM32 的 I2C 外设可用作通讯的主机及从机 ,支持 100Kbit/s 和 400Kbit/s 的速率,支持 7 位和 10 位设备地址,支持 DMA 数据传输 ,并具有 数据校验功能 。它的 I2C 外设还支持 SMBus2.0 协议, SMBus 协议与 I2C 类似,主要应用于笔记本电脑的电池管理中,本篇不展开。
STM32中I2C架构剖析:
通信引脚:
I2 C 的所有硬件架构都是根据图中左侧 SCL 线和 SDA 线展开的 ( 其中的 SMBA 线用于 SMBUS 的 警告信号,I2C 通讯没有使用 ) 。 STM32 芯片有多个 I2C 外设,它们的 I2C 通讯信号引出到不同的 GPIO 引脚上,使用时必须配置到这些指定的引脚。
时钟逻辑控制:
SCL 线的时钟信号,由 I 2 C 接口根据时钟控制寄存器 (CCR) 控制,控制的参数主要为时钟频率。
配置I2C的CCR寄存器可修改通讯速率相关的参数:
1、可选择 I2C 通讯的"标准/快速"模式,这两个模式分别 I2C 对应 100/400Kbit/s 的通讯速率。
2、 在快速模式下可选择 SCL 时钟的占空比,可选 Tlow/Thigh=2 或 Tlow/Thigh=16/9 模式,我
们知道 I2C 协议在 SCL 高电平时对 SDA 信号采样,SCL低电平时SDA准备下一个数据,修改 SCL 的高低电平比会影响数据采样,但其实这两个模式的比例差别并不大,若不是要求非常严格,这里随便选就可以了。
数据逻辑控制:
I2C 的 SDA 信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源及目标是数据寄存器 (DR)、地址寄存器 (OAR)、PEC 寄存器以及 SDA 数据线。当向外发送数据的时候,数据移位存器以"数据寄存器"为数据源,把数据一位一位地通过 SDA 信号线发送出去;当从外部接收数据时候,数据移位寄存器把 SDA 信号线采样到的数据一位一位地存储到"数据寄存器"中。 若使能了数据校验,接收到的数据会经过 PCE 计算器运算,运算结果存储在"PEC 寄存器"中。当 STM32 的I2C 工作在从机模式的时候,接收到设备地址信号时,数据移位寄存器会把接收到 的地址与STM32 的自身的"I2C 地址寄存器"的值作比较,以便响应主机的寻址。STM32 的自身I2C地址可通过修改"自身地址寄存器"修改,支持同时使用两个 I2C 设备地址,两个地址分别存储OAR1 和 OAR2 中。
整体逻辑控制:
整体控制逻辑负责协调整个 I2C 外设,控制逻辑的工作模式根据我们配置的"控制寄存器 (CR1/CR2)"的参数而改变。在外设工作时,控制逻辑会根据外设的工作状态修改"状态寄存器(SR1 和 SR2)",我们只要读取这些寄存器相关的寄存器位,就可以了解 I2C 的工作状态。除此之外,控制逻辑还根据要求,负责控制产生 I2C 中断信号、DMA 请求及各种 I2C 的通讯信号 (起始、停止、响应信号等)。
通信过程:
使用 I2C 外设通讯时,在通讯的不同阶段它会对"状态寄存器 (SR1 及 SR2)"的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。
主发送器:
主发送器发送流程及事件说明如下:
(1) 控制产生起始信号 (S) ,当发生起始信号后,它产生事件 "EV5 ",并会对 SR1 寄存器的" SB "位置 1 ,表示起始信号已经发送。
(2) 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件 "EV6"及"EV8" ,这时SR1 寄存器的" ADDR "位及" TXE "位被置 1 , ADDR 为 1 表示地址已经发送, TXE 为 1 表示数据寄存器为空。
(3) 以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的"数据寄存器 DR"写入要发送的数
据,这时 TXE 位会被重置 0,表示数据寄存器非空,I2C 外设通过 SDA 信号线一位位把数据发送
出去后,又会产生 "EV8" 事件,即 TXE 位被置 1,重复这个过程,就可以发送多个字节数据了。
(4) 当我们发送数据完成后,控制 I2C 设备产生一个停止信号 (P),这个时候会产生 EV8_2 事件 ,
SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。
主接收器:
主接收器接收流程及事件说明如下:
(1) 同主发送流程,起始信号 (S) 是由主机端产生的,控制发生起始信号后,它产生事件 "EV5" ,
并会对 SR1 寄存器的" SB "位置 1 ,表示起始信号已经发送。
(2) 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件" EV6" 这时 SR1 寄存器
的" ADDR "位被置 1 ,表示地址已经发送。
(3) 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生 "EV7" 事
件, SR1 寄存器的 RXNE 被置 1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据
寄存器清空,以便接收下一次数据。此时我们可以控制 I2C 发送应答信号 (ACK) 或非应答信号
(NACK) ,若应答,则重复以上步骤接收数据,若非应答,则停止传输。
(4) 发送非应答信号后,产生停止信号 (P) ,结束传输。在发送和接收过程中,有的事件不只是标志了我们上面提到的状态位,还可能同时标志主机状态之类的状态位,而且读了之后还需要清除标志位,比较复杂。 我们可使用 STM32 标准库函数来直接检测这些事件的触发标志 ,降低编程难度。
I2C初始化结构体:
cpp
typedef struct
{
uint32_t I2C_ClockSpeed; /*!< 设置 SCL 时钟频率,此值要低于 400000*/
uint16_t I2C_Mode; /*!< 指定工作模式,可选 I2C 模式及 SMBUS 模式 */
uint16_t I2C_DutyCycle; /* 指定时钟占空比,可选 low/high = 2:1 及 16:9 模式*/
uint16_t I2C_OwnAddress1; /*!< 指定自身的 I2C 设备地址 */
uint16_t I2C_Ack; /*!< 使能或关闭响应 (一般都要使能) */
uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的长度,可为 7 位及 10 位 */
} I2C_InitTypeDef;
配置完这些结构体成员值,调用库函数 I2C_Init 即可把结构体的配置写入到寄存器中。
说白了,硬件I2C就是帮我们完成了I2C协议的基本时序,但是如何实现通信,需要我们把他进行组合,不断检测标志位,判断通信到了哪一步,然后我们在调用硬件I2C的函数,一般是清楚标志位、发送开始时序,发送结束时序,发送非应答,接收数据(读取SR)、发送数据(写入SR)生成对应的时序,继续通信。
这里可以和DMA模块一起复用,通过DMA,可以不断的发送数据,这里可以粗略的讲解一下,我们知道,当I2C硬件完成发送一个数据之后,相应的标志位会被置位,我们在设置相应DMA触发事件,就可以不断的往SR中搬运数据,实现连续发送数据,读取也是一样。
软件I2C和硬件I2C的区别:
1、前者时序的搭建,需要CPU的参与,不断的改变引脚电平,来满足时序的要求。
2、后者不需要CPU的参与,大大节省了CPU的资源。
3、他们的波形也有些许差别。
软件:
硬件:
仔细观察,可以发现,当SCL拉低的那一瞬间之后,软件的SDA没有立即改变,而硬件控制的SDA则迅速反应,这是因为软件完成时序是通过函数改变引脚电平,这段时间会有点延时。
UART在单片机中的应用:
在单片机使用的串口通讯中,一般只使用 RXD、TXD 以及 GND 三条信号线,直接传输数据信号。
USB转TTL模块:
这里需要用到CH340USB转TTL模块。
CH340是一种USB转串口芯片,常用于单片机与计算机之间的串口通信。它的主要作用是将计算机的USB接口转换为串行通信接口,从而实现计算机与单片机之间的数据传输。
在单片机开发中,通常需要通过串口与计算机进行通信,以实现数据的传输、调试和监控等功能。而现代计算机通常只提供USB接口而不再配备传统的串口接口,因此需要通过USB转串口芯片来连接单片机和计算机。
CH340芯片具有成本低廉、稳定可靠、兼容性好等优点,因此被广泛应用于单片机开发中。它提供了简单易用的串口通信功能,使得开发者可以轻松实现单片机与计算机之间的数据交换。
协议层:
串口通讯的数据包由发送设备通过自身的 TXD 接口传输到接收设备的 RXD 接口。在串口通讯的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位组成,通讯双方 的数据包格式要约定一致才能正常收发数据,其组成见图串口数据包的基本组成 。
波特率:
本章中主要讲解的是串口异步通讯,异步通讯中由于没有时钟信号,所以两个通讯设备之间需要约定好波特率,即每个码元的长度,以便对信号进行解码,图串口数据包的基本组成中用虚线分开的每一格就是代表一个码元。常见的波特率为 4800 、 9600 、 115200 等。
通信的起始信号和停止信号:
串口通讯的一个数据包从起始信号开始,直到停止信号结束。数据包的起始信号由一个逻辑 0 的 数据位表示,而数据包的停止信号可由 0.5 、 1 、 1.5 或 2 个逻辑 1 的数据位表示,只要双方约定 一致即可。
有效数据:
在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为 5、6、7 或 8 位长。
数据校验:
在有效数据之后,有一个可选的数据校验位。由于数据通信相对更容易受到外部干扰导致传输 数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有奇校验 (odd) 、偶校验
(even) 、 0 校验 (space) 、 1 校验 (mark) 以及无校验 (noparity) 。 奇校验要求有效数据和校验位中"1 "的个数为奇数,比如一个 8 位长的有效数据为: 01101001 , 此时总共有 4 个" 1 ",为达到奇校验效果,校验位为" 1 ",最后传输的数据将是 8 位的有效数据 加上 1 位的校验位总共 9 位。 偶校验与奇校验要求刚好相反,要求帧数据和校验位中"1 "的个数为偶数,比如数据帧: 11001010 , 此时数据帧"1 "的个数为 4 个,所以偶校验位为" 0 "。 0校验是不管有效数据中的内容是什么,校验位总为" 0 ", 1 校验是校验位总为" 1 "。
51单片机串口通信:
串口通信初始化代码可以从STC-ISP中获取:
发送数据:
cpp
//串口发送一个字节数据
void UART_SendByte(unsigned char Byte){
SBUF=Byte;
//检测是否完成
while(TI==0);
TI=0;//TI复位
}
接收数据:
这里用的是中断4
cpp
void uart() interrupt 4 //串口通信中断函数
{
u8 rec_data;
RI = 0; //清除接收中断标志位
rec_data=SBUF; //存储接收到的数据
}
STM32串口通信:
TX:发送数据输出引脚。
RX :接收数据输入引脚
数据寄存器
USART 数据寄存器 (USART_DR) 只有低 9 位有效,并且第 9 位数据是否有效要取决于 USART
控制寄存器 1(USART_CR1) 的 M 位设置,当 M 位为 0 时表示 8 位数据字长,当 M 位为 1 表示 9
位数据字长,我们一般使用 8 位数据字长。 USART_DR 包含了已发送的数据或者接收到的数据。 USART_DR 实际是包含了两个寄存器,一 个专门用于发送的可写 TDR ,一个专门用于接收的可读 RDR 。当进行发送操作时,往 USART_DR 写入数据会自动存储在 TDR 内;当进行读取操作时,向 USART_DR 读取数据会自动提取 RDR数据。 TDR 和 RDR 都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的,发送时把 TDR 内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到 RDR 。 USART 支持 DMA 传输,可以实现高速数据传输。
控制器
USART 有专门控制发送的发送器、控制接收的接收器,还有唤醒单元、中断控制等等。使用
USART 之前需要向 USART_CR1 寄存器的 UE 位置 1 使能 USART , UE 位用来开启供给给串口
的时钟。 发送或者接收数据字长可选 8 位或 9 位,由 USART_CR1 的 M 位控制。
发送器
当 USART_CR1 寄存器的发送使能位 TE 置 1 时,启动数据发送,发送移位寄存器的数据会在 TX
引脚输出,低位在前,高位在后。如果是同步模式 SCLK 也输出时钟信号。一个字符帧发送需要三个部分:起始位 + 数据帧 + 停止位。起始位是一个位周期的低电平,位周期就是每一位占用的时间;数据帧就是我们要发送的 8 位或 9 位数据,数据是从最低位开始传输的;停止位是一定时间周期的高电平。 停止位时间长短是可以通过 USART 控制寄存器 2(USART_CR2) 的 STOP[1:0] 位控制,可选 0.5 个、1 个、 1.5 个和 2 个停止位。默认使用 1 个停止位。
编程要点;
- 使能 RX 和 TX 引脚 GPIO 时钟和 USART 时钟;
- 初始化 GPIO ,并将 GPIO 复用到 USART 上;
- 配置 USART 参数;
- 配置中断控制器并使能 USART 接收中断;
- 使能 USART ;
- 在 USART 接收中断服务函数实现数据接收和发送
初始化配置:
cpp
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 嵌套向量中断控制器组选择 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置 USART 为中断源 */
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
/* 抢断优先级为 1 */
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 子优先级为 1 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
/* 初始化配置 NVIC */
NVIC_Init(&NVIC_InitStructure);
}
cpp
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口 GPIO 的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
将 USART Tx 的 GPIO 配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 将 USART Rx 的 GPIO 配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作参数
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 配置 针数据字长
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(DEBUG_USARTx, &USART_InitStructure);
// 串口中断优先级配置
NVIC_Configuration();
// 使能串口接收中断
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
// 使能串口
USART_Cmd(DEBUG_USARTx, ENABLE);
}
cpp
/**
* 函 数:串口发送一个字节
* 参 数:Byte 要发送的一个字节
* 返 回 值:无
*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
cpp
/**
* 函 数:USART1中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
Serial_RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
Serial_RxFlag = 1; //置接收标志位变量为1
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除USART1的RXNE标志位
//读取数据寄存器会自动清除此标志位
//如果已经读取了数据寄存器,也可以不执行此代码
}
}
SPI在单片机中的应用:
硬件电路:
时序:
软件模拟SPI:
这里由于还是通过CPU直接控制GPIO引脚来模拟时序,对于51和32,思路都是一样,我这里就只列举STM32中软件模拟。
cpp
/**
* 函 数:SPI写SS引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
*/
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平
}
/**
* 函 数:SPI写SCK引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平
*/
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue,设置SCK引脚的电平
}
/**
* 函 数:SPI写MOSI引脚电平
* 参 数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~0xFF
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平
*/
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
}
/**
* 函 数:I2C读MISO引脚电平
* 参 数:无
* 返 回 值:协议层需要得到的当前MISO的电平,范围0~1
* 注意事项:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1
*/
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //读取MISO电平并返回
}
/**
* 函 数:SPI初始化
* 参 数:无
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化
*/
void MySPI_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA4、PA5和PA7引脚初始化为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA6引脚初始化为上拉输入
/*设置默认电平*/
MySPI_W_SS(1); //SS默认高电平
MySPI_W_SCK(0); //SCK默认低电平
}
/*协议层*/
/**
* 函 数:SPI起始
* 参 数:无
* 返 回 值:无
*/
void MySPI_Start(void)
{
MySPI_W_SS(0); //拉低SS,开始时序
}
/**
* 函 数:SPI终止
* 参 数:无
* 返 回 值:无
*/
void MySPI_Stop(void)
{
MySPI_W_SS(1); //拉高SS,终止时序
}
/**
* 函 数:SPI交换传输一个字节,使用SPI模式0
* 参 数:ByteSend 要发送的一个字节
* 返 回 值:接收的一个字节
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i, ByteReceive = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
for (i = 0; i < 8; i ++) //循环8次,依次交换每一位数据
{
MySPI_W_MOSI(ByteSend & (0x80 >> i)); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
MySPI_W_SCK(1); //拉高SCK,上升沿移出数据
if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);} //读取MISO数据,并存储到Byte变量
//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
MySPI_W_SCK(0); //拉低SCK,下降沿移入数据
}
return ByteReceive; //返回接收到的一个字节数据
}
硬件SPI:
数据控制逻辑
SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的数据来源及目标接收、发
送缓冲区以及 MISO 、 MOSI 线。当向外发送数据的时候,数据移位寄存器以"发送缓冲区"为数
据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候,数据移位寄存器把数
据线采样到的数据一位一位地存储到"接收缓冲区"中。通过写 SPI 的"数据寄存器 DR "把数据填充到发送缓冲区中,通讯读"数据寄存器 DR ",可以获取接收缓冲区中的内容。其中数据帧长度可以通过"控制寄存器 CR1 "的" DFF 位"配置成 8 位及 16 位模式;配置" LSBFIRST 位" 可选择 MSB 先行还是 LSB 先行。
整体控制逻辑
整体控制逻辑负责协调整个 SPI 外设,控制逻辑的工作模式根据我们配置的"控制寄存(CR1/CR2)"的参数而改变,基本的控制参数包括前面提到的 SPI 模式、波特率、 LSB 先行、主从模式、单双向模式等等。在外设工作时,控制逻辑会根据外设的工作状态修改"状态寄存器 (SR)",我们只要读取状态寄存器相关的寄存器位,就可以了解 SPI 的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生 SPI 中断信号、 DMA 请求及控制 NSS 信号线。实际应用中,我们一般不使用 STM32 SPI 外设的标准 NSS 信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。
通讯过程
STM32 使用 SPI 外设通讯时,在通讯的不同阶段它会对"状态寄存器 SR "的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。图主发送器通讯过程 中的是"主模式"流程,即 STM32 作为 SPI 通讯的主机端时的数据收发过 程
主模式收发流程及事件说明如下:
(1) 控制 NSS 信号线,产生起始信号 (图中没有画出)。
(2) 把要发送的数据写入到"数据寄存器 DR"中,该数据会被存储到发送缓冲区。
(3) 通讯开始,SCK 时钟开始运行。MOSI 把发送缓冲区中的数据一位一位地传输出去;MISO 则
把数据一位一位地存储进接收缓冲区中。
(4) 当发送完一帧数据的时候,"状态寄存器 SR"中的"TXE 标志位"会被置 1,表示传输完一
帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,"RXNE 标志位"会被置 1,表示传输
完一帧,接收缓冲区非空。
(5) 等待到"TXE 标志位"为 1 时,若还要继续发送数据,则再次往"数据寄存器 DR"写入数据即可;等待到"RXNE 标志位"为 1 时,通过读取"数据寄存器 DR"可以获取接收缓冲区中的内容。 假如我们使能了 TXE 或 RXNE 中断,TXE或RXNE置1时会产生SPI中断信号,进入同一个中断服务函数,到SPI中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用 DMA 方式来收发"数据寄存器 DR"中的数据。
SPI初始化:
cpp
void MySPI_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //开启SPI1的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA4引脚初始化为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA5和PA7引脚初始化为复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA6引脚初始化为上拉输入
/*SPI初始化*/
SPI_InitTypeDef SPI_InitStructure; //定义结构体变量
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //模式,选择为SPI主模式
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //方向,选择2线全双工
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //数据宽度,选择为8位
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //先行位,选择高位先行
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率分频,选择128分频
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //SPI极性,选择低极性
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS,选择由软件控制
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC多项式,暂时用不到,给默认值7
SPI_Init(SPI1, &SPI_InitStructure); //将结构体变量交给SPI_Init,配置SPI1
/*SPI使能*/
SPI_Cmd(SPI1, ENABLE); //使能SPI1,开始运行
/*设置默认电平*/
MySPI_W_SS(1); //SS默认高电平
}
SPI配置:
cpp
/**
* 函 数:SPI写SS引脚电平,SS仍由软件模拟
* 参 数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
*/
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平
}
/**
* 函 数:SPI起始
* 参 数:无
* 返 回 值:无
*/
void MySPI_Start(void)
{
MySPI_W_SS(0); //拉低SS,开始时序
}
/**
* 函 数:SPI终止
* 参 数:无
* 返 回 值:无
*/
void MySPI_Stop(void)
{
MySPI_W_SS(1); //拉高SS,终止时序
}
/**
* 函 数:SPI交换传输一个字节,使用SPI模式0
* 参 数:ByteSend 要发送的一个字节
* 返 回 值:接收的一个字节
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); //等待发送数据寄存器空
SPI_I2S_SendData(SPI1, ByteSend); //写入数据到发送数据寄存器,开始产生时序
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); //等待接收数据寄存器非空
return SPI_I2S_ReceiveData(SPI1); //读取接收到的数据并返回
}
总结:
硬件SPI看似很复杂,实际上,我们只需要通过配置好结构体,然后把结构体放入库封装好的初始化函数,之后我们调用它给我们写好的时序函数,在不断判断标志位,并调用对应的库函数,实现SPI外设与其他设备的通信。