Heng30的博客
搜索 分类 关于 订阅

如何通过设备树访问raspi3b的i2c设备

2025-03-09

i2c是嵌入式开发的常用协议,下面我们学习通过Linux驱动来读写i2c设备的数据。通过设备树来获取i2c的信息,使用Linux内核的i2c子系统提供的接口来控制读写数据。

实验环境

raspberry pi3b

Linux raspberrypi 6.1.21-v8+ #1642 SMP PREEMPT Mon Apr  3 17:24:16 BST 2023 aarch64 GNU/Linux

设备树文件

// testoverlay.dts

/dts-v1/;
/plugin/;
/ {
    compatible = "raspberrypi,3-model-bbrcm,bcm2837";
    fragment@0 {
        target = <&i2c1>;
        __overlay__ {
            #address-cells = <1>;
            #size-cells = <0>;

            my_adc: my_adc@10 {
                compatible = "brightlight,myadc";
                reg = <0x10>;
                status = "okay";
            };
        };
    };
};

驱动代码

// simple.c

#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/platform_device.h>
#include <linux/proc_fs.h>
#include <linux/property.h>

static struct i2c_client *adc_client = NULL;
static struct proc_dir_entry *proc_file = NULL;

static struct of_device_id my_driver_ids[] = {
    {
        .compatible = "brightlight,myadc",
    },
    {},
};
MODULE_DEVICE_TABLE(of, my_driver_ids);

static struct i2c_device_id my_adc[] = {
    {"my_adc", 0},
    {},
};
MODULE_DEVICE_TABLE(i2c, my_adc);

static ssize_t _read(struct file *file, char __user *userbuf, size_t size,
                     loff_t *offset) {
    pr_info("simple_read\n");

    char buf[32];
    int value = i2c_smbus_read_byte(adc_client);

    int len = snprintf(buf, sizeof(buf), "%d\n", value);
    if (copy_to_user(userbuf, buf, len)) {
        return -EFAULT;
    }

    return len;
}

static ssize_t _write(struct file *file, const char __user *userbuf,
                      size_t size, loff_t *offset) {
    pr_info("simple_write\n");
    long val = 0;

    if (0 == kstrtol(userbuf, 0, &val)) {
        i2c_smbus_write_byte(adc_client, (unsigned char)val);
    }
    return size;
}

static struct proc_ops fops = {
    .proc_read = _read,
    .proc_write = _write,
};

// 要求i2c设备真实存在,且匹配成功才会调用该函数
static int _dt_probe(struct i2c_client *client, const struct i2c_device_id *id) {
    pr_info("simple_dt_probe\n");

    if (client->addr != 0x10) {
        pr_err("error: client addr != 10\n");
        return -1;
    }
    adc_client = client;

    proc_file = proc_create("my-adc", 0666, NULL, &fops);
    if (!proc_file) {
        pr_err("proc_create /proc/my-adc file failed\n");
        return -ENOMEM;
    }

    return 0;
}

static void _dt_remove(struct i2c_client *client) {
    pr_info("simple_dt_remove\n");
    proc_remove(proc_file);
}

static struct i2c_driver my_dirver = {
    .probe = _dt_probe,
    .remove = _dt_remove,
    .id_table = my_adc,
    .driver =
        {
            .name = "my_adc",
            .of_match_table = my_driver_ids,
        },
};

// 这里会调用init和exit函数
module_i2c_driver(my_dirver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("heng30");
MODULE_VERSION("v0.0.1");
MODULE_DESCRIPTION("A simple kernel module");

编译脚本

#!/bin/sh

top-dir = $(shell pwd)
kernel-version = $(shell uname -r)
kernel-dir ?= /lib/modules/$(kernel-version)/build

obj-m += simple.o

all: dt driver

driver:
        make -C $(kernel-dir) modules M=$(top-dir)

dt:
        dtc -@ -I dts -O dtb -o testoverlay.dtbo testoverlay.dts

clean:
        rm -f *.o *.ko *.mod *.mod.c *.order *.symvers *.dtbo
        make -C $(kernel-dir) clean m=$(top-dir)

测试

  • 安装i2c-toolssudo apt install i2c-tools

  • 查看所有i2c总线:i2cdetect -l

  • 查看总线1上的i2c设备:i2cdetect -y 1

  • 编译程序:make

  • 安装设备树节点:sudo dtoverlay testoverlay.dtbo

  • 查看设备树节点信息

    • find /proc/device-tree/ -name "*adc*"
    /proc/device-tree/soc/i2c@7e804000/my_adc@10
    
    • ls /proc/device-tree/soc/i2c@7e804000/my_adc@10
    compatible  name  phandle  reg  status
    
  • 安装驱动:insmod simple.ko

  • 写入数据:echo 1 > /proc/my-adc

  • 读取数据:head -n 1 /proc/my-adc

  • 移除驱动:rmmod simple.ko

  • 移除设备树节点:sudo dtoverlay -r testoverlay

参考