使用HAL库实现STM32串口通信

一、实验条件

将STM32的PA9复用为串口1的TX,PA10复用为串口1的RX。STM32芯片的输出TX和接收RX与CH340的接收RX和发送TX相连(收发交叉且PCB上默认没有相连,所以需要用P3跳线帽进行手动连接),CH340的另一端通过USB口引出与USB线相接。CH340作用:RS232电平标准转USB电平标准)。再使用USB转串口线实现PC与板子的通信。

PC端需要安装CH340虚拟串口驱动,目的是为了有CH340的通信协议。在使用串口调试助手进行通信时注意一下几点。

1.发送英文字符需要用一个字符即8位,发送汉字需要两个字符即16位,如上图,发送汉字“宋”实际是发送“CB(1100 1011)CE(1100 1110)”而发送英文字符S实际是发送“53(0101 0011)”,本质上没有太大区别;

2.勾选了下方“发送新行”后,XCOM就会再你输入的需要发送的数据后自动加上一个回车(0X0D+0X0A),如果不勾选则我们在手动输入完“宋S”后还需敲一个回车键只有这样点击发送后,调试助手上方窗体才能将其显示,这是因为我们在程序的串口中断中自定义了一个数据接收协议,即只有当接受的数据以回车结尾(0X0D+0X0A),串口才认可数据接受完毕。

二、例程软件实现

1、支持printf函数 这个函数大部分无需改动 但是针对不同的串口时:要记得改USART1为对应串口

//加入以下代码,支持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 

2、初始化

#if EN_USART1_RX   //如果使能了接收
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误          
u8 aRxBuffer[RXBUFFERSIZE];//HAL库使用的串口接收缓冲
UART_HandleTypeDef UART1_Handler; //UART句柄
//初始化IO 串口1 
//bound:波特率
void uart_init(u32 bound)
{    
    //UART 初始化设置
    UART1_Handler.Instance=USART1;                        //USART1  让Instance指向的寄存器基地址 等于串口1的外设基地址
    UART1_Handler.Init.BaudRate=bound;                    //波特率
    UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B;   //字长为8位数据格式
    UART1_Handler.Init.StopBits=UART_STOPBITS_1;        //一个停止位
    UART1_Handler.Init.Parity=UART_PARITY_NONE;            //无奇偶校验位
    UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE;   //无硬件流控
    UART1_Handler.Init.Mode=UART_MODE_TX_RX;            //收发模式
    HAL_UART_Init(&UART1_Handler);                        //HAL_UART_Init()会使能UART1
    
    HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);//该函数会开启接收中断:标志位UART_IT_RXNE,并且设置接收缓冲以及接收缓冲接收最大数据量
  
}

void uart_init(u32 bound)函数需要注意的点

1、HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)

(1)HAL_StatusTypeDef为一个枚举类型,也就是当返回值比较多,就全枚举到HAL_StatusTypeDef里

(2)huart是UART_HandleTypeDef类型的结构体指针变量,也称为串口句柄,指向结构体里第一个成员基地址。一开始就定义了UART_HandleTypeDef UART1_Handler;

UART1_Handler为UART_HandleTypeDef类型的结构体变量,可用于引用结构体里成员,取&为第一个成员基地址

即:huart = &UART1_Handler

所以调用HAL_UART_Init函数时,括号里应该填&UART1_Handler

(3)第二个形参的位置应该填一个指针,数组名可以也看作指向数组里第零个元素的指针,但是这里把aRxBuffer强制转换为指针类型,个人认为只是一种格式规范,已上板验证过,就算没有强制转换,也能实现串口通信。(为什么是u8?因为uint8_t *pData入口参数规定的)

(4)第三个形参,表示每次接收SIZE个字节后标示接收结束,然后会进入接收回调函数。

整个接收的过程卡了我一天……最终总结为以下:

串口调试助手每发送一个帧,接收移位寄存器把这一帧里有效位给接收数据寄存器(此观点为个人理解),然后接收寄存器(RDR)接收到了数据后,产生RXNE中断,进入中断处理函数,中断处理函数对里判断到是接收中断调用 UART_Receive_IT(huart)函数

 if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))//判断是否为读数据寄存器非空中断
    {
      UART_Receive_IT(huart); //调用接收函数
      return;
    }
  }

在接收函数里把接收到的值放pRxBuffPtr,同时RxXferCount–,直到RxXferCount减到零,调用接收回调函数。(当RxXferCount=0时表明这次已经传完,进行中断失能的处理)

    if (--huart->RxXferCount == 0U)
    {
      /* Disable the UART Data Register not empty Interrupt */  
      __HAL_UART_DISABLE_IT(huart, UART_IT_RXNE);    

      /* Disable the UART Parity Error Interrupt */
      __HAL_UART_DISABLE_IT(huart, UART_IT_PE);

      /* Disable the UART Error Interrupt: (Frame error, noise error, overrun error) */
      __HAL_UART_DISABLE_IT(huart, UART_IT_ERR);

      /* Rx process is completed, restore huart->RxState to Ready */
      huart->RxState = HAL_UART_STATE_READY;

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
      /*Call registered Rx complete callback*/
      huart->RxCpltCallback(huart);
#else
      /*Call legacy weak Rx complete callback*/
      HAL_UART_RxCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */

      return HAL_OK;
    }

问题:RxXferCount、pRxBuffPtr、RxXferSize(这三个都是UART_HandleTypeDef结构体里的成员)在哪里被赋值?(pTxBuffPtr,TxXferSize 和 TxXferCount 三个变量分别用来设置串口发送的数据缓存指针,发送的数据量和还剩余的要发送的数据量)

在HAL_UART_Receive_IT()里,把形参给pRxBuffPtr、RxXferSize。(RxXferCount初始值为RxXferSize)。

我们可以看到例程里RXBUFFERSIZE=1,aRxBuffer[]容量也为1,所以RxXferCount=1,每收到1个字节就去一次接收回调函数,那难道说这个例程只能适用于接收1个字节的时候吗?

HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);

答案是否定的,这里只是一个一个的接收,在回调函数里还要及时把接收的这个字节放在数组USART_RX_BUF[USART_REC_LEN]里,USART_REC_LEN为200。

但是这样的话,我没法知道接收完成没有,所以在串口回调函数里设计了小小的通信协议。

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance==USART1)//如果是串口1
    {
        if((USART_RX_STA&0x8000)==0)//USART_RX_STA就是个自己定义的变量 16位
        {
            if(USART_RX_STA&0x4000)//接收到了0x0d
            {
                if(aRxBuffer[0]!=0x0a)USART_RX_STA=0;//接收错误,重新开始  字节
                else USART_RX_STA|=0x8000;    //接收完成了 
            }
            
            else //还没收到0X0D
            {    
                if(aRxBuffer[0]==0x0d)USART_RX_STA|=0x4000;
                else
                {
                    USART_RX_BUF[USART_RX_STA&0X3FFF]=aRxBuffer[0] ;
                    USART_RX_STA++;
                    if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收      
                }         
            } //接收到的内容就放在USART_RX_BUF里 主函数里再决定怎么处理
        }
    }
}

当接收到从电脑发过来的数据,把接收到的数据保存在 USART_RX_BUF 中,同时在接收状态寄存器(USART_RX_STA)中计数接收到的有效数据个数,当收到回车(回车的表示由 2个字节组成: 0X0D 和 0X0A)的第一个字节 0X0D 时,计数器将不再增加,等待 0X0A 的到来,而如果 0X0A 没有来到,则认为这次接收失败,重新开始下一次接收。如果顺利接收到 0X0A,则标记 USART_RX_STA 的第 15 位,这样完成一次接收,并等待该位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到 0X0D,那么在接收数据超过 USART_REC_LEN 的时候,则会丢弃前面的数据,重新接收。

注意:USART_RX_STA[13:0]位用来装已接收的字节个数,可以有16384个,但是USART_RX_BUF[USART_REC_LEN]里,USART_REC_LEN为200,所以最多接收200个字节。


三、常用的串口中断处理函数

我们直接把中断控制逻辑写在中断服务函数内部,不适用中断回调函数

此时不用初始化HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);

直接在要开启中断的地方使用__HAL_UART_ENABLE_IT()单独开启中断即可。

调用了HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)对接收到的字符进行处理。

//串口1中断服务程序
void USART1_IRQHandler(void)                    
{ 
    u8 Res;
    HAL_StatusTypeDef err;
#if SYSTEM_SUPPORT_OS         //使用OS
    OSIntEnter();    
#endif
    if((__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_RXNE)!=RESET))  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
    {    //UART_FLAG_RXNE产生了 说明接收函数处理了数据
       HAL_UART_Receive(&USART1_Handler,&Res,1,1000);
   
        if((USART_RX_STA&0x8000)==0)//接收未完成
        {
            if(USART_RX_STA&0x4000)//接收到了0x0d
            {
                if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
                else USART_RX_STA|=0x8000;    //接收完成了 
            }
            else //还没收到0X0D
            {    
                if(Res==0x0d)USART_RX_STA|=0x4000;
                else
                {
                    USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
                    USART_RX_STA++;
                    if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收      
                }         
            }
        }            
    }
    HAL_UART_IRQHandler(&UART1_Handler); //为什么还需要这句 不明确?   
#if SYSTEM_SUPPORT_OS         //使用OS
    OSIntExit();                                               
#endif
} 
#endif    

UART_WaitOnFlagUntilTimeout(UART_HandleTypeDef *huart, uint32_t Flag, FlagStatus Status, uint32_t Tickstart, uint32_t Timeout)

四、主函数

while(1)
    {
            
       if(USART_RX_STA&0x8000)
        {                       
            len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
            printf("\r\n您发送的消息为:\r\n");
            HAL_UART_Transmit(&UART1_Handler,(uint8_t*)USART_RX_BUF,len,1000);    //发送接收到的数据
            while(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)!=SET);        //等待发送结束
            printf("\r\n\r\n");//插入换行
            USART_RX_STA=0;
        }else
        {
            times++;
            if(times%5000==0)
            {
                printf("\r\nALIENTEK 精英STM32开发板 串口实验\r\n");
                printf("正点原子@ALIENTEK\r\n\r\n\r\n");
            }
            if(times%200==0)printf("请输入数据,以回车键结束\r\n");  
            if(times%30==0)LED0=!LED0;//闪烁LED,提示系统正在运行.
            delay_ms(10);   
        } 
    }
}
HAL_UART_Transmit(&UART1_Handler,(uint8_t*)USART_RX_BUF,len,1000); 

1、第二个形参 为数组名USART_RX_BUF强制转换为的指针,指向数组第0个元素,指针实现自加的语法在这个发送函数里

if (huart->Init.Parity == UART_PARITY_NONE)
        {
          pData += 2U;
        }
        else
        {
          pData += 1U;
        }

2、这个是超时,在设置的这个时间内没有发送完成,就返回超时(HAL_TIMEOUT)

如何取:

如果波特率为9600,发送一个位需要的时间为1/9600s=0.0001042s=0.1042ms,这里按数据位为8位,停止位为2位,

加起来就是10位,10个位发送所需的时间为:0.1042*10ms = 1.042ms,如果我要发送10个字节的数据,那发送这10个字节数据给接收方需要

的时间为:10*1.042ms = 10.42ms,这是算实际的发送10个字节的数据所需要的时间。我们在接收方接收数据时可以

把时间再加宽一些,让它有一点余量。让接收方能稳定的把数据从发送方接手过来,可以加个5ms,或更宽一点10ms,

加上发送10个字节所花的时间,就是15ms或20ms。

3、len 是取的USART_RX_STA的【13:0】位,总担心是个十六进制,不要担心,所有进制都会被化为二进制。

4、while(__HAL_UART_GET_FLAG(&UART1_Handler,UART_FLAG_TC)!=SET);

这里的传输完成指的是USART_RX_BUF[len]传输完成,而不是发送一个字节寄存器产生那个传输完成。

物联沃分享整理
物联沃-IOTWORD物联网 » 使用HAL库实现STM32串口通信

发表评论