六轴传感器Motion驱动框架开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
无线透传告一段落。今天换六轴:吐的是二进制帧,不是文本。读法不对时内核计数在涨,应用一个字节都拿不到。



模块插上,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–3 | X,int16 小端 |
| 4–5 | Y,int16 小端 |
| 6–7 | Z,int16 小端 |
| 8–9 | 温度或保留,同样 int16 小端,别当加速度 |
| 10 | checksum = 前 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
更多推荐

所有评论(0)