二进制帧会拆了。今天换指纹模组:私有协议,先发亮灯确认通信,再录入、比对。

排针上又是一颗 UART 模组。这次对面不是 GPS 的文本行,也不是 LoRa 的裸载荷,是一套 私有二进制帧。常见 57600 8N1。tty 可能是 ttyS7、ttyS8、ttyS3,以亮灯为准,不以丝印为准。

先把错门焊死:

  1. 这不是 SoC 安全元件,不是系统设置里的生物识别。外置模组,UART 进,算法在模组里。
  2. 灯都没亮,不要谈录入、比对、算法。 通信还没立住。
  3. 流程必须按这个顺序:连接 → 亮灯(确认通信)→ 录入 → 比对。跳步的人会把残留应答当成成功。
  4. 写前必须 tcflush(TCIFLUSH)。模组会主动上报,缓冲区里的旧字节会冲掉你正在等的应答,表现为「亮灯假成功」。

产品 rk3568_evb。用户态 32 位 armv7 musl。不要占用 /dev/ttyS2(console,1500000)。和 GPS、BLE、LoRa、语音同组排针时,一次只留一颗。

1. 现象

常见一天:

打开 /dev/ttyS7 @ 115200(BLE 的习惯),发亮灯帧,模组没反应。改成 9600(GPS / LoRa 的习惯),还是没有。丝印写着 UART7,灯就是不亮;换到 ttyS8 同一条命令,灯亮了。

或者灯亮过一次,再发录入,屏幕上打印「成功」,手指根本没放上去。查 hex,应答里夹着上一次亮灯的尾巴,解析器在错的位置取了状态字节。

或者录入回了状态 0F,页面当成通过。0F 是「已存在」,不是成功。成功是 0E。

根因按层拆:

层看什么
口哪颗 tty 真正接到这颗模组的 TX/RX,以灯为准
波特率**57600**,不是 9600,也不是 115200
帧8 字节包头,后面是长度、命令、载荷、校验
时序写前冲掉 RX,写后 `poll` 等完整应答
状态锚定包头再取标记和状态,不要对整段 hex 做字符串包含

2. 包头和四条现成帧

包头 8 字节:

F1 1F E2 2E B6 6B A8 8A

下面四条是完整帧,hex 无空格,按字节原样 write。不要拆开当 AT,不要加 \r\n,不要自己组校验。现场改一个字节,模组常常直接不理。

亮灯(cmd 0x81):

F11FE22EB66BA88A000C8100000000020F030364003253

录入(cmd 0x82):

F11FE22EB66BA88A000B820000000001180003FFFFE6

比对(cmd 0x86):

F11FE22EB66BA88A000786000000000123DC

删除(cmd 0x83):

F11FE22EB66BA88A000A83000000000136010001C7

解析应答时:

  • 在收到的字节里扫描 f11fe22e(包头前四字节足够锚定)。
  • 指令标记在包头起始 +15 字节处(若把整段打成 hex 字符串,偏移 +30)。
  • 状态在包头起始 +20 字节处(hex 偏移 +40)。

录入应答的标记是 0118。状态:

状态含义能不能当成功
`0E`成功能
`0F`已存在**不能**

比对应答的标记是 0123。同样先对上标记,再看状态。标记对不上,说明你抓到的是别的命令的应答或半截帧,不要用里面的状态字节下结论。

亮灯这条的目的只有一个:模组灯亮,UART 通。灯都没亮,后面的录入比对全部作废。

手指平放 2–3 秒。抬太快,模组采不到图,应答会失败或超时。干、湿、脏都会让录入失败,那是传感器侧,不是 termios。


3. 口、权限、设备树

同一组 UART 排针一次一颗。测指纹时把 GPS / BLE / LoRa / 语音拔掉。

hdc shell "ls -l /dev/ttyS7 /dev/ttyS8 /dev/ttyS3"
hdc shell "cat /proc/tty/driver/serial"
hdc shell "chmod 0666 /dev/ttyS7 /dev/ttyS8 /dev/ttyS3"

期望是字符设备 crw。第一位是 -,假文件,tcgetattr 报 Not a tty。删掉再让驱动创节点。

板级:

device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts

uart7 / uart8 / uart3 需要 status = "okay",pinctrl 接到排针那组 xfer。应用不认球号,只认 /dev/ttySN。开机放权放在:

device/board/rk/rk3568_evb/board_perm/board-perm.sh

ohos.boot.hardware 为 rk30board 时,init.rk30board.cfg 里 once = 1 的服务会跑这份脚本。重启后 0666 还在,HAP 才能打开。hdc 是 root,能开不代表应用能开。某路 tty 若漏写进 perm,工具行能跑、应用 Permission denied。

不要占用 /dev/ttyS2。

