Lập trình nhúng

Giao thức truyền thông trên STM32 (Phần 3): I²C – địa chỉ, điện trở kéo lên và đọc thanh ghi

I²C trên STM32 từ START, ACK, STOP, cách tính điện trở kéo lên theo điện dung bus, quét bus, đọc thanh ghi cảm biến bằng HAL đến thủ tục gỡ bus bị treo. Phần 3 của series giao thức truyền thông trên STM32.

11 phút đọc
Giao thức truyền thông trên STM32 (Phần 3): I²C – địa chỉ, điện trở kéo lên và đọc thanh ghi

Ghi chú: sơ đồ trong bài do Capitlab vẽ để minh hoạ. Số liệu trong các bảng là giá trị tính theo công thức, cột "Đo thực tế" sẽ được cập nhật khi đo trên phần cứng. Mã nguồn soạn theo tài liệu HAL và datasheet, chưa chạy thử trên phần cứng, nên hãy kiểm tra lại trước khi dùng cho sản phẩm.

I²C (Inter-Integrated Circuit) chỉ cần hai dây cho cả một bus nhiều thiết bị, nên hay gặp ở cảm biến, EEPROM, RTC, màn hình OLED nhỏ. Đổi lại, nó chậm hơn SPI và rất nhạy với điện trở kéo lên. Khi quét bus không thấy thiết bị, thủ phạm gần như luôn là một trong ba thứ: địa chỉ, điện trở kéo lên, hoặc dây nối.

Bài này đi qua khung truyền, cách tính điện trở kéo lên theo đặc tả, mã HAL để quét bus và đọc thanh ghi, và cách gỡ bus khi SDA bị giữ ở mức thấp.

1. Nguyên lý

I²C có hai dây: SCL (clock, master phát) và SDA (dữ liệu, hai chiều). Cả hai đều là open-drain: thiết bị chỉ kéo đường xuống thấp, mức cao do điện trở kéo lên tạo ra. Vì không ai đẩy đường lên cao, nhiều thiết bị dùng chung bus mà không chập nhau khi hai bên cùng truyền.

Dạng sóng SCL và SDA: điều kiện START, tám bit địa chỉ và R/W, bit ACK do slave kéo xuống, điều kiện STOP

Hình 1. Byte địa chỉ 0x76 với bit ghi (R/W = 0). SDA chỉ được đổi khi SCL đang thấp, trừ START và STOP.

Một lần truyền gồm:

  1. START: SDA xuống thấp trong lúc SCL cao.
  2. Byte địa chỉ: 7 bit địa chỉ và 1 bit đọc/ghi (R/W, 0 là ghi, 1 là đọc).
  3. ACK: thiết bị được gọi kéo SDA xuống ở xung clock thứ 9.
  4. Các byte dữ liệu, mỗi byte có một bit ACK. Khi đọc, master trả NACK ở byte cuối để báo đã đủ.
  5. STOP: SDA lên cao trong lúc SCL cao. Hoặc START lặp (repeated START) để đổi chiều mà không nhả bus.

Mỗi byte tốn 9 xung clock (8 bit và 1 ACK). Thời gian tính theo các chế độ chuẩn:

Chế độ Tốc độ tối đa Thời gian 1 byte (9 xung) Đọc 1 thanh ghi (khoảng 36 xung)
Standard-mode 100 kbit/s 90 µs 360 µs
Fast-mode 400 kbit/s 22,5 µs 90 µs
Fast-mode Plus 1 Mbit/s 9 µs 36 µs

Đây là số tính, chưa gồm thời gian START, STOP và phần mềm. Chuẩn còn có High-speed mode (3,4 Mbit/s) nhưng ít gặp trên STM32.

Đọc một thanh ghi

Mẫu giao dịch gặp nhiều nhất là đọc thanh ghi cảm biến: ghi địa chỉ thanh ghi, phát START lặp để chuyển sang đọc, rồi nhận dữ liệu. HAL_I2C_Mem_Read làm đúng trình tự này.

Chuỗi giao dịch đọc thanh ghi 0xD0: S, 0x76 và W, ACK, 0xD0, ACK, Sr, 0x76 và R, ACK, dữ liệu 0x60, NACK, P

Hình 2. Chuỗi đọc thanh ghi ID của BME280. Ô tím do master phát, ô xanh do slave phát. Dữ liệu 0x60 là ID của BME280 theo datasheet.

2. Chọn điện trở kéo lên

Điện trở kéo lên quyết định cạnh lên nhanh hay chậm. Đặc tả I²C (UM10204) cho hai giới hạn:

  • Trên: Rp(max) = Trise / (0,8473 × Cbus), trong đó Trise là thời gian lên tối đa cho phép của chế độ đang dùng (tính từ 30% đến 70% VDD), Cbus là tổng điện dung bus.
  • Dưới: Rp(min) = (VDD − VOL(max)) / IOL, với IOL là dòng kéo xuống thiết bị chịu được. Ở VDD = 3,3 V, VOL = 0,4 V, IOL = 3 mA thì ra khoảng 0,97 kΩ.

Bảng tính Rp(max) với Trise tối đa theo đặc tả: 1000 ns (Standard), 300 ns (Fast), 120 ns (Fast-mode Plus):

Điện dung bus Cbus Rp(max) Standard 100 kHz Rp(max) Fast 400 kHz Rp(max) Fast Plus 1 MHz
50 pF 23,6 kΩ 7,08 kΩ 2,83 kΩ
100 pF 11,8 kΩ 3,54 kΩ 1,42 kΩ
200 pF 5,90 kΩ 1,77 kΩ 0,71 kΩ

Muốn dùng bảng thì phải ước lượng Cbus trước. Mỗi chân thiết bị góp vài pF, mỗi 10 cm dây góp khoảng vài chục pF. Datasheet cho số cụ thể của chân, còn dây thì nên đo. Sau đó chọn điện trở nằm giữa Rp(min) và Rp(max). Ví dụ bus 100 pF chạy 400 kHz thì 2,2 kΩ là hợp lý: dưới 3,54 kΩ và trên 0,97 kΩ. Nếu Rp(max) lại nhỏ hơn Rp(min) thì bus không đạt chuẩn ở tốc độ đó, phải hạ tốc độ, giảm điện dung, hoặc chia thành nhiều bus.

Sơ đồ bus I²C: hai điện trở Rp kéo SCL và SDA lên VDD, ba thiết bị nối vào cả hai đường

Hình 3. Bus I²C điển hình. Mỗi thiết bị chỉ kéo xuống GND, mức cao do Rp tạo ra.

Nhiều module breakout đã gắn sẵn điện trở kéo lên, thường 4,7 kΩ hoặc 10 kΩ. Cắm vài module lên cùng bus thì các điện trở này mắc song song và giá trị tương đương giảm dần, đôi khi xuống dưới Rp(min). Hãy tính lại và tháo bớt nếu cần.

Điện trở kéo lên nội của MCU thường vài chục kΩ, chỉ đủ để thử nhanh, không nên dùng trong sản phẩm.

3. Cấu hình trong STM32CubeIDE

Mục Giá trị Ghi chú
Mode I2C
I2C Speed Mode Standard hoặc Fast
Clock Speed 100000 hoặc 400000 Dòng G0, G4, L4, H7... dùng thanh ghi TIMING, CubeMX tự tính
Addressing Mode 7-bit
GPIO Open-drain CubeMX tự đặt cho chân I2C
Pull-up Không (dùng điện trở ngoài) Nội trở chỉ để thử nghiệm

4. Mã nguồn

4.1. Quét bus

Đoạn này liệt kê mọi thiết bị trả ACK. HAL nhận địa chỉ 7 bit đã dịch trái 1 bit:

void i2c_scan(void)
{
    for (uint8_t addr = 1; addr < 128; addr++) {
        if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 2, 10) == HAL_OK) {
            printf("Tim thay thiet bi: 0x%02X\r\n", addr);
        }
    }
}

printf cần được chuyển hướng ra UART (retarget) thì mới in được. Phần cấu hình UART có ở phần 1.

4.2. Đọc và ghi thanh ghi

Ví dụ đọc thanh ghi ID 0xD0 của BMP280/BME280. Địa chỉ là 0x76 hoặc 0x77 tuỳ chân SDO. BMP280 trả 0x58, BME280 trả 0x60:

#define BME_ADDR  (0x76 << 1)

HAL_StatusTypeDef bme_read_id(uint8_t *id)
{
    return HAL_I2C_Mem_Read(&hi2c1, BME_ADDR, 0xD0,
                            I2C_MEMADD_SIZE_8BIT, id, 1, 100);
}

HAL_StatusTypeDef bme_write_reg(uint8_t reg, uint8_t val)
{
    return HAL_I2C_Mem_Write(&hi2c1, BME_ADDR, reg,
                             I2C_MEMADD_SIZE_8BIT, &val, 1, 100);
}

