天天看點

PMON學習筆記

pmon 學習筆記

如果生命有終點

我就陪你到終點

如果生命沒終點

我就陪你到永遠……

本筆記零散寫于2009 年2 月~5 月,最初基于2009 年1 月20 日

dev.lemote.com 上的pmon 版本。後期使用的是更新版本,部分代

碼和目前版本有誤差,不過應該影響不大。

2

目錄

扯3

start.S 4

配置空間的通路10

superio_init 11

start.S 之記憶體15

start.S 之cache 20

從彙編到c 25

initmips 27

dbginit 29

_pci_businit 37

cs5536_pci_fixup 45

Init_net 46

Init_net 之tgt_devconfig 53

tgt_devconfig 之顯示卡55

tgt_devconfig 之config_init 58

tgt_devconfig()之configure 61

USB 71

回到init_net 80

DevicesInit 82

open 函數88

load 核心98

Termio 105

printf 和write 108

鍵盤和鍵盤事件的響應114

Ioctl 122

環境變量和flash 125

GPIO 135

你怎麼出來了-- 圖檔顯示137

3

一開始大家都不認識,先胡亂抛出幾句話扯扯,熟悉熟悉。就是開場白,也有人

說叫序。

pmon 是cpu 上電後執行的代碼,相當于x86PC 機中的BIOS,兼有bootloader

的功能,代碼來源于早期BSD 的核心,到如今已舊貌換新顔,糟蹋得差不多了。

pmon 的二進制代碼存放于主機闆上的一塊512KB 的flash 晶片上,選擇這個容量

是因為夠用了,龍芯2F 允許的最大boot rom 容量是1MB。

這塊flash 晶片的位址是确定的,虛拟位址0xbfc00000,實體位址0x1fc00000。

cpu 上電後,會在第一時刻從虛拟位址為0xbfc00000 的讀取指令執行,這個和

x86 的cpu 是一樣的,差别的隻是位址。

位址差别的一方面是位址值,另一方面是位址類型。

x86 有實模式和保護模式的差別,上電初期為實模式,位址就是實體位址。MIPS

沒有這兩種模式的差別,而且一開始就是虛拟位址。虛拟位址是虛的,是以必定

需要映射到實體位址。

下面介紹一下pmon 檔案相關的位址問題。

cpu 眼中的位址是虛拟位址,cpu 取指和取資料的位址是實體位址,經過北橋解

釋後的位址是總線位址,編譯器産生的位址(包括解析了所有引用和重定位的符

号後)為程式位址,也就是程式它自己了解的位址。不同的位址概念間需要有映

射函數來關聯。

mips 的虛拟位址到實體位址的映射比x86 要複雜一些(也更有用些),既有可供

頁表之類動态建立映射函數的機制,也有unmapped cacheed/uncached 這種固

定映射關系的轉換方式。bios 代碼由于其執行時機的限制,開始階段隻能使用

unmapped uncached 這個段。位址範圍是0xa0000000~0xc0000000,也就是

2.5G 到3G 的512MB 空間。這個範圍的映射函數為實體位址=程式位址-2.5G。

所有的32 位mips 架構的cpu 都遵循這個約定。

pmon的代碼絕大部分是用C寫的,隻有個别的體系結構相關的代碼使用了彙編。

好,先到這兒吧。

如果你對pmon 感興趣,想了解它的架構,

如果你對pmon 感興趣,想了解各個裝置在bios 中是如何工作的,

如果你對pmon 感興趣,想找一個了解pmon 的N 天搞定法。

以及不滿足上述條件的其他人

就不要在這浪費時間了。本筆記隻講述流程。

好,想看流程或者願意浪費時間的,Let‘s go。

4

start.S

start.S 在pmon 中的作用?

核心是把pmon 的二進制檔案複制到記憶體。并初始化cache,記憶體控制器,内

存和南橋的部分信号。這個代碼執行之後會執行c 代碼,解壓在二進制中壓縮

的bin 檔案,跳到解壓後的代碼繼續執行。

由于一上電的時候,記憶體和記憶體控制器都處于不确定的狀态,是以cpu 一開始執

行的代碼不能在記憶體中存放,隻能把bios 的代碼放在非易失性的媒體中(實際

都是nor 型的flash)。由于在這類媒體中的執行速度比較慢,是以盡可能的,要

把其中的bios 代碼載入到記憶體執行,這需要先初始化記憶體控制器。要初始化内

存控制器必須獲得記憶體的spd 資訊,而記憶體的spd 資訊需要通過i2c 總線讀取,

而i2c 子產品在南橋上,通路要在初始化smbus 之後,要通路南橋又必須先初始

化北橋,要通路北橋又需要先初始化cpu本身。這是個一環扣一環的過程。start.S

基本就是圍繞這個流程展開的。當然在初始化南橋的SMB 總線以外,順便把南

橋相關的其他一些信号也初始了,這些具體的可能要看電路圖了。

start.S 是在cpu 上電之後立即執行的代碼。

為什麼?就因為它叫start.S?

MIPS cpu 約定cpu 執行的第一條指令位于虛拟位址0xbfc00000,而pmon 的二

進制代碼是以load -r -f bfc00000 gzrom.bin這個指令燒入bfc00000這個位址開

始的flash,硬體布線會使得虛拟位址0xbfc00000映射到這塊flash上。而start.S

就占據了gzrom.bin 的開頭部分。

為什麼gzrom.bin 的開頭就是start.S?

是./zloader/Makefine.inc 中的L29:

mips-elf-ld -T ld.script -e start -o gzrom ${START} zloader.o 決定的。

start.S 檔案是用彙編語言寫的,關于龍芯彙編語言的内容請參閱mips 指令手冊

(龍芯沒有這方面的公開手冊)。

cpu 上電後,内部的寄存器一些可寫的寄存器的值是不可預知的,是以代碼先設

定cpu 的部分内部寄存器。在第一條彙編指令前有一行注釋如下:

就是說這裡已經是16 條指令了,不能再多了雲雲。如果你實在想不明白這怎麼

有16 條指令,那麼反彙編一把,看到的确是16 條。

80010000 <_ftext>:

80010000: 40806000 mtc0 zero,c0_sr

80010004: 40806800 mtc0 zero,c0_cause

80010008: 3c080040 lui t0,0x40

8001000c: 40886000 mtc0 t0,c0_sr

80010010: 3c1d8001 lui sp,0x8001

80010014: 67bdc000 daddiu sp,sp,-16384

5

80010018: 3c1c800f lui gp,0x800f

8001001c: 679ce0e0 daddiu gp,gp,-7968

80010020: 04110003 bal 80010030 <uncached>

80010024: 00000000 nop

80010028: 04110183 bal 80010638 <locate>

8001002c: 00000000 nop

80010030 <uncached>:

80010030: 3c01a000 lui at,0xa000

80010034: 03e1f825 or ra,ra,at

80010038: 03e00008 jr ra

8001003c: 00000000 nop

如果你沒想明白,還要問那麼為什麼不能超過16 條呢?。。。。。。。。。。

我也不知道了。呵呵。

如果你非要铤而走險,在中間多加些指令非讓它超過16 條,那麼哥們你太聰明

了,你将發現這隻是個玩笑而已。是吧,這幫寫代碼的有時沒正經。

好,言歸正傳,這個start.S 共有2656 行,的确不算太短。

現在我們按照cpu 的執行順序對start.S 代碼進行逐一分析。

cpu 執行的第一條指令在L 213,

mtc0 zero, COP_0_STATUS_REG

mtc0 zero, COP_0_CAUSE_REG

zero 就是寄存器0($0),和下面的t0($8)之類相似,都由asm.h 中incldue 的

refdef.h 定義的,由預處理去解析。

一開始将狀态寄存器和原因寄存器清零主要是禁用所有的中斷和異常檢測,并使

目前處于核心模式。之後執行:

li t0, SR_BOOT_EXC_VEC

mtc0 t0, COP_0_STATUS_REG

以上兩句隻是設定目前的異常處理模式處于啟動模式,啟動模式(BEV=1)和正常

模式(BEV=0)的差別是異常處理的入口位址不同,具體如下:

此表來自龍芯2F 的使用者手冊,更詳細的status 位描述見手冊第五章。

6

la sp, stack

la gp, _gp

sp 的指派就是初始化棧,棧是函數(或過程)調用的基礎設施,也是使用c 語

言的前提。

stack= start - 0x4000,

可見棧的大小為16KB,不算小了。

_gp 在ld.script 中定義,gp 的作用主要是加快資料的通路,這裡不予關注。

bal uncached

nop

uncached:

or ra, UNCACHED_MEMORY_ADDR

j ra

nop

先說說bal 這條指令,這條指令是一個pc 相對跳轉指令(這一點很重要),跳轉的

目的是把ra 寄存器和0xa0000000 相或,将cpu 的執行位址映射到unmapped

uncached 段(對于一般的情形,這是多此一舉,想想目前pc 寄存器的值)。

由于位址問題是一個核心問題。下面解釋一下龍芯2F 位址轉化過程。

CPU 獲得的位址是虛拟位址,在獲得這個位址之後,CPU 的内部邏輯首先判斷

這個位址落在哪個段,有的段虛拟位址到實體位址的轉換公式是固定的,比如在

32 位模式下,kseg0,kseg1 就是直接映射的。無論如何,在獲得轉換後的位址

也就是實體位址後,cpu 并不知道這個位址是發往哪裡,是記憶體,pci,還是内

部的cpu 的io位址。這個發送方向也就是位址路由是由位址視窗寄存器決定的。

在2F 中,這些位址寄存器的位址是從0x3ff0_0000 開始的48 個寄存器。注意,

這個位址是實體位址。

以32 位的為例(這個比較簡單),我們要關心的就隻有3 個位址視窗。

從http://www.loongson.cn/company/下載下傳的龍芯2F 使用者手冊中介紹了這3個地

址視窗。3 個位址視窗分别是:

cpu 視窗0,負責實體位址到記憶體位址的轉換。

cpu 視窗1,負責實體位址到pci 或者cpu localio 的轉換

pcidma 視窗0,負責把收到的DMA 請求進行pci 總線位址到記憶體位址的轉換。

比如0xbfc00000 這個虛拟位址,cpu 首先判斷這個位址落在kseg1 段,直接轉

換且不經過cache,轉換成的實體位址0x1fc00000。接着位址視窗寄存器就派

上用場了。一個一個視窗去判斷這個位址是否落在自己管轄的範圍内(當然更有

可能這個動作是并行的)。假設首先cpu 視窗0 去判斷,它拿自己的mask 寄存

器去mask 這個傳入的位址,兩個cpu 視窗的mask 都是0xffffffff_f0000000。

mask 後,0x1fc00000 就成了0x1000_0000,這個位址是cpu 位址視窗1 的基

位址,是以它就負責由cpu 視窗1 來路由。在傳往下一個部件之前這個位址會

被cpu 視窗1 包裝一下,下面的就認識了。這個轉換公式我沒怎麼看懂,當然

也可能是作者文檔沒寫清楚。有興趣的可以看看。以上的位址轉換在cpu 核内,

還每到本橋的層次。

7

下面結合代碼解釋pmon 二進制的位址問題。

在連結階段,使用了ld.scripts 作為連結的規則。生成的gzrom 程式位址是從

0xffffffff81000000 開始。ld.script 的片段如下:

ENTRY(_start)

SECTIONS

{

. = 0xffffffff81000000;

.text :

{

_ftext = . ;

*(.text)

是以gzrom(包括用objcopy 生成的gzrom.bin)的程式位址是0xffffffff81000000

到其後的有限範圍之内的。而在cpu 執行這段代碼之初,pc 寄存器的值是

0xffffffffbfc00000,而程式自己認為這條指令的位址是0xffffffff81000000,在尋

址方式為pc 相對尋址或立即尋址時沒有任何問題,在其他尋址方式時就會出現

問題。比如在pmon 拷貝到記憶體之後,現在要引用位址為0xffffffff82000000 這

個程式位址的内容,指令實質為ld t0,0(0xffffffff82000000),問題就出來了,

0xffffffff82000000這個程式位址對應的段會使用cache,而目前cache還沒有初

始化,cache 不可用,唯一的解決方法就是将這個位址轉換到0xffffffffa0000000

~0xffffffffc0000000 這段空間内,且能映射到同一實體位置的位址,其實隻要在

原位址的基礎上再加一個偏移值即可。L 223 的lacate 的目的主要就是算出這個

偏移值,具體計算如下:

locate:

la s0,start

subu s0,ra,s0

and s0,0xffff0000

這裡有個很合理的疑問,為什麼連結規則檔案ld.script 中程式位址(start)要從

0xffffffff81000000 開始呢?直接搞成0xffffffffbfc00000 不更簡單嗎?

下面分析分析這個選擇的原因.從傳統的32 位位址空間的分布看,共有4 個段:

0 ~0x80000000,

0x80000000~0xa00000000,

0xa0000000~0xc0000000

0xc0000000~0xffffffff,

首先第一個段和第四個段首先被排除,因為這兩個段内的程式位址轉化需要頁表

的輔助,現在連記憶體都不可用,不可能用頁表。再要排除的是第三個段,用這個

段内的位址,可以免去上面這個偏移值的麻煩,但是處于rom 執行階段在整個

bios 的執行過程中隻占很少的一部分,99%以上的代碼是要被載入記憶體執行的,

就沒有這個偏移值了,且在記憶體執行時,當然要盡可能使用cache 加快執行速

度,第三個段的位址引用不使用cache。當然要提一句,如果不用cache,使用

第三個段作為程式位址也不是不可能的。

locate 除了計算以上說的那個偏移值到s0 外,接着重新初始化狀态寄存器和原

8

因寄存器,這個代碼應該是不必要的。

如果使用cpu自帶的序列槽子產品,則初始化這個子產品。在顯示卡沒有初始化的情形下,

調試最好的方式就是使用序列槽。initserial代碼和普通的序列槽除了位址,沒有差別,

具體可參看任何一本接口技術書籍,都有講解這個NS16550 相容晶片的。

龍芯cpu的序列槽位址COMMON_COM_BASE_ADDR 為0xbff003f8,這裡要說一

句,其實這個0xbff003f8 就是0xffffffffbff003f8,因為龍芯2 号系列cpu 都是64

位的,而且mips 結構的64 位cpu 都是32 位相容的,32 位的位址都預設被符

号擴充成64 位的,前頭的0xbfc00000 其實和0xffffffffbfc00000 也是一樣的。

L 452~L 512 的非定義語句都是作為初始化表,其實就是一些供特殊解釋的數

據。解釋部分在L 524~L 746。這部分和cpu 相關性非常大,由于有些對舊cpu

(比如2E)的支援代碼仍然存在,部分代碼對于龍芯2F 是不需要的。

下面是對北橋的pci 配置空間頭部的初始化(大小為256 位元組)。從這個意義上

說,北橋也是一個pci 裝置。北橋的pci 配置空間頭部首位址為0xbfe00000(龍

芯2f 手冊P107 表11-1 有誤,将pci header 和registers 的位址互換了),2F 的

pci 控制器遵循pci2.3 規範。

一句一句分析吧,L472 first。

BONITO_INIT(BONITO_PCICLASS,(PCI_CLASS_BRIDGE <<

PCI_CLASS_SHIFT) |

(PCI_SUBCLASS_BRIDGE_HOST << PCI_SUBCLASS_SHIFT))

現在我們需要一個文檔,pci2.3 的标準手冊。看該文檔P195 的圖,結合代碼,

可見這句在設定class code,結合P197 開頭的解釋,最高位元組是base clase,

這裡是PCI_CLASS_BRIDGE, 值為6, 次高位元組為sub class, 這裡是

PCI_SUBCLASS_BRIDGE_HOST ,值為0。看到P270 的解釋:base class

為06h,sub-class 為00h 表示這個pci 裝置為主橋(host bridge)。在pci 結構

中,主橋就是北橋,可見這句代碼表示我們的北橋是主橋。

L 473

BONITO_INIT(BONITO_PCICMD, BONITO_PCICMD_PERR_CLR|

BONITO_PCICMD_SERR_CLR|

BONITO_PCICMD_MABORT_CLR|

BONITO_PCICMD_MTABORT_CLR|

BONITO_PCICMD_TABORT_CLR|

BONITO_PCICMD_MPERR_CLR)

全是xxxx_CLR,初始化,pci command 和status 字段具體位的功能見規範P198

開始的解釋。

L 475 BONITO_INIT(BONITO_PCILTIMER, 255)

設定這個pci 裝置是一個單功能裝置,配置空間頭部使用pci 規範中表6-1 的布

局,設定cache L 大小為255,根據規範,這個值是無效的,等同0,是以以上

這句等同于BONITO_INIT(BONITO_PCILTIMER, 0)

L476 ~L 479

BONITO_INIT(BONITO_PCIBASE0, 0)

BONITO_INIT(BONITO_PCIBASE1, 0)

BONITO_INIT(BONITO_PCIBASE2, 0)

9

BONITO_INIT(BONITO_PCIEXPRBASE, 0)

初始化基位址為0,在BIOS 中這三個寄存器分别設定為通路系統記憶體、為ISA

裝置DMA 通路和北橋BONITO 寄存器通路的PCI 基址。

L 480

BONITO_INIT(BONITO_PCIINT, 0)

中斷的初始化。

L 482~L 483

BONITO_INIT(0x150,0x8000000c)

BONITO_INIT(0x154,0xffffffff)

bfe00000 到bfe00100 是北橋pci 配置空間的頭部,這兩句的作用位址在

bfe00100 到bfe00200 之間,是北橋的io 控制寄存器位址範圍。

這兩句的功能主要是設定DMA 的時候的位址掩碼,這個設定非常的重要。

L485 ,L 487

BONITO_BIS(BONITO_PCICMD, BONITO_PCICMD_PERRRESPEN)

BONITO_BIS(BONITO_PCICMD, PCI_COMMAND_IO_ENABLE|

PCI_COMMAND_MEM_ENABLE|

PCI_COMMAND_MASTER_ENABLE)

設定某些位,使能奇偶校驗和對io 空間,mem 空間的通路并允許北橋作為pci

總線上的主裝置。

BONITO_BIS(BONITO_BONGENCFG,

BONITO_BONGENCFG_DEBUGMODE)

使能視訊加速,這個注釋有問題。

L506 ~L 512

#define ISAREFRESH (PT_CRYSTAL/(1000000/15))

ISARD_INIT(CTC_PORT+PT_CONTROL)

ISAWR_INIT(CTC_PORT+PT_CONTROL,PTCW_SC(PT_REFRESH)|

PTCW_16B|PTCW_MODE(MODE_RG))

ISAWR_INIT(CTC_PORT+PT_REFRESH, ISAREFRESH & 0xff)

ISAWR_INIT(CTC_PORT+PT_REFRESH, ISAREFRESH >> 8)

在龍芯2F 的體系中,bfd00000 開始的1MB 空間是pci 的io 空間,這個空間兼

容傳統的isa 總線的io 位址分布。傳統的io 端口位址分布如下:

I/O 端口位址配置設定表

系統闆上接口晶片的端口位址擴充槽上接口控制卡的端口位址

I/O晶片名稱端口位址I/O接口名稱端口位址

DMA控制器1

DMA控制器2

DMA頁面寄存器

000H~00FH

0C0H~0DFH

080H~09FH

遊戲控制卡200H~20FH

10

中斷控制器1

中斷控制器2

020H~03FH

0A0H~0BFH

并行口控制卡1

并行口控制卡2

370H~37FH

270H~27FH

定時器

并行接口(鍵盤)

RT/CMOS RAM

協處理器

040H~05FH

060H~06FH

070H~07FH

0F0H~0FFH

串行口控制卡1

串行口控制卡2

3F8H~3FFH

2F8H~2FFH

原型插件闆(使用者可用) 300H~31FH

同步通信卡1

同步通信卡2

3A0H~3AFH

380H~38FH

單顯MDA

彩顯CGA

彩顯EGA/VGA

硬驅控制卡

軟驅控制卡

3B0H~3BFH

3D0H~3DFH

3C0H~3CFH

1F0H~1FFH

3F0H~3F7H

PC網卡360H~36FH

以上就是8254 的初始化,初始化這個定時器的作用為無,試過了,呵呵。

L524~L746 為初始化表(reginit)的解釋,代碼比較顯然。

L752~L 801 為cpu 序列槽的初始化(如果使用的話)。

配置空間的通路

福珑盒子和逸珑筆記本都使用AMD CS5536 的南橋,接着執行superio_init。

由于下面的代碼會涉及配置空間的通路,夾在南橋中間挺不爽的,就在這介紹吧。

通路嘛,肯定就有個位址的概念,是以先簡要介紹PCI 總線配置空間的尋址方

式。配置空間位址有兩種格式,如下:

由于迄今為止,在2F 使用到的pci 體系結構中都沒有超過一個pci 總線的,所

以我們可以以上圖中的第一個作為了解裝置位址的格式,總線域總為0。

圖中的device select 的編碼都是由連線決定的,比如主機闆設計出來後,cs5536

南橋的裝置号PCI_IDSEL_CS5536 就定下是14。

好,看看配置讀寫的函數的實作。先看讀函數PCICONF_READW。

PCICONF_RWADW 這個宏的定義如下:

#define PCICONF_READW(dev, func, reg) \

11

li a0, CFGADDR(dev, func, reg); \

li a1, PHYS_TO_UNCACHED(PCI_CFG_SPACE); \

and a2, a0, 0xffff; \

or a1, a2; \

srl a0, 16; \

li a2, BONITO_BASE+BONITO_PCIMAP_CFG; \

sw a0, BONITO_PCIMAP_CFG(bonito); \

lw zero, BONITO_PCIMAP_CFG(bonito); \

lw a0, (a1);

上述這個宏中CFGADDR 定義為:

#define CFGADDR(idsel,function,reg)

((1<<(11+(idsel)))+((function)<<8) +(reg))

就是根據上圖的方式組合出32 位的位址,作為要通路的配置空間的位址。

PCI_CFG_SPACE 值為0x1fe80000,是pci 通路配置空間的位址區域。

什麼意思呢?就是所有對pci總線上裝置的配置空間的通路都映射到這塊位址區

域。這塊空間的大小為2KB。這2KB 大小夠嗎?看看上面那個圖,device 以外

的都在位址的低部分,剛好是11 位,2KB 設計時就是按這個大小來的。

在龍芯2F 的pci 體系結構中,對pci 配置空間的讀操作順序是:

1.把目的位址的高16 位寫入PCIMAP_CFG 寄存器

2.對1fe080000 開始的那2KB 内的空間做讀/寫操作

3.從目的位址的低16 位和bfe80000 相或後的位址讀内容

2F 手冊P108 的頁尾描述介紹了這個過程。

BONITO_BASE+BONITO_PCIMAP_CFG 是0x1fe00118,

sw a0, BONITO_PCIMAP_CFG(bonito)

這句就是往這個0xbfe00118 寫那個32 位位址的高16 位。

lw zero, BONITO_PCIMAP_CFG(bonito);

這句緊接這上一句,看得出來,load個東西到zero寄存器就是不關心内容本身,

這裡隻是要對1fe080000 開始的那2KB 内的空間做讀/寫操作而已,再緊接的lw

a0, (a1)是真正意義上的讀,a1 寄存器放的是那個32 位址的低16 位。

這條指令執行完之後,我們要讀的配置寄存器内容就在a0 寄存器中了。

配置的寫類似,就不講了。

s uperi o_i ni t

superio_init 的代碼在L1966~L2271,這個代碼的了解需要結合AMD cs5536 手

冊的寄存器說明。

由于在代碼中會涉及到對msr 的通路,這裡就先介紹一下,免得到時犯暈。

msr 可以認為是外界眼中5536 的寄存器的位址,實際上是一種特定格式的

packet。用于指定将要通路的目标裝置。

有關msr 的位址先看Targets/Bonito/include/cs5536.h

12

有如下定義:

#define CS5536_SB_MSR_BASE (0x00000000)

#define CS5536_GLIU_MSR_BASE (0x10000000)

#define CS5536_USB_MSR_BASE (0x40000000)

#define CS5536_IDE_MSR_BASE (0x60000000)

#define CS5536_DIVIL_MSR_BASE (0x80000000)

#define CS5536_ACC_MSR_BASE (0xa0000000)

#define CS5536_GLCP_MSR_BASE (0xe0000000)

在南橋手冊的60 頁有如下的映射關系:

GLPCI_SB 5100xxxxh

GLIU 5101xxxxh

USB 5120xxxxh

IDE 5130xxxxh

DD 5140xxxxh

ACC 5150xxxxh

GLCP 5170xxxxh

也就是有如下對應:

0x0000xxxx 5100xxxxh

0x1000xxxx 5101xxxxh

0x4000xxxx 5120xxxxh

0x6000xxxx 5130xxxxh

0x8000xxxx 5140xxxxh

0xa000xxxx 5150xxxxh

0xe000xxxx 5170xxxxh

由于這個問題的特殊性,我們以0x5120xxxxh 到0x4000xxxx 為例,看看AMD

的cpu(不是龍芯cpu)是如何通路南橋msr 的。

先看到南橋手冊60 頁的Table 4-2 中對位址位的描述。

比如AMD 的南橋要通路USB 的msr,會發送0x5120xxxx 這種類型的位址,根

據Table 4-2中的對位址高9位的描述:These bits are shifted off to the left and

never enter the CS5536 companion device,也就是說,高9 位的位址是會被丢

棄的,不會傳到5536 上。

這個表中對于bit[22:14]的描述為:These bits are shifted into posi-tions [31:23]

by the time they reach the CS5536 companion device. Bits in positions [22:14]

are always 0 after shifting,也就是說這9 位會被shift 到高9 位,原9 位為0。

現在看下0x5120xxxx 的變化。

1.丢掉高9 位後,成了0x0020xxxx

2.bit[22:13]左移,成了0x4000xxxx

也就是說,雖然AMDcpu 發出對南橋USB 子產品通路的位址是0x5120xxxx,但

是這個位址進入南橋時就成了0x4000xxxx 了。

13

龍芯cpu 不會在傳遞的過程中做任何處理,是以我們要通路南橋的USB 子產品的

時候,就是發送0x4000xxxx 的位址了。其它的位址處理類似。

msr 的介紹就先到這,以下為superio_init 的代碼:

LEAF(superio_init)

// set the id select

li v0, 0xbfd00000;

li v1, PCI_CFG_BASE;//#define PCI_CFG_BASE 0x02000000

sw v1, 0(v0);

在2F 的定義中,0xbfd00000(1fd00000)開始的1MB 空間相當于傳統PC 的

io 空間,往bfd00000 這個位址寫不知道是幹什麼。

2:

PCICONF_READW(PCI_IDSEL_CS5536, 0, 0x00);

PCICONF_READW 從命名上看就是讀取cs5536 的0 号裝置配置空間的0 号寄

存器。這個配置空間的讀操作要傳入的參數有三個:dev,func,reg。

PCICONF_READW 執行完之後,要讀的内容已經放在a0 寄存器上了。

事實上,南橋上功能号為0的就是南橋本身這個裝置,pci配置空間的第一個word

(mips 中的字都是32 位的)就是廠商id 和裝置id,讀取後馬上比較是否正确。

代碼如下,0x208f 和0x1022 就是裝置id 和廠商id,代碼如下:

li a1, 0x208f1022;

beqa0, a1, 55f;

nop;

b 2b;

nop;

55:

// set the msr enable

PCICONF_WRITEW(PCI_IDSEL_CS5536, 0, 0xf0, 0x01);

cs5536 的配置頭部的詳細介紹在AMD 南橋手冊P234,msr enable 見P236。

PCI 配置空間寫的宏和讀基本一樣,隻是最後的操作為寫操作。手冊的解釋是

set to 1 to enable access to Model Specific Registers (MSRs)。

// active all the ports

CS5536_MSR_WRITE((CS5536_GLIU_MSR_BASE | 0x81), 0x0000ffff,

0x0);

不管如何,先看看CS5536_MSR_WRITE 的定義:

#define CS5536_MSR_WRITE(reg, lo, hi) \

PCICONF_WRITEW(PCI_IDSEL_CS5536, 0, 0xF4, reg); \

PCICONF_WRITEW(PCI_IDSEL_CS5536, 0, 0xF8, lo); \

PCICONF_WRITEW(PCI_IDSEL_CS5536, 0, 0xFC, hi);

可見,這個宏的第一個參數是寄存器的位址,後兩個參數組成一個64 位數值的

高低32 位。南橋配置空間的0xF4,0xF8,0xFC,分别是pci msr 位址,低32 位,

高32 位。具體解釋見南橋手冊P237,P238。

CS5536_GLIU_MSR_BASE 這個常量的定義如下:

14

#define CS5536_SB_MSR_BASE (0x00000000)

#define CS5536_GLIU_MSR_BASE (0x10000000)

#define CS5536_ILLEGAL_MSR_BASE (0x20000000)

#define CS5536_USB_MSR_BASE (0x40000000)

是以這個msr的位址實際就是0x10000081,想當于文檔中的位址0x51010081。

參考AMD P205,看到0x51010081 是port active enable reg,這個寄存器的初

始值是0x00000000_0000FFFF,後16 位是1 表示使能全部八個端口。這八個

端口從0 到7 分别是GLIU,GLPCI_SB,USB,IDE,DD,ACC,保留,GLCP。

跳過test 代碼。

CS5536_MSR_WRITE((CS5536_SB_MSR_BASE | 0x10), 0x00000003,

0x44000030;

這個值是初始值0x44000030_00000003,後兩位的3 表示mem/io 寫使能,其

餘的表示設定flush,read,timeout。具體解釋見AMD 手冊229 頁。

CS5536_MSR_WRITE((CS5536_DIVIL_MSR_BASE | 0x0b),

SMB_BASE_ADDR, 0xf001);//AMD 357 頁

CS5536_MSR_WRITE((CS5536_DIVIL_MSR_BASE | 0x0c),

GPIO_BASE_ADDR, 0xf001 );

CS5536_MSR_WRITE((CS5536_DIVIL_MSR_BASE | 0x0f),

PMS_BASE_ADDR, 0xf001);

上面的幾行代碼的性質相同,從參數的命名就可以看出來,是設定io space 的

起始位址(也就是說這個起始位址是我們主動寫入的,這些位址會在pci裝置的初

始化之後重新配置設定)。具體解釋見AMD 手冊P357,這些位址的定義如下:

#define DIVIL_BASE_ADDR 0xB000

#define SMB_BASE_ADDR (DIVIL_BASE_ADDR | 0x320)

#define GPIO_BASE_ADDR (DIVIL_BASE_ADDR | 0x000)

#define MFGPT_BASE_ADDR (DIVIL_BASE_ADDR | 0x280)

#define PMS_BASE_ADDR (DIVIL_BASE_ADDR | 0x200)

#define ACPI_BASE_ADDR (DIVIL_BASE_ADDR | 0x2c0)

後面那個0xf001,0xf 是io mask,1 表示enable LBAR。

南橋序列槽的代碼跳過。

接下來是南橋的ide 部分,自帶的中斷控制器8259 和開機鍵的初始化。都是

GPIO 的設定,說重要吧也重要,不過太多了,跳過。

下面是SMBus 的初始化,這個是非常重要的。等會探測記憶體的spd,就是通過

SMB 總線。是以這個初始化是必須的。具體代碼略過。

略過gpio,flash 和usb 部分的測試。

以上是南橋相關的初始化,接着看代碼。

如果定義了TEST_CS5536_SMB,則執行L814~L845,預設不執行。但了解這

15

個代碼對于了解smb(i2c)的通路機制是很有好處的。是以還是委屈各位友善

的話看看吧。

start.S 之記憶體

L855~L871 是些列印語句,也不知道有什麼必要。

接下來要執行的是L929,L929~L940 實際上就是一下幾句:

li t2, 0xbfe00180 //Chip_config0

ld a1, 0x0(t2)

anda1, a1, 0x4ff

前21 位保留,bit9-10 使能pci 和cpu 寫記憶體寫到記憶體,bit8 禁用ddr2 配置

sd a1, 0x0(t2)

bfe00180 對應的是chip_config0,具體位功能定義見2F 手冊P116。

L 942 li msize, 0x0f000000

這句沒有作用,在L979 被重新初始化了,可以去除。

L944 調用ddr2_config ,初始化記憶體控制器

bal ddr2_config

nop

ddr2_config 的代碼在start.S 檔案的底部,是L2402~L2657

L2411 move s1, ra // 儲存傳回位址

L2413 la t0, ddr2_reg_data // 取初始化資料的首位址

L2416 addu t0, t0, s0

li t1, 0x1d //1d 就是29

li t2, CONFIG_BASE

這個ddr2 的初始化表和前頭的reginit 類似,但有一點明顯不同,那就是這個地

址要加個偏移值(L2416)。原因是la t0, ddr2_reg_data 這條指令執行後,t0

的值是0xffffffff8100xxxx,不能直接使用這個位址,原因在前頭已經講述。是以

在L2416 要加上這個偏移值。

2F 手冊P97 頁有句話“具體的配置操作是對實體位址0x000000000FFFFE00 相

對應的29 個64 位寄存器寫入相應的配置參數”,足以解釋L2417 和L2418 代

碼的作用。

L2465~L2470

ld a1, 0x0(t0)

sd a1, REG_ADDRESS(t2)//注意是sd,8 個位元組

subu t1, t1, 0x1

addiu t0, t0, 0x8

16

bnet1, $0, reg_write

addiu t2, t2, 0x10 //t2 是位址,每個配置寄存器是16 個位元組

以上代碼是循環些寫完29 個配置寄存器。

略過debug 代碼,執行到L2500.

L 2500 move k0 , ra //不知道這個ra 值的儲存有什麼意義。

L 2501

li a0, 0xa0

li a1, 3 //Row Address

bal i2cread

Nop

L2501的注釋已經說明了以下代碼的功能是讀取spd資訊初始化記憶體控制器了。

0xa0,0xa1 作為i2cread 的傳入參數。

這裡有一個問題,記憶體上的spd 的位址是多少?

從代碼上看是0xa0 啦。網上搜了很久,有網友有這樣的文章:“memory SPD

EEPROM 是走SMBus 總線,SMBus 總線下的裝置是通過SlaveAddress 來标

識,一般Slot 0 上的memory SPD EEPROM 的SlaveAddress 為0x0A0,Slot 1

上的memory SPD EEPROM 的SlaveAddress 為0x0A2,你可以向SMBus 控

制器發送指令來讀取SPD 資料。讀deviceID 要+1, 是以是0xa1,0xa3,0xa5,

如果是write 的話,就是0xa0,0xa2,0xa4”。

我看了幾個記憶體的手冊,都沒有看到關于spd 的i2c 位址的約定。但是我曾經看

過一個華邦的時鐘晶片手冊,手冊中就明确寫了i2c 的位址。

實際上裝置位址的最後一位為1表示讀操作,為0表示寫操作。下面看看i2cread

是如何使用這兩個傳入參數的,i2cread 的代碼在L2311~L 2394,

開始将a0 和a1 轉存到t2 和t3 上,之後:

IO_READ_BYTE(SMB_BASE_ADDR | SMB_CTRL1);

ori v0, SMB_CTRL1_START;

IO_WRITE_BYTE(SMB_BASE_ADDR | SMB_CTRL1);

IO_READ_BYTE(SMB_BASE_ADDR | SMB_STS);

andi v0, SMB_STS_BER;

bnez v0, i2cerr;

nop;

SMBUS_WAIT;

bnez a3, i2cerr;

nop;

開頭的注釋已經說明了這段的功能,就是啟動smbus 工作。

move v0, t2;

IO_WRITE_BYTE(SMB_BASE_ADDR | SMB_SDA);

IO_READ_BYTE(SMB_BASE_ADDR | SMB_STS);

andi v0, (SMB_STS_BER | SMB_STS_NEGACK);

17

bnez v0, i2cerr;

nop;

SMBUS_WAIT;

bnez a3, i2cerr;

nop;

這裡要注意到v0 是作為IO_WRITE_BYTE 的傳入參數使用的

IO_READ_BYTE(SMB_BASE_ADDR | SMB_CTRL1);

ori v0, (SMB_CTRL1_ACK);

IO_WRITE_BYTE(SMB_BASE_ADDR | SMB_CTRL1);

move v0, t3;

IO_WRITE_BYTE(SMB_BASE_ADDR | SMB_SDA);

IO_READ_BYTE(SMB_BASE_ADDR | SMB_STS);

andi v0, (SMB_STS_BER | SMB_STS_NEGACK);

bnez v0, i2cerr;

nop;

SMBUS_WAIT;

bnez a3, i2cerr;

nop;

發送指令的下一部分,這個是index,傳入參數是t3,就是開始的a1。

IO_READ_BYTE(SMB_BASE_ADDR | SMB_CTRL1);

ori v0, SMB_CTRL1_START;

IO_WRITE_BYTE(SMB_BASE_ADDR | SMB_CTRL1);

IO_READ_BYTE(SMB_BASE_ADDR | SMB_STS);

andi v0, SMB_STS_BER;

bnez v0, i2cerr;

nop;

SMBUS_WAIT;

bnez a3, i2cerr;

nop;

重新初始化,這是smbus 的約定。

move v0, t2;

ori v0, 0x01;

IO_WRITE_BYTE(SMB_BASE_ADDR | SMB_SDA);

IO_READ_BYTE(SMB_BASE_ADDR | SMB_STS);

andi v0, (SMB_STS_BER | SMB_STS_NEGACK);

bnez v0, i2cerr;

nop;

18

SMBUS_WAIT;

bnez a3, i2cerr;

nop;

上一頁提到的,讀的話deviceID 要加一,就是ori v0, 0x01 起到了加一的作用。

li v0, SMB_CTRL1_STOP;

IO_WRITE_BYTE(SMB_BASE_ADDR | SMB_CTRL1);

SMBUS_WAIT;

bnez a3, i2cerr;

nop;

IO_READ_BYTE(SMB_BASE_ADDR | SMB_SDA);

IO_READ_BYTE 的定義如下:

#define IO_READ_BYTE(reg) \

lui v1, 0xbfd0; \

ori v1, reg; \

lbu v0, 0(v1);

可見是把讀的結果放到v0 中,這樣至此我們就通過smbus 的協定把要讀的結果

讀到了v0 寄存器中,随後傳回。

jr ra;

nop;

具體的smbus 相關的約定參看smbus 總線定義。

回到我們調用的入口,就是要将smbus 總線上(相對)位址為a0,index 為3

的内容讀到v0 寄存器中。spd 的相關資訊可參看kingmax 記憶體的手冊P18

~P21,先介紹一下記憶體控制器需要的幾個參數的含義吧。

代碼中讀取的參數共有4 個,index 分别是3,4,5,11h(17)。以kingmax ddr

-667 512MB 記憶體為例解釋讀出的各參數的含義。(kingmax 手冊P20)

index 為3讀出的值為0Dh,表示row address 是13,index 為4讀出的值為0Ah,

表示col address 是10,這兩個讀出的參數經過簡單的轉化,寫入記憶體控制器的

一個寄存器(程式位址為0xaffffe50),具體見2F 手冊P99對這個寄存器的解釋。

index 為5 讀出的值為60h,表示可選的記憶體chip 為1。這個寄存器解釋見2F

手冊P100。

下面是L2550 ~L 2562 的代碼,其中bne 一句表示讀出的bank 值為8 的話,就

重新設定affffe10 這個位址的寄存器,具體見2F 手冊P98 對這個寄存器bit32

的定義。可見8bank 的情形可能是比較特殊的。

li a0, 0xa0

li a1, 0x11 //Bank

bal i2cread

nop

19

//li v1, 0x4

//beq v1, v0, 1f

li v1, 0x8

bne v1, v0, 1f

nop

li v1, 0x1

li v0, 0xaffffe10

sw v1, 4(v0) //注意位址

不管如何,記憶體的配置就這樣完成了,下面發個信号告訴記憶體控制器配置完成。

############start##########

li t2, CONFIG_BASE

la t0,DDR2_CTL_start_DATA_LO

#la t0,ddr2_start_reg

addu t0, t0, s0

ld a1, 0x0(t0)

sd a1, 0x30(t2)

記憶體控制器的初始化完成後, 列印個資訊表示一下。代碼回到L949,

L951~L962 是記憶體隻讀檢測。

L 966~L972

#if 1

TTYDBG ("\r\ndisable register space of MEMORY\r\n")

li t2, 0xbfe00180

ld a1, 0x0(t2)

or a1, a1,0x100

sd a1, 0x0(t2)

#endif

由于記憶體控制器的配置已經完成,L966~L972 代碼設定從現在起禁用DDR2 配

置端口,以免意外。具體見2F 手冊P116。

其實記憶體的工作還沒有做完,前面ddr2_config隻是做了記憶體顆粒的行列位址數,

bank 數,并沒有得到具體的記憶體大小資料。

L 979

li msize, 0x10000000;

注釋說設定預設記憶體大小是256MB,這樣具體有什麼作用呢?無論如何都在

L1009 被重新指派。

L982 開始讀取spd 上的index 為31 的内容, 還是前面kingmax DDR2-667

512MB 記憶體為例。讀出值為80h。

li a0, 0xa1;

li a1, 31;

20

bal i2cread;

nop;

beqz v0, .nodimm;

nop;

sll a0, v0, 2

la a1, bigmem

addu a1, a1, s0

addu a1, a1, a0

lw tmpsize, (a1)

li a0, 0x10

blt v0, a0, 2f

nop

sll tmpsize, v0, 22; // multiply by 4M

tempsize 存儲相應的預計記憶體大小的值,但正如我們的例子,讀出的是80h,

左移22 位,就是128x4MB=512MB,參考文檔中還有hynix512MB 的記憶體spd

表,也可參考之。這段代碼之後tmpsize 為2^29.,就是512MB。其中:

la a1, bigmem

addu a1, a1, s0

addu a1, a1, a0

這幾句話的作用不詳。

接着執行到L999,

2:

li a0, 0xa1;

li a1, 5

bal i2cread

nop

bltu v0, 0x60, .nodimm;

nop;

subu v0, 0x60;

sll tmpsize, v0;

這個内容先前已經探測過了,這個傳回的資料如果小于60h,表示這個記憶體是

DDR 記憶體或者記憶體顆粒不存在。如果傳回值是61h,表示有兩邊,記憶體容量加

倍。比如kingmax文檔中所寫的1G記憶體的spd,index31的資料為80h,與512MB

的數值相同,但index 為5 的數值1G 的為61h,512MB 的為60h,可見這兩個

資料就可确定了一條記憶體的容量。至此,spd 就再沒什麼用了。

start.S 之cache

接着執行到L1033~L1039,功能就是把bfe00108 的bit5~bit2 設成00011b,設

21

定rom 資料讀取延遲。L1041~L1046 設定io 資料讀取延遲,具體見2F 手冊

P114。(為什麼不合并這兩個操作呢???)

L1060~L1069 的代碼通過對比2E 北橋文檔和2F 文檔及bonito.h 檔案,基本确

定屬于2E 的遺留代碼。

ok,記憶體控制器的初始化算是完畢了。接下來是cache 的初始化。

由于在上電之後,cpu 的cache 處于不确定的狀态。

cache 初始化後,速度會快很多,下面看看cache 是怎麼初始化的吧。

cp0 的config 寄存器指明了cache 的基本情況,config 的位定義如下:

IC 表示一級指令cache

DC 表示一級資料cache

IB 表示一級指令cache 行大小

DB 表示一級資料cache 行大小

K0 确定cache 的一緻性算法

具體見2F 手冊P49。

龍芯2F 采用的是兩級cache 的結構,其中一級cache 包括64KB 的指令cache

和64KB 的資料cache,二級cache 為512KB(不區分資料與指令)。

龍芯的一級資料和指令cache,二級cache 都是4 路組相連的,cache 的組織有

必要說明一下,對了解代碼是必須的。

以一級指令cahce為例(指令cache和資料cache的tag有不同),示意圖如下:

注:這個圖是從龍芯手冊粘貼的,其中的index 512 應為511。

22

其中每個set 大小為16KB,每個set 内部有512 個cache 行,每個cache 行的

大小為資料32 位元組,指令cache 的tag 為29 位,其中1 位是有效位,實體地

址标記ptag 為28 位。資料cache 的組織基本與這相似,由于資料是不像指令

是隻讀的,是以tag 中多了個狀态位,其定義如下:

其中1 位W 表示已經寫回,2 位CS(cache state)訓示cache 的狀态,如下:

00 --- 無效

01 --- 共享

10 --- 獨占

11 --- 髒

龍芯的cache 以及TLB 與x86 的不同,對系統程式員不是透明的,是以cache

的組織是需要軟體人員處理的,用的武器就是cache 這條指令。cache 指令的詳

細說明在2F 手冊8.2.6。

指令和資料的cache 都是28 位的位址位,在龍芯2F 的體系中,實體位址實際

都是40 位的,tag 中的是高28 位,低12 位和虛拟位址相同。

接着看代碼到L1125,對照2F 的手冊,明顯感到這個代碼與手冊有一定差别,

很懷疑L1125~L1129 的必要性。

再到L1131,接下是算IC 和DC 的大小,IC 計算公式為size=2^(12+IC),

DC 類似。龍芯2F 的一級指令和資料cache 均為64KB,IC/DC 值為100b,也

就是4,但看這個代碼,意圖是報指令cache 的大小存進s3,但是計算方法有

誤。事實上,在pmon 後面的代碼中又對這個cache 進行了測試,這裡的探測

沒有實際意義。一級指令cache 行大小的計算公式為IC 等于1,則為32Byte,

為0 則為16Byte,DB 計算類似。L1153 和L1154 的計算方式很巧妙。之後的

是資料cache 的計算。

無論如何,現在找到了cache,要去初始化這些cache 了,這個工作是通過調用

godson2_cache_init 實作的。代碼在L1693。

L1694 到L1705 的功能是檢測一級指令和資料cache 的大小到t5 和t6 寄存器

中,可是這段代碼實在是比較爛。不說了,不過從中可看出,這裡又檢測了一次,

故前面的cache 大小的檢測是沒有意義的。

看代碼到L1717~L1720,看代碼的命名和注釋目的是初始化一級資料cache:

1: slt a3, v0, v1

beq a3, $0, 1f

nop

mtc0 $0, CP0_TAGLO

cache Index_Store_Tag_D, 0x0(v0)

mtc0 $0, CP0_TAGLO

cache Index_Store_Tag_D, 0x1(v0)

23

mtc0 $0, CP0_TAGLO

cache Index_Store_Tag_D, 0x2(v0)

mtc0 $0, CP0_TAGLO

cache Index_Store_Tag_D, 0x3(v0)

beq $0, $0, 1b

addiu v0, v0, 0x20

這裡有一個虛位址(VA)的概念,比如上面這條指令,當v0 為0x80000000 是,

這條指令的虛位址為0x80000000+0x0,這個虛位址是有講究的,位址的低兩

位表示要操作的路(way,4 路組相連的那個’路’),代碼中v0 都是每次加0x20,

是以前面代碼中cache 指令中逗号之後的數字就是路号。之是以要加

0x80000000 是因為這個位址是uncache 的,否則會有異常産生。

Index_Store_Tag_D 這個命名很明顯就是要把TAGHI 和TAGLO 中的資料按照

一定的方式填充cache。TAGHI 和TAGLO 的位定義2F 手冊5.20。這裡cache

指令目的就是設定這些cache 的内容無效。代碼中每次cache 後都要重新設定

TAGLO,不知何故,約定???

有一點要提的是Index_Store_Tag_D 的定義為0x5(00101b),反彙編中也很

明顯,但2F 手冊的P89 中這個數值對于Index Load Tag Dcache 的操作碼,

Index Store Tag Dcache 的操作碼是0x9(01001b),檢視了mips64 的cache

指令編碼定義,如果2F 遵守mips64 的規範,則2F 手冊的解釋應該是對的。代

碼的作者疏忽了。但居然還能用,奇怪了。

接下來初始化L2 cache,大小是代碼中寫死的,2F 手冊中也沒有講哪個寄存器

是标志二級緩存大小的, 隻能寫死。每路128KB, 也是4 路。這個

Index_Store_Tag_S 的操作碼和2F 手冊是一緻的。

接下來的代碼将二級緩存和指令cache 每個cache 行目前内容置為無效。(為什

麼沒有資料cache 的無效設定)

至此,godson2_cache_init 這個過程執行完畢。

代碼傳回後,執行L1171。L1171 和L1172 功能不詳,可能是2E 遺留代碼。

L1174~L1177 功能是置config 寄存器bit1 為1,bit0 為0,bit12 為0。這些代

碼設定的位可能也是有些問題,比如bit12 在2Fcpu 中是寫死的,bit0,bit1,

bit2 這3 個位合在一塊才能确定k0 的cache 一緻性算法。這裡隻寫兩位,有點

不倫不類。估計是前些版本的遺留代碼。

好了,cache 就算這麼初始化了,看看下面要幹些什麼。

這裡先縷縷思路。我們初始化記憶體的目的是使用記憶體,初始化cache 的目的是

用cache提高記憶體使用效率,現在基本設定都差不多了。該準備搬rom 中的bios

代碼到記憶體上來了吧。

在搬到記憶體前可以測測記憶體的穩定性,方法就是往0xa0000000 開始的1M 空間

寫後讀,看前後内容有沒有差異,這個位址也不是随便取的。

記憶體測時候,代碼執行到了L1257,一句“Copy PMON to execute location”告訴

我們拷貝bios 到記憶體的行動已經萬事俱備了。

跳過debug 部分,到了L1278。

la a0, start

24

li a1, 0xbfc00000

la a2, _edata

or a0, 0xa0000000

or a2, 0xa0000000

subu t1, a2, a0

srl t1, t1, 2

以上的代碼功能比較清楚,設定各個寄存器

A0 pmon 的起始程式位址

A1 pmon 的起始實體位址,對應程式位址a0

A2 pmon 末尾位址_edate

T1 pmon 的實際大小(以字為機關,拷貝的時候用的是lw 和sw 指令)

值得一說的是or ...,0xa0000000 這句的作用。此時,cache 仍不能使用,因為

TLB 還沒有初始化,是以仍要避免使用cache的程式位址的使用,a0既是pmon

的起始程式位址,作者又将其作為在記憶體中的實體位址使用(or 指令之前的那

個位址值),這個使用方式是十分正确的。比如假設start 這個标号的位址是

0x81001234,那麼作者希望它搬到記憶體實體位址為0x01001234 的位置,

後面的代碼有點意思:

move t0, a0

move t1, a1

move t2, a2

前面算好的t1 被覆寫掉了,呵呵。

至此,可以這麼了解了,t0 表示複制的目的起始位址,t2 是複制的目的結束地

址,t1 是複制的源位址。

L1292~L1307 是複制的具體代碼,是指就是一個循環,每次複制一個字,标号

1 到2 之間的代碼是用于調試目的的。

之後清零bss 段所占的空間,之後又是一段test 代碼,跳過。

程式此時執行到了L1368,看注釋可以猜測,TLB 的初始化就要開始了。在TLB

初始化前我們隻能使用的程式位址都是在0xa0000000~0xc0000000(64 表示

的話就符号擴充)之間。這個TLB 的初始化時機是有講究的,不一定說什麼時

候最好。但TLB 是很小的,如果在複制操作之前使能TLB,是不是有意義???

可能白白地看着TLB 被迅速填滿又馬上被替換。

回到TLB 初始化的具體代碼。

L1373~L1374 是設定L3 大小,對于2F 無效,這兩句代碼當舍去。

接下的是設定a0,a1,a2 這三個寄存器。

A0 --- 記憶體大小(機關MB)

A1 --- tgt_putchar 函數的在rom 中位址

A2 --- stringserial 函數的在rom 中位址

至于a1 和a2 的位址為什麼這樣設定,我暫時沒有想明白。

la v0, initmips

jalr v0

nop

以上三句,是cpu 從rom 取指令到從記憶體取指令的轉變,從此記憶體執行的時代

25

開始了。jalr v0 的操作是PC<--v0,不是bal 那樣的PC+offset。initmips 的地

址是0x8 開頭的,并不使用TLB,是否使用cache 要看config 寄存器的低3 位,

前面說到了,程式隻設定了低兩位為1,0,(非常不嚴謹的做法),底3 位隻有

3 種情況有效,111,011,010,是以就算第三位是010 了,設定不使用cache。

到這我們可以明白了,那個TLB 的注釋是挂羊頭賣狗肉,根本就沒有什麼初始

化,和程式開始的時候那個16 條指令的限制一樣,都是玩笑。

真正的TLB 初始化在哪裡呢?????????????

從彙編到c

這個initmips 在哪裡呢?看過一些文檔都說是在tgt_machdep.c 中,我一開始也

是這麼認為的,但看看前面的a0,a1,a2 顯然都是作為參數清單使用的(去看看

o32 的參數傳遞規則吧)。那個檔案中的initmips 隻有一個參數呀,實際上這個

函數的定義在initmips.c 中,這個檔案是在編譯過程中産生的。在我寫的《pmon

編譯過程分析》中寫到:“initmips.c 的生成規則對應compile-output 的line 710”,

看看compile-output 檔案中的line710,看到了吧

./genrom ../Targets/Bonito/compile/Bonito/pmon > initmips.c

genrom 是一個用perl 語言寫的腳本,我不懂這個語言,但這個腳本很短,基本

的意思也很清楚。

initmips 函數定義在initmips.c 中L66,當初我是怎麼發現這個函數的呢?在當

時這個函數的L72 和L78 的列印語句還沒有被注釋掉,我在通過序列槽(顯示卡初

始化前隻能用序列槽輸出)做pmon 的實驗的時候發現中間的“Uncompressing

Bios”這一句在不知道什麼時候列印出來的,根據大部分文檔的解釋,直接就跳

到tgt_machdep.c 中,就不可能有這一句的列印。就這樣找到了這個initmips.c

中了。當然通過反彙編看跳轉的位址其實一下就看出來了。

總算看initmips 的具體代碼了,也總算是看c 代碼了。

開始幾句的位址值是在perl 腳本中得到的。

L73 if(!debug||dctrl&1)enable_cache();

這個debug 的值正常情況下為0(因為msize 不為0),也就是enable_cache

函數要執行。但是有個問題,這個dctrl 是什麼東西?這個隻是通過形式參數傳入

的,按照o32 的标準也就是a0 對應函數的第一個形參(從左往右數,)依此類

推。a2 對應的就是第三個參數。這個initmips 的函數定義是initmips(unsigned

int msize,int dmsize,int dctrl),也就是a2 對應的就是dctrl,我們肯定還記得,

調用initmips 前給a0,a1,a2 這三個寄存器賦了值,a0 是記憶體大小(機關MB),

a1 是tgt_putchar 函數的在rom 中位址,a2 是stringserial 函數的在rom 中地

址,對照這個函數的定義,第一個參數肯定是對的,當後面的兩個就牛頭不對馬

嘴了。我是又看不懂了。

用minicom 看使用序列槽的輸出是比較簡單的。有些想法也可以修改源碼,通過

序列槽輸出來驗證。minicom 的簡單使用如下:

26

apt-get install minicom

minicom -s //設定端口為/dev/ttyS0 即可

插上序列槽線,

minicom

下面就通過minicom 看看這個dctrl 的值。在int debug=(msize==0)這一行之後

加上一行hexserial(dctrl);即可

copy text section done.

Copy PMON to execute location done.

sp=80ffc000bfc010b8

紅字加粗的部分就是了,可見的确是stringserial 函數的在rom 中位址.是以這個

命名和if(!debug||dctrl&1)enable_cache()這句的條件判斷部分就挺怪的了。可能

隻有作者能解釋了這個dctrl 的作用了。

無論如何,要去執行enable_cahe 這個函數了。enable_cache 函數在同一個文

件中,采用的是内嵌彙編。這段代碼就是簡單的把config 寄存器(cp0 的16 号

寄存器)低3 位設成011(3),即cachable。從此kseg0 段(0x8,0x9 開頭的)

開始使用cache 了。

接下來執行

while(1)

{

if(run_unzip(biosdata,0x80010000)>=0)break;

}

從名字就可以看出來是在解壓。biosdate 是一個數組, 内容在zloader

/pmon.bin.c 中。我第一次打開pmon.bin.c 檔案是差點昏過去。pmon.bin.c 檔案

的生成見compile-output 檔案:

gzip ../Targets/Bonito/compile/Bonito/pmon.bin -c > pmon.bin.gz

./bin2c pmon.bin.gz pmon.bin.c biosdata

bin2c 也是個perl 腳本,就是把pmon.b in.gz 的内容轉化成biosdata 這個數組,

否則不友善內建到代碼中。

這個解壓的代碼就不深入了,要知道壓縮軟體工作方法的可以看看。這個函數的

作用就是把biosdata 的數組加壓到0x8001000 開始的位址上去。當然這個

biosdata 的内容究竟是什麼呢? 我們是一定要探究的。biosdata 來自于

pmon.bin.gz,pmon.bin.gz 來自于pmon.bin ,那麼pmon.bin 是哪兒來的呢?

由于gzrom.bin 是來自于objcopy 後的gzrom,這個pmon.bin 很可能是來自于名

為pmon 的elf 檔案。而且./Targets/Bonito/compile/Bonito/下正好有個叫pmon

的elf 檔案,無疑了。問題來了,這個pmon 檔案哪兒冒出來的呢!

看compile-output L703

mips-elf-ld -EL -N -G 0 -T../../conf/ld.script -e start -S -o pmon

${SYSTEM_OBJ} vers.o

看到了吧,其實這個pmon 幾乎把所有代碼都編進去了。包括start.o(雖然個人

認為沒有必要), 不過這裡的ld.script 不同而已, 是代碼段位址從

0xffffffff80010000 開始配置設定。個人推斷,這個pmon 用objcopy 後的pmon.bin

是可以直接使用的,隻是沒有壓縮。有興趣時試試。

27

實際上./Targets/Bonito/compile/目錄隻是一個中轉站。回到initmips 這個函數。

接下是bss 段的初始化,就是清零。

接着調用flush_cache2(),就是把二級緩存都置為無效。如果記性好的話,前面

已經做了同樣的工作了。

godson_2f:

li $2, 0x80000000

addu $3,$2,512*1024

10:

cache 3, 0($2)

cache 3, 1($2)

cache 3, 2($2)

cache 3, 3($2)

addu $2, 32

bne $2,$3, 10b

不過前面的那個512*1024 改成了128*1024 而已,哪個才是對的呢?個人認為

是前者。還有2F 的prid 手冊标稱是0x6302,與實際不附。唉。

initmips 函數最後調用realinitmips 這個函數,從名字看出,這個realinitmips 才

是大家都認為的initmips。genrom 腳本中擷取了真實的initmips 的位址,這個

函數還初始化sp 寄存器,棧大小為16KB(0x4000),并把msize 作為參數傳

遞給那個initmips 函數,然後跳轉到那個函數。

從現在開始執行的就是那個叫pmon.bin 的檔案的内容了,看反彙編也要看

Targets/Bonito/compile/Bonito/下那個pmon 檔案的反彙編了,從此程式位址都

不再用0x81 開頭的了,都用0x8001 開始的了。

initmips

首先執行的函數是initmips,在tgt_machdep.c 檔案中。

對于512MB 的記憶體,設定memorysize 為256,memorysize_high 為256,這

個memorysize 是pmon 中能使用的記憶體大小,pmon 是不需要那麼多記憶體。

接下來執行tgt_cpufreq()函數。實際上是執行_probe_frequencies 函數探測cpu

的頻率。這個函數也在同一個檔案中。

_probe_frequencies()

{

.。。。。。。。

SBD_DISPLAY ("FREQ", CHKPNT_FREQ);

md_pipefreq = 300000000;

md_cpufreq = 66000000;

。。。。。

28

SBD_DISPLAY 這個函數就是一個列印函數,并在代碼中寫死隻列印4 個字元,

這個限制可以通過在那個函數中寫個for 循環解除,後面的那個參數沒有使用。

看到這有兩個freq,一個是pipefreq,一個是cpufreq,暫時先不解釋,看了代

碼就清楚了。HAVE_TOD 在../conf/GENERIC_ALL 檔案中定義。

接着調用init_legacy_rtc(),這個函數在tgt_machdep.c 中定義。

這個函數是對實時鐘的操作。PC 中通過I/O 端口0x70 和0x71 來讀寫RTC 芯

片的寄存器。其中端口0x70是RTC的寄存器位址索引端口,0x71是資料端口。

這個函數的意圖就是讀取目前的時間(人了解的格式,年月日時分秒)。

接着執行一個for 循環

/ * Do the next twice for two reasons. First make sure we run from

* cache. Second make sure synched on second update. (Pun intended!)

*/

for(i = 2; i != 0; i—) {

cnt = CPU_GetCOUNT();

timeout = 10000000;

while(CMOS_READ(DS_REG_CTLA) & DS_CTLA_UIP);

sec = CMOS_READ(DS_REG_SEC);

do {

timeout--;

while(CMOS_READ(DS_REG_CTLA) & DS_CTLA_UIP);

cur = CMOS_READ(DS_REG_SEC);

} while(timeout != 0 && cur == sec);

cnt = CPU_GetCOUNT() - cnt;

if(timeout == 0) {

break;

}

}

假如單看for 循環内部的話,這個代碼的功能就是看1 秒鐘cpu 的count 寄存器

變化的值。這裡先介紹這個cpu 内部的count 寄存器。這是個32 位的一個計數

器,每兩個cpu 時鐘周期加一次,假設2F 頻率為800MHz 的話,每10.74s,即(2

^32x2) / (800x10^6)溢出一次。CPU_GetCOUNT 函數就是去讀取這個寄存器

的值而已,代碼定義在./pmon/arch/mips/mips.S

LEAF(CPU_GetCOUNT)

mfc0 v0, COP_0_COUNT

nop

j ra

nop

END(CPU_GetCOUNT)

29

接着看,中間那個do-while循環目的是等待一秒,退出條件有兩個,要麼timeout

為0,要麼讀取的RTC 到了下一秒。這裡有個有趣的問題是:timeout 變成0 要

多久。timeout 的初始值為一千萬,timeout 要變成0,這個循環就要執行一千萬

次,那麼這個循環體每次執行要多少世界呢?可以确定的是這個循環體指令不

多,但有兩處對io 接口的通路,這個是比較花時間的。作者的原意是比較明顯

的,就是正常條件下,一千萬次循環時間要遠遠大于一秒。假如一個循環的時間

是80個時鐘周期,一千萬次就是80x10^7個時鐘周期,以2F 主頻800MHz 計,

就是1s 鐘。可見作者是認為一個循環的時間要遠遠大于80 個時鐘周期。當然

以上的是比較理想的情況,實際上,TLB miss,Cache miss 都加大了這個時間

的不确定性,是以外邊要套個for 循環,卻保這段代碼指令,和變量都在cache

中了。這是作者開頭注釋說的第一個原因,他還說了第二個原因,就是while

(timeout != 0 && cur == sec)這個條件為假的時候,實際上就是cur 開始不等于

sec 的時候,就是RTC上剛好更新到下一秒的時候。在第一次進入那個do-while

循環的時候,那個秒的含義是不确定的,可能是一妙的剛開始,也可能是一秒的

中鍵,也有可能是一秒的末尾了,這樣到下一秒到來的時候,這個時間間隔在0

~1s 之間,不能确定是否足夠接近一妙,而循環兩次就可基本解決這個問題。雖

然不可能絕對精确,但是這個時間間隔是極為接近1s 的。這是作者注釋的同步

(synched )的意思吧,我認為。這裡好有個問題,就是count 寄存器是會溢

出的,而且前面算了,每10 秒多一點就溢出一次,看看這個函數的定義:可見

這個傳回值是無符号的。

u_int CPU_GetCOUNT __P((void));

但是_probe_frequencies()中的cnt 卻是int 型的。用無符号的話可以處理溢出的

問題,但這用int 也可以用,不解。

接着就按這一秒鐘内的count 寄存器變化的值要算出cpu 的主頻(pipefreq),

if (timeout != 0) {

clk_invalid = 0;

md_pipefreq = cnt / 10000;

md_pipefreq *= 20000;

md_cpufreq = 66000000;

}

這裡的cpufreq 可能是外部總線的頻率吧(66M)。前面算pipefreq 直接乘以2

不行嗎,為什麼這樣?不管效率,反正cpu 的頻率算出來了。

至此這個函數調用結束,傳回initmips 函數。

dbginit

這個函數的調用層次很深,這個函數傳回後,幾乎大部分工作都完成了。這個函

30

數在.../pmon/common/main.c 中,好,下面進入dbginit 函數。

首先調用__init(),這個__init 就是對__ctors 這個函數的調用的包裝,__ctors 函

數的的定義如下:

static void

__ctors()

{

void (**p)(void) = __CTOR_LIST__ + 1;

while (*p)

(**p++)();

}

看得出來,這個函數就是挨個執行初始化清單上的函數。.__CTOR_LIST__是在

/Targets/Bonito/conf/ld.script 這個檔案中定義的,其中第一個字表示這個構造列

表的大小,這個清單的内容相當于c++中的構造函數。

.ctors :

{

__CTOR_LIST__ = .;

LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)

*(.ctors)

LONG(0)

__CTOR_END__ = .;

}

.dtors :

{

__DTOR_LIST__ = .;

LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)

*(.dtors)

LONG(0)

__DTOR_END__ = .;

}

ctors 的内容是c 代碼中所有包含constructor 關鍵字屬性的函數,constructor

是gcc 中對c 的擴充,我認為隻要函數聲明中有__attribute__ ((constructor)),

這個函數的位址就會被放在構造清單中,就會在main 函數開始前被調用。由于

這個東西我也不了解,暫時就先按自己的想法了解吧。

标有__attribute__ ((constructor))屬性的函數有多少呢?readelf -S pmon 一把。

There are 10 section headers, starting at offset 0xd3940:

Section Headers:

[Nr] Name Type Addr Off Size ES Flg Lk

Inf Al

[ 0] NULL 00000000 000000 000000 00 0

0 0

31

[ 1] .text PROGBITS 80010000 000400 0b4ea8 00 WAX 0

0 512

[ 2] .data PROGBITS 800c4ea8 0b52a8 017800 00 WA 0

0 8

[ 3] data PROGBITS 800dc800 0ccc00 006c00 00 WA 0

0 1024

[ 4] .ctors PROGBITS 800e3400 0d3800 0000f4 00 WA 0

0 8

[ 5] .dtors PROGBITS 800e34f4 0d38f4 000008 00 WA 0

0 1

看到了吧,這個ctors 節區大小為f4,也就是61 個字,除了首位兩個(首是個數,

尾是0 标記),真正的最多是59 個函數。實際上通過列印一共有53 個有效的函

數。其餘是對齊用了???

這53 個函數從性質上可分為3 類:指令初始化,檔案系統初始化,可執行檔案

類型初始化。

通過cscope 的顯示明顯可以看出,指令初始化的數量最多。這些函數都在../

pmon/cmds/的檔案中定義,幾乎是一個指令一個檔案。每個檔案的結構基本一

緻,找兩個最熟悉的指令為例分析吧,load,g。

load 指令初始化在cmds 目錄下的的load.c 檔案中。

這個檔案的基本是由這麼幾個部分(基于空間的原因,略作修改):

const Optdesc cmd_nload_opts[] ={....} // help 時的輸出内容

static int nload (int argc, char **argv) {} //指令的具體執行

Int cmd_nload (int argc, char **argv){} //

static const Cmd Cmds[] ={} //這個指令所在組的資訊

static void init_cmd() {} //指令的注冊

g 指令在cmds 目錄下的cmd_go.c 檔案中。

這個檔案的由以下幾個部分組成:

const Optdesc cmd_g_opts[] = {} //help 時的輸出

Int cmd_go (int ac,char ** av){} //指令的具體執行

static const Cmd DebugCmd[] ={} //指令所在組的資訊

static void init_cmd()={} //指令的注冊

下面就具體分析load 這個指令初始化的全過程。

首先執行init_cmd 這個函數,

static void init_cmd __P((void)) __attribute__ ((constructor));

static void init_cmd()

{

cmdlist_expand(Cmds, 1);

}

這個__P 不知是什麼原因,沒有實際作用。

Cmds 是個Cmd 結構的數組,Cmd 數組的定義如下:

32

typedef struct Cmd {

const char *name;

const char *opts;

const Optdesc *optdesc;

const char *desc;

int (*func) __P((int, char *[]));

int minac;

int maxac;

int flag;

#define CMD_REPEAT 1

#define CMD_HIDE 2

#define CMD_ALIAS 4

} Cmd;

Load.c 中這個Cmds 定義如下:

static const Cmd Cmds[] =

{

{"Boot and Load"},

{"load", "[-beastifr][-o offs]",

cmd_nload_opts,

"load file",

cmd_nload, 1, 16, 0},

{0, 0}

};

上面的數組是這麼了解的。第一項表示這個指令所屬的組是”Boot and Load “,

第二項是主角,對照Cmd 的定義,分别表示這個指令是load,可選的參數有

哪些,可選參數的解釋,對指令的解釋,指令執行的函數,最少參數個數,最多

參數個數,這個指令的類型(flag 定義解釋得比較清楚)。

現在來看看這個cmdlist_expand(Cmds,1)的作用。一看比較簡單吧,就是把

Cmdlist 的目前未使用的元素位址指向傳入的Cmds 這個數組的位址就行了。

由于Cmdlist 數組的大小為100,最多隻能添加100 次(當然一個Cmds 中

可以包含多個指令)。g 這個指令的注冊也類似,就是填充Cmdlist 這個數組。

再看檔案系統的初始化。

pmon 基本內建了最常用的檔案系統,包括ext2,fat,iso9660 等。這些檔案系

統初始化都在../pmon/fs/的下的檔案中定義。下面以我們最熟悉的ext2 為

例介紹檔案系統的初始化。

這個ext2 檔案系統實作了ext2 檔案系統功能的一個子集,包含了十幾個函

數。實作了ext2 檔案系統的基本讀寫功能。

static DiskFileSystem diskfile = {

"ext2",

ext2_open,

ext2_read,

ext2_write,

33

ext2_lseek,

is_ext2fs,

ext2_close,

NULL,

ext2_open_dir,

};

static void init_diskfs(void) __attribute__ ((constructor));

static void init_diskfs()

{

diskfs_init(&diskfile);

}

diskfs_init($diskfile)函數完成檔案系統的注冊,傳入的參數diskfile如前面定義。

diskfs_init()函數在../pmon/fs/diskfs.c 檔案中定義。

int diskfs_init(DiskFileSystem *dfs)

{

SLIST_INSERT_HEAD(&DiskFileSystems,dfs,i_next);

return (0);

}

這個就是調用SLIST_INSERT_HEAD,其中DiskFileSystems 這個參數是鍊

表頭,這個宏定義如下:

#define SLIST_INSERT_HEAD(head, elm, field) do { \

(elm)->field.sle_next = (head)->slh_first; \

(head)->slh_first = (elm); \

} while (0)

就是些連結清單的常見操作,不具體叙述。

還有一類的初始化是針對檔案類型的,分别有bin,elf,txt 等。下面以最常見的

bin格式為例介紹初始化,這些檔案格式的初始化代碼在../pmon/loaders/目錄下。

bin 格式的初始化在檔案exec_bin.c 中,這個檔案大緻有這麼三個部分:

static long load_bin (int fd, ch ar *buf, int *n, int flags){} //如何load bin檔案

static ExecType bin_exec = {} // bin 檔案格式結構體的定義

static void init_exec() {} // 注冊這種檔案格式

具體的bin 檔案格式注冊如下,

static void init_exec()

{

exec_init(&bin_exec);

}

其中bin_exec 定義為:

static ExecType bin_exec =

{

"bin", // 檔案格式的類型名

load_bin, // bin 格式檔案的load 執行過程

EXECFLAGS_NOAUTO, // flag,不自動啟動

34

};

以上是三類主要的構造函數,除此之外還有兩個函數,分别是

void init_controller_list(void) __attribute__((constructor));

static void init_kbd_driver(void) __attribute__((constructor));

都在../sys/dev/usb/的目錄下,先看第一個。在檔案usb.c 中

void init_controller_list(void)

{

TAILQ_INIT(&host_controller);

}

就是初始化host_controller 而已。

Init_kdb_driver()在usb_kdb.c 中,定義如下:

static void init_kbd_driver(void)

{

usb_driver_register(&usb_kbd_driver);

}

這個usb_kbd_driver 是個結構體,隻有兩個成員,分别是probe 連結清單資訊。

struct usb_driver usb_kbd_driver = {

.probe = usb_kbd_probe,

};

至此所有構造函數執行完畢。__init 函數傳回,回到了../pmon/common/main.c

的dbginit 函數中。

envinit的作用是環境變量的初始化。涉及的東西比較獨立,我們在後面專門講述,

這裡跳過。

環境變量設定完畢後回到dgbinit()。接着執行

s = getenv("autopower");

if (s == 0 || strcmp(s, "1") != 0) {

suppress_auto_start();

}

如果autopower 定義了這個變量,傳回這個變量的值,否則傳回0。執行

suppress_auto_start(),suppress 有壓制,制止的意思。可見就是不許自動啟

動了,這裡的自動啟動指的是上電後就啟動啟動進入系統。

void suppress_auto_start(void)

{

CMOS_WRITE(0x80, 0xd);

}

static inline void CMOS_WRITE(unsigned char val, unsigned char addr)

{

linux_outb_p(addr, 0x70);

linux_outb_p(val, 0x71);

35

}

這個suppress_auto_start()有點意思,居然往實時鐘寫個什麼東西。

cs5536_devinit

好了,接着執行tgt_devinit();

這個函數首先執行cs5536_init(),看名字就知道是對南橋的初始化。

這個函數依次調用以下四個函數。

cs5536_vsm_init();

cs5536_i8259_init();

cs5536_ide_init();

cs5536_ohci_init();

先看cs5536_vsm_init(),代碼中的那3 個PCI_SPECIAL_*都是沒有定義的,實

際cs5536_vsm_init()要執行的代碼如下:

_rdmsr(SB_MSR_REG(SB_ERROR), &hi, &lo);

lo |= SB_MAR_ERR_EN | SB_TAR_ERR_EN | SB_TAS_ERR_EN;

//具體見AMD 南橋手冊226 頁

_wrmsr(SB_MSR_REG(SB_ERROR), hi, lo);

_rdmsr(DIVIL_MSR_REG(DIVIL_LEG_IO), &hi, &lo);//見AMD手冊363頁

lo |= (1 << 28); //選擇關機的信号

_wrmsr(DIVIL_MSR_REG(DIVIL_LEG_IO), hi, lo);

_rdmsr(DIVIL_MSR_REG(DIVIL_BALL_OPTS), &hi, &lo);

lo |= 0x01; //表示IDE 引腳接IDE 控制器,具體見AMD 手冊365 頁

_wrmsr(DIVIL_MSR_REG(DIVIL_BALL_OPTS), hi, lo);

這裡的vsm(Virtual Support Module)

這是pmon 執行過程中第二次正式接觸南橋,第一次是在start.S 中。上面的代

碼就是三次多寄存器的讀寫。功能分别是重置錯誤位,确定關機的信号,表示ide

引腳都連接配接ide 控制器。

接下來是cs5536_i8259_init();可以看出來是中斷處理晶片的初始化。跳過不叙。

接下來是cs5536_ide_init(),實際執行代碼如下:

_rdmsr(DIVIL_MSR_REG(DIVIL_BALL_OPTS), &hi, &lo);

lo |= 0x01;//IDE 引腳接IDE 控制器,具體見AMD 手冊365 頁

_wrmsr(DIVIL_MSR_REG(DIVIL_BALL_OPTS), hi, lo);

作者健忘,上面剛做了一次。

_rdmsr(SB_MSR_REG(SB_CTRL), &hi, &lo);

lo &= ~( (1<<13) | (1 << 14) );

_wrmsr(SB_MSR_REG(SB_CTRL), hi, lo);

這個代碼指定主從兩個IDE 控制器的位址,具體見AMD 手冊229 頁。

36

_rdmsr(IDE_MSR_REG(IDE_CFG), &hi, &lo);

lo |= (1 << 1);

_wrmsr(IDE_MSR_REG(IDE_CFG), hi, lo);

使能IDE 控制器,具體見AMD 手冊P336

_rdmsr(IDE_MSR_REG(IDE_DTC), &hi, &lo);

lo &= 0x0;

lo |= 0x98980000; //PIO-0 mode for 300ns IO-R/W

_wrmsr(IDE_MSR_REG(IDE_DTC), hi, lo);

不同模式下(多種pio 和dma 模式)的有效脈沖寬度和最小恢複時間的設定,

具體見AMD 手冊P337

_rdmsr(IDE_MSR_REG(IDE_CAST), &hi, &lo);

lo &= 0x0;

lo |= 0x990000a0; //PIO-0 mode

_wrmsr(IDE_MSR_REG(IDE_CAST), hi, lo);

特定模式下指令的有效脈沖寬度和最小恢複時間設定,具體見AMD 手冊P338

hi = 0x60000000;//前3 位是011,表示針對IDE 操作

lo = 0x1f0ffff8; //ffff8 和001F0and 後是1F0,是主IDE 的I/O 起始位址

_wrmsr(GLIU_MSR_REG(GLIU_IOD_BM0), hi, lo);

具體見AMD 手冊P219

hi = 0x60000000;

lo = 0x3f6ffffe; //主IDE 還有一個I/O 位址是3F6

_wrmsr(GLIU_MSR_REG(GLIU_IOD_BM1), hi, lo);

GLIU_IOD_BM[0,1]具體位定義相同,見AMD 手冊P219

接下是ohci 的初始化,代碼很短,如下:

hi = 0x40000006;//前3 位是010,表示是USB 裝置

lo = 0x050fffff;//基位址是0x6050

_wrmsr(GLIU_MSR_REG(GLIU_P2D_BM1), hi, lo);

具體見AMD 手冊P203

_rdmsr(USB_MSR_REG(USB_OHCI), &hi, &lo);

hi = (1 << 1) | (0 << 2); // BUS MASTER ENABLE AND MEM ENABLE

lo = 0x06050000;//基位址是60500,mem 大小是256 位元組

_wrmsr(USB_MSR_REG(USB_OHCI), hi, lo);

具體見amd 手冊P266

ohci 也是usb 主要制器的三大标準之一。關于ohci 的内容,請參看相關規範。

至此,cs5536_init()執行完畢,函數傳回tgt_devinit()。

跳過與龍芯無關的兩個變量的查找。

接下來是CPU_ConfigCache();

這個函數配置程式中要使用的cache 相關參數,代碼在../pmon/arch/mips/

cache.S 中。這個代碼的許多工作其實在start.S 中已經做了,代碼看似很長,

真正執行的卻不多。一開始無非是資料/指令的一級緩存大小和cache 行大小之

類的探測和部分指派,start.S 中這個工作都做了,隻是沒有儲存這些值而已。

37

直接轉到實際部分,在讀prid 知道是龍芯2 号之後,跳到ConfGodson2,可以

看到ConfGodson2 開始的一些代碼都被注釋了,都是和三級緩存相關的代碼,

實際上ConfGodson2 開始的那兩句也是沒有用的。到了ConfGodsonL2。設定

二級緩存大小後,就到了ConfResult,就是些變量的cache 相關設定了。

剩下來tgt_devinit()還剩下要執行兩個函數: _pci_businit(1), cs5536_

pci_fixup().。

_pci_businit

接下來執行_pci_businit(1)。從函數名上看功能是pci 總線的初始化,實際包括

pci 裝置的探詢并初始化為一個裝置連結清單,配置設定相應的資源(io 和mem 請求)。

傳入的參數1 表示要初始化。_pci_businit 這個函數按照枚舉的方式掃描總線,

并初始化相應的結構,代碼在../sys/dev/pci/pciconfi.c 中。

跳過列印相關的語句,下面執行:

init = _pci_hwinit (init, &def_bus_iot, &def_bus_memt);

pci_roots = init;//正常的傳回值為1,pci_roots 表示root 的個數

if (init < 1)

return;

重點是_pci_hwinit (init, &def_bus_iot, &def_bus_memt),這個init 就是傳入的

參數1,表示開始初始化,其餘兩個參數一個是io 的空間描述,一個是mem 的

空間描述, 都是tgt_bus_space 的結構體指針,不過在函數中都未使用。

struct tgt_bus_space {

u_int32_t bus_base;

u_int32_t bus_reverse;

};

bus_reverse 不知道是什麼。下面進入_pci_hwinit()。這個的執行代碼較長,但

基本都是指派而已。執行語句如下:

pci_local_mem_pci_base = PCI_LOCAL_MEM_PCI_BASE;

這個值是0x8000 0000,就是說pci 的mem 位址配置設定從這個位址開始,前面都是

使用者空間。

pd = pmalloc(sizeof(struct pci_device));

pb = pmalloc(sizeof(struct pci_bus));

if(pd == NULL || pb == NULL) {

printf("pci: can't alloc memory. pci not initialized\n");

return(-1);

}

配置設定pci_device 和pci_bus 這兩個結構體。可見分别描述了裝置和總線内容。

實際上用來描述北橋(或者說是一個pci 裝置的總抽象)和唯一的pci 總線。

pd->pa.pa_flags = PCI_FLAGS_IO_ENABLED |

PCI_FLAGS_MEM_ENABLED;

38

pd->pa.pa_iot = pmalloc(sizeof(bus_space_tag_t));//這裡不檢查傳回值?

pd->pa.pa_iot->bus_reverse = 1;

pd->pa.pa_iot->bus_base = BONITO_PCIIO_BASE_VA;

這個值為0xbfd0 0000,所有io 端口都在bfd00000 開始的1MB 空間内。

pd->pa.pa_memt = pmalloc(sizeof(bus_space_tag_t));

pd->pa.pa_memt->bus_reverse = 1;

pd->pa.pa_memt->bus_base = 0xb0000000;

pd->pa.pa_dmat = &bus_dmamap_tag;

以上代碼都是對pci_device 中的pa 的成員指派,pa 是一個pci_attach_args 結

構,描述pci 裝置的一些附加屬性。

pd->bridge.secbus = pb;

表示這個裝置所接的橋的下一級總線。pd->bridge之前沒指派,pmalloc 傳回的内

容是清0 的

_pci_head = pd;

這個_pci_head 全局變量很重要,以後會多次用到。

接下來的代碼根據是否按照新的pci 視窗布局執行不同的代碼,舊的布局是3 個

64MB 的pci 視窗,新的是128,32,32。pci 視窗的目的就是把cpu 送出的地

址轉化成總線位址。pmon預設代碼中沒有定義NEW_PCI_WINDOW。小技巧:

看條件編譯的條件是否定義有個簡單的方法,加條件編譯的部分下一條要報錯的

代碼,如果報了,說明這段代碼要被編譯,條件為真。

以上是對北橋的概括描述,現在要描述PCI 總線了。執行的代碼如下:

pb->minpcimemaddr = PCI_MEM_SPACE_PCI_BASE+0x04000000;

pb->nextpcimemaddr = PCI_MEM_SPACE_PCI_BASE+

BONITO_PCILO_SIZE;

注釋說前64M(0x04000000)的pci 視窗保留給vga。BONITO_PCILO_SIZE 為

192MB, PCI_MEM_SPACE_PCI_BASE 為0 ???

BONITO_PCIMAP =

BONITO_PCIMAP_WIN(0, 0x00000000) |

BONITO_PCIMAP_WIN(1, PCI_MEM_SPACE_PCI_BASE+0x04000000) |

BONITO_PCIMAP_WIN(2, PCI_MEM_SPACE_PCI_BASE+0x08000000) |

BONITO_PCIMAP_PCIMAP_2;

這個PCIMAP 寄存器設定了CPU-->PCI 的位址轉換。以下引自《pci 裝置初始

化.pdf》:為了讓CPU 通路PCI 空間,需要将CPU 空間映射到PCI 空間。在内

存空間256M 上有三個連續的大小均為64M 的區間,分别稱為PCI_Lo0,

PCI_Lo1,PCI_Lo2。這三個區間可以被北橋映射到PCI 以64M對齊的任意位置。

映射的關系通過設定Bonito 的PCIMAP 寄存器。該寄存器的格式如下圖。

39

pci_lo0,pci_lo1,pci_lo2 分别是上面所說三個區間的高6 位位址(bit3126),

Pci_map2 是說明映射到2G 以上的空間還是2G 以下的空間。是以上面給

BONITO_PCIMAP 指派就将PCI_lo0,PCI_lo1,PCI_lo2 分别映射到了PCI 空間

的從0,64M,128M 開始的位址。

以上的說法中對于pci_map2 的描述部分在2F 中是不适用的,2F 的這個寄存器

的高14 位保留。低18 位定義相同。

pb->minpciioaddr = PCI_IO_SPACE_BASE+0x000b000;

pb->nextpciioaddr = PCI_IO_SPACE_BASE+ BONITO_PCIIO_SIZE;

傳統的16 位位址的io 空間,大小是64KB

pb->pci_mem_base = BONITO_PCILO_BASE_VA;//0xb000 0000

pb->pci_io_base = BONITO_PCIIO_BASE_VA;//0xbfd0 0000

這兩個base 很重要,前面的minpciioaddr 和nextpciioaddr 都要和base 聯系。

pb->max_lat = 255;

pb->fast_b2b = 1;

pb->prefetch = 1;

pb->bandwidth = 0x4000000;

pb->ndev = 1;

_pci_bushead = pb;

這個_pci_bushead 全局變量很重要,以後會多次用到。在我們的龍芯pc 中,尚

未采用多pci 架構,是以pcibus 都是一個。

_pci_bus[_max_pci_bus++] = pd;

bus_dmamap_tag._dmamap_offs = 0;

下面的pcibase 是指北橋PCI 配置頭中的base

BONITO_PCIBASE0 = 0x80000000;

BONITO_PCIBASE1 = 0x0;

BONITO_PCIBASE2 = 0x70000000;

BONITO_PCI_REG(0x40) = 0x80000000;//base 0 的mask

BONITO_PCI_REG(0x44) = 0xf0000000;//base 1 的mask

BONITO_PCIMEMBASECFG = 0;

_pci_hwinit 執行完以後傳回_pci_businit(),執行如下代碼:

40

struct pci_device *pb;//這個變量命名不合适,應為pd

for(i = 0, pb = _pci_head; i < pci_roots; i++, pb = pb->next) {

這裡的_pci_head 就是剛才在_pci_hwinit 中指派的。pci_roots 是1。也就是這

個for 循環的循環體實際上隻執行一次。

_pci_scan_dev(pb, i, 0, init);

}

_pci_scan_dev(pb, i, 0, init)中i 表示總線号(其實就是0),0 表示device 号從0

開始查找。

_pci_scan_dev()的内容如下:

for(; device < 32; device++) {

_pci_query_dev (dev, bus, device, initialise);

}

這個代碼功能比較清楚,就是以枚舉的方式探測相應的裝置是否存在,傳入參數

分别是北橋的裝置指針,北橋所在的總線号0,要探測的裝置号device(0~31)。

_pci_query_dev()代碼如下:。

static void

_pci_query_dev (struct pci_device *dev, int bus, int device, int initialise)

{

pcitag_t tag;

pcireg_t id;

pcireg_t misc;

tag = _pci_make_tag(bus, device, 0);

在pci 裝置空間通路的時候,所使用的位址由四部分組成。總線号,裝置号,功

能号,寄存器号。_pci_make_tag 的工作就是把這總線号,裝置号,功能号三個

數合成一個總線配置空間位址的辨別,其中的參數0 表示功能号。對于多功能設

備,不同功能的總線号和裝置号是相同的,且0 号功能必定存在,就是說多功能

的裝置功能号是從0 開始的。是以探測0 号功能是非常合理的。

if (!_pci_canscan (tag))

return;

bus=0 且device=0 的話_pc_canscan()會傳回0,表示是北橋,是特殊的。

id = _pci_conf_read(tag, PCI_ID_REG);

讀PCI 配置空間中的廠商ID 看看這個裝置是否存在。

if (id == 0 || id == 0xffffffff) {//表示沒有裝置

return;

}

misc = _pci_conf_read(tag, PCI_BHLC_REG);

檢查是否是多功能裝置

if (PCI_HDRTYPE_MULTIFN(misc)) {

如果是多功能裝置,所有功能都加到裝置(确切的說是功能清單中去)清單中去。

int function;

for (function = 0; function < 8; function++) {

41

tag = _pci_make_tag(bus, device, function);

id = _pci_conf_read(tag, PCI_ID_REG);

if (id == 0 || id == 0xffffffff) {

continue;

}

_pci_query_dev_func (dev, tag, initialise);

添加到裝置(功能)清單中

}

}

else {

_pci_query_dev_func (dev, tag, initialise);

挂上裝置清單并記下空間請求

}

_pci_query_dev 函數就是這些了。我們看看_pci_query_dev_func 的實作。

_pci_query_dev_func()的主要工作就是初始化這個探測到的pci 裝置(功能),

加入到pci 裝置(功能)連結清單,記錄各個裝置(功能)的資源請求主要是io 空間

和mem 空間的請求(滿足請求不在這個函數中),并相應調整pci 總線的特征。

一開始是申請一個pci_device 的結構體并初始化它。值得注意的是,這個新設

備的父親是dev,也就是北橋。

_pci_device_insert(dev, pd);

_pci_device_insert(dev, pd)将pd 插入dev->bridge->child 中。bus 上的所有設

備都在這個child 清單中,也就是說所有pci 裝置(功能)都是總線的兒子(不

包括北橋)。接下來是進一步初始化裝置和因這個裝置調整總線的性質。

到了_pci_setupIntRouting(pd);

看名字是設定中斷線有關,不懂,不予解釋。

stat = _pci_conf_read(tag, PCI_COMMAND_STATUS_REG);//讀狀态寄

存器,準備使能

stat &= ~(PCI_COMMAND_MASTER_ENABLE |

PCI_COMMAND_IO_ENABLE |

PCI_COMMAND_MEM_ENABLE);

_pci_conf_write(tag, PCI_COMMAND_STATUS_REG, stat);

pd->stat = stat;

if ((stat & PCI_STATUS_BACKTOBACK_SUPPORT) == 0) {

pb->fast_b2b = 0;

}

backtoback 就是背靠背,就是連續傳送,有一個裝置不支援,總線的就标志不

支援

if ((stat & PCI_STATUS_66MHZ_SUPPORT) == 0) {

pb->freq66 = 0;

42

}

一個裝置不支援66MHz 就會導緻整個總線不能使用66M 的頻率。

x = stat & PCI_STATUS_DEVSEL_MASK;

if (x > pb->devsel) {

pb->devsel = x;

}

這個見pci2.3 規範P200,x=0 表示快速裝置,1 表示中等速度裝置,2 表示慢

速裝置。

bparam = _pci_conf_read(tag, (PCI_MINGNT & ~0x3));

pd->min_gnt = 0xff & (bparam >> ((PCI_MINGNT & 3) * 8));

bparam = _pci_conf_read(tag, (PCI_MAXLAT & ~0x3));

pd->max_lat = 0xff & (bparam >> ((PCI_MAXLAT & 3) * 8));

if (pd->min_gnt != 0 || pd->max_lat != 0) {//為0 表示沒有要求

if (pd->min_gnt != 0 && pd->min_gnt > pb->min_gnt) {

pb->min_gnt = pd->min_gnt;

}

if (pd->max_lat != 0 && pd->max_lat < pb->max_lat) {

pb->max_lat = pd->max_lat;

}

if (initialise) {

pb->bandwidth -= pd->min_gnt * 4000000 /

(pd->min_gnt + pd->max_lat);

}

}

上面的和總線的性質有關, pb->bandwidth -= pd->min_gnt * 4000000 / (pd

->min_gnt + pd->max_lat);應該是根據裝置的要求配置設定帶寬。

bparam = _pci_conf_read(tag, PCI_INTERRUPT_REG);

bparam &= ~(PCI_INTERRUPT_LINE_MASK <<

PCI_INTERRUPT_LINE_SHIFT);

bparam |= ((_pci_getIntRouting(pd) & 0xff) <<

PCI_INTERRUPT_LINE_SHIFT);

_pci_conf_write(tag, PCI_INTERRUPT_REG, bparam);

表示裝置要求的interrupe pin 連接配接到中>斷控制器的哪個引腳上

接下來要判斷這個裝置(功能)的類型,分為三類:pci 橋,IDE 控制器,其它。

43

在2F 的盒子上,除了北橋外沒有pci 橋,但北橋不調用_pci_query_dev_fun,

IDE 的控制器內建在南橋中了。也就是說,所有的裝置(功能)都會從最後那條

路走。一開始是一個大循環,

for (reg = PCI_MAPREG_START; reg < PCI_MAPREG_END; reg += 4) {

讀可能存在的6 個base 寄存器,看來要配置設定io 空間和mem 空間了。

old = _pci_conf_read(tag, reg);

_pci_conf_write(tag, reg, 0xffffffff);

mask = _pci_conf_read(tag, reg);

_pci_conf_write(tag, reg, old);

上面的代碼看起來有些怪,這是讀這幾個寄存器的特殊之處,具體的解釋見

pci2.3 規範的P208。

如果要申請空間,判斷是io 空間還是mem 空間,讀出的mack 末位為1 表示要

i/o 空間,否則是mem 空間。

如果是io 空間的申請,則:

mask |= 0xffff0000;

io 空間的大小是受限的,mask1 的位數表明申請空間的大小。

pm = pmalloc(sizeof(struct pci_win));

f(pm == NULL) {

。。。。。。。

}

現在開始記下空間需求

pm->device = pd;

pm->reg = reg; //配置空間的基位址

pm->flags = PCI_MAPREG_TYPE_IO;

pm->size = -(PCI_MAPREG_IO_ADDR(mask));

_insertsort_window(&pd->parent->bridge.iospace, pm);

讓這個總線記下這個裝置的io 空間需求。

下面是對于mem 空間需求的處理:

switch (PCI_MAPREG_MEM_TYPE(mask)) {

case PCI_MAPREG_MEM_TYPE_32BIT:

case PCI_MAPREG_MEM_TYPE_32BIT_1M://這個是保留的,怎麼

break;

case PCI_MAPREG_MEM_TYPE_64BIT:

_pci_conf_write(tag, reg + 4, 0x0);

skipnext = 1;//兩個32 位當一個,接下的那個32 位也搞了

break;

default:

_pci_tagprintf (tag, "reserved mapping type 0x%x\n",

PCI_MAPREG_MEM_TYPE(mask));

continue;

}

44

可見對于mem 空間的需求和io 空間有很大的不同。mem 空間是可以支援64

位位址的,而io 空間不行,64 位的時候就要占用兩個base-address 寄存器。

skipnext 的作用就是标記是不是兩個32 位位址合成64 位。

if (!PCI_MAPREG_MEM_PREFETCHABLE(mask)) {

pb->prefetch = 0;

}

預取。接下來的代碼和io 空間的申請相同。下面來看看_insertsort_window 到底

幹了什麼。

pm1 = (struct pci_win *)pm_list;

while((pm2 = pm1->next)) {

if(pm->size >= pm2->size) {//空間大的被通路幾率高?不見得

break;

}

pm1 = pm2;

}

pm1->next = pm;

pm->next = pm2;

就是看按照申請空間的大小把這個pci_win 結構排序,從大到小排序。

我們已經記錄了一個裝置(功能)的io 和mem 請求,如果它還需要額外的擴充

rom,就再記錄并送出申請。具體可見pci2.3 規範30h 寄存器的解釋。

OK,至此,_pci_query_dev_func 函數執行完畢。也就是對一個檢測到的裝置

(功能)結構體的初始化,插入pci 裝置連結清單并初步處理了這個裝置(功能)提

出的需求。

在所有的裝置(功能)都執行_pci_query_dev 以後,_pci_businit()要執行的隻

有_setup_pcibuses(init)了。這個函數功能是配置設定帶寬并處理空間申請。

一開始配置設定帶寬。代碼中有一句def_ltim = pb->bandwidth / pb->ndev,表示平

均主義的話每個裝置多少帶寬,但是我沒有找到這個ndev 初始化後在哪兒被改

變了。然後處理空間的請求。

for(i = 0, pd = _pci_head; i < pci_roots; i++, pd = pd->next) {

_pci_setup_windows (pd);

}

處理mem/io 空間請求,并回報回請求的裝置。io 和mem 空間的請求已經以

pci_win 結構體連結清單的形式記錄下來了,mem 的請求記錄在bridge.memspace

中,io 請求在bridge.iospace 中。先處理的是mem 的請求。主要就是

pm->address = _pci_allocate_mem (dev, pm->size);

實作很顯然,就是按順序切割剩餘的空間。

address = (dev->bridge.secbus->minpcimemaddr + size - 1)& ~(size - 1);

address1 = address + size;

if (address1 > dev->bridge.secbus->nextpcimemaddr ||

address1 < dev->bridge.secbus->minpcimemaddr) {

return(-1);

}

45

dev->bridge.secbus->minpcimemaddr = address1;

return(address);

這個代碼的功能應該是很清楚了。pm->address 在得到配置設定到的空間的基位址

後,還要寫回那個基位址寄存器。下面的代碼就是這個目的。

_pci_conf_write(pd->pa.pa_tag, pm->reg, memory);

對于mem 空間的申請就是這樣了。剩下的擴充空間和io 空間過程相似。

cs5536_pci_fixup

回到tgt_devinit(),隻剩下一個cs5536_pci_fixup()了。

這個函數實際上執行兩個函數cs5536_pci_otg_fixup() 和cs5536_lpc_fixup();

先看前一個。代碼如下:

tag = _pci_make_tag(0, 14, 7);//是5536 上的USB controller

base = _pci_conf_read(tag, 0x10);

base |= 0xb0000000; //不清楚為什麼

前頭setup_pcibuses 函數已經完成了空間需求,這裡的位址是實際配置設定到的。

cmdsts = _pci_conf_read(tag, 0x04);

_pci_conf_write(tag, 0x04, cmdsts | 0x02);

使能mem space access,接下來的幾條指令就要執行寫操作

val = *(volatile u32 *)(base + 0x04);

val &= ~(3 << 0);

val |= (2 << 0);

*(volatile u32 *)(base + 0x04) = val;

val = *(volatile u32 *)(base + 0x0c);

if( (val & 0x03) == 0x02 ){

printf("cs5536 otg configured for USB HOST CONTROLLER.\n");

}else if( (val & 0x03) == 0x03 ){

printf("cs5536 otg configured for USB DEVICE.\n");

}else{

printf("cs5536 otg configured for NONE.\n");

}

_pci_conf_write(tag, 0x04, cmdsts);

這個具體的作用要看南橋關于usb controller 部分的描述。

接下是cs5536_lpc_fixup 函數,代碼如下:

46

_rdmsr(DIVIL_MSR_REG(PIC_IRQM_PRIM), &hi, &lo);//見amd 手冊382

lo &= ~((1 << 1) | (1 << 12));

_wrmsr(DIVIL_MSR_REG(PIC_IRQM_PRIM), hi, lo);

_rdmsr(DIVIL_MSR_REG(PIC_IRQM_LPC), &hi, &lo);

lo |= ((1 << 1) | (1 << 12));

_wrmsr(DIVIL_MSR_REG(PIC_IRQM_LPC), hi, lo);

使能鍵盤和二級IDE 中斷,見amd 手冊P384。

_rdmsr(DIVIL_MSR_REG(LPC_SIRQ), &hi, &lo);

lo |= (1 << 7);

_wrmsr(DIVIL_MSR_REG(LPC_SIRQ), hi, lo);

enable serial interrupt,見amd 手冊P466。

好了,至此,tgt_devinit()就全部執行完畢了。傳回dbginit()。

Init_net

接下來要執行init_net (1),這個函數極度混亂,絕不是初始化網絡這麼簡單。我

在第一次看pmon 代碼的時候,這個函數斷斷續續看了有半個月。

一開始是和loglevel 相關的,列印相關的,跳過。

接着執行paraminit();從調用前的注釋看是初始化系統相關的全局參數。代碼不

長,就以下7 行:

hz = HZ; //HZ 表示1 秒鐘時鐘中斷的産生次數

tick = 1000000 / HZ;//兩次時鐘中斷中間的us 數

tz.tz_minuteswest = TIMEZONE;//看來在pmon 中,這些東西不講究

tz.tz_dsttime = DST;

#define DST_NONE 0

#define DST_USA 1

#define DST_AUST 2

#define DST_WET 3

#define DST_MET 4

#define DST_EET 5

#define DST_CAN 6

這裡的TIMEZONE 和DST 在檔案頭都定義為0,就是0 時區,DST_NONE 了。

DST 的解釋如下(摘自網絡):美國加拿大實行DST 的時間是3 月的第二個

星期天早晨兩點開始到11 月的第一個星期日的早晨兩點。三月第二個個星期日

早晨兩點所有時鐘向前回撥一個小時,到11 月DST 截止再撥回來。

ncallout = 16 + NPROC; //程序數,pmon 中有無程序概念的必要?

47

nmbclusters = NMBCLUSTERS;

maxfiles = NDFILE * NPROC;

這三個全局變量作用有限,用到的時候再分析,留個印象先。

從paraminit 傳回。執行vminit();

談到記憶體配置設定,前面我們在多個地方見到了動态配置設定函數pmalloc,下面先講講

這個函數是怎麼個流程。pmalloc 代碼如下:

void * pmalloc(size_t nbytes)

{

void *p;

p = malloc(nbytes);

if(p) {

bzero(p, nbytes);

}

return(p);

}

可見這函數有配置設定和清零兩部分。我們隻關心的malloc 函數代碼如下藍色标記。

void * malloc(size_t nbytes)

{

HEADER *p, *q;

unsigned nunits;

nunits = (nbytes + sizeof (HEADER) - 1) / sizeof (HEADER) + 1;

這裡的HEADER 就是header,定義如下:

union header {

struct {

union header *ptr;

unsigned size;

} s;

ALIGN x;

};

本質上就是一個指向本聯合體的指針,和一塊空間的大小。從上一句代碼中可以

看出,每申請一塊空間,要同時要帶一個結構頭。

if ((q = allocp) == NULL) {

base.s.ptr = allocp = q = &base;

base.s.size = 0;

}

這裡是這樣的,allocp 是全局變量,如果沒有明确的初始化,全局變量就預設為

0,就是這裡的NULL。上面對應的是第一次執行的時候的情形,allocp 的含義是

目前可被使用的空間的描述資訊。

base 也是一個沒有初始化的全局變量。這裡算是初始化了一下。

for (p = q->s.ptr;; q = p, p = p->s.ptr) {

if (p->s.size >= nunits) {

if (p->s.size == nunits)

48

q->s.ptr = p->s.ptr;

else {

p->s.size -= nunits;

p += p->s.size;

p->s.size = nunits;

}

allocp = q;

return ((char *)(p + 1));

}

這個if 對應的是目前擁有的空間就能滿足申請的情形,

if (p == allocp)

if ((p = morecore (nunits)) == NULL) {

return (NULL);

}

這個對應現有空間不足的情形,調用morecore 函數再申請,配置設定成功後再在for

循環内判斷是否已經滿足要求了。函數代碼如下:

rnu = NALLOC * ((nu + NALLOC - 1) / NALLOC);

NALLOC 定義為128,表示每次申請的大小至少為128 個結構體的大小。

cp = sbrk (rnu * sizeof (HEADER));

從命名上就看出了要動真格的了。sbrk 代碼如下(綠色):

if (allocp1 + n <= allocbuf + ALLOCSIZE) {//大小如上定義,是32KB

allocp1 += n; //allocp1 表示目前可配置設定的空間首位址

return (allocp1 - n);

} else

return (NULL);

allocbuf 指向一個大數組,為32KB 大小。sbrk 就是從開始位置切割而已。

if ((int)cp == NULL)

return (NULL);

up = (HEADER *) cp;

up->s.size = rnu;

free ((char *)(up + 1));

return (allocp);

根據現狀更新這塊配置設定到的空間頭部的記錄塊。

看完了代碼,這個代碼就不覺得怪了。在pmalloc 能使用的32KB 空間内,每次

配置設定的空間其實包括header,申請的空間,對齊用的空間三部分。且這塊空間

至少是NALLOC 個header 個結構體大小的空間。是否浪費?不。因為這塊空間

還會被分割的。具體的可模拟不同情形把代碼走一遍。

header 所需空間對齊用

49

pmalloc 就到這裡了,回到流程上來,看vminit()函數。就一個if 語句,如下:

if (!kmem) { //這個全局變量kmem 沒有初始化,就是0

if (memorysize < VM_KMEM_SIZE * 2) {

panic ("not enough memory for network");

}

這裡的memorysize 是個外部變量,就是initmips 中的那個,最大為256MB,

盒子上面記憶體為512MB,memorysize變量就為256*2^20。VM_KMEM_SIZE 大

小為512KB。

memorysize = (memorysize - VM_KMEM_SIZE) & ~PGOFSET;

不足頁的不可用,且空出一個vm,memorysize 機關是位元組

if ((u_int32_t)&kmem < (u_int32_t)UNCACHED_MEMORY_ADDR) {

這個判斷沒有理由不成立,還要執行的就一條了,就不管else 了。

kmem = (u_char *) PHYS_TO_CACHED (memorysize);

事實上我認為kmem 的值是确定的,memorysize 是256MB-512KB,那麼

kmem 就是0x8ff8 0000(uncached 前是0x0ff80000)。vminit()至此執行完畢。

函數設定的kmem 變量值指定了待會kmem_malloc 配置設定時使用的空間。

接着執行kmeminit()函數。實際就執行以下幾句:

int npg;

npg = VM_KMEM_SIZE/ NBPG;

npg(number of page)計算一個VM_KMEM_SIZE 的頁數,頁大小為4KB。

kmemusage = (struct kmemusage *) kmem_alloc(kernel_map,

(vsize_t)(npg * sizeof(struct kmemusage)));

我們又看到一個配置設定函數,kmem_alloc(),有兩個傳入參數。第一個參數

kernel_map,這個變量實際上是沒用的,不去理睬。第二個是要申請的空間大小,

從這個大小就可猜出,每一個頁都有一個控制結構kmemusage,下面看看這個

結構體的定義:

struct kmemusage {

short ku_indx;

union {

u_short freecnt;

u_short pagecnt;

} ku_un;

};

ku_indx 料想就是一個下标,ku_un 從注釋中看出是對一塊區域的描述。

kmem_alloc(map,size)實際上就是調用kmem_malloc(map,size,0), 下面看

kmem_malloc 做了點什麼。代碼如下(藍色标記):

size = (size + PGOFSET) & ~PGOFSET;

每次配置設定的是4KB 對齊。

if (kmem_offs + size > VM_KMEM_SIZE) { //沒有足夠空間了,報錯傳回。

。。。。。

}

p = (vm_offset_t) &kmem[kmem_offs];

50

kmem_offs += size;

return p;

這個配置設定的政策很簡單,就是從kmem 開始配置設定。kmem_offs 表示目前可供分

配空間的起始位址。傳回值指派給kmemusage。

回到kmeminit(),隻剩下一句了。

kmem_map = kmem_suballoc(kernel_map, (vaddr_t *)&kmembase,

(vaddr_t *)&kmemlimit, (vsize_t)(npg * NBPG), FALSE);

kmem_suballoc()代碼如下:

vm_map_t kmem_suballoc (vm_map_t map, vm_offset_t *base,

vm_offset_t *lim, vm_size_t size, canwait)

{

if (size > VM_KMEM_SIZE)

panic ("kmem_suballoc");

*base = (vm_offset_t) kmem;

*lim = (vm_offset_t) kmem + VM_KMEM_SIZE;

return map;

}

經過這個調用以後,kmembase 就是kmem(一般就是0x8ff80000),kmemlimit

表示可供配置設定空間的邊界。kmem_alloc 和kmem_suballoc 到底是什麼關系呢?

不管了,至此,kmeminit()執行完畢。

回到init_net(),接着執行:

callout = malloc(sizeof(struct callout) * ncallout, M_TEMP, M_NOWAIT);

callfree = callout;

for (i = 1; i < ncallout; i++) {

callout[i-1].c_next = &callout[i];

}

先配置設定ncallout 個ncallout 結構體,ncallout 在paraminit 中已經指派。為19。

struct callout {

struct callout *c_next;

void *c_arg;

void (*c_func) __P((void *));

int c_time;

};

感覺是一個函數調用的隊列。具體的等用到時再說。

這裡的malloc 函數有3 個參數,和前面的不同。malloc 執行代碼如下(藍色):

void * malloc(unsigned long size, int type ,int flags)

{

。。。。。。

indx = BUCKETINDX(size);

kbp = &bucket[indx];

這個malloc 配置設定的空間大小必須為2 的多少次幂,傳回的indx 就是這個幂。對

51

應kmembuckets 數組的下标。kmembucket 結構體定義如下:

struct kmembuckets {

caddr_t kb_next;

caddr_t kb_last;

long kb_calls;

long kb_total;

long kb_totalfree;

long kb_elmpercl;

long kb_highwat;

long kb_couldfree;

};

看得出來,這個和核心中的malloc 的用法幾乎是一樣的。大緻如下圖

kmembuckets

數組

s = splimp();

這個spl 是’set priority level’的縮寫,在bsd 中曾經被用來保護一段代碼不被中

斷,就是産生臨界區的功能。一個bsd 的開發者曾經寫道:

when manipulating a disk I/O job queue, you might do:

s = splbio(); * block "block I/O" (disk controller) interrupts */

BUFQ_REMOVE(&sc->sc_queue, bp);

splx(s);

pmon 此處的用法其實和這個開發者的例子很相似,if 這段後面就有splx(s)。順

便提一下,pmon 中有很多來自于2000 年前後bsd 核心代碼。

if (kbp->kb_next == NULL) {

kbp->kb_last = NULL;

if (size > MAXALLOCSAVE)

allocsize = clrnd(round_page(size));

else

allocsize = 1 << indx;

npg = clrnd(btoc(allocsize)); //計算這個空間的頁數

va = (caddr_t) kmem_malloc(kmem_map, (vsize_t)ctob(npg),

!(flags & M_NOWAIT));//實際配置設定空間

if (va == NULL) { // 空間不足了

。。。。。

52

}

kup = btokup(va);//計算這個位址對應的kmemusage 結構

kup->ku_indx = indx;

if (allocsize > MAXALLOCSAVE) {

。。。

}

savedlist = kbp->kb_next;

kbp->kb_next = cp = va + (npg * NBPG) - allocsize;

for (;;) {

freep = (struct freelist *)cp;

if (cp <= va)

break;

cp -= allocsize;

freep->next = cp;

}

freep->next = savedlist;

if (kbp->kb_last == NULL)

kbp->kb_last = (caddr_t)freep;

}

va = kbp->kb_next;

kbp->kb_next = ((struct freelist *)va)->next;

out:

splx(s);

return ((void *) va);

}

接着執行startrtclock(),和時鐘相關。代碼如下:

unsigned long freq;

freq = tgt_cpufreq () / 4;

這個傳回外部時鐘頻率。其實這個名字有點含糊,但tgt_cpufreq的注釋很清楚,

Returns the external clock frequency, usually the bus clock,外部時鐘頻率。

tgt_pipefreq 的注釋為Returns the CPU pipelie clock frequency,cpu 頻率。

time.tv_sec = tgt_gettime ();

time.tv_usec = 0;

tgt_gettime()會讀取時鐘晶片上的時間并轉化成秒數傳回。

clkpertick = freq / hz;

clkperusec = freq / 1000000;

_softcompare = get_count() + clkpertick;

clkenable = 0;

53

這裡的get_count()恒傳回0。_softcompare 就是clkpertick。以上執行之後,返

回init_net(),接着執行:

mclrefcnt = (char *)malloc(VM_KMEM_SIZE/MCLBYTES,

M_MBUF, M_NOWAIT);

這裡的MCLBYTES 是2^11,也就是2KB。

bzero(mclrefcnt, NMBCLUSTERS+CLBYTES/MCLBYTES);

mb_map = kmem_suballoc(kernel_map, (vm_offset_t *)&mbutl,

&maxaddr, NMBCLUSTERS*MCLBYTES, FALSE);

接下來執行mbinit(),代碼如下:

s = splimp();

if (m_clalloc(max(4096 / CLBYTES, 1), M_DONTWAIT) == 0)

goto bad;

splx(s);

return;

bad:

panic("mbinit");

就是調用一個m_clalloc(),其中的cl 是cluster 的縮寫。代碼如下:

npg = ncl * CLSIZE;

從名字看出這個是計算占用多少頁。

p = (caddr_t)kmem_malloc(mb_map, ctob(npg), !nowait);

if (p == NULL) {

。。。。。

}

ncl = ncl * CLBYTES / MCLBYTES;

for (i = 0; i < ncl; i++) {

((union mcluster *)p)->mcl_next = mclfree;

mclfree = (union mcluster *)p;

p += MCLBYTES;

mbstat.m_clfree++;

}

mbstat.m_clusters += ncl;

return (1);

有必要對目前為止所有的配置設定函數,配置設定區

域和相關結構做一個總結。

54

Init_net 之tgt_devconfig

繼續看init_net(),接着執行tgt_devconfig(),這個遞歸調用的代碼量極大(藍色),

_pci_devinit(1);

這一句作用是初始化pci 裝置配置空間的一些參數,_pci_device 代碼就是一個

for 循環,雖然這個for 的循環體也隻執行一遍。代碼如下:

struct pci_device *pd;

for(i = 0, pd = _pci_head; i < pci_roots; i++, pd = pd->next) {

_pci_setup_devices (pd, initialise);

}

這個_pci_head 我們早見過了。可以了解為北橋,也可以了解為對所有pci 裝置

的一個抽象。所有已知的裝置(功能)在_pci_head->bridge.child 下。

_pci_setup_devices 函數的代碼如下(棕色):

static void _pci_setup_devices (struct pci_device *parent, int initialise)

{

struct pci_device *pd;

for (pd = parent->bridge.child; pd ; pd = pd->next) {

struct pci_bus *pb = pd->pcibus;

pcitag_t tag = pd->pa.pa_tag;

pcireg_t cmd, misc, class;

unsigned int ltim;

cmd = _pci_conf_read(tag, PCI_COMMAND_STATUS_REG);

讀取指令寄存器所在的四個位元組。

if (initialise) {

class = _pci_conf_read(tag, PCI_CLASS_REG);

cmd |= PCI_COMMAND_MASTER_ENABLE

| PCI_COMMAND_SERR_ENABLE

| PCI_COMMAND_PARITY_ENABLE;

cmd |= PCI_COMMAND_IO_ENABLE |

PCI_COMMAND_MEM_ENABLE;

if (pb->fast_b2b)

cmd |= PCI_COMMAND_BACKTOBACK_ENABLE;

_pci_conf_write(tag, PCI_COMMAND_STATUS_REG, cmd);

如果以前總線探測後支援背靠背,則設定每個裝置使能這個功能。

ltim = 64;

55

misc = _pci_conf_read (tag, PCI_BHLC_REG);

misc = (misc &

~(PCI_LATTIMER_MASK << PCI_LATTIMER_SHIFT))

| ((ltim & 0xff) << PCI_LATTIMER_SHIFT);

misc = (misc & ~(PCI_CACHELINE_MASK <<

PCI_CACHELINE_SHIFT))|

((PCI_CACHE_LINE_SIZE & 0xff) <<

PCI_CACHELINE_SHIFT);

_pci_conf_write (tag, PCI_BHLC_REG, misc);

if(PCI_CLASS(class) == PCI_CLASS_BRIDGE ||

PCI_SUBCLASS(class) ==

PCI_SUBCLASS_BRIDGE_PCI ||

pd->bridge.child != NULL) {

_pci_setup_devices (pd, initialise);

如果這個裝置是個pci 橋,則遞歸調用_pci_setup_devices。這個函數實際上并

不調用。xiangy 待定

} //if

} //if

} // for

}

_pci_devinit 就到這裡了。

tgt_devconfig 之顯示卡

下面回到tgt_devconfig()執行。現在要執行顯示卡相關的東西,到現在為止,龍芯

pc 使用過ATI(2E),SIS(2F 福珑),SM(2F 逸珑)三款不同的顯示卡。特别是

sis 的顯示卡處理非常複雜,以後可能會解釋這塊的代碼。不過這裡我們以SM712

的顯示卡為例走流程。(可以偷點懶不是,^_^),代碼如下:

rc = 1;

rc 反正表示顯示卡有效,不知道什麼的縮寫。看注釋,下面點燈。

*((volatile unsigned char *)(0xbfd00000 | HIGH_PORT)) = 0xfe;

*((volatile unsigned char *)(0xbfd00000 | LOW_PORT)) = 0x01;

temp = *((volatile unsigned char *)(0xbfd00000 | DATA_PORT));

*((volatile unsigned char *)(0xbfd00000 | HIGH_PORT)) = 0xfe;

*((volatile unsigned char *)(0xbfd00000 | LOW_PORT)) = 0x01;

*((volatile unsigned char *)(0xbfd00000 | DATA_PORT)) = 0x00;

代碼中的高端口和低端口組成一個16 位的位址,看代碼好像是先讀出上次的亮

56

度值,再搞成最暗。由于rc 為1,接着執行:

fbaddress =_pci_conf_read(vga_dev->pa.pa_tag,0x10);

ioaddress =_pci_conf_read(vga_dev->pa.pa_tag,0x18);

在執行_pci_businit()函數的時候會調用_pci_setup_windows,如果是顯示裝置,

就會給vga_dev 這個全局變量指派。這裡就是顯示卡控制器(顯示卡)。fbaddress

和ioaddress 表示了顯示卡想要申請的空間資源。

fbaddress = fbaddress &0xffffff00; //laster 8 bit

ioaddress = ioaddress &0xfffffff0; //laster 4 bit

printf("fbaddress 0x%x\tioaddress 0x%x\n",fbaddress, ioaddress);

處理對齊需求。

fbaddress |= 0xb0000000;

ioaddress |= 0xbfd00000;

變成我們可配置設定的虛拟位址。下面開始就要和sm712 這個顯示卡打交道了。

sm712_init((unsigned char *)fbaddress,(unsigned char *)ioaddress);

sm712_init 代碼如下:

int sm712_init(char * fbaddress,char * ioaddress)

{

u32 smem_size, i;

smi_init_hw();

這個函數很短,就兩行代碼:

linux_outb(0x18, 0x3c4);

linux_outb(0x11, 0x3c5);

閱讀sm712 的資料手冊,寫入0x3c4 寄存器的值表示後續寄存器通路的index

值,比如這裡的linux_outb(0x18, 0x3c4)表是下一句對0x3c5 寫操作作用于

index 為0x18的0x3c5寄存器(0x3c5寄存器有上百個index,這個用法很特别)。

0x11 表示使用0x0A0000~0xBFFFF 的mem 空間。32 位位址模式,Externded

packed pixel graphics 模式。

hw.m_pLFB = SMILFB = fbaddress;

hw.m_pMMIO = SMIRegs = SMILFB + 0x00700000;

hw.m_pDPR = hw.m_pLFB + 0x00408000;

hw.m_pVPR = hw.m_pLFB + 0x0040c000;

hw 這個結構體全局變量是在sm712.h 中定義的,用于描述712 顯示卡的屬性。上

面設定了一些位址。

hw.width = SM712_FIX_WIDTH;

hw.height = SM712_FIX_HEIGHT;

hw.bits_per_pixel = SM712_FIX_DEPTH;

hw.hz = SM712_FIX_HZ;

設定分辨率為1024x600,16 位色,重新整理率為60Hz。

smi_seqw(0x21,0x00);

這個index 為21 的寄存器基本就是使能。

smi_seqw(0x62,0x7A);

57

設定顯存與時鐘相關的參數并預設使用顯存(bit2)。

smi_seqw(0x6a,0x0c);

smi_seqw(0x6b,0x02);

這兩條是合在一塊的,用于設定顯存頻率,計算公式為

14.31818MHz * MNR / MDR

MNR就是CR6a的值,MDR就是CR6b的值。這裡我們的顯存頻率就是66MHz

左右。

smem_size = 0x00400000;

表示顯存大小為4MB。

for(i = 0; i < smem_size / 2; i += 4){

*((volatile unsigned long *)(fbaddress + i)) = 0x00;

}

上面的代碼表示黑屏,但是循環次數我不了解,

*(u32 *)(SMILFB + 4) = 0xAA551133;

if (*(u32 *)(SMILFB + 4) != 0xAA551133)

{

smem_size = 0x00200000;

smi_seqw(0x6a,0x12);

smi_seqw(0x6b,0x02);

smi_seqw(0x62,0x3e);

}

使用先寫後讀的方式探測顯存是否存在。如果不存在就修改CR62 的配置,改為

使用記憶體。

smi_set_timing(&hw);

前面的hw指派總算派上用場了。這個smi_set_timing會比對設定的這種分辨率,

色深,重新整理率并使用這個模式下預設的參數配置顯示卡。細節較多,不叙。

SMI2DBaseAddress = hw.m_pDPR;

sm712_2d_init();

設定2d 相關的寄存器。

printf("Silicon Motion, Inc. LynxEM+ Init complete.\n");

return 0;

}

執行完sm712_init 後,回到tgt_devconfig,執行:

fb_init(fbaddress, ioaddress);

fb_init 的代碼如下:

int fb_init(unsigned long fbbase, unsigned long iobase)

{

pGD = &GD;

pGD->winSizeX = GetXRes();

pGD->winSizeY = GetYRes();

pGD->gdfBytesPP = GetBytesPP();

58

pGD->gdfIndex = GetGDFIndex(GetBytesPP());

使用顯示模式初始化pGD 全局變量。gdfIndex 為GDF_16BIT_565RGB

pGD->frameAdrs = 0xb0000000 | fbbase;

設定framebuffer 起始位址。

_set_font_color();

字元顯示的配置。

video_fb_address = (void *)VIDEO_FB_ADRS;

這裡的video_fb_address 就是pGD->frameAdes。

memsetl(video_fb_address, CONSOLE_SIZE +

(CONSOLE_ROW_SIZE *5), CONSOLE_BG_COL);

為什麼多清空5 行???

video_display_bitmap(BIGBMP_START_ADDR, BIGBMP_X,

BIGBMP_Y);

這個畫圖的函數相當長,功能就是把從存放在BIGBMP_START_ADDR 這個地

址開始的圖檔内容顯示到左上角坐标為(BIGBMP_X,BIGBMP_Y)的位置。

video_console_address = video_fb_address;

printf("CONSOLE_SIZE %d, CONSOLE_ROW_SIZE %d\n",

CONSOLE_SIZE, CONSOLE_ROW_SIZE);

console_col = 0;

console_row = 0;

memset(console_buffer, ' ', sizeof console_buffer);

video_display_bitmap(SMALLBMP0_START_ADDR, SMALLBMP0_X,

SMALLBMP0_Y);

video_display_bitmap(SMALLBMP_START_ADDR_EN_01,

SMALLBMP01_EN_X, SMALLBMP01_EN_Y);

顯示那個大圖檔下面的兩個帶有漢字的小圖檔。

return 0;

}

回到tgt_devconfig 中,顯示卡相關的東西都基本完成了。現在液晶的背光都是關

閉的,現在要根據以前儲存的亮度重新打開背光。

*((volatile unsigned char *)(0xbfd00000 | HIGH_PORT)) = 0xfe;

*((volatile unsigned char *)(0xbfd00000 | LOW_PORT)) = 0x01;

*((volatile unsigned char *)(0xbfd00000 | DATA_PORT)) = temp;

好了,和顯示相關的基本到這裡就完了。咱們接着看tgt_devconfig的其它内容。

59

tgt_devconfig 之config_init

void config_init()

{

TAILQ_INIT(&deferred_config_queue);

TAILQ_INIT(&alldevs);

TAILQ_INIT(&allevents);

TAILQ_INIT(&allcftables);

上面初始化了四個連結清單,開始都為空鍊。

TAILQ_INSERT_TAIL(&allcftables, &staticcftable, list);

這個staticcftable 内容可不少,一下子都插入到allcftables 連結清單中

}

先看看看這個staticcftable 的内容。

static struct cftable staticcftable = {

cfdata

};

就是對cfdata 的一個包裝,cfdata 的内容如下:

struct cfdata cfdata[] = {

{&mainbus_ca, &mainbus_cd, 0, NORM, loc, 0, pv+ 1, 0, 0, 0},

{&pcibr_ca, &pcibr_cd, 0, NORM, loc, 0, pv+ 6, 0, 0, 0},

{&usb_ca, &usb_cd, 0, STAR, loc, 0, pv+ 8, 0, 0, 0},

{&localbus_ca, &localbus_cd, 0, NORM, loc, 0, pv+ 6, 0, 0, 0},

{&pci_ca, &pci_cd, 0, STAR, loc+ 1, 0, pv+ 4, 1, 0, 0},

{&rtl_ca, &rtl_cd, 0, NORM, loc+ 0, 0, pv+ 0, 3, 0, 0},

{&pciide_ca, &pciide_cd, 0, STAR, loc+ 0, 0, pv+ 0, 3, 0, 0},

{&ohci_ca, &ohci_cd, 0, STAR, loc+ 0, 0, pv+ 0, 3, 0, 0},

{&wd_ca, &wd_cd, 0, STAR, loc+ 0, 0, pv+ 2, 6, 0, 0},

{0},

{0},

{0},

60

{0},

{0},

{0},

{0},

{0},

{(struct cfattach *)-1}

};

cfdata 的結構體定義如下:

struct cfdata {

struct cfattach *cf_attach;

struct cfdriver *cf_driver;

short cf_unit;

short cf_fstate;

int *cf_loc;

int cf_flags;

short *cf_parents;

int cf_locnames;

void (**cf_ivstubs)

__P((void));

short cf_starunit1;

};

大緻介紹一下這個結構體:

cf_attach 包含了裝置的比對,attach,detach 等,

cf_driver 表示這一類裝置的驅動,

cf_unit 記錄這種裝置的個數

cf_fstate 表示搜尋的狀态。共有四種狀态

#define NORM FSTATE_NOTFOUND 沒有發現

#define STAR FSTATE_STAR

#define DNRM FSTATE_DNOTFOUND 不去發現

#define DSTR FSTATE_DSTAR

這些定義的值如下

#define FSTATE_NOTFOUND 0

#define FSTATE_FOUND 1

#define FSTATE_STAR 2

#define FSTATE_DNOTFOUND 3 //has not been found,and is disabled

#define FSTATE_DSTAR 4

cf_loc

cf_flags 反正定義的地方全是0

cf_parents 父親的id,是個short,cf_parents 是id 數組的下标,等會詳述。

cf_locnames 注釋說是start of names,不過我沒明白什麼意思。

cf_starunit1 對于cf_flags 為STAR 的裝置,這個參數表示第一個可用的次設

備号。

61

這個cfdata 數組是通過配置檔案../Targers/Bonito2F7inch/conf/Bonito.2F7inch

産生的,配置檔案中的部分内容如下。

mainbus0 at root

localbus0 at mainbus0

pcibr0 at mainbus0

pci* at pcibr?

rtl0 at pci? dev ? function ?

ohci* at pci? dev ? function ?

usb* at usbbus ?

pciide* at pci ? dev ? function ? flags 0x0000

wd* at pciide? channel ? drive ? flags 0x0000

實際上這裡已經表明了這個pci 架構的層次結構。這個架構也可展現cf_parents

這個成員中。

cfdata 數組中wd 的cf_parents 為pv+2,就是pv[2],pv 數組的定義如下:

short pv[10] = {

4, -1, 6, -1, 1, -1, 0, -1, 7, -1,

};

pv[2]為6,這表示它的父親在cfdata[6]被描述,cfdata[6]描述的是pciide控制器。

pciide 的cf_parents 為pv[0],值為4,它的父親就是cfdata[4],cfdata[4]描述

的是pci0。

pci 的cf_parents 為pv [4],值為1,它的父親就是cfdata[1],cfdata[1]描述pcibr。

pcibr 的cf_parents 為pv+6,pv[6]為0,可見它的父親是cfdata[0],cfdata 描

述mainbus。mainbus 的cf_parents 為pv[1],值為-1,不在這個cfdata 數組中,

表明這個mainbus 是root,沒有parents。

tgt_devconfig()之configure

在init_net 之前,我們也探測過pci 總線,不過那時探測的是裝置(功能),并不

知道這個架構的拓撲結構。而已實際上那些探測的裝置并不是我們平常了解的設

備,兩者的關系比如IDE 控制器和硬碟,usb 控制器和u 盤。前面的探測相當于

隻知道有了哪些控制器。

下面來看看這個configure,這個可是整個pci 初始化的核心。

這個函數很簡單,實質代碼隻有如下兩句:

if(config_rootfound("mainbus", "mainbus") == 0)

panic("no mainbus found");

也就是調用config_rootfound 這個函數,參數為兩個mainbus 的字元串。看看:

struct device *config_rootfound(char *rootname, void *aux)

{

void *match;

if ((match = config_rootsearch((cfmatch_t)NULL, rootname, aux)) != NULL)

62

return (config_attach(ROOT, match, aux, (cfprint_t)NULL));

return (NULL);

}

就是兩個函數,先看config_rootsearch((cfmatch_t)NULL, rootname, aux),再

看config_attach(ROOT, match, aux, (cfprint_t)NULL)。

config_rootsearch 就是傳入這個pci 架構的根的名字,找到根這個裝置。我們傳

入的參數是NULL 和兩個“mainbus”字元串,config_rootsearch 函數代碼如下:

void *config_rootsearch(cfmatch_t fn, char *rootname, void *aux)

{

register struct cfdata *cf;

register short *p;

struct matchinfo m;

m.fn = fn;

m.parent = ROOT;

m.match = NULL;

m.aux = aux;

m.indirect = 0;

m.pri = 0;

這些參數等會都會用到。

for (p = cfroots; *p >= 0; p++) {

cfroots 的第二項就是-1,表明這個pci 架構隻有一個root。是以這個循環隻執行

一次。第一項的值為0,也就是*p 為0。

cf = &cfdata[*p];

if (strcmp(cf->cf_driver->cd_name, rootname) == 0)

這個就能比對了,cf 就是cfdata[0]。

mapply(&m, cf);

這個會改變m.match,config_rootsearch()就是要傳回這個match。

}

return (m.match);

}

mapply 這個函數後面會多次用到,這裡我們就先按照config_rootsearch 中調用

的上下文,走一下執行流程。函數代碼如下:

void mapply(struct matchinfo *m, struct cfdata *cf)

{

register int pri;

void *match;

if (m->indirect)

match = config_make_softc(m->parent, cf);

else

match = cf;

63

在config_rootsearch 中indirect 為0,是以這裡match 就是cf(cfdata[0])。

if (m->fn != NULL)

pri = (*m->fn)(m->parent, match, m->aux);

else {

if (cf->cf_attach->ca_match == NULL) {

panic("mapply: no match function for '%s' device",

cf->cf_driver->cd_name);

}

pri = (*cf->cf_attach->ca_match)(m->parent, match, m->aux);

cfdata 的match 函數就是傳回1 而已,其餘什麼都不幹。是以pri=1。

}

if (pri > m->pri) {

if (m->indirect && m->match)

free(m->match, M_DEVBUF);

m->match = match;

m->pri = pri;

在config_rootsearch 中m->pri 為0,比pri 小。這裡把m->match 指派為cfdata

[0]的位址,pri 指派為1。這個match 就是我們最後要傳回的值。

} else {

if (m->indirect)

free(match, M_DEVBUF);

}

}

好了,config_rootsearch 找到了叫mainbus 的裝置,并且傳回描述這個裝置結

構體指針。下面看config_attach(ROOT, match, aux, (cfprint_t)NULL)。這裡的

ROOT 為NULL,match 為cfdata[0]的指針,aux 為字元串”mainbus”,attach

嘛,就是找個依靠,免得一個人孤零零的。

struct device *config_attach(struct device *parent, void *match,

void *aux, cfprint_t print)

{

register struct cfdata *cf;

register struct device *dev;

register struct cfdriver *cd;

register struct cfattach *ca;

struct cftable *t;

if (parent && parent->dv_cfdata->cf_driver->cd_indirect) {

dev = match;

cf = dev->dv_cfdata;

} else {

cf = match;

沒有父親或者就父親不給你開後門安排位置,那就自己去天下。

64

dev = config_make_softc(parent, cf);

config_make_softc()主要作用是初始化這個我們所要attach 的裝置,但也順便

給cf 拉點贊助,更新資料。

}

cd = cf->cf_driver;

ca = cf->cf_attach;

cd->cd_devs[dev->dv_unit] = dev;

如果這個cf 的config 是第一次被調用,它所扶持的裝置一般都是從0 這個次設

備号開始,是以這個dev->dv_init 為0。

if (cf->cf_fstate == FSTATE_STAR) {

if (dev->dv_unit == cf->cf_unit)

cf->cf_unit++;

更新這個類型的裝置數目。

} else

cf->cf_fstate = FSTATE_FOUND;

TAILQ_INSERT_TAIL(&alldevs, dev, dv_list);

找到了一個裝置了,加入到alldevs 中,我們這個dev 就叫mainbus0。

device_ref(dev);

把這個找到的裝置加入到裝置清單中,并更新ref 計數。

if (parent == ROOT)

printf("%s (root)", dev->dv_xname);

else {

printf("%s at %s", dev->dv_xname, parent->dv_xname);

if (print)

(void) (*print)(aux, (char *)0);

}

for (cf = t->tab; cf->cf_driver; cf++) {

if (cf->cf_driver == cd &&cf->cf_unit == dev->dv_unit) {

if (cf->cf_fstate == FSTATE_NOTFOUND)

cf->cf_fstate = FSTATE_FOUND;

if (cf->cf_fstate == FSTATE_STAR)

cf->cf_unit++;

根據這個新插入的dev 看是否有必要改變某些cf 的屬性。

}

}

(*ca->ca_attach)(parent, dev, aux);

現在用相關的attach 函數真正的把這個新配置的dev 依附到他的parent 上。這

裡實際調用mainbus_attach()。這個函數會導緻遞歸調用,這樣一層層下去就建

立了一個總線架構的拓撲結構,為一個樹狀結構。

65

config_process_deferred_children(dev);

return (dev);

}

調用mainbus_attach 的傳入參數是NULL,描述manbus0 的dev,“mainbus”。

static void mainbus_attach(struct device *parent, struct device *self, void *aux)

{

struct mainbus_softc *sc = (struct mainbus_softc *)self;

struct confargs nca;

mainbus_softc 的結構體包含兩部分,前面是一個device 結構體(是以才敢如上

指派),後面是一個bushook,描述了這個裝置挂載的總線的資訊,五個成員都

在下面被指派。

sc->sc_bus.bh_dv = (struct device *)sc;

sc->sc_bus.bh_type = BUS_MAIN;

sc->sc_bus.bh_intr_establish = NULL;

sc->sc_bus.bh_intr_disestablish = NULL;

sc->sc_bus.bh_matchname = mb_matchname;

上面表示傳入的dev(self)挂在mainbus 上,,沒有設定中斷相關的映射關系。

我們在前面看cfdata 數組定義的時候知道mainbus 其實有兩個下屬裝置,

localbus 和pcibr,下面就要手動建立聯系了。

nca.ca_node = NULL;

nca.ca_name = "localbus";

nca.ca_bus = &sc->sc_bus;

config_found(self, &nca, mbprint);

nca.ca_node = NULL;

nca.ca_name = "pcibr";

nca.ca_bus = &sc->sc_bus;

config_found(self, &nca, mbprint);

nca 是個confargs 結構體,在這裡起臨時變量的作用。在指派nca 作為傳入參

數後,調用config_found()。傳入參數還是描述mainbus0 的那個dev。剛說了,

mainbus 有兩個兒子,pcibr 和localbus,這個是靜态定義的,代碼中寫死了。

但他們的兒子代碼就不可能指定了,要考config_found 去探測。

下面的兩塊懷疑是沒用的,邏輯上說不通(在隻有一個pci 橋的情況下),注釋掉

也沒有發現有副作用。

nca.ca_node = NULL;

nca.ca_name = "pcibr";

nca.ca_bus = &sc->sc_bus;

config_found(self, &nca, mbprint);

nca.ca_node = NULL;

nca.ca_name = "pcibr";

nca.ca_bus = &sc->sc_bus;

66

config_found(self, &nca, mbprint);

}

現在就看config_found 做了些什麼,函數定義如下:

#define config_found(d, a, p) config_found_sm((d), (a), (p), NULL)

可見config_found 就是對config_found_sm 的包裝,函數本質如下:

struct device *config_found_sm(struct device *parent, void *aux,

cfprint_t print, cfmatch_t submatch)

{

void *match;

if ((match = config_search(submatch, parent, aux)) != NULL)

return (config_attach(parent, match, aux, print));

return (NULL);

}

這個函數看着眼熟吧,和config_rootfound幾乎是一模一樣。但config_rootfound

是點到為止,不往下走,這個函數卻會使用遞歸往下刨。以使用傳入參數aux

代表pcibr 的調用為例。先看config_search,這個傳入參數為NULL,代表

mainbus0 的dev,代表pcibr 的nca。

void * config_search(cfmatch_t fn, struct device * parent, void * aux)

{

。。。變量定義。。。

m.fn = fn;

m.parent = parent;

m.match = NULL;

m.aux = aux;

m.indirect = parent && parent->dv_cfdata->cf_driver->cd_indirect;

m.pri = 0;

上面的indirect 基本上都是0。

for(t = allcftables.tqh_first; t; t = t->list.tqe_next) {

for (cf = t->tab; cf->cf_driver; cf++) {

if (cf->cf_fstate == FSTATE_FOUND)

continue;

if (cf->cf_fstate == FSTATE_DNOTFOUND ||

cf->cf_fstate == FSTATE_DSTAR)

continue;

for (p = cf->cf_parents; *p >= 0; p++)

if (parent->dv_cfdata == &(t->tab)[*p])

mapply(&m, cf);

這裡是我們唯一關心的。對于我們的輸入,這裡找到的cf 是pcibr (cfdata[1 ])的

描述。當然細心的你也許會發現,即使我們的輸入的aux 參數表示的是pcibr,

但是這裡的這裡的cf 為localbus 也是有可能的。因為它的parents 的确也是

67

mainbus 呀。關鍵的東西是m.pri 和cf->fstate 參數,在mapply 中,pri 這個參

數有防止重入的功能,展現在mapply 中的if(pri > m->pri)這句代碼中。這種設

計確定所有的兒子都被父親探測到,不但有順序,而且不重複。

}

}

return (m.match);

}

mapply 的代碼前面已經看過,對于我們的輸入,就是把m.match 置為cfdata

數組中描述pci 的結構體指針而已。傳回這個m.match 後,config_search 函數

也傳回了,傳回值非NULL 表示search 到兒子了。

接着執行config_attach(parent, match, aux, print)。

繞了許久,緣何至此,曾記否?

皆因調config_rootfound 之config_attach 之故也。至此,竟成了輪回。

是的,遞歸了。不過現在還沒有看到這個遞歸的關鍵環節。都隻是例行公事一樣

通過attach 插入些裝置而已。這裡也是是把pcibr 加入到alldevs 中罷了。在

config_attach 中有接着執行下一級的attach,對于pcibr,就是pcibrattach。

void pcibrattach(struct device * parent,struct device * self, void * aux)

{

struct pcibr_softc *sc = (struct pcibr_softc *)self;

struct pcibus_attach_args pba;

sc->sc_iobus_space = *_pci_bus[sc->sc_dev.dv_unit]->pa.pa_iot;

sc->sc_membus_space = *_pci_bus[sc->sc_dev.dv_unit]->pa.pa_memt;

sc->sc_dmatag = *_pci_bus[sc->sc_dev.dv_unit]->pa.pa_dmat;

pba.pba_busname = "pci";

pba.pba_iot = &sc->sc_iobus_space;

pba.pba_memt = &sc->sc_membus_space;

pba.pba_dmat = &sc->sc_dmatag;

pba.pba_pc = NULL;

pba.pba_bus = sc->sc_dev.dv_unit;

config_found(self, &pba, pcibrprint);

}

就不去看太細節的東西了,很明顯有一點,bcibr 明确指定自己的兒子是pci,并

把自己的财産都給了他。接着執行config_found(self, &pba, pcibrprint),由于

pcibr 隻有一個兒子,傳回的自然是pci 了。接着執行config_attach,把pci 插

入了。

接着遞歸在mapply 中調用pci 的attach 函數pciattach。這個pciattach 和前面

的那些attach 都不太一樣,代碼比較長。其中的aux 就是上面的&pba。

void pciattach(struct device * parent, struct device *self, void *aux)

{

struct pcibus_attach_args *pba = aux;

68

bus_space_tag_t iot, memt;

pci_chipset_tag_t pc;

int bus, device, maxndevs, function, nfunctions;

iot = pba->pba_iot;

memt = pba->pba_memt;

pc = pba->pba_pc;

bus = pba->pba_bus;

maxndevs = pci_bus_maxdevs(pc, bus);

pci 總線最多能挂32 個裝置,實際上由于硬體特性的原因,一般到不了20 個。

if (bus == 0)

pci_isa_bridge_callback = NULL;

bus 為0,callback 為NULL。下面依順序探測pci 裝置是否存在(這個工作我們

不是第一次做了)

for (device = 0; device < maxndevs; device++) {

。。。。。。。。

bhlcr = pci_conf_read(pc, tag, PCI_BHLC_REG);

nfunctions = PCI_HDRTYPE_MULTIFN(bhlcr) ? 8 : 1;

看這個裝置是否為多功能的。

for (function = 0; function < nfunctions; function++) {

。。。pa 結構的指派。。。

config_found_sm(self, &pa, pciprint, pcisubmatch);

好了,沒個功能調用一次config_found_sm,self 為pci 的cfdata 描述,pa 為這

個功能的描述,pcisubmatch 是一個函數,等用到的時候再解釋。

}

}

if (bus == 0 && pci_isa_bridge_callback != NULL)

(*pci_isa_bridge_callback)(pci_isa_bridge_callback_arg);

}

我們實際上知道pci 隻有3 個兒子,pciide,ohci,rtl。作為功能他們會分别調用

config_found_sm,但和前面都不同的是,這裡的pcisubmatch 非NULL。

struct device *config_found_sm(struct device *parent,void * aux,

cfprint_t print, cfmatch_t submatch)

{

void *match;

if ((match = config_search(submatch, parent, aux)) != NULL)

return (config_attach(parent, match, aux, print));

69

if (print)

printf(msgs[(*print)(aux, parent->dv_xname)]);

return (NULL);

}

我們并不關心print 相關的東西,下面我們以pciide 的功能識别為例,再來看這

個config_found_sm 函數。由于傳入的submatch 為pcisubmatch,在mapply

中執行路徑就和原來不太一樣了。

if (m->fn != NULL)

pri = (*m->fn)(m->parent, match, m->aux);

else {

if (cf->cf_attach->ca_match == NULL) {

panic("mapply: no match function for '%s' device",

cf->cf_driver->cd_name);

}

pri = (*cf->cf_attach->ca_match)(m->parent, match, m->aux);

}

我們這裡要執行的就是pcisubmatch 了。還有這裡的傳入參數m->aux 就是用來

描述我們要注冊的那3 個功能的, aux 在之前幾乎是去沒有去用的。這個

pcisubmatch 的傳回值也是有講究的,如果傳入的match 和aux 描述的不是一

個東西,就傳回0,這樣pri 就是0,這樣就保證了mapply 傳回的match 就是

我們要它去比對的那3 個裝置。如果就是那個match 和aux 描述的是一個東西。

就調用這個裝置的match 函數。對于pciide,就是調用pciide_match

if (pri > m->pri) {

if (m->indirect && m->match)

free(m->match, M_DEVBUF);

m->match = match;

m->pri = pri;

} else {

if (m->indirect)

free(match, M_DEVBUF);

}

除非和cfdata 中的那三個pci 的兒子比對,否則pri 均為0,mapply 傳回後

m.match 為NULL。pcisubmatch 的主要功能就是調用那3 個裝置的match,下

面就說說pciide_match。

這個match 才叫做match,函數先判斷這個裝置是否是ide 控制器,如果是,調

用pp = pciide_lookup_product(pa->pa_id);這裡的pa ->pa_id 還是我們在

pciattach 中那個循環中探測并儲存。pa_id 表示廠商id。pciide_lookup_product

會比對那個ide 控制器的清單,顯然我們用的是CS5536 的南橋的ide 控制器,

廠商id 就是AMD,AMD 的IDE 控制器支援清單如下:

const struct pciide_product_desc pciide_amd_products[] = {

{ PCI_PRODUCT_AMD_PBC756_IDE,

0,

70

amd756_chip_map

},

{ PCI_PRODUCT_AMD_CS5536_IDE,

0,

amdcs5536_chip_map

},

};

pciide_lookup_product 除了比對廠商号, 還有裝置号, 這裡很清楚的就是

CS5535嘛。比對好以後就傳回描述CS5536的那個結構體描述符。pciide_match

探測好後,傳回1。

這個對于pci 的兒子pciide 的config_search 會傳回pciide 的cfdata 的描述,之

後執行config_attach,這個函數在插入pciide 到alldevs 的裝置連結清單之後會調用

pciide 的attach 函數,下面看看,很重要哦!!!

void pciide_attach(struct device *parent, struct device *self, void *aux)

{

struct pci_attach_args *pa = aux;

pcitag_t tag = pa->pa_tag;

struct pciide_softc *sc = (struct pciide_softc *)self;

pcireg_t csr;

char devinfo[256];

sc->sc_pp = pciide_lookup_product(pa->pa_id);

這個函數我們在pciide 的match 函數中已經見識過了,就是查找pmon 支援的

ide 控制器清單,這裡會傳回描述CS5536 的ide 控制器的結構體描述符。内容

為:

{ PCI_PRODUCT_AMD_CS5536_IDE,

0,

amdcs5536_chip_map

}

這個東西等會就要用上了。

sc->sc_pc = pa->pa_pc;

sc->sc_tag = pa->pa_tag;

sc->sc_pp->chip_map(sc, pa);

這個調用很重要哦。等會就看。

if (sc->sc_dma_ok) {

下面看名字貌似是DMA 相關的,就是一個使能。

csr = pci_conf_read(pc, tag, PCI_COMMAND_STATUS_REG);

csr |= PCI_COMMAND_MASTER_ENABLE;

pci_conf_write(pc, tag, PCI_COMMAND_STATUS_REG, csr);

}

71

}

這個pciide_attach 的最重要的功能當然是探測有沒有(有的話,幾個)硬碟挂

在硬碟控制器上,并把找到的硬碟以wd0,wd1 的名字插入alldevs 的連結清單中。

代碼還是比較繁瑣的,我也不大願意再去看了。阿彌佗佛。指條明路吧。

amdcs5536_chip_map->pciide_mapchan->wdcattach->wdcprobe,每個ide 控

制器最多兩個channal,amdcs5536_chip_map 會通過調用pciide_mapchan

->wdcattach 探測這兩個channal 有沒有挂載硬碟,真正探測是用過wdcattach

中的wdcprobe 函數,這個探測的方法是我們常用的,發指令看有沒有回應。如

果等了很久還沒有收到回應就認為硬碟沒有安裝。如果硬碟存在,在wdcattach

中會調用陌生的老朋友config_found 來插入到alldevs 中,這樣alldevs 中就有

了wd0 了。

USB

前面簡單介紹了一下wd0 的識别,這裡我們看一下usb。

首先聲明,我之前對USB 的了解是一窮二白,這裡特意弄這麼一節,純屬自虐。

從pci 的attach 開始吧。pciattach 函數會掃描pci 總線上的所有裝置/功能,然

後依次調用config_found_sm。對這個函數,順着代碼看的哥們都快背下了吧。

注意這個config_found_sm 的調用中match 函數為pcisubmatch,不是NULL,

這個對函數執行的流程會造成影響。具體的就是在調用mapply 的時候,m->fn

非NULL,這樣就會執行pci 那些兒子的match,當然也包括ohci。前面提過的

pci的兒子有三個,ohci,rtl和pciide。如果真的是ohci的話,調用ohci的match

會傳回1,指派給pri,這樣m->match就指派上了,否則m->match就傳回NULL。

在8089 筆記本上,有兩個ohci 控制器,一個是NEC 的,一個是5536 内部集

成的。是以能從ohci 的match 函數傳回1 的就隻有兩個裝置/功能。下面看看

ohci 的match 函數:

if(PCI_CLASS(pa->pa_class) == PCI_CLASS_SERIALBUS &&

PCI_SUBCLASS(pa->pa_class) == PCI_SUBCLASS_SERIALBUS_USB)

{

if(((pa->pa_class >>8) & 0xff) == 0x10){

//printf("Found usb ohci controller\n");

return 1;

}

}

可見判斷的标準很明确的,base class 和sub class 為0xc 和0x3 表明這個裝置

是串行總線,interface 為0x10 表明是ohci,這些都是在pci 規範的class code

中定義的,具體可檢視pci2.3 規範的附錄。

在match 上後,最終會在config_attach 中調用ohci 的attach 函數。

ohci_attach 一開始會做些正常的判斷并根據硬體連線的情況做些屏蔽處理,比

如在pmon 下wifi 這個裝置(接在usb 端口)就被屏蔽。接下的代碼如下:

72

if(pci_mem_find(pc, pa->pa_tag, OHCI_PCI_MMBA,

&membase, &memsize, &cachable)){

。。。。。。

}

讀取pci配置設定之後這個ohci控制器配置設定到的mem 位址和大小。具體的配置設定是在_

pci_businit 函數中完成的,這個我們前面已經講解過了。

if(bus_space_map(memt, membase, memsize, 0, &ohci->sc_sh)){

。。。。。。

}

從命名上看,和映射有關。bus_space_map 的定義如下:

#define bus_space_map(t, addr, size, cacheable, bshp) \

((*(bshp) = (t)->bus_base + (addr)), 0)

通過列印,這裡的memt->bus_base 為0xb0000000,如果你了解龍芯的位址映

射過程的話,這種b 開頭的虛拟位址大部分都是映射到pci 空間的。這裡的size

都是4KB。

usb_lowlevel_init(ohci);

這個函數就不進去看了。其主要内容是ohci的結構體成員(hcca,gtd,ohci_dev,

control_buf,setup)的空間配置設定和部分成員(disabled,sleeping,irq,regs,

slot_name)的初始化,随後重新開機ohci 控制器,最後開啟ohci 控制器。

ohci->hc.uop = &ohci_usb_op;

三個ohci 操作函數指針的初始化。這些函數是會在後面大量調用。

TAILQ_INSERT_TAIL(&host_controller, &ohci->hc, hc_list);

把這個找到的ohci 控制器加入到usb 控制器連結清單中。

ohci->sc_ih = pci_intr_establish(pc, ih, IPL_BIO, hc_interrupt, ohci,

self->dv_xname);

注冊中斷查詢函數。

ohci->rdev = usb_alloc_new_device(ohci);

在usb_dev數組記錄新的ohci裝置,并把這個數組項的位址指派給ohci的成員。

usb_alloc_new_device 函數非常簡單,唯一要注意的是這個新的裝置最終可能

是有小孩的。

usb_new_device(ohci->rdev);

從注釋就看出來了,這個函數是枚舉看是否有連接配接裝置。

對于ohci 來說,這個函數是我們最後要看得一個函數了。不幸的是,這個函數

非常複雜。

開始的代碼是對dev 的一些成員的初始化,這個現在也講不清楚。跳過。

接下來執行一個非常重要的函數,

usb_get_descriptor(dev, USB_DT_DEVICE, 0, &dev->descriptor, 8)

進入這個函數看看,定義如下:

int usb_get_descriptor(struct usb_device *dev, unsigned char type, unsigned

char index, void *buf, int size)

73

{

int res;

res = usb_control_msg(dev, usb_rcvctrlpipe(dev, 0),

USB_REQ_GET_DESCRIPTOR, USB_DIR_IN,

(type << 8) + index, 0,

buf, size, USB_CNTL_TIMEOUT);

return res;

}

代碼非常的清楚,先看usb_rcvctrlpipe(dev, 0)這個參數。

從usb_control_msg 這個函數的定義中會看到,這個參數叫pipe。好,看這個

pipe是如何出來的。參看sys/dev/usb/usb.h中的L314行開始的注釋就知道了。

max size: bits 0-1 (00 = 8, 01 = 16, 10 = 32, 11 = 64)

direction: bit 7 (0 = Host-to-Device [Out], 1 = Device-to-Host [In])

device: bits 8-14

endpoint: bits 15-18

Data0/1: bit 19

speed: bit 26 (0 = Full, 1 = Low Speed)

pipe type: bits30-31 (00 = isochronous, 01 = interrupt, 10 = control, 11 = bulk)

這裡的這個pipe 和我們說的EP 是一對一的關系,這裡值是0x80000080,可見

解釋為控制管道,全速,EP0,裝置0,方向輸入,maxsize 為8。注意ep0 是

個特殊的ep,必定是用于控制的。

再看usb_control_msg 的實作。

首先我們出入的timeout 為USB_CNTL_TIMEOUT 定義為100,表示100ms 内

這個get 操作要完成。

接着設定setup_packet 這個結構體。

setup_packet.requesttype = requesttype;

setup_packet.request = request;

setup_packet.value = swap_16(value);

setup_packet.index = swap_16(index);

setup_packet.length = swap_16(size);

命名都比較清楚了。requesttype 為get descriptor, request 為in。value 為

USB_DT_DEVICE<<8,index 為0,size 為0,表示8 個位元組。

這個setup 的包我在ohci 的規範中沒有找到,但是網上有些解釋。就不說了。

下面會通過submit_control_msg(dev,pipe,data,size,&setup_packet)去發送這

個setup 包,要求裝置傳回裝置描述符。

submit_control_msg 會原封不動的調用ohci_submit_control_msg。

這個函數很短,首先設定maxsize,由于pipe 為0,我們在usb_new_device

中定義了EP0 的進出包大小都是8 位元組。這裡的maxsize 就是8。

之後根據目前要操作的pipe 那頭是不是root hub 調用ohci_submit_rh_msg 或

者submit_common_msg。由于目前連的是root hub,調用ohci_submit_rh_msg。

74

參數和ohci_submit_control_msg 都是一樣的。下面看看這個setup 的msg 是

如何發到root hub 上的。注意這時setup_packet 的形參叫做cmd 了。

在接下來的大switch 中,case 比對上RH_GET_DESCRIPTOR,在這個case

中的switch中,case 比對0x01,代碼會給date_buf 賦一個預設的虛拟root hub

的内容,并把len 置為這個結構的長度。

在這個大switch 之後,如果data_buf 和目标位址不一緻,則這個結構體資料的

拷貝(注意長度)。這裡stat 預設為0,表示正常,如果不正常的話,至于這個虛

拟結構的内容在代碼中如何解釋, 用到再說。這個函數傳回之後, 回到

usb_control_msg 函數。

回去一看,根本就不判斷傳回值,可見對這個結果相當的自信,不會有事的。因

為和硬體沒有接觸。

usb_control_msg 這個函數最後看dev-<status,這個是在ohci_submit_rh_msg

中修改了值的。正常為0,傳回讀取的長度。

這裡的dev->descriptor 就是我們傳入那個緩沖區。用root_hub_dev_des 填充

的,這裡注意,目前的dev->descriptor 就是root_hub_dev_des 的位址值。

接着指派dev->devnum,這個值其實就是root_hub_dev_des 中指派的那個值。

這裡首先是1,接着遞增,在8089 筆記本上就是1 和2。

接着執行usb_set_address(dev)。

這個函數直接調用usb_control_msg,這個函數我們見過了,參數不同。看參數。

res=usb_control_msg(dev, usb_snddefctrl(dev),

USB_REQ_SET_ADDRESS, 0,

(dev->devnum),0,

NULL,0, USB_CNTL_TIMEOUT);

usb_snddefctrl(dev)這個對應的pipe0,這個值為0x80000000,和前面的那個

差别就是IN 成了OUT。type 從名字上看就是設定什麼位址而已。傳入的data

就是dev->devnum,是1。表示要把這個ohci 連接配接的root hub 叫做裝置1。接

着看。又回到ohci_submit_rh_msg,

這次要做的動作特别簡單, 就是把dev->rh.devnum 指派為傳入的dev

->devnum,就是1。僅此。

是以傳回應該也沒有什麼問題。

回到usb_new_device(),接着調用usb_get_descriptor(dev,

USB_DT_DEVICE, 0, &dev->descriptor, sizeof(dev->descriptor))。

這個函數我們前面已經調用過了。不過這次的size 參數和上次有不同。到

usb_control_msg 的層面,pipe 的值和前次調用也不同,成了0x80000180,

devnum 不同了,EP 是相同的。

usb_control_msg(dev, usb_rcvctrlpipe(dev, 0),

USB_REQ_GET_DESCRIPTOR, USB_DIR_IN,

(type << 8) + index, 0,

buf, size, USB_CNTL_TIMEOUT);

這次((pipe >> 8) & 0x7f) == gohci->rh.devnum,是以ohci_submit_control_msg

還是去調用ohci_submit_rh_msg。事實上,隻有你在usb 口上插了什麼東西,

才會調用submit_common_msg。因為那時才有除root hub 以外的usb device。

75

這次對于root bug 的操作又是get descriptor,實際上這次和上面一次的

get_descriptor 有任何差别嗎?别說大小,其實那個不導緻差别。

傳回後再次回到usb_new_device()。

注意,由于到現在為止的get descriptor 都是直接使用預設的值,是以這裡的廠

商号,産品号都是0。

接下來是這句,tmpbuf 是一個512 位元組的緩沖區。

usb_get_configuration_no(dev,&tmpbuf[0],0);

這個函數的定義如下:

config=(struct usb_config_descriptor *)&buffer[0];

result = usb_get_descriptor(dev, USB_DT_CONFIG, cfgno, buffer, 8);

這裡的cfgno 就是0。和前面的usb_get_descriptor 最大的不同是,這裡的type

域是USB_DT_CONFIG, 這導緻在調用到ohci_submit_rh_msg 中case

RH_GET_DESCRIPTOR 中的case 為0x02,按照注釋,這次的操作就是獲得

configuratio descriptor, 操作的實作很簡單, 就是把一個預定義的

root_hub_config_des 的結構體。

跳過一些錯誤處理,再執行下面一條,和前面的獲得device descriptor 一樣,這

種操作都是每次兩遍。

result = usb_get_descriptor(dev, USB_DT_CONFIG, cfgno, buffer, tmp);

實際上,很容易分析,這種兩次的操作是沒有意義的。但是如果這個get 操作是

去外部獲得的,就不同了。看看ohci 的協定,就知道了。第一次會發送指令(8

位元組),之後從裝置響應,這時主裝置再去真正獲得那個描述符。

這裡相當于模拟協定吧,maybe。

獲得config 之後就是剖析它,執行usb_parse_config(dev,&tmpbuf[0],0)

這個函數非常重要,我們一句句解析。記住我們輸入的參數。代碼如下:

int usb_parse_config(struct usb_device *dev, unsigned char *buffer, int cfgno)

{

struct usb_descriptor_header *head;

int index,ifno,epno;

ifno=-1;

epno=-1;

dev->configno=cfgno;

這個的值是0。

head =(struct usb_descriptor_header *)&buffer[0];

buffer 對應的就是root_hub_config_des,在檔案usb-ohci,c 中。

if(head->bDescriptorType!=USB_DT_CONFIG) {

。。。。。

錯誤處理,這裡我們不看

}

memcpy(&dev->config,buffer,buffer[0]);

76

填充dev->config.buffer 字段。

dev->config.wTotalLength=swap_16(dev->config.wTotalLength);

dev->config.no_of_if=0;

個别參數的微調。

index=dev->config.bLength;

這個len 是9。

head=(struct usb_descriptor_header *)&buffer[index];

現在要說的是這個root_hub_config_des 是由3 個部分組成的,有每個部分的第

一字段标記這部分的大小。前面處理的是第一個部分長度為9。第二部分是對

interface 的描述,長度為9,第三部分是對EP 的描述,長度為7。總長度在第

一部分的wTotalLength 中記錄。如注釋所說的,我們要處理其他的部分了。

while(index+1 < dev->config.wTotalLength) {

switch(head->bDescriptorType) {

case USB_DT_INTERFACE:

ifno=dev->config.no_of_if;

dev->config.no_of_if++;

memcpy(&dev->config.if_desc[ifno],&buffer[index],buffer[index]);

dev->config.if_desc[ifno].no_of_ep=0;

上面可以看出,如果找到一個interface,要動作的幾個參數就是config 的

no_of_if,if_desc 數組,其中if_desc 的no_of_ep 初始為0。

break;

case USB_DT_ENDPOINT:

epno=dev->config.if_desc[ifno].no_of_ep;

dev->config.if_desc[ifno].no_of_ep++;

memcpy(&dev->config.if_desc[ifno].ep_desc[epno],

&buffer[index],buffer[index]);

dev->config.if_desc[ifno].ep_desc[epno].wMaxPacketSize

=swap_16(dev->config.if_desc[ifno].ep_desc[epno].wMaxPacketSize);

break;

EP 和interface 的關系從這裡可以一覽無餘。一個interface 包含了n 個EP。所

以上面的複制都是對于config.if_desc[ifno]的結構體的指派。

default:

。。。

}

好,這個函數就解釋完了。

再回到usb_new_device。

接下的usb_set_maxpacket(dev)不過就是設定收發包的大小。注意這個雙重循

環實際執行一次,因為目前隻有一個interface 且那個interface 隻有一個EP。

接下來還是重複昨天的故事,get 以後是set,這次set configuratio。

中間的過程都熟悉了,看點關鍵的(ohci_submit_rh_msg 中)。

77

case RH_SET_CONFIGURATION: WR_RH_STAT (0x10000);

這個宏的定義如下:

#define WR_RH_STAT(x) writel((x), &gohci->regs->roothub.status)

writel 的定義就是指派而已。

#define writel(val, addr) *(volatile u32*)(((u32)(addr)) | 0xa0000000) = (val)

這些代碼都簡單,問題是為什麼要寫這個status。一個更讓我困惑的問題是,這

裡的寫為什麼要或上0xa0000000?難道這個位址是硬體映射到實體位址空間

的?看上去應該是的。列印出這個位址我們會覺得比較眼熟。在USB 這一節的

開始,有個bus_space_map 的函數,這個函數是處理usb 的寄存器到mem 空

間的映射,而現在發現這個位址就在那段空間内,是以至少可以确定gohci

->regs->roothub.status 的位址就是usb 的寄存器映射過來的。打出gohci

->regs的位址,就是或上0xb0000000 的usb 的membase位址。至于這個status

為0x10000 是什麼?也許是active 吧。

這個set configration 順利傳回之後,指派dev->toggle 的前兩個元素為0。

回到usb_new_device,清dev 的mf,prod,serial 三個成員。

接着執行usb_hub_probe(dev,0),檢測這個ohci 上的接口0 是不是root bug,

如果是,配置之。

檢查比較簡單,開始就是些參數的對比。從對比中可以産生這樣的了解:

root hub 是一個interface,

這個interface 隻有一個EP。

在這個probe 函數末尾有一個usb_hub_configure(dev)的調用。

這個用于hub 這個結構的注冊。

這個代碼首先占用hub_dev 數組的一項。之後調用

usb_get_hub_descriptor(dev, buffer, 4)

這個函數會調用老朋友ohci_summit_control_msg,這次會遇到的case 不同,

是case RH_GET_DESCRIPTOR | RH_CLASS,注意到buffer 傳回後是作為

usb_hub_descriptor 的結構體指針使用的,在data_buf 數組指派的時候就最好

對着usb_hub_descriptor 的定義看,就比較清楚了。

接着又非常傳統的再來一遍,接着把讀到的(代碼寫死的那些)賦給hub_dev

的desc 成員。

接着調整desc 中的部分成員,DeviceRemovable 和PortPowerCtrlMask 的初始

化。具體的功能不清楚。

status 那段跳過。接下來的函數usb_hub_power_on(hub)和供電有關,再接下

是每個port 的簡單配置。這些都挺重要的,有時間可以看看。

這個usb_new_device總算快到頭了,末尾有個老朋友,config_found。很簡單,

要看看這個ohci 上有沒有連接配接什麼usb 裝置。

配置的時候就确定了,ohci 的兒子就是usb。

config_found 我們都知道了,主要就是執行一個match 和attach,核心是裝置

的這兩個函數。對于usb 就是usb_match 和usb_attach。

usb_attach 主要代碼如下:

if(usb_storage_probe(dev,0,&usb_stor[usb_max_devs])) {

memset(&usb_dev_desc[usb_max_devs],0,sizeof(block_dev_desc_t));

78

usb_dev_desc[usb_max_devs].target=0xff;

usb_dev_desc[usb_max_devs].if_type=IF_TYPE_USB;

usb_dev_desc[usb_max_devs].dev= usb_max_devs;

usb_dev_desc[usb_max_devs].part_type=PART_TYPE_UNKNOWN;

usb_dev_desc[usb_max_devs].block_read=&usb_stor_read;

if(usb_stor_get_info(dev, &usb_stor[usb_max_devs],

&usb_dev_desc[usb_max_devs])){

usb_max_devs++;

return 1;

}

}

從代碼上看,這個過程是首先探測usb 儲存設備是否存在,如果存在就填充

usb_stor 的一項, 之後初始化usb_dev_desc 的那一項, 最後調用

usb_stor_get_info 完成探測。

先看第一個函數usb_storage_probe,我們現在以沒有外接任何usb 儲存設備的

例子來看。很快的, 在開頭的if 條件上就傳回0 了, 因為的descriptor 的

bDeviceClass 值為9,不是0,是以傳回0。整個過程也就結束了。

現在我們以在NEC 接出來的usb 口插了一個U 盤為例走流程。

首先要搞清楚插了U 盤,ohci 是如何知道的。前面我們跳過了,現在回頭。

在usb_new_device 的末尾有一句usb_hub_probe,這個函數會判斷目前裝置

是不是usb 的hub,如果是,調用usb_hub_configure,在初始化完一個hub 結

構之後。hub 嘛,就是一個疙瘩上好幾個洞。在給每個port 上電以後。探測每

個port 的狀态。

如果某個usb 口接了usb 裝置(除了被mask 掉的),那麼接着就執行

usb_get_port_status(dev, i + 1, &portsts)

這個會從老路調用ohci_submit_control_msg(),不同的是比對下面的case

case RH_GET_STATUS | RH_OTHER | RH_CLASS:

*(u32 *) data_buf = m32_swap (RD_RH_PORTSTAT); OK (4);

其中的RD_RH_POSTSTAT 會讀取root hub 中那個port 的狀态,并傳回。如

果有插個什麼東西,這個傳回值的portchange 為1,表示connection,有挂usb

裝置。其實從這個status 的擷取中我們可以确定,這個寄存器就是映射到dev

->reg 中的。從代碼我們可以看到,這個portchange 的值表征的資訊是非常豐富

的,具體見代碼的不同條件分支。

由于發現了有usb 挂載,執行usb_hub_port_connect_change(dev, i),初始化

挂載在root hub 上的那個裝置,代碼如下:

一開始重讀status,代碼中指出了如何區分低速和高速裝置。代碼略。

usb_clear_port_feature(dev, port + 1,

USB_PORT_FEAT_C_CONNECTION);

清狀态寄存器,以免下次重複探測到。

79

某些端口的比較特殊,不允許連接配接。代碼略。

wait_ms(200);

if (hub_port_reset(dev, port, &portstatus) < 0) {

。。。

}

沒細看實作,相當于使能???

//wait_ms(200);

wait_ms(400);

assert(dev->hc_private!=NULL);

usb=usb_alloc_new_device(dev->hc_private);

usb->slow = (portstatus & USB_PORT_STAT_LOW_SPEED) ? 1 : 0;

dev->children[port] = usb;

usb->parent=dev;

這裡的初始化比較明顯了,注冊一個新的usb 裝置,并和dev 連接配接上。dev 總算

有兒子了。并且usb 的hc_private 和其父是共享的。

usb->port = port;

if (usb_new_device(usb)) {

。。。

}

}

看到了吧,又遞歸了。我們就調用usb_new_device來處理這個動态插入的家夥。

這個usb_new_device 和前面的那次調用有很大的不同,首先這個裝置是接在

root hub 上的,是一個我們所無法預知的裝置(U 盤?usb 滑鼠?usb 鍵盤),在

代碼上,這個裝置的一切都要通過真正的探測,而不是像前頭有時是走過場,用

預定義的資料敷衍了事。是以要重新走一遍usb_new_device 的流程。

Let’s go !

首先看usb_get_descriptor 這個調用,前面ochi 調用這個函數的時候,就是給

ohci->descriptor 指派為一個預設的結構體指針。看看這次有什麼不同。

最大的不同是對ohci_submit_rh_msg的調用都變成了submit_common_msg.。

一看内容,這兩個函數完全不同。代碼雖長,一行行看吧。如下:

if(pipe != PIPE_INTERRUPT) {

gohci->transfer_lock ++;

gohci->hc_control &= ~OHCI_CTRL_PLE;

writel(gohci->hc_control, &gohci->regs->control);

}

control 中的PLE 是periodic list enable 的縮寫。在ohci 中,資料傳輸的類型可

分為四類:interrupt,isochronous,control,bulk。interrupt 和isochronous 是

周期性的。ohci 對于list 的管理還是比較詳細的,這裡的periodic list 就是指

80

interrupt 和isochronous 的任務list 吧。代碼中是disable 的。

if (sohci_submit_job(dev, pipe, buffer, transfer_len, setup, interval) < 0) {

。。。

}

這個函數和ohci 的核心機制相關,極為重要。大緻意思就是把這個job 化為一

個或者幾個TD,鍊到ED 中,等待被執行。

#define BULK_TO 500

if (usb_pipetype (pipe) == PIPE_BULK)

timeout = BULK_TO;

else

timeout = 2000;g

timeout *= 40;

bulk 的timeout 值明顯要小一些。

while(--timeout > 0) {

if(!(dev->status & USB_ST_NOT_PROC)){

break;

}

delay(200);

hc_check_ohci_controller(gohci);

}

這個函數基本功能是查詢done list。

如果done list 非空,就看這些幹完的TD 是什麼,并把它從那個ED 的TD 鍊中

unlink 掉。并在dl_td_done_list 中修改那個td對應的dev 的status。上面的while

就是等這個dev 的status 改變。當然在這個dev 改變的同時(或者沒改變時)

其它的dev 的status 也可能改變,隻要在td done list 有它的事完了。

剩下的所有代碼略過。

最後插入的usb 裝置執行config_attach,基本是調用這個ohci_common_msg。

我們調用pci 裝置樹上的兒子usb 的match 和attach 函數去了。

有興趣的可以接着看usb_storage.c 中的代碼。我走了。

回到init_net

流水落花春去也,換了人間。

下面的代碼不會那麼讓人頭大了。當然我的頭本來就挺大的。接着的代碼如下:

for (pdev = pdevinit; pdev->pdev_attach != NULL; pdev++) {

if (pdev->pdev_count > 0) {

(*pdev->pdev_attach)(pdev->pdev_count);

}

81

}

pdevinit 定義如下:

struct pdevinit pdevinit[] = {

{ loopattach, 1 },

{ 0, 0 }

};

pdevinit 的結構體定義如下:

struct pdevinit {

void (*pdev_attach) __P((int));

int pdev_count;

};

可見上面按個for 循環其實就是調用loopattach(1)而已。loop 是最簡單的網絡收

發模型,在pmon 下沒有什麼意義,不看了。

接下來執行ifinit()。看得出來,是網絡系統的初始化,if 就是interface的縮寫吧。

void ifinit()

{

register struct ifnet *ifp;

for (ifp = ifnet.tqh_first; ifp != 0; ifp = ifp->if_list.tqe_next)

if (ifp->if_snd.ifq_maxlen == 0)

ifp->if_snd.ifq_maxlen = ifqmaxlen;

這個ifnet 是個全局變量,功能是描述所有網卡的ifnet 結構體的連結清單頭。實際上

大部分情況下我們都隻有一個網卡。在pmon 下沒有無線網卡的驅動,雖然會被

探測到,但并不會被加入到裝置連結清單中。

實際上對于8089 的小本,在pmon 下唯一的網絡裝置就是8169。

if_slowtimo(NULL);

}

init_net()總算執行完畢,回到dbginit()。

執行histinit(),初始化指令的曆史記錄,這個曆史記錄的功能還是很有用的。

這個函數就一句話: histno=1;表示現在曆史清單中有一個指令(實際上是0)。

接着執行syminit(),實際上就是一行代碼:

defsyms ((u_int32_t)sbrk(0) + 1024, memorysize |

(CLIENTPC & 0xc0000000), CLIENTPC);

執行的函數代碼如下:

defsyms (u_int32_t minaddr, u_int32_t maxaddr, u_int32_t pc)

{

82

extern int start[];

defsym ("Pmon", (unsigned long) start);

if (minaddr != ~0)

defsym ("_ftext", minaddr);

if (maxaddr != 0)

defsym ("etext", maxaddr);

defsym ("start", pc);

}

defsym()函數把這些變量注冊在一個hash 數組中。至于這裡的CLIENTPC,不

知道是幹什麼的。

接下來執行initial_sr |= tgt_enable (tgt_getmachtype ());

tgt_getmachtype ()就是調用md_cputype,定義在arch/mips/mips.S,如下:

LEAF(md_cputype)

mfc0 v0, COP_0_PRID

j ra

nop

END(md_cputype)

就是傳回prid 而已。

tgt_enable 也隻有一句話:return(SR_COP_1_BIT|SR_FR_32|SR_EXL);

就是設定協處理器1 有效,可以使用32 個急存器,重新設定例外處理為啟動模

式。現在又看不出調用tgt_getmachtype ()有什麼用了。

接下來執行:

ioctl(STDIN, TCGETA, &consterm);

、、、、、、、、、、、、、、、、、、、、、、、、、

、、、、、、、、、、、、、、、、

、、、、、、、、、、

tgt_machprint()會調用md_cpuname(),判斷列印出cpu 的類型,其實前面就算

出來了,沒有使用。無語中。。。。。

tgt_machprint()列印些cache 的情況。

接下來執行md_clreg(NULL),函數就一句: tf = cpuinfotab[whatcpu],這個

cpuinfotag 數組在調用dbginit 前初始化cpuinfotab[0] = &DBGREG;其實

DBGREG 隻是空的一段而已。

接着執行md_setpc(NULL, (int32_t) CLIENTPC),就是把cpuinfotab[0]->pc 賦

值為CLINETPC。md_setsp(NULL, tgt_clienttos ())看名字是設定sp 寄存器,

sp 的值是256MB-64 位元組的地方。别忘了棧是向下生長的。

接着調用DevicesInit();

83

DevicesIni t

函數功能很清楚,就是挨個查詢已經注冊的裝置,所有已知裝置都在alldevs 這個

連結清單中。如果是disk 裝置,就配置設定一個DeviceDisk 結構體,填充名字後插入以

gDivice 為首的連結清單中。一般都是一個硬碟,名字就是wd0。DevicesInit()的重

頭戲是_DevPartOpen(dev_disk, dev_disk->device_name),代碼如下:

int cnt = 0;

char path[256];

strcpy(path, dev_name);

if (strncmp(dev_name, "/dev/", 5) != 0)

{

sprintf(path, "/dev/disk/%s", dev_name);

}

對于隻有一個硬碟的情形,現在path 的内容是/dev/disk/wd0。

fd = open(path, O_RDONLY | O_NONBLOCK, 0);

open()函數定義在../lib/libc/open.c。暫時可以這麼了解這個函數:open 就是看

看那檔案是否存在,如果存在,就配置設定一個檔案id,這個id 是一個結構題數組

的下标,那個結構體用來儲存相關的資訊。

if (fd < 0)

{

。。。// error when open

}

cnt = dev_part_read(fd, &dev->part);

現在開始讀取disk 上的分區,dev->part 傳入的目的是存放讀取出的分區表

if (cnt <= 0)

{

printf("no partitions\n");

close(fd);

return -1;

}

close(fd);

這個函數主要調用dev_part_read(),讀取硬碟上的分區表。下面看看代碼。

int cnt;

DiskPartitionTable* table = NULL;

DiskPartitionTable* p;

int number = 0;

if (ppTable != NULL)

84

{

*ppTable = NULL;

}

cnt = read_primary_part_table(fd, 0, &table);

這個函數将會讀取fd 代表的disk 上分區表寫到table 這個位址去。

if (cnt <= 0)

{

return 0;

}

number = cnt;

p = get_externed_part(table);

檢測一下有沒有擴充分區。

if (p != NULL)

{

cnt = dev_logical_read(fd, p);

如果存在擴充分區,那麼就把他加到分區表中

number += cnt;

number 表示分區的個數(主分區+擴充分區)

}

if (ppTable != NULL)

{

*ppTable = table;

}

讀取主分區的函數read_primary_part_table()函數的主題如下:

if ((read(fd, leadbuf, SECTOR_SIZE)) != SECTOR_SIZE)

{

free(leadbuf);

return 0;

}

現在硬碟上的第一個扇區被放到首位址為leadbuf的緩沖區中,關于這個首扇區,

有必要先解釋一下:

MBR 中的分區資訊在偏移位址01BEH--01FDH 的64 個位元組,為4 個分區項内

容(分區資訊表)。FDISK 對一個磁盤劃分的主分區可少于4 個,但最多不超過

4 個。每個分區表的項目是16 個位元組。

分區表的内容格式如下:

第0 位元組是否為活動分區,是則為80H,否則為00H

第1 位元組該分區起始磁頭号

第2 位元組該分區起始扇區号(低6 位)和起始柱面号(高2 位)

第3 位元組該分區起始柱面号的低8 位

第4 位元組檔案系統的類型标志。0x83 表示ext2/ext3,0x5 表示擴充分區。

85

第5 位元組該分區結束磁頭号

第6 位元組該分區結束扇區号(低6 位)和結束柱面号(高2 位)

第7 位元組該分區結束柱面号的低8 位

第8~11 位元組相對扇區号,該分區起始的相對邏輯扇區号,高位在後低位在前

第12~15 位元組該分區所用扇區數,高位在後,低位在前。

以我自己的機子為例,120G 的硬碟。分區情況如下:

[[email protected] ~]# fdisk -l

Disk /dev/sda: 120.0 GB, 120034123776 bytes

255 heads, 63 sectors/track, 14593 cylinders

Units = cylinders of 16065 * 512 = 8225280 bytes

Device Boot Start End Blocks Id System

/dev/sda1 * 1 3951 31736376 83 Linux

/dev/sda2 3952 7966 32250487+ 83 Linux

/dev/sda3 7967 10643 21503002+ 83 Linux

/dev/sda4 10644 14593 31728375 5 Extended

/dev/sda5 10644 12644 16072969+ 83 Linux

/dev/sda6 12645 14593 15655311 83 Linux

前面3 個是主分區,第四個是擴充分區,包含了兩個邏輯分區。

在os 下讀取mbf 的指令為dd if=/dev/sda of=part_table bs=1 count=512,在龍

芯上把sda 改為hda 就行了。

pmon 隻使用這個首扇區中的第446~510 位元組之間64 位元組的内容,使用

hexdump 看到内容如下:

00001b0 0000 0000 0000 0000 a2e0 624a 0000 0180

00001c0 0001 fe83 ffff 003f 0000 8470 03c8 fe00

00001d0 ffff fe83 ffff 84af 03c8 34ef 03d8 fe00

00001e0 ffff fe83 ffff b99e 07a0 3835 0290 fe00

00001f0 ffff fe05 ffff f1d3 0a30 45ee 03c8 aa55

0000200

排版後我們關心的4 個主分區内容如下:

1 0 3 2 5 4 7 6 9 8 11 10 13 12 15 14

0180 0001 FE83 FFFF 003F 0000 8470 03C8

FE00 FFFF FE83 FFFF 84AF 03C8 34EF 03D8

FE00 FFFF FE83 FFFF B99E 07A0 3835 0290

FE00 FFFF FE05 FFFF F1D3 0A30 45EE 03C8

好,現在開始解析這個分區表吧。

首先看第4 個位元組,前面3 個主分區都是83H,表明都是ext3 檔案系統,第四

個主分區是0x5,表明是擴充分區,至于擴充分區如何處理,等會看了代碼就知

道了。第8~11 個位元組的内容是分區的起始扇區,比如第一個分區的起始扇區

為0x37,第二個分區的起始扇區為0x3c884af,是以第一個分區的大小就是

0x3c88470(0x3c884af-0x370)個扇區。我們看到fdisk -l的時候的sda1 描述為:

Device Boot Start End Blocks Id System

86

/dev/sda1 * 1 3951 31736376 83 Linux

大小為31736376(0x1e44238)個塊,在建立檔案系統的時候,mkfs.ext3 允許指

定塊大小,預設為1KB,也就是兩個扇區的大小,是以這個分區就有63472752

(0x3c88470)個扇區,和分區資訊中第12~15 個位元組的表示是一緻的。

差不多了,看代碼吧。

//search the partion table to find the partition with id=0x83 and 0x05

for (cnt = 0, i = 446; i < 510; i += 0x10)

{

tag = leadbuf[i + 4];

sec_off = get_logical_part_offset(leadbuf + i);

size = get_part_size(leadbuf + i);

if (tag == 0 && sec_off == 0 && size == 0)

{

id++;

continue;

}

part = (DiskPartitionTable *)malloc(sizeof(DiskPartitionTable));

if (part == NULL)

{

continue;

}

memset(part, 0, sizeof(DiskPartitionTable));

下面開始填充分區的各種資訊

part->tag = tag;

part->id = id;

part->bootflag = 0;

part->sec_begin = sec_off;

對于擴充分區,sec_begin 這個成員是非常關鍵的。

part->size = size;

part->sec_end = part->size + part->sec_begin;

part->Next = NULL;

part->logical = NULL;

part->fs = NULL;

#ifndef DEBUG

get_filesystem(fd, part);

#endif

把這個探測到的分區插入分區表中。

part_node_insert(table, part);

87

cnt++;

id++;

}

以上是主分區的探測,這個函數傳回dev_part_raad()後,開始探測擴充分區并

注冊邏輯分區,代碼如下:

p = get_externed_part(table);

是否為擴充分區的判斷如下,

#define IS_EXTENDED(tag) ((tag) == 0x05 || (tag) == 0x0F || (tag) == 0x85)

可見不是隻有0x5 才表明是擴充分區哦。

if (p != NULL)

{

cnt = dev_logical_read(fd, p);

遇到了擴充分區,開始讀取邏輯分區

number += cnt;

}

dev_logical_read()函數的定義如下:

static int dev_logical_read(int fd, DiskPartitionTable* extended)

{

DiskPartitionTable* table = NULL;

DiskPartitionTable* p1;

__u32 base = 0;

int cnt;

int id = 5;

base = extended->sec_begin;

看到了吧,擴充分區的邏輯分區資訊在base 扇區中存放着呢。

p1 = extended;

while (1)

{

和注冊主分區最多隻有四個不同不同,擴充分區的邏輯分區還可以是擴充分區,

是以隻能用while 了

table = NULL;

cnt = read_logical_part_table(fd, id, base, &table);

這個read_logical_part_table 和讀取主分區表的方法幾乎一樣,就不進去看了。

if (cnt <= 0)

{

return 0;

}

p1 = remove_extended_part(&table);

88

在pmon 中分區表中删除這個擴充分區,擴充分區和普通分區不用,實際上就是

個皮包公司,不行,你有本事mount 擴充分區試試。

#ifndef DEBUG

get_filesystem(fd, table);

#endif

part_node_insert(&extended->logical, table);

插入這些探測到的新分區。

if (p1 == NULL)

{

break;

}

base = extended->sec_begin + p1->sec_begin;

free(p1);

id++;

}

return id - 5 + 1;

}

這些函數都傳回後,pmon會把包含了所有分區表資訊的描述disk 的DeviceDisk

結構體指針加入到gDivece 連結清單中。

好了,分區表就這麼點事,算是講完了。

open 函數

上面我們已經遇到了open 函數,不過當時沒有進入代碼,現在我們就以預設啟

動時要執行的open(“(wd0,0)/boot/boot.cfg”,O_RDONLY | O_NONBLOCK)為例

分析這個函數。

open()函數定義在../lib/libc/open.c

int open(const char *filename, int mode)

{

char *fname;

char *dname;

int lu, i;

int fnamelen;

首先是一些基本參數的處理

fnamelen = strlen(filename);

fname = (char *)malloc(fnamelen+1);

memcpy(fname, filename, fnamelen+1);

for (lu = 0; lu < OPEN_MAX && _file[lu].valid; lu++);

89

至此,lu 表示目前可用的檔案描述符id。

if (lu == OPEN_MAX) {

errno = EMFILE;

free(fname);

return -1;

}

ok,lu 有效。這個lu 可是非常重要。

接下來根據file 的開始符号作不同的處理。比如al 我們一般設定為/

dev/fs/[email protected]/boot/vmlinux,就進入第一個if,tftp 那種情形就進入第二種情

形。我們的例子是(wd0,0)開始的,我們跳到那個地方去。

dname = (char *)fname;

if (strncmp (dname, "/dev/", 5) == 0) {

dname += 5;

i = __try_open(fname, mode, dname, lu, 0);

free(fname);

return i;

}

else if (strpat(dname, "*ftp:/

break;

}

else if (fstype != FS_NULL && fsp->fstype == fstype) {

if(fsp->open && (*fsp->open)(lu, fname, mode, 0) == lu)

break;

}

}

if (fsp) {

如果已經打開有效的檔案,那麼就占用這個檔案資源吧

_file[lu].valid = 1;

_file[lu].posn = 0;

_file[lu].fs = fsp;

return(lu);

}

open()本身就是這些。我們順着執行流到diskfs 的open 函數去吧。調用參數為

diskfs_open(disk_fs(檔案描述符,”(wd0,0)/boot/boot.cfg”),隻讀非阻塞,0)。

open 函數到現在為止,基本的調用是

open()->

try_open()->

diskfs_open()->

函數調用基本傳回後都沒有什麼動作了,隻是些錯誤檢測而已。

diskfs_open()函數代碼在../pmon/fs/diskfs.c 中,如下:

static int diskfs_open (int fd, const char *fname, int mode, int perms)

{

DiskFile *dfp;

char filename[256];

strcpy(filename, fname);

dfp = GetDiskFile(fname, filename);

這個GetDiskFile()用來解析這個fname,分離出3 個重要内容,稍後講述

if (dfp == NULL)

{

91

return -1;

}

_file[fd].posn = 0;

_file[fd].data = (void *)dfp;

if(dfp->dfs->open)

return ((dfp->dfs->open)(fd, filename, mode, perms));

這個就是根據具體的檔案系統的open 函數,去打開檔案,我們這是ext2,

dfp->dfs 就是ext2 檔案系統的描述指針,執行的打開函數就是ext2_open

return -1;

}

diskfs_open主要調用兩個函數,一個是GetDiskFile,另一個是檔案系統的open

函數,先來看GetDiskFile。

static DiskFile* GetDiskFile(const char* fname, char* filename)

{

DeviceDisk* pdev;

DiskPartitionTable* part;

DiskFile *dfp;

char *dname;

char *fsname = NULL;

char *devname = NULL;

int i;

char* p;

dname = (char *)fname;

if (strncmp (dname, "/dev/fs/", 8) == 0) {

。。。。。

}else if (*dname == '(') {

我們進入的就是這個路徑。

devname = dname + 1;

p = strchr(devname, '/');

strchr 這個函數的功能是在devname 字元串中查找’/’,傳回第一個’/’的位置。

if (p == NULL)

{

strcpy(filename, "");

}

else

{

strcpy(filename, p + 1);

現在filename 就是”boot/boot.cfg“了

}

92

p = strchr(dname, ',');

查找逗号,目前dname 是“wd0,0)”,有逗号的。

if (p == NULL)

{

printf("device format is error[%s]\n", dname);

return NULL;

}

*p = '\0';

在字元串中把逗号變成’\0’,現在devname 實際就是字元串”wd0”。

pdev = FindDevice(devname);

FindDevice 就是從gDevice 的連結清單中查找有沒有這個名字的disk 裝置,這個鍊

表是在DevicesInit 中建立的,一般就隻有“wd0”這個裝置。代碼就不看了。

if (pdev == NULL)

{

printf("Couldn't find device [%s]\n", devname);

這個錯誤我們不少見吧,比如你不插硬碟就會見到這條語句。

return NULL;

}

fsname = p + 1;

p = strchr(fsname, ')');

if (p == NULL)

{

return NULL;

}

*p = '\0';

現在fsname 就是字元串”0”

if (isspace(*fsname))

{

return NULL;

}

part = FindPartitionFromID(pdev->part, atoi(fsname) + 1);

FindParttionFromID()第一個傳入參數是那個裝置的分區表,後一個參數是

第x 個分區,傳回值是那個分區的描述符的指針。代碼就不涉及了。

if (part == NULL || part->fs == NULL)

{

printf("Don't find partition [%s]\n", devname);

return NULL;

}

} else {

93

.。。。

}

dfp = (DiskFile *)malloc(sizeof(DiskFile));

if (dfp == NULL) {

fprintf(stderr, "Out of space for allocating a DiskFile");

return NULL;

}

好了,到現在為止,原先的(wd0,0)/boot/boot.cfg 已經被完全拆分了。pdev

現在是存儲媒體的指針,是個DeviceDisk 結構體指針,part 是檔案所在分

區的指針,是個DiskPartitionTable 結構體指針。這些拆分都被保留在dfp

的結構體中,如下:

dfp->devname = pdev->device_name;

dfp->dfs = part->fs;

dfp->part = part;

dfp 是DiskFile 的結構體指針,描述了一個裝置上檔案的基本資訊。這個結

構體最終是File 結構體的一個data 參數。

return dfp;

}

GetDiskFile 函數傳回後回到disk_open(),執行如下代碼:

if (dfp == NULL)

{

return -1;

}

_file[fd].posn = 0;

_file[fd].data = (void *)dfp;

好了,現在這個_file[fd]的描述基本都全了,File 結構體的定義如下:

typedef struct File {

short valid;

unsigned long posn;

FileSystem *fs;

void *data;

} File;

其中的valid 和fs 是在__try_open()函數中就初始了,現在這個File 結構雖然填

滿了,但是夠不夠呢?如果要讀這個檔案的内容,到哪兒去讀呢?現在通過data

結構體隻能知道那個檔案所在硬碟、分區、分區檔案系統而已。接下來是檔案系

統層面的解析。最終我們是要知道那個檔案到底在那個分區的哪個地方。由于在

龍芯上預設的檔案系統都是ext3 的,ext2 和ext3 的結構是一樣的,下面的檔案

系統的打開函數就會調用ext2_read()。

if(dfp->dfs->open)

return ((dfp->dfs->open)(fd, filename, mode, perms));

return -1;

現在看diskfs_open 最後一個調用的函數ext2_open()。檔案系統相關的代碼在..

94

/pmon/fs 下,ext2 的操作函數在../pmon/fs/ext2fs.c。ext2_open()代碼如下:

int ext2_open(int fd, const char *path, int flags, int mode)

{

char *str;

DiskFile* df;

df = (DiskFile *)_file[fd].data;

str = path;

以前面的打開boot.cfg 檔案為例,這個path 是字元串”boot/boot.cfg”,(wd0,0)

是在GetDiskFile 中剝離的。

if (*str == '/')

{

str++;

}

devio_open(fd, df->devname, flags, mode);

這個devio_open 會調用硬體的驅動wdopen,我們暫時不深入代碼,暫時可以理

解為看看那個裝置是否存在。稍後再解讀。反正沒判斷傳回值,不影響執行流。

if (!(ext2_load_linux(fd, str)))

{

return fd;

}

ext2_load_linux()是非常功利的,首先就問是不是可以啟動。動機相當不純呀。

馬上看看。

return -1;

}

ext2_open()還有兩個函數調用沒解釋,我們先看ext2_load_linux,再看和硬碟

驅動相關的devio_open()。這裡我不準備進到檔案系統的代碼中去,因為ext2

檔案系統在很多書中都有專門描述,比如《深入了解Linux 核心》。

ext2_load_linux()函數代碼還是在ext2fs.c 中定義。

static int ext2_load_linux(int fd, const unsigned char *path)

{

unsigned int inode;

char* p;

if (read_super_block(fd))

return -1;

首先讀取超級塊。超級塊儲存了對一個檔案系統的許多重要描述。這個讀超

級塊的調用會初始化許多重要的全局變量。

p = path;

if (path == NULL || *path == '\0')

{

p = "/";

95

}

memset(&File_dirent, 0, sizeof(ext2_dirent));

if ((inode = find_inode(fd, EXT2_ROOT_INO, p, &File_dirent)) == 0)

{

return -1;

}

find_inode()函數會傳回描述我們要找的那個檔案的inode 編号。并填充

File_dirent 這個結構。

if (File_dirent.file_type == EXT2_FT_DIR)

{

printf("%s is directory!\n", p);

return 0;

}

如果我們要引導的内容是目錄,報錯。也就是說我們open 的隻能是一個具

體的檔案,而能是一個目錄。

if (ext2_get_inode(fd, inode, File_inode))

{

return -1;

}

根據inode 号,把我們要讀的檔案的inode 内容讀到File_inode 中。

return 0;

}

這裡解釋一點你可能的疑惑:Fiel_inode 和File_dirent 都是個全局變量,也就是

說打開哪個檔案,這些個結構體就表示那個檔案的相關内容,是以在pmon 下不

要在一個檔案使用的過程中同時使用

如果一切正常,那麼傳回0,表示open 成功。

下面來講述devio_open(),這個函數的後兩個參數flags 和mode 在pmon 下都

沒有使用。事實上,pmon 下通過檔案系統通路的檔案都是隻讀的。

下面看看devio_open 到底幹了些什麼。

int devio_open(int fd, const char *name,int flags,int mode)

{

int mj;

u_int32_t v;

dev_t dev;

struct biodev *devstat;

char strbuf[64], *strp, *p;

dev = find_device(&name);

對于我們一直使用的那個例子,open(wd0,0)/boot/boot.cfg,這裡的name 為

“wd0”。這個傳回值dev 是wd0的裝置号,高8位是主裝置号,低8位是次裝置。

96

這個主裝置号的來源非常有意思。這個name 傳回後,正常情況下為空。

find_device()進一步沒有調用函數,代碼也很清楚,有興趣的可以去看看。

if(dev == NULL || *name == '/') {

errno = ENOENT;

return -1;

}

mj = dev >> 8;

devstat = &opendevs[fd];

opendevs 是對裝置的一些描述,五個成員在下面都被初始化。

devstat->devno = dev; //裝置号

devstat->offs = 0;

devstat->base = 0;

devstat->maptab = 0;

devstat->end = 0x10000000000;

if (*name == '@') {

。。。

}

else if (*name != '\0') {

。。。

}

curproc->p_stat = SRUN;

看到這裡,不得不說pmon 作為一個bios,是個比較委屈的用場,它本來是一個

核心,是以有程序的概念,其實作為一個bios,是不必要把事情搞這麼複雜的。

errno = (*devswitch[mj].open)(dev, 0, S_IFCHR, NULL);

對于wd0,這裡的open 就是調用wdopen()。等會再看。

curproc->p_stat = SNOTKERN;

if(errno) {

return -1;

}

return(fd);

}

可見devio_open()的函數主要就做兩件事,一個是opendevs[fd]的初始化,另一

個就是調用主裝置對應的open 函數,對于硬碟,就是wdopen()。

好,現在就來看wdopen(dev, 0, S_IFCHR,NULL),代碼在../sys/dev/ata/wd.c

int wdopen(dev_t dev, int flag, int fmt, struct proc *p)

{

struct wd_softc *wd;

int unit, part;

int error;

97

unit = WDUNIT(dev);

得到次裝置号

wd = wdlookup(unit);

通過次裝置号找到我們要打開的裝置,由于是wdlookup,就不用主裝置号了。

。。。。。錯誤處理。。。。。

if (wd->sc_dk.dk_openmask != 0) {

注釋說了,這個if 成立表示如果這個裝置已經被打開了

if ((wd->sc_flags & WDF_LOADED) == 0) {

error = EIO;

goto bad3;

}

} else {

如果這個裝置是第一次被打開

if ((wd->sc_flags & WDF_LOADED) == 0) {

wd->sc_flags |= WDF_LOADED;

wd_get_params(wd, AT_WAIT, &wd->sc_params);

這個wd_get_params()會硬碟發出具體的指令,驅動硬碟并獲得作出相應的回

應。由于比較多細節,且那些結構體的成員較多,有興趣的可以自行閱讀。這裡

這個指令的含義上獲得硬碟的一些常用資訊,填充wd->sc+params這個結構體。

。。。。。

part = WDPART(dev);

switch (fmt) {

case S_IFCHR:

wd->sc_dk.dk_copenmask |= (1 << part);

标記那個分區被打開了

break;

case S_IFBLK:

wd->sc_dk.dk_bopenmask |= (1 << part);

break;

}

wd->sc_dk.dk_openmask =

wd->sc_dk.dk_copenmask | wd->sc_dk.dk_bopenmask;

。。。。

return 0;

98

bad:

。。。。

return error;

}

好了,wdopen 就算到這裡了,由于對于這些細節大部分同志沒有了解的必要,

就這樣吧。當然最重要的原因是:我也不了解,

open()->

try_open()->

diskfs_open()->

GetDiskFile()->

ext2_open()->

devio_open()->

find_device()

wdopen()->

ext2_load_linux()->

read_super_block()

find_inode()

ext2_get_inode()

l oad 核心

龍芯論壇常有人問pmon 的引導原理,現在我們就看看引導到底是如何實作的。

首先既然引導核心,就首先有個核心所在位置的辨別,早期的pmon 使用al 這

個環境變量表示欲引導的核心,典型的al 為/dev/fs/[email protected]/boot/vmlinux,

現在的pmon 在保留這個模式的同時,引入了類似grub 中的menu.lst 的形式。

就是把欲引導的核心和傳給核心的參數以一定的格式放在boot.cfg的檔案中,這

個boot.cfg 檔案放在第一硬碟第一分區的根目錄下或者boot 目錄下。

先回顧一下執行的流程。在可惡的init_net 執行完以後,代碼會調用一個重要的

函數DevicesInit()用于初始化分區,這個函數我們之前已經分析了。

pmon 的引導相關代碼如下(基于8089 筆記本吧,删除了無關代碼):

switch (get_boot_selection()){

get_boot_selection()函數會接收你的按鍵,根據按下的不同的鍵進入不同的分

支。這個函數中調用了ioctl(),這個函數非常重要,後面會列出專門一節講述。

case TAB_KEY:

。。。

break;

case U_KEY:

。。。

break;

99

case NO_KEY:

case ENTER_KEY:

假如你沒有任何的按鍵操作,get_boot_select()就傳回NO_KEY。

if (!load_menu_list())

如果沒有設定bootdev 這個環境變量, load_menu_list()按照尋找(wd0,0)

/boot.cfg,(wd0,0)/boot/boot.cfg 的順序尋找啟動資訊,如果找到合适的可啟動

的核心,那麼就會run 那個核心,這個函數就一去不複返。這個過程的代碼和從

al讀取啟動核心位置的本質是一樣的,我準備隻分析從al環境變量讀取的情形。

{

s = getenv ("al");

在沒有boot.cfg 這種啟動選擇以前,一般都會設定al 這個環境變量用于選擇要

啟動的核心。同時會設定karg 這個變量作為g 的附帶參數。

if (s != 0){

autoload (s);

好,稍後就以s 等于/dev/fs/[email protected]/boot/vmlinux 為例來介紹這個autoload。

和那個load_boot_menu一樣,如果不出意外的話,這個函數也是一去不複返的。

} else {

vga_available = 1;

printf("[auto load error]you haven't set the kernel path!\n");

}

}

break;

}

}

好,現在看看autoload()函數。

static void autoload(char *s)

{

char buf[LINESZ] = {0};

char *pa = NULL;

char *rd;

if(s != NULL && strlen(s) != 0) {

vga_available = 0;

rd= getenv("rd");

這個變量平時很少設定,initrd 在龍芯上也不常用,我隻在一次網絡安裝debian

系統時使用過這個指令。

if (rd != 0){

sprintf(buf, "initrd %s", rd);

do_cmd(buf);

}

100

if (buf[0] == 0) {

strcpy(buf,"load ");

strcat(buf,s);

}

do_cmd(buf);

對于預設的al 來說,buf 就是字元串’load /dev/fs/[email protected]/boot/vmlinux“。

我們稍後會詳細分析這個do_cmd。這裡

if (pa == NULL || pa[0] == '\0')

pa=getenv("karg");

獲得karg 的設定。

strcpy(buf,"g ");

if(pa != NULL && strlen(pa) != 0)

strcat(buf,pa);

else

strcat(buf," -S root=/dev/hda1 console=tty");

正常的,我們這裡的buf 是“g console=tty root=/dev/hda1 no_auto_cmd”

delay(10000);

do_cmd (buf);

vga_available = 1;

}

}

可見,autoload 實際上就是兩個do_cmd 的函數調用,

do_cmd(“load /dev/fs/[email protected]/boot/vmlinu’)和

do_cmd(“g console=tty root=/dev/hda1 no_auto_cmd’)

簡單的說,第一個調用會把那個vmlinux 檔案放到記憶體上某個位置,g 會執行那

個檔案。

do_cmd()函數的解析比較繁瑣,最終會執行stat = (CmdTable[i].func) (ac, av),

先看do_cmd(“load /dev/fs/[email protected]/boot/vmlinu’)

還記得否, init()函數的初始化分為4 類,其中的一類就是指令的初始化。

CmdTable[i].func 就是執行初始化時注冊的那個指令的執行函數。這裡就是

cmd_nload 函數。這個函數就是spawn 的一個包裝。spawn 函數初始化了一個

“程序”,這些我都不關心,我關心就是spawn 倒數第二行的的exit1(sonproc,

(*func)(argc, argv) & 0xff),這關系到一個極為關鍵的地方,spawn 的傳回值是什

麼。在exit1 之後有句return(0);

如果每次都傳回0,那這個傳回值就一點價值都沒有了,而且正常的你會發現邊

上的注釋。為什麼不會執行這個return 呢?exit1 的作用。

在exit1 的末尾有一句longjmp(xx,xx),可見程式的執行流就被改變了,其實在

spawn 中也有一個setjmp 來保證longjmp 傳回到哪裡,longjmp 和setjmp 其實

是個相反的過程,代碼都在../lib/libc/arch/mips/longjmp.S 中。對于這個過程有

興趣的可以細究這個過程,我這裡隻需要明白,傳回值是由執行傳給spawn 的

那個指令(第二個參數)傳回值決定的就ok 了。在這裡,spawn 的傳回值就是

執行nload 指令的傳回值。

101

現在看看這個nload 到底做了些什麼。如果沒明白我說什麼,就再看一遍代碼。

nload 這個函數很長,這裡隻來點關鍵的。

第一個關鍵函數,getopt()。比如我們在重新整理pmon的時候會使用load -r -f xxx.bin

這樣的指令,這個-r 和-f 就是由這個getopt 解析的。解析之後會設定flags 變量。

第二個關鍵函數,open()。

if ((bootfd = open (path, O_RDONLY | O_NONBLOCK)) < 0) {

perror (path);

return EXIT_FAILURE;

}

open 函數前面專門講過,不陌生了吧。

後面我會專門講解load pmon 的過程,現在不去管flash 相關的代碼。

這樣的話,剩下的代碼就不多了,就列在下面:

dl_initialise (offset, flags);

這個函數重新初始化了符号表,就是ls 看到的東西。(試試看,load 前後ls 有差

别沒有)。之後清除所有斷點。

fprintf (stderr, "Loading file: %s ", path);

errno = 0;

n = 0;

if (flags & RFLAG) {

。。。。

} else {

ep = exec (NULL, bootfd, buf, &n, flags);

}

如果沒有-r 的選項,執行這個exec(NULL, bootfd, buf, &n, flags),記住對于我

們load /dev/fs/[email protected]/boot/vmlinux 的例子,這裡的n 和flags 都是0。

close (bootfd);

putc ('\n', stderr);

exec 從函數名上看,就是執行這個具體的指令。代碼如下:

long exec (ExecId id, int fd, char *buf, int *n, int flags)

{

ExecType *p;

long ep;

可以看出id 表示這個要load 的檔案的格式,如果為NULL 表示格式不确定,就

調用所有的格式執行函數試試,如果執行順利,傳回0 表示正确。否則繼續嘗試

下一個類型的檔案loader 函數。這裡我們執行的是elf 格式的核心,是以在執行

elf 的loader 函數後會傳回ep (entry point),具體的loader 函數稍後再看。

if (id == NULL) {

SLIST_FOREACH(p, &ExecTypes, i_next) {

if (p->flags != EXECFLAGS_NOAUTO) {

if ((ep = (*p->loader) (fd, buf, n, flags)) != -1) {

break;

102

}

}

}

} else {

。。。。。

}

在執行了elf 的loader 函數後,exec 函數退出,傳回非負數表示load 成功。我

們回到nload 函數接着看剩下的掃尾工作。

先關閉這個檔案close(bootfd),接下來是錯誤處理,不看了,就。剩下就之後下

面幾句了。我們的flags 是0,遇到’!’開頭的都執行。

if (!(flags & (FFLAG|YFLAG))) {

printf ("Entry address is %08x\n", ep);

手動load 過的哥們都見過這句吧。

if (md_cachestat())

flush_cache (DCACHE | ICACHE, NULL);

重新整理一級cache(資料,指令)

md_setpc(NULL, ep);

if (!(flags & SFLAG)) {

dl_setloadsyms ();

}

至于這兩個set 我都不大了解是做什麼用的。

}

剛才我們跳過了最重要的elf 的loader 函數,現在可以一睹其芳容了。記得我們

給的見面禮嗎?

ep = (*p->loader) (fd, buf, n, flags)),第一個參數fd 是檔案描述符,buf 是一個大

小為DLREC(550)個位元組的緩沖區的首位址,n 是值為0 的一個局部變量的位址

(表示了檔案操作的起始位元組),flags 為0。

Load_elf()函數在../pmon/loaders/exec_elf.c 中,代碼較長,我們就跳着講吧。

這個函數和elf 的格式規範密切相關,如果要閱讀這個檔案,最好先了解點elf

的檔案格式定義。我推薦滕啟明寫的《ELF 檔案格式分析》,他講了最基本的概

念。在寫這個東西的時候,我使用khexedit 和hexdump 檢視二進制的内容。

首先先讀取開頭的DLREC個位元組,判斷檔案的開頭是否是和elf 檔案格式相符。

是就列印(elf),否則出錯,傳回負數。現在龍芯使用的核心基本都是64 位的。

但是使用的時候一般都使用壓縮過的核心,這核心是32 位的。編譯後源碼目錄

的vmlinux 是64 位的,通過二進制的對比,他們的EI_CLASS 位是不同的,我

們這裡的代碼也是通過這個标志位判斷目前的核心是64/32 位的。64 位的核心

會調用load_elf64(fd, buf, n, flags)去執行。這個代碼的流程和load_elf 基本是一

樣的。中間有個明顯的疏忽,在從load_elf 複制代碼後,有個地方的32 沒有改

成64。呵呵。

在确定是32 位的elf 檔案後,判斷程式頭部表(programme header table)是

否存在。存在并且合理的話就把這個程式頭部表讀到phtab 開始的區域中。之後

103

讀取節區(section table)表。一個程式頭部描述了一個段,一個段中包含了幾

個節區,大緻是這麼個關系。在可執行的檔案中,我們平時說的代碼段,資料段,

bss 段都是段,而不是節區。

現在以8089 逸珑筆記本自帶的核心為例看看代碼如何執行,我們這裡要使用

readelf 這個工具。工欲善其事,必先利其器呀。

[[email protected] ~]# readelf -l vmlinux

Elf file type is EXEC (Executable file)

Entry point 0x81000000

There are 2 program headers, starting at offset 52

Program Headers:

Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align

REGINFO 0x01a000 0x8100a000 0x8100a000 0x00018 0x00018 R 0x4

LOAD 0x010000 0x81000000 0x81000000 0x1d3000 0x1d61d0 RWE

0x10000

Section to Segment mapping: 節區到段的映射

Segment Sections...

00 .reginfo

01 .text .reginfo .data .sbss .bss

可見核心的組織方式是非常簡單的,.text .reginfo .data .sbss .bss 這些節區都拼

在一塊,作為一個可裝載的段。

好了,有了這些感性認識,讀代碼就清楚多了。

Load_elf 接下來的代碼就是一個while 循環,看段類型是PT_LOAD 就想裝載,

但代碼中也說了,為了相容舊版本的亂序(位址順序)連結結果,把代碼寫的有

點不爽了。

最關鍵的就是bootread 了。這個直接把要引導的elf 檔案按照它自己的意願放到

記憶體中,它表達的視窗和是程式頭部的p_vaddr 這裡,通過上面的readelf 看到

核心的意願是放到0x81000000。

在pmon 下load 上面的那個核心的輸出為

PMON> load /dev/fs/[email protected]/boot/vmlinux

Loading fi le: /dev/fs/[email protected]/boot/vmlinux (el f)

0x81000000/1921024 + 0x811d5000/12752(z) + 91 syms

Entry address i s 81000000

其中的第二段以(z)結尾的表示要清零的部分,第三段表示符号表。

具體代碼位元組看,不說了。

正常情況下傳回值是return (ep->e_entry + dl_offset);,這裡的dl_offset 就是0,

不信打出來看看。

在這個函數傳回ep 後,我們回到nload 那個函數看看。

if (!(flags & (FFLAG|YFLAG))) {

printf ("Entry address is %08x\n", ep);

104

if (md_cachestat())

flush_cache (DCACHE | ICACHE, NULL);

md_setpc(NULL, ep);

if (!(flags & SFLAG)) {

dl_setloadsyms ();

}

}

當時我們不知道幾個set 是幹什麼的,現在情況不一樣了。setpc 就是在條件允

許的情況下,我們要調到的位址,setloadsysms,就是改變ls 看到的内容。

好了,nload 執行結束,傳回成功标記,這樣一個load 過程就算ok 了。也就是

do_cmd(“load /dev/fs/[email protected]/boot/vmlinu’)過程結束了。

load是好了,還有個g呢,回到autoload那個函數。假如我們的karg是”console

=tty root=/dev/hda1 no_auto_cmd’, 下面就瞅瞅do_cmd(“g console=tty root

=/dev/hda1 no_auto_cmd’)幹什麼,好,瞅瞅就瞅瞅。

g 的指令就是執行cmd_go,在../pmon/cmd/cmd_go.c 中。

由于要引導的核心已經在記憶體裡了,g 要做的就是讓cpu 到那裡執行,這需要先

處理他撒手而去的一些事宜。

開始就是一些參數檢查, 把clientcmd 設定為字元串“g console=tty root

=/dev/hda1 no_auto_cmd PMON_VER=xxxx EC_VER=xxx”。

if (!sflag) {

md_adjstack(NULL, tgt_clienttos ());

}

設定新的sp,位置在記憶體的高端最後64 位元組

clientac = argvize (clientav, clientcmd);

解析這個clientcmd,每項都在clienttav 這個數組中。這裡我們有6 項,clientac

為6。

initstack (clientac, clientav, 1);

這個initstack 是對棧的一些操作,還是非常重要的,在這個函數中還把所有的

pmon 的環境變量和g 的參數等放在棧裡,用于核心使用。

md_registers(1,NULL);

列印目前的所有寄存器内容。

closelst(2);

md_setsr(NULL, initial_sr);

設定狀态寄存器。

tgt_enable (tgt_getmachtype ());

設定一些使能。

usb_ohci_stop();

rtl8139_stop();

關閉一些usb 裝置和網卡

if (setjmp (go_return_jump) == 0) {

先設定等會我們要執行的寄存器狀态

105

goclient ();

goclient()最重要的就是最後調用_go(),把寄存器給填充為我們setjmp 時的值,

關鍵是epc 這個寄存器,執行eret 傳回,就傳回到eret 所指的位址去了。關鍵

的在這:

LOAD v0, PC * RSIZE(k0)

LOAD v1, CAUSE * RSIZE(k0)

MTC0 v0, COP_0_EXC_PC

這個PC 就是我們load 指令最後set 的前面那個md_setpc()存的。這個goclient

()中的_go()之後,pmon 就把控制權交給核心了,阿彌陀佛,再見。

}

正常的話,下面的都不執行的

console_state(1);

printf("\nclient return: %d\n", retvalue);

return 0;

}

Termio

pmon 的代碼有很多會涉及到termio 的函數,這裡先介紹一些。

termio 相關的函數代碼都在../pmon/fs/termio.c。首先看termio 在init()函數初始

化時執行的代碼。

static void init_fs()

{

devinit ();

這個函數非常重要,會初始化描述序列槽和顯示卡裝置的一些結構體,稍後詳細解釋。

filefs_init(&termfs);

往檔案系統連結清單插入termio 這個檔案系統,以備使用。

_file[0].valid = 1;

_file[0].fs = &termfs;

_file[1].valid = 1;

_file[1].fs = &termfs;

_file[2].valid = 1;

_file[2].fs = &termfs;

_file[3].valid = 1;

_file[3].fs = &termfs;

_file[4].valid = 1;

_file[4].fs = &termfs;

不同的下标代表了不同的裝置,具體代表的裝置如下面所示。

term_open(0, "/dev/tty0", 0, 0);

term_open(1, "/dev/tty0", 0, 0);

106

term_open(2, "/dev/tty0", 0, 0);

term_open(3, "/dev/tty1", 0, 0);

term_open(4, "/dev/tty1", 0, 0);

}

下面簡要的介紹devinit 和term_open。先看devinit,定義如下:

Int devinit (void)

{

int i, brate;

ConfigEntry *q;

DevEntry *p;

char dname[10];

char *s;

strcpy(dname, "tty_baud");

for (i = 0; ConfigTable[i].devinfo && i < DEV_MAX; i++) {

ConfigTable 數組在tgt_machdep.c 中定義,對于逸珑8089,實際上就是:

ConfigEntry ConfigTable[] =

{

{ (char *)COMMON_COM_BASE_ADDR, 0, ns16550, 256,

CONS_BAUD, NS16550HZ },

{ (char *)1, 0, fbterm, 256, CONS_BAUD, NS16550HZ },

{ 0 }

};

很明顯,第一個表示序列槽,第二個是顯示卡,第三個表示結束。

q = &ConfigTable[i];

p = &DevTable[i];

下面要使用ConfigTable 的内容填充DevTable 的結構體。

p->txoff = 0;

p->qsize = q->rxqsize;

p->sio = q->devinfo;

p->chan = q->chan;

p->rxoff = 0;

p->handler = q->handler;

handler 函數比較重要,會在多個場合被調用到。

p->tfunc = 0;

p->freq = q->freq;

p->nopen = 0;

if (p->chan == 0)

(*p->handler) (OP_INIT, p, NULL, q->rxqsize);

p->rxq = Qcreate (p->qsize);

107

p->txq = Qcreate (p->qsize);

這裡建立接收和發送隊列的緩沖區,大小預設都是256 個位元組。

if (p->rxq == 0 || p->txq == 0)

return (-1);

dname[3] = (i < 10) ? i + '0' : i - 10 + 'a';

if ((s = getenv(dname)) == 0 || (brate = getbaudrate(s)) == 0)

brate = q->brate;

if (brate != q->brate) {

if ((*p->handler)(OP_BAUD, p, NULL, brate)) {

brate = q->brate;

(void)(*p->handler)(OP_BAUD, p, NULL, brate);

}

}

p->t.c_ispeed = brate;

p->t.c_ospeed = brate;

}

return (0);

}

這個函數執行完以後,DevTable 的前兩個元素就表示序列槽和顯示卡了。下面看

term_open,這個函數在termio 的init_fs 函數中被連續5 次調用,分别用于初

始化前5 個檔案結構,我們下面以term_open(4, "/dev/tty1", 0, 0);

為例介紹,函數定義如下:

Int term_open(int fd, const char *fname, int mode, int perms)

{

int c, dev;

char *dname;

struct TermDev *devp;

DevEntry *p;

dname = (char *)fname;

if (strncmp (dname, "/dev/", 5) == 0)

dname += 5;

我們傳入的是/dev/tty1 或者/dev/tty0,開頭都是一樣的。

if (strlen (dname) == 4 && strncmp (dname, "tty", 3) == 0) {

108

c = dname[3];

if (c >= 'a' && c <= 'z')

dev = c - 'a' + 10;

else if (c >= 'A' && c <= 'Z')

dev = c - 'A' + 10;

else if (c >= '0' && c <= '9')

dev = c - '0';

我們的例子中dev 為1。

if (dev >= DEV_MAX || DevTable[dev].rxq == 0)

return -1;

devp = (struct TermDev *)malloc(sizeof(struct TermDev));

if (devp == NULL) {

errno = ENOMEM;

return -1;

}

每個檔案對應一個TermDev 結構。前面5 個檔案是特定的。

devp->dev = dev;

_file[fd].data = (void *)devp;

p = &DevTable[dev];

(*p->handler) (OP_OPEN, p, NULL, 0);

對于顯示卡,他的OP_OPEN 實際上什麼也不做。

if (p->nopen++ == 0)

term_ioctl (fd, SETSANE);

如果是第一次打開,那麼reset 一下這個裝置的相關設定。

}

else {

return -1;

}

return fd;

}

printf 和write

我們最熟悉的函數就是用printf 在螢幕上列印字元串了。現在就看看吧。

printf 的解析是非常繁瑣的。我們就用最簡單的情況printf("hello ,pmon")分析。

printf 定義在lib/libc/printf.c 中。代碼如下:

Int printf (const char *fmt, ...)

{

109

int len;

va_list ap;

va_start(ap, fmt);

len = vfprintf (stdout, fmt, ap);

va_end(ap);

如果各位大俠看過核心的源碼中的可變參數部分,對這幾個va_start 和va_end

一定不會陌生。我基本上也忘的差不多了,挺不好講的,不說了,好吧。

我們的例子中,可變參數的參數個數為1,printf("hello ,pmon")唯一的參數就是

這個字元串的首位址。這個ap 就不管了。我們要列印的就是fmt 開始的那個字

符串的内容。

stdout 的定義為#define stdout (vga_available?(&_iob[4]):(&_iob[1]))

在顯示卡初始化以後,vga_available 為1,stdout 就是_iob[4]

_iob[]的定義如下:

FILE _iob[OPEN_MAX] =

{

{0, 1}, // stdin

{1, 1}, // stdout

{2, 1}, // stderr

{3, 1}, // kbdin

{4, 1}, // vgaout

};

第一個參數表示fd,第二個參數都是1,表示檔案有效。這些個fd 的fs 參數在..

/pmon/fs/termio.c 中定義。均定義為termfs,馬上會用到。

return (len);

}

Vfprint 代碼如下,代碼中的fp 就是傳入的stdout:

Int vfprintf (FILE *fp, const char *fmt, va_list ap)

{

int n=0;

char buf[1024];

n = vsprintf (buf, fmt, ap);

vsprintf()會解析這個printf 傳入的參數,解析後的結果放在以buf 為首位址的緩

沖區中。n 表示解析後的字元串長度。

fputs (buf, fp);

fputs 就一句話,write (fp->fd, p, strlen (p));由于fp->fd 就是4,這個檔案代表

vgaout。

return (n);

}

以上的代碼實際上就是調用write (4, p, strlen (p)),對于我們前面的printf(

“hello , pmon’),這裡的p 就是這個字元串的首位址。

110

我們曾經分析過open的操作,現在就以這個printf 的調用為例分析write操作吧。

代碼在../lib/libc/write.c 中,如下:

int write (int fd, const void *buf, size_t n)

{

if ((fd < OPEN_MAX) && _file[fd].valid ) {

if (_file[fd].fs->write)

return (((_file[fd]).fs->write) (fd, buf, n));

如果這個檔案(裝置)所屬的檔案系統的write 函數存在,就執行之。fd 為4 代

表的vga 的fs 為termfs,其write 函數為term_write()。

else

return (-1);

}

else {

return (-1);

}

}

傳給term_write 的參數實際上是term_write(4,”hello, pmon’,長度)

term_write()代碼如下:

int term_write (int fd, const void *buf, size_t nchar)

{

DevEntry *p;

struct TermDev *devp;

char *buf2 = (char *)buf;

int i, n;

devp = (struct TermDev *)_file[fd].data;

這些fd 的data 在termio.c 中的init_fs 中的5 個term_open 調用的過程中初始

化。其中的dev 成員标志了這個到底是什麼裝置,0 就是序列槽,1 就是顯示卡。也

就是說被write 的裝置是顯示卡。

p = &DevTable[devp->dev];

n = nchar;

while (n > 0) {

while(!tgt_smplock());

tgt_smplock()這些用于互斥的操作在pmon下實際上是沒有什麼意義的。并沒有

對什麼資源的申請并占有。

i = Qspace (p->txq);

判斷發送隊列還有多少空間。下面就一個一個發送。

while (i > 2 && n > 0) {

if ((p->t.c_oflag & ONLCR) && *buf2 == '\n') {

Qput(p->txq, '\r');

111

檢視termio 的規定,如果設定oflag 的OLCUC 位,在發送換行符('\n')前先

發送回車符('\r')。Qput 把要發送的字元儲存在緩沖區中。

i--;

}

Qput(p->txq, *buf2++);

發送傳入的那個字元。

n--;

i--;

}

如果順利的話,現在要輸出的字元都放到發送隊列中去了。

tgt_smpunlock();

while (Qused(p->txq)) {

scandevs();

如果有字元要列印,現在要調用scandevs()。看函數名,就是挨個問,有沒有

要列印東西的,有就列印了。

}

}

return (nchar);

}

scandev 這個函數在多個地方被調用,是非常關鍵的。

這個函數的流程可以分為三個部分,分别是檢查輸入緩沖區,輸出緩沖區,按鍵。

我們暫時隻看後面檢查輸出的代碼,如下:

n = Qused (p->txq);

看看有沒有要輸出的,傳回的是要輸出的個數。

while (n > 0 && !p->txoff &&

(*p->handler)(OP_TXRDY, p, NULL, NULL)) {

對于逸珑8089 的顯示卡,其handler 函數為fbterm(),在../pmon/dev/vgacon.c

中,其OP_TXRDY 恒傳回1。表示随時都準備處理。

char c = Qget(p->txq);

取出要列印的字元。

(*p->handler) (OP_TX, p, NULL, c);

下面看看OP_TX 到底幹了些什麼。

case OP_TX:

if (vga_available)

video_putc(data & 0xff);

break;

如果要顯示的話,就調用video_putc()在螢幕上列印那個東西。這個函數沒有什

麼東西了,現在就隻看這個函數了吧。

n--;

112

}

Video_putc 函數代碼如下:

void video_putc(const char c)

{

switch (c) {

case 13:

break;

case '\n':

console_newline();

break;

case 9:

CURSOR_OFF console_col |= 0x0008;

console_col &= ~0x0007;

if (console_col >= CONSOLE_COLS)

console_newline();

break;

case 8:

console_back();

break;

default:

其它的都不管了,我們隻看最常見的情形,顯示一個字元。

video_putchar(console_col * VIDEO_FONT_WIDTH,

console_row * VIDEO_FONT_HEIGHT, c);

在BIOS 階段,顯示都是用的字元模式,每個字元的寬度為8 個像素,每個字元

的高度為16 個像素。video_putchar 的前面兩個參數就是要顯示的字元所在位

置的左上角的像素點的列位置,行位置。第三個參數是要顯示的字元。

console_col++;

if (console_col >= CONSOLE_COLS)

console_newline();

檢測是否到頭了。

}

CURSOR_SET

}

就看video_putchar 這個函數了,定義如下:

void video_putchar(int xx, int yy, unsigned char c)

{

video_drawchars(xx, yy + VIDEO_LOGO_HEIGHT, &c, 1);

}

VIDEO_LOGO_HEIGHT 在fb/cfb_console.c 中定義為0。這裡的xx 為水準方

向的像素位置,yy 為垂直方向的像素位置。c 是要顯示的符号,1表示顯示1次。

函數定義如下:

113

static void video_drawchars(int xx, int yy, unsigned char *s, int count)

{

unsigned char *cdat, *dest, *dest0;

int rows, offset, c;

offset = yy * VIDEO_LINE_LEN + xx * VIDEO_PIXEL_SIZE;

這裡的VIDEO_PIXEL_SIZE 為2,表示每個像素占2個位元組,VIDEO_LINE_LEN

為1024*2,表示一行像素要占用的位元組數。我怎麼知道這個值是多少呢?printf

不行,會遞歸成死循環的。

小技巧,加如下兩句。進入pmon 指令行後用m 指令看m -d 81000000 即可。

*(long *)(0x81000000) = VIDEO_LINE_LEN;

*(long *)(0x81000004) = VIDEO_PIXEL_SIZE;

dest0 = video_fb_address + offset;

通過如上的方法, 讀出這裡的video_fb_address 值為0xb4000000, 可見

framebuffer 的首位址為0xb4000000。每個螢幕上的點都由framebuffer 上的兩

個位元組控制。0xb4000000 開始的兩個位元組對應螢幕左上角的像素點。

switch (VIDEO_DATA_FORMAT) {

case GDF__8BIT_INDEX:

case GDF__8BIT_332RGB:

接下來是一個switch,VIDEO_DATA_FORMAT 定義如下:

#define VIDEO_DATA_FORMAT (pGD->gdfIndex)

pGD 在fb_init 中初始化,小本上這個是2,表示色深為16 位色。同理,同理如

果這個為3,表示24 位色。代碼中設定了如果為16 位色,就使用565 的RGB

編碼。這個值的switch 的代碼為:

case GDF_16BIT_565RGB:

while (count--) {

c = *s;

cdat = video_fontdata + c * VIDEO_FONT_HEIGHT;

我們可以算出來了,一個字元的像素為16x8(是以字元都挺水蛇腰的),每個像素

占兩個位元組。cdat 表示我們要顯示的點陣的首位址。我們假設這裡要顯示'A',

到底如何才能看到我們看到的符号呢?

vIdeo_fontdata 是一個點陣數組,字元A 的點陣如下,特地用紅色标記了,差不

多吧,去看看pmon 指令行下的的A,是不是一樣的。就是一樣的。

0x00,

0x00,

0x10,

0x38,

0x6c,

0xc6,

0xc6,

0xFe,

0xc6,

114

0xc6,

0xc6,

0xc6,

0x00,

0x00,

0x00,

0x00,

現在都明白了,video_fontdata 就是字元的點陣數組了,如何顯示呢?

這個字元A 的點陣為16 個位元組,但實際上在framebuffer 上我們一個字元占用

256 個位元組,或者說點陣中一行是一個位元組,但在framebuffer 上是16 個位元組,

這裡就有一個索引,每兩位索引四個位元組。等會就看到了。

or (rows = VIDEO_FONT_HEIGHT, dest = dest0;

rows—; dest += VIDEO_LINE_LEN) {

看這個for 的循環方式,可以看出是填充一行,一共16次,每行差2048個位元組,

這個前面都說過的。

unsigned char bits = *cdat++;

bits現在表示了點陣的第一個位元組的内容,代碼中的SHORTSWAP32沒什麼用,

去掉了,看得就清楚多了。

((unsigned int *)dest)[0] =

(video_font_draw_table16[bits >> 6] & eorx) ^ bgx;

((unsigned int *)dest)[1] =

(video_font_draw_table16[bits >> 4 & 3] & eorx) ^ bgx;

((unsigned int *)dest)[2] =

(video_font_draw_table16[bits >> 2 & 3] & eorx) ^ bgx);

((unsigned int *)dest)[3] =

(video_font_draw_table16[bits & 3] & eorx) ^ bgx;

前面剛說了,每兩位一個索引,看上面的代碼就很明白了,>>6,>>4,>>2,

不就是剝離出那兩位嗎!索引的數組是video_font_draw_table16,定義如下:

static const int video_font_draw_table16[] = {

0x00000000, 0xffff0000, 0x0000ffff, 0xffffffff

};

可見索引對應關系如下:

00 ---- 0x00000000

01 ---- 0xFFFF0000

10 ---- 0x0000FFFF

11 ---- 0xFFFFFFFF

這裡要注意高低位址的關系,01 表示後面那個像素要點亮,但是後面的像素是

在高位元組的,是以01 對應的是0xFFFF0000。

代碼中的eorx 為0xa514a514,bgx 為0。0xa514 這個數有明顯特征,就是和

它與後,RGB 都隻保留了兩位數。這就像是對565 的格式再進行采樣,也許很

性能相關吧,不去關注了。

}

115

dest0 += VIDEO_FONT_WIDTH * VIDEO_PIXEL_SIZE;

s++;

}

break;

好了,再細節的東西列位自己看吧。printf 就到這裡了。

鍵盤和鍵盤事件的響應

前面講了printf 的輸出,下面就介紹它的反方面,那就是鍵盤輸入的響應。

在dbginit 的過程中,會執行tgt_devconfig(),對于筆記本,這個函數會調用

kbd_initialize()初始化鍵盤控制器。8042 鍵盤控制器有兩個端口,狀态和控制寄

存器為64h 端口,60h 為資料寄存器端口。在開始看這個初始化代碼前,我們先

看看接受輸入的函數kbd_read_data()。

static int kbd_read_data(void)

{

int retval = KBD_NO_DATA;

unsigned char status;

status = kbd_read_status();

8042 鍵盤控制器文檔對寄存器的說明非常清楚。狀态寄存器描述如下:

|7|6|5|4|3|2|1|0| 8042 Status Register

| | | | | | | +---- output register (60h) has data for system

| | | | | | +----- input register (60h/64h) has data for 8042

| | | | | +------ system flag (set to 0 after power on reset)

| | | | +------- data in input register is command (1) or data (0)

| | | +-------- 1=keyboard enabled, 0=keyboard disabled (via switch)

| | +--------- 1=transmit timeout (data transmit not complete)

| +---------- 1=receive timeout (data transmit not complete)

+----------- 1=even parity rec'd, 0=odd parity rec'd (should be odd)

if (status & KBD_STAT_OBF) {

如果按下了鍵但系統沒有讀取

unsigned char data = kbd_read_input();

那麼從資料端口讀取那個按鍵的掃描碼。

retval = data;

if (status & (KBD_STAT_GTO | KBD_STAT_PERR))

retval = KBD_BAD_DATA;

}

return retval;

116

}

kbd_initialize 代碼較長,這裡隻講主幹。

int kbd_initialize(void)

{

int status;

int count;

status = kb3310_test();

這個kb3310_test 會讀取xram 的第一(0)個位元組,如果傳回那個值為0xff,表

明出錯。

kbd_clear_input();

這個函數會檢查目前緩沖去中是否有内容,如果有,則抛棄掉。判斷是否有内容

是用kbd_read_data(),如果傳回非KBD_NO_DATA,就繼續讀。

kbd_write_command_w(KBD_CCMD_SELF_TEST);

向8042 的控制寄存器發送指令。在8042 的文檔中對指令的解釋也很清楚,有

興趣的可以自行閱讀。KBD_CCMD_SELF_TEST 是0xAA,AA 指令的解釋為:

AA Self Test: diagnostic result placed at port 60h, 55h=OK

if (kbd_wait_for_input() != 0x55) {

printf("Self test cmd failed,ignored!\n");

}

很符合文檔的規範,傳回0x55 表示正常。下面判斷時鐘和資料線是否有效

kbd_write_command_w(KBD_CCMD_KBD_TEST);

if (kbd_wait_for_input() != 0x00) {

printf("KBD_TEST cmd failed,ignored!\n");

}

下面是正式的鍵盤接口使能。

kbd_write_command_w(KBD_CCMD_KBD_ENABLE);

count = 0;

do {

kbd_write_output_w(KBD_CMD_RESET);

status = kbd_wait_for_input();

if (status == KBD_REPLY_ACK)

break;

if (status != KBD_REPLY_RESEND) {

printf("reset failed\n");

if (++count > 1)

break;

}

} while (1);

117

if (kbd_wait_for_input() != KBD_REPLY_POR) {

printf("NO POR, ignored!\n");

}

count = 0;

do {

kbd_write_output_w(KBD_CMD_DISABLE);

發送這個指令後,鍵盤會回到上電後的預設狀态,并分會一個ACK。

status = kbd_wait_for_input();

if (status == KBD_REPLY_ACK)

break;

if (status != KBD_REPLY_RESEND) {

printf("disable failed\n");

if (++count > 1)

break;

}

} while (1);

kbd_write_command_w(KBD_CCMD_WRITE_MODE);

告訴鍵盤控制器,我的下一次操作是發指令了。

kbd_write_output_w(KBD_MODE_KBD_INT

| KBD_MODE_SYS

| KBD_MODE_DISABLE_MOUSE | KBD_MODE_KCC);

使能中斷。下面設定鍵盤的掃描碼

if (!(kbd_write_command_w_and_wait(KBD_CCMD_READ_MODE) &

KBD_MODE_KCC)) {

。。。IBM powerpc 的特殊處理部分。。。

}

下面使能鍵盤的輸入掃描。前面的指令都不是掃描輸入得到的。

if (kbd_write_output_w_and_wait(KBD_CMD_ENABLE) !=

KBD_REPLY_ACK) {

return 5;

}

設定掃描周期和延時,下一個指令傳送具體的參數。

if (kbd_write_output_w_and_wait(KBD_CMD_SET_RATE) !=

KBD_REPLY_ACK) {

return 6;

}

下面的0 是上面那個指令的參數,表示沒有延時,每秒掃描30 次。

if (kbd_write_output_w_and_wait(0x00) != KBD_REPLY_ACK) {

return 7;

}

118

return 0;

}

好,kbd_initialize 到此為止。usb 鍵盤和筆記本自帶的鍵盤的處理不同。我這隻

關注自帶鍵盤(因為這個簡單*_*)。

由于在pmon中沒有中斷的功能,是以必須有個時機讓cpu知道有按鍵的動作發

生。這個時機就是調用scandevs 函數的時候。

scandevs 這個函數函數可分為三部分,檢查輸入緩沖區(軟體層面),檢查輸出

緩沖區,檢查有沒有按鍵。前面我們看的是第二部分的代碼,現在看第三部分。

我關心的就一句話,kbd_poll()。

void kbd_poll()

{

if (esc_seq) {

。。。

} else {

while (kbd_read_status() & KBD_STAT_OBF)

這裡檢查鍵盤控制器的狀态,看是否有未處理的按鍵。如果有,那麼

handle_kbd_event();

這個函數會調用handle_kbd_event,代碼如下:

static unsigned char handle_kbd_event(void)

{

unsigned char status = kbd_read_status();

unsigned int work = 10000;

status 表示了目前8042 鍵盤控制器的狀态,work 表示嘗試的次數。避免剛處理

了按鍵很快又有鍵按下,導緻總在這個循環中的問題。其實機率非常非常小啦。

while ((--work > 0) && (status & KBD_STAT_OBF)) {

unsigned char scancode;

scancode = kbd_read_input();

從8042 讀出來的資料都是第一套的掃描碼,前面鍵盤初始化的時候設定的。

if (!(status & (KBD_STAT_GTO | KBD_STAT_PERR)))

{

if (status & KBD_STAT_MOUSE_OBF) ;

else

handle_keyboard_event(scancode);

處理鍵盤的輸入,參數是掃描碼的值。

}

status = kbd_read_status();

判斷現在還有沒有未處理的按鍵,如果有則還在這個循環中。在第一套掃描碼

中,大部分按鍵的碼都是一個位元組,部分特殊功能鍵是2 或3 個鍵碼。

}

return status;

}

119

handle_keyboard_event 的定義如下

static inline void handle_keyboard_event(unsigned char scancode)

{

if (do_acknowledge(scancode))

handle_scancode(scancode, !(scancode & 0x80));

}

跳過處理重複發送鍵碼(比如你一直按下一個鍵時)的處理,實際就是調用

handle_scancode(scancode, !(scancode & 0x80));這個scancode 為什麼要與

上0x80 呢?因為在第一套掃描碼的定義中,按下鍵和釋放這個鍵的掃描碼差

0x80,而且釋放鍵的掃描碼都是大于0x80 的。故可通過此判斷是否為釋放鍵。

handle_scancode 代碼如下:

void handle_scancode(unsigned char scancode, int down)

{

unsigned char keycode;

char up_flag = down ? 0 : 0200;

Up_flag 為0,表示是按下鍵,否則為釋放鍵。

if (!kbd_translate(scancode, &keycode))

goto out;

對于軟體系統來說,處理的不是掃描碼,而是鍵碼,是以有個kbd_translate 函

數,這個有興趣的自己看。

if (1) {

u_short keysym;

u_char type;

ushort *key_map = key_maps[shift_state];

這裡有有一層軟體的抽象,前面的轉換成keywork 是标準的,對于所有的鍵盤

都一樣,但我們知道有美式鍵盤,荷蘭語鍵盤,西班牙鍵盤等,也就是按了同一

個鍵表達的内容不一樣,這個功能是通過kaymap 的映射實作的。

if (key_map != NULL) {

keysym = key_map[keycode];

type = KTYP(keysym);

這個type 和key_map 相關聯,不同類型的鍵type 不同,比如字元鍵,ctrl 鍵,

backspace 鍵等類型就明顯不同。

if (type >= 0xf0) {

type -= 0xf0;

if (type == KT_LETTER) {

type = KT_LATIN;

表示輸入是一個字元。

}

if (*key_handler[type]) {

(*key_handler[type]) (keysym & 0xff,

up_flag);

120

根據type 調用不同的按鍵處理函數,對于字元鍵,調用的就是key_handle

[0],處理很簡單,如果是釋放鍵,直接傳回,否則給kbd_code 這個全局變量賦

值。

}

}

}

}

out:

}

函數基本上是原路傳回,并沒有什麼動作了。傳回到scandevs 中。

這裡有個問題我不了解,傳回到scandevs 後,隻有非零kbd_code 表示按下了

一個鍵并且這個按鍵沒有被處理,也就是執行了scandevs 後,我的輸入不能馬

上被放到輸入緩沖區中,而是在下次scandevs 時被放到輸入緩沖區中。也就是

說如果沒有下次對scandevs 的調用,這次的輸入就沒有作用。因為代碼中的确

沒有在kbd_poll 後有任何的對輸入緩沖區的操作呀!但是,我們明顯知道,在

pmon 指令行下,我們按下一個字元鍵,就顯示一個字元,可見是一旦輸入就處

理了呀,并沒有出現在下次按鍵後才顯示前一個鍵的狀況呀。怎麼回事?怎麼回

事?為什麼?為什麼?

是這樣嗎?

剪不斷,理還亂,别有一番滋味在心頭。

的确,世界上有很多事情是我難以精确解釋的,不過這個問題不是。

首先做一個實驗。

在pmon中,最先響應的輸入是get_boot_selection。這個檔案的開頭部分如下:

int get_boot_selection(void)

{

int flag = 1;

int c;

unsigned int cnt;

unsigned int dly;

struct termio sav;

dly = 128;

ioctl(STDIN, CBREAK, &sav);

do {

delay(10000);

ioctl (STDIN, FIONREAD, &cnt);

也許一看就明白,這個ioctl 調用後傳回的cnt 的值表示了輸入的鍵值。因為如果

cnt 非0 的話,就認為按下鍵,就getchar()。

if(cnt == 0) {

flag = NO_KEY;

continue;

121

}

c = getchar();

好,get_boot_selection就到這裡。看ioctl (STDIN, FIONREAD, &cnt)的。如下:

case FIONREAD:

scandevs ();

*(int *)argp = Qused (p->rxq);

break;

scandevs 我們都很熟悉了(或者更陌生了)。老生我再談一下吧:

這個函數分三部分:

1. 輸入緩沖區的檢查。

2. 輸出緩沖區的檢查

3. 有沒有待處理的硬體(鍵盤按鍵,網絡包處理等)

我一開始是這樣了解這個ioctl 的,我們在開機後按下了del 鍵,但是一直沒有被

處理,到這個ioctl (STDIN, FIONREAD, &cnt)執行的時候,會執行第三部分的

kbd_poll ,檢查鍵盤控制器,看有未處理的按鍵,讀出來,scancode 轉keycode,

設定kbd_code,再往輸入緩沖區中寫。之後這個scandevs 執行完畢,馬上接

着執行*(int *)argp = Qused (p->rxq),啊,輸入緩沖區不空。argp 記錄下這個輸

入緩沖區的内容數量。

這個想法很合理,但代碼不是這麼寫的。問題在于在設定了kbd_code 後,

scandevs 沒把鍵值送到緩沖區就拍屁股走了。是以實際上在執行Qused (p

->rxq)時,我這次的輸入并沒有加到這個輸入緩沖區中(receive queue)。那麼

這個傳回的東西豈不就是0 嗎?好,問題說好了。實驗來了。

在代碼中添加三句話(紅色字型顯示)

case FIONREAD:

temp1 = Qused (p->rxq);

scandevs ();

temp2 = Qused (p->rxq);

printf("temp1 %d : temp2 %d \n", temp1, temp2);

*(int *)argp = Qused (p->rxq);

break;

加入了這個修改的pmon 執行的效果如何,開機後長按del 鍵,結果出來了。

temp1 和temp2都是3。也就是說scandevs前後輸入緩沖區的内容并沒有變化。

為什麼呢?當然為什麼是3 這個問題更難回答。

其實在啟動的時候(包括kbd 初始化後)我們執行了許多的printf 函數,有的打

到序列槽,有的打到螢幕上。前面的章節中有個細節,在printf 執行的過程中會調

用scandevs,不信回去看看,也就是說每個printf 函數都可能會調用kbd_poll,

進而接收了鍵盤控制器的輸入。是以我有個猜想,如果在kbd 初始化完成後到

get_boot_selection 之間沒有調用printf(或者其它會調用scandevs 的函數),我

們在第一次get_boot_selection 調用ioctl (STDIN, FIONREAD, &cnt);的結果将

是cnt 為0,當然get_boot_selection 是個循環,第二次就會在scandevs 的輸

入緩沖區的檢查中檢查到輸入了。cnt 就不為0 了。

如果我上面的分析沒有問題的話,就是目前的輸入是下一個scandevs才能收到,

122

在pmon 指令行下即輸即顯又如何解釋呢?說不過去呀?

看代碼,不行也行。

Main->getcmd->get_line->getchar->getc->fgetc->read->term_read。

上面的順序就是pmon 指令行下接受輸入的函數調用路徑。這個路徑寫到

term_read 是因為我們看到了一個可以說明問題的代碼了。代碼如下:

for (i = 0; i < n;) {

scandevs ();

這裡的n 是1,表示我們要read 一個位元組,這個for 循環有個特殊的地方發現沒

有,沒有i++,什麼時候i++呢,在這個函數的末尾。也就是說如果沒有讀到就

一直scandevs。直到輸從入緩沖區中讀到一個才i++,都到這裡了,剩下的自己

看吧。是以在pmon 指令行下即輸即顯是通過反複調用scandevs,直到得到一

個位元組的方式實作的。要知道回顯是如何實作的可以看get_line代碼,很明顯的。

這個流程合理嗎?

scandevs 的流程是否合理呢?那三部分的順序能否掉換呢?也就是第三部分的

代碼放到第一部分前面。

呵呵。到這裡吧。

Ioctl

代碼中調用了多次ioctl,我們下面就看看這個函數。

ioctl 是一個和read,open 一樣常見的檔案操作函數。在pmon 中,主要用它來

和序列槽,顯示卡等字元裝置互動。

int ioctl(int fd, unsigned long op, ...)

{

void *argp;

va_list ap;

va_start(ap, op);

argp = va_arg(ap, void *);

va_end(ap);

又是對可變參數的處理。在pmon 代碼中,一般都隻有三個參數,argp 就是第

三個參數。

if ((fd < OPEN_MAX) && _file[fd].valid )

if (_file[fd].fs->ioctl)

return (((_file[fd]).fs->ioctl) (fd, op, argp));

。。。。。。

}

對于前5 個fd,上面調用的ioctl 就是調用term_ioctl,其餘的檔案一般就是普通

檔案,ioctl 就是檔案系統的ioctl。這裡隻看裝置檔案的ioctl 實作term_ioctl。

123

int term_ioctl (int fd, unsigned long op, ...)

{

DevEntry *p;

struct termio *at;

int i;

void *argp;

va_list ap;

struct TermDev *devp;

devp = (struct TermDev *)_file[fd].data;

data成員在term_open時初始化,devp->dev 标記了這個檔案是序列槽還是顯示卡。

va_start(ap, op);

argp = va_arg(ap, void *);

va_end(ap);

if (devp->dev < 0)

return -1;

p = &DevTable[devp->dev];

fd 為0,1,2 時,表示為序列槽,為3,4 時,表示為顯示卡。

switch (op) {

case TCGETA:

*(struct termio *)argp = p->t;

break;

傳回這個裝置的termio 結構體指針。

case TCSETAF:

while (!Qempty (p->rxq))

Qget (p->rxq);

(*p->handler) (OP_FLUSH, p, NULL, 1);

if (p->rxoff) {

(*p->handler) (OP_RXSTOP, p, NULL, p->rxoff = 0);

if (p->t.c_iflag & IXOFF)

chwrite (p, CNTRL ('Q'));

}

清空接收隊列。

case TCSETAW:

at = (struct termio *)argp;

if (p->t.c_ispeed != at->c_ispeed) {

if ((*p->handler) (OP_BAUD, p, NULL, at->c_ispeed))

return -1;

}

p->t = *at;

124

break;

用傳入的參數給裝置的termio 指派并改變波特率。

case FIONREAD:

scandevs ();

*(int *)argp = Qused (p->rxq);

break;

傳回有接收隊列中還有多少個鍵值沒有被處理。

case SETINTR:

p->intr = (struct jmp_buf *) argp;

Break;

設定中斷處理函數?

case SETSANE:

(*p->handler) (OP_RESET, p, NULL, 0);

setsane(p);

break;

重新初始化這個裝置的控制方式。

case SETNCNE:

if (argp)

*(struct termio *)argp = p->t;

p->t.c_lflag &= ~(ICANON | ECHO | ECHOE);

p->t.c_cc[4] = 1;

break;

傳回但前的termio 設定裝置并退出标準模式,不回顯輸入。在标準模式下,輸

入字元存儲在輸入隊列的緩沖區中,直到讀入換行符或是EOT(^-d)字元後才将

所有資料一次輸出;在原始模式下,字元一鍵入就立即輸出,沒有緩沖區。系統

預設的是标準模式。

case CBREAK:

if (argp)

*(struct termio *)argp = p->t;

p->t.c_lflag &= ~(ICANON | ECHO);

p->t.c_cc[4] = 1;

break;

和SETNCNE 的唯一差别是受到删除鍵允許删除前一個字元。

case GETTERM:

*(int *)argp = 0;

if (p->tfunc == 0)

return (-1);

strcpy ((char *)argp, p->tname);

break;

傳回目前的term 的name。

case SETTERM:

for (i = 0; TermTable[i].name; i++) {

125

if (!strcmp(argp, TermTable[i].name))

break;

}

這個好像沒有任何作用。

default:

break;

}

return 0;

}

開始階段在get_boot_selection 函數中使用ioctl (STDIN, FIONREAD, &cnt)接

收del輸入,現在很容易了解了,就是讀取目前的接收隊列中有幾個位元組的鍵碼,

結果放在cnt 中。

環境變量和flas h

pmon 下環境變量可分為三種,使用set 指令可以看到。一種是代碼中寫死的,

如pmon 的版本号,一種是計算出來的,比如記憶體大小,最後一種是寫在flash

上的,即使重刷pmon 代碼也不會改變。比如al 這種使用者自定義的變量。

envvar 是個存儲環境變量結構體的數組,大小為64 項。結構體的定義如下:

struct envpair {

char *name;

char *value;

};

至于64 項環境變量是否夠用?沒有特殊需要的話,64 項肯定夠了,當然非要多

于64 項,可以修改NVAR 這個常量定義。

envinit 函數定義如下:

void envinit ()

{

int i;

bzero (envvar, sizeof(envvar));

bzeor()當然就是清零。

tgt_mapenv (_setenv);

參數_setenv 是一個函數名,會在tgt_mapenv 中被調用。功能就是注冊使用者定

義的和計算出來的環境變量。

envinited = 1;

for (i = 0; stdenvtab[i].name; i++) {

if (!getenv (stdenvtab[i].name)) {

setenv (stdenvtab[i].name, stdenvtab[i].init);

126

如果這個變量使用者沒有定義這些變量,那麼就是用标準環境變量。

}

}

}

下面進入tgt_mapenv()。

這個tgt_mapenv()的代碼并不長。功能就是注冊使用者定義的和計算出來的環境

變量。由于自定義的變量是在flash 中,是以要先處理flash 相關的問題。

nvram = (char *)(tgt_flashmap()->fl_map_base + FLASH_OFFS);

printf("nvram %x\n", nvram);

if(fl_devident((void *)(tgt_flashmap()->fl_map_base), NULL) == 0 ||

cksum(nvram + NVRAM_OFFS, NVRAM_SIZE, 0) != 0) {

先執行這行代碼:

nvram = (char *)(tgt_flashmap()->fl_map_base + FLASH_OFFS);

Tgt_flashmap 的定義很簡單,就是傳回一個結構體tgt_fl_mpa_boot8 的指針:

struct fl_map * tgt_flashmap()

{

return tgt_fl_map_boot8;

}

Tgt_fl_map_boot8 就在函數上方定義:

struct fl_map tgt_fl_map_boot8[]={

TARGET_FLASH_DEVICES_8

};

TARGET_FLASH_DEVICES_8 的定義為:

#define TARGET_FLASH_DEVICES_8 \

{ 0xbfc00000, 0x00080000, 1, 1, FL_BUS_8 }, \

{ 0xbc000000, 0x02000000, 1, 1, FL_BUS_8 }, \

{ 0x00000000, 0x00000000 }

盒子使用的flash 是8 位的,相當于這個tgt_fl_map_boot8 數組的定義為

struct fl_map tgt_fl_map_boot8[]={

{ 0xbfc00000, 0x00080000, 1, 1, FL_BUS_8 },

{ 0xbc000000, 0x02000000, 1, 1, FL_BUS_8 },

{ 0x00000000, 0x00000000 }

};

從這個定義中可以看出,有兩塊flash,一塊就是pmon,起始位址是0xbfc00000,

大小為512KB。另一個起始位址為0xbc000000,大小為32MB,你可能猜到了,

對,就是顯存。

fl_map 這個結構體定義如下:

struct fl_map {

u_int32_t fl_map_base;

u_int32_t fl_map_size;

127

int fl_map_width;

int fl_map_chips;

int fl_map_bus;

int fl_map_offset;

};

#define FL_BUS_8 0x01

#define FL_BUS_16 0x02

#define FL_BUS_32 0x03

#define FL_BUS_64 0x04

#define FL_BUS_8_ON_64 0x05

#define FL_BUS_16_ON_64 0x06

回到調用語句

nvram = (char *)(tgt_flashmap()->fl_map_base + FLASH_OFFS)

FLASH_OFFS 定義為flash 大小減去4KB,按這個定義,就是pmon 所在晶片

的最後4KB 是可以用于存儲環境變量(實際上隻使用了512 位元組)。nvram 的值

就是這塊空間的起始位址。

接着執行fl_devident((void *)(tgt_flashmap()->fl_map_base), NULL) == 0

tgt_flashmap()->fl_map_base 我們知道了,就是0xbfc00000,fl_devident 一開

始執行map = fl_find_map(base);這base 就是0xbfc00000,

這個fl_find_map 本質就是挨個(也隻有兩個:->)檢查這個傳入的base 在哪兒

map 位址空間裡,找到就傳回這個map 的結構體首位址,否則繼續,如果沒找

到一個符合要求的,就傳回NULL 指針。這個位址落在pmon 的flash 上,傳回

那個map。

if (map != NULL) {

fl_autoselect(map);

識别flash 廠商和裝置id。

switch (map->fl_map_bus) {

case FL_BUS_8:

mfgid = inb(map->fl_map_base);

chipid = inb(map->fl_map_base + 1);

if (chipid == mfgid) {

chipid = inb(map->fl_map_base + 3);

}

break;

其他case:

}

fl_reset(map);

for (dev = &fl_known_dev[0]; dev->fl_name != 0; dev++) {

if (dev->fl_mfg == mfgid && dev->fl_id == chipid) {

128

tgt_flashwrite_disable();

if (m) {

*m = map;

}

return (dev);

}

}

printf("Mfg %2x, Id %2x\n", mfgid, chipid);

}

首先執行的是fl_autoselect(map),這個函數的就是執行這些outb。

switch (map->fl_map_bus) {

case FL_BUS_8:

#if NMOD_FLASH_SST

outb((map->fl_map_base + SST_CMDOFFS1), 0xAA);

outb((map->fl_map_base + SST_CMDOFFS2), 0x55);

outb((map->fl_map_base + SST_CMDOFFS1), FL_AUTOSEL);

#endif

#if NMOD_FLASH_AMD

outb((map->fl_map_base + AMD_CMDOFFS1), 0xAA);

outb((map->fl_map_base + AMD_CMDOFFS2), 0x55);

outb((map->fl_map_base + AMD_CMDOFFS1), FL_AUTOSEL);

#endif

#if NMOD_FLASH_WINBOND

outb((map->fl_map_base + WINBOND_CMDOFFS1), 0xAA);

delay(10);

outb((map->fl_map_base + WINBOND_CMDOFFS2), 0x55);

delay(10);

outb((map->fl_map_base + WINBOND_CMDOFFS1),

FL_AUTOSEL);

delay(10);

#endif

事實上,為了支援更多的flash,代碼中的三個NMOD_FLADH***都定義了。定

義在../Targets/Bonito/compile/Bonito/flash.h 中。很幸運的是,這樣的重複操作

似乎沒有造成什麼副作用。

盒子的pmon 晶片使用過MX 和SST 兩個牌子,下面就以SST 為例吧,其實不

同的晶片程式設計方式是大同小異的。

看SST 的指令吧。SST_CMDOFFS1為0x5555,SST_CMDOFFS2為0x2aaa。

outb((map->fl_map_base + SST_CMDOFFS1), 0xAA);

outb((map->fl_map_base + SST_CMDOFFS2), 0x55);

outb((map->fl_map_base + SST_CMDOFFS1), FL_AUTOSEL);

看到SST 手冊的P7,開頭就介紹了6 個軟體指令,我們上面三條指令的順序對

于其中的第四條指令Software ID Entry,一查FL_AUTOSEL 就是0x90。在按

129

照這個指令順序和内容發送後,晶片會傳回晶片的ID 号。fl_autoselect 函數返

回後就會讀取這個值。

case FL_BUS_8:

mfgid = inb(map->fl_map_base);

chipid = inb(map->fl_map_base + 1);

if (chipid == mfgid) {

chipid = inb(map->fl_map_base + 3);

}

break;

這兩個inb 一個傳回廠商号(Manufacturer),第二個傳回晶片id,手冊說這個

地方的讀操作位址A0 為0 就傳回廠商号,為1 就傳回晶片id,和其它位無關。

接着執行fl_reset(map);

手冊規定在這種狀況下,往任一位址寫0xf0 表示晶片資訊讀取結束。

到現在為止,fl_devident 的功能就是按照晶片的時序讀取晶片資訊。接着檢視

pmon 是否支援這個型号的晶片。

for (dev = &fl_known_dev[0]; dev->fl_name != 0; dev++) {

if (dev->fl_mfg == mfgid && dev->fl_id == chipid) {

tgt_flashwrite_disable();

if (m) {

*m = map;

}

return (dev);

}

}

fl_known_dev 是一個flash 晶片型号資訊的數組,包括了所有明确支援的flash

晶片。這個for 循環挨個查找剛才讀取的值是否在這個清單中,如果在的話,為

傳入的二級指針m 指派并傳回dev 這個結構的首位址。

好了,從這個函數傳回到tgt_mapenv,執行條件判斷代碼的下一個條件判斷,

if(fl_devident((void *)(tgt_flashmap()->fl_map_base), NULL) == 0 ||

cksum(nvram + NVRAM_OFFS, NVRAM_SIZE, 0) != 0) {

cksum 就是把nvram + NVRAM_OFFS 這個位址開始的NVRAM_SIZE(這裡是

512)個位元組求和,這段空間一定和是0 才正常嗎?對,這512 個位元組的第一個半

字存放的就是後面255 個半字的和的相反數,是以和為0。

這個if(fl_devident((void *)(tgt_flashmap()->fl_map_base), NULL)語句可繞了不

少時間了,概括的說就是看看我們主機闆的那個pmon 晶片是不是我們支援的型

号,并且是否正常。

這最後4KB 的内容究竟是什麼呢?讀讀看。

!ifconfig rtk0:172.16.2.22

setvga=0

al=dev/fs/[email protected]/boot/vmlinux-2.6.27.1

karg=root=/dev/hda1 console=tty

130

以上輸出是通過在cksum 函數中添加這段代碼看到的

u_int8_t *sp1 = p;

while(i<512){

if((*sp1)==0)

printf("\n");

else

printf("%c",*sp1);

if((i%5)==0) //不能一行列印太多,否則顯示異常。

printf("\n");

i++;

sp1++;

}

在進入pmon 的指令提示符後,輸入set,可以看到環境變量的前4 個就是我們

讀出來的那4 個,一模一樣。在pmon 中再設定一個變量并寫入flash 後,再次

啟動這個讀出的可讀變量就多了一個,就是我們剛添加的内容,可見這些變量都

是使用者自定義的變量。

現在看接下來的代碼就很清楚了。接下來的代碼如下:

while((*p++ = *ep++) && (ep <= nvram + NVRAM_SIZE - 1) &&

i++ < 255) {

if((*(p - 1) == '=') && (val == NULL)) {

*(p - 1) = '\0';

val = p;

}

}

這個對環境變量讀取并解析。接着執行:

if(ep <= nvram + NVRAM_SIZE - 1 && i < 255) {

(*func)(env, val);

}

在提取一個環境變量名和它的值(如果存在的話)後,調用(*func)(env, val);這

個func 是傳入的參數, 回到tgt_mapenv 函數的調用處, tgt_mapenv (_

setenv);傳入的參數是_setenv,是一個函數名,現在要調用的就是這個函數。

_setenv 函數在../pmon/common/env.c,函數原型為static int _setenv (char *

name, char *value),從命名就看出來了,參數表示将要注冊的變量名和變量值。

暫時不進入這個函數,先把tgt_mapenv 這個函數的流程理完。

bcopy(&nvram[ETHER_OFFS], hwethadr, 6);

sprintf(env, "%02x:%02x:%02x:%02x:%02x:%02x",

hwethadr[0], hwethadr[1],

hwethadr[2], hwethadr[3], hwethadr[4], hwethadr[5]);

(*func)("ethaddr", env);

131

hwethadr 是一個長度為6 的全局位元組型數組,這句代碼把相應位置的資料讀出

來作為網卡的mac 位址。并寫入env 數組,注冊ethaddr 這個環境變量。看

env 這個指令的輸出,除了使用者自定義的環境變量,ethaddr 是最前面的。

接着看,注冊幾個計算出來的環境變量,代碼如下:

sprintf(env, "%d", memorysize / (1024 * 1024));

(*func)("memsize", env);

sprintf(env, "%d", memorysize_high / (1024 * 1024));

(*func)("highmemsize", env);

sprintf(env, "%d", md_pipefreq);

(*func)("cpuclock", env);

sprintf(env, "%d", md_cpufreq);

(*func)("busclock", env);

(*func)("systype", SYSTYPE);

命名都是很清楚的。

這些執行完之後回到envinit(),剩下的也不多了。

for (i = 0; stdenvtab[i].name; i++) {

if (!getenv (stdenvtab[i].name)) {

setenv (stdenvtab[i].name, stdenvtab[i].init);

}

}

首先執行getstdenv (name),檢驗我們要注冊的變量名和保留标準環境變量名是

否沖突。比如Version 這個名字就是一個保留的名字。

這就不關心命名沖突的問題了,有興趣的可以自己閱讀。

好了, 現在進入關鍵的_setenv 函數吧。下面以在pmon 下使用“set al /

dev/fs/[email protected]/boot/vmlinux”這個指令為例解析。set 指令關鍵就是調用

do_setenv,代碼如下:

Int do_setenv (char *name, int value, int temp)

{

if (_setenv (name, value)) {

首先就是對_setenv 函數的調用,這個函數稍後解釋。

const struct stdenv *sp;

if ((sp = getstdenv (name)) && striequ (value, sp->init)) {

return tgt_unsetenv (name);

如果這個變量是重名的,那麼就删掉舊的變量。

}

else if(!temp) {

132

return tgt_setenv (name, value);

tgt_setenv 會把新環境變量寫到flash 中。這個等會再看

}

else {

return(1);

}

}

return 0;

}

我們隻關心_setenv()和tgt_setenv()兩個函數。先看_setenv()。

代碼首先會判斷這個要注冊的變量是否和标準變量(就是代碼中寫死的那些)重

名,我們直接跳過重名的情形。

for (ep = envvar; ep < &envvar[NVAR]; ep++) {

if (!ep->name && !bp) //如果這個ep 沒有名字,并且bp 為NULL

bp = ep; // 選擇這個bp 作為新注冊的環境變量的指針

else if (ep->name && striequ (name, ep->name)) //看這個新注冊的變

//量名是否和以前注冊的變量名沖突

break;

}

上面這個循環執行以後,如果bp 為NULL,說明變量數量已滿或者命名沖突,

我們不關心非正常的情形。

ep = bp;

if (!(ep->name = malloc (strlen (name) + 1)))

return 0;

strcpy (ep->name, name);

配置設定空間并把name 和value 放入。代碼夠簡單吧。

上面的代碼隻是在記憶體裡注冊了環境變量,重新開機後就沒了。tgt_setenv()是往flash

上寫,斷電後依然儲存。這個函數不短,但邏輯很清楚,就是從pmon 的最後

4KB 開始的地方讀取512 位元組到首位址為nvramsecbuf 一個記憶體空間中,在通

過合法性和可行性(放不放得下)判斷後。再把這個nvramsecbuf 的内容寫回

去。我們隻看最後關鍵的兩行。

cksum(nvrambuf, NVRAM_SIZE, 1);

fl_program(nvram, nvramsecbuf, NVRAM_SECSIZE, TRUE);

前面曾經見過cksum,就是做檢驗和,第三個參數為1 表示set,由于增加了一

個變量後校驗和就不一定為0 了,是以要修改第一個半字(16 位),内容為後

255 個半字的和的相反數。這樣校驗和就又為0 了。

fl_program 是具體的重新整理flash。第一個參數是這次操作flash 的開始位址,第二

個參數是要寫入的字元串,第三個參數是寫入的位元組數,最後一個參數和列印有

關。fl 是flash 的縮寫,函數代碼如下:

int fl_program(void *fl_base, void *data_base, int data_size, int verbose)

{

133

void *base = fl_base;

int size = data_size;

char *tmpbuf;

get_roundup(&base, &size);

處理一些位址對齊相關的東西。比如MX 的flash 如果一次寫一塊,塊大小

必須是64KB 的倍數,即使我們隻想寫一個位元組。SST 的塊是4KB。

printf("base %x, size %x\n", base, size);

這個base 在現在肯定就是0xbfc70000,size 就是0x10000

tmpbuf = (char *)malloc(size);

if (tmpbuf == 0) {

printf("[fl_program] can't malloc");

return -1;

}

memcpy(tmpbuf, base, size);

把将要改變到的flash 上的内容讀道tmpbuf 中,這裡是64KB

memcpy(tmpbuf + (unsigned int)fl_base - (unsigned int)base,

data_base, data_size);

替換修改的部分,現在tmpbuf 準備好了要寫回去的内容。

flash 差別于塊裝置的一個明顯特征就是寫操作要先擦除再程式設計。

if (fl_erase_device(base, size, verbose) == 0 &&

fl_program_device(base, tmpbuf, size, verbose) == 0){

return 0;

}

return -1;

}

flash 擦除和程式設計的操作函數都在../pmon/dev/flash.c 中定義。

先看erase 的操作fl_erase_device()。這個函數比較長,但有一半都是合法性和

可行性檢查,我們跳過。

執行到這,base 表示這次操作的首位址,size 表示操作的大小,block 表示操作

從第幾塊開始。

while (size > 0) {

int boffs = (int)base;

if (size == map->fl_map_size &&

dev->fl_cap & (FL_CAP_DE | FL_CAP_A7)) {

如果要擦除的位址從flash 起始位址開始并且這個flash 是要求要擦全擦的。那

麼就全部擦出。

} else {

if ((*dev->functions->erase_sector) (map, dev, boffs) != 0) {

這個erase_sector 函數是這裡我們唯一在意的。就是擦除特定的塊。

printf("\nError: Failed to enter erase mode\n");

134

(*dev->functions->erase_suspend) (map, dev);

(*dev->functions->reset) (map, dev);

return (-4);

}

下面的代碼用于計算是否和如何擦除下一塊。

。。。。。。。。。。

}

delay(1000);

for (timeout = 0; ((ok =(*dev->functions->isbusy) (map, dev,

0xffffffff, boffs,TRUE)) == 1) &&

(timeout < PFLASH_MAX_TIMEOUT); timeout++) {

。。。等待直到擦除操作完成。。。。

}

delay(1000);

if (!(timeout < PFLASH_MAX_TIMEOUT)) {

(*dev->functions->erase_suspend) (map, dev);

}

(*dev->functions->reset) (map, dev);

}

tgt_flashwrite_disable();

return (ok);

}

代碼中調用了很多dev->functions 的函數,對于sst 的flash函數指針清單如下:

struct fl_functions fl_func_sst = { fl_erase_chip_sst,

fl_erase_sector_sst,

fl_isbusy_sst,

fl_reset_sst,

fl_erase_suspend_sst,

fl_program_sst

};

看擦除一塊的函數:

int fl_erase_sector_sst(struct fl_map *map, struct fl_device *dev, int offset)

{

switch (map->fl_map_bus) {

case FL_BUS_8:

outb((map->fl_map_base + SST_CMDOFFS1), 0xAA);

delay(10);

outb((map->fl_map_base + SST_CMDOFFS2), 0x55);

delay(10);

outb((map->fl_map_base + SST_CMDOFFS1), FL_ERASE);

delay(10);

135

outb((map->fl_map_base + SST_CMDOFFS1), 0xAA);

delay(10);

outb((map->fl_map_base + SST_CMDOFFS2), 0x55);

delay(10);

outb((map->fl_map_base + offset), FL_SECT);

delay(10);

break;

case 其它

。。。。。。。。。

}

可見要擦除flash 的一個塊,要發送6 個位元組的指令,最後一個資料表示了要擦

除的位址,flash 操作的指令見SST 手冊的P7。清清楚楚。

寫操作是先擦除再程式設計,程式設計的指令要簡單一些,隻發4 個位元組,具體的看代碼

和晶片手冊,都沒有什麼東西啦。

GPIO

在pmon 中和GPIO 相關的代碼并不多,但是GPIO 的操作對于pmon 來說是必

須的。誰用誰知道。

GPIO 的操作根據pci 位址是否配置設定可以分為兩個階段,那麼前一階段的位址是

代碼中寫死的。後一階段的位址是pmon 配置設定函數配置設定給各個pci 裝置的。

首先看前一階段的GPIO 操作。以下是對南橋GPIO5 的操作代碼,在start.S 中

GPIO_HI_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_OUT_EN);

GPIO_LO_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_IN_EN);

GPIO_LO_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_OUT_AUX1_SEL);

GPIO_LO_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_OUT_AUX2_SEL);

GPIO_LO_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_IN_AUX1_SEL);

GPIO_HI_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_PU_EN);

從宏名上看就可以猜想功能了,GPIO_HI_BIT 就是把那個位置高。

再看看兩個輸入參數的定義:

#define GPIO_5 5

#define DIVIL_BASE_ADDR 0xB000

#define SMB_BASE_ADDR (DIVIL_BASE_ADDR | 0x320)

#define GPIO_BASE_ADDR (DIVIL_BASE_ADDR | 0x000)

至于GPIOL_OUT_EN 這樣的寄存器,都是相對于base 做個偏移而已。

從上面的定義可以看出GPIO 那一套寄存器的基位址是0xB000。SMB 那一套寄

存器的基位址是0xB320。

傳入參數都清楚了,看看GPIO_HI_BIT 這個宏都做了些什麼。

136

#define GPIO_HI_BIT(bit, reg) \

lui v1, 0xbfd0; \

ori v1, reg; \

lw v0, 0(v1); \

li a0, 0x0001; \

sll a0, bit; \

or v0, a0; \

sll a0, 16; \

not a0; \

and v0, a0; \

sw v0, 0(v1);

這個代碼很簡單,不妨翻譯成C 代碼吧。

static void GPIO_HI_BIT(int bit, int reg)

{

int orig;

orig = *(unsigned int *)(0xbfd00000 + reg);

orig = orig | (1 << bit);

orig = orig & ( ~(1 << (16 + bit)));

*(unsigned int *)(0xbfd00000 + reg) = orig;

}

看到了吧,設定一個位的操作和我們平時的單純把一個位置為1 有差别。這些寄

存器的布局都如同P484 的表格一樣,都是32 位的,但是高低16 位的位定義是

相同的。比如要使能位3 的功能,不但要把位3 置為1,而且要把位19(16

+3)置為0 方為有效的置為,清某個位剛好相反。

現在再看看這個代碼做了什麼:

GPIO_HI_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_OUT_EN);

輸出使能

GPIO_LO_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_IN_EN);

禁止輸入

GPIO_LO_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_OUT_AUX1_SEL);

GPIO_LO_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_OUT_AUX2_SEL);

GPIO_LO_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_IN_AUX1_SEL);

上面三句合在一塊表示一個意思,就是這個GPIO 口隻使用普通的IO 輸出功能。

GPIO 嘛,就是多功能,至于表現成那個功能,就要配置了,GPIOL_OUT_AUX1_

SEL,GPIOL_OUT_AUX2_SEL,GPIOL_IN_AUX1_SEL 都是用于配置功能的

寄存器。比如在南橋手冊P28 顯示GPIO6 這個引腳就可以配置成4 種功能,如

果要配成MFGPT0_C1 這個功能,就要在GPIOL_OUT_AUX1_SEL 這個寄存

器的相應位使能。這裡把那三個寄存器的描述GPIO_5 的那位都disable 掉,就

是說不使用那些功能,而是作為一般的輸出輸入口來用。

GPIO_HI_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_PU_EN);

這個是使能一種拉高屬性。

137

上面這個代碼如果真的要輸出的話,一般還要寫一句

GPIO_HI_BIT(GPIO_5, GPIO_BASE_ADDR | GPIOL_OUT_VAL);

就是輸出的時候設定輸出什麼電平,上面這句就是輸出的時候輸出高電平。

我們說了,第一階段這些寄存器的位址是代碼裡寫死的,這個在本文的第一章有

分析,第二階段的位址是pci 配置設定之後的,是多少呢。在pmon 中提供了一個命

令rdmsr 來獲得相應的基位址。

比如我想知道第二階段GPIO 這塊的基位址,就輸入rdmsr 8000000c。得到這

個的基位址是0xb000。上面為什麼查詢位址是8000000c 呢,下面就簡單介紹

一下。

在南橋手冊的P343,我們看到GPIO 的基位址可以通過51400000c 這個位址訪

問到,SMB 的基位址可以通過51400000b 這個位址通路到,至于8000_000c

這個位址是如何轉化成51400000c 的,看msr 的位址路由吧(手冊P60)。

在pmon 的代碼pmon/cmds/my_cmd.c 中,有比較完整的GPIO 第二階段的操

作示例,可以作為參考。

你怎麼出來了-- 圖檔顯示

pmon 基本上是一個黑白的世界。是以能看到些花花綠綠的圖檔,的确叫人分外

眼紅呀,截圖如下:

按照官方提供的方法,圖檔顯示的操作為把壓縮後8 位色的bmp 圖檔燒入

bfc60000 這個位址就行了。

138

這個圖檔太土了,好,我們DIY 一下。

我想把QQ 的圖像放到pmon 界面的右上角,就像平時在系統中一樣,截了個

QQ 的圖,如下:

這個圖的屬性如下:

qq.bmp: PC bitmap data, Windows 3.x format, 237 x 563 x 24

對,是24 位色的,和官方的不一緻。管他,試試先。

用gzip 壓成qq.bmp.gz,用load -r -f bfc60000 xxxx/qq.bmp.gz,顯示效果如何,

缺一節。看樣子好像是太長了。因為預設情況下圖檔不是頂天立地的。是以被截

了一塊,剛好把我的頭像截了一半,好險。不,不是好險,是好慘。

看樣子要看代碼了。在fb_init 中,就是下面這句。

video_display_bitmap(BIGBMP_START_ADDR, BIGBMP_X, BIGBMP_Y);

BIGBMP_X 和BIGBMP_Y 分别是288,100,可見這個坐标就是圖檔顯示的左

上角位址。我們要知道,8089的分辨率是1024x600,而qq這個圖檔的高是563,

加上100的确超過了600,被截掉就正常了。既然如此,換個坐标顯示不久行了,

把顯示左上角的坐标改成700,0 不久行了。哈哈。試試。

修改後燒入顯示一塌糊塗,面目全非。會不會是圖檔太大了。沒準,試了個小的,

如下:

139

這個圖檔的屬性如下:

haha.bmp: PC bitmap data, Windows 3.x format, 162 x 195 x 24

試了下這個圖檔的壓縮版,也不能正常的放到右上角。好家夥,bug 吧。沒方法

了,看代碼。

首先看到,這個代碼會判斷傳入的欲顯示圖檔位址存放的是不是bmp 格式的,

如果不是就預設為bmp 的壓縮格式。

看到這,可見bmp格式也是支援的。一試上面那個笑臉的圖檔,燒入bmp的話,

顯示到左上角是沒有問題的,可見壓縮格式的處理可能是有問題的。可是QQ 的

圖檔太大了,隻能壓縮,否則就ok 了,要個性,就要折騰。看代碼。

這個video_display_bitmap 函數代碼看起來長,其實很清楚。

首先我們跳過對壓縮格式的處理,先看對bmp 格式的處理,因為這比較簡單,

而且壓縮的格式解壓後,也會執行同樣的流程。

width = le32_to_cpu(bmp->header.width);

height = le32_to_cpu(bmp->header.height);

bpp = le16_to_cpu(bmp->header.bit_count);

colors = le32_to_cpu(bmp->header.colors_used);

compression = le32_to_cpu(bmp->header.compression);

首先讀取這個bmp 圖檔的幾個核心參數,分别是圖檔的長寬,色深,是否壓縮,

其中的colors 我們沒有使用。

有興趣的(比如我)可以使用二進制讀取工具看看上面的那兩個bmp 圖檔頭部

相關域的值。我用khexedit 這個工具感覺不錯。

這裡我們得到的bpp 是3,表示是24 位色。關于bmp 圖檔的頭部格式,網上一

大堆。

padded_line = (((width * bpp + 7) / 8) + 3) & ~0x3;

由于bmp 圖檔的存儲有個4 位元組對齊的要求,是以這裡計算為了對齊,每行的

資訊存儲要多少個位元組來填充(pad)。

if ((x + width) > VIDEO_VISIBLE_COLS)

width = VIDEO_VISIBLE_COLS - x;

if ((y + height) > VIDEO_VISIBLE_ROWS)

height = VIDEO_VISIBLE_ROWS - y;

是否越界的檢查,如果超過了顯示區域,棄之。

bmap = (unsigned char *)bmp + le32_to_cpu(bmp->header.data_offset);

很明顯了,是像素資訊存儲的基位址。

140

fb = (unsigned char *)(video_fb_address +

((y + height -1) * VIDEO_COLS * VIDEO_PIXEL_SIZE)

+ x * VIDEO_PIXEL_SIZE);

fb 嘛,就是framebuffer。這個位址有些怪不是。看看這個fb 對應的是螢幕上的

哪個像素點。就是圖檔應顯示區域的最後一行的左邊起始像素。

的确很奇怪呀,一查bmp 格式的資料,有了。

bmp 的存儲設計很怪,一張圖檔,最先存放的是最後一行的像素資訊。然後4

位元組對齊一下,不對齊就填充下。接着存儲導數第二行,如此直到第一行。

大家評評理,是不是腦子進水了。還是有什麼奇特的原因?

switch (le16_to_cpu(bmp->header.bit_count)) {

前面說了,是24 位色的。就case 24 了。

case 8:

。。。。。。

case 24:

padded_line -= 3 * width;

好,現在的padded_line 的值就是每行像素資訊存放時為對齊而增加的位元組數。

ycount = height;

switch (VIDEO_DATA_FORMAT) {

這個在前面講printf 那一章就見到過了,pmon中的framebuffer 都是16位色的。

采用的是565 的RGB 編碼方式。就是case GDF_16BIT_565RGB 了。

case GDF__8BIT_332RGB:

。。。。

case GDF_16BIT_565RGB:

while (ycount--) {

xcount = width;

while (xcount--) {

FILL_16BIT_565RGB(bmap[2], bmap[1],

bmap[0]);

bmap += 3;

}

每次這個while 執行完,就顯示了一行。FILL_16BIT_565RG 函數負責把888

模式轉化成565 的模式,實作就是丢些精度。

bmap += padded_line;

跳過補齊用的内容,不要顯示

fb -= (VIDEO_VISIBLE_COLS +width) * VIDEO_PIXEL_SIZE;

一行顯示完了,顯示上一行。這個是由bmp 格式的存儲方式決定的。

}

就是這樣一行一行顯示,一幅bmp 圖就出來了。

我們接着回頭看看壓縮的bmp 是如何處理的。

bmp 圖檔的明顯特征就是頭部的前面兩個自己是字元B 和M 的碼。

代碼如下:

if( (x == BIGBMP_X) && (y == BIGBMP_Y) ){

141

is_bigbmp = 1;

}else{

is_bigbmp = 0;

}

這句話看出,有個bigbmp 的概念,事實上預設顯示的那個圖檔是由3 個部分組

成的:一個大圖檔和兩個漢字圖檔。顯示時的處理有不同,具體的往下看。

if(is_bigbmp){

dst_size = 0x80000;

len = 0xd000;

}else{

dst_size = 0x8000;

len = 0x300;

}

是吧,如果是大的圖檔,代碼認為從bfc60000 開始的0xd000 的範圍内都是這

個圖檔的内容。否則就是小圖檔,大小在0x300 以内。

這裡的dst_size 表示預計的解壓後大小的上限。

bg_img_src = malloc(len);

if (bg_img_src == NULL) {

。。。

}

dst = malloc(dst_size);

if (dst == NULL) {

。。。

}

空間申請。

memcpy(bg_img_src, bmp_image, len);

把壓縮的圖檔放到bg_img_src 中。

if( gunzip(dst, dst_size, (unsigned char *)bg_img_src, &len) != 0 ){

。。。

}

解壓那個圖檔到dst 那快區域,這個空間是我們剛剛申請的。

bmp = (bmp_image_t *) dst;

至此,下面的代碼就不知道我們處理的bmp 圖檔究竟是不是壓縮過的,對于後

面的代碼處理的就是bmp 格式了。

好,圖檔顯示的代碼介紹完畢。我們的DIY 行動也很容易了。

關鍵就是傳入的坐标不合,導緻就認為是小圖檔了,這個可以改呀,改後,顯示

正常。在pmon 下也可以上QQ 喽。

想試試嘛,自己動手。

繼續閱讀