4. Linux设备驱动与用户空间接口

在STM32MP平台上做数据采集,最终落到应用层时,几乎都绕不开“用文件IO访问硬件”这件事。Linux把设备抽象成文件,用户空间通过open/read/write/ioctl等系统调用与内核驱动交互。理解字符设备、sysfs和devicetree覆盖机制这三层接口,是写出稳定采集程序的前提。

字符设备:最直接的字节流通道

字符设备以字节流方式访问,典型代表是/dev/ttySTM0、/dev/i2c-1、/dev/spidev0.0。内核中每个字符设备由struct cdev描述,通过主设备号(major)和次设备号(minor)唯一标识。用户空间打开设备节点后,内核根据file_operations结构体把read/write/ioctl分发到具体驱动函数。

以GPIO为例,STM32MP的GPIO控制器驱动会注册gpiochip,用户空间可通过/dev/gpiochipN配合GPIO_GET_LINEHANDLE_IOCTL等ioctl请求行句柄,再对句柄做读写。相比直接操作寄存器,这种方式由内核统一管理方向、上下拉和中断,避免多进程竞争。

# 查看字符设备主次设备号
ls -l /dev/gpiochip0
# crw------- 1 root root 254, 0 Jan 1 00:00 /dev/gpiochip0

# 查看已注册的gpio控制器
gpiodetect
# gpiochip0 [GPIOA] (16 lines)
# gpiochip1 [GPIOB] (16 lines)

sysfs:把内核对象映射成目录树

sysfs挂载在/sys,把内核中的设备、驱动、总线以目录和属性文件的形式暴露出来。对GPIO而言,传统接口是/sys/class/gpio/:向export写入编号即可导出引脚,随后在/sys/class/gpio/gpioN/下出现direction、value、edge等属性文件。读value得到电平,写value改变输出。

ADC则通常通过IIO子系统暴露在/sys/bus/iio/devices/iio:deviceN/下,属性文件如in_voltage0_raw给出原始码值,in_voltage_scale给出换算系数。二者相乘即得电压。这种“一个属性文件对应一个参数”的设计,让shell脚本也能完成采集。

# 导出GPIO并读取电平(示例引脚编号)
echo 12 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio12/direction
cat /sys/class/gpio/gpio12/value

# 读取ADC原始值与比例系数
cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw
cat /sys/bus/iio/devices/iio:device0/in_voltage_scale

devicetree覆盖机制:不改内核也能改硬件描述

STM32MP的硬件资源由设备树(Device Tree)描述。设备树覆盖(overlay)允许在运行时或启动时对基础设备树打补丁,动态增加节点或修改属性,而无需重新编译整个内核。典型流程是:编写.dts覆盖文件,用dtc编译成.dtbo,再通过U-Boot的fdt apply或内核的configfs接口加载。

例如要把PA12配置为GPIO输入并启用内部上拉,可以写一个覆盖片段,把该引脚从默认功能中“摘”出来,改为gpio-keys或自定义节点。加载后/proc/device-tree下会出现对应节点,驱动据此完成初始化。覆盖机制的价值在于:同一块板子可以按不同外设组合切换配置,适合产品化和快速验证。

STM32MP 用户空间访问硬件的三层接口 用户空间应用程序 open / read / write ioctl 控制请求 sysfs 属性文件读写 libgpiod / IIO 库 内核接口层 字符设备 /dev/gpiochipN cdev + file_operations sysfs / GPIO /sys/class/gpio/ export · direction · value IIO 子系统 /sys/bus/iio/devices/ in_voltage0_raw · scale 设备树 /proc/device-tree 节点与属性 STM32MP 硬件资源 GPIO 控制器 PA / PB / PC ... 引脚 ADC 控制器 12位 · 多通道 传感器 / 外设 光敏 · 电位器 · 按键 引脚复用 AF 配置 设备树覆盖(.dtbo)在启动或运行时修改节点,决定上述接口是否可用及引脚功能

用文件IO写一个采集程序

下面给出一个C语言示例,同时读取GPIO电平与ADC电压。程序假设GPIO已通过sysfs导出,ADC对应iio:device0。注意:以下参数为教学示例,实际设备编号需用ls /sys/bus/iio/devices/确认。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#define GPIO_PATH "/sys/class/gpio/gpio12/value"
#define ADC_RAW   "/sys/bus/iio/devices/iio:device0/in_voltage0_raw"
#define ADC_SCALE "/sys/bus/iio/devices/iio:device0/in_voltage_scale"

static int read_int(const char *path) {
    char buf[32] = {0};
    int fd = open(path, O_RDONLY);
    if (fd < 0) { perror(path); return -1; }
    read(fd, buf, sizeof(buf) - 1);
    close(fd);
    return atoi(buf);
}

static double read_double(const char *path) {
    char buf[32] = {0};
    int fd = open(path, O_RDONLY);
    if (fd < 0) { perror(path); return -1; }
    read(fd, buf, sizeof(buf) - 1);
    close(fd);
    return atof(buf);
}

int main(void) {
    int gpio = read_int(GPIO_PATH);
    int raw  = read_int(ADC_RAW);
    double scale = read_double(ADC_SCALE);
    if (gpio < 0 || raw < 0 || scale < 0) return 1;
    printf("GPIO12=%d, ADC_raw=%d, scale=%.6f, voltage=%.3f V\n",
           gpio, raw, scale, raw * scale / 1000.0);
    return 0;
}

编译运行:arm-linux-gnueabihf-gcc -o collect collect.c,拷贝到板端执行。若ADC的scale单位为mV,则raw * scale / 1000.0得到伏特。例如raw=2048、scale=0.805664 mV,则电压约1.650 V。判断采集是否正常,可观察:GPIO值随按键变化在0/1之间跳变;ADC值随电位器旋转单调变化;若读数恒为0或满量程,应检查引脚复用是否被其他功能占用、参考电压是否正常。

调试建议:先用cat直接读属性文件确认硬件通路正常,再写程序。若export报“Device or resource busy”,说明该引脚已被设备树中的其他驱动占用,需要修改覆盖文件释放引脚。
注意:sysfs GPIO接口在新内核中标记为legacy,推荐使用libgpiod的字符设备接口。但理解sysfs有助于快速验证和排障,两者并不冲突。

三层接口的协作关系

设备树决定“有哪些硬件、引脚怎么复用”,是静态描述;字符设备和sysfs是内核根据设备树实例化出来的“运行时接口”;用户程序则通过这些接口读写数据。三者形成“描述—实例化—访问”的链条。当采集异常时,排查顺序通常是:先看/proc/device-tree确认节点存在,再看/sys或/dev确认接口生成,最后检查程序打开路径与权限。掌握这条链路,STM32MP上的传感器采集就能从“能跑”走向“可控、可维护”。