EEPROM có địa chỉ ô nhớ 16 bit (từ 24C32 trở lên) thì dùng I2C_MEMADD_SIZE_16BIT. Sau mỗi lệnh ghi EEPROM, chip cần thời gian ghi nội bộ và sẽ không trả ACK trong lúc đó. Gọi HAL_I2C_IsDeviceReady lặp lại cho đến khi chip trả ACK.

Với khối dữ liệu lớn có HAL_I2C_Mem_Read_DMA và HAL_I2C_Mem_Read_IT, kết thúc báo qua HAL_I2C_MemRxCpltCallback.

4.3. Xử lý lỗi

Kiểm tra giá trị trả về của mọi lệnh HAL, và đọc mã lỗi khi thất bại:

HAL_StatusTypeDef st = bme_read_id(&id);
if (st != HAL_OK) {
    uint32_t err = HAL_I2C_GetError(&hi2c1);
    if (err & HAL_I2C_ERROR_AF)    { /* NACK: sai địa chỉ hoặc thiết bị bận */ }
    if (err & HAL_I2C_ERROR_BERR)  { /* lỗi bus: START hoặc STOP sai vị trí */ }
    if (err & HAL_I2C_ERROR_ARLO)  { /* mất quyền điều khiển bus */ }
    if (st == HAL_TIMEOUT)         { /* bus bị giữ, xem mục gỡ bus treo */ }
}

5. Gỡ bus bị treo

Dấu hiệu: lệnh HAL trả HAL_BUSY mãi, hoặc đo thấy SDA nằm ở mức thấp. Trường hợp hay gặp là MCU reset giữa một giao dịch, trong khi slave đang xuất dở một bit 0 và giữ SDA. Slave chỉ thả SDA khi nó dịch xong byte đó, nên master cần phát thêm xung clock cho nó chạy nốt:

  1. Huỷ khởi tạo ngoại vi I²C (HAL_I2C_DeInit).
  2. Cấu hình SCL và SDA thành GPIO output open-drain.
  3. Phát tối đa 9 xung trên SCL, dừng khi đọc thấy SDA lên cao.
  4. Phát STOP (SDA từ thấp lên cao khi SCL cao).
  5. Khởi tạo lại ngoại vi I²C.
// PB6 = SCL, PB7 = SDA (I2C1), ví dụ
static void i2c_bus_recover(void)
{
    GPIO_InitTypeDef g = { .Pin = GPIO_PIN_6 | GPIO_PIN_7,
                           .Mode = GPIO_MODE_OUTPUT_OD,
                           .Pull = GPIO_NOPULL, .Speed = GPIO_SPEED_FREQ_LOW };
    HAL_I2C_DeInit(&hi2c1);
    HAL_GPIO_Init(GPIOB, &g);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6 | GPIO_PIN_7, GPIO_PIN_SET);

    for (int i = 0; i < 9 && HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) == GPIO_PIN_RESET; i++) {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);   HAL_Delay(1);
    }
    // STOP
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);     HAL_Delay(1);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);       HAL_Delay(1);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);       HAL_Delay(1);

    MX_I2C1_Init();                       // hàm khởi tạo do CubeMX sinh ra
}

Nếu sau 9 xung SDA vẫn thấp, thiết bị đã treo hẳn hoặc đường bị chập. Lúc đó chỉ còn cách ngắt nguồn thiết bị, hoặc kéo chân reset của nó (nếu có) xuống thấp.

6. Lỗi thường gặp

Triệu chứng Nguyên nhân thường gặp Cách xử lý
Quét không thấy thiết bị Quên dịch trái địa chỉ, thiếu điện trở kéo lên, nhầm SCL/SDA, sai địa chỉ (chân ADDR/SDO của module) Đo SCL/SDA bằng logic analyzer, kiểm tra pull-up, thử địa chỉ lân cận
Lỗi NACK (AF) Địa chỉ sai hoặc thiết bị đang bận (ví dụ EEPROM đang ghi) Kiểm tra địa chỉ, thăm dò bằng HAL_I2C_IsDeviceReady
Chạy 100 kHz được, 400 kHz lỗi Pull-up quá lớn hoặc dây dài (Cbus lớn) Đo cạnh lên bằng oscilloscope, giảm điện trở, rút ngắn dây
HAL_BUSY hoặc timeout liên tục Bus bị treo, SDA giữ mức thấp Chạy thủ tục gỡ bus ở mục 5
Hai thiết bị cùng địa chỉ Cùng loại cảm biến trên một bus Đổi chân chọn địa chỉ hoặc dùng bộ ghép kênh I²C (như TCA9548A)
Đọc ra dữ liệu lạ Sai kích thước địa chỉ thanh ghi (8 bit và 16 bit), sai thứ tự byte Đối chiếu datasheet, đổi I2C_MEMADD_SIZE_*
Lỗi khi nối thiết bị 5 V với MCU 3,3 V Pull-up lên 5 V làm SDA/SCL vượt mức chịu của chân MCU Dùng mạch chuyển mức cho I²C, hoặc kéo lên 3,3 V

