Embedded System I2C

Wonho Kim·2025년 1월 1일

Embedded System

목록 보기
6/7

I2CI^2C

마이크로프로세서 저속 주변 장치 간의 통신을 위해 필립스(Philips)에서 개발한 통신 규격

반이중 통신 방식

송/수신 동시에 불가
SDA: Serial Data

일대다 통신 지원

마스터-슬레이브 구조
소프트웨어 주소를 사용하여 슬레이브 선택
(추가적인 선의 개수가 줄어듬)

동기 방식

클럭 신호(SCL) 공유

SDA, SCL은 Open-drain 방식으로 구성되어있다.

Master에서 SDA에 전압을 인가하지 않으면 (TR Off), SDA의 전압이 HIGH 상태인 VDD로 유지되고, 전압을 인가하면 (TR On) SDA의 전압이 0V로 변경되는 구조이다.

* SDA는 개념적으로 In/Out으로 표기하였으나 실제로는 하나의 선이다.

아래는 I2CI^2C의 1:5 연결 예시이다. Input인 SDA와 클럭신호인 SCL 만으로 구성한 것을 확인할 수 있다.

프로토콜

I2CI^2C는 2개의 선만으로 시리얼 통신을 해야하므로 SPI나 UART에 비해 프로토콜이 복잡하다.

한번에 보면 어려우니 하나씩 단계적으로 살펴보면서 이해해보도록 하자.

Start 컨디션

SCL이 HIGH를 유지한 상태에서, SDA가 HIGH에서 LOW로 바뀌면 Master가 전체 Slave들에게 통신 시작을 알리는 신호가 된다.

Address 프레임

Master가 통신을 원하는 장치의 주소를 Start 컨디션 이후에 바로 전송하게 되며, 모든 통신 시퀀스에 필수적으로 포함되어야 하는 요소이다.

7-bits Addressing

  • MSB부터 7bit 주소 + R/WR/\overline{W}
  • 총 127개의 장치와 통신 가능
    (0000 0000이 모든 슬레이브로 메시지를 전송하는 역할을 담당하기 때문에 컴네 브로드캐스트? 282^8=128개에서 0000 0000 주소를 제외하고 127개가 된다.)

ACK와 NAK

1 byte 전송을 성공할때마다 ACK 신호를 보낸다.

Data 프레임

Address 프레임 이후에 전송하며 설정에 따라 M->S or M<-S을 결정하게 된다. 필요한 데이터 프레임 만큼 전송한다.

Stop 컨디션

SCL이 HIGH인 상태로 SDA가 LOW에서 HIGH로 바뀌면 Stop 컨디션이 발생하고 전체 Slave들에게 통신 종료를 알리게 된다.

I2CI^2C 통신 시퀀스 예시

빨간색이 Master, 파란색이 Slave 송신이며, 마스터에서 2byte 데이터 쓰기를 예시로 보면 아래와 같다.

마스터에서 데이터를 쓰기 작업(Write)을 진행하므로 M->S 송신 구조가 되며 1byte 작업을 받을 때 마다 Slave가 ACK 신호를 보내는 것을 확인할 수 있다.

반대로 마스터에서 2byte 데이터 읽기를 예시로 보면 아래와 같다.

마스터 데이터를 읽기 작업(Read)를 진행하므로 M<-S 송신 구조가 되며 마스터의 7-bit Addressing 이후에는 Master가 ACK 신호를 보내는것을 확인할 수 있다.

라즈베리파이의 I2CI^2C

I2C1 (GPIO2, GPIO3)을 사용하며 1.8kΩ1.8k\Omega 풀업 저항이 이미 장착되어 있다.

I2C0 (GPIO0, GPIO1)은 일반적으로 EEPROM을 사용하기 위한 용도이다.

I2C 클럭 주파수는 100kHz가 기본 값으로 설정되어 있다.
(기본 클럭 주파수를 변경하고 싶다면 아래 링크 참고)
https://www.raspberrypi-spy.co.uk/2018/02/change-raspberry-pi-i2c-bus-speed/

RTC 모듈

실시간으로 시간을 측정해서 출력하는 장치이며, 배터리 탑재 시 마이크로컨트롤러와 연결되지 않은 상태에서도 시간 측정이 가능하다. (수명은 2~3년 정도)

우리가 사용할 RTC의 제품명은 DS3231이다. 정밀 RTC이며 I2C 통신을 지원한다. 400kHz의 빠른 통신 속도를 지원한다고 한다.

DS3231 레지스터 주소 맵

초(Seconds) 데이터를 BCD 형식을 표현한다.
☃️ Binary-coded decimal(BCD)란?
10진수의 각 자릿수를 2진수로 표현하는 방식
10진수 53 => 2진수 0101 0011

wiringPi의 I2CI^2C 라이브러리

헤더 파일

#include <wiringPiI2C.h>

I2C 셋업 함수

