Embedded System SPI

Wonho Kim·2025년 1월 1일

Embedded System

목록 보기
5/7

시리얼 통신

기존의 GPIO 핀으로는 1 bit 데이터 송/수신만 가능하다는 한계점이 있다.

따라서 여러 비트의 데이터를 전송하기 위해서는 통신 프로토콜을 활용해야 한다.

병렬 통신: 여러 개의 핀을 사용하여 한번에 여러 비트의 데이터를 전송
시리얼 통신: 하나의 핀을 활용하여 여러 비트의 데이터를 순차적으로 전송

병렬 통신은 구현이 어렵고 비용이 크며, 클럭이 높을수록 전파지연/노이즈/타이밍 틀어짐 문제가 발생하고, 반이중 통신 기법만 사용할 수 있다.

반면 시리얼(직렬) 통신은 구현이 쉽고 비용이 상대적으로 작으며, 전파지연/노이즈 등의 문제가 발생하지 않고, 전이중 통신 기법도 사용할 수 있다.

시리얼 통신 방식 종류

SPI 통신

Serial Peripheral Interface의 약어이며 아래와 같은 특징을 가진다.

전이중 통신 방식

  • 송/수신 동시에 가능
  • MOSI: Master Out Slave In
  • MISO: Master In Slave Out

일대다(1:n) 통신 지원

  • 마스터-슬레이브 구조
  • SS(Slave Select) or CS(Chip Select) 신호를 활용하여 슬레이브를 선택
    => 하드웨어적 기법
  • 슬레이브 당 SS 선 할당
  • 모든 슬레이브는 MOSI, MISO 핀 공유
  • SS신호로 선택된 슬레이브만 데이터 송/수신

동기 방식

  • 클럭 신호를 공유함

아래는 Master-Slave 연결이 1:2로 구성된 방식이다.

데이터의 송/수신 과정이 동기식이므로 클럭에 맞춰서 Master-Slave 간 통신이 진행된다.

위 그림을 SPI에 맞게 구성하면 아래와 같은 그림이 되는데, MOSI는 Master입장에서 출력, Slave입장에서 입력이 되므로 먼저 데이터를 송신하게 되고, MISO는 반대이므로 데이터를 수신받은 후에 진행하게 된다.

동기식이니 클럭에 맞춰 데이터 샘플링을 하게 되는데, 어떤 시점에서 샘플링을 하게될 것인가?

위 질문에 대한 답으로 아래와 같이 4가지 경우가 나온다.

CPOL: idle high or idle low를 결정

idle(유휴 상태)가 클럭 신호보다 high(높은)하다는 의미이므로 하강하는 파형을 선택하는 것을 의미한다. 이 경우 1로 설정한다.

idle(유휴 상태)가 클럭 신호보다 low(낮은)하다는 의미이므로 상승하는 파형을 선택하는 것을 의미한다. 이 경우 0으로 설정한다.

CPHA: 상승 에지 or 하강 에지를 결정

첫 번째로 만나는 에지일 경우 0, 두 번째로 만나는 에지일 경우 1로 설정한다.

여기서 우리가 사용하는 가속도 센서(ADXL345)는 CPOL=1, CPHA=1을 사용하게 된다. 따라서 그림 속의 Mode 3에 맞춰서 클럭 신호가 동작하고, 데이터 샘플링을 진행하게 된다.

SPI 통신 예제

우리는 SPI 통신을 가속도 센서(ADXL345)를 통해 실험을 진행할 것이다.

가속도 센서란 3축의 가속도를 측정하는 도구로 설명은 아래와 같다.

동작 전압: 3.3V ~ 5V
입력 전압: 1.7V ~ Vs
SPI/I2C 통신 지원
최대 13bit의 해상도 지원

또한 가속도 센서의 x, y, z축을 측정하는 단위 값은 Roll, Pitch, Yaw라고 한다.

아래 그림과 설명은 가속도 센서에 달린 핀에 대한 설명이다. 어떻게 구성되어있는지 나와있는 설명이니 읽어보면 될듯하다.

라즈베리 파이와 ADXL-345를 연결하기 위해서는 아래와 같이 회로를 구성하면 된다.

해당 예제에서는 라즈베리파이가 Master, ADXL-345가 Slave로 연결하게 되는 것이다.

가속도 센서의 데이터 쓰기 동작

SPI 개념 중 가장 핵심이 되면서 어려운 내용인데, 하나씩 뜯어보면서 살펴보도록 하자.

  1. 칩 선택(CS)
    SPI 통신을 시작하기 위해 CS의 신호를 LOW로 설정한다. 해당 신호가 낮으면 ADXL345가 마스터장치(라즈베리파이)와 통신할 준비가 되었음을 알리는 신호이다.

  2. 클럭 신호(SCLK)
    데이터 전송 속도를 제어한다. 클럭의 주기와 파형에 맞추어 데이터 전송이 동기화된다.

  3. 데이터 입력(SDI)
    마스터장치(라즈베리파이)에서 보내는 데이터가 SDI 라인을 통해 ADXL345로 전송된다. 데이터는 8비트 단위로 전송되며, 각 비트는 SCLK 신호의 상승과 하강에 맞춰 하나씩 전달된다.

    • W/Bar: 쓰기/읽기 비트
      전송할 데이터가 읽기인지 쓰기 작업인지 나타낸다. W/Bar 이므로 비트가 0일 경우 쓰기 작업을 의미한다.
    • MB: 멀티바이트 비트
      여러 바이트의 데이터를 한꺼번에 전송할지 여부를 결정한다. 1이면 여러 바이트를 연속적으로 보내고, 0이면 단일 바이트만 전송한다.
    • A5 ~ A0: 주소 비트
      6비트로 이루어진 레지스터 주소를 나타낸다. ADXL345의 특정 레지스터를 가리키며, 레지스터에 데이터를 쓸 때 또는 읽을 때 해당 주소가 사용된다.
    • D7 ~ D0: 데이터 비트
      8비트로 이루어진 데이터이다. 우리가 선택한 레지스터에 쓰거나 해당 레지스터에서 읽어오는 값을 의미한다.
      만약 MB가 1로 설정되어 있으면 첫 번째 바이트가 전송된 후 이어서 다음 바이트들이 전송되며, 해당 데이터는 연속적인 레지스터들에 기록된다.
  4. 데이터 처리
    ADXL345는 받은 데이터를 처리하며, 원하는 레지스터에 데이터를 저장하거나 명령을 실행한다.

  5. 통신 종료
    데이터 전송이 완료되면 CS 신호를 HIGH로 설정하여 통신을 종료한다.

가속도 센서의 데이터 읽기 동작

  1. 칩 선택(CS) 활성화
    CS 신호를 LOW로 설정하여 ADXL345와 통신할 준비가 되었음을 알린다.

  2. 데이터 준비 및 클럭 신호 (SCLK)
    클럭 신호를 통해 데이터 전송 타이밍을 제어한다. 해당 부분 역시 데이터 쓰기 동작과 동일한 설명이다.

  3. 읽기 명령 전송 (SDI)
    W/Bar 비트가 이제 읽기 동작이므로 1로 설정한다. 다음으로 읽고자 하는 레지스터의 주소를 6비트로 전송한다. 이 때 MB가 1로 설정되면 연속해서 여러 바이트를 읽을 수 있다.

  4. 데이터 전송 (SDO)
    준비된 데이터를 ADXL345가 SDO 라인을 통해 마스터 장치로 보낸다. 이 때 전송되는 데이터는 X, Y, Z 축에 대한 정보(DATAX0~DATAZ1)로 구성된다.

  5. 데이터 수신
    마스터 장치는 SDO를 통해 전달된 데이터를 받아 MSB와 LSB를 결합하여 16비트의 완전한 데이터를 구성한다.
    (우리가 예제 코드에서 직접 구하는 부분이 됨)

  6. 통신 종료
    데이터 전송이 완료되면 CS 신호를 다시 HIGH로 설정하여 통신을 종료한다.

☃️ 레지스터 주소가 6bit인 이유?
바로 밑에서 설명할 레지스터 주소 맵을 보면 0x00 부터 0x39까지 최대 58개의 주소 범위를 가지는데, 2^6 = 64개면 충분히 표현할 수 있기 때문에 6bit를 사용한다.

가속도 센서의 레지스터맵

레지스터를 통해서 센서의 다양한 설정들을 변경할 수 있는 기능들을 제공한다. 아래 사진은 레지스터 구성도이다.

여기서 우리가 확인해야할 주요 레지스터는 DATA_FORMAT(0x31), POWER_CTL(0x2D), 그리고 DATAX0 ~ DATAZ1(0x32 ~ 0x37)이다.

DATA_FORMAT(0x31)

데이터 포맷 레지스터는 가속도계의 데이터 형식을 설정하는 역할을 한다. 8비트로 구성되어 있으며 각 비트는 다음과 같은 기능을 가진다.

SELF_TEST(D7): 1로 설정되면 센서에 자체 테스트를 적용하여 출력 데이터에 변화를 일으킴. 0이면 비활성화

SPI 모드 (D6): 1이면 3-Wire SPI 모드, 0이면 4-Wire SPI 모드

INT_INVERT (D5): 0이면 인터럽트가 high일 때 활성화, 1이면 인터럽트가 low일 때 활성화

JUSTIFY (D2): 출력 데이터의 정렬 방식 결정함. 1이면 데이터가 왼쪽 정렬(최상위 비트 기준), 0이면 오른쪽 정렬(부호 확장 포함)된다.

Range 비트 (D1 ~ D0): g(gravity) 범위를 설정한다. 범위는 ±2g, ±4g, ±8g, ±16g로 설정할 수 있으며, 설정에 따라 측정 가능한 최대 가속도가 달라진다.

POWER_CTL(0x2D)

해당 센서에 전압이 인가되면 기본적으로 Standby 모드로 진입한다. 측정을 시작하기 위해서는 POWER_CTL 레지스터의 D3번 비트를 1로 설정해야한다.

DATAX0 ~ DATAZ1(0x32 ~ 0x37)

X, Y, Z축의 값이 저장되는 레지스터이다. ADXL345는 최대 13비트 해상도로 가속도를 측정할 수 있게 만들었다고 설명하였기 때문에 이를 표현하기 위해 8비트를 2개로 구성하여 총 16비트가 필요하기 때문에 X0, X1, Y0, Y1, Z0, Z1이 나오게 되었다.

각 축마다 0은 하위 8 bit, 1은 상위 8 bit를 저장한다. (e.g. X0 LSB, X1 MSB...)

wiringPi의 SPI 라이브러리

SPI Setup 함수

int wiringPiSPISetupMode(int channel, int speed, int mode)

channel: SPI 채널 설정
speed: 클럭 스피드 설정 (단위 Hz)
mode: SPI 모드 설정 (0~3)

SPI 읽고 쓰기 함수

int wiringPISPIDataRW(int channel, unsigned char *data, int len)

data: 송/수신할 데이터 셋의 주소
len: 송/수신할 데이터 길이

wiringPI를 활용한 예제코드

#include <stdio.h>
#include <wiringPi.h>
#include <wiringPiSPI.h>
#define CS_GPIO 8 //CS 핀 설정
#define SPI_CH 0
#define SPI_SPEED 1000000 // 1MHz
#define SPI_MODE 3 // CPOL=1, CPHA=1이므로 Mode 3
#define BW_RATE 0x2C //통신 속도 설정
#define POWER_CTL 0x2D //Power Control Register

#define DATA_FORMAT 0x31
#define DATAX0 0x32 //X-Axis Data 0
#define DATAX1 0x33 //X-Axis Data 1
#define DATAY0 0x34 //Y-Axis Data 0
#define DATAY1 0x35 //Y-Axis Data 1
#define DATAZ0 0x36 //Z-Axis Data 0
#define DATAZ1 0x37 //Z-Axis Data 1

//ADXL345에서 값을 읽는 함수
void readRegister_ADXL345(char registerAddress, int numBytes, char *values
{
	//read 1 기능을 시작하기 위해서는 주소의 최상위 비트가 1로 설정되어야함.
    values[0] = 0x80 | registerAddress;
    //멀티 바이트 읽기를 사용하는 경우 6번 비트도 1로 설정
    if(numBytes > 1) values[0] = values[0] | 0x40;
    
    digitalWrite(CS_GPIO, LOW); // Low : CS Active
    //데이터를 읽고 values에 저장
    wiringPiSPIDataRW(SPI_CH, values, numBytes + 1);
    digitalWrite(CS_GPIO, HIGH); // High : CS Inactive
}

//ADXL345에 값을 쓰는 함수
void writeRegister_ADXL345(char address, char value)
{
	unsigned char buff[2];
    buff[0] = address;
    buff[1] = value;
    
    digitalWrite(CS_GPIO, LOW); // Low : CS Active
    wiringPiSPIDataRW(SPI_CH, buff, 2);
    digitalWrite(CS_GPIO, HIGH); // High : CS Inactive
}

int main(void)
{
	unsigned char buffer[100];
    short x, y=0, z=0;
    if(wiringPiSetupGpio() == -1) return 1;
    if(wiringPiSPISetupMode(SPI_CH, SPI_SPEED, SPI_MODE) == -1) return 1;
    
    pinMode(CS_GPIO, OUTPUT); //Chip Select로 사용할 핀은 OUTPUT으로 설정
    digitalWrite(CS_GPIO, HIGH); //IDLE 상태 유지
    
    //범위 설정 +- 4G
    writeRegister_ADXL345(DATA_FORMAT, 0x01);
    //Output Data Rate 400 Hz
    writeRegister_ADXL345(BW_RATE, 0x0C);
    //측정모드
    writeRegister_ADXL345(POWER_CTL, 0x08);
    
    while(1)
    {
		readRegister_ADXL345(DATAX0, 6, buffer); //데이터 수신
        x = ((short)buffer[2] << 8) | (short)buffer[1];
        y = ((short)buffer[4] << 8) | (short)buffer[3];
        z = ((short)buffer[6] << 8) | (short)buffer[5];
        printf("%d %d %d\n", x, y, z);
        delay(50);
    }
    return 0;
}

실습1, 2 코드

실습1에서는 위 예제 코드를 아래의 조건에 맞게 변경하였다.

  1. Range ±4g로 설정
  2. 최대 Resolution이 되도록 설정 변경
  3. 물리량 형태로 변경 (단위 g(g=9.81m/s^2)
  4. 가속도 센서 측정값을 토대로 기울임 각도 측정

실습 2에서는 Roll 값에 따라 LED 밝기를 조절하는 코드이다.

  1. LED0 (PWM0)
    Roll 값이 양의 값일 때 (0 ~ 90) 작동
    Roll 값의 크기에 따라 LED 밝기 변경
    Roll = 0: LED0 꺼짐
    Roll = 90: LED0 최대 밝기

  2. LED1 (PWM1)
    Roll 값이 음의 값일 때 (0 ~ 90) 작동
    Roll의 절대값 크기에 따라 LED 밝기 변경
    Roll = 0: LED1 꺼짐
    Roll = 90: LED1 최대 밝기

#include <stdio.h>
#include <wiringPi.h>
#include <wiringPiSPI.h>
#include <softPwm.h>
#include <math.h>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

#define CS_GPIO   8 //CS 핀 설정
#define SPI_CH    0
#define SPI_SPEED 1000000 // 1MHz
#define SPI_MODE  3

#define BW_RATE   0x2C //통신 속도 설정
#define POWER_CTL 0x2D //Power Control Register

#define DATA_FORMAT 0x31
#define DATAX0      0x32 //X-Axis Data 0
#define DATAX1      0x33 //X-Axis Data 1
#define DATAY0      0x34 //Y-Axis Data 0
#define DATAY1      0x35 //Y-Axis Data 1
#define DATAZ0      0x36 //Z-Axis Data 0
#define DATAZ1      0x37 //Z-Axis Data 1

#define LED0 13
#define LED1 18


//ADXL345 에서 값을 읽는 함수
void readRegister_ADXL345(char registerAddress, int numBytes, char * values) {
    //read 기능을 시작하기 위해서는 주소의 최상위 비트가 1로 설정되어야 함
    values[0] = 0x80 | registerAddress;

    //멀티 바이트 읽기를 사용하는 경우, 6번 비트도 1로 설정
    if(numBytes > 1) values[0] = values[0] | 0x40;
    
    digitalWrite(CS_GPIO, LOW); // Low : CS Active
    
    //데이터를 읽고 values에 저장
    wiringPiSPIDataRW(SPI_CH, values, numBytes + 1);
    digitalWrite(CS_GPIO, HIGH); // High : CS Inactive
}

//ADXL345에 값을 쓰는 함수
void writeRegister_ADXL345(char address, char value) {
    unsigned char buff[2];
    buff[0] = address;
    buff[1] = value;

    digitalWrite(CS_GPIO, LOW); // Low : CS Active
    wiringPiSPIDataRW(SPI_CH, buff, 2);
    digitalWrite(CS_GPIO, HIGH); // High : CS Inactive
}

int main (void) {
    unsigned char buffer[100];
    short x, y= 0 , z= 0;

    float x_g, y_g, z_g;
    float roll, pitch;

    int LED0_value, LED1_value;

    const float scaleFactor = 0.0038; // 1 LSB = 0.00098g for ±4g range with 13-bit resolution

    if(wiringPiSetupGpio() == -1) return 1;
    if(wiringPiSPISetupMode(SPI_CH, SPI_SPEED,SPI_MODE) == -1) return 1;

    pinMode(CS_GPIO, OUTPUT); //Chip Select로 사용할 핀은 OUTPUT 으로 설정
    digitalWrite(CS_GPIO, HIGH); //IDLE 상태로 유지

    writeRegister_ADXL345(DATA_FORMAT, 0x09); //범위 설정 +- 4G, 풀 해상도 (13비트)
    writeRegister_ADXL345(BW_RATE,     0x0C); //Output Data Rate 400 Hz
    writeRegister_ADXL345(POWER_CTL,   0x08); //측정 모드

    pinMode(LED0, PWM_OUTPUT);
    pinMode(LED1, PWM_OUTPUT);

    pwmSetMode(PWM_MODE_MS);
    pwmSetRange(90);
    pwmSetClock(96);

    while(1) {
        readRegister_ADXL345(DATAX0,6,buffer); //데이터 수신
        x = ((short)buffer[2]<<8)|(short)buffer[1]; //X축 값 반환
        y = ((short)buffer[4]<<8)|(short)buffer[3]; //Y축 값 반환
        z = ((short)buffer[6]<<8)|(short)buffer[5]; //Z축 값 반환

        x_g = x * scaleFactor;
        y_g = y * scaleFactor;
        z_g = z * scaleFactor;

        roll = atan2(y_g, sqrt(x_g * x_g + z_g * z_g)) * 180 / M_PI;
        pitch = atan2(-x_g, sqrt(y_g * y_g + z_g * z_g)) * 180 / M_PI;

        // printf("X: %4d  Y: %4d  Z: %4d\n", x,y,z); //X, Y, Z 축 출력
        printf("X: %8.3f g   Y: %8.3f g   Z: %8.3f g  |  Roll: %7.2f°   Pitch: %7.2f°\n", x_g, y_g, z_g, roll, pitch); //X, Y, Z 축 출력
  
        if (roll >= 0 && roll <= 90) {
            LED0_value = (int)roll;
            LED1_value = 0;
        }
        else if (roll <= 0 && roll >= -90) {
            LED0_value = 0;
            LED1_value = -(int)roll;
        }
        else {
            LED0_value = 0;
            LED1_value = 0;
        }

        pwmWrite(LED0, LED0_value);
        pwmWrite(LED1, LED1_value);

        delay(50);
    }
    return 0;
}
profile
새싹 백엔드 개발자

0개의 댓글