4. 车载设备号动态分配与udev规则适配

任务说明

本案例实现一个车载电池电压采集驱动:通过ADC控制器读取电池电压原始值,转换为毫伏电压后,通过字符设备接口提供给用户空间程序读取。驱动基于Linux内核的IIO子系统实现,用户空间可通过cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw读取原始值,或通过自定义字符设备节点读取转换后的毫伏值。

环境依赖

  • Linux内核源码树(本案例基于5.10版本验证)
  • 交叉编译工具链(ARM平台)或本机gcc(x86模拟验证)
  • 内核已启用CONFIG_IIO和CONFIG_IIO_BUFFER

驱动实现

以下代码为完整驱动,包含平台驱动框架、IIO设备注册、ADC原始值读取及毫伏转换逻辑。驱动通过设备树获取ADC通道信息,并注册一个misc设备提供read接口返回电压值(单位:mV)。

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/iio/iio.h>
#include <linux/iio/consumer.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/of.h>

#define DRIVER_NAME "battery_adc"

struct batt_adc_dev {
    struct iio_channel *channel;
    struct miscdevice misc;
};

static ssize_t batt_adc_read(struct file *file, char __user *buf,
                             size_t count, loff_t *ppos)
{
    struct batt_adc_dev *dev = container_of(file->private_data,
                                            struct batt_adc_dev, misc);
    int ret, val, uv;
    char kbuf[32];
    int len;

    if (*ppos != 0)
        return 0;

    ret = iio_read_channel_processed(dev->channel, &val);
    if (ret < 0) {
        dev_err(&dev->misc.this_device, "read ADC failed: %d\n", ret);
        return ret;
    }

    /* iio_read_channel_processed 返回微伏(uV),转换为毫伏(mV) */
    uv = val;
    len = snprintf(kbuf, sizeof(kbuf), "%d\n", uv / 1000);

    if (copy_to_user(buf, kbuf, len))
        return -EFAULT;

    *ppos = len;
    return len;
}

static const struct file_operations batt_adc_fops = {
    .owner = THIS_MODULE,
    .read = batt_adc_read,
};

static int batt_adc_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct batt_adc_dev *bdev;
    struct iio_channel *chan;
    int ret;

    bdev = devm_kzalloc(dev, sizeof(*bdev), GFP_KERNEL);
    if (!bdev)
        return -ENOMEM;

    chan = devm_iio_channel_get(dev, "battery-volt");
    if (IS_ERR(chan)) {
        ret = PTR_ERR(chan);
        if (ret == -EPROBE_DEFER)
            return ret;
        dev_err(dev, "failed to get IIO channel: %d\n", ret);
        return ret;
    }
    bdev->channel = chan;

    bdev->misc.minor = MISC_DYNAMIC_MINOR;
    bdev->misc.name = "battery_voltage";
    bdev->misc.fops = &batt_adc_fops;
    bdev->misc.parent = dev;

    ret = misc_register(&bdev->misc);
    if (ret) {
        dev_err(dev, "failed to register misc device: %d\n", ret);
        return ret;
    }

    platform_set_drvdata(pdev, bdev);
    dev_info(dev, "battery ADC driver probed\n");
    return 0;
}

static int batt_adc_remove(struct platform_device *pdev)
{
    struct batt_adc_dev *bdev = platform_get_drvdata(pdev);
    misc_deregister(&bdev->misc);
    return 0;
}

static const struct of_device_id batt_adc_of_match[] = {
    { .compatible = "vendor,battery-adc" },
    { }
};
MODULE_DEVICE_TABLE(of, batt_adc_of_match);

static struct platform_driver batt_adc_driver = {
    .probe = batt_adc_probe,
    .remove = batt_adc_remove,
    .driver = {
        .name = DRIVER_NAME,
        .of_match_table = batt_adc_of_match,
    },
};
module_platform_driver(batt_adc_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Course");
MODULE_DESCRIPTION("Battery voltage ADC driver");

设备树配置

在设备树中,ADC控制器节点需已定义(如adc@ff650000),驱动节点通过io-channels属性绑定到具体ADC通道:

&battery_adc_node {
    compatible = "vendor,battery-adc";
    io-channels = <&adc 0>;  /* 使用ADC通道0 */
    io-channel-names = "battery-volt";
    status = "okay";
};

编译与加载

  1. 将驱动源码放入内核drivers/iio/adc/目录,并在Kconfig/Makefile中添加对应条目。
  2. 编译内核或编译为模块:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
  3. 加载模块:insmod battery_adc.ko(若编译为模块)

验证方法

  1. 检查驱动加载日志:dmesg | grep "battery ADC",应看到battery ADC driver probed。
  2. 确认设备节点存在:ls -l /dev/battery_voltage。
  3. 读取电压值:cat /dev/battery_voltage,预期输出一个整数(单位mV),例如12400表示12.4V。
  4. 与IIO原始节点对比:cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw,该值乘以通道缩放系数(由ADC驱动提供)应等于字符设备输出的毫伏值。

通过判定标准:cat /dev/battery_voltage能稳定输出合理电压值(如车载电池12V对应约12000mV),且连续读取数值波动在ADC量化误差范围内(通常±10mV以内),即视为驱动工作正常。