Arduino I2C/TWI 통신 (2013.10.23)

2개 이상의 Arduino 보드를 서로 직접 연결하여 통신하려면? (또는 Raspberry Pi와 Arduino를 USB 시리얼이 아니라 직접 연결하여 통신하려면?)

I2C와 SPI, 이 두 가지 중 하나를 사용할 수 있으나 아주 간단한 저속 통신용으로는 I2C만으로 충분하다.

우선 I2C란 무엇인가?

  • 회로 상에서 장치들이 어떻게 통신할 것인지 필립스 사가 1980년대 고안한 BUS 방식
  • SDA(Serial Data Line), SLK(Serial Clock)라 부르는 2 개의 연결만 사용.
  • 단 2개의 연결을 사용하는 방식이어서 Two Wire Interface, 즉 TWI라고 부르기도 한다.
  • 하나의 마스터(Master) 장치와 여러 개의 슬레이브(Slave) 장치가 이 두 회선을 통해 연결.

어떻게 연결하는가? I2C로 통신하고자 하는 모든 장치는 SDA, SCL 핀끼리 연결한다. 만약 2 개의 장치라면 서로 직접 연결하고 그보다 많으면 빵판 등을 통해 공통의 선에다 연결한다. 참고로 연결에 참여하는 모든 장치는 공통의 GND로 연결해야 한다.

한편, I2C/TWI 핀은 각 아두이노 보드마다 그 위치가 다르다:

  • UNO, Duemilanove(2009), Pro, Pro Mini 등: A4 (SDA), A5 (SCL)
    • Atmega328 칩셋
    • UNO REV 3에서는 AREF 핀 옆에 SDA, SCL 핀이 있어 이 곳을 사용 가능
  • Leonardo, Micro: 2 (SDA), 3 (SCL)
    • Atmega32u4 칩셋
  • Mega2560: 20 (SDA), 21 (SCL)
  • Due: 20 (SDA), 21 (SCL)
    • 별도의 SDA1, SCL1 핀이 있어 총 4개의 핀

다음은 Arduino UNO 보드의 A4, A5를 연결한 그림이다.

i2c-bw-uno

Arduino UNO Rev 3 에서는 전용으로 추가된 핀을 사용해도 좋다. AREF라고 적힌 핀 옆에 있는 2 개의 핀이 SDA, SCL 이다. (AREF에 가까운 쪽이 SDA)

i2c-bw-uno-2

다음은 다른 보드 간의 연결 예제다. UNO와 Micro를 연결해 보았다.

i2c-bw-uno-micro

다음은 UNO와 Micro 보드를 I2C 연결한 예제 사진이다:

  • 공통 GND 연결 (언제나 중요!)
  • 전원 입력
    • 각각 전원을 입력하거나
    • 또는 한 쪽에만 전원을 연결하고 한 쪽의 5V를 다른 한 쪽의 Vin으로 연결하여 전원 공급 가능
  • SDA, SCL 핀 연결:
    • 보드 타입이 같으면 둘 다 같은 핀을 연결
    • 보드 타입이 다르면 각각 SDA, SCL로 어떤 핀을 사용하는지 확인하고 연결.

P20131024_125709606_75EC1061-F82E-410A-B874-10EB87C37C7F

위 예제에서는 두 보드에 따로 전원을 연결하지 않고 한 쪽에만 전원을 공급한 뒤, 5V 핀을 다른 보드의 Vin을 통해 전원을 공급해 주고 있다.

[[ 통신 방식 ]]

I2C에서는 전형적으로 하나의 마스터(Master) 장치가 있고 1 개 이상의 슬레이브(Slave) 장치가 존재한다.  마스터 장치와 슬레이브 장치 간의 통신은 항상 마스터 장치가 어떤 요청을 특정 슬레이브 장치에게 요청하는 것으로 시작한다.  다음 두 가지 경우가 있다:

  • 마스터 전송 / 슬레이브 수신
  • 마스터 수신 / 슬레이브 송신

[[ 마스터 전송/슬레이브 수신 ]]

마스터가 전송하고 슬레이브가 수신하는 예제는 다음과 같다:

  • 마스터 쪽에서는 기본 예제 중 Wire -> master_writer 예제 선택하여 업로드
  • 슬레이브 쪽에서는 기본 예제 중 Wire -> slave_receiver 예제 선택하여 업로드

마스터 스케치는 다음과 같다:

#include <Wire.h>

void setup()
{
  Wire.begin(); // i2c 버스에 참여(마스터의 경우, 주소를 적지 않아도 된다)
}

byte x = 0;

void loop()
{
  Wire.beginTransmission(4); // 4번 슬레이브 장치로 전송 시작
  Wire.write("x is ");        // 5 바이트 전송
  Wire.write(x);              // 그 다음 1 바이트 전송
  Wire.endTransmission();    // 전송 종료

  x++;
  delay(500);
}
  • i2c 프로그래밍에는 Wire 라이브러리를 사용한다.
  • Wire.begin(addr)를 호출하면 i2c 버스에 참여하게 된다.
  • 이 때 마스터는 addr을 생략할 수 있다.
  • 슬레이브에게 데이터를 전송하고자 할 때에는 beginTransmission(slave_address)로 시작해서 endTransmission()으로 마친다.
  • 그 중간에 write()를 사용하여 실제 데이터를 자유롭게 전송한다.

이에 대응하는 슬레이브 스케치는 다음과 같다:

#include <Wire.h>

void setup()
{
  Wire.begin(4); // 자신의 주소를 4번으로 설정하고 i2c 버스에 참여
  Wire.onReceive(receiveEvent); // 수신 이벤트 함수 등록
  Serial.begin(9600);
}

void loop()
{
  delay(100);
}

// 다음 함수는 마스터가 데이터를 전송해 올 때마다 호출됨
void receiveEvent(int howMany)
{
  while(1 < Wire.available()) // 마지막 한 개의 데이터를 제외하고 읽어 들임
  {
    char c = Wire.read(); // byte를 읽어 char로 변환
    Serial.print(c);
  }
  int x = Wire.read(); // byte를 읽어 int로 변환
  Serial.println(x);
}
  • 슬레이브는 마스터와 달리 i2c 버스에 참여하기 위해 Wire.begin(my_address)를 호출할 때 자신의 주소를 적어야 한다.
  • 슬레이브의 프로그래밍 방식이 전혀 다르다.
  • loop()에서 데이터가 오기를 기다리는 것이 아니라 인터럽트 방식으로 작성한다.
  • 마스터로부터 데이터가 오면 onReceive()를 통해 등록한 함수가 호출된다.

[[ 주소 개념 ]]

일대일 통신이 아닌 경우에는 서로를 구별하기 위해 “주소(address)”라는 개념이 필요하다. 그런데 위에서 보는 것과 같이 i2c에서는 장치가 주소를 마음대로(?) 결정한다. 즉, 주소 결정의 책임이 우리에게 있으므로 장치들이 같은 주소를 사용하지 않도록 조심해야 한다. (I2C 방식을 사용하는 제품을 구입할 때에는 그 제품이 사용하는 기본 주소가 무엇인지 그리고 필요할 때 어떻게 변경할 수 있는지 점검해야 한다.)

[[ 마스터 수신/슬레이브 송신 ]]

이게 좀 독특하다. 일반적으로 송신하는 쪽은 자유롭게 송신하고 수신하는 쪽이 대기하기 마련이데, I2C에서는 마스터가 데이터를 수신하고자 할 때에도 마스터가 먼저 슬레이브에게 “너로부터 데이터를 받고자 한다”라고 신호를 보낸다.

마스터가 수신하고 슬레이브가 전송하는 예제는 다음과 같다:

  • 마스터 쪽에서는 기본 예제 중 Wire -> master_reader 예제 선택하여 업로드
  • 슬레이브 쪽에서는 기본 예제 중 Wire -> slave_sender 예제 선택하여 업로드

마스터의 스케치는 다음과 같다:

#include 

void setup()
{
  Wire.begin(); // i2c 버스에 참여
  Serial.begin(9600);
}

void loop()
{
  Wire.requestFrom(2, 6); // 슬레이브 장치 #2에게 6 바이트의 데이터를 요청

  while(Wire.available()) // 슬레이브가 요청한 바이트 수를 보내라는 보장은 없음. 데이터가 있는지 점검하면서 읽음
  { 
    char c = Wire.read(); // byte를 읽어 char로 변환
    Serial.print(c);
  }

  delay(500);
}
  • 마스터가 슬레이브로 데이터를 받고자 할 때에는 requestFrom(slave_address, how_many_bytes)를 호출한 뒤 기다린다.

슬레이브의 스케치는 다음과 같다:

#include <Wire.h>

void setup()
{
  Wire.begin(2);                // 자신의 주소를 2번으로 설정하고 i2c 버스에 참여
  Wire.onRequest(requestEvent); // 송신 요청을 받았을 때 호출할 함수 등록
}

void loop()
{
  delay(100);
}

// 마스터가 자신에게 데이터를 요청할 때 호출됨
void requestEvent()
{
  Wire.write("hello ");
}
  • 마스터가 데이터를 요청할 때 호출할 함수를 .onRequest()에서 등록한다.

[[ 참고 자료 ]]

[[ 사족 ]]

  • SMBus는 필립스 사의 I2C를 기반으로 인텔이 1990년대 만든 BUS 규격이며 System Management Bus의 약자
  • 각 보드마다 SDA, SCL 용도의 핀이 다르다는 것을 주의깊게 받아들이지 않아 UNO와 Micro를 연결하는데 한 시간을 허비했다!

댓글 남기기