STM32F103串口UART详解

目录

  • 一、UART协议简介
    • 1.UART通信的基本原理
    • 2.UART通信过程
    • 3.UART的常见特点
    • 4.UART通信的应用场景
    • 5.补充
  • 二、UART硬件结构
    • 1.内部框图
    • 2.设置:波特率、数据位、校验位、停止位
    • 3.发送数据
    • 4.接收数据
    • 5.中断方式
  • 三、UART串口中断模式配置说明
    • 1.HAL库外设初始化MSP回调机制-USART
    • 2.HAL库中断回调机制-USART
    • 3.USART/UART异步通信配置
  • 四、UART编程
    • 1.查询方式
    • 2.中断方式
    • 3.DMA方式
    • 4.DMA+IDLE方式
    • 5.printf函数实现(需包含 #include "stdio.h")

一、UART协议简介

UART(Universal Asynchronous Receiver-Transmitter) 协议是一种常用的串行通信协议,用于计算机和外设之间进行数据传输。它是一种异步通信协议,意味着发送和接收数据时不需要共享时钟信号。UART通常用于短距离、低速的数据交换,广泛应用于嵌入式系统、传感器通信、串口设备等领域。

1.UART通信的基本原理

  1. 串行传输:数据通过一条传输线(TX)发送,一条接收线(RX)接收,数据位逐位传送。
  2. 异步传输:不需要发送方和接收方有共同的时钟信号,数据的发送和接收通过约定的波特率(Baud Rate)同步。
  3. 起始位、数据位和停止位:
    • 起始位:在数据传输开始时,发送一位低电平信号(通常是0),以表明数据传输开始。
    • 数据位:数据以二进制形式逐位传输,常见的数据位长度是5、6、7或8位,其中8位居多。
    • 停止位:在数据传输完成后,发送一或多个高电平(通常是1)信号,表示数据传输结束。常见的停止位长度是1位或2位。
  4. 波特率:波特率是指每秒钟传输的数据位数。例如,9600、115200是常见的波特率值。发送方和接收方必须设置相同的波特率,确保数据正确传输。

2.UART通信过程

  1. 发送端将数据分解为多个数据位,每个数据位通过TX线按约定波特率发送出去。
  2. 接收端通过RX线接收数据位,并根据设定的波特率、起始位、数据位和停止位重新组装数据。

3.UART的常见特点

  1. 简单易用:由于不需要时钟信号,UART通信简单、硬件要求低。
  2. 低速传输:通常适用于短距离、低速的数据交换,传输速率取决于波特率。
  3. 全双工/半双工:可以支持全双工(同时发送和接收)或半双工(只能单向传输)通信。
  4. 低成本:硬件实现简单,通常只需要两条线(TX和RX)即可完成通信。

4.UART通信的应用场景

  1. 嵌入式系统:在微控制器、传感器、GPS模块、蓝牙模块等嵌入式设备之间进行数据交换。
  2. 串口通信:计算机与外部设备(如调制解调器、打印机、串口设备)之间的通信。
  3. 调试接口:许多开发板和嵌入式系统提供UART接口用于调试、日志输出等功能。

5.补充

  1. 起始位探测:自己的RX引脚之前是高电平状态,一旦检测到有一个低电平的跳变之后会发起16次的检测,如果前面7次检测到最少两次的低电平就说明有可能是低电平从而继续检测,在8、9、10三次中至少检测到两个低电平就认为接收到了起始位,下面便开始接收数据。
    起始位

  2. 数据位探测:在1bit的时间中连续检测16次,如果中间8、9、10三次检测到两次及以上低电平则认为是低电平,检测到两次及以上高电平则认为是高电平。
    在这里插入图片描述

  3. TTL/COMS逻辑电平下传输’A’的波形:电压较低容易受到像静电这样的干扰
    A的ASCII码为65,二进制表示是 01000001
    在这里插入图片描述

  4. RS-232逻辑电平下传输’A’的波形:提高电压以便提高抗干扰能力
    在这里插入图片描述

  5. 校验位

    • 发送方计算数据位中1的个数,如果是偶数个,则校验位设置为1,使得数据位和校验位的1的总数为奇数;如果是奇数个,则校验位设置为0,确保1的个数是奇数。
    • 假设我们传输的数据是10101100,如果采用偶校验:数据位是10101100,其中1的个数是4(偶数),所以校验位应该是0,以保持1的个数为偶数;如果采用奇校验,校验位应该是1,因为这样可以使1的总数变为奇数(5个1)。
  6. 波特率:1秒内传输信号的状态数(波形数)。比特率:1秒内传输数据的bit数。如果一个波形能表示N个bit,那么波特率*N=比特率

二、UART硬件结构

1.内部框图

在实际工作中不需要像发送起始位、数据位、停止位这样一位一位发送数据,有专门的硬件串口帮忙进行发送,只需要将数据写入对应寄存器中即可。
在这里插入图片描述

2.设置:波特率、数据位、校验位、停止位

  1. USART_BRR (波特率寄存器):波特率通过设置 USART_BRR 寄存器来配置。该寄存器存储了波特率的分频值。

    • DIV_Mantissa:波特率的整数部分。
    • DIV_Fraction:波特率的小数部分。
  2. 设置数据位(Data Bits):数据位的设置由 USART_CR1 寄存器中的 M 位 (Bit 12)来控制。

    • M = 0 表示 8 位数据。
    • M = 1 表示 9 位数据。
  3. 设置停止位(Stop Bits):停止位的设置由 USART_CR2 寄存器中的STOP 位( Bits 13:12)来控制。

    • 00 = 1 位停止位。
    • 01 = 0.5 位停止位。
    • 10 = 2 位停止位。
    • 11 = 1.5 位停止位。
  4. 设置校验位(Parity):校验位的设置由 USART_CR1 寄存器中的PS位 ( Bit 9)和 PCE位 (Bit 10)来控制。

    • PCE (Parity Enable):启用或禁用校验位。设置为 1 启用,设置为 0 禁用。
    • PS (Parity Selection):设置校验类型:
    • PS = 0:偶校验。
    • PS = 1:奇校验。

3.发送数据

  1. 数据val写入到TDR寄存器中,该寄存器中的数值会移动到Shift寄存器中,然后Shift寄存器会自动将数据一位一位的发送出去。
    unsigned int *p = &TDR; *p = 0x78;
  2. TDR寄存器同一时间只能容纳一个数据,只有数据被全部移动到Shift寄存器中才可以写下一个数据。USART_SR 寄存器中的TXE位 ( Bit 7)表示TDR寄存器是否空,1表示数据已经被发送到Shift寄存器。USART_SR 寄存器中的TC位 ( Bit 6)表示Shift移位寄存器中的一字节数据是否被一位一位的发送出去了,该位由硬件进行设置,1表示数据已经发送完成。
    在这里插入图片描述

4.接收数据

  1. 数据从RX引脚一位一位的接收保存到接收Shift寄存器中,接收完成后将数据发送到RDR寄存器中,此时便可以从该寄存器中读取数据。
    unsigned int *p = &RDR; val = *p;
  2. 只有当接收Shift寄存器接收完成并把数据发送到RDR寄存器中才有数据可以读。USART_SR 寄存器中的RXNE位 ( Bit 5)表示RDR寄存器非空,1表示数据已经准备好被读取,此时就可以从该寄存器中读取数据。
  3. TDR寄存器和RDR寄存器的地址是一样的,发送数据时操作的是TDR寄存器,接收数据时操作的是RDR寄存器。
    在这里插入图片描述

5.中断方式

上述的发送和接收数据都是通过查询的方式进行,需要使用while循环来读取状态位,该查询方式会自动阻塞程序,直到数据发送或接收完成才会释放。而使用中断的方式进行数据的发送和接收时,只有当完成操作时才会产生中断,节省CPU时间。
在这里插入图片描述

  1. 相关寄存器
    在这里插入图片描述
    在这里插入图片描述

  2. 补充

    • 在STM32F103C8T6型号的单片机串口硬件中没有环形缓冲区,如果不及时的读取RDR寄存器中的数据很有可能会被后续接收的数据覆盖掉从而丢失数据。如果在接收移位寄存器和RDR寄存器中间加上RxFIFO接收环形缓冲区那么就可以暂存一些数据,避免因为没有及时处理数据造成数据的丢失。
    • 在发送移位寄存器和TDR寄存器中间也加上TxFIFO发送环形缓冲区可以一次性地将数据写入到环形缓冲区中从而提高发送的效率。
      在这里插入图片描述

三、UART串口中断模式配置说明

1.HAL库外设初始化MSP回调机制-USART

HAL_UART_Init();//串口初始化
HAL_UART_MspInit();//调用MSP回调函数
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
	GPIO_InitTypeDef gpio_init_struct;
	if(huart->Instance == USART1) //如果是串口1,进行MSP初始化
	{
		/*1.使能USART1和对应IO时钟  2.初始化IO  3.使能USART1中断,设置优先级*/
	}
}

