一、引言

串口通讯是最常见的通讯方式,基本所有的MCU都会带有串口,由其引出了各种通信标准和通信协议,如232、485、Modbus、LIN等,是一种全双工串行通信,STM32的串口USART同时支持同步、异步通讯,(一般仅支持异步通信的串口名为UART)。

典型的异步串口通信使用三线制:Tx、Rx、GND。

二、基于固件库配置串口通讯

1. 硬件连接

STM32F407ZGT6 最多可提供 6 路串口,一般STM32的USART1均为PA9/PA10,

2. 常用寄存器

2.1 状态寄存器 (USART_SR)

常用的标志位有

2.2 数据寄存器 (USART_DR)

用来存放收发数据

其他的,比如串口内部电路架构、波特率寄存器、控制寄存器,感兴趣的同志们可自行研究。另外,关于串口通讯总线上的空闲位、停止位等,感兴趣也可自己查阅芯片手册。

3. 基于标准固件库STM32串口通讯

关于如何在Keil5工程中添加.c\.h文件,在前面几篇文章中已经多次提到,本章不再赘述。

3.1 中断接收

串口中断接收是一种常用的策略,基于固件库,使串口收发数据,具体初始化配置如下

static GPIO_InitTypeDef USART1_pin_init_str =
{
    GPIO_Pin_9 | GPIO_Pin_10,
    GPIO_Mode_AF,//复用模式
    GPIO_Fast_Speed,
    GPIO_OType_PP,
    GPIO_PuPd_UP,
};

USART_InitTypeDef USART1_init_str = 
{
    115200,//波特率115200
    USART_WordLength_8b,//8位数据位
    USART_StopBits_1,//1位停止位
    USART_Parity_No,//无奇偶校验
    USART_Mode_Rx|USART_Mode_Tx,//RX和TX功能全部初始化
    USART_HardwareFlowControl_None//无硬件流
};

NVIC_InitTypeDef USART1_IRQ_Init =
{
    USART1_IRQn,//串口1中断事件
    1,//抢占优先级为1
    1,//响应优先级为1
    ENABLE//使能中断事件
};
void USART1_Init(void)
{
    /*enable clock for PA9&PA10*/
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
    /*init PA9&PA10*/
    GPIO_Init(GPIOA,&USART1_pin_init_str);
    /*choose which periph to pin reuse */
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);
    
    /*enable clock for USART1*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
    /*init usart1*/
    USART_Init(USART1,&USART1_init_str);
    /*Enable usart1*/
    USART_Cmd(USART1,ENABLE);
    /*enough configration if do not use interrupt*/

    /*ENABLE USART1 Rx Interrupt: receive is not empty*/
    USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
    NVIC_Init(&USART1_IRQ_Init);/*NVIC*/

}

配置比较简单,PA9、PA10复用为串口,并打开接收中断,抢占优先级和响应优先级均为1

中断处理函数如下:其目的是将接收到的数据原封不动的发送出去,而后清除中断标志位

/*interrupt function, all interrupt functions are in startup_stm32f40_41xxx.s, no return and input parameters*/
void USART1_IRQHandler(void)
{
    uint16_t data = 0;
    if (SET == USART_GetITStatus(USART1,USART_IT_RXNE))
    {
        data = USART_ReceiveData(USART1);
        USART_SendData(USART1,data);

        USART_ClearITPendingBit(USART1,USART_IT_RXNE);
    }
    
}

主函数中的代码如下

int main(void)
{

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    USART1_Init();

    while (1)
    {
        /*do nothing*/
    }
    
}

3.2 发送字符串

上文中,发送数据,调用的是USART_SendData(USART1,data);该函数每次仅能发送一个字节数据,通过循环调用该函数,便可以实现字符串的发送

static void USART_Str_Send(uint8_t *in_str,uint8_t len)
{
    uint8_t i;
    for ( i = 0; i < len; i++)
    {
        while (RESET == USART_GetFlagStatus(USART1,USART_FLAG_TC));
        USART_SendData(USART1,in_str[i]);

    }
}

上段代码判断TC,发送完成标志,如果该标志位为0,则盲等。

3.3 查询接收

如果不使用中断,通过不断查询RXNE位,也能实现串口接收

此时,串口初始化配置为3.1节中的配置删去中断的内容

static GPIO_InitTypeDef USART1_pin_init_str =
{
    GPIO_Pin_9 | GPIO_Pin_10,
    GPIO_Mode_AF,//复用模式
    GPIO_Fast_Speed,
    GPIO_OType_PP,
    GPIO_PuPd_UP,
};

USART_InitTypeDef USART1_init_str = 
{
    115200,//波特率115200
    USART_WordLength_8b,//8位数据位
    USART_StopBits_1,//1位停止位
    USART_Parity_No,//无奇偶校验
    USART_Mode_Rx|USART_Mode_Tx,//RX和TX功能全部初始化
    USART_HardwareFlowControl_None//无硬件流
};

void USART1_Init(void)
{
    /*enable clock for PA9&PA10*/
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
    /*init PA9&PA10*/
    GPIO_Init(GPIOA,&USART1_pin_init_str);
    /*choose which periph to pin reuse */
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);
    
    /*enable clock for USART1*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
    /*init usart1*/
    USART_Init(USART1,&USART1_init_str);
    /*Enable usart1*/
    USART_Cmd(USART1,ENABLE);
    /*enough configration if do not use interrupt*/

}