无响应时的硬件顺序:换 tty、查供电、先断开再连以同步。模组上电瞬间会吐主动上报,这时发亮灯,应答和上报叠在一起。拔掉供电或排针、等灯灭、再插、立刻 tcflush 再发,比改代码有效。


4. 完整 C

O_NONBLOCK + poll。关 CRTSCTS。不要 O_NDELAY + select,不要纯阻塞 + VTIME。每次 write 之前 tcflush(TCIFLUSH)。

uart_fp.c:

/* uart_fp.c — OpenHarmony 4.1 用户态,32 位 musl
 *   uart_fp /dev/ttyS7              默认 57600,发亮灯,打印 hex
 *   uart_fp /dev/ttyS7 light
 *   uart_fp /dev/ttyS7 enroll
 *   uart_fp /dev/ttyS7 match
 *   uart_fp /dev/ttyS7 delete
 */

#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 const char *HEX_LIGHT  =
    "F11FE22EB66BA88A000C8100000000020F030364003253";
static const char *HEX_ENROLL =
    "F11FE22EB66BA88A000B820000000001180003FFFFE6";
static const char *HEX_MATCH  =
    "F11FE22EB66BA88A000786000000000123DC";
static const char *HEX_DELETE =
    "F11FE22EB66BA88A000A83000000000136010001C7";

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 int hexval(int c)
{
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    return -1;
}

static int parse_hex(const char *s, unsigned char *out, int max)
{
    int n = 0;
    const char *p = s;
    while (*p) {
        if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') {
            p++;
            continue;
        }
        int hi = hexval(*p++);
        if (hi < 0 || !*p)
            return -1;
        int lo = hexval(*p++);
        if (lo < 0)
            return -1;
        if (n >= max)
            return -1;
        out[n++] = (unsigned char)((hi << 4) | lo);
    }
    return n;
}

static void hexdump_line(const unsigned char *b, int n)
{
    int i;
    for (i = 0; i < n; i++)
        printf("%02X", b[i]);
    printf("\n");
}

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 uart_read_bytes(int fd, unsigned char *out, int outsz, int timeout_ms)
{
    int total = 0, idle = 0;
    while (idle < timeout_ms && total < outsz) {
        struct pollfd p;
        p.fd = fd;
        p.events = POLLIN;
        p.revents = 0;
        int pr = poll(&p, 1, 100);
        if (pr > 0 && (p.revents & POLLIN)) {
            int r = (int)read(fd, out + total, (size_t)(outsz - total));
            if (r > 0) {
                total += r;
                idle = 0;
                continue;
            }
        }
        idle += 100;
    }
    return total;
}

static int find_hdr(const unsigned char *b, int n)
{
    int i;
    for (i = 0; i + 4 <= n; i++) {
        if (b[i] == 0xF1 && b[i + 1] == 0x1F &&
            b[i + 2] == 0xE2 && b[i + 3] == 0x2E)
            return i;
    }
    return -1;
}

static void parse_reply(const unsigned char *b, int n, unsigned short want_mark)
{
    int off = find_hdr(b, n);
    if (off < 0) {
        printf("未找到包头 F11FE22E。无响应:换 tty、查供电、断开再连。\n");
        return;
    }
    printf("包头偏移 %d\n", off);
    if (off + 21 > n) {
        printf("帧太短,不够取标记/状态。RX 可能被截断,加大等待。\n");
        return;
    }
    /* 标记:包头+15 起两字节。状态:包头+20。hex 则 +30 / +40。 */
    unsigned short mark = (unsigned short)((b[off + 15] << 8) | b[off + 16]);
    unsigned char st = b[off + 20];
    printf("标记 %04X(期望 %04X) 状态 %02X\n", mark, want_mark, st);
    if (want_mark && mark != want_mark) {
        printf("标记对不上,这不是这条命令的应答。不要用这个状态。\n");
        return;
    }
    if (st == 0x0E)
        printf("状态 0E = 成功\n");
    else if (st == 0x0F)
        printf("状态 0F = 已存在,不能当成功\n");
    else
        printf("状态 %02X,按手册查,不要猜\n", st);
}

static int send_cmd(int fd, const char *hex, const char *name,
                    unsigned short want_mark, int wait_ms)
{
    unsigned char tx[64];
    int n = parse_hex(hex, tx, (int)sizeof(tx));
    if (n <= 0) {
        fprintf(stderr, "内部 hex 错\n");
        return -1;
    }
    tcflush(fd, TCIFLUSH);
    ssize_t w = write(fd, tx, (size_t)n);
    if (w < 0) {
        fprintf(stderr, "write: %s\n", strerror(errno));
        return -1;
    }
    printf("TX %s %d 字节: ", name, (int)w);
    hexdump_line(tx, n);
    unsigned char rx[512];
    int r = uart_read_bytes(fd, rx, sizeof(rx), wait_ms);
    printf("RX %d: ", r);
    if (r > 0)
        hexdump_line(rx, r);
    else
        printf("(空)\n");
    parse_reply(rx, r, want_mark);
    return r;
}

