芯片平台的可用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去设置闪烁。