main函数中的代码如下

int main(void)
{
    uint8_t data;

    USART1_Init();

    while (1)
    {   
        if (SET == USART_GetFlagStatus(USART1,USART_FLAG_RXNE))
        {
            data = USART_ReceiveData(USART1);
            USART_SendData(USART1,data);
        }
    }
}

上述代码,在while(1)循环中不断查询RXNE标志位,一旦该标志位置1,意味着接收非空

3.4 usart文件夹

前文介绍了正点原子SYSTEM文件夹中所提供的sys、delay文件夹,现在介绍一下usart文件夹,该文件夹默认以PA9\PA10作为串口,进行串口初始化,并可以在usart.h文件中通过EN_USART1_RX宏定义打开或关闭接收中断的配置。其中的内容还是那个问均已经介绍过。值得关注的是,在usart.c文件中有如下重定义函数:

struct __FILE 
{ 
	int handle; 
}; 

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

该重定义基于一个C语言的一个原则:如果用户自己定义的函数与标准库中已有的函数冲突,则以用户自定义函数为准。上述函数中,USART1->SR&0X40其实判断的就是TC标志位

如此,便可使用printf函数实现字符串发送,如

printf("hello world!")

三、基于CubeMx配置USART

关于CubeMx如何新建工程,前文已经讲得很细致,读者若不理解,可查看专栏内往期文章。本文基于前文所建立的工程,进行串口配置添加

使用串口1,PA9(USART_TX), PA10(USART_RX),波特率115200

首先要配置GPIO引脚,CubeMx中引脚配置不知如何开始的请参照本专栏往期内容

串口USART1模块配置

生成代码,此时默认使用轮询方式进行输入输出

接收接口:HAL_UART_Receive()  发送接口:HAL_UART_Transmit()

以上两条函数均为固定长度数据接收\发送

此时,接收与发送的代码可写为

// Pollng USART1
    if (HAL_OK == HAL_UART_Receive(&huart1,UART1_RecBuffer_Polling,7,100))
    {
      HAL_UART_Transmit(&huart1,UART1_RecBuffer_Polling,7,100);
    }

上述代码实现功能为,将接收的数据存到UART1_RecBuffer_Polling中,然后进行发送。

利用重定向fputc/fgetc函数使用printf/scanf函数实现输入输出

  1. main.h文件中添加stdio.h,注意添加位置在USER区域
  2. 在main.c文件user code4区域重定义
int fputc(int ch, FILE *F)
{
  //串口1采用轮询方式发送一个字节数据,超时时间设置为无限等待
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}
int fgetc(FILE *F)
{
  //串口1采用轮询方式接收1字节数据,超时时间设置为无限等待
  int ch;
  HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

编译之前,需要勾选Use MicroLIB

进行重定义后,便可写出如下代码,实现的功能为,scanf函数将接收到的数据保存到UART1_RecBuffer_Polling中,如果开始两个字符均为'a',则printf函数输出"Get it!!!",此时,可以收发不固定长度的数据

    scanf("%s",UART1_RecBuffer_Polling);
    if ((UART1_RecBuffer_Polling[0] == 'a') && (UART1_RecBuffer_Polling[1] == 'a'))
    {
       printf("Get it!!!");
    }

如果想要开启中断,则需要配置NVIC

中断接收:HAL_UART_Receive_IT,中断发送:HAL_UART_Transmit_IT,也均为固定长度

并且可用上述函数使能接收、发送中断,该函数执行一次后会关闭中断使能,有需要的话要在重新调用上述接口使能中断

HAL库的USART1_IRQHandler自动判断哪个中断,接受完相应长度的数据后自动进入不同的中断函数,自动清除标志位

如接收中断回调函数: HAL_UART_RxCpltCallback

对于串口中断接收,用户只需开启接收中断,并在callback函数中重复开启中断即可

实现代码如下

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    USART1_RX_Finished = 1;//Rx Finished
    //enable Rx interrupt again, data will store in UART1_RecBuffer_IT, 7字节
    HAL_UART_Receive_IT(&huart1,(uint8_t *)UART1_RecBuffer_IT,7);again

  }
  
}

并在main函数中初始化时候使能接受中断,随后while(1)循环中执行如下动作

//use this to enable rx interrupt, data will store in UART1_RecBuffer_IT,7 bytes
HAL_UART_Receive_IT(&huart1,UART1_RecBuffer_IT,7);

while(1)
{
    if (USART1_RX_Finished == 1)
    {
        USART1_RX_Finished = 0;
        printf("Rx data is: ");
        //enable Tx interrupt, 7 bytes
        HAL_UART_Transmit_IT(&huart1,UART1_RecBuffer_IT,7);
        printf("\n\r");
    }
}

注意,使用中断方式接收也会有长度的要求

四. 结语

本文介绍了STM32串口开发过程,基于标准固件库、CubeMx+HAL库的形式,介绍了USART的配置过程,包括查询、中断,以及scanf和printf函数的重定义,并分别辅以简单的示例代码

在本文的介绍过程中,对于串口的电路原理、寄存器、总线电平等方面并未做详细介绍,因为那些背离了我写这篇专栏的初衷:不进行老学究式的详细研究,而是能够帮助同学们快速上手STM32的开发,始终牢记程序员第一要义:能跑就行,别管怎么跑的。

另外,不对上述内容做进一步介绍还有一个原因:我也不会。。。。。。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