[经验分享] 【2024 DigiKey 创意大赛】ESP32-S3-LCD-EV-BOARD点亮lcd、联网、SNTP校时

eew_Ya3s2d   2024-10-23 23:57 楼主

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

 

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

 

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

 

一、点亮lcd屏幕

 

由于我们是拿的乐鑫官方的开发板,所以其实点亮屏幕是很简单的,直接用官方的例程就可以了,GitHub地址如下:

esp-dev-kits/esp32-s3-lcd-ev-board at master · espressif/esp-dev-kits · GitHub

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

 

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

 

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

image.png  

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

 

二、联网

 

用乐鑫的芯片又怎么可以不使用联网功能呢,下面是一段简单的联网测试代码

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);
}

简单来说,esp32s3联网有以下的步骤:

  1. 初始化网络堆栈: 初始化网络接口和协议栈。

    ESP_ERROR_CHECK(esp_netif_init());
  2. 创建默认的 Wi-Fi 接口: 创建一个默认的 Wi-Fi Station(STA)接口。

    esp_netif_create_default_wifi_sta();
  3. 配置 Wi-Fi 接口: 设置 Wi-Fi 的模式和配置参数。

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
  4. 设置 Wi-Fi 模式: 设置 ESP32-S3 为 Wi-Fi Station 模式。

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
  5. 配置 Wi-Fi 凭证: 设置 Wi-Fi SSID 和密码。

    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));
  6. 启动 Wi-Fi: 启动 Wi-Fi 接口。

    ESP_ERROR_CHECK(esp_wifi_start());
  7. 连接到 Wi-Fi 网络: 使用 esp_wifi_connect() 函数连接到 Wi-Fi 网络。

    ESP_ERROR_CHECK(esp_wifi_connect());
  8. 等待连接完成: 通常需要等待 Wi-Fi 连接完成,可以通过轮询或注册事件回调函数来实现。

    while (!esp_netif_is_connected()) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
  9. 获取 IP 地址: 连接成功后,获取分配给 ESP32-S3 的 IP 地址。

    ip_info_t ip;
    esp_netif_get_ip_info(esp_netif_get_handle(ESP_IF_WIFI_STA), &ip);
  10. 使用网络: 此时 ESP32-S3 已经连接到 Wi-Fi 网络,可以进行网络通信。

然后函数需要添加以下判断网络状态,自动重连的业务代码即可。

 

三、SNTP校时

 

这个主题我做了两个程序,一个是根据之前做follow me任务使用的网络时间服务api获取时间,二个是使用ESP-IDF提供的SNTP(simple network time potocol)。下面我就帖以下代码讲解以下。

 

网络api

#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[32];
        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[MAX_HTTP_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));
    }

}

测试代码可以通过xTaskCreate(&http_test_task, "http_test_task", 8192, NULL, 5, NULL);添加任务。

 

代码主要就是通过网络请求访问http://api.pinduoduo.com/api/server/_stm拼多多时间api,获取时间戳,返回数据的样式是如:

{"server_time":1729698004538}

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

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

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

 

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

 

SNTP时间服务器

 

第二个办法就是使用ESP-IDF提供的SNTP时间服务器,使用方法简单,不怎么占用系统资源,不需要维护系统时间,代码如下:

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();
}

大家可以看到,代码非常的简单,基本上执行一次,就可以通过time函数获取本地时间了,获取时间的方法也很简单,只需要调用两个函数就可以

    // 获取当前时间
    time_t unix_time = time(NULL);
    // 将时间转换为本地时间,这是非线程安全的方法,只有一个参数,所以不能在多线程中使用
    struct tm *time_info = localtime(&unix_time);

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

 

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

 

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

查看你的 API 密钥 | 心知天气文档 (seniverse.com)

回复评论 (2)

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

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

是呀,一开始没仔细看,获取到的都成了最大值了,都是固定的时间,还以为出问题没获取到时间

点赞  2024-10-30 11:44
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复