蓝牙透传会了。今天换 LoRa。一颗模组只能证明口通、模块活着;真要验证无线,得两套对打。

同一根 UART 排针,上午可以挂 GPS、下午可以挂 BLE。再换一颗 LoRa 透传模组,软件上还是 /dev/ttyS7,差别只有三件:

  1. 波特率是 9600,不是 115200。
  2. 数据按 十六进制裸字节 走,不要当文本行,也不要发明一套 AT。
  3. 一颗模组只能证明串口活着。 无线电要两套:一边发、一边收。

我见过的空转几乎全落在这三件上。有人把 BLE 的 115200 原样拿来,write 返回 3,模组 AUX 灯不闪,对端什么都没有。有人对着单颗模组发 FE 05 90,等无线回显,等不到就开始改驱动。有人在串口里敲 AT+ADDR?,期望 OK——透传模式下那六个字符会当射频载荷打出去,模组不会回答你。

两根模式脚都接到地上(低电平),上电就是透传。串口进、射频出,字节不解析。不要进配置态,不要发 AT。手册里若画了 M0/M1 拨码,看的是「两根都低」,不是「发一条指令切模式」。

产品代号仍是 rk3568_evb。节点、termios、poll、32 位 musl,和 BLE 那篇是同一套 C。这篇只把发送改成十六进制,并把「单模块口径」写死,免得验收时扯皮。


1. 它在链路上的位置

板 A 用户态
    │ write(FE 05 90)  @ ttyS7 9600 8N1
    ▼
uart7
    │ TX/RX
    ▼
LoRa 透传模组 A          ← 两根模式脚接地,透传
    │ 射频
    ▼
LoRa 透传模组 B
    │ TX/RX
    ▼
uart7 / ttyS7
    │ poll + read
    ▼
板 B 用户态 打印 FE0590

单板、单模组时,图里的「射频」那一跳不存在。你能验证的只有虚线以上:open 成功、write 返回正数、模组活动灯闪一下。灯闪说明芯片收到了 UART 字节并开始干活。灯不闪,字节没进模组,查波特率、TX/RX 交叉、供电。灯闪了却没有第二套设备,到此为止,不要再往下骂协议。

和 BLE 的对比,省得混:

BLE 透传LoRa 透传
口同组排针,常见 `ttyS7`同左
波特率115200**9600**
载荷ASCII 文本即可**十六进制裸字节**
对端手机 nRF Connect**另一颗 LoRa 模组**
单套能证明什么GATT 双向五个字母**只有 UART**

排针一次一颗。LoRa 和 BLE、GPS 的 TX 并在一起,谁都不说话。测完拔掉。

不要占用 /dev/ttyS2。 那是调试口,1500000。往那里写十六进制,你的串口窗口会开花。


2. 设备树还是 uart7

device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts
&uart7 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart7m1_xfer>;
};

现场核对:

hdc shell "ls -l /dev/ttyS7"
hdc shell "cat /proc/tty/driver/serial"

期望字符设备:

crw-rw-rw- 1 root root 4, 71 ... /dev/ttyS7

serial 表要有 7: 行,类型常见 16550A。第一位是 - 不是 c,是有人 echo > /dev/ttyS7 在 tmpfs 上建的普通文件,tcgetattr 报 Not a tty。删掉假文件,查 DTS status 和 pinctrl。

HAP 不是 root。临时:

hdc shell "chmod 0666 /dev/ttyS7"

重启会丢。开机脚本:

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

由 *.rk30board.cfg 里 once = 1 的服务拉起来。ohos.boot.hardware 必须是 rk30board。

模式脚:两根都接地。有的板子出厂已经用电阻拉低,上电即透传;有的露出两个焊盘或拨码。量一下,低就是透传。一根悬空,模组可能进配置态,UART 上的字节被当成命令吃掉,射频侧什么都不发。这不是驱动 bug。


3. 完整 C:十六进制收发

