(三)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个字节后灯的状态就会改变一次。
在这里我们整理一下变量:
全局动态变量:作用范围为整个工程,不释放内存,会保持上一次的值。
全局静态变量:作用范围为当前文件,不释放内存,会保持上一次的值。
局部动态变量:作用范围为当前函数,每次函数执行结束释放内存,不会保持上一次的值。
局部静态变量:作用范围为当前函数,不释放内存,会保持上一次的值。