This commit is contained in:
tang1219
2026-06-12 17:03:10 +08:00
parent e488cd3216
commit 9eaffcc9ff
31 changed files with 6829 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# Project Structure (Auto-generated)
> This file was automatically generated by CodeWhale.
> You can edit or delete it at any time.
**Summary:** Unknown project type
**Tree:**
```
FILE: .gitignore
DIR: .pio
DIR: build
DIR: .vscode
FILE: c_cpp_properties.json
FILE: extensions.json
FILE: launch.json
FILE: CMakeLists.txt
DIR: include
FILE: README
DIR: lib
FILE: README
FILE: platformio.ini
FILE: sdkconfig.seeed_xiao_esp32s3
DIR: src
FILE: CMakeLists.txt
FILE: main.c
DIR: test
FILE: README
```
+7
View File
@@ -86,3 +86,10 @@ dkms.conf
*.out *.out
*.app *.app
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
+10
View File
@@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}
+3
View File
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ESP32_Dass2)
+357
View File
@@ -0,0 +1,357 @@
# OTA Manager 回调文档
> 版本: 1.0
> 目标芯片: ESP32-S3
> 框架: ESP-IDF 5.1.x
> 模块: `ota_manager.h` / `ota_manager.c`
---
## 1. 架构概览
```
┌──────────────────────────────────────────────────────────┐
│ 传输层(USB / 串口 / 蓝牙 / SD / HTTP
│ ↓ │
│ ota_begin(size) → ota_write(chunk)×N → ota_end() │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ OTA Manager(本模块) │ │
│ │ · 状态机管理(IDLE → PROGRESS → VERIFY → DONE)│ │
│ │ · esp_ota_ops 封装(分区选择 / 擦除 / 写入) │ │
│ │ · 回调触发(进度 / 状态 / 完成 / 错误) │ │
│ │ · LED 反馈 │ │
│ └──────────────┬──────────────────────────────────┘ │
│ │ 回调(可注册 4 种) │
│ ┌───────────┼───────────┬──────────────┐ │
│ ▼ ▼ ▼ ▼ │
│ progress state complete error │
│ (进度条) (状态灯) (重启逻辑) (告警/日志) │
└──────────────────────────────────────────────────────────┘
```
**设计原则**OTA Manager 只负责底层固件写入与分区切换,**不关心**固件数据从哪里来。传输层代码只需顺序调用三个 API,并通过回调感知进度。
---
## 2. 状态机
```
ota_begin()
IDLE ──────────────────────────→ IN_PROGRESS
↑ │
│ │ ota_write()
│ ▼ (循环)
│ IN_PROGRESS
│ │
│ ┌─────────────┼──────────────┐
│ │ ota_end() │ (写入失败) │
│ ▼ ▼ │
│ VERIFYING ERROR │
│ │ │ │
│ ┌───────────┤ │ ota_abort() │
│ │ 成功 │ 失败 ▼ │
│ ▼ ▼ IDLE │
│ COMPLETE ERROR │
│ │ │
│ │ esp_restart() │
│ ▼ │
│ (重启) │
└────────────────────────────────────────────────────┘
```
**状态枚举** (`ota_state_t`)
| 状态 | 含义 | 允许的操作 |
|------|------|-----------|
| `OTA_STATE_IDLE` | 空闲,就绪 | `ota_begin()` |
| `OTA_STATE_IN_PROGRESS` | 正在接收固件 | `ota_write()`, `ota_end()`, `ota_abort()` |
| `OTA_STATE_VERIFYING` | 正在校验固件 | 无(内部过程) |
| `OTA_STATE_COMPLETE` | 更新成功 | 重启设备 |
| `OTA_STATE_ERROR` | 发生错误 | `ota_abort()` |
---
## 3. 回调详解
### 3.1 进度回调 — `ota_progress_cb_t`
```c
typedef void (*ota_progress_cb_t)(int percent, size_t bytes_written, size_t total_size);
```
**触发时机**:每次 `ota_write()` 成功返回后。
**参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| `percent` | `int` | 完成百分比 (0100)。若 `ota_begin()` 传入 `total_size=0`,则始终为 0 |
| `bytes_written` | `size_t` | 累计已写入字节数 |
| `total_size` | `size_t` | 声明的固件总字节数(即 `ota_begin` 的参数,0=未知) |
**用途**:驱动进度条 UI、串口日志输出。
**示例**
```c
static void on_progress(int percent, size_t written, size_t total)
{
printf("\rOTA: %d%% (%zu/%zu bytes)", percent, written, total);
// 或通过 LVGL 更新进度条:
// lv_bar_set_value(bar, percent, LV_ANIM_OFF);
}
```
---
### 3.2 状态变化回调 — `ota_state_cb_t`
```c
typedef void (*ota_state_cb_t)(ota_state_t old_state, ota_state_t new_state);
```
**触发时机**:状态机每次发生转换时。
**参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| `old_state` | `ota_state_t` | 变化前的状态 |
| `new_state` | `ota_state_t` | 变化后的状态 |
**典型用法**
| 场景 | 检查条件 | 操作 |
|------|---------|------|
| 更新成功 | `new_state == OTA_STATE_COMPLETE` | 自动重启 `esp_restart()` |
| 更新失败 | `new_state == OTA_STATE_ERROR` | 显示错误提示、记录日志 |
| 开始更新 | `new_state == OTA_STATE_IN_PROGRESS` | 禁用用户交互、显示进度 UI |
| 恢复空闲 | `new_state == OTA_STATE_IDLE` | 恢复用户交互 |
**示例**
```c
static void on_state_change(ota_state_t old, ota_state_t new_state)
{
ESP_LOGI("app", "OTA state: %d → %d", old, new_state);
if (new_state == OTA_STATE_COMPLETE) {
ESP_LOGI("app", "Update OK, restarting in 3s...");
vTaskDelay(pdMS_TO_TICKS(3000));
esp_restart();
} else if (new_state == OTA_STATE_ERROR) {
ESP_LOGE("app", "OTA failed, back to idle");
}
}
```
---
### 3.3 完成回调 — `ota_complete_cb_t`
```c
typedef void (*ota_complete_cb_t)(bool success, ota_error_t error_code);
```
**触发时机**
- `ota_end()` 处理完毕后
- `ota_abort()` 处理完毕后
**参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| `success` | `bool` | `true` = 更新成功,`false` = 失败或被中止 |
| `error_code` | `ota_error_t` | 失败时的错误码(`success=true` 时始终为 `OTA_ERR_NONE` |
**与状态变化回调的区别**
- `ota_state_cb_t` 关注「状态发生了什么变化」
- `ota_complete_cb_t` 关注「这次 OTA 最终成功了还是失败了」+ 失败原因
两者可同时使用,但建议至少注册一个来处理重启逻辑。
**示例**
```c
static void on_complete(bool success, ota_error_t err)
{
if (success) {
ESP_LOGI("app", "OTA complete, rebooting...");
esp_restart();
} else {
ESP_LOGW("app", "OTA failed, err=%d", err);
// 返回主界面
}
}
```
---
### 3.4 错误回调 — `ota_error_cb_t`
```c
typedef void (*ota_error_cb_t)(ota_error_t error_code, const char *message);
```
**触发时机**:每当发生可恢复或不致命的错误时(写入失败、校验失败等)。
**参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| `error_code` | `ota_error_t` | 错误码枚举值 |
| `message` | `const char *` | 人类可读的错误描述(静态字符串,无需释放) |
**错误码一览**
| 错误码 | 值 | 含义 |
|-------|---|------|
| `OTA_ERR_NONE` | 0 | 无错误 |
| `OTA_ERR_NOT_INITIALIZED` | 1 | 未调用 `ota_begin()` |
| `OTA_ERR_ALREADY_IN_PROGRESS` | 2 | 已有进行中的 OTA |
| `OTA_ERR_BEGIN_FAILED` | 3 | `ota_begin()` 失败(分区擦除失败等) |
| `OTA_ERR_WRITE_FAILED` | 4 | 写入 flash 失败 |
| `OTA_ERR_END_FAILED` | 5 | `ota_end()` 失败 |
| `OTA_ERR_ABORT_FAILED` | 6 | `ota_abort()` 失败 |
| `OTA_ERR_SIZE_MISMATCH` | 7 | 实写大小与声明不符 |
| `OTA_ERR_NO_OTA_PARTITION` | 8 | 分区表无可用 OTA 分区 |
| `OTA_ERR_STATE_ERROR` | 9 | 当前状态不允许该操作 |
| `OTA_ERR_VERIFY_FAILED` | 10 | 固件校验失败 |
---
## 4. 完整注册示例
```c
#include "ota_manager.h"
#include "esp_system.h"
/* ---- 回调实现 ---- */
static void on_progress(int pct, size_t written, size_t total)
{
printf("\rOTA %d%% (%zu/%zu)", pct, written, total);
}
static void on_state_change(ota_state_t old, ota_state_t new_state)
{
if (new_state == OTA_STATE_COMPLETE) {
printf("\nUpdate OK. Rebooting...\n");
esp_restart();
}
}
static void on_complete(bool ok, ota_error_t err)
{
if (!ok) printf("\nOTA failed: %d\n", err);
}
static void on_error(ota_error_t code, const char *msg)
{
ESP_LOGE("app", "OTA error %d: %s", code, msg);
}
/* ---- 注册(在 app_main 初始化阶段调用一次) ---- */
void ota_callbacks_init(void)
{
ota_set_progress_callback(on_progress);
ota_set_state_callback(on_state_change);
ota_set_complete_callback(on_complete);
ota_set_error_callback(on_error);
}
```
---
## 5. 与 LVGL UI 集成要点
### 5.1 FreeRTOS 线程安全
LVGL 对象操作**必须在 UI Task 中进行**。OTA 的调用方通常运行在另一个 Task 中,回调在调用线程中触发。因此:
```c
// ❌ 错误:在进度回调中直接操作 LVGL
static void on_progress(int pct, ...) {
lv_bar_set_value(ui_bar, pct, LV_ANIM_OFF); // 非 UI 线程!
}
// ✅ 正确:通过全局变量 + LVGL Timer 轮询
static volatile int g_ota_pct = 0;
static void on_progress(int pct, ...) {
g_ota_pct = pct;
}
// 在 LVGL Timer10Hz)中:
static void lv_timer_update_ota(lv_timer_t *timer) {
lv_bar_set_value(ui_bar, g_ota_pct, LV_ANIM_ON);
}
```
### 5.2 建议的 UI 状态映射
| OTA 状态 | UI 行为 |
|---------|--------|
| IDLE | 显示「固件升级」入口 |
| IN_PROGRESS | 显示进度条 + 「正在更新…」 |
| VERIFYING | 进度条 100%,文字改为「正在校验…」 |
| COMPLETE | 「更新成功,3 秒后重启」+ 倒计时 |
| ERROR | 红色警告 + 错误描述 + «重试»按钮 |
---
## 6. LED 反馈模式
OTA Manager 内置了 LED 反馈(通过 `hw_led_set()`),无需额外配置。
| 阶段 | LED 行为 | 含义 |
|------|---------|------|
| `begin` 调用后 | 常亮 | OTA 进行中 |
| 每次 `write` | 短暂熄灭→亮起 | 数据写入闪烁(约 2ms) |
| `end` 成功 | 常亮 | 更新完成,等待重启 |
| `end` 失败 / `abort` | 熄灭 | 回到空闲 |
---
## 7. 错误处理建议
```
ota_begin() 返回非 ESP_OK
├─ OTA_ERR_ALREADY_IN_PROGRESS → 先调 ota_abort(),再重试 begin
├─ OTA_ERR_NO_OTA_PARTITION → 检查分区表是否正确烧录
└─ OTA_ERR_BEGIN_FAILED → 检查 flash 是否正常
ota_write() 返回非 ESP_OK
└─ 调 ota_abort(),提示用户重试
ota_end() 返回非 ESP_OK
└─ 调 ota_abort(),固件未切换,设备仍运行旧版本
```
---
## 8. 平台兼容性
| 特性 | 要求 |
|------|------|
| ESP-IDF 版本 | ≥ 5.0(本模块使用 v5.1.x API |
| 分区表 | 必须含 `factory` + 至少一个 `ota_0` 类型分区 |
| Flash 大小 | ≥ 2MB(推荐 8MB,双 OTA 槽位) |
| `hw_led_set()` | 需由项目提供(当前在 `hw_init.h` 中定义) |
---
## 9. API 快速参考
| 函数 | 返回值 | 副作用 |
|------|-------|--------|
| `ota_begin(size)` | `esp_err_t` | 擦除 OTA 分区,LED 亮 |
| `ota_write(data, len)` | `esp_err_t` | 写入 flash,触发进度回调,LED 闪烁 |
| `ota_end()` | `esp_err_t` | 校验固件,切换启动分区,触发完成回调 |
| `ota_abort()` | `esp_err_t` | 放弃更新,LED 灭,回到 IDLE |
| `ota_get_state()` | `ota_state_t` | 无 |
| `ota_get_bytes_written()` | `size_t` | 无 |
| `ota_get_firmware_size()` | `size_t` | 无 |
| `ota_get_running_partition()` | `const esp_partition_t *` | 无 |
| `ota_get_update_partition()` | `const esp_partition_t *` | 无 |
| `ota_set_progress_callback(cb)` | `void` | 覆盖之前的回调 |
| `ota_set_complete_callback(cb)` | `void` | 覆盖之前的回调 |
| `ota_set_state_callback(cb)` | `void` | 覆盖之前的回调 |
| `ota_set_error_callback(cb)` | `void` | 覆盖之前的回调 |
| `ota_clear_callbacks()` | `void` | 注销所有回调 |
+1018
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
+65
View File
@@ -0,0 +1,65 @@
#ifndef HW_CONFIG_H
#define HW_CONFIG_H
/* ============================================================
* ESP32 DASS2 引脚宏定义
* 来源: pin.txt
* ============================================================ */
/* ---------- 电源 / 电池 ---------- */
#define BAT_VOLTAGE_GPIO GPIO_NUM_6 /* ADC1_CH5 读取电池电压 */
#define ADC_CTL_GPIO GPIO_NUM_7 /* 开关读取电压的MOS管 */
#define POW_CTL_GPIO GPIO_NUM_15 /* 开关各种外围芯片电源 */
#define USB_PLUGIN_DETECT_GPIO GPIO_NUM_18 /* 检测USB是否插入(高=插入) */
/* ---------- 按钮 ---------- */
#define BUTTON0_GPIO GPIO_NUM_19
#define BUTTON1_GPIO GPIO_NUM_20
/* 两个按钮状态编码:
* 00 = 无按键 ; 10 = 左键 ; 01 = 右键 ; 11 = 中键
*/
/* ---------- LED ---------- */
#define LED_CTL_GPIO GPIO_NUM_42 /* 状态指示灯 */
/* ---------- I2C ---------- */
#define I2C_SDA_GPIO GPIO_NUM_8
#define I2C_SCL_GPIO GPIO_NUM_9
#define I2C_PORT I2C_NUM_0
/* ---------- SPI (TFT 专用总线) ---------- */
#define SPI_MOSI_GPIO GPIO_NUM_11
#define SPI_MISO_GPIO GPIO_NUM_12
#define SPI_SCLK_GPIO GPIO_NUM_21
#define SPI_HOST SPI2_HOST
/* TFT 片选 / 数据命令 / 复位 / 背光 */
#define TFT_CS_GPIO GPIO_NUM_13
#define TFT_DC_GPIO GPIO_NUM_14
#define TFT_RST_GPIO GPIO_NUM_47
#define TFT_PWM_GPIO GPIO_NUM_10
/* ---------- SDMMC (4-bit 模式, 独立于 SPI) ---------- */
#define SDMMC_CLK_GPIO GPIO_NUM_5
#define SDMMC_CMD_GPIO GPIO_NUM_4
#define SDMMC_D0_GPIO GPIO_NUM_41
#define SDMMC_D1_GPIO GPIO_NUM_40
#define SDMMC_D2_GPIO GPIO_NUM_39
#define SDMMC_D3_GPIO GPIO_NUM_38
#define SDMMC_CD_GPIO GPIO_NUM_48 /* 卡检测, 有卡=低电平 */
/* ---------- SHT30 温湿度传感器 (I2C) ---------- */
#define SHT30_I2C_ADDR 0x44 /* ADDR 引脚接地 */
/* ---------- UART2 (Arduino Serial2, 用于温湿度数据输出) ---------- */
#define UART2_TX_GPIO 16
#define UART2_RX_GPIO 17
#define UART2_BAUD_RATE 115200
/* ---------- 显示参数 ---------- */
/* 按实际 TFT 型号修改 下面以 ST7789 240×240 为例 */
#define TFT_WIDTH 240
#define TFT_HEIGHT 240
#define TFT_MADCTL_ROTATION 0x00 /* MADCTL 无旋转(竖屏) */
#endif /* HW_CONFIG_H */
+44
View File
@@ -0,0 +1,44 @@
#ifndef HW_INIT_H
#define HW_INIT_H
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ---------- 基本 GPIO ---------- */
esp_err_t hw_gpio_init(void);
/* ---------- 电源控制 ---------- */
esp_err_t hw_power_init(void); /* 开启外围电源 (POW_CTL) */
esp_err_t hw_battery_adc_init(void); /* 初始化电池电压 ADC */
uint32_t hw_battery_read_mv(void); /* 读取电池电压 (mV),内部自动开关MOS */
/* ---------- 按钮 ---------- */
esp_err_t hw_button_init(void);
uint8_t hw_button_read(void); /* 返回编码: 0=none, 1=left, 2=right, 3=mid */
/* ---------- LED ---------- */
esp_err_t hw_led_init(void);
void hw_led_set(bool on);
/* ---------- I2C ---------- */
esp_err_t hw_i2c_init(void);
/* ---------- SPI ---------- */
esp_err_t hw_spi_init(void);
/* ---------- TFT 背光 PWM ---------- */
esp_err_t hw_tft_backlight_init(void);
void hw_tft_backlight_set(uint8_t duty); /* 0100 */
/* ---------- 一键初始化所有外设 ---------- */
esp_err_t hw_all_init(void);
#ifdef __cplusplus
}
#endif
#endif /* HW_INIT_H */
+784
View File
@@ -0,0 +1,784 @@
/**
* @file lv_conf.h
* Configuration file for v8.4.0
*/
/*
* Copy this file as `lv_conf.h`
* 1. simply next to the `lvgl` folder
* 2. or any other places and
* - define `LV_CONF_INCLUDE_SIMPLE`
* - add the path as include path
*/
/* clang-format off */
#if 1 /*Set it to "1" to enable content*/
#ifndef LV_CONF_H
#define LV_CONF_H
#include <stdint.h>
/*====================
COLOR SETTINGS
*====================*/
/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
#define LV_COLOR_DEPTH 16
/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/
#define LV_COLOR_16_SWAP 1
/*Enable features to draw on transparent background.
*It's required if opa, and transform_* style properties are used.
*Can be also used if the UI is above another layer, e.g. an OSD menu or video player.*/
#define LV_COLOR_SCREEN_TRANSP 0
/* Adjust color mix functions rounding. GPUs might calculate color mix (blending) differently.
* 0: round down, 64: round up from x.75, 128: round up from half, 192: round up from x.25, 254: round up */
#define LV_COLOR_MIX_ROUND_OFS 0
/*Images pixels with this color will not be drawn if they are chroma keyed)*/
#define LV_COLOR_CHROMA_KEY lv_color_hex(0x00ff00) /*pure green*/
/*=========================
MEMORY SETTINGS
*=========================*/
/*1: use custom malloc/free, 0: use the built-in `lv_mem_alloc()` and `lv_mem_free()`*/
#define LV_MEM_CUSTOM 0
#if LV_MEM_CUSTOM == 0
/*Size of the memory available for `lv_mem_alloc()` in bytes (>= 2kB)*/
#define LV_MEM_SIZE (64U * 1024U) /*[bytes]*/
/*Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too.*/
#define LV_MEM_ADR 0 /*0: unused*/
/*Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc*/
#if LV_MEM_ADR == 0
#undef LV_MEM_POOL_INCLUDE
#undef LV_MEM_POOL_ALLOC
#endif
#else /*LV_MEM_CUSTOM*/
#define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /*Header for the dynamic memory function*/
#define LV_MEM_CUSTOM_ALLOC malloc
#define LV_MEM_CUSTOM_FREE free
#define LV_MEM_CUSTOM_REALLOC realloc
#endif /*LV_MEM_CUSTOM*/
/*Number of the intermediate memory buffer used during rendering and other internal processing mechanisms.
*You will see an error log message if there wasn't enough buffers. */
#define LV_MEM_BUF_MAX_NUM 16
/*Use the standard `memcpy` and `memset` instead of LVGL's own functions. (Might or might not be faster).*/
#define LV_MEMCPY_MEMSET_STD 0
/*====================
HAL SETTINGS
*====================*/
/*Default display refresh period. LVG will redraw changed areas with this period time*/
#define LV_DISP_DEF_REFR_PERIOD 30 /*[ms]*/
/*Input device read period in milliseconds*/
#define LV_INDEV_DEF_READ_PERIOD 30 /*[ms]*/
/*Use a custom tick source that tells the elapsed time in milliseconds.
*It removes the need to manually update the tick with `lv_tick_inc()`)*/
#define LV_TICK_CUSTOM 1
#if LV_TICK_CUSTOM
// #define LV_TICK_CUSTOM_INCLUDE "Arduino.h" /*Header for the system time function*/
// #define LV_TICK_CUSTOM_SYS_TIME_EXPR (millis()) /*Expression evaluating to current system time in ms*/
/*If using lvgl as ESP32 component*/
#define LV_TICK_CUSTOM_INCLUDE "esp_timer.h"
#define LV_TICK_CUSTOM_SYS_TIME_EXPR ((esp_timer_get_time() / 1000LL))
#endif /*LV_TICK_CUSTOM*/
/*Default Dot Per Inch. Used to initialize default sizes such as widgets sized, style paddings.
*(Not so important, you can adjust it to modify default sizes and spaces)*/
#define LV_DPI_DEF 130 /*[px/inch]*/
/*=======================
* FEATURE CONFIGURATION
*=======================*/
/*-------------
* Drawing
*-----------*/
/*Enable complex draw engine.
*Required to draw shadow, gradient, rounded corners, circles, arc, skew lines, image transformations or any masks*/
#define LV_DRAW_COMPLEX 1
#if LV_DRAW_COMPLEX != 0
/*Allow buffering some shadow calculation.
*LV_SHADOW_CACHE_SIZE is the max. shadow size to buffer, where shadow size is `shadow_width + radius`
*Caching has LV_SHADOW_CACHE_SIZE^2 RAM cost*/
#define LV_SHADOW_CACHE_SIZE 0
/* Set number of maximally cached circle data.
* The circumference of 1/4 circle are saved for anti-aliasing
* radius * 4 bytes are used per circle (the most often used radiuses are saved)
* 0: to disable caching */
#define LV_CIRCLE_CACHE_SIZE 4
#endif /*LV_DRAW_COMPLEX*/
/**
* "Simple layers" are used when a widget has `style_opa < 255` to buffer the widget into a layer
* and blend it as an image with the given opacity.
* Note that `bg_opa`, `text_opa` etc don't require buffering into layer)
* The widget can be buffered in smaller chunks to avoid using large buffers.
*
* - LV_LAYER_SIMPLE_BUF_SIZE: [bytes] the optimal target buffer size. LVGL will try to allocate it
* - LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE: [bytes] used if `LV_LAYER_SIMPLE_BUF_SIZE` couldn't be allocated.
*
* Both buffer sizes are in bytes.
* "Transformed layers" (where transform_angle/zoom properties are used) use larger buffers
* and can't be drawn in chunks. So these settings affects only widgets with opacity.
*/
#define LV_LAYER_SIMPLE_BUF_SIZE (24 * 1024)
#define LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE (3 * 1024)
/*Default image cache size. Image caching keeps the images opened.
*If only the built-in image formats are used there is no real advantage of caching. (I.e. if no new image decoder is added)
*With complex image decoders (e.g. PNG or JPG) caching can save the continuous open/decode of images.
*However the opened images might consume additional RAM.
*0: to disable caching*/
#define LV_IMG_CACHE_DEF_SIZE 0
/*Number of stops allowed per gradient. Increase this to allow more stops.
*This adds (sizeof(lv_color_t) + 1) bytes per additional stop*/
#define LV_GRADIENT_MAX_STOPS 2
/*Default gradient buffer size.
*When LVGL calculates the gradient "maps" it can save them into a cache to avoid calculating them again.
*LV_GRAD_CACHE_DEF_SIZE sets the size of this cache in bytes.
*If the cache is too small the map will be allocated only while it's required for the drawing.
*0 mean no caching.*/
#define LV_GRAD_CACHE_DEF_SIZE 0
/*Allow dithering the gradients (to achieve visual smooth color gradients on limited color depth display)
*LV_DITHER_GRADIENT implies allocating one or two more lines of the object's rendering surface
*The increase in memory consumption is (32 bits * object width) plus 24 bits * object width if using error diffusion */
#define LV_DITHER_GRADIENT 0
#if LV_DITHER_GRADIENT
/*Add support for error diffusion dithering.
*Error diffusion dithering gets a much better visual result, but implies more CPU consumption and memory when drawing.
*The increase in memory consumption is (24 bits * object's width)*/
#define LV_DITHER_ERROR_DIFFUSION 0
#endif
/*Maximum buffer size to allocate for rotation.
*Only used if software rotation is enabled in the display driver.*/
#define LV_DISP_ROT_MAX_BUF (10*1024)
/*-------------
* GPU
*-----------*/
/*Use Arm's 2D acceleration library Arm-2D */
#define LV_USE_GPU_ARM2D 0
/*Use STM32's DMA2D (aka Chrom Art) GPU*/
#define LV_USE_GPU_STM32_DMA2D 0
#if LV_USE_GPU_STM32_DMA2D
/*Must be defined to include path of CMSIS header of target processor
e.g. "stm32f7xx.h" or "stm32f4xx.h"*/
#define LV_GPU_DMA2D_CMSIS_INCLUDE
#endif
/*Enable RA6M3 G2D GPU*/
#define LV_USE_GPU_RA6M3_G2D 0
#if LV_USE_GPU_RA6M3_G2D
/*include path of target processor
e.g. "hal_data.h"*/
#define LV_GPU_RA6M3_G2D_INCLUDE "hal_data.h"
#endif
/*Use SWM341's DMA2D GPU*/
#define LV_USE_GPU_SWM341_DMA2D 0
#if LV_USE_GPU_SWM341_DMA2D
#define LV_GPU_SWM341_DMA2D_INCLUDE "SWM341.h"
#endif
/*Use NXP's PXP GPU iMX RTxxx platforms*/
#define LV_USE_GPU_NXP_PXP 0
#if LV_USE_GPU_NXP_PXP
/*1: Add default bare metal and FreeRTOS interrupt handling routines for PXP (lv_gpu_nxp_pxp_osa.c)
* and call lv_gpu_nxp_pxp_init() automatically during lv_init(). Note that symbol SDK_OS_FREE_RTOS
* has to be defined in order to use FreeRTOS OSA, otherwise bare-metal implementation is selected.
*0: lv_gpu_nxp_pxp_init() has to be called manually before lv_init()
*/
#define LV_USE_GPU_NXP_PXP_AUTO_INIT 0
#endif
/*Use NXP's VG-Lite GPU iMX RTxxx platforms*/
#define LV_USE_GPU_NXP_VG_LITE 0
/*Use SDL renderer API*/
#define LV_USE_GPU_SDL 0
#if LV_USE_GPU_SDL
#define LV_GPU_SDL_INCLUDE_PATH <SDL2/SDL.h>
/*Texture cache size, 8MB by default*/
#define LV_GPU_SDL_LRU_SIZE (1024 * 1024 * 8)
/*Custom blend mode for mask drawing, disable if you need to link with older SDL2 lib*/
#define LV_GPU_SDL_CUSTOM_BLEND_MODE (SDL_VERSION_ATLEAST(2, 0, 6))
#endif
/*-------------
* Logging
*-----------*/
/*Enable the log module*/
#define LV_USE_LOG 0
#if LV_USE_LOG
/*How important log should be added:
*LV_LOG_LEVEL_TRACE A lot of logs to give detailed information
*LV_LOG_LEVEL_INFO Log important events
*LV_LOG_LEVEL_WARN Log if something unwanted happened but didn't cause a problem
*LV_LOG_LEVEL_ERROR Only critical issue, when the system may fail
*LV_LOG_LEVEL_USER Only logs added by the user
*LV_LOG_LEVEL_NONE Do not log anything*/
#define LV_LOG_LEVEL LV_LOG_LEVEL_WARN
/*1: Print the log with 'printf';
*0: User need to register a callback with `lv_log_register_print_cb()`*/
#define LV_LOG_PRINTF 0
/*Enable/disable LV_LOG_TRACE in modules that produces a huge number of logs*/
#define LV_LOG_TRACE_MEM 1
#define LV_LOG_TRACE_TIMER 1
#define LV_LOG_TRACE_INDEV 1
#define LV_LOG_TRACE_DISP_REFR 1
#define LV_LOG_TRACE_EVENT 1
#define LV_LOG_TRACE_OBJ_CREATE 1
#define LV_LOG_TRACE_LAYOUT 1
#define LV_LOG_TRACE_ANIM 1
#endif /*LV_USE_LOG*/
/*-------------
* Asserts
*-----------*/
/*Enable asserts if an operation is failed or an invalid data is found.
*If LV_USE_LOG is enabled an error message will be printed on failure*/
#define LV_USE_ASSERT_NULL 1 /*Check if the parameter is NULL. (Very fast, recommended)*/
#define LV_USE_ASSERT_MALLOC 1 /*Checks is the memory is successfully allocated or no. (Very fast, recommended)*/
#define LV_USE_ASSERT_STYLE 0 /*Check if the styles are properly initialized. (Very fast, recommended)*/
#define LV_USE_ASSERT_MEM_INTEGRITY 0 /*Check the integrity of `lv_mem` after critical operations. (Slow)*/
#define LV_USE_ASSERT_OBJ 0 /*Check the object's type and existence (e.g. not deleted). (Slow)*/
/*Add a custom handler when assert happens e.g. to restart the MCU*/
#define LV_ASSERT_HANDLER_INCLUDE <stdint.h>
#define LV_ASSERT_HANDLER while(1); /*Halt by default*/
/*-------------
* Others
*-----------*/
/*1: Show CPU usage and FPS count*/
#define LV_USE_PERF_MONITOR 0
#if LV_USE_PERF_MONITOR
#define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT
#endif
/*1: Show the used memory and the memory fragmentation
* Requires LV_MEM_CUSTOM = 0*/
#define LV_USE_MEM_MONITOR 0
#if LV_USE_MEM_MONITOR
#define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT
#endif
/*1: Draw random colored rectangles over the redrawn areas*/
#define LV_USE_REFR_DEBUG 0
/*Change the built in (v)snprintf functions*/
#define LV_SPRINTF_CUSTOM 0
#if LV_SPRINTF_CUSTOM
#define LV_SPRINTF_INCLUDE <stdio.h>
#define lv_snprintf snprintf
#define lv_vsnprintf vsnprintf
#else /*LV_SPRINTF_CUSTOM*/
#define LV_SPRINTF_USE_FLOAT 0
#endif /*LV_SPRINTF_CUSTOM*/
#define LV_USE_USER_DATA 1
/*Garbage Collector settings
*Used if lvgl is bound to higher level language and the memory is managed by that language*/
#define LV_ENABLE_GC 0
#if LV_ENABLE_GC != 0
#define LV_GC_INCLUDE "gc.h" /*Include Garbage Collector related things*/
#endif /*LV_ENABLE_GC*/
/*=====================
* COMPILER SETTINGS
*====================*/
/*For big endian systems set to 1*/
#define LV_BIG_ENDIAN_SYSTEM 0
/*Define a custom attribute to `lv_tick_inc` function*/
#define LV_ATTRIBUTE_TICK_INC
/*Define a custom attribute to `lv_timer_handler` function*/
#define LV_ATTRIBUTE_TIMER_HANDLER
/*Define a custom attribute to `lv_disp_flush_ready` function*/
#define LV_ATTRIBUTE_FLUSH_READY
/*Required alignment size for buffers*/
#define LV_ATTRIBUTE_MEM_ALIGN_SIZE 1
/*Will be added where memories needs to be aligned (with -Os data might not be aligned to boundary by default).
* E.g. __attribute__((aligned(4)))*/
#define LV_ATTRIBUTE_MEM_ALIGN
/*Attribute to mark large constant arrays for example font's bitmaps*/
#define LV_ATTRIBUTE_LARGE_CONST
/*Compiler prefix for a big array declaration in RAM*/
#define LV_ATTRIBUTE_LARGE_RAM_ARRAY
/*Place performance critical functions into a faster memory (e.g RAM)*/
#define LV_ATTRIBUTE_FAST_MEM
/*Prefix variables that are used in GPU accelerated operations, often these need to be placed in RAM sections that are DMA accessible*/
#define LV_ATTRIBUTE_DMA
/*Export integer constant to binding. This macro is used with constants in the form of LV_<CONST> that
*should also appear on LVGL binding API such as Micropython.*/
#define LV_EXPORT_CONST_INT(int_value) struct _silence_gcc_warning /*The default value just prevents GCC warning*/
/*Extend the default -32k..32k coordinate range to -4M..4M by using int32_t for coordinates instead of int16_t*/
#define LV_USE_LARGE_COORD 0
/*==================
* FONT USAGE
*===================*/
/*Montserrat fonts with ASCII range and some symbols using bpp = 4
*https://fonts.google.com/specimen/Montserrat*/
#define LV_FONT_MONTSERRAT_8 0
#define LV_FONT_MONTSERRAT_10 0
#define LV_FONT_MONTSERRAT_12 0
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_16 1
#define LV_FONT_MONTSERRAT_18 0
#define LV_FONT_MONTSERRAT_20 0
#define LV_FONT_MONTSERRAT_22 0
#define LV_FONT_MONTSERRAT_24 1
#define LV_FONT_MONTSERRAT_26 0
#define LV_FONT_MONTSERRAT_28 0
#define LV_FONT_MONTSERRAT_30 0
#define LV_FONT_MONTSERRAT_32 0
#define LV_FONT_MONTSERRAT_34 0
#define LV_FONT_MONTSERRAT_36 0
#define LV_FONT_MONTSERRAT_38 0
#define LV_FONT_MONTSERRAT_40 0
#define LV_FONT_MONTSERRAT_42 0
#define LV_FONT_MONTSERRAT_44 0
#define LV_FONT_MONTSERRAT_46 0
#define LV_FONT_MONTSERRAT_48 0
/*Demonstrate special features*/
#define LV_FONT_MONTSERRAT_12_SUBPX 0
#define LV_FONT_MONTSERRAT_28_COMPRESSED 0 /*bpp = 3*/
#define LV_FONT_DEJAVU_16_PERSIAN_HEBREW 0 /*Hebrew, Arabic, Persian letters and all their forms*/
#define LV_FONT_SIMSUN_16_CJK 0 /*1000 most common CJK radicals*/
/*Pixel perfect monospace fonts*/
#define LV_FONT_UNSCII_8 0
#define LV_FONT_UNSCII_16 0
/*Optionally declare custom fonts here.
*You can use these fonts as default font too and they will be available globally.
*E.g. #define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(my_font_1) LV_FONT_DECLARE(my_font_2)*/
#define LV_FONT_CUSTOM_DECLARE
/*Always set a default font*/
#define LV_FONT_DEFAULT &lv_font_montserrat_16
/*Enable handling large font and/or fonts with a lot of characters.
*The limit depends on the font size, font face and bpp.
*Compiler error will be triggered if a font needs it.*/
#define LV_FONT_FMT_TXT_LARGE 0
/*Enables/disables support for compressed fonts.*/
#define LV_USE_FONT_COMPRESSED 0
/*Enable subpixel rendering*/
#define LV_USE_FONT_SUBPX 0
#if LV_USE_FONT_SUBPX
/*Set the pixel order of the display. Physical order of RGB channels. Doesn't matter with "normal" fonts.*/
#define LV_FONT_SUBPX_BGR 0 /*0: RGB; 1:BGR order*/
#endif
/*Enable drawing placeholders when glyph dsc is not found*/
#define LV_USE_FONT_PLACEHOLDER 1
/*=================
* TEXT SETTINGS
*=================*/
/**
* Select a character encoding for strings.
* Your IDE or editor should have the same character encoding
* - LV_TXT_ENC_UTF8
* - LV_TXT_ENC_ASCII
*/
#define LV_TXT_ENC LV_TXT_ENC_UTF8
/*Can break (wrap) texts on these chars*/
#define LV_TXT_BREAK_CHARS " ,.;:-_"
/*If a word is at least this long, will break wherever "prettiest"
*To disable, set to a value <= 0*/
#define LV_TXT_LINE_BREAK_LONG_LEN 0
/*Minimum number of characters in a long word to put on a line before a break.
*Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/
#define LV_TXT_LINE_BREAK_LONG_PRE_MIN_LEN 3
/*Minimum number of characters in a long word to put on a line after a break.
*Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/
#define LV_TXT_LINE_BREAK_LONG_POST_MIN_LEN 3
/*The control character to use for signalling text recoloring.*/
#define LV_TXT_COLOR_CMD "#"
/*Support bidirectional texts. Allows mixing Left-to-Right and Right-to-Left texts.
*The direction will be processed according to the Unicode Bidirectional Algorithm:
*https://www.w3.org/International/articles/inline-bidi-markup/uba-basics*/
#define LV_USE_BIDI 0
#if LV_USE_BIDI
/*Set the default direction. Supported values:
*`LV_BASE_DIR_LTR` Left-to-Right
*`LV_BASE_DIR_RTL` Right-to-Left
*`LV_BASE_DIR_AUTO` detect texts base direction*/
#define LV_BIDI_BASE_DIR_DEF LV_BASE_DIR_AUTO
#endif
/*Enable Arabic/Persian processing
*In these languages characters should be replaced with an other form based on their position in the text*/
#define LV_USE_ARABIC_PERSIAN_CHARS 0
/*==================
* WIDGET USAGE
*================*/
/*Documentation of the widgets: https://docs.lvgl.io/latest/en/html/widgets/index.html*/
#define LV_USE_ARC 1
#define LV_USE_BAR 1
#define LV_USE_BTN 1
#define LV_USE_BTNMATRIX 1
#define LV_USE_CANVAS 1
#define LV_USE_CHECKBOX 1
#define LV_USE_DROPDOWN 1 /*Requires: lv_label*/
#define LV_USE_IMG 1 /*Requires: lv_label*/
#define LV_USE_LABEL 1
#if LV_USE_LABEL
#define LV_LABEL_TEXT_SELECTION 1 /*Enable selecting text of the label*/
#define LV_LABEL_LONG_TXT_HINT 1 /*Store some extra info in labels to speed up drawing of very long texts*/
#endif
#define LV_USE_LINE 1
#define LV_USE_ROLLER 1 /*Requires: lv_label*/
#if LV_USE_ROLLER
#define LV_ROLLER_INF_PAGES 7 /*Number of extra "pages" when the roller is infinite*/
#endif
#define LV_USE_SLIDER 1 /*Requires: lv_bar*/
#define LV_USE_SWITCH 1
#define LV_USE_TEXTAREA 1 /*Requires: lv_label*/
#if LV_USE_TEXTAREA != 0
#define LV_TEXTAREA_DEF_PWD_SHOW_TIME 1500 /*ms*/
#endif
#define LV_USE_TABLE 1
/*==================
* EXTRA COMPONENTS
*==================*/
/*-----------
* Widgets
*----------*/
#define LV_USE_ANIMIMG 1
#define LV_USE_CALENDAR 1
#if LV_USE_CALENDAR
#define LV_CALENDAR_WEEK_STARTS_MONDAY 0
#if LV_CALENDAR_WEEK_STARTS_MONDAY
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}
#else
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}
#endif
#define LV_CALENDAR_DEFAULT_MONTH_NAMES {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
#define LV_USE_CALENDAR_HEADER_ARROW 1
#define LV_USE_CALENDAR_HEADER_DROPDOWN 1
#endif /*LV_USE_CALENDAR*/
#define LV_USE_CHART 1
#define LV_USE_COLORWHEEL 1
#define LV_USE_IMGBTN 1
#define LV_USE_KEYBOARD 1
#define LV_USE_LED 1
#define LV_USE_LIST 1
#define LV_USE_MENU 1
#define LV_USE_METER 1
#define LV_USE_MSGBOX 1
#define LV_USE_SPAN 1
#if LV_USE_SPAN
/*A line text can contain maximum num of span descriptor */
#define LV_SPAN_SNIPPET_STACK_SIZE 64
#endif
#define LV_USE_SPINBOX 1
#define LV_USE_SPINNER 1
#define LV_USE_TABVIEW 1
#define LV_USE_TILEVIEW 1
#define LV_USE_WIN 1
/*-----------
* Themes
*----------*/
/*A simple, impressive and very complete theme*/
#define LV_USE_THEME_DEFAULT 1
#if LV_USE_THEME_DEFAULT
/*0: Light mode; 1: Dark mode*/
#define LV_THEME_DEFAULT_DARK 0
/*1: Enable grow on press*/
#define LV_THEME_DEFAULT_GROW 1
/*Default transition time in [ms]*/
#define LV_THEME_DEFAULT_TRANSITION_TIME 80
#endif /*LV_USE_THEME_DEFAULT*/
/*A very simple theme that is a good starting point for a custom theme*/
#define LV_USE_THEME_BASIC 1
/*A theme designed for monochrome displays*/
#define LV_USE_THEME_MONO 1
/*-----------
* Layouts
*----------*/
/*A layout similar to Flexbox in CSS.*/
#define LV_USE_FLEX 1
/*A layout similar to Grid in CSS.*/
#define LV_USE_GRID 1
/*---------------------
* 3rd party libraries
*--------------------*/
/*File system interfaces for common APIs */
/*API for fopen, fread, etc*/
#define LV_USE_FS_STDIO 0
#if LV_USE_FS_STDIO
#define LV_FS_STDIO_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_STDIO_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_STDIO_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for open, read, etc*/
#define LV_USE_FS_POSIX 0
#if LV_USE_FS_POSIX
#define LV_FS_POSIX_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_POSIX_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_POSIX_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for CreateFile, ReadFile, etc*/
#define LV_USE_FS_WIN32 0
#if LV_USE_FS_WIN32
#define LV_FS_WIN32_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_WIN32_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
#define LV_FS_WIN32_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for FATFS (needs to be added separately). Uses f_open, f_read, etc*/
#define LV_USE_FS_FATFS 0
#if LV_USE_FS_FATFS
#define LV_FS_FATFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_FATFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*API for LittleFS (library needs to be added separately). Uses lfs_file_open, lfs_file_read, etc*/
#define LV_USE_FS_LITTLEFS 0
#if LV_USE_FS_LITTLEFS
#define LV_FS_LITTLEFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
#define LV_FS_LITTLEFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
#endif
/*PNG decoder library*/
#define LV_USE_PNG 0
/*BMP decoder library*/
#define LV_USE_BMP 0
/* JPG + split JPG decoder library.
* Split JPG is a custom format optimized for embedded systems. */
#define LV_USE_SJPG 0
/*GIF decoder library*/
#define LV_USE_GIF 0
/*QR code library*/
#define LV_USE_QRCODE 0
/*FreeType library*/
#define LV_USE_FREETYPE 0
#if LV_USE_FREETYPE
/*Memory used by FreeType to cache characters [bytes] (-1: no caching)*/
#define LV_FREETYPE_CACHE_SIZE (16 * 1024)
#if LV_FREETYPE_CACHE_SIZE >= 0
/* 1: bitmap cache use the sbit cache, 0:bitmap cache use the image cache. */
/* sbit cache:it is much more memory efficient for small bitmaps(font size < 256) */
/* if font size >= 256, must be configured as image cache */
#define LV_FREETYPE_SBIT_CACHE 0
/* Maximum number of opened FT_Face/FT_Size objects managed by this cache instance. */
/* (0:use system defaults) */
#define LV_FREETYPE_CACHE_FT_FACES 0
#define LV_FREETYPE_CACHE_FT_SIZES 0
#endif
#endif
/*Tiny TTF library*/
#define LV_USE_TINY_TTF 0
#if LV_USE_TINY_TTF
/*Load TTF data from files*/
#define LV_TINY_TTF_FILE_SUPPORT 0
#endif
/*Rlottie library*/
#define LV_USE_RLOTTIE 0
/*FFmpeg library for image decoding and playing videos
*Supports all major image formats so do not enable other image decoder with it*/
#define LV_USE_FFMPEG 0
#if LV_USE_FFMPEG
/*Dump input information to stderr*/
#define LV_FFMPEG_DUMP_FORMAT 0
#endif
/*-----------
* Others
*----------*/
/*1: Enable API to take snapshot for object*/
#define LV_USE_SNAPSHOT 0
/*1: Enable Monkey test*/
#define LV_USE_MONKEY 0
/*1: Enable grid navigation*/
#define LV_USE_GRIDNAV 0
/*1: Enable lv_obj fragment*/
#define LV_USE_FRAGMENT 0
/*1: Support using images as font in label or span widgets */
#define LV_USE_IMGFONT 0
/*1: Enable a published subscriber based messaging system */
#define LV_USE_MSG 0
/*1: Enable Pinyin input method*/
/*Requires: lv_keyboard*/
#define LV_USE_IME_PINYIN 0
#if LV_USE_IME_PINYIN
/*1: Use default thesaurus*/
/*If you do not use the default thesaurus, be sure to use `lv_ime_pinyin` after setting the thesauruss*/
#define LV_IME_PINYIN_USE_DEFAULT_DICT 1
/*Set the maximum number of candidate panels that can be displayed*/
/*This needs to be adjusted according to the size of the screen*/
#define LV_IME_PINYIN_CAND_TEXT_NUM 6
/*Use 9 key input(k9)*/
#define LV_IME_PINYIN_USE_K9_MODE 1
#if LV_IME_PINYIN_USE_K9_MODE == 1
#define LV_IME_PINYIN_K9_CAND_TEXT_NUM 3
#endif // LV_IME_PINYIN_USE_K9_MODE
#endif
/*==================
* EXAMPLES
*==================*/
/*Enable the examples to be built with the library*/
#define LV_BUILD_EXAMPLES 1
/*===================
* DEMO USAGE
====================*/
/*Show some widget. It might be required to increase `LV_MEM_SIZE` */
#define LV_USE_DEMO_WIDGETS 0
#if LV_USE_DEMO_WIDGETS
#define LV_DEMO_WIDGETS_SLIDESHOW 0
#endif
/*Demonstrate the usage of encoder and keyboard*/
#define LV_USE_DEMO_KEYPAD_AND_ENCODER 0
/*Benchmark your system*/
#define LV_USE_DEMO_BENCHMARK 0
#if LV_USE_DEMO_BENCHMARK
/*Use RGB565A8 images with 16 bit color depth instead of ARGB8565*/
#define LV_DEMO_BENCHMARK_RGB565A8 0
#endif
/*Stress test for LVGL*/
#define LV_USE_DEMO_STRESS 0
/*Music player demo*/
#define LV_USE_DEMO_MUSIC 0
#if LV_USE_DEMO_MUSIC
#define LV_DEMO_MUSIC_SQUARE 0
#define LV_DEMO_MUSIC_LANDSCAPE 0
#define LV_DEMO_MUSIC_ROUND 0
#define LV_DEMO_MUSIC_LARGE 0
#define LV_DEMO_MUSIC_AUTO_PLAY 0
#endif
/*--END OF LV_CONF_H--*/
#endif /*LV_CONF_H*/
#endif /*End of "Content enable"*/
+26
View File
@@ -0,0 +1,26 @@
#ifndef LVGL_DEMO_H
#define LVGL_DEMO_H
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief 初始化 LVGL + TFT 显示驱动,并启动 Demo UI
*
* 内部步骤:
* 1. lv_init()
* 2. 添加 SPI TFT 设备 (ST7789 / ST7735)
* 3. 注册 LVGL flush 回调
* 4. 创建 Demo UI 对象
* 5. 启动周期性 LVGL 刷新 Task
*/
esp_err_t lvgl_demo_start(void);
#ifdef __cplusplus
}
#endif
#endif /* LVGL_DEMO_H */
+209
View File
@@ -0,0 +1,209 @@
/**
* @file ota_manager.h
* @brief 固件 OTAOver-The-Air)底层管理器
*
* 提供与传输层无关的固件更新基础设施。
* 上层(USB / 串口 / 蓝牙 / SD 卡 / HTTP)只需:
* 1. 调用 ota_begin(total_size) 开始更新
* 2. 循环调用 ota_write(chunk, len) 写入固件数据
* 3. 调用 ota_end() 完成更新(验证 + 切换启动分区)
* 4. 异常时调用 ota_abort() 放弃更新
*
* 回调机制:
* 注册回调后,在 OTA 各阶段自动触发,用于 UI 更新 / 日志 / 错误处理。
*
* 框架: ESP-IDF 5.1.x
* 依赖: hw_led_set()(状态 LED 反馈)
*/
#ifndef OTA_MANAGER_H
#define OTA_MANAGER_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "esp_err.h"
#include "esp_partition.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ================================================================
* 类型定义
* ================================================================ */
/** OTA 状态机 */
typedef enum {
OTA_STATE_IDLE, /**< 空闲,等待开始 */
OTA_STATE_IN_PROGRESS, /**< 固件写入中 */
OTA_STATE_VERIFYING, /**< 固件校验中(end 阶段) */
OTA_STATE_COMPLETE, /**< 更新成功,准备重启 */
OTA_STATE_ERROR, /**< 发生错误,需 abort 或重试 */
} ota_state_t;
/** OTA 错误码(扩展 esp_err_t,负值为 ESP-IDF 错误,正值为自定义错误) */
typedef enum {
OTA_ERR_NONE = 0, /**< 无错误 */
OTA_ERR_NOT_INITIALIZED = 1, /**< 未调用 begin */
OTA_ERR_ALREADY_IN_PROGRESS = 2, /**< 已有进行中的 OTA */
OTA_ERR_BEGIN_FAILED = 3, /**< begin 阶段失败 */
OTA_ERR_WRITE_FAILED = 4, /**< 写入 flash 失败 */
OTA_ERR_END_FAILED = 5, /**< end 阶段失败 */
OTA_ERR_ABORT_FAILED = 6, /**< abort 失败 */
OTA_ERR_SIZE_MISMATCH = 7, /**< 写入长度与声明的 total 不符 */
OTA_ERR_NO_OTA_PARTITION = 8, /**< 无可用 OTA 分区 */
OTA_ERR_STATE_ERROR = 9, /**< 当前状态不允许该操作 */
OTA_ERR_VERIFY_FAILED = 10, /**< 固件校验失败 */
} ota_error_t;
/* ================================================================
* 回调函数类型
* ================================================================ */
/**
* @brief 进度回调
* @param percent 完成百分比 (0100)
* @param bytes_written 已写入字节数
* @param total_size 固件总字节数
*
* 调用频率: 每完成一个 ota_write() 调用后触发一次(约 1–4KB 一次)。
* 上层可在此回调中更新进度条或打印日志。
*/
typedef void (*ota_progress_cb_t)(int percent, size_t bytes_written, size_t total_size);
/**
* @brief 完成回调
* @param success true=更新成功, false=更新失败
* @param error_code 失败时的错误码(success 为 true 时无效)
*
* 调用时机: ota_end() 处理完毕后触发。
* 若 success=true,设备应在适当时机重启以加载新固件。
*/
typedef void (*ota_complete_cb_t)(bool success, ota_error_t error_code);
/**
* @brief 状态变化回调
* @param old_state 变化前的状态
* @param new_state 变化后的状态
*
* 每次 OTA 状态机发生转换时触发。
* 上层可在 OTA_STATE_COMPLETE 时自动重启,或在 OTA_STATE_ERROR 时告警。
*/
typedef void (*ota_state_cb_t)(ota_state_t old_state, ota_state_t new_state);
/**
* @brief 错误回调(颗粒度更细)
* @param error_code 错误码
* @param message 可读错误描述
*
* 在 ota_write 失败、end 失败等场景触发,先于状态变化回调。
* 可用于日志记录、错误提示等。
*/
typedef void (*ota_error_cb_t)(ota_error_t error_code, const char *message);
/* ================================================================
* 回调注册
* ================================================================ */
/** 注册进度回调(最多一个,后注册的覆盖前面的) */
void ota_set_progress_callback(ota_progress_cb_t cb);
/** 注册完成回调 */
void ota_set_complete_callback(ota_complete_cb_t cb);
/** 注册状态变化回调 */
void ota_set_state_callback(ota_state_cb_t cb);
/** 注册错误回调 */
void ota_set_error_callback(ota_error_cb_t cb);
/** 一次性注销所有回调 */
void ota_clear_callbacks(void);
/* ================================================================
* 底层 OTA API
*
* 调用顺序:
* 正常流程: ota_begin() → ota_write()×N → ota_end() → 重启设备
* 错误处理: ota_begin() → ota_write()×N → ota_abort()
* ================================================================ */
/**
* @brief 开始 OTA 更新
*
* 内部自动选择下一个可用的 OTA 分区,擦除分区空间。
*
* @param firmware_size 固件总大小(字节)。
* 传入 0 表示大小未知:跳过长度校验,progress 百分比始终为 0。
* @return
* - ESP_OK: 开始成功,可调用 ota_write
* - OTA_ERR_ALREADY_IN_PROGRESS: 上次 OTA 未结束
* - OTA_ERR_NO_OTA_PARTITION: 分区表无可用 OTA 分区
* - OTA_ERR_BEGIN_FAILED: ESP-IDF OTA begin 失败
*/
esp_err_t ota_begin(size_t firmware_size);
/**
* @brief 写入固件数据块
*
* 阻塞调用,写入 flash 前先进行必要的擦除。
* 建议每次写入 1024–4096 字节以获得最佳吞吐量。
*
* @param data 数据指针
* @param len 数据长度(字节)
* @return
* - ESP_OK: 写入成功
* - OTA_ERR_NOT_INITIALIZED: 未调用 ota_begin
* - OTA_ERR_STATE_ERROR: 当前状态不允许写入
* - OTA_ERR_WRITE_FAILED: 写入 flash 失败
*/
esp_err_t ota_write(const uint8_t *data, size_t len);
/**
* @brief 完成 OTA 更新
*
* 校验已写入数据的完整性,验证固件镜像,切换启动分区。
* 成功返回后,设备应在适当时机调用 esp_restart() 重启。
*
* @return
* - ESP_OK: 更新完成,可重启
* - OTA_ERR_END_FAILED: 校验或切换失败
* - OTA_ERR_SIZE_MISMATCH: 实写大小与声明不符
*/
esp_err_t ota_end(void);
/**
* @brief 中止 OTA 更新
*
* 放弃当前写入的固件,恢复状态机到 IDLE。
* 调用后目标 OTA 分区数据不可靠,下次 begin 会重新擦除。
*
* @return ESP_OK 或错误码
*/
esp_err_t ota_abort(void);
/* ================================================================
* 状态查询
* ================================================================ */
/** 获取当前 OTA 状态 */
ota_state_t ota_get_state(void);
/** 获取当前运行的固件分区(const 指针,不可修改) */
const esp_partition_t *ota_get_running_partition(void);
/** 获取正在更新的目标分区(仅在 IN_PROGRESS 或 VERIFYING 时有效) */
const esp_partition_t *ota_get_update_partition(void);
/** 获取已写入字节数 */
size_t ota_get_bytes_written(void);
/** 获取声明的固件总大小 */
size_t ota_get_firmware_size(void);
#ifdef __cplusplus
}
#endif
#endif /* OTA_MANAGER_H */
+102
View File
@@ -0,0 +1,102 @@
/**
* @file sd_card.h
* @brief SD 卡驱动 (SDMMC 4-bit 模式 + FATFS)
*
* 硬件接口: SDMMC1, 4-bit 模式
* 引脚定义: 见 hw_config.h (SDMMC_CLK/CMD/D0-D3/CD_GPIO)
* 框架: ESP-IDF 5.1.x
*/
#ifndef SD_CARD_H
#define SD_CARD_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ================================================================
* 类型定义
* ================================================================ */
/** SD 卡文件系统类型 */
typedef enum {
SD_FS_NONE = 0, /* 未挂载 / 无卡 */
SD_FS_FAT32, /* FAT32 格式 */
SD_FS_OTHER, /* 其他文件系统(需要格式化) */
} sd_fs_type_t;
/** SD 卡文件信息 */
typedef struct {
char name[64];
size_t size;
bool is_dir;
} sd_file_info_t;
/* ================================================================
* 挂载 / 卸载 / 格式化
* ================================================================ */
/** 初始化 SDMMC 主机并尝试挂载 FAT 文件系统到 /sdcard */
esp_err_t sd_card_init(void);
/** 卸载文件系统并释放 SDMMC 资源 */
esp_err_t sd_card_deinit(void);
/** 获取当前 SD 卡的文件系统类型 */
sd_fs_type_t sd_card_get_fs_type(void);
/** 检测 SD 卡是否物理插入 (读取 CD 引脚) */
bool sd_card_is_inserted(void);
/** 将 SD 卡格式化为 FAT32 并重新挂载 */
esp_err_t sd_card_format(void);
/* ================================================================
* 文件枚举
* ================================================================ */
/** 枚举 SD 卡根目录下的文件和目录
* @param list 输出缓冲区
* @param max_files 最多返回的文件数
* @return 实际枚举到的文件数量 (≤ max_files)
*/
int sd_card_list_files(sd_file_info_t *list, int max_files);
/* ================================================================
* 文件读取
* ================================================================ */
/** 读取文本文件全部内容到字符串缓冲区
* @param path 文件路径 (相对于 /sdcard, 如 "readme.txt")
* @param buf 输出缓冲区 (会以 '\0' 结尾)
* @param max_len 缓冲区最大字节数 (含结尾 '\0')
* @return ESP_OK 成功, 否则失败
*
* 注意: 文件超过 max_len-1 时会被截断。
*/
esp_err_t sd_card_read_text(const char *path, char *buf, size_t max_len);
/** 读取图片文件到内存缓冲区 (二进制原始数据)
* @param path 文件路径 (相对于 /sdcard, 如 "photo.bmp")
* @param buf 输出缓冲区 (调用者负责分配)
* @param max_len 缓冲区最大字节数
* @param out_len [输出] 实际读取的字节数
* @return ESP_OK 成功, 否则失败
*
* 注意: 此函数只负责读取原始二进制数据,
* 图片解析和显示由上层实现。
*/
esp_err_t sd_card_read_image(const char *path,
uint8_t *buf, size_t max_len,
size_t *out_len);
#ifdef __cplusplus
}
#endif
#endif /* SD_CARD_H */
+48
View File
@@ -0,0 +1,48 @@
/**
* @file sht30.h
* @brief SHT30 温湿度传感器驱动 (I2C)
*
* 硬件接口: I2C (复用 hw_init 中已初始化的 I2C_PORT)
* I2C 地址: 0x44 (ADDR 接地) 或 0x45 (ADDR 接 VDD)
* 框架: ESP-IDF 5.1.x
*/
#ifndef SHT30_H
#define SHT30_H
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/** 单次测量结果 */
typedef struct {
float temperature; /* 摄氏度 */
float humidity; /* 相对湿度 % */
} sht30_data_t;
/**
* @brief 初始化 SHT30 传感器 (验证 I2C 通信)
* @return ESP_OK 成功, 否则失败
*
* 注意: 调用前需确保 I2C 总线已初始化 (hw_i2c_init)。
*/
esp_err_t sht30_init(void);
/**
* @brief 执行单次测量 (高重复性、时钟拉伸模式)
* @param data [输出] 温湿度结果
* @return ESP_OK 成功, 否则失败
*
* 内部等待约 15 ms 后读取 6 字节原始数据,
* 经 CRC-8 校验后转换为物理量。
*/
esp_err_t sht30_read(sht30_data_t *data);
#ifdef __cplusplus
}
#endif
#endif /* SHT30_H */
+46
View File
@@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html
+6
View File
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xD000, 0x2000,
phy_init, data, phy, 0xF000, 0x1000,
factory, app, factory, 0x10000, 0xE0000,
ota_0, app, ota_0, 0xF0000, 0xE0000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x4000
3 otadata data ota 0xD000 0x2000
4 phy_init data phy 0xF000 0x1000
5 factory app factory 0x10000 0xE0000
6 ota_0 app ota_0 0xF0000 0xE0000
+7
View File
@@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xF000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
factory, app, factory, 0x20000, 0x180000,
ota_0, app, ota_0, 0x1A0000, 0x180000,
ota_1, app, ota_1, 0x320000, 0xE0000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x6000
3 otadata data ota 0xF000 0x2000
4 phy_init data phy 0x11000 0x1000
5 factory app factory 0x20000 0x180000
6 ota_0 app ota_0 0x1A0000 0x180000
7 ota_1 app ota_1 0x320000 0xE0000
+7
View File
@@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xF000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
factory, app, factory, 0x20000, 0x300000,
ota_0, app, ota_0, 0x320000, 0x300000,
ota_1, app, ota_1, 0x620000, 0x1E0000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x6000
3 otadata data ota 0xF000 0x2000
4 phy_init data phy 0x11000 0x1000
5 factory app factory 0x20000 0x300000
6 ota_0 app ota_0 0x320000 0x300000
7 ota_1 app ota_1 0x620000 0x1E0000
+30
View File
@@ -0,0 +1,30 @@
IO6 = BAT_VOLTAGE 读取电池电压,作为电量的粗略判断
IO7 = ADC_CTL 开关读取电压的MOS管,防止电池电量跑光
IO15 = POW_CTL 开关各种外围芯片的电源
IO8 = I2C_SDA
IO19 = Button0 按钮信号0
IO20 = Button1 按钮信号1,两个按钮的状态:00无按键;10左键;01右键;11中键
~~IO41 = LED_CTL 控制状态LED灯
~~IO46 = SD_CS sd卡片选
IO9 = I2C_SCL
IO10 = TFT_PWM TFT显示屏背光亮度控制
IO11 = SPI_SDO(MISO)
IO12 = SPI_SDI(MOSI)
IO13 = SPI_CS
IO14 = SPI_DC
IO21 = SPI_SCL
IO47 = TFE_RST TFT显示屏复位引脚
~~IO45 = USB_PLUGIN_DETECT 检测USB是否插入,高电平有usb
新增或修改的IO口
IO4 = SD_CMD
IO5 = SD_CLK
IO16 = ESP_TX
IO17 = ESP_RX
IO18 = USB_PLUGIN_DETECT
IO42 = LED_CTL
IO41 = SD_DATA0
IO40 = SD_DATA1
IO39 = SD_DATA2
IO38 = SD_DATA3
IO48 = SD_CON
+28
View File
@@ -0,0 +1,28 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:seeed_xiao_esp32s3]
platform = espressif32
board = seeed_xiao_esp32s3
framework = espidf
; OTA 分区表(与 sdkconfig.defaults 中的 flash 大小保持一致)
board_build.partitions = partitions_8mb.csv
; LVGL 显示框架
lib_deps =
lvgl/lvgl@^8.4.0
; 确保 LVGL 找到 lv_conf.h (include/ 目录 + 项目根目录)
; 字体宏直接定义兜底,防止配置未生效
build_flags =
-D LV_CONF_INCLUDE_SIMPLE
-I include
+9
View File
@@ -0,0 +1,9 @@
# FATFS + SD card through VFS
CONFIG_FATFS_VFS_FAT_SDCARD=y
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_8mb.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions_8mb.csv"
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
# ================================================================
# ESP32 DASS2 SDK 默认配置覆盖
#
# 此文件中的设置会覆盖 sdkconfig 中的对应值。
# 删除 .pio/build 目录后重新编译生效。
# ================================================================
# FATFS + SD card through VFS
CONFIG_FATFS_VFS_FAT_SDCARD=y
# ================================================================
# Flash 大小 Seeed XIAO ESP32S3 板载 8MB
# 如果你的硬件实际为 4MB/2MB,请修改此值
# ================================================================
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
# ================================================================
# 分区表 – 自定义 OTA 分区表
# ================================================================
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_8mb.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions_8mb.csv"
+6
View File
@@ -0,0 +1,6 @@
# This file was automatically generated for projects
# without default 'CMakeLists.txt' file.
FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*)
idf_component_register(SRCS ${app_sources})
+305
View File
@@ -0,0 +1,305 @@
/**
* @file hw_init.c
* @brief 外设初始化实现
*
* 基于 pin.txt 引脚定义,初始化 ESP32S3 各外设。
* 适用框架: ESP-IDF 5.1.x
*/
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "driver/spi_master.h"
#include "driver/ledc.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "hw_config.h"
#include "hw_init.h"
#include "sd_card.h"
static const char *TAG = "hw_init";
/* ================================================================
* 内部状态 ADC 句柄
* ================================================================ */
static adc_oneshot_unit_handle_t s_adc1_handle = NULL;
static adc_cali_handle_t s_adc_cali_handle = NULL;
static bool s_adc_cali_done = false;
/* 电池电压分压比 – 按实际硬件修改
* 假设: 电池通过 100k+100k 分压 => 2:1, 即 ADC 读数 ×2 = 实际电压
* 若直接 1:1 接电池, 设 1.0 */
static const float BAT_DIVIDER_RATIO = 2.0f;
static const uint32_t BAT_REF_MV = 3300; /* 内部参考 3.3V (已含 ADC 校准) */
/* ================================================================
* GPIO 通用初始化
* ================================================================ */
esp_err_t hw_gpio_init(void)
{
/* USB 检测 + SD 卡检测 输入 */
gpio_config_t io_conf = {
.pin_bit_mask = BIT64(USB_PLUGIN_DETECT_GPIO) | BIT64(SDMMC_CD_GPIO),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE, /* CD 需要上拉, 卡插入时接地 */
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
ESP_LOGI(TAG, "GPIO init done (USB_DET=IO%d, SD_CD=IO%d)",
USB_PLUGIN_DETECT_GPIO, SDMMC_CD_GPIO);
return ESP_OK;
}
/* ================================================================
* 电源控制
* ================================================================ */
esp_err_t hw_power_init(void)
{
/* POW_CTL – 输出, 初始拉高使外围芯片上电 */
gpio_config_t io_conf = {
.pin_bit_mask = BIT64(POW_CTL_GPIO) | BIT64(ADC_CTL_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
gpio_set_level(POW_CTL_GPIO, 1); /* 开启外围电源 */
gpio_set_level(ADC_CTL_GPIO, 0); /* ADC MOS 默认关闭(低功耗) */
ESP_LOGI(TAG, "Power control init done (POW_CTL=ON, ADC_CTL=OFF)");
return ESP_OK;
}
/* ================================================================
* 电池电压 ADC
* ================================================================ */
esp_err_t hw_battery_adc_init(void)
{
/* 1) 创建 ADC1 单次采集单元 */
adc_oneshot_unit_init_cfg_t unit_cfg = {
.unit_id = ADC_UNIT_1,
.clk_src = ADC_RTC_CLK_SRC_DEFAULT,
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&unit_cfg, &s_adc1_handle));
/* 2) 配置通道 GPIO6 = ADC1_CH5 */
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = ADC_ATTEN_DB_11, /* 03.3V 量程 */
.bitwidth = ADC_BITWIDTH_DEFAULT, /* 12-bit */
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(s_adc1_handle, ADC_CHANNEL_5, &chan_cfg));
/* 3) 尝试创建校准曲线 (curve-fitting / eFuse Vref) */
adc_cali_curve_fitting_config_t cali_cfg = {
.unit_id = ADC_UNIT_1,
.atten = ADC_ATTEN_DB_11,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
esp_err_t ret = adc_cali_create_scheme_curve_fitting(&cali_cfg, &s_adc_cali_handle);
if (ret == ESP_OK) {
s_adc_cali_done = true;
ESP_LOGI(TAG, "ADC calibration ready (curve_fitting)");
} else {
s_adc_cali_done = false;
ESP_LOGW(TAG, "ADC calibration not available, using raw*multipler fallback");
}
ESP_LOGI(TAG, "Battery ADC init done (GPIO6 / ADC1_CH5)");
return ESP_OK;
}
uint32_t hw_battery_read_mv(void)
{
int raw = 0;
/* 开启 MOS 管让 ADC 采样电池电压 */
gpio_set_level(ADC_CTL_GPIO, 1);
vTaskDelay(pdMS_TO_TICKS(10)); /* 等待电压稳定 */
if (adc_oneshot_read(s_adc1_handle, ADC_CHANNEL_5, &raw) != ESP_OK) {
gpio_set_level(ADC_CTL_GPIO, 0);
return 0;
}
/* 关闭 MOS */
gpio_set_level(ADC_CTL_GPIO, 0);
/* 计算电压 */
uint32_t mv = 0;
if (s_adc_cali_done) {
adc_cali_raw_to_voltage(s_adc_cali_handle, raw, (int *)&mv);
} else {
/* 无校准时粗略估算: raw * 3300 / 4096 */
mv = (uint32_t)(raw * BAT_REF_MV / 4096);
}
/* 应用分压比 */
mv = (uint32_t)(mv * BAT_DIVIDER_RATIO);
ESP_LOGD(TAG, "Battery: raw=%d, mv=%" PRIu32, raw, mv);
return mv;
}
/* ================================================================
* 按钮
* ================================================================ */
esp_err_t hw_button_init(void)
{
gpio_config_t io_conf = {
.pin_bit_mask = BIT64(BUTTON0_GPIO) | BIT64(BUTTON1_GPIO),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE, /* 默认上拉, 按下接地 */
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
ESP_LOGI(TAG, "Button init done (GPIO19, GPIO20, pull-up)");
return ESP_OK;
}
uint8_t hw_button_read(void)
{
uint8_t b0 = gpio_get_level(BUTTON0_GPIO) ? 0 : 1; /* 低有效 -> 取反 */
uint8_t b1 = gpio_get_level(BUTTON1_GPIO) ? 0 : 1;
return (b1 << 1) | b0; /* 编码: 00=none, 01=left(B0), 10=right(B1), 11=mid */
}
/* ================================================================
* LED
* ================================================================ */
esp_err_t hw_led_init(void)
{
gpio_config_t io_conf = {
.pin_bit_mask = BIT64(LED_CTL_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
gpio_set_level(LED_CTL_GPIO, 0); /* 默认关 */
ESP_LOGI(TAG, "LED init done (GPIO42)");
return ESP_OK;
}
void hw_led_set(bool on)
{
gpio_set_level(LED_CTL_GPIO, on ? 1 : 0);
}
/* ================================================================
* I2C
* ================================================================ */
esp_err_t hw_i2c_init(void)
{
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_SDA_GPIO,
.scl_io_num = I2C_SCL_GPIO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = 100000, /* 100 kHz, 需要更高改 400000 */
};
ESP_ERROR_CHECK(i2c_param_config(I2C_PORT, &conf));
ESP_ERROR_CHECK(i2c_driver_install(I2C_PORT, conf.mode, 0, 0, 0));
ESP_LOGI(TAG, "I2C init done (SDA=GPIO8, SCL=GPIO9, %" PRIu32 " Hz)", conf.master.clk_speed);
return ESP_OK;
}
/* ================================================================
* SPI (TFT + SD 卡共用总线)
* ================================================================ */
esp_err_t hw_spi_init(void)
{
spi_bus_config_t bus_cfg = {
.mosi_io_num = SPI_MOSI_GPIO,
.miso_io_num = SPI_MISO_GPIO,
.sclk_io_num = SPI_SCLK_GPIO,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = TFT_WIDTH * TFT_HEIGHT * 2 + 8, /* 最大一帧+余量 */
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI_HOST, &bus_cfg, SPI_DMA_CH_AUTO));
ESP_LOGI(TAG, "SPI init done (MOSI=GPIO11, MISO=GPIO12, SCLK=GPIO21)");
return ESP_OK;
}
/* ================================================================
* TFT 背光 PWM (LEDC)
* ================================================================ */
esp_err_t hw_tft_backlight_init(void)
{
ledc_timer_config_t timer_cfg = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_10_BIT, /* 10-bit = 01023 */
.timer_num = LEDC_TIMER_0,
.freq_hz = 5000, /* 5 kHz PWM */
.clk_cfg = LEDC_AUTO_CLK,
};
ESP_ERROR_CHECK(ledc_timer_config(&timer_cfg));
ledc_channel_config_t ch_cfg = {
.gpio_num = TFT_PWM_GPIO,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = LEDC_TIMER_0,
.duty = 0, /* 默认关 */
.hpoint = 0,
};
ESP_ERROR_CHECK(ledc_channel_config(&ch_cfg));
ESP_LOGI(TAG, "TFT backlight PWM init done (GPIO10, 5 kHz, 10-bit)");
return ESP_OK;
}
void hw_tft_backlight_set(uint8_t duty)
{
/* duty: 0100 */
if (duty > 100) duty = 100;
uint32_t max_duty = (1 << 10) - 1; /* 10-bit: 1023 */
uint32_t val = (uint32_t)duty * max_duty / 100;
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, val);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}
/* ================================================================
* 一键初始化所有外设
* ================================================================ */
esp_err_t hw_all_init(void)
{
ESP_LOGI(TAG, "=== HW All Init Start ===");
ESP_ERROR_CHECK(hw_gpio_init());
ESP_ERROR_CHECK(hw_power_init());
ESP_ERROR_CHECK(hw_battery_adc_init());
ESP_ERROR_CHECK(hw_button_init());
ESP_ERROR_CHECK(hw_led_init());
ESP_ERROR_CHECK(hw_i2c_init());
ESP_ERROR_CHECK(hw_spi_init());
ESP_ERROR_CHECK(hw_tft_backlight_init());
/* SD 卡初始化 (不阻塞启动,失败也继续) */
esp_err_t sd_ret = sd_card_init();
if (sd_ret != ESP_OK) {
ESP_LOGW(TAG, "SD card init returned 0x%x (will retry later)", sd_ret);
}
ESP_LOGI(TAG, "=== HW All Init Done ===");
return ESP_OK;
}
+747
View File
@@ -0,0 +1,747 @@
/**
* @file lvgl_demo.c
* @brief LVGL TFT 显示驱动 (ST7789) + SD 卡交互 Demo
*
* 硬件接口: SPI 四线 (CS/DC/RST/PWM) + 背光 PWM
* 适用芯片: ST7789 (也可适配 ST7735 / GC9A01, 改初始化数组即可)
* 框架: ESP-IDF 5.1.x + LVGL 8.4
*
* 交互流程:
* 1. 启动后显示原始 demo 界面
* 2. 检测到 SD 卡插入后:
* - 非 FAT32 → 对话框"是否格式化?" [左=否] [右=格式化]
* - FAT32 → 对话框"是否浏览文件?" [左=否] [右=浏览]
* 3. 文件列表界面: 左键上翻 / 右键下翻 / 中键返回
*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "lvgl.h"
#include "hw_config.h"
#include "hw_init.h"
#include "sd_card.h"
static const char *TAG = "lvgl_demo";
/* ================================================================
* 显示参数 (按实际 TFT 修改)
* ================================================================ */
#define TFT_SPI_CLOCK_HZ (40 * 1000 * 1000) /* 40 MHz SPI */
#define LVGL_TASK_INTERVAL_MS 5 /* 5 ms 刷新循环 */
#define DISP_BUF_LINES 60 /* 每次刷新行数 */
/* ================================================================
* 文件列表参数
* ================================================================ */
#define FILE_LIST_MAX 64 /* 最多枚举文件数 */
#define FILE_LIST_PAGE_SIZE 6 /* 每页显示文件数 */
/* ================================================================
* UI 状态机
* ================================================================ */
typedef enum {
UI_STATE_DEMO, /* 显示原始 demo 界面 */
UI_STATE_DIALOG, /* 显示对话框 (格式化 / 浏览确认) */
UI_STATE_FILE_LIST, /* 显示 SD 卡文件列表 */
} ui_state_t;
typedef enum {
DIALOG_FORMAT = 0, /* "是否格式化 SD 卡?" 对话框 */
DIALOG_BROWSE, /* "是否浏览 SD 卡文件?" 对话框 */
DIALOG_FORMATTING, /* "格式化中,请稍候..." */
} dialog_type_t;
/* ================================================================
* SPI 设备句柄 (TFT)
* ================================================================ */
static spi_device_handle_t s_spi_tft = NULL;
/* ================================================================
* LVGL 显示缓存
* ================================================================ */
static lv_disp_draw_buf_t s_draw_buf;
static lv_disp_drv_t s_disp_drv;
static lv_color_t *s_buf1;
static lv_color_t *s_buf2;
/* ================================================================
* UI 全局句柄
* ================================================================ */
/* -------- Demo 界面 -------- */
static lv_obj_t *s_cont_demo = NULL; /* demo 主容器 */
static lv_obj_t *s_label_title = NULL;
static lv_obj_t *s_label_bat = NULL;
static lv_obj_t *s_label_btn = NULL;
static lv_obj_t *s_bar = NULL;
static lv_obj_t *s_label_sd_status = NULL; /* SD 卡状态标签 */
/* -------- 对话框界面 -------- */
static lv_obj_t *s_cont_dialog = NULL;
static lv_obj_t *s_label_dialog = NULL;
/* -------- 文件列表界面 -------- */
static lv_obj_t *s_cont_files = NULL;
static lv_obj_t *s_list_files = NULL;
static lv_obj_t *s_label_page = NULL; /* 页码指示 */
/* -------- 状态变量 -------- */
static ui_state_t s_ui_state = UI_STATE_DEMO;
static dialog_type_t s_dialog_type = DIALOG_FORMAT;
static sd_fs_type_t s_last_fs_type = SD_FS_NONE;
static bool s_dialog_shown = false; /* 防止重复弹出 */
/* 文件列表分页 */
static sd_file_info_t s_files[FILE_LIST_MAX];
static int s_file_count = 0;
static int s_file_page = 0;
/* 按钮去抖动 */
static uint8_t s_last_btn = 0;
static uint32_t s_btn_press_ms = 0;
/* ================================================================
* TFT 基本操作 – SPI 发送命令 / 数据
* ================================================================ */
/** 写一个字节的命令 */
static void tft_write_cmd(uint8_t cmd)
{
spi_transaction_t t = {
.length = 8,
.tx_buffer = &cmd,
.user = (void *)0, /* DC = 0: 命令 */
};
gpio_set_level(TFT_DC_GPIO, 0);
spi_device_transmit(s_spi_tft, &t);
}
/** 写多个字节的数据 */
static void tft_write_data(const uint8_t *data, size_t len)
{
spi_transaction_t t = {
.length = len * 8,
.tx_buffer = data,
.user = (void *)1, /* DC = 1: 数据 */
};
gpio_set_level(TFT_DC_GPIO, 1);
spi_device_transmit(s_spi_tft, &t);
}
/** 写单个字节的数据 (便捷封装) */
static inline void tft_write_data_byte(uint8_t data)
{
tft_write_data(&data, 1);
}
/* ================================================================
* TFT 初始化序列 ST7789
* ================================================================ */
typedef struct { uint8_t cmd; uint8_t len; const uint8_t *data; } tft_cmd_t;
/* ST7789 初始化命令表 (常见 240×240 / 240×280 面板) */
static const uint8_t ST7789_DATA_SWRESET[] = {};
static const uint8_t ST7789_DATA_SLPOUT[] = {};
static const uint8_t ST7789_DATA_COLMOD[] = {0x55}; /* 16-bit 565 */
static const uint8_t ST7789_DATA_MADCTL[] = {TFT_MADCTL_ROTATION};
static const uint8_t ST7789_DATA_CASET[] = {0x00, 0x00,
(TFT_WIDTH-1)>>8,
(TFT_WIDTH-1)&0xFF};
static const uint8_t ST7789_DATA_RASET[] = {0x00, 0x00,
(TFT_HEIGHT-1)>>8,
(TFT_HEIGHT-1)&0xFF};
static const uint8_t ST7789_DATA_INVON[] = {};
static const uint8_t ST7789_DATA_NORON[] = {};
static const uint8_t ST7789_DATA_DISPON[] = {};
static const tft_cmd_t s_st7789_init[] = {
{0x01, sizeof(ST7789_DATA_SWRESET), ST7789_DATA_SWRESET}, /* SWRESET */
{0x11, sizeof(ST7789_DATA_SLPOUT), ST7789_DATA_SLPOUT}, /* SLPOUT */
{0x3A, sizeof(ST7789_DATA_COLMOD), ST7789_DATA_COLMOD}, /* COLMOD */
{0x36, sizeof(ST7789_DATA_MADCTL), ST7789_DATA_MADCTL}, /* MADCTL */
{0x2A, sizeof(ST7789_DATA_CASET), ST7789_DATA_CASET}, /* CASET */
{0x2B, sizeof(ST7789_DATA_RASET), ST7789_DATA_RASET}, /* RASET */
{0x21, sizeof(ST7789_DATA_INVON), ST7789_DATA_INVON}, /* INVON */
{0x13, sizeof(ST7789_DATA_NORON), ST7789_DATA_NORON}, /* NORON */
{0x29, sizeof(ST7789_DATA_DISPON), ST7789_DATA_DISPON}, /* DISPON */
};
#define ST7789_INIT_LEN (sizeof(s_st7789_init) / sizeof(s_st7789_init[0]))
/** 硬件复位 TFT */
static void tft_hard_reset(void)
{
gpio_set_level(TFT_RST_GPIO, 0);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(TFT_RST_GPIO, 1);
vTaskDelay(pdMS_TO_TICKS(120));
}
/** 发送初始化命令序列 */
static void tft_init_sequence(const tft_cmd_t *cmds, size_t count)
{
for (size_t i = 0; i < count; i++) {
tft_write_cmd(cmds[i].cmd);
if (cmds[i].len > 0)
tft_write_data(cmds[i].data, cmds[i].len);
switch (cmds[i].cmd) {
case 0x01: vTaskDelay(pdMS_TO_TICKS(120)); break; /* SWRESET */
case 0x11: vTaskDelay(pdMS_TO_TICKS(120)); break; /* SLPOUT */
case 0x29: vTaskDelay(pdMS_TO_TICKS(10)); break; /* DISPON */
default: break;
}
}
}
/** 设置显示窗口 (用于局部刷新) */
static void tft_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
uint8_t caset[4] = {x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF};
uint8_t raset[4] = {y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF};
tft_write_cmd(0x2A); /* CASET */
tft_write_data(caset, 4);
tft_write_cmd(0x2B); /* RASET */
tft_write_data(raset, 4);
tft_write_cmd(0x2C); /* RAMWR */
}
/* ================================================================
* LVGL flush 回调
* ================================================================ */
static void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area,
lv_color_t *color_map)
{
tft_set_window(area->x1, area->y1, area->x2, area->y2);
size_t pixel_cnt = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1);
tft_write_data((const uint8_t *)color_map, pixel_cnt * 2);
lv_disp_flush_ready(drv);
}
/* ================================================================
* 初始化 SPI TFT 设备
* ================================================================ */
static esp_err_t tft_spi_add_device(void)
{
spi_device_interface_config_t dev_cfg = {
.clock_speed_hz = TFT_SPI_CLOCK_HZ,
.mode = 0, /* SPI mode 0: CPOL=0 CPHA=0 */
.spics_io_num = TFT_CS_GPIO,
.queue_size = 1,
.flags = SPI_DEVICE_NO_DUMMY,
};
return spi_bus_add_device(SPI_HOST, &dev_cfg, &s_spi_tft);
}
/* ================================================================
* 工具函数 格式化文件大小
* ================================================================ */
static void fmt_file_size(char *buf, size_t buf_size, size_t size)
{
if (size < 1024) {
snprintf(buf, buf_size, "%u B", (unsigned)size);
} else if (size < 1024 * 1024) {
snprintf(buf, buf_size, "%.1f KB", (float)size / 1024);
} else {
snprintf(buf, buf_size, "%.1f MB", (float)size / (1024 * 1024));
}
}
/* ================================================================
* Demo 界面切换 – 显示/隐藏各容器
* ================================================================ */
static void ui_show_demo(void)
{
if (s_cont_demo) lv_obj_clear_flag(s_cont_demo, LV_OBJ_FLAG_HIDDEN);
if (s_cont_dialog) lv_obj_add_flag(s_cont_dialog, LV_OBJ_FLAG_HIDDEN);
if (s_cont_files) lv_obj_add_flag(s_cont_files, LV_OBJ_FLAG_HIDDEN);
}
static void ui_show_dialog(void)
{
if (s_cont_demo) lv_obj_add_flag(s_cont_demo, LV_OBJ_FLAG_HIDDEN);
if (s_cont_dialog) lv_obj_clear_flag(s_cont_dialog, LV_OBJ_FLAG_HIDDEN);
if (s_cont_files) lv_obj_add_flag(s_cont_files, LV_OBJ_FLAG_HIDDEN);
}
static void ui_show_filelist(void)
{
if (s_cont_demo) lv_obj_add_flag(s_cont_demo, LV_OBJ_FLAG_HIDDEN);
if (s_cont_dialog) lv_obj_add_flag(s_cont_dialog, LV_OBJ_FLAG_HIDDEN);
if (s_cont_files) lv_obj_clear_flag(s_cont_files, LV_OBJ_FLAG_HIDDEN);
}
/* ================================================================
* 对话框内容更新
* ================================================================ */
static void dialog_set_content(dialog_type_t dtype)
{
if (!s_label_dialog) return;
s_dialog_type = dtype;
switch (dtype) {
case DIALOG_FORMAT:
lv_label_set_text(s_label_dialog,
"SD卡不是FAT32格式\n\n"
"是否格式化?\n\n"
"[左键] 否 [右键] 是");
break;
case DIALOG_BROWSE:
lv_label_set_text(s_label_dialog,
"检测到SD卡\n"
"是否浏览文件?\n\n"
"[左键] 否 [右键] 是");
break;
case DIALOG_FORMATTING:
lv_label_set_text(s_label_dialog,
"格式化中\n请稍候...");
break;
}
}
/* ================================================================
* 文件列表刷新
* ================================================================ */
static void file_list_update_page(void)
{
if (!s_list_files || !s_label_page) return;
lv_obj_t *list = s_list_files;
lv_obj_clean(list); /* 清空所有子项 */
int total_pages = (s_file_count + FILE_LIST_PAGE_SIZE - 1)
/ FILE_LIST_PAGE_SIZE;
if (total_pages < 1) total_pages = 1;
if (s_file_page >= total_pages) s_file_page = total_pages - 1;
if (s_file_page < 0) s_file_page = 0;
int start = s_file_page * FILE_LIST_PAGE_SIZE;
int end = start + FILE_LIST_PAGE_SIZE;
if (end > s_file_count) end = s_file_count;
if (s_file_count == 0) {
lv_obj_t *btn = lv_list_add_btn(list, NULL, "(空)");
lv_obj_set_style_text_font(btn, &lv_font_montserrat_14, 0);
} else {
for (int i = start; i < end; i++) {
char label[128];
char size_str[32];
fmt_file_size(size_str, sizeof(size_str), s_files[i].size);
if (s_files[i].is_dir) {
snprintf(label, sizeof(label),
LV_SYMBOL_DIRECTORY " %s",
s_files[i].name);
} else {
snprintf(label, sizeof(label),
LV_SYMBOL_FILE " %s %s",
s_files[i].name, size_str);
}
lv_obj_t *btn = lv_list_add_btn(list, NULL, label);
lv_obj_set_style_text_font(btn, &lv_font_montserrat_14, 0);
}
}
/* 更新页码 */
char page_buf[32];
snprintf(page_buf, sizeof(page_buf), "%d/%d",
s_file_page + 1, total_pages);
lv_label_set_text(s_label_page, page_buf);
}
/* ================================================================
* 按钮读取 & 交互处理
* ================================================================ */
/** 读取按钮, 带 200ms 去抖动 */
static uint8_t read_button(void)
{
uint8_t raw = hw_button_read();
if (raw != s_last_btn) {
s_last_btn = raw;
s_btn_press_ms = (raw != 0) ? lv_tick_get() : 0;
return raw;
}
/* 同一状态持续 200ms 后才确认 */
if (raw != 0 && s_btn_press_ms != 0) {
if (lv_tick_get() - s_btn_press_ms >= 200) {
s_btn_press_ms = 0; /* 只触发一次, 直到释放再重新计时 */
return raw;
}
}
return 0; /* 无有效按键 */
}
/** 处理按键事件 */
static void handle_button(uint8_t btn)
{
if (btn == 0) return;
switch (s_ui_state) {
case UI_STATE_DEMO: {
/* 在 demo 界面: 左键切换 LED */
if (btn == 1) {
static bool led_on = false;
led_on = !led_on;
hw_led_set(led_on);
if (s_label_btn) {
lv_label_set_text(s_label_btn, led_on ? "LED: ON" : "LED: OFF");
}
}
break;
}
case UI_STATE_DIALOG: {
if (s_dialog_type == DIALOG_FORMATTING) {
/* 格式化进行中, 不接受按键 */
break;
}
if (btn == 1) {
/* 左键 = 否 → 回到 demo */
ESP_LOGI(TAG, "Dialog: user chose NO");
s_ui_state = UI_STATE_DEMO;
ui_show_demo();
s_dialog_shown = true; /* 标记已处理, 不再弹出 */
} else if (btn == 2) {
/* 右键 = 是 → 格式化 或 浏览文件 */
ESP_LOGI(TAG, "Dialog: user chose YES");
if (s_dialog_type == DIALOG_FORMAT) {
/* 显示格式化中 */
dialog_set_content(DIALOG_FORMATTING);
lv_timer_handler(); /* 立即刷新一次 */
vTaskDelay(pdMS_TO_TICKS(500));
esp_err_t ret = sd_card_format();
if (ret == ESP_OK) {
ESP_LOGI(TAG, "SD format OK, switching to file list");
s_file_count = sd_card_list_files(s_files, FILE_LIST_MAX);
s_file_page = 0;
s_dialog_shown = true;
s_ui_state = UI_STATE_FILE_LIST;
file_list_update_page();
ui_show_filelist();
} else {
ESP_LOGE(TAG, "SD format failed: 0x%x", ret);
dialog_set_content(DIALOG_FORMAT);
lv_label_set_text(s_label_dialog,
"格式化失败!\n\n"
"[左键] 返回");
s_dialog_type = DIALOG_FORMAT;
}
} else {
/* DIALOG_BROWSE → 显示文件列表 */
s_file_count = sd_card_list_files(s_files, FILE_LIST_MAX);
s_file_page = 0;
s_dialog_shown = true;
s_ui_state = UI_STATE_FILE_LIST;
file_list_update_page();
ui_show_filelist();
}
}
break;
}
case UI_STATE_FILE_LIST: {
if (btn == 1) {
/* 左键 = 上翻页 */
if (s_file_page > 0) {
s_file_page--;
file_list_update_page();
}
} else if (btn == 2) {
/* 右键 = 下翻页 */
int total_pages = (s_file_count + FILE_LIST_PAGE_SIZE - 1)
/ FILE_LIST_PAGE_SIZE;
if (total_pages < 1) total_pages = 1;
if (s_file_page < total_pages - 1) {
s_file_page++;
file_list_update_page();
}
} else if (btn == 3) {
/* 中键 = 返回 demo */
s_ui_state = UI_STATE_DEMO;
ui_show_demo();
}
break;
}
}
}
/* ================================================================
* UI 创建函数
* ================================================================ */
/** LED 切换回调 (demo 界面触摸按钮) */
static void btn_cb(lv_event_t *e)
{
static bool led_on = false;
led_on = !led_on;
hw_led_set(led_on);
if (s_label_btn)
lv_label_set_text(s_label_btn, led_on ? "LED: ON" : "LED: OFF");
}
/** 创建 Demo 界面容器 */
static void create_demo_container(void)
{
s_cont_demo = lv_obj_create(lv_scr_act());
lv_obj_set_size(s_cont_demo, lv_pct(100), lv_pct(100));
lv_obj_set_flex_flow(s_cont_demo, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(s_cont_demo, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(s_cont_demo, 8, 0);
lv_obj_clear_flag(s_cont_demo, LV_OBJ_FLAG_SCROLLABLE);
/* 标题 */
s_label_title = lv_label_create(s_cont_demo);
lv_label_set_text(s_label_title, "ESP32 DASS2");
lv_obj_set_style_text_font(s_label_title, &lv_font_montserrat_24, 0);
/* SD 卡状态 */
s_label_sd_status = lv_label_create(s_cont_demo);
lv_label_set_text(s_label_sd_status, "SD: --");
lv_obj_set_style_text_font(s_label_sd_status, &lv_font_montserrat_14, 0);
/* 电池电压 */
s_label_bat = lv_label_create(s_cont_demo);
lv_label_set_text(s_label_bat, "Battery: -- mV");
lv_obj_set_style_text_font(s_label_bat, &lv_font_montserrat_16, 0);
/* 进度条-模拟电量 */
s_bar = lv_bar_create(s_cont_demo);
lv_obj_set_size(s_bar, lv_pct(60), 20);
lv_bar_set_range(s_bar, 0, 100);
lv_bar_set_value(s_bar, 50, LV_ANIM_ON);
/* 按钮 切换 LED */
lv_obj_t *btn = lv_btn_create(s_cont_demo);
lv_obj_set_size(btn, 120, 40);
lv_obj_add_event_cb(btn, btn_cb, LV_EVENT_CLICKED, NULL);
s_label_btn = lv_label_create(btn);
lv_label_set_text(s_label_btn, "Toggle LED");
lv_obj_center(s_label_btn);
}
/** 创建对话框容器 */
static void create_dialog_container(void)
{
s_cont_dialog = lv_obj_create(lv_scr_act());
lv_obj_set_size(s_cont_dialog, lv_pct(100), lv_pct(100));
lv_obj_set_flex_flow(s_cont_dialog, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(s_cont_dialog, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_clear_flag(s_cont_dialog, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(s_cont_dialog, LV_OBJ_FLAG_HIDDEN);
s_label_dialog = lv_label_create(s_cont_dialog);
lv_obj_set_style_text_font(s_label_dialog, &lv_font_montserrat_16, 0);
lv_label_set_text(s_label_dialog, "SD卡检测中...");
lv_obj_set_style_text_align(s_label_dialog, LV_TEXT_ALIGN_CENTER, 0);
}
/** 创建文件列表容器 */
static void create_filelist_container(void)
{
s_cont_files = lv_obj_create(lv_scr_act());
lv_obj_set_size(s_cont_files, lv_pct(100), lv_pct(100));
lv_obj_set_flex_flow(s_cont_files, LV_FLEX_FLOW_COLUMN);
lv_obj_clear_flag(s_cont_files, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(s_cont_files, LV_OBJ_FLAG_HIDDEN);
/* 标题 */
lv_obj_t *title = lv_label_create(s_cont_files);
lv_label_set_text(title, "SD Card Files");
lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0);
/* 文件列表 */
s_list_files = lv_list_create(s_cont_files);
lv_obj_set_size(s_list_files, lv_pct(100), lv_pct(65));
lv_obj_set_style_text_font(s_list_files, &lv_font_montserrat_14, 0);
/* 页码 + 操作提示 */
s_label_page = lv_label_create(s_cont_files);
lv_label_set_text(s_label_page, "0/0");
lv_obj_set_style_text_font(s_label_page, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_align(s_label_page, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_t *hint = lv_label_create(s_cont_files);
lv_label_set_text(hint,
"[左]上翻 [右]下翻 [中]返回");
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_align(hint, LV_TEXT_ALIGN_CENTER, 0);
}
/* ================================================================
* 周期性定时器回调
* ================================================================ */
/** 电池电压更新 – 每 2 秒 */
static void battery_timer_cb(lv_timer_t *timer)
{
uint32_t mv = hw_battery_read_mv();
if (s_label_bat) {
char buf[32];
snprintf(buf, sizeof(buf), "Battery: %lu mV", (unsigned long)mv);
lv_label_set_text(s_label_bat, buf);
}
if (s_bar) {
int pct = (int)((mv - 3000) * 100 / (4200 - 3000));
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
lv_bar_set_value(s_bar, pct, LV_ANIM_ON);
}
}
/** SD 卡检测 + 按钮轮询 每 500ms */
static void sd_and_button_timer_cb(lv_timer_t *timer)
{
/* --- 按钮轮询 --- */
uint8_t btn = read_button();
if (btn != 0) {
handle_button(btn);
}
/* --- SD 卡状态检测 --- */
sd_fs_type_t current_fs = sd_card_get_fs_type();
/* 更新 demo 界面的 SD 状态标签 */
if (s_label_sd_status && s_ui_state == UI_STATE_DEMO) {
const char *status;
switch (current_fs) {
case SD_FS_NONE: status = "SD: 无卡"; break;
case SD_FS_FAT32: status = "SD: FAT32 (就绪)"; break;
case SD_FS_OTHER: status = "SD: 非FAT32"; break;
default: status = "SD: ??"; break;
}
lv_label_set_text(s_label_sd_status, status);
}
/* 只在 demo 状态下弹出对话框 */
if (s_ui_state != UI_STATE_DEMO) {
s_last_fs_type = current_fs;
return;
}
/* SD 卡状态变化检测 */
if (current_fs != s_last_fs_type) {
s_last_fs_type = current_fs;
s_dialog_shown = false; /* 新状态, 允许弹出 */
}
if (s_dialog_shown) return;
/* 根据 SD 卡状态决定是否弹出对话框 */
if (current_fs == SD_FS_OTHER) {
s_ui_state = UI_STATE_DIALOG;
dialog_set_content(DIALOG_FORMAT);
ui_show_dialog();
} else if (current_fs == SD_FS_FAT32) {
s_ui_state = UI_STATE_DIALOG;
dialog_set_content(DIALOG_BROWSE);
ui_show_dialog();
}
/* SD_FS_NONE: 不弹对话框,显示 demo */
}
/* ================================================================
* LVGL 周期性刷新 Task (FreeRTOS)
* ================================================================ */
static void lvgl_task(void *arg)
{
ESP_LOGI(TAG, "LVGL task started");
while (1) {
lv_timer_handler();
vTaskDelay(pdMS_TO_TICKS(LVGL_TASK_INTERVAL_MS));
}
}
/* ================================================================
* 公开接口
* ================================================================ */
esp_err_t lvgl_demo_start(void)
{
ESP_LOGI(TAG, "Starting LVGL demo...");
/* 1. 初始化 LVGL 核心 */
lv_init();
/* 2. 将 TFT 作为 SPI 设备添加到总线 */
ESP_ERROR_CHECK(tft_spi_add_device());
/* 3. TFT 硬件复位 + 初始化序列 */
gpio_config_t io_conf = {
.pin_bit_mask = BIT64(TFT_DC_GPIO) | BIT64(TFT_RST_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
tft_hard_reset();
tft_init_sequence(s_st7789_init, ST7789_INIT_LEN);
/* 4. 分配 LVGL 显示缓存 */
size_t buf_size = TFT_WIDTH * DISP_BUF_LINES * sizeof(lv_color_t);
s_buf1 = heap_caps_malloc(buf_size, MALLOC_CAP_DMA);
s_buf2 = heap_caps_malloc(buf_size, MALLOC_CAP_DMA);
if (!s_buf1 || !s_buf2) {
ESP_LOGE(TAG, "Failed to allocate LVGL draw buffers");
return ESP_ERR_NO_MEM;
}
lv_disp_draw_buf_init(&s_draw_buf, s_buf1, s_buf2,
TFT_WIDTH * DISP_BUF_LINES);
/* 5. 注册显示驱动 */
lv_disp_drv_init(&s_disp_drv);
s_disp_drv.hor_res = TFT_WIDTH;
s_disp_drv.ver_res = TFT_HEIGHT;
s_disp_drv.flush_cb = disp_flush;
s_disp_drv.draw_buf = &s_draw_buf;
lv_disp_drv_register(&s_disp_drv);
/* 6. 创建所有 UI 容器 */
create_demo_container();
create_dialog_container();
create_filelist_container();
/* 初始状态: 显示 demo */
s_ui_state = UI_STATE_DEMO;
ui_show_demo();
/* 7. 创建定时器 */
lv_timer_create(battery_timer_cb, 2000, NULL); /* 电池: 2s */
lv_timer_create(sd_and_button_timer_cb, 500, NULL); /* SD+按键: 500ms */
/* 8. 启动背光 */
hw_tft_backlight_set(80);
/* 9. 创建 LVGL 刷新 Task */
TaskHandle_t task_handle = NULL;
xTaskCreatePinnedToCore(lvgl_task, "lvgl", 4096, NULL, 5, &task_handle, 1);
if (!task_handle) {
ESP_LOGE(TAG, "Failed to create LVGL task");
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "LVGL demo started (TFT %dx%d)", TFT_WIDTH, TFT_HEIGHT);
return ESP_OK;
}
+118
View File
@@ -0,0 +1,118 @@
/**
* @file main.c
* @brief ESP32 DASS2 主程序入口
*
* 启动流程:
* 1. hw_all_init() 初始化所有硬件外设 (含 SD 卡)
* 2. lvgl_demo_start() 启动 LVGL + TFT Demo (含 SD 卡交互 UI)
* 3. sht30_uart_task 每 30 秒通过 UART2 输出温湿度
*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_system.h"
#include "driver/uart.h"
#include "hw_config.h"
#include "hw_init.h"
#include "lvgl_demo.h"
#include "sht30.h"
static const char *TAG = "main";
/* ================================================================
* UART2 初始化 (Arduino Serial2 GPIO16 TX, GPIO17 RX)
* ================================================================ */
static void uart2_init(void)
{
uart_config_t uart_cfg = {
.baud_rate = UART2_BAUD_RATE,
.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,
};
ESP_ERROR_CHECK(uart_param_config(UART_NUM_2, &uart_cfg));
ESP_ERROR_CHECK(uart_set_pin(UART_NUM_2,
UART2_TX_GPIO, UART2_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_driver_install(UART_NUM_2, 256, 0, 0, NULL, 0));
ESP_LOGI(TAG, "UART2 init done (TX=IO%d, RX=IO%d, %d baud)",
UART2_TX_GPIO, UART2_RX_GPIO, UART2_BAUD_RATE);
}
/* ================================================================
* SHT30 温湿度采集 Task 每 30 秒通过 UART2 输出
* ================================================================ */
static void sht30_uart_task(void *arg)
{
ESP_LOGI(TAG, "SHT30-UART task started");
/* 初始化 UART2 */
uart2_init();
/* 初始化 SHT30 传感器 */
esp_err_t ret = sht30_init();
if (ret != ESP_OK) {
ESP_LOGW(TAG, "SHT30 init failed (0x%x), task will retry on each cycle", ret);
}
/* 输出启动消息 */
const char *banner = "\r\n=== ESP32 DASS2 SHT30 ===\r\n";
uart_write_bytes(UART_NUM_2, banner, strlen(banner));
while (1) {
sht30_data_t data;
ret = sht30_read(&data);
char buf[96];
if (ret == ESP_OK) {
snprintf(buf, sizeof(buf),
"T=%.1f C H=%.1f %%\r\n",
data.temperature, data.humidity);
} else {
snprintf(buf, sizeof(buf),
"SHT30 read error: 0x%x\r\n", ret);
}
uart_write_bytes(UART_NUM_2, buf, strlen(buf));
ESP_LOGI(TAG, "SHT30 -> UART2: %s", buf);
vTaskDelay(pdMS_TO_TICKS(30000));
}
}
/* ================================================================
* app_main
* ================================================================ */
void app_main(void)
{
ESP_LOGI(TAG, "======================================");
ESP_LOGI(TAG, " ESP32 DASS2 Starting...");
ESP_LOGI(TAG, "======================================");
/* 1. 初始化所有硬件外设 */
ESP_ERROR_CHECK(hw_all_init());
/* 2. 启动 LVGL + TFT Demo */
ESP_ERROR_CHECK(lvgl_demo_start());
/* 3. 创建 SHT30 + UART2 输出 Task */
TaskHandle_t sht30_handle = NULL;
xTaskCreatePinnedToCore(sht30_uart_task, "sht30_uart",
3072, NULL, 3, &sht30_handle, 1);
ESP_LOGI(TAG, "System ready. Free heap: %lu",
(unsigned long)esp_get_free_heap_size());
/* 4. 主循环 空闲 */
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
+399
View File
@@ -0,0 +1,399 @@
/**
* @file ota_manager.c
* @brief 固件 OTA 底层管理器实现
*
* 基于 ESP-IDF 5.1.x 的 esp_ota_ops API
* 提供状态机管理、回调触发、LED 反馈。
*
* 线程安全说明:
* 本模块不含互斥锁,假定只从一个 Task 中调用。
* 如需多任务并发访问,调用者应自行加锁。
*/
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "hw_init.h"
#include "ota_manager.h"
static const char *TAG = "ota_mgr";
/* ================================================================
* 内部状态
* ================================================================ */
static ota_state_t s_state = OTA_STATE_IDLE;
static esp_ota_handle_t s_ota_handle = 0;
static const esp_partition_t *s_update_partition = NULL;
static const esp_partition_t *s_running_partition = NULL;
static size_t s_firmware_size = 0; /* 声明的固件总大小,0 = 未知 */
static size_t s_bytes_written = 0;
/* 回调指针 */
static ota_progress_cb_t s_progress_cb = NULL;
static ota_complete_cb_t s_complete_cb = NULL;
static ota_state_cb_t s_state_cb = NULL;
static ota_error_cb_t s_error_cb = NULL;
/* ================================================================
* 内部辅助函数
* ================================================================ */
/** 更新状态机并触发回调 */
static void change_state(ota_state_t new_state)
{
if (s_state == new_state) return;
ota_state_t old = s_state;
s_state = new_state;
ESP_LOGI(TAG, "State: %d → %d", (int)old, (int)new_state);
if (s_state_cb) {
s_state_cb(old, new_state);
}
}
/** 触发错误回调 */
static void trigger_error(ota_error_t code, const char *msg)
{
ESP_LOGE(TAG, "Error %d: %s", (int)code, msg);
if (s_error_cb) {
s_error_cb(code, msg);
}
}
/** 触发进度回调 */
static void trigger_progress(void)
{
if (!s_progress_cb) return;
int percent = 0;
if (s_firmware_size > 0 && s_bytes_written <= s_firmware_size) {
percent = (int)(s_bytes_written * 100 / s_firmware_size);
if (percent > 100) percent = 100;
}
s_progress_cb(percent, s_bytes_written, s_firmware_size);
}
/** 触发完成回调 */
static void trigger_complete(bool success, ota_error_t err)
{
if (s_complete_cb) {
s_complete_cb(success, err);
}
}
/** 将 esp_err_t 映射为 ota_error_t 日志字符串 */
static const char *error_string(ota_error_t code)
{
switch (code) {
case OTA_ERR_NONE: return "No error";
case OTA_ERR_NOT_INITIALIZED: return "OTA not initialized";
case OTA_ERR_ALREADY_IN_PROGRESS: return "OTA already in progress";
case OTA_ERR_BEGIN_FAILED: return "OTA begin failed";
case OTA_ERR_WRITE_FAILED: return "OTA write failed";
case OTA_ERR_END_FAILED: return "OTA end failed";
case OTA_ERR_ABORT_FAILED: return "OTA abort failed";
case OTA_ERR_SIZE_MISMATCH: return "Firmware size mismatch";
case OTA_ERR_NO_OTA_PARTITION: return "No OTA partition available";
case OTA_ERR_STATE_ERROR: return "Invalid state for operation";
case OTA_ERR_VERIFY_FAILED: return "Firmware verification failed";
default: return "Unknown error";
}
}
/* ================================================================
* 回调注册
* ================================================================ */
void ota_set_progress_callback(ota_progress_cb_t cb) { s_progress_cb = cb; }
void ota_set_complete_callback(ota_complete_cb_t cb) { s_complete_cb = cb; }
void ota_set_state_callback(ota_state_cb_t cb) { s_state_cb = cb; }
void ota_set_error_callback(ota_error_cb_t cb) { s_error_cb = cb; }
void ota_clear_callbacks(void)
{
s_progress_cb = NULL;
s_complete_cb = NULL;
s_state_cb = NULL;
s_error_cb = NULL;
}
/* ================================================================
* 状态查询
* ================================================================ */
ota_state_t ota_get_state(void) { return s_state; }
size_t ota_get_bytes_written(void) { return s_bytes_written; }
size_t ota_get_firmware_size(void) { return s_firmware_size; }
const esp_partition_t *ota_get_running_partition(void)
{
if (s_running_partition == NULL) {
s_running_partition = esp_ota_get_running_partition();
}
return s_running_partition;
}
const esp_partition_t *ota_get_update_partition(void)
{
return s_update_partition;
}
/* ================================================================
* ota_begin 开始 OTA 更新
* ================================================================ */
esp_err_t ota_begin(size_t firmware_size)
{
ESP_LOGI(TAG, "ota_begin: size=%zu bytes", firmware_size);
/* 状态检查 */
if (s_state != OTA_STATE_IDLE) {
trigger_error(OTA_ERR_ALREADY_IN_PROGRESS,
error_string(OTA_ERR_ALREADY_IN_PROGRESS));
return OTA_ERR_ALREADY_IN_PROGRESS;
}
/* 获取运行分区(缓存) */
s_running_partition = esp_ota_get_running_partition();
if (s_running_partition == NULL) {
ESP_LOGE(TAG, "Failed to get running partition");
change_state(OTA_STATE_ERROR);
trigger_error(OTA_ERR_NO_OTA_PARTITION,
error_string(OTA_ERR_NO_OTA_PARTITION));
return OTA_ERR_NO_OTA_PARTITION;
}
ESP_LOGI(TAG, "Running partition: %s (subtype=%d)",
s_running_partition->label, (int)s_running_partition->subtype);
/* 获取下一个 OTA 分区 */
s_update_partition = esp_ota_get_next_update_partition(NULL);
if (s_update_partition == NULL) {
ESP_LOGE(TAG, "No next OTA partition found");
change_state(OTA_STATE_ERROR);
trigger_error(OTA_ERR_NO_OTA_PARTITION,
error_string(OTA_ERR_NO_OTA_PARTITION));
return OTA_ERR_NO_OTA_PARTITION;
}
ESP_LOGI(TAG, "Update partition: %s (subtype=%d, size=%" PRIu32 ")",
s_update_partition->label, (int)s_update_partition->subtype,
s_update_partition->size);
/* 大小检查:固件不能超过目标分区 */
if (firmware_size > 0 && firmware_size > s_update_partition->size) {
ESP_LOGE(TAG, "Firmware (%zu) exceeds partition size (%" PRIu32 ")",
firmware_size, s_update_partition->size);
change_state(OTA_STATE_ERROR);
trigger_error(OTA_ERR_BEGIN_FAILED,
"Firmware size exceeds partition capacity");
return OTA_ERR_BEGIN_FAILED;
}
/* 调用 ESP-IDF OTA begin */
esp_err_t ret = esp_ota_begin(s_update_partition, firmware_size, &s_ota_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_begin failed: 0x%x", ret);
s_ota_handle = 0;
s_update_partition = NULL;
change_state(OTA_STATE_ERROR);
trigger_error(OTA_ERR_BEGIN_FAILED, error_string(OTA_ERR_BEGIN_FAILED));
return OTA_ERR_BEGIN_FAILED;
}
/* 初始化内部计数器 */
s_firmware_size = firmware_size;
s_bytes_written = 0;
/* LED 亮起,指示 OTA 进行中 */
hw_led_set(true);
change_state(OTA_STATE_IN_PROGRESS);
/* 触发初始进度(0% */
trigger_progress();
ESP_LOGI(TAG, "OTA begin OK, ready to receive data");
return ESP_OK;
}
/* ================================================================
* ota_write 写入固件数据块
* ================================================================ */
esp_err_t ota_write(const uint8_t *data, size_t len)
{
if (data == NULL || len == 0) {
return ESP_ERR_INVALID_ARG;
}
/* 状态检查 */
if (s_state != OTA_STATE_IN_PROGRESS) {
trigger_error(OTA_ERR_STATE_ERROR, error_string(OTA_ERR_STATE_ERROR));
return OTA_ERR_STATE_ERROR;
}
/* 大小检查(仅在声明了固件大小时校验) */
if (s_firmware_size > 0 && (s_bytes_written + len) > s_firmware_size) {
ESP_LOGE(TAG, "Size overflow: %zu + %zu > %zu",
s_bytes_written, len, s_firmware_size);
trigger_error(OTA_ERR_SIZE_MISMATCH,
error_string(OTA_ERR_SIZE_MISMATCH));
return OTA_ERR_SIZE_MISMATCH;
}
/* 写入 flash */
esp_err_t ret = esp_ota_write(s_ota_handle, data, len);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_write failed: 0x%x", ret);
trigger_error(OTA_ERR_WRITE_FAILED,
error_string(OTA_ERR_WRITE_FAILED));
return OTA_ERR_WRITE_FAILED;
}
s_bytes_written += len;
ESP_LOGD(TAG, "Write %zu bytes, total %zu/%zu",
len, s_bytes_written, s_firmware_size);
/* 触发进度回调 */
trigger_progress();
/* 写入时的 LED 反馈 —— 短暂翻转 */
hw_led_set(false);
vTaskDelay(pdMS_TO_TICKS(2));
hw_led_set(true);
return ESP_OK;
}
/* ================================================================
* ota_end 完成 OTA 更新
* ================================================================ */
esp_err_t ota_end(void)
{
ESP_LOGI(TAG, "ota_end: finalizing...");
/* 状态检查 */
if (s_state != OTA_STATE_IN_PROGRESS) {
trigger_error(OTA_ERR_STATE_ERROR, error_string(OTA_ERR_STATE_ERROR));
return OTA_ERR_STATE_ERROR;
}
/* 大小校验 */
if (s_firmware_size > 0 && s_bytes_written != s_firmware_size) {
ESP_LOGE(TAG, "Size mismatch: written=%zu, declared=%zu",
s_bytes_written, s_firmware_size);
trigger_error(OTA_ERR_SIZE_MISMATCH,
error_string(OTA_ERR_SIZE_MISMATCH));
change_state(OTA_STATE_ERROR);
hw_led_set(false);
return OTA_ERR_SIZE_MISMATCH;
}
change_state(OTA_STATE_VERIFYING);
/* 调用 ESP-IDF OTA end(内部校验镜像头、SHA256 等) */
esp_err_t ret = esp_ota_end(s_ota_handle);
s_ota_handle = 0;
if (ret != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_end failed: 0x%x", ret);
trigger_error(OTA_ERR_END_FAILED, error_string(OTA_ERR_END_FAILED));
change_state(OTA_STATE_ERROR);
hw_led_set(false);
s_update_partition = NULL;
return OTA_ERR_END_FAILED;
}
/* 设置启动分区 */
ret = esp_ota_set_boot_partition(s_update_partition);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: 0x%x", ret);
trigger_error(OTA_ERR_END_FAILED,
"Failed to set boot partition");
change_state(OTA_STATE_ERROR);
hw_led_set(false);
s_update_partition = NULL;
return OTA_ERR_END_FAILED;
}
/* 验证启动分区切换是否生效 */
const esp_partition_t *new_boot = esp_ota_get_boot_partition();
if (new_boot != s_update_partition) {
ESP_LOGW(TAG, "Boot partition not updated as expected");
trigger_error(OTA_ERR_VERIFY_FAILED,
"Boot partition switch verification failed");
change_state(OTA_STATE_ERROR);
hw_led_set(false);
s_update_partition = NULL;
return OTA_ERR_VERIFY_FAILED;
}
ESP_LOGI(TAG, "OTA complete! New boot partition: %s", s_update_partition->label);
/* LED 常亮表示更新成功 */
hw_led_set(true);
change_state(OTA_STATE_COMPLETE);
/* 触发进度 100% */
s_bytes_written = s_firmware_size;
trigger_progress();
/* 触发完成回调 */
trigger_complete(true, OTA_ERR_NONE);
/* 不清除 s_update_partition,便于上层读取信息 */
return ESP_OK;
}
/* ================================================================
* ota_abort 中止 OTA 更新
* ================================================================ */
esp_err_t ota_abort(void)
{
ESP_LOGI(TAG, "ota_abort: aborting...");
/* IDLE 状态下 abort 是合法的空操作 */
if (s_state == OTA_STATE_IDLE) {
ESP_LOGI(TAG, "Already idle, nothing to abort");
return ESP_OK;
}
/* COMPLETE 状态下 abort 无意义 */
if (s_state == OTA_STATE_COMPLETE) {
ESP_LOGW(TAG, "OTA already complete, cannot abort");
return OTA_ERR_STATE_ERROR;
}
/* 调用 ESP-IDF abort(若句柄仍有效) */
if (s_ota_handle != 0) {
esp_err_t ret = esp_ota_abort(s_ota_handle);
s_ota_handle = 0;
if (ret != ESP_OK) {
ESP_LOGW(TAG, "esp_ota_abort warning: 0x%x", ret);
}
}
/* 清理内部状态 */
s_update_partition = NULL;
s_firmware_size = 0;
s_bytes_written = 0;
/* LED 关闭 */
hw_led_set(false);
/* 触发完成回调(失败) */
trigger_complete(false, OTA_ERR_ABORT_FAILED);
change_state(OTA_STATE_IDLE);
ESP_LOGI(TAG, "OTA aborted, back to idle");
return ESP_OK;
}
+272
View File
@@ -0,0 +1,272 @@
/**
* @file sd_card.c
* @brief SD 卡驱动实现 (SDMMC 4-bit 模式 + FATFS)
*
* 提供 SD 卡挂载/卸载/格式化/文件枚举/文件读取。
* 挂载点固定为 /sdcard。
* 框架: ESP-IDF 5.1.x
*/
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include "esp_log.h"
#include "driver/gpio.h"
#include "driver/sdmmc_host.h"
#include "sdmmc_cmd.h"
#include "esp_vfs_fat.h"
#include "hw_config.h"
#include "sd_card.h"
static const char *TAG = "sd_card";
/* ================================================================
* 内部状态
* ================================================================ */
static sdmmc_card_t *s_card = NULL;
static sd_fs_type_t s_fs_type = SD_FS_NONE;
static const char *SD_MOUNT_POINT = "/sdcard";
/* ================================================================
* 卡检测
* ================================================================ */
bool sd_card_is_inserted(void)
{
return gpio_get_level(SDMMC_CD_GPIO) == 0; /* 低电平 = 卡插入 */
}
/* ================================================================
* 辅助 – 构建 slot 配置 (避免重复代码)
* ================================================================ */
static void fill_slot_config(sdmmc_slot_config_t *slot)
{
*slot = (sdmmc_slot_config_t)SDMMC_SLOT_CONFIG_DEFAULT();
slot->clk = SDMMC_CLK_GPIO;
slot->cmd = SDMMC_CMD_GPIO;
slot->d0 = SDMMC_D0_GPIO;
slot->d1 = SDMMC_D1_GPIO;
slot->d2 = SDMMC_D2_GPIO;
slot->d3 = SDMMC_D3_GPIO;
slot->cd = SDMMC_CD_GPIO;
slot->flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
}
/* ================================================================
* 构建完整路径
* ================================================================ */
static void make_path(char *out, size_t size, const char *path)
{
if (path[0] == '/') {
snprintf(out, size, "%s%s", SD_MOUNT_POINT, path);
} else {
snprintf(out, size, "%s/%s", SD_MOUNT_POINT, path);
}
}
/* ================================================================
* 挂载
* ================================================================ */
esp_err_t sd_card_init(void)
{
/* 如果已经挂载,先卸载 */
if (s_card != NULL || s_fs_type != SD_FS_NONE) {
sd_card_deinit();
}
if (!sd_card_is_inserted()) {
s_fs_type = SD_FS_NONE;
ESP_LOGI(TAG, "SD card not inserted");
return ESP_ERR_NOT_FOUND;
}
ESP_LOGI(TAG, "SD card detected, mounting...");
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.flags = SDMMC_HOST_FLAG_4BIT;
host.max_freq_khz = SDMMC_FREQ_DEFAULT;
sdmmc_slot_config_t slot;
fill_slot_config(&slot);
esp_vfs_fat_mount_config_t mount_cfg = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 16 * 1024,
};
sdmmc_card_t *card = NULL;
esp_err_t ret = esp_vfs_fat_sdmmc_mount(SD_MOUNT_POINT, &host, &slot,
&mount_cfg, &card);
if (ret == ESP_OK) {
s_card = card;
s_fs_type = SD_FS_FAT32;
ESP_LOGI(TAG, "Mounted at %s (FAT32)", SD_MOUNT_POINT);
return ESP_OK;
}
/* 挂载失败但 CD 显示有卡 → 文件系统不是 FAT32
* (esp_vfs_fat_sdmmc_mount 失败后 host 已被内部清理, 无需额外操作) */
ESP_LOGW(TAG, "Mount failed (err 0x%x), card present but not FAT32", ret);
s_fs_type = SD_FS_OTHER;
return ESP_FAIL;
}
/* ================================================================
* 卸载
* ================================================================ */
esp_err_t sd_card_deinit(void)
{
if (s_card != NULL) {
esp_err_t ret = esp_vfs_fat_sdcard_unmount(SD_MOUNT_POINT, s_card);
s_card = NULL;
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Unmount warning: 0x%x", ret);
}
}
s_fs_type = SD_FS_NONE;
ESP_LOGI(TAG, "Deinitialized");
return ESP_OK;
}
/* ================================================================
* 状态查询
* ================================================================ */
sd_fs_type_t sd_card_get_fs_type(void)
{
return s_fs_type;
}
/* ================================================================
* 格式化
*
* 使用 esp_vfs_fat_sdmmc_mount 的 format_if_mount_failed 选项:
* 挂载失败时自动格式化为 FAT32 然后重新挂载。
* 避免直接调用 sdmmc_card_init (该 API 在不同 ESP-IDF 版本中签名不同)。
* ================================================================ */
esp_err_t sd_card_format(void)
{
ESP_LOGI(TAG, "Formatting as FAT32...");
if (s_card != NULL) {
esp_vfs_fat_sdcard_unmount(SD_MOUNT_POINT, s_card);
s_card = NULL;
}
s_fs_type = SD_FS_NONE;
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.flags = SDMMC_HOST_FLAG_4BIT;
host.max_freq_khz = SDMMC_FREQ_DEFAULT;
sdmmc_slot_config_t slot;
fill_slot_config(&slot);
esp_vfs_fat_mount_config_t mount_cfg = {
.format_if_mount_failed = true, /* 挂载失败时自动格式化 */
.max_files = 5,
.allocation_unit_size = 16 * 1024,
};
sdmmc_card_t *card = NULL;
esp_err_t ret = esp_vfs_fat_sdmmc_mount(SD_MOUNT_POINT, &host, &slot,
&mount_cfg, &card);
if (ret == ESP_OK) {
s_card = card;
s_fs_type = SD_FS_FAT32;
ESP_LOGI(TAG, "Format/Mount OK at %s", SD_MOUNT_POINT);
} else {
ESP_LOGE(TAG, "Format/Mount failed: 0x%x", ret);
s_fs_type = SD_FS_NONE;
}
return ret;
}
/* ================================================================
* 文件枚举
* ================================================================ */
int sd_card_list_files(sd_file_info_t *list, int max_files)
{
if (s_fs_type != SD_FS_FAT32 || list == NULL || max_files <= 0) return 0;
DIR *dir = opendir(SD_MOUNT_POINT);
if (!dir) { ESP_LOGW(TAG, "opendir failed"); return 0; }
int count = 0;
struct dirent *entry;
while ((entry = readdir(dir)) != NULL && count < max_files) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) continue;
strncpy(list[count].name, entry->d_name, sizeof(list[count].name) - 1);
list[count].name[sizeof(list[count].name) - 1] = '\0';
char full[256];
make_path(full, sizeof(full), entry->d_name);
struct stat st;
if (stat(full, &st) == 0) {
list[count].size = st.st_size;
list[count].is_dir = S_ISDIR(st.st_mode);
} else {
list[count].size = 0;
list[count].is_dir = (entry->d_type == DT_DIR);
}
count++;
}
closedir(dir);
ESP_LOGI(TAG, "Listed %d files", count);
return count;
}
/* ================================================================
* 文件读取
* ================================================================ */
esp_err_t sd_card_read_text(const char *path, char *buf, size_t max_len)
{
if (s_fs_type != SD_FS_FAT32 || path == NULL || buf == NULL || max_len == 0)
return ESP_ERR_INVALID_ARG;
char full[256];
make_path(full, sizeof(full), path);
FILE *fp = fopen(full, "r");
if (!fp) {
ESP_LOGW(TAG, "fopen(%s) failed", full);
return ESP_ERR_NOT_FOUND;
}
size_t read = fread(buf, 1, max_len - 1, fp);
buf[read] = '\0';
fclose(fp);
ESP_LOGI(TAG, "Read text: %s (%u bytes)", path, (unsigned)read);
return ESP_OK;
}
esp_err_t sd_card_read_image(const char *path,
uint8_t *buf, size_t max_len,
size_t *out_len)
{
if (s_fs_type != SD_FS_FAT32 || path == NULL || buf == NULL ||
max_len == 0 || out_len == NULL)
return ESP_ERR_INVALID_ARG;
char full[256];
make_path(full, sizeof(full), path);
FILE *fp = fopen(full, "rb");
if (!fp) {
ESP_LOGW(TAG, "fopen(%s) failed", full);
return ESP_ERR_NOT_FOUND;
}
*out_len = fread(buf, 1, max_len, fp);
fclose(fp);
ESP_LOGI(TAG, "Read image: %s (%u bytes)", path, (unsigned)(*out_len));
return ESP_OK;
}
+122
View File
@@ -0,0 +1,122 @@
/**
* @file sht30.c
* @brief SHT30 温湿度传感器驱动实现 (I2C)
*
* 通信协议:
* 1. 发送 2 字节命令 0x2C 0x06 (单次测量, 高重复性, 时钟拉伸)
* 2. 等待测量完成 (时钟拉伸由传感器控制, 约 15 ms)
* 3. 读取 6 字节: [T_MSB, T_LSB, T_CRC, H_MSB, H_LSB, H_CRC]
* 4. CRC-8 校验 (多项式 0x31, 初始值 0xFF)
* 5. 转换为物理量
*
* 框架: ESP-IDF 5.1.x
*/
#include <stdio.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/i2c.h"
#include "hw_config.h"
#include "sht30.h"
static const char *TAG = "sht30";
/* ================================================================
* CRC-8 (多项式 0x31, 初始值 0xFF)
* SHT30 数据手册 §4.12 Checksum Calculation
* ================================================================ */
static uint8_t sht30_crc8(const uint8_t *data, size_t len)
{
uint8_t crc = 0xFF;
for (size_t i = 0; i < len; i++) {
crc ^= data[i];
for (int b = 0; b < 8; b++) {
if (crc & 0x80)
crc = (uint8_t)((crc << 1) ^ 0x31);
else
crc <<= 1;
}
}
return crc;
}
/* ================================================================
* 初始化
* ================================================================ */
esp_err_t sht30_init(void)
{
/* 发送软复位命令, 将传感器置于已知状态 */
uint8_t cmd[2] = {0x30, 0xA2};
esp_err_t ret = i2c_master_write_to_device(I2C_PORT, SHT30_I2C_ADDR,
cmd, sizeof(cmd),
pdMS_TO_TICKS(100));
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Soft reset failed (0x%x), sensor may not be present", ret);
return ret;
}
vTaskDelay(pdMS_TO_TICKS(2)); /* 复位后等待 */
ESP_LOGI(TAG, "SHT30 initialized (addr=0x%02X)", SHT30_I2C_ADDR);
return ESP_OK;
}
/* ================================================================
* 单次测量
* ================================================================ */
esp_err_t sht30_read(sht30_data_t *data)
{
if (data == NULL) return ESP_ERR_INVALID_ARG;
/* 1. 发送测量命令: 高重复性 + 时钟拉伸 */
uint8_t cmd[2] = {0x2C, 0x06};
esp_err_t ret = i2c_master_write_to_device(I2C_PORT, SHT30_I2C_ADDR,
cmd, sizeof(cmd),
pdMS_TO_TICKS(100));
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Write command failed: 0x%x", ret);
return ret;
}
/* 2. 等待测量完成 (时钟拉伸模式下传感器会拉低 SCL,
* 这里额外等待确保数据就绪, 约 15 ms) */
vTaskDelay(pdMS_TO_TICKS(20));
/* 3. 读取 6 字节原始数据 */
uint8_t raw[6] = {0};
ret = i2c_master_read_from_device(I2C_PORT, SHT30_I2C_ADDR,
raw, sizeof(raw),
pdMS_TO_TICKS(100));
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Read data failed: 0x%x", ret);
return ret;
}
/* 4. CRC-8 校验 */
if (sht30_crc8(raw, 2) != raw[2]) {
ESP_LOGE(TAG, "Temperature CRC mismatch");
return ESP_ERR_INVALID_CRC;
}
if (sht30_crc8(raw + 3, 2) != raw[5]) {
ESP_LOGE(TAG, "Humidity CRC mismatch");
return ESP_ERR_INVALID_CRC;
}
/* 5. 转换为物理量
* T[°C] = -45 + 175 * (S_T / (2^16 - 1))
* RH[%] = 100 * (S_H / (2^16 - 1))
*/
uint16_t raw_t = ((uint16_t)raw[0] << 8) | raw[1];
uint16_t raw_h = ((uint16_t)raw[3] << 8) | raw[4];
data->temperature = -45.0f + 175.0f * ((float)raw_t / 65535.0f);
data->humidity = 100.0f * ((float)raw_h / 65535.0f);
if (data->humidity > 100.0f) data->humidity = 100.0f;
if (data->humidity < 0.0f) data->humidity = 0.0f;
ESP_LOGD(TAG, "T=%.1f C H=%.1f %%", data->temperature, data->humidity);
return ESP_OK;
}
+11
View File
@@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html