江科大DMA直接存储器访问hal库实现

发布于:2025-05-24 ⋅ 阅读:(15) ⋅ 点赞:(0)

 DMA 直接存储器访问,通过硬件自动在外设与存储器之间进行数据的传输,不需要CPU的介入,减少了CPU的数据传递这种简单工作的消耗。

例如上次的ADC多通道,我们需要在主函数循环里面不断判断ADC的状态再读取ADC转换结束的值,但是如果我们通过DMA数据转运,把ADC转换完成的数据,存放到一个数组内,就不需要我们在主函数里面对ADC进行任何操作,直接在指定数组里面读取我们需要的数据即可。

减轻了CPU的负担,提高程序的速度。

DMA的相关hal库函数

DMA初始化结构体

typedef struct
{
  uint32_t Direction;                 /*传输方向*/

  uint32_t PeriphInc;                 /*外设自增与否*/

  uint32_t MemInc;                    /*存储器自增与否*/

  uint32_t PeriphDataAlignment;       /*外设数据对齐方式*/

  uint32_t MemDataAlignment;          /*存储器数据对齐方式 */

  uint32_t Mode;                      /*DMA转运模式*/

  uint32_t Priority;                  /*DMA转运优先级供DMA仲裁使用 */
} DMA_InitTypeDef;

DMA句柄 

typedef struct __DMA_HandleTypeDef
{
  DMA_Channel_TypeDef        *Instance; /*我们选择的DMA外设地址*/
  
  DMA_InitTypeDef            Init;  /*DMA初始化结构体*/
  
  HAL_LockTypeDef            Lock; /*DMA句柄锁*/
  
  __IO HAL_DMA_StateTypeDef  State; /*DMA传输状态*/
  
  void                       *Parent;/*用于存储一个指向父对象的指针,便于对象间的层级关联 */
  
  void                       (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma);     /*DMA转运完成回调函数 */
  
  void                       (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); /*DMA转运过半回调函数 */
  
  void                       (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);    /*DMA转运出错回调函数 */

  void                       (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma);    /*DMA转运传输被异常终止回调函数*/
  
  __IO uint32_t              ErrorCode; /*DMA错误类型*/

  DMA_TypeDef                *DmaBaseAddress; /*DMA的基地址*/
  
  uint32_t                   ChannelIndex; /*DMA通道索引*/

} DMA_HandleTypeDef;    

DMA初始化相关函数 

HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMA_DeInit (DMA_HandleTypeDef *hdma);

DMA运行函数,前面两个是开启DMA传输函数,之间两个是DMA中断函数,最后一个DMA轮询函数,询问DMA的传输状态。 

HAL_StatusTypeDef HAL_DMA_Start (DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
HAL_StatusTypeDef HAL_DMA_Abort(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMA_Abort_IT(DMA_HandleTypeDef *hdma);
HAL_StatusTypeDef HAL_DMA_PollForTransfer(DMA_HandleTypeDef *hdma, uint32_t CompleteLevel, uint32_t Timeout);

DMA状态获取函数,错误获取函数。 

HAL_DMA_StateTypeDef HAL_DMA_GetState(DMA_HandleTypeDef *hdma);
uint32_t HAL_DMA_GetError(DMA_HandleTypeDef *hdma);

 DMA数据转运

DMA的设置就这么点,和标准库相比,他把原地址和目标地址,传输长度放到了开始函数里面,更加方便我们的数据传输,以及DMA通道的重复利用。

使用memtomem模式,DMA1的通道1,优先级为中断,模式为普通模式,原地址自增,目标地址自增,传输的数据大小都为字节大小。

江科大的DMA初始化函数里面,首先就是失能了DMA,这样一开始初始化的时候就不会直接开始数据传递,容纳后在传输的函数里面,再次失能DMA,在写入传输次数,才开始使能DMA,DMA使能后就开始传输,等待传输完成,清除传输完成标志位。

HAL_DMA_Start() 在循环中重复调用却只执行一次,这是因为 STM32 HAL 库的 DMA 设计机制导致的,按照江科大的写入传输计数器,指定将要转运的次数,本质就是修改CNDTR寄存器,我们对hal库进行修改CNDTR寄存器操作就能正常实现江科大的功能,但是HAL_DMA_Start在正常模式下只能调用一次,按照豆包的说法,就是正常模式调用一次后,硬件自动禁用通道(CCR 的 EN 位清零),但 HAL 库内部状态可能仍标记为 “忙碌”,从而在start函数里面对DMA通道状态的判断的时候出错,导致不能正常开启转运。

if(HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)DataA, (uint32_t)DataB, 4)==HAL_BUSY) DataA[0]+=10;

通过这个判断句,我们可以清晰的看出确实如此,多次调用DMA_Start的时候一直显示的就是DMA在忙碌状态,任何解决这个忙碌状态才是更需要思考的部分。

而直接修改传输次数,在传输完成的时候,CNDTR为0,我们修改了传输次数,DMA就继续传输,而不会对DMA的状态进行判断,跳过了DMA的状态判断,进而能够完成数据的传输。

初始化的校准部分我们通过

HAL_StatusTypeDef HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef* hadc)

来完成,这是在ADC_Ex里面。 

void MyDMA_Transfer(void)
{   __HAL_DMA_DISABLE(&hdma_memtomem_dma1_channel1);
    // 启动 DMA 传输
    hdma_memtomem_dma1_channel1.Instance->CNDTR = 4;
    //if(HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)DataA, (uint32_t)DataB, 4)==HAL_BUSY) DataA[0]+=10;
    
    __HAL_DMA_ENABLE(&hdma_memtomem_dma1_channel1);
    while(__HAL_DMA_GET_FLAG(&hdma_memtomem_dma1_channel1, DMA_FLAG_TC1) == RESET)
    // 清除完成标志
    __HAL_DMA_CLEAR_FLAG(&hdma_memtomem_dma1_channel1, DMA_FLAG_TC1);
}

在主函数前进行一次DMA的Start,然后在循环里面不断设置传输次数

HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)DataA, (uint32_t)DataB, 4);

把转换函数改成这个hal库的函数即可,其余的不需要我们的操作。

DMA+AD多通道

DMA+AD多通道就是DMA在外设与寄存器中间进行数据传输的最好例子,也是DMA帮助外设传递数据不需要CPU干预的最好验证。

因为我们模式是连续转换模式,扫描模式,所以配置ADC的时候就得把转换通道修改成我们所用到的四个通道,并对通道的序号进行排序扫描。他就会按照通道0,1,2,3,这样的顺序进行扫描,再回到0123不断进行,再扫描结束后会把转换结果发送到ADC规则组存储器,DMA在这个时候取走数据然后传输到我们指定的位置即可,所以对于原地址,不需要自增,目标地址就需要不断自增,没转换完成一次,就自增一次,然后全部转换完成又重新从头开始转换,DMA转运的地址也重新从头开始传输。

DMA的配置 ,这边数据的长度变成了半字(16位),且外设地址不自增,存储器地址自增,模式也改成了循环模式。

二配置完成后,我们先在主函数前面校准一次ADC,再开启一次ADC_DMA转换,就能自己一直连续转换了,我们只需要在循环里面不断读取我们存放转移数据的数组内容即可。

但是这边就会发现问题,就是OLED屏幕是全黑的,或者说OLED上面的数据变化缓慢且有明显的滞后性。我网上找了B站UP主野生绿波电龙看他的视频他也出现了这个问题,他说是DMA中断自动开启了,导致中断频繁主程序不能正常运行,我按照他视频内的方法:

把Force DMA channels Interrupts关闭,再把DMA的全局中断使能关掉。

这样下来程序就能正常运行了,这也给我们一个BUG的寻找方法,我们不仅要对代码进行差错,还得对代码的逻辑性进行查询错误,比如说中断的频繁触发导致主程序不能正常运行,这个点我们也得记下来,以后说不定就会遇到这类型的BUG。

HAL_ADCEx_Calibration_Start(&hadc1);//校准ADC
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)AD_Value,4);

 再主函数前面校准ADC后启动一次ADC_DMA传输即可自动连续扫描传输。

配置无误且关掉了DMA中断就能正常显示了。


网站公告

今日签到

点亮在社区的每一天
去签到