int wiringPiI2CSetupInterface(const char *device, int devId)

device: 장치 파일의 경로
devid: 슬레이브 주소
반환값: 파일 서술자

I2C 쓰기 함수

int wiringPiI2CWriteReg8 (int fd, int reg, int data);

id: 파일 서술자
reg: 레지스터 주소
data: 쓰기를 수행할 데이터

I2C 읽기 함수

int wiringPiI2CReadReg8 (int fd, int reg);

fd: 파일 서술자
reg: 레지스터 주소
반환값: 읽은 데이터

예제 코드

#include <stdio.h> 
#include <unistd.h> 
#include <wiringPi.h> 
#include <wiringPiI2C.h> 
#include <sys/ioctl.h>

#define SLAVE_ADDR_01 0x68 //슬레이브 주소

static const char* I2C_DEV = "/dev/i2c-1"; //I2C 연결을 위한 장치 파일

int main()
{
	int i2c_fd;
    
    if(wiringPiSetup() < 0 )
    {
    	printf("wiringPiSetup() is failed\n");
        return 1;
    }
    
    i2c_fd = wiringPiI2CSetupInterface (I2C_DEV, SLAVE_ADDR_01); //i2C 연결 시도
    printf("I2C start....\n");
    
    while(1)
    {
    	//초를 읽어 오는 코드
        int sec = wiringPiI2CReadReg8 (i2c_fd, 0x00);
        printf("%x\n", sec);
        
        //달을 10월로 설정
        wiringPiI2CWriteReg8 (i2c_fd, 0x05, 0x10);
        int month = wiringPiI2CReadReg8 (i2c_fd, 0x05);
        printf("%x\n", month);
        delay(1000);
    }
    return 0;
}

실습1

예제 코드를 아래와 같은 조건으로 수정해보자.

  1. 년/월/일/요일/시/분을 현재 시각으로 변경한다.
  2. 24시간 체계로 표현되도록 레지스터를 수정한다.
  3. 현재 시각을 매초 마다 업데이트 하면서 표현한다.

e.g. 2024년 10월 19일 금요일 13시 25분 13초

#include <stdio.h>
#include <unistd.h>
#include <wiringPi.h>
#include <wiringPiI2C.h>
#include <sys/ioctl.h>

#define SLAVE_ADDR_01 0x68 //슬레이브 주소

static const char* I2C_DEV = "/dev/i2c-1"; //I2C 연결을 위한 장치 파일

int main()
{ 
	int i2c_fd;

	if(wiringPiSetup() < 0)
	{
		printf("wiringPiSetup() is failed\n");
		return 1;
	}

	i2c_fd = wiringPiI2CSetupInterface (I2C_DEV, SLAVE_ADDR_01); //i2C 연결 시도

	printf("I2C start....\n");

	// 시간을 2024년 10월 19일 금요일 13시 25분 13초로 설정
	wiringPiI2CWriteReg8(i2c_fd, 0x00, 0x13); // 초 설정
	wiringPiI2CWriteReg8(i2c_fd, 0x01, 0x25); // 분 설정
	wiringPiI2CWriteReg8(i2c_fd, 0x02, 0x13); // 시 설정
	wiringPiI2CWriteReg8(i2c_fd, 0x03, 0x05); // 요일 설정 (금요일)
	wiringPiI2CWriteReg8(i2c_fd, 0x04, 0x19); // 날짜 설정
	wiringPiI2CWriteReg8(i2c_fd, 0x05, 0x10); // 월 설정 (10월)
	wiringPiI2CWriteReg8(i2c_fd, 0x06, 0x24); // 연도 설정 (2024년)

	const char* dayOfWeek[] = { "월", "화", "수", "목", "금", "토", "일" };

	while(1)
	{	
		int sec = wiringPiI2CReadReg8 (i2c_fd, 0x00);
		int minutes = wiringPiI2CReadReg8 (i2c_fd, 0x01);
		int hours = wiringPiI2CReadReg8 (i2c_fd, 0x02);
		int day = wiringPiI2CReadReg8 (i2c_fd, 0x03);
		int date = wiringPiI2CReadReg8 (i2c_fd, 0x04);
		int month = wiringPiI2CReadReg8 (i2c_fd, 0x05);
		int year = wiringPiI2CReadReg8 (i2c_fd, 0x06);

		// day 값이 1~7 범위 내에 있는지 확인하고, 그에 맞는 요일을 선택
		// day 값이 1부터 시작하므로 인덱스를 맞추기 위해 day - 1 사용
		const char* dayString = (day >= 1 && day <= 7) ? dayOfWeek[day - 1] : "N/A";
		
		// 읽어온 시간 출력
		printf("\r20%x년 %x월 %x일 %s요일 시간: %x %x %x", year, month, date, dayString, hours, minutes, sec);
		fflush(stdout);
		delay(1000);
	}
	return 0;
}
profile
새싹 백엔드 개발자

0개의 댓글