1.设备树介绍

参考:

  1. Linux——设备树_linux 设备树-CSDN博客
  2. 设备树详解 | TimerのBlog (yanglianoo.github.io)

设备树:是一种描述硬件的数据结构,提供一种语言将硬件配置从Linux内核源码中提取出来,使得目标板和设备变成数据驱动的,它们必须基于传递给内核的数据进行初始化。

设备树文件 使用一种”Device Tree Source”(DTS)的语言编写,文件描述了硬件设备的层次结构,寄存器地址,中断线路,DMA通道和其他的相关属性。

设备树文件 经过DTC编译后会生成一种称为”Device Tree Blob”(DTB)的二进制格式。DTB文件在引导过程中由引导加载程序(Bootloader)提供给内核。内核会解析DTB文件,根据其中的描述信息初始化硬件设备,并加载相应的驱动程序。

名词解释:

  • DTSdevice tree source ,.dts文件,是一种ASCII 文本格式的文件,一般一个文件对应一个硬件平台
  • DTCdevice tree complier 编译设备树源码的编译工具,一般情况下需要手动安装这个编译工具
  • DTB:device tree bin 生成的编译文件

image-20240609150607554

2.编写quard_star的设备树文件

新建dts文件夹和quard_star_sbi.dts

dts/quard_star_sbi.dts

1
2
3
4
5
6
7
8
9
10
11
12
13
#address-cells = <0x2>;
#size-cells = <0x2>;
compatible = "riscv-quard-star";
model = "riscv-quard-star,qemu";

chosen {
stdout-path = "/soc/uart0@10000000";
};

memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x40000000>;
};
  • compatible属性:属性值类型:字符串,
    • compatible是系统用来决定绑定到设备的设备驱动的关键。 compatible属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。
  • model属性:属性值类型:字符串
    • model属性用于指定设备的制造商和型号,推荐使用“制造商, 型号”的格式,当然也可以自定义。
  • reg属性
    • reg属性描述设备资源在其父总线定义的地址空间内的地址。通常情况下用于表示一块内存的起始地址(偏移地址)和长度,
  • chosen子节点:chosen子节点位于根节点下,
    • chosen子节点不代表实际硬件,它主要用于给内核传递参数。 这里设置了uart0

总的代码,定义了CPU uart 中断等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
/dts-v1/;

/ {
#address-cells = <0x2>;
#size-cells = <0x2>;
compatible = "riscv-quard-star";
model = "riscv-quard-star,qemu";

chosen {
stdout-path = "/soc/uart0@10000000";
};

memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x40000000>;
};

cpus {
#address-cells = <0x1>;
#size-cells = <0x0>;
timebase-frequency = <0x989680>;

cpu0: cpu@0 {
phandle = <0xf>;
device_type = "cpu";
reg = <0x0>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv64imafdcsu";
mmu-type = "riscv,sv48";

interrupt-controller {
#interrupt-cells = <0x1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
phandle = <0x10>;
};
};

cpu1: cpu@1 {
phandle = <0xd>;
device_type = "cpu";
reg = <0x1>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv64imafdcsu";
mmu-type = "riscv,sv48";

interrupt-controller {
#interrupt-cells = <0x1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
phandle = <0xe>;
};
};

cpu2: cpu@2 {
phandle = <0xb>;
device_type = "cpu";
reg = <0x2>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv64imafdcsu";
mmu-type = "riscv,sv48";

interrupt-controller {
#interrupt-cells = <0x1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
phandle = <0xc>;
};
};

cpu3: cpu@3 {
phandle = <0x9>;
device_type = "cpu";
reg = <0x3>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv64imafdcsu";
mmu-type = "riscv,sv48";

interrupt-controller {
#interrupt-cells = <0x1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
phandle = <0xa>;
};
};

cpu4: cpu@4 {
phandle = <0x7>;
device_type = "cpu";
reg = <0x4>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv64imafdcsu";
mmu-type = "riscv,sv48";

interrupt-controller {
#interrupt-cells = <0x1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
phandle = <0x8>;
};
};

cpu5: cpu@5 {
phandle = <0x5>;
device_type = "cpu";
reg = <0x5>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv64imafdcsu";
mmu-type = "riscv,sv48";

interrupt-controller {
#interrupt-cells = <0x1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
phandle = <0x6>;
};
};

cpu6: cpu@6 {
phandle = <0x3>;
device_type = "cpu";
reg = <0x6>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv64imafdcsu";
mmu-type = "riscv,sv48";

interrupt-controller {
#interrupt-cells = <0x1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
phandle = <0x4>;
};
};

cpu7: cpu@7 {
phandle = <0x1>;
device_type = "cpu";
reg = <0x7>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv64imafdcsu";
mmu-type = "riscv,sv48";

interrupt-controller {
#interrupt-cells = <0x1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
phandle = <0x2>;
};
};

cpu-map {

cluster0 {

core0 {
cpu = <0xf>;
};

core1 {
cpu = <0xd>;
};

core2 {
cpu = <0xb>;
};

core3 {
cpu = <0x9>;
};

core4 {
cpu = <0x7>;
};

core5 {
cpu = <0x5>;
};

core6 {
cpu = <0x3>;
};

core7 {
cpu = <0x1>;
};
};
};
};

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

