简介:一份基于MSP430F5529微控制器的按键简易计算器工程资源,适合正在学习MSP430系列或嵌入式开发的新手,也适合需要快速上手OLED显示与按键交互的物联网爱好者。该工程使用两个按键分别实现数字加一、减一操作,并在128x64 OLED上实时显示,同时通过计数值的奇偶性控制左右LED灯,综合运用了GPIO中断、IIC通信、SSD1306驱动等知识。资源包共27个文件,工程结构完整,包含C源码、头文件,以及IAR环境下工程配置、调试链接所需的.ewp、.ewd、.xcl等辅助文件,整体仅557KB,体积小巧,便于下载和移植。压缩包内工程结构清晰,可直接查看并对照学习按键防抖处理、OLED绘图函数、奇偶判断LED逻辑等关键代码,理解从硬件配置到上层显示的全部流程。该项目已被1575人浏览学习,适合作为单片机课程设计或毕业设计的实践参考。
1. 按键简易计算器与 MSP430F5529 的四个调试门槛
在 51 单片机上半天就能跑通的矩阵键盘计算器,换到 MSP430F5529 上往往会多耗一个周末。原因不是 TI 这颗芯片慢,而是它的时钟树、GPIO 复用、低功耗模式和 16 位 int 默认宽度,把“扫按键、做运算、刷显示”这件小事拆成了四个独立决策点。F5529 的 int 只有 16 位,计算器随便按几下就溢出;LCD1602 的初始化时序和低功耗唤醒又会互相干扰;printf 输出不像开发板那样即插即用,调试手段也得换。这篇文章按我实际调这套东西的路线展开:先用矩阵扫描原理定硬件连接,再给定时器消抖和按键状态机,然后是整数运算核心,最后落到 LCD1602 驱动和 LPM3 低功耗改造。所有代码用寄存器配置,不绑定特定库,移植到其他 MSP430 型号也只需要改引脚宏。
2. 矩阵键盘扫描原理与 MSP430F5529 引脚配置
2.1 行列扫描为什么是行输出、列输入
4x4 键盘如果每个按键独占一个 GPIO,需要 16 个引脚,F5529 虽然有 63 个 IO,但这样接线对计算器来说太浪费。行列扫描只用 8 个引脚:4 根行线、4 根列线。行线配置为推挽输出,逐行拉低;列线配置为输入并开启内部上拉。当某个按键被按下,该行输出低电平会通过按键短接到对应列,那一列的电平就被拉低。
这里有个容易搞反的点:为什么不反过来,列输出、行输入?两种方式理论上都能工作,但实际接法里,行线上经常要同时挂数码管位选之类的功能,列线留给输入更有利于布线。更重要的是,输入引脚用内部上拉可以从硬件上保证未按下时读到确定的高电平,不需要外加上拉电阻。F5529 的 P1 和 P2 都带内部上拉使能寄存器 PxREN,这比外接排阻省事得多。
2.2 引脚分配与 F5529 选脚依据
我这个方案里行线用 P1.0 到 P1.3,列线用 P2.0 到 P2.3,按键布局如下表。
| 行\列 | P2.0 | P2.1 | P2.2 | P2.3 |
|---|---|---|---|---|
| P1.0 | 7 | 8 | 9 | / |
| P1.1 | 4 | 5 | 6 | * |
| P1.2 | 1 | 2 | 3 | - |
| P1.3 | C | 0 | = | + |
选 P1 和 P2 而不选 P3 到 P8,主要原因是 P1、P2 支持三个特性:内部上拉 REN、每个引脚独立方向控制、以及引脚中断。前两个是所有 IO 都有的,但引脚中断只有 P1 和 P2 具备。虽然这套代码最终用定时器扫描而不是 GPIO 中断,但保留引脚中断选项,后面想改成中断唤醒方案不需要重新布线。另外 P1、P2 的多路复用功能相对少,至少 P1.0-P1.3 和 P2.0-P2.3 默认都是 GPIO,不用和 UART、定时器捕获等功能抢引脚。
2.3 GPIO 方向、上下拉与初始化代码
F5529 的 GPIO 初始化有个顺序讲究:先设输出值,再使能上拉,最后切方向。顺序反了,在使能上拉的瞬间引脚会短暂悬空,可能产生一次虚假的下降沿。下面是完整初始化代码。
#define ROW_PORT P1OUT #define ROW_DIR P1DIR #define COL_IN P2IN #define COL_DIR P2DIR #define COL_REN P2REN #define COL_OUT P2OUT void keypad_init(void) { P1SEL &= ~0x0F; /* P1.0-P1.3 设为 GPIO,关闭复用功能 */ P1DIR |= 0x0F; /* 行线设为输出 */ P1OUT &= ~0x0F; /* 行线默认输出低电平 */ P2SEL &= ~0x0F; /* P2.0-P2.3 设为 GPIO */ P2OUT |= 0x0F; /* 先拉高列线输出值 */ P2REN |= 0x0F; /* 再使能内部上拉电阻 */ P2DIR &= ~0x0F; /* 最后把列线切为输入 */ }代码里四个关键点:P1SEL 和 P2SEL 清零是解除引脚复用,F5529 上 P2.0-P2.3 可能被映射为 UCA0 的收发或者 TA 的捕获输入;P1OUT 先清零再置方向,是为了避免切方向瞬间输出引脚给出不确定电平;列线方向切输入之前,先让 P2OUT 为 1 再使能 REN,这是 TI 手册推荐的上拉电阻开启顺序,否则外部浮空会引入额外功耗和误触发;方向寄存器最后一步修改,保证整个初始化过程不会出现 IO 短路。
2.4 按键码生成与去重
扫描函数逐行拉低,读回列线状态,把行列位置翻译成按键字符。这样计算器上层拿到的是一个 0 到 15 的键号,再查表得到符号,而不是直接用引脚状态参与逻辑。
unsigned char keypad_scan(void) { unsigned char row, col, code = 0xFF; static const unsigned char keymap[16] = { '7','8','9','/', '4','5','6','*', '1','2','3','-', 'C','0','=','+' }; static const unsigned char rowlow[4] = {0x0E, 0x0D, 0x0B, 0x07}; for (row = 0; row < 4; row++) { ROW_PORT = (ROW_PORT & ~0x0F) | rowlow[row]; _delay_cycles(5); /* 留出电平建立时间 */ col = COL_IN & 0x0F; if (col != 0x0F) { if (col == 0x0E) code = keymap[row * 4 + 0]; else if (col == 0x0D) code = keymap[row * 4 + 1]; else if (col == 0x0B) code = keymap[row * 4 + 2]; else code = keymap[row * 4 + 3]; break; } } return code; /* 没有按键时返回 0xFF */ }rowlow 数组里的 0x0E 表示 P1.0 输出 0,其余行线输出 1;0x0D 是 P1.1 为 0,依此类推。判断列码时用 0x0E、0x0D、0x0B、0x07 四个值定位具体是哪一列,而不是位运算找零的位置,因为一次多个键按下时位运算会返回错误位置。_delay_cycles(5) 是为了等行线电平经过按键触点和 PCB 走线到达列输入,扫描频率是 8ms 一个周期,这 5 个周期约 200ns 的延时完全可以忽略。
提示:键号到按键值的查表要放在扫描函数外面也可以,但放在里面能让上层只认字符,状态机代码可读性高很多。
3. 用 TA0 定时器中断做按键消抖与扫描轮询
3.1 为什么不用 delay 消抖
最简单的消抖是检测到按键后 delay 20ms 再读一次,机械抖动通常不超过 10ms,这个办法在点个灯的场景够用。但在计算器里不行:需要驱动 LCD1602,LCD 写一次地址和数据要几十微秒;如果后续还要做低功耗,主循环会进入 LPM3,delay 期间芯片根本没有办法保持睡眠再准时醒来。更关键的是,delay 消抖把 CPU 空转掉,消抖期间任何其他外设事件都没法处理。
所以正确做法是定一个固定扫描周期,在定时器中断里周期性地读键盘,用连续多次读到相同电平来确认按键。这个方案天然不阻塞,而且扫描周期本身就是一个时间基准,消抖计数、长按处理、轮询 LCD 刷新都能挂在同一个节拍上。
3.2 TA0 配置:ACLK 32.768kHz 与 8ms 周期
MSP430F5529 的 TA0 是一个 16 位定时器,选择 ACLK 作为时钟源,外部低频晶振 32768Hz。CCR0 设 262,中断周期为 32768 除以 262,约等于 8ms,这个数值避开了机械抖动主频段,又不会让按键手感变迟钝。
void timer_init(void) { TA0CCTL0 = CCIE; /* 使能 TA0 比较/捕获 0 中断 */ TA0CCR0 = 262; /* 32768 / 262 ≈ 125Hz,周期约 8ms */ TA0CTL = TASSEL_1 | MC_1; /* ACLK,向上计数模式 */ }TASSEL_1 是选择 ACLK,MC_1 是向上计数模式。为什么不用 SMCLK?F5529 的 SMCLK 默认来自 DCO,DCO 频率受温度和电压影响会漂移,更关键的是芯片进入 LPM3 低功耗模式后 SMCLK 会停振,只有 ACLK 继续运行。用 ACLK 驱动定时器,意味着低功耗模式下定时器依然能唤醒 CPU 扫描键盘,这在第六章会用上。中断周期参数可以按手感在 200 到 300 之间调,太小消抖没意义,太大按键会感觉要按很久才有反应。
3.3 三态消抖状态机
消抖最可靠的做法是状态机,而不是简单延时。这里用三个状态:空闲、确认中、按下。空闲状态检测到按键进入确认中,确认中连续两次扫描都读到同一个键才判定为真实按下;如果中途读到无按键,立即回到空闲,不累计之前的计数。
typedef enum { KEY_UP, KEY_CONFIRM, KEY_DOWN } key_state_t; static key_state_t kstate = KEY_UP; static unsigned char confirm_count = 0; static unsigned char press_code = 0xFF; void keypad_periodic(void) /* 每 8ms 由 TA0 中断调用一次 */ { unsigned char code = keypad_scan(); switch (kstate) { case KEY_UP: if (code != 0xFF) { confirm_count = 1; kstate = KEY_CONFIRM; } break; case KEY_CONFIRM: if (code == 0xFF) { kstate = KEY_UP; /* 抖动掉了,撤销确认 */ } else if (++confirm_count >= 2) { press_code = code; kstate = KEY_DOWN; on_key_pressed(code); /* 只上报按下事件 */ } break; case KEY_DOWN: if (code == 0xFF) { press_code = 0xFF; kstate = KEY_UP; /* 释放,恢复待按键状态 */ } break; } }确认状态里连续两次读到相同按键才输出事件,中间读回 0xFF 就回退到空闲,这是消抖状态机和延时消抖最大的差异:延时方案在延时结束后只读一次,结果不可靠;状态机每一次扫描都在参与判断,抖动期间的错误电平不会累积成误触发。释放检测单独对应 KEY_DOWN 状态,计算器只需要按键事件,但保留释放分支是给长按清零这类扩展功能留的位置。
3.4 中断里的逐行扫描与开销控制
keypad_scan 每次会把四行全部扫一遍。在 8ms 周期里,四行各设置一次 ROW_PORT、各读一次 COL_IN,总共大约 40 到 60 个 CPU 周期,在 25MHz 主频下不到 3 微秒,占 8ms 周期的比例可以忽略。所以没必要一开始就做逐行分时扫描,先保证功能正确。
如果后续扫描代码变重,比如加入了按键连发检测或低功耗下的唤醒统计,再改成逐行扫描:定义静态变量 scan_row,每次中断只扫一行,四轮扫完一圈。这样一个中断里只有一次端口写和一次端口读,CPU 占用进一步降低,代价是按键事件延迟变的不是固定 8ms,而是 8ms 到 32ms 之间浮动。计算器这种交互对这么点延迟完全无感,我一般在调好功能后会切到这种方式。
3.5 定时器扫描和 GPIO 中断方案的取舍
F5529 的 P1、P2 支持引脚上升沿或下降沿中断,有人会把四根列线接 P2 中断,按下时下降沿唤醒,然后在中断里轮询行线找出按键位置。这个方案在响应速度上有优势,但也有三个明显的坑:一是列线共享,两个键同时按下时中断标志会被吞掉;二是消抖还是得靠软件,中断触发后要延时再确认,低功耗下延时方案很难写对;三是释放检测需要切换中断沿方向,状态逻辑比定时器扫描复杂得多。下表是两个方案的对比。
| 对比项 | 定时器轮询扫描 | GPIO 沿中断 |
|---|---|---|
| 消抖实现 | 状态机计数,天然可靠 | 中断后仍需软件延时确认 |
| 多键同按 | 按行扫描可识别部分组合 | 列共享,第三个键丢失 |
| 低功耗配合 | ACLK 唤醒,逻辑简单 | 可睡眠,但后续确认要保留唤醒 |
| 代码复杂度 | 一个状态机 + 一个定时器 | 沿配置 + 消抖延时 + 释放检测 |
结论是定时器轮询更适合按键计算器这种低速、结构固定的人机交互。GPIO 中断的优势在低功耗场景,但第六章仍然用定时器实现了 LPM3,所以中断方案唯一的优势也被抵消了。
4. 简易计算器的整数运算状态机与连续等号
4.1 为什么坚持用 long 而不是 float
MSP430F5529 没有硬件浮点单元,float 的加减乘除全靠编译器链接软浮点库,一段乘法会展开成几十条指令,对计算器这不算大问题。真正的问题是 float 的精度:0.1 在二进制里是无限循环小数,累加几次后屏幕上会出现 0.9999999 这类结果,而计算器恰恰是三行代码就要显示一次结果的应用。
所以运算核心全部用 long。F5529 的 long 是 32 位有符号,范围正负 21 亿,超出 LCD1602 能显示的 8 位数字范围,够用。如果以后要做带小数的计算,把数值放大 10 倍或 100 倍用定点数存,显示时手动插小数点,运算逻辑完全不用变。int 在这个芯片上是 16 位,最大值才 32767,按两下乘法就溢出了,一定不要用 int 存操作数。
4.2 运算状态机的变量设计
计算器需要记住的不只是当前屏幕上的数字,还有上一次的运算结果、待执行的运算符、以及连续等号要重复的第二个操作数。我把这些拆成几个全局变量,在按键事件处理函数里统一操作。
static long calc_acc; /* 累积值,按下运算符前的数值 */ static char calc_op; /* 待执行的运算符,0 表示没有 */ static long disp; /* 当前显示值,也是数字键累积目标 */ static char last_calc_op; /* 最近一次实际执行的运算符 */ static long last_calc_b; /* 最近一次等号实际使用的第二操作数 */ static char waiting; /* 1 表示等待输入新数字 */ static char err_flag; /* 1 表示溢出或除零错误 */waiting 标志是这个状态机里最重要的一位。按下数字键时,如果 waiting 为 1,说明当前显示的数字应该是上一次运算的结果,而不是用户正在输入的新的数,需要把屏幕清零重新累积。按下运算符时,如果 waiting 为 0,说明用户是先输入数字再按运算符,这时才需要把屏幕上的数存进 calc_acc;如果 waiting 已经是 1,说明用户连续按了两次运算符,直接覆盖运算符即可。
4.3 数字输入与运算符结算的完整实现
按键事件到达 on_key_pressed 后,先按字符分类处理。数字键做显示值累积,运算符做结算和状态切换,等号触发实际运算。
static long do_calc(long a, char op, long b) { switch (op) { case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': if (b == 0) { err_flag = 1; return 0; } return a / b; } return b; }void on_key_pressed(unsigned char key) { if (err_flag) { if (key == 'C') { /* 只有清除键能解除错误 */ err_flag = 0; disp = 0; calc_acc = 0; calc_op = 0; waiting = 1; last_calc_op = 0; } return; } if (key >= '0' && key <= '9') { if (waiting) { disp = 0; waiting = 0; } if (disp > 999999L) { /* 超过显示上限直接置错 */ err_flag = 1; return; } disp = disp * 10 + (key - '0'); } else if (key == '+' || key == '-' || key == '*' || key == '/') { if (!waiting) { /* 当前屏幕数是完整操作数 */ if (calc_op) { disp = do_calc(calc_acc, calc_op, disp); } calc_acc = disp; } calc_op = key; last_calc_op = 0; /* 按运算符打断连续等号 */ waiting = 1; } else if (key == '=') { if (last_calc_op && waiting) { /* 连续等号:对当前结果显示的值再执行一次相同运算 */ disp = do_calc(disp, last_calc_op, last_calc_b); } else if (!waiting) { if (calc_op) { last_calc_b = disp; last_calc_op = calc_op; disp = do_calc(calc_acc, calc_op, disp); calc_acc = disp; calc_op = 0; } } waiting = 1; } else if (key == 'C') { disp = 0; calc_acc = 0; calc_op = 0; waiting = 1; last_calc_op = 0; } }数字分支里 disp 先判断是否超过 999999,防止输入 8 位以上的数字在显示层溢出,也避免后续乘法导致 long 溢出。运算符分支中if (!waiting) { if (calc_op) { ... } }这个嵌套的含义是:连续输入“123 + 456 +”,第二次按加号时要先把 123 加 456 结算,再把结果作为新的累积值,这就是计算器“按运算符即结算”的行为。等号分支分成两种情况:last_calc_op && waiting对应“10 + 5 = = ”里的第二个等号,此时屏幕上是上次结果 15,直接拿 15 再执行一次加 5 得到 20;!waiting对应正常的“操作数 运算符 操作数 等号”结算。
连续等号的逻辑值得单独说一下:第一次按等号时,把当前屏幕数存进 last_calc_b,执行运算后把运算符和这个操作数保存下来;之后每次再按等号,都把当前显示值当作左操作数,重复执行保存的运算。第三个数 20 再按一次等于 25,符合常见计算器行为。
4.4 除法、取负与溢出边界
除法在 do_calc 里检查除数为零,返回 0 并置 err_flag。取负运算在简易键盘上通常用一个单独的 +/- 键,这里没画进 4x4 布局,如果要用可以把 C 键旁边空位改成 +/-,处理逻辑是disp = -disp,不需要改变状态机。
乘法最容易溢出,比如 9999999 乘以 9999999 结果接近 1e14,long 直接溢出变成负数。所以每次运算完成后要检查结果是否超出 -9999999 到 9999999 范围,超出就置 err_flag。这个检查放在 do_calc 调用完成后、disp 更新之前统一做一次:
if (disp > 9999999L || disp < -9999999L) { err_flag = 1; disp = 0; }注意:err_flag 置位后,所有按键除 C 外都被忽略。这是计算器必须的行为,不然错误状态下按数字键会把无效结果显示成新输入,用户会以为是运算结果。
5. LCD1602 四线驱动与计算器显示刷新
5.1 显示方案选型
计算器结果显示最常见的两种方案是八位数码管加 74HC595 和 LCD1602 模块。数码管亮度高、代码也简单,但 F5529 的 IO 够用,没必要引入 595;更重要的是数码管显示负号和错误提示需要自己拼段码,而 1602 可以直接写 ASCII 字符。我选 LCD1602 四线模式,数据线只用 P3.0-P3.3 四根,控制线用 P3.4 的 RS 和 P3.5 的 E。硬件上注意 LCD 模块的背光引脚要串限流电阻,不要直接接 3.3V 电源,按键和 LCD 的走线尽量分开,避免扫描信号耦合到显示数据线上。
5.2 四线写字节时序与代码
四线模式一次只传 4 位数据,先发高 4 位再发低 4 位,E 引脚产生一个高脉冲,数据在下落沿被锁存。下面是核心写函数。
#define LCD_D4 0x01 #define LCD_D5 0x02 #define LCD_D6 0x04 #define LCD_D7 0x08 #define LCD_RS 0x10 #define LCD_E 0x20 void lcd_write_nibble(unsigned char nib) { P3OUT = (P3OUT & ~0x0F) | (nib & 0x0F); /* 送低四位数据 */ P3OUT |= LCD_E; /* E 拉高 */ _delay_cycles(50); /* E 高电平保持,约 2us@25MHz */ P3OUT &= ~LCD_E; /* E 拉低,数据被锁存 */ _delay_cycles(50); /* 指令执行时间 */ } void lcd_write_byte(unsigned char dat, unsigned char rs) { if (rs) P3OUT |= LCD_RS; else P3OUT &= ~LCD_RS; lcd_write_nibble(dat >> 4); /* 先发高四位 */ lcd_write_nibble(dat & 0x0F); /* 再发低四位 */ }_delay_cycles(50) 在 25MHz 下大约是 2 微秒,保证 E 高电平宽度超过 HD44780 要求的 450ns 最小脉宽。如果用的是 IAR 或 GCC 的 __delay_cycles,参数单位是 CPU 指令周期,更换主频后要重新算。四线模式下每次写指令需要两次 nibble 写操作,两次之间必须等待 E 完全落下去,否则模块会认为是连续脉冲。
5.3 初始化序列与常用指令参数
LCD1602 上电后必须先执行一段固定初始化序列,顺序不能乱,也不能省略中间的延时。关键点在于,模块可能以 8 位模式上电,也可能已经在 4 位模式,初始化代码要兼容两种情况。
void lcd_init(void) { P3DIR |= 0x3F; /* P3.0-P3.5 全部设为输出 */ _delay_cycles(50000); /* 上电等待,约 2ms */ lcd_write_nibble(0x03); /* 复位指令,8位模式 */ _delay_cycles(5000); lcd_write_nibble(0x03); /* 第二次复位 */ _delay_cycles(1000); lcd_write_nibble(0x03); /* 第三次复位 */ lcd_write_nibble(0x02); /* 切换为 4 位模式 */ lcd_write_byte(0x28, 0); /* 4位总线、2行、5x8点阵 */ lcd_write_byte(0x0C, 0); /* 显示开,光标和闪烁关 */ lcd_write_byte(0x01, 0); /* 清屏 */ _delay_cycles(50000); /* 清屏需要约 1.6ms */ lcd_write_byte(0x80, 0); /* 设置地址为第一行第一列 */ }初始化里的三个 0x03 是让模块无论当前处于什么模式都能正确识别复位指令,前两次如果模块已经在 4 位模式,会忽略半个字节,第三次之后发送 0x02 才真正切换宽度。0x28 是 4 位总线加两行显示的标志,0x0C 关闭光标闪烁是为了显示计算结果时界面干净。0x80 是 DDRAM 地址,80 代表第一行第一个字符,如果写到 0xC0 就是第二行第一个字符,后面显示数字串时用它控制起始位置。
5.4 整数转字符串与刷新时机
显示层要把 long 转成字符串右对齐写到 LCD。注意不要让 LCD 写在定时器中断里,LCD 写一个字符要几百微秒,在 8ms 周期里会严重干扰按键扫描节拍。正确做法是在按键事件处理函数末尾调用显示函数,一次刷新到位。
static void lcd_display_num(long val) { char buf[9]; int pos = 7; int neg = 0; int i; if (err_flag) { lcd_write_byte(0xC0, 0); /* 第二行显示错误 */ lcd_write_byte('E', 1); lcd_write_byte('r', 1); lcd_write_byte('r', 1); return; } if (val < 0) { neg = 1; val = -val; } if (val > 9999999L) val = 9999999L; buf[8] = '\0'; do { buf[pos--] = '0' + (int)(val % 10); val /= 10; } while (val > 0 && pos >= 0); if (neg) buf[pos--] = '-'; lcd_write_byte(0x80, 0); /* 回到第一行行首 */ for (i = pos + 1; i < 8; i++) { lcd_write_byte(buf[i], 1); } }右对齐显示是计算器的常规习惯,结果不满 8 位时前面留空,和真实计算器一致。错误提示放在第二行,这样用户能同时看到第二行的 Err 和第一行之前残留的运算过程。刷新函数的调用位置在 on_key_pressed 的最末尾,每次按键事件处理后调用一次,主循环里不需要主动轮询 LCD 状态。
6. 低功耗优化:LPM3 睡眠下用 ACLK 定时扫描
6.1 让 CPU 在空闲时真正睡下去
到这里计算器已经能完整工作,但主循环是空的,芯片空转。F5529 运行在 25MHz DCO 时电流接近 3mA,如果以后用电池供电,这个功耗没法接受。MSP430 的看家本领是低功耗模式,LPM3 下 ACLK 保持运行,CPU、MCLK、SMCLK 全部关停,整机电流可以降到微安级别。
改造的关键点:之前 TA0 的时钟源已经选了 ACLK,所以在 LPM3 模式下定时器仍在运行,还能按 8ms 周期唤醒 CPU。而 SMCLK 和 DCO 在 LPM3 下会停掉,如果当初选的是 SMCLK,这步就得换晶振或者改时钟树。
6.2 中断里按需唤醒 CPU
进入低功耗的代码放在主循环里,按一次键会唤醒 CPU,处理完按键事件后回到主循环再次睡眠。真正需要主循环 CPU 干活的只有按键处理、LCD 刷新、运算状态机,这些都在中断里完成,所以唤醒动作要放在确认按键的有效中断里。
static volatile unsigned char need_wake = 0; #pragma vector = TIMER0_A0_VECTOR __interrupt void TA0_ISR(void) { TA0CCTL0 &= ~CCIFG; /* 清除中断标志 */ keypad_periodic(); /* 扫描 + 消抖状态机 */ if (need_wake) { need_wake = 0; __bic_SR_register_on_exit(LPM3_bits); } }在 on_key_pressed 结尾设置need_wake = 1,中断每次正常退出时保持 LPM3 模式,只有确认有按键需要处理时,才用__bic_SR_register_on_exit在退出中断时唤醒 CPU。这里不要用__enable_interrupt加 while 轮询,直接在主循环里进 LPM3 最省事。
void main(void) { WDTCTL = WDTPW | WDTHOLD; keypad_init(); lcd_init(); timer_init(); __bis_SR_register(GIE); /* 开总中断 */ while (1) { __bis_SR_register(LPM3_bits | GIE); /* 进入 LPM3 */ } }6.3 验证 CPU 真的睡眠而不是空转
判断是不是真的进入低功耗,别只看电流表,先确认程序执行路径。最直接的办法是在调试器里给__bis_SR_register(LPM3_bits | GIE)这条指令设断点,单步执行后查看 PC 是否停在低功耗入口处。如果运行几分钟断点被反复触发,说明 CPU 一直在被唤醒,此时检查是不是某个外设中断标志没有清除导致连续中断。
另一个实用技巧是用 GPIO 翻转辅助观察:在进入 LPM3 前把某个 LED 引脚拉低,唤醒后拉高,用示波器看这个引脚的波形。正常情况是绝大部分时间保持低电平,按键按下时出现一个短暂的高脉冲。如果波形显示高电平时间占了很大比例,说明中断唤醒太频繁,多半是 TA0 的中断标志在退出中断后又被重新置位。电流测量作为最终验证手段,裸板在 LPM3 下应该能测到几十微安以内的电流,超过这个数量级就回去查 GPIO 悬空和上拉配置。这套调试顺序能节省大量对着手册猜功耗来源的时间。
本文还有配套的精品资源,点击获取