怎么读出网卡的mac地址(在30天自制操作系统上编写网卡驱动)
怎么读出网卡的mac地址(在30天自制操作系统上编写网卡驱动)BAR各bits的意思我们查询到,网卡设备对应的bar为0x0000 c101.那么这个数值代表着什么呢?是怎样对应于网卡内部的“控制面板”呢?既然在配置信息内部了,仍然使用我们昨天的方法去读取配置信息即可使用代码: int offset=4; // 读取第offset行 unsigned int addr3 = addr | (offset<<2); io_out32(0xCF8 addr3); unsigned int abr = io_in32(0xCFC);通过以上代码,我们读取到了配置信息中的BA0 它的值显示出来为:class_code=02时为网卡,其BAR为0xc101
昨天把网卡的配置信息读进来了,利用配置信息中的class_code 我们找到了pc上的网卡设备。
昨天的教程:在自制操作系统上写网卡驱动(2): 网卡的I/O配置
PCI配置信息
第4--9行分别是BA0 BA1 BA2 BA3 BA4 BA5 一个6个BAR.
既然在配置信息内部了,仍然使用我们昨天的方法去读取配置信息即可使用代码:
int offset=4; // 读取第offset行
unsigned int addr3 = addr | (offset<<2);
io_out32(0xCF8 addr3);
unsigned int abr = io_in32(0xCFC);
通过以上代码,我们读取到了配置信息中的BA0 它的值显示出来为:
class_code=02时为网卡,其BAR为0xc101
我们查询到,网卡设备对应的bar为0x0000 c101.那么这个数值代表着什么呢?是怎样对应于网卡内部的“控制面板”呢?
BAR各bits的意思
先说结论:如上图bar=0x0000c101时,我们访问I/O端口 0xc100,就是访问网卡内部的寄存器。这个结论是如何得到的?
查看IO BAR Written With Base Address字样下面的说明:
bit0为1,表示访问网卡内部的寄存器需要用CPU的I/O口;如果bit0为0,表示访问网卡内部的寄存器可以像访问内存那样,不用通过I/O口。
当bits0为1时,网卡内部寄存器对应于CPU的I/O的地址net_card_base_addr为:bar & 0xffff ff00
取bar的第8到31位作为net_card_base_addr的第8到31位,然后把net_card_base_addr的第0到7位设置为零即可。
所以,我们这里得到结论,只要访问I/O端口0xc100 就是访问网卡内部的寄存器。
具体的访问代码为:
//读取网卡内部寄存器的值
unsigned int CR = io_in8(0xc100);
//往网卡内部寄存器里写值
unsigned int CR = io_out8(0xc100);
这里为什么要用io_in8和io_out8 为什么要用8位的端口读写函数?不用16位的?不用32位的?
因为这款网卡的内部寄存器是8位的。
这是一款什么样的网卡呢?它的内部寄存器到底是怎样的呢?
网卡内部的寄存器我们要查询网卡的内部寄存器,首先要得到网卡的型号,怎么看呢?
通过PCI配置信息的第一行vonder_id和device_id。
比如我们这款网卡的vonder_id和device_id分别为:0x10ec和0x8029
经过搜索,在这里找到了具体解释:
10EC是Realtek公司
那么8029是什么?8029是芯片的型号,所以,连起来,就是Realtek 8029。
把"Realtek 8029"作为关键字搜索后,发现这款网卡的核心芯片就是 RTL8029芯片,
那么就可以去搜索RTL8029芯片的datasheet了。这个还是比较容易搜索到。
最终我找到了这个:
RTL8029AS的芯片手册。
虽然是8029AS,不是8029,但是估计也是8029芯片的升级版,应该跟8029操作起来差不多。这是总体介绍:
在这个芯片手册中,关于寄存器是这样说的:
8029AS的内部寄存器总览
表5.1.1. Register Table就是8029内部所有的寄存器了。我们要控制网卡收发信息,触发中断信号,就得给这些寄存器设置合适的值。
表中,每个寄存器的意义在datasheet中都有详细说明。
我们先来看一下如何访问表中的寄存器,比如访问某个单独的寄存器:CR寄存器。
先看表中的第1列No(Hex),它表示序号,CR寄存器的No为00 这说明通过0xc100 0x00就可以访问到CR寄存器了。
比如表中有个寄存器FIFO 它的No为06 这意味着通过0xc100 0x06就可以访问CR寄存器了。
但是注意到,这个表每一行的几个寄存器都同时对应着一个No 那么问题就来了,当我们in_io8(0xc100 0x06)时,具体读的是哪一个寄存器呢?
这个还要看CR寄存器内的值。如果CR寄存器的第8 7bits为00 我们此时执行的是读操作,也就是in_io8(0xc100 0x06) 那么此时操作的就是FIFO
如果此时执行的是写操作,那么此时操作寄存器就是TBCR1.
如果CR寄存器的第8 7bits为01 我们此时执行的是读操作,也就是in_io8(0xc100 0x06) 那么此时操作的就是PAR5 如果此时执行的是写操作,那么此时操作寄存器就还是PAR5.
如果CR寄存器的第8 7bits为10 我们此时执行的是读操作,也就是in_io8(0xc100 0x06) 那么此时操作的就是PAR5 如果此时执行的是写操作,那么此时操作寄存器就还是PAR5.
所以说:某个寄存器的方位方式,由这个寄存器所在的行头和列头的值共同决定。
比如PSTART,我们可以看其所在行01 其所在的列为:Page2-[R] 那么要操作这个寄存器,要把CR寄存器的第8 7bit设置为2,即0x10 然后用io_in8(0xc100 0x01)来操作这个寄存器。
所以,我们在访问寄存器前,总是需要先设置一下CR寄存器,那么如何设置这个寄存器呢?
CR的行为00,CR存在于所以列,这意味着访问CR就只用io_in8(0xc100)和io_out8(0xc100 0x01)就可以了。
所以,想把CR寄存器的bit8 bit7的值设置为10时,可以这样操作:
unsigned int temp = in_in8(0xc100); // 取CR原来的值
temp=temp&0x3F;
temp=temp|0x80; // 把bit8 bit7设置为10
in_out8(0xc100 0x00 temp) //temp的值给到CR
由于CR寄存器的bit8 bit7对应着Page0 Page1 Page2 Page3,为了方便的操作各寄存器,我们写了一个改变page的函数page_select:
//输入参数pagenumber 取之可以是0 1 2 3
void page_select(unsigned char pagenumber )
{
unsigned int temp;
temp=io_in8(0xc100);
temp=temp&0x3B;
pagenumber=pagenumber<<6;// 将pagenumber向左移动6为,从bit2 bit1 移动到bit8 bit7
temp=temp|pagenumber;
io_out8(0xc100 temp);
return;
}
这样,当我们需要操作寄存器PSTART的时候,就可以
page_select(2);
io_in8(0xc100 0x01);
这就大大简化了代码的编写,当然还可以继续简化,比如
void operation(unsigned char pagenumber unsigned char No)
{
page_select(pagenumber);
io_in8(0xc100 No);
}
operation(2 0x01);//只用一句就可以完成对PSTART的操作了
不过我们这里似乎没有必要做太复杂。太复杂了程序的可读性就变差了。如果以后有必要就再说吧。
到这里,我不仅感概,拿到这个datasheet真是太好了,终于可以实现对网卡的自由操作了。
先试试page_select韩式是否能够工作:先调用page_select(2) 然后再查看0xc100处的值,看其bit8 bit7是否被设置为10.
设置CR
我们先调用page_select(2) 然后再io_in8(base_addr)得到CR 然后把CR的值放到显示变量bar里进行显示。结果如下:
可以看到在表格的BAR列,最后一行的值为0x00000080 就是说CR的值为0x80 也就意味着CR的bit8 bit7位10.
那么page_select(3)之后是怎样的呢?
代码改为:
对应的结果为:0x000000c0.
这个结果说明我们对RTL8029网卡芯片内的寄存器CR设置成功了。
这就意味着,我们可以对RTL8029网卡芯片内的任何寄存器进行操作了。
这还意味着我们可以对不限于网卡,可以是显卡,也可以是其他卡比如显卡,比如声卡进行芯片内的寄存器设置了。
回到我们的RTL8029芯片。
是时候按照datasheet上的说明,来初始化网卡,获取网卡的mac地址,等操作了。
这些操作是数据接收和发送的基础步骤。
发送数据前的准备工作:读取网卡的MAC地址网卡的初始化init比较繁琐,需要设置一大堆的寄存器。
我先把代码放出来,然后再解释这些代码:
// 先关闭网卡,然后设置接收数据的内存区域信息,中断信息,以及如果对接收的数据过滤
void netcard_init()
{
unsigned int base_addr=0xC100;// 访问网卡的内部寄存器,只用访问的I/O地址0xC100即可
io_out8(base_addr 0x21);// 设置page0 并且停止显卡的一切操作。这里涉及到CR寄存器的所有8位的意义,在datasheet中有详细解释;
io_out8(base_addr 1 0x4c);// 写PSTART寄存器,即接收数据的内存的开始地址为0x4c页
io_out8(base_addr 2 0x80);// 写PSTOP寄存器,即接收数据的内存的结束地址为0x80页
io_out8(base_addr 3 0x4c);// 当前接收到的最后一个数据所在的页
io_out8(base_addr 4 0x45);// 当前发送的第一数据所在的页
io_out8(base_addr 0xc 0xcc);//RCR:决定哪些数据接收,哪些数据不接收
io_out8(base_addr 0xd 0xe0);//TCR:决定哪些数据发送,哪些数据不发送
io_out8(base_addr 0xe 0xc8);//DCR:决定处理数据时按照怎样的顺序,怎样的长度,是否loopback
io_out8(base_addr 0xf 0x00);//IMR:决定开启哪些中断,这些设置为00,关闭所有中断
// 开始去设置page1的寄存器
page_select(1);
io_out8(base_addr 7 0x4c 1); // CURR FIFO中,要存储的下一页数据的地址
io_out8(base_addr 8 0x00); // MAR0 // 多播地址的过滤
io_out8(base_addr 9 0x41); // MAR1
io_out8(base_addr 10 0x00);// MAR2
io_out8(base_addr 11 0x80);// MAR3
io_out8(base_addr 12 0x00);// MAR4
io_out8(base_addr 13 0x00);// MAR5
io_out8(base_addr 14 0x00);// MAR6
io_out8(base_addr 15 0x00);// MAR7
// 初始化完成,开启网卡
io_out8(base_addr 0x22); //与开始的0x21对应,0x21是关闭网卡,0x22是开启网卡
return;
}
CR寄存器每一bit的作用
可以对照CR寄存器的详细解释,看0x21。 其中PS1 PS0=00 即page0.
RD2,RD1,RD0位100,即Abort/Complete remote dma
然后TXP=0 即has no effect.
STA=0 这一位不控制任何事情,controls nothing.
STP=1 关闭网卡。即不再接收和发送任何的数据包。
PSTART设置了网卡内部用于接收数据的内存区域的开始地址。
PSTOP设置了网卡内部用于存储数据的内存区域的结束地址。
BNRY,接收的最后一个数据的地址。BNRY是boundary的缩写,表示接收数据的边界。
TPSR,发送数据的地址,它是Transmet Page Start Register的缩写。
这4个寄存器设置了收到数据的存放位置。
需要注意的是,这里的数据都是按page页为单位处理的。
每256个字节bytes为一页page.
所以PSTART=0x4C页 PSTOP=0x80页 那么这里一共包含0x80-0x4C=52页。
代码中设置了RCR=0xcc
这意味着MON=0 PRO=0 AM=1 AB=1 AR=0 SEP=0
MON=0 数据校验关
PRO=0 MAC地址匹配的数据才接收
AM=1 接受有多个目的地的数据
AB=1,接受广播的数据
AR=0 不接受少于64 bytes的数据
SEP=0,不接受有错误的数据。
这个寄存器果然如其名RCR,Receive Configuration Register 接收配置寄存器。用来配置哪些数据不接收,哪些数据接收。
那么再看一个寄存器TCR 发送数据的配置寄存器
io_out8(base_addr 0xd 0xe0);//TCR:决定哪些数据发送,哪些数据不发送
0xe0意味着OFST=0 ATD=0 LB1=0 LB0=0 CRC=0
OFST=0 冲突偏移功能关闭
ATD=0 关闭自动transmitter
LB1=0 LB0=0 正常操作,没有Loopback
CRC=0 开启CRC校验
下一行代码对应的是DCR:
io_out8(base_addr 0xe 0xc8);//DCR
0xc8意味着:FT1=1 FT0=0 ARM=0 LS=1 LAS=0 BOS=0 WTS=0
FT1=1 FT0=0 FIFO的阈值设置寄存器的bit1和 bit0位
ARM=0 发送没有被执行的包命令
LS=1 正常操作,不进行Loopback
LAS=0 16-位的DMA
BOS=0 MS byte placed on MD15-8 LS byte on MD7-0 高位和低位的顺序
WTS=0: byte-wide DMA transfer ,DMA传输时的单位是byte还是word
DCR中的位,都是选择位。
下一行代码是是关于中断的寄存器:
IMR,与 ISR连用。每个bits对应一个中断interrupt.
既然与ISR有关了,我们就把ISR看了:
ISR配置在什么情况下触发中断的。
RST:reset或者接收数据满时触发中断
RDC: 远程DMA操作完成时,触发中断。
CNT:技术吻合时触发中断。
OVW: 接收数据太多,太快,数据缓冲区满时,触发中断
TXE:因为冲突造成发送数据取消,触发中断。
RXE:接收数据时,CRC错误,帧对齐错误,包丢失时触发中断
PTX:发送成功,产生中断
PRX:接收成功,产生中断
再往后的代码:
page_select(1);
io_out8(base_addr 7 0x4d); // CURR 当前正在写的页的下一页
io_out8(base_addr 8 0x00); // MAR0
io_out8(base_addr 9 0x41); // MAR1
io_out8(base_addr 10 0x00);// MAR2
io_out8(base_addr 11 0x80);// MAR3
io_out8(base_addr 12 0x00);// MAR4
io_out8(base_addr 13 0x00);// MAR5
io_out8(base_addr 14 0x00);// MAR6
io_out8(base_addr 15 0x00);// MAR7
CUPR就相当于FIFO中P 接收到下一个数据的时候,所存放的页地址。
关于FIFO可以看这里:30天自制操作系统day07:使鼠标指针可移动
RSAR0 1,这两位设置了读取remote DMA的开始地址.
RBCR0 1 这两位设置了从remote DMA读取多少个byte.
注意到,这个代码中,使用一个联合体数据结构:
//存储mac地址中的一个字
// 利用这样的一个联合体,方便的把字的高8位,低8位拆出来。
union u {
unsigned int word;
struct{
unsigned char high;
unsigned char low;
}bytes;
char addr[2];
};
还有,为什么丢掉一个byte,因为:
remote read时,存储在0x0000-0x000b里的网卡物理地址0x52544CC118CF是这样的:
525254544C4CC1C11818CFCF
这12字节把网卡地址重复存储了一次。
不过这样存储,单和双的地址存储的是一样的。
其实存储在0x000b后面的是生产厂商的代码和产品标识代码,也是单双地址重复存储。
我们就先不去读生成厂商的代码和产品表示了。
以上代码最后读取到的MAC地址结果为::
可以看到,读取到的Mac地址为54-52-12-00-56-34。
读取到了网卡的MAC地址,就可以在发送数据包的时候,加上这个地址了。
我们理使用网卡发送数据越来越近了。
总结今天利用配置信息中的Vendor_id=0x10ec和device_id=0x8029,我们对应了网卡的生产商为Realtek 网卡里芯片的具体型号为8029.
根据这写信息,我么找到了这款网卡芯片的操作手册datasheet.
芯片的操作手册就是芯片的使用说明了,里面详细地介绍了如何使用芯片里的寄存器来控制芯片接收数据,发送数据。一旦拿到芯片的操作手册,我们其实已经里操作网卡芯片发送数据很近了。
这里面涉及到两方面的知识:如果去操作网卡里的寄存器,以及网卡里都有哪些寄存器。
通过CPI配置信息中的BAR就可以获取到一个地址,通过这个地址,我们就可以用io_in8(地址)命令读取到网卡内的寄存器值。用io_out8(地址)往网卡的寄存器的值内写入内容,达到控制网卡的目的。
网卡的操作手册datasheet里有对网卡里的寄存器的详细说明。
我们参考着这份datasheet完成了对网卡打开,关闭,设置接收数据缓冲区,设置中断等初始化工作,并读取了属于这个网卡的mac地址。
今天的工作先到这里,后续就可以开始真正的使用网络传输协议来收发数据了。
附录记录一些写代码过程中的bug 以及debug的过程。
在读取网卡内部的寄存器时,一开始读取总是失败的,后来更改了地址变量的类型后
读取物理地址就成功了。
由unsigned char base_addr
unsigned int bar = io_in32(0xCFC);
unsigned char base_addr = bar&0x0000ff00;
if(class_code==2){
page_select(3);
unsigned int CR = io_in8(base_addr);
bar = CR;
}
改为了unsigned int base_addr
unsigned int bar = io_in32(0xCFC);
unsigned int base_addr = bar&0x0000ff00;
if(class_code==2){
page_select(3);
unsigned int CR = io_in8(base_addr);
bar = CR;
}
就成功了。
写代码时,主要参考了RTL8019AS的--以太网协议:http://www.doczj.com/doc/cb952888.html
这个代码让我把看到的一些介绍信息和具体的芯片联系起来,总算把datasheet看懂了个大概。
另外,对BAR的理解其实又用的知识挺多,但是我们这里只是用到了部分信息,我是通过一下地址以及图片学习了BAR的相关设置的:
一个介绍比较完整的资料是:https://www.pianshen.com/article/40881826037/