Embedded : : Architecture/: : Peripheral

I2C / SPI / GPIO 서브시스템, 그리고 CAN Bus

Jay.P Morgan 2023. 11. 13. 23:59

 

 

I.  I2C / SPI / GPIO 서브시스템

임베디드 Linux에서 가장 많이 쓰이는 I2C/SPI/GPIO 서브시스템을 보드 초기화부터 드라이버 운영까지 실무 관점으로 정리합니다. I2C/SPI 전송 모델과 버스 arbitration, GPIO descriptor 기반 안전한 제어 패턴, Device Tree 바인딩 및 pinctrl 상호작용, regmap을 활용한 레지스터 추상화, IRQ-capable GPIO 처리, 전원관리와 슬립 복귀 시 상태 복원, 로직 분석기와 tracepoint를 이용한 타이밍 문제 진단까지 하드웨어 근접 드라이버 개발에 필요한 핵심을 다룹니다.

 

전제 조건: 디바이스 드라이버와 인터럽트 문서를 먼저 읽으세요. 버스/열거/프로브 경로는 초기화 순서와 자원 등록 규칙이 핵심이므로, 장치 발견부터 바인딩까지 흐름을 먼저 고정해야 합니다.

 

일상 비유: 이 주제는 터미널 입출고 게이트 운영과 비슷합니다. 차량(디바이스)이 들어오면 게이트 규칙(버스 규약)에 맞춰 배정하고 점검하듯이, 드라이버도 바인딩 규약을 정확히 따라야 합니다.

 

 

  1.  핵심 요약

 

  • 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
  • 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
  • IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
  • 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
  • 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.

 

  2.  단계별 이해

 

    1. 장치 수명주기 확인

        probe부터 remove까지 흐름을 점검합니다.

    2. 비동기 경로 설계

        IRQ, 워크큐, 타이머 역할을 분리합니다.

    3. 자원 정합성 검증

        DMA/클록/전원 참조를 교차 확인합니다.

    4. 현장 조건 테스트

        연결 끊김/복구/부하 상황을 재현합니다.

 

관련 표준: I2C-bus specification (NXP UM10204), SPI (Motorola/de facto), MIPI I3C (MIPI Alliance HCI 1.0) — 이 문서에서 다루는 버스 프로토콜의 기반 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

 

 

 

  3.  I2C 프로토콜 기초

 

I2C (Inter-Integrated Circuit)는 Philips(현 NXP)가 1982년 개발한 2-wire 직렬 버스입니다. 센서, EEPROM, RTC, PMIC 등 저속 주변장치 연결에 널리 사용됩니다.

신호선역할특성

신호선 역할 특성
SCL Serial Clock 마스터가 생성, 오픈 드레인
SDA Serial Data 양방향 데이터, 오픈 드레인

 

 

  3.1 신호 프로토콜

 

I2C 통신의 기본 단위는 START 조건으로 시작하여 STOP 조건으로 끝나는 트랜잭션입니다:

조건 SDA 상태 SCL 상태 의미
START (S) HIGH → LOW HIGH 트랜잭션 시작
STOP (P) LOW → HIGH HIGH 트랜잭션 종료
Repeated START (Sr) HIGH → LOW HIGH STOP 없이 재시작
ACK LOW (수신측) 9번째 클럭 수신 확인
NACK HIGH (수신측) 9번째 클럭 수신 거부 / 마지막 바이트

 

 

  3.2  주소 체계

 

I2C는 7비트(표준)와 10비트(확장) 주소를 지원합니다. 7비트 주소의 경우 첫 번째 바이트는 [A6:A0 | R/W] 형식으로, 최하위 비트가 방향을 나타냅니다 (0 = Write, 1 = Read).

예약 주소0x00 (General Call), 0x01 (CBUS), 0x02 (다른 버스 형식), 0x03 (미래 용도), 0x04-0x07 (Hs-mode 마스터 코드), 0x78-0x7B (10비트 주소 prefix), 0x7C-0x7F (미래 용도)는 슬레이브 주소로 사용할 수 없습니다.
속도 모드 클럭 주파수 용도
Standard Mode (Sm) 100 kHz 일반 센서, EEPROM
Fast Mode (Fm) 400 kHz 가속도계, 터치 컨트롤러
Fast Mode Plus (Fm+) 1 MHz 고속 센서
High Speed Mode (Hs) 3.4 MHz 고속 메모리

 

 

  4. Linux I2C 서브시스템

 

커널 I2C 서브시스템은 drivers/i2c/ 디렉토리에 구현되어 있으며, 다음 핵심 구조체로 구성됩니다:

구 조 체 역    할 헤    더
i2c_adapter I2C 버스 컨트롤러 (마스터) <linux/i2c.h>
i2c_algorithm 전송 알고리즘 (HW 접근 방법) <linux/i2c.h>
i2c_client I2C 버스 상의 슬레이브 디바이스 <linux/i2c.h>
i2c_driver I2C 디바이스 드라이버 <linux/i2c.h>
i2c_msg 단일 I2C 메시지 (주소+데이터) <linux/i2c.h>

 

 

  4.1 i2c_adapter와 i2c_algorithm

 

i2c_adapter는 물리적 I2C 컨트롤러를 나타내며, i2c_algorithm을 통해 실제 하드웨어 전송을 수행합니다:

 
  struct i2c_algorithm {
          int (*master_xfer)(struct i2c_adapter *adap,
                                          struct i2c_msg *msgs, int num);
          int (*master_xfer_atomic)(struct i2c_adapter *adap,
                                                          struct i2c_msg *msgs, int num);
          int (*smbus_xfer)(struct i2c_adapter *adap,
                                          u16 addr, unsigned short flags,
                                          char read_write, u8 command,
                                          int size, union i2c_smbus_data *data);
          u32 (*functionality)(struct i2c_adapter *adap);
  }
;
master_xfer vs smbus_xfer: master_xfer는 raw I2C 메시지를 전송하며, smbus_xfer는 SMBus 프로토콜에 최적화된 전송을 수행합니다. 대부분의 어댑터는 master_xfer만 구현하고, 커널이 SMBus 호출을 I2C 메시지로 에뮬레이션합니다.

 

 

  4.2  i2c_client와 i2c_driver

  

i2c_client는 특정 어댑터의 특정 주소에 위치한 디바이스를 나타내며, i2c_driver가 이를 제어합니다:

 
  struct i2c_driver {
          int (*probe)(struct i2c_client *client);
          void (*remove)(struct i2c_client *client);
          void (*shutdown)(struct i2c_client *client);
          struct device_driver driver;
          const struct i2c_device_id *id_table;
  };
 

 

 

  5. I2C 드라이버 작성

 

실제 I2C 센서 드라이버 예제를 통해 작성 방법을 살펴봅니다. 아래는 가상의 온도 센서 드라이버입니다:

 
  #include <linux/module.h>
  #include <linux/i2c.h>
  #include <linux/iio/iio.h>
 
  #define TEMP_REG_VALUE 0x00
  #define TEMP_REG_CONFIG 0x01
  #define TEMP_REG_ID 0xFF
 
  struct my_temp_data {
          struct i2c_client *client;
          struct mutex lock;
          u8 config;
  };
 
  static int my_temp_read_reg(struct my_temp_data *data, u8 reg)
  {
          int ret;
 
          ret = i2c_smbus_read_byte_data(data->client, reg);
          if (ret < 0)
                  dev_err(&data->client->dev,
                                  "failed to read reg 0x%02x: %d\\n", reg, ret);
          return ret;
  }
 
  static int my_temp_read_raw(struct iio_dev *indio_dev,
                                                     struct iio_chan_spec const *chan,
                                                     int *val, int *val2, long mask)
  {
          struct my_temp_data *data = iio_priv(indio_dev);
          int ret;
 
          switch (mask) {
          case IIO_CHAN_INFO_RAW:
                  mutex_lock(&data->lock);
                  ret = my_temp_read_reg(data, TEMP_REG_VALUE);
                  mutex_unlock(&data->lock);
                  if (ret < 0)
                          return ret;
                  *val = (s8)ret; /* 부호 확장 */
                  return IIO_VAL_INT;
          case IIO_CHAN_INFO_SCALE:
                  *val = 1000; /* milli-degrees */
                  return IIO_VAL_INT;
          default:
                  return -EINVAL;
          }
  }
 
  static const struct iio_chan_spec my_temp_channels[] = {
          {
                  .type = IIO_TEMP,
                  .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
                                                          BIT(IIO_CHAN_INFO_SCALE),
          },
  };
 
  static const struct iio_info my_temp_info = {
          .read_raw = my_temp_read_raw,
  };
 
  static int my_temp_probe(struct i2c_client *client)
  {
          struct iio_dev *indio_dev;
          struct my_temp_data *data;
          int chip_id;
 
          /* 디바이스 ID 확인 */
          chip_id = i2c_smbus_read_byte_data(client, TEMP_REG_ID);
          if (chip_id < 0)
                  return chip_id;
          if (chip_id != 0xA1) {
                  dev_err(&client->dev, "unexpected chip id: 0x%02x\\n", chip_id);
                  return -ENODEV;
          }
          indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
          if (!indio_dev)
                  return -ENOMEM;
 
          data = iio_priv(indio_dev);
          data->client = client;
          mutex_init(&data->lock);
 
          indio_dev->name = "my_temp";
          indio_dev->info = &my_temp_info;
          indio_dev->channels = my_temp_channels;
          indio_dev->num_channels = ARRAY_SIZE(my_temp_channels);
          indio_dev->modes = INDIO_DIRECT_MODE;
 
          /* 센서 활성화 */
          i2c_smbus_write_byte_data(client, TEMP_REG_CONFIG, 0x01);
          return devm_iio_device_register(&client->dev, indio_dev);
  }
 
  static const struct i2c_device_id my_temp_id[] = {
          { "my-temp-sensor", 0 },
          { }
  };
  MODULE_DEVICE_TABLE(i2c, my_temp_id);
 
  static const struct of_device_id my_temp_of_match[] = {
          { .compatible = "vendor,my-temp-sensor" },
          { }
  };
  MODULE_DEVICE_TABLE(of, my_temp_of_match);
 
  static struct i2c_driver my_temp_driver = {
                  .driver = {
                          .name = "my-temp-sensor",
                          .of_match_table = my_temp_of_match,
                  },
                  .probe = my_temp_probe,
                  .id_table = my_temp_id,
  };
  module_i2c_driver(my_temp_driver);
 

 

 

  5.1  SMBus API

 

