【STM32开发之寄存器版】(十一)-DMA和串口空闲中断实现不定长数据接收
一、引言
在前文 【STM32开发之寄存器版】(二)-USART 中我们介绍了如何使用 接收缓冲区非空中断 实现串口数据的接收。但从本质上讲,该过程是以CPU为主导的数据搬运过程,如此简单重复的工作是对CPU资源的极大浪费。本文将在STM32F103ZET6单片机上,利用DMA和串口空闲中断,实现串口不定长数据的接收。
二、STM32-DMA介绍
2.1 DMA简介
直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。
STM32F103ZET6有2个DMA控制器。两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。
DMA控制器和Cortex™-M3核心共享系统数据总线,执行直接存储器数据传输。当CPU和DMA 同时访问相同的目标(RAM或外设)时,DMA请求会暂停CPU访问系统总线达若干个周期,总线仲裁器执行循环调度,以保证CPU至少可以得到一半的系统总线(存储器或外设)带宽。
2.2 DMA主要特性
STM32F103的DMA主要由以下特性:
- 12个独立的可配置的通道(请求):DMA1有7个通道,DMA2有5个通道
- 每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置。
- 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推) 。
- 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。
- 支持循环的缓冲器管理
- 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求。
- 存储器和存储器间的传输
- 外设和存储器、存储器和外设之间的传输
- 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标。
- 可编程的数据传输数目:最大为65535
2.3 DMA请求通道
STM32F103ZET6的DMA1通道请求如下:
STM32F103ZET6的DMA2通道请求如下:
本文使用STM32的USART1,故仅需要使用到DMA1的通道4和通道5.
三、串口+DMA+空闲中断实现不定长数据接收逻辑
串口+DMA+空闲中断实现不定长数据接收的核心在于DMA和串口空闲中断的搭配。
对于串口空闲中断的理解
STM32的空闲中断在一直没有数据到来时是不会触发的,必须从接收到一个数据开始,直到数据断流,之后再也没有数据,IDLE空闲中断才会产生。
对于DMA参与搬运的理解
DMA不断从串口外设数据寄存器(USARTx_DR)搬运数据到指定数据缓存空间(从RAM划分出来,可以自己定义)。
串口空闲中断与DMA的搭配逻辑
- 在DMA初始化好后,将一直处于准备搬运数据的状态,一旦串口外设数据寄存器中有数据,DMA立即将其搬运到指定缓存。此过程无需CPU参与。
- 从接收到第一个数据开始直至数据断流,DMA已经自主将串口接收到的数据搬运到指定缓存,此时USART产生IDLE空闲中断,在中断中处理搬运好的缓存数据。
- 在空闲中断处理函数中,为了防止后来的数据干扰,需要先关闭DMA通道。(可能有问题,后文说)。
- 在空闲中断处理函数中,清除DMA标志位,并获得接收到的字节数,串口回环打印接收到的数据至上位机XCOM。
- 在空闲中断处理函数中,重新设置DMA下次要接收到的数据字节数。
- 在空闲中断处理函数中,给出信号量,供前台程序查询。
- 在空闲中断处理函数中,开启DMA通道,等待下一次数据接收。
四、时钟树分析
对于USART1的时钟树分析详见 【STM32开发之寄存器版】(二)-USART ,本文不再赘述,下面分析DMA1的时钟树。由于DMA1时钟直接来源于AHB分频器,故本文选择DMA1时钟为72MHz(直接使能AHB时钟即可)。具体时钟树配置如下所示:
五、寄存器介绍
本DEMO主要涉及以下寄存器(有关USART的大部分寄存器设置详见 【STM32开发之寄存器版】(二)-USART ):
寄存器 | 功能 |
---|---|
USART1_CR1 | USART1控制寄存器1 |
USART1_CR3 | USART1控制寄存器3 |
USART1_SR | USART1状态寄存器 |
DMA_CAPRx | DMA通道x外设地址寄存器 |
DMA_CMARx | DMA通道x存储器地址寄存器 |
DMA_CCRx | DMA通道x配置寄存器 |
DMA_CNDTRx | DMA通道x传输数量寄存器 |
DMA_IFCR | DMA中断标志清除寄存器 |
下面对这些寄存器进行一一介绍。
5.1 USART1_CR1控制寄存器1
《STM32中文参考手册》对USART1_CR1寄存器的描述如下:
我们仅关注[13]UE和[4]IDLEIE:
[13]UE USART使能:本文置1使能USART模块。
[4]IDLEIE IDLE中断使能:本文置1,当USART_SR中的IDLE为'1'时,产生USART中断。
5.2 USART1_CR3控制寄存器3
《STM32中文参考手册》对USART1_CR3寄存器的描述如下:
我们仅关注[6]DMAR:
[6]DMAR DMA使能接收:本文置1 使能接收时的DMA模式。
5.3 USART1_SR状态寄存器
《STM32中文参考手册》对USART1_SR寄存器的描述如下:
我们仅关注[4]IDLE:
[4]IDLE:先读USART_SR,当该位为1时,检测到空闲总线,然后再读USART_DR清除该位。
5.4 DMA_CAPRx外设地址寄存器
《STM32中文参考手册》对DMA_CAPRx寄存器的描述如下:
该寄存器存放的是外设地址寄存器,本文设置为串口的数据寄存器地址&USART1_DR。
5.5 DMA_CMARx存储器地址寄存器
《STM32中文参考手册》对DMA_CMARx寄存器的描述如下:
该寄存器存放的是存储器地址寄存器,本文设置为自定义的数据缓存地址RX_Buff。
5.6 DMA_CCRx配置寄存器
《STM32中文参考手册》对DMA_CCRx寄存器的描述如下:
该寄存器功能较多,我们需要关注以下寄存器:
[14]MEM2MEN存储器到存储器模式:本文置0,非存储器到存储器模式。
[13:12]PL通道优先级:本文置11,最高优先级。
[11:10]MSIZE存储器数据宽度:本文置00,8位。
[9:8]PSIZE外设数据宽度:本文置00,8位。
[7]MINC存储器地址增量模式:本文置1,执行存储器地址增量操作。
[6]PINC外设地址增量模式:本文置0,不执行外设地址增量操作。
[5]CIRC循环模式:本文置1,执行循环操作。
[4]DIR数据传输方向:本文置0,从外设读。
[3]TEIE允许传输错误中断:本文置1,允许TE中断。
[0]EN通道开启:本文置1,通道开启。
5.7 DMA_CNDTRx传输数量寄存器
《STM32中文参考手册》对DMA_CNDTRx寄存器的描述如下:
我们仅关注其[15:0],表示的是数据传输数量,数据传输量为0-65535.
5.8 DMA_IFCR中断标志清除寄存器
《STM32中文参考手册》对DMA_IFCR寄存器的描述如下:
我们仅关注其[12]CGIF4,因为USART1_RX对应的是DMA1_Channel4,因此需要将[12]CGIF4置1清除DMA相应标志。
六、程序设计
本DEMO程序主要包括USART1初始化、USART1的DMA配置、USART1接收数据处理函数、USART1中断处理函数。下面对这几个函数进行一一剖析。
6.1 USART1初始化
该部分程序位于SYSTEM/usart.c/uart_dma_init(),该函数的主要作用是设置USART1时钟、配置IO复用、设置USART1参数,使能串口空闲中断以及DMA传输。具体代码如下所示:
void uart_dma_init(u32 pclk2,u32 bound)
{
float temp;
u16 mantissa;
u16 fraction;
/* 得到USARTDIV */
temp=(float)(pclk2*1000000)/(bound*16);
/* 得到整数部分 */
mantissa=temp;
/* 得到小数部分 */
fraction=(temp-mantissa)*16;
/* 计算得到波特率寄存器值 */
mantissa<<=4;
mantissa+=fraction;
/* 使能PORTA口时钟 */
RCC->APB2ENR|=1<<2;
/* 使能串口时钟 */
RCC->APB2ENR|=1<<14;
/* IO状态设置 */
GPIOA->CRH&=0XFFFFF00F;
/* IO状态设置 */
GPIOA->CRH|=0X000008B0;
/* 复位串口1 */
RCC->APB2RSTR|=1<<14;
/* 停止复位 */
RCC->APB2RSTR&=~(1<<14);
/* 波特率设置 */
USART1->BRR=mantissa;
/* 1位停止,无校验位.TX使能,RX使能 */
USART1->CR1|=0X000C;
/* 组2,最低优先级 */
MY_NVIC_Init(3,3,USART1_IRQn,2);
#if USE_USART_DMA_RX
/* 开启串口空闲中断 */
USART1->CR1 |= (1<<4);
/* 开启串口DMA接收 */
USART1->CR3 |= (1<<6);
/* 使能串口接收DMA */
USART1_DMA_Rx_Config();
#endif
/* USART1使能 */
USART1->CR1|=0X2000;
}
6.2 USART1的DMA配置
该部分程序位于SYSTEM/usart.c/USART1_DMA_Rx_Config(),该函数主要功能是配置DMA传输的源地址和目标地址、传输数据大小、仲裁优先级并使能DMA。具体代码如下所示:
static void USART1_DMA_Rx_Config(void){
/* 开启DMA1时钟 */
RCC->AHBENR|=1<<0;
/* 等待DMA1时钟稳定 */
delay_ms(5);
/* 设置DMA源地址:串口数据寄存器地址 */
DMA1_Channel5->CPAR=(u32)&USART1->DR;
/* 内存地址(要传输的变量的指针) */
DMA1_Channel5->CMAR=(u32)RX_Buff;
/* 方向:从外设到内存 */
DMA1_Channel5->CCR &= ~(1<<4);
/* 传输大小 */
DMA1_Channel5->CNDTR = (u16)RX_BUF_SIZE;
/* 外设地址不增 */
DMA1_Channel5->CCR &= ~(1<<6);
/* 内存地址自增*/
DMA1_Channel5->CCR |= 1<<7;
/* 外设数据单位 */
DMA1_Channel5->CCR &= ~(1<<9);
DMA1_Channel5->CCR &= ~(1<<8);
/* 内存数据单位 */
DMA1_Channel5->CCR &= ~(1<<11);
DMA1_Channel5->CCR &= ~(1<<10);
/* DMA模式,循环模式 */
DMA1_Channel5->CCR |= 1<<5;
/* 优先级:最高 */
DMA1_Channel5->CCR |= 1<<13;
DMA1_Channel5->CCR |= 1<<12;
/* 禁止内存到内存的传输 */
DMA1_Channel5->CCR &= ~(1<<14);
/* 清除DMA所有标志 */
DMA1->IFCR = (uint32_t)0x00020000;
/* 允许传输错误中断 */
DMA1_Channel5->CCR |= 1<<3;
/* 使能DMA */
DMA1_Channel5->CCR |= 1<<0;
}
6.3 USART1接收数据处理函数
该部分程序位于SYSTEM/usart.c/Receive_DataPack(),该部分功能是关闭DMA、获取数据长度、回环显示接收到的数据、清除DMA标志位并重新打开DMA。具体代码如下所示:
void Receive_DataPack(void){
/* 接收的数据长度 */
uint32_t buff_length;
/* 关闭DMA ,防止干扰 */
DMA1_Channel5->CCR &= ~(1<<0);
/* 获取接收到的数据长度 单位为字节*/
buff_length = RX_BUF_SIZE - DMA1_Channel5->CNDTR;
printf("buff_length = %d\r\n ",buff_length);
/*将数组改成字符串*/
RX_Buff[buff_length] = '\0';
/*打印发送的数据*/
printf("您发送的数据是:\r\n");
printf("%s\r\n",RX_Buff);
/* 清DMA标志位 */
DMA1->IFCR = (uint32_t)0x00020000;
/* 重新赋值计数值,必须大于等于最大可能接收到的数据帧数目 */
DMA1_Channel5->CNDTR = RX_BUF_SIZE;
/* 此处应该在处理完数据再打开*/
DMA1_Channel5->CCR |= 1<<0;
}
6.4 USART1中断处理函数
该部分程序位于SYSTEM/usart.c/USART1_IRQHandler(),该部分功能主要是响应串口空闲中断,接收数据并清除串口空闲中断。具体代码如下所示:
void USART1_IRQHandler(void)
{
#if USE_USART_DMA_RX
if(((USART1->SR)>>4)&&0X0001 == 1){
/* 接收数据包 */
Receive_DataPack();
/* 清除IDLE中断标志位 */
USART1->DR;
}
#endif
}
七、上机实验
将代码烧录进STM32F103ZET6,使用上位机XCOM进行回环测试,实验效果如下:
测试成功!至此完成本次DEMO。
八、一些思考
实测过程中遇到的一些问题:
第三部分我们说过,在空闲中断处理函数中,为了防止后来的数据干扰,需要先关闭DMA通道。但是中断处理函数中关闭DMA通道后,USART_DR中仍然可能出现一些后来的数据,这些数据被程序默认丢弃。实测发现,在波特率3000000bps,传输数据长度大于255(我设置的数据buffer大小是255)时出现丢包情况。
我在网上查到一些资料供大家参考优化:
- 在重新开启接收DMA通道之前,将Rx_Buf缓冲区里面的数据复制到另外一个数组中,然后再开启DMA,然后马上处理复制出来的数据。
- 建立双缓冲,重新配置DMA_MemoryBaseAddr的缓冲区地址,那么下次接收到的数据就会保存到新的缓冲区中,不至于被覆盖。
九、代码共享
本文代码放在这里,供大家交流!
原文地址:https://blog.csdn.net/Jlinkneeder/article/details/142958041
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!