鸿蒙UART总线怎么用?—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
I2C 和 GPIO 传感器先到这里。今天换串口:定位、电台、语音、仪表很多走这里。uartN 映射成 ttySN,调试口不要给应用占。


第 33 章到第 40 章,总线是 I2C,或者干脆是一根 GPIO。从这章起换跑道:外设跟 SoC 说话,靠的是 UART。
有人把第 33 章的写法套过来,上来就找 I2C_RDWR,或者把串口当 8250 寄存器去 poke。前者完全跑偏——UART 没有寄存器地址这回事,它是字节流。后者倒接近内核实现:RK3568 上这些串口控制器,内核里报的就是 uart:16550A,底层驱动叫 dw-apb-uart,和老 PC 的 8250 兼容。8250 这一套只在 UART 章讲。I2C 章禁止拿 termios 去套。
这章把「怎么打开一根串口、怎么读、怎么写、怎么不把调试口占死」一次写完。后面 RS232、GPS、BLE、LoRa、六轴,全是这章工具的换皮。


1. 一张对应表,先钉死
RK3568 的 UART 控制器在设备树里叫 uart0、uart2、uart3、uart7、uart8。Linux 串口子系统按 别名序号 创节点:uartN → /dev/ttySN。
本产品 rk3568_evb 实际接线:
| 设备树节点 | 字符设备 | 波特率(默认) | 干什么 | 应用能不能用 |
|---|---|---|---|---|
| `&uart0` | `/dev/ttyS0` | 115200 | RS232,经过电平转换芯片,第 42 章 | 能 |
| `&uart2` | `/dev/ttyS2` | **1500000** | 内核 console、U-Boot、你敲 `#` 的那根 | **不能** |
| `&uart3` | `/dev/ttyS3` | 模块定 | 扩展排针上的另一路 UART | 能 |
| `&uart7` | `/dev/ttyS7` | 模块定 | BLE / LoRa / GPS,看你插哪颗;同一组排针一次一个 | 能 |
| `&uart8` | `/dev/ttyS8` | 模块定 | GPS 或 RS485,以 DTS 注释和实测句子为准 | 能 |
内核命令行写死了:
console=ttyS2,1500000
第 05 章你已经用 1500000 8N1 看过 U-Boot。那根线就是 ttyS2。应用里 open("/dev/ttyS2") 再 tcsetattr 改成 9600,下一秒串口工具全是乱码,内核 log 也没了。调试口不是外设口。
核对活着的控制器:
hdc shell "cat /proc/tty/driver/serial"
期望类似:
serinfo:1.0 driver revision:
0: uart:16550A mmio:0xFDD50000 irq:xx tx:0 rx:0
2: uart:16550A mmio:0xFE660000 irq:xx tx:.... rx:....
3: uart:16550A mmio:0xFE650000 irq:xx tx:0 rx:0
7: uart:16550A mmio:0xFE6A0000 irq:xx tx:0 rx:0
8: uart:16550A mmio:0xFE6C0000 irq:xx tx:0 rx:0
uart:16550A 就是 8250 兼容层。mmio 地址能对上 SoC 手册里的 UART 基址。没有那一行,节点要么 disabled,要么 probe 失败(第 42 章 uart0 被抢脚就是这个症状)。
hdc shell "ls -l /dev/ttyS0 /dev/ttyS2 /dev/ttyS3 /dev/ttyS7 /dev/ttyS8"
必须是字符设备 crw。如果某条是普通文件 -rw,有人曾经 echo hello > /dev/ttyS0,当时节点还不存在,shell 在 tmpfs 的 /dev 下建了个假文件。tcgetattr 会报 Not a tty。删掉假文件,重新插模块或重启,让驱动创真正的 c 节点。
2. 通信怎么走
模块 TX ──► SoC RX ──► dw-apb-uart ──► tty 层 ──► /dev/ttySN
模块 RX ◄── SoC TX ◄── 发送 FIFO ◄── write()
UART 是 全双工字节流。没有地址,没有寄存器指针,没有 ACK。你 write 几个字节,对端就该在它的 RX 上看到同样几个字节,顺序不变。协议(NMEA、AT、私有帧)是字节流上面的约定,内核不管。
电气上:SoC 脚是 3.3 V TTL。RS232 要电平转换(第 42 章)。GPS / BLE / LoRa 模块通常直接吃 3.3 V TTL,接扩展排针即可。TX 接 RX、RX 接 TX,地要共。接成 TX-TX,两头都在说话,谁也听不见。
同一组排针的 TX/RX 只有一对。BLE、LoRa、GPS 都可能接到 uart7 或 uart8,拔插,一次一个。三个同时插,总线冲突,全是乱码。设备树不会因为你拔了 GPS 就自动把节点改给 LoRa——软件侧打开的仍是那根 tty,只是对面那颗芯片换了。
GPS 尤其不要写死 ttyS7。有的板级 DTS 把 uart8 注释成 GPS,有的排针把 GPS 接到 uart7。正确做法:对 ttyS3 / ttyS7 / ttyS8 依次打开、读 1 秒,谁吐出 $GNGGA / $GPGGA / $GNRMC,谁就是 GPS。第 43 章按这个扫。
3. 设备树:打开哪一路
板级文件:
device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts
调试口、扩展口最小集合:
/* 调试串口:UART2, 1500000。应用别占。 */
&uart2 {
status = "okay";
};
/* 扩展 UART:GPS / BLE / LoRa 轮流,一次一个 */
&uart7 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart7m1_xfer>;
};
/* 另一组扩展脚,视底板丝印 */
&uart3 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart3m0_xfer>;
};
&uart8 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart8m0_xfer>;
};
pinctrl-0 决定这对 TX/RX 复用到哪组 GPIO。写错 mux,节点能 probe,脚上没波形。改完只刷 boot_linux 不够——dtb 在 resource 分区(第 11、12 章)。只刷 Image、不刷 resource,跑的还是旧脚。
uart0 的抢脚故事留给第 42 章。这章你只要保证:要用的那路 status = "okay",pinctrl 是 xfer 而不是 gpio。
4. 打开方式:termios 8N1 原始模式
用户态打开串口,不是 ioctl 一个魔法数字就完了,是 termios。本教程统一:
| 项 | 值 | 为什么 |
|---|---|---|
| 数据位 | 8(`CS8`) | GPS / BLE / LoRa / RS232 默认 |
| 校验 | 无(`~PARENB`) | 同上 |
| 停止位 | 1(`~CSTOPB`) | 同上 |
| 硬件流控 | 关(`~CRTSCTS`) | 模块常常没有 CTS 脚,CTS 悬空会让发送卡死 |
| 规范模式 | 关(`~ICANON`) | 不要等换行才返回 |
| 回显 | 关(`~ECHO`) | 否则你写下的字会混进读缓冲 |
| 软件流控 | 关(`~IXON`) | 0x11/0x13 是数据,不是 XON/XOFF |
| 输出处理 | 关(`~OPOST`) | 不要把 `\n` 变成 `\r\n` |
| `VMIN` | 0 | 配合非阻塞 |
| `VTIME` | 0 | **不要靠 VTIME 做超时** |
打开标志:O_RDWR | O_NOCTTY | O_NONBLOCK。
O_NOCTTY:这个 fd 不要变成进程的控制终端。否则模块乱发的字符可能被当成 Ctrl+C。
O_NONBLOCK:read 没数据立刻返回。超时交给 poll。
4.1 为什么不是阻塞 + VTIME
经典 Unix 写法:阻塞 fd,VMIN=0,VTIME=1,read 最多等 0.1 秒。教科书这么教。
这块板的 dw-apb-uart 没有实现 VTIME 超时。你按教科书写,read 会永久阻塞。ArkTS 如果在 UI 线程调这个 native,整页卡死,看起来像 ANR,点停止按钮没反应。
所以:不要阻塞 + VTIME。
4.2 为什么不是 select
有人改成 O_NDELAY + select。内核 /proc/tty/driver/serial 的 rx 计数在飞,示波器上模块确实在吐字节,用户态 select 却一直说 fd 没就绪。
原因:控制器 DMA 请求失败之后回退到中断模式。这条路径上,select 的就绪检测不可靠。数据已经进了 tty 缓冲,用户态看不见。
poll 对非阻塞串口 fd 是可靠的。 带超时,就绪了再 read。这是本产品上的最终方案。第 47 章六轴、第 43 章 GPS,踩的都是这个坑。
4.3 文本模式必须过滤非 ASCII
GPS、BLE 在波特率不对时会吐 0x80 以上的脏字节。NAPI 用 napi_create_string_utf8 把缓冲区交给 ArkTS。非法 UTF-8 在 ArkTS 里 split / startsWith 会直接崩。
文本模式只保留:
0x20~0x7E可打印 ASCII\r\n\t
十六进制模式原样输出,不过滤。
5. 完整 C:uart_tool.c
把下面整份存成 uart_tool.c。这是命令行工具,也是 NAPI 里那四个函数的原型。编译目标是 32 位 musl,板上只有 /system/lib/ld-musl-arm.so.1。
/* uart_tool.c — OpenHarmony 4.1 / rk3568_evb 用户态串口工具
* 8N1 原始模式,O_NONBLOCK + poll,文本过滤非 ASCII。
*/
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
static int g_fd = -1;
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 B9600;
}
}
static int hexval(char 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;
}
/* UartOpen(path, baud) → 0 成功 / -1 失败 */
int UartOpen(const char *path, int baud)
{
if (g_fd >= 0) {
close(g_fd);
g_fd = -1;
}
int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd < 0) {
perror("open");
return -1;
}
struct termios t;
memset(&t, 0, sizeof(t));
if (tcgetattr(fd, &t) != 0) {
perror("tcgetattr");
close(fd);
return -1;
}
cfsetispeed(&t, baud_const(baud));
cfsetospeed(&t, baud_const(baud));
t.c_cflag = (t.c_cflag & ~CSIZE) | CS8;
t.c_cflag |= (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) {
perror("tcsetattr");
close(fd);
return -1;
}
g_fd = fd;
return 0;
}
void UartClose(void)
{
if (g_fd >= 0) {
close(g_fd);
g_fd = -1;
}
}
/* UartWrite:is_hex=1 时 data 是 "0A1B2C" 或带空格的十六进制串 */
int UartWrite(const char *data, int is_hex)
{
if (g_fd < 0) {
fprintf(stderr, "not open\n");
return -1;
}
tcflush(g_fd, TCIFLUSH);
if (!is_hex) {
size_t n = strlen(data);
return (int)write(g_fd, data, n);
}
uint8_t out[256];
int o = 0;
size_t len = strlen(data);
size_t i = 0;
while (i < len && o < (int)sizeof(out)) {
if (data[i] == ' ' || data[i] == '\t') {
i++;
continue;
}
if (i + 1 >= len) break;
int hi = hexval(data[i]);
int lo = hexval(data[i + 1]);
if (hi < 0 || lo < 0) break;
out[o++] = (uint8_t)((hi << 4) | lo);
i += 2;
}
return (int)write(g_fd, out, (size_t)o);
}
/* UartRead:timeout_ms 内尽量收齐;as_hex=1 返回大写十六进制,否则文本(过滤非 ASCII)
* 调用方提供 buf/bufsz,返回写入的字符数(不含结尾 NUL)。
*/
int UartRead(int timeout_ms, int as_hex, char *buf, int bufsz)
{
if (g_fd < 0 || bufsz < 2) return -1;
int used = 0;
int idle_ms = 0;
uint8_t rb[1024];
buf[0] = '\0';
while (idle_ms < timeout_ms && used < bufsz - 8) {
struct pollfd pfd;
pfd.fd = g_fd;
pfd.events = POLLIN;
pfd.revents = 0;
int pr = poll(&pfd, 1, 100);
int r = 0;
if (pr > 0 && (pfd.revents & POLLIN))
r = (int)read(g_fd, rb, sizeof(rb));
if (r > 0) {
if (as_hex) {
int k;
for (k = 0; k < r && used < bufsz - 3; k++) {
int n = snprintf(buf + used, (size_t)(bufsz - used),
"%02X", rb[k]);
if (n > 0) used += n;
}
} else {
int k;
for (k = 0; k < r && used < bufsz - 1; k++) {
unsigned char c = rb[k];
if ((c >= 0x20 && c <= 0x7E) ||
c == '\r' || c == '\n' || c == '\t')
buf[used++] = (char)c;
}
}
idle_ms = 0;
} else {
idle_ms += 100;
}
}
buf[used] = '\0';
return used;
}
static void usage(const char *argv0)
{
fprintf(stderr,
"usage:\n"
" %s open <dev> <baud>\n"
" %s write <dev> <baud> text <string>\n"
" %s write <dev> <baud> hex <AABBCC>\n"
" %s read <dev> <baud> text <timeout_ms>\n"
" %s read <dev> <baud> hex <timeout_ms>\n",
argv0, argv0, argv0, argv0, argv0);
}
int main(int argc, char **argv)
{
if (argc < 3) {
usage(argv[0]);
return 1;
}
const char *cmd = argv[1];
if (strcmp(cmd, "open") == 0) {
if (argc < 4) { usage(argv[0]); return 1; }
if (UartOpen(argv[2], atoi(argv[3])) != 0) return 1;
printf("open %s %s ok (fd stays only for this process)\n",
argv[2], argv[3]);
UartClose();
return 0;
}
if (strcmp(cmd, "write") == 0) {
if (argc < 6) { usage(argv[0]); return 1; }
if (UartOpen(argv[2], atoi(argv[3])) != 0) return 1;
int is_hex = (strcmp(argv[4], "hex") == 0);
int n = UartWrite(argv[5], is_hex);
printf("wrote %d\n", n);
UartClose();
return n < 0 ? 1 : 0;
}
if (strcmp(cmd, "read") == 0) {
if (argc < 6) { usage(argv[0]); return 1; }
if (UartOpen(argv[2], atoi(argv[3])) != 0) return 1;
int as_hex = (strcmp(argv[4], "hex") == 0);
char buf[4096];
int n = UartRead(atoi(argv[5]), as_hex, buf, sizeof(buf));
if (n < 0) {
UartClose();
return 1;
}
fwrite(buf, 1, (size_t)n, stdout);
if (n == 0 || buf[n - 1] != '\n') putchar('\n');
UartClose();
return 0;
}
usage(argv[0]);
return 1;
}
注意:命令行每次调用都是「打开 → 干活 → 关闭」。进程退出 fd 就没了。NAPI 里会把 g_fd 留在 so 的全局里,供 ArkTS 连续读写。命令行工具不需要常开。
6. 交叉编译、丢到板上
源码树根目录记成 $OHOS。clang 和 sysroot 在预编译工具里:
OHOS=/path/to/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
核对是 32 位 ARM、动态 musl:
file uart_tool
# ELF 32-bit LSB executable, ARM, ... interpreter /system/lib/ld-musl-arm.so.1
用 aarch64-linux-gnu-gcc 编出来的,板上 exec 直接失败。第 02 章写过:用户态 32 位,内核 64 位。
hdc file send uart_tool /data/local/tmp/uart_tool
hdc shell "chmod 0755 /data/local/tmp/uart_tool"
先测打开 ttyS7 会不会立刻失败:
hdc shell "/data/local/tmp/uart_tool open /dev/ttyS7 9600"
Permission denied:节点还是 600 root。跳到第 10 节放权。Not a tty:假文件。No such file:DTS 没 okay,或 uart 号认错。
板上如果有 microcom:
hdc shell "microcom -s 9600 /dev/ttyS7"
没有就用 uart_tool。microcom 是交互式的,hdc shell 里 Ctrl+X 退出,有的版本是 Ctrl+A。GPS 这种自己吐数据的模块,read 子命令更干净。
7. 我带你排一次障
场景:GPS 模块插在扩展排针上,你以为是 ttyS7,应用读不到字。
第一步,确认节点是真串口。
hdc shell "ls -l /dev/ttyS7"
hdc shell "cat /proc/tty/driver/serial | grep '^7:'"
7: 那行不存在 → 设备树 &uart7 不是 okay,或者 pinctrl 失败。去 /proc/device-tree 看 status:
hdc shell "cat /proc/device-tree/serial@fe6a0000/status"
(基址以你 dtsi 为准,ls /proc/device-tree/serial@* 找。)disabled 就是没打开。okay 但 /proc/tty/driver/serial 没有 7,是 probe 失败,dmesg 搜 fe6a0000 或 dw-apb-uart。
第二步,看权限。
hdc shell "ls -l /dev/ttyS7"
crw------- 1 root root:root 的 hdc shell 能开,HAP 不能。应用报 -1,shell 下 uart_tool 却成功——就是这个。
第三步,确认对面真的在吐数据。
hdc shell "cat /proc/tty/driver/serial | grep '^7:'"
# 记 rx=
hdc shell "sleep 2"
hdc shell "cat /proc/tty/driver/serial | grep '^7:'"
rx 在涨,内核已经收到字节。再:
hdc shell "/data/local/tmp/uart_tool read /dev/ttyS7 9600 text 1500"
有 $GNGGA 一类,通路通,问题在应用。rx 不涨:波特率不对、TX/RX 接反、模块没上电、插错排针、或者这根 tty 根本不是这组脚。
第四步,扫波特率和口。
模块 supposes 9600,你开 115200,内核 rx 可能仍涨(它不校验对方波特率,只是采样错),读出来是乱码。文本模式过滤之后变成空串——这就是「rx 在涨、应用没字」。用 hex 读一眼:
hdc shell "/data/local/tmp/uart_tool read /dev/ttyS7 9600 hex 800"
hdc shell "/data/local/tmp/uart_tool read /dev/ttyS7 115200 hex 800"
NMEA 的 $ 是 24,G 是 47。看到 24474E($GN)就对了。一堆 00 或 FF 是线没接好。
ttyS3 / ttyS7 / ttyS8 都扫一遍。排针丝印和你脑子里的「应该是 7」经常打架。
第五步,确认你没占 ttyS2。
hdc shell "lsof /dev/ttyS2" # 有的镜像没有 lsof,改看
hdc shell "ls -l /proc/*/fd 2>/dev/null | grep ttyS2"
应用误开 ttyS2 之后,你的 PuTTY 窗口会停,或者变成 9600 乱码。立刻杀那个进程,别重启盲试。
8. NAPI:libboard.so
应用不直接链 uart_tool.c,而是把四个函数暴露给 ArkTS。模块名 libboard.so,在 oh-package.json5 里声明。下面是 napi_init.cpp 里串口相关的完整实现。g_uart 是进程内全局:同时只开一个口。GPS 和 LoRa 不要两个页面一起 open。
/* 片段:放入 entry/src/main/cpp/napi_init.cpp
* 依赖:上面的 UartOpen / UartWrite / UartRead / UartClose
* 以及 <poll.h> <termios.h>
*/
#include "napi/native_api.h"
static int g_uart = -1; /* 与工具里的 g_fd 同一角色,NAPI 单独一份 */
/* 把第 5 节四个函数拷进来,把 g_fd 改成 g_uart,或直接复用同一份 static。 */
static napi_value NapiUartOpen(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 = 9600;
napi_get_value_string_utf8(env, a[0], path, sizeof(path) - 1, &len);
napi_get_value_int32(env, a[1], &baud);
int rc = UartOpen(path, baud);
napi_value ret;
napi_create_int32(env, rc, &ret);
return ret;
}
static napi_value NapiUartClose(napi_env env, napi_callback_info info)
{
(void)info;
UartClose();
napi_value ret;
napi_create_int32(env, 0, &ret);
return ret;
}
static napi_value NapiUartWrite(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 is_hex = 0;
napi_get_value_string_utf8(env, a[0], buf, sizeof(buf) - 1, &len);
napi_get_value_int32(env, a[1], &is_hex);
int n = UartWrite(buf, is_hex);
napi_value ret;
napi_create_int32(env, n, &ret);
return ret;
}
static napi_value NapiUartRead(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 timeout_ms = 500, as_hex = 0;
napi_get_value_int32(env, a[0], &timeout_ms);
napi_get_value_int32(env, a[1], &as_hex);
char buf[4096];
int n = UartRead(timeout_ms, as_hex, buf, sizeof(buf));
napi_value ret;
napi_create_string_utf8(env, n > 0 ? buf : "", NAPI_AUTO_LENGTH, &ret);
return ret;
}
/* Init 里注册:
* { "uartOpen", NULL, NapiUartOpen, NULL, NULL, NULL, napi_default, NULL },
* { "uartClose", NULL, NapiUartClose, NULL, NULL, NULL, napi_default, NULL },
* { "uartWrite", NULL, NapiUartWrite, NULL, NULL, NULL, napi_default, NULL },
* { "uartRead", NULL, NapiUartRead, NULL, NULL, NULL, napi_default, NULL },
*/
对应 index.d.ts:
export const uartOpen: (path: string, baud: number) => number;
export const uartClose: () => number;
export const uartWrite: (data: string, isHex: number) => number;
export const uartRead: (timeoutMs: number, asHex: number) => string;
同步 uartRead 不要放在 UI 线程里轮询。 poll 100 ms × 若干次,一次调用就能吃掉 300~600 ms。setInterval(300) 叠上去,停止按钮点不了。连续读要么:
- 手动点「读一次」,超时 400~600 ms;
- 用
napi_async_work把 poll+read 丢到 worker 线程,返回 Promise(第 47 章六轴会把异步版写全)。
这章先把同步版做对。页面上只提供「读一次」。
9. ArkTS 页面
新建一页 UartConsole.ets,给 GPS / BLE / LoRa 当底板。设备路径和波特率做成输入框,不要写死。排针换了,改框就能试。
import { router } from '@kit.ArkUI';
import board from 'libboard.so';
@Entry
@Component
struct UartConsole {
@State msg: string = '填设备节点和波特率,点打开';
@State dev: string = '/dev/ttyS7';
@State baudStr: string = '9600';
@State opened: boolean = false;
@State hex: boolean = false;
@State out: string = '';
@State tx: string = '';
aboutToDisappear() {
if (this.opened) {
board.uartClose();
this.opened = false;
}
}
build() {
Column() {
Text('UART 控制台').fontSize(20).fontWeight(FontWeight.Bold).margin({ bottom: 8 })
Text('同一组排针一次一个模块。ttyS2 是调试口,不要填。')
.fontSize(13).fontColor('#C62828').margin({ bottom: 8 })
Row() {
Text('设备').width(48)
TextInput({ text: this.dev })
.onChange((v: string) => { this.dev = v; })
.layoutWeight(1)
}.margin({ bottom: 6 })
Row() {
Text('波特率').width(48)
TextInput({ text: this.baudStr })
.onChange((v: string) => { this.baudStr = v; })
.layoutWeight(1)
}.margin({ bottom: 6 })
Row() {
Button(this.opened ? '关闭' : '打开').onClick(() => { this.toggle(); })
Button('读一次').margin({ left: 8 }).onClick(() => { this.readOnce(); })
Button(this.hex ? 'HEX 开' : 'HEX 关').margin({ left: 8 })
.onClick(() => { this.hex = !this.hex; })
}.margin({ bottom: 8 })
TextInput({ text: this.tx, placeholder: '要发送的内容' })
.onChange((v: string) => { this.tx = v; })
.margin({ bottom: 6 })
Button('发送').onClick(() => { this.send(); })
Text(this.msg).fontSize(14).margin({ top: 10 }).fontColor('#1565C0')
Text(this.out).fontSize(13).margin({ top: 8 }).fontColor('#333')
}
.padding(16)
.width('100%')
}
toggle() {
if (this.opened) {
board.uartClose();
this.opened = false;
this.msg = '已关闭';
return;
}
if (this.dev.indexOf('ttyS2') >= 0) {
this.msg = '拒绝:ttyS2 是调试口,换 ttyS7 / ttyS3 / ttyS8';
return;
}
let baud: number = parseInt(this.baudStr);
if (Number.isNaN(baud)) baud = 9600;
let rc: number = board.uartOpen(this.dev, baud);
if (rc === 0) {
this.opened = true;
this.msg = '已打开 ' + this.dev + ' @' + baud.toString() +
'。点「读一次」,不要自动轮询(同步 uartRead 会卡住 UI)。';
} else {
this.msg = '打开失败 rc=' + rc.toString() +
'。查节点是不是 crw、权限 0666、DTS okay、有没有占 ttyS2。';
}
}
readOnce() {
if (!this.opened) {
this.msg = '先打开';
return;
}
let s: string = board.uartRead(600, this.hex ? 1 : 0);
if (s.length === 0) {
this.out = '(600ms 无数据。换口、换波特率、看 /proc/tty/driver/serial 的 rx)';
} else {
this.out = s;
}
}
send() {
if (!this.opened) {
this.msg = '先打开';
return;
}
let n: number = board.uartWrite(this.tx, this.hex ? 1 : 0);
this.msg = 'write 返回 ' + n.toString();
}
}
oh-package.json5 的 devDependencies 里不要漏 so。abiFilters 必须有 armeabi-v7a。只编 arm64,板上 loader 对不上。
10. 权限:HAP 不是 root
hdc shell 是 root,open("/dev/ttyS7") 永远成功,会给你一种「驱动好了」的错觉。HAP 进程 uid 不是 0,默认 600 的 tty 节点打不开,uartOpen 返回 -1。
开机服务里预放权。脚本放:
device/board/rk/rk3568_evb/board_perm/board-perm.sh
相关片段:
#!/system/bin/sh
# 开机一次性。HAP 非 root,写不了 chmod,只能这里做。
chmod 0666 /dev/ttyS3 2>/dev/null
chmod 0666 /dev/ttyS7 2>/dev/null
chmod 0666 /dev/ttyS8 2>/dev/null
# 不要 chmod ttyS2。调试口保持 root。
init.rk30board.cfg 里注册 once 服务,console 起来之后跑。改脚本要重打 vendor,刷 mmcblk1p8(SD)或对应 eMMC 分区。只重装 HAP 不够。
临时调试:
hdc shell "chmod 0666 /dev/ttyS7 /dev/ttyS8 /dev/ttyS3"
重启就丢。不要把临时 chmod 当成交付。
SELinux 若是 enforcing,光 chmod 也不够。本教程 bring-up 阶段 enforcing=0。交付转 enforcing 时,tty 节点要补 te 规则,那是后话。
11. 同一组排针,一次一个模块
硬件就一对 TX/RX。GPS 模块、BLE 透传模块、LoRa 模块,电气上都接同一组脚时:
- 软件永远打开
/dev/ttyS7(或你实测的那根) - 物理上插谁,谁就是对端
- 三个一起插 = 三颗 TX 并联,电平打架
页面上设备框保留 ttyS3 / ttyS7 / ttyS8,是因为底板丝印和线序会骗人,不是让你同时开三个。打开新口之前必须 uartClose 旧的。g_uart 是全局,后一次 uartOpen 会先关前一次,但波特率、对端芯片都变了,页面状态机要自己跟上。
RS232 走 ttyS0,不和这组排针抢。RS485 可能走 ttyS8,GPS 也可能走 ttyS8,以打开后读到的内容为准。不要把「我在某根丝印上」写进代码,认 tty 号,认实测 rx 在涨、句子对得上的那根。
12. 验收
/proc/tty/driver/serial有 0/2/3/7/8(按你实际 okay 的集合),类型16550A。/dev/ttyS7是crw,权限0666(应用要用的那几根)。uart_tool open /dev/ttyS7 9600返回 ok。- 插一个自己会吐数据的模块(GPS 最省事),
read text 1500能看到 ASCII;hex 模式能看到24开头的$。 - 应用打开 ttyS7,点「读一次」有字;打开 ttyS2 被页面拒绝。
- 写一段短字符串,对端(另一台串口工具,或第 42 章回环)能原样收到。
有 rx 没应用数据:先 hex 读,再查 ASCII 过滤是不是把乱码滤空了——那是波特率问题,不是 read 失败。
13. 坑,都是真事
占了 ttyS2。 应用一 open,你的调试窗口死。先查代码里有没有把「默认串口」写成 ttyS2。有的模板从 PC 抄来 /dev/ttyUSB0,有人改成 ttyS2「反正是串口」。
假节点。 echo 1 > /dev/ttyS7 在节点出现之前执行,留下普通文件。ls -l 看第一位是 - 还是 c。
select 就绪假阴性。 rx 计数涨、应用永远空。改 poll。不要和驱动工程师争论「select 按 POSIX 应该行」——这块控制器的 DMA 回退路径就是这样。
VTIME 永久阻塞。 页面假死,hilog 里没有崩溃。把 fd 改非阻塞,超时用 poll。
CTS 卡发送。 开了 CRTSCTS,模块没有 RTS/CTS 脚,write 阻塞或写 0。本教程默认关硬件流控。
写前不 flush。 上一次模块主动上报的半帧还在缓冲,你写下一次命令,读回来的第一包是垃圾。UartWrite 里 tcflush(TCIFLUSH) 就是干这个。指纹灯「假成功」一类问题,根子经常在这。
32 位没编对。 so 是 arm64,HAP 装上 dlopen 失败。abiFilters 加 armeabi-v7a。
只 chmod 了当前启动盘以外的那份 /dev。 /dev 是 tmpfs,每次启动重建。必须开机脚本,不是你昨天 hdc 敲过就完了。
uartN 就是 ttySN。ttyS2 是 1500000 调试口,应用禁止占用。内核报 16550A,驱动是 dw-apb-uart。用户态 8N1 原始模式,O_NONBLOCK + poll,不要 select,不要 VTIME。文本模式过滤非 ASCII。同一组排针一次一个模块。权限靠开机 chmod 0666。
uart0 从「probe -22、没有真 ttyS0」拉起来,下一篇。GPS 的 $GGA 字段怎么拆,再下一篇。
路径
| 路径 | 用途 |
|---|---|
| `device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts` | `&uart2` `&uart3` `&uart7` `&uart8` |
| `/dev/ttySN` | 用户态节点 |
| `/proc/tty/driver/serial` | 是否真 16550A、tx/rx 计数 |
| `/proc/device-tree/serial@.../status` | 运行中的 dtb 是否 okay |
| `device/board/rk/rk3568_evb/board_perm/board-perm.sh` | 开机 chmod ttyS3/7/8 |
| `entry/src/main/cpp/napi_init.cpp` | `libboard.so` 的 uartOpen/Write/Read/Close |
| `uart_tool.c` | 本章命令行工具,32 位 musl 交叉编译 |
系列第 41 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10
更多推荐



所有评论(0)