简介:面向嵌入式入门与进阶开发者,这份LPC1114例程与教程合集以NXP Cortex-M0内核芯片为主线,系统讲解外设配置与典型应用,可广泛用于物联网节点、智能家居与教学实验等场景。资源源自《LPC1114芯片基础教程与应用实践》,覆盖GPIO按键中断、UART串口通信、ADC模拟采集、PWM输出、NTC测温、RGB灯控制、旋转编码器等常见模块,并展示GPS、Flash、SPILCD等综合项目,适合对照学习裸机开发和工程搭建流程。包内共692个文件,含大量c/h源文件、uvproj/uvopt等Keil工程配置、axf/hex烧录文件及lst/map编译输出,另有txt说明、htm文档、PDF教程和示例图片,便于边看边练。压缩包仅6.61MB,整体轻量精简,适合快速阅读与工程复用,已有391人学习下载。通过阅读教程文档并结合工程例程,可快速掌握LPC1114的初始化、时钟配置、中断处理和串口通信等核心技能,也能以完整工程为模板迁移到实际开发中,并据此扩展更多功能。
1. LPC1114 例程与教程:先别抄代码,确认你在给谁写程序
LPC1114 的例程和 STM32 那类 HAL 库风格完全不同,很多从 GitHub 上下到的 LPC1114 教程代码,打开后发现全是寄存器操作,连外设结构体都要自己对着手册填。这颗 Cortex-M0 芯片主频 50MHz、Flash 和 RAM 都不大,但它在工控小模块、传感器采集和低成本串口设备里仍然大量服役,尤其适合那些不需要跑协议栈、只要 GPIO 加 UART 就能交付的场景。这篇内容按“先看懂例程结构,再搭最小工程,接着调 UART,最后用定时器反向验证时钟”的顺序展开,目标是让你拿到任意一份 LPC1114 例程都能在三十分钟内定位到 main 和外设初始化,并且照着改出自己能用的版本。
2. LPC1114 例程包里有什么:从启动文件到 main 的调用链
看例程,先看目录。一份完整的裸机工程通常由四个文件组成:启动文件、时钟初始化文件、外设头文件和应用程序。以 Keil 工程为例,编译入口是启动文件,它负责建立中断向量表、初始化堆栈,然后调用 SystemInit(),再跳进 main()。很多人把精力全放在 main 里的业务代码,结果 UART 收发不对、延时不对,最后发现是启动文件和时钟没配对。
2.1 例程文件分工与常见命名
| 文件名 | 作用 | 改错之后的表现 |
|---|---|---|
| startup_LPC111x.s | 中断向量表、堆栈初值和 Reset_Handler | 编译能过,进不了 main,或中断服务函数不生效 |
| system_LPC11xx.c | SystemInit()、SystemCoreClock 变量 | 延时比例全错,UART 波特率全错 |
| LPC11xx.h | 芯片寄存器结构体和地址映射 | 寄存器名对不上,光标悬停没有类型信息 |
| main.c / 外设驱动 | 业务逻辑和外设初始化 | 直接看现象,表现各异 |
拿到例程先建立一个习惯:把 system_LPC11xx.c 里的 SystemInit() 和 SystemCoreClock 的赋值看成整份代码的“原点”。例程里所有基于时间的操作,SysTick、UART、定时器,都默认用了这个全局变量。如果 SystemCoreClock 写的是 48000000,而实际外部晶振只有 12MHz 且 PLL 没有真正把频率提上去,那么点灯延时和串口波特率就会变成另一个值。
2.2 时钟树是 LPC1114 例程的第一个分水岭
LPC1114 内部有一个 12MHz IRC 振荡器,很多例程因为板子小、走线短,干脆直接用 IRC,这也是一份例程能跑但串口 115200 乱码的根本原因——12MHz 时钟做 115200 波特率会产生接近 8.5% 的误差。例程里如果出现LPC_SYSCON->SYSPLLCTRL、SYSPLLCLKSEL这类寄存器,说明作者打算启用 PLL,把核时钟拉高到 48MHz 或 50MHz,这时需要结合板载晶振计算倍频分频比。
还有一个非常值得留意的寄存器:LPC_SYSCON->SYSAHBCLKDIV。这个分频器决定 CPU 和外设总线最终拿到多少频率。很多教程只写了 PLL 设置,却忽略了 SYSAHBCLKDIV 仍保留在默认除法链上,导致 SystemCoreClock 和实际 CPU 频率对不上。遇到例程里“改了波特率计算值但是串口依然乱码”的问题,先查这条链路,不要一上来就怀疑芯片坏了。
2.3 用 Git 管理例程而不是靠文件名区分版本
我一般在 GitHub 上搜索 LPC1114 例程时会先看仓库的提交记录,NXP 的老例子散在早期 LPCXpresso 项目里,很多作者把寄存器定义升级过。下载回来之后,用 git 初始化一次,把所有原始文件作为第一个提交,之后每改一个外设就单独提交一次。这样一旦把某个寄存器改坏,git diff能直接看到改动点,不至于对着三份相似的 main.c 找哪份才是能用的。这里不涉及复杂的 git 用法,只需要git init、git add、git commit三个命令,配合git diff看差异即可。
注意:如果你的 Keil5 还没装 LPC1100 系列的 Device Family Pack,后面无论打开哪个例程,Device 下拉框里都找不到 LPC1114,先回安装流程把 pack 补上。
3. 用 Keil5 创建 LPC1114 最小工程:GPIO 点灯与 SysTick 延时
不要直接照抄网上的工程文件,我建议还是自己建一次最小工程,这样后边换芯片型号、换封装、换外部晶振时才知道哪里需要动。Keil5 新建工程时,在 Device 搜索框输入 LPC1114,选择 LPC1114FBD48/302 或者其他和你手中芯片后缀一致的型号,然后从 Pack 里勾选 CMSIS_CORE 和 Device:Startup。如果找不到这两个组件,说明 Pack 没有装对。启动文件在 Keil 里通常以 Startup 形式出现,它会自动匹配芯片的向量表。
3.1 最小工程必须要有的三个文件
工程建立之后,至少保证项目里有startup_LPC111x.s、system_LPC11xx.c和main.c。Startup 文件负责把所有异常和中断的服务函数符号名暴露出来:比如SysTick_Handler、UART_IRQHandler、TIMER32_0_IRQHandler,你写的 C 函数如果名字和向量表里不一致,就会静默地跑到死循环里,没有任何编译报错。这也是例程教程里最隐蔽的一个坑。
3.2 GPIO 点灯代码:先把最小闭环跑起来
以下代码按 CMSIS 头文件LPC11xx.h的寄存器命名来写,用 PIO1_10 做示例输出脚,实际引脚根据自己的板子修改。
#include "LPC11xx.h" // 简单的毫秒级延时,使用 SysTick void delay_ms(uint32_t ms) { SysTick->LOAD = SystemCoreClock / 1000 - 1; // 1ms 需要的倒数周期数 SysTick->VAL = 0; SysTick->CTRL = 0x05; // 使能 SysTick,时钟源选处理器时钟 while (ms) { if (SysTick->CTRL & (1 << 16)) { // COUNTFLAG 置位表示倒计数到零 ms--; } } SysTick->CTRL = 0; // 关闭,避免干扰后续外设 } int main(void) { // 打开 GPIO 模块的 AHB 时钟门控,bit6 对应 GPIO LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 6); // 把 PIO1_10 配置为输出,方向寄存器 bit10 写 1 LPC_GPIO->DIR |= (1 << 10); while (1) { LPC_GPIO->DATA |= (1 << 10); // 输出高 delay_ms(200); LPC_GPIO->DATA &= ~(1 << 10); // 输出低 delay_ms(200); } }代码逻辑很简单,但有两个参数值得强调。SystemCoreClock不是编译期宏,它来自 system_LPC11xx.c,启动阶段由 SystemInit() 赋值;如果外部晶振或 PLL 配置变了,必须同步修改这个全局变量,否则 LOAD 算出来的时间就是错的。LPC_GPIO->DATA是整组 IO 口的读写窗口,对单个 bit 做读改写,如果并发中断里也在动同一个 IO 口,会出现丢位问题;常见做法是把要改的引脚掩码放入写路径,或用预计算的整组数据一次性写入 DATA。
3.3 编译烧录的成功标准与常见失败
下载前检查 Target Options 里的 Debug 页,选择实际使用的下载器。CMSIS-DAP 或 J-Link 都支持 SWD,LPC1114 的 SWD 引脚一般在 PIO0_5 和 PIO0_6 上。如果点击 Load 后报Flash Download failed,先看 Flash 算法列表里有没有 LPC1114 32KB Flash 对应的算法。这个算法在早期 Keil 里经常需要手动 Add。
另一个更隐蔽的情况是低功耗模式下 SWD 被关闭,或者复位引脚被复用成 GPIO,这时要先把复位线接上,或者在 Options 里把 Reset and Run 取消,用“按住复位再点下载”的方式拉回可下载状态。烧录成功后不要急着调串口,先确认灯在闪,而且单次闪动周期大约 400ms(亮 200ms 灭 200ms)。如果灯常亮或完全不闪,优先用调试器在 SystemInit() 和 main() 入口打断点,确认复位后是否走到用户程序。这个闭环建立起来,后面所有外设例程都可以在这个工程上叠加。
4. LPC1114 串口例程:UART 初始化、波特率误差与中断收发
点灯通过之后,一般第二个需求就是串口。LPC1114 的 UART 是 16C550 兼容设计,寄存器风格和 STM32 的 USART 很不一样,很多人照 STM32 的思路去找 BRR 寄存器,结果发现根本没有。这里只有 DLL/DLM、THR、RBR、LSR、IIR 这一套老式寄存器。先记住一个原则:UART 的时钟门控、引脚复用、波特率计算三个步骤,按顺序做。
4.1 初始化顺序里最容易漏掉的时钟门控
UART 模块的时钟由SYSAHBCLKCTRL控制,bit11 通常对应 UART。如果不先打开这个位,后面的寄存器写入全部不生效,现象是波特率寄存器写进去读回来还是 0。打开时钟之后,还需要通过LPC_IOCON配置引脚复用功能。LPC1114 例程中最常见的串口引脚是 PIO1_6 和 PIO1_7,FUNC 位写 1 时分别映射到 UART_RXD 和 UART_TXD。如果板子的 USB 转串口线接的是 PIO1_6/7,就直接可以连;如果板子用的是下载口复用的 UART,要去查原理图确认引脚编号。
4.2 波特率计算的三个数值
UART 波特率公式是 PCLK / (16 * baud),PCLK 来自主时钟再除以UARTCLKDIV。这个公式里的 16 是 16C550 的过采样系数,不能省。LPC1114 的系统时钟主要就两种场景:12MHz IRC 直跑,以及 48MHz PLL 后的总线时钟。差异非常明显:
| 系统时钟 | 目标波特率 | 分频计算值 | 取整数 | 实际波特率 | 误差 |
|---|---|---|---|---|---|
| 12 MHz | 9600 | 78.125 | 78 | 9615 | 0.16% |
| 12 MHz | 115200 | 6.51 | 6 | 125000 | 8.5% |
| 48 MHz | 9600 | 312.5 | 312 | 9615 | 0.16% |
| 48 MHz | 115200 | 26.04 | 26 | 115384 | 0.16% |
这就是为什么 LPC1114 老例程默认用 9600:在 12MHz IRC 下 115200 的误差大到无法握手。如果你拿到一份教程代码默认 115200,先确认它是否启用了 PLL 并让 SystemCoreClock 走到 48MHz;如果内部 IRC 直跑,改回 9600 是更稳妥的做法。
4.3 可抄的 UART 中断回显例程
下面代码在上一章点灯工程的基础上,增加一个串口接收中断,收到一个字节后原样回发。发送路径采用轮询 THRE 标志,接收路径走 UART_IRQHandler。完整初始化函数如下:
void uart_init(uint32_t baud) { uint32_t div; // 1. 打开 UART 和外设引脚的时钟,bit11 是 UART LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 11); LPC_SYSCON->UARTCLKDIV = 1; // 不分频 // 2. 引脚复用 LPC_IOCON->PIO1_6 = 0x01; // RXD LPC_IOCON->PIO1_7 = 0x01; // TXD // 3. 写波特率:先使能 DLAB,再写 DLL/DLM,最后关 DLAB LPC_UART->LCR = 0x83; // 8 位数据,1 停止位,DLAB=1 div = (SystemCoreClock / LPC_SYSCON->UARTCLKDIV) / (16 * baud); LPC_UART->DLL = div & 0xFF; LPC_UART->DLM = (div >> 8) & 0xFF; LPC_UART->LCR = 0x03; // 关 DLAB,线控制恢复 8N1 // 4. FIFO 和中断使能 LPC_UART->FCR = 0x07; // 使能 FIFO,复位收发 FIFO LPC_UART->IER = 0x01; // 允许接收数据可用中断 NVIC_EnableIRQ(UART_IRQn); // LPC1114 的 UART 中断号 } void uart_putc(uint8_t c) { while (!(LPC_UART->LSR & (1 << 5))); // 等待 THR 为空 LPC_UART->THR = c; } void UART_IRQHandler(void) { uint8_t ch; if ((LPC_UART->IIR & 0x06) == 0x04) { // IIR 位 3:1 表示 RDA ch = LPC_UART->RBR; // 读数据同时清接收中断 uart_putc(ch); // 回发 } }有几个参数要特别说明。LPC_UART->LCR低三位不是使能位,而是数据长度编码,0x03 表示 8 位数据;写波特率时必须让 DLAB=1,写完后一定要关掉 DLAB,否则后续 THR 会写不进去。LPC_UART->IER的 bit0 是接收数据可用中断,如果以后要开发送完成中断,再置 bit1。
回显函数里的uart_putc是阻塞的,字符量不大时没问题,但如果要做大量发送,建议先做一版环形缓冲区再接中断写出。还有一个例程移植中的差异点:不同版本的 LPC11xx.h 对 UART 中断号的命名可能不同,有的版本把弱实现写成默认别名。遇到“编译通过但串口进不了中断”的情况,第一步去启动文件里找UART_IRQHandler这个名字,确认和你的函数名完全一致。
5. 用 32 位定时器例程验证时钟:500Hz 方波该不该信
前面几章都在“用例程”,这一章讲“反推验证”的办法。很多 LPC1114 教程里都有定时器例程,但直接把MR0填一个固定常量如 5000,这在不同的 SystemCoreClock 下结果完全不一样。真正可移植的写法是让MR0跟着 SystemCoreClock 走,然后用外部测量结果反过来确认这个系统时钟变量是否和数据手册一致。
5.1 让定时器输出 1ms 中断并翻转 GPIO
void TIMER32_0_IRQHandler(void) { if (LPC_TIMER0->IR & 1) { LPC_TIMER0->IR = 1; // 写 1 清除中断标志 LPC_GPIO->DATA ^= (1 << 10); // 翻转 LED 引脚 } } void timer32_0_init(void) { // 打开定时器模块的 AHB 时钟,不同头文件里可能是 TIMER0 或 TMR32B0 LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 14); LPC_TIMER0->TCR = 0; // 先停止定时器 LPC_TIMER0->PR = 0; // 预分频为 0,直接数系统时钟 LPC_TIMER0->MR0 = SystemCoreClock / 1000 - 1; // 1ms 匹配值,注意减 1 LPC_TIMER0->MCR = 3; // MR0 中断使能 + 计数归零 NVIC_EnableIRQ(TIMER32_0_IRQn); LPC_TIMER0->TCR = 1; // 启动 }这里MR0减 1 是很多例程和教程容易踩的小坑。LPC1114 的定时器从 0 递增到 MR0 触发,匹配后的周期是 MR0 + 1 个时钟。如果直接写SystemCoreClock / 1000,最终频率会差大约 0.1%,点亮 LED 看不出来,但用示波器量频就能看到明显的系统误差。
LED 引脚每进一次中断翻转一次,所以输出的是频率为 500Hz 的方波。用示波器或逻辑分析仪测量 PIO1_10,如果量到 500Hz ± 1Hz 以内,说明 SystemCoreClock、PLL 和定时器预分频三者是自洽的;如果量到 480Hz 甚至更低,先回去查 system_LPC11xx.c 里的时钟主频赋值,再查外部晶振频率是否和预计一致。
更进一步的验证做法是让 SysTick 和 32 位定时器同时运行,SysTick 翻转一个引脚,定时器翻转另一个引脚,观察两个边沿的偏差。偏差纹丝不动说明两边用的是同一个时钟源;偏差随时间漂移,则板子上大概率存在外部晶振偏频或者 PLL 锁定异常。值得再试的是把PR从 0 改成 99,MR0保持SystemCoreClock / 1000 / 100 - 1,这样你能得到一个 10kHz 的干净时基,后续做软件 PWM 或脉冲计数都会更方便。
本文还有配套的精品资源,点击获取