대부분의 I2C 디바이스 드라이버는 raw i2c_transfer() 대신 SMBus 래퍼를 사용합니다:

함수동작데이터 크기

함    수 동    작 데 이 터 크기
i2c_smbus_read_byte() 커맨드 없이 1바이트 읽기 1 byte
i2c_smbus_write_byte() 커맨드 없이 1바이트 쓰기 1 byte
i2c_smbus_read_byte_data() 레지스터에서 1바이트 읽기 1 byte
i2c_smbus_write_byte_data() 레지스터에 1바이트 쓰기 1 byte
i2c_smbus_read_word_data() 레지스터에서 2바이트 읽기 2 bytes
i2c_smbus_write_word_data() 레지스터에 2바이트 쓰기 2 bytes
i2c_smbus_read_block_data() 레지스터에서 블록 읽기 최대 32 bytes
i2c_smbus_read_i2c_block_data() I2C 블록 읽기 (길이 지정) 지정 길이

 

 

  5.2  i2c_transfer (Raw API)

 

복잡한 다중 메시지 트랜잭션에는 i2c_transfer()를 직접 사용합니다:

 
  /* 레지스터 주소 쓰기 후 데이터 읽기 (Repeated START) */
  static int read_reg16(struct i2c_client *client,
                                          u8 reg, u16 *val)
  {
          u8 buf[2];
          struct i2c_msg msgs[2] = {
                  {         /* 쓰기: 레지스터 주소 전송 */
                          .addr = client->addr,
                          .flags = 0,
                          .len = 1,
                          .buf = &reg,
                  },
                  {         /* 읽기: 데이터 수신 */
                          .addr = client->addr,
                          .flags = I2C_M_RD,
                          .len = 2,
                          .buf = buf,
                  },
          };
          int ret;
 
          ret = i2c_transfer(client->adapter, msgs, 2);
          if (ret != 2)
                  return ret < 0 ? ret : -EIO;
          *val = (u16)(buf[0] << 8) | buf[1];
          return 0;
  }
 

 

 

 

  6. I2C Device Tree 바인딩

 

Device Tree에서 I2C 버스와 슬레이브 디바이스를 선언하는 패턴:

 
  /* SoC dtsi: I2C 컨트롤러 노드 */
  i2c1: i2c@40005400 {
          compatible = "st,stm32f7-i2c";
          reg = <0x40005400 0x400>;
          interrupts = <31>, <32>;
          clocks = <&rcc 0x40 21>;
          #address-cells = <1>;
          #size-cells = <0>;
          status = "disabled";
  };
 
/* 보드 dts: 슬레이브 디바이스 추가 */
  &i2c1 {
          status = "okay";
          clock-frequency = <400000>; /* Fast mode 400kHz */
 
          temp_sensor: temperature@48 {
                  compatible = "vendor,my-temp-sensor";
                  reg = <0x48>; /* 7비트 I2C 주소 */
                  interrupt-parent = <&gpio1>;
                  interrupts = <7 IRQ_TYPE_EDGE_FALLING>;
  };
 
          eeprom@50 {
                  compatible = "atmel,24c256";
                  reg = <0x50>;
                  pagesize = <64>;
          };
  };
 
주소 충돌 주의: 같은 I2C 버스에 동일 주소의 디바이스가 두 개 이상 존재하면 커널이 경고를 출력하고 두 번째 디바이스 등록이 실패합니다. 주소가 겹치는 경우 I2C 멀티플렉서(i2c-mux)를 사용하세요.

 

 

  7. I3C 서브 시스템 개요

 

I3C (Improved Inter-Integrated Circuit)는 MIPI Alliance가 표준화한 차세대 직렬 버스로, I2C와의 하위 호환성을 유지하면서 성능과 기능을 대폭 개선했습니다.

특성I2C (Fm+)I3C (SDR)I3C (HDR-DDR)

       
최대 클럭 1 MHz 12.5 MHz 12.5 MHz
최대 데이터율 1 Mbps 12.5 Mbps 25 Mbps
주소 할당 정적 동적 (DAA) 동적 (DAA)
In-Band Interrupt 별도 IRQ 라인 필요 SDA로 IBI 지원 SDA로 IBI 지원
핫조인 미지원 지원 지원

Linux I3C 서브시스템은 drivers/i3c/에 위치하며, i3c_master_controller, i3c_device, i3c_driver 구조체를 사용합니다:

 
  #include <linux/i3c/device.h>
  #include <linux/i3c/master.h>
 
  static int my_i3c_probe(struct i3c_device *i3cdev)
  {
          struct device *dev = i3cdev_to_dev(i3cdev);
          struct i3c_priv_xfer xfer;
          u8 tx_buf = 0x00;
          u8 rx_buf[2];
 
          /* I3C private transfer: 레지스터 읽기 */
          xfer.rnw = 0;
          xfer.len = 1;
          xfer.data.out = &tx_buf;
          i3c_device_do_priv_xfers(i3cdev, &xfer, 1);
 
          xfer.rnw = 1;
          xfer.len = 2;
          xfer.data.in = rx_buf;
          i3c_device_do_priv_xfers(i3cdev, &xfer, 1);
 
          dev_info(dev, "sensor value: 0x%02x%02x\\n", rx_buf[0], rx_buf[1]);
          return 0;
  }
 
  static const struct i3c_device_id my_i3c_ids[] = {
          I3C_DEVICE(0x0123, 0x4567, NULL),
          { }
  };
 
          static struct i3c_driver my_i3c_driver = {
                  .driver.name = "my-i3c-sensor",
                  .probe = my_i3c_probe,
                  .id_table = my_i3c_ids,
  };
  module_i3c_driver(my_i3c_driver);
 

 

 

 

  8. SPI 프로토콜 기초

 

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

신호선별칭역할

신 호 선 별    칭 역    할
MOSI SDO, COPI, DI Master Out Slave In
MISO SDI, CIPO, DO Master In Slave Out
SCK SCLK, CLK Serial Clock (마스터 생성)
CS/SS NSS, CE Chip Select (Active Low)

 

 

  8.1  SPI 모드 (CPOL/CPHA)

 

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

모드CPOLCPHA유휴 클럭데이터 샘플링

모  드 CPOL CPHA 유휴 클럭 데이터 샘플링
Mode 0 0 0 LOW 상승 에지
Mode 1 0 1 LOW 하강 에지
Mode 2 1 0 HIGH 하강 에지
Mode 3 1 1 HIGH 상승 에지

 

SPI vs I2C 선택 기준SPI는 전이중 통신, 높은 클럭 속도(수십 MHz), 단순한 프로토콜이 장점이지만, 디바이스당 전용 CS 라인이 필요하여 핀 수가 증가합니다. I2C는 2선으로 다수 디바이스를 연결할 수 있지만 속도가 제한됩니다.

 

 

 

  9.  Linux SPI 서브시스템

 

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

구조체역할헤더

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

 

 

  9.1  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;
          /* ... */
  };
 

 

 

 

  10. 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속성을 적용하세요.

 

 

  10.1  spi_message와 spi_trasfer

 

복잡한 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);
  }
 

 

 

  11.  QSPI / Dual / Quad SPI

 

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

모드데이터 라인명령 전송대표 디바이스

모드 데이터 라인 명령 전송 대표 디바이스
Standard SPI 1-bit (MOSI/MISO) 1-bit 일반 SPI Flash
Dual Output 2-bit 1-bit W25Q series
Quad Output 4-bit 1-bit W25Q series
QPI (Quad I/O) 4-bit 4-bit 고속 NOR Flash
Octal (8D-8D-8D) 8-bit DDR 8-bit DDR Macronix MX25

 

 

  11.1  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 칩 지원은 벤더별 파일에 파라미터만 추가하면 됩니다.

 

 

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

 

 

 

  12.  GPIO 개요

 

GPIO (General-Purpose Input/Output)는 소프트웨어로 제어 가능한 범용 디지털 핀입니다. LED, 버튼, 리셋 라인, 칩 셀렉트, 인터럽트 입력 등 다양한 용도로 사용됩니다.

Linux GPIO 서브시스템은 drivers/gpio/에 구현되며, 크게 두 가지 API가 있습니다:

API헤더상태특징

API 헤  더 상  태 특  징
Legacy (integer-based) <linux/gpio.h> Deprecated gpio_request(), gpio_direction_input()
Descriptor-based (gpiod) <linux/gpio/consumer.h> 현재 표준 gpiod_get(), gpiod_set_value()
Legacy API 사용 금지: 새 코드에서 gpio_request(), gpio_free(), gpio_get_value() 등 정수 기반 legacy API를 사용하지 마세요. 커널 메인라인에서는 legacy GPIO API를 사용하는 새 드라이버를 받아들이지 않습니다.

 

 

  13.  gpiod API (Descriptor-based)

 

현대 Linux 커널의 표준 GPIO 인터페이스인 gpiod API를 사용합니다:

 

  13.1  GPIO 획득과 해제

 

 
  #include <linux/gpio/consumer.h>
 
  /* Device Tree에서 "reset-gpios" 속성을 참조하여 GPIO 획득 */
  struct gpio_desc *reset_gpio;
 
  reset_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_HIGH);
  if (IS_ERR(reset_gpio))
          return PTR_ERR(reset_gpio);
 
  /* 선택적(optional) GPIO: 없어도 에러 아님 */
  struct gpio_desc *led_gpio;
  led_gpio = devm_gpiod_get_optional(&pdev->dev, "led", GPIOD_OUT_LOW);
 
  /* 인덱스로 여러 GPIO 획득 */
  struct gpio_desc *cs_gpio;
  cs_gpio = devm_gpiod_get_index(&pdev->dev, "cs", 0, GPIOD_OUT_HIGH);
 

 

 

  13.2  GPIO 동작

  

 
  /* 출력 값 설정 (active-low 자동 처리) */
  gpiod_set_value(reset_gpio, 1); /* active (논리적 1) */
  gpiod_set_value(reset_gpio, 0); /* inactive (논리적 0) */
 
  /* sleepable context에서 사용 (I2C/SPI GPIO expander 등) */
  gpiod_set_value_cansleep(reset_gpio, 1);
 
  /* 입력 값 읽기 */
  int val = gpiod_get_value(button_gpio);
 
  /* 방향 변경 */
  gpiod_direction_input(gpio);
  gpiod_direction_output(gpio, 1);
 
  /* GPIO → IRQ 번호 변환 */
  int irq = gpiod_to_irq(button_gpio);
  if (irq < 0)
          return irq;
 
  ret = devm_request_threaded_irq(&pdev->dev, irq, NULL,
                  my_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                  "my-button", data);
 
gpiod_set_value vs gpiod_set_raw_value: gpiod_set_value()는 Device Tree의 GPIO_ACTIVE_LOW 플래그를 자동 반영합니다. gpiod_set_raw_value()는 물리적 라인 레벨을 직접 제어합니다. 일반적으로 gpiod_set_value()를 사용하세요.

 

 

  13.3  GPIO Device Tree 바인딩

 

 
  my_device: my-device@0 {
          compatible = "vendor,my-device";
          /* 프로퍼티 이름: <con-id>-gpios */
          reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>;
          led-gpios = <&gpio2 3 GPIO_ACTIVE_HIGH>;
          cs-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>,
                              <&gpio1 5 GPIO_ACTIVE_LOW>;
  };
 

 

 

  14.  gpio_chip 구현

 

GPIO 컨트롤러 드라이버를 작성하려면 gpio_chip 구조체를 구현하고 등록합니다:

 
  #include <linux/gpio/driver.h>
 
  struct my_gpio {
          struct gpio_chip gc;
          void __iomem *base;
          struct mutex lock;
  };
 
  static int my_gpio_get(struct gpio_chip *gc, unsigned int offset)
  {
          struct my_gpio *priv = gpiochip_get_data(gc);
          u32 reg;
 
          reg = readl(priv->base + 0x10); /* Data Input Register */
          return !!(reg & BIT(offset));
  }
 
  static void my_gpio_set(struct gpio_chip *gc,
                                            unsigned int offset, int value)
  {
          struct my_gpio *priv = gpiochip_get_data(gc);
          u32 reg;
 
          mutex_lock(&priv->lock);
          reg = readl(priv->base + 0x14); /* Data Output Register */
          if (value)
                  reg |= BIT(offset);
          else
                  reg &= ~BIT(offset);
          writel(reg, priv->base + 0x14);
          mutex_unlock(&priv->lock);
  }
 
  static int my_gpio_direction_input(struct gpio_chip *gc,
                                                              unsigned int offset)
  {
          struct my_gpio *priv = gpiochip_get_data(gc);
          u32 reg;
 
          mutex_lock(&priv->lock);
          reg = readl(priv->base + 0x04); /* Direction Register */
          reg &= ~BIT(offset); /* 0 = input */
          writel(reg, priv->base + 0x04);
          mutex_unlock(&priv->lock);
          return 0;
  }
  static int my_gpio_direction_output(struct gpio_chip *gc,
                                                                unsigned int offset, int value)
  {
          my_gpio_set(gc, offset, value);
 
          struct my_gpio *priv = gpiochip_get_data(gc);
          u32 reg;
 
          mutex_lock(&priv->lock);
          reg = readl(priv->base + 0x04);
          reg |= BIT(offset); /* 1 = output */
          writel(reg, priv->base + 0x04);
          mutex_unlock(&priv->lock);
          return 0;
  }
 
  static int my_gpio_probe(struct platform_device *pdev)
  {
          struct my_gpio *priv;
 
          priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
          if (!priv)
                  return -ENOMEM;
          priv->base = devm_platform_ioremap_resource(pdev, 0);
          if (IS_ERR(priv->base))
                 return PTR_ERR(priv->base);
 
          mutex_init(&priv->lock);
 
          priv->gc.label = "my-gpio";
          priv->gc.parent = &pdev->dev;
          priv->gc.owner = THIS_MODULE;
          priv->gc.base = -1; /* 동적 번호 할당 */
          priv->gc.ngpio = 32;
          priv->gc.get = my_gpio_get;
          priv->gc.set = my_gpio_set;
          priv->gc.direction_input = my_gpio_direction_input;
          priv->gc.direction_output = my_gpio_direction_output;
 
          return devm_gpiochip_add_data(&pdev->dev, &priv->gc, priv);
  }
 

 

 

  15.  libgpiod 유저스페이스

 

libgpiod는 Linux GPIO character device (/dev/gpiochipN)를 통한 유저스페이스 GPIO 접근 라이브러리입니다. 기존의 /sys/class/gpio/ sysfs 인터페이스를 대체합니다.

sysfs GPIO 폐기: /sys/class/gpio/export 인터페이스는 deprecated 상태입니다. 새 프로젝트에서는 chardev 기반(/dev/gpiochipN) 접근을 권장하며, 가능한 경우 libgpiod(v2 이상)를 사용하세요.

 

 

  15.1  libgpiod 명령행 도구

 

도  구 용    도 예    시
gpiodetect 시스템의 GPIO 칩 목록 gpiodetect
gpioinfo GPIO 라인 상세 정보 gpioinfo gpiochip0
gpioget GPIO 입력 값 읽기 gpioget gpiochip0 7
gpioset GPIO 출력 값 설정 gpioset gpiochip0 7=1
gpiomon GPIO 이벤트 모니터링 gpiomon gpiochip0 7

 

 

  15.2  libgpiod C API (v2)

 

 
  #include <gpiod.h>
  #include <stdio.h>
  #include <unistd.h>
 
  int main(void)
  {
          struct gpiod_chip *chip;
          struct gpiod_line_settings *settings;
          struct gpiod_line_config *line_cfg;
          struct gpiod_request_config *req_cfg;
          struct gpiod_line_request *request;
          unsigned int offsets[] = { 7 };
          enum gpiod_line_value value;
 
          chip = gpiod_chip_open("/dev/gpiochip0");
          settings = gpiod_line_settings_new();
          gpiod_line_settings_set_direction(settings,
                                                                      GPIOD_LINE_DIRECTION_INPUT);
 
          gpiod_line_settings_set_bias(settings,
                                                               GPIOD_LINE_BIAS_PULL_UP);
 
          line_cfg = gpiod_line_config_new();
          gpiod_line_config_add_line_settings(line_cfg, offsets, 1, settings);
 
          req_cfg = gpiod_request_config_new();
          gpiod_request_config_set_consumer(req_cfg, "my-app");
 
          request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
 
          value = gpiod_line_request_get_value(request, 7);
          printf("GPIO 7 = %d\\n", value);
 
          gpiod_line_request_release(request);
          gpiod_request_config_free(req_cfg);
          gpiod_line_config_free(line_cfg);
          gpiod_line_settings_free(settings);
          gpiod_chip_close(chip);
          return 0;
  }
 

 

 

 

  16.  GPIO Expander

 

GPIO expander는 I2C 또는 SPI를 통해 GPIO 핀 수를 확장하는 디바이스입니다. 커널에서는 일반 GPIO 컨트롤러와 동일한 gpio_chip 인터페이스로 통합됩니다.

