AVR TWI와 I2C 통신: 레지스터부터 Arduino Wire 활용까지

AVR의 TWI 인터페이스와 I2C 호환성, 레지스터 구성, 마스터·슬레이브 통신, Arduino Wire 사용법을 정리합니다.

2026-08-14 · 최초 발행 2026-01-16

TWI(Two Wire Interface)는 Atmel(현 Microchip)이 AVR 마이크로컨트롤러에 넣은 2선식 직렬 통신 인터페이스다. Philips의 I2C 프로토콜과 전기적·기능적으로 완전히 호환되며, AVR 기반 시스템과 Arduino에서 센서, 메모리, 디스플레이 같은 주변 장치를 연결할 때 사용된다.

AVR에서 I2C를 TWI라고 부르는 이유

TWI와 I2C는 본질적으로 같은 프로토콜이다. Philips(현 NXP)가 I2C 상표를 보유하고 있었기 때문에 Atmel은 자사 MCU 문서에서 라이선스 비용을 피하고자 TWI라는 이름을 사용했다. 명칭은 달라도 TWI 장치는 I2C 버스에 연결할 수 있다.

  • Atmel AVR: TWI (Two Wire Interface)
  • NXP/Philips: I2C (Inter-Integrated Circuit)
  • Texas Instruments: I2C
  • STMicroelectronics: I2C
  • Microchip PIC: MSSP (Master Synchronous Serial Port)
명칭은 다르지만 동일한 프로토콜완전 호환부분 호환I2C(NXP 원조)TWI(Atmel/Microchip)SMBus(Intel)

TWI는 SDA 데이터선과 SCL 클럭선으로 통신하며, 마스터·슬레이브 모드와 멀티 마스터를 지원한다. 주소는 7비트와 10비트를 사용할 수 있고 Fast-mode에서 최대 400kbps로 동작한다. AVR TWI에는 하드웨어 주소 인식, 범용 호출(General Call), 인터럽트 기반 처리, 자동 클럭 스트레칭, 노이즈 필터가 포함된다.

특성 TWI 사양
신호선 SDA, SCL
전압 1.8V ~ 5.5V
속도 100kbps, 400kbps
주소 7비트 (10비트 소프트웨어)
버퍼 1바이트 데이터 레지스터

레지스터와 버스 인터페이스가 연결되는 방식

AVR의 TWI 모듈은 비트레이트, 제어, 상태, 데이터, 주소 레지스터를 중심으로 CPU와 버스 사이의 통신을 처리한다.

AVR MCUTWI 모듈TWBR(Bit Rate Register)TWCR(Control Register)TWSR(Status Register)TWDR(Data Register)TWAR(Address Register)TWAMR(Address Mask)버스 인터페이스 유닛주소 매칭 유닛클럭 제어 유닛CPU인터럽트 컨트롤러SDASCL

TWBR(TWI Bit Rate Register)는 비트 전송 속도를 정한다.

SCL 주파수 = F_CPU / (16 + 2 * TWBR * 4^TWPS)

예: 16MHz CPU, 100kHz SCL
TWBR = (16000000/100000 - 16) / 2 = 72

TWCR(TWI Control Register)은 START·STOP 생성, ACK 응답, 인터럽트와 TWI 활성화를 제어한다.

비트 명칭 기능
7 TWINT 인터럽트 플래그
6 TWEA ACK 활성화
5 TWSTA START 조건 생성
4 TWSTO STOP 조건 생성
3 TWWC 쓰기 충돌 플래그
2 TWEN TWI 활성화
1 - 예약
0 TWIE 인터럽트 활성화

TWSR(TWI Status Register)은 현재 상태 코드와 프리스케일러를 보관한다.

비트 명칭 기능
7-3 TWS7:3 상태 코드
2 - 예약
1-0 TWPS1:0 프리스케일러

TWDR(TWI Data Register)은 송수신 데이터 버퍼다. 쓸 때는 전송할 데이터를 넣고, 읽을 때는 수신된 데이터를 가져온다.

TWAR(TWI Address Register)은 슬레이브 주소와 범용 호출 응답 여부를 설정한다.

