下面从一个实际的案例讲起,记录rt-thread usb驱动框架的一个bug(目前已由rt-thread usb维护人员一弘法师修正),也让大家更直观的掌握临界区使用和调试跟踪问题的方法。
最近在rt-thread上实现龙芯1C的otg usb device驱动。这方面主要参考rt-thread里面STM32F4-HAL的bsp中的实现。这里面验证usb device功能主要通过USB虚拟串口(VCOM)。经过很多天的努力,win10上终于识别到了VCOM设备。我迫不及待写了个测试程序来看看是否可以用。测试程序如下:
void vcom_test(void)
{
rt_device_t dev = RT_NULL;
dev = rt_device_find("vcom");
if (rt_device_open(dev, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX | \
RT_DEVICE_FLAG_STREAM) == RT_EOK)
{
rt_device_write(dev, 0, "abc", 4);
}
}
#ifdef RT_USING_FINSH
#include <finsh.h>
FINSH_FUNCTION_EXPORT(vcom_test, vcom_test);
#ifdef FINSH_USING_MSH
MSH_CMD_EXPORT(vcom_test, vcom_test)
#endif /* FINSH_USING_MSH */
#endif /* RT_USING_FINSH */
准备工作:将microusb连接到PC,win10上识别为COM5。使用putty打开,波特率为115200。另一边,调试的串口也要打开,调试串口主要为了查看打印信息以及rt-thread MSH交互。
再啰嗦两句。上面这段代码主要是打开了注册的vcom设备,往设备写入“abc”。
在准备工作做好之后,在调试串口输入vcom_test命令,预计COM5所在的putty终端会打印出“abc”。可是事与愿违,putty打印出来的是“aabbcc”。虽然有点小问题,但是至少说明至此usb device驱动已经基本完成。下面只能硬着头皮查了。
由于龙芯的ejtag超级贵,我手头上没有。接下来的调试主要采用rt_kprintf的方式在适当位置打印有用的信息。主要采用rt_krpintf("%s:%d\n",__func__,__LINE__)来跟踪函数运行情况。经过一会儿的排查,发现在下面这个函数里面的dcd_ep_write函数调用了两次!该函数位于components\drivers\usb\usbdevice\core\core.c,具体如下:
static rt_size_t rt_usbd_ep_write(udevice_t device, uep_t ep, void *buffer, rt_size_t size)
{
rt_uint16_t maxpacket;
RT_ASSERT(device != RT_NULL);
RT_ASSERT(device->dcd != RT_NULL);
RT_ASSERT(ep != RT_NULL);
maxpacket = EP_MAXPACKET(ep);
if(ep->request.remain_size >= maxpacket)
{
dcd_ep_write(device->dcd, EP_ADDRESS(ep), ep->request.buffer, maxpacket);
ep->request.remain_size -= maxpacket;
ep->request.buffer += maxpacket;
}
else
{
dcd_ep_write(device->dcd, EP_ADDRESS(ep), ep->request.buffer,
ep->request.remain_size);
ep->request.remain_size = 0;
}
return size;
}
由于vcom是注册rt-thread的serial驱动框架,采用putc一个字节一个字节的发送,所以这里会执行到else里面。从这个函数,从表面上来看dcd_ep_write函数(上面第18行)只会被执行一遍,可是从实际打印的调试信息来看,他执行了两遍!这到底怎么回事呢?我们接着看dcd_ep_write的函数:
rt_inline rt_size_t dcd_ep_write(udcd_t dcd, rt_uint8_t address, void *buffer,
rt_size_t size)
{
RT_ASSERT(dcd != RT_NULL);
RT_ASSERT(dcd->ops != RT_NULL);
RT_ASSERT(dcd->ops->ep_write != RT_NULL);
return dcd->ops->ep_write(address, buffer, size);
}
这个函数主要是调用了usb device驱动注册的ep_write操作函数,往USB端点写入数据。实在看不出来有什么端倪啊!只能采用绝招了,在dcd_ep_write函数中打印一下RA寄存器值,然后对比objdump出来的反编译文件了。PS:龙芯1C兼容MIPS32。在MIPS中,A函数需要调用B函数时,在跳转之前会把A函数中的返回地址存储在RA寄存器中,具体参考《see mips run》。
重新上电运行,从打印的信息中可以看到,两次调用dcd_ep_write函数时,RA是不同的。一次是指向rt_usbd_ep_write(上文中有贴出来代码),一次是指向了components\drivers\usb\usbdevice\core\core.c中的_data_notify函数(下面第33行):
static rt_err_t _data_notify(udevice_t device, struct ep_msg* ep_msg)
{
uep_t ep;
ufunction_t func;
rt_size_t size = 0;
RT_ASSERT(device != RT_NULL);
RT_ASSERT(ep_msg != RT_NULL);
if (device->state != USB_STATE_CONFIGURED)
{
return -RT_ERROR;
}
ep = rt_usbd_find_endpoint(device, &func, ep_msg->ep_addr);
if(ep == RT_NULL)
{
rt_kprintf("invalid endpoint\n");
return -RT_ERROR;
}
if(EP_ADDRESS(ep) & USB_DIR_IN)
{
size = ep_msg->size;
if(ep->request.remain_size >= EP_MAXPACKET(ep))
{
dcd_ep_write(device->dcd, EP_ADDRESS(ep), ep->request.buffer, EP_MAXPACKET(ep));
ep->request.remain_size -= EP_MAXPACKET(ep);
ep->request.buffer += EP_MAXPACKET(ep);
}
else if(ep->request.remain_size > 0)
{
dcd_ep_write(device->dcd, EP_ADDRESS(ep), ep->request.buffer, ep->request.remain_size);
ep->request.remain_size = 0;
}
else
{
EP_HANDLER(ep, func, size);
}
}
else
{
size = ep_msg->size;
if(ep->request.remain_size == 0)
{
return RT_EOK;
}
if(size == 0)
{
size = dcd_ep_read(device->dcd, EP_ADDRESS(ep), ep->request.buffer);
}
ep->request.remain_size -= size;
ep->request.buffer += size;
if(ep->request.req_type == UIO_REQUEST_READ_BEST)
{
EP_HANDLER(ep, func, size);
}
else if(ep->request.remain_size == 0)
{
EP_HANDLER(ep, func, ep->request.size);
}
else
{
dcd_ep_read_prepare(device->dcd, EP_ADDRESS(ep), ep->request.buffer, ep->request.remain_size > EP_MAXPACKET(ep) ? EP_MAXPACKET(ep) : ep->request.remain_size);
}
}
return RT_EOK;
}
到底是怎么一回事呢?我在结合打印信息,根据代码整理出了函数的执行流程:
答案揭晓:上图四个标题栏中括号中的数字表示各个线程的优先级。红色填充的就是我们驱动中的发送函数。在他执行发送的时候触发了EP1 TXFIFO空中断。中断的优先级比任何的线程优先级都高,在执行往EP1 TXFIFO空中断后,按作者本意应该按照蓝实线返回执行清零。可是USB HS发送单个字节的速度太快了,程序又按照蓝色虚线指向跑去执行发送完成中断。发送完成中断会回调到rt_usbd_ep_in函数中。该函数会发送一个事件给usbd线程。usbd线程的优先级比vcom高,这里中断执行完,系统会调度到usbd线程。usbd线程中会判断ep->request.remain_size。如果大于0,表示还有数据没发送完,又调用了dcd_ep_write,从图中红色箭头过去,又调用了一次发送!
原因:这个是由于两个线程(vcom线程和usbd线程)使用公共的资源(ep->request.remain_size)时竞争导致的。如果发送完成中断执行结束之后,按照图中绿色箭头指示,调度到vcom线程去运行,这样就可以完成清零动作,再后面调度到usbd线程的时候,就不会判定成还有数据没有发送,也就不会重复发送了。
解决办法:在第9行加入rt_enter_critical(),第23行加入rt_exit_critical()。
static rt_size_t rt_usbd_ep_write(udevice_t device, uep_t ep, void *buffer, rt_size_t size)
{
rt_uint16_t maxpacket;
RT_ASSERT(device != RT_NULL);
RT_ASSERT(device->dcd != RT_NULL);
RT_ASSERT(ep != RT_NULL);
rt_enter_critical();
maxpacket = EP_MAXPACKET(ep);
if(ep->request.remain_size >= maxpacket)
{
dcd_ep_write(device->dcd, EP_ADDRESS(ep), ep->request.buffer, maxpacket);
ep->request.remain_size -= maxpacket;
ep->request.buffer += maxpacket;
}
else
{
dcd_ep_write(device->dcd, EP_ADDRESS(ep), ep->request.buffer,
ep->request.remain_size);
ep->request.remain_size = 0;
}
rt_exit_critical();
return size;
}
这就是今天的主角(也有可能是配角,哈哈哈):“调度器锁”。
同中断锁一样把调度器锁住也能让当前运行的任务不被换出,直到调度器解锁。但和中断锁有一点不相同的是,对调度器上锁,系统依然能响应外部中断,中断服务例程依然能进行相应的响应。所以在使用调度器上锁的方式进行任务同步时,需要考虑好任务访问的临界资源是否会被中断服务例程所修改,如果可能会被修改,那么将不适合采用此种方式进行同步。
这里加了调度器锁,可以保证中断的正常响应,在中断结束之后,不会被usbd线程抢占CPU,直到调度器解锁。
rt_enter_critical()与rt_exit_critical()之间的代码称之为“临界区”。