Linux UART/SPI/I2C 드라이버
LinuxEmbedded
Linux에서 UART, SPI, I2C는 각각 별도의 서브시스템으로 관리된다. 세 버스 모두 Device Tree로 하드웨어를 기술하고, 커널 드라이버 또는 유저스페이스에서 접근할 수 있다.
UART
Device Tree
/ {
aliases {
serial0 = &uart0; /* → /dev/ttyS0 */
serial1 = &uart1; /* → /dev/ttyS1 */
};
};
&uart1 {
compatible = "brcm,bcm2835-aux-uart";
reg = <0x7e215040 0x40>;
interrupts = <1 29>;
clock-frequency = <48000000>;
status = "okay";
};
aliases의 serialN이 /dev/ttySN 번호를 결정한다.
유저스페이스 접근
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY);
struct termios opt;
tcgetattr(fd, &opt);
cfsetispeed(&opt, B115200);
cfsetospeed(&opt, B115200);
opt.c_cflag |= (CLOCAL | CREAD);
opt.c_cflag &= ~PARENB; /* no parity */
opt.c_cflag &= ~CSTOPB; /* 1 stop bit */
opt.c_cflag = (opt.c_cflag & ~CSIZE) | CS8;
opt.c_lflag &= ~(ICANON | ECHO | ISIG);
opt.c_oflag &= ~OPOST;
tcsetattr(fd, TCSANOW, &opt);
write(fd, "AT\r\n", 4);
char buf[64];
int n = read(fd, buf, sizeof(buf) - 1);
buf[n] = '\0';
close(fd);
핵심은 termios 설정이다. raw 모드 (ICANON 끄기)로 해야 바이너리 프로토콜을 처리할 수 있다.
SPI
Device Tree
&spi0 {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
/* 유저스페이스 접근용 (spidev) */
spidev0: spidev@0 {
compatible = "rohm,dh2228fv";
reg = <0>; /* CS0 */
spi-max-frequency = <1000000>;
};
/* 커널 드라이버 매칭 */
accel@1 {
compatible = "adi,adxl345";
reg = <1>; /* CS1 */
spi-max-frequency = <5000000>;
interrupt-parent = <&gpio>;
interrupts = <25 IRQ_TYPE_EDGE_RISING>;
};
};
reg가 chip select 번호다. spi-max-frequency는 해당 디바이스의 최대 클럭.
커널 SPI 드라이버
#include <linux/spi/spi.h>
static int my_spi_read_reg(struct spi_device *spi, u8 reg, u8 *val)
{
u8 tx[2] = { reg | 0x80, 0x00 }; /* MSB=1: read */
u8 rx[2] = { 0 };
struct spi_transfer xfer = {
.tx_buf = tx,
.rx_buf = rx,
.len = 2,
};
struct spi_message msg;
spi_message_init(&msg);
spi_message_add_tail(&xfer, &msg);
spi_sync(spi, &msg);
*val = rx[1];
return 0;
}
static int my_probe(struct spi_device *spi)
{
u8 id;
my_spi_read_reg(spi, 0x00, &id);
dev_info(&spi->dev, "chip id: 0x%02x\n", id);
return 0;
}
static const struct of_device_id my_of_match[] = {
{ .compatible = "adi,adxl345" },
{ }
};
static struct spi_driver my_drv = {
.driver = {
.name = "adxl345",
.of_match_table = my_of_match,
},
.probe = my_probe,
};
module_spi_driver(my_drv);
MODULE_LICENSE("GPL");
spi_sync는 블로킹이다. 인터럽트 컨텍스트에서는 spi_async를 사용한다.
유저스페이스 접근 (/dev/spidevN.M)
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>
int fd = open("/dev/spidev0.0", O_RDWR);
uint8_t mode = SPI_MODE_0;
uint32_t speed = 1000000;
ioctl(fd, SPI_IOC_WR_MODE, &mode);
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
uint8_t tx[] = { 0x80, 0x00 };
uint8_t rx[2] = { 0 };
struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)tx,
.rx_buf = (unsigned long)rx,
.len = 2,
.speed_hz = speed,
.bits_per_word = 8,
};
ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
printf("reg 0x00 = 0x%02x\n", rx[1]);
close(fd);
SPI_IOC_MESSAGE(N)에서 N은 동시에 전송할 transfer 수다. CS는 전체 메시지 동안 유지된다.
I2C
Device Tree
&i2c1 {
#address-cells = <1>;
#size-cells = <0>;
clock-frequency = <100000>; /* 100kHz standard mode */
status = "okay";
temp_sensor: lm75@48 {
compatible = "national,lm75";
reg = <0x48>; /* I2C 슬레이브 주소 */
};
eeprom@50 {
compatible = "atmel,24c256";
reg = <0x50>;
pagesize = <64>;
};
};
reg가 I2C 슬레이브 주소다. 7-bit 주소를 사용한다 (0x48은 7-bit).
커널 I2C 드라이버
#include <linux/i2c.h>
static int my_probe(struct i2c_client *client)
{
s32 val;
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_WORD_DATA))
return -ENODEV;
val = i2c_smbus_read_word_data(client, 0x00);
if (val < 0)
return val;
/* LM75: 온도 = (MSB:LSB >> 7) × 0.5°C */
int temp = (s16)swab16(val) >> 7;
dev_info(&client->dev, "temp: %d.%d C\n", temp / 2, (temp & 1) * 5);
return 0;
}
static const struct of_device_id my_of_match[] = {
{ .compatible = "national,lm75" },
{ }
};
static struct i2c_driver my_drv = {
.driver = {
.name = "lm75",
.of_match_table = my_of_match,
},
.probe = my_probe,
};
module_i2c_driver(my_drv);
MODULE_LICENSE("GPL");
i2c_smbus_* 함수가 SMBus 프로토콜을 추상화한다. 대부분의 I2C 센서는 SMBus로 충분하다.
유저스페이스 접근 (/dev/i2c-N)
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
int fd = open("/dev/i2c-1", O_RDWR);
ioctl(fd, I2C_SLAVE, 0x48);
/* SMBus word read */
uint8_t reg = 0x00;
write(fd, ®, 1);
uint8_t buf[2];
read(fd, buf, 2);
int raw = (buf[0] << 8) | buf[1];
int temp = (raw >> 7) * 5; /* 0.5°C 단위 → 0.1°C */
printf("temp: %d.%d C\n", temp / 10, temp % 10);
close(fd);
i2c-tools
i2cdetect -l # 버스 목록
i2cdetect -y 1 # 버스 1에서 디바이스 스캔
i2cget -y 1 0x48 0x00 w # 워드 읽기
i2cset -y 1 0x48 0x01 0x00 b # 바이트 쓰기
i2cdump -y 1 0x48 b # 전체 레지스터 덤프
세 버스 비교
| UART | SPI | I2C | |
|---|---|---|---|
| 와이어 수 | 2 (TX/RX) | 4+ (MOSI/MISO/CLK/CS) | 2 (SDA/SCL) |
| 토폴로지 | Point-to-point | 버스 (CS로 선택) | 버스 (주소로 선택) |
| 속도 | ~115.2 Kbps 일반 | ~10 MHz+ | 100K/400K/1MHz |
DT reg 의미 | 없음 | CS 번호 | 슬레이브 주소 |
| 유저스페이스 | /dev/ttyS* | /dev/spidevN.M | /dev/i2c-N |
| 커널 드라이버 매크로 | - | module_spi_driver | module_i2c_driver |
| 전이중 | O | O | X (half-duplex) |
메모
- spidev의 compatible 문자열
compatible = "spidev"를 쓰면 커널이 경고를 출력한다."rohm,dh2228fv"같은 구체적 compatible을 사용한다- spidev는 개발/테스트용이다. 프로덕션에서는 커널 드라이버를 작성하는 것이 권장된다
- I2C 주소 충돌
- 같은 버스에 같은 주소의 디바이스 두 개를 연결하면 데이터가 깨진다
- 많은 I2C 칩이 A0/A1/A2 핀으로 주소 하위 비트를 변경할 수 있다
i2cdetect로 버스 스캔 시 일부 디바이스가 의도치 않게 응답할 수 있다. 센서류는 스캔에 안전하지만, EEPROM은 스캔 시 데이터가 변경될 수 있다
- regmap
- SPI와 I2C 모두
regmap추상화를 사용할 수 있다. 레지스터 읽기/쓰기 패턴이 동일해지므로 동일 센서의 SPI/I2C 버전을 하나의 드라이버로 지원 가능 devm_regmap_init_spi(spi, &config)/devm_regmap_init_i2c(client, &config)
- SPI와 I2C 모두
- UART는 서브시스템 구조가 다르다
- SPI, I2C는
spi_driver,i2c_driver로 등록하고 Device Tree에서 매칭한다 - UART 디바이스(예: GPS 모듈)는
serdev(serial device) 프레임워크를 사용한다. 기존 tty 레이어 위에 Device Tree 매칭을 추가한 것이다
- SPI, I2C는
- DMA 전송
- SPI 대용량 전송 시 DMA를 사용하면 CPU 부하를 줄일 수 있다. 컨트롤러 드라이버가 DMA를 지원하면
spi_sync가 자동으로 DMA를 사용한다 - 일정 바이트 이상일 때만 DMA가 효율적이다. 임계값은 컨트롤러마다 다르지만 보통 64~256 바이트
- SPI 대용량 전송 시 DMA를 사용하면 CPU 부하를 줄일 수 있다. 컨트롤러 드라이버가 DMA를 지원하면