首先先在CubeIDE新建一个工程,具体新建过程不作详细介绍了
在下列说明中使用STM32F429xx为例子做说明。
1、CubeIDE设置串口
此处设置为 Usart1 作为例子
设置为异步通信,波特率等参数根据自己需求进行设置,此处以115200为例。
设置完成后 Ctrl+S 保存,系统会自动生成代码
2、串口常用函数详解
该函数均在 stm32f4xx_hal_uart 文件中体现
分组 | 函数名 | 功能说明 |
---|---|---|
初始化和总体功能 | HAL_UART_Init | 串口初始化,设置串口通信参数 |
HAL_UART_MspInit | 串口初始化的 MSP 弱函数,在 HAL UART_ Tnito中被调用。重新实现的这个函数一般用于串口引脚的GPIO 初始化和中断设置 | |
HAL_UART_GetState | 获取串口当前状态 | |
HAL_UART_GetError | 返回串口错误代码 | |
阻塞方式传输 | HAL_UART_Transmit | 阻塞方式发送一个缓冲区的数据,发送完成或超时后才返回 |
HAL_UART_Receive | 阻塞方式將数据接收到一个缓沖区,接收完成或超时后才返回 | |
中断方式传输 | HAL_UART_Transmit_IT | 以中断方式(非阻塞式)发送一个缓冲区的数据 |
HAL_UART_Receive_IT | 以中断方式(非阻塞式)将指定长度的数据接收到缓冲区 | |
DAM方式传输 | HAL_UART_Transmit_DMA | 以 DMA 方式发送一个缓冲区的数据 |
HAL_UART_Receive_DMA | 以 DMA 方式发送一个缓冲区的数据 | |
HAL_UART_DMAPause | 暂停 DMA 传输过程 | |
HAL_UART_DMAResume | 继续先前暂停的 DMA 传输过程 | |
HAL_UART_DMAStop | 停止 DMA 传输过程 | |
取消数据传输 | HAL_UART_Abort | 终止以中断方式或 DMA 方式启动的传输过程,函数自身以阻塞方式运行 |
HAL_UART_AbortTransmit | 终止以中断方式或 DMA 方式启动的数据发送过程,函数自身以阻塞方式运行 | |
HAL_UART_AbortReceive | 终止以中断方式或 DMA 方式启动的数据接收过程,函数自身以阻塞方式运行 | |
HAL_UART_Abort_IT | 终止以中断方式或 DMA 方式启动的传输过程,函数自身以非阻塞方式运行 | |
HAL_UART_AbortTransmit_IT | 终止以中断方式或 DMA 方式启动的数据发送过程,函数自身以非阻塞方式运行 | |
HAL_UART_AbortReceive_IT | 终止以中断方式或 DMA 方式启动的数据接收过程,函数自身以非阻塞方式运行 |
有
由上述可知,主要分三种方式传输,阻塞、中断以及DMA,下面将这三种方式进行详
3、串口printf()重定义
首先重定向printf()函数使printf通过串口打印字符串
在usart.c文件下 在/* USER CODE BEGIN 1 */后面添加以代码,
在usart.h文件下添加 #include <stdio.h>
/***************** 重新编写printf函数 **********************/
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#define GETCHAR_PROTOTYPE int __io_getchar(FILE *f)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#define GETCHAR_PROTOTYPE int fgetc(FILE *f)
#endif /* __GNUC__ */
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
GETCHAR_PROTOTYPE
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1,(uint8_t *)&ch, 1, 0xFFFF);
if (ch == '\r')
{
__io_putchar('\r');
ch = '\n';
}
return __io_putchar(ch);
}
/*********************************************************/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
完成以后就可以直接使用 printf 函数打印数据了!!!
4、阻塞方式
在main函数中定义一个数组
/* USER CODE BEGIN 1 */
uint8_t RxBuffer[10] = {0};
/* USER CODE END 1 */
然后在while函数中编写函数
/* USER CODE BEGIN WHILE */
while (1)
{
if(HAL_UART_Receive(&huart1,RxBuffer,sizeof(RxBuffer),1000) == HAL_OK)
{
HAL_UART_Transmit(&huart1,RxBuffer,sizeof(RxBuffer),100);
}
/*
* HAL_UART_Transmit()函数和HAL_UART_Receive()一样,有四个参数
* 第一个参数是要使用的串口句柄地址,比如要使用U(S)ART1,参数就设置为U(S)ART1的句柄地址&huart1
* 第二个参数是要接受/发送的数据缓冲区首地址
* 第三个参数是接受/发送的数据长度,这里可以直接用sizeof()函数获取发送缓冲区的长度
* 第四个参数是超时时间,单位是ms,如果超过设置的时间,则函数返回HAL_TIMEOUT,如果设置为HAL_MAX_DELAY,处理器就会一直等到数据发送完成再执行下一条语句。
*/
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
编写完后就可以编译下载烧录了,然后打开串口调试助手,效果如下
但是这里会有一个问题,本文RxBuffer的长度为10,如果我发送的数据长度不为10个就会出现问题,这里设置的超时时间为1S,若1s内没接收到10个数据,函数就会跳出if语句,不进行发送。如果1s内发送超过10个数据,则只会发出10个数据出来。
为了解决上述,建议增加数据包判断,每次只接受一个字节,接受到头码尾码再后续处理数据包内容。
5、中断方式
首先修改CubeIED内的图形化设置
设置完成后,保存,系统自动修改代码
具体的函数流程以及中断过程在此处不做具体详解,具体请查看https://blog.csdn.net/u010779035/article/details/103765707
在中断过程中,最终会跑到 HAL_UART_RxCpltCallback() 函数中,在stm32f4xx_hal_usart.c中有__weak前缀,是弱定义项,我们可以在其他文件中重新写一个 HAL_UART_RxCpltCallback() 函数,这样中断最后就会跑到我们自己编写的 HAL_UART_RxCpltCallback() 函数中。
在 MX_USART1_UART_Init 函数中加入 HAL_UART_Receive_IT()(必须加有此函数,该函数不但是一个接收函数,还是一个初始化开始下一次中断的函数)
void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
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();
}
/* USER CODE BEGIN USART1_Init 2 */
HAL_UART_Receive_IT(&huart1,&RxBuf,1);//加入此函数,开始中断,并将接受到一个字节放到RxBuf地址中
/* USER CODE END USART1_Init 2 */
}
编写一个 HAL_UART_RxCpltCallback() 放在 usart.c 文件中,本例子只编写了接受一个字节,若需要多个字节请自行编写。
//接受一个字节
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
uint8_t res=0;
if(huart->Instance == USART1) //如果中断号为串口1
{
res = RxBuf[0];
HAL_UART_Transmit( &huart1,&res ,1,1 );
HAL_UART_Receive_IT(&huart1,&RxBuf,1);//重新开始下一轮中断
}
}
main函数的初始化以及while中不需要增加任何东西,保持空白即可
试验现象:
可以看到,我发送一个0x55的字节,收到后返回了一个0x55字节,表示已经成功了。
6、DMA方式
首先修改CubeIED内的图形化设置 TX以及RX
保存并且生成代码即可,
6.1 DMA发送函数 HAL_UART_Transmit_DMA
STM32CubeIDE使能了DMA三个中断:DMA 半传输(数据传输到一半会进入一次中断),DMA传输完成和DMA传输出错。如果发送数据正常,进入2次进入DMA中断(DMA 半传输和DMA传输完成);错误的话进入DMA传输出错中断;从这里也能看出HAL库比标准库严谨但效率低,DMA 半传输中断如果觉得效率低可以在程序中屏蔽掉,这样数据正常发送完成就只会进入一次DMA完成中断,三种DMA中断其实是同一个函数HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
此函数功能是处理DMA中断请求,主要工作是清除中断标志位,改写DMA的状态,只有把状态改成HAL_DMA_STATE_READY,下一次才能正常使用DMA功能,否则会进入 HAL_BUSY 状态
如果是HAL_BUSY状态,则无法连续使用HAL_UART_Transmit_DMA()函数发送数据,第二次会发不出来数据,而且第二次函数会进入HAL_BUSY状态,所以要想使用HAL_UART_Transmit_DMA()函数连续发送数据,相邻两次之间要有延时间隔或者检测DMA数据是否完成。
修改DMA半传输中断
在 stm32f4xx_hal_dma.c 文件中的 DMA HAL_DMA_Start_IT 函数中,将所有的中断都打开了,我们只需要将需要打开的中断使能位置1即可将半传输完成中断屏蔽,如下:
if(hdma->XferHalfCpltCallback != NULL)
{
//hdma->Instance->CR |= DMA_IT_HT;//关闭DMA半传输中断
}
6.2 DMA接收函数 HAL_UART_Receive_DMA()
HAL_UART_Receive_DMA()具有接收以及初始化函数的作用。在开始时需要先调用该函数,否则无法进入中断。,接收DMA函数同样具有三个中断DMA 半传输(数据接收到一半会进入一次中断),DMA接收完成和DMA接收出错。关闭DMA半传输函数与上述一样。
6.3 空闲中断
空闲中断是接受数据后出现一个byte的高电平(空闲)状态,就会触发空闲中断.并不是空闲就会一直中断,准确的说应该是上升沿(停止位)后一个byte,如果一直是低电平是不会触发空闲中断的(会触发break中断)。所以为了减少误进入串口空闲中断,串口RX的IO管脚一定设置成Pull-up<上拉模式>,串口空闲中断只是接收的数据时触发,发送时不触发。
串口空闲中断的判定是:当串口开始接收数据后,检测到1字节数据的时间内没有数据发生,则认为串口空闲了,进入相应的串口中断。在中断内清除空闲中断标志位和调用串口回调函数,在回调函数内处理读取,判断,处理接收的一帧数据。处理完一帧数据以后我再把串口中断打开重复上面的流程,就可以完整的接收一帧一帧的数据。同时利用空闲中断也可以省去很多的的判断。
首先在main()之前初始化的时候调用__HAL_UART_ENABLE_IT(&huart2,UART_IT_RXNE);
函数的功能是打开了串口的接收中断。注意这个时候我还没有打开空闲中断。而是在接收到了一个byte以后打开空闲中断。
当发送一帧数据接收完成后,会进入串口中断函数,如下函数
HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
HAL库提供的这个串口中断函数,并没有针对空闲中断的处理,所以得我们自己加相应的代码。
if(RESET != __HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)) //判断是否是空闲中断
{
__HAL_UART_CLEAR_IDLEFLAG(&huart2); //清楚空闲中断标志(否则会一直不断进入中断)
USAR_UART_IDLECallback(huart); //调用中断回调函数
}
6.4 DMA中断程序编写
在 main 函数的 /* USER CODE BEGIN 2 */ 后面开始编写
/* USER CODE BEGIN 2 */
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //使能串UART1 IDLE中断
HAL_UART_Receive_DMA(&huart1, (uint8_t*)receive_buff, BUFFER_SIZE); //设置DMA传输,讲串口2的数据搬运到recvive_buff中,
printf("DMA串口中断接收回显函数实验\n");
HAL_UART_Transmit_DMA(&huart1,tx_buffer,sizeof(tx_buffer)); //串口DMA发送一帧数据
/* USER CODE END 2 */
在 usart.c 文件的 /* USER CODE BEGIN 1 */ 后编写
void USER_UART_IDLECallback(UART_HandleTypeDef *huart)
{
HAL_UART_DMAStop(huart); //停止本次DMA传输
uint8_t data_length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);//计算接收到的数据长度
printf("\r\ndata length = %d\r\n",data_length); //接收到的数据长度
HAL_UART_Transmit_DMA(huart,receive_buff,data_length); //DMA发送函数:将接收到的数据打印出去
HAL_UART_Receive_DMA(huart, (uint8_t*)receive_buff, BUFFER_SIZE); //重新开始始下一轮的DMA中断
data_length = 0;
}
void USER_UART_IRQHandler(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1) //判断是否是串口1
{
if(RESET != __HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) //判断是否是空闲中断
{
__HAL_UART_CLEAR_IDLEFLAG(huart); //清楚空闲中断标志(否则会一直不断进入中断)
USER_UART_IDLECallback(huart); //调用中断处理函数
}
}
}
在 usart.h 文件中添加
/* USER CODE BEGIN Prototypes */
void USER_UART_IDLECallback(UART_HandleTypeDef *huart);
void USER_UART_IRQHandler(UART_HandleTypeDef *huart);
/* USER CODE END Prototypes */
最后在文件 stm32f4xx_it.c 文件中修改串口中断函数
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_UART_IRQHandler(&huart1); //新添加的函数,用来处理串口空闲中断
/* USER CODE END USART1_IRQn 1 */
}
DMA方式发现一篇更好的文章描述,具体请查看https://blog.csdn.net/CSDN_Xu_xue/article/details/104403532