summaryrefslogtreecommitdiff
path: root/board/samsung
diff options
context:
space:
mode:
Diffstat (limited to 'board/samsung')
-rw-r--r--board/samsung/e850-96/Makefile2
-rw-r--r--board/samsung/e850-96/acpm.c169
-rw-r--r--board/samsung/e850-96/acpm.h27
-rw-r--r--board/samsung/e850-96/e850-96.c97
-rw-r--r--board/samsung/e850-96/e850-96.env2
-rw-r--r--board/samsung/e850-96/fw.c45
-rw-r--r--board/samsung/e850-96/fw.h4
-rw-r--r--board/samsung/e850-96/pmic.c144
-rw-r--r--board/samsung/e850-96/pmic.h14
9 files changed, 473 insertions, 31 deletions
diff --git a/board/samsung/e850-96/Makefile b/board/samsung/e850-96/Makefile
index 71d46ea3d2b..76b8d47994e 100644
--- a/board/samsung/e850-96/Makefile
+++ b/board/samsung/e850-96/Makefile
@@ -3,4 +3,4 @@
# Copyright (C) 2024, Linaro Limited
# Sam Protsenko <semen.protsenko@linaro.org>
-obj-y := e850-96.o fw.o
+obj-y := e850-96.o fw.o acpm.o pmic.o
diff --git a/board/samsung/e850-96/acpm.c b/board/samsung/e850-96/acpm.c
new file mode 100644
index 00000000000..1cc5c6d0e4a
--- /dev/null
+++ b/board/samsung/e850-96/acpm.c
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (c) 2025 Linaro Ltd.
+ * Author: Sam Protsenko <semen.protsenko@linaro.org>
+ *
+ * ACPM (Active Clock and Power Management) is an IPC protocol for communicating
+ * with APM (Active Power Management) core. The message exchange between AP
+ * (Application Processor) and APM is happening by using shared memory in SRAM
+ * (iRAM) and generating interrupts using Mailbox block. By using this IPC
+ * interface it's possible to offload power management tasks to APM core, which
+ * acts as a supervisor for CPU. One of the main tasks of APM is controlling
+ * PMIC chip over I3C bus. So in order to access PMIC chip registers it's
+ * recommended to do so by sending corresponding commands to APM via ACPM IPC
+ * protocol. The IPC interaction sequence looks like this:
+ *
+ * AP (CPU) <-> ACPM IPC (Mailbox + SRAM) <-> APM <-> I3C <-> PMIC
+ *
+ * This file contains functions for accessing I3C bus via APM block using
+ * ACPM IPC.
+ */
+
+#include <linux/iopoll.h>
+#include <linux/time.h>
+#include <asm/io.h>
+#include "acpm.h"
+
+/* Mailbox registers */
+#define MBOX_INTGR0 0x8 /* Interrupt Generation */
+#define MBOX_INTCR1 0x20 /* Interrupt Clear */
+#define MBOX_INTSR1 0x28 /* Interrupt Status */
+#define MBOX_INTGR_OFFSET 16
+#define MBOX_TIMEOUT (1 * USEC_PER_SEC)
+
+/* APM shared memory registers */
+#define SHMEM_SR0 0x0
+#define SHMEM_SR1 0x4
+#define SHMEM_SR2 0x8
+#define SHMEM_SR3 0xc
+
+/* IPC functions */
+#define IPC_FUNC_READ 0x0
+#define IPC_FUNC_WRITE 0x1
+/* Command 0 shifts and masks */
+#define IPC_REG_SHIFT 0
+#define IPC_REG_MASK 0xff
+#define IPC_TYPE_SHIFT 8
+#define IPC_TYPE_MASK 0xf
+#define IPC_CHANNEL_SHIFT 12
+#define IPC_CHANNEL_MASK 0xf
+/* Command 1 shifts and masks */
+#define IPC_FUNC_SHIFT 0
+#define IPC_FUNC_MASK 0xff
+#define IPC_WRITE_VAL_SHIFT 8
+#define IPC_WRITE_VAL_MASK 0xff
+/* Command 3 shifts and masks */
+#define IPC_DEST_SHIFT 8
+#define IPC_DEST_MASK 0xff
+#define IPC_RETURN_SHIFT 24
+#define IPC_RETURN_MASK 0xff
+
+/**
+ * acpm_ipc_send_data_async() - Send data to I3C block over ACPM IPC
+ * @acpm: ACPM data
+ * @cmd0: Command 0 value to send
+ * @cmd1: Command 1 value to send
+ */
+static void acpm_ipc_send_data_async(struct acpm *acpm, u32 cmd0, u32 cmd1)
+{
+ u32 irq_bit = 1 << acpm->ipc_ch;
+ u32 intgr = irq_bit << MBOX_INTGR_OFFSET;
+
+ /* Write data to the shared memory */
+ writel(cmd0, acpm->sram_base + SHMEM_SR0);
+ writel(cmd1, acpm->sram_base + SHMEM_SR1);
+ dsb();
+
+ /* Generate interrupt for I3C block */
+ writel(intgr, acpm->mbox_base + MBOX_INTGR0);
+}
+
+/**
+ * acpm_ipc_wait_resp() - Read response data from I3C block over ACPM IPC
+ * @acpm: ACPM data
+ * @cmd2: Will contain read value for command 2
+ * @cmd3: Will contain read value for command 3
+ *
+ * Return: 0 on success or negative value on error.
+ */
+static int acpm_ipc_wait_resp(struct acpm *acpm, u32 *cmd2, u32 *cmd3)
+{
+ u32 irq_bit = 1 << acpm->ipc_ch;
+ u32 reg;
+ int ret;
+
+ /* Wait for the interrupt from I3C block */
+ ret = readl_poll_timeout(acpm->mbox_base + MBOX_INTSR1, reg,
+ reg & irq_bit, MBOX_TIMEOUT);
+ if (ret < 0)
+ return ret;
+
+ /* Clear the interrupt */
+ writel(irq_bit, acpm->mbox_base + MBOX_INTCR1);
+
+ /* Read data from the shared memory */
+ *cmd2 = readl(acpm->sram_base + SHMEM_SR2);
+ *cmd3 = readl(acpm->sram_base + SHMEM_SR3);
+
+ return 0;
+}
+
+/**
+ * acpm_i3c_read() - Read an I3C register of some I3C slave device
+ * @acpm: ACPM data
+ * @ch: I3C channel (bus) number (0-15)
+ * @addr: I3C address of slave device (0-15)
+ * @reg: Address of I3C register in the slave device to read from
+ * @val: Will contain the read value
+ *
+ * Return: 0 on success or non-zero code on error (may be positive).
+ */
+int acpm_i3c_read(struct acpm *acpm, u8 ch, u8 addr, u8 reg, u8 *val)
+{
+ u32 cmd[4] = { 0 };
+ u8 ret;
+
+ cmd[0] = (ch & IPC_CHANNEL_MASK) << IPC_CHANNEL_SHIFT |
+ (addr & IPC_TYPE_MASK) << IPC_TYPE_SHIFT |
+ (reg & IPC_REG_MASK) << IPC_REG_SHIFT;
+ cmd[1] = IPC_FUNC_READ << IPC_FUNC_SHIFT;
+
+ acpm_ipc_send_data_async(acpm, cmd[0], cmd[1]);
+ ret = acpm_ipc_wait_resp(acpm, &cmd[2], &cmd[3]);
+ if (ret)
+ return ret;
+
+ *val = (cmd[3] >> IPC_DEST_SHIFT) & IPC_DEST_MASK;
+ ret = (cmd[3] >> IPC_RETURN_SHIFT) & IPC_RETURN_MASK;
+ return ret;
+}
+
+/**
+ * acpm_i3c_write() - Write an I3C register of some I3C slave device
+ * @acpm: ACPM data
+ * @ch: I3C channel (bus) number (0-15)
+ * @addr: I3C address of slave device (0-15)
+ * @reg: Address of I3C register in the slave device to write into
+ * @val: Value to write
+ *
+ * Return: 0 on success or non-zero code on error (may be positive).
+ */
+int acpm_i3c_write(struct acpm *acpm, u8 ch, u8 addr, u8 reg, u8 val)
+{
+ u32 cmd[4] = { 0 };
+ u8 ret;
+
+ cmd[0] = (ch & IPC_CHANNEL_MASK) << IPC_CHANNEL_SHIFT |
+ (addr & IPC_TYPE_MASK) << IPC_TYPE_SHIFT |
+ (reg & IPC_REG_MASK) << IPC_REG_SHIFT;
+ cmd[1] = IPC_FUNC_WRITE << IPC_FUNC_SHIFT |
+ (val & IPC_WRITE_VAL_MASK) << IPC_WRITE_VAL_SHIFT;
+
+ acpm_ipc_send_data_async(acpm, cmd[0], cmd[1]);
+ ret = acpm_ipc_wait_resp(acpm, &cmd[2], &cmd[3]);
+ if (ret)
+ return ret;
+
+ ret = (cmd[3] >> IPC_RETURN_SHIFT) & IPC_RETURN_MASK;
+ return ret;
+}
diff --git a/board/samsung/e850-96/acpm.h b/board/samsung/e850-96/acpm.h
new file mode 100644
index 00000000000..9373969209f
--- /dev/null
+++ b/board/samsung/e850-96/acpm.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright (c) 2025 Linaro Ltd.
+ * Sam Protsenko <semen.protsenko@linaro.org>
+ */
+
+#ifndef __E850_96_ACPM_H
+#define __E850_96_ACPM_H
+
+#include <linux/types.h>
+
+/**
+ * struct acpm - Data for I3C communication over ACPM IPC protocol
+ * @mbox_base: Base address of APM mailbox block
+ * @sram_base: Base address of shared memory used for APM messages
+ * @ipc_ch: Mailbox channel number used for communication with I3C block (0-15)
+ */
+struct acpm {
+ void __iomem *mbox_base;
+ void __iomem *sram_base;
+ u8 ipc_ch;
+};
+
+int acpm_i3c_read(struct acpm *acpm, u8 ch, u8 addr, u8 reg, u8 *val);
+int acpm_i3c_write(struct acpm *acpm, u8 ch, u8 addr, u8 reg, u8 val);
+
+#endif /* __E850_96_ACPM_H */
diff --git a/board/samsung/e850-96/e850-96.c b/board/samsung/e850-96/e850-96.c
index a6c264d1248..3df241edde2 100644
--- a/board/samsung/e850-96/e850-96.c
+++ b/board/samsung/e850-96/e850-96.c
@@ -8,13 +8,28 @@
#include <env.h>
#include <init.h>
#include <mapmem.h>
+#include <net.h>
+#include <usb.h>
#include <asm/io.h>
#include "fw.h"
+#include "pmic.h"
/* OTP Controller base address and register offsets */
-#define EXYNOS850_OTP_BASE 0x10000000
-#define OTP_CHIPID0 0x4
-#define OTP_CHIPID1 0x8
+#define EXYNOS850_OTP_BASE 0x10000000
+#define OTP_CHIPID0 0x4
+#define OTP_CHIPID1 0x8
+
+/* ACPM and PMIC definitions */
+#define EXYNOS850_MBOX_APM2AP_BASE 0x11900000
+#define EXYNOS850_APM_SRAM_BASE 0x02039000 /* in iRAM */
+#define EXYNOS850_APM_SHMEM_OFFSET 0x3200
+#define EXYNOS850_IPC_AP_I3C 10
+
+/* LDFW firmware definitions */
+#define LDFW_NWD_ADDR 0x88000000
+#define EMMC_IFNAME "mmc"
+#define EMMC_DEV_NUM 0
+#define EMMC_ESP_PART 1
struct efi_fw_image fw_images[] = {
{
@@ -55,6 +70,13 @@ struct efi_capsule_update_info update_info = {
.images = fw_images,
};
+static struct acpm acpm = {
+ .mbox_base = (void __iomem *)EXYNOS850_MBOX_APM2AP_BASE,
+ .sram_base = (void __iomem *)(EXYNOS850_APM_SRAM_BASE +
+ EXYNOS850_APM_SHMEM_OFFSET),
+ .ipc_ch = EXYNOS850_IPC_AP_I3C,
+};
+
int dram_init(void)
{
return fdtdec_setup_mem_size_base();
@@ -92,19 +114,74 @@ static void setup_serial(void)
env_set("serial#", serial_str);
}
-int board_late_init(void)
+static void setup_ethaddr(void)
+{
+ u64 serial_num;
+ u32 mac_hi, mac_lo;
+ u8 mac_addr[6];
+
+ if (env_get("ethaddr"))
+ return;
+
+ serial_num = get_chip_id();
+ mac_lo = (u32)serial_num; /* OTP_CHIPID0 */
+ mac_hi = (u32)(serial_num >> 32UL); /* OTP_CHIPID1 */
+ mac_addr[0] = (mac_hi >> 8) & 0xff;
+ mac_addr[1] = mac_hi & 0xff;
+ mac_addr[2] = (mac_lo >> 24) & 0xff;
+ mac_addr[3] = (mac_lo >> 16) & 0xff;
+ mac_addr[4] = (mac_lo >> 8) & 0xff;
+ mac_addr[5] = mac_lo & 0xff;
+ mac_addr[0] &= ~0x1; /* make sure it's not a multicast address */
+ if (is_valid_ethaddr(mac_addr))
+ eth_env_set_enetaddr("ethaddr", mac_addr);
+}
+
+/*
+ * Call this in board_late_init() to avoid probing block devices before
+ * efi_init_early().
+ */
+void load_firmware(void)
{
+ const char *ifname;
+ ulong dev, part;
int err;
+ ifname = env_get("bootdev");
+ if (!ifname)
+ ifname = EMMC_IFNAME;
+ dev = env_get_ulong("bootdevnum", 10, EMMC_DEV_NUM);
+ part = env_get_ulong("bootdevpart", 10, EMMC_ESP_PART);
+
+ if (!strcmp(ifname, "usb")) {
+ printf("Starting USB (bootdev=usb)...\n");
+ err = usb_init();
+ if (err)
+ return;
+ }
+
+ printf("Loading LDFW firmware (from %s %ld)...\n", ifname, dev);
+ err = load_ldfw(ifname, dev, part, LDFW_NWD_ADDR);
+ if (err)
+ printf("ERROR: LDFW loading failed (%d)\n", err);
+}
+
+int board_late_init(void)
+{
setup_serial();
+ setup_ethaddr();
+ load_firmware();
- /*
- * Do this in board_late_init() to make sure MMC is not probed before
- * efi_init_early().
- */
- err = load_ldfw();
+ return 0;
+}
+
+int power_init_board(void)
+{
+ int err;
+
+ err = pmic_init(&acpm);
if (err)
- printf("ERROR: LDFW loading failed (%d)\n", err);
+ printf("ERROR: Failed to configure PMIC (%d)\n", err);
return 0;
}
diff --git a/board/samsung/e850-96/e850-96.env b/board/samsung/e850-96/e850-96.env
index aed7a71046d..992318b0ab2 100644
--- a/board/samsung/e850-96/e850-96.env
+++ b/board/samsung/e850-96/e850-96.env
@@ -5,7 +5,7 @@ fdt_addr_r=0x8c000000
scriptaddr=0x8c100000
pxefile_addr_r=0x8c200000
ramdisk_addr_r=0x8c300000
-fdtfile=CONFIG_DEFAULT_FDT_FILE
+fdtfile=exynos/exynos850-e850-96.dtb
dfu_alt_info=
rawemmc raw 0 0x747c000 mmcpart 1;
diff --git a/board/samsung/e850-96/fw.c b/board/samsung/e850-96/fw.c
index 8f64e759b43..64235c01a25 100644
--- a/board/samsung/e850-96/fw.c
+++ b/board/samsung/e850-96/fw.c
@@ -11,13 +11,9 @@
#include <linux/arm-smccc.h>
#include "fw.h"
-#define EMMC_IFACE "mmc"
-#define EMMC_DEV_NUM 0
#define LDFW_RAW_PART "ldfw"
-#define LDFW_FAT_PART "esp"
#define LDFW_FAT_PATH "/EFI/firmware/ldfw.bin"
-#define LDFW_NWD_ADDR 0x88000000
#define LDFW_MAGIC 0x10adab1e
#define SMC_CMD_LOAD_LDFW -0x500
#define SDM_HW_RESET_STATUS 0x1230
@@ -39,19 +35,23 @@ struct ldfw_header {
};
/* Load LDFW binary as a file from FAT partition */
-static int read_fw_from_fat(const char *part_name, const char *path, void *buf)
+static int read_fw_from_fat(const char *ifname, int dev, int part,
+ const char *path, void *buf)
{
- char dev_part_str[8];
+ struct blk_desc *blk_desc;
loff_t len_read;
int err;
- snprintf(dev_part_str, sizeof(dev_part_str), "%d#%s", EMMC_DEV_NUM,
- LDFW_FAT_PART);
+ blk_desc = blk_get_dev(ifname, dev);
+ if (!blk_desc) {
+ debug("%s: Can't get block device\n", __func__);
+ return -ENODEV;
+ }
- err = fs_set_blk_dev(EMMC_IFACE, dev_part_str, FS_TYPE_FAT);
+ err = fs_set_blk_dev_with_part(blk_desc, part);
if (err) {
- debug("%s: Can't set block device\n", __func__);
- return -ENODEV;
+ debug("%s: Can't set partition\n", __func__);
+ return -ENOENT;
}
err = fs_read(path, (ulong)buf, 0, 0, &len_read);
@@ -64,16 +64,17 @@ static int read_fw_from_fat(const char *part_name, const char *path, void *buf)
}
/* Load LDFW binary from raw partition on block device into RAM buffer */
-static int read_fw_from_raw(const char *part_name, void *buf)
+static int read_fw_from_raw(const char *ifname, int dev, const char *part_name,
+ void *buf)
{
struct blk_desc *blk_desc;
struct disk_partition part;
unsigned long cnt;
int part_num;
- blk_desc = blk_get_dev(EMMC_IFACE, EMMC_DEV_NUM);
+ blk_desc = blk_get_dev(ifname, dev);
if (!blk_desc) {
- debug("%s: Can't get eMMC device\n", __func__);
+ debug("%s: Can't get block device\n", __func__);
return -ENODEV;
}
@@ -92,9 +93,17 @@ static int read_fw_from_raw(const char *part_name, void *buf)
return 0;
}
-int load_ldfw(void)
+/**
+ * load_ldfw - Load the loadable firmware (LDFW)
+ * @ifname: Interface name of the block device to load the firmware from
+ * @dev: Device number
+ * @part: Partition number
+ * @addr: Temporary memory (Normal World) to use for loading the firmware
+ *
+ * Return: 0 on success or a negative value on error.
+ */
+int load_ldfw(const char *ifname, int dev, int part, phys_addr_t addr)
{
- const phys_addr_t addr = (phys_addr_t)LDFW_NWD_ADDR;
struct ldfw_header *hdr;
struct arm_smccc_res res;
void *buf = (void *)addr;
@@ -102,9 +111,9 @@ int load_ldfw(void)
int err, i;
/* First try to read LDFW from EFI partition, then from the raw one */
- err = read_fw_from_fat(LDFW_FAT_PART, LDFW_FAT_PATH, buf);
+ err = read_fw_from_fat(ifname, dev, part, LDFW_FAT_PATH, buf);
if (err) {
- err = read_fw_from_raw(LDFW_RAW_PART, buf);
+ err = read_fw_from_raw(ifname, dev, LDFW_RAW_PART, buf);
if (err)
return err;
}
diff --git a/board/samsung/e850-96/fw.h b/board/samsung/e850-96/fw.h
index 472664e4ed2..73d9615d4a9 100644
--- a/board/samsung/e850-96/fw.h
+++ b/board/samsung/e850-96/fw.h
@@ -7,6 +7,8 @@
#ifndef __E850_96_FW_H
#define __E850_96_FW_H
-int load_ldfw(void);
+#include <asm/types.h>
+
+int load_ldfw(const char *ifname, int dev, int part, phys_addr_t addr);
#endif /* __E850_96_FW_H */
diff --git a/board/samsung/e850-96/pmic.c b/board/samsung/e850-96/pmic.c
new file mode 100644
index 00000000000..037fd4844c5
--- /dev/null
+++ b/board/samsung/e850-96/pmic.c
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (c) 2025 Linaro Ltd.
+ * Author: Sam Protsenko <semen.protsenko@linaro.org>
+ *
+ * This file contains functions for S2MPU12 PMIC regulators configuration.
+ *
+ * Example of voltage calculation for LDO24 and LDO32:
+ * - V_min = 1800 mV
+ * - V_step = 25 mV
+ * - V_wanted = 3300 mV
+ * - register value: (V_wanted - V_min) / V_step = 60 = 0x3c
+ *
+ * NOTE: 0x3c value might mean different voltage for other LDOs.
+ */
+
+#include <linux/errno.h>
+#include <linux/kernel.h>
+#include "pmic.h"
+
+/* PMIC definitions */
+#define S2MPU12_CHANNEL 0 /* I3C bus number of PMIC */
+#define S2MPU12_PM_ADDR 0x1 /* I3C slave addr of PM part */
+
+/* PMIC I3C registers */
+#define S2MPU12_PM_LDO1_CTRL 0x2b
+#define S2MPU12_PM_LDO_CTRL(n) (S2MPU12_PM_LDO1_CTRL + (n) - 1)
+
+/* LDOx_CTRL values */
+#define S2MPU12_LDO_CTRL_OUT_MASK (0x3 << 6)
+#define S2MPU12_LDO_CTRL_OUT_ALWAYS_ON (0x3 << 6)
+
+struct pmic_ldo {
+ u8 num; /* LDO number */
+ u8 en; /* "enable" bits value in LDOx_CTRL register */
+ u8 out; /* "output voltage" bits value in LDOx_CTRL register */
+};
+
+/* List of LDOs to enable only */
+static u8 pmic_ldos_en[] = {
+ 2, /* 1.8V/450mA: multiple lines */
+ 11, /* 3.0V/150mA: AVDD33_USB20 */
+ 23, /* 2.85V/800mA: VDD_EMMC_2P85 */
+ 27, /* 3.0V/150mA: MIPI_SWITCH_3V3 */
+ 28, /* 1.8V/150mA: HDMI_CONV_1V8 */
+ 30, /* 1.8V/150mA: NPU_VDD18 */
+};
+
+/* List of LDOs to enable and set output voltage */
+static struct pmic_ldo pmic_ldos_en_out[] = {
+ {
+ .num = 24, /* 3.0V/800mA: VDD_LAN (LAN9514) */
+ .en = S2MPU12_LDO_CTRL_OUT_ALWAYS_ON,
+ .out = 0x3c, /* means 3.3V for LDO24 */
+ }, {
+ .num = 32, /* 3.3V/300mA: CAM_VDD (RPi camera module) */
+ .en = S2MPU12_LDO_CTRL_OUT_ALWAYS_ON,
+ .out = 0x3c, /* means 3.3V for LDO32 */
+ },
+};
+
+/* Enable specified LDO */
+static int pmic_ldo_set_en(struct acpm *acpm, u8 ldo)
+{
+ const u8 reg = S2MPU12_PM_LDO_CTRL(ldo);
+ u8 val;
+ int err;
+
+ err = acpm_i3c_read(acpm, S2MPU12_CHANNEL, S2MPU12_PM_ADDR, reg, &val);
+ if (err)
+ return err;
+
+ val &= ~S2MPU12_LDO_CTRL_OUT_MASK;
+ val |= S2MPU12_LDO_CTRL_OUT_ALWAYS_ON;
+
+ return acpm_i3c_write(acpm, S2MPU12_CHANNEL, S2MPU12_PM_ADDR, reg, val);
+}
+
+/* Enable specified LDO and set its voltage to 0xc0 value */
+static int pmic_ldo_set_en_out(struct acpm *acpm, struct pmic_ldo *ldo)
+{
+ const u8 reg = S2MPU12_PM_LDO_CTRL(ldo->num);
+ const u8 val = ldo->en | ldo->out;
+
+ return acpm_i3c_write(acpm, S2MPU12_CHANNEL, S2MPU12_PM_ADDR, reg, val);
+}
+
+#ifdef DEBUG
+static void pmic_trace_ldo(struct acpm *acpm, u8 ldo)
+{
+ const u8 reg = S2MPU12_PM_LDO_CTRL(ldo);
+ u8 val;
+ int err;
+
+ err = acpm_i3c_read(acpm, S2MPU12_CHANNEL, S2MPU12_PM_ADDR, reg, &val);
+ if (err)
+ printf(" S2MPU12_PM_LDO%u_CTRL: Read error!\n", ldo);
+ else
+ printf(" S2MPU12_PM_LDO%u_CTRL: 0x%x\n", ldo, val);
+}
+
+static void pmic_trace_ldos(struct acpm *acpm)
+{
+ size_t i;
+
+ printf("Tracing LDOs...\n");
+ for (i = 0; i < ARRAY_SIZE(pmic_ldos_en); ++i)
+ pmic_trace_ldo(acpm, pmic_ldos_en[i]);
+ for (i = 0; i < ARRAY_SIZE(pmic_ldos_en_out); ++i)
+ pmic_trace_ldo(acpm, pmic_ldos_en_out[i].num);
+}
+#endif
+
+/**
+ * pmic_init() - Enable power regulators in S2MPU12 PMIC.
+ * @acpm: Data for I3C communication with PMIC over ACPM protocol
+ *
+ * Enable LDOs needed for devices used in the bootloader and kernel.
+ *
+ * Return: 0 on success or non-zero code on error.
+ */
+int pmic_init(struct acpm *acpm)
+{
+ size_t i;
+ int err;
+
+ for (i = 0; i < ARRAY_SIZE(pmic_ldos_en); ++i) {
+ err = pmic_ldo_set_en(acpm, pmic_ldos_en[i]);
+ if (err)
+ return -EIO;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(pmic_ldos_en_out); ++i) {
+ err = pmic_ldo_set_en_out(acpm, &pmic_ldos_en_out[i]);
+ if (err)
+ return -EIO;
+ }
+
+#ifdef DEBUG
+ pmic_trace_ldos(acpm);
+#endif
+
+ return 0;
+}
diff --git a/board/samsung/e850-96/pmic.h b/board/samsung/e850-96/pmic.h
new file mode 100644
index 00000000000..46624c2ebd4
--- /dev/null
+++ b/board/samsung/e850-96/pmic.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright (c) 2025 Linaro Ltd.
+ * Sam Protsenko <semen.protsenko@linaro.org>
+ */
+
+#ifndef __E850_96_PMIC_H
+#define __E850_96_PMIC_H
+
+#include "acpm.h"
+
+int pmic_init(struct acpm *acpm);
+
+#endif /* __E850_96_PMIC_H */