我是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中做好了的,改一下,就好了。我把他改成了上升沿触发。
别人的批注:
最好改为高电平触发,我就吃过这样的苦头,当时我那个驱动不太稳定,一旦有错误,
开始的时候,我也是摸不着头脑,不知道该从什么地方入手。用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中做好了的,改一下,就好了。我把他改成了上升沿触发。
别人的批注:
最好改为高电平触发,我就吃过这样的苦头,当时我那个驱动不太稳定,一旦有错误,