#一些相對高效能的單片機常帶有乙太網路接口,網口在MCU中被認為是相對複雜的外設,因為它涉及到網路協定棧的運作。通常情況下,網路協定堆疊會在一個即時作業系統(RTOS)中運行,因此對普通單晶片開發人員來說,使用網口具有一定的難度。在Linux下,網口是常用的接口,因為Linux具備成熟完整的網路通訊協定棧,而且底層驅動程式也已經由廠商提供好,因此使用起來相對比較方便。本文將對Linux下的網口使用進行簡單總結,希望能對大家有幫助。
#2.1.硬體
#\1) 網路上的一個第三方做的NUC972開發板:
有興趣購買的朋友,可以去他們的淘寶店購買:
https://s.click.taobao.com/X8mza8w
本篇和板子打交道的主要是板子的網口。
\2) 1條USB轉RS232線、1條網路線、1條電源線、1條Micrco USB線
2.2.軟體
#\1) Uboot、Kernel我們繼續使用上一篇文章用的。
\2) Rootfs我們使用Buildroot來重新生成,NUC972 Buildroot的下載地址:https://github.com/OpenNuvoton/NUC970_Buildroot ,這裡使用Buildroot重新製作Rootfs的原因是藉助Buildroot工具來添加我們想要的東西,像是本篇我們需要的ssh功能,會非常的方便,相對於自己手工去移植就容易的多。也許你體會不到,有興趣的話你可以參考網上教程手動去移植dropbear來實現ssh功能,透過兩種方式對比,你就會深有感悟了。
3)交叉工具鏈arm_linux_4.8.tar.gz,還是上一篇文章用的,我猜這個工具鏈也是Buildroot產生的。
詳細的步驟不再這裡介紹了,大家可以參考我之前發過的一篇文章《使用 Buildroot 為 I.MX6 製作根檔案系統》,有幾點在此說明一下:
\1) 下載完官方提供的Buildroot後,進入對應目錄,執行以下指令:
make nuvoton_nuc972_defconfig
#make
第一次編譯時間會稍微有點長,大家要有耐心,因為它會在線上下載很多文件。
2)關於交叉工具鏈的問題,採用的是Buildroot toolchain,選擇這個Buildroot會從零開始製作工具鏈。編譯完成後你可以看到在output/host/目錄下會有全新製作好的工具鏈,個人猜測官方提供的工具鏈也是這麼來的。
3)預設設定下沒有選擇dropbear,自己選即可。
4)編譯完成後,產生的rootfs是output/images/rootfs.tar,為了能夠燒寫到NUC972板子裡,需要先解壓縮,然後透過mkyaffs2去產生.img格式檔。
\5) 將Uboot、Kernel、Rootfs等重新下載到板子裡,配置一下dropbear和網口就可以使用了,使用passwd指令給root用戶設定一個密碼,設密碼的好處是可以防止任何人都可以直接登入系統。
將網路線連接板子和電腦,把電腦IP設定為192.168.0.50,在串口登入介面我們輸入ifconfig eth0 192.168.0.100,為了確保開機後網路就可用,將這句話加入/etc/init. d/rcS 檔案結尾。這樣後面我們就不用連接串口了,單獨使用網口就可以登入Linux系統了,同時可以給板子傳文件,不需要再按照之前那樣通過U盤拷來拷去了,效率會大大的提高。
#4.1.相關指令
和網路相關的指令,常用的ifconfig,前面設定網路卡時用過,還有ping,用來測試網路通不通,其他還有route、ethtool等,等後面實際用到時再介紹。
4.2.C語言範例
#平常用的最多就是udp和tcp通信,關於它們的基礎介紹不再這裡細述了,不太清楚的同學直接百度看兩篇文章就行了。這裡以UDP為例,下面我們來看一個十分經典的例子。
要實現的功能是:
\1) Client接收手動輸入的資料
\2) Client將上述資料傳送給Server端
\3) Server端將接收到的資料回傳給Client
直接上程式碼:
/*********************************************** * @{ * @file : udp_client.c * @brief : * @author: TopSemic * @email : topsemic@sina.com * @date : 2019-06-20 ***********************************************/ //-------------------------------------------------- // Copyright (c) Topsemic //-------------------------------------------------- #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SEND_DEST_PORT 8888 int main() { int sockfd; int ret; struct sockaddr_in addr_sender; struct sockaddr_in addr_dest; int nlen = sizeof(struct sockaddr); int recvlen=0; // sender address bzero(&addr_sender,sizeof(addr_sender)); // dest address bzero(&addr_dest,sizeof(struct sockaddr_in));//每个字节都用0填充 addr_dest.sin_family=AF_INET; addr_dest.sin_addr.s_addr=inet_addr("127.0.0.1"); addr_dest.sin_port=htons(SEND_DEST_PORT); sockfd=socket(AF_INET,SOCK_DGRAM,0); //udp 创建套接字 if(sockfd printf("create socket failure,sockfd:%d\n",sockfd); return -1; } //不断获取用户输入并发送给服务器,然后接受服务器数据 while(1) { char buff[1024] = {0x00}; printf("Please Input a string: "); fgets(buff,1024,stdin); sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&addr_dest, sizeof(struct sockaddr_in)); recvlen = recvfrom(sockfd,buff,sizeof(buff),0,(struct sockaddr *)&addr_sender,(socklen_t *)&nlen); if(recvlen > 0) { buff[recvlen] = 0x00; printf("Message form server: %s\n", buff); printf("sender ip:%s port:%d\n",inet_ntoa(addr_sender.sin_addr),ntohs(addr_sender.sin_port)); } printf("**************************************\n"); } close(sockfd); return 0; }
首先我們在Ubuntu裡使用gcc編譯一下,注意不是交叉編譯arm-linux-gcc,我們先在PC上先運行Server後運行Client,可以看下效果,它實現了上述我們想要的功能。
上述程式碼大家可以仔細去看一遍,有幾處需要說明的地方:
1)UDP不同於TCP,不存在請求連接和受理過程,因此實際中是不明確區分伺服器端和客戶端的,上述命名為server和client只是方便描述而已,我是這麼理解的:先發送資料(請求資料)後接收的是客戶端,先接收資料後發送資料的是服務端。
\2) 大家有沒有註意到在server 範例裡有呼叫bind函數,但是client例子裡並沒有,這個原因是什麼呢?原因是這樣,因為Server工作首先得接收數據,如果不綁定端口的話,那是沒法知道該在哪裡接收數據的。 Client之所以不用綁定是因為它先發,發完緊接著是可以在發送的連接埠接收到資料的。
\3) 實際工作中我發現好多人包括我自己會經常被端口給繞暈。這裡再總結一下,UDP接收的時候需要bind一個連接埠號碼(這個連接埠自己設備的連接埠),才可以接從這個連接埠接收數據,收到資料後會得到對方的IP位址和發送連接埠號碼。發送的時候指明對方的IP和連接埠即可,本機的發送埠隨機分配,不需要綁定埠。
為了驗證不綁定端口發送的端口是隨機分配的,我們可以再做個小試驗,我們把Client關掉,再重新打開一次,我們看一下前後兩次打印的端口信息,我們可以看到兩次的連接埠號碼是不同的。
\4) 在呼叫socket建立套接字時,函數的第二個參數傳遞 SOCK_DGRAM,指明使用的是UDP協定。如果是TCP的話,該參數是SOCK_STREAM.
\5) addr_local成員變數賦值時使用htonl(INADDR_ANY)來自動取得IP位址。
使用INADDR_ANY的好處是,當軟體運行到其他主機或主機IP位址改變時,不用再更改原始碼重新編譯,也不用在啟動軟體時手動輸入。而且,如果一台主機中已分配多個IP位址,那麼只要連接埠號碼一致,就可以從不同的IP位址接收資料。
\6) 在Client裡發送時我們制定的IP是127.0.0.1,這是一個比較特殊的ip位址,你用ifconfig看看,在Ubuntu下和板子上都可以看到:
從網路找了一段英文描述:
127.0.0.1 is the loopback Internet protocol (IP) address also referred to as the “localhost.” The address is used to establish an IP connection to the same machine or computer being used by the enduser. 可以簡單的理解是代表本機自己。
下一步我們把Client程式碼交叉編譯放到板子上跑一下試試,我們需要做兩個小一點的改變:
第一處addr_dest.sin_addr.s_addr=inet_addr(“127.0.0.1”);改為:
addr_dest.sin_addr.s_addr=inet_addr(“192.168.0.50”);
192.168.0.50 是PC端的IP位址。
第二處while(1)中的這三句話
char buff[1024] = {0x00};
#printf(“Please Input a string: “);
fgets(buff,1024,stdin);
改為:
char buff[1024] = “Hello TopSemic Friends!”;
//printf(“Please Input a string: “);
//fgets(buff,1024,stdin);
目的是讓client自動的傳送資料、接收數據,不再等待使用者輸入訊息。
在Ubuntu下透過scp指令直接將檔案放到板子的/opt目錄裡
scp udp_client root@192.168.0.100:/opt
另外我們直接在Ubuntu下透過ssh指令登入Linux系統
ssh root@192.168.0.100:/opt
退出的話輸入exit即可,就可以回到Ubuntu的命令列視窗。
這樣登入板子、給板子上傳檔案過程都可以很方便的在Ubuntu裡操作了,相比之前的Windows串口登入、U盤傳輸文件方便很多。
我滿懷欣喜的在Ubuntu下運行udp_server,Ubuntu下ssh登入板子裡運行udp_client,以為直接就能運行成功,結果出現了意外情況,實際上是壓根就沒有結果輸出。
可是明明虛擬機Ubuntu都能登入板子上也能ping通,板子也能ping 192.168.0.50這個IP,為什麼udp卻通不了呢。後來經過一段時間琢磨解決了這個問題。解決辦法如下:
虛擬機器預設的網路設定模式是下面所示的NAT模式,
我們把它修改成下圖所示的橋接模式:
然後進入把網路線拔掉重新連接一下,在Ubuntu虛擬機器裡修改一下網路設定
將虛擬機器的有線連接改成手動設定的固定IP,192.168.0.xx網段(不要和Windows 以及板子IP衝突)。可以ifconfig驗證一下是否設定成功
這時再透過登入板子上ping 192.168.0.80,是可以ping通的。之前ping 192.168.0.50 那是Windows主機的IP,可以通不代表和虛擬機器能通。
最後把上述程式碼裡IP改掉,
addr_dest.sin_addr.s_addr=inet_addr(“192.168.0.80”);
重新再編譯下載運行一次,就可以正常運作了。
補充一點:板子平時調試,也會經常使用Windows下的網路調試助手,該工具使用只要正確配置協議類型、本地主機地址、本地主機端口,遠端主機,之後發送,就可以查看結果了。
例如我們也可以在Windows開啟網路偵錯助手,模擬客戶端和虛擬機器Server通信,如下:
舉出一個實際工作中非常普遍容易犯的錯誤。
假設你的處理器透過網口和外部的一個設備通信,使用udp通信方式,正常的工作流程如下圖,由你先發送資料過去,然後外部設備給你應答。
這個模型和上述Server、Client模型都非常類似,你要實現的就是Client。也就是呼叫sendto函數先發送,然後呼叫recvfrom函數去接收。正常情況下程式這麼寫是沒有問題的,但是實際中你得考慮很多的異常情況,比如正常工作的過程中外部設備突然斷電再上電或者重啟(但是你的CPU設備沒有斷電),這時會出現什麼問題呢?由於外部設備斷電,recvfrom函數就會因為收不到數據而阻塞,即使外部設備重新上電初始化後,它也因為沒有收到數據而不會給出應答數據,導致你的recvfrom函數一直卡住不動。
這樣的程式碼如果發佈到現場,將會帶來很大的隱患,因為現場出現上述情況很正常,還有比如你的CPU設備先上電,外部設備後上電也會出現上述問題。我之前專案就因為這個問題,導致客戶抱怨產品有問題,客戶發現如果通訊失敗,只有設備重上電才可以解決。
解决上述问题的办法也很简单,可以设置一个超时,使用setsockopt函数,让接收函数在超时时间内没有接收到数据时就返回就行了。返回后再接着重头发送数据即可,框架如下:
/* 设置阻塞超时 */
struct timeval timeout = {3, 0}; // 设置3s超时
if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(struct timeval))
{
<code style="display: -webkit-box;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;border-radius: 0px;font-size: 12px">printf("time out setting failed "); </code>
}
.
.
.
/* 数据阻塞接收 */
int receivePacketLen = recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)& addr_sender,&addrLen);
if(receivePacketLen != -1)
{
//接收到数据
…
}
else if (errno == EAGAIN) //阻塞接收超时
{
<code style="display: -webkit-box;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;border-radius: 0px;font-size: 12px">printf("udp receive timeout! "); return -1; </code>
}
为了大家更直观的感受这个问题,我们在上面实验的基础上来模拟这个场景,我们先运行upd_client,后运行udp_server,大家看下现象,结果自然是没有数据输出。
道理不难想明白,client程序运行后,先发送了数据,然后就阻塞在读那里不动了。我们把程序简单修改下:
// Max Recv block timeout in second #define gMaxRecvBlockTimeout 3 … … … // Set recv timeout struct timeval timeout = {gMaxRecvBlockTimeout, 0}; if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(struct timeval)) printf("time out setting failed "); } //不断获取用户输入并发送给服务器,然后接受服务器数据 while(1) { char buff[1024] = "Hello TopSemic Friends!"; //printf("Please Input a string: "); //fgets(buff,1024,stdin); sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&addr_dest, sizeof(struct sockaddr_in)); recvlen = recvfrom(sockfd,buff,sizeof(buff),0,(struct sockaddr *)&addr_sender,(socklen_t *)&nlen); if(recvlen > 0) { buff[recvlen] = 0x00; printf("Message form server: %s ", buff); printf("sender ip:%s port:%d ",inet_ntoa(addr_sender.sin_addr),ntohs(addr_sender.sin_port)); } else if(errno == EAGAIN) // 阻塞接收超时 { printf("udp receive timeout! "); } printf("************************************** "); } close(sockfd); return 0;
这时我们先运行client,
打印如上,然后再运行Server,就可以正常工作了,不会再出现上述问题。
以上是嵌入式Linux系列第8篇:操作網口的詳細內容。更多資訊請關注PHP中文網其他相關文章!