# 007ePaperPhotoFrame **Repository Path**: dwl301/007e-paper-photo-frame ## Basic Information - **Project Name**: 007ePaperPhotoFrame - **Description**: 基于ESP32C3的墨水屏显示项目 - **Primary Language**: Unknown - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2024-04-09 - **Last Updated**: 2024-04-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ePaperPhotoFrame * 实现了图片接收,并显示在EPD上,也可缓存多个Photo到SD Card中,并可选择显示,或删除已有缓存 * 使用esp-idf 5.0.1以上版本 ## 1. 硬件设计 * 使用ESP32C3模块+SD Card模块+自建EPD驱动+充放电模块+拨动开关 ### 1.1 ESP32C3核心板 > [ESP32C3-CORE开发板 - LuatOS 文档](https://wiki.luatos.com/chips/esp32c3/board.html) ![ESP32C3-CORE核心板](./doc/pic/ESP32C3-CORE.png) #### 1.2 引脚分配 | ESP32 | Func | TypeC | Func | ESP32 | | :--- | :---- | ---: | ---: | ---: | | GND | | | | VBUS | | VBUS | | | | PWB | | GPIO9 | SD_CS | | | GND | | GPIO8 | SPI_MOSI | | | 3V3 | | GPIO4 | SPI_CLK | | | CHIP_EN | | GPIO5 | SPI_MISO | | | NC | | 3V3 | | | | GPIO13 | | GND | | | | GPIO21 | | GPIO11| | | | GPIO20 | | GPIO7 | EPD_CS | | | GND | | GPIO6 | EPD_D/C | | | GPIO19 | | GPIO10| EPD_RES | | | GPIO18 | | GPIO3 | EPD_BUSY | | BTN_UP | GPIO12 | | GPIO2 | | | BTN_M | GPIO1 | | 3V3 | | | BTN_DWN | GPIO0 | | GND | | | | GND | ### 1.3 SD Card模块 * 淘宝购买模块 * 使用引脚:3V3/GND/MISO/MOSI/SCLK/CS ### 1.4 EPD屏幕 - GDEQ0426T82 [4.26寸 快刷1秒纸墨水屏 分辨率800x480 支持局刷 GDEQ0426T82-大连佳显电子有限公司](https://www.good-display.cn/product/452.html) ![GDEQ0426T82](./doc/pic/GDEQ0426T82.png) * 对应驱动板: DESPI-C02 * [24Pin串口电子墨水屏专用转接板 DESPI-C02](https://www.good-display.cn/product/505.html) * 使用引脚:BUSY/RES/D\_C/CS/SCK/SDI/GND/3V3 * 驱动型号:SSD1677 **(具体寄存器配置可以查它的手测)** ![DESPI-C02](./doc/pic/DESPI-C02.jpg) ### 1.5充放电模块 * 特性 * 5V输入 * 接3.7V锂电池 * 自升压到5V输出 #### 1.5.1 自建板子(未成功) * 基于IP5306 ### 1.6 拨轮开关 * 特性 * 具有上/中/下,三个独立开关 ## 2 功能模块 ### 2.1 主函数 #### 2.1.1 多SPI设备 * ESP32C3有三个SPI控制器,SPI0/1部分资源共用,SPI2基本独立 * SPI0/1可配存储器模式(SPI Memory) * SPI2可配通用模式(GP-SPI) ![ESP32 SPI架构](./doc/pic/ESP32C3_SPI.jpg) * 程序中,在app_main内首先初始化SPI2_HOST总线,并指定所使用的**SPI控制器**和**部分pin(MOSI/MISO/CLK)** ``` C #define SPI_HOST_USING SPI2_HOST //HSPI_HOST //初始化SPI总线 void __ESP32_SPI_Bus_Init(spi_host_device_t host_id) { esp_err_t ret; spi_bus_config_t spiBusCfg = { .mosi_io_num = SPI_PIN_MOSI, .miso_io_num = SPI_PIN_MISO, .sclk_io_num = SPI_PIN_SCLK, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = 160*800*2+8 //TODO: 这里太大了??? }; ret = spi_bus_initialize(host_id, &spiBusCfg, SPI_DMA_CH_AUTO); ESP_ERROR_CHECK(ret); } ``` * 再在对应模块内,为已初始化的SPI总线添加设备,此时要指定该设备使用的CS * 添加完一个设备后,可以再添加其他设备,app_main中为SPI2_HOST挂载了EPD和SDCard两个设备 ``` C void __ESP32_GPIO_SPI_Init(spi_host_device_t host_id) { esp_err_t ret; //------------ GPIO init ------------ ...... //------------ SPI device init ------------ spi_device_interface_config_t spiDevInterCfg = { .clock_speed_hz = 10*1000*1000, .mode= 0, //TODO: 也许是其他模式 .spics_io_num = PIN_CS, //指定该设备使用的CS .queue_size = 7, .pre_cb = __SPI_Each_Trans_Callback, }; //attach device to spi bus ret = spi_bus_add_device(host_id, &spiDevInterCfg, &SPI_Device_Handle); //将返回的句柄保存到全局变量SPI_Device_Handle中 ESP_ERROR_CHECK(ret); //------------ EPD init ------------ } ``` ### 2.2 WiFi模块 - WebServer > [Waveshare提供的WiFi例程](https://www.waveshare.net/wiki/E-Paper_ESP32_Driver_Board) * 作为AccessPoint,可提供图片传输接口 #### 2.2.1 作为WebServer * Server需要处理Get请求,也就是浏览器向Server请求数据 * 从req->uri拿到请求的文件名,返回对应文件 ``` C //绑定get处理 httpd_uri_t httpd_uri_get = { .uri = "/*", .method = HTTP_GET, //要被处理的请求类型为get .handler = http_uri_get_handler, //自定义的处理函数 // .user_ctx = , //传递数据 }; httpd_register_uri_handler(httpd_hdl, &httpd_uri_get); ``` ``` C static esp_err_t http_uri_get_handler(httpd_req_t *req) { uint8_t file_type; //判断req->uri内容 if(strcmp(req->uri, "/index.html") == 0) file_type = 0; else if(strcmp(req->uri, "/styles.css") == 0) file_type = 1; else if(strcmp(req->uri, "/scriptA.js") == 0) file_type = 2; else if(strcmp(req->uri, "/scriptB.js") == 0) file_type = 3; else if(strcmp(req->uri, "/scriptC.js") == 0) file_type = 4; else if(strcmp(req->uri, "/scriptD.js") == 0) file_type = 5; else { ESP_LOGI(TAG_WEB, "!!! err, request a unknow file"); return ESP_FAIL; } //返回所需文件 //用于EMBED_FILES, 先以SDCard+FS替代 __sendWebFile(req, file_type); //__sendWebFileFromFS(req, file_type); return ESP_OK; } ``` * Server还需要处理Post请求,也就是浏览器向Server发送数据 * 这里规定了POST的URI中携带了要传输的数据,因此要对req->uri进行解析 * 注意,处理完后要对Server要回复"200 OK" ``` C //绑定post处理 httpd_uri_t httpd_uri_post = { .uri = "/*", .method = HTTP_POST, //要被处理的请求类型为post .handler = http_uri_post_handler, //自定义的处理函数 .user_ctx = NULL }; httpd_register_uri_handler(httpd_hdl, &httpd_uri_post); ``` ``` C static esp_err_t http_uri_post_handler(httpd_req_t *req) { //------------ step1 receive all data char *buf = req->uri; int buf_len = strlen(buf); char data_cmd[5]; //--------- 从buf中获取cmd __getSomeChar(data_cmd, 5, buf, buf_len, buf_len-5, 5); //--------- 判断&执行cmd //------ data_cmd='EPDx_' if(strstr(data_cmd, "EPD")) { //...... } //------ data_cmd='LOAD_' else if(strstr(data_cmd, "LOAD")) { //...... } //------ data_cmd='NEXT_' else if(strstr(data_cmd, "NEXT")) { //...... } //------ data_cmd='SHOW_' else if(strstr(data_cmd, "SHOW")) { //...... } //------ 其他情况不处理 else { //...... } //------------ step2 response Client const char resp[] = "POST Response!"; httpd_resp_set_status(req, "200 OK"); httpd_resp_sendstr(req, resp); return ESP_OK; } ``` #### 2.2.2 如何将.html/.css/.js前端文件放入工程 * 参考[ESP32作为WebServer](https://blog.csdn.net/qq_27114397/article/details/89643232): * 方法一,MTML转为.h,编译在工程内 * 方法二(前期采用的方法),HTML嵌入FLASH中 ##### HTML嵌入FLASH * 该方式将文件写入到了.rodata中 * 实现步骤: 1. 将前端页面文件单独保存,例如,保存到./main/web_files文件中 2. 在./main/CMakeList.txt中罗列要嵌入的文件 ``` shell ### ./main/CMakeList.txt idf_component_register(SRCS "main.c" INCLUDE_DIRS "." # 用于EMBED_FILES, 先以SDCard+FS替代 EMBED_FILES "./HTTPServer/web_files/styles.css" "./HTTPServer/web_files/index.html" "./HTTPServer/web_files/scriptA.js" "./HTTPServer/web_files/scriptB.js" "./HTTPServer/web_files/scriptC.js" "./HTTPServer/web_files/scriptD.js" ) ``` 3. 在程序中申明这些EMBED_FILES,这里的\_binary_index_html_start,绿色部分是固定的,红色部分来自文件名 ``` C //indext.html extern const unsigned char html_index_start[] asm("_binary_index_html_start"); extern const unsigned char html_index_end[] asm("_binary_index_html_end"); //styles.css extern const unsigned char html_styles_start[] asm("_binary_styles_css_start"); extern const unsigned char html_styles_end[] asm("_binary_styles_css_end"); //scriptA.js extern const unsigned char html_scriptA_start[] asm("_binary_scriptA_js_start"); extern const unsigned char html_scriptA_end[] asm("_binary_scriptA_js_end"); //每个文件都是start和end标记, 并可计算size const size_t html_tmp_size = (html_index_end - html_index_start); ``` 4. 向浏览器返回前端文件,发送时要指定文件类型,
.html对应text/html,
.css对应text/css
.js对应applicaiont/javascript ``` C //向浏览器响应页面, 发送Web页面数据 if(strcmp(req->uri, "/index.html") == 0) { //这些EMBED_FILE已集中声明过 // extern const unsigned char html_tmp_start[] asm("_binary_index_html_start"); // extern const unsigned char html_tmp_end[] asm("_binary_index_html_end"); // const size_t html_tmp_size = (html_index_end - html_index_start); httpd_resp_set_status(req, "200 OK"); httpd_resp_set_type(req, "text/html"); //返回文件类型 httpd_resp_send_chunk(req, (const char *)html_index_start, html_tmp_size); //文件开始位置 & 文件长度 httpd_resp_sendstr_chunk(req, NULL); //表示返回结束 ESP_LOGI(TAG_MAIN, ">>> has sent index page"); } ``` ### 2.3 EPD模块 - 驱动EPD显示 > [GoodDispaly提供的驱动例程](https://www.good-display.cn/companyfile/894.html) * 具有全局刷新/局部刷新功能 * 提供一些GUI绘制(点/线/圆/字符) #### 2.3.1 基础功能 ##### 存储图像数据 * 使用一维数组缓存像素信息 * 将8bit合成1char,将char存入数组中,因此数组长度=480*800/8(即在X方向按8bit压缩) * b0000_0000到b1111_1111对应字符'a'到'p' ``` C #define IMG_INDEX_MAX EPD_ARRAY //48000=480*800/8 // uint8_t PhotoImg[IMG_INDEX_MAX]; //缓存照片 uint8_t GUIImg[IMG_INDEX_MAX]; //缓存界面 ``` ![像素对应位置](./doc/pic/ImgBuf.png) ##### EPD配置流程 ###### 全局刷新 * 初始化 * 写入PhotoImg缓存 * 进入休眠 ``` C void ePaper_FullDispaly() { //PhotoImg中已有图像数据, DispImg指向PhotoImg DispImg = PhotoImg; EPD_W21_Init_Modes(Init_Mode_X_3); //Full screen refresh initialization. EPD_WhiteScreen_ALL(DispImg); //To Display one image using full screen refresh. EPD_DeepSleep(); //Enter the sleep mode and please do not delete it, otherwise it will reduce the lifespan of the screen. } ``` ###### 局部刷新 * 初始化,设置背景图像(ePaper_GuiImg_Init) * 刷新GUIImg缓存 * GUIImg缓存写入EPD(ePaper_GuiImg_Display) ``` C void ePaper_GuiImg_Init(void) { //------------ 使用GUI, 并支持局部刷新 GUI_Init_Img(GUIImg, EPD_X_LEN, EPD_Y_LEN, Rotate0, ColorWhite); //需要交换W和H GUI_Clear_All(ColorWhite); // EPD_W21_Init_GUI(); EPD_SetRAMValue_BaseMap(GUIImg); //设置背景 } void ePaper_GuiImg_Display() { DispImg = GUIImg; EPD_Dis_PartAll(DispImg); EPD_DeepSleep(); } ``` ##### SSD1677中的关键寄存器 * addr=0x11 - Data Entry mode setting(EPD_W21_Init_Modes中使用)
[2:0] = [A2:A1:A0] = [AM:ID1:ID0] ![addr=0x11](./doc/pic/addr=0x11.png) * addr=0x44/0x45 - Set RAM X/Y(EPD_W21_Init_Modes中使用)
需要4组数据,每组10bit组成
X方向最大=0x3BF=959 -> 960
Y方向最大=0x2A7=679 -> 680 ![addr=0x44_0x45](./doc/pic/addr=0x44_0x45.png) * addr=0x24 - Write RAM(Black White)(EPD_WhiteScreen_ALL中子函数使用)
绘制像素
![addr=0x24](./doc/pic/addr=0x24.png) * addr=0x22 - Display Update Control2(EPD_WhiteScreen_ALL中子函数使用)
未找到详细解释,按照例程用法有以下设置,
0x91 -> DISPLAY Mode1, fast refresh init
0xC7 -> DISPLAY Mode1, fast refresh update
0xF7 -> DISPLAY Mode1, full screen refresh update
0xFF -> DISPLAY Mode2, partial refersh update ###### 可能的问题 * 等待过长,触发WDT,
例如:等待BUSY相应时,如果时间过长会触发ESP32的WDT,因此要加入vTaskDelay,让系统响应其他任务 ``` C void __Epaper_ReadBusy(void) { while(1) { //=1 BUSY if(EPD_W21_Get_BUSY == 0) break; else vTaskDelay(3); //防止一直等待触发WDT } } ``` #### 2.3.2 GUI支持 ##### 绘制线 - 基于Bresenham直线算法(我看不懂,但是十分震撼) [A Rasterizing Algorithm for Drawing Curves](./doc/Bresenham.pdf) ![栅格化直线绘制](./doc/pic/Straight%20lines.png) ##### 绘制圆 - 四分圆算法 * 设置圆心、半径 ``` C int8_t GUI_Paint_DrawCircle(uint16_t x_center, uint16_t y_center, uint16_t radius, CircleStyle_t circle_style, Color_t color, DotPixel_t dot_pixel) { int16_t r = radius; int16_t x = -r, y = 0, err = 2 - 2 * r; //bottom left to top right int16_t i; //空心圆 do{ //绘制四分点 __Paint_SetPixel(x_center - x, y_center + y, color); // I. Quadrant +x +y __Paint_SetPixel(x_center - y, y_center - x, color); // II. Quadrant -x +y __Paint_SetPixel(x_center + x, y_center - y, color); // III. Quadrant -x -y __Paint_SetPixel(x_center + y, y_center + x, color); // IV. Quadrant +x -y //迭代 r = err; if(r <= y) //e_xy + e_y < 0 err += ++y*2 + 1; if(r > x || err >y) //e_xy + e_x > 0 OR no 2nd y-step err += ++x*2 + 1; //-> x-step now }while(x < 0); return 0; } ``` ##### 绘制字符 * 预先规定好字符的Width、Height,并准备对应图像的一维数组 ![绘制字符](./doc/pic/GUI_char.png) ###### 字符方向问题 * 分别沿着X和Y方向,各自存在4个不同的朝向 ![字符方向](./doc/pic/GUI_char%20dirction.jpg) * 如下,将从x_axis开始,沿着x方向移动(x_axis+x_add),font到一行处理完后,y方向再移动一(y_axis+y_add) ``` C //举例: 沿EPD的X方向,该方向在循环中先变化 x_add = &x_font; //EPD的x方向将被施加Width y_add = &y_font; //EPD的y方向将被施加Height x_dir = -1; //从左到右, 因此x逐渐减小, 用减法 y_dir = 1; //从上到下, 因此y逐渐增大, 用加法 // //沿着EPD的Y方向,该方向在循环中先变化 // x_add = &y_font; //EPD的x方向将被施加Height // y_add = &x_font; //EPD的x方向将被施加Width // x_dir = 1; y_dir = 1; //绘制char // 起始点是(x_axis, y_axis) // 字符的长、高为font->Width、font->Height if(color_background == FONT_BACKGROUND_DEFAULT) //背景色为 白 { for(y_font = 0; y_font < font->Height; y_font++) //绘制height行 { for(x_font = 0; x_font < font->Width; x_font++) //绘制一行中的width列, 一行中有n个8bits { if(*char_data & (0x80 >> (x_font % 8))) //展开成8bit取其中1bit, 设置对应颜色 { __Paint_SetPixel(x_axis + (*x_add) * x_dir, y_axis + (*y_add) * y_dir, color_frontground); //(x_axis, y_axis)是起始点 } // if(x_font % 8 == 7) //处理完8bits后, 移动到下一个table数据 char_data++; } //绘制完一行 if(font->Width % 8 != 0) //如果一行数据量font->Width不够n个char, 就跳过当前table数据 char_data++; } } ``` ### 2.4 SDCard模块 * 实现向SDCard进行读/写/遍历操作 #### 2.4.1 SDCard初始化 * 使用esp-idf提供的便捷函数 esp_vfs_fat_sdspi_mount ``` C #define MOUNT_POINT "/sdcard" const char MountPoint[] = MOUNT_POINT; static sdmmc_card_t *SDCard; //a SD Card, it keeps SD/MMC card information //需要配置三组信息, 分别是 挂载配置/SD控制器配置/SD设备配置 ret = esp_vfs_fat_sdspi_mount(MountPoint, //挂载点 &sd_host, //SD host(控制器) &sd_dev_cfg, //SD device(设备) &sd_mount_cfg, //挂载配置 &SDCard //返回SDCard对象 ); sdmmc_card_print_info(stdout, SDCard); //输出SDCard信息 ``` #### 2.4.2 打开文件 * 打开文件是读/写的前提 * SDCard.c中维护了一个全局变量**File**,打开文件时对其进行设置,操作完关闭时重新赋NULL ``` C static FILE *File = NULL; //打开的文件描述符 ```
* SDCard_File_Open,获取文件信息 & 打开文件 * 入参 * file_path,传入要操作的文件路径,会自动在前拼接上MountPoint * mode,传入操作文件的模式,支持FILE_MODE_READ/FILE_MODE_WRITE/FILE_MODE_READ_WRITE * 出参 * ESP_OK=0,打开文件成功 * ESP_FAIL=-1,打开文件失败 ``` C esp_err_t SDCard_File_Open(char *file_path, const char *mode) // { struct stat sta; char full_file_path[50]; //限制完整文件路径要小于50byte //拼接文件路径 sprintf(full_file_path, MOUNT_POINT"%s", file_path); // ESP_LOGI(TAG_SD, "mode=%s, full_path=%s", mode, full_file_path); //模式判断 - 读, 打开文件前需要判断文件是否存在 if((strstr(mode, FILE_MODE_READ) != NULL) || (strstr(mode, FILE_MODE_READ_WRITE) != NULL)) //比较字符串 { if((stat(full_file_path, &sta) != 0)) // stat()!=0 表示没有这个文件 { ESP_LOGI(TAG_SD, "FileOpen - !!! read mode AND no file"); return ESP_FAIL; } } //模式判断 - 写, 可以直接创建文件 else if(strstr(mode, FILE_MODE_WRITE) != NULL) ESP_LOGI(TAG_SD, "FileOpen - write mode"); //模式判断 - 其他 else { ESP_LOGI(TAG_SD, "FileOpen - !!! other mode"); return ESP_FAIL; } //打开文件并判断是否成功 File = fopen(full_file_path, mode); // File是.c全局变量 if(File == NULL) { ESP_LOGI(TAG_SD, "FileOpen - open file failed"); return ESP_FAIL; } else { ESP_LOGI(TAG_SD, "FileOpen - open file ok"); return ESP_OK; } } ``` #### 2.4.3 文件操作 ##### 读取文件 * 实现SDCard读取后,WebServer部分的EMBED_FILE最改用SDCard进行存储
* 文件读取用**fgets**实现,每次返回一行数据到buf中,外部调用通过返回值判断是否可以继续读取 ``` C esp_err_t SDCard_File_Read(char *buf, uint16_t size) { if(feof(File) == 0) //遇到结束返回非0; 未遇到结束返回0 { // memset(file_buf, NULL, sizeof(file_buf)); fgets(buf, sizeof(buf), File); //可以直接用sizeof而不使用size return ESP_OK; } return ESP_FAIL; } ``` ##### 写入img * 文件写入用**fprintf**实现,将准备好的字符串数据写入文件中 ``` C #define FILE_WRITE_BLOCK_SIZE 1024 //写文件允许操作的块长度 esp_err_t SDCard_File_Write_Img(char *imgVal, uint16_t len) { if(File == NULL) //检查是否已打开文件File { return ESP_FAIL; } char tmp[FILE_WRITE_BLOCK_SIZE]; //len最大应该在1000 tmp[len] = '\0'; //设置结束符 memcpy(tmp, imgVal, len); //将img数据组织成字符串 fprintf(File, "%s\n", tmp); //按十六进制写入 return ESP_OK; } ``` ##### 复制img * 复制img文件时,使用**fgets**从源文件读取一行,再用**fputs**写入目标文件中,直到触发eof ``` C int8_t SDCard_File_Copy_Img() { FILE *dest_file; char full_file_path[FILE_PATH_LEN_MAX]; uint32_t random = esp_random(); char *buffer; uint16_t cnt; //设置被复制文件的路径, 文件名随机生成 sprintf(full_file_path, "%s%s%5ld.txt", MOUNT_POINT, DEFAULT_IMG_DIR, random % 0x0000FFFF); //创建被复制的文件 dest_file = fopen(full_file_path, FILE_MODE_WRITE); if(dest_file == NULL) //检查文件创建是否成功 { ESP_LOGI(TAG_SD, "SDCard_File_Copy_Img: open file failed"); return -1; } else { //设置缓存 buffer = (char *)malloc(sizeof(char) * FILE_WRITE_BLOCK_SIZE); cnt = 0; while(fgets(buffer, FILE_WRITE_BLOCK_SIZE, File)) { fputs(buffer, dest_file); cnt++; } free(buffer); //释放缓存 fclose(dest_file); ESP_LOGI(TAG_SD, "SDCard_File_Copy_Img: %d lines copied", cnt); return 0; } } ``` ##### 删除文件 * 删除文件使用**remove**,只需要指定文件路径 ``` C int8_t SDCard_File_Delete_Img() { struct stat sta; char file_name[FILE_PATH_LEN_MAX]; //获取文件名 SDCard_File_GetOnePath(file_name); ESP_LOGI(TAG_SD, "try to delete %s", file_name); //判断文件存在 if(stat(file_name, &sta) == 0) { if(remove(file_name) == 0) //删除Photo { return 0; } else { ESP_LOGI(TAG_SD, "delete Photo failed.."); return -1; } } else { ESP_LOGI(TAG_SD, "file not exist"); return -1; } } ``` ##### 遍历操作 * 遍历文件时,先使用**opendir**打开路径,再使用**readdir**逐一读取文件,最后使用**closedir**关闭路径 ``` C int8_t SDCard_File_ListAllNames(char *dir) { char full_dir_path[50]; //拼接路径 sprintf(full_dir_path, MOUNT_POINT"%s", dir); ESP_LOGI(TAG_SD, "list dir: %s", dir); //打开目录 DIR *root_dir = opendir(full_dir_path); if(root_dir == NULL) return -1; //遍历目录内文件 DirFileNum = 0; memset(DirFileNames, '\0', sizeof(DirFileNames)); while(1) { struct dirent* de = readdir(root_dir); //用于遍历 if(!de) break; //保存文件名, 文件数量计数+1 sprintf(DirFileNames[DirFileNum++], "%s", de->d_name); if(DirFileNum >= FILE_NUM_MAX) break; } //关闭目录 closedir(root_dir); //...... return 0; } ``` ### 2.5 BTN模块 * 响应拨轮开关操作,执行对应绑定的函数 * 使用esp-iot-component
* 工程中添加button组件,在CMake步骤中会自动下载 ``` shell $ idf.py add-dependency "espressif/button^3.1.3" ```
* 支持的按键事件 - Single Click/Double Click/Long Press ![](./doc/pic/iot_button.png) * 初始化iot_button ``` C typedef enum { BTN_IDX_UP = 0, BTN_IDX_M, BTN_IDX_DWN, BTN_IDX_ALL //按键数量 }Btn_Idx_t; //自定义类型 static button_handle_t g_btns[BTN_IDX_ALL] = {0}; //其中每个元素对应一个btn void Btn_init() { uint8_t i; //每个btn的公共设置 button_config_t cfg = { .type = BUTTON_TYPE_GPIO, //btn类型, 这里是GPIO .long_press_time = CONFIG_BUTTON_LONG_PRESS_TIME_MS, //CONFIG_BUTTON_LONG_PRESS_TIME_MS=1500 .short_press_time = CONFIG_BUTTON_SHORT_PRESS_TIME_MS, //CONFIG_BUTTON_SHORT_PRESS_TIME_MS=180 .gpio_button_config = { // .gpio_num = BTN_UP, .active_level = 1, }, }; //设置&创建每个gpio按键 for(i = 0; i < BTN_IDX_ALL; i++) { cfg.gpio_button_config.gpio_num = BtnInfo[i].gpioNum; g_btns[BtnInfo[i].btnIdx] = iot_button_create(&cfg); //使用cfg创建btn } //注册按键回调 // 单击事件 iot_button_register_cb(g_btns[BTN_IDX_UP], BUTTON_SINGLE_CLICK, __btnCb_PressDown, NULL); iot_button_register_cb(g_btns[BTN_IDX_M], BUTTON_SINGLE_CLICK, __btnCb_PressDown, NULL); iot_button_register_cb(g_btns[BTN_IDX_DWN], BUTTON_SINGLE_CLICK, __btnCb_PressDown, NULL); // 长按事件 iot_button_register_cb(g_btns[BTN_IDX_M], BUTTON_LONG_PRESS_START, __btnCb_LongPressStart, NULL); // 双击事件 iot_button_register_cb(g_btns[BTN_IDX_M], BUTTON_DOUBLE_CLICK, __btnCb_DblPressDown, NULL); } ``` * btn处理函数 ``` C //获取当前btn的序号 static int __getBtnIndex(button_handle_t btn) { //通过遍历, 判断是哪个btn for (size_t i = 0; i < BTN_IDX_ALL; i++) { if (btn == g_btns[i]) { return i; //返回该btn对应的序号 } } return -1; } //单击事件的处理函数 static void __btnCb_PressDown(void *arg, void *data) { TEST_ASSERT_EQUAL_HEX(BUTTON_PRESS_DOWN, iot_button_get_event(arg)); //获取btn序号 Btn_Idx_t btn_idx = __getBtnIndex((button_handle_t)arg); if(MenuState == MENU_EMPTY) //空闲态 { } else if(MenuState == MENU_SELECT) //选择态 { //根据当前触发的btn执行所需的操作 switch (btn_idx) { case BTN_IDX_UP: ESP_LOGI(TAG_BTN, "BTN_IDX_UP"); if(Update_GuiImg_to_Select(-1) == 1) Update_GuiImg_from_List(); break; case BTN_IDX_DWN: ESP_LOGI(TAG_BTN, "BTN_IDX_DWN"); if(Update_GuiImg_to_Select(1) == 1) Update_GuiImg_from_List(); break; default: break; } } else return; } ``` ## 3 项目细节 ### 3.1 工程配置 #### Flash Size * ESP32C3-Core板载了4MB SPI flash,需要对应修改 ![flash size](./doc/pic/config_flash%20size.png) #### stack大小 * 程序触发了stack overflow ![stack overflow error](./doc/pic/config_stack_error.jpg) * 调整main stack的大小 - **最终Main task stack size设置为5632** ![stack overflow](./doc/pic/config_stack.jpg) #### WebServer的URI * 增加Max HTTP URI Length,以便用POST方式回传img数据 ![max uri length](./doc/pic/config_max%20uri.png) #### file name长度 * 调整Max long filename length,程序中实际允许长度为50 ![](./doc/pic/config_filename.png) ## TODO:4 最终效果 ## 5 遗留问题 * 响应速度待提高