디바이스 인터페이스 GPIO 수 인터럽트 커널 드라이버
MCP23017 I2C 16 지원 gpio-mcp23s08
MCP23S17 SPI 16 지원 gpio-mcp23s08
PCA9555 I2C 16 지원 gpio-pca953x
PCA9535 I2C 16 지원 gpio-pca953x
PCF8574 I2C 8 지원 gpio-pcf857x
TCA6424A I2C 24 지원 gpio-pca953x

 

 

  16.1  GPIO Expander Device Tree예시

  

 
  &i2c1 {
          gpio_exp: gpio-expander@20 {
                  compatible = "nxp,pca9555";
                  reg = <0x20>;
                  gpio-controller;
                  #gpio-cells = <2>;
                  interrupt-parent = <&gpio1>;
                  interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
                  interrupt-controller;
                  #interrupt-cells = <2>;
          };
  };
 
  /* GPIO expander의 핀을 다른 디바이스에서 참조 */
  my_led: led-controller {
          compatible = "gpio-leds";
          led-status {
                  gpios = <&gpio_exp 3 GPIO_ACTIVE_HIGH>;
                  label = "status";
                  linux,default-trigger = "heartbeat";
          };
  };
 
can_sleep 플래그: I2C/SPI 기반 GPIO expander는 버스 전송이 필요하므로 gpio_chip.can_sleep = true로 설정됩니다. 이 경우 인터럽트 컨텍스트에서 gpiod_get_value()를 호출할 수 없으며, 반드시 gpiod_get_value_cansleep()을 사용해야 합니다.

 

 

  17. regmap: 레지스터 추상화 API

 

  17.0  regmap 소개

 

임베디드 시스템이나 커널 드라이버 개발을 공부하시다 보면 반드시 마주치게 되는 개념이 바로 Regmap(Register Map)입니다.

regmap리눅스 커널에서 다양한 버스(I2C, SPI, MMIO)를 사용하는 장치 위의 레지스터 접근을 하나의 공통된 인터페이스로 통합 추상화하는 프레임워크입니다. 드라이버 코드에서 버스별 전송 함수 호출을 제거하고, 캐싱, 범위 검사, endian 변환 등 공통 기능을 투명하게 제공합니다.

 

 

1. Regmap이 필요한 이유

하드웨어 제어의 핵심은 레지스터에 값을 읽고 쓰는 것입니다. 그런데 장치마다 통신 방식이 다르면 드라이버 코드가 복잡해집니다.

  • 기존 방식: I2C 장치용 드라이버는 i2c_master_send()를 쓰고, SPI 장치는 spi_write()를 써야 했습니다. 통신 방식이 바뀌면 드라이버 코드 자체를 대대적으로 수정해야 했죠.
  • Regmap 방식: 드라이버는 그저 regmap_write() 하나만 호출하면 됩니다. 하단에서 Regmap이 해당 장치가 I2C인지 SPI인지 판단해서 적절한 통신을 수행합니다.

 

2. Regmap의 주요 장점

단순히 코드만 깔끔해지는 게 아니라, 강력한 기능들을 내장하고 있습니다.

  • 추상화 (Abstraction): 프로토콜에 상관없이 동일한 API(regmap_read, regmap_write)를 사용합니다.
  • 캐싱 (Caching): 레지스터 값을 로컬 메모리에 저장해둡니다. 매번 느린 I2C/SPI 통신을 하지 않고 메모리에서 바로 읽어올 수 있어 성능이 향상됩니다.
  • 원자성 보장 (Locking): 여러 프로세스가 동시에 레지스터에 접근할 때 발생할 수 있는 데이터 꼬임(Race condition)을 방지하기 위해 자체적으로 락(Lock) 기능을 제공합니다.
  • 디버깅 용이성: debugfs를 통해 커널의 레지스터 상태를 파일 형태로 쉽게 확인할 수 있습니다.

 

3. 핵심 API 구조

개발자가 주로 사용하는 함수는 다음과 같습니다.

함수명 설명
regmap_init_i2c() I2C 장치를 위한 regmap 초기화
regmap_read() 특정 레지스터에서 값 읽기
regmap_write() 특정 레지스터에 값 쓰기
regmap_update_bits() 특정 비트만 골라서 수정 (Read-Modify-Write 과정을 한 번에 처리)
 
 

4. 실제 동작 흐름

  1. 설정(Config): 레지스터의 비트 수, 주소 범위, 캐시 방식 등을 regmap_config 구조체에 정의합니다.
  2. 초기화: 드라이버의 probe 함수에서 devm_regmap_init_i2c 등을 호출해 regmap 객체를 생성합니다.
  3. 사용: 이후 데이터 시트에 나온 레지스터 주소를 이용해 값을 읽고 씁니다.

 

💡 요약하자면

Regmap은 하드웨어와 소프트웨어 사이의 "공용 통역사"와 같습니다. 드라이버 개발자는 하드웨어가 어떤 전선으로 연결되어 있는지 고민하기보다, 어떤 데이터를 주고받을지에만 집중할 수 있게 해줍니다.

 

 

  17.1  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),
  };
 

 

 

  17.2  버스별 regmap 생성

 

 
  /* I2C regmap */
  static int my_i2c_probe(struct i2c_client *client)
  {
          struct regmap *regmap;
 
          regmap = devm_regmap_init_i2c(client, &my_regmap_config);
          if (IS_ERR(regmap))
                  return PTR_ERR(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);
          /* ... */
  }
 
  /* MMIO regmap */
  static int my_platform_probe(struct platform_device *pdev)
  {
          void __iomem *base;
          struct regmap *regmap;
 
          base = devm_platform_ioremap_resource(pdev, 0);
          if (IS_ERR(base))
                  return PTR_ERR(base);
 
          regmap = devm_regmap_init_mmio(&pdev->dev, base, &my_regmap_config);
          if (IS_ERR(regmap))
                  return PTR_ERR(regmap);
          /* ... */
  }
 

 

 

  17.3  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 캐시 타입: REGCACHE_NONE (캐시 없음), REGCACHE_RBTREE (희소 레지스터용, RB 트리), REGCACHE_FLAT (연속 레지스터용, 배열), REGCACHE_MAPLE (v6.4+, maple tree 기반). 휘발성(volatile) 레지스터는 항상 하드웨어에서 직접 읽습니다.

 

 

  17.4  regmap IRQ 프레임워크

 

regmap-irq는 레지스터 기반 인터럽트 컨트롤러를 위한 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(&client->dev, regmap,
                                                              client->irq, IRQF_ONESHOT, 0,
                                                              &my_irq_chip, &irq_data);
 
  /* 하위 디바이스에서 가상 IRQ 사용 */
  int virq = regmap_irq_get_virq(irq_data, 0);
 

 

 

  17.5  regmap 내부 아키텍처

 

regmap 프레임워크는 버스 추상화 계층(bus abstraction layer)과 캐시 백엔드로 구성됩니다.

핵심 구조체 struct regmap은 드라이버에 불투명(opaque)하며, 내부적으로 버스 콜백, 캐시 상태, 잠금 메커니즘을 관리합니다.

 

 
  /* 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; /* 버스 기본 엔디안 */
  };
 
