Skip to content
VLSI Mentor

SPI · Module 10

Command Encoding and Register Access

How a command byte packs direction, auto-increment and a register address, why the polarity of the read/write bit differs between parts and silently turns reads into destructive writes, and the codec that encodes and decodes any convention.

Pass three of Chapter 10.1's route reaches the command table. For a register-access device that table is usually one page, and almost all of it is one byte.

A sensor's datasheet says register 0x2D controls power mode, and that the first byte carries the register address with the MSB indicating the operation. You send 0x2D to read it, and the device powers down instead. What happened?

The MSB indicated the operation, and you did not set it — so the device read your byte as a write of zero to register 0x2D. Nothing failed, nothing reported an error, and the part did exactly what it was told.

1. What a Command Byte Carries

For a device with a small register map, the entire address space fits alongside the control bits in the first byte:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    7     6     5     4     3     2     1     0
  ┌─────┬─────┬─────────────────────────────────┐
  │ R/W │ INC │        register address         │
  └─────┴─────┴─────────────────────────────────┘

Three fields, and every one of them varies between parts:

  • The R/W bit's position. Usually bit 7. Occasionally bit 0, on parts that shift LSB-first.
  • The R/W bit's polarity. Set for read on many sensors; set for write on many others. There is no convention, only a per-part fact.
  • The increment bit. Present on some parts, absent on others, and where present it may mean "auto-increment the address" or "this is a multi-byte transfer" — which are not quite the same thing.
  • The address field width. Six bits leaves room for two control bits; seven bits leaves room for one; eight bits leaves room for none and forces the address into its own phase.

Devices with larger maps — flash, most memory — abandon this packing entirely and use an opcode byte followed by a separate address phase. That is Chapter 10.5's subject. This chapter is about the packed form, which covers most sensors, ADCs, clock generators and radio transceivers.

2. The Polarity Trap

The failure in the question above is worth stating plainly, because it is the single most expensive misreading in this whole module.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Convention A:  bit 7 SET   → READ     (many accelerometers, gyros)
   Convention B:  bit 7 SET   → WRITE    (many ADCs, DACs, radios)

A driver written for one and pointed at the other does not fail. It performs the opposite operation, and the consequences are asymmetric:

  • An intended read becomes a write, storing whatever the driver happened to send in its second byte — usually a don't-care value — into the register it meant to inspect. On a configuration register that reconfigures the part. On a power-mode register it powers it down.
  • An intended write becomes a read, which is harmless in itself, but means the configuration never took effect and the part runs with defaults. The symptom appears much later, as a part that "ignores its settings".

Both are silent. Neither produces an error. And the read-becomes-write case is actively destructive, which is why the polarity is the first thing to confirm in the command table and why the codec in §5 makes it a parameter rather than an assumption.

3. Reaching the Register Map

With the command byte settled, a register access is two or three elements.

A sequence diagram of two register accesses. In the first, the master asserts chip select, sends one command byte encoding a read of a register, receives one byte, and releases chip select. In the second, the master sends a command byte with the increment bit set and receives several consecutive registers before releasing chip select.Register read, single and multi-bytemasterdeviceCS assertscmd: read, no inc,reg 0x2Dvalue of 0x2DCS releasesCS asserts againcmd: read, INC set,reg 0x320x32, then 0x33,then 0x34CS releases —pointer resets
Figure 1 — a single-register read and a multi-byte read on a packed-command device. The command byte carries direction, the increment flag and the address; the device then returns bytes, advancing its internal pointer only if the increment bit was set.

Three properties of that picture matter more than the byte counts.

The address is sent once. Everything after the command byte is data, and the device's internal pointer supplies the address — which is why the increment bit exists and why reading six consecutive registers costs one command byte rather than six.

Chip select frames the access. Releasing CS ends the burst and, on most parts, resets the internal pointer. This is the continuous-transfer behaviour of Chapter 7.1 applied to a register map, and it means a driver that releases CS between bytes gets six single accesses rather than one burst.

The increment bit is not always what it appears. On some parts it enables auto-increment; on others it simply means "more than one byte follows" and the increment is implied. On a few it must be set even for a single-byte access. The command table says which, and the difference shows up as a burst that reads the same register repeatedly.

4. Reading the Table Correctly

Four questions, asked of the command table, settle the encoding completely:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   1. Which bit is R/W, and which LEVEL means read?
   2. Is there an increment bit, where, and what does it mean?
   3. How many bits remain for the address?
   4. What happens to an address that does not fit?

The fourth is not rhetorical. A device packing six address bits has 64 reachable registers; a driver that sends register 0x40 has its address silently truncated to 0x00 by the device, which then acts on the wrong register. The device cannot report this — the bits it would need to notice simply are not there. So the check belongs in the controller, which is exactly what enc_overflow does in §5.

5. Building the Command Codec — Three HDLs

The circuit

Circuit. A parameterised bit-field permutation, in both directions.

State. None. Encoding a request into a command byte is a rearrangement of bits, and a register in front of a permutation buys nothing but a cycle of latency. This is the one block in the module with no clock at all, and that is the right answer rather than an omission.

Datapath. The address is masked to the packed width; the read/write bit is set to the level the datasheet says means read, or its complement; the increment bit is placed if the part has one. Decoding reverses each step.

Control. None required.

Clock and reset. Neither. A purely combinational block in a clocked design is unusual enough to be worth justifying: the surrounding controller registers the command byte when it loads its shift register, and adding a register here would simply add a cycle between request and issue.

Enables. enc_overflow flags an address wider than the packed field — the case §4 ends on, which the device physically cannot detect.

Timing. Combinational from the request. The consumer is expected to register it.

Synthesis. A mask, two OR terms and a comparison. Tens of gates.

Limitations. It handles the packed form only. A device using a separate address phase needs Chapter 10.5's planner instead, and a device whose read and write opcodes are unrelated values — as flash opcodes are — needs a table rather than a bit.

Bit selects. Every field is extracted with a shift and a mask rather than a parameterised part-select. The two are equivalent in intent, but a variable part-select inside a combinational block is poorly supported by several tools, and the mask form makes each field's width explicit at the point of use. This is the same choice Chapter 9.3 made for its boundary arithmetic, for the same reason.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cmd_codec.sv — the convention as parameters, not assumptions
// spi_cmd_codec.sv
//
// Chapter 10.4 -- the command table, turned into logic.
//
// A register-access device packs three things into its first byte: a
// read/write bit, sometimes an auto-increment bit, and the register
// address. Which bit is which, and which POLARITY means read, are
// datasheet facts that differ between parts -- and a driver that assumes
// the common convention works perfectly on the devices that share it and
// fails completely on the ones that do not.
//
// This block makes the convention a set of parameters instead of an
// assumption, and implements both directions: encode builds the command
// byte, decode recovers the request from it. Both are combinational --
// encoding is a permutation of bits, and putting a register in front of a
// permutation buys nothing but a cycle of latency.
//
// The default parameters describe a widely used accelerometer convention:
// bit 7 set means READ, bit 6 set means multi-byte, and the low six bits
// carry the register address. Reading register 0x2D is then 0xAD, and
// reading it with auto-increment is 0xED.
//
// BIT SELECTS. Every field is extracted with a shift and a mask rather
// than a parameterised part-select. The two are equivalent in intent, but
// a variable part-select inside a combinational block is poorly supported
// by several tools, and the mask form makes the width of each field
// explicit at the point of use.

module spi_cmd_codec #(
    parameter int RW_POS         = 7,  // bit carrying read/write
    parameter int RW_READ_LEVEL  = 1,  // the value of that bit meaning READ
    parameter int HAS_INC        = 1,  // does the part have a multi-byte bit
    parameter int INC_POS        = 6,  // and where
    parameter int ADDR_W_PACKED  = 6   // address bits inside the command byte
) (
    // Encode: a request in, a command byte out.
    input  logic       enc_read,
    input  logic       enc_inc,
    input  logic [7:0] enc_addr,
    output logic [7:0] enc_cmd,
    output logic       enc_overflow,   // address does not fit the packed field

    // Decode: a captured command byte in, the request it represents out.
    input  logic [7:0] dec_cmd,
    output logic       dec_read,
    output logic       dec_inc,
    output logic [7:0] dec_addr
);

    localparam logic [7:0] ADDR_MASK = 8'((1 << ADDR_W_PACKED) - 1);

    logic rw_level;
    logic inc_level;

    always_comb begin
        // The read/write bit takes the level the datasheet says means read,
        // or its complement for a write. Parameterising the LEVEL rather
        // than hard-coding "1 means read" is the whole point: the opposite
        // convention is common, and a driver written for one silently
        // performs writes when pointed at a device using the other.
        rw_level  = enc_read ? RW_READ_LEVEL[0] : ~RW_READ_LEVEL[0];
        inc_level = (HAS_INC != 0) ? enc_inc : 1'b0;

        enc_cmd = (enc_addr & ADDR_MASK)
                | (8'(rw_level)  << RW_POS)
                | (8'(inc_level) << INC_POS);

        // An address wider than the packed field is not a small error: the
        // high bits would be silently dropped and the access would land on
        // a different register. A part with more registers than fit here
        // sends the address as its own phase instead -- which is Chapter
        // 10.5's subject.
        enc_overflow = ((enc_addr & ~ADDR_MASK) != 8'h00);
    end

    always_comb begin
        dec_read = (((dec_cmd >> RW_POS) & 8'h01) == 8'(RW_READ_LEVEL[0]));
        dec_inc  = (HAS_INC != 0) ? (((dec_cmd >> INC_POS) & 8'h01) == 8'h01)
                                  : 1'b0;
        dec_addr = dec_cmd & ADDR_MASK;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cmd_codec_tb.sv — two devices with opposite polarity, round-tripped exhaustively
// spi_cmd_codec_tb.sv
//
// Two instances with OPPOSITE read/write polarity, checked against
// hand-computed bytes from a real convention, then round-tripped
// exhaustively. The round trip is the property that matters: encoding
// followed by decoding must return the request unchanged for every legal
// input, which catches an overlapping field far more reliably than any
// list of examples.

`timescale 1ns/1ps

module spi_cmd_codec_tb;

    int errors = 0;

    // Device A: bit 7 SET means read -- the accelerometer convention.
    logic       a_read, a_inc;
    logic [7:0] a_addr, a_cmd;
    logic       a_ovf;
    logic [7:0] a_dec_cmd, a_dec_addr;
    logic       a_dec_read, a_dec_inc;

    spi_cmd_codec #(
        .RW_POS(7), .RW_READ_LEVEL(1), .HAS_INC(1), .INC_POS(6),
        .ADDR_W_PACKED(6)
    ) dev_a (
        .enc_read(a_read), .enc_inc(a_inc), .enc_addr(a_addr),
        .enc_cmd(a_cmd), .enc_overflow(a_ovf),
        .dec_cmd(a_dec_cmd), .dec_read(a_dec_read),
        .dec_inc(a_dec_inc), .dec_addr(a_dec_addr)
    );

    // Device B: bit 7 CLEAR means read -- the opposite convention, equally
    // common, and the reason the level is a parameter.
    logic       b_read, b_inc;
    logic [7:0] b_addr, b_cmd;
    logic       b_ovf;
    logic [7:0] b_dec_cmd, b_dec_addr;
    logic       b_dec_read, b_dec_inc;

    spi_cmd_codec #(
        .RW_POS(7), .RW_READ_LEVEL(0), .HAS_INC(0), .INC_POS(6),
        .ADDR_W_PACKED(7)
    ) dev_b (
        .enc_read(b_read), .enc_inc(b_inc), .enc_addr(b_addr),
        .enc_cmd(b_cmd), .enc_overflow(b_ovf),
        .dec_cmd(b_dec_cmd), .dec_read(b_dec_read),
        .dec_inc(b_dec_inc), .dec_addr(b_dec_addr)
    );

    task automatic check_a(input logic rd, input logic inc,
                           input logic [7:0] addr, input logic [7:0] want);
        begin
            a_read = rd; a_inc = inc; a_addr = addr;
            #1;
            if (a_cmd !== want) begin
                $display("  FAIL: device A %s of 0x%02h%s encoded as 0x%02h, expected 0x%02h",
                         rd ? "read" : "write", addr, inc ? " (inc)" : "",
                         a_cmd, want);
                errors++;
            end else begin
                $display("  device A: %-5s 0x%02h%-6s -> 0x%02h",
                         rd ? "read" : "write", addr, inc ? " (inc)" : "", a_cmd);
            end
        end
    endtask

    initial begin
        // 1. Hand-computed bytes from the stated convention. Reading
        //    register 0x2D with bit 7 for read is 0xAD; adding the
        //    multi-byte bit makes it 0xED. A writer of the driver who got
        //    the polarity backwards would produce 0x2D and 0x6D here --
        //    both valid-looking bytes that write instead of read.
        check_a(1'b1, 1'b0, 8'h2D, 8'hAD);
        check_a(1'b1, 1'b1, 8'h2D, 8'hED);
        check_a(1'b0, 1'b0, 8'h2D, 8'h2D);
        check_a(1'b0, 1'b1, 8'h2D, 8'h6D);
        check_a(1'b1, 1'b0, 8'h00, 8'h80);
        check_a(1'b1, 1'b0, 8'h3F, 8'hBF);

        // 2. The opposite polarity, same logic. A read is now the byte
        //    WITHOUT the top bit set.
        b_read = 1'b1; b_inc = 1'b0; b_addr = 8'h2D; #1;
        if (b_cmd !== 8'h2D) begin
            $display("  FAIL: device B read of 0x2D encoded as 0x%02h, expected 0x2D", b_cmd);
            errors++;
        end
        b_read = 1'b0; #1;
        if (b_cmd !== 8'hAD) begin
            $display("  FAIL: device B write of 0x2D encoded as 0x%02h, expected 0xAD", b_cmd);
            errors++;
        end
        $display("  device B (inverted polarity): read 0x2D -> 0x2D, write 0x2D -> 0xAD");

        // 3. Overflow. Device A packs six address bits, so register 0x40 and
        //    above cannot be expressed. Dropping the high bits silently
        //    would land the access on register 0x00.
        a_read = 1'b1; a_inc = 1'b0; a_addr = 8'h3F; #1;
        if (a_ovf) begin
            $display("  FAIL: 0x3F fits six bits but was reported as overflow");
            errors++;
        end
        a_addr = 8'h40; #1;
        if (!a_ovf) begin
            $display("  FAIL: 0x40 does not fit six bits and was not reported");
            errors++;
        end
        $display("  device A: 0x3F fits, 0x40 overflows -- the boundary is exact");

        // 4. ROUND TRIP, exhaustive. For every legal address and both
        //    flags, encoding then decoding must return the request
        //    unchanged. This is the property that catches two fields
        //    overlapping, which no list of examples reliably does.
        for (int addr = 0; addr < 64; addr++) begin
            for (int rd = 0; rd < 2; rd++) begin
                for (int inc = 0; inc < 2; inc++) begin
                    a_read = rd[0]; a_inc = inc[0]; a_addr = 8'(addr);
                    #1;
                    a_dec_cmd = a_cmd;
                    #1;
                    if (a_dec_read !== rd[0] || a_dec_inc !== inc[0] ||
                        a_dec_addr !== 8'(addr)) begin
                        $display("  FAIL: round trip lost %0s addr=0x%02h inc=%0b (cmd 0x%02h decoded as rd=%0b inc=%0b addr=0x%02h)",
                                 rd[0] ? "read" : "write", addr, inc[0],
                                 a_cmd, a_dec_read, a_dec_inc, a_dec_addr);
                        errors++;
                    end
                end
            end
        end
        $display("  round trip: 256 request/command pairs recovered exactly");

        // 5. The same exhaustively for device B, whose seven packed address
        //    bits leave no room for an increment bit -- proving the
        //    parameterisation does not quietly assume the first device.
        for (int addr = 0; addr < 128; addr++) begin
            for (int rd = 0; rd < 2; rd++) begin
                b_read = rd[0]; b_inc = 1'b0; b_addr = 8'(addr);
                #1;
                b_dec_cmd = b_cmd;
                #1;
                if (b_dec_read !== rd[0] || b_dec_addr !== 8'(addr) ||
                    b_dec_inc !== 1'b0) begin
                    $display("  FAIL: device B round trip lost addr=0x%02h rd=%0b",
                             addr, rd[0]);
                    errors++;
                end
            end
        end
        $display("  device B round trip: 256 pairs recovered exactly");

        if (errors == 0)
            $display("PASS: both polarity conventions encode to the bytes the datasheet specifies, the packed-address boundary is exact, and encode-then-decode is the identity for every legal request on both devices");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule

That testbench does three things worth copying.

It instantiates two devices with opposite read/write polarity and checks both. A single instance with one convention would pass whatever the parameter did, since the parameter would also define the expectation — two instances with opposite conventions cannot both be satisfied by a block that ignores the parameter.

It checks hand-computed bytes from a real convention: reading register 0x2D with bit 7 meaning read is 0xAD, and 0xED with the multi-byte bit. Those are not values derived from the design; they come from the same arithmetic a person does with the datasheet, which is the only kind of expected value worth having here. A driver written with the polarity backwards produces 0x2D and 0x6D — both valid-looking bytes that write instead of read.

And it round-trips exhaustively: for every legal address and both flags, encoding followed by decoding must return the request unchanged. That single property catches two fields overlapping, which no list of examples reliably does — and it is cheap, because the whole space is 256 combinations.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cmd_codec.v — the same codec in Verilog-2001
// spi_cmd_codec.v
//
// Chapter 10.4 -- the command table turned into logic, in Verilog-2001.
//
// A register-access device packs a read/write bit, sometimes an
// auto-increment bit, and the register address into its first byte. Which
// bit is which -- and which POLARITY means read -- are datasheet facts
// that differ between parts. This block makes the convention a set of
// parameters instead of an assumption, and implements both directions.
//
// Both paths are combinational: encoding is a permutation of bits, and a
// register in front of a permutation buys nothing but latency.
//
// The default parameters describe a widely used accelerometer convention:
// bit 7 set means READ, bit 6 set means multi-byte, and the low six bits
// carry the address. Reading register 0x2D is 0xAD; with auto-increment,
// 0xED.

module spi_cmd_codec #(
    parameter RW_POS        = 7,  // bit carrying read/write
    parameter RW_READ_LEVEL = 1,  // the value of that bit meaning READ
    parameter HAS_INC       = 1,  // does the part have a multi-byte bit
    parameter INC_POS       = 6,  // and where
    parameter ADDR_W_PACKED = 6   // address bits inside the command byte
) (
    // Encode: a request in, a command byte out.
    input  wire       enc_read,
    input  wire       enc_inc,
    input  wire [7:0] enc_addr,
    output reg  [7:0] enc_cmd,
    output reg        enc_overflow,   // address does not fit the packed field

    // Decode: a captured command byte in, the request it represents out.
    input  wire [7:0] dec_cmd,
    output reg        dec_read,
    output reg        dec_inc,
    output reg  [7:0] dec_addr
);

    // Every field is extracted with a shift and a mask rather than a
    // parameterised part-select: the two are equivalent in intent, but a
    // variable part-select inside a combinational block is poorly supported
    // by several tools, and the mask form makes each field's width explicit.
    localparam [7:0] ADDR_MASK = ((1 << ADDR_W_PACKED) - 1);

    reg rw_level;
    reg inc_level;

    always @(*) begin
        // The read/write bit takes the level the datasheet says means read,
        // or its complement for a write. Parameterising the LEVEL rather
        // than hard-coding "1 means read" is the point: the opposite
        // convention is common, and a driver written for one silently
        // performs writes against a device using the other.
        if (enc_read) rw_level = (RW_READ_LEVEL != 0);
        else          rw_level = (RW_READ_LEVEL == 0);
        if (HAS_INC != 0) inc_level = enc_inc;
        else              inc_level = 1'b0;

        enc_cmd = (enc_addr & ADDR_MASK)
                | ({7'b0, rw_level}  << RW_POS)
                | ({7'b0, inc_level} << INC_POS);

        // An address wider than the packed field is not a small error: the
        // high bits would be dropped and the access would land on a
        // different register. A part with more registers than fit here
        // sends the address as its own phase -- Chapter 10.5's subject.
        enc_overflow = ((enc_addr & ~ADDR_MASK) != 8'h00);
    end

    always @(*) begin
        dec_read = (((dec_cmd >> RW_POS) & 8'h01) == ((RW_READ_LEVEL != 0) ? 8'h01 : 8'h00));
        if (HAS_INC != 0) dec_inc = (((dec_cmd >> INC_POS) & 8'h01) == 8'h01);
        else              dec_inc = 1'b0;
        dec_addr = dec_cmd & ADDR_MASK;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cmd_codec_tb.v — the same two conventions in Verilog-2001
// spi_cmd_codec_tb.v
//
// The same checks as the SystemVerilog testbench: two instances with
// opposite read/write polarity, hand-computed bytes from a real
// convention, an exact packed-address boundary, and an exhaustive round
// trip on both devices.

`timescale 1ns/1ps

module spi_cmd_codec_tb;

    integer errors;
    integer addr, rd, inc;

    reg         a_read, a_inc;
    reg  [7:0]  a_addr;
    wire [7:0]  a_cmd;
    wire        a_ovf;
    reg  [7:0]  a_dec_cmd;
    wire [7:0]  a_dec_addr;
    wire        a_dec_read, a_dec_inc;

    // Device A: bit 7 SET means read -- the accelerometer convention.
    spi_cmd_codec #(
        .RW_POS(7), .RW_READ_LEVEL(1), .HAS_INC(1), .INC_POS(6),
        .ADDR_W_PACKED(6)
    ) dev_a (
        .enc_read(a_read), .enc_inc(a_inc), .enc_addr(a_addr),
        .enc_cmd(a_cmd), .enc_overflow(a_ovf),
        .dec_cmd(a_dec_cmd), .dec_read(a_dec_read),
        .dec_inc(a_dec_inc), .dec_addr(a_dec_addr)
    );

    reg         b_read, b_inc;
    reg  [7:0]  b_addr;
    wire [7:0]  b_cmd;
    wire        b_ovf;
    reg  [7:0]  b_dec_cmd;
    wire [7:0]  b_dec_addr;
    wire        b_dec_read, b_dec_inc;

    // Device B: bit 7 CLEAR means read -- the opposite convention, equally
    // common, and the reason the level is a parameter.
    spi_cmd_codec #(
        .RW_POS(7), .RW_READ_LEVEL(0), .HAS_INC(0), .INC_POS(6),
        .ADDR_W_PACKED(7)
    ) dev_b (
        .enc_read(b_read), .enc_inc(b_inc), .enc_addr(b_addr),
        .enc_cmd(b_cmd), .enc_overflow(b_ovf),
        .dec_cmd(b_dec_cmd), .dec_read(b_dec_read),
        .dec_inc(b_dec_inc), .dec_addr(b_dec_addr)
    );

    initial begin
        errors = 0;
        a_read = 1'b0; a_inc = 1'b0; a_addr = 8'h00; a_dec_cmd = 8'h00;
        b_read = 1'b0; b_inc = 1'b0; b_addr = 8'h00; b_dec_cmd = 8'h00;
    end

    task check_a;
        input       rd_i;
        input       inc_i;
        input [7:0] addr_i;
        input [7:0] want;
        begin
            a_read = rd_i; a_inc = inc_i; a_addr = addr_i;
            #1;
            if (a_cmd !== want) begin
                $display("  FAIL: device A %0s of 0x%02h%0s encoded as 0x%02h, expected 0x%02h",
                         rd_i ? "read" : "write", addr_i, inc_i ? " (inc)" : "",
                         a_cmd, want);
                errors = errors + 1;
            end else begin
                $display("  device A: %0s 0x%02h%0s -> 0x%02h",
                         rd_i ? "read " : "write", addr_i,
                         inc_i ? " (inc)" : "      ", a_cmd);
            end
        end
    endtask

    initial begin
        #1;
        // 1. Hand-computed bytes from the stated convention. A writer who
        //    got the polarity backwards would produce 0x2D and 0x6D here --
        //    valid-looking bytes that write instead of read.
        check_a(1'b1, 1'b0, 8'h2D, 8'hAD);
        check_a(1'b1, 1'b1, 8'h2D, 8'hED);
        check_a(1'b0, 1'b0, 8'h2D, 8'h2D);
        check_a(1'b0, 1'b1, 8'h2D, 8'h6D);
        check_a(1'b1, 1'b0, 8'h00, 8'h80);
        check_a(1'b1, 1'b0, 8'h3F, 8'hBF);

        // 2. The opposite polarity, same logic.
        b_read = 1'b1; b_inc = 1'b0; b_addr = 8'h2D; #1;
        if (b_cmd !== 8'h2D) begin
            $display("  FAIL: device B read of 0x2D encoded as 0x%02h, expected 0x2D", b_cmd);
            errors = errors + 1;
        end
        b_read = 1'b0; #1;
        if (b_cmd !== 8'hAD) begin
            $display("  FAIL: device B write of 0x2D encoded as 0x%02h, expected 0xAD", b_cmd);
            errors = errors + 1;
        end
        $display("  device B (inverted polarity): read 0x2D -> 0x2D, write 0x2D -> 0xAD");

        // 3. Overflow boundary, exact.
        a_read = 1'b1; a_inc = 1'b0; a_addr = 8'h3F; #1;
        if (a_ovf) begin
            $display("  FAIL: 0x3F fits six bits but was reported as overflow");
            errors = errors + 1;
        end
        a_addr = 8'h40; #1;
        if (!a_ovf) begin
            $display("  FAIL: 0x40 does not fit six bits and was not reported");
            errors = errors + 1;
        end
        $display("  device A: 0x3F fits, 0x40 overflows -- the boundary is exact");

        // 4. ROUND TRIP, exhaustive. Encoding then decoding must return the
        //    request unchanged for every legal input -- the property that
        //    catches two fields overlapping.
        for (addr = 0; addr < 64; addr = addr + 1) begin
            for (rd = 0; rd < 2; rd = rd + 1) begin
                for (inc = 0; inc < 2; inc = inc + 1) begin
                    a_read = rd[0]; a_inc = inc[0]; a_addr = addr[7:0];
                    #1;
                    a_dec_cmd = a_cmd;
                    #1;
                    if (a_dec_read !== rd[0] || a_dec_inc !== inc[0] ||
                        a_dec_addr !== addr[7:0]) begin
                        $display("  FAIL: round trip lost addr=0x%02h rd=%0b inc=%0b (cmd 0x%02h)",
                                 addr[7:0], rd[0], inc[0], a_cmd);
                        errors = errors + 1;
                    end
                end
            end
        end
        $display("  round trip: 256 request/command pairs recovered exactly");

        // 5. The same for device B, whose seven packed address bits leave
        //    no room for an increment bit.
        for (addr = 0; addr < 128; addr = addr + 1) begin
            for (rd = 0; rd < 2; rd = rd + 1) begin
                b_read = rd[0]; b_inc = 1'b0; b_addr = addr[7:0];
                #1;
                b_dec_cmd = b_cmd;
                #1;
                if (b_dec_read !== rd[0] || b_dec_addr !== addr[7:0] ||
                    b_dec_inc !== 1'b0) begin
                    $display("  FAIL: device B round trip lost addr=0x%02h rd=%0b",
                             addr[7:0], rd[0]);
                    errors = errors + 1;
                end
            end
        end
        $display("  device B round trip: 256 pairs recovered exactly");

        if (errors == 0)
            $display("PASS: both polarity conventions encode to the bytes the datasheet specifies, the packed-address boundary is exact, and encode-then-decode is the identity for every legal request on both devices");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cmd_codec.vhd — the same codec in VHDL
-- spi_cmd_codec.vhd
--
-- Chapter 10.4 -- the command table turned into logic, in VHDL.
--
-- A register-access device packs a read/write bit, sometimes an
-- auto-increment bit, and the register address into its first byte. Which
-- bit is which -- and which POLARITY means read -- are datasheet facts
-- that differ between parts. This block makes the convention a set of
-- generics instead of an assumption, and implements both directions.
--
-- Both paths are combinational: encoding is a permutation of bits, and a
-- register in front of a permutation buys nothing but latency.
--
-- The default generics describe a widely used accelerometer convention:
-- bit 7 set means READ, bit 6 set means multi-byte, and the low six bits
-- carry the address. Reading register 0x2D is 0xAD; with auto-increment,
-- 0xED.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_cmd_codec is
    generic (
        RW_POS        : natural := 7;  -- bit carrying read/write
        RW_READ_LEVEL : natural := 1;  -- the value of that bit meaning READ
        HAS_INC       : natural := 1;  -- does the part have a multi-byte bit
        INC_POS       : natural := 6;  -- and where
        ADDR_W_PACKED : natural := 6   -- address bits inside the command byte
    );
    port (
        -- Encode: a request in, a command byte out.
        enc_read     : in  std_logic;
        enc_inc      : in  std_logic;
        enc_addr     : in  unsigned(7 downto 0);
        enc_cmd      : out unsigned(7 downto 0);
        enc_overflow : out std_logic;   -- address does not fit the packed field

        -- Decode: a captured command byte in, the request it represents out.
        dec_cmd      : in  unsigned(7 downto 0);
        dec_read     : out std_logic;
        dec_inc      : out std_logic;
        dec_addr     : out unsigned(7 downto 0)
    );
end entity;

architecture rtl of spi_cmd_codec is

    -- Every field is extracted with a shift and a mask rather than a
    -- generic-indexed slice: the mask form makes each field's width
    -- explicit at the point of use and avoids a slice whose bounds depend
    -- on a generic.
    constant ADDR_MASK : unsigned(7 downto 0) :=
        to_unsigned(2 ** ADDR_W_PACKED - 1, 8);

begin

    encode : process (enc_read, enc_inc, enc_addr)
        variable rw_level  : std_logic;
        variable inc_level : std_logic;
        variable c         : unsigned(7 downto 0);
    begin
        -- The read/write bit takes the level the datasheet says means read,
        -- or its complement for a write. Parameterising the LEVEL rather
        -- than hard-coding "1 means read" is the point: the opposite
        -- convention is common, and a driver written for one silently
        -- performs writes against a device using the other.
        if enc_read = '1' then
            if RW_READ_LEVEL /= 0 then rw_level := '1';
            else                       rw_level := '0'; end if;
        else
            if RW_READ_LEVEL /= 0 then rw_level := '0';
            else                       rw_level := '1'; end if;
        end if;

        if HAS_INC /= 0 then inc_level := enc_inc;
        else                 inc_level := '0'; end if;

        c := enc_addr and ADDR_MASK;
        if rw_level = '1' then
            c := c or shift_left(to_unsigned(1, 8), RW_POS);
        end if;
        if inc_level = '1' then
            c := c or shift_left(to_unsigned(1, 8), INC_POS);
        end if;
        enc_cmd <= c;

        -- An address wider than the packed field is not a small error: the
        -- high bits would be dropped and the access would land on a
        -- different register. A part with more registers than fit here
        -- sends the address as its own phase -- Chapter 10.5's subject.
        if (enc_addr and not ADDR_MASK) /= 0 then
            enc_overflow <= '1';
        else
            enc_overflow <= '0';
        end if;
    end process;

    decode : process (dec_cmd)
        variable rw_bit  : std_logic;
        variable inc_bit : std_logic;
    begin
        rw_bit  := dec_cmd(RW_POS);
        inc_bit := dec_cmd(INC_POS);

        if (RW_READ_LEVEL /= 0 and rw_bit = '1') or
           (RW_READ_LEVEL = 0 and rw_bit = '0') then
            dec_read <= '1';
        else
            dec_read <= '0';
        end if;

        if HAS_INC /= 0 then dec_inc <= inc_bit;
        else                 dec_inc <= '0'; end if;

        dec_addr <= dec_cmd and ADDR_MASK;
    end process;

end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cmd_codec_tb.vhd — the same two conventions in VHDL
-- spi_cmd_codec_tb.vhd
--
-- The same checks as the SystemVerilog and Verilog testbenches: two
-- instances with opposite read/write polarity, hand-computed bytes from a
-- real convention, an exact packed-address boundary, and an exhaustive
-- round trip on both devices.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_cmd_codec_tb is
end entity;

architecture sim of spi_cmd_codec_tb is

    signal a_read, a_inc : std_logic := '0';
    signal a_addr        : unsigned(7 downto 0) := (others => '0');
    signal a_cmd         : unsigned(7 downto 0);
    signal a_ovf         : std_logic;
    signal a_dec_cmd     : unsigned(7 downto 0) := (others => '0');
    signal a_dec_addr    : unsigned(7 downto 0);
    signal a_dec_read    : std_logic;
    signal a_dec_inc     : std_logic;

    signal b_read, b_inc : std_logic := '0';
    signal b_addr        : unsigned(7 downto 0) := (others => '0');
    signal b_cmd         : unsigned(7 downto 0);
    signal b_ovf         : std_logic;
    signal b_dec_cmd     : unsigned(7 downto 0) := (others => '0');
    signal b_dec_addr    : unsigned(7 downto 0);
    signal b_dec_read    : std_logic;
    signal b_dec_inc     : std_logic;

    signal errors : natural := 0;

begin

    -- Device A: bit 7 SET means read -- the accelerometer convention.
    dev_a : entity work.spi_cmd_codec
        generic map (RW_POS => 7, RW_READ_LEVEL => 1, HAS_INC => 1,
                     INC_POS => 6, ADDR_W_PACKED => 6)
        port map (
            enc_read => a_read, enc_inc => a_inc, enc_addr => a_addr,
            enc_cmd => a_cmd, enc_overflow => a_ovf,
            dec_cmd => a_dec_cmd, dec_read => a_dec_read,
            dec_inc => a_dec_inc, dec_addr => a_dec_addr
        );

    -- Device B: bit 7 CLEAR means read -- the opposite convention.
    dev_b : entity work.spi_cmd_codec
        generic map (RW_POS => 7, RW_READ_LEVEL => 0, HAS_INC => 0,
                     INC_POS => 6, ADDR_W_PACKED => 7)
        port map (
            enc_read => b_read, enc_inc => b_inc, enc_addr => b_addr,
            enc_cmd => b_cmd, enc_overflow => b_ovf,
            dec_cmd => b_dec_cmd, dec_read => b_dec_read,
            dec_inc => b_dec_inc, dec_addr => b_dec_addr
        );

    stim : process
        variable errs : natural := 0;

        procedure check_a(rd : std_logic; inc : std_logic;
                          addr : natural; want : natural) is
        begin
            a_read <= rd; a_inc <= inc; a_addr <= to_unsigned(addr, 8);
            wait for 1 ns;
            if to_integer(a_cmd) /= want then
                report "  FAIL: device A encode of register " &
                       integer'image(addr) & " gave " &
                       integer'image(to_integer(a_cmd)) & ", expected " &
                       integer'image(want);
                errs := errs + 1;
            else
                report "  device A: register " & integer'image(addr) &
                       " -> command " & integer'image(to_integer(a_cmd));
            end if;
        end procedure;
    begin
        wait for 1 ns;

        -- 1. Hand-computed bytes from the stated convention. 0xAD = 173,
        --    0xED = 237, 0x2D = 45, 0x6D = 109, 0x80 = 128, 0xBF = 191.
        --    A writer who got the polarity backwards would produce 45 and
        --    109 for the reads -- valid-looking bytes that write instead.
        check_a('1', '0', 16#2D#, 16#AD#);
        check_a('1', '1', 16#2D#, 16#ED#);
        check_a('0', '0', 16#2D#, 16#2D#);
        check_a('0', '1', 16#2D#, 16#6D#);
        check_a('1', '0', 16#00#, 16#80#);
        check_a('1', '0', 16#3F#, 16#BF#);

        -- 2. The opposite polarity, same logic.
        b_read <= '1'; b_inc <= '0'; b_addr <= to_unsigned(16#2D#, 8);
        wait for 1 ns;
        if to_integer(b_cmd) /= 16#2D# then
            report "  FAIL: device B read of 0x2D did not encode as 0x2D";
            errs := errs + 1;
        end if;
        b_read <= '0';
        wait for 1 ns;
        if to_integer(b_cmd) /= 16#AD# then
            report "  FAIL: device B write of 0x2D did not encode as 0xAD";
            errs := errs + 1;
        end if;
        report "  device B (inverted polarity): read 0x2D -> 0x2D, write 0x2D -> 0xAD";

        -- 3. Overflow boundary, exact.
        a_read <= '1'; a_inc <= '0'; a_addr <= to_unsigned(16#3F#, 8);
        wait for 1 ns;
        if a_ovf = '1' then
            report "  FAIL: 0x3F fits six bits but was reported as overflow";
            errs := errs + 1;
        end if;
        a_addr <= to_unsigned(16#40#, 8);
        wait for 1 ns;
        if a_ovf /= '1' then
            report "  FAIL: 0x40 does not fit six bits and was not reported";
            errs := errs + 1;
        end if;
        report "  device A: 0x3F fits, 0x40 overflows -- the boundary is exact";

        -- 4. ROUND TRIP, exhaustive. Encoding then decoding must return the
        --    request unchanged for every legal input -- the property that
        --    catches two fields overlapping.
        for addr in 0 to 63 loop
            for rd in 0 to 1 loop
                for inc in 0 to 1 loop
                    if rd = 1 then a_read <= '1'; else a_read <= '0'; end if;
                    if inc = 1 then a_inc <= '1'; else a_inc <= '0'; end if;
                    a_addr <= to_unsigned(addr, 8);
                    wait for 1 ns;
                    a_dec_cmd <= a_cmd;
                    wait for 1 ns;
                    if (a_dec_read = '1') /= (rd = 1) or
                       (a_dec_inc = '1') /= (inc = 1) or
                       to_integer(a_dec_addr) /= addr then
                        report "  FAIL: round trip lost register " &
                               integer'image(addr);
                        errs := errs + 1;
                    end if;
                end loop;
            end loop;
        end loop;
        report "  round trip: 256 request/command pairs recovered exactly";

        -- 5. The same for device B, whose seven packed address bits leave
        --    no room for an increment bit.
        for addr in 0 to 127 loop
            for rd in 0 to 1 loop
                if rd = 1 then b_read <= '1'; else b_read <= '0'; end if;
                b_inc  <= '0';
                b_addr <= to_unsigned(addr, 8);
                wait for 1 ns;
                b_dec_cmd <= b_cmd;
                wait for 1 ns;
                if (b_dec_read = '1') /= (rd = 1) or
                   to_integer(b_dec_addr) /= addr or b_dec_inc /= '0' then
                    report "  FAIL: device B round trip lost register " &
                           integer'image(addr);
                    errs := errs + 1;
                end if;
            end loop;
        end loop;
        report "  device B round trip: 256 pairs recovered exactly";

        errors <= errs;
        if errs = 0 then
            report "PASS: both polarity conventions encode to the bytes the datasheet specifies, the packed-address boundary is exact, and encode-then-decode is the identity for every legal request on both devices";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        wait;
    end process;

end architecture;

Parity

All three implement the same codec: identical ports and generics, purely combinational in both directions, the read/write level parameterised rather than assumed, an increment bit that can be absent, and an overflow flag on an address wider than the packed field. All three testbenches check the same hand-computed bytes, the same exact overflow boundary at 0x3F and 0x40, and the same two exhaustive round trips of 256 pairs each.

6. Why a Verification Engineer Cares

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cmd_codec.sva — the identity property and its friends
   // 1. ROUND TRIP. Decoding an encoded request returns the request.
   //    This single property subsumes most of what a directed test would
   //    check, and catches two fields overlapping -- which a list of
   //    examples reliably does not.
   a_round_trip : assert property (
       @(posedge clk) disable iff (!rst_n)
           (!enc_overflow) |-> (dec_read == enc_read &&
                                dec_inc  == enc_inc  &&
                                dec_addr == (enc_addr & ADDR_MASK)))
       else $error("encode followed by decode did not return the request");

   // 2. DIRECTION IS NEVER AMBIGUOUS. A read and a write of the same
   //    register must differ -- a codec that produced the same byte for
   //    both would be catastrophic and would pass any test that only
   //    checked reads.
   a_direction_distinct : assert property (
       @(posedge clk) disable iff (!rst_n)
           (cmd_read_of_addr != cmd_write_of_addr))
       else $error("read and write encode to the same command byte");

   // 3. The address field is never corrupted by the control bits, and the
   //    control bits are never corrupted by a wide address.
   a_fields_disjoint : assert property (
       @(posedge clk) disable iff (!rst_n)
           ((enc_cmd & ADDR_MASK) == (enc_addr & ADDR_MASK)))
       else $error("a control bit landed inside the address field");

   // 4. An address that does not fit is REPORTED, not truncated. The
   //    device cannot detect this -- the bits are simply not there -- so
   //    the obligation is entirely the controller's.
   a_overflow_reported : assert property (
       @(posedge clk) disable iff (!rst_n)
           ((enc_addr & ~ADDR_MASK) != 0) |-> enc_overflow)
       else $error("an over-wide address was accepted silently");

Property 1 is the shape to remember. When a design has an inverse, the round trip is the specification — and it is almost always both cheaper and stronger than enumerating cases. Encoders and decoders, packers and unpackers, serialisers and deserialisers all have this structure.

Property 2 looks trivially true and is the one that would have caught the failure in this chapter's opening question, not in the codec but in the testbench that validates the parameters. A suite that only ever encodes reads cannot tell a correct polarity from an inverted one.

Coverage must cross direction with the parameter, not just exercise addresses:

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cmd_cg.sv — the convention is the variable
   covergroup spi_cmd_cg @(posedge clk iff req_valid);
       cp_dir : coverpoint enc_read { bins rd = {1}; bins wr = {0}; }

       cp_inc : coverpoint enc_inc  { bins single = {0}; bins burst = {1}; }

       // The packed-address boundary, exactly. 0x3F must encode, 0x40 must
       // overflow -- one apart, and different behaviour.
       cp_addr : coverpoint enc_addr {
           bins zero     = {0};
           bins low      = {[1:16]};
           bins top_fit  = {63};      // the last address that fits
           bins first_over = {64};    // the first that does not
           bins high_over  = {[65:255]};
       }

       // The parameter itself is the variable this design exists for. A
       // suite run against only one polarity has verified half the block.
       cp_polarity : coverpoint rw_read_level { bins set_means_read = {1};
                                                bins set_means_write = {0}; }

       x_dir_polarity : cross cp_dir, cp_polarity;
   endgroup

x_dir_polarity is the cross that matters. Four combinations — read and write under each convention — and a suite covering fewer has not tested the thing the block was built to handle.

7. Why an FPGA or ASIC Engineer Cares

Make the convention a profile field. A controller with the polarity, bit positions and address width as configuration serves every packed-command device on the board. Baking one part's convention into the RTL means a second sensor needs a second controller.

Check the address width in hardware. The device cannot detect an over-wide address — the bits do not exist in its command byte — so the only place the check can live is the controller. Three gates buy a flag that turns a silent access to the wrong register into a reported error.

Do not register a permutation. Encoding is combinational. A register here adds a cycle to every access and prevents the surrounding controller from issuing in the cycle it decides to.

Expose the encoded byte. A readable register holding the last command byte issued turns "the sensor is misbehaving" into a one-line comparison against the datasheet. It is one register and it removes the need for a logic analyser in the most common case.

Treat the polarity as a safety-relevant parameter. A wrong polarity turns reads into writes. If the design has any notion of a locked or protected configuration, the polarity field belongs inside it — changing it at runtime should be as deliberate as changing the divisor.

8. Failure Signature — A Sensor That Ignores Its Configuration

Symptom. A sensor is configured at start-up: sample rate, range, filter settings, all written in sequence with no errors. It then runs with default settings — the sample rate is wrong, the range is wrong. Reading the configuration registers back returns the values that were written, so the configuration appears to have taken.

What "reads back correctly" establishes. This is the observation that makes the case interesting, because it seems to rule out the obvious explanation. It does not — and seeing why is the whole diagnosis.

Plausible mechanisms.

  • Inverted R/W polarity. If set-means-write and the driver sets the bit for reads, then every "write" is actually a read and every "read" is actually a write. The read-back appears to work because the read-back writes the expected value first and the driver's own buffer supplies the comparison — or, more simply, because the register never changed and the driver is comparing against what it meant to write.
  • A missing enable or commit step. Many parts require a configuration-update bit, or require settings to be written while the device is in standby.
  • Writes to a shadow bank that is only applied on a trigger.
  • The device being reset after configuration by something else in the system.
  • The increment bit being misused, so a block write lands entirely in the first register and the rest keep their defaults.

The discriminating observation. Write a value, then read it back after power-cycling nothing but re-asserting CS between the two, and compare against a value the driver has never held — for instance, write 0x55 to a register whose default is 0x00, then read with an independent tool or a different code path. If the read returns 0x00, the write never happened.

Better still, and faster: read a read-only register with a known non-zero constant, such as the ID register. Under inverted polarity that access becomes a write to a read-only register and returns whatever the bus floats to — typically zeros or the last byte sent. An ID read that returns 0x00 or an echo of the transmitted byte is close to conclusive.

Finally, check the increment mechanism: if only the first register of each block took effect, the increment bit is the cause rather than the polarity.

The fix. Correct the polarity in the profile. The reason this is worth catching early is the asymmetry of §2: under inverted polarity every intended read is a destructive write, so a driver that polls a status register in a loop is writing to it continuously.

Why the investigation goes wrong. Because "the registers read back correctly" is accepted as proof that the writes worked, when under the exact fault in question the read-back is not reading. The self-consistency of a driver comparing against its own expectations is the trap — which is why the discriminating test uses a value the driver never supplied, or a register whose correct answer is fixed and known.

9. Common Misconceptions

10. Reason It Through

Work this before reading the answer.

A part's command table says: "The first byte contains the register address in bits 6:0. Bit 7 is set to 1 for a write operation. For multi-byte access, the address auto-increments; the device supports up to 16 consecutive registers per access."

Write the command byte for: (a) reading register 0x10, (b) writing register 0x10, (c) reading registers 0x10 through 0x13 in one access. What is missing from this description, and what would you check first?

(a) Reading register 0x10. Bit 7 set means write, so a read leaves bit 7 clear. The address occupies bits 6:0, and 0x10 fits. The byte is 0x10.

(b) Writing register 0x10. Set bit 7: 0x90.

(c) Reading 0x10 through 0x13 in one access. Here is the trap. The description mentions auto-increment but does not give an increment bit — and it cannot, because bits 6:0 are all address and bit 7 is direction. There is no bit left.

So the increment must be implicit: the device auto-increments whenever the access continues, and the number of registers read is determined entirely by how long chip select stays asserted. The command byte is therefore the same as (a) — 0x10 — and the master simply clocks four bytes instead of one before releasing CS.

What is missing from the description? Several things, and noticing them is the exercise:

  • The address field is seven bits, so registers 0x00 to 0x7F are reachable. Nothing says what happens above that — and per §4, the device will truncate silently.
  • The "up to 16 consecutive registers" limit has no stated mechanism. What happens on the seventeenth byte? The pointer might wrap to the start of the block, stop advancing and repeat one register, or run into reserved space. All three occur in real parts, and the difference is invisible until a driver reads 17 bytes.
  • Nothing says whether the pointer resets on CS release. Almost all parts do reset it, but a part that does not turns a sequence of accesses into a walk through the map.
  • Nothing says whether writes also auto-increment. Read and write burst behaviour differ on some parts.

What would you check first? The polarity, and by reading rather than writing. Send 0x10 and see whether the ID register — or any register with a known constant — returns its expected value. Under the stated convention a clear bit 7 is a read, and a correct value confirms the reading of the table. If the wrong operation is being performed, you find out with a read whose failure is harmless rather than with a write whose success is destructive.

The general lesson. A command table that describes fields in prose usually leaves the boundaries undefined: what happens past the address range, past the burst limit, and across a CS release. Those are exactly the cases a driver hits in production and never on the bench, and the only reliable answers come from testing them deliberately — which is what the exhaustive round trip and the boundary bins of §5 and §6 are for.

11. Understanding Check

12. Summary

A packed command byte carries direction, sometimes increment, and the register address, and all three fields vary between parts.

The polarity of the read/write bit is the dangerous variable. Set-means-read and set-means-write are both common, there is no convention, and getting it backwards turns every intended read into a destructive write while turning every write into a harmless read whose absence only shows up later.

A register access sends the address once; everything after the command byte is data supplied by the device's internal pointer, and chip select frames the burst — releasing it usually resets the pointer.

The device cannot detect an address too wide for its packed field. It truncates and acts on the wrong register, so that check belongs entirely to the controller.

In hardware the encoding is a parameterised permutation with no clock, in both directions, because a register in front of a permutation buys only latency.

For verification the round trip is the specification — encode then decode must be the identity — and it is both cheaper and stronger than a list of examples. The coverage cross that matters is direction against polarity, because a suite run under one convention has tested half the block.

And "the registers read back correctly" proves nothing under the fault most likely to be present: use a value the driver never supplied, or a register whose correct answer is fixed.

13. What Comes Next

Packing the address into the command byte works while the map is small. A memory has far more addresses than bits, so the address becomes its own phase — and once it does, three new questions appear: how many bytes it takes, in what order they go, and how many cycles of latency follow before data appears.

Chapter 10.5 — Address Fields, Dummy Cycles, and Burst Behaviour answers all three, including why dummy latency is quoted in cycles rather than bytes and what sending a dummy byte instead does to the payload — with the planner that turns those datasheet numbers into a cycle-accurate schedule, in all three HDLs.

Continue learning