嵌入式Linux驱动开发(I2C专题)(六)

完善虚拟的I2C_Adapter驱动并模拟EEPROM

参考资料:

  • Linux内核文档:

    • Linux-4.9.88\Documentation\devicetree\bindings\i2c\i2c-gpio.txt
    • Linux-5.4\Documentation\devicetree\bindings\i2c\i2c-gpio.yaml
  • Linux内核驱动程序:使用GPIO模拟I2C

    • Linux-4.9.88\drivers\i2c\busses\i2c-gpio.c
    • Linux-5.4\drivers\i2c\busses\i2c-gpio.c
  • Linux内核真正的I2C控制器驱动程序

    • IMX6ULL: Linux-4.9.88\drivers\i2c\busses\i2c-imx.c
    • STM32MP157: Linux-5.4\drivers\i2c\busses\i2c-stm32f7.c

1. 实现master_xfer函数

在虚拟的I2C_Adapter驱动程序里,只要实现了其中的master_xfer函数,这个I2C Adapter就可以使用了。

在master_xfer函数里,我们模拟一个EEPROM,思路如下:

  • 分配一个512自己的buffer,表示EEPROM
  • 对于slave address为0x50的i2c_msg,解析并处理
    • 对于写:把i2c_msg的数据写入buffer
    • 对于读:从buffer中把数据写入i2c_msg
  • 对于slave address为其他值的i2c_msg,返回错误

2. 编程

adapter.c

c 复制代码
#include <linux/completion.h>
#include <linux/debugfs.h>
#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/slab.h>

static struct i2c_adapter *g_adapter;

static unsigned char eeprom_buffer[512];
static int eeprom_cur_addr = 0;
static void eeprom_emulate_xfer(struct i2c_adapter *i2c_adap, struct i2c_msg *msg)
{
	int i;
	if (msg->flags & I2C_M_RD)			//读操作
	{
		for (i = 0; i < msg->len; i++)
		{
			msg->buf[i] = eeprom_buffer[eeprom_cur_addr++];
			if (eeprom_cur_addr == 512)
				eeprom_cur_addr = 0;
		}
	}
	else								//写操作
	{
		if (msg->len >= 1)
		{
			eeprom_cur_addr = msg->buf[0];
			for (i = 1; i < msg->len; i++)
			{
				eeprom_buffer[eeprom_cur_addr++] = msg->buf[i];
				if (eeprom_cur_addr == 512)
					eeprom_cur_addr = 0;
			}
		}
	}
}

static int i2c_bus_virtual_master_xfer(struct i2c_adapter *i2c_adap,
		    struct i2c_msg msgs[], int num)
{
	int i;

	// emulate eeprom , addr = 0x50
	for (i = 0; i < num; i++)
	{
		if (msgs[i].addr == 0x50)
		{
			eeprom_emulate_xfer(i2c_adap, &msgs[i]);
		}
		else
		{
			i = -EIO;
			break;
		}
	}
	
	return i;
}

static u32 i2c_bus_virtual_func(struct i2c_adapter *adap)
{
	return I2C_FUNC_I2C | I2C_FUNC_NOSTART | I2C_FUNC_SMBUS_EMUL |
	       I2C_FUNC_SMBUS_READ_BLOCK_DATA |
	       I2C_FUNC_SMBUS_BLOCK_PROC_CALL |
	       I2C_FUNC_PROTOCOL_MANGLING;
}


const struct i2c_algorithm i2c_bus_virtual_algo = {
	.master_xfer   = i2c_bus_virtual_master_xfer,
	.functionality = i2c_bus_virtual_func,
};


static int i2c_bus_virtual_probe(struct platform_device *pdev)
{
	/* get info from device tree, to set i2c_adapter/hardware  */
	
	/* alloc, set, register i2c_adapter */
	g_adapter = kzalloc(sizeof(*g_adapter), GFP_KERNEL);

	g_adapter->owner = THIS_MODULE;
	g_adapter->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
	g_adapter->nr = -1;
	snprintf(g_adapter->name, sizeof(g_adapter->name), "i2c-bus-virtual");

	g_adapter->algo = &i2c_bus_virtual_algo;

	i2c_add_adapter(g_adapter); // i2c_add_numbered_adapter(g_adapter);
	
	return 0;
}

