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)
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와 버스 사이의 통신을 처리한다.
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 및 상태 코드를 확인한다.
| 코드 | 설명 |
|---|---|
| 0x08 | START 조건 전송됨 |
| 0x10 | REPEATED START 전송됨 |
| 0x18 | SLA+W 전송, ACK 수신 |
| 0x20 | SLA+W 전송, NACK 수신 |
| 0x28 | 데이터 전송, ACK 수신 |
| 0x30 | 데이터 전송, NACK 수신 |
| 0x38 | 중재 패배 |
마스터 수신은 SLA+R 전송 후 수신을 계속할 데이터에 ACK를, 마지막 데이터에 NACK를 응답하는 흐름이다.
| 코드 | 설명 |
|---|---|
| 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 슬레이브와 통신한다.
마스터 모드에서는 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 속도 모드, 주소 충돌 여부를 확인한다.