Skip to content

24 Thí nghiệm I2C

Trong bài này, chúng ta sẽ sử dụng cảm biến nhiệt độ và độ ẩm SHT20 làm ví dụ. Việc giao tiếp với cảm biến được thực hiện thông qua giao thức I2C bằng phần mềm (software I2C) để thu thập dữ liệu nhiệt độ và độ ẩm môi trường xung quanh.

Tài liệu

Datasheet của cảm biến Sensirion SHT20

Link mua cảm biến SHT20

Giới thiệu thí nghiệm

SHT20 là một cảm biến kỹ thuật số đo nhiệt độ và độ ẩm. Nó sử dụng công nghệ đo bằng điện dung, đảm bảo độ chính xác cao và độ ổn định tốt. Cảm biến giao tiếp với vi điều khiển thông qua chuẩn giao tiếp kỹ thuật số I2C.

  • Dải đo nhiệt độ: từ -40°C đến +125°C
  • Dải đo độ ẩm tương đối: từ 0% đến 100% RH

SHT20 được ứng dụng rộng rãi trong các lĩnh vực như:

  • Giám sát chất lượng không khí
  • Quan trắc khí tượng
  • Điều khiển nhiệt độ và độ ẩm chính xác (ví dụ trong nhà máy, kho lạnh)
  • Bảo quản thực phẩm

img

Các thông số kỹ thuật chính của cảm biến SHT20 được minh họa trong hình dưới đây:

image-20250717090840436

image-20250717090857945

image-20250717090914761

Sơ đồ nối dây cảm biến SHT20 với vi điều khiển như sau:

📌 Lưu ý:

  • Cảm biến SHT20 sử dụng giao tiếp I2C nên chỉ cần kết nối 4 dây: VCC, GND, SCLSDA.
  • Đảm bảo có điện trở kéo lên (pull-up) 4.7kΩ hoặc 10kΩ trên đường SDA và SCL nếu không có sẵn trên module.

image-20250717090954711

img

Cấu hình giao tiếp I2C bằng phần mềm

Khi sử dụng I2C bằng phần mềm (software I2C), ta cần thực hiện các bước sau:

  1. Cấu hình chân GPIO Chọn hai chân GPIO để đóng vai trò đường SCL (clock)SDA (data).
  2. Cấu hình thời gian (timing) của I2C Đảm bảo tạo đúng các mức thời gian cần thiết cho tín hiệu start, stop và truyền nhận dữ liệu — thường sử dụng hàm delay đơn giản để điều khiển độ rộng xung.
  3. Xác định quy trình truyền thông Gồm các bước: gửi tín hiệu bắt đầu (start), gửi địa chỉ thiết bị, đọc/ghi dữ liệu, kiểm tra bit ACK, và kết thúc (stop).

Cấu hình chân GPIO

Khi sử dụng giao tiếp I2C bằng phần mềm, cần chọn hai chân GPIO phù hợp để làm dây dữ liệu (SDA) và dây xung nhịp (SCL). Thông thường, có thể chọn bất kỳ chân GPIO nào có thể lập trình được. Tuy nhiên, cần đảm bảo các chân này đáp ứng yêu cầu về mặt thời gian và điện áp của giao thức I2C.

1. Dây dữ liệu (SDA): Là chân dùng để truyền và nhận dữ liệu. Trong quá trình giao tiếp, chân này cần được cấu hình linh hoạt giữa chế độ đầu ra (để gửi dữ liệu) và chế độ đầu vào (để nhận dữ liệu). Dữ liệu được truyền bằng cách thay đổi mức điện áp của chân này theo đúng thời gian quy định.

2. Dây xung nhịp (SCL): Là chân dùng để phát xung đồng hồ điều khiển việc truyền dữ liệu. Cần cấu hình chân này ở chế độ đầu ra. Bộ xử lý sẽ tạo ra các xung cao-thấp để điều khiển quá trình truyền trên đường SDA.

Các yêu cầu khi chọn chân GPIO để làm I2C phần mềm:

  • Chân phải hỗ trợ thay đổi giữa chế độ đầu vào và đầu ra bằng phần mềm.
  • Không bị xung đột với các chức năng ngoại vi khác như USART, SPI, ADC...
  • Đáp ứng yêu cầu về điện áp và dòng điện theo chuẩn I2C.

So với I2C phần cứng, I2C phần mềm yêu cầu nhiều thao tác xử lý hơn, do đó phụ thuộc khá nhiều vào tốc độ của vi điều khiển và khả năng xử lý thời gian thực.

Việc sử dụng macro giúp dễ dàng thay đổi chân hoặc cổng nếu cần thiết mà không phải sửa đổi nhiều dòng mã trong chương trình.

/*
 * Tài liệu phần mềm và phần cứng của bo mạch phát triển Lichuang cũng như các bo mở rộng đều được mã nguồn mở trên trang chính thức:
 * Website bo mạch phát triển: www.lckfb.com
 * Diễn đàn kỹ thuật hỗ trợ lâu dài, hoan nghênh mọi người trao đổi học tập bất kỳ lúc nào
 * Diễn đàn Lichuang: https://oshwhub.com/forum
 * Theo dõi kênh Bilibili: 【立创开发板】để cập nhật thông tin mới nhất
 * Không kiếm tiền từ việc bán bo mạch, mục tiêu là đào tạo kỹ sư
 *
 * Thay đổi lịch sử:
 * Ngày         Tác giả       Ghi chú
 * 2024-03-08   LCKFB-LP     Phiên bản đầu tiên
 */
#ifndef __BSP_SHT20_H__
#define __BSP_SHT20_H__

#include "stm32f4xx.h"

// Cấu hình chân SCL (Clock) của I2C phần mềm
#define RCU_SCL          RCC_AHB1Periph_GPIOB     // Clock cho cổng B
#define PORT_SCL         GPIOB                    // Cổng GPIOB
#define GPIO_SCL         GPIO_Pin_6               // Chân PB6 làm SCL

// Cấu hình chân SDA (Data) của I2C phần mềm
#define RCU_SDA          RCC_AHB1Periph_GPIOB
#define PORT_SDA         GPIOB
#define GPIO_SDA         GPIO_Pin_7               // Chân PB7 làm SDA

// Macro chuyển đổi chế độ của SDA (giữa input và output)
#define SDA_IN()   {SHT20_MODE_SET(GPIO_Mode_IN);}   // Cấu hình SDA là đầu vào
#define SDA_OUT()  {SHT20_MODE_SET(GPIO_Mode_OUT);}  // Cấu hình SDA là đầu ra

// Macro điều khiển mức logic của SCL và SDA
#define SCL(BIT)   GPIO_WriteBit(PORT_SCL, GPIO_SCL, BIT)  // Ghi giá trị ra chân SCL
#define SDA(BIT)   GPIO_WriteBit(PORT_SDA, GPIO_SDA, BIT)  // Ghi giá trị ra chân SDA
#define SDA_GET()  GPIO_ReadInputDataBit(PORT_SDA, GPIO_SDA)  // Đọc giá trị mức logic hiện tại của SDA

// Hàm khởi tạo GPIO cho SHT20
void SHT20_GPIO_INIT(void);

// Đọc dữ liệu từ thanh ghi của SHT20 (địa chỉ thanh ghi truyền vào)
float SHT20_Read(unsigned char regaddr);

// Cấu hình chế độ đầu vào/ra cho SDA
void SHT20_MODE_SET(uint8_t __mode);

#endif

Khởi tạo GPIO

Để thao tác với các chân GPIO, bạn bắt buộc phải thực hiện đầy đủ các bước như: bật clock cho GPIO, cấu hình chế độ làm việc của chân, cấu hình đầu ra, và thiết lập chức năng tương ứng.

Dưới đây là đoạn mã khởi tạo GPIO cho giao tiếp I2C bằng phần mềm:

