輕鬆實現Lua腳本控制W5500

※已刊登在“無線電”12月刊上輕鬆實現Lua腳本控制W5500


作者:孔東明,張博

1、引言

Lua是巴西里約熱內盧天主教大學裡的一個研究小組於1993年基於標準C開發的一個輕量級的嵌入式腳本語言,其設計目的是為了將傳統嵌入式程序“編寫→編譯→鏈接→運行”的複雜過程簡化為“編寫→運行”兩個環節,從而為嵌入應用程序提供靈活的擴展和定製功能。

Lua腳本可以很容易的被C/C++ 代碼調用,也可以反過來調用C/C++的函數,這使得Lua在應用程序中可以被廣泛應用。不僅僅作為擴展腳本,也可以作為普通的配置文件,代替XML,ini等文件格式,並且更容易理解和維護。一個完整的Lua解釋器不過200K,在目前所有腳本引擎中,Lua的速度是最快的。這一切都決定了Lua是作為嵌入式腳本的最佳選擇。

2、項目背景

隨着物聯網的快速發展,傳統的工控、電力、銀行機、閘機甚至家電等設備也紛紛加入了連接互聯網大軍。工廠的車床需要把運行數據實時上傳至PLC,水表、電錶、燃氣表實現了遠程抄錄,點驗鈔機可以實時將RMB的冠字號上傳至銀行數據庫,停車場無人值守,家裡的窗帘用某貓精靈很方便的進行語音控制……

小編在一家做網絡通信設備的公司上班,領導要求基於現有的串口轉以太網模塊開發出一款支持用戶使用Lua語言進行二次開發的串口轉以太網模塊,項目工期1個月。小編剛剛畢業4個月,沒有多少項目經驗,只是在學校玩過ARM M3的開發板,C語言自我感覺勉強及格,以太網技術基本小白一枚,對如何實現用戶使用Lua語言“二次開發”更是一竅不通。但是任務時間緊迫,再難也要搞定,要不然沒有獎金就要勒緊褲腰帶了。

接到項目當晚就去找度娘商討對策。經過一番搜索,方才大致了解了什麼是Lua,什麼是腳本語言,為什麼客戶要二次開發。用戶在使用串口轉以太網模塊時,由於應用場景的不同及嵌入式產品資源的限制,需要靈活的調用模塊的各項功能去實現差異化應用,而傳統的模塊只能實現既定的功能,因此支持二次開發的產品應用範圍將大為拓展。而用戶二次開發輸入的代碼肯定是無法執行傳統的“編寫→編譯→鏈接→運行”這整個過程,腳本語言將這個過程簡化為“編寫→運行”就可以完美的解決了這個問題,Lua便是一款最佳的嵌入式腳本語言。

原理理順了,如何落實便成了當務之急,我需要先做一個Demo來模擬整個過程。我找來了之前開發串口轉以太網模塊用到的以太網開發板W5500EVB,如下圖。W5500EVB是由ST的STM32F103RC+W5500網絡芯片構成,STM32F103內部256K的Flash足以容納最大200K的Lua驅動。W5500是一顆以太網接口芯片,它用全硬件邏輯門電路搭建了一整套全硬件TCP/IP協議棧,發送數據時單片機只需將用戶數據通過SPI發送至W5500,W5500內部會自動完成數據TCP/IP封包,並發送至網口,接收數據時W5500內部自動完成解包,僅將MCU關心的用戶數據提交。W5500內含8路完全獨立的硬件Socket,這意味着W5500可以同時運行8個上層應用程序,而且傳輸速率互不影響,不會像軟件協議棧那樣線程增加,速度明顯降下來。W5500內部還集成了MAC和PHY,符合了接入以太網的所有條件,對於剛剛接觸以太網的攻城獅來說,是一款簡單易上手的網絡接口芯片。

圖 1 W5500EVB

我想象中的Demo是這樣的:用戶通過Web網頁向W5500EVB提交一段能讓W5500EVB連接到TCP服務器的Lua腳本代碼,W5500EVB解析出來這段代碼後通過已經運行的Lua虛擬機中的Lua接口函數來解釋用戶代碼要實現的功能,最後 W5500EVB按照用戶代碼中的參數連接到一個指定的TCP服務器實現以太網數據通信。這個過程可以參考W5500官網提供的HTTP Server和TCP Client的例程。

圖 2 實施方案原理圖

3、準備工作

(1)安裝編譯環境:Keil V5.11

(2)硬件:W5500EVB、Jlink調試器

(3)驅動:Lua最新驅動V5.3.2

4、宿主C部分

4.1 加載驅動

驅動包括STM32F103RC的單片機驅動、W5500以太網部分驅動以及Lua驅動。STM32F103RC驅動不必多說,W5500驅動和Lua驅動如下圖所示,均可以在對應官網下載到。

                                         

圖 3 W5500驅動                                                                                    圖 4 Lua驅動-V5.3.2

4.2 初始化部分

初始化部分包括STM32初始化及W5500初始化,Lua在用的時候才需要初始化。

01 /******* STM32初始化********/

02 Systick_Init(72);

03 RCC_Configuration();

04 GPIO_Configuration();

05 Timer_Configuration();

06 NVIC_Configuration();

07 USART1_Init();

08 at24c16_init();

09

10 /******* W5500初始化********/

11 printf(“W5500 Config….\r\n”);

12 Reset_W5500();                  //重啟W5500

13 WIZ_SPI_Init();                          //SPI初始化

14 set_default();                  //配置默認信息

15 set_network();                           //用默認信息初始化W5500

4.3 Socket分配

W5500內含8個socket(0~7),可以同時進行8路獨立的數據通信或上次應用服務。小編這裡用Socket 7作為HTTP Server服務的端口,這樣用戶可以在後續的Lua腳本中任意使用socket 0~6來進行數據收發。

01 //Socket 0~6 can be used by user for data communication

02 #define SOCK_HTTP             7

4.4 HTTP Server部分

該部分主要是往W5500EVB內部嵌入一個HTTP Server服務和一個網頁,以實現瀏覽器訪問並接收用戶Lua腳本的功能。HTTP Server服務是基於TCP Server的短連接實現的,第一個過程,用戶在瀏覽器中輸入HTTP Server的IP地址後,瀏覽器默認會向服務器請求index.html這個頁面,W5500EVB會相應的返回一個如下的示例網頁並關閉這個連接,等待下一次請求。

圖 5 HTTP Server網頁

以下是該嵌入式網頁的源碼:

第二個過程也差不多,用戶在瀏覽器中輸入Lua腳本並提交,HTTP Server收到Lua腳本後將其原原本本的解析出來,保存在C的緩存中,等待Lua虛擬機的調用。下面是HTTP的實現過程。

