无线透传告一段落。今天换六轴:吐的是二进制帧,不是文本。读法不对时内核计数在涨,应用一个字节都拿不到。

模块插上,cat /dev/ttyS7 | od -An -tx1 立刻看见 55 51、55 52、55 53。/proc/tty/driver/serial 里 ttyS7 的 rx 两个数字往上窜。应用这边打开成功、读循环在转,打印却是空的。

这不是六轴模组没吐数据,也不是设备树没打开串口。数据已经进了内核 tty 缓冲。用户态用错了等待方式:O_NDELAY 配 select,在 dw-apb-uart 这条 DMA 申请失败、回退中断的路径上,描述符的“可读”是假的。内核在收,应用以为没数据。

产品代号 rk3568_evb,板级 device/board/rk/rk3568_evb/。OpenHarmony 4.1,用户态 32 位 armv7 musl,板上只有 /system/lib/ld-musl-arm.so.1。六轴模组走 UART,9600 固定,上电自己吐帧,大约 20 Hz,不要主机发命令。


1. 它怎么接到 SoC

六轴模组 TX ──► SoC RX ──► dw-apb-uart ──► tty 层 ──► /dev/ttyS7
六轴模组 RX ◄── SoC TX   (这条线闲着,模组不听命令)

常见接到 uart7,节点 /dev/ttyS7。同一组排针的 TX/RX 只有一对。GPS、LoRa、BLE 透传也吃这组脚,拔插,一次一个。三颗同时插,总线冲突,全是噪声,校验永远过不了。

不要占用 /dev/ttyS2。那是调试口,1500000。往那里读 9600 的二进制帧,串口窗口开花,内核 log 也没了。

电气:SoC 脚 3.3 V TTL。TX 接 RX、RX 接 TX,地要共。接成 TX-TX,两头都在说话,谁也听不见。模组侧 TX 在吐,SoC 侧 RX 必须接到那根线上,否则 od 也是空的。


2. 11 字节帧,不要当文本

波特率 9600 8N1,模组手册写死,改不了。上电之后自己按大约 20 Hz 往 TX 推帧,主机不用 write 任何命令。你在串口里敲 AT、敲 0xFF 复位,模组当噪声,不会停,也不会换格式。

一帧 11 字节:

偏移内容
0帧头 `0x55`
1`0x51` 加速度 / `0x52` 角速度 / `0x53` 角度
2–3X,int16 小端
4–5Y,int16 小端
6–7Z,int16 小端
8–9温度或保留,同样 int16 小端,别当加速度
10checksum = 前 10 字节算术和的低 8 位

三种类型轮流来,所以你看到的字节流是:

55 51 .. .. .. .. .. .. .. .. cs
55 52 .. .. .. .. .. .. .. .. cs
55 53 .. .. .. .. .. .. .. .. cs
55 51 ...

校验:把 buf[0] 到 buf[9] 加起来,取低 8 位,等于 buf[10] 才算一帧。错了就丢掉,从下一个 0x55 再扫。不要“差 1 也凑合”——差 1 往往是丢了字节,后面 X/Y/Z 全错位,静置会读出几十 g。

文本模式会毁掉这条流。0x55 不是给人类看的字符。ICANON、echo、CRNL、按行 fgets,都会把非打印字节吃掉或改掉,帧头没了,后面全是碎片。必须 raw 二进制,打印用十六进制。硬件流控 CRTSCTS 关掉:CTS 悬空时控制器一直等,表现为打不开或者永远读不到。

115200 会拼不出校验通过的帧。控制器按 115200 采样 9600 的电平,字节对不上 0x55,偶发对上了 checksum 也对不上。先把波特率钉死,再谈解析。


3. 换算,以及静置 Z 接近 1 g

加速度(0x51),16 g 量程:

ax = raw_x / 32768 * 16     单位 g
ay = raw_y / 32768 * 16
az = raw_z / 32768 * 16

角度(0x53):

roll  = raw_x / 32768 * 180    单位度
pitch = raw_y / 32768 * 180
yaw   = raw_z / 32768 * 180

角速度(0x52)按手册 2000 dps 量程:

gx = raw_x / 32768 * 2000    单位 °/s
gy = raw_y / 32768 * 2000
gz = raw_z / 32768 * 2000

量程以你手上那份手册为准。16 g / 2000 dps / 180° 是这类模组最常见的三套满量程。换算系数写错,静置会看到 8 g、90° 这种“刚好差一倍”的数,先回头查量程,别先骂 I2C——它不是 I2C。

