文件位置:drivers/net/sun8i_emac.c
这里顺便把linux的位置jie贴出来,因为全志H5(orangepi-pc2)的网口使用的是stm的ip核,所以驱动位置比较难找,位于drivers\net\ethernet\stmicro\stmmac\dwmac-sun8i.c ,可以作为参考。
本文主要分为两大部分,一是简要分析下uboot实现,二是结合调试经验分析下uboot实现的缺陷
1.网卡驱动uboot实现简析
1.1dma描述符
先看下dma使用的描述符(收发用同一结构体),如下:
struct emac_dma_desc {
u32 status;
u32 st;
u32 buf_addr;
u32 next;
} __aligned(ARCH_DMA_MINALIGN);
其中ARCH_DMA_MINALIGN定义为64,“__aligned”为gcc的对齐属性符号,这里告诉gcc此描述符是64字节对齐的,即便它只占用了16字节。这就意味着如果定义一个struct emac_dma_desc的数组,数组的每个元素都是64字节,而不是16字节。即,sizeof(struct emac_dma_desc)等于64。
简要介绍下描述符结构体的每个成员:
status字段:重要说一下bit31,对于收发描述符都一样,为1表示此描述符可被dma使用。Dma发送或接受完成此帧数据后会自动将bit31清0。其余字段,收、发描述符不同,具体可参考H5芯片手册。
st字段:对于发送描述符,主要包括帧长度(bit0~10)、帧开始结束标志以及发送中断控制(bit31);对于接收描述符,则主要包括接收buffer长度限制。
buf_addr字段:用于指示收发buffer物理地址。
next字段:指示下一个描述符物理地址。
1.2 网卡设备结构体
struct emac_eth_dev {
struct emac_dma_desc rx_chain[CONFIG_TX_DESCR_NUM]; //dma接收描述符数组
struct emac_dma_desc tx_chain[CONFIG_RX_DESCR_NUM]; //dma发送描述符数组
char rxbuffer[RX_TOTAL_BUFSIZE] __aligned(ARCH_DMA_MINALIGN); //接收buffer
char txbuffer[TX_TOTAL_BUFSIZE] __aligned(ARCH_DMA_MINALIGN);//发送buffer
u32 interface; //网卡phy接口协议
u32 phyaddr; //网卡phy地址
u32 link; //连接状态
u32 speed; //速度(10、100、1000)
u32 duplex; //全双工
u32 phy_configured;
u32 tx_currdescnum; //当前发送使用的描述符
u32 rx_currdescnum; //当前接收使用的描述符
u32 addr;
u32 tx_slot;
bool use_internal_phy; //内部phy
enum emac_variant variant; //emac属性
void *mac_reg; //mac寄存器基址
phys_addr_t sysctl_reg; //系统控制寄存器基址
struct phy_device *phydev;
struct mii_dev *bus; //mdio总线接口
#ifdef CONFIG_DM_GPIO
struct gpio_desc reset_gpio;
#endif
};
可以看到结构体第1、2个成员——dma描述符数组接收和发送的宏定义用反了!!不过幸亏这两个值定义为相同值,不然就走远了。这是此驱动的第一个bug!
1.3 sun8i_emac_eth_ofdata_to_platdata函数
主要是解析dts节点,获取寄存器、phy mode等信息。
1.4 sun8i_emac_eth_probe函数
- 调用sun8i_emac_board_setup函数,通过设置系统控制寄存器,使能emac时钟。
- 调用sun8i_emac_set_syscon函数,设置phy模式。
- 调用sun8i_mdio_init函数,设置mdio总线读写函数,并注册。
- 调用sun8i_phy_init函数,初始化phy
1.5 Start ops函数——sun8i_emac_eth_start
- soft reset mac
- 设置mac地址
- 设置发送、接收控制寄存器
- 设置dma 的burst len为8
- 调用rx_descs_init函数,初始化接收描述符
- 调用tx_descs_init函数,初始化发送描述符
- 调用genphy_parse_link函数,获取speed、duplex等信息
- 调用sun8i_adjust_link函数,根据获取的speed、duplex信息,设置mac的寄存器
- 使能tx、rx dma
- 使能mac rx和tx
移植时注意:发送、接收描述符中“buf_addr”和“next”都需要是物理地址(uboot中没有使能MMU,所以不需要区分)。
1.6 Send ops函数——sun8i_emac_eth_send
主要是根据需要发送包的物理地址和长度来fill发送描述符,并start 发送dma。
1.7 Recv ops函数——sun8i_emac_eth_recv
根据接收描述符status字段的bit31判断是否dma完成(完成bit31会被清零)。若完成,则取出描述符中的buffer地址返回给上层调用者。
总结:总的来说,uboot的orangepi网卡驱动比较简单。移植时注意虚实地址转换、数据cache处理、寄存器也较简单不需要做特殊处理。
2. Uboot实现缺陷分析
不过就是这么简单的一个uboot网卡驱动,确隐藏着多个缺陷。下面列举一二。
- 就是上面1.2节提到的,收发dma描述符数量宏定义。使用反了,要不是恰好两个值一样,这样的低级错误,完全是没有code review的!
- 发送函数中操作寄存器使能dma,没有进行判断
/* Start the DMA */ v = readl(priv->mac_reg + EMAC_TX_CTL1); v |= BIT(31);/* mandatory */ v |= BIT(30);/* mandatory */ writel(v, priv->mac_reg + EMAC_TX_CTL1); |
读取发送控制寄存器(EMAC_TX_CTL1)之后直接置相应位(bit31),然后写入控制寄存器。正确的做法是判断bit31是否为1,只有在dma停止执行时,才可以写此寄存器,否则在dma工作时操作寄存器是可能导致dma异常的!而且bit30在函数sun8i_emac_eth_init中已经使能,没必要每次都设置。可以改为下面的实现:
/* Start the DMA */ v = readl(priv->mac_reg + EMAC_TX_CTL1); if(~(v&BIT(31))) { v |= BIT(31);/* mandatory */ writel(v, priv->mac_reg + EMAC_TX_CTL1); } |
- 未考虑promisc模式场景
这一点除了在寄存器上需要使能外,还体现在接收函数sun8i_emac_eth_recv。其只处理了当前一个描述符。在使能promisc模式后,线路上可能同时来多个包(包括杂包)。另外,在一个包处理的同时也可能继续来包。由于有杂包的混入,只处理一个包返回给上层,并不一定就是上层需要的那个正确包。更妥当的方式是在接收函数中对所有描述符轮询一遍,保证当前所有可用的描述符都得到处理返回给上层。当然,这个逻辑也可以在上层来做。
- 未考虑中断场景
当然,uboot中均未使用中断,都是轮询模式。这里提出这点,主要是想披露下全志datasheet的bug。通看“Allwinner H5 Datasheet”居然没有找到网口中断清标志寄存器,这让我在准备自己实现中断处理函数时非常疑惑。由于H5网卡中断是高电平触发,这要没有清中断标志位的寄存器,中断一旦触发就会持续触发,不可能不提供这个寄存器的!Uboot虽然没有使用到中断,linux可是使用了的啊。经仔细研读linux相关代码,发现中断状态寄存器—— Interrupt Status Register(偏移为0x08)。其实也是中断清除寄存器(写1清除)。而datasheet显示这个寄存器为只读,也并未说明写1可以清除对应中断!!
- 函数未考虑入参有效性检查
收发函数均未对入参进行有效性检查。一般的函数也就算了,网卡收发函数入参涉及收发包物理地址指针、长度等重要信息。如果不对入参进行判断,如指针为空、发包长度为0均会导致硬件dma异常或系统崩盘。
总结:真是国产的芯片品质、国产的datasheet品质、国产的驱动品质啊!哎,不说了。