During my studies at university, the low level aspects of implementing I2C was not required, and I was too busy (or lazy) to look at the details of the bus. In interviews, when asked, “Do you know I2C?”, I would smile and nod, but deep inside, that hole, where I2C knowledge needed to be filled, grew. Sleepless nights, plagued with excessive drinking of caffeine and lamentation of the deeper meaning of I2C led me to embark on a journey, nay a discovery, of what I2C really is, and how to make a master module.
I2C, or Inter-integrated Circuit, is a two line bus that allows multiple masters to communicate with multiple slaves. The lines used include SCL ( or clock), SDA (or data), and it holds both of these lines high for idle operation. The start, stop, and data levels are all relational to the state of SDA and SCL, specifically the value of one line as another is changing.
For a start condition, the SCL line is high, while the SDA line is driven low. Stop conditions reverse the SDA change, driving it from low to high when SCL is high. All bit transmission requires SDA to change while SCL is low, so that neither condition is triggered accidentally. For a regular transmission of I2C, look at the below graph.

Great. We have a visual representation of how bytes should be sent, in what order, and how to do write and read conditions. If you’re a sane person, you’ll just make a satisfied nod, as you use whatever API function available to write and read I2C data, and stop reading this post. However, if you’re a masochist, you might ask yourself, “How could I implement this in Verilog?”
Welcome dear masochist, here we begin on a quest! To start our epic adventure, we need to set up our goal: developing an I2C master module for a slave device. I’m using the ADT7420, since it’s a temp sensor on my development board. In the specification sheet of your target device, there should be an “I2C Timing Specification”, which tells the developer how long to hold the SDA line per bit, and the minimum times needed to wait before generating SCL after a start condition, ect. Below is a screenshot from the ADT7420 Specification sheet, visually showing it’s different time based holds and setup times for I2C information.

All those barely readable labels next to the arrows are associated with a table, denoting the minimum, and maximum times for clock duration, holding data lines for a bit, and start and stop conditions. Additionally, it tells the developer the minimum and maximum speed for SCL. Since I’m already painted head to toe in red, I know this needs to go blazing-ly fast (WH40K Orks reference anyone?) so lets choose a 400KHz SCL speed.
Since all the timing requirements are based on a time unit (usually microseconds for my device) we need to set a relationship between a counter, and the time it takes for it to increment. I lucked out on my board, with a 100MHz reference clock, it’s easy to divide it to 400KHz. If you’re iffy on the math, to find the amount of clocks needed to go from one clock speed to another, notice 100MHz means 100*10^6 /second, with 400KHz meaning 400*10^3/ second. If you invert 400KHz, and multiply it with 100MHz, you will find the number of clocks needed to convert between the two speeds (250 clocks). Note that this calculated time describes the number of clocks in 100MHz, at which a full period would have occurred in a 400KHz clock. A period consists of the full rise and fall time, meaning we must divide our found number by 2, and invert the output 400KHz clock at these 125 clock markers.
always@(posedge i_clk or negedge reset_n) begin
if(!reset_n)
{clk_i2c_cntr, clk_i2c} <= 17'b1;
else if(!en_scl)
{clk_i2c_cntr, clk_i2c} <= 17'b1;
else begin
clk_i2c_cntr <= clk_i2c_cntr + 1;
if(clk_i2c_cntr == DIV_100MHZ-1) begin
clk_i2c <= !clk_i2c;
clk_i2c_cntr <= 0;
end
end
end
Notice that for the above code, an enable is utilized in conjunction with the async reset, as we don’t want our SCL line to continuously be running. We need to know the state of SCL in order to safely operate a state machine around it’s high and low state. Notice here the initial conditions, when driving the SCL line, I’m keeping it high, with the counter equal to 0. This is to ensure that when we are starting I2C, the slave won’t see the SCL line pulled low before a start indication occurs.
With the 400KHz clock written with an enable, we can construct a method to send and receive information from the slave per posedge of the input 100MHz clock. Note that 400KHz is much slower than 100MHz, so we’ll need a few registers to signify when the SCL line has gone high or low.
always@(negedge i_clk or negedge reset_n) begin
if(!reset_n) begin
{sda_curr, sda_prev} <= 0;
{scl_curr, scl_prev} <= 0;
end
else begin
sda_curr <= {sda_curr[0], sda_o}; //2 flip flop synchronization chain
sda_prev <= sda_curr[1];
scl_curr <= clk_i2c;
scl_prev <= scl_curr;
end
end
Note, that for SDA, there is a shift register with the sda_o line, which is the modules input pin for SDA. Since we aren’t synchronous to the data on SDA line when receiving information from slave, we need to take meta stability into account. For the SCL, we are taking this information from a register already synchronous to i_clk, meaning we don’t need to have any synchronization code.
Using the previous and current state, we can write a quick if statement determining if the line has gone high, or gone low. Once that occurs, monitor the clk_i2c_cntr for the time needed to meet timing constraints with the slave device, and then move to the next state required by the FSM.
The rest of the code is just state machine maintenance, and simulation testing to ensure you didn’t make any terrible mistakes to cause your FPGA to silently scream into the abyss. If the coding gods don’t smile upon you, or if you just don’t want to deal with coding up all the state transitions and input outputs, here is a link to my implementation: