해당 글은 건국대학교 진현욱 교수님의 임베디드 시스템 소프트웨어 수업 내용을 정리한 글입니다.
시리얼 통신 프로토콜
우리는 I2C와 SPI 라는 두 가지 대표적인 시리얼 통신 프로토콜에 대해 설명을 하겠다.
Inter-Integrated Circuit
단거리, 2-wire serial communication protocol로서 low-speed peripheral(주변장치)를 연결하기 위해 사용되는 시리얼 컴퓨터 버스다.
2개의 물리적인 라인은 SCL과 SDA를 말하며 각각 아래와 같다.
- SCL (Serial Clock): 데이터 전송을 동기화하는 클록 신호가 흐른다.
- SDA (Serial Data): 실제 데이터가 전송되는 라인이다
모든 컴포넌트 사이에 2가지 관계가 존재하는데
- Master: SCL라인을 구동하며, 일반적으로 버스에 하나만 존재하여 신호를 주도한다.
- Slave: 마스터의 신호에 응답하며, 스스로 전송을 시작할 수 없다. 무조건 Master가 주도함
하드웨어 연결

라즈베리 파이에서 SCL, SDA를 위한 핀이 정해져 있다.
I2C 통신 프로토콜
이 버스에 연결된 모든 장치는 SCL과 SDA 라인의 상태를 상시 모니터링하여 아래 조건들을 감시한다.

Start는 SCL이 High일 때, SDA가 High에서 Low로 떨어지면 통신 시작을 알리는 것(1->0)
이 신호가 오면 통신을 시작하겠구나 하고 준비를 한다.
Start 조건을 만든 직후에는 반드시 SCL을 Low로 떨어뜨려 주어야 한다.

Stop는 SCL이 High일 때, SDA가 Low에서 High로 떨어지면 통신 시작을 알리는 것(0->1)
이 신호가 나오면 현재 진행 중이던 트랜잭션이 완전히 종료된다.
#include <stdio.h>
// 가상의 하드웨어 레지스터/핀 제어를 시뮬레이션하기 위한 변수
int SCL = 1;
int SDA = 1;
// 신호 안정화를 위한 미세 지연 함수
void i2c_dly(void) {
// 실제 임베디드 환경에서는 nop 명령어 방식이나 마이크로초(us) 단위의 delay를 사용합니다.
for (volatile int i = 0; i < 100; i++);
}
// [Start Sequence] 통신 시작 신호 생성
void i2c_start(void) {
SCL = 1; // SCL을 High로 설정 [cite: 122]
i2c_dly(); // [cite: 124]
SDA = 0; // SCL이 High인 상태에서 SDA를 Low로 떨어뜨림 (Start 조건 발생) [cite: 126]
i2c_dly(); // 인식 지연 [cite: 127]
SCL = 0; // 다음 데이터 비트 송신을 위해 SCL을 Low로 당김
i2c_dly(); // [cite: 135]
}
// [Stop Sequence] 통신 종료 신호 생성
void i2c_stop(void) {
SDA = 0; // Stop 조건을 만들기 위해 먼저 SDA를 Low로 설정 [cite: 128]
i2c_dly(); // [cite: 129]
SCL = 1; // SCL을 High로 설정 [cite: 131]
i2c_dly(); // [cite: 138]
SDA = 1; // SCL이 High인 상태에서 SDA를 High로 올림 (Stop 조건 발생) [cite: 139]
i2c_dly(); // [cite: 140]
}
int main(void) {
printf("I2C Start & Stop Sequence Simulation\n");
i2c_start();
printf("Start Condition Generated -> SCL: %d, SDA: %d\n", SCL, SDA);
i2c_stop();
printf("Stop Condition Generated -> SCL: %d, SDA: %d\n", SCL, SDA);
return 0;
}
data transfer

