SPI 서브시스템 — SPI / QSPI

임베디드 Linux에서 고속 직렬 통신에 사용되는 SPI 서브시스템을 드라이버 개발 관점으로 정리합니다. SPI 프로토콜과 CPOL/CPHA 모드, spi_controller/spi_device 구조체, FIFO/DMA 기반 전송 경로, Quad/Octal SPI 및 XIP, Device Tree 바인딩, regmap을 활용한 레지스터(Register) 추상화까지 SPI 드라이버 개발에 필요한 핵심을 다룹니다.

전제 조건: 디바이스 드라이버인터럽트(Interrupt) 문서를 먼저 읽으세요. 버스/열거/프로브(Probe) 경로는 초기화 순서와 자원 등록 규칙이 핵심이므로, 장치 발견부터 바인딩까지 흐름을 먼저 고정해야 합니다. I2C는 I2C 서브시스템, GPIO는 GPIO 서브시스템 문서를 참조하세요.
일상 비유: SPI 통신은 고속도로 하이패스(Hi-pass) 시스템과 비슷합니다. 전용 게이트(CS)를 열고, 양방향으로 동시에 데이터를 주고받으며(전이중), 클럭 신호에 맞춰 정확한 타이밍으로 통과합니다.

핵심 요약

  • 전이중 직렬 통신 — MOSI/MISO로 동시에 송수신하며, CS로 슬레이브를 선택합니다.
  • CPOL/CPHA 모드 — 클럭 극성과 위상 조합으로 4가지 모드를 지원합니다.
  • FIFO/DMA 전송 — 대용량 전송 시 DMA를 활용하여 CPU 부하를 줄입니다.
  • Quad/Octal SPI — 다중 데이터 라인으로 대역폭을 확장합니다.
  • regmap 추상화 — 버스 종류에 무관한 레지스터 접근 API를 제공합니다.

단계별 이해

  1. SPI 프로토콜 이해
    신호선(MOSI/MISO/SCK/CS)과 CPOL/CPHA 모드를 파악합니다.
  2. 커널 구조체 학습
    spi_controller, spi_device, spi_transfer, spi_message 관계를 이해합니다.
  3. 드라이버 작성
    probe/remove, 전송 API(spi_sync, spi_async)를 구현합니다.
  4. Device Tree 바인딩
    spi-max-frequency, spi-cpol, spi-cpha 등 프로퍼티를 설정합니다.
관련 표준: SPI (Motorola/de facto) — 이 문서에서 다루는 버스 프로토콜의 기반 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
SPI Controller spi_controller MOSI (Master Out) MISO (Master In) SCLK (Clock) CS0 / CS1 / CS2 SPI Device 0 spi_device (CS0) SPI Device 1 spi_device (CS1) MOSI/MISO/SCLK MOSI/MISO/SCLK CS0 CS1

SPI 프로토콜 기초

SPI (Serial Peripheral Interface)는 Motorola가 개발한 전이중(full-duplex) 동기 직렬 버스입니다. I2C보다 빠른 속도가 필요한 ADC, DAC, 디스플레이, Flash 메모리 등에 사용됩니다.

신호선별칭역할
MOSISDO, COPI, DIMaster Out Slave In
MISOSDI, CIPO, DOMaster In Slave Out
SCKSCLK, CLKSerial Clock (마스터 생성)
CS/SSNSS, CEChip Select (Active Low)

SPI 모드 (CPOL/CPHA)

SPI는 클럭 극성(CPOL)과 클럭 위상(CPHA) 조합으로 4가지 동작 모드를 정의합니다:

모드CPOLCPHA유휴 클럭데이터 샘플링
Mode 000LOW상승 에지
Mode 101LOW하강 에지
Mode 210HIGH하강 에지
Mode 311HIGH상승 에지
SPI vs I2C 선택 기준: SPI는 전이중 통신, 높은 클럭 속도(수십 MHz), 단순한 프로토콜이 장점이지만, 디바이스당 전용 CS 라인이 필요하여 핀 수가 증가합니다. I2C는 2선으로 다수 디바이스를 연결할 수 있지만 속도가 제한됩니다. I2C에 대한 자세한 내용은 I2C 서브시스템 문서를 참조하세요.

SPI 모드별 파형과 클럭 에지 동작

SPI의 4가지 모드는 CPOL(Clock Polarity)과 CPHA(Clock Phase) 비트 조합으로 결정됩니다. 데이터 시트에서 "상승 에지에서 샘플링"이라고 적혀 있다면 Mode 0 또는 Mode 3에 해당합니다. 아래 다이어그램은 각 모드의 SCK/MOSI/MISO 타이밍 관계를 보여줍니다.

SPI Mode 0~3: SCK / MOSI / MISO Timing Mode 0 (CPOL=0, CPHA=0) 유휴 LOW, 상승 에지 샘플링 CS SCK MOSI ● = 샘플링 포인트 (상승 에지) Mode 1 (CPOL=0, CPHA=1) 유휴 LOW, 하강 에지 샘플링 CS SCK MOSI ● = 샘플링 포인트 (하강 에지) Mode 2 (CPOL=1, CPHA=0) 유휴 HIGH, 하강 에지 샘플링 CS SCK MOSI ● = 샘플링 포인트 (하강 에지) Mode 3 (CPOL=1, CPHA=1) 유휴 HIGH, 상승 에지 샘플링 CS SCK MOSI ● = 샘플링 포인트 (상승 에지) 모드별 에지 동작 정리 Mode 유휴 SCK 데이터 출력(전환) 데이터 샘플링 대표 디바이스 Mode 0 LOW 하강 에지 상승 에지 대부분의 ADC, 센서 Mode 1 LOW 상승 에지 하강 에지 일부 DAC, 디스플레이 Mode 2 HIGH 상승 에지 하강 에지 일부 SPI Flash Mode 3 HIGH 하강 에지 상승 에지 SPI NOR Flash, SD 카드 핵심: CPHA=0이면 CS 활성화 직후 첫 에지에서 샘플링, CPHA=1이면 두 번째 에지에서 샘플링 Mode 0과 Mode 3이 가장 널리 사용됨 (둘 다 상승 에지에서 샘플링하지만 유휴 클럭 극성이 다름) Linux 커널: spi-cpol = CPOL=1, spi-cpha = CPHA=1 (Device Tree 프로퍼티)

Setup Time과 Hold Time

SPI 통신의 신뢰성은 setup time(tSU)과 hold time(tHD)에 의존합니다. Setup time은 샘플링 에지 전에 데이터가 안정되어야 하는 최소 시간이고, hold time은 샘플링 에지 후에 데이터가 유지되어야 하는 최소 시간입니다:

파라미터기호일반 범위설명
Setup Time (데이터)tSU5~15 ns샘플링 에지 전 데이터 안정 시간
Hold Time (데이터)tHD5~10 ns샘플링 에지 후 데이터 유지 시간
CS SetuptCSS10~50 nsCS 활성화 후 첫 클럭까지 대기
CS HoldtCSH10~50 ns마지막 클럭 후 CS 비활성화까지
CS 비활성 간격tCSI50~100 ns연속 트랜잭션 사이 CS 최소 비활성 시간

전이중(Full Duplex) vs 반이중(Half Duplex)

SPI는 기본적으로 전이중(Full Duplex) 통신을 지원합니다. 마스터가 MOSI로 데이터를 송신하는 동시에 MISO로 데이터를 수신합니다. 그러나 일부 디바이스는 단방향 또는 반이중 모드로 동작합니다:

통신 방식데이터 라인동작Linux 커널 플래그
Full DuplexMOSI + MISO동시 양방향 전송(기본값)
Half Duplex단일 양방향 라인교대 송수신 (3-wire SPI)SPI_3WIRE
Simplex TXMOSI만송신 전용 (예: LED 드라이버)SPI_NO_RX
Simplex RXMISO만수신 전용 (예: ADC)SPI_NO_TX

전기적 특성과 신호 무결성

고속 SPI(10 MHz 이상)에서는 신호 무결성이 중요합니다. PCB 설계 시 다음 사항을 고려해야 합니다:

항목권장 값설명
배선 길이< 10 cm (50 MHz 기준)짧을수록 반사와 크로스톡 감소
임피던스 매칭50 Ω (단선) / 100 Ω (차동)고속에서 반사 최소화
풀업 저항 (CS)10 kΩ미사용 CS 라인은 풀업하여 비활성 유지
바이패스 커패시터100 nF + 10 μFVCC 핀 가까이 배치하여 노이즈 감소
Ground 플레인연속 GND 레이어신호 리턴 경로 확보, EMI 감소
클럭 속도 한계: SPI는 공식 속도 규격이 없지만, 실무에서 일반 SPI Flash는 50~133 MHz, ADC/DAC는 1~50 MHz, 디스플레이 컨트롤러는 10~80 MHz 범위로 동작합니다. 실제 최대 속도는 슬레이브 디바이스의 데이터 시트, PCB 배선 품질, 그리고 SPI 컨트롤러의 하드웨어 능력에 의해 결정됩니다.

Linux SPI 서브시스템

SPI 서브시스템은 drivers/spi/에 구현되어 있으며 다음 핵심 구조체를 사용합니다:

구조체역할헤더
spi_controllerSPI 마스터 (호스트) 컨트롤러<linux/spi/spi.h>
spi_deviceSPI 버스 상의 슬레이브 디바이스<linux/spi/spi.h>
spi_driverSPI 디바이스 드라이버<linux/spi/spi.h>
spi_messageSPI 트랜잭션 (transfer 묶음)<linux/spi/spi.h>
spi_transfer단일 전이중 전송 단위<linux/spi/spi.h>

spi_controller 구조

SPI 컨트롤러 드라이버의 핵심 콜백:

struct spi_controller {
    int (*setup)(struct spi_device *spi);
    int (*transfer_one_message)(struct spi_controller *ctlr,
                                 struct spi_message *msg);
    int (*transfer_one)(struct spi_controller *ctlr,
                         struct spi_device *spi,
                         struct spi_transfer *xfer);
    void (*set_cs)(struct spi_device *spi, bool enable);
    u32 min_speed_hz;
    u32 max_speed_hz;
    u16 num_chipselect;
    /* ... */
};

SPI 컨트롤러 레지스터 프로그래밍

SPI 컨트롤러 드라이버는 하드웨어 레지스터를 직접 프로그래밍하여 클럭 분주, FIFO 관리, DMA 채널 설정을 수행합니다. 아래 다이어그램은 SPI 컨트롤러 내부의 전송 데이터 경로를 보여줍니다.

SPI Controller Transfer Flow (TX/RX Data Path) CPU (PIO) 레지스터 Write DMA Engine TX DMA Channel TX FIFO Watermark: N entries FIFO 깊이: 16~256 TXFIFOLVL IRQ Shift Register bits_per_word (8/16/32 bit) Clock Divider f_sck = f_pclk / (2 × (div+1)) MOSI Pad 직렬 출력 MISO Pad 직렬 입력 SCK Pad SPI Slave Device RX FIFO Watermark: M entries Overflow 보호 RXFIFOLVL IRQ DMA Engine RX DMA Channel CPU (PIO) 레지스터 Read RX data RX Shift Reg 병렬→FIFO 저장 TX 경로 RX 경로 Clock 분배 DMA

클럭 분주(Divider) 계산

SPI 컨트롤러의 SCK 클럭 주파수는 부모 클럭(PCLK 또는 APB 클럭)을 분주하여 생성합니다. 대부분의 컨트롤러는 짝수 분주를 사용하며, 실제 속도가 요청 속도를 초과하지 않도록 올림 처리해야 합니다:

/* SPI 클럭 분주 계산: f_sck = f_pclk / (2 * (div + 1)) */
static u32 calc_spi_clk_div(u32 pclk_hz, u32 target_hz)
{
    u32 div;

    if (target_hz >= pclk_hz / 2)
        return 0;  /* 최소 분주 = 2배 */

    /* div = ceil(pclk / (2 * target)) - 1
     * 실제 속도가 target을 넘지 않도록 올림 처리 */
    div = DIV_ROUND_UP(pclk_hz, 2 * target_hz) - 1;

    /* 하드웨어 최대값 클램프 (예: 8-bit divider → 0~255) */
    return min_t(u32, div, 0xFF);
}

FIFO 관리와 Watermark 설정

TX/RX FIFO의 watermark 레벨은 인터럽트 빈도와 전송 효율의 균형을 결정합니다. Watermark가 너무 낮으면 인터럽트가 과다 발생하고, 너무 높으면 RX FIFO 오버플로 위험이 있습니다:

Watermark 전략TX WatermarkRX Watermark특성
낮은 지연FIFO_DEPTH / 41인터럽트 빈번, 응답 빠름
균형FIFO_DEPTH / 2FIFO_DEPTH / 2범용 설정
높은 처리량(Throughput)FIFO_DEPTH * 3/4FIFO_DEPTH * 3/4DMA 연동 시 최적

SPI 컨트롤러 드라이버 스켈레톤

#include <linux/spi/spi.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <linux/dmaengine.h>

#define SPI_CR1       0x00   /* Control Register 1 */
#define SPI_CR2       0x04   /* Control Register 2 */
#define SPI_SR        0x08   /* Status Register */
#define SPI_DR        0x0C   /* Data Register */
#define SPI_TXFR      0x10   /* TX FIFO Threshold */
#define SPI_RXFR      0x14   /* RX FIFO Threshold */

#define SPI_CR1_SPE   BIT(0)  /* SPI Enable */
#define SPI_CR1_MSTR  BIT(2)  /* Master Mode */
#define SPI_CR1_CPOL  BIT(3)  /* Clock Polarity */
#define SPI_CR1_CPHA  BIT(4)  /* Clock Phase */
#define SPI_SR_TXE    BIT(1)  /* TX FIFO Empty */
#define SPI_SR_RXNE   BIT(0)  /* RX FIFO Not Empty */
#define SPI_SR_BSY    BIT(7)  /* Busy */

struct my_spi_priv {
    void __iomem *base;
    struct clk *clk;
    int irq;
    struct completion xfer_done;
    const u8 *tx_buf;
    u8 *rx_buf;
    int tx_len, rx_len;
};

static int my_spi_transfer_one(struct spi_controller *ctlr,
                                struct spi_device *spi,
                                struct spi_transfer *xfer)
{
    struct my_spi_priv *priv = spi_controller_get_devdata(ctlr);
    u32 cr1, div;

    /* 클럭 분주 설정 */
    div = calc_spi_clk_div(clk_get_rate(priv->clk), xfer->speed_hz);
    cr1 = readl(priv->base + SPI_CR1);
    cr1 &= ~(0xFF << 8);      /* divider 필드 클리어 */
    cr1 |= (div << 8);          /* 새 divider 설정 */

    /* SPI 모드 설정 (CPOL/CPHA) */
    if (spi->mode & SPI_CPOL)
        cr1 |= SPI_CR1_CPOL;
    else
        cr1 &= ~SPI_CR1_CPOL;
    if (spi->mode & SPI_CPHA)
        cr1 |= SPI_CR1_CPHA;
    else
        cr1 &= ~SPI_CR1_CPHA;

    writel(cr1 | SPI_CR1_SPE, priv->base + SPI_CR1);

    /* FIFO watermark 설정 (깊이의 절반) */
    writel(8, priv->base + SPI_TXFR);  /* TX threshold */
    writel(8, priv->base + SPI_RXFR);  /* RX threshold */

    /* 전송 시작: PIO 또는 DMA */
    priv->tx_buf = xfer->tx_buf;
    priv->rx_buf = xfer->rx_buf;
    priv->tx_len = xfer->len;
    priv->rx_len = xfer->len;
    reinit_completion(&priv->xfer_done);

    /* IRQ 기반 PIO: TX empty 인터럽트 활성화 */
    writel(readl(priv->base + SPI_CR2) | BIT(0),
           priv->base + SPI_CR2);

    return 1;  /* 비동기: completion 대기 */
}

SPI DMA 통합

대용량 SPI 전송(수백 바이트 이상)에서는 DMA를 사용하여 CPU 부하를 줄입니다. DMA 버퍼(Buffer)는 캐시 라인(Cache Line) 정렬이 필수이며, scatter-gather 리스트로 비연속 메모리를 처리할 수 있습니다:

DMA 버퍼 정렬: DMA 전송에 사용하는 버퍼는 반드시 캐시 라인에 정렬되어야 합니다. 스택 변수나 구조체 중간의 버퍼를 DMA에 사용하면 캐시 일관성(Cache Coherency) 문제로 데이터 손상이 발생할 수 있습니다. ____cacheline_aligned 속성이나 kmalloc()/dma_alloc_coherent()를 사용하세요.
/* SPI DMA 전송 설정 */
static int my_spi_dma_xfer(struct my_spi_priv *priv,
                             struct spi_transfer *xfer)
{
    struct dma_async_tx_descriptor *tx_desc, *rx_desc;
    dma_addr_t tx_dma, rx_dma;
    struct device *dev = priv->dev;

    /* TX: 메모리 → 주변장치 (MEM_TO_DEV) */
    tx_dma = dma_map_single(dev, (void *)xfer->tx_buf,
                             xfer->len, DMA_TO_DEVICE);
    if (dma_mapping_error(dev, tx_dma))
        return -ENOMEM;

    /* RX: 주변장치 → 메모리 (DEV_TO_MEM) */
    rx_dma = dma_map_single(dev, xfer->rx_buf,
                             xfer->len, DMA_FROM_DEVICE);
    if (dma_mapping_error(dev, rx_dma)) {
        dma_unmap_single(dev, tx_dma, xfer->len, DMA_TO_DEVICE);
        return -ENOMEM;
    }

    /* RX DMA 먼저 준비 (데이터 유실 방지) */
    rx_desc = dmaengine_prep_slave_single(
        priv->rx_chan, rx_dma, xfer->len,
        DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
    rx_desc->callback = my_spi_dma_rx_done;
    rx_desc->callback_param = priv;
    dmaengine_submit(rx_desc);
    dma_async_issue_pending(priv->rx_chan);

    /* TX DMA 시작 → SCK 생성 → 수신 자동 진행 */
    tx_desc = dmaengine_prep_slave_single(
        priv->tx_chan, tx_dma, xfer->len,
        DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
    tx_desc->callback = my_spi_dma_tx_done;
    tx_desc->callback_param = priv;
    dmaengine_submit(tx_desc);
    dma_async_issue_pending(priv->tx_chan);

    return 0;
}

칩 셀렉트(CS) 관리

SPI 컨트롤러의 CS(Chip Select) 관리는 하드웨어 CS와 GPIO CS 두 가지 방식이 있습니다. 다중 슬레이브 환경에서는 CS 타이밍이 정확해야 버스 충돌을 방지할 수 있습니다:

방식장점단점용도
HW CS정확한 타이밍, 자동 토글제한된 CS 수 (보통 2~4개)고속 전송, 정밀 타이밍
GPIO CS무제한 CS, 유연한 핀 배치소프트웨어 지연, 약간의 오버헤드(Overhead)다수 슬레이브, 비표준 핀

spi_transfer 구조체의 cs_change 필드는 전송 간 CS 동작을 제어합니다. 이 필드가 설정되면 해당 transfer 종료 후 CS를 잠시 비활성화(deassert)했다가 다시 활성화합니다. 이는 일부 슬레이브 디바이스가 명령 사이에 CS 토글을 요구하는 경우에 필요합니다:

/* cs_change를 활용한 다중 명령 전송 */
static int spi_multi_cmd(struct spi_device *spi,
                          u8 *cmd1, size_t len1,
                          u8 *cmd2, size_t len2)
{
    struct spi_transfer xfer[2] = { };
    struct spi_message msg;

    /* Transfer 1: 첫 번째 명령 후 CS 토글 */
    xfer[0].tx_buf = cmd1;
    xfer[0].len = len1;
    xfer[0].cs_change = 1;  /* 이 transfer 후 CS deassert→reassert */
    xfer[0].cs_change_delay.value = 10;
    xfer[0].cs_change_delay.unit = SPI_DELAY_UNIT_USECS;

    /* Transfer 2: 두 번째 명령 (마지막이므로 CS 자동 해제) */
    xfer[1].tx_buf = cmd2;
    xfer[1].len = len2;

    spi_message_init_with_transfers(&msg, xfer, ARRAY_SIZE(xfer));
    return spi_sync(spi, &msg);
}
GPIO CS 설정: Device Tree에서 cs-gpios 프로퍼티로 GPIO 기반 CS를 지정합니다: cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;. SPI 프레임워크가 자동으로 GPIO를 assert/deassert하며, 컨트롤러 드라이버에서 set_cs 콜백을 구현하지 않아도 됩니다. GPIO에 대한 자세한 내용은 GPIO 서브시스템 문서를 참조하세요.

SPI 드라이버 작성

SPI ADC (Analog-to-Digital Converter) 드라이버 예제:

#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/iio/iio.h>

#define ADC_CMD_READ_CH0  0x06
#define ADC_CMD_READ_CH1  0x07

struct my_adc_data {
    struct spi_device *spi;
    struct mutex lock;
    u8 tx_buf[3] ____cacheline_aligned;
    u8 rx_buf[3];
};

static int my_adc_read_channel(struct my_adc_data *data, int channel)
{
    struct spi_transfer xfer = {
        .tx_buf = data->tx_buf,
        .rx_buf = data->rx_buf,
        .len = 3,
        .speed_hz = 1000000,  /* 1 MHz */
    };
    int ret;

    data->tx_buf[0] = (channel == 0) ? ADC_CMD_READ_CH0 : ADC_CMD_READ_CH1;
    data->tx_buf[1] = 0x00;
    data->tx_buf[2] = 0x00;

    ret = spi_sync_transfer(data->spi, &xfer, 1);
    if (ret)
        return ret;

    /* 12비트 ADC: 상위 4비트 버림 */
    return ((data->rx_buf[1] & 0x0F) << 8) | data->rx_buf[2];
}

static int my_adc_read_raw(struct iio_dev *indio_dev,
                            struct iio_chan_spec const *chan,
                            int *val, int *val2, long mask)
{
    struct my_adc_data *data = iio_priv(indio_dev);
    int ret;

    switch (mask) {
    case IIO_CHAN_INFO_RAW:
        mutex_lock(&data->lock);
        ret = my_adc_read_channel(data, chan->channel);
        mutex_unlock(&data->lock);
        if (ret < 0)
            return ret;
        *val = ret;
        return IIO_VAL_INT;
    case IIO_CHAN_INFO_SCALE:
        /* Vref=3.3V, 12-bit: 3300/4096 = 0.805664 mV/LSB */
        *val = 3300;
        *val2 = 12;
        return IIO_VAL_FRACTIONAL_LOG2;
    default:
        return -EINVAL;
    }
}

static const struct iio_chan_spec my_adc_channels[] = {
    {
        .type = IIO_VOLTAGE,
        .channel = 0,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
    },
    {
        .type = IIO_VOLTAGE,
        .channel = 1,
        .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
        .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
    },
};

static const struct iio_info my_adc_info = {
    .read_raw = my_adc_read_raw,
};

static int my_adc_probe(struct spi_device *spi)
{
    struct iio_dev *indio_dev;
    struct my_adc_data *data;

    indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*data));
    if (!indio_dev)
        return -ENOMEM;

    data = iio_priv(indio_dev);
    data->spi = spi;
    mutex_init(&data->lock);

    indio_dev->name = "my-adc";
    indio_dev->info = &my_adc_info;
    indio_dev->channels = my_adc_channels;
    indio_dev->num_channels = ARRAY_SIZE(my_adc_channels);
    indio_dev->modes = INDIO_DIRECT_MODE;

    return devm_iio_device_register(&spi->dev, indio_dev);
}

static const struct spi_device_id my_adc_spi_id[] = {
    { "my-adc", 0 },
    { }
};
MODULE_DEVICE_TABLE(spi, my_adc_spi_id);

static const struct of_device_id my_adc_of_match[] = {
    { .compatible = "vendor,my-adc" },
    { }
};
MODULE_DEVICE_TABLE(of, my_adc_of_match);

static struct spi_driver my_adc_driver = {
    .driver = {
        .name = "my-adc",
        .of_match_table = my_adc_of_match,
    },
    .probe = my_adc_probe,
    .id_table = my_adc_spi_id,
};
module_spi_driver(my_adc_driver);
DMA 정렬: SPI 전송 버퍼가 DMA를 사용할 수 있으므로, tx_buf/rx_buf를 스택에 할당하면 안 됩니다. 구조체 멤버로 선언하거나 kmalloc()으로 할당하고, ____cacheline_aligned 속성을 적용하세요.

spi_message와 spi_transfer

복잡한 SPI 트랜잭션은 spi_message에 여러 spi_transfer를 연결하여 구성합니다:

/* 명령어 전송 후 데이터 수신 (CS 유지) */
static int spi_flash_read(struct spi_device *spi,
                           u32 addr, u8 *buf, size_t len)
{
    struct spi_message msg;
    struct spi_transfer xfer[2];
    u8 cmd[4];

    cmd[0] = 0x03;  /* READ 명령 */
    cmd[1] = (addr >> 16) & 0xFF;
    cmd[2] = (addr >> 8)  & 0xFF;
    cmd[3] = addr & 0xFF;

    memset(xfer, 0, sizeof(xfer));

    /* Transfer 1: 명령 + 주소 전송 */
    xfer[0].tx_buf = cmd;
    xfer[0].len = 4;

    /* Transfer 2: 데이터 수신 */
    xfer[1].rx_buf = buf;
    xfer[1].len = len;

    spi_message_init(&msg);
    spi_message_add_tail(&xfer[0], &msg);
    spi_message_add_tail(&xfer[1], &msg);

    return spi_sync(spi, &msg);
}

QSPI / Dual / Quad SPI

고속 SPI NOR Flash 등은 표준 SPI 외에 Dual (2-bit), Quad (4-bit), Octal (8-bit) I/O를 지원합니다. Linux 커널은 spi-mem 레이어를 통해 이를 추상화합니다.

모드데이터 라인명령 전송대표 디바이스
Standard SPI1-bit (MOSI/MISO)1-bit일반 SPI Flash
Dual Output2-bit1-bitW25Q series
Quad Output4-bit1-bitW25Q series
QPI (Quad I/O)4-bit4-bit고속 NOR Flash
Octal (8D-8D-8D)8-bit DDR8-bit DDRMacronix MX25

spi-mem 프레임워크

spi-mem은 메모리형 SPI 디바이스를 위한 표준화된 인터페이스입니다:

#include <linux/spi/spi-mem.h>

/* spi_mem_op: 명령/주소/더미/데이터 단계를 분리하여 기술 */
struct spi_mem_op op = SPI_MEM_OP(
    SPI_MEM_OP_CMD(0xEB, 1),           /* Quad I/O Fast Read, 1-wire cmd */
    SPI_MEM_OP_ADDR(3, addr, 4),       /* 3-byte addr, 4-wire */
    SPI_MEM_OP_DUMMY(6, 4),            /* 6 dummy cycles, 4-wire */
    SPI_MEM_OP_DATA_IN(len, buf, 4)   /* 데이터 수신, 4-wire */
);

ret = spi_mem_exec_op(spi_mem, &op);
SPI NOR 프레임워크: drivers/mtd/spi-nor/의 SPI NOR 프레임워크는 spi-mem 위에서 동작하며, JEDEC 표준 명령어 셋을 자동으로 처리합니다. 새 Flash 칩 지원은 벤더별 파일에 파라미터만 추가하면 됩니다.

Quad SPI: DDR 모드와 XIP

고성능 SPI NOR Flash는 기본 Quad I/O를 넘어 DDR (Double Data Rate) 모드, Data Strobe (DQS), XIP (eXecute-In-Place) 등 고급 기능을 지원합니다. 아래 다이어그램은 Quad SPI 명령의 4단계 구조를 보여줍니다.

Quad SPI Command Phases (1-1-4 Read Example: 0xEB) 시간 CS Command 1-wire (MOSI only) 0xEB (8 cycles) IO0: CMD[7:0] Address 4-wire (IO0~IO3) 24-bit (6 cycles) IO0~3: ADDR[23:0] Dummy 4-wire (대기) 2~10 cycles IO: Hi-Z 또는 고정 Data 4-wire (IO0~IO3 수신) N bytes (N/2 cycles each) IO0~3: DATA[N*8-1:0] (연속 수신) Standard SPI vs Quad SPI vs QPI 비교 모드 CMD 전송 ADDR 전송 DATA 전송 표기법 유효 대역폭 Standard SPI 1-wire 1-wire 1-wire 1-1-1 f_sck x 1 bit Quad Output 1-wire 1-wire 4-wire 1-1-4 f_sck x 4 bit (data) Quad I/O 1-wire 4-wire 4-wire 1-4-4 f_sck x 4 bit (addr+data) QPI 4-wire 4-wire 4-wire 4-4-4 f_sck x 4 bit (전체)

DDR (Double Data Rate) 모드

DDR 모드에서는 SCK의 상승 에지와 하강 에지 양쪽에서 데이터를 전송하여 SDR(Single Data Rate) 대비 2배 대역폭(Bandwidth)을 달성합니다. Octal DDR (8D-8D-8D) 모드에서는 8개 데이터 라인 x DDR로 사이클당 16비트를 전송합니다:

전송 모드데이터 라인클럭 에지사이클당 비트200 MHz 시 대역폭
1-1-1 SDR1단일125 MB/s
1-1-4 SDR4단일4100 MB/s
1-4-4 DDR4양쪽8200 MB/s
4-4-4 DDR4양쪽8200 MB/s
8D-8D-8D8양쪽16400 MB/s

Data Strobe (DQS)

DDR 모드에서 고주파 클럭의 정확한 샘플링 타이밍을 보장하기 위해 DQS (Data Strobe) 신호가 사용됩니다. DQS는 슬레이브 디바이스가 데이터와 함께 출력하는 스트로브 신호로, 컨트롤러가 이 신호의 에지에서 데이터를 캡처합니다. 이는 SDR에서는 불필요하지만, DDR에서 클럭-데이터 스큐 보상에 필수적입니다.

XIP (eXecute-In-Place) 모드

XIP 모드는 SPI NOR Flash의 내용을 CPU 메모리 맵(Memory Map)에 직접 매핑하여, 별도의 복사 없이 Flash에서 직접 코드를 실행할 수 있게 합니다. QSPI 컨트롤러가 CPU의 메모리 접근을 자동으로 SPI 읽기 명령으로 변환합니다:

/* XIP 모드 활성화: QSPI 컨트롤러 메모리 매핑 예시 */
static int qspi_enable_xip(struct qspi_priv *priv)
{
    struct spi_mem_op op = SPI_MEM_OP(
        SPI_MEM_OP_CMD(0xEB, 1),          /* Fast Read Quad I/O */
        SPI_MEM_OP_ADDR(3, 0, 4),         /* 24-bit addr, 4-wire */
        SPI_MEM_OP_DUMMY(6, 4),           /* 6 dummy cycles */
        SPI_MEM_OP_DATA_IN(0, NULL, 4)   /* 4-wire data */
    );
    u32 cr;

    /* 메모리 매핑 윈도우 설정 */
    writel(priv->mmap_phys, priv->base + QSPI_MMAP_BASE);
    writel(priv->flash_size - 1, priv->base + QSPI_MMAP_SIZE);

    /* Continuous Read 모드: 반복 CMD 전송 생략 */
    cr = readl(priv->base + QSPI_CR);
    cr |= QSPI_CR_XIP_EN | QSPI_CR_CONT_READ;
    writel(cr, priv->base + QSPI_CR);

    /* XIP 후 Flash는 메모리처럼 접근 가능:
     * void *data = ioremap(priv->mmap_phys, priv->flash_size);
     * memcpy(dst, data + offset, len);  */

    return 0;
}
XIP 제한사항: XIP 모드에서는 Flash 쓰기/지우기 작업 시 반드시 XIP를 비활성화해야 합니다. 쓰기 중 XIP 읽기가 발생하면 데이터 손상이 발생합니다. 또한 XIP 모드의 읽기 지연 시간은 DDR RAM 대비 10~100배 느리므로, 성능이 중요한 코드는 RAM에 복사 후 실행하는 것이 좋습니다.

SPI Device Tree 바인딩

&spi1 {
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;

    adc@0 {
        compatible = "vendor,my-adc";
        reg = <0>;          /* chip select 0 */
        spi-max-frequency = <10000000>;  /* 10 MHz */
        spi-cpol;           /* CPOL=1 (Mode 2 or 3) */
        spi-cpha;           /* CPHA=1 (Mode 1 or 3) */
        /* spi-cpol + spi-cpha = Mode 3 */
    };

    flash@1 {
        compatible = "jedec,spi-nor";
        reg = <1>;
        spi-max-frequency = <50000000>;
        spi-rx-bus-width = <4>;  /* quad read */
        spi-tx-bus-width = <4>;  /* quad write */
        m25p,fast-read;
    };
};

regmap: SPI 레지스터 추상화 API

regmap은 다양한 버스(I2C, SPI, MMIO) 위의 레지스터 접근을 통합 추상화하는 프레임워크입니다. 드라이버 코드에서 버스별 전송 함수 호출을 제거하고, 캐싱, 범위 검사, endian 변환 등 공통 기능을 투명하게 제공합니다. SPI 드라이버에서는 devm_regmap_init_spi()로 초기화합니다. I2C 및 MMIO 버스에서의 regmap 사용에 대한 자세한 내용은 I2C 서브시스템 문서를 참조하세요.

regmap 설정

#include <linux/regmap.h>

/* 레지스터 기본값 (캐시 초기화용) */
static const struct reg_default my_reg_defaults[] = {
    { 0x00, 0x0000 },  /* STATUS */
    { 0x01, 0x001F },  /* CONFIG */
    { 0x02, 0x0000 },  /* DATA */
};

/* 읽기 가능 레지스터 범위 */
static bool my_readable_reg(struct device *dev, unsigned int reg)
{
    return reg <= 0x10;
}

/* 휘발성 레지스터 (캐시하지 않음) */
static bool my_volatile_reg(struct device *dev, unsigned int reg)
{
    return reg == 0x00 || reg == 0x02;  /* STATUS, DATA */
}

static const struct regmap_config my_regmap_config = {
    .reg_bits      = 8,         /* 레지스터 주소 비트 수 */
    .val_bits      = 16,        /* 레지스터 값 비트 수 */
    .max_register  = 0x10,
    .readable_reg  = my_readable_reg,
    .volatile_reg  = my_volatile_reg,
    .cache_type    = REGCACHE_RBTREE,
    .reg_defaults  = my_reg_defaults,
    .num_reg_defaults = ARRAY_SIZE(my_reg_defaults),
};

SPI regmap 생성

/* SPI regmap */
static int my_spi_probe(struct spi_device *spi)
{
    struct regmap *regmap;

    regmap = devm_regmap_init_spi(spi, &my_regmap_config);
    if (IS_ERR(regmap))
        return PTR_ERR(regmap);
    /* ... */
}
버스 타입별 초기화 함수: devm_regmap_init_spi() 외에도 devm_regmap_init_i2c(), devm_regmap_init_mmio(), devm_regmap_init_spi_avmm() (SPI Avalon-MM), devm_regmap_init_spmi_base/ext() (SPMI) 등 다양한 버스를 지원합니다. fast_io = true인 버스(MMIO 등)는 mutex 대신 spinlock을 사용하여 원자적(Atomic) 컨텍스트에서도 접근 가능합니다.

regmap 사용

unsigned int val;
int ret;

/* 단일 레지스터 읽기 */
ret = regmap_read(regmap, 0x00, &val);
if (ret)
    return ret;

/* 단일 레지스터 쓰기 */
ret = regmap_write(regmap, 0x01, 0x1234);

/* 비트 필드 업데이트 (read-modify-write 원자적 수행) */
ret = regmap_update_bits(regmap, 0x01,
                         0x00FF,    /* mask */
                         0x0042);   /* value */

/* 벌크 읽기 */
u16 buf[4];
ret = regmap_bulk_read(regmap, 0x00, buf, 4);

/* 폴링: 비트가 설정될 때까지 대기 */
ret = regmap_read_poll_timeout(regmap, 0x00, val,
                               val & BIT(0),     /* condition */
                               1000,              /* sleep_us */
                               100000);           /* timeout_us */
regmap 캐시(Cache) 타입: REGCACHE_NONE (캐시 없음), REGCACHE_RBTREE (희소 레지스터용, RB 트리), REGCACHE_FLAT (연속 레지스터용, 배열), REGCACHE_MAPLE (v6.4+, maple tree 기반). 휘발성(volatile) 레지스터는 항상 하드웨어에서 직접 읽습니다.

regmap IRQ 프레임워크

regmap-irq는 레지스터 기반 인터럽트 컨트롤러(Interrupt Controller)를 위한 generic IRQ chip을 제공합니다:

static const struct regmap_irq my_irqs[] = {
    REGMAP_IRQ_REG(0, 0, BIT(0)),  /* IRQ 0: bit 0 of reg 0 */
    REGMAP_IRQ_REG(1, 0, BIT(1)),  /* IRQ 1: bit 1 of reg 0 */
    REGMAP_IRQ_REG(2, 0, BIT(2)),  /* IRQ 2: bit 2 of reg 0 */
};

static const struct regmap_irq_chip my_irq_chip = {
    .name           = "my-device",
    .irqs           = my_irqs,
    .num_irqs       = ARRAY_SIZE(my_irqs),
    .num_regs       = 1,
    .status_base    = 0x08,  /* 인터럽트 상태 레지스터 */
    .mask_base      = 0x09,  /* 인터럽트 마스크 레지스터 */
    .ack_base       = 0x08,  /* ACK = 상태 레지스터에 W1C */
};

/* probe에서 등록 */
struct regmap_irq_chip_data *irq_data;

ret = devm_regmap_add_irq_chip(&spi->dev, regmap,
        spi->irq, IRQF_ONESHOT, 0,
        &my_irq_chip, &irq_data);

/* 하위 디바이스에서 가상 IRQ 사용 */
int virq = regmap_irq_get_virq(irq_data, 0);

regmap 내부 아키텍처

regmap 프레임워크는 버스 추상화 계층(bus abstraction layer)과 캐시 백엔드로 구성됩니다. 핵심 구조체 struct regmap은 드라이버에 불투명(opaque)하며, 내부적으로 버스 콜백, 캐시 상태, 잠금(Lock) 메커니즘을 관리합니다.

struct regmap 개념 구조 - bus / bus_context: 버스 콜백과 컨텍스트 - config: reg_bits, val_bits, stride, volatile/writeable/readable 정책 - cache_ops / cache: 레지스터 캐시 백엔드와 데이터 - mutex/spinlock + lock/unlock callback: 동기화 - patch / async_list: 초기 패치와 비동기 I/O 관리
/* include/linux/regmap.h - regmap 핵심 구조 (개념도) */

/* struct regmap 내부 주요 필드(위 SVG 참고) */

/* 버스 추상화: 각 버스 타입이 이 콜백을 구현 */
struct regmap_bus {
    bool fast_io;                   /* true면 spinlock 사용 */
    int (*write)(void *context,      /* 레지스터 쓰기 */
                 const void *data, size_t count);
    int (*read)(void *context,       /* 레지스터 읽기 */
                const void *reg_buf, size_t reg_size,
                void *val_buf, size_t val_size);
    int (*reg_write)(void *context,  /* 단일 reg 쓰기 (선택) */
                     unsigned int reg, unsigned int val);
    int (*reg_read)(void *context,   /* 단일 reg 읽기 (선택) */
                    unsigned int reg, unsigned int *val);
    int (*reg_update_bits)(void *context, /* RMW 최적화 (선택) */
                          unsigned int reg,
                          unsigned int mask, unsigned int val);
    enum regmap_endian val_format_endian_default; /* 버스 기본 엔디안 */
};

regmap_field: 비트 필드 추상화

regmap_field는 레지스터 내 특정 비트 필드를 독립적인 객체로 추상화합니다. 비트 마스크/시프트 연산을 캡슐화(Encapsulation)하여 드라이버 코드의 가독성과 유지보수성을 높입니다.

0x04 레지스터 비트 필드 bit15 EN bit14:12 MODE bit11:8 GAIN bit7:0 OFFSET
#include <linux/regmap.h>

/* 레지스터 필드 정의 매크로 */
/* REG_FIELD(reg, lsb, msb) - 레지스터 주소와 비트 범위 지정 */

/* 예: 0x04 레지스터 레이아웃 (위 SVG 참고) */
static const struct reg_field enable_field  = REG_FIELD(0x04, 15, 15);
static const struct reg_field mode_field    = REG_FIELD(0x04, 12, 14);
static const struct reg_field gain_field    = REG_FIELD(0x04, 8,  11);
static const struct reg_field offset_field  = REG_FIELD(0x04, 0,  7);

struct my_device {
    struct regmap *regmap;
    struct regmap_field *f_enable;
    struct regmap_field *f_mode;
    struct regmap_field *f_gain;
    struct regmap_field *f_offset;
};

static int my_probe(struct spi_device *spi)
{
    struct my_device *dev;
    struct regmap *regmap;

    dev = devm_kzalloc(&spi->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    regmap = devm_regmap_init_spi(spi, &my_regmap_config);
    if (IS_ERR(regmap))
        return PTR_ERR(regmap);
    dev->regmap = regmap;

    /* 필드 객체 할당 */
    dev->f_enable = devm_regmap_field_alloc(&spi->dev, regmap, enable_field);
    if (IS_ERR(dev->f_enable))
        return PTR_ERR(dev->f_enable);

    dev->f_mode = devm_regmap_field_alloc(&spi->dev, regmap, mode_field);
    dev->f_gain = devm_regmap_field_alloc(&spi->dev, regmap, gain_field);
    dev->f_offset = devm_regmap_field_alloc(&spi->dev, regmap, offset_field);

    /* 필드 접근: 비트 마스크/시프트 불필요 */
    regmap_field_write(dev->f_enable, 1);           /* bit 15 = 1 */
    regmap_field_write(dev->f_mode, 0x5);           /* bits 14:12 = 5 */
    regmap_field_write(dev->f_gain, 0xA);           /* bits 11:8 = 0xA */

    unsigned int val;
    regmap_field_read(dev->f_offset, &val);         /* bits 7:0 읽기 */

    /* 필드 비트 업데이트 (field 범위 내에서 RMW) */
    regmap_field_update_bits(dev->f_mode, 0x3, 0x2); /* mode 하위 2비트만 수정 */

    return 0;
}

/* 벌크 필드 할당 (v5.3+): 여러 필드를 한 번에 할당 */
static const struct reg_field my_fields[] = {
    [F_ENABLE] = REG_FIELD(0x04, 15, 15),
    [F_MODE]   = REG_FIELD(0x04, 12, 14),
    [F_GAIN]   = REG_FIELD(0x04, 8,  11),
    [F_OFFSET] = REG_FIELD(0x04, 0,  7),
};

struct regmap_field *fields[F_MAX];
ret = devm_regmap_field_bulk_alloc(&spi->dev, regmap,
                                   fields, my_fields, F_MAX);
regmap_field vs regmap_update_bits: 직접 regmap_update_bits(regmap, 0x04, 0x7000, 0x5000)로 작성하면 매직 넘버가 코드 전체에 흩어집니다. regmap_field를 사용하면 필드 정의가 한곳에 집중되고, 드라이버 코드는 regmap_field_write(f_mode, 5)처럼 의미가 명확해집니다. IIO, regulator, clock 등 커널 서브시스템 드라이버에서 널리 사용됩니다.

레지스터 접근 테이블

콜백 함수 대신 regmap_access_table을 사용하면 테이블 기반으로 레지스터 접근 권한을 정의할 수 있습니다. 레지스터가 많은 디바이스에서 더 간결합니다.

/* 레지스터 범위 정의 */
static const struct regmap_range my_readable_ranges[] = {
    regmap_reg_range(0x00, 0x0F),   /* 0x00 ~ 0x0F 읽기 가능 */
    regmap_reg_range(0x20, 0x2F),   /* 0x20 ~ 0x2F 읽기 가능 */
    regmap_reg_range(0x80, 0x80),   /* 0x80 단일 레지스터 */
};

static const struct regmap_range my_writeable_ranges[] = {
    regmap_reg_range(0x01, 0x0F),   /* 0x00(STATUS)은 읽기 전용 */
    regmap_reg_range(0x20, 0x2F),
};

static const struct regmap_range my_volatile_ranges[] = {
    regmap_reg_range(0x00, 0x00),   /* STATUS: 항상 HW 읽기 */
    regmap_reg_range(0x08, 0x09),   /* IRQ 상태/마스크 */
};

static const struct regmap_range my_precious_ranges[] = {
    regmap_reg_range(0x0A, 0x0A),   /* FIFO: 읽으면 값 소비됨 */
};

static const struct regmap_access_table my_rd_table = {
    .yes_ranges = my_readable_ranges,
    .n_yes_ranges = ARRAY_SIZE(my_readable_ranges),
};

static const struct regmap_access_table my_wr_table = {
    .yes_ranges = my_writeable_ranges,
    .n_yes_ranges = ARRAY_SIZE(my_writeable_ranges),
};

static const struct regmap_access_table my_volatile_table = {
    .yes_ranges = my_volatile_ranges,
    .n_yes_ranges = ARRAY_SIZE(my_volatile_ranges),
};

static const struct regmap_access_table my_precious_table = {
    .yes_ranges = my_precious_ranges,
    .n_yes_ranges = ARRAY_SIZE(my_precious_ranges),
};

static const struct regmap_config my_table_config = {
    .reg_bits      = 8,
    .val_bits      = 16,
    .max_register  = 0x80,
    .rd_table      = &my_rd_table,       /* 읽기 가능 범위 */
    .wr_table      = &my_wr_table,       /* 쓰기 가능 범위 */
    .volatile_table = &my_volatile_table, /* 캐시 안 함 */
    .precious_table = &my_precious_table, /* debugfs에서 읽지 않음 */
    .cache_type    = REGCACHE_RBTREE,
};
접근 제어(Access Control) 우선순위(Priority): *_table*_reg() 콜백을 동시에 설정할 수 없습니다. precious 레지스터는 FIFO 포트처럼 읽기 자체가 부작용을 유발하는 레지스터입니다. debugfs의 register dump에서 자동 제외되어 디버깅 시 의도치 않은 데이터 손실을 방지합니다. no_ranges 필드를 사용하면 "이 범위를 제외한 나머지 전부"와 같은 역전 논리도 표현할 수 있습니다.

레지스터 윈도우와 페이지(Page) 매핑

일부 디바이스는 레지스터 공간이 커서 페이지 레지스터를 통해 윈도우 방식으로 접근합니다. regmap_range_cfg는 이러한 페이지 기반 레지스터 접근을 투명하게 처리합니다.

regmap 페이지 윈도우 매핑 물리 주소 공간 (8-bit) 0x00-0x7F: 글로벌 레지스터 0x80: 페이지 선택 레지스터 0x81-0xFF: 페이지 윈도우 가상 주소 공간 (regmap) 0x000-0x07F: 글로벌 0x081-0x0FF: 페이지 0 0x181-0x1FF: 페이지 1 ...
/*
 * 디바이스 레지스터 구조 (위 SVG 참고):
 */

static const struct regmap_range_cfg my_range_cfg[] = {
    {
        .name         = "pages",
        .range_min    = 0x81,        /* 윈도우 시작 (가상) */
        .range_max    = 0x2FF,       /* 윈도우 끝 (가상) */
        .selector_reg = 0x80,        /* 페이지 선택 레지스터 */
        .selector_mask = 0xFF,       /* 선택 비트 마스크 */
        .selector_shift = 0,         /* 선택 비트 시프트 */
        .window_start = 0x81,        /* 물리 윈도우 시작 */
        .window_len   = 0x7F,        /* 윈도우 크기 (바이트) */
    },
};

static const struct regmap_config my_paged_config = {
    .reg_bits      = 8,
    .val_bits      = 8,
    .max_register  = 0x2FF,        /* 가상 주소 공간 최대 */
    .ranges        = my_range_cfg,
    .num_ranges    = ARRAY_SIZE(my_range_cfg),
};

/* 드라이버에서는 가상 주소로 투명하게 접근 */
regmap_read(regmap, 0x185, &val);   /* 자동으로: page=1 선택 → 0x85 읽기 */
regmap_write(regmap, 0x281, 0x42);  /* 자동으로: page=2 선택 → 0x81 쓰기 */
실제 사용 사례: TI TAS2770/TAS2781 오디오 앰프, Maxim MAX77686 PMIC, NXP PCA9685 PWM 컨트롤러 등 레지스터 수가 256개를 초과하는 I2C/SPI 디바이스에서 활용됩니다. regmap이 페이지 전환을 자동 관리하므로 드라이버 코드에서 페이지 선택 로직이 완전히 제거됩니다.

다중 레지스터 연산

여러 레지스터를 원자적으로 읽거나 쓸 때 사용하는 고급 API입니다.

/* 다중 레지스터 쓰기 (시퀀스 보장) */
static const struct reg_sequence init_seq[] = {
    { 0x01, 0x0000 },                  /* CONFIG = 0 (리셋) */
    REG_SEQ0(0x01, 0x0000),              /* 동일, delay_us = 0 */
    { 0x02, 0x1234, 1000 },             /* DATA = 0x1234, 1ms 지연 후 다음 */
    { 0x01, 0x001F },                  /* CONFIG = 0x1F (활성화) */
};

/* 시퀀스를 한 번에 실행 */
ret = regmap_multi_reg_write(regmap, init_seq, ARRAY_SIZE(init_seq));

/* 레지스터 패치: regmap 생성 시 자동 적용되는 초기화 시퀀스 */
static const struct reg_sequence my_patch[] = {
    { 0x10, 0xABCD },
    { 0x11, 0x1234 },
};
ret = regmap_register_patch(regmap, my_patch, ARRAY_SIZE(my_patch));

/* raw 읽기/쓰기: 포맷 변환 없이 바이트 스트림 전송 */
u8 raw_buf[16];
ret = regmap_raw_read(regmap, 0x00, raw_buf, sizeof(raw_buf));
ret = regmap_raw_write(regmap, 0x00, raw_buf, sizeof(raw_buf));

/* noinc 읽기/쓰기: 주소 증가 없이 같은 레지스터 반복 접근 (FIFO) */
u8 fifo_buf[64];
ret = regmap_noinc_read(regmap, 0x0A, fifo_buf, sizeof(fifo_buf));
ret = regmap_noinc_write(regmap, 0x0A, fifo_buf, sizeof(fifo_buf));
bulk vs raw vs noinc 차이:
  • regmap_bulk_read/write: 연속 레지스터를 val_bits 단위로 읽기/쓰기. 엔디안(Endianness) 변환 적용
  • regmap_raw_read/write: 연속 레지스터를 바이트 스트림으로 전송. 엔디안 변환 없음. val_bits > 8일 때만 사용 가능
  • regmap_noinc_read/write: 주소 증가 없이 동일 레지스터에 반복 접근. FIFO 포트 등에 사용. regmap_config.read_flag_mask 설정 필요할 수 있음

regmap 캐시

regmap 캐시는 레지스터 값의 로컬 복사본을 유지하여 불필요한 버스 트랜잭션을 줄입니다. 전원 관리와 긴밀하게 연동되며, 캐시 상태 관리를 통해 resume 시 효율적인 레지스터 복원을 수행합니다.

regmap 캐시 상태 머신 CLEAN (HW=SW) cache_only=0 DIRTY (HW!=SW) cache_only=1 또는 write 누적 regmap_write()로 DIRTY 전이, resume 시 cache_sync()로 CLEAN 복귀
/*
 * regmap 캐시 상태 머신 (위 SVG 참고)
 */

/* 캐시 제어 API */

/* 캐시 전용 모드: 버스 접근 차단, 캐시만 갱신 */
regcache_cache_only(regmap, true);   /* suspend 시 */
regmap_write(regmap, 0x01, 0x42);    /* 캐시에만 기록, dirty 마킹 */
regcache_cache_only(regmap, false);  /* resume 시 */

/* 캐시 → HW 동기화: dirty 레지스터만 하드웨어에 기록 */
ret = regcache_sync(regmap);

/* 특정 범위만 동기화 */
ret = regcache_sync_region(regmap, 0x00, 0x0F);

/* 캐시 전체를 dirty로 마킹 (resume 시 전체 복원 강제) */
regcache_mark_dirty(regmap);

/* 캐시 무효화: 캐시된 값 폐기, 다음 읽기 시 HW 접근 */
ret = regcache_drop_region(regmap, 0x00, 0x0F);

/* 캐시 바이패스: 일시적으로 캐시 우회, HW 직접 접근 */
regcache_cache_bypass(regmap, true);
regmap_read(regmap, 0x00, &val);      /* HW에서 직접 읽기 */
regcache_cache_bypass(regmap, false);
캐시 타입자료구조적합한 경우메모리 사용
REGCACHE_NONE없음캐시 불필요 (volatile 위주)0
REGCACHE_FLAT배열연속/조밀한 레지스터 맵O(max_register)
REGCACHE_RBTREERed-Black Tree희소(sparse) 레지스터 맵O(사용 레지스터 수)
REGCACHE_MAPLEMaple Treev6.4+, 범위 기반 최적화O(사용 레지스터 수)
캐시 타입 선택 가이드: max_register가 작고 대부분의 레지스터를 사용하면 REGCACHE_FLAT이 가장 빠릅니다 (O(1) 접근). 레지스터 주소가 넓게 분산되어 있으면 REGCACHE_RBTREEREGCACHE_MAPLE이 메모리 효율적입니다. REGCACHE_MAPLE은 v6.4에서 추가되었으며, 연속 범위 탐색이 rbtree보다 캐시 친화적입니다.

regmap 전원 관리 연동

regmap 캐시는 시스템 suspend/resume과 runtime PM에서 핵심적인 역할을 합니다. 전원 차단 시 캐시 전용 모드로 전환하고, 복원 시 dirty 레지스터만 하드웨어에 동기화하여 resume 시간을 최소화합니다.

/* 시스템 suspend/resume */
static int my_suspend(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    /* 디바이스 비활성화 */
    regmap_update_bits(mydev->regmap, 0x01, BIT(0), 0);

    /* 캐시 전용 모드: 이후 접근은 캐시에만 기록 */
    regcache_cache_only(mydev->regmap, true);

    /* 전체 캐시를 dirty로 마킹: resume 시 전체 복원 */
    regcache_mark_dirty(mydev->regmap);

    return 0;
}

static int my_resume(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);
    int ret;

    /* 캐시 전용 모드 해제: 버스 접근 재개 */
    regcache_cache_only(mydev->regmap, false);

    /* dirty 레지스터만 HW에 동기화 */
    ret = regcache_sync(mydev->regmap);
    if (ret)
        dev_err(dev, "regcache sync failed: %d\\n", ret);

    return ret;
}

/* Runtime PM과 regmap 연동 */
static int my_runtime_suspend(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    regcache_cache_only(mydev->regmap, true);
    regcache_mark_dirty(mydev->regmap);

    /* 레귤레이터/클록 비활성화 */
    regulator_disable(mydev->vdd);
    clk_disable_unprepare(mydev->clk);

    return 0;
}

static int my_runtime_resume(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);
    int ret;

    /* 레귤레이터/클록 활성화 */
    ret = clk_prepare_enable(mydev->clk);
    if (ret)
        return ret;

    ret = regulator_enable(mydev->vdd);
    if (ret) {
        clk_disable_unprepare(mydev->clk);
        return ret;
    }

    /* HW 안정화 대기 */
    usleep_range(1000, 1500);

    regcache_cache_only(mydev->regmap, false);
    ret = regcache_sync(mydev->regmap);

    return ret;
}

static DEFINE_RUNTIME_DEV_PM_OPS(my_pm_ops,
    my_runtime_suspend, my_runtime_resume, NULL);
PM 연동 주의사항:
  • regcache_mark_dirty()는 반드시 regcache_cache_only(true) 이후에 호출해야 합니다. 순서가 바뀌면 dirty 마킹 후 HW 동기화가 시도되어 전원 차단된 디바이스에 버스 접근이 발생할 수 있습니다
  • resume 시 regcache_cache_only(false)는 반드시 HW가 준비된 이후에 호출하세요
  • 디바이스가 소프트 리셋이 아닌 완전 전원 차단을 거치면, POR(Power-On Reset) 기본값이 적용되므로 mark_dirty로 전체 복원이 필요합니다

regmap debugfs 디버깅

regmap은 자동으로 debugfs 인터페이스를 생성하여 런타임에 레지스터 값을 검사할 수 있습니다. CONFIG_DEBUG_FSCONFIG_REGMAP이 활성화되어야 합니다.

# regmap debugfs 위치
/sys/kernel/debug/regmap/

# 디바이스별 디렉토리 예시
/sys/kernel/debug/regmap/spi0.0/         # SPI bus 0, CS 0

# 레지스터 값 확인
$ cat /sys/kernel/debug/regmap/spi0.0/registers
00: 0042
01: 001f
02: 0000
03: abcd
...

# 접근 권한 확인
$ cat /sys/kernel/debug/regmap/spi0.0/access
00: RV      # R=readable, V=volatile
01: RW      # R=readable, W=writable
02: RWv     # v=volatile (소문자 = 캐시 안 함)
0a: RWP     # P=precious (debugfs dump 제외)

# 캐시 상태 확인
$ cat /sys/kernel/debug/regmap/spi0.0/cache_only
N           # Y=캐시 전용 모드 활성
항목의미
nameregmap 이름
range레지스터 주소 범위
registers레지스터 덤프 (precious 제외)
access읽기/쓰기/volatile/precious 권한 정보
cache_only캐시 전용 모드 상태
디버깅 팁: registers 파일에 값을 쓰면 런타임에 레지스터를 변경할 수 있습니다: echo "01 abcd" > registers. 이는 프로토타이핑 시 매우 유용하지만, precious 레지스터는 dump에서 자동 제외되므로 FIFO 데이터가 의도치 않게 소비되는 문제를 방지합니다.

regmap 고급 설정

regmap_config의 다양한 고급 옵션으로 복잡한 하드웨어 특성을 처리할 수 있습니다. SPI 디바이스에서 특히 read_flag_maskpad_bits 설정이 자주 사용됩니다.

