语音识别模块驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
指纹灯能亮了。今天接离线语音。对模块说话,识别结果从串口吐回来,同样占用一根口。


排针上再换一颗模组,软件上还是 UART。这次对面是 ASR 私有帧:模组自己听麦克风,识别到命令之后往串口推一包。板上读 /dev/ttyS7,不是 OpenHarmony 的语音 Kit,不是片上 DSP,不要去翻 @ohos.ai。
先把错门焊死:
- 先唤醒,再命令。 唤醒词、命令词都以模块出厂为准。跳过唤醒,对着麦克风喊命令,帧不会来。
- 识别结果帧长这样:
A5 FC ... FB。当data[4]=0xA0且data[5]=0x91时,data[7]才是动作码。 - 动作码:
02开灯、03关灯、04播音、05停。应用把动作名打出来就够。播本地音频不是这篇范围。 - 必须同时选对 tty 和 波特率。常见
/dev/ttyS7@ 115200 8N1。写成 9600,永远拼不出A5FC。 - 贴近麦克风。远了只有启动垃圾,没有识别帧。
产品 rk3568_evb。用户态 32 位 armv7 musl。不要占用 /dev/ttyS2(console,1500000)。和 GPS、BLE、LoRa、指纹同组排针,一次只留一颗。
1. 现象
打开串口,页面空白。有人对着空气喊「开灯」,什么都不来,开始改 termios。有人把 GPS 的 9600 拿来,RX 全是噪声,hex 里永远没有 A5FC。有人插着 LoRa 再插语音,两颗 TX 短在一起,谁都不说话。
另一种:hex 里能看到 A5FC,页面却显示「未识别」。查代码,走了文本模式,0xA5 被当成非 ASCII 丢掉,看起来像没收到。
根因按层拆:
| 层 | 看什么 |
|---|---|
| 口 | 排针接到哪颗 tty,常见 `ttyS7` |
| 波特率 | **115200**。对不上就没有 `A5FC` |
| 唤醒 | 出厂唤醒词说过没有。没唤醒,命令帧不来 |
| 帧 | 锚定 `A5FC`,再看 `data[4]` / `data[5]` / `data[7]` |
| 读法 | 必须 hex。文本模式会吞 `A5` |
模组上电会先吐一串启动信息。那一串常常 不是 A5FC。把它打印出来,正好用来确认「UART 活着、波特率对不对」。波特率错,前若干字节是散乱值;波特率对,能看到模组自己的启动模式,然后再等识别帧。
2. 帧怎么拆
识别结果帧:头 A5 FC,尾 FB。中间若干字节。按下标(从 0):
data[0] = A5
data[1] = FC
data[2] data[3] 长度或保留,各家不同,不要猜
data[4] = A0 这一组才是「识别结果」
data[5] = 91
data[6] 先忽略
data[7] 动作码
...
data[n] = FB
只在 data[4]==0xA0 且 data[5]==0x91 时解释 data[7]:
| data[7] | 动作名 |
|---|---|
| `02` | 开灯 |
| `03` | 关灯 |
| `04` | 播音 |
| `05` | 停 |
其它 data[4]/data[5] 组合:打印整帧 hex,不要当动作。模组还可能推版本、心跳、错误。应用只认这一组。
命令词、唤醒词写在模组出厂说明书里。页面不要写死某句口号,也不要写商标。提示一句「以模块出厂唤醒词为准」即可。词表烧在模组里,改应用改不了词。
播音 / 停 这两个动作码,意思是模组认为用户说了「播」和「停」。板子要不要真的播一段本地音频,是音频通路的事,这里只把动作名打出来。点到为止。
hex 字符串里下标怎么算:找到 A5FC 的字符下标 i,头占 4 个字符:
d4 = substr(i+8, 2) // 字节[4]
d5 = substr(i+10, 2) // 字节[5]
d7 = substr(i+14, 2) // 字节[7]
必须先 toUpperCase() 再比 A0 / 91 / 02。
3. 口和权限
hdc shell "ls -l /dev/ttyS7"
hdc shell "cat /proc/tty/driver/serial"
hdc shell "chmod 0666 /dev/ttyS7"
期望 crw,serial 表有 7:。假文件 tcgetattr 报 Not a tty。删掉。
板级:
device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts
&uart7 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart7m1_xfer>;
};
开机放权:
device/board/rk/rk3568_evb/board_perm/board-perm.sh
init.rk30board.cfg 里 once = 1。hdc 能开不代表 HAP 能开。
不要占用 /dev/ttyS2。同一组排针一次一颗:测语音时把 GPS / LoRa / BLE / 指纹拔掉。
模组供电和麦克风。没供电,UART 打不开灯;有供电没对着麦,只有启动垃圾没有 A091。
4. 完整 C
O_NONBLOCK + poll。关 CRTSCTS。不要 O_NDELAY + select,不要纯阻塞 + VTIME。循环读 hex,找 A5FC,解析 d4/d5/d7。收到不是 A5FC 开头的块,打印前若干字节,便于对波特率。
uart_asr.c:
/* uart_asr.c — OpenHarmony 4.1 用户态,32 位 musl
* uart_asr /dev/ttyS7 默认 115200,循环听
* uart_asr /dev/ttyS7 115200
*
* 先唤醒再命令。贴近麦克风。以模块出厂唤醒词为准。
*/
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
static speed_t baud_const(int b)
{
switch (b) {
case 9600: return B9600;
case 57600: return B57600;
case 115200: return B115200;
default: return B0;
}
}
static void hexdump_n(const unsigned char *b, int n)
{
int i;
for (i = 0; i < n; i++)
printf("%02X", b[i]);
}
static const char *action_name(unsigned char d7)
{
switch (d7) {
case 0x02: return "开灯";
case 0x03: return "关灯";
case 0x04: return "播音";
case 0x05: return "停";
default: return NULL;
}
}
static int uart_open(const char *path, int baud)
{
speed_t sp = baud_const(baud);
if (sp == B0) {
fprintf(stderr, "波特率 %d 不在表里\n", baud);
return -1;
}
int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd < 0) {
fprintf(stderr, "open %s: %s\n", path, strerror(errno));
return -1;
}
struct termios t;
memset(&t, 0, sizeof(t));
if (tcgetattr(fd, &t) != 0) {
fprintf(stderr, "tcgetattr: %s(假节点?)\n", strerror(errno));
close(fd);
return -1;
}
cfsetispeed(&t, sp);
cfsetospeed(&t, sp);
t.c_cflag &= ~CSIZE;
t.c_cflag |= CS8 | CLOCAL | CREAD;
t.c_cflag &= ~PARENB;
t.c_cflag &= ~CSTOPB;
t.c_cflag &= ~CRTSCTS;
t.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
t.c_iflag &= ~(IXON | IXOFF | IXANY | INLCR | ICRNL);
t.c_oflag &= ~OPOST;
t.c_cc[VMIN] = 0;
t.c_cc[VTIME] = 0;
tcflush(fd, TCIOFLUSH);
if (tcsetattr(fd, TCSANOW, &t) != 0) {
fprintf(stderr, "tcsetattr: %s\n", strerror(errno));
close(fd);
return -1;
}
return fd;
}
static int find_a5fc(const unsigned char *b, int n)
{
int i;
for (i = 0; i + 2 <= n; i++) {
if (b[i] == 0xA5 && b[i + 1] == 0xFC)
return i;
}
return -1;
}
static int find_fb(const unsigned char *b, int n, int from)
{
int i;
for (i = from; i < n; i++) {
if (b[i] == 0xFB)
return i;
}
return -1;
}
static void handle_frame(const unsigned char *f, int n)
{
printf("FRAME %d: ", n);
hexdump_n(f, n);
printf("\n");
if (n < 8) {
printf(" 太短,不够取 data[4]/[5]/[7]\n");
return;
}
unsigned char d4 = f[4];
unsigned char d5 = f[5];
unsigned char d7 = f[7];
printf(" d4=%02X d5=%02X d7=%02X\n", d4, d5, d7);
if (d4 == 0xA0 && d5 == 0x91) {
const char *name = action_name(d7);
if (name)
printf(" 动作: %s\n", name);
else
printf(" A091,动作码 %02X 不在 02/03/04/05\n", d7);
} else {
printf(" 不是 A091 识别结果,忽略动作码\n");
}
}
static void consume(unsigned char *acc, int *accn)
{
for (;;) {
int n = *accn;
int h = find_a5fc(acc, n);
if (h < 0) {
if (n > 0) {
int dump = n < 16 ? n : 16;
printf("非A5FC 前%d字节: ", dump);
hexdump_n(acc, dump);
printf(" (对波特率:115200 才能拼出 A5FC)\n");
*accn = 0;
}
return;
}
if (h > 0) {
int dump = h < 16 ? h : 16;
printf("A5FC 前垃圾 %d 字节, 前%d: ", h, dump);
hexdump_n(acc, dump);
printf("\n");
memmove(acc, acc + h, (size_t)(n - h));
*accn = n - h;
continue;
}
int t = find_fb(acc, n, 2);
if (t < 0)
return;
handle_frame(acc, t + 1);
int left = n - (t + 1);
if (left > 0)
memmove(acc, acc + t + 1, (size_t)left);
*accn = left;
}
}
int main(int argc, char **argv)
{
const char *dev = "/dev/ttyS7";
int baud = 115200;
if (argc >= 2)
dev = argv[1];
if (argc >= 3)
baud = atoi(argv[2]);
int fd = uart_open(dev, baud);
if (fd < 0)
return 1;
printf("听 %s @%d。先唤醒再命令。贴近麦克风。Ctrl-C 退出。\n",
dev, baud);
unsigned char acc[1024];
int accn = 0;
for (;;) {
struct pollfd p;
p.fd = fd;
p.events = POLLIN;
p.revents = 0;
int pr = poll(&p, 1, 500);
if (pr <= 0)
continue;
if (!(p.revents & POLLIN))
continue;
unsigned char buf[256];
int r = (int)read(fd, buf, sizeof(buf));
if (r <= 0)
continue;
if (accn + r > (int)sizeof(acc)) {
fprintf(stderr, "缓冲溢出,丢弃旧数据\n");
accn = 0;
}
memcpy(acc + accn, buf, (size_t)r);
accn += r;
consume(acc, &accn);
}
}
32 位 musl:
OHOS=$HOME/code/opcode/OpenHarmony
CLANG=$OHOS/prebuilts/clang/ohos/linux-x86_64/llvm/bin/clang
SYSROOT=$OHOS/prebuilts/ohos-sdk/linux/11/native/sysroot
$CLANG --target=arm-linux-ohos --sysroot=$SYSROOT \
-O2 -Wall \
-Wl,--dynamic-linker=/system/lib/ld-musl-arm.so.1 \
-o uart_asr uart_asr.c
file uart_asr
期望 ELF 32-bit LSB executable, ARM,interpreter /system/lib/ld-musl-arm.so.1。
hdc file send uart_asr /data/local/tmp/uart_asr
hdc shell "chmod 0755 /data/local/tmp/uart_asr"
hdc shell "chmod 0666 /dev/ttyS7"
hdc shell "/data/local/tmp/uart_asr /dev/ttyS7 115200"
打开监听后,先按出厂唤醒词说一句,再给命令。贴近麦克风。C 应打出 动作: 开灯 这类名字。
5. 现场怎么跑
只插这一颗 UART 模组。打开工具。上电那一瞬间若打出 非A5FC 前N字节: ...,这是好事:UART 在吐启动信息,波特率多半对。完全没有字节:口、供电、TX/RX 交叉、0666。
然后唤醒。模组一般有提示音或灯。再给命令。期望类似:
FRAME 12: A5FC....A091..02...FB
d4=A0 d5=91 d7=02
动作: 开灯
d7 换 03 / 04 / 05,动作名跟着变。
只有启动垃圾、永远没有 A5FC:
- 没唤醒。以模块出厂唤醒词为准,先唤醒。
- 离麦克风太远,或麦被外壳挡住。
- 命令词不是出厂那套。改应用改不了词表。
- 波特率其实不是 115200。对照启动垃圾:若是可辨认的 ASCII 横幅,口对了;若是散乱
FF 00 81,换 9600 试一次——以能刷出A5FC的那档为准,常见仍是 115200。
有 A5FC 但 d4/d5 不是 A091:那是别的推送,打印 hex 即可,不要当动作。
有 A091 页面无动作:文本模式吞了 A5,或 hex 下标按字节算成了按字符算(差一倍)。
TX 这边基本不用发。ASR 是模组主动推。你往串口写 AT 或任意字符串,有的模组会进配置态,识别就停了。通路验证阶段不要写。
6. 和同排针其它模组
| GPS | LoRa | BLE | 指纹 | 语音 | |
|---|---|---|---|---|---|
| 常见口 | ttyS7 | ttyS7 | ttyS7 | ttyS7/8/3 | ttyS7 |
| 波特率 | 9600 | 9600 | 115200 | 57600 | **115200** |
| 方向 | 模组→板 | 双工透传 | 双工透传 | 板发命令 | **模组主动推** |
| 载荷 | ASCII | hex 裸字节 | ASCII | 私有帧 | `A5FC` 帧 |
口可以共用,参数不能共用。拔插一次一个。插着 GPS 再听语音,NMEA 的 $ 会冒充「非 A5FC 垃圾」,把缓冲打满,识别帧夹在缝里。
不要占用 ttyS2。
7. 现场对照
9600。 上午 GPS / LoRa,下午语音,第二个参数没改。没有 A5FC。默认记 115200。
没唤醒。 直接喊命令。帧不来。出厂唤醒词先说一遍。
离得远。 启动垃圾有,识别没有。贴近。
文本读。 A5 被过滤。必须 hex。
两颗并排。 TX 短路。拔掉另一颗。
CRTSCTS。 CTS 悬空。关掉。
select / VTIME。 dw-apb-uart 这两条路是假的。poll。
写进串口。 误进配置态。听的阶段不要 write。
32 位。 file 看 interpreter。
权限。 chmod 0666。开机脚本。
播本地音频失败。 不是这篇。这里只要求动作名打出来。音频通路另说。
8. 通不了时的顺序
1. ls -l /dev/ttyS7 字符设备?0666?
2. cat /proc/tty/driver/serial 有 7: ?
3. 排针只留这一颗
4. uart_asr /dev/ttyS7 115200 有没有启动垃圾?
5. 没有字节:供电、TX/RX、不是 ttyS2、换 ttyS8 试一次
6. 有垃圾无 A5FC:唤醒、贴近、词表、对照 9600
7. 有 A5FC 无动作名:是不是 A091?d7 是不是 02..05?
8. 工具有、应用无:chmod、hex 读、32 位 so
第 4 步有启动垃圾,UART 已过。没过不要谈唤醒词。过了还没有 A091,是麦克风和词表,不是 poll。
9. 什么叫过
| 项 | 通过 |
|---|---|
| 打开 | `/dev/ttyS7` `c` 节点,115200 |
| UART | 上电能打出非空 hex(启动垃圾也算) |
| 识别 | 唤醒 + 命令后出现 `A5FC`,且 `d4=A0 d5=91` |
| 动作 | `d7` 为 02/03/04/05 时打印开灯/关灯/播音/停 |
不要用「能 open」当过。不要用系统语音助手当过。不要要求板子真的亮灯或播歌——那是别的口。
路径
| 用途 | 路径 |
|---|---|
| DTS uart7 | `device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts` |
| 放权 | `device/board/rk/rk3568_evb/board_perm/board-perm.sh` |
| 工具 | `uart_asr.c` → `/data/local/tmp/uart_asr` |
| 节点 | `/dev/ttyS7` |
| 勿占 | `/dev/ttyS2`,1500000 |
系列第 49 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10
更多推荐



所有评论(0)