STM32——通信接口(串口通信)
通信接口介绍,串口通信相关知识点讲解,USART的相关知识点讲解及其代码编写(发送数据+发送和接收数据+串口收发HEX和文本数据包)
目录
3.6硬件数据流控 (Hardware Flow Control)
3.8SCLK控制 (Synchronous Clock Control)
#USART串口协议#
一、通信接口
- 通信目的:将一个设备的数据传送到另一个设备,扩展硬件系统
STM32芯片内部集成许多功能模块(eg.定时器计数,PWM输出,AD采集……)电路所配置的寄存器,数据寄存器都在芯片,操作芯片直接读写即可
STM内部芯片没有的功能需要外挂芯片完成,其数据在STM32外部,获取方式是:在俩个设备之间连接一根或多根通讯线,通过通讯线路发送或者接收数据,完成数据交换从而实现控制外挂模块和读取外挂模块数据。
- 通信方式:设备与设备之间的连接方式
串行 : 一根或几根线,一次传1个比特(bit)。 节省引脚,适合远距离(相对并行),是主流。UART, I2C, SPI, USB, CAN都是串行。
并行: 多根线(如8根, 16根),一次传多个比特(一个字节或字)。 速度快,但引脚占用多,线间干扰大,距离短。STM32内部总线常用,外部用得少了(如老式打印机接口)。
有线 vs 无线:STM32本身通常处理有线协议,无线(如WiFi, BLE)常通过额外模块(如ESP8266, HC-05)用UART/SPI连接STM32。
- 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
举个例子:考试的时候想给别人传答案,就可以和对方约定一个通信协议。比如先咳嗽一声,代表通信开始,然后竖一个手指,代表发生a,竖两个手指代表发生b,竖三个手指代表发送c,然后挥一挥手,代表通讯结束。这就是一种通信方式。
通信的目的是进行信息传递。双方约定的规则,就是通信协议。
|
名称 |
引脚 |
双工 |
时钟 |
电平 |
设备 |
|
USART |
TX、RX |
全双工 |
异步 |
单端 |
点对点 |
|
I2C |
SCL、SDA |
半双工 |
同步 |
单端 |
多设备 |
|
SPI |
SCLK、MOSI、MISO、CS |
全双工 |
同步 |
单端 |
多设备 |
|
CAN |
CAN_H、CAN_L |
半双工 |
异步 |
差分 |
多设备 |
|
USB |
DP、DM |
半双工 |
异步 |
差分 |
点对点 |
以上通信接口STM32F103C8T6都支持,表只列举最典型参数和配置。
1.1通信接口(5个)
- UART (Universal Asynchronous Receiver/Transmitter): 最基础的点对点串行协议。像两个人面对面聊天。
TX (Transmit): 发数据的引脚。STM32的嘴巴。
RX (Receive): 收数据的引脚。STM32的耳朵。
(可选) RTS/CTS: 硬件流控引脚,用于协调发送速度(防止数据淹没对方)。
(uart是通用异步收/发器,usart通用同步/异步收/发器,相较于UART多一个时钟输出,无时钟输入)
- I2C (Inter-Integrated Circuit): 多设备共享“总线”的低速串行协议。像一个小组讨论,大家共用一条电话线,靠“名字”(地址)区分。
SCL (Serial Clock): 时钟线。由主设备产生,像打拍子指挥节奏。大家共用。
SDA (Serial Data): 数据线。主从设备都通过它收发数据。大家共用。
注意: I2C总线需要上拉电阻(通常4.7KΩ)连接到电源(3.3V),让总线在空闲时保持高电平。
- SPI (Serial Peripheral Interface): 高速全双工串行协议,通常点对点或点对少量设备。像两个人用两条专用电话线同时说和听。
SCK (Serial Clock): 时钟线。由主设备产生,指挥节奏。
MOSI (Master Out Slave In): 主设备发,从设备收的数据线。
MISO (Master In Slave Out): 主设备收,从设备发的数据线。
NSS / CS (Slave Select / Chip Select): 片选线。由主设备控制,低电平有效。拉低哪条线,就选中哪个从设备进行对话。主设备可以有多个CS引脚连接不同从设备。
- USB (Universal Serial Bus): 复杂但强大的通用串行总线协议。
DP (Data Positive)
DM (Data Negative)
专用差分信号线对,需要符合USB规范的硬件设计。
- CAN (Controller Area Network): 主要用于汽车、工业的可靠网络协议。
CAN_H (CAN High)
CAN_L (CAN Low)
专用差分信号线对,需要终端电阻(120Ω)。
1.2双工模式
-
比喻:对话的方向限制。是只能一方说(单工)?还是能轮流说(半双工)?还是能同时说和听(全双工)?
-
单工: 数据只能单向流动。例如:只读传感器、广播(电台发送,收音机只能接收)。STM32较少纯粹单工。
-
半双工: 数据可以双向流动,但同一时间只能一个方向。就像对讲机,说完“Over”才能听。I2C是半双工 (SDA线同一时刻只能一个设备驱动)。CAN也是半双工 (总线竞争)。
-
全双工:数据可以同时双向流动。就像打电话,能边说边听。UART、SPI是典型全双工 (有独立的TX/RX线或MOSI/MISO线)。注意: SPI虽然是物理全双工,但实际应用中,有时从设备在收到主设备命令前并不知道要发什么数据,所以主设备发的可能是命令,从设备发的可能是数据,看起来像半双工,但硬件上具备同时收发的能力。
1.3时钟特性
-
比喻:对话的语速或节拍器。
-
同步通信 : 通信双方共用同一个时钟信号来同步数据的发送和接收。主设备提供时钟(SCK/SCL),像指挥拍子。SPI,I2C是同步通信。
优点: 速度快,抗干扰相对好(因为有时钟边沿采样)。
缺点: 需要额外的时钟线。时钟频率限制通信距离。
-
异步通信: 没有共享的时钟线。双方事先约定好一个速度(波特率),依靠数据帧内部的起始位、停止位来同步每个字节。UART是异步通信。
优点: 只需要两根数据线(TX/RX),适合远距离(相对)。
缺点: 需要精确的波特率发生器,双方波特率误差不能太大(通常<3%),速度相对同步通信慢。更容易受干扰(没有时钟边沿约束)。
-
波特率 (Baud Rate): 衡量异步通信速度的单位,表示每秒传输的符号数(1个符号通常代表1个比特bit)。常用值:9600, 19200, 38400, 115200等。波特率越高,速度越快,但对时钟精度和信号质量要求越高。
-
时钟频率 (SCK/SCL): 衡量同步通信速度的单位(Hz)。SPI可达几十MHz,I2C标准模式100KHz,快速模式400KHz,高速模式3.4MHz等。
1.4信号传输方式
- 单端信号
用1根信号线 + 公共地线(GND)传输数据。
逻辑状态由信号线对地(GND)的电压差决定:
逻辑1:信号线电压 > 高电平阈值(如3.3V系统为2.0V)
逻辑0:信号线电压 < 低电平阈值(如0.8V)
通俗比喻:像一个人用嗓门音量大小传递信息:
大喊(高电平) = 1
小声(低电平) = 0
参考点是环境背景噪音(相当于GND)。
|
优点 |
缺点 |
|
电路简单,成本低(少1根线) |
抗干扰能力差 |
|
功耗较低 |
传输距离短(通常<1米) |
|
适合低速通信 |
地线噪声直接影响信号 |
- 差分信号
用2根相位相反的信号线传输数据(无地线依赖):
D+(正向信号线)
D-(反向信号线)
逻辑状态由两根线之间的电压差决定:
逻辑1:D+电压 - D-电压 > +阈值(如+200mV)
逻辑0:D+电压 - D-电压 < -阈值(如-200mV)
通俗比喻:像两个人用反义词对暗号:
A说“高”,B说“低” → 表示1
A说“低”,B说“高” → 表示0
外界干扰同时影响两人(如噪音+10dB),但暗号差值不变。
|
优点 |
缺点 |
|
超强抗干扰(抑制共模噪声) |
电路复杂(多1根线) |
|
传输距离远(可达百米) |
功耗较高 |
|
高速传输(GHz级) |
成本更高 |
|
降低电磁辐射(EMI) |
需阻抗匹配 |
|
特性 |
单端信号 |
差分信号 |
|
信号线数 |
1根 + GND |
2根(D+和D-) |
|
抗干扰 |
弱(依赖GND质量) |
极强(抵消共模噪声) |
|
传输距离 |
短(<1米) |
长(可达100米+) |
|
速度 |
低速(通常<10Mbps) |
高速(可达Gbps) |
|
成本 |
低 |
高(多1根线+专用芯片) |
|
噪声辐射 |
高(电流经地线形成环路) |
低(磁场相互抵消) |
1.5设备特性
点对点:俩个设备直接传输数据
多设备:需要寻址以确定通信对象
二、串口通信
-
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信
-
单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力

USB转串口模块:芯片型号CH340(将串口协议转换为USB协议),实现串口和电脑通信。
陀螺仪传感器模块:可测量角速度,加速度等姿态参数,左右各四个引脚,一边串口引脚,一边I2C引脚。
蓝牙串口模块:四个角是串口通信引脚,芯片可以和手机互联,实现手机遥控单片机功能。
2.1硬件电路
-
简单双向串口通信有两根通信线(发送端TX和接收端RX)
-
TX与RX要交叉连接
-
当只需单向的数据传输时,可以只接一根通信线
一般串口通信的模块有四个引脚,VCC和GND供电,TX和RX通信且为单端信号,它们的高低电平是相对于GND,因此GND严格上也是通信线。所以串口通信的RX、TX、GND必须连接。如果两个设备都有独立供电,VCC可以不接。
如果其中一个设备没有供电,比如设备1是STM32,设备2是蓝牙串口模块。STM32有独立供电,蓝牙串口没有独立供电,就需要把蓝牙串口的VCC和STM32的VCC接在一起。STM32通过VCC-VCC这根线向右边的子模块供电。供电电压需要注意,是按照子模块要求。
-
当电平标准不一致时,需要加电平转换芯片
3.3V器件 ↔ 5V器件:
5V输出到3.3V输入:需分压电阻或电平转换芯片。
3.3V输出到5V输入:通常可直接连接。
TTL ↔ RS232:必须使用电平转换芯片(如MAX3232)。

2.2电平标准
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
-
TTL电平:+3.3V或+5V表示1,0V表示0
-
RS232电平:-3~-15V表示1,+3~+15V表示0
一般在大型机器使用,由于环境可能恶劣,静电干扰较大,因此电瓶电压比较大,允许波动范围也大。
-
RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)
差分信号抗干扰能力强。其通信距离可达上千米。
2.3串口参数
-
波特率:串口通信的速率
串口一般异步通信,需要双方确定通信速率(波特率,每秒传输码元的个数,二进制情况下,一个码元为一个比特,此时波特率=比特率)
每位比特(bit)的持续时间 T = 1 / 波特率。
优先选用 11.0592MHz 晶振(可被常用波特率整除)
72MHz系统时钟下,9600波特率误差仅0.14%(推荐)
-
起始位:标志一个数据帧的开始,固定为低电平
-
数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
若发送数据超过设定位数,高位会被截断。
-
校验位:用于数据验证,根据数据位计算得来
三种方式:无校验,奇校验(数据位+校验位=奇数个1),偶校验
但是若俩个数据发送过程都出错,奇偶校验就检测不出来错误。
串口助手中,数据位为有效载荷,校验位是独立的1位
-
停止位:用于数据帧间隔,固定为高电平
2.4串口数据帧整体结构(传输数据最小单位)
串口发送的字节格式,由串口协议规定。
串口每个字节都装载在一个数据帧,每个数据帧都由起始位,数据位和停止位组成。数据位8个代表一个字节8位。
数据帧结构:
空闲态:
-
通信未开始时,TX/RX线保持高电平(逻辑1)。
起始位(Start Bit):
-
1个比特的低电平(0),向接收方宣告“数据开始传输!”
-
关键作用:实现帧同步(接收方检测到下降沿即启动计时)。
数据位(Data Bits):
-
5~9个比特(通常选8位,兼容ASCII)
-
传输顺序:LSB(最低位)先传!
例如发送字符 'A' (0x41 = 二进制 01000001):
实际传输顺序为 1→0→0→0→0→0→1→0(从D0到D7)
校验位(Parity Bit):
-
可选,1个比特,用于检错(不能纠错):
-
奇校验(Odd):数据位+校验位的“1”总数=奇数
-
偶校验(Even):数据位+校验位的“1”总数=偶数。
-
例:0x41 (01000001)有2个“1”(偶数),若用偶校验则校验位=0
停止位(Stop Bit):
-
1~2个比特的高电平(1) ,表示“本帧结束”
-
强制拉高,为下一帧起始位的下降沿做准备。

数据位8位,无校验位

数据位9位,有校验位
2.5串口通信实际波形

第一个波形,发送字节数据0x55,波特率9600,每位时间是1/9600,大概104us。没发送数据时为空闲状态-高电平。数据帧开始先发送起始位,产生下降沿代表数据帧开始。数据0x55转为二进制,低位先行(依次发送1010 1010)。这个参数是八位数据,1位停止,无校验。没有校验位,后面的停止位把引脚置回高电平,数据帧完成发送。
STM32中,根据字节数据反转高低电平是USART外设自动完成,若软件模拟产生此波形,定时器104us+GPIO_WriteBit置高低电平。TX引脚发送是置高低电平,RX引脚接收为读取高低电平,也可以是USART外设自动完成,若软件模拟,定时调用GPIO_ReadInputDataBit读取每一位+接收使用外部中断(起始位下降沿触发)+对齐采样位采取8次。
其余波形也是如此(+校验位)
右下角俩个波形可以看出:串口停止位可以配置(1/1.5/2位)中间没有空闲状态
总结:串口通信——TX引脚输出定时翻转的高低电平,RX引脚定时读取引脚的高低电平。每个字节的数据加上起始位、停止位、可选的校验位,打包成数据帧依次输出在TX引脚,另一端RX引脚依次接收,就完成字节数据的传递。
三、USART
3.1USART简介
-
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器
相较于UART模式,多一个时钟输出。
USART=UART+同步通信扩展功能
USART 与 UART 的本质区别
特性
UART
USART
通信模式
仅异步
同步 + 异步
时钟线
无
同步模式需SCLK引脚
协议支持
基础串行协议
支持LIN、智能卡、IrDA等
硬件复杂度
简单
更复杂(寄存器更多)
STM32资源
部分型号仅有UART
主流型号均有USART
-
USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
-
自带波特率发生器,最高达4.5Mbits/s
波特率发生器相当于一个分频器。比如APB2总线72MHz,波特率发生器极性分频,得到波特率时钟(通信波特率),在此时钟下进行接收发送波形。波特率常用9600和115200.
-
可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
-
可选校验位(无校验/奇校验/偶校验)
-
支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN
-
STM32F103C8T6 USART资源: USART1(APB2)、 USART2、 USART3
3.2USART六大高级功能
1.多处理器通信(地址标记)
-
用途:一主多从通信(类似I2C寻址)
-
原理:
发送特殊地址帧唤醒目标从机
从机忽略非自身地址的数据帧
-
配置:USART_CR2 寄存器的 ADD0~3 设置自身地址
2. 硬件流控(RTS/CTS)
-
引脚:
RTS:输出,指示“本机准备好接收”
CTS:输入,检测“对方是否允许发送”
-
作用:防止接收缓存溢出(尤其高速传输)
3. LIN总线支持
-
LIN Break检测:自动识别13位低电平起始符
-
同步场生成:硬件自动发送 0x55 同步字节
-
配置:USART_CR2 寄存器的 LINEN 使能
4. 智能卡模式(ISO 7816)
-
支持T=0/T=1协议(SIM卡、金融IC卡)
-
自动生成ETU(Elementary Time Unit)时钟
-
硬件校验奇偶错误并重传
5. IrDA红外编码
-
内置编解码器:将数据转为3/16位脉宽红外信号
-
支持SIR(115.2kbps)和MIR(1.152Mbps)速率
6. DMA联动
-
解放CPU:自动搬运大量数据到USART缓存
3.3USART框图

3.3.1STM32 USART寄存器精要
|
寄存器 |
作用 |
关键位域 |
|
USART_SR |
状态寄存器 |
RXNE(接收缓存非空), TC(发送完成), PE(校验错) |
|
USART_DR |
数据寄存器(读写合一) (TDR发送+RDR接收) |
写入数据启动发送,读取获取数据 |
|
USART_BRR |
波特率寄存器(16位) |
存储 USARTDIV 值 |
|
USART_CR1 |
控制寄存器1 |
UE(使能), M(字长), PCE(校验使能) |
|
USART_CR2 |
控制寄存器2 |
STOP(停止位), CLKEN(同步时钟使能) |
3.3.2发送接收控制器—数据寄存器(DR)
两个数据寄存器——发送数据寄存器TDR(Transmit DR)和接收数据寄存器RDR(Receive DR)。两个寄存器占用同一个地址(与51单片机串口的SBUF寄存器相似)。在程序上只表现为一个寄存器——数据寄存器DR。但实际硬件中是分成两个寄存器,一个用于发送TDR,一个用于接收RDR。TDR只写,RDR只读。当进行写操作时,数据就写到TDR,当进行读操作时,数据就从RDR读出。
发送器控制
控制发送移位寄存器工作
负责把CPU准备好的数据通过TX引脚一位一位地发送出去(比如发送一个字节0x55)
工作流程(发快递)=发送数据流程
发送数据流程
发送数据寄存器TDR:类似于快递站“打包台”(临时存放待发送的快递)
发送移位寄存器:快递站的“传送带”(把包裹逐个送出)

关键状态标志位(USART_SR寄存器)
TXE(Transmit Data Register Empty):
-
TXE=1:打包台空着,可以放新快递(CPU可写入新数据)
-
TXE=1:TDR为空(数据已转移到移位寄存器),可写入新数据
-
写入USART_DR后自动清零
-
-
TXE=0:打包台被占用(正在发送)
TC(Transmission Complete):
-
TC=1:所有快递已发出(包括最后的包装盒——停止位)
-
TC=1:移位寄存器发送完毕(包括停止位),一帧完成
-
读SR寄存器 + 写USART_DR可清零
-
-
TC=0:还在发送中
-
移位寄存器:像传送带一样,把数据从低位到高位逐个比特推出去
发送数据流程文字描述
在某时刻给TDR写入0x55,在寄存器中二进制存储0101 0101。此时,硬件检测到写入的数据并检查当前移位寄存器是否有数据正在移位。如果没有,0101 0101全部移动到发送移位寄存器,准备发送。
当数据从TDR移动到移位寄存器时,会置一个标志位TXE(TX Empty),发送寄存器空。我们检查这个标志位,如果置1了,我们就可以在TDR写入下一个数据了。若TXE=1,数据没有发送,但是数据已经从TDR转移到发送移位寄存器,从而可写入新数据。此时,发送移位寄存器会在发送器控制的驱动下向右移位,一位一位地把数据输出到TX引脚。这里是向右移位的,正好和串口协议规定的低位先行一致。
数据移位完成后,新的数据会再次自动地从TDR转移到发送移位寄存器。如果当前移位寄存器移位还没有完成,TDR的数据就会进行等待,一旦移位完成,就会立刻转移过来。
TDR和移位寄存器的双重缓存,可保证在连续发送数据时,数据帧之间不会有空闲,提高工作效率。简单来说,数据一旦从TDR转移到移位寄存器了,不管有没有移位完成,都立刻把下一个数据放在TDR里等着,一旦移位完成,新的数据就会立刻跟上。
接受控制器
控制接收移位寄存器工作
负责把RX引脚一位一位地接收数据,并组装成完整的字节交给CPU(比如接收0XAA)
工作流程(收快递)=接收数据流程
接收数据流程
接收移位寄存器:快递站的“扫描仪”(逐个检查收到的快递)
接收数据寄存器RDR:快递站的“暂存架”(存放已扫描的完整快递)

