2014년 4월 4일 금요일

Device Tree 기반의 SPI programming

본 고에서는 open source hardware로 유명한 beaglebone black(BBB)을 이용하여 최신 linux kernel(3.10.x)에서 device driver를 어떻게 작성하는지에 관하여 정리해 보고자 한다.
(앞서 게재한 내용이 너무 길어져 7절의 내용을 별도로 정리하였음^^)

목차는 다음과 같다.

1. BBB 개요
2. BBB용 kernel source download & build 절차 소개
3. Device Tree 소개
4. GPIO driver example - LED driver
5. i2c driver 구현 - Wii nunchuk i2c slave driver
6. UART adapter driver 구현
7. SPI driver 구현
8. USB driver 구현
9. SD/MMC driver 구현
References

7. SPI driver 구현
이번 절에서는 device tree에서 SPI controller(master)를 활성화시키기 위한 pinmux 설정 방법을 소개하고, ADC 칩을 대상으로 slave device driver를 작성하는 방법을 정리해 보고자 한다. 또한, 원할한 내용 전개를 위해, 아래 내용도 부분적으로 설명에 포함시킬 것이다.
1) Linux SPI subsystem
2) ADC 및 analog 센서 chip을 위해 최신 커널에 추가된 IIO subsystem


먼저, 아래 그림은 SPI의 H/W 아키텍쳐를 그림으로 표현한 것으로, SPI 관련 일반적은 내용은 별도로 정리하지 않았다. 보다 자세한 사항은 아래 site의 내용을 참조하기로 한다.

그림 7.1 SPI H/W 아키텍쳐


SPI는 다양한 칩(예: flash, eeprom, touchscreen, LCD, network, analog sensor 등)을 제어하는 용도로 사용하는데, 이번 글에서 테스트해 볼 SPI slave 장치는 Analog Devices Inc가 설계한 AD7887 ADC(Analog Digital Converter)이다. ADC는 한마디로 표현하면, 아날로그 입력 신호 전압을 디지털 입력 숫자로 바꾸는 장치를 말한다.  ADC가 활용되는 경우는 매우 다양한데, audio codec, camera  센서, 각종 analog 센서(온도 센서, 광 센서, 가속도 센서, 압력 센서, 자기장 센서 등) 등이 그 대표적인 예라 할 수 있다. 

AD7887 ADC는 2 채널 12 bit 아날로그 디지털 컨버터로 두개의 채널에 아날로그 센서를 달아 동작테스트가 가능하다. 한가지 주의할 사항으로는 BBB의 확장 헤더에 연결된 slave 칩이 5V 전압에서 동작할 경우, BBB의 SPI 수신 단자(MISO)가 손상될 가능성이 크다. 이를 방지하기 위해서는 적절한 전압 변환 회로(Level convertor)를 중간에 연결해 주어야 한다. 아래 그림 7.2는 AD7887 ADC의 블럭도(왼편 두개의 채널 - AIN0/1, 하단에 4개의 SPI 핀 - DIN, CS/, DOUT, SCLK, 나머지 전원 VDD, GND)를 표현한 것으로, AD7887 ADC 관련 보다 자세한 사항은 아래 site를 참조하기 바란다.




그림 7.2 AD7887 ADC 블럭 다이어그램

이절의 내용 전개 순서는 크게 다음과 같다.
1) 리눅스 SPI 프레임워크 소개
2) Device Tree를 사용하여 SPI controller & slave device 표현
  : pinmux 선언 및 결합(association)
  : ad7887 slave device 노드 추가
3) SPI controller(master) driver 코드 분석
4) SPI slave device driver 코드 분석
  : ad7887 slave device driver
  : IIO subsystem


7.1 리눅스 SPI 프레임워크 소개
SPI 장치는 i2c와 마찬가지로 master와 slave 장치로 이루어져 있다. SPI master는 대개 SPI bus controller를 의미하며, struct spi_master data structure를 통해 표현한다. 당연한 얘기지만, master의 역할은 slave device로 데이터를 전달하거나, slave device로 부터 데이터를 전달받는 부분을 관장한다. 반면, SPI slave는 struct spi_device data structure로 표현할 수 있으며, 역할은 master와 반대라고 볼 수 있다. SPI는 장치에 대한 실시간 enumeration을 지원하지 않기 때문에, 보드 파일 혹은 device tree에 자세한 사항을 명확히 기술해 주어야 하며, (당연한 얘기지만) 이와 연결되는 master 및 slave 디바이스 드라이버를 함께 구현해 주어야 한다.



리눅스 SPI 프레임워크(혹은 subsystem)를 좀 더 구체적으로 이해하기 위하여, Aric Wang님이 작성하신 blog 내용 중, 일부를 아래에 복사해 두었다. 원문을 번역하지 않고 그대로 둔 이유는, 보다 명확한 의미 전달을 위해서이므로, 아래 내용을 자세히 읽어 보기 바란다.


///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
<SPI Master Driver>
SPI master driver should be registered with probe() function. When SPI master device is found, the probe() function will be called. 
The probe() function will initialize the SPI hardware based on the platform data of SPI master. It will allocate struct spi_master and register it to the system. The it will scan the platform data to find the SPI devices connected to this SPI bus.
scan_boardinfo() scans the platform data, and call spi_new_device() to create SPI device data structure, and set up struct spi_device based on the platform information. Then it calls the master's setup() method to further initialize, link the struct spi_device with struct spi_master, and add the SPI device to the system.
To this point, the SPI master and SPI device are created and added to the system. But it still can't communicate with the SPI device as no specific driver is installed yet.

