Skip to content

3.2 总体架构与启动流程 ​

这节要搞懂:当前工程代码怎么分文件,开机时按什么顺序初始化。 需要基础:3.1 ⭐ 本章最重要的两节之一

一、为什么不能全写在一个文件里 ​

新手常见写法:所有代码都堆进 main.c。

这样虽然一开始写起来快,但项目一复杂就会出问题:

比如 WiFi、蓝牙、屏幕、传感器、天气、MQTT、OTA 都写在一个文件里,后面想改某个功能,就很难找到对应代码。

所以工程通常会按功能拆文件。

二、当前工程是怎么组织的 ​

当前工程所有源码都放在 main/ 目录下,没有物理拆成多层目录,但已经按功能拆成了多个 .c/.h 文件:

text
main/
├── main.c                   启动流程、任务创建、主界面循环、传感器/闹钟/背光任务
├── app_display.c/h          主界面绘制封装
├── app_config.c/h           NVS 配置保存与读取
├── app_wifi.c/h             WiFi 连接、HTTP 网页配网、SD 卡网页管理
├── app_time.c/h             SNTP 时间同步
├── weather.c/h              天气 API 请求
├── app_ble.c/h              BLE GATT 蓝牙通信
├── app_mqtt.c/h             MQTT 云端通信
├── app_onenet_ota.c/h       OTA 远程升级
├── app_sd.c/h               SDMMC / FATFS 挂载与路径管理
├── app_audio_player.c/h     音频播放、提示音、闹钟音
├── app_image_viewer.c/h     SD 卡图片显示
├── aht20.c/h                AHT20 温湿度传感器
├── tftlcd.c/h               LCD 屏幕底层驱动
└── nocodec_audio.c/h        PDM 麦克风与 I2S 喇叭底层读写

三、当前工程的真实开机流程 ​

工程入口是 app_main(),但它只做一件事 —— 创建 app_task:

c
void app_main(void)
{
    BaseType_t ret = xTaskCreate(app_task, "app_task",
                                 APP_TASK_STACK_SIZE, NULL,
                                 APP_TASK_PRIORITY, NULL);
}

真正的初始化流程在 app_task() 里。

为什么不直接写在 app_main 里

技术上完全可以 —— ESP-IDF 的 app_main() 本身就跑在一个 FreeRTOS 任务里,不是裸机的那个 main()。

项目小的时候,直接在 app_main() 里初始化、创建任务,完全没问题。

但这个项目功能比较多,所以选择让 app_main() 只创建 app_task,真正的初始化顺序和后续主界面循环都放到 app_task 里。

但这个工程选择再开一个 app_task,有两个实际理由:

① app_task 不只是"初始化",它后面要变成主界面刷新循环

初始化跑完,它会进入 update_dashboard_loop() 长期运行下去。如果写在 app_main() 里,等于把一个无限循环塞进了启动入口。

② 栈大小和优先级可以自己定

app_main() 的栈由系统配置决定;自己创建的任务可以按需要指定 APP_TASK_STACK_SIZE 和 APP_TASK_PRIORITY。

一句话:

app_main 只负责"点火",app_task 才是项目真正的主任务。 它前半段初始化系统,后半段进入主界面刷新循环。 其它任务(alarm_task、light_task、sensor_task……)都由它创建出来。

顺序大致是:

text
① 创建数据互斥锁 s_data_mutex
② 初始化配置 app_config_init()
   - 内部会初始化 NVS
   - 从 NVS 读取城市、亮度、闹钟、阈值等配置

③ 创建配置事件队列 s_config_event_queue
④ 注册配置变化回调、MQTT 快照回调、BLE 状态回调
⑤ 启动 BLE:app_ble_start()

⑥ 初始化 LCD:TFTLCD_Init()
⑦ 设置背光亮度:TFTLCD_SetBacklightBrightness()

⑧ 创建本地任务
   - config_apply_task
   - light_task
   - voice_command_task
   - alarm_task

⑨ 显示开机页
⑩ 显示 WiFi 连接中页面
⑪ 调用 app_wifi_connect() 连接 WiFi

⑫ 如果 WiFi 成功:
   - 显示 WiFi 连接成功页面
   - 调用 app_time_sync() 进行 SNTP 校时
   - 启动在线任务:weather_task、time_sync_task

⑬ 如果 WiFi 失败:
   - 显示 WiFi 连接失败页面

⑭ 显示主界面基础布局
⑮ 创建 sensor_task 采集温湿度
⑯ 进入 update_dashboard_loop() 循环刷新时间、日期、天气、温湿度

四、这里要特别讲清楚 ​

实际代码里,app_task 会调用:

c
esp_err_t wifi_ret = app_wifi_connect();

app_wifi_connect() 内部会等待 WiFi 连接结果。连接成功后才调用:

c
app_time_sync();
start_online_tasks();

最后才进入:

c
app_display_show_dashboard_base();
xTaskCreate(sensor_task, ...);
update_dashboard_loop();

准确的说法

当前工程开机后会先尝试连接 WiFi。

  • WiFi 成功 → 进行 SNTP 校时,并启动天气、时间同步等在线任务
  • WiFi 失败 → 也会进入主界面,只是天气和网络相关功能不可用或显示占位内容

五、模块接口怎么理解 ​

虽然源码都在 main/ 下,但很多模块已经通过 .h 文件暴露了接口。

LCD 驱动:

c
void TFTLCD_Init(void);
void TFTLCD_Clear(u16 color);
void TFTLCD_ShowString32(u16 x, u16 y, const char *str, u16 fc, u16 bc, u8 mode);

AHT20 传感器:

c
esp_err_t aht20_init(const aht20_config_t *config);
esp_err_t aht20_read_temperature_humidity(float *temperature, float *humidity);

时间同步:

c
esp_err_t app_time_sync(void);
bool app_time_get_time_line(char *time_text, size_t time_size);

配置保存:

c
esp_err_t app_config_init(void);
void app_config_get(app_config_t *config);
esp_err_t app_config_update(const app_config_t *config);

这就是模块化的基本思想:

.h 文件告诉别人"我能提供什么功能",.c 文件隐藏具体实现。

本节要点 ​

  • 当前工程所有源码都在 main/ 目录下,按功能拆成多个 .c/.h 文件
  • main.c 不只是启动文件,还包含任务创建、主界面循环、传感器/闹钟/背光等逻辑
  • 真正的初始化流程在 app_task() 里,app_main() 只负责创建它
  • NVS 初始化在 app_config_init() 里完成
  • WiFi 事件系统在 app_wifi.c 的 WiFi 初始化流程中创建
  • 开机流程会先尝试 WiFi,成功后 SNTP 校时,再进入主界面
  • .h 文件是模块对外接口,.c 文件是内部实现

动手试试 ​

  1. 打开工程,对照第二节的文件清单,找出每个 .c 文件对应哪一块功能。
  2. 在 app_task() 里每一步之间加一行 ESP_LOGI,烧录后看串口打印,验证开机顺序和上面写的一致。
  3. 找出 app_wifi_connect() 的实现,看看它是怎么"等待"连接结果的。