기존의 GPIO 핀으로는 1 bit 데이터 송/수신만 가능하다는 한계점이 있다.
따라서 여러 비트의 데이터를 전송하기 위해서는 통신 프로토콜을 활용해야 한다.
병렬 통신: 여러 개의 핀을 사용하여 한번에 여러 비트의 데이터를 전송
시리얼 통신: 하나의 핀을 활용하여 여러 비트의 데이터를 순차적으로 전송

병렬 통신은 구현이 어렵고 비용이 크며, 클럭이 높을수록 전파지연/노이즈/타이밍 틀어짐 문제가 발생하고, 반이중 통신 기법만 사용할 수 있다.
반면 시리얼(직렬) 통신은 구현이 쉽고 비용이 상대적으로 작으며, 전파지연/노이즈 등의 문제가 발생하지 않고, 전이중 통신 기법도 사용할 수 있다.

Serial Peripheral Interface의 약어이며 아래와 같은 특징을 가진다.
전이중 통신 방식
일대다(1:n) 통신 지원
동기 방식
아래는 Master-Slave 연결이 1:2로 구성된 방식이다.

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

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

동기식이니 클럭에 맞춰 데이터 샘플링을 하게 되는데, 어떤 시점에서 샘플링을 하게될 것인가?
위 질문에 대한 답으로 아래와 같이 4가지 경우가 나온다.

idle(유휴 상태)가 클럭 신호보다 high(높은)하다는 의미이므로 하강하는 파형을 선택하는 것을 의미한다. 이 경우 1로 설정한다.
idle(유휴 상태)가 클럭 신호보다 low(낮은)하다는 의미이므로 상승하는 파형을 선택하는 것을 의미한다. 이 경우 0으로 설정한다.
첫 번째로 만나는 에지일 경우 0, 두 번째로 만나는 에지일 경우 1로 설정한다.
여기서 우리가 사용하는 가속도 센서(ADXL345)는 CPOL=1, CPHA=1을 사용하게 된다. 따라서 그림 속의 Mode 3에 맞춰서 클럭 신호가 동작하고, 데이터 샘플링을 진행하게 된다.
우리는 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 개념 중 가장 핵심이 되면서 어려운 내용인데, 하나씩 뜯어보면서 살펴보도록 하자.
칩 선택(CS)
SPI 통신을 시작하기 위해 CS의 신호를 LOW로 설정한다. 해당 신호가 낮으면 ADXL345가 마스터장치(라즈베리파이)와 통신할 준비가 되었음을 알리는 신호이다.
클럭 신호(SCLK)
데이터 전송 속도를 제어한다. 클럭의 주기와 파형에 맞추어 데이터 전송이 동기화된다.
데이터 입력(SDI)
마스터장치(라즈베리파이)에서 보내는 데이터가 SDI 라인을 통해 ADXL345로 전송된다. 데이터는 8비트 단위로 전송되며, 각 비트는 SCLK 신호의 상승과 하강에 맞춰 하나씩 전달된다.
데이터 처리
ADXL345는 받은 데이터를 처리하며, 원하는 레지스터에 데이터를 저장하거나 명령을 실행한다.
통신 종료
데이터 전송이 완료되면 CS 신호를 HIGH로 설정하여 통신을 종료한다.

칩 선택(CS) 활성화
CS 신호를 LOW로 설정하여 ADXL345와 통신할 준비가 되었음을 알린다.
데이터 준비 및 클럭 신호 (SCLK)
클럭 신호를 통해 데이터 전송 타이밍을 제어한다. 해당 부분 역시 데이터 쓰기 동작과 동일한 설명이다.
읽기 명령 전송 (SDI)
W/Bar 비트가 이제 읽기 동작이므로 1로 설정한다. 다음으로 읽고자 하는 레지스터의 주소를 6비트로 전송한다. 이 때 MB가 1로 설정되면 연속해서 여러 바이트를 읽을 수 있다.
데이터 전송 (SDO)
준비된 데이터를 ADXL345가 SDO 라인을 통해 마스터 장치로 보낸다. 이 때 전송되는 데이터는 X, Y, Z 축에 대한 정보(DATAX0~DATAZ1)로 구성된다.
데이터 수신
마스터 장치는 SDO를 통해 전달된 데이터를 받아 MSB와 LSB를 결합하여 16비트의 완전한 데이터를 구성한다.
(우리가 예제 코드에서 직접 구하는 부분이 됨)
통신 종료
데이터 전송이 완료되면 CS 신호를 다시 HIGH로 설정하여 통신을 종료한다.
☃️ 레지스터 주소가 6bit인 이유?
바로 밑에서 설명할 레지스터 주소 맵을 보면 0x00 부터 0x39까지 최대 58개의 주소 범위를 가지는데, 2^6 = 64개면 충분히 표현할 수 있기 때문에 6bit를 사용한다.
레지스터를 통해서 센서의 다양한 설정들을 변경할 수 있는 기능들을 제공한다. 아래 사진은 레지스터 구성도이다.

여기서 우리가 확인해야할 주요 레지스터는 DATA_FORMAT(0x31), POWER_CTL(0x2D), 그리고 DATAX0 ~ DATAZ1(0x32 ~ 0x37)이다.
데이터 포맷 레지스터는 가속도계의 데이터 형식을 설정하는 역할을 한다. 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로 설정할 수 있으며, 설정에 따라 측정 가능한 최대 가속도가 달라진다.
해당 센서에 전압이 인가되면 기본적으로 Standby 모드로 진입한다. 측정을 시작하기 위해서는 POWER_CTL 레지스터의 D3번 비트를 1로 설정해야한다.

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...)

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: 송/수신할 데이터 길이
#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에서는 Roll 값에 따라 LED 밝기를 조절하는 코드이다.
LED0 (PWM0)
Roll 값이 양의 값일 때 (0 ~ 90) 작동
Roll 값의 크기에 따라 LED 밝기 변경
Roll = 0: LED0 꺼짐
Roll = 90: LED0 최대 밝기
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;
}