0
0
VhdlHow-ToIntermediate · 4 min read

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.