在U-boot中添加以太網驅動
當定義CONFIG_CMD_NET和CONFIG_CMD_PING,編譯之後執行ping命令,告警沒有找到以太網。
因此,需要打開U-boot的網絡功能, u-boot-sunxi-sunxi中沒有找到明顯的網絡驅動代碼,或許有通用的驅動,但可以獲得資料的途徑有限,再說我是個初學者,平時工作屬於自動控製類,網絡方麵很菜,因此想通過修改一個網絡驅動,進行一次初步學習,想到就開工...
邊做邊寫,恐怕會比較亂。
開發環境: 1、筆記本RHEL5,安裝編譯器arm-none-eabi-版本4.7.2; 編輯器Vim;minicom2.1
2、台式機XPsp3,安裝SourceInsight3.5
基本思路: 1、找到u-boot內網絡代碼運行的軌跡,初始化、數據交換的驅動接口等等
2、實現一個驅動,加入到這個運行軌跡中,設備初始化,數據讀寫等等
準備工作: 1、找到芯片資料,這個比較坑,隻能找到RTL8021CP的PDF,至於CPU芯片資料,
×,那簡直不能稱為資料,看看三星處理器資料,為啥別人能做大,不是沒有原因的。
2、Cubieboard原理圖一份,這個好弄,人手一份呀
3、網線準備了兩根,一個常用上網的,一根交叉線。路由器一隻。
在沒有研究清楚硬件連接之前,這樣準備應該比較充足了。
4、下個新版本編譯器,找了一個arm-none-eabi-的編譯器,版本4.7.2,估計是目前最高版本。
下載:http://www.codesourcery.com/sgpp/lite/arm
選擇arm處理器,linux版本,點進去之後需要用郵箱注冊,下載地址會發到郵箱。
5、因為沒有CPU資料,需要尋找一個A10的網絡驅動代碼,在支持Cubieboard的內核中找到了。
下載:https://linux-sunxi.org/Cubieboard/Cubieboard_Linux-3.9
這個驅動是linux下的,需要修改一下或參考其操作硬件的過程,以便在u-boot內運行。
一、環境建立,初步編譯
考慮到這次需要參考其他開發板或CPU的網絡驅動,因此用SourceInsight3.5建立一個u-boot-sunxi-sunxi的工程,建立關聯關係,方便代碼查閱。建立方法很簡單,不懂的可以網上搜索一下。建立工程時不要刪除其他代碼,全部使用。
linux下編譯環境設置,將下載的編譯器解壓到 /usr/local/arm/目錄,解壓之後目錄是arm-2012.09
設置環境變量,在RHEL5係統內,在/ect/profile的末尾加上一句:
export PATH=$PATH:/usr/local/arm/arm-2012.09/bin
保存之後,執行:source /ect/profile 或者logout,或者重啟機器。
回到bash,輸入arm再按Tab鍵,可以看到編譯工具鏈列出來了,執行
arm-none-eabi-gcc -v,可以看到版本是4.7.2
其他係統編譯器安裝設置可以在網上搜索,有很多文章會提到。
進入u-boot-sunxi-sunxi目錄,修改Makefile內容,找到 CROSS_COMPILE ?= 這一行
改為:CROSS_COMPILE ?= arm-none-eabi-
進入 u-boot-sunxi-sunxi/arch/arm,修改config.mk內容,也是這一句 CROSS_COMPILE ?=
改為:CROSS_COMPILE ?= arm-none-eabi-
回到u-boot-sunxi-sunxi目錄,執行:
make distclean //清除之前編譯的內容
make cubieboard_config //設置板子
make -j4 //編譯,-j4表示多線程編譯,可以使用-j2,如果是虛擬機可以不用這個參數,
//如果電腦配置比較好,可以使用 -j8
等待一會,不出意外,編譯完成,寫入SD卡,到板子上運行,可以看到串口輸出信息。
二、U-boot網絡模塊分析
1、網絡初始化
之前曾經分析過主循環,在主循環main_loop()調用之前就是初始化。
在文件\u-boot-sunxi-sunxi\arch\arm\lib\board.c, 函數board_init_r()內:
...........
#if defined(CONFIG_CMD_NET)
puts("Net: ");
eth_initialize(gd->bd);
#if defined(CONFIG_RESET_PHY_R)
debug("Reset Ethernet PHY\n");
reset_phy();
#endif
#endif
...........
這一段代碼可以看出,要執行網絡初始化,需要定義CONFIG_CMD_NET。
因此在\u-boot-sunxi-sunxi\include\configs\sunxi-common.h 末尾加一句:
#define CONFIG_CMD_NET
再來看看函數eth_initialize() 的內容,在文件 \u-boot-sunxi-sunxi\net\eth.c內:
.................
if (board_eth_init != __def_eth_init) {
if (board_eth_init(bis) < 0)
printf("Board Net Initialization Failed\n");
} else if (cpu_eth_init != __def_eth_init) {
if (cpu_eth_init(bis) < 0)
printf("CPU Net Initialization Failed\n");
} else
printf("Net Initialization Skipped\n");
.................
如果隻定義CONFIG_CMD_NET,在上電時就會打印 Net Initialization Skipped,執行了最後一個else的內容,因此,需要完成網絡初始化,需要實現函數board_eth_init()或者cpu_eth_init()
看代碼,這兩個函數誰等於__def_eth_init就執行誰,在看看__def_eth_init是啥,也在eth.c這個文件內
static int __def_eth_init(bd_t *bis)
{
return -1;
}
int cpu_eth_init(bd_t *bis) __attribute__((weak, alias("__def_eth_init")));
int board_eth_init(bd_t *bis) __attribute__((weak, alias("__def_eth_init")));
可見,實現__def_eth_init的“alias ”,bash命令裏麵有個alias,如果你用過就明白這是什麼意思了,實現它,就可以進行初始化啦。
想知道weak, alias是什麼意思,請圍觀這位大俠的博客 https://blog.chinaunix.net/uid-20272712-id-1969771.html
以太網模塊在A10內,A10的手冊稱之為WEMAC模塊,因此,我們需要實現cpu_eth_init函數,表明eathnet模塊在CPU內部。
在文件\u-boot-sunxi-sunxi\arch\arm\cpu\armv7\sunxi\board.c內增加函數cpu_eth_init,內容如下:
#ifdef CONFIG_SUN4I_WEMAC
int cpu_eth_init(bd_t *bis)
{
return sun4i_wemac_initialize(bis);
}
#endif
2、如何實現網絡模塊
網絡模塊已經成熟了,我們並不需要增加很多代碼,隻需要實現對硬件的操作就可以了。
基本的操作大約就幾個:初始化、打開、關閉、接收、發送、掛起
分析\u-boot-sunxi-sunxi\net目錄內的代碼,發現網絡的數據結構定義在\u-boot-sunxi-sunxi\include\net.h內
struct eth_device {
char name[16]; //網絡名
unsigned char enetaddr[6]; //以太網地址
int iobase; //io基址?
int state; //設備狀態
int (*init) (struct eth_device *, bd_t *); //網絡設備初始化
int (*send) (struct eth_device *, void *packet, int length); //發送數據
int (*recv) (struct eth_device *); //接收數據
void (*halt) (struct eth_device *); //掛起
#ifdef CONFIG_MCAST_TFTP
int (*mcast) (struct eth_device *, u32 ip, u8 set);
#endif
int (*write_hwaddr) (struct eth_device *); //寫硬件地址?這個是啥?
struct eth_device *next;
int index;
void *priv;
};
struct eth_device結構體定義了網絡設備的基本操作,從麵向對象的角度來說,隻要在驅動代碼內將struct eth_device的init,send,recv,halt,write_hwaddr這幾種方法實現,就可以實現網絡的操作了,至於數據收回來之後在上層的解析方式那就不是驅動關心的了。任務變得簡單了,實現這幾種操作便可
再看struct eth_device結構體下麵這幾個函數:
extern int eth_initialize(bd_t *bis); /* Initialize network subsystem */
extern int eth_register(struct eth_device* dev);/* Register network device */
extern int eth_unregister(struct eth_device *dev);/* Remove network device */
extern void eth_try_another(int first_restart); /* Change the device */
extern void eth_set_current(void); /* set nterface to ethcur var */
都在\u-boot-sunxi-sunxi\net\eth.c內實現,分析一下eth_register這個函數(其他的就不多說了),如下,
int eth_register( struct eth_device *dev )
{
struct eth_device *d;
static int index;
assert( strlen(dev->name) < sizeof(dev->name) );
if (!eth_devices) {
//注冊之前eth_devices應該初始化為NULL,在當前文件的函數int eth_initialize( bd_t *bis ) 中,
//開始幾句代碼就這樣初始化了,但是這個函數eth_initialize將調用我們自己寫的cpu_eth_init(),
//並未調用eth_register,因此可以預見,自己寫的函數cpu_eth_init將要調用eth_register對網絡進行注冊,
//實際工作還是要自己實現
eth_current = eth_devices = dev;
eth_current_changed();
} else {
for (d = eth_devices; d->next != eth_devices; d = d->next)
;
d->next = dev;
}
dev->state = ETH_STATE_INIT; //網絡設備狀態為初始化
dev->next = eth_devices; //鏈表的下一個指向自己,為啥呢?
dev->index = index++; //設備個數增加了
return 0;
}
三、實現網絡驅動
這些內容都將實現在sun4i_wemac.c和sun4i_wemac.h內。這兩個文件都在\u-boot-sunxi-sunxi\driver\net\目錄下。
1、數據結構
經過上麵分析,可以了解到,假設自己定義一個網絡設備的數據結構,那麼這個結構大致如下,下麵直接寫一個,實際代碼還要推敲推敲:
struct sun4i_wemac_dev {
void *wemac_base; // A10內部wemac模塊的基地址
void *gpio_base; //wemac模塊使用的gpio的基地址,去看看原理圖,實際使用A10的PA口
//接收發送buffer管理,使用隊列
unsigned int rx_head;
unsigned int rx_tail;
unsigned int tx_head;
unsigned int tx_tail;
void *rx_buffer;
void *tx_buffer;
struct eth_device netdev; //這就是上麵的以太網設備,這個一定要有
unsigned short phy_addr; //PHY地址,就是板子上RTL8201CP的地址
};
有的人又問啦,為什麼要自己定義一個數據結構呢?net.h裏麵不是已經有一個struct eth_device了嗎?仔細想一下,struct eth_device是u-boot定義的數據結構,裏麵的每一個成員可能在u-boot的其他網絡模塊代碼中被使用修改,我們並不完全知道所有代碼對struct eth_device的操作,因此需要自己定義一個結構,提供這個接口就可以了。用一句話總結就是:struct eth_device是定義給u-boot的網絡模塊使用的,用戶得定義自己的設備,以此隔離了驅動代碼與u-boot代碼。
2、實現初始化
就是上麵提到的cpu_eth_init()內調用的sun4i_wemac_initialize()函數,隻寫一個思路,具體代碼還需要推敲:
int sun4i_wemac_initialize( bd_t *bis )
{
struct sun4i_wemac_dev *wemac_dev; //自己定義的結構
struct eth_device *netdev; //u-boot已經定義的結構
wemac_dev = malloc( sizeof ( struct sun4i_wemac_dev ) ); //分配內存
if ( wemac_dev ) {
printf("Error: Failed to allocate memory for WEMAC\n");
return -1;
}
memset(wemac_dev , 0, sizeof( struct sun4i_wemac_dev ));
netdev = &wemac_dev ->netdev;
/*****************************/
初始化發送、接收管理隊列
若還需要其他功能,可以增加到數據結構struct sun4i_wemac_dev中,並在此初始化,
寫文章時,代碼還沒寫,留待後續補全
/*****************************/
wemac_dev ->wemac_base = (void *)EMAC_BASE;
wemac_dev ->gpio_base = (void *)PA_BASE;
wemac_dev ->phy_addr = WEMAC_PHY;
//以下就是要實現的網絡設備結構體內的“方法”,就是驅動代碼中主要的幾個函數,
//可以參考從內核拷貝過來的驅動是如何實現的:
netdev->init = sun4i_wemac_init; //注意這個初始化函數跟當前函數是不同的,這個函數
//主要初始化wemac模塊和RTL8201芯片
netdev->halt = sun4i_wemac_halt;
netdev->send = sun4i_wemac_send;
netdev->recv = sun4i_wemac_recv;
netdev->write_hwaddr = sun4i_wemac_write_hwaddr;
.....................其他初始化...........
eth_register( netdev ); // 最後將網絡設備注冊,這樣u-boot就能使用驅動程序啦
// 注冊之後,eth.c內的全局變量eth_current 指向wemac_dev ->netdev,
//這是在u-boot任何代碼使用eth_current,
// 都表示使用的是現在初始化的模塊
return 0;
}
3、其他模塊的實現
看看eth.c內怎麼實現接收和發送接口的,就可以知道驅動代碼應該怎麼寫了
int eth_send(void *packet, int length)
{
if ( !eth_current ) //eth_current是全局指針,指向驅動初始化的結構體wemac_dev ->netdev
return -1;
return eth_current->send(eth_current, packet, length); //注冊之後,實際調用就是sun4i_wemac_send;
}
int eth_rx(void)
{
if (!eth_current)
return -1;
return eth_current->recv(eth_current); //注冊之後,實際調用就是sun4i_wemac_recv;
}
因此,可以知道,驅動代碼實現的接收發送形式如下:
static int sun4i_wemac_send( struct eth_device *dev, void *packet, int length )
{
struct sun4i_wemac_dev wemac_dev = to_wemac(dev);
//功能如何實現,可以參考內核代碼的驅動。
}
static int sun4i_wemac_recv( struct eth_device *dev )
{
struct sun4i_wemac_dev wemac_dev = to_wemac(dev);
//功能如何實現,可以參考內核代碼的驅動。
}
由於我們自己定義的結構體是struct sun4i_wemac_dev,而傳過來的參數是全局指針eth_current,
根據上麵的分析,eth_current指向struct sun4i_wemac_dev結構內的struct eth_device netdev,
因此要獲得指向struct sun4i_wemac_dev的指針需要使用linux內核常用的手段,“容器”的概念就出來了,
看代碼定義:
struct sun4i_wemac_dev wemac_dev = container_of( dev , struct sun4i_wemac_dev , netdev);
就可以獲得注冊之前申請的結構體的首地址,如何辦到的呢?
來看看這個在linux內核中常用的宏:
#define container_of( ptr, type, member ) ( { \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) ); } )
#define offsetof( TYPE, MEMBER ) ( (size_t) & ((TYPE *)0)->MEMBER )
這是一個很神奇的宏,可以把它用於自己以後的C代碼開發中,如果使用其他編譯器,這裏麵的關鍵字要改一下。
意思就是,dev指向的內存地址,減掉它在struct sun4i_wemac_dev中的偏移量,就獲得了初始化時定義的
struct sun4i_wemac_dev 指針
偏移量的獲取很簡單 (( struct sun4i_wemac_dev *) 0)->netdev 就是偏移量;我們知道使用 -> 操作時,獲得指向當前結構體的某一個成員的地址, 而這個成員的地址與結構體首地址的差值,就是它在結構體內的偏移量,如果把結構體的首地址設置為0,結果妙不可言。
由於很多函數都要用到這個宏,因此可以再寫成下麵這樣:
#define to_wemac(_nd) container_of( _nd, struct sun4i_wemac_dev, netdev)
上麵的定義就可以這樣: struct sun4i_wemac_dev wemac_dev = to_wemac(dev);
另外3個個函數,實現方式也是一樣,先去分析eth.c內調用,一並都分析一下吧:
int eth_init(bd_t *bis)
{
struct eth_device *old_current, *dev;
if (!eth_current) { //必須注冊成功之後,否則這裏判斷失敗,將打印找不到ethernet
puts("No ethernet found.\n");
return -1;
}
/* Sync environment with network devices */
dev = eth_devices;
do {
uchar env_enetaddr[6];
if ( eth_getenv_enetaddr_by_index( "eth", dev->index, env_enetaddr))
memcpy(dev->enetaddr, env_enetaddr, 6);
//這裏就是獲取IP地址了,從哪裏獲取呢?這是u-boot解決的問題啦
//熟悉情況的人這時會想起一個文件,就是u-boot移植時說的參數配置文件
//uEnv.txt或boot.scr,怎麼獲得,需要進一步追查代碼流程,這不是網絡驅動關心的。
dev = dev->next;
} while ( dev != eth_devices ); // CB上隻有一個以太網設備,因此循環一次就會結束
old_current = eth_current;
do {
debug("Trying %s\n", eth_current->name);
if ( eth_current->init( eth_current, bis ) >= 0 ) { //實際調用的就是函數sun4i_wemac_init ,
//對WEMAC模塊和PHY芯片進行初始化
eth_current->state = ETH_STATE_ACTIVE; //初始化成功,處於活動狀態標誌
return 0; //成功就返回啦
}
debug("FAIL\n");
eth_try_another(0); //不成功,試試另一個,不用分析,這個函數可定會嚐試改變全局指針eth_current
} while (old_current != eth_current);
return -1;
}
還有一個疑問,那又是什麼地方調用int eth_init(bd_t *bis)這個函數呢?借助強大的Source Insight,很快就能找到調用它的地方啦,搜索工程,出來一大片,隻需關心文件net.c的調用即可,其他調用是不會被編譯進來的,以下兩個函數調用了這個初始化,從名字就能看出其大概功能了
int NetLoop( enum proto_t protocol ); //這就是讀取網絡數據的主循環
void NetStartAgain( void ); //這個是net重新開始,當然要初始化
再來看看ping命令幹了啥,在文件\u-boot-sunxi-sunxi\common\cmd_net.c
#if defined(CONFIG_CMD_PING)
int do_ping (cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
if (argc < 2)
return -1;
NetPingIP = string_to_ip(argv[1]);
if (NetPingIP == 0)
return CMD_RET_USAGE;
if (NetLoop(PING) < 0) { //這裏調用了NetLoop函數,再調用eth_init,再調用自己寫的sun4i_wemac_init
printf("ping failed; host %s is not alive\n", argv[1]);
return 1;
}
printf("host %s is alive\n", argv[1]);
return 0;
}
U_BOOT_CMD(
ping, 2, 1, do_ping,
"send ICMP ECHO_REQUEST to network host",
"pingAddress"
);
#endif
至於NetLoop內如何實現數據打包,如何解析,有興趣的可以繼續深入。
void eth_halt(void)
{
if (!eth_current)
return;
eth_current->halt(eth_current); //實際就是調用sun4i_wemac_halt,網絡設備掛起
eth_current->state = ETH_STATE_PASSIVE; //標誌變化
}
int eth_write_hwaddr(struct eth_device *dev, const char *base_name, int eth_number)
{
unsigned char env_enetaddr[6];
int ret = 0;
eth_getenv_enetaddr_by_index(base_name, eth_number, env_enetaddr);
if (memcmp(env_enetaddr, "\0\0\0\0\0\0", 6)) {
if (memcmp(dev->enetaddr, "\0\0\0\0\0\0", 6) && memcmp(dev->enetaddr, env_enetaddr, 6)) {
printf("\nWarning: %s MAC addresses don't match:\n", dev->name);
printf("Address in SROM is %pM\n", dev->enetaddr);
printf("Address in environment is %pM\n", env_enetaddr);
}
memcpy(dev->enetaddr, env_enetaddr, 6);
} else if (is_valid_ether_addr(dev->enetaddr)) {
eth_setenv_enetaddr_by_index(base_name, eth_number, dev->enetaddr);
printf("\nWarning: %s using MAC address from net device\n", dev->name);
}
if (dev->write_hwaddr && !eth_mac_skip(eth_number)) {
if (!is_valid_ether_addr(dev->enetaddr))
return -1;
ret = dev->write_hwaddr(dev);//實際就是調用sun4i_wemac_write_hwaddr,將IP地址寫入硬件??
}
return ret;
}
這三個函數形式如下:
int sun4i_wemac_init ( struct eth_device *, bd_t * );
void sun4i_wemac_halt( struct eth_device * );
int sun4i_wemac_write_hwaddr( struct eth_device * );
具體內容如何實現,在沒有詳細CPU手冊的情況下,參考內核驅動代碼是最好的選擇,如果熟悉內核驅動編程就更好了,那是下一個目標。
四、驅動完成之後的初步測試
測試ping命令
測試tftp命令-----這個命令還沒有,下一次學習的目標。
代碼還沒寫,留待後續補全
最後更新:2017-04-03 12:53:54