An I2C controller implemented in VHDL

I2C, I²C or Inter Integrated Circuit is a synchronous serial protocol designed by Philips in the early 1980s for linking together ICs on PCBs so that they can exchange data. It operates in the same area as SPI, which I’ve talked about in this blog a few times. I’ve also talked about I2C before though not for a long time.

I2C has the following general characteristics, in comparison to SPI:

  • Uses two signals, one for clock (SCL) and one for data (SDA) instead of 3
  • Reads and writes can’t happen at the same time
  • Generally operates slower then SPI in the 100KHz range
  • No Select pin on the devices unlike SPI
  • Instead the devices are selected using an address sent on the SDA pin, with the target recognising the address and signalling its acknowledgement
  • Both pins are bi-directional; the master device is the one driving the clock at any particular instant

I2C is considerably more complicated then SPI, for which implementing a controller is relatively trivial and can be accomplished with a couple of parallel to/from serial shift registers.

The best source of information on it is from NXP (previously Philips) themselves: the UM10204 (PDF) I2C-bus specification and user manual.

The platform for developing the I2C controller was MAXI00. It has three spare IO pins attached to each of the two FPGAs. Somewhat arbitrarily I chose the Alpha FPGA. As I had some handy, and have used them before, the principle target (slave) IC is a DS1307 (PDF) Real Time Clock.

Here’s a picture of the first test setup where the RTC was used. At this point both the oscilloscope and the logic analyser were attached to the SCL and SDA pins. The scope was attached to get an “analogue” view of the signals to investigate a problem I was having:

The circuit used is trivial and is straight from the DS1307 datasheet:

The backup battery was omitted, and in this case the datasheet states that the Vbat pin be grounded. Also, the SQW output wasn’t used.

Before starting to implement my own I2C controller in VHDL, I did look at existing solutions. There are several including this one on Digikey‘s site. I did look at that one for ideas and inspiration, though the code is my own. One thing I did borrow from this design is the external interface, though I did have to modify it somewhat since the Digikey controller is not really designed to be driven by a processor.

One thing I wanted to try to leverage in my design was I2C’s fairly rigid nature. Other controller implementation, for instance the one within an AVR mcirocontroller, require the programmer to drive the pins through each I2C state: start, slave address, reads and writes, stop, and handle the ACK bits in a fairly “manual” way. In contrast I wanted to try to make it easier for the programmer (me) who is driving the controller.

There are two layers to this design:

  • The low level controller itself
  • The interface between the controller and the microprocessor, ie. the registers presented

Both can be considered in relative isolation, but of course there is an overlap.

Here are the input and output connections for controller entity. These signals are manipulated as the processor accesses the presented registers:

The initial problem was generating a c. 100KHz clock for the I2C serial data stream. There is a interesting angle to this clock: it can’t just be, say, the processor clock divided down. The reason for this is the serial data should be clocked in or out as soon as the processor requests it, and not on the next edge of the 100KHz clock as this will delay the processor unnecessarily.

To accomplish this two synchronous processes were created, both clocked on the processor clock. The first process clocks a counter, the top most bit of which generates the approximately 100KHz I2C clock. This counter starts counting only after a trigger event (eg. the processor writing to the slave address register), which is active for exactly one processor clock cycle.

The actual I2C controller state machine is in the second process. As mentioned this process is also clocked on the processor clock, but only on the rising edge of the top most bit of the above described counter does it perform any action. Otherwise it does nothing.

The following is a state-transition diagram showing the behavior of this second process:

The chunks of state break down as follows:

  1. START1 and START2: The start waveform is generated, ie the SDA pin lowers while the SCL remains high.
  2. WRITING_DATA : The 8 bits of data are clocked out. The same state is used for sending the slave address as sending useful data. 16 input clocks are required, since the SCL output must cycle for each bit written.
  3. WRITING_ACK : The ACK or NAK from the slave is latched by the controller. Also the controller switches to either writing or reading waiting state based on the mode requested. If this is the last byte that has to be written (last_byte = 1), the controller enters the first STOP state.
  4. WRITE_WAITING : Here the state machine simply hangs around waiting for the next trigger event, upon which it will latch the write_data input for the next byte.
  5. READING_DATA : 8 bits of data are clocked in. 16 “state clocks” are required.
  6. READING_ACK : The controller generates an ACK (ie. it brings SDA low) if this is not the last byte that needs reading, otherwise it makes it high for a NAK. If this is the last byte to read then the controller then enters the first STOP state, otherwise it moves onto READ_WAITING.
  7. READ_WAITING : The controller waits for another trigger before entering READING_DATA.
  8. STOP1 through STOP3 : The stop sequence generator. SDA pin is raised while SCL remains high.
  9. RESTART1 : This is used to lower the SDA and SCL pins in preparation for sending a fresh START sequence, and is used by some slave ICs to remove the requirement of creating a new transaction by going through a stop/start sequence.

A further complication of I2C is that it is a shared bus. When not in use (or for reading), the SDA and SCL lines must be tristated and external pull-up resistors used to bring the lines high.  In practice this is achieved by simply tristating the SCL and SDA pins when are 1 would normally be output.

It’s important to note that the I2C logic I’ve built could be used in many environments, for example in circuits built out of custom programmable logic, not just as a processor’s peripheral, which is what I’m using it for here.

