【无标题】基于Linux内核2.6x以上版本Platform驱动编程(一)

1 篇文章 0 订阅
1 篇文章 0 订阅
  • 前言

  • 在Linux设备模型的抽象中,存在着一类称作“Platform Device”的设备,内核文档这样描述它们的(Documentation/driver-model/platform.txt)
  • Platform devices
  • ~~~~~~~~~~~~~~~~
  • Platform devices are devices that typically appear as autonomous entities in the system. This includes legacy port-based devices and host bridges to peripheral buses, and most controllers integrated into system-on-chip platforms.  What they usually have in common is direct addressing from a CPU bus.  Rarely, a platform_device will be connected through a segment of some other kind of bus; but its registers will still be directly addressable.
  • 翻译就是:平台设备通常作为系统中的自动实体出现。这包括遗留的基于端口的设备和到外围总线的主机桥,以及集成到片上系统平台的大多数控制器。它们的共同之处在于从CPU总线直接寻址。很少情况下,platform_device将通过某种其他类型的总线段连接;但是它的寄存器仍然是可直接寻址的
  • 一、Platform模块的软件架

  • 一旦设备和驱动匹配那么就会执行驱动中probe函数
  • 二、设备树讲解

  • 什么是设备树 dts(device tree)?
  • 设备树(Device Tree)是描述计算机的特定硬件设备信息的数据结构,以便于操作系统的内核可以管理和使用这些硬件,包括CPU或CPU,内存,总线和其他一些外设。dtb文件会被保存到ROM中,最终通过bootbolader被加载到内核,这样内核就可以通过解析设备树来让驱动去控制实际的硬件了。
  • 设备树dts解析
  • 内核启动会执行架构下的启动汇编代码arch/xxx/cpu/head.S后跳转到start_kernel。
  • asmlinkage __visible void __init start_kernel(void)
    
    {
    ...
    setup_arch(&command_line);
    ...
    }
    
    void __init setup_arch(char **cmdline_p)
    {
    const struct machine_desc *mdesc;
    mdesc = setup_machine_fdt(__atags_pointer);
    ...
    arm_memblock_init(mdesc);
    ...
    unflatten_device_tree();
    ...
    
    }
  • 这三个被调用的函数就是主要的设备树处理函数
  • setup_machine_fdt()函数根据传入的设备树dtb的首地址完成一些初始化操作
  • arm_memblock_init()函数主要是内存相关,为设备树保留相应的内存空间,保证设备树dtb本身存在于内存中而不被覆盖用户可以在设备树中设置保留内存,这一部分同时作了保留指定内存的工作。
  • unflatten_device_tree()这个函数就是对设备树具体的解析,将设备树各节点转换成相应的struct device_node结构体
  • (1)setup_machine_fdt()开始

  • const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
    {
    const struct machine_desc *mdesc, *mdesc_best = NULL;
    
    if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys))) --------part 1
    
    return NULL;
    
    mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); --------part 2
    
    early_init_dt_scan_nodes(); ------------------part 3
    
    ...
    
    }
  • (2) setup_machine_fdt(__atags_pointer)
  • __atags_pointer这个全局变量存储的就是r2的寄存器值,是设备树在内存中的起始地址,将设备树起始地址传递给setup_machine_fdt,对设备树进行解析。
  • 第一部分先将设备树在内存中的物理地址转换为虚拟地址,然后再检查该地址上是否有设备树的魔数(magic),魔数就是一串用于识别的字节码,如果没有或者魔数不匹配,表明该地址没有设备树文件,函数返回,否则验证成功,将设备树地址赋值给全局变量initial_boot_params。
  • 第二部分of_flat_dt_match_machine(mdesc_best, arch_get_next_mach),逐一读取设备树根目录下的compatible属性。
  • 将compatible中的属性一一与内核中支持的硬件单板相对比,匹配成功后返回相应的machine_desc结构体指针。
  • machine_desc结构体中描述了单板相关的一些硬件信息,这里不过多描述。
  • 主要的的行为就是根据这个compatible属性选取相应的硬件单板描述信息,一般compatible属性名就是"厂商,芯片型号"。
  • 第三部分就是扫描设备树中的各节点,主要分析这部分代码
  • void __init early_init_dt_scan_nodes(void)
    {
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
    of_scan_flat_dt(early_init_dt_scan_root, NULL);
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);
    }
  • 出人意料的是,这个函数中只有一个函数的三个调用,直觉告诉我这三个函数调用并不简单。
  • 首先of_scan_flat_dt()这个函数接收两个参数,一个是函数指针,一个为boot_command_line,boot_command_line是一个静态数组,存放着启动参数,而of_scan_flat_dt()函数的作用就是扫描设备树中的节点,然后对各节点分别调用传入的回调函数。
  • 在上述代码中,传入的参数分别为early_init_dt_scan_chosen,early_init_dt_scan_root,early_init_dt_scan_memory这三个函数,从名称可以猜测,这三个函数分别是处理chosen节点、root节点中除子节点外的属性信息、memory节点。
  • int __init early_init_dt_scan_chosen(unsigned long node, const char *uname, int depth, void *data){
    ...
    p = of_get_flat_dt_prop(node, "bootargs", &l);
    if (p != NULL && l > 0)
    strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));
    ...
    }
  • 经过代码分析,第一个被传入的函数参数作用是获取bootargs,然后将bootargs放入boot_command_line中,作为启动参数,而并非处理整个chosen节点。
  • int __init early_init_dt_scan_root(unsigned long node, const char *uname, int depth, void *data)
    {
    dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
    dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT;
    
    prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
    if (prop)
    dt_root_size_cells = be32_to_cpup(prop);
    
    prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
    if (prop)
    dt_root_addr_cells = be32_to_cpup(prop);
    ...
    }

  • 函数先判断节点的unit name是memory@0,如果不是,则返回。然后将所有memory相关的reg属性取出来,根据address-cell和size-cell的值进行解析,然后调用early_init_dt_add_memory_arch()来申请相应的内存空间。
  • 二,setup_machine_fdt()函数对于设备树的第一次扫描解析就完成了,主要是获取了一些设备树提供的总览信息。
  • 2、arm_memblock_init

  • void __init arm_memblock_init(const struct machine_desc *mdesc)
    {
    ...
    early_init_fdt_reserve_self();
    early_init_fdt_scan_reserved_mem();
    ...
    }
  • arm_memblock_init()对于设备树的初始化而言,主要做了两件事:
  • 调用early_init_fdt_reserve_self,根据设备树的大小为设备树分配空间,设备树的totalsize在dtb头部中有指明,因此当系统启动之后,设备树就一直存在在系统中。扫描设备树节点中的"reserved-memory"节点,为其分配保留空间。memblock_init对于设备树的部分解析就完成了,主要是为设备树指定保留内存空间。接下来继续回到setup_arch()函数中,继续向下跟踪代码。
  • 3、unflatten_device_tree

    void __init unflatten_device_tree(void)
    {
    __unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false); ------------------ part1
    of_alias_scan(early_init_dt_alloc_memory_arch); ------------------ part2
    ...
    }
  • 主要的解析函数为unflatten_dt_nodes(),在__unflatten_device_tree()函数中,unflatten_dt_nodes()被调用两次,第一次是扫描得出设备树转换成device node需要的空间,然后系统申请内存空间,第二次就进行真正的解析工作,我们继续看unflatten_dt_nodes()函数:
  • 值得注意的是,在第二次调用unflatten_dt_nodes()时传入的参数为unflatten_dt_nodes(blob, mem, dad, mynodes);
  • 第一个参数是设备树存放首地址,第二个参数是申请的内存空间,第三个参数为父节点,初始值为NULL,第四个参数为mynodes,初始值为of_node.
  • static int unflatten_dt_nodes(const void *blob,void *mem,struct device_node *dad,struct device_node **nodepp)
    {
    ...
    for (offset = 0;offset = 0 && depth = initial_depth;offset = fdt_next_node(blob, offset, &depth)) {
    populate_node(blob, offset, &mem,nps[depth], fpsizes[depth], &nps[depth+1], dryrun);
    ...
    }
    }
  • 这个函数中主要的作用就是从根节点开始,对子节点依次调用populate_node(),从函数命名上来看,这个函数就是填充节点,为节点分配内存。
  • static unsigned int populate_node(const void *blob,int offset,void **mem,
    struct device_node *dad,unsigned int fpsize,struct device_node **pnp,bool dryrun){
    struct device_node *np;
    
    ...
    np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,__alignof__(struct device_node));
    of_node_init(np);
    np->full_name = fn = ((char *)np) + sizeof(*np);
    if (dad != NULL) {
    np->parent = dad;
    np->sibling = dad->child;
    dad->child = np;
    }
    
    ...
    populate_properties(blob, offset, mem, np, pathp, dryrun);
    np->name = of_get_property(np, "name", NULL);
    np->type = of_get_property(np, "device_type", NULL);
    if (!np->name)
    np->name = "<NULL";
    if (!np->type)
    np->type = "<NULL";
    ...
    }
  • 通过跟踪populate_node()函数,可以看出,首先为当前节点申请内存空间,使用of_node_init()函数对node进行初始化,of_node_init()函数也较为简单:
  • static inline void of_node_init(struct device_node *node)
    {
    kobject_init(&node-kobj, &of_node_ktype);
    node->fwnode.ops = &of_fwnode_ops;
    }
  • 设置kobj,接着设置node的fwnode.ops。
  • 然后再设置一些参数,需要特别注意的是:对于一个struct device_node结构体,申请的内存空间是sizeof(struct device_node)+allocl,这个allocl是节点的unit_name长度(类似于chosen、memory这类子节点描述开头时的名字,并非.name成员)。
  • 然后通过np->full_name = fn = ((char )np) + sizeof(np);将device_node的full_name指向结构体结尾处,即将一个节点的unit name放置在一个struct device_node的结尾处。
  • 同时,设置其parent和sibling节点。接着,调用populate_properties()函数,从命名上来看,这个函数的作用是为节点的各个属性分配空间。紧接着,设置device_node节点的name和type属性,name由设备树中.name属性而来,type则由设备树中.device_type而来。
  • 一个设备树中节点转换成一个struct device_node结构的过程渐渐就清晰起来,现在我们接着看看populate_properties()这个函数,看看属性是怎么解析的:
  • static void populate_properties(const void *blob,int offset,void **mem,struct device_node *np,const char *nodename,bool dryrun){
    ...
    for (cur = fdt_first_property_offset(blob, offset);
    cur = 0;
    cur = fdt_next_property_offset(blob, cur))
    {
    fdt_getprop_by_offset(blob, cur, &pname, &sz);
    unflatten_dt_alloc(mem, sizeof(struct property),__alignof__(struct property));
    if (!strcmp(pname, "phandle") || !strcmp(pname, "linux,phandle")) {
    if (!np->phandle)
    np->phandle = be32_to_cpup(val);
    pp->name = (char *)pname;
    pp->length = sz;
    pp->value = (__be32 *)val;
    *pprev = pp;
    pprev = &pp->next;
    ...
    }
    }
    }
  • 从属性转换部分的程序可以看出,对于大部分的属性,都是直接填充一个struct property属性,而对于,"phandle"属性和"linux,phandle"属性,直接填充struct device_node 的phandle字段,不放在属性链表中。
  • 在设备树中,对于属性的描述是key = value,这个结构体中的name和value分别对应key和value,而length表示value的长度,next指针指向下一个struct property结构体。
  • 从属性转换部分的程序可以看出,对于大部分的属性,都是直接填充一个struct property属性,而对于,"phandle"属性和"linux,phandle"属性,直接填充struct device_node 的phandle字段,不放在属性链表中。
  • 首先,对于所有的device_node,如果要转换成platform_device,必须满足以下条件
  • (1) 该节点必须含有compatible属性
  • (2) 根节点的子节点(节点必须含有compatible属性)
  • (3) 含有特殊compatible属性的节点的子节点(子节点必须含有compatible属性):这些特殊的compatilbe属性为: “simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”
  • 具体流程分析过程:
  • Linux dts设备树和platform驱动详解_night boss的博客-CSDN博客_linux platform驱动
  • 以知晓设备树解析过程后,需要编写
  • 三、编写platform的设备树编写

  • 通过第三步我们知道了设备、驱动总线三者关系后,我们只需要编写相应的驱动程序即可,至于设备程序只需要在设备树上配置上设备表述信息。
  • 以内核中driver/leds/leds-gpio.c文件为例子。
  • 该源文件中以编写好了platform驱动,我们只需在设备树上配置对应的驱动信息就行
  • 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,
            },
    };
    
    module_platform_driver(gpio_led_driver);
    有这样的驱动。找到of_gpio_leds_match结构体
    static const struct of_device_id of_gpio_leds_match[] = {
            { .compatible = "gpio-leds", },
            {},
    };
    
    MODULE_DEVICE_TABLE(of, of_gpio_leds_match);

  • 我们需要配置下设备树,根据第三小节内容我们在设备树中找到一个是否存在compatible等于其中“simple-bus”,“simple-mfd”,“isa”,“arm,amba-bus”
  • 的结点。然后在该节点下添加一个leds的节点可参考
  • Documentation/devicetree/bindings/leds/leds-gpio.txt
  • 继续跟踪driver/leds/leds-gpio.c
  • 我们会看到一个函数
  • static int gpio_led_probe(struct platform_device *pdev)
    {
            struct gpio_led_platform_data *pdata = dev_get_platdata(&pdev->dev);
            struct gpio_leds_priv *priv;
            int i, ret = 0;
    
            if (pdata && pdata->num_leds) {
                    priv = devm_kzalloc(&pdev->dev,
                                    sizeof_gpio_leds_priv(pdata->num_leds),
                                            GFP_KERNEL);
                    if (!priv)
                            return -ENOMEM;
    
                    priv->num_leds = pdata->num_leds;
                    for (i = 0; i < priv->num_leds; i++) {
                            ret = create_gpio_led(&pdata->leds[i], &priv->leds[i],
                                                  &pdev->dev, NULL,
                                                  pdata->gpio_blink_set);
                            if (ret < 0)
                                    return ret;
                    }
            } else {
                    priv = gpio_leds_create(pdev);
                    if (IS_ERR(priv))
                            return PTR_ERR(priv);
            }
    
            platform_set_drvdata(pdev, priv);
    
            return 0;
    }

  • 通过该函数我们知道需要在设备树的key和value的相应值。
  • 如我在设备树中添加的节点
  •         leds {
                    compatible = "gpio-leds";
    
                    led1 {
                            label = "ls2k:red:led1";
                            gpios = <&pca9555_0 6 GPIO_ACTIVE_LOW>;
                            linux,default-trigger = "heartbeat";
                            default-state = "on";
                    };
    /**********************************************
                    buzz {
                            label = "ls2k:buzz";
                            gpios = <&pca9555_0 7 GPIO_ACTIVE_HIGH>;
                            linux,default-trigger = "default-off";
                            default-state = "off";
                    };
    ***********************************************/
            };

  • 其中label值是在系统启动后/sys/bus/class/leds/ls2k:red:led1的名称
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
测试用例驱动是一种基于测试用例的软件开发方法,它将测试用例视为软件开发的主要驱动力量。在Linux内核中,测试用例驱动可以帮助开发人员更容易地设计、开发和测试内核代码。 以下是一个基于Linux内核的测试用例驱动示例: 1. 定义测试用例:首先,需要定义测试用例。这些测试用例应该覆盖内核中主要的功能和功能模块。例如,可以定义一个测试用例来测试内核中的进程管理功能。 2. 编写测试用例:接下来,需要编写测试用例代码。测试用例代码应该能够模拟实际的使用情况,并验证内核代码的正确性。在进程管理测试用例中,可以编写代码来创建、销毁和管理进程。 3. 运行测试用例:一旦测试用例编写完成,就可以运行测试用例了。测试用例可以使用自动化测试框架(如JUnit)自动运行,并生成测试报告和日志。 4. 调试和修复问题:如果测试用例失败,需要进行调试和修复问题。在进程管理测试用例中,如果测试用例失败,则需要检查代码并确定错误的原因。 5. 重复测试:一旦问题得到修复,需要再次运行测试用例以确保问题已经解决。如果测试用例通过了测试,则可以将代码提交到内核代码库中。 通过测试用例驱动的方法,开发人员可以更容易地编写、测试和维护内核代码,从而提高软件质量并降低开发成本。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值