从STM32采集到云端识别:SenseVoice-Small在嵌入式音频处理链中的应用
本文介绍了如何利用星图GPU平台,自动化部署sensevoice-small-语音识别-onnx模型(带量化后)镜像,构建嵌入式语音识别系统。该方案将STM32采集的音频数据发送至云端,由SenseVoice-Small模型完成高精度语音转文字,典型应用于智能家居的语音指令识别与控制场景。
从STM32采集到云端识别:SenseVoice-Small在嵌入式音频处理链中的应用
最近在做一个智能家居的原型,需要让一个简单的嵌入式设备能“听懂”人说话。手头正好有一块经典的STM32F103C8T6最小系统板和一个麦克风模块,但问题来了:STM32的计算能力有限,直接在上面跑复杂的语音识别模型几乎不可能。传统的离线方案要么识别率低,要么词汇量小,很难满足实际需求。
于是,我把目光投向了云端。现在的大语言模型和语音识别模型能力越来越强,如果能将嵌入式设备采集的音频实时送到云端处理,再把识别结果返回来,岂不是两全其美?这听起来像是一个复杂的系统集成问题,涉及到硬件采集、网络传输和云端服务调用。经过一番折腾,我成功搭建了一套从端到云的完整链路:用STM32采集音频,通过内网穿透技术将音频流发送到部署在星图GPU服务器上的SenseVoice-Small语音识别服务,最后把识别出的文字显示出来。整个过程虽然涉及多个环节,但每一步拆解开来都挺有意思的。下面我就把这个从零搭建的过程和其中踩过的坑,跟大家详细分享一下。
1. 场景与方案总览
我们首先得想清楚,这个方案到底要解决什么问题。在很多物联网和嵌入式场景里,设备需要具备语音交互能力,比如智能开关、语音记录仪或者带语音指令的工业控制器。这些设备通常对成本敏感,主控芯片(比如STM32)的资源非常有限,内存可能只有几十KB,闪存几百KB,根本装不下动辄几百MB的现代语音识别模型。
所以,一个很自然的思路就是把“重计算”的任务卸载到云端。设备只负责它最擅长的事情:可靠地采集音频信号。云端则利用强大的GPU算力和大模型,完成高精度的语音转文字。这个架构的核心优势在于,嵌入式端极其轻量,成本可控;云端能力可以随时升级,识别准确率和词汇量都能得到保障。
我选择的云端模型是SenseVoice-Small。它是一个专注于语音识别的模型,在中文场景下表现不错,大小相对适中,非常适合部署在云服务上进行实时推理。整个方案的数据流可以概括为以下几步:
- 音频采集:STM32通过I2S接口驱动麦克风,以固定的采样率(如16kHz)录制音频。
- 数据预处理与发送:STM32将采集到的PCM音频数据打包,通过串口发送给连接在同一个局域网内的“网关”设备(比如一台树莓派或运行了特定软件的电脑)。
- 网络穿透与转发:“网关”设备利用内网穿透工具,将收到的音频数据流转发到公网上的星图GPU服务器。
- 云端识别:星图服务器上部署的SenseVoice-Small服务接收音频流,实时进行识别,并将文本结果通过原路返回。
- 结果展示:返回的文本最终被“网关”设备接收,并可以通过串口传回STM32显示在屏幕上,或者直接由“网关”进行后续处理。
这个链条听起来环节不少,但每个环节都有比较成熟的技术方案可以选用,组合起来就能实现一个功能完整的嵌入式语音识别系统。
2. 嵌入式端:STM32的音频采集与发送
硬件部分的核心是STM32F103C8T6,也就是大家常说的“蓝板”或“最小系统板”。它价格便宜,资源够用,有基本的定时器、DMA和通信接口,非常适合做这种数据采集和转发的前端。
2.1 硬件连接与配置
我用的麦克风模块是常见的INMP441,这是一个数字麦克风,通过I2S接口输出数据,比模拟麦克风+ADC的方案更简单,音质也更好。接线很简单:
- INMP441的SCK 接 STM32的PB13(SPI2_SCK,复用为I2S2_CK)
- INMP441的WS 接 STM32的PB12(SPI2_NSS,复用为I2S2_WS)
- INMP441的SD 接 STM32的PB15(SPI2_MOSI,复用为I2S2_SD)
- INMP441的L/R 接地(选择左声道)
- VCC和GND 接3.3V和地。
在STM32CubeMX里配置起来也很直观。启用I2S2,模式设为“主接收”,数据格式为16位,标准Philips格式。采样率我设置为16kHz,这是语音识别的常用采样率。关键是要启用DMA,让I2S接收的数据自动存放到我们指定的内存数组中,这样不占用CPU,可以实现稳定不间断的录音。
配置好一个定时器,比如每500ms触发一次中断。在中断服务函数里,我们不是去处理音频数据,而是设置一个标志位,告诉主循环:“已经录了500ms的数据了,可以准备发送了”。
2.2 数据打包与串口发送
主循环里,我们不断检查这个“发送标志”。一旦标志置位,就把DMA循环缓冲区里这500ms的音频数据(16kHz * 16bit * 0.5s = 16000字节)读取出来。直接发送16000字节的原始数据可能不太可靠,我们需要加一个简单的帧头。
我的做法是定义一个小的数据包结构:[帧头0xAA] [帧头0x55] [数据长度高字节] [数据长度低字节] [音频数据...] [校验和]。校验和可以用所有音频数据字节的简单累加和。STM32通过HAL库的串口发送函数,将这个数据包发送出去。这里我用的是UART1,波特率设置为115200或更高(如921600),以确保传输实时性。
发送完毕后,清零标志,等待下一个500ms的周期。这样,STM32端就形成了一个稳定的音频流输出。完整的采集发送核心代码如下:
// 定义音频缓冲区
#define AUDIO_BUFFER_SIZE 16000 // 500ms @ 16kHz, 16bit
uint16_t pcm_buffer[AUDIO_BUFFER_SIZE];
volatile uint8_t send_ready_flag = 0;
// 定时器中断回调函数(每500ms触发一次)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2_Instance) { // 假设使用TIM2
send_ready_flag = 1;
}
}
// 主循环中的处理部分
while (1) {
if (send_ready_flag) {
send_ready_flag = 0;
// 1. 计算当前DMA缓冲区中500ms数据的起始位置(略,需根据DMA环形缓冲处理)
// 2. 将数据从DMA缓冲区复制到pcm_buffer
// 3. 计算校验和
uint8_t checksum = 0;
for (int i = 0; i < AUDIO_BUFFER_SIZE; i++) {
checksum += (uint8_t)(pcm_buffer[i] & 0xFF);
checksum += (uint8_t)((pcm_buffer[i] >> 8) & 0xFF);
}
// 4. 通过串口发送数据包
uint8_t header[] = {0xAA, 0x55, (AUDIO_BUFFER_SIZE >> 8) & 0xFF, AUDIO_BUFFER_SIZE & 0xFF};
HAL_UART_Transmit(&huart1, header, 4, HAL_MAX_DELAY);
HAL_UART_Transmit(&huart1, (uint8_t*)pcm_buffer, AUDIO_BUFFER_SIZE * 2, HAL_MAX_DELAY); // 16bit数据,故*2
HAL_UART_Transmit(&huart1, &checksum, 1, HAL_MAX_DELAY);
// 可以点亮一个LED指示发送状态
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
// ... 其他任务
}
3. 网关与网络穿透:打通内网到公云
STM32发出的音频数据通过串口到了哪里?这里需要一台“网关”设备。我用了一台闲置的旧笔记本,它运行一个Python脚本,负责三件事:读取串口数据、转发到云端、接收并显示结果。
3.1 串口数据接收与解析
Python脚本使用pyserial库读取串口数据。关键是要正确解析我们自定义的数据包格式,找到帧头,读取长度,然后收取对应长度的音频数据,最后验证校验和。解析成功后,我们得到的就是原始的16位、16kHz的PCM数据。通常云端API需要的是WAV格式或者base64编码的PCM,所以我们需要做一步转换,比如将数据封装成一个WAV文件在内存中,或者直接进行base64编码。
import serial
import struct
import threading
ser = serial.Serial('COM3', 921600) # 根据实际情况修改串口号和波特率
audio_buffer = bytearray()
def parse_audio_packet(data):
# 简单解析,寻找帧头0xAA 0x55
start = 0
while start < len(data) - 5:
if data[start] == 0xAA and data[start+1] == 0x55:
length = (data[start+2] << 8) | data[start+3]
if start + 5 + length < len(data):
audio_data = data[start+4: start+4+length]
checksum = data[start+4+length]
# 计算校验和并验证(略)
# 验证通过,返回有效的audio_data
return audio_data, length
start += 1
return None, 0
def read_from_serial():
while True:
if ser.in_waiting:
chunk = ser.read(ser.in_waiting)
audio_buffer.extend(chunk)
# 尝试解析数据包
audio_data, length = parse_audio_packet(audio_buffer)
if audio_data:
# 处理有效的音频数据,例如放入发送队列
send_queue.put(audio_data)
# 从缓冲区中移除已处理的数据
del audio_buffer[:4+2+length+1] # 移除帧头+长度+数据+校验和
# 启动串口读取线程
serial_thread = threading.Thread(target=read_from_serial)
serial_thread.daemon = True
serial_thread.start()
3.2 内网穿透与云端连接
我的笔记本和STM32在家庭或公司局域网内,没有公网IP。要让星图GPU服务器能主动连接到我笔记本上的服务是不可能的。因此,需要“内网穿透”。这里我选用了一种反向代理的思路:在星图服务器上运行一个服务端,我的笔记本作为客户端主动连接到这个公网服务器,并建立一个持久的隧道。之后,所有数据都通过这个隧道进行交换。
具体实现上,可以使用像frp或ngrok这样的成熟工具。我在星图服务器上部署了frps(服务端),在笔记本上运行frpc(客户端),配置它将笔记本本地的一个TCP端口(比如localhost:8000)映射到服务器的一个公网端口。这样,我笔记本上运行的Python脚本,只需要把音频数据发送到localhost:8000,frpc就会自动通过隧道转发给公网服务器的frps,再由frps转发给同样运行在星图服务器上的SenseVoice-Small服务。反向的文本结果也是通过这条路径返回。
这一步是网络连通的关键,可能需要一些网络知识来配置,但工具本身的使用文档都很详细。
4. 云端服务:SenseVoice-Small的部署与调用
云端是整个系统的大脑,负责最核心的语音识别任务。
4.1 服务部署
在星图GPU服务器上,我们可以利用其预置的镜像环境快速部署SenseVoice-Small。SenseVoice-Small通常提供了HTTP或WebSocket接口的API服务。部署过程可能包括拉取模型镜像、配置模型路径、设置服务端口等。确保服务启动后,可以通过http://服务器IP:端口进行访问。
4.2 音频发送与结果接收
笔记本上的Python脚本在解析出音频数据后,需要将其发送给云端服务。假设SenseVoice-Small服务提供了一个接收音频二进制流并返回JSON格式识别结果的HTTP API端点。
脚本需要将PCM数据封装成API要求的格式(例如,放在HTTP POST请求的body中,并设置正确的Content-Type,如audio/wav或application/octet-stream)。然后,通过我们建立的内网穿透隧道地址(localhost:8000)向这个API发送请求。
import requests
import queue
import threading
import base64
send_queue = queue.Queue()
result_queue = queue.Queue()
def send_to_cloud():
while True:
audio_data = send_queue.get() # 从队列获取音频数据
# 假设API需要base64编码的PCM
audio_b64 = base64.b64encode(audio_data).decode('utf-8')
payload = {"audio": audio_b64, "sample_rate": 16000}
try:
# cloud_service_url 是通过内网穿透映射后的地址,例如 http://localhost:8000/asr
response = requests.post(cloud_service_url, json=payload, timeout=10)
if response.status_code == 200:
result = response.json()
text = result.get('text', '')
if text:
result_queue.put(text)
print(f"识别结果: {text}")
# 也可以通过串口发回给STM32
# ser.write(f"TEXT:{text}\n".encode())
except Exception as e:
print(f"发送到云端失败: {e}")
# 启动发送线程
cloud_thread = threading.Thread(target=send_to_cloud)
cloud_thread.daemon = True
cloud_thread.start()
5. 整合测试与效果体验
将STM32、网关笔记本、星图服务器全部连接起来,上电运行。对着麦克风说话,观察笔记本上的日志输出。
实际测试中,从说话到在笔记本上看到识别文字,延迟大概在1到2秒左右。这个延迟主要来自几个方面:500ms的音频打包周期、网络传输时间(尤其是经过内网穿透)、云端模型推理时间。对于很多非实时性要求极高的语音指令场景(比如“打开客厅灯”),这个延迟是可以接受的。
识别准确率方面,SenseVoice-Small在安静环境下的中文普通话识别效果相当不错,日常短句基本能准确转写。当然,如果环境嘈杂,或者有很强的口音,准确率会下降,这是所有语音识别系统面临的共同挑战。
整个系统跑通后,感觉就像给那个小小的STM32板子装上了“云大脑”。它本身还是那个简单的单片机,但能力边界通过云端协同被极大地扩展了。你可以基于返回的文本结果,让STM32控制继电器、点亮不同的LED,或者通过屏幕显示,实现各种有趣的交互。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐
所有评论(0)