在嵌入式系统开发中,DMA(直接内存访问)控制器是提升系统性能的关键组件。ARM PrimeCell PL080 DMA控制器作为典型的第二代DMA IP核,采用双AHB总线架构,支持8个独立通道的并发操作。每个通道都拥有完整的寄存器组,包括地址寄存器、控制寄存器和配置寄存器,能够实现复杂的数据传输场景。
PL080的寄存器空间采用分层设计,主要分为全局寄存器和通道专用寄存器两大类。全局寄存器位于0x000-0x0FF地址范围,包含DMACConfiguration、DMACIntStatus等控制整个控制器行为的寄存器。通道寄存器则从0x100开始按0x20间隔分布,每个通道独占一组寄存器:
code复制Channel 0: 0x100-0x11F
Channel 1: 0x120-0x13F
...
Channel 7: 0x1E0-0x1FF
这种规整的地址布局使得寄存器访问可以通过基地址+偏移量的方式高效实现。在编程时,我们通常会定义如下的寄存器映射结构体:
c复制typedef struct {
uint32_t DMACCxSrcAddr; // 源地址寄存器 @0x00
uint32_t DMACCxDestAddr; // 目标地址寄存器 @0x04
uint32_t DMACCxLLI; // 链表项寄存器 @0x08
uint32_t DMACCxControl; // 控制寄存器 @0x0C
uint32_t DMACCxConfiguration; // 配置寄存器 @0x10
uint32_t Reserved[3]; // 保留区域
} DMA_Channel_Registers;
PL080通过两个独立的AHB主接口与系统互联:
这种双主接口设计使得控制器可以同时从源设备读取数据和向目标设备写入数据,实现真正的并行传输。在寄存器编程时,需要通过控制寄存器的S(bit24)和D(bit25)位分别指定源和目标使用的主接口:
c复制// 设置源使用Master1,目标使用Master2
control_reg |= (0 << 24) | (1 << 25);
注意:AHB总线宽度默认为32位,但某些SoC可能配置为64位或更宽。这需要与DMACPeriphID3寄存器的[6:4]位设置保持一致,否则会导致传输异常。
源地址寄存器(偏移0x00)存储数据传输的起始内存地址或外设FIFO地址。关键编程要点:
地址必须按照数据宽度对齐:
地址递增模式由控制寄存器的SI位(bit26)控制:
c复制// 启用源地址自动递增
control_reg |= (1 << 26);
在链表模式下,完成当前数据块传输后会自动从下一个LLI加载新地址
目标地址寄存器(偏移0x04)的编程规则与源地址寄存器类似,但需要注意:
外设目标地址通常固定,不应启用地址递增:
c复制// 禁用目标地址递增
control_reg &= ~(1 << 27);
内存到内存传输时,双方都应启用地址递增:
c复制control_reg |= (1 << 26) | (1 << 27); // SI和DI都置1
链表模式是PL080的高级特性,允许非连续内存块的自动传输。DMACCxLLI寄存器(偏移0x08)存储下一个LLI描述符的地址,其[31:2]位有效,[1:0]固定为0。
典型链表初始化流程:
c复制typedef struct {
uint32_t src_addr;
uint32_t dest_addr;
uint32_t next_lli;
uint32_t control;
} LLI_Descriptor;
LLI_Descriptor lli_chain[3];
// 初始化第一个LLI
lli_chain[0].src_addr = 0x20000000;
lli_chain[0].dest_addr = 0x30000000;
lli_chain[0].next_lli = (uint32_t)&lli_chain[1];
lli_chain[0].control = ...; // 控制参数
// 中间LLI
lli_chain[1].next_lli = (uint32_t)&lli_chain[2];
// 最后一个LLI
lli_chain[2].next_lli = 0; // 链表结束标志
重要提示:LLI描述符必须4字对齐(地址低4位为0),否则会导致不可预知的行为。建议使用GCC的__attribute__((aligned(16)))确保对齐。
控制寄存器(偏移0x0C)是DMA通道的核心配置点,主要包含以下关键位域:
SWidth[20:18]和DWidth[23:21]分别控制源和目标的数据宽度:
c复制#define WIDTH_8BIT 0x0
#define WIDTH_16BIT 0x1
#define WIDTH_32BIT 0x2
// 设置源16位,目标32位
control_reg |= (WIDTH_16BIT << 18) | (WIDTH_32BIT << 21);
硬件会自动处理不同宽度间的数据打包/解包,但需要注意:
SBSize[14:12]和DBSize[17:15]控制突发传输长度,对应关系如下:
| 值 | 突发长度 | 适用场景 |
|---|---|---|
| 0x0 | 1 | 单次传输 |
| 0x1 | 4 | 适合缓存行 |
| 0x2 | 8 | 内存批量传输 |
| 0x3 | 16 | 高带宽设备 |
c复制// 设置源突发8次,目标突发4次
control_reg |= (0x2 << 12) | (0x1 << 15);
经验法则:内存端突发长度应匹配CPU缓存行大小(通常32/64字节),外设端需参考器件手册。
TransferSize[11:0]字段指定传输总数(以SWidth为单位)。例如:
c复制// 传输1024个16位数据
control_reg |= (1024 & 0xFFF);
关键注意事项:
DMACCxConfiguration寄存器的FlowCntrl[13:11]位决定流控策略:
| 值 | 模式 | 适用场景 |
|---|---|---|
| 000 | DMA控制 | 内存到内存 |
| 001 | DMA控制 | 内存到外设 |
| 010 | DMA控制 | 外设到内存 |
| 100 | 外设控制 | 外设间传输 |
| 101 | 外设控制 | 内存到外设 |
| 110 | 外设控制 | 外设到内存 |
典型配置示例:
c复制// 外设到内存,由外设控制流
config_reg |= (0x6 << 11); // 二进制110
PL080提供两种中断信号:
中断使能通过以下位控制:
c复制// 使能终端计数中断
control_reg |= (1 << 31); // I位
config_reg &= ~(1 << 15); // ITC位不屏蔽
// 使能错误中断
config_reg &= ~(1 << 14); // IE位不屏蔽
中断处理最佳实践:
c复制while(1) {
if(DMACIntTCStatus & (1 << ch)) {
// 处理已完成缓冲区
process_buffer(active_buf);
// 切换缓冲区
active_buf ^= 1;
DMACIntTCClear = (1 << ch);
}
}
c复制// 32位对齐分配内存
uint32_t *buf = memalign(4, BUF_SIZE);
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 传输数据错位 | 宽度/突发配置不匹配 | 检查SWidth/DWidth与外设特性 |
| 只传输部分数据 | TransferSize设置过小 | 确认计算单位是数据项而非字节 |
| 中断不触发 | 中断屏蔽位设置错误 | 检查ITC/IE位和全局中断使能 |
| 链表传输中断 | LLI地址不对齐 | 确保LLI描述符16字节对齐 |
寄存器检查清单:
总线监控:使用逻辑分析仪捕获AHB总线信号,确认:
DMA状态查询:
c复制uint32_t state = DMACEnbldChns;
if(state & (1 << ch)) {
// 通道已激活
uint32_t remaining = (DMACCxControl[ch] >> 12) & 0xFFF;
printf("剩余传输量: %u\n", remaining);
}
外设FIFO超时:
当外设FIFO未就绪时,DMA会等待HREADY信号。为避免系统挂死,应:
内存一致性:
在Cache使能系统中,必须确保:
c复制// ARMv7处理cache一致性示例
void clean_cache(void *addr, size_t size) {
uint32_t addr_aligned = (uint32_t)addr & ~0x1F;
uint32_t size_aligned = ((size + 31) & ~0x1F);
SCB_CleanDCache_by_Addr((uint32_t*)addr_aligned, size_aligned);
}
通过深入理解PL080 DMA控制器的寄存器结构和编程模型,开发者可以充分发挥其高性能数据传输能力,满足各类嵌入式系统对实时性和效率的严苛要求。实际开发中建议结合具体SoC的参考手册,注意不同厂商可能存在的实现差异。