uart0: uart0@10000000 {
interrupts = <0xa>;
interrupt-parent = <0x11>;
clock-frequency = <0x384000>;
reg = <0x0 0x10000000 0x0 0x1000>;
compatible = "ns16550a";
};

uart1: uart1@10001000 {
interrupts = <0xa>;
interrupt-parent = <0x11>;
clock-frequency = <0x384000>;
reg = <0x0 0x10001000 0x0 0x1000>;
compatible = "ns16550a";
};

uart2: uart2@10002000 {
interrupts = <0xa>;
interrupt-parent = <0x11>;
clock-frequency = <0x384000>;
reg = <0x0 0x10002000 0x0 0x1000>;
compatible = "ns16550a";
};

plic@c000000 {
phandle = <0x11>;
riscv,ndev = <0x35>;
reg = <0x0 0xc000000 0x0 0x210000>;
interrupts-extended = <0x10 0xb 0x10 0x9 0xe 0xb 0xe 0x9 0xc 0xb 0xc 0x9 0xa 0xb 0xa 0x9 0x8 0xb 0x8 0x9 0x6 0xb 0x6 0x9 0x4 0xb 0x4 0x9 0x2 0xb 0x2 0x9>;
interrupt-controller;
compatible = "riscv,plic0";
#interrupt-cells = <0x1>;
#address-cells = <0x0>;
};

clint@2000000 {
interrupts-extended = <0x10 0x3 0x10 0x7 0xe 0x3 0xe 0x7 0xc 0x3 0xc 0x7 0xa 0x3 0xa 0x7 0x8 0x3 0x8 0x7 0x6 0x3 0x6 0x7 0x4 0x3 0x4 0x7 0x2 0x3 0x2 0x7>;
reg = <0x0 0x2000000 0x0 0x10000>;
compatible = "riscv,clint0";
};
};
};

3.start.s重新编写

start.s需要重新编写用于引导加载opensbi固件以及设备树,然后跳转到DRAM执行opensbi

boot/start.s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
	.macro loop,cunt
li t1, 0xffff
li t2, \cunt
1:
nop
addi t1, t1, -1
bne t1, x0, 1b
li t1, 0xffff
addi t2, t2, -1
bne t2, x0, 1b
.endm

.macro load_data,_src_start,_dst_start,_dst_end
bgeu \_dst_start, \_dst_end, 2f
1:
lw t0, (\_src_start)
sw t0, (\_dst_start)
addi \_src_start, \_src_start, 4
addi \_dst_start, \_dst_start, 4
bltu \_dst_start, \_dst_end, 1b
2:
.endm

.section .text
.globl _start
.type _start,@function

_start:
//load opensbi_fw.bin
//[0x20200000:0x20400000] --> [0x80000000:0x80200000]
li a0, 0x202
slli a0, a0, 20 //a0 = 0x20200000
li a1, 0x800
slli a1, a1, 20 //a1 = 0x80000000
li a2, 0x802
slli a2, a2, 20 //a2 = 0x80200000
load_data a0,a1,a2

//load qemu_sbi.dtb
//[0x20080000:0x20100000] --> [0x82200000:0x82280000]
li a0, 0x2008
slli a0, a0, 16 //a0 = 0x20080000
li a1, 0x822
slli a1, a1, 20 //a1 = 0x82200000
li a2, 0x8228
slli a2, a2, 16 //a2 = 0x82280000
load_data a0,a1,a2

csrr a0, mhartid
li t0, 0x0
beq a0, t0, _no_wait
loop 0x1000
_no_wait:
li a1, 0x822
slli a1, a1, 20 //a1 = 0x82200000
li t0, 0x800
slli t0, t0, 20 //t0 = 0x80000000
jr t0

.end

解析:

1
2
3
4
5
6
7
8
9
10
11
	.macro loop,cunt
li t1, 0xffff # 将0xffff 加载到寄存器他
li t2, \cunt # 将参数 cunt 的值加载到寄存器 t2
1:
nop # 空操作
addi t1, t1, -1 # t1 减 1
bne t1, x0, 1b # x0是riscv的零寄存器,值始终为0。如果 t1 不等于零,跳回标签 1,继续循环。 ben(非等)
li t1, 0xffff # 重置
addi t2, t2, -1 # t2 寄存器 减一
bne t2, x0, 1b
.endm
  • loop宏,这里定义了一个双重循环,用于执行一个固定次数的空操作。宏接收一个参数 cunt 表示外层循环数 t1是内存循环数
1
2
3
4
5
6
7
8
9
10
.macro load_data,_src_start,_dst_start,_dst_end
bgeu \_dst_start, \_dst_end, 2f # 如果 _dst_start >= _dst_end,跳到标签 2
1:
lw t0, (\_src_start) # 从 _src_start 加载一个字到 t0 lw load word
sw t0, (\_dst_start) # 将 t0 存储到 _dst_start
addi \_src_start, \_src_start, 4 # 将 _src_start 增加 4
addi \_dst_start, \_dst_start, 4 # 将 _dst_start 增加 4
bltu \_dst_start, \_dst_end, 1b # 如果 _dst_start < _dst_end,跳回标签 1
2:
.endm

load_data宏,定义了一个从源地址加载数据到目标地址的循环,直到目标地址到达结束地址。

1
2
3
.section .text
.globl _start
.type _start, @function
  • .section .text:定义代码段(.text 段),用于存放可执行代码。
  • .globl _start:声明 _start 标签为全局符号,使其在链接时可见。
  • .type _start, @function:指定 _start 为函数类型
1
2
3
4
5
6
7
8
9
//load opensbi_fw.bin 
//[0x20200000:0x20400000] --> [0x80000000:0x80200000]
li a0, 0x202
slli a0, a0, 20 //a0 = 0x20200000 左移20位
li a1, 0x800
slli a1, a1, 20 //a1 = 0x80000000
li a2, 0x802
slli a2, a2, 20 //a2 = 0x80200000
load_data a0,a1,a2
  • 加载opensbi固件
  • load_data a0, a1, a2: 调用宏 load_data,将从 0x202000000x20400000 的数据复制到 0x800000000x80200000
1
2
3
4
5
6
7
8
9
//load qemu_sbi.dtb
//[0x20080000:0x20100000] --> [0x82200000:0x82280000]
li a0, 0x2008
slli a0, a0, 16 //a0 = 0x20080000
li a1, 0x822
slli a1, a1, 20 //a1 = 0x82200000
li a2, 0x8228
slli a2, a2, 16 //a2 = 0x82280000 左移16位
load_data a0,a1,a2
  • 加载qemu sbi 设备树
  • load_data a0, a1, a2: 调用宏 load_data,将从 0x200800000x20100000 的数据复制到 0x822000000x82280000
1
2
3
4
5
6
7
8
9
10
	csrr    a0, mhartid
li t0, 0x0
beq a0, t0, _no_wait
loop 0x1000
_no_wait:
li a1, 0x822
slli a1, a1, 20 //a1 = 0x82200000
li t0, 0x800
slli t0, t0, 20 //t0 = 0x80000000
jr t0
  • 检查当前hart,

  • csrr a0, mhartid: 读取当前 hart ID(硬件线程 ID)到寄存器 a0

    li t0, 0x0: 将 0 加载到寄存器 t0

    beq a0, t0, _no_wait: 如果 a0 等于 t0(即当前 hart ID 为 0),跳转到 _no_wait 标签。

    loop 0x1000: 否则,执行一个循环(定义在上面的 loop 宏中),大约等待一段时间。

  • nowait

  • li a1, 0x822: 将 0x822 加载到寄存器 a1 中。

    slli a1, a1, 20: 将 a1 左移 20 位,使其值变为 0x82200000。 —设备树

    li t0, 0x800: 将 0x800 加载到寄存器 t0 中。

    slli t0, t0, 20: 将 t0 左移 20 位,使其值变为 0x80000000。 —opensbi

    jr t0: 跳转到 t0(即 0x80000000)处,开始执行新程序。 —opensbi

总结,这段代码实现了以下功能:

  1. 将 OpenSBI 固件从 0x20200000 复制到 0x80000000
  2. 将 QEMU SBI 设备树从 0x20080000 复制到 0x82200000
  3. 检查当前 hart ID,如果不是 0,则执行一个等待循环。
  4. 跳转到 0x80000000 处,执行新的程序。

4.build.sh和 run.sh修改

build.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#---------------------------编译opensbi-----------------------------
echo "------------------------- 编译opensbi-----------------------------"
if [ ! -d "$SHELL_FOLDER/output/opensbi" ]; then
mkdir $SHELL_FOLDER/output/opensbi
fi
cd $SHELL_FOLDER/opensbi-1.2
make CROSS_COMPILE=$CROSS_PREFIX- PLATFORM=quard_star
cp -r $SHELL_FOLDER/opensbi-1.2/build/platform/quard_star/firmware/*.bin $SHELL_FOLDER/output/opensbi/

# 生成sbi.dtb
cd $SHELL_FOLDER/dts
dtc -I dts -O dtb -o $SHELL_FOLDER/output/opensbi/quard_star_sbi.dtb quard_star_sbi.dts

# 合成firmware固件
if [ ! -d "$SHELL_FOLDER/output/fw" ]; then
mkdir $SHELL_FOLDER/output/fw
fi
cd $SHELL_FOLDER/output/fw
rm -rf fw.bin
# 填充 32K的0
dd of=fw.bin bs=1k count=32k if=/dev/zero
# # 写入 lowlevel_fw.bin 偏移量地址为 0
dd of=fw.bin bs=1k conv=notrunc seek=0 if=$SHELL_FOLDER/output/lowlevelboot/lowlevel_fw.bin
# 写入 quard_star_sbi.dtb 地址偏移量为 512K,因此 fdt的地址偏移量为 0x80000
dd of=fw.bin bs=1k conv=notrunc seek=512 if=$SHELL_FOLDER/output/opensbi/quard_star_sbi.dtb
# 写入 fw_jump.bin 地址偏移量为 2K*1K= 0x2000000,因此 fw_jump.bin的地址偏移量为 0x2000000
dd of=fw.bin bs=1k conv=notrunc seek=2k if=$SHELL_FOLDER/output/opensbi/fw_jump.bin

操作:

  1. 首先编译了opensbi,并且指定了板
  2. 编译生成设备树dtb
  3. 合成固件
  • 根据dd命令的使用可以看见将quard_star_sbi.dtb写入到了偏移地址为0x80000的地方,用于这个固件会被加载到flash处,所以设备树的地址为:0x20080000 —- 对应了start.s文件加载设备树的地址
  • fw_jump.bin 地址偏移量为 2K*1K= 0x2000000,因此 fw_jump.bin的地址偏移量为 0x2000000,同理 这个固件会被加载到flash,所以opensbi固件的地址为:0x20200000 —对应start.s 加载 opensbi的位置
  • lowlevel_fw.bin 偏移量地址为 0,因此lowlevel_fw.bin 的地址为:0x20000000
  • dd of=fw.bin bs=1k count=32k if=/dev/zero 来填充填充 32K的0,这是qemu使用-drive加载的固件的大小要大于等于定义的flash的大小,不然会报错。

注意

现在固件包含三个内容:

  1. 首先是起始0x20000000lowlevel_fw.bin
    1. lowlevel的作用是将opensbi 以及设备树文件从flash中 加载到 DRAM(0x80000000),然后跳转到DRAM执行
  2. 然后是0x20080000地址处的dtb文件
  3. 接下来是0x20200000处的opensbi程序

run.sh

1
2
3
4
5
6
7
8
9
10
SHELL_FOLDER=$(cd "$(dirname "$0")";pwd)

$SHELL_FOLDER/output/qemu/bin/qemu-system-riscv64 \
-M quard-star \
-m 1G \
-smp 8 \
-bios none \
-drive if=pflash,bus=0,unit=0,format=raw,file=$SHELL_FOLDER/output/fw/fw.bin \
-d in_asm -D qemu.log \
-nographic --parallel none \

这里添加了一个qemu.log会生成汇编代码,可以根据这个log来查看固件是否有正确的加载、跳转。

执行build.sh run.sh

报错./build.sh: 45: dtc: not found

执行:sudo apt-get install device-tree-complier

还需要修改

quard_star.c 把DRAM内存扩大

1
[QUARD_STAR_DRAM]  = { 0x80000000,    0x40000000 },

显示结果

image-20240609181609447

5.内存布局

  1. rom中先从(fw_dynamic_info这个结构体指示了flash的起始地址 在哪找到它)flash记载lowlevel,然后跳转执行flash (flash中的固件是直接 drive注入的)
  2. flash中存放lowerlevel.binopensbi固件,设备树固件
  3. opensbi,设备树固件加载到DRAM 并跳转执行

image-20240609182439432