For some time now I’ve been working to improve the i2c capability of leJOS on the EV3. In particular to enable operation at a higher speed than the standard LEGO devices and to improve compatibility with devices using standard pull up resistors. Devices designed to work with the NXT/EV3 must operate at 9.6Kbps and have pull up resistors of around 70K Ohms, but standard devices typically operate at 100Kbps and have 10K Ohm pull ups. Although few leJOS users will get involved with things at this level I thought it might prove the basis of an interesting article.
The basic mechanism is simple software bit banging of the I/O lines, however there are a number of special considerations:
- When standard LEGO pull up resistors are used (70K) then the rise time for the clock and data lines can be very slow leading to spurious i2c stop/start events.
- Many devices when operating at this speed use clock stretching operations (the slave holds the clock line low until it is ready), so we need to ensure that this works.
- The protection resistors in the EV3 (4.7K) when combined with a standard i2c pull up resistor (10K) in effect forms a potential divider when the EV3 drives a pin low. This means that many devices will not detect the resultant voltage as a logic 0 value.
- Because we are running on Linux (and there is a lot of things going on, like network I/O etc.) the output pulse stream timing may get disrupted when interrupt routines run. Some devices do not seem to like this and can produce odd results.
To work around these problems the leJOS i2c driver uses the following techniques:
- To provide faster rise times the EV3 always drives the bus (rather then using open collector outputs). The output from the EV3 is always via a resistor and for this reason the technique is referred to as an active pull up. In effect the EV3 is applying an additional pull up voltage to the bus when it drives it high. The data line is driven high both when the EV3 is sending a logic level 1 output and at any time that the EV3 expects the slave to drive the bus.
- To ensure detection of pulse stretching operations even when using an active pull up the buffers that form part of the EV3 I/O system are used. These logic level buffers are connected to the same external pins as the normal logic level I/O lines and are intended for use for UART operations but have been re-used here to allow the i2c lines to be both driven and the actual logic level monitored at the same time. In the case of the clock line the UART output buffer is used to drive the line while the “normal” EV3 digital I/O line monitors the state looking for the line being held low. For the data line the EV3 digital line is used to drive the i2c output values while the UART buffer is used to read the input and to check for the slave holding the data lines low.
- The steps outlined above help when driving standard pull up resistors. In particular using the UART buffer (which has a lower value protection resistor) to drive the clock line means that there are no issues with the clock output. However some devices still fail to detect a logic 0 output on the data line. To ensure that such devices work correctly an external pull down resistor (value 3.3K) is used on the data line to shift the output to a lower level. This also lowers the logic 1 level seen by the slave but most devices still seem to operate correctly.
- This code disables interrupts while reading and writing byte values (9 bits in total) to ensure that the timing during the operation remains consistent. Interrupts are enabled between byte operations with the i2c clock held low. This means that interrupts are disabled for approximately 45uS at a time, but this does not seem to have caused problems.
To illustrate some of the issues let’s first take a look at part of a good i2c transaction. The first image is the output from my Saleae logic analyser when capturing i2c operations to a Dexter Industries IMU.
The top trace is the clock line, the lower the data. In this case we can see part of a transaction to perform an i2c read from a particular register on the gyro part of the device. The first part of the trace shows the EV3 writing the data register address (0xA7) to the device, followed by a start operation (the green circle) and then the EV3 sends a read request to the gyro address (0xD3). The trace looks pretty clean with nice crisp pulse edges etc. However sometimes tools do not show the whole picture. Let’s take a look at the same operation, but this time using an oscilloscope:
Now we get to see a more complete view of the actual signals being used. As you can see they are not quite so crisp! Firstly take a look at the signal level on the data line when being driven by the EV3 to logic level 0. In an ideal world this should be 0V but due to the problem mentioned in point 3 above it is actually around 0.5v (if you look carefully you will see that when the device drives the line to 0V then it is much lower). Now the Dexter device is designed to work with LEGO hardware and so uses 70K Ohm resistors, this problem is much worse when using 10K Ohm pull ups. Now let’s take a look at what happens if instead of driving the clock line we use the more traditional open collector drive:
Here the top trace is the clock signal when driven by an open collector drive (which means it is the pull up that drives the logic level 1 values). As you can see with the 70K Ohm pull up the rise time of the signal is rather slow (if something that takes only 5uS can be said to be slow!). It is certainly not a good clock signal. The lower trace shows the same signal when being driven using the “active pull up” technique mentioned above in item 1.
Interestingly this trace also shows the effect of the implementation of clock stretching mentioned in item 2. Normally the code on the EV3 will be monitoring the clock line looking to see if the slave is holding the line at logic 0. It does this by simply raising the clock line (via the UART buffer) and then reading the clock pin value via the normal I/O line (which is connected to the same output pin), the code loops waiting for the result to be a logic level 1 (or for a timeout). In this case this code is actually slowing down the operation a little resulting in the upper trace running slightly slower than the lower one because the code has to wait an extra 1 or 2 uS for the voltage on the pin to actually get to a value that is recognized as a logic 1! This can be clearly seen if you look at the first complete pulse on both traces. The pulse starts at the same time in both cases, but the upper trace does not drop to zero until slightly later.
So there we go, far more information about i2c on the EV3 than you probably needed to know, but I hope someone finds it of interest. The latest version of this code is present in the post 0.9.1 source base (there is an earlier high speed i2c implementation in 0.9.1 but it does not include all of the features mentioned here). I’ve tested this with a number of devices using both 10K and 70K Ohm pull up resistors and all seem to run happily at 100KHz. I intended to use this code in a number of future projects so it will be tested further as part of that work. If you have any questions or thoughts please post on the leJOS forum.