1. 项目概述:用STM32的TIMER当“外部事件计数器”

在嵌入式开发里,我们经常需要统计外部脉冲的数量,比如旋转编码器的脉冲、光电传感器的触发次数,或者简单点,就是想知道一个引脚上来了多少个上升沿。很多朋友的第一反应可能是用外部中断(EXTI),来一个脉冲就进一次中断,然后在中断服务函数里累加一个变量。这个方法对于低频信号(比如每秒几个脉冲)没问题,但一旦频率上去,比如几十KHz甚至更高,频繁的中断就会成为CPU不可承受之重,严重拖累系统性能,甚至导致丢失脉冲。

这时候,STM32片内集成的通用定时器(TIMER)的“外部时钟模式”功能,简直就是为我们量身定做的硬件外挂。它能把一个指定的GPIO引脚(通常是定时器的ETR引脚)直接作为定时器的计数时钟源。外部来一个有效边沿,定时器的计数器就自动加1(或减1),完全不需要CPU干预。你可以把它想象成一个独立的、硬件实现的“电子计数器”,CPU只需要在需要的时候去读取一下当前的计数值就行,效率极高,且绝对精准。

今天要拆解的这个例程,就完美演示了如何将STM32的TIM2配置成一个纯粹的外部事件计数器。它的核心思路是: 使用TIM2的ETR引脚(PA1)作为计数信号的输入口,同时用另一个GPIO(PC6)模拟生成一个方波信号用于测试 。我们将通过代码和原理,把配置的每一个参数、每一步操作背后的“为什么”都讲清楚,让你不仅能照搬代码,更能理解其精髓,举一反三应用到你的编码器测速、频率测量或者产品产量计数等实际项目中去。

2. 核心思路与硬件连接解析

2.1 为什么选择“外部时钟模式2”?

STM32的通用定时器有几种时钟源:内部时钟(CK_INT)、外部触发输入(ETR)、内部触发输入(ITRx,用于定时器级联)等。要实现外部计数,我们需要让定时器的时钟来自外部引脚,这就需要用到“外部时钟模式”。

细看参考手册,你会发现外部时钟模式又分两种:

  • 外部时钟模式1(External Clock Mode 1) :时钟信号来自定时器的某个输入通道(TI1或TI2),经过边沿检测器后,作为触发信号(TRGI)驱动定时器。这种方式更常用于编码器接口或特定的触发同步。
  • 外部时钟模式2(External Clock Mode 2) :时钟信号直接来自 ETR引脚 。这是最直接、最纯粹的外部计数方式。ETR引脚上的信号经过可配置的极性选择、预分频器和滤波器后,直接成为定时器的计数时钟(CK_PSC)。

本例程的目标是 计数 ,信号源明确来自一个GPIO引脚(PA1),因此 外部时钟模式2 是最自然、最合适的选择。它配置简单,意图清晰:ETR脚来一个脉冲(上升沿或下降沿,可配置),计数器就动一下。

2.2 引脚功能映射与硬件连接要点

理解STM32的引脚复用功能是正确配置的前提。不是任何一个GPIO都能作为TIM2的ETR输入。

  • 信号输入引脚(PA1) :对于STM32F1系列(这也是本例程最可能基于的系列),TIM2的ETR输入固定映射在 PA1 引脚上。这意味着,你必须将PA1配置为复用功能输入(或者浮空输入,具体看外部驱动能力),信号才能进入TIM2的内部电路。如果你错接到PA0或PA2,代码怎么配都不会有计数。
  • 信号输出引脚(PC6) :这里选择PC6来模拟方波,是一个随意的选择,因为本例程中它并不需要连接到任何定时器的特殊功能,仅仅作为一个普通的GPIO输出。用 GPIO_SetBits GPIO_ResetBits 配合延时函数,来模拟一个频率粗略可控的方波。
  • 硬件连接 :测试时,用一根杜邦线将 PC6(输出) PA1(输入) 短接即可。这样PC6产生的方波就直接送到了TIM2的计数输入端。在实际项目中,PA1连接的就是你的外部信号源,如光电传感器输出、另一MCU的脉冲信号等。

注意 :不同系列的STM32,定时器引脚映射可能不同。例如,在STM32F4系列上,TIM2的ETR可能映射在PA0或PA5上。 务必查阅你所使用芯片型号对应的《数据手册》(Datasheet)中的“Pinouts and pin description”章节,以及《参考手册》(Reference Manual)中定时器章节的“TIMx alternate function mapping”部分。 这是硬件工程师和嵌入式工程师最重要的习惯之一。

3. 代码逐行深度解析与配置原理

下面我们跳出代码注释,从“为什么要这样设置”的角度,把每一行关键配置嚼碎了讲。

3.1 定时器时基单元初始化:设定计数器的“舞台”

TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
TIM_TimeBaseStructure.TIM_Prescaler = 0x00;
TIM_TimeBaseStructure.TIM_ClockDivision = 0x0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

这是定时器的基础框架配置,即使使用外部时钟,这个框架也决定了计数器如何运行。

  • TIM_Period = 0xFFFF :这是自动重装载寄存器(ARR)的值,设置为最大值65535。 为什么是0xFFFF? 因为在这个纯计数应用中,我们不希望计数器自动重载产生更新事件(UIF)或中断。我们只关心它从0开始能连续计多少个数。设置成最大值,意味着计数器从0累加到65535后,下一个脉冲会变成0(溢出),但如果我们在此前就读取了计数值,这个溢出对我们没影响。如果你想计数超过65535,就需要在计数器溢出时触发中断,在中断里用一个软件变量做高位累加,实现扩展计数。
  • TIM_Prescaler = 0x00 :预分频器设置为0,即不分频。 这里是个关键理解点 :这个预分频器是对 定时器时钟源(CK_PSC) 进行分频。现在我们的时钟源是外部ETR引脚信号。 TIM_Prescaler=0 意味着ETR引脚每来一个有效边沿,计数器 CNT 就加1。如果你设置 TIM_Prescaler=1 ,则是每2个边沿CNT加1(因为分频器是N+1生效)。 所以,在外部时钟模式下,这个预分频器实际上变成了“输入脉冲分频器” 。本例程不想分频,所以设为0。
  • TIM_ClockDivision = 0x0 :时钟分频,这个参数与外部时钟模式2的计数功能 无关 。它(CDR)是决定定时器内部时钟(CK_INT)与数字滤波器采样时钟(f_DTS)之间的比例,主要影响的是输入捕获模块的滤波器和死区时间生成。对于纯ETR计数,通常设为0即可。
  • TIM_CounterMode = TIM_CounterMode_Up :计数模式为向上计数。从0开始,加到ARR值(0xFFFF)。也可以配置为向下计数,取决于你的应用逻辑。

3.2 外部时钟模式2的精细配置:信号如何被“识别”

TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0);

这行代码是激活外部时钟模式2的灵魂。我们拆开看它的四个参数:

  1. TIM2 :指定定时器。
  2. TIM_ExtTRGPSC_OFF :ETR外部触发预分频器。这是 独立于时基单元预分频器 的、专属于ETR通路的又一个分频器。它有4种选择:OFF(不分频)、DIV2(2分频)、DIV4(4分频)、DIV8(8分频)。本例选择 OFF ,意味着ETR引脚信号不经过这个分频器,直接进入后续环节。 如果需要降低计数频率,可以优先考虑修改这里,而不是修改时基单元的 TIM_Prescaler ,因为逻辑上更清晰。
  3. TIM_ExtTRGPolarity_NonInverted :ETR极性选择。这是关键!
    • NonInverted (不反相):ETR引脚上的 上升沿 高电平 (取决于滤波器配置)被识别为有效信号。这是最常用的设置。
    • Inverted (反相):ETR引脚上的 下降沿 低电平 被识别为有效信号。 你的外部信号是上升沿有效还是下降沿有效,必须根据传感器或信号源的规格来正确设置这个参数,否则计数值不会变化。
  4. 0 :ETR数字滤波器参数。这个参数非常重要,特别是在工业环境或有噪声的场景。它由一个4位值(0-15)表示,用于配置一个采样频率和数字滤波窗口。
    • 0 无滤波器 。ETR信号直接以 f_DTS 频率被采样。 f_DTS 由前面的 TIM_ClockDivision 决定。当 TIM_ClockDivision=0 时, f_DTS = f_CK_INT (系统定时器时钟,通常72MHz)。这意味着在72MHz的频率下对ETR信号采样,抗噪声能力最弱,但响应最快。
    • 1~15 :启用滤波器。例如,设置值为 1 ,代表 f_SAMPLING = f_CK_INT, N=2 。其含义是:以 f_CK_INT 的频率对ETR引脚进行采样,当连续采样到2次(N)相同的电平时,才认为输入电平发生了改变。这能有效滤除毛刺脉冲。 如何选择? 如果你的信号很干净,用 0 。如果环境噪声大,信号有毛刺,就需要根据噪声脉宽和信号频率来计算并选择一个合适的滤波器值。例如,你的信号是100kHz,但可能有50ns的毛刺,而 f_CK_INT=72MHz (周期约13.9ns),那么毛刺会持续约3-4个采样周期。设置 N=4 N=6 的滤波器就能将其滤除,而真正的信号边沿变化会持续更长时间,能通过滤波。

