diff --git a/arch/arm/mach-sunxi/Kconfig b/arch/arm/mach-sunxi/Kconfig
index ed2402f71f2..0326c7a976b 100644
--- a/arch/arm/mach-sunxi/Kconfig
+++ b/arch/arm/mach-sunxi/Kconfig
@@ -788,6 +788,7 @@ config DRAM_ODT_EN
 	depends on !MACH_SUN50I_H616
 	depends on !MACH_SUN55I_A523
 	default y if MACH_SUN8I_A23
+	default y if MACH_SUN8I_B288
 	default y if MACH_SUNXI_H3_H5
 	default y if MACH_SUN8I_R40
 	default y if MACH_SUN50I
diff --git a/arch/arm/mach-sunxi/dram_sunxi_dw.c b/arch/arm/mach-sunxi/dram_sunxi_dw.c
index 4ed295909ce..ee96186abc6 100644
--- a/arch/arm/mach-sunxi/dram_sunxi_dw.c
+++ b/arch/arm/mach-sunxi/dram_sunxi_dw.c
@@ -21,6 +21,10 @@ static void mctl_phy_init(u32 val)
 	struct sunxi_mctl_ctl_reg * const mctl_ctl =
 			(struct sunxi_mctl_ctl_reg *)SUNXI_DRAM_CTL0_BASE;
 
+	/* the B288 latches the configuration before triggering */
+	if (IS_ENABLED(CONFIG_MACH_SUN8I_B288))
+		writel(val, &mctl_ctl->pir);
+
 	writel(val | PIR_INIT, &mctl_ctl->pir);
 	mctl_await_completion(&mctl_ctl->pgsr[0], PGSR_INIT_DONE, 0x1);
 }
@@ -48,6 +52,11 @@ static void mctl_set_bit_delays(struct dram_para *para)
 	for (i = 0; i < 4; i++)
 		writel(0x6 << 24, &mctl_ctl->dx[i].sdlr);
 #endif
+	if (IS_ENABLED(CONFIG_MACH_SUN8I_B288)) {
+		/* boot0 uses 8 on DX0/DX1 and 6 on DX2/DX3 */
+		for (i = 0; i < 4; i++)
+			writel((i < 2 ? 0x8 : 0x6) << 24, &mctl_ctl->dx[i].sdlr);
+	}
 
 	setbits_le32(&mctl_ctl->pgcr[0], 1 << 26);
 }
@@ -248,9 +257,24 @@ static void mctl_set_master_priority_r40(void)
 	MBUS_CONF(UNKNOWN3, true,    HIGH, 0, 1280,  144,   64);
 }
 
