얼땅불땅 2026. 7. 19. 04:35

저번엔 I2C를 정리했으니 이번엔 SPI를 정리해본다. 둘이 사촌지간 같은 녀석들이라 I2C 알고 나면 SPI는 오히려 쉽다.

라고 나도 처음엔 그렇게 생각했다 ㅋㅋ

용어적 설명부터

SPI(Serial Peripheral Interface)는 모토로라에서 개발한 동기식 직렬 통신 방식이다. 마이크로컨트롤러와 주변 기기(센서, EEPROM, SD카드, LCD 등) 사이에서 짧은 거리 고속 통신에 널리 쓰인다. "에스-피-아이" 라고 그냥 읽으면 된다. 이건 발음 시비 걸릴 게 없어서 마음이 편하다 ^^

SPI는 기본적으로 4개의 라인을 사용한다.

  • SCLK (SCK) : 클럭. 마스터가 만들어서 내보낸다.
  • MOSI : Master Out Slave In. 마스터 → 슬레이브 방향 데이터.
  • MISO : Master In Slave Out. 슬레이브 → 마스터 방향 데이터.
  • SS (CS) : Slave Select(Chip Select). 마스터가 "너랑 얘기할래" 하고 슬레이브를 지목하는 선. 보통 LOW일 때 선택(active low)이다.

SPI 4선 결선도. 클럭은 마스터가 만들고, MOSI/MISO는 마스터 기준 이름이다.

여기서 이름 헷갈리지 말자. MOSI, MISO는 마스터 기준이다. 마스터든 슬레이브든 칩에 적힌 핀 이름은 똑같이 MOSI/MISO라서, "마스터의 MOSI ↔ 슬레이브의 MOSI" 이렇게 그냥 같은 이름끼리 이어주면 된다. (몇몇 칩은 SDI/SDO로 적혀있기도 한데 그건 또 반대로 이어야 해서… 처음에 여기서 배선 한 번 틀렸다 ㅡㅡ;;;)

그래서 I2C랑 뭐가 다른데?

I2C를 먼저 봤으니 비교로 이해하는 게 제일 빠르다.

  I2C SPI
선 개수 2선 (SDA, SCL) 4선 (SCK, MOSI, MISO, SS)
슬레이브 구분 주소(7비트) SS선을 따로 하나씩
통신 방향 반이중 (한 선으로 주고받음) 전이중 (MOSI/MISO 동시에)
속도 100k ~ 3.4Mbit/s 보통 수~수십 MHz, 더 빠름
START/STOP, ACK 있음 없음 (그냥 SS로 시작/끝)

즉 SPI는 주소 개념이 없다. 슬레이브가 여러 개면 SS선을 슬레이브 수만큼 따로 뽑아서 "이번엔 너!" 하고 하나만 LOW로 내려주는 방식이다. 그래서 슬레이브가 많아지면 선이 늘어난다는 단점이 있다. 대신 START/STOP이나 ACK 같은 규칙이 없어서 프로토콜 자체는 훨씬 단순하다.

데이터 전송은 어떻게?

SPI는 START/STOP이 없다. 그럼 언제 시작하고 끝나느냐?

  • 시작 : 마스터가 SS를 LOW로 내리면 그 슬레이브랑 통신 시작.
  • : 다시 SS를 HIGH로 올리면 끝.

이게 I2C의 START/STOP 역할을 대신한다. 훨씬 직관적이다.

데이터는 마스터가 만드는 클럭(SCK)에 맞춰 한 비트씩 흘러간다. 핵심은 전이중이라는 거다. 마스터가 MOSI로 1비트 내보낼 때, 동시에 MISO로 슬레이브가 보낸 1비트가 들어온다. 즉 보내기와 받기가 한 번에 일어난다. (I2C는 쓸 때 쓰고 읽을 때 읽었지만 SPI는 동시다.)

SCK 8펄스면 마스터와 슬레이브의 바이트가 통째로 맞교환된다.

 

여기서 처음에 나를 괴롭혔던 게 CPOL / CPHA 라는 놈이다.

  • CPOL (클럭 극성) : 통신 안 할 때(idle) SCK가 LOW냐(0) HIGH냐(1).
  • CPHA (클럭 위상) : 데이터를 클럭의 앞쪽 엣지에서 읽냐(0) 뒷쪽 엣지에서 읽냐(1).

이 둘 조합으로 모드 0~3이 나온다. 뭐 복잡해 보이지만, 실무에선 데이터시트에 "이 칩은 SPI 모드 0" 이런 식으로 적혀 있으니 그거 맞춰주면 된다. 제일 흔한 게 모드 0 (CPOL=0, CPHA=0)이다.

  • 모드 0 기준: 평소 SCK는 LOW, SCK가 올라가는(상승) 엣지에서 데이터를 읽는다. MOSI/MISO 값은 SCK가 내려가 있을 때 바꿔놓는다.

모드 0: SS를 내리고, SCK 상승 엣지(↑)마다 데이터를 읽는다. (● = 읽는 순간)

말로 하면 헷갈리니까 그냥 코드로 보자. (이게 마음이 편하다.)

코드로 보자 (비트뱅잉, 모드 0 기준)

I2C 때처럼 PORTC 핀을 직접 두드리는 방식이다. 핀 배치는 예시니까 본인 보드에 맞게 바꾸면 된다.

#define MOSI        PORTC_Bit4   // 마스터가 내보내는 데이터
#define MISO_IN     PINC_Bit5    // 슬레이브가 보낸 데이터 (입력)
#define SCK         PORTC_Bit6   // 클럭 (마스터가 생성)
#define SS          PORTC_Bit7   // 슬레이브 선택 (active low)

// 슬레이브 선택 : SS를 LOW로
void SPI_Start(void)
{
    DDRC = 0xDC;    // MOSI, SCK, SS = 출력 / MISO = 입력
    SCK = 0;        // 모드 0 : idle 클럭은 LOW
    SS  = 0;        // 슬레이브 선택 시작
    USEC_Delay(1);
}

// 슬레이브 선택 해제 : SS를 HIGH로
void SPI_Stop(void)
{
    SS = 1;         // 통신 끝
    USEC_Delay(1);
}

이제 핵심인 데이터 주고받기다. SPI는 전이중이라 보내면서 동시에 받는다. 그래서 함수 하나로 끝난다. val을 내보내면서, 그 사이에 들어온 값을 리턴한다.

// 1바이트 전송 + 동시에 1바이트 수신 (MSB first, 모드 0)
BYTE SPI_Transfer(BYTE val)
{
    BYTE i;
    BYTE recv = 0;

    for(i = 0x80; i > 0; i /= 2)   // MSB부터 8비트
    {
        // 1) SCK가 LOW인 동안 MOSI에 보낼 비트를 올려둔다
        if (i & val) MOSI = 1;
        else         MOSI = 0;
        USEC_Delay(1);

        // 2) SCK 상승 엣지 : 이 순간 서로 데이터를 읽는다
        SCK = 1;
        if (MISO_IN) recv = (recv | i);   // 들어온 비트를 저장
        USEC_Delay(1);

        // 3) SCK 하강 : 다음 비트 준비
        SCK = 0;
        USEC_Delay(1);
    }

    return recv;   // 나가는 동안 들어온 8비트
}

I2C와 비교해보면 재밌다. I2C는 I2CWrite_Data(), I2CRead_Data()가 따로 있었는데, SPI는 SPI_Transfer() 하나가 쓰기/읽기를 겸한다. "보내는 만큼 받는다"가 SPI의 성격이다.

  • 쓰기만 하고 싶다 → 리턴값 무시하면 된다.
  • 읽기만 하고 싶다 → 아무 값(보통 0x00, 더미 바이트)이나 보내면서 리턴값을 받으면 된다. 어차피 클럭을 굴려야 슬레이브가 데이터를 실어주니까.

전체 흐름 예시

예를 들어 어떤 SPI EEPROM에서 데이터를 하나 읽는다고 해보자. (명령 → 주소 → 더미 보내며 읽기)

void main(void)
{
    BYTE data;

    SPI_Start();              // SS LOW : 슬레이브 선택

    SPI_Transfer(0x03);       // READ 명령 (예시)
    SPI_Transfer(0x00);       // 읽을 주소 상위
    SPI_Transfer(0x10);       // 읽을 주소 하위

    data = SPI_Transfer(0x00); // 더미(0x00) 보내면서 실제 데이터 수신

    SPI_Stop();               // SS HIGH : 선택 해제
}

포인트는 마지막 SPI_Transfer(0x00)이다. 보낼 데이터는 없지만 클럭을 굴려야 슬레이브가 MISO로 값을 실어주기 때문에 더미 바이트를 하나 흘려보내는 거다. 이게 처음엔 "왜 굳이 0을 보내?" 싶었는데, 전이중이라 클럭이 있어야만 데이터가 나온다는 걸 생각하면 납득이 된다.

정리하면

  • SPI = 4선(SCK, MOSI, MISO, SS), 마스터가 클럭을 만든다.
  • 슬레이브 구분은 주소가 아니라 SS선. LOW로 내리면 선택, HIGH로 올리면 끝.
  • 전이중 — 보내면서 동시에 받는다. 그래서 함수 하나(SPI_Transfer)로 끝.
  • CPOL/CPHA(모드)는 데이터시트 보고 맞춰주면 된다. 흔한 건 모드 0.

I2C보다 선은 많지만 프로토콜은 오히려 단순하다. START/STOP/ACK 같은 거 신경 안 쓰고 SS만 잘 내렸다 올리면 되니까. 다음엔 SPI 모드(CPOL/CPHA) 4개를 파형으로 하나씩 뜯어보든가 해야겠다.

반응형