SPI相关通信--看懂这篇就够, test later

2024-01-03 14:01:38

?对于生活中大家普遍常用到的一些基本通信总线协议,也是成为大家关注的焦点,那么今天小易就同大家在知识海洋中进行一次比较深入的探讨,同时也希望小易的这篇文章能给大家带来一定的帮助。

?SPI通信原理:

SPI的通信原理很简单,一般主从方式工作,这种模式通常有一个主设备和一个或多个从设备,通常采用的是4根线,它们是MISO(数据输入,针对主机来说)、MOSI(数据输出,针对主机来说)、SCLK(时钟,主机产生)、CS/SS(片选,一般由主机发送或者直接使能,通常为低电平有效)。

本次讲解标准的 4 线 SPI,四根线如下:

①、CS/SS,Slave Select/Chip Select,片选信号线,用于选择需要进行通信的从设备。

②、SCK,Serial Clock,串行时钟,和 I2C 的 SCL 一样,为 SPI 通信提供时钟。

③、MOSI/SDO,Master Out Slave In/Serial Data Output,主输出从输入。

④、MISO/SDI,Master In Slave Out/Serial Data Input,主输入从输出。

那么问题来了,对于一主多从的SPI能支持多少从设备,我认为一般是6个,如果6个以上其占用资源较大、传输速率会下降。

对于SPI控制时序图如下

SPI协议规定数据采样是在SCK的上升沿或下降沿时刻(由SPI模式决定,下面会说到),观察上图,在SCK的边沿处(上升沿或下降沿),主机会在MISO数据线上采样(接收来从机的数据),从机会在MOSI数据线上采样(接收来自主机的数据),所以每个时钟周期中会有1位的数据交换。

SPI传输机制:

主机和从机都有一个串行移位寄存器,主机通过向它的 SPI 串行寄存器写入一个字节来发起一次传输。寄存器通过 MOSI 信号线将字节传送给从机,从机也将自己的移位寄存器中的内容通过 MISO 信号线返回给主机。这样,两个移位寄存器中的内容就被交换。

外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。

SPI传输模式:

SPI总线传输一共有4种模式,这4种模式分别由时钟极性(CPOL)和时钟相位(CPHA)来定义。

CPOL:规定了SCK时钟信号空闲状态的电平

CPHA:规定了数据是在SCK时钟的上升沿还是下降沿被采样

-----------------------------------------------------------------------------------------------------

模式0:CPOL=0,CPHA =0 ?SCK空闲为低电平,数据在SCK的上升沿被采样(提取数据)

模式1:CPOL=0,CPHA =1 ?SCK空闲为低电平,数据在SCK的下降沿被采样(提取数据)

模式2:CPOL=1,CPHA =0 ?SCK空闲为高电平,数据在SCK的下降沿被采样(提取数据)

模式3:CPOL=1,CPHA =1 ?SCK空闲为高电平,数据在SCK的上升沿被采样(提取数据)

以模式0为例:SCK空闲为低电平,数据在SCK的上升沿被采样(提取数据),在SCK的下降沿切换数据线的数据。

?在时钟的第1个上升沿(游标1处)(采样点)

  MOSI上数据为1,则在此边沿从机采样(提取)数据为1,采样点在MOSI数据线的中间。

  MISO上数据为0,则在此边沿主机采样(提取)数据为0,采样点在MISO数据线的中间。

?在时钟的第1个下降沿(游标2处)(切换点)

  MOSI上数据由1切换为0,,数据在时钟下降沿时切换数据。

  MISO上数据由0切换为1,,数据在时钟下降沿时切换数据。

?在时钟的第2~8个上升沿(采样点),主机在MISO上采样数据,从机在MOSI上采样数据。

?在时钟的第2~8个下降沿(切换点),主机在MISO上切换数据,从机在MOSI上切换数据。

主机向从机写数据过程:

半双工通信图

主机先发送 8 bits,第一个 bit 为 0 代表这次主机是想写数据到从机,AD6~AD0 表示要写的寄存器地址。然后,主机就会一直写下去。在这期间 SDO 一直没用,一直是高阻态,算是一直读到1。

全双工通信图

主机向从机读数据过程:

这种情况下,主机先发送 8 bits,第一位为 1 代表这次是读,然后 AD6 ~ AD0 是想要读的寄存器地址,然后 SDO 开始返回数据。

主机模组下示波器测量波形图:

主机时钟和发送信号

从机示波器测量波形图:

从机时钟clok信号

从机ss片选信号

从机mosi输入数据信号

有了这些知识储备,那么不要停,接着就开始着手测试程序的编写工作,为此小易特意上网借鉴了一些测试程序的用例,但是都不是比较通用型,由此小易特意写了一个自己的,在这里分享出来,希望对大家有所帮助。

#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include <string.h>
#include <linux/gpio.h>
#include <time.h>
#include <poll.h>
 
static const char *devices = "/dev/spidev0.0"; // this is a dev node to check spi0
// SPI_MODE_2;//SPI_MODE_0;//SPI_MODE_1;/*SPI transfer use double work set POL=0 PHA=0*/
static uint8_t mode = SPI_MODE_1;
 
static uint8_t bits = 8; // 8 bits read or write
 
uint32_t speed = 8 * 1000 * 1000; // 8M rate
 
static uint32_t delay = 10000; // use delay to delay more than transfer;
 
int fd; // this is file descriptor

首先这里定义了一些模式和spi频率以及相关操控文件设备节点,那么最关键的部分来了,spi具体是如何全双工或半双工通信的呢?大家不要着急,小易马上解答。

/********************************************************************
 *  @Function:  SPI_transfer()
 *  @Effect:    this is a transfer about spi_sync
 *  @param:     None
********************************************************************/
const int bufferlen = 3096; //给予应答数据的空脉冲数据时钟
uint8_t txbuffer[bufferlen] = {
    0x00,
},
        rxbuffer[bufferlen] = {
            0x00,
};
int spi_transfer(uint8_t *txbuffercmd, uint8_t *rxbuffercmd, int buffersize)
{
  int ret = 0;
  // set a struct tr handle transfer
  struct spi_ioc_transfer tr[2] = {
      [0] = {
          .tx_buf = (unsigned long)txbuffercmd,
          .rx_buf = (unsigned long)rxbuffercmd,
          .len = buffersize,
          .delay_usecs = delay,
          .speed_hz = speed,
          .bits_per_word = bits,
      },
      [1] = {
          .tx_buf = (unsigned long)txbuffer,
          .rx_buf = (unsigned long)rxbuffer,
          .len = bufferlen,
          .delay_usecs = delay,
          .speed_hz = speed,
          .bits_per_word = bits,
      },
  };
  // this send frame for data to get this mcu data length about frame
  ret = ioctl(fd, SPI_IOC_MESSAGE(2), &tr);
  if (ret < 2)
    return -1;
  return ret;
}

这里设计就比较通用了,其他人员想跟换逻辑只需要跟换业务表就行,实现到了程序与业务分离的思想。所谓的全双工通信,其实就是spi的miso和mosi同时作用的效果,若是半双工通信就是我上面所说的形式。linux对spi通信的封装比较完善,用户在应用层面只需要通过ioctl、read、write函数去读写spi设备节点即可实现通信过程。下面就是主函数的效果逻辑操作,也是通过程序与业务分离思想进行的相关业务表设计,此处代码仅供参考,具体逻辑大家自己实现。

int main()
{
  int ret, choose;
  int num = 0;
  clock_t start, end;
  const int buffersize = 30;
  uint8_t rxbuffercmd[buffersize] = {
      0x00,
  },
          txbuffercmd[3][buffersize] = {
              {
                  0x45,
                  0x46,
                  0x47,
                  0x48,
                  0x05,
                  0xfe,
                  0x00,
                  0x00,
                  0xfb,
              },
              {
                  0x45,
                  0x46,
                  0x47,
                  0x48,
                  0x08,
                  0xfe,
                  0x00,
                  0x13,
                  0x00,
                  0x00,
                  0x08,
                  0x00,
                  0x00,
                  0x0e,
                  0x10,
                  0x00,
                  0x00,
                  0x00,
                  0x00,
                  0x00,
                  0x00,
                  0x00,
                  0x00,
                  0x00,
                  0x00,
                  0x00,
                  0x00,
                  0xf3,
              },
              {
                  0x45,
                  0x46,
                  0x47,
                  0x48,
                  0x01,
                  0xfe,
                  0x00,
                  0x00,
                  0xff,
              },
 
          };
  int len[3] = {
      9,
      11,
      28,
  };
  spi_init();
  printf("please input your choose:(0/1):");
  scanf("%d", &choose);
  printf("your input %d\n", choose);
  while (1)
  {
    if (choose == 0)
    {
      // polltimer(10);//等待10ms
      if (num == 0)
      {
        start = clock(); // clock()函数计算出来的是硬件滴答的数目,不是毫秒 在这个版本中硬件每1000000个滴答是一秒
        printf("start time = %.f us\n", (double)start);
        ret = spi_transfer(txbuffercmd[0], rxbuffercmd, len[0]);
        end = clock();
        printf("end time = %.f us\n", (double)end);
        printf("time=%f ms\n", (double)(end - start) / (CLOCKS_PER_SEC / 1000));
        hex_dump(txbuffercmd[0], len[0], len[0], "txcmd");
        hex_dump(rxbuffercmd, len[0], len[0], "rxcmd");
        hex_dump(txbuffer, bufferlen, bufferlen, "tx");
        hex_dump(rxbuffer, bufferlen, bufferlen, "rx");
        printf("ret = %d\n", ret);
        num = 1;
        if (ret < 0)
          return -1;
      }
      if (num == 1)
      {
        start = clock(); // clock()函数计算出来的是硬件滴答的数目,不是毫秒 在这个版本中硬件每1000000个滴答是一秒
        printf("start time = %.f us\n", (double)start);
        ret = spi_transfer(txbuffercmd[2], rxbuffercmd, len[0]);
        end = clock();
        printf("end time = %.f us\n", (double)end);
        printf("time=%f ms\n", (double)(end - start) / (CLOCKS_PER_SEC / 1000));
        hex_dump(txbuffercmd[2], len[0], len[0], "txcmd");
        hex_dump(rxbuffercmd, len[0], len[0], "rxcmd");
        hex_dump(txbuffer, bufferlen, bufferlen, "tx");
        hex_dump(rxbuffer, bufferlen, bufferlen, "rx");
        printf("ret = %d\n", ret);
        num = 2;
        if (ret < 0)
          return -1;
      }
      if (num == 2)
      {
        start = clock(); // clock()函数计算出来的是硬件滴答的数目,不是毫秒 在这个版本中硬件每1000000个滴答是一秒
        printf("start time = %.f us\n", (double)start);
        ret = spi_transfer(txbuffercmd[1], rxbuffercmd, len[2]);
        end = clock();
        printf("end time = %.f us\n", (double)end);
        printf("time=%f ms\n", (double)(end - start) / (CLOCKS_PER_SEC / 1000));
        hex_dump(txbuffercmd[1], len[2], len[2], "txcmd");
        hex_dump(rxbuffercmd, len[2], len[2], "rxcmd");
        hex_dump(txbuffer, bufferlen, bufferlen, "tx");
        hex_dump(rxbuffer, bufferlen, bufferlen, "rx");
        printf("ret = %d\n", ret);
        num = 0;
        if (ret < 0)
          return -1;
      }
    }
    if (choose == 1)
    {
      start = clock();
      ret = spi_write(txbuffercmd[0], 9);
      polltimer(10); //等待10ms
      if (ret > 0)
        spi_read(rxbuffer, bufferlen);
      end = clock();
      printf("time=%f ms\n", (double)(end - start) / 1000);
 
      if (ret < 0)
        return -1;
    }
  }
  spi_close();
  return ret;
}

那么小易的这篇文章讲到这就差不多啦,大家还有其他问题可以随时加我博客提问评论,希望写这篇博文能对大家有所帮助吧,小易要求不高,希望大家觉得有用的别忘记一键三联给小易关注点赞是小易不断进行创作的动力!!!

文章来源:https://blog.csdn.net/hfut31415926/article/details/135230318
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。