代码改变世界

Cosmos的里程碑2(Mile Stone 2)之RTL8139网卡芯片编程---- .net/C#开源操作系统学习系列八

2011-09-28 17:50  Hundre  阅读(3887)  评论(4编辑  收藏  举报

下载的源代码包为cosomos-16025.zip

从发布的时间上看,MS2和MS1的时间只差了2个月,比较明显的变化有在编译操作系统是出来了一个图形化的选择界面同时内核文件夹里面多了两个项目RL8139驱动和文件系统,其他的基本没啥大变化。

image     image

接下来我们启动MS2看看,把启动项目设为FrodeTest,只有这个项目里面使用到了RL8139驱动,其他的项目和MS1中的基本一样。

image

先看一下Prgrom.cs里面的源代码(去掉了原文中的一些无用的注释):

using System;
using Cosmos.Build.Windows;
 
namespace FrodeTest
{
    class Program
    {
        #region Cosmos Builder logic
        // Most users wont touch this. This will call the Cosmos Build tool
        [STAThread]
        static void Main(string[] args)
        {
            var xBuilder = new Builder();
            xBuilder.Build();
        }
        #endregion
 
        // Main entry point of the kernel
        
//This is the playground for Frode "Scalpel" Lillerud.
        public static void Init()
        {
            Cosmos.Kernel.Boot.Default();
 
            Cosmos.Kernel.Staging.DefaultStageQueue stages = new Cosmos.Kernel.Staging.DefaultStageQueue();
            stages.Enqueue(new Cosmos.Kernel.Staging.Stages.KernelStage());
            stages.Run();
 
            //以上代码和MS1中的基本一样,就不说了 
            
//下面这三行应该是为了给多用户登陆做准备用的吧,可惜MS2目前还是个单进程单线程的程序,无法让多用户进行操作
            
//总之这里实例化了一个用户,然后在加载一次会话,最后是run运行,跳到下一段代码块来分析run里面的代码
            Security.User currentUser = Security.User.Authenticate("frode""secret");
            Shell.Session currentSession =  Shell.Session.CreateSession(currentUser);
            currentSession.Run();

            Test.RTL8139Test.RunTest();

            Console.WriteLine("Shutting down computer");
            while (true)
                ;
        }
    }
}
 

以下是run部分的代码:

这段代码用来响应用户输入的命令,可以看到这里里面包括了使用RTL8139网卡芯片来发送数据包的代码,其实就是调用RTL8139驱动程序提供的类和方法。其实MS2个人感觉主要是实现了对PCI设备的控制

internal void Run()
        {
            Console.Write(Prompt.LoadPrompt(xUser).PromptText());
            string command = Console.ReadLine();
            Cosmos.Driver.RTL8139.RTL8139 nic = null;
 
            if (command.Equals("exit"))
                return;
            else if (command.Equals("nic"))
            {
                //获取PCI设备,下一篇文章再共同学习一下对PCI的编程控制
                Cosmos.Hardware.PC.Bus.PCIDevice pciNic = Cosmos.Hardware.PC.Bus.PCIBus.GetPCIDevice(030);
                nic = new Cosmos.Driver.RTL8139.RTL8139(pciNic);
                nic.Enable();
                Console.WriteLine("Enabled Network card");
            }
            else if (command.Equals("send"))
            {
 
                //这里调用网卡发送数据包其实也只是简单的使用了几个网卡驱动程序中提供出来的几个函数而已
                
//数据包的格式是自己构造的,并没有遵循任何协议(比如TCP/IP)的标准
                Cosmos.Driver.RTL8139.PacketHeader head = new Cosmos.Driver.RTL8139.PacketHeader(0xFF);
                byte[] data = FrodeTest.Test.Mock.FakeBroadcastPacket.GetFakePacketAllHigh();
                Cosmos.Driver.RTL8139.Packet packet = new Cosmos.Driver.RTL8139.Packet(head, data);
                if (nic == null)
                {
                    Console.WriteLine("Enable NIC with command nic first");
                    return;
                }
                nic.Enable();
                nic.EnableRecieve();
                nic.EnableTransmit();
                nic.Transmit(packet);
            }
            else if (command.Equals("reset"))
            {
                nic.SoftReset();
                nic.EnableTransmit();
                nic.EnableRecieve();
                Console.WriteLine("NIC reset");
            }
            else
                Console.WriteLine("No such systemcommand or application: " + command);
 
            Run(); 
        }

好接下来就是分析RTL8139这个类了

image

可以看到,这个项目里面除了RTL8139这个类外还有其他一些文件,这些合起来构成了一个cosmos中RTL8139网卡的驱动程序。从代码中可以看到RTL8139继承自NetwordDevice,NetworkDevice继承自Device,Device继承自Hardware,虽然继承层次比较多,但并不复杂,各基类只是定义了一些接口用来规范和统一操作,其他的代码也并不多,基本一看就能明白。

在接下来继续讲解RTL8139这个驱动之前,得对RTL8139这个网卡有点了解(小弟百度了N也没找到多少关于RTL8139的编程资料,可能是百度技术还不够高,泪奔。最后找到了一篇英文的,这里自己翻译一下,希望大家能看懂)

****************************************************

原文地址(需要 fan qiang 访问):http://wiki.osdev.org/RTL8139

前言

RTL8139通过寄存器(这里的寄存器应该指的是网卡上的寄存器)来进行配置。它的IO基址和IRQ号可以通过PCI配置信息获取。

寄存器

以下是RTL8139使用的寄存器

Offset (from IO base)

相对于IO基址的偏移值

Size

大小

Name

名称

0x00

6

MAC0-5

0x08

8

MAR0-7

0x30

4

RBSTART

0x37

1

CMD

0x3C

2

IMR

0x3E

2

ISR

(原文的表格不太简单了,更多寄存器信息请参考:http://www.soiseek.cn/ETC/RTL8139/10.htm

再附上RTL8139的datasheet(做硬件的应该很熟悉了,在此多谢下小弟做硬件的同事,不然还真不知道从datasheet中找资料)/Files/li0803/RTL8139-datasheet.pdf

编程手册

启动RTL8139

发送0x00到config1(0x52)寄存器来设置LWAKE+LWPTN(这两个应该是针脚的名字)为高电平(to active high.)这一步是启动该设备的关键操作。

软复位

接下来我们应该做一次软复位来清理RX和TX缓冲区并设置所有东西回默认值。这一操作用来清除在通电启动时候仍然留在缓冲区或者寄存器中的垃圾。

发送0x01到Command寄存器(0x37)将会使RTL8139进入软复位。只要这个字节被发送过去,可以检查RST位来确认芯片是否完成复位。如果RST位为high(1),那么复位仍在进行中。

另:在Qemu模拟器中有个小小的bug,如果在执行软复位前检查command寄存器,你可能会发现RST是high(1)。这时,尽管忽略它继续进行初始化操作。

初始化接收缓冲区

这一部分,我们将设置一段芯片地址位置为接收缓存的开始地址。一种方法是,定义个缓存变量并把这个变量地址发送到PBSTART寄存器(0x30),如:

char rx_buffer[8192+16]; // declare the local buffer space (8k + header)

outportd(0x30, (unsigned long)rx_buffer); // send dword memory location to RBSTART (0x30)

设置IMR+ISR

Interrupt Mask 寄存器(IMR)和Interrupt Service寄存器(ISR)用来负责激活不同的IRQ。IMR的各个位和ISR的各个位排成一排来同步工作(The IMR bits line up with ISR bits to work in sync.)如果IMR的一个位为低电平 (low) ,则对应的ISR位将不会触发一个IRQ当这个IRQ的发生周期来到的时候(翻译得真拗口,自己都受不了。上原文吧:. If an IMR bit is low, then the corresponding ISR bit with never fire an IRQ when the time comes for it to happen)。IMR的位置为0x3C,ISR的位置为 0x3E。

要设置RTL8139为只接收Transmit OK(TOK) 和 Receive OK (ROK)中断的话,我们需要让IMR中的TOK位和ROK位为高电平而其他位为低电平。这样设置后,当TOK或者ROK中断请求发生时,就会触发一个IRQ(中断请求)。如:

outportw(0x3C, 0x0005); // Sets the TOK and ROK bits high

另:当你处理一个中断时,你必须写入这个中断对应的位来重置它。文档(不知道什么文档,RTL8139的datasheet?)上说重置缓冲区为0的话读这个寄存器就够了而写入是没有任何效果的。这在QEMU中无效,估计在一些或者大部分硬件中也是如此。而且如果无效的话写入一个位也无妨。

01000101:确认。实际上,清理一个中断的唯一办法是写入这个中断。Datasheet上说读(reading)是你必须做的,但是这完全是错的。

配置接收缓冲区

在期望见到一个数据包到来之前,你应该告诉RTL8139根据各种规则来接收数据包。这里的配置寄存器为RCR。

你可以开启不同的匹配规则:

AB – Accept Broadcast: 允许广播数据包发送到MAC地址 FF:FF:FF:FF:FF:FF

AM – Accept Multicast: 允许多播数据包

APM – Accept Physical Match: 允许数据包被发送到NIC(网络接口卡?Network Interface Card)的MAC地址

AAP – Accept All Packets: 允许所有数据包 (运行在 promiscuous模式下)

另外一个位----WRAP位,控制着接收缓冲区wrap around(环绕?不知如何翻译)的处理。

如果WRAP是0,RX缓冲区则被当作传统的环缓冲(ring buffer)处理;如果一个数据包被写入缓冲区结尾附近并且RTL8139知道你已经在它之前处理了(根据CAPR来判断是否被处理)这个被写入的数据,这个数据包将会在缓冲的开始继续(怎么觉得这句话有问题呢,求高手:if a packet is being written near the end of the buffer and the RTL8139 knows you've already handled data before this (thanks to CAPR), the packet will continue at the beginning of the buffer.)

如果WRAP是1,数据包的remainder将会被连续地写入(使实际的接收缓冲区溢出)这样它能被更有效地处理。这将意味着为了接收可能使缓冲区溢出的最大的数据包,接收缓冲需要额外的1500个字节。

你也可以在这里设置你的RX缓冲区的大小,尽管如此,如果你使用一个像之前描述的8K+16的缓冲,填0已经足够了。要是用WRAP=1的位的话,一个8K的缓冲实际上必须是8K+16+1500字节。例如:

outportl(0x44, 0xf | (1 << 7)); // (1 << 7) is the WRAP bit, 0xf is AB+AM+APM+AAP

开启接收和传送功能

现在是时候设置RX和TX的功能了。这真的是小意思,在我看来,这个只应该在RTL8139的寄存器按照我们想要的配置完成后才来完成这部分内容。RE(Receiver Enabled)和TE(Transmitter Enabled)位都在Command寄存器(0x37)中。设置RE和TE真的很简单,但还是让我们来看一下。

为了允许RTL8139接收和传送数据包,RE和TE位必须设置成高电平。一旦这个设置完成,网卡就将同意数据包进出。如

outportb(0x37, 0x0C); // Sets the RE and TE bits high

****************************************************

好了,有了上面RTL8139的编程知识后,我们在来理解Cosmos中RTL8139的驱动代码就容易很多了。来一起学习下几个比较重要的public函数。

 

//构造函数,由于RTL8139通过PCI总线进行通信,所以这里传入一个PCI设备类,然后是获取设备能进行读写的内存范围,最后用这段内存来实例化网卡的寄存器。
//这里的寄存器应该和我们CPU汇编中使用的寄存器有点不太一样,这里虽然也是使用寄存器名字,但是读写寄存器还是读写内存,不同的寄存器对应的是不同的内存范围。
public RTL8139(PCIDevice device)
        {
            pciCard = device;
            mem = device.GetAddressSpace(1as MemoryAddressSpace;
            regs = new MainRegister(mem);
        }

//启动网卡,有前面翻译的文章中知道,启动网卡就是往config1寄存器中写入0x00
public override bool Enable()
        {
            regs.Config1 = 0x00;            
            return true;
        }

//软复位,根据翻译的文章基本可以了解代码的内容了,不多解释
public void SoftReset()
        {
            Console.WriteLine("Performing software reset of RTL8139");
           
            regs.ChipCmd = MainRegister.ChipCommandFlags.RST;
            while (regs.ChipCmdTest(MainRegister.ChipCommandFlags.RST))
            {
                Console.WriteLine("Reset in progress...");
            }
 
            Console.WriteLine("Reset Complete!");
        }

//开启接收功能
public void EnableRecieve()
        {
            regs.ChipCmd = MainRegister.ChipCommandFlags.TE;
        }

//开启传送功能
public void EnableTransmit()
        {
            regs.ChipCmd = MainRegister.ChipCommandFlags.TE;
        }
 
//Early TX Threshold特指在传送开始前TX先进先出寄存器中的的临界值
//bytecount不能超过2048(2K 字节)
//bytecount是一个能被32整除的数
//bytecount是0则NIC(网卡适配器,其实就是网卡)将使用8字节作为临界值
public void SetEarlyTxThreshold(uint bytecount)
        {
            if (bytecount != 0 & (bytecount % 32 > 0))
                throw new ArgumentException("Early TX Threshold must be 0 or dividable by 32");
 
            //Each of the four Transmit Status Descriptors (TSD) has its own EarlyTxThreshold.
            UInt32 address = pciCard.BaseAddress1 + (byte)MainRegister.Bit.RxEarlyCnt;
            IOSpace.Write8(address, (byte)bytecount);
        }

//初始化接收缓冲区。RBSTART寄存器由4字节组成(0x30h 到 0x33h),这4个字节应该包括将要保存数据的缓冲区地址 
public void InitReceiveBuffer()
        {
            byte[] rxbuffer = new byte[2048];
            UInt32 address = pciCard.BaseAddress1 + (byte)MainRegister.Bit.RxBuf;

            //把缓冲区的地址空间(就是一段地址)写入RBSTART 
            WriteAddressToPCI(ref rxbuffer, address);
 
            Console.WriteLine("RxBuffer contains address: " + IOSpace.Read32(address));
        }
 
//传送指定的数据包
public unsafe void Transmit(Packet packet)
        {
            //告诉PCI卡数据包中内容部分的地址.
            UInt32 address = 
                pciCard.BaseAddress1 + 
                (byte)MainRegister.Bit.TSD0 + //I think this should be TSAD0, but then no packet is sent...
                TransmitStatusDescriptor.GetCurrentTSDescriptor();
            Console.WriteLine("Address of TSAD0: " + address);
 
            byte[] body = packet.PacketBody;
 
            WriteAddressToPCI(ref body, address);
 
            Console.Write("Data in Transmit Status Descriptor " + TransmitStatusDescriptor.GetCurrentTSDescriptor() + ":");
            Console.WriteLine(IOSpace.Read32(address));
            //就目前来看,TSDA0应该包含有数据的地址.
            Console.WriteLine("The Data pointed to: " + IOSpace.Read32(IOSpace.Read32(address)));
 
            //把传送状态设置为允许传送.
            TransmitStatusDescriptor tsd = TransmitStatusDescriptor.Load(pciCard);
            tsd.Size = body.Length;
            Console.WriteLine("Told NIC to send " + tsd.Size + " bytes.");
            SetEarlyTxThreshold(1024);
            Console.WriteLine("Sending...");
            tsd.ClearOWNBit();
            TransmitStatusDescriptor.IncrementTSDescriptor();
        }

这里解释一下,RTL8139中所谓的寄存器其实就是其自身对其自己已划分好的各个内存块的名称,操作不同的寄存器,其实就是操作不同的内存块。Cosmos中RTL8139的驱动还另外写了一些类来专门负责读写对应的寄存器,其实就是专门修改对应的内存地址段。里面的代码都比较简单,基本上都是对内存段的读写,就不说了

image

 

最后,我们来运行这个项目看看

image

运行了两个命令:nic和send,可以看到输出了一些和网卡相关的信息。不过在此寻求高人帮助:怎么检测本机的网卡是否是真的有数据发送出去。还有这里显示出来的MAC地址虽然是从硬件的配置信息用读取出来的,但和用ipconfig列出的本机的网卡的MAC不太一样,不确定这里的代码是否真的运行成功了。

image

 

好了,此次就写到这里吧,以上列出的代码中还有些PCIdevice,IOSpace之类的类没有涉及到,这些将留在下一篇对PCI总线的编程学习中和大家共享学习心得。

这次的好像有点长,希望写的东西大家能看得懂,也非常欢迎大家点评。

 

接下来是广告时间,大家懂得……

欢迎大家经常光顾小弟的淘宝充值店:http://hundre.taobao.com/

最后是多谢大家捧场 微笑