婚庆网站开发目的,律师资格证报考条件,网架公司股价,网址导航推广1. DMA初始化结构体详解
标准库函数对每个外设都建立了一个初始化结构体xxx_InitTypeDef(xxx为外设名称)#xff0c;结构体成员用于设置外设工作参数#xff0c; 并由标准库函数xxx_Init()调用这些设定参数进入设置外设相应的寄存器#xff0c;达到配置外设工作环境的目的。… 1. DMA初始化结构体详解
标准库函数对每个外设都建立了一个初始化结构体xxx_InitTypeDef(xxx为外设名称)结构体成员用于设置外设工作参数 并由标准库函数xxx_Init()调用这些设定参数进入设置外设相应的寄存器达到配置外设工作环境的目的。 结构体xxx_InitTypeDef和库函数xxx_Init配合使用是标准库精髓所在理解了结构体xxx_InitTypeDef每个成员意义基本上就可以对该外设运用自如。 结构体xxx_InitTypeDef定义在stm32f10x_xxx.h(后面xxx为外设名称)文件中库函数xxx_Init定义在stm32f10x_xxx.c文件中 编程时我们可以结合这两个文件内注释使用。 DMA_ InitTypeDef初始化结构体 typedef struct { uint32_t DMA_PeripheralBaseAddr; // 外设地址 uint32_t DMA_MemoryBaseAddr; // 存储器地址 uint32_t DMA_DIR; // 传输方向 uint32_t DMA_BufferSize; // 传输数目 uint32_t DMA_PeripheralInc; // 外设地址增量模式 uint32_t DMA_MemoryInc; // 存储器地址增量模式 uint32_t DMA_PeripheralDataSize; // 外设数据宽度 uint32_t DMA_MemoryDataSize; // 存储器数据宽度 uint32_t DMA_Mode; // 模式选择 uint32_t DMA_Priority; // 通道优先级 uint32_t DMA_M2M; // 存储器到存储器模式 } DMA_InitTypeDef;
1) DMA_PeripheralBaseAddr 外设地址设定DMA_CPAR寄存器的值一般设置为外设的数据寄存器地址如果是存储器到存储器模式则设置为其中一个存储器地址。2) DMA_Memory0BaseAddr 存储器地址设定DMA_CMAR寄存器值一般设置为我们自定义存储区的首地址。3) DMA_DIR 传输方向选择可选外设到存储器、存储器到外设。它设定DMA_CCR寄存器的DIR[1:0]位的值。这里并没有存储器到存储器的方向选择当使用存储器到存储器时只需要把其中一个存储器当作外设使用即可。4) DMA_BufferSize 设定待传输数据数目初始化设定DMA_CNDTR寄存器的值单位是字节数最大值为2^16。 5) DMA_PeripheralInc 如果配置为DMA_PeripheralInc_Enable使能外设地址自动递增功能它设定DMA_CCR寄存器的PINC位的值一般外设都是只有一个数据寄存器所以一般不会使能该位。6) DMA_MemoryInc 如果配置为DMA_MemoryInc_Enable使能存储器地址自动递增功能它设定DMA_CCR寄存器的MINC位的值自定义的存储区一般都是存放多个数据的所以要使能存储器地址自动递增功能。7) DMA_PeripheralDataSize 外设数据宽度可选字节(8位)、半字(16位)和字(32位)它设定DMA_CCR寄存器的PSIZE[1:0]位的值。8) DMA_MemoryDataSize 存储器数据宽度可选字节(8位)、半字(16位)和字(32位)它设定DMA_CCR寄存器的MSIZE[1:0]位的值。当外设和存储器之间传数据时两边的数据宽度应该设置为一致大小。9) DMA_Mode DMA传输模式选择可选一次传输或者循环传输它设定DMA_CCR寄存器的CIRC位的值。例程我们的ADC采集是持续循环进行的所以使用循环传输模式。10) DMA_Priority 软件设置通道的优先级有4个可选优先级分别为非常高、高、中和低它设定DMA_CCR寄存器的PL[1:0]位的值。DMA通道优先级只有在多个DMA通道同时使用时才有意义如果是单个通道优先级可以随便设置。11) DMA_M2M 存储器到存储器模式使用存储器到存储器时用到设定DMA_CCR的位14 MEN2MEN即可启动存储器到存储器模式。 2. DMA数据转运 初始化的步骤我们还是看一下PPT的这个基本结构图。 第一步RCC开启DMA的时钟第二步直接调用DMA_Init初始化这里各个参数包括外设和存储器站点的起始地址数据宽度地址是否自增、方向、传输计数器、是否需要自动重装选择触发源、通道优先级那这所有的参数通过一个结构体就可以配置好了。第三步就可以进行开关控制调用DMA_Cmd函数给指定的通道使能就完成了。在这里如果你选择的是硬件触发不要忘了在对应的外设调用一下XXX_DMACmd开启一下触发信号的输出如果你需要DMA的中断那就调用DMA_ITConfig开启中断输出再在NVIC里配置相应的中断通道然后写中断函数就行了。中断的配置各个外设都一样上面的结构图暂时没有画中断的部分。第四步在运行的过程中如果转运完成传输计数器清0了。这时想再给传输计数器赋值的话就DMA失能、写传输计数器、DMA使能这样就行了这就是dma的编程思路。 DMA库函数 //恢复缺省配置 void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx); //初始化 void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct); //结构体初始化 void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct); //使能 void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState); //中断输出使能 void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState); //设置当前计数器给传输计数器写数据的 void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); //获取当前数据寄存器是返回传输计数器的值 uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx); //获取标志位状态 FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG); //清除标志位 void DMA_ClearFlag(uint32_t DMAy_FLAG); //获取中断状态 ITStatus DMA_GetITStatus(uint32_t DMAy_IT); //清除中断挂起位 void DMA_ClearITPendingBit(uint32_t DMAy_IT); 初始化第一步RCC开启DMA的时钟 注意这里开启DMA时钟的时候根据型号不同开启时钟参数也不同 字节uint8_t 半字uint16_t 字uint32_t 第一个是外设站点为DST目的地传输方向是存储器站点到外设站点 第二个是外设站点为SRC源端传输方向是 外设站点到存储器站点 打算把DataA放在外设站点把DataB放在存储器站点传输方向是 外设站点到存储器站点 第一个是循环模式就是传输计数器自动重装。 第二个是正常模式就是传输计数器不自动重装自减到0后停下来。 这里我们转运数组是存储器到存储器的传输所以选正常模式。因为循环模式不能应用于存储器到存储器的情况因为自动重装和软件触发不能同时使用如果同时使用DMA就会连续触发永远也不会停下来。 第一个M2M_Enable就是使用软件触发。
第二个M2M_Disable, 就是不使用软件触发也就是使用硬件触发。
MyDMA.c #include stm32f10x.h // Device header uint16_t MyDMA_Size; //定义全局变量用于Init函数的Size供Transfer函数使用 /** *函数:DMA初始化 *参数AddrA 源数组的首地址 *参数AddrB 目的数组的首地址 *参数size转运的数据大小 */ void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size) //对于SRAM的数组它的地址是编译器分配的并不是固定的是通过数组名来获取地址这里把这个地址提取成初始化函数的参数。这样在初始化的时候你相转运那个数组就把哪个数组的地址传进来就行。 { MyDMA_Size Size; //初始化时把size往全局变量也存一份 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启时钟DMA是AHB总线的设备 DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr AddrA; //外设站点的起始地址 DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; //外设站点的数据宽度。 以字节的方式传输 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Enable; //外设站点的地址是否自增 。 地址自增 DMA_InitStructure.DMA_MemoryBaseAddr AddrB; //存储器站点的起始地址。 给定形参AddrB DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; //存储器站点的数据宽度。 以字节的方式传输 DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; //存储器站点的地址是否自增。 地址自增 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; //传输方向。 SRC外设站点为数据源外设站点到存储器站点 DMA_InitStructure.DMA_BufferSize Size; //缓存区大小其实就是配置传输计数器指定传输几次 DMA_InitStructure.DMA_Mode DMA_Mode_Normal; //传输模式其实是选择是否使用自动重装。 这里我们转运数组是存储器到存储器的传输所以选正常模式。循环模式不能应用于存储器到存储器的情况因为自动重装和软件触发不能同时使用如果同时使用DMA就会连续触发永远也不会停下来。所以DMA_M2M_Enable和DMA_Mode_Circular不能同时配置。 DMA_InitStructure.DMA_M2M DMA_M2M_Enable; //选择是否其实是选择软件触发还是硬件触发这里是存储器到存储器转运对应使用软件触发 DMA_InitStructure.DMA_Priority DMA_Priority_Medium; //指定通道的软件优先级选择中等优先级 DMA_Init(DMA1_Channel1, DMA_InitStructure); //初始化 把结构体指定的参数配置到DMA1的通道1 //转运有三个条件第一个传输计数器大于0。第二个触发源有触发信号。第三个DMA使能 DMA_Cmd(DMA1_Channel1, DISABLE); //DMA使能 这里不给使能初始化后不会立刻工作等后续调用Transfer函数再进行转运 } void MyDMA_Transfer(void) //DMA传输函数调用一次函数就再次启动DMA { DMA_Cmd(DMA1_Channel1, DISABLE); //DMA先失能在写入传输计数器之前需要DMA暂停工作 DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size); //写入传输计数器指定将要转运的次数 DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能开始工作 while (DMA_GetFlagStatus(DMA1_FLAG_TC1) RESET); //等待DMA工作完成 DMA_ClearFlag(DMA1_FLAG_TC1); //清除工作完成标志 } MyDMA.h #ifndef __MYDMA_H #define __MYDMA_H void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size); void MyDMA_Transfer(void); #endif Main.c #include stm32f10x.h // Device header #include Delay.h #include OLED.h #include MyDMA.h //uint8_t aa 0x66; //OLED_ShowHexNum(1, 1, (uint32_t)aa,8); //显示地址的话必须强制类型转换为数字才可以把地址当成一个数字显示不然编译会报警告显示指针跨级了 uint8_t DataA[] {0x01, 0x02, 0x03, 0x04}; //定义测试数组DataA为数据源 uint8_t DataB[] {0, 0, 0, 0}; //定义测试数组DataB为数据目的地 int main(void) { /*模块初始化*/ OLED_Init(); //OLED初始化 MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4); //DMA初始化把源数组和目的数组的地址传入 /*显示静态字符串*/ OLED_ShowString(1, 1, DataA); OLED_ShowString(3, 1, DataB); /*显示数组的首地址*/ OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8); OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8); while (1) { DataA[0] ; //变换测试数据 DataA[1] ; DataA[2] ; DataA[3] ; OLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataA OLED_ShowHexNum(2, 4, DataA[1], 2); OLED_ShowHexNum(2, 7, DataA[2], 2); OLED_ShowHexNum(2, 10, DataA[3], 2); OLED_ShowHexNum(4, 1, DataB[0], 2); //显示数组DataB OLED_ShowHexNum(4, 4, DataB[1], 2); OLED_ShowHexNum(4, 7, DataB[2], 2); OLED_ShowHexNum(4, 10, DataB[3], 2); Delay_ms(1000); //延时1s观察转运前的现象 MyDMA_Transfer(); //使用DMA转运数组从DataA转运到DataB OLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataA OLED_ShowHexNum(2, 4, DataA[1], 2); OLED_ShowHexNum(2, 7, DataA[2], 2); OLED_ShowHexNum(2, 10, DataA[3], 2); OLED_ShowHexNum(4, 1, DataB[0], 2); //显示数组DataB OLED_ShowHexNum(4, 4, DataB[1], 2); OLED_ShowHexNum(4, 7, DataB[2], 2); OLED_ShowHexNum(4, 10, DataB[3], 2); Delay_ms(1000); //延时1s观察转运后的现象 } } 效果 可以看到临时变量的地址是存在SRAM中的。且DataA数据成功转运到DataB数组中 3.DMAAD多通道 和AD多通道接法一样。PA0接一个电位器PA1~PA3接三个传感器模块的AO输出 用ADC的扫描模式来实现多通道采集然后使用DMA来进行数据转运。
ADC单次扫描DMA单次转运
ADDMA.c #include stm32f10x.h // Device header uint16_t AD_Value[4]; void AD_Init(void) { //开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA1的时钟 //设置ADC时钟 RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频 //GPIO初始化 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); //规则组通道配置配置4个通道到顾泽序列组中 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //通道0放在序列1每个通道和引脚关系是一一对应的引脚定义表中规定死的。 开始点菜操作 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //通道1放在序列2 ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //通道2放在序列3 ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //通道3放在序列4 //ADC初始化 ADC_InitTypeDef ADC_InitStructure; //定义结构体变量 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; //选择独立模式单独使用ADC1 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right;//数据对齐选择右对齐 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; //外部触发使用软件触发 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; //连续转换每转换一次后立刻开始下一次转换 ADC_InitStructure.ADC_ScanConvMode ENABLE; //扫描模式多通道 ADC_InitStructure.ADC_NbrOfChannel 4; //通道数为4扫描规则组的前4个通道 ADC_Init(ADC1, ADC_InitStructure); //DMA初始化 DMA_InitTypeDef DMA_InitStructure; //定义结构体变量 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; //外设基地址源头地址ADC-DR存的是数据寄存器的内容这里参数是数据寄存器的地址 DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; //外设数据宽度半字对应16位的ADC数据寄存器 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; //外设地址自增选择失能以ADC数据寄存器为源 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)AD_Value; //存储器基地址端菜的目的地 。给定存放AD转换结果的全局数组AD_Value把数据AD_Value作为目的地 DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; //存储器数据宽度半字对应16位的ADC数据寄存器 DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; //存储器地址自增选择使能每次转运后数组移到下一个位置 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; //传输方向由外设到存储器 DMA_InitStructure.DMA_BufferSize 4; //4个ADC通道需要传输4次DMA转运的第一个条件满足 DMA_InitStructure.DMA_Mode DMA_Mode_Circular; //循环模式即选择使用自动重装 DMA_InitStructure.DMA_M2M DMA_M2M_Disable; //存储器到存储器选择失能数据由ADC外设触发到存储器使用硬件触发 DMA_InitStructure.DMA_Priority DMA_Priority_Medium; //优先级选择中等 DMA_Init(DMA1_Channel1, DMA_InitStructure); //ADC1的硬件触发只接在了DMA1的通道1上所以这里通道必须要使用DMA的通道1其他的通道都不行 //DMA和ADC使能 DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能DMA转运的第三条件满足 //但是DMA转运的第二个条件触发源有信号目前是不满足的因为这里是硬件触发ADC还没启动不会有触发信号所以这里DMA使能之后不会立刻工作。 //上图中有三个硬件触发源具体使用哪个取决于把哪个的DMA输出给开启了ADC_DMACmd函数就是用来开启ADC到DMA的传输的定时器也是类似的 ADC_DMACmd(ADC1, ENABLE); //开启DMA触发信号的 开启ADC到DMA输出 ADC_Cmd(ADC1, ENABLE); //ADC使能 //ADC校准 固定流程内部有电路会自动执行校准 ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1) SET); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1) SET); } /** * 函 数获取AD转换的值 * 参 数无 * 返 回 值无 */ void AD_GetValue(void) { DMA_Cmd(DMA1_Channel1, DISABLE); //DMA先失能在写入传输计数器之前需要DMA暂停工作 DMA_SetCurrDataCounter(DMA1_Channel1, 4); //写入传输计数器指定将要转运的次数 DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能开始工作 ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发AD转换一次等待ADC和DMA转换完成 while (DMA_GetFlagStatus(DMA1_FLAG_TC1) RESET); //因为转运总是在转换之后的等待DMA工作完成即可等待ADC转换完成的代码就不需要了 DMA_ClearFlag(DMA1_FLAG_TC1); //清除工作完成标志 } ADDMA.h #ifndef __AD_H #define __AD_H extern uint16_t AD_Value[4]; void AD_Init(void); void AD_GetValue(void) #endif Main.c #include stm32f10x.h // Device header #include Delay.h #include OLED.h #include AD.h int main(void) { OLED_Init(); AD_Init(); OLED_ShowString(1, 1, AD0:); OLED_ShowString(2, 1, AD1:); OLED_ShowString(3, 1, AD2:); OLED_ShowString(4, 1, AD3:); while (1) { AD_GetValue(); OLED_ShowNum(1, 5, AD_Value[0], 4); //AD_Value[0]是通道0的转换结果 OLED_ShowNum(2, 5, AD_Value[1], 4); OLED_ShowNum(3, 5, AD_Value[2], 4); OLED_ShowNum(4, 5, AD_Value[3], 4); Delay_ms(100); } } 这样当我们调用一下AD_GetValue函数ADC开始转换连续扫描四个通道DMA也同步进行转运。ADC转换结果依次存放在这上面的AD_Value数组里。
在主循环里先调用AD_GetValue无参无返回值之后数据就直接跑到AD_Value数组里了。AD_Value[0]是第一个序列通道0的转换结果。AD_Value[1]是通道1的转换结果AD_Value[2]是通道2的转换结果AD_Value[3]是通道3的转换结果。下载看一下这里四个通道的转换结果就出来了。
这就是ADC单次扫描加DMA单次转运的模式那我们还可以配置成ADC连续扫描加DMA循环转运的模式,这样代码就会更加方便。
ADC连续扫描DMA循环转运。
ADDMA.c #include stm32f10x.h // Device header uint16_t AD_Value[4]; void AD_Init(void) { //开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA1的时钟 //设置ADC时钟 RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频 //GPIO初始化 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); //规则组通道配置配置4个通道到顾泽序列组中 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //通道0放在序列1每个通道和引脚关系是一一对应的引脚定义表中规定死的。 开始点菜操作 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //通道1放在序列2 ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //通道2放在序列3 ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //通道3放在序列4 //ADC初始化 ADC_InitTypeDef ADC_InitStructure; //定义结构体变量 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; //选择独立模式单独使用ADC1 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right;//数据对齐选择右对齐 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; //外部触发使用软件触发 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; //连续转换每转换一次后立刻开始下一次转换 ADC_InitStructure.ADC_ScanConvMode ENABLE; //扫描模式多通道 ADC_InitStructure.ADC_NbrOfChannel 4; //通道数为4扫描规则组的前4个通道 ADC_Init(ADC1, ADC_InitStructure); //DMA初始化 DMA_InitTypeDef DMA_InitStructure; //定义结构体变量 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; //外设基地址源头地址ADC-DR存的是数据寄存器的内容这里参数是数据寄存器的地址 DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; //外设数据宽度半字对应16位的ADC数据寄存器 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; //外设地址自增选择失能以ADC数据寄存器为源 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)AD_Value; //存储器基地址端菜的目的地 。给定存放AD转换结果的全局数组AD_Value把数据AD_Value作为目的地 DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; //存储器数据宽度半字对应16位的ADC数据寄存器 DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; //存储器地址自增选择使能每次转运后数组移到下一个位置 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; //传输方向由外设到存储器 DMA_InitStructure.DMA_BufferSize 4; //4个ADC通道需要传输4次DMA转运的第一个条件满足 DMA_InitStructure.DMA_Mode DMA_Mode_Circular; //循环模式即选择使用自动重装 DMA_InitStructure.DMA_M2M DMA_M2M_Disable; //存储器到存储器选择失能数据由ADC外设触发到存储器使用硬件触发 DMA_InitStructure.DMA_Priority DMA_Priority_Medium; //优先级选择中等 DMA_Init(DMA1_Channel1, DMA_InitStructure); //ADC1的硬件触发只接在了DMA1的通道1上所以这里通道必须要使用DMA的通道1其他的通道都不行 //DMA和ADC使能 DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能DMA转运的第三条件满足 //但是DMA转运的第二个条件触发源有信号目前是不满足的因为这里是硬件触发ADC还没启动不会有触发信号所以这里DMA使能之后不会立刻工作。 //上图中有三个硬件触发源具体使用哪个取决于把哪个的DMA输出给开启了ADC_DMACmd函数就是用来开启ADC到DMA的传输的定时器也是类似的 ADC_DMACmd(ADC1, ENABLE); //开启DMA触发信号的 开启ADC到DMA输出 ADC_Cmd(ADC1, ENABLE); //ADC使能 //ADC校准 ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1) SET); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1) SET); //ADC触发 ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发ADC之后由于ADC处于连续转换模式所以触发一次后ADC就可以一直连续工作另外DMA循环转运两者一直在工作始终把最新的转换结果刷新到SRAM数组里我们想要数据的时候随时去数组里取就行了GetValue()获取ADC转换数据函数也不需要了 } ADDMA.h #ifndef __AD_H #define __AD_H extern uint16_t AD_Value[4]; void AD_Init(void); #endif Main.c #include stm32f10x.h // Device header #include Delay.h #include OLED.h #include AD.h int main(void) { OLED_Init(); AD_Init(); OLED_ShowString(1, 1, AD0:); OLED_ShowString(2, 1, AD1:); OLED_ShowString(3, 1, AD2:); OLED_ShowString(4, 1, AD3:); while (1) { OLED_ShowNum(1, 5, AD_Value[0], 4); //AD_Value[0]是通道0的转换结果 OLED_ShowNum(2, 5, AD_Value[1], 4); OLED_ShowNum(3, 5, AD_Value[2], 4); OLED_ShowNum(4, 5, AD_Value[3], 4); Delay_ms(100); } } ADC连续扫描DMA循环转运模式主循环啥都不干直接读取AD_Value数组就能得到结果可以看到此时硬件外设已经实现了相互配合和高度的自动化各种操作都是硬件自己完成的极大地减轻了软件负担软件什么都不需要做也不需要进行任何中断硬件自动把活干完了。 另外还可以在AD_init函数中再加一个外设比如定时器ADC用单词扫描再用定时器去定时触发这样就是定时器触发ADCADC触发DMA整个过程完全自动不需要程序手动进行操作节省软件资源。这就是STM32的硬件自动化一大特色各个外设相互连接互相交织不再是传统这样一个CPU单独控制多个独立的外设这样的星型结构而是外设之间互相连接互相合作形成一个网状结构这样在完成某些简单且繁琐的工作的时候就不需要CPU来同一调度了可以直接通过外设之间的相互配合自动完成这些繁琐的工作不仅可以减轻CPU负担还可以大大提高外设的性能。在我们之前的学习中也经常遇到过这样的设计比如定时器的输出可以通向adc dac或其他定时器。的触发源可以来自定时器或外部中断的触发源可以来自定时器串口等等。 还有一个存储器到外设的情况看第5小节比如串口发送一批数据。就可以使用进行存储器到外设的转运。 4. DMA存储器到存储器模式实验
本章只讲解存储器到存储器和存储器到外设这两种模式其他功能模式在其他章节使用到的时候再讲。 存储器到存储器模式可以实现数据在两个内存的快速拷贝。我们先定义一个静态的源数据存放在内部FLASH 然后使用DMA传输把源数据拷贝到目标地址上内部SRAM最后对比源数据和目标地址的数据看看是否传输准确 。
4.1. 硬件设计
DMA存储器到存储器实验不需要其他硬件要求只用到RGB彩色灯用于指示程序状态。
4.2. 软件设计
这里只讲解核心的部分代码有些变量的设置头文件的包含等并没有涉及到完整的代码请参考本章配套的工程。 这个实验代码比较简单主要程序代码都在main.c文件中。
4.2.1. 编程要点
使能DMA时钟 配置DMA数据参数 使能DMA进行传输 等待传输完成并对源数据和目标地址数据进行比较。
4.2.2. 代码分析 DMA宏定义及相关变量定义 代码清单 22‑1 DMA数据流和相关变量定义 // 当使用存储器到存储器模式时候通道可以随便选没有硬性的规定 #define DMA_CHANNEL DMA1_Channel6 #define DMA_CLOCK RCC_AHBPeriph_DMA1 // 传输完成标志 #define DMA_FLAG_TC DMA1_FLAG_TC6 // 要发送的数据大小 #define BUFFER_SIZE 32 /* 定义aSRC_Const_Buffer数组作为DMA传输数据源 * const关键字将aSRC_Const_Buffer数组变量定义为常量类型 * 表示数据存储在内部的FLASH中 */ const uint32_t aSRC_Const_Buffer[BUFFER_SIZE] { 0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10, 0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20, 0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30, 0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40, 0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50, 0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60, 0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70, 0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80 }; /* 定义DMA传输目标存储器 * 存储在内部的SRAM中 */ uint32_t aDST_Buffer[BUFFER_SIZE]; 使用宏定义设置外设配置方便程序修改和升级。 存储器到存储器传输通道没有硬性规定可以随意选择。aSRC_Const_Buffer[BUFFER_SIZE]定义用来存放源数据并且使用了const关键字修饰即常量类型使得变量是存储在内部flash空间上。 DMA数据配置 代码清单 22‑2 DMA传输参数配置 void DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; // 开启DMA时钟 RCC_AHBPeriphClockCmd(DMA_CLOCK, ENABLE); // 源数据地址 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)aSRC_Const_Buffer; // 目标地址 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)aDST_Buffer; // 方向外设到存储器这里的外设是内部的FLASH DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; // 传输大小 DMA_InitStructure.DMA_BufferSize BUFFER_SIZE; // 外设内部的FLASH地址递增 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Enable; // 内存地址递增 DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; // 外设数据单位 DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Word; // 内存数据单位 DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Word; // DMA模式一次或者循环模式 DMA_InitStructure.DMA_Mode DMA_Mode_Normal ; //DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 优先级高 DMA_InitStructure.DMA_Priority DMA_Priority_High; // 使能内存到内存的传输 DMA_InitStructure.DMA_M2M DMA_M2M_Enable; // 配置DMA通道 DMA_Init(DMA_CHANNEL, DMA_InitStructure); // 使能DMA DMA_Cmd(DMA_CHANNEL,ENABLE); //启动或者停止DMA数据传输 } 使用DMA_InitTypeDef结构体定义一个DMA初始化变量这个结构体内容我们之前已经有详细讲解。 调用RCC_AHBPeriphClockCmd函数开启DMA时钟使用DMA控制器之前必须开启对应的时钟。 源地址和目标地址使用之前定义的数组首地址传输的数据量为宏BUFFER_SIZE决定源和目标地址指针地址递增 使用一次传输模式不能循环传输因为只有一个DMA通道优先级随便设置最后调用DMA_Init函数完成DMA的初始化配置。 DMA_ClearFlag函数用于清除DMA标志位代码用到传输完成标志位使用之前先清除传输完成标志位以免产生不必要干扰。 DMA_ClearFlag函数需要1个形参即事件标志位可选有传输完成标志位、半传输标志位、FIFO错误标志位、传输错误标志位等等 非常多我们这里选择传输完成标志位由宏DMA_FLAG_TC定义。 DMA_Cmd函数用于启动或者停止DMA数据传输它接收两个参数第一个是DMA通道另外一个是开启ENABLE或者停止DISABLE。 存储器数据对比 代码清单 22‑3 源数据与目标地址数据对比 uint8_t Buffercmp(const uint32_t* pBuffer, uint32_t* pBuffer1, uint16_t BufferLength) { /* 数据长度递减 */ while (BufferLength--) { /* 判断两个数据源是否对应相等 */ if (*pBuffer ! *pBuffer1) { /* 对应数据源不相等马上退出函数并返回0 */ return 0; } /* 递增两个数据源的地址指针 */ pBuffer; pBuffer1; } /* 完成判断并且对应数据相对 */ return 1; } 判断指定长度的两个数据源是否完全相等如果完全相等返回1只要其中一对数据不相等返回0。 它需要三个形参前两个是两个数据源的地址第三个是要比较数据长度。 主函数 代码清单 22‑4 存储器到存储器模式主函数 int main(void) { /* 定义存放比较结果变量 */ uint8_t TransferStatus; /* LED 端口初始化 */ LED_GPIO_Config(); /* 设置RGB彩色灯为紫色 */ LED_PURPLE; /* 简单延时函数 */ Delay(0xFFFFFF); /* DMA传输配置 */ DMA_Config(); /* 等待DMA传输完成 */ while (DMA_GetFlagStatus(DMA_FLAG_TC)RESET) { } /* 比较源数据与传输后数据 */ TransferStatusBuffercmp(aSRC_Const_Buffer, aDST_Buffer, BUFFER_SIZE); /* 判断源数据与传输后数据比较结果*/ if (TransferStatus0) { /* 源数据与传输后数据不相等时RGB彩色灯显示红色 */ LED_RED; } else { /* 源数据与传输后数据相等时RGB彩色灯显示蓝色 */ LED_BLUE; } while (1) { } } 首先定义一个变量用来保存存储器数据比较结果。 RGB彩色灯用来指示程序进程使用之前需要初始化它LED_GPIO_Config定义在bsp_led.c文件中。开始设置RGB彩色灯为紫色 LED_PURPLE是定义在bsp_led.h文件的一个宏定义。 Delay函数只是一个简单的延时函数。 调用DMA_Config函数完成DMA数据流配置并启动DMA数据传输。 DMA_GetFlagStatus函数获取DMA事件标志位的当前状态这里获取DMA数据传输完成这个标志位使用循环持续等待直到该标志位被置位 即DMA传输完成这个事件发生然后退出循环运行之后程序。 确定DMA传输完成之后就可以调用Buffercmp函数比较源数据与DMA传输后目标地址的数据是否一一对应。TransferStatus保存比较结果 如果为1表示两个数据源一一对应相等说明DMA传输成功相反如果为0表示两个数据源数据存在不等情况说明DMA传输出错。 如果DMA传输成功设置RGB彩色灯为蓝色如果DMA传输出错设置RGB彩色灯为红色。
4.3. 下载验证
确保开发板供电正常编译程序并下载。观察RGB彩色灯变化情况。正常情况下RGB彩色灯先为紫色然后变成蓝色。如果DMA传输出错才会为红色。
5. DMA存储器到外设模式实验
将内存中的数据发到串口助手工具DMA只需传送数据到开发板的串口发送模块开发板串口再发给串口助手工具硬件自动实现
上个实验我们讲了DMA存储器到存储器模式接下来我们再讲一个存储器到外设的实验。我们先定义一个数据变量 存于SRAM中然后通过DMA的方式传输到串口的数据寄存器然后通过串口把这些数据发送到电脑的上位机显示出来。
5.1. 硬件设计
存储器到外设模式使用到USART1功能具体电路设置参考USART章节无需其他硬件设计。
5.2. 软件设计
这里只讲解核心的部分代码有些变量的设置头文件的包含等并没有涉及到完整的代码请参考本章配套的工程。 我们编写两个串口驱动文件bsp_usart_dma.c和bsp_usart_dma.h有关串口和DMA的宏定义以及驱动函数都在里边。
5.2.1. 编程要点
配置USART通信功能 设置串口DMA工作参数 使能DMA DMA传输同时CPU可以运行其他任务。
5.2.2. 代码分析
将内存中的数据发到串口助手工具DMA只需传送数据到开发板的串口发送模块开发板串口再发给串口助手工具硬件自动实现所以代码里是内存与开发板串口发送引脚之间传输的。
USART和DMA宏定义 代码清单 22‑5 USART和DMA相关宏定义 // 串口工作参数宏定义 #define DEBUG_USARTx USART1 #define DEBUG_USART_CLK RCC_APB2Periph_USART1 #define DEBUG_USART_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_BAUDRATE 115200 // USART GPIO 引脚宏定义 #define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOA) #define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd #define DEBUG_USART_TX_GPIO_PORT GPIOA #define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_9 #define DEBUG_USART_RX_GPIO_PORT GPIOA #define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_10 // 串口对应的DMA请求通道 #define USART_TX_DMA_CHANNEL DMA1_Channel4 // 外设寄存器地址 #define USART_DR_ADDRESS (USART1_BASE0x04) // 一次发送的数据量 #define SENDBUFF_SIZE 5000 使用宏定义设置外设配置方便程序修改和升级。 USART部分设置与USART章节内容相同可以参考USART章节内容理解。串口的发送请求对应有固定的DMA通道 这里的外设的地址即串口的数据寄存器一次要发送的数据量可以自定义配置SENDBUFF_SIZE这个宏即可。 串口DMA传输配置 代码清单 22‑6 USART1 发送请求DMA设置 void USARTx_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; // 开启DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 设置DMA源地址串口数据寄存器地址*/ DMA_InitStructure.DMA_PeripheralBaseAddr USART_DR_ADDRESS; // 内存地址(要传输的变量的指针) DMA_InitStructure.DMA_MemoryBaseAddr (u32)SendBuff; // 方向从内存到外设 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; // 传输大小 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; // 内存数据单位 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; // 配置DMA通道 DMA_Init(USART_TX_DMA_CHANNEL, DMA_InitStructure); // 使能DMA DMA_Cmd (USART_TX_DMA_CHANNEL,ENABLE); } 首先定义一个DMA初始化变量用来填充DMA的参数然后使能DMA时钟。 因为数据是从存储器到串口所以设置存储器为源地址串口的数据寄存器为目标地址要发送的数据有很多且都先存储在存储器中 则存储器地址指针递增串口数据寄存器只有一个则外设地址地址不变两边数据单位设置成一致传输模式可选一次或者循环传输 只有一个DMA请求优先级随便设最后调用DMA_Init函数把这些参数写到DMA的寄存器中然后使能DMA开始传输. 主函数 代码清单 22‑7 存储器到外设模式主函数 int main(void) { uint16_t i; /* 初始化USART */ USART_Config(); /* 配置使用DMA模式 */ USARTx_DMA_Config(); /* 配置RGB彩色灯 */ LED_GPIO_Config(); printf(\r\n USART1 DMA TX 测试 \r\n); /*填充将要发送的数据*/ for (i0; iSENDBUFF_SIZE; i) { SendBuff[i] P; } /*为演示DMA持续运行而CPU还能处理其它事情持续使用DMA发送数据量非常大 *长时间运行可能会导致电脑端串口调试助手会卡死鼠标乱飞的情况 *或把DMA配置中的循环模式改为单次模式*/ /* USART1 向 DMA发出TX请求 */ USART_DMACmd(USARTx, USART_DMAReq_Tx, ENABLE); /* 此时CPU是空闲的可以干其他的事情 */ //例如同时控制LED while (1) { LED1_TOGGLE Delay(0xFFFFF); } } USART_Config函数定义在bsp_usart_dma.c中它完成USART初始化配置包括GPIO初始化USART通信参数设置等等具体可参考USART章节讲解。 USARTx_DMA_Config函数也是定义在bsp_usart_dma.c中之前我们已详细分析。 LED_GPIO_Config函数定义在bsp_led.c中它完成RGB彩色灯初始化配置具体可参考GPIO章节讲解。 使用for循环填充源数据SendBuff[SENDBUFF_SIZE]是定义在bsp_usart_dma.c中的一个全局无符号8位整数数组是DMA传输的源数据 在USART_DMA_Config函数中已经被设置为存储器地址。
USART_DMACmd函数用于控制USART的DMA请求的启动和关闭。它接收三个参数第一个参数用于设置串口外设可以是USART1/2/3和UART4/5这5个参数可选 第二个参数设置串口的具体DMA请求有串口发送请求USART_DMAReq_Tx和接收请求USART_DMAReq_Rx可选 第三个参数用于设置启动请求ENABLE或者关闭请求DISABLE。运行该函数后USART的DMA发送传输就开始了根据配置存储器的数据会发送到串口。
DMA传输过程是不占用CPU资源的可以一边传输一次运行其他任务。 5.3. 下载验证
保证开发板相关硬件连接正确用USB线连接开发板的USB转串口和电脑在电脑端打开串口调试助手把编译好的程序下载到开发板。 程序运行后在串口调试助手可接收到大量的数据同时开发板上RGB彩色灯不断闪烁。 这里要注意为演示DMA持续运行并且CPU还能处理其它事情持续使用DMA发送数据量非常大长时间运行可能会导致电脑端串口调试助手会卡死 鼠标乱飞的情况所以在测试时最好把串口调试助手的自动清除接收区数据功能勾选上或把DMA配置中的循环模式改为单次模式。 参考
野火和江科大的教程