无人问津也好,技不如人也罢,都应静下心来,去做该做的事。
最近在学STM32,所以也开贴记录一下主要内容,省的过目即忘。视频教程为江科大(改名江协科技),网站jiangxiekeji.com
本期开始学习DMA,直接存储器存取。DMA是一个数据转运小助手,它主要是用来协助CPU,完成数据转运的工作。
const是c语言的关键字,表示的是常量的意思,被const修饰的变量,在程序中,只能读,不能写。在STM32中,使用const定义的变量,是存储在Flash里面的。当然这里就不应该说是“变量"了,而应该说是“常量"。也就是说,被const修饰的变量,它的值只能在定义的时候赋值,不能在后面的程序中再次赋值。
什么时候需要用const定义常量呢?这个是当我们程序中出现了一大批数据,并且不需要更改时,就可以把它定义成常量,这样能节省SRAM的空间。比如查找表、字库数据等等。如果你有一个很大的查找表或者字库,最好加一个const。
DMA基本结构
DMA常用函数
打开dma.h,拉到最后。
Delnit:恢复缺省配置
void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);
DMA_Init:初始化
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);
DMA_StructInit:结构体初始化
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);
DMA_Cmd:使能
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
DMA_ITConfig:中断输出使能
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);
DMA设置当前数据寄存器:就是给这个传输计数器写数据的,
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);
DMA获取当前数据寄存器:这个函数就是返回传输计数器的值,如果你想看看,还剩多少数据没有转运,就可以调用这个函数,获取一下传输计数器,这样就行了。
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);
两个程序现象
DMA数据转运
在这个程序里,我们将使用DMA,进行存储器到存储器的数据转运。也就是把一个数组里面的数据,复制到另一个数组里。这里先定义了一个数组DataA,里面存的是1、2、3、4,作为待转运的源数据。然后下面再定义一个数组DataB,里面存的是4个0,作为转运数据的目的地。之后我们将会写一个模块,叫MyDMA。然后MyDMA初始化,把源数组和目的数组的地址传进去,再传入转运数据的长度4,接着执行主循环的流程。
第一步,自增,变化一下源数组DataA的测试数据。
第二步,显示一下DataA和DataB,然后延时一秒,方便观看。
第三步,调用一下MyDMA_Transfer函数,使用DMA进行数据转运。和主程序里直接使用for循环,使用CPU一个个手动地转运数据,效果是一样的。
接着最后,再显示一下DataA和DataB,看一下数据是不是从DataA转运到了DataB。
这里第一行是DataA,右边是DataA数组的地址。第二行就是DataA的源数据了,每隔两秒变一次。第三行是DataB,右边是DataB数组的地址。最后一行,是DataB的目的地数据。
可以看到,DataA每变一次,Delay1秒后,数据就转运到了DataB。这个转运过程,就是由DMA来完成的。你也可以定义100个、1000个等等数据,然后使用DMA来进行转运,都是可以的。这是第一个程序的现象。
接线图
和OLED显示实验接线时一样的。
初始化步骤
根据DMA基本结构图来配置即可
第一步,RCC开启DMA的时钟,DMA要开启AHB总线。
/*开启时钟*/
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA的时钟
第二步,就可以直接调用DMA_Init,初始化这里的各个参数了,包括外设和存储器站点的起始地址、数据宽度、地址是否自增,方向、传输计数器、是否需要自动重装、选择触发源,当然还有一个通道优先级,图没画出来。这些参数,通过一个结构体就可以配置好了。
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; //外设基地址,给定形参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; //数据传输方向,选择由外设到存储器
DMA_InitStructure.DMA_BufferSize = Size; //转运的数据大小(转运次数)
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //模式,选择正常模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //存储器到存储器,选择使能
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
这里,如果你选择的是硬件触发,不要忘了在对应的外设调用一下XXXDMA_Cmd,开启一下触发信号的输出。如果你需要DMA的中断,那就调用DMA_ITConfig,开启中断输出,再在NVIC里,配置相应的中断通道,然后写中断函数就行了。中断的配置各个外设都一样。
第三步,就可以进行开关控制,DMA_Cmd,给指定的通道使能,就完成了。
/*DMA使能*/
DMA_Cmd(DMA1_Channel1, DISABLE); //这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
最后,在运行的过程中,如果转运完成,传输计数器清0了,这时想再给传输计数器赋值的话,就DMA失能、写传输计数器、DMA使能,这样就行了。
DMA转运有三个条件:第一,就是开关控制,DMA_Cmd必须使能;第二,就是传输计数器必须大于0;第三,就是触发源,必须有触发信号。触发一次,转运一次,传输计数器自减一次。当传输计数器等手0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运了。此时就需要DMA_Cmd,给DISABLE,关闭DMA,再为传输计数器写入一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA,DMA才能继续工作。注意一下,写传输计数器时,必须要先关闭DMA,再进行,不能在DMA开启时,写传输计数器,这是手册里的规定。
代码展示
这里DMA不涉及外部电路,所以直接在System里添加MyDMA.c、.h文件,封装对应的驱动函数。
main函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"
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,观察转运后的现象
}
}
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
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)
{
MyDMA_Size = Size; //将Size写入到全局变量,记住参数Size
/*开启时钟*/
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA的时钟
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; //外设基地址,给定形参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; //数据传输方向,选择由外设到存储器
DMA_InitStructure.DMA_BufferSize = Size; //转运的数据大小(转运次数)
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //模式,选择正常模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //存储器到存储器,选择使能
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
/*DMA使能*/
DMA_Cmd(DMA1_Channel1, DISABLE); //这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
}
/**
* 函 数:启动DMA数据转运
* 参 数:无
* 返 回 值:无
*/
void MyDMA_Transfer(void)
{
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); //清除工作完成标志位
}
DMA+AD多通道
用ADC的扫描模式来实现多通道采集,然后使用DMA来进行数据转运。最终,AD转换的数据就会直接自动地跑到我们定义的数组里面来,之后我们就只需要用OLED显示一下就行了。看上去就很方便。
这个硬件电路和程序现象和上一节的AD多通道都是一模一样的,也是测量PA0~PA3这4个通道的模拟量。就只是在STM32端,使用了扫描模式,并且加了DMA转运数据。