static int i2c_bus_virtual_remove(struct platform_device *pdev)
{
	i2c_del_adapter(g_adapter);
	return 0;
}
static const struct of_device_id i2c_bus_virtual_dt_ids[] = {
	{ .compatible = "100ask,i2c-bus-virtual", },
	{ /* sentinel */ }
};

static struct platform_driver i2c_bus_virtual_driver = {
	.driver		= {
		.name	= "i2c-gpio",
		.of_match_table	= of_match_ptr(i2c_bus_virtual_dt_ids),
	},
	.probe		= i2c_bus_virtual_probe,
	.remove		= i2c_bus_virtual_remove,
};


static int __init i2c_bus_virtual_init(void)
{
	int ret;

	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	ret = platform_driver_register(&i2c_bus_virtual_driver);
	if (ret)
		printk(KERN_ERR "i2c-gpio: probe failed: %d\n", ret);

	return ret;
}
module_init(i2c_bus_virtual_init);

static void __exit i2c_bus_virtual_exit(void)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	platform_driver_unregister(&i2c_bus_virtual_driver);
}
module_exit(i2c_bus_virtual_exit);

MODULE_AUTHOR("www.100ask.net");
MODULE_LICENSE("GPL");

3. 上机实验

3.1 设置工具链
  • IMX6ULL

    shell 复制代码
    export ARCH=arm
    export CROSS_COMPILE=arm-linux-gnueabihf-
    export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin
  • STM32MP157
    注意:对于STM32MP157,以前说编译内核/驱动、编译APP的工具链不一样,其实编译APP用的工具链也能用来编译内核。

    shell 复制代码
    export ARCH=arm
    export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
    export PATH=$PATH:/home/book/100ask_stm32mp157_pro-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
3.2 编译、替换设备树

在设备树根节点下,添加如下代码:

shell 复制代码
	i2c-bus-virtual {
		 compatible = "100ask,i2c-bus-virtual";
	};
1. STM32MP157
  • 修改arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dts,添加如下代码:

    shell 复制代码
    / {
    	i2c-bus-virtual {
    		 compatible = "100ask,i2c-bus-virtual";
    	};
    };
  • 编译设备树:

    在Ubuntu的STM32MP157内核目录下执行如下命令,

    得到设备树文件:arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb

    shell 复制代码
    make dtbs
  • 复制到NFS目录:

    shell 复制代码
    $ cp arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb ~/nfs_rootfs/
  • 开发板上挂载NFS文件系统

    • vmware使用NAT(假设windowsIP为192.168.1.100)

      shell 复制代码
      [root@100ask:~]# mount -t nfs -o nolock,vers=3,port=2049,mountport=9999 
      192.168.1.100:/home/book/nfs_rootfs /mnt
    • vmware使用桥接,或者不使用vmware而是直接使用服务器:假设Ubuntu IP为192.168.1.137

      shell 复制代码
      [root@100ask:~]#  mount -t nfs -o nolock,vers=3 192.168.1.137:/home/book/nfs_rootfs /mnt
  • 确定设备树分区挂载在哪里

    由于版本变化,STM32MP157单板上烧录的系统可能有细微差别。

    在开发板上执行cat /proc/mounts后,可以得到两种结果(见下图):

    • mmcblk2p2分区挂载在/boot目录下(下图左边):无需特殊操作,下面把文件复制到/boot目录即可

    • mmcblk2p2挂载在/mnt目录下(下图右边)

      • 在视频里、后面文档里,都是更新/boot目录下的文件,所以要先执行以下命令重新挂载:
        • mount /dev/mmcblk2p2 /boot
    • 更新设备树

      shell 复制代码
      [root@100ask:~]# cp /mnt/stm32mp157c-100ask-512d-lcd-v1.dtb /boot
      [root@100ask:~]# sync
  • 重启开发板

