• STM32-串口通信波特率计算以及寄存器的配置详解


    串口通信基本原理

    处理器与外部设备通信的两种方式

    并行通信

    传输原理:数据各个位同时传输。

    优点:速度快

    缺点:占用引脚资源多

    串行通信

    传输原理:数据按位顺序传输

    优点:占用引脚资源少

    缺点:速度相对较慢

    按照数据传送方向,分为:

    单工:数据传输只支持数据在一个方向上传输;

    半双工:允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;

    全双工:允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。

    串行通信的通信方式

    同步通信:带时钟同步信号传输。如SPI,lIC通信接口

    异步通信:不带时钟同步信号。如UART(通用异步收发器),单总线

    常见串行通信接口

    在这里插入图片描述

    STM32的串口通信接口

    UART:通用异步收发器

    USART:通用同步异步收发器

    大容量STM32F10x系列芯片,如STM32F103ZET6,包含3个USART和2个UART

    可以在芯片的数据手册中查到各串口所接的引脚

    在这里插入图片描述

    UART异步通信方式特点

    1. 全双工异步通信;
    2. 分数波特率发生器系统,提供精确的波特率;
    3. 发送和接收共用的可编程波特率,最高可达4.5Mbits/s;
    4. 可编程的数据字长度(8位或者9位);
    5. 可配置的停止位(支持1或者2位停止位);
    6. 可配置的使用DMA多缓冲器通信;
    7. 单独的发送器和接收器使能位;
    8. 检测标志:1.接收缓冲器 2.发送缓冲器空 3.传输结束标志
    9. 多个带标志的中断源。触发中断。
    10. 其他:校验控制,四个错误检测标志。

    串口通信过程

    在这里插入图片描述

    STM32串口异步通信需要定义的参数

    当通信线路上电平状态为1,表示当前线路上没有数据传送,串口处于空闲。

    1.起始位:先发出一个逻辑"0"的信号,表示传输字符的开始。

    2.数据位(8位或者9位):紧接着起始位之后,数据位的个数可以是4、5、6、7、8等,构成一个字符,从最低位开始传送。

    3.奇偶校验位(第9位):数据位加上这一位后,使得"1"的位数应为偶数(偶校验)或奇数(奇校验),以此来校验资料传送的正确性。可有可无,CRC校验更准确

    4.停止位(1,15,2位):它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。

    5.波特率设置

    波特率是每秒钟传输的比特位,一般为4800、9600、115200,波特率越大,则数据传输速度越快,但这样就需要接收端也要快速的接收,同时容易受到干扰;波特率越小则传输越慢,抗干扰能力加强;

    一般工业领域使用9600比较多,如果是单片机与模块之间的通信,如WiFi模块,则用115200,能及时通信

    在这里插入图片描述

    常用的串口相关寄存器

    USART_SR状态寄存器

    USART_ DR数据寄存器

    USART_BRR波特率寄存器

    寄存器的详细描述在stm32中文参考手册中25.6节USART寄存器描述中可以查看到

    波特率计算方法

    在STM32中文参考手册的USART框图中,这一部分是波特率设置示意图

    在这里插入图片描述

    公式:

    在这里插入图片描述

    上式中,fPCLKx是给串口的时钟(PCLK1用于USART2、3、4、5,PCLK2用于USART1);USARTDIV是一个无符号定点数。我们只要得到USARTDIV的值,就可以得到串口波特率寄存器USART1->BRR的值,反过来,我们得到USART1->BRR的值,也可以推导出USARTDIV的值。但我们更关心的是如何从 USARTDIV的值得到USART_BRR的值,因为一般我们知道的是波特率,和 fPCLKx的时钟,要求的就是USART_BRR的值。

    在这里插入图片描述

    注意

    只有USART1使用PCLK2(最高72MHz),其它USART使用PCLK1(最高36MHz)

    参考手册中的波特率计算表

    在这里插入图片描述

    串口相关寄存器

    状态寄存器(USART_SR)

    该寄存器的TXE、TC、RXNE位比较重要,这些位分别与发送数据寄存器(TDR)、发送移位寄存器、接收数据寄存器(RDR)相关

    注意:TXE和TC的复位值为1,RXNE的复位值为0!

    在这里插入图片描述

    TXE为1: TDR里的数据全部移到了移位寄存器,且没有新的数据进TDR。

    TXE为0: TDR里有数据,未空,则TXE=0。

    在串口中使用USART_SendData()函数发送数据后,最好用USART_GetFlagStatus()函数判断TXE位是否为1,为1才表示数据已经被转移到移位寄存器中,如果一直往发送数据寄存器写入数据,没有等待数据转移到移位寄存器中的话,先写入的数据会被后写入的数据覆盖

    TC为1:从TDR过来的数据全部被移送到TX引脚,且TDR里也没有新的数据

    TC为0:从TDR里过来的数据还没有全部移过来,或者之前TDR里的数据被移走了,但TDR里又来了新的数据

    TC标志位的清除是要先读USART_SR,再写入USART_DR,顺序不能反

    这里有个串口发送的例子解析TC标志位的影响

    在这里插入图片描述

    上图程序先调用USART_SendData函数往DR寄存器放入一个‘H’字符,再判断TC标志位是否为1,为1则表示‘H’字符从串口TX发送了出去,上面程序最后结果应该是在串口助手上显示‘HKG’,但实际是显示‘KG’,这是为什么呢?

    首先这里有两个问题,1:为什么’H‘字符没有发送出去;2:为什么后面KG都正常发送

    解答1:需要注意的是,USART_SR寄存器的复位值是0x00C0,这说明上电复位时TXE和TC位默认是高电平的,系统刚上电,程序执行完了第10行,把‘H’字符放入到了DR寄存器中,但由于TC位上电默认高电平,所以11行的whlie循环就没有起到延时等待效果,就立马执行了12行,又往DR寄存器里放入了’K‘字符,此时’H‘字符还没来得及送到移位寄存器就被’K‘字符覆盖了,所以造成了’H‘字符的发送丢失

    解答2:因为TC标志位清除的条件是先读USART_SR,再写入USART_DR,而程序中的第10、11行是先写入USART_SR,再读USART_SR,这顺序明显反了,所以不会清除TC标志位,而程序执行完11行再执行12行后,正好是先读USART_SR,再写入USART_SR,所以满足了TC清零的条件,TC在12行后被清零,所以在13行的whlie循环就起到了延时等待TC标志位再次被置1的作用,当TC被置1,退出循环,'K’字符就被发送了出去,后面的’G‘字符同理

    修改方法:

    1.在USART_SendData(USART1,‘H’)前加上读USART_SR的操作即可,也就是加上一条while循环,加上后串口助手接到的就是’HKG’;

    2.也可以在发送’H‘字符前,调用USART_ClearFlag(USART1,USART_FLAG_TC),手动清除上电时TC标志位默认的高电平,同样能发送’HKG’

    控制寄存器(USART_CR)

    5、6、7位与状态寄存器的有点类似,这里主要是控制中断使能

    在这里插入图片描述

    串口操作相关库函数(省略入口参数)

    void USART_Init();		//串口初始化:波特率,数据字长,奇偶校验,硬件流控以及收发使能
    void USART_Cmd();		//使能串口
    void USART_ITConfig();	//使能相关中断
    
    • 1
    • 2
    • 3

    操作DR数据寄存器:

    void USART_SendData();			//发送数据到串口, 放入DR
    uint16_t USART_ReceiveData();	//接受数据,从DR读取接收到的数据
    
    • 1
    • 2

    操作SR状态寄存器

    FlagStatus USART_GetFlagStatus();		//获取状态标志位
    void USART_ClearFlag();					//清除状态标志位
    ITStatus USART_GetITStatus();			//获取中断状态标志位
    void USART_ClearITPendingBit();			//清除中断状态标志位
    
    • 1
    • 2
    • 3
    • 4

    USART_GetFlagStatus()和USART_GetITStatus()的区别

    共同点:都是访问串口的SR状态寄存器。

    不同点:

    USART_GetITStatus():

    该函数不仅会判断标志位是否置1,同时还会判断是否使能了相应的中断。所以在串口中断函数中,通常使用该函数

    返回值:ITStatus,ITStatus的值为1,表示可以读取数值了,ITStatus的值为0,表示还不可以读取数值

    USART_GetFlagStatus() :

    该函数只判断标志位。在没有使能相应的中断时,通常使用该函数来判断标志位是否置1。

    串口配置的一般步骤

    串口中断根据需要开启

    TX引脚模式设置为推挽复用输出(GPIO_Mode_AF_PP)

    RX引脚模式设置为浮空输入或带上拉输入(GPIO_Mode_IN_FLOATING)

    在这里插入图片描述

    在这里插入图片描述

    串口实验代码详解

    串口初始化USART1_Init()

    先对串口1所接的引脚PA9和PA10初始化,再对串口初始化,要开启中断的话,要对NVIC进行初始化

    /**
    * @name 	USART1_Init
    * @brief	串口1初始化
    * @param	bound:波特率
    * @retval	None
    */
    void USART1_Init(u32 bound)
    {
        GPIO_InitTypeDef  GPIO_InitStructure;
        USART_InitTypeDef USART_InitStructure;
        NVIC_InitTypeDef NVIC_InitStructure;
    
        //GPIO时钟,串口1时钟使能
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_USART1,ENABLE);
    
        //串口复位(可无)
        USART_DeInit(USART1);
    
        //USART1_TX PA9
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;        //设置为复用推挽输出
        GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_9;             //PA9接TX
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
        //PA9引脚初始化
        GPIO_Init(GPIOA,&GPIO_InitStructure);
    
        //USART1_RX PA10
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;      //设置为浮空输入
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;                 //PA10接RX
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        
        //PA10引脚初始化
        GPIO_Init(GPIOA,&GPIO_InitStructure);
    
        //USART1
        USART_InitStructure.USART_BaudRate = bound;                  //波特率
        USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;     //无硬件数据流控制
        USART_InitStructure.USART_Mode = USART_Mode_Tx|USART_Mode_Rx;       //设置发送和接收模式
        USART_InitStructure.USART_Parity = USART_Parity_No;         //无奇偶校验位
        USART_InitStructure.USART_StopBits = USART_StopBits_1;      //一个停止位
        USART_InitStructure.USART_WordLength = USART_WordLength_8b;     //字长为8位数据格式
    
        //USART1初始化
        USART_Init(USART1,&USART_InitStructure);
    
        //开启中断并初始化NVIC
        //USART1 NVIC 配置
        NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;           //串口1全局中断
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;             //IRQ通道使能
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;   //抢占优先级为3
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;          //子优先级为3
    
        //根据指定的参数初始化NVIC寄存器
        NVIC_Init(&NVIC_InitStructure);
    
        //开启中断,接收到数据就进入中断
        USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
    
        //使能串口
        USART_Cmd(USART1,ENABLE);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    中断处理函数USART1_IRQHandler()

    在这里插入图片描述

    //0x0d是回车键的十六进制值
    //0x0a是换行/n的十六进制值
    //中断处理判断数据结束标准是:回车+换行
    
    #define USART_REC_LEN 200
    u16 USART_RX_STA = 0;                   //接收状态标记
    u8 USART_RX_BUF[USART_REC_LEN];        //接收数组,最大USART_REC_LEN个字节
    
    /**
    * @name 	USART1_IRQHandler
    * @brief	串口1中断处理函数
    * @param	None
    * @retval	None
    */
    void USART1_IRQHandler(void)        //名字不能随便起
    {
        u8 Res;
        if(USART_GetITStatus(USART1,USART_IT_RXNE) != RESET)    //接收中断(接收到的数据必须是0x0d 0x0a结尾)
        {
            Res = USART_ReceiveData(USART1);    //读取到的数据,函数返回的是USART1->DR寄存器的[8:0]位
    
            /*STA最高位为接收完成标志位,接收完一串数据后才被置位,会被主函数或其他地方清零;
            这里判断该位是否为0,为0表示一串数据正在接收,没接收完,或者是其他地方清零了标志位,准备下一串数据的接收*/
            if((USART_RX_STA & 0x8000) == 0)     //接收未完成
            {
    		/*判断接收0x0d标志位是否为1,如果上一个字节是0x0d,那该标志位就会被置位,这里取出该位判断,为1则进入里面继续判断本			次接收的字节是否为0x0a(换行),是则表示一串数据接收完毕,最高位接收完成标志位置1,否则清除STA标志,重新接收数据*/
                if(USART_RX_STA & 0x4000)   
                {
                    if(Res != 0x0a)USART_RX_STA = 0;    //接收错误,重新开始
                    else USART_RX_STA |= 0x8000;        //接收完成了,接收到回车后的换行字符,接收完成标志位置位
                }
                else    //没接收到0x0d,说明数据还没结束,继续接收
                {
                    if(Res == 0x0d)     //如果这次收到的数据是0x0d
                    {
                        USART_RX_STA |= 0x4000;      //接收到0x0d标志位置位
                    }
                    else    //如果这次收到的数据不是0x0d,说明是普通数据,进行保存
                    {
                        /*将接收的数据存入数组中,STA的0 ~ 13位为接收到数据的个数,因为STA 0 ~ 13位一开始都是0,
                        0&1=0,所以[USART_RX_STA & 0x3FFF]为0,这里相当于下标的作用
                        */
                        USART_RX_BUF[USART_RX_STA & 0x3FFF] = Res;  
                        USART_RX_STA++;                             //长度+1,放下一个数据
                        if(USART_RX_STA>(USART_REC_LEN-1))          //如果数据个数(下标)大于接收数组长度,则清零STA
                        {
                            USART_RX_STA = 0;   				//STA清零,重新接收数据
                        }
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    主函数main
    #include "delay.h"
    #include "USART1.h"
    
    int main()
    {
    	u16 i;
    	u16 len;
    	u16 time = 0;
    	//延时函数初始化
    	delay_Init();
    	//设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    	//串口初始化,波特率9600
    	USART1_Init(9600);
    	while(1)
    	{
    		if(USART_RX_STA & 0x8000)	//判断接收完成标志位是否为1
    		{
    			len = USART_RX_STA & 0x3FFF;	//得到此次接收到的数据长度
    			for(i=0;i<len;i++)
    			{
    				USART_SendData(USART1,USART_RX_BUF[i]);	//往串口1发送数据
    				while(USART_GetFlagStatus(USART1,USART_FLAG_TC) != SET);	//等待发送结束
    			}
    			printf("\r\n\r\n");		//换行
    			USART_RX_STA = 0;		//清零STA
    		}
    		else		//接收完成标志位为0,说明stm32没有接收到数据
    		{
    			time++;
    			if(time%500 == 0){printf("请输入数据,以回车键结束\r\n");}
    			if(time%2000 == 0){printf("USART1串口实验\r\n");}
    			delay_ms(10);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    重写printf函数

    在串口初始化的源文件中加入下面的代码,重写printf函数,让其往串口发送

    //加入以下代码,支持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 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    串口发送一个字节函数

    仿照USART_SendData函数进行重写

    void USART_SendByte(USART_TypeDef* USARTx,uint8_t Data)
    {
        assert_param(IS_USART_ALL_PERIPH(USARTx));
        assert_param(IS_USART_DATA(Data));
        USARTx->DR = (Data & (uint16_t)0x01FF);
        while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE)==RESET);	//等待发送完成
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    串口发送一个字符串函数

    依次发送字符串中字符,每发送一个检查下TXE标志位,发送完全部字符以后,最后检查TC标志位。

    void USART_SendString( USART_TypeDef *USARTx, char*str)
    {
        while(*str!=0)
        {
            USART_SendByte( USARTx,*str++ );
        }
    	while(USART_GetFlagStatus(USARTx,USART_FLAG_TC)==RESET);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  • 相关阅读:
    营销邮件主题怎么写?编写邮件主题的技巧?
    ORACLE设置快照回滚点
    Linux ubuntu 20.04.5 Server桌面设置NetworkManager网络管理
    VBA处理文件,发现打开的word文档分布在出现多个实例下,无法全部遍历
    2022年SQL大厂高频实战面试题(详细解析)
    膜拜,终于拿到了美团大佬分享的Netty源码剖析与应用PDF
    axure 8 修改axhub-column-data的数据和配置
    oracle19c升级时区版本 32-->42,解决数据泵导数据TSTZ报错
    Scrapy第六篇:日志记录和try except
    Linux sed工具的使用
  • 原文地址:https://blog.csdn.net/weixin_46251230/article/details/126658178