비트 명칭 기능
7-1 TWA6:0 슬레이브 주소
0 TWGCE 범용 호출 응답

상태 코드로 추적하는 마스터 통신

마스터 송신에서는 START 조건 뒤에 SLA+W와 데이터를 보내고, 각 단계의 ACK·NACK 및 상태 코드를 확인한다.

슬레이브TWI 모듈AVR CPU슬레이브TWI 모듈AVR CPUSTART 설정 (TWSTA=1)START 조건인터럽트 (상태=0x08)주소+W 로드SLA+WACK인터럽트 (상태=0x18)데이터 로드데이터ACK인터럽트 (상태=0x28)STOP 설정 (TWSTO=1)STOP 조건
코드 설명
0x08 START 조건 전송됨
0x10 REPEATED START 전송됨
0x18 SLA+W 전송, ACK 수신
0x20 SLA+W 전송, NACK 수신
0x28 데이터 전송, ACK 수신
0x30 데이터 전송, NACK 수신
0x38 중재 패배

마스터 수신은 SLA+R 전송 후 수신을 계속할 데이터에 ACK를, 마지막 데이터에 NACK를 응답하는 흐름이다.

슬레이브TWI 모듈AVR CPU슬레이브TWI 모듈AVR CPUSTART 설정START 조건인터럽트 (상태=0x08)주소+R 로드SLA+RACK인터럽트 (상태=0x40)TWEA=1 (ACK 응답)데이터인터럽트 (상태=0x50)TWEA=0 (NACK 응답)데이터인터럽트 (상태=0x58)STOP 설정STOP 조건
코드 설명
0x40 SLA+R 전송, ACK 수신
0x48 SLA+R 전송, NACK 수신
0x50 데이터 수신, ACK 반환
0x58 데이터 수신, NACK 반환

슬레이브 수신(SR)에서는 자신의 SLA+W 또는 범용 호출을 받고 데이터 수신 상태를 처리한다.

코드 설명
0x60 자신의 SLA+W 수신, ACK 반환
0x68 중재 패배, SLA+W 수신
0x70 범용 호출, ACK 반환
0x80 데이터 수신 (SLA+W 후), ACK 반환
0x88 데이터 수신, NACK 반환
0xA0 STOP 또는 REPEATED START 수신

슬레이브 송신(ST)에서는 자신의 SLA+R에 응답하고, 마스터의 ACK·NACK에 맞춰 다음 데이터를 준비한다.

코드 설명
0xA8 자신의 SLA+R 수신, ACK 반환
0xB0 중재 패배, SLA+R 수신
0xB8 데이터 전송, ACK 수신
0xC0 데이터 전송, NACK 수신
0xC8 마지막 데이터 전송, ACK 수신

직접 제어할 때의 초기화와 송수신 함수

다음 초기화 코드는 16MHz CPU에서 100kHz로 TWI를 활성화한다.

// TWI 초기화 (100kHz, 16MHz CPU)
void TWI_Init(void) {
    // 비트 레이트 설정
    TWBR = 72;  // (16MHz/100kHz - 16) / 2
    TWSR = 0;   // 프리스케일러 = 1

    // TWI 활성화
    TWCR = (1 << TWEN);
}

START·STOP, 바이트 쓰기, ACK·NACK 수신은 아래 함수처럼 제어 레지스터와 상태 레지스터를 통해 다룬다.

// START 조건 전송
uint8_t TWI_Start(void) {
    TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN);
    while (!(TWCR & (1 << TWINT)));  // 완료 대기
    return (TWSR & 0xF8);  // 상태 코드 반환
}

// STOP 조건 전송
void TWI_Stop(void) {
    TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN);
    while (TWCR & (1 << TWSTO));  // 완료 대기
}

// 바이트 전송
uint8_t TWI_Write(uint8_t data) {
    TWDR = data;
    TWCR = (1 << TWINT) | (1 << TWEN);
    while (!(TWCR & (1 << TWINT)));
    return (TWSR & 0xF8);
}

// 바이트 수신 (ACK)
uint8_t TWI_ReadACK(void) {
    TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

// 바이트 수신 (NACK)
uint8_t TWI_ReadNACK(void) {
    TWCR = (1 << TWINT) | (1 << TWEN);
    while (!(TWCR & (1 << TWINT)));
    return TWDR;
}

슬레이브는 주소를 설정하고 인터럽트를 활성화한 뒤, TWI 벡터에서 상태 코드별로 수신·송신을 처리한다.

// 슬레이브 초기화
void TWI_SlaveInit(uint8_t address) {
    // 슬레이브 주소 설정 (7비트를 왼쪽 시프트)
    TWAR = (address << 1);

    // TWI 활성화, ACK 활성화, 인터럽트 활성화
    TWCR = (1 << TWEN) | (1 << TWEA) | (1 << TWIE);
}

// 슬레이브 인터럽트 핸들러
ISR(TWI_vect) {
    uint8_t status = TWSR & 0xF8;

    switch (status) {
        case 0x60:  // SLA+W 수신
            // 수신 준비
            break;

        case 0x80:  // 데이터 수신
            received_data = TWDR;
            break;

        case 0xA8:  // SLA+R 수신
            TWDR = send_data;
            break;

        case 0xB8:  // 데이터 전송, ACK 수신
            TWDR = next_data;
            break;
    }

    // 다음 동작 준비
    TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA) | (1 << TWIE);
}

Arduino에서는 Wire가 TWI를 감싼다

Arduino의 Wire 라이브러리는 AVR TWI 하드웨어를 추상화한 고수준 API다. 애플리케이션은 Wire API를 호출하고, 라이브러리는 TWI 모듈을 통해 I2C 슬레이브와 통신한다.

외부 장치AVR 하드웨어Arduino 라이브러리애플리케이션사용자 코드Wire 라이브러리TWI 모듈I2C 슬레이브

마스터 모드에서는 begin(), beginTransmission(), write(), requestFrom()을 사용해 데이터를 주고받는다.

#include <Wire.h>

void setup() {
    Wire.begin();  // 마스터로 초기화
}

void loop() {
    // 쓰기
    Wire.beginTransmission(0x50);  // EEPROM 주소
    Wire.write(0x00);              // 메모리 주소
    Wire.write(0xAB);              // 데이터
    Wire.endTransmission();

    // 읽기
    Wire.beginTransmission(0x50);
    Wire.write(0x00);  // 읽을 주소
    Wire.endTransmission(false);  // REPEATED START

    Wire.requestFrom(0x50, 1);
    if (Wire.available()) {
        byte data = Wire.read();
    }
}

슬레이브 모드에서는 주소와 수신·요청 콜백을 등록한다.

#include <Wire.h>

volatile byte receivedData;
byte dataToSend = 0x42;

void setup() {
    Wire.begin(0x08);  // 슬레이브 주소 0x08
    Wire.onReceive(receiveEvent);
    Wire.onRequest(requestEvent);
}

void receiveEvent(int numBytes) {
    while (Wire.available()) {
        receivedData = Wire.read();
    }
}

void requestEvent() {
    Wire.write(dataToSend);
}

void loop() {
    // 메인 루프
}

EEPROM·온도 센서·버스 스캔에 적용하기

24LC256 EEPROM은 256Kbit = 32KB 용량이며, 아래 코드는 바이트 단위 읽기와 쓰기를 구현한다.

// 24LC256 EEPROM (256Kbit = 32KB)
#define EEPROM_ADDR 0x50

void EEPROM_WriteByte(uint16_t addr, uint8_t data) {
    Wire.beginTransmission(EEPROM_ADDR);
    Wire.write((addr >> 8) & 0xFF);  // 주소 상위
    Wire.write(addr & 0xFF);          // 주소 하위
    Wire.write(data);
    Wire.endTransmission();
    delay(5);  // 쓰기 완료 대기
}

uint8_t EEPROM_ReadByte(uint16_t addr) {
    Wire.beginTransmission(EEPROM_ADDR);
    Wire.write((addr >> 8) & 0xFF);
    Wire.write(addr & 0xFF);
    Wire.endTransmission(false);

    Wire.requestFrom(EEPROM_ADDR, 1);
    return Wire.read();
}

LM75 온도 센서는 온도 레지스터에서 2바이트를 읽어 값을 구성한다.

// LM75 온도 센서
#define LM75_ADDR 0x48
#define TEMP_REG  0x00

float readTemperature() {
    Wire.beginTransmission(LM75_ADDR);
    Wire.write(TEMP_REG);
    Wire.endTransmission(false);

    Wire.requestFrom(LM75_ADDR, 2);
    int16_t raw = (Wire.read() << 8) | Wire.read();
    raw >>= 5;  // 11비트 데이터

    return raw * 0.125;  // 0.125도 분해능
}

버스에 연결된 장치를 확인할 때는 각 주소로 전송을 시도하는 스캐너를 사용할 수 있다.

void scanI2CBus() {
    Serial.println("I2C 스캐너 시작...");

    for (uint8_t addr = 1; addr < 127; addr++) {
        Wire.beginTransmission(addr);
        uint8_t error = Wire.endTransmission();

        if (error == 0) {
            Serial.print("장치 발견: 0x");
            Serial.println(addr, HEX);
        }
    }

    Serial.println("스캔 완료");
}

통신이 멈췄을 때 확인할 항목

응답이 없거나 데이터가 깨질 때는 주소, 풀업 저항, 속도, 노이즈를 먼저 확인한다.

증상 원인 해결책
응답 없음 주소 오류 주소 확인 (7비트)
응답 없음 풀업 저항 없음 4.7kΩ 풀업 추가
데이터 오류 속도 초과 TWBR 값 증가
간헐적 실패 노이즈 케이블 단축, 차폐

상태 코드를 출력하면 마스터 송신 흐름이 어느 지점에 있는지 확인할 수 있다.

void printTWIStatus() {
    uint8_t status = TWSR & 0xF8;
    Serial.print("TWI 상태: 0x");
    Serial.println(status, HEX);

    switch (status) {
        case 0x08: Serial.println("START 전송됨"); break;
        case 0x18: Serial.println("SLA+W ACK"); break;
        case 0x20: Serial.println("SLA+W NACK"); break;
        case 0x28: Serial.println("Data ACK"); break;
        case 0x30: Serial.println("Data NACK"); break;
        case 0x38: Serial.println("중재 패배"); break;
        default: Serial.println("기타 상태");
    }
}

버스가 복구되지 않을 때는 TWI를 끄고 SDA·SCL을 GPIO로 설정한 다음, SCL을 9회 토글하고 STOP 조건을 만든 뒤 다시 초기화한다.

void TWI_Recovery() {
    // TWI 비활성화
    TWCR = 0;

    // SDA, SCL 핀을 GPIO로 설정
    DDRC |= (1 << PC4) | (1 << PC5);  // 출력

    // 클럭 9회 토글로 슬레이브 리셋
    for (int i = 0; i < 9; i++) {
        PORTC |= (1 << PC5);   // SCL HIGH
        _delay_us(5);
        PORTC &= ~(1 << PC5);  // SCL LOW
        _delay_us(5);
    }

    // STOP 조건 생성
    PORTC &= ~(1 << PC4);  // SDA LOW
    PORTC |= (1 << PC5);   // SCL HIGH
    _delay_us(5);
    PORTC |= (1 << PC4);   // SDA HIGH (STOP)

    // TWI 재초기화
    TWI_Init();
}

명칭은 달라도 호환성 확인은 필요하다

TWI와 I2C 사이에는 전기적 사양, 프로토콜, 타이밍의 기술적 차이가 없고 상호 호환된다. 문서에서는 Atmel/Microchip이 TWI, Arduino 라이브러리가 Wire, 일반 산업계가 I2C라는 명칭을 주로 사용하며 데이터시트 표기는 제조사마다 다를 수 있다.

장치를 함께 연결할 때는 전압 레벨 3.3V와 5V, 풀업 저항 값, 100k와 400k 속도 모드, 주소 충돌 여부를 확인한다.

AVRTWII2CArduino임베디드 시스템