我们接着上小节,在上一小节中,我们利用USART的中断系统实现了串口的接收功能,但是在收数据的时候,只能一字节一字节传输,或者由我们设置的字节数顺序传输,如果设置了传送数字,就不能传送字符串,实在有些不方便(比如,我要发送123456给单片机,它智能一个字节一个字节的从队列读出来再依次传给我),除此之外,每次传输数据,都要占用CPU,在任务量多且数据量大的时候,CPU负荷将会大大增加,那么本小节就来解决以上讲到的问题:如何利用USART串口通信实现字符串、数字的直接传输且不占用CPU。
首先先要引入一下DMA数据通道的概念,简单来说,DMA就是单片机上的一个不占用CPU的对外连接的通道,可以连接各种外设,在传输数据的过程中,不用预先存储在单片机的寄存器,而是直接通过DMA通道传输。这样一来,数据的传输不会占用CPU内存,实现大量数据的传输,DMA在工程中的应用十分广泛。
一、CubeMX配置
接下来利用Cubemx进行配置:
重新新建一个工程,配置RCC和SYS,因为要用到Free RTOS,所以不选用SYSTICL,这里我们使用的是TIM1
使能USART1为异步通信:
打开NVIC中断,设置DMA
添加两个DMA,分别为发送DMA通道和接收DMA通道,优先级设置为very high
确保三个中断都打开
配置FREERTOS,添加一个任务,命名为USART_Rx(这里没有发送任务,以接收任务为例)
再创建一个队列,用来存放DMA传输的数据,注意:这里的队列我们设置为指针模式,即队列传输时传的是一个地址,传输的数据我们要放到指针当中。
Queue Size设置为5(看具体需求,设置为1也没问题,当然越多传输的越快)
Item Size 我们设置为void * ,即设为一个指针
这个Interface设为V2 V1 都可以,V2 具有更大的API函数,这里以V2 为例
配置时钟为72MHz
命名并将IDE改为MDK
注意:如果使用的Interface是V2 ,下面的Package要改为1.8.5的版本
生成Keil5 代码
二、代码程序
先看到我们的freertos.c文件,由目的出发,我们想要上位机通过DMA传送数据给单片机,而DMA又是通过传送数据到队列中进行数据的传输,那么我们在任务中就应该从队列中取数据,并对数据进行处理(我这里对数据做了一个回显操作),接下来看代码部分:
void task2(void *argument)
{
/* USER CODE BEGIN task2 */
__HAL_UART_ENABLE_IT(&huart1 ,UART_IT_IDLE);
//开启空闲中断,当然你也可以用上一节的HAL_UART_Receive_IT(&huart1,&存放地址,1)由于这里我们要用DMA传输,没有用到地址,所以不用该函数,直接开启中断
HAL_UART_Receive_DMA(&huart1 ,uart_rx_data_t[uart_buff_ctrl].buffer,UART_BUFFER_SIZE);//我们用DMA传输,用这个receive函数,存储在结构体中
UART_RX_TypeDef *RecvUartData;//定义一个结构体指针,用来存储接收的数据的.buffer和.size
/* Infinite loop */
for(;;)
{
if(xQueueReceive(uart_queueHandle,&RecvUartData,portMAX_DELAY )== pdTRUE){
//队列中存入了数据
//将数据做了一个串口回显,当然你用作其它处理也可以
HAL_UART_Transmit(&huart1,RecvUartData->buffer,RecvUartData->size,1000);
}
osDelay(1);
}
先看到for函数,我们肯定是先编写代码,然后看需要用到什么变量,再依次去定义它;
在for循环中,我写了一个if语句,如果队列中有数据了,就把数据回显到上位机;
用到了xQueueReceive函数,uart_queueHandle是创建的队列的名称,在文件的上方有cubemx帮我们定义
RecvUartData是我们存数据的地址,这里我们需要定义一下,由于USART在传输数据时需要知道数据的字节数,所以指针RecvUartData中应该包含两个数据1.buffer具体的数据;2.size数据的长度,于是这个指针应该是一个结构体,所以我们定义一个结构体,这个结构体中应该包含两个数据:
typedef struct {
uint8_t buffer[UART_BUFFER_SIZE];
uint16_t size;
}UART_RX_TypeDef;
在文件的最上方定义一个结构体,我们命名为 UART_RX_TypeDef;可以看到结构体中有两个参数,一个是buffer数组,数组的个数为UART_BUFFER_SIZE个,这里我们需要定义一下UART_BUFFER_SIZE,也就是说,buffer数组共有256个数据位供我们存放,也就是buffer的数据空间大小;我们再定义了 一个UART_BUFFER_QUANTITY为5,这与我们的队列长度Item Size一致,Item Size改变时,这个也要修改;
有了结构体,我们再去定义RecvUartData;UART_RX_TypeDef *RecvUartData;
HAL_UART_Transmit串口发送函数,RecvUartData->buffer结构体中传输的数据;RecvUartData->size结构体中的size数据的长度;
目的已经明确了,那现在怎么让数据通过DMA传输到队列里呢?接着往下,打开main.c文件
如果我们打开了串口的中断,当上位机发送数据过来时,触发中断,如果这个时候我们再中断函数中执行DMA存入队列的操作,即可达到目的;
所以我们定义一个函数void USART1_DMAHandler(void),一旦中断发生,执行该函数。
接下来看该函数怎么实现传输队列的操作:
void USART1_DMAHandler(void){
//创建一个DMA函数,一旦串口产生中断,则执行该函数,
//所以该函数要写在USART1_IRQHandler函数当中,一旦上位机发送信号触发,执行USART1_IRQHandler函数,即可执行到该函数
if(__HAL_UART_GET_FLAG(&huart1 ,UART_FLAG_IDLE)!=RESET){
//判断是否发生空闲中断,此时既然能进入该函数,一定已经发生了空闲中断
UART_RX_TypeDef *Data;//再定义一个这个结构体,包含的东西与uart_rx_data_t一致,我们要将uart_rx_data_t中的数据传到Data指针当中再一个一个传输
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清楚空闲中断标志,方便下次判断是否空闲中断
HAL_UART_DMAStop(&huart1 );//停止DMA传输,关闭DMA
uart_rx_data_t[uart_buff_ctrl].size = UART_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
//从DMA的结构体中找到这个数据长度(单片机接收的数据);注意这里的__HAL_DMA_GET_COUNTER函数计算的是未发送的数据长度,所以要用总长度UART_BUFFER_SIZE减一下
//实际上算出来的这个长度,不用管它具体是多少,减完之后肯定大于.size里实际传输的长度
//uart_rx_data_t[uart_buff_ctrl].size的意思是uart_rx_data_t这个结构体中的第uart_buff_ctrl个数组中的size项的值
Data = &uart_rx_data_t[uart_buff_ctrl];
//第一次传输时,uart_buff_ctrl = 0,传的是结构体中的第一个数组的意思,之后每次接收时uart_buff_ctrl都自加,分别传到不同的结构体数组当中,传到第五个时,uart_buff_ctrl又要重新取0,达到环形
//将这个Data指针地址发送至队列,让队列存一个地址
xQueueSendFromISR(uart_queueHandle,&Data,NULL);
uart_buff_ctrl++;//自加
uart_buff_ctrl %= UART_BUFFER_QUANTITY; //再取余,让它计到5时重新归零,当然这里也可以直接使用if语句
HAL_UART_Receive_DMA(&huart1 , uart_rx_data_t[uart_buff_ctrl].buffer,UART_BUFFER_SIZE);//重新开启DMA传输,传输buffer。这个函数在任务当中也要用到
}
}
创建一个DMA函数,一旦串口产生中断,则执行该函数,所以该函数要写在USART1_IRQHandler函数当中,一旦上位机发送信号触发,执行USART1_IRQHandler函数,即可执行到该函数;
if语句判断是否发生空闲中断,此时既然能进入该函数,一定已经发生了空闲中断;
UART_RX_TypeDef *Data;再定义一个这个结构体,包含的东西与uart_rx_data_t一致,我们要将uart_rx_data_t中的数据传到Data指针当中再一个一个传输。注意:刚刚结构体是在freertos.c中定义的,不是一个全局结构体,这里我们要在main.c中重新定义,操作与前面一样
清楚空闲中断标志,方便下次判断是否空闲中断;停止DMA传输,关闭DMA;
再定义一个结构体变量,uart_rx_data_t[];这是一个数组,里面共有UART_BUFFER_QUANTITY位,即5位,与我们的队列长度一致。
uart_rx_data_t 是一个数组,包含 UART_BUFFER_QUANTITY 个 UART_RX_TypeDef 结构体,每个结构体可以独立地存储一组 UART 接收数据
uint8_t uart_buff_ctrl = 0;定义一个 uart_buff_ctrl ,由于我们的结构体有5个,每次传输时只用得到一个,要想区分它们,需要有一个标志,我们用 uart_buff_ctrl作为标志,起始位为0,每传输一次 uart_buff_ctrl加一;
uart_rx_data_t[uart_buff_ctrl].size结构体中的size大小:
从DMA的结构体中找到这个数据长度(单片机接收的数据);注意这里的__HAL_DMA_GET_COUNTER函数计算的是未发送的数据长度,所以要用总长度UART_BUFFER_SIZE减一下;实际上算出来的这个长度,不用管它具体是多少,减完之后肯定大于.size里实际传输的长度;uart_rx_data_t[uart_buff_ctrl].size的意思是uart_rx_data_t这个结构体中的第uart_buff_ctrl个数组中的size项的值
Data = &uart_rx_data_t[uart_buff_ctrl];
第一次传输时,uart_buff_ctrl = 0,传的是结构体中的第一个数组的意思,之后每次接收时uart_buff_ctrl都自加,分别传到不同的结构体数组当中,传到第五个时,uart_buff_ctrl又要重新取0,达到环形;将这个Data指针地址发送至队列,让队列存一个地址
xQueueSendFromISR;将Data发送至队列;uart_buff_ctrl自加再取余,让它计到5时重新归零,当然这里也可以直接使用if语句;
HAL_UART_Receive_DMA();最后重启DMA传输;传输buffer。这个函数在任务当中也要用到,用作DMA开始传输的标志;
注意:在main.c中用到了很多其它文件的结构体如下所示,需要进行全局声明一下,否则Keil认不到这些结构体和函数;
xQueueSendFromISR();这个函数在queue.h中,用的时候要加一下头文件
USART1_DMAHandler()编写完毕,现在我们要去串口中断函数中加上该函数:
找到it.c文件,找到USART1_IRQHandler函数,该函数即中断执行函数,我们将我们自己写的函数写进去
注意:我们自己写的这个函数也是一个外来函数,也要在头部声明一下
接下来回到我们的任务;
我们需要开启中断(虽然外面已经配置过了开启USART中断,但是里面还需要写中断开始函数):
__HAL_UART_ENABLE_IT(&huart1 ,UART_IT_IDLE);
当然你也可以用上一节的HAL_UART_Receive_IT(&huart1,&存放地址,1) 这个函数
但是由于这里我们要用DMA传输,没有直接传输到地址,所以不用该函数;所以直接开启中断
HAL_UART_Receive_DMA();开启DMA传输,这个我们在刚刚写的函数最后也用到了,关闭了DMA还要再打开它,这里我们用在任务的开始,表示开始DMA的传输;
至此,代码编写完毕。
三、实际调试
我们将代码载入单片机,看看实际调试效果:
可以看到,我们发送123,单片机接收123,发送”我是发送“,单片机也能收到中文”我是发送“,发送英文字符串”woshifasong“,单片机也能收到;如果我们一次性发送又带中文又带数字又带英文的消息,单片机也能收到,并且一次性全部回显。
这是由于我们DMA传输的是一个指针地址,我们将数据全部放到了指针里面,队列传输时,传的也是地址,该地址中不仅包含了数据,还有数据的字节长度,所以在单片机回显的时候,可以直接引用指针中的数据buffer,也可以直接引用指针中的长度Size,完成DMA的传输。
以上就是利用USART串口通信基于DMA的接收数据功能,关于DMA的发送数据功能与接收功能大差不差,读者可以自行模仿修改。其实,实现代码一次性传输的是指针的作用,而DMA在这里只是起到了一个跳过CPU直接传输的作用。在串口通信时,也可以使用指针的方法,使数据完成一次性传输。
以上就是本小节的全部内容,如有错误,敬请雅正!
参考文献: