指纹灯能亮了。今天接离线语音。对模块说话,识别结果从串口吐回来,同样占用一根口。

排针上再换一颗模组,软件上还是 UART。这次对面是 ASR 私有帧:模组自己听麦克风,识别到命令之后往串口推一包。板上读 /dev/ttyS7,不是 OpenHarmony 的语音 Kit,不是片上 DSP,不要去翻 @ohos.ai。

先把错门焊死:

  1. 先唤醒,再命令。 唤醒词、命令词都以模块出厂为准。跳过唤醒,对着麦克风喊命令,帧不会来。
  2. 识别结果帧长这样:A5 FC ... FB。当 data[4]=0xA0 且 data[5]=0x91 时,data[7] 才是动作码。
  3. 动作码:02 开灯、03 关灯、04 播音、05 停。应用把动作名打出来就够。播本地音频不是这篇范围。
  4. 必须同时选对 tty 和 波特率。常见 /dev/ttyS7 @ 115200 8N1。写成 9600,永远拼不出 A5FC。
  5. 贴近麦克风。远了只有启动垃圾,没有识别帧。

产品 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. 和同排针其它模组

GPSLoRaBLE指纹语音
常见口ttyS7ttyS7ttyS7ttyS7/8/3ttyS7
波特率9600960011520057600**115200**
方向模组→板双工透传双工透传板发命令**模组主动推**
载荷ASCIIhex 裸字节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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