应用笔记 4246

怎样使用MAXQ3180微控制器的串行外设接口(SPI)

By: Ben Smith

摘要 : MAXQ3180微控制器是电表多相模拟前端。它具备现代多功能电表的所有功能。MAXQ3180通过串行外设互联(SPI™)总线将其读数传送给主机微控制器。本应用笔记介绍怎样实现这一接口,演示实例代码以帮助设计人员实现这一通信机制。

SPI简介

串行外设接口(SPI)是器件间总线协议,实现芯片间的快速、同步、全双工通信。由一个主机驱动同步时钟,选择对哪些从机寻址。每个SPI外设含有一个移位寄存器和控制电路,使被寻址的串行外设接口SPI外设能够同时发送和接收数据。

图1. SPI从机示意图
图1. SPI从机示意图

SPI通信采用了四种不同的电路:
  • SCLK:所有器件使用的同步时钟。主机驱动该时钟,从机接收时钟。注意,SCLK可以被选通,不需要在SPI操作之间进行驱动。
  • MOSI:主机出,从机入。这是主机在SPI总线上驱动所有从机的主要数据线。只有所选的从机同步来自MOSI的数据。
  • MISO:主机入,从机出。这是所选从机向主机发送时驱动的主要数据线。只有所选的从机可以驱动该电路。实际上,这是SPI总线安排中允许从机驱动的唯一电路。
  • SSEL:该信号在每一从机上都不同。当有效(通常为低电平)时,所选从机必须驱动MISO。
对于这一讨论,需要特别说明的是,SPI外设同时发送和接收。最简单的理解是主机总是发送一个字节,接收一个字节。

有些SPI外设牺牲速率以模拟半双工工作。MAXQ3180微控制器没有采用这一方式,它是真正的全双工SPI从机。

本应用笔记的其他部分介绍怎样连接并成功使用SPI总线上的MAXQ3180。

MAXQ3180通信简介

对于主机,MAXQ3180看起来象一个存储器阵列,同时含有RAM和ROM。这是因为MAXQ3180中的ROM固件读取RAM的工作参数,将结果放到RAM中。因此,配置MAXQ3180和对RAM进行块写入一样简单。

有些MAXQ3180 “存储”位置触发器件内部操作,“随时”计算电表测量结果。向这些位置写入的是“nop”。对RAM和虚拟ROM位置特殊功能和目的的讨论已经超出了本文档的范围。此处最重要的是微控制器的确只有两种SPI通信操作:读和写。

MAXQ3180中的每一次操作以主机发送两个字节开始,它含有命令(例如,读或者写)、要访问的地址、访问的字节数。如前所述,每个SPI外设对接收到的每个字节返回一个字节。因此,MAXQ3180在接收到第一个命令字节后返回0xC1,第二个命令字节后返回0xC2。该协议显示在下面的图2中。

图2. 主机向MAXQ3180读写数据
图2. 主机向MAXQ3180读写数据

如果主机读取一个或者多个字节,它必须发送空字节。记住,主机不能接收来自从机的任何信息,除非它发送某些信息:发送一个字节以得到一个字节。但是接收一条命令后,MAXQ3180要计算结果,当主机发送空字节时,它可能还没有准备好。出于这一原因,MAXQ3180总是在发送数据之前发送零或者NAK字符等多个字节(0x4E或者ASCII 'N'),随后是一个ACK字符(0x41,或者ASCII 'A')。

如果主机写入一个或者多个字节,发送命令后,它立即发送要写入的数据。MAXQ3180为每一个数据字节返回ACK (0x41)。然后,它返回NAK (0x4E),直到写周期完成,随后返回最终ACK。

注意,最终ACK之后,MAXQ3180立即准备开始下一操作;它不需要进行任何其他等待。它甚至不需要触发SSEL以开始下一操作。MAXQ3180知道第一次操作已经完成,准备进行下一操作。

不论什么原因,如果需要复位主机和MAXQ3180之间的通信(例如,如果通信是异步的),从第一个命令字节重新启动通信之前,主机只需要等待200ms。200ms延时指示MAXQ3180,主机放弃了前面的操作。

命令字节

命令字节告诉MAXQ3180:
  1. 申请的操作是READ还是WRITE
  2. 操作的长度
  3. RAM中要改动的地址(或者要读取的虚拟ROM地址)
图3. 命令字节结构
图3. 命令字节结构

第一个命令字节(图3)告诉MAXQ3180,所申请的操作是READ还是WRITE,以及操作的长度。命令字节结构如下:

Length Code Data Length
0b00 1 byte
0b01 2 bytes
0b10 4 bytes
0b11 8 bytes

命令字节1的其他部分和所有的命令字节2提供要访问的RAM字节的地址(或者一样的虚拟ROM功能)。

主机软件设计

虽然MAXQ3180含有一个硬件SPI控制器,ROM固件中的软件程序还是要处理每一消息字节。出于这一原因,连续字节之间需要有延时。在当前的MAXQ3180型号中,这一延时不得小于100µs才能实现可靠的工作。请参考图4图5

图4. 读取MAXQ3180的流程图
图4. 读取MAXQ3180的流程图

图5. 写入MAXQ3180的流程图
图5. 写入MAXQ3180的流程图

代码清单

提供代码以实现具有内置SPI主机的MAXQ2000微控制器和MAXQ3180的接口。其他微控制器用户需要提供自己的SPI原语,还可能要修改高层子程序。

在下面的清单中,dly_us子程序使程序线程停止执行几个微秒。定义SPI_TIMEOUT常数以提供比字符超时时间更长的参数。

在高层子程序中,采用ENUM按名称来选择寄存器。在其他参数中,它提供register_lookup_table阵列的索引,含有每个MAXQ3180寄存器的寄存器长度。请参考图6图7图8

unsigned char Send_SPI(unsigned char x)
{
  unsigned char y = 0;
  int z;
  int error = 0;
  SPICN = 3; /* MSTSM, SPIEN */
  z = 0; while ((SPICN_bit.STBY) && (++z < SPI_TIMEOUT));
  if (z == SPI_TIMEOUT) error = 1;
  SPICN_bit.SPIC = 0; /* Clear transfer complete flag */
  SPIB = x;
  z = 0; while ((!SPICN_bit.SPIC) && (++z < SPI_TIMEOUT));
  if (z == SPI_TIMEOUT) error = 1;
  y = SPIB;
  SPICN_bit.SPIC = 0;
  dly_us(100);
  if (error) return 0;
  return y;
}
图6. Send_SPI原代码

long Read_AFE(enum METER_REGISTER_RECORD reg, uint16 reg_addr)
{
  extern unsigned char record[8];
  unsigned long x = 0;
  unsigned char i, regadd, command_code = 0;
  for(i=0; i<8; i++) record[i] = 0;
  switch(register_lookup_table[reg].register_length)
  {
  case 2: command_code |= 0x10; break;
  case 4: command_code |= 0x20; break;
  case 8: command_code |= 0x30; break;
  }
  command_code |= reg_addr >> 8;
  regadd = reg_addr & 0xff;

  /* Disable SPI to reset it */
  SPICN_bit.SPIEN = 0;
  for(x=0; x<300; x++);
  SPICN_bit.SPIEN = 1;

  SPI_SELECT_0;
  i = 0;
  while((Send_SPI(command_code)!= 0xC1)&&(++i < SPI_COMMAND_RETRIES))
    spi_comm_timeout();

  x = 0xffffffff;
  if (i == SPI_COMMAND_RETRIES) goto spierror;
  Send_SPI(regadd);
  i = 0; while((Send_SPI(0) != 'A') && (++i < SPI_RETRIES));
  if (i == SPI_RETRIES) goto spierror;
  x = 0;
  for(i=0; i<register_lookup_table[reg].register_length; i++)
  {
    record[i] = Send_SPI(0);
    x |= ((uint32)record[i]) << (i * 8);
  }

spierror:
  SPI_DESELECT_0;
  return (int32)x;
}
图7. ReadAFE (SPI_Read)子程序代码

void Write_AFE(enum METER_REGISTER_RECORD reg, uint16 reg_addr, uint32 data)
{
  uint8 i, regadd, command_code = 0x80;
  int x;
  switch(register_lookup_table[reg].register_length)
  {
  case 2: command_code |= 0x10; break;
  case 4: command_code |= 0x20; break;
  case 8: command_code |= 0x30; break;
  }
  command_code |= reg_addr >> 8;
  regadd = reg_addr & 0xff;

  /* Disable SPI hardware to reset it */
  SPICN_bit.SPIEN = 0;
  for(x=0; x<300; x++);
  SPICN_bit.SPIEN = 1;

  SPI_SELECT_0;
  i = 0;
  while((Send_SPI(command_code)!=0xC1)&&(++i < SPI_COMMAND_RETRIES))
    spi_comm_timeout();
  if (i == SPI_COMMAND_RETRIES) goto spierror;
  Send_SPI(regadd);
  for(i=0; i<register_lookup_table[reg].register_length; i++)
    Send_SPI((data >> (i * 8)) & 0xff);
  i = 0; while((Send_SPI(0) != 'A') && (++i < SPI_RETRIES));

spierror:
  SPI_DESELECT_0;
}
图8. Write_AFE (SPI_Write)子程序代码