4. reg属性深度解析

好,咱们今天来啃一块硬骨头——reg属性。

说实话,我刚接触设备树那会儿,最头疼的就是这个reg。看着一堆数字,完全不知道它在说什么。后来做BSP调优,踩了不少坑,才慢慢摸透了它的脾气。

今天我就把这块掰开了揉碎了讲给你听。保证你学完就能自己写。

4.1 reg属性到底长什么样?

先看一个最简单的例子:

uart@ff010000 {
    compatible = "ns16550";
    reg = <0xff010000 0x1000>;
};

这个reg里有两个数字:0xff010000 和 0x1000。

第一个是起始地址,第二个是长度。说白了,就是告诉内核:「我的寄存器从0xff010000开始,占了4KB的空间,你可别让别人碰这里。」

核心公式:

reg = < 地址1 长度1 地址2 长度2 ... >

每一对「地址+长度」描述一段连续的内存区域。

嗯,这里要注意:地址和长度都是32位整数。那如果地址是64位的怎么办?这就引出了下一个话题。

4.2 address-cells 和 size-cells——reg的翻译官

你想想看,CPU读设备树的时候,怎么知道reg里哪个数字是地址、哪个是长度?

答案就在父节点里:#address-cells 和 #size-cells。

  • #address-cells:表示地址需要几个32位整数来描述
  • #size-cells:表示长度需要几个32位整数来描述

举个例子:

soc {
    #address-cells = <1>;
    #size-cells = <1>;

    uart@ff010000 {
        reg = <0xff010000 0x1000>;
    };
};

这里address-cells=1,size-cells=1。所以reg里每两个数一组:第一个是地址,第二个是长度。

那64位地址怎么写?看这个:

soc {
    #address-cells = <2>;
    #size-cells = <1>;

    memory@8000000000 {
        reg = <0x00000008 0x00000000 0x10000000>;
    };
};

地址用了两个32位数拼成64位:0x00000008_00000000。长度还是1个数:0x10000000(256MB)。

我的小技巧:

我习惯在写设备树前,先看一眼父节点的address-cells和size-cells。这俩值决定了reg的解析方式。搞反了,地址和长度就全乱了。

4.3 多段内存区域描述

一个设备可能有多段不连续的地址空间。比如一个网卡,控制寄存器在一块区域,DMA缓冲在另一块区域。

这时候reg可以写多组:

ethernet@ff300000 {
    compatible = "my-gmac";
    reg = <0xff300000 0x1000    /* 控制寄存器 */
           0xff310000 0x2000>;  /* DMA描述符区 */
};

内核解析时,会按顺序把每组地址-长度对存到struct resource数组里。驱动里用platform_get_resource()按索引取就行了。

我曾经踩过的坑:

有一次我写SPI Flash的reg,把地址和长度写反了。结果内核报错说「resource overlap」。查了两天才发现,原来是address-cells和size-cells没对上。从那以后,我每次写reg都会先确认父节点的这俩属性。

4.4 实战:描述一个SPI Flash

好,理论说完了。咱们来写一个真实的SPI Flash设备树节点。

假设硬件是这样的:

  • SPI控制器挂在APB总线上,地址0x10000000,大小4KB
  • Flash芯片是Winbond W25Q128,容量16MB
  • Flash的地址范围从0x20000000到0x20FFFFFF

先画个结构图,帮你理清关系:

SoC APB 总线 #address-cells = <1> #size-cells = <1> SPI 控制器 reg = <0x10000000 0x1000> SPI Flash reg = <0x20000000 0x1000000> SPI 片选 内存映射关系 0x10000000 - 0x10000FFF : SPI 控制器寄存器 0x20000000 - 0x20FFFFFF : SPI Flash 映射区

好,现在写设备树:

/ {
    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        spi@10000000 {
            compatible = "my-spi-controller";
            reg = <0x10000000 0x1000>;
            interrupts = <0 31 4>;
            clocks = <&clk 10>;
            
            #address-cells = <1>;
            #size-cells = <1>;

            flash@0 {
                compatible = "winbond,w25q128", "jedec,spi-nor";
                reg = <0x20000000 0x1000000>;
                spi-max-frequency = <104000000>;

                partitions {
                    compatible = "fixed-partitions";
                    #address-cells = <1>;
                    #size-cells = <1>;

                    partition@0 {
                        label = "bootloader";
                        reg = <0x00000000 0x00100000>;
                    };
                    partition@1 {
                        label = "kernel";
                        reg = <0x00100000 0x00800000>;
                    };
                    partition@2 {
                        label = "rootfs";
                        reg = <0x00900000 0x00700000>;
                    };
                };
            };
        };
    };
};

这里有几个关键点:

  1. SPI控制器的reg:描述它自己的寄存器空间
  2. Flash的reg:描述Flash在CPU地址空间中的映射范围
  3. 分区reg:描述每个分区在Flash内部的偏移和大小

注意:

Flash的reg地址0x20000000,是CPU访问Flash的映射基地址,不是Flash芯片内部的地址。芯片内部地址从0开始,由驱动通过SPI命令访问。

我个人习惯在写这种多层嵌套的reg时,先画个地址映射表:

节点 地址 长度 说明
spi@10000000 0x10000000 0x1000 SPI控制器寄存器
flash@0 0x20000000 0x1000000 Flash内存映射区(16MB)
partition@0 0x00000000 0x00100000 Bootloader(1MB)
partition@1 0x00100000 0x00800000 Kernel(8MB)
partition@2 0x00900000 0x00700000 Rootfs(7MB)

你看,有了这个表,每个reg在说什么就一目了然了。

避坑指南:

我曾经在调试一个SPI Flash驱动时,发现分区读取总是偏移。查了半天,原来是分区reg的地址写成了绝对地址(0x20000000+偏移),而不是相对Flash基地址的偏移。记住:分区的reg是相对地址,不是绝对地址。

好了,reg属性的核心内容就这些。说白了就是三件事:

  • 搞清楚父节点的address-cells和size-cells
  • 按「地址+长度」的格式写reg
  • 多段区域就写多组

下次你写设备树的时候,照着这个思路来,基本不会出错。

可可精品资料库