作者:junziyang
(注:如非特別声明,以下笔记内容均针对stm32f103ZET6而言。不同型号,细节可能存在差别)
8.1 DMA简介DMA是Direct memory access的简称,意为直接存储器访问,是一种提高数据传输速度,并在数据传输过程中为CPU减负的技术。常规的数据传输过程中,CPU要先将数据从存储器或外设的数据寄存器读取到缓存,然后再写入到目的地(存储器或外设数据寄存器)。DMA传输数据时则无需经CPU处理,而是直接将数据从一个地址复制到另一个地址,因此数据传输过程中极大地减少了对CPU资源的占用。
8.2 DMA的原理框图stm32内置有两个DMA控制器,原理框图如图1所示。
图1. DMA原理框图
从图中可以看出,DMA1有7个通道,DMA2有5个通道,共有12个通道。DMA充当的是外设和存储器(内部SRAM,SD卡,FSMC接口的外部存储器)之间的桥梁,由外设发起DMA请求,实现数据在DMA控制下在外设和存储器,以及存储器和存储器之间传输。每个DMA通道都可以独立响应一到多个外设访问请求,由DMA内部的仲裁器(Arbiter)来管理DMA请求的优先级。DMA的主要特征包括:
- 每个通道都与专门的硬件DMA请求关联,支持软件触发和配置。
- 向同一个DMA各通道发出的多个请求,可由软件配置优先级。共支持4级:低、中、高、最高。优先级相同时,由硬件来管理,低编号通道的请求优先。
- 传输数据宽度可以为字节、半字或字。源和目标双方的数据宽度可以独立设置,传输过程类似于打包(源)和解包(目标)。源和目标地址必须按数据宽度对齐。
- 支持缓存区循环使用。
- 支持传输过半、传输完成、传输错误3个事件即相关的中断。这3个事件的标志位可以通过或操作,合成一个全局中断,即发生其中任何一个事件都会触发的中断。
- 支持3种模式:外设到存储器、存储器到外设,以及存储器到存储器的DMA传输。
- Flash、SRAM,APB1和APB2以及AHB的外设均可作为源或目标。
- 可编程设置数据传输个数,范围:0-65535。
任何外设的功能配置都是通过设置相关寄存器实现的。为了便于后续学习,将DMA相关的寄存器先列于此。DMA相关寄存器地址映射与复位值如图2所示。
图2. DMA相关寄存器地址映射与复位值表
前两个寄存器是DMA各个通道共用的寄存器,DMA_ISR(Interrupt Status Register)管理各个通道的中断状态标志,每个通道都有4个中断。DMA_IFCR(Interrupt Flag Clear Register)用来复位DMA_ISR的对应位。
后面的4个寄存器是各个通道专用的寄存器,也就是说每个通道都有4个这样的寄存器。DMA_CMAx后,地址偏移4个字节,然后排入下一个通道的4个寄存器,依次类推。
8.4 DMA主要功能及设置DMA控制器是通过与内核共享总线来实现直接存储器访问的。总线矩阵通过轮询调度来管理CPU和DMA对总线的访问,一般会保证CPU对总线有至少一半的占用带宽。尽管如此,当出现CPU和DMA同时要访问同一个目标(存储器或外设)时,仍存在CPU对总线的访问被DMA请求妨碍几个总线时钟周期的可能。因此,程序中执行完DMA相关的指令后(例如HAL_ADC_START_DMA()),一般要适当延迟,然后再去读取DMA传输过去的数据,否则可能会出现数据读取错误的现象。
DMA的最大优点是可以独立于CPU去实现数据的传输,最好将DMA传输数据的过程和CPU对数据的处理过程分开。例如,ADC扫描模式下必须使用DMA,此时可将数据采集和数据处理分开,在一个循环中进行AD转换,DMA将数据存入数组;在另一个循环中再去访问数组中的数据并进行处理。而不是在一个循环中,触发ADC一次,立即去读数组中的数据,这样容易导致程序“跑飞”。
8.3.1 DMA传输过程触发DMA的某个事件发生后,先由外设向DMA控制器发送请求信号。DMA控制器会按信道的优先级响应收到的请求。当DMA控制器访问外设时,会向外设发出应答信号。外设收到应答信号后立刻释放请求,进而DMA控制器立刻释放应答,并开始传输数据。如果有更多的请求,外设可以启动下一次传输。简而言之,一次DMA传输包括一下三个操作步骤:
- 从源地址载入数据:源可以是外设的数据寄存器或存储器。外设寄存器和存储器的基地址分别在DMA_CPARx (Channelx Peripheral Address Register)寄存器或DMA_CMARx(Channelx Memory Address Register)寄存器中设置。
- 向目标地址存储数据:目标同样可以是外设的数据寄存器或存储器。首地址也是在上述两个寄存器中设置。
- 计数递减:DMA_CNDTRx寄存器中设置的是待传输数据个数,每次传输完成,数值递减1。也就是说,传输数据个数是提前设置的,数据传输期间通过读此寄存器可以判断传输进度。
仲裁器Arbiter根据通道DMA请求的优先级设置,来依次启动对外设/存储器的访问。优先权的管理分两个阶段:
- 软件设置:通过DMA_CCRx寄存器的PL[1:0]位,可以为通道x设置优先级。2个寄存器位可以设置4级(低、中、高、最高),从00-11,值越大优先级越高。
- 硬件仲裁:软件设置优先级高的有效。如果软件设置的优先级相同,则低编号通道优先,这由硬件来仲裁。例如,若同时收到来自通道2和通道4优先级相同的请求,则通道2的请求优先。
上述是同一个DMA内部通道的优先级管理,由各自的仲裁器负责。另外,在有两个DMA的MCU中,在经Bus Matrix争取总线使用权的过程中,DMA1控制器的优先级高于DMA2的。
8.3.3 DMA的通道管理用DMA来传输数据时,需要约定通道两端的地址、传输数据的宽度(字节/半字/全字)、待传输数据个数、数据传输模式、缓存使用方式等。
1. 通道设置步骤
通道配置的步骤如下:(x是通道编号)
- 设置外设的地址:DMA_CPARx寄存器。外设的地址一般是固定的,即外设DR寄存器的地址。
- 设置存储器地址:DMA_CMARx寄存器。一般是程序中一个数组的首地址。
- 设置待传输数据个数:DMA_CNDTRx寄存器。每次传输后该数值会递减1。
- 设置通道优先级:DMA_CCRx寄存器的PL[1:0]位。
- 通道参数设置:在DMA_CCRx寄存器中设置 - 传输方向(DIR位)、循环模式(CIRC位)、外设&存储器递增模式(PINC&MINC位)、外设&存储器数据宽度(PSIZE[1:0]&MSIZE[1:0]位),并按需使能中断(TCIE/HTIE/TEIE)。
- 激活DMA通道:DMA_CCRx的EN位。
DMA通道激活后即可响应来自外设的DMA请求了。
2. 循环模式
设置DMA_CCRx寄存器中的CIRC位为1可以开启循环模式。循环模式主要用来处理连续的数据流,例如ADC扫描模式中的连续数据采集。该模式下DMA_CNDTRx寄存器中的值递减到0后,会重新装载回初值,缓存也会被循环利用。
3. 存储器-存储器模式
除了实现外设-存储器间的数据传输,DMA模式也可以用来在两个存储器间传输数据。通过设置DMA_CRx寄存器中的MEM2MEM位即可开启存储器-存储器模式。无论是外设-存储器模式,还是存储器-存储器模式,本质上来说DMA都只是将数据从一个地址搬运到另一个地址。区别之处在于:外设地址通常是固定的,而存储器地址可以人为设置且可递增;外设可以发出DMA请求,而存储器间的数据传输需要通过软件设置DMA_CCRx寄存器的EN位来触发DMA。通信双方的首地址仍然是在CPARx和CMARx两个寄存器中设置,传输方向也是由CCRx寄存器中的DIR位设定。
存储器间的数据传输不要同时开启循环模式。DMA_CNDTRx寄存器递减到0,传输自动停止。
8.3.4 数据宽度与对齐问题DMA支持按字节、半字或全字传输数据。如果双方的数据宽度相同,DMA只需将数据依次传输过去即可。但如果源和目标端的数据宽度不同,即PSIZE和MSIZE不相等,就涉及到传输过程中的数据对齐问题。
如果目标端数据宽度比源端大,则目标端接收数据后按右对齐存储,闲置高位补0。例如,源为8位,目标为32位,传输4个数据,传输过程和结果如下:
如果目标端数据宽度比源端小,则源端高位被截掉,仅传输目标端能容纳的低位。例如,源为32位,目标为16位,传输4个数据,传输过程和结果如下:
可见,无论哪种情况,DMA都是按源端的数据宽度读取数据,然后按目标端的数据宽度按右对齐写入数据的。目标端按自己的数据宽度和传输数据个数分配内存,传输过来的数据“多去少补,对号入座”。如果条件允许,尽量双方一致,尤其要避免“大筐倒小筐”。
注意:
对于不支持字节和半字写入的AHB外设,传入的字节或半字会从低位到高位重复填满总线数据寄存器HWDATA。例如0xAB在会被重复成0xABABABAB。这种情况下,如果从内存向寄存器写入数据,内存端MSIZE应该按存储器数据宽度设置,而外设端PSIZE则设为32bit。例如向APB备份寄存器(16位有效,但占32位地址)写入,MSIZE配置位16位,PSIZE配置位32位。
8.3.5 错误与中断管理在DMA读/写预设地址的过程中,如果出现错误,硬件会自动关闭出错的通道,清除DMA_CCRx中的EN位,设置DMA_IFR中的TEIF(Transfer Error Interrupt Flag)中断标志位。如果开启了CCRx中的TEIE位,则会产生中断。
DMA传输过程中会设置3个中断标志位:传输过半(HTIF)、传输完成(TCIF)、传输出错(TEIF)。如果设置了中断使能位(TCIE/HTIE/TEIE),则会产生对应的中断。
8.3.6 DMA请求的通道映射来自外设的DMA请求共有7类:TIMx[1,2,3,4], ADC1, SPI1, SPI/I2S2, I2Cx[1,2] 以及
USARTx[1,2,3]。这些请求被固定的映射到了DMA1的7个通道和DMA2的5个通道。DMA1的映射关系如图3所示。DMA2的映射关系如图4所示。
图3. DMA1的通道映射
图4. DMA2的通道映射
总结:
- 同一通道不同外设的DMA请求在进入DMA前是“或”的关系,即每次只能有1个外设可以占用该通道。
- 通道编号越低,硬件优先级越高。
- 每个通道都支持软件触发的MEM2MEM模式。
- 外设请求与通道的关系是固定的,不可通过编程改变。
DMA的寄存器非常简单,如图2所示。有2个公用寄存器:DMA_ISR和DMA_IFCR。每个通道还有4个专用寄存器:DMA_CCRx/CNDTRx/CPARx/CMARx。
8.4.1 DMA_ISR中断状态寄存器DMA_ISR(Interrupt Status Register)用来管理DMA的中断状态。由硬件设置,软件清除(设置IFCR寄存器的对应位)。每个通道的4个中断占连续的4个位。从低到高依次为GIFx(全局中断标志)、TCIFx(传输完成中断标志)、HTIFx(传输过半中断标志)、TEIFx(传输错误中断标志)。每个通道实际只有3个中断,其中GIFx是其余3个标志位相“与”的结果,三者中任何一种中断事件发生,该位都会被置1。
8.4.2 DMA_IFCR中断状态清除寄存器DMA_IFCR(Interrupt Flag Clear Register)用来清除ISR寄存器中的对应位,清除中断状态。ISR是只读的,中断发生时由硬件设置,要清除中断标志位,需要向DMA_IFCR寄存器的对应位写入1(写0无影响)。另外,向CGIFx写入1可以清除通道x所有的中断标志位。
8.4.3 DMA_CCRx通道配置寄存器DMA_CCR(Channel x Configuration Register)用来配置通道x。各功能位作用如下:
- MEM2MEM[14] 1-使能存储器-存储器模式;0-关闭。
- PL[13:12] 通道优先级设置。00-11,共4级,数值越大优先级越高。
- MSIZE[11:10] 存储器端数据宽度。00-8bits;01-16bits;10-32bits;11-保留。
- PSIZE[9:8] 外设端数据宽度,配位逻辑同MSIZE。
- MINC[7] 1-开启内存端地址递增;0-关闭。
- PINC[6] 1-开启外设端地址递增;0-关闭。
- CIRC[5] 循环模式。1-开启循环模式;0-关闭;
- DIR[4] 数据传输方向。1-内存到外设;0-外设到内存。
- TEIE[3] 1-使能传输错误中断;0-关闭。
- HTIE[2] 1-使能传输过半中断;0-关闭。
- TCIE[1] 1-使能传输完成中断;0-关闭。
- EN[0] 1-使能通道;0-关闭。
DMA_CNDTRx(Channel x Number of Data Register)用来存储待传输数据个数。每次传输数值递减1。非循环模式下,递减到0时DMA请求结束;循环模式下,递减凋0后会自动加载初值,继续DMA传输。
8.4.5 DMA_CPARx外设地址寄存器DMA_CPARx(Channel x Peripheral Address Register)用来设置外设基地址。当PSIZE=01,即数据宽度为16位时,地址中的PA[0]位被忽略,数据自动对齐到半字地址;当PSIZE=10,即数据宽度为32位时,地址中的PA[1:0]位被忽略,数据自动对齐到字地址。
说明:
半字为16bits,即2个字节。字节是最小的存储单位,即每个地址存1个字节。地址0b000x-0b001x,间隔2个字节,恰为1个半字的空间。同理,0b01xx-0b02xx,间隔4个字节,即一个全字的空间,所以32位时忽略地址的末2位。
8.4.6 DMA_CMARx内存地址寄存器
DMA_CMARx(Channel x Memory Address Register)用来设置存储器(数组)基地址。DMA_CPARx和CMARx实际上就是两个地址存储器,用来存储源和目标的地址。MEM2MEM时存储的都是存储器中的地址。
8.5 HAL库DMA相关的函数HAL库stm32f103xe.h中对DMA的通道地址进行了映射,可以通过通道宏名称,访问通道的CCR/CNDTR/CPAR/CMAR寄存器。DMA相关的库函数在stm32f1xx_hal_dma.h中声明,在stm32f1xx_hal_dma.c中定义。分类概述如下:
1. 初始化与复位函数
- HAL_DMA_Init()
DMA初始化函数。设置CCR寄存器中的PL/MSIZE/PSIZE/MINC/PINC/CIRC/DIR,共7个功能位。
- HAL_DMA_DeInit()
DMA复位函数。关闭DMA,然后清除CCR&CNDTR&CPAR&CMAR,清除hdma句柄中的中断函数,复位State状态机和ErrorCode。
2. 启动与停止函数
- HAL_DMA_Start()
启动DMA。函数中完成的工作包括:中断标志位清除、设置CNDTR、CPAR、CMAR。设置这些参数前必须先将CCRx的EN位置0(__HAL_DMA_DISABLE(hdma)),然后调用DMA_SetConfig()函数进行设置,设置完成后重新开启(__HAL_DMA_ENABLE(hdma))。
- HAL_DMA_Start_IT()
以中断模式启动DMA。除完成HAL_DMA_Start()的工作外,还开启了3个中断使能位。
- HAL_DMA_Abort()
终止DMA传输。函数中完成的工作包括:关闭中断、关闭DMA通道、中断标志位清除(向IFCR中的CGIFx写入1)。
- HAL_DMA_Abort_IT()
终止DMA传输并触发相应的中断。
3. 状态查询函数
- HAL_DMA_PollForTransfer()
检查传输状态。通过轮询等待返回传输状态(传输过半/传输完成)标志。
- HAL_DMA_GetState()
查询DMA状态。返回DMA状态机的值,即hdma->State。
- HAL_DMA_GetError()
查询DMA错误。返回DMA错误代码,即hdma->ErrorCode。
4. 中断处理函数
- HAL_DMA_IRQHandler()
中断请求处理函数。根据hdma的中断设置,调用相应的回调函数(传输完成/过半/出错/终止)。回调函数需要用户自己编写,一般是在外设的HAL_PPP_Stat_DMA()函数中被关联到hdma的句柄。例如,HAL_ADC_Start_DMA()中会把HAL_ADC_ConvHalfCpltCallback/HAL_ADC_ConvCpltCallback/HAL_ADC_ErrorCallback函数关联到DMA的句柄hdma。回调函数是weak函数,需要用户自己实现。
5. 宏库函数
- __HAL_DMA_ENABLE() 使能DMA(CCR->EN)
- __HAL_DMA_ENABLE_IT() 使能指定中断(CCR->*IE)
- __HAL_DMA_DISABLE() 关闭DMA
- __HAL_DMA_DISABLE_IT() 关闭指定中断
- __HAL_DMA_GET_IT_SOURCE() 查询中断源
- __HAL_DMA_GET_COUNTER() 查询计数寄存器CNDTRx