尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32输入捕获原理与工业级应用实战

STM32输入捕获原理与工业级应用实战 1. 输入捕获不是“测脉宽”那么简单先搞清你到底想抓什么信号很多人一看到“STM32输入捕获”第一反应就是“测频率”或“测占空比”然后翻出江科大视频、正点原子例程照着 HAL_TIM_IC_Init 和 HAL_TIM_IC_Start_IT 一顿配置烧进去一跑——咦波形不对数值跳变偶尔还卡死。我当年在做两轮差速小车的编码器测速模块时就在这上面栽了整整三天。后来才发现问题根本不在代码写错而在于连“输入捕获到底要解决什么问题”都没想清楚。输入捕获的本质是用定时器的计数器作为高精度时间标尺去标记外部引脚电平跳变发生的精确时刻。它不直接输出“频率是多少Hz”也不直接告诉你“占空比是75%”它只干一件事在上升沿或下降沿到来的瞬间把当前计数器的值CNT锁存进捕获寄存器CCR。后续所有计算——频率、周期、脉宽、相位差——都是基于这一组时间戳做减法和换算得来的。这就决定了输入捕获的成败首先取决于你对被测信号的物理特性和应用场景的准确理解。比如如果你接的是编码器A/B相正交信号那你需要同时捕获两个通道的边沿并判断边沿顺序来确定旋转方向。这时单次捕获没意义必须做状态机解析如果你接的是超声波模块的回波信号如HC-SR04那是一个窄脉冲几十微秒要求定时器能稳定分辨1us甚至更小的时间间隔且必须处理好溢出重载如果你接的是红外遥控NEC协议的载波信号那是一个高频38kHz调制波你需要先用滤波电路或软件消抖再捕获其包络的起始和结束位置如果你接的是伺服电机控制信号如PWM舵机标准脉宽是0.5ms~2.5ms对应0°~180°但实际应用中可能有抖动、噪声必须设计合理的滤波窗口和超时机制。这些场景底层都是同一个硬件模块通用定时器的输入捕获通道但上层逻辑天差地别。HAL库的 HAL_TIM_IC_Start_IT 只是启动了中断服务真正的“灵魂”在你的回调函数里——你如何解读那一连串 CCR 值才是决定项目成败的关键。所以在动手写任何一行初始化代码之前请务必自问三个问题这个信号的典型周期/频率范围是多少决定定时器预分频PSC和自动重装载ARR的取值信号边沿的抖动幅度大概多大决定是否需要硬件滤波器或软件消抖阈值我最终需要的输出是什么是实时频率值、单次脉宽、还是连续的边沿序列决定数据结构设计和中断处理策略这三个问题的答案将直接决定你后续所有配置参数的选择逻辑而不是盲目套用“PSC72-1, ARR65535”这种万能公式。我见过太多人因为没想清楚第三点导致中断里疯狂memcpy最后内存溢出系统崩溃——其实他只需要一个最大值和一个最小值。2. 通用定时器框图不是装饰画从寄存器级看捕获通路如何工作很多初学者把“通用定时器框图”当成教材插图扫一眼就过。但当你调试捕获不准、数值乱跳时这张图就是唯一的救命稻草。我拆解过 STM32F103 和 STM32G030 的参考手册发现它们的输入捕获通路核心逻辑高度一致只是寄存器命名略有差异。下面以 F103 为例带你真正“看见”信号是怎么一步步被捕捉下来的。整个通路可以分为四个关键阶段每个阶段都有可配置的寄存器2.1 输入滤波与边沿选择TIMx_CCMRx TIMx_CCER信号从 GPIO 引脚进来第一站是输入滤波器。这个滤波器不是简单的 RC 电路而是由定时器内部的数字逻辑实现的采样计数模式。它通过设置 CCMRx 寄存器中的 ICxF[3:0] 字段决定对输入信号进行多少个定时器时钟周期CK_INT的采样只有当连续 N 个采样值都为高或低时才认为有效边沿发生。提示ICxF0b0001 表示采样4个 CK_INT 周期这是最常用值。如果信号噪声大可设为 0b0011采样8周期但会牺牲响应速度。千万别设为 0b0000无滤波工业现场几乎必抖。紧接着是边沿极性选择由 CCER 寄存器的 CCxP 和 CCxNP 位控制。CCxP0 是上升沿触发CCxP1 是下降沿触发CCxNP 配合使用可实现双边沿捕获需配合软件翻转极性。这里有个极易忽略的细节边沿选择是在滤波之后生效的。也就是说滤波器先“确认”这是一个有效电平变化然后才由极性位决定是否触发捕获动作。2.2 捕获预分频与触发TIMx_CCMRx TIMx_SMCR滤波后的有效边沿会触发一个“捕获事件”。但这个事件不一定每次都锁存 CNT 值——这由捕获预分频器ICPS控制。ICPS 字段允许你设置“每N个有效边沿才进行一次捕获”比如 ICPS0b01 表示每2个边沿捕获一次。这在测高频信号时很有用可以降低中断频率。更关键的是触发源选择。通用定时器支持多种触发方式TI1FP1通道1输入、TI2FP2通道2输入、ETRF外部触发等。对于标准输入捕获我们用 TI1FP1。但要注意TI1FP1 本身又受 CCMRx 中的 CCxS 位控制——CCxS0b01 才表示通道1用于输入捕获而不是 PWM 输出。2.3 计数器同步与锁存TIMx_CNT TIMx_CCRx当满足所有条件有效边沿 极性匹配 预分频计数完成定时器就会执行一次“捕获动作”将当前计数器CNT的值原子性地复制到对应的捕获寄存器 CCRx 中。这个过程是硬件自动完成的无需 CPU 干预保证了时间戳的绝对精确。这里有个致命陷阱CNT 是 16 位寄存器0~65535它会自动溢出归零。如果你的信号周期很长比如1秒而 ARR 设为 65535那么 CNT 在1秒内会溢出多次。此时单纯读 CCRx 的值毫无意义你必须结合更新事件UEV中断记录每次溢出的次数才能还原出真实的绝对时间戳。2.4 中断与 DMA 请求TIMx_DIER TIMx_SR最后捕获事件会置位状态寄存器SR中的 CCxIF 标志位。如果使能了捕获中断DIER 的 CCxIE1CPU 就会进入中断服务函数。HAL 库的 HAL_TIM_IC_IRQHandler 就是干这个活的它读取 SR清除 CCxIF然后调用用户注册的回调函数 HAL_TIM_IC_CaptureCallback。注意HAL_TIM_IC_Start_IT 启动的是“中断模式”但你也可以用 HAL_TIM_IC_Start_DMA 启动 DMA 模式让 CCRx 的值自动搬运到内存数组。DMA 模式适合高速连续捕获如音频采样但对初学者来说中断模式更直观、更易调试。这张框图的价值不在于让你背下所有寄存器地址而在于当你遇到“捕获值总为0”、“中断不触发”、“数值突变”等问题时你能像侦探一样沿着这条通路逐段排查是滤波没过是极性设反是预分频阻塞还是溢出没处理——这才是高手和新手的根本区别。3. HAL_TIM_IC_Init 不是魔法咒语参数背后的物理意义与计算逻辑HAL_TIM_IC_Init 函数看起来很简单传入一个 TIM_IC_InitTypeDef 结构体就完事。但里面每个字段都不是随便填的它们共同决定了你的定时器能否在物理层面“看清”目标信号。我曾帮一个做智能台灯的团队调过光敏电阻输出的 PWM 信号他们 PSC 设为 71ARR 设为 999结果测出来频率误差高达 ±15%。问题就出在没理解 PSC 和 ARR 的物理含义。我们来拆解这个结构体的核心字段typedef struct { uint32_t ICPolarity; // 边沿极性上升沿/下降沿/双边沿 uint32_t ICSelection; // 输入通道选择直连/交叉/滤波后 uint32_t ICPrescaler; // 捕获预分频每N个边沿捕获一次 uint32_t ICFilter; // 输入滤波器采样周期数0~15 } TIM_IC_InitTypeDef;其中ICPolarity和ICFilter前面已讲过这里重点说ICPrescaler和ICFilter的协同关系以及它们与TIM_TimeBaseInitTypeDef中Prescaler和Period的联动。3.1 定时器基础时钟CK_INT的源头所有计算的起点是定时器的输入时钟 CK_INT。它通常来自 APB1 或 APB2 总线时钟取决于定时器编号经过一个倍频器得到。例如F103 的 APB1 时钟为 36MHzTIM2~TIM7 的 CK_INT APB1CLK * 1 36MHz而 APB2 时钟为 72MHzTIM1/TIM8 的 CK_INT APB2CLK * 2 144MHz因APB2预分频为1。提示务必查你芯片的数据手册“Clock tree”章节确认 CK_INT 的真实频率。很多人的错误源于误以为 CK_INT 就是系统主频。3.2 PSCPrescaler决定时间分辨率的基石PSC 是一个 16 位预分频器它把 CK_INT 分频后得到定时器的计数时钟CK_CNT。CK_CNT 的频率 CK_INT / (PSC 1)。这个值直接决定了你的时间分辨率。例如CK_INT 72MHzPSC 71 → CK_CNT 1MHz → 每个计数器 tick 1usCK_INT 72MHzPSC 7199 → CK_CNT 10kHz → 每个 tick 100us选 PSC 的原则是你的最小可分辨时间tick必须小于信号最小特征时间的 1/4。比如测 10kHz 方波周期100us你至少需要 25us 的分辨率即 CK_CNT ≥ 40kHzPSC ≤ (72M/40k)-1 ≈ 1799。3.3 ARRAuto-Reload Register决定测量范围的边界ARR 设定了计数器的最大值。CNT 从 0 开始计数到达 ARR 后产生更新事件UEV并归零重新计数。因此一次完整计数周期的时间 (ARR 1) / CK_CNT。这个周期必须大于你要测量的最大信号周期。例如测电机编码器最大转速对应最小周期为 1ms则 ARR 至少要满足(ARR 1) / CK_CNT 1ms。但 ARR 也不能太大否则溢出处理复杂。一个经验公式是ARR ≈ (预期最大周期 × CK_CNT) × 0.8留 20% 余量给溢出处理。3.4 ICFilter数字滤波器的采样窗口ICFilter 决定了滤波器的采样周期数 N其实际滤波时间 N × (PSC 1) / CK_INT。例如CK_INT 72MHzPSC 71则 CK_CNT 1MHz每个 tick 1us。若 ICFilter 0b0001N4则滤波窗口 4us。这个窗口必须大于信号的最大抖动时间但小于信号的最小边沿间隔。比如一个干净的按键信号抖动约 10ms你可以设 ICFilter0b1100N12滤波窗口12us但若测 1MHz 方波边沿间隔1usICFilter 必须 ≤ 0b0000N1否则会漏掉边沿。3.5 实战参数计算表以测 1kHz~10kHz 方波为例参数计算逻辑推荐值理由CK_INT查手册F103 TIM2~TIM7 APB1CLK 36MHz36MHz基础时钟PSC要求分辨率 ≤ 100ns → CK_CNT ≥ 10MHz → PSC ≤ (36M/10M)-1 2.6 → 取 22得到 CK_CNT 12MHztick 83.3nsARR最大周期1ms → (ARR1) 1ms × 12MHz 12000 → 取 1500015000留余量周期1.25msICFilter信号抖动 1us → N × 83.3ns 1us → N 12 → 取 40b0001平衡抗噪与响应速度这套参数比网上泛滥的“PSC71, ARR65535”精准得多也更可靠。记住参数不是抄来的是算出来的。4. HAL_TIM_IC_Start_IT 启动后中断回调里藏着所有真相HAL_TIM_IC_Start_IT 的作用仅仅是使能捕获中断并启动定时器计数。真正的“捕获逻辑”全部在你写的 HAL_TIM_IC_CaptureCallback 回调函数里。很多人的代码在这里就分出了高下有人写个 printf 打印 CCR 值就完事有人却能实现亚微秒级精度的频率跟踪。差别就在这个回调函数的设计哲学。我以一个最典型的场景——实时测量方波频率——为例展示一个工业级可用的回调实现。它不仅要准还要稳、要快、要防错。4.1 基础版只读 CCR不处理溢出危险uint32_t last_ccr 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if(htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { uint32_t ccr HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); uint32_t diff ccr - last_ccr; last_ccr ccr; // diff 就是周期tick数乘以 tick 时间得周期s float period_s diff * 83.3e-9f; // 假设 tick83.3ns float freq_hz 1.0f / period_s; printf(Freq: %.1f Hz\n, freq_hz); } }这段代码的问题极其隐蔽没处理溢出当 CCR 从 65535 跳到 0 时diff 变成巨大负数没消抖单次测量受噪声影响大没防抖动如果信号短暂失锁diff 会异常大导致 freq 瞬间飙高。4.2 工业版环形缓冲 溢出校正 中值滤波#define CAPTURE_BUF_SIZE 16 static uint32_t capture_buf[CAPTURE_BUF_SIZE]; static uint8_t buf_head 0, buf_tail 0; static uint32_t overflow_count 0; // 溢出计数器 static uint32_t last_captured 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if(htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { // 1. 读取当前 CCR 值 uint32_t ccr HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 2. 检查更新事件溢出更新 overflow_count if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE); overflow_count; } // 3. 计算绝对时间戳考虑溢出 uint64_t abs_time ((uint64_t)overflow_count 16) | ccr; // 4. 计算与上次捕获的时间差delta uint64_t delta abs_time - last_captured; last_captured abs_time; // 5. 存入环形缓冲仅存 delta单位tick capture_buf[buf_head] (uint32_t)delta; buf_head (buf_head 1) % CAPTURE_BUF_SIZE; if(buf_head buf_tail) buf_tail (buf_tail 1) % CAPTURE_BUF_SIZE; // 缓冲满则覆盖最老值 // 6. 从中值滤波后的 delta 计算频率 if(buf_head ! buf_tail) // 缓冲非空 { uint32_t sorted[CAPTURE_BUF_SIZE]; memcpy(sorted, capture_buf, sizeof(capture_buf)); // 简单冒泡排序取中值实际用快速排序 for(int i0; iCAPTURE_BUF_SIZE; i) for(int ji1; jCAPTURE_BUF_SIZE; j) if(sorted[i] sorted[j]) { uint32_t t sorted[i]; sorted[i] sorted[j]; sorted[j] t; } uint32_t median_delta sorted[CAPTURE_BUF_SIZE/2]; // 转换为频率Hz float tick_us 83.3f; // PSC2 时的 tick 时间us float period_us median_delta * tick_us; float freq_hz 1000000.0f / period_us; // 7. 输出避免在中断里 printf改用队列或标志位 latest_freq freq_hz; freq_updated 1; } } }这个版本解决了所有基础版的缺陷溢出校正用overflow_count和ccr拼接成 64 位绝对时间戳彻底消除溢出导致的跳变环形缓冲避免动态内存分配实时性好中值滤波比均值滤波更能抵抗脉冲噪声无阻塞输出freq_updated标志位由主循环读取避免中断里调用重函数。4.3 关键避坑经验血泪总结永远不要在中断里做浮点运算1.0f / period_s看似简单但在 Cortex-M3/M0 上会触发 FPU 异常如果未使能。正确做法是预先计算好1.0f / (tick_us * 1000000.0f)存为常量。CCR 读取后立即清标志HAL_TIM_ReadCapturedValue 内部会调用__HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_CC1)但如果你手动读htim-Instance-CCR1必须自己清标志否则下次中断不触发。双边沿捕获要翻转极性如果用双边沿测 PWM 占空比必须在每次捕获后用HAL_TIM_IC_ConfigChannel()动态切换ICPolarity否则第二次捕获会失败。PA0 口的特殊性G030 的 PA0 默认复位为 BOOT0 功能必须在__HAL_RCC_GPIOA_CLK_ENABLE()后立即调用__HAL_AFIO_REMAP_PA0()解除重映射否则输入捕获无效。这些细节教科书不会写视频教程很少提但它们就是你项目能否稳定运行的分水岭。5. 从 STM32G030 到 S32K312不同芯片的输入捕获实践差异虽然 STM32 和 S32K 都是 ARM Cortex-M 内核但它们的定时器外设架构、时钟树、HAL 库封装存在显著差异。很多开发者把 STM32 的代码直接移植到 S32K312 上结果捕获功能完全失效。这不是代码错了而是没意识到底层硬件的“方言”变了。我做过一个跨平台项目用同一套逻辑在 G030测电机转速和 S32K312测车载 CAN 信号边沿上实现输入捕获踩过不少坑。下面对比关键差异点5.1 时钟配置S32K312 的 PCC 是个“开关阵列”STM32 的时钟使能是统一的__HAL_RCC_TIMx_CLK_ENABLE()而 S32K312 使用PCCPeripheral Clock Control模块每个外设有自己的时钟门控寄存器。例如使能 TIM0 的代码是// STM32HAL库 __HAL_RCC_TIM2_CLK_ENABLE(); // S32K312S32DS SDK PCC-PCCn[PCC_TMR0_INDEX] PCC_PCCn_CGC_MASK; // 使能 TMR0 时钟更麻烦的是S32K312 的定时器时钟源不是直接来自 PLL而是经过一个TMRx_CLK_SEL多路复用器。默认可能是 IRC48MHz而非你期望的 SYS_CLK160MHz。必须显式配置// 选择 SYS_CLK 作为 TMR0 时钟源 TMR0-MCR ~TMR_MCR_CLKSRC_MASK; TMR0-MCR | TMR_MCR_CLKSRC(1); // 1SYS_CLK没这一步你的 PSC 计算全错CK_CNT 远低于预期。5.2 输入捕获通道映射S32K312 的“输入复用器”更复杂STM32 的 GPIO 与定时器通道是固定映射的如 PA0 - TIM2_CH1而 S32K312 的每个定时器通道TMRx_CHy可以连接到多个 GPIO 引脚通过SIUL2System Integration Unit Level 2的 PAD_MUX 寄存器配置。例如TMR0_CH0 可以接 PTB0、PTC0、PTD0 三个引脚。你必须在 SIUL2 的PAD_MUX寄存器中将目标引脚的 MUX 设置为ALT3TMR0_CH0 功能在PAD_CTL寄存器中配置该引脚为输入、启用上拉/下拉在 TMR0 的CHANNELy_CTRL寄存器中使能捕获功能。这三步缺一不可而 STM32 只需HAL_GPIO_Init()和HAL_TIM_IC_ConfigChannel()两步。5.3 HAL 库 API 的“同名异义”S32K312 的 SDK 也提供了类似 HAL 的 API如TMR_DRV_Init()和TMR_DRV_SetInputCapture()但参数结构体完全不同// STM32 HAL TIM_IC_InitTypeDef sConfigIC; sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0x0; HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_1); // S32K312 SDK tmc_input_capture_config_t ic_config; ic_config.channel TMR_CH_0; ic_config.edge TMR_EDGE_RISING; ic_config.filter TMR_FILTER_4_CYCLES; // 直接枚举非数值 ic_config.prescaler TMR_PRESCALE_1; // 直接枚举 TMR_DRV_SetInputCapture(TMR0, ic_config);表面看只是参数名不同但TMR_FILTER_4_CYCLES对应的硬件行为可能与 STM32 的ICFilter0b0001不完全等价。必须查 S32K312 的 Reference Manual确认其滤波器采样周期是基于哪个时钟SYS_CLK 还是 TMR_CLK。5.4 实战建议跨平台开发的“最小公约数”策略如果你的项目需要在多个芯片平台STM32G030、S32K312、甚至 ESP32上运行输入捕获我强烈建议放弃“一套代码跑所有平台”的幻想采用分层抽象策略底层驱动层Platform-Dependent为每个芯片单独实现platform_ic_init()、platform_ic_start()、platform_ic_get_value()。这部分代码量不大但必须精准匹配硬件。中间逻辑层Platform-Independent只处理时间戳、溢出校正、滤波算法。输入是uint64_t timestamp输出是float frequency。这部分代码可 100% 复用。应用层只调用中间层的 API完全不知道底层是 STM32 还是 S32K。这样当你要支持新芯片时只需重写 20 行底层驱动而核心算法、滤波逻辑、状态机全部不动。我在做“两轮差速小车”项目时就是用这个策略一周内就把 STM32F4 的编码器测速模块无缝迁移到了 S32K312 的底盘控制器上。6. 输入捕获的终极战场伺服电机控制与 HTTP 库的意外交集输入捕获最常见的用途是测频、测脉宽但它的真正价值往往体现在一些“跨界”场景里。我最近在一个基于 STM32 的智能灌溉系统中就用输入捕获解决了一个看似与它八竿子打不着的问题如何在资源极度受限的 G030 上实现可靠的 HTTP POST 请求超时检测。这个系统用 ESP8266 作为 Wi-Fi 模块STM32G030 作为主控通过 UART 与 ESP 通信。问题来了ESP8266 有时会卡死、无响应或者返回格式错误的 JSON。如果主控一直等它回复整个系统就僵死了。传统方案是用 SysTick 做软件定时器但 G030 的 RAM 只有 8KBSysTick 中断频繁抢占影响其他任务如 ADC 采样、PWM 输出。我的解决方案是把 ESP8266 的 UART RX 引脚接到 STM32 的一个定时器输入捕获通道上。6.1 思路用硬件“监听”通信活性UART 数据是串行的每个字节之间有固定的空闲时间idle time。正常通信时RX 引脚会频繁出现下降沿起始位。如果 ESP 长时间没发数据RX 就一直保持高电平空闲态。那么只要检测到“两次下降沿之间的时间间隔 预设阈值”就判定为超时。这本质上就是一个测最大空闲时间的问题完美契合输入捕获的能力。6.2 具体实现步骤硬件连接将 ESP8266 的 TX即 STM32 的 RX引脚接到 TIM2_CH1PA0。定时器配置PSC 71CK_CNT 1MHztick 1usARR 0xFFFF65535最大测时长 65.535ms足够覆盖 UART 字节间隔ICFilter 0b0000无滤波UART 信号干净ICPolarity TIM_INPUTCHANNELPOLARITY_FALLING捕获下降沿即起始位中断回调逻辑static uint32_t last_fall_time 0; static uint32_t max_idle_us 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if(htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { uint32_t ccr HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); uint32_t delta_us ccr - last_fall_time; // 单位us last_fall_time ccr; if(delta_us max_idle_us) { max_idle_us delta_us; // 如果超过 50ms认为 ESP 卡死 if(max_idle_us 50000) { esp_timeout_flag 1; // 触发复位 ESP 或切换备用通信路径 } } } }主循环中重置每次成功收到 ESP 的完整响应如 IPD,就清零max_idle_us和last_fall_time。6.3 为什么这比软件定时器更优零 CPU 占用捕获和计时全部由硬件完成CPU 只在有边沿时才中断平时完全不干预超高精度1us 级别的时间测量远超 SysTick 的 1ms 分辨率资源节省不需要额外的定时器、不需要维护软件计数器、RAM 占用趋近于零可靠性高不受其他中断延迟影响即使主循环卡在某个地方硬件捕获依然在工作。这个案例说明输入捕获的价值远不止于“测信号”。它是 STM32 通用定时器赋予你的一把“时间显微镜”只要你能想到需要精确测量时间间隔的场景它就很可能成为最优解。无论是测伺服电机的 PWM 周期、测红外遥控的码间间隔、还是测网络模块的响应活性底层逻辑都是相通的把物理世界的“时间”转化为数字世界的“计数器值”。我在实际项目中发现真正用好输入捕获的人往往不是那些背熟 HAL 函数的人而是那些习惯问“这个物理现象用时间怎么描述”的人。当你开始用时间维度去思考问题STM32 的定时器就从一个外设变成了你嵌入式系统的“第六感”。
返回列表