【2024 DigiKey 创意大赛】ESP32-S3-LCD-EV-BOARD点亮lcd、联网、SNTP校时
<p>大家好,我是郑工,尘世间一个迷途小工程师。</p><p> </p>
<p>这次大赛想挑战一下自己,也想写一个手把手esp idf开发入门的帖子,所以这次大赛决定用esp idf开发应用(真是给自己整了个大活呀T_T)</p>
<p> </p>
<p>经过了好多天的调试,终于是把WiFi联网功能与SNTP功能给摸透了,下面就给大家分享一下一些经验,希望对大家有帮助。</p>
<p> </p>
<p><strong><span style="font-size:20px;">一、点亮lcd屏幕</span></strong></p>
<p> </p>
<p>由于我们是拿的乐鑫官方的开发板,所以其实点亮屏幕是很简单的,直接用官方的例程就可以了,GitHub地址如下:</p>
<p><a href="https://github.com/espressif/esp-dev-kits/tree/master/esp32-s3-lcd-ev-board">esp-dev-kits/esp32-s3-lcd-ev-board at master · espressif/esp-dev-kits · GitHub</a></p>
<p>出厂的程序用的是86box_smart_panel,我看过代码了,逻辑和界面写了好多,不方面我们从零开始学习,所以我决定还是用lvgl_demo这个例程。</p>
<p> </p>
<p>直接帮我们把lvgl移植好了,省去我们好多的开发工作,而esp32s3一直lvgl的文档视频网上都挺多的了,总的来说不难,就是下载lvgl官方库,然后对接液晶接口和触摸接口。这里就不详细介绍了。</p>
<p> </p>
<p>然后我们就可以开始做界面开发了,这里我学习了一些代码写界面的方法,感觉跟用tkinter开发界面一样,每个控件每个控件的创建,调整大小样式布局,没有仿真,esp32s3的下载速度又说不上快,调整多几次,一个小时就过去了。所以最后我选择使用squareline studio开发界面。</p>
<p> </p>
<p>这样只要拖拽,调整属性什么的,图片也会自动转码,实在是可以节省很多功夫。</p>
<p> </p>
<p><strong><span style="font-size:20px;">二、联网</span></strong></p>
<p> </p>
<p>用乐鑫的芯片又怎么可以不使用联网功能呢,下面是一段简单的联网测试代码</p>
<pre>
<code>static EventGroupHandle_t wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static void event_handler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data){
static uint32_t wifi_retry_cnt = 0;
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
}else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if( wifi_retry_cnt < 10){
ESP_LOGI(TAG, "WiFi disconnected, retrying...");
esp_wifi_connect();
wifi_retry_cnt++;
}else {
ESP_LOGE(TAG, "WiFi disconnected, retrying failed");
xEventGroupSetBits(wifi_event_group, WIFI_FAIL_BIT);
}
}else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "Got IP address: %s", ip4addr_ntoa(&event->ip_info.ip));
wifi_retry_cnt = 0;
xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void wifi_init_sta(void){
esp_err_t ret =nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
wifi_event_group = xEventGroupCreate();
// tcpip_adapter_init();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));
wifi_config_t wifi_config = {
.sta = {
.ssid = "QC",
.password = "Qaz123456",
.scan_method = WIFI_FAST_SCAN,
.sort_method = WIFI_CONNECT_AP_BY_SIGNAL,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) );
ESP_ERROR_CHECK(esp_wifi_start() );
ESP_LOGI(TAG, "wifi_init finished.");
EventBits_t bits = xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "connected to ap");
}else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG, "fail to connected to ap");
}else {
ESP_LOGE(TAG, "WIFI_EVENT_STA_DISCONNECTED");
}
ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler));
ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler));
vEventGroupDelete(wifi_event_group);
}</code></pre>
<p>简单来说,esp32s3联网有以下的步骤:</p>
<ol data-spm-anchor-id="0.0.0.i253.5d8a3cf6ljTtin">
<li>
<p data-spm-anchor-id="0.0.0.i249.5d8a3cf6ljTtin"><strong data-spm-anchor-id="0.0.0.i248.5d8a3cf6ljTtin">初始化网络堆栈</strong>: 初始化网络接口和协议栈。<button tabindex="0" type="button"></button></p>
<pre>
<code>ESP_ERROR_CHECK(esp_netif_init());</code></pre>
</li>
<li data-spm-anchor-id="0.0.0.i251.5d8a3cf6ljTtin">
<p><strong>创建默认的 Wi-Fi 接口</strong>: 创建一个默认的 Wi-Fi Station(STA)接口。<button tabindex="0" type="button"></button></p>
<pre>
<code>esp_netif_create_default_wifi_sta();</code></pre>
</li>
<li>
<p><strong>配置 Wi-Fi 接口</strong>: 设置 Wi-Fi 的模式和配置参数。<button tabindex="0" type="button"></button></p>
<pre>
<code>wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));</code></pre>
</li>
<li>
<p><strong>设置 Wi-Fi 模式</strong>: 设置 ESP32-S3 为 Wi-Fi Station 模式。<button tabindex="0" type="button"></button></p>
<pre>
<code>ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));</code></pre>
</li>
<li>
<p><strong>配置 Wi-Fi 凭证</strong>: 设置 Wi-Fi SSID 和密码。<button tabindex="0" type="button"></button></p>
<pre>
<code>wifi_config_t wifi_config;
memset(&wifi_config, 0, sizeof(wifi_config_t));
strncpy((char *)wifi_config.sta.ssid, "your_ssid", sizeof(wifi_config.sta.ssid));
strncpy((char *)wifi_config.sta.password, "your_password", sizeof(wifi_config.sta.password));
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));</code></pre>
</li>
<li>
<p><strong>启动 Wi-Fi</strong>: 启动 Wi-Fi 接口。<button tabindex="0" type="button"></button></p>
<pre>
<code>ESP_ERROR_CHECK(esp_wifi_start());</code></pre>
</li>
<li>
<p><strong>连接到 Wi-Fi 网络</strong>: 使用 <code class="hljs">esp_wifi_connect()</code> 函数连接到 Wi-Fi 网络。<button tabindex="0" type="button"></button></p>
<pre>
<code>ESP_ERROR_CHECK(esp_wifi_connect());</code></pre>
</li>
<li>
<p><strong>等待连接完成</strong>: 通常需要等待 Wi-Fi 连接完成,可以通过轮询或注册事件回调函数来实现。<button tabindex="0" type="button"></button></p>
<pre>
<code>while (!esp_netif_is_connected()) {
vTaskDelay(pdMS_TO_TICKS(1000));
}</code></pre>
</li>
<li>
<p data-spm-anchor-id="0.0.0.i254.5d8a3cf6ljTtin"><strong>获取 IP 地址</strong>: 连接成功后,获取分配给 ESP32-S3 的 IP 地址。<button tabindex="0" type="button"></button></p>
<pre>
<code>ip_info_t ip;
esp_netif_get_ip_info(esp_netif_get_handle(ESP_IF_WIFI_STA), &ip);</code></pre>
</li>
<li>
<p data-spm-anchor-id="0.0.0.i252.5d8a3cf6ljTtin"><strong>使用网络</strong>: 此时 ESP32-S3 已经连接到 Wi-Fi 网络,可以进行网络通信。</p>
</li>
</ol>
<p data-spm-anchor-id="0.0.0.i252.5d8a3cf6ljTtin">然后函数需要添加以下判断网络状态,自动重连的业务代码即可。</p>
<p data-spm-anchor-id="0.0.0.i252.5d8a3cf6ljTtin"> </p>
<p data-spm-anchor-id="0.0.0.i252.5d8a3cf6ljTtin"><strong><span style="font-size:20px;">三、SNTP校时</span></strong></p>
<p data-spm-anchor-id="0.0.0.i252.5d8a3cf6ljTtin"> </p>
<p data-spm-anchor-id="0.0.0.i252.5d8a3cf6ljTtin">这个主题我做了两个程序,一个是根据之前做follow me任务使用的网络时间服务api获取时间,二个是使用ESP-IDF提供的SNTP(simple network time potocol)。下面我就帖以下代码讲解以下。</p>
<p data-spm-anchor-id="0.0.0.i252.5d8a3cf6ljTtin"> </p>
<p data-spm-anchor-id="0.0.0.i252.5d8a3cf6ljTtin"><strong><span style="font-size:18px;">网络api</span></strong></p>
<pre>
<code>#include "cJSON.h"
#include "esp_http_client.h"
struct tm timeinfo;
void parse_json_time(const char *json_str) {
// 解析 JSON 字符串
cJSON *json = cJSON_Parse(json_str);
if (json == NULL) {
const char *error_ptr = cJSON_GetErrorPtr();
if (error_ptr != NULL) {
ESP_LOGE(TAG, "Error before: %s", error_ptr);
}
return;
}
// 提取 server_time 字段
cJSON *server_time_item = cJSON_GetObjectItemCaseSensitive(json, "server_time");
if (cJSON_IsNumber(server_time_item)) {
// 获取时间戳
long server_time = server_time_item->valuedouble /1000;
// 将时间戳转换为本地时间
time_t l_time = (time_t)server_time;
struct tm *utc_time = gmtime(&l_time);
utc_time->tm_hour += 8; //东八区
if(utc_time->tm_hour > 23) //防止过界
utc_time->tm_hour -= 24;
timeinfo = *utc_time;
// 格式化时间并打印
char time_str;
printf("TIME: %02d:%02d:%02d\n",utc_time->tm_hour, utc_time->tm_min, utc_time->tm_sec);
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", utc_time);
ESP_LOGI(TAG, "Server time: %s", time_str);
} else {
ESP_LOGE(TAG, "server_time is not a number");
}
// 清理 cJSON 对象
cJSON_Delete(json);
}
void http_test_task(void *pvParameters)
{
char output_buffer = {0}; //用于接收通过http协议返回的数据
int content_length = 0;//http协议头的长度
struct tm* l_time = get_time();
//02-2 配置http结构体
//定义http配置结构体,并且进行清零
esp_http_client_config_t config ;
memset(&config,0,sizeof(config));
//向配置结构体内部写入url
static const char *URL = "http://api.pinduoduo.com/api/server/_stm";
config.url = URL;
//初始化结构体
esp_http_client_handle_t client = esp_http_client_init(&config); //初始化http连接
//设置发送请求
esp_http_client_set_method(client, HTTP_METHOD_GET);
//02-3 循环通讯
while(1)
{
// 与目标主机创建连接,并且声明写入内容长度为0
esp_err_t err = esp_http_client_open(client, 0);
//如果连接失败
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
}
//如果连接成功
else {
//读取目标主机的返回内容的协议头
content_length = esp_http_client_fetch_headers(client);
//如果协议头长度小于0,说明没有成功读取到
if (content_length < 0) {
ESP_LOGE(TAG, "HTTP client fetch headers failed");
}
//如果成功读取到了协议头
else {
//读取目标主机通过http的响应内容
int data_read = esp_http_client_read_response(client, output_buffer, MAX_HTTP_OUTPUT_BUFFER);
if (data_read >= 0) {
//打印响应内容,包括响应状态,响应体长度及其内容
ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %d",
esp_http_client_get_status_code(client), //获取响应状态信息
esp_http_client_get_content_length(client)); //获取响应信息长度
// printf("data:%s\n", output_buffer);
parse_json_time(output_buffer);
}
//如果不成功
else {
ESP_LOGE(TAG, "Failed to read response");
}
}
}
//关闭连接
esp_http_client_close(client);
//延时
vTaskDelay(pdMS_TO_TICKS(1000));
}
}</code></pre>
<p>测试代码可以通过xTaskCreate(&http_test_task, "http_test_task", 8192, NULL, 5, NULL);添加任务。</p>
<p> </p>
<p>代码主要就是通过网络请求访问<a href="http://api.pinduoduo.com/api/server/_stm" target="_blank">http://api.pinduoduo.com/api/server/_stm</a>拼多多时间api,获取时间戳,返回数据的样式是如:</p>
<pre data-spm-anchor-id="0.0.0.i0.521frBGcrBGcjV">
{"server_time":1729698004538}</pre>
<p>的时间戳,然后解析json获取"server_time"对应的值,再把时间戳通过gmtin函数转换为标准时间,需要注意的是</p>
<p>1、时间戳范围的单位是ms,gmtime处理时间的单位是秒,所以需要把时间戳先除以1000再传入函数。</p>
<p>2、获取的时间戳是本初子午线上的时间,北京时间需要把时间+8处理。</p>
<p> </p>
<p>这种办法很难实现精确到秒的时间显示,或许可以请求一次,后续内部自己创建一个时间维护,然后定期去校准时间,不然如我例子那样,每一秒请求一次,会浪费好多网络资源,而且请求返回也需要时间,经常会发生跳秒的情况,实现效果并不理想</p>
<p> </p>
<p>SNTP时间服务器</p>
<p> </p>
<p>第二个办法就是使用ESP-IDF提供的SNTP时间服务器,使用方法简单,不怎么占用系统资源,不需要维护系统时间,代码如下:</p>
<pre>
<code>static void initialize_sntp(void);
static void obtain_time(void);
static void time_sync_notification_cb(struct timeval *tv)
{
ESP_LOGI(TAG, "Notification of a time synchronization event, sec=%lu", tv->tv_sec);
settimeofday(tv, NULL);
}
void app_sntp_init(void)
{
setenv("TZ", "CST-8", 1);
tzset();
obtain_time();
}
static void obtain_time(void)
{
initialize_sntp();
int retry = 0;
const int retry_count = 10;
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) {
ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
if (retry == retry_count) {
ESP_LOGI(TAG, "Could not obtain time after %d attempts", retry);
}else {
ESP_LOGI(TAG, "Time synchronized");
}
}
static void initialize_sntp(void)
{
ESP_LOGI(TAG, "Initializing SNTP");
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
//设置3个时间服务器
esp_sntp_setservername(0, "ntp.aliyun.com");
esp_sntp_setservername(1, "time.asia.apple.com");
esp_sntp_setservername(2, "pool.ntp.org");
esp_sntp_set_time_sync_notification_cb(time_sync_notification_cb);
esp_sntp_init();
}</code></pre>
<p>大家可以看到,代码非常的简单,基本上执行一次,就可以通过time函数获取本地时间了,获取时间的方法也很简单,只需要调用两个函数就可以</p>
<pre>
<code> // 获取当前时间
time_t unix_time = time(NULL);
// 将时间转换为本地时间,这是非线程安全的方法,只有一个参数,所以不能在多线程中使用
struct tm *time_info = localtime(&unix_time);</code></pre>
<p>时间更新的间隔可以使用idf.py menuconfig打开系统设置,在Component config -> LWIP -> SNTP下设置Request interval to update time(ms)中设置,我设置了12小时校准一次,一般也够用了。</p>
<p> </p>
<p>需要注意的是,最好不要在任何的callback函数或者中断处理中调用obtain_time函数,不然都有可能被卡死,结合上面的联网内容,可以在断网重连之后重新校准一次时间。</p>
<p> </p>
<p>后面还会增加天气功能进去,到时候就是使用网络api的方法,注册心知天气的个人业务即可。</p>
<p><a href="https://docs.seniverse.com/api/start/key.html">查看你的 API 密钥 | 心知天气文档 (seniverse.com)</a></p>
<p>时间戳范围的单位是ms,gmtime处理时间的单位是秒,所以需要把时间戳先除以1000再传入函数,这是技巧</p>
Jacktang 发表于 2024-10-25 07:24
时间戳范围的单位是ms,gmtime处理时间的单位是秒,所以需要把时间戳先除以1000再传入函数,这是技巧
<p>是呀,一开始没仔细看,获取到的都成了最大值了,都是固定的时间,还以为出问题没获取到时间</p>
页:
[1]