版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2022 凌云物网智科实验室·郭工

  • Author: Guo Wenxue Email: guowenxue@gmail.com QQ: 281143292

wechat_pub

2.8 SPI接口编程之回环测试

2.8.1 SPI简介

SPI 是串行外设接口(Serial Peripheral Interface)的缩写。是Motorola 公司推出的一种同步串行接口技术,是一种高速的,全双工,同步的通信总线。通常由一个主模块和一个或多个从模块组成,主模块选择一个从模块进行同步通信,从而完成数据的交换。SPI 是一个环形结构,通信时需要至少4 根线。它们是MISO(主设备数据输入)、MOSI(主设备数据输出)、SCLK(时钟)、CS(片选),一个主机和一个从机的一般连接方式如下图所示。

spi_master_slave_connect

SPI 优点:支持全双工通信、通信简单、数据传输速率快。 ​ SPI 缺点:没有指定的流控制,没有应答机制确认是否接收到数据,所以跟IIC 总线协议比较在数据可靠性上有一定的缺陷。

2.8.1.1 SPI传输模式

​ SPI 通信有4 种不同的模式,不同的从设备可能在出厂时就是配置为某种模式,这是不能改变的。但我们的通信双方必须是工作在同一模式下,所以我们可以对我们的主设备的SPI 模式进行配置,通过CPOL(时钟极性)和CPHA(时钟相位)来控制我们主设备的通信模式。

​ CPOL(时钟的极性):规定SPI 总线空闲时,时钟是高电平还是低电平。 ​ CPHA(时钟的相位):规定SPI 设备是在上升沿还是下降沿触发采样数据。

spi_four_modes

模式0:CPOL=0,CPHA=0。SCLK 串行时钟线空闲是低电平。数据在SCLK 的上升沿被采样,在SCLK 下降沿切换。

模式1:CPOL=0,CPHA=1。SCLK 串行时钟线空闲是低电平。数据在SCLK 的下降沿被采样,在SCLK 上升沿切换。

模式2:CPOL=1,CPHA=0。SCLK 串行时钟线空闲是高电平。数据在SCLK 的下降沿被采样,在SCLK 上升沿切换。

模式3:CPOL=1,CPHA=1。SCLK 串行时钟线空闲是高电平。数据在SCLK 的上升沿被采样,在SCLK 下降沿切换。

2.8.1.2 SPI数据交换

​ 想要在主从设备间通过SPI 来交换数据,从设备必须能够被主设备访问。所以主设备需要访问从设备,需要拉低从设备的NSS(片选)引脚,进行片选。

​ SPI 之间的数据传输又叫做数据交换,SPI 不同于其他协议,SPI 在通信的时候,两边各有一个移位寄存器。在进行数据传输的时候,其实是一个数据的交换,数据交换过程如下图所示。

spi_data_change

2.8.2 使能开发板SPI驱动

查看开发板底板原理图和其40pin扩展口可以知道,开发板上可以使用的有1路完整的SPI1总线管脚,其中

GPIO03_IO25 -----> ECSPI1_SCLK
GPIO03_IO26 -----> ECSPI1_SS0
GPIO03_IO27 -----> ECSPI1_MOSI
GPIO03_IO28 -----> ECSPI1_MISO

想要使能40pin扩展口的SPI1的话,需要修改开发板上的DTOverlay配置文件,添加该管脚对SPI1的支持,具体修改具体方法为修改 eMMC 启动介质的 boot 分区下的 config.txt 文件,将dtoverlay_spi1的选项修改为yes,如下所示。

root@igkboard:~# vi /run/media/mmcblk1p1/config.txt 

# Enable SPI overlay, SPI1 conflict with UART8(NB-IoT/4G module)
dtoverlay_spi1=yes

修改完成后重启系统,系统启动时将会自动加载 SPI 协议驱动。查看/dev下是否存在spi设备节点,已验证spi驱动是否加载

