SPI(Serial Peripheral Interface) 모듈은 고속, 고효율의 동기 직렬 인터페이스 기술입니다. 일반적으로 마스터(Master) 모듈 하나와 하나 이상의 슬레이브(Slave) 모듈로 구성되며, 마스터는 특정 슬레이브를 선택하여 데이터를 동기식으로 교환합니다. 이 기술은 ADC, LCD와 같은 다양한 임베디드 장치와 MCU 간의 통신에 널리 활용됩니다. 올위너(Allwinner) SoC에 통합된 SPI 컨트롤러는 다음과 같은 고급 기능을 지원합니다:
- 전이중 동기 직렬 통신.
- 다양한 클럭 소스 선택(5가지).
- 마스터 및 슬레이브 모드 지원.
- 최대 4개의 칩 셀렉트(CS) 라인.
- 8비트 데이터 폭 및 64바이트 FIFO(First-In, First-Out) 깊이.
- CS 및 클럭의 극성(Polarity)과 위상(Phase) 설정 가능.
- DMA(Direct Memory Access) 지원.
- 네 가지 통신 모드 지원 (CPOL/CPHA 조합).
- 최대 100MHz의 I/O 속도.
- 3선 및 4선 SPI 모드 지원.
- 프로그래밍 가능한 직렬 데이터 프레임 길이 (0~32비트).
- 표준 SPI, 듀얼 출력, 듀얼 입력, 듀얼 I/O SPI, 쿼드 출력, 쿼드 입력 SPI 지원.
주요 용어 정의
SPI 개발에서 사용되는 핵심 하드웨어 및 소프트웨어 용어는 다음과 같습니다:
- SPI (Serial Peripheral Interface): 직렬 주변 장치 인터페이스의 약자.
- Sunxi: 올위너(Allwinner) 사의 특정 SoC 하드웨어 플랫폼 제품군을 지칭합니다.
- SPI 마스터(Master): SPI 통신을 시작하고 클럭을 생성하여 데이터 전송을 제어하는 장치.
- SPI 슬레이브(Slave): SPI 마스터의 클럭 신호에 따라 데이터를 송수신하는 장치.
모듈 구성
디바이스 트리(Device Tree) 설정
Sunxi 플랫폼에서는 SPI 컨트롤러 수가 다양하지만, 각 컨트롤러의 디바이스 트리 설정은 유사합니다. 플랫폼 디바이스 트리 파일은 kernel/[커널 버전]/arch/arm64(/arm)/boot/dts/sunxi/CHIP.dtsi 경로에 있습니다. SPI1 컨트롤러를 위한 일반적인 설정 예시는 아래와 같습니다:
spi1_ctrl: spi@05011000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "allwinner,sun50i-spi"; /* 드라이버 바인딩용 호환성 문자열 */
reg = <0x0 0x05011000 0x0 0x1000>; /* 버스 레지스터 주소 */
interrupts = <GIC_SPI 13 IRQ_TYPE_LEVEL_HIGH>; /* 인터럽트 번호 및 유형 */
clocks = <&clk_pll_periph0>, <&clk_spi1>; /* 장치 사용 클럭 */
clock-frequency = <100000000>; /* 컨트롤러 클럭 주파수 */
pinctrl-names = "default", "sleep"; /* 핀 제어 상태 이름 */
pinctrl-0 = <&spi1_active_pins &spi1_cs0_pin>; /* 활성 핀 설정 */
pinctrl-1 = <&spi1_sleep_pins>; /* 절전 핀 설정 */
cs-gpios = <&pio 3 3 GPIO_ACTIVE_LOW>; /* GPIO 기반 CS 핀 (옵션) */
status = "disabled"; /* 컨트롤러 활성화 여부 */
};
Linux-5.4 커널 버전에서는 클럭 및 DMA 구성에 약간의 차이가 있습니다:
spi1_ctrl: spi@4026000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "allwinner,sun20i-spi"; /* 드라이버 바인딩용 호환성 문자열 */
reg = <0x0 0x04026000 0x0 0x1000>; /* 버스 레지스터 주소 */
interrupts-extended = <&plic0 32 IRQ_TYPE_LEVEL_HIGH>; /* 인터럽트 확장 설정 */
clocks = <&ccu CLK_PLL_PERIPH0>, <&ccu CLK_SPI1>, <&ccu CLK_BUS_SPI1>; /* 장치 클럭들 */
clock-names = "pll", "mod", "bus"; /* 클럭 이름 */
resets = <&ccu RST_BUS_SPI1>; /* 리셋 클럭 */
clock-frequency = <100000000>; /* 컨트롤러 클럭 주파수 */
dmas = <&dma 23>, <&dma 23>; /* DMA 채널 번호 */
dma-names = "tx", "rx"; /* DMA 채널 이름 */
status = "disabled"; /* 컨트롤러 활성화 여부 */
};
각 SPI 컨트롤러를 구별하기 위해 Device Tree의 aliases 노드에 별칭을 지정해야 합니다:
aliases {
soc_spi0 = &spi0_ctrl;
soc_spi1 = &spi1_ctrl;
/* ... 기타 SPI 컨트롤러 ... */
};
별칭은 "spi" 문자열과 연속 번호의 조합으로 지정됩니다. SPI 버스 드라이버는 of_alias_get_id() 함수를 사용하여 이 번호를 얻어 각 컨트롤러를 식별할 수 있습니다.
Linux-4.9 커널의 핀 제어 설정(spi1_active_pins, spi1_cs0_pin 등)은 kernel/linux-4.9/arch/arm64(/arm)/boot/dts/sunxi/xxx-pinctrl.dtsi에서 정의됩니다:
spi1_active_pins: spi1_pins@0 {
allwinner,pins = "PH4", "PH5", "PH6"; /* SCLK, MOSI, MISO */
allwinner,pname = "spi1_sclk", "spi1_mosi", "spi1_miso";
allwinner,function = "spi1";
allwinner,muxsel = <2>;
allwinner,drive = <1>;
allwinner,pull = <0>;
};
spi1_cs0_pin: spi1_cs@1 {
allwinner,pins = "PH3"; /* CS0 */
allwinner,pname = "spi1_cs0";
allwinner,function = "spi1";
allwinner,muxsel = <2>;
allwinner,drive = <1>;
allwinner,pull = <1>; /* CS는 풀업 권장 */
};
spi1_sleep_pins: spi1_sleep@2 {
allwinner,pins = "PH3", "PH4", "PH5", "PH6";
allwinner,function = "io_disabled"; /* 절전 시 비활성화 */
allwinner,muxsel = <7>;
allwinner,drive = <1>;
allwinner,pull = <0>;
};
Linux-5.4 커널의 핀 제어 설정은 다음과 같습니다:
spi1_active_pins: spi1_pins_a {
pins = "PD11", "PD12", "PD13"; /* SCLK, MOSI, MISO */
function = "spi1";
drive-strength = <10>;
};
spi1_cs0_pin: spi1_pins_b {
pins = "PD10"; /* CS0 */
function = "spi1";
drive-strength = <10>;
bias-pull-up; /* CS는 풀업 권장 */
};
spi1_sleep_pins: spi1_pins_c {
pins = "PD10", "PD11", "PD12", "PD13";
function = "gpio_in"; /* 절전 시 GPIO 입력으로 설정 */
};
board.dts 설정
board.dts 파일은 보드별 하드웨어 차이를 보완하는 데 사용되며, 상위 .dtsi 파일의 기본 설정을 덮어씁니다. 이 파일은 /device/config/chips/{IC}/configs/{BOARD}/board.dts 경로에 위치합니다. SPI1에 대한 예시 설정은 다음과 같습니다.
참고: Linux-5.4 커널에서는 board.dts 문법이 변경되어, 동일 이름 노드 덮어쓰기가 지원되지 않으며, "&" 기호를 사용하여 노드를 참조해야 합니다.
&spi1_ctrl {
clock-frequency = <100000000>;
pinctrl-0 = <&spi1_active_pins &spi1_cs0_pin>;
pinctrl-1 = <&spi1_sleep_pins>;
pinctrl-names = "default", "sleep";
spi_slave_mode = <0>; /* 0: 마스터, 1: 슬레이브 */
status = "disabled";
spi_device_flash@0 {
device_type = "spi_nor_flash"; /* 장치 유형 */
compatible = "acme,flash-mem"; /* 드라이버 호환성 */
spi-max-frequency = <0x5f5e100>; /* 최대 주파수 */
reg = <0x0>; /* 칩 셀렉트 번호 (CS0) */
spi-rx-bus-width = <0x4>; /* 수신 버스 폭 */
spi-tx-bus-width = <0x4>; /* 송신 버스 폭 */
status = "disabled";
};
};
SPI 슬레이브 모드를 사용하려면 spi_slave_mode = <0>를 spi_slave_mode = <1>로 변경해야 합니다.
spi_device_flash 노드에는 다음과 같은 추가 구성 가능한 매개변수가 있습니다:
spi-cpha및spi-cpol: SPI의 4가지 전송 모드 설정 (모드 0-3).spi-cs-high: CS 핀의 활성 상태 레벨 설정 (높음 또는 낮음).
spi1_active_pins, spi1_cs0_pin, spi1_sleep_pins의 board.dts 내 오버라이드 구성은 다음과 같습니다:
&spi1_active_pins {
pins = "PD11", "PD12", "PD13", "PD14", "PD15"; /* CLK, MOSI, MISO, HOLD, WP */
function = "spi1";
drive-strength = <10>;
};
&spi1_cs0_pin {
pins = "PD10";
function = "spi1";
drive-strength = <10>;
bias-pull-up; /* CS는 풀업 권장 */
};
&spi1_sleep_pins {
pins = "PD10", "PD11", "PD12", "PD13", "PD14", "PD15";
function = "gpio_in"; /* 절전 시 GPIO 입력으로 설정 */
muxsel = <0>;
drive-strength = <10>;
};
Menuconfig 설정
리눅스 커널 소스 디렉토리에서 make ARCH=arm64 menuconfig (32비트 시스템은 make ARCH=arm menuconfig) 명령을 실행하여 설정 주 인터페이스로 진입합니다 (Linux-5.4 커널은 ./build.sh menuconfig 실행). 다음 단계를 따릅니다.
Device Drivers옵션을 선택하여 다음 설정 메뉴로 진입합니다.SPI support옵션을 선택하여 다음 설정 메뉴로 진입합니다.SUNXI SPI Controller옵션을 선택하여 커널에 직접 컴파일하거나 모듈로 컴파일하도록 설정합니다.- SPI 디버그 메시지를 활성화하려면
Debug support for SPI drivers옵션을 선택합니다.
드라이버 아키텍처
리눅스 SPI 시스템은 세 가지 주요 계층으로 구성됩니다.
사용자 공간
이 계층은 SPI 장치를 활용하는 모든 애플리케이션을 포함합니다. 사용자는 실제 요구 사항에 따라 SPI 장치에 특수한 처리를 적용할 수 있습니다. 예를 들어, MTD(Memory Technology Device) 계층과 상호 작용하여 SPI 플래시를 파일 시스템으로 구현하거나, TTY 서브시스템과 연동하여 SPI 장치를 TTY 장치로 만들거나, 네트워크 서브시스템과 연결하여 SPI 장치를 네트워크 장치로 활용할 수 있습니다. 특정 SPI 장치에는 해당 프로토콜 요구 사항에 따라 사용자 정의 프로토콜 드라이버를 구현할 수도 있습니다. 이 계층에서는 장치의 구체적인 기능이 사용자 공간 프로그램에 의해 처리되며, 컨트롤러 드라이버는 장치 자체의 기능에 대해 직접 관여하지 않습니다.
커널 공간
커널 공간은 세 가지 부분으로 나눌 수 있습니다.
SPI 장치 드라이버 계층
이 계층은 SPI 컨트롤러에 연결된 장치의 가변성을 고려하여, 커널에 자체 프로토콜 드라이버가 없는 경우를 위해 일반적인 SPI 장치 드라이버(예: spidev.c)를 제공합니다. 이 일반 드라이버는 사용자 공간에 SPI 컨트롤러를 제어하는 인터페이스를 제공하며, 구체적인 프로토콜 제어 및 데이터 전송 작업은 사용자 공간에서 해당 장치에 따라 처리됩니다. 이 방식은 주로 데이터 양이 적은 간단한 SPI 장치와의 동기식 통신에 사용됩니다. 또한, spi-nand.c와 같이 특정 SPI NAND 장치를 지원하는 드라이버도 이 계층에 속합니다. 이 계층의 드라이버는 read, write, ioctl 등의 사용자 공간 인터페이스를 구현하여 특정 SPI 장치의 기능을 수행합니다. SPI 버스 드라이버는 특정 SPI 컨트롤러에 맞는 버스 읽기/쓰기 메서드를 구현하고 리눅스 커널의 SPI 아키텍처에 등록됩니다. 이로써 SPI 주변 장치는 SPI 아키텍처를 통해 장치와 버스를 연결할 수 있습니다. 버스 드라이버 자체는 통신을 직접 수행하지 않으며, 단지 통신 구현을 제공하고 장치 드라이버의 호출을 기다립니다.
SPI 공통 인터페이스 캡슐화 계층
SPI 드라이버의 프로그래밍 작업을 간소화하고, 프로토콜 드라이버와 컨트롤러 드라이버 간의 결합도를 낮추기 위해 커널은 컨트롤러 및 프로토콜 드라이버의 일반적인 작업을 표준 인터페이스로 캡슐화했습니다. 이 계층은 일반적인 논리 처리 작업을 포함하여 SPI 공통 인터페이스 캡슐화 계층을 구성합니다. 이 방식의 장점은 컨트롤러 드라이버가 표준 인터페이스 콜백 API를 구현하고 이를 공통 인터페이스 계층에 등록하기만 하면 되며, 프로토콜 계층 드라이버와 직접 상호 작용할 필요가 없다는 것입니다. 프로토콜 계층 드라이버는 공통 인터페이스 계층이 제공하는 API를 통해 장치 및 드라이버 등록을 완료하고 데이터 전송을 수행할 수 있으며, SPI 컨트롤러 드라이버의 구현 세부 사항에 신경 쓸 필요가 없습니다. 이 계층은 커널의 spi.c 파일에 해당합니다.
SPI 컨트롤러 드라이버 계층
이 계층은 하드웨어 종속적인 SPI 컨트롤러 드라이버입니다. SPI 컨트롤러의 하드웨어 레지스터를 직접 제어하여 실제 SPI 통신을 담당합니다. 이 계층의 드라이버는 컨트롤러의 초기화, 클럭 설정, 모드 설정(마스터/슬레이브), 데이터 전송(FIFO 또는 DMA 활용), 인터럽트 처리 등을 구현합니다. 예를 들어, spi-sunxi.c 파일이 이 계층에 해당합니다. 이 계층은 SPI 공통 인터페이스 캡슐화 계층의 표준 API를 구현하고 등록하여 상위 계층과 통신합니다. 이 부분은 개발자가 주로 다루는 핵심 영역입니다.
하드웨어
이 계층은 SPI 컨트롤러와 컨트롤러에 연결된 다양한 SPI 서브 장치를 포함하는 실제 물리적 장치를 나타냅니다. SPI 버스를 통해 CPU와 데이터를 주고받습니다.
SPI 커널 인터페이스
SPI 장치 드라이버 등록 및 데이터 전송을 위한 주요 인터페이스는 include/linux/spi/spi.h 파일에 정의되어 있습니다. SPI 장치 드라이버의 빠른 등록을 위해 module_spi_driver() 매크로가 제공됩니다:
#define module_spi_driver(__spi_drv) \
module_driver(__spi_drv, spi_register_driver, spi_unregister_driver)
spi_register_driver()
- 함수 원형:
int spi_register_driver(struct spi_driver *sdrv) - 기능 설명: SPI 장치 드라이버를 커널에 등록합니다.
- 매개변수:
sdrv는 SPI 드라이버 구조체 포인터로, SPI 장치의 이름,probe콜백 등의 정보를 포함합니다. - 반환 값: 성공 시 0, 실패 시 음수 값을 반환합니다.
spi_unregister_driver()
- 함수 원형:
void spi_unregister_driver(struct spi_driver *sdrv) - 기능 설명: 등록된 SPI 장치 드라이버를 커널에서 해제합니다.
- 매개변수:
sdrv는 SPI 드라이버 구조체 포인터입니다. - 반환 값: 없음.
데이터 전송 인터페이스
SPI 장치 드라이버는 struct spi_message를 사용하여 SPI 버스에 읽기/쓰기 I/O를 요청합니다. 하나의 spi_message는 일련의 작업 시퀀스를 포함하며, 각 작업은 spi_transfer라고 불립니다. 이를 통해 SPI 버스 드라이버는 일련의 원자적 시퀀스를 직렬로 실행할 수 있습니다. 커널 스레드는 큐를 사용하여 비동기 전송 기능을 구현합니다. 동일한 데이터 전송 요청자가 비동기 방식으로 데이터 전송 완료를 기다리지 않고 즉시 반환할 수 있으며, 이어서 다음 메시지를 요청할 수 있습니다. 다른 요청자도 동시에 메시지 전송 요청을 시작할 수 있습니다.
/* 단일 SPI 전송 작업을 정의하는 구조체 */
struct spi_transfer {
const void *tx_buffer_ptr; /* 송신 버퍼 포인터 */
void *rx_buffer_ptr; /* 수신 버퍼 포인터 */
unsigned int data_len; /* 전송할 데이터 길이 (바이트) */
dma_addr_t tx_dma_addr; /* 송신 DMA 주소 */
dma_addr_t rx_dma_addr; /* 수신 DMA 주소 */
unsigned int cs_toggle:1; /* 이 전송 후 CS 토글 여부 (1=토글, 0=유지) */
u8 data_bits_per_word; /* 워드당 비트 수 (예: 8) */
u16 inter_delay_us; /* 전송 간 지연 시간 (마이크로초) */
u32 clock_rate_hz; /* 이 전송에 사용될 클럭 주파수 (Hz) */
struct list_head transfer_node; /* spi_message 내 transfer 리스트 노드 */
};
/* SPI 장치와의 전체 메시지 교환을 정의하는 구조체 */
struct spi_message {
struct list_head transfer_list; /* spi_transfer 리스트 */
struct spi_device *target_device; /* 대상 SPI 장치 */
unsigned int is_dma_mapped:1; /* DMA 매핑 여부 */
void (*completion_cb)(void *context_ptr); /* 전송 완료 콜백 함수 */
void *callback_context; /* 콜백 함수에 전달될 컨텍스트 */
unsigned int transferred_length; /* 실제 전송된 바이트 수 */
int transfer_status; /* 전송 결과 상태 */
struct list_head message_queue_node; /* 내부 큐 리스트 노드 */
void *internal_state; /* 내부 상태 포인터 */
};
spi_message_init()
- 함수 원형:
void spi_message_init(struct spi_message *msg) - 기능 설명: SPI 메시지 구조체를 초기화합니다. 주로 모든 필드를 0으로 설정하고 전송 큐를 초기화합니다.
- 매개변수:
msg는 초기화할spi_message구조체에 대한 포인터입니다. - 반환 값: 없음.
spi_message_add_tail()
- 함수 원형:
void spi_message_add_tail(struct spi_transfer *xfer, struct spi_message *msg) - 기능 설명: 지정된
spi_transfer를spi_message의 전송 리스트 끝에 추가합니다. - 매개변수:
xfer:spi_message에 추가할spi_transfer구조체 포인터.msg:spi_message구조체 포인터.
- 반환 값: 없음.
spi_sync()
- 함수 원형:
int spi_sync(struct spi_device *dev, struct spi_message *msg) - 기능 설명: 지정된
spi_message의 처리를 시작하고, SPI 버스가 메시지 처리를 완료할 때까지 기다립니다 (동기식). - 매개변수:
dev: 현재 SPI 장치에 대한 포인터.msg: 처리할spi_transfer큐를 포함하는spi_message구조체 포인터.
- 반환 값: 성공 시 0, 실패 시 음수 값을 반환합니다.
모듈 사용 예시
커널 내장 드라이버 예시 (spidev)
drivers/spi/spidev.c 파일에 있는 spidev 드라이버는 리눅스 커널에 기본으로 포함된 범용 SPI 드라이버입니다. 이 드라이버는 spi_register_driver()를 호출하여 SPI 드라이버를 등록하고, 사용자가 SPI 메시지 데이터를 읽고 쓸 수 있도록 편의를 제공합니다.
static int __init spidev_mod_init(void)
{
int ret_status;
/* 예약된 장치 번호 256개를 요청하고, udev/mdev가 /dev 노드를 추가/삭제하도록
* 지시하는 클래스를 등록한 다음, 이 장치 번호를 관리하는 드라이버를 등록합니다.
*/
BUILD_BUG_ON(SPIDEV_MINORS > 256);
ret_status = register_chrdev(SPIDEV_MAJOR, "spidev_app", &spidev_file_ops);
if (ret_status < 0)
return ret_status;
spidev_global_class = class_create(THIS_MODULE, "spi_dev_class");
if (IS_ERR(spidev_global_class)) {
unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
return PTR_ERR(spidev_global_class);
}
ret_status = spi_register_driver(&spidev_spi_driver);
if (ret_status < 0) {
class_destroy(spidev_global_class);
unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
}
return ret_status;
}
module_init(spidev_mod_init);
static void __exit spidev_mod_exit(void)
{
spi_unregister_driver(&spidev_spi_driver);
class_destroy(spidev_global_class);
unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
}
module_exit(spidev_mod_exit);
또한, 해당하는 SPI 컨트롤러의 DTS(Device Tree Source) 파일에 SPI 자식 장치의 정보를 추가해야 합니다. 다음은 SPI 컨트롤러에 연결된 가상의 SPI 플래시 장치에 대한 예시 구성입니다:
&spi1_ctrl {
status = "okay";
spi_slave_mode = <0>; /* 마스터 모드 */
spi_flash_device@0 {
device_type = "spi_flash_nor";
compatible = "vendor_a,my_flash"; /* 드라이버 호환성 문자열 */
spi-max-frequency = <25000000>; /* SPI 장치의 최대 주파수 */
reg = <0x0>; /* 칩 셀렉트 번호 (CS0) */
spi-rx-bus-width = <0x1>; /* 데이터 읽기 시 사용될 데이터 라인 수 */
spi-tx-bus-width = <0x1>; /* 데이터 쓰기 시 사용될 데이터 라인 수 */
status = "okay";
};
};
device_type: 장치의 유형을 나타냅니다.compatible: 드라이버 매칭 정보를 제공합니다.spi-max-frequency: 슬레이브 장치의 최대 동작 주파수입니다.reg: 슬레이브 장치의 칩 셀렉트(CS) 주소(보통 0부터 시작).spi-rx-bus-width: 슬레이브 장치에서 데이터를 읽을 때 사용되는 데이터 라인 수.spi-tx-bus-width: 슬레이브 장치에 데이터를 쓸 때 사용되는 데이터 라인 수.status: 슬레이브 장치의 활성화 상태.
menuconfig (Device Drivers -> SPI support)에서 User mode SPI device driver support 옵션을 활성화해야 합니다.
펌웨어를 컴파일하고 플래싱한 후, 임베디드 장치의 /dev 디렉토리에서 /dev/spidevX.Y (X는 버스 번호, Y는 CS 번호)와 같은 장치 노드를 찾을 수 있으며, 이를 통해 읽기/쓰기 작업을 수행할 수 있습니다. 또는 리눅스에 포함된 spi_test 유틸리티를 사용할 수 있습니다. tina/lichee/linux-5.4/tools 디렉토리에서 다음 명령을 실행하여 빌드합니다:
make spi
빌드된 spidev_test 실행 파일을 임베디드 장치 파일 시스템의 루트 디렉토리에 복사한 후, 다음 명령으로 테스트를 수행합니다:
/spidev_test -D /dev/spidevX.Y
슬레이브 모드 드라이버 예시
슬레이브 모드를 사용하려면 board.dts의 해당 SPI 노드에 spi_slave_mode = <1>을 설정해야 합니다.
슬레이브 쓰기 데이터
/dev/spidev1.0 장치를 예로 들어, 0부터 9까지의 10개 데이터를 슬레이브로 보내는 예시 코드입니다:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#define TARGET_DEV_PATH "/dev/spidev1.0"
#define CMD_HEADER_SIZE 5
#define MAX_PKT_BUF_SIZE 0x40 // 64 바이트
#define SPI_OP_WRITE_CMD 0x01
#define SPI_OP_READ_CMD 0x03
#define TX_SHORT_DELAY_US 200
#define RX_LONG_DELAY_US 100000
/* 데이터를 덤프하는 유틸리티 함수 */
void hex_dump_buffer(unsigned char *buffer, unsigned int length) {
unsigned int i, line_char_count = 0;
char temp_line[length * 3 + 20]; // 충분한 버퍼 공간 확보
for (i = 0; i < length; i++) {
if (i % 0x10 == 0) {
line_char_count += sprintf(temp_line + line_char_count, "0x%08x: ", i);
}
line_char_count += sprintf(temp_line + line_char_count, "%02x ", buffer[i]);
if ((i % 0x10 == 0x0f) || (i == (length - 1))) {
printf("%s\n", temp_line);
line_char_count = 0;
}
}
}
int main(int argc, const char *argv[]) {
unsigned int data_to_send_len;
char tx_header[CMD_HEADER_SIZE] = {SPI_OP_WRITE_CMD, 0x00, 0x00, 0x00, 0x00};
char rx_header[CMD_HEADER_SIZE] = {SPI_OP_READ_CMD, 0x00, 0x00, 0x00, 0x00};
char tx_data_buf[MAX_PKT_BUF_SIZE];
int file_descriptor, result_code;
data_to_send_len = 10; // 10개의 숫자 전송
if (data_to_send_len > MAX_PKT_BUF_SIZE) {
printf("오류: 전송할 데이터는 64바이트 미만이어야 합니다.\n");
return -1;
}
tx_header[4] = (char)data_to_send_len;
rx_header[4] = (char)data_to_send_len;
for (unsigned char i = 0; i < data_to_send_len; i++) {
tx_data_buf[i] = i;
}
printf("송신 데이터 버퍼:\n");
hex_dump_buffer((unsigned char*)tx_data_buf, data_to_send_len);
file_descriptor = open(TARGET_DEV_PATH, O_RDWR);
if (file_descriptor <= 0) {
printf("오류: %s 장치를 열 수 없습니다.\n", TARGET_DEV_PATH);
return -1;
}
// 쓰기 작업
if (write(file_descriptor, tx_header, CMD_HEADER_SIZE) != CMD_HEADER_SIZE) {
printf("오류: 헤더 쓰기 실패.\n");
result_code = -1;
goto cleanup;
} else {
printf("정보: 헤더 쓰기 성공.\n");
}
usleep(TX_SHORT_DELAY_US);
if (write(file_descriptor, tx_data_buf, data_to_send_len) != data_to_send_len) {
printf("오류: 데이터 쓰기 실패.\n");
result_code = -1;
goto cleanup;
} else {
printf("정보: 데이터 쓰기 성공.\n");
}
usleep(RX_LONG_DELAY_US); // 충분한 지연 시간을 주어 슬레이브가 데이터를 처리하도록 함
result_code = 0;
cleanup:
if (file_descriptor > 0) {
close(file_descriptor);
}
return result_code;
}
슬레이브 읽기 데이터
/dev/spidev1.0 장치를 예로 들어, 슬레이브에서 10개 데이터를 읽어오는 예시 코드입니다:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#define TARGET_DEV_PATH "/dev/spidev1.0"
#define CMD_HEADER_SIZE 5
#define MAX_PKT_BUF_SIZE 0x40 // 64 바이트
#define SPI_OP_WRITE_CMD 0x01
#define SPI_OP_READ_CMD 0x03
#define TX_SHORT_DELAY_US 200
#define RX_LONG_DELAY_US 100000
/* 데이터를 덤프하는 유틸리티 함수 */
void hex_dump_buffer(unsigned char *buffer, unsigned int length) {
unsigned int i, line_char_count = 0;
char temp_line[length * 3 + 20]; // 충분한 버퍼 공간 확보
for (i = 0; i < length; i++) {
if (i % 0x10 == 0) {
line_char_count += sprintf(temp_line + line_char_count, "0x%08x: ", i);
}
line_char_count += sprintf(temp_line + line_char_count, "%02x ", buffer[i]);
if ((i % 0x10 == 0x0f) || (i == (length - 1))) {
printf("%s\n", temp_line);
line_char_count = 0;
}
}
}
int main(int argc, const char *argv[]) {
unsigned int data_to_read_len;
char tx_header[CMD_HEADER_SIZE] = {SPI_OP_WRITE_CMD, 0x00, 0x00, 0x00, 0x00};
char rx_header[CMD_HEADER_SIZE] = {SPI_OP_READ_CMD, 0x00, 0x00, 0x00, 0x00};
char rx_data_buf[MAX_PKT_BUF_SIZE];
int file_descriptor, result_code;
data_to_read_len = 10;
if (data_to_read_len > MAX_PKT_BUF_SIZE) {
printf("오류: 읽을 데이터는 64바이트 미만이어야 합니다.\n");
return -1;
}
tx_header[4] = (char)data_to_read_len;
rx_header[4] = (char)data_to_read_len;
file_descriptor = open(TARGET_DEV_PATH, O_RDWR);
if (file_descriptor <= 0) {
printf("오류: %s 장치를 열 수 없습니다.\n", TARGET_DEV_PATH);
return -1;
}
// 읽기 작업
if (write(file_descriptor, rx_header, CMD_HEADER_SIZE) != CMD_HEADER_SIZE) {
printf("오류: 읽기 헤더 쓰기 실패.\n");
result_code = -1;
goto cleanup;
} else {
printf("정보: 읽기 헤더 쓰기 성공.\n");
}
usleep(RX_LONG_DELAY_US); // 슬레이브가 데이터를 준비할 시간 제공
if (read(file_descriptor, rx_data_buf, data_to_read_len) != data_to_read_len) {
printf("오류: 데이터 읽기 실패.\n");
result_code = -1;
goto cleanup;
} else {
printf("정보: 데이터 읽기 성공.\n");
}
usleep(RX_LONG_DELAY_US);
printf("수신 데이터 버퍼:\n");
hex_dump_buffer((unsigned char*)rx_data_buf, data_to_read_len);
result_code = 0;
cleanup:
if (file_descriptor > 0) {
close(file_descriptor);
}
return result_code;
}
슬레이브 모드 사용 및 테스트
환경 설정
하드웨어 환경
두 개의 개발 보드를 사용하여 마스터와 슬레이브 환경을 구축합니다. 마스터 보드와 슬레이브 보드의 SPI1 인터페이스에서 CS, CLK 핀을 각 이름에 맞춰 연결합니다. 마스터의 MOSI는 슬레이브의 MOSI에, 마스터의 MISO는 슬레이브의 MISO에 연결하고, 두 보드 간에 공통 접지를 연결합니다.
Menuconfig 설정
menuconfig에서 CONFIG_SPI_SUNXI와 CONFIG_SPI_SPIDEV 옵션을 활성화합니다.
DTS 설정
디바이스 트리 경로는 device/config/chips/xxx(t507)/configs/xxx(demo2.0)/board.dts입니다. 다음 노드를 추가합니다:
&spi1_ctrl {
pinctrl-0 = <&spi1_active_pins &spi1_cs0_pin>;
pinctrl-1 = <&spi1_sleep_pins>;
spi_slave_mode = <0>; /* 마스터용: 0, 슬레이브용: 1 */
status = "okay";
spi_virt_slave@0 {
device_type = "virtual_spi_slave";
compatible = "acme,spi-slave-device"; /* 가상 슬레이브 드라이버 호환성 */
spi-max-frequency = <30000000>;
reg = <0x0>;
spi-rx-bus-width = <0x1>;
spi-tx-bus-width = <0x1>;
status = "okay";
};
};
테스트 실행
마스터와 슬레이브 각각의 DTS를 설정하고, 해당하는 펌웨어를 컴파일하여 플래싱합니다. 슬레이브 장치에서 디버그 출력을 활성화하여 읽기/쓰기 데이터를 확인할 수 있습니다.
테스트 결과
마스터에서 전송한 원본 데이터와 수신한 대상 데이터가 일치하면 테스트가 성공한 것입니다. 다음은 성공적인 테스트 출력의 예시입니다:
--------------------------------------------
n test
--------------------------------------------
정보: 헤더 쓰기 성공.
정보: 데이터 쓰기 성공.
원본 데이터:
0x00000000: 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a
0x00000010: 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a
정보: 읽기 헤더 쓰기 성공.
정보: 데이터 읽기 성공.
대상 데이터:
0x00000000: 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a
0x00000010: 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a
슬레이브 기능 [PASS]
사용자 정의 가이드
사용자는 슬레이브 장치 기능을 사용자 정의할 수 있습니다. 슬레이브 장치를 조작하려면 5바이트의 작업 요청을 보내야 합니다. 형식은 다음과 같습니다:
- 첫 번째 바이트: 작업 코드
SPI_OP_WRITE_CMD 0x01(마스터 기준으로 쓰기)SPI_OP_READ_CMD 0x03(마스터 기준으로 읽기)
- 두 번째~네 번째 바이트: 주소 (두 번째 바이트가 상위 주소)
- 다섯 번째 바이트: 길이 (길이는 64바이트 미만이어야 합니다)
작업 코드 추가
현재는 읽기/쓰기 작업만 지원됩니다. 사용자는 drivers/spi/spi-sunxi.c의 sunxi_spi_slave_handle_header 함수에 해당 명령의 작업 함수를 추가하여 기능을 확장할 수 있습니다.
if (msg_header->opcode == SPI_OP_WRITE_CMD) {
sunxi_spi_slave_configure_rx(spi_ctrl);
} else if (msg_header->opcode == SPI_OP_READ_CMD) {
sunxi_spi_slave_configure_tx(spi_ctrl);
} else {
printk(KERN_INFO "[spi%d] 패킷 헤더 작업 코드 오류\n", spi_ctrl->master->bus_num);
return -EINVAL;
}
주소 및 버퍼
두 번째에서 네 번째 바이트의 주소는 읽기/쓰기 버퍼 데이터를 지정하는 데 사용됩니다. 버퍼 크기 매크로는 drivers/spi/spi-slave-protocol.h에 정의되어 있으며, 사용자가 직접 설정할 수 있습니다 (단위: 바이트).
#define SLAVE_STORAGE_BUFFER_SIZE 128
길이 제한
매번 읽기/쓰기 데이터 길이는 64바이트 미만이어야 합니다. SPI RX/TX FIFO 버퍼 크기가 64바이트이기 때문에, 읽기/쓰기 시 한쪽 장치가 제때 데이터를 가져가지 못하여 버퍼 오버플로우가 발생하는 것을 방지하기 위함입니다. 64바이트보다 큰 데이터를 읽거나 써야 하는 경우, 주소 오프셋을 잘 설정하여 여러 번에 걸쳐 전송할 수 있습니다.
자주 묻는 질문 (FAQ)
디버그 노드
/sys/module/spi_sunxi/parameters/debug
기본적으로 debug 값은 1로 설정되어 디버그 정보가 출력되지 않습니다. 다음 명령으로 디버그 메시지를 활성화할 수 있습니다:
echo 255 > /sys/module/spi_sunxi/parameters/debug
/sys/devices/platform/soc/spi1/info
이 노드 파일을 통해 현재 SPI1 채널의 하드웨어 리소스 정보를 확인할 수 있습니다:
cat /sys/devices/platform/soc/spi1/info
/sys/devices/platform/soc/spi1/status
이 노드 파일을 통해 현재 SPI1 채널의 동작 상태 정보(컨트롤러의 각 레지스터 값 포함)를 확인할 수 있습니다:
cat /sys/devices/platform/soc/spi1/status
DTS 활성화 설정이 적용되지 않는 문제
문제 현상: board.dts에서 SPI의 status를 "okay"로 설정했지만, 리눅스 커널 부팅 후 SPI 컨트롤러가 활성화되지 않는 현상.
문제 분석: 상태 설정이 잘못되었거나, SPI0과 같은 다른 컨트롤러를 잘못 사용했을 수 있습니다.
문제 해결 단계:
- 이러한 문제는 일반적으로 디바이스 트리에서 장치가 다른 장치에 의존하지만, 해당 의존 장치가 성공적으로 프로브되지 않아 현재 장치가 프로브되지 못하는 경우 발생합니다. SPI가 의존하는 DMA 모듈을 검사하고,
menuconfig에서 DMA가 활성화되어 있는지 확인하는 것이 좋습니다. out/디렉토리에서.sunxi.dts파일을 검색하여 엽니다:
파일에서 해당 노드를 찾아 SPI가 성공적으로 구성되었는지 확인합니다.find -name ".sunxi.dts"- 임베디드 장치의 U-Boot 콘솔에서
fdt list spi*명령을 통해 DTS를 확인하여 SPI가 활성화되었는지 (status = "okay") 확인합니다. 여전히 "disabled"라면, SPI가 U-Boot 단계에서 비활성화되었을 수 있습니다 (일반적으로 SPI0은 플래시용으로 예약되어 U-Boot 단계에서 비활성화될 수 있습니다).
SPI-Flash 데이터 전송 이상
문제 현상: SPI 플래시에 데이터를 쓰고 다시 읽었을 때 데이터가 일치하지 않는 현상.
문제 해결 단계:
- 호환성 확인: NOR 플래시를 예로 들면, 일부 부품은 호환성 문제가 있어 읽기/쓰기 오류를 일으킬 수 있습니다. 이때 먼저 해당 부품이 지원 목록에 있는지 확인합니다. 목록에 없다면 다른 부품으로 교체하여 테스트해 봅니다.
- 드라이버 디버깅: 이러한 문제는 범위가 넓지만, 기본적인 디버깅 방법을 통해 추적할 수 있습니다. 일반적인 접근 방식은 데이터 로깅을 활성화하여 쓰기 값이 SPI 버스 드라이버로 올바르게 전달되는지 확인하는 것입니다. 마찬가지로, SPI 버스 드라이버가 읽어 들인 데이터와 이전에 기록된 데이터가 일치하는지 확인하여 어떤 단계에서 읽기/쓰기 오류가 발생했는지 판단합니다. 이 방법은 파일 시스템 계층, MTD 계층, SPI 버스 드라이버 계층 등 다른 계층으로 확장하여 읽기/쓰기 문제의 원인을 파악하는 데 활용할 수 있습니다.