1. I2C 통신 프로토콜 개요 및 설계 목적
I2C(Inter-Integrated Circuit)는 동기식, 반이중, 멀티 슬레이브 구조를 가지며 ACK/NACK 응답 메커니즘을 지원하는 저속 직렬 통신 프로토콜이다. 주로 마이크로컨트롤러와 센서, EEPROM, RTC 등의 주변장치 간 연결에 사용된다.
이 프로토콜은 하드웨어 배선을 단순화하고, 여러 장치를 동일한 버스에서 제어할 수 있도록 설계되었으며, 낮은 전력 소모와 함께 소프트웨어 기반 구현도 가능하다.
2. 대표적인 I2C 장치 예시
- MPU6050 (6축 자이로/가속도 센서)
- OLED 디스플레이 모듈
- AT24C02 EEPROM
- DS3231 실시간 시계(RTC) 모듈
3. 물리적 인터페이스
I2C는 두 개의 신호선으로 구성된다:
- SCL: 클록 라인
- SDA: 데이터 라인
4. 하드웨어 동작 규칙
- 슬레이브는 SCL 라인을 직접 제어할 수 없음
- 슬레이브는 마스터가 읽기 명령을 보낸 후 또는 ACK 단계에서만 SDA를 제어 가능
- 장치 주소는 일반적으로 7비트 또는 10비트 형식
5. 기본 타이밍 유닛
시작 조건(Start Condition)
SCL이 HIGH일 때 SDA가 HIGH → LOW로 전환
정지 조건(Stop Condition)
SCL이 HIGH일 때 SDA가 LOW → HIGH로 전환
바이트 전송
- SCL을 LOW로 설정
- SDA에 비트 출력
- SCL을 HIGH로 설정 (데이터 샘플링)
- SCL을 다시 LOW로 설정
바이트 수신
- SCL을 LOW로 설정
- 슬레이브가 SDA에 비트 출력
- SCL을 HIGH로 설정 (마스터가 데이터 읽음)
- SCL을 다시 LOW로 설정
ACK/NACK 전송
수신 측이 다음 바이트 수신 의사를 표시하기 위해 SDA를 LOW(ACK) 또는 HIGH(NACK)로 설정
ACK/NACK 수신
전송 후 SDA를 해제하고 SCL을 HIGH로 올려 ACK/NACK 상태를 판독
6. 일반적인 I2C 통신 시퀀스
지정 주소 쓰기
START → [슬레이브 주소 + W] → ACK → 레지스터 주소 → ACK → 데이터 → STOP
현재 주소 읽기
START → [슬레이브 주소 + R] → ACK → 데이터 수신 → ACK/NACK → STOP
지정 주소 읽기
START → [슬레이브 주소 + W] → ACK → 레지스터 주소 → ACK → RESTART → [슬레이브 주소 + R] → ACK → 데이터 수신 → NACK → STOP
7. MPU6050 장치 주소 확인 예제
MPU6050의 I2C 주소는 AD0 핀 상태에 따라 0xD0(AD0=GND) 또는 0xD1(AD0=VCC)가 된다.
#include "stm32f10x.h"
#include "Delay.h"
// SCL 핀(PB10), SDA 핀(PB11) 제어 함수
void MyI2C_SetSCL(uint8_t val) {
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)val);
Delay_us(10);
}
void MyI2C_SetSDA(uint8_t val) {
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)val);
Delay_us(10);
}
uint8_t MyI2C_ReadSDA(void) {
return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
}
void MyI2C_Start(void) {
MyI2C_SetSDA(1);
MyI2C_SetSCL(1);
MyI2C_SetSDA(0);
MyI2C_SetSCL(0);
}
void MyI2C_Stop(void) {
MyI2C_SetSDA(0);
MyI2C_SetSCL(1);
MyI2C_SetSDA(1);
}
void MyI2C_SendByte(uint8_t data) {
for (int i = 0; i < 8; i++) {
MyI2C_SetSDA(data & (0x80 >> i));
MyI2C_SetSCL(1);
MyI2C_SetSCL(0);
}
}
uint8_t MyI2C_RecvByte(void) {
uint8_t byte = 0;
MyI2C_SetSDA(1); // 입력 모드 준비
for (int i = 0; i < 8; i++) {
MyI2C_SetSCL(1);
if (MyI2C_ReadSDA()) byte |= (0x80 >> i);
MyI2C_SetSCL(0);
}
return byte;
}
void MyI2C_SendAck(uint8_t ack) {
MyI2C_SetSDA(ack);
MyI2C_SetSCL(1);
MyI2C_SetSCL(0);
}
uint8_t MyI2C_RecvAck(void) {
MyI2C_SetSDA(1);
MyI2C_SetSCL(1);
uint8_t ack = MyI2C_ReadSDA();
MyI2C_SetSCL(0);
return ack;
}
void MyI2C_Init(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef gpio;
gpio.GPIO_Mode = GPIO_Mode_Out_OD;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
gpio.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_Init(GPIOB, &gpio);
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); // 초기 HIGH 상태
}
주소 확인 메인 코드
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "MyI2C.h"
int main(void) {
OLED_Init();
MyI2C_Init();
MyI2C_Start();
MyI2C_SendByte(0xD0); // AD0=GND 가정
uint8_t ack = MyI2C_RecvAck();
MyI2C_Stop();
OLED_ShowNum(1, 1, ack, 1); // ACK=0이면 정상 응답
while (1);
}
8. MPU6050 레지스터 읽기/쓰기 구현
#include "MyI2C.h"
#define MPU_ADDR 0xD0
void MPU6050_WriteReg(uint8_t reg, uint8_t val) {
MyI2C_Start();
MyI2C_SendByte(MPU_ADDR);
MyI2C_RecvAck();
MyI2C_SendByte(reg);
MyI2C_RecvAck();
MyI2C_SendByte(val);
MyI2C_RecvAck();
MyI2C_Stop();
}
uint8_t MPU6050_ReadReg(uint8_t reg) {
MyI2C_Start();
MyI2C_SendByte(MPU_ADDR);
MyI2C_RecvAck();
MyI2C_SendByte(reg);
MyI2C_RecvAck();
MyI2C_Start(); // Repeated Start
MyI2C_SendByte(MPU_ADDR | 0x01); // Read 모드
MyI2C_RecvAck();
uint8_t data = MyI2C_RecvByte();
MyI2C_SendAck(1); // NACK 전송
MyI2C_Stop();
return data;
}
void MPU6050_Init(void) {
MyI2C_Init();
}
레지스터 접근 테스트
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
int main(void) {
OLED_Init();
MPU6050_Init();
// 파워 매니지먼트 레지스터 설정
MPU6050_WriteReg(0x6B, 0x00); // 클록 소스 활성화
uint8_t val = MPU6050_ReadReg(0x6B);
OLED_ShowHexNum(1, 1, val, 2);
Delay_s(2);
// 칩 ID 확인 (고정값 0x68)
val = MPU6050_ReadReg(0x75);
OLED_ShowHexNum(1, 1, val, 2);
while (1);
}