dw-apb-uart 上两条假路不要走:纯阻塞 read + VTIME 会永远不返回;select 在 DMA 回退中断模式时,内核 rx 涨、用户态却说没数据。稳的是 O_NONBLOCK + poll(POLLIN)。CRTSCTS 关掉,CTS 悬空会卡死收。

十六进制发送:字符串 FE0590 或 FE 05 90 都收,写成三个字节 0xFE 0x05 0x90。打印接收时用大写连续 hex,中间不插空格,对端好对。

uart_lora.c:

/* uart_lora.c — OpenHarmony 4.1 用户态,32 位 musl
 *   uart_lora /dev/ttyS7 9600                 交互:一行当 hex 发出去
 *   uart_lora /dev/ttyS7 9600 send FE0590     发 3 字节,再听 1 秒
 *   uart_lora /dev/ttyS7 9600 recv 5000       只听
 */

#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 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;
}

/* 把 "FE 05 90" / "FE0590" 收成字节。返回字节数,-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_write_bytes(int fd, const unsigned char *b, int n)
{
    ssize_t w = write(fd, b, (size_t)n);
    if (w < 0) {
        fprintf(stderr, "write: %s\n", strerror(errno));
        return -1;
    }
    return (int)w;
}

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 void interactive(int fd)
{
    fprintf(stderr, "一行当 hex 发出去,例如 FE0590。Ctrl-C 退出。\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) {
            unsigned char rx[512];
            int n = uart_read_bytes(fd, rx, sizeof(rx), 200);
            if (n > 0) {
                printf("RX %d: ", n);
                hexdump_line(rx, n);
            }
        }
        if (ps[1].revents & POLLIN) {
            char line[512];
            if (!fgets(line, sizeof(line), stdin))
                break;
            unsigned char tx[256];
            int n = parse_hex(line, tx, (int)sizeof(tx));
            if (n <= 0) {
                fprintf(stderr, "hex 格式不对\n");
                continue;
            }
            int w = uart_write_bytes(fd, tx, n);
            printf("TX %d: ", w);
            hexdump_line(tx, n);
        }
    }
}

int main(int argc, char **argv)
{
    if (argc < 3) {
        fprintf(stderr, "用法: %s <设备> <波特率> [send <hex>|recv <毫秒>]\n",
                argv[0]);
        return 1;
    }
    int baud = atoi(argv[2]);
    int fd = uart_open(argv[1], baud);
    if (fd < 0)
        return 1;

    if (argc >= 5 && strcmp(argv[3], "send") == 0) {
        unsigned char tx[256];
        int n = parse_hex(argv[4], tx, (int)sizeof(tx));
        if (n <= 0) {
            fprintf(stderr, "hex 格式不对: %s\n", argv[4]);
            close(fd);
            return 1;
        }
        int w = uart_write_bytes(fd, tx, n);
        printf("TX %d 字节: ", w);
        hexdump_line(tx, n);
        unsigned char rx[512];
        int r = uart_read_bytes(fd, rx, sizeof(rx), 1000);
        if (r > 0) {
            printf("RX %d: ", r);
            hexdump_line(rx, r);
        } else {
            printf("RX: 1 秒内无数据(单模块正常;对端要另一套)\n");
        }
        close(fd);
        return 0;
    }
    if (argc >= 5 && strcmp(argv[3], "recv") == 0) {
        int ms = atoi(argv[4]);
        unsigned char rx[1024];
        int r = uart_read_bytes(fd, rx, sizeof(rx), ms);
        if (r > 0) {
            printf("RX %d: ", r);
            hexdump_line(rx, r);
        } else {
            printf("RX: %d ms 内无数据\n", ms);
        }
        close(fd);
        return 0;
    }
    interactive(fd);
    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_lora uart_lora.c

file uart_lora
# ELF 32-bit LSB executable, ARM, interpreter /system/lib/ld-musl-arm.so.1
hdc file send uart_lora /data/local/tmp/uart_lora
hdc shell "chmod 0755 /data/local/tmp/uart_lora"

file 若是 aarch64,exec 失败。那是 target 写错。

FE0590 没有特殊协议含义,三个任意非空字节都行。选它只因为肉眼好认,活动灯也好对照:发下去灯该闪。不要把这三字节当成某家模组的「查询 ID」命令——透传模式不认命令。


4. 单模块:只验 UART

插一颗。模式脚都低。旁边不要第二颗 UART 模块。

hdc shell "/data/local/tmp/uart_lora /dev/ttyS7 9600 send FE0590"

期望:

TX 3 字节: FE0590
RX: 1 秒内无数据(单模块正常;对端要另一套)

TX 3 是这步的判据。write 返回 3,内核 serial 表 7: 的 tx 计数加 3,模组活动灯闪一下:UART 通路成立。

RX 空是正常的。没有第二颗模组在空中接,本地 UART 也不会环回——TX 和 RX 是两根线,除非你在排针上短接,否则自己发的字节不会从 RX 爬回来。有人把「RX 空」当成故障,开始改 termios,改一天。

打开失败对照:

open /dev/ttyS7: No such file or directory

DTS 没 okay。

tcgetattr: Not a tty

假节点。

open /dev/ttyS7: Permission denied

chmod 0666。hdc 是 root,通常碰不到;HAP 会。

TX 3 了灯不闪:

  • 波特率不是 9600。115200 时控制器吐字节,模组按 9600 采样,全是噪声,芯片不认为收到完整帧,灯可能完全不动。
  • TX/RX 没交叉。SoC TX 接模组 RX,SoC RX 接模组 TX。两根都接 TX,灯不闪。
  • 模组没供电,或 3.3 V / 5 V 接反。先量 VCC。
  • 模式脚不是双低,字节被吃进配置态。

tx 计数都不涨:write 没进控制器。查 fd、termios、是不是写到了 ttyS2。

单模块验收到此结束。不要在这一步要求「空中有回包」。

交互模式也可以用来盯灯:

hdc shell "/data/local/tmp/uart_lora /dev/ttyS7 9600"

敲 FE0590 回车,看灯。Ctrl-C 退出。


5. 两套:空中那一跳

两块板,或一块板加一台带同一频段透传模组的 USB 转串口。两边都 9600 8N1,都是透传(模式脚双低)。信道、地址、空速出厂通常一致;若一边被人改过配置,透传对不上,那是射频参数问题,不是开源鸿蒙的 UART 问题。先保证两边都是出厂透传,再谈参数。

板 B 先听:

hdc shell "/data/local/tmp/uart_lora /dev/ttyS7 9600 recv 20000"

板 A 发:

hdc shell "/data/local/tmp/uart_lora /dev/ttyS7 9600 send FE0590"

板 B 期望:

RX 3: FE0590

板 A 的 RX: 1 秒内无数据 仍正常,除非 B 也在发。

对不上时,先把两套都退化成「单模块 UART 自检」。两边灯都闪,再把它们放到几米内、天线拧紧、远离金属。一边灯闪一边 recv 空,才是射频:信道、空速、天线、距离、模式脚。不要先改 poll。

短接回环(SoC TX 短接 RX、模组拔掉)只能证明 控制器 活着,证明不了模组。LoRa 的单模块口径是「写成功 + 灯闪」,不是短接回环。

两套对发时注意:透传模组从收到 UART 到射频出去有几十毫秒级的缓冲。A send 之后立刻 recv 100 可能空,B 侧 recv 20000 更稳。不要用过短超时当「射频丢包」。

应用侧 uartWrite(..., isHex=1)、uartRead(..., asHex=1),默认 9600、/dev/ttyS7。不要在透传里发 AT。排障顺序:波特率 9600 → 灯闪 → 单模块到此为止 → 两套才谈射频。


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

Logo

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

更多推荐