<SPI Device Driver>
SPI device needs a driver to work. A general char device driver is implemented in Linux to support basic read()/write()/ioctl() methods. To link up the SPI device with this driver, just need to define the modalias of struct spi_board_info to "spidev".
If a driver is available for SPI device, its probe() method will be called when the SPI device is created and added to the system.

<SPI Data Transfer>
The user application needs a SPI device to access the data transfer service from SPI. Read/write/ioctl can be used for data transfer. The SPI device driver utlizes the SPI framework structures to communicate with SPI master driver. The struct spi_message is used to schedule a message to the SPI master's queue. Each message might includes a list of struct spi_transfer.
The SPI master's transfer function will be called by the SPI device driver to start the transfer. The SPI device driver normally needs to wait the SPI master finishs the transfer before return to the user application.
The SPI master driver needs to implement a mechanism to send the data on the SPI bus using the SPI device specified settings. It's the SPI master driver's responsibilities to operate the hardware to send out the data. Normally, the SPI master needs to implement:
  - a message queue: to hold the messages from the SPI device driver
  - a workqueue and workqueue thread: to pump the messages from the message queue and start transfer
  - a tasklet and tasklet handler: to send the data on the hardware
  - a interrupt handler: to handle the interrupts during the transfer

While the SPI master transfering the data, the SPI device driver normally implement a completion to wait the SPI master finishes the transfer.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////



SPI는 장치에 대한 실시간 enumeration을 지원하지 않기 때문에 non-dynamic 방식으로 구분(반대로 USB의 경우는 dynamic 방식으로 볼 수 있음)할 수 있다. 따라서 시스템에서는 어느 장치(device)가 어느 SPI bus의 어느 위치에 놓여 있는지를 사전에 알고 있어야 한다.
리눅스 커널의 SPI 서브시스템 관련 코드는 drivers/spi 디렉토리에 위치하며, 그 중, drivers/spi/spi.c 파일이 spi core에 해당하는 부분으로, 이 곳에서 SPI 관련 struct bus_type data structure를 정의하고 있으며, SPI 디바이스 드라이버를 등록하는 함수도 이곳에 구현되어 있다.  즉, SPI controller(adapter) 드라이버는 spi_register_master() 함수를 통해서 등록할 수 있으며, SPI device는 spi_register_driver() 함수를 사용해서 등록이 가능하다.
drivers/spi 디렉토리 아래에는 이 밖에도, 다양한 SPI adapter 드라이버가 포함되어 있는데, 예를 들자면, Atmel, OMAP, Xilinx, Samsung 등이 이에 해당한다. 이들 중 대부분은 platform_drivers나 of_platform_drivers 형태이지만, pci, amba, partport 드라이버도 일부 포함되어 있음을 알 수 있다.
drivers/spi/spidev.c 파일은 사용자 계층에서 SPI bus를 접근할 수 있도록 하는 코드를 담고 있다. 만일 사용자 영역의 application에서 SPI 장치를 제어하고자 한다면, "/dev/spidev0.0"나 "/dev/spidev0.1" 등의 파일을 open하여 사용하여야 한다. 아래 코드는 이러한 내용을 잘 보여주는 예로, Gordon Henderson이 만든 WiringPI 프로젝트 코드 중, SPI 관련 내용 중 일부를 발췌한 것이다.





////////////////////////////////////////////////////////////////////////////////////////////////////////////

const static char       *spiDev0  = "/dev/spidev0.0" ;
const static char       *spiDev1  = "/dev/spidev0.1" ;
const static uint8_t     spiMode  = 0 ;
const static uint8_t     spiBPW   = 8 ;
const static uint16_t    spiDelay = 0 ;