2. IMX6ULL
  • 修改arch/arm/boot/dts/100ask_imx6ull-14x14.dts,添加如下代码:

    shell 复制代码
    / {
    	i2c-bus-virtual {
    		 compatible = "100ask,i2c-bus-virtual";
    	};
    };
  • 编译设备树:

    在Ubuntu的IMX6ULL内核目录下执行如下命令,

    得到设备树文件:arch/arm/boot/dts/100ask_imx6ull-14x14.dtb

    shell 复制代码
    make dtbs
  • 复制到NFS目录:

    shell 复制代码
    $ cp arch/arm/boot/dts/100ask_imx6ull-14x14.dtb ~/nfs_rootfs/
  • 开发板上挂载NFS文件系统

    • vmware使用NAT(假设windowsIP为192.168.1.100)

      shell 复制代码
      [root@100ask:~]# mount -t nfs -o nolock,vers=3,port=2049,mountport=9999 
      192.168.1.100:/home/book/nfs_rootfs /mnt
    • vmware使用桥接,或者不使用vmware而是直接使用服务器:假设Ubuntu IP为192.168.1.137

      shell 复制代码
      [root@100ask:~]#  mount -t nfs -o nolock,vers=3 192.168.1.137:/home/book/nfs_rootfs /mnt
    • 更新设备树

      shell 复制代码
      [root@100ask:~]# cp /mnt/100ask_imx6ull-14x14.dtb /boot
      [root@100ask:~]# sync
  • 重启开发板

3.4 编译、安装驱动程序
  • 编译:

    • 在Ubuntu上
    • 修改06_i2c_adapter_virtual_ok中的Makefile,指定内核路径KERN_DIR,在执行make命令即可。
  • 安装:

    • 在开发板上

    • 挂载NFS,复制文件,insmod,类似如下命令:

      shell 复制代码
      mount -t nfs -o nolock,vers=3 192.168.1.137:/home/book/nfs_rootfs /mnt
      // 对于IMX6ULL,想看到驱动打印信息,需要先执行
      echo "7 4 1 7" > /proc/sys/kernel/printk
      
      insmod /mnt/i2c_adapter_drv.ko
3.5 使用i2c-tools测试

在开发板上执行,命令如下:

  • 列出I2C总线

    shell 复制代码
    i2cdetect -l

    结果类似下列的信息:

    shell 复制代码
    i2c-1   i2c             21a4000.i2c                             I2C adapter
    i2c-4   i2c             i2c-bus-virtual                         I2C adapter
    i2c-0   i2c             21a0000.i2c                             I2C adapter

    注意:不同的板子上,i2c-bus-virtual的总线号可能不一样,上问中总线号是4。

  • 检查虚拟总线下的I2C设备

    shell 复制代码
    // 假设虚拟I2C BUS号为4
    [root@100ask:~]# i2cdetect -y -a 4
         0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
    00: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    50: 50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    70: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  • 读写模拟的EEPROM

    shell 复制代码
    // 假设虚拟I2C BUS号为4
    [root@100ask:~]# i2cset -f -y 4 0x50 0 0x55   // 往0地址写入0x55
    [root@100ask:~]# i2cget -f -y 4 0x50 0        // 读0地址
    0x55

使用GPIO模拟I2C的驱动程序分析

参考资料:

  • i2c_spec.pdf
  • Linux文档
    • Linux-5.4\Documentation\devicetree\bindings\i2c\i2c-gpio.yaml
    • Linux-4.9.88\Documentation\devicetree\bindings\i2c\i2c-gpio.txt
  • Linux驱动源码
    • Linux-5.4\drivers\i2c\busses\i2c-gpio.c
    • Linux-4.9.88\drivers\i2c\busses\i2c-gpio.c

1. 回顾I2C协议

1.1 硬件连接

I2C在硬件上的接法如下所示,主控芯片引出两条线SCL,SDA线,在一条I2C总线上可以接很多I2C设备,我们还会放一个上拉电阻(放一个上拉电阻的原因以后我们再说)。

1.2 I2C信号

I2C协议中数据传输的单位是字节,也就是8位。但是要用到9个时钟:前面8个时钟用来传输8数据,第9个时钟用来传输回应信号。传输时,先传输最高位(MSB)。

  • 开始信号(S):SCL为高电平时,SDA山高电平向低电平跳变,开始传送数据。
  • 结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
  • 响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA
  • SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化

I2C协议信号如下:

1.3 协议细节
  • 如何在SDA上实现双向传输?

    主芯片通过一根SDA线既可以把数据发给从设备,也可以从SDA上读取数据,连接SDA线的引脚里面必然有两个引脚(发送引脚/接受引脚)。

  • 主、从设备都可以通过SDA发送数据,肯定不能同时发送数据,怎么错开时间?

    在9个时钟里,

    前8个时钟由主设备发送数据的话,第9个时钟就由从设备发送数据;

    前8个时钟由从设备发送数据的话,第9个时钟就由主设备发送数据。

  • 双方设备中,某个设备发送数据时,另一方怎样才能不影响SDA上的数据?

    设备的SDA中有一个三极管,使用开极/开漏电路(三极管是开极,CMOS管是开漏,作用一样),如下图:

    真值表如下:

从真值表和电路图我们可以知道:

  • 当某一个芯片不想影响SDA线时,那就不驱动这个三极管
  • 想让SDA输出高电平,双方都不驱动三极管(SDA通过上拉电阻变为高电平)
  • 想让SDA输出低电平,就驱动三极管

从下面的例子可以看看数据是怎么传的(实现双向传输)。

举例:主设备发送(8bit)给从设备

  • 前8个clk

    • 从设备不要影响SDA,从设备不驱动三极管
    • 主设备决定数据,主设备要发送1时不驱动三极管,要发送0时驱动三极管
  • 第9个clk,由从设备决定数据

    • 主设备不驱动三极管
    • 从设备决定数据,要发出回应信号的话,就驱动三极管让SDA变为0
    • 从这里也可以知道ACK信号是低电平

从上面的例子,就可以知道怎样在一条线上实现双向传输,这就是SDA上要使用上拉电阻的原因。

为何SCL也要使用上拉电阻?

在第9个时钟之后,如果有某一方需要更多的时间来处理数据,它可以一直驱动三极管把SCL拉低。

当SCL为低电平时候,大家都不应该使用IIC总线,只有当SCL从低电平变为高电平的时候,IIC总线才能被使用。

当它就绪后,就可以不再驱动三极管,这是上拉电阻把SCL变为高电平,其他设备就可以继续使用I2C总线了。

2. 使用GPIO模拟I2C的要点

  • 引脚设为GPIO
  • GPIO设为输出、开极/开漏(open collector/open drain)
  • 要有上拉电阻

3. 驱动程序分析

3.1 平台总线设备驱动模型
3.2 设备树

对于GPIO引脚的定义,有两种方法:

  • 老方法:gpios
  • 新方法:sda-gpios、scl-gpios
3.3 驱动程序分析
1. I2C-GPIO驱动层次
2. 传输函数分析

看视频分析i2c_outb函数:drivers\i2c\algos\i2c-algo-bit.c

4. 怎么使用I2C-GPIO

设置设备数,在里面添加一个节点即可,示例代码看上面:

  • compatible = "i2c-gpio";

  • 使用pinctrl把 SDA、SCL所涉及引脚配置为GPIO、开极

    • 可选
  • 指定SDA、SCL所用的GPIO

  • 指定频率(2种方法):

    • i2c-gpio,delay-us = <5>; /* ~100 kHz */
    • clock-frequency = <400000>;
  • #address-cells = <1>;

  • #size-cells = <0>;

  • i2c-gpio,sda-open-drain:

    • 它表示其他驱动、其他系统已经把SDA设置为open drain了
    • 在驱动里不需要在设置为open drain
    • 如果需要驱动代码自己去设置SDA为open drain,就不要提供这个属性
相关推荐
imc.1116 分钟前
linux基础IO
linux·运维·服务器
躺不平的理查德17 分钟前
让Ubuntu 的第二块网卡 ens37 永久固定为 192.168.30.111
运维·服务器
bitbrowser1 小时前
Facebook 验证反复回到原页面,应该从哪里排查
运维·服务器·facebook
Land03291 小时前
AI网页元素变化无法自动修复?自带元素自愈的自动化解决方案
运维·人工智能·ai·自动化·rpa
fengyehongWorld1 小时前
Jenkins 安装与简单配置
运维·jenkins
云智慧AIOps社区1 小时前
2026 国产化 ITSM 替代指南:横向测评 ServiceNow、轻帆云、Jira等五款主流IT服务管理平台
运维·人工智能·运维开发·it服务管理·itsm平台
Urbano2 小时前
针织卫衣全流程生产工序科普:自动化替代、产能优化与设备选型实战方案
运维·自动化
BelongPanda2 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
AC赳赳老秦2 小时前
招投标公开数据自动化采集实战:基于 OpenClaw 的定时抓取与业务关键词精准推送
运维·服务器·数据库·自动化·测试用例·deepseek·openclaw
小的博客2 小时前
windows下安装Docker Desptop
运维·docker·容器