赞
踩
在电子产品硬件设计当中,I2C 是一种很常见的同步、串行、低速、近距离通信接口,用于连接各种IC、传感器等器件,它们都会提供I2C接口与SoC主控相连,比如陀螺仪、加速度计、触摸屏等,其最大优势在于可以在总线上扩展多个外围设备的支持。
Linux内核开发者为了让驱动开发工程师在内核中方便的添加自己的I2C设备驱动程序,更容易的在linux下驱动自己的I2C接口硬件,进而引入了I2C总线框架。与Linux下的platform虚拟总线不同的是,I2C是实际的物理总线,所以I2C总线框架也是Linux下总线、设备、驱动模型的产物。
本章来学习一下如何在Linux下的I2C总线框架,以及如何使用I2C总线框架编写一个I2C接口的外设驱动程序;本章重点是学习Linux下的I2C总线框架。
这里跟stm32裸机开发就是一样的了,不再赘述。
I2C主要的就是总线的读写时序,下图是写时序:
下图是I2C读时序:
STM32MP157D有6个I2C接口,其中I2C4和I2C6可以在A7安全模式或者A7非安全模式下使用,M4无法使用,STM32MP157 的I2C部分特性如下:
STM32MP1开发板上通过I2C5连接了一个三合一环境传感器:AP3216C。AP3216C是由敦南科技推出的一款传感器,其支持环境光强度(ALS)、接近距离(PS)和红外线强度(IR)这三个环境参数检测。该芯片可以通过IIC接口与主控制相连,并且支持中断,AP3216C的特点如下:
AP3216C常被用于手机、平板、导航设备等,其内置的接近传感器可以用于检测是否有物体接近,比如手机上用来检测耳朵是否接触听筒,如果检测到的话就表示正在打电话,手机就会关闭手机屏幕以省电。也可以使用环境光传感器检测光照强度,可以实现自动背光亮度调节。AP3216C结构如下图所示:
AP3216的设备地址为0X1E,同几乎所有的 I2C从器件一样,AP3216C内部也有一些寄存器,通过这些寄存器可以配置AP3216C的工作模式,并且读取相应的数据。 AP3216C用的寄存器如下图所示:
在上图中,0X00这个寄存器是模式控制寄存器,用来设置AP3216C的工作模式,一般开始先将其设置为0X04,也就是先软件复位一次AP3216C。接下来根据实际使用情况选择合适的工作模式,比如设置为0X03,也就是开启ALS+PS+IR。0X0A-0X0F这6个寄存器就是数据寄存器,保存着ALS、PS和IR这三个传感器获取到的数据值。如果同时打开ALS、PS和IR的读取间隔最少要112.5ms,因为AP3216C完成一次转换需要112.5ms。
使用裸机的方式编写一个I2C器件的驱动程序,一般需要实现两部分:
I2C主机驱动也就是SoC的I2C控制器对应的驱动程序,I2C设备驱动其实就是挂在I2C总线下的具体设备对应的驱动程序,例如eeprom、触摸屏IC、传感器IC等;对于主机驱动来说,一旦编写完成就不需要再做修改,其他的I2C设备直接调用主机驱动提供的API函数完成读写操作即可。这个正好符合Linux的驱动分离与分层的思想,因此Linux内核也将I2C驱动分为两部分。
Linux内核开发者为了让驱动开发工程师在内核中方便的添加自己的I2C设备驱动程序,方便更容易的在linux下驱动自己的I2C接口硬件,进而引入了I2C总线框架,一般也叫作I2C子系统,Linux下I2C子系统总体框架如下所示:
从上图可以看出,I2C子系统分为三大组成部分。
I2C核心提供了I2C总线驱动(适配器)和设备驱动的注册、注销方法,I2C通信方法(algorithm)与具体硬件无关的代码,以及探测设备地址的上层代码等。
I2C总线驱动是I2C适配器的软件实现,提供I2C适配器与从设备间完成数据通信的能力。I2C总线驱动由i2c_adapter和i2c_algorithm来描述。I2C适配器是SoC中内置i2c控制器的软件抽象,可以理解为他所代表的是一个I2C主机。
包括两部分:设备的注册和驱动的注册。
I2C子系统帮助内核统一管理I2C设备,让驱动开发工程师在内核中可以更加容易地添加自己的I2C设备驱动程序。
首先来看一下I2C总线,在讲platform的时候就说过,platform是虚拟出来的一条总线,目的是为了实现总线、设备、驱动框架。对于I2C而言,不需要虚拟出一条总线,直接使用I2C总线即可。I2C总线驱动重点是I2C适配器(也就是SoC的I2C接口控制器)驱动,这里要用到两个重要的数据结构:i2c_adapter和i2c_algorithm。I2C子系统将SoC的I2C适配器(控制器)抽象成一个i2c_adapter结构体,i2c_adapter结构体定义在include/linux/i2c.h文件中,结构体内容如下:
第688行,i2c_algorithm类型的指针变量algo,对于一个I2C适配器,肯定要对外提供读写API函数,设备驱动程序可以使用这些API函数来完成读写操作。i2c_algorithm就是I2C适配器与IIC设备进行通信的方法。
i2c_algorithm结构体定义在include/linux/i2c.h文件中,内容如下:
第536行,master_xfer就是I2C适配器的传输函数,可以通过此函数来完成与IIC设备之间的通信。
第540行,smbus_xfer就是SMBUS总线的传输函数。smbus协议是从I2C协议的基础上发展而来的,他们之间有很大的相似度,SMBus与I2C总线之间在时序特性上存在一些差别,应用于移动PC和桌面PC系统中的低速率通讯。
综上所述,I2C总线驱动,或者说I2C适配器驱动的主要工作就是初始化i2c_adapter结构体变量,然后设置i2c_algorithm中的master_xfer函数。完成以后通过i2c_add_numbered_adapter或i2c_add_adapter这两个函数向I2C子系统注册设置好的i2c_adapter,这两个 函数的原型如下:
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
这两个函数的区别在于i2c_add_adapter会动态分配一个总线编号,而i2c_add_numbered_adapter函数则指定一个静态的总线编号。函数参数和返回值含义如下:
如果要删除I2C适配器的话使用i2c_del_adapter函数即可,函数原型如下:
void i2c_del_adapter(struct i2c_adapter * adap)
函数参数和返回值含义如下:
关于I2C的总线(控制器或适配器)驱动就讲解到这里,一般SoC的I2C总线驱动都是由半导体厂商编写的,比如STM32MP1的I2C适配器驱动ST官方已经编写好了,这个不需要用户去编写。因此I2C总线驱动对SoC使用者来说是被屏蔽掉的,只要专注I2C设备驱动即可。
I2C设备驱动重点关注两个数据结构:i2c_client和i2c_driver,根据总线、设备和驱动模型,I2C总线上一小节已经讲了。还剩下设备和驱动,i2c_client用于描述I2C总线下的设备,i2c_driver则用于描述I2C总线下的设备驱动,类似于platform总线下的platform_device和platform_driver。
i2c_client结构体定义在include/linux/i2c.h文件中,内容如下:
一个I2C设备对应一个i2c_client结构体变量,系统每检测到一个I2C从设备就会给这个设备分配一个i2c_client。
i2c_driver类似platform_driver,是编写I2C设备驱动重点要处理的内容,i2c_driver结构体定义在include/linux/i2c.h文件中,内容如下:
示例代码41.2.2.2 i2c_driver结构体 253 struct i2c_driver { 254 unsigned int class; 255 256 /* Standard driver model interfaces */ 257 int (*probe)(struct i2c_client *client, const struct i2c_device_id *id); 258 int (*remove)(struct i2c_client *client); 259 260 /* New driver model interface to aid the seamless removal of 261 * the current probe()'s, more commonly unused than used 262 second parameter.*/ 263 int (*probe_new)(struct i2c_client *client); 264 265 /* driver model interfaces that don't relate to enumeration */ 266 void (*shutdown)(struct i2c_client *client); 267 268 /* Alert callback, for example for the SMBus alert protocol. 269 * The format and meaning of the data value depends on the 270 * protocol. For the SMBus alert protocol, there is a single 271 * bit of data passed as the alert response's low bit ("event 272 * flag"). For the SMBus Host Notify protocol, the data 273 * corresponds to the 16-bit payload data reported by the 274 slave device acting as master.*/ 275 void (*alert)(struct i2c_client *client, enum i2c_alert_protocol protocol, 276 unsigned int data); 277 278 /* a ioctl like command that can be used to perform specific 279 * functions with the device. 280 */ 281 int (*command)(struct i2c_client *client, unsigned int cmd, void *arg); 282 283 struct device_driver driver; 284 const struct i2c_device_id *id_table; 285 286 /* Device detection callback for automatic device creation */ 287 int (*detect)(struct i2c_client *client, struct i2c_board_info *info); 288 const unsigned short *address_list; 289 struct list_head clients; 290 291 bool disable_i2c_core_irq_mapping; 292 };
第257行,当I2C设备和驱动匹配成功以后probe函数就会执行,和platform驱动一样。
第283行,device_driver驱动结构体,如果使用设备树的话,需要设置device_driver的of_match_table成员变量,也就是驱动的兼容(compatible)属性。
第284行,id_table是传统的、未使用设备树的设备匹配ID表。
对于I2C设备驱动编写人来说,重点工作就是构建i2c_driver,构建完成以后需要向I2C子系统注册这个i2c_driver。i2c_driver注册函数为int i2c_register_driver,此函数原型如下:
int i2c_register_driver(struct module *owner,
struct i2c_driver *driver)
函数参数和返回值含义如下:
另外i2c_add_driver也常常用于注册i2c_driver,i2c_add_driver是一个宏,定义如下:
i2c_add_driver就是对i2c_register_driver做了一个简单的封装,只有一个参数,就是要注册
的i2c_driver。
注销I2C设备驱动的时候需要将前面注册的i2c_driver从I2C子系统中注销掉,需要用到
i2c_del_driver函数,此函数原型如下:
void i2c_del_driver(struct i2c_driver *driver)
函数参数和返回值含义如下:
i2c_driver的注册示例代码如下:
示例代码41.2.2.4 i2c_driver注册流程 1 /* i2c驱动的probe函数 */ 2 static int xxx_probe(struct i2c_client *client, const struct i2c_device_id *id) 3 { 4 /* 函数具体程序 */ 5 return 0; 6 } 7 8 /* i2c驱动的remove函数 */ 9 static int ap3216c_remove(struct i2c_client *client) 10 { 11 /* 函数具体程序 */ 12 return 0; 13 } 14 15 /* 传统匹配方式ID列表 */ 16 static const struct i2c_device_id xxx_id[] = { 17 {"xxx", 0}, 18 {} 19 }; 20 21 /* 设备树匹配列表 */ 22 static const struct of_device_id xxx_of_match[] = { 23 { .compatible = "xxx" }, 24 { /* Sentinel */ } 25 }; 26 27 /* i2c驱动结构体 */ 28 static struct i2c_driver xxx_driver = { 29 .probe = xxx_probe, 30 .remove = xxx_remove, 31 .driver = { 32 .owner = THIS_MODULE, 33 .name = "xxx", 34 .of_match_table = xxx_of_match, 35 }, 36 .id_table = xxx_id, 37 }; 38 39 /* 驱动入口函数 */ 40 static int __init xxx_init(void) 41 { 42 int ret = 0; 43 44 ret = i2c_add_driver(&xxx_driver); 45 return ret; 46 } 47 48 /* 驱动出口函数 */ 49 static void __exit xxx_exit(void) 50 { 51 i2c_del_driver(&xxx_driver); 52 } 53 54 module_init(xxx_init); 55 module_exit(xxx_exit);
第16-19行,i2c_device_id,无设备树的时候匹配ID表。
第22-25行,of_device_id,设备树所使用的匹配表。
第28-37行,i2c_driver,当I2C设备和I2C驱动匹配成功以后probe函数就会执行,这些和platform驱动一样,probe函数里面基本就是标准的字符设备驱动那一套了。
I2C设备和驱动的匹配过程是由I2C子系统核心层来完成的,drivers/i2c/i2c-core-base.c就
是I2C的核心部分,I2C核心提供了一些与具体硬件无关的API函数,比如前面讲过的:
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
void i2c_del_adapter(struct i2c_adapter * adap)
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
int i2c_add_driver (struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)
设备和驱动的匹配过程也是由核心层完成的,I2C总线的数据结构为i2c_bus_type,定义在drivers/i2c/i2c-core-base.c文件,i2c_bus_type内容如下:
.match就是I2C总线的设备和驱动匹配函数,在这里就是i2c_device_match函数,此函数内容如下:
第100行,i2c_of_match_device函数用于完成设备树中定义的设备与驱动匹配过程。比较I2C设备节点的compatible属性和of_device_id中的compatible属性是否相等,如果相当的话就表示I2C设备和驱动匹配。
第104行,acpi_driver_match_device函数用于ACPI形式的匹配。
第110行,i2c_match_id函数用于传统的、无设备树的I2C设备和驱动匹配过程。比较I2C设备名字和i2c_device_id的 name字段是否相等,相等的话就说明I2C设备和驱动匹配成功。
上一小节讲解了Linux下的I2C子系统,重点分为I2C适配器驱动和I2C设备驱动,其中I2C适配器驱动就是SoC的I2C控制器驱动。I2C设备驱动是需要用户根据不同的I2C从设备去编写,而I2C适配器驱动一般都是SoC厂商去编写的,比如ST就已经提供了STM3MP21的I2C适配器驱动程序。在内核源码arch/arm/boot/dts/stm32mp151.dtsi设备树文件中找到STM32MP1的I2C控制器节点,节点内容如下所示:
重点关注i2c1节点的compatible属性值,因为通过compatible属性值可以在Linux源码里
面找到对应的驱动文件。这里i2c1节点的compatible属性值“st,stm32mp15-i2c”,在Linux源码中搜索这个字符串即可找到对应的驱动文件。STM32MP1的I2C适配器驱动驱动文件为drivers/i2c/busses/i2c-stm32f7.c,在此文件中有如下内容:
从示例代码41.3.2可以看出,STM32MP1的I2C适配器驱动是个标准的platform驱动,由此可以看出,虽然I2C总线为别的设备提供了一种总线驱动框架,但是I2C适配器却是platform驱动。
第2529行,“st,stm32mp15-i2c”属性值,设备树中i2c1节点的compatible属性值就是与此匹配上的。因此i2c-stm32f7.c文件就是STM32MP1的I2C适配器驱动文件。
第2533行,当设备和驱动匹配成功以后stm32f7_i2c_probe函数就会执行,stm32f7_i2c_probe函数就会完成I2C适配器初始化工作。stm32f7_i2c_probe函数内容如下所示(有省略):
示例代码41.3.3 stm32f7_i2c_probe函数代码段 2106 static int stm32f7_i2c_probe(struct platform_device *pdev) 2107 { 2108 struct stm32f7_i2c_dev *i2c_dev; 2109 const struct stm32f7_i2c_setup *setup; 2110 struct resource *res; 2111 u32 rise_time, fall_time; 2112 struct i2c_adapter *adap; 2113 struct reset_control *rst; 2114 dma_addr_t phy_addr; 2115 int irq_error, ret; 2116 2117 i2c_dev = devm_kzalloc(&pdev->dev, sizeof(*i2c_dev), GFP_KERNEL); 2118 if (!i2c_dev) 2119 return -ENOMEM; 2120 2121 res = platform_get_resource(pdev, IORESOURCE_MEM, 0); 2122 i2c_dev->base = devm_ioremap_resource(&pdev->dev, res); 2123 if (IS_ERR(i2c_dev->base)) 2124 return PTR_ERR(i2c_dev->base); 2125 phy_addr = (dma_addr_t)res->start; 2126 2127 i2c_dev->irq_event = platform_get_irq(pdev, 0); 2128 if (i2c_dev->irq_event <= 0) { 2129 if (i2c_dev->irq_event != -EPROBE_DEFER) 2130 dev_err(&pdev->dev, "Failed to get IRQ event: %d\n", 2131 i2c_dev->irq_event); 2132 return i2c_dev->irq_event ? : -ENOENT; 2133 } 2134 2135 irq_error = platform_get_irq(pdev, 1); 2136 if (irq_error <= 0) { 2137 if (irq_error != -EPROBE_DEFER) 2138 dev_err(&pdev->dev, "Failed to get IRQ error: %d\n", 2139 irq_error); 2140 return irq_error ? : -ENOENT; 2141 } ...... 2159 ret = device_property_read_u32(&pdev->dev, "clock-frequency", 2160 &i2c_dev->bus_rate); 2161 if (ret) 2162 i2c_dev->bus_rate = I2C_STD_RATE; 2163 2164 if (i2c_dev->bus_rate > I2C_FASTPLUS_RATE) { 2165 dev_err(&pdev->dev, "Invalid bus speed (%i>%i)\n", 2166 i2c_dev->bus_rate, I2C_FASTPLUS_RATE); 2167 return -EINVAL; 2168 } ...... 2183 2184 ret = devm_request_threaded_irq(&pdev->dev, i2c_dev->irq_event, 2185 stm32f7_i2c_isr_event, 2186 stm32f7_i2c_isr_event_thread, 2187 IRQF_ONESHOT, 2188 pdev->name, i2c_dev); 2189 if (ret) { 2190 dev_err(&pdev->dev, "Failed to request irq event %i\n", 2191 i2c_dev->irq_event); 2192 goto clk_free; 2193 } 2194 2195 ret = devm_request_irq(&pdev->dev, irq_error, stm32f7_i2c_isr_error, 0, 2196 pdev->name, i2c_dev); 2197 if (ret) { 2198 dev_err(&pdev->dev, "Failed to request irq error %i\n", 2199 irq_error); 2200 goto clk_free; 2201 } 2226 if (i2c_dev->bus_rate > I2C_FAST_RATE) { 2227 ret = stm32f7_i2c_setup_fm_plus_bits(pdev, i2c_dev); 2228 if (ret) 2229 goto clk_free; 2230 } 2231 2232 adap = &i2c_dev->adap; 2233 i2c_set_adapdata(adap, i2c_dev); 2234 snprintf(adap->name, sizeof(adap->name), "STM32F7 I2C(%pa)", 2235 &res->start); 2236 adap->owner = THIS_MODULE; 2237 adap->timeout = 2 * HZ; 2238 adap->retries = 3; 2239 adap->algo = &stm32f7_i2c_algo; 2240 adap->dev.parent = &pdev->dev; 2241 adap->dev.of_node = pdev->dev.of_node; 2242 2243 init_completion(&i2c_dev->complete); 2244 2245 /* Init DMA config if supported */ 2246 i2c_dev->dma = stm32_i2c_dma_request(i2c_dev->dev, phy_addr, 2247 STM32F7_I2C_TXDR, 2248 STM32F7_I2C_RXDR); 2249 if (PTR_ERR(i2c_dev->dma) == -ENODEV) 2250 i2c_dev->dma = NULL; 2251 else if (IS_ERR(i2c_dev->dma)) { 2252 ret = PTR_ERR(i2c_dev->dma); 2253 goto fmp_clear; 2254 } ...... 2276 stm32f7_i2c_hw_config(i2c_dev); 2277 2278 ret = i2c_add_adapter(adap); 2279 if (ret) 2280 goto pm_disable; ...... 2307 return 0; ...... 2340 }
第2117行,ST使用stm32f7_i2c_dev结构体来表示STM32MP1系列SOC的I2C控制器,这里使用devm_kzalloc函数来申请内存。
第2121-2122行,调用platform_get_resource函数从设备树中获取I2C1控制器寄存器物理基地址,也就是0x40012000。获取到寄存器基地址以后使用devm_ioremap_resource函数对其进行内存映射,得到可以在Linux中使用的虚拟地址。
第2127行和第2135行,调用platform_get_irq函数获取中断号。
第2159-2160行,设置I2C频率默认为I2C_STD_RATE=100KHz,如果设备树节点设置了“clock-frequency”属性的话I2C频率就使用clock-frequency属性值。
第2184-2196行,注册I2C控制器的两个中断。
第2232-2241行,stm32f7_i2c_dev结构体有个adap的成员变量,adap就是i2c_adapter,这里初始化i2c_adapter。第2239行设置i2c_adapter的algo成员变量为stm32f7_i2c_algo,也就是设置i2c_algorithm。
第2246行,申请DMA,看来STM32MP1的I2C适配器驱动是可以采用DMA方式。
第2276行,调用stm32f7_i2c_hw_config函数初始化I2C1控制器的相关硬件寄存器。
第2278行,调用i2c_add_adapter函数向Linux内核注册i2c_adapter。
stm32f7_i2c_probe函数主要的工作就是一下两点:
stm32f7_i2c_algo包含I2C1适配器与I2C设备的通信函数master_xfer,stm32f7_i2c_algo 结构体定义如下:
先来看一下.functionality,functionality用于返回此I2C适配器支持什么样的通信协议,在这里functionality就是stm32f7_i2c_func函数,stm32f7_i2c_func函数内容如下:
重点来看一下stm32f7_i2c_xfer函数 ,因为最终就是通过此函数来完成与I2C设备通信的,此函数内容如下:
示例代码41.3.6stm32f7_i2c_xfer函数 1657 static int stm32f7_i2c_xfer(struct i2c_adapter *i2c_adap, 1658 struct i2c_msg msgs[], int num) 1659 { 1660 struct stm32f7_i2c_dev *i2c_dev = i2c_get_adapdata(i2c_adap); 1661 struct stm32f7_i2c_msg *f7_msg = &i2c_dev->f7_msg; 1662 struct stm32_i2c_dma *dma = i2c_dev->dma; 1663 unsigned long time_left; 1664 int ret; 1665 1666 i2c_dev->msg = msgs; 1667 i2c_dev->msg_num = num; 1668 i2c_dev->msg_id = 0; 1669 f7_msg->smbus = false; 1670 1671 ret = pm_runtime_get_sync(i2c_dev->dev); 1672 if (ret < 0) 1673 return ret; 1674 1675 ret = stm32f7_i2c_wait_free_bus(i2c_dev); 1676 if (ret) 1677 goto pm_free; 1678 1679 stm32f7_i2c_xfer_msg(i2c_dev, msgs); 1680 1681 time_left = wait_for_completion_timeout(&i2c_dev->complete, 1682 i2c_dev->adap.timeout); 1683 ret = f7_msg->result; 1684 1685 if (!time_left) { 1686 dev_dbg(i2c_dev->dev, "Access to slave 0x%x timed out\n", 1687 i2c_dev->msg->addr); 1688 if (i2c_dev->use_dma) 1689 dmaengine_terminate_all(dma->chan_using); 1690 ret = -ETIMEDOUT; 1691 } 1692 1693 pm_free: 1694 pm_runtime_mark_last_busy(i2c_dev->dev); 1695 pm_runtime_put_autosuspend(i2c_dev->dev); 1696 1697 return (ret < 0) ? ret : num; 1698 }
第1675行,调用stm32f7_i2c_wait_free_bus函数等待I2C总线空闲,也就是读取I2C控制的ISR寄存器的bit15(BUSY)位,此位用来标记I2C控制器是否忙。
第1679行,调用stm32f7_i2c_xfer_msg函数发送数据,此函数也是操作I2C控制器硬件寄存器的。
I2C适配器驱动SOC厂商已经编写好了,需要做的就是编写具体的设备驱动,本小节就来学习一下I2C设备驱动的详细编写流程。
首先肯定要描述I2C设备节点信息,先来看一下没有使用设备树的时候是如何在BSP里面描述I2C设备信息的,在未使用设备树的时候需要在BSP里面使用i2c_board_info结构体来描述一个具体的I2C设备。i2c_board_info结构体如下:
type和addr这两个成员变量是必须要设置的,一个是I2C设备的名字,一个是I2C设备的器件地址。举个例子,打开arch/arm/mach-imx/mach-armadillo5x0.c文件,此文件中有关于s35390a这个I2C器件对应的设备描述信息:
示例代码41.4.1.2中使用I2C_BOARD_INFO来完成armadillo5x0_i2c_rtc的初始化工作,I2C_BOARD_INFO是一个宏,定义如下:
可以看出I2C_BOARD_INFO宏其实就是设置i2c_board_info的type和addr这两个成员变量,因此示例代码41.4.1.2的主要工作就是设置I2C设备名字为s35390a,器件地址为0X30。
可以在Linux源码里面全局搜索i2c_board_info,会找到大量以i2c_board_info定义的I2C设备信息,这些就是未使用设备树的时候I2C设备的描述方式,当采用了设备树以后就不会再使用i2c_board_info来描述I2C设备了。
使用设备树的时候I2C设备信息通过创建相应的节点就行了,比如在STM32MP1的开发板上有一个I2C器件AP3216C,这是三合一的环境传感器,并且该器件挂在STM32MP1的I2C5总线接口上,因此必须在i2c5节点下创建一个子节点来描述AP3216C设备,节点示例如下所示:
示例代码41.4.1.4 i2c从设备节点示例
1 &i2c5 {
2 pinctrl-names = "default", "sleep";
3 pinctrl-0 = <&i2c5_pins_a>;
4 pinctrl-1 = <&i2c5_pins_sleep_a>;
5 status = "okay";
6
7 ap3216c@1e {
8 compatible = " alientek,ap3216c";
9 reg = <0x1e>;
10 };
11 };
第2-4行,设置了i2c5的pinmux的配置。
第7-10行,向i2c5添加ap3216c子节点,第7行“ap3216c@1e”是子节点名字 ,“@”后面的“1e”就是ap3216c的I2C器件地址。第8行设置compatible属性值为“alientek,ap3216c”。
第9行的reg属性也是设置ap3216c的器件地址的,因此值为0x1e。
I2C设备节点的创建重点是compatible属性和reg属性的设置,一个用于匹配驱动,一个用于设置器件地址。
I2C设备驱动首先要做的就是初始化i2c_driver并向Linux内核注册。当设备和驱动匹配以后i2c_driver里面的probe函数就会执行,probe函数里面所做的就是字符设备驱动那一套了。一般需要在probe函数里面初始化I2C设备,要初始化I2C设备就必须能够对I2C设备寄存器进行读写操作,这里就要用到i2c_transfer函数了。i2c_transfer函数最终会调用I2C适配器中i2c_algorithm里面的master_xfer函数,对于STM32MP1而言就是stm32f7_i2c_xfer这个函数。i2c_transfer函数原型如下:
int i2c_transfer(struct i2c_adapter *adap,
struct i2c_msg *msgs,
int num)
函数参数和返回值含义如下:
重点来看一下msgs这个参数,这是一个i2c_msg类型的指针参数,I2C进行数据收发就是消息的传递,Linux内核使用i2c_msg结构体来描述一个消息。i2c_msg结构体定义在include/uapi/linux/i2c.h文件中,结构体内容如下:
使用i2c_transfer函数发送数据之前要先构建好i2c_msg,使用i2c_transfer进行I2C数据收
发的示例代码如下:
示例代码41.4.2.2 I2C设备多寄存器数据读写 1 /* 设备结构体 */ 2 struct xxx_dev { 3 ...... 4 void *private_data; /* 私有数据,一般会设置为i2c_client */ 5 }; 6 7 /* 8 * @description : 读取I2C设备多个寄存器数据 9 * @param – dev : I2C设备 10 * @param – reg : 要读取的寄存器首地址 11 * @param – val : 读取到的数据 12 * @param – len : 要读取的数据长度 13 * @return : 操作结果 14 */ 15 static int xxx_read_regs(struct xxx_dev *dev, u8 reg, void *val, int len) 16 { 17 int ret; 18 struct i2c_msg msg[2]; 19 struct i2c_client *client = (struct i2c_client *) dev->private_data; 20 21 /* msg[0],第一条写消息,发送要读取的寄存器首地址 */ 22 msg[0].addr = client->addr; /* I2C器件地址 */ 23 msg[0].flags = 0; /* 标记为发送数据 */ 24 msg[0].buf = ® /* 读取的首地址 */ 25 msg[0].len = 1; /* reg长度 */ 26 27 /* msg[1],第二条读消息,读取寄存器数据 */ 28 msg[1].addr = client->addr; /* I2C器件地址 */ 29 msg[1].flags = I2C_M_RD; /* 标记为读取数据 */ 30 msg[1].buf = val; /* 读取数据缓冲区 */ 31 msg[1].len = len; /* 要读取的数据长度 */ 32 33 ret = i2c_transfer(client->adapter, msg, 2); 34 if(ret == 2) { 35 ret = 0; 36 } else { 37 ret = -EREMOTEIO; 38 } 39 return ret; 40 } 41 42 /* 43 * @description : 向I2C设备多个寄存器写入数据 44 * @param – dev : 要写入的设备结构体 45 * @param – reg : 要写入的寄存器首地址 46 * @param – val : 要写入的数据缓冲区 47 * @param – len : 要写入的数据长度 48 * @return : 操作结果 49 */ 50 static s32 xxx_write_regs(struct xxx_dev *dev, u8 reg, u8 *buf, u8 len) 51 { 52 u8 b[256]; 53 struct i2c_msg msg; 54 struct i2c_client *client = (struct i2c_client *) dev->private_data; 55 56 b[0] = reg; /* 寄存器首地址 */ 57 memcpy(&b[1],buf,len); /* 将要发送的数据拷贝到数组b里面 */ 58 59 msg.addr = client->addr; /* I2C器件地址 */ 60 msg.flags = 0; /* 标记为写数据 */ 61 62 msg.buf = b; /* 要发送的数据缓冲区 */ 63 msg.len = len + 1; /* 要发送的数据长度 */ 64 65 return i2c_transfer(client->adapter, &msg, 1); 66 }
第2-5行,设备结构体,在设备结构体里面添加一个执行void的指针成员变量private_data,此成员变量用于保存设备的私有数据。在I2C设备驱动中一般将其指向I2C设备对应的i2c_client。
第15-40行,xxx_read_regs函数用于读取I2C设备多个寄存器数据。第18行定义了一个i2c_msg数组,2个数组元素,因为I2C读取数据的时候要先发送要读取的寄存器地址,然后再读取数据,所以需要准备两个i2c_msg。一个用于发送寄存器地址,一个用于读取寄存器值。对于msg[0],将flags设置为0,表示写数据。msg[0]的addr是I2C设备的器件地址,msg[0]的buf成员变量就是要读取的寄存器地址。对于msg[1],将flags设置为I2C_M_RD,表示读取数据。msg[1]的buf成员变量用于保存读取到的数据,len成员变量就是要读取的数据长度。调用i2c_transfer函数完成I2C数据读操作。
第50-66行,xxx_write_regs函数用于向I2C设备多个寄存器写数据,I2C写操作要比读操作简单一点,因此一个i2c_msg即可。数组b用于存放寄存器首地址和要发送的数据,第59行设置msg的addr为I2C器件地址。第60行设置msg的flags为0,也就是写数据。第62行设
置要发送的数据,也就是数组b。第63行设置msg的len为len+1,因为要加上一个字节的寄存器地址。最后通过i2c_transfer函数完成向I2C设备的写操作。
另外还有两个API函数分别用于I2C数据的收发操作,这两个函数最终都会调用i2c_transfer。
首先来看一下I2C数据发送函数i2c_master_send,函数原型如下:
int i2c_master_send(const struct i2c_client *client,
const char *buf,
int count)
函数参数和返回值含义如下:
I2C数据接收函数为i2c_master_recv,函数原型如下:
int i2c_master_recv(const struct i2c_client *client,
char *buf,
int count)
函数参数和返回值含义如下:
关于Linux下I2C设备驱动的编写流程就讲解到这里,重点就是i2c_msg的构建和i2c_transfer函数的调用,接下来就编写AP3216C这个I2C设备的Linux驱动。
AP3216C的原理图如下图所示:
从上图可以看出AP3216C使用的是I2C5,其中I2C5_SCL使用的是PA11这个IO,I2C_SDA使用的是PA12这个IO。AP3216C还有个中断引脚,这里没有用到中断功能。
AP3216C用到了I2C5接口。因为I2C5所使用的IO分别为PA11和PA12,所以要根据数据手册设置I2C5的pinmux的配置。如果要用到AP3216C的中断功能的话还需要初始化AP_INT对应的PE4这个 IO,本章实验不使用中断功能。因此只需要设置PA11和PA12这两个IO复用为AF4功能,ST其实已经将这个两个IO设置好了,打开stm32mp15-pinctrl.dtsi
然后找到如下内容:
示例代码41.6.1.1 I2C5的pinmux配置 1 i2c5_pins_a: i2c5-0 { 2 pins { 3 pinmux = <STM32_PINMUX('A', 11, AF4)>, /* I2C5_SCL */ 4 <STM32_PINMUX('A', 12, AF4)>; /* I2C5_SDA */ 5 bias-disable; 6 drive-open-drain; 7 slew-rate = <0>; 8 }; 9 }; 10 11 i2c5_pins_sleep_a: i2c5-1 { 12 pins { 13 pinmux = <STM32_PINMUX('A', 11, ANALOG)>, /* I2C5_SCL */ 14 <STM32_PINMUX('A', 12, ANALOG)>; /* I2C5_SDA */ 15 16 }; 17 };
示例代码41.6.1.1中,定义了I2C5接口的两个pinmux配置分别为:i2c5_pins_a和i2c5_pins_sleep_a。第一个默认的状态下使用,第二个是在sleep状态下使用。
接着打开stm32mp157d-atk.dts文件,通过节点内容追加的方式,向i2c5节点中添加“ap3216c@1e”子节点,节点如下所示:
示例代码41.6.1.2 向i2c5追加ap3216c子节点
1 &i2c5 {
2 pinctrl-names = "default", "sleep";
3 pinctrl-0 = <&i2c5_pins_a>;
4 pinctrl-1 = <&i2c5_pins_sleep_a>;
5 status = "okay";
6
7 ap3216c@1e {
8 compatible = "alientek,ap3216c";
9 reg = <0x1e>;
10 };
11 };
第2-4行,给I2C5节点设置了pinmux配置。
第7行,ap3216c子节点,@后面的“1e”是ap3216c的器件地址。
第8行,设置compatible值为“alientek,ap3216c”。
第9行,reg属性也是设置ap3216c器件地址的,因此reg设置为0x1e。
设备树修改完成以后使用“make dtbs”重新编译一下,然后使用新的设备树启动Linux内核。/sys/bus/i2c/devices目录下存放着所有I2C设备,如果设备树修改正确的话,会在/sys/bus/i2c/devices目录下看到一个名为“0-001e”的子目录,如下图所示:
上图中的“0-001e”就是ap3216c的设备目录,“1e”就是ap3216c器件地址。进入0-001e目录,可以看到“name”文件,name文件保存着此设备名字,在这里就是“ap3216c”。
需要先创建一个ap3216creg.h文件,是一个寄存器头文件,要在其中保存寄存器的地址。
ap3216c.c就是正式的驱动程序。
首先创建设备结构体,与之前的字符设备区别就是,需要添加一个i2c_client结构体指针*client表示i2c设备,以及最后要添加光传感器的unsigner short变量ir,als,ps。
接着编写ap3216c_read_regs来读取寄存器数据,这个与之前源码解读中的内容是很类似的,创建一个i2c_msg结构体类型的数组msg[2],msg[0]用来保存发送的首地址,msg[1]保存接收的首地址;然后调用i2c_transfer进行传输。
编写ap3216c_write_regs来写入寄存器,同样创建一个i2c_msg结构体类型的msg,并定义一个u8的b[256]数组,其保存寄存器首地址,并通过memcpy把数据拷贝到b中;msg则是设子ap3216c的地址,标记,buf就是刚才的b,写入长度是len+1(还有寄存器地址需要写入),最后直接return出来i2c_transfer。
然后进行封装,ap3216c_read_reg里面调用刚写好的ap3216c_read_regs;ap3216c_write_reg调用ap3216c_write_regs。
之后编写ap3216c_readdata来读取AP3216C的数据,这里就是要注意数据读取需要有大于112.5ms的时间间隔,这边应该就是根据这个传感器的手册来编写的读取的函数。
接着编写ap3216c_open这个打开设备的函数,首先要通过filp获取cdev指针,再通过cdev获取ap3216c_dev的首地址;然后就是通过ap3216c_write_reg进行传感器的初始化。
接着编写ap3216c_read从设备读取数据,同样的方法获取ap3216c_dev的首地址之后,通过ap3216c_readdata读取,然后传入自定义的short类型的data[3]数组,之后copy_to_user读取。
关闭设备就是release函数,里面直接return 0就可以。
操作函数集file_operations就是open、read和release函数。
接着就是probe函数ap3216c_probe,里面需要通过devm_kzalloc申请ap3216cdev的空间,然后就是注册字符设备驱动的常规操作,alloc_chrdev_region创建设备号,然后ap3216cdev->cdev.owner就是THIS_MODULE,cdev_init初始化cdev,cdev_add添加cdev,然后class_create创建类,device_create创建设备;区别是,最后要i2c_set_clientdata保存一下ap3216cdev结构体。
驱动的remove函数,需要先i2c_get_clientdata获取cdev,然后就是老样子cdev_del删除cdev,unregister_chrdev_region注销设备号,device_destroy注销设备然后class_destroy注销类。
建立一个ID的匹配列表,i2c_device_id结构体类型的ap3216c_id[]的数组,里面就是匹配的"alientek,ap3216c";还有一个of_device_id结构体类型的ap3216c_of_match[]数组保存.compatible属性。
建立i2c驱动结构体i2c_driver结构体类型的ap3216c_driver,保存.probe和.remove函数,.driver就是.owner属性,.name以及.of_match_table属性,最后要加一个.id_table。(这里如果使用设备树,这个id_table是可以不用的,那个是传统的没有设备数的时候才需要的)
最后就是驱动的入口和出口函数,这里分别是ap3216c_init调用i2c_add_driver;以及ap3216c_exit调用i2c_del_driver。
最最后面就是module_init和module_exit,以及MODULE_LICENSE、MODULE_AUTHER以及MODULE_INFO。
传入的argc是2个。
filename=argv[1]之后,open打开字符设备,然后通过while(1)死循环,read得到数据并分别把3个传感器数据保存,死循环外面再加一个close即可。
这里还是一样,就把Makefile的obj-m改成ap3216c.o,然后“make”就可以了。
可以通过如下命令:
arm-none-linux-gnueabihf-gcc ap3216cApp.c -o ap3216cApp |
将上一小节编译出来ap3216c.ko和ap3216cApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中。输入如下命令加载ap3216c.ko这个驱动模块:
depmod //第一次加载驱动的时候需要运行此命令 modprobe ap3216c //加载驱动模块 |
加载成功以后,可以通过如下命令测试:
./ap3216cApp /dev/ap3216c |
测试APP会不断从AP3216C中读取数据,并打印到终端,如下图所示:
与裸机开发还是有所区别,在裸机开发的时候,当时的说法是IIC因为专利等问题,一般都是不会直接用硬件IIC的,当时是直接手动GPIO来软件实现IIC的。
在STM32MP1这边,I2C就是直接采用硬件I2C,需要关注的就是怎么在设备树中添加i2c节点,然后驱动程序的写法就可以了。
pinctrl一般都是会有写好的,就是要在自己的设备树里面添加对应的i2c节点。
驱动程序中,具体的传感器读取,需要参考传感器的使用文档,I2C的读写在本篇笔记中是有一个模板的,只要把他搬过去就可以了;至于驱动程序的其他部分,基本就是字符设备的基本驱动代码,区别就是获取设备首地址的时候,要先通过cdev=filp->f_path.dentry->d_inode->i_cdev; 获取cdev首地址之后,再通过container_of获取传感器设备的首地址。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。