int main(int argc, char **argv)
{
    const char *dev = "/dev/ttyS7";
    const char *cmd = "light";
    if (argc >= 2)
        dev = argv[1];
    if (argc >= 3)
        cmd = argv[2];

    int fd = uart_open(dev, 57600);
    if (fd < 0)
        return 1;

    if (strcmp(cmd, "light") == 0) {
        printf("发亮灯。模组灯要亮。灯不亮不要谈录入。\n");
        send_cmd(fd, HEX_LIGHT, "light", 0, 1500);
    } else if (strcmp(cmd, "enroll") == 0) {
        printf("录入:手指平放 2-3 秒。标记 0118,成功=0E,已存在=0F。\n");
        send_cmd(fd, HEX_ENROLL, "enroll", 0x0118, 4000);
    } else if (strcmp(cmd, "match") == 0) {
        printf("比对:同一根手指平放 2-3 秒。标记 0123。\n");
        send_cmd(fd, HEX_MATCH, "match", 0x0123, 4000);
    } else if (strcmp(cmd, "delete") == 0) {
        send_cmd(fd, HEX_DELETE, "delete", 0, 1500);
    } else {
        fprintf(stderr, "用法: %s /dev/ttyS7 [light|enroll|match|delete]\n",
                argv[0]);
        close(fd);
        return 1;
    }
    close(fd);
    return 0;
}

32 位 musl,板上只有 /system/lib/ld-musl-arm.so.1:

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_fp uart_fp.c

file uart_fp

期望 ELF 32-bit LSB executable, ARM,interpreter 是 /system/lib/ld-musl-arm.so.1。aarch64 的产物板上 exec 失败。

hdc file send uart_fp /data/local/tmp/uart_fp
hdc shell "chmod 0755 /data/local/tmp/uart_fp"
hdc shell "chmod 0666 /dev/ttyS7 /dev/ttyS8 /dev/ttyS3"

5. 现场:先亮灯

hdc shell "/data/local/tmp/uart_fp /dev/ttyS7 light"

眼睛看模组。灯亮了,这条 tty 就是对的,UART 通了。灯不亮,换口,不要改帧:

hdc shell "/data/local/tmp/uart_fp /dev/ttyS8 light"
hdc shell "/data/local/tmp/uart_fp /dev/ttyS3 light"

三个都不亮:

  • 供电。量 VCC。3.3 V / 5 V 接反会完全哑巴。
  • 波特率是不是被你改成了 9600 或 115200。工具写死 57600,自己改过源码就对一下。
  • TX/RX 交叉。SoC TX → 模组 RX,SoC RX → 模组 TX。
  • 排针上还有另一颗 UART 模组。拔掉。
  • 写到了 ttyS2。console 乱码,模组灯不动。
  • 模组死在半同步。拔掉再插,上电后再发。

TX 数字必须等于帧长(亮灯 23 字节)。TX 46 说明你把 hex 字符串当 ASCII 发出去了,模组听不懂。

RX 打出一长串、找不到 F11FE22E:波特率不对,或两颗模组抢总线。RX 空但灯亮了:灯命令有的模组不回包,以灯为准。RX 空且灯不亮:字节没进芯片。

「亮灯假成功」:应用显示收到包头,灯其实没亮。典型是 没 tcflush,读到的是上电上报或上一次命令的尾巴。工具每次写前都冲 RX。你自己写 native 也要冲。点亮灯时手指离开窗口,按在窗口上会插入主动上报。


6. 录入和比对

灯亮之后才做这两步。手指平放 2–3 秒,不要点一下就抬。

hdc shell "/data/local/tmp/uart_fp /dev/ttyS7 enroll"

期望能扫到标记 0118。状态 0E 才是录入成功。0F 打印「已存在」,模板库里已经有这根手指,不要当成功,也不要当通信失败。要清库再录,发删除:

hdc shell "/data/local/tmp/uart_fp /dev/ttyS7 delete"
hdc shell "/data/local/tmp/uart_fp /dev/ttyS7 enroll"

比对必须同一根手指:

hdc shell "/data/local/tmp/uart_fp /dev/ttyS7 match"

标记 0123。换一根没录过的手指,应该失败,这恰好证明比对在干活,不是灯命令的回声。

录入等待给了 4 秒,因为手指要放上去。poll 100 ms 一轮,期间来的字节会拼进同一缓冲,再找包头。不要按「一条 write 立刻一条短 read」来,模组采图要时间。

干:打滑。湿:水膜。擦干、平放、保持 2–3 秒,不要滚动。假手指会被拒绝。比对用录入的那一根。


7. ArkTS 状态机(主代码仍是 C)

页面不要一上来就录入。四个状态:idle → opened → lit → enrolled。灯没亮,录入按钮直接拒绝。

type FpStep = 'idle' | 'opened' | 'lit' | 'enrolled';

@State step: FpStep = 'idle';
@State log: string = '先开串口,再亮灯。灯不亮不要录入。';
@State port: string = '/dev/ttyS7';

private hexLight: string =
  'F11FE22EB66BA88A000C8100000000020F030364003253';
private hexEnroll: string =
  'F11FE22EB66BA88A000B820000000001180003FFFFE6';
private hexMatch: string =
  'F11FE22EB66BA88A000786000000000123DC';

openPort() {
  let r: number = board.uartOpen(this.port, 57600);
  this.step = (r === 0) ? 'opened' : 'idle';
  this.log = (r === 0) ? '已打开,下一步亮灯' : '打开失败:tty / chmod / 假节点';
}

doLight() {
  if (this.step === 'idle') {
    this.log = '先打开';
    return;
  }
  let n: number = board.uartWrite(this.hexLight, 1);
  let s: string = board.uartRead(1500, 1);
  let ok: boolean = s.toLowerCase().indexOf('f11fe22e') >= 0;
  this.step = ok ? 'lit' : this.step;
  this.log = 'TX ' + n + ' RX=' + s + (ok ? ' 看灯。' : ' 灯呢?换 tty。');
}

doEnroll() {
  if (this.step !== 'lit' && this.step !== 'enrolled') {
    this.log = '灯都没亮,不要录入';
    return;
  }
  this.log = '手指平放 2-3 秒';
  board.uartWrite(this.hexEnroll, 1);
  let s: string = board.uartRead(4000, 1);
  let i: number = s.toLowerCase().indexOf('f11fe22e');
  if (i < 0) {
    this.log = '无包头 ' + s;
    return;
  }
  let mark: string = s.substring(i + 30, i + 34).toUpperCase();
  let st: string = s.substring(i + 40, i + 42).toUpperCase();
  if (mark !== '0118') {
    this.log = '标记不是 0118: ' + mark;
    return;
  }
  if (st === '0E') {
    this.step = 'enrolled';
    this.log = '录入成功';
  } else if (st === '0F') {
    this.log = '已存在,不是成功';
  } else {
    this.log = '录入状态 ' + st;
  }
}

hex 偏移:包头 f11fe22e 在字符串里的下标 i,标记在 i+30 起 4 个字符(对应字节 +15),状态在 i+40 起 2 个字符(对应字节 +20)。不要用 indexOf('0E') 这种全串搜索,0E 随时出现在校验里。

UI 线程里同步 poll 4 秒会把停止按钮发黏。通路验证用 C 工具;页面要流畅,把读放到异步 native。换口先断开,否则 fd 还是旧的。


8. 现场对照

57600 写成 115200。 灯不亮,RX 是噪声,找不到包头。

写前不冲 RX。 亮灯「成功」一次,之后全乱。tcflush(TCIFLUSH)。

0F 当成功。 模板已在,再录同一根手指会一直 0F。要先删,或换手指。

标记没对就读状态。 半截帧、两条帧粘在一起,+20 处是别的字段。先 0118 / 0123。

手指点一下。 采图失败。平放 2–3 秒。

丝印 ttyS7,实际 ttyS8。 以灯为准。

两颗模组并排。 TX 短路。一次一个。

CRTSCTS。 CTS 悬空,发得出收不回。关掉。

select / VTIME。 本芯片 dw-apb-uart 这两条路是假的。poll。

32 位。 file 看 interpreter。

权限。 chmod 0666。开机脚本,不要只靠 hdc。

无响应三板斧:换 tty、查供电、断开再连。比改帧有效。


9. 什么叫过

项通过
打开`ttyS7` 或 `ttyS8` 或 `ttyS3`,`c` 节点,57600
亮灯模组灯亮;RX 里能扫到 `F11FE22E` 更好,灯是硬判据
录入标记 `0118`,状态 `0E`;`0F` 不算这条过
比对同一根手指,标记 `0123`
反例没录过的手指比对失败

灯没亮,后面全不算。不要用「能 open」当过。不要用系统设置里的指纹菜单当过。


路径

用途路径
DTS`device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts`
放权`device/board/rk/rk3568_evb/board_perm/board-perm.sh`
工具`uart_fp.c` → `/data/local/tmp/uart_fp`
节点`/dev/ttyS7` `/dev/ttyS8` `/dev/ttyS3`,以灯为准
勿占`/dev/ttyS2`,1500000

系列第 48 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