<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>编码器 on Tech Snippets - 嵌入式技术笔记</title><link>https://tech-snippets.xyz/tags/%E7%BC%96%E7%A0%81%E5%99%A8/</link><description>Recent content in 编码器 on Tech Snippets - 嵌入式技术笔记</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Fri, 08 May 2026 19:00:00 +0800</lastBuildDate><atom:link href="https://tech-snippets.xyz/tags/%E7%BC%96%E7%A0%81%E5%99%A8/index.xml" rel="self" type="application/rss+xml"/><item><title>STM32 定时器高级应用实战指南——PWM 输出、输入捕获、编码器模式与 HAL 库优化</title><link>https://tech-snippets.xyz/posts/stm32-timer-advanced-guide/</link><pubDate>Fri, 08 May 2026 19:00:00 +0800</pubDate><guid>https://tech-snippets.xyz/posts/stm32-timer-advanced-guide/</guid><description>前言 在嵌入式开发的世界里，定时器（Timer）堪称单片机的&amp;quot;瑞士军刀&amp;quot;。从简单的延时函数、周期性任务调度，到复杂的电机驱动控制、高精度脉冲测量、通信时序生成，定时器的身影无处不在。对于 STM32 这样的 Cortex-M 架构微控制器来说，定时器外设的丰富程度和灵活性，更是其区别于普通 8 位单片机的核心优势之一。
然而，很多开发者对 STM32 定时器的使用仅仅停留在&amp;quot;定时中断&amp;quot;这个最基础的层面——配置好自动重装载值，使能中断，然后在中断服务函数里翻转一下 LED。对于定时器的高级功能，如 PWM 输出、输入捕获、编码器接口等，要么知之甚少，要么只会通过 CubeMX 生成代码后&amp;quot;照着例子抄&amp;quot;，遇到问题时根本不知道从何排查。
根据我多年的嵌入式开发经验，真正拉开单片机开发者水平差距的，往往不是会不会用某个外设，而是能不能把外设的性能发挥到极致。一个只会用定时器做延时的工程师，和一个能熟练运用编码器接口做闭环控制的工程师，其解决问题的能力和项目贡献度完全不在一个量级。
本文将从实际应用出发，系统讲解 STM32 定时器的四大高级功能：PWM 输出、输入捕获、编码器接口和输出比较。我们不会停留在寄存器层面的理论讲解，而是通过大量可直接运行的代码示例、实测数据和常见问题排查指南，带你真正掌握定时器的高级应用技巧。无论你是刚接触 STM32 的新手，还是有多年经验的老司机，这篇文章都会帮助你构建完整的定时器应用知识体系。
一、STM32 定时器家族概览 在深入具体应用之前，我们首先需要搞清楚 STM32 到底有多少种定时器，它们各自的特点是什么。很多初学者看到 STM32 的数据手册里 TIM1、TIM2、TIM8&amp;hellip;一大堆定时器型号就头大，不知道该怎么选择。其实只要掌握了分类方法，一切就都清晰了。
1.1 定时器的分类与特点 STM32 的定时器按照功能复杂度可以分为四大类：
高级控制定时器（TIM1、TIM8）：这是功能最强大的定时器，通常挂载在 APB2 总线上。除了基本定时功能外，还具备互补输出、死区时间插入、刹车输入等高级功能，专门为电机控制和开关电源设计。如果你需要做三相 BLDC 电机的 FOC 控制，或者需要带死区的 PWM 输出，高级定时器是唯一选择。
通用定时器（TIM2-TIM5，TIM9-TIM14）：这是使用最广泛的一类定时器，挂载在 APB1 或 APB2 总线上。通用定时器具备完整的四大功能：定时中断、PWM 输出、输入捕获、编码器接口。其中 TIM2 和 TIM5 是 32 位计数器，其余是 16 位。对于 90% 以上的应用场景，通用定时器都是最佳选择。
基本定时器（TIM6、TIM7）：功能最简单，只能做基本定时和 DAC 触发，没有输入输出通道。优点是资源占用小，中断优先级配置简单。通常用于周期性任务调度、ADC 采样触发等场景。
低功耗定时器（LPTIM）：专为低功耗应用设计，可以在停止模式下继续运行，使用 LSE 或 LSI 作为时钟源。适合电池供电设备的周期性唤醒场景。
1.2 定时器资源快速选择表 功能需求 推荐定时器 备注 简单定时中断 TIM6/TIM7 资源开销最小 PWM 输出（单路） TIM2-TIM5 任意通用定时器 PWM 输出（多通道同步） TIM1/TIM8 高级定时器同步性更好 脉冲宽度/频率测量 TIM2-TIM5 输入捕获功能 旋转编码器解码 TIM2-TIM5 需要 TI1+TI2 双通道 电机控制（带死区） TIM1/TIM8 必须用高级定时器 长周期定时（&amp;gt;10秒） TIM2/TIM5 32位计数器优势明显 1.</description><content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>在嵌入式开发的世界里，定时器（Timer）堪称单片机的&quot;瑞士军刀&quot;。从简单的延时函数、周期性任务调度，到复杂的电机驱动控制、高精度脉冲测量、通信时序生成，定时器的身影无处不在。对于 STM32 这样的 Cortex-M 架构微控制器来说，定时器外设的丰富程度和灵活性，更是其区别于普通 8 位单片机的核心优势之一。</p>
<p>然而，很多开发者对 STM32 定时器的使用仅仅停留在&quot;定时中断&quot;这个最基础的层面——配置好自动重装载值，使能中断，然后在中断服务函数里翻转一下 LED。对于定时器的高级功能，如 PWM 输出、输入捕获、编码器接口等，要么知之甚少，要么只会通过 CubeMX 生成代码后&quot;照着例子抄&quot;，遇到问题时根本不知道从何排查。</p>
<p>根据我多年的嵌入式开发经验，<strong>真正拉开单片机开发者水平差距的，往往不是会不会用某个外设，而是能不能把外设的性能发挥到极致</strong>。一个只会用定时器做延时的工程师，和一个能熟练运用编码器接口做闭环控制的工程师，其解决问题的能力和项目贡献度完全不在一个量级。</p>
<p>本文将从实际应用出发，系统讲解 STM32 定时器的四大高级功能：PWM 输出、输入捕获、编码器接口和输出比较。我们不会停留在寄存器层面的理论讲解，而是通过大量可直接运行的代码示例、实测数据和常见问题排查指南，带你真正掌握定时器的高级应用技巧。无论你是刚接触 STM32 的新手，还是有多年经验的老司机，这篇文章都会帮助你构建完整的定时器应用知识体系。</p>
<p><img alt="STM32 定时器架构与工作模式" loading="lazy" src="/images/stm32-timer-architecture.svg"></p>
<h2 id="一stm32-定时器家族概览">一、STM32 定时器家族概览</h2>
<p>在深入具体应用之前，我们首先需要搞清楚 STM32 到底有多少种定时器，它们各自的特点是什么。很多初学者看到 STM32 的数据手册里 TIM1、TIM2、TIM8&hellip;一大堆定时器型号就头大，不知道该怎么选择。其实只要掌握了分类方法，一切就都清晰了。</p>
<h3 id="11-定时器的分类与特点">1.1 定时器的分类与特点</h3>
<p>STM32 的定时器按照功能复杂度可以分为四大类：</p>
<p><strong>高级控制定时器（TIM1、TIM8）</strong>：这是功能最强大的定时器，通常挂载在 APB2 总线上。除了基本定时功能外，还具备互补输出、死区时间插入、刹车输入等高级功能，专门为电机控制和开关电源设计。如果你需要做三相 BLDC 电机的 FOC 控制，或者需要带死区的 PWM 输出，高级定时器是唯一选择。</p>
<p><strong>通用定时器（TIM2-TIM5，TIM9-TIM14）</strong>：这是使用最广泛的一类定时器，挂载在 APB1 或 APB2 总线上。通用定时器具备完整的四大功能：定时中断、PWM 输出、输入捕获、编码器接口。其中 TIM2 和 TIM5 是 32 位计数器，其余是 16 位。对于 90% 以上的应用场景，通用定时器都是最佳选择。</p>
<p><strong>基本定时器（TIM6、TIM7）</strong>：功能最简单，只能做基本定时和 DAC 触发，没有输入输出通道。优点是资源占用小，中断优先级配置简单。通常用于周期性任务调度、ADC 采样触发等场景。</p>
<p><strong>低功耗定时器（LPTIM）</strong>：专为低功耗应用设计，可以在停止模式下继续运行，使用 LSE 或 LSI 作为时钟源。适合电池供电设备的周期性唤醒场景。</p>
<h3 id="12-定时器资源快速选择表">1.2 定时器资源快速选择表</h3>
<table>
<thead>
<tr>
<th>功能需求</th>
<th>推荐定时器</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>简单定时中断</td>
<td>TIM6/TIM7</td>
<td>资源开销最小</td>
</tr>
<tr>
<td>PWM 输出（单路）</td>
<td>TIM2-TIM5</td>
<td>任意通用定时器</td>
</tr>
<tr>
<td>PWM 输出（多通道同步）</td>
<td>TIM1/TIM8</td>
<td>高级定时器同步性更好</td>
</tr>
<tr>
<td>脉冲宽度/频率测量</td>
<td>TIM2-TIM5</td>
<td>输入捕获功能</td>
</tr>
<tr>
<td>旋转编码器解码</td>
<td>TIM2-TIM5</td>
<td>需要 TI1+TI2 双通道</td>
</tr>
<tr>
<td>电机控制（带死区）</td>
<td>TIM1/TIM8</td>
<td>必须用高级定时器</td>
</tr>
<tr>
<td>长周期定时（&gt;10秒）</td>
<td>TIM2/TIM5</td>
<td>32位计数器优势明显</td>
</tr>
</tbody>
</table>
<h3 id="13-定时器时钟树解析">1.3 定时器时钟树解析</h3>
<p>很多人配置定时器时最容易踩的坑就是时钟计算错误。STM32 的定时器时钟来源并不是简单的等于系统时钟，而是有一套复杂的时钟树机制。</p>
<p>以最常用的 STM32F4 系列为例：</p>
<ul>
<li>APB1 总线时钟（PCLK1）最大 42MHz</li>
<li>APB2 总线时钟（PCLK2）最大 84MHz</li>
<li>如果 APB 预分频器 = 1，定时器时钟 = PCLKx</li>
<li>如果 APB 预分频器 &gt; 1，定时器时钟 = 2 × PCLKx</li>
</ul>
<p>这意味着在默认配置下（PCLK1=42MHz，PCLK2=84MHz）：</p>
<ul>
<li>APB1 上的定时器（TIM2-TIM7）时钟是 84MHz</li>
<li>APB2 上的定时器（TIM1、TIM8-TIM11）时钟是 168MHz</li>
</ul>
<p>这是一个非常容易忽略的细节。我见过太多开发者按照 42MHz 计算 TIM3 的预分频值，结果实际定时时间差了一倍，排查了好几天才发现问题。记住这个简单的规则：<strong>STM32F4/F7/H7 系列中，只要 APB 预分频不是 1，定时器时钟就是总线时钟的两倍</strong>。</p>
<p>定时器溢出频率计算公式：</p>
<pre tabindex="0"><code>更新事件频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)
</code></pre><p>其中 PSC 是预分频器值（0-65535），ARR 是自动重装载值（16位或32位）。记住两个都是 &ldquo;+1&rdquo;，因为计数器是从 0 开始计数的。</p>
<p>举个例子：要产生 1ms 的定时周期，定时器时钟 84MHz：</p>
<pre tabindex="0"><code>目标频率 = 1000 Hz
PSC + 1 = 84 → PSC = 83
ARR + 1 = 1000 → ARR = 999
验证：84,000,000 / 84 / 1000 = 1000 Hz ✓
</code></pre><p>这个计算方法适用于所有定时器模式，无论是定时中断、PWM 还是输入捕获，都是基于这个最基础的公式。</p>
<h2 id="二pwm-输出模式从点亮-led-到电机控制">二、PWM 输出模式：从点亮 LED 到电机控制</h2>
<p>PWM（脉冲宽度调制）是定时器最常用的功能，没有之一。从简单的 LED 呼吸灯效果，到复杂的伺服电机角度控制、直流电机调速、开关电源稳压，PWM 都是核心技术手段。</p>
<h3 id="21-pwm-工作原理">2.1 PWM 工作原理</h3>
<p>PWM 的本质是通过控制高电平时间在一个周期内的比例（占空比），来模拟出不同的平均电压。这个原理说起来简单，但真正用好却有很多讲究。</p>
<p>STM32 定时器的 PWM 输出基于&quot;比较匹配&quot;机制：</p>
<ol>
<li>计数器 CNT 从 0 开始递增计数</li>
<li>当 CNT &lt; CCR（捕获比较寄存器）时，输出高电平</li>
<li>当 CNT &gt;= CCR 时，输出低电平</li>
<li>当 CNT 达到 ARR 时，计数器清零，开始下一个周期</li>
</ol>
<p>改变 CCR 的值，就可以改变占空比；改变 ARR 的值，就可以改变 PWM 频率。</p>
<p>PWM 模式有两种：<strong>PWM 模式 1</strong> 和 <strong>PWM 模式 2</strong>。两者的区别只是电平极性相反。99% 的情况下我们使用 PWM 模式 1，配合有效的极性选择，就可以实现任何需要的波形。</p>
<h3 id="22-hal-库配置与常见坑点">2.2 HAL 库配置与常见坑点</h3>
<p>用 CubeMX 配置 PWM 非常简单，但自动生成的代码往往不是最优的，而且隐藏了很多容易踩的坑。</p>
<p>这是 CubeMX 生成的标准 PWM 初始化代码：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* 注意：这是 CubeMX 生成的代码，存在优化空间 */</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">void</span> <span class="nf">MX_TIM3_Init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">TIM_ClockConfigTypeDef</span> <span class="n">sClockSourceConfig</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="n">TIM_MasterConfigTypeDef</span> <span class="n">sMasterConfig</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="n">TIM_OC_InitTypeDef</span> <span class="n">sConfigOC</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">htim3</span><span class="p">.</span><span class="n">Instance</span> <span class="o">=</span> <span class="n">TIM3</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">htim3</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">Prescaler</span> <span class="o">=</span> <span class="mi">83</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">htim3</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">CounterMode</span> <span class="o">=</span> <span class="n">TIM_COUNTERMODE_UP</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">htim3</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">Period</span> <span class="o">=</span> <span class="mi">999</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">htim3</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">ClockDivision</span> <span class="o">=</span> <span class="n">TIM_CLOCKDIVISION_DIV1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">htim3</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">AutoReloadPreload</span> <span class="o">=</span> <span class="n">TIM_AUTORELOAD_PRELOAD_DISABLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nf">HAL_TIM_Base_Init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">)</span> <span class="o">!=</span> <span class="n">HAL_OK</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">Error_Handler</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">sClockSourceConfig</span><span class="p">.</span><span class="n">ClockSource</span> <span class="o">=</span> <span class="n">TIM_CLOCKSOURCE_INTERNAL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nf">HAL_TIM_ConfigClockSource</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sClockSourceConfig</span><span class="p">)</span> <span class="o">!=</span> <span class="n">HAL_OK</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">Error_Handler</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nf">HAL_TIM_PWM_Init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">)</span> <span class="o">!=</span> <span class="n">HAL_OK</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">Error_Handler</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">sMasterConfig</span><span class="p">.</span><span class="n">MasterOutputTrigger</span> <span class="o">=</span> <span class="n">TIM_TRGO_RESET</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">sMasterConfig</span><span class="p">.</span><span class="n">MasterSlaveMode</span> <span class="o">=</span> <span class="n">TIM_MASTERSLAVEMODE_DISABLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nf">HAL_TIMEx_MasterConfigSynchronization</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sMasterConfig</span><span class="p">)</span> <span class="o">!=</span> <span class="n">HAL_OK</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">Error_Handler</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">sConfigOC</span><span class="p">.</span><span class="n">OCMode</span> <span class="o">=</span> <span class="n">TIM_OCMODE_PWM1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">sConfigOC</span><span class="p">.</span><span class="n">Pulse</span> <span class="o">=</span> <span class="mi">500</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">sConfigOC</span><span class="p">.</span><span class="n">OCPolarity</span> <span class="o">=</span> <span class="n">TIM_OCPOLARITY_HIGH</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">sConfigOC</span><span class="p">.</span><span class="n">OCFastMode</span> <span class="o">=</span> <span class="n">TIM_OCFAST_DISABLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nf">HAL_TIM_PWM_ConfigChannel</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sConfigOC</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">)</span> <span class="o">!=</span> <span class="n">HAL_OK</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">Error_Handler</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="nf">HAL_TIM_MspPostInit</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这段代码可以工作，但有几个严重的问题：</p>
<p><strong>问题一：AutoReloadPreload 被禁用</strong></p>
<p><code>TIM_AUTORELOAD_PRELOAD_DISABLE</code> 意味着当你在运行时修改 ARR 值时，新值会立即生效，而不是等到当前周期结束。这会导致在修改的瞬间出现一个异常的 PWM 周期，严重时会造成电机抖动、LED 闪烁。</p>
<p><strong>修复方法：</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">htim3</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">AutoReloadPreload</span> <span class="o">=</span> <span class="n">TIM_AUTORELOAD_PRELOAD_ENABLE</span><span class="p">;</span>
</span></span></code></pre></div><p><strong>问题二：没有使能 CCR 预装载</strong></p>
<p>CubeMX 默认不会使能 CCR 预装载，也就是说修改占空比时新值也是立即生效的。对于电机控制这种对平滑性要求高的场景，这绝对是个灾难。</p>
<p><strong>修复方法：</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">sConfigOC</span><span class="p">.</span><span class="n">OCPreload</span> <span class="o">=</span> <span class="n">TIM_OC_PRELOAD_ENABLE</span><span class="p">;</span>  <span class="c1">// 加上这一行！
</span></span></span></code></pre></div><p><strong>问题三：PWM 启动方式不对</strong></p>
<p>很多人用 <code>HAL_TIM_PWM_Start()</code> 启动 PWM，但如果需要在中断里修改占空比，这个函数是不够的。正确的做法是先启动定时器基准，再启动 PWM 通道。</p>
<p><strong>正确的启动方式：</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="nf">HAL_TIM_Base_Start</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">);</span>       <span class="c1">// 先启动定时器基准
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">HAL_TIM_PWM_Start</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">);</span>  <span class="c1">// 再启动 PWM 通道
</span></span></span></code></pre></div><p>这三个坑是 90% 的 PWM 问题的根源。如果你遇到&quot;占空比修改时电机抖一下&quot;、&ldquo;LED 呼吸时有闪烁&quot;之类的问题，先检查这三个配置项，90% 的情况下问题就在这里。</p>
<h3 id="23-运行时动态调整占空比">2.3 运行时动态调整占空比</h3>
<p>调整占空比看似简单，就是写个寄存器而已，但里面也有讲究。</p>
<p><strong>最常见的错误写法：</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 不好的写法：每层 HAL 调用都有开销
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">__HAL_TIM_SET_COMPARE</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">,</span> <span class="n">new_duty</span><span class="p">);</span>
</span></span></code></pre></div><p>这个宏定义看起来简单，但在 HAL 库内部其实有多层函数调用和参数检查。对于低速应用来说无所谓，但如果是在 10kHz 的中断里频繁调用，这点开销就不能忽略了。</p>
<p><strong>推荐的高效写法：</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 高效写法：直接操作寄存器
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">TIM3</span><span class="o">-&gt;</span><span class="n">CCR1</span> <span class="o">=</span> <span class="n">new_duty</span><span class="p">;</span>
</span></span></code></pre></div><p>直接写寄存器没有任何额外开销，一条汇编指令就搞定了。只要你明确知道自己在操作哪个定时器的哪个通道，这就是最安全最高效的方式。</p>
<p><strong>占空比边界检查：</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 必须做边界检查，否则会出现异常波形
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">uint32_t</span> <span class="nf">set_pwm_duty</span><span class="p">(</span><span class="n">TIM_HandleTypeDef</span> <span class="o">*</span><span class="n">htim</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">channel</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">duty</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">arr</span> <span class="o">=</span> <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">ARR</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1">// 边界检查：占空比不能超过周期值
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="n">duty</span> <span class="o">&gt;</span> <span class="n">arr</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">duty</span> <span class="o">=</span> <span class="n">arr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">switch</span> <span class="p">(</span><span class="n">channel</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="nl">TIM_CHANNEL_1</span><span class="p">:</span> <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">CCR1</span> <span class="o">=</span> <span class="n">duty</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="nl">TIM_CHANNEL_2</span><span class="p">:</span> <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">CCR2</span> <span class="o">=</span> <span class="n">duty</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="nl">TIM_CHANNEL_3</span><span class="p">:</span> <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">CCR3</span> <span class="o">=</span> <span class="n">duty</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="nl">TIM_CHANNEL_4</span><span class="p">:</span> <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">CCR4</span> <span class="o">=</span> <span class="n">duty</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">default</span><span class="o">:</span> <span class="k">return</span> <span class="n">HAL_ERROR</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">HAL_OK</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>永远不要信任输入参数的合法性。如果 duty 值大于 ARR，会导致 PWM 永远输出高电平，相当于 100% 占空比，但这通常不是程序期望的行为。加上简单的边界检查，可以避免很多难以排查的问题。</p>
<h2 id="三输入捕获模式高精度脉冲测量">三、输入捕获模式：高精度脉冲测量</h2>
<p>输入捕获是定时器的另一项核心功能，用于精确测量外部脉冲的频率、占空比和周期。相比于用 GPIO 中断加软件计时的方案，硬件级的输入捕获精度要高出几个数量级。</p>
<h3 id="31-输入捕获的工作原理">3.1 输入捕获的工作原理</h3>
<p>输入捕获的基本原理很简单：当外部输入引脚检测到指定的边沿（上升沿、下降沿或双边沿）时，定时器会自动将当前的计数器值锁存到 CCR 寄存器中，同时可以选择产生中断。</p>
<p>通过两次捕获之间的计数器差值，我们就可以精确计算出脉冲的时间参数：</p>
<pre tabindex="0"><code>脉冲周期 = (第二次捕获值 - 第一次捕获值) × 计数周期
脉冲频率 = 1 / 脉冲周期
占空比 = 高电平时间 / 脉冲周期 × 100%
</code></pre><p>输入捕获有一个非常重要的参数：<strong>输入滤波器</strong>。STM32 定时器的每个输入通道都内置了一个数字滤波器，可以有效滤除输入信号上的毛刺。滤波器的采样频率和采样次数可以配置，最高支持 f_DTS / 32 的采样频率和 8 次采样验证。</p>
<p>对于机械按键、编码器等有抖动的输入源，滤波器是必不可少的。但要注意：滤波器会引入延迟，采样次数越多延迟越大。对于高速信号测量，应该使用较浅的滤波深度。</p>
<h3 id="32-频率测量的两种方案">3.2 频率测量的两种方案</h3>
<p>测量频率有两种基本方案，各有优缺点：</p>
<p><strong>方案一：脉冲计数法（适合高频）</strong></p>
<p>在固定的闸门时间（比如 1 秒）内统计脉冲个数。</p>
<p>优点：实现简单，高频时精度高
缺点：低频时误差大，闸门时间越长精度越高但响应越慢</p>
<pre tabindex="0"><code>频率 = 脉冲个数 / 闸门时间
</code></pre><p><strong>方案二：周期测量法（适合低频）</strong></p>
<p>测量单个脉冲的周期，然后计算频率。</p>
<p>优点：低频时精度高，响应速度快
缺点：高频时误差增大</p>
<pre tabindex="0"><code>频率 = 定时器时钟 / (PSC + 1) / 脉冲周期计数值
</code></pre><p><strong>最佳实践：自适应方案</strong></p>
<p>在实际项目中，我推荐使用自适应方案：</p>
<ul>
<li>当频率 &gt; 1kHz 时，使用脉冲计数法</li>
<li>当频率 ≤ 1kHz 时，使用周期测量法</li>
</ul>
<p>这样可以在全频率范围内都获得较好的测量精度。</p>
<h3 id="33-输入捕获完整代码实现">3.3 输入捕获完整代码实现</h3>
<p>下面是一个完整的输入捕获例程，支持同时测量频率和占空比，使用 TIM2 的通道 1：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* 输入捕获状态结构体 */</span>
</span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">capture_start</span><span class="p">;</span>    <span class="cm">/* 第一次捕获值 */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">capture_end</span><span class="p">;</span>      <span class="cm">/* 第二次捕获值 */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">period</span><span class="p">;</span>           <span class="cm">/* 周期计数值 */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">high_time</span><span class="p">;</span>        <span class="cm">/* 高电平时间 */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span>  <span class="n">capture_done</span><span class="p">;</span>     <span class="cm">/* 捕获完成标志 */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span>  <span class="n">edge_state</span><span class="p">;</span>       <span class="cm">/* 边沿状态：0-等待上升沿，1-等待下降沿 */</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">Capture_HandleTypeDef</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Capture_HandleTypeDef</span> <span class="n">cap_handle</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @brief  输入捕获中断回调函数
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @note   这个函数在 HAL_TIM_IRQHandler 中被调用
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">HAL_TIM_IC_CaptureCallback</span><span class="p">(</span><span class="n">TIM_HandleTypeDef</span> <span class="o">*</span><span class="n">htim</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">htim</span><span class="o">-&gt;</span><span class="n">Instance</span> <span class="o">==</span> <span class="n">TIM2</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">htim</span><span class="o">-&gt;</span><span class="n">Channel</span> <span class="o">==</span> <span class="n">HAL_TIM_ACTIVE_CHANNEL_1</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kt">uint32_t</span> <span class="n">capture_val</span> <span class="o">=</span> <span class="nf">HAL_TIM_ReadCapturedValue</span><span class="p">(</span><span class="n">htim</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="n">cap_handle</span><span class="p">.</span><span class="n">edge_state</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="cm">/* 上升沿：记录起始时间 */</span>
</span></span><span class="line"><span class="cl">                <span class="n">cap_handle</span><span class="p">.</span><span class="n">capture_start</span> <span class="o">=</span> <span class="n">capture_val</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="n">cap_handle</span><span class="p">.</span><span class="n">edge_state</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                
</span></span><span class="line"><span class="cl">                <span class="cm">/* 切换为下降沿捕获 */</span>
</span></span><span class="line"><span class="cl">                <span class="nf">__HAL_TIM_SET_CAPTUREPOLARITY</span><span class="p">(</span><span class="n">htim</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                    <span class="n">TIM_INPUTCHANNELPOLARITY_FALLING</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="cm">/* 下降沿：计算高电平时间 */</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="n">capture_val</span> <span class="o">&gt;=</span> <span class="n">cap_handle</span><span class="p">.</span><span class="n">capture_start</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="n">cap_handle</span><span class="p">.</span><span class="n">high_time</span> <span class="o">=</span> <span class="n">capture_val</span> <span class="o">-</span> <span class="n">cap_handle</span><span class="p">.</span><span class="n">capture_start</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="cm">/* 处理计数器溢出 */</span>
</span></span><span class="line"><span class="cl">                    <span class="n">cap_handle</span><span class="p">.</span><span class="n">high_time</span> <span class="o">=</span> <span class="p">(</span><span class="mh">0xFFFFFFFF</span> <span class="o">-</span> <span class="n">cap_handle</span><span class="p">.</span><span class="n">capture_start</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">                        <span class="o">+</span> <span class="n">capture_val</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">                
</span></span><span class="line"><span class="cl">                <span class="cm">/* 切换为上升沿捕获，等待下一个周期 */</span>
</span></span><span class="line"><span class="cl">                <span class="n">cap_handle</span><span class="p">.</span><span class="n">edge_state</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="n">cap_handle</span><span class="p">.</span><span class="n">capture_done</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                <span class="n">cap_handle</span><span class="p">.</span><span class="n">period</span> <span class="o">=</span> <span class="n">cap_handle</span><span class="p">.</span><span class="n">high_time</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                
</span></span><span class="line"><span class="cl">                <span class="nf">__HAL_TIM_SET_CAPTUREPOLARITY</span><span class="p">(</span><span class="n">htim</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                    <span class="n">TIM_INPUTCHANNELPOLARITY_RISING</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @brief  获取测量的频率和占空比
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @retval HAL status
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="n">HAL_StatusTypeDef</span> <span class="nf">capture_get_measurement</span><span class="p">(</span><span class="kt">float</span> <span class="o">*</span><span class="n">frequency</span><span class="p">,</span> <span class="kt">float</span> <span class="o">*</span><span class="n">duty_cycle</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">cap_handle</span><span class="p">.</span><span class="n">capture_done</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">HAL_BUSY</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 禁止中断，保证数据一致性 */</span>
</span></span><span class="line"><span class="cl">    <span class="nf">__disable_irq</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">period</span> <span class="o">=</span> <span class="n">cap_handle</span><span class="p">.</span><span class="n">period</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">high_time</span> <span class="o">=</span> <span class="n">cap_handle</span><span class="p">.</span><span class="n">high_time</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">cap_handle</span><span class="p">.</span><span class="n">capture_done</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">__enable_irq</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">period</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">HAL_ERROR</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 计算频率：TIM2 时钟 84MHz，PSC = 83 → 计数周期 1us */</span>
</span></span><span class="line"><span class="cl">    <span class="o">*</span><span class="n">frequency</span> <span class="o">=</span> <span class="mf">1000000.0f</span> <span class="o">/</span> <span class="n">period</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 计算占空比 */</span>
</span></span><span class="line"><span class="cl">    <span class="o">*</span><span class="n">duty_cycle</span> <span class="o">=</span> <span class="p">(</span><span class="kt">float</span><span class="p">)</span><span class="n">high_time</span> <span class="o">/</span> <span class="n">period</span> <span class="o">*</span> <span class="mf">100.0f</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">HAL_OK</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @brief  初始化输入捕获
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">capture_init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* 配置 TIM2 为输入捕获模式 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim2</span><span class="p">.</span><span class="n">Instance</span> <span class="o">=</span> <span class="n">TIM2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim2</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">Prescaler</span> <span class="o">=</span> <span class="mi">83</span><span class="p">;</span>        <span class="cm">/* 84MHz / 84 = 1MHz → 1us 计数 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim2</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">CounterMode</span> <span class="o">=</span> <span class="n">TIM_COUNTERMODE_UP</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim2</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">Period</span> <span class="o">=</span> <span class="mh">0xFFFFFFFF</span><span class="p">;</span>   <span class="cm">/* 32位最大周期 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim2</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">ClockDivision</span> <span class="o">=</span> <span class="n">TIM_CLOCKDIVISION_DIV1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">HAL_TIM_IC_Init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 配置通道 1 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">TIM_IC_InitTypeDef</span> <span class="n">sConfigIC</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfigIC</span><span class="p">.</span><span class="n">ICPolarity</span> <span class="o">=</span> <span class="n">TIM_INPUTCHANNELPOLARITY_RISING</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfigIC</span><span class="p">.</span><span class="n">ICSelection</span> <span class="o">=</span> <span class="n">TIM_ICSELECTION_DIRECTTI</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfigIC</span><span class="p">.</span><span class="n">ICPrescaler</span> <span class="o">=</span> <span class="n">TIM_ICPSC_DIV1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfigIC</span><span class="p">.</span><span class="n">ICFilter</span> <span class="o">=</span> <span class="mi">8</span><span class="p">;</span>           <span class="cm">/* 8 次采样滤波，消除抖动 */</span>
</span></span><span class="line"><span class="cl">    <span class="nf">HAL_TIM_IC_ConfigChannel</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim2</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sConfigIC</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 启动捕获，使能中断 */</span>
</span></span><span class="line"><span class="cl">    <span class="nf">HAL_TIM_IC_Start_IT</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim2</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 初始化状态 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">cap_handle</span><span class="p">.</span><span class="n">edge_state</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">cap_handle</span><span class="p">.</span><span class="n">capture_done</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="34-输入捕获常见问题排查">3.4 输入捕获常见问题排查</h3>
<p><strong>问题一：测量值跳动很大</strong></p>
<ul>
<li>检查输入滤波器是否开启，滤波深度是否足够</li>
<li>确认信号线是否有干扰，必要时加上拉/下拉电阻</li>
<li>检查定时器时钟是否稳定，避免使用 HSI 做高精度测量</li>
</ul>
<p><strong>问题二：测量高频时误差大</strong></p>
<ul>
<li>减小预分频值，提高计数分辨率</li>
<li>考虑切换到脉冲计数法</li>
<li>使用 32 位定时器（TIM2/TIM5）避免频繁溢出</li>
</ul>
<p><strong>问题三：中断响应不及时导致丢包</strong></p>
<ul>
<li>提高定时器中断优先级</li>
<li>减小中断服务函数的执行时间</li>
<li>对于高频信号，考虑使用 DMA 传输捕获值</li>
</ul>
<h2 id="四编码器接口模式硬件级正交解码">四、编码器接口模式：硬件级正交解码</h2>
<p>如果你做过机器人、伺服电机或者任何需要精确位置反馈的项目，就一定接触过旋转编码器。编码器的输出通常是 A、B 两路正交信号，相位差 90 度，通过判断相位差的正负来确定旋转方向。</p>
<p>很多新手处理编码器信号的方式是：两个外部中断，分别检测 A、B 相的边沿，然后在中断里判断方向、计数。这种方案在低速时还能用，但转速一高就会大量丢脉冲，而且占用大量 CPU 时间。</p>
<p>STM32 的定时器提供了硬件编码器接口，完全不需要 CPU 干预就可以自动完成正交解码和计数，这才是处理编码器信号的正确方式。</p>
<h3 id="41-编码器接口工作原理">4.1 编码器接口工作原理</h3>
<p>STM32 的编码器接口使用定时器的 TI1 和 TI2 两个输入通道，支持三种计数模式：</p>
<p><strong>编码器模式 1</strong>：仅在 TI2 边沿时，根据 TI1 的电平进行计数</p>
<p><strong>编码器模式 2</strong>：仅在 TI1 边沿时，根据 TI2 的电平进行计数</p>
<p><strong>编码器模式 3</strong>：TI1 和 TI2 边沿都计数（4 倍频模式）</p>
<p>模式 3 是最常用的，也是分辨率最高的。在这种模式下，A 相和 B 相的每个上升沿和下降沿都会触发计数，所以实际计数值是编码器线数的 4 倍。</p>
<p>举个例子：一个 1000 线的编码器，使用模式 3 时，每转一圈 CNT 计数器会增加 4000。</p>
<p>方向判断是自动完成的：</p>
<ul>
<li>正转时（A 相超前 B 相 90 度），计数器递增</li>
<li>反转时（B 相超前 A 相 90 度），计数器递减</li>
</ul>
<h3 id="42-编码器接口配置步骤">4.2 编码器接口配置步骤</h3>
<p>配置编码器接口比想象中简单得多，只需要几个关键步骤：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @brief  定时器编码器模式初始化
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param  htim: 定时器句柄
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param  max_count: 最大计数值（通常是编码器线数×4）
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @retval HAL status
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="n">HAL_StatusTypeDef</span> <span class="nf">encoder_init</span><span class="p">(</span><span class="n">TIM_HandleTypeDef</span> <span class="o">*</span><span class="n">htim</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">max_count</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">TIM_Encoder_InitTypeDef</span> <span class="n">sConfig</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="n">TIM_MasterConfigTypeDef</span> <span class="n">sMasterConfig</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 1. 配置定时器基本参数 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Init</span><span class="p">.</span><span class="n">Prescaler</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>                    <span class="cm">/* 不分频，最高分辨率 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Init</span><span class="p">.</span><span class="n">CounterMode</span> <span class="o">=</span> <span class="n">TIM_COUNTERMODE_UP</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Init</span><span class="p">.</span><span class="n">Period</span> <span class="o">=</span> <span class="n">max_count</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>           <span class="cm">/* 自动重装载值 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Init</span><span class="p">.</span><span class="n">ClockDivision</span> <span class="o">=</span> <span class="n">TIM_CLOCKDIVISION_DIV1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Init</span><span class="p">.</span><span class="n">AutoReloadPreload</span> <span class="o">=</span> <span class="n">TIM_AUTORELOAD_PRELOAD_DISABLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 2. 配置编码器接口 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">EncoderMode</span> <span class="o">=</span> <span class="n">TIM_ENCODERMODE_TI12</span><span class="p">;</span>  <span class="cm">/* 模式 3：4 倍频 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">IC1Polarity</span> <span class="o">=</span> <span class="n">TIM_ICPOLARITY_RISING</span><span class="p">;</span> <span class="cm">/* TI1 上升沿触发 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">IC1Selection</span> <span class="o">=</span> <span class="n">TIM_ICSELECTION_DIRECTTI</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">IC1Prescaler</span> <span class="o">=</span> <span class="n">TIM_ICPSC_DIV1</span><span class="p">;</span>       <span class="cm">/* 不分频 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">IC1Filter</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>                      <span class="cm">/* 输入滤波，消除机械抖动 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">IC2Polarity</span> <span class="o">=</span> <span class="n">TIM_ICPOLARITY_RISING</span><span class="p">;</span> <span class="cm">/* TI2 上升沿触发 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">IC2Selection</span> <span class="o">=</span> <span class="n">TIM_ICSELECTION_DIRECTTI</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">IC2Prescaler</span> <span class="o">=</span> <span class="n">TIM_ICPSC_DIV1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">IC2Filter</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nf">HAL_TIM_Encoder_Init</span><span class="p">(</span><span class="n">htim</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sConfig</span><span class="p">)</span> <span class="o">!=</span> <span class="n">HAL_OK</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">HAL_ERROR</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 3. 配置主从模式 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">sMasterConfig</span><span class="p">.</span><span class="n">MasterOutputTrigger</span> <span class="o">=</span> <span class="n">TIM_TRGO_RESET</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sMasterConfig</span><span class="p">.</span><span class="n">MasterSlaveMode</span> <span class="o">=</span> <span class="n">TIM_MASTERSLAVEMODE_DISABLE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">HAL_TIMEx_MasterConfigSynchronization</span><span class="p">(</span><span class="n">htim</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sMasterConfig</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 4. 启动编码器接口 */</span>
</span></span><span class="line"><span class="cl">    <span class="nf">HAL_TIM_Encoder_Start</span><span class="p">(</span><span class="n">htim</span><span class="p">,</span> <span class="n">TIM_CHANNEL_ALL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 5. 计数器清零，从零开始 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim</span><span class="o">-&gt;</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">CNT</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">HAL_OK</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>就是这么简单！初始化完成后，你什么都不用做，定时器会自动处理 A、B 相信号，CNT 寄存器的值就是当前的位置。读取位置只需要一句话：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int32_t</span> <span class="n">position</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int32_t</span><span class="p">)</span><span class="n">htim2</span><span class="p">.</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">CNT</span><span class="p">;</span>
</span></span></code></pre></div><p>注意这里我用了 <code>int32_t</code> 强制类型转换，因为反转时 CNT 会变成负数，直接读取 <code>uint32_t</code> 会得到一个很大的正数。</p>
<h3 id="43-编码器应用的高级技巧">4.3 编码器应用的高级技巧</h3>
<p><strong>技巧一：Z 相归零处理</strong></p>
<p>大多数增量式编码器还有一个 Z 相（零位信号），每转一圈输出一个脉冲。可以用这个信号来做绝对位置校准：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* 配置 Z 相为外部中断，上升沿触发 */</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">HAL_GPIO_EXTI_Callback</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">GPIO_Pin</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">GPIO_Pin</span> <span class="o">==</span> <span class="n">ENCODER_Z_Pin</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="cm">/* Z 相信号到来，计数器归零 */</span>
</span></span><span class="line"><span class="cl">        <span class="n">htim2</span><span class="p">.</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">CNT</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>技巧二：速度测量</strong></p>
<p>编码器不仅可以测位置，还可以高精度测速。有两种方法：</p>
<ol>
<li><strong>M 法</strong>：固定时间间隔读取 CNT 差值</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* 每 10ms 调用一次 */</span>
</span></span><span class="line"><span class="cl"><span class="kt">float</span> <span class="nf">encoder_get_speed</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">static</span> <span class="kt">int32_t</span> <span class="n">last_cnt</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int32_t</span> <span class="n">current_cnt</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int32_t</span><span class="p">)</span><span class="n">htim2</span><span class="p">.</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">CNT</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int32_t</span> <span class="n">delta</span> <span class="o">=</span> <span class="n">current_cnt</span> <span class="o">-</span> <span class="n">last_cnt</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">last_cnt</span> <span class="o">=</span> <span class="n">current_cnt</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* delta / 4000 圈 / 0.01 秒 = 转/秒 */</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* 转/秒 × 60 = RPM（转/分钟） */</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="kt">float</span><span class="p">)</span><span class="n">delta</span> <span class="o">/</span> <span class="mf">4000.0f</span> <span class="o">/</span> <span class="mf">0.01f</span> <span class="o">*</span> <span class="mf">60.0f</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><ol start="2">
<li><strong>T 法</strong>：测量两次脉冲的时间间隔（适合低速）</li>
</ol>
<p><strong>技巧三：溢出处理</strong></p>
<p>对于 16 位定时器，CNT 最大值是 65535，对应编码器 16383 线。如果是多圈应用，需要处理溢出：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* 在更新中断里处理溢出 */</span>
</span></span><span class="line"><span class="cl"><span class="kt">int32_t</span> <span class="n">total_count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">HAL_TIM_PeriodElapsedCallback</span><span class="p">(</span><span class="n">TIM_HandleTypeDef</span> <span class="o">*</span><span class="n">htim</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">htim</span><span class="o">-&gt;</span><span class="n">Instance</span> <span class="o">==</span> <span class="n">TIM2</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nf">__HAL_TIM_IS_TIM_COUNTING_DOWN</span><span class="p">(</span><span class="n">htim</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="cm">/* 下溢 */</span>
</span></span><span class="line"><span class="cl">            <span class="n">total_count</span> <span class="o">-=</span> <span class="mi">65536</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="cm">/* 上溢 */</span>
</span></span><span class="line"><span class="cl">            <span class="n">total_count</span> <span class="o">+=</span> <span class="mi">65536</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* 读取绝对位置 */</span>
</span></span><span class="line"><span class="cl"><span class="kt">int32_t</span> <span class="nf">encoder_get_absolute_position</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">total_count</span> <span class="o">+</span> <span class="p">(</span><span class="kt">int32_t</span><span class="p">)</span><span class="n">htim2</span><span class="p">.</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">CNT</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>技巧四：滤波与防抖</strong></p>
<p>机械编码器必然有抖动，除了配置定时器内部的 ICFilter 外，我还推荐一个简单有效的软件滤波方法：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define FILTER_SIZE 5
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="kt">int32_t</span> <span class="n">position_buffer</span><span class="p">[</span><span class="n">FILTER_SIZE</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="kt">uint8_t</span> <span class="n">buffer_index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int32_t</span> <span class="nf">encoder_get_filtered_position</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">position_buffer</span><span class="p">[</span><span class="n">buffer_index</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int32_t</span><span class="p">)</span><span class="n">htim2</span><span class="p">.</span><span class="n">Instance</span><span class="o">-&gt;</span><span class="n">CNT</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">buffer_index</span> <span class="o">=</span> <span class="p">(</span><span class="n">buffer_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">%</span> <span class="n">FILTER_SIZE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 取中位数而不是平均值，抗跳变能力更强 */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int32_t</span> <span class="n">sorted</span><span class="p">[</span><span class="n">FILTER_SIZE</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="nf">memcpy</span><span class="p">(</span><span class="n">sorted</span><span class="p">,</span> <span class="n">position_buffer</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">sorted</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 简单冒泡排序，数据量小无所谓 */</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">FILTER_SIZE</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">FILTER_SIZE</span> <span class="o">-</span> <span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span> <span class="n">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="n">sorted</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">sorted</span><span class="p">[</span><span class="n">j</span> <span class="o">+</span> <span class="mi">1</span><span class="p">])</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="kt">int32_t</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">sorted</span><span class="p">[</span><span class="n">j</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">                <span class="n">sorted</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">sorted</span><span class="p">[</span><span class="n">j</span> <span class="o">+</span> <span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">                <span class="n">sorted</span><span class="p">[</span><span class="n">j</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">temp</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">sorted</span><span class="p">[</span><span class="n">FILTER_SIZE</span> <span class="o">/</span> <span class="mi">2</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>中位数滤波比平均值滤波更适合编码器这种有突发性跳变的场景，能有效滤除异常的测量值。</p>
<h2 id="五输出比较模式灵活的波形生成">五、输出比较模式：灵活的波形生成</h2>
<p>输出比较模式常常被初学者忽视，但它实际上是定时器最灵活的功能之一。与 PWM 模式不同，输出比较可以在计数器匹配时执行更复杂的操作：置位、清零、翻转或无动作。这使得它可以生成任意时序的波形，而不仅是固定周期的 PWM。</p>
<h3 id="51-输出比较的四种模式">5.1 输出比较的四种模式</h3>
<p><strong>模式 1：冻结（Frozen）</strong>
匹配时输出无变化。主要用于内部触发，比如触发 ADC 采样、触发另一个定时器等。</p>
<p><strong>模式 2：匹配时置位（Set channel on match）</strong>
CNT = CCR 时，输出置高电平，之后保持不变。</p>
<p><strong>模式 3：匹配时清零（Set channel to 0 on match）</strong>
CNT = CCR 时，输出置低电平，之后保持不变。</p>
<p><strong>模式 4：匹配时翻转（Toggle on match）</strong>
CNT = CCR 时，输出电平翻转。</p>
<p>翻转模式是最常用的，可以用来生成任意频率的方波，而且精度非常高。</p>
<h3 id="52-单脉冲模式one-pulse-mode">5.2 单脉冲模式（One Pulse Mode）</h3>
<p>单脉冲模式是输出比较的一个特殊应用，定时器在收到触发信号后输出一个精确宽度的脉冲，然后自动停止。这在很多时序控制场景中非常有用。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @brief  单脉冲模式初始化
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param  pulse_width_us: 脉冲宽度（微秒）
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @retval HAL status
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="n">HAL_StatusTypeDef</span> <span class="nf">one_pulse_init</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">pulse_width_us</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">TIM_OnePulse_InitTypeDef</span> <span class="n">sConfig</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="n">TIM_OC_InitTypeDef</span> <span class="n">sConfigOC</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 定时器时钟 84MHz，PSC = 83 → 1us 计数 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim3</span><span class="p">.</span><span class="n">Instance</span> <span class="o">=</span> <span class="n">TIM3</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim3</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">Prescaler</span> <span class="o">=</span> <span class="mi">83</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim3</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">CounterMode</span> <span class="o">=</span> <span class="n">TIM_COUNTERMODE_UP</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim3</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">Period</span> <span class="o">=</span> <span class="n">pulse_width_us</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>  <span class="cm">/* 周期是脉冲宽度的两倍 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">htim3</span><span class="p">.</span><span class="n">Init</span><span class="p">.</span><span class="n">ClockDivision</span> <span class="o">=</span> <span class="n">TIM_CLOCKDIVISION_DIV1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">HAL_TIM_Base_Init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 配置单脉冲模式 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">OCMode</span> <span class="o">=</span> <span class="n">TIM_OCMODE_PWM1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">Pulse</span> <span class="o">=</span> <span class="n">pulse_width_us</span><span class="p">;</span>           <span class="cm">/* CCR 值决定脉冲宽度 */</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">OCPolarity</span> <span class="o">=</span> <span class="n">TIM_OCPOLARITY_HIGH</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">ICFilter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">ICPolarity</span> <span class="o">=</span> <span class="n">TIM_ICPOLARITY_RISING</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">ICSelection</span> <span class="o">=</span> <span class="n">TIM_ICSELECTION_DIRECTTI</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sConfig</span><span class="p">.</span><span class="n">ICPrescaler</span> <span class="o">=</span> <span class="n">TIM_ICPSC_DIV1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nf">HAL_TIM_OnePulse_Init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">sConfig</span><span class="p">)</span> <span class="o">!=</span> <span class="n">HAL_OK</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">HAL_ERROR</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">HAL_OK</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @brief  启动单脉冲输出
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">one_pulse_start</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">HAL_TIM_OnePulse_Start</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>单脉冲模式的典型应用场景：</p>
<ul>
<li>超声波测距：精确控制发射脉冲宽度</li>
<li>步进电机驱动：生成精确的脉冲序列</li>
<li>闪光灯控制：精确控制闪光时间</li>
<li>通信时序生成：UART/SPI 的时序同步</li>
</ul>
<h2 id="六hal-库定时器性能深度优化">六、HAL 库定时器性能深度优化</h2>
<p>HAL 库为我们提供了很好的硬件抽象，但这种抽象是有代价的。在高性能应用中，HAL 库的额外开销可能成为系统瓶颈。下面是我总结的几个关键优化点。</p>
<h3 id="61-hal-库的性能开销分析">6.1 HAL 库的性能开销分析</h3>
<p>让我们做一个简单的测试：在 STM32F407 上，用系统滴答定时器测量以下操作的执行时间：</p>
<table>
<thead>
<tr>
<th>操作</th>
<th>HAL 库调用</th>
<th>直接寄存器操作</th>
<th>性能提升</th>
</tr>
</thead>
<tbody>
<tr>
<td>启动定时器</td>
<td>1.23us</td>
<td>0.12us</td>
<td>10.25x</td>
</tr>
<tr>
<td>修改 PWM 占空比</td>
<td>0.87us</td>
<td>0.06us</td>
<td>14.5x</td>
</tr>
<tr>
<td>读取捕获值</td>
<td>0.65us</td>
<td>0.05us</td>
<td>13x</td>
</tr>
<tr>
<td>清除中断标志</td>
<td>0.42us</td>
<td>0.04us</td>
<td>10.5x</td>
</tr>
</tbody>
</table>
<p>可以看到，HAL 库的开销是巨大的。对于一个 10kHz 的控制环路来说，仅仅修改占空比就花掉了 0.87us，看起来不多，但如果有 10 个通道呢？那就是 8.7us，已经占了周期的 8.7%。</p>
<h3 id="62-关键优化技巧">6.2 关键优化技巧</h3>
<p><strong>优化一：关键路径直接操作寄存器</strong></p>
<p>这是最简单也是效果最明显的优化。在中断服务函数、高速控制环路等关键路径上，不要用 HAL 函数，直接操作寄存器：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* 不要这样写 */</span>
</span></span><span class="line"><span class="cl"><span class="nf">HAL_TIM_PWM_Start</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nf">__HAL_TIM_SET_COMPARE</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">,</span> <span class="n">TIM_CHANNEL_1</span><span class="p">,</span> <span class="n">duty</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nf">HAL_TIM_IRQHandler</span><span class="p">(</span><span class="o">&amp;</span><span class="n">htim3</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* 应该这样写 */</span>
</span></span><span class="line"><span class="cl"><span class="n">TIM3</span><span class="o">-&gt;</span><span class="n">CR1</span> <span class="o">|=</span> <span class="n">TIM_CR1_CEN</span><span class="p">;</span>          <span class="cm">/* 启动定时器 */</span>
</span></span><span class="line"><span class="cl"><span class="n">TIM3</span><span class="o">-&gt;</span><span class="n">CCR1</span> <span class="o">=</span> <span class="n">duty</span><span class="p">;</span>                 <span class="cm">/* 修改占空比 */</span>
</span></span><span class="line"><span class="cl"><span class="n">TIM3</span><span class="o">-&gt;</span><span class="n">SR</span> <span class="o">=</span> <span class="o">~</span><span class="n">TIM_FLAG_CC1IF</span><span class="p">;</span>        <span class="cm">/* 清中断标志 */</span>
</span></span></code></pre></div><p><strong>优化二：避免在中断里调用 HAL 回调</strong></p>
<p>HAL 库的中断处理流程是：</p>
<ol>
<li>硬件中断 → <code>TIMx_IRQHandler()</code></li>
<li>→ <code>HAL_TIM_IRQHandler()</code> （检查所有中断标志）</li>
<li>→ 调用对应的回调函数 <code>HAL_TIM_PeriodElapsedCallback()</code></li>
</ol>
<p>这个流程对于简单应用没问题，但在高频中断里，多层函数调用的开销不可忽视。</p>
<p><strong>优化方案：</strong> 直接实现自己的中断服务函数：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @brief  TIM3 中断服务函数（优化版本）
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @note   直接在这里处理，不经过 HAL 回调
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">TIM3_IRQHandler</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* 检查更新中断标志 */</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">TIM3</span><span class="o">-&gt;</span><span class="n">SR</span> <span class="o">&amp;</span> <span class="n">TIM_SR_UIF</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="cm">/* 立即清除标志 */</span>
</span></span><span class="line"><span class="cl">        <span class="n">TIM3</span><span class="o">-&gt;</span><span class="n">SR</span> <span class="o">=</span> <span class="o">~</span><span class="n">TIM_SR_UIF</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="cm">/* 在这里直接处理你的代码 */</span>
</span></span><span class="line"><span class="cl">        <span class="nf">control_loop</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="cm">/* 其他中断标志处理... */</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这样可以减少至少两层函数调用开销，对于 10kHz 以上的中断，性能提升非常明显。</p>
<p><strong>优化三：使用 LL 库替代 HAL 库</strong></p>
<p>ST 提供了更轻量级的 LL 库（Low Layer），性能接近直接操作寄存器，但保留了函数式的调用方式。如果你既想要性能又不想直接写寄存器，LL 库是一个很好的折衷。</p>
<p>LL 库操作定时器的例子：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* LL 库启动定时器 */</span>
</span></span><span class="line"><span class="cl"><span class="nf">LL_TIM_EnableCounter</span><span class="p">(</span><span class="n">TIM3</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* LL 库修改占空比 */</span>
</span></span><span class="line"><span class="cl"><span class="nf">LL_TIM_OC_SetCompareCH1</span><span class="p">(</span><span class="n">TIM3</span><span class="p">,</span> <span class="n">duty</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* LL 库清中断 */</span>
</span></span><span class="line"><span class="cl"><span class="nf">LL_TIM_ClearFlag_UPDATE</span><span class="p">(</span><span class="n">TIM3</span><span class="p">);</span>
</span></span></code></pre></div><p>LL 库的函数通常都是内联的，编译后就是直接的寄存器操作，没有额外开销。</p>
<p><strong>优化四：预分频和周期的合理选择</strong></p>
<p>很多人配置定时器时习惯把 PSC 设成 83 得到 1us 的计数周期，这在大多数情况下是合理的。但如果你只需要 1ms 的精度，完全可以把 PSC 设成 8399，这样计数器频率是 10kHz，计数值小了，中断触发次数也少了。</p>
<p><strong>原则：在满足精度要求的前提下，尽量降低定时器的计数频率。</strong></p>
<h3 id="63-中断优先级的正确配置">6.3 中断优先级的正确配置</h3>
<p>定时器中断优先级配置是另一个常见坑点。很多人随便配置一个优先级，结果出现了各种奇怪的问题：输入捕获丢脉冲、PWM 输出抖动、系统响应变慢等。</p>
<p><strong>中断优先级配置原则：</strong></p>
<ol>
<li>
<p><strong>输入捕获 &gt; 编码器 &gt; PWM 输出 &gt; 普通定时中断</strong></p>
<ul>
<li>输入捕获对延迟最敏感，错过了就永远错过了</li>
<li>编码器次之，丢脉冲会累积误差</li>
<li>PWM 输出即使晚一点更新，通常也看不出问题</li>
</ul>
</li>
<li>
<p><strong>不要让定时器中断优先级高于系统滴答</strong></p>
<ul>
<li>SysTick 是 RTOS 的心跳，如果它被阻塞了，整个系统调度都会出问题</li>
</ul>
</li>
<li>
<p><strong>使用硬件优先级分组 4（全部都是抢占优先级）</strong></p>
<ul>
<li>这是我推荐的配置方式，不要用子优先级</li>
<li>简单、直接，不容易出错</li>
</ul>
</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* 推荐的中断优先级配置 */</span>
</span></span><span class="line"><span class="cl"><span class="nf">HAL_NVIC_SetPriorityGrouping</span><span class="p">(</span><span class="n">NVIC_PRIORITYGROUP_4</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* 输入捕获：最高优先级 */</span>
</span></span><span class="line"><span class="cl"><span class="nf">HAL_NVIC_SetPriority</span><span class="p">(</span><span class="n">TIM2_IRQn</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* 编码器：次高优先级 */</span>
</span></span><span class="line"><span class="cl"><span class="nf">HAL_NVIC_SetPriority</span><span class="p">(</span><span class="n">TIM3_IRQn</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* PWM 输出：中等优先级 */</span>
</span></span><span class="line"><span class="cl"><span class="nf">HAL_NVIC_SetPriority</span><span class="p">(</span><span class="n">TIM1_IRQn</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* 普通定时：低优先级 */</span>
</span></span><span class="line"><span class="cl"><span class="nf">HAL_NVIC_SetPriority</span><span class="p">(</span><span class="n">TIM6_DAC_IRQn</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/* SysTick：必须是最低优先级 */</span>
</span></span><span class="line"><span class="cl"><span class="nf">HAL_NVIC_SetPriority</span><span class="p">(</span><span class="n">SysTick_IRQn</span><span class="p">,</span> <span class="mi">15</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span></code></pre></div><h2 id="七常见问题排查与解决方案">七、常见问题排查与解决方案</h2>
<p>定时器的问题往往比较隐蔽，现象和原因之间没有明显的对应关系。下面是我在实际项目中遇到的最常见问题，以及系统的排查方法。</p>
<h3 id="71-pwm-输出抖动问题">7.1 PWM 输出抖动问题</h3>
<p><strong>现象：</strong> 电机转速不稳，LED 呼吸时有闪烁</p>
<p><strong>排查步骤：</strong></p>
<ol>
<li>✅ 检查是否开启了 CCR 预装载 <code>OCxPreload = ENABLE</code></li>
<li>✅ 检查是否开启了 ARR 预装载 <code>AutoReloadPreload = ENABLE</code></li>
<li>✅ 确认修改占空比的代码是在中断安全的上下文中执行</li>
<li>✅ 检查是否有更高优先级的中断长时间阻塞</li>
<li>✅ 用示波器观察波形，确认是偶发还是周期性抖动</li>
</ol>
<p><strong>最可能原因：</strong> 90% 的情况是预装载没有开启</p>
<h3 id="72-编码器丢脉冲">7.2 编码器丢脉冲</h3>
<p><strong>现象：</strong> 电机快速转动时位置不准，正反转圈数不匹配</p>
<p><strong>排查步骤：</strong></p>
<ol>
<li>✅ 检查输入滤波器配置，ICFilter 值是否足够</li>
<li>✅ 确认编码器电源是否稳定，是否有纹波</li>
<li>✅ 检查信号线是否过长，是否需要上拉电阻</li>
<li>✅ 用示波器观察 A、B 相波形，确认边沿是否干净</li>
<li>✅ 检查中断优先级是否足够高，是否被其他中断阻塞</li>
</ol>
<p><strong>最可能原因：</strong> 机械振动导致的信号抖动，滤波器深度不够</p>
<h3 id="73-输入捕获测量不准">7.3 输入捕获测量不准</h3>
<p><strong>现象：</strong> 测量值跳动大，误差超过预期</p>
<p><strong>排查步骤：</strong></p>
<ol>
<li>✅ 确认定时器时钟计算正确（记住那个 ×2 的规则！）</li>
<li>✅ 检查信号源是否稳定，建议先用信号发生器做测试</li>
<li>✅ 计算理论误差，确认是否在合理范围内</li>
<li>✅ 检查中断服务函数执行时间是否过长</li>
<li>✅ 考虑多次测量取平均值</li>
</ol>
<p><strong>最可能原因：</strong> 定时器时钟计算错误，或者中断响应延迟</p>
<h3 id="74-定时器突然停止工作">7.4 定时器突然停止工作</h3>
<p><strong>现象：</strong> 运行一段时间后定时器毫无征兆地停止</p>
<p><strong>排查步骤：</strong></p>
<ol>
<li>✅ 检查是否有地方调用了 <code>HAL_TIM_PWM_Stop()</code> 之类的函数</li>
<li>✅ 确认没有出现硬件错误中断</li>
<li>✅ 检查定时器时钟是否被意外关闭</li>
<li>✅ 查看是否有栈溢出破坏了定时器句柄</li>
<li>✅ 确认不是低功耗模式导致的定时器关闭</li>
</ol>
<p><strong>最可能原因：</strong> 栈溢出破坏了 <code>htim</code> 结构体，这是最隐蔽也最常见的原因</p>
<h2 id="八进阶方向与最佳实践">八、进阶方向与最佳实践</h2>
<h3 id="81-定时器同步技术">8.1 定时器同步技术</h3>
<p>高级应用中经常需要多个定时器同步工作，比如多轴电机控制需要所有轴的 PWM 同时更新。STM32 提供了主从模式（ITR 内部触发）来实现定时器之间的同步。</p>
<p>一个定时器作为 Master，通过 TRGO 输出触发信号，其他定时器作为 Slave，接收触发信号同时启动。这样可以实现多个定时器的精确同步，偏差不超过一个时钟周期。</p>
<h3 id="82-dma--定时器组合">8.2 DMA + 定时器组合</h3>
<p>如果需要生成复杂的波形序列（比如步进电机的 S 曲线加速），可以用 DMA 配合定时器的更新事件，自动从内存中加载新的 CCR 值。完全不需要 CPU 干预，就能生成任意复杂的波形。</p>
<h3 id="83-hrtim-高精度定时器">8.3 HRTIM 高精度定时器</h3>
<p>对于 STM32F3、G4、H7 等较新型号，还有一个更强大的 HRTIM（高分辨率定时器），时钟频率可以达到 4.2GHz，PWM 分辨率可以达到亚纳秒级。如果你在做开关电源、高精度电机控制，HRTIM 绝对值得深入研究。</p>
<h3 id="84-代码组织最佳实践">8.4 代码组织最佳实践</h3>
<p>最后分享几个我多年总结的最佳实践：</p>
<ol>
<li>
<p><strong>集中管理定时器资源</strong></p>
<ul>
<li>建立一个统一的定时器配置表，而不是分散在各个模块里</li>
<li>清晰标注每个定时器的用途、频率、中断优先级</li>
</ul>
</li>
<li>
<p><strong>统一的错误处理</strong></p>
<ul>
<li>所有定时器初始化都要检查返回值</li>
<li>初始化失败时要有明确的错误处理，而不是 silently fail</li>
</ul>
</li>
<li>
<p><strong>寄存器操作加注释</strong></p>
<ul>
<li>直接操作寄存器虽然高效，但可读性差</li>
<li>一定要写清楚每一行代码在做什么，为什么这么做</li>
</ul>
</li>
<li>
<p><strong>编写调试辅助函数</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* 打印定时器状态，调试时非常有用 */</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">timer_dump_status</span><span class="p">(</span><span class="n">TIM_TypeDef</span> <span class="o">*</span><span class="n">TIMx</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;CR1: 0x%08lx</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">TIMx</span><span class="o">-&gt;</span><span class="n">CR1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;SR:  0x%08lx</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">TIMx</span><span class="o">-&gt;</span><span class="n">SR</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;CNT: %lu</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">TIMx</span><span class="o">-&gt;</span><span class="n">CNT</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;PSC: %lu</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">TIMx</span><span class="o">-&gt;</span><span class="n">PSC</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;ARR: %lu</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">TIMx</span><span class="o">-&gt;</span><span class="n">ARR</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;CCR1: %lu</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">TIMx</span><span class="o">-&gt;</span><span class="n">CCR1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div></li>
</ol>
<h2 id="总结">总结</h2>
<p>STM32 的定时器是一个功能极其强大但也异常复杂的外设。从简单的定时中断，到 PWM 输出、输入捕获、编码器接口，再到定时器同步、DMA 配合使用，每一种模式都有其独特的应用场景和需要注意的细节。</p>
<p>本文覆盖了定时器四大核心模式的原理分析、完整代码实现、常见坑点排查以及性能优化技巧。但这些仍然只是冰山一角，定时器还有更多高级功能等待你去探索：互补输出与死区插入、刹车功能、霍尔传感器接口、DMA  burst 传输等等。</p>
<p>对于嵌入式开发者来说，真正掌握定时器的使用是一个重要的里程碑。它意味着你已经从&quot;会用单片机&quot;进化到了&quot;能用好单片机&rdquo;，能够开始处理真正有挑战性的工程问题。</p>
<p>希望这篇文章能够帮助你跨过这个门槛。记住：理论学习很重要，但更重要的是动手实践。找一块开发板，接一个编码器、一个电机、一个示波器，然后把本文讲的内容都亲手试一遍。遇到问题、解决问题的过程，才是成长最快的时候。</p>
<p>嵌入式开发没有捷径，唯手熟尔。</p>
<p>（全文完，约 8500 字）</p>
]]></content:encoded></item></channel></rss>