(三)stm32之串口通信DMA传输完成中断

一、DMA功能简介

  首先唠叨一下DMA的基本概念,DMA的出现大大减轻了CPU的工作量。在硬件系统中,主要由CPU(内核)、外设、内存(SRAM)、总线等结构组成,数据经常要在内存和外设之间,外设和外设之间转移。例如:CPU需要处理从外设采集回来的数据,CPU需要先将数据从ADC外设的寄存器读取到内存中(变量)去,然后进行运算处理,这是一般的解决方法。CPU的资源是非常宝贵的,我们可以设法把转移的工作交给其他部件来完成,CPU把更多的资源用于数据运算和中断响应上,如此DMA便登场了。DMA正是为CPU分担数据转移工作,因为DMA的存在,CPU才被解放出来,它可以在数据转移的同时进行数据运算,相应中断,大大提高了效率。

二、DMA的主要特性

  

三、DMA中断特性

四、DMA之串口通信

  我们实现一个简单的功能,在DMA中处理串口通信,把数据转移的工作交给DMA,DMA把数据从内存(数组)到外设(串口)的转移,在main函数中不断进行闪灯操作,这样我们可以看到DMA在工作的时候CPU也在工作。非常有必要复习一下DMA的对应关系,我们知道stm32总共有2个DMA控制器(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自一个或多个外设对存储器访问的请求,还有一个仲裁器来协调DMA请求的优先级(优先级分:很高、高、中等、低),这可不是随便对应的。

  

1、LED初始化程序如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void LED_GPIO_Config(void)
{      
        /*定义一个GPIO_InitTypeDef类型的结构体*/
        GPIO_InitTypeDef GPIO_InitStructure;
        /*开启LED的外设时钟*/
        RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);
        /*选择要控制的GPIOB引脚*/                                                             
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; 
        /*设置引脚模式为通用推挽输出*/
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  
        /*设置引脚速率为50MHz */  
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        /*调用库函数,初始化GPIOB0*/
        GPIO_Init(GPIOB, &GPIO_InitStructure);               
        /* 关闭所有led灯 */
        GPIO_SetBits(GPIOB, GPIO_Pin_14);
}

  这个地方地方没什么要注意的,唯一要注意的就是输入输出模式,我们按需求这样配就好了。

2、串口初始化

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void USART3_Config(void)
{
        GPIO_InitTypeDef GPIO_InitStructure;
        USART_InitTypeDef USART_InitStructure;     
        /* config USART3 clock */
        RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB , ENABLE);
        RCC_APB1PeriphClockCmd( RCC_APB1Periph_USART3, ENABLE);
        /* USART1 GPIO config */
        /* Configure USART1 Tx (PA.09) as alternate function push-pull */
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(GPIOB, &GPIO_InitStructure);   
        /* Configure USART1 Rx (PA.10) as input floating */
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
        GPIO_Init(GPIOB, &GPIO_InitStructure);         
        /* USART1 mode config */
        USART_InitStructure.USART_BaudRate = 38400;
        USART_InitStructure.USART_WordLength = USART_WordLength_8b;
        USART_InitStructure.USART_StopBits = USART_StopBits_1;
        USART_InitStructure.USART_Parity = USART_Parity_No ;
        USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
        USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
        USART_Init(USART3, &USART_InitStructure);
        USART_Cmd(USART3, ENABLE);
}

3、DMA初始化

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void USART3_DMA_Config(void)
{
    DMA_InitTypeDef DMA_InitStructure; 
    /*开启DMA时钟*/
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); 
    //NVIC_Config();                //配置DMA中断
    //NVIC_Configuration();
    /*设置DMA源:串口数据寄存器地址*/
    DMA_InitStructure.DMA_PeripheralBaseAddr = USART3_DR_Base;    
    /*内存地址(要传输的变量的指针)*/
    DMA_InitStructure.DMA_MemoryBaseAddr = (u32)SendBuff;
    /*方向:从内存到外设*/      
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; 
    /*传输大小DMA_BufferSize=SENDBUFF_SIZE*/   
    DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE;
    /*外设地址不增*/     
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    /*内存地址自增*/
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    /*外设数据单位*/ 
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    /*内存数据单位 8bit*/
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; 
    /*DMA模式:不断循环*/
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
    //DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;   
    /*优先级:中*/  
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; 
    /*禁止内存到内存的传输    */
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    /*配置DMA1的2通道*/        
    DMA_Init(DMA1_Channel2, &DMA_InitStructure);      
        //DMA_ITConfig(DMA1_Channel2,DMA_IT_TC,ENABLE);  //配置DMA发送完成后产生中断
    /*使能DMA*/
    DMA_Cmd (DMA1_Channel2,ENABLE);                    
}

在这里我们要注意以下几点:

(1)DMA_InitStructure.DMA_PeripheralBaseAddr = USART3_DR_Base;这里对应USART数据寄存器地址,这个地址我们是这样定义的:#define USART3_DR_Base  0x40004804,这个值是怎么算出来的呢?我们可以查看stm32存储器映射表:

USART3的起始地址是0x40004800,我们查看stm32串口数据寄存器偏移地址为0x04

因此我们可以计算到USART3数据寄存器地址为0x40004804

(2)我们数据传输方向内存(变量)到外设(串口),所以DMA方向为内存到外设

(3)DMA传输模式有两种:DMA_Mode_Normal(普通模式),DMA只传输一次;DMA_Mode_Circular(循环模式),DMA循环传输,比如在AD采集时要配置成循环模式。

4、主函数

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main(void)
{
        /* USART1 config 115200 8-N-1 */
        USART3_Config();   
        USART3_DMA_Config();
        LED_GPIO_Config(); 
        printf("\r\n usart3 DMA TX 测试 \r\n");  
        {
            uint16_t i;        
            /*填充将要发送的数据*/
            for(i=0;i<SENDBUFF_SIZE;i++)
            {
                SendBuff[i]  = 'A';
            }
        }      
        /* USART1 向 DMA发出TX请求 */
        USART_DMACmd(USART3, USART_DMAReq_Tx, ENABLE);
        /* 此时CPU是空闲的,可以干其他的事情 */   
        //例如同时控制LED
        for(;;)
        {
            LED1(ON);
            Delay(0xFFFFF);
            LED1(OFF);
            Delay(0xFFFFF);
        }
}

这个函数很简单,我们很容易就可以实现,达到效果,这里就不贴图片了。

五、串口通信DMA传输完成中断

  我们知道DMA可以在传输过半,传输完成,传输错误时产生中断。我们实现的功能是,DMA工作在普通模式下即只传输一次,LED灯初始化是关闭的,DMA传输完成后产生一个中断,在中断中我们做点灯操作。这个程序调了一天才调了出来,并不是因为它很难,而是有一些要注意的地方没有注意到,从而到时耽误了好长时间才调出来。不过有错误就会有进步嘛。

  我先贴出正确的代码,然后在讨论我犯的错误,由于和上一个程序好多都是一样的,这里我们只贴出不同的地方。

(1)DMA初始化程序:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void USART3_DMA_Config(void)
{
    DMA_InitTypeDef DMA_InitStructure; 
    /*开启DMA时钟*/
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); 
    //NVIC_Config();                //配置DMA中断
    NVIC_Configuration();
    /*设置DMA源:串口数据寄存器地址*/
    DMA_InitStructure.DMA_PeripheralBaseAddr = USART3_DR_Base;    
    /*内存地址(要传输的变量的指针)*/
    DMA_InitStructure.DMA_MemoryBaseAddr = (u32)SendBuff;
    /*方向:从内存到外设*/      
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; 
    /*传输大小DMA_BufferSize=SENDBUFF_SIZE*/   
    DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE;
    /*外设地址不增*/     
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    /*内存地址自增*/
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    /*外设数据单位*/ 
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    /*内存数据单位 8bit*/
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; 
    /*DMA模式:不断循环*/
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
    //DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;   
    /*优先级:中*/  
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; 
    /*禁止内存到内存的传输    */
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    /*配置DMA1的2通道*/        
    DMA_Init(DMA1_Channel2, &DMA_InitStructure);      
        DMA_ITConfig(DMA1_Channel2,DMA_IT_TC,ENABLE);  //配置DMA发送完成后产生中断
    /*使能DMA*/
    DMA_Cmd (DMA1_Channel2,ENABLE);            
         
}

注意我们在这里打开了DMA传输完成中断。

(2)NVIC初始化

?
1
2
3
4
5
6
7
8
9
10
static void NVIC_Configuration(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;     /* Configure one bit for preemption priority */
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);     
  NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel2_IRQn;     
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;          
  NVIC_Init(&NVIC_InitStructure);
}

(3)中断处理程序

我们在stm32f10x_it.c中编写我们的中断处理程序:

?
1
2
3
4
5
6
7
8
void DMA1_Channel2_IRQHandler(void)
{
    if(DMA_GetITStatus(DMA1_IT_TC2))
    {
        LED1(ON);
        DMA_ClearITPendingBit(DMA1_IT_GL2); //清除全部中断标志
    }
}

我们也可以这样写中断处理程序:

?
1
2
3
4
5
6
7
8
void DMA1_Channel2_IRQHandler(void)
{
    if(DMA_GetFlagStatus(DMA1_FLAG_TC2))
    {
        LED1(ON);
        DMA_ClearFLAG(DMA1_FLAG_TC2); //清除全部中断标志
    }
}

这两种写法都行,我们在库开发文档可以查看。都代表DMA的通道2传输完成中断。  

(4)主函数

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main(void)
{
        /* USART1 config 115200 8-N-1 */
        USART3_Config();   
        USART3_DMA_Config();   
        LED_GPIO_Config(); 
        printf("\r\n usart3 DMA TX 测试 \r\n");  
        {
            uint16_t i;        
            /*填充将要发送的数据*/
            for(i=0;i<SENDBUFF_SIZE;i++)
            {
                SendBuff[i]  = 'A';
            }
        }      
        /* USART1 向 DMA发出TX请求 */
        USART_DMACmd(USART3, USART_DMAReq_Tx, ENABLE);
        /* 此时CPU是空闲的,可以干其他的事情 */       
        //例如同时控制LED
        for(;;)
        {
        }
}

这样我们实验便可以看到,LED灯初始化是关闭的,当串口发送完40000字节的'A’后,LED等亮。

(5)补充

原意是测试DMA发送完成中断指的是每次指定字节发送完成后便产生一个中断还是最终都传输完成触发一次中断,刚开始中断处理函数写的程序如下:

?
1
2
3
4
5
6
7
8
9
10
11
void DMA1_Channel2_IRQHandler(void)
{   
    uint16_t n = 0;
    if(DMA_GetFlagStatus(DMA1_FLAG_TC2))   
    {  
        n = ~n;
        if(n) LED1(ON);
        else  LED1(OFF);
        DMA_ClearFlag(DMA1_FLAG_TC2); //清除全部中断标志
    }
}

通过测试,我发现LED灯并没有像试想的那样每次发送完成后便触发一次中断,然后灯会间隔闪烁,而实际是第一次传输完成后灯点亮,之后就一直保持亮的状态。刚开始我还以为DMA只会触发第一次中断,后来仔细分析后才发现了问题。正确的代码应该如下。

void DMA1_Channel2_IRQHandler(void)
{
    static uint16_t n = 0;
        if(DMA_GetFlagStatus(DMA1_FLAG_TC2))
       {
            n = ~n;
            if(n) LED1(ON);
            else  LED1(OFF);
            DMA_ClearFlag(DMA1_FLAG_TC2); //清除全部中断标志
        }
}

在这里n是一个局部变量,如果不定义成静态变量,每次出中断时后n所占的内存(栈)便会释放,这样再次进入后n还是会初始化为0.与我们要达到的效果不符。因此,在这里我们把它指定为静态变量,那么内存就不会释放,它会保持上一次的的值,修改之后达到了效果,每次传输完成3000个字节后灯的状态就会改变一次。  

