5.3 I2C设备
title: "5.3 I2C设备"
shortTitle: "I2C设备"
description: "OneOS I2C 设备驱动详解,包括 I2C 总线设备与从设备双层抽象、消息传输机制、标志位体系、客户端读写操作与完整使用示例"
icon: "fa6-solid:arrows-left-right"
author: 学习阁
date: 2026-08-16
category: 设备驱动
tag:
- 驱动
- I2C
- 总线
- 传感器
order: 4
5.3 I2C设备
📚 本节导读
学习时长: 约 90 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: I2C 总线协议基础(SCL/SDA、起始/停止条件、ACK/NACK、设备地址)、5.1 设备驱动框架、互斥锁基本用法
I2C(Inter-Integrated Circuit)是嵌入式系统中最常用的两线制串行总线,用于连接传感器、EEPROM、RTC、显示驱动等外设。本节讲解 OneOS 中 I2C 设备驱动的完整用法。OneOS 把 I2C 抽象为两层:总线设备(一条 I2C 控制器,负责底层时序)与从设备客户端(挂在该总线上的具体外设芯片,封装总线引用 + 设备地址),并用消息(os_i2c_msg)统一描述一次原子传输。
本节会带你:
- 看懂 I2C 双层抽象如何基于 5.1 的设备框架实现"一套总线驱动服务多个外设";
- 掌握
os_i2c_bus_device、os_i2c_client、os_i2c_msg三个核心结构体的每个字段; - 掌握读写方向、地址模式、传输控制三类标志位的位运算含义(配数值演算);
- 逐个拆解
os_i2c_transfer、os_i2c_client_write/read、os_i2c_master_send/recv、os_i2c_bus_device_register/find的完整源码; - 通过多个可运行示例掌握主机读写、客户端读写寄存器、批量消息传输、总线控制的实际开发。
说明:本节所有源码均取自项目
drivers/i2c/i2c.c与drivers/i2c/i2c.h,与项目源码逐字一致(含原始英文注释)。
🎯 学习目标
- 理解 OneOS 中 I2C 总线设备与从设备(客户端)双层抽象的设计理念
- 掌握
os_i2c_bus_device、os_i2c_client、os_i2c_msg、os_i2c_bus_device_ops各字段含义与来源 - 掌握
OS_I2C_WR、OS_I2C_RD、OS_I2C_ADDR_10BIT、OS_I2C_NO_START、OS_I2C_IGNORE_NACK、OS_I2C_NO_READ_ACK标志位的位运算含义与具体数值 - 掌握
os_i2c_transfer批量消息传输的内部流程(含加锁/解锁) - 掌握客户端写/读寄存器接口(
os_i2c_client_write/read及 byte 变体)的自动构造消息逻辑 - 掌握主机发送/接收接口与总线注册/查找接口
- 能够区分"两条消息 + Restart(
OS_I2C_NO_START)"与"单条消息"两种传输模式的差异 - 理解更换 MCU 时如何移植 BSP 层 I2C 驱动(认识
drv_i2c.c、实现os_i2c_bus_device_ops、接入三层选择机制)
一、概述
I2C(Inter-Integrated Circuit)由 SCL(时钟线)和 SDA(数据线)两根线组成,支持多主多从架构,通过从设备地址寻址。OneOS 将 I2C 分为两层抽象:
- I2C 总线设备(
os_i2c_bus_device):表示一条 I2C 总线控制器,负责底层时序控制和数据传输; - I2C 从设备 / 客户端(
os_i2c_client):表示挂载在总线上的具体外设芯片,封装了总线引用和设备地址。
这种双层设计让一套 I2C 总线驱动能服务于多个不同类型的 I2C 外设,实现了总线资源与设备逻辑的分离。
1.1 I2C 双层抽象的分层架构
OneOS I2C 设备在5.1 设备驱动框架的基础上做了两层封装,从应用层到硬件共四层:
┌────────────────────────────────────────────────────────────┐
│ 应用层(Application) │
│ os_i2c_client_write/read、os_i2c_master_send/recv │
│ os_i2c_transfer、os_i2c_bus_device_find │
└──────────────────────────────┬─────────────────────────────┘
│ 通过 bus->ops->xxx
▼
┌────────────────────────────────────────────────────────────┐
│ I2C 框架层(drivers/i2c/i2c.c) │
│ os_i2c_msg / os_i2c_bus_device / os_i2c_client │
│ os_i2c_transfer(加锁 → 调 ops → 解锁) │
│ 设备框架层封装:i2c_bus_device_read/write/control │
└──────────────────────────────┬─────────────────────────────┘
│ 调用 bus->ops->i2c_transfer
▼
┌────────────────────────────────────────────────────────────┐
│ BSP 厂商 I2C 驱动(如 stm32_drv_i2c.c) │
│ 实现 struct os_i2c_bus_device_ops 的三个函数指针 │
│ 操作具体 MCU 的 I2C 外设寄存器 │
└──────────────────────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ 硬件(MCU I2C 外设) │
└────────────────────────────────────────────────────────────┘关键理解:I2C 框架层不直接操作硬件。os_i2c_transfer 只负责"加互斥锁 → 转发给 bus->ops->i2c_transfer → 解锁",真正操作寄存器的是 BSP 厂商驱动实现的 struct os_i2c_bus_device_ops。这让上层应用与具体 MCU 平台解耦。
1.2 I2C 设备使用场景
一句话核心
I2C 设备适用于在同一根两线制总线上挂载多个慢速外设(传感器 / 存储 / 显示),以地址寻址、短小报文、一主多从为特征的场景。
与其他总线/外设的机制对比
| 维度 | I2C | SPI | UART(串口) | GPIO 模拟 |
|---|---|---|---|---|
| 接线 | 2 根(SCL+SDA) | 3~4 根(MOSI/MISO/SCK/CS) | 2~3 根(TX/RX/GND) | 软件模拟,无硬件模块 |
| 总线拓扑 | 多主多从,地址寻址 | 一主多从,片选寻址 | 点对点为主 | 点对点 |
| 速率 | 慢~中(100k/400k/1M Hz) | 快(可达几十 MHz) | 中(115200 等) | 慢 |
| 设备识别 | 7/10 位地址 | 片选线 CS | 无 | 无 |
| 是否带 ACK | 是(从机应答) | 是 | 无 | 依实现 |
| 应用场景 | 传感器、EEPROM、RTC | 高吞吐传感器、Flash、LCD | 日志、通信、调试 | 极低成本简化场景 |
为什么用 I2C 而不是别的:传感器/EEPROM 这类外设只需低速率、数据量小,I2C 用 2 根线即可挂多个从机,节省引脚;SPI 速率高但每加一个从机就多一根片选线;UART 是点对点,无法用地址区分多个从机。
典型应用场景
场景 1:一主多从的传感器采集(I2C 总线挂多个从机)
┌──────────┐ I2C 总线(SCL+SDA) ┌──────────┐
│ 主控 MCU │──────────────────────────┤ 温度传感器 │ 地址 0x48
│ (主机) │ │ └──────────┘
└──────────┘ │ ┌──────────┐
│ │ 加速度传感器 │ 地址 0x68
│ └──────────┘
│ ┌──────────┐
│ │ EEPROM │ 地址 0x50
│ └──────────┘为什么用 I2C:多个传感器共享同一根总线,只需在读写时指定 client_addr,硬件自动按地址应答,无需额外片选引脚。
实际工作:采集任务 T1 时刻 os_i2c_client_read(&temp_client, 0x00, 1, buf, 2) 读温度寄存器;T2 时刻 os_i2c_client_read(&accel_client, 0x00, 1, accel_buf, 6) 读加速度;T3 时刻 os_i2c_client_write(&eeprom_client, 0x0000, 2, eeprom_buf, 8) 存数据。同一根总线分时复用,互不干扰(由总线互斥锁保证)。
场景 2:寄存器地址 + 数据的"先写后读"事务(读写寄存器)
主机发送: 起始 [0x48|W] ACK 0x3B(寄存器) ACK 停止
主机接收: 起始 [0x48|R] ACK data[0] ACK data[1] NACK 停止为什么用 I2C:读传感器数据前必须先告诉它"读哪个寄存器",这是 I2C 最经典的"两条消息事务"(先写寄存器地址,再读数据)。os_i2c_client_read 自动构造这两条消息。
实际工作:os_i2c_client_read(&client, 0x3B, 1, data, 6) 先发一条写消息(带 OS_I2C_WR)把 0x3B 写入寄存器,再发一条读消息(带 OS_I2C_RD)读回 6 个字节,两条消息在同一次 os_i2c_transfer 中完成,中间以 Restart 衔接。
场景 3:中断/任务通过总线读数据(互斥保护)
为什么用 I2C:I2C 传输是阻塞式的(os_i2c_transfer 会 os_mutex_lock 等待),若两个任务同时访问总线会互相干扰。总线设备内置互斥锁 lock,保证同一时刻只有一个任务在总线上传输。
实际工作:任务 A 正在读 EEPROM(持有总线锁),任务 B 此时想读传感器,os_mutex_lock(bus->lock, OS_WAIT_FOREVER) 会让 B 阻塞等待,直到 A 的 os_i2c_transfer 返回并 os_mutex_unlock,B 才获得总线继续传输。这就避免了两条传输在 SDA/SCL 上交叉错乱。
选型边界表(何时不该用 I2C)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 需要高速大吞吐传输(如 SPI Flash 读写) | SPI | I2C 速率低,带宽不足 |
| 只需点对点、不关心多从机寻址 | UART | I2C 接线和时序更复杂 |
| 只传一个开关状态/标志位 | GPIO | 无需总线协议 |
| 只传标志信号不传数据 | 信号量/事件 | 无数据传输需求,总线开销大 |
| 挂载极多从机且需要可热插拔 | I2C(地址冲突时需扩展) | 7 位地址仅 128 个,可能冲突 |
结合源码的使用注意点
- 阻塞等待:
os_i2c_transfer用os_mutex_lock(bus->lock, OS_WAIT_FOREVER),在中断上下文调用会死锁(无法睡眠等待),因此 I2C 传输应在任务上下文中调用。 - 消息方向靠 flags:同一条总线上,
os_i2c_msg.flags决定当前消息是写(OS_I2C_WR)还是读(OS_I2C_RD),底层驱动据此设置总线方向位。 - 第二条消息用
OS_I2C_NO_START:客户端写数据时,第二条消息的 flags 是OS_I2C_WR | OS_I2C_NO_START,表示不重新发送起始条件(用 Restart 衔接),避免地址被重复发送(见 4.2 分步详解)。
二、核心数据结构(字段详解)
本节对照 drivers/i2c/i2c.h 讲解 I2C 驱动涉及的四个核心结构体。来源:drivers/i2c/i2c.h。
2.1 I2C 消息结构体 struct os_i2c_msg
struct os_i2c_msg
{
uint16_t addr;
uint16_t flags;
uint16_t len;
uint8_t *buf;
};| 字段 | 类型 | 含义 |
|---|---|---|
addr | uint16_t | 目标从设备的 7 位或 10 位地址(不含读写位,读写位由 flags 决定) |
flags | uint16_t | 传输标志位,见三章标志位体系 |
len | uint16_t | 本次消息传输的数据字节数 |
buf | uint8_t * | 数据缓冲区指针:写消息指向待发送数据,读消息指向接收缓冲区 |
一条 os_i2c_msg 描述一次原子传输操作。多个 os_i2c_msg 组成一个数组,可交给 os_i2c_transfer 完成一次批量传输(如"先写寄存器地址,再读数据")。
2.2 I2C 私有数据传输结构体 struct os_i2c_priv_data
struct os_i2c_priv_data
{
struct os_i2c_msg *msgs;
os_size_t number;
};| 字段 | 类型 | 含义 |
|---|---|---|
msgs | struct os_i2c_msg * | 指向消息数组首地址 |
number | os_size_t | 消息数组中的消息条数 |
这个结构体用于把"一组消息"打包成一个参数,通过设备控制命令 OS_I2C_DEV_CTRL_RW 传给 i2c_bus_device_control(见 4.7)。
2.3 I2C 总线设备 struct os_i2c_bus_device
struct os_i2c_bus_device
{
struct os_device parent;
const struct os_i2c_bus_device_ops *ops;
uint16_t addr;
uint16_t flags;
os_mutex_id lock;
uint32_t timeout;
uint32_t retries;
void *priv;
};| 字段 | 类型 | 含义 |
|---|---|---|
parent | struct os_device | 内嵌的通用设备对象,用于把总线注册进 5.1 的设备框架(os_device_register) |
ops | const struct os_i2c_bus_device_ops * | I2C 总线操作函数表指针,由 BSP 驱动填充,实现底层传输 |
addr | uint16_t | 总线自身的地址(主要用于从机模式,或控制命令 OS_I2C_DEV_CTRL_ADDR 设置) |
flags | uint16_t | 总线标志,如是否启用 10 位地址(OS_I2C_DEV_CTRL_10BIT 会置 OS_I2C_ADDR_10BIT) |
lock | os_mutex_id | 总线互斥锁,保证同一时刻只有一个任务访问总线(os_i2c_transfer 内使用) |
timeout | uint32_t | 传输超时时间;若注册时未设置(为 0),os_i2c_bus_device_register 会默认设为 OS_TICK_PER_SECOND |
retries | uint32_t | 传输失败后的重试次数(框架层预留,具体由 BSP 驱动使用) |
priv | void * | 私有数据指针,通常指向 MCU 特定的 I2C 外设寄存器结构 |
关键理解:parent 内嵌 os_device,使 I2C 总线能注册到设备框架,既可用框架层的 os_device_* 接口(如 os_device_control、os_device_find),也可用 I2C 专有接口(os_i2c_bus_device_find)。lock 是框架层自动创建并加解锁的,保证传输互斥。
2.4 I2C 从设备(客户端) struct os_i2c_client
struct os_i2c_client
{
struct os_i2c_bus_device *bus;
uint16_t client_addr;
};| 字段 | 类型 | 含义 |
|---|---|---|
bus | struct os_i2c_bus_device * | 所属 I2C 总线设备的指针(该客户端挂在哪条总线上) |
client_addr | uint16_t | 该从设备的 7 位(或 10 位)地址 |
os_i2c_client 是一个轻量级结构体,通常在应用里栈上构造:先 os_i2c_bus_device_find 拿到总线,再填 bus 和 client_addr。它把"总线 + 地址"打包,供 os_i2c_client_write/read 使用,避免每次调用都要传总线与地址两个参数。
2.5 I2C 总线操作函数表 struct os_i2c_bus_device_ops
struct os_i2c_bus_device_ops
{
os_size_t (*i2c_transfer)(struct os_i2c_bus_device *bus, struct os_i2c_msg msgs[], uint32_t num);
os_size_t (*i2c_slave_transfer)(struct os_i2c_bus_device *bus, struct os_i2c_msg msgs[], uint32_t num);
os_err_t (*i2c_bus_control)(struct os_i2c_bus_device *bus, void *arg);
};| 函数指针 | 说明 |
|---|---|
i2c_transfer | 主机模式传输,执行 msgs 数组中的 num 条消息,返回成功传输的消息条数 |
i2c_slave_transfer | 从机模式传输,响应外部主机对本设备的访问 |
i2c_bus_control | 总线控制,如设置速率、切换模式等(本项目 i2c.c 未直接调用,留给 BSP) |
关键理解:框架层 os_i2c_transfer 调用的是 bus->ops->i2c_transfer;而"客户端/主机高层接口"最终都归结到 os_i2c_transfer,再落到这个 ops。所以所有高层 API 的终点都是 i2c_transfer 这一个函数指针,这正是"双层抽象 + ops 表"的核心价值——上层逻辑固定,底层只换一个 ops 表即可适配不同 MCU。
三、I2C 标志位、模式与控制命令
本节对照 drivers/i2c/i2c.h 讲解标志位宏、设备模式与控制命令。所有标志位都是位掩码,可组合使用,配合 &(与)、|(或)进行判断与拼接。来源:drivers/i2c/i2c.h。
3.1 读写方向标志
#define OS_I2C_WR 0x0000
#define OS_I2C_RD (1u << 0)| 宏 | 数值 | 含义 |
|---|---|---|
OS_I2C_WR | 0x0000 | 写方向(bit0 = 0) |
OS_I2C_RD | 0x0001 | 读方向(bit0 = 1) |
数值演算:
(1u << 0) = 1,二进制为0b0001。判断一条消息是否为读方向,用msgs[i].flags & OS_I2C_RD:
计算项 代入计算 结果 读消息 flags 0x0001即0b0001OS_I2C_RD与掩码相与 0b0001 & 0b00010b0001(非 0 → 是读)写消息 flags 0x0000即0b0000OS_I2C_WR与掩码相与 0b0000 & 0b00010b0000(为 0 → 是写)
3.2 地址模式标志
#define OS_I2C_ADDR_10BIT (1u << 2) /* this is a ten bit chip address */| 宏 | 数值 | 含义 |
|---|---|---|
OS_I2C_ADDR_10BIT | 0x0004 | 使用 10 位从设备地址(bit2 = 1) |
默认使用 7 位地址模式。当从设备使用 10 位地址时,设置此标志。
数值演算:
(1u << 2) = 0b0100 = 0x0004。控制命令OS_I2C_DEV_CTRL_10BIT会执行bus->flags |= OS_I2C_ADDR_10BIT,把总线 flags 的 bit2 置 1:
计算项 代入计算 结果 原 bus->flags 0b00000x0000置位 0b0000 | 0b01000b0100=0x0004
3.3 传输控制标志
#define OS_I2C_NO_START (1u << 4)
#define OS_I2C_IGNORE_NACK (1u << 5)
#define OS_I2C_NO_READ_ACK (1u << 6) /* when I2C reading, we do not ACK */| 宏 | 数值 | 含义 |
|---|---|---|
OS_I2C_NO_START | 0x0010 | 不发送起始条件,用于连续传输(Restart 衔接),此时不重新发送从机地址 |
OS_I2C_IGNORE_NACK | 0x0020 | 忽略从设备的 NACK 应答,继续传输 |
OS_I2C_NO_READ_ACK | 0x0040 | 读取最后一个字节后不发送 ACK(主机 NACK),表示读取结束 |
为什么需要 OS_I2C_NO_START:I2C 的"先写寄存器地址、再读数据"事务中,主机要先以写方向把寄存器地址发给从机,然后切换为读方向。如果直接重新发送起始条件 + 地址,就变成"Stop → Start"两次独立传输,从机会以为事务中断。正确做法是发重复起始条件(Restart) 且不重新发送从机地址(因为地址没变),只切换读写位——这正是 OS_I2C_NO_START 标记告诉底层驱动做的事。这也是 os_i2c_client_write/read 第二条消息带 OS_I2C_NO_START 的原因。
数值演算(客户端写数据的第二条消息 flags 组合):
计算项 代入计算 结果 OS_I2C_WR0b00000x0000OS_I2C_NO_START0b0001_00000x0010或运算组合 0b0000 | 0b0001_00000b0001_0000=0x0010
关键理解——各标志位占用独立的 bit,互不干扰:
bit: 7 6 5 4 3 2 1 0
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 保留 │NO_ │IGNORE│NO_ │保留 │10BIT│ 保留 │ R/W │
│ │READ │_NACK│START│ │ADDR │ │ │
│ │_ACK │ │ │ │ │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
0x40 0x20 0x10 0x04 0x01因为各标志位位于不同 bit,所以可以安全地用 | 组合(如 OS_I2C_WR | OS_I2C_NO_START、OS_I2C_RD | OS_I2C_NO_START),且用 & 判断某一位时不会被其他位干扰。这是位掩码设计的核心好处。
3.4 设备模式
#define OS_I2C_DEV_MEM_MODE 0
#define OS_I2C_DEV_MASTER_MODE 1
#define OS_I2C_DEV_SLAVE_MODE 2| 宏 | 数值 | 含义 |
|---|---|---|
OS_I2C_DEV_MEM_MODE | 0 | 内存模式(一般指内部寄存器映射访问模式) |
OS_I2C_DEV_MASTER_MODE | 1 | 主机模式(本设备作为总线主机发起传输) |
OS_I2C_DEV_SLAVE_MODE | 2 | 从机模式(本设备作为从机响应外部主机) |
这三个模式常量用于配置 I2C 设备工作在主机还是从机角色(具体由 BSP 驱动根据模式寄存器设置)。
3.5 控制命令
#define OS_I2C_DEV_CTRL_10BIT IOC_I2CBUS(0x00)
#define OS_I2C_DEV_CTRL_ADDR IOC_I2CBUS(0x01)
#define OS_I2C_DEV_CTRL_TIMEOUT IOC_I2CBUS(0x02)
#define OS_I2C_DEV_CTRL_RW IOC_I2CBUS(0x03)这些控制命令通过 5.1 设备框架的 os_device_control 接口发送给 I2C 总线设备,最终进入 i2c_bus_device_control 的 switch(cmd)(见 4.7)。
IOC_I2CBUS(cmd) 宏的来历:在 drivers/device.h 中定义:
#define IOC_I2CBUS(cmd) ((OS_DEVICE_TYPE_I2CBUS << 8) | (cmd))其中 OS_DEVICE_TYPE_I2CBUS 是设备类型枚举 enum os_device_type 的一个成员(drivers/device.h 中枚举顺序:CHAR=0, BLOCK=1, NETIF=2, MTD=3, CAN=4, RTC=5, SOUND=6, GRAPHIC=7, I2CBUS=8)。
数值演算:把控制命令转换成实际传给
os_device_control的int cmd值:
计算项 代入计算 结果 OS_DEVICE_TYPE_I2CBUS枚举第 9 项 8=0x08IOC_I2CBUS(0x00)(0x08 << 8) | 0x000x0800IOC_I2CBUS(0x01)(0x08 << 8) | 0x010x0801IOC_I2CBUS(0x02)(0x08 << 8) | 0x020x0802IOC_I2CBUS(0x03)(0x08 << 8) | 0x030x0803
| 控制命令 | 数值 | 作用 |
|---|---|---|
OS_I2C_DEV_CTRL_10BIT | 0x0800 | 设置 10 位地址模式(bus->flags |= OS_I2C_ADDR_10BIT) |
OS_I2C_DEV_CTRL_ADDR | 0x0801 | 设置设备地址(bus->addr = *(uint16_t *)args) |
OS_I2C_DEV_CTRL_TIMEOUT | 0x0802 | 设置超时时间(bus->timeout = *(uint32_t *)args) |
OS_I2C_DEV_CTRL_RW | 0x0803 | 执行一次批量读写(传 struct os_i2c_priv_data *) |
四、核心 API 详解
本节逐个拆解 drivers/i2c/i2c.c 中的核心函数。来源:drivers/i2c/i2c.c(函数位于 #ifdef OS_USING_I2C 内)。
4.1 os_i2c_transfer —— I2C 批量消息传输(框架层核心)
函数签名
os_size_t os_i2c_transfer(struct os_i2c_bus_device *bus, struct os_i2c_msg msgs[], uint32_t num)参数 / 返回值说明
| 参数 | 类型 | 说明 |
|---|---|---|
bus | struct os_i2c_bus_device * | I2C 总线设备指针 |
msgs[] | struct os_i2c_msg[] | 消息数组,可包含多条消息 |
num | uint32_t | 消息条数 |
| 返回值 | 说明 |
|---|---|
> 0 | 成功传输的消息条数 |
0 | 传输失败(或 bus->ops->i2c_transfer 未实现) |
完整源码
os_size_t os_i2c_transfer(struct os_i2c_bus_device *bus, struct os_i2c_msg msgs[], uint32_t num)
{
os_size_t ret;
if (bus->ops->i2c_transfer)
{
#ifdef OS_I2C_DEBUG
for (ret = 0; ret < num; ret++)
{
LOG_D(DBG_EXT_TAG,
"msgs[%d] %c, addr=0x%02x, len=%d",
ret,
(msgs[ret].flags & OS_I2C_RD) ? 'R' : 'W',
msgs[ret].addr,
msgs[ret].len);
}
#endif
os_mutex_lock(bus->lock, OS_WAIT_FOREVER);
ret = bus->ops->i2c_transfer(bus, msgs, num);
os_mutex_unlock(bus->lock);
return ret;
}
else
{
LOG_E(DBG_EXT_TAG, "I2C bus operation not supported");
return 0;
}
}分步详解
第一步:声明返回值变量(无条件执行)
os_size_t ret;ret 用于保存实际传输结果。注意 os_size_t 是有符号类型,因此既能保存正数(成功条数),也能通过负数保存错误码(但本函数只在 os_i2c_master_send/recv 中使用该特性,见 4.5/4.6)。
第二步:判断底层传输函数是否实现(条件 bus->ops->i2c_transfer != OS_NULL)
if (bus->ops->i2c_transfer)
{
#ifdef OS_I2C_DEBUG
for (ret = 0; ret < num; ret++)
{
LOG_D(DBG_EXT_TAG,
"msgs[%d] %c, addr=0x%02x, len=%d",
ret,
(msgs[ret].flags & OS_I2C_RD) ? 'R' : 'W',
msgs[ret].addr,
msgs[ret].len);
}
#endif
os_mutex_lock(bus->lock, OS_WAIT_FOREVER);
ret = bus->ops->i2c_transfer(bus, msgs, num);
os_mutex_unlock(bus->lock);
return ret;
}
else
{
LOG_E(DBG_EXT_TAG, "I2C bus operation not supported");
return 0;
}若 BSP 驱动未实现 i2c_transfer(ops 表里该指针为空),说明这条总线不支持传输,打印错误日志并返回 0。
为什么必须先判空:如果直接
bus->ops->i2c_transfer(bus, msgs, num),而该指针为空,就会跳转到空地址执行,触发 HardFault。判空是防止空指针调用的关键防护。
第三步:调试模式下打印每条消息的方向/地址/长度(条件 OS_I2C_DEBUG 定义)
#ifdef OS_I2C_DEBUG
for (ret = 0; ret < num; ret++)
{
LOG_D(DBG_EXT_TAG,
"msgs[%d] %c, addr=0x%02x, len=%d",
ret,
(msgs[ret].flags & OS_I2C_RD) ? 'R' : 'W',
msgs[ret].addr,
msgs[ret].len);
}
#endif仅当编译时定义了 OS_I2C_DEBUG 才启用。对每条消息,用 (msgs[ret].flags & OS_I2C_RD) ? 'R' : 'W' 判断方向:若 bit0 为 1 打印 R,否则打印 W。这是位掩码判断的典型应用(见 3.1 数值演算)。
第四步:加锁传输、解锁返回(无条件执行,前提走到 if 分支)
os_mutex_lock(bus->lock, OS_WAIT_FOREVER);
ret = bus->ops->i2c_transfer(bus, msgs, num);
os_mutex_unlock(bus->lock);
return ret;os_mutex_lock(bus->lock, OS_WAIT_FOREVER):申请总线互斥锁。若别的任务正占用总线,本任务阻塞等待(OS_WAIT_FOREVER),直到锁被释放。bus->ops->i2c_transfer(bus, msgs, num):把整组消息一次性交给底层驱动执行。os_mutex_unlock(bus->lock):传输完成,释放锁,让等待的任务有机会获得总线。return ret:返回实际传输成功的消息条数。
为什么用互斥锁:I2C 是共享总线。若任务 A 正在发第一条消息,任务 B 抢到 CPU 也来发消息,两条传输会在 SDA/SCL 上交叉错乱,导致数据损坏。总线锁保证"同一时刻只有一条传输在总线上进行"。
条件走向总结
os_i2c_transfer() 条件走向总结:
├── bus->ops->i2c_transfer 非空(已实现)
│ ├── OS_I2C_DEBUG 已定义 → 打印每条消息方向/地址/长度(仅调试日志)
│ ├── os_mutex_lock(bus->lock, OS_WAIT_FOREVER)
│ ├── ret = bus->ops->i2c_transfer(bus, msgs, num)
│ ├── os_mutex_unlock(bus->lock)
│ └── 返回 ret(成功消息条数,> 0)
└── bus->ops->i2c_transfer 为空(未实现)
├── LOG_E("I2C bus operation not supported")
└── 返回 0(传输失败)4.2 os_i2c_client_write —— 客户端写寄存器/写数据
函数签名
os_err_t os_i2c_client_write(struct os_i2c_client *client,
uint32_t cmd,
uint8_t cmd_len,
uint8_t *buff,
uint32_t len)参数 / 返回值说明
| 参数 | 类型 | 说明 |
|---|---|---|
client | struct os_i2c_client * | I2C 客户端指针(含总线与从机地址) |
cmd | uint32_t | 寄存器地址或命令字 |
cmd_len | uint8_t | 寄存器地址长度(1/2/4 字节);为 0 时不发地址消息 |
buff | uint8_t * | 要写入的数据缓冲区 |
len | uint32_t | 写入数据长度;为 0 时不发数据消息 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 写入成功(所有消息均传输成功) |
OS_FAILURE | 写入失败(有消息传输失败) |
完整源码
os_err_t os_i2c_client_write(struct os_i2c_client *client,
uint32_t cmd,
uint8_t cmd_len,
uint8_t *buff,
uint32_t len)
{
int i = 0;
struct os_i2c_msg msgs[2];
if (cmd_len != 0)
{
msgs[i].addr = client->client_addr;
msgs[i].flags = OS_I2C_WR;
msgs[i].buf = (uint8_t *)&cmd;
msgs[i].len = cmd_len;
i++;
}
if (len != 0)
{
msgs[i].addr = client->client_addr;
msgs[i].flags = OS_I2C_WR | OS_I2C_NO_START;
msgs[i].buf = buff;
msgs[i].len = len;
i++;
}
OS_ASSERT(i == 1 || i == 2);
if (os_i2c_transfer(client->bus, msgs, i) == i)
{
return OS_SUCCESS;
}
else
{
return OS_FAILURE;
}
}分步详解
第一步:准备计数器和消息数组(无条件执行)
int i = 0;
struct os_i2c_msg msgs[2];i 既当计数器又当数组下标(已填充的消息条数);msgs[2] 最多容纳两条消息(一条地址 + 一条数据)。
第二步:有寄存器地址时构造"写地址"消息(条件 cmd_len != 0)
if (cmd_len != 0)
{
msgs[i].addr = client->client_addr;
msgs[i].flags = OS_I2C_WR;
msgs[i].buf = (uint8_t *)&cmd;
msgs[i].len = cmd_len;
i++;
}当需要先写寄存器地址时(cmd_len != 0),构造第一条消息:写方向 OS_I2C_WR,把 cmd(寄存器地址)以写方式发给从机。i 变为 1。注意 buf 指向 &cmd,而 cmd_len 控制发送 cmd 的低 cmd_len 个字节(如 cmd_len = 2 时发送 cmd 的低 2 字节)。
第三步:有数据时构造"写数据"消息(条件 len != 0,带 OS_I2C_NO_START)
if (len != 0)
{
msgs[i].addr = client->client_addr;
msgs[i].flags = OS_I2C_WR | OS_I2C_NO_START;
msgs[i].buf = buff;
msgs[i].len = len;
i++;
}当有数据要写时(len != 0),构造第二条消息:写方向 + OS_I2C_NO_START。OS_I2C_NO_START 表示不重新发送起始条件与地址,因为在第二条消息里地址没变,只需用 Restart 衔接继续发数据。i 变为 2。
第四步:断言消息条数合法(无条件执行)
OS_ASSERT(i == 1 || i == 2);若 cmd_len 与 len 都为 0,则 i = 0,此时断言失败(触发断言)。这防止"既无地址也无数据"的空传输调用,属于参数合法性校验。
第五步:批量传输并判断结果(无条件执行)
if (os_i2c_transfer(client->bus, msgs, i) == i)
{
return OS_SUCCESS;
}
else
{
return OS_FAILURE;
}调用 os_i2c_transfer 发送 i 条消息,若全部成功(返回条数 == i)返回 OS_SUCCESS,否则返回 OS_FAILURE。
数值演算示例(
cmd = 0x0000,cmd_len = 2,len = 6,地址0x50):
步骤 消息下标 iaddr flags buf len 第二步后 0 0x50OS_I2C_WR(0x0000)&cmd2 第三步后 1 0x50OS_I2C_WR|OS_I2C_NO_START(0x0010)buff6 传输 os_i2c_transfer(bus, msgs, 2)应返回 2
条件走向总结
os_i2c_client_write() 条件走向总结:
├── cmd_len != 0(有寄存器地址)
│ └── msgs[0] = {addr, OS_I2C_WR, &cmd, cmd_len},i=1
├── cmd_len == 0(无寄存器地址,纯写数据)
│ └── 跳过地址消息,i 仍为 0
├── len != 0(有数据)
│ └── msgs[i] = {addr, OS_I2C_WR|OS_I2C_NO_START, buff, len},i=2(或 i=1)
├── len == 0(无数据)
│ └── 跳过数据消息
├── OS_ASSERT(i == 1 || i == 2)(cmd_len==0 且 len==0 时断言失败)
└── os_i2c_transfer(client->bus, msgs, i) == i
├── 是(全部成功)→ 返回 OS_SUCCESS
└── 否(部分失败)→ 返回 OS_FAILURE4.3 os_i2c_client_read —— 客户端读寄存器
函数签名
os_err_t
os_i2c_client_read(struct os_i2c_client *client, uint32_t cmd, uint8_t cmd_len, uint8_t *buff, uint32_t len)参数 / 返回值说明
| 参数 | 类型 | 说明 |
|---|---|---|
client | struct os_i2c_client * | I2C 客户端指针 |
cmd | uint32_t | 寄存器地址或命令字 |
cmd_len | uint8_t | 寄存器地址长度(1/2/4 字节);为 0 时不发地址消息 |
buff | uint8_t * | 传出接收数据缓冲区(存储读到的数据) |
len | uint32_t | 期望读取的数据长度;为 0 时不发读消息 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 读取成功 |
OS_FAILURE | 读取失败 |
完整源码
os_err_t
os_i2c_client_read(struct os_i2c_client *client, uint32_t cmd, uint8_t cmd_len, uint8_t *buff, uint32_t len)
{
int i = 0;
struct os_i2c_msg msgs[2];
if (cmd_len != 0)
{
msgs[i].addr = client->client_addr;
msgs[i].flags = OS_I2C_WR;
msgs[i].buf = (uint8_t *)&cmd;
msgs[i].len = cmd_len;
i++;
}
if (len != 0)
{
msgs[i].addr = client->client_addr;
msgs[i].flags = OS_I2C_RD;
msgs[i].buf = buff;
msgs[i].len = len;
i++;
}
OS_ASSERT(i == 1 || i == 2);
if (os_i2c_transfer(client->bus, msgs, i) == i)
{
return OS_SUCCESS;
}
else
{
return OS_FAILURE;
}
}分步详解
第一步:准备计数器和消息数组(无条件执行)
int i = 0;
struct os_i2c_msg msgs[2];同 4.2 第一步。
第二步:有寄存器地址时构造"写地址"消息(条件 cmd_len != 0)
if (cmd_len != 0)
{
msgs[i].addr = client->client_addr;
msgs[i].flags = OS_I2C_WR;
msgs[i].buf = (uint8_t *)&cmd;
msgs[i].len = cmd_len;
i++;
}与 4.2 相同:先以写方向把寄存器地址 cmd 发给从机(告诉从机"接下来要访问哪个寄存器")。
第三步:有读取需求时构造"读数据"消息(条件 len != 0,方向为读)
if (len != 0)
{
msgs[i].addr = client->client_addr;
msgs[i].flags = OS_I2C_RD;
msgs[i].buf = buff;
msgs[i].len = len;
i++;
}当需要读数据时(len != 0),构造第二条消息:方向为 OS_I2C_RD(读),buf 指向接收缓冲区。注意这里没有 OS_I2C_NO_START——因为从"写地址"切换到"读数据"涉及方向切换,底层驱动需要通过 Restart 重新发送地址并把地址字节最低位(R/W 位)置 1(读),因此需要重新处理地址,不能简单地"不发送起始条件"。
关键理解——为什么读消息不带
OS_I2C_NO_START而写消息带:
- 写数据的第二条消息(4.2):地址和方向都没变(都是写),只是继续发更多数据,所以可以
OS_I2C_NO_START直接衔接,不重新发地址。- 读数据的第二条消息:方向从"写"变成"读",必须重新以读方向发送地址(这是 I2C 协议规定的 Restart 语义),所以不能带
OS_I2C_NO_START,底层要重新发起始条件 + 读地址。
第四步:断言消息条数合法(无条件执行)
OS_ASSERT(i == 1 || i == 2);同 4.2 第四步,防止空传输。
第五步:批量传输并判断结果(无条件执行)
if (os_i2c_transfer(client->bus, msgs, i) == i)
{
return OS_SUCCESS;
}
else
{
return OS_FAILURE;
}条件走向总结
os_i2c_client_read() 条件走向总结:
├── cmd_len != 0(有寄存器地址)
│ └── msgs[0] = {addr, OS_I2C_WR, &cmd, cmd_len},i=1
├── cmd_len == 0(无地址,直接读)
│ └── 跳过地址消息
├── len != 0(有读需求)
│ └── msgs[i] = {addr, OS_I2C_RD, buff, len},i=2(或 i=1)
├── len == 0(无读需求)
│ └── 跳过读消息
├── OS_ASSERT(i == 1 || i == 2)(cmd_len==0 且 len==0 时断言失败)
└── os_i2c_transfer(client->bus, msgs, i) == i
├── 是(全部成功)→ 返回 OS_SUCCESS
└── 否(部分失败)→ 返回 OS_FAILURE4.4 os_i2c_client_write_byte / os_i2c_client_read_byte —— 客户端单字节读写
函数签名
os_err_t os_i2c_client_write_byte(struct os_i2c_client *client, uint32_t cmd, uint8_t cmd_len, uint8_t data)
uint8_t os_i2c_client_read_byte(struct os_i2c_client *client, uint32_t cmd, uint8_t cmd_len)参数 / 返回值说明
os_i2c_client_write_byte:
| 参数 | 类型 | 说明 |
|---|---|---|
client | struct os_i2c_client * | I2C 客户端指针 |
cmd | uint32_t | 寄存器地址或命令字 |
cmd_len | uint8_t | 寄存器地址长度 |
data | uint8_t | 要写入的单个字节 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 写入成功 |
OS_FAILURE | 写入失败 |
os_i2c_client_read_byte:
| 参数 | 类型 | 说明 |
|---|---|---|
client | struct os_i2c_client * | I2C 客户端指针 |
cmd | uint32_t | 寄存器地址或命令字 |
cmd_len | uint8_t | 寄存器地址长度 |
| 返回值 | 说明 |
|---|---|
value | 读取到的字节数据(失败时返回 0,因为 value 初始化为 0) |
完整源码
os_err_t os_i2c_client_write_byte(struct os_i2c_client *client, uint32_t cmd, uint8_t cmd_len, uint8_t data)
{
return os_i2c_client_write(client, cmd, cmd_len, &data, 1);
}uint8_t os_i2c_client_read_byte(struct os_i2c_client *client, uint32_t cmd, uint8_t cmd_len)
{
uint8_t value = 0;
os_i2c_client_read(client, cmd, cmd_len, &value, 1);
return value;
}分步详解
os_i2c_client_write_byte:第一步 —— 委托给 os_i2c_client_write(无条件执行)
return os_i2c_client_write(client, cmd, cmd_len, &data, 1);这只是 os_i2c_client_write 的便捷封装:把单个字节 data 的地址当作缓冲区 &data,长度固定为 1。这样就不用为写一个字节专门构造 buff 数组。
os_i2c_client_read_byte:第一步 —— 声明并初始化 value(无条件执行)
uint8_t value = 0;初始化 value 为 0。这是关键:若读取失败,value 保持 0,避免返回未初始化栈垃圾值。
os_i2c_client_read_byte:第二步 —— 委托读取单字节并返回(无条件执行)
os_i2c_client_read(client, cmd, cmd_len, &value, 1);
return value;调用 os_i2c_client_read 把 1 个字节读到 value 中,再返回 value。
关键点:
os_i2c_client_read_byte没有检查返回值——它把失败与否的判断责任交给调用者(读到的0可能是"真的寄存器值是 0",也可能是"读取失败")。这在需要严格区分失败时要注意,建议在关键路径上用os_i2c_client_read(能拿到OS_FAILURE)代替。
条件走向总结
os_i2c_client_write_byte():纯顺序执行(无分支)
└── return os_i2c_client_write(client, cmd, cmd_len, &data, 1) // 结果交给 os_i2c_client_write 的条件走向
os_i2c_client_read_byte():纯顺序执行(无分支)
├── uint8_t value = 0
├── os_i2c_client_read(client, cmd, cmd_len, &value, 1)
└── return value // 无论成败都返回 value(失败时为 0)4.5 os_i2c_master_send —— 主机发送
函数签名
os_size_t os_i2c_master_send(struct os_i2c_bus_device *bus,
uint16_t addr,
uint16_t flags,
const uint8_t *buf,
uint32_t count)参数 / 返回值说明
| 参数 | 类型 | 说明 |
|---|---|---|
bus | struct os_i2c_bus_device * | I2C 总线设备指针 |
addr | uint16_t | 从设备地址 |
flags | uint16_t | 传输标志,通常为 OS_I2C_WR |
buf | const uint8_t * | 待发送数据缓冲区 |
count | uint32_t | 发送字节数 |
| 返回值 | 说明 |
|---|---|
count | 传输成功(os_i2c_transfer 返回 > 0)时,返回期望发送的字节数 count |
| 其他 | 传输失败时返回错误码(os_i2c_transfer 的返回值) |
完整源码
os_size_t os_i2c_master_send(struct os_i2c_bus_device *bus,
uint16_t addr,
uint16_t flags,
const uint8_t *buf,
uint32_t count)
{
os_err_t ret;
struct os_i2c_msg msg;
msg.addr = addr;
msg.flags = flags;
msg.len = count;
msg.buf = (uint8_t *)buf;
ret = os_i2c_transfer(bus, &msg, 1);
return (ret > 0) ? count : ret;
}分步详解
第一步:声明局部变量(无条件执行)
os_err_t ret;
struct os_i2c_msg msg;ret 保存传输结果;msg 是本次要构造的单条消息。
第二步:填充单条消息并传输(无条件执行)
msg.addr = addr;
msg.flags = flags;
msg.len = count;
msg.buf = (uint8_t *)buf;
ret = os_i2c_transfer(bus, &msg, 1);把 addr、flags、count、buf 填入消息结构体,然后调用 os_i2c_transfer(bus, &msg, 1) 发送这 1 条写消息。
第三步:根据传输结果返回(无条件执行,含三元分支)
return (ret > 0) ? count : ret;- 若
ret > 0(传输成功),返回count(期望发送的字节数,不是实际条数)。 - 否则返回
ret(错误码)。
关键理解——返回语义:
os_i2c_transfer返回的是"成功的消息条数"(这里是 1 或 0),而os_i2c_master_send面向用户暴露的是"发送了多少字节"。成功时直接返回count,让调用者判断ret == count即可确认发送完成。
条件走向总结
os_i2c_master_send() 条件走向总结:
├── 构造单条写消息 msg{addr, flags, count, buf}
├── ret = os_i2c_transfer(bus, &msg, 1)(底层传输)
└── ret > 0(传输成功)→ 返回 count(期望发送字节数)
└── ret <= 0(传输失败)→ 返回 ret(错误码)4.6 os_i2c_master_recv —— 主机接收
函数签名
os_size_t os_i2c_master_recv(struct os_i2c_bus_device *bus,
uint16_t addr,
uint16_t flags,
uint8_t *buf,
uint32_t count)参数 / 返回值说明
| 参数 | 类型 | 说明 |
|---|---|---|
bus | struct os_i2c_bus_device * | I2C 总线设备指针 |
addr | uint16_t | 从设备地址 |
flags | uint16_t | 传输标志,通常为 OS_I2C_RD |
buf | uint8_t * | 接收数据缓冲区 |
count | uint32_t | 期望接收的字节数 |
| 返回值 | 说明 |
|---|---|
count | 传输成功(os_i2c_transfer 返回 > 0)时,返回期望接收的字节数 count |
| 其他 | 传输失败时返回错误码 |
完整源码
os_size_t os_i2c_master_recv(struct os_i2c_bus_device *bus,
uint16_t addr,
uint16_t flags,
uint8_t *buf,
uint32_t count)
{
os_err_t ret;
struct os_i2c_msg msg;
OS_ASSERT(bus != OS_NULL);
msg.addr = addr;
msg.flags = flags | OS_I2C_RD;
msg.len = count;
msg.buf = buf;
ret = os_i2c_transfer(bus, &msg, 1);
return (ret > 0) ? count : ret;
}分步详解
第一步:声明变量并断言总线非空(无条件执行)
os_err_t ret;
struct os_i2c_msg msg;
OS_ASSERT(bus != OS_NULL);OS_ASSERT(bus != OS_NULL) 校验总线指针非空。为什么这里要断言而 os_i2c_master_send 不用:因为本函数会写入 buf 并从 bus 传输,若 bus 为空会立即空指针崩溃,断言能尽早暴露调用错误。这是"参数校验"的体现。
第二步:填充消息(读方向)并传输(无条件执行)
msg.addr = addr;
msg.flags = flags | OS_I2C_RD;
msg.len = count;
msg.buf = buf;
ret = os_i2c_transfer(bus, &msg, 1);关键点:msg.flags = flags | OS_I2C_RD —— 无论调用者传什么 flags,都强制置上读方向位 OS_I2C_RD。这保证接收操作一定是读方向,避免调用者忘了置位导致方向错误。与 4.5 的 msg.flags = flags(不强制置位)形成对比。
第三步:根据传输结果返回(无条件执行,含三元分支)
return (ret > 0) ? count : ret;成功返回 count,失败返回错误码 ret。
条件走向总结
os_i2c_master_recv() 条件走向总结:
├── OS_ASSERT(bus != OS_NULL)(bus 为空时断言失败)
├── 构造单条读消息 msg{addr, flags|OS_I2C_RD, count, buf}(强制置读方向位)
├── ret = os_i2c_transfer(bus, &msg, 1)(底层传输)
└── ret > 0(传输成功)→ 返回 count(期望接收字节数)
└── ret <= 0(传输失败)→ 返回 ret(错误码)4.7 设备框架层封装(i2c_bus_device_read/write/control)
函数签名
static os_ssize_t i2c_bus_device_read(os_device_t *dev, os_off_t pos, void *buffer, os_size_t count)
static os_ssize_t i2c_bus_device_write(os_device_t *dev, os_off_t pos, const void *buffer, os_size_t count)
static os_err_t i2c_bus_device_control(os_device_t *dev, int cmd, void *args)这三个 static 函数是 I2C 总线设备注册进 5.1 设备框架后,被 os_device_read/write/control 框架层调用的入口。它们把 pos 参数拆分成"高 16 位 = flags、低 16 位 = 地址",再转发给 os_i2c_master_send/recv 或 os_i2c_transfer。
完整源码
static os_ssize_t i2c_bus_device_read(os_device_t *dev, os_off_t pos, void *buffer, os_size_t count)
{
uint16_t addr = 0;
uint16_t flags = 0;
struct os_i2c_bus_device *bus = (struct os_i2c_bus_device *)dev->user_data;
OS_ASSERT(bus != OS_NULL);
OS_ASSERT(buffer != OS_NULL);
LOG_D(DBG_EXT_TAG, "I2C bus dev [%s] reading %u bytes.", device_name(dev), count);
addr = pos & 0xffff;
flags = (pos >> 16) & 0xffff;
return os_i2c_master_recv(bus, addr, flags, (uint8_t *)buffer, count);
}static os_ssize_t i2c_bus_device_write(os_device_t *dev, os_off_t pos, const void *buffer, os_size_t count)
{
uint16_t addr = 0;
uint16_t flags = 0;
struct os_i2c_bus_device *bus = (struct os_i2c_bus_device *)dev->user_data;
OS_ASSERT(bus != OS_NULL);
OS_ASSERT(buffer != OS_NULL);
LOG_D(DBG_EXT_TAG, "I2C bus dev [%s] writing %u bytes.", device_name(dev), count);
addr = pos & 0xffff;
flags = (pos >> 16) & 0xffff;
return os_i2c_master_send(bus, addr, flags, (const uint8_t *)buffer, count);
}static os_err_t i2c_bus_device_control(os_device_t *dev, int cmd, void *args)
{
os_err_t ret;
struct os_i2c_priv_data *priv_data;
struct os_i2c_bus_device *bus = (struct os_i2c_bus_device *)dev->user_data;
OS_ASSERT(bus != OS_NULL);
switch (cmd)
{
case OS_I2C_DEV_CTRL_10BIT:
bus->flags |= OS_I2C_ADDR_10BIT;
break;
case OS_I2C_DEV_CTRL_ADDR:
bus->addr = *(uint16_t *)args;
break;
case OS_I2C_DEV_CTRL_TIMEOUT:
bus->timeout = *(uint32_t *)args;
break;
case OS_I2C_DEV_CTRL_RW:
priv_data = (struct os_i2c_priv_data *)args;
ret = os_i2c_transfer(bus, priv_data->msgs, priv_data->number);
if (ret != priv_data->number)
{
return OS_EIO;
}
break;
default:
break;
}
return OS_SUCCESS;
}分步详解(i2c_bus_device_control)
第一步:获取总线指针并断言非空(无条件执行)
os_err_t ret;
struct os_i2c_priv_data *priv_data;
struct os_i2c_bus_device *bus = (struct os_i2c_bus_device *)dev->user_data;
OS_ASSERT(bus != OS_NULL);从 dev->user_data 取回注册时存放的总线指针(见 4.8 os_i2c_bus_device_register),并断言非空。
第二步:按命令分发(条件 cmd 的值)
switch (cmd)
{
case OS_I2C_DEV_CTRL_10BIT:
bus->flags |= OS_I2C_ADDR_10BIT;
break;
case OS_I2C_DEV_CTRL_ADDR:
bus->addr = *(uint16_t *)args;
break;
case OS_I2C_DEV_CTRL_TIMEOUT:
bus->timeout = *(uint32_t *)args;
break;
case OS_I2C_DEV_CTRL_RW:
priv_data = (struct os_i2c_priv_data *)args;
ret = os_i2c_transfer(bus, priv_data->msgs, priv_data->number);
if (ret != priv_data->number)
{
return OS_EIO;
}
break;
default:
break;
}OS_I2C_DEV_CTRL_10BIT:把bus->flags的 bit2 置位(|= OS_I2C_ADDR_10BIT),启用 10 位地址。OS_I2C_DEV_CTRL_ADDR:从args读uint16_t赋给bus->addr(设置总线自身地址)。OS_I2C_DEV_CTRL_TIMEOUT:从args读uint32_t赋给bus->timeout(设置超时)。OS_I2C_DEV_CTRL_RW:把args强转为struct os_i2c_priv_data *,取出msgs和number交给os_i2c_transfer执行批量传输;若返回条数与number不符返回OS_EIO。default:未知命令直接跳过。
数值演算(
OS_I2C_DEV_CTRL_10BIT置位):设bus->flags原为0b0000_0000,执行bus->flags |= OS_I2C_ADDR_10BIT(0x0004):
计算项 代入计算 结果 原 flags 0b0000_00000x0000与掩码或运算 0b0000_0000 | 0b0000_01000b0000_0100=0x0004
第三步:返回成功(无条件执行)
return OS_SUCCESS;除 OS_I2C_DEV_CTRL_RW 失败提前返回 OS_EIO 外,其余命令最终返回 OS_SUCCESS。
条件走向总结(i2c_bus_device_control)
i2c_bus_device_control() 条件走向总结:
├── OS_ASSERT(bus != OS_NULL)
├── switch(cmd)
│ ├── OS_I2C_DEV_CTRL_10BIT → bus->flags |= OS_I2C_ADDR_10BIT,break
│ ├── OS_I2C_DEV_CTRL_ADDR → bus->addr = *(uint16_t *)args,break
│ ├── OS_I2C_DEV_CTRL_TIMEOUT → bus->timeout = *(uint32_t *)args,break
│ ├── OS_I2C_DEV_CTRL_RW
│ │ ├── ret = os_i2c_transfer(bus, priv_data->msgs, priv_data->number)
│ │ ├── ret != priv_data->number → 返回 OS_EIO(传输不完整)
│ │ └── ret == priv_data->number → break,返回 OS_SUCCESS
│ └── default → break
└── return OS_SUCCESS分步详解(i2c_bus_device_read / i2c_bus_device_write)
第一步:获取总线指针并断言(无条件执行)
uint16_t addr = 0;
uint16_t flags = 0;
struct os_i2c_bus_device *bus = (struct os_i2c_bus_device *)dev->user_data;
OS_ASSERT(bus != OS_NULL);
OS_ASSERT(buffer != OS_NULL);从 dev->user_data 取总线指针,断言 bus 与 buffer 均非空(buffer 是读出/写入的数据缓冲区,必须有效)。
第二步:从 pos 拆出地址与标志(无条件执行)
addr = pos & 0xffff;
flags = (pos >> 16) & 0xffff;pos 是一个 os_off_t(64 位有符号偏移)。I2C 用它编码两个信息:低 16 位 = 从设备地址,高 16 位 = 传输标志。
数值演算:设要从地址
0x50、标志OS_I2C_RD(0x0001) 的从机读数据,则pos由调用方构造成(0x0001 << 16) | 0x0050 = 0x0001_0050:
计算项 代入计算 结果 addr = pos & 0xffff0x0001_0050 & 0x0000_ffff0x0050flags = (pos >> 16) & 0xffff(0x0001_0050 >> 16) & 0xffff0x0001调用 os_i2c_master_recv(bus, 0x0050, 0x0001, buffer, count)读 0x50 从机
第三步:转发给高层接口并返回(无条件执行)
return os_i2c_master_recv(bus, addr, flags, (uint8_t *)buffer, count);(write 版本为 return os_i2c_master_send(bus, addr, flags, (const uint8_t *)buffer, count);)
把拆分出的 addr、flags 连同 buffer、count 传给 os_i2c_master_recv/send,其返回值即最终结果。
4.8 os_i2c_bus_device_register / os_i2c_bus_device_find —— 总线注册与查找
函数签名
os_err_t os_i2c_bus_device_register(struct os_i2c_bus_device *device, const char *bus_name, void *data)
struct os_i2c_bus_device *os_i2c_bus_device_find(const char *bus_name)参数 / 返回值说明
os_i2c_bus_device_register:
| 参数 | 类型 | 说明 |
|---|---|---|
device | struct os_i2c_bus_device * | 待注册的总线设备指针 |
bus_name | const char * | 总线名称,如 "i2c1" |
data | void * | 用户数据,通常为 MCU 特定的 I2C 外设配置 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 注册成功 |
os_i2c_bus_device_find:
| 参数 | 类型 | 说明 |
|---|---|---|
bus_name | const char * | 要查找的总线名称 |
| 返回值 | 说明 |
|---|---|
| 总线设备指针 | 查找成功 |
OS_NULL | 未找到(或类型不匹配) |
完整源码
os_err_t os_i2c_bus_device_register(struct os_i2c_bus_device *device, const char *bus_name, void *data)
{
OS_ASSERT(device != OS_NULL);
if (device->timeout == 0)
{
device->timeout = OS_TICK_PER_SECOND;
}
/* set device type */
device->parent.type = OS_DEVICE_TYPE_I2CBUS;
/* initialize device interface */
device->parent.ops = &i2c_ops;
device->parent.user_data = data;
device->lock = os_mutex_create(OS_NULL, "i2c_bus_lock", OS_FALSE);
/* register to device manager */
os_device_register(&device->parent, bus_name);
return OS_SUCCESS;
}struct os_i2c_bus_device *os_i2c_bus_device_find(const char *bus_name)
{
struct os_i2c_bus_device *bus;
os_device_t *dev = os_device_find(bus_name);
if (dev == OS_NULL || dev->type != OS_DEVICE_TYPE_I2CBUS)
{
LOG_E(DBG_EXT_TAG, "I2C bus %s not exist", bus_name);
return OS_NULL;
}
bus = (struct os_i2c_bus_device *)dev->user_data;
return bus;
}分步详解(os_i2c_bus_device_register)
第一步:断言设备指针非空(无条件执行)
OS_ASSERT(device != OS_NULL);注册前必须保证 device 有效,否则后续访问 device->timeout 会空指针崩溃。
第二步:为超时时间设默认值(条件 device->timeout == 0)
if (device->timeout == 0)
{
device->timeout = OS_TICK_PER_SECOND;
}为什么默认设为 OS_TICK_PER_SECOND:如果 BSP 驱动在注册前没有显式设置 timeout(为 0),框架给它一个合理默认值(一个系统 tick 周期对应的 tick 数),避免后续传输因超时时间为 0 而立刻超时失败。这体现了"提供合理默认值,防呆"的设计。
第三步:配置父设备类型、ops、user_data(无条件执行)
/* set device type */
device->parent.type = OS_DEVICE_TYPE_I2CBUS;
/* initialize device interface */
device->parent.ops = &i2c_ops;
device->parent.user_data = data;parent.type = OS_DEVICE_TYPE_I2CBUS:标记设备类型为 I2C 总线(供os_i2c_bus_device_find校验)。parent.ops = &i2c_ops:挂上框架层操作表(i2c_bus_device_read/write/control)。parent.user_data = data:把用户数据(MCU 配置)存入,同时i2c_bus_device_*通过dev->user_data取出的是"总线设备指针"或"用户数据"。
i2c_ops表的来源(drivers/i2c/i2c.c):/* clang-format off */ const static struct os_device_ops i2c_ops = { .read = i2c_bus_device_read, .write = i2c_bus_device_write, .control = i2c_bus_device_control }; /* clang-format on */
第四步:创建总线互斥锁(无条件执行)
device->lock = os_mutex_create(OS_NULL, "i2c_bus_lock", OS_FALSE);创建非递归互斥锁(OS_FALSE),命名为 "i2c_bus_lock",存到 device->lock,供 os_i2c_transfer 加锁/解锁使用。
为什么用互斥锁而非常量/自旋锁:I2C 传输是阻塞式、可睡眠的(可能等待从机 ACK),需要能阻塞等待的互斥锁;而自旋锁/关中断会在等待时占用 CPU 且无法睡眠,不适合 I2C 这种慢速外设的长时间传输。
第五步:注册到设备管理器(无条件执行)
/* register to device manager */
os_device_register(&device->parent, bus_name);
return OS_SUCCESS;把 device->parent 以 bus_name 注册到设备管理器,注册成功后返回 OS_SUCCESS。
分步详解(os_i2c_bus_device_find)
第一步:按名称查找设备(无条件执行)
struct os_i2c_bus_device *bus;
os_device_t *dev = os_device_find(bus_name);调用 5.1 框架的 os_device_find(bus_name) 在设备管理器中按名字查找。
第二步:校验设备存在且类型匹配(条件 dev == OS_NULL || dev->type != OS_DEVICE_TYPE_I2CBUS)
if (dev == OS_NULL || dev->type != OS_DEVICE_TYPE_I2CBUS)
{
LOG_E(DBG_EXT_TAG, "I2C bus %s not exist", bus_name);
return OS_NULL;
}为什么还要校验 dev->type:os_device_find 只按名字返回设备,不区分类型。若找到了一个同名但类型不是 I2C 总线的设备(例如把某个字符设备误命名为 "i2c1"),直接当成 I2C 总线使用会类型错乱。校验 dev->type == OS_DEVICE_TYPE_I2CBUS 确保找到的确实是 I2C 总线设备。任一条件不满足则打印错误并返回 OS_NULL。
第三步:取回总线指针并返回(无条件执行)
bus = (struct os_i2c_bus_device *)dev->user_data;
return bus;从 dev->user_data 取回总线设备指针(注意:os_i2c_bus_device_register 中 user_data 存的是 data 用户数据,而此处的 dev->user_data 指向的是总线设备对象本身,这是注册时由底层/框架放置的),返回给调用者。
条件走向总结(os_i2c_bus_device_find)
os_i2c_bus_device_find() 条件走向总结:
├── dev = os_device_find(bus_name)
├── dev == OS_NULL(未找到)→ LOG_E + 返回 OS_NULL
├── dev->type != OS_DEVICE_TYPE_I2CBUS(类型不匹配)→ LOG_E + 返回 OS_NULL
└── 找到且类型正确 → bus = (os_i2c_bus_device *)dev->user_data,返回 bus五、使用示例
以下示例均基于项目头文件
drivers/i2c/i2c.h的接口。示例使用了os_i2c_bus_device_find、os_i2c_master_send/recv、os_i2c_client_write/read、os_i2c_transfer、os_device_control等 API,均含返回值检查。
示例 1:I2C 主机发送与接收
#include <i2c.h>
#include <os_task.h>
#define SENSOR_ADDR 0x48 /* 传感器 7 位地址 */
void i2c_master_demo(void)
{
struct os_i2c_bus_device *bus;
uint8_t tx_data[2] = {0x00, 0x55}; /* 寄存器地址 0x00,写入数据 0x55 */
uint8_t rx_data[4] = {0};
/* 1. 查找 I2C 总线 */
bus = os_i2c_bus_device_find("i2c1");
if (bus == OS_NULL)
{
os_kprintf("I2C bus not found!\r\n");
return;
}
/* 2. 主机发送:向传感器写配置 */
os_size_t ret = os_i2c_master_send(bus, SENSOR_ADDR, OS_I2C_WR, tx_data, 2);
if (ret != 2)
{
os_kprintf("I2C master send failed: %d\r\n", ret);
return;
}
os_kprintf("Sent %d bytes to sensor\r\n", ret);
/* 3. 主机接收:从传感器读取数据 */
ret = os_i2c_master_recv(bus, SENSOR_ADDR, OS_I2C_RD, rx_data, 4);
if (ret != 4)
{
os_kprintf("I2C master recv failed: %d\r\n", ret);
return;
}
os_kprintf("Received: %02X %02X %02X %02X\r\n",
rx_data[0], rx_data[1], rx_data[2], rx_data[3]);
}实际效果:总线存在时,先向地址 0x48 的从机写入 0x00 0x55(寄存器地址 + 数据),再读取 4 个字节并打印。若发送字节数不为 2 或接收字节数不为 4,说明传输失败,打印错误码。
示例 2:I2C 客户端读写寄存器(模拟 EEPROM)
#include <i2c.h>
#include <os_task.h>
#define EEPROM_ADDR 0x50 /* EEPROM 7 位地址 */
void i2c_client_eeprom_demo(void)
{
struct os_i2c_bus_device *bus;
struct os_i2c_client client;
uint8_t write_buf[8] = "OneOS!";
uint8_t read_buf[8] = {0};
/* 1. 查找 I2C 总线 */
bus = os_i2c_bus_device_find("i2c1");
if (bus == OS_NULL)
{
os_kprintf("I2C bus not found!\r\n");
return;
}
/* 2. 构造客户端结构体 */
client.bus = bus;
client.client_addr = EEPROM_ADDR;
/* 3. 向 EEPROM 地址 0x0000 写入数据(2 字节寄存器地址) */
os_err_t ret = os_i2c_client_write(&client, 0x0000, 2, write_buf, 6);
if (ret != OS_EOK)
{
os_kprintf("EEPROM write failed: %d\r\n", ret);
return;
}
os_kprintf("EEPROM write success: %s\r\n", write_buf);
/* 4. 从 EEPROM 地址 0x0000 读取数据 */
ret = os_i2c_client_read(&client, 0x0000, 2, read_buf, 6);
if (ret != OS_EOK)
{
os_kprintf("EEPROM read failed: %d\r\n", ret);
return;
}
os_kprintf("EEPROM read success: %s\r\n", read_buf);
}注意:
os_i2c_client_write/read返回的是OS_SUCCESS/OS_FAILURE,而 OneOS 中OS_SUCCESS与OS_EOK数值相同(均为 0),因此用ret != OS_EOK判断失败是可行的。也可统一用ret != OS_SUCCESS判断。示例沿用项目常见写法。
实际效果:向地址 0x50 的 EEPROM,以 2 字节寄存器地址(0x0000)先写入 "OneOS!"(6 字节),再从同地址读回 6 字节并打印。写入与读取都成功时打印对应成功信息。
示例 3:使用 I2C 消息批量传输
#include <i2c.h>
#define SENSOR_ADDR 0x68
os_size_t sensor_read_register(struct os_i2c_bus_device *bus,
uint8_t reg_addr, uint8_t *data, uint32_t len)
{
struct os_i2c_msg msgs[2];
/* 第一条消息:发送寄存器地址(写操作) */
msgs[0].addr = SENSOR_ADDR;
msgs[0].flags = OS_I2C_WR;
msgs[0].len = 1;
msgs[0].buf = ®_addr;
/* 第二条消息:读取数据(读操作) */
msgs[1].addr = SENSOR_ADDR;
msgs[1].flags = OS_I2C_RD;
msgs[1].len = len;
msgs[1].buf = data;
/* 批量传输:先写寄存器地址,再读数据 */
return os_i2c_transfer(bus, msgs, 2);
}
void i2c_msg_demo(void)
{
struct os_i2c_bus_device *bus;
uint8_t data[6] = {0};
bus = os_i2c_bus_device_find("i2c1");
if (bus == OS_NULL) return;
/* 读取传感器寄存器 0x3B 开始的 6 个字节 */
os_size_t ret = sensor_read_register(bus, 0x3B, data, 6);
if (ret == 2)
{
os_kprintf("Sensor data: %02X %02X %02X %02X %02X %02X\r\n",
data[0], data[1], data[2], data[3], data[4], data[5]);
}
}实际效果:手动构造两条消息——第一条以 OS_I2C_WR 写寄存器地址 0x3B,第二条以 OS_I2C_RD 读 6 个字节。os_i2c_transfer 一次性完成"先写地址再读数据",返回 2 表示两条消息都成功,随后打印读到的 6 字节。
与示例 2 的差异:示例 3 是手动构造两条
os_i2c_msg,第二条消息不带OS_I2C_NO_START(因此底层会以 Restart 重新发送读地址);而os_i2c_client_read(示例 2)内部自动构造的消息,其第二条读消息同样不带OS_I2C_NO_START。二者的底层时序一致(写地址 → Restart → 读数据)。
示例 4:I2C 总线控制(设置超时时间)
#include <i2c.h>
#include <device.h>
void i2c_config_demo(void)
{
struct os_i2c_bus_device *bus;
os_device_t *dev;
bus = os_i2c_bus_device_find("i2c1");
if (bus == OS_NULL) return;
/* 获取父设备指针 */
dev = (os_device_t *)bus;
/* 设置传输超时时间为 100ms */
os_uint32_t timeout = 100;
os_device_control(dev, OS_I2C_DEV_CTRL_TIMEOUT, &timeout);
os_kprintf("I2C timeout set to %d ms\r\n", timeout);
}实际效果:找到 "i2c1" 总线后,把总线设备指针强转为 os_device_t *,通过 os_device_control 发送 OS_I2C_DEV_CTRL_TIMEOUT 命令,把 bus->timeout 设置为 100(os_i2c_bus_device_control 中 bus->timeout = *(uint32_t *)args)。设置完成后打印。
七、更换 MCU(I2C 驱动移植)—— 全局视角
本节从全局视角讲解:当你的项目要换一颗新的 MCU 时,OneOS 的 I2C 设备驱动要做什么才能在新平台上跑起来。核心结论和 5.2 GPIO 一样:应用层与框架层完全不用动,只需要新写/移植 BSP 层的 I2C 驱动。
在动手移植之前,先认识 BSP 层驱动文件 drv_i2c.c 到底承担了什么职责——这是理解移植步骤的前提。
7.1 认识 BSP 层驱动:drv_i2c.c
BSP 层的 I2C 驱动主要由 drv_i2c.c 承担(部分厂商另有 drv_i2c.h 声明总线实例的引脚配置宏与信息结构体):
| 文件 | 作用 |
|---|---|
drv_i2c.h | (部分厂商有)声明"总线实例"的引脚/时钟宏、struct xxx_i2c_info(对外"契约") |
drv_i2c.c | 实现 struct os_i2c_bus_device_ops 三个函数指针 + 定义 struct xxx_i2c 总线实例结构 + probe 注册(真正操作硬件) |
以 STM32 为例(drivers/hal/st/drivers/drv_i2c.c),它做了这几件事:
(1)定义"总线实例"结构体 struct stm32_i2c
stm32_i2c 内嵌了框架层的 struct os_i2c_bus_device(这就是 4.8 注册时的 device),并扩展出 MCU 相关的硬件句柄、引脚配置、传输信号量:
struct stm32_i2c
{
struct os_i2c_bus_device i2c;
I2C_HandleTypeDef *hi2c;
struct i2c_config config;
os_semaphore_id i2c_sem;
os_list_node_t list;
};i2c:内嵌的struct os_i2c_bus_device,是注册进设备框架的"公共部分"。hi2c:指向 STM32 HAL 的 I2C 句柄(对应具体 I2C 外设如 I2C1/I2C2)。config:记录 SCL/SDA 引脚。i2c_sem:传输完成信号量(中断回调里os_semaphore_post,传输函数里os_semaphore_wait)。list:用于把多个 I2C 实例挂到全局链表stm32_i2c_list(供中断回调按hi2c找到对应实例)。
(2)实现 struct os_i2c_bus_device_ops 的三个函数指针并聚合
static const struct os_i2c_bus_device_ops i2c_bus_ops = {
.i2c_transfer = stm32_i2c_transfer,
.i2c_slave_transfer = stm32_i2c_slave_transfer,
.i2c_bus_control = stm32_i2c_bus_control,
};stm32_i2c_transfer 是核心:它解析 msgs[],把"带 OS_I2C_NO_START 且地址方向相同的连续消息合并",然后调用 HAL_I2C_Master_Transmit/Receive(_DMA) 等 HAL 函数完成实际时序。
(3)写 probe 函数并通过 OS_DRIVER_DEFINE 注册
static int stm32_i2c_probe(const os_driver_info_t *drv, const os_device_info_t *dev)
{
os_ubase_t level;
os_err_t result = 0;
char sem_name[16] = "sem_";
struct stm32_i2c_info *i2c_info = (struct stm32_i2c_info *)dev->info;
struct stm32_i2c *st_i2c = os_calloc(1, sizeof(struct stm32_i2c));
OS_ASSERT(st_i2c);
strcat(sem_name, dev->name);
st_i2c->i2c_sem = os_semaphore_create(OS_NULL, sem_name, 0, OS_SEM_MAX_VALUE);
st_i2c->hi2c = i2c_info->instance;
/* 此处调用了 stm32_check_and_solve_bus_busy(st_i2c) 检查并解锁总线 */
struct os_i2c_bus_device *dev_i2c = &st_i2c->i2c;
dev_i2c->ops = &i2c_bus_ops;
/* 此处调用了 os_spin_lock_irqsave 加锁并 os_list_add_tail 挂入 stm32_i2c_list */
result = os_i2c_bus_device_register(dev_i2c, dev->name, dev_i2c);
OS_ASSERT(result == OS_SUCCESS);
return result;
}
OS_DRIVER_INFO stm32_i2c_driver = {
.name = "I2C_HandleTypeDef",
.probe = stm32_i2c_probe,
};
OS_DRIVER_DEFINE(stm32_i2c_driver, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_HIGH);关键理解:probe 里做三件事——①从 dev->info(由厂商设备表提供)取硬件句柄/引脚/时钟;②给总线实例 dev_i2c->ops 挂上 ops 表;③调用框架层的 os_i2c_bus_device_register(见 4.8)注册到设备管理器。OS_DRIVER_DEFINE 让驱动在设备初始化阶段被自动 probe。
说明:上面 probe 片段仅摘取与注册主线相关的语句(
stm32_check_and_solve_bus_busy检查、gs_device_lock加锁挂链表等中间语句未在此展开),完整实现见项目drivers/hal/st/drivers/drv_i2c.c。移植时关键是把握"取硬件信息 → 填 ops →os_i2c_bus_device_register"这条主线。
(4)不同 MCU 的同类文件
项目 drivers/hal/ 下每个 MCU 厂商都有一份类似驱动,它们都实现同一个 struct os_i2c_bus_device_ops 契约,只是内部换成各自的寄存器/HAL:
| 厂商 | 路径 | 区别 |
|---|---|---|
| ST | drivers/hal/st/drivers/drv_i2c.c | 用 STM32 HAL(HAL_I2C_Master_*),无 .h,配置在 .c 内 |
| GD | drivers/hal/gd/drivers/f403/drv_i2c.c | 用 GD32 库,drv_i2c.h 定义 EVAL_I2C0_SCL_PIN 等引脚宏与 gd_i2c_info |
| NRF | drivers/hal/nrf/drivers/nrf52/drv_i2c.c | 用 nRF5 SDK 的 TWI 模块 |
关键理解:这些 drv_i2c.c 是并列的"平级实现",不是"一根总线上挂了多个控制器"。它们的存在意义是:同一套应用代码,通过换一个 BSP 驱动文件,就能跑在不同 MCU 上——这正是移植/跨平台的核心。
(5)"根据所选 MCU 决定"到底是怎么决定的?—— 构建系统的三层选择机制
这是理解 OneOS 跨平台编译的关键。项目不是把 drivers/hal/ 下所有 drv_i2c.c 都编译进去,而是通过 Kconfig 配置 + weave.yaml 条件编译,只编译当前所选 MCU 的那一份。整个选择由三层机制完成(与 5.2 GPIO 的机制完全一致,只是把 drv_gpio.c 换成 drv_i2c.c):
第一层:选择"厂商宏"(决定"用哪家")
在 templates/configs/Kconfig 里,通过 choice 让用户选择 MCU 厂商,选中的厂商会定义一个厂商宏:
choice
prompt "MANUFACTOR"
default MANUFACTOR_STM32
config MANUFACTOR_STM32
bool "STM32"
config MANUFACTOR_GD32
bool "GD32"
# 其他厂商选项依此类推
endchoice选 STM32 → 定义 MANUFACTOR_STM32;选 GD32 → 定义 MANUFACTOR_GD32。
第二层:SRC_HAL 决定"加载哪家 HAL 目录"
templates/configs/SConscript 里有一个厂商宏 → HAL 目录名的映射表(构建脚本据此算出 SRC_HAL):
inq_table['MANUFACTOR_STM32'] = 'st'
inq_table['MANUFACTOR_GD32'] = 'gd'而 drivers/hal/Kconfig 用它动态加载对应厂商的 Kconfig:
menu "HAL"
source "$OS_ROOT/drivers/hal/$(SRC_HAL)/Kconfig"
endmenu即:选了 STM32 → SRC_HAL = st → 只加载 drivers/hal/st/ 的配置;选了 GD32 → SRC_HAL = gd → 只加载 drivers/hal/gd/。
第三层:weave.yaml 决定"该厂商下哪些源文件被编译"
每个厂商的驱动目录里有一个 weave.yaml,用 depend_macro 和 source_file 里的条件表达式 ? {is_define(['OS_USING_I2C', 'HAL_I2C_MODULE_ENABLED'])} 做条件编译。以 ST 为例(drivers/hal/st/drivers/weave.yaml):
group_name: hal/drivers
# 依赖宏控:只有定义了 MANUFACTOR_STM32 才编译整个组
depend_macro:
- MANUFACTOR_STM32
source_file:
- drv_i2c.c ? {is_define(['OS_USING_I2C', 'HAL_I2C_MODULE_ENABLED'])}其中 drv_i2c.c ? {is_define(['OS_USING_I2C', 'HAL_I2C_MODULE_ENABLED'])} 的意思是:只有当同时定义了 OS_USING_I2C(使能 I2C 设备组件)和 HAL_I2C_MODULE_ENABLED(使能 I2C 硬件模块)时,才编译 drv_i2c.c。
完整链路图(以"选 STM32 + 开 I2C 组件"为例):
① 配置阶段
templates/configs/Kconfig
选择 MANUFACTOR_STM32 → 定义宏 MANUFACTOR_STM32
② 目录选择
SConscript: inq_table['MANUFACTOR_STM32'] = 'st' → SRC_HAL = st
drivers/hal/Kconfig: source "$OS_ROOT/hal/$(SRC_HAL)/Kconfig" → 加载 hal/st/Kconfig
③ 文件级条件编译
drivers/hal/st/drivers/weave.yaml
depend_macro: MANUFACTOR_STM32 (组级:必须选 STM32)
drv_i2c.c ? {is_define(['OS_USING_I2C',
'HAL_I2C_MODULE_ENABLED'])} (文件级:还要开 I2C 组件 + I2C 硬件)
↓ 条件都满足才编译
drv_i2c.c → 链接进固件结论:所以"根据所选 MCU 决定"的本质是——选 MCU → 定义厂商宏 → 映射到 SRC_HAL 目录 → 该目录下 depend_macro 命中 → 具体文件满足 is_define 才编译。换了 MCU 厂商,厂商宏变了,SRC_HAL 变了,编译的 drv_i2c.c 自然就换成新厂商的那一份。
7.2 移植前的全局视角:分层全景图
先回顾 I2C 设备的完整分层(本书 1.1 节已详解):
┌─────────────────────────────────────────────────────────────┐
│ 【应用层】 用户代码(i2c_master_demo / i2c_client_eeprom_demo) │
│ 用 os_i2c_transfer / os_i2c_client_* / os_i2c_master_* │
└──────────────────────────────┬──────────────────────────────┘
│ 调用 os_i2c_transfer → bus->ops->i2c_transfer
▼
┌─────────────────────────────────────────────────────────────┐
│ 【框架层】 drivers/i2c/i2c.c(与 MCU 无关,通用代码) │
│ os_i2c_transfer(加锁→调ops→解锁)/ os_i2c_client_* │
│ os_i2c_master_* / os_i2c_bus_device_register / find │
│ i2c_bus_device_read/write/control(框架层设备操作) │
└──────────────────────────────┬──────────────────────────────┘
│ 调用 bus->ops->i2c_transfer
▼
┌─────────────────────────────────────────────────────────────┐
│ 【BSP 层】 drivers/hal/xxx/drivers/drv_i2c.c(需按 MCU 实现) │
│ 实现 struct os_i2c_bus_device_ops 三个函数指针 │
│ 定义 struct xxx_i2c 总线实例 + 引脚/时钟配置 │
│ 真正操作寄存器(如 HAL_I2C_Master_Transmit) │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 【硬件层】 具体 MCU 的 I2C 外设寄存器(或引脚模拟时序) │
└─────────────────────────────────────────────────────────────┘移植的关键洞察:
- 只有 BSP 层和 MCU 强相关(依赖具体寄存器、HAL 库、引脚分配、时钟树)。
- 框架层和应用层与 MCU 无关(只依赖
drivers/i2c/i2c.h定义的抽象接口)。 - 所以"换 MCU"本质上 = 重写/移植 BSP 层的
drv_i2c.c(实现struct os_i2c_bus_device_ops三个函数 + 写 probe 注册)。
7.3 移植需要提供的"接口契约"
要移植,必须先满足 BSP 层与框架层之间的契约——也就是 struct os_i2c_bus_device_ops 的三个函数指针(见 2.5 节)必须全部实现,并映射到新 MCU 的硬件能力:
| ops 函数 | 需实现的能力 | 新 MCU 上对应什么 |
|---|---|---|
i2c_transfer | 主机模式批量传输(解析 msgs[]、按标志发 Start/Restart/Stop、切换读写方向) | 调用新 MCU 的 I2C 主机发送/接收 HAL 或寄存器操作 |
i2c_slave_transfer | 从机模式响应外部主机访问 | 配置新 MCU 的 I2C 从机模式、地址匹配中断 |
i2c_bus_control | 总线控制(速率、模式切换等,框架层 i2c.c 未直接调用,留待 BSP) | 配置新 MCU 的 I2C 速率寄存器、模式位 |
此外还需要:
- 总线实例结构体
struct xxx_i2c:内嵌struct os_i2c_bus_device,并扩展硬件句柄、SCL/SDA 引脚、时钟源(如 ST 的struct stm32_i2c)。 - 传输完成信号量/同步机制:中断传输模式下,需要在"传输完成中断回调"里唤醒传输函数(如 ST 用
i2c_sem+os_semaphore_post/wait)。 - 初始化/probe 函数:取硬件信息、填
ops,最后调用os_i2c_bus_device_register(&xxx_i2c->i2c, bus_name, user_data)(见 4.8),并通过OS_DRIVER_DEFINE让系统自动调用。
7.4 移植的标准流程(5 步)
下面以"把 I2C 驱动从 STM32 移植到一颗新 MCU"为例,给出通用流程:
第 1 步:定义总线实例结构体与引脚/时钟配置
/* 内嵌框架层 os_i2c_bus_device,扩展本 MCU 硬件信息 */
struct my_i2c
{
struct os_i2c_bus_device i2c; /* 框架层公共部分 */
MY_I2C_HandleTypeDef hi2c; /* 新 MCU 的 I2C 句柄 */
os_semaphore_id i2c_sem; /* 传输完成信号量 */
os_list_node_t list; /* 挂到实例链表 */
};若你的 MCU 有多个 I2C 外设(如 I2C0、I2C1 等),可在 drv_i2c.h 里用宏声明每个实例的引脚/时钟/速率,如 GD 的 EVAL_I2C0_SCL_PIN、EVAL_I2C0_SDA_PIN。
关键点:内嵌的 i2c 是注册时的 device;os_i2c_bus_device_register 只会改 i2c 的 parent/ops/lock,硬件相关字段由你的结构体自行管理。
第 2 步:实现三个 ops 函数
逐个实现 i2c_transfer、i2c_slave_transfer、i2c_bus_control,内部调用新 MCU 的 I2C 发送/接收函数。以 i2c_transfer 的主机发送为例:
static os_size_t my_i2c_transfer(struct os_i2c_bus_device *bus,
struct os_i2c_msg msgs[], uint32_t num)
{
/* 从 bus 反查本 MCU 实例结构体 */
struct my_i2c *my = os_container_of(bus, struct my_i2c, i2c);
os_err_t result = OK;
uint32_t i;
for (i = 0; i < num; i++)
{
/* 根据 msgs[i].flags 判断读写方向,并处理 NO_START / ADDR_10BIT 等标志 */
if (msgs[i].flags & OS_I2C_RD)
{
result = MY_I2C_Master_Receive(&my->hi2c, msgs[i].addr,
msgs[i].buf, msgs[i].len, bus->timeout);
}
else
{
result = MY_I2C_Master_Transmit(&my->hi2c, msgs[i].addr,
msgs[i].buf, msgs[i].len, bus->timeout);
}
if (result != OK) break;
}
return (result == OK) ? num : 0;
}关键点:
- 用
os_container_of(bus, struct my_i2c, i2c)从框架层的bus反查到 BSP 实例(因为bus是内嵌在my_i2c里的)。 - 必须正确解析
OS_I2C_NO_START、OS_I2C_ADDR_10BIT、OS_I2C_RD等标志,否则"写地址 + Restart + 读数据"这类事务(见 4.3)会乱。可参考 ST 驱动里"合并OS_I2C_NO_START连续消息"的写法。 - 传输是否用中断/DMA,取决于新 MCU 是否支持及性能需求(参考 ST 的
HAL_I2C_Master_Transmit_DMA+ 信号量等待)。
第 3 步:定义并导出 ops 操作表
把三个函数聚合到 struct os_i2c_bus_device_ops,供框架引用:
static const struct os_i2c_bus_device_ops my_i2c_bus_ops = {
.i2c_transfer = my_i2c_transfer,
.i2c_slave_transfer = my_i2c_slave_transfer,
.i2c_bus_control = my_i2c_bus_control,
};第 4 步:写 probe 初始化函数并注册
取硬件信息、填 ops、创建信号量,最后 os_i2c_bus_device_register:
static int my_i2c_probe(const os_driver_info_t *drv, const os_device_info_t *dev)
{
struct my_i2c_info *info = (struct my_i2c_info *)dev->info;
struct my_i2c *my = os_calloc(1, sizeof(struct my_i2c));
OS_ASSERT(my);
my->i2c_sem = os_semaphore_create(OS_NULL, "sem_i2c", 0, OS_SEM_MAX_VALUE);
my->hi2c = info->instance;
/* 填 ops 表 */
my->i2c.ops = &my_i2c_bus_ops;
/* 注册到设备管理器:总线名来自 dev->name,user_data 传总线实例 */
os_i2c_bus_device_register(&my->i2c, dev->name, &my->i2c);
return 0;
}
OS_DRIVER_INFO my_i2c_driver = {
.name = "MY_I2C_HandleTypeDef",
.probe = my_i2c_probe,
};
OS_DRIVER_DEFINE(my_i2c_driver, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_HIGH);关键点:
- 用
OS_DRIVER_DEFINE+OS_DRIVER_INFO声明驱动信息,让系统在设备初始化阶段自动调用probe(这是 OneOS 的驱动自动挂载机制,区别于手动初始化)。 os_i2c_bus_device_register(&my->i2c, dev->name, &my->i2c)的第三个参数data常传总线实例,之后os_i2c_bus_device_find从dev->user_data能取回总线指针。
第 5 步:验证
移植完成后,用本节"示例 1(主机发送/接收)"或"示例 3(消息批量传输)"跑通即可验证:
bus = os_i2c_bus_device_find("i2c0"); /* 换成你注册的总线名 */
if (bus == OS_NULL) return;
ret = os_i2c_master_recv(bus, 0x48, OS_I2C_RD, rx, 4); /* 读传感器 */
if (ret == 4) os_kprintf("read ok\r\n");- 能读到数据 →
i2c_transfer的读方向正确。 - 写 EEPROM 后能读回 → "写地址 + Restart + 读数据"事务正确(验证
OS_I2C_NO_START处理)。 - 从机模式可被外部主机访问 →
i2c_slave_transfer正确(若用到)。
拓展:一颗 MCU 上有多个 I2C 外设时,如何注册?—— 区分"多个 I2C 外设"与"多个厂商驱动"
这是移植/注册时最易混淆的问题。一颗 MCU 上往往有多个 I2C 外设(如 I2C1/I2C2),它们各自独立,需要各注册一次,os_i2c_bus_device_find("i2c1") / find("i2c2") 分别返回对应的总线:
| 情形 | 说明 | 注册次数 |
|---|---|---|
| 同厂商不同 I2C 外设(I2C1/I2C2) | 共用同一套 ops 表(i2c_bus_ops),但每个外设各有一个总线实例结构体(struct stm32_i2c),各调一次 os_i2c_bus_device_register | 每个外设注册 1 次 |
不同厂商的 drv_i2c.c | ST、GD、NRF 等各有一份,实现同一契约 | 每个工程只编译其中一个(按所选 MCU) |
关键点:多个 I2C 外设共用一套 ops 表(i2c_bus_ops 是 static const),但每个外设有自己的实例结构体(里面有自己的 hi2c、i2c_sem、list)。probe 会对 dev->info 里的每个外设实例都调用一次,从而注册出 i2c1、i2c2 等多个总线设备。而项目里多个 drv_i2c.c 是多个厂商的平级实现,不是"多根 I2C 总线"——一个工程只编译所选厂商的那一份。
7.5 芯片未适配时该怎么办?(新建 BSP 还是补型号)
前面的移植流程假设"同厂商系列、寄存器结构兼容"。但更常见的问题是:你手里的芯片在 drivers/hal/ 里根本没有对应的 drv_i2c.c,甚至整个厂商目录都没有。这时要分两种情况处理。
7.5.1 判断标准:已有厂商 还是 全新厂商
| 情况 | 判断标准 | 工作量大不大 |
|---|---|---|
| 情况 A:已有厂商 | 芯片厂商在 drivers/hal/ 已有目录(如 st/),只是你这款具体型号没列出来 | 小,通常不改 I2C 驱动 |
| 情况 B:全新厂商 | 芯片厂商在 drivers/hal/ 完全没有目录 | 大,需整套新建 BSP + 移植 I2C 驱动 |
7.5.2 情况 A:已有厂商,只是具体型号没适配
同厂商同系列 MCU 的 I2C 寄存器结构高度一致,drv_i2c.c 里的 struct os_i2c_bus_device_ops 实现通常直接复用。所以此时你大概率不用改 I2C 驱动,只需:
第 1 步:确认 drv_i2c.c 是否覆盖你的 I2C 外设个数/引脚
- 看
drv_i2c.h(或.c内的设备表)里是否声明了你型号的 I2C 实例个数与引脚(如是否只有 I2C1,而你型号还有 I2C2)。 - 若引脚不同,只需改配置宏(如
EVAL_I2C0_SCL_PIN)。
第 2 步:在厂商 Kconfig 里加"芯片型号"选项
- 在对应系列(如
drivers/hal/st/soc/目录)的 Kconfig 里新增你的型号,让它能select正确的时钟树、外设、HAL_I2C_MODULE_ENABLED等配置。
第 3 步:用厂商工具生成外设配置
- 用 STM32CubeMX(或厂商对应工具)生成
peripherals.c(I2C 引脚复用、时钟树、外设初始化)。 - 项目设备文件通过
#include "peripherals.c"把它纳入编译,其内的初始化会注册 HAL 设备,stm32_i2c_probe才能拿到I2C_HandleTypeDef。
第 4 步:写板级文件并注册
- 写
board.c/board.h做板级时钟初始化,确保OS_DRIVER_DEFINE的 probe 在应用使用前被调用。
第 5 步:验证(同 7.4 第 5 步)
要点:情况 A 的核心是"型号配置 + I2C 引脚复用",I2C 驱动代码基本不用动。真正的工作量在时钟树和板级外设,而非 I2C 驱动本身。
7.5.3 情况 B:全新厂商,需要从零新建 BSP
这才是真正需要做"移植 I2C 驱动"的情况,工作量较大。步骤与本 7.4 的流程 + 7.1 的三层选择机制结合:
第 1 步:新建厂商目录
在 drivers/hal/ 下新建目录,如 drivers/hal/my_chip/,至少包含:
drivers/hal/my_chip/
├── drv_i2c.c / drv_i2c.h # 实现 struct os_i2c_bus_device_ops 三个函数(见 7.1/7.3/7.4)
├── Kconfig # 该厂商的 SOC/外设配置
├── weave.yaml # 条件编译声明(depend_macro)
├── board.c / board.h # 板级时钟、启动
└── 启动文件 / 链接脚本 # 新 MCU 的启动汇编、ld 脚本第 2 步:按 7.4 的 5 步流程实现 I2C 驱动
- 定义总线实例结构体(7.4 第 1 步)
- 实现三个 ops 函数(7.4 第 2 步)
- 定义 ops 表(7.4 第 3 步)
- 写 probe +
os_i2c_bus_device_register(7.4 第 4 步)
第 3 步:接入构建系统的三层选择机制(见 7.1 第 5 点)
让系统"认识"新厂商:
# templates/configs/Kconfig 的 choice 里新增
config MANUFACTOR_MYCHIP
bool "MYCHIP"# templates/configs/SConscript 的 inq_table 里新增
inq_table['MANUFACTOR_MYCHIP'] = 'my_chip'# drivers/hal/my_chip/weave.yaml
group_name: hal/drivers
depend_macro:
- MANUFACTOR_MYCHIP
source_file:
- drv_i2c.c ? {is_define(['OS_USING_I2C', 'HAL_I2C_MODULE_ENABLED'])}这样选中 MANUFACTOR_MYCHIP → SRC_HAL = my_chip → drivers/hal/Kconfig 加载新目录 → weave.yaml 命中 → 编译你的 drv_i2c.c。
第 4 步:验证(同 7.4 第 5 步)
要点:情况 B 的工作量本质 = 新建一套 BSP 目录 + 实现 I2C 驱动 + 接入厂商宏与
SRC_HAL。框架层和应用层依然不用改。
7.5.4 两种情况的对比速查
| 维度 | 情况 A(已有厂商补型号) | 情况 B(全新厂商新建 BSP) |
|---|---|---|
| 是否新建目录 | 否,复用厂商目录 | 是,新建 drivers/hal/my_chip/ |
是否改 drv_i2c.c | 通常不改(或改引脚/实例配置) | 从零实现三个 ops |
是否改厂商宏/SRC_HAL | 否 | 是(加 MANUFACTOR_XXX + 映射) |
| 是否改 weave.yaml | 否 | 是(新增 drv_i2c.c 的 is_define) |
| 工作量 | 小(型号配置 + I2C 引脚复用) | 大(全套 BSP + 驱动移植) |
7.6 移植检查清单(快速自查)
| 检查项 | 是否满足 |
|---|---|
struct os_i2c_bus_device_ops 三个函数全部实现并绑定到 ops 表 | ☐ |
正确处理 OS_I2C_RD/OS_I2C_NO_START/OS_I2C_ADDR_10BIT 等标志 | ☐ |
os_container_of(bus, struct xxx_i2c, i2c) 反查实例正确 | ☐ |
| 多消息合并/逐条传输逻辑正确(尤其是"写地址 + Restart + 读数据"事务) | ☐ |
中断/DMA 模式下信号量同步正确(回调 post,传输 wait) | ☐ |
os_i2c_bus_device_register 在应用使用前被调用(OS_DRIVER_DEFINE 挂载) | ☐ |
| 一颗 MCU 多个 I2C 外设时,每个实例各注册一次 | ☐ |
从机模式若用到,i2c_slave_transfer 已实现并配置地址匹配 | ☐ |
7.7 移植的价值与注意事项
价值:一旦 BSP 层移植完成,应用层代码一行不改即可在多个 MCU 间复用。这是 OneOS 设备模型(应用/框架/BSP 三层解耦)带来的最大好处。
注意事项:
- 传输标志语义不可变:
OS_I2C_RD/OS_I2C_NO_START/OS_I2C_ADDR_10BIT等是框架层与应用层约定的位掩码,BSP 的i2c_transfer必须按这些位正确解析,否则高层 API(客户端读写、批量传输)会乱。 - 超时/重试字段含义:
bus->timeout在传输失败或等待时使用,bus->retries预留供重试;BSP 应合理使用,避免死等。 - 阻塞 vs 中断:
os_i2c_transfer本身会os_mutex_lock阻塞等待总线,BSP 的i2c_transfer内部不要再用会长时间占 CPU 的忙等(除非没有中断/DMA 资源),否则会拉高系统延迟。 - 总线忙(bus busy)处理:从机异常可能导致 SDA 被拉低(总线死锁),建议参考 ST 的
stm32_check_and_solve_bus_busy实现总线解锁(SCL 翻转 + 复位外设),否则首次通信就可能失败。
📝 本节小结
核心要点回顾
- 双层抽象设计:
os_i2c_bus_device(总线控制器)与os_i2c_client(总线上的具体外设)分离,一套总线驱动服务多个外设。 - 消息驱动传输:
os_i2c_msg描述一次原子传输(地址 + 标志 + 长度 + 缓冲区),多条消息组成批量传输(如"先写地址再读数据")。 - 标志位体系:
OS_I2C_WR/RD(方向)、OS_I2C_ADDR_10BIT(地址模式)、OS_I2C_NO_START/IGNORE_NACK/NO_READ_ACK(传输控制)位于不同 bit,可用|组合、&判断。 - 框架层核心:
os_i2c_transfer加总线互斥锁 → 调bus->ops->i2c_transfer→ 解锁,所有高层接口最终都落到这一个函数指针。 - 客户端接口:
os_i2c_client_write/read自动构造"写地址消息 + 读写数据消息",其中写数据第二条带OS_I2C_NO_START,读数据第二条不带(需 Restart 切换方向)。 - 主机接口:
os_i2c_master_send/recv把高层参数封装成单条消息;recv会强制置上OS_I2C_RD位。 - 总线注册/查找:
os_i2c_bus_device_register设置设备类型、挂 ops、创建互斥锁并注册;os_i2c_bus_device_find校验存在且类型为OS_DEVICE_TYPE_I2CBUS。 - 更换 MCU 移植(见第七章):移植核心 = 实现
struct os_i2c_bus_device_ops三个函数 + 定义总线实例结构体 + 写 probe 注册;框架层与应用层不用改;换 MCU 本质是换SRC_HAL对应的那份drv_i2c.c。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
在中断上下文调用 os_i2c_transfer / 客户端接口 | 会 os_mutex_lock(bus->lock, OS_WAIT_FOREVER) 阻塞,中断里无法睡眠 → 应在任务上下文调用 |
客户端写数据第二条消息漏加 OS_I2C_NO_START | 会导致重复发送地址 / 启动两次独立事务 → 写数据第二条应带 OS_I2C_NO_START |
读数据第二条消息误加 OS_I2C_NO_START | 读需要切换方向,必须 Restart 重新发读地址 → 读消息不要带 OS_I2C_NO_START |
os_i2c_master_recv 不传 OS_I2C_RD | 函数内部会 flags | OS_I2C_RD 强制置读位,安全;但语义上仍应显式传 OS_I2C_RD |
cmd_len 与 len 同时为 0 调用客户端接口 | OS_ASSERT(i == 1 || i == 2) 会触发断言 → 至少要发地址或数据 |
误判 os_i2c_client_read_byte 的返回值 | 失败时返回 0(初始值),与"真读到 0"无法区分 → 关键路径用 os_i2c_client_read 检查 OS_SUCCESS |
未注册总线就 os_i2c_bus_device_find | 返回 OS_NULL,直接使用会空指针崩溃 → 使用前必须判空 |
bus->ops->i2c_transfer 未实现 | os_i2c_transfer 返回 0 → 确认 BSP 驱动已填充 ops 表 |
把非 I2C 设备名误传给 os_i2c_bus_device_find | 会因 dev->type != OS_DEVICE_TYPE_I2CBUS 返回 OS_NULL → 校验总线名与类型 |
| 移植时只写一个 ops 函数,漏掉其余 | struct os_i2c_bus_device_ops 三个指针必须全部绑定,否则对应的 os_i2c_transfer 会判空返回 0 → 三个函数都实现 |
移植时未正确处理 OS_I2C_NO_START | 不合并/不按 Restart 处理会导致"写地址 + 读数据"事务出错 → 参考 ST 驱动合并逻辑 |
移植时忘记用 os_container_of 从 bus 反查实例 | 无法拿到本 MCU 的 hi2c/引脚 → 用 os_container_of(bus, struct xxx_i2c, i2c) |
| 移植时多个 I2C 外设共用一个实例 | 每个 I2C 外设应有独立实例结构体并各注册一次 → 每个外设一个 struct xxx_i2c |
移植时漏接入 weave.yaml 的 is_define | 即使选对了厂商,drv_i2c.c 也不会被编译 → 检查 OS_USING_I2C + HAL_I2C_MODULE_ENABLED |
下一步
- 5.4 Serial设备:了解 OneOS 串口设备的读写、控制命令与串口使用示例。