温湿度传感器驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
血氧的读法见过了。今天是温湿度:要先触发转换再读。有的料在总线上根本不应答,那是硬件边界。
同一条 i2c1 上,环境光 0x10 能出数,红外测温 0x5a 能出数,血氧 0x57 轮询 FIFO 也能出数。轮到 HDC1080,地址 0x40,很多人会在这里浪费两天。
症状很统一:设备树写了节点,/sys/bus/i2c/devices/1-0040 在,driver 软链指着 hdc100x,IIO 也枚举出来了。一读温度,ENXIO。dmesg 里有一句冷冰冰的:
hdc100x 1-0040: cannot set seq config
这句话的意思不是「序列模式配错了再改一版驱动就好」。意思是:驱动往 0x40 写配置寄存器,芯片连 ACK 都没有。 总线在,地址在设备树里,硅片没说话。
这章把协议讲死,把「组合读会重触发」这个坑讲死,再把「部分板子 0x40 完全 NACK」定性成硬件。软件层不要空转。同类芯片可以看 AHT20,地址 0x38——很多底板把温湿度备在这颗上,DTS 里常常 disabled,因为 HDF 传感器模型想独占。


1. 先把一次失败走完,免得你重复交学费
工程上真实发生过的顺序,我按时间摊开。你对照自己的日志,卡在哪一步就从哪一步跳出来。
第一天,按 IIO 习惯找名字。
hdc shell
for d in /sys/bus/iio/devices/iio:device*; do
echo -n "$d "
cat $d/name
done
环境光叫 veml6040,红外叫 mlx90614,温湿度呢?没有 hdc1080 这几个字母。有人以为驱动没编进来,打开 menuconfig,打开 CONFIG_HDC100X,重编内核,刷 boot_linux。半小时后名字还是没有 hdc1080。
因为它就叫 1-0040:i2c 总线号 + 7 位地址。hdc100x.c 把 indio_dev->name 设成了 client 的名字,不是芯片型号字符串。findIio("hdc1080") 永远空,findIio("0040") 才能撞上。
第二天,找到了 1-0040,读 in_temp_raw 恒 ENXIO。
去看驱动。probe 里写序列模式配置,返回值没查。写失败照样 iio_device_register。于是 sysfs 里设备「在」,第一次真正读才暴露 NACK。dmesg 翻到:
hdc100x 1-0040: cannot set seq config
有人开始改驱动:序列模式 ACQ_MODE 翻来覆去、msleep(20) 加长、每次读前重写 config。两次内核重建,现象不变。
第三天,放弃 IIO,用户态 /dev/i2c-1 按手册裸跑。
写 config 到 0x02,ioctl 返回 -1。和内核走的是同一条电气通路。0x10、0x57、0x5a 在同一根 SDA/SCL 上 ACK 正常。结论到这里才能下:0x40 这颗在这颗模块上电气不应答。 量芯片 VDD 脚(不是连接器上的 3.3 V 总线上),看独立 LDO 有没有出来。虚焊、LDO 没焊、芯片本身死,都不是 C 能救的。
如果你的板子写 config 是成功的,后面的协议章节对你有用。如果是 NACK,读到第 8 节「定性硬件」就可以停,去 AHT20。
2. 通信怎么走
HDC1080 是 I2C,但转换时序像「启动一次 ADC,过一会儿来取结果」。
寄存器(16 位宽,高字节先):
| 指针 | 含义 |
|---|---|
| 0x00 | 温度。写这个指针 = 触发转换 |
| 0x01 | 湿度。单独测湿时写它 |
| 0x02 | 配置。bit12 是序列模式 ACQ_MODE:1 表示一次触发温+湿 |
序列模式(本章用的)时序:
主机 HDC1080
| |
|--S 0x40+W 0x02 0x10 0x00 P--> 写 config = 0x1000
| |
|--S 0x40+W 0x00 P------> 只写指针,不带数据,开始转
| | 内部 ADC,约 15 ms 温 + 湿
| (等 20 ms) |
|--S 0x40+R 4 bytes P--> 裸读:T_H T_L H_H H_L
换算:
T = rawT / 65536 * 165 - 40 单位 ℃
H = rawH / 65536 * 100 单位 %RH
rawT、rawH 都是大端 16 位。
禁止组合读。 组合读是:
S 0x40+W 0x00 Sr 0x40+R 4bytes P
对很多 EEPROM 风格的芯片这叫「设地址再读」。对 HDC1080,写 0x00 本身就是触发。你组合读的时候又写了一次 0x00,芯片刚转完(或者还在转)被重新砸进转换,读的那一拍总线上空的,NACK 或者读到 0。手册要求触发之后只发读,不再发寄存器地址。第 33 章的 i2cRdPlain 就是为它准备的。
单独测温度也可以:config 不打开序列位,写指针 0x00,等 7~15 ms,裸读 2 字节。本章用序列模式一次拿温湿,少一次总线事务。
3. 设备树和内核驱动
device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts
kernel/linux/linux-5.10/drivers/iio/humidity/hdc100x.c
&i2c1 {
status = "okay";
clock-frequency = <100000>; /* 400 kHz 上这颗也不稳 */
hdc1080: hdc1080@40 {
compatible = "ti,hdc1080";
reg = <0x40>;
status = "okay";
};
/* 同类:AHT20 / DHT20,地址 0x38。
* 若 HDF 传感器模型要独占,内核节点保持 disabled。 */
aht20: aht20@38 {
compatible = "aosong,aht20";
reg = <0x38>;
status = "disabled";
};
};
compatible 用 ti,hdc1080 或 ti,hdc100x,驱动同一份。CONFIG_HDC100X=y 三处同步。
打开驱动看 probe,你要自己确认那句「写 config 失败仍然注册」:
/* hdc100x.c 里大意如此,行号随版本浮动 */
if (hdc100x_update_config(data) < 0)
dev_err(&client->dev, "cannot set seq config");
/* 注意:这里常常没有 return 那个错误,后面照样 iio_device_register */
所以验收 不要 以「有没有 IIO 设备」为准,以「写 0x02 成不成功」为准。
i2c1 上 0x40 还有别的居民:RK8600 一类 CPU 供电芯片在 i2c0 上也是 0x40,那是另一条总线,别看花眼。i2cdetect -y -r 1 看到的 40 才是这颗温湿度。
4. 完整 C:照手册跑,写 config 失败立刻退出
存成 hdc1080_tool.c。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#ifndef I2C_RDWR
#define I2C_RDWR 0x0707
#endif
#ifndef I2C_M_RD
#define I2C_M_RD 0x0001
#endif
struct i2c_msg {
uint16_t addr;
uint16_t flags;
uint16_t len;
uint8_t *buf;
};
struct i2c_rdwr_ioctl_data {
struct i2c_msg *msgs;
uint32_t nmsgs;
};
#define ADDR 0x40
#define REG_TEMP 0x00
#define REG_CONF 0x02
static int i2cWr(int fd, uint8_t addr, uint8_t reg, const uint8_t *data, int len)
{
uint8_t b[24];
int i;
b[0] = reg;
for (i = 0; i < len && i < 23; i++)
b[1 + i] = data[i];
struct i2c_msg m = { addr, 0, (uint16_t)(len + 1), b };
struct i2c_rdwr_ioctl_data d = { &m, 1 };
return ioctl(fd, I2C_RDWR, &d);
}
static int i2cRdReg(int fd, uint8_t addr, uint8_t reg, uint8_t *data, int len)
{
struct i2c_msg m[2] = {
{ addr, 0, 1, ® },
{ addr, I2C_M_RD, (uint16_t)len, data }
};
struct i2c_rdwr_ioctl_data d = { m, 2 };
return ioctl(fd, I2C_RDWR, &d);
}
/* 裸读:S addr+R data... P。指针必须已经在芯片里。 */
static int i2cRdPlain(int fd, uint8_t addr, uint8_t *data, int len)
{
struct i2c_msg m = { addr, I2C_M_RD, (uint16_t)len, data };
struct i2c_rdwr_ioctl_data d = { &m, 1 };
return ioctl(fd, I2C_RDWR, &d);
}
static int read_once(int fd, double *t, double *h, int use_combined)
{
uint8_t cfg[2] = { 0x10, 0x00 }; /* 0x1000,序列模式 */
uint8_t buf[4] = { 0, 0, 0, 0 };
unsigned tr, hr;
int r;
r = i2cWr(fd, ADDR, REG_CONF, cfg, 2);
if (r < 0) {
fprintf(stderr,
"ERR:HDC1080 写 config 被拒 (0x40 在 i2c1 不应答)\n"
"同总线 0x10/0x57/0x5a 若正常,这是硬件,不要再改软件。\n");
return -1;
}
/* 触发:只写指针 0x00,len=0 */
r = i2cWr(fd, ADDR, REG_TEMP, NULL, 0);
if (r < 0) {
fprintf(stderr, "ERR:HDC1080 写指针 0x00 被拒\n");
return -1;
}
usleep(20000);
if (use_combined) {
/* 错误示范:会重触发。工具加这个开关,只为让你看它怎么死。 */
r = i2cRdReg(fd, ADDR, REG_TEMP, buf, 4);
if (r < 0) {
fprintf(stderr, "ERR:组合读被拒(预期中的失败,改回裸读)\n");
return -1;
}
} else {
r = i2cRdPlain(fd, ADDR, buf, 4);
if (r < 0) {
fprintf(stderr, "ERR:HDC1080 裸读 4 字节被拒\n");
return -1;
}
}
tr = ((unsigned)buf[0] << 8) | buf[1];
hr = ((unsigned)buf[2] << 8) | buf[3];
*t = tr / 65536.0 * 165.0 - 40.0;
*h = hr / 65536.0 * 100.0;
printf("raw T=%u H=%u -> 温度 = %.1f C 湿度 = %.0f %%RH\n",
tr, hr, *t, *h);
if (tr == 0 && hr == 0)
printf("注意: 全 0,像没转完或读错方式。确认没用组合读、等够 20 ms。\n");
return 0;
}
int main(int argc, char **argv)
{
const char *dev = "/dev/i2c-1";
int combined = 0;
int loop = 0;
int fd, i;
double t = 0, h = 0;
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "combined") == 0)
combined = 1;
else if (strcmp(argv[i], "loop") == 0)
loop = 1;
}
fd = open(dev, O_RDWR);
if (fd < 0) {
fprintf(stderr, "open %s: %s (chmod 0666?)\n", dev, strerror(errno));
return 1;
}
do {
if (read_once(fd, &t, &h, combined) < 0) {
close(fd);
return 1;
}
if (loop)
sleep(1);
} while (loop);
close(fd);
return 0;
}
编译与第 36 章同一套工具链:
OHOS=$HOME/code/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 hdc1080_tool hdc1080_tool.c
hdc file send hdc1080_tool /data/local/tmp/hdc1080_tool
hdc shell chmod 0755 /data/local/tmp/hdc1080_tool
hdc shell chmod 0666 /dev/i2c-1
活芯片的期望:
hdc shell /data/local/tmp/hdc1080_tool
# 温度 = 26.3 C 湿度 = 48 %RH
hdc shell /data/local/tmp/hdc1080_tool loop
# 对着芯片呼气,湿度往上爬,离开回落
死芯片的期望,必须是 ERR,不许算出 25 ℃ 这种「看起来合理」的假数:
hdc shell /data/local/tmp/hdc1080_tool
# ERR:HDC1080 写 config 被拒 (0x40 在 i2c1 不应答)
故意用组合读,活芯片也可能翻车:
hdc shell /data/local/tmp/hdc1080_tool combined
# ERR:组合读被拒
# 或 raw 全 0
把这条留在实验记录里:同一颗活芯片,只换读方式就失败。以后有人说「I2C_RDWR 两段式更标准」,你把这条命令甩给他。
5. 排障:从 IIO 名字到芯片脚
5.1 总线还在吗。
i2cdetect -y -r 1
10、57、5a 在,40 是 --:芯片没 ACK,后面全部是硬件问题。40 显示 UU:内核驱动占着,用户态通常仍能 I2C_RDWR;若不能,unbind:
echo 1-0040 > /sys/bus/i2c/drivers/hdc100x/unbind
5.2 dmesg 一句定音。
dmesg | grep -i hdc
cannot set seq config 出现过,就不要再猜 IIO 通道名、scale、offset。那是写 0x02 失败。用户态工具会打出同一件事的人话。
5.3 量电压。
连接器上的 3.3 V 有电,不代表芯片 VDD 脚有电。这类模块经常给温湿度单独一颗 LDO。表笔要挨着芯片电源脚。0 V:跟 LDO、虚焊。3.3 V 仍 NACK:芯片死或地址脚被焊成别的值(HDC1080 地址是固定 0x40,没有地址脚可改,所以更像死芯片或 SDA 没接到这颗)。
5.4 别在 400 kHz 上自欺。
i2c1 曾经 400 kHz,MLX90614 先罢工,HDC1080 也不稳。已经降到 100 kHz 仍然 NACK,就不是速率问题。不要把时钟来回改当进度。
5.5 IIO 读法(仅活芯片、且你想走内核时)。
BASE=/sys/bus/iio/devices/iio:deviceN # name 含 0040
cat $BASE/name
cat $BASE/in_temp_raw
cat $BASE/in_temp_offset
cat $BASE/in_temp_scale
cat $BASE/in_humidityrelative_raw
hdc100x 的换算和用户态公式同类,但 offset/scale 拆法以 sysfs 为准。读 raw 得到 ENXIO,立刻回头跑用户态工具,不要在 sysfs 上循环。
5.6 和 RK8600 搞混。
ls /sys/bus/i2c/devices/
# 0-0040 可能是 i2c0 上的供电芯片
# 1-0040 才是 i2c1 上的 HDC1080
对 0-0040 发温湿度协议,轻则 NACK,重则 CPU 供电被你写乱。工具写死 /dev/i2c-1。
6. ArkTS:一次读取,失败文案要诚实
index.d.ts:
export const hdc1080Read: () => string;
页面:
import board from 'libboard.so';
@Entry
@Component
struct HdcPage {
@State line: string = '点「读温湿度」。若写 config 被拒,是硬件,不要反复点。';
build() {
Column() {
Text('HDC1080 温湿度').fontSize(20).margin({ bottom: 8 })
Text('i2c1 地址 0x40。触发后裸读 4 字节,禁止组合读。')
.fontSize(14).fontColor('#666').margin({ bottom: 12 })
Button('读温湿度').onClick(() => {
this.line = board.hdc1080Read();
})
Text(this.line).fontSize(16).margin({ top: 16 }).fontColor('#1565C0')
}
.padding(16)
.width('100%')
.alignItems(HorizontalAlign.Start)
}
}
NAPI 把 read_once 包一层,成功返回 "温度 = 26.3 °C 湿度 = 48 %",失败把 ERR:HDC1080 写config被拒 原样交上去。不要 在失败时返回 "--" 或上次的 26 ℃。界面上要让人看见「被拒」。
循环刷新的话间隔 1 秒够了。每次循环都要走完整触发+20 ms+裸读,不能假设芯片会自己更新一个保持寄存器。
7. 同类对照:AHT20 @ 0x38
HDC1080 这条线若定性硬件,温湿度演示还可以走 AHT20(有的料单写成 DHT20,同一颗核)。地址 0x38,也在 i2c1。
协议和 HDC 不同,不要复用 i2cRdPlain 那套指针:
- 软复位、等 20 ms。
- 发触发命令
0xAC 0x33 0x00。 - 等 80 ms。
- 读 6~7 字节,里面是状态 + 20 bit 湿 + 20 bit 温。
DTS 里那颗 aht20@38 默认 disabled,常见原因:HDF 传感器模型要在 HCS 里声明同一地址,两边一起 okay 会抢。你走用户态 i2c-dev 时,保持内核节点 disabled,HCS 里也别同时开。只留 i2c1 控制器本身。
i2cdetect -y -r 1 在 0x38 看到 ACK,再写 AHT20 的工具。没 ACK 就同样是硬件或没贴这颗。本章不把 AHT20 的完整 C 展开,避免两套时序缠在一起。你要写的时候,独立文件 aht20_tool.c,不要在 hdc1080_tool.c 里加 if (chip==aht)。
8. 定性硬件之后做什么
写进测试记录,一行就够:
HDC1080 @ i2c1 0x40:写 config NACK。同总线 VEML6040/MAX30100/MLX90614 ACK。
dmesg: hdc100x 1-0040: cannot set seq config。用户态 I2C_RDWR 同样拒。
结论:硬件。不改驱动。备选 AHT20@0x38。
然后停。不要:
- 再降 I2C 速率到 50 kHz、10 kHz「试试看」;
- 把
compatible改成hdc1008碰运气; - 在 ArkTS 里 catch 住错误显示 25 ℃ 25% 以免「页面难看」;
- 为了过验收去模拟一组数。验收方拿同一套工具跑,假数撑不过现场。
活芯片的验收才是呼气湿度变。死芯片的验收是 工具稳定打出 ERR,记录里写硬件。两种都叫「这章做完了」。没做完的是:明明 NACK,还在改软件。
9. 坑
IIO 名字。 hdc1080 搜不到。搜 0040。更好的办法是根本不靠 IIO。
probe 成功幻觉。 驱动作者省了一次 return ret。你要用写 config 的返回值当活口。
组合读。 第 33 章把 i2cRdReg 讲得太好,到这颗芯片要收起来。HDC 用 i2cRdPlain。MAX30100 用 i2cRdReg。PAJ7620 用 i2cRdReg。按芯片选,不要按喜好选。
20 ms 不够就加到 30 ms,不要减到 5 ms。 手册给的转换时间是上限附近。提前去读,NACK 或全 0。
len=0 的写。 i2cWr(..., NULL, 0) 只发寄存器地址。有人改成发一个 0x00 数据字节,芯片理解成往温度寄存器写数据,行为未定义。
和 CPU 供电 0x40 抢脑子。 总线号必须是 1。
HDF 同时开。 若 HCS 里也声明了温湿度 I2C 从设备,用户态偶发 NACK。二选一。
呼气验收对着模块外壳吹。 要靠近传感窗口。隔着塑料壳,湿度变化慢,别一秒钟没变就判死。活芯片 loop 模式看趋势,10 秒窗口。
10. 验收
活芯片:
hdc1080_tool打出室温附近的温度(15~35 ℃ 这个宽范围),不是 0,不是 -40。loop对着窗口呼气,湿度上升至少几个百分点,离开后回落。combined参数下失败或全 0,证明你真的理解了裸读。- 应用按钮读一次,文案带单位 ℃ 和 %。
死芯片:
- 工具 ERR,dmesg 有
cannot set seq config(若内核驱动还绑着)。 i2cdetect上 0x10/0x57/0x5a 仍在。- 记录表写「硬件」,备选 AHT20 的地址 0x38 是否 ACK 另测一行。
- 应用显示 ERR 原文,不显示假温湿度。
对照:AHT20 在 0x38,协议不是这一套
HDC1080 判死之后,同总线常备一颗 AHT20(也有人写作 DHT20,I2C 地址一样是 0x38)。它不是「换个地址的 HDC1080」。
AHT20 典型流程:
- 软复位、等 20 ms 以上。
- 写初始化命令。
- 发触发测量,等 80 ms 量级。
- 读 6 字节,里面塞了状态、湿度 20 位、温度 20 位。
把它套进本章的「写 0x02 = 0x1000,再裸读 4 字节」,会得到一堆无意义的数,或者 NACK。DTS 里 aht20@38 常常 disabled,是因为 HDF 传感器模型想独占这颗。内核 IIO 和 HDF 同时 bind,必有一方失败。
对照实验只做一件事:
i2cdetect -y -r 1
# 看 38 这一格是 UU、38,还是 --
--:这颗也不在,温湿度这条产品线要换模块或修硬件。
38:芯片 ACK,没有驱动占着,用户态可以按 AHT20 协议另写工具。
UU:已经有驱动或 HDF 占着,先查是谁,再决定还让不让用户态碰。
不要把 HDC1080 的公式用在 AHT20 的 20 位字段上。不要把 AHT20 的 6 字节当 HDC1080 的 4 字节。料号不同,时序不同。
0x40 完全 NACK、0x38 有 ACK:验收表写成「HDC1080 硬件不应答,温湿度改测 0x38」。这比再交两次内核、再猜一遍 IIO 名字要诚实。
路径清单
| 路径 | 用途 |
|---|---|
| `device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts` | `hdc1080@40`、`aht20@38` |
| `kernel/linux/linux-5.10/drivers/iio/humidity/hdc100x.c` | 内核驱动,注意写 config 失败路径 |
| `kernel/linux/linux-5.10/Documentation/devicetree/bindings/iio/humidity/hdc100x.txt` | 绑定 |
| defconfig `CONFIG_HDC100X=y` | 三处同步 |
| `/dev/i2c-1` | 用户态 |
| `/sys/bus/i2c/devices/1-0040/` | 节点与 driver 软链 |
| `/sys/bus/iio/devices/iio:deviceN/name` | 常为 `1-0040` |
| `hdc1080_tool.c`(本章) | 验收工具 |
| `libboard.so` 的 `hdc1080Read` | 应用 |
万用表怎么量,才叫「定性硬件」
软件停手之后,还差一次电。连错测量点会得出假结论。
连接器上的 3.3 V 有电,只说明排针供电到了模块板。HDC1080 往往有自己的小 LDO,输出才进芯片 VDD。量连接器、芯片照样没电,dmesg 仍然 cannot set seq config。表笔要碰到 芯片 VDD 脚对地,不是排针丝印 3V3。
SDA、SCL 对地应能看到上拉带来的高电平(接近 3.3 V)。两根都接近 0:上拉没焊,或某颗从设备把总线卡死。这时 0x10/0x57/0x5a 也会一起消失,不是 HDC1080 单独的事。只有 0x40 NACK、邻机正常:总线健康,问题在这颗从设备的供电、虚焊、地址脚、死芯。
不要为了「再试一次软件」把 i2c1 改回 400 kHz。那会把已经能绑上的红外测温打掉,换来 HDC1080 依然 NACK。速率不是这颗的救命稻草。
记录表建议三行,以后的人就不必再编两次内核:
i2c1 100 kHz
0x10 ACK / 0x57 ACK / 0x5a ACK / 0x40 NACK
hdc100x: cannot set seq config
用户态写 0x02 同样失败
结论:硬件,改测 0x38 或换模块
工具同样 32 位 musl。多一个开关值得当面跑一次:
hdc shell /data/local/tmp/hdc1080_tool
# 成功:温度接近室温,对着芯片呼气,湿度往上跳
hdc shell /data/local/tmp/hdc1080_tool combined
# 预期失败或全 0。这就是「禁止组合读」的现场,不是工具写错。
写 config 已经 ERR 的板子,combined 不必跑。再跑也是 NACK,只会让人以为还有软件空间。把 ERR 原文和 i2cdetect 那一行贴进记录表,这章对这颗硬件就算闭合。
ArkTS 调 hdc1080Read,返回值是带单位的字符串,或者 ERR: 开头。界面遇到 ERR: 原样显示,不要解析成 0 ℃ / 0 %——那会让下一个人以为芯片活着、房间真的在冰点。按钮就一个「读温湿度」,成功再加一个「连续读」看呼气。
NAPI 里写 config 失败立刻 return,不要兜底返回上次缓存。缓存会把硬件 NACK 伪装成「数值卡住」。
hdc shell "cat /proc/partitions"
hdc shell "ls -l /dev/i2c-1 /dev/ttyS0"
hdc shell "dmesg | grep -E 'i2c|mlx|hdc|uart0|vcc-camera' | tail -30"
上面三条在换模块、改 dts、刷完分区之后各跑一次,比在应用里加 log 便宜。输出贴进记录,下一轮对照。
系列第 37 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10
更多推荐




所有评论(0)