ESP32项目 --- 智能门锁(WiFi 蓝牙 OTA)
1 项目简介
1.1 项目概述
本项目是实现一款智能门锁中的智能控制部分, 可以应用在家庭, 办公室等任何使用门锁的场所.
本项目实现了以下主要功能:
(1)通过按键配置密码
(2)通过按键输入密码开锁
(3)录入指纹
(4)通过录入的指纹开锁
(5)通过蓝牙配置密码
(6)语音播报模块
(7)通过蓝牙输入密码开锁
(8)通过WIFI实现OTA在线升级
1.2 功能概述
智能门锁使用的主控芯片为ESP32-C3, 其他功能模块包括:
(1)电容触摸按键. 一共有提供12个电容触摸按键, 分别为数字0-9, M和#
(2)单总线全彩LED. 分别为每个电容触摸按键提供了一个单总线全彩LED, 当按键被按下时可以进行灯光提示.
(3)指纹模块. 指纹模块可以采集指纹
(4)蓝牙模块. 由esp32-c3芯片提供. 用来接收用户手机蓝牙传来的密码, 匹配成功之后,执行开锁动作.
(5)语音播报模块. 当用户执行了一些操作之后, 给用户进行相应的语音播报提示.
(6)WIFI模块. 由esp32-c3芯片提供. 用来进行OTA下载最新固件,实现在线固件升级电机. 使用esp32的GPIO来控制电机的转动,达到开锁的目的
开发流程
ESP32开发环境搭建
- 下载ESP-IDF离线安装包
下载地址: https://dl.espressif.cn/dl/esp-idf/?idf=4.4
- 安装
注意我们使用的是ESP32-C3,所以安装时要选择对型号
- 在VSCode安装扩展ESP-IDF
- 使用ESP插件配置ESP环境
- 在vscode中, 点击菜单查看->命令面板
- 输入: Configure esp, 然后选择第一项
- 点击EXPRESS进入配置界面
- 安装
- 注意:安装报错
如果在安装的过程中报错"…python.exe -m pip" is not valid. (ERROR_INVALID_PIP), 则去esp-idf的安装目录:Espressif\tools, 把idf-python目录删除, 然后再点Install重新安装即可.
创建项目
- 配置Flash大小
- 配置时钟滴答定时器
ESP32的开发案例
gpio
- Espressif\frameworks\esp-idf-v5.3.1\examples\peripherals\gpio\generic_gpio
官方给的初始化写法:
//句柄
gpio_config_t io_conf = {};
//中断
io_conf.intr_type = GPIO_INTR_DISABLE;
//模式
io_conf.mode = GPIO_MODE_OUTPUT;
//引脚屏蔽位
io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
//下拉
io_conf.pull_down_en = 0;
//上拉
io_conf.pull_up_en = 0;
//让配置信息生效
gpio_config(&io_conf);
gpio相关函数:
esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level);
int gpio_get_level(gpio_num_t gpio_num)
i2c
- 参考示例:
https://gitee.com/EspressifSystems/esp-idf/blob/release/v4.4/examples/peripherals/i2c/i2c_simple/main/i2c_simple_main.c
#define I2C_MASTER_SCL_IO CONFIG_I2C_MASTER_SCL //时钟引脚
#define I2C_MASTER_SDA_IO CONFIG_I2C_MASTER_SDA //数据引脚
#define I2C_MASTER_NUM 0 //IIC端口号,本芯片是有一个
#define I2C_MASTER_FREQ_HZ 400000 //IIC频率
#define I2C_MASTER_TX_BUF_DISABLE 0 //master无需提供
#define I2C_MASTER_RX_BUF_DISABLE 0 //master无需提供
#define I2C_MASTER_TIMEOUT_MS 1000
- 相关函数
i2c_master_write_read_device() //根据这个设备读写
static esp_err_t i2c_master_init(void)
{
int i2c_master_port = I2C_MASTER_NUM;
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO, //可读可写
.scl_io_num = I2C_MASTER_SCL_IO, //可读可写
.sda_pullup_en = GPIO_PULLUP_ENABLE, //上拉
.scl_pullup_en = GPIO_PULLUP_ENABLE, //上拉
.master.clk_speed = I2C_MASTER_FREQ_HZ, //传输速率
};
i2c_param_config(i2c_master_port, &conf);
return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
}
rmt
- examples\peripherals\rmt\led_strip_simple_encoder
//1. 宏定义
//2. 时序 ( 0时序,1时序,重置时序)
//3. 编码器回调
//4. 初始化(发送通道 简单编码器)
跟着案例走
...
nvs
- 参考案例: examples\storage\nvs_rw_value
一些常用的函数:
static nvs_handle_t my_handle; //声明NVS操作句柄
esp_err_t err = nvs_flash_init();
err = nvs_open("pwd", NVS_READWRITE, &my_handle); //打开NVS命名空间
nvs_get_u8(my_handle, char* key, uint8_t* value);
nvs_set_u8(my_handle, char* key, value);
nvs_find_key(my_handle,char* key,NULL);
nvs_erase_key(my_handle,char* key);
例:
- Dri_NVS.c
#include "Dri_NVS.h"
//声明NVS操作句柄
static nvs_handle_t my_handle;
/**
* @brief 初始化NVS Flash
*
*/
void Dri_NVS_Init(void)
{
//1. 初始化NVS_Flash
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
//2.打开NVS命名空间
err = nvs_open("pwd", NVS_READWRITE, &my_handle);
}
/**
* @brief 读取u8数据
*
* @param key
* @param value
* @return esp_err_t
*/
esp_err_t Dri_NVS_ReadU8(char *key, uint8_t *value)
{
return nvs_get_u8(my_handle, key, value);
}
/**
* @brief 写入u8数据
*
* @param key
* @param value
* @return esp_err_t
*/
esp_err_t Dri_NVS_WriteU8(char *key, uint8_t value)
{
//判断 key 是否存在
if (nvs_find_key(my_handle, key,NULL) == ESP_OK)
{
//如果存在就不用存储 直接返回
return ESP_FAIL;
}
else
{
//存储数据
return nvs_set_u8(my_handle, key, value);
}
}
/**
* @brief 删除数据
*
* @param key
* @return esp_err_t
*/
esp_err_t Dri_NVS_DeletePwd(char *key)
{
//判断 key 是否存在
if (nvs_find_key(my_handle, key,NULL) == ESP_OK)
{
//存在再执行删除操作
return nvs_erase_key(my_handle, key);
}
else
{
//不存在则不用删除 直接返回
return ESP_FAIL;
}
}
/**
* @brief 判断验证密码是否存在
*
* @param key
* @return esp_err_t
*/
esp_err_t Dri_NVS_IsPwdExists(char *key)
{
return nvs_find_key(my_handle, key,NULL);
}
uart
Espressif\frameworks\esp-idf-v5.3.1\examples\peripherals\uart\uart_async_rxtxtasks
void init(void)
{
const uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
// We won't use a buffer for sending data.
uart_driver_install(UART_NUM_1, RX_BUF_SIZE * 2, 0, 0, NULL, 0);
uart_param_config(UART_NUM_1, &uart_config);
uart_set_pin(UART_NUM_1, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}
...
uart_write_bytes(UART_NUM_1, data, len);
...
uart_read_bytes(UART_NUM_1, data, RX_BUF_SIZE, 1000 / portTICK_PERIOD_MS);
...
2 硬件选型
2.1 主控芯片
- ESP32-C3
(1)ESP-RISC-V CPU 是基于 RISC-V ISA 的 32 位内核
(2)ESP32-C3芯片有22个物理通用输入输出管脚(GPIOPin)
(3)支持 2.4 GHz Wi-Fi 和 Bluetooth 5 (LE)
(4)通信模块: ESP32 有2组串口,1个IIC,3个SPI控制器:SPI0, SPI1和GP-SPI2
(5)存储器:
- 4MB 的内部Flash(ESP32-C3FN4), 用于存储较大的固件和应用程序代码
- 384KB内部ROM, 用于存储引导程序和固件的一小部分
- 400KB内部SRAM, 用于运行时的高速数据存储和缓存
(6) 时钟频率高达160MHz
(7) ADC,两个12位逐次逼近型模拟数字转换器(SARADC):SARADC1和SARADC2,共支持六个通道的模拟信号检测
(8) RMT(红外收发器)是一个红外发送和接收控制器,支持多种红外协议。RMT模块可以实现将模块内置RAM中的脉冲编码转换为信号输出,或将模块的输入信号转换为脉冲编码存入RAM中。
- CPU 内核架构包含中断控制器 (INTC)、调试模块 (DM) 和用于访问存储器和外设的系统总线 (SYS BUS) 接口。
2.2 按键芯片
- SC12B 2036
一根总线 通过IIC与CPU连接,内部寄存器是八位,使用两个寄存器中的12位才能表示12个按键
IIC参考示例:
https://gitee.com/EspressifSystems/esp-idf/blob/release/v4.4/examples/peripherals/i2c/i2c_simple/main/i2c_simple_main.c
2.3 单总线全彩LED
- WS2812
24位全彩灯 一个Pin一个Pout
2.4 语音模块
- WTN6170
串口时序 先把数据线拉低 4~20ms 后,推荐 10ms,发送 8 位数据,先发送低位,再发送高位,使用高电平和低电
平比例来表示每个数据位的值。
保持高低电平3:1 表示 1
保持高低电平1:3 表示 0
2.5 指纹采集
- HLK-FPM383F
2.6 直流电机驱动
- BDR6120S
直流有刷
3 功能实现
- 软件架构
按键输入密码开锁
通用层:
- common_config.h
#include "esp_task.h"
#include "sys/unistd.h"
typedef enum
{
Com_OK,
Com_ERROR,
Com_TIMEOUT,
Com_OTHER,
} Com_Status;
#define delay_us(x) usleep(x)
#define delay_ms(x) vTaskDelay(x / portTICK_PERIOD_MS)
语音模块
根据数据手册的时序图以及定制的指令语音对应表进行操作
- Inf_WTN6170.c
...
/**
* @brief 发送语音指令
*
* @param cmd
*/
void Inf_WTN6170_SendCmd(uint8_t cmd)
{
//1. 拉低并延时10ms
WTN6170_SDA_L;
delay_ms(10);
//2. 发送数据位
for (uint8_t i = 0; i < 8; i++)
{
if (cmd & 0x01)
{
WTN6170_SDA_H;
delay_us(600);
WTN6170_SDA_L;
delay_us(200);
}
else
{
WTN6170_SDA_H;
delay_us(200);
WTN6170_SDA_L;
delay_us(600);
}
cmd >>= 1;
}
//3. 最后拉高
WTN6170_SDA_H;
delay_ms(5);
}
触控键盘
与主控通过IIC通信.
- 读流程
- 寄存器地址
按键信息寄存器 Output0 (地址 08H) Output1 (地址 09H)
CH[11:0] 分别对应 CIN[11:0]的按键情况。 无按键时为0, 有按键时为1。
- 设备地址
数据手册给的案例是软件模拟IIC,太过麻烦.但最新版本的工具包把简单案例删除了,所以找到了之前版本的简单案例如下.
- 参考示例:
https://gitee.com/EspressifSystems/esp-idf/blob/release/v4.4/examples/peripherals/i2c/i2c_simple/main/i2c_simple_main.c
- IIC接线图
- Inf_SC12B.h
...
typedef enum
{
KEY_0,
KEY_1,
KEY_2,
KEY_3,
KEY_4,
KEY_5,
KEY_6,
KEY_7,
KEY_8,
KEY_9,
KEY_SHARP,
KEY_M,
KEY_NO
} Touch_Key;
//数据引脚
#define I2C_MASTER_SDA_IO GPIO_NUM_2
#define I2C_MASTER_SCL_IO GPIO_NUM_1
#define I2C_MASTER_INTR GPIO_NUM_0
#define SC12B_I2C_ADDR 0x40
#define I2C_MASTER_FREQ_HZ 100000
- Inf_SC12B.c
#include "Inf/Inf_SC12B.h"
/**
* @brief 读取寄存器函数
*
* @param reg
* @return uint8_t
*/
uint8_t Inf_SC12B_ReadReg(uint8_t reg)
{
uint8_t data = 0;
i2c_master_write_read_device(I2C_NUM_0,
SC12B_I2C_ADDR,
®,
1,
&data,
1,
2000
);
return data;
}
uint8_t isTouch = 0;
/**
* @brief 按键中断回调函数
*
* @param arg
*/
void SC12B_Handler(void *arg)
{
isTouch = 1;
}
/**
* @brief 初始化ESP32-C3 的I2C模块
*
*/
void Inf_SC12B_Init(void)
{
//1. 设置i2c参数
i2c_config_t config = {
.mode = I2C_MODE_MASTER,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_pullup_en = GPIO_PULLUP_ENABLE, //IIC必须配置上拉
.sda_pullup_en = GPIO_PULLUP_ENABLE, //IIC必须配置上拉
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
//2. 使配置生效
i2c_param_config(I2C_NUM_0, &config);
//3. 开启iic模块
i2c_driver_install(I2C_NUM_0,
config.mode,
0,0,//主模式下不用分配接收和发送缓冲区
0 //中断优先级
);
/*4. 中断引脚配置*/
//4.1 引脚工作配置信息
gpio_config_t io_config = {
.intr_type = GPIO_INTR_POSEDGE, //上升沿触发
.mode = GPIO_MODE_INPUT, //输入
.pull_down_en = GPIO_PULLDOWN_ENABLE, //下拉
.pull_up_en = GPIO_PULLUP_DISABLE,
.pin_bit_mask = 1 << I2C_MASTER_INTR,
};
//4.2 让配置项生效
gpio_config(&io_config);
//4.3 安装ISR服务
gpio_install_isr_service(0);
//4.4 将引脚与回调函数绑定
gpio_isr_handler_add(I2C_MASTER_INTR,SC12B_Handler,(void *)I2C_MASTER_INTR);
}
/**
* @description: 获取按下的按键值
* 1.读取08和09寄存器中的数据
* 2.拼接读取到的两部分数据
* 3.判断按下的是哪一个按键
* 4.输出
* @return {*}
*/
Touch_Key Inf_SC12B_ReadKey(void)
{
// 1.读取08和09寄存器中的数据
uint8_t data1 = Inf_SC12B_ReadReg(0x08);
uint8_t data2 = Inf_SC12B_ReadReg(0x09);
// 2.拼接读取到的两部分数据
uint16_t key = (data1 << 8) | data2;
Touch_Key touchKey = KEY_NO;
// 3.判断按下的是哪一个按键
switch (key)
{
case 0x8000:
touchKey = KEY_0;
break;
case 0x4000:
touchKey = KEY_1;
break;
case 0x2000:
touchKey = KEY_2;
break;
case 0x1000:
touchKey = KEY_3;
break;
case 0x0100:
touchKey = KEY_4;
break;
case 0x0400:
touchKey = KEY_5;
break;
case 0x0200:
touchKey = KEY_6;
break;
case 0x0800:
touchKey = KEY_7;
break;
case 0x0040:
touchKey = KEY_8;
break;
case 0x0020:
touchKey = KEY_9;
break;
case 0x0010:
touchKey = KEY_SHARP;
break;
case 0x0080:
touchKey = KEY_M;
break;
default:
break;
}
// 4.输出
return touchKey;
}
Touch_Key Inf_SC2B_KeyClick(void)
{
Touch_Key key = KEY_NO;
if(isTouch) /* 如果有按键按下 */
{
key = Inf_SC12B_ReadKey(); /* 读取按下的按键 */
isTouch = 0;
}
return key;
}
全色LED灯
全色LED需要的时序信号可用使用红外线外设精确生成.代码可用从官方案例直接移植修改: examples\peripherals\rmt\led_strip_simple_encoder
- Inf_WS2812.h
#include "Inf_WS2812.h"
static uint8_t led_strip_pixels[EXAMPLE_LED_NUMBERS * 3];
static rmt_channel_handle_t led_chan = NULL;
static rmt_encoder_handle_t simple_encoder = NULL;
/* 定义几种常见颜色 */
uint8_t black[3] = {0, 0, 0};
uint8_t white[3] = {255, 255, 255};
uint8_t red[3] = {0, 255, 0};
uint8_t green[3] = {255, 0, 0};
uint8_t blue[3] = {0, 0, 255};
uint8_t cyan[3] = {255, 0, 255}; /* 青色 */
uint8_t purple[3] = {0, 255, 255}; /* 紫色 */
//0时序
static const rmt_symbol_word_t ws2812_zero = {
.level0 = 1,
.duration0 = 0.3 * RMT_LED_STRIP_RESOLUTION_HZ / 1000000, // T0H=0.3us
.level1 = 0,
.duration1 = 0.9 * RMT_LED_STRIP_RESOLUTION_HZ / 1000000, // T0L=0.9us
};
//1时序
static const rmt_symbol_word_t ws2812_one = {
.level0 = 1,
.duration0 = 0.9 * RMT_LED_STRIP_RESOLUTION_HZ / 1000000, // T1H=0.9us
.level1 = 0,
.duration1 = 0.3 * RMT_LED_STRIP_RESOLUTION_HZ / 1000000, // T1L=0.3us
};
//reset defaults to 50uS
static const rmt_symbol_word_t ws2812_reset = {
.level0 = 1,
.duration0 = RMT_LED_STRIP_RESOLUTION_HZ / 1000000 * 50 / 2,
.level1 = 0,
.duration1 = RMT_LED_STRIP_RESOLUTION_HZ / 1000000 * 50 / 2,
};
//编码器回调
static size_t encoder_callback(const void *data, size_t data_size,
size_t symbols_written, size_t symbols_free,
rmt_symbol_word_t *symbols, bool *done, void *arg)
{
// We need a minimum of 8 symbol spaces to encode a byte. We only
// need one to encode a reset, but it's simpler to simply demand that
// there are 8 symbol spaces free to write anything.
if (symbols_free < 8) {
return 0;
}
// We can calculate where in the data we are from the symbol pos.
// Alternatively, we could use some counter referenced by the arg
// parameter to keep track of this.
size_t data_pos = symbols_written / 8;
uint8_t *data_bytes = (uint8_t*)data;
if (data_pos < data_size) {
// Encode a byte
size_t symbol_pos = 0;
for (int bitmask = 0x80; bitmask != 0; bitmask >>= 1) {
if (data_bytes[data_pos]&bitmask) {
symbols[symbol_pos++] = ws2812_one;
} else {
symbols[symbol_pos++] = ws2812_zero;
}
}
// We're done; we should have written 8 symbols.
return symbol_pos;
} else {
//All bytes already are encoded.
//Encode the reset, and we're done.
symbols[0] = ws2812_reset;
*done = 1; //Indicate end of the transaction.
return 1; //we only wrote one symbol
}
}
void Inf_WS2812_Init(void)
{
//1. 构建发送通道函数
rmt_tx_channel_config_t tx_chan_config = {
.clk_src = RMT_CLK_SRC_DEFAULT, // select source clock
.gpio_num = RMT_LED_STRIP_GPIO_NUM,
.mem_block_symbols = 64, // increase the block size can make the LED less flickering
.resolution_hz = RMT_LED_STRIP_RESOLUTION_HZ,
.trans_queue_depth = 4, // set the number of transactions that can be pending in the background
};
//2. 创建发送通道
ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &led_chan));
//3. 简单编码器配置信息
const rmt_simple_encoder_config_t simple_encoder_cfg = {
.callback = encoder_callback
//Note we don't set min_chunk_size here as the default of 64 is good enough.
};
//4. 创建简单编码器
ESP_ERROR_CHECK(rmt_new_simple_encoder(&simple_encoder_cfg, &simple_encoder));
//5. 启动发送通道
ESP_ERROR_CHECK(rmt_enable(led_chan));
}
void Inf_WS2812_LightLed(void)
{
rmt_transmit_config_t tx_config = {
.loop_count = 0, // no transfer loop
};
// Flush RGB values to LEDs
rmt_transmit(led_chan, simple_encoder, led_strip_pixels, sizeof(led_strip_pixels), &tx_config);
rmt_tx_wait_all_done(led_chan, 0xffff);
}
/**
* @brief 所有灯亮同一个颜色
*
* @param color
*/
void Inf_WS2812_LightAllLeds(uint8_t color[])
{
for (uint8_t i = 0; i < EXAMPLE_LED_NUMBERS; i++)
{
memcpy(&led_strip_pixels[i * 3], color, 3);
}
//亮灯
Inf_WS2812_LightLed();
}
/**
* @brief 指定灯亮一个颜色
*
* @param index 指定灯索引
* @param color 指定灯颜色
*/
void Inf_WS2812_LightKeyLed(uint8_t index,uint8_t color[])
{
//1. 先关闭所有灯
Inf_WS2812_LightAllLeds(black);
//2. 修改指定灯位置的颜色
memcpy(&led_strip_pixels[index * 3], color, 3);
//3. 刷新灯
Inf_WS2812_LightLed();
}
App层,按键逻辑
- App_IO.h
...
#define PWD_CT "pwd_ct"
extern TaskHandle_t FingerScanHandler;
extern TaskHandle_t otaHandler;
extern uint8_t isHasFinger;
/* 输入状态 */
typedef enum
{
FREE = 0, /* 空闲状态 */
INPUT, /* 输入阶段 */
DONE /* 输入完成 */
} Input_Status;
/* 密码操作状态 */
typedef enum
{
ADD = 0, /* 添加密码 */
DELETE, /* 删除密码 */
CHECK /* 校验密码 */
} Pwd_Op_Status;
...
- App_IO.c
没有按键逻辑处理:
/**
* @description: 按键扫描
*
* 密码输入和设定 状态机: 共分为3个状态
* 0:自由状态: 默认状态. 在此状态下, 如果检测到有任何按键, 则进入 1:密码输入阶段
*
* 1:密码输入阶段
* 保存密码
* 2:输入完成阶段
* 对输入密码根据协议进行各种处理
*
* @return {*}
*/
Input_Status inputStatus = FREE;
Pwd_Op_Status pwdOpStatus = CHECK;
uint8_t password[100] = {0};
uint8_t pwdLen = 0;
void App_IO_KeyScan(void)
{
// 定义没有按键时间
static uint16_t noKeyTime = 0;
// 读取按键
Touch_Key key = Inf_SC12B_ReadKey();
if (key == KEY_NO)
{
noKeyTime++;
if (noKeyTime >= 100) // 如果超过5s种没有按键按下则进入空闲状态
{
inputStatus = FREE;
Inf_WS2812_LightAllLeds(black);
noKeyTime = 100; // 防止溢出
// 清理前置所有输入
pwdLen = 0;
memset(password, 0, sizeof(password));
}
return;
}
else
{
// 一旦按下按键,重新开始计时
noKeyTime = 0;
printf("Key = %d\r\n", key);
switch (inputStatus)
{
case FREE:
Inf_WS2812_LightAllLeds(white);
inputStatus = INPUT;
break;
case INPUT:
// 无论是那种按键,统一逻辑处理:亮按键灯,响水滴声
Inf_WS2812_LightAllLeds(black);
delay_ms(10);
Inf_WS2812_LightKeyLed((uint8_t)key, purple);
sayWaterDrop();
delay_ms(500);
// 根据具体按键,做不同逻辑业务处理
if (key == KEY_M)
{
printf("按下M键,非法输入,清除前置所有数据\r\n");
inputStatus = FREE;
Inf_WS2812_LightAllLeds(black);
delay_ms(50);
sayIllegalOperation();
// 清理前置所有输入
pwdLen = 0;
memset(password, 0, sizeof(password));
}
else if (key == KEY_SHARP)
{
// #按下,根据前置输入进行逻辑处理
inputStatus = DONE;
// 调用逻辑处理函数
App_IO_InputHandler();
// 恢复空闲状态
inputStatus = INPUT;
// 清理前置所有输入
pwdLen = 0;
memset(password, 0, sizeof(password));
}
else
{
// 数值键被按下,保持到临时存储
password[pwdLen++] = key + 48; // 将数字转为字符 1 ==> '1'
}
break;
default:
break;
}
}
}
按 # 逻辑处理 根据数字个数处理:
/*键盘输入协议:
1. 所有输入都是以 # 结束
2. 输入M位非法输入, 以前所有输入作废
3. 协议规则
01# 新增密码
02# 删除密码10# 新增指纹 11# 删除指纹 21# OTA更新 ...
- 数字超过2位的认为是在输入密码开门
*/
void App_IO_InputHandler(void)
{
// 如果输入的数字 < 2 ,则为非法操作
if (pwdLen < 2)
{
printf("输入长度小于2位,非法操作\r\n");
sayIllegalOperation();
}
else if (pwdLen == 2)
{
// 输入的为操作指令
if (password[0] == '0' && password[1] == '1') // 添加密码指令
{
delay_ms(1000); // 首尾加点延迟让第一次闪烁消失
// isAdd = 1;
pwdOpStatus = ADD;
sayAddUser();
delay_ms(2000);
sayPassword();
delay_ms(50);
delay_ms(50); // 首尾加点延迟让第一次闪烁消失
}
else if (password[0] == '0' && password[1] == '2') // 删除密码指令
{
// isDel = 1;
pwdOpStatus = DELETE;
sayDelUser();
delay_ms(2000);
sayPassword();
delay_ms(50);
}
else if (password[0] == '1' && password[1] == '1') // 添加指纹指令
{
// 通知指纹扫描业务,录入指纹
xTaskNotify(FingerScanHandler,(uint32_t)'1',eSetValueWithOverwrite);
}
else if (password[0] == '1' && password[1] == '2') // 删除指纹指令
{
// 通知指纹扫描业务,删除指纹
xTaskNotify(FingerScanHandler,(uint32_t)'2',eSetValueWithOverwrite);
}
else if (password[0] == '1' && password[1] == '3') // 删除指纹库指令
{
// 通知指纹扫描业务,删除所有指纹
xTaskNotify(FingerScanHandler,(uint32_t)'3',eSetValueWithOverwrite);
}
else if (password[0] == '2' && password[1] == '1') // OTA升级指令
{
// 通知OTA升级业务
xTaskNotify(otaHandler,(uint32_t)'4',eSetValueWithOverwrite);
}
else
{
printf("输入指令不存在\r\n");
sayIllegalOperation();
}
}
else
{
if (pwdLen < 5 || pwdLen > 10)
{
printf("密码长度不规范\r\n");
sayIllegalOperation();
delay_ms(50);
}
else
{
switch (pwdOpStatus)
{
case ADD:
App_IO_AddPwd(password);
pwdOpStatus = CHECK;
break;
case DELETE:
App_IO_DelPwd(password);
pwdOpStatus = CHECK;
break;
case CHECK:
App_IO_CheckPwd(password);
break;
default:
break;
}
}
}
}
/**
* @brief 添加密码
*
*/
void App_IO_AddPwd(uint8_t pwd[])
{
// 限定密码存储上限为100
uint8_t pwdCount = 0;
// 读取Flash中存储的密码个数
Dri_NVS_ReadU8(PWD_CT, &pwdCount);
if (pwdCount >= 100)
{
printf("密码数量已达上限\r\n");
sayPasswordAddFail();
}
else
{
if (Dri_NVS_IsPwdExists((char *)pwd) == ESP_OK)
{
sayPasswordAddFail();
return;
}
// 存储密码
esp_err_t err = Dri_NVS_WriteU8((char *)pwd, 0);
if (err == ESP_OK)
{
sayPasswordAddSucc();
delay_ms(2000);
// 将个数+1 并将密码存储进Flash
pwdCount++;
Dri_NVS_WriteU8(PWD_CT, pwdCount);
printf("密码个数:%d\r\n", pwdCount);
}
else
{
sayPasswordAddFail();
delay_ms(2000);
}
}
}
/**
* @brief 删除密码
*
*/
void App_IO_DelPwd(uint8_t pwd[])
{
esp_err_t err = Dri_NVS_DeletePwd((char *)pwd);
if (err == ESP_OK)
{
sayDelSucc();
// 将密码个数-1
uint8_t pwdCt = 0;
Dri_NVS_ReadU8(PWD_CT, &pwdCt);
Dri_NVS_WriteU8(PWD_CT, pwdCt - 1);
}
else
{
sayDelFail();
}
}
/**
* @brief 校验开锁
*
*/
void App_IO_CheckPwd(uint8_t pwd[])
{
esp_err_t err = Dri_NVS_IsPwdExists((char *)pwd);
if (err == ESP_OK)
{
// 验证成功,执行开锁
sayPasswordVerifySucc();
delay_ms(2000);
Inf_BDR6120_OpenLock();
sayDoorOpen();
delay_ms(50);
}
else
{
// 验证失败,重试
sayPasswordVerifyFail();
delay_ms(2000);
sayRetry();
}
}
指纹模块
HLK-FPM583F接口支持UART,UART默认波特率为57600。
本次使用的芯片特征:
a) UART 缺省波特率为 57.6Kbps,数据格式:8 位数据位,1 位停止位(用户手册写的两位,但实测两位不行),无校验位;
b) UART 波特率可以通过指令进行设置,范围从 9600 至 115200;
c) 如果主控是 MCU(3.3V),则直接与 UART_TD 和 UART_RD 连接;如果主控是 PC,则需要挂接
RS232 电平转换设备。
- Inf_FPM383.c
/**
* @brief 初始化,用到了uart
*
*/
void Inf_FPM383_Init(void)
{
// 1. 参数列表
const uart_config_t uart_config = {
.baud_rate = 57600,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
// 2.安装uart服务
uart_driver_install(UART_NUM_1, 1024 * 2, 0, 0, NULL, 0);
// 3.让配置信息生效
uart_param_config(UART_NUM_1, &uart_config);
// 4. 绑定引脚
uart_set_pin(UART_NUM_1, FPM_TX_PIN, FPM_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
// 5. 处理中断
// 5.1 中断引脚相关配置
gpio_config_t io_config = {
.intr_type = GPIO_INTR_POSEDGE,
.mode = GPIO_MODE_INPUT,
.pull_down_en = GPIO_PULLDOWN_ENABLE,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pin_bit_mask = (1ULL << FPM_INTR_PIN),
};
// 5.2 配置信息生效
gpio_config(&io_config);
// 5.3 采用默认配置
gpio_install_isr_service(0);
// 5.4 添加中断的回调函数
gpio_isr_handler_add(FPM_INTR_PIN, Inf_FPM383_Intr_Handler, (void *)FPM_INTR_PIN);
// 5.5 控制中断开启与否
// gpio_intr_enable(FPM_INTR_PIN);
gpio_intr_disable(FPM_INTR_PIN);
// 5.6 芯片休眠 a. 低功耗 b. 在进入休眠模式后会将Touch引脚拉低(中断引脚拉低)
Inf_FPM383_Sleep();
}
/**
* @brief 中断回调函数
*
*/
static void Inf_FPM383_Intr_Handler(void *)
{
esp_rom_printf("123...\r\n");
isHasFinger = 1;
gpio_intr_disable(FPM_INTR_PIN);
}
/**
* @brief ESP32发送数据到FPM指纹模块
*
* @param data
* @param len
* @return Com_Status
*/
Com_Status Inf_FPM383_WriteCmd(uint8_t *data, uint8_t len)
{
int txBytes = uart_write_bytes(UART_NUM_1, data, len);
return txBytes == len ? Com_OK : Com_ERROR;
}
/**
* @brief 计算校验和,同时设置指令
*
* @param cmd
* @param len
*/
void Inf_FPM383_AddCheckSum(uint8_t *cmd, uint8_t len)
{
// 校验和是从包标识至校验和之间所有字节之和,包含包标识不包含校验和
uint16_t checkSum = 0;
for (uint8_t i = 6; i < len - 2; i++)
{
checkSum += cmd[i];
}
// 将计算完的校验和写入指令集
cmd[len - 2] = checkSum >> 8;
cmd[len - 1] = checkSum;
}
...
一站式注册指纹:
辅助说明:
ID 号:高字节在前,低字节在后。
参数:最低位为 bit0。
- bit0:采图背光灯控制位,0-LED 长亮,1-LED 获取图像成功后灭;— 没用
- bit1:采图预处理控制位,0-关闭预处理,1-打开预处理;
- bit2:注册过程中,是否要求模组在关键步骤,返回当前状态,0-要求返回,1-不
要求返回;- bit3:是否允许覆盖 ID 号,0-不允许,1-允许;
- bit4:允许指纹重复注册控制位,0-允许,1-不允许;
- bit5:注册时,多次指纹采集过程中,是否要求手指离开才能进入下一次指纹图
像采集, 0-要求离开;1-不要求离开;- bit6~bit15:预留。
/**
* @brief 一站式注册指纹
*
* @param id
* @return Com_Status
*/
Com_Status Inf_FPM383_AutoEnroll(uint16_t id)
{
// 1. 一站式注册指纹指令
uint8_t cmd[17] = {
0xEF, 0x01, // 包头
0xFF, 0xFF, 0xFF, 0xFF, // 设备地址
0x01, // 包标识
0x00, 0x08, // 包长度
0x31, // 指令码
'\0', '\0', // ID号
0x02, // 录入次数 2次
0x00, 0x3B, // 参数
'\0', '\0' // 校验和
};
// 2. 补充ID
cmd[10] = id >> 8;
cmd[11] = id;
// 3. 添加校验和
Inf_FPM383_AddCheckSum(cmd, 17);
// 4.bug,需要取消4次自动注册
Inf_FPM383_CancelAutoAction();
Inf_FPM383_CancelAutoAction();
Inf_FPM383_CancelAutoAction();
Inf_FPM383_CancelAutoAction();
// 5. 发送指令
Inf_FPM383_WriteCmd(cmd, 17);
while (1)
{
// 提取关键阶段的返回值结果
Inf_FPM383_ReadData(14, 2000);
// 只要中间关键阶段任何一次返回的不是00就直接退出
if (receData[9] != 0x00)
{
return Com_ERROR;
}
// 返回的确认码为00,同时返回的参数1的结果为0x06,说明注册成功
else if (receData[10] == 0x06)
{
return Com_OK;
}
}
return Com_TIMEOUT;
}
在删除指纹是需要索引id,但是官方提供的那个获取索引并不好用,所以这里我们可以使用验证时使用的获取id指令
/**
* @brief 搜索指定的指纹Id号
*
* @return uint16_t
*/
int16_t Inf_FPM383_SearchFingerPrint(void)
{
// 1. 验证指纹指令
uint8_t cmd[17] = {
0xEF, 0x01, // 包头
0xFF, 0xFF, 0xFF, 0xFF, // 设备地址
0x01, // 包标识
0x00, 0x08, // 包长度
0x32, // 指令码
0x03, // 分数等级
0xFF, 0xFF, // ID号,如果为FFFF,则表示与所有指纹进行对比,反之只与指定ID号指纹进行对比
0x00, 0x06, // 参数
'\0', '\0' // 校验和
};
// 2. 添加校验和
Inf_FPM383_AddCheckSum(cmd, 17);
// 3. 发送指令
Inf_FPM383_WriteCmd(cmd, 17);
// 4. 获取最后一次返回值结果
Inf_FPM383_ReadData(17, 3000);
if (receData[9] == 0x00)
{
// 获取存储在指纹库中的ID号
uint16_t id = (receData[11] << 8) | receData[12];
return id;
}
else
{
return -1;
}
}
App应用层
- App_IO.c
/**
* @brief 指纹扫描任务调用的函数
* 1.录入指纹(由按键任务通知)
* 2.删除指纹(由按键任务通知)
* 3.验证指纹
*/
void App_IO_FingerScan(void)
{
uint32_t action = 0;
xTaskNotifyWait(0xFFFFFFFF,0xFFFFFFFF,&action,0);
if (action != 0)
{
//关闭中断
gpio_intr_disable(FPM_INTR_PIN);
//注册指纹
if (action == '1')
{
sayAddUserFingerprint();
delay_ms(2000);
sayPlaceFinger();
delay_ms(2000);
//先获得最小的可用ID
uint16_t id = Inf_FPM383_GetMindId();
esp_rom_printf("ADD id = %d\r\n",id);
//一站式注册指纹
Com_Status comstatus = Inf_FPM383_AutoEnroll(id);
if (comstatus == Com_OK)
{
sayFingerprintAddSucc();
}
else
{
sayFingerprintAddFail();
}
//进入休眠
Inf_FPM383_Sleep();
//在注册以及删除指纹后芯片会出现问题,所以重启芯片
esp_restart();
}
else if (action == '2')
{
//删除指纹
sayDelUserFingerprint();
delay_ms(2000);
sayPlaceFinger();
delay_ms(4000);
//获取按下手指,存在指纹库中的id
int16_t id = Inf_FPM383_SearchFingerPrint();
esp_rom_printf("DEL id = %d\r\n",id);
if (id == -1)
{
sayDelFail();
}
else
{
//执行删除指纹命令
Com_Status comstatus = Inf_FPM383_DeleteFingerPrint(id);
if (comstatus == Com_OK)
{
sayDelSucc();
}
else
{
sayDelFail();
}
}
//进入休眠
Inf_FPM383_Sleep();
esp_restart();
}
else if (action == '3')
{
//删除指纹
Inf_FPM383_DeleteAllFingerPrint();
sayDelUserFingerprint();
delay_ms(4000);
//进入休眠
Inf_FPM383_Sleep();
esp_restart();
}
}
else
{
// 验证指纹
if (isHasFinger)
{
//清除标志位
isHasFinger = 0;
//开始验证
Com_Status comstatus = Inf_FPM383_CheckFingerPrint();
if (comstatus == Com_OK)
{
//验证成功
sayFingerprintVerifySucc();
delay_ms(2000);
Inf_BDR6120_OpenLock();
sayDoorOpen();
}
else
{
//验证失败
sayFingerprintVerifyFail();
}
//进入休眠
Inf_FPM383_Sleep();
}
}
}
蓝牙模块
Espressif\frameworks\esp-idf-v5.3.1\examples\bluetooth\bluedroid\ble\gatt_security_server
打开蓝牙:
打开4.2,关闭5.0
移植的时候修改下引用的头文件 蓝牙名称 以及主函数名
然后拿到数据后到App层里再做处理,此处定义弱实现函数,:
- Dri_BT.c
/* 定义esp32收到手机数据时的回调弱函数函数 */
void __attribute__((weak)) App_Communication_RecvDataCb(uint8_t *data, uint16_t dataLen)
{
}
...
case ESP_GATTS_WRITE_EVT:
ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_WRITE_EVT, write value:");
esp_log_buffer_hex(GATTS_TABLE_TAG, param->write.value, param->write.len);
// printf("接收到手机发过来的消息:%s\r\n",param->write.value);
App_Communication_RecvDataCb(param->write.value,param->write.len);
OTA模块
- 简介:
OTA 升级机制可以让设备在固件正常运行时根据接收数据(如通过 Wi-Fi、蓝牙或以太网)进行自我更新。
要运行 OTA 机制,需配置设备的分区表,该分区表至少包括两个OTA 应用程序分区(即 ota_0 和 ota_1)和一个 OTA 数据分区。
OTA 功能启动后,向当前未用于启动的 OTA 应用分区写入新的应用固件镜像。镜像验证后,OTA 数据分区更新,指定在下一次启动时使用该镜像。
- 创建分区表
- 修改配置
- partitions.csv
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, , 0x4000,
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
# Original App Section
ota_0, app, ota_0, , 1800K,
# New App Section
ota_1, app, ota_1, , 1800K,
-
WiFi移植
我们是通过wifi进行ota升级, 移植官方示例: examples\wifi\getting_started\station
先创建一个配置文件Kconfig.projbuild
方便修改wifi账户和密码.
然后移植官方驱动,修改初始化.
- 启动Http服务
这里本机模拟服务端,在一个空文件夹启动,使用PowerShell在这里打开:
python.exe -m http.server 8080
然后把.bin文件放入其中即可联网访问下载,也就可以使用OTA在线升级
- OTA移植
移植官方案例: examples\system\ota\simple_ota_example
- App_Communication.c
static void get_sha256_of_partitions(void)
{
uint8_t sha_256[HASH_LEN] = {0};
esp_partition_t partition;
// get sha256 digest for bootloader
partition.address = ESP_BOOTLOADER_OFFSET;
partition.size = ESP_PARTITION_TABLE_OFFSET;
partition.type = ESP_PARTITION_TYPE_APP;
esp_partition_get_sha256(&partition, sha_256);
// get sha256 digest for running partition
esp_partition_get_sha256(esp_ota_get_running_partition(), sha_256);
}
#define TAG "ota"
/// 处理一系列的HTTP事件
esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
switch(evt->event_id)
{
case HTTP_EVENT_ERROR:
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
break;
case HTTP_EVENT_HEADER_SENT:
ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
break;
case HTTP_EVENT_ON_HEADER:
ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
break;
case HTTP_EVENT_REDIRECT:
ESP_LOGD(TAG, "HTTP_EVENT_REDIRECT");
break;
}
return ESP_OK;
}
/**
* @description: 下载ota用的二进制文件
* @return {*}
*/
static void App_Communication_OTADownloadBin(void)
{
// esp_err_t err = nvs_flash_init();
// if(err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND)
// {
// nvs_flash_erase();
// err = nvs_flash_init();
// }
/* 1. 获取分区信息 */
get_sha256_of_partitions();
/* 2. 初始化网络 */
esp_netif_init();
/* 3. 创建和初始化默认事件循环 */
esp_event_loop_create_default();
esp_http_client_config_t config = {
.url = "http://172.20.10.4:8080/esp-hello-world.bin",
.crt_bundle_attach = esp_crt_bundle_attach,
.event_handler = NULL,
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
esp_https_ota(&ota_config);
}
void App_Communication_OTA(void)
{
/* 1. 连接wifi */
Dri_Wifi_Init();
/* 2. ota升级 使用python启动个本地http-server 命令
C:\esp\tools\idf-python\3.11.2\python -m http.server 8080
*/
printf("ota开始升级\r\n");
App_Communication_OTADownloadBin();
printf("ota完成升级\r\n");
/* 3. 关闭wifi */
esp_wifi_stop();
/* 4. 重启esp32 */
esp_restart();
}
/**
* @description: 蓝牙模块初始化
* @return {*}
*/
void App_Communication_Init(void)
{
Dri_BT_Init();
}
/**
* @description: 蓝牙模块中弱函数的回调实现
* @param {uint8_t} *data
* @param {uint16_t} dataLen
* @return {*}
*/
void App_Communication_RecvDataCb(uint8_t *data, uint16_t dataLen)
{
printf("接收到手机传输过来的数据:%s\r\n", data);
/*
蓝牙发送数据格式: 功能
1: 开锁
2:密码 设置密码
3:密码 删除密码
*/
/* 1. 数据长度 < 2, 直接返回, 没有任何操作 */
if (dataLen < 2)
{
sayIllegalOperation();
return;
}
/*
客户端连接上蓝牙之后, 会发送锁的 序列号 +open 来开锁
锁的序列号一般在锁出厂的时候就已经固定了,而且是唯一的
我们可以使用 esp32的mac地址作为序列号
*/
uint8_t pwd[100] = {0};
switch (data[0])
{
case '1': // 1+666666
memcpy(pwd, &data[2], dataLen - 2);
printf("密码为:%s\r\n", pwd);
App_IO_CheckPwd(pwd);
break;
case '2': // 2+55555
memcpy(pwd, &data[2], dataLen - 2);
printf("密码为:%s\r\n", pwd);
App_IO_AddPwd(pwd);
break;
case '3': // 3+55555
memcpy(pwd, &data[2], dataLen - 2);
printf("密码为:%s\r\n", pwd);
App_IO_DelPwd(pwd);
break;
default:
break;
}
}
main函数
- main.c
...
int app_main(void)
{
// 1. 初始化
App_IO_Init();
App_Communication_Init();
Inf_FPM383_ReadId();
Inf_FPM383_Sleep();
xTaskCreate(Key_Scan_Task, "Key_Scan", 2048, NULL, 5, &KeyScanHandler);
xTaskCreate(Finger_Scan_Task, "Finger_Scan", 2048, NULL, 5, &FingerScanHandler);
xTaskCreate(OTA_Task, "Ota_Scan", 8192, NULL, 5, &otaHandler);
return 0;
}
/**
* @brief 按键扫描任务
*
*/
void Key_Scan_Task(void *)
{
TickType_t tickType = xTaskGetTickCount();
while (1)
{
App_IO_KeyScan();
vTaskDelayUntil(&tickType, 50);
}
}
/**
* @brief 手指检测任务
*
*/
void Finger_Scan_Task(void *)
{
delay_ms(500);
TickType_t tickType = xTaskGetTickCount();
while (1)
{
App_IO_FingerScan();
vTaskDelayUntil(&tickType, 50);
}
}
void OTA_Task(void *)
{
uint32_t action = 0;
while (1)
{
xTaskNotifyWait(0xFFFFFFFF,0xFFFFFFFF,&action,portMAX_DELAY);
if (action == '4')
{
//执行OTA固件升级
App_Communication_OTA();
}
}
}