由于众所周知的原因,这几天宅在家里闲来无事(毕业论文:?那我呢),刚好这次回家带了个CH558T开发板(主要是体积小方便带),于是乎研究了一下USB功能。
刚好一直以来对USB协议挺感兴趣,所以这次没有参考官方demo,而是照着datasheet自己敲了个USB协议栈,调试了两天终于能用了,来坑网分享一下源代码核相关的调试经验。
目前实现的功能是对多种不同的传输方式组合进行速度测试,在保证性能的同时发现一些datasheet没有提及的问题,需要配合上位机进行测试。
当然,鼓捣这玩意儿还是有目的的,不过现在先卖个关子(否则到时咕掉了就不好意思了)。目前看来,CH552的资源和性能可能稍微有些不够用,CH558挺好的,虽然调试确实有点头疼
离线
先晒一个调试组合,逻辑分析仪+CH558T最小系统板,随身携带不碍事儿~
有一说一,对于USB协议的分析真的是需要专门的工具(特别是不带驱动的裸机开发),全速设备尚且可以拿普通的逻辑分析仪凑合一下,高速以上的设备还是需要更专业的协议分析仪,可以少走很多弯路(很多底层的错误是没办法在软件层面发现的)。
另外,CH55x芯片虽然性价比高,但是调试一直是个老大难问题,不仅没有专用的调试接口(基本靠GPIO,串口偶尔能用,ISD51和USB八字不合就不要想了),而且低端的CH551/2/4还有烧写次数的限制(200次,所以这也就是我买CH558的原因,能折腾),烧写还非常麻烦(我是基本靠USB,必须按着按键上电才行,通电时重启都不好使)。为此,我DIY了一根带开关的USB信号线(某宝有这玩意,但是快递已经基本停业了),勉强还能用吧。后面考虑一下用单片机做个简单的烧录器,通过按键时间长短决定是否进入bootloader。
离线
下面分享一下代码。由于实现了比较完整的协议栈(虽然没有Class,不过Request基本都实现了),所以代码体积略大,不过编译后也不到2K。
代码经过官方工具USB3CV的验证(Test All Pass),所以大概率不会有很大问题,就当献丑了。
源程序是在Keil编译的,后面有时间再尝试一下SDCC。头文件CH558.H需要自备。
#include "../CH558.H"
#include <string.h>
#define bMaxPacketSize0 64
#define wMaxPacketSize 64
#define MIN(x, y) ((x) < (y) ? (x) : (y))
#define HI(x) ((x) >> 8)
#define LO(x) ((x) & 0xFF)
#define WBVAL(x) LO(x), HI(x)
#define UIS_EP_IN(x) (UIS_TOKEN_IN | (x))
#define UIS_EP_OUT(x) (UIS_TOKEN_OUT | (x))
#define EP_IN(x) (USB_ENDP_DIR_MASK | (x))
#define EP_OUT(x) (x)
UINT8C DeviceDescriptor[] = {
18, // bLength
1, // bDescriptorType, DEVICE
WBVAL(0x0200), // bcdUSB
0x00, // bDeviceClass
0x00, // bDeviceSubClass
0x00, // bDeviceProtocol
bMaxPacketSize0, // bMaxPacketSize0
WBVAL(0x04B4), // idVendor
WBVAL(0x1003), // idProduct
WBVAL(0x0000), // bcdDevice
1, // iManufacturer
2, // iProduct
0, // iSerialNumber
1 // bNumConfigurations
};
UINT8C ConfigurationDescriptor[] = {
9, // bLength
2, // bDescriptorType, CONFIGURATION
WBVAL(74), // wTotalLength
1, // bNumInterfaces
1, // bConfigurationValue
0, // iConfiguration
0x80, // bmAttributes
250, // bMaxPower
// Interface 0
9, // bLength
4, // bDescriptorType, INTERFACE
0, // bInterfaceNumber
0, // bAlternateSetting
8, // bNumEndpoints
0xFF, // bInterfaceClass
0x00, // bInterfaceSubClass
0x00, // bInterfaceProtocol
0, // iInterface
// Endpoint 1 IN
7, // bLength
5, // bDescriptorType, ENDPOINT
0x81, // bEndpointAddress
0x02, // bmAttributes
WBVAL(wMaxPacketSize), // wMaxPacketSize
0, // bInterval
// Endpoint 1 OUT
7, // bLength
5, // bDescriptorType, ENDPOINT
0x01, // bEndpointAddress
0x02, // bmAttributes
WBVAL(wMaxPacketSize), // wMaxPacketSize
0, // bInterval
// Endpoint 2 IN
7, // bLength
5, // bDescriptorType, ENDPOINT
0x82, // bEndpointAddress
0x02, // bmAttributes
WBVAL(wMaxPacketSize), // wMaxPacketSize
0, // bInterval
// Endpoint 2 OUT
7, // bLength
5, // bDescriptorType, ENDPOINT
0x02, // bEndpointAddress
0x02, // bmAttributes
WBVAL(wMaxPacketSize), // wMaxPacketSize
0, // bInterval
// Endpoint 3 IN
7, // bLength
5, // bDescriptorType, ENDPOINT
0x83, // bEndpointAddress
0x02, // bmAttributes
WBVAL(wMaxPacketSize), // wMaxPacketSize
0, // bInterval
// Endpoint 3 OUT
7, // bLength
5, // bDescriptorType, ENDPOINT
0x03, // bEndpointAddress
0x02, // bmAttributes
WBVAL(wMaxPacketSize), // wMaxPacketSize
0, // bInterval
// Endpoint 4 IN
7, // bLength
5, // bDescriptorType, ENDPOINT
0x84, // bEndpointAddress
0x02, // bmAttributes
WBVAL(wMaxPacketSize), // wMaxPacketSize
0, // bInterval
// Endpoint 4 OUT
7, // bLength
5, // bDescriptorType, ENDPOINT
0x04, // bEndpointAddress
0x02, // bmAttributes
WBVAL(wMaxPacketSize), // wMaxPacketSize
0 // bInterval
};
UINT8C LanguageString[] = {
4, // bLength
3, // bDescriptorType, STRING
WBVAL(0x0409)
};
UINT8C ManufacturerString[] = {
9, // bLength
3, // bDescriptorType, STRING
'C', 'y', 'p', 'r', 'e', 's', 's'
};
UINT8C ProductString[] = {
11, // bLength
3, // bDescriptorType, STRING
'C', 'Y', '-', 'S', 't', 'r', 'e', 'a', 'm'
};
PUINT8C StringDescriptors[] = {
LanguageString,
ManufacturerString,
ProductString
};
#define STR_DESC_NUM (sizeof(StringDescriptors) / sizeof(PUINT8C))
static UINT8XV EP0Buf[64] _at_ 0x0;
static UINT8XV EP4Buf[2][64] _at_ 0x40;
static UINT8XV EP1Buf[4][64] _at_ 0x100;
static UINT8XV EP2Buf[4][64] _at_ 0x200;
static UINT8XV EP3Buf[2][64] _at_ 0x300;
static UINT8XV unused[64] _at_ 0x400;
struct {
UINT8 req;
UINT16 len;
PUINT8C desc;
} data setup;
#define SetupPacket ((const USB_SETUP_REQ xdata *)EP0Buf)
#define GET_CONFIGURED() (USB_DEV_AD & bUDA_GP_BIT)
#define SET_CONFIGURED() (USB_DEV_AD |= bUDA_GP_BIT)
#define CLR_CONFIGURED() (USB_DEV_AD &= ~bUDA_GP_BIT)
void Clock_Initialize(void)
{
SAFE_MOD = 0x55;
SAFE_MOD = 0xAA;
PLL_CFG = 7 << 5 | 28 << 0;
CLOCK_CFG = bOSC_EN_INT | 6 << 0;
SLEEP_CTRL = bSLP_OFF_USB | bSLP_OFF_ADC | bSLP_OFF_UART1 | bSLP_OFF_SPI0 | bSLP_OFF_TMR3 | bSLP_OFF_LED;
++SAFE_MOD;
}
void Port_Initialize(void)
{
PORT_CFG &= ~bP2_OC;
TNOW = 0;
P2_PU &= ~bTNOW;
P2_DIR |= bTNOW;
}
void USBD_Initialize(void)
{
TNOW = 1;
SAFE_MOD = 0x55;
SAFE_MOD = 0xAA;
SLEEP_CTRL &= ~bSLP_OFF_USB;
WAKE_CTRL |= bWAK_BY_USB;
++SAFE_MOD;
USB_CTRL = bUC_RESET_SIE | bUC_CLR_ALL;
USB_CTRL = bUC_DEV_PU_EN | bUC_INT_BUSY | bUC_DMA_EN;
UDEV_CTRL = bUD_DP_PD_DIS | bUD_DM_PD_DIS | bUD_PORT_EN;
UEP0_DMA = (UINT16)EP0Buf;
UEP1_DMA = (UINT16)EP1Buf;
UEP2_DMA = (UINT16)EP2Buf;
UEP3_DMA = (UINT16)EP3Buf;
// UEP4_DMA = UEP0_DMA + 64
UEP4_1_MOD = bUEP1_RX_EN | bUEP1_TX_EN | bUEP1_BUF_MOD | bUEP4_RX_EN | bUEP4_TX_EN;
UEP2_3_MOD = bUEP3_RX_EN | bUEP3_TX_EN | bUEP2_RX_EN | bUEP2_TX_EN | bUEP2_BUF_MOD;
USB_INT_EN = bUIE_TRANSFER | bUIE_BUS_RST;
USB_INT_FG = 0xFF;
IE_USB = 1;
TNOW = 0;
}
void USBD_ISR(void) interrupt INT_NO_USB using 1
{
TNOW = 1;
if (UIF_TRANSFER) {
if (U_TOG_OK)
switch (USB_INT_ST & (MASK_UIS_TOKEN | MASK_UIS_ENDP)) {
case UIS_EP_IN(1):
break;
case UIS_EP_OUT(1):
break;
case UIS_EP_IN(2):
memcpy((PUINT8XV)USB_DMA, unused, wMaxPacketSize);
break;
case UIS_EP_OUT(2):
memcpy(unused, (PUINT8XV)USB_DMA, USB_RX_LEN);
break;
case UIS_EP_IN(3):
break;
case UIS_EP_OUT(3):
break;
case UIS_EP_IN(4):
memcpy((PUINT8XV)USB_DMA, unused, wMaxPacketSize);
UEP4_CTRL ^= bUEP_T_TOG;
break;
case UIS_EP_OUT(4):
memcpy(unused, (PUINT8XV)USB_DMA, USB_RX_LEN);
UEP4_CTRL ^= bUEP_R_TOG;
break;
case UIS_EP_IN(0):
if ((setup.req & 0x80) == USB_REQ_TYP_IN) {
if ((setup.req & 0x7F) == USB_GET_DESCRIPTOR) {
UEP0_T_LEN = (UINT8)MIN(setup.len, bMaxPacketSize0);
memcpy(EP0Buf, setup.desc, UEP0_T_LEN);
setup.desc += UEP0_T_LEN;
setup.len -= UEP0_T_LEN;
UEP0_CTRL ^= bUEP_T_TOG;
}
} else {
// Status Stage finished
if ((setup.req & 0x7F) == USB_SET_ADDRESS)
USB_DEV_AD = *(UINT8D *)&setup.len;
setup.req = 0xFF;
UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
}
break;
case UIS_EP_OUT(0):
if ((setup.req & 0x80) == USB_REQ_TYP_OUT) {
// Not possible unless SET_DESCRIPTOR
UEP0_CTRL ^= bUEP_R_TOG;
} else {
// Status Stage finished
setup.req = 0xFF;
UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
}
break;
case UIS_TOKEN_SETUP:
// Avoid STALL because of slow Setup Packet handling
UEP0_CTRL = UEP0_CTRL & ~MASK_UEP_T_RES | UEP_T_RES_NAK;
setup.req = SetupPacket->bRequestType & 0x80 | SetupPacket->bRequest;
switch (SetupPacket->bRequest) {
case USB_GET_STATUS:
if ((SetupPacket->bRequestType & USB_REQ_RECIP_MASK) == USB_REQ_RECIP_DEVICE ||
(SetupPacket->bRequestType & USB_REQ_RECIP_MASK) == USB_REQ_RECIP_INTERF && SetupPacket->wIndexL == 0) {
EP0Buf[0] = 0x0;
} else if ((SetupPacket->bRequestType & USB_REQ_RECIP_MASK) == USB_REQ_RECIP_ENDP) {
if (!GET_CONFIGURED()) {
if (SetupPacket->wIndexL == 0)
EP0Buf[0] = 0x0;
else
goto REQUEST_ERROR;
} else {
switch (SetupPacket->wIndexL) {
case EP_IN(0):
case EP_OUT(0):
EP0Buf[0] = 0x0;
break;
case EP_IN(1):
EP0Buf[0] = !!((UEP1_CTRL & MASK_UEP_T_RES) == UEP_T_RES_STALL);
break;
case EP_OUT(1):
EP0Buf[0] = !!((UEP1_CTRL & MASK_UEP_R_RES) == UEP_R_RES_STALL);
break;
case EP_IN(2):
EP0Buf[0] = !!((UEP2_CTRL & MASK_UEP_T_RES) == UEP_T_RES_STALL);
break;
case EP_OUT(2):
EP0Buf[0] = !!((UEP2_CTRL & MASK_UEP_R_RES) == UEP_R_RES_STALL);
break;
case EP_IN(3):
EP0Buf[0] = !!((UEP3_CTRL & MASK_UEP_T_RES) == UEP_T_RES_STALL);
break;
case EP_OUT(3):
EP0Buf[0] = !!((UEP3_CTRL & MASK_UEP_R_RES) == UEP_R_RES_STALL);
break;
case EP_IN(4):
EP0Buf[0] = !!((UEP4_CTRL & MASK_UEP_T_RES) == UEP_T_RES_STALL);
break;
case EP_OUT(4):
EP0Buf[0] = !!((UEP4_CTRL & MASK_UEP_R_RES) == UEP_R_RES_STALL);
break;
default:
goto REQUEST_ERROR;
}
}
} else {
goto REQUEST_ERROR;
}
EP0Buf[1] = 0x0;
UEP0_T_LEN = 2;
UEP0_CTRL = bUEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
break;
case USB_CLEAR_FEATURE:
switch (SetupPacket->wValueL) {
case 0: // ENDPOINT_HALT
if (!GET_CONFIGURED())
goto REQUEST_ERROR;
switch (SetupPacket->wIndexL) {
case EP_IN(1):
UEP1_CTRL = UEP1_CTRL & ~(MASK_UEP_T_RES | bUEP_T_TOG) | UEP_T_RES_ACK;
break;
case EP_OUT(1):
UEP1_CTRL = UEP1_CTRL & ~(MASK_UEP_R_RES | bUEP_R_TOG) | UEP_R_RES_ACK;
break;
case EP_IN(2):
UEP2_CTRL = UEP2_CTRL & ~(MASK_UEP_T_RES | bUEP_T_TOG) | UEP_T_RES_ACK;
break;
case EP_OUT(2):
UEP2_CTRL = UEP2_CTRL & ~(MASK_UEP_R_RES | bUEP_R_TOG) | UEP_R_RES_ACK;
break;
case EP_IN(3):
UEP3_CTRL = UEP3_CTRL & ~(MASK_UEP_T_RES | bUEP_T_TOG) | UEP_T_RES_ACK;
break;
case EP_OUT(3):
UEP3_CTRL = UEP3_CTRL & ~(MASK_UEP_R_RES | bUEP_R_TOG) | UEP_R_RES_ACK;
break;
case EP_IN(4):
UEP4_CTRL = UEP4_CTRL & ~(MASK_UEP_T_RES | bUEP_T_TOG) | UEP_T_RES_ACK;
break;
case EP_OUT(4):
UEP4_CTRL = UEP4_CTRL & ~(MASK_UEP_R_RES | bUEP_R_TOG) | UEP_R_RES_ACK;
break;
default:
goto REQUEST_ERROR;
}
UEP0_T_LEN = 0;
UEP0_CTRL = bUEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
break;
case 1: // DEVICE_REMOTE_WAKEUP
case 2: // TEST_MODE
default:
goto REQUEST_ERROR;
}
break;
case USB_SET_FEATURE:
switch (SetupPacket->wValueL) {
case 0: // ENDPOINT_HALT
if (!GET_CONFIGURED())
goto REQUEST_ERROR;
switch (SetupPacket->wIndexL) {
case EP_IN(1):
UEP1_CTRL = UEP1_CTRL & ~MASK_UEP_T_RES | UEP_T_RES_STALL;
break;
case EP_OUT(1):
UEP1_CTRL = UEP1_CTRL & ~MASK_UEP_R_RES | UEP_R_RES_STALL;
break;
case EP_IN(2):
UEP2_CTRL = UEP2_CTRL & ~MASK_UEP_T_RES | UEP_T_RES_STALL;
break;
case EP_OUT(2):
UEP2_CTRL = UEP2_CTRL & ~MASK_UEP_R_RES | UEP_R_RES_STALL;
break;
case EP_IN(3):
UEP3_CTRL = UEP3_CTRL & ~MASK_UEP_T_RES | UEP_T_RES_STALL;
break;
case EP_OUT(3):
UEP3_CTRL = UEP3_CTRL & ~MASK_UEP_R_RES | UEP_R_RES_STALL;
break;
case EP_IN(4):
UEP4_CTRL = UEP4_CTRL & ~MASK_UEP_T_RES | UEP_T_RES_STALL;
break;
case EP_OUT(4):
UEP4_CTRL = UEP4_CTRL & ~MASK_UEP_R_RES | UEP_R_RES_STALL;
break;
default:
goto REQUEST_ERROR;
}
UEP0_T_LEN = 0;
UEP0_CTRL = bUEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
break;
case 1: // DEVICE_REMOTE_WAKEUP
case 2: // TEST_MODE
default:
goto REQUEST_ERROR;
}
break;
case USB_SET_ADDRESS:
// USB Address should be changed after handshake packet, so temporarily kept in len
*(UINT8D *)&setup.len = SetupPacket->wValueL;
UEP0_T_LEN = 0;
UEP0_CTRL = bUEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
break;
case USB_GET_DESCRIPTOR:
setup.len = (UINT16)SetupPacket->wLengthL << 0 | (UINT16)SetupPacket->wLengthH << 8;
switch (SetupPacket->wValueH) {
case USB_DESCR_TYP_DEVICE:
if (SetupPacket->wValueL > 0)
goto REQUEST_ERROR;
setup.desc = DeviceDescriptor;
if (setup.desc[0] < setup.len)
setup.len = setup.desc[0];
break;
case USB_DESCR_TYP_CONFIG:
if (SetupPacket->wValueL > 0)
goto REQUEST_ERROR;
setup.desc = ConfigurationDescriptor;
// Assuming wTotalLength < 256
if (setup.desc[2] < setup.len)
setup.len = setup.desc[2];
break;
case USB_DESCR_TYP_STRING:
if (SetupPacket->wValueL >= STR_DESC_NUM)
goto REQUEST_ERROR;
setup.desc = StringDescriptors[SetupPacket->wValueL];
if (setup.desc[0] < setup.len)
setup.len = setup.desc[0];
break;
case USB_DESCR_TYP_INTERF:
case USB_DESCR_TYP_ENDP:
case USB_DESCR_TYP_QUALIF:
case USB_DESCR_TYP_SPEED:
default:
goto REQUEST_ERROR;
}
UEP0_T_LEN = (UINT8)MIN(setup.len, bMaxPacketSize0);
memcpy(EP0Buf, setup.desc, UEP0_T_LEN);
setup.desc += UEP0_T_LEN;
setup.len -= UEP0_T_LEN;
UEP0_CTRL = bUEP_R_TOG | bUEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
break;
case USB_GET_CONFIGURATION:
EP0Buf[0] = !!GET_CONFIGURED();
UEP0_T_LEN = 1;
UEP0_CTRL = bUEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
break;
case USB_SET_CONFIGURATION:
switch (SetupPacket->wValueL) {
case 0:
CLR_CONFIGURED();
UEP1_CTRL = bUEP_AUTO_TOG | UEP_R_RES_STALL | UEP_T_RES_STALL;
UEP2_CTRL = bUEP_AUTO_TOG | UEP_R_RES_STALL | UEP_T_RES_STALL;
UEP3_CTRL = bUEP_AUTO_TOG | UEP_R_RES_STALL | UEP_T_RES_STALL;
UEP4_CTRL = UEP_R_RES_STALL | UEP_T_RES_STALL;
break;
case 1:
SET_CONFIGURED();
UEP1_T_LEN = wMaxPacketSize;
UEP1_CTRL = bUEP_AUTO_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
UEP2_T_LEN = wMaxPacketSize;
UEP2_CTRL = bUEP_AUTO_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
UEP3_T_LEN = wMaxPacketSize;
UEP3_CTRL = bUEP_AUTO_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
UEP4_T_LEN = wMaxPacketSize;
UEP4_CTRL = UEP_R_RES_ACK | UEP_T_RES_ACK;
break;
default:
goto REQUEST_ERROR;
}
UEP0_T_LEN = 0;
UEP0_CTRL = bUEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
break;
case USB_GET_INTERFACE:
if (!GET_CONFIGURED() || SetupPacket->wIndexL != 0)
goto REQUEST_ERROR;
EP0Buf[0] = 0;
UEP0_T_LEN = 1;
UEP0_CTRL = bUEP_T_TOG | UEP_R_RES_ACK | UEP_T_RES_ACK;
break;
case USB_SET_DESCRIPTOR:
// Unsupported, STALL at Status Stage
UEP0_CTRL = bUEP_R_TOG | UEP_R_RES_ACK | UEP_T_RES_STALL;
break;
case USB_SET_INTERFACE: // STALL to use default alt settings
case USB_SYNCH_FRAME:
default:
REQUEST_ERROR:
UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_STALL;
}
}
}
if (UIF_BUS_RST) {
UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_NAK;
UEP1_CTRL = bUEP_AUTO_TOG | UEP_R_RES_STALL | UEP_T_RES_STALL;
UEP2_CTRL = bUEP_AUTO_TOG | UEP_R_RES_STALL | UEP_T_RES_STALL;
UEP3_CTRL = bUEP_AUTO_TOG | UEP_R_RES_STALL | UEP_T_RES_STALL;
UEP4_CTRL = UEP_R_RES_STALL | UEP_T_RES_STALL;
USB_DEV_AD = 0x00;
}
USB_INT_FG = 0xFF;
TNOW = 0;
}
void main(void)
{
Clock_Initialize();
Port_Initialize();
USBD_Initialize();
EA = 1;
for (;;) {
if (USB_MIS_ST & bUMS_SUSPEND)
PCON |= PD;
}
}
离线
大概解释一下程序的功能吧。
如上文所述,这是个USB性能测试程序。匹配的上位机程序实际上是来自于Cypress FX2(也就是大名鼎鼎的CY7C68013A的所属系列),名为CyStreamer。该程序有配套的CY7C68013A固件,如果买了相关开发板的可以直接烧录并测试。
本身测试程序的功能比较简单,就是扫描除了EP0外的可用Endpoints(包括Altenate Settings),并且通过模拟传输的方式测试速度(包括IN和OUT);另外,还通过P2.5(TNOW)输出当前USB的状态,在调用USBD_Initialize和USBD_ISR时将会置高以表示处于活动状态。该程序并没有包括回环测试(loopback),我个人认为loopback更适合用CDC类作为虚拟串口的方式实现,而这在官方demo中已经实现了。
当然,为了使用Cypress的程序,这里需要把VID和PID改一改,这样才能用上官方的驱动,从而可被CyUSB.dll调用。其它描述符也尽可能照着官方固件的改了。
写这个程序的目的呢,除了实现USB协议之外,其实也有探究CH55x的USB实现的想法。所以,这里设置了4套端点(IN和OUT都有),分别对应以下几种情况:
EP1 IN/OUT:双缓冲,ISR中无复制
EP2 IN/OUT:双缓冲,ISR中有复制(使用memcpy,下同)
EP3 IN/OUT:单缓冲,ISR中无复制
EP4 IN/OUT:单缓冲,ISR中有复制
首先说说双缓冲机制。很明显,这是为了提升传输速度用的:毕竟这种级别的单片机主频不太高且执行周期长,移动速度未必比USB FS本身的读写快多少,如果没有缓冲的话,复制就要占据大部分时间,从而在复制完成前没办法准备下一阶段的传输,这通常意味着下一次传输通常只能NAK。对于没有PING机制的全速设备而言,Bulk OUT尤其是问题(毕竟需要传输一整个Packet后才能答复NAK)。双缓冲机制可以通过允许提前准备下一阶段的传输,从而比较有效地解决这个问题。
那么,在单缓冲的情况下,要实现数据的搬移,就不得不和ISR扯上关系了——要么直接在ISR中复制数据,要么直接在ISR中置为NAK,等到正常执行的阶段准备好下一阶段的传输之后再置为ACK,而后者肯定比前者更慢。因此,EP1和EP4应该是最为常用的两种情况,而EP2和EP3则是作为对照组存在。
当然,如果ISR的处理速度够快(通常中断都是在本次传输结束后开始的,因此对应的是本次传输结束到下次传输开始,当然理论上下次传输的Token Phase时也是来得及操作的),其实还有一种方案:自己维护缓冲区(通常是环形缓冲),端点缓冲只使用单缓冲,并且在ISR中切换当前缓冲。下面的测试将会看到,即使是CH558这种级别的单片机,也是完全来得及的。
最近编辑记录 metro (2020-01-27 00:18:38)
离线
下文将会按顺序说一下调试CH558T时踩到的坑。
首先,时钟和休眠之类的倒还好(除了一开始忘了SAFE_MOD这回事儿懵逼了半天)。为了支持USB Suspend,需要在WAKE_CTRL开启USB唤醒,不过可以不用在中断里面开启Suspend,在我的代码里面是直接在loop部分判断bUMS_SUSPEND了,这样的好处是保证在当前不活动的情况下再进入休眠模式,不会影响当前的工作。
USB SIE的重置是一个小坑。原本我是在USB接收到Reset中断时对SIE进行重置(USB_CTRL = bUC_RESET_SIE | bUC_CLR_ALL,当然还有相关的初始化),但是发现这样处理之后SIE就会进入无响应状态,可能是因为重置需要一定时间。将重置改成只在Initialize阶段进行就解决了。
离线
昨天太晚了,起床继续。
接下来说一说一个很坑的点:UIS_TRANSFER和U_IS_NAK。
在datasheet中,只提到了UIS_TRANSFER表示传输完成,而U_IS_NAK表示接收到NAK。但是,众所周知,Host并不会发送NAK,所以这里应该是用设备发送NAK更为准确?另外,这个传输完成到底是什么意思?NAK和STALL算传输完成吗?无响应呢?
带着这些问题,我测试了一下各种搭配,结果令人震惊(夸张了哈):
先说U_IS_NAK,很明显是在发送NAK的时候会置1,这倒没啥好惊讶的。当然,要实现这个功能,就应该事先置bUIE_DEV_NAK为1。
比较令人困惑的是UIS_TRANSFER。在bUIE_DEV_NAK为0时,只有接收/发送ACK的时候置1,NAK和STALL没有反应(没有测试无响应,猜测和STALL一致);但是当bUIE_DEV_NAK为1时,如果接收/发送NAK,UIS_TRANSFER也会置1(STALL仍然没反应)。这不得不说是比较有趣的行为。
另外,在bUIE_DEV_NAK为1,且对应端点的bUEP_AUTO_TOG为1时,如果使用设备的接收功能(对应Bulk OUT)会出现奇怪的bug,表现是U_IS_NAK没有正确设置,且自动切换TOG的功能错误(反应就是U_TOG_OK的值错误),如下图所示。但是,只要上述两个flag的任意一个为0(不使能NAK中断,或是手动切换TOG),则功能又恢复正常。而且,这个bug并不影响Bulk IN,或是没有发送NAK的情形。这一点也是令人困惑。
鉴于以上原因,在使用CH558的USB功能时,我建议:
不要使用U_IS_NAK,也就是置bUIE_DEV_NAK为0。一般来说,对于NAK我们也不需要进行特殊的操作,而是让它重新传输一次就行了。
在不打开bUIE_DEV_NAK的时候,自动切换TOG还是好用的,当然手动切换也并不麻烦,只需要异或一下TOG就好了。
由于STALL信号不触发中断,因此对于Control Transfer而言,不能在Token为OUT(Host到Device方向)的时候设置Device为回复STALL,否则无法正确接收到下一个Setup包。正确的选择是,在需要表明Request Error的情况下,对于Control Read应当在Data Stage阶段回复STALL,而对于Control Write则只能在Status Stage阶段回复STALL。也就是说,只能令UEP0_CTRL = UEP_R_RES_ACK | UEP_T_RES_STALL,而不能同时使用UEP_R_RES_STALL。
另外,在UEP0_CTRL的STALL设置优先级高于bUC_INT_BUSY发出的NAK,也就是说即使bUC_INT_BUSY为1,在ISR中仍然会优先发送STALL信号。由于STALL信号仅需发送一个周期(下一个周期应当恢复到Setup包),而Setup包本身的处理逻辑又十分复杂,在发送STALL表明Request Error之后(典型例子是全速设备的Device_Qualifier),如果接收到Setup包(对于SETUP Token正确回复ACK)并且按原有路径在ISR中处理Setup包,则会因为当前Setup包的处理时间过长,从而对于当前的Request错误地回复STALL(也就是说在ISR中已经经历了Setup和Data两个Stage),造成逻辑错误。对此,解决方案是在接收到Setup包时第一步先将UEP0_CTRL的STALL去掉(见case UIS_TOKEN_SETUP下一行),之后正常处理便是(bUC_INT_BUSY在此时会正确地回复NAK)。
最近编辑记录 metro (2020-01-27 11:38:14)
离线
最后说说CH558的USB缓冲机制。
经过测试发现,CH558实现的USB缓冲机制是最怂的方案:不管是否开了双缓冲,也不管发送/接收是否做了并行处理(也就是一边CPU复制一边USB传输),只要设置了回复NAK(不管是通过bUC_INT_BUSY还是手动修改UEPn_CTRL),且在数据传输阶段(即Data Phase)时当前中断仍处于活跃状态(可能更准确的说法是UIS_TRANSFER为1?不是很清楚内部机制),那么当前的Transaction就会自动回复NAK表示暂缓传输。
换句话说,所有基于ISR内复制的方案,单端点的最高速度都只能达到理论值的一半左右(一半时间用于回复NAK,当然不影响多个端点同时传输的情形)。(当然关掉bUC_INT_BUSY手动切换,在ISR中保证数据传输时不会出现冲突也是可行的,不过这样一来逻辑就复杂了,不如移到外面。)
好消息是我们总能保证数据是正确的,理论上不会出现新数据覆盖旧数据的情形。
不过,上面的结论从另一个方面证实了,只要ISR中的处理速度够快(大概100个周期还是够的?),把数据搬运的过程移到ISR之外,那么还是可以达到最高速度的。这也在某种程度上说明了双缓冲的优势是有局限性的,既然可以通过ISR中快速切换缓冲区的方式获得更大的缓冲区,那为什么还要用自带的双缓冲呢?因此,除非想好了确实有必要使用双缓冲,否则以CH558的硬件设计来看并没有很大必要。(猜测多缓冲可能更适合可以提前submit的架构,类似CY7C68013A?)
(只要处理速度够快,NAK就追不上我,单缓冲也一样)
最近编辑记录 metro (2020-01-27 13:04:07)
离线
关注,祝愿热衷分享的楼主新年快乐!
离线
感觉还是需要补充说明一下测试结果,其实尚未已经有答案了。
各个端点的测试结果如下(IN/OUT均一致):
EP1:~1000 KB/s
EP2:~500 KB/s
EP3:~1000 KB/s
EP4:~500 KB/s
所以,在此次测试中,对速度产生影响的主要因素还是在于搬移数据导致的ISR执行时间过长,从而每两个数据包会有一个返回NAK。单双缓冲则影响不大。
大家也可以自己用电脑测试一下结果。
离线
USB1.1? USB2.0 淘宝那种逻辑分析仪好像很多都分析不了
离线
USB1.1? USB2.0 淘宝那种逻辑分析仪好像很多都分析不了
淘宝山上的 Saleae16 可以分析 USB 1.1(USB 2.0 FullSpeed)
就是那种传输速率为12Mbps的协议.
离线
USB1.1? USB2.0 淘宝那种逻辑分析仪好像很多都分析不了
USB 2.0是包括高速(High-speed 480 Mb/s)、全速(Full-speed 12 Mb/s)和低速(Low-speed 1.5 Mb/s)三种传输速度的,而USB 1.1只涵盖了后两种,也就是说后两种设备可以选择1.1和2.0两种标准予以实现。
要分析高速USB 2.0的话当然就不能这么搞了,首先要有足够的采样率,其次还要正确分析高速传输状态下的差分信号(±400 mV),没有相应PHY的话通常搞不来的。
最近编辑记录 metro (2020-01-29 15:10:06)
离线
厉害!!!在学习Ch552的例子
离线
好贴帮顶,有专门的HS分析仪,利用USB2.0 PHY和FPGA来抓取数据,价格就不太美好了。
离线
Excellent analyzed on CH558
离线