输入捕获:频率与占空比测量
本篇是《Zig 裸机开发实战:STM32F103ZET6 全栈解析》系列第 11 篇。上一篇:定时器基础:时基、PWM 与呼吸灯,下一篇:中断驱动串口:环形缓冲区与临界区设计,目录见文末。
输入捕获:频率与占空比测量
导读
第 10 篇里我们让定时器"往外发"波形(PWM),这一篇反过来:让定时器"往里收"波形,测出它的频率与占空比。输入捕获是定时器三大用途(时基、PWM、捕获)里最少被讲透的一个,因为它的精华不在"读 CCR"这个动作,而在两个硬件机制:TIxFP 输入滤波(把毛刺挡在锁存器之外)和PWM 输入模式(一个引脚、两个通道、一次测量同时拿到周期和高电平宽度)。我们会逐字拆解上游 timer_capture.zig 这个官方最难的示例,把它从 8 MHz HSE 时钟改到本系列的 72 MHz 配置,并把 ISR 里的浮点除法换成整数运算。你会顺带认识一个 API 边界:microzig 的 SyncTriggerSource 枚举没有 TI4FP4,所以 PWM 输入模式只能落在 CH1/CH2——本篇的输入引脚是 PA0,与 KEY1 复用。
为什么需要输入捕获
测频率最天真的办法是 GPIO 中断 + 软件计时:每个上升沿进一次 ISR,用两次中断的时间差算周期。它在低频下能用,但有三个硬伤:
- 中断延迟抖动。NVIC 响应 + 入栈 + 退出要几十个周期,且会被更高优先级中断推迟。72 MHz 下这几十纳秒对 1 kHz 信号无所谓,对 100 kHz 信号就是百分之几的误差。
- 高频丢失。中断进出成本约 0.5 µs,信号周期低于 2 µs(>500 kHz)时 CPU 全部时间都花在进出 ISR 上,主循环饿死。
- 占空比要双倍中断。上升沿、下降沿各进一次 ISR,抖动误差也翻倍。
输入捕获把"打时间戳"这件事交给硬件:信号边沿到来时,定时器硬件自动把计数器 CNT 的当前值锁存进 CCR 寄存器,全程零 CPU 参与,CPU 只需要事后读 CCR。误差只剩一个计数周期(1 MHz 时基下 ±1 µs),与软件负载完全无关。
F103 每个通用定时器(TIM2–TIM5)有 4 个捕获/比较通道,每个通道可独立配置为输入捕获:选择输入源(TIx)、设置滤波与分频、选择触发边沿。第 10 篇已经把 CCR 当"比较值"用过——同一个寄存器,输入模式下变成"被锁存的时间戳",这就是"捕获/比较寄存器"名字里两个词的由来。
代码案例:TIM2 测 PWM 频率与占空比
接线:PA0(TIM2_CH1,即 TI1)接信号源。最省事的信号源是回环——把第 10 篇呼吸灯的输出 PB5(TIM3_CH2,1 kHz 渐变占空比 PWM)用杜邦线跳到 PA0,本篇程序就能实时测出第 10 篇生成的波形;或者接信号发生器。注意 PA0 也是 KEY1 的引脚:做本篇实验时按键暂时不可用(EXTI0 与输入捕获共用引脚),按下去相当于给信号叠加一个手动沿。
const std = @import("std");
const microzig = @import("microzig");
const stm32 = microzig.hal;
const rcc = stm32.rcc;
const gpio = stm32.gpio;
const time = stm32.time;
const GPTimer = stm32.timer.GPTimer;
pub const panic = microzig.panic;
pub const std_options = microzig.std_options(.{ .logFn = stm32.uart.log });
comptime {
_ = microzig.export_startup();
}
// TIM2 被 HAL 的 default_interrupts 占用,自绑 ISR 必须显式允许覆盖
pub const microzig_options: microzig.Options = .{
.interrupts = .{ .TIM2 = .{ .c = isr_tim2 } },
.overwrite_hal_interrupts = true,
};
const comp = GPTimer.init(.TIM2);
const sig = gpio.Pin.from_port(.A, 0); // TI1 = PA0,与 KEY1 复用
var period_ticks: u32 = 0; // 周期(1 MHz 时基的 tick 数)
var high_ticks: u32 = 0; // 高电平宽度
var freq_hz: u32 = 0;
var duty_x100: u32 = 0; // 占空比 × 100,整数化,ISR 里不做浮点
fn isr_tim2() callconv(.c) void {
const flags = comp.get_interrupt_flags();
// 下降沿(channel2)先于下一个上升沿发生,此时两个 CCR 都已锁存
if (flags.channel2) {
const period = comp.read_ccr(0); // CCR1:上升沿时间戳 = 周期
const high = comp.read_ccr(1); // CCR2:下降沿时间戳 = 高电平
if (period != 0) {
period_ticks = period;
high_ticks = high;
freq_hz = 1_000_000 / period;
duty_x100 = (@as(u32, high) * 100) / period;
}
}
comp.clear_interrupts();
}
pub fn main() !void {
_ = try rcc.apply(.{
.SYSCLKSource = .PLL1_P,
.PLLSourceVirtual = .HSE_Div_PREDIV,
.PLLMUL = .Mul9,
.APB1CLKDivider = .Div2,
.RTCClockSelection = .LSI,
.flags = .{ .HSEOscillator = true, .RTCUsed_ForRCC = true },
});
// 时基仍归 TIM4(time 模块),TIM2 交给输入捕获
time.init_timer(.TIM4);
rcc.enable_clock(.GPIOA);
rcc.enable_clock(.TIM2);
rcc.enable_clock(.USART1);
const uart = stm32.uart.UART.init(.USART1);
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);
sig.set_input_mode(.floating);
// 72 MHz / (71 + 1) = 1 MHz 计数频率,1 tick = 1 us
// 同步模式:TI1 上升沿(TI1FP1)复位计数器 —— PWM 输入模式的核心
comp.timer_general_config(.{
.prescaler = 71,
.counter_mode = .{ .up = {} },
.sync_config = .{
.trigger_source = .TI1FP1,
.mode = .ResetMode,
},
});
comp.start();
// CH1(索引 0):直连 TI1,上升沿捕获 -> 锁存周期
comp.configure_ccr(0, .{
.ch_mode = .{ .capture = .{} },
});
// CH2(索引 1):input_alternate 把 IC2 映射到同一个 TI1,
// 极性 .low = 下降沿捕获 -> 锁存高电平宽度;只让 CH2 开中断
comp.configure_ccr(1, .{
.ch_mode = .{ .capture = .{ .mode = .input_alternate } },
.polarity = .low,
.channel_interrupt_enable = true,
});
comp.set_channel(0, true);
comp.set_channel(1, true);
comp.set_interrupt(true);
microzig.interrupt.enable_interrupts();
microzig.interrupt.enable(.TIM2);
while (true) {
std.log.info("period: {d} us", .{period_ticks});
std.log.info("high: {d} us", .{high_ticks});
std.log.info("freq: {d} Hz", .{freq_hz});
std.log.info("duty: {d}.{d:0>2}%", .{ duty_x100 / 100, duty_x100 % 100 });
time.sleep_ms(500);
}
}
逐段解析
输入捕获的信号链:从引脚到 CCR 的四级流水
一个捕获通道从引脚到锁存要过四道关卡(RM0008 §14.3.5 的框图值得打印一张贴墙上):
- TIx 输入:TIM2 的 CH1 默认在 PA0(部分重映射可到 PA15,完全重映射涉及 JTAG 引脚需释放)。引脚配
.floating浮空输入即可,电平由信号源驱动。 - TIxFP 滤波与映射:信号先过一个数字滤波器(CCMR 的 ICxF 位域,连续 N 个采样点相同才算有效沿,N 可配 1–8 档),再经输入选择器映射到通道。关键概念:通道与物理输入不是一一绑定的——IC1 默认接 TI1(
input_normal),IC2 也可以接 TI1(input_alternate),这正是"一个引脚两个通道"的来源。HAL 里CaptureModes枚举三个值逐字是:input_normal(正常映射)、input_alternate(交叉映射:IC1=TI2、IC2=TI1、IC3=TI4、IC4=TI3)、input_TRC(接触发输入 TRC)。 - 边沿检测与预分频:CCER 的 CCxP 选极性(
Polarity.high= 上升沿有效,.low= 下降沿有效),可选每 1/2/4/8 个有效沿才捕获一次(CapturePrescaler)。 - CCR 锁存:有效沿到达的瞬间,硬件把 CNT 抄进 CCRx 并置状态标志。这发生在 1 个时钟周期内,无软件参与。
对照代码:configure_ccr(0, ...) 配的是 CH1(通道号 0 基,第 10 篇的老约定),ch_mode = .{ .capture = .{} } 用了 Capture 结构的三个默认值——mode = .input_normal、prescaler = .no_prescaler、filter = .NoFilter。滤波全关意味着毛刺会原样进 ISR,实测若输入线较长,给 Capture.filter 配一档滤波(该枚举在 HAL 里只逐字确认了 .NoFilter,具体档位名以你拉到的源码为准;RM0008 上 ICxF 是 4 位、共 16 档采样窗口)。
PWM 输入模式:一个引脚、两个通道、三个寄存器
测占空比需要两个时间戳:上升沿时刻(周期起点)和下降沿时刻(高电平终点)。F103 的 PWM 输入模式用三个配置达成:
- 两个通道接同一个输入。CH1 用
input_normal直连 TI1,配.high极性(默认)→ 上升沿把 CNT 锁进 CCR1;CH2 用input_alternate交叉映射到 TI1,配polarity = .low→ 下降沿把 CNT 锁进 CCR2。因为计数器在每次上升沿被复位,CCR1 恰好等于"周期",CCR2 恰好等于"高电平宽度"。 - 从模式 Reset。
sync_config = .{ .trigger_source = .TI1FP1, .mode = .ResetMode }写 SMCR 的 TS/MS/SMS 位:把 TI1 经滤波后的信号(TI1FP1)选为触发源,触发模式设为 Reset——每个上升沿把 CNT 清零。没有这一步,CCR1 读到的会是"两次上升沿之间计数的差",你还得自己减;有了这一步,CCR1 直接就是周期。 - 只在 CH2 开通道中断。事件顺序是:上升沿(复位 CNT、锁 CCR1)→ 下降沿(锁 CCR2、置 channel2 标志)→ 下一个上升沿。所以当 ISR 因 channel2 进入时,两个 CCR 都已是本周期的完整数据——上游示例
if (flags.channel2)判断的依据就在这里。若把中断也开在 CH1 上,每次上升沿会多进一次 ISR,纯属浪费。
值得停下来想一想:ResetMode 下计数器被外部信号牵着走,这个定时器已经不能做时基/PWM 了——所以第 10 篇才约定时基放 TIM4、TIM2 留给捕获。另一个隐含事实:TIM2 的 HAL 中断向量被 time.TIM_handler 占用(default_interrupts 占了 TIM2/3/4/5 四个向量),所以 microzig_options 里必须写 .overwrite_hal_interrupts = true,否则编译期直接报 Interrupt TIM2 is used internally by the HAL。这个开关只影响你显式提供了 handler 的字段——TIM4 的节拍 handler 不受影响,sleep_ms 照常工作。
时钟与量程:为什么 prescaler 是 71 而不是 7
上游示例写 prescaler = 7,注释说 1 MHz 计数——那是在 .SYSCLKSource = .HSE(8 MHz 直驱,APB1 = 8 MHz)的时钟配置下。本系列的标准时钟是 72 MHz(HSE 8 MHz × PLL9,APB1 分频 2 得 36 MHz),挂 APB1 的 TIM2 适用第 10 篇讲过的 ×2 规则:APB 预分频 ≠ 1 时定时器输入 = PCLK × 2 = 72 MHz。所以 1 MHz 时基要 prescaler = 71(72 / 72 = 1 MHz),照抄上游的 7 会得到 9 MHz 计数、频率读数差 9 倍。
量程由 ARR 和 16 位计数器决定:TimerGenealConfig.auto_reload 默认 maxInt(u16) = 65535,ARR 不用改。1 MHz 时基下可测周期 1 µs – 65.5 ms,对应频率约 15.3 Hz – 1 MHz。要测更低频率就把 prescaler 加大(如 7199 → 10 kHz 时基,最低可测 0.15 Hz,分辨率降到 100 µs);要测更高频率则受"每周期至少要来一次捕获"限制,超过 1 MHz 应改用"外部时钟模式"数沿而非测周期。
精度:±1 LSB 从哪来,怎么压
量化误差的来源很朴素:边沿到达时刻落在两个计数节拍之间,锁存值与真实值最多差 1 tick。1 MHz 时基、1 kHz 信号下,周期读数 1000 ± 1,频率相对误差约 ±0.1%。三个改善手段按性价比排序:
- 多周期平均。主循环把最近 N 次
freq_hz求平均,随机性量化误差按 1/√N 收敛。N = 100 时 ±0.01%,代价只是显示刷新变慢。 - 延长测量窗口。用
CapturePrescaler(每 8 个沿捕获一次)或软件累计 M 个周期再除,等效于把分辨率提高 M 倍。 - 提高计数频率。prescaler 减小可压量化误差,但会同时压低量程上限——这是分辨率与量程的直接交换。
还有两个不归定时器管的误差源:滤波器延迟(ICxF 滤波让有效沿晚 N 个采样钟生效,周期测量两端同样延迟所以相消,占空比测量则不对称)与晶振精度(72 MHz 来自 ±20 ppm 的无源晶振,1 kHz 读数偏差约 0.02 Hz,比量化误差小一个量级)。反过来说:如果你的板上没有外部晶振、跑在 HSI(±1%,全温区更差)下,测频精度上限就是 HSI 本身——这也让输入捕获成为"反向校验时钟树"的工具:给 PA0 接一个已知精度的信号源,读数偏差百分比就是你的实际时钟偏差。
ISR 的整数化:一个上游示例留下的浮点尾巴
上游 timer_capture.zig 在 ISR 里做了 f32 除法(duty_cycle = (rev_fch2 / rev_fch1) * 100)。F103 是 Cortex-M3,没有 FPU,一次软浮点除法要几十到上百个周期,还会拉进整套软浮点库函数。1 kHz 信号下 ISR 每毫秒进一次,浮点开销勉强扛得住;但只要信号到 10 kHz,浮点 ISR 就会吃掉可观的 CPU 时间。本篇把占空比改存 duty_x100(×100 的整数),主循环打印时才拆成整数与小数部分——ISR 里只剩一次 u32 除法(硬件 UDIV,M3 上约 2–12 周期)。注意 @as(u32, high) * 100 的 widen:high 最大 65535,乘 100 后 650 万,u16 装不下,必须先提升到 u32。
运行现象
把 PB5(呼吸灯输出)跳线到 PA0,烧录后串口(115200 8N1)每 500 ms 打印一组:
period: 1000 us
high: 431 us
freq: 1000 Hz
duty: 43.10%
- 频率稳定在 1000 Hz(呼吸灯的 PWM 载波),duty 以约 100 步/秒的速度在 0–100% 间爬坡——这是第 10 篇的程序在另一颗定时器上跑出来的波形,被本篇程序"看见"了。两个程序、两颗定时器、一条杜邦线,构成一个完整的测量闭环。
- 用信号发生器打 10 kHz 方波验证量程:period ≈ 100 µs、duty ≈ 50.0%,误差在 ±1% 内。
- 把信号撤掉:CNT 停止复位,
channel2不再置位,串口保持打印上一次的值(ISR 没有再更新)。生产固件应在主循环加"超过 N ms 无新捕获就显示 --"的超时逻辑。 - 断开跳线、接回 KEY1 电路后,第 08 篇的 EXTI0 按键实验可正常恢复——两个功能不能同时用一个引脚,但可以换着用。
常见问题
Q1:测出来的频率是期望值的 9 倍(或 1/9)。
时钟基数错了。prescaler = 7 是上游示例在 8 MHz HSE 配置下的参数;72 MHz 配置下 TIM2 输入是 72 MHz(APB1 ×2 规则),要 1 MHz 时基必须写 71。自查方法:rcc.get_clock(.TIM) 返回的值就是 TimPrescalerAPB1——注意它是预分频器之前的定时器输入时钟(72 MHz),你的计数频率 = 它 / (PSC + 1)。凡是从别的时钟配置抄来的定时器参数,都要按本系列 72 MHz 重算一遍。
Q2:duty 一直显示 100% 或不变,freq 正常。
两个通道的极性/映射配反了。常见错误:CH2 没写 .polarity = .low(两个通道都在上升沿锁存,CCR2 ≈ CCR1,duty 恒 100%);或 CH2 漏写 input_alternate(IC2 接的是 TI2 = PA1,PA1 上没信号,CCR2 永远是 0)。对照检查三件套:CH1 input_normal + 默认 .high、CH2 input_alternate + .low、sync_config 的 trigger 必须是 .TI1FP1。
Q3:编译报 Interrupt TIM2 is used internally by the HAL。
HAL 的 default_interrupts 占用了 TIM2/TIM3/TIM4/TIM5 四个向量,自绑 ISR 必须加 .overwrite_hal_interrupts = true。加了之后注意连锁反应:只覆盖你写了 handler 的向量,其余照旧。千万不要顺手把时基从 TIM4 换成 TIM2 来"绕开"这个报错——那等于把节拍中断也交出去了,sleep_ms 会永远卡死。
Q4:PA0 既要接信号又要接按键,怎么共存?
不能同时共存,引脚只有一个输入端。三个方案按推荐排序:① 实验期跳线切换,测频时断开按键、平时断开信号线(本篇做法);② 换通道——把信号接到 PA5(TIM2_CH1 无重映射是 PA0,但 TIM2 部分重映射后 CH1/CH2 可到 PA15/PB3,需 AFIO 重映射 + 释放 JTAG 引脚,且 PA15/PB3 属于"默认被调试口占用"的高危脚);③ 需要固定长期测量时,捕获挪到 TIM3/TIM4 的空闲通道(如 PA6 = TIM3_CH1),时基照旧 TIM4——但注意这些通道的 timer_general_config 同样要按 72 MHz 算。
Q5:为什么不用 CH4/PA3 做 PWM 输入?接线表里 TIM2_CH4 明明是给捕获预留的。
硬件上 TI4FP4 触发源存在(RM0008 的 SMCR.TS 可以选 TI4FP4),PWM 输入模式在 CH3/CH4 上也能搭。但 microzig 的 SyncTriggerSource 枚举只有 8 个值:ITR0–ITR3、TI1F_ED、TI1FP1、TI2FP2、ETRF——没有 TI3FP3/TI4FP4。也就是说 HAL 表达不了"CH4 触发复位",这条路在 HAL 层走不通;绕过它要寄存器直写 SMCR/CCMR(并把 CH3/CH4 配成交叉映射对),超出本篇范围。所以接线表里 PA3 的定位是"预留",本篇实际用 PA0/CH1+CH2,并接受与 KEY1 的临时复用。
Q6:信号频率低于 15 Hz 后读数明显偏小甚至停在 65535。
超出了 16 位计数器量程:1 MHz 时基 + ARR = 65535,周期超过 65.5 ms 时 CNT 在下一个上升沿到来前就溢出回卷,CCR1 锁存的是回卷后的残值。处理:加大 prescaler 压低时基(如 71 → 719,量程下限变 1.5 Hz,分辨率降为 10 µs),或软件检测溢出中断扩展量程(开 enable_update_interrupt,在 ISR 里累计溢出次数,把 16 位周期扩展成 32 位)。反之,频率高于 1 MHz 时也应改思路:直接把 TI1 当外部时钟(ExternalClockMode1)数沿,1 s 窗口内的计数值就是频率。
面试考点
Q1:什么是输入捕获?相比 GPIO 中断打时间戳,它的精度优势来自哪里?
参考答案:输入捕获是定时器硬件在输入信号有效沿到达时自动把计数器当前值锁存进 CCR 的机制。优势有三:① 时间戳由硬件产生,延迟固定为约 1 个计数时钟,不受中断响应、入栈、优先级抢占影响;② 无需 CPU 参与,高频信号不会像中断方案那样在约 500 kHz 后把 CPU 耗尽;③ PWM 输入模式下一次配置可同时得到周期与占空比,而中断方案要两种边沿各进一次 ISR、误差翻倍。残余误差主要是 ±1 计数节拍的量化误差,可用多周期平均或延长测量窗口压缩。
Q2:画出 PWM 输入模式的配置要点,说明 CCR1 和 CCR2 分别对应什么。
参考答案:三个要点:① CH1 与 CH2 接同一物理输入——CH1 用 input_normal 直连 TIx,CH2 用 input_alternate 交叉映射到同一 TIx;② 边沿相反——CH1 极性 high(上升沿)、CH2 极性 low(下降沿);③ 从模式 Reset,触发源选 TIxFP1,每个上升沿把计数器清零。结果:每次上升沿 CCR1 锁存的计数值即信号周期,同周期内的下降沿使 CCR2 锁存高电平宽度,占空比 = CCR2/CCR1。中断通常只开在"后发生"的那个通道(下降沿通道),因为该事件到达时两个 CCR 都已就绪。
Q3:F103 定时器的输入滤波器(TIxFP)作用是什么?它对测量精度有什么影响?
参考答案:ICxF 滤波器要求输入信号在连续 N 个采样钟周期内保持一致才承认有效沿,N 共 16 档(RM0008 IC1F[3:0]),用于抑制窄毛刺、抖动和长线干扰。影响:① 滤波引入固定延迟(约 N 个采样钟),周期测量中上升沿与下一个上升沿的延迟相同、相互抵消,不影响周期读数;② 占空比测量中上升沿与下降沿的滤波延迟不对称,高电平宽度会有偏差;③ 滤波采样钟通常取定时器输入时钟经分频(CKD 与 ICxF 联合决定),滤波档位越高,可测最小脉宽越大。工程取舍:短线上用低档滤波或不滤,长线/机械触点用高档滤波。
Q4:72 MHz 系统时钟下要把 TIM2 配成 1 MHz 计数,PSC 写多少?为什么不是 7?
参考答案:写 71。TIM2 挂 APB1(PCLK1 = 36 MHz),适用"APB 预分频 ≠ 1 时定时器时钟 ×2"规则,输入为 72 MHz;PSC 是先 +1 再相除的,72 MHz/(71+1) = 1 MHz。写 7 得到 9 MHz 计数,频率读数会差 9 倍。这个错误的常见来源是照抄别的时钟配置(如 8 MHz HSE 直驱)下的示例参数。通用公式:PSC = 定时器输入时钟/期望计数频率 − 1,且先确认输入时钟是 PCLK 还是 PCLK×2。
Q5:ISR 里做浮点运算有什么问题?在 Cortex-M3 上如何正确计算占空比?
参考答案:M3 没有 FPU,f32 除法由编译器调用软浮点库实现,一次几十到上百周期,还拉进整段库代码。高频率捕获事件下 ISR 执行时间可能超过信号周期,导致标志未清完就重入或主循环饿死。正确做法:ISR 内只用整数运算,把结果以定标整数保存(如 duty_x100 = high × 100 / period,注意中间乘法提升到 u32 防溢出),格式化与换算推迟到主循环。若必须浮点(如对数运算),把浮点部分放主循环,ISR 只传递原始计数值。
Q6:microzig 的 overwrite_hal_interrupts = true 到底覆盖了什么?本篇为什么敢开着它用 TIM4 做时基?
参考答案:向量表装配时(generate_vector_table),对每个用户提供了 handler 且 HAL 也有默认 handler 的字段,若开关为 false 则编译期报错;为 true 则用用户的 handler。它逐字段生效——本篇只写了 .TIM2 一个字段,所以只有 TIM2 的向量被换成捕获 ISR,TIM3/TIM4/TIM5 仍指向 HAL 的 time.TIM_handler。时基放 TIM4 后,sleep_ms 靠 TIM4 的更新中断推进,完全不受影响。前提是不把 TIM4 的向量也覆盖掉,否则 time 模块的节拍丢失、sleep_ms 永久阻塞。
小结
- 输入捕获信号链:TIx 引脚 → TIxFP 滤波/映射 → 边沿检测(CCER 极性)→ 硬件锁存 CCR,时间戳精度 ±1 计数节拍。
- PWM 输入模式三件套:双通道接同一输入(normal + alternate)、极性相反、从模式 Reset(trigger_source =
.TI1FP1);CCR1 = 周期、CCR2 = 高电平宽度,中断只开后发的通道。 - 通道号 0 基:
configure_ccr(0, ...)是 CH1;read_ccr(0)读 CCR1。 - 72 MHz 配置下 TIM2 时基参数:
prescaler = 71得 1 µs/tick;量程 15.3 Hz–1 MHz,低频加大 prescaler,高频改数沿。 - TIM2 自绑 ISR 必须
overwrite_hal_interrupts = true;时基仍由 TIM4 承担,sleep_ms不受影响。 - HAL 的
SyncTriggerSource没有 TI4FP4,PWM 输入只能落在 CH1+CH2,PA0 与 KEY1 临时复用;PA3 的捕获留给寄存器层方案。 - ISR 内整数定标(duty_x100)替代浮点,M3 无 FPU 的标准姿势。
- 精度三板斧:多周期平均、延长测量窗口、提高计数频率;误差来源还有滤波延迟与晶振精度。
- 下一篇把目光转向串口:中断驱动的接收 + 环形缓冲区,解决第 09 篇轮询收发的阻塞问题。
系列目录
- 系列总览: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号