01 void do_http(void) {

02     uint8 ch = SOCK_HTTP;                //使用Socket 7來處理HTTP Server服務

03     uint16 len;

04

05     st_http_request *http_request;

06     memset(rx_buf,0x00,MAX_URI_SIZE);

07     http_request = (st_http_request*)rx_buf;

08     /* socket輪詢狀態機 */

09     switch (getSn_SR(ch)) {

10     case SOCK_INIT:              //socket已經初始化

11         listen(ch);              //建立監聽,等待TCP Client連接

12         break;

13     case SOCK_LISTEN:

14         break;

15     case SOCK_ESTABLISHED:                                 //已連接

16         if (getSn_IR(ch) & Sn_IR_CON) {

17             setSn_IR(ch, Sn_IR_CON);

18         }

19         if ((len = getSn_RX_RSR(ch)) > 0) {

20             len = recv(ch, (uint8*)http_request, len);      //接收HTTP請求

21             *(((uint8*)http_request)+len) = 0;

22             proc_http(ch, (uint8*)http_request);            //發送HTTP應答

23             disconnect(ch);                                 //關閉連接

24         }

25         break;

26     case SOCK_CLOSE_WAIT:        //等待連接關閉過程中也要處理HTTP

27         if ((len = getSn_RX_RSR(ch)) > 0) {

28             len = recv(ch, (uint8*)http_request, len);

29             *(((uint8*)http_request)+len) = 0;

30             proc_http(ch, (uint8*)http_request);

31         }

32         disconnect(ch);

33         break;

34     case SOCK_CLOSED:                    //socket處於關閉狀態

35         socket(ch,Sn_MR_TCP,80,0×00);     //初始化並打開該socket

36         break;

37     default:

38         break;

39     }

5、C與Lua的交互

5.1 C與Lua的交互過程

下面是C與Lua的交互過程。第2行,Lua_State是Lua語言中的一種基本類型,用來初始化一個Lua虛擬機的運行環境。第3行,luaL_newstate是為了初始化並啟動Lua虛擬機,包括創建Lua棧空間等(Lua與C交互主要通過棧來進行,因此需要足夠的棧空間,建議設為800byte)。第4行,聲明Lua基礎庫,Lua有豐富的庫供調用,這裡基礎庫主要用來處理字符串,聲明後的Lua基礎庫會註冊在Lua全局表(Global Table)中,形成了Lua全局表的一部分。第5、6行,聲明Lua接口函數及運行用戶Lua腳本。第7行,關閉Lua。

01 void do_lua(void) {

02     lua_State* L;

03     L= luaL_newstate();                           //初始化並啟動Lua虛擬機

04     luaopen_base(L);                              //聲明Lua基礎庫

05     luaL_setfuncs(L, mylib, 0);                   //聲明Lua接口函數

06     luaL_dostring(L, LUA_SCRIPT_GLOBAL); //運行解析出來的用戶Lua腳本

07     lua_close(L);                                    //關閉Lua虛擬機

08 }

以下是聲明Lua接口函數,聲明後的Lua接口函數會註冊在Lua全局表中,形成了Lua全局表的另一部分。這些接口函數其實是在C中定義的,在運行Lua腳本時,Lua虛擬機會在Lua全局表中查詢已經註冊的C接口函數,查到後就將Lua腳本中的參數經過棧傳遞給C,C通過對應的接口函數計算出結果,再通過棧傳遞給Lua虛擬機。

01 static const struct luaL_Reg mylib[]= { //以下是聲明Lua接口函數

02     {“Get_rxbuf”,get_rx_buf},

03     {“get_state”,get_state},

04     {“socket_config”,socket_config},

05     {“connet_server”,connet_server},

06     {“recv_server”,recv_server},

07     {“close_socket”,close_socket},

08     {NULL,NULL}

09 };

以下是運行用戶Lua腳本,這是運行用戶Lua腳本的入口。當執行Get_rxbuf()時,Lua虛擬機根據Lua全局表找到get_rx_buf這個C函數接口,然後通過棧操作執行之。

01 const char LUA_SCRIPT_GLOBAL[] =”\

02   local array \

03       array=Get_rxbuf() \

04   fun=load(array)\

05   fun()\

06 “;

以下是C執行get_rx_buf的過程。C將存放input_temp緩存中的HTTP傳遞給W5500EVB的Lua腳本字符串,通過棧壓入給Lua虛擬機,Lua虛擬機會依照上述過程逐句解析該Lua腳本,再結合其他的C接口函數的計算,最終實現需要的功能。

01 static int get_rx_buf(lua_State *L) {

02     uint8 size=0;

03

04     size=strlen(input_temp);

05     if (size) {

06         lua_pushstring(L,input_temp);

07         return 1;

08     }

09     return 0;

10 }

5.2 解釋Lua腳本

以下是用戶通過網頁上傳的Lua示例腳本,Lua虛擬機通過解釋該腳本,就可以連接到TCP Server,並實現數據通信。

01 lua_mode=1                      //進入輪詢模式

02 local state=nil

03 local s=2                       //使用socket 2來進行數據TCP通信

04 local lport=5000                //配置TCP Client端口號

05 local rport=6000                //配置TCP Server端口號

06 local rip=”192.168.1.100″       //配置TCP ClientIP地址

07

08 state=get_state(s)              //查詢該socket的狀態

09 if state==”SOCK_CLOSED” then             //socket處於關閉狀態

10       socket_config(s,lport)             //初始化並打開該socket

11 elseif state==”SOCK_INIT” then           //socket處於已初始化狀態

12       connet_server(s,rip,rport)         //向服務器發起TCP連接請求

13 elseif state==”SOCK_ESTABLISHED” then    //socket處於連接建立狀態

14       recv_server(s)            //接收TCP Server發來的數據再回給TCP Server

15 else

16       close_socket(s)           //socket處於其他狀態時,關閉該socket

17 end

6、測試過程

第一步 在瀏覽器中輸入HTTP Server的IP地址(192.168.1.111)登錄網頁並提交 Lua腳本,W5500EVB解析到Lua腳本後向TCP Server發起連接。

圖 6 登錄網頁並提交 Lua腳本

第二步 PC建立TCP Server監聽、建立連接、數據收發。

圖 7 建立TCP連接並實現通信

         至此,整個Demo就實現了,其實核心部分就是要搞清楚Lua程序與C是如何交互的,以及用戶的Lua腳本字符串是如何輸入並一步步傳遞至Lua環境。當然,站在產品的角度上,這樣操作還有一些問題,如果模塊重新上電,Lua腳本就會消失。我這裡提供兩種解決思路:第一,可以在單片機中運行文件系統,將網頁提交的Lua腳本以.lua文件的形式存儲起來;第二,實際的用戶拿到模塊後一般是需要一個MCU去控制的,可以將Lua腳本放在用戶MCU中,這樣就不需要用網頁輸入Lua腳本了。不知小編的理解是否合理,還請諸位看客指正,謝過!