ch394 spi接口转以太网设备调试记录

前言:此文档针对的是有线网口

一、调试前需要先了解如下知识:

1、申请netdevice

#define alloc_netdev(sizeof_priv, name, name_assign_type, setup) \ 
 alloc_netdev_mqs(sizeof_priv, name, name_assign_type, setup, 1, 1)

alloc_netdev本质调用的是alloc_netdev_mqs原型如下:
struct net_device * alloc_netdev_mqs ( int sizeof_priv, const char *name, void (*setup) (struct net_device *)),unsigned int txqs, unsigned int rxqs);

sizeof_priv:私有数据块大小。 
name:设备名字。 
setup:回调函数,初始化设备的设备后调用此函数。 
txqs:分配的发送队列数量。 
rxqs:分配的接收队列数量。 
返回值:如果申请成功的话就返回申请到的 net_device 指针,失败的话就返回 NULL


注册网络设备前首先需要申请网络设备,网络设备有很多,包括网卡、路由器、交换机以及CAN网络设备等
,
Linux内核针对以太网设备封装了申请etherdev接口:

#define alloc_etherdev(sizeof_priv) alloc_etherdev_mq(sizeof_priv, 1) 
#define alloc_etherdev_mq(sizeof_priv, count) alloc_etherdev_mqs(sizeof_priv,count,count)

可以看到最终调用的是这个:
struct net_device *alloc_etherdev_mqs(int sizeof_priv, unsigned int txqs,				      unsigned int rxqs)
{	
return alloc_netdev_mqs(sizeof_priv, "eth%d",NET_NAME_UNKNOWN,ether_setup, txqs, rxqs);
}

其中ether_setup会对net_devie做初步初始化原型如下:
void ether_setup(struct net_device *dev)
{	
    dev->header_ops	= &eth_header_ops;	
    dev->type		= ARPHRD_ETHER;	
    dev->hard_header_len = ETH_HLEN;	
    dev->min_header_len = ETH_HLEN;	
    dev->mtu           = ETH_DATA_LEN;	
    dev->min_mtu	= ETH_MIN_MTU;	
    dev->max_mtu	= ETH_DATA_LEN;
    dev->addr_len	= ETH_ALEN;	
    dev->tx_queue_len	= DEFAULT_TX_QUEUE_LEN;	
    dev->flags		= IFF_BROADCAST|IFF_MULTICAST;	
    dev->priv_flags	|= IFF_TX_SKB_SHARING;
    eth_broadcast_addr(dev->broadcast);
}

2、填充net_device_ops

struct net_device_ops {
..............	
    int	(*ndo_open)(struct net_device *dev);	
    int	(*ndo_stop)(struct net_device *dev);	
    netdev_tx_t	(*ndo_start_xmit)(struct sk_buff *skb),
    int	(*ndo_set_mac_address)(struct net_device *dev),
...............
};

此结构体成员函数很多,不需要全部都填充,只需要填充如上必要的接口。 
ndo_open 函数,打开网络设备的时候此函数会执行,网络驱动程序需要实现此函数,非常重要!会在此函数中做如下工作: 
·使能网络外设时钟。 
·申请网络所使用的环形缓冲区。 
·初始化 MAC 外设。 
·绑定接口对应的 PHY。 
·如果使用 NAPI 的话要使能 NAPI 模块,通过 napi_enable 函数来使能。 
·开启 PHY。 
·调用 netif_tx_start_all_queues 来使能传输队列,也可能调用 netif_start_queue 函数。
 
·…… 
ndo_stop 函数,关闭网络设备的时候此函数会执行,网络驱动程序也需要实现此 
函数。会在此函数中做如下工作: 
·停止 PHY。 
·停止 NAPI 功能。 
·停止发送功能。 
·关闭 MAC。 
·断开 PHY 连接。 
·关闭网络时钟。 
·释放数据缓冲区。 

·…… 
ndo_start_xmit 函数,当需要发送数据的时候此函数就会执行,此函数有一个参数为 sk_buff 结构体指针,sk_buff 结构体在 Linux 的网络驱动中非常重要,sk_buff 保存了上层传递给网络驱动层的数据。也就是说,要发送出去的数据都存在了 sk_buff 中。如果发送成功的话此函数返回 NETDEV_TX_OK,如果发送失败了就返回 NETDEV_TX_BUSY,如果发送失败了我们就需要停止队列。
ndo_set_mac_address 函数,此函数用于修改网卡的 MAC 地址,设置 net_device 
的 dev_addr 成员变量,并且将 MAC 地址写入到网络外设的硬件寄存器中。

①数据的发送

此函数用于将网络数据发送出去:

int dev_queue_xmit(struct sk_buff *skb);

最终调用的是上面提到net_device_opsndo_start_xmit(struct sk_buff *skb)。

skb:要发送的数据,这是一个 sk_buff 结构体指针,sk_buff 是 Linux 网络驱动中一个非常重要的结构体,网络数据就是以 sk_buff 保存的,各个协议层在 sk_buff 中添加自己的协议头, 最终由底层驱动将 sk_buff 中的数据发送出去。网络数据的接收过程恰好相反,网络底层驱动将接收到的原始数据打包成 sk_buff,然后发送给上层协议,上层会取掉相应的头部,然后将最终的数据发送给用户。

返回值:0 发送成功,负值 发送失败。

②数据的接收

上层接收数据的话使用 netif_rx 函数,但是最原始的网络数据一般是通过轮询、中断或 NAPI的方式来接收。

int netif_rx(struct sk_buff *skb);

skb保存接收数据的 sk_buff,。

返回值:NET_RX_SUCCESS 成功,NET_RX_DROP 数据包丢弃。

3、sk_buff结构体

sk_buff Linux 网络重要的数据结构,用于管理接收或发送数据包。

struct sk_buff { 
    union { 
        struct{ 
            /* These two members must be first. */ 
            struct sk_buff *next; 
            struct sk_buff *prev; 
            union { 
                ktime_t tstamp; 
                struct skb_mstamp skb_mstamp; 
            }; 
        }; 
    struct rb_node rbnode; /* used in netem & tcp stack */ 
}; 
    struct sock *sk; 
    struct net_device *dev; 
    char cb[48] __aligned(8); 
    unsigned long _skb_refdst; 
    void (*destructor)(struct sk_buff *skb); 
...... 
    unsigned int len, data_len; 
    __u16 mac_len, hdr_len; 
...... 
    __be16 protocol; 
    __u16 transport_header; 
    __u16 network_header; 
    __u16 mac_header; 
 
    /* private: */ 
    __u32 
    headers_end[0]; 
    /* public: */ 
 
    /* These elements must be at the end, see alloc_skb() for details. */ 
    sk_buff_data_t tail; 
    sk_buff_data_t end; 
    unsigned char *head, *data; 
    unsigned int truesize; 
    atomic_t users; 
};

nextprev 分别指向下一个和前一个 sk_buff,构成一个双向链表。

tstamp 表示数据包接收时或准备发送时的时间戳。

sk 表示当前 sk_buff 所属的 Socket。

dev 表示当前 sk_buff 从哪个设备接收到或者发出的。

cb 为控制缓冲区,不管哪个层都可以自由使用此缓冲区,用于放置私有数据。

destructor 函数,当释放缓冲区的时候可以在此函数里面完成某些动作。

len 为实际的数据长度,包括主缓冲区中数据长度和分片中的数据长度。data_len

为数据长度,只计算分片中数据的长度。

mac_len 为连接层头部长度,也就是 MAC 头的长度。

protocol 协议。

transport_header 为传输层头部。

network_header 为网络层头部

mac_header 为链接层头部。

tail 指向实际数据的尾部。

end 指向缓冲区的尾部。

head 指向缓冲区的头部,data 指向实际数据的头部。datatail 指向实际数据的头部和尾部,head end 指向缓冲区的头部和尾部。

1、分配 sk_buff

要使用 sk_buff 必须先分配:

static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)

size:要分配的大小,也就是 skb 数据段大小。

priority:为 GFP MASK 宏,比如 GFP_KERNEL、GFP_ATOMIC 等。

返回值:分配成功的话就返回申请到的 sk_buff 首地址,失败的话就返回 NULL。

static inline struct sk_buff *netdev_alloc_skb(struct net_device *dev, unsigned int length)

dev:要给哪个设备分配 sk_buff。

length:要分配的大小。

返回值:分配成功的话就返回申请到的 sk_buff 首地址,失败的话就返回 NULL。

2、释放 sk_buff

当使用完成以后就要释放掉 sk_buff

void kfree_skb(struct sk_buff *skb)  

对于网络设备而言最好使用如下所示释放函数:

void dev_kfree_skb (struct sk_buff  *skb)

3、变更 sk_buff

注意:此操作仅仅只是变更skb_buff大小,并未修改数据

①skb_put

在尾部扩展 skb_buff 的数据区,也就是将 skb_buff 的 tail指针 后移 n 个字节,从而将 skb_buff 的 len 增加 n 个字节。

unsigned char *skb_put(struct sk_buff *skb, unsigned int len)

skb:要操作的 sk_buff。

len:要增加多少个字节。

返回值:扩展出来的那一段数据区首地址。

②skb_push

在头部扩展 skb_buff 的数据区

unsigned char *skb_push(struct sk_buff  *skb, unsigned int len)

skb:要操作的 sk_buff。

len:要增加多少个字节。

返回值:扩展完成以后新的数据区首地址。

③sbk_pull

从 sk_buff 的数据区起始位置删除数据

unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)

skb:要操作的 sk_buff。

len:要删除的字节数。

返回值:删除以后新的数据区首地址。

④skb_reserve

调整缓冲区的头部大小,方法很简单讲 skb_buff 的 data 和 tail 同时后移 n 个字节即可。

static inline void skb_reserve(struct sk_buff *skb, int len)

skb:要操作的 sk_buff。

len:要增加的缓冲区头部大小。

返回值:无。

二、实际代码大致流程:

ch394有两种寄存器类型,通用寄存器和socket寄存器,由数据帧格式中的8位控制字中的bit3区分,如下图。寄存器类型和读写需要做区分,发送缓存和接收缓存不需要作区分,最开始写spi通信接口时就被手册的介绍迷惑了。由于ch394已经集成了phy芯片,所以不需要我们再用mdio去做phy相关初始化。

struct ch394_priv *priv;  //自行定义的私有数据
struct net_device *ndev; 

1、申请netdevie  

ndev = alloc_etherdev(priv)

将申请到的netdev设置到dev

SET_NETDEV_DEV(ndev, dev);
dev_set_drvdata(dev, ndev);

拿到私有数据内存地址

priv = netdev_priv(ndev);

2、填充需要用的私有数据

包括socket0寄存器地址(需要根据实际使用IC设置,这里是为了方便spi数据通信)、中断号、netdev_ops和ethtool_ops。

priv->s0_regs = CH394_S0_REGS;
priv->s0_tx_buf = CH394_TX_MEM_START;
priv->s0_tx_buf_size = CH394_TX_MEM_SIZE;
priv->s0_rx_buf = CH394_RX_MEM_START;
priv->s0_rx_buf_size = CH394_RX_MEM_SIZE;

priv->ndev = ndev;
priv->ops = ops;
priv->irq = irq;

ndev->netdev_ops = &ch394_netdev_ops;
ndev->ethtool_ops = &ch394_ethtool_ops;

3、注册netdev

register_netdev(ndev)

4、设置物理地址

if(mac_addr)  //如果设置了物理地址则直接设置到netdev
    memcpy(ndev->dev_addr, mac_addr, ETH_ALEN);
else
    eth_hw_addr_random(ndev);  //没有则使用随机物理地址

5、复位初始化

接下来是做基本的复位初始化,不同ic有不同的操作,具体需要看datasheet,当前ch394需要做如下操作:

static int ch394_hw_reset(struct ch394_priv *priv)
{
    u32 rtr;

    ch394_reset(priv);  //软复位,对通用寄存器0x00写0x80

    ch394_disable_intr(priv); //Socket中断使能寄存器0x18写0


    ch394_write_macaddr(priv); //将上面ndev->dev_addr设置的物理地址填充到MAC 地址寄存器(MAC)[0x0009-0x000E]

    ch394_memory_configure(priv); //设置发送和接收缓存区的大小为最大,并且将缓存区初始化为0为后面使用。
    rtr = CH394_RTR;

    //设置 TCP 通讯中的重传时间,超时则进行重传操作或者触发超时中断
    //当前设置默认值为200ms
    if (ch394_read16(priv, rtr) != RTR_DEFAULT)   
        return -ENODEV;

    return 0;
}

6、申请中断线程

NETIF_START_QUEUE:

驱动程序调用这个函数来告诉内核网络子系统,现在可以开始数据包的发送。

NETIF_STOP_QUEUE

在驱动程序中调用这个函数来告诉内核的网络子系统,当前网卡设备内存不够,不能继续传输数据包,内核要停止数据包的发送。

NETIF_WAKE_QUEUE:

除了实现netif_start_queue的作用外,还会将设备的发送队列加入到CPU的发送队列,并且触发中断处理的下半部来触发数据包发送。

//中断线程化,下半部的一种方式,第二个参数是中断上半部(这里不使用,直接设置为NULL),第三个参数是中断下半部

request_threaded_irq(priv->irq, NULL, ch394_interrupt, IRQF_TRIGGER_LOW | IRQF_ONESHOT,
   netdev_name(ndev), ndev);


static irqreturn_t ch394_interrupt(int irq, void *ndev_instance)
{
    struct net_device *ndev = ndev_instance;
    struct ch394_priv *priv = netdev_priv(ndev);

    int ir = ch394_read(priv, CH394_S0_IR(priv));   //读中断寄存器,看看是产生了什么中断
    if (!ir)
        return IRQ_NONE;
    ch394_write(priv, CH394_S0_IR(priv), ir);  //清中断

    if (ir & S0_IR_SENDOK) {   //ch394网卡发送完成中断
        netif_dbg(priv, tx_done, ndev, "tx done\n");
        netif_wake_queue(ndev); //会调用ndo_start_xmit发送数据,最终调用到我们自定义的发送队    列ch394_start_tx->ch394_tx_work (具体解析在后面)
    }

    if (ir & S0_IR_RECV) {  //接收中断
        ch394_disable_intr(priv);

        queue_work(priv->xfer_wq, &priv->rx_work);   //将接收数据的工作队列加到系统调度中
    }

    return IRQ_HANDLED;
}

7、发送数据和接收数据队列

①接收队列

static void ch394_rx_work(struct work_struct *work)
{
    struct ch394_priv *priv = container_of(work,struct ch394_priv, rx_work);
    struct sk_buff *skb;
    while ((skb = ch394_rx_skb(priv->ndev)))
        netif_rx(skb); //接收来自网卡的数据包提交到网络协议栈

    ch394_enable_intr(priv);  //使能socket0中断
}



static struct sk_buff *ch394_rx_skb(struct net_device *ndev)
{
    struct ch394_priv *priv = netdev_priv(ndev);
    struct sk_buff   *skb;
    u16    rx_len;
    u16    offset;
    u8    header[2];

    //先获取socket0接收到数据的长度
    u16 rx_buf_len = ch394_read16(priv, CH394_S0_RX_RSR(priv));

    if (rx_buf_len == 0)
        return NULL;

    offset = ch394_read16(priv, CH394_S0_RX_RD(priv)); //将接收缓存区读地址拿到


    ch394_readbuf(priv, offset, header, 2);  //将读地址存储的协议头读取到header


    rx_len = get_unaligned_be16(header) - 2; //减去协议头两个字节就是实际数据长度 



netdev_alloc_skb_ip_align此函数的作用是申请一个skb描述符及为报文数据buffer分配内存空间。
通常内存空间的分配地址以32位处理器来说是四字节对齐的,但由于以太网的帧格式的
MAC头没有四字节对齐,这样会导致以太网报文的IP头也不符合四字节对齐,
如此会造成IP协议解析校验错误。如果在申请skb描述符时
调用netdev_alloc_skb_ip_align(),分配的报文空间做DMA映射时MAC头
会自动四字节对齐,以使接收到的以太网报文在上传到IP层时能够解析通过。


    skb = netdev_alloc_skb_ip_align(ndev, rx_len);
    if (unlikely(!skb)) {  //接收的数据异常处理
        ch394_write16(priv, CH394_S0_RX_RD(priv), offset + rx_buf_len);
        ch394_command(priv, S0_CR_RECV);
        ndev->stats.rx_dropped++;
        return NULL;
    }

    skb_put(skb, rx_len);  //在skb尾部增加rx_len长度的内存
    ch394_readbuf(priv, offset + 2, skb->data, rx_len);  //将接收缓存区的数据读取到skb
    ch394_write16(priv, CH394_S0_RX_RD(priv), offset + 2 + rx_len); //在读取完数据后更新该寄存器值


