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
- Là bus đa chủ / đa từ: nhiều master và nhiều slave cùng chia sẻ 2 tuyến tín hiệu.
- Chỉ cần 2 dây: SDA (data) và SCL (clock).
- 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.
- 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:
- Đánh thức cảm biến
- Gửi lệnh đọc
- Đọc dữ liệu trả về
Chi tiết:
- Đánh thức: START → gửi
0xB8→ chờ > 800 µs → STOP - 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 - Đọ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
- Giới thiệu VPS/đám mây giá rẻ, chất lượng cao: https://blog.vpszj.cn/archives/41.html
- Dùng NPS dựng máy chủ xuyên mạng nội bộ, có Web panel: https://blog.vpszj.cn/archives/748.html
- Hướng dẫn dựng website trên Linux, hướng dẫn làm web: https://blog.vpszj.cn/archives/1094.html
- Hướng dẫn dựng máy chủ Minecraft: https://blog.zeruns.com/tag/mc/
- Đo khoảng cách bằng siêu âm với STM32 và module HC-SR04: https://blog.zeruns.com/archives/680.html
- Dựng môi trường phát triển ESP8266 và demo dự án: https://blog.zeruns.com/archives/526.html











