Skip to content

I2C

By the end of this lesson, you will be able to

  1. Scan the I2C bus and identify the devices found by their address.
  2. Read and write a device’s registers, holding the bus lock for one entire transaction and always releasing it.
  3. Decode an I2C trace in full: start, address, read/write, ACK/NACK and stop.

Takes about 70 minutes (concepts 15 · practice 25 · lab 25 · check 5). The lab needs a two-channel logic analyzer that can accept 3.3 V.

Two review questions from earlier lessons.

  1. UART has no clock wire — how does the receiver know each bit’s timing? How does a bus with a separate clock wire, like I2C, differ from that?
  2. The 06_raw_register_access.c example you read in lesson 1.1 does a read-modify-write of PWR_CTRL inside what, and why must the whole step be wrapped in it?

Open examples/12_i2c_transaction.c. This program simulates the board’s sensor bus and prints transactions the way a logic analyzer’s decoder would show them. Predict before you run it: what is the first byte on the wire when reading from the BMI270 (address 0x68)?

Terminal window
gcc -std=c11 -Wall -Wextra -o i2c_transaction examples/12_i2c_transaction.c
./i2c_transaction

The answer is D0, not 68, because the 7-bit address is shifted left by one bit, with the R/W bit appended. The probe line shows what a scan does: send an address and see whether anyone ACKs. The read line shows that reading one register involves writing the register number, a repeated START, and then reading — all inside one single transaction.

I2C uses two wires: SCL (the clock, driven by the master) and SDA (data). Both are open-drain, with pull-up resistors. Several devices share the same wires, told apart by their address.

Event On the wire Meaning
START SDA goes low while SCL is high Begins a transaction
address + R/W 8 bits, MSB first: a 7-bit address then an R/W bit (0 write, 1 read) Addresses one device
ACK / NACK The ninth bit; the receiver pulls SDA low = ACK, leaves it high = NACK “Received” or “no one there” / “that’s enough”
data 8 bits MSB first, followed by ACK/NACK for every byte One byte at a time
repeated START A new START without a STOP first Switches from write to read without releasing the bus
STOP SDA goes high while SCL is high Ends the transaction

Most devices on a bus behave as a “register file”. The SDK’s 06_raw_register_access.c example explains that sensor_i2c_read_reg() writes the register number “with NO stop”, followed by a repeated START and the read, which “is what the parts expect and what you would get wrong writing it yourself.” Some devices, such as the SHT40, have no registers at all — they use command-then-response instead, which is why the same file also has sensor_i2c_write_raw() and sensor_i2c_read_raw().

2. Scan, then prove the wire before trusting the driver

Section titled “2. Scan, then prove the wire before trusting the driver”

Scanning means probing every address from 0x08 to 0x77 and noting which ones respond with ACK. The board’s sensor bus is SCB0, on P8.0 (SCL) and P8.1 (SDA), running at 1.8 V and 400 kHz, and the addresses expected on this board are listed in the bus_device_name() table in 01_i2c_bus_scan.c.

Address Device
0x08 CapSense PSoC 4000T (base board)
0x18 TLV320DAC3100 audio codec
0x44 SHT40 humidity and temperature
0x68 BMI270 IMU
0x77 DPS368 pressure and temperature

Two things the example warns about: the BMM350 (0x15) is on I3C, a different kind of device, “and never appears here”, and finding an address not in the table “is not an error — it is a board you have added something to.” Finding an address is still not proof that it’s the chip you assumed. 02_read_imu.c teaches to “PROVE THE WIRE FIRST” — read register 0x00 first: getting 0x24 confirms it’s a real BMI270; getting 0xFF or a failed read means the bus or chip has a problem; getting anything else means it’s a different chip sharing that same address.

3. The bus lock: one transaction, one lock, released on every path

Section titled “3. The bus lock: one transaction, one lock, released on every path”

SCB0 has several owners. The SDK documentation’s chapter J1 states that OPTIGA Trust M’s PAL is also a master on SCB0, alongside a background task that reads sensors every 100 ms. The SDK’s read/write functions do not hold the lock for you — the caller must wrap it themselves with sensor_i2c_lock() and sensor_i2c_unlock(). A comment in 01_i2c_bus_scan.c explains what happens without it: “It works, it keeps working, and then one day a repeated START from this task lands between the address phase and the data phase of the auto task’s read and both come back wrong.”

/* Step 2 — take the bus. Never scan without this. */
if (!sensor_i2c_lock(SCAN_LOCK_TIMEOUT_MS)) {
/* Someone still holds it after a full second. The likeliest cause is
* the hazard in the header block: the auto task was suspended between
* its own lock and unlock. Resuming it lets it finish and release. */
printf(" sensor_i2c_lock(%u ms) TIMED OUT — someone holds the bus\r\n",
(unsigned)SCAN_LOCK_TIMEOUT_MS);
if (auto_was_running) {
printf(" resuming the auto task so it can release the mutex\r\n");
sensor_auto_start();
}
return SDK_EX_BUSY;
}

Source: 01_i2c_bus_scan.c lines 143-155 (Apache-2.0, tesaiot-pse84-devkit-sdk)

Rules drawn from these examples:

  • A lock always has a timeout, and when it times out, report busy — don’t conclude the bus is broken.
  • One lock per set of readings, not per register. 02_read_imu reads acceleration, gyro and temperature inside a single lock, so all three come from the same instant, and a command-then-response device must hold the lock across “the command, the wait AND the response” (06_raw_register_access).
  • Release the lock on every exit path. The scan example has a comment: “There is no path out of here that skips this.” And chapter J1 notes that even reporting an error must happen after unlocking.
  • Never hold the lock across a long delay — it blocks every other job waiting on the bus.

examples/12_i2c_transaction.c runs in three parts.

  • Part 1: address_byte() shifts the 7-bit address and appends the R/W bit.
  • Part 2: probe() simulates scanning four addresses around 0x68 — the one with a device answers ACK, the rest NACK.
  • Part 3: read_register() prints reading a one-byte chip id, and reading six bytes of acceleration in a single transaction (a burst read).

Try changing things and predicting the result before you run it.

  1. Switch to reading the DPS368 (0x77). What is the address byte on the write, and on the read?
  2. If the six-byte read were split into six separate one-byte transactions, how many more bytes would appear on the wire, and why does the SDK’s example say a burst read is “both faster and ATOMIC”?
  3. Add 0x10 (the DFR0522 RGB matrix, connected to the header’s 3.3 V bus) to the device list, and probe it.

Open practice/12_i2c_trace.c. There are 4 gaps to fill in.

  1. addr_byte() assembles the address byte.
  2. parse_addr_byte() takes it apart again, and rejects an address outside 0x08 to 0x77.
  3. encode_read_reg() produces the event sequence for reading a register, including the NACK on the final byte.
  4. scan() scans the same way sensor_i2c_scan() does, and stops adding results once storage is full.
Terminal window
gcc -std=c11 -Wall -Wextra -o i2c_trace practice/12_i2c_trace.c && ./i2c_trace

Try it yourself for at least 15 minutes first, then open solution/12_i2c_trace.c. The comments in the solution explain two things often overlooked: why there is no STOP between selecting the register and reading it, and why the master must respond with NACK on the last byte.

Answer the 5 questions in quiz.yaml (shown at the bottom of this page on the website), covering all three objectives. Getting 4 or more right counts as finishing the lesson.

Task: scan the board’s sensor bus with the SDK’s example, then capture a real I2C transaction on the header’s 3.3 V bus with a logic analyzer.

Part 1: scanning with the SDK

  1. Build with make build -j ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/sensors/01_i2c_bus_scan, then flash and unplug/replug the cable.
  2. From the serial console, note every address that responds and the name the example prints for it. Compare against the table in concept 2 — which ones are missing, and which are extra?
  3. Build again with SDK_EXAMPLE_CM33=cm33/sensors/02_read_imu. Note the chip id line, and answer: what kind of evidence is this that a scan alone cannot give?

Part 2: capturing signals (the sensor bus runs at 1.8 V and isn’t exposed at the header, so this part uses the header’s 3.3 V I2C bus instead)

  1. Flash the QWA309 — Header I/O Test example from the Developer Hub. Connect the logic analyzer, one channel to SCL and one to SDA of the Arduino header’s I2C, plus ground. Set the decoder to I2C.
  2. Capture while pressing the Scan button on the screen. This example scans 0x08 to 0x77 on the same bus as the touch screen. Find one address in the decoder that got a NACK and one that got an ACK, then decode by eye, bit by bit: START, the 7 address bits, the R/W bit, the ninth bit, STOP.
  3. If you have a DFR0522 RGB matrix, connect it and scan again — you should see 0x10 respond with ACK (the DFR0522 RGB Dot Matrix example uses this address).
  4. Measure SCL’s frequency from the waveform.

Evidence to keep in your portfolio: the scan log and chip id from the serial console, one waveform picture of an ACK transaction and one of a NACK transaction, both labelled by hand, and the SCL frequency you measured (state which part of the waveform you measured it from, since I2C speeds differ by bus configuration).

  • Read the SDK documentation’s chapter J1 — The sensor bus and its lock, the section explaining why sensor_i2c_init() calls a bus recovery routine (freeing a device stuck holding SDA) before configuring the pins.
  • Read Understanding the I2C Bus (Texas Instruments SLVA704) on pull-ups and clock stretching, then connect it to Appendix X #13 of the SDK documentation, which recounts a clock-stretching device stuck on the display’s bus starving other CM55 tasks.

Next lesson: lesson 5.3, SPI

  • If you were adding a new sensor to the SCB0 bus, what would you need to check first, to avoid breaking the four sensors already there?
  • Have you ever run into code that “mostly works, but occasionally gets a wrong value”? Could that be a symptom of bus contention?

Try the real thing on the TESAIoT Dev Kit: open an example on the Developer Hub to read the code, download it, or flash a prebuilt firmware image.

  • QWA309 — DFR0522 RGB Dot Matrix — controls a DFRobot DFR0522 8x16 RGB matrix (I2C 0x10) on the 3.3 V bus alongside the display, showing clear/fill/pixel/pattern through an LVGL UI.
  • EP01 — DPS368 Monitor — reads atmospheric pressure and temperature from the Infineon DPS368 sensor over I2C, and displays it on an LVGL screen.
  • QWA309 — Header I/O Test — diagnostic: scans the header’s 3.3 V I2C bus, and tests UART, SPI, GPIO and PWM, with a console UI.

Review questions

