玩转行空板K10:基于Arduino的教育与创客开发探索
本帖最后由 岑剑伟 于 2025-3-11 11:03 编辑《玩转行空板K10:基于Arduino的教育与创客开发探索》
目录序言 行空板K10简介 体验感受 第一章 编程环境搭建 第二章 XL9535QF24扩展芯片使用 1. IO_expander使用方法 2. XL95x5_Driver使用方法 第三章 点亮屏幕 第四章 板载传感器驱动 1. 按键驱动 2. AHT20温湿度传感器 3. 光照强度传感器 4. 加速度传感器 5. 喇叭的驱动 第五章 LVGL图形库应用案例 1. LVGL图形库移植 2. 老司机图片浏览器 3. 基于LVGL的视频播放器 第六章 高级综合应用案例 1. 贪吃蛇游戏 2. 贪吃蛇游戏升级版 3. 板载资源应用综合案例 4. 基于FreeRTOS的人脸检测 5. mp3播放器 总结
static/image/hrline/1.gif
https://www.bilibili.com/video/BV1zvFBeYERP?buvid=Z343EF33C8185D50402F985852378FFD6AAB&is_story_h5=false&mid=UeMYnHruLfOHHdo8CDiFSQ%3D%3D&plat_id=116&share_from=ugc&share_medium=iphone&share_plat=ios&share_session_id=6B14BDA6-51D7-4B7E-9E95-135890E21DDE&share_source=WEIXIN&share_tag=s_i&spmid=united.player-video-detail.0.0×tamp=1738469738&unique_k=kIEELI2&up_id=396355825&share_source=weixin
全文内容导读: 本文围绕行空板K10展开,详细介绍了其背景、功能、编程环境搭建及丰富的应用案例,为开发者提供了全面的使用指南。1. 行空板K10概述:在esp32S3系列产品竞争激烈的市场环境下,dfrobot公司推出了定位中小学编程教育的行空板K10。它集成摄像头、LCD、麦克风等多种硬件资源,AI功能突出,具备离线图像检测和语音交互能力,在教育市场潜力巨大。2. 编程环境搭建:鉴于Arduino IDE编译大型项目速度慢,推荐使用VS Code并安装PlatformIO插件。工程文件可从文末gitee链接获取,所有案例统一管理,共享支持库依赖,调试方便。3. 硬件相关XL9535QF24扩展芯片:因esp32s3模组引脚被摄像头和麦克风大量占用,K10使用XL9535QF24芯片扩展引脚。该芯片通过I2C通信,负责屏幕背光和按键等功能。文中介绍了其引脚配置、两种支持库(esp32官方的IO—expander和“东方神秘力量”提供的库 )及使用方法。点亮屏幕:K10采用ILI9341屏幕,介绍了LovyanGFX、TFT_eSPI、Arduino_GFX三种常用的LCD屏幕支持库,并以康威生命游戏作为屏幕测试程序。板载传感器驱动按键驱动:基于XL9535芯片,通过封装检测逻辑和回调函数,实现按键事件异步处理,具备防抖功能。AHT20温湿度传感器:驱动库封装了数据读取逻辑,支持多种温度单位读取,通过任务机制定期读取数据。光照强度传感器:使用Adafruit公司的LTR329_LTR303驱动,可设置灵敏度档位,建议官方校准。加速度传感器:驱动库封装了SC7A20H传感器的相关逻辑,支持多轴加速度读取和强度计算。 喇叭驱动:实现基于ESP32的I2S音频播放器,可通过网络播放MP3文件。4. LVGL图形库应用案例LVGL图形库移植:LVGL是开源嵌入式图形库,功能强大。本案例实现了在K10上初始化硬件并配置LVGL库,显示小米澎湃系统开机画面,开启了学习LVGL的测试环境。 老司机图形浏览器:基于LVGL实现从SD卡读取PNG图片并显示的功能,支持手动和自动切换图片,提供图片缩放工具代码。基于LVGL的视频播放器:构建简易MJPEG视频播放器,利用LVGL库展示视频帧,ESP32_JPEG_Library库解码。5. 高级综合应用案例贪吃蛇游戏:基于Arduino平台,使用LovyanGFX库渲染图形,通过AB键控制,展示了游戏核心逻辑和图形绘制方法。贪吃蛇游戏升级版:在前一版基础上增加多种效果,使用外置按键控制,扩展板设计将开源。板载资源应用综合案例:基于LVGL图形库和MVC架构,使用FreeRTOS管理任务,实现报时、天气预报、室温监测等功能。基于FreeRTOS的人脸检测:利用FreeRTOS任务和队列机制,实现摄像头图像捕获和人脸检测的并行处理,在LCD显示结果。 mp3播放器:通过音频驱动、MP3文件处理、音乐播放控制和按键控制,实现基于lvgl的精美音乐播放器。
static/image/hrline/1.gif
序言:在当下这个科技飞速发展的繁华时代,创客编程社区呈现出一片热闹非凡的景象。随着乐鑫公司的 esp32S3 系列产品步入成熟阶段,达到性能与应用的顶峰,市面上涌现出了一大批以其为核心的优秀作品。像 M5stack core3,凭借出色的硬件配置和丰富的接口,在物联网应用开发中备受青睐;Espressif 的 Box3,以其独特的设计和强大的功能,在智能硬件领域崭露头角;Seeed 的 Sensecap_indicator,专注于环境监测等领域,为数据采集提供了高效的解决方案;Lilygo-esp32S3 系列产品和 waveshare 的 esp32S3 系列产品,也都凭借各自的优势,在市场中占据了一席之地。然而,这些产品在各有特长的同时,也面临着明显的同质化竞争,正是这种激烈的竞争,将 esp32s3 产品系列推向了高光时刻,真可谓是 “百花渐欲迷人眼”。在 2024 年末,dfrobot 公司在成功推出行空板 m10 之后,经过长时间的精心打磨,又强势推出了行空板 K10。在这个竞争已经进入白热化的市场阶段,行空板 K10 宛如一员勇猛的战将,横空出世。K10 有着十分明确的定位,它不走寻常路,选择了与其他品牌同类产品截然不同的差异化路线,将中小学编程教育作为主攻方向。dfrobot 公司依托其多年深耕图形化编程终端的深厚底蕴,行空板 K10 有望收获青少年编程教育界老师和学生们的喜爱与认可,在教育市场中开辟出一片属于自己的新天地。作为一名专注于 esp32 产品收藏的爱好者,我成功获取了 K10 板卡。此次测评,我将主要围绕基于 vscodepio+Arduino 的编程环境展开,向各位同仁总结这段时间我对 K10 板卡底层资源的开发与利用情况。汇报内容侧重于Arduino的硬件驱动和应用开发研究,功能实现机理与 Mind + 编程实现原理基本一致,不仅可作为理解 Mind + 编程的辅助参考资料,也能为有志于深入挖掘 K10 板卡功能的朋友提供有益借鉴。本项目的研究成果,为 K10 板卡开拓出一条融入繁荣的 Arduino 生态的有效路径,使其得以迈向更强大、更灵活的应用场景。
行空板 K10简介:行空板 K10,作为一款专为信息科技教学打造的开发板,凭借其丰富的功能特性与超高性价比,在编程学习和 AI 项目教学等场景中独领风骚。它的硬件资源高度集成,摄像头、2.8 寸彩色 LCD、麦克风、扬声器、WiFi、蓝牙、RGB 指示灯以及温湿度、环境光、加速度等多种传感器一应俱全。这意味着,无需额外外接设备,便能轻松开展各类教学活动与项目实践,极大地提高了教学与实践的便捷性。在行空板 K10 的众多特性中,其 AI 功能堪称一大亮点。板载的集成摄像头具备强大的离线图像检测能力,无论是人脸、动物识别,还是二维码扫描,都能精准完成,为图像识别教学提供了坚实的技术支撑,让学生能够在实际操作中深入理解图像识别原理。在语音交互方面,行空板 K10 同样表现出色。即便处于无网络环境,凭借先进的双麦降噪技术以及对 100 多条中文自定义命令词的支持,其离线语音识别功能依旧能够实现流畅的人机对话。搭配语音合成能力,它不仅 “听得懂”,还能 “说得清”,为用户带来更加自然、便捷的交互体验。值得一提的是,行空板 K10 深度挖掘了 esp32S3 芯片的潜力,将人脸识别、本地语音唤醒以及语音合成功能发挥到极致。在这一领域,它积累了众多成功案例,这些宝贵经验不仅展现了行空板 K10 的技术实力,也为其他开发者和教育工作者提供了极具价值的参考,值得深入挖掘与学习。行空板 K10功能介绍:https://www.dfrobot.com.cn/goods4014.html
体验感受:行空板 K10 的硬件资源集成度极高,这一点在实际使用中感受尤为明显。其中,摄像头的集成十分亮眼,不过大家都清楚,摄像头运行时会占用大量引脚资源。为解决这一难题,K10 采用了引脚扩充 IC 来点亮彩屏的背光和两颗操控按钮等其他硬件的协同操作。这一设计虽然有效解决了引脚耗尽的问题,却也带来了新的挑战。相较于原生引脚,扩充 IC 引脚的使用方法更为复杂,再加上大众对扩展引脚芯片的熟悉程度普遍不高,使得从 Arduino 编程入手的难度大幅增加,在实际操作中需要花费更多时间和精力去摸索与适应。
static/image/hrline/1.gif
项目内容 第一章 编程环境搭建最近,dfrobot官方开源了K10基于Arduino 1.8.9版本的板卡支持及应用示例。不过,Arduino 1.8.9版本已发布许久,在不久的将来,官方很可能会推出针对Arduino 2.X版本的支持。倘若您有较为迫切的使用需求,可重新安装1.8.X版本进行体验。考虑到Arduino IDE在编译大型项目时速度较慢,建议您转移至VS Code并安装PlatformIO插件来运行Arduino代码。这种转变能显著提升编译速度,同时赋予更多丰富功能。比如,现在您还可以借助众多AI编程助手,这对工作与学习都大有裨益。因此,强烈推荐使用PlatformIO(Pio)进行Arduino编程。工程文件可以文末gitee链接获取,然后直接用vscode打开工程文件即可,无需额外特殊的设置,属于开袋即食操作,(请提前先给vscode安装好pio插件)。本项目的所有案例均统一管理在同一项目下, 所有案例共享同一套支持库依赖,调试起来非常友好和方便。具体对项目的工程文件platformio.ini 作了以下规划和调整,请仔细阅读下图,需留意以下几点,您便能轻松上手本教程。注意所有案例程序文件放置在examples文件夹里,全部在本地多次验证可以正常运行,如有报错可以留言提醒。 platformio.ini文件截图(源文件在项目根目录)
platform = espressif32@6.6.0
; platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
board_build.partitions = large_spiffs_16MB.csv
; board_build.filesystem = littlefs
board_build.filesystem = spiffs
board_build.arduino.memory_type = qio_opi
board_upload.flash_size = 16MB
monitor_speed = 115200
upload_speed = 921600
build_flags =
-w -g
-DCORE_DEBUG_LEVEL=5
-DBOARD_HAS_PSRAM
-DLV_CONF_INCLUDE_SIMPLE
-DDISABLE_ALL_LIBRARY_WARNINGS
-DARDUINO_USB_CDC_ON_BOOT=1
-DLV_LVGL_H_INCLUDE_SIMPLE
-I src
-L lib/ESP32_JPEG/src/esp32s3
-llibesp_codec.a
lib_deps =
https://github.com/esp-arduino-libs/ESP32_JPEG
; default_envs = 01_display_esp_expnader ; 使用esp_IOexpnader库驱动扩展芯片
; default_envs = 02_display_XL95x5_LTR303 ; 使用XL95x5库驱动扩展芯片
; default_envs = 10_Lgfx_usage ; 使用Lgfx库驱动点亮屏幕
; default_envs = 11_TFTeSPI_usage ; 使用TFTeSPI库驱动点亮屏幕
; default_envs = 12_Arduino_gfx ; 使用Arduino_gfx库驱动点亮屏幕
; default_envs = 20_button ; 按键使用案例,查看串口输出
; default_envs = 21_aht20 ; 温湿度使用案例
; default_envs = 22_SC207aH ; 加速度传感器测试,查看串口输出
; default_envs = 23_music_test ; 音频输出基本测试,播放网络音乐
; default_envs = 30_lvgl_Gif ; LVGL驱动并播放澎湃开机gif动图
; default_envs = 31_png_browser ; 基于LVGL的图片浏览器
; default_envs = 32_DEMO_MJPEG ; 基于LVGL视频播放器演示
; default_envs = 40_Snake_Game ; 按键使用案例
; default_envs = 41_Snake_Game2 ; 使用IO按键使用案例
; default_envs = 42_demo_ui_lcd ; 基于LVGL的图形界面设计
; default_envs = 43_RTOS_face ; 基于LVGL的mp3播放器
default_envs = 44_mp3Player2 ; 基于LVGL的mp3播放器
src_dir = examples/${platformio.default_envs}
本项目从基本的点亮屏幕到板载传感器及执行器的驱动到深入使用板载资源的应用案例17项,覆盖了K10使用的主要场景,难度由浅至深,环环相扣,形成了完整的知识链条,这些案例不仅可以在K10上使用,在其他具有相似配置的硬件上也具有很强的参考意义。
static/image/hrline/1.gif
第二章 XL9535QF24扩展芯片使用 想要点亮屏幕就不得不提及XL9535QF24芯片,前面内容已有略有带过,因为esp32s3模组的引脚虽然已是很多,但K10原生支持摄像头和麦克风必然会消耗大量的引脚,通常的做法就是使用扩展引脚芯片。在k10上使用的是一颗国产芯片XL9535QF24芯片,是可以替代TAC95XX的芯片。这颗芯片却是掌管K10关键操控的交互主要部件,因为它负责了显示屏幕的背光和两颗按键,k10屏幕并不是触摸屏,两颗按钮的在使用过程的作用显得尤为突出,后续会有更多的使用经验介绍。 XL9535QF24是一款可能用于GPIO扩展等功能的国产芯片。 I2C通信引脚:20脚SDA(串行数据引脚)和19脚SCL(串行时钟引脚),用于与外部设备进行I2C通信,并且通过P20/SDA和P19/SCL与外部连接,上拉电阻R28(10K/1%)连接到3V3,保证I2C总线信号的稳定性。(1) 中断引脚:22脚INT为中断引脚,连接到BUS_INT (目前还没有使用中断的高级用法)。(2) 地址选择引脚:18脚A0、23脚A1、24脚A2用于设置芯片的I2C从机地址,当前地址设置为0B 0100 (A2 A1 A0) ,即十六进制0x20。(3) 其他引脚连接芯片的P00P17(实际是16个引脚)引脚分别连接到不同的功能模块或引脚,如LCD_BL(可能是LCD背光灯控制)、Camera_RST(相机复位)、两个按键(KeyA、KeyB)、以及UserLed(用户指示灯)等,用于实现对这些外部设备或功能的控制,其他的引脚在金手指上自定义使用。 因为XL9535QF24是TCA95XX系列芯片的替代芯片,在Arduino上以tca95为关键词可以搜索到将近20个支持库,可以是说是个在业界广泛应用的芯片,主要是一般情况下,esp32系列芯片的引脚已然够用,因此,国内使用的案例并不多,相信通过k10的示范带动作用,今后这种情况会变得更为常见。居然有这么可选项,其中不乏一些国外知名大厂提供的支持库,反而难道是了选择困难症了,在这里就不对这些TCA95xx支持库进行逐一介绍了,有兴趣的可以自行探索,做个横向比较,然后回来告诉大家,你心目中哪款更好用。 在这里选择了两款支持库进行比较实用,一个是esp32官方提供的IO—expander,据判断df官方也是使用这款,另一款是来“东方神秘力量”提供的支持库,使用起来也比较顺手,在Arduino环境运行流畅、简单。 请按前述要求开启XL95x5芯片驱动的测试。 1、IO_expander使用方法 XL9535QF24芯片是通过k10主板上的i2c协议进行控制的,板载上的所有传感器资源都寄托在同一对I2C引脚上,因此使用时应当小心谨慎。XL9535使用的核心要义就是创建跟I2C的连接,然后初始化设备,就可以控制对应的扩展引脚的功能了,lcd背光属于输出类型,而按键属于读取类型。设置输出输入类型后,即可执行对应的操作。原理也不复杂,相比原生的板载IO,增加了I2C管理部分内容,熟练掌握XL9535芯片的使用方法,对于K10后续项目的开展尤为重要,但是后续项目我已经把驱动封装好了,可以直接使用,就可以完成屏幕背光点亮和按键初始化。
#include <ESP_IOExpander_Library.h>
#define _EXAMPLE_CHIP_CLASS(name, ...) ESP_IOExpander_##name(__VA_ARGS__)
#define EXAMPLE_CHIP_CLASS(name, ...) _EXAMPLE_CHIP_CLASS(name, ##__VA_ARGS__)
ESP_IOExpander *expander = NULL;
#include "Lcd_handler.hpp"
K10_Lcd display;
LGFX_Sprite canvas(&display);
char info;
void setup() {
Serial.begin(115200);
while (!Serial) {
delay(10); // wait for serial port to open
}
Serial.println("Adafruit LTR-303 simple test");
Wire.begin(47, 48);
// init_board();
expander =
new EXAMPLE_CHIP_CLASS(TCA95xx_16bit, (i2c_port_t)0,
ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000, 48, 47);
// expander->init();
expander->begin();
expander->reset();
expander->pinMode(0, OUTPUT);
expander->pinMode(1, OUTPUT);
delay(20);
expander->digitalWrite(0,HIGH) ;
expander->digitalWrite(1, HIGH);
// init_board();
// display.setFont(&fonts::Orbitron_Light_24);
Serial.println("Sensor begin.....");
}
2、XL95x5_Driver使用方法与esp32_IOexpander原理一致,使用方法稍有不同,相信其他驱动支持库使用起来也不会太难,理解原理就可以畅通无阻了。
#include "XL95x5_Driver.h"
#include "Lcd_handler.hpp"
K10_Lcd display;
LGFX_Sprite canvas(&display);
XL95x5 ExpandPin(XL95x5_IIC_ADDRESS, XL95x5_SDA, XL95x5_SCL);
char info;
void setup() {
Serial.begin(115200);
while (!Serial) {
delay(10);// wait for serial port to open
}
Serial.println("Adafruit LTR-303 simple test");
Wire.begin(47, 48);
ExpandPin.begin();
ExpandPin.read_all_reg();// Read all registers
ExpandPin.pinMode(0,OUTPUT);
ExpandPin.pinMode(1,OUTPUT);
delay(10);
ExpandPin.digitalWrite(0, HIGH);
ExpandPin.digitalWrite(1, HIGH);
ExpandPin.pinMode(2,INPUT);
ExpandPin.pinMode(12,INPUT);
static/image/hrline/1.gif
第三章 点亮屏幕经过前面引脚扩展芯片的驱动之后,最艰难的时刻已经过去,后面的内容可以参照其他esp32开发板的使用案例进行操作了。 K10使用的屏幕是ILI9341 ,相信大家对个屏幕驱动芯片也不陌生,在早期很多开发板都是采用这款驱动,但是随着时代的进步,目前很多高分辨率的屏幕不断出现,使得生态更加多样化和繁荣。 在这里我列举了3个经典的和常用的lcd屏幕支持库,总有一款适合你。分别是LovyanGFX,TFT_eSPI,Arduino_GFX,是不是都很熟悉呀?LovyanGFX是由日本大师Lovyan主理(大神就职于日本M5Stack),该库与M5Stack公司核心系列产品的驱动底层一致,这个库功能强大支持的大部分主流的屏幕芯片,功能函数最多,内置多国语言,包括中文字体的支持,不用为字体的问题而烦恼。TFT_eSPI是一款全球广泛接受的lcd屏幕驱动库,很多创客朋友都是从学习这个库开始入门的,自然也不需要作过多的介绍,直接开干就是了。 Arduino_GFX则是有香港大神陈亮维护的屏幕支持库,陈亮大神比较热衷各种类型新屏幕的驱动开发,当前一些最新推出的屏幕都可以在这里体验到,请大家多多支持他,支持国货的崛起。 这里使用了经典的康威生命游戏作为屏幕测试程序,康威生命游戏规则是当前位点与相连的8个位点细胞数量进行生存与否的判断,可以对比一下不同驱动的屏幕刷新的动态效果,这里就让小伙伴们亲自去体验感受了。
lgfx驱动:#ifndef __LCD_HANDLER__H__
#define __LCD_HANDLER__H__
#include <LovyanGFX.hpp>
#include <Wire.h>
// #include "pin_config.h"
/// 要进行自定义设置的类,从LGFX_Device派生创建。
class K10_Lcd : public lgfx::LGFX_Device
{
// 准备与要连接的面板类型相匹配的实例。
lgfx::Panel_ILI9341 _panel_instance;
// 准备与连接面板的总线种类相匹配的实例。
lgfx::Bus_SPI _bus_instance; // SPI总线的实例
//lgfx::Bus_I2C _bus_instance; // I2C总线的实例
//lgfx::Bus_Parallel8_bus_instance; // 8位并行总线的实例
public:
K10_Lcd(void)
{
{ // 进行总线控制的设置。
auto cfg = _bus_instance.config(); // 获取用于总线设置的结构体。
// SPI总线的设置
cfg.spi_host = SPI2_HOST; // 选择要使用的SPIESP32-S2,C3 : SPI2_HOST或SPI3_HOST / ESP32 : VSPI_HOST或HSPI_HOST
// ※ 随着ESP-IDF版本升级,VSPI_HOST、HSPI_HOST的写法已不推荐使用,若出现错误,请使用SPI2_HOST、SPI3_HOST替代。
cfg.spi_mode = 0; // 设置SPI通信模式 (0 ~ 3)
cfg.freq_write = 40000000; // 发送时的SPI时钟(最大80MHz,会被舍入为80MHz除以整数后的值)
cfg.freq_read= 16000000; // 接收时的SPI时钟
cfg.spi_3wire= true; // 如果使用MOSI引脚进行接收,则设置为true
cfg.use_lock = true; // 如果使用事务锁,则设置为true
cfg.dma_channel = SPI_DMA_CH_AUTO; // 设置要使用的DMA通道 (0=不使用DMA / 1=1通道 / 2=通道 / SPI_DMA_CH_AUTO=自动设置)
// ※ 随着ESP-IDF版本升级,DMA通道推荐使用SPI_DMA_CH_AUTO(自动设置),指定1通道、2通道已不推荐使用。
cfg.pin_sclk = 12; // 设置SPI的SCLK引脚编号
cfg.pin_mosi = 21; // 设置SPI的MOSI引脚编号
cfg.pin_miso = -1; // 设置SPI的MISO引脚编号 (-1 = 禁用)
cfg.pin_dc = 13; // 设置SPI的D/C引脚编号(-1 = 禁用)
_bus_instance.config(cfg); // 将设置值反映到总线上。
_panel_instance.setBus(&_bus_instance); // 将总线设置到面板上。
}
{ // 进行显示面板控制的设置。
auto cfg = _panel_instance.config(); // 获取用于显示面板设置的结构体。
cfg.pin_cs = 14;// CS所连接的引脚编号 (-1 = 禁用)
cfg.pin_rst = -1;// RST所连接的引脚编号(-1 = 禁用)
cfg.pin_busy = -1;// BUSY所连接的引脚编号 (-1 = 禁用)
cfg.panel_width = 240;// 实际可显示的宽度
cfg.panel_height = 320;// 实际可显示的高度
cfg.offset_x = 0;// 面板在X方向的偏移量
cfg.offset_y = 0;// 面板在Y方向的偏移量
cfg.offset_rotation= 0;// 旋转方向值的偏移量,取值范围是0~7(4~7表示上下翻转)
cfg.dummy_read_pixel = 8;// 像素读取前的虚拟读取位数
cfg.dummy_read_bits= 1;// 读取非像素数据前的虚拟读取位数
cfg.readable =true;// 如果可以读取数据,则设置为true
cfg.invert = false;// 如果面板的明暗出现反转情况,则设置为true
cfg.rgb_order = false;// 如果面板的红色和蓝色发生互换情况,则设置为true
cfg.dlen_16bit = false;// 对于通过16位并行或SPI以16位为单位发送数据长度的面板,设置为true
cfg.bus_shared =true;// 如果与SD卡共享总线的情况(在drawJpgFile等操作中会进行总线控制),设置为true
_panel_instance.config(cfg);
}
setPanel(&_panel_instance); // 设置要使用的面板。
}
};
#endif//!__LCD_HANDLER__H__
Arduino_gfx设置:
#include <Arduino_GFX_Library.h>
#include "initBoard.h"
// #define GFX_BL DF_GFX_BL // default backlight pin, you may replace DF_GFX_BL to actual backlight pin
/* More dev device declaration: https://github.com/moononournation/Arduino_GFX/wiki/Dev-Device-Declaration */
#if defined(DISPLAY_DEV_KIT)
Arduino_GFX *gfx = create_default_Arduino_GFX();
#else /* !defined(DISPLAY_DEV_KIT) */
// #define TFT_MISO-1// Automatically assigned with ESP8266 if not defined
// #define TFT_MOSI21// Automatically assigned with ESP8266 if not defined
// #define TFT_SCLK12// Automatically assigned with ESP8266 if not defined
// #define TFT_CS 14// Chip select control pin D8
// #define TFT_DC 13// Data Command control pin
// #define TFT_RST -1// Reset pin (could connect to NodeMCU RST, see next line)
/* More data bus class: https://github.com/moononournation/Arduino_GFX/wiki/Data-Bus-Class */
// Arduino_DataBus *bus = create_default_Arduino_DataBus();
Arduino_DataBus *bus = new Arduino_ESP32SPI(13 /* DC */, 14 /* CS */, 12 /* SCK */, 21/* MOSI */, GFX_NOT_DEFINED /* MISO */, SPI2_HOST /* spi_num */);
/* More display class: https://github.com/moononournation/Arduino_GFX/wiki/Display-Class */
Arduino_GFX *gfx = new Arduino_ILI9341(bus, DF_GFX_RST, 0 /* rotation */, false /* IPS */);
#endif /* !defined(DISPLAY_DEV_KIT) */
/*******************************************************************************
* End of Arduino_GFX setting
******************************************************************************/
void setup(void)
{
Serial.begin(115200);
init_board();
// Serial.setDebugOutput(true);
// while(!Serial);
Serial.println("Arduino_GFX Hello World example");
#ifdef GFX_EXTRA_PRE_INIT
GFX_EXTRA_PRE_INIT();
#endif
// Init Display
if (!gfx->begin())
{
Serial.println("gfx->begin() failed!");
}
gfx->fillScreen(BLACK);
#ifdef GFX_BL
pinMode(GFX_BL, OUTPUT);
digitalWrite(GFX_BL, HIGH);
#endif
gfx->setRotation(2);
gfx->setCursor(10, 10);
gfx->setTextColor(RED);
gfx->println("Hello World!");
delay(5000); // 5 seconds
}
TFT_eSPI驱动查看源码。
为K10制作microblocks板卡驱动Microblocks (https://github.com/MicroBlocksCN/smallvm)是一种面向嵌入式开发和微控制器编程的图形化编程框架。它将编程抽象为一个可视化的模块化环境,特别适用于初学者或者希望以更直观方式进行编程的人。Microblocks 的核心理念是通过简洁的图形化编程界面来编写和控制硬件设备,尤其是嵌入式系统(如微控制器、传感器、LED、马达等)。Microblocks 在国内由一群热心的创客团队进行维护和更新,目前已经集成了多种流行开发板的支持,利用本次学习的机会和朋友们为k10提交了板卡支持,其中核心内容就是“Adafruit_ILI9341”库点亮了屏幕
(https://github.com/MicroBlocksCN/smallvm/pull/136/files)。 #elif defined(DF_K10)
#include <ESP_IOExpander_Library.h>
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#include "Audio.h"
Audio audio;
ESP_IOExpander *expander = new ESP_IOExpander_TCA95xx_16bit(I2C_NUM_0, ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000, I2C_SCL_PIN, I2C_SDA_PIN);
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST, TFT_MISO);
void tftInit() {
pinMode(45,OUTPUT);
digitalWrite(45,LOW);
expander->init();
expander->begin();
expander->printStatus();
expander->pinMode(0, OUTPUT);
expander->pinMode(1, OUTPUT);
expander->digitalWrite(0, HIGH);
expander->digitalWrite(1, HIGH);
tft.begin(40000000);
tft.setRotation(2);
// tft.invertDisplay(invertFlag);
uint8_t m = 0x08 | 0x04; // RGB pixel order, refresh LCD right to left
tft.sendCommand(ILI9341_MADCTL, &m, 1);
tftClear();
useTFT = true;
audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
audio.setVolume(0);// 0...21
}(屏幕输出)
(点亮3颗led)
第四章 板载传感器驱动
1、按键驱动 K10板载的两个按键是基于XL9535芯片之上的,所以按键驱动实质上是XL9535芯片的深入应用案例。这里参照dfrobot官方按键驱动的模板填补了其中空缺关键功能函数,复现了跟官方同样功能的按键驱动,具体实现方式参见如下代码。 板卡初始化关键代码:#include "initBoard.h"
/**
* @brief 初始化主板电源芯片
*
* @return 返回初始化状态
*/
// XL95x5 ExpandPin(XL95x5_IIC_ADDRESS, Wire);
// XL95x5 ExpandPin(XL95x5_IIC_ADDRESS, Wire);
XL95x5 ExpandPin(XL95x5_IIC_ADDRESS, XL95x5_SDA, XL95x5_SCL);
esp_err_t init_board(void){
ExpandPin.begin();
ExpandPin.read_all_reg();// Read all registers
ExpandPin.pinMode(eLCD_BLK,OUTPUT);
ExpandPin.pinMode(eCamera_rst,OUTPUT);
ExpandPin.pinMode(eAmp_Gain,OUTPUT);
ExpandPin.digitalWrite(eLCD_BLK, LOW);
// ExpandPin.digitalWrite(eCamera_rst, LOW);
ExpandPin.digitalWrite(eAmp_Gain,LOW);
delay(10);
ExpandPin.digitalWrite(eLCD_BLK, HIGH);
// ExpandPin.digitalWrite(eCamera_rst, HIGH);
ExpandPin.digitalWrite(eAmp_Gain, HIGH);
ExpandPin.read_all_reg();
ExpandPin.digitalWrite(eCamera_rst, HIGH);
ExpandPin.pinMode(eP5_KeyA,INPUT);
ExpandPin.pinMode(eP11_KeyB,INPUT);
ExpandPin.pinMode(eP8,INPUT);
ExpandPin.pinMode(eP9,INPUT);
ExpandPin.pinMode(eP13,INPUT);
ExpandPin.pinMode(eP14,INPUT);
ExpandPin.pinMode(eP15,INPUT);
return ESP_OK;
};
/**
* @brief 设置引脚输出电平
*
* @param pin 引脚选择
* @param state 输出电平
*/
void digital_write(ePin_t pin,uint8_t state){
ExpandPin.digitalWrite(pin,state);
};
/**
* @brief 获取引脚电平
*
* @param pin 引脚选择
* @return 返回引脚电平
*/
uint8_t digital_read(ePin_t pin){
return ExpandPin.digitalRead(pin);
};
/**
* @brief 配置引脚中断
*
* @param pin 引脚号
* @param function 中断处理函数
*/
//void digitalInterrupt(ePin_t pin,interruptFunc function, uint8_t state);
/**
* @brief 配置po口输出模式
*
* @param model 模式
* @return esp_err_t 返回配置状态
*/
//esp_err_t poart0_model(uint8_t model);
按键驱动关键代码:#include "initBoard.h"
/
* @brief 初始化主板电源芯片
*
* @return 返回初始化状态
*/
// XL95x5 ExpandPin(XL95x5_IIC_ADDRESS, Wire);
// XL95x5 ExpandPin(XL95x5_IIC_ADDRESS, Wire);
XL95x5 ExpandPin(XL95x5_IIC_ADDRESS, XL95x5_SDA, XL95x5_SCL);
esp_err_t init_board(void){
ExpandPin.begin();
ExpandPin.read_all_reg();// Read all registers
ExpandPin.pinMode(eLCD_BLK,OUTPUT);
ExpandPin.pinMode(eCamera_rst,OUTPUT);
ExpandPin.pinMode(eAmp_Gain,OUTPUT);
ExpandPin.digitalWrite(eLCD_BLK, LOW);
// ExpandPin.digitalWrite(eCamera_rst, LOW);
ExpandPin.digitalWrite(eAmp_Gain,LOW);
delay(10);
ExpandPin.digitalWrite(eLCD_BLK, HIGH);
// ExpandPin.digitalWrite(eCamera_rst, HIGH);
ExpandPin.digitalWrite(eAmp_Gain, HIGH);
ExpandPin.read_all_reg();
ExpandPin.digitalWrite(eCamera_rst, HIGH);
ExpandPin.pinMode(eP5_KeyA,INPUT);
ExpandPin.pinMode(eP11_KeyB,INPUT);
ExpandPin.pinMode(eP8,INPUT);
ExpandPin.pinMode(eP9,INPUT);
ExpandPin.pinMode(eP13,INPUT);
ExpandPin.pinMode(eP14,INPUT);
ExpandPin.pinMode(eP15,INPUT);
return ESP_OK;
};
/
* @brief 设置引脚输出电平
*
* @param pin 引脚选择
* @param state 输出电平
*/
void digital_write(ePin_t pin,uint8_t state){
ExpandPin.digitalWrite(pin,state);
};
/
* @brief 获取引脚电平
*
* @param pin 引脚选择
* @return 返回引脚电平
*/
uint8_t digital_read(ePin_t pin){
return ExpandPin.digitalRead(pin);
};
/
* @brief 配置引脚中断
*
* @param pin 引脚号
* @param function 中断处理函数
*/
//void digitalInterrupt(ePin_t pin,interruptFunc function, uint8_t state);
/
* @brief 配置po口输出模式
*
* @param model 模式
* @return esp_err_t 返回配置状态
*/
//esp_err_t poart0_model(uint8_t model);
这个按键驱动库通过封装按键的检测逻辑和回调函数处理,提供了一个简单易用的接口来处理按键事件。它支持单引脚和双引脚的按键配置,并通过任务机制实现了按键事件的异步处理。防抖处理和多次重试检测确保了按键状态的准确性。值得一提的是通过esp32的freeRTOS任务事件来负责按键轮询检测,在后台自动运行,极大地减少了用户的工作量,让用户可以更专注应用功能的实现逻辑,这是df官方着重考虑的地方。防抖动能的实现也比较完美,比较不加防抖措施,按键抖动还是很厉害的。由此,带来的少少的负面影响是 esp32的多任务消耗了一些内存,据测按键的任务栈内存要大于6k以上才能正常运行,不然系统容易崩溃,显然官方也觉察到了这个问题,官方给按键任务栈设置的8k,提供了足够的冗余来保障按键的正常工作。 具体内容请查阅板卡初始化及按键驱动文件,如下图所示。
2、 AHT20温湿度传感器 AHT20温湿度传感器驱动库通过封装 传感器的数据读取逻辑和任务处理,提供了一个简单易用的接口来获取温湿度数据。它支持摄氏温度、华氏温度和相对湿度的读取,并通过任务机制实现了数据的定期读取和处理。任务循环确保了数据的实时性和准确性,适合在需要持续监控温湿度数据的应用场景中使用。跟按键驱动的业务逻辑相似,为了减少后期数据读取的工作量,在驱动库里已完成了数据自动读取的逻辑,用户只需直接获取数据即可,这种传感器数据读取的管理方式值得学习和推广。
private:
static void taskLoop(void *param)
{
AHT20 *self = (AHT20 *)param;
uint8_t status;
while((status = self>begin()) != 0){
printf("AHT20 sensor initialization failed.\n");
delay(1000);
break;
}
TickType_t xLastWakeTime;
const TickType_t xFrequency = pdMS_TO_TICKS(1000); // Specify the task to run every 1000ms
while (1)
{
if(self>startMeasurementReady(true)){
}
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
};
AHT20::AHT20(TwoWire &wire) : DFRobot_AHT20(wire)
{
xTaskCreatePinnedToCore(AHT20::taskLoop, "aht20", 4 * 1024, this, 2, NULL, 1);
}
3、光照强度传感器使用了Adafruit公司提供的LTR329_LTR303光照强度传感器来驱动了板载光照强度传感器,dfrobot官方也有该传感器驱动可以参考,也比较的简洁,但Adafruit公司提供的这个驱动可以设置灵敏度档位,经测试档位调整到LTR3XX_GAIN_4 后与其他光照传感器的lux数值接近,建议官方调整该传感器的灵敏度或者对其进行相应的校准,使其更具实用性。
ltr.setGain(LTR3XX_GAIN_4);
// Set integration time of 50ms (see advanced demo for all options!
ltr.setIntegrationTime(LTR3XX_INTEGTIME_50);
// Set measurement rate of 50ms (see advanced demo for all options!
ltr.setMeasurementRate(LTR3XX_MEASRATE_50);
4、加速度传感器 加速度传感器驱动库通过封装 SC7A20H 传感器的初始化、数据读取和处理逻辑,提供了一个简单易用的接口来获取加速度数据。支持 X、Y、Z 轴加速度的读取,并提供了合强度的计算功能。通过 I2C 接口与传感器通信,确保了数据的实时性和准确性,适合在需要监测加速度的应用场景中使用。在此不多作展开,后面还有个综合应用案例(42_demo_ui_lcd ),展示了板载传感器的所有功能。 #include "SC7A20H.h"
// 构造函数,初始化传感器地址
SC7A20H::SC7A20H(TwoWire &wire) : _pWire(&wire)
{
accX = 0;
accY = 0;
accZ = 0;
}
// 析构函数
SC7A20H::~SC7A20H(void)
{
// 如果有需要释放的资源,可以在这里处理
// 暂时没有动态分配资源,因此这里可以为空
}
// 假设 readData 和 writeCommand 通过某种方式来访问硬件寄存器
uint8_tSC7A20H::readData(uint8_t addr, uint8_t arg, void *pBuf, size_t size)
// readData(uint8_t addr, uint8_t reg, uint8_t* buf, uint8_t len)
{
uint8_t ret = 0;
if (pBuf == NULL)
{
printf("pBuf ERROR!! : null pointer");
return 0;
}
uint8_t *_pBuf = (uint8_t *)pBuf;
_pWire->beginTransmission(addr);
_pWire->write(arg);
if (_pWire->endTransmission() != 0)
{
return 0;
}
ret = _pWire->requestFrom(addr, (uint8_t)size);
for (uint16_t i = 0; i < size; i++)
{
_pBuf = (char)_pWire->read();
}
return ret;
}
void SC7A20H::writeCommand(uint8_t addr, uint8_t reg, uint8_t cmd)
{
_pWire->beginTransmission(addr);
_pWire->write(reg);
_pWire->write(cmd);
_pWire->endTransmission();
}
void SC7A20H::initSC7A20H(void)
{
uint8_t data = 0;
uint8_t buf;
readData(_sc7a20hAddr, 0x24, buf, 1);
data = buf;
data |= 0x08;
writeCommand(_sc7a20hAddr, 0x24, data);
// AOI1 CONFIG
data = 0x00;
data |= 0x40;
data |= 0x03;
data |= 0x0c;
data |= 0x38;
writeCommand(_sc7a20hAddr, 0x30, data);
readData(_sc7a20hAddr, 0x21, buf, 1);
data = buf;
data |= 0x81;
writeCommand(_sc7a20hAddr, 0x21, data);
data = 0x60;
writeCommand(_sc7a20hAddr, 0x32, data);
data = 0x02;
writeCommand(_sc7a20hAddr, 0x33, data);
readData(_sc7a20hAddr, 0x22, buf, 1);
data = buf;
data |= 0x40;
writeCommand(_sc7a20hAddr, 0x21, data);
readData(_sc7a20hAddr, 0x24, buf, 1);
data = buf;
data |= 0x02;
writeCommand(_sc7a20hAddr, 0x24, data);
readData(_sc7a20hAddr, 0x25, buf, 1);
data = buf;
data |= 0x02;
writeCommand(_sc7a20hAddr, 0x25, data);
data = 0x00;
data |= 0xc0;
data |= 0x3f;
writeCommand(_sc7a20hAddr, 0x34, data);
readData(_sc7a20hAddr, 0x21, buf, 1);
data = buf;
data |= 0xfd;
writeCommand(_sc7a20hAddr, 0x21, data);
data = 0x18;
writeCommand(_sc7a20hAddr, 0x36, data);
data = 0x02;
writeCommand(_sc7a20hAddr, 0x37, data);
readData(_sc7a20hAddr, 0x25, buf, 1);
data = buf;
data |= 0x20;
writeCommand(_sc7a20hAddr, 0x21, data);
// fs_config
readData(_sc7a20hAddr, 0x23, buf, 1);
data = 0x80;
data |= 0x08;
data |= 0x00;
writeCommand(_sc7a20hAddr, 0x23, data);
// 电源
data = 0x27;
writeCommand(_sc7a20hAddr, 0x20, data);
}
void SC7A20H::updateAccelerometerData() {
uint8_t buf;
// 读取加速度数据
readData(_sc7a20hAddr, 0xA8, buf, 6);
// 处理并更新 X、Y、Z 数据
accX = (int)((buf << 8 | buf) >> 4);
accY = (int)((buf << 8 | buf) >> 4);
accZ = (int)((buf << 8 | buf) >> 4);
// 补码处理
if ((accX & 0x800) == 0x800) {
accX -= 4096;
}
if ((accY & 0x800) == 0x800) {
accY -= 4096;
}
if ((accZ & 0x800) == 0x800) {
accZ -= 4096;
}
}
// self->readData(0x19, 0xA8, buf, 6);
// self->accX = (int)((buf << 8 | buf) >> 4);
// self->accY = (int)((buf << 8 | buf) >> 4);
// self->accZ = (int)((buf << 8 | buf) >> 4);
// if ((self->accX & 0x800) == 0x800)
// {
// self->accX -= 4096;
// }
// if ((self->accY & 0x800) == 0x800)
// {
// self->accY -= 4096;
// }
// if ((self->accZ & 0x800) == 0x800)
// {
// self->accZ -= 4096;
// }
int SC7A20H::getAccelerometerX(void)
{
return this->accX;
}
int SC7A20H::getAccelerometerY(void)
{
return this->accY;
}
int SC7A20H::getAccelerometerZ(void)
{
return this->accZ;
}
int SC7A20H::getStrength(void)
{
int x = getAccelerometerX();
int y = getAccelerometerY();
int z = getAccelerometerZ();
return sqrt(x * x + y * y + z * z);
}
5、喇叭的驱动 实现了一个基于 ESP32 的 I2S 音频播放器,能够通过网络流媒体播放 MP3 音频文件。通过 WiFi 连接和 I2S 接口,实现了音频数据的接收和解码,并通过回调函数提供了丰富的音频信息反馈。修改自己的wifi信息之后,编译上传,如无意外就可以听到从喇叭传出来悠扬清脆的歌声。
#include "Arduino.h"
#include "WiFiMulti.h"
#include "Audio.h"
#include "SPI.h"
// Digital I/O used
#define I2S_DOUT 45
#define I2S_BCLK 0
#define I2S_LRC38
Audio audio;
WiFiMulti wifiMulti;
String ssid = "";
String password = "";
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
wifiMulti.addAP(ssid.c_str(), password.c_str());
wifiMulti.run();
if (WiFi.status() != WL_CONNECTED) {
WiFi.disconnect(true);
wifiMulti.run();
}
audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
audio.setVolume(20);// 0...21
const char *url = "http://42.193.120.65:8002/ygzjhyl.mp3";
// const char * url = "http://42.193.120.65:8002/520AM.mp3";
audio.connecttohost(url);
}
void loop() {
audio.loop();
if (Serial.available()) {// put streamURL in serial monitor
audio.stopSong();
String r = Serial.readString();
r.trim();
if (r.length() > 5) audio.connecttohost(r.c_str());
log_i("free heap=%i", ESP.getFreeHeap());
}
}
static/image/hrline/1.gif
第五章 LVGL图形库应用案例1、LVGL图形库移植
LVGL,即Light and Versatile Graphics Library,是一款开源的嵌入式图形库,专门面向资源受限的嵌入式系统而设计。它功能强大,提供了丰富的图形组件,助力开发者快速搭建现代化且精美的用户界面。得益于LVGL的强大功能,开发者能够构建出视觉效果极佳的界面。在色彩搭配上,支持多种色彩模式和调色板,能够实现丰富且和谐的色彩组合,从清新淡雅到浓郁鲜明,满足不同风格需求。图形绘制细腻,无论是按钮的边角弧度、图表的线条粗细,还是图像的像素呈现,都能做到精致入微。组件布局合理,各元素之间的间距、层次分明,不仅美观还极具实用性,为用户带来了舒适的视觉体验。在图形组件方面,LVGL十分全面。基础组件涵盖了按钮、标签、滑块、列表、图表、图像等常用元素,满足开发者构建基础界面的需求。而高级组件更是支持表格、日历、键盘、文件浏览器等复杂UI元素,为开发者打造功能丰富的应用提供了可能。LVGL还支持多种动画效果和过渡特效,能够为用户界面增添流畅的交互体验,极大地提升用户的使用感受。在社区与生态系统建设上,它提供了详尽的文档和教程,帮助开发者快速上手。同时,大量的示例代码和演示项目,方便开发者参考学习,迅速掌握开发技巧。其活跃的社区、持续的更新以及强大的功能,使其成为嵌入式系统开发者的首选图形库之一。随着物联网和智能设备的迅猛发展,LVGL的应用前景将愈发广阔,有望在更多场景中大放异彩。因此,在嵌入系统领域,多少都要学习一下LVGL,汲取其中的精华,精通其逻辑原理。 在本案例中实现了始化硬件(XL95x5 扩展芯片和 LCD 显示屏),并配置 LVGL 库以在 LCD 上显示图形界面。通过Lgfx提供的 DMA驱动 和得益于esp32s3的宽裕 SPIRAM,实现了高效的图形数据传输和显示刷新。本案例实现了小米澎湃系统的开机画面,实际是播放一个gif文件,也是相当有趣的尝试。本案例已开启examples和demo的测试环境,可以非常方便的对lvgl自带的教程进行钻研学习,学习lvgl最好的途径也就是运行lvgl自带的案例了,学习完全部教程就可以自行开发一些简单的项目了。 #ifndef _MAIN_H_
#define _MAIN_H_
#include "Lcd_handler.hpp"
#include <LovyanGFX.hpp>
#include "lvgl.h"
#include "examples/lv_examples.h"
#include "demos/lv_demos.h"
// #include "tiger_lcd_bsp.hpp"
// static SSD1306 oled;
#include "XL95x5_Driver.h"
// 在ESP32上以自定义设置使用LovyanGFX时的设置示例
// XL95x5对配置进行初始化
XL95x5 ExpandPin(XL95x5_IIC_ADDRESS, XL95x5_SDA, XL95x5_SCL);
static K10_Lcd tft;
static void lvgl_begin(void);
static void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area,
lv_color_t *color_p);
lv_disp_t *Lcd_disp; /*Descriptor of a display driver*/
static void lvgl_begin(void) {
#define LVGL_HOR_RES (240)
#define LVGL_VER_RES (320)
// #define DISP_BUF_SIZE (LVGL_HOR_RES * 100)
ExpandPin.begin();
ExpandPin.read_all_reg();// Read all registers
ExpandPin.pinMode(XL95x5_GPIO_NUM_0,OUTPUT);
ExpandPin.digitalWrite(XL95x5_GPIO_NUM_0, LOW);
delay(10);
ExpandPin.digitalWrite(XL95x5_GPIO_NUM_0, HIGH);
// ExpandPin.digitalWrite(XL95x5_GPIO_NUM_15, HIGH);
tft.init();
tft.initDMA();
tft.setRotation(0);
lv_init();
static lv_disp_draw_buf_t draw_buf;
size_t DISP_BUF_SIZE =
sizeof(lv_color_t) * (LVGL_HOR_RES * LVGL_VER_RES);// best operation.
static lv_color_t *buf1 = (lv_color_t *)heap_caps_malloc(
DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);
static lv_color_t *buf2 = (lv_color_t *)heap_caps_malloc(
DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);
lv_disp_draw_buf_init(&draw_buf, buf1, buf2,
DISP_BUF_SIZE); /*Initialize the display buffer*/
/*-----------------------------------
* Register the display in LVGL
*----------------------------------*/
static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
/*Set up the functions to access to your display*/
/*Set the resolution of the display*/
disp_drv.hor_res = LVGL_HOR_RES;
disp_drv.ver_res = LVGL_VER_RES;
/*Used to copy the buffer's content to the display*/
disp_drv.flush_cb = my_disp_flush;
/*Set a display buffer*/
disp_drv.draw_buf = &draw_buf;
disp_drv.full_refresh = 1;
/*Finally register the driver*/
lv_disp_drv_register(&disp_drv);
}
static void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area,
lv_color_t *color_p) {
int w = (area->x2 - area->x1 + 1);
int h = (area->y2 - area->y1 + 1);
/* Start new TFT transaction */
tft.startWrite();
/* set the working window */
tft.setAddrWindow(area->x1, area->y1, w, h);
// tft.writePixelsDMA((lgfx::swap565_t* )&color_p->full, w * h);
tft.writePixelsDMA((lgfx::rgb565_t* )&color_p->full, w * h);
/* terminate TFT transaction */
tft.endWrite();
/* tell lvgl that flushing is done */
lv_disp_flush_ready(disp);
}
#endif
2、老司机图片浏览器实现一个基于 LVGL图片浏览器功能。它能够从 SD 卡中读取 PNG 图片文件,并通过 LVGL 图形库在屏幕上显示这些图片。该程序的主要功能是浏览存储在 SD 卡中的 PNG 图片文件。程序通过 LVGL 的文件系统接口访问 SD 卡,并递归查找所有 PNG 文件用户可以通过按钮或定时器切换图片。程序通过按钮(ButtonA 和 ButtonB)与用户交互,允许用户手动切换图片。自动切换:程序还支持定时器自动切换图片功能,每隔一定时间自动显示下一张图片。主要的学习内容为在lvgl之上构建文件sd卡的文件系统,图片切换的逻辑。程序中提供基于Python的图片缩放工具代码,可以用来处理自己的图片,适配到K10的屏幕使用,可以给自己做一个电子相册,值得动手尝试一下。
/**
* @file camera.ino
* @brief The running example will display the camera feed on the screen
*
* @copyright Copyright (c) 2010 DFRobot Co.Ltd (http://www.dfrobot.com)
* @license The MIT License (MIT)
* @author tangjie(jie.tang@dfrobot.com)
* @version V1.0
* @date 2024-6-21
*/
#include "main.h"
#include <demos/lv_demos.h>
#include <examples/lv_examples.h>
#include "SD_Card.h"
#include "Button_handler.h"
SdCard tf;
Button *buttonA = new Button(eP5_KeyA);
Button *buttonB = new Button(eP11_KeyB);
Button *buttonAB = new Button(eP5_KeyA, eP11_KeyB);
LV_IMG_DECLARE(output)
SemaphoreHandle_t xGuiSemaphore;
#define MAX_FILES 100
#define MAX_PATH_LEN 256
lv_obj_t *img;
lv_obj_t *btn_prev;
lv_obj_t *btn_next;
#define MAX_PNG_FILES 100// 假设最多存储 1000 个 PNG 文件路径
#define MAX_PATH_LENGTH 256 // 假设路径长度不超过 255 个字符
// 全局变量
char img_paths;// 存储 PNG 文件路径的数组
int png_count = 0;// 记录找到的 PNG 文件数量
// char *img_paths;
// int img_count = 0;
int current_img_index = 0;
void load_img(int index);
void prev_img_event_cb(lv_event_t *e);
void next_img_event_cb(lv_event_t *e);
void create_img_browser(void);
int my_strcasecmp(const char *s1, const char *s2);
void find_png_files_recursive(const char *base_path);
void KEYB(void);
void KEYA(void);
TimerHandle_t xAutoSwitchTimer = NULL;
void vTimerCallback( TimerHandle_t pxTimer )
{
current_img_index++;
if (current_img_index >= png_count) {
current_img_index = 0;
}
load_img(current_img_index);
}
void setup() {
Serial.begin(115200);
// Wire.begin(47,48);
Wire.begin(47, 48);
pinMode(45,OUTPUT);
digitalWrite(45,HIGH);
lvgl_begin();
xGuiSemaphore = xSemaphoreCreateMutex();
xSemaphoreGive(xGuiSemaphore);
tf.init();
lv_fs_fatfs_init();
find_png_files_recursive("S:");
printf("Found %d PNG files:\n", png_count);
for (int i = 0; i < png_count; i++) {
printf("Stored PNG file: %s\n", img_paths);
}
create_img_browser();
buttonA->setPressedCallback(&KEYA);
buttonB->setPressedCallback(&KEYB);
if (png_count > 0) {
// 加载第一张图片
load_img(0);
} else {
printf("No .jpg files found!\n");
}
xAutoSwitchTimer = xTimerCreate(
"AutoSwitchTimer", // 定时器名称
pdMS_TO_TICKS(10000), // 定时器周期,2000ms = 2s
pdTRUE, // 自动重载
(void *)0, // 定时器ID
vTimerCallback // 定时器回调函数
);
// 启动定时器
if (xAutoSwitchTimer != NULL) {
xTimerStart(xAutoSwitchTimer, 0);
}
}
void loop() {
/* Try to take the semaphore, call lvgl related function on success */
vTaskDelay(pdMS_TO_TICKS(10));
/* Try to take the semaphore, call lvgl related function on success */
if (pdTRUE == xSemaphoreTake(xGuiSemaphore, portMAX_DELAY)) {
lv_task_handler();
xSemaphoreGive(xGuiSemaphore);
}
}
void load_img(int index) {
if (index < 0 || index >= png_count) {
return;// 超出范围
}
lv_img_set_src(img, img_paths);
}
void prev_img_event_cb(lv_event_t *e) {
current_img_index--;
if (current_img_index < 0) {
current_img_index = png_count - 1;
}
load_img(current_img_index);
}
void next_img_event_cb(lv_event_t *e) {
current_img_index++;
if (current_img_index >= png_count) {
current_img_index = 0;
}
load_img(current_img_index);
}
void create_img_browser(void) {
// 创建一个样式对象
static lv_style_t btn_style;
lv_style_init(&btn_style);
// 设置按钮的背景颜色
lv_style_set_bg_color(&btn_style, lv_palette_main(LV_PALETTE_CYAN));
// 设置按钮的背景透明度
lv_style_set_bg_opa(&btn_style, 40);
// 设置按钮的文本颜色
lv_style_set_text_color(&btn_style, lv_color_white());
img = lv_img_create(lv_scr_act());
lv_obj_align(img, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_clear_flag(img, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
btn_prev = lv_btn_create(lv_scr_act());
lv_obj_add_style(btn_prev, &btn_style, 0); // 将样式应用到按钮上
lv_obj_align(btn_prev, LV_ALIGN_BOTTOM_LEFT, 10, -10);
lv_obj_t *label_prev = lv_label_create(btn_prev);
lv_label_set_text(label_prev, "Prev");
btn_next = lv_btn_create(lv_scr_act());
lv_obj_add_style(btn_next, &btn_style, 0); // 将样式应用到按钮上
lv_obj_align(btn_next, LV_ALIGN_BOTTOM_RIGHT, -10, -10);
lv_obj_t *label_next = lv_label_create(btn_next);
lv_label_set_text(label_next, "Next");
lv_obj_add_event_cb(btn_prev, prev_img_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(btn_next, next_img_event_cb, LV_EVENT_CLICKED, NULL);
}
// void lvgl_init(void) {
// lv_init();
// lv_fs_if_init();// 初始化文件系统接口
// // 其他初始化代码,如显示驱动、输入设备等
// }
// 自定义不区分大小写的字符串比较函数
int my_strcasecmp(const char *s1, const char *s2) {
while (*s1 && *s2) {
int diff = tolower(*s1) - tolower(*s2);
if (diff != 0) {
return diff;
}
s1++;
s2++;
}
return tolower(*s1) - tolower(*s2);
}
// 递归查找 PNG 文件并将路径存储到全局数组中
void find_png_files_recursive(const char *base_path) {
lv_fs_dir_t dir;
lv_fs_res_t res;
char fn; // 用于存储文件名或目录名
char full_path;// 用于存储完整路径
// 打开当前目录
res = lv_fs_dir_open(&dir, base_path);
if (res != LV_FS_RES_OK) {
printf("Failed to open directory: %s\n", base_path);
return;// 返回表示没有找到文件
}
// 遍历目录中的文件和子目录
while (png_count < MAX_PNG_FILES) {
res = lv_fs_dir_read(&dir, fn);
if (res != LV_FS_RES_OK || fn == '\0') {
break;// 读取结束或出错
}
// 构建完整路径
snprintf(full_path, MAX_PATH_LENGTH, "%s/%s", base_path, fn);
printf("Checking: %s\n", full_path);// 调试:打印正在检查的路径
// 检查是否为目录
lv_fs_file_t file;
if (lv_fs_open(&file, full_path, LV_FS_MODE_RD) == LV_FS_RES_OK) {
lv_fs_close(&file);// 关闭文件
} else {
// 如果是目录,递归查找
find_png_files_recursive(full_path);
}
// 检查文件扩展名是否为 ".png"
const char *filename = lv_fs_get_last(fn); // 提取文件名
const char *ext = lv_fs_get_ext(filename);// 获取扩展名
if (strcmp(ext, "png") == 0) {
// 将文件路径存储到全局数组中
snprintf(img_paths, MAX_PATH_LENGTH, "%s", full_path);
printf("Found PNG file: %s\n", img_paths);
png_count++;
}
}
// 关闭目录
lv_fs_dir_close(&dir);
}
void KEYB(void)// Callback function for button B
{
current_img_index++;
if (current_img_index >= png_count) {
current_img_index = 0;
}
load_img(current_img_index);
}
void KEYA(void)// Callback function for button A
{
current_img_index--;
if (current_img_index < 0) {
current_img_index = png_count - 1;
}
load_img(current_img_index);
}
提供一段可以将图片修改成可以在K10上播放的尺寸的python代码:(最好在jupyter notebook环境运行)import os
from PIL import Image
from tqdm import tqdm
# 定义输入和输出文件夹
input_folder = 'path/to/your/input/folder'
output_folder = 'path/to/your/output/folder'
# 确保输出文件夹存在
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# 获取所有jpg文件
jpg_files =
# 处理每个文件
for filename in tqdm(jpg_files, desc="Processing images"):
# 打开图像
img_path = os.path.join(input_folder, filename)
img = Image.open(img_path)
# 调整大小
img = img.resize((240, 320))
# 转换为png并保存
output_filename = os.path.splitext(filename) + '.png'
output_path = os.path.join(output_folder, output_filename)
img.save(output_path, 'PNG')
print("处理完成!")
3、基于LVGL的视频播放器在过去,利用 esp32 实现复杂功能曾困难重重,遥不可及。但随着芯片核心能力的持续提升以及软件技术的飞速发展,如今用 esp32 芯片播放视频已成为现实。本案例成功构建了一个简易的 MJPEG 视频播放器,该播放器借助 LVGL 库来展示视频帧,同时运用 ESP32_JPEG_Library 库对 MJPEG 文件进行解码。其视频播放的核心逻辑涵盖多个关键环节:首先对显示设备进行初始化,接着读取 MJPEG 文件,随后对文件中的数据进行解码,并将解码后的视频帧显示出来;在播放过程中,还通过控制帧率的方式,保证视频播放的流畅性,为用户带来良好的观看体验。需要注意的是ESP32_JPEG_Library 库不是一个开源库,库里的真实内容是已经预编译好的.a文件,需要在.ini文件里进行引用,不然会报错,在Arduino环境则不会出现这种现象。目前播放的帧率仍然还有优化的空间,有兴趣的朋友可以在此基础上继续发力。该技术可以应用到动态背景上,例如在做天气预报项目时,使用个小短片来替代传统图片来展示天气状况,使得项目更加出彩。 项目文件夹中也附带了将视频文件转化为k10可以播放的mpeg文件工具使用方法指引。
#include <Arduino.h>
#include <lvgl.h>
#include "SD_Card.h"
#include "main.h"
#include <vector>
#define MP3_FOLDER "/music"
#define PIC_FOLDER "/pic"
#define MJPEG_FOLDER "/mjpeg"
#define DEMO_LVGL 1
#define DEMO_PIC 2
#define DEMO_MJPEG 3
#define DEMO_MP3 4
#define CURR_DEMO DEMO_MJPEG //DEMO_PIC
std::vector<char *> v_fileContent;
File dir;
void listDir(const char *dirname, uint8_t levels);
int curr_show_index = 0;
#if CURR_DEMO == DEMO_LVGL
#include <demos/lv_demos.h>
#elif CURR_DEMO == DEMO_PIC
#include <ESP32_JPEG_Library.h>
#define MAX_PIC_FILE_SIZE 307200 //= 480 * 320 * 2
static uint8_t *img_read_data = NULL;
static lv_obj_t *img_obj = NULL;
static uint16_t *pic_img_data = NULL;
static uint32_t img_ticker = 0;
static lv_img_dsc_t pic_img_dsc = {
.header = {
.cf = LV_IMG_CF_TRUE_COLOR,
.always_zero = 0,
.reserved = 0,
.w = 480,
.h = 320,
},
.data_size = 480 * 320 * 2,
.data = NULL,
};
const char picDir[] = PIC_FOLDER;
static jpeg_error_t esp_jpeg_decoder_one_image(uint8_t *input_buf, int len, uint8_t *output_buf)
{
jpeg_error_t ret = JPEG_ERR_OK;
int inbuf_consumed = 0;
// Generate default configuration
jpeg_dec_config_t config = {
.output_type = JPEG_RAW_TYPE_RGB565_BE,
.rotate = JPEG_ROTATE_0D,
};
// Empty handle to jpeg_decoder
jpeg_dec_handle_t *jpeg_dec = NULL;
// Create jpeg_dec
jpeg_dec = jpeg_dec_open(&config);
// Create io_callback handle
jpeg_dec_io_t *jpeg_io = (jpeg_dec_io_t *)calloc(1, sizeof(jpeg_dec_io_t));
if (jpeg_io == NULL)
{
return JPEG_ERR_MEM;
}
// Create out_info handle
jpeg_dec_header_info_t *out_info = (jpeg_dec_header_info_t *)calloc(1, sizeof(jpeg_dec_header_info_t));
if (out_info == NULL)
{
return JPEG_ERR_MEM;
}
// Set input buffer and buffer len to io_callback
jpeg_io->inbuf = input_buf;
jpeg_io->inbuf_len = len;
// Parse jpeg picture header and get picture for user and decoder
ret = jpeg_dec_parse_header(jpeg_dec, jpeg_io, out_info);
if (ret < 0)
{
esp_rom_printf("JPEG decode parse failed\n");
goto _exit;
}
jpeg_io->outbuf = output_buf;
inbuf_consumed = jpeg_io->inbuf_len - jpeg_io->inbuf_remain;
jpeg_io->inbuf = input_buf + inbuf_consumed;
jpeg_io->inbuf_len = jpeg_io->inbuf_remain;
// Start decode jpeg raw data
ret = jpeg_dec_process(jpeg_dec, jpeg_io);
if (ret < 0)
{
esp_rom_printf("JPEG decode process failed\n");
goto _exit;
}
_exit:
// Decoder deinitialize
jpeg_dec_close(jpeg_dec);
free(out_info);
free(jpeg_io);
return ret;
}
void show_new_pic()
{
if (v_fileContent.size() > 0)
{
const char *s = (const char *)v_fileContent;
esp_rom_printf("showing %s\n", s);
memset(img_read_data, 0, MAX_PIC_FILE_SIZE);
File loadFile;
loadFile = SD_MMC.open(s, FILE_READ);
if (!loadFile)
{
return;
}
char buff;
size_t bytesRead = 0;
while (loadFile.available() && bytesRead < MAX_PIC_FILE_SIZE)
{
size_t len = min((size_t)loadFile.available(), min(sizeof(buff), MAX_PIC_FILE_SIZE - bytesRead));
loadFile.read((uint8_t *)buff, len);
memcpy(img_read_data + bytesRead, buff, len);
bytesRead += len;
}
loadFile.close();
jpeg_error_t ret = JPEG_ERR_OK;
ret = esp_jpeg_decoder_one_image(img_read_data, bytesRead, (uint8_t *)pic_img_data);
if (ret != JPEG_ERR_OK)
{
esp_rom_printf("JPEG decode failed - %d\n", (int)ret);
return;
}
bsp_display_lock(0);
lv_obj_invalidate(img_obj);
bsp_display_unlock();
curr_show_index--;
if (curr_show_index < 0)
{
curr_show_index = v_fileContent.size() - 1;
}
}
}
#elif CURR_DEMO == DEMO_MJPEG
#include <ESP32_JPEG_Library.h>
#define MJPEG_OUTPUT_SIZE (320*240*2) // memory for a output image frame
#define MJPEG_BUFFER_SIZE (MJPEG_OUTPUT_SIZE / 20) // memory for a single JPEG frame
#define MAX_PIC_FILE_SIZE 320*240*2 //= 480 * 320 * 2
#include "MjpegClass.h"
static MjpegClass mjpeg;
static uint8_t *mjpeg_buf = NULL;
static lv_obj_t *img_obj = NULL;
static uint16_t *pic_img_data = NULL;
static uint32_t img_ticker = 0;
static lv_img_dsc_t pic_img_dsc = {
.header = {
.cf = LV_IMG_CF_TRUE_COLOR,
.always_zero = 0,
.reserved = 0,
.w = 320,
.h = 240,
},
.data_size = 320 * 240 * 2,
.data = NULL,
};
const char mjpegDir[] = MJPEG_FOLDER;
void show_new_mjpeg()
{
if (v_fileContent.size() < 1)
return;
const char *s = (const char *)v_fileContent;
esp_rom_printf("showing %s\n", s);
File mjpegFile =SD.open(s, "r");
if (!mjpegFile || mjpegFile.isDirectory())
{
esp_rom_printf("ERROR: Failed to open %s file for reading\n", s);
return;
}
else
{
esp_rom_printf("MJPEG start\n");
if (!mjpeg.setup(
&mjpegFile, mjpeg_buf,
pic_img_data, MJPEG_OUTPUT_SIZE, true /* useBigEndian */))
{
esp_rom_printf("mjpeg.setup() failed!\n");
}
else
{
uint32_t currFrame = 0;
uint32_t ts1 = 0;
uint32_t ts2 = 0;
while (mjpegFile.available() && mjpeg.readMjpegBuf())
{
ts1 = millis();
mjpeg.decodeJpg();
lv_obj_invalidate(img_obj);
lv_timer_handler();
ts2 = millis() - ts1;
if (ts2 < 40 && ts2 > 2)
{
vTaskDelay(40 - ts2);
}
else
{
vTaskDelay(2);
}
currFrame++;
}
esp_rom_printf("MJPEG end\n");
mjpegFile.close();
curr_show_index--;
if (curr_show_index < 0)
{
curr_show_index = v_fileContent.size() - 1;
}
}
}
}
#elif CURR_DEMO == DEMO_MP3
#include "Audio.h"
Audio audio;
const char audioDir[] = MP3_FOLDER;
lv_obj_t *lb_info = NULL;
#endif
/**
* Set the rotation degree:
* - 0: 0 degree
* - 90: 90 degree
* - 180: 180 degree
* - 270: 270 degree
*
*/
#define LVGL_PORT_ROTATION_DEGREE (90)
SdCard tf;
void setup()
{
String title = "LVGL porting example";
Serial.begin(115200);
tf.init();
init_board();
lvgl_begin();
#if CURR_DEMO != DEMO_LVGL
esp_rom_printf("Initialize tf card\n");
#endif
esp_rom_printf("Initialize panel device\n");
/* Lock the mutex due to the LVGL APIs are not thread-safe */
#if CURR_DEMO == DEMO_LVGL
bsp_display_lock(0);
lv_demo_stress();
bsp_display_unlock();
#elif CURR_DEMO == DEMO_PIC
pic_img_data = (uint16_t *)heap_caps_aligned_alloc(16, 480 * 320 * 2, MALLOC_CAP_SPIRAM);
memset(pic_img_data, 0, 480 * 320 * 2);
img_read_data = (uint8_t *)heap_caps_malloc(MAX_PIC_FILE_SIZE, MALLOC_CAP_SPIRAM);
memset(img_read_data, 0, MAX_PIC_FILE_SIZE);
pic_img_dsc.data = (uint8_t *)pic_img_data;
bsp_display_lock(0);
img_obj = lv_img_create(lv_scr_act());
lv_img_set_src(img_obj, &pic_img_dsc);
lv_obj_center(img_obj);
bsp_display_unlock();
dir = SD_MMC.open(picDir);
listDir(SD_MMC, picDir, 1);
if (v_fileContent.size() > 0)
{
curr_show_index = v_fileContent.size() - 1;
}
show_new_pic();
#elif CURR_DEMO == DEMO_MJPEG
pic_img_data = (uint16_t *)heap_caps_aligned_alloc(16, MJPEG_OUTPUT_SIZE, MALLOC_CAP_SPIRAM);
memset(pic_img_data, 0, MJPEG_OUTPUT_SIZE);
mjpeg_buf = (uint8_t *)heap_caps_malloc(MAX_PIC_FILE_SIZE, MALLOC_CAP_SPIRAM);
memset(mjpeg_buf, 0, MAX_PIC_FILE_SIZE);
pic_img_dsc.data = (uint8_t *)pic_img_data;
img_obj = lv_img_create(lv_scr_act());
lv_img_set_src(img_obj, &pic_img_dsc);
lv_obj_center(img_obj);
dir = SD.open(mjpegDir);
listDir(mjpegDir, 1);
if (v_fileContent.size() > 0)
{
curr_show_index = v_fileContent.size() - 1;
}
show_new_mjpeg();
#elif CURR_DEMO == DEMO_MP3
bsp_display_lock(0);
lb_info = lv_label_create(lv_scr_act());
lv_obj_center(lb_info);
lv_label_set_text(lb_info, "MP3 PLAYER");
lv_obj_set_style_text_line_space(lb_info, 12, 0);
lv_label_set_long_mode(lb_info, LV_LABEL_LONG_WRAP);
bsp_display_unlock();
audio.setPinout(AUDIO_I2S_BCK_IO, AUDIO_I2S_LRCK_IO, AUDIO_I2S_DO_IO);
audio.setVolume(17); // 0...21 Will need to add a volume setting in the app
dir = SD_MMC.open(audioDir);
listDir(SD_MMC, audioDir, 1);
if (v_fileContent.size() > 0)
{
const char *s = (const char *)v_fileContent;
esp_rom_printf("playing %s\n", s);
audio.connecttoFS(SD_MMC, s);
v_fileContent.pop_back();
}
#endif
}
void loop()
{
#if CURR_DEMO == DEMO_LVGL
delay(1000);
#elif CURR_DEMO == DEMO_PIC
delay(1000);
img_ticker++;
if (img_ticker >= 8)
{
img_ticker = 0;
show_new_pic();
}
#elif CURR_DEMO == DEMO_MJPEG
delay(1000);
show_new_mjpeg();
#elif CURR_DEMO == DEMO_MP3
audio.loop();
#endif
lv_timer_handler();
vTaskDelay(5);
}
void listDir( const char *dirname, uint8_t levels)
{
esp_rom_printf("Listing directory: %s\n", dirname);
File root = SD.open(dirname);
if (!root)
{
esp_rom_printf("Failed to open directory\n");
return;
}
if (!root.isDirectory())
{
esp_rom_printf("Not a directory\n");
return;
}
File file = root.openNextFile();
while (file)
{
if (file.isDirectory())
{
if (levels)
{
listDir(file.path(), levels - 1);
}
}
else
{
v_fileContent.insert(v_fileContent.begin(), strdup(file.path()));
}
file = root.openNextFile();
}
esp_rom_printf("found files : %d\n", v_fileContent.size());
root.close();
file.close();
}
#if CURR_DEMO == DEMO_MP3
// optional
void audio_info(const char *info)
{
esp_rom_printf("info \n");
esp_rom_printf(info);
bsp_display_lock(0);
lv_label_set_text(lb_info, info);
bsp_display_unlock();
}
void audio_id3data(const char *info)
{ // id3 metadata
esp_rom_printf("id3data \n");
bsp_display_lock(0);
lv_label_set_text(lb_info, info);
bsp_display_unlock();
}
void audio_eof_mp3(const char *info)
{ // end of file
esp_rom_printf("eof_mp3 \n");
bsp_display_lock(0);
lv_label_set_text(lb_info, info);
bsp_display_unlock();
if (v_fileContent.size() == 0)
{
return;
}
const char *s = (const char *)v_fileContent;
esp_rom_printf("playing %s\n", s);
audio.connecttoFS(SD_MMC, s);
v_fileContent.pop_back();
}
#endif
(播放功夫电影片段,像素感人)提供一条视频转化工具函数:用ffmpeg 把avi视频转化为 mjpeg文件,分辨率320*240
ffmpeg -i write_system.avi -vf "scale=320:240:force_original_aspect_ratio=decrease,pad=320:240:(ow-iw)/2:(oh-ih)/2" -c:v mjpeg output.mjpeg
static/image/hrline/1.gif
第六章 高级综合应用案例
1、贪吃蛇游戏这是一个基于 Arduino 平台的贪吃蛇游戏实现,游戏的核心逻辑是控制蛇的移动和生长。蛇的身体由一系列坐标点组成,每次移动时,蛇头向前移动一个单位,身体的其他部分依次跟随。当蛇头碰到食物时,蛇的身体会增长一个单位,同时食物会随机出现在屏幕上的其他位置。使用 LovyanGFX 库进行图形渲染。通过 LGFX 和 LGFX_Sprite 类,可以方便地在屏幕上绘制图形。蛇的身体、食物和**效果等都是通过绘制矩形来实现的。通过设置不同的颜色和阴影效果,可以增强游戏的视觉效果。LGFX_Sprite显示缓存机制,在完成图形的所有元素绘制后进行一次整体的输出,这样减少了图形输出的闪烁,更加稳定。游戏使用AB键进行控制,在K10板载资源紧缺的条件下,完成了游戏的交互功能,可作为其他游戏的设计参考。 2、贪吃蛇游戏升级版 第二版的贪吃蛇游戏是基于前一版的基础增加了一些效果,例如食物是自动随时间增长,同一时间屏幕上可以有很多食物,蛇的身体颜色会随吃到的食物颜色变化,连续吃到食物的时候,烟花也是可以连续**,另外一个更重要的改变是控制方式不再是基于K10板载的两个按键来控制。在本次活动中专门为K10制作了一个专用的扩展板,把从金手指引出的引脚资源利用起来。
K10板载配置已支持自家的gravity接口,为满足更多功能拓展需求,专门设计了一款扩展板。扩展板增设了3个I2C、1个UART的grove接口以及1对I2C的qwiic接口。国内M5stack和Seeed公司生产的传感器大多基于grove接口。借助这款扩展板,K10能够毫无障碍地连接这两家公司数以千计、各式各样的传感器,极大地丰富了可获取的数据类型和应用场景。而qwiic接口,则是国外嵌入式系统领域的领军企业Adafruit公司采用的传感器连接方式。Adafruit堪称嵌入式系统创客领域的鼻祖,其拥有的传感器种类极为丰富。通过在扩展板上设置qwiic接口,K10也能够接入Adafruit公司的各类传感器。如此一来,有了这块扩展板,兼容gravity、grove、qwiic三种接口,拥有了一个庞大的传感器资源库,能够轻松开展各种创新项目,为开发者带来极大的便利和丰富的创作灵感。(注意:早期的K10硬件插在microbit插座上会比较松动,后期的产品已经改良,可以放心使用) 在本游戏中使用引出的p0,p1引脚作为外置按键的接口,使用外置按键使得游戏操作的手感更好,控制更加灵活。该扩展板的设计内容也将开源供大家参考使用,后续还会继续优化该扩展板的布局和功能。
(专用扩展板,支持grove,qwiic接口)
https://www.bilibili.com/video/BV1WgRJYgEQg/?share_source=copy_web&vd_source=bed775561efaa18e90586ab291600126&share_source=weixin
3、板载资源应用综合案例 该项目基于LVGL图形库构建界面,采用MVC (ModelViewController)架构设计,它将软件系统分为三个主要部分,以实现关注点分离,提高软件的可维护性、可扩展性和可测试性,模型(Model)负责处理应用程序的业务逻辑和数据,视图(View)主要用于展示数据给用户,通常是用户界面的一部分。控制器(Controller)作为模型和视图之间的桥梁,负责接收用户的输入和操作,然后根据用户的请求调用相应的模型方法来处理业务逻辑,并将处理结果返回给视图进行展示。使用FreeRTOS进行任务管理,定时器控制等高级组件,保证多功能并行运行,支持按键切换界面。包括以下3个模块,具有报时、天气预报、室温环境监测等功能,能够很好地展示板载传感器的功能。 项目实现细节限于篇幅就不再一一展开了,等待热爱学习的你来发掘。如在运行过程中出现问题,可以来留言探讨。
(1)时钟/天气模块:• 支持WiFi连接 • 从高德地图API获取天气信息• 显示温度、湿度、风向等数据• 板载传感器数据采集(2)MPU传感器模块: • 实时显示传感器数据 • 使用图表显示加速度数据• 带有弧形指示器(3)Bubble模块:• 气泡动画效果
温馨提示:需要在TimerModel.cpp文件中修改wifi账号信息和在TimerModel.h填充高德天气api密钥
UI首页代码:
#include "TimerView.h"
// #include "font/font_alibaba.h"
// LV_FONT_DECLARE(font_Alibaba);
#define UI_BG_COLOR lv_color_white()
#define UI_FRAME_COLOR lv_color_white()
#define UI_FONT_COLORlv_color_black()
#define UI_PAGE_COUNT3
LV_FONT_DECLARE(lv_font_chinese_16)
LV_FONT_DECLARE(lv_font_chinese_24)
LV_FONT_DECLARE(lv_font_montserrat_16)
LV_IMG_DECLARE(bg_paper)
// LV_FONT_DECLARE(lv_font_montserrat_24)
static lv_point_t line_points[] = {{-240, 0}, {240, 0}};
TimerView::TimerView() {
}
TimerView::~TimerView() {
}
/// @brief
void TimerView::create() {
// lv_obj_t *bg =lv_img_create(NULL);
// lv_img_set_src(bg,&bg_paper);
#if 1
ui.root = lv_obj_create(NULL);
ui.clockDiv = lv_obj_create(ui.root);
lv_obj_set_size(ui.clockDiv, LV_HOR_RES, LV_VER_RES/2);
lv_obj_set_style_border_width(ui.clockDiv, 0, 0);
lv_obj_set_style_radius(ui.clockDiv, 0, 0);
lv_obj_set_style_bg_color(ui.clockDiv, UI_BG_COLOR, 0);
lv_obj_clear_flag(ui.clockDiv,
LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
lv_obj_align(ui.clockDiv, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_set_style_pad_all(ui.clockDiv, 0, 0);
lv_obj_t *img2 = lv_img_create(ui.clockDiv);
lv_img_set_src(img2,
LV_SYMBOL_BLUETOOTH LV_SYMBOL_WIFI LV_SYMBOL_BATTERY_FULL);
lv_obj_align(img2, LV_ALIGN_TOP_RIGHT, -5, 0);
static lv_obj_t *label_telecom = lv_label_create(ui.clockDiv);
lv_obj_set_size(label_telecom, 120, 30); /*Fix size*/
lv_obj_set_style_text_font(label_telecom, &lv_font_chinese_16, 0);
lv_obj_set_style_text_color(label_telecom,lv_color_hex(0xFF0000), 0);
lv_label_set_long_mode(label_telecom, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_label_set_text(label_telecom, "新年快乐,万事如意,合家安康!");
lv_obj_align(label_telecom, LV_ALIGN_TOP_LEFT, 0, 0);
ui.hourDiv = lv_obj_create(ui.clockDiv);
lv_obj_set_size(ui.hourDiv, 50, 50);
lv_obj_align(ui.hourDiv, LV_ALIGN_LEFT_MID, LV_HOR_RES / 6, 0);
lv_obj_set_style_bg_color(ui.hourDiv, UI_FRAME_COLOR, 0);
lv_obj_clear_flag(ui.hourDiv, LV_OBJ_FLAG_SCROLLABLE);
ui.minDiv = lv_obj_create(ui.clockDiv);
lv_obj_set_size(ui.minDiv, 50, 50);
lv_obj_align(ui.minDiv, LV_ALIGN_RIGHT_MID, -LV_HOR_RES / 6, 0);
lv_obj_set_style_bg_color(ui.minDiv, UI_FRAME_COLOR, 0);
lv_obj_clear_flag(ui.minDiv, LV_OBJ_FLAG_SCROLLABLE);
ui.segLabel = lv_label_create(ui.clockDiv);
lv_obj_align(ui.segLabel, LV_ALIGN_CENTER, 0, -6);
lv_obj_set_style_text_font(ui.segLabel, &lv_font_montserrat_32, 0);
lv_label_set_text(ui.segLabel, ":");
lv_obj_set_style_text_color(ui.segLabel, UI_FONT_COLOR, 0);
ui.hourLabel = lv_label_create(ui.hourDiv);
lv_obj_center(ui.hourLabel);
lv_obj_set_style_text_font(ui.hourLabel, &lv_font_montserrat_32, 0);
lv_label_set_text(ui.hourLabel, "12");
lv_obj_set_style_text_color(ui.hourLabel, UI_FONT_COLOR, 0);
ui.minLabel = lv_label_create(ui.minDiv);
lv_obj_center(ui.minLabel);
lv_obj_set_style_text_font(ui.minLabel, &lv_font_montserrat_32, 0);
lv_label_set_text(ui.minLabel, "34");
lv_obj_set_style_text_color(ui.minLabel, UI_FONT_COLOR, 0);
lv_obj_t *timelabel = lv_label_create(ui.clockDiv);
// lv_obj_set_size(ui.city, 240, 30); /*Fix size*/
lv_obj_align(timelabel, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_set_style_border_color(timelabel, lv_color_white(), 0);
lv_obj_set_style_border_opa(timelabel, LV_OPA_0, 0);
lv_obj_set_style_text_color(timelabel, lv_palette_main(LV_PALETTE_BLUE), 0);
lv_obj_set_style_text_font(timelabel, &lv_font_chinese_24, 0);
lv_label_set_text_fmt(timelabel, "2025年1月1日 星期三");
ui.timelabel = timelabel;
lv_obj_t *sensorStation = lv_obj_create(ui.root);
lv_obj_set_size(sensorStation, LV_HOR_RES, LV_VER_RES /2);
lv_obj_set_style_border_width(sensorStation, 0, 0);
lv_obj_set_style_radius(sensorStation, 0, 0);
lv_obj_set_style_bg_color(sensorStation, UI_BG_COLOR, 0);
lv_obj_clear_flag(sensorStation,
LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
lv_obj_align(sensorStation, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_pad_all(sensorStation, 0, 0);
ui.sensorDiv = sensorStation;
// 创建一个样式对象
static lv_style_t label_style;
lv_style_init(&label_style);
// 设置边框颜色
lv_style_set_border_color(&label_style, lv_color_white());
// 设置边框透明度
lv_style_set_border_opa(&label_style, LV_OPA_0);
// 设置文本颜色
lv_style_set_text_color(&label_style, lv_palette_main(LV_PALETTE_BLUE));
// 设置文本字体
lv_style_set_text_font(&label_style, &lv_font_chinese_24);
lv_obj_t *sensor_label = lv_label_create(ui.sensorDiv);
// lv_obj_set_style_border_color(sensor_label, lv_color_white(), 0);
// lv_obj_set_style_border_opa(sensor_label, LV_OPA_0, 0);
// lv_obj_set_style_text_color(sensor_label, lv_palette_main(LV_PALETTE_BLUE),
// 0);
// lv_obj_set_style_text_font(sensor_label, &lv_font_chinese_24, 0);
// 将样式应用于 temperature 对象
lv_obj_add_style(sensor_label, &label_style, 0);
lv_obj_align(sensor_label, LV_ALIGN_TOP_MID, 0, 0);
lv_label_set_text_fmt(sensor_label, "室内环境");
lv_obj_t *temperature = lv_label_create(ui.sensorDiv);
// lv_obj_set_size(sensor_label, 240, 30); /*Fix size*/
lv_obj_add_style(temperature, &label_style, 0);
lv_obj_align(temperature, LV_ALIGN_LEFT_MID, 5, -20);
lv_label_set_text_fmt(temperature, "温度");
ui.temperature = temperature;
lv_obj_t *humidity = lv_label_create(ui.sensorDiv);
// lv_obj_set_size(sensor_label, 240, 30); /*Fix size*/
lv_obj_add_style(humidity, &label_style, 0);
lv_obj_align(humidity, LV_ALIGN_LEFT_MID, 5,20);
lv_label_set_text_fmt(humidity, "湿度");
ui.humidity = humidity;
lv_obj_t *visb_light = lv_label_create(ui.sensorDiv);
// lv_obj_set_size(sensor_label, 240, 30); /*Fix size*/
lv_obj_align(visb_light, LV_ALIGN_RIGHT_MID, -5, -20);
lv_obj_add_style(visb_light, &label_style, 0);
lv_label_set_text_fmt(visb_light, "可见光");
ui.visb_light = visb_light;
lv_obj_t *IR_light = lv_label_create(ui.sensorDiv);
// lv_obj_set_size(sensor_label, 240, 30); /*Fix size*/
lv_obj_align(IR_light, LV_ALIGN_RIGHT_MID, -5, 20);
lv_obj_add_style(IR_light, &label_style, 0);
lv_label_set_text_fmt(IR_light, "红外光");
ui.IR_light = IR_light;
ui.city = lv_label_create(ui.sensorDiv);
lv_obj_set_size(ui.city, 240, 30); /*Fix size*/
lv_obj_align(ui.city, LV_ALIGN_BOTTOM_LEFT, 0, 0);
lv_obj_set_style_border_color(ui.city, lv_color_white(), 0);
lv_obj_set_style_border_opa(ui.city, LV_OPA_0, 0);
lv_obj_set_style_text_color(ui.city, lv_palette_main(LV_PALETTE_BLUE), 0);
lv_obj_set_style_text_font(ui.city, &lv_font_chinese_24, 0);
lv_label_set_text_fmt(ui.city, "佛山");
lv_label_set_long_mode(ui.city, LV_LABEL_LONG_SCROLL_CIRCULAR);
#endif
}
void TimerView::load() {
// lv_disp_load_scr(ui.root);
lv_scr_load_anim(
ui.root, lv_scr_load_anim_t(rand() & 15) /*LV_SCR_LOAD_ANIM_FADE_IN*/,
300, 0, false);
}
void TimerView::destroy() {
if (ui.root != NULL) {
lv_obj_del(ui.root);
ui.root = NULL;
}
}
void TimerView::setSegLabel(bool hidden) {
if (hidden) {
lv_obj_add_flag(ui.segLabel, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_clear_flag(ui.segLabel, LV_OBJ_FLAG_HIDDEN);
}
}
void TimerView::updateHour(int32_t hour) {
lv_label_set_text_fmt(ui.hourLabel, "%02d", hour);
}
/**
* @brief update the min value at the view.
* @
*
* @param min
*/
void TimerView::updateMin(int32_t min) {
lv_label_set_text_fmt(ui.minLabel, "%02d", min);
}
void TimerView::updateCity(String cityName, String weather, String temp,
String humd, String wind, String windD) {
// static long last_value = 0;
// lv_label_set_text_fmt(ui.city, "%s", cityName.c_str());
lv_label_set_text_fmt(ui.city, "%s %s %s°C %s%% %s级%s风",
cityName.c_str(), weather.c_str(), temp.c_str(),
humd.c_str(), windD.c_str(), wind.c_str());
// last_value = ND_value;
}
void TimerView::updateSensor(int temp, int humi ,uint16_t visb,uint16_t IR) {
// static long last_value = 0;
// lv_label_set_text_fmt(ui.city, "%s", cityName.c_str());
lv_label_set_text_fmt(ui.temperature, "温度:%d°C",temp);
lv_label_set_text_fmt(ui.humidity, "湿度:%d%%",humi);
lv_label_set_text_fmt(ui.visb_light, "光强:%d",visb);
lv_label_set_text_fmt(ui.IR_light, "红外:%d",IR);
// last_value = ND_value;
}
void TimerView::updateTime(const char *time) {
// 确保 time 参数不为空
if (time != nullptr) {
lv_label_set_text_fmt(ui.timelabel, "%s", time);
} else {
// 处理空指针情况,例如设置一个默认值或记录错误日志
lv_label_set_text_fmt(ui.timelabel, "Invalid Time");
}
}
4、基于FreeRTOS的人脸检测主要功能是通过ESP32的摄像头模块实时捕捉图像,使用人脸检测算法检测图像中的人脸,并在LCD屏幕上显示检测结果。使用了FreeRTOS的任务和队列机制来实现多任务并行处理,确保摄像头数据的获取和人脸检测能够同时进行。摄像头任务负责捕获图像并发送到队列,AI处理任务从队列中接收图像并进行人脸检测。队列机制确保了任务间的数据传递和同步,而任务的优先级和调度机制确保了系统的实时性和高效性,使得系统能够高效地处理摄像头数据,并在LCD屏幕上实时显示检测结果。熟练掌握人脸检测技能后,为实现人脸识别铺平了道路。
1. FreeRTOS任务在FreeRTOS中,任务是独立的执行单元,每个任务都有自己的堆栈和优先级。任务可以并行运行,FreeRTOS调度器会根据任务的优先级和时间片来切换任务的执行。代码中使用 `xTaskCreatePinnedToCore` 函数创建了一个AI处理任务 `ai_process_handler`,并将其绑定到特定的CPU核心(核心1)。任务的优先级为5,堆栈大小为6KB。任务的入口函数是 `ai_process_handler`,该函数负责从摄像头获取图像数据并进行人脸检测。 xTaskCreatePinnedToCore(ai_process_handler, "ai_process_handler", 6 * 1024, NULL, 5, &ai_task_handle, 1); 2. FreeRTOS队列(Queue)队列是FreeRTOS中用于任务间通信的一种机制。任务可以通过队列发送和接收数据,队列可以存储多个数据项,并且支持阻塞和非阻塞操作。 代码中使用 `xQueueCreate` 函数创建了两个队列:`xQueueCamer`:用于传递摄像头图像数据。`xQueueAIFrameO`:用于传递AI处理后的图像数据(当前未使用)。队列的大小为5(不要跟任务的优先级别搞混了),每个队列项的大小为 `sizeof(camera_fb_t *)`。 xQueueCamer = xQueueCreate(5, sizeof(camera_fb_t *)); xQueueAIFrameO = xQueueCreate(5, sizeof(camera_fb_t *)); 3. 任务间的数据传递 摄像头模块通过 `register_camera` 函数注册,并将捕获的图像数据通过 `xQueueCamer` 队列发送给AI处理任务。 在 `ai_process_handler` 任务中,使用 `xQueueReceive` 函数从 `xQueueCamer` 队列中接收摄像头图像数据。 if (xQueueReceive(xQueueCamer, &face_ai_frameI, portMAX_DELAY)) { // 处理摄像头图像数据 } AI处理后的数据传递: 当前代码中,AI处理后的数据并未通过 `xQueueAIFrameO` 队列发送给其他任务,但可以扩展为将处理后的图像数据发送给其他任务进行进一步处理或显示。 // xQueueSend(xQueueAIFrameO, &face_ai_frameI, portMAX_DELAY); 4. 任务的并行处理虽然代码中没有显式创建摄像头任务,但 `register_camera` 函数内部创建一个任务或使用中断机制来捕获摄像头图像数据,并将数据发送到 `xQueueCamer` 队列。AI处理任务: `ai_process_handler` 任务从 `xQueueCamer` 队列中接收图像数据,进行人脸检测,并将结果显示在LCD屏幕上。 由于使用了队列机制,摄像头任务和AI处理任务可以并行运行,摄像头任务不断捕获图像并发送到队列,而AI处理任务从队列中接收图像并进行处理,实现数据的无缝连接,速度更快。 5. 任务的同步与阻塞在 `ai_process_handler` 任务中,`xQueueReceive` 函数使用了 `portMAX_DELAY` 参数,表示任务在队列为空时会阻塞,直到有数据可用。 这种阻塞机制确保了任务在没有数据时不会占用CPU资源,提高了系统的效率。 if (xQueueReceive(xQueueCamer, &face_ai_frameI, portMAX_DELAY)) { // 处理摄像头图像数据 } 6. 任务的优先级与调度 `ai_process_handler` 任务的优先级为5,这意味着它在系统中具有较高的优先级,能够及时响应摄像头数据的处理需求。 FreeRTOS调度器会根据任务的优先级和时间片来切换任务的执行。高优先级的任务会优先获得CPU资源,确保关键任务(如人脸检测)能够及时执行。
#include "Lcd_handler.hpp"
#include <Wire.h>
#include <initBoard.h>
#include <esp_camera.h>
#include "who_camera.hpp"
#include "human_face_detect_msr01.hpp"
#include "human_face_detect_mnp01.hpp"
#include "dl_image.hpp"
#define LCD_WIDTH (320)
#define LCD_HEIGHT (240)
K10_Lcd lcd;
LGFX_Sprite canvas(&lcd);
int xf = 160, yf = 120;
#define FACE_COLOR_WHITE 0x00FFFFFF
TaskHandle_t camera_task_handle;
TaskHandle_t ai_task_handle;
QueueHandle_t xQueueCamer = NULL;
QueueHandle_t xQueueAIFrameO = NULL;
/**
* @brief 摄像头图像数据获取任务
* @param arg:未使用
* @retval 无
*/
static void face_coordinate(camera_fb_t *fb, std::list<dl::detect::result_t> *results, int face_id)
{
int x, y, w, h;
int i = 0;
for (const auto &prediction : *results)
{
x = (int)prediction.box;
y = (int)prediction.box;
if (y < 0)
y = 0;
w = (int)prediction.box - x + 1;
h = (int)prediction.box - y + 1;
if ((x + w) > fb->width)
w = fb->width - x;
if ((y + h) > fb->height)
h = fb->height - y;
xf = x + w / 2;
yf = y + h / 2;
canvas.drawRect(x, y, w, h, TFT_GREEN);
}
}
static void ai_process_handler(void *arg)
{
arg = arg;
camera_fb_t *face_ai_frameI = NULL;
HumanFaceDetectMSR01 detector(0.1F, 0.5F, 10, 0.2F);
HumanFaceDetectMNP01 detector2(0.4F, 0.3F, 10);
int face_id = 0;
while (1)
{
/* 以队列的形式获取摄像头图像数据 */
if (xQueueReceive(xQueueCamer, &face_ai_frameI, portMAX_DELAY))
{
/* 判断图像是否出现人脸 */
std::list<dl::detect::result_t> &detect_candidates = detector.infer((uint16_t *)face_ai_frameI->buf, {(int)face_ai_frameI->height, (int)face_ai_frameI->width, 3});
// std::list<dl::detect::result_t> &detect_results = detector2.infer((uint16_t *)face_ai_frameI->buf, {(int)face_ai_frameI->height, (int)face_ai_frameI->width, 3}, detect_candidates);
canvas.pushImage(0, 0, face_ai_frameI->width, face_ai_frameI->height, (uint16_t *)face_ai_frameI->buf); // Draw camera frame
if (detect_candidates.size() > 0)
{
face_coordinate((camera_fb_t *)face_ai_frameI->buf, &detect_candidates, face_id);
}
lcd.startWrite();
canvas.pushSprite(0, 0);
lcd.endWrite();
esp_camera_fb_return(face_ai_frameI);
}
/* 以队列的形式发送AI处理的图像 */
// xQueueSend(xQueueAIFrameO, &face_ai_frameI, portMAX_DELAY);
}
}
// static void face_coordinate(fb_data_t *fb, std::list<dl::detect::result_t> *results, int face_id) {
uint8_t esp_face_detection_ai_strat(void)
{
/* 创建队列及任务 */
xQueueCamer = xQueueCreate(5, sizeof(camera_fb_t *));
xQueueAIFrameO = xQueueCreate(5, sizeof(camera_fb_t *));
// xTaskCreatePinnedToCore(camera_process_handler, "camera_process_handler", 4 * 1024, NULL, 5, &camera_task_handle, 1);
register_camera(PIXFORMAT_RGB565, FRAMESIZE_HVGA, 3, xQueueCamer);
xTaskCreatePinnedToCore(ai_process_handler, "ai_process_handler", 6 * 1024, NULL, 5, &ai_task_handle, 1);
if (xQueueCamer != NULL || xQueueAIFrameO != NULL || camera_task_handle != NULL || ai_task_handle != NULL)
{
return 0;
}
return 1;
}
void esp_face_detection_ai_deinit(void)
{
if (xQueueCamer != NULL)
{
vQueueDelete(xQueueCamer);
}
if (xQueueAIFrameO != NULL)
{
vQueueDelete(xQueueAIFrameO);
}
if (camera_task_handle != NULL)
{
vTaskDelete(camera_task_handle);
}
if (ai_task_handle != NULL)
{
vTaskDelete(ai_task_handle);
}
}
void setup()
{
Serial.begin(115200);
init_board();
lcd.init();
lcd.setRotation(0);
// camera_init();
canvas.setPsram(true);
canvas.createSprite(240, 320);
esp_face_detection_ai_strat();
}
void loop()
{
// camera_capture_and_face_detect();
// delay(200);
}
5、mp3播放器 该项目通过音频驱动实现音频数据的播放,通过MP3文件处理搜索和读取SD卡中的MP3文件,通过音乐播放控制实现音乐的播放、暂停和切换,通过按键控制实现用户对音乐播放的操作。播放界面是基于lvgl的music_demo上结合K10的音频资源配置实现了一个精美的音乐播放器,实际上是对I2S音频设备和LVGL应用的深入学习。 1. 音频驱动 I2S接口配置: 配置I2S接口以实现音频数据的传输,配置时钟配置、数据格式、数据传输模式等。处理音频数据的播放,需要考虑音频数据的解码、缓存、同步等问题,以确保音频播放的流畅性和音质。音频硬件初始化: 在Audio_hal.cpp中的Audio_Init()函数中,初始化音频硬件,包括I2S接口的配置。在Audio_hal.cpp中的loop_i2s()函数中,处理音频数据的播放,通过I2S接口将音频数据发送到音频输出设备。 2. MP3文件处理操作SD卡文件系统,需要学习文件系统的结构和操作,包括文件的读取、写入、目录遍历等。使用Folder_retrieval()函数搜索SD卡中的MP3文件,并将文件列表返回给调用者。3. 音乐播放实现音乐的播放、暂停、恢复和切换,需要掌握音频数据的状态管理和切换逻辑,以确保音乐播放的连贯性和稳定性。在音乐播放的同时,需要处理图形界面的更新和用户输入,需要考虑多任务处理的同步和互斥问题,以确保系统的稳定性和响应性。播放控制: 在LVGL_Music.cpp中,通过按钮的回调函数调用相应的音乐控制函数,如_lv_demo_music_play()、_lv_demo_music_pause()、_lv_demo_music_resume()等,实现音乐的播放、暂停和恢复。歌曲切换: 在LVGL_Music.cpp中,通过_lv_demo_music_album_prev()和_lv_demo_music_album_next()函数实现歌曲的上一首和下一首切换。4. 按键控制在LVGL_Music.cpp中,创建播放、暂停、上一首、下一首等按钮,并设置按钮的回调函数。在AB按钮的回调函数中,根据用户的按键操作调用相应的音乐控制函数,实现音乐播放的控制。播放器部分核心代码:#include "LVGL_Music.h"
/*********************
* DEFINES
*********************/
#define INTRO_TIME 2000
#define BAR_COLOR1 lv_color_hex(0xe9dbfc)
#define BAR_COLOR2 lv_color_hex(0x6f8af6)
#define BAR_COLOR3 lv_color_hex(0xffffff)
#if LV_DEMO_MUSIC_LARGE
#define BAR_COLOR1_STOP 160
#define BAR_COLOR2_STOP 200
#else
#define BAR_COLOR1_STOP 80
#define BAR_COLOR2_STOP 100
#endif
#define BAR_COLOR3_STOP (2 * LV_HOR_RES / 3)
#define BAR_CNT 20
#define DEG_STEP (180/BAR_CNT)
#define BAND_CNT 4
#define BAR_PER_BAND_CNT (BAR_CNT / BAND_CNT)
/**********************
*STATIC VARIABLES
**********************/
lv_style_t music_style;
lv_style_t parts_style;
lv_obj_t * panel1;
lv_obj_t * panel2;
static lv_obj_t * main_cont;
static lv_obj_t * spectrum_obj;
static lv_obj_t * title_label;
static lv_obj_t * time_obj;
static lv_obj_t * album_img_obj;
static lv_obj_t * slider_obj;
static uint32_t spectrum_i = 0;
static uint32_t spectrum_i_pause = 0;
static uint32_t bar_ofs = 0;
static uint32_t bar_rot = 0;
static uint32_t time_act;
static lv_timer_t* sec_counter_timer;
static lv_timer_t* spectrum_timer;
static const lv_font_t * font_small;
static const lv_font_t * font_large;
static bool Playing_Flag;
static uint32_t track_id;
static bool start_anim;
static lv_coord_t start_anim_values;
static lv_obj_t * play_obj;
static const uint16_t (* spectrum);
static uint32_t spectrum_len;
static const uint16_t rnd_array = {994, 285, 553, 11, 792, 707, 966, 641, 852, 827, 44, 352, 146, 581, 490, 80, 729, 58, 695, 940, 724, 561, 124, 653, 27, 292, 557, 506, 382, 199};
char File_Name ;
char Song_Name ;
char Audio_Name ;
uint16_t ACTIVE_TRACK_CNT;
uint32_t Audio_duration;
uint32_t Audio_duration_A;
uint32_t Audio_Elapsed;
uint16_t Audio_energy;
static lv_obj_t * list;
static lv_style_t style_btn_round;
static lv_style_t style_btn_pr;
static lv_style_t style_btn_play;
static lv_style_t style_btn_stop;
static lv_style_t style_title;
static bool first_Flag = false;
LV_IMG_DECLARE(img_lv_demo_music_btn_list_play);
LV_IMG_DECLARE(img_lv_demo_music_btn_list_pause);
/*
* Callback adapter function to convert parameter types to avoid compile-time
* warning.
*/
void _img_set_zoom_anim_cb(void * obj, int32_t zoom)
{
lv_img_set_zoom((lv_obj_t *)obj, (uint16_t)zoom);
}
/*
* Callback adapter function to convert parameter types to avoid compile-time
* warning.
*/
void _obj_set_x_anim_cb(void * obj, int32_t x)
{
lv_obj_set_x((lv_obj_t *)obj, (lv_coord_t)x);
}
lv_obj_t * _lv_demo_music_main_create(lv_obj_t * parent)
{
LVGL_Search_Music();
if(ACTIVE_TRACK_CNT) {
lv_style_init(&music_style);
lv_style_set_text_font(&music_style, font_large);
font_small = &lv_font_montserrat_12;
font_large = &lv_font_montserrat_16;
// 1
panel1 = lv_obj_create(parent);
lv_obj_set_height(panel1, LV_SIZE_CONTENT);
lv_obj_t * cont = create_cont(panel1);
create_wave_images(cont);
spectrum_obj = create_spectrum_obj(panel1);
lv_obj_add_style(spectrum_obj, &music_style, 0);
lv_obj_t * title_box = create_title_box(panel1);
lv_obj_add_style(title_box, &music_style, 0);
lv_obj_t * ctrl_box = create_ctrl_box(panel1);
lv_obj_add_style(ctrl_box, &music_style, 0);
static lv_coord_t grid_main_col_dsc[] = {LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
static lv_coord_t grid_main_row_dsc[] = {LV_GRID_CONTENT, LV_GRID_CONTENT, LV_GRID_CONTENT, LV_GRID_TEMPLATE_LAST};
lv_obj_set_grid_dsc_array(parent, grid_main_col_dsc, grid_main_row_dsc);
/*Create the top panel*/
static lv_coord_t grid_1_col_dsc[] = {LV_GRID_CONTENT, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
static lv_coord_t grid_1_row_dsc[] = {
LV_GRID_CONTENT, /*title_box*/
LV_GRID_CONTENT, /*cont*/
170, /*spectrum_obj*/
LV_GRID_CONTENT, /*ctrl_box*/
LV_GRID_CONTENT, /*handle_box*/
LV_GRID_CONTENT, /*Button2*/
LV_GRID_TEMPLATE_LAST
};
lv_obj_set_grid_cell(panel1, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_START, 0, 1);
lv_obj_set_grid_dsc_array(panel1, grid_1_col_dsc, grid_1_row_dsc);
lv_obj_set_grid_cell(title_box , LV_GRID_ALIGN_STRETCH, 0, 2, LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_set_grid_cell(cont , LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_CENTER, 1, 1);
lv_obj_set_grid_cell(spectrum_obj , LV_GRID_ALIGN_STRETCH, 0, 2, LV_GRID_ALIGN_CENTER, 2, 1);
lv_obj_set_grid_cell(ctrl_box , LV_GRID_ALIGN_STRETCH, 0, 2, LV_GRID_ALIGN_CENTER, 3, 1);
// 2
panel2 = lv_obj_create(parent);
lv_obj_set_height(panel2, LV_SIZE_CONTENT);
lv_obj_t * list_box = create_List_box(panel2);
lv_obj_set_size(list_box, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
// lv_obj_add_style(list_box, &music_style, 0);
static lv_coord_t grid_2_col_dsc[] = {LV_GRID_FR(1),LV_GRID_FR(1),LV_GRID_TEMPLATE_LAST};
static lv_coord_t grid_2_row_dsc[] = {
LV_GRID_CONTENT, /*list_box*/
LV_GRID_TEMPLATE_LAST
};
lv_obj_set_grid_cell(panel2, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_START, 1, 1);
lv_obj_set_grid_dsc_array(panel2, grid_2_col_dsc, grid_2_row_dsc);
lv_obj_set_grid_cell(list_box , LV_GRID_ALIGN_STRETCH, 0, 2, LV_GRID_ALIGN_CENTER, 0, 1);
sec_counter_timer = lv_timer_create(timer_cb, 1000, NULL);
lv_timer_pause(sec_counter_timer);
spectrum_timer = lv_timer_create(spectrum_timer_cb, 100, NULL);
lv_timer_pause(spectrum_timer);
lv_obj_fade_in(title_box, 500, INTRO_TIME - 1000);
lv_obj_fade_in(ctrl_box, 500, INTRO_TIME - 1000);
lv_obj_fade_in(album_img_obj, 300, INTRO_TIME - 1000);
lv_obj_fade_in(spectrum_obj, 0, INTRO_TIME - 1000);
}
else{
lv_obj_t *label = lv_label_create(parent);
lv_label_set_text(label, "No MP3 file found in SD card!");
// lv_obj_set_size(label, LV_PCT(100), LV_PCT(100));
lv_obj_set_size(label, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
}
return main_cont;
}
/************************************************************************************************************************************
* create_title_box
************************************************************************************************************************************/
lv_obj_t * create_title_box(lv_obj_t * parent)
{
/*Create the titles*/
lv_obj_t * cont = lv_obj_create(parent);
lv_obj_remove_style_all(cont);
lv_obj_set_height(cont, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
title_label = lv_label_create(cont);
lv_obj_set_style_text_font(title_label, font_large, 0);
lv_obj_set_style_text_color(title_label, lv_color_hex(0x504d6d), 0);
lv_label_set_text(title_label, Audio_Name);
lv_obj_set_height(title_label, lv_font_get_line_height(font_large) );
return cont;
}
/************************************************************************************************************************************
*create_title_box END *create_title_box END *create_title_box END *create_title_box END
************************************************************************************************************************************/
/************************************************************************************************************************************
*create_cont *create_cont *create_cont *create_cont
************************************************************************************************************************************/
lv_obj_t * create_cont(lv_obj_t * parent)
{
/**/
/*A transparent container in which the player section will be scrolled*/
main_cont = lv_obj_create(parent);
lv_obj_clear_flag(main_cont, LV_OBJ_FLAG_CLICKABLE);
lv_obj_clear_flag(main_cont, LV_OBJ_FLAG_SCROLL_ELASTIC);
lv_obj_remove_style_all(main_cont); /*Make it transparent*/
lv_obj_set_size(main_cont, lv_pct(100), lv_pct(100));
lv_obj_set_scroll_snap_y(main_cont, LV_SCROLL_SNAP_CENTER); /*Snap the children to the center*/
/*Create a container for the player*/
lv_obj_t * player = lv_obj_create(main_cont);
lv_obj_set_y(player, - LV_DEMO_MUSIC_HANDLE_SIZE);
lv_obj_set_size(player, LV_HOR_RES, 2 * LV_VER_RES + LV_DEMO_MUSIC_HANDLE_SIZE * 2);
lv_obj_set_style_bg_color(player, lv_color_hex(0xffffff), 0);
lv_obj_set_style_border_width(player, 0, 0);
lv_obj_set_style_pad_all(player, 0, 0);
lv_obj_set_scroll_dir(player, LV_DIR_VER);
/* Transparent placeholders below the player container
* It is used only to snap it to center.*/
lv_obj_t * placeholder1 = lv_obj_create(main_cont);
lv_obj_remove_style_all(placeholder1);
lv_obj_clear_flag(placeholder1, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t * placeholder2 = lv_obj_create(main_cont);
lv_obj_remove_style_all(placeholder2);
lv_obj_clear_flag(placeholder2, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t * placeholder3 = lv_obj_create(main_cont);
lv_obj_remove_style_all(placeholder3);
lv_obj_clear_flag(placeholder3, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_size(placeholder1, lv_pct(100), LV_VER_RES);
lv_obj_set_y(placeholder1, 0);
lv_obj_set_size(placeholder2, lv_pct(100), LV_VER_RES);
lv_obj_set_y(placeholder2, LV_VER_RES);
lv_obj_set_size(placeholder3, lv_pct(100),LV_VER_RES - 2 * LV_DEMO_MUSIC_HANDLE_SIZE);
lv_obj_set_y(placeholder3, 2 * LV_VER_RES + LV_DEMO_MUSIC_HANDLE_SIZE);
lv_obj_update_layout(main_cont);
return player;
}
void create_wave_images(lv_obj_t * parent)
{
LV_IMG_DECLARE(img_lv_demo_music_wave_top);
LV_IMG_DECLARE(img_lv_demo_music_wave_bottom);
lv_obj_t * wave_top = lv_img_create(parent);
lv_img_set_src(wave_top, &img_lv_demo_music_wave_top);
lv_obj_set_width(wave_top, LV_HOR_RES);
lv_obj_align(wave_top, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_add_flag(wave_top, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_t * wave_bottom = lv_img_create(parent);
lv_img_set_src(wave_bottom, &img_lv_demo_music_wave_bottom);
lv_obj_set_width(wave_bottom, LV_HOR_RES);
lv_obj_align(wave_bottom, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_add_flag(wave_bottom, LV_OBJ_FLAG_IGNORE_LAYOUT);
LV_IMG_DECLARE(img_lv_demo_music_corner_left);
LV_IMG_DECLARE(img_lv_demo_music_corner_right);
lv_obj_t * wave_corner = lv_img_create(parent);
lv_img_set_src(wave_corner, &img_lv_demo_music_corner_left);
lv_obj_align(wave_corner, LV_ALIGN_BOTTOM_LEFT, -LV_HOR_RES / 6, 0);
lv_obj_add_flag(wave_corner, LV_OBJ_FLAG_IGNORE_LAYOUT);
wave_corner = lv_img_create(parent);
lv_img_set_src(wave_corner, &img_lv_demo_music_corner_right);
lv_obj_align(wave_corner, LV_ALIGN_BOTTOM_RIGHT, LV_HOR_RES / 6, 0);
lv_obj_add_flag(wave_corner, LV_OBJ_FLAG_IGNORE_LAYOUT);
}
/************************************************************************************************************************************
* create_cont END * create_cont END * create_cont END * create_cont END
************************************************************************************************************************************/
/************************************************************************************************************************************
*spectrum *spectrum *spectrum *spectrum
************************************************************************************************************************************/
lv_obj_t * create_spectrum_obj(lv_obj_t * parent)
{
/*Create the spectrum visualizer*/
lv_obj_t * obj = lv_obj_create(parent);
lv_obj_remove_style_all(obj);
lv_obj_set_height(obj, 250);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(obj, spectrum_draw_event_cb, LV_EVENT_ALL, NULL);
lv_obj_refresh_ext_draw_size(obj);
album_img_obj = album_img_create(obj);
return obj;
}
int32_t get_cos(int32_t deg, int32_t a)
{
int32_t r = (lv_trigo_cos(deg) * a);
r += LV_TRIGO_SIN_MAX / 2;
return r >> LV_TRIGO_SHIFT;
}
int32_t get_sin(int32_t deg, int32_t a)
{
int32_t r = lv_trigo_sin(deg) * a;
r += LV_TRIGO_SIN_MAX / 2;
return r >> LV_TRIGO_SHIFT;
}
void spectrum_draw_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_REFR_EXT_DRAW_SIZE)
lv_event_set_ext_draw_size(e, LV_VER_RES);
else if(code == LV_EVENT_COVER_CHECK)
lv_event_set_cover_res(e, LV_COVER_RES_NOT_COVER);
else if(code == LV_EVENT_DRAW_POST) {
lv_obj_t * obj = lv_event_get_target(e);
lv_draw_ctx_t * draw_ctx = lv_event_get_draw_ctx(e);
lv_opa_t opa = lv_obj_get_style_opa_recursive(obj, LV_PART_MAIN);
if(opa < LV_OPA_MIN) return;
lv_point_t poly;
lv_point_t center;
center.x = obj->coords.x1 + lv_obj_get_width(obj) / 2;
center.y = obj->coords.y1 + lv_obj_get_height(obj) / 2;
lv_draw_rect_dsc_t draw_dsc;
lv_draw_rect_dsc_init(&draw_dsc);
draw_dsc.bg_opa = LV_OPA_COVER;
uint16_t r;
uint32_t i;
lv_coord_t min_a = 5;
lv_coord_t r_in = 77;
r_in = (r_in * lv_img_get_zoom(album_img_obj)) >> 8;
for(i = 0; i < BAR_CNT; i++) r = r_in + min_a;
uint32_t s;
for(s = 0; s < 4; s++) {
uint32_t f;
uint32_t band_w = 0; /*Real number of bars in this band.*/
switch(s) {
case 0:
band_w = 20;
break;
case 1:
band_w = 8;
break;
case 2:
band_w = 4;
break;
case 3:
band_w = 2;
break;
}
uint32_t Audio_spectrum = Audio_energy/1300;
uint32_t random_number =(uint32_t) (rand() % (Audio_spectrum + 1));
/* Add "side bars" with cosine characteristic.*/
for(f = 0; f < band_w; f++) {
uint32_t ampl_main = random_number ;
int32_t ampl_mod = get_cos(f * 360 / band_w + 180, 180) + 180;
int32_t t = BAR_PER_BAND_CNT * s - band_w / 2 + f;
if(t < 0) t = BAR_CNT + t;
if(t >= BAR_CNT) t = t - BAR_CNT;
r += (ampl_main * ampl_mod) >> 9;
}
}
uint32_t amax = 20;
int32_t animv = Audio_energy/2000;;
if(animv > amax) animv = amax;
for(i = 0; i < BAR_CNT; i++) {
uint32_t deg_space = 1;
uint32_t deg = i * DEG_STEP + 90;
uint32_t j = (i + bar_rot + rnd_array) % BAR_CNT;
uint32_t k = (i + bar_rot + rnd_array[(bar_ofs + 1) % 10]) % BAR_CNT;
uint32_t v = (r * animv + r * (amax - animv)) / amax;
if(v < BAR_COLOR1_STOP) draw_dsc.bg_color = BAR_COLOR1;
else if(v > BAR_COLOR3_STOP) draw_dsc.bg_color = BAR_COLOR3;
else if(v > BAR_COLOR2_STOP) draw_dsc.bg_color = lv_color_mix(BAR_COLOR3, BAR_COLOR2,
((v - BAR_COLOR2_STOP) * 255) / (BAR_COLOR3_STOP - BAR_COLOR2_STOP));
else draw_dsc.bg_color = lv_color_mix(BAR_COLOR2, BAR_COLOR1,
((v - BAR_COLOR1_STOP) * 255) / (BAR_COLOR2_STOP - BAR_COLOR1_STOP));
uint32_t di = deg + deg_space;
int32_t x1_out = get_cos(di, v);
poly.x = center.x + x1_out;
poly.y = center.y + get_sin(di, v);
int32_t x1_in = get_cos(di, r_in);
poly.x = center.x + x1_in;
poly.y = center.y + get_sin(di, r_in);
di += DEG_STEP - deg_space * 2;
int32_t x2_in = get_cos(di, r_in);
poly.x = center.x + x2_in;
poly.y = center.y + get_sin(di, r_in);
int32_t x2_out = get_cos(di, v);
poly.x = center.x + x2_out;
poly.y = center.y + get_sin(di, v);
lv_draw_polygon(draw_ctx, &draw_dsc, poly, 4);
poly.x = center.x - x1_out;
poly.x = center.x - x1_in;
poly.x = center.x - x2_in;
poly.x = center.x - x2_out;
lv_draw_polygon(draw_ctx, &draw_dsc, poly, 4);
}
}
}
lv_obj_t * album_img_create(lv_obj_t * parent)
{
LV_IMG_DECLARE(img_lv_demo_music_cover_1);
LV_IMG_DECLARE(img_lv_demo_music_cover_2);
LV_IMG_DECLARE(img_lv_demo_music_cover_3);
lv_obj_t * img;
img = lv_img_create(parent);
switch(track_id % 3) {
case 2:
lv_img_set_src(img, &img_lv_demo_music_cover_3);
break;
case 1:
lv_img_set_src(img, &img_lv_demo_music_cover_2);
break;
case 0:
lv_img_set_src(img, &img_lv_demo_music_cover_1);
break;
}
spectrum = spectrum_3;
spectrum_len = sizeof(spectrum_3) / sizeof(spectrum_3);
lv_img_set_antialias(img, false);
lv_obj_align(img, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(img, album_gesture_event_cb, LV_EVENT_GESTURE, NULL);
lv_obj_clear_flag(img, LV_OBJ_FLAG_GESTURE_BUBBLE);
lv_obj_add_flag(img, LV_OBJ_FLAG_CLICKABLE);
return img;
}
void spectrum_anim_cb(void * a, int32_t v)
{
lv_obj_t * obj = (lv_obj_t *)a;
lv_obj_invalidate(obj);
static uint16_t Audio_energy_old=0;
LVGL_Music_Energy();
if(Audio_energy_old > Audio_energy + 10000 || Audio_energy > Audio_energy_old + 10000)
lv_img_set_zoom(album_img_obj, LV_IMG_ZOOM_NONE + (Audio_energy/2000));
Audio_energy_old = Audio_energy;
}
void spectrum_end_cb(lv_anim_t * a)
{
LV_UNUSED(a);
_lv_demo_music_album_next(true);
}
void album_gesture_event_cb(lv_event_t * e)
{
lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_get_act());
if(dir == LV_DIR_LEFT) _lv_demo_music_album_next(true);
if(dir == LV_DIR_RIGHT) _lv_demo_music_album_next(false);
}
void spectrum_timer_cb(lv_timer_t * t){
LV_UNUSED(t);
lv_obj_invalidate(panel1);
}
/************************************************************************************************************************************
*spectrumEND *spectrum END *spectrum END *spectrumEND
************************************************************************************************************************************/
/************************************************************************************************************************************
* create_ctrl_box * create_ctrl_box * create_ctrl_box * create_ctrl_box
************************************************************************************************************************************/
lv_obj_t * create_ctrl_box(lv_obj_t * parent)
{
lv_obj_t * cont = lv_obj_create(parent);
lv_obj_remove_style_all(cont);
lv_obj_set_height(cont, LV_SIZE_CONTENT);
lv_obj_set_style_pad_bottom(cont, 8, 0);
static const lv_coord_t grid_col[] = { LV_GRID_FR(10), LV_GRID_FR(40), LV_GRID_FR(40), LV_GRID_FR(50), LV_GRID_FR(40), LV_GRID_FR(40), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
static const lv_coord_t grid_row[] = {LV_GRID_CONTENT, LV_GRID_CONTENT, LV_GRID_TEMPLATE_LAST};
lv_obj_set_grid_dsc_array(cont, grid_col, grid_row);
LV_IMG_DECLARE(img_lv_demo_music_btn_loop);
LV_IMG_DECLARE(img_lv_demo_music_btn_rnd);
LV_IMG_DECLARE(img_lv_demo_music_btn_next);
LV_IMG_DECLARE(img_lv_demo_music_btn_prev);
LV_IMG_DECLARE(img_lv_demo_music_btn_play);
LV_IMG_DECLARE(img_lv_demo_music_btn_pause);
lv_obj_t * icon1;
lv_obj_t * icon2;
lv_obj_t * icon3;
lv_obj_t * icon4;
icon1 = lv_img_create(cont);
lv_img_set_src(icon1, &img_lv_demo_music_btn_rnd);
lv_obj_set_grid_cell(icon1, LV_GRID_ALIGN_CENTER, 1, 1, LV_GRID_ALIGN_CENTER, 0, 1);
icon2 = lv_obj_create(cont);
lv_obj_set_size(icon2, 20, 20);
lv_obj_set_grid_cell(icon2, LV_GRID_ALIGN_CENTER, 5, 1, LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_set_style_bg_opa(icon2, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(icon2, 0, 0);
lv_obj_set_style_radius(icon2, LV_RADIUS_CIRCLE, 0);
lv_obj_clear_flag(icon2, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t * icon2_volume = lv_label_create(icon2);
lv_label_set_text(icon2_volume, LV_SYMBOL_VOLUME_MAX);
lv_obj_set_style_text_font(icon2_volume, &lv_font_montserrat_14, 0);
lv_obj_align(icon2_volume, LV_ALIGN_CENTER, 0, 0);
lv_obj_t *circle_button = lv_obj_create(icon2);
lv_obj_set_style_bg_opa(circle_button, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(circle_button, 0, 0);
lv_obj_set_style_radius(circle_button, LV_RADIUS_CIRCLE, 0);
lv_obj_align(circle_button, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(circle_button, volume_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_flag(circle_button, LV_OBJ_FLAG_CLICKABLE);
icon3 = lv_img_create(cont);
lv_img_set_src(icon3, &img_lv_demo_music_btn_prev);
lv_obj_set_grid_cell(icon3, LV_GRID_ALIGN_CENTER, 2, 1, LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_add_event_cb(icon3, prev_click_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_flag(icon3, LV_OBJ_FLAG_CLICKABLE);
play_obj = lv_imgbtn_create(cont);
lv_imgbtn_set_src(play_obj, LV_IMGBTN_STATE_RELEASED, NULL, &img_lv_demo_music_btn_play, NULL);
lv_imgbtn_set_src(play_obj, LV_IMGBTN_STATE_CHECKED_RELEASED, NULL, &img_lv_demo_music_btn_pause, NULL);
lv_obj_add_flag(play_obj, LV_OBJ_FLAG_CHECKABLE);
lv_obj_set_grid_cell(play_obj, LV_GRID_ALIGN_CENTER, 3, 1, LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_add_event_cb(play_obj, play_event_click_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_flag(play_obj, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_width(play_obj, img_lv_demo_music_btn_play.header.w);
icon4 = lv_img_create(cont);
lv_img_set_src(icon4, &img_lv_demo_music_btn_next);
lv_obj_set_grid_cell(icon4, LV_GRID_ALIGN_CENTER, 4, 1, LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_add_event_cb(icon4, next_click_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_flag(icon4, LV_OBJ_FLAG_CLICKABLE);
LV_IMG_DECLARE(img_lv_demo_music_slider_knob);
slider_obj = lv_slider_create(cont);
lv_obj_set_style_anim_time(slider_obj, 100, 0);
lv_obj_add_flag(slider_obj, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_width(slider_obj, 200);
lv_obj_set_height(slider_obj, 3);
lv_obj_set_grid_cell(slider_obj, LV_GRID_ALIGN_STRETCH, 1, 4, LV_GRID_ALIGN_CENTER, 1, 1);
lv_obj_set_style_bg_img_src(slider_obj, &img_lv_demo_music_slider_knob, LV_PART_KNOB);
lv_obj_set_style_bg_opa(slider_obj, LV_OPA_TRANSP, LV_PART_KNOB);
lv_obj_set_style_pad_all(slider_obj, 20, LV_PART_KNOB);
lv_obj_set_style_bg_grad_dir(slider_obj, LV_GRAD_DIR_HOR, LV_PART_INDICATOR);
lv_obj_set_style_bg_color(slider_obj, lv_color_hex(0x569af8), LV_PART_INDICATOR);
lv_obj_set_style_bg_grad_color(slider_obj, lv_color_hex(0xa666f1), LV_PART_INDICATOR);
lv_obj_set_style_outline_width(slider_obj, 0, 0);
time_obj = lv_label_create(cont);
lv_obj_set_style_text_font(time_obj, font_small, 0);
lv_obj_set_style_text_color(time_obj, lv_color_hex(0x8a86b8), 0);
lv_label_set_text(time_obj, "0:00");
lv_obj_set_grid_cell(time_obj, LV_GRID_ALIGN_END, 6, 1, LV_GRID_ALIGN_CENTER, 1, 1);
return cont;
}
void track_load(uint32_t id)
{
if(first_Flag) {
if(id == track_id) return;
}
time_act = 0;
lv_slider_set_value(slider_obj, 0, LV_ANIM_OFF);
lv_label_set_text(time_obj, "0:00");
bool next = false;
if((track_id + 1) % ACTIVE_TRACK_CNT == id) next = true;
if(first_Flag || id != track_id) {
_lv_demo_music_list_btn_check(track_id, false);
track_id = id;
}
_lv_demo_music_list_btn_check(id, true);
first_Flag = true;
lv_label_set_text(title_label, Audio_Name);
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, album_img_obj);
lv_anim_set_values(&a, lv_obj_get_style_img_opa(album_img_obj, 0), LV_OPA_TRANSP);
lv_anim_set_exec_cb(&a, album_fade_anim_cb);
lv_anim_set_time(&a, 500);
lv_anim_start(&a);
lv_anim_init(&a);
lv_anim_set_var(&a, album_img_obj);
lv_anim_set_time(&a, 500);
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
if(next) {
lv_anim_set_values(&a, 0, - LV_HOR_RES / 2);
}
else {
lv_anim_set_values(&a, 0, LV_HOR_RES / 2);
}
lv_anim_set_exec_cb(&a, _obj_set_x_anim_cb);
lv_anim_set_ready_cb(&a, lv_obj_del_anim_ready_cb);
lv_anim_start(&a);
lv_anim_set_path_cb(&a, lv_anim_path_linear);
lv_anim_set_var(&a, album_img_obj);
lv_anim_set_time(&a, 500);
lv_anim_set_values(&a, LV_IMG_ZOOM_NONE, LV_IMG_ZOOM_NONE / 2);
lv_anim_set_exec_cb(&a, _img_set_zoom_anim_cb);
lv_anim_set_ready_cb(&a, NULL);
lv_anim_start(&a);
album_img_obj = album_img_create(spectrum_obj);
lv_anim_set_path_cb(&a, lv_anim_path_overshoot);
lv_anim_set_var(&a, album_img_obj);
lv_anim_set_time(&a, 500);
lv_anim_set_delay(&a, 100);
lv_anim_set_values(&a, LV_IMG_ZOOM_NONE / 4, LV_IMG_ZOOM_NONE);
lv_anim_set_exec_cb(&a, _img_set_zoom_anim_cb);
lv_anim_set_ready_cb(&a, NULL);
lv_anim_start(&a);
lv_anim_init(&a);
lv_anim_set_var(&a, album_img_obj);
lv_anim_set_values(&a, 0, LV_OPA_COVER);
lv_anim_set_exec_cb(&a, album_fade_anim_cb);
lv_anim_set_time(&a, 500);
lv_anim_set_delay(&a, 100);
lv_anim_start(&a);
}
void play_event_click_cb(lv_event_t * e)
{
lv_obj_t * obj = lv_event_get_target(e);
if(lv_obj_has_state(obj, LV_STATE_CHECKED)) {
_lv_demo_music_resume();
}
else {
_lv_demo_music_pause();
}
}
void prev_click_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) {
_lv_demo_music_album_next(false);
}
}
void next_click_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) {
_lv_demo_music_album_next(true);
}
}
static lv_obj_t * panel;
static lv_obj_t * slider;
static lv_obj_t * slider_volume;
void volume_adjustment_event_cb(lv_event_t * e) {
uint8_t Volume = lv_slider_get_value(lv_event_get_target(e));
if (Volume >= 0 && Volume <= Volume_MAX){
lv_slider_set_value(slider, Volume, LV_ANIM_ON);
LVGL_volume_adjustment(Volume);
// printf("Volume:%d\r\n",Volume) ;
}
else
printf("Volume out of range: %d\n", Volume);
}
void background_event_cb(lv_event_t * e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
if (lv_event_get_target(e) == panel && lv_event_get_target(e) != slider_volume) {
lv_obj_add_flag(panel, LV_OBJ_FLAG_HIDDEN);
}
}
}
void volume_event_cb(lv_event_t * e) {
printf("Clicked on volume icon\r\n");
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_CLICKED) {
if (!slider) {
panel = lv_obj_create(lv_scr_act());
lv_obj_set_size(panel, lv_obj_get_width(lv_scr_act()), lv_obj_get_height(lv_scr_act()));
lv_obj_set_pos(panel, 0, 0);
lv_obj_set_style_border_width(panel, 0, 0);
lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_opa(panel, LV_OPA_TRANSP, LV_PART_MAIN);
slider = lv_slider_create(panel);
lv_obj_add_flag(slider, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_size(slider, 20, 170);
lv_obj_set_style_bg_opa(slider, LV_OPA_TRANSP, LV_PART_KNOB);
lv_obj_set_style_pad_all(slider, 20, LV_PART_KNOB);
lv_obj_set_style_bg_color(slider, lv_color_hex(0xADD8F6), LV_PART_INDICATOR);
lv_obj_set_style_outline_width(slider, 0, 0);
lv_slider_set_range(slider, 0, Volume_MAX);
lv_slider_set_value(slider, Volume, LV_ANIM_ON);
lv_obj_align_to(slider, panel, LV_ALIGN_RIGHT_MID, 0, 0);
slider_volume = lv_slider_create(panel);
lv_obj_add_flag(slider_volume, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_size(slider_volume, 50, 170);
lv_obj_set_style_bg_opa(slider_volume, LV_OPA_TRANSP, LV_PART_KNOB);
lv_obj_set_style_pad_all(slider_volume, 50, LV_PART_KNOB);
lv_obj_set_style_bg_color(slider_volume, lv_color_hex(0xFFFFFF), LV_PART_INDICATOR);
lv_obj_set_style_outline_width(slider_volume, 0, 0);
lv_slider_set_range(slider_volume, 0, Volume_MAX);
lv_slider_set_value(slider_volume, Volume, LV_ANIM_ON);
lv_obj_align_to(slider_volume, panel, LV_ALIGN_RIGHT_MID, 20, 0);
lv_obj_set_style_bg_opa(slider_volume, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_bg_opa(slider_volume, LV_OPA_TRANSP, LV_PART_KNOB);
lv_obj_set_style_bg_opa(slider_volume, LV_OPA_TRANSP, LV_PART_INDICATOR);
lv_obj_add_event_cb(slider_volume, volume_adjustment_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_add_event_cb(panel, background_event_cb, LV_EVENT_ALL, NULL);
}
lv_obj_clear_flag(panel, LV_OBJ_FLAG_HIDDEN);
}
}
void hide_slider(lv_event_t * e) {
lv_obj_t * obj = lv_event_get_target(e);
if (obj != slider) {
lv_obj_add_flag(slider, LV_OBJ_FLAG_HIDDEN);
}
}
void timer_cb(lv_timer_t * t)
{
LV_UNUSED(t);
if(Audio_duration == 0){
Audio_duration = Music_Duration();
if(Audio_duration != 0)
{
_lv_demo_music_resume();
}
}
else{
time_act++;
lv_label_set_text_fmt(time_obj, "%"LV_PRIu32":%02"LV_PRIu32, time_act / 60, time_act % 60);
lv_slider_set_value(slider_obj, time_act, LV_ANIM_ON);
if(time_act >= Audio_duration){
_lv_demo_music_album_next(true);
}
}
}
void album_fade_anim_cb(void * var, int32_t v)
{
lv_obj_set_style_img_opa((_lv_obj_t*)var, v, 0);
}
/************************************************************************************************************************************
*create_ctrl_boxEND *create_ctrl_boxEND *create_ctrl_boxEND *create_ctrl_boxEND
************************************************************************************************************************************/
/************************************************************************************************************************************
* create_ctrl_box * create_ctrl_box * create_ctrl_box * create_ctrl_box
************************************************************************************************************************************/
lv_obj_t * create_List_box(lv_obj_t * parent)
{
static const lv_coord_t grid_cols[] = {LV_GRID_CONTENT, LV_GRID_FR(1), LV_GRID_CONTENT, LV_GRID_TEMPLATE_LAST};
static const lv_coord_t grid_rows[] = {LV_GRID_CONTENT,LV_GRID_CONTENT,LV_GRID_CONTENT,LV_GRID_CONTENT, LV_GRID_TEMPLATE_LAST};
lv_style_init(&style_btn_stop);
lv_style_set_bg_opa(&style_btn_stop, LV_OPA_TRANSP);
lv_style_set_grid_column_dsc_array(&style_btn_stop, grid_cols);
lv_style_set_grid_row_dsc_array(&style_btn_stop, grid_rows);
lv_style_set_grid_row_align(&style_btn_stop, LV_GRID_ALIGN_CENTER);
lv_style_set_layout(&style_btn_stop, LV_LAYOUT_GRID);
lv_style_set_pad_right(&style_btn_stop, 20);
lv_style_init(&style_btn_round);
lv_style_set_radius(&style_btn_round, 10);
lv_style_init(&style_btn_pr);
lv_style_set_bg_opa(&style_btn_pr, LV_OPA_COVER);
lv_style_set_bg_color(&style_btn_pr,lv_color_hex(0xCDE8F3));
lv_style_init(&style_btn_play);
lv_style_set_bg_opa(&style_btn_play, LV_OPA_COVER);
lv_style_set_bg_color(&style_btn_play, lv_color_hex(0xAAD3E0));
lv_style_init(&style_title);
lv_style_set_text_font(&style_title, font_small);
lv_style_set_text_color(&style_title, lv_color_hex(0x101010));
list = lv_obj_create(parent);
lv_obj_remove_style_all(list);
lv_obj_set_size(list, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_pos(list, 0, LV_DEMO_MUSIC_HANDLE_SIZE);
// lv_obj_set_y(list, LV_DEMO_MUSIC_HANDLE_SIZE);
lv_obj_add_style(list, &music_style, LV_PART_SCROLLBAR);
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
uint32_t List_id;
for(List_id = 0; List_id < ACTIVE_TRACK_CNT; List_id++) {
add_list_btn(list,List_id);
}
lv_obj_set_scroll_snap_y(list, LV_SCROLL_SNAP_CENTER);
_lv_demo_music_list_btn_check(0, true);
return list;
}
lv_obj_t * add_list_btn(lv_obj_t * parent, uint32_t List_id)
{
lv_obj_t * btn = lv_obj_create(parent);
lv_obj_remove_style_all(btn);
lv_obj_set_size(btn, lv_pct(100), 60);
lv_obj_add_style(btn, &style_btn_round, 0);
lv_obj_add_style(btn, &style_btn_stop, 0);
lv_obj_add_style(btn, &style_btn_play, LV_STATE_CHECKED);
lv_obj_add_style(btn, &style_btn_pr, LV_STATE_PRESSED);
lv_obj_add_event_cb(btn, btn_click_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t * icon = lv_img_create(btn);
lv_img_set_src(icon, &img_lv_demo_music_btn_list_play);
lv_obj_set_grid_cell(icon, LV_GRID_ALIGN_START, 0, 1, LV_GRID_ALIGN_CENTER, 0, 2);
lv_obj_t * title_label = lv_label_create(btn);
lv_label_set_text(title_label, Song_Name);
lv_obj_set_grid_cell(title_label, LV_GRID_ALIGN_START, 1, 1, LV_GRID_ALIGN_CENTER, 0, 1);
lv_obj_add_style(title_label, &style_title, 0);
// LV_IMG_DECLARE(img_lv_demo_music_list_border);
// lv_obj_t * border = lv_img_create(btn);
// lv_img_set_src(border, &img_lv_demo_music_list_border);
// lv_obj_set_width(border, lv_pct(120));
// lv_obj_align(border, LV_ALIGN_BOTTOM_MID, 0, 0);
// lv_obj_add_flag(border, LV_OBJ_FLAG_IGNORE_LAYOUT);
return btn;
}
void _lv_demo_music_list_btn_check(uint32_t List_id, bool state)
{
lv_obj_t * btn = lv_obj_get_child(list, List_id);
lv_obj_t * icon = lv_obj_get_child(btn, 0);
if(state) {
lv_obj_add_state(btn, LV_STATE_CHECKED);
lv_img_set_src(icon, &img_lv_demo_music_btn_list_pause);
lv_obj_scroll_to_view(btn, LV_ANIM_ON);
}
else {
lv_obj_clear_state(btn, LV_STATE_CHECKED);
lv_img_set_src(icon, &img_lv_demo_music_btn_list_play);
}
// lv_obj_scroll_to_view(panel1, LV_ANIM_ON);
lv_obj_invalidate(panel1);
}
void btn_click_event_cb(lv_event_t * e)
{
lv_obj_t * btn = lv_event_get_target(e);
uint32_t idx = lv_obj_get_child_id(btn);
_lv_demo_music_play(idx);
}
/************************************************************************************************************************************
* create_ctrl_box END * create_ctrl_box END * create_ctrl_box END * create_ctrl_box END
************************************************************************************************************************************/
/************************************************************************************************************************************
*Music *Music *Music *Music
************************************************************************************************************************************/
void _lv_demo_music_main_close(void)
{
lv_timer_del(sec_counter_timer);
}
void _lv_demo_music_album_next(bool next)
{
uint32_t id = track_id;
if(next) {
id++;
if(id >= ACTIVE_TRACK_CNT) id = 0;
}
else {
if(id == 0) {
id = ACTIVE_TRACK_CNT - 1;
}
else {
id--;
}
}
_lv_demo_music_play(id);
}
void _lv_demo_music_play(uint32_t id)
{
if(Playing_Flag && id == track_id){
}
else{
if(id == track_id){
LVGL_Elapsed_Music();
LVGL_Resume_Music();
_lv_demo_music_resume();
}
else{
Audio_Elapsed = 0;
LVGL_Play_Music(id);
track_load(id);
_lv_demo_music_resume();
}
}
}
void _lv_demo_music_resume(void) {
Playing_Flag = true;
lv_anim_t a;
lv_anim_init(&a);
if(Audio_duration == 0)
Audio_duration_A = Audio_Elapsed + 100;
else
Audio_duration_A = Audio_duration;
lv_anim_set_values(&a, Audio_Elapsed * 1000, Audio_duration_A * 1000 - 1);
lv_anim_set_exec_cb(&a, spectrum_anim_cb);
lv_anim_set_var(&a, spectrum_obj);
lv_anim_set_time(&a, (Audio_duration_A - Audio_Elapsed) * 1000);
lv_anim_set_playback_time(&a, 0);
lv_anim_set_ready_cb(&a, spectrum_end_cb);
lv_anim_start(&a);
lv_timer_resume(sec_counter_timer);
lv_timer_resume(spectrum_timer);
lv_slider_set_range(slider_obj, 0, Audio_duration_A );
lv_obj_add_state(play_obj, LV_STATE_CHECKED);
LVGL_Resume_Music();
}
void _lv_demo_music_pause(void)
{
Playing_Flag = false;
lv_anim_del(spectrum_obj, spectrum_anim_cb);
lv_obj_invalidate(spectrum_obj);
lv_img_set_zoom(album_img_obj, LV_IMG_ZOOM_NONE);
lv_timer_pause(spectrum_timer);
lv_timer_pause(sec_counter_timer);
lv_obj_clear_state(play_obj, LV_STATE_CHECKED);
LVGL_Pause_Music();
}
/************************************************************************************************************************************
*Other *Other *Other *Other
************************************************************************************************************************************/
void remove_file_extension(char *Song_Name) {
char *last_dot = strrchr(Song_Name, '.');
if (last_dot != NULL) {
*last_dot = '\0';
}
}
void LVGL_Search_Music() {
ACTIVE_TRACK_CNT = tf.Folder_retrieval("/",".mp3",File_Name,100);
if(ACTIVE_TRACK_CNT) {
for (int i = 0; i < ACTIVE_TRACK_CNT; i++) {
strcpy(Song_Name, File_Name);
remove_file_extension(Song_Name);
}
LVGL_Play_Music(0);
}
}
void LVGL_Play_Music(uint32_t ID) {
Play_Music("/",File_Name);
// printf(File_Name);
strncpy(Audio_Name,Song_Name, sizeof(Song_Name));
// strncpy(Audio_Name,Song_Name, sizeof(Audio_Name));
Audio_duration = Music_Duration();
// while(Audio_duration == 0)
// Audio_duration = Music_Duration();
}
void LVGL_Elapsed_Music() {
Audio_Elapsed = Music_Elapsed();
}
uint16_t LVGL_Music_Energy( ) {
Audio_energy = Music_Energy();
return Audio_energy;
}
void LVGL_Resume_Music() {
Music_resume();
}
void LVGL_Pause_Music() {
Music_pause();
}
void LVGL_volume_adjustment(uint8_t Volume) {
Volume_adjustment(Volume);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
(操作提示:A键下一首,B键暂停播放)
https://www.bilibili.com/video/BV1PgRJYgEaR/?share_source=copy_web&vd_source=bed775561efaa18e90586ab291600126
static/image/hrline/1.gif
static/image/hrline/line1.png
总结:行空板K10专为信息科技教学打造,集成多种硬件资源,配合丰富的案例,能让学生在实际操作中学习编程知识,理解编程原理,提升编程技能。其AI功能如离线图像检测、语音交互,为图像识别和语音编程教学提供了有力支持。板载的温湿度、光照、加速度等传感器,可用于物理、地理等学科的数据采集实验,实现跨学科教学,拓宽学生的知识视野,培养综合应用知识的能力。K10具备WiFi、蓝牙等通信功能,结合传感器和执行器,能快速搭建物联网应用原型。在智能家居控制、环境监测等方面,可实现对设备的远程控制和数据实时采集分析,助力物联网项目的开发与落地。本项目围绕行空板K10展开,在行空板K10在硬件集成、教育应用、编程开发、功能实现等方面积极探索,可以帮助用户从底层原理上加深对mind+的工作机制的理解,和图形积木块编程形成良好的互补,为创客开发者和教育工作者提供了独特的价值。深入挖掘了从编程环境搭建到各硬件驱动、功能实现的过程,为学习Arduino开发、esp32芯片应用、LVGL图形库使用以及FreeRTOS任务管理等技术,为k10提供了系统的学习资料。k10丰富的硬件资源和图形库应用,为创意项目提供了实现基础,开发者可以基于LVGL图形库制作个性化的图形界面,开发如电子相册、视频播放器等创意应用。本项目具有以下特色:1. VS Code+PlatformIO的开发环境鉴于Arduino IDE在编译大型项目时速度较慢,推荐使用VS Code并安装PlatformIO插件来运行Arduino代码。PIO开发环境不仅显著提升了编译速度,还提供了更多功能,如AI编程助手支持,极大地提高了开发效率。所有案例统一管理在同一项目下,共享支持库依赖,调试方便。这种管理方式降低了开发难度,使得开发者可以更专注于应用功能的实现逻辑。2.LVGL图形库的移植与应用采用3项主流的lcd图形库点亮了屏幕,并成功将LVGL图形库移植到行空板K10上,并实现了多种应用,方便用户将自自有的代码快速地在k10上实现。这些应用不仅展示了LVGL的强大功能,还为嵌入式图形界面开发提供了新的思路和实践案例。3.基于FreeRTOS的多任务处理在人脸检测和综合应用案例中,利用FreeRTOS的任务和队列机制,实现了多任务并行处理。这种设计提高了系统的实时性和效率,为复杂应用的开发提供了技术支持。4.创意应用开发开发了多种创意应用,如贪吃蛇游戏、MP3播放器等。这些应用不仅展示了行空板K10的强大功能,还为开发者提供了丰富的学习和参考案例。5.扩展板设计为满足更多功能拓展需求,设计了一款专用扩展板,增加了2个I2C、1个UART的Grove接口以及1对I2C的Qwiic接口。这种扩展板设计使得K10能够连接更多类型的传感器,极大地丰富了应用场景。
项目全部资源在gitee上开源共享,包括K10专用扩展板设计,让更多开发者能够参与到项目的改进和创新中来。
完整项目源码gitee连接https://gitee.com/genvex/dfk10_arduino_demo
static/image/hrline/line1.png
static/image/hrline/1.gif
番外篇:行空板10移植小智(Xiaozhi-ESP32)人工智能对话机器人 在当今人工智能技术飞速发展的浪潮中,大模型层出不穷,仿佛成为每个项目不可或缺的元素。从智能家居到智能办公,从教育到娱乐,AI的应用场景不断拓展,而人工智能对话机器人更是成为当下最炙手可热的项目之一。无数开发者纷纷涌入这一赛道,试图打造出令人惊叹的智能语音助手,让AI真正走进人们的日常生活。从最新战报看来,小智(Xiaozhi-ESP32)无疑是目前最为成功的项目之一,成为这个农历新年最为热议的话题,网友们纷纷奔走相告。小智基于低成本的ESP32开发板,实现了本地化的语音识别与智能对话功能,为用户带来了一种全新的智能交互体验。小智基于乐鑫的ESP-IDF框架开发,通过WebSocket或UDP协议与大模型及TTS API服务连接,实现流式语音对话。能够进行自然流畅的对话,达到openAI的WebRTC水平,实测对话过程中没有延时和卡顿的情况。小智已被移植到很多具备音频解码的esp32设备上,如立创实战派ESP32-S3、乐鑫ESP32-S3-BOX3、M5Stack CoreS3等。K10上丰富的硬件资源,特别是双通道的麦克风用来实现语音对话项目优秀的解决方案。 那么如果也把小智移植到K10之上,岂不美哉!将小智移植到K10之上,不仅能够充分发挥K10的硬件优势,还为今天出现的嵌入式AI项目铺平了道路,例如加入摄像头智能项目。这无疑对于AI爱好者和教育工作者来说,都具有极高的价值和意义。 一、板卡驱动工作思路跟在Arduino环境是一样的,想要在idf环境下点亮屏幕,必须首先解决扩展芯片的驱动问题,因为在Arduino环境下我们已对xl95x4芯片有了比较深入的了解,我们在idf环境下需要选择一个跟idf环境兼容比较好的驱动,那么我首先想到的是esp自家的IO_expander. IO_expander将协助我们点亮屏幕和驱动交互按钮。 (1)项目根目录下idf_component.yml文件添加“espressif/esp_io_expander_tca95xx_16bit: ^2.0.0”IO_expander依赖库。之后再managed_components文件夹中会自动下载实际的支持库文件。
(2)点亮屏幕参照其他项目中ili9431屏幕的驱动在idf环境下点亮了屏幕,构造了K10专属的板卡支持。k10_audio_codec.cc 源码#include "application.h"
#include "audio_codecs/k10_audio_codec.h"
#include "config.h"
#include "display/lcd_display.h"
#include "esp_lcd_ili9341.h"
// #include "ex-button.h"
#include "font_awesome_symbols.h"
#include "iot/thing_manager.h"
#include "wifi_board.h"
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_log.h>
#include <wifi_station.h>
#include "esp_io_expander_tca95xx_16bit.h"
#define TAG "DF-K10"
static void pin_2_12_status_task(void *arg);
class Df_K10Board : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
LcdDisplay *display_;
void InitializeI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)1,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags =
{
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
}
void InitializeSpi() {
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = GPIO_NUM_21;
buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = GPIO_NUM_12;
buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeIoExpander() {
esp_io_expander_new_i2c_tca95xx_16bit(
i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_000, &io_expander);
esp_err_t ret;
ret = esp_io_expander_print_state(io_expander);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Print state failed: %s", esp_err_to_name(ret));
}
ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_0,
IO_EXPANDER_OUTPUT);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Set direction failed: %s", esp_err_to_name(ret));
}
ret = esp_io_expander_set_level(io_expander, 0, 1);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Set level failed: %s", esp_err_to_name(ret));
}
ret = esp_io_expander_set_dir(
io_expander, (IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_12),
IO_EXPANDER_INPUT);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Set direction failed: %s", esp_err_to_name(ret));
}
xTaskCreate(pin_2_12_status_task, "pin_2_12_status_task", 8*1024,
(void *)this, 5, NULL);
}
void InitializeButtons() {
// boot_button_ = EXButton(io_expander);
// boot_button_.setIOExpander(io_expander);
// boot_button_.OnClick(() {
// auto &app = Application::GetInstance();
// if (app.GetDeviceState() == kDeviceStateStarting &&
// !WifiStation::GetInstance().IsConnected()) {
// ResetWifiConfiguration();
// }
// app.ToggleChatState();
// });
}
void InitializeIli9341Display() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
// 液晶屏控制IO初始化
ESP_LOGD(TAG, "Install panel IO");
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = GPIO_NUM_14;
io_config.dc_gpio_num = GPIO_NUM_13;
io_config.spi_mode = 0;
io_config.pclk_hz = 40 * 1000 * 1000;
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io));
// 初始化液晶屏驱动芯片
ESP_LOGD(TAG, "Install LCD driver");
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.bits_per_pixel = 16;
panel_config.color_space = ESP_LCD_COLOR_SPACE_BGR;
ESP_ERROR_CHECK(esp_lcd_new_panel_ili9341(panel_io, &panel_config, &panel));
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel));
ESP_ERROR_CHECK(esp_lcd_panel_init(panel));
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, false));
ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY));
ESP_ERROR_CHECK(
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y));
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true));
display_ = new LcdDisplay(
panel_io, panel, DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y,
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
}
// 物联网初始化,添加对 AI 可见设备
void InitializeIot() {
auto &thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Speaker"));
}
public:
esp_io_expander_handle_t io_expander;
Df_K10Board() {
InitializeI2c();
InitializeIoExpander();
InitializeSpi();
InitializeIli9341Display();
InitializeButtons();
InitializeIot();
}
virtual AudioCodec *GetAudioCodec() override {
static K10AudioCodec *audio_codec = nullptr;
if (audio_codec == nullptr) {
audio_codec = new K10AudioCodec(
i2c_bus_, AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN, AUDIO_CODEC_PA_PIN,
AUDIO_CODEC_ES8311_ADDR, AUDIO_CODEC_ES7210_ADDR,
AUDIO_INPUT_REFERENCE);
}
return audio_codec;
}
virtual Display *GetDisplay() override { return display_; }
};
static void pin_2_12_status_task(void *arg) {
Df_K10Board *board = (Df_K10Board *)arg;// Use pointer to the board object
uint32_t input_level_mask = 0;
uint32_t prev_input_level_mask = 0; // To store the previous state
uint32_t debounce_delay =
50 / portTICK_PERIOD_MS; // Set debounce time (50 ms)
while (1) {
vTaskDelay(20 / portTICK_PERIOD_MS); // Regular task delay
esp_err_t ret = esp_io_expander_get_level(
board->io_expander, (IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_12),
&input_level_mask);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to get level mask: %s", esp_err_to_name(ret));
continue;
}
// If the state has changed, start debounce process
if (input_level_mask != prev_input_level_mask) {
// State changed, wait for debounce time
vTaskDelay(debounce_delay);
// Re-check the state after debounce time
ret = esp_io_expander_get_level(
board->io_expander, (IO_EXPANDER_PIN_NUM_2 | IO_EXPANDER_PIN_NUM_12),
&input_level_mask);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to get level mask: %s", esp_err_to_name(ret));
continue;
}
// If the state is still the same, consider it stable and log
if (input_level_mask != prev_input_level_mask) {
prev_input_level_mask = input_level_mask;
// Now check the stable state and log the button press
if (!(input_level_mask & IO_EXPANDER_PIN_NUM_2)) {
ESP_LOGI(TAG, "Button B pressed");
auto &app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting &&
!WifiStation::GetInstance().IsConnected()) {
board->ResetWifiConfiguration();
}
app.ToggleChatState();
}
if (!(input_level_mask & IO_EXPANDER_PIN_NUM_12)) {
ESP_LOGI(TAG, "Button A pressed");
auto &app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting &&
!WifiStation::GetInstance().IsConnected()) {
board->ResetWifiConfiguration();
}
app.ToggleChatState();
}
}
}
// No change in state, simply continue with the loop
}
}
DECLARE_BOARD(Df_K10Board);
二、音频驱动解决完了基础适配工作后,另外一个艰巨的任务是适配音频解码,k10使用的音频处理器是 ES7243E,功放是NS4168,在小智系统里没有找到使用es7243的音频的板卡可以参考,经过反复测试,幸运地发现ES7243E跟ESP7211兼容,参照乐鑫的box_audio_codec样式做少量修改即可完成。 为了保障可移植性为k10新建了一套音频解码文件,方便适配小智的升级版本,只要把关键文件拷贝到对应的位置和作相应的修改就可以了。
k10_audio_codec.cc 源码#include "k10_audio_codec.h"
#include <esp_log.h>
#include <driver/i2c.h>
#include <driver/i2s_tdm.h>
#include <cmath>
static const char TAG[] = "K10AudioCodec";
K10AudioCodec::K10AudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate,
gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference) {
duplex_ = true; // 是否双工
input_reference_ = input_reference; // 是否使用参考输入,实现回声消除
input_channels_ = input_reference_ ? 2 : 1; // 输入通道数
input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate;
CreateDuplexChannels(mclk, bclk, ws, dout, din);
// Do initialize of related interface: data_if, ctrl_if and gpio_if
audio_codec_i2s_cfg_t i2s_cfg = {
.port = I2S_NUM_0,
.rx_handle = rx_handle_,
.tx_handle = tx_handle_,
};
data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
assert(data_if_ != NULL);
audio_codec_i2c_cfg_t i2c_cfg = {
.port = I2C_NUM_1,
.addr = es7210_addr,
.bus_handle = i2c_master_handle,
};
const audio_codec_ctrl_if_t *in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(in_ctrl_if_ != NULL);
es7243e_codec_cfg_t es7243e_cfg = {
.ctrl_if = in_ctrl_if_,
};
const audio_codec_if_t *in_codec_if_ = es7243e_codec_new(&es7243e_cfg);
assert(in_codec_if_ != NULL);
esp_codec_dev_cfg_t codec_es7243e_dev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_IN,
.codec_if = in_codec_if_,
.data_if = data_if_,
};
input_dev_ = esp_codec_dev_new(&codec_es7243e_dev_cfg);
assert(input_dev_ != NULL);
ESP_LOGI(TAG, "BoxAudioDevice initialized");
}
K10AudioCodec::~K10AudioCodec() {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
esp_codec_dev_delete(output_dev_);
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
esp_codec_dev_delete(input_dev_);
audio_codec_delete_codec_if(in_codec_if_);
audio_codec_delete_ctrl_if(in_ctrl_if_);
audio_codec_delete_codec_if(out_codec_if_);
audio_codec_delete_ctrl_if(out_ctrl_if_);
audio_codec_delete_gpio_if(gpio_if_);
audio_codec_delete_data_if(data_if_);
}
void K10AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
assert(input_sample_rate_ == output_sample_rate_);
i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0,
.role = I2S_ROLE_MASTER,
.dma_desc_num = 6,
.dma_frame_num = 240,
.auto_clear_after_cb = true,
.auto_clear_before_cb = false,
.intr_priority = 0,
};
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
i2s_std_config_t std_cfg = {
.clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT,
.ext_clk_freq_hz = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256
},
.slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_MONO,
.slot_mask = I2S_STD_SLOT_BOTH,
.ws_width = I2S_DATA_BIT_WIDTH_16BIT,
.ws_pol = false,
.bit_shift = true,
.left_align = true,
.big_endian = false,
.bit_order_lsb = false
},
.gpio_cfg = {
// .mclk = mclk,
.bclk = bclk,
.ws = ws,
.dout = dout,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false
}
}
};
i2s_tdm_config_t tdm_cfg = {
.clk_cfg = {
.sample_rate_hz = (uint32_t)input_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT,
.ext_clk_freq_hz = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
.bclk_div = 8,
},
.slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3),
.ws_width = I2S_TDM_AUTO_WS_WIDTH,
.ws_pol = false,
.bit_shift = true,
.left_align = false,
.big_endian = false,
.bit_order_lsb = false,
.skip_mask = false,
.total_slot = I2S_TDM_AUTO_SLOT_NUM
},
.gpio_cfg = {
.mclk = mclk,
.bclk = bclk,
.ws = ws,
.dout = I2S_GPIO_UNUSED,
.din = din,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false
}
}
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg));
ESP_LOGI(TAG, "Duplex channels created");
}
void K10AudioCodec::SetOutputVolume(int volume) {
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
AudioCodec::SetOutputVolume(volume);
}
void K10AudioCodec::EnableInput(bool enable) {
if (enable == input_enabled_) {
return;
}
if (enable) {
esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16,
.channel = 4,
.channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
.sample_rate = (uint32_t)output_sample_rate_,
.mclk_multiple = 0,
};
if (input_reference_) {
fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1);
}
ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
// ESP_ERROR_CHECK(esp_codec_dev_set_in_channel_gain(input_dev_, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), 40.0));
} else {
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
}
AudioCodec::EnableInput(enable);
}
void K10AudioCodec::EnableOutput(bool enable) {
if (enable == output_enabled_) {
return;
}
AudioCodec::SetOutputVolume(100);
AudioCodec::EnableOutput(enable);
}
int K10AudioCodec::Read(int16_t* dest, int samples) {
if (input_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
}
return samples;
}
// int K10AudioCodec::Write(const int16_t* data, int samples) {
// // if (output_enabled_) {
// // ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
// std::vector<int32_t> buffer(samples);
// // output_volume_: 0-100
// // volume_factor_: 0-65536
// int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536;
// for (int i = 0; i < samples; i++) {
// int64_t temp = int64_t(data) * volume_factor; // 使用 int64_t 进行乘法运算
// if (temp > INT32_MAX) {
// buffer = INT32_MAX;
// } else if (temp < INT32_MIN) {
// buffer = INT32_MIN;
// } else {
// buffer = static_cast<int32_t>(temp);
// }
// }
// size_t bytes_written;
// ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * sizeof(int32_t), &bytes_written, portMAX_DELAY));
// return bytes_written / sizeof(int32_t);
// // }
// // return samples;
// }
int K10AudioCodec::Write(const int16_t* data, int samples) {
// if (output_enabled_) {
std::vector<int32_t> buffer(samples * 2);// Allocate buffer for 2x samples
// Apply volume adjustment (same as before)
int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536;
for (int i = 0; i < samples; i++) {
int64_t temp = int64_t(data) * volume_factor;
if (temp > INT32_MAX) {
buffer = INT32_MAX;
} else if (temp < INT32_MIN) {
buffer = INT32_MIN;
} else {
buffer = static_cast<int32_t>(temp);
}
// Repeat each sample for slow playback (assuming mono audio)
buffer = buffer;
}
size_t bytes_written;
ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * 2 * sizeof(int32_t), &bytes_written, portMAX_DELAY));
return bytes_written / sizeof(int32_t);
// }
// return samples;
}
三、适配文件的重要修改 根目录CMakeLists.txt文件修改: (1)set(SOURCES位置添加"audio_codecs/k10_audio_codec.cc" 确保新添加的音频解码源文件被编译。# 根据 BOARD_TYPE 配置添加对应的板级文件 处添加:elseif(CONFIG_BOARD_TYPE_DF_K10)set(BOARD_TYPE "df-k10")确保menuconfig里增加df-k10选项。 (2)根目录Kconfig.projbuild文件修改:choice BOARD_TYPE 处 添加 config BOARD_TYPE_DF_K10 bool "DF K10"字段。
Config里出现 k10可选项。 四、编译过程遇到的问题: 如果板卡选不是esp32s3的话会出现 img_font(表情包)相关问题,需要重新把板卡设置会S3,有时会设置失败,此时,需要把项目中的build文件夹整个删除,再来设置板卡就可以了。 需要手动清理构建目录由于 CMake 检测到该目录并不符合预期的构建目录结构,它拒绝自动删除该目录中的文件。你可以手动删除该目录中的内容,或者直接删除整个 build 目录:进入 L:\2025\xiaozhi-esp32 目录。删除 build 目录(可以直接删除整个目录)。然后重新执行构建命令。
版本升级后,屏幕增加了字体参数,参考其他板卡支持,进行修改。
0x0偏移写入
static/image/hrline/1.gif
至此我们成功地把小智移植到了行空板k10之上,使得我们手上的行空板成为这条街最靓D仔,不仅能开展编程教育还可以当“台湾女朋友”陪聊。 (让她萌俩愉快地聊起来,我负责吃瓜)
https://www.bilibili.com/video/BV12AfdYsE5N?buvid=Z343EF33C8185D50402F985852378FFD6AAB&is_story_h5=false&mid=UeMYnHruLfOHHdo8CDiFSQ%3D%3D&plat_id=116&share_from=ugc&share_medium=iphone&share_plat=ios&share_session_id=ADA4C09D-42EA-4A5D-94D8-55CE28B23BD6&share_source=WEIXIN&share_tag=s_i&spmid=united.player-video-detail.0.0×tamp=1738555791&unique_k=aAemF5d&up_id=396355825
附件提供了最新版小智(1.01)的K10适配版。 刷机说明:1.伍老师提供的刷机说明:https://mc.dfrobot.com.cn/thread-323803-1-1.html2.小智 AI 聊天机器人 https://github.com/78/xiaozhi-esp32 (本项目的语音对话服务由“小智”提供,本项目仅提供k10接入”小智”的解决方案作学习交流,如对小智的实质运行机理请参考github网址深入学习)
dfk10_xiaozhi项目源码 https://gitee.com/genvex/df_k10_xiaozhi
小智固件1.1.2版本,0偏置刷入,配网即可使用。
MicroBlocks在线积木编程 固件及网址
本帖最后由 岑剑伟 于 2025-2-16 11:21 编辑k10专用固件项目地址:https://gitee.com/genvex/smallvm,可自行编译固件学习,已合并到MicroBlocks中国。
附件为MicroBlocks固件,0偏置刷入即可 体验。
MicroBlocks在线积木编程体验网址:
https://microblocksfun.cn/run
欢迎大家来共建生态。
扩展板接口的VCC咋都是5V?? 豆爸 发表于 2025-2-17 06:51
扩展板接口的VCC咋都是5V??
目标是 要升到 5v的,为了兼容m5家的传感器 k10小智固件版本1.2.2,支持语音“音量”控制。 岑剑伟 发表于 2025-2-22 11:02
k10小智固件版本1.2.2,支持语音“音量”控制。
老师,这个还是旧版 anthonzhai 发表于 2025-2-23 12:40
老师,这个还是旧版
谢谢回复,我检查下哈 小智1.3.0版本固件,对于K10来说,可见变化是标题增加了当前时间显示。
小智1.3.0版本固件,16号字体(小字体),表情包,字体小一号,可以显示更多内容。 老师,可以教一下在最新版的小智源码上如何新增对k10板子的支持吗?已经有k10的驱动等! anthonzhai 发表于 2025-3-6 19:34
老师,可以教一下在最新版的小智源码上如何新增对k10板子的支持吗?已经有k10的驱动等! ...
已完成github合并,可以同步升级了
页:
[1]