3. LPTIM 寄存器详解:CR、CMP、ARR、CNT、ISR

寄存器这东西,说白了就是外设的“控制面板”。你往哪个地址写什么值,它就干什么事。LPTIM 虽然叫低功耗定时器,但寄存器数量并不多,常用的也就五六个。我刚开始接触 LPTIM 时,总觉得寄存器手册翻起来头疼,后来发现只要抓住几个核心的,其他都是变种。

今天咱们就把 LPTIM 的五个关键寄存器掰开揉碎讲清楚:控制寄存器 (CR)、比较寄存器 (CMP)、自动重载寄存器 (ARR)、计数器寄存器 (CNT)、中断状态寄存器 (ISR)。嗯,这五个搞明白了,LPTIM 你就掌握了八成。

3.1 控制寄存器 (CR) —— 定时器的“总开关”

控制寄存器,地址偏移 0x00。它负责定时器的启动、停止、复位,以及时钟源选择。我个人习惯把 CR 比作汽车的“点火钥匙”——拧到哪个档位,车就进入什么状态。

位域 名称 功能描述
31:3 保留 必须保持复位值
2 CNTSTRT 启动计数器(写1启动,写0无效)
1 SNGSTRT 单次启动模式(启动后计数一次自动停止)
0 ENABLE 使能定时器(必须先使能才能启动)

这里有个坑,我踩过好几次:必须先写 ENABLE 位,再写 CNTSTRT 位。顺序反了,定时器根本不会动。你想想看,就像你先把车钥匙拧到点火位,再踩油门,车才能走。反过来先踩油门再拧钥匙,发动机都没通电,怎么可能走?

关键操作顺序:

  1. 配置其他寄存器(CMP、ARR、预分频等)
  2. 写 CR 的 ENABLE 位 = 1
  3. 写 CR 的 CNTSTRT 位 = 1

另外,SNGSTRT 位适合做“一次性延时”。比如我只想等 100ms 后触发一次中断,用单次模式最省电——计数器跑完就自动停了,不用软件干预。

3.2 自动重载寄存器 (ARR) —— 定时的“刻度尺”

ARR 寄存器,地址偏移 0x14。它决定了定时器的计数周期。LPTIM 是向上计数的,从 0 开始,一直数到 ARR 的值,然后产生更新事件(或者溢出),再重新从 0 开始。

公式很简单:

定时周期 = (ARR + 1) × 预分频系数 × 时钟周期

举个例子,假如时钟源是 32.768 kHz 的 LSE,预分频设为 128,ARR 设为 255:

定时周期 = (255 + 1) × 128 × (1 / 32768) = 256 × 128 / 32768 = 1 秒

我在项目中遇到过一个问题:ARR 的值不能设为 0。虽然手册没明说,但 ARR = 0 时计数器会一直停在 0,不会产生任何事件。嗯,这个坑让我浪费了半天调试时间。

小技巧:如果你需要极短的定时(比如几十微秒),可以把预分频设小一点,ARR 设小一点。但注意,LPTIM 的时钟源频率通常不高(LSE 32.768 kHz),想做到微秒级精度,建议切换到 LSI 或 HSI16 时钟源。

3.3 比较寄存器 (CMP) —— 灵活的“闹钟”

CMP 寄存器,地址偏移 0x10。它用来设置一个比较值,当 CNT 计数到 CMP 的值时,会产生比较匹配事件。这个事件可以触发中断,也可以触发 DMA。

ARR 和 CMP 的区别,我打个比方:

  • ARR 像“下课铃”——时间到了,全班下课(更新事件)。
  • CMP 像“课间提醒”——时间到了,提醒你该准备下课了(比较事件)。

你可以把 CMP 设成 ARR 的一半,实现 50% 占空比的 PWM。也可以设成任意值,实现“提前通知”的效果。

我曾经在一个电池供电的传感器项目中,用 CMP 实现“预唤醒”:主 CPU 还在睡眠,LPTIM 计数到 CMP 值时先唤醒 DMA 准备数据,等计数到 ARR 时再唤醒 CPU 处理。这样 CPU 的睡眠时间更长,整体功耗更低。

注意:CMP 的值必须小于等于 ARR。如果 CMP > ARR,比较事件永远不会发生。别问我怎么知道的……

3.4 计数器寄存器 (CNT) —— 实时“里程表”

CNT 寄存器,地址偏移 0x24。它只读,显示当前计数值。你可以随时读取它,了解定时器跑到哪了。

读取 CNT 有个讲究:必须在 LPTIM 使能且启动后才能读到有效值。如果定时器没启动,读出来永远是 0。

另外,CNT 是 16 位的,最大值 65535。如果你的 ARR 设得很大,CNT 会从 0 一直跑到 ARR,然后归零重新跑。所以读 CNT 时要注意:它可能已经翻过好几次了。

我习惯在调试时把 CNT 值通过串口打印出来,看看定时器跑得准不准。有一次发现 CNT 跳变异常,排查了半天,结果是时钟源配置错了——LSE 没起振,系统自动切到了 MSI,频率完全不对。

3.5 中断状态寄存器 (ISR) —— 事件“公告板”

ISR 寄存器,地址偏移 0x1C。它记录了定时器发生的各种事件。当某个事件发生时,对应的标志位会置 1。你需要在中断服务函数里读取 ISR,判断是哪个事件,然后写 1 清除标志位。

位域 名称 触发条件
8 DOWN 计数器向下计数到 0(仅编码器模式)
7 UP 计数器向上计数到 ARR(仅编码器模式)
6 ARROK ARR 寄存器更新完成
5 CMPOK CMP 寄存器更新完成
4 EXTTRIG 外部触发事件
3 ARRM 自动重载匹配(计数器到达 ARR)
2 CMPM 比较匹配(计数器到达 CMP)
1 EXTTRIGF 外部触发边沿检测
0 GLOBAL 全局中断标志(任意事件触发)

最常用的两个标志位是 ARRM 和 CMPM。ARRM 表示一个定时周期结束,CMPM 表示到达预设的比较点。

清除标志位的方法是:向该位写 1。对,你没看错,是写 1 清除,不是写 0。这是 STM32 的老传统了,我第一次用的时候习惯性写 0,结果中断一直触发,死循环了。

中断服务函数典型写法:

void LPTIM1_IRQHandler(void)
{
    if (LPTIM1->ISR & LPTIM_ISR_ARRM)  // 检查 ARR 匹配
    {
        LPTIM1->ICR |= LPTIM_ICR_ARRMCF;  // 清除标志位
        // 你的业务代码
    }
    if (LPTIM1->ISR & LPTIM_ISR_CMPM)  // 检查 CMP 匹配
    {
        LPTIM1->ICR |= LPTIM_ICR_CMPMCF;  // 清除标志位
        // 你的业务代码
    }
}

注意,我上面用的是 ICR 寄存器(中断清除寄存器),不是直接写 ISR。虽然写 ISR 也能清除,但 ST 官方推荐用 ICR,更规范。ICR 的地址偏移是 0x20,各位定义和 ISR 一一对应。

3.6 寄存器关系全景图

下面这张图,把五个寄存器的关系串起来了。我建议你把它存下来,调试时对照着看。

LPTIM 核心寄存器关系图 LPTIM 核心寄存器工作关系 时钟源 (LSE/LSI/HSI16) 预分频器 (CFGR) 计数器 (CNT) 从 0 向上计数到 ARR 自动重载 (ARR) 设定计数上限 比较值 (CMP) 设定比较点 中断状态 (ISR) ARRM / CMPM 标志位 控制寄存器 (CR) ENABLE / CNTSTRT / SNGSTRT 中断 / DMA / 唤醒 CNT 从 0 向上计数,到达 CMP 时产生比较事件,到达 ARR 时产生更新事件并归零 ISR 记录事件状态,CR 控制启动/停止,ICR 清除标志位

3.7 寄存器配置实战步骤

说了这么多理论,咱们来走一遍实际配置流程。假设我要用 LPTIM1,时钟源 LSE,预分频 128,ARR = 255,CMP = 127,使能中断:

  1. 开启时钟:RCC->CSR |= RCC_CSR_LSEON;等待 LSERDY 置位。
  2. 配置预分频:LPTIM1->CFGR = (128 - 1) << LPTIM_CFGR_PRESC_Pos。
  3. 设置 ARR:LPTIM1->ARR = 255。
  4. 设置 CMP:LPTIM1->CMP = 127。
  5. 使能中断:LPTIM1->IER |= LPTIM_IER_ARRMIE | LPTIM_IER_CMPMIE。
  6. 使能定时器:LPTIM1->CR |= LPTIM_CR_ENABLE。
  7. 启动计数:LPTIM1->CR |= LPTIM_CR_CNTSTRT。

嗯,第 6 步和第 7 步的顺序不能错。我曾经在代码里把这两步写反了,结果定时器纹丝不动,查了半天才发现是顺序问题。

调试小技巧:如果定时器不工作,先读 ISR 看看有没有任何标志位置位。如果 ISR 全为 0,说明定时器根本没跑起来,检查时钟源和 CR 配置。如果 ARRM 一直为 1,说明中断没清除,检查 ICR 操作。

好了,这五个寄存器讲完了。你可能会问:“就这?LPTIM 就这几个寄存器?” 对,核心的就这些。其他寄存器比如 CFGR(配置寄存器)、IER(中断使能寄存器)、ICR(中断清除寄存器)都是辅助性的,理解了这五个,其他的自然就通了。