[ BBB - yocto (13) ] SPI(1)

 0) SPI 인터페이스

SPI는 슬레이브와 마스터의 개념이 있다.


마스터와 슬레이브는 특정 역할을 갖고 있다.

마스터는 슬레이브에 특정 요청을 전송한다.

슬레이브는 마스터의 요청에 따라 동작을 수행하게 하거나, 슬레이브로 레지스터를 읽어 마스터로 전송하는등의 역할을 한다.


SPI는 4개의 핀을 사용한다.

MISO : 마스터 인풋, 슬레이브 아웃풋이다.

MOSI : 마스터 아웃풋, 슬레이브 인풋이다.

CS : SPI는 통신을 하려면 한번에 하나의 칩만 통신하는게 가능하다.

SCLK : SPI 클럭으로, I2C와 마찬가지로 클럭을 가지고 통신한다.


SCLK는 CPOL CHPA를 갖는다.


1. CPOL : 클럭 극성을 의미한다. (Clock POLarity)

예를 들어 CPOL이 1이면, 클럭이 IDLE 상태에서 HIGH가 된다.

CPOL이 0이면, 클럭이 IDLE 상태에서 LOW가 된다. 

IDLE 상태는 아무 전송도 이루어 지지 않고 있는 상황이다.


2. CPHA : 클럭 위상을 의미한다. (Clock PHAse)

예를 들어 CPHA 이 1일때,

첫번째로 변화한 엣지를 1번째 엣지라 하면, 클럭의 1, 3, 5, 7 ... 홀수번에서 엣지에서 비트가 변화하며, 2, 4, 6, 8 짝수 엣지에서 비트를 샘플링(읽기)한다. 

CPHA 이 1이면, 

첫번째로 변화한 엣지를 1번째 엣지라 하면, 클럭의 1, 3, 5, 7 ... 홀수번에서 엣지에서 비트를 샘플링(읽기)하며, 2, 4, 6, 8 짝수 엣지에서 비트를 비트가 변화한다. 



MODECPOLCPHA설명
MODE100상승에지에서 비트를 읽어낸다. 하강에지에서 비트가 변화한다.
MODE201상승에지에서 비트가 변화한다. 하강에지에서 비트를 읽어낸다.
MODE310상승에지에서 비트가 변화한다. 하강에지에서 비트를 읽어낸다.
MODE411상승에지에서 비트를 읽어낸다. 하강에지에서 비트가 변화한다.

여기서 읽기/쓰기는 MISO에서 읽고, MOSI에서 쓴다는 개념이 아니라, MISO, MOSI 둘다에 적용되는 개념이다. 아래 그림을 보자


1) MODE0, MODE3은 하강에지에서 MOSI/MISO 비트가 변화한다(쓰기), 상승에지에서 비트를 읽는다.

2) MODE1, MODE2은 상승에지에서 MOSI/MISO 비트가 변화한다(쓰기), 하강에지에서 비트를 읽는다.





1) 디바이스 트리 설정하기


리눅스 사용자 어플리케이션에서는 SPI 최대 클럭을 갖고 있으며, 이를 바탕으로 클럭 속도를 설정 할 수 있다. SPI는 비트, 모드, MSB 우선인지, LSB 우선인지를 설정해 주어야 한다.


위 그림과 같은 경우는 MSB 우선 방식이며, LSB가 만약 먼저 나오면, LSB 우선 방식으로 보면 된다.


이는 칩마다 다르므로 칩 데이터 시트를 보고 설정하여 주자.


그렇다면 SPI의 최대 클럭은 디바이스 트리 쪽에서 설정해주어야 한다.


또한 SPI는 특이하게, SPI의 내부에 서브 노드를 두어 chip select 핀 마다 구분을 둔다. i2c는 주소를 서브노드로 두는것에 대비된다.


디바이스 트리 SPI 문서를 봐보자

tmp/work-shared/beaglebone/kernel-source/Documentation/devicetree/bindings/spi/spi-controller.yaml

# SPDX-License-Identifier: GPL-2.0
%YAML 1.2
---
$id: http://devicetree.org/schemas/spi/spi-controller.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#

title: SPI Controller Generic Binding

maintainers:
  - Mark Brown <broonie@kernel.org>

description: |
  SPI 버스는 SPI 컨트롤러 장치에 대한 노드와 버스의 각 SPI 슬레이브에 대한 자식 노드 세트로 설명될 수 있습니다.
  시스템 SPI 컨트롤러는 SPI 마스터 모드 또는 SPI 슬레이브 모드로 사용될 수 있지만, 동시에 두 가지 모드로 동작할 수는 없습니다.

properties:
  $nodename:
    pattern: "^spi(@.*|-[0-9a-f])*$"

  "#address-cells":
    enum: [0, 1]

  "#size-cells":
    const: 0

  cs-gpios:
    description: |
      칩 셀렉트(CS)로 사용되는 GPIO 핀 목록입니다.
      이 속성이 사용되면, 칩 셀렉트의 개수는 자동으로 증가하며, 증가된 개수는 cs-gpios 개수와 하드웨어 칩 셀렉트 개수 중 최댓값으로 결정됩니다.

      예를 들어, 컨트롤러가 4개의 CS 라인을 가지고 있고, cs-gpios 속성이 다음과 같이 설정된 경우:
        cs-gpios = <&gpio1 0 0>, <0>, <&gpio1 1 0>, <&gpio1 2 0>;

      num_chipselect 값은 4로 설정되며, CS 매핑은 다음과 같습니다:
        cs0 : &gpio1 0 0
        cs1 : 기본 (native) 칩 셀렉트
        cs2 : &gpio1 1 0
        cs3 : &gpio1 2 0

      GPIO 디스크립터의 두 번째 플래그는 GPIO_ACTIVE_HIGH(0) 또는 GPIO_ACTIVE_LOW(1)일 수 있습니다.
      기존 장치 트리에서는 일반적으로 0을 사용합니다.

      cs-gpio의 두 번째 플래그와 SPI 슬레이브의 선택적 spi-cs-high 플래그를 조합하는 데 특별한 규칙이 적용됩니다.

      각 테이블 항목은 CS 핀이 실제로 어떻게 구동되는지를 정의합니다
      (핀 멀티플렉서에 의해 추가적인 반전이 있을 수 있음):

      장치 노드     | cs-gpio       | CS 핀 활성 상태 | 비고
      =============+===============+=================+=====
      spi-cs-high  | -             | H               |
      -            | -             | L               |
      spi-cs-high  | ACTIVE_HIGH   | H               |
      -            | ACTIVE_HIGH   | L               | 1
      spi-cs-high  | ACTIVE_LOW    | H               | 2
      -            | ACTIVE_LOW    | L               |

      비고:
      1) 극성이 반전되었음을 경고하는 메시지를 출력해야 합니다.
         가능한 경우 GPIO를 ACTIVE_LOW로 정의하는 것이 바람직합니다.
      2) ACTIVE_LOW가 spi-cs-high에 의해 덮어씌워지므로 극성이 반전되었음을 경고하는 메시지를 출력해야 합니다.
         일반적으로 피하는 것이 좋으며, 대신 spi-cs-high + ACTIVE_HIGH를 사용하는 것이 바람직합니다.

  num-cs:
    $ref: /schemas/types.yaml#/definitions/uint32
    description:
      전체 칩 셀렉트 개수입니다.

  spi-slave:
    $ref: /schemas/types.yaml#/definitions/flag
    description:
      SPI 컨트롤러가 마스터가 아닌 슬레이브로 동작함을 나타냅니다.

  slave:
    type: object

    properties:
      compatible:
        description:
          SPI 장치의 호환성을 정의합니다.

    required:
      - compatible