+/*
+ * The B288 boot0 only sets the MBUS window timer: it leaves the per-port
+ * table, bwcr and mapr alone, so no master is bandwidth limited.
+ */
+static void mctl_set_master_priority_b288(void)
+{
+	struct sunxi_mctl_com_reg * const mctl_com =
+			(struct sunxi_mctl_com_reg *)SUNXI_DRAM_COM_BASE;
+
+	clrsetbits_le32(&mctl_com->tmr, 0xfff, 399);
+}
+
 static void mctl_set_master_priority(uint16_t socid)
 {
 	switch (socid) {
+	case SOCID_B288:
+		mctl_set_master_priority_b288();
+		return;
 	case SOCID_H3:
 		mctl_set_master_priority_h3();
 		return;
@@ -430,14 +454,15 @@ static void mctl_sys_init(uint16_t socid, struct dram_para *para)
 	clrbits_le32(&ccm->ahb_gate0, 1 << AHB_GATE_OFFSET_MCTL);
 	clrbits_le32(&ccm->ahb_reset0_cfg, 1 << AHB_RESET_OFFSET_MCTL);
 	clrbits_le32(&ccm->pll5_cfg, CCM_PLL5_CTRL_EN);
-	if (socid == SOCID_A64 || socid == SOCID_R40)
+	if (socid == SOCID_A64 || socid == SOCID_R40 || socid == SOCID_B288)
 		clrbits_le32(&ccm->pll11_cfg, CCM_PLL11_CTRL_EN);
 	udelay(10);
 
 	clrbits_le32(&ccm->dram_clk_cfg, CCM_DRAMCLK_CFG_RST);
 	udelay(1000);
 
-	if (socid == SOCID_A64 || socid == SOCID_R40) {
+	if (socid == SOCID_A64 || socid == SOCID_R40 || socid == SOCID_B288) {
+		/* The B288 feeds the controller from PLL_DDR1 (pll11), too */
 		clock_set_pll11(CONFIG_DRAM_CLK * 2 * 1000000, false);
 		clrsetbits_le32(&ccm->dram_clk_cfg,
 				CCM_DRAMCLK_CFG_DIV_MASK |
@@ -464,7 +489,8 @@ static void mctl_sys_init(uint16_t socid, struct dram_para *para)
 	setbits_le32(&ccm->dram_clk_cfg, CCM_DRAMCLK_CFG_RST);
 	udelay(10);
 
-	writel(socid == SOCID_H5 ? 0x8000 : 0xc00e, &mctl_ctl->clken);
+	writel((socid == SOCID_H5 || socid == SOCID_B288) ? 0x8000 : 0xc00e,
+	       &mctl_ctl->clken);
 	udelay(500);
 }
 
@@ -473,6 +499,14 @@ static void mctl_sys_init(uint16_t socid, struct dram_para *para)
 #define DX_GCR_ODT_ALWAYS_ON	(0x1 << 4)
 #define DX_GCR_ODT_OFF		(0x2 << 4)
 
+static void mctl_set_odtmap(struct dram_para *para)
+{
+	struct sunxi_mctl_ctl_reg * const mctl_ctl =
+			(struct sunxi_mctl_ctl_reg *)SUNXI_DRAM_CTL0_BASE;
+
+	writel(para->dual_rank ? 0x00000303 : 0x00000201, &mctl_ctl->odtmap);
+}
+
 static int mctl_channel_init(uint16_t socid, struct dram_para *para)
 {
 	struct sunxi_mctl_com_reg * const mctl_com =
@@ -488,7 +522,7 @@ static int mctl_channel_init(uint16_t socid, struct dram_para *para)
 
 	/* setting VTC, default disable all VT */
 	clrbits_le32(&mctl_ctl->pgcr[0], (1 << 30) | 0x3f);
-	if (socid == SOCID_H5)
+	if (socid == SOCID_H5 || socid == SOCID_B288)
 		setbits_le32(&mctl_ctl->pgcr[1], (1 << 24) | (1 << 26));
 	else
 		clrsetbits_le32(&mctl_ctl->pgcr[1], 1 << 24, 1 << 26);
@@ -515,8 +549,9 @@ static int mctl_channel_init(uint16_t socid, struct dram_para *para)
 	}
 
 	/* AC PDR should always ON */
-	clrsetbits_le32(&mctl_ctl->aciocr, socid == SOCID_H5 ? (0x1 << 11) : 0,
-			0x1 << 1);
+	clrsetbits_le32(&mctl_ctl->aciocr,
+			(socid == SOCID_H5 || socid == SOCID_B288) ?
+			(0x1 << 11) : 0, 0x1 << 1);
 
 	/* set DQS auto gating PD mode */
 	setbits_le32(&mctl_ctl->pgcr[2], 0x3 << 6);
@@ -535,7 +570,8 @@ static int mctl_channel_init(uint16_t socid, struct dram_para *para)
 		/* dphy & aphy phase select 270 degree */
 		clrsetbits_le32(&mctl_ctl->pgcr[2], (0x3 << 10) | (0x3 << 8),
 				(0x1 << 10) | (0x1 << 8));
-	} else if (socid == SOCID_A64 || socid == SOCID_H5) {
+	} else if (socid == SOCID_A64 || socid == SOCID_H5 ||
+		   socid == SOCID_B288) {
 		/* dphy & aphy phase select ? */
 		clrsetbits_le32(&mctl_ctl->pgcr[2], (0x3 << 10) | (0x3 << 8),
 				(0x0 << 10) | (0x3 << 8));
@@ -572,6 +608,19 @@ static int mctl_channel_init(uint16_t socid, struct dram_para *para)
 
 		mctl_phy_init(PIR_PLLINIT | PIR_DCAL | PIR_PHYRST |
 			      PIR_DRAMRST | PIR_DRAMINIT | PIR_QSGATE);
+	} else if (socid == SOCID_B288) {
+		/* A 24-bit zqcr value, as on the A64; the H3 quirk hangs */
+		clrsetbits_le32(&mctl_ctl->zqcr, 0xffffff, CONFIG_DRAM_ZQ);
+		mctl_set_odtmap(para);
+
+		/* DQS gating mode 1: gate training finds the ranks and lanes */
+		clrbits_le32(&mctl_ctl->pgcr[2], 0x3 << 6);
+		clrbits_le32(&mctl_ctl->dqsgmr, 0x107);
+		/* the vendor's training configuration */
+		clrsetbits_le32(&mctl_ctl->dtcr, 0xffffff, 0x3087);
+		mctl_phy_init(PIR_ZCAL | PIR_PLLINIT | PIR_PHYRST);
+		mctl_phy_init(PIR_DCAL | PIR_DRAMRST | PIR_DRAMINIT |
+			      PIR_QSGATE);
 	} else if (socid == SOCID_H3) {
 		mctl_h3_zq_calibration_quirk(para);
 
@@ -636,7 +685,7 @@ static int mctl_channel_init(uint16_t socid, struct dram_para *para)
 	udelay(10);
 
 	/* set PGCR3, CKE polarity */
-	if (socid == SOCID_H3 || socid == SOCID_V3S)
+	if (socid == SOCID_H3 || socid == SOCID_V3S || socid == SOCID_B288)
 		writel(0x00aa0060, &mctl_ctl->pgcr[3]);
 	else if (socid == SOCID_A64 || socid == SOCID_H5 || socid == SOCID_R40)
 		writel(0xc0aa0060, &mctl_ctl->pgcr[3]);
@@ -822,6 +871,23 @@ static void mctl_auto_detect_dram_size(uint16_t socid, struct dram_para *para)
 	   3,  3,  3,  3,  3,  3,  3,  3,			\
 	   3,  3,  3,  3,  2,  0,  0      }
 
+/* As programmed by the B288 boot0 */
+#define SUN8I_B288_DX_READ_DELAYS				\
+	{{ 16, 17, 18, 19, 18, 19, 19, 19, 16,  0,  0 },	\
+	 { 26, 26, 26, 25, 25, 25, 25, 25, 20,  0,  0 },	\
+	 { 20, 22, 24, 22, 26, 25, 25, 25, 20,  0,  0 },	\
+	 { 21, 21, 21, 20, 20, 19, 19, 19, 18,  0,  0 }}
+#define SUN8I_B288_DX_WRITE_DELAYS				\
+	{{  0,  1,  3,  3,  3,  3,  3,  4,  3, 11,  0 },	\
+	 {  3,  3,  4,  4,  3,  3,  3,  3,  3, 11,  0 },	\
+	 {  0,  3,  3,  1,  6,  6,  5,  6,  3,  9,  0 },	\
+	 {  5,  5,  6,  6,  4,  5,  3,  3,  3,  9,  0 }}
+#define SUN8I_B288_AC_DELAYS					\
+	{  0,  0,  8,  3,  0,  0,  0,  0,			\
+	   0,  0,  0,  0,  0,  0,  0,  0,			\
+	   0,  0,  0,  0,  0,  0,  0,  0,			\
+	   0,  0,  0,  0,  3,  0,  0      }
+
 unsigned long sunxi_dram_init(void)
 {
 	struct sunxi_mctl_com_reg * const mctl_com =
@@ -851,6 +917,10 @@ unsigned long sunxi_dram_init(void)
 		.dx_read_delays  = SUN8I_H3_DX_READ_DELAYS,
 		.dx_write_delays = SUN8I_H3_DX_WRITE_DELAYS,
 		.ac_delays	 = SUN8I_H3_AC_DELAYS,
+#elif defined(CONFIG_MACH_SUN8I_B288)
+		.dx_read_delays  = SUN8I_B288_DX_READ_DELAYS,
+		.dx_write_delays = SUN8I_B288_DX_WRITE_DELAYS,
+		.ac_delays	 = SUN8I_B288_AC_DELAYS,
 #elif defined(CONFIG_MACH_SUN8I_V3S)
 		.dx_read_delays  = SUN8I_V3S_DX_READ_DELAYS,
 		.dx_write_delays = SUN8I_V3S_DX_WRITE_DELAYS,
@@ -878,6 +948,8 @@ unsigned long sunxi_dram_init(void)
 	uint16_t socid = SOCID_H3;
 #elif defined(CONFIG_MACH_SUN8I_R40)
 	uint16_t socid = SOCID_R40;
+#elif defined(CONFIG_MACH_SUN8I_B288)
+	uint16_t socid = SOCID_B288;
 #elif defined(CONFIG_MACH_SUN8I_V3S)
 	uint16_t socid = SOCID_V3S;
 #elif defined(CONFIG_MACH_SUN50I)
@@ -890,12 +962,13 @@ unsigned long sunxi_dram_init(void)
 	if (mctl_channel_init(socid, &para))
 		return 0;
 
-	if (para.dual_rank)
-		writel(0x00000303, &mctl_ctl->odtmap);
-	else
-		writel(0x00000201, &mctl_ctl->odtmap);
+	mctl_set_odtmap(&para);
 	udelay(1);
 
+	/* the B288 trains with static DX clocks; dynamic from now on */
+	if (socid == SOCID_B288)
+		clrbits_le32(&mctl_ctl->pgcr[0], (0x3 << 14) | (0x3 << 12));
+
 	/* odt delay */
 	if (socid == SOCID_H3)
 		writel(0x0c000400, &mctl_ctl->odtcfg);
