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`115200RS232,经过电平转换芯片,第 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) 叠上去,停止按钮点不了。连续读要么:

  1. 手动点「读一次」,超时 400~600 ms;
  2. 用 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. 验收

  1. /proc/tty/driver/serial 有 0/2/3/7/8(按你实际 okay 的集合),类型 16550A。
  2. /dev/ttyS7 是 crw,权限 0666(应用要用的那几根)。
  3. uart_tool open /dev/ttyS7 9600 返回 ok。
  4. 插一个自己会吐数据的模块(GPS 最省事),read text 1500 能看到 ASCII;hex 模式能看到 24 开头的 $。
  5. 应用打开 ttyS7,点「读一次」有字;打开 ttyS2 被页面拒绝。
  6. 写一段短字符串,对端(另一台串口工具,或第 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

Logo

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

更多推荐