[应用相关] STM32标准库USART串口通信

[复制链接]
436|14
小海师 发表于 2025-10-9 15:16 | 显示全部楼层 |阅读模式
1.串口通信的基本概念
串口通信(Serial Communication)是一种通过串行方式在设备之间传输数据的通信协议,其核心是逐位依次传输数据(而非并行通信的多位同时传输),广泛应用于嵌入式系统、工业控制、物联网设备等场景。

核心特点
传输方式:数据以二进制位(bit)为单位,按时间顺序一位一位地在单条传输线上发送(或接收)。
硬件简单:只需少数几根线(最少 2 根:发送线 TX、接收线 RX)即可实现双向通信,成本低、布线方便。
适用场景:适合中低速、短距离(通常数米到数十米)的数据传输,如传感器数据采集、设备调试、PLC 控制等。
关键概念
波特率(Baud Rate)

定义:单位时间内传输的二进制位数(bit/s),是衡量传输速度的核心参数。
示例:9600、115200 是常见波特率(9600 表示每秒传输 9600 位)。
要求:通信双方必须使用相同的波特率,否则会出现数据解析错误。
数据格式
串口通信的每帧数据(一个字符)通常包含以下部分(可配置):

起始位:1 位,标志数据传输开始(通常为低电平)。
数据位:8-9位,实际传输的有效数据(常用 8 位)。
校验位:0 或 1 位,用于简单错误检测(奇校验、偶校验、无校验)。
停止位:1-2 位,标志数据传输结束(通常为高电平)。
示例:“8 位数据位,无校验位,1 位停止位”(最常用格式)。

通信方向

单工:数据只能单向传输(如红外遥控器)。
半双工:数据可双向传输,但同一时间只能一个方向(如对讲机)。
全双工:数据可同时双向传输(需两条线:TX 发送、RX 接收,如电脑与单片机通信)。
2.理解串行,并行通信,同步以及异步通信
1,串行通信跟并行通信
串行通信:数据以二进制位(bit)为单位,通过单条(或一对差分)传输线,按时间顺序逐位依次传输的通信方式

并行通信:数据以多位二进制位(如 8 位、16 位、32 位)为单位,通过多条独立传输线(线数 = 一次传输的位数),在同一时刻同步传输多位数据的通信方式。

5158868e7614438ca5.png

2.同步通信(USART)跟异步通信(UART)
异步通信(UART):不使用独立时钟信号,通过预先约定的波特率和数据帧格式实现发送端与接收端同步的串行通信方式。

同步通信(USART):需要独立时钟信号(SCLK)来同步发送端与接收端的串行通信方式。
USART 是一种 “可同步可异步” 的通用协议(支持同步模式和异步模式),同步模式下需额外的时钟线协调数据传输。

7768168e7613eb347a.png

2272468e7613964871.png

以下是有一些常见的总线接口:

829568e76134951d5.png

总结:串口通信一次发送一个数据单位,并行通信一次发送一串数据,同步通信带有时钟信号,异步时钟不带有时钟信号

3.USART串口通信的基础知识
USART 通用同步、异步收发。如果芯片支持时钟同步,则称 USART,如果没有就是 UART。

UART 包含 TxD 发送线,RxD 接收线两根数据线,TxD 与 RxD 交叉连接。全双工、异步、串行通信。

一帧包含:起始位 + 数据位 + 奇偶校验位 + 停止位。

2915268e7612e52b8e.png

起始位:信号由高变到低并且维持一个位的低电平,称为起始位。

数据位:LSB 低位先出,0 低电平,1 高电平,逐位发送。有 8 位和 9 位。

奇偶校验位:用于检验数据是否出错。分奇校验、偶校验、无校验。

奇校验是指数据位中 1 的个数如果是奇数个,则为 0,否则为 1;

偶校验是指数据位中 1 的个数如果是偶数个,则为 0,否则为 1。

无校验即不使用校验位。

停止位:数据位和奇偶校验位结束后,总线变为高电平,并且维持 1 位、1.5 位、2 位等位数的高电平。

空闲状态:总线处于高电平定义为空闲状态。

波特率:每秒传输的位数,单位 bps。常见的 9600,115200。

硬件流控:

接收方的 nRTS 引脚与发送方的 nCTS 连接,当接收方将 nRTS 拉高电平时,发送方检测到 nCTS 高电平就暂停发送;

当接收方将 nRTS 拉低电平,发送方检测到 nCTS 低电平就继续发送。

这样即可实现总线上数据流量的控制。流控是由接收方控制的

串口通信的过程图:

8264468e76126c52d4.png

USART常用的API函数:
// 初始化
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
// 使能/禁用
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);

// DMA 使能/禁用
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);

// 发送、接收数据
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);

// 中断配置
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);
// 获取/清除状态
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);

4.USART工程创建(通过串口发送字符串实现)

4272868e7611f23786.png

通过原理图我们可以看到接受信号rx跟发送信号tx在PA9跟PA10上,因此可以通过这俩个引脚来控制字符串的接受与发送,

代码结构与实现思路
1. 串口初始化(MX_USART1_Init 与 MX_USART1_NVIC_Init)
目标:配置 USART1 的硬件引脚、通信参数和中断优先级,使其能收发数据并触发中断。


GPIO 配置:

复用 PA9 作为 USART1 的发送引脚(TX),设置为GPIO_Mode_AF_PP(复用推挽输出),确保串口数据能正常发送。
复用 PA10 作为 USART1 的接收引脚(RX),设置为GPIO_Mode_IPU(上拉输入),提高抗干扰能力,确保稳定接收数据。
使能 GPIOA 和 USART1 的时钟,保证硬件正常工作。
USART 参数配置:

通过USART_InitStruct设置核心通信参数:波特率(由外部传入,如 115200)、8 位数据位、1 位停止位、无校验位,同时使能收发模式(USART_Mode_Rx | USART_Mode_Tx)。
调用USART_Init和USART_Cmd完成初始化并使能串口。
中断配置:

使能两种中断:USART_IT_RXNE(接收数据非空,即收到 1 个字节时触发)和USART_IT_IDLE(空闲中断,即数据传输结束后触发)。
通过NVIC_Init配置中断优先级,确保中断能被正确响应。
2. 串口中断服务函数(USART1_IRQHandler)
目标:实时接收串口数据,通过中断标志判断数据接收状态,完成数据缓存和接收结束标记。


接收数据非空中断(RXNE):

当收到 1 个字节时,USART_IT_RXNE标志置位,进入中断处理。
将接收的数据存入全局数组U1RX_DATA,并通过U1RX_LEN记录接收长度。
若接收长度超过max_len(最大缓存),强制置位U1RX_IDLE_FLAG,触发数据处理。
空闲中断(IDLE):

当串口接收完一帧数据后(一段时间内无新数据),USART_IT_IDLE标志置位。
需先读USART1->SR再读USART1->DR,才能清除 IDLE 中断标志(STM32 硬件要求)。
置位U1RX_IDLE_FLAG,通知主循环 “数据接收完成,可以处理”。
3. 重定向printf函数
目标:使标准库printf函数能通过 USART1 发送数据,简化串口输出操作。


通过重定义fputc函数,将输出字符通过 USART1 发送:
循环等待发送寄存器为空(USART1->SR&0X40,即TXE位),确保上一个字符发送完成。
将字符写入USART1->DR(数据寄存器),完成发送。
禁用半主机模式(__use_no_semihosting),避免调试模式下的冲突。
4. 主函数(main)逻辑
目标:初始化系统资源,主循环中处理串口接收数据并控制 LED。


系统初始化:

配置 NVIC 优先级分组(NVIC_PriorityGroup_2),统一中断优先级管理。
初始化 GPIO(LED 引脚)、延时函数、外部中断(未在核心逻辑中使用)、USART1 及其中断。
启动时通过printf发送初始化完成信息(System Init OK!!!)。
主循环处理:

串口数据处理:当U1RX_IDLE_FLAG置位(接收完成)时:
打印接收的长度和数据(printf("Recv[%d]:%s",...))。
通过strncmp判断指令:若为 “绿灯亮” 则拉低 PC7;若为 “绿灯灭” 则拉高 PC7。
清空接收缓存(memset)、重置标志位和长度,准备下一次接收。
LED 定时翻转:通过GetTick()(系统滴答计数)实现 PC8 每 10 秒翻转一次状态(模拟心跳灯)。
以下是各个文件的代码

main.c

#include "stm32f10x.h"
#include <stdio.h>
#include "gpio.h"
#include "delay.h"
#include "exti.h"
#include "usart.h"
#include "string.h"

uint8_t SendData[]={"System Init OK!!!\r\n"};
int main(void)
{
                BitAction BitVal=Bit_RESET;
        uint32_t  Led_ts=0;
        NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
        MX_GPIO_Init();
        delay_Init();       
        MX_EXTI0_Init(3,3);
        MX_USART1_Init(115200);
        MX_USART1_NVIC_Init(2,0);
       
       

        printf("%s",SendData);
        while(1)
        {
                                if( U1RX_IDLE_FLAG ==1)
                {
                                //处理数据
                        printf("Recv[%d]:%s",U1RX_LEN,U1RX_DATA);
                        if(strncmp(U1RX_DATA,"绿灯亮",6)==0)
                        {
                                GPIO_WriteBit(GPIOC,  GPIO_Pin_7,Bit_RESET);
                       
                        }
                        else if(strncmp(U1RX_DATA,"绿灯灭",6)==0)
                        {
                                                                GPIO_WriteBit(GPIOC,  GPIO_Pin_7,Bit_SET);

                        }
                                //标志位清空 方便下一次接收
                        memset(U1RX_DATA,0,max_len);//清空接收区
                        U1RX_IDLE_FLAG=0;
                        U1RX_LEN=0;
               
               
                }
                if((GetTick() - Led_ts) >10000)
                {
                        BitVal=(BitAction)(!BitVal);
                        GPIO_WriteBit( GPIOC ,GPIO_Pin_8,BitVal);
                        Led_ts=GetTick();
                }
        }
               
       
        }

               
       


exit.c

#include "exti.h"
#include "gpio.h"



void MX_EXTI0_Init(uint8_t Preemption,uint8_t Sub)
{
//-------GPIO Init---------------------------
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);//使能 AFIO 时钟
        MX_Key_Init();
        GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0 );//将PA0与外部中断绑定
//-------EXTI Init---------------------------------
        EXTI_InitTypeDef  EXTI_InitStruct;
        EXTI_InitStruct.EXTI_Line           =EXTI_Line0;//中断线0
        EXTI_InitStruct.EXTI_LineCmd        =ENABLE;//使能外部中断
        EXTI_InitStruct.EXTI_Mode           =EXTI_Mode_Interrupt;//使用中断模式
        EXTI_InitStruct.EXTI_Trigger        =EXTI_Trigger_Falling;//下降沿触发
        EXTI_Init(&EXTI_InitStruct);
       
//------------NVIC Init----------------------------------
       
       
        NVIC_InitTypeDef NVIC_InitStruct;
        NVIC_InitStruct.NVIC_IRQChannel                   =EXTI0_IRQn;//外部中断线
        NVIC_InitStruct.NVIC_IRQChannelCmd                =ENABLE;//使能中断
        NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =Preemption;//抢占优先级
        NVIC_InitStruct.NVIC_IRQChannelSubPriority         =Sub;//响应优先级
       
        NVIC_Init(&NVIC_InitStruct);
       
       


}

                BitAction BitVal=Bit_RESET;
void EXTI0_IRQHandler(void)
{
       

               
                                if(GPIO_ReadInputDataBit( GPIOA,  GPIO_Pin_0)==Bit_RESET)
                {
                                BitVal=(BitAction)(!BitVal);
               
                                GPIO_WriteBit(GPIOC,  GPIO_Pin_9,BitVal);//默认输出低电平
                }
                EXTI_ClearITPendingBit(EXTI_Line0);//清除中断线0
       
}





exit.h

#ifndef  __EXTI_H_
#define  __EXTI_H_
#include "stm32f10x.h"
#include <stdio.h>

void MX_EXTI0_Init(uint8_t Preemption,uint8_t Sub);

void EXTI0_IRQHandler(void);

#endif


gpio.c

#include"gpio.h"


void MX_GPIO_Init(void)
{

  GPIO_InitTypeDef GPIO_InitStruct;
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);//使能时钟

        GPIO_InitStruct.GPIO_Mode   = GPIO_Mode_Out_PP;//推挽输出模式
        GPIO_InitStruct.GPIO_Pin    = GPIO_Pin_6| GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9;//选择Pin
        GPIO_InitStruct.GPIO_Speed  = GPIO_Speed_50MHz;
        GPIO_Init(GPIOC, & GPIO_InitStruct);
        GPIO_WriteBit(GPIOC, GPIO_Pin_6| GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9,Bit_SET);//默认输出高电平

}
void MX_Key_Init(void)
{
        GPIO_InitTypeDef GPIO_InitStruct;
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能时钟

        GPIO_InitStruct.GPIO_Mode   = GPIO_Mode_IPU;//上拉输入
        GPIO_InitStruct.GPIO_Pin    = GPIO_Pin_0;//选择Pin
        GPIO_Init(GPIOA, & GPIO_InitStruct);
       


}



gpio.h

#ifndef  __GPIO_H_
#define  __GPIO_H_
#include "stm32f10x.h"
#include <stdio.h>
void MX_GPIO_Init(void);

//void delay(uint32_t ms);
void MX_Key_Init(void);
#endif


usart.c

#include "usart.h"
#include "string.h"

char U1RX_DATA[max_len ] = {0};

uint8_t  U1RX_LEN=0;
bool U1RX_IDLE_FLAG =0;




void MX_USART1_Init(uint32_t Baud)       
{

//---------------GPIO  Init---------------------
        GPIO_InitTypeDef GPIO_InitStruct;
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能时钟
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 , ENABLE);
        //PA9 TX
        GPIO_InitStruct.GPIO_Mode   = GPIO_Mode_AF_PP;//复用推挽输出模式
        GPIO_InitStruct.GPIO_Pin    = GPIO_Pin_9;//选择Pin
        GPIO_InitStruct.GPIO_Speed  = GPIO_Speed_50MHz;
        GPIO_Init(GPIOA, & GPIO_InitStruct);
        //PA10 RX
        GPIO_InitStruct.GPIO_Mode   = GPIO_Mode_IPU;//上拉输出模式
        GPIO_InitStruct.GPIO_Pin    = GPIO_Pin_10;//选择Pin
        GPIO_InitStruct.GPIO_Speed  = GPIO_Speed_50MHz;
        GPIO_Init(GPIOA, & GPIO_InitStruct);

       
        //----------------USART Init-------------------------
       
         USART_InitTypeDef  USART_InitStruct;
         USART_InitStruct.USART_BaudRate               =Baud;//波特率
         USART_InitStruct.USART_HardwareFlowControl    =USART_HardwareFlowControl_None;    //  硬件流控制
         USART_InitStruct.USART_Mode                   =USART_Mode_Rx  | USART_Mode_Tx ;   //使用串口收发模式
         USART_InitStruct.USART_Parity                 =USART_Parity_No;                   //不校验
         USART_InitStruct.USART_StopBits               =USART_StopBits_1 ;                 //停止位1位
         USART_InitStruct.USART_WordLength             =USART_WordLength_8b;               //数据位为8位
       
         USART_Init(USART1, &USART_InitStruct);
         USART_Cmd(USART1,ENABLE);//使能串口

}



void MX_USART1_NVIC_Init(uint8_t Preemption,uint8_t Sub)
{
//---------------NVIC Init-----------------
        NVIC_InitTypeDef NVIC_InitStruct;
       
        USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//使能串口接收中断
        USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
        NVIC_InitStruct.NVIC_IRQChannel                   =USART1_IRQn;//串口中断
        NVIC_InitStruct.NVIC_IRQChannelCmd                =ENABLE;//使能中断
        NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority =Preemption;//抢占优先级
        NVIC_InitStruct.NVIC_IRQChannelSubPriority         =Sub;//响应优先级
       
        NVIC_Init(&NVIC_InitStruct);

}


void USART1_IRQHandler(void)
{
        if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET )
                {
                        if(U1RX_LEN<max_len)
                        {
                                U1RX_DATA[U1RX_LEN++] = USART_ReceiveData(USART1);
                        }
                        else
                        {
                                U1RX_IDLE_FLAG=1;//超过最大接收长度,强制处理接收数据
                        }
                }
                        if(USART_GetITStatus(USART1,USART_IT_IDLE)==SET )
                        {
                                        USART1->SR;//先读SR 清除IDLE中断标志位
                                        USART1->DR;//再读DR 清除IDLE中断标志位
                        U1RX_IDLE_FLAG = 1;//置位空闲中断接收标志位,以处理数据
                        }
                       
       
}
       

       




//加入以下代码,支持printf函数,而不需要选择use MicroLIB          
#if 1
#pragma import(__use_no_semihosting)            
//标准库需要的支持函数                 
struct __FILE
{ int handle;
};

FILE __stdout;      
//定义_sys_exit()以避免使用半主机模式   
void _sys_exit(int x)
{ x = x; }

//重定义fputc函数
int fputc(int ch, FILE *f)
{      
    while((USART1->SR&0X40)==0);//循环发送,直到发送完毕   
    USART1->DR = (u8) ch;      
   return ch;
}
#endif






usart.h

#ifndef  __USART_H_
#define  __USART_H_
#include "stm32f10x.h"
#include <stdio.h>
#include "stdbool.h"
#define max_len 128


void MX_USART1_Init(uint32_t Baud);

void MX_USART1_NVIC_Init(uint8_t Preemption,uint8_t Sub);

extern char U1RX_DATA[max_len ];
extern uint8_t  U1RX_LEN;
extern bool U1RX_IDLE_FLAG ;

#endif


delay.c

#include  "delay.h"
//---------查询法延时------------------------------------------

void delay_Init(void)
{

        SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);//选择8分频
               

}
void delay_us(uint32_t us)//最大值:1864135
{
        uint32_t temp = 0;
        SysTick->LOAD = fac_us*us;
        SysTick->VAL=0;//清空VAL
        SysTick->CTRL |=SysTick_CTRL_ENABLE_Msk;//使能SysTick
        do
        {
                temp=SysTick->CTRL;
       
        }while((temp & 0x01) && (!(temp &(0x01 <<16))) );
                SysTick->VAL=0;//清空VAL
                SysTick->CTRL  &=!SysTick_CTRL_ENABLE_Msk;//关闭SysTick
//或真真
//与假假
//非翻转
       
}




void delay_ms(uint32_t ms)//最大值1864
{
        uint32_t temp = 0;
        SysTick->LOAD = fac_ms*ms;
        SysTick->VAL=0;//清空VAL
        SysTick->CTRL |=SysTick_CTRL_ENABLE_Msk;//使能SysTick
        do
        {
                temp=SysTick->CTRL;
       
        }while((temp & 0x01) && (!(temp &(0x01 <<16))) );
                SysTick->VAL=0;//清空VAL
                SysTick->CTRL  &=!SysTick_CTRL_ENABLE_Msk;//关闭SysTick
}

void delay_s(uint32_t s)
{
  uint32_t temp=s;
        while(temp--)
        {
                delay_ms(1000);
        }
}

//----中断法延时----------------------
static uint32_t  TimingDelay = 0;
static  uint32_t Tick=0;
void  SysTick_Interrupt_Init(void)
{
        SysTick_Config(SystemCoreClock/1000);//Systick 时钟频率72兆,1ms进入一次中断

}
void   SysTick_Handler (void)
{
        Tick++;
        if(TimingDelay != 0)
        {
                TimingDelay--;
        }

}

void Delay_ms(uint32_t  ms)
{
        TimingDelay = ms;
        while(TimingDelay!=0)
        {}


}
uint32_t GetTick(void)
{

  return Tick;
}






delay.h

#ifndef  __DELAY_H_
#define  __DELAY_H_
#include "stm32f10x.h"
#include <stdio.h>



#define  fac_us (SystemCoreClock/8000000)  
#define  fac_ms fac_us*1000

//---------查询法延时------------------------------------------
void delay_Init(void);
void delay_us(uint32_t us);
void delay_ms(uint32_t ms);
void delay_s(uint32_t s);
//----中断法延时----------------------

void  SysTick_Interrupt_Init(void);
void   SysTick_Handler (void);
void Delay_ms(uint32_t  ms);
uint32_t GetTick(void);
#endif





————————————————
版权声明:本文为CSDN博主「热爱学习的小白一个」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_74350616/article/details/151865241

earlmax 发表于 2025-10-10 10:42 | 显示全部楼层
USART是STM32的通用串行通信接口,支持 ​​异步通信 ​​ 和 ​​同步通信
benjaminka 发表于 2025-10-10 11:30 | 显示全部楼层
高优先级中断会打断低优先级中断处理,需合理分配组别
primojones 发表于 2025-10-10 12:21 | 显示全部楼层
使用环形缓冲区配合 DMA 请求生成器,可实现无缝数据传输
claretttt 发表于 2025-10-10 14:05 | 显示全部楼层
未处理噪声错误、溢出错误等,会导致通信不稳定。
mickit 发表于 2025-10-10 16:08 | 显示全部楼层
标准库提供了基本的发送和接收函数
kkzz 发表于 2025-10-11 10:46 | 显示全部楼层
使用标准库配置 USART 的核心流程清晰
OliviaSH 发表于 2025-10-11 10:52 | 显示全部楼层
现在新出的芯片都不支持标准库了
dspmana 发表于 2025-10-11 12:17 | 显示全部楼层
USART是STM32的关键通信接口,支持异步/同步模式
wengh2016 发表于 2025-10-11 12:58 | 显示全部楼层
最容易出错的地方是时钟配置和中断标志位处理。
febgxu 发表于 2025-10-11 14:20 | 显示全部楼层
TX设为复用推挽输出,RX设为浮空输入
albertaabbot 发表于 2025-10-11 15:35 | 显示全部楼层
发送前需检查发送缓冲区是否为空              
mmbs 发表于 2025-10-11 17:11 | 显示全部楼层
中断模式              
linfelix 发表于 2025-10-11 19:17 | 显示全部楼层
接收数据速度过快,FIFO溢出导致数据丢失。
pixhw 发表于 2025-10-11 20:37 | 显示全部楼层
串口通信是一种异步通信方式,它不需要时钟信号线。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

89

主题

272

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部