c
void SHT20_GPIO_INIT(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    // Bật clock cho cổng GPIOB
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);

    // Cấu hình chân SCL (PB6)
    GPIO_InitStructure.GPIO_Pin   = GPIO_SCL;               // Chọn chân SCL
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_OUT;          // Chế độ đầu ra
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;          // Push-pull (đẩy-kéo)
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;      // Tốc độ cao
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_NOPULL;       // Không kéo lên/kéo xuống
    GPIO_Init(PORT_SCL, &GPIO_InitStructure);               // Áp dụng cấu hình cho chân SCL

    // Cấu hình chân SDA (PB7)
    GPIO_InitStructure.GPIO_Pin   = GPIO_SDA;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_NOPULL;
    GPIO_Init(PORT_SDA, &GPIO_InitStructure);
}

Cấu hình thời gian I2C

Phần thời gian (timing) của giao tiếp I2C đã được trình bày chi tiết trong phần 23 nên sẽ không nhắc lại ở đây. Dưới đây là phần hiện thực các hàm cơ bản để tạo tín hiệu giao tiếp I2C bằng phần mềm:

c
/******************************************************************
 * Tên hàm     : IIC_Start
 * Chức năng   : Gửi tín hiệu bắt đầu giao tiếp I2C (Start condition)
 * Tham số vào : Không có
 * Trả về      : Không có
 * Tác giả     : LC
 * Ghi chú     : Không có
******************************************************************/
void IIC_Start(void)
{
    SDA_OUT();     // Thiết lập SDA là đầu ra

    SCL(0);
    SDA(1);
    SCL(1);

    delay_us(5);   // Tạo thời gian ổn định

    SDA(0);        // SDA từ 1 → 0 trong khi SCL = 1 tạo điều kiện START
    delay_us(5);
    SCL(0);
    delay_us(5);
}

/******************************************************************
 * Tên hàm     : IIC_Stop
 * Chức năng   : Gửi tín hiệu dừng giao tiếp I2C (Stop condition)
 * Tham số vào : Không có
 * Trả về      : Không có
 * Tác giả     : LC
 * Ghi chú     : Không có
******************************************************************/
void IIC_Stop(void)
{
    SDA_OUT();

    SCL(0);
    SDA(0);

    SCL(1);
    delay_us(5);
    SDA(1);
    delay_us(5);
}

/******************************************************************
 * Tên hàm     : IIC_Send_Ack
 * Chức năng   : Chủ (master) gửi tín hiệu phản hồi
 * Tham số vào : ack – 0 = gửi ACK, 1 = gửi NACK
 * Trả về      : Không có
 * Tác giả     : LC
 * Ghi chú     : Không có
******************************************************************/
void IIC_Send_Ack(uint8_t ack)
{
    SDA_OUT();
    SCL(0);
    SDA(0);
    delay_us(5);

    if(!ack)
        SDA(0);    // Gửi ACK (bit 0)
    else
        SDA(1);    // Gửi NACK (bit 1)

    SCL(1);
    delay_us(5);
    SCL(0);
    SDA(1);        // Nhả đường SDA
}

/******************************************************************
 * Tên hàm     : IIC_Wait_Ack
 * Chức năng   : Chờ thiết bị slave gửi tín hiệu phản hồi (ACK)
 * Tham số vào : Không có
 * Trả về      : 1 = không có phản hồi (NACK), 0 = có phản hồi (ACK)
 * Tác giả     : LC
 * Ghi chú     : Không có
******************************************************************/
uint8_t IIC_Wait_Ack(void)
{
    char ack = 0;
    unsigned char ack_flag = 10;

    SDA_IN();       // Đặt SDA ở chế độ input
    SDA(1);
    delay_us(5);
    SCL(1);
    delay_us(5);

    // Chờ SDA được kéo xuống bởi slave
    while((SDA_GET() == 1) && ack_flag)
    {
        ack_flag--;
        delay_us(5);
    }

    if (ack_flag <= 0)
    {
        IIC_Stop();     // Nếu không có phản hồi thì kết thúc
        return 1;       // Không nhận được ACK
    }
    else
    {
        SCL(0);
        SDA_OUT();      // Trả lại quyền điều khiển SDA
    }

    return ack;         // Trả về 0 nếu nhận được ACK
}
/******************************************************************
 * Tên hàm     : IIC_Write
 * Chức năng   : Ghi 1 byte dữ liệu qua giao tiếp I2C
 * Tham số vào : dat – dữ liệu cần ghi
 * Trả về      : Không có
 * Tác giả     : LC
 * Ghi chú     : Không có
******************************************************************/
void IIC_Write(uint8_t data)
{
    int i = 0;
    SDA_OUT();
    SCL(0); // Bắt đầu truyền

    for(i = 0; i < 8; i++)
    {
        SDA((data & 0x80) >> 7);  // Gửi bit cao nhất trước (MSB)
        delay_us(2);
        data <<= 1;
        delay_us(6);
        SCL(1); // Tạo xung đồng hồ
        delay_us(4);
        SCL(0);
        delay_us(4);
    }
}

