我是linux的新手,可以说从来没有在linux下写过程序,对于linux内核也是相当陌生,前一段时间,拿着tpu一个移植好了的uClinux在S3C44B0(ARM7TDMI核的嵌入式处理器)上的版本,把它成功的跑在了我自己的S3C44B0的板子上,这也就算是平生在uClinux下作的第一个工作吧。接下来就是添加网卡驱动,我用的是RTL801ArrayAS--比较常用的ISA接口的以太网芯片。下面就从一个新手的角度来说说我的移植过程吧,其实很简单,我的整个摸索+移植的过程也就花了2天的时间,我尽量写的详细(罗嗦?)一点,希望对像我这样的新手入门有所帮助,错误之处在所难免,欢迎指正。
开始的时候,我也是摸不着头脑,不知道该从什么地方入手。用SoureInsight把整个uClinux内核的源码都添加进来,熟悉一下linux的内核(其实就是在里面瞎撞,也不怎么能看懂)。按照linux内核目录的分类,很自然的就找到Ne2000网卡的驱动就是./drivers/net/ne.c,和它相关的还有83Array0.h和83Array0.c。看看代码,逐渐的就明白了:
首先,在Ne.c中函数ne_probe就是网卡的检测函数,如果检测到Ne2000兼容的网卡就return 0。那个函数没有什么具体的工作,就是搭了一个架子。看的有前人在这个函数开始写到:
#if defined (CONFIG_NETtel) && defined (CONFIG_M5307)
…………
#elif defined(CONFIG_COLDFIRE)
static int once = 0;
if (once)
return -ENXIO;
if (base_addr == 0) {
dev->base_addr = base_addr = NE2000_ADDR;
dev->irq = NE2000_IRQ_VECTOR;
once++;
}
#endif
就明白了,可以把网卡的基地址、中断号都放到这里面定义。我也跟着照葫芦画瓢,添
加了一个:
#elif defined(CONFIG_ARCH_S3C44B0) //--by threewater
static int once = 0;
if (once)
return -ENXIO;
if (base_addr == 0) {
dev->base_addr = base_addr = ARM_NE2000_BASE;
dev->irq = ARM_NE2000_IRQ;
once++;
}
其中:ARM_NE2000_BASE和ARM_NE2000_IRQ是在配置内核的时候定义的,这个以后再说。
接下来,具体的工作就转移到了ne_probe1函数里面做。用SourceInight跟进来看(这个软件太好用了,忍不住在这里再坐一会广告)。Ne_probe1中,一开始就是
reg0 = inb_p(ioaddr);
if (reg0 == 0xFF) {
ret = -ENODEV;
goto err_out;
}
很容易理解,就是读一下网卡的基地址,对我来说也就是RTL801Array的REG0,如果是0xff,说明没有检测到网卡,返回错误。好了,在下面添加一行
printk("begin find Ne2000 Net Card...\tbase address=0x%X\n",ioaddr);
//--add by threewater
来证明我们的想法是正确的,程序如果能读取801Array的REG0,就应该显示出这一行。可是,那个ne_probe是谁调用的呢?还是用SourceInsight去找,用jamp to caller,哈哈,太容易了,立刻就看到了,网卡的检测是从./drivers/net/Space.c的ethif_probe函数
中实现的,关键代码:
if (probe_list(dev, eisa_probes) == 0)
return 0;
eisa_probes在前面定义成全局:
static struct devprobe eisa_probes[] __initdata = {
#ifdef CONFIG_DE4X5 /* DEC DE425, DE434, DE435 adapters */
{de4x5_probe, 0},
#endif
…………
{NULL, 0},
};
我也照着添加了:
if (probe_list(dev, arm_probes) == 0)
return 0;
并定义:
static struct devprobe arm_probes[] __initdata = {
#ifdef CONFIG_ARM
{ne_probe, 0},
#endif
{NULL, 0},
};
这样,编译内核启动,果然,显示出了输出结果。
继续分析修改ne.c中ne_probe1的代码(关键的东东全在这里面呢)。接下来就是
outb_p(E83Array0_NODMA+E83Array0_PAGE1+E83Array0_STOP, ioaddr + E83Array0_CMD);
regd = inb_p(ioaddr + 0x0d);
outb_p(0xff, ioaddr + 0x0d);
读取REGD中的数据,这里,再仔细跟踪一下outb_p这个函数,在x86中,这个就是一个IO口的输出函数,在S3C44B0是存储器和IO统一编址的(或者说不分存储器还是IO),经过了几次宏定义以后,很快找到如下宏代码:
(*(volatile unsigned char *)(a))
和我想的一样,就是靠这个访问外部总线的。我的801Array在S3C44B0的Bank 5上,工作在跳线模式,算了一下,起始基地址就是0x0a000600。
这里,需要说明一下我的硬件配置和连接,801Array工作在16位模式下,S3C44B0的Bank5配置成16位模式,数据线一对一的连接,地址线错开一位--801Array的A0连接S3C44B0的A1……这样,801Array的基地址(Reg0的地址)是0x0a000600,Reg1的地址就是0x0a000602……地址不是连续增加的,所以,对应的驱动程序要做相应的修改。查找E83Array0_CMD的定义,发现,在83Array0.h中有:
#define E83Array0_CMD EI_SHIFT(0x00) /* The command register (for all pages)
*/
/* Page 0 register offsets. */
#define EN0_CLDALO EI_SHIFT(0x01) /* Low byte of current local dma addr
RD */
#define EN0_STARTPG EI_SHIFT(0x01) /* Starting page of ring bfr WR */
……
而EI_SHIFT根据不同的配置有两种定义,如下:
#if defined(CONFIG_MAC) || defined(CONFIG_AMIGA_PCMCIA) || \
defined(CONFIG_ARIADNE2) || defined(CONFIG_ARIADNE2_MODULE) || \
defined(CONFIG_HYDRA) || defined(CONFIG_HYDRA_MODULE) || \
defined(CONFIG_ARM_ETHERH) || defined(CONFIG_ARM_ETHERH_MODULE)
#define EI_SHIFT(x) (ei_local->reg_offset[x])
#else
#define EI_SHIFT(x) (x)
#endif
看来,在83Array0的驱动中已经考虑到了不连续增长的地址的问题了,继续跟踪查看ei_local->regoffset[x]的定义就比较麻烦了。干脆,我用一个笨方法,直接添加:
#elif defined(CONFIG_ARM) || defined(CONFIG_ARM_MODULE) //--by
threewater
#define EI_SHIFT(x) ((x)*2)
对应的,在ne.c也有类似的定义问题:
#define NE_CMD 0x00
#define NE_DATAPORT 0x10 /* NatSemi-defined port window offset. */
#define NE_RESET 0x1f /* Issue a read to reset, a write to clear. */
#define NE_IO_EXTENT 0x20
添加成:
#ifdef CONFIG_ARM //--by threewater
#define NE_CMD 0x00
#define NE_DATAPORT 0x20 /* NatSemi-defined port window offset. */
#define NE_RESET 0x3e /* Issue a read to reset, a write to clear. */
#define NE_IO_EXTENT 0x40
#else
……
这样,地址偏移的问题就基本解决了。当然,在Ne.c中,也有直接访问reg的代码,比如上面说的代码也相应的添加成:
#ifdef CONFIG_ARM //--add by threewater
regd = inb_p(ioaddr + 0x0d*2);
outb_p(0xff, ioaddr + 0x0d*2);
#else
regd = inb_p(ioaddr + 0x0d);
outb_p(0xff, ioaddr + 0x0d);
我没有看过linux编程的规范,也不知的修改内核有什么规矩,不过,我都是用预处理来添加我自己的代码,从来不直接在原有的代码上修改,我觉得这样更可以保证代码的完整性和可移植性,而且,还容易比较,容易找出问题(当然,如果#if嵌套多了,也很难看的:()。
接下来的初始化801Array,就没有什么问题了,然后就是配置网卡的物理地址了。在我的系统上,没有使用801Array的初始化配置芯片,物理地址需要在程序中直接写入(其实,就是使用配置芯片,也需要用程序读出再写入的),物理地址可以编译到代码里,也可以存储到flash的一个固定地址中。可以参考ne_probe1里面的例子,照着勒就可以了。剩下注册中断什么的,也就是算好了中断号,照着添加自己的代码。很容易的。
到这里,似乎就没有什么工作了。编译内核,启动,恩Ne2000兼容的网卡找到了,接下来就不正常了。系统报告,反复陷入那个网卡的中断……
反复陷入中断,很容易想到就是中断模式配置的问题,801Array的中断是高电平有效,看看S3C44B0上的配置,果然不对。这个配置是在Bootloader中做好了的,改一下,就好了。我把他改成了上升沿触发。
别人的批注:
最好改为高电平触发,我就吃过这样的苦头,当时我那个驱动不太稳定,一旦有错误,就死活不工作,后来发现是801Array的中断线一直为高,显示有中断,但CPU不知什么原因,开始的时候没有检测到上升沿,因此以后就再也收不到中断了,把触发方式改了以后,就非常稳定了
另外,因为S3C44B0是IO空间和存储器统一编址。这就容易忽视一个问题,就是缓冲。对于存储器,加上片内的缓冲可以提高效率,不过对于外部设备比如这个801Array,就不能使用缓冲。记住,缓冲的范围一定要配置正确,我开始就弄错了,产生了一些莫名其妙的问题,耽误了不少时间。
上述问题都解决了,启动的时候可以找到网卡,可以配置好物理地址,启动以后ifconfig eth0也没有问题,这次应该没有问题了吧。可是,结果还是ping通。这次就比较麻烦了,没有内核跟踪调试的手段,只能靠printk来输出?不知道应该从哪里入手了。不过还好,调试以太网,有Sniffer(一个功能强大的抓包软件,在局域网内的数据包都是抓到)。就靠它了,在我的PC上运行,抓包。在uClinux下ping主机的IP。果然能抓到数据包。分析抓取的数据包发现问题。
按理说,ping的时候,第一次不知道目的主机的Mac地址,所以,应该发送ARP广播,发送的数据大概的格式开头应该是FF FF FF FF FF FF AA BB CC DD EE FF…………(AABB CC DD EE FF表示发送方的Mac地址),可是我抓到的数据包是FF FF FF FF FF FF AA AA BB BB CC CC DD DD EE EE FF FF…… 看明白了,这个问题应该是网卡发送的时候,向网卡写入数据连续写了两次。这个问题最容易让人想到是S3C44B0的挂801Array的那个Bank的数据宽度配置错了。可是,我仔细的看了,不是这个问题。那就只有再仔细看看源码了。还是在drivers/net/ne.c里面,ne_block_output函数--这个就是801Array发送时候调用的函数了,里面有代码:
if (ei_status.word16) {
outsw(NE_BASE + NE_DATAPORT, (void*)buf, count>>1);
} else {
outsb(NE_BASE + NE_DATAPORT, (void*)buf, count);
}
我跟踪了一下,ei_status.word16=1,这个没有问题。那么,问题就出在outsw函数上了。用SourceInsight一层层的跟踪(做一个函数右一个宏的,定义的可真多,好多不同模式或者处理器下的相同定义,要看清楚自己的),最后,终于把目标锁定在了arch/armnommu/lib/ io-writesw-armv3.S和io-writesw-armv4.S两个汇编文件。到底是哪个呢?
熟悉ARM家族的人应该知道ARMv3和ARMv4的一些区别,看看这两汇编,就可以开出来他们对16位数读写操作的不同,按照道理S3C44B0应该是ARMv4(我记得应该是,不到出处了,至少看了那个两汇编文件,我认定应该用ARMv4那个),可是,看了一下便一输出的.o文件,是io-writesw-armv3.o,显然弄错了,这里就是问题了。那么为什么要编译ARMv3而不是ARMv4这个文件呢?在Makefile和Config.in中经过一番寻找,终于找,原来在定义arch/armnommu/config.in中,定义CONFIG_ARCH_S3C44B0的时候,没有定义
CONFIG_CPU_32v4
那么,默认情况下,就定义CONFIG_CPU_32v3,用它来编译。好了。把ARMv4的定义添上。顺便把前面说的ARM_NE2000_BASE和ARM_NE2000_IRQ的定义以添加到这里,让用户可以自己定义裁剪。
hex ’Base Address for NE2000 ethernet’ ARM_NE2000_BASE youraddr
hex ’IRQ for NE2000 ethernet’ ARM_NE2000_IRQ yourinterrupt
好了,编译通过。运行,果然没有问题了。Ping可以,telnet可以,在内核中把NFS打开,mount -t nfs ……也好用。哈哈。太好了。至此,801Array在S344B0组成的uclinux平台上的驱动,移植成功。相信其他的网卡芯片移植驱动程序应该也基本是这个思路。现在写出来与大家共享。希望对新手入门有所帮助,同时文章中有我理解错误的地方,也希望高手指教。
最后,再总结一下,移植过程中需要注意的几个问题:
1、确定网卡的基地址、中断无误
2、注意网卡的数据总线宽度,地址是否连续,如果不连续,如何映射
3、注意网卡的中断的模式和处理对应的外部中断是不是一致
4、对于IO和RAM统一编址的处理器,注意缓冲区范围的设置
5、注意ARMv3和ARMv4等一些和处理器结构相关的底层函数库带来的问题
6、用抓包软件可以帮助分析定位问题所在
Btw,我的PC平台是在WindowsXP+Virtual PC下安装的Red Hat linux 8.0,我觉得这样调试起来比较方便,可以用SoureInsigh来阅读,编写代码,可以在Linux编译。充分发挥两个操作系统的优势。很适合于像我这样的,不熟悉Linux人开发。