ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

LPC2124定时器实现跑马灯:ARM7裸机开发详解

2026/10/5 6:06:38 拓冰建站 浏览量
LPC2124定时器实现跑马灯:ARM7裸机开发详解 跑马灯大概是所有单片机玩家的入门仪式。但同样是跑马灯用51单片机的寄存器写法、用STM32标准库的写法和用LPC2124这种ARM7芯片裸机操作背后的思路完全不一样。这篇我把自己用LPC2124定时器实现跑马灯的完整过程记录下来从时钟计算、寄存器配置、GPIO驱动到状态机设计再到实际调试中踩过的坑全部展开讲清楚希望能帮到正在啃LPC2124手册、或者刚接触ARM7裸机开发的朋友。这个项目表面上是点灯实际上涵盖了ARM7程序开发的几条主线定时器中断怎么进、匹配寄存器怎么算时间、引脚复用怎么切GPIO、中断服务程序和主循环之间怎么配合。把这些东西串起来跑马灯只是顺便做出来的效果。所以下面不会只贴一段能跑的代码而是把每一步的思考过程也写出来这样你换成别的芯片、别的定时器通道也能照着一套方法迁移。1. 跑马灯方案设计先想清楚再动手1.1 为什么不用软件延时而坚持用定时器很多新手做跑马灯第一反应是写一个 delay 函数然后用 while(1) 循环去翻转 LED。这个思路在只做“灯来回闪”的时候确实能跑但问题在于delay 期间 CPU 被完全占死了干不了任何别的事。你哪怕想在点灯间隙扫描一个按键、刷新一个数码管都要重新设计延时结构代码很快就变成一团乱麻。用硬件定时器的核心价值是定时器自己数数数到指定值以后产生中断CPU 只在中断里花极少的时间处理状态主循环可以空出来做别的事。这是一种“时间基准 状态机”的思想。跑马灯的时间基准由定时器提供灯怎么跑由主循环里的状态机决定两边解耦后面想调速、想改成呼吸灯、想加按键交互都不需要推翻重写。LPC2124 内部有多个定时器这里我用 Timer0。它本质是一个 32 位计数器挂在 VPB 总线上时钟来自 PCLK。通过预分频器 PR 可以降低计数频率通过匹配寄存器 MR0~MR3 可以设定比较值一旦 TC 的值等于 MR0硬件就会置起中断标志甚至自动清零 TC 重新计数。这个“自动清零重装”的机制是跑马灯精准定时的基础。1.2 先确定跑马灯的效果和目标时间基准开始写代码之前我习惯先把效果定义清楚。这个例程我设定为 8 个 LED 做往返流水从第 1 个亮到第 8 个再从第 8 个回到第 1 个每个灯点亮 50ms。用间隔 50ms 而不是 500ms是因为流水效果需要一定速度感肉眼能明显看到灯在“追着跑”但又不至于闪到看不清。50ms 这个数值我不会直接写在定时器匹配值里而是拆成“时间基准”和“计数倍数”两层定时器每 10ms 进一次中断维护一个 tick 计数器累计 5 个 tick 就执行一次点亮位置的移动。这样做的最大好处是以后想改成 100ms 间隔只需要改倍数不需要动定时器参数。硬件上LED 接在 P0.16~P0.23 这 8 个引脚上。这个选择有讲究后面在 GPIO 部分细说。2. 定时器的工作原理与寄存器级配置2.1 从 PCLK 到中断事件整条链路是怎么走的很多人配置定时器时只关心“我该往哪个寄存器写什么数”却不清楚信号是怎么流的。其实搞清楚链路以后寄存器配置就不用背了。LPC2124 的 Timer0 时钟链路是这样的外部晶振给 CPU 提供主时钟 CCLKCCLK 经过 VPB 分频器产生外设时钟 PCLK。Timer0 首先把 PCLK 送入预分频器预分频器每计到 PR 寄存器的值就输出一个时钟脉冲给主计数器 TCTC 每收到一个脉冲就加 1。TC 的值会不断和 MR0~MR3 比较一旦相等根据 MCR 寄存器的配置可以产生中断、复位 TC、或翻转匹配输出引脚。这就像秒表计时PCLK 是连续跳动的最小刻度预分频器相当于把每分钟分成 60 秒TC 则记录已经过了多少秒MR0 是预设的闹钟时间。闹钟一响就是一次匹配事件。中断产生后IR 寄存器的对应位会置 1这个标志不会自动清除必须在中断服务程序里手动写 1 清除。同时中断服务程序退出前还要向 VICVectAddr 写 0通知中断控制器本次中断处理完毕。这两步漏掉任何一个程序都会表现得非常诡异后面常见问题里专门说。2.2 匹配时间到底怎么算给出可套用的公式跑马灯的时间精度不要求特别高但作为基本功必须会算 PR 和 MR0 的组合。公式其实很简单计数频率 PCLK / (PR 1)单次匹配周期 (MR0 1) / 计数频率注意这里很多新手会栽跟头为什么是 PR1、MR01因为计数器是从 0 开始计数的。PR 写入 2999那么预分频器会依次计 0、1、2……2999这已经是 3000 个 PCLK 周期了。同样MR0 写入 9TC 会从 0 跑到 9一共 10 个计数周期。Thật ra就是数数的起点决定了“数了多少个”而不是“数到了哪个数”。以我这块板子为例外部晶振 12MHz不启用 PLL所以 CCLK 就是 12MHz。VPB 分频器复位后的默认值是 4 分频所以 PCLK 12MHz / 4 3MHz。我需要 10ms 的时间基准。先让计数频率等于 1kHz也就是每个计数周期 1ms那么PR PCLK / 1000 - 1 3000 - 1 2999MR0 10 - 1 9这样 TC 从 0 加到 9正好 10ms。为了后面代码更好维护我会直接用宏来计算而不是写死数字。这样换晶振、换 PCLK、换时间基准的时候只需要改一个宏。2.3 关键寄存器的配置清单与含义LPC2124 的 Timer0 有一组寄存器这里不打算全讲只列跑马灯必须用的几个每一个对应什么作用必须清楚。TCR 是定时器控制寄存器bit0 写 1 启动计数写 0 停止bit1 写 1 复位 TC 和 PC。初始化时一般先写 0x02 复位再写 0x01 启动。IR 是中断标志寄存器。MR0 匹配后 bit0 会变 1读它判断中断来源写 1 把标志清掉。即使你确定只用了 MR0我建议还是读出来判断一下这样代码清晰以后加其他通道也方便。MCR 是匹配控制寄存器。bit0 是 MR0I置 1 允许 MR0 中断bit1 是 MR0R置 1 表示 MR0 匹配后自动复位 TC。跑马灯必须这两个同时置 1否则 TC 会一路加到 0xFFFFFFFF 才归零中断间隔完全失控。IMR 是中断屏蔽寄存器bit0 置 1 放行 MR0 中断信号到 VIC。VIC 是 ARM7 的中断控制器Timer0 默认是 IRQ 通道 4。VICIntSelect 要把这一位清零让它走 IRQ 而不是 FIQVICIntEnable 的 bit4 置 1 开启通道VICVectCntl0 和 VICVectAddr0 则把 Timer0 的中断服务函数挂到向量表里。3. GPIO 与 LED 驱动电路细节3.1 LPC2124 的引脚复用这一步漏了灯根本不亮LPC2124 的 GPIO 引脚不是上电就能直接用的。P0.16~P0.23 默认很可能被复用成其他功能必须在初始化时通过 PINSEL 寄存器把它们切换成 GPIO 模式。P0.16~P0.23 属于高 16 位引脚由 PINSEL1 寄存器管理。每组引脚由 2 个 bit 决定功能00 表示 GPIO01、10、11 分别对应不同的第二、第三、第四功能。所以我把 PINSEL1 的低 16 位全部清零也就是把 P0.16~P0.23 全部切到 GPIO。然后是方向控制IO0DIR 的对应位写 1 表示输出。对于 P0.16 到 P0.23就是高 8 位置 1。方向设置完成之前引脚还处于输入模式你写 IO0SET 和 IO0CLR 也不会有什么电平效果。这里我想特别提醒LPC2124 的 P0.14、P0.15 这两根引脚比较特殊P0.14 在复位时如果被拉低芯片会进入 ISP 下载模式程序不跑P0.15 也影响启动行为。所以做实验时我刻意避开了这两个引脚避免上一电连仿真器都连不上还以为芯片坏了。3.2 低电平点亮还是高电平点亮电路决定代码点亮 LED 的电路方案有两种常见接法一种是 IO 输出高电平点亮LED 正极接 IO负极经限流电阻接地另一种是 IO 输出低电平点亮LED 正极接 VCC负极经限流电阻接 IO。LPC2124 的 GPIO 输出高电平时驱动能力并不强而灌电流能力相对好一些所以很多开发板上采用低电平点亮。我这个例程也按低电平点亮设计初始化时把所有 LED 引脚全部输出高电平也就是全部熄灭需要点亮某一颗时把对应引脚拉低。限流电阻的选择上假设 LED 压降约 2V电源 3.3V电阻两端约 1.3V。选 1kΩ 电阻电流大约 1.3mA对于普通高亮 LED 已经足够亮而且不会给 GPIO 造成压力。如果你发现亮度不够可以换 470Ω但不要低于 330Ω否则电流偏大长期运行对引脚不友好。GPIO 的相关寄存器也很简单IO0SET 是置位寄存器向某位写 1 让对应引脚输出高电平IO0CLR 是清零寄存器写 1 输出低电平。这两个寄存器都是“写 1 生效写 0 无影响”所以操作单个引脚不影响其他引脚的状态。4. 完整代码实现与工程组织4.1 建议的文件划分与寄存器地址定义跑马灯代码不多但我不建议全部塞在 main 文件里。这个项目我用三个文件组织main.c 负责主循环timer0.c 负责定时器和中断led.c 负责 LED 的初始化和状态切换。头文件里统一声明接口函数定时器和 LED 的细节对外部隐藏。如果你用的是 Keil MDK 或者 ADS启动文件会帮你完成堆栈初始化和 main 跳转这部分不用自己写。寄存器地址我直接在头文件里用宏定义好比如#define T0IR (*(volatile unsigned int *)0xE0004000) #define T0TCR (*(volatile unsigned int *)0xE0004004) #define T0TC (*(volatile unsigned int *)0xE0004008) #define T0PR (*(volatile unsigned int *)0xE000400C) #define T0MCR (*(volatile unsigned int *)0xE0004014) #define T0MR0 (*(volatile unsigned int *)0xE0004018) #define IMR (*(volatile unsigned int *)0xE0004020)注意寄存器地址最好用 unsigned int 指针访问避免 char 或 short 指针带来的读写宽度问题。VIC 相关的寄存器地址也一并定义好放在同样的头文件里。4.2 定时器初始化把参数算清楚再写代码定时器初始化的顺序很重要乱写容易出怪问题。我的习惯是先复位定时器再配置预分频和匹配值最后配 VIC 和中断全部就绪后再启动计数。时钟参数我用宏定义#define FOSC 12000000UL // 外部晶振 12MHz #define CCLK 12000000UL // 未启用 PLLCCLK FOSC #define VPBDIV 4UL // VPB 分频系数 #define PCLK (CCLK / VPBDIV) // 3MHz #define TICK_US 10000UL // 时间基准 10ms #define PR_VAL (PCLK / 1000UL) // 计数频率 1kHz #define MR0_VAL (TICK_US / 1000UL) // 10ms 需要 10 个计数初始化函数这样写void Timer0_Init(void) { T0TCR 0x02; // 先复位定时器 T0PR PR_VAL - 1; // 预分频让计数频率为 1kHz T0MR0 MR0_VAL - 1; // 匹配值10ms T0MCR (1 0) | (1 1); // MR0 匹配产生中断并自动复位 TC T0IR 0xFF; // 清掉所有中断标志 VICIntSelect ~(1 4); // Timer0 设为 IRQ VICVectCntl0 (1 5) | 4; // 使能向量槽 0绑定 Timer0 通道 VICVectAddr0 (unsigned long)Timer0_IRQHandler; VICIntEnable | (1 4); // 使能 Timer0 中断 T0TCR 0x01; // 启动定时器 }这里要重点解释 VICVectCntl0 的写法。bit5 写 1 表示该向量槽使能低 5 位写 4 表示这是 Timer0 的中断通道。如果把通道号写错中断会跳到一个错误的地方程序就飞了。VICVectAddr0 存放的是中断服务函数的地址编译器会自动把函数名转换成地址。4.3 中断服务函数标志清除与 tick 累积中断服务函数要尽量短。我的处理是先判断是不是 MR0 引起的中断是的话把标志清掉然后递增 tick。当 tick 累计到 5 次也就是 50ms 时让 LED 状态机走一步。注意LPC2124 的 GPIO 操作在主循环和中断里都可能发生但只要我们只在中断里调用 LED_Step主循环不直接操作 LED 引脚就不会产生竞争问题。volatile unsigned int g_tick 0; __irq void Timer0_IRQHandler(void) { if (T0IR (1 0)) { T0IR (1 0); // 清 MR0 中断标志 g_tick; if (g_tick 5) { g_tick 0; LED_Step(); } } VICVectAddr 0; // 通知 VIC 中断处理结束 }如果你用的编译器是 IAR中断函数可能是 __irq __arm 修饰如果是 GCC 环境可能是attribute((interrupt(IRQ)))。这个写法要跟着工具链走但逻辑是一致的。4.4 LED 驱动查表法实现往返流水LED 初始化和状态切换放在 led.c 中。我用了一个简单的查表方式每个 LED 对应一个引脚掩码状态切换时先全部熄灭再点亮当前需要亮的那个引脚。#define LED_START_PIN 16 #define LED_NUM 8 #define LED_ALL_MASK (0xFFUL LED_START_PIN) static unsigned char led_pos 0; static unsigned char led_dir 1; void LED_Init(void) { PINSEL1 ~0xFFFF; // P0.16~P0.23 全部设为 GPIO IO0DIR | LED_ALL_MASK; // 设为输出 IO0SET LED_ALL_MASK; // 低电平点亮所以先全部拉高熄灭 } void LED_Step(void) { IO0SET LED_ALL_MASK; // 熄灭所有 LED if (led_pos 0) { led_dir 1; } if (led_pos LED_NUM - 1) { led_dir 0; } IO0CLR (1UL (LED_START_PIN led_pos)); // 点亮当前位置 if (led_dir) { led_pos; } else { led_pos--; } }主函数则非常简单初始化完以后主循环就空着了int main(void) { LED_Init(); Timer0_Init(); while (1) { // 主循环可以留空也可以在这做按键扫描等其他任务 } }这个结构看起来简单但已经具备了一个小型嵌入式裸机程序的骨架初始化、中断驱动、状态机刷新、主循环空闲。后面你想加按键、加串口打印只需要挂到主循环里不用碰中断部分的代码。5. 调试验证与常见问题速查5.1 跑起来之后怎么验证时间准不准程序下载以后第一件事不是看灯跑得花不花而是确认时间基准对不对。我一般用示波器或者逻辑分析仪抓某个 LED 引脚的电平测一个完整流水周期的时间。比如 8 个灯做往返每个灯亮 50ms一个来回是 800ms。如果示波器测出来的周期明显偏长或偏短说明 PCLK 的计算和实际不一致。最常见的两种原因一是晶振实际频率不是 12MHz二是 VPB 分频不是默认的 4 分频。这两种情况下代码里的 PCLK 和硬件实际值对不上时间自然不准。没有示波器的话也可以用万用表看平均电压但只能大致判断在闪测不了精确时间。我更推荐把某个引脚翻转频率放到 1Hz 左右用手机秒表对着灯光数 10 个周期误差在一两秒内基本说明定时器链路是对的。5.2 典型故障为什么灯不亮、不流水、乱流水调试过程中我遇到过不少问题这里整理成一张速查表按症状排查会快很多。症状LED 全不亮。优先检查引脚复用和方向寄存器。PINSEL1 如果没设为 GPIO引脚根本不归 GPIO 外设管IO0DIR 没设输出写电平无效。这俩是新手最容易漏的。症状LED 全亮但不动。大概率是定时器没有启动或者 MCR 的 bit1 没置 1。T0TCR 没写 0x01TC 不计数MCR 没配自动复位TC 会跑到溢出才产生一次中断中间间隔极其漫长看起来就是没在跑。症状中断进了出不来程序卡死。检查两点有没有在 ISR 里清中断标志有没有在 ISR 末尾写 VICVectAddr 0。ARM7 的 VIC 在中断响应后如果不清 VICVectAddr中断返回时会再次触发同一中断形成死循环。症状上电后程序不跑仿真器也连不上。别再怀疑代码去量 P0.14 的电平。如果这个引脚被外部电路拉低芯片会进入 ISP 模式不执行用户程序。这个坑在 LPC2124 上特别常见。症状时间比预期慢一倍。看看 PCLK 是否被配置成了 2 分频或者 1 分频。VPB 分频寄存器复位值通常是 4 分频但如果 bootloader 或者其他代码改过它PCLK 就会变。跑不到预期频率时先在调试器里读一下 VPBDIV 寄存器的实际值。5.3 进阶扩展从跑马灯到真正的定时器应用跑马灯跑顺之后不建议马上换项目而是先在原工程上做几个小改动把定时器的能力吃透。第一个改动是把流水间隔改成按键调节。每次按下按键就把 tick 倍率改一下50ms 变成 100ms、200ms。这能让你理解“中断只负责产生时间基准业务逻辑在主循环里”的架构价值。第二个改动是尝试使用两路匹配通道。Timer0 有 MR0~MR3MR0 继续做流水基准MR1 可以产生一个 500ms 的慢速中断用来闪烁另一个状态灯。这能帮你理解多路匹配通道的独立性和共享计数器的特点。第三个改动是把匹配输出引出来。LPC2124 的部分定时器通道可以把匹配事件直接映射到 MAT 引脚不需要进中断硬件自动翻转电平。这种方式用于产生精确方波非常有用也是你以后做 PWM 输出、输出比较模式的基础。还有一个我特别推荐的练习把定时器从“定时中断”换成“输入捕获”用另一个信号源测量脉冲宽度。跑马灯只是定时器的单向使用输入捕获才是定时器更强大的另一面。等你把匹配、捕获两条路都打通再回去看 STM32 的定时器你会发现大部分概念都能平移过去。最后再说点实在的这个跑马灯例程代码量不到一百行但它值得反复咀嚼的地方其实很多。我个人在做完这个项目以后最明显的感觉是以前写单片机代码总喜欢把逻辑全部堆在 while(1) 里靠延时硬撑时序做完定时器中断驱动以后才慢慢养成“硬件负责计时软件负责状态”的思维方式。这种思维方式的转变比会配几个寄存器重要得多。如果你在自己实验过程中发现灯跑起来有“一卡一卡”的感觉先不要急着优化代码去查一查中断服务函数里是不是做了太多事情。中断里只做标志置位和极其简单的状态切换其他事情丢给主循环处理这是裸机编程里性价比最高的优化手段。另外8 个 LED 的限流电阻如果没有合适阻值用 1k 附近的都行不必强求精确关键是不要从驱动能力弱的高电平方向硬拉电流那样容易让引脚进入异常状态。顺着这条路往下走你可以把跑马灯的 LED 换成 PWM 控制的呼吸灯或者把 Timer0 的匹配通道扩展成软件定时器维护一个定时器任务链表。这些都是在同一颗 LPC2124 上能完成的进阶玩法跑马灯只是个开始。