* [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo
@ 2026-10-21 12:54 Hiago De Franco
2026-10-21 12:54 ` [PATCH 1/8] binman: add Sophgo CV18xx FIP entry type Hiago De Franco
` (8 more replies)
0 siblings, 9 replies; 16+ messages in thread
From: Hiago De Franco @ 2026-10-21 12:54 UTC (permalink / raw)
To: u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
This series adds SPL support for the Milk-V Duo (Sophgo CV1800B, 64 MiB
DDR2), so the board no longer needs the vendor FSBL to bring up DDR,
configure the PLLs and hand off to OpenSBI and U-Boot proper.
Currently this board only builds a bare u-boot.bin that depends on the
vendor FSBL (and the vendor fiptool) to do all of the early hardware
bring-up before U-Boot starts. With this series:
- The SPL is loaded directly by the BootROM from fip.bin, built by a new
binman entry type that reproduces the fiptool image layout (param1
header + BL2 header) without depending on the vendor tool.
- SPL enables the C906 caches and T-HEAD ISA extensions, brings up and
trains the DDR2 controller/PHY, and initializes the PLLs, all ported
from the Sophgo FSBL sources [1] (plat/cv180x).
- SPL also takes over the RTC power domain configuration for shutdown,
warm reset, power-cycle and watchdog reset, matching what the vendor
FSBL used to configure.
- SPL then loads the FIT image (OpenSBI, U-Boot and the DT) placed at
offset 0x40000 of fip.bin via the BootROM ROMAPI, and hands off to
OpenSBI, which starts U-Boot in S-mode.
- Board documentation is updated.
Tested by booting a Milk-V Duo board from an SD card (fip.bin on the FAT
partition) built with this series (SPL -> OpenSBI -> U-Boot -> Linux).
This series used the help of Claude Opus 5.5 to read the FSBL and turn
it into U-Boot SPL.
[1] https://github.com/sophgo/fsbl.git
Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
---
Hiago De Franco (8):
binman: add Sophgo CV18xx FIP entry type
riscv: cv1800b: add SPL support
ram: sophgo: add CV180x DDR2 driver
board: sophgo: milkv_duo: add SPL support
board: sophgo: milkv_duo: initialize PLLs in SPL
board: sophgo: milkv_duo: enable RTC power and reset
doc: board: sophgo: milkv_duo: update documentation
doc: board: sophgo: milkv_duo: document the SPL boot flow
arch/riscv/cpu/cv1800b/Kconfig | 2 +
arch/riscv/cpu/cv1800b/Makefile | 4 +
arch/riscv/cpu/cv1800b/spl.c | 31 +
arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi | 92 +++
arch/riscv/include/asm/arch-cv1800b/rom_api.h | 36 ++
board/sophgo/milkv_duo/Kconfig | 5 +
board/sophgo/milkv_duo/MAINTAINERS | 6 +-
board/sophgo/milkv_duo/Makefile | 1 +
board/sophgo/milkv_duo/spl.c | 250 ++++++++
configs/milkv_duo_defconfig | 18 +
doc/board/sophgo/milkv_duo.rst | 76 ++-
drivers/ram/Kconfig | 1 +
drivers/ram/Makefile | 1 +
drivers/ram/sophgo/Kconfig | 6 +
drivers/ram/sophgo/Makefile | 6 +
drivers/ram/sophgo/cv180x_ddr.c | 140 +++++
drivers/ram/sophgo/cvx16_pinmux.c | 54 ++
drivers/ram/sophgo/ddr_patch_regs.c | 39 ++
drivers/ram/sophgo/ddr_pkg_info.c | 110 ++++
drivers/ram/sophgo/ddr_sys.c | 635 +++++++++++++++++++++
drivers/ram/sophgo/ddr_sys.h | 108 ++++
drivers/ram/sophgo/ddrc_init.c | 476 +++++++++++++++
drivers/ram/sophgo/phy_init.c | 193 +++++++
tools/binman/etype/sophgo_cv18xx_fip.py | 89 +++
tools/binman/ftest.py | 54 ++
tools/binman/test/vendor/sophgo_cv18xx_fip.dts | 18 +
.../test/vendor/sophgo_cv18xx_fip_bad_offset.dts | 16 +
.../test/vendor/sophgo_cv18xx_fip_collection.dts | 19 +
28 files changed, 2454 insertions(+), 32 deletions(-)
---
base-commit: a58e2b7af42c179ace2c6210de0a219fa3f3cf3c
change-id: 20261017-milkv-duo-spl-2e2fae6858f1
prerequisite-change-id: 20260929-milkv-duo-bootstd-14de5e4e03ef:v2
prerequisite-patch-id: 0aaa37872f2b82c2abfc84364f9256c7cb3394fe
prerequisite-patch-id: 927c8bc825dd9897467da6b194ae8c7c411ec874
prerequisite-patch-id: 7c0fedb64aabab799b72eef81efc8becec8f44cd
Best regards,
--
Hiago
^ permalink raw reply [flat|nested] 16+ messages in thread
* [PATCH 1/8] binman: add Sophgo CV18xx FIP entry type
2026-10-21 12:54 [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Hiago De Franco
@ 2026-10-21 12:54 ` Hiago De Franco
2026-11-03 14:54 ` Simon Glass
2026-10-21 12:54 ` [PATCH 2/8] riscv: cv1800b: add SPL support Hiago De Franco
` (7 subsequent siblings)
8 siblings, 1 reply; 16+ messages in thread
From: Hiago De Franco @ 2026-10-21 12:54 UTC (permalink / raw)
To: u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
Add a binman entry type that builds the FIP image loaded by the
BootROM of the Sophgo CV180x/CV181x SoCs named fip.bin.
The image reuses the layout produced by the vendor fiptool: a 4KiB
param1 header with size and CRC-16/XMODEM checksums, followed by a
32-byte BL2 header (first instruction jumps over it). Unlike the
vendor tool, only the entry's subnodes (typically u-boot-spl) are
packed as BL2, since the BootROM does not use the BLCP, param2,
monitor or second stage loader it also supports.
Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
---
tools/binman/etype/sophgo_cv18xx_fip.py | 89 ++++++++++++++++++++++
tools/binman/ftest.py | 54 +++++++++++++
tools/binman/test/vendor/sophgo_cv18xx_fip.dts | 18 +++++
.../test/vendor/sophgo_cv18xx_fip_bad_offset.dts | 16 ++++
.../test/vendor/sophgo_cv18xx_fip_collection.dts | 19 +++++
5 files changed, 196 insertions(+)
diff --git a/tools/binman/etype/sophgo_cv18xx_fip.py b/tools/binman/etype/sophgo_cv18xx_fip.py
new file mode 100644
index 00000000000..6fb189d2616
--- /dev/null
+++ b/tools/binman/etype/sophgo_cv18xx_fip.py
@@ -0,0 +1,89 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+#
+# Entry-type module for the Sophgo CV18xx BootROM FIP image
+#
+
+import binascii
+import struct
+
+from binman.etype.section import Entry_section
+from u_boot_pylib import tools
+
+FIP_ALIGN = 0x200
+PARAM1_SIZE = 0x1000
+
+# jal zero, +32: jump over the BL2 header expected by the BootROM
+BL2_HEADER = struct.pack('<I', 0x0200006f) + tools.get_bytes(0, 28)
+
+# The subnodes follow param1 and the BL2 header
+SPL_OFFSET = PARAM1_SIZE + len(BL2_HEADER)
+
+# Empty register list, only the "scan start" terminator
+CHIP_CONF = struct.pack('<II', 0xffffffa0, 0xffffffff)
+
+
+def fip_cksum(data):
+ """CRC-16/XMODEM followed by the 0xcafe marker"""
+ return struct.pack('<HH', binascii.crc_hqx(data, 0), 0xcafe)
+
+
+class Entry_sophgo_cv18xx_fip(Entry_section):
+ """Sophgo CV18xx FIP image
+
+ This creates the FIP image loaded by the BootROM of the Sophgo CV180x and
+ CV181x SoCs, named fip.bin. The subnodes, typically just u-boot-spl, form
+ the BL2 image, which the BootROM loads into SRAM and runs::
+
+ sophgo-cv18xx-fip {
+ u-boot-spl {
+ };
+ };
+
+ The image contains:
+
+ - param1: 4KiB header with the BL2 size and checksums, read by the BootROM
+ - BL2: a 32-byte header whose first instruction jumps over it, followed by
+ the subnodes, aligned to 512 bytes
+
+ Unlike the vendor fiptool, no BLCP, param2, monitor or second stage loader
+ is packed, as the BootROM does not use them: SPL loads the next stages
+ itself, e.g. from a FIT placed after this entry.
+ """
+
+ def _PackEntries(self):
+ """Pack the subnodes after param1 and the BL2 header"""
+ offset = SPL_OFFSET
+ for entry in self.GetEntries().values():
+ offset = entry.Pack(offset)
+ return offset
+
+ def BuildSectionData(self, required):
+ data = super().BuildSectionData(required)
+ if data is None:
+ return None
+
+ # Once packed, the subnodes are preceded by the space reserved for
+ # param1 and the BL2 header, which is filled in below
+ entries = self.GetEntries().values()
+ if all(entry.offset is not None for entry in entries):
+ if any(entry.offset < SPL_OFFSET for entry in entries):
+ self.Raise('Subnodes must be placed at %#x or later' %
+ SPL_OFFSET)
+ data = data[SPL_OFFSET:]
+
+ bl2 = BL2_HEADER + data
+ bl2 += tools.get_bytes(0, tools.align(len(bl2), FIP_ALIGN) - len(bl2))
+
+ param1 = bytearray(PARAM1_SIZE)
+ param1[0x00:0x08] = b'CVBL01\n\0'
+ param1[0x90:0xb4] = tools.get_bytes(0xff, 36) # NOR_INFO
+ struct.pack_into('<I', param1, 0xbc, len(CHIP_CONF)) # CHIP_CONF_SIZE
+ param1[0xc0:0xc4] = fip_cksum(b'') # BLCP_IMG_CKSUM
+ param1[0xd4:0xd8] = fip_cksum(bl2) # BL2_IMG_CKSUM
+ struct.pack_into('<I', param1, 0xd8, len(bl2)) # BL2_IMG_SIZE
+ param1[0xe8:0xe8 + len(CHIP_CONF)] = CHIP_CONF
+ # PARAM_CKSUM covers NAND_INFO up to the signatures
+ param1[0x0c:0x10] = fip_cksum(param1[0x10:0x800])
+
+ return bytes(param1) + bl2
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index a0e8bde7901..413850cf13d 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -6,6 +6,7 @@
#
# python -m unittest func_test.TestFunctional.testHelp
+import binascii
import collections
import configparser
import glob
@@ -5598,6 +5599,59 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
self.assertIn("Node '/binman/renesas-rcar4-sa0': SRAM data longer than 966656 Bytes",
str(exc.exception))
+ def testSophgoCv18xxFip(self):
+ """Test that binman can produce a Sophgo CV18xx FIP image"""
+ def cksum(buf):
+ return struct.pack('<HH', binascii.crc_hqx(buf, 0), 0xcafe)
+
+ self._SetupSplElf()
+ data = self._DoReadFileRealDtb('vendor/sophgo_cv18xx_fip.dts')
+
+ # param1
+ self.assertEqual(b'CVBL01\n\0', data[:8])
+ self.assertEqual(cksum(data[0x10:0x800]), data[0xc:0x10])
+ self.assertEqual(tools.get_bytes(0xff, 36), data[0x90:0xb4])
+ self.assertEqual(struct.pack('<I', 8), data[0xbc:0xc0])
+ self.assertEqual(cksum(b''), data[0xc0:0xc4])
+ bl2 = data[0x1000:0x1200]
+ self.assertEqual(cksum(bl2), data[0xd4:0xd8])
+ self.assertEqual(struct.pack('<I', len(bl2)), data[0xd8:0xdc])
+ self.assertEqual(struct.pack('<II', 0xffffffa0, 0xffffffff),
+ data[0xe8:0xf0])
+
+ # BL2: header, SPL and padding to 512 bytes
+ spl_end = 0x20 + len(U_BOOT_SPL_DATA)
+ self.assertEqual(struct.pack('<I', 0x0200006f) +
+ tools.get_bytes(0, 28), bl2[:0x20])
+ self.assertEqual(U_BOOT_SPL_DATA, bl2[0x20:spl_end])
+ self.assertEqual(tools.get_bytes(0, 0x200 - spl_end), bl2[spl_end:])
+
+ fip = control.images['image'].GetEntries()['sophgo-cv18xx-fip']
+ self.assertEqual(0x1200, fip.size)
+ spl = fip.GetEntries()['u-boot-spl']
+ self.assertEqual(0x1020, spl.offset)
+ self.assertEqual(0x1020, spl.image_pos)
+
+ image_fname = tools.get_output_filename('image.bin')
+ self.assertEqual(U_BOOT_SPL_DATA,
+ control.ReadEntry(image_fname,
+ 'sophgo-cv18xx-fip/u-boot-spl'))
+
+ def testSophgoCv18xxFipCollection(self):
+ """Test using a Sophgo CV18xx FIP image in a collection"""
+ self._SetupSplElf()
+ data = self._DoReadFile('vendor/sophgo_cv18xx_fip_collection.dts')
+ self.assertEqual(data[:0x1200], data[0x1200:])
+
+ def testSophgoCv18xxFipBadOffset(self):
+ """Test a Sophgo CV18xx FIP subnode overlapping the headers"""
+ self._SetupSplElf()
+ with self.assertRaises(ValueError) as exc:
+ self._DoReadFile('vendor/sophgo_cv18xx_fip_bad_offset.dts')
+ self.assertIn(
+ "Node '/binman/sophgo-cv18xx-fip': Subnodes must be placed at 0x1020 or later",
+ str(exc.exception))
+
def testFitFdtOper(self):
"""Check handling of a specified FIT operation"""
entry_args = {
diff --git a/tools/binman/test/vendor/sophgo_cv18xx_fip.dts b/tools/binman/test/vendor/sophgo_cv18xx_fip.dts
new file mode 100644
index 00000000000..7bec144e2b7
--- /dev/null
+++ b/tools/binman/test/vendor/sophgo_cv18xx_fip.dts
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ sophgo-cv18xx-fip {
+ u-boot-spl {
+ };
+ };
+
+ fdtmap {
+ };
+ };
+};
diff --git a/tools/binman/test/vendor/sophgo_cv18xx_fip_bad_offset.dts b/tools/binman/test/vendor/sophgo_cv18xx_fip_bad_offset.dts
new file mode 100644
index 00000000000..8c0b68008e2
--- /dev/null
+++ b/tools/binman/test/vendor/sophgo_cv18xx_fip_bad_offset.dts
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ sophgo-cv18xx-fip {
+ u-boot-spl {
+ offset = <0>;
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/vendor/sophgo_cv18xx_fip_collection.dts b/tools/binman/test/vendor/sophgo_cv18xx_fip_collection.dts
new file mode 100644
index 00000000000..a13cb7b7ba6
--- /dev/null
+++ b/tools/binman/test/vendor/sophgo_cv18xx_fip_collection.dts
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ collection {
+ content = <&fip>;
+ };
+
+ fip: sophgo-cv18xx-fip {
+ u-boot-spl {
+ };
+ };
+ };
+};
--
2.53.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH 2/8] riscv: cv1800b: add SPL support
2026-10-21 12:54 [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Hiago De Franco
2026-10-21 12:54 ` [PATCH 1/8] binman: add Sophgo CV18xx FIP entry type Hiago De Franco
@ 2026-10-21 12:54 ` Hiago De Franco
2026-11-03 13:53 ` Yao Zi
2026-10-21 12:54 ` [PATCH 3/8] ram: sophgo: add CV180x DDR2 driver Hiago De Franco
` (6 subsequent siblings)
8 siblings, 1 reply; 16+ messages in thread
From: Hiago De Franco @ 2026-10-21 12:54 UTC (permalink / raw)
To: u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
Add SUPPORT_SPL and SPL_CPU.
harts_early_init() enables the C906 caches and T-HEAD ISA extensions.
Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
---
arch/riscv/cpu/cv1800b/Kconfig | 2 ++
arch/riscv/cpu/cv1800b/Makefile | 4 ++++
arch/riscv/cpu/cv1800b/spl.c | 31 +++++++++++++++++++++++++++++++
3 files changed, 37 insertions(+)
diff --git a/arch/riscv/cpu/cv1800b/Kconfig b/arch/riscv/cpu/cv1800b/Kconfig
index 57f724ae043..29a65b93942 100644
--- a/arch/riscv/cpu/cv1800b/Kconfig
+++ b/arch/riscv/cpu/cv1800b/Kconfig
@@ -7,7 +7,9 @@ config SOPHGO_CV1800B
select ARCH_EARLY_INIT_R
select SYS_CACHE_SHIFT_6
select SYS_CACHE_THEAD_CMO
+ select SUPPORT_SPL
imply CPU
imply CPU_RISCV
imply RISCV_TIMER
+ imply SPL_CPU
imply CMD_CPU
diff --git a/arch/riscv/cpu/cv1800b/Makefile b/arch/riscv/cpu/cv1800b/Makefile
index da12e0f64e1..d16211f3a60 100644
--- a/arch/riscv/cpu/cv1800b/Makefile
+++ b/arch/riscv/cpu/cv1800b/Makefile
@@ -2,5 +2,9 @@
#
# Copyright (c) 2024, Kongyang Liu <seashell11234455@gmail.com>
+ifdef CONFIG_XPL_BUILD
+obj-y += spl.o
+else
obj-y += dram.o
obj-y += cpu.o
+endif
diff --git a/arch/riscv/cpu/cv1800b/spl.c b/arch/riscv/cpu/cv1800b/spl.c
new file mode 100644
index 00000000000..6fd62d66da7
--- /dev/null
+++ b/arch/riscv/cpu/cv1800b/spl.c
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ *
+ * CSR setup based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * lib/cpu/riscv/bl2_entrypoint.S.
+ */
+
+#include <asm/csr.h>
+#include <linux/bitops.h>
+
+#define CSR_MCOR 0x7c2
+#define CSR_MCOR_CACHE_INV BIT(4)
+#define CSR_MCOR_CACHE_SEL_BOTH (BIT(1) | BIT(0))
+#define CSR_MHCR 0x7c1
+#define CSR_MHCR_DE BIT(1)
+#define CSR_MHCR_IE BIT(0)
+#define CSR_MXSTATUS 0x7c0
+#define CSR_MXSTATUS_CLINTEE BIT(17)
+#define CSR_MXSTATUS_MAEE BIT(21)
+#define CSR_MXSTATUS_MM BIT(15)
+#define CSR_MXSTATUS_THEADISAEE BIT(22)
+#define CSR_MXSTATUS_UCME BIT(16)
+
+void harts_early_init(void)
+{
+ csr_write(CSR_MXSTATUS, CSR_MXSTATUS_THEADISAEE | CSR_MXSTATUS_MAEE |
+ CSR_MXSTATUS_CLINTEE | CSR_MXSTATUS_UCME | CSR_MXSTATUS_MM);
+ csr_write(CSR_MCOR, CSR_MCOR_CACHE_INV | CSR_MCOR_CACHE_SEL_BOTH);
+ csr_set(CSR_MHCR, CSR_MHCR_IE | CSR_MHCR_DE);
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH 3/8] ram: sophgo: add CV180x DDR2 driver
2026-10-21 12:54 [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Hiago De Franco
2026-10-21 12:54 ` [PATCH 1/8] binman: add Sophgo CV18xx FIP entry type Hiago De Franco
2026-10-21 12:54 ` [PATCH 2/8] riscv: cv1800b: add SPL support Hiago De Franco
@ 2026-10-21 12:54 ` Hiago De Franco
2026-10-21 12:54 ` [PATCH 4/8] board: sophgo: milkv_duo: add SPL support Hiago De Franco
` (5 subsequent siblings)
8 siblings, 0 replies; 16+ messages in thread
From: Hiago De Franco @ 2026-10-21 12:54 UTC (permalink / raw)
To: u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
Add a RAM driver that brings up and trains the DDR2 controller and PHY
of the Sophgo CV180x (CV1800B) SoC, for use in SPL.
The driver is ported from the Sophgo FSBL [1]
(plat/cv180x/ddr/ddr_sys_bring_up.c), split into the same stages as the
vendor code: controller/PHY init, register patching, pinmux setup and
package detection, followed by read-gating, write-DQ and read-DQ
training with BIST verification after each step. DDR2 has no write
leveling.
[1] https://github.com/sophgo/fsbl
Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
---
board/sophgo/milkv_duo/MAINTAINERS | 1 +
drivers/ram/Kconfig | 1 +
drivers/ram/Makefile | 1 +
drivers/ram/sophgo/Kconfig | 6 +
drivers/ram/sophgo/Makefile | 6 +
drivers/ram/sophgo/cv180x_ddr.c | 140 ++++++++
drivers/ram/sophgo/cvx16_pinmux.c | 54 +++
drivers/ram/sophgo/ddr_patch_regs.c | 39 +++
drivers/ram/sophgo/ddr_pkg_info.c | 110 +++++++
drivers/ram/sophgo/ddr_sys.c | 635 ++++++++++++++++++++++++++++++++++++
drivers/ram/sophgo/ddr_sys.h | 108 ++++++
drivers/ram/sophgo/ddrc_init.c | 476 +++++++++++++++++++++++++++
drivers/ram/sophgo/phy_init.c | 193 +++++++++++
13 files changed, 1770 insertions(+)
diff --git a/board/sophgo/milkv_duo/MAINTAINERS b/board/sophgo/milkv_duo/MAINTAINERS
index 6941dc5b376..5e15e461b17 100644
--- a/board/sophgo/milkv_duo/MAINTAINERS
+++ b/board/sophgo/milkv_duo/MAINTAINERS
@@ -5,3 +5,4 @@ F: board/sophgo/milkv_duo/
F: board/sophgo/common/
F: configs/milkv_duo_defconfig
F: doc/board/sophgo/milkv_duo.rst
+F: drivers/ram/sophgo/
diff --git a/drivers/ram/Kconfig b/drivers/ram/Kconfig
index acd6d9cfd6a..3432aa30839 100644
--- a/drivers/ram/Kconfig
+++ b/drivers/ram/Kconfig
@@ -145,6 +145,7 @@ source "drivers/ram/octeon/Kconfig"
source "drivers/ram/renesas/Kconfig"
source "drivers/ram/rockchip/Kconfig"
source "drivers/ram/sifive/Kconfig"
+source "drivers/ram/sophgo/Kconfig"
source "drivers/ram/stm32mp1/Kconfig"
source "drivers/ram/starfive/Kconfig"
source "drivers/ram/sunxi/Kconfig"
diff --git a/drivers/ram/Makefile b/drivers/ram/Makefile
index 82afd5fcbcc..e9f4581c4f0 100644
--- a/drivers/ram/Makefile
+++ b/drivers/ram/Makefile
@@ -33,4 +33,5 @@ obj-$(CONFIG_CADENCE_DDR_CTRL) += cadence/
ifdef CONFIG_XPL_BUILD
obj-$(CONFIG_SPL_THEAD_TH1520_DDR) += thead/
+obj-$(CONFIG_SPL_SOPHGO_CV180X_DDR) += sophgo/
endif
diff --git a/drivers/ram/sophgo/Kconfig b/drivers/ram/sophgo/Kconfig
new file mode 100644
index 00000000000..44ba85dbcff
--- /dev/null
+++ b/drivers/ram/sophgo/Kconfig
@@ -0,0 +1,6 @@
+config SPL_SOPHGO_CV180X_DDR
+ bool "Sophgo CV180x DDR driver in SPL"
+ depends on SPL_RAM && SOPHGO_CV1800B
+ help
+ This enables the DDR2 controller and PHY initialization and
+ training for the Sophgo CV180x (CV1800B) SoC in SPL.
diff --git a/drivers/ram/sophgo/Makefile b/drivers/ram/sophgo/Makefile
new file mode 100644
index 00000000000..161f993b4ed
--- /dev/null
+++ b/drivers/ram/sophgo/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Copyright (c) 2026 Hiago De Franco <hfranco@baylibre.com>
+
+obj-y += cv180x_ddr.o ddr_pkg_info.o ddr_sys.o cvx16_pinmux.o \
+ ddrc_init.o phy_init.o ddr_patch_regs.o
diff --git a/drivers/ram/sophgo/cv180x_ddr.c b/drivers/ram/sophgo/cv180x_ddr.c
new file mode 100644
index 00000000000..28af828a2f7
--- /dev/null
+++ b/drivers/ram/sophgo/cv180x_ddr.c
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Sophgo CV180x DDR2 driver for SPL
+ *
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ *
+ * Based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * plat/cv180x/ddr/ddr_sys_bring_up.c.
+ *
+ */
+
+#include <config.h>
+#include <dm.h>
+#include <linux/sizes.h>
+#include <ram.h>
+
+#include "ddr_sys.h"
+
+struct cv180x_ddr_priv {
+ struct ram_info info;
+};
+
+static int cv180x_ddr_bist(const char *step, bool sram)
+{
+ int ret;
+
+ if (sram)
+ cvx16_bist_wr_sram_init();
+ else
+ cvx16_bist_wr_prbs_init();
+
+ ret = cvx16_bist_start_check();
+ if (ret)
+ debug("DDR: %s BIST failed after %s: %d\n",
+ sram ? "SRAM" : "PRBS", step, ret);
+
+ return ret;
+}
+
+static int cv180x_ddr_probe(struct udevice *dev)
+{
+ struct cv180x_ddr_priv *priv = dev_get_priv(dev);
+ u8 vendor, dram_cap;
+ int ret;
+
+ ret = read_ddr_pkg_info(&vendor);
+ if (ret)
+ return ret;
+
+ cvx16_pll_init();
+ ddrc_init(vendor);
+
+ /* Release the DDRC soft reset */
+ writel(0x0, DDR_TOP_BASE + 0x20);
+
+ /*
+ * AXI QoS: M1 VIP realtime 0xa, M2 VIP offline 0x8, M3 CPU 0x7,
+ * M4 TPU 0x0, M5 video codec 0x9, M6 high speed peripherals 0x2
+ */
+ writel(0x007788aa, TOP_BASE + 0x1d8);
+ writel(0x00002299, TOP_BASE + 0x1dc);
+
+ phy_init();
+ writel(0x02620504, PHYD_BASE + 0xa4);
+ ret = cvx16_pinmux(vendor);
+ if (ret)
+ return ret;
+ ddr_patch_set();
+ cvx16_en_rec_vol_mode();
+
+ cvx16_set_dfi_init_start();
+ cvx16_ddr_phy_power_on_seq1();
+ cvx16_polling_dfi_init_start();
+ cvx16_dfi_init_start_isr();
+ cvx16_ddr_phy_power_on_seq3();
+ cvx16_wait_for_dfi_init_complete();
+ cvx16_polling_synp_normal_mode();
+ cv180x_ddr_bist("init", false);
+
+ /* DDR2 has no write leveling */
+ ctrl_init_low_patch();
+ cv180x_ddr_bist("low patch", false);
+ cvx16_rdglvl_req();
+ cv180x_ddr_bist("rdglvl", false);
+
+ cvx16_wdqlvl_req(WDQLVL_DQ_DM);
+ cvx16_wdqlvl_req(WDQLVL_DQ);
+ cvx16_wdqlvl_req(WDQLVL_DM);
+ cv180x_ddr_bist("wdqlvl", false);
+
+ /* param_phyd_pirdlvl_capture_cnt = 1 */
+ clrsetbits_le32(PHYD_BASE + 0x8c, GENMASK(7, 4), FIELD_PREP(GENMASK(7, 4), 1));
+ cvx16_rdlvl_req();
+ cv180x_ddr_bist("rdlvl", false);
+
+ ctrl_init_high_patch();
+ ret = ctrl_init_detect_dram_size(&dram_cap);
+ if (ret)
+ return ret;
+ ctrl_init_update_by_dram_size(vendor, dram_cap);
+ cvx16_clk_gating_enable();
+
+ ret = cv180x_ddr_bist("training", false) ?: cv180x_ddr_bist("training", true);
+ if (ret) {
+ pr_err("DDR: BIST failed\n");
+ return ret;
+ }
+
+ priv->info.base = CFG_SYS_SDRAM_BASE;
+ priv->info.size = (size_t)SZ_1M << dram_cap;
+
+ return 0;
+}
+
+static int cv180x_ddr_get_info(struct udevice *dev, struct ram_info *info)
+{
+ struct cv180x_ddr_priv *priv = dev_get_priv(dev);
+
+ *info = priv->info;
+
+ return 0;
+}
+
+static const struct ram_ops cv180x_ddr_ops = {
+ .get_info = cv180x_ddr_get_info,
+};
+
+static const struct udevice_id cv180x_ddr_ids[] = {
+ { .compatible = "sophgo,cv1800b-ddr" },
+ { }
+};
+
+U_BOOT_DRIVER(cv180x_ddr) = {
+ .name = "cv180x_ddr",
+ .id = UCLASS_RAM,
+ .of_match = cv180x_ddr_ids,
+ .ops = &cv180x_ddr_ops,
+ .probe = cv180x_ddr_probe,
+ .priv_auto = sizeof(struct cv180x_ddr_priv),
+};
diff --git a/drivers/ram/sophgo/cvx16_pinmux.c b/drivers/ram/sophgo/cvx16_pinmux.c
new file mode 100644
index 00000000000..835c993253f
--- /dev/null
+++ b/drivers/ram/sophgo/cvx16_pinmux.c
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Sophgo CV180x DDR PHY DQ/CA swap for each SiP DDR2 vendor
+ *
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ *
+ * Based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * plat/cv180x/ddr/cvx16_pinmux.c.
+ *
+ */
+
+#include "ddr_sys.h"
+
+struct ddr_pinmux {
+ u8 vendor;
+ u32 regs[12];
+};
+
+static const struct ddr_pinmux ddr_pinmux[] = {
+ { DDR_VENDOR_ESMT_512M_DDR2, { /* N25_DDR2_512 */
+ 0x0c06080b, 0x070d0904, 0x00010502, 0x110a0e03,
+ 0x0f141610, 0x00151312, 0x00000000, 0x00000100,
+ 0x71840532, 0x00000006, 0x76103425, 0x00000008,
+ } },
+ { DDR_VENDOR_ETRON_512M_DDR2, { /* ETRON_DDR2_512 */
+ 0x070b090c, 0x04050608, 0x0e02030d, 0x110a0100,
+ 0x0f131614, 0x00151012, 0x00000000, 0x00000100,
+ 0x86014532, 0x00000007, 0x76012345, 0x00000008,
+ } },
+ { DDR_VENDOR_UNILC_N25_512M_DDR2, { /* UniIC_DDR2_512M */
+ 0x00040608, 0x02090b0c, 0x0e01070d, 0x0a031605,
+ 0x10110f14, 0x00121513, 0x00000000, 0x00000100,
+ 0x61437025, 0x00000008, 0x63410725, 0x00000008,
+ } },
+};
+
+int cvx16_pinmux(u8 vendor)
+{
+ const struct ddr_pinmux *p;
+ int i;
+
+ for (p = ddr_pinmux; p < ddr_pinmux + ARRAY_SIZE(ddr_pinmux); p++) {
+ if (p->vendor != vendor)
+ continue;
+
+ for (i = 0; i < ARRAY_SIZE(p->regs); i++)
+ writel(p->regs[i], PHYD_BASE + i * 4);
+ return 0;
+ }
+
+ pr_err("DDR: no pinmux for vendor %x\n", vendor);
+
+ return -ENODEV;
+}
diff --git a/drivers/ram/sophgo/ddr_patch_regs.c b/drivers/ram/sophgo/ddr_patch_regs.c
new file mode 100644
index 00000000000..31f003150fd
--- /dev/null
+++ b/drivers/ram/sophgo/ddr_patch_regs.c
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Sophgo CV180x DDR2-1333 x16 PHY patch settings
+ *
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ *
+ * Based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * plat/cv180x/ddr/ddr_config/ddr2_1333_x16/ddr_patch_regs.c.
+ *
+ */
+
+#include "ddr_sys.h"
+
+static const struct ddr_reg ddr_patch_regs[] = {
+ /* BYTE0 DQ driving */
+ { 0xa38, 0x00000808 },
+ /* BYTE1 DQ driving */
+ { 0xa78, 0x00000808 },
+
+ /* BYTE0 RX DQ deskew */
+ { 0xb00, 0x03010504 },
+ { 0xb04, 0x02030203 },
+ /* BYTE0 DQ8 deskew [6:0], neg DQS [15:8], pos DQS [23:16] */
+ { 0xb08, 0x002a4204 },
+
+ /* BYTE1 RX DQ deskew */
+ { 0xb30, 0x08030803 },
+ { 0xb34, 0x02070205 },
+ /* BYTE1 DQ8 deskew [6:0], neg DQS [15:8], pos DQS [23:16] */
+ { 0xb38, 0x002b4003 },
+};
+
+void ddr_patch_set(void)
+{
+ const struct ddr_reg *r;
+
+ for (r = ddr_patch_regs; r < ddr_patch_regs + ARRAY_SIZE(ddr_patch_regs); r++)
+ writel(r->val, PHYD_BASE + r->off);
+}
diff --git a/drivers/ram/sophgo/ddr_pkg_info.c b/drivers/ram/sophgo/ddr_pkg_info.c
new file mode 100644
index 00000000000..64d818dda56
--- /dev/null
+++ b/drivers/ram/sophgo/ddr_pkg_info.c
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Sophgo CV180x SiP DDR detection
+ *
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ *
+ * Based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * plat/cv180x/ddr/ddr_pkg_info.c.
+ *
+ */
+
+#include "ddr_sys.h"
+
+#define REG_CONF_INFO (TOP_BASE + 0x4)
+#define CONF_INFO_PKG_TYPE GENMASK(30, 28)
+#define REG_GP_REG3 (TOP_BASE + 0x8c)
+#define REG_EFUSE_LEAKAGE ((void __iomem *)0x03050108)
+#define EFUSE_DDR_VENDOR GENMASK(25, 21)
+#define EFUSE_DDR_CAPACITY GENMASK(28, 26)
+#define EFUSE_DDR_PKG GENMASK(31, 29)
+
+/* Second source parts, described by the efuse */
+#define PKG_TYPE_FROM_EFUSE_0 0x0
+#define PKG_TYPE_FROM_EFUSE_1 0x4
+
+#define DDR_CAPACITY_UNKNOWN 0
+#define DDR_CAPACITY_512M 1
+#define DDR_CAPACITY_1G 2
+#define DDR_CAPACITY_2G 3
+#define DDR_CAPACITY_4G 4
+
+#define PKG_QFN88 1
+#define PKG_BGA 2
+#define PKG_QFN68 3
+
+struct ddr_pkg {
+ u8 vendor;
+ u8 capacity;
+ u8 pkg;
+};
+
+static const struct ddr_pkg ddr_pkg_types[] = {
+ /* QFN68 7x7, 1Gb DDR3 */
+ [0x2] = { DDR_VENDOR_ESMT_1G, DDR_CAPACITY_1G, PKG_QFN68 },
+ /* QFN68 7x7, 512Mb DDR2 (25nm) */
+ [0x3] = { DDR_VENDOR_ESMT_512M_DDR2, DDR_CAPACITY_512M, PKG_QFN68 },
+ /* QFN88 9x9, 1Gb DDR3 */
+ [0x6] = { DDR_VENDOR_ESMT_1G, DDR_CAPACITY_1G, PKG_QFN88 },
+ /* QFN88 9x9, 512Mb DDR2 (25nm) */
+ [0x7] = { DDR_VENDOR_ESMT_512M_DDR2, DDR_CAPACITY_512M, PKG_QFN88 },
+};
+
+static bool ddr_vendor_is_ddr2(u8 vendor)
+{
+ switch (vendor) {
+ case DDR_VENDOR_ESMT_512M_DDR2:
+ case DDR_VENDOR_ETRON_512M_DDR2:
+ case DDR_VENDOR_UNILC_N25_512M_DDR2:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/*
+ * Identify the DDR from the package type strap and the efuse, and record the
+ * chip id in GP_REG3 for later stages. Only DDR2 is supported.
+ */
+int read_ddr_pkg_info(u8 *vendor)
+{
+ u32 conf_info = readl(REG_CONF_INFO);
+ u32 efuse = readl(REG_EFUSE_LEAKAGE);
+ u32 pkg_type = FIELD_GET(CONF_INFO_PKG_TYPE, conf_info);
+ struct ddr_pkg ddr = { };
+ u32 chip_id = 0;
+
+ if (pkg_type == PKG_TYPE_FROM_EFUSE_0 || pkg_type == PKG_TYPE_FROM_EFUSE_1) {
+ ddr.vendor = FIELD_GET(EFUSE_DDR_VENDOR, efuse);
+ ddr.capacity = FIELD_GET(EFUSE_DDR_CAPACITY, efuse);
+ ddr.pkg = FIELD_GET(EFUSE_DDR_PKG, efuse);
+ } else if (pkg_type < ARRAY_SIZE(ddr_pkg_types)) {
+ ddr = ddr_pkg_types[pkg_type];
+ }
+
+ debug("DDR: pkg_type %x, pkg %x, capacity %x, vendor %x\n",
+ pkg_type, ddr.pkg, ddr.capacity, ddr.vendor);
+
+ /* CV1800B/CV1801B on QFN68, CV1800C/CV1801C on QFN88 */
+ switch (ddr.capacity) {
+ case DDR_CAPACITY_512M:
+ chip_id = ddr.pkg == PKG_QFN88 ? 0x1800c : 0x1800b;
+ break;
+ case DDR_CAPACITY_1G:
+ chip_id = ddr.pkg == PKG_QFN88 ? 0x1801c : 0x1801b;
+ break;
+ default:
+ /* No known CV180x chip id for the other capacities */
+ break;
+ }
+ writel(chip_id, REG_GP_REG3);
+
+ if (!ddr_vendor_is_ddr2(ddr.vendor)) {
+ pr_err("DDR: unsupported DDR, pkg_type %x vendor %x\n", pkg_type, ddr.vendor);
+ return -ENODEV;
+ }
+
+ *vendor = ddr.vendor;
+
+ return 0;
+}
diff --git a/drivers/ram/sophgo/ddr_sys.c b/drivers/ram/sophgo/ddr_sys.c
new file mode 100644
index 00000000000..31166fa64d7
--- /dev/null
+++ b/drivers/ram/sophgo/ddr_sys.c
@@ -0,0 +1,635 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Sophgo CV180x DDR PHY training
+ *
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ *
+ * Based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * plat/cv180x/ddr/ddr_sys.c.
+ */
+
+#include "ddr_sys.h"
+
+/* DDR PLL synthesizer for DDR_DATA_RATE, SSC itself stays disabled */
+#define DDR_PLL_TAR_FREQ (DDR_DATA_RATE >> 4)
+#define DDR_PLL_SSC_SET (752ULL * 67108864 / DDR_PLL_TAR_FREQ)
+#define DDR_PLL_SSC_SPAN (DDR_PLL_TAR_FREQ * 250 / 100)
+#define DDR_PLL_SSC_STEP (DDR_PLL_SSC_SET * 15 / (DDR_PLL_SSC_SPAN * 1000))
+
+/* BIST controller */
+#define BIST_CTRL 0x00
+#define BIST_ADDR_START 0x10
+#define BIST_ADDR_END 0x14
+#define BIST_ADDR_STEP 0x18
+#define BIST_SSO_PERIOD 0x24
+#define BIST_CMD(n) (0x40 + (n) * 4)
+#define BIST_NUM_CMDS 6
+#define BIST_STATUS 0x80
+#define BIST_STATUS_DONE BIT(2)
+#define BIST_STATUS_FAIL BIT(3)
+
+#define BIST_OP_WRITE 1
+#define BIST_OP_READ 2
+#define BIST_OP_GOTO 3
+#define BIST_PAT_0F 3
+#define BIST_PAT_PRBS 5
+#define BIST_PAT_SRAM 6
+#define BIST_PAT_SSO_8X1_DM 7
+
+/* op, SRAM start/stop, pattern and repeat count, no DQ/DM inversion or rotation */
+#define BIST_CMD_VAL(op, start, stop, pat, repeat) \
+ (((op) << 30) | ((start) << 21) | ((stop) << 12) | ((pat) << 9) | (repeat))
+#define BIST_CMD_GOTO(loop) ((BIST_OP_GOTO << 30) | (loop))
+
+#define BIST_SSO_FMIN 5
+#define BIST_SSO_FMAX 15
+#define BIST_SSO_SRAM_STOP \
+ (9 * (BIST_SSO_FMIN + BIST_SSO_FMAX) * \
+ (BIST_SSO_FMAX - BIST_SSO_FMIN + 1) / 8 + \
+ (BIST_SSO_FMAX - BIST_SSO_FMIN + 1))
+
+/* DDR controller */
+#define DDRC_PWRCTL 0x30
+#define DDRC_PWRCTL_SELFREF_SW BIT(5)
+#define DDRC_PWRCTL_DFI_DRAM_CLK_DIS BIT(3)
+#define DDRC_PWRCTL_POWERDOWN_EN BIT(1)
+#define DDRC_PWRCTL_SELFREF_EN BIT(0)
+#define DDRC_PWRCTL_TRAINING_MASK (DDRC_PWRCTL_SELFREF_SW | \
+ DDRC_PWRCTL_DFI_DRAM_CLK_DIS | \
+ DDRC_PWRCTL_POWERDOWN_EN | \
+ DDRC_PWRCTL_SELFREF_EN)
+#define DDRC_PHY_STOP_CLK 0x30
+#define DDRC_EN_PHYD_STOP_CLK BIT(9)
+#define DDRC_PHYD_STOP_CLK_SEL BIT(10)
+#define DDRC_EN_PHYA_STOP_CLK BIT(11)
+#define DDRC_PHYA_STOP_CLK_SEL BIT(12)
+#define DDRC_PCTRL(n) (0x490 + 0xb0 * (n))
+/* Only ports 0 to 2 are configured by ddrc_init() */
+#define DDRC_NUM_PORTS 3
+#define DDRC_PSTAT 0x3fc
+
+/* PHY training status */
+#define PHYD_TRAIN_DONE 0x3444
+#define PHYD_RDGLVL_DONE BIT(1)
+#define PHYD_RDLVL_DONE BIT(2)
+#define PHYD_WDQLVL_DONE BIT(3)
+
+/*
+ * All the CV180x SiP DDR2 parts are 512 Mbit. As in the vendor code,
+ * dram_cap_in_mbyte holds log2 of the size in MiB: 6 means 64 MiB.
+ */
+#define DDR2_CAP_IN_MBYTE 6
+
+static void cvx16_bist_setup(const u32 *cmd, int count, u32 addr_step)
+{
+ int i;
+
+ for (i = 0; i < BIST_NUM_CMDS; i++)
+ writel(i < count ? cmd[i] : 0, DDR_BIST_BASE + BIST_CMD(i));
+
+ writel(0x00000000, DDR_BIST_BASE + BIST_ADDR_START);
+ writel(0x000fffff, DDR_BIST_BASE + BIST_ADDR_END);
+ writel(addr_step, DDR_BIST_BASE + BIST_ADDR_STEP);
+}
+
+void cvx16_bist_wr_prbs_init(void)
+{
+ static const u32 cmd[] = {
+ BIST_CMD_VAL(BIST_OP_WRITE, 0, 511, BIST_PAT_PRBS, 0),
+ BIST_CMD_VAL(BIST_OP_READ, 0, 511, BIST_PAT_PRBS, 0),
+ };
+
+ /* BIST clock enable */
+ writel(0x00060006, DDR_BIST_BASE + BIST_CTRL);
+ cvx16_bist_setup(cmd, ARRAY_SIZE(cmd), 4);
+}
+
+void cvx16_bist_wr_sram_init(void)
+{
+ static const u32 cmd[] = {
+ BIST_CMD_VAL(BIST_OP_WRITE, 0, BIST_SSO_SRAM_STOP, BIST_PAT_SRAM, 15),
+ BIST_CMD_VAL(BIST_OP_READ, 0, BIST_SSO_SRAM_STOP, BIST_PAT_SRAM, 15),
+ 0,
+ BIST_CMD_GOTO(1),
+ };
+
+ /* BIST clock enable, axi_len 8 */
+ writel(0x000c000c, DDR_BIST_BASE + BIST_CTRL);
+ writel((BIST_SSO_FMAX << 8) | BIST_SSO_FMIN, DDR_BIST_BASE + BIST_SSO_PERIOD);
+ /* 2 KiB AXI address step */
+ cvx16_bist_setup(cmd, ARRAY_SIZE(cmd), 2048 / 4 / 16);
+}
+
+static void cvx16_bist_rdglvl_init(void)
+{
+ static const u32 cmd[] = {
+ BIST_CMD_VAL(BIST_OP_READ, 0, 3, BIST_PAT_PRBS, 0),
+ };
+
+ writel(0x00060006, DDR_BIST_BASE + BIST_CTRL);
+ cvx16_bist_setup(cmd, ARRAY_SIZE(cmd), 4);
+}
+
+/* Continuous PRBS and SRAM write/read */
+static void cvx16_bist_rdlvl_init(void)
+{
+ static const u32 cmd[] = {
+ BIST_CMD_VAL(BIST_OP_WRITE, 0, 511, BIST_PAT_PRBS, 2),
+ BIST_CMD_VAL(BIST_OP_READ, 0, 511, BIST_PAT_PRBS, 2),
+ BIST_CMD_VAL(BIST_OP_WRITE, 0, BIST_SSO_SRAM_STOP, BIST_PAT_SRAM, 2),
+ BIST_CMD_VAL(BIST_OP_READ, 0, BIST_SSO_SRAM_STOP, BIST_PAT_SRAM, 2),
+ BIST_CMD_GOTO(1),
+ };
+
+ writel(0x00060006, DDR_BIST_BASE + BIST_CTRL);
+ writel((BIST_SSO_FMAX << 8) | BIST_SSO_FMIN, DDR_BIST_BASE + BIST_SSO_PERIOD);
+ cvx16_bist_setup(cmd, ARRAY_SIZE(cmd), 4);
+}
+
+/* PRBS and SRAM write/read */
+static void cvx16_bist_wdqlvl_init(void)
+{
+ static const u32 cmd[] = {
+ BIST_CMD_VAL(BIST_OP_WRITE, 0, 511, BIST_PAT_PRBS, 0),
+ BIST_CMD_VAL(BIST_OP_READ, 0, 511, BIST_PAT_PRBS, 0),
+ BIST_CMD_VAL(BIST_OP_WRITE, 0, BIST_SSO_SRAM_STOP, BIST_PAT_SRAM, 0),
+ BIST_CMD_VAL(BIST_OP_READ, 0, BIST_SSO_SRAM_STOP, BIST_PAT_SRAM, 0),
+ };
+
+ writel(0x00060006, DDR_BIST_BASE + BIST_CTRL);
+ writel((BIST_SSO_FMAX << 8) | BIST_SSO_FMIN, DDR_BIST_BASE + BIST_SSO_PERIOD);
+ cvx16_bist_setup(cmd, ARRAY_SIZE(cmd), 4);
+}
+
+static void cvx16_bist_wdmlvl_init(void)
+{
+ static const u32 cmd[] = {
+ BIST_CMD_VAL(BIST_OP_WRITE, 0, BIST_SSO_SRAM_STOP, BIST_PAT_0F, 0),
+ BIST_CMD_VAL(BIST_OP_WRITE, 0, BIST_SSO_SRAM_STOP, BIST_PAT_SSO_8X1_DM, 0),
+ BIST_CMD_VAL(BIST_OP_READ, 0, BIST_SSO_SRAM_STOP, BIST_PAT_SSO_8X1_DM, 0),
+ };
+
+ writel(0x00060006, DDR_BIST_BASE + BIST_CTRL);
+ writel((BIST_SSO_FMAX << 8) | BIST_SSO_FMIN, DDR_BIST_BASE + BIST_SSO_PERIOD);
+ cvx16_bist_setup(cmd, ARRAY_SIZE(cmd), 4);
+}
+
+/* Run the programmed BIST, returns 0 if it passed */
+int cvx16_bist_start_check(void)
+{
+ u32 status;
+ int ret;
+
+ /* BIST enable */
+ writel(0x00030003, DDR_BIST_BASE + BIST_CTRL);
+
+ ret = ddr_readl_poll(DDR_BIST_BASE + BIST_STATUS, status,
+ status & BIST_STATUS_DONE);
+ if (!ret && (status & BIST_STATUS_FAIL))
+ ret = -EIO;
+
+ /* BIST disable */
+ writel(0x00050000, DDR_BIST_BASE + BIST_CTRL);
+
+ return ret;
+}
+
+static void cvx16_rdvld_train(void)
+{
+ u32 byte0_vld, rdvld_offset;
+ int i;
+
+ cvx16_bist_wr_prbs_init();
+
+ byte0_vld = readl(PHYD_BASE + 0xb14);
+ rdvld_offset = FIELD_GET(GENMASK(3, 0), readl(PHYD_BASE + 0x94));
+
+ /*
+ * Lower the read valid window until the BIST fails, then step back.
+ * Byte 1 is written with the byte 0 value, as the vendor code does.
+ */
+ for (i = 9; i > 1; i--) {
+ byte0_vld = u32_replace_bits(byte0_vld, i, GENMASK(20, 16));
+ writel(byte0_vld, PHYD_BASE + 0xb14);
+ writel(byte0_vld, PHYD_BASE + 0xb44);
+ if (cvx16_bist_start_check()) {
+ i += 1 + rdvld_offset;
+ byte0_vld = u32_replace_bits(byte0_vld, i, GENMASK(20, 16));
+ writel(byte0_vld, PHYD_BASE + 0xb14);
+ writel(byte0_vld, PHYD_BASE + 0xb44);
+ break;
+ }
+ }
+}
+
+static void cvx16_ddr_pll_ssc_set(void)
+{
+ u32 rddata;
+
+ writel(DDR_PLL_SSC_SET, TOP_BASE + 0x2954);
+ writel(FIELD_GET(GENMASK(15, 0), DDR_PLL_SSC_SPAN), TOP_BASE + 0x2958);
+ writel(FIELD_GET(GENMASK(23, 0), DDR_PLL_SSC_STEP), TOP_BASE + 0x295c);
+
+ rddata = readl(TOP_BASE + 0x2950);
+ /* Toggle SSC_SW_UP, disable SSC and bypass, set EXTPULSE */
+ rddata ^= BIT(0);
+ rddata &= ~(BIT(1) | GENMASK(3, 2) | BIT(4) | BIT(6));
+ rddata |= BIT(5);
+ writel(rddata, TOP_BASE + 0x2950);
+}
+
+static void cvx16_clk_normal(void)
+{
+ /* DDRPLL_SEL_LOW_SPEED = 0, DDRPLL_MAS_DIV_OUT_SEL = 0 */
+ clrbits_le32(PHYD_APB_BASE + 0xc, BIT(13) | BIT(14));
+ cvx16_ddr_pll_ssc_set();
+}
+
+static void cvx16_clk_div2(void)
+{
+ /* DDRPLL_MAS_DIV_OUT_SEL = 1 */
+ setbits_le32(PHYD_APB_BASE + 0xc, BIT(14));
+}
+
+static void cvx16_clk_div40(void)
+{
+ /* DDRPLL_SEL_LOW_SPEED = 1 */
+ setbits_le32(PHYD_APB_BASE + 0xc, BIT(13));
+}
+
+static void cvx16_chg_pll_freq(void)
+{
+ u32 rddata, cur_speed, next_speed;
+
+ /* Assert RESETZ_DIV, RESETZ_DQS and DDRPLL_MAS_RSTZ_DIV */
+ writel(0, PHYD_APB_BASE + 0x4);
+ writel(0, PHYD_APB_BASE + 0x8);
+ clrbits_le32(PHYD_APB_BASE + 0xc, BIT(7));
+
+ /* [0] EN_PLL_SPEED_CHG, [5:4] CUR_PLL_SPEED, [9:8] NEXT_PLL_SPEED */
+ rddata = readl(PHYD_APB_BASE + 0x4c);
+ cur_speed = FIELD_GET(GENMASK(5, 4), rddata);
+ next_speed = FIELD_GET(GENMASK(9, 8), rddata);
+
+ if ((rddata & BIT(0)) && next_speed != 3) {
+ rddata = u32_replace_bits(rddata, next_speed, GENMASK(5, 4));
+ rddata = u32_replace_bits(rddata, cur_speed, GENMASK(9, 8));
+ writel(rddata, PHYD_APB_BASE + 0x4c);
+
+ if (next_speed == 0)
+ cvx16_clk_div40();
+ else if (next_speed == 1)
+ cvx16_clk_div2();
+ else
+ cvx16_clk_normal();
+ }
+
+ /* De-assert RESETZ_DIV, DDRPLL_MAS_RSTZ_DIV and RESETZ_DQS */
+ writel(1, PHYD_APB_BASE + 0x4);
+ setbits_le32(PHYD_APB_BASE + 0xc, BIT(7));
+ writel(1, PHYD_APB_BASE + 0x8);
+
+ /* DDRPLL lock */
+ ddr_readl_poll(PHYD_APB_BASE + 0x10, rddata, rddata & BIT(15));
+}
+
+static void cvx16_dll_cal(void)
+{
+ u32 rddata;
+
+ /* param_phyd_dll_rx_start_cal [1], param_phyd_dll_tx_start_cal [17] */
+ clrbits_le32(PHYD_BASE + 0x40, BIT(1) | BIT(17));
+
+ /* Only calibrate at high speed */
+ if (FIELD_GET(GENMASK(5, 4), readl(PHYD_APB_BASE + 0x4c))) {
+ setbits_le32(PHYD_BASE + 0x40, BIT(1) | BIT(17));
+ ddr_readl_poll(PHYD_BASE + 0x3014, rddata, rddata & BIT(16));
+ }
+}
+
+static void cvx16_set_dfi_init_complete(void)
+{
+ udelay(20);
+ writel(0x00000010, PHYD_BASE + 0x120);
+ /* param_phyd_clkctrl_init_complete */
+ writel(0x00000001, PHYD_BASE + 0x118);
+}
+
+void cvx16_ddr_phy_power_on_seq1(void)
+{
+ /* TX_CA_PD_CKE0 and TX_CA_PD_RESETZ, then all CA PD = 0 */
+ clrbits_le32(PHYD_APB_BASE + 0x40, BIT(24) | BIT(30));
+ writel(0, PHYD_APB_BASE + 0x40);
+
+ /* TX_SEL_GPIO = 1, DQ PD = 0, TX_SEL_GPIO = 0 */
+ setbits_le32(PHYD_APB_BASE + 0x1c, BIT(7));
+ writel(0, PHYD_APB_BASE);
+ clrbits_le32(PHYD_APB_BASE + 0x1c, BIT(7));
+}
+
+static void cvx16_ddr_phy_power_on_seq2(void)
+{
+ cvx16_chg_pll_freq();
+
+ /* param_phyd_sel_cke_oenz, param_phyd_tx_ca_*_oenz */
+ clrbits_le32(PHYD_BASE + 0x154, BIT(0));
+ writel(0, PHYD_BASE + 0x130);
+
+ cvx16_dll_cal();
+
+ /* CA PD = 0, BYTE PD = 0 */
+ writel(0x80000000, PHYD_APB_BASE + 0x40);
+ writel(0, PHYD_APB_BASE);
+}
+
+void cvx16_ddr_phy_power_on_seq3(void)
+{
+ /* param_phyd_sel_cke_oenz, param_phyd_tx_ca_*_oenz */
+ clrbits_le32(PHYD_BASE + 0x154, BIT(0));
+ writel(0, PHYD_BASE + 0x130);
+
+ /* Gate the extended OENZ dline clock of both bytes to save power */
+ setbits_le32(PHYD_BASE + 0x204, BIT(18));
+ setbits_le32(PHYD_BASE + 0x224, BIT(18));
+}
+
+/* Handle the PHY dfi_init_start request */
+void cvx16_dfi_init_start_isr(void)
+{
+ /* param_phyd_clkctrl_init_complete */
+ writel(0, PHYD_BASE + 0x118);
+ cvx16_ddr_phy_power_on_seq2();
+ cvx16_set_dfi_init_complete();
+}
+
+void cvx16_wait_for_dfi_init_complete(void)
+{
+ u32 rddata;
+
+ /* DFISTAT.dfi_init_complete */
+ ddr_readl_poll(DDRC_BASE + 0x1bc, rddata, rddata & BIT(0));
+ writel(0, DDRC_BASE + 0x320);
+ clrsetbits_le32(DDRC_BASE + 0x1b0, GENMASK(5, 0), 5);
+ writel(1, DDRC_BASE + 0x320);
+}
+
+void cvx16_polling_dfi_init_start(void)
+{
+ u32 rddata;
+
+ ddr_readl_poll(PHYD_BASE + 0x3028, rddata, rddata & BIT(8));
+}
+
+void cvx16_polling_synp_normal_mode(void)
+{
+ u32 rddata;
+
+ /* STAT.operating_mode */
+ ddr_readl_poll(DDRC_BASE + 0x4, rddata, FIELD_GET(GENMASK(2, 0), rddata) == 1);
+}
+
+static void cvx16_dfi_ca_park_prbs(bool enable)
+{
+ u32 rddata;
+
+ /* param_phyd_sw_dfi_phyupd_req */
+ writel(0x00000001, PHYD_BASE + 0x174);
+ /* dfi_phyupd_req and dfi_phyupd_ack */
+ ddr_readl_poll(PHYD_BASE + 0x3030, rddata,
+ FIELD_GET(GENMASK(9, 8), rddata) == 3);
+
+ writel(enable ? 0x1b : 0, DDR_TOP_BASE);
+ writel(0x01, DDR_TOP_BASE + 0x4);
+ writel(0x1ffffcb, DDR_TOP_BASE + 0x8);
+ writel(0x3fffffff, DDR_TOP_BASE + 0xc);
+
+ /* param_phyd_sw_dfi_phyupd_req_clr */
+ writel(0x00000010, PHYD_BASE + 0x174);
+}
+
+static void cvx16_clk_gating_disable(void)
+{
+ /* TOP_REG_CG_EN_* */
+ writel(0x000012f5, PHYD_APB_BASE + 0x44);
+ /* PHYD_SHIFT_GATING_EN */
+ writel(0x00000000, PHYD_BASE + 0xf4);
+ clrbits_le32(DDRC_BASE + DDRC_PHY_STOP_CLK,
+ DDRC_EN_PHYD_STOP_CLK | DDRC_EN_PHYA_STOP_CLK);
+ /* DFI read/write clock gating */
+ clrbits_le32(DDRC_BASE + 0x148, BIT(23) | BIT(31));
+}
+
+void cvx16_clk_gating_enable(void)
+{
+ /* TOP_REG_CG_EN_*, with TOP_REG_CG_EN_PHYA_GATED from the controller */
+ writel(0x00006c81, PHYD_APB_BASE + 0x44);
+ clrsetbits_le32(DDRC_BASE + 0x190, GENMASK(28, 24), FIELD_PREP(GENMASK(28, 24), 6));
+ /* PHYD_SHIFT_GATING_EN */
+ writel(0x00030033, PHYD_BASE + 0xf4);
+ /* Enable PHYD and PHYA stop clock, with the new stop clock selection */
+ clrsetbits_le32(DDRC_BASE + DDRC_PHY_STOP_CLK,
+ DDRC_PHYD_STOP_CLK_SEL | DDRC_PHYA_STOP_CLK_SEL,
+ DDRC_EN_PHYD_STOP_CLK | DDRC_EN_PHYA_STOP_CLK);
+ /* DFI read/write clock gating */
+ setbits_le32(DDRC_BASE + 0x148, BIT(23) | BIT(31));
+}
+
+/*
+ * Quiesce the controller before a training step: disable all AXI ports but
+ * port 0, low power modes and clock gating. Returns the PWRCTL bits to restore.
+ */
+static u32 cvx16_training_enter(void)
+{
+ u32 rddata, pwrctl;
+ int i;
+
+ for (i = 1; i < DDRC_NUM_PORTS; i++)
+ writel(0, DDRC_BASE + DDRC_PCTRL(i));
+
+ /* PSTAT.rd_port_busy_n and PSTAT.wr_port_busy_n */
+ ddr_readl_poll(DDRC_BASE + DDRC_PSTAT, rddata, rddata == 0);
+
+ /* PWRCTL.deeppowerdown_en must stay 0 for DDR2 */
+ pwrctl = readl(DDRC_BASE + DDRC_PWRCTL);
+ writel(pwrctl & ~DDRC_PWRCTL_TRAINING_MASK, DDRC_BASE + DDRC_PWRCTL);
+ cvx16_clk_gating_disable();
+
+ return pwrctl & DDRC_PWRCTL_TRAINING_MASK;
+}
+
+static void cvx16_training_exit(u32 pwrctl)
+{
+ int i;
+
+ clrsetbits_le32(DDRC_BASE + DDRC_PWRCTL, DDRC_PWRCTL_TRAINING_MASK, pwrctl);
+
+ for (i = 1; i < DDRC_NUM_PORTS; i++)
+ writel(1, DDRC_BASE + DDRC_PCTRL(i));
+
+ cvx16_clk_gating_enable();
+}
+
+/* Needs ctrl_init_low_patch() first, as all the training steps */
+void cvx16_rdglvl_req(void)
+{
+ u32 rddata, pwrctl;
+
+ pwrctl = cvx16_training_enter();
+
+ cvx16_bist_rdglvl_init();
+ /* param_phyd_dfi_rdglvl_req */
+ setbits_le32(PHYD_BASE + 0x184, BIT(0));
+
+ ddr_readl_poll(PHYD_BASE + PHYD_TRAIN_DONE, rddata, rddata & PHYD_RDGLVL_DONE);
+ /* BIST clock disable */
+ writel(0x00040000, DDR_BIST_BASE + BIST_CTRL);
+
+ cvx16_training_exit(pwrctl);
+}
+
+void cvx16_rdlvl_req(void)
+{
+ u32 rddata, pwrctl;
+ bool vref_training_en;
+
+ pwrctl = cvx16_training_enter();
+ cvx16_dfi_ca_park_prbs(true);
+
+ /* param_phyd_pirdlvl_deskew_start = 0x20, param_phyd_pirdlvl_deskew_end = 0x1f */
+ rddata = readl(PHYD_BASE + 0x80);
+ rddata = u32_replace_bits(rddata, 0x20, GENMASK(22, 16));
+ rddata = u32_replace_bits(rddata, 0x1f, GENMASK(30, 24));
+ writel(rddata, PHYD_BASE + 0x80);
+
+ /*
+ * Save param_phyd_pirdlvl_vref_training_en [2], then clear it together
+ * with param_phyd_pirdlvl_rx_init_deskew_en [1] and
+ * param_phyd_pirdlvl_rdvld_training_en [3]
+ */
+ rddata = readl(PHYD_BASE + 0x8c);
+ vref_training_en = rddata & BIT(2);
+ writel(rddata & ~GENMASK(3, 1), PHYD_BASE + 0x8c);
+
+ cvx16_bist_rdlvl_init();
+ /* param_phyd_dfi_rdlvl_req */
+ setbits_le32(PHYD_BASE + 0x188, BIT(0));
+ ddr_readl_poll(PHYD_BASE + PHYD_TRAIN_DONE, rddata, rddata & PHYD_RDLVL_DONE);
+
+ if (vref_training_en) {
+ /*
+ * Final training keeping the RX trigger level, still without
+ * vref training, then restore param_phyd_pirdlvl_vref_training_en
+ */
+ setbits_le32(PHYD_BASE + 0x188, BIT(0));
+ ddr_readl_poll(PHYD_BASE + PHYD_TRAIN_DONE, rddata, rddata & PHYD_RDLVL_DONE);
+ setbits_le32(PHYD_BASE + 0x8c, BIT(2));
+ }
+
+ cvx16_rdvld_train();
+
+ /* BIST clock disable */
+ writel(0x00040000, DDR_BIST_BASE + BIST_CTRL);
+ cvx16_dfi_ca_park_prbs(false);
+ cvx16_training_exit(pwrctl);
+}
+
+/**
+ * cvx16_wdqlvl_req() - Write DQ/DM leveling with BIST data
+ * @lvl_mode: WDQLVL_DM, WDQLVL_DQ or WDQLVL_DQ_DM
+ */
+void cvx16_wdqlvl_req(u32 lvl_mode)
+{
+ u32 rddata, pwrctl;
+
+ pwrctl = cvx16_training_enter();
+ cvx16_dfi_ca_park_prbs(true);
+
+ /* param_phyd_piwdqlvl_dq_mode [12], param_phyd_piwdqlvl_dm_mode [13] */
+ rddata = readl(PHYD_BASE + 0xbc);
+ rddata = u32_replace_bits(rddata, lvl_mode != WDQLVL_DM, BIT(12));
+ rddata = u32_replace_bits(rddata, lvl_mode != WDQLVL_DQ, BIT(13));
+ writel(rddata, PHYD_BASE + 0xbc);
+
+ if (lvl_mode == WDQLVL_DM) {
+ setbits_le32(DDRC_BASE + 0xc, BIT(7));
+ cvx16_bist_wdmlvl_init();
+ } else {
+ cvx16_bist_wdqlvl_init();
+ }
+
+ /*
+ * param_phyd_dfi_wdqlvl_req [0], param_phyd_dfi_wdqlvl_bist_data_en [4],
+ * param_phyd_dfi_wdqlvl_vref_train_en [10]
+ */
+ rddata = readl(PHYD_BASE + 0x18c);
+ rddata |= BIT(0) | BIT(4);
+ rddata = u32_replace_bits(rddata, lvl_mode != WDQLVL_DM, BIT(10));
+ writel(rddata, PHYD_BASE + 0x18c);
+
+ ddr_readl_poll(PHYD_BASE + PHYD_TRAIN_DONE, rddata, rddata & PHYD_WDQLVL_DONE);
+
+ clrbits_le32(DDRC_BASE + 0xc, BIT(7));
+ /* BIST clock disable */
+ writel(0x00040000, DDR_BIST_BASE + BIST_CTRL);
+ cvx16_dfi_ca_park_prbs(false);
+ cvx16_training_exit(pwrctl);
+}
+
+void cvx16_set_dfi_init_start(void)
+{
+ /* Trigger dfi_init_start from the controller, the PHY is ready */
+ writel(0, DDRC_BASE + 0x320);
+ setbits_le32(DDRC_BASE + 0x1b0, BIT(5));
+ writel(1, DDRC_BASE + 0x320);
+}
+
+/* Enable the voltage mode receiver of both bytes, DDR2 only */
+void cvx16_en_rec_vol_mode(void)
+{
+ /*
+ * f0_param_phya_reg_rx_byteN_en_lsmode [0],
+ * f0_param_phya_reg_byteN_en_rec_vol_mode [12]
+ */
+ writel(0x00001001, PHYD_BASE + 0x500);
+ writel(0x00001001, PHYD_BASE + 0x540);
+}
+
+void cvx16_pll_init(void)
+{
+ u32 rddata;
+
+ /* TX_VREF_PD */
+ writel(0x00000000, PHYD_APB_BASE + 0x28);
+ /* ZQ_240 option */
+ writel(0x00080001, PHYD_APB_BASE + 0x54);
+
+ cvx16_ddr_pll_ssc_set();
+
+ /*
+ * DDRPLL: EN_DLLCLK, EN_LCKDET, ICTRL = 1, SEL_4BIT, SEL_MODE = 1,
+ * everything else cleared
+ */
+ clrsetbits_le32(PHYD_APB_BASE + 0xc, GENMASK(15, 0), 0x030b);
+ /* TOP_REG_DDRPLL_TEST */
+ clrbits_le32(PHYD_APB_BASE + 0x10, GENMASK(7, 0));
+ /* TOP_REG_RESETZ_DIV, TOP_REG_DDRPLL_MAS_RSTZ_DIV */
+ writel(0x1, PHYD_APB_BASE + 0x4);
+ setbits_le32(PHYD_APB_BASE + 0xc, BIT(7));
+
+ ddr_readl_poll(PHYD_APB_BASE + 0x10, rddata, rddata & BIT(15));
+}
+
+/*
+ * The vendor code does not probe the size of the DDR2 parts, they are all
+ * 512 Mbit. Report it and store it for later stages, as the vendor does.
+ */
+int ctrl_init_detect_dram_size(u8 *dram_cap_in_mbyte)
+{
+ *dram_cap_in_mbyte = DDR2_CAP_IN_MBYTE;
+ writel(DDR2_CAP_IN_MBYTE, PHYD_BASE + 0x208);
+
+ /* BIST clock disable */
+ writel(0x00040000, DDR_BIST_BASE + BIST_CTRL);
+
+ return 0;
+}
diff --git a/drivers/ram/sophgo/ddr_sys.h b/drivers/ram/sophgo/ddr_sys.h
new file mode 100644
index 00000000000..71964542d5f
--- /dev/null
+++ b/drivers/ram/sophgo/ddr_sys.h
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Sophgo CV180x DDR controller and PHY
+ *
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ *
+ * Based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * plat/cv180x/ddr (DDR_CFG=ddr2_1333_x16).
+ */
+
+#ifndef __SOPHGO_DDR_SYS_H
+#define __SOPHGO_DDR_SYS_H
+
+#include <asm/io.h>
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/iopoll.h>
+#include <linux/kernel.h>
+#include <linux/printk.h>
+#include <linux/types.h>
+
+#define TOP_BASE ((void __iomem *)0x03000000)
+#define PHYD_BASE ((void __iomem *)0x08000000)
+#define DDRC_BASE (PHYD_BASE + 0x4000)
+#define PHYD_APB_BASE (PHYD_BASE + 0x6000)
+#define DDR_TOP_BASE (PHYD_BASE + 0xa000)
+#define DDR_BIST_BASE ((void __iomem *)0x08010000)
+
+/* MBps; only the DDR2-1333 x16 register set is ported */
+#define DDR_DATA_RATE 1333
+
+#define DDR_POLL_TIMEOUT_US 5000000
+
+/* The vendor code polls these forever; give up and report instead */
+#define ddr_readl_poll(addr, val, cond) \
+({ \
+ int __ret = readl_poll_timeout(addr, val, cond, DDR_POLL_TIMEOUT_US); \
+ \
+ if (__ret) \
+ pr_err("%s: DDR poll timeout\n", __func__); \
+ __ret; \
+})
+
+/* SiP DDR vendor codes, from conf_info/efuse */
+#define DDR_VENDOR_UNKNOWN 0x00
+#define DDR_VENDOR_NY_4G 0x01
+#define DDR_VENDOR_NY_2G 0x02
+#define DDR_VENDOR_ESMT_1G 0x03
+#define DDR_VENDOR_ESMT_512M_DDR2 0x04
+#define DDR_VENDOR_ETRON_1G 0x05
+#define DDR_VENDOR_ESMT_2G 0x06
+#define DDR_VENDOR_PM_2G 0x07
+#define DDR_VENDOR_PM_1G 0x08
+#define DDR_VENDOR_ETRON_512M_DDR2 0x09
+#define DDR_VENDOR_ESMT_N25_1G 0x0a
+#define DDR_VENDOR_NY_N20_1G 0x0b
+#define DDR_VENDOR_ESMT_N21_2G 0x0c
+#define DDR_VENDOR_ESMT_N19_4G 0x0d
+#define DDR_VENDOR_UNILC_N25_512M_DDR2 0x0e
+#define DDR_VENDOR_UNILC_N25_1G 0x0f
+#define DDR_VENDOR_UNILC_N21_2G 0x10
+
+/* offset from PHYD_BASE, value */
+struct ddr_reg {
+ u16 off;
+ u32 val;
+};
+
+/* cvx16_wdqlvl_req() lvl_mode */
+#define WDQLVL_DM 0
+#define WDQLVL_DQ 1
+#define WDQLVL_DQ_DM 2
+
+/* ddr_pkg_info.c */
+int read_ddr_pkg_info(u8 *vendor);
+
+/* cvx16_pinmux.c, ddr_patch_regs.c, phy_init.c, ddrc_init.c */
+int cvx16_pinmux(u8 vendor);
+void ddr_patch_set(void);
+void phy_init(void);
+void ddrc_init(u8 vendor);
+void ctrl_init_high_patch(void);
+void ctrl_init_low_patch(void);
+/* dram_cap_in_mbyte is log2 of the DRAM size in MiB */
+void ctrl_init_update_by_dram_size(u8 vendor, u8 dram_cap_in_mbyte);
+
+/* ddr_sys.c */
+void cvx16_bist_wr_prbs_init(void);
+void cvx16_bist_wr_sram_init(void);
+int cvx16_bist_start_check(void);
+void cvx16_pll_init(void);
+void cvx16_en_rec_vol_mode(void);
+void cvx16_set_dfi_init_start(void);
+void cvx16_ddr_phy_power_on_seq1(void);
+void cvx16_polling_dfi_init_start(void);
+void cvx16_dfi_init_start_isr(void);
+void cvx16_ddr_phy_power_on_seq3(void);
+void cvx16_wait_for_dfi_init_complete(void);
+void cvx16_polling_synp_normal_mode(void);
+void cvx16_rdglvl_req(void);
+void cvx16_rdlvl_req(void);
+void cvx16_wdqlvl_req(u32 lvl_mode);
+void cvx16_clk_gating_enable(void);
+int ctrl_init_detect_dram_size(u8 *dram_cap_in_mbyte);
+
+#endif /* __SOPHGO_DDR_SYS_H */
diff --git a/drivers/ram/sophgo/ddrc_init.c b/drivers/ram/sophgo/ddrc_init.c
new file mode 100644
index 00000000000..c34dd601edf
--- /dev/null
+++ b/drivers/ram/sophgo/ddrc_init.c
@@ -0,0 +1,476 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Sophgo CV180x DDR2-1333 x16 controller settings
+ *
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ *
+ * Based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * plat/cv180x/ddr/ddr_config/ddr2_1333_x16/ddrc_init.c.
+ *
+ */
+
+#include "ddr_sys.h"
+
+#define DDRC_ADDRMAP(n) (0x200 + (n) * 4)
+#define DDRC_NUM_ADDRMAP 12
+
+/* ADDRMAP0 to ADDRMAP11 */
+static void ddrc_set_addrmap(const u32 *map)
+{
+ int i;
+
+ for (i = 0; i < DDRC_NUM_ADDRMAP; i++)
+ writel(map[i], DDRC_BASE + DDRC_ADDRMAP(i));
+}
+
+/* R[17:13] B[2] R[12:0] B[1:0] C[9:0], from 0.5 Gbit to 4 Gbit */
+static const u32 ddrc_addrmap_etron[DDRC_NUM_ADDRMAP] = {
+ 0x00001f1f, 0x00141414, 0x00000000, 0x1f000000,
+ 0x00001f1f, 0x030f0303, 0x06060603, 0x00000606,
+ 0x00003f3f, 0x03030303, 0x03030303, 0x001f1f03,
+};
+
+static const u32 ddrc_addrmap_esmt[DDRC_NUM_ADDRMAP] = {
+ 0x00001f1f, 0x00140707, 0x00000000, 0x1f000000,
+ 0x00001f1f, 0x050f0505, 0x06060605, 0x00000606,
+ 0x00003f3f, 0x05050505, 0x05050505, 0x001f1f05,
+};
+
+void ddrc_init(u8 vendor)
+{
+ bool etron = vendor == DDR_VENDOR_ETRON_512M_DDR2;
+
+ /*
+ * PATCH0.use_blk_ext:0:2:=0x1
+ * PATCH0.dis_auto_ref_cnt_fix:2:1:=0x0
+ * PATCH0.dis_auto_ref_algn_to_8:3:1:=0x0
+ * PATCH0.starve_stall_at_dfi_ctrlupd:4:1:=0x1
+ * PATCH0.starve_stall_at_abr:5:1:=0x1
+ * PATCH0.dis_rdwr_switch_at_abr:6:1:=0x1
+ * PATCH0.dfi_wdata_same_to_axi:7:1:=0x0
+ * PATCH0.pagematch_limit_threshold:8:3=0x3
+ * PATCH0.qos_sel:12:2:=0x2
+ * PATCH0.burst_rdwr_xpi:16:4:=0x4
+ * PATCH0.always_critical_when_urgent_hpr:20:1:=0x1
+ * PATCH0.always_critical_when_urgent_lpr:21:1:=0x1
+ * PATCH0.always_critical_when_urgent_wr:22:1:=0x1
+ * PATCH0.disable_hif_rcmd_stall_path:24:1:=0x1
+ * PATCH0.disable_hif_wcmd_stall_path:25:1:=0x1
+ * PATCH0.derate_sys_en:29:1:=0x1
+ * PATCH0.ref_4x_sys_high_temp:30:1:=0x1
+ */
+ writel(0x63746371, DDRC_BASE + 0xc);
+ /*
+ * PATCH1.ref_adv_stop_threshold:0:7:=0x0
+ * PATCH1.ref_adv_dec_threshold:8:7:=0x0
+ * PATCH1.ref_adv_max:16:7:=0x0
+ * PATCH1.burst_rdwr_wr_xpi:24:4:=0x4
+ * PATCH1.use_blk_extend:28:2:=0x1
+ */
+ writel(0x14000000, DDRC_BASE + 0x44);
+ /*
+ * PATCH5.vpr_fix:0:1:=0x1
+ * PATCH5.vpw_fix:1:1:=0x1
+ */
+ writel(0x00000003, DDRC_BASE + 0x6c);
+ /*
+ * PATCH4.t_phyd_rden:16:6=0x1d (ETRON), 0x1c (others)
+ * PATCH4.phyd_rd_clk_stop:23:1=0x1
+ * PATCH4.t_phyd_wren:24:6=0x18 (ETRON), 0x17 (others)
+ * PATCH4.phyd_wr_clk_stop:31:1=0x1
+ */
+ writel(etron ? 0x989d0000 : 0x979c0000, DDRC_BASE + 0x148);
+ writel(0x81041400, DDRC_BASE);
+ writel(0x00000000, DDRC_BASE + 0x30);
+ writel(0x006a0001, DDRC_BASE + 0x34);
+ writel(0x00020000, DDRC_BASE + 0x38);
+ writel(0x00201070, DDRC_BASE + 0x50);
+ writel(0x00000000, DDRC_BASE + 0x60);
+ writel(etron ? 0x00510023 : 0x0051002c, DDRC_BASE + 0x64);
+ writel(0x00010043, DDRC_BASE + 0xd0);
+ writel(0x00000000, DDRC_BASE + 0xd4);
+ if (etron) {
+ writel(0x03030040, DDRC_BASE + 0xdc);
+ writel(0x00800000, DDRC_BASE + 0xe0);
+ writel(0x0b01160f, DDRC_BASE + 0x100);
+ writel(0x00020414, DDRC_BASE + 0x104);
+ writel(0x04050409, DDRC_BASE + 0x108);
+ writel(0x00001004, DDRC_BASE + 0x10c);
+ writel(0x05020405, DDRC_BASE + 0x110);
+ writel(0x01010202, DDRC_BASE + 0x114);
+ writel(0x00000503, DDRC_BASE + 0x120);
+ } else {
+ writel(0x03730040, DDRC_BASE + 0xdc);
+ writel(0x00800000, DDRC_BASE + 0xe0);
+ writel(0x0a011610, DDRC_BASE + 0x100);
+ writel(0x00030414, DDRC_BASE + 0x104);
+ writel(0x03040408, DDRC_BASE + 0x108);
+ writel(0x00003004, DDRC_BASE + 0x10c);
+ writel(0x05020406, DDRC_BASE + 0x110);
+ writel(0x01010303, DDRC_BASE + 0x114);
+ writel(0x00000503, DDRC_BASE + 0x120);
+ }
+ /* phyd related */
+ /*
+ * DFITMG0.dfi_t_ctrl_delay:24:5:=0x4
+ * DFITMG0.dfi_rddata_use_dfi_phy_clk:23:1:=0x1
+ * DFITMG0.dfi_t_rddata_en:16:7:=0x7 (ETRON), 0x5 (others)
+ * DFITMG0.dfi_wrdata_use_dfi_phy_clk:15:1:=0x1
+ * DFITMG0.dfi_tphy_wrdata:8:6:=0x3
+ * DFITMG0.dfi_tphy_wrlat:0:6:=0x4 (ETRON), 0x2 (others)
+ */
+ writel(etron ? 0x04878304 : 0x04858302, DDRC_BASE + 0x190);
+ /*
+ * DFITMG1.dfi_t_cmd_lat:28:4:=0x0
+ * DFITMG1.dfi_t_parin_lat:24:2:=0x0
+ * DFITMG1.dfi_t_wrdata_delay:16:5:=0x7
+ * DFITMG1.dfi_t_dram_clk_disable:8:5:=0x1
+ * DFITMG1.dfi_t_dram_clk_enable:0:5:=0x2
+ */
+ writel(0x00070102, DDRC_BASE + 0x194);
+ /*
+ * DFILPCFG0.dfi_tlp_resp:24:5:=0x7
+ * DFILPCFG0.dfi_lp_wakeup_dpd:20:4:=0xc
+ * DFILPCFG0.dfi_lp_en_dpd:16:1:=0x1
+ * DFILPCFG0.dfi_lp_wakeup_sr:12:4:=0x3
+ * DFILPCFG0.dfi_lp_en_sr:8:1:=0x1
+ * DFILPCFG0.dfi_lp_wakeup_pd:4:4:=0x2
+ * DFILPCFG0.dfi_lp_en_pd:0:1:=0x1
+ */
+ writel(0x07c13121, DDRC_BASE + 0x198);
+ /*
+ * DFILPCFG1.dfi_lp_wakeup_mpsm:4:4:=0x2
+ * DFILPCFG1.dfi_lp_en_mpsm:0:1:=0x1
+ */
+ writel(0x00000021, DDRC_BASE + 0x19c);
+ writel(0xc0400018, DDRC_BASE + 0x1a0);
+ writel(0x00fe00ff, DDRC_BASE + 0x1a4);
+ writel(0x80000000, DDRC_BASE + 0x1a8);
+ writel(0x000002c1, DDRC_BASE + 0x1b0);
+ writel(0x00000001, DDRC_BASE + 0x1c0);
+ writel(0x00000001, DDRC_BASE + 0x1c4);
+ /* address map, auto gen. */
+ ddrc_set_addrmap(etron ? ddrc_addrmap_etron : ddrc_addrmap_esmt);
+ writel(etron ? 0x07030710 : 0x07010708, DDRC_BASE + 0x240);
+ writel(0x00000000, DDRC_BASE + 0x244);
+ /*
+ * SCHED.opt_vprw_sch:31:1:=0x0
+ * SCHED.rdwr_idle_gap:24:7:=0x0
+ * SCHED.go2critical_hysteresis:16:8:=0x0
+ * SCHED.lpddr4_opt_act_timing:15:1:=0x0
+ * SCHED.lpr_num_entries:8:7:=0x3f
+ * SCHED.autopre_rmw:7:1:=0x1
+ * SCHED.dis_opt_ntt_by_pre:6:1:=0x0
+ * SCHED.dis_opt_ntt_by_act:5:1:=0x0
+ * SCHED.opt_wrcam_fill_level:4:1:=0x0
+ * SCHED.rdwr_switch_policy_sel:3:1:=0x0
+ * SCHED.pageclose:2:1:=0x1
+ * SCHED.prefer_write:1:1:=0x0
+ * SCHED.dis_opt_wrecc_collision_flush:0:1:=0x1
+ */
+ writel(0x00003f85, DDRC_BASE + 0x250);
+ /*
+ * SCHED1.page_hit_limit_rd:28:3:=0x0
+ * SCHED1.page_hit_limit_wr:24:3:=0x0
+ * SCHED1.visible_window_limit_rd:20:3:=0x0
+ * SCHED1.visible_window_limit_wr:16:3:=0x0
+ * SCHED1.delay_switch_write:12:4:=0x0
+ * SCHED1.pageclose_timer:0:8:=0x20
+ */
+ writel(0x00000020, DDRC_BASE + 0x254);
+ /*
+ * PERFHPR1.hpr_xact_run_length:24:8:=0x10
+ * PERFHPR1.hpr_max_starve:0:16:=0xa8
+ */
+ writel(0x100000a8, DDRC_BASE + 0x25c);
+ /*
+ * PERFLPR1.lpr_xact_run_length:24:8:=0x10
+ * PERFLPR1.lpr_max_starve:0:16:=0xa8
+ */
+ writel(0x100000a8, DDRC_BASE + 0x264);
+ /*
+ * PERFWR1.w_xact_run_length:24:8:=0x10
+ * PERFWR1.w_max_starve:0:16:=0xa8
+ */
+ writel(0x100000a8, DDRC_BASE + 0x26c);
+ /*
+ * DBG0.dis_max_rank_wr_opt:7:1:=0x0
+ * DBG0.dis_max_rank_rd_opt:6:1:=0x0
+ * DBG0.dis_collision_page_opt:4:1:=0x0
+ * DBG0.dis_act_bypass:2:1:=0x0
+ * DBG0.dis_rd_bypass:1:1:=0x0
+ * DBG0.dis_wc:0:1:=0x0
+ */
+ writel(0x00000000, DDRC_BASE + 0x300);
+ /*
+ * DBG1.dis_hif:1:1:=0x0
+ * DBG1.dis_dq:0:1:=0x0
+ */
+ writel(0x00000000, DDRC_BASE + 0x304);
+ writel(0x00000000, DDRC_BASE + 0x30c);
+ /* SWCTL.sw_done:0:1:=0x1 */
+ writel(0x00000001, DDRC_BASE + 0x320);
+ /*
+ * POISONCFG.rd_poison_intr_clr:24:1:=0x0
+ * POISONCFG.rd_poison_intr_en:20:1:=0x0
+ * POISONCFG.rd_poison_slverr_en:16:1:=0x0
+ * POISONCFG.wr_poison_intr_clr:8:1:=0x0
+ * POISONCFG.wr_poison_intr_en:4:1:=0x0
+ * POISONCFG.wr_poison_slverr_en:0:1:=0x0
+ */
+ writel(0x00000000, DDRC_BASE + 0x36c);
+ /*
+ * PCCFG.dch_density_ratio:12:2:=0x0
+ * PCCFG.bl_exp_mode:8:1:=0x0
+ * PCCFG.pagematch_limit:4:1:=0x1
+ * PCCFG.go2critical_en:0:1:=0x1
+ */
+ writel(0x00000011, DDRC_BASE + 0x400);
+ /*
+ * PCFGR_0.rdwr_ordered_en:16:1:=0x0
+ * PCFGR_0.rd_port_pagematch_en:14:1:=0x1
+ * PCFGR_0.rd_port_urgent_en:13:1:=0x1
+ * PCFGR_0.rd_port_aging_en:12:1:=0x0
+ * PCFGR_0.read_reorder_bypass_en:11:1:=0x0
+ * PCFGR_0.rd_port_priority:0:10:=0x0
+ */
+ writel(0x00006000, DDRC_BASE + 0x404);
+ /*
+ * PCFGW_0.wr_port_pagematch_en:14:1:=0x1
+ * PCFGW_0.wr_port_urgent_en:13:1:=0x1
+ * PCFGW_0.wr_port_aging_en:12:1:=0x0
+ * PCFGW_0.wr_port_priority:0:10:=0x0
+ */
+ writel(0x00006000, DDRC_BASE + 0x408);
+ /* PCTRL_0.port_en:0:1:=0x1 */
+ writel(0x00000001, DDRC_BASE + 0x490);
+ /*
+ * PCFGQOS0_0.rqos_map_region2:24:8:=0x0
+ * PCFGQOS0_0.rqos_map_region1:20:4:=0x0
+ * PCFGQOS0_0.rqos_map_region0:16:4:=0x0
+ * PCFGQOS0_0.rqos_map_level2:8:8:=0x0
+ * PCFGQOS0_0.rqos_map_level1:0:8:=0x7
+ */
+ writel(0x00000007, DDRC_BASE + 0x494);
+ /*
+ * PCFGQOS1_0.rqos_map_timeoutr:16:16:=0x0
+ * PCFGQOS1_0.rqos_map_timeoutb:0:16:=0x6a
+ */
+ writel(0x0000006a, DDRC_BASE + 0x498);
+ /*
+ * PCFGWQOS0_0.wqos_map_region2:24:8:=0x0
+ * PCFGWQOS0_0.wqos_map_region1:20:4:=0x0
+ * PCFGWQOS0_0.wqos_map_region0:16:4:=0x0
+ * PCFGWQOS0_0.wqos_map_level2:8:8:=0xe
+ * PCFGWQOS0_0.wqos_map_level1:0:8:=0x7
+ */
+ writel(0x00000e07, DDRC_BASE + 0x49c);
+ /*
+ * PCFGWQOS1_0.wqos_map_timeout2:16:16:=0x1a8
+ * PCFGWQOS1_0.wqos_map_timeout1:0:16:=0x1a8
+ */
+ writel(0x01a801a8, DDRC_BASE + 0x4a0);
+ /*
+ * PCFGR_1.rdwr_ordered_en:16:1:=0x0
+ * PCFGR_1.rd_port_pagematch_en:14:1:=0x1
+ * PCFGR_1.rd_port_urgent_en:13:1:=0x1
+ * PCFGR_1.rd_port_aging_en:12:1:=0x0
+ * PCFGR_1.read_reorder_bypass_en:11:1:=0x0
+ * PCFGR_1.rd_port_priority:0:10:=0x0
+ */
+ writel(0x00006000, DDRC_BASE + 0x4b4);
+ /*
+ * PCFGW_1.wr_port_pagematch_en:14:1:=0x1
+ * PCFGW_1.wr_port_urgent_en:13:1:=0x1
+ * PCFGW_1.wr_port_aging_en:12:1:=0x0
+ * PCFGW_1.wr_port_priority:0:10:=0x0
+ */
+ writel(0x00006000, DDRC_BASE + 0x4b8);
+ /* PCTRL_1.port_en:0:1:=0x1 */
+ writel(0x00000001, DDRC_BASE + 0x540);
+ /*
+ * PCFGQOS0_1.rqos_map_region2:24:8:=0x0
+ * PCFGQOS0_1.rqos_map_region1:20:4:=0x0
+ * PCFGQOS0_1.rqos_map_region0:16:4:=0x0
+ * PCFGQOS0_1.rqos_map_level2:8:8:=0x0
+ * PCFGQOS0_1.rqos_map_level1:0:8:=0x7
+ */
+ writel(0x00000007, DDRC_BASE + 0x544);
+ /*
+ * PCFGQOS1_1.rqos_map_timeoutr:16:16:=0x0
+ * PCFGQOS1_1.rqos_map_timeoutb:0:16:=0x6a
+ */
+ writel(0x0000006a, DDRC_BASE + 0x548);
+ /*
+ * PCFGWQOS0_1.wqos_map_region2:24:8:=0x0
+ * PCFGWQOS0_1.wqos_map_region1:20:4:=0x0
+ * PCFGWQOS0_1.wqos_map_region0:16:4:=0x0
+ * PCFGWQOS0_1.wqos_map_level2:8:8:=0xe
+ * PCFGWQOS0_1.wqos_map_level1:0:8:=0x7
+ */
+ writel(0x00000e07, DDRC_BASE + 0x54c);
+ /*
+ * PCFGWQOS1_1.wqos_map_timeout2:16:16:=0x1a8
+ * PCFGWQOS1_1.wqos_map_timeout1:0:16:=0x1a8
+ */
+ writel(0x01a801a8, DDRC_BASE + 0x550);
+ /*
+ * PCFGR_2.rdwr_ordered_en:16:1:=0x0
+ * PCFGR_2.rd_port_pagematch_en:14:1:=0x1
+ * PCFGR_2.rd_port_urgent_en:13:1:=0x1
+ * PCFGR_2.rd_port_aging_en:12:1:=0x0
+ * PCFGR_2.read_reorder_bypass_en:11:1:=0x0
+ * PCFGR_2.rd_port_priority:0:10:=0x0
+ */
+ writel(0x00006000, DDRC_BASE + 0x564);
+ /*
+ * PCFGW_2.wr_port_pagematch_en:14:1:=0x1
+ * PCFGW_2.wr_port_urgent_en:13:1:=0x1
+ * PCFGW_2.wr_port_aging_en:12:1:=0x0
+ * PCFGW_2.wr_port_priority:0:10:=0x0
+ */
+ writel(0x00006000, DDRC_BASE + 0x568);
+ /* PCTRL_2.port_en:0:1:=0x1 */
+ writel(0x00000001, DDRC_BASE + 0x5f0);
+ /*
+ * PCFGQOS0_2.rqos_map_region2:24:8:=0x0
+ * PCFGQOS0_2.rqos_map_region1:20:4:=0x0
+ * PCFGQOS0_2.rqos_map_region0:16:4:=0x0
+ * PCFGQOS0_2.rqos_map_level2:8:8:=0x0
+ * PCFGQOS0_2.rqos_map_level1:0:8:=0x7
+ */
+ writel(0x00000007, DDRC_BASE + 0x5f4);
+ /*
+ * PCFGQOS1_2.rqos_map_timeoutr:16:16:=0x0
+ * PCFGQOS1_2.rqos_map_timeoutb:0:16:=0x6a
+ */
+ writel(0x0000006a, DDRC_BASE + 0x5f8);
+ /*
+ * PCFGWQOS0_2.wqos_map_region2:24:8:=0x0
+ * PCFGWQOS0_2.wqos_map_region1:20:4:=0x0
+ * PCFGWQOS0_2.wqos_map_region0:16:4:=0x0
+ * PCFGWQOS0_2.wqos_map_level2:8:8:=0xe
+ * PCFGWQOS0_2.wqos_map_level1:0:8:=0x7
+ */
+ writel(0x00000e07, DDRC_BASE + 0x5fc);
+ /*
+ * PCFGWQOS1_2.wqos_map_timeout2:16:16:=0x1a8
+ * PCFGWQOS1_2.wqos_map_timeout1:0:16:=0x1a8
+ */
+ writel(0x01a801a8, DDRC_BASE + 0x600);
+}
+
+void ctrl_init_high_patch(void)
+{
+ /* enable auto PD */
+ writel(0x00000002, DDRC_BASE + 0x30);
+ /* enable auto ctrl_upd */
+ writel(0x00400018, DDRC_BASE + 0x1a0);
+ /* enable clock gating */
+ writel(0x00000000, DDR_TOP_BASE + 0x14);
+ /* change xpi to multi DDR burst */
+ writel(0x63746371, DDRC_BASE + 0xc);
+ writel(0x08000000, DDRC_BASE + 0x44);
+}
+
+void ctrl_init_low_patch(void)
+{
+ /* disable auto PD/SR */
+ writel(0x00000000, DDRC_BASE + 0x30);
+ /* disable auto ctrl_upd */
+ writel(0xc0400018, DDRC_BASE + 0x1a0);
+ /* disable clock gating */
+ writel(0x00000fff, DDR_TOP_BASE + 0x14);
+ /* change xpi to single DDR burst */
+ writel(0x63746371, DDRC_BASE + 0xc);
+ writel(0x14000000, DDRC_BASE + 0x44);
+}
+
+/* RFSHTMG, DRAMTMG0 and DRAMTMG8 for 32 MiB (5) to 512 MiB (9) per device */
+struct ddrc_size_timing {
+ u32 rfshtmg;
+ u32 dramtmg0;
+ u32 dramtmg8;
+};
+
+#define DDRC_SIZE_MIN 5
+#define DDRC_SIZE_MAX 9
+
+static const struct ddrc_size_timing ddrc_size_timing_etron[] = {
+ { 0x00510019, 0x0b01160f, 0x00000502 },
+ { 0x00510023, 0x0b01160f, 0x00000503 },
+ { 0x0051002b, 0x0b0f160f, 0x00000503 },
+ { 0x00510041, 0x0b0f160f, 0x00000504 },
+ { 0x0051006e, 0x0b0f160f, 0x00000505 },
+};
+
+static const struct ddrc_size_timing ddrc_size_timing_esmt[] = {
+ { 0x00510019, 0x0b011610, 0x00000502 },
+ { 0x0051002c, 0x0a011610, 0x00000503 },
+ { 0x0051002b, 0x0b0f1610, 0x00000503 },
+ { 0x00510041, 0x0b0f1610, 0x00000504 },
+ { 0x0051006e, 0x0b0f1610, 0x00000505 },
+};
+
+static const u32 ddrc_addrmap_32m[DDRC_NUM_ADDRMAP] = {
+ 0x00001f1f, 0x003f0606, 0x00000000, 0x1f1f0000,
+ 0x00001f1f, 0x040f0404, 0x04040404, 0x00000404,
+ 0x00003f3f, 0x04040404, 0x04040404, 0x001f1f04,
+};
+
+static const u32 ddrc_addrmap_64m[DDRC_NUM_ADDRMAP] = {
+ 0x00001f1f, 0x003f0707, 0x00000000, 0x1f000000,
+ 0x00001f1f, 0x050f0505, 0x05050505, 0x00000505,
+ 0x00003f3f, 0x05050505, 0x05050505, 0x001f1f05,
+};
+
+static const u32 ddrc_addrmap_128m[DDRC_NUM_ADDRMAP] = {
+ 0x00001f1f, 0x00070707, 0x00000000, 0x1f000000,
+ 0x00001f1f, 0x060f0606, 0x06060606, 0x00000606,
+ 0x00003f3f, 0x06060606, 0x06060606, 0x001f1f06,
+};
+
+void ctrl_init_update_by_dram_size(u8 vendor, u8 dram_cap_in_mbyte)
+{
+ u32 mstr = readl(DDRC_BASE);
+ u32 bus_width = FIELD_GET(GENMASK(13, 12), mstr);
+ u32 dev_config = FIELD_GET(GENMASK(31, 30), mstr);
+ u8 dram_cap_in_mbyte_per_dev = dram_cap_in_mbyte;
+ const struct ddrc_size_timing *t;
+
+ if (bus_width > 1 || dev_config > 2) {
+ pr_err("DDR: unsupported MSTR %#x\n", mstr);
+ return;
+ }
+
+ /* System capacity to x16 capacity, then to device capacity */
+ dram_cap_in_mbyte_per_dev >>= 1 - bus_width;
+ dram_cap_in_mbyte_per_dev >>= 2 - dev_config;
+
+ if (dram_cap_in_mbyte_per_dev < DDRC_SIZE_MIN ||
+ dram_cap_in_mbyte_per_dev > DDRC_SIZE_MAX) {
+ pr_err("DDR: no timing for %u MiB per device\n",
+ 1 << dram_cap_in_mbyte_per_dev);
+ return;
+ }
+
+ if (vendor == DDR_VENDOR_ETRON_512M_DDR2)
+ t = &ddrc_size_timing_etron[dram_cap_in_mbyte_per_dev - DDRC_SIZE_MIN];
+ else
+ t = &ddrc_size_timing_esmt[dram_cap_in_mbyte_per_dev - DDRC_SIZE_MIN];
+
+ writel(t->rfshtmg, DDRC_BASE + 0x64);
+ writel(t->dramtmg0, DDRC_BASE + 0x100);
+ writel(t->dramtmg8, DDRC_BASE + 0x120);
+
+ if (dram_cap_in_mbyte_per_dev == 5)
+ ddrc_set_addrmap(ddrc_addrmap_32m);
+ else if (dram_cap_in_mbyte_per_dev == 6)
+ ddrc_set_addrmap(ddrc_addrmap_64m);
+ else
+ ddrc_set_addrmap(ddrc_addrmap_128m);
+
+ /* toggle refresh_update_level */
+ writel(0x00000002, DDRC_BASE + 0x60);
+ writel(0x00000000, DDRC_BASE + 0x60);
+}
diff --git a/drivers/ram/sophgo/phy_init.c b/drivers/ram/sophgo/phy_init.c
new file mode 100644
index 00000000000..7fbccf8e770
--- /dev/null
+++ b/drivers/ram/sophgo/phy_init.c
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Sophgo CV180x DDR2-1333 x16 PHY settings
+ *
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ *
+ * Based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * plat/cv180x/ddr/ddr_config/ddr2_1333_x16/phy_init.c.
+ *
+ */
+
+#include "ddr_sys.h"
+
+static const struct ddr_reg phy_init_regs[] = {
+ { 0x000, 0x03020100 },
+ { 0x004, 0x07060504 },
+ { 0x008, 0x0b0a0908 },
+ { 0x00c, 0x0f0e0d0c },
+ { 0x010, 0x13121110 },
+ { 0x014, 0x00161514 },
+ { 0x018, 0x00000000 },
+ { 0x01c, 0x00000100 },
+ { 0x020, 0x76543210 },
+ { 0x024, 0x00000008 },
+ { 0x028, 0x76543210 },
+ { 0x02c, 0x00000008 },
+ { 0x040, 0x00000000 },
+ { 0x044, 0x007f000d },
+ { 0x048, 0x00000000 },
+ { 0x04c, 0x00000010 },
+ { 0x050, 0x00000004 },
+ { 0x054, 0x0c000b00 },
+ { 0x058, 0x0916004f },
+ { 0x05c, 0x00000000 },
+ { 0x060, 0x04000370 },
+ { 0x064, 0x0e000940 },
+ { 0x068, 0x00800000 },
+ { 0x070, 0x00000108 },
+ { 0x074, 0x00072000 },
+ { 0x080, 0x1f00e000 },
+ { 0x084, 0x0109100f },
+ { 0x088, 0x0a010114 },
+ { 0x08c, 0xaaaa008f },
+ { 0x090, 0x00003800 },
+ { 0x094, 0x00000801 },
+ { 0x0a0, 0x07400540 },
+ { 0x0a4, 0x01420504 },
+ { 0x0a8, 0x0055000a },
+ { 0x0ac, 0x00553c5a },
+ { 0x0b0, 0x00aaa5c3 },
+ { 0x0b4, 0x00aaf0f0 },
+ { 0x0b8, 0x1e000f0f },
+ { 0x0bc, 0x00083e13 },
+ { 0x0f0, 0x00000000 },
+ { 0x0f4, 0x00030033 },
+ { 0x0f8, 0x0021002c },
+ { 0x0fc, 0x00370016 },
+ { 0x100, 0x04000000 },
+ { 0x104, 0x00000000 },
+ { 0x10c, 0x00000000 },
+ { 0x110, 0x00000000 },
+ { 0x114, 0x00000000 },
+ { 0x118, 0x00000000 },
+ { 0x11c, 0x00006a10 },
+ { 0x120, 0x00000000 },
+ { 0x124, 0xfffffef7 },
+ { 0x128, 0x0000001f },
+ { 0x12c, 0x00000001 },
+ { 0x130, 0x00000101 },
+ { 0x134, 0x00000100 },
+ { 0x138, 0x00000014 },
+ { 0x140, 0x00000000 },
+ { 0x144, 0x00000000 },
+ { 0x148, 0x00000808 },
+ { 0x14c, 0x00000000 },
+ { 0x150, 0x00000005 },
+ { 0x154, 0x00000101 },
+ { 0x158, 0x00000000 },
+ { 0x15c, 0x00000000 },
+ { 0x164, 0x00000000 },
+ { 0x168, 0x00333331 },
+ { 0x16c, 0x00000000 },
+ { 0x170, 0x00000000 },
+ { 0x174, 0x00000100 },
+ { 0x180, 0x00020010 },
+ { 0x184, 0x00000000 },
+ { 0x188, 0x00000000 },
+ { 0x18c, 0x00050400 },
+ { 0x190, 0x0002120e },
+ { 0x200, 0x00000000 },
+ { 0x204, 0x00000440 },
+ { 0x208, 0x00000000 },
+ { 0x220, 0x00000000 },
+ { 0x224, 0x00000440 },
+ { 0x228, 0x00000000 },
+ { 0x400, 0x00000000 },
+ { 0x404, 0x00000000 },
+ { 0x408, 0x00000000 },
+ { 0x40c, 0x00000000 },
+ { 0x410, 0x00100000 },
+ { 0x414, 0x00100000 },
+ { 0x418, 0x00000000 },
+ { 0x41c, 0x00004040 },
+ { 0x500, 0x00041001 },
+ { 0x504, 0x00100000 },
+ { 0x508, 0x00000000 },
+ { 0x50c, 0x00000000 },
+ { 0x510, 0x00000004 },
+ { 0x514, 0x00000000 },
+ { 0x518, 0x00000000 },
+ { 0x51c, 0x00000000 },
+ { 0x520, 0x00000003 },
+ { 0x540, 0x00041001 },
+ { 0x544, 0x00100000 },
+ { 0x548, 0x00000000 },
+ { 0x54c, 0x00000000 },
+ { 0x550, 0x00000004 },
+ { 0x554, 0x00000000 },
+ { 0x558, 0x00000000 },
+ { 0x55c, 0x00000000 },
+ { 0x560, 0x00000003 },
+ { 0x900, 0x00000400 },
+ { 0x92c, 0x00000400 },
+ { 0x930, 0x00000400 },
+ { 0x934, 0x00000400 },
+ { 0x938, 0x00000400 },
+ { 0x940, 0x00000000 },
+ { 0x970, 0x00000000 },
+ { 0x974, 0x00000000 },
+ { 0x978, 0x00000000 },
+ { 0x97c, 0x08080404 },
+ { 0x980, 0x08080808 },
+ { 0xa00, 0x06400640 },
+ { 0xa04, 0x06400640 },
+ { 0xa08, 0x06400640 },
+ { 0xa0c, 0x06400640 },
+ { 0xa10, 0x00000640 },
+ { 0xa14, 0x0d000000 },
+ { 0xa18, 0x0a000b00 },
+ { 0xa1c, 0x00040400 },
+ { 0xa20, 0x00400040 },
+ { 0xa24, 0x00400040 },
+ { 0xa28, 0x00400040 },
+ { 0xa2c, 0x00400040 },
+ { 0xa30, 0x00000040 },
+ { 0xa34, 0x00000000 },
+ { 0xa38, 0x00000808 },
+ { 0xa3c, 0x08080808 },
+ { 0xa40, 0x06400640 },
+ { 0xa44, 0x06400640 },
+ { 0xa48, 0x06400640 },
+ { 0xa4c, 0x06400640 },
+ { 0xa50, 0x00000640 },
+ { 0xa54, 0x0d000000 },
+ { 0xa58, 0x0a000b00 },
+ { 0xa5c, 0x00040400 },
+ { 0xa60, 0x00400040 },
+ { 0xa64, 0x00400040 },
+ { 0xa68, 0x00400040 },
+ { 0xa6c, 0x00400040 },
+ { 0xa70, 0x00000040 },
+ { 0xa74, 0x00000000 },
+ { 0xa78, 0x00000808 },
+ { 0xa7c, 0x08080808 },
+ { 0xb00, 0x00000000 },
+ { 0xb04, 0x00000000 },
+ { 0xb08, 0x00404000 },
+ { 0xb0c, 0x00000900 },
+ { 0xb10, 0x00000000 },
+ { 0xb14, 0x00070e0e },
+ { 0xb18, 0x00000000 },
+ { 0xb1c, 0x00000000 },
+ { 0xb20, 0x40400000 },
+ { 0xb24, 0x00100010 },
+ { 0xb30, 0x00000000 },
+ { 0xb34, 0x00000000 },
+ { 0xb38, 0x00404000 },
+ { 0xb3c, 0x00000900 },
+ { 0xb40, 0x00000000 },
+ { 0xb44, 0x00070e0e },
+ { 0xb48, 0x00000000 },
+ { 0xb4c, 0x00000000 },
+ { 0xb50, 0x40400000 },
+ { 0xb54, 0x00100010 },
+};
+
+void phy_init(void)
+{
+ const struct ddr_reg *r;
+
+ for (r = phy_init_regs; r < phy_init_regs + ARRAY_SIZE(phy_init_regs); r++)
+ writel(r->val, PHYD_BASE + r->off);
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH 4/8] board: sophgo: milkv_duo: add SPL support
2026-10-21 12:54 [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Hiago De Franco
` (2 preceding siblings ...)
2026-10-21 12:54 ` [PATCH 3/8] ram: sophgo: add CV180x DDR2 driver Hiago De Franco
@ 2026-10-21 12:54 ` Hiago De Franco
2026-11-04 3:56 ` Yao Zi
2026-10-21 12:54 ` [PATCH 5/8] board: sophgo: milkv_duo: initialize PLLs in SPL Hiago De Franco
` (4 subsequent siblings)
8 siblings, 1 reply; 16+ messages in thread
From: Hiago De Franco @ 2026-10-21 12:54 UTC (permalink / raw)
To: u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
Wire up SPL for the Milk-V Duo so it no longer depends on the vendor
FSBL.
The SPL uses the BootROM ROMAPI to load the FIT image with OpenSBI,
U-Boot and the device tree, from offset 0x40000 of fip.bin.
The BootROM loads the SPL into the 100 KiB VC SRAM at 0x3bc00000 and
jumps past the 32-byte BL2 header, so SPL_TEXT_BASE is 0x3bc00020.
SPL_MAX_SIZE (0x13fe0) is exactly SPL_BSS_START_ADDR (0x3bc14000) minus
SPL_TEXT_BASE, so the SPL image cannot grow into its own BSS.
SPL_BSS_MAX_SIZE (0x1000) caps that 4 KiB BSS, ending at 0x3bc15000, and
SPL_STACK (0x3bc19000) is the end of the SRAM, leaving 16 KiB below it
for the stack, the pre-relocation malloc_f pool (SPL_SYS_MALLOC_F_LEN, 8
KiB) and the global data, as the generic SPL board_init_f flow expects.
In DRAM, SPL_OPENSBI_LOAD_ADDR (0x80000000) is the base of RAM: the
fixed link address OpenSBI's "generic" platform uses for fw_dynamic, and
the load address given to it in the FIT. SPL_CUSTOM_SYS_MALLOC_ADDR
(0x81000000) moves the SPL heap into DRAM once it is up, 16 MiB in,
clear of SYS_LOAD_ADDR (0x80080000), U-Boot's default load address, and
of TEXT_BASE (0x80200000).
SPL_SMP is disabled, as only one hart is brought up, and
SPL_OPENSBI_SCRATCH_OPTIONS is cleared so that OpenSBI prints its boot
banner, which the default (SBI_SCRATCH_NO_BOOT_PRINTS) hides.
Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
---
arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi | 92 +++++++++++++++++++++++++++
arch/riscv/include/asm/arch-cv1800b/rom_api.h | 36 +++++++++++
board/sophgo/milkv_duo/Kconfig | 5 ++
board/sophgo/milkv_duo/MAINTAINERS | 5 +-
board/sophgo/milkv_duo/Makefile | 1 +
board/sophgo/milkv_duo/spl.c | 76 ++++++++++++++++++++++
configs/milkv_duo_defconfig | 18 ++++++
7 files changed, 232 insertions(+), 1 deletion(-)
diff --git a/arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi b/arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi
new file mode 100644
index 00000000000..7f9a7a03c5d
--- /dev/null
+++ b/arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+/*
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ */
+
+#include <config.h>
+
+/ {
+ binman {
+ filename = "fip.bin";
+
+ sophgo-cv18xx-fip {
+ u-boot-spl {
+ };
+ };
+
+ fit {
+ /* Keep in sync with CV18XX_ITB_OFFSET in spl.c */
+ offset = <0x40000>;
+ description = "OpenSBI and S-mode U-Boot";
+ #address-cells = <2>;
+ fit,fdt-list = "of-list";
+
+ images {
+ opensbi {
+ description = "OpenSBI fw_dynamic Firmware";
+ type = "firmware";
+ os = "opensbi";
+ arch = "riscv";
+ load = /bits/ 64 <CONFIG_SPL_OPENSBI_LOAD_ADDR>;
+ entry = /bits/ 64 <CONFIG_SPL_OPENSBI_LOAD_ADDR>;
+
+ opensbi {
+ filename = "fw_dynamic.bin";
+ missing-msg = "opensbi";
+ };
+ };
+
+ uboot {
+ description = "U-Boot";
+ type = "standalone";
+ os = "U-Boot";
+ arch = "riscv";
+ compression = "none";
+ load = /bits/ 64 <CONFIG_TEXT_BASE>;
+
+ u-boot-nodtb {
+ };
+ };
+
+ @fdt-SEQ {
+ fit,operation = "gen-fdt-nodes";
+ description = "NAME";
+ type = "flat_dt";
+ compression = "none";
+ };
+ };
+
+ configurations {
+ default = "@conf-DEFAULT-SEQ";
+
+ @conf-SEQ {
+ description = "NAME";
+ fdt = "fdt-SEQ";
+ firmware = "opensbi";
+ loadables = "uboot";
+ };
+ };
+ };
+ };
+};
+
+&{/soc} {
+ memory-controller@8000000 {
+ compatible = "sophgo,cv1800b-ddr";
+ reg = <0x08000000 0x12000>;
+ bootph-pre-ram;
+ };
+};
+
+&cpus {
+ bootph-pre-ram;
+};
+
+&cpu0 {
+ bootph-pre-ram;
+};
+
+&uart0 {
+ bootph-pre-ram;
+ clock-frequency = <25000000>;
+};
diff --git a/arch/riscv/include/asm/arch-cv1800b/rom_api.h b/arch/riscv/include/asm/arch-cv1800b/rom_api.h
new file mode 100644
index 00000000000..fe4c55321de
--- /dev/null
+++ b/arch/riscv/include/asm/arch-cv1800b/rom_api.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ */
+
+#ifndef __ASM_ARCH_CV1800B_ROM_API_H
+#define __ASM_ARCH_CV1800B_ROM_API_H
+
+#include <linux/types.h>
+
+#define CV1800B_ROM_API_BASE 0x04400000
+#define CV1800B_ROM_API_LOAD_IMAGE (CV1800B_ROM_API_BASE + 0x60)
+#define CV1800B_ROM_API_GET_RETRIES (CV1800B_ROM_API_BASE + 0xc0)
+
+/*
+ * Read @size bytes at @offset of the boot image from the boot device the
+ * BootROM booted from. Returns a negative value on failure.
+ */
+static inline int rom_api_load_image(void *buf, u32 offset, size_t size,
+ int retry)
+{
+ int (*load_image)(void *buf, u32 offset, size_t size, int retry) =
+ (void *)CV1800B_ROM_API_LOAD_IMAGE;
+
+ return load_image(buf, offset, size, retry);
+}
+
+/* Number of retries the BootROM allows for the current boot device */
+static inline int rom_api_get_number_of_retries(void)
+{
+ int (*get_retries)(void) = (void *)CV1800B_ROM_API_GET_RETRIES;
+
+ return get_retries();
+}
+
+#endif /* __ASM_ARCH_CV1800B_ROM_API_H */
diff --git a/board/sophgo/milkv_duo/Kconfig b/board/sophgo/milkv_duo/Kconfig
index 040a7487f1b..b2727355589 100644
--- a/board/sophgo/milkv_duo/Kconfig
+++ b/board/sophgo/milkv_duo/Kconfig
@@ -24,5 +24,10 @@ config ENV_SECT_SIZE
config BOARD_SPECIFIC_OPTIONS
def_bool y
select SOPHGO_CV1800B
+ select BINMAN if SPL
+ select RAM if SPL
+ select SPL_LOAD_BLOCK if SPL
+ select SPL_RAM if SPL
+ select SPL_SOPHGO_CV180X_DDR if SPL
endif
diff --git a/board/sophgo/milkv_duo/MAINTAINERS b/board/sophgo/milkv_duo/MAINTAINERS
index 5e15e461b17..a384a0e6186 100644
--- a/board/sophgo/milkv_duo/MAINTAINERS
+++ b/board/sophgo/milkv_duo/MAINTAINERS
@@ -1,8 +1,11 @@
Milk-V Duo
M: Kongyang Liu <seashell11234455@gmail.com>
S: Maintained
-F: board/sophgo/milkv_duo/
+F: arch/riscv/cpu/cv1800b/spl.c
+F: arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi
+F: arch/riscv/include/asm/arch-cv1800b/rom_api.h
F: board/sophgo/common/
+F: board/sophgo/milkv_duo/
F: configs/milkv_duo_defconfig
F: doc/board/sophgo/milkv_duo.rst
F: drivers/ram/sophgo/
diff --git a/board/sophgo/milkv_duo/Makefile b/board/sophgo/milkv_duo/Makefile
index aa321d15687..6671631a7ba 100644
--- a/board/sophgo/milkv_duo/Makefile
+++ b/board/sophgo/milkv_duo/Makefile
@@ -4,3 +4,4 @@
obj-y += board.o
obj-$(CONFIG_NET_LEGACY) += ../common/ethernet.o
+obj-$(CONFIG_XPL_BUILD) += spl.o
diff --git a/board/sophgo/milkv_duo/spl.c b/board/sophgo/milkv_duo/spl.c
new file mode 100644
index 00000000000..3db50df0615
--- /dev/null
+++ b/board/sophgo/milkv_duo/spl.c
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
+ *
+ */
+
+#include <asm/arch/rom_api.h>
+#include <asm/system.h>
+#include <dm.h>
+#include <hang.h>
+#include <linux/kernel.h>
+#include <spl.h>
+#include <spl_load.h>
+
+/*
+ * Offset of the FIT image in fip.bin, keep in sync with the fit node in
+ * cv1800b-milkv-duo-u-boot.dtsi
+ */
+#define CV18XX_ITB_OFFSET 0x40000
+
+u32 spl_boot_device(void)
+{
+ return BOOT_DEVICE_BOARD;
+}
+
+static ulong cv18xx_rom_read(struct spl_load_info *load, ulong off,
+ ulong count, void *buf)
+{
+ int retries = rom_api_get_number_of_retries();
+ int i;
+
+ /* The BootROM reports no retries for some boot devices */
+ if (retries < 1)
+ retries = 1;
+
+ for (i = 0; i < retries; i++)
+ if (rom_api_load_image(buf, CV18XX_ITB_OFFSET + off, count, i) >= 0)
+ return count;
+
+ return 0;
+}
+
+static int cv18xx_spl_romapi_load(struct spl_image_info *spl_image,
+ struct spl_boot_device *bootdev)
+{
+ struct spl_load_info load;
+
+ spl_load_init(&load, cv18xx_rom_read, NULL, 512);
+
+ return spl_load(spl_image, bootdev, &load, 0, 0);
+}
+
+SPL_LOAD_IMAGE_METHOD("ROMAPI", 0, BOOT_DEVICE_BOARD, cv18xx_spl_romapi_load);
+
+void board_init_f(ulong dummy)
+{
+ struct udevice *dev;
+ int ret;
+
+ ret = spl_early_init();
+ if (ret)
+ hang();
+
+ preloader_console_init();
+
+ /* Probe the CPU so that the RISC-V timer is available */
+ ret = uclass_get_device(UCLASS_CPU, 0, &dev);
+ if (ret)
+ panic("Failed to probe the CPU: %d\n", ret);
+
+ riscv_cpu_setup();
+
+ ret = uclass_get_device(UCLASS_RAM, 0, &dev);
+ if (ret)
+ panic("DRAM init failed: %d\n", ret);
+}
diff --git a/configs/milkv_duo_defconfig b/configs/milkv_duo_defconfig
index c6c0dbb4be6..e279e7c7d5f 100644
--- a/configs/milkv_duo_defconfig
+++ b/configs/milkv_duo_defconfig
@@ -5,16 +5,33 @@ CONFIG_NR_DRAM_BANKS=1
CONFIG_HAS_CUSTOM_SYS_INIT_SP_ADDR=y
CONFIG_CUSTOM_SYS_INIT_SP_ADDR=0x82300000
CONFIG_DEFAULT_DEVICE_TREE="cv1800b-milkv-duo"
+CONFIG_SPL_SYS_MALLOC_F_LEN=0x2000
+CONFIG_SPL_STACK=0x3bc19000
+CONFIG_SPL_TEXT_BASE=0x3bc00020
+CONFIG_SPL_BSS_START_ADDR=0x3bc14000
+CONFIG_SPL_BSS_MAX_SIZE=0x1000
CONFIG_SYS_LOAD_ADDR=0x80080000
+CONFIG_SPL=y
CONFIG_IDENT_STRING="milkv_duo"
CONFIG_TARGET_MILKV_DUO=y
+CONFIG_SPL_OPENSBI_LOAD_ADDR=0x80000000
CONFIG_ARCH_RV64I=y
CONFIG_RISCV_SMODE=y
+# CONFIG_SPL_SMP is not set
CONFIG_STACK_SIZE=0x100000
CONFIG_FIT=y
+CONFIG_SPL_LOAD_FIT=y
CONFIG_BOOTSTD_DEFAULTS=y
CONFIG_SYS_CBSIZE=512
CONFIG_SYS_PBSIZE=544
+CONFIG_SPL_MAX_SIZE=0x13fe0
+# CONFIG_SPL_SHARES_INIT_SP_ADDR is not set
+CONFIG_SPL_HAVE_INIT_STACK=y
+CONFIG_SPL_SYS_MALLOC=y
+CONFIG_SPL_HAS_CUSTOM_MALLOC_START=y
+CONFIG_SPL_CUSTOM_SYS_MALLOC_ADDR=0x81000000
+CONFIG_SPL_OPENSBI=y
+CONFIG_SPL_OPENSBI_SCRATCH_OPTIONS=0x0
CONFIG_SYS_PROMPT="milkv_duo# "
CONFIG_CMD_MMC=y
CONFIG_CMD_POWEROFF=y
@@ -32,6 +49,7 @@ CONFIG_MMC_SDHCI_ADMA=y
CONFIG_MMC_SDHCI_CV1800B=y
CONFIG_SPI_FLASH_MACRONIX=y
CONFIG_ETH_DESIGNWARE=y
+# CONFIG_RAM_SIFIVE is not set
CONFIG_SYS_NS16550=y
CONFIG_SYS_NS16550_MEM32=y
CONFIG_SPI=y
--
2.53.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH 5/8] board: sophgo: milkv_duo: initialize PLLs in SPL
2026-10-21 12:54 [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Hiago De Franco
` (3 preceding siblings ...)
2026-10-21 12:54 ` [PATCH 4/8] board: sophgo: milkv_duo: add SPL support Hiago De Franco
@ 2026-10-21 12:54 ` Hiago De Franco
2026-11-04 4:19 ` Yao Zi
2026-10-21 12:54 ` [PATCH 6/8] board: sophgo: milkv_duo: enable RTC power and reset Hiago De Franco
` (3 subsequent siblings)
8 siblings, 1 reply; 16+ messages in thread
From: Hiago De Franco @ 2026-10-21 12:54 UTC (permalink / raw)
To: u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
DDR training runs from the XTAL, before the PLLs are configured, so add
PLL setup after it in board_init_f().
The sequence bypasses all clocks to the XTAL, reprograms the APLL,
DISPPLL, CAM0PLL, CAM1PLL, MIPIMPLL, MPLL and TPLL synthesizers and
their derived dividers, waits for lock, then switches every clock from
the XTAL bypass to its PLL source. It is ported from the Sophgo FSBL
(plat/cv180x/platform.c), https://github.com/sophgo/fsbl.
Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
---
board/sophgo/milkv_duo/spl.c | 123 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 123 insertions(+)
diff --git a/board/sophgo/milkv_duo/spl.c b/board/sophgo/milkv_duo/spl.c
index 3db50df0615..8b2dc2756ae 100644
--- a/board/sophgo/milkv_duo/spl.c
+++ b/board/sophgo/milkv_duo/spl.c
@@ -2,12 +2,17 @@
/*
* Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
*
+ * PLL setup based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * plat/cv180x/platform.c.
*/
#include <asm/arch/rom_api.h>
+#include <asm/io.h>
#include <asm/system.h>
#include <dm.h>
#include <hang.h>
+#include <linux/bitops.h>
+#include <linux/delay.h>
#include <linux/kernel.h>
#include <spl.h>
#include <spl_load.h>
@@ -18,6 +23,76 @@
*/
#define CV18XX_ITB_OFFSET 0x40000
+#define CLKGEN_BASE 0x03002000UL
+#define CLK_SEL_0 0x020
+#define CLK_SEL_0_C906_1 BIT(24)
+#define CLK_SEL_0_C906_0 BIT(23)
+#define CLK_BYP_0 0x030
+#define CLK_BYP_1 0x034
+#define PLL_G2_CTRL 0x800
+#define PLL_G2_CTRL_PD 0x00011111
+#define MIPIMPLL_CSR 0x808
+#define APLL0_CSR 0x80c
+#define PLL_G2_SSC_SYN_CTRL 0x840
+#define APLL_SSC_SYN_CTRL 0x850
+#define SSC_SYN_CTRL_BYPASS BIT(4)
+#define SSC_SYN_CTRL_SW_UP BIT(0)
+#define APLL_SSC_SYN_SET 0x854
+#define MIPIMPLL_D3_PD 0x8a0
+#define MIPIMPLL_D3_PD_PD BIT(2)
+#define CAM0PLL_D2_PD 0x8ac
+#define CAM0PLL_D2_PD_PD (BIT(2) | BIT(1))
+#define MPLL_CSR 0x908
+#define TPLL_CSR 0x90c
+
+/*
+ * Divider register: [20:16] divider factor, [9:8] clock source, [3] take the
+ * divider factor from the register, [0] de-assert the divider reset.
+ */
+#define CLK_DIV(div, src) (((div) << 16) | ((src) << 8) | BIT(3) | BIT(0))
+#define clkgen_reg(off) ((void __iomem *)(CLKGEN_BASE + (off)))
+
+struct cv18xx_clk_div {
+ u16 off;
+ u32 val;
+};
+
+static const struct cv18xx_clk_div cv18xx_clk_divs[] = {
+ { 0x130, CLK_DIV(1, 3) }, /* clk_c906_0 = MPLL(850) / 1 */
+ { 0x138, CLK_DIV(2, 2) }, /* clk_c906_1 = DISPPLL(1188) / 2 */
+ { 0x054, CLK_DIV(3, 3) }, /* clk_tpu = FPLL(1500) / 3 */
+ { 0x048, CLK_DIV(3, 0) }, /* clk_cpu_axi0 = FPLL(1500) / 3 */
+ { 0x064, CLK_DIV(4, 0) }, /* clk_emmc = FPLL(1500) / 4 */
+ { 0x088, CLK_DIV(8, 0) }, /* clk_spi_nand = FPLL(1500) / 8 */
+ { 0x098, CLK_DIV(18, 0) }, /* clk_sdma_aud0 = APLL(442.368) / 18 */
+ { 0x120, CLK_DIV(15, 0) }, /* clk_pwm_src = FPLL(1500) / 15 */
+ { 0x0a8, CLK_DIV(1, 0) }, /* clk_cam0_200 = XTAL(25) / 1 */
+ { 0x0c8, CLK_DIV(3, 0) }, /* clk_axi_vip = MIPIPLL(900) / 3 */
+ { 0x110, CLK_DIV(2, 2) }, /* clk_src_vip_sys_2 = DISPPLL(1188) / 2 */
+ { 0x144, CLK_DIV(3, 2) }, /* clk_src_vip_sys_4 = DISPPLL(1188) / 3 */
+ { 0x0e4, CLK_DIV(3, 1) }, /* clk_axi_video_codec = MIPIPLL(900) / 3 */
+ { 0x0ec, CLK_DIV(4, 3) }, /* clk_vc_src0 = FPLL(1500) / 4 */
+ { 0x0d0, CLK_DIV(6, 2) }, /* clk_src_vip_sys_0 = DISPPLL(1188) / 6 */
+ { 0x0d8, CLK_DIV(6, 2) }, /* clk_src_vip_sys_1 = DISPPLL(1188) / 6 */
+ { 0x0b8, CLK_DIV(5, 0) }, /* clk_axi4 = FPLL(1500) / 5 */
+ { 0x12c, CLK_DIV(5, 0) }, /* clk_src_rtc_sys_0 = FPLL(1500) / 5 */
+};
+
+/* APLL, DISPPLL, CAM0PLL and CAM1PLL synthesizers and dividers */
+static const u32 cv18xx_pll_syn_set[] = {
+ 614440960, /* 98.304 MHz */
+ 610080582, /* 99 MHz */
+ 610080582, /* 99 MHz */
+ 615164587, /* 98.18181818 MHz */
+};
+
+static const u32 cv18xx_pll_csr[] = {
+ 0x00128201, /* * 9 / 2 = 442.368 MHz */
+ 0x00188101, /* * 12 / 1 = 1188 MHz */
+ 0x00308201, /* * 24 / 2 = 1188 MHz */
+ 0x00168101, /* * 11 / 1 = 1080 MHz */
+};
+
u32 spl_boot_device(void)
{
return BOOT_DEVICE_BOARD;
@@ -52,6 +127,52 @@ static int cv18xx_spl_romapi_load(struct spl_image_info *spl_image,
SPL_LOAD_IMAGE_METHOD("ROMAPI", 0, BOOT_DEVICE_BOARD, cv18xx_spl_romapi_load);
+static void cv18xx_pll_init(void)
+{
+ int i;
+
+ /* Run everything from the XTAL while the PLLs are reprogrammed */
+ writel(0xffffffff, clkgen_reg(CLK_BYP_0));
+ writel(0x0000003f, clkgen_reg(CLK_BYP_1));
+
+ /* MIPIMPLL = 900 MHz */
+ writel(0x05488101, clkgen_reg(MIPIMPLL_CSR));
+
+ writel(0x3f, clkgen_reg(PLL_G2_SSC_SYN_CTRL));
+ for (i = 0; i < ARRAY_SIZE(cv18xx_pll_csr); i++) {
+ writel(cv18xx_pll_syn_set[i],
+ clkgen_reg(APLL_SSC_SYN_SET + 0x10 * i));
+ clrsetbits_le32(clkgen_reg(APLL_SSC_SYN_CTRL + 0x10 * i),
+ SSC_SYN_CTRL_BYPASS, SSC_SYN_CTRL_SW_UP);
+ writel(cv18xx_pll_csr[i], clkgen_reg(APLL0_CSR + 4 * i));
+ }
+
+ /* Power up the PLLs */
+ clrbits_le32(clkgen_reg(PLL_G2_CTRL), PLL_G2_CTRL_PD);
+
+ /* MPLL = TPLL = 850 MHz */
+ writel(0x00448101, clkgen_reg(MPLL_CSR));
+ writel(0x00448101, clkgen_reg(TPLL_CSR));
+
+ for (i = 0; i < ARRAY_SIZE(cv18xx_clk_divs); i++)
+ writel(cv18xx_clk_divs[i].val,
+ clkgen_reg(cv18xx_clk_divs[i].off));
+
+ clrbits_le32(clkgen_reg(MIPIMPLL_D3_PD), MIPIMPLL_D3_PD_PD);
+ clrbits_le32(clkgen_reg(CAM0PLL_D2_PD), CAM0PLL_D2_PD_PD);
+
+ /* Wait for the PLLs to lock */
+ udelay(200);
+
+ /*
+ * C906 clocks from the *_0 dividers, then switch every clock from the
+ * XTAL to its PLL source.
+ */
+ writel(CLK_SEL_0_C906_0 | CLK_SEL_0_C906_1, clkgen_reg(CLK_SEL_0));
+ writel(0, clkgen_reg(CLK_BYP_0));
+ writel(0, clkgen_reg(CLK_BYP_1));
+}
+
void board_init_f(ulong dummy)
{
struct udevice *dev;
@@ -73,4 +194,6 @@ void board_init_f(ulong dummy)
ret = uclass_get_device(UCLASS_RAM, 0, &dev);
if (ret)
panic("DRAM init failed: %d\n", ret);
+
+ cv18xx_pll_init();
}
--
2.53.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH 6/8] board: sophgo: milkv_duo: enable RTC power and reset
2026-10-21 12:54 [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Hiago De Franco
` (4 preceding siblings ...)
2026-10-21 12:54 ` [PATCH 5/8] board: sophgo: milkv_duo: initialize PLLs in SPL Hiago De Franco
@ 2026-10-21 12:54 ` Hiago De Franco
2026-11-03 17:31 ` João Paulo Gonçalves
2026-10-21 12:54 ` [PATCH 7/8] doc: board: sophgo: milkv_duo: update documentation Hiago De Franco
` (2 subsequent siblings)
8 siblings, 1 reply; 16+ messages in thread
From: Hiago De Franco @ 2026-10-21 12:54 UTC (permalink / raw)
To: u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
Let the RTC power domain handle shutdown, warm reset, power-cycle and
watchdog reset requests, and stop the board from powering back on by
itself after a power off.
Without this, the reset and poweroff paths are left in their BootROM
defaults, which do not match what the vendor FSBL configures.
Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
---
board/sophgo/milkv_duo/spl.c | 53 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 52 insertions(+), 1 deletion(-)
diff --git a/board/sophgo/milkv_duo/spl.c b/board/sophgo/milkv_duo/spl.c
index 8b2dc2756ae..97681e5a61b 100644
--- a/board/sophgo/milkv_duo/spl.c
+++ b/board/sophgo/milkv_duo/spl.c
@@ -2,7 +2,7 @@
/*
* Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
*
- * PLL setup based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
+ * PLL and RTC setup based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
* plat/cv180x/platform.c.
*/
@@ -13,7 +13,9 @@
#include <hang.h>
#include <linux/bitops.h>
#include <linux/delay.h>
+#include <linux/iopoll.h>
#include <linux/kernel.h>
+#include <log.h>
#include <spl.h>
#include <spl_load.h>
@@ -52,6 +54,25 @@
#define CLK_DIV(div, src) (((div) << 16) | ((src) << 8) | BIT(3) | BIT(0))
#define clkgen_reg(off) ((void __iomem *)(CLKGEN_BASE + (off)))
+#define RTC_BASE 0x05026000UL
+#define RTC_EN_SHDN_REQ 0x0c0
+#define RTC_EN_PWR_CYC_REQ 0x0c8
+#define RTC_EN_WARM_RST_REQ 0x0cc
+#define RTC_EN_PWR_VBAT_DET 0x0d0
+#define RTC_EN_PWR_VBAT_DET_PWR_UP BIT(2)
+#define RTC_EN_WDT_RST_REQ 0x0e0
+#define RTC_CTRL_BASE 0x05025000UL
+#define RTC_CTRL0_UNLOCKKEY 0x004
+#define RTC_CTRL0_UNLOCKKEY_VAL 0xab18
+#define RTC_CTRL0 0x008
+#define RTC_CTRL0_WRITE_MASK GENMASK(31, 16)
+#define RTC_CTRL0_HW_WDG_RST_EN BIT(11)
+#define RTC_POR_RST_CTRL 0x0ac
+#define RTC_EN_REQ_TIMEOUT_US 100000
+
+#define rtc_reg(off) ((void __iomem *)(RTC_BASE + (off)))
+#define rtc_ctrl_reg(off) ((void __iomem *)(RTC_CTRL_BASE + (off)))
+
struct cv18xx_clk_div {
u16 off;
u32 val;
@@ -173,6 +194,35 @@ static void cv18xx_pll_init(void)
writel(0, clkgen_reg(CLK_BYP_1));
}
+static void cv18xx_rtc_init(void)
+{
+ static const u16 en_reqs[] = {
+ RTC_EN_SHDN_REQ, RTC_EN_WARM_RST_REQ,
+ RTC_EN_PWR_CYC_REQ, RTC_EN_WDT_RST_REQ,
+ };
+ u32 val;
+ int i;
+
+ /* Let the RTC domain handle power off, reset and watchdog requests */
+ for (i = 0; i < ARRAY_SIZE(en_reqs); i++) {
+ writel(1, rtc_reg(en_reqs[i]));
+ if (readl_poll_timeout(rtc_reg(en_reqs[i]), val, val == 1,
+ RTC_EN_REQ_TIMEOUT_US))
+ log_warning("RTC: failed to set register 0x%lx\n",
+ RTC_BASE + en_reqs[i]);
+ }
+
+ writel(1, rtc_ctrl_reg(RTC_POR_RST_CTRL));
+
+ writel(RTC_CTRL0_UNLOCKKEY_VAL, rtc_ctrl_reg(RTC_CTRL0_UNLOCKKEY));
+ /* BIT(6) is set as in the vendor FSBL */
+ setbits_le32(rtc_ctrl_reg(RTC_CTRL0), RTC_CTRL0_WRITE_MASK |
+ RTC_CTRL0_HW_WDG_RST_EN | BIT(6));
+
+ /* Do not power up again after a power off */
+ clrbits_le32(rtc_reg(RTC_EN_PWR_VBAT_DET), RTC_EN_PWR_VBAT_DET_PWR_UP);
+}
+
void board_init_f(ulong dummy)
{
struct udevice *dev;
@@ -190,6 +240,7 @@ void board_init_f(ulong dummy)
panic("Failed to probe the CPU: %d\n", ret);
riscv_cpu_setup();
+ cv18xx_rtc_init();
ret = uclass_get_device(UCLASS_RAM, 0, &dev);
if (ret)
--
2.53.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH 7/8] doc: board: sophgo: milkv_duo: update documentation
2026-10-21 12:54 [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Hiago De Franco
` (5 preceding siblings ...)
2026-10-21 12:54 ` [PATCH 6/8] board: sophgo: milkv_duo: enable RTC power and reset Hiago De Franco
@ 2026-10-21 12:54 ` Hiago De Franco
2026-10-21 12:54 ` [PATCH 8/8] doc: board: sophgo: milkv_duo: document the SPL boot flow Hiago De Franco
2026-10-25 17:33 ` [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Andrei Lalaev
8 siblings, 0 replies; 16+ messages in thread
From: Hiago De Franco @ 2026-10-21 12:54 UTC (permalink / raw)
To: u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
List the drivers already supported in mainline, fix the grammar of the
sentence introducing them and add a blank line before the list to fix
its indentation on https://docs.u-boot-project.org.
Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
---
doc/board/sophgo/milkv_duo.rst | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/doc/board/sophgo/milkv_duo.rst b/doc/board/sophgo/milkv_duo.rst
index cb2ed1ad987..d99a945a61f 100644
--- a/doc/board/sophgo/milkv_duo.rst
+++ b/doc/board/sophgo/milkv_duo.rst
@@ -9,8 +9,11 @@ The CV1800B is a high-performance, low-power 1+1 64-bit RISC-V SoC from Sophgo.
Mainline support
----------------
-The support for following drivers are already enabled:
+Support for the following drivers is already enabled:
+
1. ns16550 UART Driver.
+2. Synopsys DesignWare MSHC Driver.
+3. Synopsys DesignWare Ethernet Controller.
Building
~~~~~~~~
--
2.53.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* [PATCH 8/8] doc: board: sophgo: milkv_duo: document the SPL boot flow
2026-10-21 12:54 [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Hiago De Franco
` (6 preceding siblings ...)
2026-10-21 12:54 ` [PATCH 7/8] doc: board: sophgo: milkv_duo: update documentation Hiago De Franco
@ 2026-10-21 12:54 ` Hiago De Franco
2026-10-25 17:33 ` [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Andrei Lalaev
8 siblings, 0 replies; 16+ messages in thread
From: Hiago De Franco @ 2026-10-21 12:54 UTC (permalink / raw)
To: u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
Update the board documentation now that SPL replaces the vendor
FSBL: OpenSBI is built standalone and passed to the U-Boot build via
OPENSBI=, which produces fip.bin directly instead of u-boot-dtb.bin.
Replace the FSBL + vendor fiptool boot instructions with copying
fip.bin to the SD card, and refresh the sample boot log to include
the SPL banner.
Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
---
doc/board/sophgo/milkv_duo.rst | 71 ++++++++++++++++++++++++------------------
1 file changed, 41 insertions(+), 30 deletions(-)
diff --git a/doc/board/sophgo/milkv_duo.rst b/doc/board/sophgo/milkv_duo.rst
index d99a945a61f..39d54d70cf3 100644
--- a/doc/board/sophgo/milkv_duo.rst
+++ b/doc/board/sophgo/milkv_duo.rst
@@ -14,54 +14,65 @@ Support for the following drivers is already enabled:
1. ns16550 UART Driver.
2. Synopsys DesignWare MSHC Driver.
3. Synopsys DesignWare Ethernet Controller.
+4. SPL with DDR2 initialization.
Building
~~~~~~~~
1. Add the RISC-V toolchain to your PATH.
-2. Setup ARCH & cross compilation environment variable:
+2. Build mainline OpenSBI for the generic platform:
+
+.. code-block:: console
+
+ cd <OpenSBI-dir>
+ PLATFORM=generic CROSS_COMPILE=<riscv64 toolchain prefix> make
+
+This will generate build/platform/generic/firmware/fw_dynamic.bin.
+
+3. Build U-Boot, pointing OPENSBI to the fw_dynamic.bin file:
.. code-block:: console
export CROSS_COMPILE=<riscv64 toolchain prefix>
cd <U-Boot-dir>
make milkv_duo_defconfig
- make
+ OPENSBI=<OpenSBI-dir>/build/platform/generic/firmware/fw_dynamic.bin make
-This will generate u-boot-dtb.bin
+This will generate fip.bin, the image the BootROM loads. Binman builds it from
+the SPL, with the 32-byte BL2 header the BootROM expects, followed at offset
+0x40000 by a FIT image containing OpenSBI, U-Boot and the device tree. The
+vendor fiptool is not needed.
Booting
~~~~~~~
-Currently, we rely on vendor FSBL(First Stage Boot Loader) to initialize the
-clock and load the u-boot image, then bootup from it.
-
-Alternatively, to run u-boot-dtb.bin on top of FSBL, follow these steps:
-
-1. Use the vendor-provided tool to create a unified fip.bin file containing
- FSBL, OpenSBI, and U-Boot.
-
-2. Place the generated fip.bin file into the FAT partition of the SD card.
-
-3. Insert the SD card into the board and power it on.
-
-The board will automatically execute the FSBL from the fip.bin file.
-Subsequently, it will transition to OpenSBI, and finally, OpenSBI will invoke
-U-Boot.
+The BootROM loads the SPL from the fip.bin file in the FAT partition of the SD
+card. The SPL initializes the DDR and the PLLs, then loads the FIT image from
+offset 0x40000 of fip.bin through the BootROM API and jumps to OpenSBI, which
+starts U-Boot in S-mode.
+Copy fip.bin to the FAT partition of the SD card, insert the card into the
+board and power it on.
Sample boot log from Milk-V Duo board
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: none
- U-Boot 2024.01-rc5-00010-g51965baa36 (Dec 28 2023 - 13:15:53 +0800)milkv_duo
+ U-Boot SPL 2026.10-00011-gf0ec1f67053d (Oct 20 2026 - 15:08:30 -0300)
+ Trying to boot from ROMAPI
+ SD/0x40000/0x200/0x200/0.SD/0x40000/0xca000/0xc9ee9/0.
+ OpenSBI v1.9
+ ...
- DRAM: 63.3 MiB
- Core: 10 devices, 8 uclasses, devicetree: separate
- Loading Environment from nowhere... OK
- In: serial@4140000
- Out: serial@4140000
- Err: serial@4140000
- Net: No ethernet found.
- milkv_duo# cpu detail
- 0: cpu@0 rv64imafdc
- ID = 0, freq = 0 Hz: L1 cache, MMU
- milkv_duo#
+ U-Boot 2026.10-00011-gf0ec1f67053d (Oct 20 2026 - 15:08:30 -0300)milkv_duo
+
+ DRAM: 63.3 MiB
+ Core: 23 devices, 15 uclasses, devicetree: separate
+ MMC: mmc@4310000: 0
+ Loading Environment from nowhere... OK
+ In: serial@4140000
+ Out: serial@4140000
+ Err: serial@4140000
+ Net:
+ Warning: ethernet@4070000 (eth0) using random MAC address - be:2a:c8:b9:53:10
+ eth0: ethernet@4070000
+ Hit any key to stop autoboot: 0
+ milkv_duo#
--
2.53.0
^ permalink raw reply related [flat|nested] 16+ messages in thread
* Re: [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo
2026-10-21 12:54 [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Hiago De Franco
` (7 preceding siblings ...)
2026-10-21 12:54 ` [PATCH 8/8] doc: board: sophgo: milkv_duo: document the SPL boot flow Hiago De Franco
@ 2026-10-25 17:33 ` Andrei Lalaev
8 siblings, 0 replies; 16+ messages in thread
From: Andrei Lalaev @ 2026-10-25 17:33 UTC (permalink / raw)
To: Hiago De Franco, u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Zhuxu Ran, Udit Kumar
Hi Hiago,
On 21.10.26 14:54, Hiago De Franco wrote:
> This series adds SPL support for the Milk-V Duo (Sophgo CV1800B, 64 MiB
> DDR2), so the board no longer needs the vendor FSBL to bring up DDR,
> configure the PLLs and hand off to OpenSBI and U-Boot proper.
>
> Currently this board only builds a bare u-boot.bin that depends on the
> vendor FSBL (and the vendor fiptool) to do all of the early hardware
> bring-up before U-Boot starts. With this series:
>
> - The SPL is loaded directly by the BootROM from fip.bin, built by a new
> binman entry type that reproduces the fiptool image layout (param1
> header + BL2 header) without depending on the vendor tool.
> - SPL enables the C906 caches and T-HEAD ISA extensions, brings up and
> trains the DDR2 controller/PHY, and initializes the PLLs, all ported
> from the Sophgo FSBL sources [1] (plat/cv180x).
> - SPL also takes over the RTC power domain configuration for shutdown,
> warm reset, power-cycle and watchdog reset, matching what the vendor
> FSBL used to configure.
> - SPL then loads the FIT image (OpenSBI, U-Boot and the DT) placed at
> offset 0x40000 of fip.bin via the BootROM ROMAPI, and hands off to
> OpenSBI, which starts U-Boot in S-mode.
> - Board documentation is updated.
>
> Tested by booting a Milk-V Duo board from an SD card (fip.bin on the FAT
> partition) built with this series (SPL -> OpenSBI -> U-Boot -> Linux).
>
> This series used the help of Claude Opus 5.5 to read the FSBL and turn
> it into U-Boot SPL.
>
> [1] https://github.com/sophgo/fsbl.git
>
> Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
> ---
> Hiago De Franco (8):
> binman: add Sophgo CV18xx FIP entry type
> riscv: cv1800b: add SPL support
> ram: sophgo: add CV180x DDR2 driver
> board: sophgo: milkv_duo: add SPL support
> board: sophgo: milkv_duo: initialize PLLs in SPL
> board: sophgo: milkv_duo: enable RTC power and reset
> doc: board: sophgo: milkv_duo: update documentation
> doc: board: sophgo: milkv_duo: document the SPL boot flow
>
> arch/riscv/cpu/cv1800b/Kconfig | 2 +
> arch/riscv/cpu/cv1800b/Makefile | 4 +
> arch/riscv/cpu/cv1800b/spl.c | 31 +
> arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi | 92 +++
> arch/riscv/include/asm/arch-cv1800b/rom_api.h | 36 ++
> board/sophgo/milkv_duo/Kconfig | 5 +
> board/sophgo/milkv_duo/MAINTAINERS | 6 +-
> board/sophgo/milkv_duo/Makefile | 1 +
> board/sophgo/milkv_duo/spl.c | 250 ++++++++
> configs/milkv_duo_defconfig | 18 +
> doc/board/sophgo/milkv_duo.rst | 76 ++-
> drivers/ram/Kconfig | 1 +
> drivers/ram/Makefile | 1 +
> drivers/ram/sophgo/Kconfig | 6 +
> drivers/ram/sophgo/Makefile | 6 +
> drivers/ram/sophgo/cv180x_ddr.c | 140 +++++
> drivers/ram/sophgo/cvx16_pinmux.c | 54 ++
> drivers/ram/sophgo/ddr_patch_regs.c | 39 ++
> drivers/ram/sophgo/ddr_pkg_info.c | 110 ++++
> drivers/ram/sophgo/ddr_sys.c | 635 +++++++++++++++++++++
> drivers/ram/sophgo/ddr_sys.h | 108 ++++
> drivers/ram/sophgo/ddrc_init.c | 476 +++++++++++++++
> drivers/ram/sophgo/phy_init.c | 193 +++++++
> tools/binman/etype/sophgo_cv18xx_fip.py | 89 +++
> tools/binman/ftest.py | 54 ++
> tools/binman/test/vendor/sophgo_cv18xx_fip.dts | 18 +
> .../test/vendor/sophgo_cv18xx_fip_bad_offset.dts | 16 +
> .../test/vendor/sophgo_cv18xx_fip_collection.dts | 19 +
> 28 files changed, 2454 insertions(+), 32 deletions(-)
> ---
> base-commit: a58e2b7af42c179ace2c6210de0a219fa3f3cf3c
> change-id: 20261017-milkv-duo-spl-2e2fae6858f1
> prerequisite-change-id: 20260929-milkv-duo-bootstd-14de5e4e03ef:v2
> prerequisite-patch-id: 0aaa37872f2b82c2abfc84364f9256c7cb3394fe
> prerequisite-patch-id: 927c8bc825dd9897467da6b194ae8c7c411ec874
> prerequisite-patch-id: 7c0fedb64aabab799b72eef81efc8becec8f44cd
>
> Best regards,
> --
> Hiago
>
Thank you for your work. Works as expected :)
For the whole series:
Tested-by: Andrei Lalaev <andrey.lalaev@gmail.com>
--
Best regards,
Andrei Lalaev
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH 2/8] riscv: cv1800b: add SPL support
2026-10-21 12:54 ` [PATCH 2/8] riscv: cv1800b: add SPL support Hiago De Franco
@ 2026-11-03 13:53 ` Yao Zi
0 siblings, 0 replies; 16+ messages in thread
From: Yao Zi @ 2026-11-03 13:53 UTC (permalink / raw)
To: Hiago De Franco, u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
On Wed, Oct 21, 2026 at 09:54:18AM -0300, Hiago De Franco wrote:
> Add SUPPORT_SPL and SPL_CPU.
>
> harts_early_init() enables the C906 caches and T-HEAD ISA extensions.
>
> Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
> ---
> arch/riscv/cpu/cv1800b/Kconfig | 2 ++
> arch/riscv/cpu/cv1800b/Makefile | 4 ++++
> arch/riscv/cpu/cv1800b/spl.c | 31 +++++++++++++++++++++++++++++++
> 3 files changed, 37 insertions(+)
...
> diff --git a/arch/riscv/cpu/cv1800b/spl.c b/arch/riscv/cpu/cv1800b/spl.c
> new file mode 100644
> index 00000000000..6fd62d66da7
> --- /dev/null
> +++ b/arch/riscv/cpu/cv1800b/spl.c
> @@ -0,0 +1,31 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/*
> + * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
> + *
> + * CSR setup based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
> + * lib/cpu/riscv/bl2_entrypoint.S.
> + */
> +
> +#include <asm/csr.h>
> +#include <linux/bitops.h>
> +
> +#define CSR_MCOR 0x7c2
> +#define CSR_MCOR_CACHE_INV BIT(4)
> +#define CSR_MCOR_CACHE_SEL_BOTH (BIT(1) | BIT(0))
> +#define CSR_MHCR 0x7c1
> +#define CSR_MHCR_DE BIT(1)
> +#define CSR_MHCR_IE BIT(0)
MHCR-related stuff looks the same on both C906 and C910
(the TH1520 platform). I did a quick check for C908[1] (used in K230)
and C920[2] (used in SG2042), all of them have DE/IE in MHCR
(CSR 0x7c1), so I think it makes sense to move th1520/cache.c to
something like lib/thead_cache.c to re-use it for your series, reducing
some code duplication.
Regards,
Yao Zi
[1]: https://github.com/OctopusET/XuanTie-C908-docs/blob/master/XuanTie-C908-UserManual_xrvm_20240530.pdf
[2]: https://www.xmosc.org.cn/UserFiles/upload/file/20251230/%E7%8E%84%E9%93%81C910%E4%B8%8EC920R1S6%E7%94%A8%E6%88%B7%E6%89%8B%E5%86%8C_Rev.09_20251114.pdf
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH 1/8] binman: add Sophgo CV18xx FIP entry type
2026-10-21 12:54 ` [PATCH 1/8] binman: add Sophgo CV18xx FIP entry type Hiago De Franco
@ 2026-11-03 14:54 ` Simon Glass
0 siblings, 0 replies; 16+ messages in thread
From: Simon Glass @ 2026-11-03 14:54 UTC (permalink / raw)
To: hfranco
Cc: u-boot, Yao Zi, Tom Rini, Simon Glass, Alper Nebi Yasak,
Marek Vasut, Tim Ouyang, Leo Liang, Kongyang Liu,
Neha Malcom Francis, Bin Liu, Quentin Schulz, Andrei Lalaev,
Zhuxu Ran, Udit Kumar
On 2026-10-21T12:54:16+11:00, Hiago De Franco <hfranco@baylibre.com> wrote:
> binman: add Sophgo CV18xx FIP entry type
>
> Add a binman entry type that builds the FIP image loaded by the
> BootROM of the Sophgo CV180x/CV181x SoCs named fip.bin.
>
> The image reuses the layout produced by the vendor fiptool: a 4KiB
> param1 header with size and CRC-16/XMODEM checksums, followed by a
> 32-byte BL2 header (first instruction jumps over it). Unlike the
> vendor tool, only the entry's subnodes (typically u-boot-spl) are
> packed as BL2, since the BootROM does not use the BLCP, param2,
> monitor or second stage loader it also supports.
>
> Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
>
> tools/binman/etype/sophgo_cv18xx_fip.py | 89 ++++++++++++++++++++++
> tools/binman/ftest.py | 54 +++++++++++++
> tools/binman/test/vendor/sophgo_cv18xx_fip.dts | 18 +++++
> .../test/vendor/sophgo_cv18xx_fip_bad_offset.dts | 16 ++++
> .../test/vendor/sophgo_cv18xx_fip_collection.dts | 19 +++++
> 5 files changed, 196 insertions(+)
Reviewed-by: Simon Glass <sjg@chromium.org>
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH 6/8] board: sophgo: milkv_duo: enable RTC power and reset
2026-10-21 12:54 ` [PATCH 6/8] board: sophgo: milkv_duo: enable RTC power and reset Hiago De Franco
@ 2026-11-03 17:31 ` João Paulo Gonçalves
2026-11-04 3:59 ` Yao Zi
0 siblings, 1 reply; 16+ messages in thread
From: João Paulo Gonçalves @ 2026-11-03 17:31 UTC (permalink / raw)
To: Hiago De Franco
Cc: u-boot, Yao Zi, Tom Rini, Simon Glass, Alper Nebi Yasak,
Marek Vasut, Tim Ouyang, Leo Liang, Kongyang Liu,
Neha Malcom Francis, Bin Liu, Quentin Schulz, Andrei Lalaev,
Zhuxu Ran, Udit Kumar
On Wed, Oct 21, 2026 at 09:54:22AM -0300, Hiago De Franco wrote:
> Let the RTC power domain handle shutdown, warm reset, power-cycle and
> watchdog reset requests, and stop the board from powering back on by
> itself after a power off.
>
> Without this, the reset and poweroff paths are left in their BootROM
> defaults, which do not match what the vendor FSBL configures.
>
> Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
> ---
> board/sophgo/milkv_duo/spl.c | 53 +++++++++++++++++++++++++++++++++++++++++++-
> 1 file changed, 52 insertions(+), 1 deletion(-)
>
> diff --git a/board/sophgo/milkv_duo/spl.c b/board/sophgo/milkv_duo/spl.c
> index 8b2dc2756ae..97681e5a61b 100644
> --- a/board/sophgo/milkv_duo/spl.c
> +++ b/board/sophgo/milkv_duo/spl.c
> @@ -2,7 +2,7 @@
> /*
> * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
> *
> - * PLL setup based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
> + * PLL and RTC setup based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
> * plat/cv180x/platform.c.
> */
>
> @@ -13,7 +13,9 @@
> #include <hang.h>
> #include <linux/bitops.h>
> #include <linux/delay.h>
> +#include <linux/iopoll.h>
> #include <linux/kernel.h>
> +#include <log.h>
> #include <spl.h>
> #include <spl_load.h>
>
> @@ -52,6 +54,25 @@
> #define CLK_DIV(div, src) (((div) << 16) | ((src) << 8) | BIT(3) | BIT(0))
> #define clkgen_reg(off) ((void __iomem *)(CLKGEN_BASE + (off)))
>
> +#define RTC_BASE 0x05026000UL
> +#define RTC_EN_SHDN_REQ 0x0c0
> +#define RTC_EN_PWR_CYC_REQ 0x0c8
> +#define RTC_EN_WARM_RST_REQ 0x0cc
> +#define RTC_EN_PWR_VBAT_DET 0x0d0
> +#define RTC_EN_PWR_VBAT_DET_PWR_UP BIT(2)
> +#define RTC_EN_WDT_RST_REQ 0x0e0
> +#define RTC_CTRL_BASE 0x05025000UL
> +#define RTC_CTRL0_UNLOCKKEY 0x004
> +#define RTC_CTRL0_UNLOCKKEY_VAL 0xab18
> +#define RTC_CTRL0 0x008
> +#define RTC_CTRL0_WRITE_MASK GENMASK(31, 16)
> +#define RTC_CTRL0_HW_WDG_RST_EN BIT(11)
> +#define RTC_POR_RST_CTRL 0x0ac
> +#define RTC_EN_REQ_TIMEOUT_US 100000
> +
> +#define rtc_reg(off) ((void __iomem *)(RTC_BASE + (off)))
> +#define rtc_ctrl_reg(off) ((void __iomem *)(RTC_CTRL_BASE + (off)))
Instead of hardcoding the bases here, could we get them from the DT? The
upstream cv18xx.dtsi already has the RTC node (rtc@5025000, with
"syscon" in its compatible), and its range covers both RTC_CTRL_BASE and
RTC_BASE. Also, use syscon/regmap in SPL (CONFIG_SPL_SYSCON and
CONFIG_SPL_REGMAP) to read/write the registers.
Just a suggestion though, I'll leave it to the maintainers to decide.
> +
> struct cv18xx_clk_div {
> u16 off;
> u32 val;
> @@ -173,6 +194,35 @@ static void cv18xx_pll_init(void)
> writel(0, clkgen_reg(CLK_BYP_1));
> }
>
> +static void cv18xx_rtc_init(void)
> +{
> + static const u16 en_reqs[] = {
> + RTC_EN_SHDN_REQ, RTC_EN_WARM_RST_REQ,
> + RTC_EN_PWR_CYC_REQ, RTC_EN_WDT_RST_REQ,
> + };
> + u32 val;
> + int i;
> +
> + /* Let the RTC domain handle power off, reset and watchdog requests */
> + for (i = 0; i < ARRAY_SIZE(en_reqs); i++) {
> + writel(1, rtc_reg(en_reqs[i]));
> + if (readl_poll_timeout(rtc_reg(en_reqs[i]), val, val == 1,
> + RTC_EN_REQ_TIMEOUT_US))
> + log_warning("RTC: failed to set register 0x%lx\n",
> + RTC_BASE + en_reqs[i]);
> + }
> +
> + writel(1, rtc_ctrl_reg(RTC_POR_RST_CTRL));
> +
> + writel(RTC_CTRL0_UNLOCKKEY_VAL, rtc_ctrl_reg(RTC_CTRL0_UNLOCKKEY));
> + /* BIT(6) is set as in the vendor FSBL */
> + setbits_le32(rtc_ctrl_reg(RTC_CTRL0), RTC_CTRL0_WRITE_MASK |
> + RTC_CTRL0_HW_WDG_RST_EN | BIT(6));
> +
> + /* Do not power up again after a power off */
> + clrbits_le32(rtc_reg(RTC_EN_PWR_VBAT_DET), RTC_EN_PWR_VBAT_DET_PWR_UP);
> +}
> +
> void board_init_f(ulong dummy)
> {
> struct udevice *dev;
> @@ -190,6 +240,7 @@ void board_init_f(ulong dummy)
> panic("Failed to probe the CPU: %d\n", ret);
>
> riscv_cpu_setup();
> + cv18xx_rtc_init();
>
> ret = uclass_get_device(UCLASS_RAM, 0, &dev);
> if (ret)
>
> --
> 2.53.0
>
Best regards,
João Paulo Gonçalves
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH 4/8] board: sophgo: milkv_duo: add SPL support
2026-10-21 12:54 ` [PATCH 4/8] board: sophgo: milkv_duo: add SPL support Hiago De Franco
@ 2026-11-04 3:56 ` Yao Zi
0 siblings, 0 replies; 16+ messages in thread
From: Yao Zi @ 2026-11-04 3:56 UTC (permalink / raw)
To: Hiago De Franco, u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
On Wed, Oct 21, 2026 at 09:54:20AM -0300, Hiago De Franco wrote:
> Wire up SPL for the Milk-V Duo so it no longer depends on the vendor
> FSBL.
>
> The SPL uses the BootROM ROMAPI to load the FIT image with OpenSBI,
> U-Boot and the device tree, from offset 0x40000 of fip.bin.
>
> The BootROM loads the SPL into the 100 KiB VC SRAM at 0x3bc00000 and
> jumps past the 32-byte BL2 header, so SPL_TEXT_BASE is 0x3bc00020.
> SPL_MAX_SIZE (0x13fe0) is exactly SPL_BSS_START_ADDR (0x3bc14000) minus
> SPL_TEXT_BASE, so the SPL image cannot grow into its own BSS.
> SPL_BSS_MAX_SIZE (0x1000) caps that 4 KiB BSS, ending at 0x3bc15000, and
> SPL_STACK (0x3bc19000) is the end of the SRAM, leaving 16 KiB below it
> for the stack, the pre-relocation malloc_f pool (SPL_SYS_MALLOC_F_LEN, 8
> KiB) and the global data, as the generic SPL board_init_f flow expects.
Thanks for the helpful explanation, I think it would be better to add
some documentation for the SoC bootflow.
> In DRAM, SPL_OPENSBI_LOAD_ADDR (0x80000000) is the base of RAM: the
> fixed link address OpenSBI's "generic" platform uses for fw_dynamic, and
> the load address given to it in the FIT. SPL_CUSTOM_SYS_MALLOC_ADDR
> (0x81000000) moves the SPL heap into DRAM once it is up, 16 MiB in,
> clear of SYS_LOAD_ADDR (0x80080000), U-Boot's default load address, and
> of TEXT_BASE (0x80200000).
>
> SPL_SMP is disabled, as only one hart is brought up, and
> SPL_OPENSBI_SCRATCH_OPTIONS is cleared so that OpenSBI prints its boot
> banner, which the default (SBI_SCRATCH_NO_BOOT_PRINTS) hides.
Should we avoid this to keep aligned with other RISC-V boards? The
banner, IMHO, is only meaningful for debugging purpose.
> Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
> ---
> arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi | 92 +++++++++++++++++++++++++++
> arch/riscv/include/asm/arch-cv1800b/rom_api.h | 36 +++++++++++
> board/sophgo/milkv_duo/Kconfig | 5 ++
> board/sophgo/milkv_duo/MAINTAINERS | 5 +-
> board/sophgo/milkv_duo/Makefile | 1 +
> board/sophgo/milkv_duo/spl.c | 76 ++++++++++++++++++++++
> configs/milkv_duo_defconfig | 18 ++++++
> 7 files changed, 232 insertions(+), 1 deletion(-)
>
> diff --git a/arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi b/arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi
> new file mode 100644
> index 00000000000..7f9a7a03c5d
> --- /dev/null
> +++ b/arch/riscv/dts/cv1800b-milkv-duo-u-boot.dtsi
> @@ -0,0 +1,92 @@
> +// SPDX-License-Identifier: (GPL-2.0 OR MIT)
> +/*
> + * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
> + */
...
> +&uart0 {
> + bootph-pre-ram;
> + clock-frequency = <25000000>;
The upstream UART 8250 binding disallows clocks and clock-frequency
to co-exist, and with current Linux drivers, it does cause issues, see
this series[1].
If you worry about the burden of introducing clock related stuff to SPL,
consider define CFG_SYS_NS16550_CLK in the board configuration header
instead.
...
Best regards,
Yao Zi
[1]: https://lore.kernel.org/all/20250623093445.62327-1-ziyao@disroot.org/
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH 6/8] board: sophgo: milkv_duo: enable RTC power and reset
2026-11-03 17:31 ` João Paulo Gonçalves
@ 2026-11-04 3:59 ` Yao Zi
0 siblings, 0 replies; 16+ messages in thread
From: Yao Zi @ 2026-11-04 3:59 UTC (permalink / raw)
To: João Paulo Gonçalves, Hiago De Franco
Cc: u-boot, Yao Zi, Tom Rini, Simon Glass, Alper Nebi Yasak,
Marek Vasut, Tim Ouyang, Leo Liang, Kongyang Liu,
Neha Malcom Francis, Bin Liu, Quentin Schulz, Andrei Lalaev,
Zhuxu Ran, Udit Kumar
On Tue, Nov 03, 2026 at 02:31:51PM -0300, João Paulo Gonçalves wrote:
> On Wed, Oct 21, 2026 at 09:54:22AM -0300, Hiago De Franco wrote:
> > Let the RTC power domain handle shutdown, warm reset, power-cycle and
> > watchdog reset requests, and stop the board from powering back on by
> > itself after a power off.
> >
> > Without this, the reset and poweroff paths are left in their BootROM
> > defaults, which do not match what the vendor FSBL configures.
> >
> > Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
> > ---
> > board/sophgo/milkv_duo/spl.c | 53 +++++++++++++++++++++++++++++++++++++++++++-
> > 1 file changed, 52 insertions(+), 1 deletion(-)
> >
> > diff --git a/board/sophgo/milkv_duo/spl.c b/board/sophgo/milkv_duo/spl.c
> > index 8b2dc2756ae..97681e5a61b 100644
> > --- a/board/sophgo/milkv_duo/spl.c
> > +++ b/board/sophgo/milkv_duo/spl.c
> > @@ -2,7 +2,7 @@
> > /*
> > * Copyright (c) 2026, Hiago De Franco <hfranco@baylibre.com>
> > *
> > - * PLL setup based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
> > + * PLL and RTC setup based on the Sophgo FSBL (https://github.com/sophgo/fsbl),
> > * plat/cv180x/platform.c.
> > */
> >
> > @@ -13,7 +13,9 @@
> > #include <hang.h>
> > #include <linux/bitops.h>
> > #include <linux/delay.h>
> > +#include <linux/iopoll.h>
> > #include <linux/kernel.h>
> > +#include <log.h>
> > #include <spl.h>
> > #include <spl_load.h>
> >
> > @@ -52,6 +54,25 @@
> > #define CLK_DIV(div, src) (((div) << 16) | ((src) << 8) | BIT(3) | BIT(0))
> > #define clkgen_reg(off) ((void __iomem *)(CLKGEN_BASE + (off)))
> >
> > +#define RTC_BASE 0x05026000UL
> > +#define RTC_EN_SHDN_REQ 0x0c0
> > +#define RTC_EN_PWR_CYC_REQ 0x0c8
> > +#define RTC_EN_WARM_RST_REQ 0x0cc
> > +#define RTC_EN_PWR_VBAT_DET 0x0d0
> > +#define RTC_EN_PWR_VBAT_DET_PWR_UP BIT(2)
> > +#define RTC_EN_WDT_RST_REQ 0x0e0
> > +#define RTC_CTRL_BASE 0x05025000UL
> > +#define RTC_CTRL0_UNLOCKKEY 0x004
> > +#define RTC_CTRL0_UNLOCKKEY_VAL 0xab18
> > +#define RTC_CTRL0 0x008
> > +#define RTC_CTRL0_WRITE_MASK GENMASK(31, 16)
> > +#define RTC_CTRL0_HW_WDG_RST_EN BIT(11)
> > +#define RTC_POR_RST_CTRL 0x0ac
> > +#define RTC_EN_REQ_TIMEOUT_US 100000
> > +
> > +#define rtc_reg(off) ((void __iomem *)(RTC_BASE + (off)))
> > +#define rtc_ctrl_reg(off) ((void __iomem *)(RTC_CTRL_BASE + (off)))
>
> Instead of hardcoding the bases here, could we get them from the DT? The
> upstream cv18xx.dtsi already has the RTC node (rtc@5025000, with
> "syscon" in its compatible), and its range covers both RTC_CTRL_BASE and
> RTC_BASE. Also, use syscon/regmap in SPL (CONFIG_SPL_SYSCON and
> CONFIG_SPL_REGMAP) to read/write the registers.
>
> Just a suggestion though, I'll leave it to the maintainers to decide.
I'll second the idea of handling RTC stuff in its driver, and I think it
doesn't even need being configured at the SPL stage, right?
...
> Best regards,
> João Paulo Gonçalves
Regards,
Yao Zi
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [PATCH 5/8] board: sophgo: milkv_duo: initialize PLLs in SPL
2026-10-21 12:54 ` [PATCH 5/8] board: sophgo: milkv_duo: initialize PLLs in SPL Hiago De Franco
@ 2026-11-04 4:19 ` Yao Zi
0 siblings, 0 replies; 16+ messages in thread
From: Yao Zi @ 2026-11-04 4:19 UTC (permalink / raw)
To: Hiago De Franco, u-boot, Yao Zi
Cc: Tom Rini, Simon Glass, Alper Nebi Yasak, Marek Vasut, Tim Ouyang,
Leo Liang, Kongyang Liu, Neha Malcom Francis, Bin Liu,
Quentin Schulz, Andrei Lalaev, Zhuxu Ran, Udit Kumar
On Wed, Oct 21, 2026 at 09:54:21AM -0300, Hiago De Franco wrote:
> DDR training runs from the XTAL, before the PLLs are configured, so add
> PLL setup after it in board_init_f().
>
> The sequence bypasses all clocks to the XTAL, reprograms the APLL,
> DISPPLL, CAM0PLL, CAM1PLL, MIPIMPLL, MPLL and TPLL synthesizers and
> their derived dividers, waits for lock, then switches every clock from
> the XTAL bypass to its PLL source. It is ported from the Sophgo FSBL
> (plat/cv180x/platform.c), https://github.com/sophgo/fsbl.
I'm worried about code duplication since we already have a driver with
more features under clk/sophgo.
I think this setup work doesn't have to be done in the SPL stage? If so,
I'll suggest delaying the work to proper U-Boot or even the kernel, with
assigned-clock-rates/assigned-clock-parents for re-using existing clock
driver.
> Signed-off-by: Hiago De Franco <hfranco@baylibre.com>
> ---
> board/sophgo/milkv_duo/spl.c | 123 +++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 123 insertions(+)
>
> diff --git a/board/sophgo/milkv_duo/spl.c b/board/sophgo/milkv_duo/spl.c
> index 3db50df0615..8b2dc2756ae 100644
> --- a/board/sophgo/milkv_duo/spl.c
> +++ b/board/sophgo/milkv_duo/spl.c
...
> +static void cv18xx_pll_init(void)
> +{
> + int i;
> +
> + /* Run everything from the XTAL while the PLLs are reprogrammed */
> + writel(0xffffffff, clkgen_reg(CLK_BYP_0));
> + writel(0x0000003f, clkgen_reg(CLK_BYP_1));
> +
> + /* MIPIMPLL = 900 MHz */
> + writel(0x05488101, clkgen_reg(MIPIMPLL_CSR));
> +
> + writel(0x3f, clkgen_reg(PLL_G2_SSC_SYN_CTRL));
> + for (i = 0; i < ARRAY_SIZE(cv18xx_pll_csr); i++) {
> + writel(cv18xx_pll_syn_set[i],
> + clkgen_reg(APLL_SSC_SYN_SET + 0x10 * i));
> + clrsetbits_le32(clkgen_reg(APLL_SSC_SYN_CTRL + 0x10 * i),
> + SSC_SYN_CTRL_BYPASS, SSC_SYN_CTRL_SW_UP);
> + writel(cv18xx_pll_csr[i], clkgen_reg(APLL0_CSR + 4 * i));
> + }
> +
> + /* Power up the PLLs */
> + clrbits_le32(clkgen_reg(PLL_G2_CTRL), PLL_G2_CTRL_PD);
> +
> + /* MPLL = TPLL = 850 MHz */
> + writel(0x00448101, clkgen_reg(MPLL_CSR));
> + writel(0x00448101, clkgen_reg(TPLL_CSR));
> +
> + for (i = 0; i < ARRAY_SIZE(cv18xx_clk_divs); i++)
> + writel(cv18xx_clk_divs[i].val,
> + clkgen_reg(cv18xx_clk_divs[i].off));
> +
> + clrbits_le32(clkgen_reg(MIPIMPLL_D3_PD), MIPIMPLL_D3_PD_PD);
> + clrbits_le32(clkgen_reg(CAM0PLL_D2_PD), CAM0PLL_D2_PD_PD);
> +
> + /* Wait for the PLLs to lock */
> + udelay(200);
If I read the datasheet correctly, the pll_g2_status register have bits
indicating PLL's locking status. We should probably poll for these bits
instead.
> + /*
> + * C906 clocks from the *_0 dividers, then switch every clock from the
> + * XTAL to its PLL source.
> + */
> + writel(CLK_SEL_0_C906_0 | CLK_SEL_0_C906_1, clkgen_reg(CLK_SEL_0));
> + writel(0, clkgen_reg(CLK_BYP_0));
> + writel(0, clkgen_reg(CLK_BYP_1));
> +}
Regards,
Yao Zi
^ permalink raw reply [flat|nested] 16+ messages in thread
end of thread, other threads:[~2026-11-04 4:20 UTC | newest]
Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-21 12:54 [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Hiago De Franco
2026-10-21 12:54 ` [PATCH 1/8] binman: add Sophgo CV18xx FIP entry type Hiago De Franco
2026-11-03 14:54 ` Simon Glass
2026-10-21 12:54 ` [PATCH 2/8] riscv: cv1800b: add SPL support Hiago De Franco
2026-11-03 13:53 ` Yao Zi
2026-10-21 12:54 ` [PATCH 3/8] ram: sophgo: add CV180x DDR2 driver Hiago De Franco
2026-10-21 12:54 ` [PATCH 4/8] board: sophgo: milkv_duo: add SPL support Hiago De Franco
2026-11-04 3:56 ` Yao Zi
2026-10-21 12:54 ` [PATCH 5/8] board: sophgo: milkv_duo: initialize PLLs in SPL Hiago De Franco
2026-11-04 4:19 ` Yao Zi
2026-10-21 12:54 ` [PATCH 6/8] board: sophgo: milkv_duo: enable RTC power and reset Hiago De Franco
2026-11-03 17:31 ` João Paulo Gonçalves
2026-11-04 3:59 ` Yao Zi
2026-10-21 12:54 ` [PATCH 7/8] doc: board: sophgo: milkv_duo: update documentation Hiago De Franco
2026-10-21 12:54 ` [PATCH 8/8] doc: board: sophgo: milkv_duo: document the SPL boot flow Hiago De Franco
2026-10-25 17:33 ` [PATCH 0/8] riscv: sophgo: add SPL support for the Milk-V Duo Andrei Lalaev
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.