4. ADC驱动开发:单次转换与连续转换模式、DMA传输、内部温度传感器读取
ADC驱动,说白了就是让芯片的“眼睛”能看清外面的模拟世界。医疗贴片里,ADC用得特别多——测皮肤阻抗、检测生物电信号、监控电池电压,哪样都离不开它。今天咱们就聊聊ADC驱动的几个核心玩法。
4.1 单次转换模式:按需采样,省电为王
单次转换,就是“叫一声动一下”。你触发一次,它采一次。采完就睡,功耗极低。
我个人习惯在电池供电的贴片设备里大量使用单次模式。比如体温监测,每10秒采一次就够了,没必要让ADC一直开着。
核心配置要点:
- 选择软件触发或外部引脚触发
- 配置采样时间(根据信号源阻抗调整)
- 使能转换结束中断(EOC)
// 单次转换配置示例(基于STM32G0系列)
void ADC_SingleConversion_Init(void)
{
// 使能ADC时钟
RCC->APBENR2 |= RCC_APBENR2_ADCEN;
// 校准ADC(这一步不能省,我踩过坑)
ADC1->CR |= ADC_CR_ADCAL;
while(ADC1->CR & ADC_CR_ADCAL);
// 配置为单次转换模式
ADC1->CFGR1 &= ~ADC_CFGR1_CONT; // 清除连续模式位
// 配置通道0(PA0引脚)
ADC1->CHSELR = ADC_CHSELR_CHSEL0;
// 设置采样时间:2.5个ADC时钟周期
ADC1->SMPR = ADC_SMPR_SMP_2P5;
// 使能ADC
ADC1->CR |= ADC_CR_ADEN;
while(!(ADC1->ISR & ADC_ISR_ADRDY));
}
// 触发单次转换并读取结果
uint16_t ADC_ReadSingle(void)
{
ADC1->CR |= ADC_CR_ADSTART; // 启动转换
while(!(ADC1->ISR & ADC_ISR_EOC)); // 等待转换完成
return (uint16_t)ADC1->DR; // 读取数据寄存器
}
经验之谈: 单次转换的启动到完成时间是可以精确计算的。比如ADC时钟14MHz,采样时间2.5周期,12位分辨率需要12.5个周期,总共15个周期,大约1.07μs。这个时间在实时性要求高的场景下很有参考价值。
4.2 连续转换模式:数据流不停歇
连续模式就是ADC自己不停地采,采完一个马上开始下一个。适合监测快速变化的信号,比如心电信号或肌电信号。
我记得有一次做动态心电贴片,信号采样率需要1000Hz。如果用单次模式,CPU得频繁被中断打断,干不了别的活。换成连续模式+DMA,CPU几乎零负担。
// 连续转换模式配置
void ADC_ContinuousConversion_Init(void)
{
// 基础配置同单次模式...
// 关键区别:使能连续模式
ADC1->CFGR1 |= ADC_CFGR1_CONT;
// 使能DMA请求(后面会细讲)
ADC1->CFGR1 |= ADC_CFGR1_DMAEN;
// 使能ADC
ADC1->CR |= ADC_CR_ADEN;
while(!(ADC1->ISR & ADC_ISR_ADRDY));
// 启动连续转换
ADC1->CR |= ADC_CR_ADSTART;
}
注意: 连续模式下,ADC会一直占用总线。如果你的系统对功耗敏感,记得在不采样时关闭ADC或切换到低功耗模式。我曾经有个项目,就是因为忘了关连续模式,电池续航直接砍半。
4.3 DMA传输:让数据自己跑起来
DMA(直接存储器访问)是个好东西。它能让ADC采集的数据直接存到内存里,CPU完全不用管。你想想看,CPU只需要在缓冲区满了之后处理数据,中间的时间可以去做算法运算、控制显示、管理通信——这才是嵌入式系统的正确打开方式。
我建议在医疗贴片里,只要采样率超过100Hz,就上DMA。否则CPU会被中断淹死。
// ADC + DMA 配置示例
#define ADC_BUFFER_SIZE 256
uint16_t adc_buffer[ADC_BUFFER_SIZE];
void ADC_DMA_Init(void)
{
// 1. 配置ADC(连续模式)
ADC_ContinuousConversion_Init();
// 2. 配置DMA
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 配置DMA通道1(ADC1的DMA请求)
DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR); // 外设地址
DMA1_Channel1->CMAR = (uint32_t)adc_buffer; // 内存地址
DMA1_Channel1->CNDTR = ADC_BUFFER_SIZE; // 传输次数
// 配置传输方向:外设到内存
DMA1_Channel1->CCR = DMA_CCR_MINC | // 内存地址递增
DMA_CCR_MSIZE_0 | // 内存数据宽度16位
DMA_CCR_PSIZE_0 | // 外设数据宽度16位
DMA_CCR_CIRC; // 循环模式(关键!)
// 使能DMA传输完成中断
DMA1_Channel1->CCR |= DMA_CCR_TCIE;
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
// 使能DMA
DMA1_Channel1->CCR |= DMA_CCR_EN;
}
// DMA传输完成中断回调
void DMA1_Channel1_IRQHandler(void)
{
if(DMA1->ISR & DMA_ISR_TCIF1)
{
DMA1->IFCR = DMA_IFCR_CTCIF1; // 清除标志
// 缓冲区已满,处理数据
Process_ADCBuffer(adc_buffer, ADC_BUFFER_SIZE);
}
}
DMA循环模式的好处:
- 缓冲区自动回绕,无需手动重置
- 配合双缓冲技术,可以实现“零拷贝”数据处理
- CPU几乎零开销,适合高采样率场景
4.4 内部温度传感器读取:芯片自带的温度计
大多数MCU内部都集成了一个温度传感器。虽然精度不高(通常±2°C到±5°C),但用来监测芯片结温、判断是否过热,完全够用。
我在做一款胰岛素泵贴片时,就用内部温度传感器来检测设备是否贴附在皮肤上——因为人体皮肤温度通常在32-37°C,如果传感器读到室温(25°C以下),就判断贴片脱落了。这个功能救过好几个测试样品。
// 读取内部温度传感器
float Read_InternalTemperature(void)
{
uint16_t adc_value;
float temperature;
// 1. 选择内部温度传感器通道(不同芯片通道号不同)
// STM32G0系列内部温度传感器在通道12
ADC1->CHSELR = ADC_CHSELR_CHSEL12;
// 2. 设置较长的采样时间(内部传感器需要更长的采样时间)
ADC1->SMPR = ADC_SMPR_SMP_39P5; // 39.5个ADC时钟周期
// 3. 启动转换
ADC1->CR |= ADC_CR_ADSTART;
while(!(ADC1->ISR & ADC_ISR_EOC));
adc_value = (uint16_t)ADC1->DR;
// 4. 转换为温度值(公式来自芯片参考手册)
// TS_CAL1: 30°C时的校准值(存储在系统存储器)
// TS_CAL2: 130°C时的校准值
// VDD: 供电电压(3.3V)
// 温度 = (TS_CAL2 - TS_CAL1) / 100 * (adc_value - TS_CAL1) + 30
// 读取出厂校准值
uint16_t ts_cal1 = *((uint16_t*)0x1FFF75A8); // 30°C校准值
uint16_t ts_cal2 = *((uint16_t*)0x1FFF75CA); // 130°C校准值
temperature = (float)(ts_cal2 - ts_cal1) / 100.0f *
(float)(adc_value - ts_cal1) + 30.0f;
return temperature;
}
校准值的秘密: 每个芯片出厂时都会在系统存储器里烧录两个温度点的ADC值。一定要用这两个值做线性校正,否则误差会很大。我见过有人直接用公式算,结果读出来50°C,芯片明明才30°C——就是因为没读校准值。
4.5 避坑指南与实战建议
做ADC驱动这么多年,踩过的坑不少。挑几个典型的说说:
- 采样时间不够: 信号源阻抗高时,采样时间要加长。我曾经用10kΩ的传感器,采样时间设成1.5周期,结果ADC值跳得像心电图——其实是采样电容没充满。
- 参考电压不稳定: 医疗贴片常用电池供电,电压会波动。建议用内部参考电压(如1.2V)或外置基准源,否则测量值会随电池电压漂移。
- DMA缓冲区溢出: 如果DMA传输速度比CPU处理速度快,缓冲区会被覆盖。解决办法是用双缓冲或环形缓冲区,并确保中断处理足够快。
- 内部温度传感器自热: ADC工作时会发热,连续转换模式下芯片温度会上升。如果要做精确测温,建议用单次模式,或者做温度补偿。
重要提醒: 医疗设备对ADC的精度和稳定性要求极高。量产前一定要做全温区标定,并考虑ADC的DNL(微分非线性)和INL(积分非线性)误差。这些参数在芯片数据手册里都有,别偷懒不看。
好了,ADC驱动这块就聊到这儿。下一章咱们会讲定时器驱动,那是实现PWM输出和精确时序控制的关键。到时候见。