简介
不知道大家过年了都在干什么呢?我最近回到了老家日常也比较无聊就是睡觉和玩手机,于是在在快递停运之前怕春节没有什么东西玩就入手了一个微雪的ESP32S3-E-paper 电纸书的一个开发板。来陪着我度过一个新年。先上几张图来给大家掌掌眼。
开发板参数介绍

开发板的正面
开发板的背面,是V2版本即S3N8R8,原本标签是在右下角贴着但是由于我拆卸安装电池的时候还以为是那里有螺丝就撕掉了。 中间是当前的开发板的总体的框图和对应的GPIO引出。
侧面的两个案件和SD卡的插槽。
顶部的扬声器
尾部的USB接口和麦克风

拆开后的内部图 + 已经安装了电池

PCB
修改版的Factory固件
主要修改内容: 基于Factory固件修改移除了WIFI和蓝牙扫描的代码、移除了SD卡的初始代码、移除了LVGL的相关UI和LED的测试任务。增加了低功耗模式(60秒刷新一次)、降低了CPU的主频。增加了默认上电连接WIFI使用NTP同步时间,然后删除WIFI任务。使用本地的RTC芯片进行时间的展示。同时上方保留了湿度和温度的显示。
微雪在上方已经提供了Factory固件,但是其中的Factory固件如果使用电池供电的话,会经常刷新屏幕和LED闪烁,使用原本的电池进行供电的话段时间内就会把所有的电量进行消耗完毕,同时原本Factory固件中的时间和日期也都是设置的固定的日期。因此我们需要对其出厂的固件代码进行修改使其低功耗运行能待机更长的时间。(本文章代码借助了Copilot耗费一下午和一个晚上进行了完善)
在下载的Factory固件的Component下,微雪已经提供了各个组件的BSP
其中我们并不需要怎么关注代码main下的main.cpp ,因为这里只是一个程序的入口。 程序的主要控制逻辑是在ui_bsp 和 user_bsp中。 其中ui_bsp 则是使用Gui_guider 生成的代码。 而UserBsp则是控制了所有程序的逻辑。
其中原本的user_ui_init 启动了很多无关的测试任务,即为了验证开发板硬件好坏。下述为修改的简化版。
void user_ui_init(void)
{
setup_ui(&src_ui); // 初始化LVGL用户界面
// 创建各个功能任务(指定核心1运行)
xTaskCreatePinnedToCore(button_power_task, "button_power_task", 4 * 1024, (void *)&src_ui, 2, NULL, 1);
xTaskCreatePinnedToCore(user_lvgl_task, "user_lvgl_task", 4 * 1024, (void *)&src_ui, 2, NULL, 1);
xTaskCreatePinnedToCore(power_Testing_task, "power_Testing_task", 3 * 1024, NULL, 2, NULL, 1);
// 创建周期性传感器更新任务 (CPU 1)
// xTaskCreatePinnedToCore(periodic_sensor_update_task, "periodic_update", 2048, NULL, 2, NULL, 1);
// 传感器更新任务将在周期性任务中按需创建
ESP_LOGI(TAG, "Performing initial sensor data refresh");
// 初始刷新使用直接调用方式,避免锁竞争问题
update_all_sensor_data_with_timer(&src_ui, false);
if (driver != NULL)
{
lv_refr_now(NULL);
}
// 更新状态
sensor_update_completed = true;
current_work_state = WORK_STATE_READY_FOR_SLEEP;
last_sensor_update_time = xTaskGetTickCount() * portTICK_PERIOD_MS;
update_user_interaction_time();
ESP_LOGI(TAG, "Initial sensor data refresh completed via direct call");
}主要的初始化代码
/**
* @brief 用户应用初始化函数
* 负责初始化所有硬件外设和系统组件
*/
void user_app_init(void)
{
// 初始化电源管理 - 开启各路电源
board_div.VBAT_POWER_ON(); // 开启电池供电
board_div.POWEER_EPD_ON(); // 开启电子墨水屏供电
board_div.POWEER_Audio_OFF(); // 关闭音频供电(未使用音频功能)
#if ENABLE_LOW_POWER_MODE
ESP_LOGI(TAG, "Low power mode ENABLED");
ESP_LOGI(TAG, "Sleep duration: %lu ms, Sensor update interval: %lu ms",
SLEEP_DURATION_MS, SENSOR_DATA_UPDATE_INTERVAL_MS);
#else
ESP_LOGI(TAG, "Low power mode DISABLED");
#endif
ESP_LOGI(TAG, "Sensor update task delay: %d ms, Min LVGL task delay: %d ms",
SENSOR_UPDATE_TASK_DELAY_MS, MIN_LVGL_TASK_DELAY_MS);
ESP_LOGI(TAG, "Work cycle: %lu ms sleep + %lu ms active = %lu ms total",
SLEEP_DURATION_MS,
SENSOR_DATA_UPDATE_INTERVAL_MS - SLEEP_DURATION_MS,
SENSOR_DATA_UPDATE_INTERVAL_MS);
// 初始化传感器更新定时器
esp_timer_create_args_t timer_args = {};
timer_args.callback = &sensor_update_timer_callback;
timer_args.name = "sensor_update_timer";
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &sensor_update_timer));
// 注意:传感器更新任务在此时不创建,等到LVGL初始化完成后再创建
sensor_update_semaphore = xSemaphoreCreateBinary();
assert(sensor_update_semaphore);
ESP_LOGI(TAG, "Sensor update timer initialized, tasks will be created after LVGL init");
// 等待LVGL初始化完成后再创建传感器任务和触发初始刷新
// 初始化I2C总线
i2c_master_Init();
// RTC实时时钟设备初始化(在WiFi同步前,便于后续写入)
rtc_dev = new i2c_equipment();
RtcDateTime_t rtc_time = rtc_dev->get_rtcTime();
ESP_LOGI(TAG, "RTC initial time: %04d-%02d-%02d %02d:%02d:%02d",
rtc_time.year, rtc_time.month, rtc_time.day, rtc_time.hour, rtc_time.minute, rtc_time.second);
if (rtc_time.year < 2020 || rtc_time.month == 0 || rtc_time.day == 0)
{
ESP_LOGW(TAG, "RTC time invalid, setting default time before NTP sync");
rtc_dev->set_rtcTime(2025, 8, 27, 8, 0, 0);
}
// WiFi时间同步 - 异步启动并由定时器检查
ESP_LOGI(TAG, "Starting asynchronous WiFi time synchronization...");
if (!wifi_sync_started && espwifi_start_time_sync())
{
ESP_LOGI(TAG, "WiFi time sync started, will complete in background");
wifi_sync_started = true;
wifi_sync_wait_ms = 0;
if (wifi_sync_timer == NULL)
{
esp_timer_create_args_t wifi_timer_args = {};
wifi_timer_args.callback = &wifi_sync_timer_callback;
wifi_timer_args.name = "wifi_sync_timer";
ESP_ERROR_CHECK(esp_timer_create(&wifi_timer_args, &wifi_sync_timer));
}
ESP_ERROR_CHECK(esp_timer_start_periodic(wifi_sync_timer, WIFI_SYNC_POLL_INTERVAL_MS * 1000));
}
else if (!wifi_sync_started)
{
ESP_LOGW(TAG, "Failed to start WiFi time synchronization");
}
/* 电子墨水屏初始化 */
custom_lcd_spi_t driver_config = {}; // SPI配置结构体
driver_config.cs = EPD_CS_PIN; // 片选引脚
driver_config.dc = EPD_DC_PIN; // 数据/命令引脚
driver_config.rst = EPD_RST_PIN; // 复位引脚
driver_config.busy = EPD_BUSY_PIN; // 忙信号引脚
driver_config.mosi = EPD_MOSI_PIN; // MOSI数据引脚
driver_config.scl = EPD_SCK_PIN; // 时钟引脚
driver_config.spi_host = EPD_SPI_NUM; // SPI主机号
driver_config.buffer_len = 5000; // 缓冲区长度(5000字节)
// 创建电子墨水屏驱动实例并初始化
driver = new epaper_driver_display(EPD_WIDTH, EPD_HEIGHT, driver_config);
driver->EPD_Init(); // 电子墨水屏初始化
driver->EPD_Clear(); // 清屏
driver->EPD_DisplayPartBaseImage(); // 显示基础图像
driver->EPD_Init_Partial(); // 局部刷新初始化
// SHTC3温湿度传感器初始化
shtc3_dev = new i2c_equipment_shtc3();
// 触摸屏初始化(已注释)
// touch_dev = new touch_bsp(ESP32_I2C_DEV_NUM,0x38,EPD_WIDTH,EPD_HEIGHT);
// 按键初始化
user_button_init();
// 其他可选功能初始化(已注释)
// _sdcard_init(); // SD卡初始化
// espwifi_init(); // WiFi初始化
// audio_bsp_init(); // 音频初始化
}原本的代码中WIFI和蓝牙支持Scan并且在屏幕上显示WIFI和蓝牙设备的数量、实际上没有什么意义、以及对应的音频控制的代码在上述初始化的时候我直接关闭了供电以达成低功耗的目的。
主要的传感器的刷新代码如下
/**
* @brief 更新所有传感器数据(支持定时器控制)
* @param src LVGL界面对象指针
* @param use_timer 是否使用定时器控制
*/
// 更新所有传感器数据(带定时器控制)
void update_all_sensor_data_with_timer(lv_ui *src, bool use_timer)
{
char lvgl_buffer[30] = {""};
bool update_success = true;
static uint8_t last_hour = 255, last_minute = 255; // 用于跟踪时间变化
// 记录开始时间
if (use_timer)
{
update_start_time = esp_timer_get_time() / 1000;
sensor_update_completed = false;
current_work_state = WORK_STATE_UPDATING;
ESP_LOGI(TAG, "Starting sensor update with timer control");
}
// 更新RTC时间
if (rtc_dev != NULL)
{
RtcDateTime_t rtc = rtc_dev->get_rtcTime();
ESP_LOGI(TAG, "Reading RTC time: %02d:%02d:%02d (last: %02d:%02d)",
rtc.hour, rtc.minute, rtc.second, last_hour, last_minute);
bool time_changed = (rtc.hour != last_hour) || (rtc.minute != last_minute);
bool first_update = (last_hour == 255 || last_minute == 255);
if (time_changed || first_update)
{
snprintf(lvgl_buffer, 28, "%02d", rtc.hour);
lv_label_set_text(src->screen_label_1, lvgl_buffer);
lv_obj_invalidate(src->screen_label_1);
snprintf(lvgl_buffer, 28, "%02d", rtc.minute);
lv_label_set_text(src->screen_label_2, lvgl_buffer);
lv_obj_invalidate(src->screen_label_2);
ESP_LOGI(TAG, "RTC Time updated: %02d:%02d %s",
rtc.hour, rtc.minute, time_changed ? "(CHANGED)" : "(FIRST)");
}
// 更新上次记录的时间
last_hour = rtc.hour;
last_minute = rtc.minute;
// 喂狗并短暂延时
vTaskDelay(1); // 1ms延时允许任务切换
}
// 更新温湿度
if (shtc3_dev != NULL)
{
shtc3_data_t shtc3_data = shtc3_dev->readTempHumi();
// 检查传感器数据是否有效
if (shtc3_data.Temp > 0 && shtc3_data.RH > 0)
{
snprintf(lvgl_buffer, 28, "%d%%", (int)shtc3_data.RH);
lv_label_set_text(src->screen_label_3, lvgl_buffer);
snprintf(lvgl_buffer, 28, "%d°", (int)shtc3_data.Temp);
lv_label_set_text(src->screen_label_4, lvgl_buffer);
ESP_LOGI(TAG, "Temperature: %d°C, Humidity: %d%%", (int)shtc3_data.Temp, (int)shtc3_data.RH);
}
else
{
ESP_LOGW(TAG, "Invalid SHTC3 data - Temp: %d°C, RH: %d%%", (int)shtc3_data.Temp, (int)shtc3_data.RH);
update_success = false;
}
// 喂狗并短暂延时
vTaskDelay(1); // 1ms延时允许任务切换
}
// 更新电池电量
uint8_t level = adc_dev.get_Batterylevel();
snprintf(lvgl_buffer, 28, "%d%%", level);
lv_label_set_text(src->screen_label_8, lvgl_buffer);
ESP_LOGI(TAG, "Battery level: %d%%", level);
// 喂狗并短暂延时
vTaskDelay(1); // 1ms延时允许任务切换
// 电池电量过低保护
if (level < 4)
{
lv_label_set_text(src->screen_label_9, "OFF");
lv_obj_invalidate(src->screen_label_9);
vTaskDelay(pdMS_TO_TICKS(500));
board_div.VBAT_POWER_OFF();
board_div.POWEER_EPD_OFF();
board_div.POWEER_Audio_OFF();
}
// 更新传感器数据时间戳
last_sensor_update_time = xTaskGetTickCount() * portTICK_PERIOD_MS;
// 同时更新自动刷新时间戳,确保下次按时刷新
last_user_interaction_time = last_sensor_update_time;
ESP_LOGI(TAG, "Sensor data updated at %lu ms", last_sensor_update_time);
// 如果使用定时器控制,启动定时器
if (use_timer && sensor_update_timer != NULL)
{
// 设置5秒后触发定时器(给屏幕刷新留时间)
ESP_ERROR_CHECK(esp_timer_start_once(sensor_update_timer, 5000000)); // 5秒 = 5,000,000微秒
ESP_LOGI(TAG, "Timer started for 5-second completion monitoring");
}
else if (use_timer)
{
// 定时器未初始化,直接标记完成
sensor_update_completed = true;
current_work_state = WORK_STATE_READY_FOR_SLEEP;
ESP_LOGW(TAG, "Timer not initialized, marking update as completed");
}
else
{
// 非定时器模式,立即标记完成
sensor_update_completed = true;
current_work_state = WORK_STATE_READY_FOR_SLEEP;
}
}目前的这个桌面完成度的Demo已经非常高了,但是还有一点需要修改一下即如果开机的时候搜索WIFI的时候并没有找到上一次保存的wifi信息的时候应该打开AP模式然后使用类似Wifi manager的一类的库来让用户手动更新wifi的连接信息。还有一点很重要就是降低CPU的主频,如下所示。

代码如下:
总结
目前体验下来这个开发板还是蛮好玩的,价格也不算太贵,微雪已经提供了BSP的支持,也不需要我们去手动点亮这个墨水屏。可以非常方便的使用GUI_guider 来生成对应的UI(在下一篇文章中会更新)。在使用电池的和上述低功耗的配置下,基本上可以达到两天的待机时间,所以当一个桌面小摆件还是不错的。
我要赚赏金
