This lesson has one job: choose the right memory building block and wrap it so a wrong assumption fails loudly. On NanoSoC the symptom of getting this wrong is wrong data with no error: a read latency assumed as one cycle when the macro delivers in two, or a byte-enable polarity flipped so writes land in the wrong lanes. The fabric cannot see any of it. Only the wrapper stands between the macro's truth and the interconnect's assumptions.
A memory compiler generates a macro from a size and option sheet: words, bits per word, ports, mux factor, and extras such as test ports and repair rows. A fixed macro is a ready-made block with no choices. NanoSoC uses two fixed 64 KB single-port macros, one per bank, each 32 bits wide with one access per clock at 96 MHz for a peak of 384.0 MB/s per bank. The compiler route matters when the size is awkward, when the aspect ratio must fit a floorplan slot, or when low-power options such as light sleep are needed; the fixed route wins when the size is standard and the schedule is short. Either way the macro never meets the fabric directly.
The wrapper owns five translations. It adapts the address and data widths to the fabric, applies the byte enables with the macro's polarity, registers the read path to the documented latency, multiplexes the test controller onto the macro ports during test, and routes repair and retention controls to their pins. The NanoSoC wrapper below shows the skeleton: byte-wise writes under the active-high enables, a two-cycle read pipeline, and the test-mode mux stated explicitly (repair and retention pins are left out of the snippet).
// NanoSoC teaching values: single-port SRAM wrapper skeleton.
// The two-cycle read latency, active-high enables and the test-mode mux
// are the documented contract; change the macro, keep the contract.
module nano_sram_wrap (
input logic clk,
input logic rst_n,
input logic ce,
input logic we,
input logic [13:0] addr,
input logic [31:0] wdata,
input logic [3:0] be,
output logic [31:0] rdata,
input logic test_en,
input logic [13:0] test_addr,
input logic [31:0] test_wdata,
input logic test_we,
output logic [31:0] test_rdata
);
logic [13:0] m_addr;
logic [31:0] m_wdata;
logic m_we;
logic [31:0] m_rdata;
logic [31:0] q1;
logic [3:0] m_be;
assign m_addr = test_en ? test_addr : addr;
assign m_wdata = test_en ? test_wdata : wdata;
assign m_we = test_en ? test_we : (ce & we);
assign m_be = test_en ? 4'hF : be; // the test controller writes whole words
// Illustrative macro model: real macro goes here.
logic [31:0] mem [0:16383];
always_ff @(posedge clk) begin
if (m_we) begin
if (m_be[0]) mem[m_addr][7:0] <= m_wdata[7:0];
if (m_be[1]) mem[m_addr][15:8] <= m_wdata[15:8];
if (m_be[2]) mem[m_addr][23:16] <= m_wdata[23:16];
if (m_be[3]) mem[m_addr][31:24] <= m_wdata[31:24];
end
q1 <= mem[m_addr];
end
assign m_rdata = q1;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) rdata <= 32'h0;
else rdata <= m_rdata;
end
assign test_rdata = m_rdata;
endmoduleBanking is the payoff. Each 64 KB bank sustains 361.4 MB/s on long bursts of 16, which is the 384.0 MB/s peak times the 16/17 burst efficiency. When the CPU stack in SRAM0 and a DMA stream in SRAM1 use different banks, each keeps the full rate (SRAM1 is secure-only, so this split needs the SBR's attribute cleared by secure software, or the roles swapped: DMA buffers in SRAM0 and the secure stack in SRAM1); when source and destination share one bank, the single port halves each stream to 180.7 MB/s. The script below prices three placements so the software rule earns its keep: keep DMA buffers in the other bank from the stack.
peak = 4 * 96
eff = 16 / 17
sustained = peak * eff
assert abs(sustained - 361.4) < 0.05, sustained
split = (sustained, sustained)
shared = (sustained / 2, sustained / 2)
assert abs(shared[0] - 180.7) < 0.05, shared
cases = {"split banks": split, "shared bank": shared}
for name, (a, b) in cases.items():
total = a + b
print(f"{name}: {a:.1f} + {b:.1f} = {total:.1f} MB/s")
assert abs(split[0] + split[1] - 722.8) < 0.1
print("PASS: split banks 361.4 each, shared bank 180.7 each")The wrapper document names the macro latency, enable polarity, byte-enable polarity and test ports, each with the macro datasheet line it came from.
No fabric signal reaches the macro except through the wrapper; the connectivity check proves it.
Both banks carry the same wrapper contract, so one verification environment covers both.
Simulate against the real macro model, never a behavioural stand-in with friendlier timing: drive single reads and writes, then back-to-back bursts, and compare every beat. Then flip each polarity assumption once in a scratch copy and confirm the wrapper check flags it. A mismatch that simulation misses usually means the model was not the real one.
The map window must be a power of two, so the window is 64 KB. The macro itself may be smaller: compilers produce word counts that are not powers of two, so a 40 KB macro is possible, otherwise a 64 KB one. Either way the wrapper decodes only the bottom 40 KB as usable, and the spare part of the window answers with an error and is never aliased onto live addresses.