原始资料来自网络 整理日: 2015年2月12日
算是给所有正在学习USB,还徘徊着不得其门而入的朋友一个入门的契机吧,我也深知入门的痛苦,有些人入门就是抱着那什么USB协议,包定义,帧格式......啃来啃去的,结果啃不出个所以然来。
依我的经验来看,协议方面的东东,随便找本书,过一遍就行了;然后,你的终点应该放在你如何来写第一个成功的USB固件;而要写USB固件,那么了解Cypress固件架构是必要的,也是重中之重;再然后,等你积累了一些端点,控制,bulk,中断传输,SlaveFIFO,GPIF等等的经验后,再回过头去看协议方面的内容,就会有更加深刻的体会了;然后,你就可以试着更改FW。c文件了——这个时候你就是高手了。
68013的USB开发板
淘宝随便买块好了,还送不少资料。
要准备开发工具
去Cypress官网下一个Cy3684的开发包;
全称: cy3684_ez_usb_fx2lp_development_kit_15.exe
网址: www.cypress.com/?rID=14321
安装开发包
工具就是Cypress USB Console了。怎么用不用我说了吧.
以一个3684开发包自带的例子讲解。
进入目录(个人找自己的)
D:\Program Files\Cypress\USB\Examples\FX2LP\Bulkloop,
3个头文件拷到bulkloop文件夹
文件夹:D:\Program Files\Cypress\USB\Target
Cypress头文件: Fx2.h, fx2regs.h, syncdly.h
keil设置
output里关掉Run User Program #1(前面的勾去掉)
检查keil C51文件路径是否正确
如果你的keil是直接装载C:\Keil....下,那不会有错误,否则,自行设置正确的路径。
在工程下,有以下几个文件:其中,USBJmp.OBJ, EZUSB.LIB基本上是每个工程都要添加的,是一些中断向量表,EZUSB的函数库等等,不用管它们。 现在重点看前面三个文件:
fw.c
这个文件是整个USB的固件根本(FirmWare的缩写),USB协议方面的通信都是在这里完成的,包括上电枚举,重枚举,唤醒以及调用用户自己的程序和控制命令等等。基本上,如非必要,尽量不要动这个文件的内容,也不要在里面书写你自己的任何代码。
bulkllop.c
这个就是用户自己的代码书写文件(原始名称:periph.c)。
我们所有的代码都在这个文件里书写。Cypress已经给我们搭好了架构。
void TD_Init(void)
这个函数只会在USB启动后调用一次。在这个函数里添加你自己的初始化代码,也就是传输数据前要处理的,例如IO口配置,时钟,端点,FIFO的选择等等。 我们看bulkloop的初始化,它在USB的in,out传输启动前进行了哪些初始化: CPU时钟频率,USB工作模式选择,端点选择,端点传输方向,FIFO大小的配置等等。
void TD_Poll(void)
Poll中文意思调度,这个函数就是用户调度程序,USB会在空闲的时候反复调用该函数,所以我们把自己需要反复执行的代码放在这里。例如在bulkloop里,它就实现了反复从端点2接收上位机数据然后传给端点6,再从端点6传给上位机(4,8端点一样)。 BOOL DR_VendorCmnd(void):这个函数就是自定义命令代码的书写处。我们的Vendor命令都会写在这里,fw.c固件会自动调用我们的代码。
void ISR_Ep0in(void)
interrupt 0~void ISR_Ep8inout(void) interrupt 0:这几个函数是当使用端点中断传输时,中断代码的书写处,很少用。 以上,是经常会用到的几个函数;其他,基本不常用。
dscr.51
这个文件是USB描述符文件,包括了设备描述符,接口描述符,端点描述符,字符串等等。里面的英文都注释得很详细了,我就不多做介绍了,刚开始入门的时候,这个文件也不必改动。
fx2.h
预定义,宏及函数声明
fx2regs.h
68013的寄存器地址定义。
syncdly.h
同步延时。在其他文件里经常调用的一个函数SYNCDELAY就是这里定义的。
intrins.h
C51一些数据类型及函数定义。
好了,就写到这里,搞懂每个文件的作用非常非常重要,这样,你就可以知道自己的代码书写在什么地方,遇到不明的函数,定义可以到指定的位置查询,或者想修改某个设置(例如想把端点2设成IN,端点6设成out),知道到哪个文件里去修改。
USB入门的第一个例子,肯定是bulkloop了,装好驱动,开发包后,在开发包下....
Cypress\USB\Examples\FX2LP\Bulkloop就是bulkloop例子。
我之所以从bulkllop开始说,也是深有体会的。想当初刚开始学USB的时候,抱着协议闷头看楞是看不出个所以然,后来从例子开始学,才慢慢搞懂了USB,说实话,我到现在对USB协议类的DD还是一知半解,不过并不妨碍我进行USB的开发不是,所以说,Cypress的固件架构是个好东东,我们可以偷懒了。
开发包里例子是基于Cypress的一块开发板的,我想很少有人能弄到吧,而bulkloop就简单了,什么外围都不需要,一块68013,加个cyconsole interface就可以看到效果了.可以说,看懂了bulkloop,USB也就算正式入门了,剩下的要做的就是触类旁通,然后再不停的回过头去看USB协议,就会恍然大悟“然来是这样”。
首先,看bulkloop文件夹下的readme.txt文件,告诉我们这个固件主要实现的功能。
数据从EP2OUT->EP6IN ,从端点2的out缓冲区到端点6的in缓冲区。数据流向为:
然后看固件,看一个固件总是从TD_Init()开始的:
CPUCS = ((CPUCS & ~bmCLKSPD) | bmCLKSPD1) ;
IFCONFIG |= 0x40;
这两句设定CPU工作状态以及端口的工作模式。
bmCLKSPD,bmCLKSPD1是一个预定义,在FX2.H文件中有定义,keil应该都会用的,直接go to definition...跳到定义处查看;至于语法,使用了与或操作,用来进行位操作,置1或清0,编程语言中常用的技巧,C语言不要太差哦
然后寄存器每位的意义,我们在TRM中可以查到,这两句告诉我们:
设定68013 CPU时钟为48M,端口工作模式为普通的IO口。
EP1OUTCFG = 0xA0;
EP1INCFG = 0xA0;
SYNCDELAY; // see TRM section 15.14
EP2CFG = 0xA2;
SYNCDELAY;
EP4CFG = 0xA0;
SYNCDELAY;
EP6CFG = 0xE2;
SYNCDELAY;
EP8CFG = 0xE0;
这几行代码进行端点的配置EP2,EP4为out端点,512×2缓冲;EP6,EP8为in端点,512×2缓冲。
例如
EP2CFG=0XA2=1010 0010,看TRM对该寄存器的解释,
key | comment |
---|---|
b7 = 1 | 该端点有效; |
b6 = 0 | 该端点为out传输; |
b5b4 = 10 | 该端点进行bulk传输; |
b3 = 0 | 端点为512缓冲; |
b2 = 0 | 只读; |
b1b0 = 10 | 该端点缓冲区倍率为双重,即512×2.其他同理。 |
SYNCDELAY;
EP2BCL = 0x80; // arm EP2OUT by writing byte count w/skip.
SYNCDELAY;
EP2BCL = 0x80;
SYNCDELAY;
EP4BCL = 0x80; // arm EP4OUT by writing byte count w/skip.
SYNCDELAY;
EP4BCL = 0x80;
这段代码对端点计数器进行初始化
注意这里两个端点都写了两次,那是因为我们设置的端点缓冲为512×2,假如缓冲倍率为4,即512×4的话,那么这里初始化要写4次。
// enable dual autopointer feature
AUTOPTRSETUP |= 0x01;
这端代码告诉我们可以使用自动指针,也就是AUTOPTRHx两个自动指针,这两个自动指针使用方便,可以自动指向端点缓冲区。
然后是TD_POLL();在这里处理相关数据传送,USB在空闲的时候会自动调用这里面的代码。
WORD i;
WORD count;
if(!(EP2468STAT & bmEP2EMPTY)) {
// check EP2 EMPTY(busy) bit in EP2468STAT (SFR),
// core set's this bit when FIFO is empty
if(!(EP2468STAT & bmEP6FULL)) {
// check EP6 FULL(busy) bit in EP2468STAT (SFR),
// core set's this bit when FIFO is full
// 首先,查询端点2的EMPTY标志,如果不为1,说明有数据,
// 然后查询端点6的FULL标志,如果不为1,说明端点6 FIFO为空,
// 可以接收数据。至于bmEP2EMPTY,bmEP6FULL,自行go to definition... */
// 当检查到端点2 out fifo有数据且端点6 in fifo为空时,就可以将ep2的数据"copy"到ep6.
// 使用自动指针直接更换2者的指针,实现数据传送:
APTR1H = MSB( &EP2FIFOBUF ); // 取端点2 FIFO指针
APTR1L = LSB( &EP2FIFOBUF );
AUTOPTRH2 = MSB( &EP6FIFOBUF ); // 取端点6 fifo指针
AUTOPTRL2 = LSB( &EP6FIFOBUF );
count = (EP2BCH << 8) + EP2BCL; // 计数器
// loop EP2OUT buffer data to EP6IN
// 传送count字节
for( i = 0x0000; i < count; i++ ) {
// setup to transfer EP2OUT buffer to EP6IN buffer using AUTOPOINTER(s)
EXTAUTODAT2 = EXTAUTODAT1; // APTR1指针赋给APTR2,实现数据传送
}
// 完毕后,重置计数器,以进行下次传输:
EP6BCH = EP2BCH;
SYNCDELAY;
EP6BCL = EP2BCL; // arm EP6IN SYNCDELAY;
EP2BCL = 0x80; // re(arm) EP2OUT
}
}
以上即时bulkloop的整个工作过程,可以在interface中方便的看到结果,板子不在身边,就懒的贴图了。
FW.C文件,是比较难看懂的了,这个要逐字逐句研读,
我当初整整看了一个星期,边理解,边一行一行的注释,可以说,看懂了,USB协议部分也就差不多了。
从main()函数开始看:
DWORD i;
WORD offset;
DWORD DevDescrLen;
WORD IntDescrAddr;
WORD ExtDescrAddr;
Sleep = FALSE; // 初始化用户变量 休眠使能--禁止
Rwuen = FALSE; // 远程唤醒--禁止
Selfpwr = FALSE; //
GotSUD = FALSE; // SetUp令牌包到来标志
定义了一些变量,具体用途在后面;第二段同时对变量进行初始化,从名字可以看出其用途。
紧接着调用TD_Init()函数,是一些我们自己的初始化配置。
// 定向USB描述符
pDeviceDscr = (WORD)&DeviceDscr;
pDeviceQualDscr = (WORD)&DeviceQualDscr;
pHighSpeedConfigDscr = (WORD)&HighSpeedConfigDscr;
pFullSpeedConfigDscr = (WORD)&FullSpeedConfigDscr;
pStringDscr = (WORD)&StringDscr;
这段代码用来获取USB的各个描述符在68013内存中的地址,准确说是在RAM中的地址,在dscrpt.a51文件中有定义,所有的描述符组成了整个的描述符表,后面会用到。
if ((WORD)&DeviceDscr & 0xC000)
这段代码及以后的,在固件中解释是:
Is the descriptor table in external RAM (> 16Kbytes)? If yes, then relocate.
Note that this code only checks if the descriptors START in external RAM. It will not work if the descriptor table spans internal and external RAM.
意思是说,这段代码用来判断描述符表首址,也就是前面的DeviceDscr、DeviceQualDscr等是否位于68013的外部RAM区,如果是,则移除,然后将描述符表移到内部RAM区,为什么要移到内部RAM区,因为当描述符表位于外部RAM时,USB是不工作的。那么如何判断描述符地址是否超出内部RAM的地址呢?首先,&DeviceDscr取得整个描述符表的首地址(它也是DeviceDscr设备描述的首址),然后和0XC000相与,为什么要和0XC000相与?这就牵涉到68013 FX2LP(注意是LP)的内部结构图:
图片暂咯
上图针对的是128pin的FX2LP,如果是56或100pin的,那么没有外部RAM,只有内部RAM。
可以看到,FX2LP内部RAM从0000-FFFF,其他为外部RAM。而内部RAM中,只有从0000-3FFF和从E000-FFFF的区域可用,其他为系统保留。从0000-3FFF这16K bytes的内部RAM空间,叫做主RAM,对56,100,128pin来说,都可以同时作为程序或数据存储器(对128pin来说,EA=0)。
再看&DeviceDscr & 0xC000的结果,要为“真”的话,显然,必须&DeviceDscr>=0X4000,也就是说判断的是描述表首址&DeviceDsc是否大于3FFF,刚好是主RAM区的大小,这就是为什么要用&DeviceDscr 和 0xC000相与就来判断实现了描述符表首地址的原因了(外部 or 内部 ram?)。
// 重定向描述符S
IntDescrAddr = INTERNAL_DSCR_ADDR;
ExtDescrAddr = (WORD)&DeviceDscr;
DevDescrLen = (WORD)&UserDscr - (WORD)&DeviceDscr + 2;
for (i = 0; i < DevDescrLen; i++)
*((BYTE xdata *)IntDescrAddr+i) = *((BYTE xdata *)ExtDescrAddr+i);
判断发现描述符表首址位于外部RAM的后,紧接着就将外部RAM的描述符移到内部RAM。这里就用到了前面定义的变量,
IntDescrAddr
保存内部RAM首址0X80,
ExtDescrAddr
保存我们获得的当前描述符表外部RAM的首址 ,
DevDescrLen
是整个描述表的长度,从DeviceDscr段到UserDscr段,在dscrptr中有定义。然后for循环,将从ExtDescrAddr地址开始的外部RAM中的数据逐个copy到从IntDescrAddr地址开始的内部RAM区。
// 更新描述符指针
pDeviceDscr = IntDescrAddr;
offset = (WORD)&DeviceDscr - INTERNAL_DSCR_ADDR;
pDeviceQualDscr -= offset;
pConfigDscr -= offset;
pOtherConfigDscr -= offset;
pHighSpeedConfigDscr -= offset;
pFullSpeedConfigDscr -= offset;
pStringDscr -= offset;
完毕后更新描述符指针,指向内部RAM区,通过原指针减去一个偏移量得到。
然后是USB的一些初始状态设置:
EZUSB_IRQ_ENABLE(); // EZUSB中断使能
EZUSB_ENABLE_RSMIRQ(); // 使能远程唤醒中断
INTSETUP |= (bmAV2EN | bmAV4EN); // 使能INT2,4自动向量跳转
USBIE |= bmSUDAV | bmSUTOK | bmSUSP | bmURES | bmHSGRANT; // 使能所选择中断
EA = 1; // 开8051中断
EZUSB_IRQ_ENABLE();
预定义是EZUSB=1,查TRM得知,ezusb是EIE寄存器的第0位,EIE.0=1,使能USB中断;
EZUSB_ENABLE_RSMIRQ();
EICON |= 0x20,EICON.5=1,使能远程唤醒中断;
INTSETUP |= (bmAV2EN | bmAV4EN);
使能INT2,4自动向量跳转; USBIE |= bmSUDAV | bmSUTOK | bmSUSP | bmURES | bmHSGRANT;使能所选择中断,相关的中断意义,我也一知半解,后面慢慢学习补充;
EA = 1;
开8051中断。从下面的代码开始,才真正开始我们自己的USB事务处理:
// Main Loop主循环
while(TRUE) {
// Poll User Device 用户调度程序
TD_Poll(); // Check for pending SETUP
// 等待SETUP令牌数据的到来
if(GotSUD) {
SetupCommand(); // Implement setup command // 处理SETUP事务
GotSUD = FALSE; // Clear SETUP flag 清Setup标志
}
}
TD_Poll,也就是用户调度程序,USB空闲时调用,不过为什么要放在开始呢?照理说应该放在令牌包后面的,这里不是很明白。。。。。。
if(GotSUD),GotSUD是令牌包标志,准确的说是“令牌阶段数据到来”
首先,USB一连串的数据传输、处理、响应等就叫做USB事务。
例如,上位机要读取一个描述符,那么就会触发一次USB事务。
一个完整的USB事务处理有三个阶段:
USB主机启动事务处理,开始发送令牌包,这个时候假如说我们当前的USB设备地址号为2(重枚举时分配的),而主机发送的地址号也为2,那么这个USB设备硬件会产生中断,进入中断处理。也就是periph.c文件中的void ISR_Sudav(void)函数,在这个中断处理中,设置 GotSUD标志为TRUE,表示收到令牌数据,要启动USB传输了。然后我们固件中判断if(GotSUD),GotSUD为真,则执行SetupCommand()函数,在这里处理控制传输,读取描述符,设置特性,处理Vendor命令等。完毕后置GotSUD = FALSE;然后检查USB各种状态并处理:
if (Sleep){
// 如果USB进入了休眠状态,这里Sleep是USB休眠标志,通GotSUD一样,USB休眠后产生中断,
// 进入void ISR_Susp(void)函数处理,置sleep标志为TRUE。
// 检测USB是否挂起
if(TD_Suspend()) {
Sleep = FALSE; // 清Sleep标志
do {
EZUSB_Susp(); // 置8051为空闲状态.
} while(!Rwuen && EZUSB_EXTWAKEUP());
// 如果唤醒
EZUSB_Resume();
// 从空闲状态中恢复
TD_Resume();
}
到这里,一个事务处理完毕,等待下次事务处理中断的到来.
在继续看fw.c文件前,我觉得有必要先把USB的整个工作过程搞懂!
这本书上讲的不错搞懂了USB整个工作过程,对FW中的列举/重列举就比较好理解了。这也是我自己曾经学习的一个过程吧,我并没有一开始就去仔细推敲USB协议方面的东西,而是先看一个大概,然后学到哪里不懂的时候,再回过头去进一步研究,这样慢慢就理解深入了。
个人觉得好多书里一上来就给你细细剖析协议的,完全是浪费脑细胞,越看越糊。哎~USB协议真的非常非常难搞通,我也深深的痛苦中。。。。。。
以下参照《USB原理与工程实践》这本书上讲的,我简化了一下,算是自己的理解吧:
USB连上电脑(实际是集线器HUB),但是还没有上电。
也就是说VCC还没有电压,到VCC上有5V电压有一个很短的延时过程。该过程主机PC和HUB通信。
复位USB总线。
PC通知HUB复位USB总线,获得传输模式低速or全速or高速。复位后,USB设备获得一个默认的设备地址号0。此过程PC和HUB通信。
USB设备获得地址号0后,可使用该地址号进行某些事务处理。
使用地址号0,控制传输,端点0,主机开始和USB设备功能层开始通信。 USB功能层是USB总线结构中的一个,USB总线结构由USB功能层,USB设备层,USB接口层构成。USB功能层,主要负责数据传输操作,就是控制传输,中断传输,块传输,同步传输。
主机开始获得USB设备的信息
例如刚开始要获得USB控制传输所支持的最大数据包长度,那么就要向USB设备发命令(发送GetDescription获得设备描述符信息),于是启动一个USB事务处理,而USB事务处理分为3个阶段:
令牌阶段
数据阶段
握手阶段
也就是说,这一步中,主机发送GetDescription请求读取设备描述符,获得USB控制传输所支持的最大数据包长度(只需读取前8个字节即可),这是一个USB事务,既然是 事务,那么所有的USB事务必然从令牌包开始,于是USB固件首先等待令牌包到来,然后处理相应的命令。这样,主机通过发送GetDescription请求(USB 11中标准请求之一 ),读取设备描述符DeviceDscr,目的是获取控制传输支持的最大字节数(第8个字节),一旦检测到这个数,主机复位USB总线并开始进入枚举过程.
至此,开始进入枚举过程。
主机向USB设备发送SetAddress请求,为其分配一个新的,唯一的设备地址(1~127,总共128个)。以后,USB将使用这个新的地址号与主机通 信。
主机循环向USB设备发出GetDescription请求,读取所有的描述符,获得该USB设备的全部配置信息。
首先读取设备描述符DeviceDscr全部字段,
然后读取配置描述符 Configuration,接口描述符,端点描述符,其他各种设备类描述符以及自定义描述符等。
至此,USB枚举结束。
那么,什么是重枚举呢?
首先,上面的USB枚举是对通用USB来说的,一般USB设备只有枚举过程,没有重枚举过程。也就是说其实,
对EZ-USB系列来说,上面的枚举举实际包含了EZ-USB枚举和重枚举两个过程:
对EZ-USB来说,枚举过程就是USB上电复位到加载固件前这段过程,此时USB设备地址号为默认的0号,枚举完成后,驱动为cypress...eeprom..missing(FX2/FX2LP来说)。
然后加载固件,进行重枚举,重枚举完成后,显示驱动为cypress...ez-usb...example或其他自定义设备。
为什么EZ-USB有重枚举过程呢?这就是为了可以让主机在前期固件未加载前自动枚举,识别USB,从而可以从上位机加载固件到68013的RAM中,而无需使用ROM,EEPROM,FLASH等内存。实现“软件”架构加载程序代码,随时修改固件。
首先,EZUSB有一个寄存器USBCS,其中第1位USBCS.1为Renum控制枚举重枚举。
在USB上电未加载固件代码前,Renum=0,表示使用“EZUSB核心”对芯片的初始配置,处理主机设备请求,并负责把固件下载到RAM中,这个过程就是“枚举”,完成该枚举过程的设备叫做“缺省USB设备(地址号0)”,即驱动显示为“...missing”的设备。
当固件被下载到8051 RAM后,此时Renum=1,表示使用“增强8051核心”处理主机设备请求,并按照固件的代码(读取所有描述符)重新配置USB设备,这个过程就叫做“重枚举”,完成重枚举后,显示自定义的USB设备“...example”,实际是模拟断开与连接的过程。 用以下表表示:
处理设备请求 | Renum | 8051动作 | |
---|---|---|---|
枚举 | EZUSB核心 | Renum=0 | 8051置Renum=1 |
重枚举 | 8051 | Renum=1 | 8051重置Renum=0 |
在重枚举完成后,对控制端点0的设备请求可以由“EZUSB核心”处理或由“增强8051核心”处理,有Renum的值决定。芯片上电时Renum=0,由“EZUSB核心”处理;一旦8051开始运行,就可以设置Renum=1,由“增强8051核心”处理,表示按照8051下载的固件代码处理。
当然,这时也可以设置Renum=0,让“EZUSB核心”处理端点0的设备请求,而让8051完成具体的USB数据传输,这样做会大大简化8051固件代码。 然后再看接下来的代码:
注意,在这段代码之前,EZUSB已经枚举完成,当然,整个过程是自动的,我们看不见的。
#ifndef NO_RENUM if(!(USBCS & bmRENUM)) // 如果RENUM位为0,则重列举
{
EZUSB_Discon(TRUE); // renumerate 重列举
}
#endif
这段代码即告诉USB进行重枚举,用软件设置,模拟USB断开与连接,。EZUSB_Discon(TRUE)完成断开连接,具体实现在EZUSB.lib库中,大概代码是这样的:
void UsbDisconnect(BOOL renum) {
if(renum)
USBCS |= (bmDISCON | bmRENUM);
else
USBCS |= bmDISCON;
EZUSB_Delay(1500);
USBIRQ = 0xff;
EPIRQ = 0xff;
EZUSB_IRQ_CLEAR();
USBCS &=~bmDISCON;
}
将USBCS寄存器的DICON位置1,断开USB,同时如果RENUM位为0,则置1;然后重新连接USB。
// unconditionally re-connect. If we loaded from eeprom we are
// disconnected and need to connect. If we just renumerated this
// is not necessary but doesn't hurt anything
USBCS &=~bmDISCON; // 重新连接
CKCON = (CKCON&(~bmSTRETCH)) | FW_STRETCH_VALUE;
// Set stretch
// clear the Sleep flag.
Sleep = FALSE; // 清sleep标志
这段代码英文意思说的很清楚了,CKCON还不清楚干什么用的。。。 紧接着主机开始读描述符,并进行配置(发送SetConfiguration),加载驱动等,完成重枚举。
重新看了描述符文件,对照了好多参考书,发现以前很多不明白的地方现在清晰了很多,不过有些地方可能从来没有用过的缘故,我也是不甚明了(打上了问号),只能以后用到的时候,有新的发现再慢慢理解了...
dscr51里放的是USB描述符表,EZ-USB在重枚举阶段会读取或设置相应的描述符
db DSCR_DEVICE_LEN ;; Descriptor length
db DSCR_DEVICE ;; Decriptor type
dw 0002H ;; Specification Version (BCD)
db 00H ;; Device class
db 00H ;; Device sub-class
db 00H ;; Device sub-sub-class
db 64 ;; Maximum packet size
dw 0B404H ;; Vendor ID
dw 0410H ;; Product ID (Sample Device)
dw 0000H ;; Product version ID
db 1 ;; Manufacturer string index
db 2 ;; Product string index
db 0 ;; Serial number string index
db 1 ;; Number of configurations
db DSCR_DEVICE_LEN
bLength段, 指明整个设备描述符的长度,单位字节。
db DSCR_DEVICE
bDescriporType段, 描述符类型值。DSCR_DEVICE=04H--设备描述符。
dw 0002H
bcdUSB, 表明该USB设备所遵循的USB协议版本,用bcd码表示,2字节。例如2.0版本,值为0200H,用bcd码表示,低字节在前,高字节在后,表示为0002H;同理,1.1版本,则表示为1001H。
db 00H
bDeviceClass段, 指明USB设备所属的设备类。
Value | comment |
---|---|
0 | 表示USB各接口相互独立工作,分属不通的设备类,具体信息在接口描述符中说明 |
1~FEH | 表明该USB设备属于某个明确的设备类,例如04H代表显示设备 |
FFH | 厂商自定义的设备类 |
db 00H
bDeviceSubClass段, 指明USB设备所述的设备子类。其值依赖bDeviceClass。 =0,此时bDeviceClass必须首先为0 =1~FEH,详细的设备子类。例如如果bDeviceClass=04H,是显示设备,则bDeviceSubClass=01H,表示CRT显示器; =FFH,厂家自定义
db 00H
bDevicePortcol段, 指明USB所使用的设备类协议。其值依赖bDeviceClass和bDeviceSubClass。
Value | comment |
---|---|
0 | 表示该设备不使用任何设备类协议 |
1~FEH | 该USB必须属于某个明确的设备类和子设备类。如视频类协议(UVC),音频类协议(UAC)等 |
FFH | 厂家自定义 |
db 64
bMaxPacketSize0段, 指明该USB设备端点0控制传输所支持的最大数据包长度,单位字节。
dw 0B404H
VID
dw 0410H
PID
dw 0000H
bcdDevice段, 指明USB设备版本号。产品ID???
db 1
iManuFacture段, 厂商信息字符串索引值,没有时为0.这里为1,,即下面的“Cypress”字符串。
db 2
iProduct段, 产品信息字符串索引值,没有时为0.后面的“EZ-USB”字符串。
db 0
iSerial段, USB设备序列号信息字符串索引值,没有时为0.
db 1
bNumConfigurations段 指明USB设备所支持的配置数。???如果USB设备支持两种传输速率,则该字段指出的是该速率下的配置数,而不是两种速率下的配置数和。
设备限定描述符:DeviceQualDscr
DeviceQualDscr:
db DSCR_DEVQUAL_LEN ;; Descriptor length
db DSCR_DEVQUAL ;; Decriptor type
dw 0002H ;; Specification Version (BCD)
db 00H ;; Device class
db 00H ;; Device sub-class
db 00H ;; Device sub-sub-class
db 64 ;; Maximum packet size
db 1 ;; Number of configurations
db 0 ;; Reserved
设备限定描述符,9个字段,共10字节。
仅当该USB为高速USB设备,且设备既需支持高速(High Speed)又需支持全速(Full)时,就需要用到设备限定描述符。
例如该高速USB设备目前工作于全速模式,则该描述符中包含高速模式的总体信息。
在设备请求处理函数SetupCommand(void)中,当收到读设备限定描述符请求时,会首先判断是否为高速USB设备。if (HighSpeedCapable())。
db DSCR_DEVQUAL_LEN
bLength段, 整个设备限定描述符的长度,单位字节,共10个字节。
db DSCR_DEVQUAL
bDescriptorType段, 指出该描述符类型。06H->设备限定描述符。
dw 0002H
bcdUSB段, USB协议版本号。
db 00H
bDeviceClass段, 该USB设备所属的USB设备类。
db 00H
bDeviceSubClass段, 所属子类。对bDeviceClass的进一步细化分类说明。
db 00H
bDeviceProtocol段, 该设备所使用的设备类协议。
db 64
bMaxPacketSize0段, 端点0控制传输所支持的最大数据包长度,单位字节。
db 1
bNumConfigurations段 另一速率所支持的配置数。
db 0
bReserved段 保留,必须为0.
配置描述符:
HighSpeedConfigDscr/FullSpeedConfigDscr
db DSCR_CONFIG_LEN ;; Descriptor length
db DSCR_CONFIG ;; Descriptor type
db (HighSpeedConfigDscrEnd-HighSpeedConfigDscr) mod 256 ;; Total Length (LSB)
db (HighSpeedConfigDscrEnd-HighSpeedConfigDscr) / 256 ;; Total Length (MSB)
db 1 ;; Number of interfaces
db 1 ;; Configuration number
db 0 ;; Configuration string
db 10000000b ;; Attributes (b7 - buspwr, b6 - selfpwr, b5 - rwu)
db 100 ;; Power requirement (div 2 ma)
配置描述符包含8个字段,共9字节。所有的USB设备至少包含一个配置描述符,例如这里包含两个配置描述符高速HighSpeedConfigDscr和全速FullSpeedConfigDscr。
db DSCR_CONFIG_LEN
bLength段, 描述符长度,9字节。
db DSCR_CONFIG
bDescriptorType段, 描述符类型。
db (HighSpeedConfigDscrEnd-HighSpeedConfigDscr) mod 256
db (HighSpeedConfigDscrEnd-HighSpeedConfigDscr) / 256
wTotalLength段,指明配置信息总长度,2字节表示。为配置描述符,接口描述符,端点描述符,设备类定义描述符,供应商自定义描述符长度的和。在这里只有配置、接口和端点描述符。
db 1
bNumInterface段, 指明该配置所支持的接口数(??),最小为1.
db 1
bConfigurationValue段, 指明该配置的配置值。例如这里值为1,在重枚举时,主机发送Setconfiguration(x),当x=1时,就调用该配置。
db 00
iConfiguration段, 该配置的字符串索引值,没有时为0.
db 10000000b
bmAttributes段, 指明该配置的特性,8位。
b0~b4,保留,必须为0.
b5:远程唤醒选择。=1,支持远程唤醒;=0,不支持远程唤醒。
b6:是否总线电源选择。如果该USB设备外加了电源,=1,使用总线电源,=0,使用自供的电源。
b7:必须为1. 在主机设备请求case SC_GET_STATUS,case GS_DEVICE中获得该信息。
db 100
bMaxPower段, 总线供电时,该USB设备可获得的最大电流。单位2mA,所以最大值为250.如果该电流得不到满足,USB将不能使用这个配置。
接口描述符
;; Interface Descriptor
db DSCR_INTRFC_LEN ;; Descriptor length
db DSCR_INTRFC ;; Descriptor type
db 0 ;; Zero-based index of this interface
db 0 ;; Alternate setting
db 6 ;; Number of end points
db 0ffH ;; Interface class
db 00H ;; Interface sub class
db 00H ;; Interface sub sub class
db 0 ;; Interface descriptor string index
接口描述符有9个字段,共9字节。
注意,主机不能用SetDescription和GetDescription来设置和读取接口描述符,它只能作为配置描述符的一部分信息返回,在主机发送case SC_GET_DESCRIPTOR,且case GD_CONFIGURATION时一并读取。所以我们看到,在fw.c文件中并没有对接口描述符的判断。
db DSCR_INTRFC_LEN
bLength段, 描述符长度。
db DSCR_INTRFC
bDescriptorType段, 描述符类型。
db 0
bInterfaceNumber段, 指明该接口的接口号。
db 0
bAlternateSetting段, 指明接口的可替换设置值。
db 6
bNumberEndpoints段, 指明接口所使用的断点数,不包括端点0.
db 0ffH
bInterfaceClass段, 指明接口所使用的设备类。
Value | comment |
---|---|
0 | 保留 |
1~FEH | 表明该接口属于某个明确的USB设备类 |
FFH | 厂家自定义的设备类 |
db 00H
bInterfaceSubClass段, 该接口所属的USB设备子类。
db 00H
bInterfaceProtocol段, 该接口所使用的设备类协议。
db 0
iInterface段, 接口字符串描述符的索引值,没有时为0.
;; Endpoint Descriptor
db DSCR_ENDPNT_LEN ;; Descriptor length
db DSCR_ENDPNT ;; Descriptor type
db 02H ;; Endpoint number, and direction
db ET_BULK ;; Endpoint type
db 00H ;; Maximun packet size (LSB)
db 02H ;; Max packect size (MSB)
db 00H ;; Polling interval
端点描述符有6个字段,共7字节。和接口描述符一样,也不能由主机通过发送GetDedcription()请求读取,只能作为配置信息case GD_CONFIGURATION的一部分返回给主机。
db DSCR_ENDPNT_LEN
bLength段, 该描述符长度,单位字节。
db DSCR_ENDPNT
bDescriptorType段, 该描述符类型。
db 02H
bEndpointAddress段, 指明端点的端点号及传输方向。
b0~b3:该端点的端点号。如0001端点1,0010端点2;
b4~b6::保留,必须为0
b7:端点传输方向。1-IN传输;0-OUT传输
db ET_BULK
bmAttributes段, 指明端点的一些特性。
b0~b1:端点的传输类型。
Value | comment |
---|---|
00 | 控制传输 |
01 | 同步传输 |
10 | 块传输 |
11 | 中断传输 |
b2~b3:当该端点为同步端点时,这两位指出同步类型。
Value | comment |
---|---|
00 | 非同步 |
01 | 异步 |
10 | 自适应 |
11 | 同步 |
b4~b5:端点用法类型。
Value | comment |
---|---|
00 | 数据端点 |
01 | 显示反馈端点 |
10 | 隐式反馈端点 |
11 | 保留 |
b6~b7:保留,必须为0
key | comment |
---|---|
00 | 每小帧1次(默认), |
01 | 每小帧2次(附加一次) |
10 | 每小帧3次(附加2次) |
11 | 保留。 |
b13~b15保留,必须为0
db 02H
wMaxpacketSize段(MSB)
高8位。
数据包大小为:0000 0010 0000 0000,取0~10位,还是0200,512字节。
db 00H
bInterval段, 指明端点数据传输的访问间隔。
低速中断端点:=10~255ms.
全速中断端点:=1~255ms
高速中断端点:=1~16,
访问间隔为2(bInterval-1)(幂)×1us 全速/高速同步端点:=1~16,
访问间隔为2(bInterval-1)(幂)×1ms 和2(bInterval-1)(幂)×1us 高速块/控制out端点:指明其最大NAK握手包发送速率。 =0,表示该端点永远不会发出NAK握手包 =其他值,表示每个bInterval时间内,该端点最多只能发送一次NAK握手包。 其他类型端点:该字段无效。
字符串描述符:StringDscr1:
StringDscr1:
db StringDscr1End-StringDscr1 ;; String descriptor length
db DSCR_STRING
db 'C',00
db 'y',00
db 'p',00
db 'r',00
db 'e',00
db 's',00
db 's',00
StringDscr1End:
3个自独,长度是变化的(字节)。
偏移量 | 域 | 大小 | 值 | 描述 |
---|---|---|---|---|
0 | bLength | 1 | N+2 | 此描述表的字节数 |
1 | bDescriptorType | 1 | 常量 | 字串描述表类型 |
2 | wLANGID[0] | 2 数字 | 语言标识 | (LANGID)码0 |
… | … | … | … | … |
N | wLANGID[x] | 2 | 数字 | 语言标识(LANGID) |
db StringDscr1End-StringDscr1
bLength段, 描述符长度
db DSCR_STRING
bDescriptorType段, 描述符类型。
Unicode编码的字符串。
【关于USB】这家伙总结得不孬啊,我刚看一会儿就学了不少东西。
摘自: http://www.daxiamcu.com/bibis/moredata30_1384202.shtml
元器件
CYPRESS 68013A:支持USB 2.0协议,带增强型8051单片机,时钟频率48Mhz。支持串口通讯。
文档
文档名 | 说明 |
---|---|
cy7c68013.pdf | 68013外设手册 |
cy7c68013_5.pdf | 68013外设手册 |
CY3684_A_SCH.PDF | 68013A外围电路图 |
FX2 TechRefManual.pdf | EZUSB-FX2技术手册 |
fx2_to_fx2lp.pdf | FX2和FX2LP的区别 |
CYAPI.PDF | CYAPI手册 高级类库 |
CYUSB.PDF | CYUSB手册 底层API |
EFROM
因为本产品要求将二进制代码和硬件PID/VID烧录在EEPROM,而不是使用CYPRESS推荐的在线下载方式,所以外部采用了8K 的EEPROM。上电后68013A会将EEPROM中的数据和程序加载到RAM中运行。
HEX2BIN.EXE
HEX文件只是68013A上8051的程序代码,还要加上PID/VID等信息才能正确运行,CYPRESS在开发包中提供了HEX2BIN.EXE这个工具,可以根据HEX生成完备的IIC文件,将此文件烧录到EEPROM上即可。
HEX2BIN.EXE的使用方法如下:
将XXX.HEX文件拷贝到HEX2BIN.EXE所在目录,打开CMD,按如下格式输入:
hex2bix -i -o xxx.iic xxx.hex -f 0xC2 -v 0x1234 -p 0x1234
key | comment |
---|---|
-i | 表示输出文件,也就是IIC文件 |
-o | 表示输入文件,也就是HEX文件 |
-f | 表示68013A发送PID/VID的方式,这里为C0,即从EEPROM上读取。 |
-v | 表示VID的BCD码,开发阶段使用1234 |
-p | 表示PID的BCD码,开发阶段使用1234 |
CYPRESS提供了非常好的程序框架,免去了用户自己编写一些通用性比较强、模式化的程序(如果不提供,很少有人能写出如 此高效,结构紧凑的程序,实际上此框架和68013A内部结构关系密切,一般人也没有足够的内部资料也不可能写出来)。在框 架的基础上,用户只需在相应的地方写相应的代码即可完成USB工作。
一般来说框架可以分成3个部分。
描述符文件
例如dscr.a51文件,里面定义了枚举设备的时候要用的各种描述符信息,这部分用户需要根据实际的情况自己编写。我写的时候发现一个最大的问题就是各种书籍协议版本不同,翻译质量不同,同一字段的意义表述不同,容易让人产生困惑。例如USB 1.1/2.0/2.13对设备类型的子类定义都不完全相同,所以写的时候最好几种文档对比起来写。由于USB官方网站的文档中字段解释过于专业化,所以对USB不是很熟悉的人比较难以理解其真正含义。所以要多参考不同的书籍,某种程度上降低了开发速度,但对第一次做USB开发的人来说,这也是值得的。
固件文件
例如FW.C文件,这是硬件程序的函数入口。主要有以下这些方法:
void SetupCommand(void); // 握手命令处理
void TD_Init(void); // 初始化,完成配置,启动时调用一次
void TD_Poll(void); // 用户处理程序,循环调用
void IO_Init(void); // 8051 IO初始化
void REG_Init(void); // 8051寄存器初始化
BOOL TD_Suspend(void); // 挂起处理
BOOL TD_Resume(void); // 唤醒处理
// 以下为各种描述符的获取和设置函数,重枚举时自动调用
BOOL DR_GetDescriptor(void);
BOOL DR_SetConfiguration(void);
BOOL DR_GetConfiguration(void);
BOOL DR_SetInterface(void);
BOOL DR_GetInterface(void);
BOOL DR_GetStatus(void);
BOOL DR_ClearFeature(void);
BOOL DR_SetFeature(void);
BOOL DR_VendorCmnd(void);
void ISR_Sudav(void) interrupt 0 // 收到setup包
void ISR_Sutok(void) interrupt 0 // 收到SETUP令牌
void ISR_Sof(void) interrupt 0 // 收到起始帧
void ISR_Ures(void) interrupt 0 // 收到RESET
void ISR_Susp(void) interrupt 0 // 收到挂起信息
void ISR_Highspeed(void) interrupt 0 // 高速模式
void ISR_Ep0ack(void) interrupt 0 // 正常响应ACK
void ISR_Stub(void) interrupt 0
void ISR_Ep0in(void) interrupt 0
void ISR_Ep0out(void) interrupt 0
void ISR_Ep1in(void) interrupt 0
void ISR_Ep1out(void) interrupt 0 // EP1输入中断
void ISR_Ep2inout(void) interrupt 0 // EP2中断
void ISR_Ep4inout(void) interrupt 0
void ISR_Ep6inout(void) interrupt 0
void ISR_Ep8inout(void) interrupt 0
void ISR_Ibn(void) interrupt 0
void ISR_Ep0pingnak(void) interrupt 0
void ISR_Ep1pingnak(void) interrupt 0
void ISR_Ep2pingnak(void) interrupt 0
void ISR_Ep4pingnak(void) interrupt 0
void ISR_Ep6pingnak(void) interrupt 0
void ISR_Ep8pingnak(void) interrupt 0
void ISR_Errorlimit(void) interrupt 0
void ISR_Ep2piderror(void) interrupt 0
void ISR_Ep4piderror(void) interrupt 0
void ISR_Ep6piderror(void) interrupt 0
void ISR_Ep8piderror(void) interrupt 0
void ISR_Ep2pflag(void) interrupt 0
void ISR_Ep4pflag(void) interrupt 0
void ISR_Ep6pflag(void) interrupt 0
void ISR_Ep8pflag(void) interrupt 0
void ISR_Ep2eflag(void) interrupt 0
void ISR_Ep4eflag(void) interrupt 0
void ISR_Ep6eflag(void) interrupt 0
void ISR_Ep8eflag(void) interrupt 0
void ISR_Ep2fflag(void) interrupt 0
void ISR_Ep4fflag(void) interrupt 0
void ISR_Ep6fflag(void) interrupt 0
void ISR_Ep8fflag(void) interrupt 0
void ISR_GpifComplete(void) interrupt 0
void ISR_GpifWaveform(void) interrupt 0
特别是对于接受数据,一般都在中断中完成相应处理,“中断中适合进行少量简短的操作,不适合进行复杂操作”,这句话在 此依然有效。如果要进行复杂的操作可以在TD_POLL()中进行(多数操作都是在这个函数中完成的)。
另外非常重要的一点是,中断程序的结尾应该让中断复位,允许下一次中断,有些端点的计数器也要清零并允许接受新的中断 请求。例如:
EP1OUTBC = 0; // 清空计数
EZUSB_IRQ_CLEAR(); // USB中断复位
EPIRQ = 0x08; // 允许EP1中断请求
68013A内部的寄存器约有300个上下,一次都记住是不可能的,而且每个寄存器都有8个位,也就是说一共有2000多个可以配置的位,一次都理解掌握这些位的含义也是不可能的,所幸地是开发中并不会用到所有的寄存器,但是依然强烈建议把 FX2REGS.H和FX2.H走读一边,这就像读书一样,没有学会识字,再看都是天书。结合FX2 TechRefManual.pdf走读这些寄存器 大约需要一到两天时间,这点时间投入还是值得的。
在通讯过程中,打交道最多的是各种端点寄存器,掌握好这些寄存器地使用对提升开发效率是很有帮助。值得特别关注的寄存 器和配置位如下:
其中有些寄存器的设置需要连续设置多次,看似重复了,其实不然,这和设置的缓冲区数量有关。
有些寄存器中间必须用SYNCDELAY来延时。这类寄存器FX2 TechRefManual.pdf上有说明。
对于EP0,用于系统握手,相关的寄存器操作基本上都由68013A的内核(SIE)来完成了。
对于EP1,分为OUT/IN两组配置和寄存器。
对于EP2~EP8,不分OUT/IN输入输出,主要有EP2CFG/ EP2FIFOCFG/ EP2BCH/EP2BCL寄存器。
自动指针是CYPRESS提供的一个非常有用的特性。
在数据交互的过程中,很多时候都涉及到数据的搬迁,比如从EP2OUT收到的 数据需要转发到EP6IN上(一些转换类设备);再比如从RAM中拷贝数据到EP4IN上,传统的做法是申明两个指针,指向源和目 的地址,然后用循环一个个字节拷贝,同时还要考虑增加指针地址,对于连续的空间这到不是问题,关键是如果数据需要拷贝 到多个缓冲时,指针地址是循环的。这时候如果手工完成操作很容易出错。 因此CYPRESS提供了两组自动指针,用的时候一组指向源,一组指向目的地址。然后循环拷贝数据就行了,自动指针会自动指 向下一个源或目的空间,不论是否是循环地址方式。这样减少了程序出错的几率。
下面的程序将EP2OUT接受到的数据拷贝到EP6IN发送出去:
if(!(EP2468STAT & bmEP6FULL)) {
// check EP6 FULL(busy) bit in EP2468STAT (SFR),
// core set's this bit when FIFO is full APTR1H = MSB( &EP2FIFOBUF );
APTR1L = LSB( &EP2FIFOBUF );
AUTOPTRH2 = MSB( &EP6FIFOBUF );
AUTOPTRL2 = LSB( &EP6FIFOBUF );
count = (EP2BCH << 8) + EP2BCL;
// loop EP2OUT buffer data to EP6IN
for( i = 0x0000; i < count; i++ ) {
// setup to transfer EP2OUT buffer to EP6IN buffer using AUTOPOINTER(s)
EXTAUTODAT2 = EXTAUTODAT1;
}
EP6BCH = EP2BCH;
SYNCDELAY;
EP6BCL = EP2BCL; // arm EP6IN
SYNCDELAY;
EP2BCL = 0x80; // re(arm) EP2OUT
}
APTR1H/APTR1H
通过MSB和LSB获取EP2FIFOBUF的高位地址和地位地址。
EXTAUTODAT1
表示APTR1H/APTR1H指向的数据。
AUTOPTRH2/AUTOPTRL2
通过MSB和LSB获取EP6FIFOBUF的高位地址和地位地址。
EXTAUTODAT2
表示AUTOPTRH2/AUTOPTRL2指向的数据。
以前68013上位机程序的编写过程中,应用程序端通过调用DeviceIoControl() API或CREATEPIPE() API与驱动进行交互,继而读写控制硬件设备,在新的68013A的驱动中采用了两种新的调用方法:
第一种
继续使用DeviceIoControl()函数读写,不同的是,IOCTL控制字和老驱动完全不同,具体定义参考CYUSB.PDF。用户可以通过这些底层API完成操作。
第二种
使用CYPRESS提供的面对对象的类,一共有9个类,调用这些类的方法就可以和硬件打交道。这些类是对
第一种方法的封装,使用起来非常简便。 用户可以根据需要选择这两种方法或混合使用,使用时需要加上头文件CyAPI.h和cyioctl.h,另外在项目中还要引用 CyAPI.lib。
CYAPI提供了同步和异步读写方式。
同步方式的时候调用线程阻塞在哪里,直到读写到数据或超时;
异步方式的时候调用线程 立即返回。具体实例参考CYAPI.PDF。
在WINDOWS 2000/XP上U盘使用的PID/VID应该直接能加载操作系统默认的海量存储器的驱动程序。为了使用正确的PID/VID,可以通过以下途径: