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";
};

aliasesserialN/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, &reg, 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      # 전체 레지스터 덤프

세 버스 비교

UARTSPII2C
와이어 수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_drivermodule_i2c_driver
전이중OOX (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)
  • UART는 서브시스템 구조가 다르다
    • SPI, I2C는 spi_driver, i2c_driver로 등록하고 Device Tree에서 매칭한다
    • UART 디바이스(예: GPS 모듈)는 serdev (serial device) 프레임워크를 사용한다. 기존 tty 레이어 위에 Device Tree 매칭을 추가한 것이다
  • DMA 전송
    • SPI 대용량 전송 시 DMA를 사용하면 CPU 부하를 줄일 수 있다. 컨트롤러 드라이버가 DMA를 지원하면 spi_sync가 자동으로 DMA를 사용한다
    • 일정 바이트 이상일 때만 DMA가 효율적이다. 임계값은 컨트롤러마다 다르지만 보통 64~256 바이트