指纹识别传感器驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
二进制帧会拆了。今天换指纹模组:私有协议,先发亮灯确认通信,再录入、比对。


排针上又是一颗 UART 模组。这次对面不是 GPS 的文本行,也不是 LoRa 的裸载荷,是一套 私有二进制帧。常见 57600 8N1。tty 可能是 ttyS7、ttyS8、ttyS3,以亮灯为准,不以丝印为准。
先把错门焊死:
- 这不是 SoC 安全元件,不是系统设置里的生物识别。外置模组,UART 进,算法在模组里。
- 灯都没亮,不要谈录入、比对、算法。 通信还没立住。
- 流程必须按这个顺序:连接 → 亮灯(确认通信)→ 录入 → 比对。跳步的人会把残留应答当成成功。
- 写前必须
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
更多推荐


所有评论(0)