深入PIC16F887:电子密码锁的矩阵键盘与LCD1602驱动实现
2026/9/17 15:15:44 网站建设 项目流程

简介:面向单片机课程设计与嵌入式入门学习者,这份报告围绕PIC16F887单片机在电子密码锁中的应用展开,完整呈现从需求分析到软硬件实现的课设流程,适合需要参考单片机项目设计思路、电路搭建与C语言编程实践的读者使用。资源为单份PDF文档,共1个文件,压缩包大小542KB,内容涵盖项目目标、系统描述、硬件选型、电路设计、模块化程序设计、项目总结及附录程序代码,结构完整,便于按章节查阅。目前已有110人学习。报告中详细说明了4×4矩阵键盘、LCD1602显示、直流电机与蜂鸣器的接口设计,并给出了初始化、密码输入、锁定解锁、报警和显示等模块的编程要点,对理解PIC单片机I/O配置、中断处理和实时控制有直接帮助。附录代码可辅助读者验证和复用,是完成类似电子密码锁课题的实用参考资料。

1. 电子密码锁课设真正难的不是密码比较,而是人机交互

做PIC单片机课设,十个人里有八个选电子密码锁,但大多数交上来的代码都能跑,却经不起三个追问:矩阵键盘为什么扫完行还要翻转扫列?LCD1602走4位数据线时字节是怎么拆成两次写的?连续三次输错报警后,键盘锁定到底锁住了什么?这篇把PIC16F887电子密码锁的硬件分配、矩阵键盘扫描、LCD 4位驱动和错误处理逻辑逐层拆开。项目本身不复杂,但把这四件事讲透,等于把单片机课设里最常见的人机交互链路做了一遍。适合正在做PIC课程设计的学生,也适合想快速上手PIC16F887外设配置的工程师。

2. 硬件选型与引脚分配:PIC16F887 的 I/O 怎么分才够用

2.1 为什么课设选 PIC16F887 而不是 51 系列

PIC16F887 是 Microchip 的 40 引脚 8 位增强型闪存单片机,内部资源在课设级别里属于"富余但不过剩":8K 字的程序存储器(注意是 14 位指令字,不是 8 位字节)、368 字节 RAM、256 字节 EEPROM、36 个 I/O 口、14 路 A/D、1 个 CCP、1 个增强型 CCP、1 个 EUSART、1 个 MSSP、2 个比较器。这些参数决定了它做密码锁的场景很合适:EEPROM 以后可以存密码,CCP 模块可以输出 PWM 驱动蜂鸣器变调,RB 口的电平变化中断配合弱上拉能直接做矩阵键盘。

对比 51 系列,PIC16F887 有个容易被忽略的优势:RB 口自带可编程弱上拉和电平变化中断。做 4x4 矩阵键盘时,51 通常要在外部加上拉电阻,而 PIC 只需要配置 WPUB 寄存器和 IOCB 寄存器,硬件上省掉一排电阻。另一个关键点是它支持在线调试和自编程,用 Pickit3 烧录调试比老式编程器方便得多,这也是很多学校把它作为课设指定芯片的原因。

2.2 引脚分配的核心逻辑

原课设的引脚分配可以整理成下面的表格,这个映射关系是整个系统的基础:

外设引脚方向说明
晶振RA7/OSC1、RA6/OSC2输入4MHz 外部晶振
复位RE3/MCLR/Vpp输入接按键 K1
外部中断RB0/AN12/INT输入接按键 K2
矩阵键盘行RB0~RB3输入弱上拉使能,电平变化中断
矩阵键盘列RB4~RB7输出/输入扫描时翻转方向
LCD 数据RD0~RD3输出LCD1602 DB4~DB7
LCD 控制RD4=R/S、RD5=R/W、RD6=E输出RS/RW/E
直流电机RC1/T1OSI/CCP2输出密码正确时驱动电机
蜂鸣器RC2/P1A/CCP1输出按键音与报警音

这个分配有两处值得注意。第一,LCD 只用 4 位数据线,省下 4 个 I/O 口,这在 40 引脚的单片机上看似没必要,但如果后续要扩展 EEPROM 芯片、红外接收头或者第二路电机,省下来的 I/O 就是余量。第二,电机和蜂鸣器都挂在 RC 口上,而且相邻,实际布板时蜂鸣器用三极管驱动、电机用电机驱动芯片,两者电源要做好去耦,否则电机启动瞬间的电流跌落会导致单片机复位。

2.3 4x4 矩阵键盘的行列翻转原理

矩阵键盘扫描的常见做法是行列翻转法。把 RB0~RB3 设为输入并开启弱上拉,RB4~RB7 设为输出低电平。没有按键按下时,低四位读到的都是 1,即 PORTB 等于 0x0F。按下某个键后,对应行的引脚被拉低,PORTB 的低四位出现一个 0,单片机据此知道是哪一行被按下。但此时还不知道是哪一列,所以需要翻转:把 RB4~RB7 设为输入带弱上拉,RB0~RB3 设为输出低电平,再读一次 PORTB,高四位为 0 的那一位就是列号。行号和列号一组合,键值就确定了。

这套思路在原代码的anjian()函数里体现得很清楚。第一次读 PORTB 得到0x0e0x0d0x0b0x07分别对应第一行到第四行,然后翻转方向再查 RB4~RB7 哪个是低电平。为什么第一行按下时 PORTB 低四位是0x0e而不是0x01?因为弱上拉的结构是"非按下为 1,按下为 0",所以四行中某一行被拉低,读回来的值就是0x0e(1110)、0x0d(1101)这类"只有一个 0"的形态。用switch匹配这几个值,就能确定行号。

2.4 LCD1602 的 4 位接线为什么是省 I/O 的正解

YB1602A 是标准 1602 字符型液晶,内部控制器是 HD44780 兼容芯片,支持 8 位和 4 位两种数据接口。8 位模式需要 DB0~DB7 共 8 根数据线,加上 RS、RW、E 三根控制线,总共 11 个引脚。4 位模式只需要 DB4~DB7 四根数据线,加上控制线共 7 个引脚,省下 4 个 I/O。在 4 位模式下,每个字节要分两次写入:先写高四位,再写低四位。

这个方案对课设的意义不只是省引脚。PIC16F887 的 RD 口是真正的双向 I/O,配置成输出后驱动能力足够直接带 LCD 的数据线,不需要额外加缓冲。而 RS、RW、E 分别接 RD4、RD5、RD6,意味着可以用一条 PORTD 赋值语句同时控制数据和命令字的发送,代码上更紧凑。后面第四章会详细讲 4 位写入的时序拆解。

3. 主程序框架与矩阵键盘扫描实现

3.1 初始化配置:TRIS、弱上拉和电平变化中断

主程序开头的初始化是整个系统能跑起来的前提,关键代码在main()的前几行:

TRISB = 0x0F; // RB0~RB3 输入,RB4~RB7 输出 TRISC = 0x00; // RC 全输出,驱动电机和蜂鸣器 TRISD = 0x00; // RD 全输出,驱动 LCD PORTB = 0x0F; // 高四位输出低电平,低四位弱上拉有效 PORTC = 0x00; // 电机和蜂鸣器初始关闭 WPUB = 0x0F; // 低四位使能弱上拉 IOCB = 0x0F; // 低四位允许电平变化中断 ANSEL = 0x00; // 全部引脚设为数字 I/O ANSELH = 0x00; OPTION = 0x00; // 选项寄存器,使能弱上拉

这里有个顺序问题容易踩坑。TRISB 决定方向,PORTB 的初始电平在切换方向前就要写好,因为 PORTB 输出锁存器里的值会直接影响弱上拉和电平变化中断的行为。WPUB 只在引脚配置为输入时生效,所以要先设 TRISB 再设 PORTB,最后开 WPUB,这个顺序不能乱。

ANSELANSELH是 PIC16F887 特有的寄存器,用来把引脚从模拟功能(ADC、比较器)切换到数字 I/O。RA 口某些引脚和 RB 口部分引脚默认可能是模拟输入,如果不对ANSEL清零,读这些引脚的电平会读到错误的值。这是从 PIC 的旧型号迁移过来时常犯的错——旧型号没有 ANSEL 寄存器,很多人不习惯初始化它。

OPTION = 0x00设的是选项寄存器,其中第七位RBPU控制 PORTB 弱上拉的全局开关。注意是"全局开关",而WPUB是"逐个引脚开关",两者是 AND 关系。OPTION = 0x00RBPU清 0,允许弱上拉工作,WPUB = 0x0F只对低四位使能,高四位在工作时作为输出不受影响。

3.2 按键检测与消抖的完整流程

主循环里按键检测的代码逻辑如下:

while (1) { while (PORTB == 0x0F); // 等待有键按下 delay(2); // 消抖延时 RBIF = 0; // 清电平变化中断标志 anjiansheng(); // 按键提示音 anjian(); // 行列翻转扫描,得到键值 k switch (k) { case 'C': // 删除 qingchu(); j--; break; case 'E': // 确认 if (bijiao() == 0) { errorflag++; if (errorflag >= 3) { xie_zfc(str2, 4); // 显示 "lock" jingbao(); // 报警 TRISB = 0x00; // 键盘锁定 } } else { PORTC = 0x02; // 电机转动 errorflag = 0; } break; default: pw[j] = k; xie_sj('*'); j++; break; } }

这里的消抖分两层。第一层是delay(2)的软件延时,跳过按键机械抖动的不稳定区间;第二层是靠 RBIF 中断标志确认电平变化已经稳定。原文代码在while (PORTB == 0x0F)delay(2)之间加了while (RBIF == 0)的等待,实际意义是确保中断标志已经置位。我一般会把两条合起来写,直接读 PORTB 再延时,逻辑更直观,但原代码这种写法在电平变化中断被使能时也没问题。

有一个细节值得注意:while (PORTB == 0x0F)这个空循环如果一直等不到按键,程序就卡在这里,LED 显示、蜂鸣器等其他任务全部停摆。这是裸机轮询方案的固有缺陷,课设可以接受,如果要优化,应该把键盘扫描放进定时器中断,主循环处理显示和其他逻辑。

3.3 行列翻转扫描的代码拆解

核心扫描函数anjian()的结构是行扫描加列扫描的组合:

void anjian(void) { unsigned int y; y = PORTB; switch (y) { case 0x0e: // 第一行有键按下 TRISB = 0xF0; // 翻转:高四位输入,低四位输出 PORTB = 0xF0; // 低四位输出低电平 WPUB = 0xF0; // 高四位使能弱上拉 IOCB = 0xF0; // 高四位允许电平变化中断 if (RB4 == 0) k = '0'; // 第一列 if (RB5 == 0) k = '1'; // 第二列 if (RB6 == 0) k = '2'; if (RB7 == 0) k = '3'; break; // 其他行省略,结构一致 } TRISB = 0x0F; // 恢复初始状态 PORTB = 0x0F; WPUB = 0x0F; IOCB = 0x0F; while (PORTB != 0x0F); // 等待按键释放 }

这个函数的关键是TRISB = 0xF0的方向翻转。第一次读 PORTB 拿到行信息后,立刻把方向反转,然后读列信息。注意这里有一个while (PORTB != 0x0F)的等待释放循环,它的作用是防止一次按键触发多次扫描。如果不加这个循环,按住按键不松手,主循环会不停地进入anjian()switch (k)会重复执行删除或确认操作。

还有一个容易忽略的坑:PORTB = 0xF0这句在翻转方向后写的是输出锁存器。因为低四位现在是输出,写 0 就是拉低,这样行的低电平才能维持;高四位是输入,写 1 到锁存器配合弱上拉才能读回高电平。如果这里写反,整个扫描结果就全错了。

3.4 键值映射与输入状态管理

四个case分支对应矩阵键盘的四行,每行四个键,组成 16 个键位。原设计里键值从0F,其中C被定义为删除键,E被定义为确认键,其余数字键是密码内容。

输入状态管理用一个字符数组pw[6]保存当前输入的 6 位密码,用j作为写入指针。每按一个数字键,就执行pw[j] = k; j++;并显示一个*号遮蔽真实密码。这里有个边界条件:pw数组只有 6 个元素,如果用户按了 7 个数字键再按确认,j已经越界。原代码没有做越界保护,实际测试时会在密码比较函数里读到数组之外的内存垃圾值。改进方式很简单,在default分支加一个判断:if (j < 6) { pw[j] = k; j++; }

bijiao()密码比较函数也很简单,就是逐位比较数组:

int bijiao(void) { int flag = 1; unsigned char h; for (h = 0; h < 6; h++) { if (pw[h] != password[h]) flag = 0; } return flag; }

这个函数没有检查pw是否已经填满 6 位,只比较前 6 位。如果用户只输了 4 个数字就按确认,pw[4]pw[5]是残留的旧值,可能导致密码错误误判。建议在比较前先检查j == 6,不满足直接返回错误。

4. LCD 4 位驱动与密码验证的状态机

4.1 4 位写入协议的关键时序

LCD1602 的 4 位模式写入一个字节要分两步:先送高四位,再送低四位。每条数据的写入时序是:设置 RS(命令还是数据)和 RW(读还是写),拉高 E,数据稳定后拉低 E,E 的下降沿把数据锁存进 LCD 控制器。

原代码里的指令写入函数:

void xie_ml(unsigned char ml) { RW = 0; // 写模式 E = 1; // 使能拉高 RS = 0; // 命令模式 delay(1); LCD = ml; // 数据送到 PORTD delay(1); E = 0; // 下降沿锁存 }

这个函数有个细节:E = 1LCD = ml之前。严格按 HD44780 时序,数据应该在 E 拉高之前就稳定在数据线上,E 下降沿锁存时才有效。原代码先拉 E 再送数,靠delay(1)给数据稳定时间,实际也能工作,但不严谨。我一般会先写 PORTD 再拉 E,这样下降沿到来时数据早已稳定。

字符数据写入函数xie_sj()是 4 位拆包的核心:

void xie_sj(unsigned char sj) { char sj_h, sj_l; sj_h = (sj & 0xf0) >> 4; // 取高四位 sj_l = sj & 0x0f; // 取低四位 E = 1; NOP(); RW = 0; NOP(); RS = 1; NOP(); // 数据模式 delay(1); LCD &= 0xf0; // 清低四位 LCD |= sj_h; // 送高四位 E = 0; delay(1); E = 1; // 第二次使能 LCD &= 0xf0; // 清低四位 LCD |= sj_l; // 送低四位 delay(1); E = 0; // 第二次下降沿锁存 }

LCD &= 0xf0这行的作用是保留 PORTD 的高四位、清掉低四位,再把sj_h或到低四位上。因为 RD0~RD3 接的是 LCD 数据线,而 RD4~RD6 接的是控制线,如果直接用LCD = sj_h赋值,会把 RS、RW、E 的状态覆盖掉。用与运算加或运算的组合,既能更新数据位,又不碰控制位。这是 4 位模式下 LCD 驱动的关键技术点。

4.2 LCD 初始化序列为什么要写三遍 0x03

LCD 初始化函数lcdcsh()里的一个经典细节是连续写三次0x03。这是因为上电后 LCD 可能处于 8 位模式或未定义状态,连续发送三次0x03(二进制 0011),LCD 会把它识别为"8 位模式下的功能设置命令",第三次后再发0x02(0010),LCD 才切换到 4 位模式。之后才能发真正的功能设置命令0x28(4 位、2 行、5x7 点阵)。

原代码的初始化序列是:

for (i = 0; i < 3; i++) { xie_ml(0x03); // 三次 0x03 delay(10); } xie_ml(0x02); // 切换到 4 位模式 xie_ml(0x02); // 0x28 的高四位 xie_ml(0x08); // 0x28 的低四位:4 位模式、2 行、5x7 xie_ml(0x00); // 0x0F 的高四位 xie_ml(0x0f); // 0x0F 的低四位:显示开、光标开、闪烁 xie_ml(0x00); // 0x01 的高四位 xie_ml(0x01); // 0x01 的低四位:清屏 xie_ml(0x00); // 0x06 的高四位 xie_ml(0x06); // 0x06 的低四位:写入后地址加一

注意 4 位模式下每条真正命令都要拆成两次写。0x28在 4 位模式的发送方式是先发 0x02 再发 0x08,0x0F先发 0x00 再发 0x0F,以此类推。初始化序列里xie_ml(0x02)出现了两次:第一次是切换模式,第二次是功能设置的高四位。xie_ml(0x00)也出现了多次,分别是清零命令和光标移动命令的高四位。这个容易搞混,建议对照 LCD1602 的数据手册逐条核对。

4.3 删除键的显示实现:空格覆盖加光标左移

删除键的实现在qingchu()函数里,思路是在 LCD 当前光标位置先写空格覆盖字符,再把光标左移:

void qingchu(void) { xie_ml(0x04); // 光标左移一位 xie_sj(' '); // 当前位置写空格 xie_ml(0x06); // 光标右移一位 }

原代码在xie_sj(' ')之前先执行了xie_ml(0x00)xie_ml(0x04),顺序是先把光标移到要删除的位置,写空格,再移回去。这里建议简化成上面的三行,效果相同但更好理解。核心点是:LCD 没有原生的"退格"指令,删除必须用空格覆盖。xie_sj(' ')写在当前光标位置会把原来的字符盖掉,但光标会朝写方向自动移动一格,所以还要用0x04命令把光标退回来。

4.4 三次错误报警的状态切换

错误状态管理在switch (k)case 'E'分支里。errorflag是全局变量,每次按确认键时如果密码错误就加一。达到 3 次时,系统执行三个动作:显示锁屏、蜂鸣器报警、锁定键盘。

锁键盘的实现方式是把 PORTB 全部设为输出并清零:

TRISB = 0x00; // B 口全部输出,键盘失效

这样做的原理是:矩阵键盘的按键检测依赖弱上拉的输入状态,现在引脚全部变成输出,电平变化中断也失效了,按键按下不会再引起 PORTB 的变化。但要注意,主循环仍然在跑,while (PORTB == 0x0F)这个判断条件现在恒为假,因为 PORTB 被写成了输出值,程序会反复进入anjian(),而anjian()里的while (PORTB != 0x0F)会直接卡死。实际表现是:报警后整个系统卡在按键扫描函数里,虽然达到了"键盘被锁"的目的,但电机也不再响应。

更稳妥的做法是设置一个系统状态变量,比如locked = 1,在主循环里判断这个变量,锁定后只保留蜂鸣器报警和 LCD 显示,不再进入键盘扫描。或者用定时器在报警 3 秒后自动复位,让用户能重新尝试。原代码没有自动复位功能,一旦锁死只能断电重启,这是课设级的简化方案。

5. EEPROM 存储密码、看门狗与裸机避坑

5.1 用内置 EEPROM 实现硬件级密码修改

PIC16F887 自带 256 字节 EEPROM,地址从 00H 到 FFH,数据手册建议使用时避开 00H~20H 的系统校准区。原设计把密码固定写在代码里,字符串常量password[] = "123456",用户无法修改。利用 EEPROM 可以在不增加任何外部芯片的情况下支持密码修改。

#include <pic.h> void eeprom_write_byte(unsigned char addr, unsigned char data) { EEADR = addr; // 目标地址 EEDATA = data; // 要写入的数据 EECON1 = 0x34; // 写使能序列 EECON1 = 0x55; EECON1 = 0xAA; EECON1 |= 0x04; // 开始写 while (EECON1 & 0x04); // 等待写完成 } void eeprom_read_password(unsigned char *buf) { unsigned char i; for (i = 0; i < 6; i++) { EEADR = i + 0x20; // 从 0x20 地址开始存密码 EECON1 = 0x00; // 读模式 buf[i] = EEDATA; // 读回数据 } }

写 EEPROM 前必须判断地址是否在有效范围,以及WR位是否仍在忙。读操作不需要解锁序列,但 EECON1 的设置和写不同,读时EECON1 = 0x00,写时按顺序设置WRENWR,中间还要插入0x550xAA两个解锁字节。如果漏掉EEADR的赋值,读写都会落在错误的地址上。启动时用eeprom_read_password把存储的密码读入password[]数组,验证逻辑完全不用改,就实现了"硬件级修改密码"。

5.2 看门狗防止报警死循环

原代码的报警函数jingbao()是个while(1)死循环:

void jingbao(void) { while (1) { RC2 = 1; delay(500); RC2 = 0; delay(300); } }

蜂鸣器响个不停,CPU 也卡在这里不干别的。如果这个死循环配合看门狗定时器使用,系统会不断复位,表现是蜂鸣器响一阵、重启、再响一阵,反而制造了"自动重试"的效果。PIC16F887 的看门狗在配置字里使能:

__CONFIG(0x3FF1); // WDTE 使能,其他位保持默认

代码里定期执行CLRWDT()清狗指令,一旦程序跑飞或卡死,看门狗超时后强制复位。但注意:看门狗复位后程序从 main 开始重新执行,所有状态变量清零,errorflag也被清掉,相当于自动解锁。如果想在报警期间不被看门狗干扰,就不要用while(1)死循环,改成定时翻转方式,比如在定时器中断里翻转 RC2,主循环继续运行。

5.3 常见误用与排查技巧

  • getch()系列函数在 PIC 里不存在,原代码用的NOP()是内嵌汇编空指令,头文件里自带,不要自己定义。
  • 4 位模式下清屏命令要拆两次写,只写低四位0x01会导致显示残影。清屏后要加 1.52ms 以上延时,delay(10)才够。
  • LCD 的RW引脚通常直接接地(只写不读),原代码留了 RD5 控制它也没错,但接 GND 可以省一个 I/O。前提是不需要读 busy 标志,延时代替判断即可。
  • 密码比较前先检查j == 6,否则pw未填满时的残留值会导致误判。
  • 按键释放检测循环while (PORTB != 0x0F)在锁键盘后恒为真,程序卡死,锁键盘应该用状态变量而不是改 TRISB。

调试时可以用 Pickit3 的在线调试功能,在anjian()bijiao()里打断点,watch 窗口观察 PORTB 值和pw数组内容。如果按键扫描异常,先看WPUB是否使能、OPTION第七位是否为 0,再看翻转扫描后 PORTB 的读取时机。LCD 显示乱码时,优先检查初始化序列的顺序和 4 位拆包是否把高低位弄反,用逻辑分析仪抓 E 引脚和 DB4~DB7 可以快速定位问题。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询