关键状态标志位(USART_SR寄存器)
RXNE=1:暂存架上有快递待取(CPU需及时读取)
移位寄存器:像扫描仪一样,从低位到高位逐个比特组装数据
错误处理:
-
停止位不对 → 标记为"破损快递"(帧错误FE)
-
暂存架未取又来新快递 → 标记"爆仓"(溢出错误ORE)
数据从RX引脚通向接收移位寄存器,在接收器控制的驱动下一位一位的读取RX电平,先放在最高位,然后往右移,移位八次之后就能接收一个字节。由于串口协议规定低位先行,因此接收移位器从高位往低位方向移动。当一个字节移位完成,一整个字节就会转移到接收数据寄存器RDR。在转移过程中产生标志位RXNE,接收数据寄存器非空。当RXNE=1时,可将数据读取。同样俩个寄存器进行缓存,当数据从移位寄存器转移到RDR时,可直接移位接收下一帧数据。
发送有帧头和帧尾,接收需要剔除它们,电路内部自动执行。
双缓冲机制的优势
为什么需要两个寄存器?
|
场景 |
单寄存器方案 |
双寄存器方案(TDR+RDR) |
|
发送时 |
必须等全部发完才能写新数据 |
数据装上传送带后立刻可写下一包 |
|
接收时 |
必须立刻取走否则丢数据 |
前一包在暂存架时仍能收下一包 |
|
效率 |
低 |
高(实现"流水线"作业) |
类比:就像快递站有两个工作台:
一个在打包新快递(TDR),另一个在送出已打包的快递(移位寄存器)
互不干扰,效率翻倍!
常见问题:
Q1: 为什么发送时要先检查TXE?
A:就像快递站——只有打包台空着(TXE=1),才能放新快递,否则会覆盖未处理的快递
Q2: 数据为什么要从低位(LSB)开始传?
A:就像写数字"123"——先写个位"3",再十位"2",最后百位"1",接收方反过来组装即可还原
Q3: 移位寄存器怎么知道何时停止?
A:根据配置的"数据位数"(如8位)——就像扫描仪知道快递单号有几位数字。
Q4:为什么发送时要用 TXE 和 TC 两个标志?
TXE=1(打包台空着) 只表示可以放新数据到TDR,但可能还有数据在移位寄存器没发完。
TC=1 (所有快递发送完毕)表示所有数据(包括停止位)已经全部发出。
Q5:接收数据时为什么会丢数据?
原因1:CPU没有及时读取 USART_DR(导致 ORE 溢出错误)。
原因2:波特率不匹配(比如STM32设成115200,但对方发的是9600)。
解决方法:
- 使用DMA自动接收数据。
- 确保双方波特率一致。
Q6:发送和接收能同时进行吗?
可以! USART是 全双工 的,发送和接收完全独立(有各自的寄存器和引脚)。
发送 vs 接收控制器对比
|
功能 |
发送控制器 |
接收控制器 |
|
核心寄存器 |
TDR(发送数据寄存器) |
RDR(接收数据寄存器) |
|
移位寄存器 |
把数据逐位推到TX引脚 |
从RX引脚逐位读取数据 |
|
关键标志 |
TXE(可写新数据)、TC(发送完成) |
RXNE(有数据可取)、FE/PE/ORE(错误) |
|
数据顺序 |
LSB(最低位)先发 |
LSB(最低位)先收 |
|
触发方式 |
CPU写入 USART_DR 启动发送 |
检测起始位(下降沿)自动启动接收 |
-
发送控制器:把数据从 TDR → 移位寄存器 → TX引脚 逐位发出(LSB先发)。
-
接收控制器:从RX引脚 → 移位寄存器 → RDR 逐位接收(LSB先收)。
-
状态寄存器(USART_SR) 就像 仪表盘,告诉你当前状态(能不能发/有没有数据/是否出错)。
3.3.3状态寄存器(SR)
状态寄存器(USART_SR) 就像是USART模块的“工作状态仪表盘”,实时显示发送、接收、错误等各种状态。它的每个标志位都像是一个小灯泡,亮起(=1)时表示某种状态发生。
状态寄存器核心标志位
|
标志位 |
名称(英文全称) |
作用(通俗解释) |
谁负责点亮它? |
如何熄灭它? |
|
TXE |
Transmit Data Register Empty |
发送快递台空啦! (可以放新数据了) |
当TDR数据转移到移位寄存器时 |
写入新数据到USART_DR自动熄灭 |
|
TC |
Transmission Complete |
所有快递发完啦! (包括最后的包装盒-停止位) |
移位寄存器发完最后一比特时 |
读SR寄存器 + 写USART_DR |
|
RXNE |
Receive Data Register Not Empty |
暂存架上有快递! (快来取数据) |
当移位寄存器存满一帧数据到RDR时 |
读取USART_DR自动熄灭 |
|
ORE |
Overrun Error |
爆仓啦! (新快递把旧快递挤掉了) |
RDR数据未读又收到新数据时 |
读SR + 读USART_DR |
|
FE |
Framing Error |
快递包装破损! (停止位不对) |
检测到停止位为低电平时 |
读SR + 读USART_DR |
|
PE |
Parity Error |
快递单号对不上! (校验错误) |
奇偶校验失败时 |
读SR + 读USART_DR |
工作状态实时演示

状态寄存器的三大关键作用
通知CPU该干啥
-
亮TXE灯:提醒CPU"可以发下一个数据了"
-
亮RXNE灯:提醒CPU"快来取数据"
报告错误情况
-
亮FE灯:警告"对方发来的数据格式不对"
-
亮ORE灯:警告"你取数据太慢了!"
控制DMA传输
-
TXE/RXNE标志可直接触发DMA请求,实现"自动搬运数据"
状态寄存器使用避坑指南
TXE vs TC的区别
-
TXE=1:仅表示可以放新数据(可能还有数据在移位寄存器未发完)
-
TC=1:表示所有数据(含停止位)已完全发出
清除标志的玄机
-
TXE:写入DR自动清除
-
TC:需要读SR + 写DR(两步操作!)
-
错误标志(ORE/FE/PE):需要读SR + 读DR
DMA模式下的特殊行为
-
启用DMA发送时,TXE标志不会置1(由DMA自动维护)
-
但TC标志仍会正常置位,可用于判断发送完成
标志位更新时序
TXE置位时刻:刚好在数据从TDR加载到移位寄存器时
TC置位时刻:停止位发送完成后立即发生
RXNE置位时刻:停止位采样正确的瞬间
|
事件 |
标志位变化 |
延迟周期(72MHz系统) |
|
写入USART_DR |
TXE立即清零 |
0 |
|
TDR→移位寄存器 |
TXE置位 |
1-2 |
|
停止位发送完成 |
TC置位 |
1 |
|
RX引脚采样完成 |
RXNE置位 |
1 |
硬件连接关系
USART_SR ← 发送控制器/接收控制器/错误检测单元
USART_SR → 中断控制器/NVIC
USART_SR ↔DMA控制器(通过TXE/RXNE触发请求)
3.4USART工作原理
1.异步模式(UART模式)
-
无时钟线,靠起始位/停止位同步
-
数据帧结构:起始位 + 数据位(8~9) + 校验位 + 停止位
-
波特率由内部定时器生成(依赖APB总线时钟)
2. 同步模式
-
有时钟线(SCLK),由主设备(通常是STM32)产生
-
数据在时钟边沿采样(类似SPI),支持全双工
-
可配置时钟极性(CPOL)和相位(CPHA)
3. 数据收发引擎
-
发送器:并行数据 → 移位寄存器 → 按比特串行输出
-
接收器:采样RX信号 → 移位寄存器 → 数据校验 → 存入缓存
3.5波特率发生器
-
USART模块的“心跳控制器”,决定数据发送和接收的速度节奏,确保通信双方以相同的速率传输数据
-
定义:每秒传输的符号数(1符号=1比特),单位是bps(比特/秒)
-
常见值:9600、115200(数值越大速度越快)
-
关键公式:
每位持续时间(秒) = 1 / 波特率
例如:115200波特率 → 每位持续 8.68μs
波特率发生器结构

时钟源:来自STM32的APB总线(如APB1=72MHz)
分频器:16位寄存器(USART_BRR)计算分频系数
输出:生成发送和接收所需的精确时钟
计算公式:
USARTDIV = f_PCLK / (16 * 波特率)
f_PCLK1/2:APB总线时钟频率(如72MHz)
USARTDIV:写入USART_BRR的值
分频值拆分:
整数部分:USART_BRR[15:4]
例:39 → 0x27小数部分:USART_BRR[3:0](步长0.0625)
例:0.0625 → 0x1, 0.125 → 0x2
工作流程
1.发送时钟控制

2.接收采样时钟
3次采样抗干扰:在每位中点附近采样3次,取多数值
问题:为什么波特率通信不稳定?
1.APB时钟配置错误(可能误用HSI内部时钟)
2.BRR计算未四舍五入(小数部分处理不当)
3.6硬件数据流控 (Hardware Flow Control)
USART通信的“交通警察”,通过nRTS(Request To Send请求发送)和nCTS(Clear To Send清除发送)俩跟信号线,防止数据拥堵丢失
硬件流控核心概念
1. 两根关键信号线
|
信号线 |
方向 |
作用(低电平有效) |
等效比喻 |
|
nRTS |
输出 |
告诉对方:“我的接收缓冲区有空,可以发数据” |
快递仓库的“有空位”指示灯 |
|
nCTS |
输入 |
检测对方:“是否允许我发送数据?” |
快递员的“绿灯通行”信号 |
2.工作逻辑

3.完整工作流程

硬件流控的三大核心机制
1. nRTS触发阈值
- STM32自动控制:当接收缓冲区达到预设阈值时自动拉高/拉低nRTS
- 阈值配置(通过USART_RTOR寄存器):USART1->RTOR = 0x10;
- // 当空闲空间<16字节时拉高nRTS
2. nCTS响应时间
- 实时检测:发送前检查nCTS,若为高电平则阻塞发送
- 典型响应延迟:<2个波特周期(115200波特率下约17μs)
3. 错误恢复
|
错误类型 |
触发条件 |
处理方式 |
|
CTS超时 |
nCTS持续高电平>1ms |
触发中断,重新初始化通信 |
|
RTS冲突 |
nRTS被意外拉高 |
检查缓冲区是否溢出 |
3.7中断控制
-
通信系统的智能警报器,当事件发生时(数据送达/发送完成/出现错误),立即通知CPU处理。
-
中断触发条件
|
中断类型 |
触发标志位 |
典型应用场景 |
比喻解释 |
|
TXE |
USART_SR.TXE |
发送寄存器空,可填充新数据 |
快递员喊:“箱子空了,快装货!” |
|
TC |
USART_SR.TC |
一帧数据完全发送完毕 |
快递站通知:“包裹已全部发出!” |
|
RXNE |
USART_SR.RXNE |
接收到新数据 |
收件人收到短信:“快递到货!” |
|
ORE |
USART_SR.ORE |
数据溢出(旧数据被覆盖) |
仓库报警:“货物堆积爆仓了!” |
|
IDLE |
USART_SR.IDLE |
检测到总线空闲(无数据) |
快递员报告:“今天没货送了” |
-
中断优先级:可配置4个抢占级别
-
发送中断流程

-
接收中断流程

TXE要数据,TC报完成,RXNE催取件,ORE喊救命
3.8SCLK控制 (Synchronous Clock Control)
SCLK基础概念
1. 同步 vs 异步模式
|
特性 |
异步模式(UART) |
同步模式(USART+SCLK) |
|
时钟线 |
无 |
需SCLK引脚 |
|
速率 |
较低(通常<1Mbps) |
更高(可达SPI级速度) |
|
适用场景 |
简单设备间通信 |
需精确时钟同步的设备 |
2.SCLK引脚作用
-
主模式:STM32输出时钟信号(驱动从设备)
-
从模式:STM32接收外部时钟(需外部主设备)
3.工作流程
同步发送时序(主模式)

同步接受时序(从模式)

SCLK三大核心参数:
1. 时钟极性(CPOL) - 空闲状态电平
2. 时钟相位(CPHA) - 数据采样边沿
3. 频率控制(BRR) - 通过波特率寄存器调节
调试口诀:
“SCLK不正常,先查CPOL/CPHA,再量时钟频率”
四种时钟模式(类似SPI)
|
模式 |
CPOL |
CPHA |
数据采样边沿 |
数据变化边沿 |
|
0 |
0 |
0 |
上升沿 |
下降沿 |
|
1 |
0 |
1 |
下降沿 |
上升沿 |
|
2 |
1 |
0 |
下降沿 |
上升沿 |
|
3 |
1 |
1 |
上升沿 |
下降沿 |
3.9TE使能
USART发送功能总开关,控制整个发送通道的激活与关闭
TE使能的核心作用
|
行为 |
TE=0(关闭) |
TE=1(使能) |
|
TX引脚状态 |
高阻态或保持最后电平 |
正常输出数据 |
|
数据发送 |
禁止发送(忽略写入DR) |
允许发送 |
|
移位寄存器操作 |
冻结 |
正常工作 |
|
中断/DMA触发 |
不产生TXE/TC中断 |
正常触发 |
TE使能的四大关键特性
硬件自动管理空闲状态
TE使能后,TX引脚自动拉高(空闲状态),直到发送第一个起始位。
首次写入DR的同步作用
第一次写入USART_DR会触发:起始位(低电平);后续数据位按LSB-first发出
关闭时的缓冲保护
若TE由1→0时移位寄存器仍在发送:会完成当前帧传输(包括停止位),之后TX引脚进入高阻态
与DMA的联动
即使TE=1,如果DMA未配置,TXE不会自动触发DMA请求
3.10唤醒单元
作用:实现串口挂载多设备
功能:给串口分配地址。串口一般时点对点通信,仅支持俩个设备互相通信。多设备在一条线上可以接都哦个设备,每个设备分配一个地址,想跟某个设备通信,先寻址确定通信对象在进行数据收发。当发送指定地址时,设备唤醒开始工作。没有接收到的地址保持沉默。
3.11USART基本结构

串口数据收发过程
波特率发生器用于产生约定的通信速率。时钟来源是PCLK2或1。经过波特率发生器分频后,产生的时钟通向发送控制器和接收控制器,发送控制器和接收控制器用来控制发送移位和接收移位。
之后由发送数据寄存器和发送移位寄存器这两个寄存器的配合,将数据一位一位地移出去,通过GPIO口的复用输出(引脚图),输出到TX引脚,产生串口协议规定的波形。
当数据由数据寄存器转移到移位寄存器时,会置TXE标志位,判断标志位就可以知道是否可以写下一个数据。
接收部分与之类似,RX引脚的波形通过GPIO口输入,在接收控制器的控制下,一位一位地移入接收移位寄存器。由于低位先行,因此数据要从左边开始移进来,移完一帧数据后,数据就会统一转运到接收寄存器,在转移的同时置RXNE标志位,检查标志位就可以知道是否收到数据。同时这个标志位也可以去申请中断,这样就可以在收到数据时直接进入中断函数,之后快速地读取和保存数据。
四、数据帧
4.1字节设置

字长=数据位长度(包含校验位)
9位字长波形:
第一条时序是TX发送或者RX接收的数据帧格式。空闲高电平,起始为零,接着根据写入的数据置1或置0,依次发送位0到位8,最后停止位1,数据帧结束。一般选择8位数据位(一个字节)+1位校验位
第二条时钟是同步时钟输出功能。每个数据位的中间都有一个时钟上升沿,时钟的频率和数据速率一样。接收端可以在时钟上升沿进行采样,就可以精准定位每一位数据。时钟的最后一位可以通过BCL位控制要不要输出。时钟的相位极性可通过配置寄存器配置。
第三条时空闲帧1
第四条时断开帧0
三四俩条时局域网协议使用。
8位字长波形:
一般8位数据位(一个字节)+无校验位
4.2配置停止位

STM32的串口可以配置停止位长度为0.5、1、1.5、2四种。
1个停止位,停止位的时长=数据位的1位时长
1.5个停止位,停止位时长=1.5倍数据位1位时长
2个停止位的,停止位时长=2倍数据位1位时长
0.5个停止位,停止位时长=0.5倍数据位1位时长
一般选择1位停止位
对于串口来说,串口的输出TX比输入RX简单很多。
输出只要定时翻转TX引脚高低电平即可。
输入不仅要保证输入的采样频率和波特率一致,还要保证每次输入采样的位置,正好处于每一位的正中间。只有在每一位的正中间采样,高低电平读进来才是最可靠的。如果采样点过于靠前或靠后,有可能高低电平还正在翻转,电平还不稳定,或者稍有误差,数据容易采样错。
此外,输入还要对噪声有一定的判断能力。
输入RX采样方法:
4.3起始位侦测

当输入电路侦测到一个数据帧的起始位后,就会以波特率的频率连续采样一帧数据。同时从起始位开始采样位置就要对齐位的正中间。
为了实现这一功能,输入对电路采样时钟进行了细分,以波特率的16倍频率进行采样。
最开始空闲状态高电平,采样为1,在某个位置采样到0(出现下降沿)且没有噪音,就为起始位。在起始位进行16次采样,没有噪音位0,为了采集准确,根据手册描述,接收电路还会在下降沿之后的第三次、五次、七次进行一批采样。
在第八次、九次、十次再进行一批采样,且这两批采样都要要求每三位里面至少应该有两个零。
①没有噪声置0,满足情况。
②有一些轻微的噪声,导致起始位中三位里有两个0,另一个是1,符合情况,并且在状态寄存器置NE(Noise Error)噪声标志位。若起始位中三位里有一个零,不符合情况。可猜测下降沿是噪声导致,电路需要重新捕捉下降沿。
若起始位侦测正确,接收状态从空闲变为接收起始位。同时,第八、九、十次采样的位置,正好是起始位的正中间,之后接收数据位时,就都在第八、九、十、次进采样。由此保证采样位置在位的正中间。
4.4数据采样

数据位的时间长度为16。一个数据位有十六个采样时钟,由于起始位侦测已经对齐了采样时钟,因此可直接在第八、九、十、次采样数据位,为保证数据的可靠性,连续采样三次。
没有噪声的理想情况下,这三次全为1或者全为0。
全为1就认为收到了1,全为0就认为收到了0。
如果有噪声导致三次采样不是全为1或者全为0,就按照2:1的规则确定:两次为1就认为收到了1,两次为0就认为收到了0。在这种情况下,噪声标志位NE也会置1。
4.5波特率发生器
- 发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
- 计算公式:波特率 = fPCLK2/1 / (16 * DIV)

五、USART初始化
5.1初始化流程
①开启时钟,需要GPIO时钟和USART时钟
②GPIO初始化,把TX配置成复用输出,RX配置成输入
③配置USART,使用结构体
④若只需要发送功能,直接开启USART
⑤若需要接收功能,需要配置中断,(NVIC+ITConfig),再开启USART
初始化完成
发送数据:调用发送函数
接收数据:调用接收函数
获取发送和接受状态:调用标志位函数
5.2USART库函数
USART基本库函数
①USART恢复省配置(恢复默认初始状态)
void USART_DeInit(USART_TypeDef* USARTx);
②USART初始化
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
③USART结构体初始化
void USART_StructInit(USART_InitTypeDef* USART_InitStruct);
④配置同步时钟输出
void USART_ClockInit(USART_TypeDef* USARTx, USART_ClockInitTypeDef* USART_ClockInitStruct);
void USART_ClockStructInit(USART_ClockInitTypeDef* USART_ClockInitStruct);
⑤USART使能
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
⑥USART中断输出配置
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);
⑦开启USART到DMA的触发通道
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);
⑧发送数据(写DR寄存器)
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
⑨接收数据(读DR寄存器)
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
⑩标志位相关函数
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);
5.3USART初始化代码
#include "stm32f10x.h" // Device header
void serial_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Mode=GPIO_Mode_AF_PP;//TX使用复用推挽输出
GPIO_Initstructure.GPIO_Pin=GPIO_Pin_9;
GPIO_Initstructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_Initstructure);
USART_InitTypeDef USART_Initstructure;
USART_Initstructure.USART_BaudRate=9600;//波特率
USART_Initstructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//硬件流控制
/*
#define USART_HardwareFlowControl_None 不使用流控
#define USART_HardwareFlowControl_RTS 只用CTS
#define USART_HardwareFlowControl_CTS 只用RTS
#define USART_HardwareFlowControl_RTS_CTS CTSRTS都使用
*/
USART_Initstructure.USART_Mode=USART_Mode_Tx;//USART模式
/*
#define USART_Mode_Rx 发送模式
#define USART_Mode_Tx 接收模式
*/
USART_Initstructure.USART_Parity=USART_Parity_No;//校验位
/*
#define USART_Parity_No 无校验
#define USART_Parity_Even 偶校验
#define USART_Parity_Odd 奇校验
*/
USART_Initstructure.USART_StopBits=USART_StopBits_1;//停止位
/*
#define USART_StopBits_1
#define USART_StopBits_0_5
#define USART_StopBits_2
#define USART_StopBits_1_5
*/
USART_Initstructure.USART_WordLength=USART_WordLength_8b;//字长
/*
#define USART_WordLength_8b
#define USART_WordLength_9b
*/
USART_Init(USART1,&USART_Initstructure);
USART_Cmd(USART1 ,ENABLE);
}
5.4发送数据
void serial_sendbyte(uint8_t byte)
{
USART_SendData(USART1,byte);//等待数据转移到移位寄存器,再进行数据传递,为了防止新发送的数据数据覆盖原先数据,等待标志位
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
//标志位是否手动清除
/*
TXE:发送数据寄存器空 (Transmit data register empty)
当TDR寄存器中的数据被硬件转移到移位寄存器的时候,该位被硬件置位。如果USART_CR1
寄存器中的TXEIE为1,则产生中断。对USART_DR的写操作,将该位清零。
0:数据还没有被转移到移位寄存器;
1:数据已经被转移到移位寄存器。
注意:单缓冲器传输中使用该位。
*/
//不需要手动清除标志位,在下一次的senddata时,标志位会自动清0
}
标志位是否手动清零?
TXE 标志位通常不需要(也不应该)手动清除。写入新数据到 USART_DR 寄存器是清除它的唯一且正确的方式,并且这个清除操作是自动发生的。
以下是关键点的解释
1.硬件置位 (Set by Hardware):
-
当硬件完成将
TDR(发送数据寄存器) 中的数据转移到移位寄存器(准备开始通过 TX 线一位一位地发送出去)时,硬件会自动将 TXE 标志位置为 1。 -
这个动作表示
TDR现在空了,可以接收新的数据了。 -
如果此时
TXEIE(TXE 中断使能) 位为 1,这个置位操作还会触发一个中断。
2.清零机制 (Clearing Mechanism):
-
文档明确说明:“对 USART_DR 的写操作,将该位清零”。
-
这是唯一的清除 TXE 标志位的方式。
-
当你向
USART_DR(数据寄存器,通常通过写这个寄存器来发送数据) 写入一个新的字节时:-
硬件首先将这个新字节加载到
TDR寄存器中。 -
然后,硬件自动地、立即地将 TXE 标志位清零 (0)。
-
-
你不需要(也无法)通过直接向标志位寄存器写 0 或 1 来手动清除 TXE 标志位。 尝试这样做要么无效,要么可能影响其他标志位。
5.5主函数程序
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "serial.h"
int main(void)
{
OLED_Init();
serial_init();
serial_sendbyte(0x44);
while(1)
{
}
}
5.6数据模式
- HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
- 文本模式/字符模式:以原始数据编码后的形式显示

字符和数据在发送和接收的转换关系

5.7SendByte函数分装
5.7.1发送数组
//发送数组
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
{
serial_sendbyte(Array[i]);
}
}
//main函数中的代码编写
uint8_t arr[]={0x41,0x42,0x43,0x44};
Serial_SendArray(arr,4);
5.7.2发送数字
//次方函数
uint32_t serial_pow(uint32_t x,uint32_t y)
{
uint32_t result=1;
while(y--)
{
result *=x;
}
return result ;
}
//发送数字
void serial_sendnumber(uint32_t number,uint8_t length)
{
uint8_t i;
for(i=0;i<length;i++)
{
serial_sendbyte(number/serial_pow(10,length-1-i)%10+0x30);//ASCII码中字符0对应0x30
}
}
//主函数代码
serial_sendnumber(12345,5);
5.7.3printf函数分装
//使用printf函数需要点击魔术棒Target->Use MicroLIB
//printf函数移植方法,最多使用方法
printf("num=%d\r\n",1234);//搭配fputc函数
int fputc(int ch,FILE *f)
{
serial_sendbyte(ch);
return ch;
}
//此方法printf只能有一个,重定向到串口1,串口2无法使用
//若每个串口想使用printf,可使用sprintf
//sprintf可以把格式化字符传输到一个字符串里
char string[100];
sprintf(string "num=%d\r\n",1234);
serial_sendstring(string);
//封装printf函数
//在serial.c文件添加头文件
#include "stdarg.h"
void serial_printf(char*format,...)
{
char string[100];
va_list arg;//定义参数列表变量
va_start(arg,format);//从format位置开始接收参数表,放在arg里
vsprintf(string,format,arg);//打印位置string 格式化字符串format,参数表为arg
serial_sendstring(string);
}
//主函数
serial_printf("Num=%d\n\r",666);
//printf汉字显示
第一种方法:
Target->Editor->Encoding->UTF-8
Target->C/C++->Misc Control填写:__no_multibyte-chars
串口助手接收区配置:文本编码:UTF-8
第二种方法:
Target->Editor->Encoding->GB232
将文件关掉,重新打开,中文打入显示宋体格式
串口助手接收区配置:文本编码:GBK
5.8串口发送和接收
serial.c函数修改不多
①GPIO口加PA10引脚,使用上拉输入
②USART结构体中模式加入接收模式
③接收串口可以是查询或者中断俩种方法
5.8.1串口接收查询方法
查询方法
serial.c文件中
需要改变的地方
Ⅰ
GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Mode=GPIO_Mode_AF_PP;//TX使用复用推挽输出
GPIO_Initstructure.GPIO_Pin=GPIO_Pin_9;
GPIO_Initstructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_Initstructure);
GPIO_Initstructure.GPIO_Mode=GPIO_Mode_IPU;//RX使用上拉输入
GPIO_Initstructure.GPIO_Pin=GPIO_Pin_10;
GPIO_Initstructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_Initstructure);
Ⅱ
USART_Initstructure.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;
//USART模式:同时开启发送和接收
/*
#define USART_Mode_Rx 发送模式
#define USART_Mode_Tx 接收模式
*/
uint16_t serial_getbyte(void)
{
uint16_t RXdata;
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
{
RXdata=USART_ReceiveData(USART1);
//是否需要清除标志位-不需要,DR寄存器清除
}
return RXdata;
}
main.c文件中
int main(void)
{
OLED_Init();
serial_init();
while(1)
{
OLED_ShowHexNum(1,1,serial_getbyte(),2);
}
}
5.8.2串口接收中断方法
中断方法
在serial.c文件中
添加相关中断函数
USART_ITConfig(USART1 ,USART_IT_RXNE,ENABLE );
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE ;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1 ,ENABLE);
中断函数配置
uint8_t serial_getRxfalg(void)//读取标志位自动清零
{
if(Serial_RXFlag==1)//如果接收到数据
{
Serial_RXFlag=0;//清楚标志位
return 1;//返回值为1,说明数据已经接收
}
return 0;//否则,就是数据没有接收
}
uint8_t serial_getRXdata(void)
{
return Serial_RXData;
}
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
{
Serial_RXData=USART_ReceiveData(USART1);//读取后置标志位
Serial_RXFlag=1;//读取成功,标志位置1
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
main.c文件中
uint16_t Rxdata;
int main(void)
{
OLED_Init();
serial_init();
OLED_ShowString(1,1,"RXdata:");
while(1)
{
if(serial_getRxfalg()==1)//标志位为1后,自动置0,除非再次接收数据,标志位置1
{
Rxdata=serial_getRXdata();
serial_sendbyte(Rxdata);//接收区,串口接收回传
}
OLED_ShowHexNum(1,8,Rxdata,2);
}
}
#以上俩种方法只能接收一个字节,但大部分情况是对大量数据进行回传,因此需要用数据包形式进行传输,接收部分也用数据包格式接收,以致接收多字节#
六、串口数据包
数据包:把一个个单独数据打包成一个整体,方便多字节数据通信。
但是当多字节连续发送时,由于接收方会从任意一个位置进行接收,接收方不知道数据的开头和结尾,容易出现数据错位现象,因此需要将数据分割成数据包。
数据包作用:将同一批数据进行打包和分割,方便接收方进行识别
6.1数据包分割方法
通常使用额外添加包头包尾(额外增加字节),不影响中间数据字节内容
HEX数据包
数据以原始字节数据本身呈现

6.1.1包头包尾和数据载荷重复问题
Q1:定义0XFF为包头,0XFE为包尾,如果传输的数据本身有FF或者FE怎么办?
A:解决方法:①限制载荷数据范围②若无法避免载荷数据和包头包尾重复,使用固定长度的数据包③增加包头包尾的数量,呈现载荷数据出现不了的状态
Q2:包头包尾一定都要设定吗?
不需要。可以只需要包头,删除包尾(数据包格式:包头FF+4个数据),检测到包头开始接收数据,接收四个数据后,置标志位,一个数据包接收完成(载荷数据重复会更严重)。
Q3:固定包长和可变包长如何选择?
对于HEX数据包,若出现载荷数据和包头包尾重复,选择固定包长;反之,数据和包头包尾不重复,选择可变包长。
Q4:如何将各种数据转换成字节流?
数据包由一个字节一个字节组成,若想发送16位整型数据、32位整型数据、float、double、结构体没有问题,只需要用uint8_t指针指向它,把它们当作字节数组发送即可。
文本数据包
每个字节经过一层编码和译码,呈现文本格式

接收到载荷数据,得到字符串,在软件中,对字符串进行操作和判断,可实现各种指令控制功能。通常以换行为包尾,打印时可一行行显示。
6.1.2HEX数据包和文本数据包比较
| 数据包 | HEX数据包 | 文本数据包 |
| 优点 | 传输直接,解析数据简单,适合一些模块发送原始数据 | 数据直观易理解,灵活,适合输入指令进行人机交互场合 |
| 适用模块 | 使用串口通信的陀螺仪、温湿度传感器 | 蓝牙模块AT指令、CNC、3D打印机G代码 |
| 缺点 | 灵活性不足,载荷容易和包头包尾重复 | 解析效率低 |
6.2串口数据包收发流程
6.2.1数据包发送
在HEX数据包中,定义数组,填充数据,调用发送数组的串口发送模块SendArray
在文本数据包中,写字符串,调用发送字符串的串口发送模块SendString
6.2.2数据包接收
由于每接收一个字节,程序会进一便中断,从中断拿数据再退出中断,因此,每拿到一个数据,都是一个独立过程。
而对于数据包来说,它具有前后关联性,对于包头、数据、包尾三个状态有不同的处理逻辑。
因此,在程序中设计一个能记住不同状态的机制,在不同状态执行不同操作,同时还要进行状态合理转移,此程序设计思维为“状态机”。
用状态机方式接收一个数据包。
使用状态机流程:定义状态→各个状态在何时进行转移→画好状态转移图→编程
下方为状态转移图👇
固定包长HEX数据包接收

定义三个状态并用一个变量标志:等待包头S=0、接收数据S=1、等待包尾S=2
执行流程:
①最开始S=0,收到一个数据,进中断;根据S=0,进入第一个状态的程序,判断数据包头是否为0XFF。若是FF,代表收到包头,置S=1,退出中断,结束;若不是FF,证明数据包没有对齐,等待数据包包头出现,此时S=0,下次进中断,还是判断包头的逻辑,直到出现FF,再进入下一个状态。
②接收到0XFF,再进入中断,根据S=1,进行接收数据程序。再收到的数据,直接存入数组,再使用另一个变量,记录接收数据的个数,若没有接收4个数据,一直是接收状态,直至接收4个数据,置S=2,下次进中断,直接进入下一个状态。
③判断接收数据是否为0XFE,若是,置S=0,回到最初状态,开始下一个轮回;若不是,进行重复等待包尾状态,直至接收正确包尾。
可变包长文本数据包接收

定义三个状态并用一个变量标志:等待包头S=0、接收数据等待包尾S=1、等待包尾S=2
执行流程:
①最开始S=0,等待包头状态,判断接收是否为规定的“@”符号,若收到,S=1进入接收状态
②S=1,接受等待包尾状态,依次接收判断数据,同时等待包尾。若接收到数据并判断不是“\r”,正常接收数据;若是“\r”,不接受,同时S=2进入等待包尾“\n”
③S=2,等待包尾状态,若接收到“\n”,S=0,开始下一个轮回
6.3串口收发固定包长HEX数据包
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "stdarg.h"
uint8_t Serial_RXData;
uint8_t Serial_RXFlag;
//定义俩个缓存区数组,四个数据只存储发送或者接收的载荷数据,不包含包头包尾
uint8_t Serial_TxPacket[4];
uint8_t Serial_RxPacket[4];
uint8_t Serial_packet_flag=0;
//----------------------------------------------
与serial.c代码一致
//----------------------------------------------
//发送数据的函数
void serial_sendbyte(uint8_t byte)
{
USART_SendData(USART1,byte);//等待数据转移到移位寄存器,再进行数据传递,为了防止新发送的数据数据覆盖原先数据,等待标志位
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]);
}
}
//发送HEX数据包
uint8_t serial_Packet_getRxfalg_return(void)
{
if(Serial_packet_flag==1)
{
Serial_packet_flag=0;
return 1;
}
return 0;
}
//调用函数,TXPack数组四个数据,自动加上包头包尾发送出去
void serial_SendPacket(void)
{
serial_sendbyte(0XFF);
Serial_SendArray(Serial_TxPacket,4);
serial_sendbyte(0XFE);
}
//接收HEX数据包
void USART1_IRQHandler(void)
{
static uint8_t RxState=0;//状态
static uint8_t pRxPacket=0;
if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
{
uint8_t Rxdata=USART_ReceiveData(USART1);
if(RxState==0)
{
if(Rxdata==0xFF)
{
RxState =1;
}
}
else if(RxState==1)
{
Serial_RxPacket[pRxPacket]=Rxdata;
pRxPacket++;
if(pRxPacket>=4)
{
RxState=2;
pRxPacket=0;
}
}
else
{
if(Rxdata==0xFE)
{
RxState =0;
Serial_packet_flag=1;
}
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "serial.h"
#include "stdio.h"
#include "key.h"
uint16_t Rxdata;
void key_pro(void)
{
uint8_t key_num;
key_num=key_scan();
if(key_num==1)
{
Serial_TxPacket[0]++;
Serial_TxPacket[1]++;
Serial_TxPacket[2]++;
Serial_TxPacket[3]++;
serial_SendPacket();
OLED_ShowHexNum(2,1,Serial_TxPacket[0],2);
OLED_ShowHexNum(2,4,Serial_TxPacket[1],2);
OLED_ShowHexNum(2,7,Serial_TxPacket[2],2);
OLED_ShowHexNum(2,10,Serial_TxPacket[3],2);
}
if(serial_Packet_getRxfalg_return()==1)//接收到数据包
OLED_ShowHexNum(4,1,Serial_RxPacket[0],2);
OLED_ShowHexNum(4,4,Serial_RxPacket[1],2);
OLED_ShowHexNum(4,7,Serial_RxPacket[2],2);
OLED_ShowHexNum(4,10,Serial_RxPacket[3],2);
}
int main(void)
{
OLED_Init();
key_init();
serial_init();
OLED_ShowString(1,1,"TXPacket");
OLED_ShowString(3,1,"RXPacket");
Serial_TxPacket[0]=0x01;
Serial_TxPacket[1]=0x02;
Serial_TxPacket[2]=0x03;
Serial_TxPacket[3]=0x04;
while(1)
{
key_pro();
}
}
#我们对数据进行写入与读写,如果读取太慢,对下一次的数据读取就会是俩个数据包的结合,解决方法:在每个数据包读取完毕加入判断#
6.4串口收发可变包长文本数据包
#include "stm32f10x.h" // Device header
#include "stdio.h"
#include "stdarg.h"
uint8_t Serial_RXData;
uint8_t Serial_RXFlag;
//定义俩个缓存区数组,四个数据只存储发送或者接收的载荷数据,不包含包头包尾
char Serial_RxPacket[100];
uint8_t Serial_packet_flag=0;
-----------------------
与Seria.c代码一致
-----------------------
//发送数据包
uint8_t serial_Packet_getRxfalg_return(void)
{
if(Serial_packet_flag==1)
{
Serial_packet_flag=0;
return 1;
}
return 0;
}
//接收文本数据包
void USART1_IRQHandler(void)
{
static uint8_t RxState=0;//状态
static uint8_t pRxPacket=0;
if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
{
uint8_t Rxdata=USART_ReceiveData(USART1);
if(RxState==0)
{
if(Rxdata=='@')
{
RxState =1;
}
}
else if(RxState==1)
{
if(Rxdata=='\r')
{
RxState=2;
}
else
{
Serial_RxPacket[pRxPacket]=Rxdata;
pRxPacket++;
}
}
else
{
if(Rxdata=='\n')
{
RxState =0;
Serial_RxPacket[pRxPacket]='\0';
Serial_packet_flag=1;
}
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "serial.h"
#include "stdio.h"
#include "key.h"
#include "LED.h"
//C语言官方库
#include "string.h"
int main(void)
{
OLED_Init();
key_init();
serial_init();
LED_Init();
OLED_ShowString(1,1,"TXPacket");
OLED_ShowString(3,1,"RXPacket");
while(1)
{
if(serial_Packet_getRxfalg_return()==1)
{
OLED_ShowString(4, 1, " ");
OLED_ShowString(4, 1, Serial_RxPacket); //OLED清除指定位置,并显示接收到的数据包
//判断俩个字符串是否相等
if(strcmp(Serial_RxPacket,"LED_ON")==0)
{
led_on(1);
serial_sendstring("LED_ON_OK\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2,1,"LED_ON_OK");
}
else if(strcmp(Serial_RxPacket,"LED_OFF")==0)
{
led_off(1);
serial_sendstring("LED_OFF_OK\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2,1,"LED_OFF_OK");
}
else
{
serial_sendstring("ERROR_COMMAND\r\n");
OLED_ShowString(2, 1, " ");
OLED_ShowString(2,1,"ERROR_COMMAND");
}
}
}
}
更多推荐



所有评论(0)