介绍

串口(UART通用异步收发器,TTL)通讯是一种设备间的串行全双工通讯方式。由于UART是异步传输,没有传输同步时钟,为了保证数据的正确性,UART采用16倍数据波特率的时钟进行采样。因为它简便捷,因此大部分电子设备都支持该通讯方式工程师在调试设备时也经常使用该方式输出调试信息。
本文详细的介绍如何来编写一个串口收发程序,我们采用常用的收发逻辑,发送直接编写函数进行实现,而接收使用中断进行完成。接收中断使用接收到一个字节和一帧数据两种中断触发方式。

USART中断

USART 有多个中断请求事件。

之所以介绍这个USART中断请求,是因为很多人在初学阶段,对串口怎么判断串口中断的状态不太了解,所以我这里重点来介绍一下。
一般在我们开始和配置完串口中断后,进入串口中断处理程序的情况会有很多,我们也可以自己选择打开哪些串口中断情况。一般情况下,我们在接受时主要使用的中断事件标志是RXNE和IDLE。
RXNE是接收中断,每接收一个字节都会出发这个中断,也是我们用的最频繁的中断请求。
IDLE 是空闲中断,每接收完一帧数据,总线就会暂时空闲,就会触发这个中断。

串口状态

串口的状态可以通过状态寄存器 USART_SR 读取。USART_SR 的各位描述如下:

这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。
RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以该位清零,也可以向该位写 0,直接清除。
TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。果设置了这个位的中断,则会产生中断。该位也有两种清零方式:

  1. 读 USART_SR,USART_DR。
  2. 直接向该位写 0。

实例

需求分析

本项目主要编写一个串口收发的实例。使用STM32F103C8T6充当MCU,在PC上使用串口调试助手充当上位机。每次PC向MCU下发一帧数据, MCU每接收一个字节数据,检查一下数据中是否有指令0x23,当接收到指令0x23的时候,MCU向上位机发送“PC”。当一帧数据接收完毕后,MCU向上位机发送“Receive a frame data”.

串口初始化

串口初始化的一般步骤可以总结为如下几个步骤:

  1. 串口时钟使能,GPIO 时钟使能。
  2. 设置引脚复用器映射:调用 GPIO_PinAFConfig 函数。
  3. GPIO 初始化设置:要设置模式为复用功能。
  4. 串口参数初始化:设置波特率,字长,奇偶校验等参数。
  5. 开启中断并且初始化 NVIC,使能中断(如果需要开启中断才需要这个步骤)。
  6. 使能串口。
#include "usart.h"
#include <stdio.h>
#include "stm32f1xx_hal.h
UART_HandleTypeDef huart3
void MX_USART3_UART_Init(void)

  huart3.Instance = USART3;
  huart3.Init.BaudRate = 115200;
  huart3.Init.WordLength = UART_WORDLENGTH_8B;
  huart3.Init.StopBits = UART_STOPBITS_1;
  huart3.Init.Parity = UART_PARITY_NONE;
  huart3.Init.Mode = UART_MODE_TX_RX;
  huart3.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart3.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart3) != HAL_OK)
  {
    Error_Handler()
	__HAL_UART_ENABLE_IT(&huart3,UART_IT_RXNE);//接收中断使能
	__HAL_UART_ENABLE_IT(&huart3,UART_IT_IDLE);//空闲中断使能 
}

void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART3)
  {
    __HAL_RCC_USART3_CLK_ENABLE()
    __HAL_RCC_GPIOB_CLK_ENABLE();
    /**USART3 GPIO Configuration
    PB10     ------> USART3_TX
    PB11     ------> USART3_RX
    */
    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct)
    GPIO_InitStruct.Pin = GPIO_PIN_11;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    HAL_NVIC_SetPriority(USART3_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(USART3_IRQn);

我们平时需要改的其实就是串口的一些参数配置。

  • BaudRate:波特率
  • WordLength;:字长
  • StopBits:停止位
  • Parity:奇偶校验
  • Mode:收/发模式设置
  • HwFlowCtl:硬件流设置
  • OverSampling:过采样设置

串口发送

串口发送这里使用的非中断发送方式。

/*******************************************************************************
  * @函数名称	USART_Send
  * @函数说明   发送信息
  * @输入参数   _UART:串口号
			data:要发送的信息的首地址
			len: 发送的长度
  * @输出参数   无
  * @返回参数   无
*******************************************************************************/
void USART_Send(USART_TypeDef *_UART,uint8_t *data,uint8_t len)
{
	for(int i;i<len;i++)
	{
		HAL_UART_Transmit(&huart3,&data[i],1,1000);
	}
}

主要使用的是HAL_UART_Transmit(&huart3,&Res,1,0Xffff);
这是一个阻塞的发送函数,无需重复判断串口是否发送完成。发送每个字符,直到遇空字符才停止发送。其中第一个参数是串口号,第二个参数是要发送的数据起始地址,第三个是要发送的数据长度,第四个超时时间(超过此长度仍未发送成功则阻塞完毕,停止发送,函数执行完毕)。

串口接收

这里串口接收使用的是中断的方式。
中断的类别在文章的最上边已经介绍过。我们在初始化时设定触发中断的类型。本文中设置的

__HAL_UART_ENABLE_IT(&huart3,UART_IT_RXNE);//接收中断使能
__HAL_UART_ENABLE_IT(&huart3,UART_IT_IDLE);//空闲中断使能 

代表只有接收数据和空闲中断会触发。
在stm32f1xx_it.c中有我们的串口中断处理函数。我们将这个函数进行重构。

void USART3_IRQHandler(void)
{
	uint8_t Res;
if(__HAL_UART_GET_FLAG(&huart3,UART_FLAG_RXNE)!=RESET
{
	HAL_UART_Receive(&huart3,&Res,1,0Xffff); 
 if(Res==0x23)	
	printf("PC");
}
else if(__HAL_UART_GET_FLAG(&huart3,UART_FLAG_IDLE)!=RESET)//空闲中断(代表这一帧数据传输完了)
{
	printf("Receive a frame data.");
	__HAL_UART_CLEAR_IDLEFLAG(&huart3);
}

这里面的几个重点,我们来一一介绍。
首先是判断标志位,我们使用HAL库中的__HAL_UART_GET_FLAG()函数,里面有两个参数,前者是串口句柄,后者是具体哪个标志位。
if(__HAL_UART_GET_FLAG(&huart3,UART_FLAG_RXNE)!=RESET)用来检测是否检测到有单个字节的中断。
if(__HAL_UART_GET_FLAG(&huart3,UART_FLAG_IDLE)!=RESET)用来检测是否有空闲中断(代表这一帧数据传输完了)。

重定向printf和scanf

还有一点需要注意的,使用 fput 和 fgetc 函数达到重定向 C 语言标准库输入输出函数必须在 MDK 的工程选项把“Use MicroLIB”勾选上, MicoroLIB 是缺省 C 库的备选库,它对标准 C 库进行了高度优化使代码更少,占用更少资源
为使用 printf、 scanf 函数需要在文件中包含 stdio.h 头文件。

/**
  * 函数功能: 重定向c库函数printf
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明:无
  */
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}
 
/**
  * 函数功能: 重定向c库函数getchar,scanf
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明:无
  */
int fgetc(FILE *f)
{
  uint8_t ch = 0;
  HAL_UART_Receive(&huart3, &ch, 1, 0xffff);
  return ch;
}

效果

  1. PC下发:11 22 33 44
  2. PC下发:12 23 34 45

后续

欢迎关注#公众号:物联网知识。
更多知识可以订阅我的项目实战专栏。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