Skip to content

drivers/mbox: Add mailbox framework and PL320 driver#18859

Draft
Jiaqi-YP7 wants to merge 1 commit intoapache:masterfrom
Jiaqi-YP7:feat/mbox-framework-pl320-clean
Draft

drivers/mbox: Add mailbox framework and PL320 driver#18859
Jiaqi-YP7 wants to merge 1 commit intoapache:masterfrom
Jiaqi-YP7:feat/mbox-framework-pl320-clean

Conversation

@Jiaqi-YP7
Copy link
Copy Markdown
Contributor

drivers/mbox: Add mailbox framework and PL320 driver

Summary

This change adds a generic mailbox (MBOX) framework under drivers/mbox
and an initial ARM PL320 IPCM lower-half driver.

The motivation is to provide a small, reusable kernel-level abstraction for
SoC mailbox controllers. Many multicore SoCs provide mailbox/IPCM hardware for
short inter-processor messages, event notifications, and resource coordination,
but each controller usually has slightly different register layout and
completion semantics. Without a common framework, each user has to duplicate
channel lookup, send/receive state handling, timeout handling, callbacks, and
controller-specific glue.

This patch introduces an upper-half/lower-half split:

  • The generic upper half defines struct mbox_dev_s, struct mbox_chan_s,
    struct mbox_sender_s, struct mbox_receiver_s, and the public mailbox
    APIs in include/nuttx/mbox/mbox.h.
  • Lower-half drivers implement struct mbox_ops_s to handle controller
    details such as channel setup, message send/receive, ACK generation, TX
    completion, and hardware ready/available checks.
  • The PL320 lower half implements the first mailbox controller using this
    interface and exposes pl320_initialize() through
    include/nuttx/mbox/pl320.h.

The generic framework supports the following TX/RX flow:

  • TX clients send data through a struct mbox_sender_s.
  • Blocking sends wait until an ACK/completion is reported or until the
    configured timeout expires.
  • Non-blocking sends can enqueue data when a TX channel is already active.
  • Lower-half drivers call mbox_chan_tx_done() when an ACK/completion is
    observed, either from an interrupt handler or from polling logic.
  • RX lower halves call mbox_chan_rx_data() when data is available. The
    generic layer receives the message, sends an ACK when supported, and invokes
    the registered RX callback.
  • Optional CONFIG_MBOX_TRACE statistics are provided for sent, received,
    acknowledged, buffered, and timed-out messages.

For PL320, the driver maps each configured mailbox to an mbox_chan_s,
programs the source/destination/mask registers during setup, transfers payloads
through the PL320 data registers, and routes IRQ events to the generic TX
completion or RX receive paths.

Design

The design separates common mailbox semantics from hardware-specific control.
The framework only assumes that a mailbox channel can report whether TX is
ready, whether RX data is available, and whether a TX message has completed or
been acknowledged. The exact hardware mechanism remains in the lower-half
driver.

The generic channel object stores common runtime state:

  • Channel direction (MBOX_TX or MBOX_RX)
  • Associated lower-half device
  • RX callback and callback argument
  • TX circular buffer for queued non-blocking messages
  • TX semaphore for blocking waiters
  • Watchdog timer for TX timeout detection
  • TX state and result
  • Optional trace statistics

This keeps board/controller drivers small. A lower-half driver only needs to
provide the hardware operations:

  • getchan() to resolve a platform-specific channel argument
  • setup() / shutdown() for channel ownership and configuration
  • send() / recv() for payload transfer
  • acknowledge() for RX-side ACK generation when the hardware supports it
  • txfinish() to clear completion state and prepare for the next transfer
  • setcallback() for RX callback registration
  • rxavailable(), txready(), and txacked() for status checks

The PL320 implementation follows this model directly. Board code supplies a
struct pl320_config_s with the local IPCM interrupt index, IRQ number, and an
array of configured PL320 channels. Each struct pl320_chan_s describes the
mailbox index, source core, destination core, whether the channel should be
configured by this side, and the TX buffer used by the generic framework.

Impact

This is additive and gated by Kconfig:

  • CONFIG_MBOX enables the generic mailbox framework.
  • CONFIG_MBOX_PL320 enables the ARM PL320 lower-half driver.
  • CONFIG_MBOX_TRACE optionally enables per-channel statistics.

There is no intended behavior change for existing drivers or boards unless
they enable and instantiate the new mailbox driver.

Build impact is limited to the new drivers/mbox directory and the new
drivers/Kconfig / drivers/Makefile integration. Existing IPCC, RPMsg, and
other driver users are not changed by this patch.

