第三章 外设低功耗策略:ADC低功耗采样
ADC采样,在医疗贴片里是个耗电大户。你想想看,心电信号、体温、血氧,哪个不需要ADC?但问题是,很多工程师一上来就用连续采样模式,电池很快就见底了。
我个人习惯是:能不采就不采,能少采就少采。这不是偷懒,这是低功耗设计的核心思想。
3.1 单次采样 vs 序列采样
单次采样,说白了就是采一次就睡。我做过一个体温贴片,每10秒只采一次,ADC转换完立刻进休眠。功耗从原来的2.3mA降到了0.4mA。效果立竿见影。
序列采样呢?适合需要连续监测的场景。比如心电信号,你需要连续采256个点。但注意,序列采样结束后,一定要手动关掉ADC时钟。我在项目中遇到过,有人开了序列采样忘了关,结果一夜之间电池掉了30%。
核心原则:
- 单次采样:适合低频监测(体温、湿度等)
- 序列采样:适合突发数据采集(心电、脑电等)
- 连续采样:除非万不得已,否则别用
3.2 定时采样与触发源选择
定时采样,我建议用硬件定时器触发,而不是软件延时。为什么?因为软件延时的时候CPU还在跑,白白耗电。
我曾经做过一个项目,用SysTick定时器触发ADC,CPU在采样间隙进入深度睡眠。功耗直接降了60%。嗯,这里要注意:定时器的时钟源要选低速的,比如32kHz的LSE,别用主时钟分频。
// 伪代码示例:定时器触发ADC单次采样
void ADC_Timer_Init(void) {
// 配置定时器,32kHz时钟,每100ms触发一次
TIM_Prescaler = 32; // 分频到1kHz
TIM_Period = 100; // 100ms周期
TIM_TriggerEvent = TIM_TRGO_UPDATE;
// ADC配置为定时器触发
ADC_TriggerSource = ADC_EXT_TRIG_TIM;
ADC_SingleConversionMode = ENABLE;
// 关键:转换完成后自动关ADC
ADC_AutoPowerDown = ENABLE;
}
我的小技巧: 定时采样时,把采样率降到信号带宽的2倍就够了。比如体温信号变化很慢,1Hz采样都嫌多。别被「高精度」三个字忽悠了。
SPI/I2C总线低功耗设计
总线通信,是另一个耗电陷阱。很多工程师习惯把SPI/I2C一直开着,等着数据来。这就像你家大门一直敞着,等着快递员来——冷气全跑光了。
3.3 SPI总线低功耗策略
SPI的功耗主要来自时钟翻转。我建议:通信前开时钟,通信完立刻关。别小看这个动作,一个8MHz的SPI时钟,开着和关着能差0.5mA。
我记得有一次调试一个血糖贴片,SPI通信完后忘了关时钟,结果待机电流多了0.8mA。查了两天才找到原因。从那以后,我每次写完SPI代码,都会加一句:
void SPI_Deinit(void) {
SPI_Cmd(DISABLE); // 关SPI外设
SPI_ClockConfig(DISABLE); // 关时钟
GPIO_Config(SPI_SCK, GPIO_MODE_INPUT_PULLDOWN); // 把SCK拉低,防止漏电
}
警告: SPI的片选信号(CS)一定要处理好。如果CS一直拉低,从设备会认为主机还在通信,可能一直保持高功耗状态。我见过有人因为这个原因,从设备功耗多了2mA。
3.4 I2C总线低功耗设计
I2C比SPI好一点,因为它是开漏结构,没有时钟时基本不耗电。但坑也不少。
首先,上拉电阻要选对。我一般用4.7kΩ到10kΩ。阻值太小,灌电流太大;阻值太大,上升沿太慢,通信容易出错。你想想看,一个100kHz的I2C总线,用1kΩ上拉,光上拉电阻就耗掉0.3mA。不值当。
其次,通信完成后,把SCL和SDA都设为输入模式,带上拉。这样总线处于空闲状态,功耗最低。
void I2C_EnterLowPower(void) {
I2C_Cmd(DISABLE);
GPIO_Init(SCL_PIN, GPIO_MODE_INPUT, GPIO_PULLUP);
GPIO_Init(SDA_PIN, GPIO_MODE_INPUT, GPIO_PULLUP);
}
UART低功耗设计
UART在医疗贴片里用得不多,但蓝牙模块、调试口还是离不开。低功耗设计主要看三点:波特率、停止位、唤醒方式。
3.5 波特率与停止位的选择
波特率越高,传输时间越短,但瞬时功耗也越高。我建议:能低就低。比如调试口用9600bps就够了,别用115200。为什么?因为高波特率意味着更快的时钟,更高的动态功耗。
停止位呢?我习惯用2位停止位。虽然多花一点时间,但容错性更好。尤其是在干扰强的医疗环境中,多一位停止位能减少重传,反而省电。
我的经验数据:
| 波特率 | 传输1字节时间 | 平均功耗(@3.3V) |
|---|---|---|
| 9600 | 1.04ms | 0.15mA |
| 115200 | 0.087ms | 0.45mA |
| 921600 | 0.011ms | 1.2mA |
你看,虽然高波特率传输时间短,但瞬时功耗高,而且CPU被频繁唤醒,整体功耗反而更大。
3.6 UART唤醒机制
UART的唤醒,我建议用硬件唤醒,别用软件轮询。很多MCU支持UART空闲唤醒或地址匹配唤醒。
我曾经做过一个蓝牙贴片,主机通过UART发送唤醒命令。我配置了UART的地址匹配唤醒,只有收到特定地址字节时,MCU才从睡眠中醒来。其他时候,UART接收器处于低功耗监听模式,功耗只有几微安。
// UART地址匹配唤醒配置
void UART_WakeUp_Config(void) {
UART_WakeUpMode = UART_WAKEUP_ON_ADDRESS;
UART_Address = 0x55; // 只有收到0x55才唤醒
UART_AddressDetection = UART_ADDRESS_DETECT_4BIT;
// 使能唤醒功能
UART_WakeUpCmd(ENABLE);
// 进入睡眠前,确保UART接收器仍在工作
UART_ReceiverCmd(ENABLE);
}
避坑指南: 我曾经遇到过,UART唤醒后第一个字节会丢失。原因是唤醒需要时间,MCU还没准备好接收。解决办法是:在唤醒命令后面加一个同步字节,或者用硬件流控(RTS/CTS)。
DMA在低功耗中的应用
DMA是低功耗的好帮手。它能让CPU在数据传输时睡觉,自己干活。
3.7 DMA与ADC的黄金搭档
ADC采样时,如果用CPU去读数据,CPU就得一直醒着。但用DMA呢?ADC转换完,DMA自动把数据搬到内存,CPU可以继续睡。
我做过一个心电贴片,用DMA循环模式采集128个点。CPU只在DMA传输完成中断时醒来一次,处理完数据又睡。整体功耗比用CPU轮询低了40%。
// DMA+ADC循环采样配置
void DMA_ADC_Config(void) {
DMA_Channel = DMA_CHANNEL_0;
DMA_PeripheralBaseAddr = (uint32_t)&ADC_DR;
DMA_MemoryBaseAddr = (uint32_t)adc_buffer;
DMA_BufferSize = 128;
DMA_Mode = DMA_MODE_CIRCULAR; // 循环模式
// 使能DMA传输完成中断
DMA_ITConfig(DMA_IT_TC, ENABLE);
// CPU可以睡了,DMA会自己干活
ADC_DMACmd(ENABLE);
}
关键点: DMA传输完成后,记得在中断里把DMA时钟关了。否则DMA虽然不干活,但时钟还在跑,白白耗电。
GPIO电平与上下拉优化
GPIO看似简单,但低功耗设计里坑最多。我见过太多工程师因为GPIO没处理好,待机电流多了几十微安。
3.8 GPIO电平状态管理
GPIO在睡眠时,电平状态一定要确定。浮空输入是最忌讳的——电平不确定,会导致CMOS管反复翻转,产生漏电流。
我建议:不用的GPIO,要么拉高,要么拉低,别悬空。具体怎么选?看外部电路。
- 如果外部有上拉电阻(比如I2C),就设成输入带上拉
- 如果外部有下拉电阻,就设成输入带下拉
- 如果外部什么都没接,就设成输出,拉低或拉高都行
3.9 上下拉电阻的功耗权衡
上下拉电阻的功耗,很多人会忽略。一个10kΩ的上拉电阻,在3.3V下,静态电流是0.33mA。如果你有10个这样的引脚,光上下拉就耗掉3.3mA。
我一般这样处理:
- 能不用就不用:如果外部电路已经提供了上下拉,内部就设成浮空
- 阻值选大一点:内部上下拉一般40kΩ左右,功耗已经很低了。如果外部必须用,选100kΩ以上
- 动态上下拉:只在需要的时候打开上下拉,平时关掉
我曾经踩过的坑: 有一个项目,GPIO接了外部中断,但忘了设上下拉。结果睡眠时,引脚电平受外界干扰,频繁触发中断,MCU根本睡不踏实。待机电流从5μA飙到了200μA。查了三天,最后发现是GPIO配置的问题。
3.10 GPIO输出电平优化
输出电平也要注意。我习惯在睡眠前,把所有输出引脚设成低电平。为什么?因为高电平输出时,如果外部负载有漏电路径,会额外耗电。
举个例子:你驱动一个LED,睡眠前把引脚拉低,LED自然就灭了。但如果你忘了,引脚还是高电平,LED可能微亮,或者通过其他路径漏电。
void GPIO_SleepConfig(void) {
// 把所有输出引脚拉低
GPIO_WriteLow(GPIOA, 0xFFFF);
GPIO_WriteLow(GPIOB, 0xFFFF);
// 不用的引脚设成模拟模式(功耗最低)
GPIO_Config(GPIO_PIN_8, GPIO_MODE_ANALOG);
GPIO_Config(GPIO_PIN_9, GPIO_MODE_ANALOG);
// 关键引脚保留功能,但确保电平确定
GPIO_Config(WAKEUP_PIN, GPIO_MODE_INPUT, GPIO_PULLDOWN);
}
我的习惯: 每次写完GPIO配置,我都会用万用表量一下睡眠时的引脚电压。如果发现某个引脚电压在1.5V左右(既不是高也不是低),那肯定有问题,赶紧查。
好了,外设低功耗策略就讲到这里。下一章我们聊聊时钟系统的低功耗设计——这可是个大学问。记住一句话:外设就像家里的电器,不用就拔插头。