简介:这是一套面向LPC1114嵌入式开发的完整例程与教程资料包,基于NXP Cortex-M0内核微控制器,覆盖从基础GPIO到常用外设的工程实例,适合刚接触Cortex-M0的初学者,也可作为项目移植和驱动调试的参考。压缩包共692个文件,大小6.61MB,主要包含c/h源码、uvproj/uvopt工程配置、axf/hex/lst编译输出,以及o/d/crf中间文件,另有txt/htm说明文档和PDF/PNG资料,目录按例程划分,便于按需查阅。目前已有391人学习/下载。资料内含ADC、PWM、RGB、UART、SPILCD、GPS、旋转编码器、KEY_INT等实测例程,覆盖按键中断、串口通信、ADC采集、PWM输出、LCD显示、GPS解析等多类应用场景,并配有基础教程,可帮助快速理解芯片配置流程与程序框架,减少重复开发成本。
1. LPC1114 例程和教程的本质:为什么一堆代码不如一套流程
LPC1114 例程和教程在网上能搜出一堆,从跑马灯到 IAP 升级都有,但大部分人在拿到例程包的前两小时就卡住了:不是看不懂 main.c,而是不知道先装哪个编译环境、Flash 里的 ISP 引脚怎么拉、为什么例程下载进去后串口全是乱码。LPC1114 是 NXP 基于 Cortex-M0 的低成本 MCU,主频最高 50MHz,Flash 从 8KB 到 32KB 不等,定位就是替代 8 位机的主力。它的例程更像是一套「初始化流程示范」而不是功能成品:芯片上电时先跑内部 IRC,要进自己的外设配置,必须先过 SystemInit、引脚复用和时钟分配这三关。这组内容适合刚拿到开发板想跑通 UART 或 PWM 的入门者,也适合需要把寄存器操作改成 CMSIS 风格的老工程师。
2. 搭建 LPC1114 例程的最小开发环境:Keil、Flash Magic 和串口
2.1 从零到能编译:Keil MDK 与 Device Family Pack
LPC1114 例程最常见的编译环境是 Keil MDK,因为它的设备库和调试器支持最省事。Keil MDK 的安装教程网上已经很多,这里只提醒一点:不要拿评估版和社区版混着去编译老工程,LPC1114 的 Device Family Pack 要从 Pack Installer 里选 NXP LPC1100 系列安装。安装完成后还有一个编译器兼容问题:老例程可能用 ARMCC 4.x/5.x 编写,新版 MDK 默认不带 ARMCC 5,需要单独安装 legacy pack,否则打开工程会报Target uses ARM-Compiler错误。这一步不处理,后面所有例程都无法编译。
工程能打开后先看三处。Options for Target -> Device要选具体型号,比如 LPC1114FBD48/302,没有选到具体型号时外设寄存器地址是错的。C/C++ 选项卡的 Define里通常要有USE_STDPERIPH_DRIVER, LPC1114,老工程是LPC11XX,要核对例程头文件里的条件编译。Utilities -> Settings里选择 Flash 下载算法,16KB 和 32KB 必须对应实际芯片,选错会报No Algorithm found。
把所有源码拖进工程时还要注意:LPC1114 例程依赖core_cm0.h和system_LPC11xx.c,这两个文件只能各保留一份。常见错误是在项目里同时放了CMSIS/CM0/DeviceSupport和LPC11xx_Lib中两份相同的 core 文件,编译后会出现多重定义或类型冲突。
2.2 LPC1114 例程的标准目录结构怎么认
一个典型的 LPC1114 例程包,无论来自 NXP 还是板卡商,目录结构基本是这几类:
| 目录/文件 | 作用 | 改的时候注意什么 |
|---|---|---|
app/main.c | 入口和应用层代码 | 大部分演示逻辑在这里,移植时先读这里 |
board/ | 板级初始化、GPIO 引脚映射、LED/按键定义 | 换了板子第一个要改的地方 |
driver/ | 外设驱动封装,如 UART、I2C、ADC | 可能引用chip.h或标准外设库 |
lib或cmsis/ | CMSIS core、启动文件、system 文件 | 一般不需要动,但必须存在 |
project/或.uvprojx | Keil 工程文件 | 路径用相对路径,不要带绝对路径 |
常见的误操作是直接改main.c里的寄存器,却忽略了板级文件里已经对引脚做了复用配置。LPC1114 的引脚功能由 IOCON 寄存器控制,同一个引脚可以是 GPIO、UART TX 或比较器输入。例程能在原板上跑,换到自己的板子上就不行,90% 的原因是把board/忽略了。所以拿到例程包之后,先编译完跑一次原程序,再改板级文件,而不是上来就删 main。
2.3 使用 Flash Magic 把例程烧进片内 Flash
编译通过之后,最常见的烧录方式是通过 UART 的 ISP 功能,工具用 Flash Magic。先把开发板上的 PIO0_1 拉低进入 ISP 模式,然后复位上电,USB 转串口接到 UART0。ISP 模式下的管脚分配如下:
| 功能 | 引脚 | 电平要求 |
|---|---|---|
| ISP 进入选择 | PIO0_1 | 复位上升沿时保持低电平 |
| UART0 TX | PIO1_7 | 芯片输出,接 USB 转串口 RX |
| UART0 RX | PIO1_6 | 芯片输入,接 USB 转串口 TX |
| 复位 | RESET | 上电时先拉低再释放 |
烧录时在 Flash Magic 里选好串口号,波特率不要一上来选 115200,老式 USB 转串口在低电压下不稳,先选 9600 握手成功后再调高。Hex 文件路径要选编译输出目录下的,不要选临时目录里生成的。关键参数说明:ISP 模式下时钟是内部 IRC,波特率误差受 IRC 精度影响,所以 9600 起步最稳。烧进去之后把 PIO0_1 拉回高电平再复位,程序才会从 Flash 正常启动。如果看到Cannot connect. Check COM port and restart target,先检查目标板是否真的复位了、PIO0_1 是否拉低,而不是去改波特率。
3. 解剖一个 LPC1114 例程:从 system_init 到 while(1) 的实线
3.1 时钟树:IRC 12MHz 倍频到 50MHz 的完整配置
LPC1114 上电后不是直接把主频跑到 50MHz,而是先停在内部 IRC 12MHz。几乎所有官方例程的SystemInit里都会做这样三步:设置SYSAHBCLKCTRL给外设模块开时钟,配置MAINCLKSEL选择 PLL 输出或 IRC 作为主时钟,最后通过SYSPLLCLKSEL和SYSPLLCTRL设置倍频。一个典型的 50MHz 主频配置片段如下:
/* LPC1114: 外部晶振 12MHz, 倍频到 50MHz */ void ClockInit_50MHz(void) { /* 1. 选择外部晶振作为 PLL 输入 */ LPC_SYSCON->SYSPLLCLKSEL = 0x01; /* 0=IRC, 1=外部晶振 */ LPC_SYSCON->SYSPLLCLKUEN = 0x01; /* 更新时钟源选择 */ LPC_SYSCON->SYSPLLCLKUEN = 0x00; /* 先写 0 再写 1 触发更新 */ LPC_SYSCON->SYSPLLCLKUEN = 0x01; /* 2. 配置 PLL: M=4, P=2, 输出 = 12MHz * (M+1) / P */ LPC_SYSCON->SYSPLLCTRL = (0x04 << 0) | (0x01 << 5); /* MSEL=4, PSEL=1 => P=2 */ LPC_SYSCON->PDRUNCFG &= ~(0x1 << 7); /* 给 PLL 供电 */ while (!(LPC_SYSCON->SYSPLLSTAT & 0x01)); /* 等 PLL 锁定 */ /* 3. 切换主时钟到 PLL */ LPC_SYSCON->MAINCLKSEL = 0x03; /* 0=IRC, 1=PLL, 3=外部晶振 */ LPC_SYSCON->MAINCLKUEN = 0x01; LPC_SYSCON->MAINCLKUEN = 0x00; LPC_SYSCON->MAINCLKUEN = 0x01; /* 4. 使能外设时钟 */ LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 6); /* 使能 GPIO 时钟 */ }这段代码里的注释特意标出了容易写错的地方。PLL 输出公式是Fclk = Fclkin * M / P,其中M = MSEL + 1,P = 2^PSEL。上面配置 MSEL=4、PSEL=1 时,M=5、P=2,12MHz * 5 / 2 = 30MHz,并不是 50MHz。要把 LPC1114 跑在 50MHz,需要外部晶振 10MHz 配合 M=5、P=1 才能不超上限。很多开发板板载 12MHz 晶振,所以例程主频实际是 48MHz 或 30MHz。建议不要盲目抄 50MHz 参数,先读原理图确认晶振频率。
参数说明:SYSPLLCTRL的 bit0-4 是 MSEL,bit5-6 是 PSEL。PDRUNCFG的 bit7 是 PLL 电源控制,为 1 时 PLL 关闭,必须清零才能锁定。如果主频不对,最直接的表现是串口波特率全乱,因为 UART 的时钟源也来自系统时钟树。
3.2 GPIO 例程:读引脚、写引脚、开漏模式与去抖哪个先做
GPIO 例程往往第一个演示 LED 闪烁,但它的细节比一个LPC_GPIO->DATA = 1多得多。LPC1114 的 GPIO 操作分两部分:IOCON寄存器决定引脚功能为上拉、下拉、开漏还是普通输入输出,GPIO寄存器的 DIR 决定方向。完整的初始化顺序如下:
void GPIO_Init_LED(void) { /* PIO0_11 作为 GPIO 输出,接 LED 低电平点亮 */ LPC_IOCON->PIO0_11 = 0x10; /* 0x10 表示禁止上拉/下拉,纯 GPIO */ LPC_GPIO->DIR[0] |= (1 << 11); /* 输出方向 */ LPC_GPIO->DATA[0] |= (1 << 11); /* 初始拉高,LED 灭 */ } /* 读取按键输入,返回 1 表示按下 */ uint8_t ReadKey(void) { return (LPC_GPIO->DATA[0] >> 6) & 0x01; /* 假设按键接 PIO0_6 */ }这个例程里最容易忽视的是IOCON的上拉设置。按键输入场景,如果不把 IOCON 配成内部上拉,按键悬空时会读到随机电平。抖动也是常见问题,常见做法是硬件上并一个 100nF 电容,软件里做 10ms 去抖延时。顺序上「先读、延时、再读」比「先去抖、再读」可靠,因为去抖的本质是等待信号稳定,不是过滤信号。
还要注意 LPC1114 的DIR是按端口数组访问的:LPC_GPIO->DATA[0]是整组端口数据,支持位带操作,但位带别名地址在例程里通常被封装成GPIO_SetValue()。从例程学习时,先分清底层封装层和应用层调用,再决定改哪里。
3.3 用 SysTick 做时间基准,而不是傻等 delay
很多例程的延时是for循环空转,这个在调好主频后也能用,但不适合做多个任务的时序基准。LPC1114 的 SysTick 是 Cortex-M0 自带的 24 位递减计数器,用它做时基不占用定时器外设。例程里最常见的封装如下:
static volatile uint32_t tick_count = 0; void SysTick_Handler(void) { tick_count++; } void DelayMs(uint32_t ms) { uint32_t start = tick_count; while (tick_count - start < ms) { } } void SysTick_Init(uint32_t freq_hz) { SysTick_Config(SystemCoreClock / freq_hz); /* 每 tick 中断一次 */ }逻辑说明:SysTick_Config传入的是重装载值,SystemCoreClock / 1000就是 1ms 中断一次。tick_count必须用volatile,否则编译器会优化掉读取。用tick_count - start计算差值,可以避免取模回绕问题,在计数溢出后仍然正确。
需要注意的是SysTick_Handler是中断服务函数,在 Keil 工程里函数名必须严格一致,否则中断向量表里找不到入口,程序会跑飞。中断优先级默认是最高,如果例程里同时使用定时器中断,建议把 SysTick 优先级调低,避免时间基准抢占 UART 收发。
4. 把 LPC1114 的 UART、定时器和 ADC 例程改造成自己的项目:关键参数与坑
4.1 UART 波特率误差怎么算,校验位不生效的常见原因
例程包里 UART 示例通常是一收一发,移植到自己项目时最容易出错的是波特率计算。UART 时钟源默认是UARTCLKDIV分频后的系统时钟,波特率寄存器由DLM/DLL和FDR配合计算。公式是:UART_CLK = 系统主频 / UARTCLKDIV,DLM:DLL = UART_CLK / (16 * 目标波特率),余数用FDR做分数分频。
void UART_Init(uint32_t baudrate) { uint32_t uart_clk = SystemCoreClock / 1; /* 1 分频 */ uint32_t dl = uart_clk / (16 * baudrate); float actual = (float)uart_clk / (16 * dl); float error = (actual - baudrate) / baudrate * 100.0f; LPC_SYSCON->UARTCLKDIV = 1; LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 12); /* 打开 UART 时钟 */ LPC_UART->LCR = 0x83; /* 8 位数据,1 停止位,使能 DLAB */ LPC_UART->DLL = dl & 0xFF; LPC_UART->DLM = (dl >> 8) & 0xFF; LPC_UART->FDR = 0x10; /* 分数分频,默认值 */ LPC_UART->LCR = 0x03; /* 关 DLAB */ if (error > 1.0f || error < -1.0f) { /* 超过 1% 通常会造成误码,需要换晶振或调整分频 */ } }参数说明:LCR的 bit7 是 DLAB,写波特率时必须置 1,写完要清零。校验位不生效的常见原因是LCR的第三位和第四位设置不对:0x03表示 8 位无校验,要加偶校验应该是0x1B(0x03 | 0x18),很多例程只改了一个 bit 导致数据位多了一位。调试时优先看示波器的波形电平而不是怀疑芯片。
4.2 定时器匹配与捕获:PWM 例程里边沿对齐和中心对齐的差异
LPC1114 的 32 位定时器有匹配和捕获两种工作方式。PWM 例程多用匹配寄存器产生周期和占空比。边沿对齐和中心对齐的差别在于计数方向:边沿对齐是计数器从 0 数到MR0然后清零,中心对齐是先增到MR0再减到 0。中心对齐的 PWM 会导致中断频率加倍,占空比最小脉宽更宽,适合电机控制减少冲击。
void PWM_Init(void) { LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 10); /* 定时器0时钟 */ LPC_IOCON->PIO1_6 = 0x02; /* 引脚复用为 CT32B0_MAT0 */ LPC_SYSCON->PINTSEL &= ~(0x03 << 0); /* 关闭引脚中断,避免干扰 */ LPC_TIMER0->MCR = (1 << 0) | (1 << 1); /* MR0 中断并复位 */ LPC_TIMER0->MR0 = 1000; /* 周期 1000 计数 */ LPC_TIMER0->MR1 = 250; /* 25% 占空比 */ LPC_TIMER0->PWMC = 1; /* 使能 PWM 模式 */ LPC_TIMER0->TCR = 1; /* 启动计数 */ }坑在于PWMC寄存器不是通用的 PWM 总开关,而是每个匹配通道一个 bit:PWMC的 bit0 对应 MR0,bit1 对应 MR1。照抄例程把PWMC=1当成使能 PWM,实际只使能了 MR0,PWM 输出根本不是 MR1。正确写法是LPC_TIMER0->PWMC = (1 << 1)。另一个坑:引脚复用为 CT32B0_MAT0 后,原来的 GPIO 配置立即失效,所以初始化顺序必须是 IOCON 先于 PWM 配置。
4.3 ADC 例程:参考电压、采样时间和偏移校准
ADC 例程通常配置为 10 位转换,参考电压默认是VDD(3.3V)。有的板子用内部参考,电压不完全等于 VDD,读出的值就会整体偏移。转换结果计算公式是code / 1023 * Vref,但实际要先减去偏移。
void ADC_Init(void) { LPC_SYSCON->PDRUNCFG &= ~(0x1 << 4); /* 打开 ADC 电源 */ LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 13); /* 开启 ADC 时钟 */ LPC_IOCON->PIO1_0 = 0x02; /* AD0 输入 */ LPC_ADC->CR = (1 << 0) | /* 选择通道0 */ (4 << 8) | /* CLKDIV = 4,分频后的ADC时钟 */ (1 << 16) | /* 转换次数 1 次 */ (1 << 24); /* ADGINTEN 全局中断使能 */ } uint16_t ADC_Read(void) { LPC_ADC->CR |= (1 << 24); /* 软件触发转换 */ while (!(LPC_ADC->GDR & (1 << 31))); /* 等待 DONE */ return (LPC_ADC->GDR >> 6) & 0x3FF; /* 取 10 位结果 */ }对照下面这个表排查异常:
| 现象 | 可能原因 | 检查方法 |
|---|---|---|
| 所有通道读值都偏大 | 参考电压不是 VDD | 用万用表量 VREF 引脚 |
| 转换值跳变超过 10 LSB | 采样时间太短 | 调大CLKDIV或增加采样周期 |
| 读固定电压但值周期性波动 | 电源噪声 | 在 VREF 和 VSS 之间并电容 |
采样时间这个参数最容易被忽略:LPC1114 的 ADC 要求采样时间至少 600ns 左右,如果系统时钟 30MHz 而CLKDIV=0,转换时钟过快,采样电容充不满,高位抖动就大。例程里给的参数是为了演示速度,实际项目要按目标板主频重新算。
5. 例程移植到 LPC1114 新板子的验证技巧
5.1 用逻辑分析仪看 GPIO 翻转频率,轻量验证时钟配置
不管从哪种教程里复制过来的工程,烧进新板后第一件事是验证系统时钟而不是测试 UART。方法是让一个 GPIO 翻转,并将翻转周期和预期主频比对。比如翻转周期是 1ms,那么误差应该在 1% 以内。如果发现频率是预期的整数分之一,大概率是 PLL 配置里的PSEL或MSEL算错了。逻辑分析仪或示波器只需一根探头,比串口助手更直观。
5.2 例程里最容易被改坏的三处:中断优先级、引脚复用、电源去耦
从例程学习时,常备一份勘误清单会省很多时间。第一处是中断优先级:Cortex-M0 只有 4 级优先级(0-3),例程里可能使用NVIC_SetPriority(SysTick_IRQn, 0),如果照搬到有多个中断的项目中,把低优先级外设中断也设成 0,会导致 UART 丢字节。第二处是引脚复用:只要改了IOCON,原本 GPIO 功能立即失效,排查硬件无输出时要先看IOCON。第三处是电源去耦,LPC1114 的 VDD 和 VDDA 必须分别加 100nF 和 10uF 电容,否则 ADC 和 LDO 都容易出问题,这属于硬件问题但反映到软件上是随机复位。
5.3 从例程到产品:一个最小状态机的写法
例程代码大多用while(1)加轮询,产品中建议用状态机方式组织。举一个按键控制的例子:
typedef enum { IDLE, DEBOUNCE, RUN } AppState; void App_Task(void) { static AppState state = IDLE; static uint32_t t = 0; switch (state) { case IDLE: if (ReadKey() == 1) { t = GetTick(); state = DEBOUNCE; } break; case DEBOUNCE: if (GetTick() - t > 10) { if (ReadKey() == 1) { ToggleLed(); state = RUN; } else { state = IDLE; } } break; case RUN: break; } }这个模式比直接跳转可靠,因为每个状态都有明确的进入条件和退出条件。LPC1114 资源很小,状态机不会增加多少 Flash 占用,却能让多个例程的初始化代码自然地分离。把例程库里的外设初始化和使用逻辑分开放,基本就是自己产品代码的雏形。
本文还有配套的精品资源,点击获取