STM32 phần mềm I2C đọc dữ liệu cảm biến nhiệt độ và độ ẩm AM2320

STM32 MCU đọc dữ liệu cảm biến nhiệt độ, độ ẩm AM2320 qua I2C phần mềm và hiển thị lên màn hình OLED 0,96 inch.

Mình dùng board STM32F103C8T6, code viết bằng thư viện chuẩn ST.

STM32 dùng I2C cứng đọc cảm biến nhiệt độ, độ ẩm SHTC3: https://blog.zeruns.com/archives/692.html
STM32 đọc dữ liệu cảm biến nhiệt độ, độ ẩm AHT10: https://blog.zeruns.com/archives/693.html

Nhóm thảo luận kỹ thuật điện tử / MCU: 2169025065

Ảnh thực tế

Giới thiệu nhanh về giao thức I2C

Giao thức I2C (Inter-Integrated Circuit) do hãng Philips phát triển; ít chân, dễ triển khai phần cứng, mở rộng tốt, không cần thêm IC chuyển đổi mức như USART hay CAN, nên ngày nay được dùng phổ biến để giao tiếp giữa các IC trong hệ thống.

I2C chỉ có một đường dữ liệu SDA (Serial Data Line), truyền từng bit, thuộc loại truyền thông song công bán song công (half-duplex): hai chiều nhưng phải luân phiên, cùng lúc chỉ một chiều, đủ một dây dữ liệu.

Trong bài này ta chia I2C thành 2 lớp:

  • Lớp vật lý: quy định tính chất cơ khí, điện (phần cứng) để đảm bảo tín hiệu nguyên vẹn.
  • Lớp giao thức: quy định logic, cách đóng gói / giải nén dữ liệu cho khớp giữa hai bên (phần mềm).

Lớp vật lý I2C

Cách nối thông dụng giữa các thiết bị I2C

  1. Là bus đa chủ / đa từ: nhiều master và nhiều slave cùng chia sẻ 2 tuyến tín hiệu.
  2. Chỉ cần 2 dây: SDA (data) và SCL (clock).
  3. Mỗi đường kéo lên (pull-up) qua điện trở lên nguồn. Khi thiết bị ở trạng thái rảnh nó xuất trạng thái high-Z; tất cả đều high-Z thì điện trở kéo bus lên mức cao.
  4. GPIO của MCU phải đặt chế độ open-drain nếu không sẽ có nguy cơ chập.

Chi tiết thêm về I2C trên STM32 xem tại: https://url.zeruns.com/JC0Ah

Cảm biến nhiệt độ, độ ẩm AM2320

Giới thiệu

AM2320 là cảm biến kỹ thuật số đã hiệu chuẩn sẵn, đầu ra là tín hiệu số. Dùng công nghệ đo ẩm điện dung và cảm biến nhiệt độ chính xác cao, kết nối với MCU. Có thể giao tiếp qua 1-Wire hoặc I2C chuẩn, tần số clock ≤ 100 kHz. Nguồn 3,1 – 5,5 V. Độ ẩm 0 – 99,9 % (±3 %), nhiệt độ –40 – 80 °C (±0,5 °C). Tín hiệu đã bù nhiệt độ, kèm CRC. Kích thước nhỏ, tiêu thụ thấp, dây dẫn dài đến 20 m.

Datasheet AM2320: https://url.zeruns.com/74o6F



Tóm tắt thông số:

  • Nhiệt độ: –40 °C ÷ 80 °C, sai số ±0,5 °C
  • Độ ẩm: 0 % ÷ 99,9 %, sai số ±3 %
  • Điện áp: 3,1 V ÷ 5,5 V
  • Giao tiếp: I2C hoặc 1-Wire
  • Tần số I2C: ≤ 100 kHz

Địa chỉ thiết bị & lệnh đọc / ghi

Byte đầu sau START gồm 7 bit địa chỉ (mặc định 0b1011 100) + 1 bit R/W:

  • Ghi: 0xB8 (0b1011 1000)
  • Đọc: 0xB9 (0b1011 1001)

Quy trình đọc dữ liệu




Một chu kỳ đọc gồm 3 bước:

  1. Đánh thức cảm biến
  2. Gửi lệnh đọc
  3. Đọc dữ liệu trả về

Chi tiết:

  1. Đánh thức: START → gửi 0xB8 → chờ > 800 µs → STOP
  2. Gửi lệnh đọc: START → 0xB8 (SLA) → 0x03 (mã chức năng) → 0x00 (địa chỉ bắt đầu) → 0x04 (số thanh ghi) → STOP
  3. Đọc dữ liệu: START → 0xB9 → liên tục nhận 8 byte:
    • Byte 0: độ dài dữ liệu
    • Byte 1: độ ẩm cao
    • Byte 2: độ ẩm thấp
    • Byte 3: nhiệt độ cao
    • Byte 4: nhiệt độ thấp
    • Byte```c
      #include “stm32f10x.h” // Device header
      #include “Delay.h”
      #include “OLED.h”
      #include “AM2320.h”
      #include “IWDG.h”

int main(void)
{
IWDG_Configuration(); //Khởi tạo Watchdog
AM2320_I2C_Init();
OLED_Init();

OLED_ShowString(1, 1, "T:");
OLED_ShowString(2, 1, "H:");

uint16_t i = 0;
uint16_t err_count = 0;

while (1)
{
	OLED_ShowNum(4, 1, i, 5);
	float Temp, Hum; //Khai báo biến lưu dữ liệu nhiệt độ và độ ẩm

	if (ReadAM2320(&Hum, &Temp)) //Đọc dữ liệu nhiệt độ và độ ẩm
	{
		if (Temp >= 0)
		{
			char String[10];
			sprintf(String, "+%.2fC", Temp); //Định dạng chuỗi xuất ra biến String
			OLED_ShowString(1, 3, String);	 //Hiển thị nhiệt độ

			sprintf(String, " %.2f%%", Hum); //Định dạng chuỗi xuất ra biến String
			OLED_ShowString(2, 3, String);	 //Hiển thị độ ẩm
		}
		else
		{
			char String[10];
			sprintf(String, "-%.2fC", Temp); //Định dạng chuỗi xuất ra biến String
			OLED_ShowString(1, 3, String);	 //Hiển thị nhiệt độ

			sprintf(String, " %.2f%%", Hum); //Định dạng chuỗi xuất ra biến String
			OLED_ShowString(2, 3, String);	 //Hiển thị độ ẩm
		}
	}
	else
	{
		err_count++;
		OLED_ShowNum(3, 1, err_count, 5); //Hiển thị bộ đếm số lần lỗi
	}
	Delay_ms(100);
	i++;
	if (i >= 99999)
		i = 0;
	if (err_count >= 99999)
		err_count = 0;
	IWDG_FeedDog(); //Cho Watchdog ăn (nếu quá 1 giây không cho ăn sẽ tự động reset)
}
// blog.zeruns.com

}


### AM2320.c

```c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"

/*
Tác giả blog:https://blog.zeruns.com
WeChat công chúng:zeruns-gzh
Trang chủ Bilibili:https://space.bilibili.com/8320520
*/

/*Địa chỉ AM2320*/
#define AM2320_ADDRESS 0xB8

/*Cấu hình chân*/
#define AM2320_SCL GPIO_Pin_12
#define AM2320_SDA GPIO_Pin_13
#define AM2320_W_SCL(x) GPIO_WriteBit(GPIOB, AM2320_SCL, (BitAction)(x))
#define AM2320_W_SDA(x) GPIO_WriteBit(GPIOB, AM2320_SDA, (BitAction)(x))
#define AM2320_R_SDA() GPIO_ReadInputDataBit(GPIOB, AM2320_SDA)
#define AM2320_R_SCL() GPIO_ReadInputDataBit(GPIOB, AM2320_SCL)
/*Khi GPIO của STM32 được cấu hình ở chế độ xuất mở (open-drain), nó vẫn có thể đọc
mức điện áp đầu vào từ thanh ghi dữ liệu đầu vào, tức là nó đồng thời có chức năng của chế độ đầu vào nổi (floating input)*/

/**
 * @brief  Tính toán CRC
 * @param  *ptr Dữ liệu byte cần tính (lưu dưới dạng mảng)
 * @param  len  Số byte cần tính (độ dài mảng)
 * @retval Mã kiểm tra CRC
 */
unsigned short CRC16(unsigned char *ptr, unsigned char len)
{
	unsigned short crc = 0xFFFF;
	unsigned char i;
	while (len--)
	{
		crc ^= *ptr++;
		for (i = 0; i < 8; i++)
		{
			if (crc & 0x01)
			{
				crc >>= 1;
				crc ^= 0xA001;
			}
			else
			{
				crc >>= 1;
			}
		}
	}
	return crc;
}

/**
 * @brief  Bắt đầu I2C
 * @param  Không
 * @retval Không
 */
void AM2320_I2C_Start(void)
{
	AM2320_W_SDA(1);
	Delay_us(2); //Trễ 2 micro giây
	AM2320_W_SCL(1);
	Delay_us(4);
	AM2320_W_SDA(0);
	Delay_us(3);
	AM2320_W_SCL(0);
	Delay_us(5);
}

/**
 * @brief  Dừng I2C
 * @param  Không
 * @retval Không
 */
void AM2320_I2C_Stop(void)
{
	AM2320_W_SDA(0);
	Delay_us(3);
	AM2320_W_SCL(1);
	Delay_us(4);
	AM2320_W_SDA(1);
	Delay_us(4);
}

/**
 * @brief  I2C gửi một byte
 * @param  Byte Byte cần gửi
 * @retval Không
 */
void AM2320_I2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i++)
	{
		AM2320_W_SDA((Byte << i) & 0x80);
		AM2320_W_SCL(1);
		Delay_us(4);
		AM2320_W_SCL(0);
		Delay_us(5);
	}
	AM2320_W_SDA(1); //Giải phóng tổng tiếp SDA
}

/**
 * @brief  Chờ tín hiệu ACK
 * @param  Không
 * @retval 1-Không ACK, 0-ACK
 */
uint8_t WaitAck(void)
{
	uint8_t ret;

	AM2320_W_SCL(1);
	Delay_us(4);
	if (AM2320_R_SDA())
	{
		ret = 1;
	}
	else
	{
		ret = 0;
	}
	AM2320_W_SCL(0);
	Delay_us(5);
	return ret;
}

/**
 * @brief  I2C đọc một byte
 * @param  NACK 1-Không ACK, 0-ACK
 * @retval Byte dữ liệu đọc được
 */
uint8_t AM2320_I2C_ReadByte(uint8_t NACK)
{
	uint8_t i, Byte = 0;
	AM2320_W_SDA(1); //Giải phóng tổng tiếp SDA
	for (i = 0; i < 8; i++)
	{
		AM2320_W_SCL(1);
		Delay_us(4);
		Byte = Byte | (AM2320_R_SDA() << (7 - i));
		AM2320_W_SCL(0);
		Delay_us(5);
	}
	AM2320_W_SDA(NACK); //Gửi tín hiệu ACK/Không ACK
	AM2320_W_SCL(1);
	Delay_us(4);
	AM2320_W_SCL(0);
	Delay_us(5);
	AM2320_W_SDA(1); //Giải phóng tổng tiếp SDA
	return Byte;
}

/*Đánh thức cảm biến*/
void AM2320_Wake(void)
{
	AM2320_I2C_Start();
	AM2320_I2C_SendByte(AM2320_ADDRESS);
	WaitAck();
	Delay_us(1000); //Trễ 1000 micro giây
	AM2320_I2C_Stop();
}

/*Khởi tạo chân*/
void AM2320_I2C_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //Bật clock GPIOB

	GPIO_InitTypeDef GPIO_InitStructure;				 //Khai báo cấu trúc cấu hình GPIO
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD### OLED.c

```c
#include "stm32f10x.h"
#include "OLED_Font.h"

/*Cấu hình chân*/
#define OLED_SCL GPIO_Pin_12
#define OLED_SDA GPIO_Pin_13
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, OLED_SCL, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, OLED_SDA, (BitAction)(x))

/*Khởi tạo chân*/
void OLED_I2C_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = OLED_SCL;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = OLED_SDA;
	GPIO_Init(GPIOB, &GPIO_InitStructure);

	OLED_W_SCL(1);
	OLED_W_SDA(1);
}

/**
 * @brief  I2C bắt đầu
 * @param  Không
 * @retval Không
 */
void OLED_I2C_Start(void)
{
	OLED_W_SDA(1);
	OLED_W_SCL(1);
	OLED_W_SDA(0);
	OLED_W_SCL(0);
}

/**
 * @brief  I2C dừng
 * @param  Không
 * @retval Không
 */
void OLED_I2C_Stop(void)
{
	OLED_W_SDA(0);
	OLED_W_SCL(1);
	OLED_W_SDA(1);
}

/**
 * @brief  I2C gửi một byte
 * @param  Byte byte cần gửi
 * @retval Không
 */
void OLED_I2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i++)
	{
		OLED_W_SDA(Byte & (0x80 >> i));
		OLED_W_SCL(1);
		OLED_W_SCL(0);
	}
	OLED_W_SDA(1); //Giải phóng tổng hợp SDA
	OLED_W_SCL(1); //Một xung clock thêm, không xử lý tín hiệu ACK
	OLED_W_SCL(0);
}

/**
 * @brief  OLED viết lệnh
 * @param  Command lệnh cần viết
 * @retval Không
 */
void OLED_WriteCommand(uint8_t Command)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78); //Địa chỉ slave
	OLED_I2C_SendByte(0x00); //Viết lệnh
	OLED_I2C_SendByte(Command);
	OLED_I2C_Stop();
}

/**
 * @brief  OLED viết dữ liệu
 * @param  Data dữ liệu cần viết
 * @retval Không
 */
void OLED_WriteData(uint8_t Data)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78); //Địa chỉ slave
	OLED_I2C_SendByte(0x40); //Viết dữ liệu
	OLED_I2C_SendByte(Data);
	OLED_I2C_Stop();
}

/**
 * @brief  OLED đặt vị trí con trỏ
 * @param  Y tọa độ hướng xuống, gốc trên-trái, phạm vi: 0~7
 * @param  X tọa độ hướng phải, gốc trên-trái, phạm vi: 0~127
 * @retval Không
 */
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
	OLED_WriteCommand(0xB0 | Y);			 //Đặt vị trí Y
	OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //Đặt 4 bit thấp vị trí X
	OLED_WriteCommand(0x00 | (X & 0x0F));		 //Đặt 4 bit cao vị trí X
}

/**
 * @brief  OLED xóa màn hình
 * @param  Không
 * @retval Không
 */
void OLED_Clear(void)
{
	uint8_t i, j;
	for (j = 0; j < 8; j++)
	{
		OLED_SetCursor(j, 0);
		for (i = 0; i < 128; i++)
		{
			OLED_WriteData(0x00);
		}
	}
}

/**
 * @brief  OLED xóa một phần màn hình
 * @param  Line vị trí hàng, phạm vi: 1~4
 * @param  start vị trí cột bắt đầu, phạm vi: 1~16
 * @param  end vị trí cột kết thúc, phạm vi: 1~16
 * @retval Không
 */
void OLED_Clear_Part(uint8_t Line, uint8_t start, uint8_t end)
{
	uint8_t i, Column;
	for (Column = start; Column <= end; Column++)
	{
		OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //Đặt con trỏ nửa trên
		for (i = 0; i < 8; i++)
		{
			OLED_WriteData(0x00); //Hiển thị nội dung nửa trên
		}
		OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //Đặt con trỏ nửa dưới
		for (i = 0; i < 8; i++)
		{
			OLED_WriteData(0x00); //Hiển thị nội dung nửa dưới
		}
	}
}

/**
 * @brief  OLED hiển thị một ký tự
 * @param  Line vị trí hàng, phạm vi: 1~4
 * @param  Column vị trí cột, phạm vi: 1~16
 * @param  Char ký tự cần hiển thị, phạm vi: ký tự ASCII có thể nhìn thấy
 * @retval Không
 */
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
	uint8_t i;
	OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //Đặt con trỏ nửa trên
	for (i = 0; i < 8; i++)
	{
		OLED_WriteData(OLED_F8x16[Char - ' '][i]); //Hiển thị nội dung nửa trên
	}
	OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //Đặt con trỏ nửa dưới
	for (i = 0; i < 8; i++)
	{
		OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //Hiển thị nội dung nửa dưới
	}
}

/**
 * @brief  OLED hiển thị chuỗi
 * @param  Line vị trí hàng bắt đầu, phạm vi: 1~4
 * @param  Column vị trí cột bắt đầu, phạm vi: 1~16
 * @param  String chuỗi cần hiển thị, phạm vi: ký tự ASCII có thể nhìn thấy
 * @retval Không
 */
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i++)
	{
		OLED_ShowChar(Line, Column + i, String[i]);
	}
}

/**
 * @brief  OLED hàm lũy thừa
 * @retval Trả về X mũ Y
 */
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}

/**
 * @brief  OLED hiển thị số (thập phân, số dương)
 * @param  Line vị trí hàng bắt đầu, phạm vi: 1~4
 * @param  Column vị trí cột bắt đầu, phạm vi: 1~16
 * @param  Number số cần hiển thị, phạm vi: 0~4294967295
 * @param  Length độ dài số cần hiển thị, phạm vi: 1~10
 * @retval Không
 */
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i++)
	{
		OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
	}
}

/**
 * @brief  OLED hiển thị số (thập phân, có dấu)
 * @param  Line vị trí hàng bắt đầu, phạm vi: 1~4
 * @param  Column vị trí cột bắt đầu, phạm vi: 1~16
 * @param  Number số cần hiển thị, phạm vi: -2147483648~2147483647
 * @param  Length độ dài số cần hiển thị, phạm vi: 1~10
 * @retval Không
 */
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
	uint8_t i;
	uint32_t Number1;
	if (Number >= 0)
	{
		OLED_ShowChar(Line, Column, '+');
		Number1 = Number;
	}
	else
	{
		OLED_ShowChar(Line, Column, '-');
		Number1 = -Number;
	}
	for (i = 0; i < Length; i++)
	{
		OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
	}
}

/**
 * @brief  OLED hiển thị số (thập lục phân, số dương)
 * @param  Line vị trí hàng bắt đầu, phạm vi: 1~4
 * @param  Column vị trí cột bắt đầu, phạm vi: 1~16
 * @param  Number số cần hiển thị, phạm vi: 0~0xFFFFFFFF
 * @param  Length độ dài số cần hiển thị, phạm vi: 1~8
 * @retval Không
 */
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
	uint8_t i, SingleNumber;
	for (i = 0; i < Length; i++)
	{
		SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
		if (SingleNumber < 10)
		{
			OLED_ShowChar(Line, Column + i, SingleNumber + '0');
		}
		else
		{
			OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
		}
	}
}

/**
 * @brief  OLED hiển thị số (nhị phân, số dương)
 * @param  Line vị trí hàng bắt đầu, phạm vi: 1~4
 * @param  Column vị trí cột bắt đầu, phạm vi: 1~16
 * @param  Number số cần hiển thị, phạm vi: 0~1111 1111 1111 1111
 * @param  Length độ dài số cần hiển thị, phạm vi: 1~16
 * @retval Không
 */
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i++)
	{
		OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
	}
}

/**
 * @brief  OLED khởi tạo
 * @param  Không
 * @retval Không
 */
void OLED_Init(void)
{
	uint32_t i, j;

	for (i = 0; i < 1000; i++) //Trì hoãn sau khi cấp nguồn
	{
		for (j = 0; j < 1000; j++)
			;
	}

	OLED_I2C_Init(); //Khởi tạo cổng

	OLED_WriteCommand(0xAE); //Tắt hiển thị

	OLED_WriteCommand(0xD5); //Đặt tỷ số chia clock / tần số dao động
	OLED_WriteCommand(0x80);

	OLED_WriteCommand(0xA8); //Đặt tỷ lệ đa hợp
	OLED_WriteCommand(0x3F);

	OLED_WriteCommand(0xD3); //Đặt độ lệch hiển thị
	OLED_WriteCommand(0x00);

	OLED_WriteCommand(0x40); //Đặt hàng bắt đầu hiển thị

	OLED_WriteCommand(0xA1); //Đặt chiều trái-phải, 0xA1 bình thường 0xA0 đảo trái-phải

	OLED_WriteCommand(0xC8); //Đặt chiều trên-dưới, 0xC8 bình thường 0xC0 đảo trên-dưới

	OLED_WriteCommand(0xDA); //Đặt cấu hình phần cứng chân COM
	OLED_WriteCommand(0x12);

	OLED_WriteCommand(0x81); //Đặt điều khiển độ tương phản
	OLED_WriteCommand(0xCF);

	OLED_WriteCommand(0xD9); //Đặt chu kỳ sạc trước
	OLED_WriteCommand(0xF1);

	OLED_WriteCommand(0xDB); //Đặt mức hủy chọn VCOMH
	OLED_WriteCommand(0x30);

	OLED_WriteCommand(0xA4); //Đặt bật/tắt toàn bộ hiển thị

	OLED_WriteCommand(0xA6); //Đặt hiển thị bình thường/đảo

	OLED_WriteCommand(0x8D); //Đặt bơm sạc
	OLED_WriteCommand(0x14);

	OLED_WriteCommand(0xAF); //Bật hiển thị

	OLED_Clear(); //Xóa màn hình OLED
}

Sơ đồ thời gian

Sơ đồ thời gian thực tế khi AM2320 gửi lệnh và đọc dữ liệu.

Đọc thêm

1 Lượt thích