REALTEK 8139 NETWORK CARD DRIVER

2013-12-03

While building my homebrew OS, I go to the point where I needed a netcard driver.
I run my os in QEMU, which provides a RealTek 8139 netcard. The specs for that card are very
easy to find.

Before I continue, you should know that when the datasheet specifies a register that is
2 bytes long (like ISR), it is important to read it as a 16bit word even if all you
need is the first 8bit. I was reading ISR with "inb" and couldn't make my software
work event if all I needed was the first byte. Changing "inb" for "inw" worked. The datasheet
indicates that some registers need to be read or written as words or dwords even if it
looks like they could be accessed as bytes.

Initializing

Enable the card: OUTPORTB(0,iobase+0x52);

Reset the card:
You need to write the "reset" bit in register 0x37, and then wait until that bit gets cleared
unsigned char v=0x10;
OUTPORTB(v,iobase+0x37);
while ((v&0x10)!=0) INPORTB(v,iobase+0x37);

enable TX and RX interrupts: OUTPORTB(0b101, iobase+0x3C); There are other interrupts in
register 0x3C that can be interesting but I just need TOK and ROK for now.

enable 100mbps full duplex: OUTPORTB(0b00100001, iobase+0x63)

Set the Receive Configuration Register (RCR):
OUTPORTL(0x8F, iobase+0x44);
Looking at the datasheet, you can see what those bits mean. Bascically what we did is:

set promiscuous mode

accept frames for our MAC address

accept frames for out multicast address

accept broadcasted frames

Do not accept runts and erroneous frames

set the RX buffer size to 8k

disable WRAP. This means that is a frame is received and we are near the end of the RX buffer,
the card will continue copying data after the buffer. We are basically allowing buffer overflow here.
so for this reason, we need to give extra space to our buffer. I chose to use a 10k buffer just to be sure

Set the RX buffer address. The details of this buffer will be explained in the next section.
For now, let's just reserve a buffer of 34k and tell the card about it: OUTPORTL(buf_addr, iobase+0x30)

Warning: The addresses for TX and RX buffers must be physical addresses. Not virtual addresses

Set the Transmit Configuration Register (TCR): The default values after reset are fine. So I'm not
touching that register.

Set the tx descriptors
for now, I won't go in the details of those buffers, this will be explained in the next section
all you need to know right now is that you need 4 2k buffers and tell the card about them
OUTPORTL(buf_addr_desc0, iobase+0x20);
OUTPORTL(buf_addr_desc1, iobase+0x24);
OUTPORTL(buf_addr_desc2, iobase+0x28);
OUTPORTL(buf_addr_desc3, iobase+0x2C);

enable TX and RX: OUTPORTB(0b00001100,iobase+0x37);

This is my init code. Note that there is some PCI stuff in there that I don't describe. I am assuming that you
have a PCI driver written at this point

Receiving

Since we have enabled the ROK and TOK interrupts, we will receive and interrupt when a new frame
arrives. So from my interrupt handler I check the ISR register to know if I got a TOK
or ROK. if ROK, then proceed with getting the frame. First, some definitions:

CAPR: This register holds the address within the RX buffer where the driver should read
the next frame. This register must be incremented by the driver when a frame is read.
The netcard will check that register to determine if a buffer overrun is occuring.

packet header: This is a 4bytes field that is found at the begining of the frame. The first word is a bitfield
indicating if the frame is OK, if it was received as part of multicast ect. More information can
be found in section 5.1 of the datasheet. The following 2 bytes indicate the size of the frame

This is what I do:

1) Trigger on interrupt: Since interrupts have been enabled, IRQ will have been raised.
So this will be done from the handler. We need to check TOK in the ISR register

2) Get position of frame within the RX buffer by reading CAPR

3) Get size of data: 2nd 16bit word from begining of buffer (CAPR+2)

4) copy the frame: address starts at rx_buffer_base+CAPR

5) Update CAPR: CAPR=((rxBufIndex+size+4+3)&0xFFFC)-0x10
We are adding 4 to take into account the header size and the +3&0xFFFC is to align on a 4bytes boundary. I have no idea
why we need to substract 0x10 from there. Note that you should keep track of rxBufIndex separately. I.e: do not update it with CAPR everytime.

Sending

I found that Sending was easier than receiving. The first thing that needs to be done is to setup the buffer pointers in TSAD0-TSAD3.
I'm not sure if these buffers require any special alignment but I've aligned mine on 2k boundaries.

Sending a frame

There are 4 TX buffers available. You should keep track of which one is free by incrementing an index everytime you send a frame.
This way, you will know what buffer to use next time. You will need to copy your frame into the buffer pointed to by TSAD[CurrentSendIndex].
You will then need to write the size of the frame into TSD[CurrentSendIndex] and clear bit 13. Bit 13 is the OWN bit. It indicates to the card that
this buffer is ready to be transmitted. Then you increment CurrentSendIndex to be ready for next time. At the next send, if TSD[CurrentSendIndex].bit13
is cleared, it means that the frame still belongs to the card and it wasn't transmitted. This would indicate a buffer overrun, your software
is sending faster than what the card can handle.

Handling TX interrupt

Handling the interrupt is mostly done to detect send errors. I don't use it much. I won't go into details here, as the code
explains pretty much everything.

unsigned short isr;
INPORTW(isr,iobase+0x3E);
OUTPORTW(0xFFFF,iobase + 0x3E);
if (isr&0b100) //TOK
{
unsigned long tsdCount = 0;
unsigned int tsdValue;
while (tsdCount <4)
{
unsigned short tsd = 0x10 + (transmittedDescriptor*4);
transmittedDescriptor = (transmittedDescriptor+1)&0b11;
INPORTL(tsdValue,iobase+tsd);
if (tsd&0x2000) // OWN is set, so it means that the data was transmitted to FIFO
{
if ((tsd&0x8000)==0)
{
//TOK is false, so the packet transmission was bad. Ignore that for now. We will drop it.
}
}
else
{
// this frame is pending transmission, we will get another interrupt.
break;
}
OUTPORTL(0x2000,iobase+tsd); // set lenght to zero to clear the other flags but leave OWN to 1
tsdCount++;
}
}