int wiringPiSPISetup (int channel, int speed)
{
    int fd ;

   channel &= 1 ;

   if ((fd = open (channel == 0 ? spiDev0 : spiDev1, O_RDWR)) < 0)
     return wiringPiFailure (WPI_ALMOST, "Unable to open SPI device: %s\n", strerror (errno)) ;

   spiSpeeds [channel] = speed ;
   spiFds    [channel] = fd ;

   // Set SPI parameters.
  //  Why are we reading it afterwriting it? I've no idea, but for now I'm blindly
  //  copying example code I've seen online...

  if (ioctl (fd, SPI_IOC_WR_MODE, &spiMode)         < 0)
    return wiringPiFailure (WPI_ALMOST, "SPI Mode Change failure: %s\n", strerror (errno)) ;

  if (ioctl (fd, SPI_IOC_WR_BITS_PER_WORD, &spiBPW) < 0)
    return wiringPiFailure (WPI_ALMOST, "SPI BPW Change failure: %s\n", strerror (errno)) ;

  if (ioctl (fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed)   < 0)
    return wiringPiFailure (WPI_ALMOST, "SPI Speed Change failure: %s\n", strerror (errno)) ;

  return fd ;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////


마지막으로 SPI 디바이스 드라이버(slave device driver)는 당연히 kernel tree 이곳 저곳에 널려 있다.

[여기서 잠깐 !] device driver 위치와 관련하여
리눅스 커널에서 drivers 디레토리 아래의 i2c/, spi/, tty/serial/, usb/, mmc/ 등에 있는 내용은 모두 bus controller(혹은 adapter) 드라이버를 위한 코드이며, SoC 형태의 칩을 기반으로 할 경우, 모두 platform driver 형태로 구현되어 있다. 반면, 이들 bus controller를 사용하는(붙어있는) device(보통 slave device, consumer, 혹은 단순히 device로 표현함)들은 그 device가 사용되는 실제 용도, 가령 네트워크, 디스플레이, 입력장치, 오디오, 비디오, 메모리 장치, 아날로그 센서 등에 따라 존재하는 위치가 결정되게 된다.따라서 그 위치는 위에서 표현한 것과 마찬가지로, kernel tree 이곳 저곳에 널려 있다고 말할 수 있다. 또하나, 참고할 사항으로는 이들 device driver의 경우는 사용자 영역과의 통신, 사용하는 버스, 실제 칩에 존재하는 여러 기능(muli-function) 등을 고려해, 관련 기능이 복합적으로 얽혀 있는 경우가 대부분이다.
/////////////////////////////////////////////////////////////////////////////////////////////

지금까지 설명한 SPI 프레임워크 관련 내용을 하나로 그림으로 표현하면 다음과 같다(참고문서 [19] 참조).

그림 7.3 리눅스 SPI 아키텍쳐

7.2 SPI 관련 Device Tree
AM335x의 SPI controller 관련하여 device tree에 정의된 내용을 살펴보기에 앞서, AM335x TRM 문서의 24장 "Multichannel Serial Port Interface (McSPI)"을 보면 아래 그림 7.4와 같은 McSPI SPI controller(McSPI0)가 두개(SPI0, SPI1) 존재함을 알 수 있다.



그림 7.4 AM335x SPI Controller 구조

[참고] SPI adapter(controller)는 반드시 master로만 동작하는 것이 아니고, 상황에 따라 slave로도 동작 가능하다.

이러한 내용(두개의 SPI controller가 존재)은 am33xx.dtsi 파일을 통해서도 재확인이 가능한데, 그 내용을 정리해 보면 다음과 같다.

        <arch/arm/boot/dts/am33xx.dtsi 파일 내용 중, spi controller 부분 발췌>

        spi0: spi@48030000 {
            compatible = "ti,omap4-mcspi";    /* drivers/spi/spi-omap2-mcspi.c와 binding됨 */
            #address-cells = <1>;           /* 하위 노드의 주소 셀의 갯수 = 1개 */
            #size-cells = <0>;                 
            reg = <0x48030000 0x400>;   /* spi0 레지스터 정보(번지, 값) */
            interrupt = <65>;                  /* spi0 controller가 갖는 인터럽트 번호 */
            ti,spi-num-cs = <2>;            /* SPI chipselect 라인의 갯수 = 2 */
            ti,hwmods = "spi0";
            dmas = <&edma 16              /* spi0 관련 dma 설정 */
                &edma 17
                &edma 18
                &edma 19>;
            dma-names = "tx0", "rx0", "tx1", "rx1";
            status = "disabled";              /* spi0 controller의 상태 - 비사용중 */
        };

        spi1: spi@481a0000 {
            compatible = "ti,omap4-mcspi";
            #address-cells = <1>;
            #size-cells = <0>;
            reg = <0x481a0000 0x400>;
            interrupt = <125>;
            ti,spi-num-cs = <2>;
            ti,hwmods = "spi1";
            dmas = <&edma 42
                &edma 43
                &edma 44
                &edma 45>;
            dma-names = "tx0", "rx0", "tx1", "rx1";
            status = "disabled";

        };

이제부터 살펴볼 내용은, BBB P9 확장핀 헤더에 SPI slave device를 연결하기 위해 dts 파일을 어떻게 수정해야 하는지에 관한 것이다. 문제를 좀 더 명확히 하기위해, 이 단계에서 수행하는 작업을 간단히 요약해 보면 다음과 같다.

1) BBB P9 Pin 22, 21, 18, 17번 4개 핀에 SPI slave device 연결(그림 7.5 - 7.6 참조)
  => pinmux 설정
2) AD8997 SPI slave node 추가


그림 7.5 BBB P9 Pin 배치도(spi 관련 - Pin 22, 21, 18, 17 - 모두 Mode 0)



그림 7.6 AM335x TRM Control Module 레지스터 관련(spi 관련)

Pinmux 설정과 관련하여, BBB 확장 헤더 핀을 보는 법은 이미, 5절 및 6절에서 자세히 소개했으므로, 여기서는 생략하기로 하겠다. 그렇다면 위의 테이블 내용을 참고로 할 때, SPI0 pinmux 설정을 추가하고, ad7887 ADC slave node를 추가하기 위해서는 어떻게 해야 할까? 해답은 아래에 있다.

        <arch/arm/boot/dts/am335x-boneblack.dts 수정 사항>
        spi0_pins_s0: spi0_pins_s0 {
            pinctrl-single,pins = <
                0x150 0x30  /* spi0_sclk.spi0_sclk, INPUT_PULLUP | MODE0 */
                0x154 0x30  /* spi0_d0.spi0_d0, INPUT_PULLUP | MODE0 */
                0x158 0x10  /* spi0_d1.spi0_d1, OUTPUT_PULLUP | MODE0 */
                0x15c 0x10  /* spi0_cs0.spi0_cs0, OUTPUT_PULLUP | MODE0 */
            >;
        };
       &spi0 {
            pinctrl-names = "default";
            pinctrl-0 = <&spi0_pins_s0>;

            status = "okay";
            ad7887@0 {
                compatible = "adinc,ad7887";
                spi-max-frequency = <24000000>;   /* TODO */
                reg = <0>;
            };
       };


7.3 SPI controller 드라이버 분석
drivers/spi/spi-omap2-mcspi.c 파일의 주요 부분을 분석해 보기로 하자.

<TODO>
/////////////////////////////////////////////////////////////////////////////////////////////
static const struct of_device_id omap_mcspi_of_match[] = {
{
.compatible = "ti,omap2-mcspi",
.data = &omap2_pdata,
},
{
.compatible = "ti,omap4-mcspi",
.data = &omap4_pdata,
},
{ },
};
MODULE_DEVICE_TABLE(of, omap_mcspi_of_match);

static int omap2_mcspi_probe(struct platform_device *pdev)
{
struct spi_master *master;
const struct omap2_mcspi_platform_config *pdata;
struct omap2_mcspi *mcspi;
struct resource *r;
int status = 0, i;
u32 regs_offset = 0;
static int bus_num = 1;
struct device_node *node = pdev->dev.of_node;
const struct of_device_id *match;
struct pinctrl *pinctrl;

master = spi_alloc_master(&pdev->dev, sizeof *mcspi);
if (master == NULL) {
dev_dbg(&pdev->dev, "master allocation failed\n");
return -ENOMEM;
}

/* the spi->mode bits understood by this driver: */
master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;

master->setup = omap2_mcspi_setup;
master->prepare_transfer_hardware = omap2_prepare_transfer;
master->unprepare_transfer_hardware = omap2_unprepare_transfer;
master->transfer_one_message = omap2_mcspi_transfer_one_message;
master->cleanup = omap2_mcspi_cleanup;
master->dev.of_node = node;

dev_set_drvdata(&pdev->dev, master);

mcspi = spi_master_get_devdata(master);
mcspi->master = master;

match = of_match_device(omap_mcspi_of_match, &pdev->dev);
if (match) {
u32 num_cs = 1; /* default number of chipselect */
pdata = match->data;

of_property_read_u32(node, "ti,spi-num-cs", &num_cs);
master->num_chipselect = num_cs;
master->bus_num = bus_num++;
if (of_get_property(node, "ti,pindir-d0-out-d1-in", NULL))
mcspi->pin_dir = MCSPI_PINDIR_D0_OUT_D1_IN;
} else {
pdata = pdev->dev.platform_data;
master->num_chipselect = pdata->num_cs;
if (pdev->id != -1)
master->bus_num = pdev->id;
mcspi->pin_dir = pdata->pin_dir;
}
regs_offset = pdata->regs_offset;

r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (r == NULL) {
status = -ENODEV;
goto free_master;
}

r->start += regs_offset;
r->end += regs_offset;
mcspi->phys = r->start;

mcspi->base = devm_request_and_ioremap(&pdev->dev, r);
if (!mcspi->base) {
dev_dbg(&pdev->dev, "can't ioremap MCSPI\n");
status = -ENOMEM;
goto free_master;
}

mcspi->dev = &pdev->dev;

INIT_LIST_HEAD(&mcspi->ctx.cs);

mcspi->dma_channels = kcalloc(master->num_chipselect,
sizeof(struct omap2_mcspi_dma),
GFP_KERNEL);

if (mcspi->dma_channels == NULL)
goto free_master;

for (i = 0; i < master->num_chipselect; i++) {
char *dma_rx_ch_name = mcspi->dma_channels[i].dma_rx_ch_name;
char *dma_tx_ch_name = mcspi->dma_channels[i].dma_tx_ch_name;
struct resource *dma_res;

sprintf(dma_rx_ch_name, "rx%d", i);
if (!pdev->dev.of_node) {
dma_res =
platform_get_resource_byname(pdev,
    IORESOURCE_DMA,
    dma_rx_ch_name);
if (!dma_res) {
dev_dbg(&pdev->dev,
"cannot get DMA RX channel\n");
status = -ENODEV;
break;
}

mcspi->dma_channels[i].dma_rx_sync_dev =
dma_res->start;
}
sprintf(dma_tx_ch_name, "tx%d", i);
if (!pdev->dev.of_node) {
dma_res =
platform_get_resource_byname(pdev,
    IORESOURCE_DMA,
    dma_tx_ch_name);
if (!dma_res) {
dev_dbg(&pdev->dev,
"cannot get DMA TX channel\n");
status = -ENODEV;
break;
}

mcspi->dma_channels[i].dma_tx_sync_dev =
dma_res->start;
}
}

if (status < 0)
goto dma_chnl_free;

pinctrl = devm_pinctrl_get_select_default(&pdev->dev);
if (IS_ERR(pinctrl))
dev_warn(&pdev->dev,
"pins are not configured from the driver\n");

pm_runtime_use_autosuspend(&pdev->dev);
pm_runtime_set_autosuspend_delay(&pdev->dev, SPI_AUTOSUSPEND_TIMEOUT);
pm_runtime_enable(&pdev->dev);

if (status || omap2_mcspi_master_setup(mcspi) < 0)
goto disable_pm;

status = spi_register_master(master);
if (status < 0)
goto disable_pm;

return status;

disable_pm:
pm_runtime_disable(&pdev->dev);
dma_chnl_free:
kfree(mcspi->dma_channels);
free_master:
spi_master_put(master);
return status;
}

static int omap2_mcspi_remove(struct platform_device *pdev)
{
struct spi_master *master;
struct omap2_mcspi *mcspi;
struct omap2_mcspi_dma *dma_channels;

master = dev_get_drvdata(&pdev->dev);
mcspi = spi_master_get_devdata(master);
dma_channels = mcspi->dma_channels;

pm_runtime_put_sync(mcspi->dev);
pm_runtime_disable(&pdev->dev);

spi_unregister_master(master);
kfree(dma_channels);

return 0;
}

/* work with hotplug and coldplug */
MODULE_ALIAS("platform:omap2_mcspi");

static struct platform_driver omap2_mcspi_driver = {
.driver = {
.name = "omap2_mcspi",
.owner = THIS_MODULE,
.pm = &omap2_mcspi_pm_ops,
.of_match_table = omap_mcspi_of_match,
},
.probe = omap2_mcspi_probe,
.remove = omap2_mcspi_remove,
};

module_platform_driver(omap2_mcspi_driver);
MODULE_LICENSE("GPL");
/////////////////////////////////////////////////////////////////////////////////////////////

7.4 SPI device 드라이버
마지막으로 ad7887 SPI slave device driver 코드를 분석하는 것으로 이절을 마무리하도록 하겠다.

/////////////////////////////////////////////////////////////////////////////////////////////
[여기서 잠깐 !] IIO subsystem에 관하여
<TODO>
/////////////////////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////////////////////////////////

/*
 * AD7887 SPI ADC driver
 *
 * Copyright 2010-2011 Analog Devices Inc.
 *
 * Licensed under the GPL-2.
 */

#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/sysfs.h>
#include <linux/spi/spi.h>
#include <linux/regulator/consumer.h>
#include <linux/err.h>
#include <linux/module.h>
#include <linux/interrupt.h>

#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/buffer.h>

#include <linux/iio/trigger_consumer.h>
#include <linux/iio/triggered_buffer.h>

#include <linux/platform_data/ad7887.h>

#define AD7887_REF_DIS (1 << 5) /* on-chip reference disable */
#define AD7887_DUAL (1 << 4) /* dual-channel mode */
#define AD7887_CH_AIN1 (1 << 3) /* convert on channel 1, DUAL=1 */
#define AD7887_CH_AIN0 (0 << 3) /* convert on channel 0, DUAL=0,1 */
#define AD7887_PM_MODE1 (0) /* CS based shutdown */
#define AD7887_PM_MODE2 (1) /* full on */
#define AD7887_PM_MODE3 (2) /* auto shutdown after conversion */
#define AD7887_PM_MODE4 (3) /* standby mode */

enum ad7887_channels {
AD7887_CH0,
AD7887_CH0_CH1,
AD7887_CH1,
};

#define RES_MASK(bits) ((1 << (bits)) - 1)

/**
 * struct ad7887_chip_info - chip specifc information
 * @int_vref_mv: the internal reference voltage
 * @channel: channel specification
 */
struct ad7887_chip_info {
u16 int_vref_mv;
struct iio_chan_spec channel[3];
};

struct ad7887_state {
struct spi_device *spi;
const struct ad7887_chip_info *chip_info;
struct regulator *reg;
struct spi_transfer xfer[4];
struct spi_message msg[3];
struct spi_message *ring_msg;
unsigned char tx_cmd_buf[4];

/*
* DMA (thus cache coherency maintenance) requires the
* transfer buffers to live in their own cache lines.
* Buffer needs to be large enough to hold two 16 bit samples and a
* 64 bit aligned 64 bit timestamp.
*/
unsigned char data[ALIGN(4, sizeof(s64)) + sizeof(s64)]
____cacheline_aligned;
};

enum ad7887_supported_device_ids {
ID_AD7887
};

static int ad7887_ring_preenable(struct iio_dev *indio_dev)
{
struct ad7887_state *st = iio_priv(indio_dev);
int ret;

ret = iio_sw_buffer_preenable(indio_dev);
if (ret < 0)
return ret;

/* We know this is a single long so can 'cheat' */
switch (*indio_dev->active_scan_mask) {
case (1 << 0):
st->ring_msg = &st->msg[AD7887_CH0];
break;
case (1 << 1):
st->ring_msg = &st->msg[AD7887_CH1];
/* Dummy read: push CH1 setting down to hardware */
spi_sync(st->spi, st->ring_msg);
break;
case ((1 << 1) | (1 << 0)):
st->ring_msg = &st->msg[AD7887_CH0_CH1];
break;
}

return 0;
}

static int ad7887_ring_postdisable(struct iio_dev *indio_dev)
{
struct ad7887_state *st = iio_priv(indio_dev);

/* dummy read: restore default CH0 settin */
return spi_sync(st->spi, &st->msg[AD7887_CH0]);
}

/**
 * ad7887_trigger_handler() bh of trigger launched polling to ring buffer
 *
 * Currently there is no option in this driver to disable the saving of
 * timestamps within the ring.
 **/
static irqreturn_t ad7887_trigger_handler(int irq, void *p)
{
struct iio_poll_func *pf = p;
struct iio_dev *indio_dev = pf->indio_dev;
struct ad7887_state *st = iio_priv(indio_dev);
s64 time_ns;
int b_sent;

b_sent = spi_sync(st->spi, st->ring_msg);
if (b_sent)
goto done;

time_ns = iio_get_time_ns();

if (indio_dev->scan_timestamp)
memcpy(st->data + indio_dev->scan_bytes - sizeof(s64),
      &time_ns, sizeof(time_ns));

iio_push_to_buffers(indio_dev, st->data);
done:
iio_trigger_notify_done(indio_dev->trig);

return IRQ_HANDLED;
}

static const struct iio_buffer_setup_ops ad7887_ring_setup_ops = {
.preenable = &ad7887_ring_preenable,
.postenable = &iio_triggered_buffer_postenable,
.predisable = &iio_triggered_buffer_predisable,
.postdisable = &ad7887_ring_postdisable,
};

static int ad7887_scan_direct(struct ad7887_state *st, unsigned ch)
{
int ret = spi_sync(st->spi, &st->msg[ch]);
if (ret)
return ret;

return (st->data[(ch * 2)] << 8) | st->data[(ch * 2) + 1];
}

static int ad7887_read_raw(struct iio_dev *indio_dev,
  struct iio_chan_spec const *chan,
  int *val,
  int *val2,
  long m)
{
int ret;
struct ad7887_state *st = iio_priv(indio_dev);

switch (m) {
case IIO_CHAN_INFO_RAW:
mutex_lock(&indio_dev->mlock);
if (iio_buffer_enabled(indio_dev))
ret = -EBUSY;
else
ret = ad7887_scan_direct(st, chan->address);
mutex_unlock(&indio_dev->mlock);

if (ret < 0)
return ret;
*val = ret >> chan->scan_type.shift;
*val &= RES_MASK(chan->scan_type.realbits);
return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
if (st->reg) {
*val = regulator_get_voltage(st->reg);
if (*val < 0)
return *val;
*val /= 1000;
} else {
*val = st->chip_info->int_vref_mv;
}

*val2 = chan->scan_type.realbits;

return IIO_VAL_FRACTIONAL_LOG2;
}
return -EINVAL;
}


static const struct ad7887_chip_info ad7887_chip_info_tbl[] = {
/*
* More devices added in future
*/
[ID_AD7887] = {
.channel[0] = {
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 1,
.info_mask = IIO_CHAN_INFO_RAW_SEPARATE_BIT |
IIO_CHAN_INFO_SCALE_SHARED_BIT,
.address = 1,
.scan_index = 1,
.scan_type = IIO_ST('u', 12, 16, 0),
},
.channel[1] = {
.type = IIO_VOLTAGE,
.indexed = 1,
.channel = 0,
.info_mask = IIO_CHAN_INFO_RAW_SEPARATE_BIT |
IIO_CHAN_INFO_SCALE_SHARED_BIT,
.address = 0,
.scan_index = 0,
.scan_type = IIO_ST('u', 12, 16, 0),
},
.channel[2] = IIO_CHAN_SOFT_TIMESTAMP(2),
.int_vref_mv = 2500,
},
};

static const struct iio_info ad7887_info = {
.read_raw = &ad7887_read_raw,
.driver_module = THIS_MODULE,
};

static int ad7887_probe(struct spi_device *spi)
{
struct ad7887_platform_data *pdata = spi->dev.platform_data;
struct ad7887_state *st;
struct iio_dev *indio_dev = iio_device_alloc(sizeof(*st));
uint8_t mode;
int ret;

if (indio_dev == NULL)
return -ENOMEM;

st = iio_priv(indio_dev);

if (!pdata || !pdata->use_onchip_ref) {
st->reg = regulator_get(&spi->dev, "vref");
if (IS_ERR(st->reg)) {
ret = PTR_ERR(st->reg);
goto error_free;
}

ret = regulator_enable(st->reg);
if (ret)
goto error_put_reg;
}

st->chip_info =
&ad7887_chip_info_tbl[spi_get_device_id(spi)->driver_data];

spi_set_drvdata(spi, indio_dev);
st->spi = spi;

/* Estabilish that the iio_dev is a child of the spi device */
indio_dev->dev.parent = &spi->dev;
indio_dev->name = spi_get_device_id(spi)->name;
indio_dev->info = &ad7887_info;
indio_dev->modes = INDIO_DIRECT_MODE;

/* Setup default message */

mode = AD7887_PM_MODE4;
if (!pdata || !pdata->use_onchip_ref)
mode |= AD7887_REF_DIS;
if (pdata && pdata->en_dual)
mode |= AD7887_DUAL;

st->tx_cmd_buf[0] = AD7887_CH_AIN0 | mode;

st->xfer[0].rx_buf = &st->data[0];
st->xfer[0].tx_buf = &st->tx_cmd_buf[0];
st->xfer[0].len = 2;

spi_message_init(&st->msg[AD7887_CH0]);
spi_message_add_tail(&st->xfer[0], &st->msg[AD7887_CH0]);

if (pdata && pdata->en_dual) {
st->tx_cmd_buf[2] = AD7887_CH_AIN1 | mode;

st->xfer[1].rx_buf = &st->data[0];
st->xfer[1].tx_buf = &st->tx_cmd_buf[2];
st->xfer[1].len = 2;

st->xfer[2].rx_buf = &st->data[2];
st->xfer[2].tx_buf = &st->tx_cmd_buf[0];
st->xfer[2].len = 2;

spi_message_init(&st->msg[AD7887_CH0_CH1]);
spi_message_add_tail(&st->xfer[1], &st->msg[AD7887_CH0_CH1]);
spi_message_add_tail(&st->xfer[2], &st->msg[AD7887_CH0_CH1]);

st->xfer[3].rx_buf = &st->data[2];
st->xfer[3].tx_buf = &st->tx_cmd_buf[2];
st->xfer[3].len = 2;

spi_message_init(&st->msg[AD7887_CH1]);
spi_message_add_tail(&st->xfer[3], &st->msg[AD7887_CH1]);

indio_dev->channels = st->chip_info->channel;
indio_dev->num_channels = 3;
} else {
indio_dev->channels = &st->chip_info->channel[1];
indio_dev->num_channels = 2;
}

ret = iio_triggered_buffer_setup(indio_dev, &iio_pollfunc_store_time,
&ad7887_trigger_handler, &ad7887_ring_setup_ops);
if (ret)
goto error_disable_reg;

ret = iio_device_register(indio_dev);
if (ret)
goto error_unregister_ring;

return 0;
error_unregister_ring:
iio_triggered_buffer_cleanup(indio_dev);
error_disable_reg:
if (st->reg)
regulator_disable(st->reg);
error_put_reg:
if (st->reg)
regulator_put(st->reg);
error_free:
iio_device_free(indio_dev);

return ret;
}

static int ad7887_remove(struct spi_device *spi)
{
struct iio_dev *indio_dev = spi_get_drvdata(spi);
struct ad7887_state *st = iio_priv(indio_dev);

iio_device_unregister(indio_dev);
iio_triggered_buffer_cleanup(indio_dev);
if (st->reg) {
regulator_disable(st->reg);
regulator_put(st->reg);
}
iio_device_free(indio_dev);

return 0;
}

static const struct spi_device_id ad7887_id[] = {
{"ad7887", ID_AD7887},
{}
};
MODULE_DEVICE_TABLE(spi, ad7887_id);

static struct spi_driver ad7887_driver = {
.driver = {
.name = "ad7887",
.owner = THIS_MODULE,
},
.probe = ad7887_probe,
.remove = ad7887_remove,
.id_table = ad7887_id,
};
module_spi_driver(ad7887_driver);

MODULE_AUTHOR("Michael Hennerich <hennerich@blackfin.uclinux.org>");
MODULE_DESCRIPTION("Analog Devices AD7887 ADC");
MODULE_LICENSE("GPL v2");

/////////////////////////////////////////////////////////////////////////////////////////////

이상으로 간단하게 나마, BBB에서 SPI controller를 사용할 수 있도록 활성화시키고, 이를 사용하는 adapter 드라이버를 작성하는 방법에 관하여 소개하였다. 또한, SPI bus에 ADC slave device를 연결하고, 이를 제어하는 디바이스 드라이버 관련 코드를 분석해 보았다. 


References

<BBB>
1) BeagleBone Black System Reference Manual Revision B, January 20, 2014, Gerald Coley

2) AM335x ARM® CortexTM-A8 Microprocessors(MPUs) Technical Reference Manual, Texas Instruments

3) Getting Started With BeagleBone, by Matt Richardson, Maker Media

4) http://eewiki.net/display/linuxonarm/BeagleBone+Black - Robert Nelson

<Device Tree & Overlay>
5) Device Tree for dummies.pdf - Thomas Petazzoni, Free Electrons

6) Linux kernel and driver development training Lab Book, Free Electrons

7) Linux Kernel and Driver Development Training, Free Electrons

8) Linux kernel: consolidation in the ARM architecture support, Thomas Petazzoni, Free Electrons,

9) ARM support in the Linux kernel, Thomas Petazzoni, Free Electrons

10) Your new ARM SoC Linux support check-list! Thomas Petazzoni, CLEMENT, Free Electrons,

11) Supporting 200 different expansionboards - The broken promise of device tree, CircuitCo.

12) The Device Tree: Plug and play for Embedded Linux, Eli Billauer

13) Introduction to the BeagleBone Black Device Tree, Created by Justin Cooper

<Pin Control>
14) Pin Control Sybsystem – Building Pins and GPIO from the ground up, Linus Walleij

15) PIN CONTROL OVERVIEW, Linaro Kernel Workgroup, ST-Ericsson, Linus Walleij

<Wii Nunchuk>
16) ZX-NUNCHUK(#8000339) Wii-Nunchuk interface board

<UART>
17) Essential Linux Device Drivers, Sreekrishnan Venkateswaran, PRENTICE HALL
18) Serial drivers, Thomas Petazzoni, Free Electrons

<SPI>
19) Linux kernel and driver development training, Sebastien Jan/Gregory Clement/Thomas Petazzoni/Michael Opdenacker, 2011

<AD7887 datasheet>
20) AD7887 datasheet, Analog Devices Inc.

<IIO subsystem>
21) IIO, a new kernel subsystem, Maxime Ripard, Free Electrons

댓글 5개:

  1. BBB SPI 1:1 통신을 하려고 합니다. BBB의 Slave 셋팅은 어떻게 하나요??

    답글삭제
  2. 글쎄요 .... 좀 고민이되는 문제군요.
    일단은 먼저, "AM335x ARM Cortex-A8 Microprocessors Technical Reference Manual"을 살펴 보는 것으로 시작을 해야 할 것 같군요.


    ==================================
    24.3.4 Slave Mode
    McSPI is in slave mode when the bit MS of the register MCSPI_MODULCTRL is set.
    In slave mode, McSPI can be connected to up to 4 external SPI master devices. McSPI handles
    transactions with a single SPI master device at a time.
    In slave mode, McSPI initiates data transfer on the data lines (SPIDAT[1;0]) when it receives an SPI clock
    (SPICLK) from the external SPI master device.
    The controller is able to work with or without a chip select SPIEN depending on
    MCSPI_MODULCTRL[PIN34] bit setting. It also supports transfers without a dead cycle between two
    successive words.
    -------------------------------------------------------------------------------------------------------------------------------------------------
    위의 내용을 잠시 살펴 보면, McSPI controller가 slave 모드로 동작하기 위해서는 MCSPI_MODULCTRL 레지스터의 MS bit(3번째 bit임)를 1로 설정해 주어야만 한다는군요(관련 레지스터의 필드 정보는 4492 페이지에서 참조 가능).

    위의 내용과 관련한 SPI controller driver(drivers/spi/spi-omap2-mcspi.c)의 코드 내용(원본)을 살펴보면 아래와 같습니다.
    코드를 보시면 아시겠지만, omap2_mcspi_probe() 함수의 끝 부분에서 omap2_mcspi_master_setup() 함수를 호출하고, 다시 이 함수에서 아래 함수를 호출해 주고 있는 것을 알 수 있습니다.

    static void omap2_mcspi_set_master_mode(struct spi_master *master)
    {
    struct omap2_mcspi *mcspi = spi_master_get_devdata(master);
    struct omap2_mcspi_regs *ctx = &mcspi->ctx;
    u32 l;

    /*
    * Setup when switching from (reset default) slave mode
    * to single-channel master mode
    */
    l = mcspi_read_reg(master, OMAP2_MCSPI_MODULCTRL);
    l &= ~(OMAP2_MCSPI_MODULCTRL_STEST | OMAP2_MCSPI_MODULCTRL_MS); /* 이 부분을 수정해야 함 */
    l |= OMAP2_MCSPI_MODULCTRL_SINGLE;
    mcspi_write_reg(master, OMAP2_MCSPI_MODULCTRL, l);

    ctx->modulctrl = l;
    }

    그렇다면, 이 부분만 수정하면 될 까요 ? 솔직히 두대의 BBB를 가지고 직접 연결 시험을 해본 것이 아니므로, 위의 내용 외에도 의심(고심)해 보아야할 부분을 아래에 몇자 적어 보았습니다.

    ----------------
    0) MCSPI_MODULCTRL 레지스터의 MS bit(3번째 bit임)를 1로 설정(이미 앞서 설명한 내용)

    1) SPIDAT0(MOSI), SPIDAT1(MISO)의 방향 값을 바꿔야 할 수도 있을 듯 보임(MCSPI_SYST 레지스터 설정 변경)
    => 두개의 data line을 cross로 연결하거나, 레지스터 설정을 변경하여 방향을 바꿔야 할 수도 있을 듯.
    => 연결 선을 최대한 짧게 해야 할 수도 있을 보임. 아래 site 내용도 한번 참조해 보시구요.
    (http://linux.omap.com/pipermail/linux-omap-open-source/2007-July/010633.html)

    2) BBB 두대를 상호 연결하는 것이니, BBB 확장 헤더의 특정 핀을 사용해야 할텐데, 이를 위해서는 필히 pinmux 설정을 해주셔야 함(이 내용은 본 블로그 내용 참조).

    이 밖에도 실제로 실험을 하다보면, 예상치 못한 다른 문제가 발견될 수 있을 것으로 보이는데, 직접 한번 해보시고, 내용 공유해 주시면 감사하겠네요^^.


    답글삭제
    답글
    1. 좋은 정보를 주셔서 감사합니다.

      삭제
  3. 좋은 자료 제공해 주셔서 감사합니다.

    답글삭제
  4. 상세한 설명 감사 드리며 많은 도움이 되고 있습니다.

    "그림 7.4 AM335x SPI Controller 구조"에 대해 질문이 있습니다.

    해당 그림에서는 Slave Device 두 개를 하나의 SPI포트에 연결하여 CS0,1을 사용하여 제어 하고 있습니다.

    리눅스에서 이와 같이 사용하고자 한다면 어느부분을 수정 또는 무엇을 참고 해야 할지 알고 있으시면 도움 부탁드립니다.

    감사합니다.

    답글삭제