diff --git a/articles/20230511-l2-prefetch-driver.md b/articles/20230511-l2-prefetch-driver.md
new file mode 100644
index 0000000000000000000000000000000000000000..a798a1dc0214de6cc76a614989c32357e2b92ca8
--- /dev/null
+++ b/articles/20230511-l2-prefetch-driver.md
@@ -0,0 +1,293 @@
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.1 - [tounix spaces codeinline tables urls pangu autocorrect]
+> Author: Kepontry
+> Date: 2023/5/11
+> Revisor: Falcon
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [VisionFive 2 开发板软硬件评测及软件 gap 分析](https://gitee.com/tinylab/riscv-linux/issues/I64ESM)
+> Sponsor: PLCT Lab, ISCAS
+
+# 基于 RISC-V SoC JH7110 的 L2 预取器驱动分析
+
+## 简介
+
+### L2 预取器
+
+预取是一种预测 CPU 未来需要的数据,并将其提前从内存取进 Cache 的技术。赛昉科技推出的 VisionFive 2 开发板搭载的 JH7110 SoC 具有 L2 预取功能,具体介绍与参数调优可以参见 [上一篇文章][003]。
+
+### 设备驱动
+
+设备通常分为字符设备、块设备与网络设备,字符设备以字节为单位传输数据,块设备则以固定大小的块为单位。设备驱动是内核与外设的接口,字符驱动中通常需要实现 file_operations 结构体中定义的成员函数,如 open()、ioctl()、read() 和 write() 等,以完成数据交互。此外,还可以通过设置 `_ATTR` 宏的方式,在用户空间通过 sysfs 文件系统查看和写入设备属性。
+
+## L2 预取器驱动示例
+
+通过查阅[数据手册](https://starfivetech.com/uploads/u74mc_core_complex_manual_21G1.pdf),JH7110 SoC 中的四个 U74 核都配备两个 32 位的 L2 预取控制寄存器。第一个预取控制寄存器 basicCtrl 中包含的预取参数如下表所示,第二个控制寄存器 additionalCtrl 中包含的预取参数可以在 [数据手册][005] 的第 224 页找到。在 [上一篇文章][003] 中,我们通过在 U-Boot 中写入 SOC 的 MMIO 地址空间来实现控制寄存器的修改,但启动 Linux 后无法修改控制寄存器的值,十分不便。本文介绍另一种通过设备驱动实现设置预取参数的方式。
+
+| 位范围 | 变量名 | 设置值 | 用途描述 |
+|--------|------------------|--------|-----------------------------------|
+| 0 | en | 1 | 开启/关闭预取器 |
+| 1 | crossPageOptmDis | 0 | 关闭/开启跨页优化 |
+| 7-2 | dist | 3 | 设置初始预取距离 |
+| 13-8 | maxAllowedDist | 10 | 设置最大允许预取距离 |
+| 19-14 | linToExpThrd | 5 | 设置预取距离的调节速度,值越小越快 |
+| 20 | ageOutEn | 0 | 开启/关闭替换机制 |
+| 27-21 | numLdsToAgeOut | 64 | 触发替换所需的预测错误次数 |
+| 28 | crossPageEn | 0 | 开启/关闭跨页预取 |
+| 31-29 | 保留 | | |
+
+驱动与设备进行数据交互可以通过定义 file_operations 结构体中的 read() 和 write() 函数实现,但这是以字节为单位的,而预取参数以位为单位且长度不固定,在读写时会有很多不便。而且用户需要编写程序,调用 open()、read() 等一系列设备操作函数。另一种方式是将每个参数设置为设备属性,并定义 show 和 store 方法,在设备注册到 sysfs 时,在/sys 目录下生成对应参数的可读写文件,在用户空间仅需使用 cat 和 echo 命令即可实现参数读写。接下来将介绍后一种方式的示例驱动代码。
+
+### 添加设备描述
+
+与设备树相关的常见文件有三种,`.dts` 是设备树源码文件,`.dtb` 是编译后得到的二进制文件,而 `.dtsi` 是设备树的头文件。为了让驱动获取到各 CPU 预取控制寄存器的 IO 地址,需要在 `jh7110.dtsi` 文件中进行声明。以定义的第一个设备为例,冒号前的 l2pf0 是节点标签,可在设备树文件内使用,冒号后的 l2pf0 是节点名称,驱动根据该名称找到节点,`@` 后为设备地址。compatible 声明的是兼容属性,可用于驱动绑定兼容设备。reg 声明地址范围,前 64 位表示首址,后 64 位表示长度,第一个设备声明的是 0x2032000-0x2034000 这块 IO 空间。
+
+```shell
+$ cd linux
+$ vim arch/riscv/boot/dts/starfive/jh7110.dtsi
+...
+soc: soc {
+ ......
+ cachectrl: cache-controller@2010000 {
+ ......
+ };
+
++ l2pf0: l2pf0@2032000 {
++ compatible = "sifive,l2pf";
++ reg = <0x0 0x2032000 0x0 0x2000>;
++ };
++
++ l2pf1: l2pf1@2034000 {
++ compatible = "sifive,l2pf";
++ reg = <0x0 0x2034000 0x0 0x2000>;
++ };
++
++ l2pf2: l2pf2@2036000 {
++ compatible = "sifive,l2pf";
++ reg = <0x0 0x2036000 0x0 0x2000>;
++ };
++
++ l2pf3: l2pf3@2038000 {
++ compatible = "sifive,l2pf";
++ reg = <0x0 0x2038000 0x0 0x2000>;
++ };
+...
+```
+
+### 设置设备属性
+
+为了方便读写各预取参数,需要使用 `DEVICE_ATTR_RW` 宏,以在 `/sys` 目录下生成对应参数的可读写文件。查找定义可以发现,这个宏定义一个 device_attribute 结构体的设备属性,并调用了 `__ATTR_RW` 宏。`__ATTR_RW` 宏设置属性的读写权限并调用了 `_ATTR` 宏。`_ATTR` 宏最后指定属性的名称、show 函数和 store 函数。
+
+```shell
+$ cat include/linux/device.h
+......
+#define DEVICE_ATTR_RW(_name) \
+ struct device_attribute dev_attr_##_name = __ATTR_RW(_name)
+
+$ cat include/linux/sysfs.h
+......
+#define __ATTR_RW(_name) __ATTR(_name, 0644, _name##_show, _name##_store)
+
+#define __ATTR(_name, _mode, _show, _store) { \
+ .attr = {.name = __stringify(_name), \
+ .mode = VERIFY_OCTAL_PERMISSIONS(_mode) }, \
+ .show = _show, \
+ .store = _store, \
+}
+```
+
+show 函数和 store 函数是属性对应的读写函数,命名方式为属性名后加 `_show` 和 `_store`。我们定义了 `basic_attr_func(name, high, low)` 和 `add_attr_func(name, high, low)` 宏,分别辅助 basicCtrl 和 additionalCtrl 控制寄存器中各参数的定义,并且可以精简代码。其中,name 指属性名,high 和 low 分别指该参数在对应控制寄存器中的最高位和最低位。该宏自动定义了属性的 show 函数和 store 函数,并调用了 `DEVICE_ATTR_RW` 宏,声明设备属性。
+
+接下来以 `basic_attr_func(name, high, low)` 宏为例进行介绍。l2_pf_base 是控制寄存器基址,由上面的设备树中定义的 IO 地址经虚实地址转换后得到。SIFIVE_L2_PF_BASIC_CTRL 和 SIFIVE_L2_PF_ADD_CTRL 定义的是两个控制寄存器的地址偏移。reg_basic_ctrl 数组用于暂存各 CPU 对应的 basicCtrl 控制寄存器值,每次读写都将更新。`name##_mask` 变量通过移位操作,定义各属性对应的掩码,便于读写。该宏使用 readl 和 writel 函数读写控制寄存器,一次读写 4 字节,与控制寄存器大小相等。
+
+```c
+#define SIFIVE_L2_PF_BASIC_CTRL 0x00
+#define SIFIVE_L2_PF_ADD_CTRL 0x04
+#define NUM_CPUS 4
+
+static u32 reg_basic_ctrl[NUM_CPUS], reg_add_ctrl[NUM_CPUS], temp[NUM_CPUS];
+static void __iomem *l2_pf_base[NUM_CPUS];
+
+#define basic_attr_func(name, high, low) \
+static u32 name##_mask = (((1 << (high - low + 1)) - 1) << low); \
+static ssize_t name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+{ \
+ reg_basic_ctrl[dev->id] = readl(l2_pf_base[dev->id] \
+ + SIFIVE_L2_PF_BASIC_CTRL); \
+ return sprintf(buf, "%u\n", (reg_basic_ctrl[dev->id] \
+ & name##_mask) >> low); \
+} \
+static ssize_t name##_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t size) \
+{ \
+ sscanf(buf, "%u", &temp[dev->id]); \
+ reg_basic_ctrl[dev->id] = (reg_basic_ctrl[dev->id] & ~name##_mask) \
+ | ((temp[dev->id] << low) & name##_mask); \
+ writel(reg_basic_ctrl[dev->id], l2_pf_base[dev->id] + SIFIVE_L2_PF_BASIC_CTRL); \
+ return size; \
+} \
+static DEVICE_ATTR_RW(name);
+
+basic_attr_func(prefetch_enable, 0, 0)
+basic_attr_func(cross_page_opt_dis, 1, 1)
+basic_attr_func(distance, 7, 2)
+basic_attr_func(max_allow_dist, 13, 8)
+basic_attr_func(line_to_exp_thrd, 19, 14)
+basic_attr_func(age_out_enable, 20, 20)
+basic_attr_func(num_loads_to_age_out, 27, 21)
+basic_attr_func(cross_page_enable, 28, 28)
+```
+
+设置完各属性后,需要定义 attribute 属性的指针数组 l2_prefetch_attrs,包含上面定义的所有属性,并以 NULL 结尾。之后定义属性组 l2_prefetch_attr_group,指定 attrs 和 name 成员变量,这将在 `/sys/devices/system/cpu/cpux` 目录下生成一个名为 l2_prefetch 的文件夹,里面包含了上面定义的所有属性。
+
+```shell
+static struct attribute *l2_prefetch_attrs[] = {
+ &dev_attr_prefetch_enable.attr,
+ &dev_attr_cross_page_opt_dis.attr,
+ &dev_attr_distance.attr,
+ &dev_attr_max_allow_dist.attr,
+ &dev_attr_line_to_exp_thrd.attr,
+ &dev_attr_age_out_enable.attr,
+ &dev_attr_num_loads_to_age_out.attr,
+ &dev_attr_cross_page_enable.attr,
+ &dev_attr_q_full_thrd.attr,
+ &dev_attr_hit_cache_thrd.attr,
+ &dev_attr_hit_mshr_thrd.attr,
+ &dev_attr_window.attr,
+ NULL,
+};
+
+static const struct attribute_group l2_prefetch_attr_group = {
+ .attrs = l2_prefetch_attrs,
+ .name = "l2_prefetch"
+};
+```
+
+### 驱动初始化操作
+
+定义了设备属性后,还需要定义初始化函数 l2_prefetch_add_dev,通过传入的 CPU 序号,查找对应设备,在 sysfs 上创建节点并挂接属性。of_find_node_by_name 函数通过指定的名称在设备树中查找设备节点。of_iomap 函数根据找到的设备节点直接进行 ioremap 操作,将物理 IO 地址映射为虚拟地址,第二个参数为设备树节点的 reg 段索引,由于只定义了一个内存段,所以索引为 0。之后使用 readl 函数读取该 CPU 的控制寄存器,初始化 reg_basic_ctrl 和 reg_add_ctrl 数组中对应值。get_cpu_device 函数用于获取指定 CPU 的 device 结构体,并传入 sysfs_create_group 函数,在该 CPU 的 sysfs 节点下创建属性组。
+
+l2_prefetch_remove_dev 函数用于指定 CPU 被移除时的行为,这里调用 sysfs_remove_group 函数,移除属性组。使用 cpuhp_setup_state 函数以支持 CPU 热插拔功能,后两个参数用于指定 CPU 上线和下线的回调函数。最后调用 device_initcall 函数,注册 sifive_l2_pf_init 为设备初始化函数。
+
+```C
+static int l2_prefetch_add_dev(unsigned int cpu)
+{
+ struct device_node *np;
+ char buf[10];
+
+ sprintf(buf, "l2pf%u", cpu);
+ np = of_find_node_by_name(NULL, buf);
+ if (!np)
+ return -ENODEV;
+
+ l2_pf_base[cpu] = of_iomap(np, 0);
+ if (!l2_pf_base[cpu])
+ return -ENOMEM;
+
+ reg_basic_ctrl[cpu] = readl(l2_pf_base[cpu] + SIFIVE_L2_PF_BASIC_CTRL);
+ reg_add_ctrl[cpu] = readl(l2_pf_base[cpu] + SIFIVE_L2_PF_ADD_CTRL);
+
+ struct device *dev = get_cpu_device(cpu);
+ if(cpu != dev->id)
+ pr_err("L2PF: cpu %u != dev_id %u", cpu, dev->id);
+ return sysfs_create_group(&dev->kobj, &l2_prefetch_attr_group);
+}
+
+static int l2_prefetch_remove_dev(unsigned int cpu)
+{
+ struct device *dev = get_cpu_device(cpu);
+ sysfs_remove_group(&dev->kobj, &l2_prefetch_attr_group);
+ return 0;
+}
+
+static int __init sifive_l2_pf_init(void)
+{
+ return cpuhp_setup_state(CPUHP_L2PREFETCH_PREPARE,
+ "soc/l2prefetch:prepare", l2_prefetch_add_dev,
+ l2_prefetch_remove_dev);
+}
+
+device_initcall(sifive_l2_pf_init);
+```
+
+## 使用示例
+
+### 编译内核
+
+完整的示例代码 sifive_l2_prefetcher.c 放在 [GitHub 仓库][004] 中,需要将其拷贝至内核代码的 `drivers/soc/sifive/` 目录下。并向该目录下的 Makefile 中添加 `sifive_l2_prefetcher.o` 文件,以编译链接驱动代码。这里为简洁起见,默认与 L2 Cache 控制器使用同一个变量 CONFIG_SIFIVE_L2,也可以在 Kconfig 文件中定义自己的 CONFIG_SIFIVE_L2_PREFETCH 变量。
+
+```shell
+$ export LINUX_DIR=/path/to/linux
+$ git clone https://github.com/Kepontry/sifive-l2pf-driver.git
+$ cp sifive-l2pf-driver/sifive_l2_prefetcher.c $LINUX_DIR/drivers/soc/sifive/
+$ vim $LINUX_DIR/drivers/soc/sifive/Makefile
+ # SPDX-License-Identifier: GPL-2.0
+
+ obj-$(CONFIG_SIFIVE_L2) += sifive_l2_cache.o
++ obj-$(CONFIG_SIFIVE_L2) += sifive_l2_prefetcher.o
+```
+
+此外,还需要在 `cpuhotplug.h` 中定义枚举变量 CPUHP_L2PREFETCH_PREPARE,以支持 CPU 热插拔功能。
+
+```shell
+$ vim include/linux/cpuhotplug.h
+ CPUHP_TOPOLOGY_PREPARE,
++ CPUHP_L2PREFETCH_PREPARE,
+ CPUHP_NET_IUCV_PREPARE,
+```
+
+编译内核,更新内核压缩镜像 vmlinuz 和设备树 dtb 文件。重启后选择从刚编译的内核镜像启动,启动项的添加请参见 [之前的文章][001]。
+
+```shell
+# 编译内核
+$ make CROSS_COMPILE=riscv64-linux-gnu- ARCH=riscv -j$(nproc)
+$ mkdir -p vmlinuz
+# 生成 vmlinuz 文件
+$ make CROSS_COMPILE=riscv64-linux-gnu- ARCH=riscv INSTALL_PATH=~/linux/vmlinuz zinstall -j$(nproc)
+$ cp vmlinuz/* /boot/boot
+$ cp arch/riscv/boot/dts/starfive/jh7110-visionfive-v2.dtb /boot/boot/dtbs/starfive/
+# 确保落盘
+$ sync
+```
+
+### 参数读写与验证
+
+启动新内核后,使用 cat 和 echo 命令可以读写在 `/sys` 目录下创建的属性文件,注意写入操作需要 root 权限。以下示例尝试读写 CPU 0 的 prefetch_enable 变量,以关闭 CPU 0 的预取功能。
+
+```shell
+# cat /sys/devices/system/cpu/cpu0/l2_prefetch/prefetch_enable
+1
+# echo "0" > /sys/devices/system/cpu/cpu0/l2_prefetch/prefetch_enable
+# cat /sys/devices/system/cpu/cpu0/l2_prefetch/prefetch_enable
+0
+```
+
+使用 taskset 命令在 CPU 0 上运行 [之前文章][002] 中介绍的存储器山程序进行验证,与通过 U-Boot 写入物理内存从而关闭预取的现象一致。
+
+```shell
+$ cd CpuCacheMountainViewer
+$ taskset -c 0 ./mountain
+Clock frequency is approx. 4.0 MHz
+Memory mountain (MB/sec)
+ s1 s3 s5 s7 s9 s11 s13 s15 s17 s19 s21 s23 s25 s27 s29 s31
+128m 190 111 80 62 51 43 37 33 31 31 31 31 31 31 31 31
+64m 190 111 80 62 51 43 37 33 31 31 31 31 31 31 31 31
+32m 190 111 80 62 51 43 37 33 31 31 31 31 31 31 31 31
+16m 190 111 80 62 51 43 37 33 31 31 31 31 31 31 32 32
+8m 191 112 81 63 52 44 38 33 32 32 32 33 34 35 35 36
+4m 199 125 91 72 60 51 44 39 38 42 46 51 58 71 89 113
+2m 260 229 201 184 171 157 145 133 150 155 157 157 157 157 157 156
+1m 272 248 228 213 198 185 173 162 157 157 157 157 157 157 157 156
+```
+
+## 总结
+
+本文为 RISC-V SoC JH7110 实现了一个简单的 L2 预取器驱动,使得能够通过读写 `/sys` 目录下的属性文件,查看和修改预取控制参数,便于进行预取功能的性能调优。
+
+## 参考资料
+
+[001]: https://gitee.com/tinylab/riscv-linux/blob/master/articles/20230227-vf2-kernel-compile.md
+[002]: https://gitee.com/tinylab/riscv-linux/blob/master/articles/20230425-vf2-mem-mountain.md
+[003]: https://gitee.com/tinylab/riscv-linux/blob/master/articles/20230509-vf2-hw-prefetch.md
+[004]: https://github.com/Kepontry/sifive-l2pf-driver
+[005]: https://starfivetech.com/uploads/u74mc_core_complex_manual_21G1.pdf