3.3 启动与测试逻辑

TIM_SetCounter(TIM2, 0); // 计数器清零
TIM_Cmd(TIM2, ENABLE);   // 使能定时器,开始计数

// 模拟产生100个方波脉冲
for(i_Loop = 0; i_Loop < 100; i_Loop ++) {
    GPIO_SetBits(GPIOC, GPIO_Pin_6);
    Delay(10); // 假设Delay(10)大约延时10ms
    GPIO_ResetBits(GPIOC, GPIO_Pin_6);
    Delay(10);
}
// 读取计数结果
n_Counter = TIM_GetCounter(TIM2);
  1. TIM_SetCounter(TIM2, 0) :在开始前,将计数器寄存器(CNT)清零,确保我们从0开始计数。这是一个好习惯。
  2. TIM_Cmd(TIM2, ENABLE) :这个函数调用后,定时器才真正开始工作。对于外部时钟模式,一旦使能,定时器就“竖起耳朵”监听ETR引脚,等待第一个有效边沿的到来。
  3. 模拟方波循环 :这个循环通过拉高、拉低PC6,并用 Delay(10) 间隔,产生一个周期大约为20ms(高10ms,低10ms),即频率约为50Hz的方波。循环100次,共产生 100个上升沿 和100个下降沿。
    • 重要 :由于前面配置了 TIM_ExtTRGPolarity_NonInverted (上升沿有效),因此只有 上升沿 会被计数。所以,理论上最终 n_Counter 的值应该等于 100
    • 如果配置为 TIM_ExtTRGPolarity_Inverted ,则会计数下降沿,结果同样应该是100。
    • 如果信号有抖动,且未启用滤波器,计数值可能会大于100。
  4. TIM_GetCounter(TIM2) :这是读取当前计数值的函数。它直接返回TIM2->CNT寄存器的值。 读取操作本身不会影响计数器的持续运行 。定时器仍在后台默默地根据ETR引脚信号进行计数。

4. 进阶应用与实战经验分享

掌握了基础配置,我们来看看如何把这个功能用在更实际的场景,以及有哪些容易踩的坑。

4.1 应用场景拓展

  1. 旋转编码器脉冲计数(单相) :这是最直接的应用。将编码器的A相输出接到PA1(TIM2_ETR),配置为上升沿和下降沿均计数(通常需要结合输入捕获模式或编码器接口模式,纯ETR模式只能单边沿)。本例的纯计数模式适合对方向不敏感,只关心转动量的场合。
  2. 产品产量计数 :在流水线上,每个产品通过光电传感器产生一个脉冲。将这个脉冲接到ETR引脚,就可以实现非接触式的高速计数,CPU负载几乎为0。
  3. 频率测量的基础 :结合定时器的内部时钟,可以实现高精度频率测量。方法:用另一个定时器(如TIM3)产生一个精确的闸门时间(比如1秒),在这个闸门时间内,使能TIM2的外部计数。闸门时间到后,关闭TIM2并读取计数值,该值就是信号的频率(Hz)。这种方法测频范围宽,精度取决于闸门时间的精度。
  4. 作为其他定时器的触发源 :ETR信号不仅可以作为时钟,还可以作为触发输入,去启动、停止或同步其他定时器,实现复杂的定时器联动逻辑。

4.2 关键注意事项与避坑指南

  1. 引脚复用功能必须使能 :这是最常见的问题。除了将PA1配置为输入模式( GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING GPIO_Mode_IPU / GPIO_Mode_IPD ),对于很多STM32系列(尤其是F1),还需要 使能AFIO时钟 ,并且可能需要 重映射 (虽然TIM2_ETR通常不需要重映射)。最稳妥的做法是参考官方库例程中对应定时器输入通道的GPIO配置。
    // 以STM32F1标准外设库为例,通常需要这两步
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    
  2. 信号电压与电平兼容 :STM32的GPIO引脚通常是3.3V电平。确保你的外部信号源也是3.3V电平。如果是5V TTL信号,必须进行电平转换(如使用电平转换芯片或电阻分压),否则可能损坏芯片或无法正确识别。
  3. 计数器溢出处理 :本例中ARR设置为最大值0xFFFF,计满65535后会从0重新开始。如果你需要计更大的数,有几种方案:
    • 方案A(查询溢出) :使能定时器更新中断(UIE)。在中断服务函数中,对一个32位或64位的软件计数器(如 static volatile uint32_t s_overflow_count )进行加1操作。最终计数值 = s_overflow_count * 65536 + TIM_GetCounter(TIM2)
    • 方案B(使用DMA) :将定时器的计数器寄存器(CNT)配置为DMA的源地址,设定DMA在每次定时器更新事件(溢出)时,将CNT的值传输到内存的一个数组中。这样可以实现无CPU干预的连续大数据块采集。
  4. 滤波器的实战选择 :滤波器参数( TIM_ETRClockMode2Config 的最后一个参数)不是摆设。如果你发现计数值莫名其妙多了很多,大概率是信号有毛刺。你可以用示波器观察ETR引脚的实际波形。根据噪声的脉宽来估算滤波器参数。例如,系统时钟 f_CK_INT=72MHz ,周期T≈13.9ns。噪声脉宽 t_noise ≈100ns。则噪声持续约 100ns / 13.9ns ≈ 7 个采样周期。为了滤除它,你需要设置 N 大于7,比如选择 f_SAMPLING=f_CK_INT, N=8 (对应参数值?需要查表,可能是 TIM_ETRFilter_8FF 之类的宏定义,本例程中用的是数值,需要查手册对应表)。 原则是:在滤除噪声的前提下, N 值越小越好,以减少对真实信号边沿的延迟。
  5. 读取计数值的时机 TIM_GetCounter() 是瞬间读取CNT寄存器。在高速计数时,如果读取瞬间刚好发生在计数器递增的过程中,理论上存在读到中间状态的风险(虽然概率极低)。对于16位计数器,一次读操作是原子的,问题不大。但对于需要32位以上计数值的场景(结合了溢出中断),在读取软件高位计数和硬件低位计数时,可能会因为中断发生而产生误差。这时需要采取临界区保护,即在读取时暂时关闭定时器更新中断,读完后立即恢复。

4.3 调试技巧实录

当你按照代码配置好,却发现 TIM_GetCounter 读回来一直是0,可以按以下步骤排查:

  1. 确认硬件连接 :万用表或示波器检查PA1引脚上是否有预期的脉冲信号?电压幅度是否正确(0-3.3V)?
  2. 确认GPIO配置 :使用调试器,在运行时查看GPIOA的配置寄存器(CRL),确认PA1是否被正确配置为输入模式。或者,简单写段代码,将PA1配置成普通输入,然后用 GPIO_ReadInputDataBit 函数循环读取并打印其电平,看是否能随外部信号变化。
  3. 确认定时器是否使能 :查看TIM2的CR1寄存器(控制寄存器1)的 CEN 位是否为1。
  4. 确认时钟模式 :查看TIM2的SMCR寄存器(从模式控制寄存器)的 SMS 位是否为 111 (外部时钟模式2)。同时检查 ECE 位(外部时钟使能位)是否为1。
  5. 检查ETR配置 :查看TIM2的SMCR寄存器的 ETP (极性)、 ETPS (预分频)、 ETF (滤波器)位是否与你的代码设置一致。
  6. 简化测试 :先不用PC6模拟,直接用一个导线,手动触碰PA1引脚到3.3V(产生上升沿)和GND(产生下降沿),同时在调试器中观察TIM2->CNT的值是否变化。这是最直接的验证方法。
  7. 利用定时器状态标志 :定时器的SR(状态寄存器)中有个 TIF (触发中断标志)位。当ETR引脚检测到有效边沿时,这个标志位会被置1(即使你没开中断)。在调试时,可以循环读取这个标志位,看它是否会随着你给PA1施加脉冲而置位,这能帮你确认信号是否成功进入了定时器。

通过以上层层剖析,你应该不再仅仅是一个代码的搬运工,而是真正理解了STM32定时器外部计数模式的机理和所有关键配置项的用途。下次当你需要统计电机转速、计量流量脉冲或者做任何事件计数时,你就能自信地甩开低效的外部中断,祭出这个硬件计数利器,让你的系统运行得更稳健、更高效。记住,嵌入式开发中, 能让硬件干的事,绝不要麻烦CPU ,这是提升系统性能与可靠性的黄金法则。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