4、DMA通道配置:通道使能、优先级设置、中断配置、传输完成回调机制
好,咱们接着聊DMA通道的配置。说实话,这部分是DMA用得顺不顺手的关键。很多工程师把DMA配置想得太简单,觉得不就是搬个数据嘛。其实不然,通道使能、优先级、中断、回调,这四个东西环环相扣,任何一个没处理好,都可能让你的系统跑飞。
我个人习惯,在开始配置之前,先想清楚一个问题:这个DMA通道要服务谁?是高速的ADC采样,还是慢速的SPI通信?不同的外设,配置思路完全不一样。我见过有人把ADC的DMA优先级设得比CPU还低,结果采样数据老是丢包。嗯,这种坑我踩过。
4.1 通道使能:不是简单的开关
通道使能,说白了就是告诉DMA控制器:「嘿,这个通道可以干活了。」但这里有个细节——使能时机。
你想想看,如果你在配置还没完成的时候就使能了通道,DMA可能拿到一堆垃圾数据就开始搬运。所以我建议的流程是:
- 先把所有配置项写进去(源地址、目的地址、传输长度、模式等)
- 最后一步才写使能位
代码示例是这样的:
/* 先配置,后使能 */
DMA->CH[channel].SRC_ADDR = (uint32_t)src_buf;
DMA->CH[channel].DST_ADDR = (uint32_t)dst_buf;
DMA->CH[channel].LENGTH = transfer_size;
DMA->CH[channel].CONTROL = dma_config_value;
/* 最后使能通道 */
DMA->CH[channel].ENABLE = 1;
注意:我曾经在项目里犯过一个低级错误——先使能了通道,然后才去配置源地址。结果DMA直接从一个未初始化的地址开始搬运,把整个内存区都污染了。从那以后,我每次写DMA使能代码,都会下意识地检查一下配置顺序。
4.2 优先级设置:谁先谁后,得有个规矩
TC3xx的DMA支持多级优先级。一般来说,优先级高的通道可以打断优先级低的通道。但这里有个容易忽略的点:优先级不是越高越好。
为什么?因为如果你把某个通道的优先级设得太高,它可能会频繁打断其他通道的传输,导致低优先级通道一直得不到服务。这在实时性要求高的系统里尤其要小心。
我一般遵循这个原则:
| 场景 | 推荐优先级 | 理由 |
|---|---|---|
| 高速ADC采样(1MHz以上) | 高(0或1) | 数据不能丢,必须及时搬走 |
| SPI通信(中等速率) | 中(2或3) | 可以容忍少量延迟 |
| UART接收(低速) | 低(4或5) | 数据量小,偶尔延迟没关系 |
| 内存到内存拷贝 | 最低 | 纯软件任务,优先级最低 |
配置代码也很直接:
/* 设置通道优先级,数值越小优先级越高 */
DMA->CH[channel].PRIORITY = 2; /* 中等优先级 */
小技巧:如果你不确定优先级怎么设,可以先从中间值开始。跑起来之后用逻辑分析仪抓一下DMA请求的响应时间,再根据实际情况调整。我习惯在调试阶段把优先级设得保守一点,等系统稳定了再优化。
4.3 中断配置:别让DMA默默干活
DMA传输完成之后,总得有人知道吧?中断就是干这个的。TC3xx的DMA中断配置其实挺灵活的,你可以选择在以下时机触发中断:
- 传输完成:整块数据搬完了
- 传输错误:地址越界、总线错误等
- 半传输完成:搬了一半,适合做双缓冲切换
我个人最常用的是「传输完成中断」。但要注意,中断服务程序(ISR)里不要做太多事情。DMA中断的优先级通常比较高,如果你在ISR里处理太多逻辑,会影响其他中断的响应。
配置代码示例:
/* 使能传输完成中断 */
DMA->CH[channel].INT_ENABLE |= DMA_INT_TC;
/* 配置中断优先级(NVIC层面) */
NVIC_SetPriority(DMA_IRQn, 3);
NVIC_EnableIRQ(DMA_IRQn);
我曾经踩过的坑:有一次我把DMA中断优先级设得和定时器中断一样高,结果两个中断互相嵌套,导致系统响应变得很不稳定。后来我养成了一个习惯——DMA中断优先级比定时器中断低一级,这样定时器能保证准时触发,DMA稍微等一等也没关系。
4.4 传输完成回调机制:让代码更优雅
回调机制,说白了就是「干完活叫我一声」。在嵌入式系统里,我们通常用函数指针来实现回调。这样DMA传输完成后,自动调用你注册的那个函数,省得你在主循环里轮询状态。
我习惯这样设计回调结构:
/* 定义回调函数类型 */
typedef void (*dma_callback_t)(uint32_t channel, uint32_t status);
/* 注册回调函数 */
void DMA_RegisterCallback(uint32_t channel, dma_callback_t cb)
{
dma_callbacks[channel] = cb;
}
/* 在中断服务程序里调用回调 */
void DMA_IRQHandler(void)
{
uint32_t channel = get_active_channel();
uint32_t status = DMA->CH[channel].STATUS;
if (status & DMA_STATUS_TC) {
if (dma_callbacks[channel] != NULL) {
dma_callbacks[channel](channel, status);
}
}
/* 清除中断标志 */
DMA->CH[channel].STATUS = status;
}
使用的时候,你只需要注册一个回调函数:
/* 定义回调函数 */
void my_dma_done(uint32_t ch, uint32_t status)
{
/* 传输完成,可以处理数据了 */
process_data(ch);
}
/* 注册回调 */
DMA_RegisterCallback(0, my_dma_done);
核心要点:回调函数里不要做耗时操作。我一般只在回调里设置一个标志位,或者唤醒一个任务。真正的数据处理放到主循环或者任务里去做。这样既保证了中断的快速响应,又不会阻塞其他中断。
4.5 综合配置示例
最后,我把上面这些整合成一个完整的配置流程。你照着这个顺序写,基本不会出问题:
void DMA_ChannelConfig(uint32_t ch)
{
/* 1. 关闭通道(防止配置过程中被触发) */
DMA->CH[ch].ENABLE = 0;
/* 2. 配置源地址、目的地址、传输长度 */
DMA->CH[ch].SRC_ADDR = (uint32_t)adc_buffer;
DMA->CH[ch].DST_ADDR = (uint32_t)ram_buffer;
DMA->CH[ch].LENGTH = 1024;
/* 3. 配置控制参数(模式、数据宽度等) */
DMA->CH[ch].CONTROL = DMA_MODE_PERIPH_TO_MEM |
DMA_WIDTH_32BIT |
DMA_CIRCULAR_ENABLE;
/* 4. 设置优先级 */
DMA->CH[ch].PRIORITY = 1;
/* 5. 使能中断并注册回调 */
DMA->CH[ch].INT_ENABLE = DMA_INT_TC;
DMA_RegisterCallback(ch, adc_data_ready);
/* 6. 最后使能通道 */
DMA->CH[ch].ENABLE = 1;
}
嗯,这个流程我用了好几年,基本没出过问题。你刚开始用的时候,建议先跑一个简单的测试——比如搬一个固定数组,看看中断能不能正常触发,回调能不能正常执行。确认没问题了,再接入实际的外设。
好了,DMA通道配置的核心内容就这些。下一章咱们聊聊更高级的话题——DMA的链式传输和 scatter-gather 模式。到时候你会发现,DMA能做的事情远比你想象的多。