初始提交卡包 USB 工程
纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。 Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,742 @@
|
||||
#include "FLASH/flash.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_timer.h"
|
||||
#include "ZIP_EXTRACTOR/zip_extractor.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include <sys/stat.h>
|
||||
#include "esp_system.h"
|
||||
#include "soc/spi_pins.h"
|
||||
#include "esp_vfs_fat_nand.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "EVENT/event.h"
|
||||
#include "BLE/ble.h"
|
||||
#include "EMOJI_STORAGE/emoji_storage.h"
|
||||
#include "LVGL/lvgl_init.h"
|
||||
#include <errno.h>
|
||||
|
||||
static const char *TAG_FLASH = "example";
|
||||
|
||||
/* 字体已由产线直接烧录到外部 Flash,不再从 app 内置资源同步 */
|
||||
#define UI_EMBED_FONT_BIN_TO_APP 0
|
||||
|
||||
/* extflash 访问互斥锁:保护所有对 /extflash 的 fopen/fread/fwrite/stat/opendir 操作 */
|
||||
static SemaphoreHandle_t s_extflash_mutex = NULL;
|
||||
/* 解压进行中:动画切换应延后,避免与解压写操作竞争 */
|
||||
static volatile bool s_extraction_in_progress = false;
|
||||
|
||||
void extflash_lock(void)
|
||||
{
|
||||
if (s_extflash_mutex) {
|
||||
xSemaphoreTake(s_extflash_mutex, portMAX_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
void extflash_unlock(void)
|
||||
{
|
||||
if (s_extflash_mutex) {
|
||||
xSemaphoreGive(s_extflash_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
void extflash_extraction_started(void)
|
||||
{
|
||||
s_extraction_in_progress = true;
|
||||
}
|
||||
|
||||
void extflash_extraction_finished(void)
|
||||
{
|
||||
s_extraction_in_progress = false;
|
||||
}
|
||||
|
||||
bool extflash_is_extracting(void)
|
||||
{
|
||||
return s_extraction_in_progress;
|
||||
}
|
||||
#define EXTRACT_TASK_STACK_SIZE 16384 // 16KB 栈
|
||||
#define EXTRACT_TASK_PRIORITY 4
|
||||
// 分区的挂载路径
|
||||
const char *base_path = "/extflash";//
|
||||
|
||||
// ********** 适配GD5F1GM7UEYIG(SPI NAND)+ WP/HD硬件接3.3V **********
|
||||
#define EXAMPLE_FLASH_FREQ_KHZ 40000 // 先升到40MHz做稳定性/性能验证(后续可再评估到80MHz)
|
||||
static spi_nand_flash_device_t *g_nand_flash_handle = NULL;
|
||||
static spi_device_handle_t g_spi_handle = NULL;
|
||||
// 磨损均衡句柄(原NOR的WL,NAND驱动内部已实现,保留兼容)
|
||||
static wl_handle_t s_wl_handle = WL_INVALID_HANDLE;
|
||||
|
||||
void list_files_in_directory(const char *path);
|
||||
static bool copy_file(const char *source_path, const char *dest_path);
|
||||
static void extract_task(void *pvParameters);
|
||||
static StackType_t *s_extract_stack = NULL;
|
||||
static StaticTask_t *s_extract_tcb = NULL;
|
||||
static bool example_mount_fatfs(void);
|
||||
static esp_err_t example_init_nand_flash(void);
|
||||
static esp_err_t read_chip_id(spi_device_handle_t handle, uint8_t *id_data, size_t len);
|
||||
esp_err_t nand_read_jedec_id(spi_device_handle_t handle, uint8_t id[3]);
|
||||
static bool sync_embedded_fonts_to_extflash(void);
|
||||
static bool is_zip_file(const char *filename);
|
||||
|
||||
static void example_list_data_partitions(void);
|
||||
|
||||
static bool sync_embedded_fonts_to_extflash(void)
|
||||
{
|
||||
static const char *fonts[] = {
|
||||
"iconfont_25.bin",
|
||||
"MiSans_VF_25.bin",
|
||||
"MiSans_VF_30.bin",
|
||||
"MiSans_VF_40.bin",
|
||||
"MiSans_VF_60.bin",
|
||||
"MiSans_VF_100.bin",
|
||||
};
|
||||
bool all_ok = true;
|
||||
char fonts_dir[64] = {0};
|
||||
snprintf(fonts_dir, sizeof(fonts_dir), "%s/fonts", base_path);
|
||||
|
||||
#if UI_EMBED_FONT_BIN_TO_APP
|
||||
ESP_LOGI(TAG_FLASH, "font sync mode: app embed -> extflash");
|
||||
#else
|
||||
ESP_LOGI(TAG_FLASH, "font sync mode: extflash preloaded only");
|
||||
#endif
|
||||
|
||||
extflash_lock();
|
||||
for (size_t i = 0; i < sizeof(fonts) / sizeof(fonts[0]); ++i) {
|
||||
char dst_path[128] = {0};
|
||||
struct stat st;
|
||||
snprintf(dst_path, sizeof(dst_path), "%s/%s", fonts_dir, fonts[i]);
|
||||
if (stat(dst_path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0) {
|
||||
ESP_LOGI(TAG_FLASH, "font ready: %s (%ld bytes)", dst_path, (long)st.st_size);
|
||||
continue;
|
||||
}
|
||||
ESP_LOGE(TAG_FLASH, "font missing or invalid: %s", dst_path);
|
||||
all_ok = false;
|
||||
}
|
||||
|
||||
extflash_unlock();
|
||||
return all_ok;
|
||||
}
|
||||
|
||||
// 初始化外部Flash芯片
|
||||
static esp_err_t example_init_nand_flash(void)
|
||||
{
|
||||
// SPI总线配置:WP/HD设为-1(硬件接3.3V),适配大传输量
|
||||
const spi_bus_config_t bus_config = {
|
||||
.mosi_io_num = PIN_MOSI,
|
||||
.miso_io_num = PIN_MISO,
|
||||
.sclk_io_num = PIN_CLK,
|
||||
.quadhd_io_num = PIN_HD, // -1:禁用HD引脚(硬件接3.3V)
|
||||
.quadwp_io_num = PIN_WP, // -1:禁用WP引脚(硬件接3.3V)
|
||||
.max_transfer_sz = 4096 * 2, // 增大传输缓冲区,适配NAND页操作
|
||||
};
|
||||
|
||||
ESP_LOGI(TAG_FLASH, "Initializing GD5F1GM7UEYIG SPI NAND Flash (WP/HD HW 3.3V)");
|
||||
ESP_LOGI(TAG_FLASH, "Pin assignments:");
|
||||
ESP_LOGI(TAG_FLASH, "MOSI: %2d MISO: %2d SCLK: %2d CS: %2d WP: %2d HD: %2d",
|
||||
PIN_MOSI, PIN_MISO, PIN_CLK, PIN_CS, PIN_WP, PIN_HD);
|
||||
ESP_LOGI(TAG_FLASH, "DMA CHANNEL: %d", SPI_DMA_CHAN);
|
||||
|
||||
// 初始化SPI总线
|
||||
ESP_ERROR_CHECK(spi_bus_initialize(HOST_ID, &bus_config, SPI_DMA_CHAN));
|
||||
|
||||
// SPI设备配置:半双工(NAND推荐)、80MHz时钟
|
||||
spi_device_interface_config_t devcfg = {
|
||||
.clock_speed_hz = EXAMPLE_FLASH_FREQ_KHZ * 1000,
|
||||
.mode = 0,
|
||||
.spics_io_num = PIN_CS,
|
||||
.queue_size = 10,
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
// 添加SPI设备到总线
|
||||
ESP_ERROR_CHECK(spi_bus_add_device(HOST_ID, &devcfg, &g_spi_handle));
|
||||
|
||||
uint8_t id[4] = {0};
|
||||
nand_read_jedec_id(g_spi_handle, id);
|
||||
printf("NAND ID: 0x%02X 0x%02X 0x%02X\n", id[0], id[1], id[2]);
|
||||
|
||||
if (id[1] != 0xC8 || id[2] != 0x91) {
|
||||
ESP_LOGE(TAG_FLASH, "Flash chip ID error! Not GD5F1GM7");
|
||||
return ESP_ERR_INVALID_VERSION;
|
||||
}
|
||||
|
||||
// SPI NAND Flash专用配置:适配GD5F1GM7的SIO模式(默认),WP/HD硬件禁用
|
||||
spi_nand_flash_config_t nand_flash_config = {
|
||||
.device_handle = g_spi_handle,
|
||||
.io_mode = SPI_NAND_IO_MODE_SIO, // GD5F1GM7支持SIO/DIO/QIO,先选SIO(兼容)
|
||||
.flags = 0, // 与SPI设备保持一致
|
||||
};
|
||||
assert(devcfg.flags == nand_flash_config.flags);
|
||||
|
||||
// 初始化NAND Flash设备(驱动自动识别GD5F1GM7芯片ID/容量)
|
||||
ESP_ERROR_CHECK(spi_nand_flash_init_device(&nand_flash_config, &g_nand_flash_handle));
|
||||
ESP_LOGI(TAG_FLASH, "GD5F1GM7UEYIG NAND Flash initialized successfully");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ********** 复制JSON文件到NAND Flash(原逻辑不变) **********
|
||||
static bool copy_json_files_to_ext_flash(const char *source_dir, const char *dest_dir)
|
||||
{
|
||||
DIR *dir;
|
||||
struct dirent *entry;
|
||||
char source_path[256];
|
||||
char dest_path[256];
|
||||
|
||||
ESP_LOGI(TAG_FLASH, "开始复制JSON文件从 %s 到 %s", source_dir, dest_dir);
|
||||
dir = opendir(source_dir);
|
||||
if (dir == NULL) {
|
||||
ESP_LOGE(TAG_FLASH, "无法打开源目录: %s, err: %s", source_dir, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int file_count = 0;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
|
||||
char *ext = strrchr(entry->d_name, '.');
|
||||
if (ext != NULL && strcasecmp(ext, ".json") == 0) {
|
||||
snprintf(source_path, sizeof(source_path), "%s/%s", source_dir, entry->d_name);
|
||||
snprintf(dest_path, sizeof(dest_path), "%s/%s", dest_dir, entry->d_name);
|
||||
|
||||
ESP_LOGI(TAG_FLASH, "复制文件: %s -> %s", source_path, dest_path);
|
||||
if (copy_file(source_path, dest_path)) {
|
||||
file_count++;
|
||||
ESP_LOGI(TAG_FLASH, "文件复制成功: %s", entry->d_name);
|
||||
} else {
|
||||
ESP_LOGE(TAG_FLASH, "文件复制失败: %s", entry->d_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
ESP_LOGI(TAG_FLASH, "总共复制了 %d 个JSON文件", file_count);
|
||||
return (file_count > 0);
|
||||
}
|
||||
|
||||
// ********** 复制单个文件(原逻辑不变,NAND写入优化) **********
|
||||
static bool copy_file(const char *source_path, const char *dest_path)
|
||||
{
|
||||
FILE *src_file, *dest_file;
|
||||
char buffer[4096]; // 增大缓冲区,适配NAND写入
|
||||
size_t bytes_read;
|
||||
bool need_lock = (strstr(dest_path, "/extflash") != NULL);
|
||||
|
||||
if (need_lock) extflash_lock();
|
||||
src_file = fopen(source_path, "rb");
|
||||
if (src_file == NULL) {
|
||||
ESP_LOGE(TAG_FLASH, "无法打开源文件: %s, err: %s", source_path, strerror(errno));
|
||||
if (need_lock) extflash_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
dest_file = fopen(dest_path, "wb");
|
||||
if (dest_file == NULL) {
|
||||
ESP_LOGE(TAG_FLASH, "无法创建目标文件: %s, err: %s", dest_path, strerror(errno));
|
||||
fclose(src_file);
|
||||
if (need_lock) extflash_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
while ((bytes_read = fread(buffer, 1, sizeof(buffer), src_file)) > 0) {
|
||||
if (fwrite(buffer, 1, bytes_read, dest_file) != bytes_read) {
|
||||
ESP_LOGE(TAG_FLASH, "写入文件失败: %s", dest_path);
|
||||
fclose(src_file);
|
||||
fclose(dest_file);
|
||||
if (need_lock) extflash_unlock();
|
||||
return false;
|
||||
}
|
||||
fflush(dest_file); // NAND建议及时刷新,避免数据丢失
|
||||
}
|
||||
|
||||
fclose(src_file);
|
||||
fclose(dest_file);
|
||||
if (need_lock) extflash_unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ********** 列出目录中的文件(原逻辑不变) **********
|
||||
void list_files_in_directory(const char *path)
|
||||
{
|
||||
DIR *dir;
|
||||
struct dirent *entry;
|
||||
struct stat file_stat;
|
||||
char file_path[256];
|
||||
|
||||
dir = opendir(path);
|
||||
if (dir == NULL) {
|
||||
ESP_LOGE(TAG_FLASH, "无法打开目录: %s, err: %s", path, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_FLASH, "目录 %s 下的文件/目录:", path);
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
|
||||
snprintf(file_path, sizeof(file_path), "%s/%s", path, entry->d_name);
|
||||
if (stat(file_path, &file_stat) == 0) {
|
||||
if (S_ISREG(file_stat.st_mode)) {
|
||||
ESP_LOGI(TAG_FLASH, " 文件: %s, 大小: %ld 字节", entry->d_name, file_stat.st_size);
|
||||
} else if (S_ISDIR(file_stat.st_mode)) {
|
||||
ESP_LOGI(TAG_FLASH, " 目录: %s", entry->d_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
// ********** 下载文件到NAND Flash(原逻辑微调,适配NAND写入) **********
|
||||
esp_err_t download_file(char *firmware_url, char *filename)
|
||||
{
|
||||
if (filename == NULL || filename[0] == '\0' || !strcmp(filename, ".") || !strcmp(filename, "..")) {
|
||||
ESP_LOGE(TAG_FLASH, "download_file: 无效文件名");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_http_client_config_t config = {
|
||||
.method = HTTP_METHOD_GET,
|
||||
.url = firmware_url,
|
||||
.timeout_ms = 300000,
|
||||
.buffer_size = 32768,
|
||||
.buffer_size_tx = 4096,
|
||||
.keep_alive_enable = true,
|
||||
.keep_alive_idle = 30,
|
||||
.keep_alive_interval = 10,
|
||||
.keep_alive_count = 5,
|
||||
.disable_auto_redirect = false,
|
||||
.max_redirection_count = 3,
|
||||
.auth_type = HTTP_AUTH_TYPE_NONE,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint32_t total_size;
|
||||
uint32_t downloaded;
|
||||
bool download_complete;
|
||||
esp_err_t last_error;
|
||||
} download_status_t;
|
||||
static download_status_t g_download_status = {0};
|
||||
memset(&g_download_status, 0, sizeof(g_download_status));
|
||||
|
||||
esp_err_t ret = ESP_FAIL;
|
||||
FILE *file = NULL;
|
||||
uint8_t *buffer = heap_caps_malloc(16384, MALLOC_CAP_SPIRAM);
|
||||
if (!buffer) {
|
||||
ESP_LOGE(TAG_FLASH, "Failed to allocate buffer");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
char full_path[256];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", base_path, filename);
|
||||
|
||||
extflash_lock();
|
||||
struct stat st;
|
||||
if (stat(full_path, &st) == 0 && !S_ISDIR(st.st_mode)) {
|
||||
ESP_LOGI(TAG_FLASH, "Removing existing file: %s", full_path);
|
||||
remove(full_path);
|
||||
}
|
||||
mkdir(base_path, 0755);
|
||||
/* 文件名可带子目录,例如 eaf/foo.eaf → /extflash/eaf/ */
|
||||
{
|
||||
char parent[256];
|
||||
snprintf(parent, sizeof(parent), "%s", full_path);
|
||||
char *slash = strrchr(parent, '/');
|
||||
if (slash && slash != parent) {
|
||||
*slash = '\0';
|
||||
if (strcmp(parent, base_path) != 0 && mkdir(parent, 0755) < 0 && errno != EEXIST) {
|
||||
ESP_LOGW(TAG_FLASH, "mkdir %s 失败: %s", parent, strerror(errno));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 断点重连循环(减少重试次数,NAND不建议频繁重试)
|
||||
for (int retry_count = 0; retry_count <= 5; retry_count++) {
|
||||
if (retry_count > 0) {
|
||||
extflash_unlock();
|
||||
ESP_LOGW(TAG_FLASH, "Retry attempt %d/5", retry_count);
|
||||
vTaskDelay(pdMS_TO_TICKS(2000 * retry_count));
|
||||
extflash_lock();
|
||||
}
|
||||
|
||||
file = fopen(full_path, "ab+");
|
||||
if (!file) {
|
||||
ESP_LOGE(TAG_FLASH, "Failed to open file: %s, err: %s", full_path, strerror(errno));
|
||||
continue;
|
||||
}
|
||||
fseek(file, 0, SEEK_END);
|
||||
size_t current_size = ftell(file);
|
||||
ESP_LOGI(TAG_FLASH, "Current file size: %zu bytes", current_size);
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
if (!client) {
|
||||
ESP_LOGE(TAG_FLASH, "Failed to initialize HTTP client");
|
||||
fclose(file);
|
||||
extflash_unlock();
|
||||
continue;
|
||||
}
|
||||
esp_http_client_set_header(client, "Content-Type", "text/plain");
|
||||
if (current_size > 0) {
|
||||
char range_header[64];
|
||||
snprintf(range_header, sizeof(range_header), "bytes=%zu-", current_size);
|
||||
esp_http_client_set_header(client, "Range", range_header);
|
||||
ESP_LOGI(TAG_FLASH, "Setting Range header: %s", range_header);
|
||||
}
|
||||
|
||||
ret = esp_http_client_open(client, 0);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG_FLASH, "Failed to open HTTP connection: %s", esp_err_to_name(ret));
|
||||
esp_http_client_cleanup(client);
|
||||
fclose(file);
|
||||
extflash_unlock();
|
||||
continue;
|
||||
}
|
||||
|
||||
int content_length = esp_http_client_fetch_headers(client);
|
||||
g_download_status.total_size = (content_length > 0) ? (content_length + current_size) : 0;
|
||||
if (content_length > 0) {
|
||||
ESP_LOGI(TAG_FLASH, "Content length: %d bytes, Total expected: %" PRIu32 " bytes",
|
||||
content_length, (uint32_t)g_download_status.total_size);
|
||||
}
|
||||
g_download_status.downloaded = current_size;
|
||||
|
||||
// 下载数据
|
||||
int read_len;
|
||||
bool download_error = false;
|
||||
int64_t last_progress_time = esp_timer_get_time();
|
||||
uint32_t last_progress_size = current_size;
|
||||
while (true) {
|
||||
read_len = esp_http_client_read(client, (char *)buffer, 16384);
|
||||
if (read_len < 0) {
|
||||
ESP_LOGE(TAG_FLASH, "Error reading HTTP data");
|
||||
download_error = true;
|
||||
break;
|
||||
} else if (read_len == 0) {
|
||||
ESP_LOGI(TAG_FLASH, "HTTP read complete");
|
||||
break;
|
||||
}
|
||||
|
||||
if (fwrite(buffer, 1, read_len, file) != read_len) {
|
||||
ESP_LOGE(TAG_FLASH, "Write error: expected %d, got %zu", read_len, (size_t)fwrite(buffer,1,read_len,file));
|
||||
download_error = true;
|
||||
break;
|
||||
}
|
||||
if (g_download_status.downloaded % 65536 == 0) {
|
||||
fflush(file);
|
||||
}
|
||||
|
||||
g_download_status.downloaded += read_len;
|
||||
// 打印下载进度
|
||||
int64_t current_time = esp_timer_get_time();
|
||||
if (current_time - last_progress_time > 1000000) {
|
||||
uint32_t speed = (g_download_status.downloaded - last_progress_size) / 1024;
|
||||
if (g_download_status.total_size > 0) {
|
||||
int percent = (g_download_status.downloaded * 100) / g_download_status.total_size;
|
||||
ESP_LOGI(TAG_FLASH, "Progress: %" PRIu32 "/%" PRIu32 " bytes (%d%%) - Speed: %" PRIu32 " KB/s",
|
||||
(uint32_t)g_download_status.downloaded, (uint32_t)g_download_status.total_size, percent, (uint32_t)speed);
|
||||
} else {
|
||||
ESP_LOGI(TAG_FLASH, "Progress: %" PRIu32 " bytes - Speed: %" PRIu32 " KB/s",
|
||||
(uint32_t)g_download_status.downloaded, (uint32_t)speed);
|
||||
}
|
||||
last_progress_time = current_time;
|
||||
last_progress_size = g_download_status.downloaded;
|
||||
}
|
||||
|
||||
if (esp_http_client_get_status_code(client) == 408) {
|
||||
ESP_LOGE(TAG_FLASH, "HTTP timeout occurred");
|
||||
download_error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fflush(file);
|
||||
fclose(file);
|
||||
if (download_error) {
|
||||
esp_http_client_cleanup(client);
|
||||
extflash_unlock();
|
||||
continue;
|
||||
}
|
||||
if (g_download_status.total_size > 0 && g_download_status.downloaded < g_download_status.total_size) {
|
||||
ESP_LOGW(TAG_FLASH, "Download incomplete: %" PRIu32 "/%" PRIu32 " bytes",
|
||||
(uint32_t)g_download_status.downloaded, (uint32_t)g_download_status.total_size);
|
||||
esp_http_client_cleanup(client);
|
||||
extflash_unlock();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 下载成功,创建解压任务
|
||||
ESP_LOGI(TAG_FLASH, "Download completed successfully: %" PRIu32 " bytes",
|
||||
(uint32_t)g_download_status.downloaded);
|
||||
esp_http_client_cleanup(client);
|
||||
if (!is_zip_file(filename)) {
|
||||
ESP_LOGI(TAG_FLASH, "非 ZIP,已写入 %s(%" PRIu32 " 字节),跳过解压",
|
||||
full_path, (uint32_t)g_download_status.downloaded);
|
||||
extflash_unlock();
|
||||
free(buffer);
|
||||
return ESP_OK;
|
||||
}
|
||||
char *filename_copy = strdup(filename);
|
||||
if (!filename_copy) {
|
||||
ESP_LOGE(TAG_FLASH, "strdup(filename) failed");
|
||||
extflash_unlock();
|
||||
free(buffer);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
size_t stack_size = 256 * 1024;
|
||||
heap_caps_free(s_extract_stack);
|
||||
vPortFree(s_extract_tcb);
|
||||
s_extract_stack = heap_caps_malloc(stack_size, MALLOC_CAP_SPIRAM);
|
||||
s_extract_tcb = pvPortMalloc(sizeof(StaticTask_t));
|
||||
if (s_extract_stack && s_extract_tcb) {
|
||||
ESP_LOGI(TAG_FLASH, "[DEBUG] Creating extract_task (static, SPIRAM stack %u bytes)", (unsigned)stack_size);
|
||||
xTaskCreateStatic(extract_task, "extract_task", stack_size / sizeof(StackType_t),
|
||||
(void *)filename_copy, configMAX_PRIORITIES - 1,
|
||||
s_extract_stack, s_extract_tcb);
|
||||
} else {
|
||||
ESP_LOGW(TAG_FLASH, "[DEBUG] SPIRAM alloc failed, falling back to dynamic task");
|
||||
xTaskCreatePinnedToCore(extract_task, "extract_task", stack_size, (void *)filename_copy,
|
||||
configMAX_PRIORITIES - 1, NULL, 1);
|
||||
heap_caps_free(s_extract_stack);
|
||||
vPortFree(s_extract_tcb);
|
||||
s_extract_stack = NULL;
|
||||
s_extract_tcb = NULL;
|
||||
}
|
||||
extflash_unlock();
|
||||
free(buffer);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
extflash_unlock();
|
||||
free(buffer);
|
||||
ESP_LOGE(TAG_FLASH, "Download failed after all retries");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
static void extract_task(void *pvParameters)
|
||||
{
|
||||
char *filename_copy = (char *)pvParameters;
|
||||
ESP_LOGI("EXTRACT", "[DEBUG] extract_task started, file=%s, core=%d, prio=%d, stack_hwm=%u",
|
||||
filename_copy ? filename_copy : "NULL",
|
||||
(int)xPortGetCoreID(),
|
||||
(int)uxTaskPriorityGet(NULL),
|
||||
(unsigned)uxTaskGetStackHighWaterMark(NULL));
|
||||
|
||||
extflash_extraction_started();
|
||||
extflash_lock();
|
||||
if (filename_copy) {
|
||||
extract_json_zip(filename_copy);
|
||||
free(filename_copy);
|
||||
}
|
||||
list_files_in_directory(base_path);
|
||||
|
||||
/* ── Fallback: main_task 不可用时直接处理下载结果 ── */
|
||||
if (main_task_handle == NULL)
|
||||
{
|
||||
char pack_id = zip_extractor_get_last_downloaded_pack();
|
||||
if (pack_id != '0')
|
||||
{
|
||||
ESP_LOGW("EXTRACT", "main_task not running, handling DOWNLOAD_SUCCESS directly (pack='%c')", pack_id);
|
||||
|
||||
if (notification_task_handle)
|
||||
xTaskNotify(notification_task_handle, BLE_SEND_DL_EMOJI_SUCCESS, eSetBits);
|
||||
|
||||
emoji_storage_add_downloaded_pack(pack_id);
|
||||
emoji_storage_save_using_pack(pack_id);
|
||||
ESP_LOGI("EXTRACT", "pack '%c' saved as downloaded & using", pack_id);
|
||||
|
||||
char path_buf[64];
|
||||
snprintf(path_buf, sizeof(path_buf), "/extflash/%c/default.json", pack_id);
|
||||
struct stat st;
|
||||
if (stat(path_buf, &st) == 0)
|
||||
{
|
||||
ESP_LOGI("EXTRACT", "Playing animation: %s (%ld bytes)", path_buf, (long)st.st_size);
|
||||
request_play_lottie_path(path_buf);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW("EXTRACT", "Animation file not found: %s, skip play", path_buf);
|
||||
}
|
||||
|
||||
g_emoji_upgrade_downloaded_pack = pack_id;
|
||||
g_emoji_upgrade_pending_finalize = true;
|
||||
ESP_LOGI("EXTRACT", "下载成功 pack='%c':等待首次切换后收尾(不重启,不回原来)", pack_id);
|
||||
clear_system_event(DOWNLOAD_SUCCESS_BIT);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW("EXTRACT", "main_task not running, handling DOWNLOAD_FAILED directly");
|
||||
|
||||
if (notification_task_handle)
|
||||
xTaskNotify(notification_task_handle, BLE_SEND_DL_EMOJI_FAILED, eSetBits);
|
||||
|
||||
clear_system_event(DOWNLOAD_FAILED_BIT);
|
||||
}
|
||||
}
|
||||
extflash_unlock();
|
||||
extflash_extraction_finished();
|
||||
/*
|
||||
ESP_LOGI("EXTRACT", "[DEBUG] extract_task finished, about to delete self. "
|
||||
"queue_pending=%d, stack_hwm=%u",
|
||||
(int)uxQueueMessagesWaiting(system_event_queue),
|
||||
(unsigned)uxTaskGetStackHighWaterMark(NULL)); //这里解压时注意可能死机
|
||||
*/
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
// ********** 检查文件是否为ZIP格式(原逻辑不变) **********
|
||||
static bool is_zip_file(const char *filename)
|
||||
{
|
||||
if (!filename) return false;
|
||||
const char *dot = strrchr(filename, '.');
|
||||
return (dot && strcasecmp(dot, ".zip") == 0);
|
||||
}
|
||||
|
||||
// ********** 反初始化NAND Flash(新增,用于程序退出/复位) **********
|
||||
static void example_deinit_nand_flash(void)
|
||||
{
|
||||
if (g_nand_flash_handle) {
|
||||
esp_vfs_fat_nand_unmount(base_path, g_nand_flash_handle);
|
||||
spi_nand_flash_deinit_device(g_nand_flash_handle);
|
||||
g_nand_flash_handle = NULL;
|
||||
}
|
||||
if (g_spi_handle) {
|
||||
spi_bus_remove_device(g_spi_handle);
|
||||
g_spi_handle = NULL;
|
||||
}
|
||||
spi_bus_free(HOST_ID);
|
||||
ESP_LOGI(TAG_FLASH, "GD5F1GM7UEYIG NAND Flash deinitialized");
|
||||
}
|
||||
// ********** 正确 NAND 版本:无参数 **********
|
||||
static bool example_mount_fatfs(void)
|
||||
{
|
||||
ESP_LOGI(TAG_FLASH, "挂载 NAND FAT 文件系统");
|
||||
|
||||
if (g_nand_flash_handle == NULL) {
|
||||
ESP_LOGE(TAG_FLASH, "NAND Flash 句柄为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// FAT 配置(NAND 专用)
|
||||
const esp_vfs_fat_mount_config_t mount_config = {
|
||||
.max_files = 6,
|
||||
#ifdef CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED
|
||||
.format_if_mount_failed = true,
|
||||
#else
|
||||
.format_if_mount_failed = false,
|
||||
#endif
|
||||
.allocation_unit_size = 16 * 1024,
|
||||
};
|
||||
|
||||
// NAND 专用挂载接口(不是 NOR 的 spiflash_mount!)
|
||||
esp_err_t err = esp_vfs_fat_nand_mount(base_path, g_nand_flash_handle, &mount_config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG_FLASH, "NAND FATFS 挂载失败: %s (0x%x)", esp_err_to_name(err), err);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 打印容量
|
||||
uint64_t bytes_total, bytes_free;
|
||||
esp_vfs_fat_info(base_path, &bytes_total, &bytes_free);
|
||||
ESP_LOGI(TAG_FLASH, "NAND FAT 挂载成功: %s", base_path);
|
||||
ESP_LOGI(TAG_FLASH, "总容量: %" PRIu64 " KB, 剩余: %" PRIu64 " KB",
|
||||
bytes_total / 1024, bytes_free / 1024);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// GD5F1GM7UEYIG 读 ID 函数
|
||||
// 输出:id[0] = 0xC8, id[1] = 0x91, id[2] = 0x01
|
||||
esp_err_t nand_read_jedec_id(spi_device_handle_t handle, uint8_t id[3])
|
||||
{
|
||||
if (handle == NULL || id == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
memset(id, 0, 3);
|
||||
|
||||
// GD5F1GM7 标准 Read ID 时序:必须读 4 字节
|
||||
uint8_t tx_data[4] = {0x9F, 0x00, 0x00, 0x00};
|
||||
uint8_t rx_data[4] = {0};
|
||||
|
||||
spi_transaction_t t = {
|
||||
.length = 4 * 8, // 必须 32bit,才能读到 0x91
|
||||
.tx_buffer = tx_data,
|
||||
.rx_buffer = rx_data,
|
||||
};
|
||||
|
||||
esp_err_t ret = spi_device_transmit(handle, &t);
|
||||
|
||||
if (ret == ESP_OK) {
|
||||
id[0] = rx_data[1]; // 0xC8 厂商ID
|
||||
id[1] = rx_data[2]; // 0x91 设备ID ✅
|
||||
id[2] = rx_data[3]; // 扩展ID
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void example_list_data_partitions(void)
|
||||
{
|
||||
ESP_LOGI(TAG_FLASH, "Listing data partitions:");
|
||||
// 查找所有数据类型的分区
|
||||
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, NULL);
|
||||
|
||||
// 遍历并打印每个分区的信息
|
||||
for (; it != NULL; it = esp_partition_next(it))
|
||||
{
|
||||
const esp_partition_t *part = esp_partition_get(it);
|
||||
ESP_LOGI(TAG_FLASH, "- partition '%s', subtype %d, offset 0x%" PRIx32 ", size %" PRIu32 " kB",
|
||||
part->label, part->subtype, part->address, part->size / 1024);
|
||||
}
|
||||
|
||||
esp_partition_iterator_release(it); // 释放迭代器
|
||||
}
|
||||
|
||||
// ********** 主入口函数(替换原flash_main,适配NAND+WP/HD接3.3V) **********
|
||||
void flash_main(void)
|
||||
{
|
||||
if (s_extflash_mutex == NULL) {
|
||||
s_extflash_mutex = xSemaphoreCreateMutex();
|
||||
if (s_extflash_mutex == NULL) {
|
||||
ESP_LOGE(TAG_FLASH, "Failed to create extflash mutex");
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 初始化GD5F1GM7UEYIG SPI NAND Flash(WP/HD硬件接3.3V)
|
||||
if (example_init_nand_flash() != ESP_OK) {
|
||||
ESP_LOGE(TAG_FLASH, "GD5F1GM7UEYIG NAND Flash初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 挂载NAND FAT文件系统
|
||||
if (!example_mount_fatfs()) {
|
||||
ESP_LOGE(TAG_FLASH, "NAND FAT文件系统挂载失败,无法继续");
|
||||
example_deinit_nand_flash();
|
||||
return;
|
||||
}
|
||||
|
||||
example_list_data_partitions();
|
||||
|
||||
// 3. 校验 /extflash/fonts 下字体是否就绪(不再从 app 内同步)
|
||||
if (!sync_embedded_fonts_to_extflash()) {
|
||||
ESP_LOGW(TAG_FLASH, "部分字体缺失/异常,运行时可能回退到默认字体");
|
||||
}
|
||||
|
||||
// 4. 列出NAND Flash中的文件(含 fonts 目录,便于产线验收字体)
|
||||
extflash_lock();
|
||||
ESP_LOGI(TAG_FLASH, "GD5F1GM7UEYIG NAND Flash中的文件列表:");
|
||||
list_files_in_directory(base_path);
|
||||
char fonts_dir[64] = {0};
|
||||
snprintf(fonts_dir, sizeof(fonts_dir), "%s/fonts", base_path);
|
||||
ESP_LOGI(TAG_FLASH, "字体目录文件列表: %s", fonts_dir);
|
||||
list_files_in_directory(fonts_dir);
|
||||
extflash_unlock();
|
||||
|
||||
ESP_LOGI(TAG_FLASH, "GD5F1GM7UEYIG NAND Flash初始化&挂载完成,业务逻辑就绪");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef FLASH_H
|
||||
#define FLASH_H
|
||||
|
||||
#pragma GCC diagnostic ignored "-Wformat-truncation"
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include "esp_flash.h"
|
||||
#include "esp_flash_spi_init.h"
|
||||
#include "esp_partition.h"
|
||||
#include "esp_vfs.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "esp_system.h"
|
||||
#include "soc/spi_pins.h"
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define HOST_ID SPI3_HOST // 使用SPI3主机(VSPI)
|
||||
#define PIN_MOSI 39
|
||||
#define PIN_MISO 41
|
||||
#define PIN_CLK 40
|
||||
#define PIN_CS 42
|
||||
#define PIN_WP -1
|
||||
#define PIN_HD -1
|
||||
#define SPI_DMA_CHAN SPI_DMA_CH_AUTO // DMA通道:自动选择
|
||||
|
||||
/* extflash 访问互斥:所有对 /extflash 的 fopen/fread/fwrite/stat/opendir 等操作需在持锁期间进行 */
|
||||
void extflash_lock(void);
|
||||
void extflash_unlock(void);
|
||||
/* 解压进行中标志:动画切换应延后,避免与解压写操作竞争 */
|
||||
void extflash_extraction_started(void);
|
||||
void extflash_extraction_finished(void);
|
||||
bool extflash_is_extracting(void);
|
||||
|
||||
void list_files_in_directory(const char *path);
|
||||
static bool copy_file(const char *source_path, const char *dest_path);
|
||||
static void extract_task(void *pvParameters);
|
||||
static bool example_mount_fatfs(void);
|
||||
void flash_main(void);
|
||||
esp_err_t download_file(char *firmware_url, char *filename);
|
||||
static esp_err_t read_chip_id(spi_device_handle_t handle, uint8_t *id_data, size_t len);
|
||||
|
||||
|
||||
#endif // FLASH_H
|
||||
Reference in New Issue
Block a user