项目概述
本项目是一套基于 ESP32 的双通道智能密码锁:门锁侧用 4×4 矩阵键盘本地输入密码,手机侧通过低功耗蓝牙(BLE)远程开锁。锁舌由 SG90 舵机带动,ST7789 彩屏显示中文状态,无源蜂鸣器给出按键与结果提示。
固件以有限状态机组织界面与业务:上电欢迎 → 锁定待机 → 输密 / 改密 / 菜单 / 记录 / 访客 → 开锁成功与自动上锁 → 防暴力破解锁定。主密码与开锁记录写入 ESP32 片内 NVS(Flash),断电后仍保留。手机可用浏览器打开tools/ble_test.html,或按docs/app-inventor.md用 MIT App Inventor 自制 App。设备广播名为SmartLock。
适用场景是课程设计与功能演示:强调可复现的硬件、可拆分的代码和可操作的协议,而不是商用级加密门锁。
项目亮点
- 双通道开锁:本地键盘与 BLE 远程共用同一套校验与锁定策略,访客另有 6 位一次性密码(24 小时有效、用过即废)。
- 中文图形界面:自生成 16×16 点阵字库(
FontCN.h),锁屏、倒计时、菜单、记录、访客密码均在 240×240 屏上完成,不必依赖串口。 - 防暴力破解:连续 5 次错误进入锁定,初始 30 秒,再次触发时间加倍,最长 300 秒;剩余次数与倒计时同步推到屏幕和手机。
- 记录可追溯:最近 64 条事件(本地 / 远程 / 访客,开锁、失败、上锁、生成访客密码)环形写入 NVS,屏幕分页浏览,手机可拉取并导出 CSV。
- 低功耗取向:CPU 80 MHz、仅开 BLE、发射功率 −3 dBm、空闲约 20 秒关屏、舵机到位后断开 PWM,适合演示供电。
- 模块化固件:键盘、蜂鸣器、舵机、安全、显示、蓝牙、日志各自独立头文件,主文件只负责状态机与协议分发。
- 手机友好协议:16-bit UUID(
FFF0/FFF1/FFF2),命令为短 ASCII 文本,兼顾网页 Web Bluetooth 与 App Inventor。
项目难点及解决方案
1. 输出脚选错导致舵机、蜂鸣器“完全没反应”
ESP32 的 GPIO34~39 只能输入,不能输出 PWM。早期若接到 34/35,现象是程序在跑、屏幕正常,锁和声音却毫无动作。解决办法:舵机固定 GPIO21、蜂鸣器 GPIO22,并在config.h用编译期#error拦住再次误用。
2. 键盘列脚与屏幕 SPI 冲突
ST7789 走 VSPI(时钟 18、MOSI 23)。若键盘占用 GPIO19(MISO)或 GPIO5(部分板卡默认 CS),会出现花屏、键值乱跳或上电异常。列脚改到 27、14、16、17,避开 VSPI 占用脚。
3. ST7789 旋转后画面左移、右侧留白
240×240 模组内部 CGRAM 往往带偏移。setRotation(4)时若colstart不对,有效像素会偏出可视区。正确做法是在 TFT_eSPI 的ST7789_Rotation.h中,240×240 的 rotation 4 分支设colstart = 80; rowstart = 0(并开启CGRAM_OFFSET)。该类库里colstart为私有成员,无法用子类可靠改写,必须改驱动头文件。
4. 倒计时全屏重绘导致闪烁
开锁 8 秒倒计时与锁定倒计时若每秒fillScreen,闪烁明显。显示层增加脏标记:界面切换全刷,倒计时仅markPartial(),只重画数字行。
5. 屏幕没有现成中文字库
TFT_eSPI 默认不含本项目文案。用tools/gen_font.py从黑体截取所需汉字,生成FontCN.h点阵,显示层按 UTF-8 查表绘制。
6. BLE 回调与主循环并发
手机写入发生在蓝牙协议栈任务中。用临界区把命令拷入缓冲,主循环takeCommand()再处理,避免在回调里驱动舵机、写 NVS、刷屏。
7. 记录要断电仍在,又没有 RTC
事件以打包结构体环形存 NVS;时间戳用累计秒(开机后累加,重启后接上次末条),屏幕显示为MM:SS。这不是墙钟时间,但能保证离线后再上电仍能翻看历史。
一、硬件系统部分
1.1 硬件清单
| 序号 | 名称 | 规格 / 说明 | 数量 |
|---|---|---|---|
| 1 | 零知ESP32 开发板 | 3.3 V 逻辑,需引出 5 V 给舵机 | 1 |
| 2 | 零知ESP32 扩展板 | 有引出接口,方便接线 | 1 |
| 3 | ST7789 彩屏 | 240×240,SPI;VCC 接 3.3 V | 1 |
| 4 | 4×4 矩阵键盘 | 硬按键 | 1 |
| 5 | SG90 舵机 | 信号可接 3.3 V PWM,电源必须 5 V | 1 |
| 6 | 无源蜂鸣器 | 默认固件BUZZER_PASSIVE=1;有源需改宏 | 1 |
1.2 接线方案
屏幕引脚写在 TFT_eSPI 的User_Setup中,固件不再重复定义。其余外设集中在SmartLock/config.h。
键盘、舵机、蜂鸣器
键盘金属面朝下、按键朝操作者时,排针从左到右一般为C1 C2 C3 C4 R1 R2 R3 R4。
| 器件 | 线序 | ESP32 |
|---|---|---|
| 键盘行 R1~R4 | 行输出 | GPIO32、33、25、26 |
| 键盘列 C1~C4 | 列上拉输入 | GPIO27、14、16、17 |
| SG90 橙 / 黄 | PWM 信号 | GPIO21 |
| SG90 红 | 电源 | 5 V |
| SG90 棕 | 地 | GND(必须与 ESP32 共地) |
| 蜂鸣器信号 | PWM / 电平 | GPIO22 |
| 蜂鸣器地 | 地 | GND |
接线要点:
- 舵机不要接 3.3 V,电流不足会拖垮芯片,表现为复位或舵机抽动。
- 列线不要用 GPIO19、GPIO5,避免与 VSPI 冲突。
- 舵机、蜂鸣器不要用 GPIO34~39。
- 数字乱序时对调行/列线,或改
KeypadScan.h中的map表。
1.3 硬件连接图
数据流:键盘 / BLE 产生“开锁或上锁”请求 → 主循环校验 → 舵机转到解锁角或锁定角 → 屏幕与蜂鸣器反馈 → 事件写入 NVS,并经 BLE Notify 回传手机。
1.4 实物连接图
二、软件架构设计
固件是单任务循环,外加 BLE 协议栈任务。业务全部在loop()中串行执行,模块之间通过对象方法交互,不使用操作系统队列。
手机 App / 网页 4×4 键盘 │ │ │ 写 FFF1 │ 行扫列读 ▼ ▼ BleLock::onWrite KeypadScan::read │ │ └────────┬────────────┘ ▼ SmartLock.ino 状态机 handleBleCommand / handleKey │ ┌───────────┼────────────┬─────────────┐ ▼ ▼ ▼ ▼ Security ServoLock Buzzer EventLog (NVS 密码) (LEDC PWM) (LEDC 音调) (NVS 环形) │ ▼ DisplayUI ← UiModel(每圈填充,按脏标记刷新)核心状态(Mode)包括:Splash、Locked、Input、Fail、Success、Unlocked、Lockout、改密三步、Info、Logs、GuestView、Notice。显示层用平行的UiScreen枚举,由mapScreen()映射,避免 UI 直接依赖业务枚举。
2.1 系统初始化
setup()按依赖顺序启动外设,避免未初始化就刷屏或发蓝牙:
setCpuFrequencyMhz(80):降频减功耗。- 串口 115200,打印
SmartLock boot,便于确认固件已跑起来。 ui.begin():TFT 初始化、setRotation(4)、清屏。keypad.begin():行脚输出高、列脚上拉输入。buzzer.begin():无源则配置 LEDC,占空比先置 0。servoLock.begin():转到锁定角,延时后断开 PWM。eventLog.begin():从 NVS 命名空间evlog恢复环形缓冲与序号。security.begin():从smartlock读主密码与访客密码;非法长度则写入默认1234。ble.begin():设备名SmartLock,服务FFF0,写特征FFF1,通知特征FFF2,开始广播。- 记录启动时刻,清空密码缓冲,标记全屏重绘。
上电后先停留在欢迎页SPLASH_MS(2200 ms),再进入锁定待机。
2.2 主循环逻辑
loop()每一圈完成六件事,顺序固定:
- BLE 维护:
ble.loop()在断开后重新开始广播。 - 取命令:有写入则
handleBleCommand()(状态查询、上锁、日志、访客、纯数字开锁、U:/C:)。 - 取按键:有键则
handleKey(),按当前mode解释0-9 * # A B C D。 - 定时状态迁移:欢迎超时进锁定;失败/成功/提示页到时切走;锁定倒计时结束恢复待机;开锁满 8 秒自动上锁;输密超过 15 秒清空并返回锁定。
- 休眠与心跳:无操作满 20 秒且不在锁定中/已开锁/记录/访客等关键页时关屏;约每 400 ms 给倒计时页局部刷新;已连接时约每秒主动
OK:STATUS:…。 - 渲染:
fillModel()汇总密码掩码、剩余次数、倒计时、日志四行、访客码等,交给ui.render()。仅脏标记为真时才画。
自动上锁路径会转舵机、响锁定音、记一条上锁事件、通知OK:LOCKED,并弹出「已自动上锁」。蓝牙连上瞬间会OK:READY并立刻推一次状态,方便 App 刷新剩余次数。
三、代码拆分讲解
工程入口是SmartLock/SmartLock.ino,其余均为头文件模块,便于课程按文件讲解。
| 文件 | 职责 |
|---|---|
config.h | 引脚、舵机角度、蜂鸣器类型与音调、密码策略、BLE UUID、界面色、编译期引脚检查 |
KeypadScan.h | 自行扫描 4×4,去抖并等松手,不依赖第三方 Keypad 库 |
Buzzer.h | 无源 PWM 音调 / 有源高低电平;点击、成功三音、失败、报警、上锁 |
ServoLock.h | 50 Hz、0.5~2.5 ms 脉宽映射 0~180°;到位可detach |
Security.h | 主密码 NVS、失败计数与分级锁定、访客码生成/核销/过期 |
EventLog.h | 64 条环形日志、序号连续、格式化屏幕行与 BLE 行 |
DisplayUI.h | 中文绘制、状态栏、锁形图标、局部倒计时、睡眠命令 |
FontCN.h | 点阵字库(由tools/gen_font.py生成) |
BleLock.h | GATT 服务、写回调入队、Notify、断开再广播 |
SmartLock.ino | 状态机、开锁/上锁、键盘与 BLE 协议、UiModel 组装 |
3.1 键盘模块
逐行拉低,读列是否为低。确认按下后等待释放再返回字符,避免连发。键位表与实体键盘一致时,A上锁、B改密、C菜单、D清空才与说明书相符。
3.2 安全模块
verify()成功清零失败次数;失败累加,满 5 次按LOCKOUT_BASE_SEC << level进入锁定(封顶 300 秒)。改密必须旧密码正确,错误同样计入失败。访客码用esp_random()生成 6 位数字且避开主密码;consumeGuest()成功后置已用并写 NVS。注意:访客剩余时间以本次开机的millis()起算,重启后若密码尚未使用会重新按 24 小时计算。
3.3 日志模块
结构体含序号、累计秒、来源、类型、成败。缓冲区满则丢掉最旧一条。formatLine供屏幕(如#001 00:12 本地开锁),formatBle供手机(OK:LOG:序号,时间,来源,动作,结果)。
3.4 显示模块
markDirty()全刷,markPartial()只更新倒计时。睡眠发送 ST7789 的DISPOFF+SLPIN,唤醒SLPOUT+DISPON。按键或 BLE 活动会activity()唤醒。
3.5 蓝牙模块
FFF1 支持 Write 与 Write Without Response,兼容部分手机 GATT。FFF2 带 2902 描述符以便订阅通知。命令最长约 47 字节。
主文件协议摘要:
- 手机写:纯数字或
U:密码开锁;L上锁;S状态;H倒日志;G/GQ/GX生成、查询、作废访客;C:旧:新改密。 - 设备通知:
OK:STATUS:LOCKED|UNLOCKED|LOCKOUT:剩余次数:锁定秒;OK:UNLOCKED:远程;OK:GUEST:ACTIVE:密码:剩余秒;以及ERR:WRONG等。
四、操作过程及数据展示
4.1 操作步骤
编译与烧录
- 用 零知派 打开
SmartLock/SmartLock.ino。 - 开发板选 零知ESP32;上传后打开串口,应看到
SmartLock boot。 - 有源蜂鸣器把
BUZZER_PASSIVE改为0,无源蜂鸣器把BUZZER_PASSIVE改为1。锁舌方向反了则对调SERVO_ANGLE_LOCKED与SERVO_ANGLE_UNLOCKED。
本地开锁 / 上锁
- 屏幕显示锁定与「蓝牙未连接 / 已连接」。
- 输入 4~8 位数字,
*删一位,#确认。正确则舵机转到开锁角,提示成功来源「本地」,进入已开锁页并显示剩余秒数。 - 满 8 秒自动上锁,或按
A/#立即上锁。 - 输错会减剩余次数;5 次后进入锁定页,倒计时结束才能再试。
修改主密码(键盘)
锁定界面按B→ 旧密码#→ 新密码#→ 再输入一次#。两次不一致会提示并回到新密码步。旧密码错误计入失败次数。
菜单、记录、访客(键盘)
| 按键 | 作用 |
|---|---|
C | 进入菜单 |
1 | 开锁记录(*较旧页,D较新页,#返回菜单) |
2 | 访客页:再按2生成,D作废,#返回 |
D(输入页) | 清空已输入数字 |
手机开锁(推荐网页)
- 用支持 Web Bluetooth 的浏览器打开
tools/ble_test.html,扫描并连接SmartLock。 - 输入主密码或访客码,点开锁;可一键上锁、刷新状态。
- 页面会解析
OK:STATUS,显示门锁状态、剩余次数、锁定倒计时。 - 可生成 / 查询 / 作废访客密码,拉取记录并导出 CSV。
App Inventor 制作步骤见docs/app-inventor.md。Android 扫描 BLE 通常需打开定位并授予附近设备权限。
忘记密码:零知派工具菜单擦除 Flash 后重新烧录,主密码恢复为DEFAULT_PIN。只改代码里的默认值而不擦除NVS,不会覆盖已经存过的密码。
4.2 数据展示(屏幕与协议)
屏幕典型信息:顶部蓝牙状态;锁定页密码圆点(错误时变红);已开锁页自动上锁秒数;锁定页剩余秒;记录页每行#序号 MM:SS 来源动作;访客页 6 位数字与剩余时间。
串口:KEY: 5、BLE: 1234等,便于对照操作。
状态通知示例:OK:STATUS:LOCKED:5:0表示已锁定、还可试 5 次、未在惩罚锁定。OK:STATUS:UNLOCKED:5:0为已开锁。OK:STATUS:LOCKOUT:0:28为惩罚锁定还剩 28 秒。
日志 BLE 流:OK:LOG:BEGIN:条数→ 多条OK:LOG:…→OK:LOG:END。网页可将记录与通信日志分别导出 CSV。
4.3 演示视频
零知ESP32--4×4 矩阵键盘智能密码锁
五、技术原理
5.1 工作原理
矩阵键盘:行作输出、列作上拉输入。某一键按下时,对应列被拉到该行的低电平,扫描到「行 r、列 c」后查表得到字符。去抖延时约 25 ms,并等待释放,保证一次按键只产生一个事件。
舵机:50 Hz 周期 20 ms。脉宽约 0.5 ms 对应 0°、2.5 ms 对应 180°。固件默认锁定 0°、开锁 90°。转到目标角后断开 LEDC,引脚拉低,减轻抖动与功耗。
无源蜂鸣器:LEDC 输出方波,频率决定音调。有源蜂鸣器则只拉高/拉低,忽略频率宏。
密码校验:开锁时先看访客码是否匹配且未过期、未使用;再比主密码。访客优先,避免与主密码冲突时的歧义(生成时已尽量避开主密码)。失败写入日志并可能进入 lockout。
BLE GATT:手机对 FFF1 写入 UTF-8 文本;设备对 FFF2 Notify。未做配对加密,附近设备理论上可嗅探明文,仅适合教学。连接后固件周期性推送状态,网页用 1 秒轮询S也可达到同样效果。
NVS:Preferences命名空间smartlock存pin、gpin、gused;evlog存二进制环形数组、条数与序号。擦除整片 Flash 会清空二者。
显示局部刷新:已开锁与惩罚锁定页数字每约 0.4 s 变化一次。若屏幕未变且仅秒数变化,只重绘倒计时区域,避免整屏闪烁。
5.2 工作模式配置
全部策略在config.h,按演示需求修改后重新编译即可。
| 宏 | 默认值 | 含义 |
|---|---|---|
PIN_MIN_LEN/PIN_MAX_LEN | 4 / 8 | 主密码长度 |
DEFAULT_PIN | "1234" | 仅 NVS 为空或非法时写入 |
MAX_FAIL_ATTEMPTS | 5 | 连续失败次数上限 |
LOCKOUT_BASE_SEC | 30 | 首次锁定秒数 |
LOCKOUT_MAX_SEC | 300 | 锁定上限 |
AUTO_RELOCK_SEC | 8 | 开锁后自动上锁 |
INPUT_TIMEOUT_MS | 15000 | 输密超时回锁定 |
DISPLAY_SLEEP_MS | 20000 | 无操作关屏 |
LOG_CAP | 64 | 日志容量 |
GUEST_PIN_LEN | 6 | 访客密码位数 |
GUEST_TTL_MS | 24 小时 | 访客有效期 |
CPU_FREQ_MHZ | 80 | CPU 频率 |
BLE_TX_POWER | −3 dBm | 广播发射功率 |
BLE_DEVICE_NAME | SmartLock | 扫描名称 |
SERVO_ANGLE_LOCKED/UNLOCKED | 0 / 90 | 锁舌角度 |
SERVO_DETACH_AFTER_MOVE | 1 | 到位断开 PWM |
BUZZER_PASSIVE | 1 | 1 无源,0 有源 |
TFT_ROTATION | 4 | 屏幕方向 |
BLE UUID 亦可在 App Inventor 中填短形式FFF0、FFF1、FFF2。改设备名后需重新烧录,手机端按新名称扫描。
六、常见问题指引
| 现象 | 可能原因与处理 |
|---|---|
| 花屏、偏色 | TFT_eSPI 中开关TFT_INVERSION_ON;确认ST7789_DRIVER且宽高为 240。 |
| 旋转后画面左移、右侧空白 | 打开CGRAM_OFFSET;rotation 4 的 240×240 分支使用colstart=80,不要改成 0。 |
| 键盘无响应或乱码 | 检查排针左右顺序;确认列脚不是 19/5;GPIO5 被按住可能影响启动,可上电后再接键盘。 |
| 舵机抽动、ESP32 反复复位 | 5 V 供电不足,改外部 5 V 并共地;勿用 3.3 V 供舵机。 |
| 舵机、蜂鸣器完全没反应 | 禁止 GPIO34~39;改回 21 与 22。 |
| 有源蜂鸣器一直响或不响 | BUZZER_PASSIVE 0;低电平触发再设BUZZER_ACTIVE_HIGH 0。 |
| 手机搜不到 | 串口须有SmartLock boot,建议用EDGE浏览器打开;开手机蓝牙与定位;本设备是 BLE,不能用经典蓝牙串口助手。 |
| 能连不能开锁 | UUID 改完整 128 位;写入 UTF-8 且utf16=false;App Inventor 的WriteStrings必须传列表;可先发纯数字密码。 |
| 改密后忘记密码 | 擦除 Flash 再烧录,恢复DEFAULT_PIN。 |
只改了代码里的1234仍是旧密码 | 密码已在 NVS,需擦除或用改密流程覆盖。 |
| 访客时限重启后变回 24 小时 | 未使用的访客码会保留,计时基准在开机时重置。 |