前言
串口通信就是将一个设备的数据传送到另一个设备,为了扩展硬件系统。在STM32中,里面集成了很多功能,例如AD采样、TIM定时器计数、PWM输出等功能。这些都是属于STM32芯片内部的功能,就相当于使用了对应的外设(例如PWM输出就是使用了TIM的输出比较功能),配置外设的寄存器都在芯片内。如果需要无法由STM32芯片实现的功能,例如蓝牙无线遥控功能、或者是陀螺仪测量加速度功能等,就需要对应的外挂上芯片,对应的外挂芯片回传数据的时候就需要通信来进行。
1.通信协议的介绍
想要弄懂USART串口通信就需要先了解什么是通信协议。
通信的就是将一个设备的数据传送到另一个设备,扩展硬件系统,而通信协议:制定通信的规则,通信双方按照协议规则进行数据收发。通信协议也就是指定通信的参数,例如双方波特率需要一致。
根据不同的通信协议参数配置(规则)就衍生出多种通信协议,其中常见的如下:
可以看到,有USART、I2C、SPI、CAN、USB这五种常见的通信协议,对应的这五种的硬件电路也不同。从引脚名称就能看出来,该引脚只是该通信协议中最简单的部分引脚引出。
接下来介绍这些通信协议的特征:
1.1双工
然后是双工的分类,总共分为全双工、半双工以及单工(这里没有涉及到)。
全双工:一般有两根通信线,可以同时完成数据的发送和接收,互不影响,例如串口USART中的TX、RX(其他地方也叫做TXD、RXD)(Transmit Exchange、Receive Exchange)如下图:
半双工:只有一根数据线,通过一根数据线完成数据的发送和接收操作,例如I2C中的SCL、SDA(Serial Clock、Serial Data),如下图:
单工:也即只有一根通信线,只能单向传递数据,如下图:
1.2时钟(同步、异步)
然后是时钟,决定同步还是异步,看到下图:
其中,只有I2C和SPI属于同步的通信协议,也就是这两种通信协议通信时,是同步输入输出无延迟的,对应的都有一个同步时钟的引脚端口。
而异步,并没用来同步时钟的引脚端口,相对应的双方(设备1和设备2)需要规定一个采样频率,并且还需要添加一些帧头帧尾等来进行数据对齐操作。
1.3电平(单端、差分)
电平分为单端和差分,单端也就是相对应GND的电压,例如TX、RX都是相对于GND的电压,对应的设备1和设备2的GND必须要连接到一起,这样才能够规定低电平GND来参考电压。
而差分,并不需要相对GND,而是两个引脚电压差即可。例如CAN通信,对应的电平输入、输出就是CNA_L和CAN_L的电压之差。使用差分信号可以极大程度的克服干扰,所以一般差分信号一般会用于传输远距离的通信。
1.4设备(点对点、多设备)
设备,分为点对点、多设备。其中点对点也就是一对一,一个设备对应一个设备。而多设备就是一个设备可以传输至多个设备之中,对应的在多设备通信协议,还需要有寻址(寻找对应设备)来进行传输操作。
2.USART通信协议
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。
单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力 。
下面为串口通信的一般结构图:
RX和TX是交叉连接的,设备1的TX发送到设备2的RX接收,设备1的RX接收到设备2的TX发送,这样就能实现最简单的串口通信。
2.1电平标准
在串口通信中,还需要注意电平标准,不同的电平标准之间不能正常地通信,需要通过相应的电平芯片来转换电平,进而实现通信的目的。
电平标准是数据1和数据0(逻辑1和逻辑0)的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
TTL电平:+3.3V或+5V表示1,0V表示0
RS232电平:-3~-15V表示1,+3~+15V表示0
RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)
在STM32单片机中一般使用的TTL电平,也就是低电压。而RS232电平3~15,一般用在电压较大的设备上。而RS485电平,传输的是差分信号,对应的应用在远距离传输的设备上。
2.2USART串口特征
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器,一般来说USART和USAT是一样的,只不过USART多了一个SCL时钟输出的功能(并不常用),在后续介绍结构会提到。
USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。
自带波特率发生器,最高达4.5Mbits/s
使用分数波特率发生器 —— 12位整数和4位小数的表示方法
可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
可选校验位(无校验/奇校验/偶校验)
支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN
STM32F103C8T6 USART资源: USART1、 USART2、 USART3
上述为对USART串口外设的特征描述。
2.2.1波特率
波特率,就是每秒钟传输码元的个数,一般选择波特率为9600。分数波特率发生器也就是通过选择分频系数,来产生对应的波特率,同时该参数分为12位正数和4位小数,这样就可以得到更为精确的波特率。
上图就为配置参数的寄存器BBR。
计算公式:
根据不同的串口,选择不同的时钟,例如USART1就为APB2上的外设,此时f频率就为fPCLK2,同时上面的的计算公式在后续会提到如何得到的。
发送器和接收器的波特率由波特率寄存器BRR里的DIV确定,例如此时需要产生9600的波特率,同时时钟为72MHz,待入计算公式,就可以得到一个带小数的数值。此时对该数值转换为16禁止,对应的就会产生小数部分,例如上述DIV结果为468.75,转换为二进制就为111010100.11,对应的就写入:
换句话说,对BRR寄存器按上图配置(没写到的地方都为0),就可以产生一个为9600的波特率。
下图为在不同主频下的误差:
2.2.2串口字符组成
上图就为一个字符的组成,分为起始位,数据帧,检验位以及停止位。
其中,起始位标志一个数据帧的开始,固定为低电平(空闲状态下就为高电平)。
数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行。
校验位:用于数据验证,根据数据位计算得来(一般分为奇、偶校验)。
停止位:用于数据帧间隔,固定为高电平(一般为1位)。
数据位一般为8位(位0到位7),特殊情况下可以将检验位也作为数据位,也即9位数据位。
同时,在时钟的上升沿,才进行一个数据位的传输。
奇、偶校验



上面是不同PCE位和M位的组合下的数据帧的分配情况,看到CR1寄存器:
M位决定字长(数据帧的长度),PCE位则决定是否需要校验位,其中一个起始位和N个停止位是固定的,用来决定一个数据帧的开始和结束,其中N个停止位还需要另外设置,停止位越长,两个数据帧的间隔就越长。
例如,M位为0(8为数据位长),PCE为0(禁止校验控制),结果就是8位(0~7位)全为数据传输,没有数据校验功能。
一般情况下,需要校验位(PCE位为1),对应的M为就会为1,这样也会8位(0~7)数据位,对应的8位对应一个字节Byte。
同时传输模式:如果USART_CR1的PCE位被置位,写进数据寄存器的数据的MSB位被校验位替换后发送出去(如果选择偶校验偶数个’1’,如果选择奇校验奇数个’1’)。如果奇偶校验失败, USART_SR寄存器中的PE标志被置’1’,并且如果USART_CR1寄存器的PEIE在被预先设置的 话,中断产生。
这里类似ADC外设中的规则组、注入组转换完成,同样会产生标志位,根据自身需求,如果需要则使能对应的中断使能位,这样就可以在产生标志位后进入中断,执行我们需要的程序。
同时在PE位描述中看到,在清除PE位前,软件必须等待RXNE标志位被置1:
当RDR移位寄存器为空,对应的USART_DR寄存器就为非空,因为RDR移位寄存器会将数据转运至USART_DR寄存器(后续分析结构会提到),此时RXNE就置1,然后就可以完成PE标志位的清除。
串口数据帧中还有n个停止位(高电平),用于数据帧间隔,固定为高电平:
用两位STOP[1:0]来决定停止位的长度,也就是用来决定两个字符传输间隔的时长。
上图就为一个完整数据帧的具体结构。
在USART串口通信中,包含发送器和接收器,对应内部结构就是发送寄存器、接收寄存器(用来存储数据)以及发送、接收控制寄存器,接下来进行展开。
3.USART内部结构
3.1数据的接收和发送
上图位USART的内部结构,最下面为之前讲过的波特率发生器,通过对BRR寄存器位设置,就可以得到不同大小的波特率,同时还能看到BRR寄存器控制的包含发送器波特率控制,还包括接收器波特率控制。根据TE、RE位的设置,就能实现BRR寄存器对对应操作的波特率进行控制。
实际上就是,在发送、接收的过程,自动实现对发送、接收波特率的控制。
接下来重点看到发送TX和接收RX引脚的输入:
TX引脚也就是输出(发送)引脚,对发送数据寄存器TDR进行写操作,然后通过发送移位寄存器转运至TX引脚,实现数据输出。
同样,RX接收(输入)引脚,从RX中接收到数据传输至接收移位寄存器,然后存储至接收数据寄存器供用户读操作。
上述位TX发送和RX接收的一般操作,其中包含一些细节,当发送使能位(TE)被设置时,发送移位寄存器中的数据在TX脚上输出,相应的时钟脉冲在CK(SCKL)脚上输出,这一输出也就是对应USART同步时序的功能,后续提到。
TDR发送数据寄存器和RDR接收数据寄存器在外设内部只表现为1个数据寄存器:
由于RDR接收数据寄存器是只读的,而TDR发送数据寄存器是只写的,所以,对USART_DR寄存器进行读、写操作,分别会对RDR、TDR执行对应的操作,这样就实现由一个寄存器实现两个寄存器的功能。
简而言之就是,在物理层面上表现为两个寄存器(TDR、RDR),而在软件层上是通过同一个地址进行访问的,实际上在硬件层面是分离的,分别用于接送和发送数据。
3.1.1发送器
发送器主要实现的就是字符(数据帧)的发送:
字符发送是从最低为开始转移,用图表示就是:
这样就能完成一整个发送过程。
这里看到下面两个注意:
在数据传输过程中,TE位作为发送数据的使能端,不仅控制着发送器控制,同时还控制着发送器波特率的控制:
对TE进行复位,就会同时复位这两个控制端,发送器波特率控制复位就会导致波特率计数器停止计数,导致当前传输的数据丢失。
然后是TE位激活后将发送一个空闲帧,以下的解释:
- 当TE位被激活(设置)时,USART的发送器被允许开始发送数据。
- 如果此时发送缓冲区有数据,那么这些数据会按照USART的配置(如波特率、数据位、停止位等)被发送出去。
- 当发送缓冲区中的所有数据都被发送完毕,包括最后一个字节的停止位之后,USART线路会自然进入空闲态,即发送了一个“空闲帧”。