在这里我们整理一下变量:

全局动态变量:作用范围为整个工程,不释放内存,会保持上一次的值。

全局静态变量:作用范围为当前文件,不释放内存,会保持上一次的值。

局部动态变量:作用范围为当前函数,每次函数执行结束释放内存,不会保持上一次的值。

局部静态变量:作用范围为当前函数,不释放内存,会保持上一次的值。

 

(0)

相关推荐

  • STM32F1系列之常用外设说明

    STM32F1系列之常用外设说明

  • STM32如何高效接收串口数据?

    目录 USART3_DR的地址 DMA的通道 DMA的中断 USART接收回调函数 头文件源码 DMA的基本配置 环形队列接收数据 函数原型 参考用例 总结 硬件:stm32f103cbt6 软件:S ...

  • 附源码-终极串口接收(二)

    来源:公众号[鱼鹰谈单片机] 作者:鱼鹰Osprey ID   :emOsprey 前段时间需要写个串口接收程序,一时没找到源码,就想着自己写过一篇文章<终极串口接收方式,极致效率>,看看 ...

  • STM32串口通信基本原理

    通信方式 并行通信 传输原理:数据各个位同时传输 优点:速度快(一个引脚传输一个位) 缺点:占用引脚资源多 串行通信 传输原理:数据按位顺序传输 优点:占用引脚资源少(一个引脚都可以) 缺点:速度相对 ...

  • STM32系统学习——USART(串口通信)

    串口通信是一种设备间非常常用的串行通行方式,其简单便捷,大部分电子设备都支持. 一.物理层 常用RS-232标准,主要规定了信号的用途.通信接口以及信号的电平标准. "DB9接口" ...

  • STM32串口通信配置(USART1+USART2+USART3+UART4)

    一.串口一的配置(初始化+中断配置+中断接收函数) 1 /*====================================================================== ...

  • EEDrone开源四旋翼从零开始(8)——串口无中断DMA传输

    上期回顾:EEDrone开源四旋翼从零开始(7)--第一版硬件设计下面从操作系统层面上进行解释,在FreeRTOSConfig.h中有两个重要的中断配置:configKERNEL_INTERRUPT_ ...

  • 关于串口通信(232、485、422)和常见问题,一篇文章就给你说清楚

    发现更多电气知识 电气达人 电气达人 电气人择一业,终一生! 13篇原创内容 公众号 下面先讲串口通信的一些基本概念,术语.如果对串口通信比较熟悉的,就当复习,如果哪里讲的不到位,欢迎及时指出. 这里 ...

  • 温习串口通信(232,485,422)以及常见问题,心得体会等

    关于启程 启程自动化培训成立于2012年,培训项目主打西门子PLC编程系统集成.工业机器人编程.EPLAN电气设计.视觉等培训与技术服务的机构.推荐高薪就业,越努力越幸运 下面先讲一讲串口通信的一些基 ...

  • 单片机数据通信怎么学?这个工具要用好:串口通信

    刚开始学单片机的你,是不是会因用程序把LED点亮而感到高兴,会因用程序把数码管点亮而感到高兴.这是好事,这也是想继续学习下去的动力. 但是到了与数据相关的实验时,却感觉很难有所进步.有时候,把驱动写好 ...

  • 串口通信(232,485,422)以及常见问题

    下面先讲一讲串口通信的一些基本概念,术语.如果对串口通信比较熟悉的,就当复习复习,如果哪里讲错或不到位,也可以及时指出,当作一块交流交流.   这里并不对串口的编程作讲解,主要是从应用的角度去讲一讲. ...

  • 串口通信没那么难,看完这篇文章就懂了!

    技成培训 技成培训网是一家致力于制造业远程教育品牌.专注14年,专业课程涵盖了电工基础.PLC.变频器.伺服.人机界面.机械制图.数控.机器人等精品课程,利用全新线上模式,打造出制造业线上高端互动学习 ...