[아두이노 중급] 2. I2C통신

이전 포스트에선 아두이노의 유선 통신 3종 중에서 UART(Universal Asynchronous Receiver Transmitter) 방식에 대해 이야기했었다.

UART 통신은 가장 기본이 되는 통신이고 그 사용법이 간단하다보니 많이 사용되지만 실제로 사용하다보면 사실 큰 단점이 있는 것을 깨닫게 된다.

우리가 보통 통신을 사용하는 경우는 보드와 보드 간의 통신보단 센서나 모듈을 사용할 때 사용하는 경우가 많다.

일반적인 센서라면 전기신호를 바탕으로 자신의 상태를 표현하는 경우가 많지만 좀 더 복잡한 센서들을 사용하기 시작하면 단순히 전기신호를 받아들이는 것만으로 그 값을 확인하는 것이 어려운 경우가 생기기 시작한다.

간단한 예로 온도센서를 사용한다고 생각해보자.

TMP36과 같은 온도센서는 0도일 때 0.5V를 출력하고 그 이후에 온도가 1도 변화할 때마다 0.01V가 변화한다는 특성을 가진다.

그래서 우리는 간단히 온도센서에서 출력되는 전압을 측정하는 것만으로도 온도를 계산할 수 있게 된다.

하지만 조금 더 복잡한 센서라면 어떨까.

예를들어 심전도 또는 근전도 신호를 측정하는 센서들을 생각해보면 단순히 전압을 측정하는 것으로는 그 값을 알기 어렵다.

이러한 신호들은 그 값이 수십 마이크로 볼트에서 수밀리 볼트 정도의 아주 작은 전압 값이 출력되기 때문에 우리가 사용하는 아두이노와 같은 보드로는 그 값을 확인하기 어렵다.

그래서 이런 신호를 다루는 센서들은 그 값을 증폭하고 거기에 증폭하면서 생긴 잡음을 정리하는 그런 동작들이 필요하다.

이런 동작들을 하기 위해서 다양한 회로(필터)들을 설계해서 센서를 구성하지만, 요즘은 이런 아날로그 회로보단 마이크로컨트롤러(MCU, Micro Controller Unit)를 추가해서 회로와 알고리즘을 동시에 사용해서 센서 값을 정리하는 방식을 사용한다.

그럼 이렇게 되면 우리는 센서와 바로 통신을 하는 것이 아닌 센서에 탑재된 마이크로컨트롤러와 통신을 해야 하는 일이 생긴다.

이때 앞선 포스트에서 이야기한 UART 통신을 사용하면 어떻게 될까.

물론 충분히 통신을 통해 데이터를 받을 수 있다.

하지만 UART 통신은 1:1 통신 방식이기 때문에 한 번에 하나의 센서의 값만 받을 수 있을 것이다.

이렇게 되면 우리가 할 수 있는 것에 한계가 생긴다.

그래서 이런 경우엔 1:N 통신 즉 다중 통신을 지원하는 통신 방식을 사용해야 하는데 그때 사용하는 대표적인 통신 방식이 I2C(Inter-Integrated Circuit) 통신과 SPI(Serial Peripheral Interface) 통신이다.

이 두 통신은 1:N 통신을 이용해서 하나의 MCU(Master)에서 여러 대의 MCU(Slave)를 제어할 수 있다.

또한 이렇게 여러 대를 제어하는 것에 사용되는 데이터 핀도 많지 않다.

거기에 UART 통신의 경우 전송속도(Baud rate)를 정확히 지켜주어야 받는 측에서 그 값을 판단 할 수 있지만 I2C나 SPI 통신은 서로 공통된 클럭신호 맞추기 때문에 타이밍이 다소 틀어져도 정확한 값을 전달할 수 있다.

이러한 장점으로 센서류에는 보통 I2C와 SPI 통신이 많이 사용된다.

그래서 이번 포스트에서는 I2C 통신에 대해 알아보자.

I2C 통신은 실제로는 I2C라고 표현하고 또는 명칭인 Inter-Integrated Circuit에서 따와서 IIC 통신이라고 불린다.

이 통신은 주변장치 사이의 저속 통신을 용도로 Philips(현재 NXT)에서 개발한 규격으로 개발 당시에는 Philips 단독으로 사용되었지만 이후 여러 장점이 부각되면서 현재는 널리 사용되고 있다.

또 I2C 통신은 두 가닥의 선을 사용하므로 TWI(Two Wire Interface)라고도 불린다.

이 이름대로 I2C 통신은 양방향 오픈 드레인 선인 SCL(Serial Clock)과 SDA(Serial Data)로 이루어져 있고 각각 MCU들을 Master 역할과 Slave 역할로 나누어 사용한다.

SCL은 통신의 동기화를 위한 클럭 선이고 SDA는 데이터 용 선인데 Master가 SCL로 동기화를 위한 클럭을 생성하고 이 클럭에 맞춰 Master와 Slave가 SDA로 데이터를 출력하거나 입력을 받는다.
(이때 동기화를 위한 클럭이라고 했지만 클럭을 좀 쉽게 표현하면 서로 동일하게 움직일 수 있도록 박자를 맞추는 것이라고 생각하면 쉽다.)

이처럼 데이터를 주고 받는 선이 하나라 동시에 데이터를 주고 받을 수 없어 전송속도가 느리다라고 말하게 된다.

이런식으로 하나의 선으로 데이터를 주고 받는 형식을 반이중(half-duplex) 방식이라고 한다.
(그리고 추가하자면 Master와 Slave라고 표현했는데 이게 부정적은 느낌이라 최근엔 이러한 표현을 대체하기 위해 Master를 Central, Host, Controller라고 부르고 Slave를 Peripheral, Device, Target 이라고 부르는 경우가 있다.)

자, 그럼 간단한 I2C 통신의 모습을 살펴보자.

위의 그림이 I2C 통신의 기본적인 블록도다.

그림을 보면 왼쪽 끝에 Mater 장치가 연결되어 있고 Master로 부터 나온 SDA SCL 두개의 데이터선에 여러 Slave 장치들이 연결되어 있는 구조다.

우리가 사용할 때는 보통 아두이노 보드가 Master가 되고 각종 센서와 같은 모듈들이 Slave 역할을 하게 된다.

이처럼 I2C 통신은 여러 Slave들을 동시에 연결하여 사용하게 되는데 이렇게 되면 꼭 필요한 것이 있다.

바로 Slave를 구분하는 방법이다.

그림과 같이 연결되어 있다면 같은 선을 연결할 뿐 어떤게 1번 장치인지 2번 장치인지 구분할 수 없다.

그래서 I2C 통신에선 Slave들을 구분하기 위해 고유 주소를 사용한다.

고유 주소는 보통 7비트로 식별되고 개수로 친다면 128개까지 주소를 만들 수 있다.

이 말을 다르게 한다면 최대 128개의 Slave를 한 번에 연결하여 사용할 수 있다는 것이다.
(물론 현실적으로 128개를 모두 사용할 수 없다. 실제론 표준 예약어 같은 것들이 있어 진짜 많이 사용한다고 해도 112개 정도가 될 것이다.)

그럼 우리가 Slave에 각각 주소를 지정해줘야 할까?

그건 아니다.

우리가 직접 I2C 통신을 할 수 있게 각각의 역할을 만드는 것이라면 별도로 주소를 지정해야겠지만 보통 우리가 사용하는 모듈들은 자체적으로 주소가 다 배정받은 상태로 판매가 진행된다.

그래서 I2C 통신을 기반으로 하는 센서 모듈을 사용할 것이라면 그 센서의 통신 주소를 데이터시트(Datasheet)나 판매 사이트에서 먼저 확인해야 될 것이다.
(데이터시트엔 같은 센서를 여러개 사용할 경우 주소를 바꾸는 방법도 기재되어 있으니 꼭 참고하자.)

자, 이렇게 Slave를 구분하는 방법에 대해 알아봤고 이번에는 데이터를 주고 받는 방식에 대해 이야기해보자.

위의 그림이 I2C 통신의 동작 원리를 나타낸 것으로 SDA와 SCL 두 데이터 선이 어떻게 동작하는지를 표현한 것이다.

그림의 왼쪽이 통신을 시작할 때를 표현한 것이고 오른쪽이 통신이 종료될 때를 표현한 것이다.

통신을 시작할 때를 먼저 살펴보면 처음 SDA와 SCL은 HIGH 상태를 유지한다.

그리고 통신이 시작되면 SDA가 LOW로 내려가게 되고 이를 통해 I2C 통신이 시작되었을 감지한 SCL이 클럭의 역할을 하게된다.

반대로 통신이 완료되었을 땐 SCL을 HIGH 상태로 되돌리고 SDA가 HIGH 상태가 되면서 통신이 완료되는 것이다.

이와 같이 통신을 할 때 HIGH와 LOW로 변해가면서 통신을 하는데 단순히 SCL과 SDA만 연결된 상태로 동작을 하게되면 동작이 되지 않는 상황이 발생한다.

그 이유는 I2C가 오픈 드레인(Open-Drain) 방식이기 때문이다.

오픈 드레인 방식은 이름에 드레인이 들어가 있는 것처럼 MOSFET에 해당하는 용어다.

너무 깊게 들어가면 전자공학 쪽으로 깊어지니 간단히 말하자면 MOSFET은 트랜지스터(Transistor)의 일종으로 게이트(G, Gate)에 HIGH 값을 주면 드레인(D, Drain)과 소스(S, Source)가 연결되는 형태를 말한다.

오픈 드레인이라는 것은 이 MOSFET에서 소스쪽에 GND를 연결해두고 드레인 쪽은 아무것도 연결하지 않은, 열려있는(Open) 방식을 말한다.

이 드레인쪽으로 SCL이나 SDA 선이 나오게 된다고 생각하면 되겠다.

이때 이 MOSFET의 게이트로 신호를 주면 소스와 드레인이 연결되는데 이렇게 되면 드레인은 GND 값이 출력되게 된다.

그럼 당연히 드레인에서 측정되는 값은 신호로 LOW 값이 표현될 것이다.

반대로 게이트로 신호를 주지 않은 경우를 생각해보자.

게이트로 신호를 주지 않으면 소스와 드레인은 서로 끊어지게 된다.

이렇게 되면 드레인에는 어떤 값이 나올까.

우리가 원하는 것은 HIGH 값이지만 지금 드레인에 연결된 전원이 없기 때문에 HIGH 값은 물론 LOW 값도 출력할 수 없다.

이런 상태를 플로팅(Floating) 상태라고 한다.

이도 저도 아닌 상태란 뜻이다.

그렇기 때문에 드레인에서 HIGH 값을 주기 위해선 이곳에 전원을 연결해줘야 하는 것이다.

그것이 I2C 블록도에서 SCL과 SDA가 전원에 연결된 곳이다.

이 전원으로 인해 평상시엔 HIGH 값을 유지하고 게이트로 신호를 주면 LOW 값을 출력하게 되는 것이다.

그런데 이때 전원을 그냥 무작정 연결하면 어떻게 될까.

만약 5V를 SCL과 SDA에 직접 연결하게 되면 게이트에 신호를 준 순간 이 5V와 GND가 다이렉트로 연결되는 상황이 발생한다.

이런 상황을 쇼트 상태 또는 단락 상태라고 하는데 이렇게 되면 엄청난 전류가 흐르게 되면서 회로가 다 타버리는 상황이 발생한다.

그래서 전압은 공급하되 전류를 너무 많이 흐르지 않게 막아주기 위해 저항을 사용하게 되는데 이 저항을 풀업(Pull up) 저항이라고 부른다.

그리고 이 저항 값도 마찬가지로 너무 낮으면 큰 전류가 흘러서 안되고 적당한 전류를 흘려줘야 하는데 그렇다고 저항 값이 크면 신호선의 HIGH로 변하는 속도가 느려져 사용이 어렵다.

그래서 적당한 속도(100kbps 표준)에서 신호가 뭉개지지 않고 충분히 빠르게 올라갈 수 있는 안정적인 값을 찾게 되는데 그 값이 보통 4.7kΩ이다.

참고로 만약 MOSFET가 BJT 방식이라면 오픈 컬렉터(Open Collector) 방식이라고 부른다.

이렇게 보면 굳이 왜 이런 방식을 사용할까 싶지만 이 방식은 뒤에 어떤 회로가 오든지 대응할 수 있게 확장성을 높인 방식이라고 할 수 있다.

간단히 예를 들자면 어떤 회로는 3V를 기준으로 할 수도 있고, 어떤 회로는 5V를 기준으로 할 수 있다.

그런 경우 오픈 드레인 방식이라면 풀업 저항에 연결된 전압에 맞춰 회로를 동작시킬 수 있기 때문에 사용이 편리할 것이다.

뭐 이외에도 다양한 이유가 있겠지만 이런 다양성 때문에 오픈 드레인 방식을 사용한다고 알아두자.

아무튼 다시 본론으로 돌아와서 I2C 통신은 저런 방식으로 데이터를 주고 받는다.

위의 표시한 방식은 시작과 종료에 대한 내용이지만 이외에도 Master에서 데이터를 보내면 8비트마다 데이터를 받은 Slave 쪽에서 SDA 값을 LOW로 떨어뜨려 받았다는 표시를 하는 ACK(Acknowledgment)나 데이터를 받은 장치가 SDA를 계속 HIGH로 두어 못받았다는 표현이 되는 NACK(Negative  Acknowledgment) 같은 응답 방식도 있다.

자, 그럼 이번에는 이 I2C 통신 방식을 아두이노 보드에 사용해보자.

간단한 센서를 사용하면 쉽게 되긴하겠지만 센서를 통해 데이터를 주고 받는 것은 다른 포스트에서도 할 수 있으니 여기서는 2개의 아두이노 보드를 이용해서 해보자.

위의 그림은 아두이노 메가 보드와 우노 보드를 연결한 모습이다.

우노 2개로 해도 되지만 좀 구분이 되게 하려고 서로 다른 보드를 사용했다.

배선된 것을 보면 알겠지만 우노 보드에선 A4번이 SDA, A5번이 SCL 역할을 하고 메가에선 20번 핀이 SDA, 21번이 SCL의 역할을 한다.

참고로 UART 통신은 RX와 TX핀을 교차로 연결해야 하지만 I2C 통신은 서로 같은 역할을 하는 핀에 연결해야 한다.

그리고 회로를 보면 이상한 것을 볼 수 있는데 바로 위에서 이야기한 풀업 저항이 연결되어 있지 않을 것이다.

그 이유는 아두이노 보드는 I2C 통신을 사용하게 되면(라이브러리를 불러오면) 자동으로 보드 내부에서 I2C 통신 회선으로 풀업 저항이 연결되게 된다.

그래서 별도로 풀업 저항을 연결하지 않은 것이다.

아마 다른 자료를 찾아 볼때도 보통 회로들은 SCL과 SDA 핀에 풀업 저항이 연결되지 않은 모습을 많이 볼 수 있을 건데 그 이유가 바로 이것이라고 생각하면 되겠다.

다만, 이 내부 풀업 저항에 대해서는 안정성에 대한 논의가 꽤 있어서 만약 좀 정밀하게 써야 될 때는 아래 그림처럼 외부에 풀업 저항을 연결하는 것이 좋다.

그리고 추가로 전원 부를 보면 메가 보드가 우노 보드의 전원을 공유하는 것을 볼 수 있다.

이것은 편의상 이렇게 한 것이고 두 보드를 동시에 업로드 한다고 한다면 전원을 따로 넣어도 괜찮다.

다만 전원을 따로 넣는다고 해도 이렇게 통신을 할때는 서로 GND를 반드시 연결해주어야 한다.

우리가 흔히 전압의 정의를 깜박하는데 전압은 두 지점의 전위(전기적 위치에너지)차를 나타내는 것이다.

보통 +극이 높은 전위, -극이 낮은 전위라고 하고 이 두 지점의 차이를 전압이라고 한다.

그럼 5V라고 표기되어 있으면 다 같은 것일까.

물론 전위 차이니 5V는 맞겠지만 각자의 낮은 전위 즉, 기준점(0V)이 서로 같을까.

당연히 이 기준점은 다 조금씩 차이가 있다.

그래서 각자 같은 전원이라고 무작정 신호선만 연결하면 서로의 전압이 맞지 않아 이상 동작을 하는 경우가 생긴다.

이런 경우를 방지하기 위해 전원이 여러 개 들어가는 경우 이 낮은 기준점을 하나로 맞춰주는 것이 기본이다.

이것을 일반적으로 공통 접지라고 한다.

자, 이렇게 간단한 연결까지 다 해보았다.

이번에는 동작을 위한 코드에 대하여 알아보자.

I2C 통신을 위해서는 앞에서도 이야기했지만 각 Slave에 대한 주소도 알아야하고 SCL, SDA 핀을 제어해가면서 통신을 해야 한다.

이런 복잡한 동작을 줄이기 위해 아두이노에선 라이브러리를 제공하고 있다.

그 라이브러리의 이름이 Wire다.

실제론 TwoWire 라는 이름인데 이것은 I2C 통신의 또다른 이름인 TWI(Two Wire Interface)에서 따온 것이다.

TwoWire 라이브러리에서 전역개체로 Wire를 사용하고 있어 코드 상에선 Wire를 통해 통신이 이루어진다.

그럼 Wire 라이브러리에 포함된 함수에 대해 알아보자.

Wire.begin()
Wire.begin(address)

Wire 라이브러리를 초기화하는 함수다.

괄호 안에 아무것도 넣지 않는다면 Master 역할을 한다는 뜻이고 괄호 안에 address를 넣는다면 Slave 역할을 한다고 선언하는 것이다.

그리고 여기 address에 넣는 것이 Slave의 주소로 최대 7비트까지 나타낼 수 있고 보통은 편의상 0x04와 같이 16진수의 형태로 표현하게 된다.

이때 04 앞에 들어가는 0x가 16진수 숫자다라고 표현하는 약속된 기호라고 생각하면 되고 뒤에 나타낸 숫자가 실제 16진수 수에 해당한다.

Wire.beginTransmission(address)

지정한 주소의 Slave로 데이터 전송을 시작한다.

실제 전송은 wirte 함수에 의해 버퍼에 기록된 후 Wire.endTransmission 함수로 데이터를 전송하게 된다.

여기서 address는 Slave의 주소로 Wire.begin에서 선언한 주소가 사용된다.

Wire.endTransmission(sendStop)

write 함수에 의해 버퍼에 기록된 데이터를 전송함으로 Wire.beginTransmission 함수로 시작된 시작된 Slave 장치로의 데이터 전송을 마친다.

sendStop은 이 함수로 인한 요청이 완료되었을 때 I2C 통신의 정지 메시지를 보낼지를 지정하는 것으로 true나 false 값을 사용한다.

기본적으로 빈괄호로 두었을 때 디폴트 값은 true이고 만약 false를 지정하면 Master는 요청이 완료된 후에도 연결을 유지하여 다른 Master 장치가 데이터를 요구할 수 없도록 한다.

그리고 이 함수는 전송 결과를 반환 값으로 나타내는데 0이 아닌 값은 전송 괴정에서 오류가 발생하였음을 나타낸다.

반환 값은 0부터 4까지 있으며 1의 경우는 버퍼용량 초과, 2는 주소 전송 후 오류 발생, 3은 데이터 전송 후 오류 발생, 4는 기타 오류가 발생했을 경우를 뜻한다.

Wire.write(data, quantity)

Master의 요청에 따라 Slave에 전송할 데이터를 버퍼(대기실)에 기록하는데 사용된다.

data는 전송할 내용을 뜻하고 quantity는 정송할 데이터의 바이터 수를 뜻한다.

quantity는 생략이 가능하고 전송된 바이트 수가 반환된다.

실제 전송은 Wire.endTransmission 함수가 사용되었을 때 일어난다.

만약 Slave에서 사용되었다면 Master로부터 데이터 요청을 받았을 때 데이터를 응답(전송)하는 역할을 한다.

Wire.requestFrom(address, quantity, sendStop)

Master가 Slave에게 지정한 양의 데이터를 요청한다.

quantity가 지정한 양이되며 바이트 단위가 사용된다.

sendStop은 앞의 Wire.endTransmission과 마찬가지로 요청완료 후 정지 메시지 전송 여부를 지정하는 변수다.

Wire.read( )

Master에서 Wire.requestFrom 함수로 Slave에 요청한 데이터를 읽는 함수로 한 바이트씩 읽어 반환한다.

반대로 Slave에서 Master에서 온 데이터를 읽기 위해서 사용할 수도 있다.

Wire.available( )

Wire.read 함수로 읽어 들일 수 있는 유효한 바이트 수를 반환한다.

Master 장치에서 Slave 장치로 데이터를 요청한 후 도착한 데이터를 검사하기 위하여 주로 사용된다.

Wire.onReceive(void (*) (int))

Slave가 Master로부터 데이터를 수신했을 때 호출되는 핸들러(handler) 함수를 등록한다.

간단하게 표현하면 Slave가 Master로부터 데이터를 받았을 때 실행할 동작(핸들러)을 미리 등록하는 함수다.

등록할 핸들러 함수는 수신한 데이터가 총 몇 바이트인지를 알려주는 int형 매개변수를 하나 가져야 한다.

이 매개변수에는 방금 Master로부터 들어온 데이터가 총 몇 바이트인지를 나타내는 정수 값이 자동으로 전달된다.

Wire.onRequest(void (*) (void))

Slave가 Master로부터 데이터 요청을 받았을 때 호출되는 핸들러 함수를 등록한다.

간단하게 Master가 Slave에게 데이터를 보내달라고 요청했을 때 Slave가 응답할 동작을 미리 등록하는 함수다.

핸들러 함수는 void 함수이름의 형태여야하고 이 함수는 반환 값이 없으며 변수도 갖지 않는다.

전송되는 데이터의 크기는 available 함수를 통해 알아낼 수 있다.

여기까지가 간단하게 Wire 라이브러리 내의 함수를 정리한 것으로 이 함수들을 어떻게 이용할 것인지 자신의 재량에 달렸다.

그럼 이번에는 이 함수들을 이용해서 간단하게 데이터를 주고 받는 예제를 만들어보자.

우리가 연결한 것은 메가 보드와 우노 보드이니 먼저 각자의 역할을 정해야 한다.

여기선 메가 보드를 Master, 우노 보드를 Slave로 두자.

코드 1 Master – Mega
#include <Wire.h>

#define Slave 0x04

void setup() {
  Wire.begin();
  Serial.begin(9600);
}

void loop() {
  Wire.requestFrom(Slave, 1);
  char c = Wire.read();
  Serial.println(String(c, DEC));
  delay(1000);
}

코드 2 Slave – Uno
#include <Wire.h>

#define Slave 0x04

byte count = 0;

void setup() {
  Wire.begin(Slave);
  Wire.onRequest(sendToMaster);
}

void loop () {

}

void sendToMaster() {
  Wire.write(++count);
}

코드 1과 2를 각각 메가, 우노 보드에 업로드 하자.

업로드를 한 뒤엔 Master에 해당하는 메가 보드의 시리얼 모니터를 보면 숫자가 1초에 하나씩 올라가는 것이 보일 것이다.

확인을 했다면 이제 코드를 설펴보자.

코드 1을 보면 먼저 Slave의 주소를 정의해주었고 setup에서 Wire 라이브러리를 초기화 시켜주었다.

이때 Wire.begin의 괄호 안을 비어두었기 때문에 메가보드가 Master의 역할이 된다.

그리고 이때 Serial을 설정한 것은 값을 시리얼 모니터로 보기 위해서다.

다음으로 loop를 보면 Slave에서 데이터를 받아오는 코드가 작성되어 있다.

먼저 requestFrom 함수를 통해 Slave에 데이터를 요구하는데 괄호 안으로 Slave의 주소와 요구할 데이터 양을 넣어주었다.

이때 데이터 양으로 1을 넣어서 1바이트의 데이터를 요구하게 된다.

그리고 요구된 데이터는 read 함수를 통해 읽는다.

읽어온 값은 변수 c에 저장되고 그 값이 시리얼 모니터에 출력되는 것이다.

이제 코드 2로 넘어가보자.

코드 2는 Slave에 대한 코드로 여기도 마찬가지로 먼저 Slave의 주소를 설정해주었다.

거기에 setup을 보면 begin 함수의 괄호 안에 주소를 넣음으로써 해당 주소를 가지는 Slave 임을 선언했다.

다음으로 onRequest 함수가 나오는데 이 함수를 통해 데이터 전송 요구가 있을 때 괄호 안의 함수가 동작하도록 되어있다.

동작함수는 seondToMaster 함수로 loop 밑으로 정의되어 있는 함수다.

이 함수는 count 변수를 1씩 증가시키면서 write 함수를 통해 데이터를 전송하도록 한다.

그래서 메가 보드의 시리얼 모니터에서 값이 1씩 증가하는 모습이 보이는 것이다.

이렇게 간단하게 Master에서 값을 물어보고 Slave에서 대답하는 코드를 작성해보았다.

이번에는 조금 변형해서 Master에서 데이터를 보내고 Slave에서 그 데이터를 받아 어떤 동작을 하는 기능을 추가해보자.

코드 3 Master – Mega
#include <Wire.h>

#define Slave 0x04

void setup() {
  Wire.begin();
}

void loop() {
  Wire.beginTransmission(Slave);
  Wire.write(1);
  Wire.endTransmission();
  delay(1000);

  Wire.beginTransmission(Slave);
  Wire.write(2);
  Wire.endTransmission();
  delay(1000);
}

코드 4 Slave – Uno
#include <Wire.h>

#define Slave 0x04

int LED = 13;
byte rec[128];

void setup() {
  Wire.begin(Slave);
  pinMode(LED, OUTPUT);
}

void loop () {
  Wire.onReceive(record);

  if (rec[0] == 1) {
    digitalWrite(LED, HIGH);
  }

  else if (rec[0] == 2) {
    digitalWrite(LED, LOW);
  }
}

void record(int receiveNum) {
  for (int i = 0; i < receiveNum; i++) {
    rec[i] = Wire.read();
  }
}

코드 3과 4를 각각 업로드해보면 이번엔 Slave인 우노 보드에서 13번 LED가 1초에 한 번씩 깜박이는 것을 볼 수 있다.

코드 3을 살펴보면 코드 1과 비슷하지만 loop쪽이 다른 것을 알 수 있다.

앞에서는 데이터를 요청하는 것이었고 이번에는 Slaver로 데이터를 보내는 것이다.

여기 나와있는 것처럼 데이터를 보내는 것은 3개의 함수가 한 세트로 움직인다.

위에서 정의했었지만 먼저 데이터를 보내기 위해 beginTransmission 함수를 사용해서 해당 Slave에게 데이터 전송 시작을 알리고 write를 통해 전송할 데이터를 버퍼에 저장한다.

마지막으로 endTransmission으로 데이터 전송을 시작한다.

이렇게 세트로 동작하는 것이다.

그래서 loop를 다시 보면 한 세트는 ‘1’이라는 값을 보내고 다른 세트는 ‘2’라는 값을 보내고 있는 것이다.

이번에는 코드 4를 보자.

코드 4는 코드 2와 비슷하지면 onRequest 대신 onReceive 함수를 사용해서 Master가 보내주는 데이터를 받고 있다.

이때 핸들러 함수로 record가 사용됬는데 record 함수는 매개변수가 int형으로, 앞에서 설명했듯이 onReceive 함수는 데이터를 수신 받았을 때 수신 받은 데이터의 바이트 수가 이 int형 매개변수에 저장된다.

그래서 record 함수를 보면 받아온 바이트 수를 이용해서 데이터를 저장하고 있다.
(write 함수를 통해 전송된 데이터는 보통 한 바이트씩 전송된다.)

그리고 이렇게 읽어온 데이터를 rec라는 변수에 저장하고 저장된 값을 loop에서 비교해서 1이면 13번 LED를 켜고 아니면 끄는 동작을 하고 있다.

거기에 Master에서 1초에 한 번씩 1과 2를 번갈아가면서 보내고 있기 때문에 우노 보드에서 13번 LED가 1초 켜지고 1초 꺼지는 동작을 반복하는 것이다.

자, 이렇게 간단하게 Master에서 Slave로 데이터를 요청하고 받는 것과 데이터를 보내고 받는 동작을 해보았다.

이러한 동작들을 바탕으로 여러 기기들을 추가한다면 Master에서 현재 상태를 알아볼 수 있는 관리 시스템도 설계할 수 있을 것이다.

이 코드들이 기본이 되는 코드들이니 다양한 곳에 활용해보자.

error: Content is protected !!