3.1.2接收器
接收器就是通过串口来接收一个一个数据帧,对应的也就是发送器发出的数据。
当CR1寄存器中的RE位被激活,此时就开始对起始位进行寻找。
在起始位侦测中,总共会有16位的侦测位检测。当在空闲模式下突然检测到了1110,则表示这时可能开始准备接收数据,然后接着检测,在第3、5、7位的第一次采样,和在第8、9、10的第二次采样都为’0’,此时就说明接下来需要对数据进行接收操作。
然而只需要在在第3、5、7位的第一次采样,和在第8、9、10的第二次采样中至少有两位为0,就说明该侦测位有效(X表示无论为1还是0),此时会置NE噪声标志位为1,表明此时数据收到了干扰,是否需要使用根据自身需求来使用。这种序列是固定的在内部,只要检测到符合这种侦测序列,就会开始数据的接收。
标志位NE会和RXNE标志位一起出现,因为当开始接收时,移位寄存器一接收到数据,就会立刻转运到RDR接收数据寄存器中,此时RXNE标志位就为1。所以刚开始,NE几乎和RXNE标志位同时为1,所以,需要在NE检测到噪声产生中断时,可以使能RXNEIE使能中断。
实际上,发送数据也是类似的,在第一次发送时(从空闲状态下突然发送),数据会直接到移位数据寄存器,跨过了发送数据寄存器TDR,同样的TXE也会置1,然后接收到对应TXE标志位,此时就会往TDR寄存器中补充数据(重装数据)。

同样会随着RXNE一同产生,需要产生中断同理使能RXNEIE,就可以使用RXNE标志位。
根据自己需要,在检测到总线空闲,执行对应的中断。
溢出错误,又称为过载错误,在帧错误发生的同时,如果后续也发生过载错误,FE以及ORE这两个标志位都会被置起。
RDR内容将不会丢失,读USART_DR寄存器仍能得到先前的数据。
移位寄存器中以前的内容将被覆盖,随后接收到的数据都将丢失。
在多缓冲器通信情况下,如果EIE使能,则会产生中断。

