HAL库基础教程——串口
一.初步了解串口
1.串口传输信息原理

在上图中我们发现了3个引脚:TX、RX、GND。TX是发送引脚,RX是接收引脚,GND是地线。在上图我们发现设备1的发送引脚和设备2的接收引脚相连接,设备1的接收引脚和设备2的发送引脚相连接。举例,若设备1向设备2发送一个0x80的数据,那么设备1则会通过tx引脚向设备2每隔一个时间发送一位数据,若1则高电平,0则低电平。设备2只需每隔相同时间读取rx引脚的高低电平,便能读取到对应的数据。
在上面的的说明中,我们很明显能发现2个问题:
- 如何保证设备1和设备2对高低电平的定义一样。我们知道数字电路中对高低电平的定义是相对的,可能设备1对3v的定义为低电平,设备2对3v的定义为高电平。
- 如何保证设备1发送时间的间隔和设备2接收时间的间隔一样。
对于第一个问题,我们给两个设备的GND相连,这样对低电平的定义就相同了。
对于第二个问题,我们就要引出比特率的概念,比特率就是1s能发送的比特数。所以说我们两个设备的串口要能正常通信需要设置相同的比特率。
2.轮询模式输出和接收数据
首先我们先来看一个简单的输入什么输出什么的轮询数据代码
uint8_t message[2] = {0};
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_UART_Receive(&huart2,message,2,HAL_MAX_DELAY);
HAL_UART_Transmit(&huart2,message,strlen((char*)message),100);
}
在上面我们设计到了2个函数,下面是该两个函数的定义
/**
* @brief Receives an amount of data in blocking mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the received data is handled as a set of u16. In this case, Size must indicate the number
* of u16 available through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be received.
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
/**
* @brief Sends an amount of data in blocking mode.
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the sent data is handled as a set of u16. In this case, Size must indicate the number
* of u16 provided through pData.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @param pData Pointer to data buffer (u8 or u16 data elements).
* @param Size Amount of data elements (u8 or u16) to be sent
* @param Timeout Timeout duration
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);
上面2个函数的返回量我们目前不需要关注,我们关注一下他们的参数:
UART_HandleTypeDef *huart //串口句柄,我们可以理解成用哪个串口,这个变量就是该串口的地址
const uint8_t *pData //发送或接收的数据数组指针
uint16_t Size //发送或接收的数据多少
uint32_t Timeout //超时时间
二.进阶版中断处理串口
在前面的轮询的方法来处理串口存在一个很严重的问题:cpu需要一直检测RX是否有数据,无法进行其他操作。这个在我们复杂的项目工程中是基本不被允许的。所以我们一般使用中断来处理串口数据的方式来解决上述问题。
1.中断处理串口数据的原理
1.1cpu轮询处理串口数据的原理
首先我们先回到轮询处理串口数据的原理。

如上图,串口发送的流程,具体cpu会一直检查发送数据寄存器中是否有数据,知道设备2读取数据后,发送数据寄存器才会被清空,cpu才会继续发送数据。

接收则是想反,CPU会一直检测接收数据寄存器中是否有数据,如果有数据才会读取。
那么在轮询模式下cpu会被串口一直占用,无法进行其他操作,在工程当中这就给cpu增加了许多负担。于是我们引出了中断来处理串口数据。
1.2中断处理串口数据的原理
在这里我们可以把中断看成一个闹钟。一般情况下设备2还未接收完数据时,即发送数据寄存器还有数据时,cpu可以专心的运行其他代码。

然后如果一旦设备2接收完数据时,闹钟就响了(中断响应了),那么这时候CPU暂停其他工作来讲输出填入到发送数据寄存器,然后就可以回到其他代码的运行当中了。

该方式完美的解决了CPU空运行的低效垃圾时间,大大提高了CPU的利用率。
2.中断处理串口数据代码
首先我们先来配置HAL库

勾选配置NVIC即可
在串口的中断处理函数当中(在stm32xx_it.c中),会出现一个问题。

即可能产生中断请求的事件很多,但是中断处理函数只有一个,那么我们如何分辨时哪个请求呢。

HAL_UART_IRQHandler(&huart2);
在HAL库当中我们通过该函数来分辨到底时哪个中断请求,该函数的具体内部代码我们无需关注。我们只需知道该函数会根据中断请求来调用不同的函数即可。
为了方便我们写相关函数,HAL库对相关函数都是弱定义的,即我们可以自己写相关函数来覆盖默认的函数。
如果大家想查询可以在stm32(型号)_hal_uart.c中查询到相关函数,函数前加了_weak弱定义的修饰词。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit_IT(&huart2,message,2);
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
memset(message,0,strlen((char*)message));
HAL_UART_Receive_IT(&huart2,message,2);
}
上面代码就是简单的中断处理串口数据的代码,2个函数分别是接收完成和发送完成的回调函数,当接收到数据或者发送数据完成时,会自动调用该函数。
注意该回调函数是所有串口公用的,所以如果使用多串口要注意分辨是哪个串口调用的回调函数。
三.最终版DMA处理串口
尽管中断处理串口的方式已经大大的解放了CPU,但是CPU还是需要在一些特定时候来放置或者提取部分数据。那有没有什么方法可以使得CPU完全不用参与串口数据的处理呢?答案就是DMA。
1.DMA处理串口数据的原理
DMA全称Direct Memory Access,即直接内存访问。DMA可以使得外设直接访问内存,无需CPU的参与可以将数据从一个地方转移到另外一个地方。在使用DMA处理串口数据时,DMA代替了CPU的功能,负责在特定时间读取或写入数据。其余与中断处理串口数据的原理相同。
2.DMA处理串口数据代码
首先我们先来配置HAL库

点击add后选择需要DMA的引脚即可。
在这里我只开通了串口2接收引脚的DMA,下面是简单的接收发送DMA的代码。
uint8_t message[10] = {0};
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit_IT(&huart2,message,2);
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
memset(message,0,strlen((char*)message));
HAL_UART_Receive_DMA(&huart2,message,2);
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_DMA(&huart2,message,2);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
具体就是把_IT改成_DMA即可。因为DMA相当于取代了CPU的功能,具体是DMA转移了对应的数据(函数的最后一个数据)后,会申请中断告诉CPU我干完活了。然后CPU再运行中断函数。
DMA函数的参数意义和中断相同,在这里我不多赘述
3.利用DMA 串口发送或接收不定长数据
虽然上面我们使用串口接收和发送数据,但是还是有一个问题:我们总是得要到接收到一定数据才能进入中断,即判定接收完成。但是再一些负责项目中,我们需要利用串口传输的信息的长度是不确定的,那么我们如何解决该问题呢?
很简单,我们如果能能够知道什么时候串口不发生数据然后进入中断那么是不是就可以了。再HAL库中给我们配置了一个中断:串口空闲中断(Idle),该中断会在串口的RX引脚没有写入新数据到一定时间后进入中断,即我们认为接收完成时进入中断。
下面时利用DMA接收不定长数据的代码。
uint8_t message[500] = {0};
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart == &huart2)
{
HAL_UART_Transmit_IT(&huart2,message,Size);
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx,DMA_IT_HT);
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart2)
{
memset(message,0,strlen((char*)message));
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,message,sizeof(message));
}
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,message,sizeof(message));
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx,DMA_IT_HT);//关闭DMA传输过半中断
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
这里我们发上面的代码和先前的代码还是有很多不同。在这里我新使用了2个函数,下面是该函数的介绍
/**
* @brief Reception Event Callback (Rx event notification called after use of advanced reception service).
* @param huart UART handle
* @param Size Number of data available in application reception buffer (indicates a position in
* reception buffer until which, data are available)
* @retval None
*/
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);
/**
* @brief Disable the specified DMA Channel interrupts.
* @param __HANDLE__: DMA handle
* @param __INTERRUPT__: specifies the DMA interrupt sources to be enabled or disabled.
* This parameter can be any combination of the following values:
* @arg DMA_IT_TC: Transfer complete interrupt mask
* @arg DMA_IT_HT: Half transfer complete interrupt mask
* @arg DMA_IT_TE: Transfer error interrupt mask
* @retval None
*/
#define __HAL_DMA_DISABLE_IT(__HANDLE__, __INTERRUPT__) (CLEAR_BIT((__HANDLE__)->Instance->CCR , (__INTERRUPT__)))
/**
* @brief 串口空闲中断函数
* @param huart 串口句柄
* @param Size 从串口忙碌到串口空闲接收到的数据
* @retval None
*/
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);
/**
* @brief 关闭DMA通道中断
* @param __HANDLE__: DMA句柄
* @param __INTERRUPT__:中断类型:目前只需知道DMA_IT_HT是传输过半中断即可
* @retval None
*/
#define __HAL_DMA_DISABLE_IT(__HANDLE__, __INTERRUPT__) (CLEAR_BIT((__HANDLE__)->Instance->CCR , (__INTERRUPT__)))
在这里我们还使用了一个HAL_UARTEx_ReceiveToIdle_DMA函数,这个函数时DMA串口空闲中断启动的函数,这个函数有普通、中断、DMA三种类别。具体用法和前面的一样,具体要注意的就是这个函数的最后一个时最大接收数据。
由于我们使用的串口空闲中断时HAL库编写的额外中断,所以在这里我们使用下面中断
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);
该中断再串口空闲时会进入,Size为从串口忙碌到串口空闲接收到的数据。
但是再DMA当中还存在一个DMA过半中断,就时再DMA传输到最大数据的一半时会进入中断,在有些时候有用,但是在大部分时候这个会造成困扰,所以在这里我们关闭了过半中断。即使用下面函数
#define __HAL_DMA_DISABLE_IT(__HANDLE__, __INTERRUPT__) (CLEAR_BIT((__HANDLE__)->Instance->CCR , (__INTERRUPT__)))
1万+

被折叠的 条评论
为什么被折叠?



