在UVC,UAC等要求实时传输的设备中,使用的是ISOC同步传输。而UVC等应用一般发送数据都比较大,比如发送一帧未压缩图像,可能就有几MB, 对于这种大数据的发送我们可以充分利用DWC2的Scatter/Gather DMA来实现。
我们的设计目标是考虑资源消耗和性能的平衡,灵活可配,资源不够时可以降低效率但是也要能工作,资源够时可以充分发挥其性能。
dwc2的Scatter/Gather DMA模式是一种高效的操作方式,减轻了软件的负担,软件只需要设置好描述符,硬件DMA就会进行处理,我们这里即基于该模式来实现同步发送,其他传输方式也是类似。充分利用其特性实现灵活的发送处理,在资源充足要求高效时我们可以构造非常大的链表一次性发送非常大的数据,而资源紧张,性能要求没这么高时,可以构造小的链表,一次发送较少数据,发送完后继续发送剩余数据,直到发送完,后者会产生更多的中断。这样可以实现不同需求的灵活配置。
对于描述符我们参考《DesignWare Cores USB 2.0 Hi-Speed On-The Go (OTG) Programming Guide.pdf》或者前面的dwc2驱动系列文章。
对于同步传输IN的描述符设置如下,两个4字节的参数,第一个即Status,第2个即buffer指针。其中Buffer指针最好是4字节对齐(不对齐也是OK的,但是猜测可能会影响Burst操作降低性能)。Status则重点关注L 和SP的组合。BS和TxSts硬件会更新状态,软件写BS指示状态,详见手册说明这里不再赘述。
IOC设置为1则本描述符处理完产生中断,一般设置链表的最后一个描述符处理完才产生中断,尽可能减少中断次数。
PID需要根据一个微帧发送几帧来设置(端点描述符中也要对应), DCTl.IgnrFrmNum=1模式Frame Num无需设置。
架构如下
大数据可以分为多个链表发送,每个链表产生一次中断,中断之后构建下一个链表。
每个链表可以包含多个描述符,每个描述符可以发送1~3包数据。
可配参数:
1.包大小
2.一个描述符发送几包
3.一个链表包含几个描述符
即如下参数
static usbd_ep_info_st s_vs_in_ep=
{
.addr = 0x84,
.fifonum = 3,
.maxpacket = EP_SIZE,
.packets = EP_TRANS_NUM,
.type = USBD_EP_TYPE_ISO,
.dmabuffer = s_vs_in_ep_dma,
.dmanums = (sizeof(s_vs_in_ep_dma)/sizeof(s_vs_in_ep_dma[0]))/2, /* 一个描述符需要2个WORD所以需要/2 */
.buffer = 0,
};
代码实现如下:
注意以下代码没有考虑一个描述符发送2或者3帧的情况,按需求添加描述符中PID配置即可。
/**
* \fn usbd_ep_write_0
* 端点发送数据
* \param[in] epnum 端点地址
* \param[in] buffer 发送缓冲区
* \param[in] size 发送长度
* \param[in] flag 1第一次发送 0后续发送
* \return 总是返回0
*/
static int usbd_ep_write_0(uint32_t epnum, void *buffer, uint32_t size, uint32_t flag)
{
uint32_t sendsize = 0; /* 本次描述符链表发送的数据长度 */
usbd_ep_st* pep; /* 端点指针 */
uint32_t dmanums = 0; /* 需要的描述符个数 */
uint32_t last_len = 0; /* 本描述符链表,最后一个描述符对应的发送长度 */
uint32_t sndbuffer = 0; /* 记录描述符对应的待发送数据缓存开始地址 */
/* 获取端点信息 */
epnum &= USB_EPNO_MASK;
pep = &(s_usbd_dev.dep[EP_IN_OFS + epnum]);
/* 如果是第一次启动传输则初始化变量,后续传输在此基础上继续 */
if(flag != 0)
{
pep->xfer_len = size; /* 待发送的数据长度 */
pep->xfer_buff = buffer; /* 待发送数据缓存 */
pep->xfered_count = 0; /* 已经传输的长度 */
}
else
{
/* 无需初始化变量,在原来基础上继续 */
}
/* 计算本次链表待传输长度
* 一个链表有多个描述符,一个描述符可以传输pep->maxpacket * pep->packets
* 总共可以传输pep->maxpacket * pep->packets * pep->dma_nums
*
* 如果一次传输不完,则本次传输大小是pep->maxpacket * pep->packets * pep->dma_nums,
* 否则就是剩余待传输大小. 计算得到本次待传输大小sendsize.
*
* 如果一次传输不完则所有描述符都需要用上,即dmanums=pep->dma_nums
* 否则根据实际传输大小计算,注意要向上圆整
* dmanums = (size+pep->maxpacket*pep->packets-1)/(pep->maxpacket*pep->packets)
*
* 计算最后一个描述传输的大小last_len
* 如果一次传输不完则last_len是pep->maxpacket * pep->packets,
* 否则根据sendsize计算.
*/
if(size <= pep->maxpacket * pep->packets * pep->dma_nums)
{
sendsize = size; /* 一次可以发送完 */
dmanums = (size+pep->maxpacket*pep->packets-1)/(pep->maxpacket*pep->packets); /* 向上圆整 */
last_len = sendsize % (pep->maxpacket * pep->packets);
if(last_len == 0)
{
/* 注意这里如果整除,则说明刚好是倍数,最后也是一个完整pep->maxpacket * pep->packets 大小 */
last_len = pep->maxpacket * pep->packets;
}
}
else
{
sendsize = pep->maxpacket * pep->packets * pep->dma_nums; /* 分多次发送完 */
dmanums = pep->dma_nums;
last_len = pep->maxpacket * pep->packets;
}
/* 更新本次传输长度,in中断中根据该值计算本次实际传输长度,以更新下一次需要传输时的缓存地址与剩余待传输长度 */
pep->xfer_count = sendsize;
/* 填充描述符 */
//sndbuffer = (uint32_t)pep->xfer_buff; /* 开始地址,填充完一个描述符后递增 */
sndbuffer = buffer; /* 用传进来的参数,pep->xfer_buff不变 */
for(uint32_t i=0; i
{
pep->dma_addr[2*i+1] = sndbuffer; /* 待发送地址 */
if(i==(dmanums-1))
{
/* 链表的最后一个描述符 设置IOC位 产生中断
* 最后一个描述符需要设置L位
* @todo 根据不同传输类型可能需要设置SP位
*/
pep->dma_addr[2*i+0] = (uint32_t)(USBD_DESC_BIT_IOC | USBD_DESC_BIT_L | USBD_DESC_BIT_SP | last_len); /* 这是最后一笔 SP=1 */
/* 最后一次无需再计算sndbuffer */
}
else
{
pep->dma_addr[2*i+0] = (uint32_t)(0 | (pep->maxpacket * pep->packets)); /* 不是最后一笔 SP=0 */
sndbuffer += pep->maxpacket * pep->packets; /* 计算下一个描述符开始的地址 */
}
}
/* 记录本次待发送的大小 */
pep->xfer_count = sendsize;
/* 设置DMA开始发送 注意EPENA_IN_MASK放在最后 */
REG_DIEP_DMA(epnum) = (uint32_t)(pep->dma_addr);
REG_DIEP_CTL(epnum) |= (EPENA_IN_MASK | CNAK_IN_MASK);
return 0;
}
同时基于此实现两个接口
usbd_ep_write由用户调用启动传输,usbd_ep_write_continue在中断中调用继续发送。
区别是前者会初始化参数,后者无需。
int usbd_ep_write(uint32_t epnum, void *buffer, uint32_t size)
{
USBD_IN_LOG(("inep%02x start,buf:%x tosnd %d\n",epnum,buffer,size));
return usbd_ep_write_0(epnum,buffer,size, 1);
}
/**
* \fn usbd_ep_write_continue
* 中断中调用继续发送
* \param[in] epnum 端点地址
* \param[in] buffer 发送缓冲区
* \param[in] size 发送长度
* \return 参考usbd_ep_write_0返回地址
*/
static int usbd_ep_write_continue(uint32_t epnum,void *buffer, uint32_t size )
{
USBD_IN_LOG(("inep%02x continue,buf:%x tosnd %d\n",epnum,buffer,size));
return usbd_ep_write_0(epnum,buffer,size, 0);
}
中断处理很简单,只需要判断发送完没有,没有发送完则计算当前发送到了的位置和剩余长度调用usbd_ep_write_continue继续发送,发送完则回调发送完接口
static void usbd_epn_in_intr(uint8_t epnum)
{
uint32_t intr;
uint8_t addr;
addr = epnum & USB_EPNO_MASK;
intr = REG_DIEP_INT(addr);
if((intr & XFERCOMPL_IN_MASK) != 0)
{
usbd_ep_st* pep;
pep = &(s_usbd_dev.dep[addr + EP_IN_OFS]);
uint32_t send;
/* xfer_count本次待发送大小
* dma_addr是当前描述符处理完剩余未发送的长度
* 所以send本次实际发送的长度 = xfer_count待发送的长度 - dma_addr剩余未发送长度
* 这里理论上需要根据pep->xfer_count减去dma状态计算剩余未发送完的大小来确定已发哦是那个大小
* 为了简单假设全部发送完所以dma_addr中剩余未发送完为0
*/
//send = pep->xfer_count - (pep->dma_addr[addr] & 0xffff); /* 本次实际发送的大小 */
send = pep->xfer_count;
pep->xfer_len -= send; /* 计算剩余未发送大小, xfer_len不可能小于send */
pep->xfered_count += send;
if(pep->xfer_len == 0)
{
/* 全部发送完 调用回调函数 */
USBD_IN_LOG(("in ep%d done\n",addr));
if(pep->event_cb !=0)
{
pep->event_cb(epnum,pep->xfer_buff,pep->xfered_count);
}
}
else
{
/* 继续发送 */
usbd_ep_write_continue(epnum,pep->xfer_buff+pep->xfered_count,pep->xfer_len);
}
}
/*@todo 其他中断处理 */
REG_DIEP_INT(addr) = intr; /* 写1清零标志 */
}
测试一次发送完,分配较大的描述符链表,足够一次发送完。
__attribute__((aligned(8))) uint32_t s_vs_in_ep_dma[((IMG_H_MAX*IMG_V_MAX*2*2)+(EP_SIZE*EP_TRANS_NUM))/(EP_SIZE*EP_TRANS_NUM*2)*2];
//__attribute__((aligned(8))) uint32_t s_vs_in_ep_dma[8*2]; /* 8个描述符 */
static usbd_ep_info_st s_vs_in_ep=
{
.addr = 0x84,
.fifonum = 3,
.maxpacket = EP_SIZE,
.packets = EP_TRANS_NUM,
.type = USBD_EP_TYPE_ISO,
.dmabuffer = s_vs_in_ep_dma,
.dmanums = (sizeof(s_vs_in_ep_dma)/sizeof(s_vs_in_ep_dma[0]))/2, /* 一个描述符需要2个WORD所以需要/2 */
.buffer = 0,
};
对应打印如下
可以看到连续发送,没有间隔,效率高。
测试多次发送完,分配较小的描述符链表,一次不能发送完。
__attribute__((aligned(8))) uint32_t s_vs_in_ep_dma[8*2]; /* 8个描述符 */
static usbd_ep_info_st s_vs_in_ep=
{
.addr = 0x84,
.fifonum = 3,
.maxpacket = EP_SIZE,
.packets = EP_TRANS_NUM,
.type = USBD_EP_TYPE_ISO,
.dmabuffer = s_vs_in_ep_dma,
.dmanums = (sizeof(s_vs_in_ep_dma)/sizeof(s_vs_in_ep_dma[0]))/2, /* 一个描述符需要2个WORD所以需要/2 */
.buffer = 0,
};
可以看到如下3次发送完,一次发送8包,一个描述符发送一包,8个描述符。
由于中断处理,每次发送之间有间隔。效率比前者低。
在嵌入式驱动开发中,性能和资源是矛盾体,但是驱动开发者需要兼顾彼此,提供给用户灵活的可选性,这也是嵌入式系统可定制的一个体现。也就是哪怕资源不够我们也应该要能工作只是效率低一点,资源够用我们则可以跑的”嘎嘎飞快”。这是嵌入式驱动开发设计的很重要的思想,好的驱动设计时刻需要考虑该条。