单片机
返回首页

STM32 USART串口通信详解

2025-09-23 来源:cnblogs

1、原理

1、USART串口协议

同步:有单独的时钟线,接收方可以在时钟信号的指引下进行采样

异步:双方需要约定一个采样频率,并添加一些帧头帧尾等进行采样位置的对齐

单端:引脚的高低电平都是相对于GND的电压差,需要接GND引脚

差分:抗干扰,靠两个差分引脚的电压差来传输信号,可以不需要GND

点对点:直接传输数据

多设备:需要寻址,来确定通信对象

串口通信TX、RX、GND必须要接

如果设备1、设备2都有独立供电,VCC可以不接

如果其中一个设备没有供电,如设备1为STM32,设备2为蓝牙串口模块,就需要将它们的VCC接在一起,STM32通过这根线向右边的子模块供电

波特率:异步通信通信速率,每秒传输码元的个数、bit/s。高电平表示1,低电平表示0。决定了每个多久发送一位

起始位:串口的空闲状态是高电平,起始位为低电平,通知接收设备这一帧数据要开始了

停止位:用于数据帧间隔,固定为高电平

2、USART串口外设

同步模式多了一个时钟输出,不支持时钟输入,并不支持两个USART之间的通信

波特率发生器:用来配置波特率,相当于分频器,最常用(9600,115200)。

数据位长度:8、9位

停止位长度:在进行连续发送时,帧的间隔

硬件流控制:防止接收方处理慢而导致数据丢失的问题

USART资源:USART1是APB2总线上的设备,USART2、3都是APB1总线上的设备 


TDR和RDR占用同一个地址,在程序上表示为一个寄存器,数据寄存器DR,在实际的硬件中,是两个寄存器,一个用于发送,一个用于接收,TDR只写,RDR只读。写操作,数据写道TDR;读操作,数据从RDR中读出

发送端:把一个字节的数据一位一位地向右移出去(低位先行),对应串口协议的数据位波形。        当硬件检测到写入数据,会检查当前移位寄存器是否有数据正在移位,如果没有该数据会立刻全部被发送到发送移位寄存器,准备发送。当数据从TDR发送到移位寄存器时,会置标志位TEX(TX Empty),TDR为空,TEX置1,就可以在TDR里写入下一个数据了,此时发送移位寄存器中的数据还没有发送出去。当数据移位完成后,新的数据就会再次自动地从TDR转移到发送移位寄存器里。可以保证连续发送数据时,数据帧之间不会有空闲。

接收端:从高位到低位方向移动,一个字节移位完成之后,这一个字节的数据就会整体移到接收数据寄存器RDR,转移过程中会置标志位RXNE(RX Not Empty),接收数据寄存器非空。RXNE置1时,就可以把数据读走了

硬件流控制:避免发送设备发的太快,接收设备来不及处理,导致丢弃或覆盖

nRTS:接对方的CTS,能接受的时候RTS置低电平,请求对方发送,对方的CTS接收到之后,就可以发送数据;当数据处理不过来,如接收数据寄存器一直没有读,又有数据过来了,RTS置高电平,对方CTS接收到之后,暂停发送数据,直到RTS置低电平。

nCTS:清除发送,用于接收其他设备的nRTS。

SCLK:用于产生同步的时钟信号,配合发送移位寄存器输出,发送寄存器移位一次,同步时钟电平就跳变一个周期,时钟告诉对方,移出去一位数据。只支持输出,不支持输入,两个USART之间不能实现同步的串口通信。用于兼容别的协议,如SPI,和自适应波特率,如接收设备不确定发送设备给的是什么波特率,可以测量时钟周期,计算得出波特率。

唤醒单元:实现串口挂载多设备,一条总线上接多个从设备,每个设备分配一个地址,想跟某个设备通信,就先进行寻址,确定通讯对象。如给设备分配一个地址,当发送指定地址时,此设备唤醒开始工作;没收到地址就保持沉默。

状态寄存器:TEX和RXNE是判断发送状态和接收状态的必要标志位。

USART中断控制:配置中断是否能通向NVIC。

波特率发生器:相当于分频器,对APB时钟进行分频,得到发送和接收移位的时钟。USART1挂载在APB2,PCLK2的时钟,72MHZ;其余USART挂载在APB1,PCLK1的时钟,36MHZ。之后这个时钟会进行分频,除USARTDIV的分频系数,分频完之后再除16。TE为1,发送器使能,RE为1接收器使能。

3、基本结构

4、数据帧

输出定时翻转TX引脚高低电平;输入要保证采样频率和波特率一致,还要保证每次输入采样的位置正好处于每一位的正中间,这样高低电平读进来才最可靠。

空闲帧、断开帧用于局域网协议

数据长度:8位(有、无校验)、9位(有、无校验),最好选择9位字长有校验,8位字长无校验。串口传输的数据类型一般为uint8_t。

一个停止位和一个数据位时长一样,一般使用一个停止位

5、输入电路噪声处理

以波特率16倍频率采样,一位的时间进行16次采样。

为了避免噪声影响,接收电路在第一次遇到下降沿之后,第3、5、7次进行采样,第8、9、10次进行采样,这两批采样都至少有2个0。如果只用2个0,会在状态寄存器里置一个NE(Noise Error),噪声标志位。如果少于2个0,电路忽略前面的数据,重新开始捕捉下降沿。

6、数据采样

三次采样中,两次及以上为1,就认为收到了1;两次及以上为0,就认为收到了0。两次时,噪声标志位NE也会置1。

7、波特率发生器

DIV_Mantissa:DIV整数部分(二进制)

DIV_Fraction:DIV小数部分(二进制)

因为它内部还有一个16倍波特率的采样时钟,所以要多除16。

8、数据模式

可以以16进制数和字符的方式进行发送。


数据在线路中传输得形式是16进制数。


2、代码

1、初始化

1、初始化时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

2、初始化GPIO引脚

串口空闲状态是高电平,不使用下拉输入


GPIO输出模式使用复用推挽输出,因为在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);

3、初始化USART

USART_InitTypeDef USART_InitStruct;

USART_InitStruct.USART_BaudRate=9600;

USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;

//想同时接收和发送可以使用按位或

USART_InitStruct.USART_Mode=USART_Mode_Tx;

USART_InitStruct.USART_Parity=USART_Parity_No;

USART_InitStruct.USART_StopBits=USART_StopBits_1;

USART_InitStruct.USART_WordLength=USART_WordLength_8b;

USART_Init(USART1, &USART_InitStruct);

USART_Cmd(USART1, ENABLE);

2、发送数据

1、发送一位数据

不需要对TXE手动清零,下一次使用USART_SendData()时,TXE自动清零。TDR寄存器中的数据被硬件转移到移位寄存器的时候,该位被硬件置位。对USART_DR

的写操作,将该位清零。


//串口一次移动8位数据

void Serial_SendData(uint8_t Byte)

{

//数据写入发送数据寄存器TDR

USART_SendData(USART1, Byte);

//等待TDR中的数据转移到移位寄存器

while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET);

}

2、发送一个数组

void Serial_SendArray(uint8_t *Array, uint16_t Lenth)

{

uint16_t i=0;

for(i=0;i {

Serial_SendData(Array[i]);

}

}

3、发送一个字符串

void Serial_SendString(char* string)

{

uint16_t i;

for(i=0;string[i]!='';i++)

{

Serial_SendData(string[i]);

}

}

4、发送一个数字

一位一位地发送


uint32_t Serial_pow(uint32_t Num, uint16_t t)

{

uint32_t result=1;

while(t--)

{

result*=Num;

}

return result;

}

void Serial_SendNum(uint32_t Num, uint8_t Lenth)

{

uint8_t i;

for(i=0;i {

//Lenth-i-1 表示第一位数

Serial_SendData(Num/Serial_pow(10,Lenth-i-1)%10+'0');

}

5、重定向printf到串口

在工程设置->target里把Use MicroLib勾选上


#include

//fputc是printf的底层,把fput重定向到串口,printf就重定向到串口

int fputc(int ch, FILE *f)

{

Serial_SendData(ch);

return ch;

}

3、接收数据

1、初始化RX

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


USART_InitStruct.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;

2、串口接收

1、查询,在主函数里不断判断RXNE标志位

while(1)

{

if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE))

{

RxData=USART_ReceiveData(USART1);

}

OLED_ShowHexNum(1,1,RxData,4);

}

2、中断

开启RXNE标志位到NVIC的输出,RXNE一旦置1,就会向NVIC申请中断


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



//暂存数据,标志位

uint8_t RxData;

uint8_t RxFlag;


uint8_t Serial_GetRxData()

{

return RxData;

}

//每次获取标志位后清零

uint8_t Serial_GetRxFlag()

{

if(RxFlag==1)

{

RxFlag=0;

return 1;

}

else

{

return 0;

}

}

//收到USART的中断后,将标志位暂存

void USART1_IRQHandler()

{

RxData = USART_ReceiveData(USART1);

RxFlag = 1;

USART_ClearITPendingBit(USART1, USART_IT_RXNE);

}


4、收发一个数据包

1、HEX数据包

传输直接,解析数据简单,适合模块发送原始的数据如陀螺仪、温湿度传感器

2、文本数据包

数据直观易理解,灵活,适合输入指令进行人机交互的场合,如蓝牙模块AT指令

3、HEX数据包收发

//只存载荷数据

uint8_t TxPacket[4];

uint8_t RxPacket[4];

uint8_t RxFlag;

 中断函数内,用状态机表示状态


void USART1_IRQHandler()

{

static uint8_t RxState=0;

static uint8_t Rxnum=0;

//每次从串口中接收一个字节

uint8_t RxData = USART_ReceiveData(USART1);

switch(RxState)

{

case 0: 

//收到包头,进入转移状态

if(RxData==0xFF)

{

RxState=1;

}

break;

case 1:

RxPacket[Rxnum]=RxData;

Rxnum++;

if(Rxnum>=4)

{

Rxnum=0;

RxState=2;

}

break;

case 2:

if(RxData==0xFE)

{

RxState=0;

//全部接收到,置接收标志位

RxFlag=1;

}

break;

default:

break;

}

USART_ClearITPendingBit(USART1, USART_IT_RXNE);

}


4、文本数据包接收

void USART1_IRQHandler()

{

static uint8_t RxState=0;

static uint8_t Rxnum=0;

//每次从串口中接收一个字节

uint8_t RxData = USART_ReceiveData(USART1);

switch(RxState)

{

case 0: 

//收到包头,进入转移状态

//同时避免发送太快,第一组数据还没处理完就来第二组的情况

if(RxData=='@' && RxFlag==0)

{

RxState=1;

Rxnum=0;

}

break;

case 1:

//载荷字符数量不确定,先判断是不是包尾

if(RxData=='r')

{

RxState=2;

}

else

{

RxPacket[Rxnum]=RxData;

Rxnum++;

}

break;

case 2:

//等待第二个包尾

if(RxData=='n')

{

RxState=0;

//全部接收到,置接收标志位

RxFlag=1;

//字符数组最后,添加字符串结束标志位

RxPacket[Rxnum]='';

}

break;

default:

break;

}

USART_ClearITPendingBit(USART1, USART_IT_RXNE);

}

 


while(1)

{

if(Serial_GetRxFlag()==1)

{

OLED_ShowString(4,1,'                      ');

OLED_ShowString(4,1,RxPacket);

            //判断传入文本是否和目标命令匹配

if(strcmp(RxPacket, 'LED_ON')==0)

{

LED1_ON();

Serial_SendString('LED1_ON_OKrn');

OLED_ShowString(2,1,'                      ');

OLED_ShowString(2,1,'LED1_ON_OKrn');

}

else if(strcmp(RxPacket, 'LED_OFF')==0)

{

LED1_OFF();

Serial_SendString('LED1_ON_OFFrn');

OLED_ShowString(2,1,'                      ');

OLED_ShowString(2,1,'LED1_ON_OFFrn');

}

else

{

Serial_SendString('CommandErrorrn');

OLED_ShowString(2,1,'                      ');

OLED_ShowString(2,1,'CommandErrorrn');

}

}

}


进入单片机查看更多内容>>
相关视频
  • 【TI MSPM0 应用实战】智能小车+工业角度编码器+血氧仪+烟雾探测器!硬核参考设计详解!

  • 2022 Digi-Key KOL 系列: 你见过1GHz主频的单片机吗?Teensy 4.1开发板介绍

  • TI 新一代 C2000™ 微控制器:全方位助力伺服及马达驱动应用

  • MSP430电容触摸技术 - 防水Demo演示

  • 直播回放: Microchip Timberwolf™ 音频处理器在线研讨会

  • 基于灵动MM32W0系列MCU的指夹血氧仪控制及OTA升级应用方案分享

精选电路图
  • 设计汽车集群电源

  • 6晶体管H桥

  • USB自供电声卡

  • AVR LCD温度计—LM35

  • AVR PC步进电机驱动器

  • AVR温度计TCN75

    相关电子头条文章