STM32 마이크로컨트롤러가 소프트웨어 I2C로 AM2320 온습도 센서에서 데이터를 읽어 0.96인치 OLED 화면에 표시하기.
제가 사용한 MCU는 STM32F103C8T6이며, 코드는 ST 표준 라이브러리로 작성했습니다.
STM32 하드웨어 I2C로 SHTC3 온습도 센서 읽기: https://blog.zeruns.com/archives/692.html
STM32 MCU로 AHT10 온습도 센서 데이터 읽기: https://blog.zeruns.com/archives/693.html
전자/MCU 기술 단톡: 2169025065
구현 사진

I2C 프로토콜 개요
I2C(Inter-Integrated Circuit) 통신 프로토콜은 Philips사가 개발했으며, 핀 수가 적고 하드웨어 구현이 간단하며 확장성이 뛰어나 USART, CAN 등의 외부 트랜시버 없이(레벨 변환 칩 없이) 시스템 내 여러 IC 간 통신에 널리 쓰입니다.
I2C는 SDA(Serial Data Line)라는 한 개의 데이터 버스만 사용하며, 1비트씩 전송하는 반이중(serial) 통신입니다.
반이중 통신: 양방향은 가능하나 동시 양방향은 안 되고, 번갈아 전송해야 하므로 한 순간에는 한 방향만 가능하며, 데이터선 1개면 충분합니다.
I2C는 물리 계층과 프로토콜 계층으로 나눕니다.
물리 계층은 기계·전기적 특성(하드웨어)을 규정해 물리 매체에서 원시 데이터가 전송되도록 하고,
프로토콜 계층은 통신 로직을 규정해 송수신 측이 데이터를 동일한 방식으로 패킹/언패킹하도록 합니다(소프트웨어).
I2C 물리 계층
I2C 통신 장치 간 일반 연결 방식
- 다중 장치 버스 지원. “버스”는 여러 장치가 공유하는 신호선을 뜻하며, 하나의 I2C 버스에 여러 마스터·슬레이브를 연결할 수 있습니다.
- I2C 버스는 SDA(양방향 데이터)와 SCL(클럭) 2개 선만 사용합니다. 데이터선은 데이터를 나타내고, 클럭선은 동기화에 쓰입니다.
- 버스는 풀업 저항으로 전원에 연결됩니다. I2C 장치가 유휴 상태일 때 하이 임피던스를 출력하며, 모든 장치가 유휴이면 풀업 저항이 버스를 하이 레벨로 당깁니다.
I2C 통신 시 MCU GPIO는 오픈드레인 출력으로 설정해야 합니다. 그렇지 않으면 쇼트가 날 수 있습니다.
STM32 I2C에 대한 더 자세한 정보는 다음 글 참고: https://url.zeruns.com/JC0Ah
여기서는 자세히 다루지 않겠습니다.
AM2320 온습도 센서
소개
AM2320 디지털 온습도 센서는 교정된 디지털 신호를 출력하는 복합 센서로, 전용 측정 기술을 채택해 높은 신뢰성과 장기 안정성을 제공합니다. 센서는 커패시티브 습도 소자와 고정밀 온도 소자를 포함해 고성능 마이크로프로세서와 연결되어 있으며, 응답 속도가 빠르고 간섭에 강하고 가성비가 뛰어납니다. 통신 방식은 싱글 버스와 표준 I2C를 지원하며, I2C는 표준 타이밍을 사용해 별도 배선 없이 바로 버스에 연결할 수 있습니다. 두 방식 모두 온도 보정된 습도·온도 및 CRC 검증값을 직접 출력하므로 별도 계산이나 온도 보정이 필요 없습니다. 통신 방식은 자유롭게 전환 가능하며 4핀 구조로 연결이 간편합니다.
AM2320 데이터시트 다운로드: https://url.zeruns.com/74o6F
데이터시트 요약:
- 온도 범위: -40 ℃ ~ 80 ℃
- 온도 오차: ±0.5 ℃
- 습도 범위: 0 % ~ 99.9 %
- 습도 오차: ±3 %
- 동작 전압: 3.1 V ~ 5.5 V
- 통신 방식: I2C 또는 싱글 버스
- 클럭 주파수: 100 kHz 이하
핵심 정보 추출
장치 주소 및 읽기/쓰기 명령
실제 사용 시 AM2320의 장치 주소(7비트)에 읽기/쓰기 비트를 붙여 1바이트로 전송합니다.
쓰기: 시작 신호 이후 0xB8(1011 1000) 전송
읽기: 시작 신호 이후 0xB9(1011 1001) 전송
즉, 0xB8은 쓰기, 0xB9는 읽기를 의미합니다.
온습도 데이터 읽기
데이터시트에 따르면 읽기 주기는 3단계:
- 센서 깨우기
- 읽기 명령 전송
- 데이터 수신
요약:
- 깨우기: START + 0xB8 전송 → 800 µs 이상 대기 → STOP
- 읽기 명령: START + 0xB8(SLA) + 0x03(기능码) + 0x00(시작 주소) + 0x04(레지스터 길이) + STOP
- 데이터 수신: 0xB9로 읽기 시작 → 8바이트 연속 수신
수신 순서: 길이 + 습도高 + 습도低 + 온도高 + 온도低 + CRC低 + CRC高 - 데이터 변환 처리
데이터 계산
데이터시트에 따르면
예: 습도 값 0x01F4 → 10진수 500 → 습도 = ```c
#include “stm32f10x.h” // Device header
#include “Delay.h”
#include “OLED.h”
#include “AM2320.h”
#include “IWDG.h”
int main(void)
{
IWDG_Configuration(); // 초기화 독바
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; // 변수 선언하여 온습도 데이터 저장
if (ReadAM2320(&Hum, &Temp)) // 온습도 데이터 읽기
{
if (Temp >= 0)
{
char String[10];
sprintf(String, "+%.2fC", Temp); // 형식화된 문자열을 문자열 변수에 출력
OLED_ShowString(1, 3, String); // 온도 표시
sprintf(String, " %.2f%%", Hum); // 형식화된 문자열을 문자열 변수에 출력
OLED_ShowString(2, 3, String); // 습도 표시
}
else
{
char String[10];
sprintf(String, "-%.2fC", Temp); // 형식화된 문자열을 문자열 변수에 출력
OLED_ShowString(1, 3, String); // 온도 표시
sprintf(String, " %.2f%%", Hum); // 형식화된 문자열을 문자열 변수에 출력
OLED_ShowString(2, 3, String); // 습도 표시
}
}
else
{
err_count++;
OLED_ShowNum(3, 1, err_count, 5); // 오류 횟수 카운트 표시
}
Delay_ms(100);
i++;
if (i >= 99999)
i = 0;
if (err_count >= 99999)
err_count = 0;
IWDG_FeedDog(); // 독바 먹이기 (1초 이상 먹이지 않으면 자동 재설정)
}
// blog.zeruns.com
}
### AM2320.c
```c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
/*
작가 블로그:https://blog.zeruns.com
WeChat 공식 계정:zeruns-gzh
Bilibili 홈페이지:https://space.bilibili.com/8320520
*/
/*AM2320 주소*/
#define AM2320_ADDRESS 0xB8
/*핀 설정*/
#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)
/*STM32의 GPIO를 오픈 드레인 출력 모드로 설정하면
GPIO 입력 데이터 레지스터를 읽어 외부 핀 입력 레벨을 얻을 수 있으므로, 플로팅 입력 모드 기능도 동시에 가짐*/
/**
* @brief CRC 체크섬 계산
* @param *ptr 계산할 바이트 데이터 (배열 변수 형태로 저장)
* @param len 계산할 바이트 개수 (배열 길이)
* @retval 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 I2C 시작
* @param 없음
* @retval 없음
*/
void AM2320_I2C_Start(void)
{
AM2320_W_SDA(1);
Delay_us(2); // 2마이크로초 지연
AM2320_W_SCL(1);
Delay_us(4);
AM2320_W_SDA(0);
Delay_us(3);
AM2320_W_SCL(0);
Delay_us(5);
}
/**
* @brief I2C 정지
* @param 없음
* @retval 없음
*/
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 1바이트 전송
* @param Byte 전송할 1바이트
* @retval 없음
*/
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); // SDA 버스 해제
}
/**
* @brief 응답 신호 대기
* @param 없음
* @retval 1-비응답 신호, 0-응답 신호
*/
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 1바이트 읽기
* @param NACK 1-비응답 신호, 0-응답 신호
* @retval 읽은 바이트 데이터
*/
uint8_t AM2320_I2C_ReadByte(uint8_t NACK)
{
uint8_t i, Byte = 0;
AM2320_W_SDA(1); // 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); // 응답/비응답 신호 전송
AM2320_W_SCL(1);
Delay_us(4);
AM2320_W_SCL(0);
Delay_us(5);
AM2320_W_SDA(1); // SDA 버스 해제
return Byte;
}
/*센서 깨우기*/
void AM2320_Wake(void)
{
AM2320_I2C_Start();
AM2320_I2C_SendByte(AM2320_ADDRESS);
WaitAck();
Delay_us(1000); // 1000마이크로초 지연
AM2320_I2C_Stop();
}
/*핀 초기화*/
void AM2320_I2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // GPIOB 클럭 활성화
GPIO_InitTypeDef GPIO_InitStructure; // GPIO 구성용 구조체 정의
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 오픈 드레인 출력 모드
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = AM2320_SCL;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = AM2320_SDA;
GPIO_Init(GPIOB, &GPIO_InitStructure);
AM2320_W_SCL(1);
AM2320_W_SDA(1);
AM2320_Wake(); // 센서 깨우기
}
/**
* @brief AM2320 데이터 읽기
* @param *Hum 습도
* @param *Temp 온도
* @retval 1 - 읽기 성공;0 - 읽기 실패
*/
uint8_t ReadAM2320(float *Hum, float *Temp)
{
uint8_t Data[8];
AM2320_I2C_Start(); // 시작 신호 전송
AM2320_I2C_SendByte(AM2320_ADDRESS);
if (WaitAck()) // 응답 신호 판단
{
AM2320_I2C_Stop(); // 정지 신호 전송
Delay_us(50);
// 다시 한 번 읽기 시도
AM2320_I2C_Start(); // 시작 신호 전송
AM2320_I2C_SendByte(AM2320_ADDRESS);
if (WaitAck()) // 응답 신호 판단
{
Delay_us(20);
AM2320_I2C_Stop(); // 정지 신호 전송
return 0;
}
else
{
Delay_us(20);//여기서 AM2320이 알 수 없는 이유로 SCL을 한동안 낮추어 데이터 전송 오류를 일으키므로, 20마이크로초 지연 후 AM2320이 SCL을 해제할 때까지 기다림
AM2320_I2C_SendByte(0x03); // 기능 코드 전송
WaitAck(); // 응답 신호 대기
AM2320_I2C_SendByte(0x00); // 읽을 레지스터 시작 주소 전송
WaitAck(); // 응답 신호 대기
AM2320_I2C_SendByte(0x04); // 읽을 레지스터 길이 전송
WaitAck(); // 응답 신호 대기
Delay_us(20);//여기서 AM2320이 알 수 없는 이유로 SCL을 한동안 낮추어 정지 신호 전송 실패를 일으키므로, 20마이크로초 지연 후 AM2320이 SCL을 해제할 때까지 기다림
AM2320_I2C_Stop(); // 정지 신호 전송
}
}
else
{
Delay_us(20);//여기서 AM2320이 알 수 없는 이유로 SCL을 한동안 낮추어 데이터 전송 오류를 일으키므로, 20마이크로초 지연 후 AM2320이 SCL을 해제할 때까지 기다림
AM2320_I2C_SendByte(0x03); // 기능 코드 전송
WaitAck(); // 응답 신호 대기
AM2320_I2C_SendByte(0x00); // 읽을 레지스터 시작 주소 전송
WaitAck(); // 응답 신호 대기
AM2320_I2C_SendByte(0x04); // 읽을 레지스터 길이 전송
WaitAck(); // 응답 신호 대기
Delay_us(20);//여기서 AM2320이 알 수 없는 이유로 SCL을 한동안 낮추어 정지 신호 전송 실패를 일으키므로, 20마이크로초 지연 후 AM2320이 SCL을 해제할 때까지 기다림
AM2320_I2C_Stop(); // 정지 신호 전송
}
Delay_ms(2); // 2밀리초 지연
AM2320_I2C_Start();
AM2320_I2C_SendByte(AM2320_ADDRESS | 0x01); // 읽기 명령 전송
WaitAck();
Delay_us(35);
uint8_t i;
for (i = 0; i < 8; i++)
{
if (i != 7)
{
Data[i] = AM2320_I2C_ReadByte(0);
}
else
{
Data[i] = AM2320_I2C_ReadByte(1); // 마지막 바이트 읽을 때 비응답 신호 전송
}
}
AM2320_I2C_Stop();
if (CRC16(Data, 6) == (Data[6] | (Data[7] << 8))) // 데이터 검증
{
*Hum = ((((uint16_t)Data[2]) << 8) | Data[3]) / 10.0; // 습도 데이터 계산
if (Data[4] >> 7) // 온도 값이 음수인지 판단
{
*Temp = ((((uint16_t)(Data[4] && 0x7F) << 8)) | Data[5]) / -10.0; // 음수 온도 계산
}
else
{
*Temp = ((((uint16_t)Data[4]) << 8) | Data[5]) / 10.0; // 양수 온도 계산
}
return 1;
}
return 0;
}
```### OLED.c
```c
#include "stm32f10x.h"
#include "OLED_Font.h"
/*핀 설정*/
#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))
/*핀 초기화*/
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 시작
* @param 없음
* @retval 없음
*/
void OLED_I2C_Start(void)
{
OLED_W_SDA(1);
OLED_W_SCL(1);
OLED_W_SDA(0);
OLED_W_SCL(0);
}
/**
* @brief I2C 정지
* @param 없음
* @retval 없음
*/
void OLED_I2C_Stop(void)
{
OLED_W_SDA(0);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
/**
* @brief I2C 1바이트 전송
* @param Byte 전송할 1바이트
* @retval 없음
*/
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); //SDA 버스 해제
OLED_W_SCL(1); //추가 클럭, ACK 신호 무시
OLED_W_SCL(0);
}
/**
* @brief OLED 명령어 쓰기
* @param Command 쓸 명령어
* @retval 없음
*/
void OLED_WriteCommand(uint8_t Command)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //슬레이브 주소
OLED_I2C_SendByte(0x00); //명령어 쓰기
OLED_I2C_SendByte(Command);
OLED_I2C_Stop();
}
/**
* @brief OLED 데이터 쓰기
* @param Data 쓸 데이터
* @retval 없음
*/
void OLED_WriteData(uint8_t Data)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //슬레이브 주소
OLED_I2C_SendByte(0x40); //데이터 쓰기
OLED_I2C_SendByte(Data);
OLED_I2C_Stop();
}
/**
* @brief OLED 커서 위치 설정
* @param Y 좌측 상단을 원점으로 아래 방향 좌표, 범위: 0~7
* @param X 좌측 상단을 원점으로 오른쪽 방향 좌표, 범위: 0~127
* @retval 없음
*/
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
OLED_WriteCommand(0xB0 | Y); //Y 위치 설정
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //X 위치 하위 4비트 설정
OLED_WriteCommand(0x00 | (X & 0x0F)); //X 위치 상위 4비트 설정
}
/**
* @brief OLED 화면 지우기
* @param 없음
* @retval 없음
*/
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 부분 지우기
* @param Line 행 위치, 범위: 1~4
* @param start 열 시작 위치, 범위: 1~16
* @param end 열 끝 위치, 범위: 1~16
* @retval 없음
*/
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); //커서 위치를 상반부로 설정
for (i = 0; i < 8; i++)
{
OLED_WriteData(0x00); //상반부 내용 표시
}
OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //커서 위치를 하반부로 설정
for (i = 0; i < 8; i++)
{
OLED_WriteData(0x00); //하반부 내용 표시
}
}
}
/**
* @brief OLED 한 문자 표시
* @param Line 행 위치, 범위: 1~4
* @param Column 열 위치, 범위: 1~16
* @param Char 표시할 한 문자, 범위: ASCII 가시 문자
* @retval 없음
*/
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
uint8_t i;
OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //커서 위치를 상반부로 설정
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i]); //상반부 내용 표시
}
OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //커서 위치를 하반부로 설정
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //하반부 내용 표시
}
}
/**
* @brief OLED 문자열 표시
* @param Line 시작 행 위치, 범위: 1~4
* @param Column 시작 열 위치, 범위: 1~16
* @param String 표시할 문자열, 범위: ASCII 가시 문자
* @retval 없음
*/
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 거듭제곱 함수
* @retval X의 Y제곱 반환
*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
/**
* @brief OLED 숫자 표시(십진수, 양수)
* @param Line 시작 행 위치, 범위: 1~4
* @param Column 시작 열 위치, 범위: 1~16
* @param Number 표시할 숫자, 범위: 0~4294967295
* @param Length 표시할 숫자 길이, 범위: 1~10
* @retval 없음
*/
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 숫자 표시(십진수, 부호 있는 수)
* @param Line 시작 행 위치, 범위: 1~4
* @param Column 시작 열 위치, 범위: 1~16
* @param Number 표시할 숫자, 범위: -2147483648~2147483647
* @param Length 표시할 숫자











