单片机学习!
目录
前言
本文介绍了DMA初始化配置、DMA数据转运的基础内容。
一、DMA配置步骤
初始化步骤:
第一步,RCC开启DMA的时钟。
第二步,参数初始化配置。直接调用DMA_Init,初始化各个参数,包括外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源、通道优先级。这些所有的参数,通过一个结构体,就可以配置好了。
第三步,开关控制,调用DMA_Cmd函数,给指定的通道使能,就完成了。
如果选择的是硬件触发,需要在对应的外设调用一下XXX_DMACmd函数,开启一下触发信号的输出;
如果需要DMA的中断,那就调用DMA_ITConfig,开启中断输出。再在NVIC里配置相应的中断通道,然后写中断函数就可以了。
最后,在运行的过程中,如果转运完成,传输计数器清0了,这时再想给传输计数器赋值的话,就DMA失能 -> 写传输计数器 -> DMA使能,这样就行了。
二、详细步骤
2.1 RCC开启DMA的时钟
第一步,RCC开启DMA的时钟。
代码示例:
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA是AHB总线的设备,所以要用AHB开启时钟的函数。
第一个参数在函数定义中的解释是:
对于互联型设备,参数可以是下面这些值的组合(互联型是STM32F105/107的型号)
- RCC_AHBPeriph_DMA1
- RCC_AHBPeriph_DMA2
- RCC_AHBPeriph_SRAM
- RCC_AHBPeriph_FLITF
- RCC_AHBPeriph_CRC
- RCC_AHBPeriph_OTG_FS
- RCC_AHBPeriph_ETH_MAC
- RCC_AHBPeriph_ETH_MAC_Tx
- RCC_AHBPeriph_ETH_MAC_Rx
对于其他设备,参数可以是这下面的组合(F103的在其它设备下面的参数表里选)
- RCC_AHBPeriph_DMA1
- RCC_AHBPeriph_DMA2
- RCC_AHBPeriph_SRAM
- RCC_AHBPeriph_FLITF
- RCC_AHBPeriph_CRC
- RCC_AHBPeriph_FSMC
- RCC_AHBPeriph_SDIO
这里以STM32F103的芯片来举例,选择RCC_AHBPeriph_DMA1参数。
第二个参数填ENABLE,开启DMA1的时钟。
2.2 DMA参数初始化配置
第二步,参数初始化配置。直接调用DMA_Init,初始化各个参数。
参数包括外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源、通道优先级。
这些所有的参数,通过一个结构体,就可以配置好了。
代码示例:
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;//起始地址
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);
2.2.1 外设站点的三个参数
外设站点的三个参数:
- 起始地址
- 数据宽度
- 地址是否自增
1. 起始地址
DMA_PeripheralBaseAddr 起始地址,外设站点的基地址。这在里要写一个32位的地址,比如 0x20000000 这样的地址。对于SRAM的数组,他的地址是编译器分配的,并不是固定的。所以一般不会写绝对地址,而是通过数组名来获取地址。
在这里就把这个地址提取成整个初始化配置DMA函数的参数,这样在进行初始化配置的时候,想转运哪个数组就把哪个数组的地址传进来就行了。所以这里可以给整个初始化配置DMA函数传入 uint32_t 大小的参数AddrA,AddrA也就是外设的起始地址。然后给结构体DMA_PeripheralBaseAddr 外设起始地址参数这里放AddrA。
示例:
void MyDMA_Init(uint32_t AddrA)
{
DMA_InitStructure.DMA_PeripheralBaseAddr=AddrA;//起始地址
......
}
这样外设站点的地址就完成了。
2. 数据宽度
DMA_PeripheralDataSize数据宽度,函数定义中介绍是,指定数据宽度的参数可以是以下值:
- DMA_PeripheralDataSize_Byte Byte,字节,就是uint8_t ;
- DMA_PeripheralDataSize_HalfWord HalfWord,半字,就是uint16_t ;
- DMA_PeripheralDataSize_Word Word,字,就是uint32_t 。
这里需要以字节的方式传输,所以就填入DMA_PeripheralDataSize_Byte。
3. 地址是否自增
DMA_PeripheralInc 地址是否自增。函数定义中解释是,指定外设地址是自增或者不是。
参数取值:
- DMA_PeripheralInc_Enable 自增
- DMA_PeripheralInc_Disable 不自增
数组之间的转运,地址需要自增,所以这里选择DMA_PeripheralInc_Enable。
这样外设站点的参数就配置好了:
DMA_InitStructure.DMA_PeripheralBaseAddr=AddrA;//起始地址
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte;//数据宽度
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Enable;//是否自增
2.2.2 存储器站点的三个参数
存储器站点的三个参数:
- 起始地址
- 数据宽度
- 地址是否自增
1. 起始地址
DMA_MemoryBaseAddr 起始地址,存储器站点的基地址。和外设站点一样,也把这个地址提取成整个初始化配置DMA函数的参数。
所以这里可以给整个初始化配置DMA函数传入 uint32_t 大小的参数AddrB。AddrB也就是存储器的起始地址。然后给结构体 DMA_MemoryBaseAddr 存储器起始地址参数这里放AddrB。
示例:
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB)
{
......
DMA_InitStructure.DMA_MemoryBaseAddr=AddrB;//起始地址
......
}
这样存储器站点的地址就完成了。
2. 数据宽度
DMA_MemoryDataSize 数据宽度,和外设站点一样也选择Byte参数,以字节传输。
- DMA_MemoryDataSize_Byte Byte,字节,就是uint8_t ;
- DMA_MemoryDataSize_HalfWord HalfWord,半字,就是uint16_t ;
- DMA_MemoryDataSize_Word Word,字,就是uint32_t 。
3. 地址是否自增
DMA_MemoryInc 地址是否自增,和外设站点一样也选择地址自增。
- DMA_MemoryInc_Enable 自增
- DMA_MemoryInc_Disable 不自增
这样存储器站点的参数就配置好了:
DMA_InitStructure.DMA_MemoryBaseAddr=AddrB;//起始地址
DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte;//数据宽度
DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;//是否自增
2.2.3 传输方向
DMA_DIR 传输方向,函数定义解释是,指定外设站点是源端还是目的地。
- DMA_DIR_PeripheralDST 外设站点作为DST,destination,目的地,外设站点作为目的地,其实就是传输方向是存储器站点到外设站点。
- DMA_DIR_PeripheralSRC 外设站点作为SRC,source,源头,外设站点作为源头,也就是传输方向是外设站点到存储器站点。
函数设计将DataA放在外设站点,DataB放在存储器站点。传输方向就是外设站点到存储器站点,所以这里选择 DMA_DIR_PeripheralSRC参数,外设站点作为数据源。
代码示例:
DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向
2.2.4 传输计数器
DMA_BufferSize 缓存区大小,其实就是传输计数器。
DMA_BufferSize 在函数定义解释是,以数据单元指定缓存区大小,数据单元等于外设数据宽度或者存储器数据宽度,数据宽度取决于传输方向。
以数据单元指定缓存区大小,就是说需要传送几个数据单元,这个数据单元等于传输源站点的DataSize,简单理解就是,DMA_BufferSize就是传输计数器,指定传输几次。
可以查看DMA_Init函数的源码,DMA_BufferSize参数其实就是直接赋值给了传输计数器的寄存器,它的取值是0~65535.
所以这里可以把DMA_BufferSize参数也提取到初始化配置DMA函数的参数里来,给整个初始化配置DMA函数传入 uint16_t 大小的参数Size,Size也就是传输计数器指定传输的次数,然后把Size给到结构体DMA_BufferSize参数这里。
示例:
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size)
{
......
DMA_InitStructure.DMA_BufferSize=Size;//传输计数器
......
}
这样传输次数就完成了。
2.2.5 是否使用自动重装
DMA_Mode 传输模式,其实就是指定传输计数器是否使用自动重装。
DMA_Mode 在函数定义中的解释是,指定操作方式有对应参数取值列表。
配置 DMA_Mode 参数还有一个注意事项,函数定义中写的是循环模式,也就是自动重装,不能应用在存储器到存储器的情况下。也就是之前博文说的,自动重装和软件触发不能同时使用,如果同时使用,DMA就会连续触发,永远也不会停下来。
- DMA_Mode_Circular 循环模式,就是传输计数器自动重装
- DMA_Mode_Normal 正常模式,就是传输计数器不自动重装,自减到0后停下来。
这里转运数组是存储器到存储器的传输,转运一次停下来就行了。DMA_Mode 的配置选择正常模式DMA_Mode_Normal。
代码示例:
DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;//否使用自动重装
2.2.6 选择触发源
DMA_M2M 选择是否存储到存储器,其实就是选择硬件触发还是软件触发。
DMA_M2M 在函数定义中的解释是,DMA是否应用于存储器到存储器的转运模式,存储器到存储器的转运模式就是软件触发。
- DMA_M2M_Enable 使用软件触发。
- DMA_M2M_Disable 不使用软件触发,也就是使用硬件触发。
这里转运数组,所以选择 DMA_M2M_Enable 使用软件触发。
代码示例:
DMA_InitStructure.DMA_M2M=DMA_M2M_Enable;//选择触发源
2.2.7 通道优先级
DMA_Priority 优先级,按照参数要求,给一个优先级。
DMA_Priority 在函数定义中的解释是,指定通道的软件优先级。
- DMA_Priority_VeryHigh 非常高
- DMA_Priority_High 高
- DMA_Priority_Medium 中等
- DMA_Priority_Low 低
如果有多个通道,可以指定一下优先级,确保紧急的转运有更高的优先级。这里只有一个通道,那优先级可以任意配置。
代码示例:
DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级
2.2.8 DMA_Init
DMA_Init 函数参数配置:
第一个参数:DMAy_Channelx,y可以是1或2,用来选择是哪个DMA;对于DMA1,x可以是1~7,或者对于DMA2,x可以是1~5.x用来选择是哪一个通道。
DMA_Init函数的第一个参数,既选择了是哪个DMA,也选择了是DMA的哪个通道。
DMAy_Channelx,这里y写为1,选择DMA1;x选择通道,这里因为是存储器到存储器的转运,用的是软件触发,所以通道可以任意选择。这里x给1,通道1.
第二个参数的位置放 DMA_InitStructure 结构体的地址,这样就是把结构体指定的参数,配置到DMA1的通道1里面去。
代码示例:
DMA_Init(DMA1_Channel1,&DMA_InitStructure);
以上DMA的参数就配置完成了。参数虽然比较多,但是对照框图来理解的话,就比较清晰。
到目前为止,DMA还不能工作。
DMA转运有三个条件:
- 第一个条件,传输计数器大于0;
- 第二个条件,触发源有触发信号;
- 第三个条件,DMA使能。
三个条件缺一不可,
- 目前如果传一个大于0的数给Size的话,第一个条件满足。
- 触发源为软件触发,所以一直都有触发信号,第二个条件满足。
- 最后一个条件,DMA还没有使能,第三个条件不满足。
所以到目前为止DMA还不会工作。如果想在初始化之后就立刻工作的话,可以在最后加上DMA_Cmd函数。
2.3 开关控制
第三步,开关控制,调用DMA_Cmd函数,给指定的通道使能,就完成了。
代码示例:
DMA_Cmd(DMA1_Channel1,ENABLE);
使能DMA之后,三个条件满足,DMA就会进行数据转运了。转运一次,传输计数器自减一次,当传输计数器减到0之后,转运完成。转运完成的同时第一个条件就不满足了,转运停止。这样就完成了一次数组之间的数据转运。
2.4 DMA初始化配置代码
代码示例:
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size)//函数初始化DMA
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
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;//起始地址
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_Cmd(DMA1_Channel1,ENABLE);
}
对应结构图:
三、DMA转运启动函数设计
设计一个函数 MyDMA_Transfer,调用一次这个函数,就再启动一次DMA转运。
3.1 传输计数器赋值
再启动一次DMA转运,就需要重新给传输计数器赋值。
重新给传输计数器赋值必须要
- 先给DMA失能;
- 然后给传输计数器赋值;
- 最后再给DMA使能。
代码示例:
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);
DMA_Cmd(DMA1_Channel1,ENABLE);
......
}
1. 调用DMA_Cmd函数,使DMA失能。
- 第一个参数还是选择DMA1的通道1;
- 第二个参数给DISABLE。
2. 然后就可以给传输计数器赋值了,调用DMA_SetCurrDataCounter函数。
- 第一个参数,选择DMA和通道。
- 第二个参数,指定要给传输计数器写入的值。
传输计数器写入的值需要从DMA初始化配置的函数中获取一下Size参数。但是Size参数在MyDMA_Init初始化配置函数中,这里需要的传输计数器写入的值位于MyDMA_Transfer函数中,不能直接传递过来。
解决办法:可以在代码块最前面定义一个全局变量MyDMA_Size,初始化的时候把Size往这个全局变量里也存一份,之后在这个函数块里,就可以使用全局变量的MyDMA_Size了。这样就可以重新给传输计数器赋值了。
示例:
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size)//函数初始化DMA
{
MyDMA_Size = Size;
......
DMA_InitStructure.DMA_BufferSize=Size;//传输计数器
......
}
void MyDMA_Transfer(void)
{
......
DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);
......
}
3. 最后,再次调用DMA_Cmd函数,给DMA使能。
- 第一个参数还是选择DMA1的通道1;
- 第二个参数给ENABLE。
完成以上三步,DMA传输的3个条件又重新满足了。DMA就会再次开始转运。
3.2 标志位查看/清除
转运开始之后还需要做一个工作,就是等待转运完成,可以通过查看标志位来确定转运是否完成。
因为转运也是要花一些时间的,等待转运完成调用 DMA_GetFlagStatus函数可以查看标志位。
DMA_GetFlagStatus函数中总共四种标志位,以DMA1的通道1举例,其它所有的通道,都是这4种标志位。
- DMA1_FLAG_GL1:全局标志位
- DMA1_FLAG_TC1:转运完成标志位
- DMA1_FLAG_HT1:转运过半标志位
- DMA1_FLAG_TE1:转运错误标志位
代码示例:
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
这里需要检查DMA1通道1转换完成的标志位,所以选择DMA1_FLAG_TC1参数。
转运完成之后,标志位会置1,所以需要加一个while循环,等待这个标志位==RESET,如果没有完成,就一直循环等待,这样就实现了等待转运完成的效果了。
标志位置1之后,不要忘记清除标志位,这个标志位需要手动清除。调用 DMA_ClearFlag函数。
3.3 DMA转运启动函数代码
代码示例:
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);
DMA_Cmd(DMA1_Channel1,ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
DMA_ClearFlag(DMA1_FLAG_TC1)
}
总结
以上就是今天要讲的内容,本文仅仅简单介绍了DMA初始化配置以及一些配置代码的细节。