ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM8S003多通道ADC采集实战:扫描模式、轮询与双基准校正

2026/9/16 3:55:16 拓冰建站 浏览量
STM8S003多通道ADC采集实战:扫描模式、轮询与双基准校正 简介面向STM8S003单片机学习者的多通道ADC采集示例工程以“单片机轮询法”为主线完整演示了ADC初始化、工作模式与参数配置、通道选择、启动转换、轮询等待状态标志以及读取转换结果的完整流程适合嵌入式初学者、课程设计或需要快速上手STM8 ADC开发的工程师参考。资源包内含8个文件全部为C语言源码4个.c与4个.h覆盖主程序、IO配置、任务调度与中断处理等模块整体约11KB结构精简、层次清楚便于按模块阅读和复用。已有652人学习。项目场景为自动调光控制通过ADC采集环境光照强度并依据数值调节亮度相关源码对多通道采集顺序管理、轮询与中断方式对比、采样噪声抑制以及电源与时钟配置等实际工程关键点均有直接体现可帮助读者深入理解STM8S003外设编程并将其迁移到数据采集系统、传感器接口等类似应用中。1. 为什么 STM8S003 的多通道 ADC 采集要单独设计这个标题拆开就是一个光控项目包:AutoLight 做自动灯控,20180606 是打包日期,后面粘着 STM8 多通道 ADC 的检索词。这类 zip 里的代码大多能跑,但真正值钱的不是文件本身,而是它在追问一个问题:STM8S003 上怎么把多路模拟量可靠采回来。我的第一个建议是放下 STM32 的习惯——003 没有 DMA,没有 CubeMX 图形配置,连内部基准源都没有,stm32cubemx 里常见的 ADC 多通道加 DMA 采集那套,在 003 上无从谈起。正确做法是硬件扫描模式加数据缓冲寄存器,或者逐通道轮询。下面从通道规划、两种实现写到 IAR 工程和双基准校正,新手能照着跑通,老手也能看到边界和坑。2. STM8S003 多通道 ADC 的资源盘点与前端通道规划2.1 先认清家底:10bit、5 通道、参考电压就是 VDDSTM8S003F3P6 这颗 TSSOP20 封装里只有一个 ADC1,逐次逼近型,分辨率 10bit。外部模拟输入端一共 5 路:AIN0~AIN4,引脚分别是 PD2~PD6。003 不带内部温度传感器,也不带内部基准通道,ADC 的参考电压直接就是 VDD。这句话决定了后面很多设计:电源纹波会直接变成测量误差,分压网络的供电也必须从 VDD 取,这样读数才是比例值,天然免疫一部分电源波动。多通道 ADC 采集的第一步不是写代码,而是把通道表钉死。以 AutoLight 光控为例,我一般按下面这样分配:ADC 通道引脚AutoLight 场景用途前端形式AIN0PD2环境光强(主判据)LDR 光敏电阻 10k 分压AIN1PD3面板电位器(手动阈值)10k 电位器分压AIN2PD4电源电压监测100k/20k 电阻分压AIN3PD5备用传感器预留AIN4PD6备用传感器预留通道表先定下来,后面扫描模式的 CH 参数、轮询的通道宏、校正通道全部从这张表里派生,不要在写代码的过程中临时加通道,不然两块板子的固件很容易对不上。时钟关系也要在规划阶段算清楚:fMASTER 是 16MHz 的 HSI,芯片手册要求 ADC 时钟不超过 4MHz,所以 SPSEL 分频最小选 /4,也就是 D4。一次 10bit 转换大约需要 12 到 14 个 ADC 时钟,按 4MHz 算就是 3 到 3.5µs。单通道非常快,多通道的瓶颈其实在引脚状态和前端电路阻抗上,不在 ADC 本身。2.2 寄存器与 SPL 函数:多通道操作就四个寄存器RM0016 里 ADC1 的寄存器不多,多通道用得上的只有几个:CSR、CR1、DR 和 DB。ST 官方 SPL(标准外设库)把它们封装成了 ADC1_Init、ADC1_StartConversion、ADC1_GetConversionValue、ADC1_GetBufferValue 这几个函数,多通道采集的一切排列组合,说到底就是从这些函数里挑出一组来用。寄存器关键位在多通道采集里的作用ADC_CSREOC(bit7)、CH 3:0EOC 是转换结束标志;CH 选择通道,扫描模式下写最高通道号ADC_CR1SPSEL 3:0 、SCAN(bit7)、CONT(bit6)、ALIGN(bit5)ADC 分频、扫描使能、连续使能、数据对齐ADC_DRH/DRL10bit 结果最近一次转换的原始值ADC_DB0R~DB4R各通道结果扫描模式下通道 i 的结果固定存在 DBiSPL 里三个读函数要分清:ADC1_GetConversionValue 读 DR,ADC1_GetBufferValue(通道) 读 DB,ADC1_GetFlagStatus(ADC1_FLAG_EOC) 查转换是否完成。很多从别的 MCU 移植过来的代码,习惯性地只读 DR,在扫描模式下拿到的是最后一个完成通道的值,这就是多通道必须用数据缓冲寄存器的原因。2.3 引脚配置与前端电路:通道串扰大部分是电路问题STM8S003 的 GPIO 没有模拟输入模式,标准做法是配成输入浮空,浮空输入不会把分压网络拉歪。初始化代码:/* PD2~PD6 全部配成输入浮空, 对应 AIN0~AIN4 */ GPIO_Init(GPIOD, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6, GPIO_MODE_IN_FL_NO_IT);GPIO_MODE_IN_FL_NO_IT 是 SPL 宏,含义是输入浮空、不使能外部中断。STM8 的 GPIO 没有 ANALOG 选项,不要画蛇添足去配上拉或下拉:上拉会抬高 LDR 分压点的电压,下拉会把读数压低,两种都会造成阈值偏移。前端电路上,光敏电阻典型接法是 VDD 到 LDR 到采样点再到 10k 到 GND,采样点并一个 0.1µF 瓷片电容到地。采样电容要在 3µs 量级的窗口里被充满,源阻抗越高需要的并联电容越大,这也是高阻分压网络读数偏低的根源。电源电压监测用 100k 和 20k 分压,把最高 24V 缩到 4V 以内,采样点等效阻抗约 16.7k,并电容后读数才跟得上。没用到的 AIN 引脚别悬空,直接接地,或者配成推挽输出低电平,悬空引脚会把噪声串进相邻通道。3. 扫描模式 DB 缓冲:STM8 多通道 ADC 采集的最小方案3.1 没有 DMA 时,扫描模式就是 003 的“自动搬数据”STM8S003 没有 DMA 外设,但有硬件扫描。把 ADC_CR1 的 SCAN 位置 1,再把 ADC_CSR 的 CH[3:0] 写成最高通道号,ADC 就会从通道 0 开始一路采到 CH 指定的通道,结果按通道号自动落到对应的数据缓冲寄存器。这就是 003 上性价比最高的多通道 ADC 采集方式:CPU 负责启动,之后完全不用管搬运,主循环随时读 DB,拿到的就是最近一轮的结果。如果初始化时把 CONT 也置 1,ADC 会不停循环扫描,等效于用硬件做了一个不需要中断服务的“伪 DMA”。时序账要算清楚:ADC 时钟 16MHz 除以 4 等于 4MHz,每路约 3µs,5 路一轮约 15µs。AutoLight 这类光控任务做到 100Hz 刷新绰绰有余,就算加上一阶滤波,两次读取之间 CPU 仍有大量空闲去跑按键、看门狗和继电器逻辑。分频还有讲究:通道少、求实时可以用 D4;前端电路阻值大、采样电容充不满,就把 SPSEL 放到 D6 甚至 D8,转换周期变长,读数跳动反而更少。3.2 最小可跑代码:连续扫描 5 通道/* 扫描模式: 连续采集 AIN0~AIN4, 结果自动落入 DB0~DB4 */ void ADC_Scan_Init(void) { ADC1_DeInit(); /* 复位 ADC1, 清掉残留状态 */ /* 连续模式, 最高通道 4, 16MHz / 4 4MHz ADC 时钟 */ ADC1_Init(ADC1_CONVERSIONMODE_CONTINUOUS, ADC1_CHANNEL_4, ADC1_PRESSEL_FCPU_D4, ADC1_EXTTRIG_GPIO, DISABLE); ADC1-CR1 | ADC1_CR1_SCAN; /* 打开扫描模式 */ ADC1_StartConversion(); /* ADON1, 开始循环扫描 */ } static const ADC1_Channel_TypeDef ch_list[5] { ADC1_CHANNEL_0, ADC1_CHANNEL_1, ADC1_CHANNEL_2, ADC1_CHANNEL_3, ADC1_CHANNEL_4 }; uint16_t adc_raw[5]; /* 保存最近一轮的 5 通道读数 */ void ADC_Scan_Snapshot(void) { uint8_t i; for (i 0; i 5; i) { adc_raw[i] ADC1_GetBufferValue(ch_list[i]); } }ADC1_Init 的第二个参数 ADC1_CHANNEL_4 不是“只采通道 4”,而是“扫描到最高通道 4 为止”,这是 SPL 语义里最容易误解的一点。SCAN 位不在 Init 的参数里,要单独对 CR1 置位。ADC1_StartConversion 实际上是置 CR2 的 ADON 位,之后 ADC 会按照 CR1 里 CONT 和 SCAN 的组合持续工作。读通道 i 必须用 ADC1_GetBufferValue,它访问的是 DB 寄存器,和 GetConversionValue 读的 DR 不是一回事,混用会拿到错误通道的数据。3.3 扫描模式的边界:4 个新手必踩的点边界场景表现处理方式只想采 AIN2、AIN4,跳过 0/1/3扫描模式强制从通道 0 连续扫起改轮询模式,或把不用的通道贴 GND连续扫描中写 CSR 换通道当前转换被中止,数据错乱先 StopConversion,通道号只初始化一次读 DB 时扫描仍在进行5 个值可能跨两轮,时间戳不一致要求一致性就停 ADON 后读,读完重启上电后立刻读 DB第一轮数据是无效值启动后等一个完整扫描周期,丢弃第一次快照提示:STM8S 的 ADC1 数据缓冲寄存器物理上有 10 个,但 003 只引出 5 个通道,不要读 DB5 及之后的位置,那属于保留区,行为随芯片批次而定。应用里不用的通道,引脚一定要处理,要么接 GND 要么配成输出低,这也是扫描模式比轮询更怕悬空引脚的原因,因为在扫描循环里那颗悬空通道每轮都会被转换一次。4. 逐通道轮询与 IAR for STM8 工程的落地细节4.1 什么时候该放弃扫描模式:轮询的适用场景与硬件规则扫描模式很省心,但不是万能的。三个场景我会改成轮询:第一,各通道采样率不同,电池电压 1Hz 采一次就够,环境光要 10Hz 以上,扫描模式只能按同一个节奏循环;第二,想在两次转换之间穿插业务代码,扫描模式的转换在后台跑,快照时刻和业务逻辑对不齐;第三,板上只要两三个通道,但需要精确控制某一通道的采样时刻。轮询模式的核心规则只有一条:写 CSR 换通道会中止当前进行的转换,所以每换一次通道都要“先停、再换、再启、等 EOC、清标志”五步走。EOC 在 STM8S 上要软件清零,如果使能了 EOCIE,清标志的时机错了还会丢掉一次中断,表现为偶尔漏掉一次采集。规则不复杂,但顺序错了很难查,因为表现很像“通道串扰”。4.2 轮询代码:五步切换法/* 单次轮询: 采集指定通道一次, 返回 0~1023 */ uint16_t ADC_Poll_Channel(uint8_t ch) { uint16_t val; /* 1. 停转换: 防止写 CSR 时打断正在进行的转换 */ ADC1_StopConversion(); /* 2. 换通道: 保留 CSR 高 4 位, 写入新通道号到 CH[3:0] */ ADC1-CSR (uint8_t)((uint8_t)(ADC1-CSR 0xF0) | (ch 0x0F)); /* 3. 清掉上次遗留的 EOC 标志 */ ADC1_ClearFlag(ADC1_FLAG_EOC); /* 4. 启动一次转换 */ ADC1_StartConversion(); /* 5. 等 EOC, 读 DR, 再清一次 EOC 供下次使用 */ while (ADC1_GetFlagStatus(ADC1_FLAG_EOC) RESET); val ADC1_GetConversionValue(); ADC1_ClearFlag(ADC1_FLAG_EOC); return val; }初始化时用 ADC1_Init 把转换模式设为 ADC1_CONVERSIONMODE_SINGLE,并且确认 2.3 节的引脚配置已经完成。先 stop 再 start,是为了让 CR2 的 ADON 位产生一个上升沿,ADC 才真正开始新一次转换。如果上一个循环忘了 StopConversion 就直接写 CSR,当前转换被意外中止,读回来的值是上一轮旧数据,排查起来很像通道间串扰。轮询模式下每个通道的采样间隔由主循环节奏决定,要做固定频率就把这个函数放进定时器中断。4.3 IAR for STM8 工程落地:配置、定义与三个坑IAR Embedded Workbench for STM8 是最常见的开发环境,另外还有 SDCC 和 Cosmic,但团队项目里 IAR 的调试体验最好。工程步骤不多,每一条都会卡人。4.3.1 工程与预处理器配置新建工程后,在 General Options 的 Device 里选 STM8S003F3,然后在 C/C Compiler 的 Preprocessor 里 Define 加 STM8S003。SPL 的 stm8s.h 靠这个宏决定 include stm8s003.h,漏掉它编译会直接报错,错误信息会明确提示先定义芯片型号。需要加入编译的 SPL 源文件至少是 stm8s_adc1.c、stm8s_gpio.c、stm8s_clk.c,用到串口打印还要加 stm8s_uart1.c。4.3.2 调试器、烧录与 option bytes调试器选 ST-LINK,接口选 SWIM,IAR 里可以直接在线调试。量产或离线烧录,常见做法是用轩微科技那种 SWIM 编程器,把 IAR 生成的 hex 灌进去,烧之前先读回 option bytes 确认状态。003 有两个出名坑:一是 option bytes 里 IWDG 默认由硬件使能,程序没初始化看门狗会周期性复位,现象是上电几秒就重启;二是 PD4/PD5/PD6 的复用功能受 AFR 这个 option byte 控制,做 ADC 时要保持默认映射,别为了复用 TIM 或 UART 去拨 AFR,拨完这几路模拟输入就没了。提示:IAR 的 int 是 16 位,unsigned long 才是 32 位。所有累加、乘除运算只要可能超过 32767,就必须显式用 UL 后缀,这是 STM8 工程里最常见的隐蔽 bug 来源。4.4 滤波与排错:读数跳、通道串、全满量程ADC 裸值直接拿去判阈值会抖,光控场景最常用一阶低通,系数按响应需求调:uint16_t adc_filt[5]; /* 一阶低通: 新值占 1/4, 历史值占 3/4 */ void ADC_Filter_Update(uint8_t ch, uint16_t raw) { adc_filt[ch] (uint16_t)(((uint32_t)adc_filt[ch] * 3UL raw) 2); }右移两位等价于除以 4,所以整行表达式就是四分之三旧值加四分之一新值。中间量必须用 32 位,因为 16 位乘法在 IAR 下会先截断再赋值,结果偏小且难排查。这个滤波对 50Hz 灯光频闪和触点噪声衰减明显,对手电筒照射这种阶跃输入又足够快,大约 4 个采样周期完成过渡。现象可能原因对策所有通道恒为 0x3FF 或恒为 0引脚模式不是浮空输入、分压网络断线查 GPIO_Init,用万用表量采样点电压读数明显偏低且非线性采样点源阻抗太高,采样电容充不满并 0.1µF 到 1µF 电容,或改低阻分压相邻通道读数跟着动前级串扰或地弹未用通道接地,模拟地单点接入周期性跳动,和电源同步VDD 纹波,而参考电压就是 VDD电源并 100µF 加 0.1µF,或放慢 SPSEL上电几秒读数乱飞DB 或滤波初值未定义启动后先采集 20 轮再启用判据5. 双基准点校正与滞回光控:多通道 ADC 读数的落地技巧5.1 空出两路通道,给 ADC 做基准点校正分压测量天然是比例式的,所以 LDR 判据用百分比就能免疫很大一部分 VDD 波动,不需要外接基准源。如果 5 路里只有 3 路是业务信号,我习惯把 AIN3 接模拟地、AIN4 接 VDD,接在靠近 0.1µF 电容的位置,让 ADC 实时看到自己的零点和满点,做两点校正,把 ADC 自身的 offset 和增益误差剔掉:#define CH_LDR 0 #define CH_POT 1 #define CH_BAT 2 #define CH_GND 3 #define CH_VDD 4 uint16_t adc_raw[5]; /* 双基准校正: 把原始值按基准通道归一到 0~1023 */ uint16_t ADC_Calibrate(uint16_t raw) { uint16_t zero adc_raw[CH_GND]; uint16_t full adc_raw[CH_VDD]; uint32_t y; if (full zero) return 0; /* 基准通道异常保护 */ y (uint32_t)(raw - zero) * 1023UL / (uint32_t)(full - zero); return (y 1023UL) ? 1023 : (uint16_t)y; } /* 滞回光控: 暗到 35% 开灯, 亮到 45% 关灯 */ uint8_t lamp_state 0; void AutoLight_Task(void) { uint16_t ldr ADC_Calibrate(adc_raw[CH_LDR]); if (lamp_state 0 ldr 358) lamp_state 1; /* 1023 * 0.35 */ else if (lamp_state 1 ldr 460) lamp_state 0; /* 1023 * 0.45 */ }zero 是 ADC 在输入接地时的原始读数,正常在 0 到 5 之间;full 是输入接近 VDD 时的读数,受芯片增益偏差影响,可能在 1000 到 1023 之间。校正后的 ldr 是把 VDD 和 GND 视为理想端点的归一化光强,数值越大越亮。滞回的用途是防止黄昏临界点反复开关灯,两个阈值之间那 10% 的差就是防抖带宽,它保护的是继电器或灯头的电气寿命。5.2 验证手段:看通道隔离,再做现场标定调试时在 IAR 的 Watch 窗口里加 adc_raw[5],用手电筒照 LDR,应该看到 adc_raw[0] 跟随变化,另外四路几乎不动,这一步验证的是通道隔离;再用可调电源改变 VDD,观察 CH_GND 和 CH_VDD 读数跟着变,而 ADC_Calibrate 的输出基本不变,同时验证了基准通道接线和校正算法。现场标定时,遮住 LDR 读一个暗态值,开灯档位读一个亮态值,把这两个值换算成百分比写进滞回区间;如果现场光环境特殊,还可以把 AIN1 的电位器当成在线可调阈值,CH_POT 经过校正后直接和 ldr 比较,省去反复烧录调参。把 CH_GND 接到干净的模拟地、CH_VDD 就近接在 VDD 的 0.1µF 电容脚上,校正效果会比随便飞线再高一截。本文还有配套的精品资源点击获取