应用笔记 3637

为任何系统增加USB


摘要 : MAX3420E利用通用的微控制器,简化了USB外设的设计。本文在介绍MAX3420E的基础上,重点讨论器件的SPI接口。说明器件如何与硬件SPI口或位仿真的通用I/O口进行连接,并给出了利用MAXQ2000微控制器进行USB设计的C程序范例。

引言

有关通用串行总线(USB)的文章通常从USB是个人电脑的一个新连接标准开始讲起。谢天谢地现在不再需要如此做,因此本引言可以简短地写为:如果你有一个嵌入式系统并且想连到PC,主流的连接通道是USB。

本文介绍了一款Maxim Integrated最新推出的芯片,MAX3420E,它可以很容易地把USB加入到任何系统中。本文主要着重于SPI接口,提供了实现通用SPI的C例程。最后给出了一个简单的USB HID (人机接口设备)—基于Windows的应急按钮程序。

为任何系统增加USB

微控制器(µC)的选择通常基于它所集成的外设。许多处理器集成了USB功能,但是大多数处理器,特别是一些低价位的处理器不含USB。有时,您可能选用了一个I/O和外设都很完美的微控制器,但它却缺少USB。您是否希望只是添加USB功能而继续使用当前的微控制器呢?

利用Maxim的芯片MAX3420E,可以为任何处理器添加USB功能。MAX3420E集成了USB全速收发器、智能USB串行接口引擎(SIE)和一个可工作到26MHz时钟的从SPI接口。MAX3420E的使用如同一个具有单个控制端点、两个双重缓冲的64字节数据端点和一个64字节的中断端点的全速USB外设。

总线供电

图1. USB总线供电
图1. USB总线供电

图1是一个普通的USB外设结构。USB的VBUS电源线为3.3V稳压器提供5V输入,该稳压器给微控制器和MAX3420E供电,无需外部电源。SPI接口可以是3、4或5线。表1列出了5线接口。

表1. 使用3到5线的SPI接口
Signal MAX3420E Direction Description
MOSI In SPI master out, slave in
MISO Out SPI master in, slave out
SCLK In Serial clock
SS# In Slave select
INT Out Interrupt (level or pulse)

如果应用中不需要中断(MAX3420E的中断条件可以通过读取寄存器直接检测到),可以去掉INT引脚,得到一个4线接口。如果SPI主机具有双向数据接口(MOSI/MISO在同一个双向引脚上),则可以省掉另外一条接口线,这样,没有中断、支持双向通信的SPI接口只需要3个引脚。

如果微控制器没有SPI接口怎么办呢? 没问题,可以很容易地设计一个直接触发GPIO的、固件形式的SPI主控器。USB的一个非常强的特性是自控流量能力,它自动配合SPI侧的任何速度(它利用在USB侧插入NAK握手来提示“忙,重试”)。很多USB外设,特别是与人接口的,即使与最低速的SPI接口都能应答自如。

如果图1中的微控制器非常小,只有10脚以下怎么办呢? 难道就不能把这些珍贵的I/O口用来连USB芯片了? 不,这就是为什么MAX3420E提供了四个通用输出口和四个通用输入口的原因,它们可以替代被用掉的处理器上的I/O口,事实上你的系统在接上MAX3420E后还得到了更多的口线。

大规模集成芯片

图2. 只连接大规模集成芯片的少许引脚
图2. 只连接大规模集成芯片的少许引脚

MAX3420E不仅仅只限用于小系统。图2说明了如何给一个ASIC、FPGA、DSP和其它大芯片增加USB功能。这样做的一个明显原因是大芯片没有内建的USB或内部的USB不是正好符合你的所需。另一个原因是随着工艺尺寸的缩小,这些大芯片不能兼容USB所需的3.3V “高”压,此时使用外部带低压SPI接口的USB芯片就是一个很好的方案。MAX3420E内带电平转换器,VL脚设定接口电平的范围,从1.7V到3.6V。

隔离USB

图3. 隔离USB
图3. 隔离USB

图3所示,由于SPI接口信号是单向的,还很容易进行光隔。同时还可以设定较低的速率来支持廉价的光耦。

SPI接口

SPI是一个简单的串行接口,它使用两根数据线,一根串行时钟和一个片选信号。SPI主控把SS#拉低来开始传输,然后驱动串行时钟SCLK来把数据同步输入和输出从设备。SPI主控通过把SS#拉回到高电平,以终止传输。

SPI接口有四种时钟模式,反映了两个信号CPOL (时钟极性)和CPHA (时钟相位)的两个状态。这两信号以(CPOL, CPHA)的形式来表示。可以证明一个接口利用正沿SCLK且在第一个正沿时钟到来以前MOSI数据已经准备就绪可以工作在(0,0)和(1,1)模式而无需任何改变。这一属性使MAX3420E无需额外的模式引脚设置,就可以工作在(0,0)或(1,1)模式。

图4图5显示了利用SPI模式在微控制器(MAXQ2000,随后介绍)和MAX3420E之间的数据传输。图4所示为(0,0)模式,图5所示为(1,1)模式。两者的区别是SCLK信号的无效电平不同,(0,0)是低无效,(1,1)是高无效。

图4. 工作在(0,0)模式的SPI接口
图4. 工作在(0,0)模式的SPI接口

图5. 工作在(1,1)模式的SPI接口
图5. 工作在(1,1)模式的SPI接口

MAX3420E每次传输接收的第一个字节是命令字节。命令字节包含寄存器号和方向位,第二个字节和后续字节包含数据。在图4和图5中,移入命令字时,来自MAX3420E (MISO引脚)的8位数据是USB状态位。此特性只在使用分离MISO和MOSI脚的接口中有效。

SPI代码

编写MAX3420E通用C代码的窍门是把原始的最基本的SPI操作封装到独立的模块中,然后针对不同的SPI接口的只需客户化这一模块。最基本的模块只须做三件事:
  1. 初始化SPI口
  2. 读一个字节
  3. 写一个字节
本文中的应用例子使用硬件SPI单元。对没有此单元的SPI主控器,我们先看看一些位仿真SPI接口的通用C代码。

位仿真SPI

Init SPI
初始化SPI口的函数会随处理器的不同而有很多变化。比较好的做法是先指定接口使用的I/O脚,设置它们的方向,然后设定SS = 1和SCLK = 0的初始条件(我们假定用SPI主控器的(0,0)模式)。

读寄存器,写寄存器
rreg是读取MAX3420E寄存器的C函数,这个宏把功能从不同微控制器的不同I/O结构中独立出来,使用宏使代码易读且与处理器无关。wreg是写MAX3420E寄存器的例程。

更换处理器时,只需对宏进行修改即可使用这些例程。例如:下面是用于不带硬件SPI单元的微控制器的宏。
#define SCLK_HI OUTA = PINSA | 0x02;
#define SCLK_LO OUTA = PINSA & 0xFD;
#define SS_HI   OUTA = PINSA | 0x04;
#define SS_LO   OUTA = PINSA & 0xFB;
#define MOSI(v) OUTA = (PINSA & 0x7F) | (v & 0x80);
#define MISO inval |= PINSA & 0x01;

BYTE rreg(BYTE r)     // Read a register, return its value.
{
int j;
BYTE bv,inval;
inval = 0;
SS_LO
bv = r<<3;                                        // Left-shift the reg number, WRITE=0
for (j=0; j<8; j++)          // send the register number and direction bit
        {
        MOSI(bv)                             // put out a bit
        bv <<= 1;                      // shift one bit left
        SCLK_HI
        SCLK_LO
        }
for (j=0; j<7; j++)          // get 7 bits and shift left into 'inval'
        {
        SCLK_HI
        MISO
        inval <<= 1;                      // shift in one bit
        SCLK_LO
        }
SCLK_HI                            // one more bit, but don't shift 'inval' this time
MISO
SCLK_LO
SS_HI
return inval;                     // return the byte we read in
}

void wreg(BYTE r,BYTE v) // register, value
{
int j;
BYTE bv;
SS_LO
bv = (r<<3)+2;                    // Left-shift the reg number, set the WRITE direction bit
for (j=0; j<8; j++)  // send the register number and direction bit
        {
        MOSI(bv)                                // put out a bit
        bv <<= 1;                         // shift one bit left
        SCLK_HI
        SCLK_LO
        }
for (j=0; j<8; j++)  // send the register data
        {
        MOSI(v)                                 // put out a bit
        v <<= 1;                          // shift one bit left
        SCLK_HI
        SCLK_LO
        }
SS_HI
}

硬件SPI

这一部分讨论上面提到的MAXQ2000微控制器,简单地说,MAXQ2000是低功耗、16位、高性能RISC处理器家族中的第一个。“Q”代表安静,表示这一结构能够与敏感的模拟电路很好地协调工作。MAXQ2000内建了一个SPI口,它与MAX3420E配合特别合适,下面的例子使用MAXQ2000开发板和MAX3420E搭建了一个简单、有趣的Windows小产品。

MAXQ2000硬件SPI单元提供了SCLK、MOSI和MISO,但是没有SS#。由于SS#的操作方式会变化(比如寻址一个字节与突发的字节串),最好用通用I/O脚做SS#。

MAXQ I/O单元

图6. MAXQ I/O单元
图6. MAXQ I/O单元

图6所示是一个基本的MAXQ I/O单元。I/O口以格式'port.bit'表示,'p'代表端口,'b'代表位。作为例子,我们主要讨论I/O口5,第3位(用P53)表示。

每一个I/O单元有一个触发器,本例中用一个称为PO5.3的位来写。'O'代表输出。你一直可以写这个触发器,它的输出有没有与引脚相连由方向位决定。配置输出脚时,实际应用时先写触发器再连到引脚比较好,这样它可以避免引脚上出现毛刺。

P53脚的方向由称作PD5.3的位来设定。'D'代表方向,D信号充当引脚驱动的输出使能:1 = 驱动,0 = 浮空。引脚的状态一直可以通过称作PI5.3的位读取,'I'代表输入,无论引脚是如何驱动的,被内部触发器(PD5.3 = 1)还是被外部的信号(PD5.3 = 0),PI位表示引脚状态。

这种结构的一个好处是如果引脚被配制成输入(PD5.3 = 0),触发器的输出没有被用作输出,那么它可以作为上拉电阻的开关重新利用。如果D = 0,0信号被重新定义,表示“连接一个上拉电阻”,如图6中的点状线所示。

许多I/O脚有中断功能,如图6下面的框图所示,中断模块有三个信号:
  • 一个中断标志位,中断请求有效时被置位,由CPU来复位。
  • 一个边沿选择位,决定是正信号跳变还是负信号跳变引起中断请求。
  • 对每一个能引起中断的引脚有一个中断使能位。
我们的应用例子把MAX3420E的中断输出配置成正边沿触发中断,在MAXQ2000这边,程序直接测试USB中断的中断触发器,而不是使用MAXQ2000的中断系统。程序除了检测按键的状态和响应USB请求外什么都不干,因此只需一个查询循环。

初始化SPI
MAXQ2000的I/O引脚由通用I/O和像SPI单元这样的特殊功能硬件共享。使用特殊功能硬件时,先配置硬件块,然后把它连到I/O脚上。程序清单中的SPI_Init()过程设置了引脚方向,配置了SPI接口,最后使能它。
void SPI_Init(void)
{
// MAXQ2000 SPI port
  CKCN = 0x00;              // system clock divisor is 1
  SS_HI                     // SS# high  
  PD5 |= 0x070;             // Set SPI output pins (SS, SCLK, DOUT) as output.
  PD5 &= ~0x080;            // Set SPI input pin (DIN) as input.
  SPICK = 0x00;             // fastest SPI clock--div by 2 
  SPICF = 0x00;             // mode(0,0), 8 bit data
  SPICN_bit.MSTM = 1;       // Set Q2000 as the master.
  SPICN_bit.SPIEN = 1;      // Enable SPI
// MAX3420E INT pin is tied to MAXQ2000 P60; make it an input
  PD6 &= ~0x01;             // PD6.0=0 (turn off output)
}
读寄存器,写寄存器
以下函数利用了MAXQ2000硬件SPI单元的优点,因此比起那些位仿真代码尺寸小而且快。
// Read a MAX3420E register, return its value.
BYTE rreg(BYTE reg)
{
BYTE dum;
  SS_LO
  SPIB = reg<<3;          // reg number w. dir=0 (IN)
  while(SPICN_bit.STBY);  // loop if data still being sent
  dum = SPIB;             // read and toss the input byte
  SPIB=0x00;              // data is don't care, we're clocking in MISO bits
  while(SPICN_bit.STBY);  // loop if data still being sent
  SS_HI
  return(SPIB);
}

// Write a MAX3420E register.
void wreg(BYTE reg, BYTE dat)
{
  SS_LO                   // Set SS# low
  SPIB = (reg<<3)+2;      // send reg. number w. DIR bit (b1) set to WRITE
  while(SPICN_bit.STBY);  // loop if data still being sent
  SPIB = dat;             // send the data
  while(SPICN_bit.STBY);  // loop if data still being sent
  SS_HI                   // set SS# high
}

例子:基于Windows的应急按钮

这个USB小产品是一个USB HID (人机接口设备),具有一个'应急'按钮。当你按下按键,所有的活动窗口被最小化,你看到的仅剩桌面,再按一下它,所有的应用窗口又重新弹回来。

USB键盘很有意思,如果插入几个键盘,它们将同时有效。所以我们的小应急按钮可以和你的正常键盘一起工作。

如果PC在待机,这个应急按钮担当了一个新角色—它可以充当PC的远程唤醒按键。不过这取决于你的PC支持不支持USB唤醒,有些可以,有些不可以。这个按钮可以帮你判断你的PC可不可以。

此例程在带有一个USB子卡(包含MAX3420E)的MAXQ2000开发板上运行。

USB详细说明

这个应用代码包含了USB做枚举类型琐碎工作的样板代码,此设备的属性已经用Panic_Button_Enum_Data.h中的特性阵列完全描述。

这个应用使用了两个端点,强制的CONTROL端点0,和EP3-IN,单缓冲64字节端点。虽然MAX3420E内含两个双重缓冲的64字节端点(EP1-OUT和EP2-OUT),在这个应用中并不需要双重缓冲的吞吐优势。

一个普遍存在HID错误概念是HID设备仅仅工作在低速下,本例展示了即使是像键盘这样的东西也可以从全速运行中得到好处,通过发送12MHz的包来而不是1.5MHz包,它可以使用更低的总线带宽。

图7. 应急按钮的流程图
图7. 应急按钮的流程图

中断端点有查询间隔,它决定了USB主设备隔多久向IN端点要数据。每隔一段时间我们可以预计到主控制器发了一个IN请求给我们的设备端点3。图7显示了处理这些请求的一个简单的状态机。只要设备被例举了,处理器重复地执行这一过程。为了简单起见,该应用程序查询中断脚是否有效,当然,如果你还有其他事要微控制器处理,你会用中断来激活Do_IN3函数。

状态机使用了两个全局变量:statebutton。C宏定义了三个状态:IDLE、RELEASE和WAIT。状态变量初始化为IDLE。如果连在MAX3420E的GPIN0上的按键按下,变量button是高,否则为低。main()中的无穷循环增加一个按键检查定时器,当定时器到时它会读一下MAX3420E中的GPIO寄存器来决定按键状态。此方法省掉了不必要的SPI流量。

当按键处于弹起状态时,状态图转到左边的两个分支,不做任何事。如果按键在IDLE状态被按下,就发一个清除桌面上活动窗口的键码。键码次序是08 (Windows键) 00 (保留)和07 (字母d)。下一个状态转到RELEASE,这样就完成了。

只要MAX3420E把数据包送到USB,它就产生另一个EP3-IN中断请求来表示EP3-IN FIFO可以再一次装载数据。然后再次进入图7函数,此时状态state = RELEASE ,因此函数发送序列00 00 00来表示“按键弹起”,下一个状态进入WAIT,意思是“等待按键被释放”。

现在函数要做的所有工作是利用WAIT状态分支程序来检测按键释放。如果按键一直按着,程序不做任何事,当按键一被释放,状态图就进到右边的两个分支,重新初始化state 变量为IDLE,使函数等候下一个按键按下。

占大部分运行时间的代码只有少数几行,图7给出了流程图:
void Do_IN3(void)
{
  switch(state)
  {
  case IDLE:
    if (button)
      {
                        wreg(rEP3INFIFO,0x08);  // "Windows" prefix key
                        wreg(rEP3INFIFO,0);
                        wreg(rEP3INFIFO,0x07);  // "D" key
                wreg(rEP3INBC,3);                       // arm it 
                state = RELEASE;        // next state sends the "keys up" code
      }
      break;    // else do nothing (and the SIE will NAK)
//
  case RELEASE:
    {
                wreg(rEP3INFIFO,0x00);          // key up
                wreg(rEP3INFIFO,0x00);
                wreg(rEP3INFIFO,0x00);          // key up
        wreg(rEP3INBC,3);                               // arm it
        state = WAIT;                   // next state waits for the PB to be unpressed
    }
    break;     
  case WAIT:
    if (!button)
      state = IDLE;
    break;
  default:  state = IDLE;
  } // end switch
} 

代码关键

需要对代码中的一些细节加以说明。

时间敏感的USB事件

MAX3420E通过在USB总线上送'K'状态10ms时间发了一个远程唤醒信号,为了避免用SPI主控器来做设定时间这种杂活,MAX3420E用自己内部来定时这个信号(事实上,所有的USB时间敏感事件),然后在时间到时给SPI主控器发一个中断。SPI主控器对这些事件不必用上它自己的定时器-它只需启动操作,然后等待完成中断。

ACKSTAT位

函数rregASwregAS的功能与rregwreg只有一点不同,它们在SPI命令字中设了ACK STATUS位。SPI主控器(我们的例子中是MAXQ2000)用这一位表示MAX3420E已经完成了当前的CONTROL请求服务,因此用应答它的状态情况的方式来终止CONTROL传输。ACKSTAT还扮演了一个内部寄存器位的角色,而且由于它含在SPI的命令字中,对它的操作能快速执行且只需少量代码。

readbytes(), writebytes()函数

readbytes()writebytes()函数使用了MAX3420E的数据突发能力。与每次字节寻址发两个字节(一个命令字节和一个数据字节)不同,这些函数先拉低SS#,送命令字,然后送入/送出一串字节,最后把SS#来拉高结束SPI传输。

哪里找到产品ID

图8. 产品ID在这里显示
图8. 产品ID在这里显示

产品ID码(在Panic_Button_Enum_Data.h中)是第一次插入应急按钮时出现的一小段信息。在确定应急按钮为HID的枚举过程中弹出来,并把它和Windows的内部驱动联系起来。

除了插入任何USB设备时都会听到一小声“叭哒”外,后续的每个附件都不发声。任何时候如果你想检查设备状态,请打开图8所示屏幕。你可以右击“我的电脑”,选择“属性”,“硬件”页,“设备管理器”按钮,展开“人机接口设备”项,右击“USB人机接口设备”,选择“属性”。

USB兼容性

查看代码后,你可能认为这对一个单键的USB设备来说要做很多工作,因为对任何USB设备都需要写一段固定代码。幸运的是我们对USB进行了仔细的归纳,枚举代码可作为任何USB设备的一个模板(拷贝-粘贴即可)。

像所有认真的开发者一样,我们希望自己的设计能够通过USB-IF认证,以避免任何知识产权问题。这个应用已经通过了USB命令验证(USBCV版本1.2.1.0)和USB-IF网站为开发者提供的HID测试。下面的图9给出了该应急按钮的记录信息。

图9. 应急按钮USB和HID测试记录和状况报告
图9. 应急按钮USB和HID测试记录和状况报告

结论

如果需要设计一个USB外设,可选择MAX3420E。该器件提供小尺寸封装,并可提供许多免费程序。MAX3420E能够为设计增加I/O口线,在支持SPI的系统中能很好地工作。由于SPI很容易通过位仿真实现,因此可以使用任何微控制器。如果需要更高性能,可以使用高达26MHz的SPI时钟。

类似文章发表于Circuit Cellar 2005年7月刊。

附录. 代码列表


下载代码列表