20260610
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -86,3 +86,10 @@ dkms.conf
|
||||
*.out
|
||||
*.app
|
||||
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
|
||||
|
||||
|
||||
Vendored
+10
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
cmake_minimum_required(VERSION 3.16.0)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(ESP32_Dass2)
|
||||
@@ -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` | 完成百分比 (0–100)。若 `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 Timer(10Hz)中:
|
||||
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` | 注销所有回调 |
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
@@ -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); /* 0–100 */
|
||||
|
||||
/* ---------- 一键初始化所有外设 ---------- */
|
||||
esp_err_t hw_all_init(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HW_INIT_H */
|
||||
@@ -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"*/
|
||||
@@ -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 */
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* @file ota_manager.h
|
||||
* @brief 固件 OTA(Over-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 完成百分比 (0–100)
|
||||
* @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 */
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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
|
||||
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
@@ -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
@@ -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, /* 0–3.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 = 0–1023 */
|
||||
.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: 0–100 */
|
||||
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
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user