前言
1、 什么是小智:小智AI聊天机器人,以下简称“小智”
2、 什么是AI大模型:从国外的ChatGPT到国内各个巨头百花齐放百家争鸣,到最近的DeepSeek国产大模型后来居上,直接比肩元老级别的大模型,可能大多数人都已经在被这两年飞速发展的AI大语言模型冲击到或者实际运用到生产生活中了。可能有的人还是只接触过文字一收一发的模式,但是其实从前的科幻电影场面:人类与机器直接语音对话已经很成熟了,现在更是能够很低成本的实现。
3、 什么是硬禾学堂与CrowPanel屏幕开发板:硬禾学堂是我最近发现的一个极佳的学习研究平台,他们给出了很多活动,活动的显著特点就是,付费购买,完成项目之后则返款。这样的模式促使我为了我的小钱钱不得不专心坐下来进行研究学习。CrowPanel HMI开发板就是平台的一个活动中的一块板卡,我将在这个板卡实现小智的移植来作为我的项目作业提交。
注意!如果你对人工智能不感兴趣,也对AI语音对话不感兴趣,也对ESP32的IDF开发/lvgl开发不感兴趣,也对我不感兴趣的话,那么感谢看到这里,接下来建议可以退出啦,毕竟这样的你,看这个一定是感觉十分甚至九分无聊的。
移植流程
1、 首先我声明一下,我只能说是在记录一点经验,这不是教程,更像一种聊天式的分享,因为我自己本来也是一个业余选手。即使现在跑起来了,我还是有很多东西没有搞懂,但是时间紧急,交作业deadline已经到了,跑起来了就不要动了(笑)。
2、 接下来给大家看一下需要准备哪些东西
a) 文档资料,主要是一些datasheet、原理图啊之类的文档
b) 官方资料,比如乐鑫ESP32官方的开发文档,比如CrowPanel官方的示例代码
c) D老师和C老师
众所周知,D老师学识渊博,回答的有深度,就是脾气不太好,咨询问题有的时候爱理不理的,咱也不敢说啥
d) 准备一盒小番茄,吃起来还不错,甜度适中而且很有番茄味,对开发很有帮助,
e) 这是一盒哈密瓜,晚上超市打折买的,特别熟特别甜
f) 还可以准备一杯热水,多喝热水,有益健康。
3、 其他的废话不多说,项目移植。首先进行可行性分析
那么好,我们直接看小智的硬件需求(面包板Wifi版本)(小智AI聊天机器人面包板DIY硬件清单与接线教程-飞书云文档):
然后再看看手上开发板的现状(CrowPanel ESP32 Display 4.3英寸HMI开发板):
下面给出一个简单的直观对比:
平台 | 主控 | 规格 | 屏幕 | 麦克风 | 音频功放 | 备注 |
小智 | ESP32 | 16M Flash, | OLED | INMP441 | MAX98357A |
|
CrowPanel | ESP32 | 4M Flash, | TFT | 无 | NS4168 |
|
从上面的对比来看,我们的CrowPanel板子现状有点惨,首先程序Flash空间和外部扩展SPI RAM空间都如此之小,但是却要背负那么大分辨率的显示屏驱动责任。好在功放虽然型号不同,但是都是标准IIS接口,所以亲测通用,代码的这部分甚至都不用改的。板子上没有麦克风,所以需要外接麦克风,能接的下吗?除此之外小智还使用到了WS2812全彩LED用于指示状态的,我们是否要加呢,有没有地方加呢?带着这些疑问,我们看下规格:
猛的一看接口好像是预留了很多,
看下来满打满算只有4个IO是真正自由的,是不是还不如猛的一看。
那么4个IO够吗,我们看都要干些什么:
首先需要连INMP441麦克风模块,那么我们查一下INMP441的引脚定义可以发现,由于我们是单个麦克风单声道,软件里面写死左或者右之后,L/R引脚可以通过外部高低电平直接固定。只需要SCK、WS、SD三个IO;
此外需要连接WS2812全彩灯,这是一种单线通信的全彩LED灯,那么顾名思义,它只需要使用一个IO连一根数据线就可以了,接到第一个灯的DIN引脚,可以接单个LED,也可以接一串LED,只需要将灯的DOUT和下一个灯的DIN手拉手串起来即可。
根据以上分析,我们将空余的4个IO刚好用完,接入了麦克风和全彩指示灯。
我们可以直接使用杜邦线从板卡后面的排母或者4P座子接,但是不够优雅,所以不如像我这样做一个简单的扩展板,就是通过排针直插板卡的扩展口,将麦克风和WS2812全彩LED的线路连接在PCB上面完成,麦克风还是直接用淘宝买的模组,因为好买好焊接。L/R引脚预留两个方案,可以上拉也可以下拉。在这个项目我们选择上拉=右声道,可以和扬声器保持一致。
整体上来看,除了给的程序空间太小,其他方面都有了,那么这可行性是行还是不行呢?
那么我是怎么做的呢,你不是模块规格不一样吗,那N16R8的模组买回来了
其他的我不知道行不行,但是朋友,动手方面,骡子一样的动力我是有的。
上DIY的焊台,开换
那么也是二话不说换完了,之后研究了好几天,结论就是我踩了一个大坑,确实做不了,我又给换回来了����。。。
4、 硬件踩坑记录
刚才说到的为什么换上和原版小智一模一样的模组反而不行了?
我们回来看板子原理图:
此处我们会发现,这个板子将模组全部的引脚都用上了,其中主要消耗就是RGB565屏幕驱动。当我换成N16R8模组我修改引脚到板子实际的引脚然后编译发现总是提醒我,IO35,IO36,IO37已经被占用了,我?不是预留的空闲IO吗?在代码里搜索半天没有发现哪里用了这些IO。上网搜了一番才发现有人和我哦踩过一样的坑,原来规格书有一行小字:
那么什么是集成Octal SPI PSRAM的模组呢,看下表:
那么这样就清晰多了,原来是我白忙活了呀,我以为啥呢。
然后就是不语,只是一味的换回来原装的模块,否则即使其他的跑成功了,但是4个空闲IO只剩下IO38一个了,小智要变聋子了,额咳咳。
这边提醒大家做项目需要换大PSRAM的小伙伴关注一下这里,大PSRAM的模组可用IO要少3个。ESP32系列很多模组都有类似现象,比如ESP32-WROOM-1带PSRAM比不带PSRAM的模组少了一个可用IO用于PSRAM的片选;比如ESP32-C3芯片内置4M Flash的型号比不内置Flash的型号少了一个可用IO用于内部Flash的电源,还是多看看规格书吧。。
5、 软件移植记录
首先总体来说,这个移植感觉并不难,因为从我的角度来看:
a) 是我第一次接触ESP32 IDF开发
b) 是我第一次接触C++代码
c) 是我第一次接触lvgl开发
d) 是我第一次接触Cmake,menuconfig
e) 是我第一次接触IIS外设,RGB接口,WS2812驱动等等等等
另外我在研究这个项目移植的同时,项目本身也是在进行持续的迭代,所以我也在不断尝试更新。因为我目前还是没有真正研究学习过git代码管理的,所以现在只是自发的下载源码,重复操作移植。最近的新特性比如1.3版本开始官方增加了状态栏在待机状态下显示时钟以及官方的RGB屏幕的支持,1.4版本增加了一些音效。截至当前,最新的版本还是1.4版本。
那么为了增加代入感,我们直接从零开始,看看都做了什么工作。
首先第一步我们二话不说,要搞到源码。这边我是配置好了git环境,所以在想要存放代码的文件夹我们直接clone就可以了哈:
git clone https://github.com/78/xiaozhi-esp32.git
如果你的电脑没有git环境,可以选择直接去github上面去点击下载zip压缩包然后解压出来就可以了。
那么这边我们就可以打开VS Code来打开项目了。这边我就不去介绍VS Code怎么配置ESP-IDF扩展相关的内容了,毕竟我也是百度搜到的。我这边安装的是IDF V5.3.2的环境。
如果顺利的话,打开文件夹之后,首先简单的说一下我对工程内容框架的个人总结
接下来,我是根据我的经验走的,没有看过什么教程之类的,比较随心所欲,大佬勿喷。
这边我们首先看一下状态栏,也就是界面对底下一行,这边展示了一个COM口和一个芯片型号(鼠标悬停可以看到解释)。
我们需要接入板卡,然后获取到是在哪个COM口,这边就可以直接切换上了。
然后芯片我们是esp32-s3的,因此也改一下。
这两个配置好之后就其实如果是原版小智就已经配置完了。这个接下来会介绍。
我们主要的任务无非就是:将软件适配到现有的硬件上去。那么有的朋友就会问了,要修改什么呢?带着这样的疑问,我们打开menuconfig,直接点击小齿轮。首次开启会加载一些配置,有点慢,没关系,视频我会剪掉。
这里就可视化的展示了全部的工程配置,需要什么功能就使能,不需要什么就禁用,包括一些参数的配置都是在这边进行。
我们先点击Xiaozhi Assistant进入小智相关的配置。这个不是ESP-IDF自身的内容,这个是小智开发者增加的友好的配置界面,动手点一点即可完成一些基本的配置。这边我们就发现原来有一个Board Type,可以选择用的什么板子啊,默认是面包板连线版的小智,那么这边也可以给大家分享一下:这些板子其实都是基于esp32/esp32s3/esp32c3的开发板,你会发现有的是音频编解码方案,有的是iis麦克风与扬声器;有的是oled点阵屏,有的是lcd彩屏,有的还支持触摸屏;有的是可充电的;有的是板子,有的是带外壳的优雅小产品。。对于软件的角度来说,最主要的区别就是这些驱动的引脚不同,用到的外设不同。回到我们手上的CrowPanel板卡,就是一个确定的方案,所以我们需要结合手上的硬件方案去改软件即可。这里我们将其改为“鱼鹰科技3.13LCD开发板”,原因是这个板卡是目前唯一个同样使用RGB屏幕的板子。在这个基础上改会比较合适。
另外取消勾选这个“启用音频降噪、增益处理”,应该是可以一定程度减小固件的。
随后我们继续修改模组相关的配置,因为我们的模组参数是n4r2,表示是用的4M的Flash,2M的PSRAM。这边需要配置一下flash参数、分区表和PSRAM参数:
其实到这一步配置就完成了。但是如果就这样回去修改驱动相关的代码结果会发现,flash空间不足。。大家感兴趣的可以自己试。这边我们直接打开准备的资料,ESP32官方的固件大小优化指导进行配置:最小化二进制文件大小- ESP32-S3 - — ESP-IDF编程指南v5.2.4文档
文档前面的部分详细的解释了如何分析固件占用情况,感兴趣的可以看下,但是我不看哈,直接跳转到最后面学习怎么减小固件大小的操作
根据流程将能配置的配置完,然后自发尝试性的调整一些参数之后,配置部分就完成了。其中需要注意的是我们保留第三个。
接下来我们进行具体代码的修改。首先是回顾刚才改到的禁用的C++异常部分,我们考虑这个修改会对工程造成什么影响。那么主要是会导致C++的catch、try、throw这些用法报错。我们直接全局搜索,发现只在thing相关代码影响到。那么没有办法,C++我不懂,C语音我也是个半吊子,所以二话不说,我们将情况给C老师解释一下,让他帮我们在不影响原有功能的情况下移除不支持的写法。C老师也是很给力,直接帮我们写好了,那么我们怎么做,我们应该保持怀疑的态度,拿着C老师的代码去咨询一下D老师点评一下。在得到D老师的肯定之后,我们直接替换掉。
接下来开始处理具体的适配从哪里开始都行,因为代码是模块化的。
我们可以先处理音频部分。已知我们没有使用专用的音频编解码芯片,而是iis功放;另外IIS功放所接的引脚需要修改,另外IIS是右声道的,需要修改。对于main\audio_codecs\no_audio_codec.cc文件,修改了声道;对于main\boards\kevin-yuying-313lcd\config.h,修改IIS引脚IO,去除未用到的定义。此外还需要在板卡定义的源文件中修改GetAudioCodec虚函数的
接下来处理按钮输入和LED输出部分,先在main\boards\kevin-yuying-313lcd\config.h文件配置按钮和LED的引脚。然后需要调整一下代码。按钮设置为长按用于配网,单击用于激活对话;指示灯设置数量为5个,需要包含circular_strip.h,此为灯串的驱动部分。
1、 按键相关代码
void InitializeButtons() {
boot_button_.OnLongPress([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
});
boot_button_.OnClick([this]() {
Application::GetInstance().ToggleChatState();
});
}
2、 LED相关代码新增
virtual Led* GetLed() override {
static CircularStrip led(BUILTIN_LED_GPIO, 5);
return &led;
}
接下来是大头,显示部分。我们一样先处理引脚定义。这边观察一下官方这个rgb屏幕驱动代码,这是一个很标准的常见的rgb屏幕,一般会通过一个三线的SPI进行初始化配置,然后再通过RGB口进行显示通讯。所以代码中有SPI引脚的配置,SPI初始化等。但是我们手上的板卡会发现没有spi初始化部分,所以可以删除掉相关的全部代码。然后配置引脚定义,配置时序参数(参考规格书),处理多余的面板配置部分,直接调用新建函数来初始化屏幕。这里需要关注一些参数的选定,这些在规格书也似乎没有,我是通过官方例程来反向推断出来的。需要注意的是我们的屏幕分辨率不是很高,这边我们直接将lvgl声明的字体字号要改为16
LV_FONT_DECLARE(font_puhui_16_4);
LV_FONT_DECLARE(font_awesome_16_4);
其他部分:
1、RGB面板结构
#define GC9503V_LCD_RGB_BUFFER_NUMS (2)
#define GC9503V_LCD_RGB_BOUNCE_BUFFER_HEIGHT (8)
ESP_LOGI(TAG, "Install RGB LCD panel driver");
esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_rgb_panel_config_t rgb_config = {
.clk_src = LCD_CLK_SRC_PLL160M,
.timings = GC9503_376_960_PANEL_60HZ_RGB_TIMING(),
.data_width = 16, // RGB565 in parallel mode, thus 16bit in width
.bits_per_pixel = 16,
.num_fbs = GC9503V_LCD_RGB_BUFFER_NUMS,
.bounce_buffer_size_px = GC9503V_LCD_H_RES * GC9503V_LCD_RGB_BOUNCE_BUFFER_HEIGHT,
.dma_burst_size = 64,
.hsync_gpio_num = GC9503V_PIN_NUM_HSYNC,
.vsync_gpio_num = GC9503V_PIN_NUM_VSYNC,
.de_gpio_num = GC9503V_PIN_NUM_DE,
.pclk_gpio_num = GC9503V_PIN_NUM_PCLK,
.disp_gpio_num = GC9503V_PIN_NUM_DISP_EN,
.data_gpio_nums = {
GC9503V_PIN_NUM_DATA0,
GC9503V_PIN_NUM_DATA1,
GC9503V_PIN_NUM_DATA2,
GC9503V_PIN_NUM_DATA3,
GC9503V_PIN_NUM_DATA4,
GC9503V_PIN_NUM_DATA5,
GC9503V_PIN_NUM_DATA6,
GC9503V_PIN_NUM_DATA7,
GC9503V_PIN_NUM_DATA8,
GC9503V_PIN_NUM_DATA9,
GC9503V_PIN_NUM_DATA10,
GC9503V_PIN_NUM_DATA11,
GC9503V_PIN_NUM_DATA12,
GC9503V_PIN_NUM_DATA13,
GC9503V_PIN_NUM_DATA14,
GC9503V_PIN_NUM_DATA15,
},
.flags= {
.fb_in_psram = true, // allocate frame buffer in PSRAM
}
};
ESP_LOGI(TAG, "Initialize RGB LCD panel");
(esp_lcd_new_rgb_panel(&rgb_config, &panel_handle));
(esp_lcd_panel_reset(panel_handle));
(esp_lcd_panel_init(panel_handle));
display_ = new RgbLcdDisplay(panel_io, panel_handle,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X,
DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY,
{
.text_font = &font_puhui_16_4,
.icon_font = &font_awesome_16_4,
.emoji_font = font_emoji_64_init(),
});
}
2、RGB时序相关配置
#define GC9503_376_960_PANEL_60HZ_RGB_TIMING() \
{ \
.pclk_hz = GC9503V_LCD_PIXEL_CLOCK_HZ, \
.h_res = 480, \
.v_res = 272, \
.hsync_pulse_width = 4, \
.hsync_back_porch = 43, \
.hsync_front_porch = 8, \
.vsync_pulse_width = 4, \
.vsync_back_porch = 12, \
.vsync_front_porch = 8, \
.flags = { \
.hsync_idle_low = 0, \
.vsync_idle_low = 0, \
.de_idle_high = 0, \
.pclk_active_neg = 1, \
.pclk_idle_high = 0, \
}, \
}
3、 lvgl移植相关的结构体
ESP_LOGI(TAG, "Adding LCD screen");
const lvgl_port_display_cfg_t display_cfg = {
// .io_handle = panel_io_,
.panel_handle = panel_,
.buffer_size = static_cast<uint32_t>(width_ * 10),
.double_buffer = true,
.hres = static_cast<uint32_t>(width_),
.vres = static_cast<uint32_t>(height_),
.rotation = {
.swap_xy = swap_xy,
.mirror_x = mirror_x,
.mirror_y = mirror_y,
},
.flags = {
.buff_dma = 1,
.buff_spiram = 0,
.sw_rotate = 0,
.swap_bytes = 0,
.full_refresh = 0,
.direct_mode = 0,
},
};
const lvgl_port_display_rgb_cfg_t rgb_cfg = {
.flags = {
.bb_mode = false,
.avoid_tearing = false,
}
};
display_ = lvgl_port_add_disp_rgb(&display_cfg, &rgb_cfg);
处理完这些,我们回头处理前面的分区表文件。
不出意外的话,此时再进行编译是很顺利的,那么连上屏幕下载代码就可以看到小智已经成功运行起来了。
备注:优化项目有很多
1、 未激活状态下的提示词增加主动换行
2、 个人比较喜欢暗黑系的主题,这个可以在SDK配置中直接使能
成果展示
1、扩展板打板以及焊接成品
2、指示灯效果,不同颜色表示不同状态;闪烁表示配网,流水灯表示连接等
3、小智显示效果