5.2 GPIO设备
5.2 GPIO设备
📚 本节导读
学习时长: 约 60 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: C 语言基础、MCU GPIO 基础知识、5.1 设备驱动框架
本节讲解 OneOS 中 GPIO 设备(PIN 设备) 的完整用法。GPIO(General Purpose Input/Output)是嵌入式系统中最常用的数字 IO 接口,几乎所有 MCU 应用都要用到——点灯、读按键、控制继电器、检测电平、外部中断唤醒等。
OneOS 把不同 MCU 平台的 GPIO 统一抽象为 PIN 设备,让你用一套 API 操作任何平台的引脚。本节会带你:
- 看懂 PIN 设备如何基于 5.1 的设备框架二次封装(
os_device_pin_t内嵌os_device); - 掌握
pin.h中定义的所有引脚模式、电平、中断触发模式宏; - 逐个拆解
os_device_pin_register、os_pin_mode、os_pin_write、os_pin_read、os_pin_attach_irq、os_pin_detach_irq、os_pin_irq_enable以及 raw 系列接口的完整源码; - 通过多个可运行示例掌握 LED 闪烁、按键检测、外部中断的实际开发。
说明:本节所有源码均取自项目
drivers/pin/pin.c与drivers/pin/pin.h,与项目源码逐字一致(含原始英文注释)。
🎯 学习目标
- 理解 OneOS 中 GPIO 抽象为 PIN 设备的设计理念与"基地址 + 引脚编号"的编码规则
- 掌握
os_device_pin_t结构体的每个字段含义及其来源 - 掌握引脚模式(输出/输入/上拉/下拉/开漏)的配置方法
- 掌握引脚电平的读写操作
- 掌握外部中断的绑定、使能与解绑
- 理解原始操作(raw)与设备框架操作的差异及各自的适用场景
一、概述
GPIO(General Purpose Input/Output)是 MCU 最基本的数字 IO 接口。OneOS 将 GPIO 抽象为 PIN 设备,提供统一的引脚模式设置、电平读写、中断绑定功能。PIN 设备驱动位于 drivers/pin/ 目录下,核心头文件为 pin.h。
1.1 PIN 设备的分层架构
PIN 设备是基于 5.1 设备驱动框架的二次封装,它在通用 os_device_t 的基础上扩展出引脚特有的字段:
┌──────────────────────────────────────────────────────────┐
│ 应用层(Application) │
│ os_pin_mode / os_pin_write / os_pin_read / │
│ os_pin_attach_irq / os_pin_detach_irq / irq_enable │
└──────────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ PIN 设备框架层(drivers/pin/pin.c) │
│ os_device_pin_t(内嵌 os_device)+ gs_pin_table[] │
│ raw_os_pin_* 系列 / os_pin_* 系列 / os_device_pin_* │
│ 设备框架层操作封装:_pin_read/_pin_write/_pin_control │
└──────────────────────────────┬───────────────────────────┘
│ 调用 pin->ops->xxx
▼
┌──────────────────────────────────────────────────────────┐
│ BSP 引脚驱动(如 drv_gpio.c) │
│ 实现 struct os_pin_ops 的六个函数指针 │
│ 操作具体 MCU 的 GPIO 寄存器 │
└──────────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(MCU GPIO 外设) │
└──────────────────────────────────────────────────────────┘关键理解:PIN 设备框架层不直接操作硬件,它只负责"引脚编号换算 + 转发到 ops"。真正操作寄存器的是 BSP 层实现的 struct os_pin_ops(如 STM32 的 _stm32_pin_ops)。这让上层应用与具体 MCU 平台解耦。
1.2 PIN 设备的全局资源
PIN 设备在 drivers/pin/pin.c 中维护了几个全局资源,是整个框架运作的基础:
来源:drivers/pin/pin.c
#ifndef OS_PIN_MAX_CHIP
#define OS_PIN_MAX_CHIP (1)
#endif
#define DBG_TAG "pin"
/*fast division method,euquals to (pin / 1000)*/
#define PIN_BASE(pin) (int)((4294968ULL * (unsigned int)pin) >> 32)
static struct os_device_pin gs_pin_table[OS_PIN_MAX_CHIP];| 全局资源 | 类型 | 作用 |
|---|---|---|
gs_pin_table | struct os_device_pin[OS_PIN_MAX_CHIP] | 全局 PIN 设备表,每片芯片(chip)一个槽位,默认只支持 1 片(OS_PIN_MAX_CHIP = 1)。os_device_pin_register 往这里填槽 |
PIN_BASE(pin) | 宏 | 快速除法宏,等价于 pin / 1000,用于从"完整引脚编号"算出"芯片索引" |
OS_PIN_MAX_CHIP | 宏 | 最大支持芯片数,默认 1。可通过配置覆盖 |
关键理解——引脚编号体系:OneOS 用三位千位划分引脚编号:高三位表示"芯片/端口基址"(base),低三位表示"芯片内偏移"(pin - base)。例如完整编号
1000 + 13 = 1013表示"基址 1000 的第 13 号引脚"。PIN_BASE(1013) = 1(芯片索引),而1013 - 1000 = 13才是真正传给底层驱动的引脚号。
为什么几乎所有 PIN API 都要做"芯片索引越界检查"?
gs_pin_table[OS_PIN_MAX_CHIP] 是一个大小固定为 OS_PIN_MAX_CHIP(默认 1)的数组,合法索引范围是 0 ~ OS_PIN_MAX_CHIP-1。而芯片索引是用 pin / 1000 或 PIN_BASE(pin) 从引脚号算出来的——如果应用传的引脚号千位部分过大(如 5000),算出的索引 5 就越过了数组边界。
为什么不检查会出大问题:
- 若直接
&gs_pin_table[5],就是访问数组越界内存,读取到的是相邻内存的脏数据,甚至触发硬件错误(HardFault)或破坏其他全局变量。 - 越界检查
pin_index >= OS_PIN_MAX_CHIP就是为了在用索引访问数组之前拦截非法引脚号,保证&gs_pin_table[pin_index]永远落在合法槽位内。
为什么用 >= 而不是 >:因为合法索引最大值是 OS_PIN_MAX_CHIP - 1。当 pin_index 恰好等于 OS_PIN_MAX_CHIP 时,gs_pin_table[OS_PIN_MAX_CHIP] 就已经越界了(数组下标从 0 开始,OS_PIN_MAX_CHIP 是第 OS_PIN_MAX_CHIP + 1 个元素)。所以必须用 >= 把 pin_index == OS_PIN_MAX_CHIP 也拦截掉。
数值演算(默认 OS_PIN_MAX_CHIP = 1,合法索引只有 0):
| 引脚号 | pin / 1000 | 是否越界(>= 1) | 行为 |
|---|---|---|---|
13 | 0 | 否(0 >= 1 假) | 合法,继续执行 |
1013 | 1 | 是(1 >= 1 真) | 打印 invalide pin,返回错误 |
5000 | 5 | 是(5 >= 1 真) | 打印 invalide pin,返回错误 |
这个检查在
os_device_pin_register、raw_os_pin_mode/write/read、os_pin_attach_irq等 7 处 API 中都会出现,是本框架统一的越界保护机制,后续 API 详解中不再重复展开,只做简要点名。
1.3 PIN 设备使用场景
1.3.1 一句话核心
PIN 设备适用于需要以统一接口配置/读写 MCU 数字引脚,或把引脚电平变化转化为中断回调通知的场景。它让你"传一个引脚号 + 模式/电平/中断类型,就能控制任意平台的 GPIO"。
1.3.2 与设备框架的访问方式对比
在 OneOS 中访问 GPIO 有两条路:PIN 设备专用 API(os_pin_*/raw_os_pin_*)和 通用设备框架 API(os_device_find("pin_0") + os_device_read/write/control)。对比:
| 维度 | PIN 专用接口(os_pin_*) | 设备框架接口(os_device_*) |
|---|---|---|
| 参数形式 | 直接传引脚号 + 模式/电平 | 需构造 os_device_pin_status/os_device_pin_mode 结构体 |
| 易用性 | 简单直观,无需理解结构体 | 复杂,但接口统一 |
| 中断支持 | os_pin_attach_irq 等专用接口 | 框架本身不提供,需走专用接口 |
| 适用场景 | 日常 GPIO 操作(点灯、读键、中断) | 通过 VFS/devfs 以文件方式访问设备的场景 |
| 底层实现 | 框架层封装后直接调 ops->xxx | 通过 _pin_read/_pin_write/_pin_control 封装 |
实际开发中 99% 场景都用
os_pin_*专用接口,更直观;设备框架接口主要用于统一访问和文件系统集成。
1.3.3 典型应用场景
场景一:LED 闪烁 / 电平输出控制(推挽输出)
主控每隔一段时间翻转某个引脚的输出电平,控制 LED 亮灭、继电器吸合、蜂鸣器发声等。这类场景用 os_pin_mode(pin, PIN_MODE_OUTPUT) 配成推挽输出,再用 os_pin_write(pin, PIN_HIGH/LOW) 写电平。
时刻 T1:os_pin_write(LED, PIN_HIGH) → LED 亮
时刻 T2:os_task_msleep(500)
时刻 T3:os_pin_write(LED, PIN_LOW) → LED 灭
时刻 T4:os_task_msleep(500) → 回到 T1,循环为什么用 PIN 设备:LED 输出是再简单不过的操作,用 os_pin_* 两三行搞定;换成其他 MCU 平台只需改引脚号,驱动逻辑零改动。
场景二:按键 / 传感器电平输入检测(上拉/下拉输入)
读取外部按键、限位开关、光电传感器等的电平状态。这类场景把引脚配成输入模式,再用 os_pin_read(pin) 轮询电平,结合消抖逻辑判断按下/释放。
key = os_pin_read(KEY_PIN) // 上拉输入,按下时读到 PIN_LOW
if (上一次 == PIN_HIGH && key == PIN_LOW) → 按键按下
if (上一次 == PIN_LOW && key == PIN_HIGH) → 按键释放为什么用 PIN 设备:PIN_MODE_INPUT_PULLUP/PULLDOWN 直接把内部上下拉配置好,无需手动操作寄存器设置上下拉。
场景三:外部中断唤醒 / 事件触发(边沿/电平触发)
引脚电平跳变时需要及时响应(比如按键触发、外部传感器脉冲、唤醒低功耗模式)。这类场景用 os_pin_attach_irq 绑定回调 + os_pin_irq_enable 使能中断,电平变化时硬件自动调用回调,无需轮询。
os_pin_attach_irq(IRQ_PIN, PIN_IRQ_MODE_FALLING, callback, &count); // 绑定下降沿回调
os_pin_irq_enable(IRQ_PIN, PIN_IRQ_ENABLE); // 使能中断
// 此后按键按下(下降沿)→ 硬件进入 EXTI 中断 → 框架调用 callback为什么用 PIN 设备:中断绑定涉及 NVIC/EXTI 等硬件配置,PIN 设备封装成统一的 pin_attach_irq/pin_irq_enable,跨平台一致。
1.3.4 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 只需要排他地独占访问某个引脚 | 结合互斥锁保护 os_pin_* 调用 | PIN 接口本身不加锁,多任务并发写同一引脚需自己加锁 |
| 需要在极高频率中断里逐位翻转引脚 | 直接在 BSP 驱动里操作寄存器 | os_pin_* 每调用一次都要做一次索引换算和函数指针跳转,有开销 |
| 只是传递标志位/计数(不涉及引脚) | 信号量、事件等内核 IPC | 与 GPIO 无关 |
| 需要以文件方式统一管理多种外设 | 设备框架(os_device_find) | 当配合 VFS/devfs 时才需要走框架接口 |
1.3.5 结合源码的使用注意点
- 引脚编号必须带基址:
os_pin_*内部按pin / 1000换算芯片索引,若直接传13(而非1013),索引为 0 默认也能工作(OS_PIN_MAX_CHIP=1时);但多芯片场景必须用完整编号。 - 芯片索引越界返回错误:
os_pin_attach_irq/raw_os_pin_read等在pin_index >= OS_PIN_MAX_CHIP时打印invalide pin并返回OS_INVAL(见源码)。 - 中断必须在
attach之后enable:先os_pin_attach_irq绑定回调,再os_pin_irq_enable使能;顺序颠倒会导致中断触发时找不到回调。 - 电平触发模式依赖具体平台:如 STM32 驱动只支持边沿触发(
PIN_IRQ_MODE_HIGH_LEVEL/LOW_LEVEL会返回OS_FAILURE),使用前需查阅目标平台驱动。
二、核心数据结构
2.1 PIN 设备结构体 os_device_pin_t —— 字段详解
PIN 设备继承自 os_device_t,将通用设备结构体作为第一个成员,实现面向对象风格的继承。
来源:drivers/pin/pin.h
typedef struct os_device_pin
{
struct os_device parent;
const struct os_pin_ops *ops;
int base;
} os_device_pin_t;2.1.1 字段含义表
| 成员 | 类型 | 说明 |
|---|---|---|
parent | struct os_device | 继承的通用设备控制块,作为结构体第一个成员。PIN 设备就是"一个带了引脚扩展的普通设备",parent 上挂的是设备框架操作表 pin_ops(见 2.3) |
ops | const struct os_pin_ops * | PIN 操作函数表指针,指向 BSP 层实现的六个引脚操作函数(见 2.2) |
base | int | 引脚基地址,即该芯片引脚编号的千位基址(0、1000、2000...),注册时写入 |
2.1.2 关键点
parent与ops是两套操作表,不要混淆:parent.ops(继承自os_device)指向设备框架操作表pin_ops(实现read/write/control),供os_device_read_block等通用框架 API 调用。ops(本结构体新加的)指向引脚专用操作表struct os_pin_ops(实现pin_mode/pin_write/...),供os_pin_mode等专用 API 调用。
base是"本芯片的千位基址"。真正的硬件引脚号 = 完整编号 −base(源码里大量出现pin - ppin->base)。
2.1.3 全局 PIN 设备表 gs_pin_table 的"来源"说明
os_device_pin_register 返回的并不是动态分配的设备,而是写入全局数组 gs_pin_table 的某个槽位:
| 项目 | 说明 |
|---|---|
| 属于谁 | drivers/pin/pin.c 的 static 全局变量 |
| 数量 | 整个系统 OS_PIN_MAX_CHIP 个(默认 1) |
| 定义源码 | static struct os_device_pin gs_pin_table[OS_PIN_MAX_CHIP];(见 1.2 节) |
| 如何定位 | 通过 pin_base / 1000 得到 pin_index,即 &gs_pin_table[pin_index] |
关键理解:应用层拿到的 PIN 设备指针,实际上就是 gs_pin_table[pin_index] 的地址(强制转换成 os_device_pin_t *)。因此同一时刻系统里只有 OS_PIN_MAX_CHIP 个 PIN 设备实例。
2.2 PIN 操作函数表 struct os_pin_ops
这是 PIN 设备与 BSP 底层驱动之间的"契约"。每个 BSP 必须实现这六个函数指针。
来源:drivers/pin/pin.h
struct os_pin_ops
{
void (*pin_mode)(struct os_device *device, os_base_t pin, os_base_t mode);
void (*pin_write)(struct os_device *device, os_base_t pin, os_base_t value);
int (*pin_read)(struct os_device *device, os_base_t pin);
/* TODO: add GPIO interrupt */
os_err_t (*pin_attach_irq)(struct os_device *device,
int32_t pin,
uint32_t mode,
void (*hdr)(void *args),
void *args);
os_err_t (*pin_detach_irq)(struct os_device *device, int32_t pin);
os_err_t (*pin_irq_enable)(struct os_device *device, os_base_t pin, uint32_t enabled);
};| 函数指针 | 函数签名 | 说明 |
|---|---|---|
pin_mode | void (*)(struct os_device *, os_base_t, os_base_t) | 设置引脚工作模式(输出/输入/上拉/下拉等),无返回值 |
pin_write | void (*)(struct os_device *, os_base_t, os_base_t) | 设置引脚输出电平(PIN_LOW/PIN_HIGH),无返回值 |
pin_read | int (*)(struct os_device *, os_base_t) | 读取引脚输入电平,返回 PIN_LOW/PIN_HIGH |
pin_attach_irq | os_err_t (*)(struct os_device *, int32_t, uint32_t, void(*)(void*), void*) | 绑定引脚中断回调函数(含触发模式) |
pin_detach_irq | os_err_t (*)(struct os_device *, int32_t) | 解绑引脚中断 |
pin_irq_enable | os_err_t (*)(struct os_device *, os_base_t, uint32_t) | 使能/禁用引脚中断 |
关键点:
- 前三个函数(
pin_mode/pin_write/pin_read)返回void/int,是最基本的引脚操作。 - 后三个是中断相关,带
/* TODO: add GPIO interrupt */注释(项目保留的英文注释,说明这是后续补上的能力)。 pin_attach_irq的回调类型是void (*hdr)(void *args),与设备框架os_device_cb_info的回调不同——这是引脚自己的回调形式。
2.2.1 BSP 层如何实现 pin_read(读电平函数)?
前面反复提到"调用 BSP 的读电平函数",那么 BSP 到底是怎么实现并绑定 ops->pin_read 的?下面以 STM32 为例(drivers/hal/st/drivers/drv_gpio.c)完整展示。
(1)引脚索引表与编号换算宏
BSP 驱动维护一个"引脚号 → 端口/引脚号"的索引表 indexs[],以及换算宏 __PORT_INDEX(pin)(取端口)和 PIN_OFFSET(pin)(取该端口内引脚位)。这是底层把"芯片内偏移(0~15)"进一步解析成"哪个 GPIO 端口 + 第几脚"的依据。
(2)stm32_pin_read 完整源码(drv_gpio.c,与项目一致):
static int stm32_pin_read(struct os_device *dev, os_base_t pin)
{
int value;
const struct pin_index *index;
value = PIN_LOW;
index = get_pin_index(pin);
if (index == OS_NULL)
{
return value;
}
value = HAL_GPIO_ReadPin(index->gpio_port, PIN_OFFSET(pin));
return value;
}分步详解:
第 1 步:默认值为低电平(无条件执行)
value = PIN_LOW;详细解释:先把 value 默认置为 PIN_LOW。若后续查表失败,直接返回这个默认值,避免返回未初始化数据。
第 2 步:查引脚索引表(条件 index == OS_NULL)
index = get_pin_index(pin);
if (index == OS_NULL)
{
return value;
}详细解释:get_pin_index(pin) 根据引脚号在 indexs[] 里查对应的端口/引脚信息。若查不到(index == OS_NULL,引脚号越界),返回默认值 PIN_LOW。
第 3 步:调用 HAL 库读取硬件电平(无条件执行)
value = HAL_GPIO_ReadPin(index->gpio_port, PIN_OFFSET(pin));
return value;详细解释:HAL_GPIO_ReadPin 读取指定端口 index->gpio_port 的 PIN_OFFSET(pin) 号引脚的实际电平,结果存入 value 并返回。
(3)如何把 stm32_pin_read 绑定到 ops->pin_read?
在 BSP 驱动里定义一个操作表 _stm32_pin_ops(drv_gpio.c,与项目一致):
const static struct os_pin_ops _stm32_pin_ops = {
.pin_mode = stm32_pin_mode,
.pin_write = stm32_pin_write,
.pin_read = stm32_pin_read,
.pin_attach_irq = stm32_pin_attach_irq,
.pin_detach_irq = stm32_pin_dettach_irq,
.pin_irq_enable = stm32_pin_irq_enable,
};然后 BSP 在初始化(os_hw_pin_init 末尾)把它注册进 PIN 框架:
return os_device_pin_register(0, &_stm32_pin_ops, OS_NULL);注册时,os_device_pin_register 内部执行 pin->ops = ops,于是 pin->ops 就指向了 _stm32_pin_ops。
(4)完整调用链:从框架到硬件
应用 os_pin_read(1013) / os_device_read_block(...)
│
▼(框架层)
raw_os_pin_read / _pin_read
│ 换算:status->pin - pin->base = 1013 - 1000 = 13
│ 调用:pin->ops->pin_read(dev, 13)
▼
pin->ops 就是 &_stm32_pin_ops(注册时写入)
│ 即 stm32_pin_read(dev, 13)
▼(BSP 层)
get_pin_index(13) → 查 indexs[] → 得到 index(端口 + 引脚位)
▼
HAL_GPIO_ReadPin(index->gpio_port, PIN_OFFSET(13)) → 读硬件寄存器 → 返回 PIN_HIGH/PIN_LOW关键理解:
- 框架层只做"编号换算 + 函数指针跳转",不碰硬件。
- BSP 层(
stm32_pin_read)才真正操作寄存器(通过 HAL 库)。 - 两者通过
ops表连接:注册时pin->ops = &_stm32_pin_ops,之后ops->pin_read就是stm32_pin_read。 - 换一块 MCU,只需换一套
_xxx_pin_ops实现,框架和应用层零改动——这就是 PIN 设备跨平台复用的核心。
2.3 设备框架操作表 pin_ops
pin.c 里还定义了一个设备框架层的操作表 pin_ops,把"框架的 read/write/control"转换成"引脚专用操作"。
来源:drivers/pin/pin.c
const static struct os_device_ops pin_ops = {
.read = _pin_read,
.write = _pin_write,
.control = _pin_control,
};它实现了 os_device_ops 的 read/write/control 三个接口(没有实现 init/deinit),对应三个内部函数 _pin_read/_pin_write/_pin_control。这三个函数是内部辅助函数,详解见下方"2.4 核心辅助函数 _pin_read / _pin_write / _pin_control"。
2.4 核心辅助函数 _pin_read / _pin_write / _pin_control
这三个 static 函数是设备框架接口到引脚专用操作的桥接:当应用通过设备框架 API(os_device_read_block、os_device_write_block、os_device_control)访问 PIN 设备时,框架最终会调用到这三个函数,再由它们转发到 ops->pin_read / ops->pin_write / ops->pin_mode。
一句话定位:
_pin_read:框架"读" → 引脚读。应用调os_device_read_block(pin_dev, ...)时触发。_pin_write:框架"写" → 引脚写。应用调os_device_write_block(pin_dev, ...)时触发。_pin_control:框架"控制" → 引脚模式。应用调os_device_control(pin_dev, ...)时触发。
完整源码(drivers/pin/pin.c,与项目一致):
static os_ssize_t _pin_read(os_device_t *dev, os_off_t pos, void *buffer, os_size_t size)
{
struct os_device_pin_status *status;
struct os_device_pin *pin = (struct os_device_pin *)dev;
/* check parameters */
OS_ASSERT(pin != OS_NULL);
status = (struct os_device_pin_status *)buffer;
if (status == OS_NULL || size != sizeof(*status))
return 0;
status->status = pin->ops->pin_read(dev, status->pin - pin->base);
return size;
}
static os_ssize_t _pin_write(os_device_t *dev, os_off_t pos, const void *buffer, os_size_t size)
{
struct os_device_pin_status *status;
struct os_device_pin *pin = (struct os_device_pin *)dev;
/* check parameters */
OS_ASSERT(pin != OS_NULL);
status = (struct os_device_pin_status *)buffer;
if (status == OS_NULL || size != sizeof(*status))
return 0;
pin->ops->pin_write(dev, (os_base_t)status->pin - pin->base, (os_base_t)status->status);
return size;
}
static os_err_t _pin_control(os_device_t *dev, int cmd, void *args)
{
struct os_device_pin_mode *mode;
struct os_device_pin *pin = (struct os_device_pin *)dev;
/* check parameters */
OS_ASSERT(pin != OS_NULL);
mode = (struct os_device_pin_mode *)args;
if (mode == OS_NULL)
return OS_FAILURE;
pin->ops->pin_mode(dev, (os_base_t)mode->pin - pin->base, (os_base_t)mode->mode);
return 0;
}说明:这三个函数是
pin_ops(见 2.3 节)的实现者,属于内部实现细节,日常应用通常不会直接调用,仅在通过设备框架 API 访问 PIN 设备时由框架自动触发。下面的详解帮助你理解"框架调用到底做了什么"。
2.4.1 _pin_read —— 框架"读"桥接
函数签名:
static os_ssize_t _pin_read(os_device_t *dev, os_off_t pos, void *buffer, os_size_t size)| 参数 | 说明 |
|---|---|
dev | 设备控制块指针(由框架传入,实际指向 os_device_pin_t) |
pos | 读取位置偏移(本函数不使用,PIN 设备无"偏移"概念) |
buffer | 接收数据的缓冲区,必须指向一个 os_device_pin_status 结构体 |
size | 缓冲区的字节大小 |
| 返回值 | 说明 |
|---|---|
size | 成功,返回实际读取的字节数(即 sizeof(os_device_pin_status)) |
0 | 失败(buffer 为空,或 size 不是 sizeof(*status)) |
分步详解:
第 1 步:把 dev 强转为 PIN 设备结构体指针(无条件执行)
struct os_device_pin_status *status;
struct os_device_pin *pin = (struct os_device_pin *)dev;
/* check parameters */
OS_ASSERT(pin != OS_NULL);详细解释:
(1)dev 是从哪来的?——它是框架回调传入的设备指针
_pin_read 是 pin_ops 操作表(见 2.3 节)里 .read 的实现,由设备框架调用。框架在调用 dev->ops->read(dev, pos, buffer, size) 时,会把"当前设备控制块的指针"作为第一个实参 dev 传进来。这个 dev 的实际指向,就是注册时 os_device_register(&pin->parent, name) 里传的 &pin->parent(见 4.1 节)。
换句话说:应用调用
os_device_read_block(pin_dev, ...),框架拿到pin_dev(它其实就是&gs_pin_table[i].parent),最终把pin_dev传给_pin_read的dev形参。
(2)为什么 dev 能强转成 pin?—— 首成员重合,首地址相同
os_device_pin_t 的第一个成员就是 struct os_device parent(见 2.1 节),所以它的内存布局是:
偏移 0 ┌─────────────────────────────────┬───────────────┬────────┐
│ struct os_device parent │ ops(指针) │ base │
│ (这一整块就等于一个 os_device) │ │ │
└─────────────────────────────────┴───────────────┴────────┘
▲
dev 指针(类型 os_device_t *,只"看"前段)
▲
pin 指针(类型 os_device_pin_t *,"看"整段)
(两者数值完全相同,都指向 parent 起始地址)因为 parent 从偏移 0 开始、占用了整个 os_device 那么大,所以 &parent 的地址就是 pin 的地址。于是 dev(指向 parent 开头)和 pin(指向结构体开头)数值相等,强转只是让编译器换个"视角"看同一块内存——把 dev 当作 os_device_pin_t * 来解读。
(3)dev 与 pin 的 sizeof 不同,但强转为什么安全?
这里有个容易困惑的点:os_device_pin_t 比 os_device_t 大(多了 ops 和 base 两个成员),两者 sizeof 不相等。强转并不会让大小变相等,而是用更大的"可见范围"去解读同一块内存。
0x20001000 0x20001000+S 0x20001000+S+8
▼ ▼ ▼
┌──────────────────────────────┬───────────┬────────────┐
│ struct os_device parent │ ops │ base │
│ (前 S 字节) │ (4字节) │ (4字节) │
└──────────────────────────────┴───────────┴────────────┘
▲
dev 视角(os_device_t *):可见范围 = S 字节(sizeof = S)
▲
pin 视角(os_device_pin_t *):可见范围 = S+8 字节(sizeof = S+8)为什么安全:因为这块内存本来就是按 os_device_pin_t(大类型)分配的——gs_pin_table 的元素类型就是 os_device_pin_t,每个槽位都预留了 S+8 字节,ops、base 那块内存是真实存在的。所以用 pin 的大视角去读它,是读取真实存在的内存,不会越界。
什么情况才会不安全:如果某块内存真的只按 os_device_t(小类型)分配(只有 S 字节),却用 os_device_pin_t * 强转去访问 ops/base,就会读到未分配内存(越界)。但 PIN 框架不会犯这个错——它注册的就是 os_device_pin_t 数组,每个元素都是完整的大结构体。
一句话:强转不改变内存大小,只改变"解读视角"。
dev与pin首地址相同、但pin可见范围更大;因为内存本身按大类型分配,用大视角读真实存在的ops/base是安全的。
(4)强转后 pin 额外能访问 ops、base —— 这两个值是注册时写入的
dev(os_device_t *)只能访问前段的公共字段;强转成 pin(os_device_pin_t *)后,编译器允许它继续访问 parent 之后的 ops 和 base。这两个字段不是强转凭空生成的,而是早在 os_device_pin_register 里被显式写入(见 4.1 节第 4 步):
pin->ops = ops; // ops 指向 BSP 的操作函数表
pin->base = pin_base; // base 记录芯片基址所以强转后 pin->ops、pin->base 已经是有效值(注册前为静态零值 NULL/0),第 3 步才能直接用 pin->ops->pin_read(...) 和 pin->base。
(5)OS_ASSERT(pin != OS_NULL):防空指针断言。若 dev 为空则断言失败,提示调用方传入了非法设备指针。因为 dev 与 pin 地址相同,pin == OS_NULL 等价于 dev == OS_NULL。
第 2 步:校验缓冲区与大小(条件 status == OS_NULL 或 size != sizeof(*status))
status = (struct os_device_pin_status *)buffer;
if (status == OS_NULL || size != sizeof(*status))
return 0;详细解释:
status = (struct os_device_pin_status *)buffer:把buffer当作os_device_pin_status *使用。- 传出参数判空:
status == OS_NULL时返回 0——这是传出参数判空的典型场景,调用方必须提供有效的输出缓冲区。 - 大小校验:
size != sizeof(*status)时返回 0——调用方传入的字节数必须恰好等于一个os_device_pin_status结构体的大小(4 字节:uint16_t pin+uint16_t status),既不能多也不能少,防止越界。
数值演算(设 buffer 非空,sizeof(os_device_pin_status) = 4):
| 计算项 | 代入计算 | 结果 |
|---|---|---|
size 合法值 | size == 4 | 通过校验,继续执行 |
size 过小 | size == 2 | 2 != 4,条件成立 → 返回 0 |
size 过大 | size == 8 | 8 != 4,条件成立 → 返回 0 |
buffer 为空 | status == OS_NULL | 条件成立 → 返回 0 |
第 3 步:调用底层 pin_read 并把电平写入输出结构体(无条件执行)
status->status = pin->ops->pin_read(dev, status->pin - pin->base);
return size;详细解释:
status->pin - pin->base:status->pin是完整引脚编号(含千位基址),减去pin->base得到真正硬件引脚号(如1013 - 1000 = 13)。pin->ops->pin_read(dev, 真正引脚号):调用 BSP 的读电平函数,返回PIN_LOW/PIN_HIGH。status->status = ...:把读到的电平值写回输出结构体的status字段,调用方从buffer->status取出结果。return size:返回size(即4),表示成功读取 4 字节。
数值演算(设 status->pin = 1013,pin->base = 1000,读回高电平):
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 真正引脚号 | status->pin - pin->base = 1013 - 1000 | 13 |
调用 pin_read | pin_read(dev, 13) 返回 | PIN_HIGH(1) |
| 写入输出 | status->status = 1 | status->status 变为 1 |
| 返回值 | return size | 4 |
_pin_read() 条件走向总结:
_pin_read() 条件走向总结:
├── pin = (os_device_pin_t *)dev,OS_ASSERT(pin != OS_NULL)(空则断言失败)
├── status = (os_device_pin_status *)buffer
├── status == OS_NULL(传出缓冲区为空)
│ └── 返回 0
├── size != sizeof(*status)(大小不匹配,合法为 4)
│ └── 返回 0
└── 校验通过
├── status->status = pin->ops->pin_read(dev, status->pin - pin->base)
└── 返回 size(4)使用示例(完整流程:应用层通过框架 API 读引脚电平):
#include <pin.h>
#include <device.h>
#define READ_PIN (1000 + 13) /* 要读取的完整引脚号 */
int read_pin_via_framework(void)
{
struct os_device_pin_status status;
struct os_device *pin_dev;
os_ssize_t ret;
/* 1. 通过设备名 "pin_0" 找到 PIN 设备(框架 API) */
pin_dev = os_device_find("pin_0");
if (pin_dev == OS_NULL)
{
os_kprintf("pin_0 not found\r\n");
return -1;
}
/* 2. 先配成输入模式(可通过 os_pin_mode,或同样走框架 control) */
os_pin_mode(READ_PIN, PIN_MODE_INPUT_PULLUP);
/* 3. 填充要读取的引脚号(完整编号)—— status->pin 由此而来 */
status.pin = READ_PIN; /* 1013 */
/* 4. 调用框架读接口,内部会触发 _pin_read */
ret = os_device_read_block(pin_dev, 0, &status, sizeof(status));
if (ret != (os_ssize_t)sizeof(status))
{
os_kprintf("read failed, ret=%d\r\n", (int)ret);
return -1;
}
/* 5. 读到的电平写回 status.status,直接使用 */
os_kprintf("pin %d level = %s\r\n", READ_PIN, status.status == PIN_HIGH ? "HIGH" : "LOW");
return 0;
}运行过程推演(假设引脚实际为高电平):
| 步骤 | 代码 | 关键变量 | 结果 |
|---|---|---|---|
| 1 | os_device_find("pin_0") | pin_dev 指向 PIN 设备 | 非空,继续 |
| 2 | os_pin_mode(1013, INPUT_PULLUP) | — | 配置上拉输入 |
| 3 | status.pin = 1013 | status.pin = 1013 | 应用填入完整编号 |
| 4 | os_device_read_block(pin_dev, 0, &status, 4) | 框架调 _pin_read | 见下方逐步对照 |
| 5 | os_kprintf(...) | status.status = PIN_HIGH | 打印 pin 1013 level = HIGH |
第 4 步如何一步步走进 _pin_read 并回填结果:
os_device_read_block(pin_dev, 0, &status, 4)
│
▼(设备框架,见 5.1 节 4.6)
调用 pin_dev->ops->read(...) 即 pin_ops.read → _pin_read
│
▼
_pin_read(dev=pin_dev, pos=0, buffer=&status, size=4)
│
├── 第1步:pin = (os_device_pin *)dev,OS_ASSERT 非空 → 通过
├── 第2步:status = (os_device_pin_status *)buffer → &status
│ size==4 且 ==sizeof(*status) → 通过校验
├── 第3步:status->status = pin->ops->pin_read(dev, status->pin - pin->base)
│ 1013 - 1000 = 13
│ └─ BSP stm32_pin_read(dev, 13) 读到高电平,返回 PIN_HIGH
│ status->status = PIN_HIGH(1)
└── 返回 size(4)
│
▼(回到应用)
ret == 4 == sizeof(status) → 成功,打印 status.status说明:示例演示了不直接调
os_pin_read,而是通过设备框架 API 读引脚。虽然日常更常用os_pin_read(pin)(更简洁),但本示例展示了当需要通过统一设备框架访问、或配合 VFS/devfs 时的用法,也让你看清_pin_read在整条链路中的位置。
2.4.2 _pin_write —— 框架"写"桥接
函数签名:
static os_ssize_t _pin_write(os_device_t *dev, os_off_t pos, const void *buffer, os_size_t size)| 参数 | 说明 |
|---|---|
dev | 设备控制块指针(实际指向 os_device_pin_t) |
pos | 写入位置偏移(本函数不使用) |
buffer | 待写入的数据缓冲区,必须指向一个 os_device_pin_status 结构体 |
size | 缓冲区的字节大小 |
| 返回值 | 说明 |
|---|---|
size | 成功,返回实际写入的字节数(即 sizeof(os_device_pin_status)) |
0 | 失败(buffer 为空,或 size 不是 sizeof(*status)) |
分步详解:
第 1 步:把 dev 强转为 PIN 设备结构体指针(无条件执行)
struct os_device_pin_status *status;
struct os_device_pin *pin = (struct os_device_pin *)dev;
/* check parameters */
OS_ASSERT(pin != OS_NULL);详细解释:与 _pin_read 第 1 步完全一致——利用"首成员即 os_device"强转得到 pin,OS_ASSERT 防空。
第 2 步:校验缓冲区与大小(条件 status == OS_NULL 或 size != sizeof(*status))
status = (struct os_device_pin_status *)buffer;
if (status == OS_NULL || size != sizeof(*status))
return 0;详细解释:与 _pin_read 第 2 步相同的传入参数判空 + 大小校验。这里 status 是输入(调用方填充好要写的引脚和电平),判空 + 大小校验保证读结构体不越界。
第 3 步:调用底层 pin_write 写电平(无条件执行)
pin->ops->pin_write(dev, (os_base_t)status->pin - pin->base, (os_base_t)status->status);
return size;详细解释:
(os_base_t)status->pin - pin->base:完整引脚号减去基址,得真正引脚号;(os_base_t)强转保证类型一致。(os_base_t)status->status:status->status是调用方填的电平值(PIN_LOW/PIN_HIGH),强转为os_base_t传给底层。- 调用
pin->ops->pin_write(dev, 引脚号, 电平)完成硬件写。 return size:返回4,表示成功写入 4 字节。
数值演算(设 status->pin = 1013,pin->base = 1000,status->status = PIN_HIGH):
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 真正引脚号 | (os_base_t)1013 - 1000 | 13 |
| 电平值 | (os_base_t)status->status | PIN_HIGH(1) |
调用 pin_write | pin_write(dev, 13, 1) | 完成写高电平 |
| 返回值 | return size | 4 |
_pin_write() 条件走向总结:
_pin_write() 条件走向总结:
├── pin = (os_device_pin_t *)dev,OS_ASSERT(pin != OS_NULL)(空则断言失败)
├── status = (os_device_pin_status *)buffer
├── status == OS_NULL(缓冲区为空)
│ └── 返回 0
├── size != sizeof(*status)(大小不匹配,合法为 4)
│ └── 返回 0
└── 校验通过
├── pin->ops->pin_write(dev, (os_base_t)status->pin - pin->base, (os_base_t)status->status)
└── 返回 size(4)2.4.3 _pin_control —— 框架"控制"桥接
函数签名:
static os_err_t _pin_control(os_device_t *dev, int cmd, void *args)| 参数 | 说明 |
|---|---|
dev | 设备控制块指针(实际指向 os_device_pin_t) |
cmd | 控制命令(本函数不使用,PIN 设备只有"设模式"一个控制动作) |
args | 命令参数,必须指向一个 os_device_pin_mode 结构体 |
| 返回值 | 说明 |
|---|---|
0 | 成功(等价于 OS_SUCCESS) |
OS_FAILURE(-1) | 失败(args 为空) |
分步详解:
第 1 步:把 dev 强转为 PIN 设备结构体指针(无条件执行)
struct os_device_pin_mode *mode;
struct os_device_pin *pin = (struct os_device_pin *)dev;
/* check parameters */
OS_ASSERT(pin != OS_NULL);详细解释:与前两个函数相同的强转 + 防空断言。
第 2 步:取出模式结构体并判空(条件 mode == OS_NULL)
mode = (struct os_device_pin_mode *)args;
if (mode == OS_NULL)
return OS_FAILURE;详细解释:
mode = (struct os_device_pin_mode *)args:把args当作os_device_pin_mode *(含pin和mode两个字段)。- 参数判空:
mode == OS_NULL时返回OS_FAILURE——这是本函数唯一的失败分支。调用方必须提供有效的os_device_pin_mode参数。
第 3 步:调用底层 pin_mode 设置模式(无条件执行)
pin->ops->pin_mode(dev, (os_base_t)mode->pin - pin->base, (os_base_t)mode->mode);
return 0;详细解释:
(os_base_t)mode->pin - pin->base:完整引脚号减去基址,得真正引脚号。(os_base_t)mode->mode:mode->mode是调用方填的模式值(如PIN_MODE_INPUT_PULLUP),强转为os_base_t。- 调用
pin->ops->pin_mode(dev, 引脚号, 模式)完成模式配置。 return 0:返回0(即OS_SUCCESS),表示成功。
数值演算(设 mode->pin = 1005,pin->base = 1000,mode->mode = PIN_MODE_INPUT_PULLUP):
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 真正引脚号 | (os_base_t)1005 - 1000 | 5 |
| 模式值 | (os_base_t)mode->mode | PIN_MODE_INPUT_PULLUP(0x02) |
调用 pin_mode | pin_mode(dev, 5, 0x02) | 完成配置上拉输入 |
| 返回值 | return 0 | OS_SUCCESS(0) |
_pin_control() 条件走向总结:
_pin_control() 条件走向总结:
├── pin = (os_device_pin_t *)dev,OS_ASSERT(pin != OS_NULL)(空则断言失败)
├── mode = (os_device_pin_mode *)args
├── mode == OS_NULL(参数为空)
│ └── 返回 OS_FAILURE
└── mode 非空
├── pin->ops->pin_mode(dev, (os_base_t)mode->pin - pin->base, (os_base_t)mode->mode)
└── 返回 0(OS_SUCCESS)2.4.4 三个辅助函数的对比与触发链路
| 辅助函数 | 框架 API 触发 | 调用底层 ops | 返回类型 | 失败返回 |
|---|---|---|---|---|
_pin_read | os_device_read_block / os_device_read_nonblock | ops->pin_read | os_ssize_t | 0 |
_pin_write | os_device_write_block / os_device_write_nonblock | ops->pin_write | os_ssize_t | 0 |
_pin_control | os_device_control | ops->pin_mode | os_err_t | OS_FAILURE |
完整触发链路(以"读引脚"为例):
应用调用 os_device_read_block(pin_dev, 0, &status, sizeof(status))
│
▼
设备框架 os_device_read_block(见 5.1 节 4.6)
│ 调用 dev->ops->read(...) → 即 pin_ops.read → _pin_read
▼
_pin_read(dev, pos, buffer, size)
│
├── 强转 dev → pin,OS_ASSERT 防空
├── 判空/判大小,不合法返回 0
└── status->status = pin->ops->pin_read(dev, status->pin - pin->base) → 真正读硬件
│
▼
BSP 的 stm32_pin_read 等(操作 GPIO 寄存器)关键理解:
- 三个函数都是"校验 → 换算引脚号 → 转发到 ops"的薄封装,真正的硬件操作在 BSP 层。
pos/cmd参数在 PIN 设备场景下不使用,因为 PIN 设备本质是"单个引脚"操作,没有偏移、命令多样性的概念。- 三者都用
xxx->pin - pin->base做引脚编号换算(完整编号 → 芯片内偏移),这是 PIN 设备框架层的核心约定。
2.5 辅助结构体
来源:drivers/pin/pin.h
struct os_device_pin_mode
{
uint16_t pin;
uint16_t mode;
};
struct os_device_pin_status
{
uint16_t pin;
uint16_t status;
};
struct os_pin_irq_hdr
{
int16_t pin;
uint16_t mode;
void (*hdr)(void *args);
void *args;
};| 结构体 | 字段 | 说明 |
|---|---|---|
os_device_pin_mode | pin/mode | 用于设备框架 control 命令:pin 是完整引脚编号,mode 是引脚模式 |
os_device_pin_status | pin/status | 用于设备框架 read/write 命令:pin 是完整引脚编号,status 是电平值 |
os_pin_irq_hdr | pin/mode/hdr/args | 描述一次引脚中断绑定:引脚号、触发模式、回调函数、回调参数 |
关键点:
os_device_pin_mode/os_device_pin_status主要被 2.4 的内部辅助函数使用(框架接口参数载体)。os_pin_irq_hdr在 BSP 驱动中用作中断回调登记表项(如 STM32 的pin_irq_hdr_tab[],每个引脚一个,保存回调与参数)。- 注意
os_device_pin_mode/os_device_pin_status的pin字段是完整编号(含千位基址),而os_pin_irq_hdr的pin字段通常是芯片内偏移(已减 base),含义不同。
三、引脚模式与宏定义详解
pin.h 里定义了一套引脚模式、电平、中断触发模式的宏。理解这些宏的取值对正确使用至关重要。
3.1 电平定义
来源:drivers/pin/pin.h
#define PIN_LOW 0x00
#define PIN_HIGH 0x01| 宏 | 值 | 说明 |
|---|---|---|
PIN_LOW | 0x00 | 低电平(0) |
PIN_HIGH | 0x01 | 高电平(1) |
3.2 引脚模式
来源:drivers/pin/pin.h
#define PIN_MODE_OUTPUT 0x00
#define PIN_MODE_INPUT 0x01
#define PIN_MODE_INPUT_PULLUP 0x02
#define PIN_MODE_INPUT_PULLDOWN 0x03
#define PIN_MODE_OUTPUT_OD 0x04
#define PIN_MODE_DISABLE 0x08| 模式宏 | 值(二进制) | 说明 | 典型应用 |
|---|---|---|---|
PIN_MODE_OUTPUT | 0x00(0000) | 推挽输出,可主动输出高低电平 | LED 驱动、继电器控制 |
PIN_MODE_INPUT | 0x01(0001) | 浮空输入,引脚悬空时电平不确定 | 外部信号已确定电平的场景 |
PIN_MODE_INPUT_PULLUP | 0x02(0010) | 内部上拉输入,悬空时默认为高电平 | 按键检测(按下为低电平) |
PIN_MODE_INPUT_PULLDOWN | 0x03(0011) | 内部下拉输入,悬空时默认为低电平 | 按键检测(按下为高电平) |
PIN_MODE_OUTPUT_OD | 0x04(0100) | 开漏输出,只能输出低电平或高阻态,需外接上拉电阻 | I2C 总线、电平转换 |
PIN_MODE_DISABLE | 0x08(1000) | 禁用引脚(还原为默认态) | 释放引脚、低功耗 |
关键理解——这些值不是位掩码:
- 很多初学者以为这些模式值是"位标志"可以叠加(如"输入 + 上拉" =
0x01 | 0x02 = 0x03)。在 OneOS 里它们是独立的枚举值,每个宏代表一种完整模式,不能叠加。 - 观察二进制:
0x00、0x01、0x02、0x03、0x04、0x08递增排列,正好对应 BSP 驱动里if (mode == PIN_MODE_XXX)的逐条判断(见 2.2 节 STM32 的stm32_pin_mode)。
3.3 中断触发模式
来源:drivers/pin/pin.h
#define PIN_IRQ_MODE_RISING 0x00
#define PIN_IRQ_MODE_FALLING 0x01
#define PIN_IRQ_MODE_RISING_FALLING 0x02
#define PIN_IRQ_MODE_HIGH_LEVEL 0x03
#define PIN_IRQ_MODE_LOW_LEVEL 0x04| 中断模式 | 值 | 说明 |
|---|---|---|
PIN_IRQ_MODE_RISING | 0x00 | 上升沿触发(引脚从低变高时触发一次) |
PIN_IRQ_MODE_FALLING | 0x01 | 下降沿触发(引脚从高变低时触发一次) |
PIN_IRQ_MODE_RISING_FALLING | 0x02 | 双边沿触发(任意电平变化时触发) |
PIN_IRQ_MODE_HIGH_LEVEL | 0x03 | 高电平触发(引脚为高时持续触发) |
PIN_IRQ_MODE_LOW_LEVEL | 0x04 | 低电平触发(引脚为低时持续触发) |
注意:电平触发(
HIGH_LEVEL/LOW_LEVEL)依赖具体 BSP 支持。如 STM32 的stm32_pin_attach_irq只处理三个边沿模式,遇到电平模式会走default返回OS_FAILURE(见 2.2 节源码)。
3.4 中断使能
来源:drivers/pin/pin.h
#define PIN_IRQ_DISABLE 0x00
#define PIN_IRQ_ENABLE 0x01| 宏 | 值 | 说明 |
|---|---|---|
PIN_IRQ_DISABLE | 0x00 | 禁用中断 |
PIN_IRQ_ENABLE | 0x01 | 使能中断 |
3.5 其它定义
来源:drivers/pin/pin.h
#define PIN_IRQ_PIN_NONE -1PIN_IRQ_PIN_NONE = -1 表示"无中断引脚",在 BSP 驱动里作为回调登记表项的空标志(如 STM32 的 pin_irq_hdr_tab[] 初始值 {-1, 0, OS_NULL, OS_NULL}),表示该引脚未绑定中断。
四、核心 API 详解
说明:以下 API 的完整源码均取自
drivers/pin/pin.c,与项目源码逐字一致(含原始英文注释)。
4.1 os_device_pin_register —— 注册 PIN 设备
注册 PIN 设备,通常由 BSP 层在初始化时调用(如 STM32 的 os_hw_pin_init 末尾调用它)。
int os_device_pin_register(int pin_base, const struct os_pin_ops *ops, void *user_data);| 参数 | 说明 |
|---|---|
pin_base | 引脚基地址(千位),必须是 0、1000、2000... |
ops | PIN 操作函数表指针(BSP 实现) |
user_data | 用户私有数据(当前未使用) |
| 返回值 | 说明 |
|---|---|
0 | 注册成功 |
-1 | 注册失败(基址非法 / 芯片索引越界 / 该芯片已注册) |
完整源码(drivers/pin/pin.c,与项目一致):
int os_device_pin_register(int pin_base, const struct os_pin_ops *ops, void *user_data)
{
int pin_index;
char name[12];
if ((pin_base % 1000) != 0)
{
os_kprintf("pin base invalid[%d], must be 0, 1000, 2000, ...\r\n", pin_base);
return -1;
}
pin_index = pin_base / 1000;
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("%d outof chip number range:(%d) >= (%d)\r\n", pin_base, pin_index, OS_PIN_MAX_CHIP);
return -1;
}
struct os_device_pin *pin = &gs_pin_table[pin_index];
if (pin->parent.type == OS_DEVICE_TYPE_MISCELLANEOUS)
{
os_kprintf("pin %d exist.\r\n", pin_base);
return -1;
}
pin->parent.type = OS_DEVICE_TYPE_MISCELLANEOUS;
pin->parent.ops = &pin_ops;
pin->ops = ops;
pin->base = pin_base;
pin->parent.user_data = user_data;
os_snprintf(name, sizeof(name) - 1, "pin_%d", pin_base);
os_device_register(&pin->parent, name);
return 0;
}分步详解:
第 1 步:基址合法性校验(条件 (pin_base % 1000) != 0)
if ((pin_base % 1000) != 0)
{
os_kprintf("pin base invalid[%d], must be 0, 1000, 2000, ...\r\n", pin_base);
return -1;
}详细解释:pin_base 必须是 1000 的整数倍(0、1000、2000...)。这是为了用 pin_base / 1000 唯一映射到芯片索引。否则打印错误并返回 -1。
数值演算:基址与芯片索引的换算关系:
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 第 0 片基址 | 0 / 1000 | 0(索引 0) |
| 第 1 片基址 | 1000 / 1000 | 1(索引 1) |
| 第 2 片基址 | 2000 / 1000 | 2(索引 2) |
第 2 步:芯片索引越界检查(条件 pin_index >= OS_PIN_MAX_CHIP)
pin_index = pin_base / 1000;
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("%d outof chip number range:(%d) >= (%d)\r\n", pin_base, pin_index, OS_PIN_MAX_CHIP);
return -1;
}详细解释:
pin_index = pin_base / 1000:算出该基址对应的芯片索引。- 检查目的:
pin_index >= OS_PIN_MAX_CHIP时,说明pin_index已经超出gs_pin_table[OS_PIN_MAX_CHIP]数组的合法下标范围(合法为0 ~ OS_PIN_MAX_CHIP-1)。若不拦截,下一步&gs_pin_table[pin_index]就会访问越界内存,读取脏数据甚至触发硬件错误。为什么用>=而不是>:gs_pin_table[OS_PIN_MAX_CHIP]本身就越界(下标从 0 起),所以等于OS_PIN_MAX_CHIP也必须拦截(详见 1.2 节"芯片索引越界检查"共性说明)。 - 越界时打印
outof chip number range并返回-1。默认OS_PIN_MAX_CHIP = 1,即只允许注册基址0这一片芯片。
数值演算(默认 OS_PIN_MAX_CHIP = 1):
| 计算项 | 代入计算 | 结果 | 是否越界 |
|---|---|---|---|
基址 0 | 0 / 1000 | 索引 0 | 否(0 >= 1 假),允许注册 |
基址 1000 | 1000 / 1000 | 索引 1 | 是(1 >= 1 真),返回 -1 |
基址 2000 | 2000 / 1000 | 索引 2 | 是(2 >= 1 真),返回 -1 |
第 3 步:取设备槽位并检查是否已注册(条件 pin->parent.type == OS_DEVICE_TYPE_MISCELLANEOUS)
struct os_device_pin *pin = &gs_pin_table[pin_index];
if (pin->parent.type == OS_DEVICE_TYPE_MISCELLANEOUS)
{
os_kprintf("pin %d exist.\r\n", pin_base);
return -1;
}详细解释:pin 指向 gs_pin_table[pin_index]。关键技巧:用 parent.type 是否等于 OS_DEVICE_TYPE_MISCELLANEOUS 判断该槽是否已注册——因为注册时(下一步)会把 parent.type 置为 OS_DEVICE_TYPE_MISCELLANEOUS(未初始化的全局数组为 0,即 OS_DEVICE_TYPE_CHAR,不等于它)。若已注册则提示 pin %d exist. 并返回 -1。
第 4 步:填写设备框架字段(无条件执行)
pin->parent.type = OS_DEVICE_TYPE_MISCELLANEOUS;
pin->parent.ops = &pin_ops;
pin->ops = ops;
pin->base = pin_base;
pin->parent.user_data = user_data;详细解释:
parent.type = OS_DEVICE_TYPE_MISCELLANEOUS:把设备类型标为"杂项设备"(同时作为"已注册"标记)。parent.ops = &pin_ops:把设备框架操作表设为pin_ops(2.3 节),这样os_device_read_block等能通过_pin_read等桥接。ops = ops:记录引脚专用操作表(BSP 传进来的)。base = pin_base:记录本芯片的千位基址。parent.user_data = user_data:记录用户数据。
第 5 步:注册到设备框架并返回(无条件执行)
os_snprintf(name, sizeof(name) - 1, "pin_%d", pin_base);
os_device_register(&pin->parent, name);
return 0;详细解释:用 os_snprintf 生成设备名 "pin_%d"(如基址 0 生成 "pin_0"),然后调用 os_device_register 把 parent 注册进全局设备链表(详见 5.1 节)。成功后返回 0。
os_device_pin_register() 条件走向总结:
os_device_pin_register() 条件走向总结:
├── (pin_base % 1000) != 0(基址非法)
│ └── 打印 "pin base invalid" → 返回 -1
├── pin_index = pin_base / 1000
├── pin_index >= OS_PIN_MAX_CHIP(芯片索引越界)
│ └── 打印 "outof chip number range" → 返回 -1
├── pin->parent.type == OS_DEVICE_TYPE_MISCELLANEOUS(槽位已注册)
│ └── 打印 "pin %d exist" → 返回 -1
├── 填写 parent.type / parent.ops / ops / base / user_data
├── os_snprintf 生成 "pin_%d" 名称
├── os_device_register(&pin->parent, name)
└── 返回 0(成功)使用示例(BSP 层注册 PIN 设备,参考 STM32 的 os_hw_pin_init):
#include <pin/pin.h>
/* BSP 实现的操作函数表(示意) */
extern const struct os_pin_ops my_pin_ops;
int my_pin_init(void)
{
int ret = os_device_pin_register(0, &my_pin_ops, OS_NULL);
if (ret != 0)
{
os_kprintf("pin register failed, ret=%d\r\n", ret);
return -1;
}
os_kprintf("pin device registered as pin_0\r\n");
return 0;
}说明:注册成功后,设备名为
"pin_0",应用层可通过os_device_find("pin_0")用框架接口访问它。
4.2 os_pin_mode / raw_os_pin_mode —— 设置引脚模式
设置指定引脚的工作模式(输出/输入/上拉/下拉等)。
void os_pin_mode(os_base_t pin, os_base_t mode);
void raw_os_pin_mode(os_base_t pin, os_base_t mode);| 参数 | 说明 |
|---|---|
pin | 引脚编号(含千位基址,如 1013) |
mode | 模式值,如 PIN_MODE_OUTPUT、PIN_MODE_INPUT_PULLUP 等 |
无返回值(void)。
完整源码(drivers/pin/pin.c,与项目一致):
void raw_os_pin_mode(os_base_t pin, os_base_t mode)
{
int pin_index = PIN_BASE(pin);
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return;
}
struct os_device_pin *ppin = &gs_pin_table[pin_index];
ppin->ops->pin_mode((struct os_device *)ppin, pin - ppin->base, mode);
}
OS_WEAK void os_pin_mode(os_base_t pin, os_base_t mode)
{
raw_os_pin_mode(pin, mode);
}分步详解(以 raw_os_pin_mode 为主):
第 1 步:换算芯片索引(无条件执行)
int pin_index = PIN_BASE(pin);详细解释:PIN_BASE(pin) 是快速除法宏 (4294968ULL * pin) >> 32,等价于 pin / 1000,算出芯片索引。
数值演算:
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 完整引脚号 | pin = 1013 | 1013 |
PIN_BASE(1013) | (4294968 × 1013) >> 32 | 1(≈ 1013/1000) |
第 2 步:芯片索引越界检查(条件 pin_index >= OS_PIN_MAX_CHIP)
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return;
}详细解释:若芯片索引越界(即 pin_index 超出 gs_pin_table[OS_PIN_MAX_CHIP] 的合法下标 0 ~ OS_PIN_MAX_CHIP-1),打印 invalide pin 并直接返回(void 函数无返回值,静默失败)。这是该函数唯一能"报错"的方式——越界会访问数组越界内存,必须在此拦截(原理详见 1.2 节"芯片索引越界检查")。注意:os_pin_mode 是 void 函数,越界时调用方无法感知失败,因此应用层使用前应确保引脚号合法。
第 3 步:取设备并调用底层 pin_mode(无条件执行)
struct os_device_pin *ppin = &gs_pin_table[pin_index];
ppin->ops->pin_mode((struct os_device *)ppin, pin - ppin->base, mode);详细解释:
ppin指向gs_pin_table[pin_index]。pin - ppin->base:完整编号减去基址,得到真正硬件引脚号(如1013 - 1000 = 13)。- 调用
ops->pin_mode把模式写进硬件。
关于 os_pin_mode(OS_WEAK 包装):
OS_WEAK void os_pin_mode(os_base_t pin, os_base_t mode)
{
raw_os_pin_mode(pin, mode);
}详细解释:os_pin_mode 是弱函数(OS_WEAK),默认直接转发到 raw_os_pin_mode。BSP 或应用可以重新定义强符号 os_pin_mode 覆盖它,实现自定义行为(如加锁、加日志)。os_pin_write/os_pin_read 同理。
raw_os_pin_mode() 条件走向总结:
raw_os_pin_mode() 条件走向总结:
├── pin_index = PIN_BASE(pin)
├── pin_index >= OS_PIN_MAX_CHIP
│ └── 打印 "invalide pin" → 返回(静默失败)
└── 否则
├── ppin = &gs_pin_table[pin_index]
└── ppin->ops->pin_mode(dev, pin - ppin->base, mode) → 完成(无返回值)4.3 os_pin_write / raw_os_pin_write —— 写引脚电平
设置指定引脚的输出电平。
void os_pin_write(os_base_t pin, os_base_t value);
void raw_os_pin_write(os_base_t pin, os_base_t value);| 参数 | 说明 |
|---|---|
pin | 引脚编号(含千位基址) |
value | 电平值:PIN_LOW 或 PIN_HIGH |
无返回值(void)。
完整源码(drivers/pin/pin.c,与项目一致):
void raw_os_pin_write(os_base_t pin, os_base_t value)
{
int pin_index = PIN_BASE(pin);
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return;
}
struct os_device_pin *ppin = &gs_pin_table[pin_index];
ppin->ops->pin_write((struct os_device *)ppin, pin - ppin->base, value);
}
OS_WEAK void os_pin_write(os_base_t pin, os_base_t value)
{
raw_os_pin_write(pin, value);
}分步详解:
第 1 步:换算芯片索引(无条件执行)
int pin_index = PIN_BASE(pin);详细解释:与 4.2 相同,PIN_BASE(pin) 等价于 pin / 1000。
第 2 步:芯片索引越界检查(条件 pin_index >= OS_PIN_MAX_CHIP)
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return;
}详细解释:越界则打印 invalide pin 并静默返回(void 函数无返回值,调用方无法感知失败,原理详见 1.2 节"芯片索引越界检查")。应用层使用前应确保引脚号合法。
第 3 步:取设备并调用底层 pin_write(无条件执行)
struct os_device_pin *ppin = &gs_pin_table[pin_index];
ppin->ops->pin_write((struct os_device *)ppin, pin - ppin->base, value);详细解释:ppin 指向设备槽,pin - ppin->base 得到真正引脚号,调用 ops->pin_write 把 value(电平)写到硬件。
关于 os_pin_write:同样是 OS_WEAK 弱函数包装,默认转发到 raw_os_pin_write,可被强符号覆盖。
raw_os_pin_write() 条件走向总结:
raw_os_pin_write() 条件走向总结:
├── pin_index = PIN_BASE(pin)
├── pin_index >= OS_PIN_MAX_CHIP
│ └── 打印 "invalide pin" → 返回(静默失败)
└── 否则
├── ppin = &gs_pin_table[pin_index]
└── ppin->ops->pin_write(dev, pin - ppin->base, value) → 完成(无返回值)4.4 os_pin_read / raw_os_pin_read —— 读引脚电平
读取指定引脚的当前电平。
int os_pin_read(os_base_t pin);
int raw_os_pin_read(os_base_t pin);| 参数 | 说明 |
|---|---|
pin | 引脚编号(含千位基址) |
| 返回值 | 说明 |
|---|---|
PIN_LOW(0) | 读到的低电平 |
PIN_HIGH(1) | 读到的高电平 |
OS_INVAL(-10) | 芯片索引越界 |
完整源码(drivers/pin/pin.c,与项目一致):
int raw_os_pin_read(os_base_t pin)
{
int pin_index = PIN_BASE(pin);
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return OS_INVAL;
}
struct os_device_pin *ppin = &gs_pin_table[pin_index];
return ppin->ops->pin_read((struct os_device *)ppin, pin - ppin->base);
}
OS_WEAK int os_pin_read(os_base_t pin)
{
return raw_os_pin_read(pin);
}分步详解:
第 1 步:换算芯片索引(无条件执行)
int pin_index = PIN_BASE(pin);详细解释:与 4.2/4.3 相同,PIN_BASE(pin) 等价于 pin / 1000。
第 2 步:芯片索引越界检查(条件 pin_index >= OS_PIN_MAX_CHIP)
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return OS_INVAL;
}详细解释:越界打印 invalide pin 并返回 OS_INVAL(-10),拦截对 gs_pin_table 的越界访问(原理详见 1.2 节"芯片索引越界检查")。注意:这里和 mode/write 不同——mode/write 是 void 函数,越界只能静默返回;而 read 有返回值,越界时可以返回 OS_INVAL 错误码,让调用方感知失败。
第 3 步:取设备并调用底层 pin_read,返回结果(无条件执行)
struct os_device_pin *ppin = &gs_pin_table[pin_index];
return ppin->ops->pin_read((struct os_device *)ppin, pin - ppin->base);详细解释:ppin 指向设备槽,pin - ppin->base 得到真正引脚号,调用 ops->pin_read 返回电平值(PIN_LOW/PIN_HIGH),直接作为本函数返回值。
关于 os_pin_read:OS_WEAK 弱函数包装,返回 raw_os_pin_read(pin) 的结果,可被强符号覆盖。
raw_os_pin_read() 条件走向总结:
raw_os_pin_read() 条件走向总结:
├── pin_index = PIN_BASE(pin)
├── pin_index >= OS_PIN_MAX_CHIP
│ └── 打印 "invalide pin" → 返回 OS_INVAL
└── 否则
└── 返回 ppin->ops->pin_read(dev, pin - ppin->base) // PIN_LOW 或 PIN_HIGH4.5 os_pin_attach_irq —— 绑定中断回调
为指定引脚绑定外部中断回调函数。
os_err_t os_pin_attach_irq(int32_t pin, uint32_t mode, void (*hdr)(void *args), void *args);| 参数 | 说明 |
|---|---|
pin | 引脚编号(含千位基址) |
mode | 中断触发模式,如 PIN_IRQ_MODE_FALLING |
hdr | 中断回调函数指针 void (*)(void *args) |
args | 传递给回调函数的参数 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS(0) | 绑定成功 |
OS_INVAL(-10) | 芯片索引越界 |
OS_NOSYS(-6) | 底层未实现 pin_attach_irq(ops->pin_attach_irq 为空) |
OS_BUSY(-7) | 引脚已被占用(由底层驱动返回) |
完整源码(drivers/pin/pin.c,与项目一致):
os_err_t os_pin_attach_irq(int32_t pin, uint32_t mode, void (*hdr)(void *args), void *args)
{
int pin_index = pin / 1000;
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return OS_INVAL;
}
struct os_device_pin *ppin = &gs_pin_table[pin_index];
if (ppin->ops->pin_attach_irq)
{
return ppin->ops->pin_attach_irq((struct os_device *)ppin, pin - ppin->base, mode, hdr, args);
}
return OS_NOSYS;
}分步详解:
第 1 步:换算芯片索引(无条件执行)
int pin_index = pin / 1000;详细解释:注意这里直接用了 pin / 1000,而不是 PIN_BASE(pin) 宏(两者等价,写法不同)。算出芯片索引。
第 2 步:芯片索引越界检查(条件 pin_index >= OS_PIN_MAX_CHIP)
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return OS_INVAL;
}详细解释:越界打印 invalide pin 并返回 OS_INVAL——即 pin_index 超出 gs_pin_table[OS_PIN_MAX_CHIP] 合法下标 0 ~ OS_PIN_MAX_CHIP-1,必须在此拦截越界内存访问(原理详见 1.2 节"芯片索引越界检查")。
第 3 步:取设备并检查底层是否实现绑定函数(条件 ppin->ops->pin_attach_irq 非空)
struct os_device_pin *ppin = &gs_pin_table[pin_index];
if (ppin->ops->pin_attach_irq)
{
return ppin->ops->pin_attach_irq((struct os_device *)ppin, pin - ppin->base, mode, hdr, args);
}
return OS_NOSYS;详细解释:
ppin指向设备槽。- 若
ops->pin_attach_irq非空(BSP 实现了中断绑定),则调用它:传pin - ppin->base(真正引脚号)、mode(触发模式)、hdr(回调)、args(参数),并把其返回值直接返回。 - 若
ops->pin_attach_irq为空(该 BSP 不支持中断),返回OS_NOSYS(功能未实现)。
os_pin_attach_irq() 条件走向总结:
os_pin_attach_irq() 条件走向总结:
├── pin_index = pin / 1000
├── pin_index >= OS_PIN_MAX_CHIP
│ └── 打印 "invalide pin" → 返回 OS_INVAL
├── ppin = &gs_pin_table[pin_index]
├── ops->pin_attach_irq 非空
│ └── 返回 ops->pin_attach_irq(dev, pin - base, mode, hdr, args) // 底层返回 OS_SUCCESS/OS_BUSY 等
└── ops->pin_attach_irq 为空
└── 返回 OS_NOSYS使用示例(绑定下降沿中断回调):
#include <pin.h>
static void irq_callback(void *args)
{
uint32_t *cnt = (uint32_t *)args;
(*cnt)++;
os_kprintf("irq triggered, cnt=%d\r\n", *cnt);
}
void attach_irq_demo(void)
{
static uint32_t count = 0;
os_err_t ret;
ret = os_pin_attach_irq(1006, PIN_IRQ_MODE_FALLING, irq_callback, &count);
if (ret != OS_SUCCESS)
{
os_kprintf("attach irq failed, ret=%d\r\n", ret);
return;
}
os_kprintf("attach irq ok\r\n");
}4.6 os_pin_detach_irq —— 解绑中断
解除指定引脚的中断绑定。
os_err_t os_pin_detach_irq(int32_t pin);| 参数 | 说明 |
|---|---|
pin | 引脚编号(含千位基址) |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS(0) | 解绑成功 |
OS_INVAL(-10) | 芯片索引越界 |
OS_NOSYS(-6) | 底层未实现 pin_detach_irq |
完整源码(drivers/pin/pin.c,与项目一致):
os_err_t os_pin_detach_irq(int32_t pin)
{
int pin_index = pin / 1000;
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return OS_INVAL;
}
struct os_device_pin *ppin = &gs_pin_table[pin_index];
if (ppin->ops->pin_detach_irq)
{
return ppin->ops->pin_detach_irq((struct os_device *)ppin, pin - ppin->base);
}
return OS_NOSYS;
}分步详解:
第 1 步:换算芯片索引(无条件执行)
int pin_index = pin / 1000;详细解释:pin / 1000 得到芯片索引。
第 2 步:芯片索引越界检查(条件 pin_index >= OS_PIN_MAX_CHIP)
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return OS_INVAL;
}详细解释:越界返回 OS_INVAL——即 pin_index 超出 gs_pin_table[OS_PIN_MAX_CHIP] 合法下标 0 ~ OS_PIN_MAX_CHIP-1,拦截越界内存访问(原理详见 1.2 节"芯片索引越界检查")。
第 3 步:取设备并检查底层是否实现解绑函数(条件 ppin->ops->pin_detach_irq 非空)
struct os_device_pin *ppin = &gs_pin_table[pin_index];
if (ppin->ops->pin_detach_irq)
{
return ppin->ops->pin_detach_irq((struct os_device *)ppin, pin - ppin->base);
}
return OS_NOSYS;详细解释:ops->pin_detach_irq 非空则调用(传 pin - base),返回其结果;为空返回 OS_NOSYS。注意:与 attach 相比,这里不传 mode/hdr/args,因为解绑只需引脚号。
os_pin_detach_irq() 条件走向总结:
os_pin_detach_irq() 条件走向总结:
├── pin_index = pin / 1000
├── pin_index >= OS_PIN_MAX_CHIP → 返回 OS_INVAL
├── ops->pin_detach_irq 非空 → 返回 ops->pin_detach_irq(dev, pin - base)
└── ops->pin_detach_irq 为空 → 返回 OS_NOSYS4.7 os_pin_irq_enable —— 使能/禁用中断
使能或禁用指定引脚的中断。
os_err_t os_pin_irq_enable(os_base_t pin, uint32_t enabled);| 参数 | 说明 |
|---|---|
pin | 引脚编号(含千位基址) |
enabled | PIN_IRQ_ENABLE(使能)或 PIN_IRQ_DISABLE(禁用) |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS(0) | 操作成功 |
OS_INVAL(-10) | 芯片索引越界 |
OS_NOSYS(-6) | 底层未实现 pin_irq_enable |
完整源码(drivers/pin/pin.c,与项目一致):
os_err_t os_pin_irq_enable(os_base_t pin, uint32_t enabled)
{
int pin_index = pin / 1000;
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return OS_INVAL;
}
struct os_device_pin *ppin = &gs_pin_table[pin_index];
if (ppin->ops->pin_irq_enable)
{
return ppin->ops->pin_irq_enable((struct os_device *)ppin, pin - ppin->base, enabled);
}
return OS_NOSYS;
}分步详解:
第 1 步:换算芯片索引(无条件执行)
int pin_index = pin / 1000;详细解释:pin / 1000 得到芯片索引。
第 2 步:芯片索引越界检查(条件 pin_index >= OS_PIN_MAX_CHIP)
if (pin_index >= OS_PIN_MAX_CHIP)
{
os_kprintf("invalide pin %d.\r\n", pin);
return OS_INVAL;
}详细解释:越界返回 OS_INVAL——即 pin_index 超出 gs_pin_table[OS_PIN_MAX_CHIP] 合法下标 0 ~ OS_PIN_MAX_CHIP-1,拦截越界内存访问(原理详见 1.2 节"芯片索引越界检查")。
第 3 步:取设备并检查底层是否实现使能函数(条件 ppin->ops->pin_irq_enable 非空)
struct os_device_pin *ppin = &gs_pin_table[pin_index];
if (ppin->ops->pin_irq_enable)
{
return ppin->ops->pin_irq_enable((struct os_device *)ppin, pin - ppin->base, enabled);
}
return OS_NOSYS;详细解释:ops->pin_irq_enable 非空则调用(传 pin - base 和 enabled),返回其结果;为空返回 OS_NOSYS。
os_pin_irq_enable() 条件走向总结:
os_pin_irq_enable() 条件走向总结:
├── pin_index = pin / 1000
├── pin_index >= OS_PIN_MAX_CHIP → 返回 OS_INVAL
├── ops->pin_irq_enable 非空 → 返回 ops->pin_irq_enable(dev, pin - base, enabled)
└── ops->pin_irq_enable 为空 → 返回 OS_NOSYS4.8 三个中断 API 的统一模式
细心的读者会发现 os_pin_attach_irq、os_pin_detach_irq、os_pin_irq_enable 三个函数结构完全一致,都是"算索引 → 查越界 → 查 ops 函数是否存在 → 转发或返回 OS_NOSYS"。可用一张表总结:
| API | 算索引 | 越界返回 | ops 字段 | 转发参数 |
|---|---|---|---|---|
os_pin_attach_irq | pin/1000 | OS_INVAL | pin_attach_irq | (dev, pin-base, mode, hdr, args) |
os_pin_detach_irq | pin/1000 | OS_INVAL | pin_detach_irq | (dev, pin-base) |
os_pin_irq_enable | pin/1000 | OS_INVAL | pin_irq_enable | (dev, pin-base, enabled) |
关键理解:
- 三者都不直接操作硬件,而是"查表 + 转发":只要 BSP 没有实现对应 ops 函数,就返回
OS_NOSYS。 - 真正的中断行为(EXTI 配置、NVIC 使能)都在 BSP 层的 ops 函数里完成。
五、raw 接口与框架接口的差异
OneOS 的 PIN 设备提供两组功能相同的接口:
| 接口 | 函数 | 说明 |
|---|---|---|
| 标准接口 | os_pin_mode / os_pin_write / os_pin_read | OS_WEAK 弱函数,默认转发到 raw 版本,可被强符号覆盖 |
| raw 接口 | raw_os_pin_mode / raw_os_pin_write / raw_os_pin_read | 直接查 gs_pin_table 并调用 ops->xxx 的底层实现 |
关键理解:
- 默认情况下两者行为完全一致(标准接口就是 raw 接口的薄封装)。
- 差异在于可覆盖性:
os_pin_*是OS_WEAK,允许应用/中间件重新定义强符号实现"钩子"(例如统一加锁、统计调用次数);raw_os_pin_*不可覆盖。 - 命名约定:
raw前缀表示"绕过封装、直达底层"。启动早期设备框架尚未就绪、或需要极致轻量访问时,可直接用 raw 接口。
注意:当前
pin.c的实现里,raw 接口本身也要依赖gs_pin_table(即设备已注册)。所谓"绕过设备框架"是指跳过os_device_*的设备操作表封装,但仍需os_device_pin_register已执行。
六、使用示例
以下示例均假设引脚编号使用"完整编号"(含千位基址)。若你的平台只有一片芯片(基址 0),
1013可直接写成13。为清晰起见,示例用1000 + 引脚号的形式。
示例 1:LED 闪烁(推挽输出)
#include <pin.h>
#include <os_task.h>
#define LED_PIN (1000 + 13) /* 假设 LED 连接在基址1000的第13号引脚 */
static void led_blink_task(void *arg)
{
/* 1. 设置引脚为推挽输出模式 */
os_pin_mode(LED_PIN, PIN_MODE_OUTPUT);
/* 2. 循环翻转电平,实现 LED 闪烁 */
while (1)
{
os_pin_write(LED_PIN, PIN_HIGH); /* 点亮 */
os_task_msleep(500);
os_pin_write(LED_PIN, PIN_LOW); /* 熄灭 */
os_task_msleep(500);
}
}说明:实际运行时,需要先创建并启动
led_blink_task任务。效果:LED 以 1Hz 频率(亮 500ms、灭 500ms)闪烁。
示例 2:按键输入检测(上拉输入 + 消抖)
#include <pin.h>
#include <os_task.h>
#define KEY_PIN (1000 + 5) /* 按键接在基址1000的第5号引脚,上拉输入 */
static void key_scan_task(void *arg)
{
int key_state;
int last_state = PIN_HIGH; /* 上拉输入,未按下时默认高电平 */
/* 1. 设置引脚为上拉输入模式 */
os_pin_mode(KEY_PIN, PIN_MODE_INPUT_PULLUP);
while (1)
{
/* 2. 读取引脚电平 */
key_state = os_pin_read(KEY_PIN);
/* 3. 检测下降沿(按键按下) */
if (last_state == PIN_HIGH && key_state == PIN_LOW)
{
os_kprintf("Key pressed!\r\n");
}
/* 4. 检测上升沿(按键释放) */
else if (last_state == PIN_LOW && key_state == PIN_HIGH)
{
os_kprintf("Key released!\r\n");
}
last_state = key_state;
os_task_msleep(20); /* 5. 20ms 轮询间隔,配合简单消抖 */
}
}说明:上拉输入下,按键未按下读到
PIN_HIGH,按下接地读到PIN_LOW。通过记录上次电平并检测"高→低"(按下)/“低→高”(释放)沿来触发打印。实际运行会交替打印Key pressed!/Key released!。
示例 3:外部中断处理(下降沿触发)
#include <pin.h>
#include <os_task.h>
#define IRQ_PIN (1000 + 6) /* 中断引脚 */
/* 中断回调函数(在中断上下文执行,注意保持精简) */
static void pin_irq_callback(void *args)
{
uint32_t *count = (uint32_t *)args;
(*count)++;
os_kprintf("Interrupt triggered! Count: %d\r\n", *count);
}
static void exti_demo_task(void *arg)
{
static uint32_t irq_count = 0;
os_err_t ret;
/* 1. 设置引脚为上拉输入模式 */
os_pin_mode(IRQ_PIN, PIN_MODE_INPUT_PULLUP);
/* 2. 绑定中断回调:下降沿触发 */
ret = os_pin_attach_irq(IRQ_PIN, PIN_IRQ_MODE_FALLING, pin_irq_callback, &irq_count);
if (ret != OS_SUCCESS)
{
os_kprintf("attach irq failed, ret=%d\r\n", ret);
return;
}
/* 3. 使能中断 */
ret = os_pin_irq_enable(IRQ_PIN, PIN_IRQ_ENABLE);
if (ret != OS_SUCCESS)
{
os_kprintf("enable irq failed, ret=%d\r\n", ret);
return;
}
os_kprintf("External interrupt demo started. Waiting for trigger...\r\n");
/* 4. 主循环中做其他事情,中断回调异步更新 irq_count */
while (1)
{
os_task_msleep(1000);
os_kprintf("Main loop running, irq_count: %d\r\n", irq_count);
}
}说明:引脚
1006每出现一次下降沿(高→低跳变),硬件触发中断,框架调用pin_irq_callback使irq_count++并打印。主任务每 1 秒打印一次当前计数。实际运行:每次按键按下会立即打印Interrupt triggered! Count: N。
示例 4:系统启动早期使用 raw 接口
#include <pin.h>
/* 在系统启动早期、需要最轻量访问时直接使用 raw 接口 */
void early_board_init(void)
{
/* 直接查表调用底层 ops,跳过 os_pin_* 弱封装 */
raw_os_pin_mode(1000 + 13, PIN_MODE_OUTPUT);
raw_os_pin_write(1000 + 13, PIN_HIGH);
raw_os_pin_mode(1000 + 5, PIN_MODE_INPUT_PULLUP);
}说明:示例展示了 raw 接口的用法。效果与标准接口相同,但调用的是不可覆盖的底层实现。
七、更换 MCU(PIN 驱动移植)—— 全局视角
本节从全局视角讲解:当你的项目要换一颗新的 MCU 时,OneOS 的 PIN 设备驱动要做什么才能在新平台上跑起来。核心结论是:应用层与框架层完全不用动,只需要新写/移植 BSP 层的引脚驱动。
在动手移植之前,先认识 BSP 层驱动文件 drv_gpio.c / drv_gpio.h 到底承担了什么职责——这是理解移植步骤的前提。
7.1 认识 BSP 层驱动:drv_gpio.c 与 drv_gpio.h
BSP 层的 GPIO/PIN 驱动通常由两个文件组成:
| 文件 | 作用 |
|---|---|
drv_gpio.h | 声明数据结构、引脚编号宏、操作函数原型(对外"契约") |
drv_gpio.c | 实现 struct os_pin_ops 六个函数 + 引脚索引表 + 初始化注册(真正操作硬件) |
以 STM32 为例(drivers/hal/st/drivers/),两者各承担什么:
(1)drv_gpio.h —— 定义"引脚如何编号"与"端口资源"
来源:drivers/hal/st/drivers/drv_gpio.h
enum GPIO_PORT_INDEX
{
GPIO_INDEX_A = 0,
GPIO_INDEX_B,
... /* C~Y 省略 */
GPIO_INDEX_Z,
GPIO_INDEX_MAX /* = 26 */
};
#define GPIO_PIN_PER_PORT (16)
#define GPIO_PORT_MAX (GPIO_INDEX_MAX) /* = 26 */
#define GPIO_PIN_MAX (GPIO_PORT_MAX * GPIO_PIN_PER_PORT)
#define __PORT_INDEX(pin) (pin / GPIO_PIN_PER_PORT) /* 取端口索引 */
#define __PIN_INDEX(pin) (pin % GPIO_PIN_PER_PORT) /* 取端口内引脚位 */
#define GET_PIN(PORTx, PIN) (((GPIO_INDEX_##PORTx - GPIO_INDEX_A) * GPIO_PIN_PER_PORT) + PIN)
#define PIN_OFFSET(__pin) (1 << ((__pin) % GPIO_PIN_PER_PORT))详解:
GPIO_PORT_INDEX枚举:给 A~Z 端口编号,GPIO_INDEX_MAX = 26(见本章"GPIO_PORT_MAX 来源")。GPIO_PIN_PER_PORT = 16:每个端口 16 个引脚。GET_PIN(PORTx, PIN)是 BSP 的"引脚号生成宏":把"端口字母 + 引脚位"换算成一个跨端口连续递增的编号。例如GET_PIN(A, 0) = (0-0)*16 + 0 = 0,GET_PIN(B, 0) = (1-0)*16 + 0 = 16。PIN_OFFSET:把编号换算成对应引脚的位掩码(1 << 引脚位),供 HAL 库使用。struct pin_index:记录每个引脚的端口基地址gpio_port与上下拉状态,是"编号 → 端口"的映射表。
(2)drv_gpio.c —— 实现六个 ops 函数 + 索引表 + 注册
drv_gpio.c 里做了三件大事:
- 定义引脚索引表(
indexs[]/pins[]),把每个引脚编号映射到具体端口。 - 实现
stm32_pin_mode/pin_write/pin_read/attach_irq/detach_irq/irq_enable六个函数(操作寄存器)。 - 写
os_hw_pin_init,使能端口时钟、初始化 GPIO,最后os_device_pin_register注册。
(3)两个文件的协作关系
drv_gpio.h(接口契约)
├─ 引脚编号宏:GET_PIN / PIN_OFFSET / __PORT_INDEX
├─ 端口资源宏:GPIO_PORT_MAX / GPIO_PIN_MAX
└─ 结构体:struct pin_index / pin_pull_state / pin_irq_map
│ 被引用
▼
drv_gpio.c(具体实现)
├─ indexs[] / pins[] —— 按 GET_PIN 规则填充引脚映射
├─ get_pin_index(pin) —— 查表
├─ stm32_pin_xxx() —— 实现六个 ops
├─ _stm32_pin_ops —— 聚合六个函数
└─ os_hw_pin_init() —— 时钟 + 注册(4)不同 MCU 的同类文件
项目 drivers/hal/ 下每个 MCU 厂商都有一份类似驱动,它们的文件名、结构完全一致(都实现同一个 struct os_pin_ops 契约),只是内部换成各自的寄存器/HAL:
| 厂商 | 路径 | 区别 |
|---|---|---|
| ST | drivers/hal/st/drivers/drv_gpio.c | 用 STM32 HAL(HAL_GPIO_*) |
| GD | drivers/hal/gd/drivers/f403/drv_gpio.c | 用 GD32 库(gpio_*/exti_*),引脚表用 __GD32_PIN(index,port,pin) 宏 |
关键理解:这些驱动文件是并列的"平级实现",不是"多片 GPIO"。它们的存在意义是:同一套应用代码,通过换一个 BSP 驱动文件,就能跑在不同 MCU 上——这正是移植/跨平台的核心。
(5)"根据所选 MCU 决定"到底是怎么决定的?—— 构建系统的三层选择机制
这是理解 OneOS 跨平台编译的关键。项目不是把 drivers/hal/ 下所有 drv_gpio.c 都编译进去,而是通过 Kconfig 配置 + weave.yaml 条件编译,只编译当前所选 MCU 的那一份。整个选择由三层机制完成:
第一层:选择"厂商宏"(决定"用哪家")
在 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 ? {...} 做条件编译。以 ST 为例(drivers/hal/st/drivers/weave.yaml):
group_name: hal/drivers
# 依赖宏控:只有定义了 MANUFACTOR_STM32 才编译整个组
depend_macro:
- MANUFACTOR_STM32
...
source_file:
- drv_gpio.c ? {is_define(['OS_USING_PIN', 'HAL_GPIO_MODULE_ENABLED'])}其中 drv_gpio.c ? {is_define(['OS_USING_PIN', 'HAL_GPIO_MODULE_ENABLED'])} 的意思是:只有当同时定义了 OS_USING_PIN(使能 PIN 设备组件)和 HAL_GPIO_MODULE_ENABLED(使能 GPIO 硬件模块)时,才编译 drv_gpio.c。
完整链路图(以"选 STM32 + 开 PIN 组件"为例):
① 配置阶段
templates/configs/Kconfig
选择 MANUFACTOR_STM32 → 定义宏 MANUFACTOR_STM32
② 目录选择
SConscript: inq_table['MANUFACTOR_STM32'] = 'st' → SRC_HAL = st
drivers/hal/Kconfig: source ".../hal/$(SRC_HAL)/Kconfig" → 加载 hal/st/Kconfig
③ 文件级条件编译
drivers/hal/st/drivers/weave.yaml
depend_macro: MANUFACTOR_STM32 (组级:必须选 STM32)
drv_gpio.c ? {is_define(['OS_USING_PIN',
'HAL_GPIO_MODULE_ENABLED'])} (文件级:还要开 PIN + GPIO)
↓ 条件都满足才编译
drv_gpio.c → 链接进固件结论:所以"根据所选 MCU 决定"的本质是——选 MCU → 定义厂商宏 → 映射到 SRC_HAL 目录 → 该目录下 depend_macro 命中 → 具体文件满足 is_define 才编译。换了 MCU 厂商,厂商宏变了,SRC_HAL 变了,编译的 drv_gpio.c 自然就换成新厂商的那一份。
7.2 移植前的全局视角:分层全景图
先回顾 PIN 设备的完整分层(本书 2.2.1、2.3、2.4 节已分别详解):
┌─────────────────────────────────────────────────────────────┐
│ 【应用层】 用户代码(led_blink / key_scan / exti_demo ...) │
│ 用 os_pin_mode / os_pin_read / os_pin_attach_irq / raw 系列 │
└──────────────────────────────┬──────────────────────────────┘
│ 调用标准 API(编号带千位基址)
▼
┌─────────────────────────────────────────────────────────────┐
│ 【框架层】 drivers/pin/pin.c(与 MCU 无关,通用代码) │
│ gs_pin_table / os_device_pin_register / │
│ os_pin_* / raw_os_pin_* / 中断 API / _pin_read 等 │
│ 只做:编号换算 + 越界检查 + 函数指针跳转 │
└──────────────────────────────┬──────────────────────────────┘
│ 调用 pin->ops->xxx
▼
┌─────────────────────────────────────────────────────────────┐
│ 【BSP 层】 drivers/hal/xxx/drivers/drv_gpio.c(需按 MCU 实现)│
│ 实现 struct os_pin_ops 六个函数 + 引脚索引表 indexs[] │
│ 真正操作寄存器(如 HAL_GPIO_ReadPin / 写 ODR/IDR) │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 【硬件层】 具体 MCU 的 GPIO 外设寄存器 │
└─────────────────────────────────────────────────────────────┘移植的关键洞察:
- 只有 BSP 层和 MCU 强相关(依赖具体寄存器、HAL 库、引脚分配)。
- 框架层和应用层与 MCU 无关(只依赖
pin.h定义的抽象接口)。 - 所以"换 MCU"本质上 = 重写/移植 BSP 层的
drv_gpio.c。
7.3 移植需要提供的"接口契约"
要移植,必须先满足 BSP 层与框架层之间的契约——也就是 struct os_pin_ops 的六个函数(见 2.2 节)必须全部实现,并映射到新 MCU 的硬件能力:
| ops 函数 | 需实现的能力 | 新 MCU 上对应什么 |
|---|---|---|
pin_mode | 配置引脚模式(输出/输入/上下拉/开漏) | 配置 GPIO 端口的 MODER/OTYPER/PUPDR 等寄存器 |
pin_write | 设置输出电平 | 写端口 ODR/BSRR 寄存器 |
pin_read | 读取输入电平 | 读端口 IDR 寄存器 |
pin_attach_irq | 绑定外部中断回调 | 配置 EXTI + NVIC |
pin_detach_irq | 解绑外部中断 | 清除 EXTI 配置 |
pin_irq_enable | 使能/禁用中断 | 开关 NVIC/EXTI 中断 |
此外还需要:
- 引脚索引表
indexs[]:把"芯片内偏移(0~N)"映射到"具体端口 + 引脚位"。例如 STM32 里indexs[13]描述 GPIOA 的第 13 位。 - 编号换算宏:
PIN_OFFSET(pin)(取引脚位)、端口索引宏。 - 初始化函数(如
os_hw_pin_init):配置时钟、GPIO 初始化,最后调用os_device_pin_register(pin_base, &xxx_pin_ops, NULL)。
7.4 移植的标准流程(5 步)
下面以"把 PIN 驱动从 STM32 移植到一颗新 MCU"为例,给出通用流程:
第 1 步:确定引脚编号映射表
在 drv_gpio.c 里定义 indexs[],把每个可用引脚(芯片内偏移)对应到新 MCU 的端口/引脚位。
/* 示例:偏移 0~15 对应 GPIOA 的 PA0~PA15 */
static const struct pin_index indexs[] =
{
{ __GPIOA, GPIO_PIN_0 },
{ __GPIOA, GPIO_PIN_1 },
/* ... 依此类推,直到覆盖全部可用引脚 ... */
};关键点:这张表决定了应用层 os_pin_read(1013) 里的 13 最终对应哪根物理引脚。表越完整,可用引脚越多。
第 2 步:实现六个 ops 函数
逐个实现 pin_mode/pin_write/pin_read/pin_attach_irq/pin_detach_irq/pin_irq_enable,内部调用新 MCU 的寄存器操作或 HAL 库函数(参考 2.2.1 的 stm32_pin_read 写法)。
static int my_pin_read(struct os_device *dev, os_base_t pin)
{
const struct pin_index *index;
index = get_pin_index(pin);
if (index == OS_NULL)
return PIN_LOW;
return HAL_GPIO_ReadPin(index->gpio_port, PIN_OFFSET(pin)); /* 换成本 MCU 的读法 */
}第 3 步:定义并导出 ops 操作表
把六个函数聚合到 struct os_pin_ops,供框架引用:
const static struct os_pin_ops _my_pin_ops = {
.pin_mode = my_pin_mode,
.pin_write = my_pin_write,
.pin_read = my_pin_read,
.pin_attach_irq = my_pin_attach_irq,
.pin_detach_irq = my_pin_dettach_irq,
.pin_irq_enable = my_pin_irq_enable,
};第 4 步:写初始化函数并注册
在板级初始化里写 os_hw_pin_init,配置时钟和 GPIO,最后注册:
int os_hw_pin_init(void)
{
/* 1. 使能 GPIO 时钟、配置引脚 */
...
/* 2. 注册 PIN 设备:基址 0,ops 表,用户数据 NULL */
return os_device_pin_register(0, &_my_pin_ops, OS_NULL);
}
INIT_BOARD_EXPORT(os_hw_pin_init); /* 板级初始化阶段自动调用 */关键点:
INIT_BOARD_EXPORT(或平台对应的自动初始化宏)保证注册在应用使用前完成。- 基址用
0(单芯片);若板上有第二片 GPIO 控制器,用1000再注册一个。
拓展:能注册多个 GPIO 吗?——区分"同 MCU 多端口"与"多片独立 GPIO"
这是移植时最易混淆的问题。答案是:可以注册多个 os_device_pin_register,但绝大多数情况根本不需要。关键在于区分"多端口"和"多芯片"是两个不同概念。
情况 A:同一颗 MCU 的多个端口(A/B/C...)—— 只需要一次注册
STM32/GD 等 MCU 往往有 GPIOA~GPIOZ 多个端口,但它们共用同一个 ops 表、同一份驱动。靠的是跨端口连续编号(见 7.1 节 GET_PIN):
| 引脚宏 | 计算结果 | 芯片内偏移 |
|---|---|---|
GET_PIN(A, 0) | (0-0)*16 + 0 = 0 | 0 |
GET_PIN(A, 15) | (0-0)*16 + 15 = 15 | 15 |
GET_PIN(B, 0) | (1-0)*16 + 0 = 16 | 16 |
STM32/GD 的 os_hw_pin_init 只调用一次 os_device_pin_register(0, &xxx_pin_ops, OS_NULL)(见 2.2.1、7.1 节),因为编号 0~N 已经能唯一区分所有端口的所有引脚,底层 get_pin_index 查 indexs[] 就能定位到端口。无需为每个端口单独注册。
情况 B:多片独立的 GPIO 控制器芯片 —— 才需要多次注册
只有当板上真的有多片独立的 GPIO 芯片(例如 GPIO 扩展器 I2C/SPI 扩展 IO,或第二颗 MCU 的 GPIO)时,才需要为第二片用基址 1000 再次注册:
int board_pin_init(void)
{
/* 第一片(主 MCU 内建):基址 0 */
os_device_pin_register(0, &_mcu_pin_ops, OS_NULL);
/* 第二片(外部 GPIO 扩展芯片):基址 1000 */
os_device_pin_register(1000, &_expander_pin_ops, OS_NULL);
return 0;
}注册后,第二片芯片的引脚编号就是 1000 + N(如 1003 表示扩展芯片的第 3 号引脚)。
多次注册的前提条件(重要,容易踩坑):
OS_PIN_MAX_CHIP必须调大:默认OS_PIN_MAX_CHIP = 1(见 1.2 节),os_device_pin_register只在pin_index < OS_PIN_MAX_CHIP时成功(见 4.1 节)。要注册第二片(pin_index = 1),必须在配置里把OS_PIN_MAX_CHIP改为≥ 2,否则注册1000基址会失败。- 第二片也要实现自己的一套
struct os_pin_ops:扩展芯片的操作函数(读写、中断)与主 MCU 不同,需单独实现并聚合到一个新的 ops 表。 - 每片各自维护引脚索引表:
get_pin_index要能区分"这是哪片的引脚",通常两片驱动各自查自己的表。
项目里为什么有那么多 drv_gpio.c?—— 那是"多个厂商驱动",不是"多片 GPIO"
你提到项目里有很多类似的 drv_gpio.c(如 ST 的 drivers/hal/st/drivers/drv_gpio.c、GD 的 drivers/hal/gd/drivers/f403/drv_gpio.c)。请务必区分:
| 概念 | 含义 | 数量关系 |
|---|---|---|
| 多个厂商 BSP 驱动 | ST/GD/其他 MCU 各有一份 drv_gpio.c,实现同一套 struct os_pin_ops 契约 | 每个工程只编译其中一个(按所选 MCU) |
| 多片 GPIO 芯片 | 同一工程里多片独立的 GPIO 控制器 | 才需要多次 os_device_pin_register |
这些 drv_gpio.c 是并列的"平级实现",为不同硬件提供同一接口(见 7.1 节)。它们的"多"是为了跨平台复用,而不是一块板子上装了很多 GPIO。
第 5 步:验证
移植完成后,用本节"示例 1(LED 闪烁)"跑通即可验证:
os_pin_mode(0 + 13, PIN_MODE_OUTPUT); /* 若基址 0,可简写为 13 */
os_pin_write(0 + 13, PIN_HIGH); /* LED 应点亮 */- LED 能亮/灭 →
pin_mode+pin_write正确。 - 读按键电平正确 →
pin_read+pin_mode输入配置正确。 - 中断回调被触发 →
pin_attach_irq+pin_irq_enable正确。
7.5 芯片未适配时该怎么办?(新建 BSP 还是补型号)
前面的移植流程假设"同厂商系列、寄存器结构兼容"。但更常见的问题是:你手里的芯片在 drivers/hal/ 里根本没有对应的 drv_gpio.c,甚至整个厂商目录都没有。这时要分两种情况处理。
7.5.1 判断标准:已有厂商 还是 全新厂商
| 情况 | 判断标准 | 工作量大不大 |
|---|---|---|
| 情况 A:已有厂商 | 芯片厂商在 drivers/hal/ 已有目录(如 st/),只是你这款具体型号没列出来 | 小,通常不改 GPIO 驱动 |
| 情况 B:全新厂商 | 芯片厂商在 drivers/hal/ 完全没有目录 | 大,需整套新建 BSP + 移植 PIN 驱动 |
7.5.2 情况 A:已有厂商,只是具体型号没适配
同厂商同系列 MCU 的 GPIO 寄存器结构高度一致,drv_gpio.c 里的 struct os_pin_ops 实现通常直接复用。所以此时你大概率不用改 GPIO 驱动,只需:
第 1 步:确认 drv_gpio.c 是否覆盖你的引脚能力
- 看
drv_gpio.h里的GPIO_PORT_MAX/ 引脚索引表是否包含你型号的端口(如是否有 GPIOG/GPIOH)。 - 若你的型号端口更多,只需扩展
indexs[](见 7.4 第 1 步)或调整GPIO_PORT_MAX。
第 2 步:在厂商 Kconfig 里加"芯片型号"选项
- 在对应系列(如
drivers/hal/st/soc/...)的 Kconfig 里新增你的型号,让它能select正确的时钟树、外设、HAL_xxx_MODULE_ENABLED配置。
第 3 步:用厂商工具生成外设配置
- 用 STM32CubeMX(或厂商对应工具)生成
peripherals.c(引脚复用、时钟树、外设初始化)。 - 项目
devices/stm32_devices.c通过#include "peripherals.c"把它纳入编译,其内的os_hw_xxx_init会注册 HAL 设备。
第 4 步:写板级文件并注册
- 写
board.c/board.h做板级时钟初始化,在初始化中调用os_hw_pin_init(内部os_device_pin_register,见 2.2.1)。
第 5 步:验证(同 7.4 第 5 步)
要点:情况 A 的核心是"型号配置 + 引脚复用",GPIO 驱动代码基本不用动。真正的工作量在时钟树和板级外设,而非 PIN 驱动本身。
7.5.3 情况 B:全新厂商,需要从零新建 BSP
这才是真正需要做"移植 PIN 驱动"的情况,工作量较大。步骤与本 7.4 的流程 + 7.1 的三层选择机制结合:
第 1 步:新建厂商目录
在 drivers/hal/ 下新建目录,如 drivers/hal/my_chip/,至少包含:
drivers/hal/my_chip/
├── drv_gpio.c / drv_gpio.h # 实现 struct os_pin_ops 六个函数(见 7.1/7.4)
├── Kconfig # 该厂商的 SOC/外设配置
├── weave.yaml # 条件编译声明(depend_macro)
├── board.c / board.h # 板级时钟、启动
└── 启动文件 / 链接脚本 # 新 MCU 的启动汇编、ld 脚本第 2 步:按 7.4 的 5 步流程实现 PIN 驱动
- 定义引脚索引表(7.4 第 1 步)
- 实现六个 ops 函数(7.4 第 2 步)
- 定义 ops 表(7.4 第 3 步)
- 写
os_hw_pin_init+os_device_pin_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_gpio.c ? {is_define(['OS_USING_PIN', 'HAL_GPIO_MODULE_ENABLED'])}这样选中 MANUFACTOR_MYCHIP → SRC_HAL = my_chip → drivers/hal/Kconfig 加载新目录 → weave.yaml 命中 → 编译你的 drv_gpio.c。
第 4 步:验证(同 7.4 第 5 步)
要点:情况 B 的工作量本质 = 新建一套 BSP 目录 + 实现 PIN 驱动 + 接入厂商宏与
SRC_HAL。框架层和应用层依然不用改。
7.5.4 两种情况的对比速查
| 维度 | 情况 A(已有厂商补型号) | 情况 B(全新厂商新建 BSP) |
|---|---|---|
| 是否新建目录 | 否,复用厂商目录 | 是,新建 drivers/hal/my_chip/ |
是否改 drv_gpio.c | 通常不改(或扩索引表) | 从零实现六个 ops |
是否改厂商宏/SRC_HAL | 否 | 是(加 MANUFACTOR_XXX + 映射) |
是否改 OS_PIN_MAX_CHIP | 一般不改 | 多片时需调大 |
| 工作量 | 小(型号配置 + 引脚复用) | 大(全套 BSP + 驱动移植) |
7.6 移植检查清单(快速自查)
| 检查项 | 是否满足 |
|---|---|
struct os_pin_ops 六个函数全部实现并绑定到 ops 表 | ☐ |
indexs[] 覆盖了所有会用到的引脚 | ☐ |
os_device_pin_register 的基址与 PIN_BASE/pin/1000 换算一致 | ☐ |
| 初始化函数在应用使用前执行(自动初始化宏) | ☐ |
| 外部中断需正确配置 EXTI + NVIC(若用到中断) | ☐ |
| 电平触发模式是否被新 MCU 支持(部分平台只支持边沿) | ☐ |
7.7 移植的价值与注意事项
价值:一旦 BSP 层移植完成,应用层代码一行不改即可在多个 MCU 间复用。这是 OneOS 设备模型(应用/框架/BSP 三层解耦)带来的最大好处。
注意事项:
- 引脚编号体系不可变:完整编号 = 基址 + 偏移,
pin/1000算芯片索引、pin - base算真正引脚号,是框架层硬编码约定,BSP 必须遵守。 - 越界检查依赖
OS_PIN_MAX_CHIP:若板上有多片 GPIO 控制器,需在配置里调大OS_PIN_MAX_CHIP,否则注册1000/2000基址会被拒绝(见 4.1 节)。 - 中断能力差异:新 MCU 若不支持电平触发,应用层用了
PIN_IRQ_MODE_HIGH_LEVEL会失败(返回OS_FAILURE),移植时需注意。
📝 本节小结
核心要点回顾
本节围绕 OneOS 的 GPIO 设备(PIN 设备)展开,核心要点如下:
1. PIN 设备的分层结构
os_device_pin_t内嵌os_device(parent),是"带了引脚扩展的普通设备"。- 两套操作表:
parent.ops(设备框架pin_ops,含_pin_read/_pin_write/_pin_control)与ops(引脚专用struct os_pin_ops,六个引脚操作函数)。 - 设备表
gs_pin_table[]全局存储,按pin_base / 1000索引。
2. 引脚编号体系
- 完整编号 = 千位基址 + 芯片内偏移;
PIN_BASE(pin)或pin/1000得芯片索引,pin - base得真正硬件引脚号。 os_device_pin_register要求pin_base是 1000 的整数倍。
3. 引脚模式宏
- 六种模式(输出/输入/上拉/下拉/开漏/禁用)+ 五种中断触发模式 + 电平 + 使能宏。
- 模式值是独立枚举值,不是可叠加的位掩码。
4. 核心 API
- 注册:
os_device_pin_register。 - 读写模式:
os_pin_mode/os_pin_write/os_pin_read(OS_WEAK,默认转发 raw)。 - 中断:
os_pin_attach_irq/os_pin_detach_irq/os_pin_irq_enable(统一"算索引→查越界→查 ops→转发"模式)。
5. raw 与标准接口
os_pin_*是OS_WEAK可覆盖封装;raw_os_pin_*直达底层、不可覆盖。
6. 更换 MCU 移植(见第七章)
- 只有 BSP 层与 MCU 相关,框架层与应用层完全不动。
- 移植核心 = 实现
struct os_pin_ops六个函数 + 引脚索引表indexs[]+ 初始化注册(os_device_pin_register)。 - 换 MCU 后应用层代码零改动即可复用,这是设备模型三层解耦的最大价值。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 用位掩码叠加模式(如 `PIN_MODE_INPUT | 上拉`) |
| 引脚号不带千位基址(多芯片场景) | 用完整编号(如 1000 + 13),pin/1000 才能正确算芯片索引 |
注册时 pin_base 不是 1000 的整数倍 | 必须用 0/1000/2000...,否则 os_device_pin_register 返回 -1 |
| 中断顺序颠倒(先 enable 再 attach) | 必须先 os_pin_attach_irq 绑定回调,再 os_pin_irq_enable 使能 |
| 未检查中断 API 返回值 | attach/enable 可能返回 OS_NOSYS(平台不支持)或 OS_BUSY(占用),务必检查 |
| 在中断回调里做耗时操作 | 中断回调应精简(置标志、计数),耗时处理放到任务上下文 |
混淆 parent.ops 与 ops | parent.ops 是设备框架表,ops 是引脚专用表,用途不同 |
误以为 os_pin_read 越界也静默 | raw_os_pin_read 越界会返回 OS_INVAL,os_pin_mode/write 越界才静默返回 |
| 依赖电平触发但平台不支持 | 部分 BSP(如 STM32)只支持边沿触发,电平模式会失败,先查驱动 |
移植时漏改 OS_PIN_MAX_CHIP | 多片 GPIO 控制器需在配置里调大 OS_PIN_MAX_CHIP,否则注册 1000/2000 基址会被拒绝 |
移植后 indexs[] 表不完整 | 应用层用了表里没有的引脚会查表失败(返回 PIN_LOW 或空),务必覆盖所有会用到的引脚 |
芯片未适配就盲目改 drv_gpio.c | 先判断"已有厂商补型号"还是"全新厂商新建 BSP"(见 7.5),同厂商通常只需补型号、不用改 GPIO 驱动 |
下一步
掌握了 PIN 设备后,接下来学习总线类设备:
- 5.3 I2C设备:了解 OneOS 如何通过 I2C 总线驱动挂接多个从设备,掌握
os_i2c_*读写与注册流程。