把 STM8S 的 ADC 玩明白:一个连续采集的ADC项目
作者: 404是NotFound呀
日期: 2026/05/31
基于参考手册 RM0016 Rev 14, 第 24 章
项目背景: 一个用 STM8A 控制的项目,需要同时采样多路模拟信号
写在前面
最近在做一个xxx驱动项目,需要用 STM8S 同时采集多路模拟信号。翻开 RM0016 参考手册第 24 章,对着那几十页的寄存器描述和时序图啃了半小时。说实话,ST 这份手册写得不算挺好。
所以我决定把自己踩过的坑、搞明白的东西整理成这篇笔记。如果你也在用 STM8S 的 ADC,希望这篇能帮你少走些弯路。
先说结论:五个模式怎么选
STM8S 的 ADC 有五种转换模式。只需要记住三个控制位:CONT(连续)、SCAN(扫描)、DBUF(缓冲),它们的组合就决定了模式:
| 你想干嘛 | CONT | SCAN | DBUF | 简单一句话 |
|---|---|---|---|---|
| 偶尔读一个通道 | 0 | 0 | 0 | 转一次就停 |
| 不停地读一个通道 | 1 | 0 | 0 | 转完又转,但数据会被覆盖 |
| 不停地读一个通道,还要存历史 | 1 | 0 | 1 | 结果存进缓冲区,可以做平均滤波 |
| 一次性扫多个通道 | 0 | 1 | x | AIN0 到 AINn 轮一遍 |
| 不停地扫多个通道 | 1 | 1 | x | 一轮扫完自动下一轮 |
我的项目选的是单次扫描模式(CONT=0, SCAN=1),因为我需要同时采集 6 路反馈信号,但不需要连续不停地采——主循环里按需触发一次就够了。
1. 先搞清楚你手上有什么
STM8S 系列一般有两个 ADC 模块:ADC1 和 ADC2。都是 10 位 SAR(逐次逼近型),最多 16 路复用输入。
但这里有个关键区别:ADC1 是"完全体",ADC2 是"青春版"。
ADC2 只有基本的单次和连续转换,没有扫描模式、没有数据缓冲、没有模拟看门狗。所以如果你的应用涉及多通道采样,基本都得用 ADC1。我项目里也是只用了 ADC1。
小贴士: 具体有多少通道取决于你的芯片型号和封装,不是所有 16 个通道都引出来了。一定要查你那颗芯片的 datasheet pin description 表。
ADC 的引脚
这里容易被忽略的是参考电压引脚:
- 小封装(48脚及以下): V_REF+ 和 V_REF- 在芯片内部直接连到了 V_DDA 和 V_SSA,没有外部引脚。也就是说参考电压就是供电电压,没法调。
- 大封装(64脚以上): 有独立的 V_REF+ 和 V_REF- 引脚,可以外接精密参考电压源,提高采样精度。
如果你用小封装,采样精度就取决于你的电源质量。供电纹波大,ADC 读数就抖。这是硬件设计时就要考虑的事情。
2. ADON 位:一个位干两件事
这是我刚开始觉得最反直觉的地方。ADC_CR1 寄存器的 bit 0 叫 ADON,它的行为是这样的:
第一次写 ADON=1 → 唤醒 ADC(从掉电模式) 第二次写 ADON=1 → 启动一次转换 之后每次写 ADON=1 → 再启动一次转换我猜是为了省电。ADC 上电需要稳定时间(大约一个转换周期),所以唤醒和启动转换被分成了两步。你需要先唤醒它,等它稳定,然后才能真正开始采样。
实际使用中要注意两点:
- 上电前先选好通道。因为 ADON=1 之后,选中通道的数字 I/O 会被自动禁用。如果你先上电再选通道,可能会有一个短暂的窗口期引脚状态不确定。
- 不要顺手在写 ADON 的同时改 CR1 的其他位。手册明确说了,如果同一次写操作同时修改了 ADON 和其他位,转换不会被触发。这是为了防止你改分频系数的时候误触发一次转换,但如果你不知道这个设计,就会很纳闷"为什么我的 ADC 启动不了"。
3. 时钟和预分频
ADC 时钟由主时钟 f_MASTER 分频而来,分频系数通过 CR1 的 SPSEL[2:0] 配置,从 /2 到 /18。
SPSEL=000 → fMASTER/2 SPSEL=001 → fMASTER/3 ... SPSEL=111 → fMASTER/18一个实际建议:如果你要改分频系数,最好在 ADC 掉电时改(ADON=0 时)。否则时钟切换瞬间可能产生毛刺,导致第一次转换结果不准。如果你非要在 ADC 运行时改,那就忽略改完后的第一个转换结果。
每次 ADC 转换需要 14 个时钟周期。假设 f_MASTER = 16MHz,SPSEL=100 (/8),则 ADC 时钟 = 2MHz,一次转换需要 14/2MHz = 7us。这在大多数应用场景下足够快了。
4. 五种转换模式:逐一拆解
4.1 单次转换模式 — 最简单的一个
配置: CONT=0, SCAN=0
这是最基础的模式。你选好一个通道,写 ADON=1,它就转一次,转完后 EOC 标志置位,然后歇着等你下一次吩咐。
选通道 → ADON=1 → 等 EOC → 读数据 → 完事适合场景:偶尔读一下电池电压、芯片温度这种不需要高频采样的东西。
4.2 连续转换模式 — 停不下来
配置: CONT=1, SCAN=0
设了 CONT=1 之后,ADC 就像上了发条一样,转完一次马上转下一次,中间不停。但问题来了——数据只存在 ADC_DR 这一对寄存器里,新结果会直接覆盖旧结果。
ADON=1 → 转 → 存数据 → 转 → 覆盖数据 → 转 → 又覆盖 → ...如果你 CPU 处理得不够快,数据就丢了。所以这个模式适合的场景比较窄:你只关心"当前值"的情况,比如实时监测一个电源电压。
要停止的话,清 CONT 位或者直接 ADON=0 关掉 ADC。
我的忠告:如果你发现用连续模式数据总是丢,换缓冲连续模式。
4.3 缓冲连续转换模式 — 连续模式的正确打开方式
配置: CONT=1, SCAN=0, DBUF=1 | 仅 ADC1
这是连续模式的增强版。区别在于结果不再只存一个 ADC_DR,而是依次存入一组缓冲寄存器(8 个或 10 个,取决于型号)。
ADON=1 → 转→存DB0R → 转→存DB1R → ... → 转→存DB(N-1)R → 缓冲区满,EOC=1 → 转→覆盖DB0R → 转→覆盖DB1R → ... → 又满了,EOC=1 → ...这个模式最实用的场景是多次采样求平均。你可以等 EOC 置位后,把缓冲区里的 8 或 10 个值全部读出来做软件滤波,然后再等下一轮。
要注意OVR(Overrun)标志:如果你没来得及读完缓冲区,新的一轮就开始覆盖了,OVR 就会被置 1。这时候之前读到的数据可能已经不完整了,建议直接重来。设置 ADON=1 可以自动清除 OVR。
4.4 单次扫描模式 — 多通道采样的主力
配置: CONT=0, SCAN=1 | 仅 ADC1
这是我项目里用的模式,也是我觉得最值得详细讲的一个。
扫描模式的核心思想是:你告诉 ADC “从 AIN0 扫到 AINn”,它就自动把 AIN0、AIN1、…、AINn 依次转换一遍,每个通道的结果存入对应编号的缓冲寄存器。
设 CH[3:0] = 5 → ADON=1 → AIN0→DB0R, AIN1→DB1R, AIN2→DB2R, AIN3→DB3R, AIN4→DB4R, AIN5→DB5R → 全部完成,EOC=1,停止几个容易踩的坑:
CH[3:0] 设的是"终点",不是"起点"。扫描永远从 AIN0 开始,CH[3:0] 告诉硬件最后一个通道是几号。我一开始以为是"选择起始通道",结果白调试了半小时。
不要在扫描过程中清 SCAN 位。想停?清 ADON。
扫描模式下清 EOC 有坑。这是手册里用了一整段 “Caution” 来警告的事。你不能用
ADC1->CSR &= ~ADC1_CSR_EOC这种位操作来清 EOC,因为这个操作会先读 CSR(把当前硬件正在扫描的通道号读回来),然后写回去——这就把你的扫描终点给改了!正确做法:直接把一个预先准备好的值写入 CSR:
// 不要这样!ADC1->CSR&=~ADC1_CSR_EOC;// 应该这样:ADC1->CSR=last_channel_number;// EOC=0, CH=你想要的终点通道AIN12 在扫描模式下不能选。这是硬件限制,具体看 datasheet。
扫描模式下每个被选中的通道的 GPIO 输出会被禁用。所以别想着在扫描 AIN0-AIN5 的同时用 PB3 做 PWM 输出——它会被关掉。
4.5 连续扫描模式 — 不停地扫
配置: CONT=1, SCAN=1 | 仅 ADC1
就是单次扫描的"停不下来版"。一轮扫完 AIN0 到 AINn 之后,自动从头再来一轮,如此循环。
停止方法:
ADON=0:立即停(可能数据不完整)CONT=0:等当前这轮扫完再停(优雅停机)
清 EOC 的注意事项和单次扫描一样——别用位操作,直接赋值。
5. 数据对齐:左还是右?
通过 CR2 的 ALIGN 位选择。这个看似简单,但如果你读数据的顺序搞错了,数据一致性就没法保证。
右对齐 (ALIGN=1) — 我的选择
ADC_DRH: [ - - - - - - D9 D8 ] ADC_DRL: [ D7 D6 D5 D4 D3 D2 D1 D0 ]读取顺序:先读 DRL,再读 DRH。这看起来反直觉,但 STM8 是小端序,用 LDW 指令可以一次性读出 16 位。
右对齐的好处是直接拿到的就是 0~1023 的线性值,方便计算。
左对齐 (ALIGN=0)
ADC_DRH: [ D9 D8 D7 D6 D5 D4 D3 D2 ] ADC_DRL: [ D1 D0 - - - - - - ]读取顺序:先读 DRH,再读 DRL。
左对齐的好处是如果你只需要 8 位精度,直接读 DRH 就完事了,DRL 可以不看。
注意: ALIGN 位只影响 ADC_DRH/ADC_DRL 的读取顺序,不影响缓冲寄存器 ADC_DBxRH/ADC_DBxRL 的行为。
6. 模拟看门狗 — ADC1 的隐藏技能
说实话,这个功能我用得不多,但了解一下没坏处。
模拟看门狗的原理很简单:你设一个高阈值和一个低阈值,如果 ADC 转换结果跑到了阈值外面,就触发 AWD 标志(如果开了中断还会触发中断)。
高阈值 (ADC_HTR) ┌──────────────────┐ 超限 │ 危险区域 (高) │ ← AWD 报警! ├──────────────────┤ 正常 │ 安全区 │ ← 一切正常 ├──────────────────┤ 超限 │ 危险区域 (低) │ ← AWD 报警! └──────────────────┘ 低阈值 (ADC_LTR)在不同模式下看门狗的行为不同:
- 单次/非缓冲连续模式:默认使能,直接监测 ADC_DR
- 扫描模式:通过 AWENx 位选择性使能某些通道的看门狗
- 缓冲连续模式:通过 AWENx 位选择性使能某些缓冲区的看门狗
7. 外部触发 — 让定时器来控制采样节奏
如果你的采样需要精确的定时,可以用外部触发。有两种触发源:
- TIM1 的 TRGO 事件(内部定时器)
- ADC_ETR 引脚的外部信号
通过 CR2 的 EXTSEL[1:0] 选择,EXTTRIG 位使能。
重要提醒: 执行 HALT 指令前必须先禁用外部触发(EXTTRIG=0),否则从 Halt 唤醒后 ADC 的行为不可预测。
8. 寄存器速查手册
这部分我把所有 ADC 寄存器整理在了一起,方便开发时快速查阅。
ADC_CSR — 控制/状态寄存器 (0x20, 复位值 0x00)
位7 位6 位5 位4 位3 位2 位1 位0 EOC AWD EOCIE AWDIE CH3 CH2 CH1 CH0 rc_w0 rc_w0 rw rw rw rw rw rw| 位 | 名称 | 说明 |
|---|---|---|
| 7 | EOC | 转换结束。硬件置1,软件写0清。这是你最常打交道的标志位 |
| 6 | AWD | 看门狗越限标志(仅ADC1) |
| 5 | EOCIE | EOC 中断使能。开了之后每次转换完都会进中断 |
| 4 | AWDIE | 看门狗中断使能(仅ADC1) |
| 3:0 | CH[3:0] | 通道选择。扫描模式下设的是最后一个通道号 |
ADC_CR1 — 配置寄存器1 (0x21, 复位值 0x00)
位7 位6 位5 位4 位3 位2 位1 位0 Res SP2 SP1 SP0 Res Res CONT ADON r rw rw rw r r rw rw| 位 | 名称 | 说明 |
|---|---|---|
| 6:4 | SPSEL[2:0] | 预分频。/2, /3, /4, /6, /8, /10, /12, /18 |
| 1 | CONT | 0=单次, 1=连续。扫描模式下配合 SCAN 使用 |
| 0 | ADON | ADC 开关和启动触发。首次写=唤醒,再次写=启动 |
ADC_CR2 — 配置寄存器2 (0x22, 复位值 0x00)
位7 位6 位5 位4 位3 位2 位1 位0 Res EXTTRIG EXTSEL1 EXTSEL0 ALIGN Res SCAN Res r rw rw rw rw r rw r| 位 | 名称 | 说明 |
|---|---|---|
| 6 | EXTTRIG | 外部触发使能 |
| 5:4 | EXTSEL[1:0] | 00=TIM1 TRGO, 01=ADC_ETR |
| 3 | ALIGN | 0=左对齐, 1=右对齐 |
| 1 | SCAN | 扫描模式使能(仅ADC1有) |
ADC_CR3 — 配置寄存器3 (0x23, 复位值 0x00, 仅ADC1)
位7 位6 位5~位0 DBUF OVR Reserved rw rc_w0 r| 位 | 名称 | 说明 |
|---|---|---|
| 7 | DBUF | 数据缓冲使能。开了之后结果存 DBxR 而不是 DR |
| 6 | OVR | 溢出标志。缓冲区数据被覆盖前没读就走这个位 |
数据寄存器
| 寄存器 | 偏移 | 说明 |
|---|---|---|
| ADC_DRH | 0x24 | 数据高字节(复位值不定) |
| ADC_DRL | 0x25 | 数据低字节(复位值不定) |
| ADC_DBxRH | 0x00+2n | 缓冲区高字节(n=0…9,仅ADC1) |
| ADC_DBxRL | 0x01+2n | 缓冲区低字节 |
其他功能寄存器
| 偏移 | 寄存器 | 说明 |
|---|---|---|
| 0x26 | ADC_TDRH | Schmitt 触发器禁用 [15:8],设1=禁用,省电 |
| 0x27 | ADC_TDRL | Schmitt 触发器禁用 [7:0] |
| 0x28 | ADC_HTRH | 看门狗高阈值 MSB [9:2](复位值 0xFF) |
| 0x29 | ADC_HTRL | 看门狗高阈值 LSB [1:0](复位值 0x03) |
| 0x2A | ADC_LTRH | 看门狗低阈值 MSB [9:2] |
| 0x2B | ADC_LTRL | 看门狗低阈值 LSB [1:0] |
| 0x2C | ADC_AWSRH | 看门狗状态 [9:8](仅ADC1) |
| 0x2D | ADC_AWSRL | 看门狗状态 [7:0](仅ADC1) |
| 0x2E | ADC_AWCRH | 看门狗使能 [9:8](仅ADC1) |
| 0x2F | ADC_AWCRL | 看门狗使能 [7:0](仅ADC1) |
9. 寄存器映射总图
ADC1 完整映射
| 偏移 | 寄存器 | 位7 | 位6 | 位5 | 位4 | 位3 | 位2 | 位1 | 位0 | 复位值 |
|---|---|---|---|---|---|---|---|---|---|---|
| 0x00 | DB0RH | - | - | - | - | - | - | D9 | D8 | 0x00 |
| 0x01 | DB0RL | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 0x00 |
| 0x02~0x0D | Reserved | |||||||||
| 0x0E | DB7RH | - | - | - | - | - | - | D9 | D8 | 0x00 |
| 0x0F | DB7RL | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 0x00 |
| 0x10 | DB8RH* | - | - | - | - | - | - | D9 | D8 | 0x00 |
| 0x11 | DB8RL* | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 0x00 |
| 0x12 | DB9RH* | - | - | - | - | - | - | D9 | D8 | 0x00 |
| 0x13 | DB9RL* | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 0x00 |
| 0x14~0x1F | Reserved | |||||||||
| 0x20 | CSR | EOC | AWD | EOCIE | AWDIE | CH3 | CH2 | CH1 | CH0 | 0x00 |
| 0x21 | CR1 | - | SP2 | SP1 | SP0 | - | - | CONT | ADON | 0x00 |
| 0x22 | CR2 | - | EXTTRIG | EXTSEL1 | EXTSEL0 | ALIGN | - | SCAN | - | 0x00 |
| 0x23 | CR3 | DBUF | OVR | - | - | - | - | - | - | 0x00 |
| 0x24 | DRH | - | - | - | - | - | - | D9 | D8 | 0xXX |
| 0x25 | DRL | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | 0xXX |
| 0x26 | TDRH | TD15 | TD14 | TD13 | TD12 | TD11 | TD10 | TD9 | TD8 | 0x00 |
| 0x27 | TDRL | TD7 | TD6 | TD5 | TD4 | TD3 | TD2 | TD1 | TD0 | 0x00 |
| 0x28 | HTRH | HT9 | HT8 | HT7 | HT6 | HT5 | HT4 | HT3 | HT2 | 0xFF |
| 0x29 | HTRL | - | - | - | - | - | - | HT1 | HT0 | 0x03 |
| 0x2A | LTRH | LT9 | LT8 | LT7 | LT6 | LT5 | LT4 | LT3 | LT2 | 0x00 |
| 0x2B | LTRL | - | - | - | - | - | - | LT1 | LT0 | 0x00 |
| 0x2C | AWSRH | - | - | - | - | - | - | AWS9 | AWS8 | 0x00 |
| 0x2D | AWSRL | AWS7 | AWS6 | AWS5 | AWS4 | AWS3 | AWS2 | AWS1 | AWS0 | 0x00 |
| 0x2E | AWCRH | - | - | - | - | - | - | AWEN9 | AWEN8 | 0x00 |
| 0x2F | AWCRL | AWEN7 | AWEN6 | AWEN5 | AWEN4 | AWEN3 | AWEN2 | AWEN1 | AWEN0 | 0x00 |
*仅缓冲区大小为 10 的型号,8 缓冲区型号为保留
ADC2 映射
ADC2 比较精简:只有 CSR、CR1(无 SCAN)、CR2(无 SCAN)、CR3、DRH/DRL 和 TDRH/TDRL。没有数据缓冲寄存器,没有看门狗。
10. 我项目里的代码是怎么写的
项目路径:bsp/src/bsp_ADC.c,用 ADC1 的单次扫描模式采集 PB0-PB5(AIN0-AIN5)共 6 路反馈。
初始化
voidADC_Config(void){ADC1->CR1=0x00;// CONT=0, SPSEL=000 (fMASTER/2)ADC1->CR2=ADC1_CR2_ALIGN|ADC1_CR2_SCAN;// 右对齐 + 扫描ADC1->CR3=ADC1_CR3_DBUF;// 使能数据缓冲// 禁用 Schmitt 触发器,省电 + 降噪声ADC1->TDRH=0x3F;// ch8-ch13ADC1->TDRL=0x00;// 扫描范围: AIN0 到 AIN5ADC1->CSR&=~0x0F;ADC1->CSR|=adc_channels[ADC_CH_COUNT-1];// CH[3:0] = 5// 首次 ADON=1: 唤醒 ADCADC1->CR1|=ADC1_CR1_ADON;}这里我初始化时把 TDRH 设了 0x3F(禁用 ch8-ch13 的 Schmitt 触发器),虽然我实际用的是 ch0-ch5。这是因为项目早期规划过用 ch8-ch13,后来改了但这段代码留着也无害。如果你只用 ch0-ch5,应该设 TDRL = 0x3F(禁用 ch0-ch5)才对,能更有效地降低噪声。
启动扫描
voidADC_Start_Scan(void){ADC1->CSR&=~ADC1_CSR_EOC;// 清 EOCADC1->CR1|=ADC1_CR1_ADON;// 第二次 ADON=1 触发扫描}两行代码,很清晰。不过要注意,这里清 EOC 用了位操作——在单次扫描模式下这是安全的,因为扫描已经完成了(EOC=1 时才调用),CH[3:0] 的值就是终点通道号,读回来写回去没影响。但如果你在连续扫描模式的中断服务函数里清 EOC,就不能这么写了。
读取结果
// 通道 0-9: 从缓冲寄存器读(右对齐,先低后高)drl=ADC1->DB0RL;drh=ADC1->DB0RH;result=(drh<<8)|drl;// 拼成 10 位对通道号 >= 10 的回退处理
缓冲寄存器只有 8~10 个(DB0R ~ DB7R 或 DB9R),所以如果通道号 ≥ 10 就没有对应的缓冲寄存器。代码里做了个回退:临时关掉扫描模式,用单次转换模式单独读这个通道,然后再恢复扫描模式。
staticuint16_tADC_Read_Channel_Single(uint8_tchannel){ADC1->CR2&=~ADC1_CR2_SCAN;// 退出扫描ADC1->CSR&=~0x0F;ADC1->CSR|=(channel&0x0F);ADC1->CSR&=~ADC1_CSR_EOC;ADC1->CR1|=ADC1_CR1_ADON;// 单次转换while(!(ADC1->CSR&ADC1_CSR_EOC));ADC1->CSR&=~ADC1_CSR_EOC;ADC1->CR2|=ADC1_CR2_SCAN;// 恢复扫描drl=ADC1->DRL;drh=ADC1->DRH;return(drh<<8)|drl;}这个回退方案虽然有点"暴力",但对于偶尔需要读一个高通道号的场景够用了。
11. 一张表搞定所有操作
最后,我把日常开发中最常用的操作整理成一张速查表:
| 想干嘛 | 怎么做 |
|---|---|
| 单通道偶尔读一次 | CONT=0, SCAN=0 → ADON 唤醒 → ADON 启动 → 等 EOC → 读 DR |
| 单通道不停读 | CONT=1, SCAN=0 → ADON 启动 → 随时读 DR(会被覆盖) |
| 单通道不停读+存历史 | CONT=1, SCAN=0, DBUF=1 → 等 EOC → 读 DBxR 全部 |
| 多通道扫一遍 | CONT=0, SCAN=1 → CH[3:0]=末通道 → ADON 启动 → 等 EOC → 读 DBxR |
| 多通道不停扫 | CONT=1, SCAN=1 → CH[3:0]=末通道 → ADON 启动 → 周期性读 DBxR |
| 停止连续/连续扫描 | 清 CONT 或 ADON=0 |
| 停止单次扫描 | 清 ADON(立即停) |
| 读数据(右对齐) | 先读 DRL,再读 DRH |
| 读数据(左对齐) | 先读 DRH,再读 DRL |
| 清 EOC(扫描模式) | 不要用位操作!直接赋值 CSR |
写在最后
STM8S 的 ADC 设计其实挺有意思的——五种模式看起来多,但它们之间的关系是层层递进的:单次→连续→缓冲连续,单次扫描→连续扫描。理解了这个递进关系,选择模式就不是一件困难的事。
如果让我给刚上手的人一个建议,那就是:先搞清楚你要采样几个通道、采样频率是多少,然后对照那张模式选择表,基本上一眼就能确定用哪个模式。
至于寄存器,常用的其实就那几个:CSR(通道和状态)、CR1(ADON/CONT/分频)、CR2(ALIGN/SCAN)、CR3(DBUF/OVR)。其他都是看门狗和外部触发这种"锦上添花"的功能,用到的时候再翻手册也来得及。
希望这篇笔记对你有帮助。如果发现有错误或者有补充的,欢迎交流。
Winston Qu
2026/05/31 于浙江工业大学