引脚复用冲突
芯片的引脚资源有限,一个引脚往往对应多种功能。如果多个驱动直接操作寄存器配置功能,很容易出现"多个驱动抢占同一引脚"的冲突,且没有任何机制来检测和阻止这种冲突。
例如,一个引脚可能同时支持以下功能:
假设芯片上 GPIO2_PB4 这个引脚可以同时复用为普通 GPIO 或 I2C1 的 SDA 信号。在没有 Pinctrl 子系统的时代,两个驱动各自直接操作寄存器:
/* ========== 驱动A:某个LED控制驱动 ========== */
staticintmy_led_probe(struct platform_device *pdev)
{
/* 硬编码:直接把 GPIO2_PB4 配置为 GPIO 输出模式 */
/* 假设 IOMUX 寄存器基地址为 0x020E0000 */
/* GPIO2_PB4 对应的 MUX 寄存器偏移为 0x00DC */
void __iomem *mux_reg = ioremap(0x020E0000 + 0x00DC, 4);
/* 写入 0x5 —— ALT5 模式,即 GPIO 功能 */
writel(0x5, mux_reg);
/* 然后把这个引脚当 GPIO 用,控制 LED 闪烁 */
/* ... */
return0;
}
/* ========== 驱动B:I2C1 总线驱动 ========== */
staticintmy_i2c_probe(struct platform_device *pdev)
{
/* 硬编码:把同一个 GPIO2_PB4 配置为 I2C1_SDA 功能 */
void __iomem *mux_reg = ioremap(0x020E0000 + 0x00DC, 4);
/* 写入 0x0 —— ALT0 模式,即 I2C1_SDA 功能 */
writel(0x0, mux_reg);
/* 然后初始化 I2C 控制器,开始通信 */
/* ... */
return0;
}
问题所在:两个驱动都直接操作了同一个寄存器地址,后加载的驱动会静默覆盖先加载驱动的配置,且没有任何报错或警告。如果 I2C 驱动后加载,LED 驱动就会莫名其妙地失效——因为它的引脚已经被悄悄切换成了 I2C 功能。没有任何机制能检测到这种冲突。
配置繁琐且易出错
引脚配置不仅要选择"功能",还要设置电气属性:上拉/下拉电阻、驱动能力(drive strength)、电平标准(LVCMOS3.3V/1.8V)、转换速率(slew rate)等。这些都需要操作复杂的寄存器,不同芯片的寄存器地址还不一样,移植性极差。
驱动与硬件严重耦合
驱动代码中充斥着寄存器地址和位操作,换一颗芯片就要重写大量代码,违反了 Linux 内核"硬件与软件分离"的设计哲学。
例如:配置一个引脚的完整电气属性,需要同时操作多个寄存器、计算多个位域:
/*
* 目标:将 GPIO2_PB4 配置为 I2C1_SDA,要求:
* - 复用功能:ALT0(I2C1_SDA)
* - 开漏输出
* - 47KΩ 上拉电阻
* - 驱动能力:4mA
* - 转换速率:100MHz
* - 电平标准:3.3V LVCMOS
*/
staticvoidconfigure_i2c_sda_pin(void)
{
void __iomem *base = ioremap(0x020E0000, 0x1000);
/* ---- 第一步:配置 MUX 寄存器(选择功能) ---- */
/*
* 寄存器偏移 0x00DC,位域定义:
* [31:20] 保留
* [19:16] SION(软件输入使能)
* [15:4] 保留
* [3:0] MUX_MODE(复用模式选择)
*
* ALT0 = 0x0 → I2C1_SDA
*/
writel(0x00000000, base + 0x00DC);
/* ---- 第二步:配置 PAD 寄存器(电气属性) ---- */
/*
* 寄存器偏移 0x0328,位域定义:
* [31:18] 保留
* [17] HYS(施密特触发器):1=使能
* [16] PUS[1](上拉/下拉选择)
* [15:14] PUS[0] + PUE(上拉/下拉使能)
* [13:12] PKE(保持器使能)
* [11:7] ODE(开漏)+ DSE(驱动强度)
* [6:3] SPEED(转换速率)
* [2:0] 保留
*
* 计算目标值:
* HYS=1 → bit17 = 1
* 47K上拉 → PUS=10, PUE=1 → bits[16:14] = 110
* PKE=1 → bits[13:12] = 01
* ODE=1 → bit11 = 1(开漏)
* DSE=010 → bits[5:3] = 010(4mA)
* SPEED=10 → bits[6:3] = 10(100MHz)
*
* 拼出来:0x4001B8B0
*
* 请问:你能一眼看出 0x4001B8B0 每个位代表什么吗?
*/
writel(0x4001B8B0, base + 0x0328);
/* ---- 第三步:如果该引脚是输入,还要配置输入选择寄存器 ---- */
/*
* 寄存器偏移 0x0618
* 用于选择信号从哪个物理引脚进入内部逻辑
*/
writel(0x1, base + 0x0618);
iounmap(base);
}
对比:有了 Pinctrl 之后同样的配置,在设备树中只需要这样写:
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6UL_PAD_I2C1_SDA__I2C1_SDA 0x4001b8b0
>;
};

