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:
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.
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.
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:
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.
// 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// 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
endmoduleThat 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.
// 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// 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-- 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;-- 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
// 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:
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;
endgroupx_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
Related tutorials
- Related topic
Identifying the Required SPI Mode
The two observations that read CPOL and CPHA off any vendor timing diagram, why the picture is more trustworthy than the prose beside it, why trying all four modes cannot work, and the observer that infers the mode from a live capture.
- Related topic
Extracting Setup, Hold, and Maximum SCLK
Which timing-table rows constrain you and which constrain the device, why a number without its load and corner is not a specification, how a delay on one line alone destroys margin, and the monitor that measures the real link against the datasheet.
- Related topic
The System-Side Interface
A slave cannot ask the master to wait and cannot choose when its buffer changes, so it needs two mechanisms and neither is a buffer: a configured transmit default that makes an underrun recognisable at the master, and a sequence lock that makes a torn read detectable rather than merely unlikely.
- Related topic
Launch and Sample Edges
One edge of each bit time places a bit on the wire, the other captures it, and they must never be the same edge. Why the separation is forced, why it buys half a period, and how RTL maps physical edges onto those roles.
