consumer 角度讲一下i2c外设

往期内容

I2C子系统专栏:

  1. I2C(IIC)协议讲解-优快云博客
  2. SMBus 协议详解-优快云博客
  3. I2C相关结构体讲解:i2c_adapter、i2c_algorithm、i2c_msg-优快云博客
  4. 内核提供的通用I2C设备驱动I2c-dev.c分析:注册篇
  5. 内核提供的通用I2C设备驱动I2C-dev.c分析:file_ops篇
  6. 设备驱动与设备树匹配机制详解
  7. 编写一个通用的i2c设备驱动框架
  8. 编写一个通用的i2c控制器驱动框架

总线和设备树专栏:

  1. 总线和设备树_憧憬一下的博客-优快云博客
  2. 设备树与 Linux 内核设备驱动模型的整合-优快云博客

前言

在上一章节(编写一个通用的i2c控制器驱动框架)讲了i2c_adapter。这里再继之前的文章( 编写一个通用的i2c设备驱动框架)进行补充,讲一下l2c_cleint的设备形态等

1. 两种设备形态

img

可以看出是有两种你描述的这两种设备形态反映了设备通过不同的总线进行交互的差异,并且在设备树(DTS)和驱动程序的编写上有不同的表现。根据设备的主要功能和数据传输方式,它们在 Linux 设备模型中的位置不同,从而影响了 DTS 文件中的结构和驱动程序的设计。我们来分析这两种设备形态。

形态 1: I2C-only 设备(图左侧):

  • 特点:这类设备和 CPU 之间的所有数据交互都是通过 I2C 总线完成的,没有其他传输方式。
  • DTS 描述:设备直接作为 I2C 控制器的子节点存在。在 DTS 中,设备的定义紧随其所在的 I2C 总线节点之后。例如 PMIC,它是 I2C 总线上的一个设备,因此直接在 I2C 节点的子节点中定义。
&i2c1 {
    clock-frequency = <100000>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;
    status = "okay";

    pmic: pf0100@08 {
        compatible = "fsl,pfuze100";
        reg = <0x08>;
        ...
    };
};
  • 驱动模型:I2C 核心负责处理设备的创建和注册。驱动程序是以 I2C slave 设备的方式来实现,I2C 驱动框架通过 i2c_driver 结构和 i2c_device_id 来与设备匹配。设备的 proberemove 等函数由 I2C 核心进行管理。
static const struct i2c_device_id pmic_i2c_ids[] = {
    { "pfuze100", 0 },
    { /* sentinel */ }
};

static struct i2c_driver pmic_driver = {
    .driver = {
        .name = "pfuze100",
        .of_match_table = pmic_of_match,
    },
    .probe = pmic_probe,
    .remove = pmic_remove,
    .id_table = pmic_i2c_ids,
};

I2C 是主数据交互总线,设备完全依赖 I2C,所有操作通过 I2C 完成。

驱动注册流程通过 I2C 子系统的 i2c_driver 完成,设备由 I2C 核心管理。


形态 2: 复合设备(I2C 作为辅助接口,图右侧):

  • 特点:这类设备通过多种方式与 CPU 交互,例如,主要的数据传输(如音视频数据)通过 TDMS 接口,而配置信息或控制信息通过 I2C(DDC)接口。这类设备的核心功能决定了它们的主要位置在其他总线(如平台总线 platform bus)上,而 I2C 只是作为一个辅助接口。
  • DTS 描述:HDMI 设备作为 platform device 定义,并且通过 ddc-i2c-bus 属性引用 I2C 总线,用于 DDC(Display Data Channel)接口的 I2C 通信。
&hdmi {
    ddc-i2c-bus = <&i2c2>;
    status = "okay";
};
  • 驱动模型:在这种情况下,HDMI 控制器是主要设备,注册为 platform_device。I2C 通信是 HDMI 控制器的一部分,用来处理 EDID 等信息读取。在驱动程序中,I2C 总线是通过 DTS 中的 ddc-i2c-bus 属性传递给 HDMI 驱动的,驱动程序会通过 of_find_i2c_adapter_by_node() 获取并绑定 I2C 控制器。
