依我的经验来看,协议方面的东东,随便找本书,过一遍就行了;然后,你的终点应该放在你如何来写第一个成功的USB固件;而要写USB固件,那么了解Cypress固件架构是必要的,也是重中之重;再然后,等你积累了一些端点,控制,bulk,中断传输,SlaveFIFO,GPIF等等的经验后,再回过头去看协议方面的内容,就会有更加深刻的体会了;然后,你就可以试着更改FW。c文件了——这个时候你就是高手了。
【1】:首先,还是说下工具吧!
1:你要有一块68013的USB板子,淘宝随便买块好了,还送不少资料。
2:要准备开发工具,去Cypress官网下一个Cy3684的开发包,全称:
cy3684_ez_usb_fx2lp_development_kit_15.exe
网址:http://www.cypress.com/?rID=14321
3.安装开发包。工具就是Cypress USB Console了。怎么用不用我说了吧,这偏文章主题是
Cypress固件架构.
【2】好了,进入主题,固件架构(汗--!好多废话)。以一个3684开发包自带的例子讲解。
(1) 进入目录(个人找自己的):D:\Program Files\Cypress\USB\Examples\FX2LP\Bulkloop,
(2)到D:\Program Files\Cypress\USB\Target文件价下把Cypress头文件Fx2.h,fx2regs.h,syncdly.h这三个头文件拷到bulkloop文件夹里。
(3)keil设置output里关掉Run User Program #1(前面的勾去掉)(别说不会用keil。。。)
(4)检查keil C51文件路径是否正确。(如果你的keil是直接装载C:\Keil。。下,那不会有错误,否则,自行设置正确的路径。)
【3】现在可以打开bulkloop工程并编译链接正确了。
在工程下,有以下几个文件:,其中,USBJmp.OBJ,EZUSB.LIB基本上是每个工程都要添加的,是一些中断向量表,EZUSB的函数库等等,不用管它们。
现在重点看前面三个文件:
(1)fw.c:这个文件是整个USB的固件根本(FirmWare的缩写),USB协议方面的通信都是在这里完成的,包括上电枚举,重枚举,唤醒以及调用用户自己的程序和控制命令等等。基本上,如非必要,尽量不要动这个文件的内容,也不要在里面书写你自己的任何代码。
(2)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:这几个函数是当使用端点中断传输时,中断代码的书写处,很少用。
以上,是经常会用到的几个函数;其他,基本不常用。
(4):dscr.51:这个文件是USB描述符文件,包括了设备描述符,接口描述符,端点描述符,字符串等等。里面的英文都注释得很详细了,我就不多做介绍了,刚开始入门的时候,这个文件也不必改动。
【4】然后说下几个包含文件:
有这几个:
(1):fx2.h:预定义,宏及函数声明
(2):fx2regs.h:68013的寄存器地址定义。
(3):syncdly.h:同步延时。在其他文件里经常调用的一个函数SYNCDELAY就是这里定义的。
(4):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缓冲区。数据流向为:
◆PC端(console软件)设定要传输的数据
◆PC端发起端点2out传输,数据到达68013端点2的out缓冲区
◆bulkloop固件查询到端点2的out缓冲区有数据,于是将数据发往端点6的IN缓冲区
◆PC端发起端点6 IN传输,于是68013的端点6 IN缓冲区中的数据被读到PC机显示
这就是整个bulkloop过程,EP4->EP8同理。
然后看固件,看一个固件总是从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对该寄存器的解释,b7(第8位)=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
for( i = 0x0000; i < count; i++ ) //传送count字节
{
// 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();
紧接着调用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?)。
// 重定向描述符
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事务处理:
while(TRUE) // Main Loop主循环
{
// Poll User Device 用户调度程序
TD_Poll();
// Check for pending SETUP
if(GotSUD) //等待SETUP令牌数据的到来
{
SetupCommand(); // Implement setup command //处理SETUP事务
GotSUD = FALSE; // Clear SETUP flag 清Setup标志
}
TD_Poll,也就是用户调度程序,USB空闲时调用,不过为什么要放在开始呢?照理说应该放在令牌包后面的,这里不是很明白。。。。。。
if(GotSUD),GotSUD是令牌包标志,准确的说是“令牌阶段数据到来”,什么是令牌包?
首先,USB一连串的数据传输、处理、响应等就叫做USB事务。例如,上位机要读取一个描述符,那么就会触发一次USB事务。一个完整的USB事务处理有三个阶段:令牌阶段,数据阶段,握手阶段。每个阶段数据传输是有各种包组成的,例如令牌阶段:同步字段+令牌包+EOP构成。
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。
if(TD_Suspend())
检测USB是否挂起
{
Sleep = FALSE; //清Sleep标志
do
{
EZUSB_Susp(); // 置8051为空闲状态.
}
while(!Rwuen && EZUSB_EXTWAKEUP());//如果唤醒
EZUSB_Resume(); //从空闲状态中恢复
TD_Resume();
到这里,一个事务处理完毕,等待下次事务处理中断的到来.
To Be Continue...