蓝牙BLE串口透传驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
NMEA 字段数清楚了。今天换外置蓝牙:对板子就是一根串口在透传,手机侧扫服务、订通知、写特征。
第 41 章把 UART 的门打开了:uartN 对应 /dev/ttySN,termios 设成 8N1 原始模式,poll 等数据。第 43 章用同一套接口收 NMEA。这一章还是同一根排针、同一个 ttyS7,换上的却是一颗 外置 BLE 透传模组。
先把最容易走错的门焊死:
这不是 RK3568 片上的蓝牙控制器。
不是 HCI。
不是 BlueZ。
不是开源鸿蒙的 bluetooth_service。
不要去翻 @ohos.bluetooth,那一套管的是 SoC 自己的蓝牙栈。你这块底板上,BLE 是一颗独立的小模块,UART 进、射频出。板子这一侧看起来就是一个普通串口。
我见过三种典型的一天浪费:
- 打开桌面「设置 → 蓝牙」,搜附近设备,什么都没有,开始怀疑镜像缺蓝牙。
- 手机装「蓝牙串口助手」,按经典 SPP、HC-05 那套去配对,密码试
0000、1234,连不上。 - 串口打开了,波特率写成 9600(GPS 的习惯),模组按 115200 说话,两边都是噪声。
三种全错。这章按一次能通的路径走:板上打开 ttyS7 @ 115200,手机装 nRF Connect,连上之后订阅 FFF1、往 FFF2 写 hello,板子读到这五个字母,就算过关。反向再发 test。来回都要在一秒内看到。


1. 这不是片上蓝牙,别去设置里找
RK3568 可以接片上蓝牙,那是另一条线:UART HCI 或 SDIO,接到协议栈,再接到开源鸿蒙的蓝牙服务,应用调 Kit。本产品这条交付路径 没有把那条线接到应用。你看到的「蓝牙模块」,是排针上的一颗透传芯片:
手机 nRF Connect
│ BLE 空中(GATT,服务 FFF0)
▼
外置 BLE 透传模组
│ UART 115200 8N1
▼
RK3568 uart7 → /dev/ttyS7
│ 用户态 open / write / poll+read
▼
你的 C 工具,或 ArkTS 页面
模组上电自己广播。手机能在 nRF Connect 的扫描列表里看见它。不要配对、不要 PIN、不要 0000。 配对是经典蓝牙那套仪式。透传模组走的是 BLE GATT:连上、发现服务、订阅特征、读写特征。仪式到此结束。
系统设置里的蓝牙开关,打的是 SoC 协议栈。栈没起来,列表就是空的。空是正常的,不是镜像坏了。
经典「蓝牙串口助手」找的是 SPP UUID。模组根本不发那套 UUID,助手要么看不见,要么连上立刻掉。不要在助手的设置里把波特率、校验位调来调去——那是给 HC-05 一类经典模块用的。这边波特率只存在于 模组与 SoC 之间的那根 UART,手机看不到,也改不了。
2. 通信怎么走
sequenceDiagram
participant Phone as 手机 nRF Connect
participant Mod as BLE 透传模组
participant UART as RK3568 uart7 / ttyS7
participant App as 用户态工具 / ArkTS
Note over Phone,Mod: BLE 连接,无 PIN
Phone->>Mod: 发现服务 FFF0
Phone->>Mod: 订阅特征 FFF1(notify)
Phone->>Mod: 向 FFF2 写 hello
Mod->>UART: 串口 TX 吐出 hello
UART->>App: poll 就绪,read 得到 hello
App->>UART: write test
UART->>Mod: 串口 RX 收到 test
Mod->>Phone: FFF1 notify 推送 test
两句话记住方向:
| 方向 | 手机做什么 | 板上看到什么 |
|---|---|---|
| 手机 → 板 | 向 **FFF2** 写 | `/dev/ttyS7` 上 `read` 到同样的字节 |
| 板 → 手机 | 订阅 **FFF1** | 板上 `write` 的字节出现在 FFF1 通知里 |
服务 UUID 是 FFF0。展开它才能看见 FFF1、FFF2。有的 nRF Connect 版本把 128 位 UUID 显示成 0000fff0-0000-1000-8000-00805f9b34fb,短的就是 FFF0,别被长度吓住。
模组内部相当于一个双口搬运工:UART 收到的往 FFF1 推,FFF2 收到的往 UART 吐。它不解析 hello 是什么意思。你在板上写 AT 指令,它也会原样送到手机——那不是 AT 模式,只是你把四个字母当数据发了。
3. 设备树:uart7 打开就行
板级文件:
device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts
透传模组挂在 uart7,节点就是 /dev/ttyS7:
&uart7 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart7m1_xfer>;
};
uart7m1_xfer 是一组复用:把对应 GPIO 切成 UART 的 TX/RX。引脚具体是哪两个球,以你这份 dtsi 的 pinctrl 为准。应用不认球号,只认 /dev/ttyS7。
核对运行时:
hdc shell "ls -l /dev/ttyS7"
hdc shell "cat /proc/tty/driver/serial"
hdc shell "cat /proc/device-tree/serial@fe6[0-9a-f]*/status"
期望 /dev/ttyS7 是字符设备,类似:
crw-rw-rw- 1 root root 4, 71 ... /dev/ttyS7
/proc/tty/driver/serial 里要有 7: 这一行,uart 类型常见是 16550A。没有这一行,节点即使存在也可能是有人 echo > /dev/ttyS7 在 tmpfs 上建出来的普通文件。那种文件 tcgetattr 会报 Not a tty。删掉假文件,查 DTS 的 status 和 pinctrl。
不要动 uart2。 ttyS2 是调试口,1500000 8N1,console= 占着它。应用往 ttyS2 写,等于往你自己的串口窗口里灌垃圾,严重时 console 服务被搅乱。GPS、BLE、LoRa 共用的是 uart7 那组排针,不是调试口。
同一组 UART 排针一次只插一个模块。GPS、BLE、LoRa、六轴、语音,物理上抢同一对 TX/RX。插着 GPS 再插 BLE,两颗芯片的 TX 短路,轻则谁都不说话,重则把口打坏。测完拔掉,再插下一个。
4. 完整 C:一块能双向打字的工具
下面这份 uart_tool.c 第 41 章的骨架在这里展开成可执行文件。它做五件事:
open(O_RDWR | O_NOCTTY | O_NONBLOCK)- termios:8N1、原始模式、关掉 CRTSCTS、关掉软件流控
poll等可读,再read(不靠VTIME,也不靠select)- 文本模式丢掉非 ASCII,避免非法 UTF-8 把 ArkTS 闪退
- 命令行一次性发送、或进入交互:键盘一行发走,串口来的打印出来
为什么非阻塞加 poll:开源鸿蒙这颗 SoC 的 dw-apb-uart 对 VTIME 的超时实现不可靠,纯阻塞 read 会永远卡在系统调用里。select 在 DMA 请求失败、驱动回退中断模式时,会出现「内核 rx 计数在涨,用户态 select 却说没数据」。poll(POLLIN) 在这条路径上是稳的。
为什么关 CRTSCTS:模组常常只引出 TX/RX/GND/VCC,CTS 在 SoC 这边悬空。硬件流控一开,控制器认为对端不许发,接收静默。GPS 章、六轴章都踩过。透传模组同样没有流控脚,一律关掉。
把文件放到任意目录,名字就叫 uart_tool.c:
/* uart_tool.c
* OpenHarmony 4.1 用户态:32 位 musl,/dev/ttyS* 原始 8N1。
* 用法:
* uart_tool /dev/ttyS7 115200 交互
* uart_tool /dev/ttyS7 115200 send hello 发一行文本再读 1 秒
* uart_tool /dev/ttyS7 115200 recv 1000 只收 1000 ms
*/
#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 4800: return B4800;
case 9600: return B9600;
case 19200: return B19200;
case 38400: return B38400;
case 57600: return B57600;
case 115200: return B115200;
default: return B0;
}
}
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; /* 必须关:CTS 悬空会卡死收 */
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; /* 超时交给 poll */
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_write_text(int fd, const char *s)
{
size_t n = strlen(s);
ssize_t w = write(fd, s, n);
if (w < 0) {
fprintf(stderr, "write: %s\n", strerror(errno));
return -1;
}
return (int)w;
}
/* 在 timeout_ms 内尽量收。文本模式只留可打印 ASCII 和 \r\n\t。 */
static int uart_read_text(int fd, char *out, int outsz, int timeout_ms)
{
int total = 0, idle = 0;
while (idle < timeout_ms && total < outsz - 1) {
struct pollfd p;
p.fd = fd;
p.events = POLLIN;
p.revents = 0;
int pr = poll(&p, 1, 100);
if (pr > 0 && (p.revents & POLLIN)) {
unsigned char buf[256];
int r = (int)read(fd, buf, sizeof(buf));
if (r > 0) {
int i;
for (i = 0; i < r && total < outsz - 1; i++) {
unsigned char c = buf[i];
if ((c >= 0x20 && c <= 0x7E) || c == '\r' || c == '\n' || c == '\t')
out[total++] = (char)c;
}
idle = 0;
continue;
}
}
idle += 100;
}
out[total] = '\0';
return total;
}
static void interactive(int fd)
{
fprintf(stderr, "交互:一行回车发出去;Ctrl-C 退出。模组连上后往 FFF2 写,这里能看见。\n");
for (;;) {
struct pollfd ps[2];
ps[0].fd = fd;
ps[0].events = POLLIN;
ps[1].fd = STDIN_FILENO;
ps[1].events = POLLIN;
if (poll(ps, 2, 500) <= 0)
continue;
if (ps[0].revents & POLLIN) {
char rx[512];
int n = uart_read_text(fd, rx, sizeof(rx), 200);
if (n > 0)
printf("RX %d: %s\n", n, rx);
}
if (ps[1].revents & POLLIN) {
char line[256];
if (!fgets(line, sizeof(line), stdin))
break;
size_t L = strlen(line);
while (L > 0 && (line[L - 1] == '\n' || line[L - 1] == '\r'))
line[--L] = '\0';
if (L == 0)
continue;
int w = uart_write_text(fd, line);
printf("TX %d: %s\n", w, line);
}
}
}
int main(int argc, char **argv)
{
if (argc < 3) {
fprintf(stderr, "用法: %s <设备> <波特率> [send <文本>|recv <毫秒>]\n", argv[0]);
return 1;
}
const char *dev = argv[1];
int baud = atoi(argv[2]);
int fd = uart_open(dev, baud);
if (fd < 0)
return 1;
if (argc >= 5 && strcmp(argv[3], "send") == 0) {
int w = uart_write_text(fd, argv[4]);
printf("TX %d 字节\n", w);
char rx[512];
int n = uart_read_text(fd, rx, sizeof(rx), 1000);
if (n > 0)
printf("RX %d: %s\n", n, rx);
else
printf("RX: 1 秒内无 ASCII 数据(先让手机订阅 FFF1,或检查连没连上)\n");
close(fd);
return 0;
}
if (argc >= 5 && strcmp(argv[3], "recv") == 0) {
int ms = atoi(argv[4]);
char rx[1024];
int n = uart_read_text(fd, rx, sizeof(rx), ms);
if (n > 0)
printf("RX %d: %s\n", n, rx);
else
printf("RX: %d ms 内无数据\n", ms);
close(fd);
return 0;
}
interactive(fd);
close(fd);
return 0;
}
5. 编出来、丢到板上
用户态必须是 32 位 armv7 + musl。内核是 aarch64,这不矛盾,第 02 章写过。随手用 aarch64-linux-gnu-gcc 编出来的东西,板上 exec 会失败。
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_tool uart_tool.c
file uart_tool
# 期望:ELF 32-bit LSB executable, ARM, ... interpreter /system/lib/ld-musl-arm.so.1
传到板上:
hdc file send uart_tool /data/local/tmp/uart_tool
hdc shell "chmod 0755 /data/local/tmp/uart_tool"
hdc shell "ls -l /dev/ttyS7"
ttyS7 若是 crw------- 且属主 root,HAP 读不到,C 工具用 root 的 hdc shell 还能跑。先用 C 工具把通路确认了,再谈应用权限。临时放权:
hdc shell "chmod 0666 /dev/ttyS7"
重启会丢。交付做法是开机服务里 chmod,脚本放在:
device/board/rk/rk3568_evb/board_perm/board-perm.sh
init.rk30board.cfg 里 once = 1 跑一次。服务名随你,命令必须是 cfg 允许的那几个,别把注释字符串塞进 cmds。第 41 章给过完整片段,这里不重复抄。
6. 我带你跑一次双向透传
插好模组,确认排针上没有第二颗 UART 模块。USB OTG 连电脑。手机装 nRF Connect(Android / iOS 都行,商店里搜这个名字)。
6.1 板上先听着
hdc shell "/data/local/tmp/uart_tool /dev/ttyS7 115200 recv 20000"
这是「听 20 秒」。你去手机上操作,20 秒内写完 hello。
打开失败的典型输出:
open /dev/ttyS7: No such file or directory
DTS 没 okay,或 uart7 的 pinctrl 被别人抢走。
tcgetattr: Not a tty
/dev/ttyS7 是普通文件。ls -l 看第一位是 - 不是 c。删掉它,查驱动有没有 bind。
open /dev/ttyS7: Permission denied
chmod 0666。
6.2 手机 nRF Connect,逐步点
- 打开 nRF Connect,点扫描。列表里会出现模组的广播名(各家名字不同,认刚上电那一颗,离手机近的 RSSI 大)。
- 点 Connect。不要去系统设置里配对。不要输入 PIN。连上之后 Scanner 变成已连接设备。
- 进入设备页,等服务发现完成。找到 FFF0(或
0000fff0-...)。 - 展开。点 FFF1 右侧的三条斜线(Notify)。图标亮起来,表示订阅成功。不订阅,板上发的数据手机收不到,这是最常见的「单向通」。
- 点 FFF2,选 Write / Write Request,文本框输入
hello,发送。
板上那条 recv 20000 应该打出:
RX 5: hello
有的模组会在后面加 \r 或 \n,过滤之后你仍能看见 hello。20 秒到了还是 RX: 20000 ms 内无数据,按第 12 节往下查,不要先改 ArkTS。
6.3 反向:板子说话,手机听
nRF Connect 停在 FFF1 的日志窗口。板上:
hdc shell "/data/local/tmp/uart_tool /dev/ttyS7 115200 send test"
期望:
TX 4 字节
RX: 1 秒内无 ASCII 数据
TX 是关键。RX 空很正常——你没有从手机往回写。手机 FFF1 的通知列表里应出现 test,ASCII 74 65 73 74。
FFF1 没动:
- 没点 Notify。回去点三条斜线。
- 写到了别的 tty。确认命令行是
/dev/ttyS7,不是ttyS2、ttyS3。 - 连接已经掉了。nRF Connect 顶部不是 Connected,重新连。
来回都在一秒内看到,这章的硬件加驱动就过了。应用只是把同样的 open/write/poll 换皮。
6.4 交互模式,当一块双向白板
hdc shell "/data/local/tmp/uart_tool /dev/ttyS7 115200"
你在 hdc 里敲 ping 回车,手机 FFF1 出 ping。手机往 FFF2 写 pong,hdc 打 RX 4: pong。这是验收时最直观的画面。Ctrl-C 结束。
7. ArkTS 页面:文本模式的串口台
应用里不要再发明一套协议。页面三件事:打开 ttyS7 @ 115200、发送文本、轮询读。原生库约定叫 libboard.so,和第 41 章同一份。
entry/src/main/ets/pages/BleUart.ets:
import board from 'libboard.so';
@Entry
@Component
struct BleUart {
@State dev: string = '/dev/ttyS7';
@State baud: string = '115200';
@State tx: string = 'hello';
@State log: string = '先打开串口,再让手机用 nRF Connect 连模组。';
@State opened: boolean = false;
private timer: number = -1;
aboutToDisappear() {
this.stopPoll();
if (this.opened) {
board.uartClose();
this.opened = false;
}
}
build() {
Column() {
Text('BLE 串口透传').fontSize(20).fontWeight(FontWeight.Bold).margin({ bottom: 8 })
Text('外置模组,不是片上蓝牙。手机用 nRF Connect:服务 FFF0,通知 FFF1,写 FFF2。不要配对。')
.fontSize(13).fontColor('#666').margin({ bottom: 10 })
Row() {
Text('设备').fontSize(14).width(48)
TextInput({ text: this.dev }).onChange((v: string) => { this.dev = v; }).layoutWeight(1)
}.margin({ bottom: 6 })
Row() {
Text('波特').fontSize(14).width(48)
TextInput({ text: this.baud }).type(InputType.Number)
.onChange((v: string) => { this.baud = v; }).layoutWeight(1)
}.margin({ bottom: 6 })
Row() {
Button(this.opened ? '关闭' : '打开').onClick(() => { this.toggle(); }).margin({ right: 8 })
Button(this.timer >= 0 ? '停止读' : '连续读').onClick(() => { this.togglePoll(); })
}.margin({ bottom: 8 })
Row() {
TextInput({ text: this.tx, placeholder: '发给手机的文本' })
.onChange((v: string) => { this.tx = v; }).layoutWeight(1)
Button('发送').onClick(() => { this.send(); }).margin({ left: 8 })
}.margin({ bottom: 8 })
Text(this.log).fontSize(14).fontColor('#1565C0').width('100%')
}
.padding(16).width('100%').height('100%').alignItems(HorizontalAlign.Start)
}
toggle() {
if (this.opened) {
this.stopPoll();
board.uartClose();
this.opened = false;
this.log = '已关闭';
return;
}
let b: number = parseInt(this.baud);
if (Number.isNaN(b)) {
b = 115200;
}
let r: number = board.uartOpen(this.dev, b);
this.opened = (r === 0);
this.log = this.opened
? ('已打开 ' + this.dev + ' @' + b + '。手机连上后点连续读,再向 FFF2 写 hello。')
: ('打开失败。查节点、chmod 0666、DTS uart7、别占用 ttyS2。');
}
send() {
if (!this.opened) {
this.log = '先打开';
return;
}
let n: number = board.uartWrite(this.tx, 0); /* 0 = 文本,不是十六进制 */
this.log = 'TX ' + n + ' 字节:' + this.tx + '(看手机 FFF1)';
}
togglePoll() {
if (this.timer >= 0) {
this.stopPoll();
this.log = '已停止连续读';
return;
}
if (!this.opened) {
this.log = '先打开';
return;
}
this.timer = setInterval(() => {
let s: string = board.uartRead(200, 0);
if (s.length > 0) {
this.log = 'RX: ' + s;
}
}, 400);
}
stopPoll() {
if (this.timer >= 0) {
clearInterval(this.timer);
this.timer = -1;
}
}
}
连续读间隔 400 ms、每次 uartRead 超时 200 ms。uartRead 若在 ArkTS 主线程里同步跑 poll,间隔会被吃满,停止按钮点不动。第 41 章给了 uartReadAsync(napi_async_work)。页面要流畅,把上面的 board.uartRead 换成 await board.uartReadAsync(200, 0),并把回调改成 async。演示通路,同步版也能用,别开太短的 setInterval。
声明文件 entry/src/main/cpp/types/libboard/index.d.ts:
export function uartOpen(path: string, baud: number): number;
export function uartClose(): number;
export function uartWrite(data: string, isHex: number): number;
export function uartRead(timeoutMs: number, asHex: number): string;
export function uartReadAsync(timeoutMs: number, asHex: number): Promise<string>;
oh-package.json5 里模块名与 libboard.so 对齐。HAP 是 32 位 armeabi-v7a。签名、安装走第 69 章,这里只要求页面能调到 so。
8. NAPI:C 侧和工具是同一套 termios
entry/src/main/cpp/board_uart.cpp 里,打开和读写必须与 uart_tool.c 同策略。下面是精简后仍完整可编的三件套。全局一个 fd,应用同时只开一个串口——这正是排针「一次一个模块」在软件上的投影。
#include <fcntl.h>
#include <poll.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include "napi/native_api.h"
static int g_uart = -1;
static speed_t baud_const(int b)
{
switch (b) {
case 9600: return B9600;
case 115200: return B115200;
case 38400: return B38400;
case 57600: return B57600;
default: return B9600;
}
}
static napi_value UartOpen(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value a[2];
napi_get_cb_info(env, info, &argc, a, nullptr, nullptr);
char path[64] = {0};
size_t len = 0;
int baud = 115200;
napi_get_value_string_utf8(env, a[0], path, sizeof(path) - 1, &len);
napi_get_value_int32(env, a[1], &baud);
if (g_uart >= 0) {
close(g_uart);
g_uart = -1;
}
int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
int rc = -1;
if (fd >= 0) {
struct termios t;
memset(&t, 0, sizeof(t));
if (tcgetattr(fd, &t) == 0) {
cfsetispeed(&t, baud_const(baud));
cfsetospeed(&t, baud_const(baud));
t.c_cflag = (t.c_cflag & ~CSIZE) | CS8 | CLOCAL | CREAD;
t.c_cflag &= ~(PARENB | CSTOPB | 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) {
g_uart = fd;
rc = 0;
}
}
if (rc != 0)
close(fd);
}
napi_value ret;
napi_create_int32(env, rc, &ret);
return ret;
}
static napi_value UartClose(napi_env env, napi_callback_info info)
{
(void)info;
if (g_uart >= 0) {
close(g_uart);
g_uart = -1;
}
napi_value ret;
napi_create_int32(env, 0, &ret);
return ret;
}
static napi_value UartWrite(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value a[2];
napi_get_cb_info(env, info, &argc, a, nullptr, nullptr);
char buf[512] = {0};
size_t len = 0;
int isHex = 0;
napi_get_value_string_utf8(env, a[0], buf, sizeof(buf) - 1, &len);
napi_get_value_int32(env, a[1], &isHex);
int n = -1;
if (g_uart >= 0) {
/* 写前清输入:丢掉上次残留,避免和下一包粘在一起 */
tcflush(g_uart, TCIFLUSH);
if (!isHex)
n = (int)write(g_uart, buf, len);
else {
/* 十六进制路径第 45 章用,这里保留接口 */
n = (int)write(g_uart, buf, len);
}
}
napi_value ret;
napi_create_int32(env, n, &ret);
return ret;
}
static napi_value UartRead(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value a[2];
napi_get_cb_info(env, info, &argc, a, nullptr, nullptr);
int timeoutMs = 500, asHex = 0;
napi_get_value_int32(env, a[0], &timeoutMs);
napi_get_value_int32(env, a[1], &asHex);
char out[2048];
int total = 0, idle = 0;
out[0] = '\0';
if (g_uart >= 0) {
while (idle < timeoutMs && total < (int)sizeof(out) - 1) {
struct pollfd p;
p.fd = g_uart;
p.events = POLLIN;
p.revents = 0;
int pr = poll(&p, 1, 100);
unsigned char rb[256];
int r = 0;
if (pr > 0 && (p.revents & POLLIN))
r = (int)read(g_uart, rb, sizeof(rb));
if (r > 0) {
int i;
for (i = 0; i < r && total < (int)sizeof(out) - 1; i++) {
unsigned char c = rb[i];
if (asHex) {
static const char *H = "0123456789ABCDEF";
if (total + 2 >= (int)sizeof(out) - 1)
break;
out[total++] = H[c >> 4];
out[total++] = H[c & 0xF];
} else if ((c >= 0x20 && c <= 0x7E) || c == '\r' || c == '\n' || c == '\t') {
out[total++] = (char)c;
}
}
idle = 0;
} else {
idle += 100;
}
}
out[total] = '\0';
}
napi_value ret;
napi_create_string_utf8(env, out, NAPI_AUTO_LENGTH, &ret);
return ret;
}
Init 里把四个函数挂上,名字与 index.d.ts 一致。uartWrite 的十六进制解析第 45 章补全——BLE 这章发 ASCII 即可,isHex 传 0。
9. 验收
一次性清单,全打勾才算这颗模组过:
| 项 | 动作 | 通过 |
|---|---|---|
| 节点 | `ls -l /dev/ttyS7` | `c` 开头的字符设备 |
| 驱动 | `cat /proc/tty/driver/serial` | 有 `7:` 行 |
| 打开 | `uart_tool /dev/ttyS7 115200 recv 2000` | 不报 `Not a tty` / `Permission denied` |
| 手机连 | nRF Connect Connect | 无需 PIN,状态 Connected |
| 订阅 | FFF1 Notify 打开 | 图标亮 |
| 手机→板 | FFF2 写 `hello` | 板上 1 秒内打出 `hello` |
| 板→手机 | `send test` | FFF1 1 秒内出现 `test` |
| 负例 | 系统设置蓝牙列表 | 可以是空的,不作为失败 |
不要用「手机系统蓝牙里能看见名字」当验收。能看见广播只说明模组在供电,GATT 通不通还没测。
10. 坑:每一个都是真事
10.1 跑去设置里开蓝牙
桌面「设置 → 蓝牙」打的是片上协议栈。本产品这条线没把控制器交给它。列表空、开关灰,都不是模组坏了。验收工具是 nRF Connect,不是设置。
有人会在源码树里搜 bluetooth_service、ohos.bluetooth,然后开始编 HCI。那是另一条交付。你现在要的是 write(/dev/ttyS7)。先把五个字母跑通,再谈协议栈。
10.2 经典串口助手 + 密码 0000
SPP 助手假设对端是经典蓝牙、有配对、有 PIN。透传模组不走那条。助手连不上时,人会改密码、改 UUID、改手机。全是空转。卸掉助手,装 nRF Connect。
「密码 0000」这个传说来自某几颗经典模块的出厂配对码。把它用到 BLE 透传上,只会让你在配对对话框里卡死。对话框弹出来就点取消,回到 nRF Connect 的 Connect。
10.3 波特率写成 9600
同一根排针上午测 GPS,下午换 BLE,工具里波特率还停在 9600。模组侧是 115200。两边都能 open 成功,write 也返回正数,内容全是垃圾。文本过滤会把高位字节丢掉,于是「什么都读不到」。
先看模组丝印或手册上的 UART 波特率,再看命令行第二个参数。BLE 透传这章默认 115200。LoRa 那章是 9600。不要共用一个记忆。
10.4 占用 ttyS2
调试口 1500000,应用 115200,两个数都对,口错了。你在 PuTTY 窗口里突然冒出 hello,那是应用写进了 console。立刻停。uartOpen 的路径写死 /dev/ttyS7,页面上的输入框也可以改,但默认不要给 ttyS2。
10.5 CRTSCTS 让接收消失
termios 抄网上「标准 8N1」示例,里面常常 c_cflag |= CRTSCTS。模组没有 CTS 线,脚悬空,UART 认为不允许发,FIFO 不往用户态送。/proc/tty/driver/serial 的 rx 不涨。关流控之后 rx 开始涨,poll 立刻有数据。第 41 章把这条写成铁律,这里再钉一次:透传模组 关硬件流控。
10.6 只写不订
nRF Connect 里 FFF2 能写,板上也能收到,反向没有。因为 FFF1 没订 Notify。写和通知是两个特征。订 FFF1 是板→手机的开关,写 FFF2 是手机→板的开关。两个都要开。
10.7 非 ASCII 把应用打崩
波特率不对时,UART 吐 0x80 以上的字节。napi_create_string_utf8 造出非法 UTF-8,ArkTS 的 split / startsWith 直接崩。所以文本模式在 C 里就把非 ASCII 丢掉。调试「乱码」时用 recv 看过滤后还有没有字;没有字,多半是波特率或接线,不是应用解析写错。
10.8 权限
hdc shell 是 root,C 工具能开,HAP 报打开失败。看 ls -l /dev/ttyS7 是不是 0666。开机服务没跑、或跑在节点出现之前,都会这样。board-perm.sh 里对 ttyS7 做十几秒短轮询 chmod,和 GPIO 预导出是同一类活。
11. 排障顺序,别跳
通不了的时候按这个顺序,不要从改 ArkTS 开始:
1. ls -l /dev/ttyS7 字符设备?权限?
2. cat /proc/tty/driver/serial 有 7: ?
3. 拔掉排针上其它 UART 模块
4. uart_tool recv 20s,同时手机 FFF2 写 hello
5. 若 4 失败:nRF Connect 是否 Connected、FFF0 是否在、写的是不是 FFF2
6. 若 4 成功、应用失败:chmod、hap 是否 32 位、so 是否真的编进包
7. 反向 send test,看 FFF1 是否已 Notify
第 4 步成功、第 6 步失败,问题在应用,不在射频。第 4 步失败,问题在模组、接线、波特率、GATT,不在 ArkTS。分层切开,一天能结束。
本章小结
- 外置 BLE 透传模组 = 一颗挂在 uart7 上的串口设备。手机用 nRF Connect 走 GATT,不用片上蓝牙栈,不用经典配对。
- 服务 FFF0,通知 FFF1(模组 UART → 手机),写入 FFF2(手机 → 模组 UART)。不要 PIN。
- 板上 115200 8N1、关 CRTSCTS、
poll读、文本过滤非 ASCII。工具和 NAPI 同一套。 - 验收是双向五个字母,一秒内到。设置里搜不到蓝牙不是故障。
下一章还是这根排针,换成 LoRa 透传:波特率变成 9600,数据变成十六进制,单模块只能证明串口活着,无线电要两套。
路径清单
| 用途 | 路径 |
|---|---|
| 板级 DTS uart7 | `device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts` |
| 节点 | `/dev/ttyS7` |
| 内核串口表 | `/proc/tty/driver/serial` |
| 用户态工具 | 本章 `uart_tool.c`,丢 `/data/local/tmp/uart_tool` |
| 开机放权 | `device/board/rk/rk3568_evb/board_perm/board-perm.sh` |
| init | `*.rk30board.cfg` 里 once 服务 |
| NAPI | `entry/src/main/cpp/board_uart.cpp`(`libboard.so`) |
| ArkTS | `entry/src/main/ets/pages/BleUart.ets` |
| 声明 | `entry/src/main/cpp/types/libboard/index.d.ts` |
| 手机 | nRF Connect(服务 FFF0 / 通知 FFF1 / 写 FFF2) |
| 调试口(勿占) | `/dev/ttyS2`,1500000 |
系列第 44 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10
更多推荐




所有评论(0)