struct i2c_adapter *ddc;
struct device_node *ddc_node;

ddc_node = of_parse_phandle(dev->of_node, "ddc-i2c-bus", 0);
if (!ddc_node) {
    dev_err(dev, "failed to find ddc-i2c-bus node\n");
    return -ENODEV;
}

ddc = of_find_i2c_adapter_by_node(ddc_node);
of_node_put(ddc_node);
if (!ddc)
    return -EPROBE_DEFER;

/* Use ddc for I2C transactions */

I2C 是辅助总线,用于少量数据交互(如 EDID、配置信息传输),主要功能通过其他总线(如 TDMS)完成。

驱动以 platform_device 形式注册,I2C 仅作为其中一个通信接口,I2C 控制器通过设备树中的引用绑定。

2. 驱动编写

根据上面所说的形态去编写设备不同的i2c_driver

形态1: 完全依赖I2C的设备(如PMIC)

  1. 确定I2C adapter
    根据设备硬件连接的实际情况,确定该设备所从属的I2C controller,即I2C adapter(在I2C framework中,I2C controller通常被称为I2C adapter)。例如,PMIC设备可能连接在 i2c1 总线上。
  2. 在DTS中添加设备描述
    在设备树的I2C adapter节点中,添加I2C从设备的描述。这个从设备的定义格式与普通的platform device一致,例如:
&i2c1 {
    pmic: pfuze100@08 {
        compatible = "fsl,pfuze100";
        reg = <0x08>;
        // 其他字段...
    };
};
  1. compatible 字段的使用
    DTS 中的 compatible 字段用于设备驱动的匹配和 probe。I2C驱动会根据 compatible 字段匹配到合适的驱动程序,例如 "compatible = "fsl,pfuze100";"
  2. 编写I2C设备的驱动程序

示例:

static const struct of_device_id pmic_of_match[] = {
    { .compatible = "fsl,pfuze100", },
    { /* sentinel */ }
};

static struct i2c_driver pmic_driver = {
    .driver = {
        .name = "pfuze100",
        .of_match_table = pmic_of_match,
    },
    .probe = pmic_probe,
    .remove = pmic_remove,
};
static int __init i2c_driver_XXX_init(void)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	return i2c_add_driver(&i2c_XXX_driver);
}
module_init(i2c_driver_XXX_init);
    • 定义并注册 i2c_driver:编写一个 struct i2c_driver 结构体,并使用 i2c_add_driver将其注册到I2C core中。
    • 匹配设备:在 i2c_driver 结构体中,定义 of_match_table,使其可以通过 compatible 字段进行匹配。
    • 实现 probe 回调:编写 probe 函数,用于设备初始化。
  1. I2C core处理设备注册
    当I2C adapter注册时,I2C framework的核心代码会自动为其下的所有I2C从设备创建 struct i2c_client 结构体,并根据设备树中的 compatible 字段,匹配合适的 i2c_driver,然后调用驱动程序的 probe 函数。
  2. probe
    当设备被I2C framework匹配到时,调用此函数进行设备初始化。主要负责获取设备资源(如设备地址、I2C Adapter),设置硬件寄存器,或者初始化其他系统资源(如注册字符设备)。
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    // 打印日志确认 probe 触发
    printk(KERN_INFO "ap3216c i2c device probe.\n");

    // 设置I2C client指针
    ap3216c_client = client;

    // 执行设备初始化:如设置初始寄存器值
    i2c_smbus_write_byte_data(client, 0x00, 0x03);  // 设置某些寄存器

    // 注册字符设备或其他资源
    major = register_chrdev(0, "ap3216c", &ap3216c_ops);
    ap3216c_class = class_create(THIS_MODULE, "ap3216c_class");
    device_create(ap3216c_class, NULL, MKDEV(major, 0), NULL, "ap3216c");

    return 0;
}

