pca9557,i2c转gpio,按键和三色灯

芯片平台的可用GPIO太少了,可以用PCA9557来增加GPIO,挂到I2C上,然后一下增加8个GPIO,不要太爽。

接下来就要准备PCA9557的驱动。有三种方式,1.问原厂要驱动;2.自己写驱动;3.从linux源码里找对应驱动。个人认为消费级的嵌入式开发非常不适合自己写外设驱动,非常耗时间还容易出bug,基本功不牢的话一不小心就来个crash。如果既不能在kernel中找到对应驱动,也不能从原厂拿到驱动,那就得自己硬着头皮写了。但我在kernel里找到gpio-pca953x.c这个驱动文件,里面有:

static const struct of_device_id pca953x_dt_ids[] =
	{ .compatible = "nxp,pca9557", },
	
static const struct i2c_device_id pca953x_id[] =
	{ "pca9557", 8  | PCA953X_TYPE, },					--- device ID =0x1008

现在就非常轻松了。

简单记录几点。

1. 都知道probe函数是驱动的入口函数,那么它是如何回调的呢?这次注册的是i2c设备,最终通过driver->probe回调,如下:

static struct i2c_driver pca953x_driver = {
	.driver = {
		.name	= "pca953x",
		.of_match_table = pca953x_dt_ids,
		.acpi_match_table = ACPI_PTR(pca953x_acpi_ids),
	},
	.probe		= pca953x_probe,
	.remove		= pca953x_remove,
	.id_table	= pca953x_id,
};

struct bus_type i2c_bus_type = {
	.name		= "i2c",
	.match		= i2c_device_match,
	.probe		= i2c_device_probe,
	.remove		= i2c_device_remove,
	.shutdown	= i2c_device_shutdown,
};

static int __init pca953x_init(void)
	i2c_add_driver(&pca953x_driver);
		i2c_register_driver(THIS_MODULE, driver)
			
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
	driver->driver.bus = &i2c_bus_type;
	driver_register(&driver->driver);

int driver_register(struct device_driver *drv)
	driver_find(drv->name, drv->bus);				--- 查找驱动是否已经装载 
	bus_add_driver(drv);							--- 根据总线类型添加驱动
	driver_add_groups(drv, drv->groups);			--- 将驱动添加到对应组中 
	kobject_uevent(&drv->p->kobj, KOBJ_ADD);		--- 注册uevent事件


bus_add_driver
	driver_attach
		__driver_attach
			driver_match_device(drv, dev)-->i2c_device_match-->strcmp(client->name, id->name)
			driver_probe_device
				really_probe
					dev->bus->probe(dev);-->i2c_device_probe-->driver->probe(client, i2c_match_id(driver->id_table, client));

2. probe函数

static int pca953x_probe(struct i2c_client *client,const struct i2c_device_id *id)
	chip = devm_kzalloc(&client->dev,sizeof(struct pca953x_chip), GFP_KERNEL);
	chip->gpio_start = -1;
	irq_base = 0;
	chip->client = client;
	chip->driver_data = id->driver_data;
	chip->chip_type = PCA_CHIP_TYPE(chip->driver_data);		--- 0x1008 & 0xf000 = 0x1000
	pca953x_setup_gpio(chip, chip->driver_data & PCA_GPIO_MASK);	--- 定义direction value等函数,chip->ngpio= 8
	device_pca953x_init(chip, invert);				--- 尝试读下9557 output direction 寄存器
	gpiochip_add(&chip->gpio_chip);				        ---1 重点函数
	pca953x_irq_setup(chip, irq_base);
	i2c_set_clientdata(client, chip);

1:
int gpiochip_add(struct gpio_chip *chip)
	//从后往前遍历全局 gpio desc, 只要是非保留gpio且无宿主chip的连续gpio的空间起址作为base, ngpio则依次向下扩展
	gpiochip_find_base(chip->ngpio);
	chip->base = base;
	gpiochip_add_to_list(chip);
	gpiochip_sysfs_register(chip);

在probe函数中,pdata并非来自dev->platform_data,所以设置gpio_start=-1,irq=0.在pca953x_setup_gpio中填充gpio_chip结构体,包括direction_input,direction_output,get,set等,这些函数的实现主要是操作pca9557的寄存器。重点是gpiochip_add函数,这个函数注册一个gpio_chip,并且动态分配gpio起始号,有时间值得更深入分析。

3.gpio起始号

刚开始准备用动态分配的gpio起始号,但是这个产品有多个pca9557,当移除其中一个后,另外的pca9557分配的gpio起始号居然改变了。那就固定好每个pca9557的gpio起始号吧。

首先dts里增加 gpio-start = <number>,然后在probe函数里读出来number写入chip->gpio_start,如下代码。其中pca9557的设备树如何写可以参考Documentation下对应文档,注意得增加gpio-controller。number号可以自己随便定,不冲突即可,我采用的number是刚开始随机分配的,然后固定好。

struct device_node *node;
node = client->dev.of_node;
if (of_property_read_u32(node, "gpio-start", &chip->gpio_start)) 
printk("pca9557 get gpio_base failed\n");

4.按键

这8个GPIO有一部分需要做按键使用,kernel里面对应按键驱动是gpio_keys.c。但是注册中断的时候卡住了,按键中断该如何定义呢?从PCA9557的规格书里没有发现关于中断定义,那参考设备树中GPIO中断定义吧。设备树中关于gpio中断有:

			interrupt-parent = <&intc>;
			interrupts = <6>;

从主芯片手册中发现soc_cirq_int6对应gpio中断,但是没有i2c对应的中断源。怎么办?中断行不通就切换成轮询呗,恰好kernel里有gpio_keys_polled.c这个按键驱动,刚刚好。

简单说下两种按键驱动的区别。两个驱动里均有INIT_DELAYED_WORK注册延时的工作列队,其中gpio_keys.c中有通过devm_request_any_context_irq申请中断:

INIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func);
isr = gpio_keys_gpio_isr;
devm_request_any_context_irq(&pdev->dev, bdata->irq,isr, irqflags, desc, bdata);

当中断回调 gpio_keys_gpio_isr时,处理gpio_keys_gpio_work_func的工作,即向input子系统上报event事件:

static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
		mod_delayed_work(system_wq,
			 &bdata->work,					--- 即 gpio_keys_gpio_work_func
			 msecs_to_jiffies(bdata->software_debounce));

static void gpio_keys_gpio_work_func(struct work_struct *work)
	gpio_keys_gpio_report_event(bdata);
		input_event(input, type, button->code, !!state);
		input_sync(input);

而在gpio_keys_polled.c,通过input_register_polled_device注册轮询的input设备。然后定义了input->open = input_open_polled_device;即把轮询输入设备的工作任务加入队列。:

static int gpio_keys_polled_probe(struct platform_device *pdev)
	pdata = gpio_keys_polled_get_devtree_pdata(dev);--> devm_get_gpiod_from_child(dev, NULL, child);-->fwnode_get_named_gpiod(child, prop_name);-->gpiod_request(desc, NULL);
	bdev = devm_kzalloc(&pdev->dev, size, GFP_KERNEL);
	poll_dev = devm_input_allocate_polled_device(&pdev->dev);
	poll_dev->poll = gpio_keys_polled_poll;				--- poll函数里检查按键io口的状态,并汇报按键
	input->phys = DRV_NAME"/input0";
	error = input_register_polled_device(poll_dev);			--- 注册 input 设备
int input_register_polled_device(struct input_polled_dev *dev)
	input_set_drvdata(input, dev);
	INIT_DELAYED_WORK(&dev->work, input_polled_device_work);
	//初始化用于工作队列的延时工作任务,当 dev->work 工作任务得到调用时, input_polled_device_work 函数就会得到调用
    //前面我们用的工作队列,只要工作任务加入队列后,等待条件满足并唤醒队列才会得到调用. 但这里相当于定时执行一次.
	input->open = input_open_polled_device;					--- 在输入设备的设备文件open时,被触发调用
	error = input_register_device(input);
static int input_open_polled_device(struct input_dev *input)
	input_polldev_queue_work(dev); 						--- 把轮询输入设备的工作任务加入队列
	
static void input_polldev_queue_work(struct input_polled_dev *dev)
	queue_delayed_work(system_freezable_wq, &dev->work, delay); --- 把工作任务加入队列,并指定多久时间后执行. 经过delay时间后,input_polled_device_work 函数就会被调用. 
	
static void input_polled_device_work(struct work_struct *work)
    dev->poll(dev); 					//调用轮询输入设备对象的poll函数,即 gpio_keys_polled_poll 函数
    input_polldev_queue_work(dev); 			//重新把轮询输入设备对象的工作任务加入工作队列里,实现按间隔时间重复调用

从gpio_keys_polled_get_devtree_pdata中也知道,最终gpiod_request是没有带label的,所以最后查看/sys/kernel/debug/gpio,也看不到按键对应的label.

5.三色灯

三色灯即有三个分别为红蓝绿的LED,用普通的led驱动即可,主要是两个文件leds-gpio.c和led-class.c。

led-class.c,在 /sys/class/ 下创建 leds 文件夹,并创建对应属性文件及读写方法:

subsys_initcall(leds_init);

static int __init leds_init(void)

	leds_class = class_create(THIS_MODULE, "leds");		--- 在 /sys/class/ 下创建 leds 文件夹
	leds_class->pm = &leds_class_dev_pm_ops;		--> static SIMPLE_DEV_PM_OPS(leds_class_dev_pm_ops, led_suspend, led_resume);
	leds_class->dev_groups = led_groups;
	
static const struct attribute_group *led_groups[] = 
	&led_group,				--> &dev_attr_brightness.attr,&dev_attr_max_brightness.attr,
	&led_trigger_group,			--> &dev_attr_trigger.attr
	
static DEVICE_ATTR_RW(brightness);		--> brightness_store --> led_set_brightness 

static DEVICE_ATTR_RO(max_brightness);

static DEVICE_ATTR(trigger, 0644, led_trigger_show, led_trigger_store);

EXPORT_SYMBOL_GPL(devm_of_led_classdev_register);     --> of_led_classdev_register(parent, np, led_cdev);
leds-gpio.c,gpio_led_probe-->gpio_leds_create-->create_gpio_led,暂时不仔细分析了。关键是工作列队的设置,定时器的设置和启动。之后就可以通过/sys/class/leds/led-control/brightness  设置亮度,echo timer > /sys/class/leds/trigger去设置闪烁。



猜你喜欢

转载自blog.csdn.net/jin615567975/article/details/80743764