第四章 外设功耗管理:时钟门控、电源域与DMA的平衡艺术
外设功耗管理,说白了就是「用的时候供电,不用的时候断电」。
听起来简单吧?但我在实际项目中见过太多翻车案例了。有个哥们儿把UART的时钟关了,结果下次唤醒时波特率全乱了。还有个项目,ADC的电源域没处理好,测量值漂得跟心电图似的。
这一章,我就把外设功耗管理的三个核心手段掰开揉碎了讲给你听。
4.1 外设时钟门控:最基础的省电手段
时钟门控,英文叫Clock Gating。它的原理很简单——不给外设送时钟,它就不工作,自然就不耗电。
RT-Thread里,外设时钟门控通常通过驱动框架的open/close接口来实现。我习惯在驱动层做这件事,而不是让应用层直接操作寄存器。
核心原则:谁使用,谁开时钟;谁释放,谁关时钟。
来看一个典型的UART驱动实现:
static rt_err_t uart_open(rt_device_t dev, rt_uint16_t oflag)
{
struct uart_device *uart = (struct uart_device *)dev->user_data;
/* 开启外设时钟 */
uart->config->clock_enable(uart->config->instance);
/* 配置GPIO复用 */
uart->config->gpio_config();
/* 初始化硬件 */
uart_hw_init(uart);
return RT_EOK;
}
static rt_err_t uart_close(rt_device_t dev)
{
struct uart_device *uart = (struct uart_device *)dev->user_data;
/* 关闭外设时钟 */
uart->config->clock_disable(uart->config->instance);
return RT_EOK;
}
这里有个坑,我踩过好几次——时钟开启和关闭的时机。如果你在中断服务函数里关时钟,那下次中断就进不来了。嗯,这听起来像废话,但真有人这么干过。
我的习惯:在驱动层维护一个引用计数。每次open加1,close减1。只有计数归零时才真正关时钟。这样能避免多个线程共用同一个外设时,一个线程close把另一个线程的时钟给关了。
4.2 外设电源域管理:从根上断电
时钟门控省的是动态功耗,但静态功耗(漏电流)它管不了。要彻底省电,得动电源域。
电源域管理,就是把芯片内部划分成多个供电区域。每个区域可以独立开关。比如MCU的RTC、备份寄存器通常在一个独立的电源域里,主电源断了它还能跑。
我在一个物联网项目中用过STM32L4的电源域管理。它的外设被分成了几个组:
| 电源域 | 包含外设 | 典型功耗 |
|---|---|---|
| VDD | CPU、Flash、SRAM | ~100μA/MHz |
| VDDIO2 | GPIO、UART、SPI | ~10μA |
| VDDUSB | USB OTG | ~50μA |
| VBAT | RTC、备份寄存器 | ~1μA |
你想想看,如果只是用UART做数据采集,完全可以把VDDUSB域关掉。省下来的50μA,对电池供电的设备来说,可能就是多撑一个月的关键。
注意:切换电源域时,要确保外设处于空闲状态。我曾经在DMA还在传输数据时关了电源域,结果DMA卡死在半路上,下次上电都恢复不了。最后只能硬件复位。
4.3 DMA与低功耗的平衡艺术
DMA这东西,又爱又恨。爱它是因为CPU可以睡觉,DMA帮你搬数据。恨它是因为DMA本身也耗电,而且跟低功耗模式有冲突。
RT-Thread的DMA框架,我参与过一些设计。它的核心思路是:DMA传输期间,CPU可以进入浅睡眠,但不能进深睡眠。
为什么?因为深睡眠模式下,系统时钟可能被关闭,DMA控制器也就没法工作了。所以DMA和深睡眠是互斥的。
我在一个音频播放项目里遇到过这个问题。用DMA从Flash搬数据到DAC,CPU想进STOP模式省电。结果每次进STOP,DMA就罢工,音频就卡顿。
解决方案是这样的:
/* DMA传输完成回调 */
static void dma_transfer_done(struct rt_dma_channel *channel)
{
/* 通知CPU可以进深睡眠了 */
rt_sem_release(&dma_sem);
}
/* 主循环 */
void audio_playback_task(void *param)
{
while (1) {
/* 启动DMA传输 */
rt_dma_transfer_async(&dma_ch, src, dst, size);
/* CPU进入浅睡眠,等待DMA完成 */
rt_sem_take(&dma_sem, RT_WAITING_FOREVER);
/* DMA完成,CPU可以进深睡眠了 */
rt_pm_request(PM_SLEEP_MODE_DEEP);
rt_thread_mdelay(10); /* 睡一小会儿 */
rt_pm_release(PM_SLEEP_MODE_DEEP);
}
}
这个方案的精髓在于:DMA搬数据时CPU浅睡,DMA搬完了CPU深睡。既保证了数据连续性,又最大化了省电效果。
一个小技巧:如果DMA传输的数据量不大(比如几十个字节),其实没必要用DMA。CPU自己搬数据也就几微秒,比启动DMA的功耗还低。我一般设个阈值——超过256字节才用DMA。
4.4 实战:一个外设功耗管理的完整案例
说了这么多理论,咱们来点实际的。这是一个温湿度采集器的外设管理代码:
static void sensor_task(void *param)
{
struct rt_device *i2c_dev;
struct rt_device *sensor_dev;
i2c_dev = rt_device_find("i2c1");
sensor_dev = rt_device_find("sht30");
while (1) {
/* 1. 开启I2C外设时钟和电源域 */
rt_device_open(i2c_dev, RT_DEVICE_FLAG_RDWR);
pm_domain_enable(PM_DOMAIN_I2C1);
/* 2. 启动DMA传输(如果数据量大) */
/* ... */
/* 3. 读取传感器数据 */
rt_device_read(sensor_dev, 0, &data, sizeof(data));
/* 4. 关闭外设 */
pm_domain_disable(PM_DOMAIN_I2C1);
rt_device_close(i2c_dev);
/* 5. CPU进入深睡眠,等待下次采集 */
rt_pm_request(PM_SLEEP_MODE_DEEP);
rt_thread_delay(60000); /* 1分钟采集一次 */
rt_pm_release(PM_SLEEP_MODE_DEEP);
}
}
这个案例里,外设只在采集的几十毫秒内供电,其余时间完全断电。配合CPU深睡眠,整体功耗能做到10μA以下。
关键点总结:
- 时钟门控:动态功耗,开关速度快,适合频繁切换
- 电源域:静态功耗,开关速度慢,适合长时间休眠
- DMA:让CPU浅睡,但不能深睡,需要精细调度
- 三者配合:时钟门控做细粒度控制,电源域做粗粒度控制,DMA做中间层缓冲
最后说一句,外设功耗管理没有银弹。每个芯片的电源域划分、时钟树结构都不一样。我建议你拿到一个新芯片,先花半天时间把参考手册的「Power Management」和「Clock Control」章节啃透。嗯,这半天时间,值。