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 位。顺序反了,定时器根本不会动。你想想看,就像你先把车钥匙拧到点火位,再踩油门,车才能走。反过来先踩油门再拧钥匙,发动机都没通电,怎么可能走?
关键操作顺序:
- 配置其他寄存器(CMP、ARR、预分频等)
- 写 CR 的 ENABLE 位 = 1
- 写 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 寄存器关系全景图
下面这张图,把五个寄存器的关系串起来了。我建议你把它存下来,调试时对照着看。
3.7 寄存器配置实战步骤
说了这么多理论,咱们来走一遍实际配置流程。假设我要用 LPTIM1,时钟源 LSE,预分频 128,ARR = 255,CMP = 127,使能中断:
- 开启时钟:RCC->CSR |= RCC_CSR_LSEON;等待 LSERDY 置位。
- 配置预分频:LPTIM1->CFGR = (128 - 1) << LPTIM_CFGR_PRESC_Pos。
- 设置 ARR:LPTIM1->ARR = 255。
- 设置 CMP:LPTIM1->CMP = 127。
- 使能中断:LPTIM1->IER |= LPTIM_IER_ARRMIE | LPTIM_IER_CMPMIE。
- 使能定时器:LPTIM1->CR |= LPTIM_CR_ENABLE。
- 启动计数:LPTIM1->CR |= LPTIM_CR_CNTSTRT。
嗯,第 6 步和第 7 步的顺序不能错。我曾经在代码里把这两步写反了,结果定时器纹丝不动,查了半天才发现是顺序问题。
调试小技巧:如果定时器不工作,先读 ISR 看看有没有任何标志位置位。如果 ISR 全为 0,说明定时器根本没跑起来,检查时钟源和 CR 配置。如果 ARRM 一直为 1,说明中断没清除,检查 ICR 操作。
好了,这五个寄存器讲完了。你可能会问:“就这?LPTIM 就这几个寄存器?” 对,核心的就这些。其他寄存器比如 CFGR(配置寄存器)、IER(中断使能寄存器)、ICR(中断清除寄存器)都是辅助性的,理解了这五个,其他的自然就通了。