An I2C controller has to generate START and STOP conditions, clock out bytes, read the acknowledge, receive bytes and send its own acknowledge, all with open-drain outputs and while watching the bus for clock stretching and lost arbitration. The cleanest structure is a byte engine that executes one command at a time: START, STOP, WRITE or READ. Transactions are built from those commands by software or a small sequencer. This article builds that engine.
9 min read | Intermediate
What You'll Learn
- Drive open-drain lines from RTL
- Generate SCL from quarter periods and place SDA changes correctly
- Build START, STOP, byte write and byte read from those quarters
- Support clock stretching and detect lost arbitration
Open Drain in RTL
Inside the design, each line is two signals: an output enable that pulls the line low, and an input that reads the actual level. At the chip pins they become one bidirectional pad:
assign SCL = scl_oe ? 1'b0 : 1'bz; // pull low or release
assign SDA = sda_oe ? 1'b0 : 1'bz;
assign scl_i = SCL; // read back the real level
assign sda_i = SDA;
Reading back the real level is essential: it is how the controller sees acknowledges, clock stretching and other controllers.
Quarter Periods
Each SCL period is split into four quarters of Q system clocks. For a data bit: quarter 0 changes SDA while SCL is low, quarter 1 holds it, at the end of quarter 1 SCL is released, quarters 2 and 3 keep SCL high, and at the end of quarter 3 SDA is sampled and SCL pulled low again. START and STOP use the same quarters, but change SDA while SCL is high.
The RTL
// I2C controller (master) byte engine. It executes one command at a time:
// START (also used as a repeated START), STOP,
// WRITE sends wdata and returns the target's acknowledge in ack_in (0 = ACK),
// READ receives rdata and sends ack_out (0 = ACK, 1 = NACK for the last byte).
// SCL and SDA are open drain: scl_oe / sda_oe = 1 pulls the line low, 0 releases it.
// Each SCL period has four quarters of Q system clocks. While SCL is released the controller
// waits until it really is high, so a target can stretch the clock. While sending a 1 it checks
// SDA: if SDA is low, another controller has won the arbitration and arb_lost is set.
module i2c_master #(
parameter Q = 5
) (
input clk,
input rst,
input [1:0] cmd,
input cmd_valid,
output cmd_ready,
input [7:0] wdata,
input ack_out,
output reg [7:0] rdata,
output reg ack_in,
output reg done, // one-cycle pulse when a command completes
output reg arb_lost,
output reg scl_oe,
output reg sda_oe,
input scl_i,
input sda_i
);
localparam START = 2'd0, STOP = 2'd1, WRITE = 2'd2, READ = 2'd3;
reg busy;
reg [1:0] op;
reg [3:0] bitn; // bit within a byte: 0-7 data, 8 acknowledge
reg [1:0] qtr; // quarter of the current SCL period
reg [15:0] cnt;
reg [7:0] sh;
assign cmd_ready = !busy;
// The value this controller puts on SDA for the current bit (1 = release)
wire out_bit = (op == WRITE) ? ((bitn < 8) ? sh[7] : 1'b1) :
(op == READ) ? ((bitn < 8) ? 1'b1 : ack_out) : 1'b1;
always @(posedge clk) begin
if (rst) begin
busy <= 1'b0;
done <= 1'b0;
arb_lost <= 1'b0;
scl_oe <= 1'b0;
sda_oe <= 1'b0;
end
else begin
done <= 1'b0;
if (!busy) begin
if (cmd_valid) begin
busy <= 1'b1;
op <= cmd;
sh <= wdata;
bitn <= 4'd0;
qtr <= 2'd0;
cnt <= 16'd0;
arb_lost <= 1'b0;
end
end
else if (qtr == 2'd2 && !scl_i && !scl_oe) begin
// SCL released but still low: a target is stretching the clock, wait
cnt <= 16'd0;
end
else if (cnt != Q - 1) begin
cnt <= cnt + 16'd1;
end
else begin
cnt <= 16'd0;
qtr <= qtr + 2'd1;
case (op)
START: begin // SDA falls while SCL is high
case (qtr)
2'd0: sda_oe <= 1'b0; // release SDA (needed for a repeated START)
2'd1: scl_oe <= 1'b0; // release SCL
2'd2: sda_oe <= 1'b1; // SDA low while SCL high: START
2'd3: begin
scl_oe <= 1'b1;
busy <= 1'b0;
done <= 1'b1;
end
endcase
end
STOP: begin // SDA rises while SCL is high
case (qtr)
2'd0: sda_oe <= 1'b1; // SDA low while SCL is low
2'd1: scl_oe <= 1'b0; // release SCL
2'd2: sda_oe <= 1'b0; // SDA rises while SCL high: STOP
2'd3: begin
busy <= 1'b0;
done <= 1'b1;
end
endcase
end
default: begin // WRITE and READ: nine bits, SCL low - low - high - high
case (qtr)
2'd0: sda_oe <= !out_bit; // change SDA while SCL is low
2'd1: scl_oe <= 1'b0; // release SCL
2'd2: ; // SCL high: the bit is valid
2'd3: begin // sample at the end of the high time
scl_oe <= 1'b1;
if (op == WRITE && bitn < 8 && out_bit && !sda_i) begin
arb_lost <= 1'b1; // we sent 1 but the bus is 0: lost
sda_oe <= 1'b0;
scl_oe <= 1'b0;
busy <= 1'b0;
done <= 1'b1;
end
else if (bitn == 8) begin
if (op == WRITE)
ack_in <= sda_i; // 0 = the target acknowledged
else
rdata <= sh;
sda_oe <= 1'b0;
busy <= 1'b0;
done <= 1'b1;
end
else begin
sh <= (op == WRITE) ? {sh[6:0], 1'b0} : {sh[6:0], sda_i};
bitn <= bitn + 4'd1;
end
end
endcase
end
endcase
end
end
end
endmodule
Three details make it a real I2C controller rather than a shift register:
- Clock stretching: in quarter 2, if SCL has been released but is still low, the counter is held at zero. The high time only starts once the line really is high.
- Arbitration: when the controller sends a 1 (releases SDA) but samples a 0, another controller is driving. It releases both lines and reports
arb_lost. - READ and ACK: during a read, SDA is released for the eight data bits and the target drives them; in the ninth bit the controller drives its own ACK or NACK from
ack_out.
Quick Check
Q: With a 100 MHz system clock, what value of Q gives a 400 kHz Fast-mode SCL?
Show Answer
An SCL period of 2.5 µs is 250 system clocks, so each quarter is 62.5 clocks; Q = 63 gives about 397 kHz. Fast-mode also has minimum low and high times, so real controllers often make the low part longer than the high part.
Building Transactions
The testbench builds register accesses from commands, the way a driver would program a controller:
task write_reg(input [6:0] dev, input [1:0] r, input [7:0] v);
reg ok;
begin
op(START, 0, 0);
op(WRITE, {dev, 1'b0}, 0);
ok = !ack_in;
if (ok) begin
op(WRITE, {6'b0, r}, 0);
op(WRITE, v, 0);
end
op(STOP, 0, 0);
// ... report
end
endtask
6015 write 0x50 reg1 = 0xa7 11965 write 0x51 reg1 = 0x3c 19975 read 0x50 reg1 = 0xa7 27985 read 0x51 reg1 = 0x3c 30275 write 0x52: no ACK (NACK)
After a NACK on the address, the task skips the remaining bytes and sends STOP straight away, which is what a careful driver does.
Quick Check
Q: Why does a START command first release SDA and SCL before pulling SDA low?
Show Answer
So that the same command works as a repeated START. After a byte, SCL is low and SDA may be low; the controller must bring both high first, and only then pull SDA low while SCL is high.
Running It in ChipVerify Lab
The example contains this controller, two targets and the register tasks. Follow scl_oe and sda_oe through each command in the waveform.
Common Mistakes
Mistake #1: Not reading back SCL
A controller that counts its own SCL timing without looking at the line ignores clock stretching and corrupts transfers with slow targets.
Mistake #2: Changing SDA while SCL is high
Any SDA change during SCL high is a START or STOP. Change SDA only in the low phase of the clock.
Mistake #3: Ignoring arb_lost
In a multi-controller system, a controller that keeps going after losing arbitration corrupts the winner's transfer. Stop driving at once and retry later.
Practice Exercise
Challenge: Two Bytes in One Read
Write 0xA7 to register 1 and 0xB8 to register 2 of target 0x50, then read both in one transaction: the first byte with ACK, the second with NACK.
Hint
Use two READ commands with different ack_out values.
Solution
op(START, 0, 0);
op(WRITE, {7'h50, 1'b0}, 0);
op(WRITE, 8'h01, 0);
op(START, 0, 0); // repeated START
op(WRITE, {7'h50, 1'b1}, 0);
op(READ, 0, 0); // first byte: ACK, we want more
op(READ, 0, 1); // second byte: NACK, the last one
op(STOP, 0, 0);
6015 write 0x50 reg1 = 0xa7 11965 write 0x50 reg2 = 0xb8 19745 read byte 0xa7, ACK 21575 read byte 0xb8, NACK
The ACK after the first byte makes the target send the next register; the NACK after the second ends the read.
Open the ready-made example in the lab and run the simulation.
Summary
- Model each open-drain line as an output enable plus a read-back input.
- Build every bit from four quarters: change SDA in the low half, sample at the end of the high half.
- START and STOP are SDA edges while SCL is high; START also serves as a repeated START.
- Wait for SCL to be really high to support clock stretching.
- Sending a 1 but reading a 0 means lost arbitration: release the bus.