初始提交卡包 USB 工程

纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
zhengxin
2026-10-13 21:54:50 +08:00
co-authored by Cursor
commit 851e347cf5
260 changed files with 269349 additions and 0 deletions
+742
View File
@@ -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初始化&挂载完成,业务逻辑就绪");
}
+46
View File
@@ -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