UART handles a link with a single partner well, but a real system has a temperature sensor, an accelerometer, a real-time clock, external memory and maybe a display - all at once. The asynchronous interface cannot carry this load: it has no shared clock, so speed is capped by oscillator precision, and it is strictly point to point, so every new peripheral demands new wires. This lecture presents the two synchronous buses that solve the problem, with opposite trade-offs: I2C, minimalist in wires but elaborate as a protocol, and SPI, generous in wires but with almost no protocol at all.
1Scope and structure of the lecture6 min
Both buses are synchronous - they explicitly carry a clock signal alongside data, which removes at a stroke the oscillator-drift problem discussed in Lecture 08. We will trace the electrical mechanism that makes wire sharing possible on I2C, the complete write and read transactions, then the circular shift register principle of SPI, the clock modes and possible topologies, and finally the concrete ATmega328P registers for both.
- Asynchronous communication requires resynchronization on every frame, and clock error accumulates - the reason UART speed is capped (Lecture 08)
- UART is strictly point to point - with no addressing mechanism (Lecture 08)
- Explain why carrying a clock signal removes the oscillator-drift problem
- Describe the open-drain plus pull-up mechanism and its role in sharing the I2C bus
- Trace, step by step, an I2C write transaction and a read transaction, including the role of ACK/NACK
- Explain why SPI cannot read without writing, starting from the circular shift register
- Identify the four SPI modes (CPOL, CPHA) and recognize the symptom of a mismatch
- Choose, with justification, between UART, SPI and I2C for a given application
2Why a synchronous bus is needed10 min
Three limitations of UART become visible as soon as the application grows. First: the absence of a shared clock - the receiver latches onto the start edge and samples on trust; if the relative drift between the two oscillators exceeds about 2.5%, the last bit of the frame is sampled incorrectly. Second, a consequence of the first: speed is bounded by oscillator precision, not by the electronics of the line - a UART link on a ceramic resonator becomes unreliable well before one megabit per second, insufficient for a graphic display or an SD card.
Third: the number of devices. UART is strictly point to point, with no addressing - three devices on the same two wires have no way to know who a byte is meant for. An ATmega328P has a single USART module; if every peripheral required a dedicated link, the ability to expand would run out after a few modules.
3I2C: overview, open drain, addressing13 min
I2C (Inter-Integrated Circuit, Philips, 1980s) uses two lines: SDA (Serial Data) and SCL (Serial Clock), plus a shared ground. The number of wires stays the same whether two devices are connected or twelve - the great advantage of the bus.
The protocol is master-slave: the master (usually the microcontroller) generates the clock, initiates every transfer and decides when it ends. The slaves (sensors, memories, real-time clocks) never speak unprompted - they only respond on request.
Open drain and pull-up resistors
If several devices are tied to the same wire, who wins when one drives a level and another drives the opposite? The solution: the output stage of every device is open drain - a single transistor, between the line and ground. When conducting, it pulls the line to zero; when off, it releases it. No device can force the line to logic one. Each line is tied to the supply through a pull-up resistor, which brings the line back to logic one whenever nobody is pulling it down - the result is a wired-AND function: the line is at one only if every device releases it.
Many modules already have the resistors mounted on their board - four or five such modules on the same bus put the resistors in parallel, dropping the equivalent resistance below one kilohm, raising the current, and potentially destabilizing the bus. Remedy: remove the resistors from every module but one.
Seven-bit addressing
Each slave has a 7-bit address (128 combinations, of which 112 usable, the rest reserved). Every transfer begins by sending this address - all slaves listen, compare it with their own address, and only the one that recognizes itself continues the dialog. The address is set by the manufacturer, not the designer; some chips have configuration pins for the last bits, to allow several units on the same bus.
The 7 address bits are followed by an eighth direction bit (0=write, 1=read). The same address sometimes appears as 0x68 (the 7-bit convention, used by the Arduino libraries, which do the shifting) and sometimes as 0xD0 (the full byte sent on the wire) - a frequent confusion that makes the device fail to respond at all.
Standardized speeds: standard mode 100 kHz, fast mode 400 kHz, more recent variants 1 MHz and 3.4 MHz - all of these are maximums, not required values; the master can go slower without anything breaking, precisely because the clock is transmitted, not assumed.
4Start, stop conditions and ACK/NACK confirmation11 min
What remains to be solved is delimiting messages. The general rule: SDA is only allowed to change state while SCL is low; whenever SCL is high, the data must be stable, because that is exactly when the receiver samples it. A transition of SDA while SCL is high is, by definition, impossible during data transport - so it can be used as a control signal.
Stop: SDA from 0 to 1 while SCL is high - releases the bus.
Repeated start: identical to start, but with no stop before it - allows the direction of the transfer to change without releasing the bus, preventing another master from slipping in between the two halves.
ACK/NACK confirmation
Unlike UART, I2C requires confirmation after every byte. Each byte occupies 9 clock periods: eight for data (MSB first), the ninth for confirmation. On the ninth period, the transmitter releases SDA; if the receiver accepts the byte, it pulls the line to ground (ACK); if it does nothing, the pull-up resistor keeps the line high (NACK).
5I2C transactions: write, read, arbitration13 min
All the elements above combine into two patterns that every I2C library executes.
The write transaction
To write 0x1F into register 0x07 of a slave at address 0x68: the master generates start, sends 0xD0 (address+write), reads ACK; sends 0x07 (the register number, a convention of the peripheral, not of the bus), reads ACK; sends 0x1F (the value), reads ACK; generates stop. For the I2C protocol, the transfer is: start, address with write, three confirmations, two data bytes, stop - the interpretation "register plus value" belongs to the datasheet of the peripheral.
The read transaction
More subtle, because the slave usually has to be told first what is wanted, which means a write. Phase 1: start, address with write (0xD0), ACK, register number (0x07), ACK - the slave has positioned its internal pointer, but has sent nothing. Phase 2: repeated start, address with read (0xD1), ACK - the roles on SDA reverse, the slave becomes the transmitter, the master the receiver, but the master keeps generating the clock. For each byte received, the master confirms with ACK if it wants more data, or with NACK if it was the last one wanted, then generates stop.
If the master generated stop right after an ACK, the slave could be left with its output active, holding SDA to ground - the bus stays locked until power is cycled. If the microcontroller resets in the middle of a read, the slave keeps holding SDA low, waiting for the clock; the master can no longer even generate a start. Unlocking: manually generating, in software, up to nine pulses on SCL, followed by a stop condition.
Arbitration and clock stretching
Arbitration resolves the collision when two masters start simultaneously: each sends the address bits and reads SDA at the same time - if it reads 0 when it sent 1, someone else is pulling the line, and it withdraws immediately, with no bits lost. The master with the numerically smaller address wins (zero dominates). Clock stretching resolves the slow slave: SCL is also open drain, so a slave that needs time holds it pulled low after the master has released it; the master, which checks the actual level before considering the period finished, waits. It is a flow-control mechanism that neither UART nor SPI has.
6SPI: the circular shift register11 min
SPI (Serial Peripheral Interface) starts from the opposite premises - it does not economize on wires, it transfers as fast as possible with as little logic as possible. It has no single official specification; it is a hardware mechanism for exchanging bits, on top of which every peripheral defines its own conventions.
Communication is full duplex, master-slave, over four lines: SCLK (clock, generated exclusively by the master), MOSI (Master Out Slave In), MISO (Master In Slave Out), and SS (Slave Select, active on logic zero - an unselected slave puts its MISO into high impedance).
Shifting in both registers is driven by the same clock - there is no "write only" or "read only". If the master only wants to send, it still gets back the byte from the slave register (usually ignored). If it only wants to read, it still has to send a dummy byte, because otherwise it would have no reason to generate clock pulses. This is where the shape of every SPI library function comes from: a single transfer function, which takes a byte and returns a byte.
A second consequence: the slave cannot respond immediately. The byte it sends is the one already prepared in the register at the start of the transfer, not one computed from the byte received at the same time. The usual dialog has two stages: the first transfer sends a command and receives a byte with no value; the second sends a dummy byte and receives the response to the previous command. Anyone who ignores this offset systematically reads the response one position late.
The SS line has two roles: it selects the slave, and it delimits the transaction - many peripherals reset their internal logic on every falling edge of selection, and their commands are interpreted relative to the start of the transaction.
7SPI modes: polarity and phase11 min
A shift register needs both clock edges, for different operations: on one, the output is updated (the next bit goes onto the line); on the other, the input is sampled (the current bit is captured). The two must happen on opposite edges, half a period apart, otherwise the receiver would read the line right at the moment of switching.
CPHA (phase): which edge samples - 0 = sampling on the first edge, change on the second; 1 = the reverse.
| Mode | CPOL | CPHA | Sampling |
|---|---|---|---|
| 0 | 0 | 0 | rising edge (first) |
| 1 | 0 | 1 | falling edge (second) |
| 2 | 1 | 0 | falling edge (first) |
| 3 | 1 | 1 | rising edge (second) |
Modes 0 and 3 are the most common - in both, sampling happens on the rising edge, only the idle state differs. Mode 0 is the default for the Arduino library and for most memories, SD cards and converters.
If the master is in mode 0 (drives on falling, reads on rising) and the slave is in mode 1 (the reverse), the entire sequence shifts by half a period - the slave captures the next bit instead of the current one. SPI has no parity, checksum or confirmation - the master register fills with eight bits just as conscientiously as if everything were correct. Symptom: a sensor reports absurd values, a display shows scattered pixels, an SD card fails to initialize, and the errors can appear intermittently, depending on the stray delays of the layout. Diagnosis: the peripheral datasheet, an oscilloscope, or methodically trying all four modes.
8SPI topologies and the ATmega328P SPI module13 min
The single-slave case ties the four lines directly - SS can sometimes be tied permanently to
ground, if the peripheral accepts it. The independent selection configuration: all slaves
share SCLK, MOSI, MISO, but each has its own SS line - complete independence, but costing
3+n pins for n slaves. Daisy chaining: the MISO of the first goes to the MOSI of
the second and so on, SCLK and SS shared by all; the registers form a single long chain - the pin
count stays 4 regardless of how many slaves, but the devices can no longer be addressed individually,
and the duration of a transfer grows proportionally. Used only for identical devices that get updated
together anyway (a chain of display drivers), never for different sensors.
The ATmega328P SPI module
Four physical pins: PB2 (SS), PB3 (MOSI), PB4 (MISO), PB5 (SCK) - pins 10-13 on the Arduino Uno.
| Register | Bit | Role |
|---|---|---|
| SPCR | SPE / MSTR | module enable / role (1=master) |
| SPCR | CPOL / CPHA / DORD | operating mode, bit order |
| SPCR | SPR1:0 | clock prescaling: f_osc/4, /16, /64, /128 |
| SPSR | SPIF | transfer finished (8 pulses consumed) |
| SPSR | SPI2X | doubles the frequency, up to f_osc/2 |
| SPDR | - | data register - writing starts the transfer, reading returns the byte received |
If it stays an input and something pulls it low, the module automatically switches from master to slave, clearing the MSTR bit - a mechanism meant for multi-master systems, but which, in an ordinary application, produces a hard-to-explain fault: communication stops abruptly, for no apparent reason.
void SPI_MasterInit(void) {
DDRB |= (1<<PB3)|(1<<PB5)|(1<<PB2); /* MOSI, SCK, SS: outputs */
DDRB &= ~(1<<PB4); /* MISO: input */
SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR0); /* master, clock f_osc/16 */
PORTB |= (1<<PB2); /* SS inactive (High) */
}
uint8_t SPI_MasterTransfer(uint8_t data) {
PORTB &= ~(1<<PB2); /* select the slave */
SPDR = data; /* writing starts the transfer */
while (!(SPSR & (1<<SPIF))) { }
PORTB |= (1<<PB2);
return SPDR; /* byte received from the slave */
}
ATmega328P at 16 MHz, peripheral limited to 2 MHz on SCLK.
See the solution
Available dividers (without SPI2X): 4, 16, 64, 128, giving 4 MHz, 1 MHz, 250 kHz, 125 kHz. The first value (4 MHz) exceeds the limit - divide by 16 is chosen (SPR0=1, SPR1=0), a 1 MHz clock. Bit period: 1 µs, so eight bits transfer in 8 µs. At high speeds, the software overhead (writing SPDR, testing SPIF, driving the select line) becomes comparable to the duration of the transfer itself, and the effective useful rate stays noticeably below the theoretical one.
9The SPI and Wire libraries, scanning the I2C bus9 min
The SPI library provides the notion of a transaction, which declares the speed, bit order and mode required by the peripheral:
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(PIN_SS, LOW);
SPI.transfer(address | 0x80); /* command; bit 7 marks a read */
uint8_t value = SPI.transfer(0x00); /* dummy byte, receives the response */
digitalWrite(PIN_SS, HIGH);
SPI.endTransaction();
Selection is still driven explicitly by the program (the pin number depends on the wiring); the two transfers reflect exactly the offset discussed earlier.
The Wire library, for I2C, implements the transactions discussed above:
Wire.beginTransmission(ADDRESS); /* prepares START + address + W */
Wire.write(reg);
Wire.endTransmission(false); /* false = repeated START, no STOP */
Wire.requestFrom(ADDRESS, (uint8_t)1);
if (Wire.available()) value = Wire.read();
beginTransmission sends nothing - it accumulates bytes in an internal buffer; only
endTransmission generates start, address, data and (unless given false)
stop. The returned value is an error code: 0=success, 2=NACK on address (peripheral absent), 3=NACK
on data.
Scanning the I2C bus
This follows directly from the confirmation mechanism: start plus address plus stop, with no data - if ACK comes back, a device exists at that address; if NACK, it does not.
for (uint8_t address = 8; address < 120; address++) {
Wire.beginTransmission(address);
if (Wire.endTransmission() == 0) {
Serial.print("Found at 0x"); Serial.println(address, HEX);
}
}
An empty result: check for swapped SDA/SCL, a missing shared ground, absent pull-ups. A result with devices at every address: usually indicates an SDA line stuck at logic zero - every query appears confirmed.
ATmega328P at 16 MHz. The TWI module: f_SCL = f_osc / (16 + 2·TWBR·4^TWPS).
See the solution
With TWPS=0 (4^0=1): f_SCL = f_osc/(16+2·TWBR).
For 100 kHz: 16×10⁶/100000 = 160 = 16+2·TWBR ⇒ TWBR=72.
For 400 kHz: 16×10⁶/400000 = 40 = 16+2·TWBR ⇒ TWBR=12.
Both values are exact integers. Unlike UART, where rate error is a permanent concern, here a deviation from nominal would have no functional consequence - the slaves follow whatever clock they receive, as long as it does not exceed their maximum limit.
10Comparing UART, SPI and I2C6 min
| Criterion | UART | SPI | I2C |
|---|---|---|---|
| Wires | 2 | 3 shared + n select | 2 |
| Shared clock | no | yes | yes |
| Usual speed | 9600-115200 bps | up to 10 Mbit/s | 100-400 kHz |
| Devices | 2, point to point | 1 master + n slaves | up to 112 addresses |
| Confirmation | optional parity only | none | ACK/NACK per byte |
| Typical use | computer, debugging | memories, SD, fast displays | slow sensors, RTC, EEPROM |
The three coexist without conflict on the same microcontroller, because they use different hardware modules and pins - an ordinary project has, at the same time, UART to a computer for debugging, I2C with two or three sensors, and SPI to a memory card or a display.
11Common mistakes4 min
- I connected SDA and SCL with no pull-up resistors - the devices have power pins anyway. False - without the pull-up resistors, lines released by every device stay floating, at an undefined level, instead of logic one. Every I2C line needs a pull-up resistor, either external or already present on a module on the bus.
- The SPI peripheral does not respond correctly, but the code looks right - it must be a power problem. Often not - it is a mismatched SPI mode (CPOL/CPHA), which produces no error message, only wrong data. Check the datasheet for the required mode and, if it is not clearly specified, methodically try the four combinations.
- I confirmed (ACK) even the last byte of an I2C read, to make sure everything went well. False - ACK on the last byte tells the slave to keep transmitting, not that everything is fine. Deliberately send NACK after the last byte wanted, then generate the stop condition.
12Summary and glossary5 min
Both buses solve the limits of UART by explicitly carrying a clock signal. SPI does it with four lines and almost no protocol: the master and the slave form, through MOSI and MISO, a single circular shift register, and on every pulse one bit leaves and one bit enters simultaneously - reading and writing are inseparable. The four modes (CPOL, CPHA) must match exactly between master and slave, otherwise wrong data appears with no warning at all. Independent selection costs one pin per slave; daisy chaining saves pins at the price of individual addressing. I2C achieves the same with two lines, paying with an elaborate protocol: open drain and pull-up resistors make the bus shareable without conflicts, the start/stop conditions use the only transitions forbidden during data, and ACK/NACK confirmation after every byte provides a flow control that neither UART nor SPI has. The choice between them is not preference but requirements: speed for SPI, pin economy for I2C, distance and compatibility for UART.
13Review questions7 min
- Explain why a UART link cannot connect three devices on the same wires, while I2C can connect ten. What element of the protocol makes the difference?
- Why does explicitly carrying the clock remove the oscillator-drift problem?
- Describe what would happen electrically on SDA if one device had a push-pull output instead of open drain, while a second device pulled the line to ground at the same time.
- Explain why the start condition cannot be confused with an ordinary data bit.
- A master reads five bytes from an I2C slave. Who confirms each byte, and what happens if the master confirms even the last one?
- Justify the statement that on SPI you cannot read without writing, starting from the circular shift register.
- An SPI peripheral requires mode 2, the program configures it for mode 0. What happens at the level of the edges, and why does no error message appear?
14Further directions2 min
The next lectures move from the individual mechanisms of peripherals to designing a complete application: how the resources discussed so far - ports, interrupts, timers, ADC, communication - are chosen and combined into a functional, tested and documented system.
SPI configuration, the I2C protocol and bus scanning from this lecture become real hardware in Laboratory 06.