人体感应的真脚找过了。今天接气体报警。很多模块只有一根数字脚,而且这根脚可能和 I2C 时钟是同一根。
MQ-2 看起来像「模拟传感器」:金属网罩、加热丝、旁边一颗比较器。很多人第一反应是去读 ADC,换算出浓度 ppm。在这块 RK3568 底板上,扩展排针没有把 SARADC 引出来给这个模块。你能用的只有比较器翻出来的那根DO(数字报警)。
所以本章的验收不是「屏幕上出现 237 ppm」,而是:预热之后,酒精棉凑到网罩 1~2 厘米,DO 翻转;拿开,DO 回去。就这一件事。
另一件会让你卡一天的事:实测这根 DO 常常落到GPIO11,也就是GPIO0_B3。这颗球同时是I2C1 的 SCL。红外测温 MLX90614 挂在 i2c1 上时,这根脚属于 i2c 控制器,sysfs 导出 GPIO11 会失败。MQ-2 和红外测温不要同一时刻抢这根脚。
1. 通信怎么走
模块上典型有三块:气敏元件(要加热)、比较器、排针。比较器把模拟电压和电位器阈值比一下,OUT 翻成数字。
酒精 / 烟雾分子 │ 凑近金属网罩 1~2 cm ▼ 气敏元件(加热丝要先预热) │ 模拟电压(本底板没有引到 SARADC) ▼ 板上比较器 │ DO:超过阈值翻转 ▼ SoC GPIO(实测常为 GPIO0_B3 = linux 11) │ ▼ /sys/class/gpio/gpio11/value │ ▼ C 监测工具 / ArkTS 页面AO(模拟)脚即使模块上有焊盘,底板这组排针也对不到fe720000.saradc的通道。第 29 章的 SARADC 在板载按键和专用 ADC 端子上。MQ-2 页面上如果留一个「读浓度」按钮,点下去找不到通道是预期,不要当故障修。
电位器在模块上,拧它改变报警阈值,不改变「有没有 DO 这根脚」。软件读不到浓度,硬件仍可能用电位器让它更灵敏或更钝。
2. 预热:加热丝常亮不是着火
上电之后金属网罩下面那根丝会红。这是加热,不是故障,更不是短路。数据手册要求预热,现场经验是2~5 分钟以后比较器才稳。刚上电立刻凑酒精,DO 可能完全不动,你会以为脚号又错了。
正确上电顺序:
- 插好模块,确认 VCC、GND。5 V 供电的模块不要接到 3.3 V 逻辑脚上当电源。
- 上电,看到加热丝亮。
- 走开,等至少两分钟,最好五分钟。
- 再开监测,用酒精棉凑网罩。
预热期内页面提示写成:「加热丝常亮正常,请等待预热,不要用明火。」不要弹「传感器异常」。
禁止:
- 打火机火焰直接烧网罩。气敏元件会坏,也有安全问题。
- 把模块塞进密闭盒子里长时间加热不管。
- 用口吹气当测试。隔半米吹一口气,浓度到不了阈值。
推荐刺激源:酒精棉或沾了酒精的棉签,直接凑近金属网罩 1~2 厘米,停一两秒。烟雾也可以,同样要近。厨房里隔着一米「比划一下」无效。
3. GPIO 编号再算一次
第 39 章的公式这里还用:
linux 编号 = bank × 32 + 组内偏移 + 位号 A=0 B=8 C=16 D=24| 名字 | 计算 | 编号 | 备注 |
|---|---|---|---|
| GPIO0_B3 | 0×32+8+3 | **11** | 实测 DO;同时是 I2C1 SCL |
| GPIO0_B4 | 0×32+8+4 | 12 | 同时是 I2C1 SDA |
| GPIO2_B2 | 2×32+8+2 | 66 | 曾被误判为 DO,复测不动 |
| GPIO2_B1 | 2×32+8+1 | 73 | PIR,不是 MQ-2 |
/sys/kernel/debug/gpio里 gpiochip0 是 GPIO0,编号 0~31。11 就在这颗 chip 上。
为什么 11 会和 I2C 打架:i2c1 的 pinctrl 组i2c1_xfer就是 GPIO0_B3 / GPIO0_B4。设备树&i2c1 { status = "okay"; pinctrl-0 = <&i2c1_xfer>; }一生效,这两根脚属于 i2c 控制器。再echo 11 > export,内核返回 busy。
第 35 章红外测温、第 34 章环境光、第 36 章血氧、第 37 章温湿度、第 38 章手势,都挂 i2c1。它们活着的时候,不要指望 GPIO11 还能当普通输入。
4. 脚号是怎么扫出来的,以及第一次扫错
不要一上来就信某张「模块引脚表」。扩展排针丝印、原理图连接器号、设备树注释,三份对不上是常态。PIR 已经示范过一次。MQ-2 再示范一次,而且第一次扫还扫错了。
4.1 候选脚
把该插槽能碰到的、当前没被矩阵键盘等驱动占死的 GPIO 列出来。现场能export成功的包括 32、33、66、67、73、85、86,以及 11、12(若 i2c1 没占用)。被 keypad pinctrl 占住的 GPIO1 那一组export直接失败,跳过。
4.2 扫描脚本
放到板上/data/local/tmp/scan_do.sh:
#!/system/bin/sh # 用法:scan_do.sh 11 66 32 33 # 每 200 ms 打一行各脚电平。凑酒精时盯着哪一列在翻。 PINS="$*" [ -z "$PINS" ] && PINS="11 66 32 33 73" for g in $PINS; do [ -d /sys/class/gpio/gpio$g ] || echo $g > /sys/class/gpio/export 2>/dev/null echo in > /sys/class/gpio/gpio$g/direction 2>/dev/null done echo "time $(echo $PINS | tr ' ' '_')" i=0 while [ $i -lt 80 ]; do line="$i" for g in $PINS; do if [ -f /sys/class/gpio/gpio$g/value ]; then v=$(cat /sys/class/gpio/gpio$g/value) else v="x" fi line="$line $v" done echo "$line" i=$((i + 1)) usleep 200000 donehdc file send scan_do.sh /data/local/tmp/scan_do.sh hdc shell chmod 0755 /data/local/tmp/scan_do.sh hdc shell "/data/local/tmp/scan_do.sh 11 66 32 33"预热完成后开始跑。跑到一半,酒精棉凑上网罩,再拿开。看输出哪一列跟着变。
4.3 第一次误判:66
某次扫描里,GPIO66 那一列出现过一次 0→1。当时认定 DO=66,应用默认填了 66,极性还写成高电平报警。客户现场凑烟,页面不动。
复测:同样的脚本再跑三遍。66 全程:
1 1 1 1 1 1 1 1 1 1酒精棉贴上去,66 仍是 1。那一次「翻转」是接触抖动或者读的瞬间碰到了别的脚。单次跳变不能当结论。
4.4 复测:11 才跟着烟走
同一段日志里 GPIO11 在凑近时变成:
1 1 1 1 0 0 0 0 1 1 1拿开酒精棉,回到 1。再凑一次,再变 0。三遍稳定。极性是空闲 1、报警 0。
所以:
- 默认 DO 填11;
- 判定
v === 0为报警(以这颗模块为准); - 页面必须是连续监测,不能「点一下读一次」——报警可能只持续一两秒,单次采样会错过。
扫描纪律:
- 预热后再扫。
- 连续轮询,不要
cat一次就下结论。 - 同一刺激源至少复现三次。
- 拔掉模块再读候选脚:如果拔掉后仍按「报警」节奏自己翻,那根脚不是 DO。
- 写下极性,写进页面提示,不要只写在聊天记录里。
5. 和 I2C1 抢脚:export 失败不是脚本写错
GPIO11 = GPIO0_B3 = I2C1_SCL。设备树默认&i2c1 { status = "okay"; },因为环境光、红外测温、血氧都在这条总线上。
此时:
hdc shell "echo 11 > /sys/class/gpio/export"典型输出:
sh: write error: Device or resource busydebugfs:
hdc shell "cat /sys/kernel/debug/gpio" | grep gpio-11可能看到这根脚的名字是 i2c 相关,而不是sysfs。pinctrl:
hdc shell "cat /sys/kernel/debug/pinctrl/pinctrl-rockchip/pinmux-pins" | grep -i 'gpio0-11\|0-b3\|i2c1'会看到i2c1_xfer占用。
board-perm.sh里对 11 的导出失败,串口会出现:
[BOARD_PERM] gpio11: 导出失败。73 查 pinctrl;11 查是否被 i2c1 占用这不是脚本 bug。三条路,选一条:
路 A:测 MQ-2 时关掉 i2c1。设备树临时&i2c1 { status = "disabled"; },重打 dtb /boot_linux。红外测温、环境光、血氧、手势这一组全部消失。测完 MQ-2 改回 okay。这和 CAN 镜像是同一类「共脚二选一」,见第 33 章、第 58 章。
路 B:DO 改接到另一根空闲 GPIO。如果模块排针允许你把 OUT 飞到 73 以外的空脚(不要和 PIR 抢 73),软件把编号改成那根脚。硬件改动要画清楚,不要口头传说。
路 C:不要同时测。交付演示分时段:先演示 I2C 传感器,再切镜像或改接线演示 MQ-2。页面上写明「GPIO11 被 i2c1 占用时读不到 DO」。
不要走「强行 unbind i2c 驱动再 export」这种演示技巧。能临时通,重启又回去,客户无法复现。
CAN0 的 TX/RX 也是 GPIO0_B3/B4。开 CAN 的镜像同样会让 GPIO11 消失。MQ-2、I2C1 传感器、CAN,三选一坐这组脚。
6. 开机预导出:沿用第 39 章脚本
HAP 依然写不了export。第 39 章的board-perm.sh已经循环for g in 11 73。这里把和 MQ-2 相关的部分再强调一遍,脚本全文仍以第 39 章为准,不要维护两份互相覆盖的版本。
关键片段:
# 11 = MQ-2 DO(GPIO0_B3,⚠与 i2c1 SCL 共球) # 73 = PIR OUT(GPIO2_B1) for g in 11 73; do [ -d /sys/class/gpio/gpio$g ] || echo $g > /sys/class/gpio/export 2>/dev/null echo in > /sys/class/gpio/gpio$g/direction 2>/dev/null chmod 0666 /sys/class/gpio/gpio$g/value 2>/dev/null chmod 0666 /sys/class/gpio/gpio$g/direction 2>/dev/null done for g in 11 73; do if [ -d /sys/class/gpio/gpio$g ]; then log "gpio$g: direction=$(cat /sys/class/gpio/gpio$g/direction) value=$(cat /sys/class/gpio/gpio$g/value)" else log "gpio$g: 导出失败。73 查 pinctrl;11 查是否被 i2c1 占用" fi doneinit 配置、BUILD.gn、刷 vendor 分区,全部和第 39 章同一套。改脚本只重打vendor.img,刷 p8。只装 HAP 不够。
临时、没重打镜像时:
hdc shell "echo 11 > /sys/class/gpio/export" hdc shell "echo in > /sys/class/gpio/gpio11/direction" hdc shell "chmod 0666 /sys/class/gpio/gpio11/value"若第一步 busy,先解决 i2c1,不要在应用里死循环 export。
7. 完整 C:mq2_watch.c
和 PIR 的gpio_watch同类,但带极性提示、busy 诊断,以及「预热倒计时」口吻。可以和上一章共用同一个二进制,只是默认脚和文案不同。这里给一份独立文件,复制就能编。
/* mq2_watch.c * OpenHarmony 32 位 musl。 * 用法: * mq2_watch # 默认 gpio11,间隔 200 ms * mq2_watch 11 200 * mq2_watch 11 200 0 # 第三个参数:报警电平,0=低有效(本模块实测) */ #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static int gpio_exported(int num) { char dir[64]; snprintf(dir, sizeof(dir), "/sys/class/gpio/gpio%d", num); return access(dir, F_OK) == 0; } static int gpio_export(int num) { if (gpio_exported(num)) return 0; char buf[8]; int len = snprintf(buf, sizeof(buf), "%d", num); int fd = open("/sys/class/gpio/export", O_WRONLY); if (fd < 0) { fprintf(stderr, "open export: %s\n", strerror(errno)); fprintf(stderr, "HAP 会在这里失败;命令行若也失败,查是否 root。\n"); return -1; } ssize_t n = write(fd, buf, (size_t)len); int err = errno; close(fd); if (n < 0) { fprintf(stderr, "export gpio%d: %s\n", num, strerror(err)); if (err == EBUSY && num == 11) { fprintf(stderr, "gpio11=GPIO0_B3=I2C1_SCL。i2c1 okay 时不能当 DO。\n" "关掉 i2c1,或把 DO 改接到空闲脚,或不要同时测红外测温。\n"); } return -1; } usleep(50000); return 0; } static int write_str(const char *path, const char *s) { int fd = open(path, O_WRONLY); if (fd < 0) return -1; ssize_t n = write(fd, s, strlen(s)); close(fd); return n < 0 ? -1 : 0; } static int gpio_read(int num) { char path[64]; snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", num); int fd = open(path, O_RDONLY); if (fd < 0) return -1; char c = 0; ssize_t n = read(fd, &c, 1); close(fd); if (n <= 0) return -1; return (c == '1') ? 1 : 0; } int main(int argc, char **argv) { int num = (argc >= 2) ? atoi(argv[1]) : 11; int interval = (argc >= 3) ? atoi(argv[2]) : 200; int alarm_level = (argc >= 4) ? atoi(argv[3]) : 0; /* 本模块实测低有效 */ printf("MQ-2 DO watch gpio%d interval=%dms alarm_when=%d\n", num, interval, alarm_level); printf("预热 2-5 分钟;加热丝亮是正常的。酒精棉凑网罩 1-2cm。\n"); printf("禁止明火。隔空吹气无效。本底板无 ADC,只认 DO。\n"); if (gpio_export(num) < 0) return 1; char dirp[64]; snprintf(dirp, sizeof(dirp), "/sys/class/gpio/gpio%d/direction", num); if (write_str(dirp, "in") < 0) { fprintf(stderr, "direction: %s\n", strerror(errno)); return 1; } int last = -1; int flips = 0; int alarms = 0; int i = 0; for (;;) { int v = gpio_read(num); if (v < 0) { fprintf(stderr, "read: %s(value 是否 0666?)\n", strerror(errno)); return 1; } if (last >= 0 && v != last) flips++; int is_alarm = (v == alarm_level); if (is_alarm) alarms++; printf("[%03d] gpio%d=%d %s flips=%d\n", i, num, v, is_alarm ? "ALARM" : "idle ", flips); last = v; i++; usleep((useconds_t)interval * 1000); } return 0; }编译:
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 mq2_watch mq2_watch.c file mq2_watch hdc file send mq2_watch /data/local/tmp/mq2_watch hdc shell chmod 0755 /data/local/tmp/mq2_watch预热后:
hdc shell "/data/local/tmp/mq2_watch 11 200 0"空闲应连续gpio11=1 idle。酒精棉凑上:
[042] gpio11=1 idle flips=0 [043] gpio11=1 idle flips=0 [044] gpio11=0 ALARM flips=1 [045] gpio11=0 ALARM flips=1 [046] gpio11=1 idle flips=2flips增加,功能通。若全程idle:
- 还在预热;
- 酒精太远;
- 脚不是 11(再跑
scan_do.sh); - 极性反了,把第三个参数改成
1试; - 电位器拧到极钝。
若一启动就export ... Busy,停,去看 i2c1,不要改 C 里的重试次数。重试消除不了共脚。
对比复测 66:
hdc shell "/data/local/tmp/mq2_watch 66 200 0"酒精棉贴上去,flips仍接近 0。不要把 66 写回默认值。
8. NAPI 仍然只用 gpioRead
和 PIR 共用libboard.so的gpioRead。不要为 MQ-2 再开一个 ioctl。不要在 so 里写 ADC 换算假装有浓度。
/* 与第 39 章相同:只 open value,不 export */ static napi_value GpioRead(napi_env env, napi_callback_info info) { size_t argc = 1; napi_value a[1]; napi_get_cb_info(env, info, &argc, a, nullptr, nullptr); int num = -1; napi_get_value_int32(env, a[0], &num); int val = -1; if (num >= 0) { char p[64]; snprintf(p, sizeof(p), "/sys/class/gpio/gpio%d/value", num); int fd = open(p, O_RDONLY); if (fd >= 0) { char c = 0; if (read(fd, &c, 1) > 0) val = (c == '1') ? 1 : 0; close(fd); } } napi_value ret; napi_create_int32(env, val, &ret); return ret; }返回-1时,ArkTS 要把「i2c1 占用 GPIO11」和「没放权」分成两句人话,否则现场会去重编应用。
9. ArkTS 页面
import board from 'libboard.so'; @Entry @Component struct Mq2Page { @State gpioNum: string = '11'; @State live: number = -1; @State watching: boolean = false; @State hint: string = '加热丝常亮正常。预热 2~5 分钟后点监测,酒精棉凑网罩 1~2cm。'; @State alarmLow: boolean = true; /* 实测 GPIO11:0=报警 */ private timer: number = -1; aboutToDisappear() { this.stop(); } statusText(): string { if (this.live < 0) { return 'DO: -'; } let alarm: boolean = this.alarmLow ? (this.live === 0) : (this.live === 1); return alarm ? '报警(检测到气体)' : '正常(无报警)'; } toggle() { if (this.watching) { this.stop(); this.hint = '已停止'; return; } let n: number = parseInt(this.gpioNum); if (Number.isNaN(n)) { this.hint = '先填 DO 的 GPIO 号'; return; } let probe: number = board.gpioRead(n); if (probe < 0) { this.hint = 'GPIO' + n + ' 读不到。两种可能:\n' + '1) 开机 board-perm 没预导出 / value 不是 0666,需重打 vendor;\n' + '2) GPIO11 是 I2C1 SCL,红外测温占用时 export 失败,二者不要同时测。'; return; } this.watching = true; this.live = probe; this.hint = '监测中。酒精棉贴网罩 1~2cm,看电平翻不翻。远距离吹气无效。'; this.timer = setInterval(() => { let v: number = board.gpioRead(n); if (v >= 0) { this.live = v; } }, 300); } stop() { if (this.timer >= 0) { clearInterval(this.timer); this.timer = -1; } this.watching = false; } build() { Column({ space: 10 }) { Text('气体传感器 MQ-2').fontSize(20).fontWeight(FontWeight.Bold) Text('只读 DO,本排针没有 ADC。不要指望 ppm 读数。').fontSize(13) Text('加热丝常亮是预热。禁止明火烧网罩。').fontSize(13) Row() { Text('DO GPIO').margin({ right: 8 }) TextInput({ text: this.gpioNum }) .width(80) .onChange((v: string) => { this.gpioNum = v; }) Button(this.watching ? '停止监测' : '开始监测') .onClick(() => { this.toggle(); }) .margin({ left: 8 }) } Button(this.alarmLow ? '极性:低电平=报警(点此对调)' : '极性:高电平=报警(点此对调)') .onClick(() => { this.alarmLow = !this.alarmLow; }) Text('电平: ' + (this.live < 0 ? '-' : this.live.toString())).fontSize(18) Text(this.statusText()).fontSize(22).fontWeight(FontWeight.Bold) Text(this.hint).fontSize(14) } .padding(16) .width('100%') } }监测间隔 300 ms 和 PIR 一样。气体报警沿不是微秒级。单次「读 DO」按钮可以留,但文档要写:报警脉冲短,点不准。演示一律走监测。
不要在页面上放一个 ADC 通道输入还声称「浓度」。若留调试入口,注明「本槽无 SARADC,读到的是板载通道,与 MQ-2 无关」。
10. 排障过程
按这个顺序,不要先改应用默认脚号来回试。
1. 加热丝亮不亮。
不亮:没供电、插反、模块坏。亮:进入预热等待。看表两分钟起。
2. i2c1 还在不在。
hdc shell "ls -l /sys/bus/i2c/devices/i2c-1" hdc shell "cat /proc/device-tree/i2c@fe5c0000/status" # 节点名以你板子 /proc/device-tree 实际路径为准 hdc shell "i2cdetect -y -r 1"i2cdetect能扫到0x5a(MLX90614)说明 i2c1 活着,GPIO11 大概率 busy。先决定这次要测哪一个。
3. export 11。
hdc shell "echo 11 > /sys/class/gpio/export" hdc shell "ls -l /sys/class/gpio/gpio11/value"busy → 回步骤 2。成功 →direction设in,chmod 0666。
4. 空闲值。
人、酒精、烟雾都远离。连续读 20 次。记下多数值,那是 idle。本模块常见 1。
5. 酒精棉 1~2 cm。
mq2_watch 11 200 0跑着,棉签贴网罩。要看到ALARM。拿开回到idle。复现三次。
6. 对照 66。
同样刺激,66 不应跟着翻。防止旧默认值混进页面。
7. HAP -1。
hdc shell "ls -l /sys/class/gpio/gpio11/value" hdc shell "grep BOARD_PERM /proc/kmsg" hdc shell "ls -l /vendor/bin/board-perm.sh"value不是 0666,HAP 读失败。root 的mq2_watch能跑、HAP 不能,就是放权问题,不是脚号问题。
8. HAP 能读但不报警。
- 预热不够;
- 棉签太远;
- 极性反了,点页面「对调」;
- 电位器过钝;
- 监测没开,只点了一次读。
9. 误报不停。
电位器过灵、旁边有酒精瓶敞口、预热未完输出乱跳。先拿开所有刺激源,等预热结束,看 idle 是否稳定。稳定后再谈阈值。
11. 和别的章的关系
| 章 | 关系 |
|---|---|
| 33 I2C | i2c1 占用 GPIO0_B3/B4,MQ-2 用 11 时要让路 |
| 35 MLX90614 | 同抢 I2C1 SCL,不要同时测 |
| 39 PIR | 同一套 `board-perm.sh`、同一个 `gpioRead`;脚是 73,不是 11 |
| 29 SARADC | MQ-2 这组排针用不上;别把按键通道当浓度 |
| 58 CAN | can0m0 也是 GPIO0_B3/B4,三选一 |
演示脚本建议写成时段表,不要指望一张镜像、一个插槽把 I2C 传感器、MQ-2、CAN 同时点亮。共脚不是软件 bug。
12. 验收
同时满足才算过:
- 加热丝上电常亮,预热满 2 分钟(演示给 5 分钟更稳)。
gpio11能导出,或你已经改接到另一根空闲脚并在页面写明编号。- 空闲连续 10 秒电平稳定。
- 酒精棉或烟雾1~2 cm凑近网罩,DO 翻转;拿开后恢复。复现三次。
- 远距离吹气不要求翻转。
- 未使用明火。
- HAP 监测模式能显示报警/正常,且 HAP 自己没有写
export。 - 若 i2c1 保持 okay,允许 GPIO11 导出失败,但页面必须给出「与红外测温抢脚」的说明,而不是空白 -1。
- 不把 ADC 读数当作本模块浓度指标。
第 8 条是给「默认交付镜像开着 i2c1」留的口。你可以选择交两份镜像,也可以选择演示时拔掉 I2C 模块、关掉 i2c1。选哪条写进你的说明,不要让客户自己猜。
本章小结
MQ-2 在这块底板上是DO 数字报警,不是浓度计。加热丝亮、预热数分钟是正常的。刺激源贴网罩 1~2 厘米。实测 DO 常为GPIO11,空闲 1、报警 0,仍以监测为准。GPIO11 与 I2C1 SCL 共球,和红外测温冲突时换时段或换脚。扫描脚号要连续轮询并复测,66 那次误判不要再传。HAP 继续走第 39 章的预导出。
路径清单
| 路径 | 干什么 |
|---|---|
| `device/board/rk/rk3568_evb/kernel/*.dts` 的 `&i2c1` | okay 时 GPIO11 属于 SCL |
| `device/board/rk/rk3568_evb/board_perm/board-perm.sh` | 预导出 11 和 73 |
| `/sys/class/gpio/gpio11/value` | DO 电平 |
| `/sys/kernel/debug/gpio` | 看 11 被谁占 |
| `/data/local/tmp/mq2_watch` | 命令行监测 |
| `/data/local/tmp/scan_do.sh` | 扫脚,复测三次 |
| `libboard.so` `gpioRead` | HAP 只读 |
系列第 40 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10