简介:这套面向LPC17XX系列(NXP Cortex-M3内核)微控制器的例程合集,聚焦嵌入式开发中最常用的ADC模拟量采集、CAN总线通信、DAC模拟输出、外部中断EINT、GPDMA高速数据搬运以及Keil工程模板等模块,内容由浅入深,适合刚接触LPC17XX的电子、自动化、嵌入式方向学生与工程师按需查阅、移植和学习。压缩包为RAR格式,共1675个文件,其中以518个h头文件与341个c源码为主体,并配有uvproj/uvopt等Keil工程配置文件、启动文件、编译中间产物以及少量说明与调试记录,整体大小约7.79MB,文件按不同外设和模板目录组织,便于快速定位对应例程。目前已有408人学习下载,反馈可作入门参考。资源价值不止于基础代码:每个例程均包含完整的初始化、配置和处理逻辑,例如ADC通道与采样设置、CAN消息过滤与收发流程、GPDMA通道参数配置、外部中断优先级处理等,并能通过Keil模板直接编译运行、观察现象和修改验证,帮助开发者从工程结构到外设驱动形成系统认知,为后续实际项目开发打下可靠基础。
1. LPC17XX例程不是“烧进去就能跑”的代码,而是一套需要拆开看的外设说明书
你搜索 LPC17XX例程,多半是刚拿到一块带 LPC1768 的开发板,或者接了老项目要从别的 M3 芯片迁过来。常见的尴尬是:例程文件拷进工程、编译、下载,板子上 LED 不亮,串口助手打开全是乱码。问题基本不在代码本身,而在你跳过了例程里最关键的几层东西——时钟配置、引脚复用、启动文件。这篇笔记就按“先看懂例程骨架 → 本地把最小工程跑通 → 串口打通 → 避坑 → 进阶调试”的顺序,把 LPC17XX 例程里真正决定能不能跑的细节讲清楚。新手能照着一步步做,熟手可以直接跳到参数和坑的部分。
2. 例程为什么都长一个样:CMSIS 分层、启动文件与寄存器寻址
2.1 CMSIS 三层结构:把寄存器操作从“魔法数字”里解放出来
LPC17XX 的例程无论从哪个渠道拿到,代码风格几乎都一致:头文件包含LPC17xx.h,外设操作用LPC_GPIO1、LPC_UART0、LPC_TIM0这类宏。这不是某个人定的习惯,而是整个生态都建立在 CMSIS 这套标准分层上。
CMSIS 把代码分成三层:芯片寄存器定义层、外设访问层、核心外设层。寄存器定义层由芯片厂商提供,就是LPC17xx.h,里面把芯片手册里的内存映射表变成 C 语言结构体。外设访问层就是例程里直接操作的LPC_GPIO0这类宏。核心外设层管的是 NVIC、SysTick 这些内核里的部件,比如NVIC_EnableIRQ()、SysTick_Config()都是这一层提供的。
理解这个分层对看例程最大的帮助是:你不用背寄存器地址,但必须知道某个外设的寄存器在结构体里叫什么、每位是什么作用。比如LPC_GPIO1->FIODIR,FIODIR是方向寄存器,写 1 是输出,写 0 是输入。例程里出现的每一行寄存器操作,都能在芯片用户手册的外设寄存器描述里找到对应位。
新手最常见的问题是把例程当成黑匣子,改一个参数靠试。但 LPC17XX 这类芯片的外设寄存器是扁平结构,没有复杂的抽象层,完全可以直接读寄存器来确认状态。后续章节里所有代码我都会直接操作寄存器,不依赖例程包里的封装库,这样你能看清每一步到底在写什么。
2.2 上电第一行代码:复位向量、Reset_Handler 与 SystemInit 在哪改
LPC17XX 上电后第一条指令不是 main 函数,而是从 Flash 地址 0x00000000 取复位向量,跳转到启动文件里的Reset_Handler。启动文件startup_LPC17xx.s是例程里最容易被忽略的文件,但它决定了中断向量表长什么样。
Reset_Handler里做了三件事:把 RAM 里的数据段从 Flash 拷贝到 SRAM、清零 BSS 段、调用SystemInit(),最后才跳进 main。SystemInit()在system_LPC17xx.c里,主要干两件事:配置 Flash 等待周期,以及设置系统时钟来源。不同例程对SystemInit()的处理差别很大——有的只在里面设置了 Flash 加速器,时钟保持默认的内部 RC 振荡器 4MHz;有的在SystemInit()里直接配置 PLL 到 100MHz。
看例程时,先打开system_LPC17xx.c,搜索SystemInit,看里面有没有 PLL 相关代码。如果里面只设置了FLASHCFG,说明主频是默认的 4MHz,后面外设的 PCLK 都要按 4MHz 算;如果里面有PLL0CON、PLL0CFG的赋值,说明例程已经把主频拉高了,后续波特率、定时器定时值的计算基准完全不同。
这里有个实际体验说一句:很多人从例程复制代码到自己工程,只复制了 main.c,没复制修改过的system_LPC17xx.c,结果外设全部按错误时钟运行,表现就是串口乱码、定时器快了几十倍。所以拿到任何一份 LPC17XX 例程,第一步先看这个文件。
2.3 外设基地址怎么来的:LPC_GPIO1 这类宏背后是一张内存映射表
LPC_GPIO1在LPC17xx.h里被定义成一个指向结构体的指针,结构体起始地址由芯片手册的存储器映射决定。LPC17XX 把外设都放在 0x40000000 开始的地址段里,GPIO 在 0x2009C000 附近,UART0 在 0x4000C000,定时器0 在 0x40004000。这些地址不用硬记,但要明白一件事:例程里的宏不是随便命名的,它们对应的是芯片物理地址。
实际看例程时的操作方法是:在工程里右键LPC_GPIO1,选择 Go To Definition,看到结构体定义,里面就是 GPIO 的所有寄存器。比如 LPC17XX 的 GPIO 结构体里第一个成员是FIODIR,偏移 0x00;然后是FIORES0、FIOSET、FIOCLR、FIOPIN。这些成员顺序和芯片手册里的寄存器地址表是一致的。
掌握这个习惯后,遇到例程里没有的外设,比如要操作一个例程没覆盖的 UART2,自己就能定位到LPC_UART2的结构体,对照手册找到寄存器成员的偏移关系,然后照着 UART0 的写法操作。这比在网上搜“UART2 初始化代码”靠谱得多——搜来的代码很可能引脚配置跟你的板子不一样。
3. 从零建工程到点亮一颗 LED:最小可复现链路
3.1 建工程五步:选型号、装器件包、加启动文件、写 main、配下载器
在 MDK 环境下建 LPC17XX 工程,第一次操作的顺序很重要。先确认你手上的芯片具体型号,LPC1768 和 LPC1758 在器件列表里要分开选,错了后面外设配置全乱。
第一步,打开 MDK,Project → New uVision Project,把工程放在独立文件夹,文件名用英文。第二步,在器件选择窗口搜 LPC1768 或你手上的型号,双击选中。第三步,MDK 会弹出 Manage Run-Time Environment 窗口,这里只需要勾选 CMSIS 的 CORE 组件,注意不要勾选 Device 里的 Startup 选项让 MDK 自动添加启动文件;实际上现在新版 MDK 会自动把startup_LPC17xx.s加进工程,但有些版本不会,需要手动从安装目录里找。最稳妥的做法是新建完工程后,在 Project 窗口的 Device 目录下检查有没有 Startup 文件,没有就手动添加。
第四步,写 main.c,工程里只需要一个 main.c 加启动文件就能编译,system_LPC17xx.c由 MDK 自动加入。第五步,配置下载器:Options for Target → Debug 标签页,选择你手头的调试器类型,然后切到 Settings,确认能识别到芯片 ID;再切到 Utilities 标签页,勾选 Download 选项里的 Program Flash Download。
一个容易被忽略的小点:Utilities 标签页里要正确选择编程算法,LPC17XX 用的是内部 Flash 算法。如果烧录时提示找不到算法,就在 Flash Download 里手动 Add,选厂商对应的 LPC17XX 型号。做完这五步,编译空工程能通过,下载器能连上芯片,才算环境就绪。
3.2 点灯例程代码:GPIO 方向、电平寄存器与引脚复用
点灯是所有例程的第一个程序,但 LPC17XX 的点灯不是随便对引脚写高低电平就行——必须先把引脚复用寄存器 PINSEL 设成 GPIO 模式,再把方向寄存器 FIODIR 设成输出,最后通过 FIOSET/FIOCLR 控制电平。
#include "LPC17xx.h" #define LED_PORT 1 #define LED_PIN 18 // 假设 LED 接在 P1.18 #define LED_MASK (1UL << 18) void LED_Init(void) { // 1. 引脚复用:P1.18 对应 PINSEL3 的 bit [4:5],默认是 GPIO,但要显式清零保险 LPC_PINCON->PINSEL3 &= ~(0x03UL << (2 * (LED_PIN - 16))); // 2. 方向:FIODIR 置 1 表示输出 LPC_GPIO1->FIODIR |= LED_MASK; // 3. 初始电平:FIOCLR 拉低,LED 灭(假设低电平点亮) LPC_GPIO1->FIOCLR = LED_MASK; } void LED_On(void) { LPC_GPIO1->FIOCLR = LED_MASK; // 低电平点亮 } void LED_Off(void) { LPC_GPIO1->FIOSET = LED_MASK; // 高电平熄灭 } int main(void) { SystemInit(); LED_Init(); while (1) { LED_On(); for (volatile uint32_t i = 0; i < 1000000; i++); LED_Off(); for (volatile uint32_t i = 0; i < 1000000; i++); } }这里有两个关键参数要解释。PINSEL3 的位计算:LPC17XX 的引脚复用寄存器 PINSEL0 到 PINSEL11,每个寄存器管 16 个引脚,每根引脚占 2 位。P1.18 在 PINSEL3 覆盖的范围内,所以用(LED_PIN - 16) * 2算出起始位,0x03 << 起始位对应的 2 位清成 0,即 GPIO 功能。
FIODIR 寄存器每一位对应一根引脚,1 是输出,0 是输入。这个方向寄存器是读写可分的:|=拉高某位,&= ~拉低某位。很多例程里直接FIODIR = LED_MASK,这样会把其它引脚全部设成输入,虽然点灯没影响,但在复杂工程里会埋雷,建议始终用|=和&= ~操作单引脚。
电平控制用 FIOSET(置 1)和 FIOCLR(清 0)而不是直接写 FIOPIN,是因为 SET/CLR 这两个寄存器写 1 只影响对应位,写 0 不影响任何位,这样多引脚并发操作时互相独立。例程里看到有人用FIOPIN = value,那是整端口写,容易把别的引脚电平改掉,不推荐。
3.3 把主频从 4MHz 提到 100MHz:PLL 配置里容易抄错的那几行
LPC17XX 上电默认的内部 RC 振荡器是 4MHz,例程如果只点灯不通讯,4MHz 够用。但只要涉及 UART 波特率或精确延时,就必须把主频提上去。外部晶振常见 12MHz,目标主频建议配 96MHz 而不是 100MHz,原因后面串口章节细说。下面这段 PLL 配置是我常用的方式,放到 main 函数最前面:
void SystemClock_Init(void) // 外部 12MHz 晶振 -> PLL0 输出 96MHz { // 时钟源选择:0 表示内部 RC,1 表示主振荡器 LPC_SC->CLKSRCSEL = 0x01; // 等待主振荡器稳定 while (!(LPC_SC->OSCSTAT & 0x01)); // PLL0 配置:M = 96, N = 12,则 Fout = 12MHz * 96 / 12 = 96MHz // MSEL 在 bit[14:0],NSEL 在 bit[23:16],寄存器值都减 1 LPC_SC->PLL0CFG = (96 - 1) | ((12 - 1) << 16); LPC_SC->PLL0CON = 0x01; // 使能 PLL0,但尚未连接 LPC_SC->PLL0FEED = 0xAA; LPC_SC->PLL0FEED = 0x55; // 等待 PLL0 锁定,bit25 是 PLOCK 状态 while (!(LPC_SC->PLL0STAT & (1UL << 25))); // 连接 PLL0:PLLC 置 1,再喂 Feed 序列 LPC_SC->PLL0CON = 0x03; LPC_SC->PLL0FEED = 0xAA; LPC_SC->PLL0FEED = 0x55; // CPU 时钟不分频,直接用 PLL0 输出 LPC_SC->CCLKSEL = 0x00; // Flash 加速配置:提高取指速度,否则高频下 Flash 跟不上 LPC_SC->FLASHCFG = 0x00004000; }这段代码最容易被抄错的是 PLL0CFG 的值。MSEL和NSEL计算关系是:PLL 输出频率Fout = Fin * M / N。12MHz 晶振要得到 96MHz,可以设 M=96,N=12,实际写进寄存器的值要减 1,即 95 和 11。
PLL 的 Feed 序列也有讲究:每次修改 PLL0CON 或 PLL0CFG 后,必须连续往PLL0FEED写0xAA、0x55,否则修改不生效。很多人在这一步翻车,以为写 PLL0CON 就够了,结果 PLL 一直没连接上,程序还跑在 4MHz,外设时序全乱。
FLASHCFG 的值是经验值,不同主频需要设置不同的 Flash 等待周期。96MHz 下用 0x00004000 稳定可用。如果你把主频改成 120MHz,这个值要对应调整,否则会出现随机宕机或指令执行异常——这种问题极难排查,建议先照抄能跑通的配置,再研究 Flash 等待周期的细节。
4. UART 例程:串口通了才叫跑明白
4.1 引脚复用先于一切:P0.2/P0.3 的 PINSEL 配置
UART0 是 LPC17XX 默认的调试串口,引脚固定在 P0.2(TXD0)和 P0.3(RXD0)。很多例程把波特率算得很准,但串口就是不出数据,原因往往是引脚复用没设。P0.2 和 P0.3 的复位默认状态是 GPIO,要做串口必须给 PINSEL0 的对应位置值。
#include "LPC17xx.h" void UART0_PinInit(void) { // P0.2 对应 PINSEL0 bit[4:5],P0.3 对应 bit[6:7] // 值 01 表示 TXD0 / RXD0 功能,10 表示其它外设 LPC_PINCON->PINSEL0 &= ~(0x03UL << 4); // 清零 P0.2 LPC_PINCON->PINSEL0 |= (0x01UL << 4); // 设为 TXD0 LPC_PINCON->PINSEL0 &= ~(0x03UL << 6); // 清零 P0.3 LPC_PINCON->PINSEL0 |= (0x01UL << 6); // 设为 RXD0 }PINSEL 的值共 2 位,00 是 GPIO,01 是第一个外设功能,10 是第二个外设功能,11 通常无效或特殊功能。不同引脚的外设功能编号不同,查手册时看 PINSEL 寄存器对应引脚的值定义。
一个经常被忽视的搭档是引脚模式寄存器 PINMODE。每个引脚除了复用功能,还有上拉、下拉、无拉三种模式。串口引脚一般设成无拉或上拉。如果例程里出现引脚拉不下来或 RX 一直为低的情况,去查LPC_PINCON->PINMODE对应位的值。
除了 PINSEL 和 PINMODE,UART0 还需要注意 P0.2 的数字环回功能。某些 LPC17XX 型号的 P0.2 在特定配置下会连到内部环回测试电路,导致数据发不出去。遇到这种问题,确认 PINSEL0 bit4:5 只有 01 一个值,不要把两个功能位同时置位。
4.2 波特率不是随便填的:PCLK、除数锁存与 FDR 一条公式算到底
UART 波特率寄存器填多少,完全取决于外设时钟 PCLK。LPC17XX 的 UART0 挂载在 APB 总线上,PCLK 来自 CPU 时钟分频。复位后 PCLK 默认是 CCLK 的 1/4,如果主频是 96MHz,PCLK 就是 24MHz。
芯片手册给出的波特率公式是:
波特率 = PCLK / (16 × (DLM:DLL) × (1 + DivAddVal / MulVal))其中DLM:DLL是 16 位除数锁存值,DivAddVal和MulVal是小数分频寄存器 FDR 的值。手动计算的步骤是:先算出理想除数D_total = PCLK / (16 × 目标波特率),整数部分写进 DLM/DLL,小数部分通过 FDR 修正。
以 96MHz 主频、PCLK 分频设置为 1/8 即 12MHz 为例,追求 115200 波特率:
D_total = 12000000 / (16 × 115200) = 6.51 整数部分取 6,写入 DLL = 6,DLM = 0 需要 FDR 修正:1 + DivAddVal/MulVal = 6.51 / 6 = 1.085 DivAddVal/MulVal ≈ 0.085FDR 寄存器低 4 位是 DivAddVal,高 4 位是 MulVal,要求1 ≤ DivAddVal < MulVal ≤ 15。可取DivAddVal = 1,MulVal = 12,此时实际分频为6 × (1 + 1/12) = 6.5,实际波特率12000000 / (16 × 6.5) = 115384,误差约 0.16%,完全够用。
这就是为什么我前面建议主频配 96MHz 而不是 100MHz——96MHz 可以分频出 12MHz 的 PCLK,115200 波特率的除数漂亮,FDR 修正简单。如果主频是 100MHz,PCLK 分频出来的值都偏尴尬,要么误差大,要么 FDR 凑不出合适组合。这个取舍是例程里不会写但实际决定串口稳不稳的关键。
void UART0_Init(uint32_t baudrate) { // 使能 UART0 时钟:在 PCONP 寄存器 bit3 置 1 LPC_SC->PCONP |= (1UL << 3); // 先配置引脚复用 UART0_PinInit(); // 设置 UART0 外设时钟分频:CCLKSEL 之外还有 PCLKSEL0 // 这里把 PCLK 设为 CCLK 的 1/8,即 96MHz / 8 = 12MHz LPC_SC->PCLKSEL0 &= ~(0x03UL << 6); LPC_SC->PCLKSEL0 |= (0x02UL << 6); // 01 是 1/4,10 是 1/8 // 配置线控寄存器:8 位数据、无校验、1 位停止位 // DLAB 位置 1 才能访问 DLL/DLM LPC_UART0->LCR = 0x83; uint32_t pclk = 12000000; // 根据 PCLKSEL 实际分频结果 uint32_t dl = pclk / (16 * baudrate); // 整数除数 uint32_t rem = pclk - dl * 16 * baudrate; LPC_UART0->DLL = dl & 0xFF; LPC_UART0->DLM = (dl >> 8) & 0xFF; // FDR:根据余数选合适的 DivAddVal / MulVal // 这里 115200 对应 DivAddVal=1, MulVal=12 LPC_UART0->FDR = (12 << 4) | 1; // 关 DLAB,设置 LCR 为实际通信参数 LPC_UART0->LCR = 0x03; // 使能 FIFO,清空收发缓冲 LPC_UART0->FCR = 0x07; }代码里PCLKSEL0的 bit 位要对照手册确认,不同 UART 的 PCLK 分频位不一样。这里演示的是 UART0,如果用的是 UART2 或 UART3,PCLKSEL0/1 的位要重新查,这是例程里最容易抄错的部分——硬件功能一样,分频位却各不相同。
FDR 的计算在实际工程里不用每次都手算,可以把公式写进一个 Excel 或脚本,输入 PCLK 和目标波特率,直接得到 DLL/DLM/FDR。建议把常见组合记下来:12MHz PCLK 下,9600 波特率整数除数 78,115200 是 6,这两个值用得最多。
4.3 中断接收与发送缓存:例程里最容易盲抄的部分
串口初始化写完,大多数例程会提供一个发送字符串的函数,但接收部分才是真正需要理解的地方。中断接收不是只在 IER 里使能就结束,还涉及 NVIC 使能、接收 FIFO 触发深度、中断服务函数的正确命名。
volatile char rx_buffer[64]; volatile uint8_t rx_count = 0; volatile uint8_t rx_flag = 0; void UART0_IRQHandler(void) { uint8_t c; // 判断中断源:接收 FIFO 有数据 if (LPC_UART0->IIR & 0x04) { c = LPC_UART0->RBR; // 读数据,读空则中断自动清除 if (rx_count < 63) { rx_buffer[rx_count++] = c; } else { rx_flag = 1; // 溢出标志 } } } void UART0_InterruptEnable(void) { LPC_UART0->IER = 0x01; // 使能 RBR 接收中断 NVIC_EnableIRQ(UART0_IRQn); // NVIC 层使能,两个使能缺一不可 }这段代码里有个很关键的寄存器细节:IIR寄存器的 bit 0 是挂起标志,bit[3:1] 是中断标识,不能直接读IIR判断是接收中断还是发送中断。0x04 这个值对应的是接收数据可用中断。读RBR会自动清除接收中断标志,但一定要确保在中断服务函数里读,不能在主循环延时后读——FIFO 会继续收数据,读晚了会丢数据或产生过载错误。
NVIC 使能和外设 IER 使能是两级开关,很多例程只写了LPC_UART0->IER = 0x01,没写NVIC_EnableIRQ,结果中断永远不触发。反过来也有只使能 NVIC 但没配 IER 的,现象一样。排查中断问题必须先确认这两级都打开了。
中断服务函数的名字要和启动文件里的中断向量表严格一致。LPC17XX 的 UART0 中断向量在startup_LPC17xx.s里叫UART0_IRQHandler,如果你写成了Uart0_IRQHandler或UART0_Handler,编译不会报错(链接器找不到符号),但中断触发后会跳到默认的弱定义空函数里,表现就是程序死循环没反应。这个坑建议花时间看一次启动文件的向量表。
5. LPC17XX 例程避坑指南:5 个翻车现场与排查顺序
5.1 烧录一次后芯片“消失”:SWD 引脚被复用的连锁反应
现象:第一次烧录正常,第二次想下载程序时提示找不到目标设备,芯片像死了一样。
原因:LPC17XX 的 SWD 调试引脚是 P1.9(SWCLK)和 P1.10(SWDIO),如果你的代码里把这两个引脚复用成 GPIO 或其它外设功能,下载器就再也抓不住芯片。很多例程演示 GPIO 时用了 P1.9 或 P1.10,一旦烧进去,调试口就被切断了。
解决:把芯片的 ISP 引脚(P2.10)在复位时拉低,让芯片进入 ISP 模式,再用串口工具擦除整个 Flash,之后重新下载。擦除工具通过 UART0 的 P0.2/P0.3 通信,芯片只要没坏就能恢复。这个操作是 LPC17XX 用户的保命技能,建议看完这篇笔记就找张纸写下操作步骤贴在调试台。
另一个变通办法是在 MDK 里勾选 “Reset and Run” 或让下载器在连接前先发送复位信号,但 SWD 引脚被占时这种方法经常不灵。最稳妥的还 P2.10 拉低再上电,进入 ISP 模式后整片擦除。
5.2 串口输出乱码:PCLK 没算对,波特率纸面值全是幻觉
现象:串口助手里全是乱码,或每个字符都不对。
原因:波特率不对。明明是 115200 的配置,但实际发出的波特率是 912600 或者 38400,因为计算波特率时用的 PCLK 假设和实际不符。最常见的是改主频后忘了改 UART 分频,或者分频值填了寄存器但公式里用的还是老时钟。
解决:不要靠猜测,直接把 UART 的时钟配置拆开算。用示波器或逻辑分析仪抓 TX 引脚的电平宽度,数一位的脉宽,换算成实际波特率,再反向纠正。没有示波器的话,先发一个固定值比如0x55(二进制 01010101),观察波形规律——用逻辑分析仪是最快的。软件里靠 printf 重定向排查乱码是死路,因为乱码根本原因在外设时钟,不在库函数。
真的临时用的话,把 PCLK 分频改成 1/4 或 1/8 多试几个组合,总有能对上眼的,但这不是长久之计。把 PCLK 计算写在代码注释里,后面回来改主频时一眼就能看到关联关系。
5.3 中断服务函数写了却不执行:名字、优先级、NVIC 使能三连查
现象:主循环正常跑,外设事件也发生了,但中断服务函数里的断点永远不进。
原因:三个环节必有一个断了。第一,中断服务函数名字和启动文件向量表不匹配;第二,外设的 IER 或对应使能位没开;第三,NVIC_EnableIRQ 没调用或参数写错。优先级分组配置在默认状态下也能触发,但如果改过分组,中断可能被屏蔽。
解决:按固定顺序排查。先看启动文件里的向量名,再确认外设 IER 寄存器值,最后在调试里查看 NVIC 的 ISER 寄存器确认使能状态。一个实用技巧:在中断服务函数里第一行加一个全局标志位赋值,主循环里轮询这个标志位,能快速区分是中断没触发还是触发了没反应。
如果中断偶尔触发但不是每次,查外设的中断挂起标志是否被正确清除。以 UART 接收为例,读 RBR 后要从 IIR 读一次确认清除。有些外设要先把标志位写 1 清除再操作数据,写错了会导致中断风暴,表现为系统反复进中断,看起来像死机其实还在跑。
5.4 引脚输出一直是高/低电平:PINMODE 与 FIODIR 的常见混淆
现象:配置了输出,也拉了高,但万用表量引脚确认是低电平或悬空。
原因:引脚复用没治好。LPC17XX 所有引脚默认带内部上拉,你写 GPIO 输出但 PINSEL 还是默认的外设功能,内部上拉会把电平拉高,怎么拉低都拉不下去。反过来,如果 PINSEL 设成了 GPIO,但 PINMODE 设成了开漏或既无上拉也无下拉,输出高电平时引脚会浮空,量出来不是稳定高电平。
解决:写引脚之前强制把 PINSEL 对应位清 0,确认 GPIO 功能;再查 PINMODE 对应位的值。PINSEL 和 PINMODE 寄存器组是并列的,每个引脚两个寄存器都要检查。用万用表或示波器量引脚是见效最快的——先量能不能输出高低跳变,再回溯代码,别上来就改程序。
另外注意 LPC17XX 的 GPIO 结构体里还有FIOPIN,这是读引脚状态的寄存器,不是写引脚用的。有人想给引脚输出高电平直接写FIOPIN = value,结果把端口所有引脚都写了,表现非常诡异。
5.5 例程拷进自己工程就崩:SystemInit 重复调用与中断向量表冲突
现象:例程跑得好好的,复制到自己工程里,编译通过但上电不发启动,或者运行一段时间死机。
原因:复制工作遗漏了依赖文件。例程的 main 可能有自己的SystemInit()配置,你工程里 MDK 自动加的system_LPC17xx.c也有一份SystemInit(),两份定义直接链接冲突。还有的例程修改过中断向量表,你只复制了外围代码但没复制改动后的启动文件,中断映射对不上。
解决:复制工程文件时按依赖关系复制,不只是 .c 文件。最干净的做法是从原工程 export 整个目录,然后在 MDK 里删掉多余文件,而不是手动把 .c 文件拖进新工程。复制后第一步检查工程里有没有两个SystemInit警告,有就删掉其中一个。
还一个常见坑:例程里可能有自定义的_irq_handler改名宏,比如把TIMER0_IRQHandler重新映射到自己的函数名。只复制 .c 不复制头文件的宏定义,中断服务函数的绑定全断。这种问题编译不报错,运行行为却很诡异,排查时优先看启动文件向量表。
6. 例程跑通之后:一个调试习惯和一套非阻塞延时
6.1 调试器里的外设寄存器窗口比代码更诚实
程序跑起来后,别急着看代码逻辑,先看寄存器实际值。以 MDK 调试器为例,进入调试状态后打开 Peripherals 菜单,找到 GPIO 和 UART 对应项,能看到每个引脚的当前模式、方向、电平状态。比如点灯例程,LED 不亮时你看 GPIO1 的 FIOPIN,对应位是 1 还是 0,FIODIR 对应位是输出还是输入,一眼就知道初始化哪里断了。
这个习惯能省下大量猜代码的时间。软件层面的标志位、状态机都可能判断错,但寄存器值是硬件实际状态的直接映射。UART 乱码时看 UART0 的 LSR 寄存器和波特率分频寄存器,实际值和理论值对一下,问题就浮出水面了。我现在的习惯是:每启一个新外设,先跑起来看寄存器,确认硬件层面正确了,再优化代码逻辑。
6.2 SysTick 非阻塞延时代码与用法
点灯的 for 循环延时是死等,占着 CPU。真正常用的做法是用 SysTick 做毫秒计数,主循环里查时间戳决定动不动。LPC17XX 的 SysTick 是内核外设,不需要配置外设时钟,直接用。
volatile uint32_t uwTick = 0; void SysTick_Handler(void) { uwTick++; } void delay_ms(uint32_t ms) { uint32_t start = uwTick; while ((uwTick - start) < ms) ; } void SysTick_Init(void) { // SystemCoreClock 在 system_LPC17xx.c 里按实际主频定义 // 96MHz 时,每 1ms 触发一次中断 SysTick_Config(SystemCoreClock / 1000); }SysTick_Config是 CMSIS 提供的标准函数,传入重载值,自动配置 SysTick 并使能中断。注意SystemCoreClock的值必须和实际主频一致,如果没改system_LPC17xx.c,这个值会按默认 4MHz 算,延时就不准。用uwTick - start而不是直接比较大小比较,可以应付时间戳溢出的问题,这是嵌入式里的小习惯,建议保留。
这个延时骨架可以直接替换例程里的 all for 循环,配合同样基于时间戳的状态机,就能让点灯和串口任务并行跑。UART 接收也可以挂在这上面做超时判断,例如 10ms 没有新数据就算一帧结束。这套思路用在所有项目的初始化模板里,比每次从头写死等延时省心不少。
我自己现在的做法是:每拿到一份新例程,第一件事就是删掉所有“空转延时”,换成 SysTick 时间戳,再把外设寄存器初始化部分单独抽成函数。这一步做完,例程才真正变成自己能维护的工程。时间戳这种基本功,配几次项目就熟得很了,希望帮到你。
本文还有配套的精品资源,点击获取