因为后续想基于CSK6做产品,所以比较关心以下几点:
- 【硬件支持】CSK6提供的硬件能力和AI能力是否满足需求;
- 【软件支持】开发SDK是否稳定,方便开发人员更高效开发;
- 【文档支持】开放文档是否完善,后续是否有及时的技术支持;
总结:
整体玩下来,文档还是非常丰富的,从硬件参考设计、软件SDK和案例、涉及的知识能力等,看的出来花了很多的心思,好评👍。
总体评估整体实际实用风险不大,部分SDK和AI开放能力有发展空间。
以下结合对SDK的评测,说明其中遇到的一些问题、疑问,以及对后续的期待。
1、AI能力评测
通过官方提供的指导文档,可以比较容易的完成AI能力的Demo验证,比如人脸识别提供SDK源码下载地址和Sample的下载运行例程和说明,小白也可以轻松上手。
根据文档编译烧录后,通过官方提供的基于USB Web浏览器,接上板子的USB口,可以实时预览人脸识别的Demo效果。(以下是使用手机,出境人员是如花),效果上由于画面压缩,显示的图像质量不高,实际算法中是480*640的分辨率。
考虑到实用性,对人脸识别进行了一些非人脸的验证,从目前的情况看,会有一定识别错误的风险。
但在正常人脸识别时得分比较高,在做判断时,需要考虑更高的得分用于判定人脸相似度,比如得分在0.8以上判定为人脸。
另外期待AI能力后续能开放出来,并支持一些通用的机器学习、深度学习框架和模型。
对比K210、V831、V833、V853这些,在Tensorflow、Pytorch、YoLo等的一些支持,也增加了很多的玩法和场景应用的可能。
另外AIoT可以结合网络,增加用户的粘性,比如MaixHub在线训练平台,体验了下,设备的在线联动比较顺滑,体验效果不错。
2、Wi-Fi能力评测
以上提到了AIoT的能力。正好CSK6是作为AIoT的方案,套件中也提供了ESP32-C3的模块。接下来测试验证下网络的功能是否满足一些实际实用场景的需求。以下分别对官方Demo、SDK二次开发,后续再测试下TCP网络测速、实际实用场景测评。
2.1 Demo验证
在官方文档的开发实践->网络章节可以找到WIFI连接和网络通信的案例,根据说明,很容易验证Wi-Fi和网络通信的能力,这里就不过多介绍,可以参考官方的文档,赞一个👍。
2.2 SDK二次开发
由于网络遵循了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
2.3 实用场景——AP配网
验证了Wi-Fi能力后,接下来考虑的就是用户在使用时的配网怎样更加方便。
印象中,买过的一些不带屏的硬件产品,其中之一的配网流程如下:
- 长按设备端按键3秒;
- 指示灯快速闪烁,表示处于配网模式;
- 手机App指导设备联网;
- 设备由快闪变为慢速呼吸效果时,表示联网成功;
当然除了以上的方式还有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命令,来动态配网,接下来就试一下。
3、串口命令一键配网实现
看到了在开发文档开发实践->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中,下次上电执行自动重连机制。
4、文件系统SDK测评——Wi-Fi自动联网
这个部分是借用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做了封装。本章节所有的代码实现可能不能一一介绍,最后统一放到文章最后提供。
5、Wi-Fi测速及实用场景评测
考虑到有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也没有看到相关的打印日志。
6、基础SDK评测——LED亮灭闪烁呼吸
硬件产品离不开指示灯,指示灯一般有亮、灭、闪烁和呼吸效果,使用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));
}
}
7、基础SDK评测——Button软件消抖
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.
8、问题
【轻微】在开发实践->外设驱动->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函数;
9、期待
- 提供AI的开放能力,支持通用的一些框架和模型;
- 提供HIFI DSP相关的能力,期望能有多麦克风阵列+回采的方案,支持声源定位的功能,虽然画面的头肩识别、人脸识别可以完成定位,有些场景也需要多模态识别进行位置信息获取和处理;
10、本案例源码
本案例的源代码已经上传到了github
,有兴趣的可以下载体验:
git clone https://github.com/aibittek/HsdVideoStreamAPP.git
通过项目名称可以看出来,其实一开始想做一个视频流应用的,就不用多插入一个USB线预览AI识别结果了,可惜有两个问题还不确定原因,详见问题部分的说明,解决后再把这块完善下吧。