对于一个从单片机驱动开发转到linux驱动开发的人员来说,最头疼的莫过于是linux的驱动框架了。在传统单片机开发的过程中,都是直接操作寄存器,比喻说配置个IO口引脚输出为高电平,只需要向方向寄存器、数据寄存器写入值就可以实现了,这种方法比较直观简单,开发人员只需要掌握C语言、原理图以及datasheet就可以进行开发了。而linux驱动开发需要涉及的东西就比较多了,开发人员需要掌握C语言、原理图、datasheet、驱动框架等内容,特别时驱动框架,对于刚从事Linux驱动开发的人员来说,简直是天书,不知所云。
也有很多开发人员很不理解,明明就是一个很简单的led驱动程序,单片机代码可能就10行,可是在linux驱动中却需要几百行去实现,这不是吃饱了没事干,给自己找活吗?大部分人刚看到linux驱动代码就头疼,因为代码里到处都是结构体、指针、回调函数,显得代码非常的复杂。
但实际上,所有的驱动都是与硬件打交道的,如果说哪个驱动跟硬件一点关系都没有,那就是耍流氓。记住一点,所有的驱动最终都要回归到寄存器的配置,因为寄存器的配置是唯一跟硬件相关的入口,不管你框架多复杂,结构体各种转来转去,最终都会落实到寄存器的配置上,所谓的框架不过是抽象出来一种模式,把各厂家的驱动与内核层的接口区分开,这样就不需要根据驱动去修改内核接口,也就是保证了LINUX内核能够更多的兼容不同厂家的驱动。
所有的驱动最终都要落实到寄存器的配置上,天花乱坠的框架都是纸老虎。
既然前面我们已经说了天花乱坠的框架都是纸老虎,那么我们就一起来一次武松打虎吧。咱们先选择一个LINUX的源文件,这里选择的是linux-xlnx-xilinx-v2017.4的内核源码进行分析,以驱动的示例代码linux-xlnx-xilinx-v2017.4\drivers\leds\leds-gpio.c来分析。整个leds-gpio.c代码总共有286行,该从哪一行来看呢?答案就是line281:
module_platform_driver(gpio_led_driver); //注册gpio_led_driver驱动
module_platform_driver这是一个宏定义,追根溯源
module_platform_driver -> platform_driver_register -> driver_register //驱动注册函数
-> platform_driver_unregister -> driver_unregister //驱动注销函数
宏的作用就是定义指定名称的平台设备驱动注册函数和平台设备驱动注销函数,并且在函数体内分别通过platform_driver_register()函数和platform_driver_unregister()函数注册和注销该平台设备驱动。那么我们再回过头去看上面的程序,就很清楚是什么意思了。
module_platform_driver(gpio_led_driver); //向内核注册gpio_led_driver设备驱动
擒贼先擒王,咱们已经找到罪魁祸首了,就必须继续沿着这条线索走下去。顺着gpio_led_driver往下看
static struct platform_driver gpio_led_driver = {
.probe = gpio_led_probe,
.shutdown = gpio_led_shutdown,
.driver = {
.name = "leds-gpio",
.of_match_table = of_gpio_leds_match,
},
};
可以看到这个设备驱动的成员函数有probe函数、shutdown函数以及驱动名称、match函数。那这些函数有什么用呢?
- probe函数:probe的中文意思是探针的意思,在驱动代码里也是这个意思,就是做一些资源处理以及基本函数的初始化工作;
- shutdown函数:shutdown的中文意思是关机的意思,在gpio_led_driver中表示关闭led灯的意思;
- name:就是驱动的名称,只是个代号,没有啥作用;
- match函数:match的中文意思就是匹配的意思,匹配什么呢?匹配的就是设备和驱动。
LINUX驱动的架构就是将设备和驱动分离,通过match函数来匹配设备和驱动。不管是设备还是驱动,只有这两者匹配上,才能正常调用probe函数。在驱动代码里,有这样一段程序
static const struct of_device_id of_gpio_leds_match[] = {
{ .compatible = "gpio-leds", }, //驱动中的设备匹配信息
{},
};
程序里面的 .compatible = "gpio-leds"是核心,设备和驱动就是通过这个 .compatible 来进行匹配的。在板级设备树文件中,描述设备的硬件信息时,必须要将 compatible = "gpio-leds"添加在设备树节点上。如果要想正常调用probe函数,那么设备树的形式应该如下所示:
gpio-leds {
compatible = "gpio-leds"; //设备树中的节点匹配信息
#address-cells = <1>;
#size-cells = <0>;
...
...
...
};
如果在设备树文件中添加了gpio-leds的设备节点,那么probe函数就会被调用了,那么咱么继续研究probe函数:
static int gpio_led_probe(struct platform_device *pdev)
{
struct gpio_led_platform_data *pdata = dev_get_platdata(&pdev->dev);
//获取platform中device的数据
struct gpio_leds_priv *priv; //定义gpio_leds的结构体
int i, ret = 0;
if (pdata && pdata->num_leds) {
priv = devm_kzalloc(&pdev->dev, //向内核申请gpio_leds结构体大小的内存
sizeof_gpio_leds_priv(pdata->num_leds),
GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->num_leds = pdata->num_leds; //将设备传入的参数num_leds赋给priv结构体成员
for (i = 0; i < priv->num_leds; i++) { //依次在/sys/class/leds目录下创建所有led设备
ret = create_gpio_led(&pdata->leds[i],
&priv->leds[i],
&pdev->dev, pdata->gpio_blink_set);
if (ret < 0)
return ret;
}
} else {
priv = gpio_leds_create(pdev);//获取gpio资源,依次在/sys/class/leds目录下创建所有led
if (IS_ERR(priv))
return PTR_ERR(priv);
}
platform_set_drvdata(pdev, priv);//将priv的相关信息保存到platform_device中,方便后续调用
return 0;
}
那么,我们可以看到这个probe函数,里面可能跟硬件相关的就是create_gpio_led这个函数了,那么我们继续沿着这个函数往下看。
create_gpio_led -> devm_gpio_request_one -> gpio_request_one -> set_bit(FLAG_ACTIVE_LOW, &desc->flags);
很显然,沿着create_gpio_led继续往下分析代码,就会发现,通过一层一层的函数调用,最终还是回归到设置GPIO口的输入输出模式、输出高电平还是低电平的问题上,这也是gpio_leds驱动的核心,在单片机驱动开发过程中,也是设置这些寄存器来点亮LED灯的吧。但其实很多人可能有疑问了,你看这个都是对各种结构体进行赋值操作,并没有看到对GPIO的寄存器进行赋值操作,那是如何做到配置GPIO的寄存器呢?答案就是LINUX将硬件信息与软件层面的操作分开了,硬件信息通过设备树来描述,在设备树中会清楚的描述出资源的信息,比方说IO资源、中断资源、以及总线资源等等,那么这些资源传递给内核以后,就会换一种身份存在了,那就是platform_device中的信息了,所以在linux内核代码里你是追踪不到对寄存器的配置的,而都是对platform_device定义的结构体进行的配置,通过映射关系,将配置的信息映射至实际的寄存器中,实现对硬件的控制。