找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3702

积分

0

好友

482

主题
发表于 昨天 20:45 | 查看: 5| 回复: 0

什么是DMA?为何在嵌入式开发中如此重要

DMA(Direct Memory Access,直接内存访问)是一种允许内存与外设之间直接进行数据交互的控制器。其核心优势在于,整个数据传输过程无需CPU持续介入。

在实时嵌入式系统中,为了释放CPU资源、避免其长期被数据搬运任务占用,DMA起着至关重要的作用。目前市面上绝大多数主流处理器都集成了DMA功能。它使得不同速度的硬件模块能够高效沟通,而无需给CPU带来大量的中断负载。否则,CPU将不得不亲自将数据的每一个片段从源地址复制到寄存器,再写回目标地址,在此过程中无法处理其他任务。

DMA的工作类比

为了更形象地理解,我们可以把DMA想象成员工,而CPU则是老板。老板需要寄送一个快递,只需向员工下达指令即可,至于填写快递单、物流派送等具体事务,无需亲自操心。当快递最终送达时,员工知会老板一声即可。

回到UART串口发送数据上,道理也是一样的。CPU只需执行简单的“任务指派”,即可将一串数据包交给DMA自动发送;发送完成后,DMA通过一个完成中断通知CPU。 如果CPU亲自逐字节发送数据,就好比老板亲自开车送快递,虽然也能完成,但会严重消耗宝贵的时间资源。尤其在RTOS中处理大量并发任务时,或者是在裸机程序面临严苛实时性要求时,采用DMA带来的效率提升尤为显著。

RL78单片机DMA在UART中的应用实例

今天我们将基于瑞萨RL78系列单片机,深入探讨DMA在UART通信中的实际应用。

首先,我们需要理解DMA的数据搬运机制。如下图所示,DMA1负责将数据从RAM搬运至SFR(外设功能寄存器,如串口发送寄存器),而DMA0则负责将数据从SFR搬运至RAM。

DMA数据传输方向示意图:RAM与SFR间的双向数据流

DMA控制器图形化配置步骤

打开DMA0的配置界面,我们需要依次确定三个核心要素:传输方向、地址与数据长度,以及触发源。DMA1的配置思路完全相同,只需根据应用需求调整方向即可。

在接收方向上,我们将DMA0配置为SFR to internal RAM,数据宽度为8 bits。SFR地址指向串口2的接收寄存器RXD2,RAM地址指向我们自定义的缓冲区,传输字节数设为8。触发信号选择INTSR2/INTCSI21,即串口2接收中断。

DMA0接收配置界面:设置从SFR到内部RAM的传输

在发送方向上,我们将DMA1配置为Internal RAM to SFR,数据宽度同样为8 bits。此时SFR地址指向串口2的发送寄存器TXD2,RAM地址同样指向数据缓冲区,传输字节数为8。触发信号选择INTST2/INTCSI20

DMA1发送配置界面:设置从内部RAM到SFR的传输

关键代码修改与__far指针修正

UART的基本配置相对简单,在此不再赘述。配置完成后,点击“Code Generator”生成驱动代码。需要特别注意的是,自动生成的串口API函数中,指针并未声明为__far类型。在实际测试中,若定义的数据缓冲区不在镜像区,可能导致传输地址错误。为避免此问题,建议在以下位置将相关指针修改为__far类型,确保寻址正确。

代码编辑器截图:在UART发送函数中高亮显示__far关键字

接下来,我们定义接收缓冲区 uart_buf[10],并将其地址赋值给DMA的RAM地址寄存器。这样,当DMA0被触发后,便会将从串口接收到的数据直接存入 uart_buf[10]

定义全局缓冲区变量,注意使用 __far 修饰以确保寻址范围:

全局变量定义代码截图:高亮显示__far extern unsigned char uart_buf[10]

在DMA0的初始化函数 R_DMAC0_Create 中,将缓冲区首地址赋值给 DRA0 寄存器:

void R_DMAC0_Create(void)
{
    DRC0 = _80_DMA_OPERATION_ENABLE;
    NOP();
    NOP();
    DMAMK0 = 1U; /* disable INTDMA0 interrupt */
    DMAIF0 = 0U; /* clear INTDMA0 interrupt flag */
    /* Set INTDMA0 low priority */
    DMAPR10 = 1U;
    DMAPR00 = 1U;
    DSC0 = _00_DMA_TRANSFER_DIR_SFR2RAM | _00_DMA_DATA_SIZE_8 | _0B_DMA_TRIGGER_SR2_CSI21;
    DSA0 = 4A DMA0 SFR ADDRESS;
    DRA0 = &uart_buf[0];//_EF00_DMA0_RAM_ADDRESS;
    DBC0 = _0008_DMA_BYTE_COUNT;
    DEN0 = 0U; /* disable DMA0 operation */
}

同理,通过DMA1发送数据时,也将待发送数据缓冲区的地址赋给 DRA1。DMA1触发后,便会将RAM中的数据搬运至串口发送SFR。

DMA1初始化函数 R_DMA1_Create 的对应代码:

void R_DMA1_Create(void)
{
    DRC1 = _80_DMA_OPERATION_ENABLE;
    NOP();
    NOP();
    DMAMK1 = 1U; /* disable INTDMA1 interrupt */
    DMAIF1 = 0U; /* clear INTDMA1 interrupt flag */
    /* Set INTDMA1 low priority */
    DMAPR11 = 1U;
    DMAPR01 = 1U;
    DMC1 = _40_DMA_TRANSFER_DIR_RAM2SFR | _00_DMA_DATA_SIZE_8 | _0A_DMA_TRIGGER_ST2_CSI20;
    DSA1 = 48 DMA1 SFR ADDRESS;
    DRA1 =&uart_buf[0];// EF00_DMA1_RAM_ADDRESS;
    DBC1 = 7;//_0008_DMA1_BYTE_COUNT;
    DEN1 = 0U; /* disable DMA1 operation */
}

在主函数中,我们只需要依次调用各个模块的初始化函数,并启动DMA通道即可。整个数据收发过程将由硬件自动完成,CPU无需在字节传输上耗费指令周期。

主函数调用示意:

void main(void)
{
    R_MAIN_UserInit();
    /* Start user code. Do not edit comment generated here */
    R_UART2_Start();
    R_TAU0_Channel0_Start();
    R_DMAC0_Start();
    R_DMAc1_Start();
    while (1U)
    {
    }
    /* End user code. Do not edit comment generated here */
}

硬件验证与内存观察

连接好硬件,将生成的 .mot 文件烧录至MCU。通过串口调试助手向MCU发送8个字符数据 12345678。尽管应用程序代码并未主动读取接收SFR,但借助DMA0的自动搬运,这8个字节会被直接送到我们指定的缓冲区 uart_buf 中。

串口助手发送数据界面,输入字符串 12345678 进行测试:

串口调试助手界面:在输入框中键入12345678并准备发送

通过调试器观察内存,变量监视窗口显示从 [0][7] 的位置,内容正好是 '1''8' 的ASCII码,证明接收完全正确。

变量监视窗口:显示uart_buf数组索引0-7正确接收到字符1-8

进一步查看 Memory1 窗口,在 0xFEF00 开始的地址处,可以清晰看到这些数据对应的十六进制值 31 32 33 34 35 36 37 38

内存浏览器窗口:从地址0xFEF00开始显示接收到的12345678的十六进制数据

以上验证结果表明,DMA通道已成功建立并准确工作。如果你的项目正面临性能瓶颈,或需要在任务繁重的系统中实现高效通信,不妨将该方案应用到你的实际工程中。更多关于嵌入式开发中的系统架构与内存管理优化技巧,也可在云栈社区与众多开发者一同探讨。




上一篇:Agentic AI时代,CPU凭什么比GPU更重要?
下一篇:Spring AI 2.0.0-RC1 新特性解读:工具调用外置化与API统一
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-6-9 00:58 , Processed in 0.740232 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表