在注册的ap3216c_ops中,有关read和write的函数,使用到I2C数据有关的接口就有两类:

  • 一类是以i2c client为参数,进行简单的数据收发。该方法只可以通过标准方式,发送或者接收一定数量的数据。
  • 另一类是以i2c adapter和i2c msg为参数,可以更为灵活的read或者write数据,包括i2c_transfer。使用该方法可以以struct i2c_msg为参数,一次读取、或者写入、或者读取加写入,一定数量的数据。
  • 具体的函数去看\Linux-4.9.88\include\linux\i2c.h

形态2: 复合型设备(如HDMI)

  1. 确定I2C adapter
    根据设备硬件的连接方式,找到设备使用的I2C adapter。例如,HDMI 设备可以使用 i2c2 作为其 DDC 通道。
  2. 将设备作为平台设备描述
    将设备作为平台设备(platform device)进行描述。其 DTS 节点并不是直接位于I2C adapter下,而是位于根目录。例如:
&hdmi {
    ddc-i2c-bus = <&i2c2>;
    status = "okay";
};
  1. 获取I2C adapter的引用
    在DTS描述中使用 ddc-i2c-bus 属性引用I2C adapter节点。该属性通过 of_parse_phandle 来解析,并在驱动中获取相应的I2C adapter的指针。
  2. probe 函数中获取I2C adapter
    在平台设备的 probe 函数中,通过 of_parse_phandle 来获取I2C adapter节点,并通过 of_find_i2c_adapter_by_node() 获取对应的 struct i2c_adapter 指针。如下代码示例:
struct i2c_adapter *ddc;
struct device_node *ddc_node;

ddc_node = of_parse_phandle(dev->of_node, "ddc-i2c-bus", 0);
if (!ddc_node) {
    dev_err(dev, "failed to find ddc-i2c-bus node\n");
    return -ENODEV;
}

ddc = of_find_i2c_adapter_by_node(ddc_node);
of_node_put(ddc_node);
if (!ddc)
    return -EPROBE_DEFER;
  1. 使用 i2c_transfer 进行读写操作
    获取I2C adapter之后,便可以使用 i2c_transfer() 接口来进行I2C读写操作。例如,通过DDC接口读取EDID信息时:
struct i2c_msg msgs[] = {
    {
        .addr = ddc_addr,
        .flags = I2C_M_RD,
        .len = 128,
        .buf = edid_buf,
    },
};

ret = i2c_transfer(ddc, msgs, 1);  // 读取EDID数据

3. i2c_client

i2c设备中用i2c_client结构体来指代:

/**
 * struct i2c_client - represent an I2C slave device
 * @flags: I2C_CLIENT_TEN indicates the device uses a ten bit chip address;
 *	I2C_CLIENT_PEC indicates it uses SMBus Packet Error Checking
 * @addr: Address used on the I2C bus connected to the parent adapter.
 * @name: Indicates the type of the device, usually a chip name that's
 *	generic enough to hide second-sourcing and compatible revisions.
 * @adapter: manages the bus segment hosting this I2C device
 * @dev: Driver model device node for the slave.
 * @irq: indicates the IRQ generated by this device (if any)
 * @detected: member of an i2c_driver.clients list or i2c-core's
 *	userspace_devices list
 * @slave_cb: Callback when I2C slave mode of an adapter is used. The adapter
 *	calls it to pass on slave events to the slave driver.
 *
 * An i2c_client identifies a single device (i.e. chip) connected to an
 * i2c bus. The behaviour exposed to Linux is defined by the driver
 * managing the device.
 */
struct i2c_client {
	unsigned short flags;		/* div., see below		*/
	unsigned short addr;		/* chip address - NOTE: 7bit	*/
					/* addresses are stored in the	*/
					/* _LOWER_ 7 bits		*/
	char name[I2C_NAME_SIZE];
	struct i2c_adapter *adapter;	/* the adapter we sit on	*/
	struct device dev;		/* the device structure		*/
	int irq;			/* irq issued by device		*/
	struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
	i2c_slave_cb_t slave_cb;	/* callback for slave mode	*/
#endif
};

flags:

  • 表示I2C设备的标志位。可以包含以下选项:

    • I2C_CLIENT_TEN:表示设备使用的是十位地址(而不是常见的七位地址)。
    • I2C_CLIENT_PEC:表示设备使用SMBus包错误校验(Packet Error Checking,PEC),这是一种错误检测机制。

addr:

  • I2C设备的地址。地址位于I2C总线上的低7位或低10位(如果 flags 指示设备使用十位地址的话)。

name:

  • 一个字符数组,表示设备的名称,通常是设备的芯片名称或类型。名称尽可能保持通用,以适应同一设备的不同版本或兼容的设备。

adapter:

  • 指向设备所在的I2C总线适配器(i2c_adapter)的指针。每个I2C设备都挂载在一个I2C适配器上,适配器管理与设备的通信。

dev:

  • Linux驱动模型中的设备节点(struct device),用于表示I2C从设备。这是Linux设备模型的标准数据结构,提供了与设备类、驱动程序以及电源管理等相关的接口。

irq:

  • 表示设备所使用的中断号(如果该设备可以生成中断)。不使用中断的设备此字段可能为0或无效值。

detected:

  • 用于链表结构中的节点。在内核中,这个字段用于将该设备连接到 i2c_driver.clients 列表或 i2c-coreuserspace_devices 列表中。

slave_cb:

  • 仅在启用了 I2C Slave 模式(即 CONFIG_I2C_SLAVE 选项启用时)时有效。
  • 这是用于适配器从模式的回调函数指针。当I2C适配器处于从模式时,适配器调用这个回调函数,将从模式的事件传递给从设备的驱动程序。

i2c_client一般是在register adapter的时候,解析adapter的child node自行创建的,这个在对上一章节中对i2c_add_adapte函数进行分析的时候有提到过

<think>好的,用户问的是“Linux怎么驱动单片机外设”,这个问题需要分步骤解答。首先,用户可能对Linux驱动开发不太熟悉,需要先理清基本概念。 首先,应该明确Linux驱动外设的关系。Linux驱动作为内核模块,负责与硬件交互。而单片机外设可能指的是GPIO、SPI、I2C等接口。需要确认用户是否指的是在Linux系统中控制单片机,还是直接在单片机上运行Linux。不过通常单片机资源有限,可能跑的是RTOS或无系统,而Linux可能运行在如树莓派等更强大的处理器上。因此,用户可能想问的是如何在Linux系统下驱动外接的单片机外设,比如通过某种通信协议控制。 接下来,需要考虑开发环境的准备,比如内核头文件、交叉编译工具链等。用户可能不知道如何搭建环境,需要给出具体步骤,比如安装必要的软件包。 然后是驱动开发流程。用户可能对字符设备驱动、设备树等概念不熟悉,需要解释设备树的作用,以及如何编写基本的驱动框架,比如模块加载和卸载函数。同时,需要提到外设的具体类型,比如GPIO、SPI、I2C,不同的外设驱动方式不同,可能需要调用内核提供的子系统API。 另外,用户可能关心具体的代码示例,比如如何操作GPIO,需要展示如何通过sysfs或libgpiod库进行控制,或者编写内核模块使用gpiod子系统。同时,用户可能需要了解用户空间的驱动开发,因为有些情况下不需要内核模块,直接通过用户空间程序访问外设。 还要提到调试和测试的方法,比如使用逻辑分析仪、示波器检查信号,或者使用内核日志进行调试。用户可能在开发过程中遇到问题,需要这些调试技巧。 最后,总结整个流程,并推荐进一步学习的资源,如Linux设备驱动开发书籍、内核文档等。确保用户能够按步骤操作,同时理解背后的原理。</think>在Linux系统中驱动单片机外设需要结合硬件接口协议、内核驱动开发和用户空间程序,以下是具体步骤: --- ### 一、硬件层准备 1. **确定外设类型** 明确外设的接口类型,例如: - GPIO(通用输入输出) - SPI/I2C(串行通信) - UART(串口) - PWM(脉冲宽度调制) 2. **硬件连接验证** 使用示波器或逻辑分析仪检查信号是否正常,确认电平匹配(如3.3V/5V)。 --- ### 二、内核驱动开发 1. **设备树(Device Tree)配置** 在设备树(`.dts`文件)中声明外设资源,例如GPIO引脚定义: ```dts &gpio0 { my_led { label = "user_led"; gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>; }; }; ``` 编译设备树后更新到系统。 2. **编写内核驱动模块** - **字符设备驱动框架** ```c #include <linux/module.h> #include <linux/fs.h> #include <linux/gpio/consumer.h> static struct gpio_desc *led_gpio; static dev_t dev_num; static struct class *led_class; static int led_open(struct inode *inode, struct file *file) { gpiod_set_value(led_gpio, 1); // 点亮LED return 0; } static struct file_operations fops = { .owner = THIS_MODULE, .open = led_open, }; static int __init led_init(void) { // 分配设备号、创建设备节点 alloc_chrdev_region(&dev_num, 0, 1, "my_led"); led_class = class_create(THIS_MODULE, "led_class"); device_create(led_class, NULL, dev_num, NULL, "my_led"); // 获取GPIO资源 led_gpio = gpiod_get(NULL, "user_led", GPIOD_OUT_LOW); return 0; } static void __exit led_exit(void) { gpiod_set_value(led_gpio, 0); // 关闭LED device_destroy(led_class, dev_num); class_destroy(led_class); unregister_chrdev_region(dev_num, 1); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); ``` - **编译与加载** 使用`Makefile`编译生成`.ko`文件,通过`insmod`加载驱动--- ### 三、用户空间控制 1. **通过sysfs操作GPIO** ```bash # 导出GPIO引脚 echo 12 > /sys/class/gpio/export # 设置方向为输出 echo out > /sys/class/gpio/gpio12/direction # 控制电平 echo 1 > /sys/class/gpio/gpio12/value ``` 2. **使用libgpiod库(推荐)** ```c #include <gpiod.h> int main() { struct gpiod_chip *chip = gpiod_chip_open("/dev/gpiochip0"); struct gpiod_line *line = gpiod_chip_get_line(chip, 12); gpiod_line_request_output(line, "my_led", 0); gpiod_line_set_value(line, 1); // 点亮LED gpiod_line_release(line); return 0; } ``` --- ### 四、通信协议驱动(以I2C为例) 1. **内核驱动注册** ```c static struct i2c_driver my_sensor_driver = { .probe = sensor_probe, .remove = sensor_remove, .driver = { .name = "my_sensor", .of_match_table = sensor_of_match, }, }; static const struct of_device_id sensor_of_match[] = { { .compatible = "vendor,my-sensor" }, {}, }; ``` 2. **用户空间通过i2c-tools访问** ```bash # 扫描I2C总线 i2cdetect -y 1 # 读取寄存器数据 i2cget -y 1 0x50 0x00 ``` --- ### 五、调试与验证 1. **内核日志查看** ```bash dmesg | tail -n 20 # 检查驱动加载日志 ``` 2. **逻辑分析仪验证信号时序** 确认SPI/I2C的时钟、数据线波形是否符合协议。 --- ### 六、关键注意事项 1. **权限问题** 用户空间程序可能需要`root`权限或加入`gpio`/`i2c`用户组。 2. **实时性要求** 高实时性外设(如PWM)需考虑内核抢占配置或使用实时内核补丁。 --- **推荐学习资源** - 书籍:《Linux设备驱动程序》(O'Reilly) - 内核文档:`Documentation/driver-api/` - 工具:`devmem2`(直接读写物理内存)、`spidev_test`(SPI测试)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值