Here’s a clean, FPGA-friendly way to drive I²C (SCL/SDA). The big ideas: I²C lines are open-drain (never drive “1”), both lines need pull-ups, and your FPGA must use tri-state (or vendor open-drain) outputs.
1) Hardware hookup (must-dos)
- Pull-ups: Add RPU from SCL/SDA to VDD (e.g., 3.3 V). Typical 2.2–10 kΩ.
- Quick sizing: 𝑅𝑃𝑈≈𝑡𝑟/0.8473𝐶𝑏𝑢𝑠 and also obey 𝑅𝑃𝑈≤(𝑉𝑃𝑈−𝑉𝑂𝐿(𝑚𝑎𝑥))/𝐼𝑂𝐿.
- Example: Fast-mode (300 ns), 𝐶𝑏𝑢𝑠=100 pF → 𝑅𝑃𝑈≈3.5𝑘Ω → choose 3.3–4.7 kΩ.
- Levels: Most FPGA I/Os aren’t 5 V-tolerant. For 5 V I²C, use a level shifter (e.g., PCA9306/BSS138).
- Routing: Keep SCL/SDA short and together; avoid heavy RC loading. Series 22–47 Ω near the FPGA is OK if edges ring.
2) Configure FPGA pins as open-drain
You implement open-drain by driving 0 for ‘0’ and high-Z for ‘1’. Read back the line to support clock stretching & arbitration.
Generic Verilog (vendor-agnostic)
// Top-level ports must be inout
inout wire SDA, SCL;
wire sda_i, scl_i; // sampled inputs
reg sda_oe, scl_oe; // 1 = release (Z), 0 = drive low
// Open-drain behavior
assign SDA = sda_oe ? 1'bz : 1'b0;
assign SCL = scl_oe ? 1'bz : 1'b0;
// Always read the lines (needed for stretch/arbitration)
assign sda_i = SDA;
assign scl_i = SCL;
Xilinx (recommended primitive)
wire SDA_i; reg SDA_oe; // oe=1 => release
OBUFT sda_buf (.I(1'b0), .O(SDA), .T(SDA_oe)); // drives 0 or Z
IBUF sda_in (.I(SDA), .O(SDA_i));
wire SCL_i; reg SCL_oe;
OBUFT scl_buf (.I(1'b0), .O(SCL), .T(SCL_oe));
IBUF scl_in (.I(SCL), .O(SCL_i));
Intel/Altera & Lattice: use ALT_OUTBUF_TRI / SB_IO (same concept: drive 0 or Z and always sample input).
3) Master timing essentials (bit-bang or soft IP)
- Start: While SCL is high, pull SDA low.
- Bit transfer (MSB first):
- For data ‘0’: set sda_oe=0 (drive 0); for ‘1’: sda_oe=1 (release).
- Pull SCL low → prepare bit → release SCL high → wait until scl_i==1 (handles clock stretching by slaves) → sample sda_i if needed → pull SCL low.
- ACK bit: Release SDA (oen=1) for the 9th clock, raise SCL, sample sda_i==0 for ACK.
- Stop: With SCL high, release SDA from low→high.
Tiny FSM sketch (ticks at 2× or 4× SCL):
case(state)
IDLE: if(start_req) begin sda_oe<=0; state<=START1; end // SDA low while SCL high
START1: if(scl_i) begin scl_oe<=0; state<=BIT_SETUP; end
BIT_SETUP: begin
sda_oe <= (tx_bit ? 1'b1 : 1'b0); scl_oe<=0; state<=BIT_HIGH;
end
BIT_HIGH: begin
scl_oe<=1; if(scl_i) state<=BIT_LOW; // wait for stretch to end
end
BIT_LOW: begin
scl_oe<=0; advance_bit_or_ack();
end
// ... ACK state (release SDA, clock high, sample sda_i) ...
STOP: begin scl_oe<=1; if(scl_i) begin sda_oe<=1; state<=IDLE; end end
endcase
4) Multi-master arbitration & stretching
- Never drive ‘1’. If you release SDA but read sda_i==0, you lost arbitration—abort and release bus.
- Always wait for scl_i==1 after you release SCL high; a slave may be stretching.
5) Bus speed generation
Make a tick from your system clock:
- Standard (100 kHz): period ≈ 10 µs.
- Fast (400 kHz): period ≈ 2.5 µs.
- Fast-mode Plus (1 MHz): period ≈ 1 µs (needs stronger pull-ups and low Cbus).
6) Glitch filtering & sampling
I²C tolerates short spikes; add a 2-FF synchronizer and an optional minimum-pulse filter (~50 ns) on SDA/SCL before using *_i in logic to avoid metastability/glitches.
7) Pin constraints (example Xilinx XDC)
set_property IOSTANDARD LVCMOS33 [get_ports {SCL SDA}]
# Open-drain behavior comes from OBUFT usage; disable internal pull-ups unless desired
set_property PULLUP false [get_ports {SCL SDA}]
set_property SLEW SLOW [get_ports {SCL SDA}]
8) Testing & bring-up tips
- Simulate pull-ups: tri1 SDA; pullup(SDA); (and same for SCL) so Z reads as ‘1’.
- If bus is stuck low, a slave may hold SDA; try toggling SCL ~9 pulses to free it.
- Verify rise time with a scope; if too slow, use lower RPU or reduce bus capacitance.
9) Don’t reinvent if you don’t need to
Most vendors provide I²C master IP:
Xilinx AXI IIC, Intel I2C Controller, Lattice I2C Master, plus solid open-source cores (e.g., OpenCores).
These already handle start/stop/ACK/stretch/arbitration; you still must wire pins as open-drain.

Top comments (0)