一文搞懂STM32的串口通信(附库函数代码讲解)

​ 通讯是指不同计算机设备或是集成电路之间需要进行数据传输的方式,而这些传输的一系列方式就是通讯协议。

文章目录

  • 7.1 通信基础
  • 7.2 USART——串口通信
  • 7.2.2 物理层
  • 7.2.3 协议层
  • 7.3 STM32下的USART的原理
  • 7.4 代码实现
  • 7.5 串口进行调试的主机回显
  • 7.1 通信基础

    一次传输位数而言: 串行与并行

    ​ 同样的情况下,并行传输的成本更高。

    数据通讯的方向:全双工,半双工,单工

    通讯时数据同步方式:同步和异步通讯

  • 同步: 通信双方会使用一条线作为时钟线,双方按照时钟线进行时钟协调,并规定统一的时钟信号(上升沿或下降沿)对数据线进行采样。

  • 异步: 异步通信中并不使用时钟信号,而是在数据线中穿插一些校验或是起始位数据。

    **传输单位: 比特率(波特率) ** bit/s, 即每秒传输二进制的位数 波特率还表示每秒钟多少个马元(即多少个二进制位数)

  • 总的来说,目前所接触到的STM32相关的通讯均是设备A发送数据到总线,设备B再从总线上进行数据的读取。

    7.2 USART——串口通信

    USART串口通信的原理(协议层)是什么?

    STM32上实现串口通信的步骤是什么?

    STM32之USART-串口通信(含串口实验详细解析)_stm32 串口11bit-CSDN博客

    ​ 串口通信算是一种常用通讯方式,也主要常用于调试设备(即可以将机器的相关信息printf打印在电脑上),STM32上的串口通信主要是为异步串口通信。

    7.2.2 物理层

    ​ 即规定该协议具体需要哪种电子标准去进行通信。这里以RS-232标准,以及RS232信号线(带针为公头,带孔为母头)如下,其标准的COM口(也为DB9接口):

    ​ 目前的串口通信中(USART)中大多数设备只用RXD(接受数据),TXD(发送数据),GND(地线)这三条线。 因为STM32开发板上存在USB转串口的CH340G芯片,因此可直接用USB实现与主机的串口通信。

    7.2.3 协议层

    ​ 即规定USART数据的传输格式,其主要是通过设备发送方的TXD接口传输至设备接受方的RXD接口,该数据格式通常由起始位、数据位(0-7),校验位和停止位组成

    ​ 其中每一格虚线代表一个码元,通讯双方需约定好波特率,常见的为4800 9600 115200等。

    起始与停止信号

    ​ 数据包的起始信号由一个逻辑 0 的数据位表示,而数据包的停止信号可由 0.5、1、1.5 或 2 个逻辑 1 的数据位表示,

    只要双方约定一致即可。

    有效数据

    ​ 串口进行数据传输的核心内容,通常为约定的 5 6 7或8位

    数据校验位

    ​ 有效数据之后,会跟一个可选的数据校验位,主要用于检验有效数据的完整性。其检验方法为奇校验odd)、偶校验(even)、0 校验(space)、1 校验(mark)以及无校验(noparity)。

  • 奇校验odd:数据位和校验位中1的个数为奇数
  • 偶校验even:数据位和校验位中1的个数为偶数
  • 无校验No
  • ​ 这么一个包含起始信号有效数据,校验位的完整长度的数据称之为一帧数据。

    7.3 STM32下的USART的原理

    USART的核心实现原理是什么?

    ​ USART(Universal Synchronous Asynchronous Receiver and Transmitter) 全称为通用同步异步收发器,其支持使用DMA,可实现高速数据通信。USART在STM32上最多的应用在于打印信息。**其中STM32中USART的核心原理在于USART的功能图,**再根据功能图整体把握后进行编程就会有一定的思路。

    ​ 总的来说USART核心原理有4大部分构成:

    1号区域的功能引脚

  • TX:发送数据输出引脚。
  • RX:接收数据输入引脚
  • SW_RX:数据接收引脚,只用于单线和智能卡模式,属于内部引脚,没有具体外部引脚。
  • nRTS:请求以发送,n表示低电平,当USART准备接受新数据时nRTS 变成低电平;当接收寄存器已满时,nRTS 将被设置为高电平。该引脚只适用于硬件流控制。
  • nCTS:清除以发送
  • SCLK:仅适用于同步模式,发送器时钟的输出引脚
  • 2.数据寄存器

    ​ 主要是指USART相关的数据寄存器USART_DR,其只有低9为有效。USART_DR又由两大子寄存器构成,一个是发送可写[^71]的TDR,另一个是接受可读的RDR。

    3.控制器

    ​ 用于进行控制管理(中断控制,发送接收控制等)和时钟同步等相关功能。并由一系列相关寄存器构成。

    4.波特率生成器

    ​ 用于调试串口波特率的相关大小,其生成相关都由具体库函数进行完整封装。

    7.4 代码实现

    主要是实现开发板与计算机之间采用串口进行通讯

    STM32开发板与计算机之间的串口通讯有哪几步?

    USART产生的中断与GPIO外部断及设置区别是什么?

    ​ 实现串口通信,第一步是实现串口初始化操作,其初始化又主要分为:初始化GPIO并将其复用到USART上,USART结构体初始化与USART时钟初始化(同步)并使能,(若涉及到串口中断)再中断向量管家的初始化,最后则是发送和接受函数的改写。

  • USART结构体初始化

    typedef struct
    {
      uint32_t USART_BaudRate;            /*波特率*/
    
      uint16_t USART_WordLength;          /*每帧数据的长度*/
    
      uint16_t USART_StopBits;            /*停止位个数,一般选择为1 */
    
      uint16_t USART_Parity;              /*奇偶校验位,USART_Parity_No(无校验) USART_Parity_Even(偶校验)  USART_Parity_Odd (奇数校验)*/
     
      uint16_t USART_Mode;                /*模式选择即是收还是发,或者收发一起
      USART_Mode_Rx | USART_Mode_Tx;
      */
    
      uint16_t USART_HardwareFlowControl; /*!硬件流控制,硬件流用于防止发接双方波特率不一致而导致数据丢失或是溢出 硬件流控制通过使用额外的信号线,如 RTS(Request to Send)和 CTS(Clear to Send),让接收方告诉发送方何时可以发送或停止发送数据。
    USART_HardwareFlowControl_None:不使用硬件流控制。
    USART_HardwareFlowControl_RTS:仅启用 RTS。
    USART_HardwareFlowControl_CTS:仅启用 CTS。
    USART_HardwareFlowControl_RTS_CTS:同时启用 RTS 和 CTS。 */
    } USART_InitTypeDef;
    
    // 串口中断优先级配置
    NVIC_Configuration();
    static void NVIC_Configuration(void)
    {
      NVIC_InitTypeDef NVIC_InitStructure;
      
      /* 嵌套向量中断控制器组选择 */
    	/* 提示 NVIC_PriorityGroupConfig() 在整个工程只需要调用一次来配置优先级分组*/
      NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
      
      /* 配置USART为中断源 */
      NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
      /* 抢断优先级*/
      NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
      /* 子优先级 */
      NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
      /* 使能中断 */
      NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
      /* 初始化配置NVIC */
      NVIC_Init(&NVIC_InitStructure);
    }
    
    
    // 使能串口接收中断
    USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);	
    	
    // 使能串口
    USART_Cmd(DEBUG_USARTx, ENABLE);	   
    
  • USART时钟初始化(仅限同步传输下才能使用到)

    typedef struct
    {
    
      uint16_t USART_Clock;   /*!时钟使能 */
    
      uint16_t USART_CPOL;    /*!时钟极性*/
    
      uint16_t USART_CPHA;    /*!时钟相位 */
    
      uint16_t USART_LastBit; /*!最尾位时钟脉冲 */
    } USART_ClockInitTypeDef;
    
  • void USART_Config(void)
    {
    	GPIO_InitTypeDef GPIO_InitStructure;
    	USART_InitTypeDef USART_InitStructure;
    
    	// 打开串口GPIO的时钟
    	DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
    	
    	// 打开串口外设的时钟
    	DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
    
    	// 将USART Tx的GPIO配置为推挽复用模式 即输出为复用
    	GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
    
      // 将USART Rx的GPIO配置为浮空输入模式 即输入为浮空
    	GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    	GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
    	
    	// 配置串口的工作参数
    	// 配置波特率
    	USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
    	// 配置 针数据字长(有效数据长度)
    	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    	// 配置停止位
    	USART_InitStructure.USART_StopBits = USART_StopBits_1;
    	// 配置校验位
    	USART_InitStructure.USART_Parity = USART_Parity_No ;
    	// 配置硬件流控制
    	USART_InitStructure.USART_HardwareFlowControl = 
    	USART_HardwareFlowControl_None;
    	// 配置工作模式,收发一起
    	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    	// 完成串口的初始化配置
    	USART_Init(DEBUG_USARTx, &USART_InitStructure);
    	
    	// 串口中断优先级配置
    	NVIC_Configuration();
    	
    	// 使能串口接收中断
    	USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);	
    	
    	// 使能串口
    	USART_Cmd(DEBUG_USARTx, ENABLE);	    
    }
    
  • 发送数据相关函数封装

    void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
    {
    	/* 发送一个字节数据到USART */
    	USART_SendData(pUSARTx,ch);
    		
    	/* 等待发送数据寄存器为空 */
    	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);	//为空时TXE寄存器置1
    }
    
    /****************** 发送8位的数组 ************************/
    void Usart_SendArray( USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num)
    {
      uint8_t i;
    	
    	for(i=0; i<num; i++)
      {
    	    /* 发送一个字节数据到USART */
    	    Usart_SendByte(pUSARTx,array[i]);	
      
      }
    	/* 等待发送完成  发送完成后(即收到停止位) TC寄存器置1*/
    	while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET);
    }
    
    /*****************  发送字符串 **********************/
    void Usart_SendString( USART_TypeDef * pUSARTx, char *str)
    {
    	unsigned int k=0;
      do 
      {
          Usart_SendByte( pUSARTx, *(str + k) );
          k++;
      } while(*(str + k)!='\0');
      
      /* 等待发送完成 */
      while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET)
      {}
    }
    
    /*****************  发送一个16位数 **********************/
    void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch)
    {
    	uint8_t temp_h, temp_l;
    	
    	/* 取出高八位 */
    	temp_h = (ch&0XFF00)>>8;
    	/* 取出低八位 */
    	temp_l = ch&0XFF;
    	
    	/* 发送高八位 */
    	USART_SendData(pUSARTx,temp_h);	
    	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
    	
    	/* 发送低八位 */
    	USART_SendData(pUSARTx,temp_l);	
    	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);	
    }
    

    ​ 这里的发送增加了等待完成的功能,即USART_GetFlagStatus 来获取USART事件标志,并循环检测发送相关的寄存器。相关寄存器,并循环检测寄存器为空这个标志,当寄存器中内容为空时,标志位值1,以此保证数据发送完才退出。

  • 中断响应函数(因为其属于内部中断)

    #define  DEBUG_USART_IRQ                USART1_IRQn
    #define  DEBUG_USART_IRQHandler         USART1_IRQHandler //这里宏定义注意不要错了没有n
    // 串口中断服务函数
    void DEBUG_USART_IRQHandler(void)
    {
      uint8_t ucTemp;
    	if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET) //二次保险,检查相关接受中断寄存器是否触发,若触发置1 
    	{		
    		ucTemp = USART_ReceiveData(DEBUG_USARTx);//暂存接受数据
        	USART_SendData(DEBUG_USARTx,ucTemp); //将接受数据发回到主机端上   
    	}	 
    }
    

    需要注意的是USART虽然也是外部中断,但其触发的机制却与传统的GPIO(EXTI中断)触发有所不同,其触发更多来自内部收到数据(STM32库进行了一系列初始化的封装),因此只需配置其串口接收中断即可即 USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE); 串口中断的触发理论上是每发送1个字节数据触发一次中断。

  • 接受数据函数封装

  • 以下写法为什么发送字符串主机端最终只能收到字符串的最后一位数据?

    void Usart_SendString(USART_TypeDef * Usart, char *str)
    {
        unsigned int num = 0;
        do
        {
            /* code */
            //Usart_SendByte(Usart, *(str+num));//一次只能传输一个字节
            USART_SendData(Usart,*(str+num));
            num++;
        } while (*(str + num) != '\0');
        while (USART_GetFlagStatus(Usart, USART_FLAG_TC) == RESET)
        {
            /* code */
        }
        
    }
    

    USART_SendData是库函数下的串口数据发送函数,一次只能发送一个字节,而原因在于上一个数据还未发送完成就写入了下一个字节数据(字节写入速度 > 字节发送速度),因此导致发送数据的寄存器被覆盖,最终只发送最后一个字节数据。因此前面重写发送字节方法增加了串口发送字节寄存器的检测,即等到发送完一位数据后再发送下一位。

    7.5 串口进行调试的主机回显

    ​ 与上述方法不同的核心在于取消的中断,以及重写了fputc和fgetc,fputc方法使得每次使用printf后会调用该方法,fputc的核心在于设置TXD将MCU的相关信息发送到主机端,而与之相反是fgetc主要是针对scanf函数,功能类似,但是是将主机端的信息发送到MCU的RXD中去。此时也并非都需要LED

    /*bsp_usart.h***/
    #include "stm32f10x.h"
    #include <stdio.h>
    
    #define     GPIO_USART_TXD_PORT       GPIOA
    #define     GPIO_USART_TXD_Pin        GPIO_Pin_9
    #define     GPIO_USART_RXD_PORT       GPIOA
    #define     GPIO_USART_RXD_Pin        GPIO_Pin_10  
    
    #define  USARTx                   USART1
    #define  USART_CLK                RCC_APB2Periph_USART1
    #define  USART_APBxClkCmd         RCC_APB2PeriphClockCmd
    #define  USART_BAUDRATE           115200
    //#define  USART_IRQHandler         USART1_IRQnHandler 这里需要注意中断函数的正确命名是没有n的
    #define  USART_IRQHandler         USART1_IRQHandler
    
    
    void    USART_Config(void);
    void    Usart_SendString(USART_TypeDef * Usartx, char *str);
    /****bsp_usart.h***/
    
    
    #include "bsp_usart.h"
    
    static void NVIC_Config()
    {
        NVIC_InitTypeDef NVIC_InitStructure;
        NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
        NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_Init(&NVIC_InitStructure);
    }
    
    void USART_Config()
    {
        GPIO_InitTypeDef    GPIO_InitStructure;
        USART_InitTypeDef   USART_InitStructure;
    
        //第一步永远都是打开时钟(串口和GPIO时钟)
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
        //2. GPIO的设置-TXD发送为复用模式
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用模式
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_InitStructure.GPIO_Pin = GPIO_USART_TXD_Pin;
        GPIO_Init(GPIO_USART_TXD_PORT, &GPIO_InitStructure);//PORT
    
            //RXD接收为浮空输入模式
        GPIO_InitStructure.GPIO_Pin = GPIO_USART_RXD_Pin;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
        GPIO_Init(GPIO_USART_RXD_PORT, &GPIO_InitStructure);//PORT
    
        //3.配置串口相关参数
        USART_InitStructure.USART_BaudRate = USART_BAUDRATE;
        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;
        USART_Init(USARTx, &USART_InitStructure);
        
        //4.串口中断优先级配置
        //NVIC_Config();
    
        //5.使串口接受中断使能
        //USART_ITConfig(USARTx,USART_IT_RXNE,ENABLE);
       
    
        
        //6.串口使能
        USART_Cmd(USARTx, ENABLE);
    }
    
    //重写发送一个字节字符
    void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
    {
    	/* 发送一个字节数据到USART */
    	USART_SendData(pUSARTx,ch);
    		
    	/* 等待发送数据寄存器为空 */
    	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);	
    }
    
    //重写一下发送一个字符串,
    void Usart_SendString(USART_TypeDef * Usart, char *str)
    {
        unsigned int num = 0;
        do
        {
            /* code */
            Usart_SendByte(Usart, *(str+num));//一次只能传输一个字节
            //USART_SendData(Usart,*(str+num));
            num++;
        } while (*(str + num) != '\0');
        while (USART_GetFlagStatus(Usart, USART_FLAG_TC) == RESET)
        {
            /* code */
        }
        
    }
    
    //重写printf方法使其定位到串口,FILE表示一个流式写法,嵌入式系统中printf中的每一个字符会传给fput中的ch 并转换为ASCII
    int fputc(int ch, FILE *f)
    {
    		/* 发送一个字节数据到串口 */
    		USART_SendData(USARTx, (uint8_t) ch);
    		
    		/* 等待发送完毕 */
    		while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);		
    	
    		return (ch);
    }
    
    ///重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
    int fgetc(FILE *f)
    {
    		/* 等待串口输入数据 */
    		while (USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) == RESET);
    
    		return (int)USART_ReceiveData(USARTx);
    }
    

    总结: 在不同端机器通讯中,串口算是比较基础的一种方式,STM32中串口通讯只需注意 TXD,RXD,GND三个引脚即可。其中前两个是主要配置相关。串口一帧的数据通常由起始位,有效数据,(选)校验位,终止位几部分构成。通信双方的波特率需一致,1.因为串口初始化涉及到引脚,因此需先进行GPIO的初始化(TXD相关引脚为复用,RXD引脚浮空输入) 。 2.串口相关的结构初始化操作 3.(选)中断相关的初始化(主要是NVIC和中断源及中断响应函数编写) 4.发送与接收的函数改写。

    作者:神奇小炒肉

    物联沃分享整理
    物联沃-IOTWORD物联网 » 一文搞懂STM32的串口通信(附库函数代码讲解)

    发表回复