上一篇,我介绍了如何在uboot中添加i2c设备,以及移植i2c的读写接口。简单来说uboot阶段使用i2c设备和平台关联性比较大,但不同平台套路是差不多的。你可以将uboot阶段看作是引导android系统起来的另外一个系统。而系统起来后在kenerl中添加i2c设备就和uboot阶段差别非常大,内容要多很多。我仍然不会贴一大段一大段的代码上来,这没什么意义。我的目的更多的是给没玩过i2c的小伙伴看完这篇介绍后,能有个方向。
内容主要分两个方面,第一有关i2c的概念。第二在内核中添加i2c设备并实现读写的方法和流程。
首先来看第一方面,这涉及到几个点:
1. 设备树概念。
2. i2c总线概念。
3. i2c驱动
4. i2c控制器adapter
以上三点自行百度可以找到很详细的资料,在这里我只简单说明下:
1. 设备树你可以理解为它是用来专门保存板子上各个硬件设备信息的一个文件。比如某个设备GPIO口是多少,状态是否为打开,寄存器地址多少等等。你可能需要这这里添加你的设备信息。
2. i2c驱动你可以简单理解为对于应用层来说,驱动就是应用层沟通底层硬件设备的桥梁(i2c-内核层),应用层会调用驱动层的接口来沟通底层硬件。驱动主要负责拿到上层要对硬件操作的数据,然后提供给i2c控制器对硬件进行读写。
3. 总线就是负责匹配驱动与设备树硬件信息及adapter信息。
4. i2c控制器adapter和硬件相关,比如amlogic平台有5组i2c控制器。它的作用是从驱动中拿到数据后负责将数据写入硬件中。
我没有仔细去研究底层代码,大概的看了下,总的来说我的理解是设备树中包含设备信息还有当前设备所在的adapter。在加载kernel的过程中会被拿出来,通过总线匹配到相对应的i2c驱动(匹配的方式是compatible name), 匹配完后,客户就可以通过i2c驱动中提供的接口,利用匹配的i2c adapter对i2c设备进行读写。
概念简单介绍完了,接下来就是重点如何在内核中添加i2c设备并实现读写。
通常在kernel中添加i2c设备的方式一般有两种:
方式一:(不推荐) 自己在kernel中写一个驱动,然后给接口上层去调用。如果你打算自己写个驱动,那么kernel目录下的Documentation\i2c中的文件说明可以帮助你。写完驱动后你还需要在dts中,需要配置硬件的信息如下:(参考dts文件路径,内核目录下:arch\arm64\boot\dts\amlogic)
&i2c_ao {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&d_i2c_master>;
at24 {
dev_name = "at24";
reg = <0x50>;
pagesize = <8>;
........
};
};
这个dts的意思是,在i2c d组下,挂载了at24这个设备,kernel中的at24的驱动,会调用到这个dts。
对于i2c控制器本身信息也要在添加在dtsi文件中如下:(参考路径:arch/arm64/boot/dts/amlogic/mesongxl.dtsi 注意文件名是dtsi我没有写错)
i2c_ao: i2c@c8100500{
compatible = "amlogic, meson-i2c";
dev_name = "i2c-AO";
status = "disabled";
reg = <0x0 0xc8100500 0x0 0x1d>;
device_id = <0>;
pinctrl-names="default";
pinctrl-0=<&ao_i2c_master>;
#address-cells = <1>;
#size-cells = <0>;
use_pio = <0>;
master_i2c_speed = <400000>;
clocks = <&clock CLK_81>;
clock-names = "clk_i2c";
resets = <&clock GCLK_IDX_I2C>;
}
总的来说,如果使用这个方式大概流程如下:
1. 在设备树中打开你设备挂载的那个i2c控制器.
2. 在设备树中的那组i2c控制器中添加上设备相关的信息。
3. 自己写个驱动,比如申请设备号,创建设备文件,创建设备节点,加入总线,在驱动里面要写提供给上层调用的接口,比如open设备节点,i2c_read,i2c_write等。
4. 同样在系统层要控制某个设备都是以一个进程的形式(进程的意思就是相关代码C应用,可执行程序或者android APP),在这个进程里面会去调用自己在驱动中写的open、read、write来对设备进行读写操作。
这个方式不太推荐,特别对于平台工程师而言,很多平台工程师对于驱动是只见过猪跑,没真正吃过猪肉,平时驱动看的多,但让真写一个完整的驱动可能要花很多时间去搞一些细节。所以我更加倾向于第二种方式。
方式二:(推荐)
这种方式是大多数平台在移植i2c设备的时候使用的方式,开发效率快。对设备的操作的调用也同样是在进程里面,不同于方式一的是直接通过调用平台封装好的ioctl来进行读写,而不需要自己去写驱动。这个就和平台相关了,注意不同平台使用ioctrl进行读写只是过程有些差异,但使用ioctrl来读写这种方式是个标准。
我会先详细介绍这个方式的流程然后再对比方式一就豁然开朗了,流程如下:
1. 在设备树dts文件中添加以下信息:
&i2c_ao {
status = "okay";
};
在dtsi文件中添加以下信息
i2c_ao: i2c@c8100500{
compatible = "amlogic, meson-i2c";
dev_name = "i2c-AO";
status = "disabled";
reg = <0x0 0xc8100500 0x0 0x1d>;
device_id = <0>;
pinctrl-names="default";
pinctrl-0=<&ao_i2c_master>;
#address-cells = <1>;
#size-cells = <0>;
use_pio = <0>;
master_i2c_speed = <400000>;
clocks = <&clock CLK_81>;
clock-names = "clk_i2c";
resets = <&clock GCLK_IDX_I2C>;
}
2. 在操作i2c设备的进程的代码中加入打开设备节点代码,如下:
int i2c_open()
{
// printf("i2c_open()\n");
if((i2c_fd = open(DEVICE_NAME, O_RDWR)) == -1)
{
printf("i2c open %s is fail.\n",DEVICE_NAME);
return fail;
}else{
i2c_data.nmsgs=2;
i2c_data.msgs=(struct i2c_msg*)malloc(i2c_data.nmsgs*sizeof(struct i2c_msg));
if(!i2c_data.msgs){
printf("i2c_date malloc error \n");
close(i2c_fd);
exit(1);
}
ret = ioctl(i2c_fd, I2C_TIMEOUT, 2);//set timeout ,2*10ms
if(ret <0){
printf("i2c_open ioctl set timeout is fail! \n");
}
ret = ioctl(i2c_fd, I2C_RETRIES, 1);//set resend times
if(ret <0){
printf("i2c_open ioctl set resend times is fail! \n");
}
return i2c_fd;
}
}
3. 然后再调用ioctrl进行读写。这一步和平台相关,需要在源码中找demo,模仿demo的读写接口写就行了,源码找不到去找原厂工程师提供一个成功案例的历程仿照着写。我在这里提供amlogic使用ioctrl进行写的代码:
AAA_u1 i2c_read_byte( AAA_u8 slaveAddr, AAA_u8 subAddr, AAA_u8 len, AAA_u8* dataBuf, AAA_u8 device )
{
if(len > 4){
printf("dataLength is invalid\n");
return fail;
}
slaveAddr = slaveAddr >> 1 ;
int count = 0;
i2c_data.nmsgs=2;
(i2c_data.msgs[0]).len=1;
(i2c_data.msgs[0]).addr=slaveAddr;
(i2c_data.msgs[0]).flags=0;
(i2c_data.msgs[0]).buf=(unsigned char*)malloc(1);
(i2c_data.msgs[0]).buf =(unsigned char*)&subAddr;
(i2c_data.msgs[1]).len = len;
(i2c_data.msgs[1]).addr = slaveAddr;
(i2c_data.msgs[1]).flags = I2C_M_RD;
(i2c_data.msgs[1]).buf = (unsigned char*)malloc(len * sizeof(unsigned char));
(i2c_data.msgs[1]).buf = dataBuf;
if(ioctl(i2c_fd,I2C_RDWR,(unsigned long)&i2c_data)<0){
printf("ioctl read error. ret = %d \n",ioctl(i2c_fd,I2C_RDWR,(unsigned long)&i2c_data));
printf("ioctrl error : %s \n",strerror(errno));
return fail;
}
if(DEBUG){
printf("**slaveAddr=0x%x,subAddr=0x%x**\n",(i2c_data.msgs[0]).addr,(i2c_data.msgs[0]).buf[0]);
for(count=0;count < (i2c_data.msgs[1]).len;count++)
printf("***read dataBuf[%d] = 0x%x***\n",count,(i2c_data.msgs[1]).buf[count]);
}
return success;
}
我是最不愿意贴一大段代码进来的,我自己都看的都恶心,但是在这里贴进来做个借鉴。注意,不要复制上面这段代码去贴进你的读操作里去改,没有任何意义,我可以告诉你在之前的MTK平台使用ioctrl进行读操作和现在这个amlogic平台是有差异的。最快的方式还是去自己平台上找demo,要么直接问原厂要。
从上面的读操作中可以看出,所有的设备信息都是放在i2c_data这个对象里面的,然后调用了ioctl(i2c_fd,I2C_RDWR,(unsigned long)&i2c_data)就完成了读写。和方式一相比:你不用去写驱动,驱动中需要各种注册设备号,节点太麻烦!只需要打开设备所在的那组i2c控制器,在进程代码中读写i2c前打开与i2c匹配的设备节点即可。
对驱动有些了解的小伙伴可能会有疑问:如果当前dev/中不止一个设备节点,那怎么知道到底使用哪个设备节点。如果不注册设备驱动,哪来的设备节点。其实是这样的,还是那句话,平台会帮你搭建好,一般如果你在dts中打开了第ao组i2c控制器,那么相对应你会发现/dev中会自动创建一个设备节点与其相对应比如i2c-0,你再开打第B组i2c控制器,/dev下又会多一个设备节点i2c-2。
使用这种方式的核心思维在于:你只需要是站在应用的角度去使用它,从需要操作i2c读写设备的进程代码中去设置我的从设备信息,然后调用ioctrl即可实现i2c的读写,而完全不用去理会底层做了些什么。
好了,写了一大堆,好累... 如果有什么说的不对的地方,欢迎指正,相互学习。有兴趣的小伙伴可以去追一追系统中的源码...