I2C 통신 프로토콜의 소프트웨어 구현

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로 전환

바이트 전송

  1. SCL을 LOW로 설정
  2. SDA에 비트 출력
  3. SCL을 HIGH로 설정 (데이터 샘플링)
  4. SCL을 다시 LOW로 설정

바이트 수신

  1. SCL을 LOW로 설정
  2. 슬레이브가 SDA에 비트 출력
  3. SCL을 HIGH로 설정 (마스터가 데이터 읽음)
  4. 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);
}

태그: I2C STM32 MPU6050 Embedded C GPIO

10월 9일 07:35에 게시됨