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_Flashesp_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. 拉低并延时10msWTN6170_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 bytesize_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 LEDsrmt_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[])
{// 限定密码存储上限为100uint8_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 并将密码存储进FlashpwdCount++;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();// 将密码个数-1uint8_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. 补充IDcmd[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);//先获得最小的可用IDuint16_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);//获取按下手指,存在指纹库中的idint16_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 overlapnvs, 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 bootloaderpartition.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 partitionesp_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+666666memcpy(pwd, &data[2], dataLen - 2);printf("密码为:%s\r\n", pwd);App_IO_CheckPwd(pwd);break;case '2': // 2+55555memcpy(pwd, &data[2], dataLen - 2);printf("密码为:%s\r\n", pwd);App_IO_AddPwd(pwd);break;case '3': // 3+55555memcpy(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();}}
}