eew_Ya3s2d 发表于 2024-10-23 23:57

【2024 DigiKey 创意大赛】ESP32-S3-LCD-EV-BOARD点亮lcd、联网、SNTP校时

<p>大家好,我是郑工,尘世间一个迷途小工程师。</p>

<p>&nbsp;</p>

<p>这次大赛想挑战一下自己,也想写一个手把手esp idf开发入门的帖子,所以这次大赛决定用esp idf开发应用(真是给自己整了个大活呀T_T)</p>

<p>&nbsp;</p>

<p>经过了好多天的调试,终于是把WiFi联网功能与SNTP功能给摸透了,下面就给大家分享一下一些经验,希望对大家有帮助。</p>

<p>&nbsp;</p>

<p><strong><span style="font-size:20px;">一、点亮lcd屏幕</span></strong></p>

<p>&nbsp;</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 &middot; espressif/esp-dev-kits &middot; GitHub</a></p>

<p>出厂的程序用的是86box_smart_panel,我看过代码了,逻辑和界面写了好多,不方面我们从零开始学习,所以我决定还是用lvgl_demo这个例程。</p>

<p>&nbsp;</p>

<p>直接帮我们把lvgl移植好了,省去我们好多的开发工作,而esp32s3一直lvgl的文档视频网上都挺多的了,总的来说不难,就是下载lvgl官方库,然后对接液晶接口和触摸接口。这里就不详细介绍了。</p>

<p>&nbsp;</p>

<p>然后我们就可以开始做界面开发了,这里我学习了一些代码写界面的方法,感觉跟用tkinter开发界面一样,每个控件每个控件的创建,调整大小样式布局,没有仿真,esp32s3的下载速度又说不上快,调整多几次,一个小时就过去了。所以最后我选择使用squareline studio开发界面。</p>

<p> &nbsp;</p>

<p>这样只要拖拽,调整属性什么的,图片也会自动转码,实在是可以节省很多功夫。</p>

<p>&nbsp;</p>

<p><strong><span style="font-size:20px;">二、联网</span></strong></p>

<p>&nbsp;</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 &amp;&amp; event_id == WIFI_EVENT_STA_START) {
      esp_wifi_connect();
    }else if (event_base == WIFI_EVENT &amp;&amp; event_id == WIFI_EVENT_STA_DISCONNECTED) {
      if( wifi_retry_cnt &lt; 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 &amp;&amp; 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(&amp;event-&gt;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(&amp;cfg));

    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &amp;event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &amp;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, &amp;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 &amp; WIFI_CONNECTED_BIT) {
      ESP_LOGI(TAG, "connected to ap");
    }else if (bits &amp; 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, &amp;event_handler));
    ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, &amp;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(&amp;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(&amp;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, &amp;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), &amp;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">&nbsp;</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">&nbsp;</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">&nbsp;</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-&gt;valuedouble /1000;
      // 将时间戳转换为本地时间
      
      time_t l_time = (time_t)server_time;
      struct tm *utc_time = gmtime(&amp;l_time);
      utc_time-&gt;tm_hour += 8;                //东八区
                if(utc_time-&gt;tm_hour &gt; 23)                //防止过界
                        utc_time-&gt;tm_hour -= 24;
      timeinfo = *utc_time;
      // 格式化时间并打印
      char time_str;
      printf("TIME: %02d:%02d:%02d\n",utc_time-&gt;tm_hour, utc_time-&gt;tm_min, utc_time-&gt;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(&amp;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(&amp;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 &lt; 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 &gt;= 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(&amp;http_test_task, &quot;http_test_task&quot;, 8192, NULL, 5, NULL);添加任务。</p>

<p>&nbsp;</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">
{&quot;server_time&quot;:1729698004538}</pre>

<p>的时间戳,然后解析json获取&quot;server_time&quot;对应的值,再把时间戳通过gmtin函数转换为标准时间,需要注意的是</p>

<p>1、时间戳范围的单位是ms,gmtime处理时间的单位是秒,所以需要把时间戳先除以1000再传入函数。</p>

<p>2、获取的时间戳是本初子午线上的时间,北京时间需要把时间+8处理。</p>

<p>&nbsp;</p>

<p>这种办法很难实现精确到秒的时间显示,或许可以请求一次,后续内部自己创建一个时间维护,然后定期去校准时间,不然如我例子那样,每一秒请求一次,会浪费好多网络资源,而且请求返回也需要时间,经常会发生跳秒的情况,实现效果并不理想</p>

<p>&nbsp;</p>

<p>SNTP时间服务器</p>

<p>&nbsp;</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-&gt;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 &amp;&amp; ++retry &lt; 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(&amp;unix_time);</code></pre>

<p>时间更新的间隔可以使用idf.py menuconfig打开系统设置,在Component config -&gt; LWIP -&gt; SNTP下设置Request interval to update time(ms)中设置,我设置了12小时校准一次,一般也够用了。</p>

<p>&nbsp;</p>

<p>需要注意的是,最好不要在任何的callback函数或者中断处理中调用obtain_time函数,不然都有可能被卡死,结合上面的联网内容,可以在断网重连之后重新校准一次时间。</p>

<p>&nbsp;</p>

<p>后面还会增加天气功能进去,到时候就是使用网络api的方法,注册心知天气的个人业务即可。</p>

<p><a href="https://docs.seniverse.com/api/start/key.html">查看你的 API 密钥 | 心知天气文档 (seniverse.com)</a></p>

Jacktang 发表于 2024-10-25 07:24

<p>时间戳范围的单位是ms,gmtime处理时间的单位是秒,所以需要把时间戳先除以1000再传入函数,这是技巧</p>

eew_Ya3s2d 发表于 2024-10-30 11:44

Jacktang 发表于 2024-10-25 07:24
时间戳范围的单位是ms,gmtime处理时间的单位是秒,所以需要把时间戳先除以1000再传入函数,这是技巧

<p>是呀,一开始没仔细看,获取到的都成了最大值了,都是固定的时间,还以为出问题没获取到时间</p>
页: [1]
查看完整版本: 【2024 DigiKey 创意大赛】ESP32-S3-LCD-EV-BOARD点亮lcd、联网、SNTP校时