概要
现在想分析构成一个语音助手终端所需要完成所需要的资源和准备。
首先,需要确定要实现的功能。
功能
- 语音识别:解析用户输入的语音内容。
- 语音播报:播放合成语音输出。
- 语义理解:理解语音内容中的语义, 通过语音进行智能对话。
- 按钮控制:支持通过按钮控制交互。
- 屏幕显示:将相关信息显示在屏幕上。
方案构型
为了实现所需要的功能,需要根据已有的资料进行分析,选择什么样子的实现方案。
首先为了完成语音识别, 设备需要使用麦克风。
为了进行语音播报,设备需要有扬声器。
为了进行语义理解,这里直接接入大语言模型。
为了进行按钮控制,设备需要有按钮。
为了实现屏幕显示,设备需要有屏幕外设。
而为了将上面的功能构组合成一个整体,考虑到便携性,可以使用嵌入式设备来进行开发。
修改后保存为png
嵌入式最重要的主控部分,根据类型和官方SDK可以运行不同类型的系统,考虑到开发和维护方便以及功能性验证,这里选择了 Linux系统为基础来进行开发,依赖Linux系统的网络和丰富的API和库,可以方便的进行功能验证。
因为市面上有现成的开发板可以用于快速验证,这里选择了SoC为R329
的 MaixII-Sense
继续
硬件资料
来自MaixII-Sense
官方的设计资料:
Sipeed_MAIXSense_SCH_1.0
Sipeed_MAIX-II-A_SCH_1.0
Sipeed_MAIXSense_SensorBoard_SCH_1.0
设备成品预览图如下:
软件驱动
考虑到代码兼容性问题,基于 Linux
使用 Python
+ Shell
+ C/C++驱动
进行开发
Armbain 系统源码
源码: sipeed/linux at r329-wip (github.com)
编译: R329主线armbian内核,系统,驱动开发方法 - Sipeed Wiki
烧录: 系统烧录 - Sipeed Wiki
音频驱动
源码:linux/sound at r329-wip · sipeed/linux (github.com)
文字转语音
语音播放
录音
语音转文字
代码: aimi_board/azure_api/init.py at main · observerkei/aimi_board (github.com)
语音转文字也有本地模型训练运行的方式进行识别,考虑到片上资源的稀缺性和识别的准确率,这里采用azure
云端协作的方式实现,官方提供了使用的API文档: microsoft-cognitiveservices-speech-sdk package | Microsoft Learn
上面的代码已经测试过可以直接使用: 语音转文字
按键驱动
源码:linux/drivers/input at r329-wip · sipeed/linux (github.com)
检查按键驱动是否正常
安装依赖
通过程序检测按键是否按下
显示驱动
源码:
显示部分通过framebuffer
方式驱动240x240分辨率的SPI屏幕。
framebuffer可以理解为linux提供的一种内存到屏幕像素的显示映射, 在屏幕设备加载完成后,会在linux的文件系统下创建一个 /dev/fb0
的设备文件,通过在程序里面使用linux提供的系统调用访问这个设备文件,可以读取到屏幕的分辨率、显示方式、以及能直接通过内存映射的方式修改屏幕上显示的像素点。
framebuffer有多种RGB显示类型,RGB表示的是一个LED像素可以显示三种颜色,而RGB显示类型中有一种RGB565的显示方式,这种显示方式表示的是 RGB的 Read/Green/Blue 三种颜色分别分配的比特位, 加起来刚好是 5+6+5=16 位,而8位是1个字节,在内存中则表示RGB565对应显示一个RGB灯珠的时候,占用 2个字节(也就是16位),而人眼对于绿色更加敏感,所以Green分配的比特相对其他颜色多一位。
通过framebuffer方式访问屏幕设备文件 /dev/fb0
的时候,可以将屏幕对应像素通过 mmap
方式映射到内存中,这块内存就相当于显存,操作这块内存可以直接修改屏幕上显示的像素点。
而mmap
的这个显存中内存和屏幕像素的对应关系类似如下:
比如有一块分辨率为8x8的RGB565屏幕,正常看的话是一块正方形屏幕。
RGB565一个像素占用两个字节,映射到内存中的话如下:
将字体显示到屏幕上
字体文件
有16x16 像素 GB2312 中文字符和 8X16像素 的ASCII 英文字符两种
aimi_board/display_driver/font at main · observerkei/aimi_board · GitHub
字体处理方式
代码如下: aimi_board/display_driver/font_bitmap.cpp at main · observerkei/aimi_board · GitHub
通过网络上的字体制作软件,可以把字体文件(比如ttf后缀的文件)经过点阵字处理后,按照比特位依次写入到内存中。
假设把字符0
处理成4x4点阵字的话,显示效果类似如下:
比如占用4x4像素,每个像素用1比特表示的话,单个字体可以以如下方式进行映射:
4x4个像素占用一个字体, 映射到文件中后,编排方式如下:
出于字体保存方便, GB2312的中文字符从 0xa0
开始逐字写入字体文件(字体文件),也就是第一个存放的字对应的编码是 0xa0开始, 而每个16x16中文字体点阵信息是对应占用32个字节,第二个字则是从0xc0
开始,后面的字体依次按照编码计算即可。
而ASCII字体文件(字体文件)的英文字符则是和编码一致,从0x0
开始,也就是第一个ASCII字符对应的编码就是0, 每个8x16 ASCII字体占用16字节,第二个ASCII字符则从 0x10
开始,后面的字体依次按照编码计算即可。
字体编码的转换
因为中文字体是GB2312的编码,当输入不是GB2312编码的时候,需要先转换为GB2312编码然后才能正常进行显示。
考虑到iconv库有标准的编码转换,因此示例代码如下:
抽象出视图
代码: aimi_board/display_driver/display.cpp at main · observerkei/aimi_board (github.com)
出于性能方面考虑,逐步打点的话,明显会进行多次系统调用,进而影响调用性能,因此抽象出一个视图,然后在视图写完成后,再把显示的内容拷贝到framebuffer中,这样的话可以减少调用次数,进而提升性能,并且因为抽象出了多个视图,因此不同位置的视窗互相堆叠也是可以实现的。
视图view相对于显示屏display的显示关系如下:
结构对应如下:
实现显示功能对应的接口设计
CPP接口的Python化封装
有的项目使用Python开发的话,依赖于公共库,可以降低维护成本, 这里使用了 ctypes 来进行Python接口封装,在Python中定义了C的结构体封装、Python调用C接口的时候,进行结构体传参的处理,在Python中如何修改从C中读取到数据,函数如何调用等。
又因为CPP可以导出C接口,因此同样也支持CPP接口封装。
这个是官方的使用文档: https://docs.python.org/3/library/ctypes.html
我这里提供具体的使用案例。
具体代码如下: aimi_board/display_driver/init.py at main · observerkei/aimi_board (github.com)
Python中定义C的基础类型
比如定义 uint16
结构时,操作如下:
Python中定义C的结构体
Python中封装C接口
具体有以下步骤即可完成配置
- 加载编译好的C的SO文件(CPP可以导出C接口)
- 配置
argtypes
- 配置
restype
这里以一些相对复杂的接口来示例:
封装成Python接口的话,操作如下:
POINTER(c_char)
表示 c_char
类型的指针
argtypes
表示配置调用参数类型
restype
表示配置返回值类型
这样外部就能通过这样方式进行C函数调用:
实机视频
_____