독자들은 I2C라고 하는 시리얼 버스를 아는가? 임베디드 시스템을 개발할 때 여러 디바이스를 다루다 보면 약방에 감초처럼 자주 등장하는 버스다. 8비트 마이크로 컨트롤러를 다루는 프로그래머라면 EEPROM나 RTC와 같은 디바이스를 다루기 때문에 반드시 사용할 만큼 중요한 버스인데 리눅스를 사용하는 시스템이라면 디바이스 드라이버와 관련된 사용 문서를 발견하기 힘들기 때문에 조금 다루기 곤란한 버스와 디바이스가 되어 버린다. 그래서 I2C와 연결된 장치를 사용하기 위한 리눅스 디바이스 드라이버를 만들 때 직접 I2C 버스와 관련된 컨트롤러나 GPIO를 핸들링해서 처리하는 경우도 종종 보는데 여러 호환성을 염두에 두면 프로젝트야 진행되겠지만 아무래도 뒤로 찜찜한 여운이 남게 된다.
I2C란 무엇인가?
I2C(I-square-C, ‘아이스퀘어시’라고 보통 부른다)란 필립스가 제안한 통신 방식이다. Inter-IC라고도 불리지만 이 명칭은 그리 잘 쓰이지 않는 명칭이다. I2C는 로컬 버스라고 부르는 병렬 버스와 다르게 주변 장치를 단지 두 가닥의 신호선으로만 연결하여 동작하는 양방향 직렬 버스 규격이다. 필립스는 TV, VCR, 오디오 장비 등과 같은 대량 생산되는 제품용으로 I2C 버스를 이미 20년 전에 소개했는데 지금은 내장 장치를 다루기 위한 사실상의 표준 솔루션이 되었다. I2C 버스에는 표준, 고속, 초고속 등 속도에 따라 세 가지 데이터 전송 모드가 있다. 표준 모드는 100Kbps, 고속은 400Kbps 그리고 초고속 모드에서는 최고 3.4Mbps의 속도를 지원한다. 이 세 가지 모두 하위 호환성을 갖는다. I2C 버스는 각 장치에 7비트와 10비트 주소를 지정하여 여러 장치들을 독립적으로 접근할 수 있다.
<그림 1>에서 보듯이 I2C 버스는 SDA(Serial DAta Line) 신호선과 SCL(Serial Clock Line) 신호선으로 통신의 주체가 되는 마스터인 MCU와 통신 대상이 되는 주변 장치인 슬레이브(slave) 간에 데이터를 전달하고 받는다.
디바이스 주소
슬레이브가 되는 디바이스를 지정하기 위해 사용되는 주소는 7비트로 표현되거나 10비트로 표현된다. 대부분의 디바이스들은 7비트 형식의 주소를 사용한다. 이런 이유로 7비트 형식의 주소를 사용하는 경우라면 마스터(master)가 지정할 수 있는 장치는 128개로 한정된다. 주소는 디바이스 따라서 디바이스 제작사에서 정해지기도 하고 디바이스의 외부 핀을 이용하여 지정할 수도 있다. 핀 수가 적은 패키지 형식의 디바이스라면 주소의 상위 비트는 고정되고 하위 비트만 지정하는 경우가 일반적이다. 그래서 I2C 버스에 연결되는 디바이스와 통신하는 프로그램을 작성하는 프로그래머라면 반드시 매뉴얼을 참조하여 해당 디바이스 주소를 알고 있어야 한다.
SCL, SDA 신호선
I2C 버스를 이용하여 MCU가 디바이스에 데이터를 써 넣거나 읽어 들이기 위해서는 SCL과 SDA라는 신호선을 제어해야 한다. SCL은 데이터를 전달하기 위한 동기용 클럭을 전달하는 신호선이고 SDA는 전달하고자 하는 데이터의 비트 정보를 표현하기 위한 신호선이다. SCL은 데이터의 전달을 위한 클럭 동기 신호선으로 이 클럭 신호는 마스터에서 공급한다. 그래서 SCL은 마스터에서 슬레이브로 전달되는 단방향 신호선이다. 그러나 SDA는 마스터에서 슬레이브로 데이터를 전달하거나 슬레이브에서 마스터로 데이터를 가져오기 때문에 양방향 신호선이다.
하드웨어적인 접속 방법은 이 컬럼의 특성상 다룰 이야기가 아니므로 버스에 연결되는 저항에 대한 설명이나 디바이스에 인가되는 전압과 같은 처리에 대한 것은 설명하지 않겠다.
I2C 버스의 데이터 전송
I2C는 시리얼 전송 방식을 사용하기 때문에 데이터의 전달은 기본적으로 비트 정보를 전달한다고 이해해야 한다. I2C 버스에서 비트 데이터들을 디바이스에 써 넣거나 읽기 위해서는 다음과 같은 기본적인 표현요소가 필요하다.
◆ 마스터가 슬레이브에 전송을 시작한다는 표현 - Start
◆ 전송 목적지의 주소 표현 - Address
◆ 전송 목적 표현(읽기용인가 또는 쓰기용인가) - R/W
◆ 전송 데이터 표현 - Data
◆ 슬레이브가 정상적으로 데이터를 수신했다는 응답 표현 - Ack
◆ 전송 종료 표현 - Stop
이와 같은 표현을 포함하여 I2C 버스상에서 데이터를 전송하는 기본 데이터는 8비트 단위로 지정한다. 시작과 종료, 응답 표현은 1비트로 지정한다. I2C에서 데이터를 전송하는 포맷은 <그림 2>와 같은 형태가 된다.
START 표현Start는 마스터가 슬레이브에 전송 시작을 알리기 위한 것으로 I2C 버스를 사용하겠다는 신호의 시작이다. 이 신호는 1비트 형태로 구현되며 SCL이 HIGH 상태가 유지될 때 SDA가 HIGH에서 LOW로 변화되면 START 신호로 해석된다. <그림 3>은 START의 신호 변화이다. START 신호는 STOP 이전에 여러 번 나올 수도 있다. 첫 번째 START와 달리 두 번째 START 신호는 REPEAT START 신호라고 하는데 보통 디바이스 안에 내부적인 주소가 있어서 이 주소를 지정하고 데이터를 읽을 경우에 사용된다.
STOP 표현
Stop은 MCU가 슬레이브에 전송을 종료한다는 것을 알리기 위한 것으로 I2C 버스를 더 이상 사용하지 않겠다는 신호이다. 이 신호는 1비트 형태로 구현되며 SCL이 HIGH 상태가 유지될 때 SDA가 LOW에서 HIGH로 변화되면 STOP 신호로 해석된다. <그림 4>는 STOP를 표현하기 위한 신호 변화 상태이다.
DATA 1비트 신호 표현마스터에서 슬레이브에 전달하거나 슬레이브에서 데이터를 읽어 올 때 데이터의 1비트를 표현하기 위해서는 SCL 신호선을 LOW 상태에서 전송하고자 하는 비트 데이터를 SDA 신호선에 결정하고 SCL 신호선을 HIGH 상태로 만든다. 슬레이브 또는 마스터는 SCL이 HIGH 상태일 때의 SDA 신호 상태를 보고 SDA 신호가 HIGH면 1로 LOW면 0으로 판단한다(<그림 5>).
ACK 표현
슬레이브는 마스터가 전송한 데이터(주소와 읽기 쓰기를 결정하는 것도 데이터로 취급한다)를 제대로 수신받았다거나 슬레이브에서 마스터에 데이터를 전달할 경우에 8비트 데이터 다음에 ACK 신호를 표현한다. ACK도 일종의 데이터이기 때문에 SCL의 상태가 LOW일 때 SDA의 상태를 HIGH 또는 LOW로 결정하며 SCL의 상태가 HIGH일 때 SDA의 상태를 마스터가 읽으면 된다. 이때의 SDA 상태가 LOW라면 정상적인 통신이 이루어진 것이고 만약 HIGH라면 정상적인 통신에 실패한 것으로 판단한다.
일단 아주 기초적인 신호 표현에 대해서는 이 정도로 마치고 실제로 사용되는 자세한 통신 데이터 구조는 이후의 예제에서 설명할 EEPROM에 대한 것을 다루면서 설명하겠다.
응용 프로그램에서 I2C 버스에 연결된 디바이스 제어
임베디드 시스템에서 I2C 버스에 연결된 디바이스를 제어하기 위한 가장 간단한 방법은 i2c-dev 디바이스 드라이버를 이용하는 방법이다. 여기서는 S3C2410이라는 삼성전자에서 만들고 있는 프로세서를 탑재한 EZ-S2410이라는 보드를 이용한다. I2C 버스에 연결된 EEPROM KS24C080 칩에서 데이터를 기록하고 읽어 들이는 프로그램을 예제로 응용 프로그램에서 어떻게 I2C 버스에 연결된 디바이스를 제어하는 가를 살펴본다. <그림 6, 7>은 각각 EZ-S2410이라는 보드의 외형과 확장 커넥터의 I2C 버스에 EEPROM을 연결한 회로도이다.
EEPROMEEPROM(Electrically Erasable Programmable Read-Only Memory)이란 사용자가 메모리 내의 내용을 수정할 수 있는 롬이다. 정상보다 더 높은 전압을 이용하여 반복적으로 지우거나 다시 프로그램(기록)할 수 있다. 일반적인 롬인 EPROM 칩과는 달리, EEPRO M은 기록된 내용을 수정하기 위해 컴퓨터에서 빼낼 필요가 없다. 그러나 사용할 수 있는 수명에도 제한이 있는데, 다시 프로그램할 수 있는 횟수가 10만회 미만으로 제한될 수 있다. 컴퓨터가 사용되는 동안 자주 다시 프로그래밍되는 EEPROM에서는 EEPROM의 수명이 아주 중요한 설계 고려사항이 될 수 있다. EEPROM의 특별한 형태가 플래시 메모리이다.
EEPROM은 메모리 용량이 그리 크지 않기 때문에 시스템에 반드시 기억해 놓아야 할 옵션 데이터나 기타 제어 데이터를 기록해 놓는다. 우리가 예제로 사용하는 KS24C080는 내부적으로 기억해 놓을 수 있는 데이터의 크기가 1024바이트 정도의 크기를 갖는다. 외부에서 지정하는 어드레스는 A0, A1, A2로 0x50 주소를 기준으로 하부 어드레스를 설정한다. 그래서 하나의 I2C 버스상에는 8개의 KS24C 080만을 연결할 수 있다. 예제 회로에는 A0, A1, A2 를 모두 GND에 연결했으므로 I2C 버스상에서 SLAVE 주소는 0x50이 된다.
i2c-dev 디바이스 파일
리눅스에서 응용 프로그램이 I2C 버스에 연결된 디바이스와 데이터를 주고받기 위해서는 i2c-dev 디바이스 드라이버를 사용해야 한다. i2c-dev 디바이스 드라이버는 /dev/i2c-?와 디바이스 파일로 표현한다. 이 디바이스 파일은 i2c-dev 디바이스 드라이버를 응용 프로그램에서 사용할 수 있도록 시스템에서 제공하는 파일이다. 그러나 이 디바이스 파일은 대부분의 리눅스 배포판이나 임베디드 램디스크를 이용한 파일 시스템에서는 제공하지 않는다. 그래서 이 디바이스 파일은 직접 만들어줘야 하는데 mknod 유틸리티를 이용하거나 프로그램에서 mknod 함수를 이용하여 만들어줘야 한다.
i2c-dev 디바이스 파일은 주번호가 89이고 부번호가 0부터 시작하는 문자형 디바이스 파일이다. 부번호는 시스템에 존재하는 i2c 버스를 구별하는데 i2c 버스는 시스템에 하나 이상 존재할 수 있기 때문에 각 버스마다 0, 1과 같은 식으로 순서에 입각하여 부여한다. 보통 하나의 i2c 버스가 존재하는 경우가 대부분이기 때문에 /dev/i2c-0 라는 디바이스 파일만 있으면 된다.
◆ mknod 를 사용하는 경우 : mkdev 유틸리티를 이용할 때는 다음과 같은 명령 형식으로 만들어주면 된다.
# mkdev c /dev/i2c-0 c 89 0
# mkdev c /dev/i2c-1 c 89 1
…
◆ 프로그램에서 직접 만들 경우
#define I2C_DEV_FILENAME “/dev/i2c-0”
if( access( I2C_DEV_FILENAME , F_OK ) != 0 )
mknod( I2C_DEV_FILENAME, S_IRWXU|S_IRWXG|S_IFCHR,(89<<8|(0)));
커널 컴파일 설정 조건
i2c-dev 디바이스 드라이버를 사용하려면 커널에서 이에 해당하는 디바이스 드라이버가 포함되도록 커널 컴파일 조건이 설정된 상태에서 컴파일되어 시스템에 탑재돼야 한다. <화면 1>은 커널 컴파일 설정 조건이다.
i2c-dev 디바이스 파일 열기와 닫기
i2c-dev 디바이스 파일을 이용하여 I2C 버스상에 연결된 디바이스에 어떤 행위를 하려면 우선적으로 i2c-dev 디바이스 파일을 열어야 한다. 이 디바이스 파일을 열기 위한 함수는 저수준 파일 함수인 open 함수를 이용한다. 보통 블러킹 I/O 형태로 열기 때문에 다음과 같은 형식으로 열고 닫으면 된다.
int fd;
fd = open( I2C_DEV_FILENAME, O_RDWR );
if( fd >= 0 )
{
:
:
close( fd );
}
i2c-dev를 사용하기 위해서는 다음과 같은 헤더 파일을 포함해야 한다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/poll.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
i2c-dev 디바이스 드라이버를 다루기 위해서는 주로 ioctl 함수를 이용한다. ioctl 를 이용하여 여러가지 설정 사항을 i2c-dev에 전달할 수 있는데 이에 대한 ioctl 명령은 ‘이달의 디스켓’을 참고하기 바란다(ioctl.txt).
i2c-dev 디바이스 드라이버를 이용하여 EEPROM에 데이터 쓰기 i2c-dev 디바이스 드라이버를 사용하기 위한 /dev/i2c-0 디바이스 파일을 열었다면 정상적으로 i2c-dev 디바이스 드라이버를 이용하여 I2C 버스에 연결된 디바이스 드라이버에 데이터를 써 넣을 수 있다. i2c-dev를 이용하여 eeprom에 데이터를 쓰기 위해서는 전달 포맷을 알아야 한다.
보통 I2C 버스에 연결된 디바이스들은 보통 자신의 주소 이외에 내부적인 어드레스가 지정되어야 한다. 이 회로에서 들고 있는 EEP ROM은 I2C 버스에서 슬레이브 주소에 해당하는 주소 이외에 EEP ROM에 저장할 데이터의 내부 주소를 지정해야 한다. KS24C080는 내부적으로 1024바이트를 지정할 수 있으므로 내부 주소 지정을 위하여 2 바이트가 필요하다. <그림 10>이 KS24C080에 데이터를 써 넣기 위한 구조이다.
i2c-dev를 이용하여 데이터를 써 넣기 위해서는 가장 먼저 ioctl을 이용하여 주소를 지정하고 write 함수를 이용하여 데이터를 써 넣어야 한다.
unsigned char eeprom_data[32]; // EEPROM 읽기 쓰기 데이터 버퍼
ioctl( fd, I2C_SLAVE, 0x50 ); // 슬레이브 주소
i2c-dev 디바이스 드라이버를 이용한 쓰기 읽기의 조합
KS24C080의 경우에는 이와 같은 방식으로 한번은 쓰기 명령 형식을 이용하여 write 함수로 읽을 주소를 지정하고 read 함수를 이용하여 읽기를 처리할 수 있다. 이 경우에는 START와 STOP이 반복적으로 사용된다. 하지만 경우에 따라서는 START … START … STOP 형식으로만 사용해야 하는 디바이스가 있다. 이런 경우에는 단순하게 write read 함수의 조합만으로는 구현이 불가능하다. KS24C080의 경우에도 이와 같은 형식으로 데이터를 읽을 수 있는데 이런 경우의 데이터 형식을 보면 <그림 11>과 같다.
i2c-dev는 read 함수와 write 함수를 기본적으로 함수 호출시 START와 STOP을 발생시킨다. 그래서 START … START … STOP 형식을 사용하고자 한다면 read, write 함수를 사용할 수 없다. 이 때 사용하는 것이 ioctl 함수에 I2C_RDWR 명령을 사용하는 방법이다. 이런 형식으로 사용할 경우에 형식은 다음과 같다.
ioctl( fd, I2C_RDWR, (struct i2c_rdwr_ioctl_data *) msgs );
이 명령은 ioctl 함수 하나에 여러 개의 전송 데이터를 만들기 위한 것이다. 우선 struct i2c_rdwr_ioctl_data란 구조체에 대하여 알아보자.
struct i2c_rdwr_ioctl_data
이 구조체는 전송할 데이터를 묶는 역할을 하는데 다음과 같은 구성을 가진다. 이 구조체를 사용하려면 #include <i2c-dev.h>를 포함해야 한다.
struct i2c_rdwr_ioctl_data {
struct i2c_msg __user *msgs; /* pointers to i2c_msgs */
__u32 nmsgs; /* number of i2c_msgs */
};
이 구조체는 전달할 데이터 블럭이 정의된 i2c_msg 구조체들의 선두 주소와 전달할 데이터 블럭 수를 지정한다. msgs 필드 변수가 i2c_msg 구조체의 선두 주소를 지정하고 nmsgs가 블럭 수를 지정한다.
struct i2c_msg
이 구조체가 실제로 전달해야 하는 데이터의 각 블럭을 표현한다.
struct i2c_msg {
__u16 addr; /* slave address */
__u16 flags;
__u16 len; /* msg length */
__u8 *buf; /* pointer to msg data */
};
addr 필드 변수는 슬레이브 주소를 지정한다. 그러므로 I2C_ RDWR를 이용한 ioctl 함수를 사용할 때는 I2C_SLAVE를 이용하여 슬레이브 주소를 지정할 필요가 없다. buf는 전달할 데이터를, 지정한 버퍼 주소는 읽어 들일 데이터 버퍼 주소를 지정한다. len은 버퍼를 이용하여 써 넣을 데이터나 읽을 데이터 수를 지정한다. 이 형식을 이용할 때 각각 msg 구조체의 flags는 세밀한 제어를 하기 위해 다음과 같은 값의 비트 조합을 사용한다.
◆ I2C_M_TEN : 주소가 10비트이다.
◆ I2C_M_RD : 이 값이 지정되면 읽기 명령을 I2C 버스상에서 수행한다.
◆ I2C_M_NOSTART : START가 발생하면 안되는 패킷임을 표시한다. 가장 첫 번째 msg 블럭에는 사용할 수 없다.
◆ I2C_M_REV_DIR_ADDR : R/W가 반전된 처리를 해야 한다.
◆ I2C_M_IGNORE_NAK : NAK 응답 즉 ACK가 없더라도 에러 처리하지 않는다.
◆ I2C_M_NO_RD_ACK : 읽기에 따른 ACK가 없더라도 에러 처리하지 않는다.
이 옵션 중 가장 많이 사용되는 것은 I2C_M_RD, I2C_M_NOSTART 이다. 우선 I2C_RDWR 명령을 사용하여 데이터를 써 넣는 것을 구현해보자. 이것은 앞에서 설명한 write 함수의 다른 형태를 조금 복잡하게 설명한 것으로 I2C_RDWR를 설명하기 위한 것이다. 실용적인 목적에서는 이렇게 굳이 사용할 필요는 없다.
struct i2c_msg i2c_msgs[2];
struct i2c_rdwr_ioctl_data i2c_rwctl;
unsigned char eeprom_data_addres[2]; // EEPROM 데이터 주소 설정용 버퍼
unsigned char eeprom_data[32]; // EEPROM 읽기 쓰기 데이터 버퍼
무척 복잡하지만 나름대로 write 함수를 대치하기 위한 것이다. 하지만 앞에서 예를 든 읽기의 경우에 START … START … STOP 형식으로 쓰려면 반드시 I2C_RDWR를 사용해야 한다. 다음이 이런 형식으로 쓰기 위한 것이다.
I2C 버스와 커널 지금까지 응용 프로그램에서 커널에 제공된 i2c-dev 디바이스 드라이버를 이용하여 어떻게 I2C 버스에 연결된 디바이스를 다루는지 알아보았다. 실제 사용법을 보면 간단한 I2C 버스의 구조만큼이나 쉬운 인터페이스를 가지고 있다. 그렇다면 이렇게 응용 프로그램에서 i2c-dev 디바이스 드라이버를 지원하기 위해 커널을 수정하려면 어떻게 해야 하는가? PC 시스템의 경우라면 I2C 버스를 사용할 기회는 그리 많지 않다. 비디오 for 리눅스와 관련된 영상 처리 시스템이나 몇몇의 RTC와 관련이 있는 정도이다. 하지만 임베디드 시스템에서 I2C는 의외로 많은 사용도를 가진다. 영상과 관련된 시스템은 반드시라고 할 정도로 포함된다.
문제는 I2C 버스를 구현하는 것이 사용되는 시스템마다 모두 다르다는 것에 있다. 어떤 시스템은 GPIO를 이용하여 처리하기도 하고 어떤 시스템은 I2C 버스 컨트롤러를 가지고 지원하기도 한다. 임베디드 프로그래머는 자신의 시스템에 맞추어 I2C 버스를 지원하도록 리눅스 커널을 수정해야 할 경우는 당연히 발생하게 된다. 그러므로 우리는 리눅스 커널에서 I2C 버스를 구현하기 위해서는 리눅스 커널 내에서 I2C를 어떻게 다루고 있는지를 살펴봐야 한다.
리눅스 커널은 I2C 버스를 다루기 위해서 두 가지 개념을 두고 있다. 하나는 어댑터(adapter)라는 개념과 다른 하나는 알고리즘(algorithm)이라는 개념이다. 여기서 말하는 어댑터라는 것은 우리가 알고 있는 개념과 달리 i2c 버스를 커널 내부에서 관리하기 위한 정보 관리 구조체로 보면 된다. 그렇다면 알고리즘이라는 것은 어떤 것일까? 알고리즘은 이름만 보면 논리적인 구현 방법을 생각하는데 실제로는 하드웨어상에서 I2C 버스를 제어하는 방법에 대한 구체적인 프로그램 코드가 들어간다. 즉 어댑터는 커널 내부의 정보 구조체이고 알고리즘은 실제 버스를 제어하는 루틴을 다루는 것이다. 여기서 기억해둬야 할 것은 어댑터 구조체 내에 알고리즘 구조체를 포함하고 있다는 것이다.
우선 i2c-dev 디바이스 드라이버가 리눅스 커널내에서 어떻게 I2C 버스를 제어하는가를 살펴보자. <그림 12>는 i2c-dev 디바이스 드라이버가 커널 내부에서 어떤 호출 관계를 갖는가를 살펴본 것이다.
응용 프로그램이 /dev/i2c-0 디바이스 파일을 열고서 read, write, ioctl 함수를 호출하면 i2c-dev 디바이스 드라이버는 read에 대응하여 i2c_master_recv 함수를, write에 대응하여 i2c_master_send를, ioctl 함수를 호출하면 i2c_transfer 함수를 호출한다. i2c-dev에서 호출하는 i2c_master_send, i2c_master_recv, i2c_transfer 함수들은 i2c-dev에 연결된 i2c 어댑터 구조체를 참조하고, 이 구조체에 포함된 i2c 알고리즘 구조체에 정의되어 있는 master_xfer라는 함수를 호출한다. 이 master_xfer는 하드웨어 구조에 맞게 프로그램된 실제적인 I2C 버스 제어 코드가 존재하게 된다.
이런 구조이기 때문에 커널에서 I2C 버스를 지원하도록 하기 위해서는 해당 I2C 버스를 제어하는 i2C 어댑터와 i2c 알고리즘을 구현하는 소스를 커널 소스 내에 포함시켜야 한다. 프로그래머가 해야 할 작업은 바로 이 두 구조체와 관련된 루틴을 작성하는 것이다.
struct i2c_adapter
어댑터 구조체는 커널 소스상에 linux/include/linux/i2c.h에 선언되어 있다. 이 어댑터 구조체는 커널에 내부적으로 I2C 버스를 관리하기 위한 데이터와 관련된 정보를 담게 된다. 여러 필드가 있지만 반드시 구현해야 구조체 필드와 필요에 따라서 처리해야 하는 필드는 다음과 같다. 그 외의 필드는 커널 내부적으로 적절히 초기화되므로 프로그래머가 신경쓰지 않아도 된다.
필수 필드
◆ struct module *owner: 2.6 커널에서는 반드시 지정해야 하는 필드 변수이다. THIS_MODULE를 지정하면 된다.
◆ char name[I2C_NAME_SIZE] : 어댑터 명을 지정하는 필드 변수이다.
◆ struct i2c_algorithm *algo : I2C 버스를 제어하는 함수들을 등록한 구조체 주소를 지정한다. 이 부분에 대한 것은 struct i2c_algorithm를 설명하면서 자세하게 다룬다.
필요에 따라서 선언할 필요가 있는 필드들
◆ unsigned int id : 어댑터를 구별하기 위한 고유 식별 숫자를 지정한다. I2C_HW_로 시작하는 값을 지정하면 된다. 이 값은 linux/i2c-id.h에 정의되어 있다. 필요하다면 이 파일에 새로운 id를 선언하여 사용한다.
◆ unsigned int class : 어댑터의 클래스를 지정한다. I2C_CLASS_로 시작하는 값을 지정하면 된다. 이 값은 linux/i2c.h에 정의되어 있다. 필요하다면 이 파일에 새로운 클래스를 선언하여 사용한다.
◆ void *algo_data : struct i2c_algorithm에서 정의된 함수에 전달하여 관리가 필요한 데이터가 있을 경우 이 필드에 선언한다.
◆ int timeout : 하나의 데이터 블럭을 I2C 디바이스에 전송하거나 읽어 들이려 할 때의 처리가 끝날 때가지 지정해야 하는 대기 시간 초기 값이다.
◆ int retries : I2C 버스에서 에러가 발생할 경우에 반복할 값을 설정한다.
어댑터 구조체를 선언하고 관리하는 루틴의 예는 리눅스 커널 소스 디렉토리 중에 다음을 참고하면 된다.
linux/drivers/i2c/busses/
struct i2c_algorithm
i2c_algorithm 구조체는 실제로 I2C 버스상에 존재하는 디바이스에 데이터의 전송이 발생할 경우에 이를 처리하는 함수를 정의하고 이 구조체를 이용하여 등록한다. 여러 함수형이 정의되어 있지만 어댑터 구조체와 마찬가지로 반드시 정의해야 하는 필드가 있고 필요할 경우에 선언하는 것이 있다. 여기에 소개된 필드 이외에는 신경쓰지 않아도 된다.
필수 필드
◆ char name[32] : 알고리즘의 이름을 지정하는 필드 변수다.
◆ unsigned int id : 알고리즘을 구별하기 위한 고유 식별 숫자를 지정한다. I2C_ALGO_로 시작하는 값을 지정하면 된다. 이 값은 linux/i2c-id.h에 정의되어 있다. 필요하다면 이 파일에 새로운 id를 선언하여 사용한다.
◆ int (*master_xfer)(struct i2c_adapter *adap,struct i2c_msg msgs[], int num): 이 함수 필드 변수가 실제로 I2C의 버스를 이용하여 데이터를 디바이스에 전송하고 데이터를 처리하는 함수를 등록하는 필드 변수이다.
필요에 따라서 선언할 필요가 있는 필드들
◆ int (*algo_control)(struct i2c_adapter *, unsigned int, unsigned long): 알고리즘을 제어하기 위한 ioctl 확장이 필요하다면 이 함수를 선언해야 한다. 기본적인 I2C 제어 이외에 ioctl 명령을 확장하기 위해서 사용되므로 커널은 응용 프로그램에 의해서 ioctl 함수를 호출하면 디폴트로 이 함수를 호출하여 처리하도록 한다. 두 번째 매개변수가 ioctl에 전달된 명령을 받게 되고 세 번째 매개변수가 그 부가 정보를 위한 데이터를 전달받는다.
◆ u32 (*functionality) (struct i2c_adapter *) : 이 함수 필드는 알고리즘이 처리하는 기능에 대한 조회를 응용 프로그램이 요청했을 때 호출된다.
알고리즘은 커널 소스의 다음 디렉토리에 구현 예를 찾아 볼 수 있다.
linux/drivers/i2c/algos/
I2C 어댑터 디바이스 드라이버의 구조
지금까지 I2C 어댑터를 구현하기 위한 관련 구조체에 대하여 알아보았다. 그런데 실제 커널 소스를 보면 매우 복잡하게 되어 있다. 그렇다고 지면이 한정되어 있는 상황에 이에 대한 모든 것을 여기에 설명할 수는 없다. 그래서 아주 기본적으로 어댑터를 구현하는 기초적이고 개념적인 디바이스 드라이버를 여기에 소개하겠다. 물론 I2C 버스를 제어하기 위한 루틴은 여기에 소개하지는 않는다. 단지 I2C 어댑터 디바이스 드라이버를 이해하고 커널 소스에 구현된 내용을 이해하기 위한 핵심을 이해하기 위한 소스임을 기억하자.
static int iic_xxx_xfer(struct i2c_adapter *i2c_adap, struct i2c_msg msgs[], int num)
{
// I2C 버스 제어를 위한 구현 루틴
return ret;
}
static struct i2c_algorithm iic_xxx_algo =
{
.name = “Sample IIC algorithm”,
.id = I2C_ALGO_SAMPLE,
.master_xfer = iic_xxx_xfer,
};
static struct i2c_adapter iic_xxx_adapter =
{
.owner = THIS_MODULE,
.name = “Sample IIC adapter”,
.algo = &iic_xxx_algo,
};
static int __init iic_xxx_init(void)
{
iic_xxx_hw_init(); // I2C 버스를 제어하기 위한 하드웨어 초기화 루틴 메모리 할당, 인터럽트 등록 처리
i2c_add_adapter( &iic_xxx_adapter ); // 어댑터 등록
return 0;
}
static void iic_xxx_exit(void)
{
i2c_del_adapter(&iic_xxx_adapter); // 어댑터 등록 해제
iic_xxx_hw_release (); // I2C 버스를 종료하기 위한 하드웨어 셧다운 루틴 메모리 해제, 인터럽트 제거 처리
}
module_init(iic_xxx_init);
module_exit(iic_xxx_exit);
MODULE_AUTHOR(“you young-change <
frog@falinux.com>”);
MODULE_DESCRIPTION(“I2C-Bus adapter sample routines”);
MODULE_LICENSE(“GPL”);
이 소스의 함수 중에 xxx가 포함된 것은 만들어줘야 하는 함수임을 의미한다. 이 소스는 동작하는 것은 아니다. 단지 개념을 보여주기 위한 것이다. 이 소스에서 중요한 것은 iic_xxx_xfer 함수의 구현이다. 이 함수는 다음과 같이 세 가지 매개변수를 전달받는다.
int iic_xxx_xfer(struct i2c_adapter *i2c_adap, struct i2c_msg msgs[], int num)
앞에서 응용 프로그램을 설명할 때 read, write, ioctl을 이용하여 I2C 버스의 디바이스에 접근했는데 결국은 이 함수가 최종적으로 처리를 맡는다. read, 또는 write 함수는 num이 1로 전달되고 ioctl의 I2C_RDWD에 전달된 구조체는 각각 msgs 와 num에 각각 전달된다. 그래서 이 함수는 num만큼 반복적으로 적절히 msgs에 전달된 데이터를 처리하는 구조로 작성되어야 한다. 이 함수의 구현 방법은 I2C 버스의 이해와 함께 커널 소스를 참조하기 바란다.
준비된 알고리즘들
리눅스 커널에서 어댑터와 알고리즘을 분리한 이유는 알고리즘을 재사용하기 위해서이다. I2C 버스를 처리하는 경우는 크게 GPIO를 사용하여 구현하는 경우와 전용 I2C 컨트롤러를 이용하여 구현하는 경우로 나누어질 수 있다. 임베디드 시스템에서 가장 일반적으로 구현하는 방법은 GPIO를 사용하는 것이 보편적인데 이 때 사용 가능한 알고리즘은 linux/drivers/i2c/algos/i2c_algo-bit.c 소스에 구현된 것을 재사용하는 것이 좋다. 전용 컨트롤러를 사용하는 경우에는 기존 것을 재활용하는 것보다 다른 알고리즘을 구현한 소스를 보고 다시 구현하는 것이 좋다. 이 때 참고하는 가장 대표적인 것은 다음과 같다.
◆ i2c-algo-bit.c : GPIO 를 이용하여 구현하는 경우에 사용한다.
◆ i2c-algo-pcf.c : 전용 컨트롤러를 사용하여 구현하는 경우 참조할 수 있는 소스
◆ i2c-algo-ite.c : 전용 컨트롤러를 사용하여 구현하는 경우 참조할 수 있는 소스
I2C를 잘 다루길 바라며
지금까지 I2C 버스와 연결된 디바이스를 리눅스 운영체제에서 접근하기 위한 내용을 다루었다. 우선 지면 관계상 커널 내부의 I2C 버스에 관련된 부분을 대충 맛보기 정도에서 정리한 점에 대해 사과한다. 그러나 임베디드 리눅스를 다룬다고 해서 커널 내부적인 부분을 직접 수정하는 경우는 의외로 적다. 대부분의 경우 응용 프로그램 수준에서 모두 처리할 수 있다. 그 외의 부분이 필요한 경우는 어느 정도 새로운 시스템을 제작하는 몇몇의 프로그래머의 영역이라고 본다. 어쨌든 이번 글로 리눅스에서 I2C를 다루는 것에 도움이 되었으면 하는 것이 솔직한 필자의 소망이다. 마지막으로 I2C와 관련된 커널 소스상의 문서는 linux/Documentation/i2c를 참조하기 바란다. [maso]