Memory Master Patterns

In most applications, higher-level PyRogue interfaces such as RemoteVariable, Device, and RemoteCommand hide direct Master usage. Custom memory Master objects become useful when software needs to issue explicit transaction sequences at the bus level.

Typical cases include test utilities, low-level initialization flows, protocol adaptors, or software components that must perform a read-modify-write sequence without going through a higher-level PyRogue object model.

Python and C++ subclasses of Master can be mixed freely with downstream Slave implementations in either language. That makes it practical to prototype a transaction source in Python and move it into C++ later if needed.

Typical Master Flow

Most custom Master code follows the same basic pattern:

  1. Clear any previous error state.

  2. Request one or more transactions at a given address, size, and type.

  3. Wait for each transaction to complete.

  4. Check for error state before continuing.

The transaction type passed to reqTransaction or _reqTransaction is one of the key choices in custom Master code. Read and Write are the most common, but Post is also available as a distinct posted-write transaction type. In the code, that means the downstream path can see Post instead of Write and apply different behavior. Some implementations handle Post exactly like Write. Others use it for write paths that should not wait for a normal downstream response.

Python Example

The example below issues a read, increments the 32-bit value at the target address, and then writes the updated value back.

import rogue.interfaces.memory as rim

class MyMemMaster(rim.Master):

    # Init method must call the parent class init
    def __init__(self):
        super().__init__()

    # Increment the 32-bit value stored at the target address
    def inc_at_address(self, address: int, delta: int) -> bool:

        # Local buffer for the read transaction
        data = bytearray(4)

        # Clear any existing error state
        self._clearError()

        # Start the read transaction and wait for completion
        tid = self._reqTransaction(address, data, 4, 0, rim.Read)
        self._waitTransaction(tid)

        if self._getError() != "":
            return False

        # Modify the returned data locally
        value = int.from_bytes(data, 'little', signed=False)
        value += delta
        data = value.to_bytes(4, 'little', signed=False)

        # Write the updated value back to the same address
        tid = self._reqTransaction(address, data, 4, 0, rim.Write)
        self._waitTransaction(tid)

        return self._getError() == ""

This example is intentionally simple. It shows the mechanics of issuing and waiting on transactions, but it does not make the read-modify-write sequence atomic with respect to other actors on the bus.

C++ Example

#include "rogue/interfaces/memory/Constants.h"
#include "rogue/interfaces/memory/Master.h"

namespace rim = rogue::interfaces::memory;

class MyMemMaster : public rim::Master {
public:
   using Ptr = std::shared_ptr<MyMemMaster>;
   static Ptr create() { return std::make_shared<MyMemMaster>(); }

   bool incAtAddress(uint64_t address, uint32_t delta) {
      uint32_t value = 0;

      // Clear any existing error state
      clearError();

      // Read the current 32-bit value
      uint32_t tid = reqTransaction(address, 4, &value, rim::Read);
      waitTransaction(tid);
      if (getError() != "") return false;

      // Update it locally
      value += delta;

      // Write the updated value back
      tid = reqTransaction(address, 4, &value, rim::Write);
      waitTransaction(tid);
      return getError() == "";
   }
};

Posted Write Example

When a target register or command path should be driven using a posted write, the only code change may be the transaction type itself.

import rogue.interfaces.memory as rim

class MyMemMaster(rim.Master):
    def __init__(self):
        super().__init__()

    def trigger(self, address: int) -> bool:
        data = bytearray([1, 0, 0, 0])

        self._clearError()
        tid = self._reqTransaction(address, data, 4, 0, rim.Post)
        self._waitTransaction(tid)

        return self._getError() == ""

At the memory-interface layer, Post is a first-class transaction type. A downstream Slave or Hub may handle it the same way as Write or may apply different policy depending on the protocol or hardware design.

Design Notes

Read-modify-write sequences are not automatically atomic across external actors. If other software or hardware can touch the same address in the middle of the sequence, higher-level locking or coordination is required. In many PyRogue applications that coordination is handled at the device-tree level rather than inside the raw Master implementation.

Logging

The base Rogue memory Master uses Rogue C++ logging with the logger name pyrogue.memory.Master.

Enable that logger before constructing the object if you want transaction and error-path visibility from the base implementation:

import rogue

rogue.Logging.setFilter('pyrogue.memory.Master', rogue.Logging.Debug)

Master does not expose a separate runtime debug helper. For more detailed custom logging, add subclass-specific logging around transaction sequencing and error handling.

What To Explore Next

API Reference