root@igkboard:~# ls -l /dev/spidev0.0 
crw------- 1 root root 153, 0 Oct  9 09:47 /dev/spidev0.0

2.8.3 SPI的回环测试

2.8.3.1 物理连接说明

回环测试,找到IGKBoard的SPI1的MISO和MOSI管脚,使用杜邦线或跳线帽短接即可,如下图所示

spi_loop_connect

实物连接图如下。

spi_loop_real_connect

2.8.3.2 回环测试示例

我们的IGKBoard开发板的系统中已经安装好了spidev-test,所以可以使用该命令行工具进行回环测试,下面我们开始看看该命令的帮助信息:

root@igkboard:~# spidev_test -h
spidev_test: invalid option -- 'h'
Usage: spidev_test [-DsbdlHOLC3vpNR24SI]
  -D --device   device to use (default /dev/spidev1.1)
  -s --speed    max speed (Hz)
  -d --delay    delay (usec)
  -b --bpw      bits per word
  -i --input    input data from a file (e.g. "test.bin")
  -o --output   output data to a file (e.g. "results.bin")
  -l --loop     loopback
  -H --cpha     clock phase
  -O --cpol     clock polarity
  -L --lsb      least significant bit first
  -C --cs-high  chip select active high
  -3 --3wire    SI/SO signals shared
  -v --verbose  Verbose (show tx buffer)
  -p            Send data (e.g. "1234\xde\xad")
  -N --no-cs    no chip select
  -R --ready    slave pulls low to pause
  -2 --dual     dual transfer
  -4 --quad     quad transfer
  -8 --octal    octal transfer
  -S --size     transfer size
  -I --iter     iterations

这个工具使用时候,很多选项都是由缺省值的,比如默认指定的设备是spidev1.1 ,对于回环测试我们需要知道几个必须的参数

  • -D 指定spi设备节点

  • -s 设置spi传输速率,可以测试回环测试中最大传输速度

  • -v 打开发送接收回显,用于查看详细数据发送接收情况

  • -l 直接进行回环测试

  • -p 指定发送数据

使用示例如下

#直接使用默认回环测试,发送和接收数据一致,说明测试成功
root@igkboard:~# spidev_test -D /dev/spidev0.0 -v -l       
spi mode: 0x24
bits per word: 8
max speed: 500000 Hz (500 kHz)
TX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D  |......@.........................|
RX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D  |......@.........................|
#设置传输速度,自定义传输的数据
root@igkboard:~# spidev_test -D /dev/spidev0.0 -s 10000000  -v -p 123\qwe\$%^\...
spi mode: 0x4
bits per word: 8
max speed: 10000000 Hz (10000 kHz)
TX | 31 32 33 71 77 65 24 25 5E 2E 2E 2E __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __  |123qwe$%^...|
RX | 31 32 33 71 77 65 24 25 5E 2E 2E 2E __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __  |123qwe$%^...|

2.8.4 SPI编程实现数据传输

2.8.4.1 SPI相关数据结构

应用程序空间需要从spi设备传输数据时候,每组数据元素就是 struct spi_ioc_transfer 结构体类型,该结构体定义如下:

//Linux内核源码: include/uapi/linux/spi/spidev.h 
//应用编程头文件: /usr/include/linux/spi/spi/spidev.h
struct spi_ioc_transfer {
	__u64		tx_buf;  //发送数据缓存
	__u64		rx_buf;  //接收数据缓存

	__u32		len;	//数据长度
	__u32		speed_hz; //通讯速率

	__u16		delay_usecs; //两个spi_ioc_transfer之间的延时,微秒
	__u8		bits_per_word; //数据长度
	__u8		cs_change;  //取消选中片选
	__u8		tx_nbits;  //单次数据宽度(多数据线模式)
	__u8		rx_nbits;  //单次数据宽度(多数据线模式)
	__u8		word_delay_usecs;
	__u8		pad;

	/* If the contents of 'struct spi_ioc_transfer' ever change
	 * incompatibly, then the ioctl number (currently 0) must change;
	 * ioctls with constant size fields get a bit more in the way of
	 * error checking than ones (like this) where that field varies.
	 *
	 * NOTE: struct layout is the same in 64bit and 32bit userspace.
	 */
};

在编写应用程序时还需要使用ioctl函数设置spi相关配置,其函数原型如下

 #include <sys/ioctl.h>
 int ioctl(int fd, unsigned long request, ...);

其中对于SPI设备request的值常用的有以下几种

SPI_IOC_RD_MODE

设置读取SPI模式

SPI_IOC_WR_MODE

设置写入SPI模式

SPI_IOC_RD_LSB_FIRST

设置SPI读取数据模式(LSB先行返回1)

SPI_IOC_WR_LSB_FIRST

设置SPI写入数据模式。(0:MSB,非0:LSB)

SPI_IOC_RD_BITS_PER_WORD

设置SPI读取设备的字长

SPI_IOC_WR_BITS_PER_WORD

设置SPI写入设备的字长

SPI_IOC_RD_MAX_SPEED_HZ

设置读取SPI设备的最大通信频率。

SPI_IOC_WR_MAX_SPEED_HZ

设置写入SPI设备的最大通信速率

SPI_IOC_MESSAGE(N)

一次进行双向/多次读写操作

2.8.4.2 编写测试程序

使用上述系统调用和相关命令,设计一个自定义速率和数据的spi回环测试测试程序:

guowenxue@ubuntu20:~/igkboard/apps$ vim spi_loop_test.c
/*********************************************************************************
 *      Copyright:  (C) 2021 Guo Wenxue<Email:guowenxue@gmail.com QQ:281143292>
 *                  All rights reserved.
 *
 *       Filename:  spi_loop_test.c
 *    Description:  This file used to test LCD 
 *                 
 *        Version:  1.0.0(10/10/2022~)
 *         Author:  Guo Wenxue <guowenxue@gmail.com>
 *      ChangeLog:  1, Release initial version on "10/10/2022 17:46:18 PM"
 *                 
 ********************************************************************************/
 #include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <getopt.h>
#include <libgen.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>

#define PROG_VERSION        "1.0.0"

typedef struct spi_ctx_s
{
    int         fd;
    char        dev[64];
    uint8_t     bits;
    uint16_t    delay;
    uint32_t    mode;
    uint32_t    speed;
} spi_ctx_t;

//spi初始化
static int spi_init(spi_ctx_t *spi_ctx);
//spi发送数据
static int transfer(spi_ctx_t *spi_ctx, uint8_t const *tx, uint8_t const *rx, size_t len);

static void program_usage(char *progname)
{
    printf("Usage: %s [OPTION]...\n", progname);
    printf(" %s is a program to test IGKBoard loop spi\n", progname);

    printf("\nMandatory arguments to long options are mandatory for short options too:\n");
    printf(" -d[device  ]  Specify SPI device, such as: /dev/spidev0.0\n");
    printf(" -s[speed   ]  max speed (Hz), such as: -s 500000\n");
    printf(" -p[print   ]  Send data (such as: -p 1234/xde/xad)\n");

    printf("\n%s version %s\n", progname, PROG_VERSION);
    return;
}

int main(int argc,char * argv[])
{
    int                ret;
    spi_ctx_t          spi_ctx;
    char               *spi_dev = "/dev/spidev0.0";//默认设备
    uint32_t           spi_speed = 500000;  //默认速率500K
    char               *input_tx = "Hello LingYun"; //默认发送
    uint8_t            rx_buffer[100];  //接收缓存
    int                opt;
    char               *progname=NULL;

    struct option long_options[] = {
        {"device", required_argument, NULL, 'd'},
        {"speed", required_argument, NULL, 's'},
        {"print", required_argument, NULL, 'p'},
        {"help", no_argument, NULL, 'h'},
        {NULL, 0, NULL, 0}
    };

    progname = (char *)basename(argv[0]);

    while((opt = getopt_long(argc, argv, "d:s:p:h", long_options, NULL)) != -1)
    {
        switch (opt)
        {
        case 'd':
            spi_dev = optarg;
            break;
        
        case 's':
            spi_speed = atoi(optarg);
            break;
        
        case 'p':
            input_tx = optarg;
            break;

        case 'h':
            program_usage(progname);
            return 0;

        default:
            break;
        }
    }
    if( 0 == spi_speed || !input_tx)
    {
        program_usage(progname);
        return -1;
    }

    memset(&spi_ctx, 0, sizeof(spi_ctx));
    strncpy(spi_ctx.dev, spi_dev, sizeof(spi_ctx.dev));

    spi_ctx.bits = 8;  //设置字长 8bit
    spi_ctx.delay = 100; //设置时延100us
    spi_ctx.mode = SPI_MODE_2; //设置spi模式
    spi_ctx.speed = spi_speed; //设置速率

    //spi设备初始化
    if( spi_init(&spi_ctx) < 0 )
    {
        printf("spi_init error\n");
        return -1;
    }
    printf("spi [dev %s] [fd = %d] init successfully\n", spi_ctx.dev, spi_ctx.fd);

    //spi发送接收函数
    if ( transfer(&spi_ctx, input_tx, rx_buffer, strlen(input_tx)) < 0 )
    {
        printf("spi transfer error\n");
        return -2;
    }

    /*打印 tx_buffer 和 rx_buffer*/
    printf("tx_buffer: | %s |\n", input_tx);
    printf("rx_buffer: | %s |\n", rx_buffer);

    return 0;
}

int transfer(spi_ctx_t *spi_ctx, uint8_t const *tx, uint8_t const *rx, size_t len)
{
    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long )tx,
        .rx_buf = (unsigned long )rx,
        .len = len,
        .delay_usecs = spi_ctx->delay,
        .speed_hz = spi_ctx->speed,
        .bits_per_word = spi_ctx->bits,
    };

    //发送并接收一组数据
    if( ioctl(spi_ctx->fd, SPI_IOC_MESSAGE(1), &tr) < 0)
    {
        printf("ERROR: SPI transfer failure: %s\n ", strerror(errno));
        return -1;
    }

    return 0;
}

/* spi 初始化函数 */
int spi_init(spi_ctx_t *spi_ctx)
{
    int ret;
    spi_ctx->fd = open(spi_ctx->dev, O_RDWR);
    if(spi_ctx->fd < 0)
    {
        printf("open %s error\n", spi_ctx->dev);
        return -1;
    }

    //设置SPI 接收和发送的工作模式
    ret = ioctl(spi_ctx->fd, SPI_IOC_RD_MODE, &spi_ctx->mode);
    if( ret < 0 )
    {
        printf("ERROR: SPI set SPI_IOC_RD_MODE [0x%x] failure: %s\n ", spi_ctx->mode, strerror(errno));
        goto fd_close;
    }

    ret = ioctl(spi_ctx->fd, SPI_IOC_WR_MODE, &spi_ctx->mode);
    if( ret < 0 )
    {
        printf("ERROR: SPI set SPI_IOC_WR_MODE [0x%x] failure: %s\n ", spi_ctx->mode, strerror(errno));
        goto fd_close;
    }

    //设置SPI通信接收和发送的字长
    ret = ioctl(spi_ctx->fd, SPI_IOC_RD_BITS_PER_WORD, &spi_ctx->bits);
    if( ret < 0 )
    {
        printf("ERROR: SPI set SPI_IOC_RD_BITS_PER_WORD [%d] failure: %s\n ", spi_ctx->bits, strerror(errno));
        goto fd_close;
    }
    ret = ioctl(spi_ctx->fd, SPI_IOC_WR_BITS_PER_WORD, &spi_ctx->bits);
    if( ret < 0 )
    {
        printf("ERROR: SPI set SPI_IOC_WR_BITS_PER_WORD [%d] failure: %s\n ", spi_ctx->bits, strerror(errno));
        goto fd_close;
    }

    //设置SPI最高工作频率
    ret = ioctl(spi_ctx->fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_ctx->speed);
    if( ret == -1)
    {
        printf("ERROR: SPI set SPI_IOC_WR_MAX_SPEED_HZ [%d] failure: %s\n ", spi_ctx->speed, strerror(errno));
        goto fd_close;
    }
    ret = ioctl(spi_ctx->fd, SPI_IOC_RD_MAX_SPEED_HZ, &spi_ctx->speed);
    if( ret == -1)
    {
        printf("ERROR: SPI set SPI_IOC_RD_MAX_SPEED_HZ [%d] failure: %s\n ", spi_ctx->speed, strerror(errno));
        goto fd_close;
    }

    printf("spi mode: 0x%x\n", spi_ctx->mode);
    printf("bits per word: %d\n", spi_ctx->bits);
    printf("max speed: %d Hz (%d KHz)\n", spi_ctx->speed, spi_ctx->speed / 1000);

   return spi_ctx->fd;

fd_close:

   close(spi_ctx->fd);
   return -1;
}

编写Makefile如下

guowenxue@ubuntu20:~/igkboard/apps$ vim Makefile
CC=/opt/buildroot/cortexA7/bin/arm-linux-gcc
APP_NAME=spi_loop_test

all:clean
	@${CC} ${APP_NAME}.c -o ${APP_NAME}

clean:
	@rm -f ${APP_NAME}

install:
	cp ${APP_NAME} ~/linux/tftp/

2.8.4.3 交叉编译测试运行

在ubuntu下的相关源码路径下执行make命令将会编译源码生成ARM开发板上的可执行文件。

guowenxue@ubuntu20:~/igkboard/apps$ make
guowenxue@ubuntu20:~/igkboard/apps$ ls
Makefile  spi_loop_test  spi_loop_test.c

现在我们在开发板上通过 tftp 命令 或其它方式将编译生成的测试程序下载到开发板上。

root@igkboard:~# tftp -gr spi_loop_test 192.168.2.2

接下来,我们给该程序加上执行权限并运行。

root@igkboard:~# chmod +x spi_loop_test
root@igkboard:~# ./spi_loop_test -h
Usage: spi_loop_test [OPTION]...
 spi_loop_test is a program to test IGKBoard loop spi

Mandatory arguments to long options are mandatory for short options too:
 -d[device  ]  Specify SPI device, such as: /dev/spidev0.0
 -s[speed   ]  max speed (Hz), such as: -s 500000
 -p[print   ]  Send data (such as: -p 1234/xde/xad)

spi_loop_test version 1.0.0
root@igkboard:~# ./spi_loop_test  
spi mode: 0x4
bits per word: 8
max speed: 500000 Hz (500 KHz)
spi [dev /dev/spidev0.0] [fd = 3] init successfully
tx_buffer: | Hello LingYun |
rx_buffer: | Hello LingYun |
root@igkboard:~# ./spi_loop_test -D /dev/spidev0.0 -s 300000 -p 123/qwe/456/@#$
./spi_loop_test: invalid option -- 'D'
spi mode: 0x4
bits per word: 8
max speed: 300000 Hz (300 KHz)
spi [dev /dev/spidev0.0] [fd = 3] init successfully
tx_buffer: | 123/qwe/456/@#$ |
rx_buffer: | 123/qwe/456/@#$ |

可以使用 -h选项查看用法,不带任何选项参数,程序使用默认参数和数据进行发送,默认参数设备是**/dev/spidev0.0**,速率是500KHz,发送的数据是Hello LingYun,通过检查rx和rx的buffer相同,证明数据回环发送成功。