From Hamsterworks Wiki!

Jump to: navigation, search

This FPGA Project was started in May 2013.

I2C is rapidly becoming the standard way to talk to low data rate sensors and other devices. FPGAs have no problem talking I2C, but designing a custom state machine to drive every device is painful. When more than one device shares the I2C bus this can become quite challenging.

With a clock rate of under 1MHz, I2C is a relatively slow use for using an FPGA's speedy logic, and to make things worse most sensors require delays of maybe a millisecond or longer. It makes little sense to build on-off super complex high speed FSMs to support such needs, making a software driven solution using a soft micro-controller an attractive alternative.

My Intelligent I2C controller is not a full micro-controller, but a simple programmable state machine, using about the same FPGA resources as a Picoblaze processor.


Advantages over more generic controller such as a Picoblaze is that is is designed especially for configuring and accessing I2C devices, where as the Picoblaze will need either a software I2C implementation or an added I2C peripheral.


Binary Outputs

The controller has 16 binary outputs. The controller can independently set and clear each of the output signals.

Binary Inputs

The controller has 16 binary inputs. The controller test the status of the input signals.

As a way to supply data from the rest of the FPGA fabric to the an I2C device the upper or lower 8 input signals can be written over the I2C bus. This could be extended by using the binary outputs to drive the select inputs of a mux, allowing the controller to choose which entries from a bank of registers is presented on a set of eight inputs signals.

Program Memory

The controller support up to 1024x9-bit program memory. This fills half an Xilinx Block RAM, allowing two I3C2 instances to share a single BRAM resource.

The only restriction is that the destination of jumps must be aligned to 8-word boundaries - giving only 64 target addresses

Register outputs

The controller can read words from the I2C device into one of thirty two registers. This is achieved through an 8-bit data port, a 5-bit address power and a single-bit write enable port.

The controller's instruction set

Opcode Instruction Action
00nnnnnnn JUMP m Set PC to 'm' (n = m/8)
01000nnnn SKIPCLEAR n Skip if input 'n' clear
01001nnnn SKIPSET n skip if input 'n' set
01010nnnn CLEAR n Clear output 'n'
01011nnnn SET n Set output 'n'
0110nnnnn READ n Read from the I2C bus to register 'n'
01110nnnn DELAY m Delay 'm' clock cycles (n = log2(m))
011110000 SKIPNACK Skip if NACK is set
011110001 SKIPACK Skip if ACK is set
011110010 WRITELOW Write inputs 7 downto 0 to the I2C bus
011110011 WRITEHI Write inputs 15 downto 8 to the I2C bus
011110100 USER0 User defined
011111011 USER7 User defined
011111101 MASTERACK Send a Master ACK with the next read
011111110 NOP Do nothing
011111111 STOP Send Stop on i2C bus
1nnnnnnnn WRITE n Output 'n' on I2C bus

I2C transactions

An I2C transaction starts with the first WRITE instruction, and is held open until a STOP instruction is executed. Please note - failure to execute the STOP will lock the I2C bus completely!

Example project

Here is example code required to read data from a 3D Compass sensor