patternProperties:
  "^.*@[0-9a-f]+$":
    type: object
    $ref: spi-peripheral-props.yaml

    properties:
      spi-3wire:
        $ref: /schemas/types.yaml#/definitions/flag
        description:
          장치가 3선(SPI 3-wire) 모드를 요구하는지 여부입니다.

      spi-cpha:
        $ref: /schemas/types.yaml#/definitions/flag
        description:
          장치가 변경된 클럭 위상(CPHA) 모드를 요구하는지 여부입니다.

      spi-cpol:
        $ref: /schemas/types.yaml#/definitions/flag
        description:
          장치가 반전된 클럭 극성(CPOL) 모드를 요구하는지 여부입니다.

    required:
      - compatible
      - reg

allOf:
  - if:
      not:
        required:
          - spi-slave
    then:
      properties:
        "#address-cells":
          const: 1
    else:
      properties:
        "#address-cells":
          const: 0

additionalProperties: true

examples:
  - |
    spi@80010000 {
        #address-cells = <1>;
        #size-cells = <0>;
        compatible = "fsl,imx28-spi";
        reg = <0x80010000 0x2000>;
        interrupts = <96>;
        dmas = <&dma_apbh 0>;
        dma-names = "rx-tx";

        display@0 {
            compatible = "lg,lg4573";
            spi-max-frequency = <1000000>;
            reg = <0>;
        };

        sensor@1 {
            compatible = "bosch,bme680";
            spi-max-frequency = <100000>;
            reg = <1>;
        };

        flash@2 {
            compatible = "jedec,spi-nor";
            spi-max-frequency = <50000000>;
            reg = <2>, <3>;
            stacked-memories = /bits/ 64 <0x10000000 0x10000000>;
        };
    }

몇가지 노드 속성에 대해서 짚고 넘어가면, 

spi-slave : spi를 슬레이브로 사용. spi의 노드에 정의하여 놓으면 된다.


여기서 부터는 서브 노드 속성이다.

spi-3wire : 3와이어를 설정한 경우 MISO, MOSI가 하나의 핀이 되며, half-duplex로 밖에 쓸 수 없다.

spi-max-frequency : spi 최대 클럭 주파수를 설정한다.

spi-chpa : 클럭 극성을 반전 시킨다 기존에는 0(클럭 idle low)로 되어 있다.

spi-cpol : 클럭 위상을 반전 시킨다. 2 엣지가 읽기, 1 엣지는 쓰기 동작이 된다.

spi-cs-high : spi 칩 셀렉트를 high로 설정한다.

tmp/work-shared/beaglebone/kernel-source/Documentation/devicetree/bindings/spi/omap-spi.yaml

# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
%YAML 1.2
---
$id: http://devicetree.org/schemas/spi/omap-spi.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#

title: SPI controller bindings for OMAP and K3 SoCs

maintainers:
  - Aswath Govindraju <a-govindraju@ti.com>

allOf:
  - $ref: spi-controller.yaml#

properties:
  compatible:
    oneOf:
      - items:
          - enum:
              - ti,am654-mcspi
              - ti,am4372-mcspi
          - const: ti,omap4-mcspi
      - items:
          - enum:
              - ti,omap2-mcspi
              - ti,omap4-mcspi

  reg:
    maxItems: 1

  interrupts:
    maxItems: 1

  clocks:
    maxItems: 1

  power-domains:
    maxItems: 1

  ti,spi-num-cs:
    $ref: /schemas/types.yaml#/definitions/uint32
    description: 인스턴스에서 지원하는 칩 셀렉트 개수.
    minimum: 1
    maximum: 4

  ti,hwmods:
    $ref: /schemas/types.yaml#/definitions/string
    description:
      반드시 "mcspi<n>" 형식이어야 하며, 여기서 n은 1부터 시작하는 인스턴스 번호입니다.
      이 속성은 주로 OMAP2/3 및 TI81xx와 같은 레거시 플랫폼에서만 적용되며,
      다른 플랫폼에서는 사용되지 않아야 합니다.
    deprecated: true

  ti,pindir-d0-out-d1-in:
    description:
      D0 핀을 출력, D1 핀을 입력으로 선택합니다. 기본값은 D0가 입력,
      D1이 출력입니다.
    type: boolean

  dmas:
    description:
      제어기별 형식이 지정된 DMA 식별자의 목록입니다.
      이는 일반적인 DMA 클라이언트 바인딩에서 설명된 형식을 따릅니다.
      각 칩 셀렉트에 대해 TX 및 RX 식별자가 필요합니다.
    minItems: 1
    maxItems: 8

  dma-names:
    description:
      DMA 요청 이름의 목록입니다. 이 문자열은 dmas에 나열된 DMA 식별자와 1:1로 매칭됩니다.
      RX 및 TX 요청을 위해 각각 "rxN" 및 "txN" 형식의 이름을 사용해야 합니다.
      여기서 N은 칩 셀렉트 번호를 의미합니다.
    minItems: 1
    maxItems: 8

required:
  - compatible
  - reg
  - interrupts

unevaluatedProperties: false

if:
  properties:
    compatible:
      enum:
        - ti,omap2-mcspi
        - ti,omap4-mcspi

then:
  properties:
    ti,hwmods:
      items:
        - pattern: "^mcspi([1-9])$"

else:
  properties:
    ti,hwmods: false

examples:
  - |
    #include <dt-bindings/interrupt-controller/irq.h>
    #include <dt-bindings/interrupt-controller/arm-gic.h>
    #include <dt-bindings/soc/ti,sci_pm_domain.h>

    spi@2100000 {
      compatible = "ti,am654-mcspi","ti,omap4-mcspi";
      reg = <0x2100000 0x400>;
      interrupts = <GIC_SPI 184 IRQ_TYPE_LEVEL_HIGH>;
      clocks = <&k3_clks 137 1>;
      power-domains = <&k3_pds 137 TI_SCI_PD_EXCLUSIVE>;
      #address-cells = <1>;
      #size-cells = <0>;
      dmas = <&main_udmap 0xc500>, <&main_udmap 0x4500>;
      dma-names = "tx0", "rx0";
    };

특이하게, 다른 칩에서는 MISO나 MOSI를 직접 설정하지만, ti에서는 디바이스 트리에서 유동적으로 설정할 수 있다.

ti,pindir-d0-out-d1-in; 이렇게 설정하면 드라이버에서 d0를 out으로 d1을 in으로 설정한다.

반대로 아무 설정도 없으면 d0는 in, d1은 out으로 설정된다.

이건 사용자 편의에 맞춰서 쓰면 될거같다.


자 이를 바탕으로 디바이스 트리의 spi 노드를 설정해보자.

/dts-v1/;
/plugin/;

#include <dt-bindings/pinctrl/am33xx.h>

&{/chosen} {
	overlays{
		BB-USER_DEVICE_TREE.kernel = __TIMESTAMP__;
	};
};


/{
	fragment@0 {
		target = <&am33xx_pinmux>;
		__overlay__{
			spi0_pins: spi0_pins {
				pinctrl-single,pins = <
				AM33XX_PADCONF(AM335X_PIN_SPI0_D1, PIN_INPUT_PULLUP, MUX_MODE0)
				AM33XX_PADCONF(AM335X_PIN_SPI0_D0, PIN_INPUT_PULLUP, MUX_MODE0)
				AM33XX_PADCONF(AM335X_PIN_SPI0_SCLK, PIN_INPUT_PULLUP, MUX_MODE0)
				>;
			};
		};
	};

	fragment@1{
		target = <&spi0>;
		__overlay__{
			pinctrl-names = "default";
			pinctrl-0 = <&spi0_pins>;
			status = "okay";
			ti,pindir-d0-out-d1-in;

			spiuser@0 {
            	            compatible = "rohm,dh2228fv";
            	            spi-max-frequency = <16000000>;
            	            reg = <0>;
        	        };

			spiuser@1 {
            	            compatible = "rohm,dh2228fv";
            	            spi-max-frequency = <16000000>;
            	            reg = <1>;
        	        };
		};
	};

	fragment@2{
		target = <&tscadc>;
		__overlay__{
			status = "okay";
		};
	};
};

SPI의 CS 칩을 드라이버로 제어할 수 있지만, CS0, CS1 2개 핀을 한번에 사용할 수 없어 다른 핀을 gpio로 사용해 CS를 제어할 것이다.


지금 총 3개의 핀을 설정했고, ti,pindir-d0-out-d1-in;으로 설정해 d0가 MOSI, d1이 MISO로 설정되어 있다.


디바이스 트리 설정한 핀을 확장 핀헤더에서 찾아보기 위해서 코어의 핀을 보면 SPI 부분에서

이렇게 되어 있고 이를 expansion header에서 찾아보면

P9_21 : SPI0_D0

P9_22 : SPI0_SCLK

P9_18 : SPI_D1

이렇게 매핑돼 있다.


지금 보면 spiuser들의 compatible을 rohm,dh2228fv로 설정했는데 이 compatible에 대하여 잠시 설명하면

커널 소스가 있는 부분에서 드라이버 소스를 살펴보자.

kernel-source/drivers/spi/spidev.c를 열면


static const struct of_device_id spidev_dt_ids[] = {
	{ .compatible = "rohm,dh2228fv", .data = &spidev_of_check },
	{ .compatible = "lineartechnology,ltc2488", .data = &spidev_of_check },
	{ .compatible = "semtech,sx1301", .data = &spidev_of_check },
	{ .compatible = "lwn,bk4", .data = &spidev_of_check },
	{ .compatible = "dh,dhcom-board", .data = &spidev_of_check },
	{ .compatible = "menlo,m53cpld", .data = &spidev_of_check },
	{ .compatible = "cisco,spi-petra", .data = &spidev_of_check },
	{ .compatible = "micron,spi-authenta", .data = &spidev_of_check },
	{},
};

구조체의 copatible에 rohm,dh2228fv로 설정된 것을 볼 수 있다. 

이게 마음에 안들면 spidev_dt_ids에 .compatible에 사용자가 원하는 compatible이름을 넣고 .data 함수 포인터에는 spidev_of_check 주소를 넣어주면 된다고 한다.





3) 부팅하여 확인하기







4) 사용자 어플리케이션에서 spidev*.* 사용하기

자 이제 사용자 어플리케이션에서 spidev*.*를 열어서 쓰는 방법을 소개하겠다.


1. 초기 설정

1.1 포함해야 하는 include 파일은 다음과 같다

1.1.1 #include <sys/ioctl.h>

1.1.2 #include <linux/spi/spi.h>

1.1.3 #include <linux/spi/spidev.h>


1.2 open 함수로 /dev/spidev*.*를 연다.

1.3 ioctl로 spi 설정을 한다. (클럭 주파수, 비트, MSB 우선 vs LSB 우선, SPI 모드)

ioctl 함수로 설정할 내용은 아래 표와 같다.


ioctl __request 인자설명가능한 값사용 방법
SPI_IOC_WR_BITS_PER_WORD워드 당 비트를 설정한다. 워드는 SPI 전송에 쓰일 비트를 의미한다.8bit = 8
16bit = 16
int bit = 8; //8비트 설정
ioctl(fd,SPI_IOC_WR_BITS_PER_WORD, &bit);
SPI_IOC_WR_LSB_FIRSTSPI 전송 비트가 LSB 우선인지, MSB 우선인지 결정1 = LSB 우선
0 = MSB 우선
int lsb = 0; //msb 설정
ioctl(fd, SPI_IOC_WR_LSB_FIRST, &msb);
SPI_IOC_WR_MAX_SPEED_HZSPI 클럭의 최대 속도를 설정0 ~ <디바이스 트리에서 설정한 최대 클럭>int speed = 100 * 1000; //100khz
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
SPI_IOC_WR_MODESPI 모드를 설정0 = MODE 0
1 = MODE 1
2 = MODE 3
3 = MODE 4
int mode = 0; //cpol = 0, cpha = 0
ioctl(fd, SPI_IOC_WR_MODE, &mode);

초기 값은 이렇게 설정한다.


SPI_IOC_RD_BITS_PER_WORD, SPI_IOC_RD_LSB_FIRST, SPI_IOC_WR_MAX_SPEED_HZ, SPI_IOC_WR_MODE를 ioctl __request 인자로 설정하는 경우 해당 값을 읽어올 수 있다.



miso/mosi로부터 비트를 읽거나 전송하는 방법에는 2가지가 있다.

1. write/read 함수 사용하기


동작 구분설명사용법
readmiso로 부터 값을 읽음read(fd, buffer, sizeof(buffer);
writemosi에 값을 씀write(fd, buffer, sizeof(buffer);

사용방법은 매우 간단하다. write 혹은 read 함수를 이용해서 MISO로 부터 읽거나 쓸 수 있다. 이렇게 하면 read/write 시 알아서 클럭을 설정하고 디바이스 트리에서 cs 핀을 멀티플랙싱 해준 경우 드라이버가 알아서 cs핀을 제어해 준다.


half duplex 방식이다.



2. ioctl 함수 사용하기

spi_ioc_transfer 라는 구조체를 사용한다.

구조체 인자에 대해서 알아보자.

동작 구분설명
tx_buf 전송할 버퍼가 담겨있는 버퍼의 주소
rx_buf전송 받을 버퍼가 담겨있는 버퍼의 주소
len전송, 수신의 길이이며, 이때 주의할 점은 전송, 수신 받을 버퍼의 길이가 같아야 함
그렇지 않으면 잘못된 메모리 주소를 참조
speed_hz일시적으로 덮어 씌울 클럭 속도
delay_usecs다음 전송에 걸릴 딜레이, us 단위
bits_per_word워드당 비트 수를 의미. 위와 동일하게 워드는 SPI 데이터 통신 비트 크기임
cs_changetrue 설정시, 다음 전송 하기 전에 cs를 비활성화
tx_nbitstx_buf 맴버의 비트 폭을 의미한다.
rx_nbitsrx_buf 맴버의 비트 폭을 의미한다.
word_delay_usecs SPI 워드당 얼마나 기다릴 것인지를 설정한다. spi controller가 이를 지원하지 않으면 무시된다.
pad패딩 필드, 의미 없음

spi_ioc_transfer이라는 구조체를 써서 ioctl로 전송하는 방법은 다음과 같다.

uint8_t recv[10] = {0, };
uint8_t send[10] = {0, };
struct spi_ioc_transfer spi_msg = {0, };

int max_buffer_size = sizeof(recv) > sizeof(send) ? sizeof(recv) : sizeof(send);
spi_msg.rx_buf = (uint32_t)recv; //32bit 프로세서의 포인터 주소 비트 크기가 32비트 임에 주의하자. spi_msg.tx_buf = (uint32_t)send; spi_msg.len = max_buffer_size; ioctl(spi[spi_dev].fd, SPI_IOC_MESSAGE(1), &spi_msg);

주의할 점이 있는데 이 방법은 fullduplex 방식이다. 따라서 2바이트 전송을 하고 2바이트를 슬레이브로 부터 얻어온다면,  recv의 2번 인덱스부터 배열에 저장된다.


예를 들어 


uint8_t send[10] = {0x05, 0x06, 0, }; 

uint8_t recv[10] = {0, };

이 경우 send의 0, 1번 인덱스가 MOSI로 전송함에 따라 슬레이브가 MISO로 {0x01, 0x02}의 2바이트를 보낸다.

이는 recv[2], recv[3]에 저장된다.

댓글

이 블로그의 인기 게시물

[ BBB - yocto (9) ] 장치트리(DEVICE TREE)

[ BBB - yocto (11) ] uart

[ BBB - yocto (5) ] makefile 작성법과 컴파일 자동화