5.5.5 · D3Embedded Systems & Real-Time Software

Worked examples — Communication interfaces — UART, SPI, I2C (master - slave), CAN bus

3,827 words17 min readBack to topic

The scenario matrix

Each row is a case class — a kind of situation the topic can throw. Each is covered by at least one worked example (WE#) below.

Cell Case class Which protocol Covered by
A Normal timing — send a byte, find duration UART WE1
B Degenerate input — clock mismatch at the limit of tolerance UART WE2
C Full-duplex swap — read while writing (ring of shift registers) SPI WE3
D Wire-count scaling — N slaves, many-slave limit SPI WE4
E Resistor sizing between two bounds (upper & lower) I2C WE5
F Zero / no-device case — NACK, nobody answers I2C WE6
G Wired-AND arbitration — two IDs collide, who wins CAN WE7
H Limiting value — longest bus at full speed CAN WE8
I Real-world word problem — sensor budget across a system mixed WE9
J Exam-style twist — the "obvious" answer is wrong I2C/SPI WE10

WE1 — UART: how long to send one byte? (Cell A)

  1. Count the bits. 8N1 frame = 1 start + 8 data + 1 stop = 10 bits total, of which 8 are payload. Why this step? The line only carries voltage-vs-time; start and stop bits are real time on the wire, so they count toward duration.
  2. Find one bit's duration. Baud = bits/second, so . Why this step? Everything in UART is measured in bit-times; you need the ruler before you measure.
  3. Total frame time. . Why this step? Bits are sent back-to-back, so just multiply count by the ruler.
  4. Wasted fraction. Framing bits = 2 of 10 = . Why this step? Effective throughput is always less than the baud number — this is the reason.

Verify: bit-slots ✓. The value 'K' doesn't matter for timing (a 0x00 byte takes the same time) — timing depends only on bit count, not bit values. Units: ✓.

See Baud rate & clock generation (timers/PLL) for how a timer/PLL actually produces 19200 edges per second.


WE2 — UART: clock mismatch at the tolerance edge (Cell B — degenerate/limit)

Figure — Communication interfaces — UART, SPI, I2C (master - slave), CAN bus
Figure s01: a time axis in bit-times. White circles mark the ideal mid-bit sample points (one per bit, spaced exactly one bit apart). Amber squares mark where a receiver whose clock runs 4% fast actually samples — each square sits slightly to the left of its circle, and the gap grows bit by bit. The amber annotation on the last (STOP) bit shows the drift has reached 0.38 bit, still inside the 0.5-bit half-window, so the byte is read correctly.

  1. Locate the worst sample. The receiver samples mid-bit, so the STOP bit (the 10th bit) is sampled after 9.5 bit-times from the start edge. In the figure, the amber square drifts further from its white circle on every bit — the last one is the worst. Why this step? Error accumulates; the last sample has the most drift, so it is the one that fails first.
  2. Accumulate the drift. With drift fraction , timing error at that sample is bit-times. Why this step? Each bit adds of slip; after bits it's .
  3. Compare to the margin. The sample point may slip up to half a bit (0.5) before landing in the wrong window. Since , the sample still lands inside the correct bit → byte read correctly (this is the shaded margin in the figure). Why this step? Mid-bit sampling gives a bit safety window; that is the whole point of sampling at the middle.
  4. Find the breaking point. Solve . Why this step? This is the limiting value — the degenerate case where 4% would have become fatal.

Verify: Margin left = bit-times ✓. At exactly the drift equals (on the edge) ✓. Sanity: bigger (e.g. 2 stop bits, more data bits) → smaller tolerance, which matches intuition that longer frames drift more.


WE3 — SPI: full-duplex byte swap (Cell C)

Figure — Communication interfaces — UART, SPI, I2C (master - slave), CAN bus
Figure s02: the master's 8-bit shift register (left, holding 0xA5) and the slave's (right, holding 0x3C) drawn as two boxes wired into one ring — MOSI carries bits master→slave along the top, MISO carries bits slave→master along the bottom, and SCLK (cyan) runs from master to slave. Below, the "after 8 clocks" state shows the boxes have exchanged contents: master = 0x3C, slave = 0xA5.

  1. See the ring. MOSI feeds master→slave, MISO feeds slave→master. The two 8-bit Shift registers form one 16-bit loop (top of the figure). Why this step? SPI has no memory-copy; it only rotates a physical ring of flip-flops.
  2. Each clock = one shift. On each of 8 rising edges (Mode 0), the master pushes its current MSB out MOSI and latches the slave's MSB in from MISO, simultaneously. Why this step? This simultaneity is exactly what "full-duplex" means — one edge moves data both ways. MSB-first means the leftmost bit leads.
  3. After 8 edges. All 8 of the master's original bits have entered the slave, and vice-versa. Master now holds 0x3C, slave holds 0xA5 (bottom of the figure). Why this step? 8 shifts = one full byte swap; the registers have exchanged contents.

Verify: , . XOR-swap identity: swapping is its own inverse, so 8 more clocks would restore 0xA5/0x3C ✓. No bits created or destroyed: before = after (invariant of a pure permutation) ✓.


WE4 — SPI: how many wires for N slaves? (Cell D — scaling / limit)

  1. Fixed bus wires. MOSI, MISO, SCLK are shared by all slaves → 3 pins, regardless of N. Why this step? These are broadcast lines; only the selected slave listens/drives MISO.
  2. Per-slave selects. SPI has no addressing, so each slave needs its own SS line → pins. Why this step? The only way the master picks a device is by pulling that device's own SS low (see the parent's [!mistake]).
  3. Total. pins. Why this step? Wires = shared bus + one select each.
  4. I2C comparison. I2C uses SDA + SCL = 2 wires for all 6 devices, because it addresses by a 7-bit number, not a pin. Why this step? This is the fundamental SPI-vs-I2C trade: SPI spends pins to gain speed; I2C spends protocol overhead to save pins.

Verify: SPI ; I2C (constant in N). Limiting behaviour: as , SPI pins but I2C stays 2 ✓ — exactly why I2C wins on device count. See Serial vs Parallel communication for why even 9 serial pins beats a parallel bus.


WE5 — I2C: pull-up resistor between two bounds (Cell E)

  1. Upper bound (speed). The line rises as an RC toward the HIGH threshold; rise time . Solve for : Why this step? Too big a resistor → sluggish edge → the line never reaches HIGH before the next clock → bus fails at 400 kHz.
  2. Lower bound (sink current). When a device pulls LOW, current must not exceed what it can sink: Why this step? Too small a resistor → too much current → the open-drain transistor can't hold the line below → a "0" reads as a weak "1".
  3. The window. . A common choice like sits comfortably inside. Why this step? You live between the two bounds — that is the whole design rule.

Verify: At : rise ✓; sink current ✓. Both bounds satisfied. This wired-AND behaviour is Wired-AND logic in action.


WE6 — I2C: nobody answers (the NACK / zero-device case) (Cell F — degenerate)

  1. Build the address byte. 7-bit address 0x22 in the top 7 bits, R/W = 0 (write) in bit0: Why this step? The first byte is always [7-bit addr][R/W]; the shift moves the address above the R/W bit.
  2. The ACK slot. After 8 clocks, the master releases SDA (open-drain → floats HIGH via pull-up) and gives a 9th clock. A present slave would pull SDA LOW = ACK. Why this step? HIGH is the "released/nobody" state; only a real device can create the LOW that means "I'm here."
  3. No device → SDA stays HIGH during the 9th clock = NACK. Master reads HIGH. Why this step? With no one to pull it down, the pull-up wins → the master literally sees the absence of a device.
  4. Master aborts. It issues STOP and reports "no ACK / device not found." Why this step? NACK is the protocol's built-in "are you there?" — no timeout guessing needed.

Verify: ✓. If the same device were read, byte ✓. The NACK on an empty address is why bus scanners work: they probe every address 0x00–0x7F and record which ones ACK.


WE7 — CAN: two nodes collide, who wins? (Cell G — wired-AND arbitration)

Figure — Communication interfaces — UART, SPI, I2C (master - slave), CAN bus
Figure s03: three aligned rows of 11 bits each, labelled b10 (MSB, left) down to b0 (LSB, right). Top row (cyan) = P's ID 0x2C3; middle row (white) = Q's ID 0x2A0; bottom row (amber) = the wired-AND result actually seen on the bus (each bus bit = P-bit AND Q-bit). An amber vertical band highlights the first column where P and Q differ; the annotation shows P sends 1 there but the bus reads 0, so P loses and Q wins.

  1. Write both IDs in binary (MSB first), aligned to 11 bits. Padding to 11 bits and grouping as 3-4-4 for readability: Label the columns b10 (leftmost) down to b0 (rightmost), matching the figure. Why this step? Arbitration compares bits one column at a time from the MSB (b10), so both IDs must be padded to the same 11-bit width and lined up in the same columns before we can compare them fairly.
  2. Recall the rule. Bus is wired-AND: dominant 0 beats recessive 1 (defined in the callout above). Each node transmits and listens; if it sends 1 but reads 0, it lost. Why this step? This is exactly the parent's non-destructive arbitration — no bit is corrupted, the loser just steps aside.
  3. Compare column by column (the aligned rows in the figure). Reading from b10:
    • b10: P=0, Q=0 → same.
    • b9: P=1, Q=1 → same.
    • b8: P=0, Q=0 → same.
    • b7: P=1, Q=1 → same.
    • b6: P=1, Q=0 → differ. Bus reads 0 (dominant). P sent 1 but reads 0 → P loses; Q wins at bit b6. Why this step? The first differing column decides everything; the node with 0 there (lower ID) keeps going. Note the differing bit is b6, the 5th column from the left.
  4. Confirm with arithmetic. , so the lower ID (Q) wins. ✓ Why this step? "Lowest ID wins" and "wired-AND on MSB-first bits" are the same statement — this cross-check proves it.

Verify: , , and so Q wins ✓. First differing bit: , highest set bit is bit 6 (b6), where P has a 1 and Q has a 0 ✓. Q's message continues uncorrupted — that is why CAN never retransmits the winner. See Bit stuffing & CRC in CAN frames and Differential signaling & noise immunity.


WE8 — CAN: longest bus at full speed (Cell H — limiting value)

  1. The timing rule. For arbitration a dominant bit from the far node must reach the near node and the loss-detect happen within one bit time — so the round-trip must fit in one bit: Why this step? If the far node's bit hasn't arrived before this bit ends, the loser can't detect it lost — arbitration breaks (this is why WE7's "listen while you talk" needs the whole bus to settle within one bit).
  2. Bit time at 1 Mbps. . Why this step? The bit rate fixes how long the "listening window" is; we need this number before we can convert the timing rule into a distance.
  3. Solve for L. Rearrange the timing rule for the maximum length: Why this step? This isolates — the limiting length. So the maximum ideal bus is 100 m; real buses use ~40 m at 1 Mbps once transceiver and oscillator delays eat into the budget.

Verify: ✓. Check the inverse trade-off: drop to 500 kbps () → ✓ — halving speed doubles reach, matching the relationship.


WE9 — Real-world word problem: mixed-protocol system budget (Cell I)

  1. I2C bit count. START + 3 bytes, each byte = 8 bits + 1 ACK = 9 bits, + STOP. Approx bits. Why this step? I2C spends a whole clock on each ACK — overhead is real, so count it.
  2. I2C time. At 400 kHz, . Total . Why this step? Same "count × bit-time" ruler as UART.
  3. SPI bit count & time. 256 bytes × 8 bits = 2048 bits, no start/stop/ACK overhead. At 8 MHz, . Total . Why this step? SPI's zero-overhead + high clock is exactly why it's chosen for bulk transfer.
  4. Compare. SPI dump () is the bottleneck, ~3.5× the I2C read (). Why this step? Design lesson: pick I2C for slow low-pin sensors, SPI for high-volume data — the numbers justify the parent's "killer feature" column.

Verify: I2C ✓; SPI ✓; ratio ✓. Uses Interrupts vs Polling thinking: during that SPI burst you'd typically DMA rather than poll.


WE10 — Exam-style twist: the "obvious" answer is a trap (Cell J)

  1. SPI select count. SPI needs one SS per slave = 3 selects, plus shared MOSI/MISO/SCLK. There is no such thing as an SPI address — the "3" for SPI is 3 pins, not addresses. Why this step? The trap conflates SPI's pin-based selection with I2C's address-based selection; they are different resources.
  2. I2C address count. 3 I2C slaves = 0 extra select pins (all share SDA/SCL), just 3 distinct 7-bit addresses on the same 2 wires. So "6 resources" is meaningless — SPI costs pins, I2C costs address values. Why this step? Adding pins and addresses is apples + oranges; the exam's sum is a category error.
  3. The address-byte trap. I2C address 0x50 on the wire is not 0x50. It occupies the top 7 bits with the R/W bit appended: Why this step? The classic off-by-one-bit error — the datasheet's "7-bit address" must be shifted left before the R/W bit is OR'd in.
  4. Correct read byte too. . Why this step? Read vs write differ only in bit0, which sits below the shifted address.

Verify: ✓; ✓. SPI selects (pins), I2C selects (pins) but 3 addresses — so the " resources" claim is false on both counts ✓.


Recall Self-test — cover the answers

UART 8N1 frame bit count ::: 10 bits (1 start + 8 data + 1 stop) Worst-case UART clock tolerance for 8N1 ::: about 5.26% () Why SPI needs one SS per slave ::: SPI has no addressing; the master selects only by pulling that slave's own SS low I2C address 0x68 as a write byte ::: What "NACK" tells the master ::: no device pulled SDA low → nobody at that address CAN arbitration winner rule ::: lowest numeric ID (most leading dominant 0s) wins, non-destructively Max classic-CAN length at 1 Mbps (ideal) ::: ~100 m from