Linux SPI 应用编程

设备文件

/dev/spidevx.y
x是SPI总线号,即一组SCLK、MOSI、MISO
y是SPI设备号,同一条总线上用不同的片选信号区分:CE0、CE1等
对于树莓派,启用SPI功能后,有一条总线,两个设备:
/dev/spidev0.0
/dev/spidev0.1
树莓派上还可以通过dtoverlay使能第二条SPI总线,支持三个设备:
在config.txt中加入
dtoverlay=spi1-3cs
树莓派SPI相关PIN定义参考https://pinout.xyz/pinout/spi#

用户空间设备操作ioctl

相关头文件

	#include <fcntl.h>
	#include <unistd.h>
	#include <sys/ioctl.h>
	#include <linux/types.h>
	#include <linux/spi/spidev.h>

SPI设置:

  1. 时钟电平(CPOL)和采用阶段(CPHA)
  2. 时钟频率
  3. 片选电平
  4. 是否有片选信号
  5. 数据位数(通常为8bit)
  6. 数据位传输顺序(MSB或LSB)
  7. 3线或4线
功能cmdarg参数说明
读SPI工作模式SPI_IOC_RD_MODEu8*包括1,3,4,6,7
读SPI工作模式SPI_IOC_RD_MODE32u32*包括1,3,4,6,7
设置SPI工作模式SPI_IOC_WR_MODEu8*包括1,3,4,6,7
设置SPI工作模式SPI_IOC_WR_MODE32u32*包括1,3,4,6,7
读是否LSBSPI_IOC_RD_LSB_FIRSTu8*返回0或1
设置是否LSBSPI_IOC_WR_LSB_FIRSTu8*0或1
读字位数SPI_IOC_RD_BITS_PER_WORDu8*
设置字位数SPI_IOC_WR_BITS_PER_WORDu8*
读时钟频率SPI_IOC_RD_MAX_SPEED_HZu32*
设置时钟频率SPI_IOC_WR_MAX_SPEED_HZu32*

如果CPOL=0,串行同步时钟的空闲状态为低电平;
如果CPOL=1,串行同步时钟的空闲状态为高电平;
时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。
如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;
如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样;

工作模式由位组合而成:
SPI_CPHA
SPI_CPOL
SPI_CS_HIGH
SPI_LSB_FIRST
SPI_3WIRE
SPI_NO_CS
更多内容参考spidev.h

数据传输

可以调用read/write进行读写,也可以使用ioctl同时写并且读。
无论使用哪种方式,一次读写的最大字节数不能超过spi底层bufsiz,bufsiz是SPI驱动内核模块参数(TODO 如何修改此参数???)
使用ioctl方式可以一次读写多块数据,多块数据通过spi_ioc_transfer结构数组传递

ioctl参数说明:

  • cmd SPI_IOC_MESSAGE(N) 其中N是spi_ioc_transfer结构数组元素个数
  • arg 是spi_ioc_transfer结构数组地址

注意:每个数据发送和接收的数据长度是相同的,可以通过将tx_buf置NULL只读或将rx_buf置NULL只写
delay_usecs,speed_hz,bits_per_word等参数可以设置为0,使用全局或默认设置

由于有发送长度和接收长度一致的限制,并且通常先发送命令再接收应答而不会同时进行,所以一般使用read/write就可以了。确实需要同时收发的可以使用两个spi_ioc_transfer结构,一发一收。

应用举例

以树莓派操作PN532 NFC模块为例:
PN532采用微雪的PN532 NFC HAT模块,这个模块有个问题是40PIN的PI接口里片选信号没有接到PI的CE0或CE1,而是接到了GPIO4,所以,我们使用模块上的树莓派插针通过杜邦线与树莓派进行连接,将D4(GPIO4)接到树莓派的CE0,其他插针依次对应连接即可。
注意:电源需要使用模块上的树莓派5V插针连接,如需使用3.3V,可以将电源连接到模块控制接口的3.3V和GND。

使用HAT方式连接:
模块在SPI通信模式下可以使用控制接口的RX作为片选输入,将模块上的控制接口RX与模块上的树莓派插针CE0或CE1短接,将拨码开关的NSS设置为OFF,断开GPIO4与PN532的连接,这样GPIO4还可以做其他用途。

从树莓派的PIN脚定义上看,树莓派并没有接收IRQ,也没有发送H_REQ

PN532的通信参数:
PN532 SPI接口* The PN532 is configured as slave and is able to communicate with a host controller with a clock (SCK) up to 5MHz.

以下程序读取NFC tag的UID:

使用libnfc

// To compile this simple example:
// $ gcc -o quick_start_example1 quick_start_example1.c -lnfc
#include <stdlib.h>
#include <nfc/nfc.h>
static void
print_hex(const uint8_t *pbtData, const size_t szBytes)
{
  size_t  szPos;
  for (szPos = 0; szPos < szBytes; szPos++) {
    printf("%02x  ", pbtData[szPos]);
  }
  printf("\n");
}
int
main(int argc, const char *argv[])
{
  nfc_device *pnd;
  nfc_target nt;
  // Allocate only a pointer to nfc_context
  nfc_context *context;
  // Initialize libnfc and set the nfc_context
  nfc_init(&context);
  if (context == NULL) {
    printf("Unable to init libnfc (malloc)\n");
    exit(EXIT_FAILURE);
  }
  // Display libnfc version
  const char *acLibnfcVersion = nfc_version();
  (void)argc;
  printf("%s uses libnfc %s\n", argv[0], acLibnfcVersion);
  // Open, using the first available NFC device which can be in order of selection:
  //   - default device specified using environment variable or
  //   - first specified device in libnfc.conf (/etc/nfc) or
  //   - first specified device in device-configuration directory (/etc/nfc/devices.d) or
  //   - first auto-detected (if feature is not disabled in libnfc.conf) device
  pnd = nfc_open(context, NULL);
  if (pnd == NULL) {
    printf("ERROR: %s\n", "Unable to open NFC device.");
    exit(EXIT_FAILURE);
  }
  // Set opened NFC device to initiator mode
  if (nfc_initiator_init(pnd) < 0) {
    nfc_perror(pnd, "nfc_initiator_init");
    exit(EXIT_FAILURE);
  }
  printf("NFC reader: %s opened\n", nfc_device_get_name(pnd));
  // Poll for a ISO14443A (MIFARE) tag
  const nfc_modulation nmMifare = {
    .nmt = NMT_ISO14443A,
    .nbr = NBR_106,
  };
  if (nfc_initiator_select_passive_target(pnd, nmMifare, NULL, 0, &nt) > 0) {
    printf("The following (NFC) ISO14443A tag was found:\n");
    printf("    ATQA (SENS_RES): ");
    print_hex(nt.nti.nai.abtAtqa, 2);
    printf("       UID (NFCID%c): ", (nt.nti.nai.abtUid[0] == 0x08 ? '3' : '1'));
    print_hex(nt.nti.nai.abtUid, nt.nti.nai.szUidLen);
    printf("      SAK (SEL_RES): ");
    print_hex(&nt.nti.nai.btSak, 1);
    if (nt.nti.nai.szAtsLen) {
      printf("          ATS (ATR): ");
      print_hex(nt.nti.nai.abtAts, nt.nti.nai.szAtsLen);
    }
  }
  // Close NFC device
  nfc_close(pnd);
  // Release the context
  nfc_exit(context);
  exit(EXIT_SUCCESS);
}

使用ioctl/read/write
pn532.h

#pragma once

#include <memory>

namespace pn532
{

const uint8_t TFI_H2P = 0xD4;
const uint8_t TFI_P2H = 0xD5;

#pragma pack(1)
struct ni_frame_header
{
        uint8_t preamble;
        uint8_t start_code[2];
        uint8_t len;
        uint8_t lcs;
        uint8_t tfi;
};

struct frame_tailer
{
        uint8_t dcs;
        uint8_t postamble;
};
#pragma pack()

const uint8_t ACK_FRAME[6] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00};
const uint8_t NACK_FRAME[6] = {0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00};
const uint8_t ERROR_FRAME[8] = {0x00, 0x00, 0xFF, 0x01, 0xFF, 0x7F, 0x81, 0x00};

typedef std::shared_ptr<uint8_t[]> byte_ptr;
typedef byte_ptr data_ptr;
typedef byte_ptr frame_ptr;

uint8_t checksum(const uint8_t * data, int data_len);
uint8_t checksum(data_ptr data, int data_len);
bool validate_checksum(const uint8_t * data, int data_len);
bool validate_checksum(data_ptr data, int data_len);
int cal_frame_len(int data_len);
frame_ptr new_frame(int frame_len);
int make_frame(uint8_t* buf, int buf_len, uint8_t tfi, const uint8_t* data, int data_len);
int make_frame(frame_ptr& frame, uint8_t tfi, data_ptr data, int data_len);

const uint8_t SPI_DW = 0x01;
const uint8_t SPI_DR = 0x03;
const uint8_t SPI_SR = 0x02;

int cal_spi_frame_len(int data_len);
int make_spi_frame(uint8_t* buf, int buflen, uint8_t spi_tfi, uint8_t tfi, const uint8_t* data, int data_len);
int make_spi_frame(frame_ptr& frame, uint8_t spi_tfi, uint8_t tfi, const uint8_t* data, int data_len);
int make_spi_frame(frame_ptr& frame, uint8_t spi_tfi, uint8_t tfi, data_ptr data, int data_len);

}

pn532.cpp

#include "pn532.h"
#include <cassert>

namespace pn532
{

uint8_t checksum(const uint8_t * data, int data_len)
{
        assert(data_len >= 0);
        uint8_t cs = 0;
        const uint8_t* p = data;
        for (int i = 0; i < data_len; ++i)
        {
                cs += *p++;
        }
        cs = (~cs) + 1;
        return cs;
}

uint8_t checksum(data_ptr data, int data_len)
{
        return checksum(data.get(), data_len);
}

bool validate_checksum(const uint8_t* data, int data_len)
{
        assert(data_len >= 0);
        uint8_t cs = 0;
        const uint8_t* p = data;
        for (int i = 0; i < data_len; ++i)
        {
                cs += *p++;
        }
        return (cs == 0);
}

bool validate_checksum(data_ptr data, int data_len)
{
        return validate_checksum(data.get(), data_len);
}

int cal_frame_len(int data_len)
{
        assert(data_len >= 0);
        return sizeof(ni_frame_header) + data_len + sizeof(frame_tailer);
}

frame_ptr new_frame(int frame_len)
{
        assert(frame_len >= 0);
        frame_ptr frame(new uint8_t[frame_len]);
        return frame;
}

int make_frame(uint8_t* buf, int buf_len, uint8_t tfi, const uint8_t* data, int data_len)
{
        assert(data_len >= 0);
        int frame_len = cal_frame_len(data_len);
        if (buf == NULL || buf_len == 0)
        {
                return frame_len;
        }
        if (buf_len < frame_len)
        {
                return -1;
        }
        uint8_t* f = buf;
        ni_frame_header* h = (ni_frame_header*)f;
        uint8_t* d = f + sizeof(ni_frame_header);
        frame_tailer* t = (frame_tailer*)(d + data_len);
        h->preamble = 0x00;
        h->start_code[0] = 0x00;
        h->start_code[1] = 0xFF;
        h->len = data_len + 1;
        h->lcs = checksum(&h->len, 1);
    h->tfi = tfi;
        uint8_t dcs = tfi;
        const uint8_t* dd = data;
        for (int i = 0; i < data_len; ++i)
        {
                *d++ = *dd;
                dcs += *dd++;
        }
        dcs = (~dcs) + 1;
        t->dcs = dcs;
        t->postamble = 0x00;
        return frame_len;
}

int make_frame(frame_ptr & frame, uint8_t tfi, data_ptr data, int data_len)
{
        assert(data_len >= 0);
        int frame_len = cal_frame_len(data_len);
        frame = new_frame(frame_len);
        return make_frame(frame.get(), frame_len, tfi, data.get(), data_len);
}

int cal_spi_frame_len(int data_len)
{
        return cal_frame_len(data_len) + 1;
}

int make_spi_frame(uint8_t* buf, int buf_len, uint8_t spi_tfi, uint8_t tfi, const uint8_t* data, int data_len)
{
        int frame_len = cal_spi_frame_len(data_len);
        if (buf == NULL || buf_len == 0)
        {
                return frame_len;
        }
        if (buf_len < frame_len)
        {
                return -1;
        }
        buf[0] = spi_tfi;
        int ret = make_frame(buf+1, buf_len-1, tfi, data, data_len);
        if (ret < 0)
        {
                return ret;
        }
        return frame_len;
}

int make_spi_frame(frame_ptr& frame, uint8_t spi_tfi, uint8_t tfi, const uint8_t* data, int data_len)
{
        assert(data_len >= 0);
        int frame_len = cal_spi_frame_len(data_len);
        frame = new_frame(frame_len);
        return make_spi_frame(frame.get(), frame_len, spi_tfi, tfi, data, data_len);
}

int make_spi_frame(frame_ptr& frame, uint8_t spi_tfi, uint8_t tfi, data_ptr data, int data_len)
{
        assert(data_len >= 0);
        int frame_len = cal_spi_frame_len(data_len);
        frame = new_frame(frame_len);
        return make_spi_frame(frame.get(), frame_len, spi_tfi, tfi, data.get(), data_len);
}

}

get_uid.cpp

#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include <string.h>
#include "pn532.h"

using namespace pn532;

#define IOCTL(fd, cmd, arg) \
        do\
        {\
                if (ioctl(fd, cmd, arg) == -1)\
                {\
                        perror(#cmd);\
                        close(fd);\
                        return -1;\
                }\
        }\
        while(0)

void reverse_byte(uint8_t & b)
{
        uint8_t res = 0;
        uint8_t ori = b;
        for (int i = 0; i < 8; ++i)
        {
                res <<= 1;
                res |= ori & 0x01;
                ori >>= 1;
        }
        b = res;
}

void reverse_bits(uint8_t * buf, int len)
{
        uint8_t* p = buf;
        for (int i = 0; i < len; ++i)
        {
                reverse_byte(*p++);
        }
}

int init_spi(const char * dev)
{
        static const uint8_t spi_mode = 0;
        static const uint8_t spi_lsb = 1;
        static const uint8_t spi_bpw = 8;
        //static const uint32_t spi_speed = 5000000;
        static const uint32_t spi_speed = 1000000;

        int fd = open(dev, O_RDWR);
        if (fd == -1)
        {
                perror("open spi device:");
                return -1;
        }

        IOCTL(fd, SPI_IOC_WR_MODE, &spi_mode);
        //IOCTL(fd, SPI_IOC_WR_LSB_FIRST, &spi_lsb);
        IOCTL(fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bpw);
        IOCTL(fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed);

        return fd;
}

int sr_spi(int spi, const uint8_t* sendbuf, int sendlen, uint8_t* recvbuf, int recvlen)
{
        int trnum = 0;
        struct spi_ioc_transfer tr[2];
        memset(tr, 0, sizeof(tr));
        frame_ptr lsbbuf;
        if (sendlen > 0)
        {
                lsbbuf = new_frame(sendlen);
                memcpy(lsbbuf.get(), sendbuf, sendlen);
                reverse_bits(lsbbuf.get(), sendlen);
                tr[trnum].tx_buf = (uint64_t)lsbbuf.get();
                tr[trnum].rx_buf = 0;
                tr[trnum].len = sendlen;
                ++trnum;
        }
        if (recvlen > 0)
        {
                tr[trnum].tx_buf = 0;
                tr[trnum].rx_buf = (uint64_t)recvbuf;
                tr[trnum].len = recvlen;
                ++trnum;
        }
        if (trnum == 0)
        {
                return -1;
        }
        if (ioctl(spi, SPI_IOC_MESSAGE(trnum), tr) != (sendlen + recvlen))
        {
                perror("spi send recv error:");
                return -1;
        }
        if (recvlen > 0)
        {
                reverse_bits(recvbuf, recvlen);
        }
        return sendlen+recvlen;
}

int send_spi(int spi, const uint8_t* sendbuf, int sendlen)
{
        return sr_spi(spi, sendbuf, sendlen, NULL, 0);
}

int recv_spi(int spi, uint8_t* recvbuf, int recvlen)
{
        return sr_spi(spi, NULL, 0, recvbuf, recvlen);
}

void print_hex(const uint8_t * buf, int len)
{
        const uint8_t * p = buf;
        for (int i = 0; i < len; ++i)
        {
                printf("%02X ", *p++);
                if ((i+1) % 16 == 0)
                {
                        printf("\n");
                }
        }
        if (len % 16 != 0)
        {
                printf("\n");
        }
}

int wait_for_ready(int spi)
{
        static const uint8_t sr[] = {pn532::SPI_SR};
        uint8_t buf[1];
        while (1)
        {
                usleep(10000);
                if (sr_spi(spi, sr, 1, buf, 1) != 2)
                {
                        printf("spi send recv error!\n");
                        return -1;
                }
                if (buf[0] == 0x01)
                        return 0;
        }
        return -1;
}

#define BUF_SIZE 264

int do_cmd(int spi, const uint8_t* cmd, int cmdlen, const char* cmdname, int answerlen)
{
        static const uint8_t dr[] = {pn532::SPI_DR};

        if (cmdname != NULL)
        {
                printf("do command: %s\n", cmdname);
        }

        if (answerlen < 0)
        {
                answerlen = BUF_SIZE;
        }

        uint8_t buf[BUF_SIZE];
        frame_ptr cmd_frame;
        int cmd_frame_len = make_spi_frame(cmd_frame, SPI_DW, TFI_H2P, cmd, cmdlen);
        print_hex(cmd_frame.get(), cmd_frame_len);
        if (send_spi(spi, cmd_frame.get(), cmd_frame_len) != cmd_frame_len)
        {
                perror("write cmd");
                return -1;
        }
        if (wait_for_ready(spi) != 0)
        {
                return -1;
        }
        printf("ready for ack!\n");
        if (sr_spi(spi, dr, 1, buf, 6) < 0)
        {
                perror("read ack error");
                return -1;
        }
        printf("read ack: ");
        print_hex(buf, 6);
        if (wait_for_ready(spi) != 0)
        {
                return -1;
        }
        printf("ready for answer!\n");
        if (sr_spi(spi, dr, 1, buf, answerlen) < 0)
        {
                perror("read answer error");
                return -1;
        }
        printf("read answer: ");
        print_hex(buf, answerlen);

        return 0;
}

int main(int argc, char *argv[])
{
        int spi = init_spi("/dev/spidev0.0");
        if (spi < 0)
        {
                fprintf(stderr, "init spi device error!\n");
                return -1;
        }

#if 0
        uint8_t gfv_data[] = {0x02};
        if (do_cmd(spi, gfv_data, sizeof(gfv_data), "get fireware version", 13) < 0)
        {
                return -1;
        }
#endif

#if 1
        uint8_t set_normal_mode_data[] = {0x14, 0x01, 0x00, 0x00};
        if (do_cmd(spi, set_normal_mode_data, sizeof(set_normal_mode_data), "set normal mode", 9) < 0)
        {
                return -1;
        }
#endif

#if 1
        uint8_t autopoll_data[] = {0x60, 0xff, 0x02, 0x00, 0x01, 0x02, 0x03, 0x04, 0x10, 0x11, 0x12, 0x20, 0x23};
        if (do_cmd(spi, autopoll_data, sizeof(autopoll_data), "auto poll", 32) < 0)
        {
                return -1;
        }
#endif

#if 0
        uint8_t lpt_data[] = {0x4A, 0x01, 0x00};
        if (do_cmd(spi, lpt_data, sizeof(lpt_data), "list passive target", 32) < 0)
        {
                return -1;
        }
#endif

        close(spi);
        return 0;
}

注意:在使用树莓派测试过程中发现以下问题:

  1. 树莓派的SPI驱动不支持设置LSB传输,只能以MSB传输,而PN532模块要求以LSB传输,所以应用程序只能在发送之前反转位序,然后发送,在接收之后反转位序,然后再处理数据;

  2. PN532用户手册中的以下消息描述不是很清楚,其中DW,SR,DR都应该是主控发给PN532的,从PN532读到的数据中不包含这些内容。在这里插入图片描述

  3. 如果以write写入SR,以read读取Status,则大概率会丢失数据导致检测不到ready状态,所以必须使用ioctl来一次完成写入SR,读入status;

  4. 如果检测到ready,但是不读取ack,程序退出了,那么下一次运行发送命令后就检测不到ready,推测PN532按错误处理丢弃了上一次的ACK和本次命令数据,所以再次运行发送命令后又可以检测到ready;因此建议在每次程序启动时复位一下PN532模块,避免之前程序退出遗留了ACK等数据导致程序逻辑错误。

参考

https://blog.csdn.net/TAlice/article/details/83868713
https://www.cnblogs.com/subo_peng/p/4848260.html
https://www.waveshare.net/wiki/PN532_NFC_HAT
https://pinout.xyz/pinout/spi#
https://github.com/nfc-tools/libnfc

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