Skip to content

Instantly share code, notes, and snippets.

@cyyself
Last active December 2, 2025 16:46
Show Gist options
  • Select an option

  • Save cyyself/227e99c5a707308737396804188ed088 to your computer and use it in GitHub Desktop.

Select an option

Save cyyself/227e99c5a707308737396804188ed088 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
class DTSGen:
def __init__(self,
compatible: str = "xiangshan,nemu-board",
model: str = "XiangShan",
isa_extensions: list = ["i", "m", "a", "f", "d", "c"],
mmu_type="riscv,sv39",
timebase_freq: int = 10000000, # 10 MHz
nr_harts: int = 1,
clint_addr: int = 0x38000000,
plic_addr: int = 0x3c000000,
memories: list = [(0x80000000, 8*1024*1024*1024)], # [(start, size)], default 8 GB at 2 GB
reserved_memories: list = [], # [(start, size)]
bootargs: str = "console=hvc0 earlycon=sbi rdinit=/sbin/init",
rng_seed: bytes = b"NEMU_BOARD",
uartlite_addr: int = 0x40600000,
nemu_sdhci_addr: int = None):
self.isa_extensions = isa_extensions
self.mmu_type = mmu_type
self.timebase_freq = timebase_freq
self.nr_harts = nr_harts
self.clint_addr = clint_addr
self.plic_addr = plic_addr
self.memories = memories
self.reserved_memories = reserved_memories
self.compatible = compatible
self.model = model
self.bootargs = bootargs
# Note: console=hvc0 is a hack for NEMU Uartlite to use SBI
# console to get around the lack of TX FIFO empty interrupt
# implementation. The proper param should be "console=ttyUL0"
# for Uartlite IP on Xilinx FPGA.
# Ensure we have set the following kernel configs:
# CONFIG_NONPORTABLE=y
# CONFIG_HVC_RISCV_SBI=y
self.rng_seed = rng_seed
self.soc_devices = [] # [dev_str0, ...]
if uartlite_addr is not None and plic_addr is not None:
self.soc_devices.append(self.__gen_uartlite(uartlite_addr, 3))
if nemu_sdhci_addr is not None:
self.soc_devices.append(self.__gen_nemu_sdhci(nemu_sdhci_addr))
def __indent(self, text, level: int = 4):
indent_str = ' ' * level
return "\n".join(indent_str + line if line.strip() != "" else line for line in text.split("\n"))
def __gen_addrsize(self, addrsize, cell_number):
res = ""
for i in range(cell_number):
res += f"0x{(addrsize >> (32 * (cell_number - i - 1))) & 0xFFFFFFFF:x} "
return res.strip()
def __gen_memory(self, memory_start, memory_size):
return f"""
memory@{memory_start:x} {{
device_type = "memory";
reg = <{self.__gen_addrsize(memory_start, 2)} {self.__gen_addrsize(memory_size, 2)}>;
}};
""".strip()
def __gen_isa_string(self, isa, cacheline_size=64):
# As riscv,isa has been deprecated, leave it as rv64imafdc is enough for compatibility
# Cache parameters are used for Zicbom / Zicbop / Zicboz
# support in Linux
return f"""
{ f"riscv,cbom-block-size = <{cacheline_size}>;" if "zicbom" in isa else "" }
{ f"riscv,cbop-block-size = <{cacheline_size}>;" if "zicbop" in isa else "" }
{ f"riscv,cboz-block-size = <{cacheline_size}>;" if "zicboz" in isa else "" }
riscv,isa = "rv64imafdc";
riscv,isa-base = "rv64i";
riscv,isa-extensions = {", ".join(f'"{ext}"' for ext in isa)};
""".strip()
def __gen_cpu_node(self, hart_id, isa):
# Reference: https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/riscv/cpus.yaml
return f"""
cpu{hart_id}: cpu@{hart_id} {{
compatible = "riscv";
device_type = "cpu";
mmu-type = "{self.mmu_type}";
reg = <{hart_id}>;
{self.__indent(self.__gen_isa_string(isa))}
cpu{hart_id}_intc: interrupt-controller {{
#interrupt-cells = <1>;
compatible = "riscv,cpu-intc";
interrupt-controller;
}};
}};
""".strip()
def __gen_cpus(self):
return f"""
cpus {{
#address-cells = <1>;
#size-cells = <0>;
timebase-frequency = <{self.timebase_freq}>;
{self.__indent('\n'.join(self.__gen_cpu_node(hart_id, self.isa_extensions) + "\n" for hart_id in range(self.nr_harts)))}
}};
""".strip()
def __gen_clint(self):
# Reference: https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/timer/sifive%2Cclint.yaml
if self.clint_addr == None:
return ""
# 3 is MSI, 7 is MTI
cpu_intc_list = [f"<&cpu{hart_id}_intc 3>, <&cpu{hart_id}_intc 7>" for hart_id in range(self.nr_harts)]
cpu_intc_head = " interrupts-extended = ";
cpu_intc_str = cpu_intc_head
for each_intc in zip(cpu_intc_list, list(range(self.nr_harts))):
cpu_intc_str += each_intc[0]
if each_intc[1] != self.nr_harts - 1:
cpu_intc_str += ",\n" + " " * (len(cpu_intc_head))
return f"""
clint: clint@{self.clint_addr:x} {{
compatible = "riscv,clint0";
reg = <{self.__gen_addrsize(self.clint_addr, 2)} {self.__gen_addrsize(0x10000, 2)}>;
{cpu_intc_str};
}};
""".strip()
def __gen_plic(self, max_priority: int = 7, ndev: int = 64):
# Reference: https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/interrupt-controller/sifive%2Cplic-1.0.0.yaml
if self.plic_addr is None:
return ""
# 9 is SEI, 11 is MEI
cpu_intc_list = [f"<&cpu{hart_id}_intc 9>, <&cpu{hart_id}_intc 11>" for hart_id in range(self.nr_harts)]
cpu_intc_head = " interrupts-extended = ";
cpu_intc_str = cpu_intc_head
for each_intc in zip(cpu_intc_list, list(range(self.nr_harts))):
cpu_intc_str += each_intc[0]
if each_intc[1] != self.nr_harts - 1:
cpu_intc_str += ",\n" + " " * (len(cpu_intc_head))
return f"""
plic: plic@{self.plic_addr:x} {{
compatible = "riscv,plic0";
reg = <{self.__gen_addrsize(self.plic_addr, 2)} {self.__gen_addrsize(0x400000, 2)}>;
#interrupt-cells = <1>;
interrupt-controller;
{cpu_intc_str};
riscv,max-priority = <{max_priority}>;
riscv,ndev = <{ndev}>;
}};
""".strip()
def __gen_reserved_memory(self):
if len(self.reserved_memories) == 0:
return ""
res = f"""
reserved-memory {{
#address-cells = <2>;
#size-cells = <2>;
ranges;
"""
for ((start, size), idx) in zip(self.reserved_memories, list(range(len(self.reserved_memories)))):
res += self.__indent(f"""
resv{idx}@{start:x} {{
reg = <{self.__gen_addrsize(start, 2)} {self.__gen_addrsize(size, 2)}>;
no-map;
}};
""".strip() + "\n")
res += "};"
return res
def __gen_uartlite(self, uartlite_addr, plic_addr):
# https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/serial/xlnx%2Copb-uartlite.yaml
# Uartlite hardware does not handle clock division, so we do
# not specify clocks here
return f"""
serial@{uartlite_addr:x} {{
compatible = "xlnx,xps-uartlite-1.00.a";
reg = <{self.__gen_addrsize(uartlite_addr, 2)} {self.__gen_addrsize(0x1000, 2)}>;
interrupts-extended = <&plic {plic_addr}>;
current-speed = <115200>;
xlnx,data-bits = <8>;
xlnx,use-parity = <0>;
}};
""".strip()
def __gen_nemu_sdhci(self, sd_addr):
return f"""
mmc@{sd_addr:x} {{
compatible = "nemu,sdhost";
reg = <{self.__gen_addrsize(sd_addr, 2)} {self.__gen_addrsize(0x1000, 2)}>;
}};
""".strip()
def __gen_soc(self):
res = f"""
soc {{
#address-cells = <2>;
#size-cells = <2>;
compatible = "simple-bus";
ranges;
""" + "\n"
for dev_str in self.soc_devices:
res += self.__indent(dev_str, 4) + "\n\n"
res += "};"
return res.strip()
def add_device(self, dev_str: str):
self.soc_devices.append(dev_str)
def gen_dts(self):
return f"""
/dts-v1/;
/ {{
#address-cells = <2>;
#size-cells = <2>;
compatible = "{self.compatible}";
model = "{self.model}";
chosen {{
{self.__indent(f'bootargs = "{self.bootargs}";', 8) if self.bootargs else ''}
{self.__indent(f'rng-seed = /bits/ 8 <{" ".join(f"0x{b:02x}" for b in self.rng_seed)}>;', 8) if self.rng_seed else ''}
}};
{self.__indent('\n'.join(self.__gen_memory(start, size) for (start, size) in self.memories))}
{self.__indent(self.__gen_cpus())}
{self.__indent(self.__gen_clint())}
{self.__indent(self.__gen_plic())}
{self.__indent(self.__gen_reserved_memory())}
{self.__indent(self.__gen_soc())}
}};
""".strip()
if __name__ == "__main__":
gen = DTSGen(
isa_extensions=["i", "m", "a", "f", "d", "c", "zicbom", "zicbop", "zicboz"],
nr_harts=1
)
print(gen.gen_dts())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment