前言:
对于学习嵌入式来说,i2c 协议肯定是必须重点掌握的,平时大家在工作学习中接触的tp,camera,sensor等很多外设都是iic接口的,在调试这些设备之时,我们不用去关心i2c总线驱动,因为芯片厂商已经帮你解决了,但是当你在芯片公司工作,或者为了更好的理解整个i2c协议,那么我们就有必要去自己写一个IIc总线驱动程序, 那么在自己完全写一个或者根据芯片手册完全移植一个总线驱动时,完全理解内核自带的 I2c-s3c2410.c 是最重要的,本文就是通过对 I2c-s3c2410.c的全面解析,结合友善的mach-mini210.c来彻底分析i2c 总线驱动程序,并利用分析的结果自己写一个属于自己的总线驱动。
一.准备:
1.驱动位置,驱动的添加删除
i2c总线驱动位于目录drivers\i2c\busses\ 目录下,通过查看该目录下的Makefile:
obj-$(CONFIG_I2C_S3C2410) += i2c-s3c2410.o
然后make menuconfig 搜索CONFIG_I2C_S3C2410这个宏,就可以看到以下信息:
Symbol: I2C_S3C2410 [=y] |
Type : tristate |
Prompt: S3C2410 I2C Driver |
Defined at drivers/i2c/busses/Kconfig:576 |
Depends on: I2C [=y] && HAVE_S3C2410_I2C [=y] |
Location: |
-> Device Drivers |
-> I2C support (I2C [=y]) |
-> I2C Hardware Bus support
将symbol改为M 就可以该模块方便进行加载调试。
2.打印调试信息
在文件开头添加 #define DEBUG 1 这个宏,然后在开发板上执行cat /proc/kmsg 就能看到dev_dbg打印出的调试信息了,
可以很方便调试。
#include <linux/kernel.h>
#include <linux/module.h>
#define DEBUG 1
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/time.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/err.h>
#include <linux/platform_device.h>
二 代码分析:
1. 入口函数:
linux下遵循设备,总线,驱动的这一套规则,对于driver对应I2c-s3c2410.c
static struct platform_device_id s3c24xx_driver_ids[] = {
{
.name = "s3c2410-i2c", // name 来匹配 dev
.driver_data = TYPE_S3C2410,
}, {
.name = "s3c2440-i2c",
.driver_data = TYPE_S3C2440,
}, { },
};
MODULE_DEVICE_TABLE(platform, s3c24xx_driver_ids);
static struct platform_driver s3c24xx_i2c_driver = {
.probe = s3c24xx_i2c_probe,
.remove = s3c24xx_i2c_remove,
.id_table = s3c24xx_driver_ids,
.driver = {
.owner = THIS_MODULE,
.name = "s3c-i2c",
.pm = S3C24XX_DEV_PM_OPS,
},
};
static int __init i2c_adap_s3c_init(void)
{
return platform_driver_register(&s3c24xx_i2c_driver); // 注册driver
}
这里通过platform_driver_register 注册platform_driver 结构体 ,再来看看driver对应的设备 通过搜"s3c2410-i2c",
对应的设备在arch\arm\plat-samsung\Dev-i2c0.c这个文件中:
struct platform_device s3c_device_i2c0 = {
.name = "s3c2410-i2c",
#ifdef CONFIG_S3C_DEV_I2C1
.id = 0,
#else
.id = -1,
#endif
.num_resources = ARRAY_SIZE(s3c_i2c_resource),
.resource = s3c_i2c_resource,
};
这个文件中包括了i2c 总线的设备资源包括中断号,寄存器地址,私有数据等。 而注册platform_device 是在arch\arm\mach-s5pv210\Mach-mini210.c文件中注册的:static struct platform_device *mini210_devices[] __initdata = {
.....
&s3c_device_hsmmc3,
&s3c_device_i2c0, // i2c 控制器 0
&s3c_device_i2c1,
platform_add_devices(mini210_devices, ARRAY_SIZE(mini210_devices));// 注册设备
有了这些就能调用platform_driver的probe了。
2.probe函数
2.1 i2c_adapter(分配 设置 注册)
static int s3c24xx_i2c_probe(struct platform_device *pdev)
{
struct s3c24xx_i2c *i2c;
struct s3c2410_platform_i2c *pdata;
struct resource *res;
int ret;
pdata = pdev->dev.platform_data; // 获得私有数据
if (!pdata) {
dev_err(&pdev->dev, "no platform data\n");
return -EINVAL;
}
i2c = kzalloc(sizeof(struct s3c24xx_i2c), GFP_KERNEL); // 分配s3c24xx_i2c结构体
if (!i2c) {
dev_err(&pdev->dev, "no memory for state\n");
return -ENOMEM;
}
/*设置s3c24xx_i2c 结构体 */
strlcpy(i2c->adap.name, "s3c2410-i2c", sizeof(i2c->adap.name));
i2c->adap.owner = THIS_MODULE;
i2c->adap.algo = &s3c24xx_i2c_algorithm; // 算法操作
i2c->adap.retries = 2;
i2c->adap.class = I2C_CLASS_HWMON | I2C_CLASS_SPD; // 类
i2c->tx_setup = 50;
/* 辅助变量的初始化 */
spin_lock_init(&i2c->lock);
init_waitqueue_head(&i2c->wait);
一开始呢,probe函数会做一些软件方面的初始化,这里s3c24xx_i2c 这个结构体就是代表我们的i2cbus总线了, 里面有一个结构体i2c_adapter那是相当重要了,称之为i2c适配器,一般就是指i2c控制器,可以说是驱动的核心,这里会对它进行些设置,最重要的是设置i2c_algorithm成员, 这里我们可以把它理解成算法函数。
i2c->adap.algo = &s3c24xx_i2c_algorithm; // 算法操作
static const struct i2c_algorithm s3c24xx_i2c_algorithm = {
.master_xfer = s3c24xx_i2c_xfer, // i2c 传输
.functionality = s3c24xx_i2c_func,
};
s3c24xx_i2c_xfer这个函数就是i2c_adapter这个i2c控制器数据传输的核心函数,为什么这么说呢?
在我们的linux驱动中,i2c设备比如触摸屏,at24c08数据传输时都会通过i2c_smbus_write_byte_data 或者i2c_transfer等来实现,而master_xfer即s3c24xx_i2c_xfer 的实现就为这些数据传输函数提供了基础: 这里我们可以粗略的看下它们之间的调用关系。
i2c_smbus_write_byte_data // i2c设备读写数据常用函数
i2c_smbus_xfer // 下面几个函数都是在i2c-core.c中调用的
i2c_smbus_xfer_emulated
i2c_transfer
ret = adap->algo->master_xfer(adap, msgs, num);//master_xfer是在 i2c_bus总线中设置
所以说,不管你的i2c设备调用什么函数进行数据传输,最终都会调用到i2c_adapter适配器设置的master_xfer即s3c24xx_i2c_xfer函数,您说这个函数重不重要呢? 下面在用张图来形容它们之间的关系:第一此用这个软件,将就点吧。
最后将这个i2c_adapter 注册到系统中去。
..........
i2c->adap.nr = pdata->bus_num; // 0
ret = i2c_add_numbered_adapter(&i2c->adap); // 注册 i2c_adapter
if (ret < 0) {
dev_err(&pdev->dev, "failed to add bus to i2c core\n");
goto err_cpufreq;
}
platform_set_drvdata(pdev, i2c); // 设置 私有数据
这时呢,如果你去开发板上ls /dev/i* 就能看到i2c的设备了。
2.2 probe 函数中i2c_bus 硬件的初始化
在对i2c_adapter 进行相关设置操作之后,还需要对i2c做些硬件的初始化。
/* find the clock and enable it */
i2c->dev = &pdev->dev; // 绑定 dev
i2c->clk = clk_get(&pdev->dev, "i2c"); // 获得 i2c控制器的时钟资源
if (IS_ERR(i2c->clk)) {
dev_err(&pdev->dev, "cannot get clock\n");
ret = -ENOENT;
goto err_noclk;
}
dev_dbg(&pdev->dev, "clock source %p\n", i2c->clk);
clk_enable(i2c->clk); // 使能 i2c控制器的时钟
/* map the registers */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 得到 i2c控制器的寄存器地址
if (res == NULL) {
dev_err(&pdev->dev, "cannot find IO resource\n");
ret = -ENOENT;
goto err_clk;
}
/* request mem region */ // 申请I/O内存的函数是request_mem_region
/* request_mem_region函数并没有做实际性的映射工作,
*只是告诉内核要使用一块内存地址,声明占有,也方便内核管理这些资源。*/
i2c->ioarea = request_mem_region(res->start, resource_size(res),
pdev->name);
if (i2c->ioarea == NULL) {
dev_err(&pdev->dev, "cannot request IO\n");
ret = -ENXIO;
goto err_clk;
}
i2c->regs = ioremap(res->start, resource_size(res)); // 这里才真正的映射寄存器
if (i2c->regs == NULL) {
dev_err(&pdev->dev, "cannot map IO\n");
ret = -ENXIO;
goto err_ioarea;
}
dev_dbg(&pdev->dev, "registers %p (%p, %p)\n",
i2c->regs, i2c->ioarea, res);
/* setup info block for the i2c core */
i2c->adap.algo_data = i2c; // 绑定
i2c->adap.dev.parent = &pdev->dev;
/* initialise the i2c controller */
ret = s3c24xx_i2c_init(i2c); // i2c 控制器的初始化
if (ret != 0)
goto err_iomap;
/* find the IRQ for this unit (note, this relies on the init call to
* ensure no current IRQs pending
*/
i2c->irq = ret = platform_get_irq(pdev, 0); // 获得 i2c的中断资源
if (ret <= 0) {
dev_err(&pdev->dev, "cannot find IRQ\n");
goto err_iomap;
}
ret = request_irq(i2c->irq, s3c24xx_i2c_irq, IRQF_DISABLED, // 注册中断
dev_name(&pdev->dev), i2c);
if (ret != 0) {
dev_err(&pdev->dev, "cannot claim IRQ %d\n", i2c->irq);
goto err_iomap;
}
这些硬件的初始化包括获得和使能时钟源,获得i2c寄存器地址,并映射寄存器, i2c控制器的初始化 以及 注册中断。
我们再来看看i2c控制器的初始化:
static int s3c24xx_i2c_init(struct s3c24xx_i2c *i2c)
{
unsigned long iicon = S3C2410_IICCON_IRQEN | S3C2410_IICCON_ACKEN;
struct s3c2410_platform_i2c *pdata;
unsigned int freq;
/* get the plafrom data */
pdata = i2c->dev->platform_data; // 获得私有数据
/* inititalise the gpio */
if (pdata->cfg_gpio)
pdata->cfg_gpio(to_platform_device(i2c->dev)); // 初始化gpio
/* write slave address */
writeb(pdata->slave_addr, i2c->regs + S3C2410_IICADD); //0x10
dev_info(i2c->dev, "slave address 0x%02x\n", pdata->slave_addr);
writel(iicon, i2c->regs + S3C2410_IICCON); // 中断使能 和 ack使能
/* we need to work out the divisors for the clock... */
if (s3c24xx_i2c_clockrate(i2c, &freq) != 0) { // 设置i2c时钟传输频率
writel(0, i2c->regs + S3C2410_IICCON);
dev_err(i2c->dev, "cannot meet bus frequency required\n");
return -EINVAL;
}
/* todo - check that the i2c lines aren't being dragged anywhere */
dev_info(i2c->dev, "bus frequency set to %d KHz\n", freq);
dev_dbg(i2c->dev, "S3C2410_IICCON=0x%02lx\n", iicon);
dev_dbg(i2c->dev, "S3C2440_IICLC=%08x\n", pdata->sda_delay);
writel(pdata->sda_delay, i2c->regs + S3C2440_IICLC); // 设置 SDA output delay 15clocks
return 0;
}
初始化中配置了gpio 用于sda scl, 使能了ack 和 中断 , 设置了i2c总线传输频率以及sda 数据线延时(可省略),这些都是通过配置i2c控制器的寄存器来实现的,具体的参考s5pv210的芯片手册p883 。 前面提到了配置gpio的配置,即pdata->cfg_gpio函数来设置,cfg_gpio这个函数是在arch\arm\plat-samsung\Dev-i2c0.c中设置的,还记得我们前面分析的platform_device吗,这里面出了设置了寄存器资源和中断资源外还设置了私有数据,这也就是为什么s3c24xx_i2c_init一开始要得到私有数据的原因了。
void __init s3c_i2c0_set_platdata(struct s3c2410_platform_i2c *pd)
{
struct s3c2410_platform_i2c *npd;
if (!pd)
pd = &default_i2c_data0;
npd = kmemdup(pd, sizeof(struct s3c2410_platform_i2c), GFP_KERNEL);
if (!npd)
printk(KERN_ERR "%s: no memory for platform data\n", __func__);
else if (!npd->cfg_gpio)
npd->cfg_gpio = s3c_i2c0_cfg_gpio; // 这里设置了 gpio 的配置函数
s3c_device_i2c0.dev.platform_data = npd;
}
void s3c_i2c0_cfg_gpio(struct platform_device *dev)
{
#if defined(CONFIG_MACH_MINI210)
s3c_gpio_cfgall_range(S5PV210_GPD1(0), 2,
S3C_GPIO_SFN(2), S3C_GPIO_PULL_NONE); // 配置gpd1(0) 和 gpd1(1)为scl 和 sda 并且不需要上拉使能
#else
s3c_gpio_cfgall_range(S5PV210_GPD1(0), 2,
S3C_GPIO_SFN(2), S3C_GPIO_PULL_UP);
#endif
}
s3c_gpio_cfgall_range(S5PV210_GPD1(0), 2,S3C_GPIO_SFN(2), S3C_GPIO_PULL_NONE);这个函数挺有趣,有兴趣的朋友可以百度下看看s3c_gpio_cfgall_range这个函数和S3C_GPIO_SFN(2)这个宏的意思。
再回到s3c24xx_i2c_init看看s3c24xx_i2c_clockrate是如何来设置时钟频率的:
/* 根据已有的pclk频率 计算 iic传输频率 并设置相应寄存器 */
static int s3c24xx_i2c_clockrate(struct s3c24xx_i2c *i2c, unsigned int *got)
{
struct s3c2410_platform_i2c *pdata = i2c->dev->platform_data;
unsigned long clkin = clk_get_rate(i2c->clk);
unsigned int divs, div1;
unsigned long target_frequency;
u32 iiccon;
int freq;
i2c->clkrate = clkin;
clkin /= 1000; /* clkin now in KHz */ // 得到pclk频率
printk("neo frequency clkin = %lu\n",clkin );
dev_dbg(i2c->dev, "pdata desired frequency %lu\n", pdata->frequency);
target_frequency = pdata->frequency ? pdata->frequency : 100000;
target_frequency /= 1000; /* Target frequency now in KHz 400 * 1000 /1000 = 400*/
freq = s3c24xx_i2c_calcdivisor(clkin, target_frequency, &div1, &divs);
if (freq > target_frequency) {
dev_err(i2c->dev,
"Unable to achieve desired frequency %luKHz." \
" Lowest achievable %dKHz\n", target_frequency, freq);
return -EINVAL;
}
*got = freq;
// 66700 / 16 /11 =378hz
iiccon = readl(i2c->regs + S3C2410_IICCON);
iiccon &= ~(S3C2410_IICCON_SCALEMASK | S3C2410_IICCON_TXDIV_512); // 16 分频
iiccon |= (divs-1); // Tx clock = I2CCLK/(I2CCON[3:0]+1).
printk("neo frequency div1 = %d , divs = %d\n",div1,divs );
if (div1 == 512)
iiccon |= S3C2410_IICCON_TXDIV_512;
writel(iiccon, i2c->regs + S3C2410_IICCON);
return 0;
}
这个函数还真有点啰嗦,好在我们有万能的printK,你不是想通过PCLK频率得到计算 IIC总线频率吗,那我就把你的PCLK和IIC总线频率都打印出来,并且打印出div1 ,divs分频数值,通过div1 和div s来设置iiccon 寄存器 。
经过打印,猜出 具体计算公式如下:
PCLK=66700 MHZ div1 = 16 divs =11 iic总线频率 = 378MHz
iic总线频率 = PCLK / div1 /divs 。
3. 总结:
到此呢probe函数就算分析完了,现在我们来稍微总结下probe做的具体操作:
软件方面:
1. 分配设置注册了s3c24xx_i2c 和 i2c_adapter结构体,其中i2c_adapter这个是重点。
2..初始化了一些辅助变量,这个在后面代码中会用到。
硬件方面:
1. 获得和使能时钟
2. 获得和映射相关寄存器
3. 初始化iic总线控制器
4. 注册中断