7. Vài chi tiết khác

Slave được phép giữ SCL ở mức thấp để bắt master chờ (clock stretching). Ngoại vi I²C của STM32 xử lý việc này, và ở chế độ slave có thể tắt bằng tuỳ chọn No Stretch Mode. Nếu tự bit-bang I²C bằng GPIO thì phải kiểm tra SCL đã thật sự lên cao rồi mới đi tiếp.

Một số thiết bị có timeout bus: chuẩn SMBus quy định khoảng 25 đến 35 ms. Nếu SCL bị giữ thấp lâu hơn, thiết bị tự reset giao diện, nên đừng để giao dịch dừng giữa chừng quá lâu.

Một số địa chỉ được dành riêng, như 0x00 (general call) hay dải 0x78 đến 0x7F. Quét bus đôi khi thấy "thiết bị lạ" ở các địa chỉ này.

8. Kế hoạch đo thực tế

Cột "Đo thực tế" để trống cho đến khi chạy trên phần cứng.

Thử nghiệm Cách đo Giá trị tính toán Đo thực tế
Tần số SCL thực ở 100 kHz và 400 kHz Logic analyzer 100 kHz và 400 kHz (CubeMX tự chọn thông số) (điền sau)
Thời gian lên của SDA/SCL với Rp = 1 kΩ, 2,2 kΩ, 4,7 kΩ, 10 kΩ Oscilloscope, đo từ 30% đến 70% VDD Theo Rp × Cbus × 0,8473 (điền sau)
Điện trở lớn nhất còn chạy ổn ở 400 kHz Tăng dần điện trở, ghi lúc bắt đầu lỗi Bus 100 pF: dưới 3,54 kΩ (điền sau)
Thời gian đọc một thanh ghi Logic analyzer, từ START đến STOP 90 µs ở 400 kHz (điền sau)
Gỡ treo bus bằng 9 xung clock Giữ SDA thấp bằng một thiết bị phụ, chạy thủ tục, quan sát SDA lên cao sau không quá 9 xung (điền sau)
Tác động của pull-up song song từ nhiều module Đo điện trở tương đương giữa SDA và VDD Giá trị song song của các điện trở (điền sau)

9. Hỏi đáp

Địa chỉ 7 bit hay 8 bit, sao mỗi nơi ghi một kiểu? Đặc tả I²C dùng địa chỉ 7 bit. Một số datasheet ghi địa chỉ "8 bit" là đã gộp bit R/W vào. HAL của STM32 nhận địa chỉ 7 bit dịch trái 1 bit, nên 0x76 viết là 0x76 << 1, tức 0xEC.

Một bus nối được bao nhiêu thiết bị? Về địa chỉ thì tối đa 128, trừ đi các địa chỉ dành riêng. Giới hạn thật là điện dung bus, tối đa 400 pF theo đặc tả, và điện trở kéo lên tương ứng.

I²C kéo dài được bao xa? I²C dành cho kết nối trong một board hoặc vài chục centimet. Xa hơn thì phải giảm tốc, dùng chip kéo dài bus (bus extender), hoặc chuyển sang RS-485 hay CAN.

Nên dùng ngắt hay DMA? Vài byte thì polling là đủ. Đọc khối lớn theo chu kỳ, ví dụ 100 byte từ IMU, thì DMA giúp CPU rảnh tay, miễn là có callback xử lý lỗi và timeout.

Kết luận

Luôn có điện trở kéo lên và tính giá trị theo điện dung bus. Nhớ dịch trái địa chỉ 7 bit khi gọi HAL. Firmware sản phẩm nên có sẵn hàm gỡ bus treo.

Phần 4 chuyển sang CAN và FDCAN: bus vi sai nhiều master, có bộ lọc, ACK và cơ chế tự phát hiện lỗi, dùng nhiều trong ô tô và công nghiệp.

Để giải mã nhanh dữ liệu cảm biến trả về, có thể dùng Hex Converter của Capitlab.

Tài liệu tham khảo