****************************************
* 在EASYARM2200和SMARTARM2200上实现U盘 *
****************************************
2008/02/01 asdjf@163.com www.armecos.com
一些《ecos增值包》用户来信要求提供EASYARM2200和SMARTARM2200开发板上的U盘源码,尽管这是一项非常成熟的技术,但网络上几乎没有一份详细完整的文档,大部分范例代码基于查询方式,而且存在比较多的问题,为了方便《ecos增值包》用户深入学习USB技术,我们提供ecos平台上实现的U盘完整源码和详细文档。这份资料不同于网上下载的零散资料,而是专门针对这两款开发板量身打造的,所以使用者可以无障碍地使用它在很短时间内掌握U盘开发技术。同时由于此产品基于ecos平台,用户修改代码和增加功能也易如反掌。
简单说,制作U盘需要实现三个方面的内容:1、USB设备驱动;2、存储介质驱动;3、MASS STORAGE大容量存储协议(bulk only + SCSI-2指令集)。至于FAT文件系统,由主机负责管理,U盘设备只需正确响应相应的SCSI-2指令即可。如下图所示:
USB设备 存储介质
主机 -----------
------------------- | |----RAM
| | USB | U盘 |----ROM
| 管理FAT文件系统 |<------>| 或 |----NAND FLASH
| | | 读卡器 |----CF卡
------------------ | |----SD卡
-----------
<---------------->
大容量存储协议
bulk only + SCSI-2
ecos本身提供了USB设备驱动和多种大容量存储介质驱动,大大减轻了我们的编程负担,我们既可以在RAM上虚拟可读写的U盘,在ROM上实现只读的假U盘,又可以在NAND FLASH上实现真正的U盘,当然,还可以在CF卡和SD卡上实现读卡器功能。
----------------------
| 大容量存储介质驱动 |
----------------------
ecos将所有设备抽象为设备文件,我们只要打开相关存储设备,然后读写就可以了,就是这么简单!没有必要再去折腾什么CF/SD卡驱动、ATA、SPI等底层细节,所有不同存储设备的编程界面都是一致的,大大节省了开发时间,当然,如果你对这些驱动感兴趣,《ecos增值包》的其他相关章节有详细文档和源代码说明,这里我们就直接使用设备驱动。
首先,打开存储设备文件:
cyg_io_lookup("/dev/hda/", &cf); //打开CF卡设备驱动,设备文件名为/dev/hda/,句柄保存在cf变量里。
如果需要读扇区,就调用块读(bread)函数:
cyg_io_bread(cf, buf, &len, pos);
cf保存已打开设备文件句柄,buf为读出数据缓冲区,len指示读出扇区数(注意不是字节数),pos指示起始位置(注意以扇区为单位)。
如果需要写扇区,就调用块写(bwrite)函数:
cyg_io_bwrite(cf, buf, &len, pos);
cf保存已打开设备文件句柄,buf为写入数据缓冲区,len指示写入扇区数(注意不是字节数),pos指示起始位置(注意以扇区为单位)。
怎么样,很简单吧!无论什么存储设备,都是“打开---读---写”这三步,就可以读写相应扇区了,根本不用去理会底层的细枝末节,而且接口高度抽象统一,更换一种存储介质仅需要改个设备文件名即可,其余部分一个字也不用动。这也是为什么我们能在很短时间内完成多种存储介质U盘设计的原因,毕竟只需要改个名字,再多存储介质也不怕,依此类推即可。
除了操作扇区,SCSI-2指令集还需要设备提供存储器容量(总的扇区数),CHS参数等信息。ecos的设备驱动也已经提供好了相关函数ide_ident()。
对于CF卡,设备识别ECH寄存器提供了相关信息:
typedef struct ide_identify_data_t_ {
cyg_uint16 general_conf; // 00 : general configuration
cyg_uint16 num_cylinders; // 01 : number of cylinders (default CHS trans)
cyg_uint16 reserved0; // 02 : reserved
cyg_uint16 num_heads; // 03 : number of heads (default CHS trans)
cyg_uint16 num_ub_per_track; // 04 : number of unformatted bytes per track
cyg_uint16 num_ub_per_sector; // 05 : number of unformatted bytes per sector
cyg_uint16 num_sectors; // 06 : number of sectors per track (default CHS trans)
cyg_uint16 num_card_sectors[2]; // 07-08 : number of sectors per card
cyg_uint16 reserved1; // 09 : reserved
cyg_uint16 serial[10]; // 10-19 : serial number (string)
cyg_uint16 buffer_type; // 20 : buffer type (dual ported)
cyg_uint16 buffer_size; // 21 : buffer size in 512 increments
cyg_uint16 num_ECC_bytes; // 22 : number of ECC bytes passed on R/W Long cmds
cyg_uint16 firmware_rev[4]; // 23-26 : firmware revision (string)
cyg_uint16 model_num[20]; // 27-46 : model number (string)
cyg_uint16 rw_mult_support; // 47 : max number of sectors on R/W multiple cmds
cyg_uint16 reserved2; // 48 : reserved
cyg_uint16 capabilities; // 49 : LBA, DMA, IORDY support indicator
cyg_uint16 reserved3; // 50 : reserved
cyg_uint16 pio_xferc_timing; // 51 : PIO data transfer cycle timing mode
cyg_uint16 dma_xferc_timing; // 52 : single word DMA data transfer cycle timing mode
cyg_uint16 cur_field_validity; // 53 : words 54-58 validity (0 == not valid)
cyg_uint16 cur_cylinders; // 54 : number of current cylinders
cyg_uint16 cur_heads; // 55 : number of current heads
cyg_uint16 cur_spt; // 56 : number of current sectors per track
cyg_uint16 cur_capacity[2]; // 57-58 : current capacity in sectors
cyg_uint16 mult_sectors; // 59 : multiple sector setting
cyg_uint16 lba_total_sectors[2]; // 60-61 : total sectors in LBA mode
cyg_uint16 sw_dma; // 62 : single word DMA support
cyg_uint16 mw_dma; // 63 : multi word DMA support
cyg_uint16 apio_modes; // 64 : advanced PIO transfer mode supported
cyg_uint16 min_dma_timing; // 65 : minimum multiword DMA transfer cycle
cyg_uint16 rec_dma_timing; // 66 : recommended multiword DMA cycle
cyg_uint16 min_pio_timing; // 67 : min PIO transfer time without flow control
cyg_uint16 min_pio_iordy_timing; // 68 : min PIO transfer time with IORDY flow control
// cyg_uint16 reserved4[187]; // 69-255: reserved
} ide_identify_data_t;
其中cylinders_num, heads_num, sectors_num是我们需要的CHS参数,lba_sectors_num是总容量。只要调用ide_ident()函数,就可以在传回的这个结构体变量里获得相关信息。
SD卡与此类似,从CSD和盘信息区里获得相关参数,详见源代码,此处不再赘述。
综上,我们实现了任意扇区读写,获得了CHS参数和总容量,OK!做U盘读卡器的信息量已经足够了。下面完成USB设备驱动和SCSI-2协议。
---------------
| USB设备驱动 |
---------------
此处说的USB设备驱动特指D12 driver,注意EASYARM2200和SMARTARM2200上都要使用D12 PACK小板,《ecos增值包》里已经提供了D12的USB设备驱动,用户需要做的只是提供枚举数据,很简单吧!
ecos下的D12驱动写得特别专业,不得不佩服ecos社区的专家,他们一直站在最前沿,不断跟踪技术发展,不象我们半路出家,很多事情也许根本就没有意识到,写出的程序,通用性和扩展性跟ecos社区的专家没法比。一般我们用的D12程序都是基于前后台,中断加死循环那种模式的,而ecos充分发挥了OS的特性,一下子提供三种运行模式:1、ISR+DSR模式;2、线程模式;3、轮询POLL模式。ISR+DSR模式能大大减少中断延迟,提高性能指标,我们原来在中断ISR内做所有事情的模式肯定不如在DSR中做大部分工作的中断性能好。前后台采用信号量方式进行同步不会白白浪费CPU时间片。依靠应用程序提供缓冲区而不是采用全局变量缓冲区能大大提高灵活性避免内存拷贝。应用程序不直接调用驱动,而是采用申请功能,完成函数唤醒的方式使用USB设备驱动,思路清晰,能实现流水线作业,减少空等时间。总之,ecos的D12驱动非常有特色,值得学习借鉴,具体使用方法在相关文档里有详细介绍,见《ecos增值包》之USB章节。
D12驱动有一些需要特别注意的问题:1、时序问题,因为ARM芯片速度可能比D12读写速度快,所以有必要在某些位置加延时,否则可能读写错数据,某些网上下载的程序使用比较慢的CPU如51,不会遇到此问题;2、D12的主端点采用双缓冲区64*2字节,注意要等待一段时间等双缓冲区填满后再访问,否则可能出现多读64字节或少读64字节的现象。有些网上下载的程序没有采用中断方式,不会遇到此问题;3、结构体定义要加“__attribute__((packed))”属性,否则得到的结构体不是紧凑的,可能为字节对齐加入了多余字节。
下面让我们用实际程序来解释一下如何写ecos下的USB设备驱动。
ecos采用一种非常灵活的方式组织枚举数据,一个枚举数据结构体包含了接口、端点、字符串的总数量,设备、配置、接口、端点、字符串的结构体信息。一个USB设备只有一个设备描述符,可能有多个配置描述符,所以,把接口、端点、字符串描述符存放在数组里,用指针指示,同时分别记录接口、端点、字符串描述符的个数。配置描述符也存放在数组里,不过不用记录个数,由应用程序负责解释。
usb_configuration_deor usb_configuration = {
length: USB_CONFIGURATION_DEOR_LENGTH,
type: USB_CONFIGURATION_DEOR_TYPE,
total_length_lo: 0x2E,
total_length_hi: 0,
number_interfaces: 1,
configuration_id: 1, // id 0 is special according to the spec
configuration_str: 0,
attributes: 0x60,
max_power: 0x32
};
usb_interface_deor usb_interface = {
length: USB_INTERFACE_DEOR_LENGTH,
type: USB_INTERFACE_DEOR_TYPE,
interface_id: 0,
alternate_setting: 0,
number_endpoints: 4,
interface_class: 0x08,
interface_subclass: 0x06,
interface_protocol: 0x50,
interface_str: 0
};
usb_endpoint_deor usb_endpoints[USBTEST_MAX_ENDPOINTS] = {
{
0x07,
0x05,
0x81,
0x03,
0x10, 0x00,
0x0a
},
{
0x07,
0x05,
0x01,
0x03,
0x10, 0x00,
0x0a
},
{
0x07,
0x05,
0x82,
0x02,
0x40, 0x00,
0x00
},
{
0x07,
0x05,
0x02,
0x02,
0x40, 0x00,
0x0a
},
};
const unsigned char* usb_strings[] = {
"\004\003\011\004", //4 3 9 4
"\012\003\032\000\030\000\037\000\031\000\030\000\039\000\038\000\032\000"
"\020\003R\000e\000d\000 \000H\000a\000t\000",
"\054\003R\000e\000d\000 \000H\000a\000t\000 \000e\000C\000o\000s\000 \000"
"U\000S\000B\000 \000t\000e\000s\000t\000"
};
usbs_enumeration_data usb_enum_data = {
{
length: USB_DEVICE_DEOR_LENGTH,
type: USB_DEVICE_DEOR_TYPE,
usb_spec_lo: USB_DEVICE_DEOR_USB11_LO,
usb_spec_hi: USB_DEVICE_DEOR_USB11_HI,
device_class: 0,
device_subclass: 0,
device_protocol: 0,
max_packet_size: 16,
vendor_lo: 0x71, // Note: this is not an allocated vendor id
vendor_hi: 0x04,
product_lo: 0xf0,
product_hi: 0xff,
device_lo: 0x01,
device_hi: 0x00,
manufacturer_str: 0,
product_str: 0,
serial_number_str: 2,
number_configurations: 1
},
total_number_interfaces: 1,
total_number_endpoints: 4,
total_number_strings: 3,
configurations: &usb_configuration,
interfaces: &usb_interface,
endpoints: usb_endpoints,
strings: usb_strings
};
......
control_endpoint->enumeration_data = &usb_enum_data;
usbs_start(control_endpoint);
如上所示,先准备出配置、接口、端点、字符串的结构体信息,再按上面写法准备出枚举数据(内含设备信息),各字段含义请读者自行对照协议解释。在主程序里填好枚举数据结构体地址,然后调用usbs_start()函数启动USB设备驱动即可。复杂的D12驱动细节完全由ecos代劳了,只需要用户准备好枚举数据,实现ecos下的USB设备驱动就是如此地简单明了!用户完全没有必要做那些重复工作,把精力集中到有价值的事情上。
------------------------------
| bulk only + SCSI-2协议实现 |
------------------------------
完成上面步骤后,U盘还不能被正确枚举,因为bulk only中有两种类特定请求命令需要响应:1、mass storage复位;2、获取最大逻辑单元号。
这可难不倒ecos的USB设备驱动。大家知道,设备请求类型分为四种:1、标准;2、类;3、厂商;4、其他。ecos里巧妙地实现了这四种类型的驱动。首先ecos提供标准请求的通用缺省处理,一般不用改动,因为所有USB设备都是应该这样处理的。当然如果用户有特殊要求,也可以替换掉ecos本身提供的缺省处理函数。对于其他三种类型,ecos自然不知道该如何处理,但它提供了通用程序框架,用户只需提供相应请求类型的处理回调函数和数据即可。
usbs_control_return yy_class_ctl_fn(struct usbs_control_endpoint* endp, void* data)
{
usbs_control_return result = USBS_CONTROL_RETURN_UNKNOWN;
usb_devreq* req = (usb_devreq*) endp->control_buffer;
int length;
int direction;
int recipient;
int i;
unsigned char* ch;
length = (req->length_hi << 8) | req->length_lo;
direction = req->type & USB_DEVREQ_DIRECTION_MASK;
recipient = req->type & USB_DEVREQ_RECIPIENT_MASK;
//在DSR里不能使用printf函数,否则死机。
//GET MAX LUN
if((req->type == 0xA1) && (req->request == 0xFE)){
endp->control_buffer[0] = 0;
endp->buffer = endp->control_buffer;
endp->buffer_size = 1;
endp->fill_buffer_fn = (void (*)(usbs_control_endpoint*)) 0;
endp->complete_fn = (usbs_control_return (*)(usbs_control_endpoint*, cyg_bool)) 0;
result = USBS_CONTROL_RETURN_HANDLED;
}
//Mass Storage Reset
if((req->type == 0x21) && (req->request == 0xFF)){
result = USBS_CONTROL_RETURN_HANDLED;
}
return result;
}
......
ep0->class_control_fn = yy_class_ctl_fn;
ep0->class_control_data = 0;
如上所示,在类请求回调函数里实现了GET MAX LUN和Mass Storage Reset两个特定类请求命令。GET MAX LUN回应0表示设备上只有一个逻辑单元。特别注意在这个回调函数里不能使用printf函数,因为此回调函数要在DSR上下文中运行,不能使用printf函数。
接下来要实现SCSI-2指令集,其实也没什么难度,就是回应请求罢了,都是些按部就班的重复劳动,可以用bus hound抓一个U盘的响应过程,然后照着实现即可。具体过程最好看源码,一目了然。这里大致介绍一下程序结构:
usbs_start_rx_buffer(endpoint_rx, buf[buf_flag], 512, &test_callback, (void*) &test);
if(cbw->dCBWSignature!=0x43425355){
diag_printf("CBW error!\n");
Return_CSW(0x00,FAIL,endpoint_tx);
continue;
}
if(cbw->bmCBWFlags&0x80){//IN
switch(cbw->CBWCB[0]){
case Read_10 : ......
case Inquiry : ......
case Read_Capacity : ......
case Read_Format_capacity : ......
case Request_Sense : ......
case Medium_Removal : ......
case Mode_Sense : ......
default : ......
}
}
else{//OUT
switch(cbw->CBWCB[0]){
case Write_10 : ......
case Test_Unit_Ready : ......
case Verify : ......
default : ......
}
}
由上可知,先接收有效CBW命令,然后区分传输方向,根据命令选取不同处理分支,处理结束发送CSW。其中完成各个命令的处理就实现了SCSI-2指令集。下面举例说明Read_10的实现:
case Read_10 :
//pos为起始块号,len为块数
pos = 0;
for(i = 0; i < 4; i++){
pos <<= 8;
pos += cbw->CBWCB[2+i];
}
len = 0;
len += cbw->CBWCB[7];
len <<= 8;
len += cbw->CBWCB[8];
tlen = 1;
for(i = 0; i < len; i++){//读出CF卡的某个扇区并发送出去,最后发送CSW。
cyg_io_bread(cf, &secbuf, &tlen, (pos + i));
usbs_start_tx_buffer(endpoint_tx, (unsigned char *)(secbuf), 512, &test_callback, (void*) &test);
cyg_semaphore_wait(&(test.sem));
}
Return_CSW(0x00,SUCCESS,endpoint_tx);
//printf("Read_10\n");
break;
其他命令处理与此类似,详见源程序。
注意:Inquiry命令的响应数据必须正确才能被允许格式化,有些网上代码的数据信息是不正确的,如果拿来就用可能导致无法格式化。如果没有把设备标明为可移动设备,主机就不会不断探测设备是否在位。好多不正常现象其实不是程序的错,而是数据配置错,犯这种错误实在太冤枉了。不过本程序是经过实际测试成功的,拿来就可以使用,改动也很容易,配置数据确保没有问题,请放心使用!本程序加上注释才700多行,如此短的程序就能完整实现U盘格式化、读写等功能,思路特别清晰,这说明《ecos增值包》确实是一个非常有效的快速验证平台,用它来学习也是不错的选择。《ecos增值包》用户可以得到相关源码和文档,所有人都可以免费下载演示版本以及此文档。把你的EASYARM2200和SMARTARM2200开发板改造成U盘/读卡器吧,这样又多了一个实用工具。
另外,如果对读U盘的主机感兴趣,请看“《ecos增值包》之USB HOST驱动篇”,里面详细介绍了USB HOST协议栈框架,是一个通用的USB主机实现,不光U盘,HID键盘鼠标,HUB驱动等都包括了,还可以方便地增加新的协议驱动。