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下会出现对应节点,驱动据此完成初始化。覆盖机制的价值在于:同一块板子可以按不同外设组合切换配置,适合产品化和快速验证。
用文件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是内核根据设备树实例化出来的“运行时接口”;用户程序则通过这些接口读写数据。三者形成“描述—实例化—访问”的链条。当采集异常时,排查顺序通常是:先看/proc/device-tree确认节点存在,再看/sys或/dev确认接口生成,最后检查程序打开路径与权限。掌握这条链路,STM32MP上的传感器采集就能从“能跑”走向“可控、可维护”。