/******************************************************************
 * Tên hàm     : IIC_Read
 * Chức năng   : Đọc 1 byte dữ liệu từ giao tiếp I2C
 * Tham số vào : Không có
 * Trả về      : Giá trị 1 byte đọc được
 * Tác giả     : LC
 * Ghi chú     : Không có
******************************************************************/
uint8_t IIC_Read(void)
{
    unsigned char i, receive = 0;
    SDA_IN(); // Cấu hình SDA là đầu vào

    for(i = 0; i < 8; i++)
    {
        SCL(0);
        delay_us(5);
        SCL(1);
        delay_us(5);

        receive <<= 1;
        if(SDA_GET())
            receive |= 1;  // Nếu SDA đang là 1, ghi vào bit thấp nhất

        delay_us(5);
    }

    return receive;
}

Xác định các bước truyền thông

Trong ví dụ này, mục tiêu của chúng ta là đọc dữ liệu nhiệt độ và độ ẩm từ cảm biến SHT20 thông qua giao tiếp I2C.

Để thực hiện giao tiếp I2C, trước tiên cần biết địa chỉ thiết bị của cảm biến SHT20 cũng như quy trình truyền thông với nó.

Địa chỉ I2C của SHT20

  • Cảm biến SHT20 sử dụng địa chỉ 7 bit: 0b1000000 (hay 0x40 nếu dịch sang 7 bit tiêu chuẩn).
  • Trong giao tiếp I2C, địa chỉ thiết bị đầy đủ là 8 bit, trong đó:
    • 7 bit đầu là địa chỉ thiết bị
    • Bit cuối cùng (bit thứ 0) xác định chế độ truyền:
      • 0: ghi (write)
      • 1: đọc (read)

Vì vậy:

Chế độĐịa chỉ nhị phânĐịa chỉ hex
Ghi1000_00000x80
Đọc1000_00010x81

Lưu ý: Việc phân biệt giữa 0x800x81 là rất quan trọng để tránh lỗi khi đọc hoặc ghi dữ liệu qua bus I2C.

image-20250717091201587

Giải thích địa chỉ thiết bị

Lệnh đo

Cảm biến SHT20 hỗ trợ hai chế độ đo khác nhau: chế độ điều khiển bởi master (Hold Master Mode)chế độ không giữ master (No Hold Master Mode).

  • Chế độ Hold Master:
    • Trong quá trình đo, chân SCL sẽ bị cảm biến chiếm quyền điều khiển.
    • Master (vi điều khiển) không thể thực hiện các giao tiếp I2C khác trong lúc này.
    • Được thiết kế cho trường hợp master chờ dữ liệu sẵn sàng.
    • Giải thích dễ hiểu hơn: Là MCU sẽ ngồi đợi cảm biến đo dữ liệu và trả kết quả và không được sử dụng các thiết bị I2C khác trong thời gian đó.
  • Chế độ No Hold Master:
    • Trong quá trình đo, SCL vẫn tự do, master có thể thực hiện các giao tiếp I2C khác.
    • Thích hợp khi cần đo không đồng bộ hoặc xử lý đa nhiệm trên bus I2C.
    • Giải thích dễ hiểu hơn: Tranh thủ thời gian SHT20 đo dữ liệu, MCU có thể điều khiển những thiết bị I2C khác như màn hình, đồng hồ RTC rồi quay lại đọc dữ liệu từ SHT20
    • Trường hợp này được sử dụng trong ví dụ của chúng ta.

