前言
本次将用到的单片机是STM32F103c8t6最小系统板,调试器是ST-Link V2、串口USB to TTL。开发软件用的是Clion、STM32CubeMX、串口调试助手。本次主要分享实现串口通信的3种方式,分别是阻塞、中断、DMA。下面将会通过HAL库实现上述3种方式。细节时刻部分可以选看。
目录
一、阻塞方式
STM32CubeMX配置串口阻塞
首先根据自己的下载器在SYS的debug中选择对应的调试方式,我使用的是STlink-V2,因此在debug下拉选项中选择Serial Wire
其次配置时钟。在RCC(Reset and Clock Control 复位和时钟控制)的HSE(高速外部)时钟源中选择Crystal/Ceramic Resonator(外部晶振),此时时钟树可设置外部震荡电路的晶振频率
紧接着在Clock Configuration中设置时钟。如下如图,再按下回车后时钟树会自动生成所需要的时钟。
最后配置串口1。在Connectivity组件下找到USART1,并在Mode中选择Asynchronous(异步,默认串口波特率为115200)。
主要函数
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);
函数名:HAL_UART_Transmit
函数功能:用于指定串口发送指定长度数据
返回值:HAL_OK表示发送成功,HAL_ERROR、HAL_BUSY、HAL_TIMEOUT都表示发送失败
各个参数含义:
huart:串口句柄指针,如&huart1、&huart2等
pData:需要发送的数据缓冲区指针
Size:指定发送数据的长度
Timeout:发送超时时间,单位为毫秒
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
函数名:HAL_UART_Receive
函数功能:用于指定串口接收指定长度数据
返回值:HAL_OK表示接收成功,HAL_ERROR、HAL_BUSY、HAL_TIMEOUT都表示接收失败
各个参数含义:
huart:串口句柄指针,如&huart1、&huart2等
pData:需要接收的数据缓冲区指针(简单来说就是你接收的数据需要放到那个数组)
Size:指定接收数据的长度
Timeout:接收超时时间,单位为毫秒
1.阻塞发送
你可以直接在主循环之前,串口初始化之后,直接添加以下代码:
/* USER CODE BEGIN 2 */
HAL_UART_STATUS=HAL_UART_Transmit(&huart1,"hello embedded!\n", sizeof("hello embedded!\n"),0xFFFF);
if(HAL_OK == HAL_UART_STATUS)//如果成功发送就执行下面的语句
HAL_UART_Transmit(&huart1,"success", sizeof("success"),0xFFFF);
/* USER CODE END 2 */
当然这会使变量HAL_UART_STATUS报错,这时你需要在主函数一开始时声明这个变量,如下图:
完成以上的代码编写即可编译和调试。这时设置好波特率并打开串口助手,随后复位STM32F103c8t6,串口助手接收数据如下图
2.阻塞接收
首先在主函数开始处,声明一个数组用来做串口1的接收缓冲区。值得注意的是,这个数组的类型最好和HAL_UART_Receive函数的第二个参数类型uint8_t)一样。
这个UART1_RxBuffer数组的长度为2,也就意味着串口最多接收2字节的数据。在主循环中编写如下代码
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
//如果UART1在1秒内成功接收,则发送接收到的数据
if(HAL_OK == HAL_UART_Receive(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer),1000))
HAL_UART_Transmit(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer),0xFFFF);
}
/* USER CODE END 3 */
随后调试串口助手,在数据发送区发送ok,显示结果如下
3.细节时刻
如果在发送区输入的数据是 gay ,那么数据接收区会显示 ga ,这不难理解,因为接收数据缓冲区UART1_RxBuffer的大小为两字节。要是你以较慢的速度在数据发送区发送数据 A ,你会惊讶的发现数据接收区没有任何显示,如果你是快速连点两次发送数据,那么数据接收区会出现AA 。这又是怎么导致的呢?
HAL_UART_Receive(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer),1000)阻塞接收函数的第4个参数是关键。它的第四个参数规定了接收超时时间,将其改为0xFFFF(即65535毫秒,65.535秒)毫秒,那么这个时候发送数据 A ,在后续的65.535秒内,串口接收区也不会超时接收。
二、中断方式
STM32CubeMX配置串口中断
在原有的工程上,去STM32CubeMX中配置串口中断使能即可,如下图:
主要函数
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
函数名:HAL_UART_Transmit_IT
函数功能:用于指定串口以中断方式发送指定长度数据
返回值:HAL_OK表示发送成功,HAL_ERROR、HAL_BUSY都表示发送失败
各个参数含义:
huart:串口句柄指针,如&huart1、&huart2等
pData:需要发送的数据缓冲区指针
Size:指定发送数据的长度
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
函数名:HAL_UART_Receive_IT
函数功能:用于指定串口以中断方式接收指定长度数据
返回值:HAL_OK表示设置成功,HAL_ERROR、HAL_BUSY都表示设置失败
各个参数含义:
huart:串口句柄指针,如&huart1、&huart2等
pData:需要接收的数据缓冲区指针
Size:指定接收数据的长度
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { /* Prevent unused argument(s) compilation warning */ UNUSED(huart); /* NOTE: This function should not be modified, when the callback is needed, the HAL_UART_RxCpltCallback could be implemented in the user file */ }
HAL_UART_RxCpltCallback( )是以HAL_UART_Receive_IT( )启动串口数据接收,并完成指定长度数据接收后调用的回调函数。__weak表示为”弱的“,简单来说这个回调函数可以被重载(重新定义)。
void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }
USART1_IRQHandler(中断服务函数)是定义在stm32f1xx_it.c文件中的库函数,该函数在UART1触发中断时被调用,可以在里面直接添加需要执行的任务。
1.中断发送
直接在主循环前,串口初始化之后,调用HAL_UART_Transmit_IT函数,代码如下
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Transmit_IT(&huart1,"hello interrupt\n", sizeof("hello interrupt\n"));
/* 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.中断接收
首先需要在main.c文件中定义一个全局变量UART1_RxBuffer用来作为串口1的接收缓冲区,如下图
其次需要调用HAL_UART_Receive_IT开启一次串口的中断接收,代码如下
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Transmit_IT(&huart1,"hello interrupt\n", sizeof("hello interrupt\n"));
//开启一次中断接收
HAL_UART_Receive_IT(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer));
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
然后在主函数前重新定义接收回调函数。注意回调函数HAL_UART_RxCpltCallback( )中调用了一次中断发送函数HAL_UART_Transmit_IT( ),它将接收到的数据发送给串口助手,后面会考察这个点。代码实现如下图:
【难点】值得注意的是HAL_UART_Receive_IT( )函数完成一次数据接收后会关闭串口中断接收,不会自动进行下次的接收,因此需要再次调用HAL_UART_Receive_IT( )函数才能启动下次接收,但一般不在HAL_UART_RxCpltCallback( )函数中调用HAL_UART_Receive_IT( )函数。这是由于在HAL_UART_RxCpltCallback( )函数内调用HAL_UART_Receive_IT( )函数可能会导致递归调用和栈溢出,尤其是回调函数中没有适当的逻辑来限制或管理这些调用时。
因此可以在中断服务函数(USART1_IRQHandler( ))中重新启动串口接收中断,即调用HAL_UART_Receive_IT( )函数。代码如下
//外部引用UART1_RxBuffer
extern uint8_t UART1_RxBuffer[4];
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
//重新开启串口中断接收
HAL_UART_Receive_IT(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer));
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
在串口中断服务程序中,重新开启串口中断接收时,由于接收数据缓冲区(UART1_RxBuffer)定义在main.c文件中,因此需要使用 关键词extern 外部引用UART1_RxBuffer。
接着在USART1_IRQHandler( )中断服务函数中调用HAL_UART_Receive_IT( )中断接收函数开启串口中断接收,准备下次接收数据。此时调试并复位单片机,接着在数据发送区先发送数据ABCD ,串口助手数据接收区显示如下图
符合预期效果。下面试着在数据发送区依次发送 E、F、G、H 。不出意外的,只有在发送完H 后,数据接收区才会显示 EFGH 。这也就意味着 HAL_UART_RxCpltCallback( )回调函数被调用,因为在数据H被发送过去前,UART1_RxBuffer缓冲区已经接收到了EFG,还差一个数据就达到了HAL_UART_Receive_IT( )串口中断接收函数设置的接收缓冲区大小。所以,在H被发送过去后,数据缓冲区达到了指定长度, HAL_UART_RxCpltCallback( )回调函数被调用。过程如下图所示
发送完 F,注意发送字节数和接收字节数。随后在发送完 H 后,观察发送字节数和接收字节数,如下图
3.细节时刻
下面来看一看HAL_UART_Transmit_IT ( )串口中断发送函数、HAL_UART_Receive_IT( )串口中断接收函数、HAL_UART_RxCpltCallback( )接收回调函数、USART1_IRQHandler( )串口1中断服务函数它们之间的关联。
首先来看HAL_UART_RxCpltCallback( )接收回调函数和USART1_IRQHandler( )串口1中断服务函数它们的执行先后顺序。不难发现 中断服务函数是在回调函数之前被调用 的,为了验证这一点,需要在USART1_IRQHandler( )函数中添加如下代码
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
//将接收缓存第一个数据修改为A
UART1_RxBuffer[0] = 'A';
//重新开启串口中断接收
HAL_UART_Receive_IT(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer));
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
下面调试串口,复位单片机后,在数据发送区发送 1234 后,观察数据接收区接收的数据为 A234 (如下图)
这就意味着USART1_IRQHandler( )在HAL_UART_RxCpltCallback( )之前被调用。那么在HAL_UART_RxCpltCallback( )被调用前USART1_IRQHandler( )被调用了多少次呢?在USART1_IRQHandler( )添加如下代码
//外部引用UART1_RxBuffer
extern uint8_t UART1_RxBuffer[4];
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
//声明静态变量IR_cnt记录该函数被调用的次数
static uint8_t IR_cnt = 0;
//每调用一次就加1
IR_cnt++;
//调用次数赋值到缓冲区的第一个数据
UART1_RxBuffer[0] = 'A'+ IR_cnt;
//重新开启串口中断接收
HAL_UART_Receive_IT(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer));
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
//调用HAL_UART_RxCpltCallback( )
/* USER CODE END USART1_IRQn 1 */
}
调试串口助手,并复位。在数据发送区发送 1234 ,观察数据接收区如下图所示
发现接收数据区显示 X234 ,从接收数据区的X表明一共调用了 23 次( 'X' - ' A' )串口中断服务函数。为什么会调用 23 次?,23 次又是否正确?
首先,程序会先调用HAL_UART_Transmit_IT ( )函数发送 ”hello interrup\n“ ( 一共17字节),对于每个要发送的字符,都会触发一次中断。并且在 完成数据发送后还会触发一次中断 ,至此光发送字符串”hello interrup\n“就已经调用了18次串口中断,即 IR_cnt = 18 。
随后在串口助手向单片机发送 1234 。此时触发串口接收中断,每接收一个字符就会触发一次串口中断,也就是4次,这个时候 IR_cnt = 22 。当接收数据缓冲区达到指定长度,也就是接收完成后,会调用接收回调函数(当你为串口设置了接收回调函数,并且串口接收完成指定长度的数据后,通常情况下,不会再次调用串口中断服务函数(ISR)来处理这些已经在回调函数中处理过的数据)。
在接收回调函数中,又使用HAL_UART_Transmit_IT ( )函数发送接收数据缓冲区中的数据 1234 。当准备发送字符 ' 1 ' 时,触发中断,此时 IR_cnt = 23 ,那么 UART1_RxBuffer[0] = ' A ' + 23 (也就是 ‘ X ’)。因此串口助手接收到的数据是 X234 ,在发送完数据后 IR_cnt = 27。
当下次再向单片机发送数据 5678 时, 接收触发4次中断,在接收回调函数中调用HAL_UART_Transmit_IT ( )发送接收到的数据。当发送字符 ' 5 ' 时,触发中断,此时 IR_cnt = 32,即 UART1_RxBuffer[0] = 'A' + 32 (也就是 ‘ a ’) 。因此串口助手数据接收区显示 a678 。(如下图)
三、DMA方式
STM32cubeMX配置DMA
DMA简介
DMA(Direct Memory Access,直接内存访问)是一种计算机硬件技术,它允许某些硬件子系统在不直接涉及CPU的情况下,读写主内存。 这种技术大大提高了数据传输的效率,尤其是在处理大量数据时,可以减少CPU的负担,使得CPU能够专注于执行计算和控制任务。
主要函数
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)
函数名:HAL_UART_Transmit_DMA
函数功能:用于指定串口以直接内存访问方式发送指定长度数据
返回值:HAL_OK表示发送成功,HAL_ERROR、HAL_BUSY都表示发送失败
各个参数含义:
huart:串口句柄指针,如&huart1、&huart2等
pData:需要发送的数据缓冲区指针
Size:指定发送数据的长度
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
函数名:HAL_UART_Receive_DMA
函数功能:用于指定串口以直接内存访问方式接收指定长度数据
返回值:HAL_OK表示设置成功,HAL_ERROR、HAL_BUSY都表示设置失败
各个参数含义:
huart:串口句柄指针,如&huart1、&huart2等
pData:需要接收的数据缓冲区指针
Size:指定接收数据的长度
1.DMA发送
在主循环之前,DMA初始化和串口初始化之后直接调用HAL_UART_Transmit_DMA( )函数发送 " hello DMA "。如下图
调试并复位单片机,串口助手数据接收区显示如下图
2.DMA接收
DMA接收的用法和中断接收的用法大差不差,因此这里介绍 串口空闲中断和DMA实现接收不定长度数据 。
串口空闲中断(IDLE interrupt)是在串口接收/发送完一帧数据后,检测到接收/发送线在一个字节时间内没有新的数据到来时触发的。 这意味着,当串口从接收/发送状态转换到空闲状态时,会产生一个中断信号,以便CPU能够及时响应并采取相应的动作,例如读取数据或准备接收新的数据帧。简单来说,串口空闲中断是在串口不收发数据时产生的中断。
首先定义接收数据缓冲区UART1_RxBuffer,接着开启空闲中断,设置DMA接收缓冲区和大小,代码如下
随后在USART1_IRQHandler( )中添加如下代码,记得外部引用UART1_RxBuffer。
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
//获取空闲状态
uint8_t idle_state= __HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE);
uint8_t UART1_TxBuffer[128];
if(RESET != idle_state) {
//清除空闲中断标志位
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UART_DMAStop(&huart1);
strcpy(UART1_TxBuffer, UART1_RxBuffer);
//回显接收到的数据
HAL_UART_Transmit_DMA(&huart1, UART1_TxBuffer, strlen(UART1_TxBuffer));
memset(UART1_RxBuffer, '\0', 128);
HAL_UART_Receive_DMA(&huart1, UART1_RxBuffer, 128);
}
/* USER CODE END USART1_IRQn 1 */
}
最后调试串口助手,复位单片机后,向单片机分别发送 ABCD 和 0123456789 ,串口助手数据接收区结果如下
3.细节时刻
值得注意的是如果要使用串口空闲中断,记得使能开启串口空闲中断使能
//使能串口空闲中断
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
总结
以上3种方式各有千秋,要根据具体开发要求来选择实现方式。对于阻塞在对实时性要求不高的开发中可以使用,反之对实时要求较高的可以用到中断,和DMA。