简介:面向LPC2138/ARM7初学者的一套入门级Keil工程资料,围绕NXP LPC2138的启动流程、内存映射与基础外设编程展开,适合刚接触ARM7或准备课程设计的学生快速建立整体认识。压缩包共32个文件、约62KB,除C源码与头文件外,还包含启动汇编代码、分散加载脚本以及Keil工程配置文件;同时保留编译过程中生成的axf、hex、map、lst等文件,方便对照理解从源代码到可烧录固件的完整链路。资源内涉及的GPIO、串口、中断、定时器、ADC、PWM等模块知识点,均可直接对应芯片手册中的寄存器操作,阅读时配合项目内注释与文件结构可以少走弯路。学习时可按启动代码、外设配置、中断处理的顺序循序渐进。已有392人下载学习,对于想通过实例而非纯理论入门LPC2138的初学者,是一份小巧而完整的起步材料。
1. LPC2138学习入门:一片ARM7为什么到现在还值得折腾
很多人第一次接触LPC2138是在课程设计里,做完一个电子钟或者小车后就把板子收进了抽屉。但如果你愿意停一下,这片芯片其实是一个极好的32位入门样本:ARM7TDMI-S内核,512KB Flash和64KB RAM,寄存器直接到能一眼看穿地址映射,没有Cache、没有MMU、没有复杂的总线矩阵,出问题的可能性比现在自动化程度很高的工具链“帮”你处理掉的世界少得多。入门它,你能真正搞明白中断向量、PLL、引脚复用这些做嵌入式永远躲不开的底料。这里我按自己带A同学做模拟项目X时的老流程,把选型、最小系统、工程搭建、高频翻车点完整走一遍。适合从51单片机升级到32位的开发者,以及想弄懂底层时钟与中断的学生。
2. 选片与最小系统设计:先把手上的板子跑对再谈代码
2.1 型号别买错:LPC213x家族里为什么单独挑LPC2138
LPC213x是一个系列,不是一颗芯片。LPC2131、LPC2132、LPC2134、LPC2136、LPC2138共用同一个内核和外设框架,差异主要在Flash和RAM容量上。很多开发板标注“LPC2138开发板”,但实际焊的是LPC2132,因为后者更便宜。入门期这不是大问题,可一旦你想跑稍复杂一点的状态机或者多存几帧调试数据,RAM不够就会让你反复删代码。
| 型号 | Flash | RAM | 典型场景 |
|---|---|---|---|
| LPC2131 | 32KB | 8KB | 简单IO控制、小协议解析 |
| LPC2132 | 64KB | 16KB | 课程设计常见配置 |
| LPC2134 | 128KB | 16KB | 带少量通信协议 |
| LPC2136 | 256KB | 32KB | 多路ADC采集 |
| LPC2138 | 512KB | 64KB | 学习、复杂应用、存字库 |
所有型号的主频上限都受PLL配置约束,外设数量也相同:两个UART、一个I2C、SPI、8路10位ADC、1路DAC、6路PWM、两个32位定时器、RTC和看门狗。选LPC2138的核心理由只有一个:64KB RAM意味着你在学习阶段几乎不需要考虑优化内存,可以用最直白的写法把功能跑通,先把体系结构学会,再回头学优化。另一个理由是利用大Flash把启动自检、串口调试、字库表一起放进去,这块板子学完了还能改造成一个实用调试工具。
2.2 最小系统设计的核心:电源、晶振、复位三件套
LPC2138是3.3V单电源供电,模拟和数字电源分开,板上至少要有3.3V稳压芯片。常见的错误是只关心数字VDD,把VDDA直接和VDD焊在一起,导致ADC读数跳得没法用。我一般的做法:VDDA和VSSA之间放一个10µF钽电容加一个0.1µF陶瓷电容,模拟参考电压引脚单独用磁珠从3.3V引过来,这样ADC数据能稳定很多。
晶振用12MHz无源晶体,配合两个15~22pF负载电容,接到XTAL1和XTAL2。LPC2138的PLL倍频是基于这个外部频率计算的,所以晶振实际频率必须准。买晶振时注意标注的负载电容值,常见12MHz晶振在20pF负载下起振最稳,电容大了起振慢,小了频率偏。RTC如果需要独立走时,可以再加一个32.768kHz晶振,但入门阶段不做低功耗和掉电计时,先不接它。
复位电路用10kΩ上拉电阻加1µF电容到地就能工作,这是原厂参考设计最常见的组合。要注意的是复位引脚不能只靠调试器驱动,板上必须有独立的复位按键。入门调试里出现JTAG连不上、ISP连不上,有一半情况是复位信号毛刺导致的,后面避坑章我会展开说。
2.3 ISP与JTAG两条通道,各解决什么问题
LPC2138下载和调试有两条完全独立的通道。ISP走UART0,需要把P0.14在复位时拉低进入bootloader,优点是只需要一个USB转串口模块,缺点是不能在线调试、不能看寄存器、不能设断点。JTAG走专用的5根信号线,能在线调试,但需要一个调试器。入门阶段我的建议:两条通道都留在板上,JTAG用于正常开发,ISP当作“后悔药”。
在做最小系统PCB或面包板之前,把下面这张检查清单过一遍。
| 检查项 | 要求 |
|---|---|
| 电源 | 3.3V稳定输出,VDDA/VSSA独立滤波 |
| 晶振 | 12MHz,22pF负载电容,引脚尽量短 |
| 复位 | 10k上拉 + 1µF到地,保留复位按键 |
| P0.14 | 引出跳线座,可切换高低电平 |
| JTAG接口 | TCK下拉10k,TDI/TMS上拉10k |
| 串口模块 | TXD接P0.0,RXD接P0.1,共地 |
这个清单里最容易忽略的是TCK下拉。TCK如果悬空,调试器握手阶段容易受干扰,表现就是仿真器偶尔能连上、偶尔报错,玄学得很。加上10k下拉后,这个问题基本消失。
3. 开发环境与工程搭建:先跑通最小工程再谈外设
3.1 工具链、工程模板和启动文件的选择
给LPC2138选工具链,我习惯用Keil MDK,社区资料最多,遇到问题好搜。MDK的老版本直接支持LPC2138,新版本需要装对应的器件支持包,否则新建工程时器件列表里找不到它。装完后在Project窗口里加一个启动文件,这个文件一般叫Startup.s,负责初始化堆栈、中断向量表,别自己改,默认就能用。
工程配置里有两个关键地址:ROM从0x00000000开始,长度0x80000(512KB);RAM从0x40000000开始,长度0x10000(64KB)。Debug页面选你的调试器型号,如果调试器连不上,先回第2章的硬件清单查TCK和复位。编译选项里把优化等级设成-O0(不优化),这是入门期最省心的设置,断点行为和源代码行号对应关系最直接。
第一次跑通的目标不是点灯,而是“空程序能下载、能复位、能全速运行”。有些同学上来直接写复杂代码,下载后不知道是代码问题还是工程配置问题。空工程跑一遍,确认最小链路OK,再往里填功能。
3.2 用寄存器点灯,把地址映射和方向彻底搞明白
LPC2138没有像新系列那样的标准外设库,寄存器操作就是直接读写地址。这种“原始感”反而是入门的优势:你能清楚看到IO方向寄存器、置位寄存器和清零寄存器是怎么配合的。点灯是理解GPIO的最小实验。
#include "LPC213x.H" static void delay_loop(volatile unsigned int n) { while (n--); } int main(void) { IO0DIR |= (1UL << 10); // P0.10设为输出 while (1) { IO0SET = (1UL << 10); // 输出高电平,LED灭或亮取决于接法 delay_loop(3000000U); IO0CLR = (1UL << 10); // 输出低电平 delay_loop(3000000U); } }逻辑说明:IO0DIR是GPIO方向寄存器,对应位写1表示输出,写0表示输入。IO0SET和IO0CLR是置位和清零寄存器,写1生效这一点很关键,直接写IO0PIN清零是无效的,因为数据输出寄存器不是直接可写的。参数说明:delay_loop里的3000000是一个纯软件循环次数,在12MHz主频下大约能产生0.2秒左右的延时,在主频改成48MHz后时间会缩短,不能用于精确延时。
点灯后建议做一个反向实验:把P0.10设成输入,用手摸一个按键电路把电平拉低,然后用IO0PIN读回。这个实验能帮助你区分“输出方向”和“输入方向”的寄存器配置差异,后面做四脚按键、编码器输入时不会懵。
3.3 串口重定向:让printf成为你的调试眼睛
嵌入式调试最大的需求是把内部状态“拿出来看”。JTAG断点看变量当然可以,但串口打印更接近日常习惯。LPC2138的UART0寄存器很少,初始化时先配引脚复用,再写波特率分频值。
#include <stdio.h> #include "LPC213x.H" void uart0_init_9600(void) { PINSEL0 &= ~(3UL << 0); PINSEL0 |= (1UL << 0); // P0.0 复用为 TXD0 PINSEL0 &= ~(3UL << 2); PINSEL0 |= (1UL << 2); // P0.1 复用为 RXD0 U0LCR = 0x83; // DLAB=1,8位数据,1停止位,无校验 U0DLM = 0; U0DLL = 78; // PCLK=12MHz时 9600波特率 U0LCR = 0x03; // DLAB=0 } int fputc(int ch, FILE *f) { while ((U0LSR & 0x20) == 0); // 等待发送保持寄存器空 U0THR = (unsigned char)ch; return ch; } int main(void) { uart0_init_9600(); printf("LPC2138 UART0 OK\r\n"); while (1); }逻辑说明:PINSEL0配置引脚复用功能,U0LCR里DLAB位打开后,U0DLL和U0DLM存波特率分频值。fputc是标准库printf最终调用的底层发送函数,在Keil MDK下重写这个函数即可,GCC工具链下需要重写_write。参数说明:U0DLL=78来自公式PCLK/(16×波特率),12MHz/(16×9600)=78.125,取78后实际波特率误差约0.16%,完全可用。如果你把PCLK改成48MHz,分频值是312.5,误差反而变大,所以串口外设我特意保持PCLK=12MHz不去倍频。
跑通printf后,马上把上电版本信息、PLL状态、内存地址这些打出来,这就是后面避坑章节里排查问题的基本工具。
4. LPC2138学习避坑指南:五个高频翻车现场与排查手法
4.1 PLL配置翻车:程序死在你以为的“默认频率”
现象:不加PLL配置时点灯、串口都正常,一加入PLL初始化代码,程序就死机或反复复位,有时连仿真器都连不上。
原因:PLL的两个核心参数M和P需要满足约束。CCLK=Fosc×(MSEL+1),其中Fosc是晶振频率,12MHz晶振下要得到48MHz主频,MSEL=3;VCO频率=CCLK×2^PSEL,PSEL=2时VCO=192MHz,落在156~320MHz允许范围内。一旦算错,比如MSEL取大了让CCLK超过60MHz上限,或者VCO低于下限导致PLL锁不住,CPU时钟就会乱,程序跑飞。
解决:用一组经过校验的经典配置,不要自己凭感觉改倍频。
void pll_48MHz(void) { VPBDIV = 0x00; // PCLK = CCLK/4 = 12MHz PLLCFG = (3U << 0) | (2U << 5); // MSEL=3, PSEL=2 PLLCON = 0x01; // 使能PLL,不连接 PLLFEED = 0xAA; // PLL馈送序列,固定写法 PLLFEED = 0x55; while ((PLLSTAT & (1UL << 10)) == 0); // 等待PLOCK置位 PLLCON = 0x03; // 使能并连接PLL PLLFEED = 0xAA; PLLFEED = 0x55; }逻辑说明:PLLFEED必须连续写0xAA和0x55,这个序列是用来把暂存配置真正送入PLL的,漏一步配置不生效。参数说明:VPBDIV=0x00把外设时钟设为CCLK的四分之一,也就是12MHz,这样第3章的串口分频值不需要重新算。新手最常见的错误是PLLCON只写0x01不写第二个状态,PLL一直没连接,程序虽然不死但主频没变。
排查时先读PLLSTAT判断是没锁住还是没连接,比反复改参数更高效。串口能打则直接打印PLLSTAT的低5位和bit10,能看到实际的MSEL值。
4.2 ISP连不上:不是软件问题,是电平时序
现象:用串口工具进入ISP模式,点击连接后一直无应答,或者偶尔能连上、拔插一次又不行。
原因:进入ISP有两个硬性条件:P0.14在复位释放时必须是低电平,同时芯片要有正确的时钟。很多人只拉低了P0.14,但复位是调试器给的,时序上没和P0.14稳定电平对齐;还有人用USB转串口模块给板子供电,模块的TXD电平不规范,导致UART0无法正确接收同步字符。
解决:遵循固定操作顺序。先断电,把P0.14跳线短接到GND,插上串口模块并打开串口助手,再上电。ISP程序会先发一个?字符等待握手,你需要在助手里手动发送同步字符。波特率先用9600,晶振频率在软件里选12MHz,这两个参数必须和板上实际值一致,否则波特率分频偏差会大到完全无法通信。
排查这张顺序表就够了:
| 步骤 | 操作 | 检查点 |
|---|---|---|
| 1 | 断电 | 确保P0.14跳线在低电平侧 |
| 2 | 接串口模块 | TXD接P0.0,RXD接P0.1,共地 |
| 3 | 上电 | 串口助手里应有字符返回 |
| 4 | 发送同步字符 | 观察是否有回应或进入菜单 |
4.3 中断不响应:VIC配置的三板斧
现象:按键外部中断没反应,或者中断只进一次就再也不触发,更严重的是中断里执行复杂逻辑后程序跳飞。
原因:LPC2138的中断控制器是VIC,它和后来的NVIC机制不一样。VIC要求你把中断服务函数地址注册到向量表中,并且在服务函数结束前手动清VICVectAddr。此外,外部中断EXTINT寄存器在进入中断后必须软件清标志,否则CPU会一直认为中断没结束。
解决:按固定模板配置外部中断。
void ext0_isr(void) __irq { EXTINT = 0x01; // 清EINT0中断标志 // 处理按键逻辑,越短越好 VICVectAddr = 0; // 通知VIC本次中断结束 } void ext0_init(void) { EXTINT = 0x01; // 预清标志 VICVectAddr0 = (unsigned long)ext0_isr; VICVectPri0 = 1; // 优先级设为1 VICIntSelect &= ~(1UL << 14);// EINT0设为IRQ,不是FIQ VICIntEnable = (1UL << 14); // 使能EINT0中断通道 }逻辑说明:__irq是ARM7编译器用于声明中断服务函数的关键字,它会自动处理寄存器保护。VICVectAddr0是优先级最高的向量槽,地址写0对应第一个槽位。参数说明:bit14是EINT0在VIC中的通道号,不同外设的通道号不同,查头文件里的宏定义最稳妥,不要靠记忆。踩坑点在EXTINT清标志,读取和清标志不是同一操作,必须写0x01。
调试时如果中断不响应,先用串口在中断外打印VICIntEnable和VICVectAddr0的当前值,确认配置确实写进去了,再看引脚有没有复用成GPIO输入方向。
4.4 Flash擦写死机:IAP的RAM执行与扇区边界
现象:调用IAP擦除函数后程序直接死机,或者擦除后重新下载时校验失败。
原因:IAP要求调用代码在RAM中执行,因为擦写Flash的过程中CPU不能从Flash取指令。如果你直接从Flash里调用IAP,擦除瞬间CPU取不到下一条指令,必死。另一个常见问题是扇区号算错,LPC2138的Flash扇区结构不是均匀分块,不同型号扇区大小不同,拿其他型号的扇区表套用就会越界。
解决:把IAP入口定义为函数指针,调用时确保当前代码段在RAM中,或者把整个擦写函数放到RAM执行段。
#define IAP_ENTRY 0x7FFFFFF1 typedef void (*IAP_FUNC)(unsigned int *, unsigned int *); void erase_all_user_flash(void) { unsigned int cmd[5] = {50, 0, 15, 12000, 0}; unsigned int res[4] = {0}; IAP_FUNC iap; iap = (IAP_FUNC)IAP_ENTRY; iap(cmd, res); if (res[0] != 0) { // 根据状态码定位错误,1表示无效命令 } }逻辑说明:IAP入口地址0x7FFFFFF1是原厂bootloader里固定的,cmd数组里50是擦除命令号,0和15是起始和结束扇区,12000是CCLK的kHz值,必须和实际主频一致。参数说明:如果你只擦部分扇区,cmd[1]和cmd[2]按扇区表填,不要用字节地址。擦写过程中务必关全局中断,否则中断服务函数触发一次Flash访问就前功尽弃。
4.5 调试器识别不到芯片:硬件链路的低级错误
现象:仿真器连接时报错,读不到IDCODE,有时候连目标电压都检测不出来。
原因:排查顺序应该从供电开始,但实际翻车点通常很具体:一是TCK上没有下拉电阻,调试时钟悬浮受干扰;二是复位电容取得太大,比如用了10µF,复位信号下降沿太陡导致调试器握手期间芯片还在复位状态;三是板上JTAG的TDI、TMS被复用成GPIO后没恢复,调试链路对不上。
解决:按第2章的硬件清单逐项排除。
| 步骤 | 操作 | 判断标准 |
|---|---|---|
| 1 | 量3.3V电源 | 电压稳定,无跌落 |
| 2 | 量复位引脚电平 | 正常运行时为高电平 |
| 3 | 量TCK时钟 | JTAG连接时能看到跳变 |
| 4 | 断开ISP串口线 | 排除串口模块电平干扰 |
如果之前往Flash里写过把JTAG引脚复用为GPIO的程序,而且启动后立即执行,那调试器永远连不上,因为引脚功能已经被代码改了。这种情况的解法是回到第4.2节的ISP流程,擦除整片Flash再重新下载。
5. 把ISP和UART组合成“后悔药”:一个能长期复用的救砖与自检习惯
刚入门LPC2138时,我建议你做的第一块板子就预留三样东西:P0.14的跳线、复位按键、UART0的串口座。这三样不占多少面积,但能在以后的所有实验里反复救你。养成一个习惯:每次上电启动,第一件事就是通过UART0打印当前PLL状态和版本标识,不要先跑业务逻辑。
void boot_selfcheck(void) { unsigned int plls = PLLSTAT; printf("PLOCK=%d M=%d P=%d CCLK=%d kHz\r\n", (plls >> 10) & 1, (plls & 0x1F) + 1, 1 << ((plls >> 5) & 0x03), 12000 * ((plls & 0x1F) + 1) / 1000); }逻辑说明:PLLSTAT的bit10是PLOCK锁定标志,低5位是当前MSEL值,bit5到bit6是PSEL值。函数把这些信息组合成一行文本,串口一开就能确认主频配置是否生效。参数说明:12000是外部晶振频率的kHz值,如果换晶振,这个数要跟着改。
当调试器连不上、芯片疑似“锁死”时,这个救砖流程能覆盖九成情况:断电,P0.14跳线接GND,上电,串口助手确认进入ISP,用ISP工具擦除整片Flash,断电,P0.14跳线恢复高电平,重新上电后用JTAG下载正常程序。整个过程不需要换芯片,不需要额外设备,是我用过最可靠的方式。我自己就翻过一次车:把TCK引脚复用成GPIO后直接失去调试链路,折腾了一晚上才想到走ISP擦除,从那以后每块板子都强制留ISP入口,再没慌过。把这两个习惯固化下来,这块LPC2138学习板就不会只是学完即弃的课设工具,而是你后面验证各种裸机想法的最顺手的实验台。希望帮到你。
本文还有配套的精品资源,点击获取