简介LPC17xx官方例程是NXP基于Cortex-M3内核微控制器的开发参考资料适合嵌入式工程师、学生及项目开发者快速上手该系列芯片。资源覆盖GPIO、UART、SPI、I2C、定时器/PWM、ADC/DAC、USB、闪存编程、电源管理及FreeRTOS移植等关键模块提供可直接对照学习的C语言示例和工程配置并附有大量html格式帮助文档便于理解中断配置、外设初始化及Bootloader等底层细节。压缩包共2687个文件以c/h源码、uvproj/uvopt工程文件、makefile构建脚本及readme说明为主总计约57MB目录结构清晰适合按功能模块检索学习。目前已有758人学习下载既可作为初学者的入门教程也可作为实际项目中的代码参考和调试手册有助于缩短开发周期、快速解决外设驱动与系统移植中的常见问题。1. 拉通工程lpc17xx 官方例程包到底能拿来干什么如果你手里恰好有一块 LPC1768 最小系统板又刚从某宝买的散装资料包里翻出一份只有几兆的压缩包大概率就是这套 lpc17xx 官方例程。我第一次打开它的时候第一反应是“怎么这么多文件夹”第二反应是“原来芯片手册上那些外设官方已经全部写好了可编译的工程”。这套例程的价值不在代码量而在它是一个能直接烧录、能跑通、能抄的“外设驱动大全”GPIO、定时器、UART、I2C、SPI、ADC、DAC、看门狗、RTC、USB、以太网每个外设都有独立的 MDK 工程和配套说明芯片手册里的寄存器配置在本套例程里全都变成了 C 语言可以直接在调试器里单步跟。适合两类人。一是从 51 或 STM32 转过来、想快速评估 LPC17xx 的开发者官方例程能帮你把“芯片能不能满足我的外设需求”这个问题压缩到一个下午二是刚接触这颗芯片的入门者与其对着几百页用户手册猜 PINSEL 该写几不如先把例程跑起来再回头读寄存器理解效率高得多。这套资源解决的不是“怎么写代码”而是“怎么把一颗陌生芯片的最小系统点亮、串口打通、外设转起来”——这一步跨过去后面全是体力活。2. 解包与工程结构从目录布局看懂整套代码的组织方式2.1 一级目录与固件库分层驱动库、CMSIS、例程三件事分开放解压之后你会看到几个顶层目录每个都有明确分工。我最开始没看懂直接在 examples 里找工程结果编译报错找不到头文件后来才明白这套源码是按“库 例程 平台抽象”三层组织的目录是给大型项目复用的不是给单个例程服务的。lpc17xx_example/ ├── CMSIS/ # Cortex-M3 核心支持 │ ├── core_cm3.h # 内核寄存器与内置函数定义 │ ├── core_cm3.c # 内核调试与指令函数实现 │ └── startup_lpc17xx.s # 启动文件复位向量、堆栈初始化、中断向量表 ├── lpc17xx_lib/ # 外设驱动库源码 │ ├── inc/ # 各外设驱动头文件如 lpc17xx_uart.h │ └── src/ # 驱动实现如 lpc17xx_uart.c、lpc17xx_gpio.c ├── examples/ # 按外设拆分的可编译工程 │ ├── gpio/ │ ├── uart/ │ ├── timer/ │ ├── adc/ │ └── usb/ └── board/ # 板级相关配置如系统初始化、引脚映射CMSIS 目录是整个芯片运行的底座。core_cm3.h 定义了 NVIC、SysTick 这些内核外设的数据结构和操作函数启动文件里包含复位向量和全部中断向量芯片上电后第一条指令就落在 startup 文件中的 Reset_Handler。lpc17xx_lib 是原厂固件库每个外设对应两个文件函数命名有规律lpc17xx_uart.c 里的 UART0_Init 负责初始化、UART0_Send 负责发送头文件里还放着波特率相关的宏和结构体。examples 目录以工程为单位拆开每个子目录都是一个独立的可编译工程这是这套资源最好用的地方——你不用把整个库改成自己的工程直接复制对应外设的文件夹就完事。我把这套结构理解为“仓库级工程布局”就是给自己预留扩展空间。你后来新建一个功能模块放一个平级的 app 目录再在板级初始化里调用它完整工程就慢慢长出来了不用推翻重来。2.2 用 MDK 打开官方例程设备选择、全局宏与头文件路径打开例程的方式不复杂但有几个配置要提前确认不然第一波编译就会卡住。以 MDK 5 为例直接打开 examples 里任意工程目录下的 .uvprojx 文件它会把工程文件、源文件分组、编译选项全部加载好。打开后第一件事是确认 Device 是否选了你的芯片型号LPC1768、LPC1758 这些都属于 LPC17xx 系列但 Flash 容量和引脚数有差异选错型号可能出现调试器不识别或烧录超范围的问题。接着检查 C/C 选项卡里的 Include Paths。官方例程为了保持目录通用把路径写成了相对路径但 MDK 工程一旦挪位置相对路径就会失效条件包含的头文件被全部重置。编译时报错“cannot open source file core_cm3.h”基本都是这里的问题。解决办法是把 CMSIS、lpc17xx_lib/inc、board 这三个目录手动 Add 进 Include Paths。编译产物的位置也值得说一句。默认输出目录是工程下的 build 或 obj 文件夹编译完成后生成 .hex 文件用调试器或者串口 ISP 工具烧录时选对 hex 文件入口就完事。如果编译报错集中在某个外设驱动文件里先别急着改代码多半是包含关系没理清官方例程里的宏定义分散在头文件有些例程会定义 USE_LPC1768 之类的板级宏不同板卡宏定义会影响引脚复用和系统时钟初始化所以打开工程之后先全局搜一下宏定义开关逐条确认再开始编译。2.3 最小运行链路从复位向量到 main 函数之间发生了什么点灯例程是最简单的一个工程但你想真正搞懂这套代码建议先看启动文件和系统初始化因为所有例程都走同一条启动链路。startup_lpc17xx.s 里开头的向量表按顺序列出全部中断处理函数地址第一个是初始栈顶第二个是 Reset_Handler。程序上电后从 Flash 取到向量表跳到 Reset_Handler第一步就是复制数据段、清 BSS 段然后调用 SystemInit 配置时钟最后进入 __main 和 main。; startup_lpc17xx.s 关键片段 Reset_Handler PROC EXPORT Reset_Handler LDR R0, SystemInit ; 先配置时钟再进 main BLX R0 LDR R0, __main ; 交给 C 运行库初始化 BX R0 ENDP这段汇编逻辑清晰SystemInit 在进入 C 环境前就把锁相环和时钟树配好这样后面所有外设驱动代码拿到的 SystemCoreClock 才是真实主频。lpc17xx_lib 里大部分外设驱动都依赖 SystemCoreClock 计算分频系数比如 UART 波特率、定时器周期所以时钟一旦配错整套例程会集体翻车后面会有专门的踩坑记录讲这个问题。启动链路理解之后你会对官方例程的“可移植性”有更实际的感知。外设驱动里的计算逻辑是通用的但引脚复用、时钟源、主频这三个参数是板级的官方例程默认你在原厂评估板上跑拿到你的板子上必须逐个核对。这也是为什么我建议把 board 目录单独摘出来当配置层后面二次开发章节会展开讲。3. 核心链路拆解从系统时钟到串口打印的实现3.1 时钟树配置PLL0 倍频关系与 SystemInit 的工作原理LPC17xx 的时钟树是这套芯片最容易绕晕的地方也是官方例程里写得最“黑盒”的部分。外部晶振经过 PLL0 倍频再经过 CCLKCFG 分频得到 CPU 时钟外设时钟又从 CCLK 分频出来。SystemInit 函数在 system_LPC17xx.c 里用一组寄存器写操作把这条链路全部建好但寄存器里的倍频系数是从头文件宏定义的不同晶振频率要按公式重新手算。/* system_LPC17xx.c 中 SystemInit 的时钟核心部分 */ void SystemInit(void) { /* 第 1 步把 PLL0 置于旁路先切到内部 RC 时钟 */ LPC_SC-PLL0CON 0x00; LPC_SC-PLL0FEED 0xAA; LPC_SC-PLL0FEED 0x55; /* 第 2 步写入 PLL0 倍频系数 */ /* 公式: Fout Fclkin * (MSEL 1) / ((PSEL 1) * 2) */ LPC_SC-PLL0CFG PLL0CFG_Val; /* 宏定义在头文件 */ LPC_SC-PLL0FEED 0xAA; LPC_SC-PLL0FEED 0x55; /* 第 3 步等 PLL 锁定再接到 CPU 时钟树 */ while (!(LPC_SC-PLL0STAT (1 10))); /* PLOCK 位置位 */ LPC_SC-PLL0CON 0x01; LPC_SC-PLL0FEED 0xAA; LPC_SC-PLL0FEED 0x55; }每写一次 PLL 配置寄存器后面必须紧跟两次 PLL0FEED 写序列(0xAA、0x55)这是芯片的寄存器保护机制代码不是随便写的。PLL0CFG_Val 由两个字段组成bit 0-4 是 PSELbit 6-14 是 MSEL头文件里通常已经按 12MHz 晶振算好了你换晶振时不能只改数值要重新解公式。实际工程里最常见的坑是外部晶振和 PLL 参数不匹配。官方例程默认搭配 12MHz 晶振如果你的板子用了 15MHz 或 25MHz 晶振主频会偏差得离谱外设例程全部表现异常。我一般会在拿到新板子的第一步就把晶振频率确认清楚然后用枚举法列一组合法的 MSEL 和 PSEL 组合去试。解公式时记得同时校验两个约束VCO 输出要在芯片手册规定范围内目标主频要能被常用波特率整除。3.2 引脚复用PINSEL 寄存器决定这个引脚到底是 GPIO 还是串口LPC17xx 的绝大多数引脚都有多个功能GPIO、UART、I2C、ADC 都在同一个物理引脚上这跟很多芯片单独设一个 GPIO 复用寄存器不一样。官方例程里每个外设工程都会自己配置 PINSEL 和 PINMODE这部分代码零散分布在各外设例程中但规律只有一条PINSEL 每两位管一个引脚值 00 是 GPIO01 是第一备用功能10 是第二备用功能11 是第三备用功能。以 UART0 为例默认引脚是 P0.2 和 P0.3分别作为 TXD 和 RXDPINSEL0 寄存器的 bit 4-7 就管这两个引脚的复用。/* UART0 引脚复用配置 */ LPC_PINCON-PINSEL0 ~(3UL 4); /* 先清 P0.2 的功能字段 */ LPC_PINCON-PINSEL0 | (1UL 4); /* 01 UART0 TXD */ LPC_PINCON-PINSEL0 ~(3UL 6); /* 清 P0.3 字段 */ LPC_PINCON-PINSEL0 | (1UL 6); /* 01 UART0 RXD */PINMODE 寄存器管引脚的上下拉模式比如 UART 的 RXD 引脚通常要配置为无上下拉或上拉避免浮空导致串口数据乱跳。很多开发者习惯在 GPIO 例程里同时配置 PINSEL 和 PINMODE这是对的因为 GPIO 输出时引脚上拉可能造成微弱电流特殊功耗场景下不建议省这一步。调试这类问题时我习惯先把数据手册的引脚功能表打印出来贴在屏幕上因为同一引脚在不同封装、不同型号上的备用功能编号可能不一样比如 LPC1768 和 LPC1754 的 P1.30 复用功能就有差别。代码里查不到就翻手册这是 LPC17xx 系列的通用节奏。3.3 UART 例程实战波特率计算、FIFO 与 printf 重定向串口例程是每个外设里最值得仔细读的因为它牵扯到两条知识链波特率怎么算准printf 怎么拐到串口上。官方例程的 UART 驱动把这几个问题全部封装好了你把它拆开看一个是整数分频、一个是小数分频合在一起才能覆盖各种不整除的时钟关系。/* UART0 初始化按目标波特率计算分频系数 */ void UART0_Init(uint32_t baudrate) { uint32_t pclk SystemCoreClock / 4; /* 外设时钟 PCLK CCLK/4 */ uint32_t divisor pclk / (16 * baudrate); LPC_UART0-LCR 0x83; /* 8位数据、1停止位、打开DLAB */ LPC_UART0-DLM (divisor 8) 0xFF; /* 整数分频高字节 */ LPC_UART0-DLL divisor 0xFF; /* 整数分频低字节 */ LPC_UART0-FDR 0x10; /* 小数分频器默认1倍系数 */ LPC_UART0-LCR 0x03; /* 关闭DLAB进入正常模式 */ LPC_UART0-FCR 0x07; /* 使能FIFO并清空缓冲 */ }参数说明pclk 是由 SystemCoreClock 分频得到的PCLKSEL 寄存器决定分频系数官方例程默认 CCLK/4divisor 是波特率分频的整数部分但只取整会引入误差所以还有 FDR 的小数分频寄存器bit 0-3 是 DIVADDVALbit 4-7 是 MULVAL用于把误差拉进 ±2% 的范围内。实际做通信时我建议用示波器量一下 TXD 引脚的波特率因为 LPC17xx 的串口时钟误差在高温下还会再偏移光靠纸面计算不够保险。printf 重定向也藏在例程里但找起来要一点耐心。官方库并不直接提供标准 printf它只给了 UART 发送字节的函数printf 能不能用取决于你是否重写了 fputc。/* printf 重定向把标准输出接到 UART0 */ int fputc(int ch, FILE *f) { LPC_UART0-THR ch; while (!(LPC_UART0-LSR (1 5))); /* 等待发送缓冲区空 */ return ch; }这段代码在大多数 MDK 工程里要额外做一件事在工程选项里勾选 Use MicroLIB否则会引入完整的 C 标准库 printf 实现占用数十 KB Flash对 LPC17xx 这种内部 Flash 不算大的芯片来说太浪费。MicroLIB 下的 fputc 重定向是嵌入式开发的通用技巧官方例程没有单独写一篇文档但实验例程里已经把这个钩子留好了。4. 外设驱动例程解读GPIO、定时器与 ADC 的实现套路4.1 GPIO 例程FIODIR 方向配置与 FIOSET/FIOCLR 输出翻转GPIO 例程是整个官方例程里最没有惊喜但也最适合当模板的。LPC17xx 的 GPIO 没有把每个引脚做成一个单独寄存器而是用端口寄存器批量操作FIODIR 控制方向FIOSET 输出高FIOCLR 输出低FIOPIN 同时做读和写。没有像 STM32 那样的 ODR 和 IDR 分离逻辑更直白。/* 配置 P1.28 为推挽输出 */ LPC_GPIO1-FIODIR | (1UL 28); /* 1 输出方向 */ LPC_GPIO1-FIOSET (1UL 28); /* 输出高电平 */ LPC_GPIO1-FIOCLR (1UL 28); /* 输出低电平 */ /* 读取 P0.15 引脚电平 */ uint32_t key_val (LPC_GPIO0-FIOPIN 15) 0x01;寄存器组编号和端口号是对应的LPC_GPIO0 对应 P0.xLPC_GPIO1 对应 P1.x计算位置时不要搞混。官方例程里还有一组快速 GPIO 寄存器它在 GPIO 端口寄存器上做了半字和字节操作适合频繁翻转的场景但它的寄存器访问宽度限制比较严格新手不建议用普通 FIOSET 已经够点十个灯了。我做 GPIO 例程时最爱看的部分是官方怎么组织“板级映射”例程里通常不会在主程序里直接写死引脚号而是把头文件里定义一段宏把“板载 LED”映射到一个具体的端口和引脚上。这种做法在二次开发时价值很大因为换板子只改头文件不改逻辑代码。4.2 定时器例程预分频、匹配寄存器与中断回调定时器例程比 GPIO 复杂一档它涉及到 PCONP 的模块供电开启、PCLKSEL 的分频配置、PR 预分频、MR 匹配寄存器、MCR 匹配控制这五个环节。官方例程用了库函数封装但读代码时建议回到寄存器层面因为库函数在不同版本里函数名会变寄存器位的定义却不会变。/* TIMER0 配置为 1 秒匹配中断 */ void TIMER0_Init(void) { LPC_SC-PCONP | (1 1); /* 打开 TIMER0 的电源 */ LPC_SC-PCLKSEL0 ~(3UL 2); LPC_SC-PCLKSEL0 | (1 2); /* PCLK CCLK / 1 */ LPC_TIM0-PR SystemCoreClock / 1000 - 1; /* 1ms 节拍 */ LPC_TIM0-MR0 1000 - 1; /* 匹配值: 1000 次 1 秒 */ LPC_TIM0-MCR (1 0) | (1 1); /* 使能中断 匹配后计数归零 */ LPC_TIM0-TCR 1; /* 启动定时器 */ NVIC_EnableIRQ(TIMER0_IRQn); }参数说明PR 预分频的值是节拍周期减一如果系统时钟 100MHzPR 设为 99定时器每 1us 递增一次MR0 设为 999则匹配周期正好 1msMCR 的 bit0 是中断使能bit1 是匹配后复位计数器这两个一起置位才能产生周期性中断只置 bit0 会产生一次性中断定时器会继续跑。这个“减一”是定时器配置最经典的坑几乎每个入门者都在这丢过一次时间。定时器中断服务函数在官方例程里一般命名为 TIMER0_IRQHandler里面的固定操作是读中断标志、清标志、然后进入用户回调函数。这类结构已经是嵌入式中断代码的标准模板把硬件中断和业务逻辑隔离业务逻辑不会因为寄存器反复操作而臃肿。4.3 ADC 例程12 位转换、通道选择与轮询读取方式ADC 例程能完全看懂LPC17xx 的模拟采集部分就算入门了。它有一个全局的 ADC 控制寄存器 AD0CR通道选择、时钟分频、启动方式全在这一个寄存器里挤着典型配置是单次转换、软件启动、读结果时用状态寄存器判断完成。/* 配置 AD0.4 通道单次软件启动转换 */ LPC_SC-PCONP | (1 12); /* 打开 ADC 电源 */ LPC_PINCON-PINSEL1 | (1 14); /* P1.30 复用为 AD0.4 */ LPC_ADC-AD0CR (1UL 4) /* 选择通道 4 */ | (1UL 8) /* CLKDIV 1 */ | (1UL 21) /* ADC 上电 PDN */ | (1UL 24); /* START 立即启动 */ /* 等待转换完成并读取结果 */ while (!(LPC_ADC-AD0GDR (1UL 31))); /* DONE 位置位 */ uint32_t raw (LPC_ADC-AD0GDR 4) 0xFFF;AD0CR 里的 CLKDIV 控制 ADC 时钟分频芯片对 ADC 时钟有上限要求不能直接把系统时钟灌进去START 字段如果设为 0转换不会开始必须显式写 1 或者用 BURST 模式。轮询 DONE 位是最简单的方式ADC 例程还有中断模式核心区别是把 AD0INTEN 和中断服务函数串起来结果从 AD0GDR 读就行判定逻辑不变。采集结果转电压时要特别注意参考电压。官方例程默认参考电压等于 VDDA但有的板子把 VREF 引脚单独供了 3.0V 而不是 3.3V代码里如果还按满量程 3.3V 去除采集精度会偏。我处理这类问题时习惯先在板上量 VREF 实际电压再把满量程值写成一个宏例程里所有公式都引这个宏。5. 避坑记录MDK 迁移、调试连接与时钟配置高频问题5.1 编译报错找不到 core_cm3.h所有的 include 路径全失效现象编译刚开始几行就报错fatal error: core_cm3.h: No such file or directory点开错误看看跳转不到任何文件。原因官方例程的工程文件里写的是相对路径你把官方例程移到自己的工作目录或者 MDK 换了安装目录原来的相对路径就悬空了MDK 不会自动修复路径只会把它当作普通头文件缺失处理。解决打开工程 Options for Target在 C/C 选项卡里点 Include Paths把 CMSIS、lpc17xx_lib/inc、board 三目录手动加进去。之后编译若还有小文件找不到按报错逐个补路径一般三轮之内能过。5.2 程序烧录后没反应复位也没用怀疑芯片是坏的现象用调试器烧录成功但按复位键板子没有任何现象LED 不亮串口无输出。原因最常见的是 LPC17xx 的 ISP 引脚被意外拉低芯片上电后没有进入用户程序而是停在了内部的 bootloader 区等你通过串口下载。LPC17xx 的 bootloader 控制引脚一般对应 P2.10电平状态在复位瞬间被采样如果板子设计成 P2.10 默认接地程序永远起不来。解决把 P2.10 用跳线接到高电平再复位。如果板子上没有这个跳线试着用手按住引脚再上电。确认能跑起来之后把 P2.10 的状态写进板卡说明文档整个项目组就都不会再被这个坑绊一次。5.3 printf 打印乱码波特率明明设置对了现象串口助手能看到输出但全是乱码换波特率也没有一个组合能对上。原因一个是晶体频率和 PLL 参数不匹配导致 SystemCoreClock 实际值和代码里计算波特率用的值不一致分频系数全错另一个是 printf 重定向的 fputc 没生效调用了标准库但输出没进串口。解决先在调试器里查看 SystemCoreClock 变量的实际值跟硬件晶振算出来的理论值对比。如果差很多回时钟配置里重新解 PLL 公式。乱码还要检查 UART 引脚的 PINSEL 是否配置成备用功能以及 TXD/RXD 是不是接反了。5.4 SWD 调试器连不上目标芯片现象调试器界面一直提示连接超时确认供电正常、接线三根都对还是连不上。原因多数情况是芯片在上一次烧录时配置了某些引脚为高功耗状态或者复位电路上电容过大调试器在复位时序窗口内没抓住芯片极少数情况是芯片进入了 ISP 模式调试口被 bootloader 暂时接管。解决按住目标板的复位键不放启动连接动作在连接过程中松开复位键这样能强制调试器在芯片刚释放复位的瞬间抓到内核。如果多次失败把 SWD 的线换成短跳线减小寄生电容成功率会明显上升。5.5 例程 A 能跑例程 B 一进 main 就 HardFault现象同一个工程模板换成另一个官方例程代码后程序跑进 HardFault_Handler 中断调试器里看 PC 指针停在莫名地址。原因外设初始化顺序问题。LPC17xx 的外设如果供电未打开就去配置寄存器行为未定义另外定时器中断使能后如果没有及时清中断标志也可能在启动阶段被打断触发内核异常。解决严格按官方例程的初始化顺序走先打开 PCONP 外设电源再配置时钟分频再写外设寄存器最后才使能中断。遇到 HardFault 时先看 SCB-CFSR 寄存器的状态位能精确指出是总线错误还是用法错误比瞎猜快得多。6. 二次开发的改造思路把官方例程重构成自己的外设驱动骨架拿到这套例程最忌讳的是直接在一个例程里堆功能。我一般会把整个工程结构重新切一遍形成三个目录bsp 放外设驱动board 放引脚和时钟配置app 放业务逻辑。官方例程里的 lpc17xx_lib 原封不动保留我只把 examples 目录里的功能代码离散开搬进自己的文件名前缀里比如 bsp_uart.c、bsp_timer.c这样后续新增模块时只需在 board_init.c 里加一行调用。/* board_init.c 集中管理外设初始化按宏裁剪 */ /* 配置项放在 bsp_cfg.h 里做成开关式 */ #define BSP_USING_UART 1 #define BSP_USING_TIMER 0 #define BSP_USING_ADC 0然后在 board_init 里执行睡眠调动void board_init(void) { if (BSP_USING_UART) bsp_uart_init(115200); if (BSP_USING_TIMER) bsp_timer_init(1000); if (BSP_USING_ADC) bsp_adc_init(ADC_CH4); }这套“宏开关”的裁剪思路官方例程里已经有雏形但它们分散在各工程的头文件里没有合并成统一入口。我把它抽到一份 bsp_cfg.h换板子时只改这份文件和 board 目录下的引脚映射业务层代码一行不动。引脚复用也建议做一张表驱动在 board 目录里定义引脚-功能结构体数组用注册函数把每个外设绑到对应引脚上这样零散的 PINSEL 配置就能集中审查省去翻手册核对的时间。验证方法也很简单每次重构完一个模块用官方例程跑出来的现象做对比。比如我改完 UART 驱动先把官方例程的串口回环功能跑一遍再换成自己的 bsp 层测试相同引脚和波特率下的输出两边字节流一致才算通过。从那以后我每换一块板子都强制先走一遍“最小时钟加串口打印”的流程确认系统时钟和 PINSEL 没问题再动其它外设。这套官方例程的价值就在这——它不只是参考代码更是一把量尺能让你在二次开发时知道自己有没有跑偏。希望帮到你。本文还有配套的精品资源点击获取