什么是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控制器图形化配置步骤
打开DMA0的配置界面,我们需要依次确定三个核心要素:传输方向、地址与数据长度,以及触发源。DMA1的配置思路完全相同,只需根据应用需求调整方向即可。
在接收方向上,我们将DMA0配置为SFR to internal RAM,数据宽度为8 bits。SFR地址指向串口2的接收寄存器RXD2,RAM地址指向我们自定义的缓冲区,传输字节数设为8。触发信号选择INTSR2/INTCSI21,即串口2接收中断。

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

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

接下来,我们定义接收缓冲区 uart_buf[10],并将其地址赋值给DMA的RAM地址寄存器。这样,当DMA0被触发后,便会将从串口接收到的数据直接存入 uart_buf[10]。
定义全局缓冲区变量,注意使用 __far 修饰以确保寻址范围:
![全局变量定义代码截图:高亮显示__far extern unsigned char uart_buf[10]](https://static1.yunpan.plus/attachment/034abbc26b7b2310.webp)
在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 进行测试:

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

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

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