正常一般使用1位停止位即可。
上述1.5个停止位使用在智能卡模式下的,而只能卡是作为USART的扩展功能,同样的还有LIN局域互联网、红外IrDA,对应的接口:
根据自己的需要,可以自行参考手册的描述。
下面为对发送控制器、接收控制器的结构图:
从上图就可以很直观看出,那些寄存器配置说明控制,例如发送器控制,由硬件数据流控,发送器时钟、CR1寄存器的TE、SBK、PS、UE位进行控制。
3.2硬件数据流控
硬件数据流控是为了防止数据收发不及时而导致数据被覆盖,造成数据丢失。
如下图未两个设备用串口进行通信,且使用了硬件流控功能。
当设备1发送数据到设备2时(TX1到RX2),nCTS1对应的接到nRTS2,具体作用根据时序来分析:
上图为CTS流控制的时序,也就是发送的硬件流控制。当nCTS为低电平时(实际上是由nRTS2发送到nCTS1的低电平),就表示此时数据接收准备完毕,此时就开始实现数据由TX1到RX2的转运。
如果nCTS一直为高电平(实际上是由nRTS2发送到nCTS1的高电平),表示此时设备2繁忙,此时还不能接收数据,此时就会置TX为空闲状态,直到nRTS2发送到nCTS1的电平为低电平时,此时才开始设备RX2数据的接收(对应设备1的发送TX1)。
设备2的TX2到设备1的RX1也是同理,这里就不再赘述。
4.数据模式
HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
文本模式/字符模式:以原始数据编码后的形式显示
对应文本模式就是根据HEX等模式下的编码得到的字符:
例如:DEC十进制模式下下65,表示的字符就为A,而在HEX模式下则为41,表示A。
对应上图,为发送接收的具体过程,实际上是以HEX模式传输(根据你自己选的HEX、DEC模式)数据(0x41),接收方可以选择HEX数据接收,同样也可以选择译码后得到A。
同样发送也是如此(两种发送选择)。
5.USART的配置
5.1发送配置
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA9引脚初始化为复用推挽输出
初始化,开启时钟等操作。
/*USART初始化*/
USART_InitTypeDef USART_InitStructure; //定义结构体变量
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要
USART_InitStructure.USART_Mode = USART_Mode_Tx; //模式,选择为发送模式
USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位
USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
/*USART使能*/
USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
然后是串口初始化,对USART_InitStructure结构体进行配置。
首先是波特率USART_BaudRate,一般情况下是9600,因为使用的标准库,而不需要在去通过公式来计算出分频大小。
然后是硬件流控USART_HardwareFlowControl,一般应用在容易数据覆盖的情况,为了避免这种情况需要打开,一般正常情况不会出现数据覆盖,也就不需要。
接着USART_Mode串口的模式,可以为发送TX以及接收RX:
这里使用发送功能,即选择TX。
然后是奇偶校验位USART_Parity,一般不需要,而且在特殊情况下(同时变更两位)没有作用。
接着是停止位USART_StopBits,一般1位即可。
最后是字长USART_WordLength,也就是对M位进行选择,8位还是9位数据位,一般都是8位,9位为1字节。
然后将配置好的参数结构体传回初始化函数(Init)中,最后使能一下USART_Cmd即可。
串口发送函数:
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
通过上述函数,就可以实现发送一个字节的数据。
如果需要发送一个数组,则使用下面函数:
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++) //遍历数组
{
Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
类似的,想要实现其他数据类型的发送,只需要写下对应的发送函数即可。
归根结底是用到了函数USART_SendData:
5.2接收配置
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA10引脚初始化为上拉输入
同样也是开启时钟,初始化对应的GPIO端口。
/*USART初始化*/
USART_InitTypeDef USART_InitStructure; //定义结构体变量
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要
USART_InitStructure.USART_Mode = USART_Mode_Rx; //模式,接收模式
USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位
USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
和发送类似,这里的一些参数需要和发送配置一致,除了USART_Mode选择接收模式。
/*中断输出配置*/
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*USART使能*/
USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
然后配置中断控制,因为不知道数据什么时候接收,所以需要通过中断来实现数据接收,防止数据未接收到的情况。
这里要求不高,需要注意的是,中断通道USART1_IRQn。
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标志位
//读取数据寄存器会自动清除此标志位
//如果已经读取了数据寄存器,也可以不执行此代码
}
}
其中Serial_RxData为一个全局变量,用于存储接收的数据,而Serial_RxFlag为接收标志位,也是全局变量。
对应的有函数:
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1) //如果标志位为1
{
Serial_RxFlag = 0;
return 1; //则返回1,并自动清零标志位
}
return 0; //如果标志位为0,则返回0
}
用于获取接收标志位,也就是RXNE,读取后自动清0。
uint8_t Serial_GetRxData(void)
{
return Serial_RxData; //返回接收的数据变量
}
返回接收到的数据函数。
在主函数中:
while (1)
{
if (Serial_GetRxFlag() == 1) //检查串口接收数据的标志位
{
RxData = Serial_GetRxData(); //获取串口接收的数据
OLED_ShowHexNum(1, 8, RxData, 2); //显示串口接收的数据
}
}
}
每次当标志位Serial_RxFlag置1,就接收一次数据,将数据Serial_RxData传回RxData中存储。
实际上用到的是USART_ReceiveData接收函数:
这样就实现了对数据的接收。
6.小结
本篇文章中尽管篇幅较长但还是有一些没有提及,例如USART的同步功能、IrDA、智能卡等,如有需要自行查阅手册,最后欢迎各位的讨论以及错误指针。