struct pca9555_led {
u8 id;
struct i2c_client *client;
char *name;
struct led_classdev ldev;
struct work_struct work;
enum pca9555_state state;
};
struct pca9555_btn {
int irq;
char *name;
u8 id;
int keycode;
struct input_dev idev;
struct i2c_client *client;
};
struct pca9555_platform_data {
struct pca9555_led leds[5];
struct pca9555_btn btns[8];
};
上面的结构体定义在pca9555.h中,PCA9555有16个I/O,5个接led,8个接按键,结构体pca9555_platform_data描述了9555的使用情况。结构体类型pca9555_led和pca9555_btn分别用于描述led和Button,他们都属于i2c设备,因此都包括结构体指针变量struct i2c_client *client,led需要向led-class中注册,其注册的设备结构类型为led_classdev,Button为输入设备,在设备结构体中包含向input子系统注册的类型input_dev,并且包含中断号,按键码等信息。
I2C设备的注册点击(此处)折叠或打开
static struct i2c_driver pca9555_driver = {
.driver = {
.name = "pca9555",
},
.probe = pca9555_probe, //当有i2c_client与i2c_driver匹配时调用
.remove = pca9555_remove, //注销时调用
.id_table = pca9555_id, //根据id进行匹配
}
struct pca9555_data{
struct i2c_client *client;
struct pca9555_led leds[5];
struct pca9555_btn btns[8];
struct mutex update_lock;
};
这两个结构体定义在驱动文件pca9555.c中,pca9555_driver在i2c驱动注册时作为参数被调用。pca9555_data中除了定义leds、btns之外定义了互斥变量update_lock,在通过i2c总线读写设备时用到。
在Linux2.6内核中支持两种编写i2c驱动程序的方式(这里所有内核版本为linux2.6.28):Adapter方式(LEGACY)和Probe方式(new style)。对于LEGACY方式的驱动设备部分在驱动运行的时候动态创建,新式的驱动(probe方式)倾向于向传统的Linux下设备驱动看齐,采用静态定义的方式来注册设备。使用接口为:
int __init i2c_register_board_info(int busnum,
struct i2c_board_info const *info, unsigned len)
该函数定义在linux2.6.28/driver/i2c/i2c-boardinfo.c中。在平台代码中将会调用该函数完成i2c_board_info的注册。注册过程会根据info参数提供的设备信息封装一个devinfo的结构体,并添加到全局链表_i2c_board_list中。
对于i.MX233,在linux2.6.28\arch\arm\mach-stmp3xxx\stmp378x_devb.c中,在完成设备结构体的部分初始化后将会调用该接口完成注册。相关代码如下:
点击(此处)折叠或打开
static struct pca9555_platform_data imx233_lbtn = {
.leds = {
{
.id = 0;
.name = "led0";
.state = PCA9555_OFF;
},
……
{
id = 11;
.name = "led4"
.state = PCA9555_OFF;
},
}
.btns = {
{
.irq = 17;
.name = "btn1";
.id = 2;
.keycode = KEY_1;
}, ……
{
.irq = 17;
.name = "btn8";
.id = 15;
.keycode = KEY_8;
},
}
};
static struct i2c_board_info __initdata stmp3xxx_i2c_devices[] = {
{ I2C_BOARD_INFO("stfm1000", 0xc0), .flags = I2C_M_TEN },
{ I2C_BOARD_INFO("pca9555",0x20),
.platform_data = &imx233_lbtn,
},
};
static void __init stmp378x_devb_init(void)
{
struct fsl_usb2_platform_data *udata;
stmp3xxx_init();
i2c_register_board_info(0, stmp3xxx_i2c_devices, ARRAY_SIZE(stmp3xxx_i2c_devices));
stmp3xxx_set_mmc_data(&stmp3xxx_mmc.dev);
..
}
完成i2c_board_info注册后,在I2C核心中会根据板级i2c设备配置信息,创建i2c客户端设备(i2c_client)添加到i2c子系统中。
i2c驱动接下来的部分全部在pca9555.c中实现。
Probe在数据结构中提到static struct i2c_driver pca9555_driver ,在加载驱动模块时将会将其注册到i2c子系统中。接口如下:
i2c_add_driver(&pca9555_driver);
在pca9555_driver中定义了一个probe和remove两个回调函数。当加载驱动模块后i2c_client和i2c_driver匹配时将会调用pca9555_probe()。
函数pca9555_probe()所做的工作很简单。主要是取得板级的设备信息,这里将其保存到变量pca9555_pdata中,然后申请pca9555_data类型变量data空间并完成相关指针传递。接着初始化互斥量update->lock,最后跳转到pca9555_configure()函数执行。
Configurepca9555_configure(client, data, pca9555_pdata);
在pca9555_configure()中所做的工作主要分三个部分:
配置PCA9555引脚功能点击(此处)折叠或打开
u8 con[2] = { 0x54,0xf7};
for(i=0;i<2;i++) {
i2c_smbus_write_byte_data(client,PCA9555_REG_PINVERSION(i),0X0);
i2c_smbus_write_byte_data(client,PCA9555_REG_CONF(i),con[i]);
}
将极性反转寄存器配置为全0,然后通过9555的两个8位控制寄存器配置I/O引脚的输入输出功能。保证接led的引脚为输出引脚,连有按键的为输入引脚。PCA9555_REG_PINVERSION(i)、PCA9555_REG_CONF(i)定义了操作pca9555特定寄存器的命令字节的值,在后面出现时将不解释。
led设备注册点击(此处)折叠或打开
for(i=0;i<5;i++) {
struct pca9555_led *led = &data->leds[i];
struct pca9555_led *pled = &pdata->leds[i];
led->client = client;
led->id = pled->id;
led->state = pled->state;
led->name = pled->name;
led->ldev.name = led->name;
led->ldev.brightness_set = pca9555_set_brightness;
err = led_classdev_register(&client->dev,&led->ldev);
if(err<0) {
dev_err(&client->dev,
"couldn't register LED %s \n",led->name);
return -1;
}
}
点击(此处)折叠或打开
for( i =0;i < 8; i++) {
struct pca9555_btn *pbtn = &pdata->btns[i];
btns[i] = &data->btns[i];
btns[i]->client = client;
btns[i]->name = pbtn->name;
btns[i]->id = pbtn->id;
btns[i]->irq = pbtn->irq;
btns[i]->keycode = pbtn->keycode;;
btns[i]->idev.name = btns[i]->name;
btns[i]->idev.evbit[0] = BIT_MASK(EV_KEY);
set_bit(btns[i]->keycode,btns[i]->idev.keybit);
if(request_irq(btns[i]->irq,button_key_event, IRQF_SHARED,btns[i]->name,&btns[i])) {
printk("button can not be allocate irq");
return -EBUSY;
}
printk(KERN_INFO"%s successfully loaded\n",btns[i]->name);
ret = input_register_device(&btns[i]->idev);
if(ret) {
input_free_device(&btns[i]->idev);
return ret;
}
}
分别初始化8个按键所对应的变量,并设置idev域,使其支持按键事件并设置对应的按键码。在前面的Linux输入子系统部分有介绍。接着申请中断,由于这里的8个按键共享一个GPIO中断,中断类型设置为IRQF_SHARED,且中断处理函数为button_key_event,当中断触发时将会回调执行该函数。将会在后面详细分析该函数。最后注册输入设备到Linux输入子系统。
回调函数点击(此处)折叠或打开
pca9555_set_brightness
static void pca9555_set_brightness(struct led_classdev *led_cdev,
enum led_brightness value)
{
struct pca9555_led *led = ldev_to_led(led_cdev);
if(value)
led->state = PCA9555_ON;
else
led->state = PCA9555_OFF;
pca9555_setled(led);}
在/sys/class/leds/中相应的led设备目录下,通过echo写入brightness文件的值(0~255)将会传递到value 中,根据写入的值是非为0设置state域,然后调用pca9555_setled(led)来点亮或熄灭led设备。
点击(此处)折叠或打开
static void pca9555_setled(struct pca9555_led *led)
{
struct i2c_client *client = led->client;
struct pca9555_data *data = i2c_get_clientdata(client);
char reg;
mutex_lock(&data->update_lock);
reg = i2c_smbus_read_byte_data(client,LED_REG(led->id));
if(led->state)
reg = reg & ~(0x1 << ((led->id) % 8));
else
reg = reg | (0x1 <<((led->id) %8));
i2c_smbus_write_byte_data(client,LED_REG(led->id),reg);
mutex_unlock(&data->update_lock);
}
在前面提到的led的id值在这里被用到,通过宏LED_REG(led->id)确定控制该led所在引脚的寄存器字节,读取该寄存器中的值到reg中,然后根据先前设置的state域,设置led引脚所对应的位的值,最后将处理过的reg重新写入到相应的寄存器中。这样就达到改变led所在引脚的电位的目的,从而控制led的亮度。另外指出的是在读写的时候需要用到互斥机制。
button_key_event在configure中申请中断时,将8个按键共用了一个GPIO中断,当这些按键中的任意一个产生中断时都会跳转到中断处理函数button_key_event中执行,这是需要判断到底是哪个按键被按下,然后向输入子系统上报相应按键对应的事件。
为了确定被按下的按键,定义如下两个字符型的全局变量inreg1,inreg2,用于保存中断发生前反映pca9555输入引脚电位的两个输入寄存器的值。在发生第一次中断之前,在模块加载中先读取这连个寄存器的值。这里放在pca9555_configure()函数的最后,代码如下:
inreg1 = i2c_smbus_read_byte_data(client,PCA9555_REG_INPUT(0)); inreg2 = i2c_smbus_read_byte_data(client,PCA9555_REG_INPUT(1));中断处理程序button_key_event()函数代码如下:
点击(此处)折叠或打开
static irqreturn_t button_key_event(int irq,void *dev_id)
{
//struct pca9555_btn *button = (struct pca9555_btn *)dev_id;
struct i2c_client *client = btns[0]->client;
char linreg1,linreg2;
linreg1 = i2c_smbus_read_byte_data(client,PCA9555_REG_INPUT(0));
linreg2 = i2c_smbus_read_byte_data(client,PCA9555_REG_INPUT(1));
switch((int)inreg1^linreg1) {
case 2:
input_report_key(&btns[0]->idev,btns[0]->keycode,linreg1);
input_sync(&btns[0]->idev);
break;
......
}
switch((int)inreg2^linreg2) {
case 2:
input_report_key(&btns[3]->idev,btns[3]->keycode,linreg2);
input_sync(&btns[3]->idev);
break;
......
}
inreg1 = linreg1;
inreg2 = linreg2;
return IRQ_HANDLED;
}
当中断发生后,在中断处理程序中首先读取按键被按下后pca9555中两个输入寄存器中的值,然后与被按下前寄存器中的值inreg1、inreg2比较,确定电位发生变化的引脚所对应的按键,然后通过input_report_key向输入子系统上报对应的按键事件。接着将这次按键被按下后寄存器的值保存到inreg1、inreg2中作为下次按键被按下之前的值. 最后退出中断处理程序。
对于驱动的设计先写到这,在后面调试过程中发现问题再补充和完善。
联系客服