버스 타입별 초기화 함수: devm_regmap_init_i2c()devm_regmap_init_spi()devm_regmap_init_mmio() 외에도 devm_regmap_init_spi_avmm() (SPI Avalon-MM), devm_regmap_init_spmi_base/ext() (SPMI), devm_regmap_init_w1() (1-Wire), devm_regmap_init_sdw() (SoundWire), devm_regmap_init_slimbus() (SLIMbus) 등 다양한 버스를 지원합니다. fast_io = true인 버스(MMIO 등)는 mutex 대신 spinlock을 사용하여 원자적 컨텍스트에서도 접근 가능합니다.

 

 

  17.6  regmap_field: 비트 필드 추상화

 

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

 

 
  #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 i2c_client *client)
  {
        struct my_device *dev;
        struct regmap *regmap;
 
        dev = devm_kzalloc(&client->dev, sizeof(*dev), GFP_KERNEL);
        if (!dev)
              return -ENOMEM;
 
        regmap = devm_regmap_init_i2c(client, &my_regmap_config);
        if (IS_ERR(regmap))
              return PTR_ERR(regmap);
        dev->regmap = regmap;
 
        /* 필드 객체 할당 */
        dev->f_enable = devm_regmap_field_alloc(&client->dev, regmap, enable_field);
        if (IS_ERR(dev->f_enable))
              return PTR_ERR(dev->f_enable);
        dev->f_mode = devm_regmap_field_alloc(&client->dev, regmap, mode_field);
        dev->f_gain = devm_regmap_field_alloc(&client->dev, regmap, gain_field);
        dev->f_offset = devm_regmap_field_alloc(&client->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(&client->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 등 커널 서브시스템 드라이버에서 널리 사용됩니다.

 

 

  17.7  레지스터 접근 테이블

 

콜백 함수 대신 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,
  };
 
접근 제어 우선순위: *_table과 *_reg() 콜백을 동시에 설정할 수 없습니다. precious 레지스터는 FIFO 포트처럼 읽기 자체가 부작용을 유발하는 레지스터입니다. debugfs의 register dump에서 자동 제외되어 디버깅 시 의도치 않은 데이터 손실을 방지합니다. no_ranges 필드를 사용하면 "이 범위를 제외한 나머지 전부"와 같은 역전 논리도 표현할 수 있습니다.

 

 

  17.8  레지스터 윈도우와 페이지 매핑

 

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

 

 

 
  /*
  * 디바이스 레지스터 구조 (위 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이 페이지 전환을 자동 관리하므로 드라이버 코드에서 페이지 선택 로직이 완전히 제거됩니다.

 

 

  17.9  다중 레지스터 연산

 

여러 레지스터를 원자적으로 읽거나 쓸 때 사용하는 고급 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 단위로 읽기/쓰기. 엔디안 변환 적용
  • regmap_raw_read/write: 연속 레지스터를 바이트 스트림으로 전송. 엔디안 변환 없음. val_bits > 8일 때만 사용 가능
  • regmap_noinc_read/write: 주소 증가 없이 동일 레지스터에 반복 접근. FIFO 포트 등에 사용. regmap_config.read_flag_mask 설정 필요할 수 있음

 

 

  17.10  regmap 캐시 심화

 

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

 

 
  /*
  * 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_RBTREE Red-Black Tree 희소(sparse) 레지스터 맵 O(사용 레지스터 수)
REGCACHE_MAPLE Maple Tree v6.4+, 범위 기반 최적화 O(사용 레지스터 수)
캐시 타입 선택 가이드max_register가 작고 대부분의 레지스터를 사용하면 REGCACHE_FLAT이 가장 빠릅니다 (O(1) 접근). 레지스터 주소가 넓게 분산되어 있으면 REGCACHE_RBTREE나 REGCACHE_MAPLE이 메모리 효율적입니다. REGCACHE_MAPLE은 v6.4에서 추가되었으며, 연속 범위 탐색이 rbtree보다 캐시 친화적입니다.

 

 

  17.11  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로 전체 복원이 필요합니다

 

 

  17.12  regmap debugfs 디버깅

 

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

 
  # regmap debugfs 위치
  /sys/kernel/debug/regmap/
 
  # 디바이스별 디렉토리 예시
  /sys/kernel/debug/regmap/0-001a/ # I2C bus 0, addr 0x1a
 
  # 레지스터 값 확인
  $ cat /sys/kernel/debug/regmap/0-001a/registers
  00: 0042
  01: 001f
  02: 0000
  03: abcd
  ...
 
  # 접근 권한 확인
  $ cat /sys/kernel/debug/regmap/0-001a/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/0-001a/cache_only
  N # Y=캐시 전용 모드 활성
 

항  목 의  미
name regmap 이름
range 레지스터 주소 범위
registers 레지스터 덤프 (precious 제외)
access 읽기/쓰기/volatile/precious 권한 정보
cache_only 캐시 전용 모드 상태
 
  /* debugfs에서 regmap 이름 지정 */
  static const struct regmap_config my_config = {
          .name = "main", /* debugfs 디렉토리에 이름 표시 */
          .reg_bits = 8,
          .val_bits = 16,
          /* ... */
  };
 
  /* MFD 등에서 여러 regmap이 있을 때 구분에 유용:
   * /sys/kernel/debug/regmap/0-001a-main/
   * /sys/kernel/debug/regmap/0-001a-gpio/
   */
 

 

디버깅 팁: registers 파일에 값을 쓰면 런타임에 레지스터를 변경할 수 있습니다: echo "01 abcd" > registers. 이는 프로토타이핑 시 매우 유용하지만, precious 레지스터는 dump에서 자동 제외되므로 FIFO 데이터가 의도치 않게 소비되는 문제를 방지합니다. regmap 이름을 지정하면 ftrace의 regmap 이벤트에서도 구분하여 필터링할 수 있습니다: echo 'name == "main"' > /sys/kernel/debug/tracing/events/regmap/filter

 

 

  17.13  MFD 디바이스와 regmap 공유

 

Multi-Function Device(MFD)에서는 하나의 regmap을 여러 서브 디바이스가 공유합니다. 부모 MFD 드라이버가 regmap을 생성하고, 자식 드라이버가 dev_get_regmap()으로 접근합니다.

 
  /* ===== MFD 부모 드라이버 (PMIC 예시) ===== */
 
  static const struct regmap_config pmic_regmap_config = {
          .reg_bits = 8,
          .val_bits = 8,
          .max_register = 0xFF,
          .cache_type = REGCACHE_RBTREE,
  };
 
  /* MFD 셀 정의 */
  static const struct mfd_cell pmic_cells[] = {
          { .name = "pmic-regulator" },
          { .name = "pmic-gpio" },
          { .name = "pmic-rtc" },
          { .name = "pmic-charger" },
  };
 
  static int pmic_i2c_probe(struct i2c_client *client)
  {
          struct regmap *regmap;
 
          /* regmap 생성: dev에 자동 연결됨 */
          regmap = devm_regmap_init_i2c(client, &pmic_regmap_config);
          if (IS_ERR(regmap))
                  return PTR_ERR(regmap);
          /* MFD 서브디바이스 등록 */
          return devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE,
                                                                  pmic_cells, ARRAY_SIZE(pmic_cells),
                                                                  NULL, 0, NULL);
  }
 
  /* ===== MFD 자식 드라이버 (regulator 예시) ===== */
 
  static int pmic_regulator_probe(struct platform_device *pdev)
  {
          struct regmap *regmap;
 
          /* 부모 디바이스의 regmap 획득 */
          regmap = dev_get_regmap(pdev->dev.parent, NULL);
          if (!regmap) {
                  dev_err(&pdev->dev, "parent regmap not found\\n");
                  return -ENODEV;
          }
 
          /* 이제 공유 regmap으로 레지스터 접근 */
          regmap_write(regmap, 0x30, 0x01); /* regulator 제어 레지스터 */
 
          /* 또는 regmap_field로 특정 필드만 관리 */
          /* ... */
 
          return 0;
  }
 
  /* ===== 여러 regmap을 가진 MFD (이름으로 구분) ===== */
 
  /* 부모 드라이버: 두 개의 I2C 주소를 사용하는 PMIC */
  static int pmic_dual_probe(struct i2c_client *client)
  {
          struct regmap *regmap_main, *regmap_gpio;
          struct i2c_client *gpio_client;
          struct regmap_config cfg = pmic_regmap_config;
 
          /* 메인 regmap (이름 없음 = 기본) */
          regmap_main = devm_regmap_init_i2c(client, &cfg);
 
          /* 보조 I2C 주소의 regmap */
          gpio_client = devm_i2c_new_dummy_device(&client->dev,
                                 client->adapter, client->addr + 1);
          cfg.name = "gpio";
          regmap_gpio = devm_regmap_init_i2c(gpio_client, &cfg);
 
          /* ... */
  }
 
  /* 자식 드라이버: 이름으로 특정 regmap 획득 */
  regmap = dev_get_regmap(pdev->dev.parent, "gpio");
 

 

 

  17.14  regmap 고급 설정

 

regmap_config의 다양한 고급 옵션으로 복잡한 하드웨어 특성을 처리할 수 있습니다.

 
  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 사용 여부 */
  };
 
 
reg_stride 활용: MMIO 레지스터가 4바이트 정렬일 때 reg_stride = 4로 설정하면 regmap_read(regmap, 0x04, &val)이 실제 오프셋 0x04에 접근합니다. stride가 없으면 레지스터 번호와 바이트 오프셋을 혼동하기 쉽습니다. reg_stride의 배수가 아닌 주소로의 접근은 자동으로 거부(-EINVAL)됩니다.

 

 

  17.15  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:
  # 0-001a reg=01 val=1234
 
  # 특정 디바이스만 추적
  $ echo 'name == "0-001a"' > 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로 마킹된 레지스터가 성능 병목을 유발할 수 있습니다.

 

 

  18. pinctrl: 핀 멀티플렉싱

 

pinctrl 서브시스템은 SoC의 핀 멀티플렉싱(pinmux)과 핀 설정(pinconf)을 관리합니다.

GPIO 서브시스템과 밀접하게 연동되며, 하나의 물리 핀이 GPIO, I2C SDA, SPI MOSI 등 여러 기능 중 하나로 설정될 수 있습니다.

 

  18.1  pinctrl 핵심 개념

 

개    념 설    명 예    시
Pin Group 함께 설정되는 핀 그룹 i2c1_pins: {SDA, SCL}
Function 핀 그룹이 수행하는 기능 i2c, spi, gpio, uart
pinmux 핀과 기능의 매핑 PA9 → I2C1_SDA
pinconf 핀 전기적 특성 설정 풀업, 드라이브 강도, 슬루율
State 디바이스 상태별 핀 설정 default, sleep, idle

 

 

  18.2  Device Tree pinctrl 바인딩

 

 
  /* SoC pinctrl 노드에서 핀 설정 정의 */
  &pinctrl {
          i2c1_default: i2c1-default-pins {
                  pins = "PA9", "PA10";
                  function = "i2c1";
                  bias-pull-up;
                  drive-open-drain;
          };
 
          i2c1_sleep: i2c1-sleep-pins {
                  pins = "PA9", "PA10";
                  function = "gpio";
                  bias-high-impedance;
          };
 
          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;
                  };
          };
 
          user_led_pin: user-led-pin {
                  pins = "PC13";
                  function = "gpio";
                  drive-push-pull;
                  output-low;
          };
  };
 
  /* 디바이스 노드에서 pinctrl 상태 참조 */
  &i2c1 {
          pinctrl-names = "default", "sleep";
          pinctrl-0 = <&i2c1_default>;
          pinctrl-1 = <&i2c1_sleep>;
          status = "okay";
  };
 
  &spi1 {
          pinctrl-names = "default";
          pinctrl-0 = <&spi1_default>;
          status = "okay";
  };
 
pinctrl 자동 전환: 디바이스가 pm_runtime_suspend()에 들어가면 커널이 자동으로 "sleep"상태의 핀 설정을 적용하고, resume 시 "default"로 복원합니다. 이 동작은 pinctrl-names에 "default"와 "sleep"이 정의되어 있을 때 활성화됩니다.

 

 

  19. Device Tree 통합: 공통 바인딩 패턴

 