; Reading from the HMC5883L Compass (I2C device ID is 3C / 3D
	WRITE 0x3C ; Switch to continuous measurement mode
	WRITE 0x02
        WRITE 0x00

reread: WRITE 0x3C ; Start Write transaction
        WRITE 0x03 ; Pointer to Reg 3

        WRITE 0x3D ; Start Read transaction
        READ  0	   ; Reg 3 - X, most significant byte
        READ  1    ; Reg 4 - X, least significant byte
        READ  2    ; Reg 5 - Y, most significant byte
        READ  3    ; Reg 6 - Y, least significant byte
        READ  4    ; Reg 7 - Z, most significant byte
        READ  5    ; Reg 8 - Z, Least significant byte (no MACK on last read)
	SET   0	   ; Indicate that new, consistent data is available
        DELAY 32768 ; Wait for approx 1/10th of a second
        JUMP reread	

Save this code into "test_code.i3c2" and then compile it with the i3c2_assembler (code for which is below). This will generate a "test_code.vhd" file that can be added to your project.

Here is the top level VHDL module to go with this design. It captures the data being read from the sensor, and displays eight bits on the dev board's LEDs:

-- Engineer: Mike Field <hamster@snap.net.nz> 
-- Create Date: 20:18:57 05/26/2013 
-- Module Name: i3c2_test - Behavioral 
-- Description: The top_level module for testing the I3C2 processor 
--              with the HMC5883L 3-axis digital compass IC
-- Revision 0.01 - File Created
library IEEE;

entity i3c2_test is
    Port ( clk       : in    STD_LOGIC;
           scl       : out   STD_LOGIC;
           sda       : inout STD_LOGIC;

           switches  : in  STD_LOGIC_VECTOR (7 downto 0);
           btn       : in   STD_LOGIC_VECTOR (3 downto 0);
           led       : out  STD_LOGIC_VECTOR (7 downto 0);
           i2c_scl   : out std_logic := '1';
           i2c_sda   : inout std_logic := 'Z';
           error     : out   STD_LOGIC);
end i3c2_test;

architecture Behavioral of i3c2_test is
   COMPONENT test_code
      PORT( clk     : IN  std_logic;
            address : IN  std_logic_vector(9 downto 0);          
            data    : OUT std_logic_vector(8 downto 0));

	COMPONENT i3c2 GENERIC( quarter_bit_length : std_logic_vector(5 downto 0));
      PORT( clk          : IN    std_logic;
            inst_data    : IN    std_logic_vector( 8 downto 0);
            inputs       : IN    std_logic_vector(15 downto 0);    
            i2c_sda      : INOUT std_logic;      
            inst_address : OUT   std_logic_vector( 9 downto 0);
            i2c_scl      : OUT   std_logic;
            outputs      : OUT   std_logic_vector(15 downto 0);
            reg_addr     : OUT   std_logic_vector( 4 downto 0);
            reg_data     : OUT   std_logic_vector( 7 downto 0);
            reg_write    : OUT   std_logic;
            error        : OUT   std_logic);

   signal data              : std_logic_vector(8 downto 0);
   signal address           : std_logic_vector(9 downto 0);
   signal inputs, outputs   : std_logic_vector(15 downto 0);
   signal reg_write         : std_logic;
   signal reg_data          : std_logic_vector(7 downto 0);
   signal reg_addr          : std_logic_vector(4 downto 0);
   signal temp_mag_x, mag_x : std_logic_vector(15 downto 0);
   signal temp_mag_y, mag_y : std_logic_vector(15 downto 0);
   signal temp_mag_z, mag_z : std_logic_vector(15 downto 0);
   inputs(3 downto 0)  <= btn;
   inputs(15 downto 8) <= switches;
   led  <= mag_x(11 downto 4);

      if rising_edge(clk) then
         -- Save the register values
         if reg_write = '1' then
            case reg_addr is  
               when "00000" => temp_mag_x(15 downto 8) <= reg_data;
               when "00001" => temp_mag_x( 7 downto 0) <= reg_data;
               when "00010" => temp_mag_y(15 downto 8) <= reg_data;
               when "00011" => temp_mag_y( 7 downto 0) <= reg_data;
               when "00100" => temp_mag_z(15 downto 8) <= reg_data;
               when "00101" => temp_mag_z( 7 downto 0) <= reg_data;
               when others  => null;
            end case;
         end if;
         -- Present the values to the outputs when output(0) is set
         if outputs(0) = '1' then
            mag_x <= temp_mag_x;
            mag_y <= temp_mag_y;
            mag_z <= temp_mag_z;
         end if;
      end if;
   end process;
i3c2_code: test_code PORT MAP (
      clk     => clk,
      address => address,
      data    => data
i3c2_controller: i3c2 GENERIC MAP ( 
      -- Set I2C clock to 50Mhz/63/4 = 198kHz
      quarter_bit_length   => std_logic_vector(to_unsigned(63,6))  
   ) PORT MAP (
      clk          => clk,
      inst_address => address,
      inst_data    => data,
      i2c_scl      => i2c_scl,
      i2c_sda      => i2c_sda,
      inputs       => inputs,
      outputs      => outputs,
      reg_addr     => reg_addr,
      reg_data     => reg_data,
      reg_write    => reg_write,
      error        => open
end Behavioral;

An assembler

File:I3c2 assemble.c is a simple assembler, contained in a signal source file and only required C's Standard Library.

Once compiled into an executable programs for the I3C2 can be compiled with "i3c2_assemble filename.i3c2".

This will produce the following files:

  • filename.lst - A listing of the compiled code (not yet implemented)
  • filename.coe - A '.coe' file for use with Xilinx's memory generator
  • filename.vhd - A generic VHDL source file which can be used to infer the program ROM.

This is a work in progress

VHDL source files for the controller

You can find the latest source for the I3C2 here:


And here are the constraints I use with my development board:


Personal tools