最近一直在搞zynq的PL部分,为了保持对驱动程序的敏感度,看着源码分析一下rt8152的驱动程序。之前学单片机一直想着给单片机装一个USB网卡,但是一直没有思路。今天突然想到之前的想法,就带着这个想法加上对内核驱动的怀念,写一下,写点东西有时候能让人的心平静点。
usb总线是一种常见的高速总线,其实也不算高,usb2.0规定没记错应该是480Mbit/s。物理层上,usb high使用的是差分电压型串行数据线。usb full采用的是差分电流型串行串行数据线。差分抗干扰源于模拟电路的一个差动放大器,放大差模信号,抑制共模信号。再来说一下为什么高速数据总线喜欢用电流来传输信号。我们知道当导线距离增加时,其电阻会增加,那么从发送端到接收端的电压就会发生变化。使用电流传输,在发送端放置一个电流源,无论电阻怎么变换,流过电阻的电流总是恒定的。只要在接收端的端接电阻保持稳点就可以了。
简单介绍完物理层以后,在说一下USB通信的过程。很重要的一点是每次USB通信的发起者都是主机。这点很重要,至于USB的中断传输模式、块传输。。。网上好多,这里只写一些实用的、简单的。
在linux设备驱动中,把usb控制器的驱动程序可以看成是usb总线驱动程序,在设备树中定义了usb控制器的设备节点,控制器通过和设备树匹配,获取到usb控制器的基地址、中断号等资源。这部分程序一般不需要驱动工程师去修改,一般芯片厂商会写好。一般情况下,需要关心的是USB设备驱动程序,也就是挂在USB总线上的设备。通俗的讲就是u盘,usb声卡,usb暖手宝(这算吗☺)。
usb设备的匹配不需要在设备树中进行定义,而是通过vid和pid。这是usb控制器在在完成热插拔以后做的一项检测工作,会从usb的端点0中读取usb的vid和pid,然后在和已经装在的usb驱动程序进行匹配。
static struct usb_device_id rtl8152_table[] = {
{REALTEK_USB_DEVICE(VENDOR_ID_REALTEK, 0x8152)},
{REALTEK_USB_DEVICE(VENDOR_ID_REALTEK, 0x8153)},
{REALTEK_USB_DEVICE(VENDOR_ID_SAMSUNG, 0xa101)},
{REALTEK_USB_DEVICE(VENDOR_ID_LENOVO, 0x7205)},
{REALTEK_USB_DEVICE(VENDOR_ID_LENOVO, 0x304f)},
{REALTEK_USB_DEVICE(VENDOR_ID_NVIDIA, 0x09ff)},
{}
};
这是usb网卡的设备ID匹配表。当usb设备接到usb总线上,如果正确匹配,probe函数就会被执行。
之前写过一篇 linux网卡设备驱动(任意传输介质传输(与FPGA交互)) 大致说了下网卡驱动程序的结构以及怎么实现一个网络驱动程序。
这里在赘述一下,网卡驱动程序在probe函数中:
标题其实已经说明了,USB网卡驱动程序就是USB驱动程序加上网卡驱动程序。说说题外话吧,如何用带usb控制器的单片机来和usb网卡进行数据交互呢?之前做过STM32上的USB自定义设备程序,是从机的(上位机用的是libusb+qt)。所以对单片机的usb程序有一些记忆,基本都是对端点的操作。接下来去分析linux中的usb网卡驱动程序,以此来构想下单片机如何驱动一个usb网卡。
源码在drivers\net\usb\r8152.c下
先看下入口函数:
static int rtl8152_probe(struct usb_interface *intf,const struct usb_device_id *id)
{
struct usb_device *udev = interface_to_usbdev(intf);
struct r8152 *tp;
struct net_device *netdev;
int ret;
usb_driver_set_configuration(udev, 1);
usb_reset_device(udev);
netdev = alloc_etherdev(sizeof(struct r8152));
tp->mii.dev = netdev;
tp->mii.mdio_read = read_mii_word;
tp->mii.mdio_write = write_mii_word;
tp->mii.phy_id_mask = 0x3f;
tp->mii.reg_num_mask = 0x1f;
tp->mii.phy_id = R8152_PHY_ID;
ret = register_netdev(netdev);
}
把结构性的代码留下了,可以看出在probe函数中,完成了usb操作接口的获取、网络设备的注册、pyh相关设置。
static int rtl_ops_init(struct r8152 *tp)
{
struct rtl_ops *ops = &tp->rtl_ops;
int ret = 0;
switch (tp->version) {
case RTL_VER_01:
case RTL_VER_02:
ops->init = r8152b_init;
ops->enable = rtl8152_enable;
ops->disable = rtl8152_disable;
ops->up = rtl8152_up;
ops->down = rtl8152_down;
ops->unload = rtl8152_unload;
ops->eee_get = r8152_get_eee;
ops->eee_set = r8152_set_eee;
ops->in_nway = rtl8152_in_nway;
ops->hw_phy_cfg = r8152b_hw_phy_cfg;
ops->autosuspend_en = rtl_runtime_suspend_enable;
break;
case RTL_VER_03:
case RTL_VER_04:
case RTL_VER_05:
case RTL_VER_06:
ops->init = r8153_init;
ops->enable = rtl8153_enable;
ops->disable = rtl8153_disable;
ops->up = rtl8153_up;
ops->down = rtl8153_down;
ops->unload = rtl8153_unload;
ops->eee_get = r8153_get_eee;
ops->eee_set = r8153_set_eee;
ops->in_nway = rtl8153_in_nway;
ops->hw_phy_cfg = r8153_hw_phy_cfg;
ops->autosuspend_en = rtl8153_runtime_enable;
break;
default:
ret = -ENODEV;
netif_err(tp, probe, tp->netdev, "Unknown Device\n");
break;
}
return ret;
}
这段代码完成了所有网卡的操作函数的注册。
static const struct net_device_ops rtl8152_netdev_ops = {
.ndo_open = rtl8152_open,
.ndo_stop = rtl8152_close,
.ndo_do_ioctl = rtl8152_ioctl,
.ndo_start_xmit = rtl8152_start_xmit,
.ndo_tx_timeout = rtl8152_tx_timeout,
.ndo_set_features = rtl8152_set_features,
.ndo_set_rx_mode = rtl8152_set_rx_mode,
.ndo_set_mac_address = rtl8152_set_mac_address,
.ndo_change_mtu = rtl8152_change_mtu,
.ndo_validate_addr = eth_validate_addr,
.ndo_features_check = rtl8152_features_check,
};
rtl8152_start_xmit()函数很重要,完成了网络数据包的发送。实现如下:
static netdev_tx_t rtl8152_start_xmit(struct sk_buff *skb,
struct net_device *netdev)
{
struct r8152 *tp = netdev_priv(netdev);
skb_tx_timestamp(skb);
skb_queue_tail(&tp->tx_queue, skb);
if (!list_empty(&tp->tx_free)) {
if (test_bit(SELECTIVE_SUSPEND, &tp->flags)) {
set_bit(SCHEDULE_NAPI, &tp->flags);
schedule_delayed_work(&tp->schedule, 0);
} else {
usb_mark_last_busy(tp->udev);
napi_schedule(&tp->napi);
}
} else if (skb_queue_len(&tp->tx_queue) > tp->tx_qlen) {
netif_stop_queue(netdev);
}
return NETDEV_TX_OK;
}
再到usb中断服务函数中找到接收大代码:
static void tx_bottom(struct r8152 *tp)
{
int res;
do {
struct tx_agg *agg;
if (skb_queue_empty(&tp->tx_queue))
break;
agg = r8152_get_tx_agg(tp);
if (!agg)
break;
res = r8152_tx_agg_fill(tp, agg);
if (res) {
struct net_device *netdev = tp->netdev;
if (res == -ENODEV) {
set_bit(RTL8152_UNPLUG, &tp->flags);
netif_device_detach(netdev);
} else {
struct net_device_stats *stats = &netdev->stats;
unsigned long flags;
netif_warn(tp, tx_err, netdev,
"failed tx_urb %d\n", res);
stats->tx_dropped += agg->skb_num;
spin_lock_irqsave(&tp->tx_lock, flags);
list_add_tail(&agg->list, &tp->tx_free);
spin_unlock_irqrestore(&tp->tx_lock, flags);
}
}
} while (res == 0);
}
这里使用了中断的下半部,说一下这个知识,在linux内核中,中断处理一般分为以下几种方式:
直接处理就是在中断服务函数中直接处理相应的逻辑,这种情况对应于十分简短的中断处理逻辑;
软中断和tasklet都是发生在中断上下文的,因此在处理函数中不能有休眠。工作队列和中断线程化可以用休眠,但是处理的实时性会差一些。
大体结构已经出来了,还有一个urb的知识没有说;在linux驱动中,通过驱动程序通过urb和设备进行通信。urb相当于网络设备驱动程序中的skb。是传输数据的载体。
static void write_bulk_callback(struct urb *urb)
{
struct net_device_stats *stats;
struct net_device *netdev;
struct tx_agg *agg;
struct r8152 *tp;
int status = urb->status;
agg = urb->context;
if (!agg)
return;
tp = agg->context;
if (!tp)
return;
netdev = tp->netdev;
stats = &netdev->stats;
if (status) {
if (net_ratelimit())
netdev_warn(netdev, "Tx status %d\n", status);
stats->tx_errors += agg->skb_num;
} else {
stats->tx_packets += agg->skb_num;
stats->tx_bytes += agg->skb_len;
}
spin_lock(&tp->tx_lock);
list_add_tail(&agg->list, &tp->tx_free);
spin_unlock(&tp->tx_lock);
usb_autopm_put_interface_async(tp->intf);
if (!netif_carrier_ok(netdev))
return;
if (!test_bit(WORK_ENABLE, &tp->flags))
return;
if (test_bit(RTL8152_UNPLUG, &tp->flags))
return;
if (!skb_queue_empty(&tp->tx_queue))
napi_schedule(&tp->napi);
}
static void intr_callback(struct urb *urb)
{
struct r8152 *tp;
__le16 *d;
int status = urb->status;
int res;
tp = urb->context;
if (!tp)
return;
if (!test_bit(WORK_ENABLE, &tp->flags))
return;
if (test_bit(RTL8152_UNPLUG, &tp->flags))
return;
switch (status) {
case 0: /* success */
break;
case -ECONNRESET: /* unlink */
case -ESHUTDOWN:
netif_device_detach(tp->netdev);
case -ENOENT:
case -EPROTO:
netif_info(tp, intr, tp->netdev,
"Stop submitting intr, status %d\n", status);
return;
case -EOVERFLOW:
netif_info(tp, intr, tp->netdev, "intr status -EOVERFLOW\n");
goto resubmit;
/* -EPIPE: should clear the halt */
default:
netif_info(tp, intr, tp->netdev, "intr status %d\n", status);
goto resubmit;
}
d = urb->transfer_buffer;
if (INTR_LINK & __le16_to_cpu(d[0])) {
if (!netif_carrier_ok(tp->netdev)) {
set_bit(RTL8152_LINK_CHG, &tp->flags);
schedule_delayed_work(&tp->schedule, 0);
}
} else {
if (netif_carrier_ok(tp->netdev)) {
set_bit(RTL8152_LINK_CHG, &tp->flags);
schedule_delayed_work(&tp->schedule, 0);
}
}
resubmit:
res = usb_submit_urb(urb, GFP_ATOMIC);
if (res == -ENODEV) {
set_bit(RTL8152_UNPLUG, &tp->flags);
netif_device_detach(tp->netdev);
} else if (res) {
netif_err(tp, intr, tp->netdev,
"can't resubmit intr, status %d\n", res);
}
}
这是在usb网卡驱动中操作urb的实例,看起来很像i2c驱动中的i2c_msg;
usb驱动驱动具体操作下次会具体的说明。这次重点是usb网卡的结构分析;
回到最初的愿望,用单片机去控制usb网卡,实现单片机上网可行吗?
答案是可行的,首先要让单片机上有一个简单的操作系统,这样方便网络协议栈的移植,然后就是把单片机的usb host 调好,通过读取usb设备的端点来实现usb网卡和单片机的数据交互。之后有时间做一下这个,实现下最初的理想。人有时候在不具备条件时,总是幻想着自己的梦想,当具备条件时,最初的梦想早已忘得一干二净。