#include <stdio.h>
extern int SCL;
extern int SDA;
void i2c_dly(void);
// [Data Transmission] 1바이트 전송 및 ACK 확인 함수
void i2c_tx(unsigned char d) {
char x;
// 8비트 동안 반복 실행 (MSB인 D7부터 D0까지) [cite: 150, 151, 163]
for (x = 8; x > 0; x--) {
// 최상위 비트(0x80 = 1000 0000)가 1인지 0인지 검사하여 SDA에 대입 [cite: 164]
if (d & 0x80) {
SDA = 1; // [cite: 164]
} else {
SDA = 0; // [cite: 165]
}
i2c_dly(); // SDA 비트 안착을 위한 딜레이 추가 [cite: 172]
SCL = 1; // SCL을 High로 올려 슬레이브가 읽어가도록 함
i2c_dly(); // High 유지 딜레이 [cite: 172]
d <<= 1; // 데이터를 왼쪽으로 1비트 시프트하여 다음 비트 준비 [cite: 168]
SCL = 0; // SCL을 Low로 내려 다음 SDA 변동을 허용함
}
// 9번째 클록: ACK 비트 처리 구간
SDA = 1; // 마스터가 SDA 라인을 릴리즈(High)하여 슬레이브가 ACK를 줄 수 있게 함
i2c_dly();
SCL = 1; // 슬레이브의 ACK 상태를 읽기 위해 SCL을 High로 올림
i2c_dly(); // 수신 확인을 위한 비트 딜레이 [cite: 175]
// 실제 하드웨어라면 여기서 SDA 핀의 상태(0이 되었는지)를 읽어 ACK 여부를 리턴합니다.
// if (SDA == 0) printf("ACK Received!\n");
SCL = 0; // 통신을 이어서 하거나 종료할 수 있도록 SCL을 다시 Low로 정리
i2c_dly();
}
SCL=1; 에서 SCL = 0 로 되었을때 1Bit를 보냈다고 생각하면 된다.
- 마스터는 SCL이 Low일 때 SDA 핀에 1 또는 0의 값을 출력한다.
- 그런 다음 SCL을 High로 올리면, 슬레이브가 이 High 구간 동안 SDA의 값을 읽어간다.
- 마스터는 다시 SCL을 Low로 내리고 다음 비트를 준비한다. 이 과정을 8번 반복한다.
i2c-dev 커널 모듈: 리눅스에서 사용자가 사용자 공간(User Space)에서 I2C 버스에 직접 접근할 수 있게 해주는 드라이버이다.
open(), read(), write(), ioctl()로 다룰 수 있다.
하지만 이러한 커널 시스템 콜을 직접 사용하는 것은 난이도가 있다. 그래서 예전에 말한 WiringPi를 이용할 수 가 있는데
- 초기화: int wiringPiI2CSetup(int devId) 함수를 사용하여 통신할 장치를 초기화하고 파일 디스크립터(fd)를 얻는다.
- 단순 읽기/쓰기: wiringPiI2CRead(fd)나 wiringPiI2CWrite(fd, data)를 통해 1바이트 단위로 데이터를 주고받을 수 있다.
- 레지스터 기반 제어 (추상화): 장치 내부의 특정 메모리 주소(Register)를 지정하여 데이터를 쓰거나 읽는 기능을 제공한다.
- wiringPiI2CWriteReg8 / Reg16: 특정 레지스터에 8비트 혹은 16비트 값을 기록한다.
- wiringPiI2CReadReg8 / Reg16: 특정 레지스터로부터 8비트 혹은 16비트 값을 읽어온다.
GPIO 확장: 라즈베리 파이의 자체 GPIO 핀이 부족할 때, 이 칩을 연결하면 16개의 추가적인 입출력 핀(GPA0~7, GPB0~7)을 사용할 수 있다

SPI
Serial Peripheral Interface의 약자로서 모토로라에서 제안한 동기식 시리얼 통신 규격으로, 주로 아주 짧은 거리의 장치 간 통신에 사용된다.
I2C와 달리 single master를 가지고 있으며 총 4개의 물리적인 선을 사용하는 버스 구조이다

- SCLK: 시리얼 클록 (마스터가 생성).
- MOSI: 마스터 출력, 슬레이브 입력 (데이터 송신용). master -> slave
- MISO: 마스터 입력, 슬레이브 출력 (데이터 수신용). slave -> master
- 그래서 full-duplex 구조로 사용이 가능하다!!
- SS (Slave Select): 통신할 슬레이브를 선택하는 신호.