    ch394_command(priv, S0_CR_RECV); //发送socket0接收控制命令
    skb->protocol = eth_type_trans(skb, ndev);  //用于初始化skb的protocol字段,然后通过            netif_rx(skb)将skb传递给传输层协议。

    ndev->stats.rx_packets++;
    ndev->stats.rx_bytes += rx_len;

    return skb;
}

到这里将从网卡接收到的数据提交到系统网络协议栈的操作已完成。

②发送队列

static void ch394_tx_work(struct work_struct *work)
{
    struct ch394_priv *priv =
        container_of(work, struct ch394_priv, tx_work);
    struct sk_buff *skb = priv->tx_skb;

    priv->tx_skb = NULL; //发送前清理数据防止出错

    if (WARN_ON(!skb))
        return;

    ch394_tx_skb(priv->ndev,skb); 
}



static void ch394_tx_skb(struct net_device *ndev, struct sk_buff *skb)
{
    struct ch394_priv *priv = netdev_priv(ndev);
    u16    offset;

    ch394_disable_rx(priv);

    ch394_read16(priv, CH394_SOCK0_TX_FS0); //读取空闲的发送缓存区长度,此操作也可以忽略(对比了多个内核网卡驱动有些是有做这个操作有些是没有的)

    offset = ch394_read16(priv, CH394_S0_TX_WR(priv));  //读取发送缓存区写指针
    ch394_writebuf(priv, offset, skb->data, skb->len); //把skb的数据发送到写指针

    ch394_write16(priv, CH394_S0_TX_WR(priv), offset + skb->len);  //在写入待发送数据后更新该寄存器值


    ndev->stats.tx_bytes += skb->len;
    ndev->stats.tx_packets++;

    dev_kfree_skb(skb);

    ch394_command(priv, S0_CR_SEND);  //发送数据控制命令
}

数据包的发送和接收到这里就完成了。

8、net_device_ops

上面提到的ch394_netdev_ops到这里就可以去做填充了,这些接口无非就是读写ch394的寄存器,或者调度发送和接收数据的工作队列,具体就不做分析,直接贴上源码。

static const struct net_device_ops ch394_netdev_ops = {
        .ndo_open = ch394_open,
        .ndo_stop = ch394_stop,
        .ndo_start_xmit = ch394_start_tx,
        .ndo_tx_timeout = ch394_tx_timeout,
        .ndo_set_rx_mode = ch394_set_rx_mode,
        .ndo_set_mac_address = ch394_set_macaddr,
        .ndo_validate_addr = eth_validate_addr,
};









static void ch394_set_rx_mode(struct net_device *ndev)
{
    struct ch394_priv *priv = netdev_priv(ndev);
    bool    set_promisc = (ndev->flags & IFF_PROMISC) != 0;

    if (priv->promisc != set_promisc) {
        priv->promisc = set_promisc;

    schedule_work(&priv->setrx_work);
    }
}

static int ch394_set_macaddr(struct net_device *ndev, void *addr)
{
    struct ch394_priv *priv = netdev_priv(ndev);
    struct sockaddr   *sock_addr = addr;

    if (!is_valid_ether_addr(sock_addr->sa_data))
        return -EADDRNOTAVAIL;
    #if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 16, 0))
        eth_hw_addr_set(ndev, sock_addr->sa_data);
    #else
        memcpy(ndev->dev_addr, sock_addr->sa_data, ETH_ALEN);
    #endif
    ch394_write_macaddr(priv);

    return 0;
}

static int ch394_open(struct net_device *ndev)
{
    struct ch394_priv *priv = netdev_priv(ndev);

    netif_info(priv, ifup, ndev, "enabling\n");
    ch394_hw_start(priv);   //设置透传模式,打开socket,使能socket中断
    netif_start_queue(ndev);   //告诉内核数据包可以发送

    netif_carrier_on(ndev);  //告诉内核需要传递信号

    return 0;
}

static int ch394_stop(struct net_device *ndev)
{
    struct ch394_priv *priv = netdev_priv(ndev);

    netif_info(priv, ifdown, ndev, "shutting down\n");
    ch394_hw_close(priv);  //关闭中断以及socket
    netif_carrier_off(ndev);  //告诉内核丢失信号
    netif_stop_queue(ndev); //告诉内核停止发送数据包

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值