ADC 采样:单次、扫描与内部温度传感器
本篇是《Zig 裸机开发实战:STM32F103ZET6 全栈解析》系列第 16 篇。上一篇:SPI 总线:W25Q64 Flash 读写与 ID 识别,下一篇:内部 Flash 编程:参数持久化与页擦除,目录见文末。
ADC 采样:单次、扫描与内部温度传感器
导读
前面几篇都在"往外送"数字信号,这一篇往里收模拟世界:电位器的角度、电池电压、芯片自己的体温。F103ZET6 有 3 个 12 位 ADC(ADC3 只有 144 脚封装才有,是 ZET6 的专属福利),21 个通道、2.4 µs 级转换。microzig 给了两套互不兼容的 API:简单版 ADC(8 个方法,够用 90% 场景)与完整版 AdvancedADC(40+ 个方法,注入组、双 ADC、DMA),还有一串"拼写照抄"的字段名(aling、continuos、set_DMA、set_indedpended)。本篇从单次采样讲到扫描序列,再用 CH16(内部温度传感器)与 CH17(内部基准)讲两个"没有引脚的通道"——包括用 Vrefint 反推 VDDA 来修正电源波动的标准手法,最后把 DMA 连续采样接到第 13 篇的通道分配表上。
为什么需要理解 ADC 的三层结构
把 3.3 V 世界变成 0–4095 的数字,F103 的 ADC 要过三道闸:
- 通道闸(MUX):21 个输入通道(IN0–IN15 对应 PA0–PA7、PB0–PB1、PC0–PC5,IN16–IN18 是内部温度、Vrefint、VBAT)多路复用到 ADC 核。引脚必须配
.analog输入模式——数字输入的施密特触发器会干扰模拟采样,这是"读数乱漂"的经典原因。 - 时钟闸(ADCPRE):ADC 核的时钟上限 14 MHz,来源是 PCLK2(72 MHz)分频 2/4/6/8。72 MHz 配置下选 /6 = 12 MHz——这是本系列的标准选择,留有余量又不浪费速度。
- 采样闸(SMPx):每个通道独立的采样时间,1.5 到 239.5 个 ADC 时钟周期。采样期间内部采样电容(约 8 pF)充电到输入电压,之后才断开进行 12.5 周期的逐次逼近。
三层里最值得展开的是采样时间与源阻抗的关系。采样电容充电遵循 RC 曲线,源阻抗(信号源内阻 + 外部串联电阻)越大,电容充到位的时间越长。RM0008 给出的工程结论:采样时间必须满足
T_samp ≥ R_src × C_sample × ln(2^12) ≈ 8.3 × R_src × C_sample
代入 C_sample ≈ 8 pF:源阻抗 10 kΩ 时约需 0.66 µs ≈ 8 个 ADC 时钟;10 kΩ 电位器配 239.5 周期(约 20 µs)绰绰有余。反过来,若你直接接高阻抗传感器(100 kΩ+)却用了 1.5 周期采样,电容没充满就断开,读数系统性偏低——这比噪声难查得多,因为它是确定性偏差。经验法则:阻抗未知就先用最长采样时间 239.5,确认能跑再优化吞吐。
一次完整转换的时间 = 采样时间 + 12.5 周期。12 MHz 下 239.5 档约 21 µs(约 4.8 万次/秒),1.5 档约 1.17 µs(约 85 万次/秒)——这是单通道的吞吐天花板,再往上就要 DMA 连续流。
代码案例:电位器 + 内部温度 + Vrefint 修正
const std = @import("std");
const microzig = @import("microzig");
const stm32 = microzig.hal;
const rcc = stm32.rcc;
const gpio = stm32.gpio;
const time = stm32.time;
pub const panic = microzig.panic;
pub const std_options = microzig.std_options(.{ .logFn = stm32.uart.log });
comptime {
_ = microzig.export_startup();
}
// 数据手册典型值(DS5312 §5.3.22),每颗芯片有偏差,量产需校准
const V25: f32 = 1.43; // 25 度时的传感器电压
const AVG_SLOPE: f32 = 0.0043; // 4.3 mV / 度
fn adc_to_temp(val: u16) f32 {
const mv = (@as(f32, @floatFromInt(val)) / 4096.0) * 3300.0;
return ((V25 * 1000.0 - mv) / (AVG_SLOPE * 1000.0)) + 25.0;
}
pub fn main() !void {
_ = try rcc.apply(.{
.SYSCLKSource = .PLL1_P,
.PLLSourceVirtual = .HSE_Div_PREDIV,
.PLLMUL = .Mul9,
.APB1CLKDivider = .Div2,
// ADCPRE 是 PCLK2 的分频,72/6 = 12 MHz(<=14 MHz 上限)
.ADCPresc = .Div6, // 仓库内只逐字验证过 .Div2,Div6/Div8 的枚举名以本地生成为准
.RTCClockSelection = .LSI,
.flags = .{ .HSEOscillator = true, .RTCUsed_ForRCC = true, .USE_ADC1 = true },
});
time.init_timer(.TIM4);
rcc.enable_clock(.GPIOA);
rcc.enable_clock(.USART1);
rcc.enable_clock(.ADC1);
// PA1 = ADC1_IN1,模拟输入模式
const pot = gpio.Pin.from_port(.A, 1);
pot.set_input_mode(.analog);
const tx = gpio.Pin.from_port(.A, 9);
tx.set_output_mode(.alternate_function_push_pull, .max_50MHz);
try uart.apply_runtime(.{ .clock_speed = rcc.get_clock(.USART1) });
stm32.uart.init_logger(&uart);
const adc = stm32.adc.ADC.init(.ADC1);
adc.enable(); // 忘了这行,read 系列返回 error.ADCNotEnabled
// 通道 16(温度)与 17(Vrefint)阻抗高,用最长采样时间
adc.set_channel_sample_rate(16, .@"239.5");
adc.set_channel_sample_rate(17, .@"239.5");
adc.set_channel_sample_rate(1, .@"239.5"); // 10k 电位器,先求稳
// 扫描序列:温度、Vrefint、电位器,一次触发依次转换
adc.load_sequence(&.{ 16, 17, 1 });
var buf: [3]u16 = undefined;
var window: [8]f32 = @splat(0); // 滑动窗口均值
var win_idx: usize = 0;
while (true) {
const results = try adc.read_multiple_channels(&buf);
const raw_temp = results[0];
const raw_vref = results[1];
const raw_pot = results[2];
// Vrefint 反推真实 VDDA:基准典型 1.20V,读数 = VDDA * 1.20 / Vref(满量程)
// 12 位满量程 4096:VDDA = 1.20 * 4096 / raw_vref
const vdda: f32 = (1.20 * 4096.0) / @as(f32, @floatFromInt(raw_vref));
// 温度换算用真实 VDDA 而非标称 3.3V,消除电源波动
const temp = blk: {
const mv = (@as(f32, @floatFromInt(raw_temp)) / 4096.0) * vdda * 1000.0;
break :blk ((V25 * 1000.0 - mv) / (AVG_SLOPE * 1000.0)) + 25.0;
};
// 电位器:滑动窗口均值滤波
window[win_idx] = (@as(f32, @floatFromInt(raw_pot)) / 4096.0) * vdda;
win_idx = (win_idx + 1) % window.len;
var pot_avg: f32 = 0;
for (window) |v| pot_avg += v;
pot_avg /= window.len;
std.log.info("VDDA: {d:.3}V temp: {d:.1}C pot: {d:.3}V", .{ vdda, temp, pot_avg });
time.sleep_ms(500);
}
}
逐段解析
简单版 ADC 的三个方法:enable、采样率、读
简单版 ADC 的 8 个方法里,本篇用到 4 个,每个都有坑位:
ADC.init(.ADC1):参数是 comptime 实例。rcc.enable_clock(.ADC1)必须在 enable 之前——ADC 时钟没开,read_single_channel里的while (EOC == 0)是无超时忙等,程序表现为"卡在第一次读数"。enable():置 CR2 的 ADON。它不只是开电源——F103 的 ADON 第一次写是"从休眠唤醒",需要稳定时间(HAL 内部用STAB_TIME,默认 1 ms,是个可运行时改的pub var)。第二次读数前忘enable会得到error.ADCNotEnabled(是错误不是 panic,try处理即可)。set_channel_sample_rate(channel, rate):写 SMPR 的对应档位。通道号是u5,0–18;采样时间枚举是字符串字面量风格:.@"1.5"、.@"7.5"、.@"13.5"、.@"28.5"、.@"41.5"、.@"55.5"、.@"71.5"、.@"239.5"——写.239_5或.@"239.5cycles"都不行,@""引号是语法的一部分。read_single_channel(1):单通道即采即读。内部流程:序列长度设 1 → 序列第 0 槽写通道号 → SWSTART 软件触发 → 轮询 EOC → 读 DR(读 DR 顺带清 EOC)→ 清 STRT。返回ReadError!u16,原始 12 位右对齐值。
扫描版把"一次配好、多次触发"变成流水线:load_sequence(&.{16, 17, 1}) 把三个通道填进规则序列寄存器 SQR(最多 16+1 槽),read_multiple_channels(&buf) 一次触发、按序列顺序依次转换、把结果按序填回缓冲并返回切片。两个方法共享同一条序列——先 load 后 read 的顺序不能反。
CH16 与 CH17:两个没有引脚的通道
通道 16 是内部温度传感器、17 是内部参考电压(Vrefint),它们直接连在 ADC 输入 MUX 上,不需要配任何引脚。但有两个前置条件:CR2 的 TSVREFE 位要置 1(把传感器与基准接通——完整版 AdvancedADC 有专门的 enable_reftemp(),简单版跟随上游示例 adc.zig 的用法直接读;若读数恒为 0 或乱值,先查这一位,寄存器直写是 CR2 bit23);采样时间必须长(这两个源阻抗极高,RM0008 要求 ≥ 2.2 µs,推荐 17.1 µs 以上,239.5 档的约 20 µs 正合适)。
CH17 的价值:反推 VDDA。 ADC 的量化公式是 读数 = 输入电压 / VDDA × 4096——分母是真实电源电压,不是你标称的 3.3 V。USB 供电 3.1 V、LDO 带载压降 3.2 V,都会让所有读数整体漂移。Vrefint 是片内带隙基准,典型 1.20 V 且几乎不随 VDDA 变化,于是它自己就成了"尺子上的刻度":
VDDA(实际) = 1.20 V × 4096 / CH17 读数
VDDA 偏低 → CH17 读数偏大 → 反推出的 VDDA 变小 → 用它做分母的其他换算自动修正。示例里温度与电位器电压的换算都吃这个修正。要诚实说明一点:F103 没有出厂校准的 Vrefint 标定值(F0/F3/F4 系列才有 factory cal,可在系统存储里读),所以 1.20 V 是典型值(数据手册容差约 ±10 mV 量级),这套修正消除的是电源波动(百分比级),修不掉基准自身的绝对偏差(需要外部万用表一次性标定)。
CH16 的公式与误差。 温度传感器输出 V25 = 1.43 V(25 °C)、斜率 4.3 mV/°C,换算就是上游 adc.zig:16-22 的逐字公式(本篇把它改写成用真实 VDDA):
T(°C) = (V25 − V_sensor) / 4.3mV + 25
误差来源要心里有数:V25 与斜率都是典型值,数据手册标注每颗芯片的偏差可以到 ±1–2 °C 以上;自发热(CPU 满载比环境高 5–10 °C)让读数偏离室温。所以它适合"过温保护阈值""发热趋势监测",不适合当温度计卖。量产校准的办法:已知环境温度下单点记录读数,平移修正。
AdvancedADC 扫一眼:注入组、双 ADC 与拼写字典
完整版 AdvancedADC(40+ 方法,全 HAL 最大单文件)的超能力按需取用:
- 注入组(injected):独立的 4 通道转换队列,可以"插队"打断规则组——电机控制里电流环的过采样用它,保证采样时刻绝对由定时器触发。
- 双 ADC 模式:
configure_dual_mode让 ADC1/ADC2 同步采样(Regular/Injected 同时、交替、交叉交错),两路 16 位结果打包进一个 32 位字由 DMA 一次搬走——上游adc_dualmode.zig是现成范本(AdcData = packed struct(u32) { adc1: u16, adc2: u16 }+memory_size = .Bits32)。 - DMA 与中断:
set_DMA(true)置 CR2 的 DMA 位,规则组每完成一次转换发一次 DMA 请求,映射到 DMA1_Channel1(第 13 篇的映射表)。
拼写彩蛋要专门立个字典,这些字段名照抄上游、拼写即真理:
| 正确英语 | 上游实际拼写 | 出处 |
|---|---|---|
| alignment | aling |
set_data_alignment(self, aling: Alignment) |
| continuous | continuos |
set_regular_seq(seq, continuos: bool) |
| DMA | set_DMA(大写) |
set_DMA(self, set: bool) |
| independent | set_indedpended |
set_indedpended(adc1) |
AdvancedADC.enable(calib: bool) 还带一个参数:上电校准(内部电容阵列的自校准,建议 enable(true))——这是简单版没有的能力,追求精度时是升级理由之一。
滑动窗口滤波:8 行代码的真功夫
示例对电位器用了滑动窗口均值:8 个最近样本存进环形数组,每次全加求平均。它与"连续采 16 次取平均"的差别在于响应速度——后者每次输出要等 16 次新采样,滑动窗口每来一个新样本就更新一次输出,平滑与延迟兼得。窗口大小是噪声抑制与响应延迟的直接交换:8 样本 × 500 ms = 4 s 的等效时间常数,电位器转动后约 2 s 读数稳定;要更快就缩窗口,要更平滑就加长。
对抗 50 Hz 工频干扰还有一招:让采样窗口恰好对齐 20 ms 的整倍数(本例 8 × 500 ms 远超,天然抑制),或把采样率配成 50 Hz 的整数倍后做整周期平均。ADC 抗干扰的完整武器库还包括:Vrefint 修正(本篇)、输入 RC 前置滤波(1 kΩ + 100 nF 到地)、模拟走线远离 PWM 线。软件滤波永远只是最后一道防线——garbage in, garbage averaged。
运行现象
烧录后串口每 500 ms 一行:
VDDA: 3.291V temp: 31.4C pot: 1.652V
- 转电位器:pot 从 0 爬到接近 VDDA 读数,滑动窗口让它略带"惯性"——快速转动时读数滞后约 2 s 追上。
- 手指按住芯片:temp 在几十秒内缓缓上升 3–5 °C——这是芯片自发热与你的体温叠加,传感器确实在工作。对它吹口气,读数会小幅下降(气流散热)。
- 换电源验证 Vrefint 修正:把供电从 USB 换到电池(或串一个二极管压降 0.7 V),VDDA 读数同步下降约 20%,而 pot 换算出的电压几乎不变——没有修正的话它会凭空"缩水" 20%。
- 采样时间实验:把通道 1 的采样率改成
.@"1.5",在电位器上并联一个 100 nF 电容——读数依旧稳定;撤掉电容、在 PA1 串一个 100 kΩ 电阻再试,读数系统性偏低且旋到两端偏差更大——源阻抗与采样时间的活教材。 read_multiple_channels返回的切片长度是 3(序列长度),若你把 buf 配小了,Debug 构建下会越界 panic——缓冲长度必须 ≥ 序列长度。
常见问题
Q1:read_single_channel 卡死不返回。
卡点在 while (EOC == 0) 的无超时忙等,四个嫌疑:① rcc.enable_clock(.ADC1) 没调——寄存器写不进去,SWSTART 无效;② adc.enable() 没调——返回的应该是 error.ADCNotEnabled 而不是卡死,所以先排除这条;③ ADC 时钟超限或分频错(ADCPresc 写了不存在的值会编译失败,但时钟树没配对会静默);④ SWSTART 触发了但 EXTSEL 触发源不是软件触发(简单版默认软件触发,AdvancedADC 配过 set_trigger 后忘了改回来)。定位手段:逻辑分析仪看 PA1 有无采样动作没用(内部信号),直接读 SR 寄存器看 EOC/ADONS 位。
Q2:读数永远在 0 附近乱跳,电位器转到头也上不来。
三个经典成因:① 引脚没配 .analog——数字输入的施密特触发器挂在模拟信号上,读数被钳位/污染,这是第一大坑;② 接错通道——PA1 是 IN1(read_single_channel(1)),PA0 是 IN0,照抄别的教程时通道号与接线对不上;③ 电位器两端没接 3.3 V/GND,或中点根本没接 PA1(万用表量 PA1 对地电压应随旋钮变化)。顺带一查:采样时间给了 .@"1.5" 而源阻抗 >10 kΩ 也会系统性偏低,但"永远 0 附近"更像前两条。
Q3:.@"239.5" 这种枚举名太怪了,我写 .@"239_5" 为什么编译不过?
上游把采样时间枚举定义成了带小数点的字符串字面量(SampleRate = enum(u3) { @"1.5" = 0, ..., @"239.5" = 7 }),Zig 里 @"" 引号内是字面成员名,239.5 就是 239.5——写别的任何变体都报 no field named。同理记住 AddressMode 的 .@"7bits"(第 14 篇)。这类"数字带点"的枚举在 regz/embassy 风格的 HAL 里会出现,遇到就 @"" 套引号照抄。
Q4:AdvancedADC 的方法名拼写为什么错得这么整齐?我"帮忙改对"会怎样?
aling、continuos、set_DMA、set_indedpended 是上游源码的逐字事实(adc.zig 的 450/500/621/874 行)。Zig 按符号名精确匹配,你写正确英语就是 no field or member function named 'set_data_alignment'——注意报错会显示你写的名字,看起来像"上游没有这个函数",其实是"上游有但拼错了"。策略与 TimerGenealConfig 一致:照抄错误拼写,在项目里留一张拼写对照表(本篇的表格可以直接用),升级 microzig 版本时留意是否修正(修正了你的代码反而要跟着改)。
Q5:Vrefint 修正后的 VDDA 是 3.4 V,比标称还高,正常吗?
正常,两个来源:一是 1.20 V 只是典型值,单颗芯片的 Vrefint 实际值在典型值 ±1–2% 内浮动,反推出的 VDDA 带着同样的系统偏差;二是 USB 供电本来就允许 4.75–5.25 V 输入,经板载 LDO 输出 3.3 V ±2%。判断标准不是"是否等于 3.300",而是稳定性——读数波动 <0.5% 且随负载无趋势性漂移,电源就是健康的。需要绝对精度就万用表量一次 VDDA,把偏差作为常数写进代码(或按上面说的单点标定 Vrefint 的真实电压)。
Q6:简单版 ADC + load_sequence 和 AdvancedADC + set_DMA(true) 各适合什么场景?
简单版扫描(本篇):毫秒级轮询、几路慢信号(温度、电位器、电池电压),CPU 每次转换后取一次结果,实现与调试都简单。DMA 连续流(AdvancedADC):千赫兹以上采样率或成块数据——set_DMA(true) 后每次 EOC 自动触发 DMA1_Channel1 搬运,配 circular_mode + 半传输事件(第 13 篇的乒乓结构)实现零 CPU 干预的连续采样。跨界点大约在"单次转换 + 主循环处理"的时间预算被打爆时:12 MHz ADC 时钟、239.5 档下每通道 21 µs,三通道循环就是 63 µs,主循环还有别的活干就该上 DMA 了。
面试考点
Q1:F103 ADC 的时钟上限是多少?如何从 72 MHz 系统时钟得到合规的 ADC 时钟?超限会怎样?
参考答案:上限 14 MHz(数据手册与 RM0008),来源是 PCLK2 经 ADCPRE 分频 2/4/6/8。72 MHz 配置下选 /6 = 12 MHz。超限(如 /2 = 36 MHz)的后果不是立刻损坏,而是内部逐次逼近逻辑时序违规:转换结果精度下降(丢码、非线性),通常表现为读数抖动增大。选 /6 而非 /8 是吞吐与上限余量的平衡:12 MHz 留 17% 余量,/8 的 9 MHz 则牺牲 25% 吞吐。另外注意 HAL 的 .ADCPresc 是 comptime 字段,枚举名以本地生成物为准。
Q2:采样时间与信号源阻抗的关系?给一个工程上的选型法则。
参考答案:ADC 内部采样电容(约 8 pF)在采样窗口内经源阻抗充电,必须充到 1/2^12 精度,需要约 8.3 个 RC 时间常数:T_samp ≥ 8.3 × R_src × C_sample。源阻抗 10 kΩ 约需 0.7 µs(9 个 12 MHz 时钟),100 kΩ 约需 7 µs。法则:阻抗未知或信号经大串阻/分压网络时先用最长档 239.5 周期保正确性,确认后按吞吐需求回退;高阻抗源在 ADC 前加运放跟随器或 RC 缓冲(1 kΩ + 100 nF),把有效源阻抗压到千欧级,既允许短采样又抗干扰。选错的症状是确定性偏低偏差,而非随机噪声。
Q3:内部温度传感器的换算公式与主要误差源?它适合什么用途?
参考答案:公式 T = (V25 − V_sense)/Avg_Slope + 25,典型值 V25 = 1.43 V、斜率 4.3 mV/°C;电压换算的分母应使用 Vrefint 反推的真实 VDDA 而非标称 3.3 V。误差源:① V25 与斜率是典型值,芯片间分散大(±1–2 °C 量级),无出厂校准;② 传感器测的是结温,与 PCB 环境温度差自发热 5–10 °C;③ 采样时间不足时读数偏低(源阻抗极高,要求 ≥2.2 µs)。适合过温保护、发热趋势监测、相对温度比较;不适合绝对温度测量,量产需单点标定。
Q4:Vrefint 通道如何用来修正电源波动?F103 与 F4 系列在这上面有什么差别?
参考答案:ADC 满量程就是 VDDA,读数 = V_in/VDDA × 4096,电源漂移直接乘在所有结果上。把 Vrefint(内部带隙基准,典型 1.20 V、对 VDDA 不敏感)纳入扫描序列,由其读数反推 VDDA = 1.20 × 4096 / 读数,再用真实 VDDA 做其余通道的换算——电源 ±5% 波动被整体抵消。差别:F4/F0/F3 系列在出厂系统存储区固化了每颗芯片的 Vrefint 校准值(如 F4 的 TS_CAL1/TS_CAL2、VREFINT_CAL 地址),可直接读出精确基准;F103 没有这个机制,只能用 1.20 V 典型值,消除的是电源波动(相对修正),基准自身偏差要靠外部标定。做跨系列移植时这是常见的"精度倒退"点。
Q5:滑动窗口均值与连续 N 次采样取平均有什么差别?窗口长度的取舍依据?
参考答案:连续 N 次平均每输出一个结果要等 N 个新样本,输出更新率 = 采样率/N;滑动窗口每来一个新样本就输出一次(旧样本出窗),更新率等于采样率,且延迟只有窗口长度的一半(等效时间常数 N/2 个周期)。取舍依据:目标信号的变化带宽 vs 噪声幅度——白噪声按 1/√N 抑制,窗口加倍噪声降 30% 但响应迟滞加倍。工程起点:窗口 ≥ 噪声主导周期的整数倍(抑制 50 Hz 工频则窗口覆盖 20 ms 整数倍),再按"可接受的最大响应延迟"反推窗口上限。本质上是设计一个简单 FIR 低通,其截止频率 ≈ 0.44 × 采样率/N。
Q6:简单版 ADC 与 AdvancedADC 应该怎么选?列出升级的触发条件。
参考答案:简单版(8 方法):单次/扫描、软件触发、阻塞读,适合毫秒级轮询的慢信号,代码量小、心智负担低。AdvancedADC:注入组(定时器硬触发的确定时刻采样)、双 ADC 同步/交替(吞吐翻倍)、硬件校准(enable(true))、DMA 连续流(set_DMA(true) + DMA1_Channel1)、模拟看门狗。触发升级的条件按优先级:① 采样率超出"主循环阻塞可承受"(约 10 kHz 以上);② 需要确定采样时刻(控制环路);③ 需要 ADC1+ADC2 同时采两路(同步测量);④ 需要上电校准压噪声。升级的代价是拼写彩蛋四件套与更大的错误面(RegularConfigError 的 7 种错误)。通用原则:先跑通简单版验证模拟前端,再决定是否需要完整版的高级机制。
小结
- ADC 三层闸:通道 MUX(引脚配
.analog)→ 时钟(PCLK2/6 = 12 MHz,上限 14 MHz)→ 采样时间(1.5–239.5 档,与源阻抗匹配)。 - 简单版四步:
ADC.init(.ADC1)→enable()(忘了报ADCNotEnabled,时钟没开是卡死)→set_channel_sample_rate(枚举.@"239.5"引号照抄)→load_sequence+read_multiple_channels。 - CH16 温度:V25 = 1.43 V、斜率 4.3 mV/°C,典型值误差 ±1–2 °C,适合保护阈值不适合温度计。
- CH17 Vrefint:VDDA = 1.20 × 4096 / 读数,修正电源波动;F103 无出厂校准值(F4 有),绝对精度靠外部标定。
- AdvancedADC 拼写四件套照抄:
aling、continuos、set_DMA、set_indedpended;enable(true)带硬件校准。 - 滑动窗口均值:更新率 = 采样率、延迟 = 窗口/2;窗口覆盖 20 ms 整数倍可抑制工频。
- DMA 连续采样走 DMA1_Channel1 + circular,与第 13 篇的通道分配表衔接。
- 下一篇第 17 篇:把参数存进芯片自己的 Flash——解锁、擦除、编程与"擦写期间 CPU 停顿"的代价。
系列目录
- 系列总览:Zig 裸机开发的定位、生态与平台全景
- 开发环境搭建:Zig 0.17、microzig 与 probe-rs
- 工程结构剖析:build.zig、MicroBuild 与固件产物
- 第一个固件:启动流程、向量表与 GPIO 点灯
- 寄存器级编程:PAC、MMIO 与 volatile 语义
- GPIO 子系统:输入输出模式、上下拉与端口映射陷阱
- 时钟树配置:HSE、PLL 与总线分频
- 中断系统:NVIC、优先级分组与向量注册
- USART 串口通信:轮询收发与日志重定向
- 定时器基础:时基、PWM 与呼吸灯
- 输入捕获:频率与占空比测量
- 中断驱动串口:环形缓冲区与临界区设计
- DMA 传输:从外设搬运到内存
- I2C 总线:SSD1306 OLED 显示实战
- SPI 总线:W25Q64 Flash 读写与 ID 识别
- ADC 采样:单次、扫描与内部温度传感器
- 内部 Flash 编程:参数持久化与页擦除
- 独立看门狗:复位诊断与可靠性设计
- 低功耗模式:Sleep、Stop、Standby 与唤醒
- 内存布局与链接脚本:512 KB Flash 与 64 KB RAM 规划
- Zig 语言进阶:comptime、错误处理与零成本抽象
- 并发模型:中断与主循环的协作与临界区
- 协作式调度器:状态机与时间片轮转
- 实时性分析:中断延迟、抖动与 WCET
- RTOS 内核实现(上):任务控制块与上下文切换
- RTOS 内核实现(下):信号量、消息队列与优先级继承
- USB 设备开发:CDC 虚拟串口
- CAN 总线:bxCAN 过滤器与环回自测
- 调试与测试:probe-rs、RTT 与硬件在环
- 收官:分层架构、可移植性与 Zig 生态展望

浙公网安备 33010602011771号