简介:LPC1114是NXP基于ARM Cortex-M0内核的32位微控制器,最高主频60MHz,集成ADC、UART、I2C、SPI、GPIO等丰富外设,以低功耗、高性价比被广泛用于小型控制系统、物联网终端和消费电子产品。资源以《LPC1114芯片基础教程与应用实践》为主线,面向嵌入式初学者与进阶开发者,通过原理讲解和大量可直接运行的工程例程,帮助读者快速搭建开发环境并掌握常用模块编程。压缩包共692个文件,以C源码、头文件和Keil工程文件(uvproj/uvopt)为主,同时包含编译生成的axf、hex、lst、map文件以及PDF、TXT、HTML格式文档,整体仅6.61MB,便于离线查阅和工程对照。内容覆盖GPS、SPI液晶显示、PWM、NTC测温、旋转编码器、RGB灯、按键中断、UART串口等多类实验场景,并配有初始化配置、驱动库示例和开发工具链说明;从工程结构到编译下载均有完整演示,能有效降低入门门槛,也可支撑课程设计或小型项目二次开发。目前已有391人学习下载,对希望系统掌握Cortex-M0开发流程的读者颇具参考价值。
1. 从 axf 文件认识 LPC1114 的例程构成
拿到 LPC1114 例程包的时候,里面没有一长串 .c 文件,而是一堆 .axf 文件:LPC1114_PWM.axf、LPC1114_ADC.axf、旋转编码器.axf、UART.axf、LPC1114_RGB.axf、LPC1114_KEY_INT.axf。axf 是 Keil MDK 生成的 ARM 映像,包含调试信息和地址映射,每个外设都单独编译成了一个可烧录验证的版本。这意味着这套例程的学习路径是从现象往回找逻辑:先烧一个 PWM 看波形,再对照源码改占空比。LPC1114 是 NXP 的 Cortex-M0 微控制器,封装最小到 TSSOP28,主频在 50MHz 这一档,片内资源不多但引脚定义清晰,很适合用来把 GPIO、UART、ADC、PWM、SSP 这几类外设逐一跑通。下文按外设模块展开,每一章都给出寄存器级配置、参数选择和实际调试中容易踩的边界。
2. GPIO 与 KEY_INT 例程:按键中断的时钟陷阱与清中断顺序
第一次用 LPC1114 点灯,最容易遇到的问题就是“寄存器写了没反应”。原因不在 GPIO 本身,而在 SYSAHBCLKCTRL——LPC1114 把外设时钟集中在一个寄存器里管理,GPIO 和引脚复用模块 IOCON 的时钟默认都是关闭的。KEY_INT 例程之所以看起来简单,是因为它把Gpio_ClockEnable()写在了最前面,很多人把它当模板复制,却没理解为什么需要同时打开两个时钟位。下面从按键例程出发,把配置链路拆开。
2.1 先看 SYSAHBCLKCTRL,否则 GPIO 写不进
LPC1114 的SYSAHBCLKCTRL是系统控制寄存器,bit6 对应 GPIO 外设时钟,bit16 对应 IOCON 引脚配置时钟。GPIO 的方向、电平、中断寄存器都在 GPIO 外设里,IOCON 则控制引脚功能、上下拉和滞回。两个时钟缺少任何一个,都可能出现读取始终是 0、写方向位无效的怪现象。初始化代码很短,但顺序必须是先打开时钟,再写 IOCON。
void Gpio_ClockEnable(void) { // bit6=GPIO, bit16=IOCON SYSAHBCLKCTRL |= (1U << 6) | (1U << 16); }打开时钟之后,IOCON 寄存器的读写才生效。注意 LPC1114 的 GPIO 数据寄存器是寄存器组结构,GPIO1->DATA的高 16 位按地址线掩码访问,例程里如果直接操作GPIO1->DATA |= (1<<4)也是可以的,但更推荐在初始化时先把方向和初始电平写清楚,避免 GPIO 输出端产生多余跳变。
2.2 按键输入与 IOCON 上拉配置
按键通常一端接地,另一端接 MCU 引脚。KEY_INT 例程里我习惯把按键放在 PIO1_4:引脚配置成输入,内部上拉,按下时电平从高跳低,触发下降沿中断。如果不内部上拉,引脚悬空时电平会在高阻态附近抖动,中断会不断误触发。IOCON 的位 [3:4] 用来选择上拉/下拉,取0x2表示上拉。
#define KEY_PIN 4 void KEY_Init(void) { Gpio_ClockEnable(); // 功能0选择PIO1_4,位[3:4]=10 使能内部上拉 IOCON_PIO1_4 = (1U << 0) | (0x2U << 3); GPIO1->DIR &= ~(1U << KEY_PIN); // 方向为输入 GPIO1->IS &= ~(1U << KEY_PIN); // 边沿触发模式 GPIO1->IBE &= ~(1U << KEY_PIN); // 单边沿 GPIO1->IEV &= ~(1U << KEY_PIN); // 下降沿触发 GPIO1->IE |= (1U << KEY_PIN); // 使能中断 NVIC_EnableIRQ(EINT1_IRQn); // GPIO1 对应的中断 }这段代码把边沿选择、单双边沿、触发极性三个寄存器分开写,主要是为了可读性。LPC1114 的 GPIO 中断寄存器组包含IS、IBE、IEV、IE、RIS、MIS和IC。IS选择电平还是边沿,IBE决定是否双边沿,IEV选择高电平/上升沿还是低电平/下降沿。中断使能后,硬件会把挂起状态放在RIS,屏蔽之后的状态放在MIS。
下面是按键初始化的寄存器对照表,方便在调试器里对照。
| 寄存器 | 配置值 | 作用 |
|---|---|---|
| IOCON_PIO1_4 | (1<<0)|(0x2<<3) | PIO 功能、内部上拉 |
| GPIO1->DIR | bit4=0 | 输入方向 |
| GPIO1->IS | bit4=0 | 边沿触发 |
| GPIO1->IBE | bit4=0 | 单边沿 |
| GPIO1->IEV | bit4=0 | 下降沿 |
| GPIO1->IE | bit4=1 | 使能 GPIO1 引脚中断 |
| NVIC | EINT1_IRQn | 使能 Cortex-M0 中断 |
2.3 中断服务函数:先清挂起,再做业务
PIOINT1_IRQHandler是 GPIO1 的全部引脚共享的中断入口。进入中断后必须从GPIO1->MIS判断是哪一个引脚触发的,处理完业务后向GPIO1->IC写 1 清除挂起。如果不写IC,这个中断源会一直被置 1,CPU 退出中断后立刻再次进入,形成死循环一样的忙响应,主循环根本没有机会执行。
volatile uint8_t key_pressed = 0; void PIOINT1_IRQHandler(void) { // MIS 保存屏蔽后的挂起状态 if (GPIO1->MIS & (1U << KEY_PIN)) { GPIO1->IC = (1U << KEY_PIN); // 写1清除该中断 key_pressed = 1; // 只置标志,主循环里再处理 } }中断服务函数里只做“清中断 + 置标志”,这是 LPC1114 这类低主频 MCU 上最稳妥的写法。printf、LCD 刷屏这类耗时操作放进来,按键按下 100ms 时中断会占用较长时间,UART 接收和定时器中断都会受影响。更合理的做法是:主循环读到key_pressed为 1 后,启动一个 10ms 的软件定时器,再次确认电平仍为低,再做按键业务。
2.4 去抖与长按检测:不要在主循环里忙等
机械按键的抖动一般持续 5~20ms,常见做法是延时 10ms 再采样。但在按键中断里用for循环延时,等于把整个中断优先级拉低到轮询水平。我一般用一个 32 位定时器产生 1ms 时基,中断里只记录按键事件和首次触发时间,主循环里通过时间差判断是短按还是长按。这样做的好处是,旋转编码器和按键共享同一套时间基准,代码结构也更统一。
提示:GPIO0、GPIO1、GPIO2 分别对应 EINT0、EINT1、EINT2 中断源。不要误以为所有引脚都在同一个 NVIC 通道里。
3. UART 与 GPS 例程:波特率误差和 NMEA 按行解析
GPS 例程的入口是 UART.axf,本质上就是一个带 FIFO 的串口收发程序。LPC1114 的 UART0 支持 16 字节收发 FIFO,配合中断,可以在 9600 波特率下不丢字节地接收 GPS 模块的 NMEA 语句。GPS 数据是连续文本流,解析的难点不在串口,而在“按行切分”和“异常帧处理”。下面先从串口初始化说起。
3.1 UART 引脚复用与分频计算
UART0 在 LPC1114 上默认复用 PIO1_6 为 TXD、PIO1_7 为 RXD。IOCON 功能值选 1,然后把两个引脚从 GPIO 切换成串口功能。串口时钟来自 PCLK,分频寄存器是 DLM/DLL 和 FDR。DLAB 位在U0LCR的 bit7,需要先置 1 才能访问 DLM/DLL,设置完再清零。例程里常用的初始化函数是下面这种写法。
void UART0_Init(uint32_t baud) { SYSAHBCLKCTRL |= (1U << 12); // UART0 外设时钟 IOCON_PIO1_6 = 0x01; // 功能1: U0_TXD IOCON_PIO1_7 = 0x01; // 功能1: U0_RXD U0LCR = 0x83; // DLAB=1, 8数据位,1停止位,无校验 uint32_t div = SystemCoreClock / (16U * baud); U0DLL = div & 0xFF; U0DLM = (div >> 8) & 0xFF; U0LCR = 0x03; // 关闭DLAB U0FCR = 0x07; // 使能FIFO, 接收触发点1字节 NVIC_EnableIRQ(UART0_IRQn); }SystemCoreClock在启动文件里由SystemInit()赋值,如果使用内部 RC 再 PLL 到 48MHz,这个值通常是 48MHz。div计算的是 16 倍过采样后的分频值,所以 9600 波特率对应 48M/(16×9600)=312。若使用 11.0592MHz 晶振,计算出来的整数分频值对 9600 更友好;使用 48MHz 时误差在 0.2% 以内,GPS 模块通常能容忍。
下面是 48MHz 时钟下常用波特率的分频和误差情况。
| 波特率 | DLM/DLL 组合值 | 实际波特率 | 误差 |
|---|---|---|---|
| 9600 | 0x00 0x0138 | 9615 | +0.16% |
| 38400 | 0x00 0x004E | 38461 | +0.16% |
| 115200 | 0x00 0x001A | 115384 | +0.16% |
如果误差超过 2%,就要用U0FDR的小数分频寄存器补偿。U0FDR的高 4 位是 MULVAL,低 4 位是 DIVADDVAL,配置关系不是简单的除法,建议查手册的推荐表。多数例程省略了这一步,GPS 模块标准波特率是 9600,误差 0.16% 已经足够。
3.2 接收中断怎么不丢字节
使能 FIFO 后,到达触发点的字节会产生接收中断。把触发点设置为 1,可以保证 GPS 模块每次发一个字节就进一次中断,代价是中断频率高;设置为 4 或 8,则更省 CPU,但缓冲区分组处理更复杂。例程里 9600 波特率每字节约 1.04ms,用 1 字节触发点完全足够。
volatile char uart_line[128]; volatile uint16_t uart_len = 0; void UART0_IRQHandler(void) { // U0LSR 的 bit0 表示接收数据就绪 while (U0LSR & (1U << 0)) { char ch = (char)(U0RBR & 0xFF); if (ch == '\n') { // 一行结束,交给解析函数 if (uart_len > 0) { GPS_ParseLine((char *)uart_line, uart_len); uart_len = 0; } } else { if (uart_len < sizeof(uart_line) - 1) { uart_line[uart_len++] = ch; } else { uart_len = 0; // 超长数据直接丢弃 } } } }U0LSR是行状态寄存器,bit0 为 1 表示接收缓冲区有数据;U0RBR是接收缓冲寄存器。用while循环把所有可用数据读完,而不是只读一个字节,能避免同一时刻多字节进入 FIFO 时剩下几个触发一次新的中断,减少中断反复进出的开销。uart_len清零放在\n后面,注意把回车\r也过滤掉,GPS 模块每行以\r\n结尾,解析时要用\r做结束位置。
3.3 NMEA 解析函数:先校验,再取字段
NMEA 语句以$开头,*后跟两位校验和。校验和是$和*之间所有字符的异或值。处理 GPS 数据时如果只做字符串匹配,遇到校验错误的帧可能会得出错误的定位数据。我一般在解析前先运行校验函数,校验通过后再用逗号分隔字段。
uint8_t GPS_CheckSum(const char *line) { const char *p = line + 1; // 跳过 '$' uint8_t sum = 0; while (*p && *p != '*') { sum ^= (uint8_t)*p++; } if (*p != '*') return 0; // 没有校验段 uint8_t chk = (uint8_t)strtol(p + 1, NULL, 16); return sum == chk; } char *GPS_Field(char *line, int n) { while (n--) { line = strchr(line, ','); if (!line) return NULL; line++; } char *end = strchr(line, ','); if (end) *end = '\0'; return line; }GPS_Field每调用一次就把当前字段结尾的逗号替换成\0,所以不能直接在串口中断里连续调用多次,否则会把原始行缓冲区改坏。常见做法是先用校验函数判断,把行内容拷到临时数组,再在主循环或低优先级代码里调用GPS_Field。定位状态字段在$GPRMC的索引 2 位置,取出来判断是否为A,A表示定位有效,V表示无效。
// 调用示例:主循环中 if (GPS_CheckSum(line)) { char tmp[128]; strcpy(tmp, line); char *status = GPS_Field(tmp, 2); if (status && status[0] == 'A') { // 定位有效,继续解析经纬度 } }3.4 常见坑:波特率不一致和冷启动
GPS 模块启动后如果半天没有输出,先查串口回环:把 TX 和 RX 短接,自发自收。如果自己能收到自己发的字符,说明 UART 初始化没问题,问题在 GPS 模块的波特率配置。另外 GPS 冷启动第一次定位可能需要 30 秒到几分钟,例程里看到$GPGSV却一直没有$GPRMC是正常现象,不要急着改程序。$GPGSV是卫星视图,$GPRMC才是推荐定位数据。
4. ADC 与 NTC 测温:参考电压、分压电阻和软件滤波
ADC 例程在 LPC1114 上很典型:10 位转换器,12 个输入通道,支持单次和突发转换。和一个 10k NTC 热敏电阻搭配,就能做成温度采集。难点不在启动转换,而在参考电压的波动、NTC 的电阻-温度曲线以及转换结果里夹杂的高频噪声。这一章按“硬件分压→ADC 配置→软件滤波→温度换算”的顺序做。
4.1 ADC 时钟上限:CLKDIV 不是随便设的
LPC1114 的 ADC 时钟最高约 4.5MHz,如果直接用 48MHz 做转换,结果会不稳定。ADC 的CR寄存器的 bit8~15 是CLKDIV,作用是分频,分频后的时钟不能超过上限。48MHz 下设置 CLKDIV=10,ADC 时钟约 4.36MHz,符合要求。
void ADC0_Init(void) { SYSAHBCLKCTRL |= (1U << 13); // 使能 ADC0 时钟 // 引脚功能选择为 AD0,例如 PIO1_0 IOCON_PIO1_0 = 0x01; // 通道0, 分频11, 单次转换模式 ADC0->CR = (1U << 0) | (10U << 8); }SEL位 [0] 选择 ADC 通道。这里不使用BURST模式,每次读取时手动启动转换,写起来更直观。如果改用突发模式,ADC0->CR的 bit16 置 1 后 ADC 会持续扫描SEL选中的通道,结果不断写入对应的数据寄存器,主循环随时读取都是最近一次采样值,代价是功耗略高,低功耗应用里不建议。
4.2 从 ADC_DR 取 10 位结果
ADC 数据寄存器ADC0->DR[ch]的 bit31 是转换完成标志,bit6 到 bit15 是 10 位转换结果的有效位。也就是说,结果要右移 6 位再取低 10 位。如果直接读整个寄存器当作数据,会把状态位和溢出标志混进去,数值明显错误。
uint16_t ADC_ReadChannel(uint8_t ch) { ADC0->CR &= ~(0xFFU << 0); // 清除通道选择位 ADC0->CR |= (1U << ch); // 选择目标通道 ADC0->CR |= (1U << 24); // 启动单次转换 while (!(ADC0->DR[ch] & (1U << 31))); // 等待DONE return (ADC0->DR[ch] >> 6) & 0x3FF; // 取10位数据 }bit24 是软件启动转换位,在单次模式下每调用一次触发一次转换。while等待循环在最坏情况下会占用几十微秒,如果单片机主循环有严格时序要求,可以用中断方式读取,LPC1114 的 ADC 中断在 NVIC 中有独立通道。从数据手册的角度看,结果寄存器最多就是 10 位,不存在 12 位模式,看到 12 位寄存器模型的是 LPC15xx,别套用。
4.3 NTC 分压与温度换算
NTC 俗称负温度系数热敏电阻,温度升高电阻下降。把 NTC 和固定电阻串联在 3.3V 与地之间,ADC 引脚测量中点电压,就能根据阻值推算温度。例程里最常见的接法是固定电阻接 3.3V,NTC 接地,ADC 采样点在两个电阻之间。此时电压关系是:
V_adc = 3.3 * R_ntc / (R_fixed + R_ntc)
反推 NTC 阻值:
R_ntc = R_fixed * V_adc / (3.3 - V_adc)
在代码中可以用 ADC 码直接计算,省去先转电压的步骤。假设固定电阻也是 10kΩ,adc_code是 ADC 读数,则:
float NTCTemp_C(uint16_t adc_code) { float ratio = (float)adc_code / (1023.0f - adc_code); float r_ntc = 10.0f * ratio; // 固定电阻10kΩ float ln_r = logf(r_ntc / 10.0f); // 相对25度的对数比 float t = 1.0f / ((1.0f / 298.15f) + (1.0f / 3950.0f) * ln_r); return t - 273.15f; }B 值公式是 25℃标称电阻和 B 常数组成的经验公式:T = 1 / (1/T0 + (1/B) * ln(R/R0)) - 273.15。这里 T0 是 298.15K,R0 是 10kΩ,B 取 3950,是常见 10k NTC 的参数。该公式在 -20℃ 到 80℃ 范围误差可以控制在 ±1℃ 左右,超过该范围建议改用 Steinhart-Hart 三参数查表。
下面是用上述公式计算的典型 ADC 码值,前提是 10 位 ADC、3.3V 参考电压、固定电阻 10kΩ。
| 温度 | NTC 电阻 | ADC 码 | 误差 |
|---|---|---|---|
| 0℃ | 32.6kΩ | 约 780 | ±0.5℃ |
| 25℃ | 10.0kΩ | 512 | 0℃ |
| 50℃ | 3.60kΩ | 272 | ±0.8℃ |
如果实际硬件里固定电阻不是 10kΩ,表格里的 ADC 码要重新计算。公式里的1023.0f是满量程码,换成 4095 就是 12 位 ADC 场景,这行代码可以直接复用到其他设备。
4.4 软件滤波:移动平均和阈值判断
ADC 值在读数稳定后仍会有 1~2 个码的随机噪声,直接参与温度计算会让最终温度在 0.1℃ 附近抖动。最简单的处理是移动平均滤波,保存最近 N 次采样值,每次取平均。注意整个缓冲区是无符号 16 位,求和变量至少是uint32_t,否则 8 个 1023 累加就会溢出。
#define ADC_WINDOW 8 uint16_t adc_window[ADC_WINDOW]; uint8_t adc_widx; uint16_t ADC_Filter(uint16_t new_sample) { adc_window[adc_widx] = new_sample; adc_widx = (adc_widx + 1) % ADC_WINDOW; uint32_t sum = 0; for (int i = 0; i < ADC_WINDOW; i++) { sum += adc_window[i]; } return (uint16_t)(sum >> 3); // 8 个样本求平均 }窗口越大输出越平滑,但对温度变化的响应越慢。例程里测试 4 到 8 个样本效果都不错;如果系统里有 50Hz 工频干扰,最好让采样间隔取 20ms 的整数倍,让滤波窗口能覆盖一个完整工频周期。
5. PWM 与旋转编码器例程:用定时器和 GPIO 中断各管一头
LPC1114_PWM.axf 和旋转编码器.axf 都是“边沿敏感”型外设。PWM 要对输出波形精确控制,靠的是 CT32B0 定时器的匹配寄存器;旋转编码器则是连续输出的机械脉冲,必须靠 GPIO 中断和软件去抖。这两类例程放在一起看,恰好能说明为什么单片机上定时器和外部中断不能互相替代。
5.1 用 CT32B0 产生三路 PWM
LPC1114 的 32 位定时器 CT32B0 有三个匹配输出通道,例程里 MR0 做周期基准,MR1 和 MR2 做占空比。配置要点是:先设置周期,再设占空比,然后把匹配输出使能到引脚上。顺序反了,引脚在初始化到一半时会出现异常电平。
void RGB_PWM_Init(uint32_t freq) { SYSAHBCLKCTRL |= (1U << 9); // CT32B0 时钟 // 引脚复用为定时器输出,具体功能号见 LPC1114 数据手册 IOCON_PIO0_8 = 0x02; // CT32B0_MAT0 IOCON_PIO1_1 = 0x02; // CT32B0_MAT1 IOCON_PIO1_2 = 0x02; // CT32B0_MAT2 uint32_t period = SystemCoreClock / freq - 1; TIMER32_0->MR0 = period; // 周期 TIMER32_0->MR1 = period / 3; // 通道1占空比 33% TIMER32_0->MR2 = period / 2; // 通道2占空比 50% TIMER32_0->MCR = (1U << 1); // MR0 匹配后计数器复位 TIMER32_0->PWMCON = (1U << 1) | (1U << 2); TIMER32_0->TCR = 1; // 启动定时器 }MCR的 bit1 由匹配事件控制复位计数器,使得 PWM 周期等于 MR0;PWMCON 寄存器里位 1 和位 2 分别使能 MR1、MR2 作为 PWM 输出。注意:MR0 本身不在这两个 bit 中,不要写成(1U << 0)。
运行时修改TIMER32_0->MR1即可改变占空比。但需要避免一个极端值:如果 MR1 大于 MR0,输出就恒定高电平。应用里最好做一次限幅,类似duty > period ? period : duty,防止逻辑错误导致 LED 全亮或电机全转。
5.2 RGB 混色的参数调整
三路 PWM 分别控制红、绿、蓝的亮度。同一个 RGB LED 的三个颜色正向压降不同,如果三个通道用同一个占空比,色温会偏。适合用下面的参数表先手工校准:
| 通道 | 典型串联电阻 | 100% 占空比电流 | 视觉亮度系数 |
|---|---|---|---|
| R | 150Ω | 15mA | 0.3 |
| G | 100Ω | 15mA | 0.7 |
| B | 100Ω | 15mA | 0.5 |
不同 LED 的亮度系数差异较大,以上只是初始值。RGB 例程里常看到“渐进呼吸灯”效果,用MR1的值从 0 加到 period,再加回去。这个过程如果直接在主循环里 delay 会卡掉其他任务,建议放到 1ms 定时器中断里逐步更新。
5.3 旋转编码器:用 A 相边沿判断 B 相电平
机械旋转编码器的输出 A/B 是正交方波,旋转方向不同,A 相上升沿到达时 B 相的电平不同。LPC1114 没有专用正交解码器,例程用的方法是:A 相接入 GPIO0 的某引脚,上升沿触发外部中断,在中断里读 B 相电平。若高电平则为正转,低电平为反转,或者相反,取决于接线顺序。
volatile int32_t encoder_position = 0; #define ENC_A_PIN 2 #define ENC_B_PIN 3 void PIOINT0_IRQHandler(void) { if (GPIO0->MIS & (1U << ENC_A_PIN)) { GPIO0->IC = (1U << ENC_A_PIN); // 上升沿时检查另一相 if (GPIO0->DATA & (1U << ENC_B_PIN)) { encoder_position++; } else { encoder_position--; } } }这段代码把方向判定压在一次中断里,速度很快。但要注意,GPIO0 所有引脚的中断都走 EINT0,A 相和 B 相如果同时接了不同引脚,中断里不能只清 A 相而忽略 B 相的挂起。另一个问题是机械触点抖动:每次旋转可能让 A 相在边沿附近抖动 5ms,导致位置计数一次增加多格。工业做法是外接 100nF 电容,软件上则在进入中断后延迟 1ms 再读 B 相,或者读取多次取一致结果。
5.4 中断优先级与慢外设的冲突
旋转编码器和 UART 同时使用中断时,要思考优先级分配:编码器若设置为高优先级,中断服务里应尽快清中断、改计数器、退出;如果把 UART 接收也放在同样优先级,两个中断交错时可能丢字符。LPC1114 的 NVIC 支持可编程优先级,经验是把读取频率高、处理时间短的旋转编码器设为高优先级,UART 设为低优先级,UART 用 FIFO 缓冲,即使短暂延迟也不会立即丢数据。
提示:不要在旋转编码器的中断里调用
printf或 LCD 刷新函数。一次编码器旋转只产生十几个脉冲,看起来不影响;但连续快速旋转时,中断占用会超过主循环时间,CPU 基本卡在中断里。
6. SPI LCD 与片内 Flash:把例程烧进 LPC1114 的最后一公里
SPILCD.axf 和 LPC1114_Flash.axf 是例程包里比较接近“成品”的两个。前者把 SSP0 配成 SPI 主模式,驱动一块液晶屏;后者演示了 LPC1114 的片内 Flash 如何通过 IAP 自编程。两者合在一起正好覆盖了“刷屏”和“保存参数”这两个产品功能的标配。
6.1 SSP0 主模式的配置
LPC1114 的 SSP0 可以工作在 SPI 模式,例程里通常用 8 位数据长度,时钟相位和极性看 LCD 控制器的手册。SPI 驱动 LCD 的常见参数是:数据帧 8 位,SPI 模式 0 或 3,时钟频率低于 20MHz。初始化的重点有两个:SSP0->CPSR是分频值,必须为偶数;SSP0->CR0同时设置了帧格式和时钟极性。
void SSP0_SPI_Init(void) { SYSAHBCLKCTRL |= (1U << 20); // SSP0 时钟 IOCON_PIO0_15 = 0x01; // SCK IOCON_PIO0_17 = 0x01; // MOSI IOCON_PIO0_16 = 0x01; // MISO(不读回时也要配) SSP0->CPSR = 12; SSP0->CR0 = (7U << 0) | (0U << 6); // 8位, SPI 模式 0 SSP0->CR1 = (1U << 1); // 主机模式 }CPSR=12时 48MHz 时钟得到 4MHz SPI,对大多数 LCD 控制器足够。如果刷屏卡顿,试着把CPSR调到 4 或 2,前提是确认 LCD 控制器能承受。例程里的 LCD 驱动一般会提供SPI_Write函数,发送前等待SSP0->SR的 bit1 为 1,表示发送 FIFO 为空。
6.2 片内 Flash 的 IAP 调用
LPC1114 的片内 Flash 支持串口 ISP,也可以在程序里调用 IAP 命令。IAP 入口地址是 Boot ROM 中的固定位置0x1FFF1FF1,调用方式是通过函数指针。命令数组的第一个元素是命令码,返回状态在结果数组的第一个元素,0表示成功。写 Flash 前必须先准备扇区,再擦除,最后写入。
typedef void (*IAP_Func)(uint32_t[], uint32_t[]); #define IAP_ENTRY ((IAP_Func)0x1FFF1FF1) void Flash_Write_Sector(uint32_t addr, uint32_t *buf, uint32_t len) { uint32_t cmd[5], res[4]; cmd[0] = 50; // 准备写扇区 cmd[1] = addr; cmd[2] = 0; IAP_ENTRY(cmd, res); if (res[0] != 0) return; cmd[0] = 51; // 擦除扇区 cmd[1] = addr; cmd[2] = 1; IAP_ENTRY(cmd, res); if (res[0] != 0) return; cmd[0] = 52; // 写Flash cmd[1] = addr; cmd[2] = (uint32_t)buf; cmd[3] = len; IAP_ENTRY(cmd, res); }IAP 调用时 CPU 从 Flash 正常运行,命令由 Boot ROM 中的程序执行。要注意:写入目标和当前正在执行的 Flash 扇区在物理上不能是被擦除的那个扇区,否则指令取指会失败。常见做法是应用程序放在 0x0000 区域,用户参数放在最后 8KB,两个空间互不重叠,这样 Flash 例程每次修改参数时不会把包含中断向量表的 0 扇区擦掉。
6.3 用 axf 验证最终工程
Keil MDK 生成的 axf 文件可以用fromelf转成 hex 或 bin,也可以直接通过调试器下载。SPILCD 例程跑黑屏时,先检查 SSP0 的引脚功能对不对,再用逻辑分析仪看 SCK 上有没有时钟;片内 Flash 例程写不进数据时,先用串口工具发 ISP 命令,确认 Boot ROM 能正常访问 flash。最终固件如果要从 Flash 启动,把 IAP 入口放在启动文件末尾,不要被链接器的--remove优化掉。
本文还有配套的精品资源,点击获取