uboot usb ehci 控制传输
2012-11-22 15:38
696 查看
1.问题陈述
在添加uboot中增加一个function后,uboot中usb driver出现无法发送控制命令的问题。
2.如何追述问题原因
通过usb 分析仪进行usb总线上发包分析,分析仪显示在设备初始化阶段并没有检测到有效的packet发送。
直接追究其原因是有没有正确将qh于qtd传递到内存中, 接下来贴上一些有关qh意识qtd初始化的代码
1.
int
submit_control_msg(struct usb_device *dev, unsigned long pipe, void *buffer,
int length, struct devrequest *setup)
{
if (usb_pipetype(pipe) != PIPE_CONTROL) {
debug("non-control pipe (type=%lu)", usb_pipetype(pipe));
return -1;
}
if (usb_pipedevice(pipe) == rootdev) {
if (rootdev == 0)
dev->speed = USB_SPEED_HIGH;
return ehci_submit_root(dev, pipe, buffer, length, setup);
}
return ehci_submit_async(dev, pipe, buffer, length, setup);
}
这个函数很简单只是判定是否是root hub,现在我们分析的设备并不是root hub dev所以我先直接进入ehci_submit_async进行分析,随后我们在回头看看如果是root dev的情况
这个函数非常长,但是还是比较容易读懂的,接下来我们逐行进行分析
static int
ehci_submit_async(struct usb_device *dev, unsigned long pipe, void *buffer,
int length, struct devrequest *req)
{
struct QH *qh;
struct qTD *td;
struct qTD *tdp;
volatile struct qTD *vtd;
volatile struct qTD *pvtd;
unsigned long ts;
//uint32_t *tdp;
uint32_t endpt, token, usbsts;
uint32_t c, toggle;
uint32_t cmd;
int ret = 0;
// TODO - revert printf to debug
debug("dev=%p, pipe=%lx, buffer=%p, length=%d, req=%p\n", dev, pipe,
buffer, length, req);
if (req != NULL)
debug("req=%u (%#x), type=%u (%#x), value=%u (%#x), index=%u\n",
req->request, req->request,
req->requesttype, req->requesttype,
le16_to_cpu(req->value), le16_to_cpu(req->value),
le16_to_cpu(req->index));
qh = ehci_alloc(sizeof(struct QH), 32); // 分配一个qh
if (qh == NULL) {
debug("unable to allocate QH\n");
return -1;
}
/* Add it to the little chain */
qh_list.qh_list = qh; //qh_list 是一个全局变量,async执行的起始地址是指向该qh_ist,这是一个无效的qh,这里我们将qh_list视为一个链表头指针
qh->qh_list = NULL;
qh->qtd_list = NULL;
/*初始化qh*/
qh->qh_link = cpu_to_hc32((uint32_t)qh_list.qh_dma | QH_LINK_TYPE_QH); //qh 指向qh链表头指针
c = (usb_pipespeed(pipe) != USB_SPEED_HIGH &&
usb_pipeendpoint(pipe) == 0) ? 1 : 0;
endpt = (8 << 28) |
(c << 27) |
(usb_maxpacket(dev, pipe) << 16) |
(0 << 15) |
(1 << 14) |
(usb_pipespeed(pipe) << 12) |
(usb_pipeendpoint(pipe) << 8) |
(0 << 7) | (usb_pipedevice(pipe) << 0);
qh->qh_endpt1 = cpu_to_hc32(endpt);
endpt = (1 << 30) |
(dev->portnr << 23) |
(dev->parent->devnum << 16) | (0 << 8) | (0 << 0);
qh->qh_endpt2 = cpu_to_hc32(endpt);
qh->qh_overlay.qt_next = cpu_to_hc32(QT_NEXT_TERMINATE);
qh->qh_overlay.qt_altnext = cpu_to_hc32(QT_NEXT_TERMINATE);
tdp = NULL;
toggle = usb_gettoggle(dev, usb_pipeendpoint(pipe), usb_pipeout(pipe));
/*control msg 至少有2个包 第一个是setup,第二个是data (如果有data的话)第三个握手包 */
if (req != NULL) {
td = ehci_alloc(sizeof(struct qTD), 32);
if (td == NULL) {
debug("unable to allocate SETUP td\n");
goto fail;
}
if (qh->qtd_list == NULL) {
qh->qtd_list = td;
qh->qh_overlay.qt_next = td->qt_dma;//qh 的overlay部分指向第一个setup qtd
}
if (tdp != NULL) {
tdp->qt_next = cpu_to_hc32(td->qt_dma);
tdp->qtd_list = td;
}
tdp = td; //这个是qtd的指针chain
td->qtd_list = NULL;
td->qt_next = cpu_to_hc32(QT_NEXT_TERMINATE);
td->qt_altnext = cpu_to_hc32(QT_NEXT_TERMINATE);
token = (0 << 31) |
(sizeof(*req) << 16) |
(0 << 15) | (0 << 12) | (3 << 10) | (2 << 8) | (0x80 << 0);
td->qt_token = cpu_to_hc32(token);
if (ehci_td_buffer(td, req, sizeof(*req)) != 0) {
debug("unable construct SETUP td\n");
ehci_free(td, sizeof(*td));
goto fail;
}
toggle = 1;
}
if (length > 0 || req == NULL) {
td = ehci_alloc(sizeof(struct qTD), 32);
if (td == NULL) {
printf("unable to allocate DATA td\n"); // TODO puts -> debug
goto fail;
}
if (qh->qtd_list == NULL) {
qh->qtd_list = td;
qh->qh_overlay.qt_next = td->qt_dma;
}
if (tdp != NULL) {
tdp->qt_next = cpu_to_hc32(td->qt_dma); //tpd 现在是指向的是setup 包 ,赋值操作将data qtd的地址存放在set up qtd中
tdp->qtd_list = td;
}
tdp = td;
td->qtd_list = NULL;
td->qt_next = cpu_to_hc32(QT_NEXT_TERMINATE);
td->qt_altnext = cpu_to_hc32(QT_NEXT_TERMINATE);
token = (toggle << 31) |
(length << 16) |
((req == NULL ? 1 : 0) << 15) |
(0 << 12) |
(3 << 10) |
((usb_pipein(pipe) ? 1 : 0) << 8) | (0x80 << 0);
td->qt_token = cpu_to_hc32(token);
if (ehci_td_buffer(td, buffer, length) != 0) {
puts("unable construct DATA td\n"); // TODO puts -> debug
ehci_free(td, sizeof(*td));
goto fail;
}
}
if (req != NULL) {
td = ehci_alloc(sizeof(struct qTD), 32);
if (td == NULL) {
puts("unable to allocate ACK td\n"); // TODO puts -> debug
goto fail;
}
if (qh->qtd_list == NULL) {
qh->qtd_list = td;
qh->qh_overlay.qt_next = td->qt_dma;
}
if (tdp != NULL) {
tdp->qt_next = cpu_to_hc32(td->qt_dma);
tdp->qtd_list = td;
}
tdp = td;
td->qtd_list = NULL;
td->qt_next = cpu_to_hc32(QT_NEXT_TERMINATE);
td->qt_altnext = cpu_to_hc32(QT_NEXT_TERMINATE);
token = (toggle << 31) |
(0 << 16) |
(1 << 15) |
(0 << 12) |
(3 << 10) |
((usb_pipein(pipe) ? 0 : 1) << 8) | (0x80 << 0);
td->qt_token = cpu_to_hc32(token);
td->qt_buffer[0] = 0;
td->qt_buffer[1] = 0;
td->qt_buffer[2] = 0;
td->qt_buffer[3] = 0;
td->qt_buffer[4] = 0;
}
qh_list.qh_link = cpu_to_hc32((uint32_t)qh->qh_dma | QH_LINK_TYPE_QH); //qh链表头指针的next 物理地址指针指向当前qh的物理地址
ehci_flush_qh(&qh_list); // flush cache
dump_qh
(&qh_list, 0);
usbsts = ehci_readl(&hcor->or_usbsts);
ehci_writel(&hcor->or_usbsts, (usbsts & 0x3f)); //清除状态寄存器
/* Enable async. schedule. */
cmd = ehci_readl(&hcor->or_usbcmd);
cmd |= CMD_ASE;
ehci_writel(&hcor->or_usbcmd, cmd);
ret = handshake((uint32_t *)&hcor->or_usbsts, STD_ASS, STD_ASS,
200 * 1000);
if (ret < 0) {
printf("EHCI fail timeout STD_ASS set\n");
goto fail;
}
//printf("ip9028: asynclistaddr=%08x\n", ehci_readl(&hcor->or_asynclistaddr));
/* Wait for TDs to be processed. */
ts = get_timer(0);
vtd = td;
pvtd = td->qt_dma;
do { //轮询等待 数据包发送完毕
/* Invalidate dcache */
ehci_inv_qh(&qh_list);
#ifdef DEBUG
printf("TOKEN=%#x %x %#x %x\n", vtd->qt_token, vtd, pvtd->qt_token, pvtd);
#endif
if (!(vtd->qt_token & 0x80)) {
#ifdef DEBUG
printf("END-TOKEN=%#x\n", vtd->qt_token);
#endif
break;
}
} while (get_timer(ts) < CONFIG_SYS_HZ);
/* Disable async schedule. */
cmd = ehci_readl(&hcor->or_usbcmd);
cmd &= ~CMD_ASE;
ehci_writel(&hcor->or_usbcmd, cmd);
//printf("ip9028: asynclistaddr=%08x\n", ehci_readl(&hcor->or_asynclistaddr));
ret = handshake((uint32_t *)&hcor->or_usbsts, STD_ASS, 0, 200 * 1000);
if (ret < 0) {
printf("EHCI fail timeout STD_ASS reset\n");
goto fail;
}
//printf("ip9028: asynclistaddr=%08x\n", ehci_readl(&hcor->or_asynclistaddr));
dump_qh
(&qh_list, 0);
qh_list.qh_link = cpu_to_hc32((uint32_t)qh_list.qh_dma | QH_LINK_TYPE_QH);
token = hc32_to_cpu(qh->qh_overlay.qt_token);
if (!(token & 0x80)) { //分析当前的的stat
debug("TOKEN=%#x\n", token);
switch (token & 0xfc) {
case 0:
toggle = token >> 31;
usb_settoggle(dev, usb_pipeendpoint(pipe),
usb_pipeout(pipe), toggle);
dev->status = 0;
break;
case 0x40:
//debug("USB_ST_STALLED\n");
puts("USB_ST_STALLED\n");
dev->status = USB_ST_STALLED;
break;
case 0xa0:
case 0x20:
//debug("USB_ST_BUF_ERR\n");
puts("USB_ST_BUF_ERR\n");
dev->status = USB_ST_BUF_ERR;
break;
case 0x50:
case 0x10:
//debug("USB_ST_BABBLE_DET\n");
puts("USB_ST_BABBLE_DET\n");
dev->status = USB_ST_BABBLE_DET;
break;
default:
//debug("USB_ST_CRC_ERR\n");
puts("USB_ST_CRC_ERR\n");
dev->status = USB_ST_CRC_ERR;
break;
}
dev->act_len = length - ((token >> 16) & 0x7fff);
} else {
dev->act_len = 0;
debug("dev=%u, usbsts=%#x, p[1]=%#x, p[2]=%#x\n",
dev->devnum, ehci_readl(&hcor->or_usbsts),
ehci_readl(&hcor->or_portsc[0]),
ehci_readl(&hcor->or_portsc[1]));
}
return (dev->status != USB_ST_NOT_PROC) ? 0 : -1;
fail:
td = (void *)hc32_to_cpu(qh->qh_overlay.qt_next);
while (td != (void *)QT_NEXT_TERMINATE) {
qh->qh_overlay.qt_next = td->qt_next;
ehci_free(td, sizeof(*td));
td = (void *)hc32_to_cpu(qh->qh_overlay.qt_next);
}
ehci_free(qh, sizeof(*qh));
return -1;
}
代码解析完毕,在实验当中发现,在启动异步传输之前,有一段时间延时便可以成功的发送数据包,
对于当前情况唯一的解释是:qh和qtd flush 在启动异步传输的时候没有及时的flush到ddr中。在flush data 中增加一定的延时问题则不再出现
在添加uboot中增加一个function后,uboot中usb driver出现无法发送控制命令的问题。
2.如何追述问题原因
通过usb 分析仪进行usb总线上发包分析,分析仪显示在设备初始化阶段并没有检测到有效的packet发送。
直接追究其原因是有没有正确将qh于qtd传递到内存中, 接下来贴上一些有关qh意识qtd初始化的代码
1.
int
submit_control_msg(struct usb_device *dev, unsigned long pipe, void *buffer,
int length, struct devrequest *setup)
{
if (usb_pipetype(pipe) != PIPE_CONTROL) {
debug("non-control pipe (type=%lu)", usb_pipetype(pipe));
return -1;
}
if (usb_pipedevice(pipe) == rootdev) {
if (rootdev == 0)
dev->speed = USB_SPEED_HIGH;
return ehci_submit_root(dev, pipe, buffer, length, setup);
}
return ehci_submit_async(dev, pipe, buffer, length, setup);
}
这个函数很简单只是判定是否是root hub,现在我们分析的设备并不是root hub dev所以我先直接进入ehci_submit_async进行分析,随后我们在回头看看如果是root dev的情况
这个函数非常长,但是还是比较容易读懂的,接下来我们逐行进行分析
static int
ehci_submit_async(struct usb_device *dev, unsigned long pipe, void *buffer,
int length, struct devrequest *req)
{
struct QH *qh;
struct qTD *td;
struct qTD *tdp;
volatile struct qTD *vtd;
volatile struct qTD *pvtd;
unsigned long ts;
//uint32_t *tdp;
uint32_t endpt, token, usbsts;
uint32_t c, toggle;
uint32_t cmd;
int ret = 0;
// TODO - revert printf to debug
debug("dev=%p, pipe=%lx, buffer=%p, length=%d, req=%p\n", dev, pipe,
buffer, length, req);
if (req != NULL)
debug("req=%u (%#x), type=%u (%#x), value=%u (%#x), index=%u\n",
req->request, req->request,
req->requesttype, req->requesttype,
le16_to_cpu(req->value), le16_to_cpu(req->value),
le16_to_cpu(req->index));
qh = ehci_alloc(sizeof(struct QH), 32); // 分配一个qh
if (qh == NULL) {
debug("unable to allocate QH\n");
return -1;
}
/* Add it to the little chain */
qh_list.qh_list = qh; //qh_list 是一个全局变量,async执行的起始地址是指向该qh_ist,这是一个无效的qh,这里我们将qh_list视为一个链表头指针
qh->qh_list = NULL;
qh->qtd_list = NULL;
/*初始化qh*/
qh->qh_link = cpu_to_hc32((uint32_t)qh_list.qh_dma | QH_LINK_TYPE_QH); //qh 指向qh链表头指针
c = (usb_pipespeed(pipe) != USB_SPEED_HIGH &&
usb_pipeendpoint(pipe) == 0) ? 1 : 0;
endpt = (8 << 28) |
(c << 27) |
(usb_maxpacket(dev, pipe) << 16) |
(0 << 15) |
(1 << 14) |
(usb_pipespeed(pipe) << 12) |
(usb_pipeendpoint(pipe) << 8) |
(0 << 7) | (usb_pipedevice(pipe) << 0);
qh->qh_endpt1 = cpu_to_hc32(endpt);
endpt = (1 << 30) |
(dev->portnr << 23) |
(dev->parent->devnum << 16) | (0 << 8) | (0 << 0);
qh->qh_endpt2 = cpu_to_hc32(endpt);
qh->qh_overlay.qt_next = cpu_to_hc32(QT_NEXT_TERMINATE);
qh->qh_overlay.qt_altnext = cpu_to_hc32(QT_NEXT_TERMINATE);
tdp = NULL;
toggle = usb_gettoggle(dev, usb_pipeendpoint(pipe), usb_pipeout(pipe));
/*control msg 至少有2个包 第一个是setup,第二个是data (如果有data的话)第三个握手包 */
if (req != NULL) {
td = ehci_alloc(sizeof(struct qTD), 32);
if (td == NULL) {
debug("unable to allocate SETUP td\n");
goto fail;
}
if (qh->qtd_list == NULL) {
qh->qtd_list = td;
qh->qh_overlay.qt_next = td->qt_dma;//qh 的overlay部分指向第一个setup qtd
}
if (tdp != NULL) {
tdp->qt_next = cpu_to_hc32(td->qt_dma);
tdp->qtd_list = td;
}
tdp = td; //这个是qtd的指针chain
td->qtd_list = NULL;
td->qt_next = cpu_to_hc32(QT_NEXT_TERMINATE);
td->qt_altnext = cpu_to_hc32(QT_NEXT_TERMINATE);
token = (0 << 31) |
(sizeof(*req) << 16) |
(0 << 15) | (0 << 12) | (3 << 10) | (2 << 8) | (0x80 << 0);
td->qt_token = cpu_to_hc32(token);
if (ehci_td_buffer(td, req, sizeof(*req)) != 0) {
debug("unable construct SETUP td\n");
ehci_free(td, sizeof(*td));
goto fail;
}
toggle = 1;
}
if (length > 0 || req == NULL) {
td = ehci_alloc(sizeof(struct qTD), 32);
if (td == NULL) {
printf("unable to allocate DATA td\n"); // TODO puts -> debug
goto fail;
}
if (qh->qtd_list == NULL) {
qh->qtd_list = td;
qh->qh_overlay.qt_next = td->qt_dma;
}
if (tdp != NULL) {
tdp->qt_next = cpu_to_hc32(td->qt_dma); //tpd 现在是指向的是setup 包 ,赋值操作将data qtd的地址存放在set up qtd中
tdp->qtd_list = td;
}
tdp = td;
td->qtd_list = NULL;
td->qt_next = cpu_to_hc32(QT_NEXT_TERMINATE);
td->qt_altnext = cpu_to_hc32(QT_NEXT_TERMINATE);
token = (toggle << 31) |
(length << 16) |
((req == NULL ? 1 : 0) << 15) |
(0 << 12) |
(3 << 10) |
((usb_pipein(pipe) ? 1 : 0) << 8) | (0x80 << 0);
td->qt_token = cpu_to_hc32(token);
if (ehci_td_buffer(td, buffer, length) != 0) {
puts("unable construct DATA td\n"); // TODO puts -> debug
ehci_free(td, sizeof(*td));
goto fail;
}
}
if (req != NULL) {
td = ehci_alloc(sizeof(struct qTD), 32);
if (td == NULL) {
puts("unable to allocate ACK td\n"); // TODO puts -> debug
goto fail;
}
if (qh->qtd_list == NULL) {
qh->qtd_list = td;
qh->qh_overlay.qt_next = td->qt_dma;
}
if (tdp != NULL) {
tdp->qt_next = cpu_to_hc32(td->qt_dma);
tdp->qtd_list = td;
}
tdp = td;
td->qtd_list = NULL;
td->qt_next = cpu_to_hc32(QT_NEXT_TERMINATE);
td->qt_altnext = cpu_to_hc32(QT_NEXT_TERMINATE);
token = (toggle << 31) |
(0 << 16) |
(1 << 15) |
(0 << 12) |
(3 << 10) |
((usb_pipein(pipe) ? 0 : 1) << 8) | (0x80 << 0);
td->qt_token = cpu_to_hc32(token);
td->qt_buffer[0] = 0;
td->qt_buffer[1] = 0;
td->qt_buffer[2] = 0;
td->qt_buffer[3] = 0;
td->qt_buffer[4] = 0;
}
qh_list.qh_link = cpu_to_hc32((uint32_t)qh->qh_dma | QH_LINK_TYPE_QH); //qh链表头指针的next 物理地址指针指向当前qh的物理地址
ehci_flush_qh(&qh_list); // flush cache
dump_qh
(&qh_list, 0);
usbsts = ehci_readl(&hcor->or_usbsts);
ehci_writel(&hcor->or_usbsts, (usbsts & 0x3f)); //清除状态寄存器
/* Enable async. schedule. */
cmd = ehci_readl(&hcor->or_usbcmd);
cmd |= CMD_ASE;
ehci_writel(&hcor->or_usbcmd, cmd);
ret = handshake((uint32_t *)&hcor->or_usbsts, STD_ASS, STD_ASS,
200 * 1000);
if (ret < 0) {
printf("EHCI fail timeout STD_ASS set\n");
goto fail;
}
//printf("ip9028: asynclistaddr=%08x\n", ehci_readl(&hcor->or_asynclistaddr));
/* Wait for TDs to be processed. */
ts = get_timer(0);
vtd = td;
pvtd = td->qt_dma;
do { //轮询等待 数据包发送完毕
/* Invalidate dcache */
ehci_inv_qh(&qh_list);
#ifdef DEBUG
printf("TOKEN=%#x %x %#x %x\n", vtd->qt_token, vtd, pvtd->qt_token, pvtd);
#endif
if (!(vtd->qt_token & 0x80)) {
#ifdef DEBUG
printf("END-TOKEN=%#x\n", vtd->qt_token);
#endif
break;
}
} while (get_timer(ts) < CONFIG_SYS_HZ);
/* Disable async schedule. */
cmd = ehci_readl(&hcor->or_usbcmd);
cmd &= ~CMD_ASE;
ehci_writel(&hcor->or_usbcmd, cmd);
//printf("ip9028: asynclistaddr=%08x\n", ehci_readl(&hcor->or_asynclistaddr));
ret = handshake((uint32_t *)&hcor->or_usbsts, STD_ASS, 0, 200 * 1000);
if (ret < 0) {
printf("EHCI fail timeout STD_ASS reset\n");
goto fail;
}
//printf("ip9028: asynclistaddr=%08x\n", ehci_readl(&hcor->or_asynclistaddr));
dump_qh
(&qh_list, 0);
qh_list.qh_link = cpu_to_hc32((uint32_t)qh_list.qh_dma | QH_LINK_TYPE_QH);
token = hc32_to_cpu(qh->qh_overlay.qt_token);
if (!(token & 0x80)) { //分析当前的的stat
debug("TOKEN=%#x\n", token);
switch (token & 0xfc) {
case 0:
toggle = token >> 31;
usb_settoggle(dev, usb_pipeendpoint(pipe),
usb_pipeout(pipe), toggle);
dev->status = 0;
break;
case 0x40:
//debug("USB_ST_STALLED\n");
puts("USB_ST_STALLED\n");
dev->status = USB_ST_STALLED;
break;
case 0xa0:
case 0x20:
//debug("USB_ST_BUF_ERR\n");
puts("USB_ST_BUF_ERR\n");
dev->status = USB_ST_BUF_ERR;
break;
case 0x50:
case 0x10:
//debug("USB_ST_BABBLE_DET\n");
puts("USB_ST_BABBLE_DET\n");
dev->status = USB_ST_BABBLE_DET;
break;
default:
//debug("USB_ST_CRC_ERR\n");
puts("USB_ST_CRC_ERR\n");
dev->status = USB_ST_CRC_ERR;
break;
}
dev->act_len = length - ((token >> 16) & 0x7fff);
} else {
dev->act_len = 0;
debug("dev=%u, usbsts=%#x, p[1]=%#x, p[2]=%#x\n",
dev->devnum, ehci_readl(&hcor->or_usbsts),
ehci_readl(&hcor->or_portsc[0]),
ehci_readl(&hcor->or_portsc[1]));
}
return (dev->status != USB_ST_NOT_PROC) ? 0 : -1;
fail:
td = (void *)hc32_to_cpu(qh->qh_overlay.qt_next);
while (td != (void *)QT_NEXT_TERMINATE) {
qh->qh_overlay.qt_next = td->qt_next;
ehci_free(td, sizeof(*td));
td = (void *)hc32_to_cpu(qh->qh_overlay.qt_next);
}
ehci_free(qh, sizeof(*qh));
return -1;
}
代码解析完毕,在实验当中发现,在启动异步传输之前,有一段时间延时便可以成功的发送数据包,
对于当前情况唯一的解释是:qh和qtd flush 在启动异步传输的时候没有及时的flush到ddr中。在flush data 中增加一定的延时问题则不再出现
相关文章推荐
- USB传输模式(控制传输、批量传输、中断传输、同步传输)介绍
- USB驱动程序(四)——键盘驱动(控制传输)
- stm32的usb与上位机通过中断传输进行通信控制led--LibUsbDotNet
- USB控制传输过程 详细解析
- USB驱动——键盘驱动(控制传输)
- 基本固件框架的追踪研读之USB控制传输
- USB传输模式(控制传输、批量传输、中断传输、同步传输)介绍
- usb控制传输
- 在ubuntu下通过USB对u-boot传输文件(mini2440)
- USB控制传输在EZ-USB(68013)总结
- 关于Android设备USBHID通信控制传输相关接口说明
- 在ubuntu下通过USB对u-boot传输文件(mini2440)
- EZ-USB FX2LP CY7C68013A 控制传输/自定义命令(上位机编程)
- 通过DHTS(分布式散列表系统)引导P2P控制传输
- usb的包结构和传输类型
- Android 数据传输方式 WIFI 蓝牙 USB
- 使用u-boot的USB下载功能烧写程序到Nand Flash ——韦东山嵌入式Linux学习笔记06
- springboot-29-security(二)用户角色权限控制
- 采用list传输格式的webservice实现控制远程设备
- 【iCore3 双核心板】例程十六:USB_HID实验——双向数据传输