• 嵌入式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

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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");
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143

    3. 上机实验

    3.1 设置工具链
    • IMX6ULL

      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
      
      • 1
      • 2
      • 3
    • STM32MP157
      注意:对于STM32MP157,以前说编译内核/驱动、编译APP的工具链不一样,其实编译APP用的工具链也能用来编译内核。

      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
      
      • 1
      • 2
      • 3
    3.2 编译、替换设备树

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

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

      / {
      	i2c-bus-virtual {
      		 compatible = "100ask,i2c-bus-virtual";
      	};
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 编译设备树:
      在Ubuntu的STM32MP157内核目录下执行如下命令,
      得到设备树文件:arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb

      make dtbs
      
      • 1
    • 复制到NFS目录:

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

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

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

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

      由于版本变化,STM32MP157单板上烧录的系统可能有细微差别。
      在开发板上执行cat /proc/mounts后,可以得到两种结果(见下图):

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

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

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

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

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

      / {
      	i2c-bus-virtual {
      		 compatible = "100ask,i2c-bus-virtual";
      	};
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 编译设备树:
      在Ubuntu的IMX6ULL内核目录下执行如下命令,
      得到设备树文件:arch/arm/boot/dts/100ask_imx6ull-14x14.dtb

      make dtbs
      
      • 1
    • 复制到NFS目录:

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

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

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

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

        [root@100ask:~]# cp /mnt/100ask_imx6ull-14x14.dtb /boot
        [root@100ask:~]# sync
        
        • 1
        • 2
    • 重启开发板

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

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

      • 在开发板上

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

        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
        
        • 1
        • 2
        • 3
        • 4
        • 5
    3.5 使用i2c-tools测试

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

    • 列出I2C总线

      i2cdetect -l
      
      • 1

      结果类似下列的信息:

      i2c-1   i2c             21a4000.i2c                             I2C adapter
      i2c-4   i2c             i2c-bus-virtual                         I2C adapter
      i2c-0   i2c             21a0000.i2c                             I2C adapter
      
      • 1
      • 2
      • 3

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

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

      // 假设虚拟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: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • 读写模拟的EEPROM

      // 假设虚拟I2C BUS号为4
      [root@100ask:~]# i2cset -f -y 4 0x50 0 0x55   // 往0地址写入0x55
      [root@100ask:~]# i2cget -f -y 4 0x50 0        // 读0地址
      0x55
      
      • 1
      • 2
      • 3
      • 4

    使用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,就不要提供这个属性
  • 相关阅读:
    NextJS工程部署到阿里云linux Ecs
    Redis第三讲:分布式锁的三种实现方法
    Vue3学习笔记 - 禹神YYDS
    基于OpenCV的轮廓检测(2)
    Linus Torvalds:最庆幸的是 30 年后,Linux 不是一个“死”项目
    借车、挂靠风险有多大?
    初学者必读:如何使用 Nuxt 中间件简化网站开发
    某次 ctf Mobile 0x01 解题过程
    【心理学】2022-08-03 日常生活问题回答
    关于的Java线程池,简解
  • 原文地址:https://blog.csdn.net/afddasfa/article/details/132889684