静置放桌上:

  • az 接近 +1 g 或 -1 g,这是重力,不是故障。
  • ax、ay 在 0 附近小幅度抖。
  • 角速度三轴接近 0。
  • 角度里 yaw 上电能是任意值,没有磁力计融合时会漂,这是这类模组的常态。

轻晃:X/Y 加速度明显变,Z 仍在 1 g 附近。若三轴一起变成几十 g,是大端小端反了,或把 0x52 的原始值用加速度公式去除。int16 小端:raw = (int16_t)(lo | (hi << 8)),hi 是 buf[3] 不是 buf[2]。


4. 设备树:打开 uart7,别指望 DMA 名会自己冒出来

板级:

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

改 DTS 之后,dtb 要进 boot_linux 和 resource。生效的那份经常是 resource 分区里的 rk-kernel.dtb。只刷 Image、不刷 resource,脚还是旧的。

刷之前先看介质,不要抄错成 eMMC:

hdc shell "cat /proc/partitions"

SD 卡启动常见 mmcblk1。dd 必须打到 /dev/block/ 下的分区节点,不要写 /dev/mmcblk1p5 这种会在 tmpfs 上建普通文件的路径:

hdc shell "dd if=/data/local/tmp/boot_linux.img of=/dev/block/mmcblk1p5 bs=4M conv=fsync"
hdc shell "dd if=/data/local/tmp/resource.img of=/dev/block/mmcblk1p4 bs=4M conv=fsync"
hdc shell "sync"

SoC 的 uart 节点里常见这种写法:dmas 有,dma-names 没有。dw-apb-uart 去申请 DMA 通道时要按名字找 "tx" / "rx",属性缺失就失败,打一行类似:

of_dma_request_slave_channel: dma-names property of '/serial@fe6a0000' missing

然后回退中断收发。回退本身能工作:cat 和 od 看得到数据,rx 计数在涨。它坑的是 select 的就绪语义,不是物理层。不要为了消这行 log 去改 DMA,先把用户态读法改对。

现场核对:

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。删掉假文件,让驱动重新创 c 节点。

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。


5. 先证明内核在收,再谈应用

hdc 已连上,shell 是 root。模组插着,供电 3.3 V。

hdc shell "chmod 0666 /dev/ttyS7"
hdc shell "cat /proc/tty/driver/serial"

记下 7: 那一行的 rx=。再抓 64 字节:

hdc shell "dd if=/dev/ttyS7 bs=1 count=64 2>/dev/null | od -An -tx1"

期望立刻出现 55 51 或 55 52 或 55 53,11 字节一组往下排。再读一次 serial,rx 应当明显变大。

这一步过了:模组在吐、脚对了、uart7 probe 了、tty 层在攒字节。后面应用读空,责任在等待方式,不要回头改 DTS、不要换 115200、不要给模组发复位。

这一步不过:

现象先查什么
`dd` 卡住,`rx` 也不涨TX/RX 没交叉,或模组没电,或插错排针
`rx` 涨但 `od` 没有 `55`波特率不是 9600,或旁边还插着 GPS/LoRa
`No such file`DTS 没 okay,或刷了 boot_linux 没刷 resource
`Not a tty``/dev/ttyS7` 是假文件
节点是 ttyS8 才有 `55`这块板的模组接到了 uart8,改路径,别改协议

6. 为什么 select 会空读

第一版常见写法:

fd = open(path, O_RDWR | O_NOCTTY | O_NDELAY);
/* 然后 */
select(fd + 1, &rfds, NULL, NULL, &tv);
if (FD_ISSET(fd, &rfds))
   n = read(fd, buf, sizeof(buf));

O_NDELAY 在这份内核上就是非阻塞。select 等可读。按 POSIX,内核缓冲里有字节就该就绪。

dw-apb-uart 申请 DMA 失败、回退中断之后,这条路径上 select 的可读就绪不可靠:描述符不就绪,或就绪之后 read 仍返回 0。与此同时 8250 的 flip 缓冲照收,/proc/tty/driver/serial 的 rx 在飞。于是你得到那句假现场:内核有数据,应用是空的。

cat 为什么能看见?它走阻塞 read,tty 层有数据就拷,不经过这条 select 判断。所以 od 能通不能证明 select 能通。

不要在这条路上加 usleep 重试 100 次当修复。就绪位是假的,睡多久都是 0。


7. 改法怎么演进到现在这版

① 去掉 O_NDELAY,改阻塞,VMIN=0、VTIME=1。

想法:没数据时 read 最多等 0.1 秒返回 0,有数据立刻返。termios 教科书就是这么写的。

这块 dw-apb-uart 上回退中断之后,VTIME 不生效。read 变成永久阻塞。CLI 还能 Ctrl-C,页面主线程一调,整页卡死,按钮变灰,看起来像应用死了。有数据时偶尔能读出来,一碰到帧间隙或拔模块,线程就钉在 read 上。

所以 ① 只能当对照实验:证明“不是没数据”,不能当产品读法。

② 最终:O_NONBLOCK + poll 带超时;UI 里读放 worker 线程。

poll(POLLIN, timeout_ms) 在这条回退路径上能等到数据。超时返回 0,调用方继续画界面、继续下一轮。read 只在 POLLIN 之后调用,非阻塞,不会钉死。

CLI 里主循环就是 worker。页面上把这段 poll+read 放到独立线程,不要放 UI 主线程:就算 poll 有 50 ms 超时,20 Hz 的解析仍会把一帧预算吃光,点按无响应。线程里解析完再把 ax/ay/az 抛回 UI。

两版对比:

写法这块板上的结果
`O_NDELAY` + `select``rx` 涨,应用空
阻塞 `read`,`VMIN=0` `VTIME=1``VTIME` 无效,永久阻塞,卡 UI
`O_NONBLOCK` + `poll` 超时,独立线程能收,界面不卡

8. 完整 C:打开 ttyS7,poll,扫 0x55,校验,打印

imu_uart.c:

/* imu_uart.c — OpenHarmony 4.1 用户态,32 位 musl
*   imu_uart /dev/ttyS7
* 9600 8N1,O_NONBLOCK + poll。模组自己吐帧,程序只收。
*/

#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 uart_open(const char *path)
{
   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, B9600);
   cfsetospeed(&t, B9600);
   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 | IGNCR);
   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 checksum_ok(const unsigned char *f)
{
   unsigned int s = 0;
   int i;
   for (i = 0; i < 10; i++)
       s += f[i];
   return (s & 0xff) == f[10];
}

static int16_t le_i16(const unsigned char *p)
{
   return (int16_t)(p[0] | (p[1] << 8));
}

static void on_frame(const unsigned char *f)
{
   int16_t x = le_i16(f + 2);
   int16_t y = le_i16(f + 4);
   int16_t z = le_i16(f + 6);

   if (f[1] == 0x51) {
       printf("acc  ax=%7.3f ay=%7.3f az=%7.3f g\n",
              x / 32768.0 * 16.0,
              y / 32768.0 * 16.0,
              z / 32768.0 * 16.0);
   } else if (f[1] == 0x52) {
       printf("gyr  gx=%8.2f gy=%8.2f gz=%8.2f dps\n",
              x / 32768.0 * 2000.0,
              y / 32768.0 * 2000.0,
              z / 32768.0 * 2000.0);
   } else if (f[1] == 0x53) {
       printf("ang  roll=%8.2f pitch=%8.2f yaw=%8.2f deg\n",
              x / 32768.0 * 180.0,
              y / 32768.0 * 180.0,
              z / 32768.0 * 180.0);
   }
   fflush(stdout);
}

int main(int argc, char **argv)
{
   const char *path = (argc >= 2) ? argv[1] : "/dev/ttyS7";
   int fd = uart_open(path);
   if (fd < 0)
       return 1;

   unsigned char acc[64];
   int nacc = 0;
   unsigned long ok = 0, bad = 0;

   fprintf(stderr, "读 %s @9600,扫 0x55。Ctrl-C 退出。\n", path);

   for (;;) {
       struct pollfd p;
       p.fd = fd;
       p.events = POLLIN;
       p.revents = 0;
       int pr = poll(&p, 1, 200);
       if (pr < 0) {
           if (errno == EINTR)
               continue;
           fprintf(stderr, "poll: %s\n", strerror(errno));
           break;
       }
       if (pr == 0)
           continue;
       if (!(p.revents & POLLIN))
           continue;

       unsigned char tmp[128];
       int r = (int)read(fd, tmp, sizeof(tmp));
       if (r < 0) {
           if (errno == EAGAIN || errno == EWOULDBLOCK)
               continue;
           fprintf(stderr, "read: %s\n", strerror(errno));
           break;
       }
       if (r == 0)
           continue;
       if (nacc + r > (int)sizeof(acc)) {
           memmove(acc, acc + nacc / 2, (size_t)(nacc - nacc / 2));
           nacc -= nacc / 2;
       }
       if (nacc + r > (int)sizeof(acc))
           nacc = 0;
       memcpy(acc + nacc, tmp, (size_t)r);
       nacc += r;

       int i = 0;
       while (i + 11 <= nacc) {
           if (acc[i] != 0x55) {
               i++;
               continue;
           }
           if (acc[i + 1] != 0x51 && acc[i + 1] != 0x52 &&
               acc[i + 1] != 0x53) {
               i++;
               continue;
           }
           if (!checksum_ok(acc + i)) {
               bad++;
               i++;
               continue;
           }
           on_frame(acc + i);
           ok++;
           i += 11;
       }
       if (i > 0) {
           memmove(acc, acc + i, (size_t)(nacc - i));
           nacc -= i;
       }
       if ((ok + bad) > 0 && ((ok + bad) % 60) == 0)
           fprintf(stderr, "ok=%lu bad=%lu\n", ok, bad);
   }
   close(fd);
   return 0;
}

32 位 musl:

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 imu_uart imu_uart.c

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

file 若是 aarch64,exec 失败。那是 target 写错。不要用主机的 gcc。


9. 验收:轻晃 X/Y 变,Z 仍近 1 g

静置期望类似:

acc  ax=  0.020 ay= -0.015 az=  0.980 g
gyr  gx=    0.30 gy=   -0.20 gz=    0.10 dps
ang  roll=   1.20 pitch=  -0.80 yaw=  40.15 deg

az 在 0.9~1.1 g(或 -0.9~-1.1 g,看模组哪面朝上)。不要因为不是 0 去改公式。

绕 X 轴轻轻摇:ax 或 ay 明显偏离 0,az 仍在 1 g 附近。角速度对应轴出现十几到几百 dps,松手回落。yaw 慢漂可以接受。

ok 在涨、bad 接近 0。bad 很多、ok 没有:波特率不是 9600,或同排针还坐着别的 UART 模组。把 GPS/LoRa 拔掉再测。

打开失败对照:

open /dev/ttyS7: No such file or directory

DTS 没 okay,或分区没刷全。

tcgetattr: Not a tty

假节点。

open /dev/ttyS7: Permission denied

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

程序在跑、serial 的 rx 涨、打印没有 acc:你还在用旧的 select 二进制,没把这份 imu_uart 推上去。hdc shell md5sum /data/local/tmp/imu_uart 对一下。


10. 同排针、115200、以及不要给它发命令

同排针一次一颗。GPS 吐 $GNxxx 文本,LoRa 透传是另一套波特率,BLE 常见 115200。它们和六轴模组共享 TX/RX,谁都不是总线主机,同时插就是线与。验收六轴时旁边不要留着 GPS 的 VCC。

有人把 BLE 的 115200 原样拿来。open 成功,poll 也就绪,缓冲里全是采样错的字节,偶发 0x55,checksum 对不上,bad 飞涨。改回 9600,同一份程序立刻出 acc。

不要写“先发解锁再读”。这颗模组上电自己吐。主机 write 进 RX,模组可以忽略,也可以被噪声带偏。读空时去加发送,是在错误的一层打补丁。

cat /dev/ttyS7 当文本看,会把 0x55 和后面的二进制搅在一起,终端乱码,有的字节直接丢掉。要看内容用 od / hexdump,或者跑上面那份 C。应用里同样禁止按行、禁止滤非 ASCII——GPS 句子那套滤法在这里会把帧头扔掉。

UI 线程不要直接 poll。把 imu_uart 这段循环搬进 worker,主线程只收已经换算好的三个数。卡死一次,再改回阻塞 read,会把 ① 的坑再踩一遍。

排障顺序:

1. od 有没有 55 51/52/53;serial 表 rx 是否在涨
2. 有数应用无:是不是还在用 O_NDELAY + select
3. 阻塞 read 卡 UI:VTIME 在这份驱动上无效
4. 正解:O_NONBLOCK + poll,读放 worker
5. 文本模式丢掉 0x55;关 CRTSCTS
6. open 失败:0666;不要占用 ttyS2
7. 同排针一次一颗六轴模组

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