2.HAL库中断回调机制-USART

//在.s启动文件中
USARTx_IRQHandler();//同步
UARTx_IRQHandler();//异步
//用户调用HAL库中断共用处理函数
HAL_USART_IRQHandler();
HAL_UART_IRQHandler();
//HAL库自己调用中断回调函数
HAL_UART_RxCpltCallback();//接收完成
HAL_UART_TxCpltCallback();//发送完成

3.USART/UART异步通信配置

1.配置串口工作参数

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)
typedef struct
{
	uint32_t BaudRate;//波特率
	uint32_t WordLength;//字长
	uint32_t StopBits;//停止位
	uint32_t Parity;//奇偶校验位
	uint32_t Mode;//UART模式
	uint32_t HwFlowCtl;//硬件流设置---一般不用
	uint32_t OverSampling;//过采样设置---一般不用
}UART_InitTypeDef;

/*示例--串口2初始化*/
void usart2_init(uint32_t bound)
{
    uart2_handler.Instance = USART2;
    uart2_handler.Init.BaudRate = bound;                 /*波特率*/
    uart2_handler.Init.WordLength = UART_WORDLENGTH_8B;  /*字长为8位数据格式*/
    uart2_handler.Init.StopBits = UART_STOPBITS_1;       /*一个停止位*/
    uart2_handler.Init.Parity = UART_PARITY_NONE;        /*无奇偶校验位*/
    uart2_handler.Init.Mode = UART_MODE_TX_RX;           /*收发模式*/
    uart2_handler.Init.HwFlowCtl = UART_HWCONTROL_NONE;  /*无硬件流控*/
    HAL_UART_Init(&uart2_handler);                       /*使能UART2*/
}

2.串口底层初始化

void HAL_UART_MspInit(UART_HandleTypeDef *huart)
/*
作用:使能USART和对应IO时钟  初始化IO  使能USART中断和设置优先级
参数:UART_HandleTypeDef 结构体类型的指针变量
注意:该函数为弱函数,需要自己重新编写
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    GPIO_InitTypeDef gpio_init_struct;
	if(huart->Instance==USART2)              //如果是串口2
	{  
    	__HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIOA时钟
    	__HAL_RCC_USART2_CLK_ENABLE();//使能USART2时钟

    	gpio_init_struct.Pin = GPIO_PIN_2;
    	gpio_init_struct.Mode = GPIO_MODE_AF_PP;//推挽式复用输出
    	gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;//高速
   		HAL_GPIO_Init(GPIOA, &gpio_init_struct);//初始化PA2引脚
		
    	gpio_init_struct.Pin = GPIO_PIN_3;
    	gpio_init_struct.Mode = GPIO_MODE_AF_INPUT;//推挽式复用输入
    	gpio_init_struct.Pull = GPIO_PULLUP;//上拉
    	HAL_GPIO_Init(GPIOA, &gpio_init_struct);//初始化PA3引脚
		
		//在main函数中会调用HAL_Init()来进行HAL库的初始化
		//HAL_Init()中会设置中断优先级分组
		//HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
		HAL_NVIC_EnableIRQ(USART2_IRQn);//使能USART2中断
		HAL_NVIC_SetPriority(USART2_IRQn, 3, 3); //设置中断优先级
	}
}

3.开启串口异步接收中断

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
/*
作用:以中断的方式接收指定字节的数据
参数1:UART_HandleTypeDef 结构体类型的指针变量
参数2:指向接收数据缓冲区
参数3:要接收的数据大小,以字节为单位
*/

/*示例--串口2初始化*/
HAL_UART_Receive_IT(&uart2_handler, (uint8_t *)aRxBuffer2, 1);  

该函数也是属于初始化的一部分,一般放在HAL_UART_Init(&uart2_handler);的下面

4.设置优先级,使能中断
已经在串口底层初始化HAL_UART_MspInit中完成

5.编写中断服务函数

//中断服务函数在启动文件.s中进行查找
        DCD     USART1_IRQHandler          ; USART1
        DCD     USART2_IRQHandler          ; USART2
        DCD     USART3_IRQHandler          ; USART3
        DCD     EXTI15_10_IRQHandler       ; EXTI Line 15..10
        DCD     USBWakeUp_IRQHandler       ; USB Wakeup from suspend
/*示例*/
void USART2_IRQHandler(void)
{
	HAL_UART_IRQHandler(&uart2_handler);//调用HAL库中断处理公用函数
	//该函数会清除相关中断标志位并调用HAL_UART_RxCpltCallback
	//再次开启接收中断
	HAL_UART_Receive_IT(&uart2_handler, (uint8_t *)aRxBuffer2, 1);
}

每接收或发送一个字节的数据USART2_IRQHandler就会调用,当接收到设置的字节数时才会调用HAL_UART_Receive_IT,因此正常应该在HAL_UART_RxCpltCallback回调函数中再次开启接收中断。

6.串口数据发送

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
/*
作用:以阻塞的方式发送指定字节的数据
参数1:UART_HandleTypeDef 结构体类型的指针变量
参数2:指向发送的数据地址
参数3:要发送的数据大小,以字节为单位
参数4:设置超时时间,单位ms
*/

/*示例*/
HAL_UART_Transmit(&uart3_handler,ALY,m,2000);
while(__HAL_UART_GET_FLAG(&uart3_handler, UART_FLAG_TC) != 1);

HAL_UART_Transmit 会在发送完所有数据后返回,发送完成时 UART_FLAG_TC 标志会被自动设置。因此 while (__HAL_UART_GET_FLAG(&uart3_handler, UART_FLAG_TC) != 1);其实没有必要,因为 HAL_UART_Transmit 已经处理了这个逻辑。

7.编写串口中断回调处理函数

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	//在该函数中可以直接处理HAL_UART_Receive_IT中设置的接收缓冲区中的数据
	//也可以操作相关标志位,在main函数中判断标志位来进行处理
}

四、UART编程

串口接线方式
在这里插入图片描述

1.查询方式

  1. 要发送数据时,先将数据写入TDR寄存器,然后再判断TDR寄存器为空再返回。也可以先判断TDR为空再写入,这样效率更高。

  2. 要读取数据时,先判断RDR寄存器非空,再读取RDR得到数据。

  3. HAL_UART_Transmit 和HAL_UART_Receive 函数内部使用寄存器实现了1、2点的逻辑判断

  4. 查询方式只需要配置UART串口和引脚配置即可

    • 使用HAL_UART_Init进行串口初始化
    • 使用HAL_UART_MspInit进行引脚初始化
    • HAL_UART_Init() 在内部会自动调用 HAL_UART_MspInit()
    • 发送:HAL_UART_Transmit
    • 接收:HAL_UART_Receive
  5. HAL_UART_Transmit 函数发送数据流程—以发送“abcdefg”为例

    • 用户调用 HAL_UART_Transmit 并传入发送缓冲区的地址( “abcdefg”)和数据长度( 7 字节)
    • 在开始发送之前,函数会检查 UART 的状态寄存器,确保 UART 发送缓冲区空闲。通过检查 UART_FLAG_TXE标志,确认数据寄存器是否为空。如果为空,表示可以发送数据。
    • 将第一个字节写入 USARTx->DR(UART 数据寄存器)
    • 进入循环,等待每次发送完成。每次发送完一个字节后,HAL_UART_Transmit 会查询 UART_FLAG_TXE 标志,检查发送缓冲区是否空闲。如果为空,说明当前字节已被传送
    • 然后,将下一个字节写入 USARTx->DR,继续执行直到所有字节都被发送完
    • 一旦所有数据都发送完成,HAL_UART_Transmit 会等待 UART_FLAG_TC标志变为设置状态,表示所有字节都已传输完毕
    • 发送完毕后,函数返回,表示数据传输完成
  6. HAL_UART_Receive 函数接收数据流程—以接收5个字节数据为例

    • 用户调用 HAL_UART_Receive,并传入接收缓冲区的地址和接收字节数
    • 在开始接收之前,函数会检查 UART 接收缓冲区是否已经接收到数据。通过查询 UART_FLAG_RXNE标志来判断 UART 数据寄存器中是否有新数据
    • 如果 UART_FLAG_RXNE 为 1,表示接收到一个字节的数据,此时程序从 USARTx->DR(接收数据寄存器)读取该字节并将其存入用户传入的接收缓冲区
    • 如果数据还没有接收到,程序会继续循环查询 UART_FLAG_RXNE,直到接收到一个字节
    • 当接收到一个字节后,程序会继续检查 UART_FLAG_RXNE,并将字节存入缓冲区,直到接收完指定字节数
    • 当接收了所有指定的字节后,HAL_UART_Receive 返回,表示数据接收完成
  7. 超时时间作用

    • 发送数据的过程:HAL_UART_Transmit 会通过轮询方式检查 UART 状态寄存器,确保数据缓冲区(TXD)可写。如果在超时时间内没有成功发送一个字节(即没有检测到 UART_FLAG_TXE 标志或者出现其他错误),函数会超时并返回 HAL_TIMEOUT 错误,停止发送操作
    • 接收数据的过程:HAL_UART_Receive 会通过轮询方式检查 UART_FLAG_RXNE(接收缓冲区非空)标志,来判断是否接收到一个字节。如果在超时时间内没有接收到所需的字节,函数会返回 HAL_TIMEOUT 错误
    • 超时时间是针对发送或者接收的总过程而言,每个字节的发送时间由 波特率 决定。例如,在 9600 bps 的波特率下,发送一个字节大约需要 1 毫秒左右(假设一个字节包含 10 位:1 起始位 + 8 数据位 + 1 停止位,计算方式是 10 bits / 9600 bps ≈ 1.04 ms)

下面的代码演示了HAL_UART_Transmit 和HAL_UART_Receive 的使用方式。HAL_UART_Transmit 的使用很简单,发送的过程正常不会遇到问题,需要注意的是调用HAL_UART_Transmit 时程序会暂停直到所有数据发送完成或者达到超时时间返回错误信息;调用HAL_UART_Receive函数时如果在超时时间内没有接收到指定字节数的数据,函数会返回错误信息,代码中使用了while循环来判断是否接收成功,总接收字节数为5,只有在超时时间范围内接收到了5个数据才会停止循环将接收到的数据打印出来。HAL_UART_Receive 函数会按照提供的缓冲区地址,逐个字节地将接收到的数据填充到该缓冲区中,每次调用 HAL_UART_Receive 时,它都会从缓冲区的起始位置(即地址 0)开始填充数据,所以如果调用HAL_UART_Receive 函数之后如果没有在超时时间内接收到指定字节数据或者发送数据时没有及时调用HAL_UART_Receive 来接收都会丢失数据。如果只接收一个字节的数据,一次发送两个字节,则不会丢失,因为RDR寄存器会暂存一个字节,发送多个字节则会丢失数据。如果想解决接收数据丢失的问题则需要使用中断的方式来接收数据

usart.c

#include "usart.h"

UART_HandleTypeDef huart1;

void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
}

void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {
    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**USART1 GPIO Configuration
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX
    */
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  }
}

main.c

#include "main.h"
#include "usart.h"

void SystemClock_Config(void);
int main(void)
{
  HAL_Init();
  SystemClock_Config();

  MX_USART1_UART_Init();//UART串口初始化
  
  char *str = "welcome";
  char aa[5];

  HAL_UART_Transmit(&huart1, str, strlen(str), 1000);
  
  while (1)
  {
  	//一直循环等待接收到一个字符进行打印
  	while(HAL_UART_Receive(&huart1, aa, 5, 100) != HAL_OK );
	HAL_UART_Transmit(&huart1, aa, 5, 1000);
	HAL_UART_Transmit(&huart1, "\r\n", 2, 1000);
  }
}

2.中断方式

  • 使用中断方式,效率更高,并且可以在接收数据时避免数据丢失。
  • 要发送数据时,使能TXE中断(发送寄存器空中断)。在TXE中断处理函数中,从程序的发送buffer里取出一个数据,写入TDR寄存器,等再次发生中断时,再从程序的发送buffer里取出下一个数据写入TDR。
  • 对于接收数据,在一开始就使能RXNE中断(接收寄存器非空)。这样,UART接收到一个数据就会触发中断,在中断程序里读取RDR得到数据,存入程序的接收buffer,当程序想读取串口数据时,直接读取接收buffer即可。
  • 发送buffer和接收buffer特别适合使用环形缓冲区的方式进行构建。
//发送
HAL_UART_Transmit_IT
HAL_UART_TxCpltCallback
//接收
HAL_UART_Receive_IT
HAL_UART_RxCpltCallback

HAL_UART_Transmit_IT函数实现逻辑

  1. HAL_UART_Transmit_IT中会调用 __HAL_UART_ENABLE_IT(huart, UART_IT_TXE); 使能中断之后即返回,由TXE中断将用户数据填充到TDR寄存器中发送出去。
  2. 当TDR寄存器为空(即USART_SR_TXE 标志位触发)串口会触发中断,此时USART_CR1_TXEIE 也被触发(__HAL_UART_ENABLE_IT(huart, UART_IT_TXE);会使能TXE中断),USART1_IRQHandler函数会响应TXE事件,该函数中会调用HAL_UART_IRQHandler(&huart1),USART1_IRQHandler是中断的源头,下面是HAL_UART_IRQHandler(&huart1)中关于发送的逻辑代码。当TDR寄存器为空时USART_SR_TXE标志位被设置,在HAL_UART_Transmit_IT已经使能了TXE中断,所以USART_CR1_TXEIE标志位也被设置了,所以UART_Transmit_IT(huart) 会被调用。
  /* UART in mode Transmitter ------------------------------------------------*/
  if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
  {
    UART_Transmit_IT(huart);
    return;
  }

  /* UART in mode Transmitter end --------------------------------------------*/
  if (((isrflags & USART_SR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
  {
    UART_EndTransmit_IT(huart);
    return;
  }
  1. 在UART_Transmit_IT(huart)中会将一个字节的数据发送到TDR寄存器中,然后将发送的总字节数减1,判断数据是否发送完成。如果没有发送完成函数返回,这里有一个逻辑,只要TDR寄存器空就会设置USART_SR_TXE标志位,然后只有USART_CR1_TXEIE标志位被设置(即使能UART_IT_TXE中断)才会触发中断并进入USART1_IRQHandler,所以如果数据没有发送完成,会一直循环进入到USART1_IRQHandler中。如果数据发送完成,则会失能UART_IT_TXE中断,并开启UART_IT_TC中断,此时USART_CR1_TCIE标志位被触发,函数返回。
  2. 当 USART 完成数据发送,硬件会自动触发USART_SR_TC标志位,前面已经使能了UART_IT_TC中断,所以USART1_IRQHandler 会响应 TC 事件,还是和前面流程一样,进入HAL_UART_IRQHandler(&huart1)函数中,但是此时会进入到UART_EndTransmit_IT(huart)函数中。
  3. 在UART_EndTransmit_IT(huart)函数中会先失能UART_IT_TC中断(__HAL_UART_DISABLE_IT(huart, UART_IT_TC);),然后调用HAL_UART_TxCpltCallback中断回调函数,在这里编写数据发送完成后的逻辑代码。

HAL_UART_Receive_IT函数实现逻辑

  1. __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);使能UART_IT_RXNE中断。
  2. RDR寄存器有数值触发接收中断,此时USART_SR_RXNE 被设置,RXNE 中断 (USART_CR1_RXNEIE)也被使能,此时触发 USART1_IRQHandler函数,并进入HAL_UART_IRQHandler(&huart1);函数中,因为USART_SR_RXNE和USART_CR1_RXNEIE都已经被设置,所以进入UART_Receive_IT(huart);函数中。
  3. UART_Receive_IT(huart)中会将数据从DR寄存器中读取到buffer中并将地址加1,如果数据接收没有完成,则函数返回,和发送中断的逻辑一样,还会再次触发USART1_IRQHandler进行数据接收。如果数据接收完成,失能UART_IT_RXNE中断,然后调用HAL_UART_RxCpltCallback中断回调函数,可以在该函数中处理数据接收完成后的相关逻辑。

总结:

  • 当 接收数据寄存器 (RDR) 中有新数据可以读取时,USART1 会触发接收中断,此时USART_SR_RXNE 被设置,只有启用了 RXNE 中断 (USART_CR1_RXNEIE)才会触发 USART1_IRQHandler。
  • 当 发送数据寄存器 (TDR) 为空,表示可以写入数据进行发送时,USART1 会触发发送中断,此时USART_SR_TXE被设置,只有启用了 TXE 中断 (USART_CR1_TXEIE)才会触发 USART1_IRQHandler。
  • 当 USART 完成数据发送,并且所有数据都已通过串口发送出去时,USART1 会触发传输完成中断,此时USART_SR_TC 被设置,只有启用了 传输完成中断 (USART_CR1_TCIE)才会触发 USART1_IRQHandler。
  • 只有调用了HAL_UART_Receive_IT才会进行数据的接收,如果有数据到来没有及时的开启接收中断则会导致数据的丢失。

代码说明:

  • 在while循环前进行串口的相关初始化操作,特别要注意的是使用HAL_UART_Receive_IT先开启接收中断(StartUART1Recv函数),这样程序开始运行之后就可以接收到数据了,不及时开启会导致数据丢失。
  • 本代码中接收的总字节长度为1,所以接收到一个字节就会产生中断并调用HAL_UART_RxCpltCallback接收回调函数,在函数中将字符写入环形缓冲区,然后重新使能接收中断,只有这样才能及时接收到数据。
  • 在while循环中读取环形缓冲区中的数据,while( 0 != UART1GetChar(&c)); 的意思是直到成功读取到一个字符才停止循环,然后会将这个字符打印出来。
  • 需要注意的是HAL_UART_RxCpltCallback 中不应该执行耗时操作。这是因为该回调函数会在 UART 接收中断完成时被触发,而在回调函数内执行耗时操作可能会导致接收中断的处理不及时,从而引起数据丢失。可以将接收到的数据存放在环形缓冲区(FIFO)中,并在主程序中读取和处理数据,而不是在回调中直接处理。
  • 如果想接收不定长的数据,最好的办法就是每次只接收一个字符,然后通过结束字符来或者长度前缀数据判断数据接收完毕。如果知道每次接收的数据大小确定,那么可以在开启接收中断函数中设置对应的字节长度,这样处理会更加方便。

main.c

#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "circle_buffer.h"

void SystemClock_Config(void);
int main(void)
{
   HAL_Init();
   SystemClock_Config();
   MX_GPIO_Init();
   MX_USART1_UART_Init();
   char *str = "welcome\r\n";
   char c;
   StartUART1Recv();
  while (1)
  {
		HAL_UART_Transmit_IT(&huart1, str, strlen(str));
		Wait_Tx_Complete();
		//该函数就是使用标志位来等待直到发送完成在回调函数中将标志位复位为止。
		//在前一个数据没有发送完成之前再次开启发送中断是不对的
		//正常来说发送的数据量不是很大时使用HAL_UART_Transmit是比较不错的
		while( 0 != UART1GetChar(&c)); 
		HAL_UART_Transmit(&huart1, &c, 1, 1000);
		HAL_UART_Transmit(&huart1, "\r\n", 2, 1000);
  }
}
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

usart.c

#include "usart.h"
#include "circle_buffer.h"
static uint8_t RecvChar;
static uint8_t RecvBuf[100];
static circle_buf uart1_rx_bufs;

UART_HandleTypeDef huart1;

void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {
    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  }
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart)
{
	if(huart->Instance == USART1)
	{
		circle_buf_write(&uart1_rx_bufs, RecvChar);
		HAL_UART_Receive_IT(&huart1, &RecvChar, 1);
	}
}
void StartUART1Recv(void)
{
	circle_buf_init(&uart1_rx_bufs, 100, RecvBuf);
	HAL_UART_Receive_IT(&huart1, &RecvChar, 1);
}
int UART1GetChar(uint8_t *pVal)
{
	return circle_buf_read(&uart1_rx_bufs, pVal);
}

usart.h

#ifndef __USART_H__
#define __USART_H__

#ifdef __cplusplus
extern "C" {
#endif
#include "main.h"
extern UART_HandleTypeDef huart1;

void MX_USART1_UART_Init(void);
void StartUART1Recv(void);
int UART1GetChar(uint8_t *pVal);

#ifdef __cplusplus
}
#endif

#endif /* __USART_H__ */

circle_buffer.c

#include "circle_buffer.h"

void circle_buf_init(p_circle_buf pCircleBuf, uint32_t len, uint8_t *buf)
{
	pCircleBuf->buf = buf;
	pCircleBuf->r = 0;
	pCircleBuf->w = 0;
	pCircleBuf->len = len;
}

int circle_buf_read(p_circle_buf pCircleBuf, uint8_t *pval)
{
	if(pCircleBuf->r == pCircleBuf->w)
	{
		return -1;
	}
	else
	{
		*pval = pCircleBuf->buf[pCircleBuf->r];
		pCircleBuf->r++;
		if(pCircleBuf->r == pCircleBuf->len)
			pCircleBuf->r = 0;
		return 0;
	}

}

int circle_buf_write(p_circle_buf pCircleBuf, uint8_t val)
{
	uint32_t next_w = pCircleBuf->w + 1;
	if(next_w == pCircleBuf->len)
		next_w = 0;
	
	if(next_w != pCircleBuf->r)
	{
		pCircleBuf->buf[pCircleBuf->w] = val;
		pCircleBuf->w = next_w;
		return 0;
	}
	else
	{
		return -1;
	}
}

circle_buffer.h

#ifndef _CIRCLE_BUF_H
#define _CIRCLE_BUF_H

#include <stdint.h>

typedef struct circle_buf{
	uint32_t r;
	uint32_t w;
	uint32_t len;
	uint8_t *buf;
}circle_buf, *p_circle_buf;

void circle_buf_init(p_circle_buf pCircleBuf, uint32_t len, uint8_t *buf);

int circle_buf_read(p_circle_buf pCircleBuf, uint8_t *pval);

int circle_buf_write(p_circle_buf pCircleBuf, uint8_t val);

#endif

stm32f1xx_it.c

#include "main.h"
#include "stm32f1xx_it.h"
extern UART_HandleTypeDef huart1;

void SysTick_Handler(void)
{
  HAL_IncTick();
}

void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
}

3.DMA方式

简介:
STM32的DMA(Direct Memory Access,直接存储器访问)是一种用于高速数据传输的技术,可以在不占用CPU的情况下将数据直接从一个位置传输到另一个位置,从而提升系统的效率,减轻CPU的负担,广泛用于数据传输量大的应用,如音视频处理、传感器数据采集等。
工作原理:
在传统的I/O操作中,CPU会参与到每一次数据的读取和写入过程中,假设需要发送一个一千字节的数据,无论是查询方式还是中断方式,当TDR寄存器空时都是CPU去内存中取出下一个数据再写到TDR寄存器中,这些都离不开CPU的处理。而DMA传输模式的优势在于它通过DMA控制器来控制数据传输的过程,CPU只需要初始化DMA传输并在完成后进行必要的处理,从而节省了大量的计算资源。
具体来说,DMA的传输过程包括以下几个步骤:

  1. 配置DMA传输:用户需要配置DMA控制器,设定数据源地址、目标地址、传输的数据大小、传输方向(内存到外设或外设到内存)、传输模式(单次传输或循环传输)等。
  2. 触发DMA传输:DMA传输可以通过外部触发(例如外设产生的中断)或由程序主动启动。
  3. 数据传输:一旦DMA传输开始,数据将直接从源地址传输到目标地址,DMA控制器负责完成所有的数据移动工作,CPU无需干预。
  4. 传输完成:一旦DMA传输完成,DMA控制器会产生一个中断或标志位,通知CPU数据传输已完成,CPU可以进行后续处理。

DMA的类型
STM32的DMA支持几种不同的传输模式,主要包括:

  1. 内存到内存(Memory-to-Memory):将数据从一个内存区域传输到另一个内存区域。适用于在内存中进行数据搬移的场景。
  2. 外设到内存(Peripheral-to-Memory):外设(如ADC、SPI、UART等)采集的数据直接传输到内存中,而无需通过CPU处理。这种模式相当于读取。
  3. 内存到外设(Memory-to-Peripheral):数据从内存传输到外设,这种模式常用于将数据从内存发送到外设(如通过UART发送数据)。
  4. 外设到外设(Peripheral-to-Peripheral):数据从一个外设直接传输到另一个外设,如通过DMA实现从一个SPI外设读取数据并直接发送到另一个SPI外设。

内存到外设(发送)具体配置

  1. 源:char tx_buf[1000];源累加(1B/2B)。
  2. 目的:TDR寄存器;不累加。
  3. 长度:1000
  4. 当TDR里面的数据移动到移位寄存器后会发送一个DMA请求,然后DMA会取出下一个数据发给TDR,整个过程不会干扰CPU。

外设到内存(接收)具体配置

  1. 源:RDR寄存器;不累加。
  2. 目的;char rx_buf[1000];目的累加(1B/2B)。
  3. 长度:1000
  4. 当外设的RDR寄存器有新的数据时,它会触发DMA请求。DMA控制器会自动从RDR中读取数据并将其存入 rx_buf。这个过程同样不会干扰CPU的正常执行。

相关函数

//发送
HAL_UART_Transmit_DMA
HAL_UART_TxHalfCpltCallback
HAL_UART_TxCpltCallback
//接收
HAL_UART_Receive_DMA
HAL_UART_RxHalfCpltCallback
HAL_UART_RxCpltCallback

HAL_UART_Transmit_DMA函数实现逻辑

  1. 调用HAL_UART_Transmit_DMA开启DMA传输,在该函数中会配置好DMA传输完成一半和传输完成的回调函数:huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt; huart->hdmatx->XferHalfCpltCallback = UART_DMATxHalfCplt;
  2. 当DMA 完成数据传输后,触发 DMA1_Channel4_IRQHandler,在这个中断服务程序中又会调用HAL_UART_IRQHandler(&hdma_usart1_tx);该函数中会检查 DMA 传输完成标志并清除它,并在传输完成一半时调用hdma->XferHalfCpltCallback(hdma);和在传输完成时调用hdma->XferCpltCallback(hdma);因为在HAL_UART_Transmit_DMA中已经配置了接收完成一半和接收完成的中断回调函数,所以调用这两个函数就相当于调用了HAL_UART_TxHalfCpltCallback和HAL_UART_TxCpltCallback。

HAL_UART_Receive_DMA函数实现逻辑

  1. 调用 HAL_UART_Receive_DMA() 时,它会调用 UART_Start_Receive_DMA() 来启动 DMA 接收。这个函数会配置 DMA 接收的相关参数,并为接收过程配置回调函数(如接收完成一半和完成时的回调)。 配置 huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt; 和 huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt; 使得在 DMA 接收过程中的不同阶段(完成一半和完成)能够触发相应的回调函数。
  2. 当 DMA 接收完成数据后,会触发 DMA1_Channel5_IRQHandler,并调用 HAL_UART_IRQHandler(&hdma_usart1_rx)。这个函数会检查 DMA 接收的标志位,清除相关标志,并根据接收状态调用相应的回调函数。在接收完成一半时调用hdma->XferHalfCpltCallback(hdma);在接收完成时调用hdma->XferCpltCallback(hdma); 因为在UART_Start_Receive_DMA中已经配置了接收完成一半和接收完成的中断回调函数,所以调用这两个函数就相当于调用了HAL_UART_RxHalfCpltCallback和HAL_UART_RxCpltCallback。

总结
HAL_UART_Transmit_DMA和HAL_UART_Transmit_IT的用法是一模一样的,唯一不同就是HAL_UART_Transmit_DMA是使用DMA从内存搬运数据到外设,而HAL_UART_Transmit_IT是使用CPU从内存搬运数据到外设。另外就是HAL_UART_Receive_DMA不是很实用,它的用法和HAL_UART_Receive_IT的用法是类似的。如果接收不定长数据,我们不能够事先知道数据到底是多少个字节,所以就无从谈起设置接收多少个字节,使用HAL_UART_Receive_IT可以设置每次接收一个字节数据,通过结束字节或者数据长度前缀字节来判断数据接收是否完成,而使用HAL_UART_Receive_DMA接收一个字节显然是不实用的,只有结合IDLE中断才能发挥出它的作用。

代码说明:
DMA方式的代码和中断方式类似,就是多了DMA的配置和中断配置。
dma.c

#include "dma.h"

void MX_DMA_Init(void)
{
  __HAL_RCC_DMA1_CLK_ENABLE();
  HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);

}

stm32f1xx_it.c

#include "main.h"
#include "stm32f1xx_it.h"

extern DMA_HandleTypeDef hdma_usart1_tx;
extern UART_HandleTypeDef huart1;

void DMA1_Channel4_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_usart1_tx);
}

void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
}

usart.c

#include "usart.h"

#include "circle_buffer.h"
static uint8_t RecvChar;
static uint8_t RecvBuf[100];
static circle_buf uart1_rx_bufs;

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;

void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
}

void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {
    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* USART1 DMA Init   USART1_TX Init*/
    /* 这里只配置了DMA的发送通道,完整的发送和接收配置会在DMA+IDLE中介绍*/
    hdma_usart1_tx.Instance = DMA1_Channel4;
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart1_tx.Init.Mode = DMA_NORMAL;
    hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
    if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
    {
      Error_Handler();
    }
    __HAL_LINKDMA(uartHandle,hdmatx,hdma_usart1_tx);

    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  }
}

4.DMA+IDLE方式

  • IDLE 中断 是 UART 通信中一个非常有用的中断类型,特别是在接收不定长数据时。它用于检测 UART 总线上的空闲状态,通常用于指示数据接收已经完成或接收的字节序列已经结束。在至少接收到一个数据之后,发现在一个字节的时间里都没有接收到新数据才会产生IDLE中断。
  • 使用IDLE时DMA传输结束条件有三个:
    • 接收到了指定数量的数据
    • 总线空闲
    • 发生错误

相关函数

查询方式
HAL_UARTEx_ReceiveToIdle
回调函数:根据返回参数RxLen判断是否接收完毕,还是因为空闲返回

中断方式:
HAL_UARTEx_ReceiveToIdle_IT
回调函数:接收完毕时调用  HAL_UART_RxCpltCallback
		 空闲中断时调用  HAL_UARTEx_RxEventCallback

DMA方式:
HAL_UARTEx_ReceiveToIdle_DMA
回调函数:接收一半时调用  HAL_UART_RxHalfCpltCallback
		 接收完毕时调用  HAL_UART_RxCpltCallback
		 空闲中断时调用  HAL_UARTEx_RxEventCallback

查询、中断、DMA方式比较

  1. 查询方式:HAL_UARTEx_ReceiveToIdle和HAL_UART_Receive
    • HAL_UART_Receive是同步接收,它会阻塞直到接收到指定字节的数据或超时。
    • HAL_UARTEx_ReceiveToIdle也是同步接收,调用时会等待数据接收完成或空闲中断触发。虽然有空闲中断机制,但是因为函数是同步的,会阻塞程序,所以也有超时机制,保证系统的健壮性。
  2. 中断方式:HAL_UARTEx_ReceiveToIdle_IT和HAL_UART_Receive_IT
    • HAL_UART_Receive_IT基于中断接收数据,每接收到一个字节就触发一次中断进入USART1_IRQHandler,当接收到指定字节数据调用回调函数。
    • HAL_UARTEx_ReceiveToIdle_IT通过空闲中断(IDLE)来判断接收是否完成。如果在接收到第一个字节后没有新的数据到达,且 UART 线路空闲超过设定的空闲阈值,空闲中断就会触发并结束接收过程。如果在接收到第一个数据之后陆续接收到指定字节的数据,函数也会结束。
  3. DMA方式:HAL_UARTEx_ReceiveToIdle_DMA和HAL_UART_Receive_DMA
    • HAL_UART_Receive_DMA开启DMA接收,直到接收完指定字节数据会产生DMA中断并调用回调函数。
    • HAL_UARTEx_ReceiveToIdle_DMA通过 DMA 接收数据,但同时结合了空闲检测。当没有更多数据接收并且 UART 线路处于空闲状态时,空闲中断会被触发,通知数据接收结束。如果没有发生空闲中断接收到指定字节数据也会结束接收并调用回调函数。
  4. 总结:
    • HAL_UART_Receive通常用于有限时间内接收,会阻塞程序。
    • HAL_UARTEx_ReceiveToIdle 更适合用于不确定长度的数据接收,通过空闲中断来判断接收是否结束,也会阻塞程序。
    • HAL_UART_Receive_IT 每接收到一个字节就会触发中断,适合需要逐字节处理的场景,接收固定长度数据的场景也是适合的。
    • HAL_UARTEx_ReceiveToIdle_IT 函数适用于不定长的数据接收,它通过空闲中断(IDLE interrupt)来判断数据接收的结束,从而避免了在每个字节接收时都触发接收中断(RXNE interrupt)的性能负担。当数据流之间没有空闲时,接收中断仍然会在每个字节接收时触发,但空闲中断可以有效地标识接收的结束,从而减少不必要的中断处理,尤其是在数据流较长时。
    • HAL_UART_Receive_DMA 适用于接收固定长度数据。
    • HAL_UARTEx_ReceiveToIdle_DMA 适用于 不定长度数据接收,可以依赖空闲中断来判断接收是否结束,无需事先知道数据长度。
    • 查询方式就会阻塞程序。无IDLE:接收到指定字节数据或超时;有IDLE:接收到指定数据或空闲中断或超时;会结束接收。
    • 中断方式不会阻塞程序但是需要CPU进行处理。无IDLE:接收到指定数据;有IDLE:接收到指定数据或空闲中断;会结束接收。
    • DMA方式不需要CPU进行数据搬运处理。无IDLE:接收到指定数据;有IDLE:接收到指定数据或空闲中断;会结束接收。
    • 需要根据不同的应用场景选择不同的方式进行串口的收发

函数使用及原理

  1. HAL_UARTEx_ReceiveToIdle
    • HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t *RxLen,uint32_t Timeout)
    • huart:指向 UART_HandleTypeDef 的指针,包含串口的配置信息。
    • pData:指向接收数据的缓冲区。
    • Size:要接收的字节数。
    • RxLen:指向 uint16_t 类型变量的指针,该变量会保存实际接收到的字节数。
    • Timeout:超时时间,单位是毫秒。如果在超时前接收完成,则返回正常,否则返回错误。
    • 工作原理:此函数会使用轮询方式(while循环)接收数据,直到接收到指定字节数的有效数据,或者串口进入空闲状态并触发 IDLE 中断。如果在超时之前接收完成,则返回成功;如果超时,则返回错误状态。
    while (huart->RxXferCount > 0U)
    {
      //如果发送IDLE中断
      if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE))
      {
        __HAL_UART_CLEAR_IDLEFLAG(huart);
        if (*RxLen > 0U)
        {
          huart->RxEventType = HAL_UART_RXEVENT_IDLE;
          huart->RxState = HAL_UART_STATE_READY;
          return HAL_OK;
        }
      }
      //如果接收到数据
      //UART_FLAG_RXNE 表示接收数据寄存器非空,即有新的字节数据可以读取。
      if (__HAL_UART_GET_FLAG(huart, UART_FLAG_RXNE))
      {
        if (pdata8bits == NULL)
        {
          *pdata16bits = (uint16_t)(huart->Instance->DR & (uint16_t)0x01FF);
          pdata16bits++;
        }
        else
        {
          if ((huart->Init.WordLength == UART_WORDLENGTH_9B) || ((huart->Init.WordLength == UART_WORDLENGTH_8B) && (huart->Init.Parity == UART_PARITY_NONE)))
          {
            *pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
          }
          else
          {
            *pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x007F);
          }
          pdata8bits++;
        }
        *RxLen += 1U;
        huart->RxXferCount--;
      }
	  //如果超时
	  //阻塞方式(查询方式)就必须设置超时机制,它确保了接收操作不会因为意外情况而阻塞太久。
      if (Timeout != HAL_MAX_DELAY)
      {
        if (((HAL_GetTick() - tickstart) > Timeout) || (Timeout == 0U))
        {
          huart->RxState = HAL_UART_STATE_READY;
          return HAL_TIMEOUT;
        }
      }
    }
  1. HAL_UARTEx_ReceiveToIdle_IT
    • HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
    • huart:指向 UART_HandleTypeDef 结构体的指针,包含串口的配置信息。
    • pData:指向接收数据的缓冲区。
    • Size:要接收的字节数。
    • 工作原理:函数使能接收中断和IDLE中断,IDLE中断也属于串口中断,所以接收到一个字节数据或者发生IDLE中断都会进入到USART1_IRQHandler函数中,当接收到指定字节数据会调用HAL_UART_RxCpltCallback回调函数,发生IDLE中断会调用HAL_UARTEx_RxEventCallback回调函数。
  2. HAL_UARTEx_ReceiveToIdle_DMA
    • HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
    • huart:指向 UART_HandleTypeDef 结构体的指针,包含串口的配置信息。
    • pData:指向接收数据的缓冲区。
    • Size:要接收的字节数。
    • 工作原理:函数包含HAL_UART_Receive_DMA函数对DMA的相关配置还包括开启IDLE中断,与HAL_UARTEx_ReceiveToIdle_IT不同的是当 DMA 完成数据接收后,触发 DMA 完成中断(DMA1_Channel5_IRQHandler)。此时,DMA 会自动将接收到的数据从串口的数据寄存器 DR 转移到指定的内存缓冲区,并且会调用 HAL_UART_RxCpltCallback 回调函数,表示数据接收已完成。
      当在接收过程中发生了空闲中断,USART1_IRQHandler 会被调用,并且会调用 HAL_UARTEx_RxEventCallback 回调函数,表示接收事件已经完成。

代码说明

  • 代码使用的是DMA+IDLE方式即HAL_UARTEx_ReceiveToIdle_DMA进行数据的接收。
  • 使用DMA,发送完成会触发DMA1_Channel4_IRQHandler中断;接收完成会触发DMA1_Channel5_IRQHandler 中断。
  • 在StartUART1Recv();函数中使能了DMA 接收完成中断和ILDE中断,分别会调用回调函数HAL_UART_RxCpltCallback和HAL_UARTEx_RxEventCallback,为了及时接收数据都需要再次调用HAL_UARTEx_ReceiveToIdle_DMA使能DMA 接收完成中断和ILDE中断。HAL_UARTEx_ReceiveToIdle_DMA函数有两个出口就需要有两个入口才能完成闭环。
  • 与之前不同的是HAL_UARTEx_ReceiveToIdle_DMA接收N个字节数据,要在回调函数中把接收到的所有数据都存入环形缓冲区中。

main.c

#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
#include "stdio.h"
#include "circle_buffer.h"

void SystemClock_Config(void);
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  char *str = "welcome\r\n";
  char c;
  StartUART1Recv();
  while (1)
  {
	HAL_UART_Transmit_DMA(&huart1, (const uint8_t *)str, strlen(str));
	Wait_Tx_Complete();
	while( 0 != UART1GetChar((uint8_t *)&c)); 
	HAL_UART_Transmit(&huart1, (const uint8_t *)&c, 1, 1000);
	HAL_UART_Transmit(&huart1, (const uint8_t *)"\r\n", 2, 1000);
  }
  
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

usart.c

#include "usart.h"
#include "stdio.h"
#include "circle_buffer.h"
static uint8_t RecvTmpBuf[10];
static uint8_t RecvBuf[100];
static circle_buf uart1_rx_bufs;
static volatile int g_tx_cplt = 0;

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;

void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
}

void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  // 检查是否是 USART1
  if(uartHandle->Instance == USART1)
  {
    // 使能 USART1 的时钟
    __HAL_RCC_USART1_CLK_ENABLE();
    // 使能 GPIOA 的时钟,因为我们用到的 USART1 的引脚是 GPIOA 的引脚
    __HAL_RCC_GPIOA_CLK_ENABLE();

    // 配置 USART1 的 TX 引脚(GPIOA_PIN9)为复用推挽输出
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;         // 复用推挽输出模式
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;   // 高速
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);         // 初始化 GPIOA 的引脚

    // 配置 USART1 的 RX 引脚(GPIOA_PIN10)为输入模式
    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;         // 输入模式
    GPIO_InitStruct.Pull = GPIO_NOPULL;             // 不使用上下拉电阻
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);         // 初始化 GPIOA 的引脚

    /* USART1 DMA 初始化 */
    // 配置 USART1 的 TX(发送)通道的 DMA 设置
    hdma_usart1_tx.Instance = DMA1_Channel4;                             // 选择 DMA1 通道4 用于 USART1 TX
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;                // 数据从内存传输到外设
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;                    // 外设地址不增加
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;                        // 内存地址递增
    hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;       // 外设数据字节对齐
    hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;          // 内存数据字节对齐
    hdma_usart1_tx.Init.Mode = DMA_NORMAL;                                // DMA 工作模式为普通模式
    hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;                      // DMA 优先级低
    if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)                          // 初始化 DMA 通道
    {
      Error_Handler();                                                   // 如果初始化失败,调用错误处理函数
    }
    __HAL_LINKDMA(uartHandle, hdmatx, hdma_usart1_tx);                    // 将 DMA 与 UART 句柄关联

    // 配置 USART1 的 RX(接收)通道的 DMA 设置
    hdma_usart1_rx.Instance = DMA1_Channel5;                             // 选择 DMA1 通道5 用于 USART1 RX
    hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;                // 数据从外设传输到内存
    hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;                    // 外设地址不增加
    hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;                        // 内存地址递增
    hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;       // 外设数据字节对齐
    hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;          // 内存数据字节对齐
    hdma_usart1_rx.Init.Mode = DMA_NORMAL;                                // DMA 工作模式为普通模式
    hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;                      // DMA 优先级低
    if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)                          // 初始化 DMA 通道
    {
      Error_Handler();                                                   // 如果初始化失败,调用错误处理函数
    }
    __HAL_LINKDMA(uartHandle, hdmarx, hdma_usart1_rx);                    // 将 DMA 与 UART 句柄关联
    // 配置 USART1 的中断优先级并使能 USART1 中断
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);                             // 设置 USART1 中断优先级为最高(0, 0)
    HAL_NVIC_EnableIRQ(USART1_IRQn);                                      // 使能 USART1 中断
  }
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef* huart)
{
	int i;
	if(huart->Instance == USART1)
	{
		for(i = 0; i < 10; i++)
		{
			circle_buf_write(&uart1_rx_bufs, RecvTmpBuf[i]);
		}
		HAL_UARTEx_ReceiveToIdle_DMA(&huart1, RecvTmpBuf, 10);
	}
	
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef* huart)
{
	if(huart->Instance == USART1)
	{
		g_tx_cplt = 1;
	}
}
void Wait_Tx_Complete(void)
{
	while(g_tx_cplt == 0);
	g_tx_cplt = 0;
}
void StartUART1Recv(void)
{
	circle_buf_init(&uart1_rx_bufs, 100, RecvBuf);
	HAL_UARTEx_ReceiveToIdle_DMA(&huart1, RecvTmpBuf, 10);
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef* huart, uint16_t Size)
{
	int i;
	if(huart->Instance == USART1)
	{
		for(i = 0; i < Size; i++)
		{
			circle_buf_write(&uart1_rx_bufs, RecvTmpBuf[i]);
		}
		HAL_UARTEx_ReceiveToIdle_DMA(&huart1, RecvTmpBuf, 10);
	}
	
}
int UART1GetChar(uint8_t *pVal)
{
	return circle_buf_read(&uart1_rx_bufs, pVal);
}

usart.h

#ifndef __USART_H__
#define __USART_H__

#include "main.h"

extern UART_HandleTypeDef huart1;

void MX_USART1_UART_Init(void);
void StartUART1Recv(void);
int UART1GetChar(uint8_t *pVal);
void Wait_Tx_Complete(void);

#endif /* __USART_H__ */

dma.c

#include "dma.h"

void MX_DMA_Init(void)
{
  /* DMA controller clock enable */
  __HAL_RCC_DMA1_CLK_ENABLE();
  /* DMA interrupt init */
  /* DMA1_Channel4_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
  /* DMA1_Channel5_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
}

stm32f1xx_it.c

#include "main.h"
#include "stm32f1xx_it.h"

extern DMA_HandleTypeDef hdma_usart1_tx;
extern DMA_HandleTypeDef hdma_usart1_rx;
extern UART_HandleTypeDef huart1;

void DMA1_Channel4_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_usart1_tx);
}

void DMA1_Channel5_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_usart1_rx);
}

void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
}

5.printf函数实现(需包含 #include “stdio.h”)

实现printf函数需要注意两点:1.避免使用半主机模式 2.实现fputc函数。其中半主机模式通俗来讲就是通过仿真器实现开发板在电脑上面的输入和输出。我们是使用串口实现开发板在电脑上面的输入和输出,所以需要避免使用半主机模式。fputc函数是实现单个字符的输出。下面具体说明。

避免使用半主机模式

  1. 微库法
    在魔术棒→Target选项卡,勾选:Use Micro LIB,即可避免使用半主机模式。
    在这里插入图片描述

  2. 代码法:1个预处理、2个定义、3个函数

    • #pragma import(__use_no_semihosting),确保不从C库中使用半主机函数
    • 定义:__FILE结构体,避免HAL库某些情况下报错
    • 定义:FILE__stdout,避免编译时报错
    • 实现:_ttywrch、_sys_exit和_sys_command_string等三个函数

    需要注意的是AC5和AC6不使用半主机模式稍有差异,需要分开处理。然后这些操作不需要理解,直接放到工程中即可。

/* 加入以下代码, 支持printf函数, 而不需要选择use MicroLIB */
#if (__ARMCC_VERSION >= 6010050)            /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t");  /* 声明不使用半主机模式 */
__asm(".global __ARM_use_no_argv \n\t");    /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */

#else
/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)

struct __FILE
{
    int handle;
    /* Whatever you require here. If the only file you are using is */
    /* standard output using printf() for debugging, no file handling */
    /* is required. */
};

#endif

/* 不使用半主机模式,至少需要重定义_ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
int _ttywrch(int ch)
{
    ch = ch;
    return ch;
}

/* 定义_sys_exit()以避免使用半主机模式 */
void _sys_exit(int x)
{
    x = x;
}

char *_sys_command_string(char *cmd, int len)
{
    return NULL;
}


/* FILE 在 stdio.h里面定义. */
FILE __stdout;
  1. 比较
    • 微库法更简单,但是某些标准C库函数运行慢、兼容性差
    • 代码法标准C库运行快、兼容性好,但是实现稍微复杂

fputc函数是实现

/* MDK下需要重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
//寄存器操作
#define USART_UX              USART1
int fputc(int ch, FILE *f)
{
    while ((USART_UX->SR & 0X40) == 0);     /* 等待上一个字符发送完成 */
    USART_UX->DR = (uint8_t)ch;             /* 将要发送的字符 ch 写入到DR寄存器 */
    return ch;
}
/*
1. 函数定义和参数
int fputc(int ch, FILE *f):这是一个重定向 fputc 函数,ch 是待发送的字符,f 是 FILE 类型的指针,这个参数通常用于标准输出(如 stdout),但在函数中并没有使用它。
2. 等待USART发送完成
while ((USART_UX->SR & 0X40) == 0);
USART_UX->SR 是 USART 的状态寄存器 (SR),用于表示 USART 模块的状态。
使用 0x40 掩码,检查 SR 寄存器的第 6 位(TXE 位)。当 SR & 0x40 等于 0 时,说明 TXE 位为 0,即数据寄存器未空,发送没有完成;当 TXE 位为 1 时,表示数据寄存器为空,可以发送新的字符。
需要在发送新字符前等待上一个字符发送完成,不然会发送错乱导致乱码。
3. 发送字符
USART_UX->DR 是 USART 的数据寄存器 (DR),该寄存器用于发送和接收数据。
通过将待发送的字符 ch 写入 DR 寄存器,USART 会将该字符发送到串口。
4. 返回字符
return ch;
发送完字符后,返回发送的字符 ch,这个返回值一般会被忽略,但通常用来符合 fputc 函数的标准返回类型。
*/

//HAL库函数操作
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (const uint8_t *)&ch, 1, 10);
    return ch;
}
/*
因为发送一个字符很短,差不多10位数据,需要10/115200秒,所以使用查询方式即可。
需要注意的是ch是int类型的,取地址是int *类型,需要强制转化位HAL_UART_Transmit函数定义中的const uint8_t *类型指针。
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天只搬半天砖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值