그림과 같이 최대 2개를 가질 수 있으며 통신할 슬레이브를 선택하는 신호라고 보면 된다.
위에서 말했듯이 Full-deplex 통신 방식이 사용되는 것을 볼 수가 있는데,
master -> slave 시에는 무조건 MOSI 선을 사용하고 반대에는 MISO선을 사용하는 것을 볼 수 있다.

- 마스터가 SS 라인을 활성화(보통 Low)한다.
- 마스터가 클록(SCLK)을 발생시킨다.
- 클록에 맞춰 MOSI로 명령을 보내고, 동시에 MISO로 응답을 받는다.
- 전송이 끝나면 SS 라인을 다시 High로 올려 연결을 해제한다.
SPI Driver in Linux
spidev: 리눅스에서 SPI 장치에 접근하기 위한 드라이버.
- /dev/spidev0.0 (첫 번째 버스, 첫 번째 슬레이브) 같은 경로로 접근한다.
read() or write() 는 Half-duplex access 밖에 가능하지 않고
ioctl()을 사용해야 full- duplex access가 가능하다.
- SPI_IOC_WR_MODE: 통신 모드(CPOL, CPHA) 설정.
- SPI_IOC_WR_MAX_SPEED_HZ: 최대 통신 속도 설정.
- SPI_IOC_MESSAGE(N): Full-duplex 전송을 수행할 때 사용하며, spi_ioc_transfer 구조체에 버퍼 정보를 담아 보낸다.
WiringPi SPI Library
- 초기화: int wiringPiSPISetup(int channel, int speed)
- 채널은 0 또는 1을 선택하며, 속도는 보통 500,000~32,000,000Hz 범위 내에서 설정한다.
- 데이터 송수신: int wiringPiSPIDataRW(int channel, unsigned char *data, int len)
- 매우 중요: 이 함수는 data 버퍼에 있는 내용을 보내는 동시에, 슬레이브로부터 받은 내용을 같은 버퍼에 덮어쓴다. 따라서 실행 후 data를 확인하면 수신된 값이 들어있다
| 비교 항목 | Inter-Integrated Circuit (I2C) | Serial Peripheral Interface (SPI) |
필요한 선 개수 (Wire) |
단 2개 (SDA, SCL)만 있으면 되므로 회로가 매우 단순해지고 핀을 아낄 수 있습니다. |
최소 4개 (SCLK, MOSI, MISO, SS)가 필요하며, 슬레이브가 늘어날 때마다 SS 선이 추가로 필요합니다. |
통신 속도 (Speed) |
상대적으로 저속입니다. 표준 규격상 최대 3.4Mbps 정도의 속도를 냅니다. | 전이중(Full-Duplex) 통신을 지원하여 데이터를 보내면서 동시에 받을 수 있고, 속도가 Mbps이상으로 매우 빠릅니다. |
전송 거리 (Distance) |
풀업 저항을 사용하는 버스 구조 덕분에, SPI에 비해 상대적으로 먼 거리까지 신호를 안정적으로 보낼 수 있습니다. | 신호 감쇠에 취약하여 매우 짧은 거리에서만 쓸 수 있습니다. 보통 하나의 PCB 보드 내부를 벗어나기 힘듭니다. |
소비 전력 (Power) |
라인을 High 상태로 유지하기 위해 풀업 저항을 거쳐 지속적으로 전류가 흐르므로, SPI보다 전력 소모가 큽니다. | 마스터와 슬레이브가 푸시풀(Push-Pull) 방식으로 라인을 직접 구동하므로, I2C에 비해 전력 소모가 적습니다. |
'수업 정리 > 임베디드시스템소프트웨어' 카테고리의 다른 글
| [embedded system software/12주차] 이론 (1) | 2026.05.22 |
|---|---|
| [embedded system software/10주차] 이론 (0) | 2026.05.09 |
| [embedded system software/9주차] 이론 (0) | 2026.04.29 |
| [embedded system software/6주차] 이론 (0) | 2026.04.15 |
| [embedded system software/5주차] 수업정리 (0) | 2026.04.01 |