1. 引脚复用 (Pin Multiplexing / Pinmux)
2. 电气属性配置 (Pin Configuration / Pinconf)
3. 动态状态管理 (State Management) pinctrl 支持将一组引脚打包定义为不同的“状态”(State):
此外还有两个重要概念:
例如:
但实际使用中,有些设计只用 TX/RX(两线制),有些需要硬件流控(四线制)。这就产生了两种不同的引脚组合需求.
在芯片厂商的 pinctrl 驱动代码中(如 pinctrl-imx6ul.c),这些数据被定义为静态表:
/* ===== 第一层:Pin 定义 ===== */
/* 每个物理引脚的编号和名称 */
staticconststructpinctrl_pin_descimx6ul_pins[] = {
...
IMX_PIN(18, "GPIO1_IO18"), /* 编号 18 */
IMX_PIN(19, "GPIO1_IO19"), /* 编号 19 */
IMX_PIN(20, "GPIO1_IO20"), /* 编号 20 */
IMX_PIN(21, "GPIO1_IO21"), /* 编号 21 */
...
};
/* ===== 第二层:Pin Group 定义 ===== */
/* 引脚组1:基本通信(TX + RX) */
staticconstunsignedint uart1_tx_rx_pins[] = { 18, 19 };
/* 引脚组2:硬件流控(RTS + CTS) */
staticconstunsignedint uart1_rts_cts_pins[] = { 20, 21 };
/* 引脚组数组:UART1 有两个 group */
staticconstchar * const uart1_groups[] = {
"uart1_tx_rx", /* group 0:基本通信 */
"uart1_rts_cts", /* group 1:硬件流控 */
};
/* ===== 第三层:Function 定义 ===== */
/* "uart1" 这个功能,关联上面两个 group */
staticconststructimx_pmx_funcimx6ul_pmx_funcs[] = {
...
{
.name = "uart1", /* 功能名 */
.groups = uart1_groups, /* 关联的 group 数组 */
.num_groups = ARRAY_SIZE(uart1_groups), /* 共 2 个 group */
},
...
};
关系映射图
Function: "uart1"
├── Group: "uart1_tx_rx" ← 基本通信场景
│ ├── Pin 18 (GPIO1_IO18) → TX
│ └── Pin 19 (GPIO1_IO19) → RX
│
└── Group: "uart1_rts_cts" ← 硬件流控场景(可选)
├── Pin 20 (GPIO1_IO20) → RTS
└── Pin 21 (GPIO1_IO21) → CTS
设备树中如何使用
/* 场景A:只用 TX/RX,不需要硬件流控 */
pinctrl_uart1_basic: uart1-basic {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1
MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1
>;
};
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1_basic>; /* 只引用基本 group */
status = "okay";
};
/* 场景B:需要硬件流控,两个 group 都要 */
pinctrl_uart1_full: uart1-full {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1
MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1
MX6UL_PAD_UART1_CTS_B__UART1_DCE_CTS 0x1b0b1
MX6UL_PAD_UART1_RTS_B__UART1_DCE_RTS 0x1b0b1
>;
};
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1_full>; /* 引用包含全部引脚的 group */
status = "okay";
};
Pinctrl 子系统采用经典的三层架构设计:

其底层驱动的核心数据结构以(pinctrl-rk3568.c)为例:
描述引脚控制器:pinctrl_desc + pinctrl_dev
// 描述一个引脚控制器的"能力说明书"
structpinctrl_desc {
constchar *name;
conststructpinctrl_pin_desc *pins;// 所有引脚的描述数组
unsignedint npins; // 引脚总数
conststructpinctrl_ops *pctlops;// 引脚组操作回调
conststructpinmux_ops *pmxops;// 复用操作回调
conststructpinconf_ops *confops;// 电气配置操作回调
};
// 内核运行时创建的控制器实例
structpinctrl_dev {
structlist_headnode;// 全局链表节点
structpinctrl_desc *desc;// 指向描述符
structradix_tree_rootpin_desc_tree;// 引脚描述符的基数树
structlist_headgpio_ranges;// GPIO 范围映射
structdevice *dev;
void *driver_data;
};
描述单个引脚:pinctrl_pin_desc
structpinctrl_pin_desc {
unsigned number; // 引脚编号,全局唯一
constchar *name; // 引脚名称(如 "GPIO2_PB4")
void *drv_data; // 驱动私有数据
};
描述三种操作回调:pinctrl_ops(引脚组操作):
structpinctrl_ops {
int (*get_groups_count)(struct pinctrl_dev *pctldev);
constchar *(*get_group_name)(struct pinctrl_dev *pctldev, unsigned selector);
int (*get_group_pins)(struct pinctrl_dev *pctldev, unsigned selector, ...);
int (*dt_node_to_map)(...); // 解析设备树节点为 pin map
void (*dt_free_map)(...); // 释放 pin map
};
pinmux_ops(复用操作):
structpinmux_ops {
int (*request)(struct pinctrl_dev *pctldev, unsigned offset); // 请求引脚
int (*free)(struct pinctrl_dev *pctldev, unsigned offset); // 释放引脚
int (*get_functions_count)(struct pinctrl_dev *pctldev);
constchar *(*get_function_name)(struct pinctrl_dev *pctldev, unsigned selector);
int (*get_function_groups)(struct pinctrl_dev *pctldev, unsigned selector, ...);
int (*set_mux)(struct pinctrl_dev *pctldev, unsigned func_selector, unsigned group_selector);
bool strict; // 是否禁止 GPIO 和其他功能同时使用同一引脚
};
pinconf_ops(电气配置操作):
structpinconf_ops {
int (*pin_config_get)(struct pinctrl_dev *pctldev, unsigned pin, unsignedlong *config);
int (*pin_config_set)(struct pinctrl_dev *pctldev, unsigned pin, unsignedlong *configs, unsigned num_configs);
int (*pin_config_group_get)(...);
int (*pin_config_group_set)(...);
};
描述外设的引脚状态:pinctrl + pinctrl_state + pinctrl_setting
// 每个使用 pinctrl 的外设设备都有一个 pinctrl 结构体
structpinctrl {
structlist_headnode;
structdevice *dev;
structlist_headstates;// 状态链表(default、sleep 等)
structpinctrl_state *state;// 当前激活的状态
};
// 一个状态(如 "default")
structpinctrl_state {
structlist_headnode;
constchar *name; // 状态名:"default"、"sleep"、"idle"
structlist_headsettings;// 该状态下的所有设置项
};
// 单个设置项(复用或配置)
structpinctrl_setting {
structlist_headnode;
enum pinctrl_map_type type; // MUX_GROUP 或 CONFIGS_PIN/GROUP
structpinctrl_dev *pctldev;// 由哪个控制器处理
union {
structpinctrl_setting_muxmux;// 复用设置
structpinctrl_setting_configsconfigs;// 电气配置
} data;
};
Pin Map:连接设备树与硬件的桥梁
Pin map 是 pinctrl 的核心数据结构,它将设备树中的配置信息转化为内核可操作的结构:
structpinctrl_map {
constchar *dev_name; // 使用该引脚的设备名
constchar *name; // 状态名(如 "default")
enum pinctrl_map_type type; // 类型:MUX_GROUP / CONFIGS_PIN / CONFIGS_GROUP
constchar *ctrl_dev_name; // 引脚控制器设备名
union {
structpinctrl_map_muxmux;// { group, function }
structpinctrl_map_configsconfigs;// { group_or_pin, configs[], num_configs }
} data;
};
先分清三大阵营
阵营A:控制器侧(芯片厂商定义,描述"我有哪些引脚、能做什么")
pinctrl_desc / pinctrl_dev / pinctrl_pin_desc / 三种 ops
阵营B:消费者侧(每个外设设备各一套,描述"我需要引脚怎么配")
pinctrl / pinctrl_state / pinctrl_setting
阵营C:桥梁(临时产物,把设备树翻译成阵营B能用的格式)
pinctrl_map
讲pinctrl 底层实现流程前需县了解两个probe
Linux 内核中,任何驱动的 probe 函数被调用,前提都是驱动(driver)和设备(device)匹配成功。匹配方式有多种,在嵌入式设备树系统中,最常见的就是 compatible 属性匹配:

在 pinctrl 的语境下,系统中实际存在两次独立的匹配和 probe 过程:
第一个 probe:pinctrl 控制器自身的 probe
设备树(通常在芯片级 dtsi 中):
┌─────────────────────────────────────────┐
│ iomuxc: pinctrl@20e0000 { │
│ compatible = "fsl,imx6ul-iomuxc"; │
│ reg = <0x020e00000x4000>; │
│ ... │
│ }; │
└─────────────────────────────────────────┘
│
│ compatible 匹配
▼
┌─────────────────────────────────────────┐
│ pinctrl-imx6ul.c 中的 probe: │
│ │
│ staticintimx6ul_pinctrl_probe(...) │
│ { │
│ // 1. ioremap 寄存器基地址 │
│ // 2. 填充 pinctrl_desc │
│ // 3. 填充引脚表、功能表 │
│ // 4. 调用 devm_pinctrl_register() │
│ // → 阵营A就位 │
│ } │
└─────────────────────────────────────────┘
这个 probe 的职责是把阵营A注册进内核,让 pinctrl 子系统知道"这颗芯片有哪些引脚、支持哪些复用功能"。它在系统启动早期就完成了。
第二个 probe:使用 pinctrl 的外设驱动的 probe
设备树(板级 dts 中):
┌─────────────────────────────────────────┐
│ &i2c1 { │
│ compatible = "fsl,imx6ul-i2c"; │
│ pinctrl-names = "default"; │
│ pinctrl-0 = <&pinctrl_i2c1>; │
│ }; │
└─────────────────────────────────────────┘
│
│ compatible 匹配
▼
┌─────────────────────────────────────────┐
│ really_probe() 被调用 │
│ │
│ ① pinctrl_bind_pins() ← 自动执行 │
│ 解析 pinctrl-0,配置引脚 │
│ 阵营C → 阵营B → 写寄存器 │
│ │
│ ② i2c_imx_probe() ← 驱动的 probe │
│ 此时引脚已配好,直接使用 │
└─────────────────────────────────────────┘
这个 probe 的职责是初始化外设本身(如初始化 I2C 控制器、注册 adapter 等)。而在它被调用之前,内核已经自动通过 pinctrl_bind_pins() 完成了引脚配置。
时间线
系统启动
│
├── ① pinctrl 控制器 probe(系统早期)
│ 匹配 "fsl,imx6ul-iomuxc"
│ → 注册 pinctrl_desc / pinctrl_dev
│ → 阵营A就位
│
├── ② I2C 驱动 probe(稍后)
│ 匹配 "fsl,imx6ul-i2c"
│ → 内核先自动调用 pinctrl_bind_pins()
│ → 解析设备树 pinctrl-0
│ → 生成 pinctrl_map(阵营C)
│ → 转换为 pinctrl_state/setting(阵营B)
│ → 调用 set_mux + pin_config_set 写寄存器
│ → 然后才调用 I2C 驱动的 probe()
│
└── ③ 其他外设驱动 probe(更后)
... 同样的流程 ...
了解完上面的两个probe后开始分析pinctrl底层实现流程第一步。
第一步:控制器注册(系统启动早期)
芯片厂商的 pinctrl 驱动 probe 时,把阵营A的数据注册进内核
芯片厂商在代码中静态定义好:
┌─────────────────────────────────────────────────────────┐
│ pinctrl_desc(能力说明书) │
│ ├── name = "pinctrl-imx6ul" │
│ ├── pins → pinctrl_pin_desc 数组 │
│ │ [0] { number=0, name="GPIO1_IO00" } │
│ │ [1] { number=1, name="GPIO1_IO01" } │
│ │ ... │
│ │ [41] { number=41, name="GPIO2_PB3" } ← I2C1_SCL │
│ │ [42] { number=42, name="GPIO2_PB4" } ← I2C1_SDA │
│ │ ... │
│ ├── npins = 142 │
│ ├── pctlops → pinctrl_ops(引脚组查询回调) │
│ ├── pmxops → pinmux_ops(复用操作回调) │
│ └── confops → pinconf_ops(电气配置回调) │
└─────────────────────────────────────────────────────────┘
│
│ 调用 devm_pinctrl_register(&desc)
▼
┌─────────────────────────────────────────────────────────┐
│ pinctrl_dev(运行时实例,核心层自动创建) │
│ ├── desc → 指向上面的 pinctrl_desc │
│ ├── pin_desc_tree → 把 142 个 pinctrl_pin_desc 建成索引 │
│ ├── gpio_ranges → 空(后续 GPIO 子系统来填充) │
│ └── node → 挂入全局 pinctrl_list 链表 │
└─────────────────────────────────────────────────────────┘
第二步:设备树解析(外设 probe 前)
当 I2C1 设备匹配到驱动后,内核在调用 probe 之前,先解析设备树中的 pinctrl-0:
设备树中的原始数据:
&i2c1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
};
pinctrl_i2c1: i2c1grp {
fsl,pins = <
0x00900x031C0x05880x20x00x4001b8b0
0x00940x03200x058C0x20x00x4001b8b0
>;
};
│
│ 核心层调用阵营A中 pinctrl_ops 的 dt_node_to_map() 回调
│ (这是芯片厂商实现的,不同芯片解析方式不同)
▼
┌─────────────────────────────────────────────────────────┐
│ pinctrl_map 数组(阵营C,翻译产物) │
│ │
│ map[0]: │
│ dev_name = "21a0000.i2c" ← 谁要用 │
│ name = "default" ← 什么状态下用 │
│ type = MUX_GROUP ← 这是复用配置 │
│ ctrl_dev_name = "pinctrl-imx6ul" ← 找哪个控制器 │
│ data.mux = { group="i2c1", function="i2c1" } │
│ │
│ map[1]: │
│ dev_name = "21a0000.i2c" │
│ name = "default" │
│ type = CONFIGS_GROUP ← 这是电气配置 │
│ ctrl_dev_name = "pinctrl-imx6ul" │
│ data.configs = { group="i2c1", configs={0x4001b8b0} }│
└─────────────────────────────────────────────────────────┘
第三步:转换为运行时状态结构
核心层将 pinctrl_map 进一步转化为阵营B的链表结构:
pinctrl_map(临时,翻译完即弃)
│
│ 核心层内部转换
▼
┌──────────────────────────────────────────────────────────────────┐
│ pinctrl(阵营B,I2C1 设备专属) │
│ ├── dev → 指向 I2C1 设备 │
│ ├── state → 当前激活的状态(初始为 NULL,select 后指向 default) │
│ └── states 链表: │
│ │ │
│ └── pinctrl_state "default" │
│ ├── name = "default" │
│ └── settings 链表: │
│ │ │
│ ├── pinctrl_setting [0] │
│ │ type = MUX_GROUP │
│ │ pctldev → 指向阵营A的 pinctrl_dev ◄──────── 关键连接!
│ │ data.mux = { func=3, group=7 } │
│ │ │
│ └── pinctrl_setting [1] │
│ type = CONFIGS_GROUP │
│ pctldev → 指向阵营A的 pinctrl_dev ◄──────── 关键连接!
│ data.configs = { group=7, configs={0x4001b8b0} }│
└──────────────────────────────────────────────────────────────────┘
第四步:执行配置(写寄存器)
pinctrl_select_state(p, "default")
│
│ 遍历 default 状态下的 settings 链表
▼
setting[0]: type = MUX_GROUP
│
│ 通过 pctldev 找到控制器
│ 通过 pctldev->desc->pmxops 找到回调函数
▼
pctldev->desc->pmxops->set_mux(pctldev, func=3, group=7)
│
│ 控制器驱动内部:
│ 根据 group=7 查表找到该 group 包含的引脚 [41, 42]
│ 对每个引脚写 MUX 寄存器
▼
writel(mux_value, base + 0x0090) ← Pin 41 (SCL) 的 MUX 寄存器
writel(mux_value, base + 0x0094) ← Pin 42 (SDA) 的 MUX 寄存器
setting[1]: type = CONFIGS_GROUP
│
▼
pctldev->desc->confops->pin_config_set(pctldev, pin, configs)
│
▼
writel(0x4001b8b0, base + 0x031C) ← Pin 41 的 PAD 寄存器
writel(0x4001b8b0, base + 0x0320) ← Pin 42 的 PAD 寄存器
整体流程
┌─────────────────── 阵营A:控制器侧 ───────────────────┐
│ │
│ pinctrl_desc ──────▶ pinctrl_dev │
│ (静态图纸) (运行时实例) │
│ │ │ │
│ ├── pins[] ├── desc → 回指 desc │
│ │ pinctrl_pin_desc ├── pin_desc_tree │
│ │ └── node → 全局链表 │
│ │ │
│ ├── pctlops → pinctrl_ops │
│ │ └── dt_node_to_map() ← 翻译设备树 │
│ │ │
│ ├── pmxops → pinmux_ops │
│ │ └── set_mux() ← 写复用寄存器 │
│ │ │
│ └── confops → pinconf_ops │
│ └── pin_config_set() ← 写电气寄存器 │
│ │
└───────────────────────────┬───────────────────────────┘
│
pctldev 指针桥接
│
┌───────────────────────────▼───────────────────────────┐
│ │
│ ┌─────────── 阵营C:桥梁 ──────────┐ │
│ │ pinctrl_map(临时翻译产物) │ │
│ │ 设备树原始数据 → 标准格式 │ │
│ └──────────────┬───────────────────┘ │
│ │ 转换 │
│ ▼ │
│ ┌─────────── 阵营B:消费者侧 ──────┐ │
│ │ pinctrl │ │
│ │ └── pinctrl_state "default" │ │
│ │ └── pinctrl_setting │ │
│ │ ├── pctldev ────────────────── 指向阵营A │
│ │ ├── type = MUX/CONFIGS │ │
│ │ └── data = {func, group}│ │
│ └──────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────┘
阵营A(desc/dev/ops)是芯片厂商告诉内核"我有什么能力";阵营C(map)是设备树配置被翻译成的标准中间格式;阵营B(pinctrl/state/setting)是每个外设设备最终持有的、可直接执行的配置队列。数据从 A → C → B → A 单向流动,pinctrl_setting 中的 pctldev 指针是连接消费者需求和控制器能力的唯一桥梁。
下面基于 i.MX6ULL 平台,给出两个完整的 pinctrl 使用示例。
1.设备树
/ {
/*
* LED 设备节点
* 使用内核自带的 gpio-leds 驱动,无需自己写驱动
* compatible 匹配 drivers/leds/leds-gpio.c
*/
leds {
compatible = "gpio-leds";
/* 板子上的用户 LED,连接到 GPIO1_IO18 */
user_led {
label = "board:user_led"; /* sysfs 中显示的名字 */
gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>; /* GPIO1 的第 18 号引脚,高电平点亮 */
default-state = "off"; /* 默认关闭 */
linux,default-trigger = "none"; /* 不使用自动触发 */
/*
* 引用 pinctrl 配置节点
* 内核会在 probe 前自动将 GPIO1_IO18 配置为 GPIO 模式
*/
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_user_led>;
};
};
};
/*
* IOMUXC 控制器节点中添加 pinctrl 配置
* &iomuxc 是芯片级 dtsi 中已定义的 pinctrl 控制器
*/
&iomuxc {
/*
* pinctrl_user_led:LED 引脚的复用和电气配置
*
* MX6UL_PAD_UART1_TX_DATA__GPIO1_IO18 是宏,展开后包含:
* mux_reg 偏移 = 0x0074
* conf_reg 偏移 = 0x0300
* input_reg 偏移 = 0x0000(GPIO 输出不需要输入选择)
* mux_mode = 5(ALT5 = GPIO 功能)
* input_val = 0
*
* 0x17059 是 PAD 电气属性配置值:
* HYS=1(施密特触发器,抗干扰)
* PUS=11(22K 上拉)
* PUE=1(使用上下拉模式)
* PKE=1(使能)
* SPEED=10(100MHz 中速)
* DSE=100(R0/4 中等驱动能力)
* SRE=1(快压摆)
*/
pinctrl_user_led: user_led_grp {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__GPIO1_IO18 0x17059
>;
};
};
2.驱动代码
/*
* 本例使用内核自带的 gpio-leds 驱动(drivers/leds/leds-gpio.c),
* 无需编写自定义驱动。内核通过 compatible = "gpio-leds" 自动匹配。
*
* 下面展示如果不用 gpio-leds 驱动,自己写一个最简 LED 驱动是什么样的。
* 实际项目中推荐直接使用 gpio-leds,这里仅作教学演示。
*/
#include<linux/module.h>
#include<linux/platform_device.h>
#include<linux/gpio/consumer.h> /* GPIO descriptor API */
#include<linux/of.h>
#include<linux/sysfs.h>
#include<linux/device.h>
structmy_led_data {
structgpio_desc *led_gpio;/* GPIO 描述符,由 gpiod_get 获取 */
int state; /* 当前 LED 状态:0=灭,1=亮 */
};
/*
* 通过 sysfs 属性节点控制 LED
* 用户空间执行:echo 1 > /sys/devices/.../state
*/
staticssize_tstate_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
structmy_led_data *data = dev_get_drvdata(dev);
returnsprintf(buf, "%d\n", data->state);
}
staticssize_tstate_store(struct device *dev,
struct device_attribute *attr,
constchar *buf, size_t count)
{
structmy_led_data *data = dev_get_drvdata(dev);
int val;
if (kstrtoint(buf, 10, &val) < 0)
return -EINVAL;
data->state = val ? 1 : 0;
/*
* gpiod_set_value:设置 GPIO 电平
* 内部会检查 pinctrl 是否已将该引脚配置为 GPIO 功能
* 如果 pinctrl 未正确配置,这里可能无法正常工作
*/
gpiod_set_value(data->led_gpio, data->state);
return count;
}
staticDEVICE_ATTR_RW(state);
staticintmy_led_probe(struct platform_device *pdev)
{
structmy_led_data *data;
int ret;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
/*
* 获取 GPIO 描述符
* "led" 对应设备树中 gpios = <...> 的属性名
* GPIOD_OUT_LOW 表示初始化为输出、低电平
*
* 注意:在调用 probe 之前,内核已经通过 pinctrl_bind_pins()
* 将 GPIO1_IO18 的 MUX 寄存器设置为 ALT5(GPIO 模式),
* 所以这里可以直接请求 GPIO 使用
*/
data->led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(data->led_gpio)) {
dev_err(&pdev->dev, "Failed to get GPIO\n");
return PTR_ERR(data->led_gpio);
}
/* 创建 sysfs 属性节点 */
ret = device_create_file(&pdev->dev, &dev_attr_state);
if (ret) {
dev_err(&pdev->dev, "Failed to create sysfs entry\n");
return ret;
}
platform_set_drvdata(pdev, data);
dev_info(&pdev->dev, "My LED driver probed\n");
return0;
}
staticintmy_led_remove(struct platform_device *pdev)
{
device_remove_file(&pdev->dev, &dev_attr_state);
return0;
}
/* 匹配表:compatible = "my,custom-led" */
staticconststructof_device_idmy_led_of_match[] = {
{ .compatible = "my,custom-led", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);
staticstructplatform_drivermy_led_driver = {
.probe = my_led_probe,
.remove = my_led_remove,
.driver = {
.name = "my-custom-led",
.of_match_table = my_led_of_match,
},
};
module_platform_driver(my_led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Demo");
MODULE_DESCRIPTION("Custom LED driver using pinctrl + GPIO");
应用层代码
/*
* led_ctrl.c
* 用户空间控制 LED 的应用程序
*
* 使用内核自带 gpio-leds 驱动时,通过 sysfs 控制:
* echo 1 > /sys/class/leds/board:user_led/brightness 点亮
* echo 0 > /sys/class/leds/board:user_led/brightness 熄灭
*
* 使用自定义驱动时,通过自定义 sysfs 节点控制:
* echo 1 > /sys/devices/.../state 点亮
* echo 0 > /sys/devices/.../state 熄灭
*
* 本程序演示通过 sysfs 文件操作来控制 LED
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
/* 内核自带 gpio-leds 驱动的 sysfs 路径 */
#define LED_BRIGHTNESS_PATH "/sys/class/leds/board:user_led/brightness"
/*
* 向 sysfs 文件写入一个字符('0' 或 '1')
* 返回值:0 成功,-1 失败
*/
staticintwrite_sysfs(constchar *path, constchar *value)
{
int fd;
fd = open(path, O_WRONLY);
if (fd < 0) {
perror("open sysfs failed");
return-1;
}
if (write(fd, value, strlen(value)) < 0) {
perror("write sysfs failed");
close(fd);
return-1;
}
close(fd);
return0;
}
intmain(int argc, char *argv[])
{
if (argc < 2) {
printf("用法: %s <on|off|blink>\n", argv[0]);
printf(" on - 点亮 LED\n");
printf(" off - 熄灭 LED\n");
printf(" blink - 让 LED 以 heartbeat 模式闪烁\n");
return-1;
}
if (strcmp(argv[1], "on") == 0) {
/* 写入 "1" 点亮 LED */
write_sysfs(LED_BRIGHTNESS_PATH, "1");
printf("LED 已点亮\n");
} elseif (strcmp(argv[1], "off") == 0) {
/* 写入 "0" 熄灭 LED */
write_sysfs(LED_BRIGHTNESS_PATH, "0");
printf("LED 已熄灭\n");
} elseif (strcmp(argv[1], "blink") == 0) {
/*
* 切换到 heartbeat 触发模式
* gpio-leds 驱动会自动让 LED 按心跳节奏闪烁
*/
write_sysfs("/sys/class/leds/board:user_led/trigger", "heartbeat");
printf("LED 已进入 heartbeat 闪烁模式\n");
} else {
printf("未知命令: %s\n", argv[1]);
return-1;
}
return0;
}
设备树
/*
* I2C1 控制器节点
* pinctrl 配置不变,引脚复用和电气属性由 pinctrl 子系统自动处理
*/
&i2c1 {
#address-cells = <1>;
#size-cells = <0>;
clock-frequency = <100000>;
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
lm75@48 {
compatible = "my,lm75";
reg = <0x48>;
};
};
&iomuxc {
pinctrl_i2c1: i2c1_grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO00__I2C1_SCL 0x4001b8b0
MX6UL_PAD_GPIO1_IO01__I2C1_SDA 0x4001b8b0
>;
};
};
驱动代码:自定义字符设备方式
/*
* my_lm75_chrdev.c
*
* LM75 温度传感器驱动 —— 自定义字符设备版本
*
* 驱动注册一个字符设备 /dev/i2c_test,应用层通过 ioctl 读取温度。
* 这样应用层不需要知道 I2C 地址、寄存器地址等底层细节,
* 只需要 open("/dev/i2c_test") 然后 ioctl 即可。
*/
#include<linux/module.h>
#include<linux/i2c.h>
#include<linux/cdev.h>
#include<linux/fs.h>
#include<linux/device.h>
#include<linux/uaccess.h>
#include<linux/of.h>
#include<linux/slab.h>
/* ========== 自定义 ioctl 命令定义 ========== */
/*
* ioctl 命令定义规范:
* _IOR(类型, 编号, 数据类型) —— 从驱动读数据到用户空间
* _IOW(类型, 编号, 数据类型) —— 从用户空间写数据到驱动
* _IOWR(类型, 编号, 数据类型) —— 双向读写
*
* 宏参数说明:
* 类型:用一个字符标识设备类别,'T' 表示 Temperature
* 编号:命令编号,从 0 开始递增
* 数据类型:传输的数据结构
*/
#define LM75_IOCTL_MAGIC 'T'
/* 读取温度值,返回毫摄氏度整数 */
#define LM75_IOC_GET_TEMP _IOR(LM75_IOCTL_MAGIC, 0, int)
/* 读取温度值,返回带小数点的字符串,如 "25.5" */
#define LM75_IOC_GET_TEMP_STR _IOR(LM75_IOCTL_MAGIC, 1, char[32])
/* 读取 LM75 配置寄存器的原始值(调试用) */
#define LM75_IOC_GET_CONFIG _IOR(LM75_IOCTL_MAGIC, 2, int)
/* 设置过温报警阈值,单位毫摄氏度 */
#define LM75_IOC_SET_OS_THRESH _IOW(LM75_IOCTL_MAGIC, 3, int)
/* ========== LM75 寄存器定义 ========== */
#define LM75_REG_TEMP 0x00 /* 温度寄存器(只读,2字节) */
#define LM75_REG_CONF 0x01 /* 配置寄存器(读写,1字节) */
#define LM75_REG_THYST 0x02 /* 迟滞温度阈值 */
#define LM75_REG_TOS 0x03 /* 过温报警阈值 */
/* ========== 设备私有数据 ========== */
structmy_lm75_data {
structi2c_client *client;/* I2C 客户端(用于 I2C 通信) */
structcdevcdev;/* 字符设备结构体 */
dev_t devno; /* 设备号 */
structclass *cls;/* 设备类(用于自动创建设备节点) */
structdevice *device;/* 设备结构体(对应 /dev/i2c_test) */
};
/* 全局设备号(静态分配,实际项目中建议动态分配) */
staticdev_t lm75_devno;
/* 全局设备数据指针(单设备场景,多实例时应该用链表管理) */
staticstructmy_lm75_data *g_lm75_data;
/* ========== 字符设备文件操作 ========== */
/*
* open 操作
* 应用层调用 open("/dev/i2c_test", O_RDWR) 时触发
* 将设备私有数据挂到 file->private_data,供后续 read/write/ioctl 使用
*/
staticintlm75_open(struct inode *inode, struct file *filp)
{
/*
* container_of:通过 cdev 成员反推出包含它的 my_lm75_data 结构体
* inode->i_cdev 指向注册时绑定的 cdev
*/
structmy_lm75_data *data = container_of(inode->i_cdev,
structmy_lm75_data, cdev);
filp->private_data = data;
return0;
}
/*
* release 操作
* 应用层调用 close() 时触发
*/
staticintlm75_release(struct inode *inode, struct file *filp)
{
return0;
}
/*
* ioctl 操作 —— 核心接口
*
* 应用层通过 ioctl(fd, cmd, arg) 调用到这里
* cmd 决定执行什么操作,arg 是用户空间传入的参数指针
*
* 这是产品级驱动最常用的用户空间接口方式:
* - 接口清晰,应用层只需知道命令号
* - 可以传输任意复杂的数据结构
* - 不暴露底层 I2C 细节
*/
staticlonglm75_ioctl(struct file *filp, unsignedint cmd, unsignedlong arg)
{
structmy_lm75_data *data = filp->private_data;
structi2c_client *client = data->client;
int ret;
switch (cmd) {
case LM75_IOC_GET_TEMP: {
/*
* 读取温度,返回毫摄氏度整数
* 例如:25500 表示 25.5°C
*/
int raw;
int temp_x2;
int mcelsius;
/* 从 LM75 读取 16 位原始温度值 */
raw = i2c_smbus_read_word_swapped(client, LM75_REG_TEMP);
if (raw < 0)
return raw;
/*
* LM75 数据格式:
* 16 位原始值中,高 9 位有效(1 位符号 + 8 位整数),
* 第 7 位表示 0.5°C。
* 右移 7 位后得到以 0.5°C 为单位的值。
*/
temp_x2 = raw >> 7;
if (temp_x2 & 0x01)
mcelsius = (temp_x2 / 2) * 1000 + 500;
else
mcelsius = (temp_x2 / 2) * 1000;
/* 将结果拷贝到用户空间 */
if (copy_to_user((int __user *)arg, &mcelsius, sizeof(int)))
return -EFAULT;
break;
}
case LM75_IOC_GET_TEMP_STR: {
/*
* 读取温度,返回可读字符串,如 "25.5"
* 方便应用层直接显示,不需要自己做数值转换
*/
int raw;
int temp_x2;
int integer_part;
int frac_part;
char buf[32];
raw = i2c_smbus_read_word_swapped(client, LM75_REG_TEMP);
if (raw < 0)
return raw;
temp_x2 = raw >> 7;
integer_part = temp_x2 / 2;
frac_part = (temp_x2 & 0x01) ? 5 : 0;
snprintf(buf, sizeof(buf), "%d.%d", integer_part, frac_part);
/* 将字符串拷贝到用户空间 */
if (copy_to_user((char __user *)arg, buf, strlen(buf) + 1))
return -EFAULT;
break;
}
case LM75_IOC_GET_CONFIG: {
/*
* 读取配置寄存器原始值(调试用途)
*/
int conf;
conf = i2c_smbus_read_byte_data(client, LM75_REG_CONF);
if (conf < 0)
return conf;
if (copy_to_user((int __user *)arg, &conf, sizeof(int)))
return -EFAULT;
break;
}
case LM75_IOC_SET_OS_THRESH: {
/*
* 设置过温报警阈值
* 用户传入毫摄氏度值,驱动转换为 LM75 寄存器格式后写入
*/
int mcelsius;
int raw_val;
/* 从用户空间拷贝参数 */
if (copy_from_user(&mcelsius, (int __user *)arg, sizeof(int)))
return -EFAULT;
/*
* 将毫摄氏度转换为 LM75 寄存器格式
* LM75 阈值寄存器格式与温度寄存器相同:高 9 位有效
* 整数部分左移 7 位,0.5°C 部分设置 bit7
*/
raw_val = (mcelsius / 1000) << 7;
if ((mcelsius % 1000) >= 500)
raw_val |= (1 << 7);
ret = i2c_smbus_write_word_swapped(client, LM75_REG_TOS, raw_val);
if (ret < 0)
return ret;
break;
}
default:
return -ENOTTY; /* 不支持的命令 */
}
return0;
}
/*
* read 操作(可选)
* 有些驱动也会实现 read,让应用层用 read() 直接读温度
* 这里也提供,作为 ioctl 的补充
*/
staticssize_tlm75_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
structmy_lm75_data *data = filp->private_data;
structi2c_client *client = data->client;
int raw, temp_x2, mcelsius;
char kbuf[32];
int len;
/* 读取温度 */
raw = i2c_smbus_read_word_swapped(client, LM75_REG_TEMP);
if (raw < 0)
return raw;
temp_x2 = raw >> 7;
if (temp_x2 & 0x01)
mcelsius = (temp_x2 / 2) * 1000 + 500;
else
mcelsius = (temp_x2 / 2) * 1000;
/* 格式化为字符串 */
len = snprintf(kbuf, sizeof(kbuf), "%d\n", mcelsius);
/* 拷贝到用户空间 */
if (copy_to_user(buf, kbuf, len))
return -EFAULT;
return len;
}
/* 字符设备文件操作集合 */
staticconststructfile_operationslm75_fops = {
.owner = THIS_MODULE,
.open = lm75_open,
.release = lm75_release,
.unlocked_ioctl = lm75_ioctl,
.read = lm75_read,
};
/* ========== I2C 驱动 probe/remove ========== */
/*
* I2C 驱动 probe
*
* 执行顺序:
* 1. pinctrl 子系统已自动配置好 SCL/SDA 引脚为 I2C 功能
* 2. I2C 控制器已初始化
* 3. 这里注册字符设备,创建 /dev/i2c_test 节点
*/
staticintmy_lm75_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
structmy_lm75_data *data;
int ret;
/* 检查 I2C 适配器能力 */
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_WORD_DATA |
I2C_FUNC_SMBUS_BYTE_DATA)) {
dev_err(&client->dev, "Adapter doesn't support required SMBus ops\n");
return -EIO;
}
/* 分配私有数据 */
data = kzalloc(sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
/* 验证 I2C 通信:读取配置寄存器 */
ret = i2c_smbus_read_byte_data(client, LM75_REG_CONF);
if (ret < 0) {
dev_err(&client->dev, "Failed to communicate with LM75: %d\n", ret);
dev_err(&client->dev, "Check pinctrl and wiring!\n");
goto err_free;
}
dev_info(&client->dev, "LM75 detected, config reg: 0x%02x\n", ret);
/*
* ===== 注册字符设备 =====
*/
/* 步骤1:分配设备号 */
ret = alloc_chrdev_region(&lm75_devno, 0, 1, "i2c_test");
if (ret < 0) {
dev_err(&client->dev, "Failed to alloc chrdev: %d\n", ret);
goto err_free;
}
data->devno = lm75_devno;
/* 步骤2:初始化 cdev 并绑定文件操作 */
cdev_init(&data->cdev, &lm75_fops);
data->cdev.owner = THIS_MODULE;
/* 步骤3:将 cdev 添加到内核 */
ret = cdev_add(&data->cdev, lm75_devno, 1);
if (ret < 0) {
dev_err(&client->dev, "Failed to add cdev: %d\n", ret);
goto err_unreg;
}
/*
* 步骤4:创建设备类
* 设备类用于 udev/mdev 自动创建设备节点
* 没有这一步,/dev/i2c_test 节点不会自动出现
*/
data->cls = class_create(THIS_MODULE, "i2c_test_class");
if (IS_ERR(data->cls)) {
ret = PTR_ERR(data->cls);
goto err_cdev;
}
/*
* 步骤5:创建设备节点
* 这会在 /dev/ 下创建 "i2c_test" 节点
* udev/mdev 会根据 class 信息自动执行 mknod
*/
data->device = device_create(data->cls, NULL, lm75_devno,
NULL, "i2c_test");
if (IS_ERR(data->device)) {
ret = PTR_ERR(data->device);
goto err_class;
}
g_lm75_data = data;
dev_info(&client->dev, "LM75 char device /dev/i2c_test registered\n");
dev_info(&client->dev, " Major: %d, Minor: %d\n",
MAJOR(lm75_devno), MINOR(lm75_devno));
return0;
/* 错误处理的回退路径(逆序释放) */
err_class:
class_destroy(data->cls);
err_cdev:
cdev_del(&data->cdev);
err_unreg:
unregister_chrdev_region(lm75_devno, 1);
err_free:
kfree(data);
return ret;
}
/*
* I2C 驱动 remove
* 模块卸载或设备移除时调用,逆序释放所有资源
*/
staticintmy_lm75_remove(struct i2c_client *client)
{
structmy_lm75_data *data = i2c_get_clientdata(client);
device_destroy(data->cls, data->devno);
class_destroy(data->cls);
cdev_del(&data->cdev);
unregister_chrdev_region(data->devno, 1);
kfree(data);
g_lm75_data = NULL;
return0;
}
/* 设备树匹配表 */
staticconststructof_device_idmy_lm75_of_match[] = {
{ .compatible = "my,lm75", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_lm75_of_match);
/* I2C 设备 ID 表 */
staticconststructi2c_device_idmy_lm75_id[] = {
{ "my_lm75", 0 },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(i2c, my_lm75_id);
/* I2C 驱动结构体 */
staticstructi2c_drivermy_lm75_driver = {
.driver = {
.name = "my-lm75-chrdev",
.of_match_table = my_lm75_of_match,
},
.probe = my_lm75_probe,
.remove = my_lm75_remove,
.id_table = my_lm75_id,
};
module_i2c_driver(my_lm75_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Demo");
MODULE_DESCRIPTION("LM75 temperature sensor driver with char device interface");
应用层代码
/*
* temp_read_chrdev.c
*
* 通过 /dev/i2c_test 字符设备读取 LM75 温度
*
* 编译:gcc -o temp_read_chrdev temp_read_chrdev.c
* 运行:./temp_read_chrdev
*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/ioctl.h>
/* ========== ioctl 命令定义(必须与驱动中一致) ========== */
#define LM75_IOCTL_MAGIC 'T'
#define LM75_IOC_GET_TEMP _IOR(LM75_IOCTL_MAGIC, 0, int)
#define LM75_IOC_GET_TEMP_STR _IOR(LM75_IOCTL_MAGIC, 1, char[32])
#define LM75_IOC_GET_CONFIG _IOR(LM75_IOCTL_MAGIC, 2, int)
#define LM75_IOC_SET_OS_THRESH _IOW(LM75_IOCTL_MAGIC, 3, int)
/* 设备节点路径 */
#define DEV_PATH "/dev/i2c_test"
intmain(int argc, char *argv[])
{
int fd;
int ret;
printf("=== LM75 温度采集(字符设备方式)===\n\n");
/* 打开字符设备 */
fd = open(DEV_PATH, O_RDWR);
if (fd < 0) {
perror("open /dev/i2c_test failed");
printf("请确认:\n");
printf(" 1. 驱动已加载:lsmod | grep lm75\n");
printf(" 2. 设备节点存在:ls -l /dev/i2c_test\n");
printf(" 3. 权限足够:sudo ./temp_read_chrdev\n");
return-1;
}
/* ===== 方式1:ioctl 读取温度整数(毫摄氏度) ===== */
{
int temp_mc;
ret = ioctl(fd, LM75_IOC_GET_TEMP, &temp_mc);
if (ret < 0) {
perror("ioctl GET_TEMP failed");
} else {
printf("[ioctl 整数] 温度: %d 毫摄氏度 = %.1f °C\n",
temp_mc, temp_mc / 1000.0);
}
}
/* ===== 方式2:ioctl 读取温度字符串 ===== */
{
char temp_str[32] = {0};
ret = ioctl(fd, LM75_IOC_GET_TEMP_STR, temp_str);
if (ret < 0) {
perror("ioctl GET_TEMP_STR failed");
} else {
printf("[ioctl 字符串] 温度: %s °C\n", temp_str);
}
}
/* ===== 方式3:read 读取温度 ===== */
{
char buf[64] = {0};
ret = read(fd, buf, sizeof(buf) - 1);
if (ret < 0) {
perror("read failed");
} else {
buf[ret] = '\0';
printf("[read] 温度: %s 毫摄氏度\n", buf);
}
}
/* ===== 方式4:ioctl 读取配置寄存器(调试) ===== */
{
int config;
ret = ioctl(fd, LM75_IOC_GET_CONFIG, &config);
if (ret < 0) {
perror("ioctl GET_CONFIG failed");
} else {
printf("[ioctl 调试] 配置寄存器: 0x%02x\n", config);
}
}
/* ===== 方式5:ioctl 设置过温报警阈值为 35.5°C ===== */
{
int thresh_mc = 35500; /* 35.5°C = 35500 毫摄氏度 */
ret = ioctl(fd, LM75_IOC_SET_OS_THRESH, &thresh_mc);
if (ret < 0) {
perror("ioctl SET_OS_THRESH failed");
} else {
printf("[ioctl 设置] 过温报警阈值已设为 35.5 °C\n");
}
}
/* 关闭设备 */
close(fd);
printf("\n测试完成\n");
return0;
}
