I2C in a Nutshell
interrupt.memfault.com
interrupt.memfault.com
If anyone is wondering how to access an I2C bus from a Linux computer, say, a raspberry pi:
int fd = open("/dev/i2c-1", O_RDWR);
ioctl(fd, I2C_SLAVE, [slave address here]);
Then you can read() and write() to the device with the kernel taking care of all the transmission details. Usually all that's exposed is a few bytes for the registers. To set a register, write two bytes: first the register address, and then the value. To read a register, write the register address and then read a byte. Most of the devices have linear address spaces, so reading out multiple registers is as simple as reading multiple bytes.
The i2c-tools package has some very handy CLI tools for exploring an I2C bus.
Electrically, the bus is an open-collector design on both ends, so devices can only pull the lines to low, and they release them to set them high. Don't forget pull-up resistors!
https://www.raspberrypi.org/forums/viewtopic.php?p=1484482#p...
Apparent confirmation here (by mapping a different I2C bus?):
https://www.raspberrypi.org/forums/viewtopic.php?p=1544123&s...
The hardware inside those logic analyzers is fascinating in its own right, too!
https://www.youtube.com/watch?v=dobU-b0_L1I
That, and a cheapo Saldae clone might be all I need for learning how to use a logic analyser finally. :)
SPI is much easier to write correctly, and pretty much only has the extra wire (usually not a problem) and the phase polarity issues as a negative point.
One more advantage of SPI that I see is higher data rates, full-duplex communication etc..
It's quite annoying because one stuck device can block all other communication on the bus. Let's hope your reset pin (if you have it), isn't on an i2c expander on the same i2c bus.
* No way to safely bring the bus back to idle from mid-transaction. By "safely" I mean not accidentally transmit an extra byte which could e.g overwrite EEPROM memory. There is no combination of transitioning the 2-wire bus from an arbitrary state back to idle which works in the general case. If it's important, you end up adding a dedicated reset wire.
* No safe, universal way to bring the bus from tristate, or low, to pulled-up. There are designs where this ends up being necessary. You end up with a spurious transaction, which may wedge the bus, or having to add a reset wire or buffer.
* The protocol is extremely hostile to devices with non-zero latency response. It's designed as a simple "Address this register and then immediately read out a byte in the next clock cycle". Works great for trivial devices, but for anything more complex it ends up needing a bank of register acting as a "proxy" to access the higher latency side of the chip. At this point I2C is an awesomely bad choice, but people keep doing this, because it's so universal.
These are great points, and I'll add a note about them in the article. Thanks!
> The protocol is extremely hostile to devices with non-zero latency response. [...]
Technically, this is what clock-stretching is for. In practice, you're right that complex devices implemented proxy registers. I've seen it on DP->MIPI bridges for example.
Why would you want to do that? Not having the ability to do this is part of the contract. If you design your device such that it always completes the transaction, then there should be no problem, unless one of the devices on the bus doesn't play fair but then you have a different problem.
For example, you get a kernel panic, or soft-reset for some reason. When you recover, you now have a bus in an unknown state, possibly mid-transaction, and if you pick the wrong order in which to bring the bus back to idle, you might wedge it or accidentally cause a side-effect (e.g overwrite a byte in an EEPROM).
You may think of the extra pins as not a problem, but on every product I've worked on we've been pin-limited on the MCU.
[1] https://www.maximintegrated.com/en/design/technical-document...
Obviously that requires more hardware on the board tho'.
In practice, I don't see that many chips offering 7 bits of address configuration. You buy a chip, it has a hardwired address. Maybe a pin or two for selecting another address.
I agree it's still lower pin count than SPI, but realistically you don't get anywhere near 127 devices.
I've also seen JTAG as an alternative to I2C and SPI. JTAG can be part of normal operation.
I mitigated the issues by doing I2C resets on every read (see the Analog Devices I2C reset documentation linked in another comment) and reading multiple times to filter out any spurious bit errors.
Other than that I just have my fingers crossed that a bit doesn't accidentally flip and overwrite something in the RTC. Or that the bus somehow gets stuck driven between sleeps and drains the battery early. sigh SPI would have been so much nicer.
(1) I mean, okay, the massive lithium battery exploding is perhaps the most nerve racking component, but I2C is a close second.
I'm not sure that I buy that. A daisy chained SPI bus is a rats nest of configuration hassle and basically impossible to debug. You're trading hardware robustness for software complexity, and that's not always a win.
And as far as hardware messes: SPI doesn't synchronize anything beyond the bit level (modulo an out of band chip enable or reset, of course, which would work to "fix" an I2C device too), making shift/offset bugs a real danger.
Board-level digital interfacing is just hard. That's why we do it as little as possible and put everything on the SoC these days.
Granted, that's not easy to do if your peripherals have no reset pin or their power source cannot be switched by the bus master.
Would your problems go away if you had I2C plus a dedicated reset pin on all peripherals?
I try to avoid it as much as possible. If I need a peripheral, I prefer SPI, if I need a bus for connecting devices I control, I use CAN. If I'm connecting two systems together, a UART.
>I2C is not appropriate for all applications however: When higher bandwidth is required, SPI may be the right choice and can be found in many NOR-flash chips. MIPI can go even faster, and is often used in displays and cameras. If reliability is a must, CAN is the bus of choice. It is found in cars and other vehicles. When a single device is on the bus, UART may work just as well.
That said, with Classical CAN you can implement an autobaud (better: automatic bit rate detection) like mechanisms when you can make some assumptions on the used bit rates. With CAN FD and the upcoming CAN XL you cannot do that.
PS: Baud is a term specifically applying to communication systems that transmit symbols and a symbol can represent more than a bit. That is why I2C, SPI, LIN, CAN, Ethernet have a bit rate. While RS232 has a baudrate, which is different from the bit rate depending on the type of symbol used.
The board works with various apps and frameworks, including OpenOCD and CircuitPython.
Full build details and some other resources are here:
I2S is everywhere in audio.
I find handwaving recommendations like this rather pervasive and annoying, especially in a professional setting.
If you're serious about I2C after gratifying yourself with this bootcamp-style smashbang intro, I highly recommend reading the actual spec[1] (which is more like a casual app note IMHO); the blog apparently doesn't link to it. It's free, relatively short, and oh by the way, there's an entire section which properly addresses pull-up resistor sizing and then some. You'll also be able to spot inaccuracies like:
> It has transfer rates up to 400Kbps
I'm designing my first circuit board and I'm wondering if I should bother with JST connectors or just use header pins.
https://www.digikey.com/product-detail/en/w-rth-elektronik/6...
This is the type of connector most commonly used in hobby electronics, such as on the Arduino and Raspberry Pi. 0.1" is the 'pitch', the distance between pins. I picked a 1x4 (meaning <rows> x <pins> or <rows> x <pins per row>) version, but they come in many different pin and row counts.
The version above is very simple, just bare pins that would get soldered to a PCB. There are many that come with additional 'features' that could be useful. For instance, you could get a locking header that has a tab to retain the connection against vibration or tugging. Or you can get a key or shroud that forces the connector to be inserted in the correct orientation. They also come in versions that support soldering directly to a PCB, vertically or at a right angle, or connecting to wires directly.
Here is a locking header:
https://www.digikey.com/product-detail/en/molex/0022232041/W...
If you scroll down to 'mating products' you can find wire-to-board connectors designed to mate with it.
First, if you have a small number of pins (up to about 4-6), a 2x2 or 2x3 .100 header isn't that much larger than any alternative. Compare this: https://www.tag-connect.com/product/tc2030-fp-footprint to a 2x2 of .100 headers. It's actually bigger, and now you need a special cable instead of that bag of .100" jumper wires you have.
If you have something like 20 genuinely used pins (not 6 active and 14 unused), okay, you may need a different connector. But are you really sure about this? 20 pins communicating simultaneously has signal integrity needs and small connectors have WAY more coupling than .100" spacing.
Second, through-hole is always way more stable than no-through hole. Once you give your smaller pitch connector through holes, is it really smaller than .100"?
Third, manufacturers have no problems with .100" headers. Smaller pitches may increase the cost of your board. Try costing out a board that can mount and route a modern USB-C connector which has both surface mount and through-hole at small pitch. You're probably going to get a cost bump.
Fourth, you can buy really long .100" headers which allow you to conect to them and put a scope probe underneath. That's really convenient for debugging.
So, go through-hole .100" header until you've got a good reason otherwise.
Then I start looking into various kinds of connectors, and wonder if I'm overthinking it.
I can solder a set of pins (or pogo pins) on a .100 spacing and mate into the .100 header holes. Or I can offset the .100 holes very slightly so they friction grab a .100 2x2 male header in the holes.
Or I can buy a breakout board that does the same thing for $7.00 for 2 rather than $50 per cable like Tag-Connect: https://www.pcbway.com/project/gifts_detail/PogoProg_Model_D...
.100" headers are hard to beat for general effectiveness.
As for other connectors: for Li-Ion batteries I use JST-PH connectors, standardizing on the pinout used by AdaFruit for batteries they sell.
For any custom "future extension" type connectors with I2C or SPI: 0.1" header holes. I can use them to either solder wires or place pin headers if needed.
One thing to watch out for: i2c is not designed for long wires, so you may have signal integrity issues beyond a 1-2 feet. You'll want to switch to differential signaling beyond that (e.g. with https://www.sparkfun.com/products/14589).
Or GX12-4, if you want something a little smaller.
In electronics, there is nothing better than a good piece of wire soldered between two boards. Connectors are a convenience feature.
The only things I take exception in the article to is:
> When a single device is on the bus, UART may work just as well.
The problem with UART is clock drift. It's remarkably easy for your two chips to get out of sync if they don't use crystals and don't have autobaud (normally rare). That is one thing that I2C, SPI, and CAN do better. They either don't care as they have a single master clock (I2C and SPI) or they autobaud detect and then adjust (CAN).
I2C was easy enough to understand, but understanding the obscure ways to configure and speak to a device has been really challenging. Spending a ton of time reading datasheets and experimenting with assembling binary messages.
The next time you are frustrated and unhappy with the state of modern web development (REST and/or GQL) take a ublox GPS chip for a spin and try to get it to give you high frequency location data. You will think, hey gee this isn't so bad after all compared to encoding and decoding a binary protocol by hand.
Here is the library you would use to do this via Elixir, https://github.com/elixir-circuits/circuits_gpio
That's embedded development in a nutshell. The only part you're missing is wasting a week of your life tracking down a compiler heisenbug, because embedded devices have niche, poorly maintained compilers.
Though to be honest it's a matter of taste; natural sadists tend to "enjoy" embedded.
Or spending three weeks trying to achieve timing closure on a design, only to finally realize after much inspection of the routed designs by myself and an IntelFPGA FAE that the router was smoking digital crack the whole time and had no clue how to route their own divider units?
Or maybe the programming facility reversed bit ordering on a batch of the FPGA's flash chips and you only learn of that after a very, very long couple of nights of language barrier back and forth with a flummoxed customer.
The joys are endless.
(Also: needs one fewer wire)
----- During the initialization sequence the bus master trans- mits (TX) the reset pulse by pulling the 1-Wire bus low for a minimum of 480µs. -----
There is no maximum time limit for the reset pulse.
----- The bus master then releases the bus and goes into receive mode (RX). When the bus is released, the 5kΩ pullup resistor pulls the 1-Wire bus high. When the DS18B20 detects this rising edge, it waits 15µs to 60µs and then transmits a presence pulse by pull- ing the 1-Wire bus low for 60µs to 240µs. -----
A tolerance of 15uS to 60uS on the device side and 60uS to 240uS on the driver side seems pretty wide to me.
Now, it's hard to actually get a good figure for these specifications because different datasheets give different values, which further suggests the tolerances are large.
Another datasheet that I have here says that the presence pulse should be sampled after 72uS. This leaves at least 12uS slack for rise times, long wires, etc.
To give an idea of whether 10uS is very long or not, remember that the cycle time on, for example, an 8MHz AVR as you might find in an Arduino, is 125nS. That gives you 80 instructions every 10uS (at the AVR8's advertised 1MIPS/MHz). This is plenty of time to implement the 1-Wire driver in software.
Also, even for simplest slaves there is still a need to keep track of time which requires additional hardware (an oscillator or some such).
Maxim have documentation about the fact you can get device IDs but I've never actually worked out how to get some. I guess people didn't want to be reliant on Maxim as the numbers authority?
It's pretty popular for simple serial number / "Number In A Can" applications tho'.
Think of applications like LED controllers - previously with I2C all they needed was an 8 bit shift register + comparator for their address, and an 8 bit shift register for their 'brightness', and an 8 bit comparator and +-50% RC oscilator for their actual operation. Probably ~400 transistors.
With onewire, they need a 64 bit address shift register, an oscilator, a state machine for bus states, a counter to act as a timer for long/short bits, multiple comparators on the timer output. I don't think you could do it in less than 1000 transistors. Doesn't sound like much, but when every LED in a million pixel LED wall needs one of these circuits, it adds up!
I can vouch for these. We have a couple for our team to debug HW issues, and I was amused, once in a Chinese factory, to be handed one there when I asked for a logic analyzer (I know Saleae had issues with clones in the past, and had to implement countermeasures some years ago...)
- reduce number of wires / overall length of cable runs - more sensors/actuators than GPIO pins