Hardware impact is limited to boards that explicitly initialize a PL320
controller and provide a channel table. The PL320 lower half supports up to 32
mailboxes and transfers up to 28 bytes per PL320 message, matching the seven
32-bit PL320 data registers.

Compatibility impact should be low because the new APIs are introduced under a
new include/nuttx/mbox namespace. No existing public mailbox API is replaced.

Basic Usage

Enable the framework and lower-half driver in Kconfig:

CONFIG_MBOX=y
CONFIG_MBOX_PL320=y

Optionally enable trace statistics:

CONFIG_MBOX_TRACE=y

Board or SoC initialization code should define PL320 channels and initialize
the controller. A TX channel needs a TX buffer for the framework queue:

#include <nuttx/mbox/mbox.h>
#include <nuttx/mbox/pl320.h>

static uint8_t g_pl320_txbuf[128];

static struct pl320_chan_s g_pl320_chans[] =
{
  {
    .mbox    = 0,
    .src     = 0,
    .dst     = 1,
    .ctrl    = true,
    .txbuf   = g_pl320_txbuf,
    .bufsize = sizeof(g_pl320_txbuf),
  },
};

static const struct pl320_config_s g_pl320_config =
{
  .chans     = g_pl320_chans,
  .num_chans = nitems(g_pl320_chans),
  .ipcmint   = 0,
  .irq_num   = PL320_IRQ,
};

FAR struct mbox_dev_s *mbox;

mbox = pl320_initialize(&g_pl320_config, PL320_BASE);

After initialization, client code can resolve a channel and send a message:

FAR struct mbox_chan_s *chan;
struct mbox_sender_s sender;
uint32_t msg = 0x12345678;
int ret;

chan = mbox_get_chan(mbox, (FAR void *)(uintptr_t)0);
if (chan == NULL)
  {
    return -ENODEV;
  }

sender.chan = chan;
sender.priv = NULL;

ret = mbox_send(&sender, &msg, sizeof(msg));

For a bounded wait, use mbox_ticksend():

ret = mbox_ticksend(&sender, &msg, sizeof(msg), MSEC2TICK(100));

For a non-blocking send, pass a zero timeout. If the channel is active, the
message can be buffered for later transmission. If the hardware is idle but not
ready, the API returns -EAGAIN.

ret = mbox_ticksend(&sender, &msg, sizeof(msg), 0);

RX clients register a callback on an RX channel:

static int my_mbox_rxcallback(int ret, FAR struct mbox_chan_s *chan,
                              FAR void *arg, FAR void *data, size_t size)
{
  if (ret < 0)
    {
      return ret;
    }

  /* Consume data[0..size - 1]. */

  return OK;
}

struct mbox_receiver_s receiver;

receiver.chan = chan;
receiver.priv = NULL;

ret = mbox_register_rxcallback(&receiver, my_mbox_rxcallback, NULL);

The lower-half interrupt handler or polling path should notify the framework:

if (MBOX_TX_ACKED(chan))
  {
    mbox_chan_tx_done(chan);
  }

if (MBOX_RX_AVAILABLE(chan))
  {
    mbox_chan_rx_data(chan);
  }

When a channel is no longer used, platform code can release framework-owned
resources with:

mbox_chan_deinit(chan);

Testing

Test background:

This was tested on a downstream private SoC platform. The SoC has multiple
processor cores that communicate through mailbox hardware based on ARM PL320.
For this validation, one RISC-V core and one Arm R52 core were selected to
verify PL320-based mailbox communication through the new generic MBOX framework
and PL320 lower-half driver.

A downstream ipcmtest test application was developed for this validation.
The test application uses the public MBOX APIs added by this patch and covers:

  • PL320 channel initialization on both cores
  • RX callback registration on both cores
  • R52-to-RISC-V single blocking send
  • RISC-V-to-R52 single blocking send
  • Sequential stress sends
  • Non-blocking send with timeout == 0
  • Timed send with a 5-second timeout

Configuration and board initialization evidence:

The downstream board configuration enabled the generic MBOX framework, the
PL320 lower-half driver, and the ipcmtest test app:

CONFIG_MBOX=y
CONFIG_MBOX_PL320=y
CONFIG_IPCMTEST=y

Our board/vendor layer provides the PL320 channel table used by the test.
For the tested RISC-V <-> R52 pair, PL320 channel 16 is used for
RISC-V-to-R52 messages, and PL320 channel 17 is used for R52-to-RISC-V
messages:

#define TEST_PL320_TIMEOUT      5
#define TEST_PL320_TX_BUFSIZE   (PL320_MAX_DATA_BUF_SIZE * PL320_MSG_SIZE)

struct test_pl320_txbuf_s
{
  uint8_t buf[PL320_MAX_DATA_BUF_SIZE][PL320_MSG_SIZE]
    __attribute__((aligned(64)));
};

static struct test_pl320_txbuf_s g_pl320_txbuf[2];

static struct pl320_chan_s g_pl320_chans[] =
{
  {
    .base =
    {
      .id      = 16,
      .timeout = TEST_PL320_TIMEOUT,
    },
    .mbox    = 16,
    .src     = RISCV_PL320_INT,
    .dst     = R52_PL320_INT,
    .ctrl    = true,
    .txbuf   = (uint8_t *)&g_pl320_txbuf[0],
    .bufsize = TEST_PL320_TX_BUFSIZE,
  },
  {
    .base =
    {
      .id      = 17,
      .timeout = TEST_PL320_TIMEOUT,
    },
    .mbox    = 17,
    .src     = R52_PL320_INT,
    .dst     = RISCV_PL320_INT,
    .ctrl    = true,
    .txbuf   = (uint8_t *)&g_pl320_txbuf[1],
    .bufsize = TEST_PL320_TX_BUFSIZE,
  },
};

The board/vendor layer also provides the struct pl320_config_s passed to
pl320_initialize(). The local ipcmint and IRQ number are selected for the
core that is currently booting:

static const struct pl320_config_s g_pl320_config =
{
  .chans     = g_pl320_chans,
  .num_chans = ARRAY_SIZE(g_pl320_chans),
  .ipcmint   = LOCAL_PL320_INT,
  .irq_num   = LOCAL_PL320_IRQ,
};

PL320 is initialized during board mailbox initialization. The returned
struct mbox_dev_s is stored by board code and later used by ipcmtest:

static FAR struct mbox_dev_s *g_pl320_dev;

int board_mbox_init(void)
{
  if (up_cpu_index() == 0)
    {
      g_pl320_dev = pl320_initialize(&g_pl320_config, PL320_REG_BASE);
      if (g_pl320_dev == NULL)
        {
          return -ENODEV;
        }
    }

  return OK;
}

FAR struct mbox_dev_s *board_get_pl320_dev(void)
{
  return g_pl320_dev;
}

Test application initialization:

Both cores initialize the PL320 mailbox device, resolve the configured channel,
and register an RX callback with the generic MBOX API.

RISC-V side RX initialization for messages sent from the R52 core:

FAR struct mbox_dev_s *pl320_dev;
FAR struct mbox_chan_s *rx_chan;
static struct mbox_receiver_s receiver;
int ret;

pl320_dev = board_get_pl320_dev();
rx_chan = mbox_get_chan(pl320_dev, (FAR void *)(uintptr_t)17);

receiver.chan = rx_chan;
receiver.priv = NULL;

ret = mbox_register_rxcallback(&receiver, mbox_callback, NULL);

R52 side RX initialization for messages sent from the RISC-V core:

FAR struct mbox_dev_s *pl320_dev;
FAR struct mbox_chan_s *rx_chan;
static struct mbox_receiver_s receiver;
int ret;

pl320_dev = board_get_pl320_dev();
rx_chan = mbox_get_chan(pl320_dev, (FAR void *)(uintptr_t)16);

receiver.chan = rx_chan;
receiver.priv = NULL;

ret = mbox_register_rxcallback(&receiver, mbox_callback, NULL);

Callback:

The RX callback checks the lower-half result, verifies that a payload was
provided, increments the RX counter, and prints the received PL320 payload.

static int mbox_callback(int ret, FAR struct mbox_chan_s *chan,
                         FAR void *arg, FAR void *data, size_t size)
{
  FAR uint8_t *msg;

  if (ret != 0 || data == NULL)
    {
      g_rx_errors++;
      return -EIO;
    }

  g_rx_count++;
  msg = (FAR uint8_t *)data;

  printf("received msg on chan[%d] count=%d size=%zu\n",
         chan->id, g_rx_count, size);
  printf("%02x %02x %02x %02x ...\n",
         msg[0], msg[1], msg[2], msg[3]);

  return OK;
}

Send-flow:

R52-to-RISC-V send path. The R52 core resolves PL320 TX channel 17 and sends
one 28-byte PL320 payload with mbox_send().

FAR struct mbox_chan_s *tx_chan;
struct mbox_sender_s sender;
uint32_t data[7];
int ret;

tx_chan = mbox_get_chan(pl320_dev, (FAR void *)(uintptr_t)17);

sender.chan = tx_chan;
sender.priv = NULL;

memset(data, 0xff, sizeof(data));
ret = mbox_send(&sender, data, sizeof(data));

RISC-V-to-R52 send path. The RISC-V core resolves PL320 TX channel 16 and sends
one 28-byte PL320 payload with mbox_send().

FAR struct mbox_chan_s *tx_chan;
struct mbox_sender_s sender;
uint32_t data[7];
int ret;

tx_chan = mbox_get_chan(pl320_dev, (FAR void *)(uintptr_t)16);

sender.chan = tx_chan;
sender.priv = NULL;

memset(data, 0xff, sizeof(data));
ret = mbox_send(&sender, data, sizeof(data));

Non-blocking send path:

ret = mbox_ticksend(&sender, data, sizeof(data), 0);

Timed send path:

struct timespec ts = {.tv_sec = 5, .tv_nsec = 0};

ret = mbox_ticksend(&sender, data, sizeof(data), clock_time2ticks(&ts));

Test results:

  1. R52-to-RISC-V one-way PL320 send

    R52 shell output:

    [r52-0]=>ipcmtest xfer
    [r52-0]=>vs_ipcm tx chan[13] (R52-A->R52-B) 
    mbox_send(R52-A->R52-B) ret=[0] [PASS]

    RISC-V shell output:

    2025-07-10-14_40_41_146746  0 F received msg on chan[17] (count=1):
    2025-07-10-14_40_41_146752  1 F   [00:07]: ff ff ff ff ff ff ff ff
    2025-07-10-14_40_41_146757  1 F   [08:15]: ff ff ff ff ff ff ff ff
    2025-07-10-14_40_41_146762  1 F   [16:23]: ff ff ff ff ff ff ff ff
    2025-07-10-14_40_41_146767  1 F   [24:27]: ff ff ff ff
  2. RISC-V-to-R52 one-way PL320 send

    RISC-V shell output:

    [riscv-a]=>ipcmtest xfer
    [riscv-a]=>pl320 riscv->r52 tx chan[16]
    mbox_send(riscv->r52 pl320 chan[16]) ret=[0] [PASS]

    R52 shell output:

     2025-07-10-14_47_46_286970 0  1 F   [00:07]: ff ff ff ff ff ff ff ff
     2025-07-10-14_47_46_286992 0  1 F   [08:15]: ff ff ff ff ff ff ff ff
     2025-07-10-14_47_46_287014 0  1 F   [16:23]: ff ff ff ff ff ff ff ff
     2025-07-10-14_47_46_287035 0  1 F   [24:27]: ff ff ff ff
    
  3. Non-blocking send

    R52 shell output:

     [r52-0]=>ipcmtest nb_send
     [r52-0]=>
     === Non-blocking Send Test (timeout=0) ===
     [PASS] nonblocking_send
       ret=0 (sent immediately)
     === Non-blocking Send: pass=1 fail=0 ===
     
     2025-07-10-14_51_03_536758 0  8 F received msg on chan[12] (count=3):
     2025-07-10-14_51_03_536782 0  8 F   [00:07]: 01 01 00 02 b0 f8 01 00
     2025-07-10-14_51_03_536804 0  8 F   [08:15]: ff ff ff ff ff ff ff ff
     2025-07-10-14_51_03_536826 0  8 F   [16:23]: ff ff ff ff ff ff ff ff
     2025-07-10-14_51_03_536847 0  8 F   [24:27]: ff ff ff ff
    

    RISC-V shell output:

    i didn't print here, the result will show in r52 shell
    
  4. Trace

    R52 shell output:

     [r52-0]=>ipcmtest stats
     [r52-0]=>ipcmtest: enable CONFIG_MBOX_TRACE for statistics
     rx_count=3 rx_errors=0
    
  5. Timeout
    I made riscv crash to make no-ack scene, r52 send by mbox_ticksend();
    R52 shell output:

     [r52-0]=>ipcmtest timed_pl320
     
     === PL320 Timed Send Test (5-second timeout) ===
     [r52-0]=>timed_send(r52->riscv pl320 chan[17]) ret=-110  [INFO] timed out (riscv silent?)
    

@github-actions github-actions Bot added the Size: XL The size of the change in this PR is very large. Consider breaking down the PR into smaller pieces. label May 9, 2026
@Jiaqi-YP7 Jiaqi-YP7 force-pushed the feat/mbox-framework-pl320-clean branch from 8c3ca1a to 4a92248 Compare May 9, 2026 13:39
Copy link
Copy Markdown
Contributor

@acassis acassis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jiaqi-YP7 nice work! Please provide some basic Documentation about this new feature

@Jiaqi-YP7
Copy link
Copy Markdown
Contributor Author

@acassis Thanks for the review and kind words! I’ll add basic documentation as soon as possible.

@lupyuen
Copy link
Copy Markdown
Member

lupyuen commented May 10, 2026

Hi: Could you Rebase your PR with the Master Branch? We fixed the Config Error for esp32p4-function-ev-board. Thanks :-)

Add a generic mailbox framework under drivers/mbox with an upper-half API for channel lookup, TX/RX callback handling, blocking and non-blocking sends, timeout handling, queued TX messages, and optional trace statistics.

Add the first lower-half implementation for the ARM PL320 IPCM. The driver programs channel source, destination, and mask registers, transfers payloads through the PL320 data registers, handles ACK/completion flow, and exposes pl320_initialize() for board code.

This provides a reusable mailbox abstraction for SoCs that need kernel-level inter-processor communication without duplicating controller-independent TX/RX state management in each lower-half driver.

Signed-off-by: yaojiaqi <yaojiaqi@lixiang.com>
@Jiaqi-YP7 Jiaqi-YP7 force-pushed the feat/mbox-framework-pl320-clean branch from 4a92248 to 3346831 Compare May 10, 2026 06:00
@Jiaqi-YP7
Copy link
Copy Markdown
Contributor Author

Hi: Could you Rebase your PR with the Master Branch? We fixed the Config Error for esp32p4-function-ev-board. Thanks :-)

Thanks for the heads-up and the fix! :-) I've rebased my PR with the master branch.

Comment thread include/nuttx/mbox/mbox.h
#define MBOX_REGISTER_CALLBACK(d,c,cb,a) \
((d)->ops->registercallback(d,c,cb,a))
#define MBOX_REGISTER_CALLBACK(c, cb, a) \
((c)->dev->ops->setcallback(c, cb, a))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use the different name for macro and field

Comment thread drivers/mbox/mbox.c
static int mbox_enqueue_data(FAR struct mbox_chan_s *chan,
FAR const void *data, size_t size)
{
if (size > MBOX_MAX_MSG_SIZE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why limit msg size

Comment thread drivers/mbox/mbox.c
}

circbuf_write(&chan->txbuf, (FAR void *)&size, sizeof(size_t));
circbuf_write(&chan->txbuf, (FAR void *)data, size);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove ALL cast to circbuf_write

Comment thread drivers/mbox/mbox.c
return -ENODATA;
}

circbuf_read(&chan->txbuf, (FAR void *)size, sizeof(size_t));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove ALL cast to circbuf_read

Comment thread drivers/mbox/mbox.c
{
int ret;

ret = chan->dev->ops->send(chan, data, size);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call macro instead

Comment thread drivers/mbox/mbox.c

if (chan->dir == MBOX_TX)
{
if (WDOG_ISACTIVE(&chan->timer))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the check

Comment thread drivers/mbox/mbox.c
{
if (WDOG_ISACTIVE(&chan->timer))
{
wd_cancel(&chan->timer);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need wd_cancel_sync

Comment thread drivers/mbox/pl320.c
#define IPCMXMCLEAR(m) (((m) * 0x40) + 0x018)
#define IPCMXMSTATUS(m) (((m) * 0x40) + 0x01C)
#define IPCMXSEND(m) (((m) * 0x40) + 0x020)
#define IPCMXDR(m, dr) (((m) * 0x40) + ((dr) * 4) + 0x024)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#define IPCMXDR(m, dr) (((m) * 0x40) + ((dr) * 4) + 0x024)
#define IPCMXDR(m, dr) (((m) * 0x40) + 0x024+ ((dr) * 4))

Comment thread drivers/mbox/pl320.c
}

priv->base.ops = &g_pl320mbox_ops;
priv->base.chans = (FAR struct mbox_chan_s *)config->chans;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why set to base.chans, you can't safely access each element through base.chans since pl320_chan has the different size than mbox_chan

Comment thread include/nuttx/mbox/mbox.h
* non-blocking receiving methods. It is designed based on the state transfer
* of lower-half ops as follows.
*
* -----------------------------------------------------------------
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new model is even complex than linux, why not simplify it?
https://github.com/torvalds/linux/blob/master/include/linux/mailbox_controller.h#L54-L60

@Jiaqi-YP7
Copy link
Copy Markdown
Contributor Author

Thanks a lot @xiaoxiang781216 for the detailed review.

This PR is a relatively large piece of work, and several comments involve the overall framework design, so I need some time to carefully evaluate each point. I’ll first handle the straightforward cleanup items, then take a closer look at the design and simplification suggestions before updating the PR.

@Jiaqi-YP7 Jiaqi-YP7 marked this pull request as draft May 10, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Size: XL The size of the change in this PR is very large. Consider breaking down the PR into smaller pieces.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants