
简介这是一套基于STC12C5A60S2主控、采用C语言实现的智能电子密码锁完整工程源码面向单片机学习、课程设计或毕业设计人群可快速理解密码锁多方式开锁与安全防护的工程实现。系统支持矩阵键盘密码开锁、密码修改AS608指纹识别RC522刷卡开锁任一方式连续三次失败即触发蜂鸣器报警15秒无操作自动关闭LCD背光以省电还通过继电器控制电锁动作逻辑完整。压缩包共45个文件大小156KB包含9个C源文件、8个头文件、Keil工程文件、可直接烧录的hex文件及使用说明文档工程内主程序与按键、指纹、RC522、LCD1602、蜂鸣器、EEPROM等模块分文件组织可对照学习GPIO、定时器、串口、外部中断等外设用法。目前已有1040人学习下载对希望从完整项目中掌握模块化编程、状态机与低功耗设计思路的读者很有价值。1. 为什么用 STC12C5A60S2 做智能电子密码锁比换 ARM 更划算拿 STC12C5A60S2 做一套智能电子密码锁最容易被低估的是外设调度。这颗 1T 增强型 8051 要同时驱动矩阵键盘、LCD1602、AS608 指纹头和 RC522 刷卡模块资源卡在「够用但不宽裕」的位置正好逼你把中断优先级和状态机想清楚。这套 C 语言源码不是单功能 demo密码、指纹、RFID 三种开锁方式走同一套判定逻辑还带了 EEPROM 掉电保存、连续三次失败蜂鸣报警、15 秒无操作关闭背光几个真正上产品才用得上的细节做课程设计或门禁改造都能直接改着用。解开压缩包是完整的 Keil 工程 lock.uvprojmain.c 管调度key.c、lcd.c、fingerprint.c、RC522.C、EEPROM.c、uart.c、INT0.c 各管一摊编译出的 lock.hex 用 STC-ISP 烧进主控就能跑。这套源码适合刚接触 51 的初学者对照学习也适合想看看老平台怎么外挂指纹模块的嵌入式开发者。接下来从引脚分配和外设初始化开始拆。2. STC12C5A60S2 引脚分配与外设初始化GPIO 模式、UART1、外部中断2.1 六个外设的引脚映射下表是按源码头文件里的宏定义整理的默认接线方案。不同卖家板子改过引脚只需要改头文件里的宏逻辑代码不用动。外设接口方式默认引脚说明矩阵键盘 4×4GPIO 扫描P1.0~P1.3 行P1.4~P1.7 列准双向口逐行拉低扫描LCD16028 位并口P0.0~P0.7 数据RSP2.0RWP2.1ENP2.2P0 配推挽输出增强驱动AS608 指纹模块UART1RXDP3.0TXDP3.157600bps8N1 无校验RC522 刷卡模块模拟 SPI 外部中断SCKP2.3MOSIP2.4MISOP2.5RSTP2.6NSSP2.7IRQ→P3.2(INT0)读卡完成拉低引脚唤醒 CPU蜂鸣器GPIO 输出P4.0按键音与报警音复用继电器GPIO 输出P4.1开锁成功吸合 2~3 秒LCD 背光GPIO 输出P4.215 秒无操作拉低关背光选型上有个容易被忽略的点AS608 虽然同时支持 UART 和 USB 接口但在这套系统里只能用 UART因为 USB 需要额外的枚举逻辑8051 的资源扛不住。RC522 硬件上只有 SPI/IIC 接口STC12 又没有硬件 SPI 外设只能软件模拟。两个识别模块一个占 UART1、一个占软件 SPI接口刚好错开这是这套接线能成立的前提。软件 SPI 用 GPIO 翻转时钟速率比硬件 SPI 低不少但 RC522 的 SPI 时钟上限是 10MHz模拟方式跑几百 kHz 完全够用读一张 M1 卡的 UID 只是几十条指令的事。INT0 在这里不是做按键中断用的而是接 RC522 的 IRQ 输出刷卡完成后由硬件拉低引脚唤醒 CPU比主循环里反复读卡状态位省电也省时间。2.2 PxM0/PxM1 端口模式配置STC12 系列和标准 8051 最大的区别是每个端口多了一组 PxM0、PxM1 寄存器来控制引脚工作模式。默认复位后是准双向口但如果直接拿准双向口去驱动 LCD1602 的 8 根数据线高电平驱动能力不足屏幕会偶尔花屏。初始化代码里把 P0 整口配成推挽输出// main.c —— GPIO 模式初始化 void GPIO_Init(void) { // P0 接 LCD1602 数据口全部配推挽保证 8 根数据线高电平不掉 P0M0 0xFF; P0M1 0x00; // P1 接矩阵键盘保持准双向扫描时输出低电平读键值时能读回引脚状态 P1M0 0x00; P1M1 0x00; // P2.5 是 RC522 的 MISO 输入线配高阻输入P2M1.51 // 其余 P2 引脚给 LCD 控制线和 SPI 输出配推挽 P2M0 0xDF; P2M1 0x20; // 0xDF1101 11110x200010 0000 // P4.0 蜂鸣器、P4.1 继电器、P4.2 背光全是输出 P4M0 0x07; P4M1 0x00; }这段代码的关键在 P2M0 0xDF 和 P2M1 0x20 两行。很多人习惯把整个端口一次性配成推挽结果 MISO 也被推挽输出读回来永远是高电平RC522 的响应数据全是 0xFF排查半天找不到原因。配端口模式之前先把引脚角色画清楚哪些是纯输入、哪些是双向、哪些是输出再对着位操作。PxM0/PxM1 的组合含义如下排查引脚问题时对照这个表PxM1PxM0端口模式常见用途00准双向口按键扫描、普通 GPIO01推挽输出LCD 数据线、SPI 时钟/输出10高阻输入UART RXD、SPI MISO、中断输入11开漏输出需要外部上拉的 I2C 等2.3 UART1 波特率与 INT0 中断初始化AS608 指纹模块出厂默认 57600bps、8 数据位、1 停止位、无校验。UART1 的波特率由定时器 T1 产生用的是最常用的 8 位自动重装模式// uart.c —— UART1 初始化系统时钟 22.1184MHz void Uart1_Init(void) { SCON 0x50; // 模式18位UARTREN置位允许接收 TMOD 0x21; // T0 模式1留给计时T1 模式2波特率 TH1 0xFF; // 22.1184MHz 下的 57600 重装值 TL1 TH1; TR1 1; // 启动 T1 ES 1; // 开串口接收中断 EA 1; }TH1 0xFF 是算出来的波特率 时钟频率 / 12 / (256 - TH1) / 3222.1184MHz 代入后 (256 - 0xFF) 122118400 / 12 / 1 / 32 57600。如果板子上换成了 11.0592MHz 晶振TH1 仍然取 0xFF 也能工作但误差会变大最好把晶振换成 22.1184MHz 再烧录。改成 9600 的话TH1 要相应调整否则指纹模块会一直不应答。INT0 的初始化在 INT0.c 里核心配置是下降沿触发// INT0.c —— RC522 刷卡唤醒 void EX0_Init(void) { IT0 1; // 下降沿触发 EX0 1; // 使能外部中断 0 EA 1; } void INT0_ISR(void) interrupt 0 { rc522_irq_flag 1; // 只置标志位不在这里读卡 }中断服务函数里只做一件事置标志位。读卡、比对 UID 这些耗时操作全部留到主循环里做否则在中断里跑 SPI 时序会拖垮其他任务的实时性。uart.c 同理串口中断只负责把收到的字节塞进缓冲区AS608 的应答帧解析放在主循环的状态机里完成。这是 8051 上做多外设最需要养成的习惯。3. 密码校验与 EEPROM 掉电保存键盘扫描、IAP 读写与三次错误报警3.1 矩阵键盘扫描逐行拉低加释放检测4×4 矩阵键盘占 P1 全部 8 根线低四位输出行扫描码高四位读列状态。扫描代码在 key.c 中// key.c —— 返回按键码无按键返回 KEY_NONE uint8_t Key_Scan(void) { uint8_t row, col, key KEY_NONE; for (row 0; row 4; row) { P1 ~(0x01 row); // 拉低当前行其余行置高 for (col 0; col 4; col) { if ((P1 (0x10 col)) 0) { delay_ms(10); // 消抖 10ms if ((P1 (0x10 col)) 0) { while ((P1 (0x10 col)) 0); // 等释放 key key_map[row][col]; } } } } return key; }消抖放在第二次确认读到低电平之后避免抖动造成误触发等待释放再返回按键值保证一次按下只产生一次事件。密码输入场景需要的就是这种「按一下算一下」的语义所以不做连按处理。如果以后要长按 # 键进入设置模式把 while 等待释放改成计时返回把按键按下和释放拆成两个事件状态机里再判断持续时长。key_map 是 4×4 的字符映射表通常是 0~9 加、#、A、B。密码只取数字部分作为退格# 作为确认这是当前最常用的交互设计。矩阵键盘扫描一次要遍历 4 行 × 4 列带 10ms 消抖的情况下整体耗时会被拉长主循环里不要每轮都做全量扫描用标志位控制扫描频率更合理。3.2 用 IAP 方式读写内置 EEPROMSTC12C5A60S2 的 EEPROM 不是独立芯片而是程序 flash 里划分出的一块区域通过 IAP 控制器访问。写操作前要触发 0x5A、0xA5 两个连续字节这是硬件级的防误写保护。单字节读取代码// EEPROM.c —— IAP 读一个字节 uint8_t EEPROM_Read(uint16_t addr) { uint8_t dat; IAP_CONTR 0x80; // IAPEN1 使能 IAP 功能 IAP_CMD 0x01; // 读命令 IAP_ADDRL addr 0xFF; // 地址低 8 位 IAP_ADDRH addr 8; // 地址高 8 位 IAP_TRIG 0x5A; // 触发时序先 5A IAP_TRIG 0xA5; // 再 A5中间不能被打断 _nop_(); // 等待 IAP 完成 dat IAP_DATA; IAP_CONTR 0x00; // 关闭 IAP防止误操作 return dat; }IAP_TRIG 连续两次赋值是关键这两步执行期间不能有中断插进来否则触发序列被打断会导致读写出错。所以稳妥的写法是在触发前 EA 0读完再恢复。写入操作要先发擦扇区命令因为 flash 只能把 1 写成 0只有整扇区擦除后才能重新写擦除粒度是 512 字节一个扇区。源码里的 EEPROM_Write 内部已经处理了擦除流程密码和错误计数放在同一个扇区避免跨扇区频繁擦写。3.3 密码区布局与比对逻辑密码相关数据在 EEPROM 中按地址偏移划分结构如下地址内容长度0x0000初始化魔数 0x5A1 字节0x0001~0x00066 位密码 ASCII6 字节0x0007~0x0008连续失败次数2 字节魔数用来判断 EEPROM 是否已经初始化上电后读到 0x5A 说明密码区有效读到 0xFF 说明是出厂状态自动写入默认密码。这样做的好处是首次上电不需要人工初始化代码里一个分支就搞定。校验入口// main.c —— 密码校验成功清错并开锁失败累加错误计数 uint8_t Pwd_Verify(uint8_t *input) { uint8_t i, ok 0; for (i 0; i 6; i) { if (input[i] EEPROM_Read(PWD_ADDR i)) ok; // 逐字节比对全中才算通过 } if (ok 6) { EEPROM_Write(ERR_ADDR, 0); // 任何方式开锁成功错误计数清零 Relay_Open(); // 继电器吸合 2~3 秒 return 1; } err EEPROM_Read(ERR_ADDR) 1; // err 为全局变量 EEPROM_Write(ERR_ADDR, err); if (err 3) { Buzzer_Alarm(); // 连续三次失败蜂鸣器报警 } return 0; }连续失败次数写进 EEPROM 而不是留在 RAM是为了防止断电重启绕过报警有人按错两次后拔电再上电RAM 清零但 EEPROM 里还记着 2下一次再错直接触发报警。这是这套源码里比较隐蔽但很关键的设计取舍。对应的代价是 EEPROM 有擦写寿命大概十万次量级。密码输错属于低频事件每次失败写一次完全可接受但如果是做高频刷卡考勤机错误计数建议改成 RAM 缓存加定时落盘别刷一次卡写一次 flash否则几个月就把扇区写穿了。3.4 为什么不用外挂 AT24C02STC12C5A60S2 自带 EEPROM 之后AT24C02 这类 I2C 芯片在这套方案里就没有存在必要了外挂芯片要占两根 IO 写软件 I2C 时序而 P2 口已经被 RC522 和 LCD 控制线占满。IAP 方式读一个字节只需要几条指令速度远快于 I2C 的 100kHz 时钟。唯一的限制是擦扇区粒度和写寿命密码锁这种低频写入场景刚好规避了这两个短板。4. AS608 指纹识别与 RC522 刷卡双通道驱动指令帧、防冲突与统一判定4.1 AS608 的指令帧结构与指纹搜索流程AS608 通过 UART 接收指令每个指令包是固定格式的字节序列0xEF01 包头4 字节模块地址默认全 FF1 字节包标识2 字节包长中间是指令码和参数最后 2 字节校验和。指纹开锁实际是「采集指纹图像 → 生成特征 → 在库中搜索」三步对应指令码如下指令码指令名作用0x01PS_GetImage从传感器取指纹图像0x02PS_GenImg从图像生成特征文件0x04PS_Search在指纹库中搜索匹配0x05PS_RegModel合成模板录入指纹用0x06PS_Store保存模板到库中组装一条 PS_Search 指令并发送的代码// fingerprint.c —— 组帧发送 PS_Search指令码 0x04 void FP_Search(uint16_t start_pid, uint16_t cnt) { uint8_t i, sum 0; uint8_t frame[16]; frame[0] 0xEF; frame[1] 0x01; // 包头 frame[2] 0xFF; frame[3] 0xFF; // 模块地址默认全 FF frame[4] 0xFF; frame[5] 0xFF; frame[6] 0x01; // 命令包标识 frame[7] 0x00; frame[8] 0x07; // 包长 1指令 4参数 2校验 frame[9] 0x04; // PS_Search 指令码 frame[10] start_pid 8; // 起始页高字节 frame[11] start_pid 0xFF; // 起始页低字节 frame[12] cnt 8; // 搜索页数高字节 frame[13] cnt 0xFF; // 搜索页数低字节 for (i 2; i 14; i) sum frame[i]; // 校验和地址到参数累加 frame[14] sum 8; // 校验和高字节 frame[15] sum 0xFF; // 校验和低字节 for (i 0; i 16; i) uart_send_byte(frame[i]); }发送完成后主循环等 uart.c 的接收状态机凑够一个完整响应帧。响应包格式和指令包一致确认码在帧的第 9 个字节包头 2 地址 4 包标识 1 包长 2 9。0x00 表示搜到匹配0x09 表示取不到有效图像一般是手指没放平或指纹太浅0x0B 表示库中无匹配。注意 AS608 是光学指纹头对干手和浅指纹的识别率天然不高这不是代码问题录指纹时同一个手指采集两次合成模板能明显缓解。4.2 RC522 读卡寻卡、防冲突两步拿 UIDRC522 读到卡 UID 不是一条指令完成的需要依次走 PcdRequest、PcdAnticoll 两步。PcdRequest 拿卡类型PcdAnticoll 拿 4 字节 UID。只做 UID 比对的话走到防冲突就够了// RC522.C —— 读取 M1 卡 4 字节 UID uint8_t RC522_ReadUid(uint8_t *uid) { uint8_t atqa[2]; if (PcdRequest(0x52, atqa) ! MI_OK) // 寻卡0x52 能唤醒休眠卡 return MI_ERR; if (PcdAnticoll(uid) ! MI_OK) // 防冲突返回 4 字节 UID return MI_ERR; return MI_OK; // 想读扇区数据才继续 PcdSelect }0x52 是寻卡命令能感应到休眠状态的卡另一个常用值 0x26 只找非休眠卡适合做低功耗场景下的快速轮询。PcdAnticoll 拿到的 uid 是 4 字节数组比对时逐字节和 EEPROM 里存的管理卡 UID 比较全部一致才开锁。M1 卡 UID 出厂固化不可改写用它做身份凭据比读扇区数据简单也省去了管理扇区密码的麻烦。提示RC522 模块是 3.3V 电平STC12C5A60S2 是 5V 器件。多数成品模块板载了电平匹配电路但裸模块接 5V 单片机时SCK、MOSI 最好各串一个 1kΩ 电阻再进模块长期运行不会因为电平不匹配烧坏模块内部芯片。这是 RC522 在 5V 单片机上最常见的坑。4.3 三通道开锁的统一判定三种开锁方式的驱动各管各的但判定逻辑在 main.c 里收敛成一条事件链。密码是同步按键流程指纹和刷卡是异步事件。统一判定后成功和失败的出口完全一致// main.c —— 主循环中的统一开锁判定 for (;;) { key Key_Scan(); if (key KEY_CONFIRM) result Pwd_Verify(pwd_buf); // 密码6 位输入完按确认 if (fp_ready) // 指纹串口响应解析完成 result FP_CheckResult(); if (rc522_irq_flag) // 刷卡INT0 置位后读 UID { rc522_irq_flag 0; result (RC522_ReadUid(uid_buf) MI_OK Uid_Match(uid_buf)); } if (result SUCCESS) { err_cnt 0; // 任意方式成功错误计数清零 Relay_Open(); // 继电器开锁 2~3 秒 } else if (result FAIL) { err_cnt; if (err_cnt 3) Buzzer_Alarm(); // 任意方式连续三次失败 } display_refresh(); // LCD1602 刷新状态 }指纹和刷卡的异步事件源分别是 uart.c 的接收状态机和 INT0 中断置位主循环只需要轮询两个标志位。这里最关键的是不要指纹等待响应写成阻塞式延时否则等 AS608 返回的那几百毫秒里按键和刷卡全部失效。事件驱动在这个规模的项目里是性价比最高的写法。如果三种方式要分别计数把 err_cnt 拆成三个独立变量、清零时机跟着各自成功事件走即可。摘要描述的是「通过某种方式连续三次失败」所以源码用了统一计数、任意成功即清零的实现报警语义是「不管哪种方式连续错三次就响」。5. 定时器、背光休眠与报警时长把 15 秒和 10 秒调成自己的值5.1 用 Timer0 建立 1ms 时基15 秒无操作关背光、继电器 2~3 秒释放、报警 10 秒停止这些时间全部由 Timer0 的 1ms 中断驱动计数// main.c —— Timer0 1ms 时基 void Timer0_Init(void) { TMOD 0xF0; TMOD | 0x01; // T0 模式116 位定时 TH0 (65536 - 1000) 8; // 22.1184MHz 下定时 1ms TL0 (65536 - 1000) 0xFF; ET0 1; TR0 1; EA 1; } void Timer0_ISR(void) interrupt 1 { TH0 (65536 - 1000) 8; // 软件重装进中断后尽快执行 TL0 (65536 - 1000) 0xFF; idle_ms; // 无操作计时 if (idle_ms 15000) { idle_ms 15000; // 饱和计数防止溢出 LCD_BL 0; // 关闭背光 } alarm_ms; // 报警计时 if (alarm_ms 10000) Buzzer_Stop(); }16 位定时器在 8051 上必须中断里软件重装重装语句要放在中断入口附近晚几个周期会让毫秒计时产生几十微秒的漂移。对背光这种秒级功能无所谓但同一时基要给指纹模块做 500ms 通信超时的话这个误差就会影响可靠性。tick 变量全部做饱和计数防止溢出后数值回绕误触发动作。5.2 报警音与按键音的参数对照蜂鸣器由 P4.0 驱动同一个引脚承担两种声音靠频率和时长区分事件频率时长触发条件按键反馈1kHz 方波50ms每次有效按键连续失败报警4kHz 方波500ms 间歇10s任意方式连续失败 3 次按键音短促报警音是长鸣带间歇人在楼道里能分辨出是报警而不是误触。要调报警时长改 Timer0 中断里 alarm_ms 的阈值要调间歇节奏在报警分支里用 alarm_ms 对 500 取模模的窗口内输出方波、窗口外静音这样改参数不用动主循环逻辑。5.3 背光恢复逻辑与三个排错点背光关闭后键盘扫描照常运行Key_Scan 返回非空值的第一时间把 idle_ms 清零并把 LCD_BL 置高之后才进入输入流程。所以黑屏不是锁死按键按下立即亮这是互动逻辑里最容易验证的一项上电后等 15 秒按任意键背光应马上恢复。排错按通道隔离最省时间。指纹通道不通先用 USB-TTL 转接板单独接 AS608串口助手发版本查询指令能回应说明模块正常问题在主控的波特率或接线没有回应先查供电AS608 工作电流在 60mA 量级别和继电器共用一路电源。RC522 读不到卡用逻辑分析仪抓 SCK 和 MOSI确认模拟 SPI 时序翻转正常同时检查 MISO 那根线有没有被误配成推挽输出。EEPROM 写不进数据优先检查 IAP_TRIG 的 0x5A、0xA5 之间有没有被串口中断打断把触发区域的中断关掉再试。这三条路径各验证一遍再改 15 秒阈值就只需要动 idle_ms 那一处判断。本文还有配套的精品资源点击获取