static const struct regmap_config advanced_config = {
    .reg_bits        = 16,          /* 16비트 레지스터 주소 */
    .val_bits        = 32,          /* 32비트 레지스터 값 */
    .reg_stride      = 4,           /* 레지스터 주소 간격 (MMIO 워드 정렬) */
    .max_register    = 0x1000,

    /* 엔디안 설정 */
    .reg_format_endian = REGMAP_ENDIAN_BIG,     /* 주소 바이트 순서 */
    .val_format_endian = REGMAP_ENDIAN_LITTLE,  /* 값 바이트 순서 */

    /* SPI 읽기 시 상위 비트 설정 (SPI 프로토콜 관례) */
    .read_flag_mask  = 0x80,        /* 레지스터 주소에 OR */
    .write_flag_mask = 0x00,

    /* 레지스터 주소 패딩 (일부 SPI 디바이스 요구) */
    .pad_bits        = 8,           /* 주소 뒤 패딩 비트 */

    /* 읽기 전 지연 (슬로우 디바이스) */
    .read_delay_us   = 10,          /* 주소 전송 후 읽기 전 지연 */

    /* 캐시 설정 */
    .cache_type      = REGCACHE_MAPLE,
    .reg_defaults    = my_defaults,
    .num_reg_defaults = ARRAY_SIZE(my_defaults),

    /* 동기화 비활성 레지스터: cache_sync 시 기록하지 않을 범위 */
    .disable_locking = false,       /* true면 외부 잠금 사용 */

    /* 커스텀 잠금 (기존 잠금과 통합 시) */
    .lock            = my_lock_fn,
    .unlock          = my_unlock_fn,
    .lock_arg        = &my_mutex,

    /* 레지스터 값 비트 후처리 */
    .use_single_read  = true,       /* bulk를 단일 읽기로 분해 */
    .use_single_write = true,       /* bulk를 단일 쓰기로 분해 */
    .can_multi_write  = true,       /* multi_reg_write 최적화 허용 */

    /* 비동기 쓰기 지원 */
    .use_hwlock      = false,       /* HW spinlock 사용 여부 */
};
read_flag_mask 활용: 대부분의 SPI 레지스터 디바이스는 읽기 시 레지스터 주소의 최상위 비트(bit 7)를 1로 설정하는 관례를 따릅니다. read_flag_mask = 0x80으로 설정하면 regmap이 자동으로 처리합니다. reg_stride의 배수가 아닌 주소로의 접근은 자동으로 거부(-EINVAL)됩니다.

regmap ftrace 이벤트

regmap은 ftrace 이벤트를 통해 모든 레지스터 접근을 추적할 수 있습니다. 버스 트랜잭션 디버깅과 성능 분석에 활용됩니다.

# regmap 관련 ftrace 이벤트
$ ls /sys/kernel/debug/tracing/events/regmap/
regmap_reg_read/         # 레지스터 읽기
regmap_reg_write/        # 레지스터 쓰기
regmap_bulk_read/        # 벌크 읽기
regmap_bulk_write/       # 벌크 쓰기
regmap_hw_read_start/    # HW 읽기 시작
regmap_hw_read_done/     # HW 읽기 완료
regmap_hw_write_start/   # HW 쓰기 시작
regmap_hw_write_done/    # HW 쓰기 완료
regmap_cache_only/       # 캐시 전용 모드 전환
regmap_cache_sync/       # 캐시 동기화
regmap_cache_bypass/     # 캐시 바이패스 전환

# 특정 regmap만 필터링
$ cd /sys/kernel/debug/tracing
$ echo 1 > events/regmap/regmap_reg_write/enable
$ cat trace
# 출력 예:
#    my_driver-1234  [002] ....  1.234567: regmap_reg_write:
#        spi0.0 reg=01 val=1234

# 특정 디바이스만 추적
$ echo 'name == "spi0.0"' > events/regmap/regmap_reg_write/filter

# HW 접근 시간 측정 (start/done 이벤트 페어)
$ echo 1 > events/regmap/regmap_hw_read_start/enable
$ echo 1 > events/regmap/regmap_hw_read_done/enable
$ cat trace_pipe
# hw_read_start와 hw_read_done 타임스탬프 차이 = 실제 버스 지연
성능 분석 패턴: regmap_cache_sync 이벤트로 resume 시 동기화되는 레지스터 수를 확인하고, regmap_hw_read_start/done 페어로 버스 지연을 측정할 수 있습니다. 캐시 적중률이 낮다면 volatile_reg 설정을 검토하세요. 불필요하게 volatile로 마킹된 레지스터가 성능 병목(Bottleneck)을 유발할 수 있습니다.

Device Tree 통합: SPI 바인딩 패턴

SPI 서브시스템의 Device Tree 바인딩에서 사용되는 공통 프로퍼티와 패턴을 정리합니다.

SPI 공통 프로퍼티

프로퍼티적용 대상설명
compatible모든 SPI 디바이스드라이버 매칭 문자열 (vendor,device)
regSPI 디바이스CS(Chip Select) 번호
spi-max-frequencySPI 디바이스최대 클럭 주파수 (Hz)
spi-cpolSPI 디바이스CPOL=1 설정 (유휴 클럭 HIGH)
spi-cphaSPI 디바이스CPHA=1 설정 (두 번째 에지에서 샘플링)
spi-rx-bus-widthQSPI 디바이스수신 데이터 라인 수 (1, 2, 4, 8)
spi-tx-bus-widthQSPI 디바이스송신 데이터 라인 수 (1, 2, 4, 8)
cs-gpiosSPI 컨트롤러GPIO 기반 CS 핀 지정
interrupts인터럽트 사용 디바이스IRQ 스펙
*-supply전원 사용 디바이스regulator phandle
pinctrl-*핀 설정 필요 디바이스pinctrl 상태

SPI Device Tree 종합 예제

실제 임베디드 보드에서 SPI Flash와 SPI ADC를 함께 사용하는 Device Tree 예제:

&pinctrl {
    spi1_default: spi1-default-pins {
        mosi-sck-pins {
            pins = "PB3", "PB5";
            function = "spi1";
            bias-disable;
            drive-push-pull;
            slew-rate = <1>;  /* high speed */
        };
        miso-pin {
            pins = "PB4";
            function = "spi1";
            bias-pull-down;
        };
    };
};

&spi1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi1_default>;

    /* SPI NOR Flash */
    flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <50000000>;
        spi-rx-bus-width = <4>;
        m25p,fast-read;

        partitions {
            compatible = "fixed-partitions";
            #address-cells = <1>;
            #size-cells = <1>;

            bootloader@0 {
                label = "bootloader";
                reg = <0x0 0x40000>;
                read-only;
            };

            firmware@40000 {
                label = "firmware";
                reg = <0x40000 0x3C0000>;
            };
        };
    };

    /* SPI ADC */
    adc@1 {
        compatible = "vendor,my-adc";
        reg = <1>;
        spi-max-frequency = <5000000>;
        vref-supply = <&reg_3v3>;
    };
};

SPI Device Tree 디버깅

SPI 관련 Device Tree 문제를 디버깅하는 방법:

# SPI 디바이스 확인
ls /sys/bus/spi/devices/          # 등록된 SPI 디바이스
ls /sys/class/spi_master/         # SPI 컨트롤러

# pinctrl 핀 매핑 확인
cat /sys/kernel/debug/pinctrl/*/pins  # pinctrl 핀 매핑

# Device Tree 런타임 확인
ls /proc/device-tree/             # DT 노드 트리
dtc -I fs /proc/device-tree/      # 런타임 DT를 DTS로 디컴파일

# dmesg로 SPI 관련 메시지 확인
dmesg | grep -i spi               # SPI 드라이버 로드/에러 확인
SPI 디바이스가 감지되지 않을 때 체크리스트: (1) ls /sys/bus/spi/devices/로 디바이스 등록 확인, (2) dmesg | grep spi로 컨트롤러 등록 확인, (3) Device Tree의 reg 속성(CS 번호)이 올바른지 확인, (4) pinctrl 설정이 올바른지 확인 (MOSI/MISO/SCK 핀이 SPI 기능으로 mux 되었는지), (5) spi-max-frequency가 디바이스 지원 범위 내인지 확인, (6) spi-cpol/spi-cpha 설정이 디바이스 데이터 시트와 일치하는지 확인.

참고자료

커널 공식 문서

커널 소스 코드

외부 자료

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.