使用DMA+IDLE串口空闲中断接收不定长度的数据(串口1)
1、STM32CubeMX引脚设置和代码生成
该次实验的引脚设置与 《STM32CubeMX笔记(7)–使用DMA串口发送数据,UART1发送数据》的引脚设置是一样的。
相关串口DMA选项
2、编写相关中断的C文件
1、在uart.c,增加相关代码
1、创建用于接收UART1的数据格式–UsartType1;
2、创建用于UART1发送的数组 u_buf[256];
3、创建用于UART1接收的数组 Rx_buff[50];
/* USER CODE BEGIN 0 */
USART_RECEIVETYPE UsartType1;
uint8_t u_buf[256];
uint8_t Rx_buff[50];
/* USER CODE END 0 */
1、创建UART1的DMA数据发送函数,由两部分组成(发送函数+中断回调函数);
2、普通串口中断回调函数处理;
3、串口DMA接收空闲中断处理函数;
/* USER CODE BEGIN 1 */
/**************************************************************************
函数功能:UART1(串口1)发送数据
入口参数:发送数据的数组,发送数据的长度
返回 值:无
说 明:1.发送相关数据,并置位dmaSend_flag标志位;
2.DMA发送完成后会进入中断回调函数,重新置位dmaSend_flag标志位;
**************************************************************************/
//DMA发送函数
void Usart1SendData_DMA(uint8_t *pdata, uint16_t Length)
{
while(UsartType1.dmaSend_flag == USART_DMA_SENDING);
UsartType1.dmaSend_flag = USART_DMA_SENDING;
HAL_UART_Transmit_DMA(&huart1, pdata, Length);
}
//DMA发送完成中断回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
__HAL_DMA_DISABLE(huart->hdmatx);
if(huart->Instance == huart1.Instance)
UsartType1.dmaSend_flag = USART_DMA_SENDOVER;
}
/**************************************************************************
函数功能:串口中断回调函数
入口参数:串口中断号
返回 值:无
说 明:串口中断处理回调函数
**************************************************************************/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1)
{
HAL_UART_Receive_IT(&huart1,Rx_buff,1);
}
}
/**************************************************************************
函数功能:串口DMA接收空闲中断
入口参数:串口中断号
返回 值:无
说 明:串口DMA接收空闲中断处理函数;
相关串口数据数组:UsartType1.usartDMA_rxBuf;
相关串口数据数组长度:UsartType1.rx_len;
1.清除接收空闲中断标志位__HAL_UART_CLEAR_IDLEFLAG();
2.停止DMA串口接收;处理接收数据长度;置位相关标志位;
3.重新开启DMA串口接收;
**************************************************************************/
void UsartReceive_IDLE(UART_HandleTypeDef *huart)
{
uint32_t temp;
if(huart->Instance == huart1.Instance)
{
if((__HAL_UART_GET_FLAG(huart,UART_FLAG_IDLE) != RESET))
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UART_DMAStop(&huart1);
temp = huart1.hdmarx->Instance->CNDTR;
UsartType1.rx_len = RECEIVELEN - temp;
UsartType1.receive_flag = 1;
HAL_UART_Receive_DMA(&huart1,UsartType1.usartDMA_rxBuf,RECEIVELEN);
}
}
}
/* USER CODE END 1 */
2、在uart.h,增加相关代码
1、增加两个用于自定义 printf()函数 的相关头文件;
/* USER CODE BEGIN Includes */
#include "string.h"
#include "stdio.h"
/* USER CODE END Includes */
1、用宏定义 #define 构建自定义的3个不同方式输出的printf() 函数;
2、构建用于UART1数据接收的结构体USART_RECEIVETYPE;
3、链接相关的数据定义;
/* USER CODE BEGIN Private defines */
#define UART1_printf_Tr(...) HAL_UART_Transmit(&huart1,\
(uint8_t *)u_buf,\
sprintf((char*)u_buf,__VA_ARGS__),\
0xffff)
#define UART1_printf_DMA(...) HAL_UART_Transmit_DMA(&huart1,\
(uint8_t *)u_buf,\
sprintf((char*)u_buf,__VA_ARGS__))
#define UART1_printf_IT(...) HAL_UART_Transmit_IT(&huart1,\
(uint8_t *)u_buf,\
sprintf((char*)u_buf,__VA_ARGS__))
/* 构建用于UART数据接收的结构体USART_RECEIVETYPE */
#define RECEIVELEN 1024
#define USART_DMA_SENDING 1//发生未完成
#define USART_DMA_SENDOVER 0//发生完成
typedef struct
{
uint8_t receive_flag:1;//空闲接收完成
uint8_t dmaSend_flag:1;//发送完成
uint16_t rx_len;//接收长度
uint8_t usartDMA_rxBuf[RECEIVELEN];//DMA接收缓存
}USART_RECEIVETYPE;
extern USART_RECEIVETYPE UsartType1;
extern uint8_t u_buf[256];
extern uint8_t Rx_buff[50];
/* USER CODE END Private defines */
1、定义在.c文件中的相关函数名;
/* USER CODE BEGIN Prototypes */
void Usart1SendData_DMA(uint8_t *pdata, uint16_t Length);
void UsartReceive_IDLE(UART_HandleTypeDef *huart);
/* USER CODE END Prototypes */
3、在stmf1xx_it.c,增加相关代码
1、在void USART1_IRQHandler(void)中断处理中增加UsartReceive_IDLE(&huart1)串口空闲中断接收函数;
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
UsartReceive_IDLE(&huart1);
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
4、在main.c,增加相关代码
1、开启串口1的DMA接收;
2、开启串口1的IDLE串口空闲中断接收使能;
/* USER CODE BEGIN 2 */
HAL_UART_Receive_DMA(&huart1, UsartType1.usartDMA_rxBuf, RECEIVELEN);//串口1DMA
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
/* USER CODE END 2 */
1、利用Usart1SendData_DMA函数打印串口1的DMA接收的相关数据;
2、利用自定义的UART1_printf_DMA函数打印串口1的DMA接收的相关数据长度;
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if(UsartType1.receive_flag)//如果产生了空闲中断
{
UsartType1.receive_flag=0;//清零标记
//串口打印收到的数据
Usart1SendData_DMA(UsartType1.usartDMA_rxBuf,UsartType1.rx_len);
HAL_Delay(10);
//串口打印收到的数据的数据长度
UART1_printf_DMA("Len is %d \r\n",UsartType1.rx_len);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
3、编译工程文件,使用ST-Link烧录,测试串口数据DMA+IDLE空闲中断的接收
使用串口助手发送数据
DMA+IDLE is ok
接收的数据长度为16个(数据后面有回车+换行);
也可以发送其它长度的数据,该工程代码均能将其接收分辨清除。
4、相关DMA+IDLE空闲中断的接收不定长度的数据知识
DMA(Direct memory access),即直接存储器访问。用于在外设与存储器之间以及存储器与存储器之间提供一种高速数据传输的方式。它在开始发送和接收完成数据时会给CPU相应的信号或者中断,在数据传输过程中无需CPU参与,通过硬件方式为RAM与I/O设备提供一条直接传送数据的通道。
相关参考链接
4.1 DMA串口发送和接收函数
4.1.1 DMA发送函数
HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
函数主要功能是以DAM模式发送pData指针指向的数据中固定长度的数据,并同时设置和使能DMA中断,具体怎么设置和使能中断的,打开此函数源码会发现下面这个函数。
HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);
此函数是启动DMA传输并启用中断。从函数源码可以看出此函数首先判断DMA传输状态是否是Ready:
-
如果是Ready则使能了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状态 -
如果不是Ready状态,则进入HAL_BUSY状态,这也是为什么连续使用
HAL_UART_Transmit_DMA()
函数发送数据,第二次会发不出来数据,而且第二次函数会进入HAL_BUSY状态,所以要想使用HAL_UART_Transmit_DMA()
函数连续发送数据,相邻两次之间要有延时间隔或者检测DMA数据是否完成。
4.1.2 DMA接收函数
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
函数说明:此函数的功能在DMA模式下接收大量数据,同时设置DMA线和哪个串口外设连接,以及将DMA线接收到的数据搬 *pData对应地内存中,和上面DMA发送函数一样,此函数同时具有设置和使能DMA中断的功能。可以看出此函数不仅是一个接收函数同时也是一个初始化函数,在main()
之前调用,初始化串口和DMA连接和DMA接收BUF,以及设置和使能中断。如果DMA模式设置成循环模式时,只需设置这一次,如果DMA模式设置成正常模式时,每次读取完数据后需要再从新设置一次(就是再调用一次此函数),分析函数源码会发现此函数内部同样会调用以下两个函数,使用方法和分析和发送类似,不再赘述。
HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);
HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma);
主要说说HAL_UART_Receive_DMA()
,怎么配合IDLE串口空闲中断使用,main()
函数之前一般调用此函数,一个主要目的是指明DMA传输串口数据存到指定的地方。一般情况我们会开辟一个全局变量的缓存
extern uint8_t receive_buff[BUFFER_SIZE]
。比如函数初始化为HAL_UART_Receive_DMA(&huart2, (uint8_t*)receive_buff, BUFFER_SIZE);
就是设置串口2接收到数据通过DMA线直接到receive_buff
中了,配合串口空闲中断,当进入串口空闲中断,说明一帧数据已接收完成。我们读取receive_buff
相应长度的数据就是此次接收一帧的数据,这里还需要再介绍一个函数;
__HAL_DMA_GET_COUNTER(__HANDLE__)
此函数的功能:获取当前DMA通道传输中receive_buff[BUFFER_SIZE]
缓存还剩余多少个数据单元。这样就能算出这一帧数据到底接收了多少单元的数据(数据长度=缓存总长度-缓存剩余的长度),
length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
4.2 空闲中断
4.2.1 空闲中断介绍
- 空闲中断是接受数据后出现一个byte的高电平(空闲)状态,就会触发空闲中断.并不是空闲就会一直中断,准确的说应该是上升沿(停止位)后一个byte,如果一直是低电平是不会触发空闲中断的(会触发break中断)。所以为了减少误进入串口空闲中断,串口RX的IO管脚一定设置成Pull-up<上拉模式>,串口空闲中断只是接收的数据时触发,发送时不触发。
4.2.2 空闲中断使用
- 串口空闲中断的判定是:当串口开始接收数据后,检测到1字节数据的时间内没有数据发生,则认为串口空闲了,进入相应的串口中断。在中断内清除空闲中断标志位和调用串口回调函数,在回调函数内处理读取,判断,处理接收的一帧数据。处理完一帧数据以后我再把串口中断打开重复上面的流程,就可以完整的接收一帧一帧的数据。同时利用空闲中断也可以省去很多的的判断。
4.2.3 空闲函数调用
-
首先在
main()
之前初始化的时候调用__HAL_UART_ENABLE_IT(&huart2,UART_IT_RXNE);
函数的功能是打开了串口的接收中断。注意这个时候我还没有打开空闲中断。而是在接收到了一个byte以后打开空闲中断。 -
当发送一帧数据接收完成后,会进入串口中断函数,如下函数
HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
HAL库提供的这个串口中断函数,并没有针对空闲中断的处理,所以得我们自己加相应的代码。
/**************************************************************************
函数功能:串口DMA接收空闲中断
入口参数:串口中断号
返回 值:无
说 明:串口DMA接收空闲中断处理函数
1.清除接收空闲中断标志位__HAL_UART_CLEAR_IDLEFLAG();
2.停止DMA串口接收;处理接收数据长度;置位相关标志位;
3.重新开启DMA串口接收;
**************************************************************************/
void UsartReceive_IDLE(UART_HandleTypeDef *huart)
{
uint32_t temp;
if(huart->Instance == huart1.Instance)
{
if((__HAL_UART_GET_FLAG(huart,UART_FLAG_IDLE) != RESET))
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
HAL_UART_DMAStop(&huart1);
temp = huart1.hdmarx->Instance->CNDTR;
UsartType1.rx_len = RECEIVELEN - temp;
UsartType1.receive_flag = 1;
HAL_UART_Receive_DMA(&huart1,UsartType1.usartDMA_rxBuf,RECEIVELEN);
}
}
}
在stmf1xx_it.c相关的中断处理函数
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
UsartReceive_IDLE(&huart1);
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
5、相关数据的解析函数编写
将利用串口空闲中断存储的数据进行相关的解析及其数据协议的定义。
5.1 新建comm.h,增加相关代码
#ifndef __COMM_H
#define __COMM_H
#include "main.h"
#include "gpio.h"
void resetCommand(void);
void runCommand(void);
void DateProcess(uint8_t *buff, uint16_t Length);
#endif
5.2 新建comm.c,增加相关代码
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "usart.h"
#include "COMM\comm.h"
#include "LED\led.h"
#include "KEY\key.h"
/**************************************************************************
相 关 变 量 说 明:
int arg = 0;----- 数组转换标志位:arg:0--cmd 1--argv1[16] 2--argv2[16]
int inde = 0;---- 数组元素标志位
char chr;-------- 用于读取串口数据数组的转换字符型数据
char cmd;-------- 用于储存相关字符型编号任务
char argv1[16];-- 用于储存第一段数字数据的数组
char argv2[16];-- 用于储存第二段数字数据的数组
int arg1;-------- 用于将argv1[16]储存的数字数据解析出来
int arg2;-------- 用于将argv2[16]储存的数字数据解析出来
**************************************************************************/
int arg = 0;
int inde = 0;
char chr;
char cmd;
char argv1[16];
char argv2[16];
int arg1;
int arg2;
/**************************************************************************
函数功能:重置相关变量,用于下次数据解析
入口参数:无
返回 值:无
说 明:将数据解析中的相关标志位、数据存储变量、数组数据等重新置位
**************************************************************************/
void resetCommand(void)
{
cmd = NULL;
memset(argv1, 0, sizeof(argv1));
memset(argv2, 0, sizeof(argv2));
arg1 = 0;
arg2 = 0;
arg = 0;
inde = 0;
}
/**************************************************************************
函数功能:执行相关数据命令函数
入口参数:无
返回 值:无
说 明:数据解析完成后,将相关数据用于执行任务
**************************************************************************/
void runCommand(void)
{
arg1 = atoi(argv1);
arg2 = atoi(argv2);
UART1_printf_DMA("Number-1 is %d \r\n",arg1);
HAL_Delay(10);
UART1_printf_DMA("Number-2 is %d \r\n",arg2);
HAL_Delay(10);
switch (cmd)
{
case 'A':
UART1_printf_DMA("cmd = A");
break;
case 'B':
UART1_printf_DMA("cmd = B");
break;
case 'C':
UART1_printf_DMA("cmd = C");
break;
case 'D':
UART1_printf_DMA("cmd = D");
break;
case 'E':
UART1_printf_DMA("cmd = E");
break;
case 'F':
UART1_printf_DMA("cmd = F");
break;
case 'G':
UART1_printf_DMA("cmd = G");
break;
case 'H':
UART1_printf_DMA("cmd = H");
break;
default:
UART1_printf_DMA("cmd = Other");
break;
}
}
/**************************************************************************
函数功能:收到上位机发来的数据后进行处理
入口参数:uint8_t *buff: 接收到的串口数组; uint16_t Length:数组长度;
返回 值:无
说 明:搭配串口空闲中断进行数据解析;
串口1空闲中断处理函数:UsartReceive_IDLE(&huart1);
---串口1空闲处理函数得到:相关串口数据数组:UsartType1.usartDMA_rxBuf;
相关串口数据数组长度:UsartType1.rx_len;
数据接收处理函数:DateProcess(UsartType1.usartDMA_rxBuf,UsartType1.rx_len);
**************************************************************************/
void DateProcess(uint8_t *buff, uint16_t Length)
{
int i=0;
for(i=0;i<Length;i++)
{
chr = buff[i];
//使用回车命令(CR)中断解析
if (chr == 13)
{
if (arg == 1) argv1[inde] = NULL;
else if (arg == 2) argv2[inde] = NULL;
break;
}
//使用空格(' ')限定参数部分的数据命令内容
else if (chr == ' ')
{
//逐项设置相关数组的转换变量
if (arg == 0) arg = 1;
else if (arg == 1)
{
argv1[inde] = NULL;
arg = 2;
inde = 0;
}
continue;
}
else
{
if (arg == 0)
{
//第一个参数是单字母命令
cmd = chr;
}
else if (arg == 1)
{
//后续的参数可以是一个以上的字符,并将相关命令数组解析出
argv1[inde] = chr;
inde++;
}
else if (arg == 2)
{
argv2[inde] = chr;
inde++;
}
}
}
runCommand();
resetCommand();
}
5.3 在main.c,增加相关代码,测试解析函数
/* USER CODE BEGIN Includes */
#include "LED\led.h"
#include "KEY\key.h"
#include "COMM\comm.h"
/* USER CODE END Includes */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if(UsartType1.receive_flag)//如果产生了空闲中断
{
UsartType1.receive_flag=0;//清零标记
Usart1SendData_DMA(UsartType1.usartDMA_rxBuf,UsartType1.rx_len);//串口打印收到的数据
HAL_Delay(10);
UART1_printf_DMA("Len is %d \r\n",UsartType1.rx_len);//打印数据数组长度
HAL_Delay(10);
DateProcess(UsartType1.usartDMA_rxBuf,UsartType1.rx_len);//使用解析命令数据函数
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
测试:
串口发送命令如下
B 123 456
5.4 解析文件中的部分函数说明
C语言提供了几个标准库函数,可以将字符串转换为任意类型(整型、长整型、浮点型等)的数字。
字符串转换成数字的三种方法----相关参考链接
以下是用atoi()函数将字符串转换为整数的一个例子:
# include <stdio.h>
# include <stdlib.h>
void main (void) ;
void main (void)
{
int num;
char * str = "100";
num = atoi(str);
printf("The string 'str' is %s , and the number 'num' is %d. \n",
str, num);
}
atoi()函数只有一个参数,即要转换为数字的字符串。atoi()函数的返回值就是转换所得的整型值。
下列函数可以将字符串转换为数字:
函数名 | 作 用 |
---|---|
atof() | 将字符串转换为双精度浮点型值 |
atoi() | 将字符串转换为整型值 |
atol() | 将字符串转换为长整型值 |
strtod() | 将字符串转换为双精度浮点型值,并报告不能被转换的所有剩余数字 |
strtol() | 将字符串转换为长整值,并报告不能被转换的所有剩余数字 |
strtoul() | 将字符串转换为无符号长整型值,并报告不能被转换的所有剩余数字 |
将字符串转换为数字时可能会导致溢出,如果你使用的是strtoul()这样的函数,你就能检查这种溢出错误。请看下例:
# include <stdio.h>
# include <stdlib.h>
# include <limits.h>
void main(void);
void main (void)
{
char* str = "1234567891011121314151617181920" ;
unsigned long num;
char * leftover;
num = strtoul(str, &leftover, 10);
printf("Original string: %s\n",str);
printf("Converted number: %1u\n" , num);
printf("Leftover characters: %s\n" , leftover);
}
在上例中,要转换的字符串太长,超出了无符号长整型值的取值范围,因此,strtoul()函数将返回ULONG_MAX(4294967295),并使。char leftover指向字符串中导致溢出的那部分字符;同时,strtoul()函数还将全局变量errno赋值为ERANGE,以通知函数的调用者发生了溢出错误。函数strtod()和strtol()处理溢出错误的方式和函数strtoul()完全相同,你可以从编译程序文档中进一步了解这三个函数的有关细节。