赞
踩
本篇文章来自极术社区与聆思科技组织的CSK6 视觉AI开发套件活动,更多开发板试用活动请关注极术社区网站。作者:李奎
因为后续想基于CSK6做产品,所以比较关心以下几点:
总结:
整体玩下来,文档还是非常丰富的,从硬件参考设计、软件SDK和案例、涉及的知识能力等,看的出来花了很多的心思,好评 。
总体评估整体实际实用风险不大,部分SDK和AI开放能力有发展空间。
以下结合对SDK的评测,说明其中遇到的一些问题、疑问,以及对后续的期待。
通过官方提供的指导文档,可以比较容易的完成AI能力的Demo验证,比如人脸识别提供SDK源码下载地址和Sample的下载运行例程和说明,小白也可以轻松上手。
根据文档编译烧录后,通过官方提供的基于USB Web浏览器,接上板子的USB口,可以实时预览人脸识别的Demo效果。(以下是使用手机,出境人员是如花),效果上由于画面压缩,显示的图像质量不高,实际算法中是480*640的分辨率。
考虑到实用性,对人脸识别进行了一些非人脸的验证,从目前的情况看,会有一定识别错误的风险。
但在正常人脸识别时得分比较高,在做判断时,需要考虑更高的得分用于判定人脸相似度,比如得分在0.8以上判定为人脸。
另外期待AI能力后续能开放出来,并支持一些通用的机器学习、深度学习框架和模型。
对比K210、V831、V833、V853这些,在Tensorflow、Pytorch、YoLo等的一些支持,也增加了很多的玩法和场景应用的可能。
另外AIoT可以结合网络,增加用户的粘性,比如MaixHub在线训练平台,体验了下,设备的在线联动比较顺滑,体验效果不错。
以上提到了AIoT的能力。正好CSK6是作为AIoT的方案,套件中也提供了ESP32-C3的模块。接下来测试验证下网络的功能是否满足一些实际实用场景的需求。以下分别对官方Demo、SDK二次开发,后续再测试下TCP网络测速、实际实用场景测评。
在官方文档的开发实践->网络章节可以找到WIFI连接和网络通信的案例,根据说明,很容易验证Wi-Fi和网络通信的能力,这里就不过多介绍,可以参考官方的文档,赞一个 。
由于网络遵循了Posix Socket API接口规范,因此做过Linux应用开发的同学应该就非常爽了,这也是一个亮点。
考虑到应用层开发人员不需要关系Kconfig和DTS机制,因此针对提供的接口又做了一次封装,比如Wi-Fi模块,提供了统一向上的接口暴露,方便招的应用层业务开发人员使用:
#include <wifi_core.h>
#include <log.h>
void wifi_test(void)
{
// 初始化Wi-Fi模块
wifi.init();
// 连接AP热点
wifi.connect_ap("mimi", "xxxxxxxx");
}
以上wifi\_core.c/h对提供的API做了封装,大概如下。注意这里的连接AP热点是异步的,接口提供了状态用于判断。
实际实用的情况下,最好使用信号量等机制控制。
wifi\_core.h接口文件:
#ifndef _WIFI_BLE_H #define _WIFI_BLE_H #include <stdbool.h> /// \brief Wi-Fi模块的返回状态 typedef enum { WIFI_OK, WIFI_ERROR }wifi_status_t; /// \brief Wi-Fi模块的当前模式 typedef enum { WIFI_NONE_MODE = 0, WIFI_STA_MODE = 1, WIFI_AP_MODE = 2, WIFI_AP_STA_MODE = 3, }wifi_mode_t; /** * @brief * 封装了STA相关的接口,后续增加AP和其他相关接口功能 */ typedef struct { char mac[24]; /*!< MAC地址 */ char ip[16]; /*!< 设备IP */ char netmask[16]; /*!< 子网掩码 */ char gw[16]; /*!< 网关 */ char ssid[32]; /*!< 连接热点名称 */ char passwd[32]; /*!< 连接热点密码 */ /** * @brief 初始化wifi模块 * * @return wifi_status_t 返回状态 */ wifi_status_t (*init)(void); /** * @brief 反初始化wifi模块 * * @return wifi_status_t 返回状态 */ wifi_status_t (*uninit)(void); /** * @brief 连接热点 * * @return wifi_status_t 返回状态 */ wifi_status_t (*connect_ap)(const char* ssid, const char* passwd); /** * @brief: 断开热点 * @return wifi_status_t 返回状态 */ wifi_status_t (*disconnect_ap)(void); // /** * @brief: 扫描可用热点 * @return wifi_status_t 返回状态 */ // wifi_status_t (*scan_ap)(void); /** * @brief: 检查Wi-Fi模块是否工作正常 * @return wifi_status_t 返回状态 */ bool (*is_connect)(void); /*!< Wi-Fi当前连接状态 */ bool connect_status; }wifi_t; ///< wifi单例 ///< ESP32-C3产品说明:https://www.espressif.com.cn/zh-hans/products/socs/esp32-c3 extern wifi_t wifi; #endif
wifi\_core.c文件内容如下:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/printk.h> #include <sys/sys_heap.h> #include <net/net_if.h> #include <net/net_core.h> #include <net/net_context.h> #include <net/net_mgmt.h> #include "csk6/csk_wifi.h" #include <log.h> #include <wifi_core.h> __weak wifi_status_t esp32_c3_wifi_init(void); __weak wifi_status_t esp32_c3_wifi_uninit(void); __weak wifi_status_t esp32_c3_wifi_connect_ap(const char* ssid, const char* passwd); __weak wifi_status_t esp32_c3_wifi_disconnect_ap(void); // __weak wifi_status_t esp32_c3_wifi_scan_ap(void); __weak bool esp32_c3_wifi_is_connect(void); wifi_t wifi = { .init = esp32_c3_wifi_init, .uninit = esp32_c3_wifi_uninit, .connect_ap = esp32_c3_wifi_connect_ap, .disconnect_ap = esp32_c3_wifi_disconnect_ap, // .scan_ap = esp32_c3_wifi_scan_ap, .is_connect = esp32_c3_wifi_is_connect, .connect_status = false, }; static csk_wifi_event_cb_t wifi_event_cb; static csk_wifi_result_t wifi_result; static struct net_mgmt_event_callback dhcp_cb; static void handler_cb(struct net_mgmt_event_callback *cb, uint32_t mgmt_event, struct net_if *iface) { if (mgmt_event != NET_EVENT_IPV4_DHCP_BOUND) { return; } char buf[NET_IPV4_ADDR_LEN]; snprintf(wifi.ip, sizeof(wifi.ip), "%s", net_addr_ntop(AF_INET, &iface->config.dhcpv4.requested_ip, buf, sizeof(buf))); snprintf(wifi.netmask, sizeof(wifi.netmask), "%s", net_addr_ntop(AF_INET, &iface->config.ip.ipv4->netmask, buf, sizeof(buf))); snprintf(wifi.gw, sizeof(wifi.gw), "%s", net_addr_ntop(AF_INET, &iface->config.ip.ipv4->gw, buf, sizeof(buf))); LOG(EDEBUG, "Your address: %s,Subnet: %s,Router: %s", wifi.ip, wifi.netmask, wifi.gw); wifi.connect_status = true; } static void wifi_event_handler(csk_wifi_event_t events, void *event_data, uint32_t data_len, void *arg) { if (events & CSK_WIFI_EVT_STA_CONNECTED) { // wifi.connect_status = true; LOG(EDEBUG, "[WiFi sta] connected"); } else if (events & CSK_WIFI_EVT_STA_DISCONNECTED) { wifi.connect_status = false; LOG(EDEBUG, "[WiFi sta] disconnected"); } else { abort(); } } __weak wifi_status_t esp32_c3_wifi_init(void) { uint8_t mac_addr[6] = {0}; /* CSK WiFi 驱动初始化 */ int rc = csk_wifi_init(); if (rc != 0) { LOG(EERROR, "wifi get mac failed, ret: %d\n", rc); return WIFI_ERROR; } rc = csk_wifi_get_mac(CSK_WIFI_MODE_STA, mac_addr); if (rc != 0) { LOG(EERROR, "wifi get mac failed, ret: %d\n", rc); return WIFI_ERROR; } snprintf(wifi.mac, sizeof(wifi.mac), "%x:%x:%x:%x:%x:%x", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); LOG(EDEBUG, "mac address:%s", wifi.mac); return (rc == 0)?WIFI_OK:WIFI_ERROR; } __weak wifi_status_t esp32_c3_wifi_uninit(void) { /* CSK WiFi 驱动初始化 */ int rc = csk_wifi_deinit(); return (rc == 0)?WIFI_OK:WIFI_ERROR; } __weak wifi_status_t esp32_c3_wifi_connect_ap(const char* ssid, const char* passwd) { /* 配置WiFi回调事件参数 */ wifi_event_cb.handler = &wifi_event_handler; wifi_event_cb.events = CSK_WIFI_EVT_STA_CONNECTED | CSK_WIFI_EVT_STA_DISCONNECTED; wifi_event_cb.arg = NULL; /* 注册WiFi回调事件 */ csk_wifi_add_callback(&wifi_event_cb); /* WiFi参数配置 */ csk_wifi_sta_config_t sta_config = {0}; snprintf(sta_config.ssid, sizeof(sta_config.ssid), "%s", ssid); snprintf(sta_config.pwd, sizeof(sta_config.pwd), "%s", passwd); sta_config.encryption_mode = CSK_WIFI_AUTH_WPA2_PSK; int retry_count = 0; do { LOG(EDEBUG, "connecting to wifi: %s ...", sta_config.ssid); /* 连接WiFi */ int ret = csk_wifi_sta_connect(&sta_config, &wifi_result, K_FOREVER); if (ret == 0) { snprintf(wifi.ssid, sizeof(wifi.ssid), "%s", ssid); snprintf(wifi.passwd, sizeof(wifi.passwd), "%s", passwd); break; } else { if (wifi_result == CSK_WIFI_ERR_STA_FAILED) { retry_count++; LOG(EERROR, "retry to connecting wifi ... %d", retry_count); } else { LOG(EERROR, "AP not found or invalid password"); return WIFI_ERROR; } } } while (retry_count < 10); if (retry_count >= 10) return WIFI_ERROR; /* 打印已连接WiFi信息 */ LOG(EDEBUG, "--------------------------Current AP info-------------------------------"); LOG(EDEBUG, "ssid: %s pwd: %s bssid: %s channel: %d rssi: %d\n", sta_config.ssid, sta_config.pwd, sta_config.bssid, sta_config.channel, sta_config.rssi); LOG(EDEBUG, "------------------------------------------------------------------------"); /* 初始化并注册 DHCP BOUND 事件,设备获取 ipv4 地址后产生回调 */ net_mgmt_init_event_callback(&dhcp_cb, handler_cb, NET_EVENT_IPV4_DHCP_BOUND); net_mgmt_add_event_callback(&dhcp_cb); struct net_if *iface = net_if_get_default(); if (!iface) { LOG(EDEBUG, "wifi interface not available"); return WIFI_ERROR; } /* 开启dhcp client,DHCP 用来分配 IP */ net_dhcpv4_start(iface); return WIFI_OK; } __weak wifi_status_t esp32_c3_wifi_disconnect_ap(void) { int rc = csk_wifi_sta_disconnect(&wifi_result, K_FOREVER); return (rc == 0)?WIFI_OK:WIFI_ERROR; } __weak bool esp32_c3_wifi_is_connect(void) { return wifi.connect_status; }
以上程序烧录后,串口打印的结果如下:
[wifi_core.c esp32_c3_wifi_init:94][EDEBUG]mac address:10:91:a8:3e:9b:54
[wifi_core.c esp32_c3_wifi_connect_ap:129][EDEBUG]connecting to wifi: mimi ...
[wifi_core.c esp32_c3_wifi_connect_ap:150][EDEBUG]--------------------------Current AP info-------------------------------
[wifi_core.c esp32_c3_wifi_connect_ap:151][EDEBUG]ssid: mimi pwd: xxxxxxxx bssid: 52:fc:57:12:03:ec channel: 1 rssi: -48
[wifi_core.c esp32_c3_wifi_connect_ap:154][EDEBUG]------------------------------------------------------------------------
[wifi_core.c handler_cb:56][EDEBUG]Your address: 192.168.43.96,Subnet: 255.255.255.0,Router: 192.168.43.1
验证了Wi-Fi能力后,接下来考虑的就是用户在使用时的配网怎样更加方便。
印象中,买过的一些不带屏的硬件产品,其中之一的配网流程如下:
当然除了以上的方式还有ESP32-C3支持的AirKiss、EspTouch、微信小程序、声波配网等。打算使用提供的wifi\_ap的案例,完成AP配网模式验证下效果。
这个案例在这里,通过以下命令创建,编译烧录即可,注意编译时选择csk6011a\\_c3\\_nano作为boards编译。
程序运行后,通过手机搜索ESP的设备SSID,点击连接。但是这里的Demo提供的是不带DHCP服务的,不能自动给设备自动分配IP,需要手机手动设置IP后才能正常连接到设备上。记得ESP32-C3是有的,不知道SDK是否提供了DHCP服务的能力
。
以上的AP配网由于Demo没有提供自动给客户端分配IP的功能,没再继续找相关接口,想了下还有BLE的能力,但是需要手机App,也会比较麻烦。算了,这块还是问下聆思的大胸弟们吧
虽然暂时AP和BLE都不使用了,但是作为开发人员,需要用到网络功能的时候,总不能把Wi-Fi的SSID和密码都写死在设备上吧,下次丢给其他人调试网络功能,也会很不方便,还要改密码,因此想到了SDK提供的串口命令功能,咱们可以设计一个wifi connect命令,来动态配网,接下来就试一下。
看到了在开发文档开发实践->Shell的使用提供了一个Wi-Fi的案例,但未具体实现Wi-Fi配网的功能,接下来就实现一下。最终的效果是设备启动后,命令行输入tab可以看到wifi的指令,通过在命令行输出wifi connect ssid passwd 1
,即可一键动态连接到AP网络,省的还要改代码进行联网了
uart:~$
clear device devmem flash help history kernel pwm
resize shell wifi
uart:~$ wifi connect mimi 12348765 1
[shell_command.c cmd_wifi_connect:75][EDEBUG]cmd_wifi_connect ssid: mimi psw: 12348765 save: 1
[wifi_core.c esp32_c3_wifi_init:94][EDEBUG]mac address:10:91:a8:3e:9b:54
[wifi_core.c esp32_c3_wifi_connect_ap:129][EDEBUG]connecting to wifi: mimi ...
[wifi_core.c esp32_c3_wifi_connect_ap:150][EDEBUG]--------------------------Current AP info-------------------------------
[wifi_core.c esp32_c3_wifi_connect_ap:151][EDEBUG]ssid: mimi pwd: xxxxxxxx bssid: 52:fc:57:12:03:ec channel: 1 rssi: -48
[wifi_core.c esp32_c3_wifi_connect_ap:154][EDEBUG]------------------------------------------------------------------------
[wifi_core.c handler_cb:56][EDEBUG]Your address: 192.168.43.96,Subnet: 255.255.255.0,Router: 192.168.43.1
以上的功能通过shell\\_command.c中完成,shell\\_command.c如下:
/* connect 指令处理函数,附带wifi连接的参数 */ static int cmd_wifi_connect(const struct shell *shell, size_t argc, char **argv) { int rc = 0; LOG(EDEBUG, "cmd_wifi_connect ssid: %s psw: %s save: %d \n", argv[1], argv[2], atoi(argv[3])); // 判断是否已经初始化Wi-Fi模块 if (!wifi.connect_status) { if (WIFI_OK != wifi.init()) { LOG(EERROR, "wifi init failed"); return -1; } // 连接AP热点 if (WIFI_OK != wifi.connect_ap(argv[1], argv[2])) { LOG(EERROR, "wifi connect ap ssid:%s failed", argv[1]); return -1; } } return rc; } /// @brief 添加wifi命令的子命令(connect) SHELL_STATIC_SUBCMD_SET_CREATE(init_deinit_command, SHELL_CMD(connect, NULL, "<ssid> <pwd> <save>, example:mimi 12345678 1", cmd_wifi_connect), SHELL_SUBCMD_SET_END /* Array terminated. */ ); /// @brief 添加WiFi命令集的根命令 SHELL_CMD_REGISTER(wifi, &init_deinit_command, "wifi command", NULL);
通过以上的命令满足了对于开发人员的配网友好度。但还有个问题,设备掉电不会重连,下次还要重新输入进行配网。因此准备再次验证板载的16M SPI Flash和littlefs文件系统,当输入wifi账号密码连接成功后,把连接的信息保存到Flash中,下次上电执行自动重连机制。
这个部分是借用littlefs,保存Wi-Fi账号密码到Flash,系统上电后,会读取Flash中的Wi-Fi账号密码,自动联网。
这块的文档在开发实践->文件系统的使用章节,注意这里需要使用mklittlefs工具制作一个文件系统镜像,然后再通过命令烧入到板子的相关分区,最后还要根据csk6011a\_nano.overlay提供的文档,配置设备树的挂载点和系统固定分区,写入你烧录的地址和空间即可。
完成后,需要写一个测试Demo验证下挂载、遍历文件系统目录、创建删除文件、读写文件的案例来验证功能的完整性,完成这些功能验证的代码为flash\_test.c,内容如下:
#include <log.h> #include <zephyr/fs/fs.h> #include <flash_core.h> #define MAX_PATH_LEN 255 #define FLASH_MAX_DATA_LEN 256 #define FLASH_TEST_DATA "this is a flash read/write test data" #define FLASH_TEST_FILE_NAME "/lfs/test.txt" void flash_test() { int rc; struct fs_dirent dirent; char* fname = FLASH_TEST_FILE_NAME; char test_data[FLASH_MAX_DATA_LEN] = {0}; struct fs_file_t file = {0}; /*!< 初始化结构体 */ // 初始化挂载flash设备到/lfs节点 user_flash_init(); /* 如果文件先删除 */ fs_unlink(fname); /* 打开文件 */ rc = fs_open(&file, fname, FS_O_CREATE | FS_O_RDWR); if (rc < 0) { LOG(EERROR, "FAIL: open %s: %d", fname, rc); return ; } /* 获取文件状态信息 */ rc = fs_stat(fname, &dirent); if (rc < 0) { LOG(EERROR, "FAIL: stat %s: %d", fname, rc); goto out; } /* 写文件内容 */ rc = fs_write(&file, FLASH_TEST_DATA, strlen(FLASH_TEST_DATA)); if (rc < 0) { LOG(EERROR, "FAIL: write %s: %d", fname, rc); } /* 重定向文件读写指针到开始 */ rc = fs_seek(&file, 0, FS_SEEK_SET); if (rc < 0) { LOG(EERROR, "FAIL: seek %s: %d", fname, rc); goto out; } /* 读文件内容 */ rc = fs_read(&file, test_data, sizeof(test_data)); if (rc < 0) { LOG(EERROR, "FAIL: read %s: [rd:%d]", fname, rc); goto out; } LOG(EDEBUG, "read from %s, data:%s", fname, test_data); out: fs_close(&file); }
flash\\_test.c文件调用的封装层flash\\_core.c/h内容如下。
flash\_core.h文件内容:
#ifndef _FLASH_CORE_H #define _FLASH_CORE_H /** * @brief flash文件系统初始化,根目录为/lfs1,初始化之后,可以使用zephyr的文件系统API接口访问文件 * @return 0成功,其他值失败 */ int user_flash_init(void); /** * @brief 注销flash文件系统 * @return 0成功,其他值失败 */ int user_flash_uninit(void); #endif
flash\_core.c文件内容如下:
#include <zephyr/zephyr.h> #include <zephyr/device.h> #include <zephyr/fs/fs.h> #include <zephyr/fs/littlefs.h> #include <zephyr/storage/flash_map.h> #include <delay.h> #include <log.h> #include <flash_core.h> /// 文件系统设备树信息 #ifdef CONFIG_APP_LITTLEFS_STORAGE_FLASH static int littlefs_flash_erase(unsigned int id) { const struct flash_area *pfa; int rc; rc = flash_area_open(id, &pfa); if (rc < 0) { LOG(EERROR, "FAIL: unable to find flash area %u: %d\n", id, rc); return rc; } LOG(EDEBUG, "Area %u at 0x%x on %s for %u bytes\n", id, (unsigned int)pfa->fa_off, pfa->fa_dev_name, (unsigned int)pfa->fa_size); /* Optional wipe flash contents */ if (IS_ENABLED(CONFIG_APP_WIPE_STORAGE)) { rc = flash_area_erase(pfa, 0, pfa->fa_size); LOG(EERROR, "Erasing flash area ... %d", rc); } flash_area_close(pfa); return rc; } #define PARTITION_NODE DT_NODELABEL(lfs1) #if DT_NODE_EXISTS(PARTITION_NODE) FS_FSTAB_DECLARE_ENTRY(PARTITION_NODE); #else /* PARTITION_NODE */ FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(storage); static struct fs_mount_t lfs_storage_mnt = { .type = FS_LITTLEFS, .fs_data = &storage, .storage_dev = (void *)FLASH_AREA_ID(storage), .mnt_point = "/lfs", }; #endif /* PARTITION_NODE */ struct fs_mount_t *mp = #if DT_NODE_EXISTS(PARTITION_NODE) &FS_FSTAB_ENTRY(PARTITION_NODE) #else &lfs_storage_mnt #endif ; static int littlefs_mount(struct fs_mount_t *mp) { int rc; rc = littlefs_flash_erase((uintptr_t)mp->storage_dev); if (rc < 0) { return rc; } /* Do not mount if auto-mount has been enabled */ #if !DT_NODE_EXISTS(PARTITION_NODE) || \ !(FSTAB_ENTRY_DT_MOUNT_FLAGS(PARTITION_NODE) & FS_MOUNT_FLAG_AUTOMOUNT) rc = fs_mount(mp); if (rc < 0) { LOG(EDEBUG, "FAIL: mount id %" PRIuPTR " at %s: %d\n", (uintptr_t)mp->storage_dev, mp->mnt_point, rc); return rc; } LOG(EDEBUG, "%s mount: %d\n", mp->mnt_point, rc); #else LOG(EDEBUG, "%s automounted\n", mp->mnt_point); #endif return 0; } #endif /* CONFIG_APP_LITTLEFS_STORAGE_FLASH */ #ifdef CONFIG_APP_LITTLEFS_STORAGE_BLK_SDMMC struct fs_littlefs lfsfs; static struct fs_mount_t __mp = { .type = FS_LITTLEFS, .fs_data = &lfsfs, .flags = FS_MOUNT_FLAG_USE_DISK_ACCESS, }; struct fs_mount_t *mp = &__mp; static int littlefs_mount(struct fs_mount_t *mp) { static const char *disk_mount_pt = "/"CONFIG_SDMMC_VOLUME_NAME":"; static const char *disk_pdrv = CONFIG_SDMMC_VOLUME_NAME; mp->storage_dev = (void *)disk_pdrv; mp->mnt_point = disk_mount_pt; return fs_mount(mp); } #endif /* CONFIG_APP_LITTLEFS_STORAGE_BLK_SDMMC */ static int lsdir(const char *path) { int res; struct fs_dir_t dirp; static struct fs_dirent entry; fs_dir_t_init(&dirp); /* Verify fs_opendir() */ res = fs_opendir(&dirp, path); if (res) { LOG(EERROR, "Error opening dir %s [%d]", path, res); return res; } LOG(EDEBUG, "Listing dir %s ...", path); for (;;) { /* Verify fs_readdir() */ res = fs_readdir(&dirp, &entry); /* entry.name[0] == 0 means end-of-dir */ if (res || entry.name[0] == 0) { if (res < 0) { LOG(EERROR, "Error reading dir [%d]", res); } break; } if (entry.type == FS_DIR_ENTRY_DIR) { LOG(EDEBUG, "[DIR ] %s", entry.name); } else { LOG(EDEBUG, "[FILE] %s (size = %zu)", entry.name, entry.size); } } /* Verify fs_closedir() */ fs_closedir(&dirp); return res; } int user_flash_init() { static bool is_init = false; struct fs_statvfs sbuf; int rc; if (is_init) return 0; /* 挂载文件系统 */ rc = littlefs_mount(mp); if (rc < 0) { LOG(EERROR, "fs mount failed."); return -1; } /* 检索文件系统的信息,返回文件系统中的总空间和可用空间。 */ rc = fs_statvfs(mp->mnt_point, &sbuf); if (rc < 0) { LOG(EERROR, "FAIL: statvfs: %d", rc); return -1; } LOG(EDEBUG, "%s: bsize = %lu ; frsize = %lu ;" " blocks = %lu ; bfree = %lu", mp->mnt_point, sbuf.f_bsize, sbuf.f_frsize, sbuf.f_blocks, sbuf.f_bfree); /* 检索文件系统的目录 */ rc = lsdir(mp->mnt_point); if (rc < 0) { LOG(EERROR, "FAIL: lsdir %s: %d\n", mp->mnt_point, rc); } if (rc == 0) is_init=true; return rc; } int user_flash_uninit(void) { /* 卸载文件系统 */ int rc = fs_unmount(mp); LOG(EDEBUG, "%s unmount: %d", mp->mnt_point, rc); return rc; }
以上代码编译后,串口打印的结果如下,说明读写数据正常:
[flash_core.c littlefs_flash_erase:26][EDEBUG]Area 0 at 0x700000 on FLASH_CTRL for 1048576 bytes
[flash_core.c littlefs_mount:81][EDEBUG]/lfs1 automounted
[flash_core.c user_flash_init:172][EDEBUG]/lfs1: bsize = 16 ; frsize = 4096 ; blocks = 256 ; bfree = 253
[flash_core.c lsdir:125][EDEBUG]Listing dir /lfs1 ...
[flash_core.c lsdir:141][EDEBUG][FILE] boot_count (size = 1)
[flash_core.c lsdir:141][EDEBUG][FILE] pattern.bin (size = 547)
[flash_core.c lsdir:141][EDEBUG][FILE] test.txt (size = 36)
[flash_test.c flash_test:75][EDEBUG]read from /lfs1/test.txt, data:this is a flash read/write test data
接下来重新实现下shell\_command.c文件,使得命令行支持连接成功wifi后,保存到flash,命令如下
wifi connect ssid passwd 1
其中最后的1表示save保存到Flash,0表示不保存到Flash。
shell\_command.c的改造如下:
#include <cJSON_user_define.h> static int cmd_write_to_flash(const char* pathname, const char* data) { int rc; ssize_t size; struct fs_file_t file = {0}; /*!< 初始化结构体 */ // 初始化flash user_flash_init(); // 写入文件 rc = fs_open(&file, pathname, FS_O_CREATE | FS_O_WRITE); if (rc < 0) { LOG(EERROR, "FAIL: open %s: %d", pathname, rc); return -1; } /* 写入文件 */ size = fs_write(&file, data, strlen(data)); if (size < 0) { LOG(EERROR, "FAIL: write %s: %d", pathname, rc); goto out; } LOG(EDEBUG, "write %s to file %s successed", data, pathname); out: fs_close(&file); return rc; } /* connect 指令处理函数,附带wifi连接的参数 */ static int cmd_wifi_connect(const struct shell *shell, size_t argc, char **argv) { int rc = 0; char json_data[100] = {0}; int data_len = sizeof(json_data); LOG(EDEBUG, "cmd_wifi_connect ssid: %s psw: %s save: %d \n", argv[1], argv[2], atoi(argv[3])); // 判断是否已经初始化Wi-Fi模块 if (!wifi.connect_status) { if (WIFI_OK != wifi.init()) { LOG(EERROR, "wifi init failed"); return -1; } // 连接AP热点 if (WIFI_OK != wifi.connect_ap(argv[1], argv[2])) { LOG(EERROR, "wifi connect ap ssid:%s failed", argv[1]); return -1; } } // 判断是否需要把SSID和Password保存到Flash中 if (atoi(argv[3]) == 1) { // 构造json数据,格式为 // { // "ssid": "xxxxxx", // "passwd": "xxxxxx" // } JSON_SERIALIZE_CREATE_OBJECT_START(json_root_obj); JSON_SERIALIZE_ADD_STRING_TO_OBJECT(json_root_obj, "ssid", argv[1]); JSON_SERIALIZE_ADD_STRING_TO_OBJECT(json_root_obj, "passwd", argv[2]); JSON_SERIALIZE_STRING(json_root_obj, json_data, data_len); JSON_SERIALIZE_CREATE_END(json_root_obj); rc = cmd_write_to_flash(SHELL_COMMAND_WIFI_AP_INFO_PATHNAME, json_data); if (0 != rc) { LOG(EERROR, "can not write %s to %s", json_data, SHELL_COMMAND_WIFI_AP_INFO_PATHNAME); } } return rc; } /// @brief 添加wifi命令的子命令(connect) SHELL_STATIC_SUBCMD_SET_CREATE(init_deinit_command, SHELL_CMD(connect, NULL, "<ssid> <pwd> <save>, example:mimi 12345678 1", cmd_wifi_connect), SHELL_SUBCMD_SET_END /* Array terminated. */ ); /// @brief 添加WiFi命令集的根命令 SHELL_CMD_REGISTER(wifi, &init_deinit_command, "wifi command", NULL); void shell_command_init(void) { #if defined(CONFIG_USB_UART_CONSOLE) const struct device *dev; uint32_t dtr = 0; /* 获取Shell设备实例 */ dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_shell_uart)); if (!device_is_ready(dev) || usb_enable(NULL)) { LOG(EDEBUG, "device is not ready"); return; } while (!dtr) { uart_line_ctrl_get(dev, UART_LINE_CTRL_DTR, &dtr); k_sleep(K_MSEC(100)); } #endif }
改造后,编译烧录代码,在串口终端执行wifi connect ssid passwd 1
命令后的打印结果如下:
uart:~$
clear device devmem flash help history kernel pwm
resize shell wifi
uart:~$ wifi connect mimi 12348765 1
[shell_command.c cmd_wifi_connect:75][EDEBUG]cmd_wifi_connect ssid: mimi psw: 12348765 save: 1
[shell_command.c cmd_wifi_connect:98][EDEBUG]start json
[shell_command.c cmd_wifi_connect:104][EDEBUG]json data:{"ssid":"mimi","passwd":"xxxxxxxx"}
[shell_command.c cmd_write_to_flash:59][EDEBUG]write {"ssid":"mimi","passwd":"xxxxxxxx"} to file /lfs/wifi.conf successed
可以看到,成功写入了flash,设备掉电后,就可以读取Flash中保存的wifi信息进行自动联网啦
需要注意的是,这里使用了json格式保存到Flash中的,使用的是cJson库,并对cJson做了封装。本章节所有的代码实现可能不能一一介绍,最后统一放到文章最后提供。
考虑到有Wi-Fi模块,本来想把之前通过USB显示的AI识别信息,使用视频流的方式通过Wi-Fi模块发送到网页端显示,这样就不用多连接一根USB线了。
但是这个对Wi-Fi的速率有要求,已经考虑实用场景可能会传输音视频,那么传输的稳定性也需要有保障。接下来是测试过程。
首先lisa zep create提供了weboscket的案例,但是websocket服务并不是太好搭建,咱们可以使用比较常用的tcp和udp服务器工具作为测试验证。类似这种:
我这里在sdk中找了一个udp的测试案例,代码在csk-sdk/zephyr/samples/net/sockets/tcp,因为是本地测速验证,使用了udp验证。
先说结论,测试的udp速度为220KB/s左右,使用的手机热点,靠近ESP32-C3模块没什么遮挡的情况下。另外测试代码为在主线程每隔1ms向网络发送一个udp报文,报文的长度是1024字节。你要问我为什么是这个长度,为什么要延迟,暂时我也不太清楚 ,原因是如果不做延迟处理,主线程会阻塞,不会发送数据出去,另外如果不使用1024,系统会崩溃,这个可能是我CONFIG配置堆栈大小的问题,由于时间不多了,小姐姐多次和我提到deadline,这个后续再继续优化验证。以下提供下测试的代码和CONFIG配置
udp\_test.c
void udp_test() { const char* send_msg = "this is a udp test message."; const char* server_ip = "192.168.43.66"; const short server_port = 8888; struct sockaddr_in addr = {0}; // 等待Wi-Fi连接成功,实际实用场景使用信号量等阻塞方式,不使用下面这种轮询占用CPU资源 while(!wifi.connect_status) { delay_ms(100); } // 初始化本地socket,获取套接字描述符 int fd = socket(AF_INET, SOCK_DGRAM, 0); if (fd < 0) { LOG(EERROR, "socket init failed"); return ; } LOG(EDEBUG, "udp init successed"); // 配置UDP服务端IP和端口号 net_sin((struct sockaddr *)&addr)->sin_family = AF_INET; net_sin((struct sockaddr *)&addr)->sin_port = htons(server_port); inet_pton(AF_INET, server_ip, &net_sin((struct sockaddr *)&addr)->sin_addr); // 速度测试 for (;;) { ssize_t len = sendto(fd, message, 1024, 0, (struct sockaddr*)&addr, sizeof(addr)); if (len < 0) { LOG(EERROR, "socket send failed, errorcode:%d", len); } delay_ms(1); } }
虽然以上测试不太规范,但是220KB/s的速度已经可以传输双声道16K 16bit的PCM数据没有问题了,如果是图像帧数据的应用场景,需要使用网络传输的话,就需要考虑怎样优化了,实际情况ESP32-C3代码和天线方案优化好了,肯定不止220KB/s,按照之前经验,实际测试应该能到800-900KB/s的速率,传输一些低码率的视频流应该也是可以的了。
本实验也遇到了一个严重的问题,就是一旦加入网络通信的配置,当烧录完程序后,会有固定30秒的延迟才会执行到main函数,不知道是为什么,打开了CONFIG\\_WIFI\\_LOG\\_LEVEL\\_DBG=y也没有看到相关的打印日志。
硬件产品离不开指示灯,指示灯一般有亮、灭、闪烁和呼吸效果,使用SDK+定时器实现了LED的这些功能,测试代码如下:
#include <zephyr/kernel.h> #include <delay.h> #include <led_core.h> void led_test() { led.on(GREEN_LED); delay_ms(1000); led.off(GREEN_LED); delay_ms(1000); // 绿色LED每隔100ms闪烁一次,共闪烁5秒钟的时间 led.blink(GREEN_LED, 100, 3000); delay_ms(3000); // 绿色LED每隔1000ms呼吸一次,共呼吸5秒钟的时间 led.breathing(GREEN_LED, 1000, 0); }
代码实现如下(亮灭部分可以使用GPIO的方式)
led\_core.h的内容如下:
#ifndef _LED_SENSOR_H #define _LED_SENSOR_H /// \brief LED的类型,目前板载只有绿色LED typedef enum { GREEN_LED, } led_type_t; /// \brief LED灯状态,有开关、闪烁和呼吸4个状态 /// /// 其中的绿色LED不支持呼吸灯的效果,使用时需注意 typedef enum { LED_OFF = 1, LED_ON = 2, LED_BLINK = 3, LED_BREATHING = 4, }led_status_t; /// \brief LED灯当前信息 typedef struct { led_type_t led_type; led_status_t led_status; int interval; int duration; int current_interval; int current_duration; int pwm_flap; int pwm_cnt; }led_info_t; /** * @brief * LED灯的亮灭、闪烁、呼吸的控制接口 */ typedef struct { /*!< @brief 绿色LED信息 */ led_info_t green_led; /*!< @brief LED周期 */ int period; /// LED闪烁定时器 void* blink_timer; /// LED呼吸定时器 void* breathe_timer; /** * @brief 点亮LED灯 * * @param led 要被点亮的led灯,可以是red_led和green_led * @return */ void (*on)(led_type_t led_type); /** * @brief 熄灭LED灯 * * @param led 要被熄灭的led灯,可以是red_led和green_led * @return */ void (*off)(led_type_t led_type); /** * @brief LED灯闪烁 * * @param led 闪烁的led灯,可以是red_led和green_led * @param interval 闪烁间隔时间,以毫秒为单位 * @param duration 灯闪烁或呼吸持续的时间,以毫秒为单位,开关LED不关心这个值 * @return */ void (*blink)(led_type_t led_type, int interval, int duration); /** * @brief LED呼吸灯 * * @param led 呼吸的led灯,注意仅red_led支持呼吸灯 * @param interval 灯间隔时间,以毫秒为单位,开关LED不关心这个值 * @param duration 灯闪烁或呼吸持续的时间,以毫秒为单位,开关LED不关心这个值 * @return */ void (*breathing)(led_type_t led_type, int interval, int duration); }led_core_t; /*! @brief led灯单例 */ extern led_core_t led; #endif
led\_core.c的内容如下:
#include <zephyr/zephyr.h> #include <zephyr/kernel.h> #include <zephyr/device.h> #include <zephyr/drivers/gpio.h> #include <zephyr/drivers/pwm.h> #include <log.h> #include <delay.h> #include <led_core.h> #define LED_DEFAULT_INTERVAL 20U #define LED_MIN_PERIOD PWM_MSEC(1U) #define LED_MAX_PERIOD PWM_MSEC(LED_DEFAULT_INTERVAL) #define LED_TIMER_INIT(timer, timer_expiry, timer_stop) \ static struct k_timer timer; \ k_timer_init(&timer, timer_expiry, timer_stop); \ led.timer = (void*)&timer; /* 通过别名获取 "led0" 设备树 node id */ #define LED0_NODE DT_ALIAS(led0) /* 通过 node id 获取 led0 设备树信息 */ static const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(LED0_NODE, gpios); /* 获取设备树配置 */ static const struct pwm_dt_spec pwm_led0 = PWM_DT_SPEC_GET(DT_ALIAS(pwm_led0)); /// 接口实现声明 __weak void csk6011a_nano_led_on(led_type_t led_type); __weak void csk6011a_nano_led_off(led_type_t led_type); __weak void csk6011a_nano_led_blink(led_type_t led_type, int interval, int duration); __weak void csk6011a_nano_led_breathing(led_type_t led_type, int interval, int duration); // led单例初始化 led_core_t led = { .green_led = {GREEN_LED, LED_OFF, 0, 0, 0, 0, 0, 0}, .on = csk6011a_nano_led_on, .off = csk6011a_nano_led_off, .blink = csk6011a_nano_led_blink, .breathing = csk6011a_nano_led_breathing, .period = LED_MAX_PERIOD, }; static void blink_timer_expiry(struct k_timer *timer) { /* 关闭LED闪烁 */ csk6011a_nano_led_off(GREEN_LED); // LOG(EDEBUG, "blink_timer_expiry called"); } static void blink_timer_stop(struct k_timer *timer) { // LOG(EDEBUG, "blink_timer_stop called"); } static void breathe_timer_expiry(struct k_timer *timer) { if(led.green_led.pwm_flap)led.green_led.pwm_cnt -= led.green_led.interval; else led.green_led.pwm_cnt += led.green_led.interval; if(led.green_led.pwm_cnt >= led.period) { led.green_led.pwm_cnt = led.period - 1; led.green_led.pwm_flap = 1; } if(led.green_led.pwm_cnt <= 0) { led.green_led.pwm_cnt = 1; led.green_led.pwm_flap = 0; } pwm_set_dt(&pwm_led0, led.period, led.green_led.pwm_cnt); if (led.green_led.duration > 0) { led.green_led.current_duration += LED_DEFAULT_INTERVAL; if (led.green_led.current_duration >= led.green_led.duration) { k_timer_stop((struct k_timer *)led.breathe_timer); } } } static void breathe_timer_stop(struct k_timer *timer) { // LOG(EDEBUG, "breathe_timer_stop"); } static void csk6011a_nano_led_init(void) { static bool is_init = false; if (!is_init) { if (!device_is_ready(led0.port)) { LOG(EERROR, "led0 is not ready"); return ; } if (!device_is_ready(pwm_led0.dev)) { LOG(EERROR, "Error: PWM device %s is not ready", pwm_led0.dev->name); return ; } /* 配置LED闪烁定时器 */ LED_TIMER_INIT(blink_timer, blink_timer_expiry, blink_timer_stop); /* 配置LED呼吸灯定时器 */ LED_TIMER_INIT(breathe_timer, breathe_timer_expiry, breathe_timer_stop); is_init = true; } } __weak void csk6011a_nano_led_on(led_type_t led_type) { csk6011a_nano_led_init(); if (led_type == GREEN_LED) { pwm_set_dt(&pwm_led0, led.period, 1); // TODO: 按照以下设置后,无法使用新的周期设置占空比,后续使用pwm_cycles_to_usec重新初始化尝试 // pwm_set_dt(&pwm_led0, PWM_SEC(1U), 1); // gpio_pin_configure_dt(&led0, GPIO_OUTPUT_INACTIVE); } } __weak void csk6011a_nano_led_off(led_type_t led_type) { csk6011a_nano_led_init(); if (led_type == GREEN_LED) { pwm_set_dt(&pwm_led0, led.period, led.period-1); // pwm_set_dt(&pwm_led0, PWM_SEC(1U), PWM_SEC(1U)-1); // gpio_pin_configure_dt(&led0, GPIO_OUTPUT_ACTIVE); } } __weak void csk6011a_nano_led_blink(led_type_t led_type, int interval, int duration) { csk6011a_nano_led_init(); if (led_type == GREEN_LED) { k_timer_stop((struct k_timer *)led.blink_timer); k_timer_stop((struct k_timer *)led.breathe_timer); pwm_set_dt(&pwm_led0, PWM_MSEC(interval), PWM_MSEC(interval)/2); if (duration > 0) { k_timer_start((struct k_timer *)led.blink_timer, K_MSEC(duration), K_MSEC(0)); } } } __weak void csk6011a_nano_led_breathing(led_type_t led_type, int interval, int duration) { csk6011a_nano_led_init(); if (led_type == GREEN_LED) { led.green_led.led_status = LED_BREATHING; led.green_led.interval = (int)((1000.0/interval)*(led.period/(1000/LED_DEFAULT_INTERVAL))); led.green_led.duration = duration; led.green_led.current_duration = 0; led.green_led.current_interval = 0; led.green_led.pwm_flap = 0; led.green_led.pwm_cnt = 0; k_timer_stop((struct k_timer *)led.blink_timer); k_timer_stop((struct k_timer *)led.breathe_timer); k_timer_start((struct k_timer *)led.breathe_timer, K_MSEC(LED_DEFAULT_INTERVAL), K_MSEC(LED_DEFAULT_INTERVAL)); } }
BUTTON的原理图
按照以上原理图无法保证机械抖动产生的异常中断,产生的现象就是你以为只产生一次中断,但实际可能是多次,导致程序可能误判,导致执行逻辑错误。
解决的办法就是软件消抖,比较好的处理方式就是定时器,这个需要多次尝试,看示波器波形,看第一次抖动到最后一次抖动的最长间隔时间,设置一个最大的定时器值就可以了,这里使用的是10ms,保证只执行到最后一次中断产生的定时器,则任务按键中断发生的原理。
测试代码如下,屏蔽了dts的配置,方便应用人员开发应用逻辑。
#include <log.h> #include <key_core.h> #include <user_test.h> void on_key_event(key_event_t event, void* user_data) { switch(event) { case KEY_EVENT_RELEASE: LOG(EDEBUG, "key is released."); break; case KEY_EVENT_PRESSED: LOG(EDEBUG, "key is pressed."); break; case KEY_EVENT_LONG_PRESSED: LOG(EDEBUG, "key is long pressed."); break; default: LOG(EDEBUG, "key event is error."); break; } } void key_test() { // 经过消抖的按键按下、抬起、长按功能测试 key.init(); key.event_register(on_key_event, NULL); }
具体实现由key\\_core.c/h完成,key\\_core.h内容如下。
#ifndef KEY_CORE_H #define KEY_CORE_H #include <stdbool.h> /// 按键消抖延迟事件,根据实际硬件情况更改,机械按键推荐5-10毫秒 #define KEY_DEJITTER_TIME 10 /// 长按事件的时间间隔 #define KYE_LONG_PRESSED_TIME 3000 /// \brief 支持的按键事件 typedef enum { KEY_EVENT_RELEASE, KEY_EVENT_PRESSED, KEY_EVENT_LONG_PRESSED, } key_event_t; /// 定义按键的事件回调函数指针 typedef void (*on_key_event_t)(key_event_t event, void* user_data); /** * @brief 按键用户核心层接口 * * @details * 实现了按键的引脚配置,使用init完成; * 实现了按键的事件注册,支持的事件 */ typedef struct key_ { /// 记录按键当前的状态 key_event_t event; /// 引脚配置句柄 void* gpio_handle; /// 按键消抖定时器 void* dejitter_timer; /// 长按判断定时器 void* long_pressed_timer; /// 按键事件回调函数指针 on_key_event_t on_event; void* user_data; /** * @brief 按键初始化 * * @return 成功返回true, 失败返回false */ bool (*init)(void); /** * @brief 按键逆初始化 * */ void (*uninit)(void); /** * @brief: 注册按键事件 * * @param on_key_event 按键事件 * @param user_data 向按键回调提供的用户私有数据 * * @return 成功返回true,识别返回false */ bool (*event_register)(on_key_event_t on_key_event, void* user_data); /** * @brief: 反注册按键事件 * */ void (*event_unregister)(void); } user_key_t; /*!< 按键单例 */ extern user_key_t key; #endif
key\_core.c内容如下:
#include <zephyr/zephyr.h> #include <zephyr/kernel.h> #include <zephyr/device.h> #include <zephyr/drivers/gpio.h> #include <log.h> #include <key_core.h> #define SW0_NODE DT_ALIAS(sw0) #if !DT_NODE_HAS_STATUS(SW0_NODE, okay) #error "Unsupported board: sw0 devicetree alias is not defined" #endif static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET_OR(SW0_NODE, gpios, {0}); static struct gpio_callback button_cb_data; /// 接口实现声明 __weak bool csk6011a_nano_key_init(void); __weak void csk6011a_nano_key_uninit(void); __weak bool csk6011a_nano_key_event_register(on_key_event_t on_key_event, void* user_data); __weak void csk6011a_nano_key_event_unregister(void); /// @brief 按键单例初始化 user_key_t key = { .event = KEY_EVENT_RELEASE, /*!< 初始化引脚为抬起状态 */ .init = csk6011a_nano_key_init, .uninit = csk6011a_nano_key_uninit, .event_register = csk6011a_nano_key_event_register, .event_unregister = csk6011a_nano_key_event_unregister, .gpio_handle = (void*)&button, /*!< 初始化引脚配置的句柄 */ }; static void dejitter_timer_expiry(struct k_timer *timer) { /* 更新电平状态 */ int level = gpio_pin_get_dt(&button); if (level == 0) key.event = KEY_EVENT_RELEASE; else if (level == 1) key.event = KEY_EVENT_PRESSED; /* 上报用户按键事件 */ if (key.on_event) { key.on_event(key.event, key.user_data); } // LOG(EDEBUG, "Button pressed at %u, level:%d", // k_cycle_get_32(), gpio_pin_get_dt(&button)); } static void dejitter_timer_stop(struct k_timer *timer) { // LOG(EDEBUG, "dejitter_timer_stop"); } static void long_pressed_timer_expiry(struct k_timer *timer) { /* 上报用户按键事件 */ key.event = KEY_EVENT_LONG_PRESSED; if (key.on_event) { key.on_event(key.event, key.user_data); } } static void long_pressed_timer_stop(struct k_timer *timer) { // LOG(EDEBUG, "long_pressed_timer_stop"); } static void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { /* 长按判断逻辑,按键按下启动长按定时器,抬起关闭 */ int level = gpio_pin_get_dt(&button); if (level == 0) { k_timer_stop((struct k_timer *)key.long_pressed_timer); } else if (level == 1) { k_timer_start((struct k_timer *)key.long_pressed_timer, K_MSEC(KYE_LONG_PRESSED_TIME), K_MSEC(0)); } /* 触发定时器, 使用10ms间隔进行按键消抖,定时器只触发一次 */ k_timer_start((struct k_timer *)key.dejitter_timer, K_MSEC(10), K_MSEC(0)); } __weak bool csk6011a_nano_key_init() { int32_t ret; /* 检查硬件设备是否就绪 */ if (!device_is_ready(button.port)) { LOG(EERROR, "Error: key %s is not ready", button.port->name); return false; } /* 配置引脚功能为输入模式 */ ret = gpio_pin_configure_dt(&button, GPIO_INPUT | GPIO_PULL_DOWN); if (ret != 0) { LOG(EERROR, "Error %d: failed to configure %s pin %d", ret, button.port->name, button.pin); return false; } /* 配置引脚中断模式,支持上升沿和下降沿触发 */ ret = gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_BOTH); if (ret != 0) { LOG(EERROR, "Error %d: failed to configure interrupt on %s pin %d", ret, button.port->name, button.pin); return false; } /* 配置按键定时器 */ static struct k_timer dejitter_timer; k_timer_init(&dejitter_timer, dejitter_timer_expiry, dejitter_timer_stop); key.dejitter_timer = (void*)&dejitter_timer; /* 配置按键定时器 */ static struct k_timer long_pressed_timer; k_timer_init(&long_pressed_timer, long_pressed_timer_expiry, long_pressed_timer_stop); key.long_pressed_timer = (void*)&long_pressed_timer; /* 根据原理图设置默认电平状态,保存按键设备树操作接口 */ key.event = (gpio_pin_get_dt(&button) == 1)?KEY_EVENT_PRESSED:KEY_EVENT_RELEASE; key.gpio_handle = (void*)&button; return true; } __weak void csk6011a_nano_key_uninit() { k_timer_stop((struct k_timer *)key.dejitter_timer); k_timer_stop((struct k_timer *)key.long_pressed_timer); key.init = NULL; key.uninit = NULL; key.event_register = NULL; key.event_unregister = NULL; key.gpio_handle = NULL; key.event = KEY_EVENT_RELEASE; } __weak bool csk6011a_nano_key_event_register(on_key_event_t on_key_event, void* user_data) { int32_t ret; /* 初始化中断回调 */ gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin)); /* 注册回调事件 */ ret = gpio_add_callback(button.port, &button_cb_data); LOG(EDEBUG, "Set up button at %s pin %d", button.port->name, button.pin); key.on_event = on_key_event; key.user_data = user_data; return (ret==0)?true:false; } __weak void csk6011a_nano_key_event_unregister() { gpio_remove_callback(button.port, &button_cb_data); }
烧录后,短按、长按不再会出现抖动,并且支持短按和长按的动作识别:
[key_test.c on_key_event:12][EDEBUG]key is pressed.
[key_test.c on_key_event:15][EDEBUG]key is long pressed.
[key_test.c on_key_event:9][EDEBUG]key is released.
【轻微】在开发实践->外设驱动->PWM的使用示例的原理图下面,控制引脚是GPIOA\\_06改为GPIOB\\_06;
【轻微】Wi-Fi配网的文档问题,在开发实践->系统服务->网络->WIFI连接 编译和烧录->编译中使用了lisa zep build -b csk6002\\_9s\\_nano版型,实际通过验证lisa zep build -b csk6011a\\_c3\\_nano编译通过;
【一般】提供图像识别、语音识别、Wi-Fi的综合应用的案例,评估芯片综合应用场景下的能力。因为目前提供的AI Sample都是独立的Sample并会下载单独的SDK,不知道和一开始安装在系统上的SDK的差异,遇到问题不好对比跟踪;
【严重】涉及网络的SDK在使用时,需要等待固定30秒钟才能进入main函数;
本案例的源代码已经上传到了github
,有兴趣的可以下载体验:
git clone https://github.com/aibittek/HsdVideoStreamAPP.git
通过项目名称可以看出来,其实一开始想做一个视频流应用的,就不用多插入一个USB线预览AI识别结果了,可惜有两个问题还不确定原因,详见问题部分的说明,解决后再把这块完善下吧
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。