> For the complete documentation index, see [llms.txt](https://richardweiyang-2.gitbook.io/understanding_qemu/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://richardweiyang-2.gitbook.io/understanding_qemu/00-devices/00-an_example/05-nvdimm.md).

# NVDIMM

NVDIMM是PCDIMM的子类型，所以整体的流程基本差不多，着重讲一下几个不同之处。

## 全局

我们先来看一下全局上，一个nvdimm设备初始化并加入系统有哪些步骤。

```
main()
    qemu_opts_foreach(qemu_find_opts("device"), device_init_func, NULL, NULL)
        ...
        device_set_realized()
            hotplug_handler_pre_plug
                ...
                memory_device_pre_plug
                    nvdimm_prepare_memory_region                (1)

            if (dc->realize) {
              dc->realize(dev, &local_err);
            }

            hotplug_handler_plug
                ...
                nvdimm_plug
                    nvdimm_build_fit_buffer                     (2)
    qemu_run_machine_init_done_notifiers()
        pc_machine_done
            acpi_setup
                acpi_build
                    nvdimm_build_acpi                           (3)
                        nvdimm_build_ssdt
                        nvdimm_build_nfit

                acpi_add_rom_blob(build_state, tables.table_data,
                                  "etc/acpi/tables", 0x200000);
                acpi_add_rom_blob(build_state, tables.linker->cmd_blob,
                                  "etc/table-loader", 0);
```

从上面的图中可以看到NVDIMM设备初始化和PCDIMM设备类似，也是有这么几个步骤

* 插入准备
* 实例化
* 插入善后
* 添加ACPI表

其中大部分的工作和PCDIMM一样，只有标注了1，2，3的三个地方需要额外的工作。

分别是：

1. 添加nvdimm label区域
2. 创建NFIT表的内容
3. 创建SSDT和NFIT并加入acpi

## 分配label区域

按照当前的实现，使用nvdimm设备时，在分配空间的最后挖出一块做label。这个工作在nvdimm\_prepare\_memory\_region中完成。

这块label区域在虚拟机内核中通过DSM方法来操作，后面我们会看到DSM方法是如何虚拟化的。

## 填写nfit信息

nfit表是用来描述nvdimm设备上地址空间组织形式的，所以这部分需要在计算好了dimm地址后进行，有nvdimm\_build\_fit\_buffer完成。

展开这个函数我们可以看到

```
for (; device_list; device_list = device_list->next) {
    DeviceState *dev = device_list->data;

    /* build System Physical Address Range Structure. */
    nvdimm_build_structure_spa(structures, dev);

    /*
     * build Memory Device to System Physical Address Range Mapping
     * Structure.
     */
    nvdimm_build_structure_memdev(structures, dev);

    /* build NVDIMM Control Region Structure. */
    nvdimm_build_structure_dcr(structures, dev);
}
```

也就是分别构建了 SPA, MEMDEV和DCR三个信息。而这些信息就在内核函数add\_table中处理。

不看具体内容，我们只看add\_table中类型的定义：

```
enum acpi_nfit_type {
    ACPI_NFIT_TYPE_SYSTEM_ADDRESS = 0,
    ACPI_NFIT_TYPE_MEMORY_MAP = 1,
    ACPI_NFIT_TYPE_INTERLEAVE = 2,
    ACPI_NFIT_TYPE_SMBIOS = 3,
    ACPI_NFIT_TYPE_CONTROL_REGION = 4,
    ACPI_NFIT_TYPE_DATA_REGION = 5,
    ACPI_NFIT_TYPE_FLUSH_ADDRESS = 6,
    ACPI_NFIT_TYPE_CAPABILITIES = 7,
    ACPI_NFIT_TYPE_RESERVED = 8    /* 8 and greater are reserved */
};
```

上面三个类型对应了这个枚举类型的0，1，4。

## 构建acpi

最终nvdimm的信息要填写到acpi中，一共有两张表：SSDT和NFIT。这部分工作在nvdimm\_build\_acpi中完成。

其中NFIT表在nvdimm\_build\_nfit函数中完成，其实没有太多内容。就是将上面创建好的表内容拷贝过来。

而SSDT这张表就讲究多了，这里面涉及了acpi的一些概念。

### ACPI Table

在进入代码之前，还是要先了解一下acpi的一些概念。这里并不是系统性介绍acpi，只是补充一些代码阅读中涉及的概念。

ACPI(Advanced Configuration and Power Management Interface)，简单理解就是用来描述硬件配置的。

为了做好管理，acpi当然提供了很多复杂的功能，其中我们关注的就是acpi table。而这些表的作用比较直观，那就是按照一定的规范来描述硬件设备的属性。

规范中定义了很多表，而这些表按照我的理解可以分成两类：

* DSDT & SSDT
* 其他

DSDT & SSDT 虽然也叫表，但是他们使用AML语言编写的，可以被解析执行的。也就是这两张表是可编程的。 而其他的表则是静态的，按照标准写好的数据结构。

### 查看Table

在linux上可以很方便得查看acpi table。因为他们都在sysfs上。

> /sys/firmware/acpi/tables/

文件的内容还需要使用工具查看。

```
# acpidump -b
```

这个命令将会把所有的acpi table的二进制形式导出到.dat文件，并保存在当前目录下。

比如我们就看nvdimm中创建的NFIT表，那么再执行

```
# iasl NFIT.dat
```

就能解析成可阅读的文件了。在我的虚拟机上显示如下，而且正好就是spa, memdev, dcr三个部分。

```
[000h 0000   4]                    Signature : "NFIT"    [NVDIMM Firmware Interface Table]
[004h 0004   4]                 Table Length : 000000E0
[008h 0008   1]                     Revision : 01
[009h 0009   1]                     Checksum : BF
[00Ah 0010   6]                       Oem ID : "BOCHS "
[010h 0016   8]                 Oem Table ID : "BXPCNFIT"
[018h 0024   4]                 Oem Revision : 00000001
[01Ch 0028   4]              Asl Compiler ID : "BXPC"
[020h 0032   4]        Asl Compiler Revision : 00000001

[024h 0036   4]                     Reserved : 00000000

[028h 0040   2]                Subtable Type : 0000 [System Physical Address Range]
[02Ah 0042   2]                       Length : 0038

[02Ch 0044   2]                  Range Index : 0002
[02Eh 0046   2]        Flags (decoded below) : 0003
                   Add/Online Operation Only : 1
                      Proximity Domain Valid : 1
[030h 0048   4]                     Reserved : 00000000
[034h 0052   4]             Proximity Domain : 00000000
[038h 0056  16]           Address Range GUID : 66F0D379-B4F3-4074-AC43-0D3318B78CDB
[048h 0072   8]           Address Range Base : 00000001C0000000
[050h 0080   8]         Address Range Length : 0000000278000000
[058h 0088   8]         Memory Map Attribute : 0000000000008008

[060h 0096   2]                Subtable Type : 0001 [Memory Range Map]
[062h 0098   2]                       Length : 0030

[064h 0100   4]                Device Handle : 00000001
[068h 0104   2]                  Physical Id : 0000
[06Ah 0106   2]                    Region Id : 0000
[06Ch 0108   2]                  Range Index : 0002
[06Eh 0110   2]         Control Region Index : 0003
[070h 0112   8]                  Region Size : 0000000278000000
[078h 0120   8]                Region Offset : 0000000000000000
[080h 0128   8]          Address Region Base : 0000000000000000
[088h 0136   2]             Interleave Index : 0000
[08Ah 0138   2]              Interleave Ways : 0001
[08Ch 0140   2]                        Flags : 0000
                       Save to device failed : 0
                  Restore from device failed : 0
                       Platform flush failed : 0
                            Device not armed : 0
                      Health events observed : 0
                       Health events enabled : 0
                              Mapping failed : 0
[08Eh 0142   2]                     Reserved : 0000

[090h 0144   2]                Subtable Type : 0004 [NVDIMM Control Region]
[092h 0146   2]                       Length : 0050

[094h 0148   2]                 Region Index : 0003
[096h 0150   2]                    Vendor Id : 8086
[098h 0152   2]                    Device Id : 0001
[09Ah 0154   2]                  Revision Id : 0001
[09Ch 0156   2]          Subsystem Vendor Id : 0000
[09Eh 0158   2]          Subsystem Device Id : 0000
[0A0h 0160   2]        Subsystem Revision Id : 0000
[0A2h 0162   1]                 Valid Fields : 00
[0A3h 0163   1]       Manufacturing Location : 00
[0A4h 0164   2]           Manufacturing Date : 0000
[0A6h 0166   2]                     Reserved : 0000
[0A8h 0168   4]                Serial Number : 00123456
[0ACh 0172   2]                         Code : 0301
[0AEh 0174   2]                 Window Count : 0000
[0B0h 0176   8]                  Window Size : 0000000000000000
[0B8h 0184   8]               Command Offset : 0000000000000000
[0C0h 0192   8]                 Command Size : 0000000000000000
[0C8h 0200   8]                Status Offset : 0000000000000000
[0D0h 0208   8]                  Status Size : 0000000000000000
[0D8h 0216   2]                        Flags : 0000
                            Windows buffered : 0
[0DAh 0218   6]                    Reserved1 : 000000000000
```

### AML & ASL

开始我们也说了，acpi table分成两类。刚才的facs属于第二类，也就是静态的数据结构。

而对于第一种表，他们则复杂得多，是用AML编写的。

* AML: ACPI Machine Language
* ASL: ACPI source language

所以前者相当于机器语言，而后者相当于汇编。

解析这两张表的方式和刚才的一样也是通过iasl命令，但是样子就完全不一样了。我们就直接拿nvdimm构造ssdt来看。

[NVDIMM SSDT](https://gist.github.com/RichardWeiYang/aea8e71f5c9ff71499d19e77eb8a777e)

刚开始看确实有点头大，不过慢慢就好了。我稍微来解释一下。

**定义函数**

```
Method (NCAL, 5, Serialized)
```

所以看到这个表中定义了好些函数： \_DSM, RFIT, \_FIT 。

值得注意的是以\_ 开头的函数是在规范中定义的，而其他的则是可以自己添加的。

**定义变量**

如果说函数定义还比较好理解，那么变量的定义就有点不那么直观了。

最简单的方式是赋值：

```
  Local6 = MEMA
```

当然这个变量Local6是自定义的，只要直接使用就好。

另外一种就是比较特殊的，我称之为**指针方式**。因为看着和c语言的指针访问很像。而且这种方式需要做两步。

* 定义Region
* 定义Field

先定义出一块空间，里面包含了这块空间对应的起始地址和长度。

```
OperationRegion (NPIO, SystemIO, 0x0A18, 0x04)
```

然后定义这块空间的field，这点上看和c语言的结构体很像。

```
Field (NPIO, DWordAcc, NoLock, Preserve)
{
    NTFI,   32
}
```

这样相当于得到了一个指针变量NFIT，这个地址是指向0x0A18的4个字节。

最后访问这个区域就是直接用赋值语句：

```
NTFI = Local6
```

就相当于往NTFI这个指针写了Local6的值。

### 模拟nvdimm的\_DSM

好了，绕了这么大一圈，终于可以回到正题看看nvdimm的\_DSM是如何模拟的。

所谓模拟，也就是在guest执行\_DSM方法时截获该操作，由qemu进行模拟。

#### guest内核

所以正式开始前，我们还得看一眼内核中执行\_DSM时做了点什么。

下面的代码截取自acpi\_evaluate\_dsm()

```
    params[0].type = ACPI_TYPE_BUFFER;
    params[0].buffer.length = 16;
    params[0].buffer.pointer = (u8 *)guid;
    params[1].type = ACPI_TYPE_INTEGER;
    params[1].integer.value = rev;
    params[2].type = ACPI_TYPE_INTEGER;
    params[2].integer.value = func;
    if (argv4) {
        params[3] = *argv4;
    } else {
        params[3].type = ACPI_TYPE_PACKAGE;
        params[3].package.count = 0;
        params[3].package.elements = NULL;
    }

    ret = acpi_evaluate_object(handle, "_DSM", &input, &buf);
```

别的不管，主要看一共传进来了四个参数。比如有版本号和函数。这里先记着，留着后面再来看。

#### AML

回到之前看得acpi表总的来说，SSDT表在地址空间上构建了这么两段：

```
                      0xA18                 MEMA
   +------------------+--+------------------+------+-------------+
   |                  |  |                  |      |             |
   +------------------+--+------------------+------+-------------+
                                          /          \
                                        /              \
                                        +--------------+
                                        |HDLE          |
                                        |REVS          |
                                        |FUNC          |
                                        |FARG          |
                                        +--------------+
```

其目的就是创建了一块共享内存，用来在guest和qemu之间传递模拟\_DSM的参数和返回值。

比如我们可以在aml文件中看到

```
REVS = Arg1
FUNC = Arg2
```

而其中Arg1就是内核传递下来的参数rev，Arg2是内核传递参数的func。

#### Qemu

当AML执行了 NFIT = Local6 的时候，接下去的操作就被Qemu截获了。而这个Local6参数的值是什么呢？

> Local6 = MEMA

所以，实际上就是告诉了Qemu一块共享内存的空间。这下是不是明白了？

那Qemu截获后是如何操作？这还要看nvdimm\_dsm\_ops了。因为是写操作，所以只看write方法。

其中有一个变量很引人注目：NvdimmDsmIn。

```
NvdimmDsmIn *in;

in->revision = le32_to_cpu(in->revision);
in->function = le32_to_cpu(in->function);
in->handle = le32_to_cpu(in->handle);
```

而这个变量类型定义为：

```
struct NvdimmDsmIn {
    uint32_t handle;
    uint32_t revision;
    uint32_t function;
    /* the remaining size in the page is used by arg3. */
    union {
        uint8_t arg3[4084];
    };
} QEMU_PACKED;
```

这下是不是和AML中共享内存的结构对应起来了？

好了，再详细的细节就和具体的函数实现相关了。留给大家自己去探索把～

## 最后的疑问

如果大家仔细看，比较qemu中aml的代码和guest中SSDT生成的文件，其中还有一个问题没有解决。

那就是 Local6 = MEMA 的这个MEMA的地址是如何得到的。

这个就牵扯到了另外两个超纲的知识点，将在[FW\_CFG](/understanding_qemu/00-qmeu_bios_guest.md)章节中解开这个谜团。