I2C, SPI, GPIO 서브시스템의 Device Tree 바인딩에서 공통적으로 사용되는 패턴을 정리합니다.

 

  19.1  공통 프로퍼티

 

프로퍼티적용 대상설명

프 로 퍼 티 적 용    대 상 설    명
compatible 모든 디바이스 드라이버 매칭 문자열 (vendor,device)
reg I2C: 슬레이브 주소, SPI: CS 번호 버스별 주소/식별자
interrupts 인터럽트 사용 디바이스 IRQ 스펙
interrupt-parent 인터럽트 사용 디바이스 IRQ 컨트롤러 phandle
status 모든 노드 "okay", "disabled"
*-gpios GPIO 사용 디바이스 GPIO specifier
*-supply 전원 사용 디바이스 regulator phandle
pinctrl-* 핀 설정 필요 디바이스 pinctrl 상태

 

 

  19.2  종합 예제: I2C + SPI + GPIO 연동

 

실제 임베디드 보드에서 I2C 센서, SPI Flash, GPIO LED/버튼을 함께 사용하는 Device Tree 예제:

 
  /  {
          model = "My Custom Board";
          compatible = "vendor,my-board";
 
          leds {
                  compatible = "gpio-leds";
                  pinctrl-names = "default";
                  pinctrl-0 = <&user_led_pin>;
 
                  led-status {
                          gpios = <&gpioc 13 GPIO_ACTIVE_LOW>;
                          label = "board:green:status";
                          linux,default-trigger = "heartbeat";
                  };
          };
 
          gpio-keys {
                  compatible = "gpio-keys";
 
                  button-user {
                          label = "User Button";
                          gpios = <&gpioa 0 GPIO_ACTIVE_LOW>;
                          linux,code = <KEY_ENTER>;
                          debounce-interval = <20>;
                  };
          };
  };
 
  &i2c1 {
          status = "okay";
          clock-frequency = <400000>;
          pinctrl-names = "default", "sleep";
          pinctrl-0 = <&i2c1_default>;
          pinctrl-1 = <&i2c1_sleep>;
 
          /* 온습도 센서 */
          htu21d@40 {
                  compatible = "meas,htu21";
        reg = <0x40>;
          };
 
          /* 가속도계 */
          accelerometer@1d {
                  compatible = "st,lis3dh";
                  reg = <0x1D>;
                  interrupt-parent = <&gpiob>;
                  interrupts = <5 IRQ_TYPE_EDGE_RISING>;
                  vdd-supply = <&reg_3v3>;
          };
 
          /* GPIO expander */
          gpio_exp: gpio@20 {
                  compatible = "nxp,pca9555";
                  reg = <0x20>;
                  gpio-controller;
                  #gpio-cells = <2>;
                  interrupt-parent = <&gpiob>;
                  interrupts = <8 IRQ_TYPE_EDGE_FALLING>;
                  interrupt-controller;
                  #interrupt-cells = <2>;
          };
  };
 
  &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 (GPIO expander의 핀을 CS로 사용) */
          adc@1 {
                  compatible = "vendor,my-adc";
                  reg = <1>;
                  spi-max-frequency = <5000000>;
                  vref-supply = <&reg_3v3>;
          };
  };
 

 

  19.3  Device Tree 디버깅

 

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

 
  # I2C 버스 및 디바이스 확인
  i2cdetect -l # 시스템의 I2C 어댑터 목록
  i2cdetect -y 1 # I2C bus 1의 디바이스 스캔
  i2cget -y 1 0x48 0x00 # 0x48 디바이스의 레지스터 0x00 읽기
  i2cdump -y 1 0x48 # 전체 레지스터 덤프
 
  # SPI 디바이스 확인
  ls /sys/bus/spi/devices/ # 등록된 SPI 디바이스
  ls /sys/class/spi_master/ # SPI 컨트롤러
 
  # GPIO 상태 확인
  gpiodetect # GPIO 칩 목록
  gpioinfo # 모든 GPIO 라인 정보
  cat /sys/kernel/debug/gpio # debugfs GPIO 상태
  cat /sys/kernel/debug/pinctrl/*/pins # pinctrl 핀 매핑
 
  # Device Tree 런타임 확인
l  s /proc/device-tree/ # DT 노드 트리
  dtc -I fs /proc/device-tree/ # 런타임 DT를 DTS로 디컴파일
 
I2C 디바이스가 감지되지 않을 때 체크리스트: 
  (1) i2cdetect로 주소 응답 확인,
  (2) dmesg | grep i2c로 어댑터 등록 확인,
  (3) Device Tree의 reg 속성이 실제 하드웨어 주소와 일치하는지 확인,
  (4) pinctrl 설정이 올바른지 확인 (SDA/SCL 핀이 I2C 기능으로 mux 되었는지),
  (5) 풀업 저항이 있는지 확인 (오픈 드레인 버스에 외부 풀업 필요).

 

 

  19.4  I2C / SPI / GPIO 비교 요약

 

특  성 I2C SPI GPIO
신호선 2 (SCL, SDA) 4+ (MOSI, MISO, SCK, CS) 1/핀
통신 방식 반이중 전이중 단방향 (입력 또는 출력)
최대 속도 3.4 MHz (Hs) 수백 MHz N/A
어드레싱 7/10비트 주소 CS 라인 컨트롤러+오프셋
커널 헤더 <linux/i2c.h> <linux/spi/spi.h> <linux/gpio/consumer.h>
DT reg 의미 슬레이브 주소 CS 번호 base + ngpio
regmap 지원 regmap_init_i2c regmap_init_spi N/A
대표 디바이스 센서, EEPROM, RTC Flash, ADC, 디스플레이 LED, 버튼, 리셋

 

.  CAN Bus (Controller Area Network)

CAN Bus와 Linux SocketCAN을 실시간 제어 네트워크 관점에서 심층 분석합니다. 클래식 CAN과 CAN FD 프레임 구조, 비트 타이밍과 오류 프레임 처리, net_device 기반 드라이버 모델, SocketCAN RAW/BCM/ISOTP 소켓 활용, 버스 오프 복구와 상태 모니터링, timestamp/queue 설정을 통한 지연시간 관리, 차량·산업 장비 환경에서의 진단 프레임 운용, candump/cansniffer/iproute2 기반 디버깅 절차까지 안정적 필드 운영을 위한 핵심 내용을 다룹니다.

전제 조건: 네트워크 스택과 디바이스 드라이버 문서를 먼저 읽으세요. 특수 패브릭은 일반 이더넷과 다른 전송 제약과 하드웨어 모델을 가지므로, 버스 특성을 먼저 고정하고 접근해야 합니다.
일상 비유: 이 주제는 전용 철도 노선 운영과 비슷합니다. 일반 도로 규칙으로는 설명되지 않는 전용 신호/차량 규격이 있듯이, CAN/RDMA는 별도 운용 규칙이 필요합니다.

 

 

  1.  핵심 요약

 

  • 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
  • 큐/버퍼 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
  • 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
  • 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
  • 오프로딩 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.

 

  2.  단계별 이해

 

    1. 경로 고정

        문제가 발생한 ingress/egress 지점을 먼저 특정합니다.

    2. 큐 관찰

        백로그와 드롭 위치를 계측합니다.

    3. 정책 반영 확인

        라우팅/필터 변경이 데이터 경로에 반영됐는지 봅니다.

    4.부하 검증

        실제 트래픽 패턴에서 재현성을 확인합니다.

관련 문서: 네트워크 스택 (프로토콜 스택), 디바이스 드라이버 (CAN 컨트롤러), I2C/SPI/GPIO(MCP2515 SPI-CAN), USB (USB-CAN 어댑터)

 

 

  3.  개요

 

CAN(Controller Area Network)은 Robert Bosch GmbH가 1980년대에 개발한 멀티마스터 시리얼 버스 프로토콜입니다.

 

  3.1  CAN 특성

 

  • 멀티마스터 — 모든 노드가 버스 마스터 역할 가능
  • 메시지 우선순위 — CAN ID로 결정 (낮은 ID = 높은 우선순위)
  • 브로드캐스트 — 모든 노드가 모든 메시지 수신
  • 에러 검출 — CRC, ACK, Bit Monitoring, Frame Check
  • Fault Confinement — 결함 노드 자동 격리
  • 전송 속도 — 10 Kbps ~ 1 Mbps (CAN 2.0), 최대 8 Mbps (CAN FD)

 

  3.2  CAN 계층

 

계  층 설  명
Application Layer CANopen, J1939, UDS, ISO-TP
Data Link Layer CAN 2.0A/B (Standard/Extended ID)
Physical Layer CAN High/Low 차동 신호 (ISO 11898)

 

 

 

  4.  CAN 통신과 RS-485 의 비교

 

차량 제어나 산업 현장에서 가장 흔히 비교되는 두 통신 방식, CAN (Controller Area Network) RS-485의 물리적 특성을 비교해 드릴게요.

두 방식 모두 '차동 신호(Differential Signaling)'를 사용하여 노이즈에 강하다는 공통점이 있지만, 그 내부 동작은 꽤 다릅니다.

 

 

  4.1  하드웨어 구성 및 신호 방식

 

가장 큰 차이는 신호가 전압으로 어떻게 표현되느냐입니다.

  • CAN 통신: 신호가 **Dominant(논리 0)**와 Recessive(논리 1) 상태로 나뉩니다. 두 선(CAN_H, CAN_L)의 전압 차이가 발생하면 Dominant, 없으면 Recessive입니다.
  • RS-485: 두 선(A, B) 사이의 전압 극성(+/-)에 따라 데이터를 구분합니다.
항목 CAN (ISO 11898) RS-485 (TIA/EIA-485)
신호선 CAN_H, CAN_L (2선) A, B (2선 / 4선 Full-duplex 가능)
전압 상태 Dominant / Recessive Differential Voltage (+ / -)
종단 저항 양 끝단 120Ω 양 끝단 120Ω
전송 방식 반이중 (Half-Duplex) 반이중 또는 전이중 (Full-Duplex)
 

 

  4.2  물리 계층의 핵심 차이

 

1. 중재(Arbitration) 능력

  CAN의 가장 똑똑한 점은 충돌 방지입니다. 두 노드가 동시에 데이터를 보내면, 전압 특성상 Dominant(0)가 Recessive(1)를 이깁니다. 덕분에 데이터 충돌 없이 우선순위가 높은 메시지가 먼저 지나갑니다. 반면, RS-485는 두 노드가 동시에 보내면 신호가 깨져버립니다(Collision). 이를 막기 위해 소프트웨어적으로 "누구 차례인지" 관리하는 마스터-슬레이브 구조가 필수적입니다.

 

2. 연결 토폴로지

  • CAN: 멀티-마스터 구조입니다. 어떤 노드든 원할 때 데이터를 보낼 수 있습니다.
  • RS-485: 주로 1개의 마스터가 여러 슬레이브를 관리하는 구조에 최적화되어 있습니다.

 

  4.3  사양 및 성능 비교

 

특성 CAN RS-485
최대 속도 1 Mbps (CAN FD는 최대 5~8Mbps) 10 Mbps (거리에 따라 다름)
최대 거리 40m (1Mbps 기준) ~ 1km (50kbps) 최대 1.2km (100kbps 기준)
노드 수 통상 32~127개 (트랜시버 성능에 따름) 최대 32개 (Unit Load에 따라 256개까지)
결함 허용 매우 높음 (에러 감지 및 자동 재전송) 낮음 (사용자가 에러 처리를 구현해야 함)
 

 

  4.4  요약: 무엇을 선택해야 할까요?

 

  • CAN을 선택해야 하는 경우: * 시스템의 안전성과 신뢰성이 최우선일 때 (자동차, 엘리베이터).
    • 여러 노드가 마스터의 허락 없이 자유롭게 데이터를 보내야 할 때.
    • 하드웨어 차원에서 자동 에러 검출이 필요할 때.
  • RS-485를 선택해야 하는 경우:
    • 단순히 긴 거리(1km 이상)에 데이터를 저렴하게 보내고 싶을 때.
    • 전송 속도가 매우 빨라야 할 때 (단거리 10Mbps).
    • 기존의 시리얼(UART) 프로토콜을 그대로 활용하고 싶을 때.

 

 

  5.  SocketCAN

 

SocketCAN은 CAN을 Linux 네트워크 서브시스템에 통합하여 BSD 소켓 API로 CAN 통신을 제공합니다.

 

  5.1  SocketCAN 아키텍쳐

 

 

 

 

  6.  CAN Frame

 

  6.1  CAN 2.0 프레임 구조

 

 
  #include <linux/can.h>
 
  struct can_frame {
          canid_t can_id;                     /* 11-bit (Standard) or 29-bit (Extended) */
          __u8 can_dlc;                       /* Data Length Code (0~8) */
          __u8 __pad;                          /* padding */
          __u8 __res0;                         /* reserved */
          __u8 __res1;                          /* reserved */
          __u8 data[8];                        /* CAN payload (0~8 bytes) */
  };
 
  /* CAN ID 플래그 */
  #define CAN_EFF_FLAG 0x80000000U     /* Extended Frame Format */
  #define CAN_RTR_FLAG 0x40000000U    /* Remote Transmission Request */
  #define CAN_ERR_FLAG 0x20000000U    /* Error Frame */
 
  #define CAN_SFF_MASK 0x000007FFU    /* Standard Frame ID Mask */
  #define CAN_EFF_MASK 0x1FFFFFFFU      /* Extended Frame ID Mask */
 

 

 

  6.2  CAN FD 프레임

 

CAN FD(Flexible Data-rate)는 최대 64바이트 페이로드와 높은 전송 속도를 지원합니다.

 
  #include <linux/can.h>
 
  struct canfd_frame {
          canid_t can_id;             /* 29-bit CAN ID + flags */
          __u8 len;                        /* Data Length (0~64) */
          __u8 flags;                     /* CANFD_BRS, CANFD_ESI */
          __u8 __res0;                 /* reserved */
          __u8 __res1;                  /* reserved */
          __u8 data[64];              /* CAN FD payload (0~64 bytes) */
  };
 
  #define CANFD_BRS 0x01  /* Bit Rate Switch (faster data rate) */
  #define CANFD_ESI 0x02   /* Error State Indicator */

 

 

  7.  SocketCAN API

 

  7.1  CAN RAW 소켓

 

 
  #include <sys/socket.h>
  #include <linux/can.h>
  #include <linux/can/raw.h>
  #include <net/if.h>
 
  int main(void)
  {
          int s;
          struct sockaddr_can addr;
          struct ifreq ifr;
 
          /* CAN RAW 소켓 생성 */
          s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
          if (s < 0) {
                  perror("socket");
                  return 1;
          }
 
          /* 인터페이스 이름 → 인덱스 */
          strcpy(ifr.ifr_name, "can0");
          ioctl(s, SIOCGIFINDEX, &ifr);
 
          /* 소켓 바인딩 */
          memset(&addr, 0, sizeof(addr));
          addr.can_family = AF_CAN;
          addr.can_ifindex = ifr.ifr_ifindex;
 
          if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
                  perror("bind");
                  return 1;
          }
 
          printf("CAN socket bound to can0\\n");
 
          /* CAN 프레임 송신 */
          struct can_frame frame;
          frame.can_id = 0x123; /* CAN ID */
          frame.can_dlc = 4; /* 4 bytes */
          frame.data[0] = 0xDE;
          frame.data[1] = 0xAD;
          frame.data[2] = 0xBE;
          frame.data[3] = 0xEF;
 
          if (write(s, &frame, sizeof(frame)) != sizeof(frame)) {
                  perror("write");
                  return 1;
          }
 
          /* CAN 프레임 수신 */
          ssize_t nbytes = read(s, &frame, sizeof(frame));
          if (nbytes < 0) {
                  perror("read");
                  return 1;
          }
 
          printf("Received CAN frame: ID=0x%03X DLC=%d Data=",
                    frame.can_id, frame.can_dlc);
          for (int i = 0; i < frame.can_dlc; i++)
                  printf("%02X ", frame.data[i]);
          printf("\\n");
 
          close(s);
          return 0;
  }
 

 

 

  7.2  CAN 필터링

 

 
  /* CAN ID 필터 설정 */
  struct can_filter rfilter[2];
 
  /* 필터 1: ID 0x100~0x1FF 수신 */
  rfilter[0].can_id = 0x100;
  rfilter[0].can_mask = 0x700; /* 마스크: 0x100, 0x101, ..., 0x1FF */
 
  /* 필터 2: ID 0x200 정확히 매칭 */
  rfilter[1].can_id = 0x200;
  rfilter[1].can_mask = CAN_SFF_MASK; /* 0x7FF (모든 비트 매칭) */
 
  setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
 
  /* 모든 CAN ID 수신 (필터 비활성화) */
  setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
 

 

 

  7.3  에러 프레임

 

 
  /* 에러 프레임 수신 활성화 */
  can_err_mask_t err_mask = CAN_ERR_MASK;
  setsockopt(s, SOL_CAN_RAW, CAN_RAW_ERR_FILTER, &err_mask, sizeof(err_mask));
 
  /* 에러 프레임 수신 */
  struct can_frame frame;
  read(s, &frame, sizeof(frame));
 
  if (frame.can_id & CAN_ERR_FLAG) {
          printf("Error frame received:\\n");
          if (frame.can_id & CAN_ERR_TX_TIMEOUT)
                  printf(" TX timeout\\n");
          if (frame.can_id & CAN_ERR_BUSOFF)
                  printf(" Bus-off\\n");
          if (frame.can_id & CAN_ERR_BUSERROR)
                  printf(" Bus error\\n");
  }
 

 

 

  8.  CAN FD 사용

 

 
  /* CAN FD 활성화 */
  int enable_canfd = 1;
  setsockopt(s, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &enable_canfd, sizeof(enable_canfd));
 
  /* CAN FD 프레임 송신 */
  struct canfd_frame fdframe;
  fdframe.can_id = 0x123;
  fdframe.len = 16; /* 16 bytes */
  fdframe.flags = CANFD_BRS; /* Bit Rate Switch */
  memset(fdframe.data, 0xAA, 16);
 
  write(s, &fdframe, sizeof(fdframe));
 
  /* CAN FD 프레임 수신 */
  ssize_t nbytes = read(s, &fdframe, sizeof(fdframe));
  if (nbytes == CANFD_MTU) {
          printf("CAN FD frame: len=%d\\n", fdframe.len);
  }
 

 

 

 

  8.  상위 프로토콜

 

  8.1  ISO-TP (ISO 15765-2)

 

ISO-TP는 8바이트를 초과하는 데이터를 여러 CAN 프레임으로 분할/재조합하는 전송 프로토콜입니다.

 
  #include <linux/can/isotp.h>
 
  /* ISO-TP 소켓 생성 */
  int s = socket(PF_CAN, SOCK_DGRAM, CAN_ISOTP);
 
  /* 주소 설정 (TX=0x123, RX=0x456) */
  struct sockaddr_can addr = {
          .can_family = AF_CAN,
          .can_ifindex = ifr.ifr_ifindex,
          .can_addr.tp.tx_id = 0x123, /* 송신 CAN ID */
          .can_addr.tp.rx_id = 0x456, /* 수신 CAN ID */
  };
 
  bind(s, (struct sockaddr *)&addr, sizeof(addr));
 
  /* 긴 데이터 송신 (예: 100 바이트) */
  char data[100];
  memset(data, 0x55, 100);
  write(s, data, 100); /* 자동으로 여러 CAN 프레임으로 분할 */
 
  /* 긴 데이터 수신 */
  char recv_buf[1000];
  ssize_t len = read(s, recv_buf, sizeof(recv_buf));
  printf("Received %zd bytes via ISO-TP\\n", len);
 

 

 

  8.2  J1939 (SAE J1939)

 

J1939는 상용차/농기계에서 사용되는 CAN 기반 상위 프로토콜입니다.

 
  #include <linux/can/j1939.h>
 
  /* J1939 소켓 */
  int s = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);
 
  /* J1939 주소 설정 */
  struct sockaddr_can addr = {
          .can_family = AF_CAN,
          .can_ifindex = ifr.ifr_ifindex,
          .can_addr.j1939.name = J1939_NO_NAME,
          .can_addr.j1939.addr = 0x80, /* Source Address */
          .can_addr.j1939.pgn = 0xEF00, /* Parameter Group Number */
  };
 
  bind(s, (struct sockaddr *)&addr, sizeof(addr));
 
  /* J1939 메시지 송신 */
  uint8_t data[8] = {1, 2, 3, 4, 5, 6, 7, 8};
  sendto(s, data, 8, 0, (struct sockaddr *)&addr, sizeof(addr));
 

 

 

  9.  Virtual CAN (vcan)

 

vcan은 실제 하드웨어 없이 CAN 통신을 테스트할 수 있는 가상 인터페이스입니다.

 
  # vcan 모듈 로드
  $ sudo modprobe vcan
 
  # vcan0 인터페이스 생성
  $ sudo ip link add dev vcan0 type vcan
  $ sudo ip link set up vcan0
 
  # vcan0 확인
  $ ip link show vcan0
  4: vcan0: <NOARP,UP,LOWER_UP> mtu 72 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
          link/can
 
  # vcan0 삭제
  $ sudo ip link delete vcan0
 

 

 

  10. can-utils 도구

 

  10.1  cansend - CAN 프레임 송신

 

 
  # can-utils 설치
  $ sudo apt install can-utils
 
  # CAN 프레임 송신 (ID=0x123, 데이터=DEADBEEF)
  $ cansend can0 123#DEADBEEF
 
  # Extended ID (ID=0x12345678)
  $ cansend can0 12345678#1122334455667788
 
  # RTR (Remote Transmission Request) 프레임
  $ cansend can0 123#R
 

 

 

  10.2  candump - CAN 프레임 수신

 

 
  # can0의 모든 프레임 출력
  $ candump can0
      can0 123 [4] DE AD BE EF
     can0 456 [8] 01 02 03 04 05 06 07 08
 
  # 타임스탬프 포함
  $ candump can0 -t a
      (000.000000) can0 123 [4] DE AD BE EF
 
  # 로그 파일 저장
  $ candump can0 -l
  log file created: candump-2026-02-11_123456.log
 
  # 특정 ID 필터
  $ candump can0,123:7FF # ID 0x100~0x1FF만
 

 

 

  10.3  cangen - CAN 트래픽 생성

 

 
  # 랜덤 CAN 프레임 생성 (초당 100개)
  $ cangen can0 -g 10
 
  # 특정 ID로 고정 (ID=0x100)
  $ cangen can0 -I 100 -L 8 -D i -g 1
 
  # CAN FD 프레임 생성
  $ cangen can0 -f
 

 

 

  10.4  canplayer - CAN 로그 재생

 

 
  # 로그 파일 재생
  $ canplayer -I candump.log
 
  # 타이밍 유지하며 재생
  $ canplayer -t -I candump.log
 

 

 

  11.  CAN 하드웨어

 

  11.1  slcan (Serial Line CAN)

 

USB-CAN 어댑터 등 시리얼 포트로 연결된 CAN 인터페이스입니다.

  # slcan 설정 (예: /dev/ttyUSB0, 500kbps)
  $ sudo slcand -o -s6 -t hw -S 3000000 /dev/ttyUSB0 slcan0
  $ sudo ip link set up slcan0
 
  # Baudrate 코드
  # s0 = 10 Kbps
  # s3 = 100 Kbps
  # s4 = 125 Kbps
  # s5 = 250 Kbps
  # s6 = 500 Kbps
  # s8 = 1 Mbps

 

 

  11.2  MCP2515 (SPI CAN 컨트롤러)

 

 
  * Device Tree 오버레이 (Raspberry Pi) */
  /* /boot/config.txt에 추가 */
  dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25,spimaxfrequency=2000000
 
  # 재부팅 후 can0 인터페이스 확인
  $ ip link show can0
 
  # Bitrate 설정
  $ sudo ip link set can0 type can bitrate 500000
  $ sudo ip link set up can0
 

 

 

  12.  CAN 인터페이스 설정

 

  12.1  Bitrate 설정

 

 
  # 500 Kbps 설정
  $ sudo ip link set can0 type can bitrate 500000
 
  # Sample Point 조정 (기본: 87.5%)
  $ sudo ip link set can0 type can bitrate 500000 sample-point 0.875
 
  # CAN FD 설정 (Nominal 500Kbps, Data 2Mbps)
  $ sudo ip link set can0 type can bitrate 500000 dbitrate 2000000 fd on
 
  # 인터페이스 활성화
  $ sudo ip link set up can0
 
  # 통계 확인
  $ ip -details -statistics link show can0
 

 

 

  12.2  자동 재시작

 

 
  # Bus-off 상태에서 자동 재시작 (100ms 후)
  $ sudo ip link set can0 type can restart-ms 100
 
  # 수동 재시작
  $ sudo ip link set can0 type can restart
 

 

 

  13.  커널 드라이버

 

  13.1  CAN 디바이스 드라이버 구조

 

 
  #include <linux/can/dev.h>
 
  struct can_priv {
          struct can_device_stats can_stats;
          struct can_bittiming bittiming;
          struct can_bittiming_const bittiming_const;
          enum can_state state;
          u32 ctrlmode;
          int restart_ms;
  };
 
  /* CAN 네트워크 디바이스 등록 */
  struct net_device *alloc_candev(int sizeof_priv, unsigned int echo_skb_max);
  int register_candev(struct net_device *dev);
  void unregister_candev(struct net_device *dev);
  void free_candev(struct net_device *dev);
 

 

 

  14.  디버깅

 

  14.1  CAN 에러 상태

 

 
  /* CAN 컨트롤러 상태 */
  enum can_state {
          CAN_STATE_ERROR_ACTIVE, /* 정상 */
          CAN_STATE_ERROR_WARNING, /* TEC/REC > 96 */
          CAN_STATE_ERROR_PASSIVE, /* TEC/REC > 127 */
          CAN_STATE_BUS_OFF, /* TEC > 255, 버스 격리 */
          CAN_STATE_STOPPED,
          CAN_STATE_SLEEPING,
  };
 
  # 에러 카운터 확인
  $ ip -details link show can0
   . . .
          can state ERROR-ACTIVE (berr-counter tx 0 rx 0) restart-ms 0
 

 

 

  15. 커널 설정

 

 
  CONFIG_CAN=y # CAN bus 서브시스템
  CONFIG_CAN_RAW=y # CAN RAW 프로토콜
  CONFIG_CAN_BCM=y # CAN Broadcast Manager
  CONFIG_CAN_GW=y # CAN Gateway
  CONFIG_CAN_ISOTP=m # ISO-TP (15765-2)
  CONFIG_CAN_J1939=m # SAE J1939
 
  # 가상 CAN
  CONFIG_CAN_VCAN=m # Virtual CAN
  CONFIG_CAN_SLCAN=m # Serial Line CAN
 
  # 하드웨어 드라이버
  CONFIG_CAN_DEV=y # CAN Device Drivers
  CONFIG_CAN_MCP251X=m # Microchip MCP251x SPI
  CONFIG_CAN_M_CAN=m # Bosch M_CAN
  CONFIG_CAN_C_CAN=m # Bosch C_CAN/D_CAN
 

 

 

  16. 참고자료

 

 

 

 

'Embedded : : Architecture > : : Peripheral' 카테고리의 다른 글

PCIe Interrupt / PCIe states  (0) 2024.11.12
PCIe Bus  (0) 2024.11.12
DSI / CSI  (0) 2024.11.07
MCU : DIE, PAD, Drive Strength, Pad Level, Hysteresis  (0) 2024.05.04
UART / I2C / SPI  (0) 2023.11.13