STC8H系列单片机内置的12位ADC模块,可以说是传统51单片机的一次重大升级。记得我第一次用STC89C52做电压检测时,还得外挂ADC0804芯片,不仅占用PCB空间,调试时还要处理并行总线的干扰问题。而STC8H直接把ADC集成在芯片里,转换速度最高能达到80万次/秒,这个性能对于大多数嵌入式应用已经绰绰有余。
ADC模块有15个输入通道,通过多路复用器分时复用。比如我们要监测多路传感器信号,完全可以用一个ADC轮流采集不同通道。实际项目中我常用P1.0(P10)作为电压检测口,因为这个引脚离ADC模块最近,模拟信号路径最短,受干扰最小。配置时要注意将引脚设为高阻模式(P1M1=0x01; P1M0=0x00),否则内部上拉电阻会影响测量精度。
转换结果的对齐方式很有讲究:左对齐时,ADC_RES寄存器存高8位,ADC_RESL存低4位(实际是低4位有效+4位0填充);右对齐则是ADC_RES存高4位(4位0填充)+低8位在ADC_RESL。新手容易在这里搞混,我的经验是统一用右对齐,这样原始数值处理起来更直观。
查询模式就像你去快递柜取件,得不断刷新页面看快递到没到。具体到ADC采集,就是程序要主动检查ADC_FLAG标志位。我去年做的一个温控项目就用了这种方式,因为对实时性要求不高,每秒采10次就够了。
硬件连接很简单:电位器中间抽头接P10,旋转时产生0-5V可变电压。K1按键接P20,按下时触发采集。核心代码就三个步骤:
c复制ADC_CONTR |= 0x40; // 启动转换(START=1)
while(!(ADC_CONTR & 0x20)); // 等待标志位置1
ADC_CONTR &= ~0x20; // 手动清除标志位
实测时发现个坑:如果不清除标志位,下次转换根本不会启动。有次调试卡了一下午,最后发现是漏了这行清除代码。转换结果处理也有技巧,12位数据要拼接成整型:
c复制value = ADC_RES * 256 + ADC_RESL; // 右对齐时的高4位+低8位
查询模式最大优点是代码简单,但缺点很明显——CPU要不停轮询。我在功耗敏感的项目中实测过,持续查询时MCU电流会多耗3-5mA。所以这种模式适合简单场景,比如按键触发单次测量,或者对实时性要求不高的周期性采集。
中断模式就像快递员按门铃,货物到了主动通知你。在需要实时响应的场景,比如电池电压突变检测,中断模式就大显身手了。去年给无人机做的电压监控系统,就是用ADC中断实现的低电压预警。
配置中断要五个关键步骤:
具体代码实现:
c复制void init_ADC() {
ADCCFG = 0x20; // 右对齐+1MHz采样时钟
ADC_CONTR = 0x80; // 开启ADC电源
EA = EADC = 1; // 双中断使能
}
void ADC_ISR() interrupt 5 {
ADC_CONTR &= ~0x20; // 必须清除标志位
uint16_t val = (ADC_RES << 8) | ADC_RESL;
printf("ADC=%d\n", val);
// 如需连续采集需加上:ADC_CONTR |= 0x40;
}
中断方式最精妙的是能与其他任务并行执行。我在一个LED调光项目中,主循环处理PWM输出,ADC中断处理电位器采样,两者互不干扰。但要注意中断服务函数要尽量短,像浮点运算、复杂打印这些耗时操作最好放到主循环。
经过多个项目实战,我总结出这张对比表:
| 特性 | 查询模式 | 中断模式 |
|---|---|---|
| CPU占用 | 高(持续轮询) | 低(异步通知) |
| 实时性 | 依赖查询频率 | 立即响应 |
| 代码复杂度 | 简单直接 | 需处理中断上下文 |
| 适用场景 | 单次触发/低频采集 | 连续采集/紧急事件 |
| 功耗表现 | 较差 | 优(可配合休眠模式) |
有个经典案例:我用STC8H做智能花盆,土壤湿度检测用查询模式(每小时采一次),而水位报警用中断模式。这样既省电又能及时响应危险情况。
实际选择时还要考虑转换速度。STC8H的ADC时钟可以分频,最快1MHz(ADCCFG=0x20)。对于50Hz工频干扰环境,建议采样周期设为20ms的整数倍。我曾用查询模式实现过50Hz工频抑制,关键代码:
c复制ADCCFG = 0x28; // 时钟分频到250kHz
for(int i=0; i<16; i++){
ADC_Start();
Delay1ms(1); // 精确控制采样间隔
}
ADC调试最常遇到三个问题:数值跳动、响应延迟、电源干扰。分享几个实测有效的技巧:
数值跳动问题:硬件上在输入脚加0.1uF电容,软件上做滑动滤波。我常用的递推平均滤波算法:
c复制#define FILTER_LEN 8
uint16_t filter_buf[FILTER_LEN];
uint16_t adc_filter(uint16_t new_val) {
static uint8_t index = 0;
filter_buf[index++] = new_val;
if(index >= FILTER_LEN) index = 0;
uint32_t sum = 0;
for(int i=0; i<FILTER_LEN; i++){
sum += filter_buf[i];
}
return sum / FILTER_LEN;
}
降低电源干扰的方法:
有个隐蔽的坑:STC8H的ADC参考电压默认接VCC。有次产品批量出现测量偏差,最后发现是LDO输出不稳导致的。后来改用内部1.19V基准,通过分电阻测量VCC再反推输入电压,精度明显提升。
对于多通道采集,切换通道后要加延时。实测从通道0切到通道1,需要至少5us稳定时间。我的做法是:
c复制ADC_CONTR = (ADC_CONTR & 0xF8) | 0x01; // 切到通道1
_nop_(); _nop_(); _nop_(); // 12MHz下约3us
虽然STC8H没有硬件DMA,但我们可以用中断+缓冲区模拟。比如要做语音采集时,我设计了环形缓冲区方案:
c复制#define BUF_SIZE 256
volatile uint16_t adc_buf[BUF_SIZE];
volatile uint16_t wr_index = 0;
void ADC_ISR() interrupt 5 {
ADC_CONTR &= ~0x20;
adc_buf[wr_index++] = ADC_RES << 8 | ADC_RESL;
if(wr_index >= BUF_SIZE) wr_index = 0;
ADC_CONTR |= 0x40; // 连续采集
}
主程序可以非阻塞地处理缓冲区数据。这种模式在8kHz采样率下工作稳定,CPU占用仅15%。如果再配合STC8H的SPI接口传输数据,完全可以实现简易示波器功能。
最后分享一个电压换算的实用函数,包含校准系数处理:
c复制float adc_to_voltage(uint16_t adc_val, float vref) {
const float scale = vref / 4095.0; // 12位分辨率
const float offset = 0.02; // 校准偏移
return adc_val * scale + offset;
}
在电池供电产品中,我还会在ADC初始化前做基准电压自校准:
c复制void adc_self_calibrate() {
ADC_CONTR = 0x8F; // 选择内部1.19V基准
Delay1ms(10); // 稳定时间
uint16_t cal_val = ADC_Start();
g_scale_factor = 1.19 / (cal_val / 4095.0);
}