Các lệnh đo tương ứng:

Loại đoChế độ Hold MasterChế độ No Hold MasterGiá trị hex
Đo nhiệt độ0b111000110b111100110xE3 / 0xF3
Đo độ ẩm0b111001010b111101010xE5 / 0xF5

Trong tài liệu này, chúng ta sử dụng chế độ No Hold Master, vì vậy:

  • Lệnh đo nhiệt độ0xF3
  • Lệnh đo độ ẩm0xF5

image-20250717091352457

Giải thích tập lệnh

Tình huống giao tiếp thực tế (trong chế độ No Hold Master)

Theo tài liệu kỹ thuật của SHT20, quy trình giao tiếp I2C được mô tả rõ ràng. Trong bài này, ta chỉ xét quy trình trong chế độ No Hold Master. Nếu bạn muốn biết thêm về chế độ Hold Master, vui lòng tham khảo datasheet chính thức của SHT20.

Quy trình giao tiếp cơ bản

  1. MCU khởi động giao tiếp:

    • Gửi tín hiệu Start (bắt đầu truyền).
    • Gửi địa chỉ thiết bị + bit ghi (Write)0x80.
    • Chờ phản hồi ACK từ cảm biến.

    Nếu không có phản hồi, cần kiểm tra lại dây kết nối vật lý.

  2. Gửi lệnh đo:

    Ví dụ:

    • 0xF3: đo nhiệt độ (chế độ No Hold)
    • 0xF5: đo độ ẩm (chế độ No Hold)
  3. Kết thúc truyền (Stop):

    • Sau khi gửi lệnh xong, dừng giao tiếp.
    • Cảm biến bắt đầu thực hiện phép đo.
  4. MCU truy vấn trạng thái (Polling):

    • Sau vài mili giây, MCU bắt đầu truy vấn xem dữ liệu đã sẵn sàng chưa.
    • Gửi lại Start, địa chỉ thiết bị kèm bit đọc (Read)0x81.
  5. Đọc dữ liệu:

    • Nếu dữ liệu đã sẵn sàng, cảm biến gửi phản hồi ACK → có thể đọc 2 byte dữ liệu + 1 byte CRC.
    • Nếu chưa sẵn sàng, không có ACK → MCU phải gửi lại chuỗi Start sau một thời gian chờ ngắn.

Giải mã dữ liệu trả về

  • Tổng dữ liệu trả về: 2 byte chính + 1 byte CRC.
  • Độ phân giải tối đa: 14 bit.
  • 2 bit cuối (LSBs) của byte thứ hai:
    • bit1: chỉ loại phép đo (0 = nhiệt độ, 1 = độ ẩm)
    • bit0: không sử dụng (không có ý nghĩa)

📌 Vì vậy, khi xử lý dữ liệu, bạn nên bỏ đi 2 bit cuối nếu không cần kiểm tra loại phép đo hoặc CRC.

image-20250717091721061

Cấu hình đọc dữ liệu nhiệt độ/độ ẩm

Dựa theo các ví dụ giao tiếp được mô tả trong datasheet của SHT20, chúng ta có thể xác nhận quy trình đọc dữ liệu từ cảm biến ở chế độ No Hold Master.

Dưới đây là ví dụ minh họa quá trình đọc nhiệt độ:

  1. Bắt đầu truyền thông:
    • Gửi tín hiệu Start (bắt đầu giao tiếp)
    • Gửi địa chỉ thiết bị + bit ghi (Write) = 0x80 (ứng với bit 1 đến bit 8 trong sơ đồ giao tiếp)
  2. Chờ phản hồi từ cảm biến (ACK):
    • Nếu cảm biến phản hồi, tiếp tục bước tiếp theo.
    • Nếu không có phản hồi, cần kiểm tra lại kết nối vật lý hoặc nguồn cấp.
  3. Gửi lệnh đo nhiệt độ:
    • Trong chế độ No Hold Master, gửi lệnh 0xF3 để bắt đầu quá trình đo nhiệt độ.
  4. Kết thúc lần truyền đầu tiên:
    • Gửi tín hiệu Stop, sau đó đợi vài mili giây để cảm biến xử lý phép đo.
  5. Bắt đầu lần truyền thứ hai (đọc dữ liệu):
    • Gửi lại tín hiệu Start
    • Gửi địa chỉ thiết bị + bit đọc (Read) = 0x81
  6. Chờ phản hồi từ cảm biến:
    • Nếu cảm biến gửi ACK, tiến hành đọc 2 byte dữ liệu nhiệt độ và 1 byte CRC.

Toàn bộ quá trình trên phải tuân thủ đúng thứ tự để đảm bảo nhận được dữ liệu hợp lệ.

c
IIC_Start();               // Gửi tín hiệu bắt đầu (Start condition)
IIC_Send_Byte(0x80);       // Gửi địa chỉ thiết bị kèm bit ghi (Write)

// Nếu không nhận được phản hồi (ACK) từ thiết bị slave,
// in ra thông báo lỗi qua UART
if (IIC_Wait_Ack() == 1)
    printf("receive fail -1\r\n");

Sau khi gửi thành công địa chỉ thiết bị kèm bit ghi, tiếp theo là gửi lệnh đo (tương ứng với bit 10 đến bit 17 trong sơ đồ truyền dữ liệu – tham khảo phần 【Giải thích tập lệnh】).

Trong ví dụ này, ta sử dụng lệnh đo nhiệt độ ở chế độ No Hold Master, có mã lệnh là 0xF3.

Sau khi gửi lệnh, cần tiếp tục chờ phản hồi (ACK) từ thiết bị để xác nhận rằng lệnh đã được cảm biến tiếp nhận thành công.

c
IIC_Send_Byte(0xF3); // Gửi lệnh đo nhiệt độ ở chế độ No Hold Master

// Nếu không nhận được phản hồi từ cảm biến (ACK),
// in ra lỗi qua UART với mã lỗi -2
if (IIC_Wait_Ack() == 1)
{
    printf("receive fail -2\r\n");
}

Sau khi gửi lệnh đo xong, MCU cần gửi lại tín hiệu bắt đầu (Start) và địa chỉ thiết bị + bit đọc (Read)0x81 (tương ứng với bit 19 ~ bit 26 trong sơ đồ truyền hình 16).

Mục đích của bước này là thông báo cho cảm biến rằng MCU sẵn sàng đọc dữ liệu nhiệt độ.

  • Nếu cảm biến đã xử lý xong nội bộ, nó sẽ phản hồi bằng tín hiệu xác nhận ACK (bit 27).
  • Lúc này, MCU có thể đọc dữ liệu nhiệt độ từ cảm biến.

Tuy nhiên:

  • Nếu cảm biến chưa xử lý xong, nó sẽ phản hồi bằng NACK (bit 27 = 1).
  • Khi đó, MCU cần đợi thêm một khoảng thời gian, sau đó gửi lại chuỗi Start + địa chỉ đọc để kiểm tra lại.
c
// Gửi tín hiệu Start và địa chỉ thiết bị + đọc (0x81)
// Nếu cảm biến chưa sẵn sàng, sẽ phản hồi NACK → lặp lại gửi cho đến khi nhận được ACK
do
{
    delay_us(10);         // Đợi cảm biến hoàn tất quá trình đo (tránh hỏi quá sớm)
    IIC_Start();          // Gửi tín hiệu bắt đầu (Start)
    IIC_Send_Byte(0x81);  // Gửi địa chỉ thiết bị + yêu cầu đọc dữ liệu (0x81)
}
while (IIC_Wait_Ack() == 1); // Nếu chưa có ACK thì lặp lại

Sau khi cảm biến SHT20 phản hồi tín hiệu ACK, vi điều khiển sẽ tiến hành đọc 3 byte dữ liệu liên tiếp từ cảm biến thông qua giao tiếp I2C:

  1. Byte thứ nhất: Dữ liệu cao (MSB) – 8 bit đầu tiên
  2. Byte thứ hai: Dữ liệu thấp (LSB) – 8 bit tiếp theo
  3. Byte thứ ba: CRC – dùng để kiểm tra tính toàn vẹn của dữ liệu

Trong đoạn mã, các biến tương ứng như sau:

  • data_msb: byte dữ liệu cao 8 bit
  • data_lsb: byte dữ liệu thấp 8 bit
  • check: byte kiểm tra CRC (cyclic redundancy check)
c
data_msb = IIC_Read_Byte();  // Đọc byte đầu tiên (8 bit cao của dữ liệu)
IIC_Send_Ack();              // Gửi ACK để báo rằng sẵn sàng đọc byte tiếp theo

data_lsb = IIC_Read_Byte();  // Đọc byte thứ hai (8 bit thấp của dữ liệu)
IIC_Send_Ack();              // Gửi ACK tiếp tục đọc

check = IIC_Read_Byte();     // Đọc byte thứ ba (CRC kiểm tra dữ liệu)
IIC_Send_Nack();             // Gửi NACK để báo rằng đã đọc xong
IIC_Stop();                  // Gửi tín hiệu Stop kết thúc giao tiếp I2C

Sau khi nhận được 2 byte dữ liệu (MSB và LSB), cần kết hợp chúng thành một giá trị 16 bit. Theo yêu cầu trong datasheet của cảm biến SHT20, 2 bit cuối của byte thấp (LSB) là các bit trạng thái, không thuộc giá trị đo, nên cần được xóa (set bằng 0).

Cụ thể:

  • Dat là biến 16 bit dùng để lưu trữ giá trị sau khi kết hợp.
  • Cần thực hiện phép dịch bit và "AND" logic để loại bỏ 2 bit cuối.

image-20250717092339708

Giải thích bit trạng thái

Dữ liệu được cảm biến SHT20 trả về gồm 2 byte (16 bit), tuy nhiên 2 bit cuối cùng trong byte thấp (LSB) không thuộc dữ liệu đo mà được dùng làm bit trạng thái. Vì vậy, khi xử lý dữ liệu, cần xóa 2 bit này để đảm bảo giá trị đo chính xác.

c
// Hợp nhất dữ liệu từ hai byte
dat = data_msb << 8;    // Dịch 8 bit cao vào vị trí MSB
dat = dat | data_lsb;   // Ghép thêm 8 bit thấp vào LSB
dat &= ~(0x03);         // Xóa 2 bit cuối (bit trạng thái), giữ lại 14 bit giá trị thực

Quy đổi dữ liệu đo

Dữ liệu nhận được từ cảm biến SHT20 cần được chuyển đổi sang giá trị thực tế để sử dụng. Dưới đây là công thức quy đổi độ ẩm tương đối dựa trên tài liệu kỹ thuật:

image-20250717092532245

Tương tự, nếu lệnh gửi là đo nhiệt độ (0xF3) thì giá trị trả về dat sẽ được quy đổi sang độ C bằng công thức:

image-20250717092800896

Công thức chuyển đổi nhiệt độ

Sau khi cảm biến trả về 2 byte dữ liệu, ta cần kết hợp lại thành một giá trị 16 bit. Giá trị này được gọi là:

  • S(T) – dữ liệu thô đo nhiệt độ
  • S(RH) – dữ liệu thô đo độ ẩm

Theo datasheet của cảm biến SHT20, công thức chuyển đổi nhiệt độ như sau:

c
// Quy đổi dữ liệu đo được sang giá trị nhiệt độ thực tế
// temp là biến kiểu float, 2^16 = 65536
temp = (dat / 65536.0) * 175.72 - 46.85;

Dưới đây là hàm C hoàn chỉnh, đã tổng hợp tất cả các bước gồm: giao tiếp I2C bằng phần mềm, đọc dữ liệu từ SHT20 ở chế độ No Hold Master, xử lý bit trạng thái, và tính toán cả nhiệt độ và độ ẩm. Hàm này trả về kết quả qua biến tham chiếu:

c
/******************************************************************
 * Tên hàm     : SHT20_Read
 * Chức năng   : Đo nhiệt độ hoặc độ ẩm từ cảm biến SHT20
 * Tham số vào : regaddr – địa chỉ thanh ghi lệnh:
 *               = 0xF3 để đo nhiệt độ (No Hold Master Mode)
 *               = 0xF5 để đo độ ẩm (No Hold Master Mode)
 * Trả về      : Giá trị nhiệt độ (°C) hoặc độ ẩm (%RH) dạng float
 * Tác giả     : LC
 * Ghi chú     : Không kiểm tra CRC, dữ liệu được xử lý đơn giản
******************************************************************/

float SHT20_Read(uint8_t regaddr)
{
    unsigned char data_H = 0;
    unsigned char data_L = 0;
    float temp = 0;
    IIC_Start();
    IIC_Write(0x80|0);
    if( IIC_Wait_Ack() == 1 ) printf("error -1\r\n");
    IIC_Write(regaddr); 
    if( IIC_Wait_Ack() == 1 ) printf("error -2\r\n");

    do{
    delay_us(10);
    IIC_Start();
    IIC_Write(0x80|1);

    }while( IIC_Wait_Ack() == 1 );

    delay_us(20);

    data_H = IIC_Read();
    IIC_Send_Ack(0);
    data_L = IIC_Read();
    IIC_Send_Ack(1);
    IIC_Stop();

    if( regaddr == 0xf3 )
    {
        temp = ((data_H<<8)|data_L) / 65536.0 * 175.72 - 46.85;
    }
    if( regaddr == 0xf5 )
    {
        temp = ((data_H<<8)|data_L) / 65536.0 * 125.0 - 6;
    }
   return temp;

}

Xác minh giao tiếp I2C bằng phần mềm

Trong file main.c, ta có thể gọi hàm SHT20_Read() để thu thập dữ liệu nhiệt độ và độ ẩm từ môi trường xung quanh.

c
/*
 * Tài liệu phần cứng và phần mềm của bo mạch phát triển Lichuang và các bo mở rộng đều được mã nguồn mở.
 * Trang web chính thức: www.lckfb.com
 * Hỗ trợ kỹ thuật luôn có mặt tại diễn đàn – hoan nghênh mọi người cùng trao đổi học tập.
 * Diễn đàn chính thức: https://oshwhub.com/forum
 * Theo dõi kênh Bilibili: 【立创开发板】để cập nhật thông tin mới nhất!
 * Không kiếm lợi nhuận từ bán bo mạch – mục tiêu là đào tạo kỹ sư Trung Quốc.
 *
 * Thay đổi lịch sử:
 * Ngày         Tác giả     Ghi chú
 * 2024-03-08   LCKFB-LP    Phiên bản đầu tiên
 */

#include "board.h"
#include "bsp_uart.h"
#include <stdio.h>
#include "bsp_sht20.h"

int main(void)
{
    board_init();                // Khởi tạo hệ thống (xung clock, GPIO, NVIC...)
    uart1_init(115200U);        // Khởi tạo UART1 với baudrate 115200

    SHT20_GPIO_INIT();          // Khởi tạo các chân SDA/SCL cho I2C phần mềm

    delay_ms(20);               // Đợi cảm biến SHT20 khởi động xong (thời gian cấp nguồn)

    while (1)
    {
        // Đọc và hiển thị nhiệt độ
        printf("temp = %.2f\r\n", SHT20_Read(0xf3));

        // Đọc và hiển thị độ ẩm
        printf("humi = %.2f\r\n", SHT20_Read(0xf5));

        printf("\r\n");
        delay_ms(500);          // Đợi 500ms trước khi đọc lại
    }
}

Kết quả hiển thị qua UART:

img

Kết quả hiển thị qua UART

Khi chương trình chạy thành công, bạn sẽ thấy kết quả in ra qua cổng UART như sau:

c
temp = 25.87
humi = 53.62

Vị trí mã nguồn của chương trình

Mã nguồn của chương trình mẫu trong chương này nằm tại: