[아두이노 중급] 3. SPI 통신

이번 포스트에서는 이전 포스트에 이어 직렬 통신의 하나인 SPI(Serial Peripheral Interface) 통신에 대해 이야기해보자.

SPI 통신은 송신과 수신을 SDA 핀 하나로 하고 있는 I2C 통신과 달리 송신과 수신이 동시에 이루어지는 전이중(Full dupelx) 방식으로 통신을 한다.

그렇게 때문에 송신과 수신을 한 번에 하는 반이중 방식인 I2C 통신에 비해 속도가 빠른 장점이 있다.

다만 전이중 방식으로 속도가 빠른 것은 분명 큰 장점이지만 SPI는 송신과 수신을 동시에 하기 위해 I2C 보다 많은 연결 선을 사용하는 단점도 존재한다.

I2C 통신의 경우 클럭 신호를 보내주는 SCL 핀과 데이터를 송수신하는 SDA 핀 2개로 통신을 하게 되지만 SPI 통신은 클럭 신호를 보내주는 SCK(Serial Clock 또는 SCLK)핀과 마스터로부터 슬레이브로 데이터를 전달하는 MOSI(Master Output Slave Input)핀, 슬레이브에서 마스터로 데이터를 보내는 MISO(Master Input Slave Output)핀이 필요하다.

데이터를 SDA 하나로 보내 송신과 수신이 한 번에 동작하는 것이 불가능했던 I2C와 달리 마스터에서 데이터를 보내는 선과 슬레이브에서 데이터를 보내는 선을 분리하여 동시에 송신과 수신이 가능하게 한 것이다.

여기가지만 봤을 땐 데이터 선 하나가 추가됐을 뿐이네라고 생각하고 선이 추가된 것이 큰 단점이 아니라고 생각할 수 있지만 SPI 통신의 연결선은 여기서 끝이 아니다.

만약 여러 개의 Slave 장치들을 연결한다면 I2C 통신의 경우 각 Slave에 부여된 주소를 이용하여 구분하게 되는데 SPI 통신의 경우 이것을 물리적으로 하게 된다.

바로 SS(Slave Select) 또는 CS(Chip Select)라고 불리는 Slave 선택 핀을 사용하는 것이다.

위의 그림이 Master와 Slave가 연결된 모습이다.

앞서 설명한 클럭을 공유하기 위한 SCLK 핀과 데이터를 송수신하기 위한 MOSI, MISO 핀을 Slave들이 서로 공유하여 연결된 것을 볼 수 있다.

그리고 거기에 추가하여 각각의 Slave에 SS 핀이 추가로 연결된 것도 볼 수 있을 것이다.

이처럼 SPI 통신은 기본적으로 SCK, MOSI, MISO 핀을 사용하고 Slave를 구분하기 위해 SS 핀을 사용하게 되는데 이 SS 핀은 Slave가 늘어날수록 추가가되는 구조인 것이다.

그래서 SPI 통신은 Slave를 많이 연결할 수록 Master에서 나오는 선이 많아지게 된다.

그럼 어떻게 Slave를 선택할까.

SPI 통신에선 사용하고 싶은 Slave의 SS핀에 LOW 값을 출력하고 사용하지 않는 Slave에겐 HIGH 값을 출력하게 된다.

이렇게 Slave를 선택하게 되는데 한 번에 하나의 Slave만 선택을 해야 한다.

그래서 그림에 보이는 녹색 Slave를 사용한다고 하면 녹색 Slave의 SS핀에 LOW 값을 주고 나머지 Slave에 HIGH 값을 넣어주어야 하는 것이다.

만약 사용하는 Slave에 LOW값을 주고 다른 Slave에 HIGH 값을 주지 않는다면 정상동작을 하지 않는 원인이 될 수 있다.

물론 이런 Slave 연결 방식은 보통 독립적 슬레이브 구성(Independent Slave Configuration)이라고 하는 방식인데 이 방식이 연결선을 많이 사용하는 방식이긴 하다.

그래서 이것을 개량한 데이지 체인(Daisy Chain) 방식이 있는데 아두이노에서는 굳이 이 방식을 사용하지 않고 설명한 방식으로 연결하게 된다.

이번엔 데이터들이 어떻게 주고 받아지는지 살펴보자.

간단히 구조만 설명하자면 먼저 Master에서 Slave로 SS핀을 통해 신호를 받을 수 있게 활성화시킨다.

이어서 Master는 MOSI 핀을 통해 SCK의 클록에 맞춘 신호를 전송한다.

전송된 신호는 Slave의 MOSI를 통하여 SCK의 클록에 맞춰 수신된다.

SCK에 동기화된 MOSI 신호는 8비트나 16비트 등의 비트 단위로 조합되어 유의한 데이터로 사용된다.

반대로 Slave에서 Master로 데이터를 전송하는 것도 마찬가지로 Master에서 생성된 클럭에 맞춰 동일한 방식으로 하게 된다.

이런 식의 짧은 문장으로만 보면 SPI 통신은 간단해 보이지만 세부적으로 실제 한다고 하면 데이터 직렬화 방법, 클록 동기화 방법, 전송 속도 등 많은 것을 제어하고 각각에 맞는 레지스터를 사용해서 제어해야 한다.

특히 클록 동기화의 경우엔 상당히 복잡하기 때문에 구현하기 쉽지 않다.

하지만 크게 걱정하지 않아도 된다.

아두이노에선 이런 부분을 정리해서 라이브러리로 제공해주기 때문에 우린 좀 더 손쉽게 사용할 수 있다.

뭐 내가 아두이노를 어느정도 사용했고 레지스터를 이용해서 코딩을 해보고 싶다고 한다면 라이브러리 안을 열어서 참고하면서 직접 제작해보는 것도 좋을 것이다.

그래도 여기서는 라이브러리를 사용해서 SPI 통신을 해볼 것이다.

SPI 통신을 위한 라이브러리는 그 이름도 SPI다.

Wire라는 이름으로 만들어져 있던 I2C 통신과는 차이가 있다.

이 라이브러리는 아두이노의 기본 라이브러리 중 하나로 기본 설정 및 데이터 전송함수들을 같이 정의하고 있다.

그래서 I2C 통신에서 사용했던 Wire 라이브러리, 그리고 나중에 다루게 될 RTC 라이브러리와 같이 SPIclass를 정의하고 있으며 이 클래스의 객체인 SPI를 통해 통신이 이루어진다.

그럼 이제 라이브러리 내의 함수를 살펴보자.

SPI.begin( )

SPI 통신을 초기화 한다.

SPI.end( )

SPI 통신을 종료한다.

SPI.setBitOrder(bitOrder)

데이터가 전송될 때 전송 순서를 결정한다.

bitOrder는 LSBFIRST, MSBFIRST 중 하나의 값을 가지는데 LSB는 최하위 비트부터 전송하는 것이고 MSB는 최상위 비트부터 전송하게 된다.

따로 bitOrder을 사용하지 않고 비워둔다면 디폴트 값으로 MSBFIRST가 설정되어 있다.

SPI.setClockDivider(rate)

SPI 통신에 사용되는 클록의 분주 비율을 설정한다.

여기서 분주라는 것은 어느 주파수를 정수비를 이용하여 그보다 낮은 주파수로 만드는 것을 말한다.

만약 16MHz가 있으면 이를 1/4로 낮추어 사용하면 4MHz를 사용하게 되는 것이다.

이런 분주를 사용하면 원하는 낮은 주파수를 얻을 수 있고 기존의 빠른 주파수를 사용하는 것보다 안정도를 높이고 정확도를 높일 수 있는 장점이 있다.

마이크로컨트롤러가 AVR 기반인 보드들은 이 분주 비율을 2, 4, 8, 16, 32, 64, 129 중 하나의 값으로 설정 할 수 있다.

결론적으로 이 함수는 이 분주 비율을 선택하게 해주는 함수인 것이다.

그래서 rate 자리에는 분주 비율을 넣어야 하는데 2, 4, 8과 같이 숫자를 적는 것이 아닌 SPI_CLOCK_DIV와 같은 이름이 상수로 정의되어 있어 이런 명칭을 넣어야 한다.

만약 분주 비가 2라면 SPI_CLOCK_DIV2라고 넣고 4라면 SPI_CLOCK_DIV4 이렇게 끝에 원하는 분주비율의 숫자를 붙여서 입력하면 된다.

rate를 빈칸으로 둔다면 디폴트 값으로 SPI_CLOCK_DIV4가 사용된다.

일반적으로 우리가 사용하는 우노, 메가 보드 같이 AVR 기반의 보드들은 대부분 16MHz 클럭을 사용하므로 SPI 통신에선 4MHz가 기본이 된다.

SPI.setDateMode(mode)

이 함수는 전송모드를 선택하는 함수다.

SPI 통신에서 전송 모드는 위상(Phase)과 극성(Polarity)의 조합에 따라 4가지로 나눌 수 있으며 위상과 극성을 각각 CPHA(Clock PHAse)와 CPOL(Clock POLarity)이라고 부른다.

극성은 클럭이 비활성 상태일 때의 기본값을 결정한다.

밑의 그림을 보자.

그림에서 표에 나타난 것이 선택할 수 있는 4종류의 전송모드로 여기서 에지라고 하는 것은 클럭에서 올라가는 부분과 내려가는 부분을 말한다.

올라가는 부분을 상승 에지, 내려가는 부분을 하강 에지라고 한다.

그림에서 MODE1을 보면 전송시점이 상승 에지가 되고 샘플링 시점이 하강에지가 된다.

일반적으로 통신을 할 때 이런식으로 상승 에지나 하강에지 시점에 데이터를 보내거나 받는 형식으로 통신이 이루어진다.

그렇기 때문에 서로간의 통신은 같은 클록을 사용해야 한다.

물론 복잡한 말이긴 한데 굳이 몰라도 사용할 수 있는 부분이다.

하지만 마스터와 슬레이브 사이에 동일한 모드를 사용해야 정상적인 통신이 가능하므로 센서나 주변 기기를 연결할 때 기기에서 사용하는 모드를 데이터시트에서 확인하고 이에 맞게 동작모드를 설정해야 한다.

그렇기 때문에 어떤 모드가 있는지 정도만 알고 있어도 된다.

SPI.transfer(data)

SPI 통신에서 데이터를 보내고 받는 함수다.

송신과 수신을 동시에 진행하게 된다.

여기까지가 SPI 라이브러리에서 주로 사용되는 함수다.

그런데 사실 우리가 많이 볼 일은 없을 것이다.

대부분의 SPI 통신 기반의 센서들은 센서 자체 라이브러리에 이 내용들을 다 탑재해 두기 때문에 우리가 복잡하게 설정을 할 필요는 거의 없다.

그래도 내가 어느정도 아두이노를 사용했고 이제 좀 고난이도의 프로젝트를 진행해야 하는데 메모리 최적화가 필요하거나 특수한 센서들을 다뤄야 한다면 센서들의 라이브러리 대신 직접 코딩을 해야 하는 경우가 생긴다.

그럴때는 이런 내용을 직접 해야 하니 그때는 이 부분을 다시 떠올려보자.

자, 그럼 이제 실제로 이 함수들을 사용해보자.

이번에도 I2C와 마찬가지로 두 개의 보드를 연결해볼건데 Master 역할에는 메가보드 Slave에는 우노 보드를 사용할 것이다.

위의 그림이 연결된 모습인데 Master 하나와 Slave 하나이기 때문에 연결 선은 전원선을 포함해서 6개가 된다.

그리고 그림을 보면 알겠지만 SPI 통신에 사용되는 핀은 별도로 표기가 되어있지 않다.

메가 보드에 I2C 통신을 위한 SCL SDA 핀이 표기되어 있는 것과는 대조적이다.

그래서 핀의 역할을 기억해야 한다.

핀의 역할은 고정되어 있긴 하지만 SS 핀은 변경이 가능하다.

위의 표에 적혀있는 53과 10번은 마이크로컨트롤러에서 제공해주는 하드웨어 SS핀이다.

보통 하나만 정의되어 있고 SS 핀으로 사용될 땐 별도의 설정없이 사용할 수 있다.
(SPI 라이브러리에서 기본적으로 제어해준다.)

하지만 알다시피 SPI 통신은 1:N 통신이기 때문에 Slave가 많이 사용되는 경우가 있을 것이다.

그럴 경우엔 다른 핀을 사용해도 상관없다.

거기에 하나의 Slave를 연결할 때도 굳이 하드웨어 SS핀을 사용하지 않고 별도의 핀을 지정해줘도 된다.

다만, 그렇게 사용할 때는 참고해야 할 것이 만약 우노를 Master로 사용한다고 할 때 Slave를 8번에 연결한다고 해보자.

이렇게 사용할 수도 있지만 이때 10번 핀은 입력(INPUT)으로 사용하는 것을 자제해야 한다.

8번을 SS로 사용하더라도 10번이 하드웨어 SS이기 때문에 오작동을 일으킬 수 있기 때문이다.
(특히 10번 핀이 입력 상태에서 LOW가 된다면 마스터 모드가 해제될 수 있다.)

아무튼 이런 것만 지킨다면 SS 핀은 다른 핀을 사용해도 상관없다.

그래서 위의 그림에서도 Mega 보드의 하드웨어 SS 핀은 53번이지만 실제 연결은 43번을 사용하고 있다.

자, 그럼 이제 코드를 작성해보자.

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

int slave1 = 43;

void setup (void) {
  pinMode(slave1, OUTPUT);
  SPI.begin ();
  digitalWrite(slave1, HIGH);
  SPI.setClockDivider(SPI_CLOCK_DIV16);
}

void loop (void) {
  const char *p = "Hello, World\n";

  digitalWrite(slave1, LOW);
  for (int i = 0; i < strlen(p); i++) {
    SPI.transfer(p[i]);
  }
  digitalWrite(slave1, HIGH);
  delay(1000);
}

코드 2 Slave -Uno
#include <SPI.h> // SPI 라이브러리

char buf[100]; // 수신된 문자 저장을 위한 버퍼
// pos와 process_it은 인터럽트 처리 루틴에서 값을 바꾸는 변수이므로
// volatile 선언을 통해 업데이트된 값이 정확하게 반영되도록 한다.
volatile byte pos; // 수신 버퍼에 문자를 기록할 위치
volatile boolean process_it; // 개행 문자를 만난 경우 출력하기 위한 플래그

void setup (void) {
  Serial.begin (9600); // 수신 문자열 출력을 위한 직렬 통신 초기화
  
  // 디지털 핀은 디폴트값으로 입력으로 설정되어 있으므로
  // MOSI, SCLK, SS는 입력으로 설정하지 않아도 된다.
  pinMode(MISO, OUTPUT);
  
  // 마스터의 전송 속도에 맞추어 통신 속도를 설정한다.
  SPI.setClockDivider(SPI_CLOCK_DIV16);

  // SPI 통신을 사용할 수 있도록 레지스터를 설정
  // SPCR : SPI Control Register
  SPCR |= _BV(SPE); // SPE : SPI Enable

  // SPI 통신에서 슬레이브로 동작하도록 설정
  SPCR &= ~_BV(MSTR); // MSTR : Master Slave Select

  pos = 0; // 버퍼가 비어 있으므로 0번부터 수신 문자 기록
  process_it = false; // Serial로 출력할 문자열 없음

  // SPI 통신으로 문자가 수신될 경우 인터럽트 발생을 허용
  SPCR |= _BV(SPIE); // SPIE : SPI Interrupt Enable
}

// SPI 통신으로 문자가 수신될 때 발생하는 인터럽트 처리 루틴
ISR (SPI_STC_vect){
  byte c = SPDR; // 수신된 문자를 얻어온다.
  if (pos < sizeof(buf)) { // 현재 버퍼에 저장할 공간이 있는 경우
    buf[pos++] = c; // 버퍼에 수신된 문자 기록
    if (c == '\n') { // 개행 문자를 만나면 수신된 문자열을 Serial로 출력
      process_it = true;
    }
  }
}

void loop (void) {
  if (process_it) { // Serial로 출력할 문자열이 있는 경우
    buf[pos] = 0; // 문자열의 끝 표시
    Serial.print(buf); // 문자열을 Serial로 출력
    pos = 0; // 버퍼가 비었음을 표시
    process_it = false; // Serial로 출력할 문자가 없음을 표시
  }
}

코드 1은 Master인 메가 보드에 코드 2는 Slave인 우노 보드에 업로드하자.

업로드 했다면 슬레이브인 우노 보드의 시리얼 모니터를 켜보자.

그러면 시리얼 모니터에 Hello, World라는 문장이 나타난다.

문구를 확인했다면 이제 코드를 살펴보자.

코드들을 살펴보면 확실히 코드 1이 코드 2보다 확실히 짧은 것을 알 수 있다.

이건 앞에서 이야기한것처럼 SPI 라이브러리는 Master 위주로 설계되어 있기 때문에 Slave는 거의 AVR 코딩하듯이 해야 하기 때문에 그렇다.

그럼 Master 코드를 살펴보자.

먼저 SPI 라이브러리를 등록하고 Setup에서 통신을 초기화 해준다.

그리고 Slave가 연결된 SS 핀을 초기화 해준다.

Slave를 선택할 땐 LOW 값을 출력해서 선택해주게 되는데 지금은 HIGH 값을 줘서 초기화한다.

다음으론 SPI 통신 속도를 SPI_CLOCK_DIV16으로 해서 분주비를 16으로 설정해따.

loop로 넘어가면 이제 본격적으로 데이터를 전송하는 코드가 나온다.

먼저 Slave를 선택하기 위해 SS핀에 LOW를 출력한다.

그리고 Hello, World라는 문자열을 SPI.transfer 함수를 통해 전송했다.

이때 SPI 통신도 I2C 통신과 마찬가지로 기본적으로 한 번에 8비트씩 전송하게 된다.

그래서 한 번에 한 글자씩 전송하게 된다.

이렇게 전송이 끝나면 다시 Slave의 선택을 해제한다.

여기서 중요한 것은 SS핀의 동작이다.

먼저 SS를 LOW 놓으면 Slave 쪽에선 준비를 하게 되고 다시 SS를 HIGH로 변경하면 통신이 끝났다라는 신호를 주는 것이다.

그래서 SS를 잘 다루는 것이 중요하다.

이번에는 Slave의 코드를 볼 것인데 꽤 난이도가 있다.
(Slave 코드는 간단히만 이야기할 것이고 이와 같은 내용은 후에 AVR 게시판에서 자세히 다룰 것이다.)

SPI 라이브러리가 Slave 쪽을 제대로 지원하지 않아서 레지스터를 직접 제어하고 있고 여기에 인터럽트(Interrupt) 기능을 사용하고 있기 때문이다.

여기서 인터럽트는 실행 중인 프로그램을 일시 중단하고 다른 프로그램을 끼워 넣어 먼저 실행 시키는 것인데 정확히는 인터럽트 요인이 되는 조건이 생겼을 때 실행 중인 프로그램을 중단하여 강제적으로 다른 프로그램을 실행시키는 것이다.

예를들어 집에서 TV를 보고 있다고 하자.

그때 누군가 초인종을 누르면 TV 보는 것을 중단하고 문으로 갔다가 다시 TV 앞으로 와서 TV를 볼 것이다.

이것을 인터럽트와 연결해보면 실행 중인 프로그램이 TV를 보고 있는 것이고 특정 조건이라는 것이 초인종을 누르는 것이다.

그리고 초인종에 관한 일, 문으로 갔다가 일이 끝나면 다시 돌아와서 이전에 하던 일인 TV 보는 것을 이어서 하는 것이다.

이 인터럽트는 AVR 코딩에서는 상당히 많이 사용되는 기능이지만 아두이노에서는 그렇지 않다.

아니 우리가 직접 할 일이 없다는 것이 맞을 것이다.

대부분의 인터럽트가 필요한 일은 라이브러리에 다 탑재되어 있기 때문에 우리가 볼 일이 없다.

그래서 라이브러리를 무분별하게 사용하면 이 인터럽트들이 충돌해서 코드가 정상동작 하지 않는 일도 발생한다.

이건 인터럽트 사용이 꽤 어렵기 때문에 아두이노에선 라이브러리에 대부분 포함되어 있고 아두이노에서도 인터럽트 사용을 위한 자체 함수를 별로 제공하지 않는다.

그럼 다시 돌아가서 코드를 살펴보자.

코드의 앞부분은 수신된 문자를 저장하기 위해 변수를 만들었고 이어서 전송받을 문자열의 기록을 위해 pos 변수와 process_it 변수를 만들었다.

이제 setup과 loop를 살펴보자.

여기서부턴 레지스터를 사용하게 된다.

SPI 통신에서 사용하는 레지스터는 세가지 설정이 필요하다.

먼저 SPI 통신을 사용가능하도록 하고 Slave 동작인 Master로부터 데이터가 수신된 경우 인터럽트가 발생하도록 하는 레지스터 설정을 한다.

이 세가지 레지스터는 모드 SPI 레지스터인 SPCR(SPU Control Register)의 해당 비트를 설정해 줌으로써 가능하다.

위의 그림이 SPCR 레지스터의 구조를 나타낸 것이다.

여기서 SPIE, SPE, MSTR 비트를 설정하여 통신을 한다.

SPE 비트를 1로 설정하면 SPI 통신이 가능하게 하고, MSTR 비트를 1로 설정하면 Master 모드로 작동한다.

물론 0으로 설정하면 Slave 모드로 동작한다.

마지막으로 SPIE 비트를 1로 설정하면 SPI 통신에 의한 이터럽트 발생을 허용하게 하는 것이다.

다시 본론으로 돌아와서 Slave 코드를 살펴보면 _BV라는 함수가 많이 사용되어 있는데 이것은 함수라고 불리기 보단 매크로라고 많이 부르는 것이다.

_BV(bit)로 사용하는데 bit에 들어있는 비트만 1을 설정하고 나머지 비트는 0으로 설정하는 것이다.

이것을 통해 레지스터에 원하는 부분을 동작시키고 있는 것이다.

이것 말고는 코드의 주석과 대조해보면 대부분 이해할 수 있을 것이다.

뭐 한 가지 추가하자면 loop 앞에 있는 인터럽트 서비스 루틴(ISR, Interrupt Service Routine)이 있다.

이 ISR은 인터럽트 조건이 달성됬을 때 자동으로 호출되는 것으로 여기서는 ISR(SPI_STC_vect)로 되어 SPI 통신을 통해 데이터를 수신하면 자동으로 호출되게 되어 있다.

그리고 안에는 인터럽트 호출시 동작하게 될 코드들이 담겨 있다.

여기까지가 보드 간의 SPI 통신에 대해 이야기하였다.

Master에 해당하는 곳은 라이브러리를 이용해서 쉽게 코드를 작성하고 이해하기도 쉽지만 Slave 쪽은 상당히 코드가 복잡하고 어렵다.

하지만 앞서 이야기한 것과 같이 굳이 지금은 이 부분을 알 필요는 없다.

보드간의 SPI 통신 코드를 작성하는 경우는 거의 없고 대부분의 센서나 모듈들은 이미 해당 센서들의 라이브러이 코드가 다 들어가 있는 상태이기 때문이다.

그렇기 때문에 우리는 Master 입장에서만 코드를 작성하면 되고 우리가 알 것은 연결 한 SS 핀이나 모듈 또는 센서의 데이터시트를 참고하여 통신 속도, 전송 모드들을 알아서 코드를 작성하면 된다.

그래서 결론을 이야기하자면 SPI 통신은 많이 사용되는 통신이긴 하지만 보드 간의 통신을 직접해야 하는 경우라면 I2C나 UART 통신을 사용하는 것을 더 추천한다.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
error: Content is protected !!