The registers exposed by the controller to the processor are as follows, though this may change in the future:

  • REG_I2C_ADDRESS : The I2C address of the slave which the master controller wants to communicate with. Writing to this register sets trigger to 1, sending the address. The top most bit is the read/write bit (the LSB in the actual stream, but formatting the data this way is easier to manage for the programmer)
  • REG_I2C_WRITE_DATA : Data to write. Writing here sends the data byte
  • REG_I2C_READ_DATA : Data that has been read. Somewhat oddly, a write to this register triggers a read cycle. After the controller reports it is no longer busy the byte can then be read at this register
  • REG_I2C_CONTROL : Only one control bits is currently defined, and it’s value should be set before writing to either REG_I2C_READ_DATA or REG_I2C_WRITE_DATA:
    • 0 : Last byte. A 1 indicates that this is the last byte to write or read

The REG_I2C_CONTROL (aliased to REG_I2C_STATUS) is also used for reading, and two bits are defined:

  • 0 : A high value indicates the controller is busy
  • 1 : A high value indicates the slave has sent a NAK. This is used for various things, including an invalid slave address, or the slave is not currently ready

From the processor’s point of view, generating a write request to a specific slave consists of:

  1. Setting the I2C address in the low 7 bits of REG_I2C_ADDRESS, and keeping the MSB low to indicate a write
  2. Writing to REG_I2C_CONTROL with 0
  3. Waiting for the controller not to be busy (it is sending the address)
  4. For each byte to send that isn’t the last one:
    • Setting REG_I2C_WRITE with the byte to send
    • Waiting for the controller not to be bus
  5. For the last byte:
    • Writing REG_I2C_CONTROL with 1
    • Continuing as above

A read operation is as follows:

  1. Setting the I2C address in the low 7 bits of REG_I2C_ADDRESS with the MSB set to 1.
  2. Writing to REG_I2C_CONTROL with 0.
  3. Waiting for the controller not to be busy
  4. For each byte to receive that isn’t the last one:
    • Writing any value, but a 0 is the usual, to REG_I2C_READ.
    • Waiting for the controller not to be busy
    • Writing the value at REG_I2C_READ to a local buffer
  5. For the last byte:
    • Writing REG_I2C_CONTROL with “1”
    • Continuing as above.

The peculiar way that a register with READ in its name must be written to is to keep the user code as simple as possible. Triggering a read operation through reading the register was looked at, but it would have required an intiail dummy read and generally results in unpleasent looking client code. This is mostly caused by having to cater for the special case of handling the final byte.

After initially struggling with implementing the controller directly into the Alpha FPGA within MAXI000, I switched to simulation. Previously I’d used ModelSim, but after become increasingly frustrated with its 1990s UI, I switched to GHDL.

GHDL is a pretty complete VHDL compiler/simulator, though annoyingly it lacks a few few VHDL-2008 (PDF) features.

The testbench for the controller is used to produce waveforms for the SDA and SCL outputs when it is exposed to inputs mimicking what the processor would do when it accessed the defined registers.

Here is a section of the testbench. It sets a write of a single byte (0x0f) after setting the address to 1101000, with appropriate delays to wait for the controller to become idle after each operation:

               i2c_reset <= '1'; 
               i2c_trigger <= '0'; 
               wait for 2 ns; 

               i2c_reset <= '0'; 
               i2c_address <= "1101000"; 
               i2c_read_write <= '0'; 
               i2c_write_data <= x"00"; 
               i2c_last_byte <= '0'; 
               i2c_trigger <= '1'; 

               wait for 2 ns; 
               i2c_trigger <= '0'; 

               wait until (i2c_busy = '0'); 
               wait for 1 us; 

               i2c_write_data <= x"0f";
               i2c_last_byte <= '1'; 
               i2c_trigger <= '1'; 
               wait for 2 ns; 
               i2c_trigger <= '0'; 

               wait until (i2c_busy = '0'); 
               wait for 1 us;

The resultant waveforms can be viewed in GtkWave:

The testbench does not attempt to generate the slave’s signals. This is why the error state stays undefined.

Hooking the controller back into the Alpha FPGA, after seeing it generating looking reasonable waveforms in the simulator, was pretty easy. The registers were defined and the controller interface was maniupluated when they were written too, eg. the trigger input was set for the clock cycle where the REG_I2C_ADDRESS, REG_I2C_READ_DATA, or REG_I2C_WRITE_DATA registers were written to. Likewise the Alpha FPGA asserts the read_data controller signal onto its external databus when REG_I2C_READ_DATA is read.

In summary, the controller appears to function well. I have extended my machine code monitor with both low level and high level, specifically for read and writing to an EEPROM, commands.

Here is a capture from the logic analyser:

This capture shows the uses of restarts. The  AT24C64 (PDF) 8KB EEPROM attached uses them to speed up (slightly) a read operation, the two byte memory address can be written and followed immediately by a restart – the middle green dot – whereupon the memory contents can be read out.

All in all I’m very happy with my I2C controller. It should serve me well in the MIDI020 board. It does have a few missing features, particularly the ability to honor a slave which wants to stretch the clock, but is none the less pretty cool, I think. I’ve uploaded the code to github for anyone interested.

In the meantime the MIDI020 boards have arrived, and I have started construction. As a tease, here is the Test and SRAM board, which in this case was wired up to MAXI000 so I could play with the AT24C64 EEPROM on it:

After wiring this up, I immediately noticed a problem: the SCL and SDA pins are swapped. This problem exists on the Ethernet + Joystick + Printer port board as well. Very annoying, though it will be fixable with a bodge wire.

The build of MIDI020 can now begin…

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.