第4章:I2C传感器驱动:从协议到实战
各位同学,欢迎来到第四章。前面我们聊了GPIO和UART,算是热了热身。从这一章开始,我们要真正进入嵌入式驱动开发的核心地带——I2C总线。
I2C这个名字,做嵌入式的没人不知道。但说实话,很多人写了几年代码,对I2C的理解还停留在“两根线,一主多从”的层面。这不够。今天,我们就从协议细节到Zephyr API,再到一个完整的温湿度传感器驱动,把I2C彻底吃透。
4.1 I2C总线协议回顾
先快速过一遍协议。I2C只有两根线:SCL(时钟)和SDA(数据)。所有设备都挂在这两根线上,通过地址区分。
关键时序点:
- 起始条件:SCL高电平时,SDA从高到低跳变。这是通信开始的标志。
- 停止条件:SCL高电平时,SDA从低到高跳变。通信结束。
- 数据采样:数据在SCL低电平时变化,高电平时采样。记住这个,调试时很有用。
- 应答位(ACK):每发送完一个字节,接收方必须拉低SDA表示应答。如果没应答,说明设备挂了或者地址错了。
我个人习惯:写驱动前,一定先拿逻辑分析仪抓一下波形。看看起始条件、地址、数据、停止条件是否完整。很多问题,看一眼波形就明白了。
关于地址:I2C地址有7位和10位两种。绝大多数传感器用7位地址。注意,数据手册里给的地址通常是7位值,但Zephyr API里需要左移1位(即8位地址)。为什么?因为最低位用来表示读写方向。嗯,这个坑我踩过。
4.2 Zephyr I2C API使用
Zephyr的I2C API设计得很简洁。核心就几个函数,但用起来有不少门道。
4.2.1 设备树配置
在Zephyr里,I2C外设和传感器都通过设备树描述。举个例子,一个典型的I2C传感器节点:
/ {
i2c0: &i2c0 {
status = "okay";
clock-frequency = <I2C_BITRATE_STANDARD>;
sht30: sht30@44 {
compatible = "sensirion,sht30";
reg = <0x44>;
label = "SHT30";
};
};
};
这里要注意几点:
reg = <0x44>是7位地址,Zephyr内部会帮你左移。clock-frequency我建议用标准模式(100kHz),除非你确认从设备支持高速。- label字段虽然现在不强制了,但我习惯加上,方便调试时识别。
4.2.2 核心API函数
Zephyr的I2C API主要就这几个:
| 函数 | 说明 | 我常用的场景 |
|---|---|---|
i2c_configure() |
配置I2C控制器参数 | 初始化时调用一次 |
i2c_transfer() |
通用传输函数 | 复杂通信场景 |
i2c_write_read() |
写后读(组合事务) | 读取传感器数据最常用 |
i2c_burst_read() |
连续读取多个字节 | 读取温湿度值 |
我个人最常用的是 i2c_write_read()。为什么?因为大多数I2C传感器的读取流程是:先写寄存器地址,再读数据。这个函数正好把两步合并成一个原子操作,避免了总线被其他设备打断。
避坑指南:我曾经在一个项目里,用 i2c_write() 和 i2c_read() 分开调用。结果发现偶尔读到的数据是错的。后来查了半天,发现是两次调用之间,总线被另一个任务插入了。改用 i2c_write_read() 后问题解决。记住:组合事务一定要用组合函数。
4.3 编写I2C温湿度传感器驱动
好,理论讲完了。我们动手写一个SHT30温湿度传感器的驱动。SHT30是sensirion家的经典产品,I2C接口,精度不错,很适合教学。
4.3.1 驱动框架
Zephyr的传感器驱动有标准框架。你需要实现几个回调函数:
static const struct sensor_driver_api sht30_driver_api = {
.sample_fetch = sht30_sample_fetch,
.channel_get = sht30_channel_get,
.attr_set = sht30_attr_set,
};
这三个函数是必须的。sample_fetch 负责从传感器读取原始数据,channel_get 负责把原始数据转换成用户需要的值,attr_set 用来设置采样率等参数。
4.3.2 初始化函数
初始化时,我们要做两件事:获取I2C设备指针,发送初始化命令。
static int sht30_init(const struct device *dev)
{
struct sht30_data *data = dev->data;
const struct sht30_config *cfg = dev->config;
data->i2c = device_get_binding(cfg->i2c_name);
if (!data->i2c) {
return -EINVAL;
}
// 发送初始化命令:开启周期采集模式
uint8_t cmd[] = {0x20, 0x32};
return i2c_write(data->i2c, cmd, sizeof(cmd), cfg->i2c_addr);
}
这里有个细节:SHT30的初始化命令是0x2032,需要拆成两个字节发送。你想想看,如果直接发一个16位值,字节顺序搞反了,传感器根本不会理你。
4.3.3 数据采集函数
采集数据是驱动的核心。SHT30的读取流程是:先写0xE000命令触发测量,然后等待一段时间,最后读取6个字节(温度高、温度低、CRC、湿度高、湿度低、CRC)。
static int sht30_sample_fetch(const struct device *dev,
enum sensor_channel chan)
{
struct sht30_data *data = dev->data;
const struct sht30_config *cfg = dev->config;
uint8_t buf[6];
uint8_t cmd[] = {0xE0, 0x00};
// 发送测量命令
if (i2c_write(data->i2c, cmd, sizeof(cmd), cfg->i2c_addr) < 0) {
return -EIO;
}
// 等待测量完成(SHT30需要约15ms)
k_sleep(K_MSEC(20));
// 读取6字节数据
if (i2c_read(data->i2c, buf, sizeof(buf), cfg->i2c_addr) < 0) {
return -EIO;
}
// 解析温度(原始值)
uint16_t raw_temp = (buf[0] << 8) | buf[1];
data->temp = -45.0 + 175.0 * raw_temp / 65535.0;
// 解析湿度
uint16_t raw_hum = (buf[3] << 8) | buf[4];
data->hum = 100.0 * raw_hum / 65535.0;
return 0;
}
注意:这里的延时20ms是硬等。在实际产品中,我建议用状态机或者定时器来避免阻塞。但教学示例里,这样写最直观。另外,CRC校验我故意省略了——实际项目中千万别省!
4.3.4 数据获取函数
这个函数很简单,就是把采集到的值返回给上层应用:
static int sht30_channel_get(const struct device *dev,
enum sensor_channel chan,
struct sensor_value *val)
{
struct sht30_data *data = dev->data;
if (chan == SENSOR_CHAN_AMBIENT_TEMP) {
sensor_value_from_double(val, data->temp);
} else if (chan == SENSOR_CHAN_HUMIDITY) {
sensor_value_from_double(val, data->hum);
} else {
return -ENOTSUP;
}
return 0;
}
4.4 调试技巧
写驱动不难,调试驱动才是真功夫。分享几个我常用的调试方法。
4.4.1 逻辑分析仪是神器
我调试I2C驱动时,第一件事就是挂逻辑分析仪。看什么?
- 起始条件是否正确?SCL高时SDA有没有下降沿?
- 地址字节对不对?7位地址左移1位后,加上读写位,总共8位。
- ACK位有没有?如果从设备没拉低SDA,说明地址错了或者设备没上电。
- 数据字节顺序对不对?尤其是多字节数据,高低位顺序容易搞反。
我曾经调试一个气压传感器,死活读不到数据。逻辑分析仪一看,地址字节发的是0x76,但传感器手册上写的是0x5C。后来才发现,手册给的是7位地址,我忘了左移。这种低级错误,波形一看就露馅。
4.4.2 打印调试信息
Zephyr的日志系统很好用。我习惯在关键位置加LOG_DBG:
LOG_DBG("I2C addr: 0x%02x, cmd: 0x%02x%02x",
cfg->i2c_addr, cmd[0], cmd[1]);
LOG_DBG("Raw data: %02x %02x %02x %02x %02x %02x",
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);
但注意,生产代码里要关掉这些日志,否则会影响时序。
4.4.3 检查返回值
Zephyr的I2C API会返回错误码。很多新手不看返回值,以为函数调用成功了。我建议每个I2C操作都检查返回值:
int ret = i2c_write(data->i2c, cmd, sizeof(cmd), cfg->i2c_addr);
if (ret < 0) {
LOG_ERR("I2C write failed: %d", ret);
return ret;
}
常见的错误码有:-EIO(通信失败)、-EAGAIN(总线忙)、-ENODEV(设备不存在)。看到这些,你就能快速定位问题。
4.4.4 时序问题排查
如果波形看起来都对,但数据不对,十有八九是时序问题。比如:
- 两次I2C操作之间间隔太短,传感器没准备好。
- 时钟频率太高,从设备跟不上。
- 上拉电阻阻值不对,导致信号边沿太缓。
嗯,这里要注意:I2C总线的上拉电阻很关键。4.7kΩ是通用值,但如果总线电容大(比如线很长),可能需要换成2.2kΩ。我遇到过因为上拉电阻太大,导致400kHz模式下通信失败的案例。
4.5 小结
这一章我们走完了I2C传感器驱动的完整流程:从协议细节,到Zephyr API,再到一个可运行的SHT30驱动。最后还分享了一些调试技巧。
说实话,写I2C驱动本身不难。难的是遇到问题时有条不紊地排查。记住:先看波形,再查代码,最后怀疑硬件。按这个顺序来,90%的问题都能解决。
下一章,我们会讲SPI传感器驱动。SPI比I2C快,但线也多。到时候我们再细聊。