Answer on your own first, then open the answer.

  1. A scan of the board's sensor bus finds 0x44, 0x68 and 0x77 but not 0x15. Which conclusion is right? (Objective 1)

    1. แมกนีโตมิเตอร์ BMM350 เสีย
    2. พบ SHT40, BMI270 และ DPS368 ส่วน BMM350 (0x15) อยู่บน I3C ซึ่งเป็นอุปกรณ์คนละตัว การสแกนนี้จึงไม่มีวันเห็นมัน
    3. ต้องสแกนซ้ำด้วยความเร็วต่ำกว่านี้
    4. บัส SDA ค้าง
    Show answer

    Answer: B. พบ SHT40, BMI270 และ DPS368 ส่วน BMM350 (0x15) อยู่บน I3C ซึ่งเป็นอุปกรณ์คนละตัว การสแกนนี้จึงไม่มีวันเห็นมัน

    ตัวอย่าง 01_i2c_bus_scan ของ SDK เขียนไว้ว่า BMM350 lives on I3C P3[0]/P3[1] and never appears here การสแกนตอบได้เฉพาะอุปกรณ์บนบัสที่สแกน

  2. A device ACKs at 0x68. What is the best next step before calling bmi270_init()? (Objective 1)

    1. เรียก init เลย ถ้าผ่านแปลว่าถูกชิป
    2. อ่านรีจิสเตอร์ chip id (0x00) ถ้าได้ 0x24 คือ BMI270 จริง ค่าอื่นคือชิปคนละตัวที่ address เดียวกัน
    3. เขียนค่าทดสอบลงทุกรีจิสเตอร์
    4. สแกนซ้ำสิบครั้ง
    Show answer

    Answer: B. อ่านรีจิสเตอร์ chip id (0x00) ถ้าได้ 0x24 คือ BMI270 จริง ค่าอื่นคือชิปคนละตัวที่ address เดียวกัน

    02_read_imu ของ SDK สอนว่า PROVE THE WIRE FIRST การอ่านหนึ่งไบต์เปลี่ยนคำว่า init failed ให้กลายเป็นการวินิจฉัย และการเขียนมั่วอาจเปลี่ยนสถานะของชิปที่ไดรเวอร์พึ่งอยู่

  3. You must read the BMI270 accelerometer, gyro and temperature as one set. How should the lock be held? (Objective 2)

    1. lock และ unlock รอบการอ่านแต่ละค่า สามครั้ง
    2. lock ครั้งเดียว อ่านทั้งสาม unlock แล้วจึงหน่วงเวลาก่อนรอบถัดไป
    3. lock ครั้งเดียวตอนบูต แล้วไม่ต้อง unlock
    4. ไม่ต้อง lock เพราะไดรเวอร์ถือให้แล้ว
    Show answer

    Answer: B. lock ครั้งเดียว อ่านทั้งสาม unlock แล้วจึงหน่วงเวลาก่อนรอบถัดไป

    หนึ่ง lock ต่อหนึ่งชุดข้อมูล ทำให้สามค่ามาจากจังหวะเดียวกัน และห้ามถือ lock ข้ามการหน่วงเวลา ส่วนฟังก์ชันของ SDK ไม่ถือ lock ให้ ผู้เรียกต้องครอบเอง

  4. Order the steps of a safe bus scan, following the SDK's 01_i2c_bus_scan. (Objective 2)

    1. sensor_i2c_unlock()
    2. หยุด task เบื้องหลังที่อ่านเซนเซอร์ (ถ้ากำลังทำงาน)
    3. sensor_i2c_scan(addrs, max)
    4. sensor_i2c_lock(timeout) และคืน BUSY ถ้าหมดเวลา
    5. เปิด task เบื้องหลังกลับเหมือนเดิม
    Show answer

    Correct order: B. หยุด task เบื้องหลังที่อ่านเซนเซอร์ (ถ้ากำลังทำงาน) → D. sensor_i2c_lock(timeout) และคืน BUSY ถ้าหมดเวลา → C. sensor_i2c_scan(addrs, max) → A. sensor_i2c_unlock() → E. เปิด task เบื้องหลังกลับเหมือนเดิม

    เอาอีกเจ้าของออกจากสายก่อน ถือ lock สแกน คืน lock ทุกทาง แล้วคืนสภาพระบบเหมือนที่พบ การสแกนถือบัสราว 30 ถึง 60 ms ตัวอย่างจึงหยุด task เบื้องหลังแทนการแย่ง lock กับมัน

  5. A decoder shows S, D1, ACK, 24, NACK, P. Which reading is right? (Objective 3)

    1. master เขียนค่า 0x24 ไปที่อุปกรณ์ 0xD1
    2. master อ่านหนึ่งไบต์จากอุปกรณ์ 0x68 ได้ 0x24 แล้วตอบ NACK เพราะเป็นไบต์สุดท้าย ก่อน STOP
    3. อุปกรณ์ไม่มีอยู่บนบัส
    4. เกิด error เพราะมี NACK
    Show answer

    Answer: B. master อ่านหนึ่งไบต์จากอุปกรณ์ 0x68 ได้ 0x24 แล้วตอบ NACK เพราะเป็นไบต์สุดท้าย ก่อน STOP

    0xD1 คือ address 0x68 เลื่อนซ้ายแล้วบิต R/W = 1 (อ่าน) อุปกรณ์ตอบ ACK แล้วส่ง 0x24 master ตอบ NACK เพื่อบอกว่าพอแล้ว NACK ตรงนี้คือสิ่งที่ถูกต้อง ไม่ใช่ความผิดพลาด

Cite this lesson

If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.

"I2C" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0

Thai attribution: "I2C" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

Lesson link: https://tesaiot.github.io/tesa-qualification-program/en/courses/embedded-c-foundations/m05-serial-buses/l02-i2c/

This lesson adapts the source below; keep its credit too.
https://github.com/tesaiot/tesaiot-pse84-devkit-sdk/tree/ef72c1b658178eee8c38b1e47d28b006f80a59b5 · SDK examples and docs are linked at this commit, not copied into this course. Lessons quote short excerpts (at most 25 lines) with a link to the file at this commit and the credit (Apache-2.0, tesaiot-pse84-devkit-sdk).

Full guide: how to cite TESA

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA