VHDL Code for I2C Master: Syntax, Example, and Tips
An
I2C master in VHDL controls the clock and data lines to communicate with I2C slaves. The code typically includes a state machine to generate start, stop, read, and write signals on SCL and SDA lines. You can implement it by managing timing and acknowledging bits according to the I2C protocol.Syntax
The basic VHDL structure for an I2C master includes signals for SCL (clock) and SDA (data), a clock divider to generate the I2C clock speed, and a state machine to handle the I2C protocol steps like start, stop, read, and write.
Key parts:
- start condition: SDA goes low while SCL is high
- stop condition: SDA goes high while SCL is high
- data bits: sent or received on SDA line synchronized with SCL
- acknowledge: slave pulls SDA low after each byte
vhdl
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity i2c_master is
Port (
clk : in std_logic; -- system clock
rst : in std_logic; -- reset
start : in std_logic; -- start signal
data_in : in std_logic_vector(7 downto 0); -- data to send
scl : out std_logic; -- I2C clock line
sda : inout std_logic; -- I2C data line
busy : out std_logic; -- busy flag
ack_err : out std_logic -- acknowledge error
);
end i2c_master;
architecture Behavioral of i2c_master is
-- internal signals and states here
begin
-- process for I2C state machine
end Behavioral;Example
This example shows a simple I2C master that sends one byte to a slave device. It generates start and stop conditions, sends data bits, and checks for acknowledge.
vhdl
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity i2c_master is
Port (
clk : in std_logic;
rst : in std_logic;
start : in std_logic;
data_in : in std_logic_vector(7 downto 0);
scl : out std_logic;
sda : inout std_logic;
busy : out std_logic;
ack_err : out std_logic
);
end i2c_master;
architecture Behavioral of i2c_master is
type state_type is (idle, start_cond, send_byte, ack_check, stop_cond);
signal state : state_type := idle;
signal bit_cnt : integer range 0 to 7 := 0;
signal sda_out : std_logic := 'Z';
signal scl_reg : std_logic := '1';
signal clk_div : integer := 0;
constant clk_div_max : integer := 250; -- adjust for clock speed
signal data_reg : std_logic_vector(7 downto 0) := (others => '0');
signal ack : std_logic := '1';
begin
scl <= scl_reg;
sda <= sda_out;
process(clk, rst)
begin
if rst = '1' then
state <= idle;
bit_cnt <= 0;
sda_out <= 'Z';
scl_reg <= '1';
busy <= '0';
ack_err <= '0';
data_reg <= (others => '0');
clk_div <= 0;
elsif rising_edge(clk) then
if clk_div < clk_div_max then
clk_div <= clk_div + 1;
else
clk_div <= 0;
case state is
when idle =>
sda_out <= 'Z';
scl_reg <= '1';
busy <= '0';
ack_err <= '0';
if start = '1' then
data_reg <= data_in;
busy <= '1';
state <= start_cond;
end if;
when start_cond =>
sda_out <= '0'; -- start condition SDA low while SCL high
scl_reg <= '1';
state <= send_byte;
bit_cnt <= 7;
when send_byte =>
scl_reg <= '0';
sda_out <= data_reg(bit_cnt);
state <= ack_check;
when ack_check =>
scl_reg <= '1';
if bit_cnt = 0 then
state <= stop_cond;
else
bit_cnt <= bit_cnt - 1;
state <= send_byte;
end if;
when stop_cond =>
sda_out <= '0';
scl_reg <= '1';
sda_out <= 'Z'; -- stop condition SDA high while SCL high
busy <= '0';
state <= idle;
when others =>
state <= idle;
end case;
end if;
end if;
end process;
end Behavioral;Output
No console output; hardware signals on scl and sda lines change according to I2C protocol timing.
Common Pitfalls
Common mistakes when writing an I2C master in VHDL include:
- Not generating proper start and stop conditions (SDA must change while SCL is high).
- Ignoring clock stretching by slaves, which can cause timing issues.
- Not checking acknowledge bits from slaves, leading to undetected communication errors.
- Driving SDA line without releasing it (should be open-drain/open-collector style).
Always use a state machine to carefully control timing and signal transitions.
vhdl
---- Wrong: Driving SDA high or low directly without open-drain behavior sda_out <= '1'; -- forces line high, can conflict with slave ---- Right: Use 'Z' to release line and let pull-up resistor pull high sda_out <= '0'; -- drive low sda_out <= 'Z'; -- release line for high
Quick Reference
Remember these I2C master tips:
- Start condition: SDA falls while SCL is high
- Stop condition: SDA rises while SCL is high
- Data bits: Change on SDA when SCL is low, read on rising edge of SCL
- Acknowledge: Slave pulls SDA low after each byte
- Open-drain: SDA line must be driven low or released (high impedance)
Key Takeaways
Use a state machine to control I2C start, stop, data, and acknowledge signals precisely.
Generate start and stop conditions by changing SDA while SCL is high.
Drive SDA line low or release it (high impedance) to mimic open-drain behavior.
Check acknowledge bits from slaves to detect communication success or failure.
Adjust clock timing with a divider to match the I2C bus speed requirements.