外观
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文件是内部实现
动手试试
- 打开工程,对照第二节的文件清单,找出每个
.c文件对应哪一块功能。 - 在
app_task()里每一步之间加一行ESP_LOGI,烧录后看串口打印,验证开机顺序和上面写的一致。 - 找出
app_wifi_connect()的实现,看看它是怎么"等待"连接结果的。