目錄
參考示例
前言
一、需使用的組件與軟件包及其ENV配置
1、文件系統相關組件與軟件包
1.1、DFS 框架
1.2、fal 軟件包
1.3、SFUD 組件
2、網絡通信相關組件和軟件包
2.1、SAL組件
2.2、netdev組件
2.3、協議棧組件
2.4、netutils工具集軟件包
2.5、webnet軟件包
二、添加驅動和初始化代碼
1、SPI FLASH驅動
2、網卡驅動
3、FAL配置
4、格式化塊設備
三、web服務器開發基礎
1、HTTP簡介
1.1、工作原理
1.2、消息結構
2、HTML簡介
2.1、HTML 網頁結構
2.2、HTML 標簽
2.3、HTML 屬性
3、CSS簡介
4、CGI技術簡介
5、SSI簡介
四、web服務器應用程序設計
1、網頁制作
1.1、效果圖
1.2、CGI類型與SSI標簽
2、功能實現
2.1、自定義注冊CGI執行函數
2.2、自定義注冊SSI執行函數
3、頁面文件上傳
4、啟動webnet
5、解決的問題
參考示例
網絡協議棧驅動移植筆記
在 STM32F407 上應用網絡功能
在 STM32F429 上應用文件系統
SPI Flash 文件系統例程
前言
1、此次實現的web服務器是使用了rtthread的webnet軟件包來實現的。WebNet 軟件包是 RT-Thread 自主研發的,基於 HTTP 協議的 Web 服務器實現,它不僅提供設備(HTTP Seerver)與 HTTP Client 通訊的基本功能,而且支持多種模塊功能擴展,滿足開發者對嵌入式設備服務器的功能需求。要將WebNet軟件包用起來,基礎的網絡通信功能肯定是需要的,同時還需要能對 靜態頁面 進行存儲、上傳 等功能,所以WebNet的使用還需要文件系統相關的組件和網絡通信相關的組件的支持,通過這些組件和軟件包可以快速搭建好一個在STM32開發web服務器的環境。在搭建好環境后,先使用HBuilder(HTML5的Web開發IDE)制作好你的網頁,然后將這些網頁使用tftp工具上傳到/webnet目錄下,最后使用webnet軟件包提供的SSI、CGI等功能模塊實現web服務器與瀏覽器之間的交互
2、使用的硬件為 正點原子的阿波羅STM32F429開發板
3、在ENV中選中的組件或軟件包,如果開啟了包管理器自動更新或者手動使用 pkgs --update 命令,就能自動將選擇的軟件包更新到BSP中;然后再使用 scons --target=xxx 命令編譯BSP時,選擇的軟件包相關源代碼就會被自動添加進工程中並進行編譯
一、需使用的組件與軟件包及其ENV配置
1、文件系統相關組件與軟件包
1.1、DFS 框架
DFS 框架 是 RT-Thread 提供的虛擬文件系統組件,全稱為 Device File System,即設備虛擬文件系統。DFS 框架為應用程序提供統一的 POSIX 文件和目錄操作接口,如 read、write、poll/select 等。DFS 框架支持多種類型的文件系統,如 FatFS、RomFS、DevFS 等,並提供普通文件、設備文件、網絡文件描述符的管理。
1.2、fal 軟件包
fal 全稱為 Flash Abstraction Layer,即 Flash 抽象層,是對 Flash 及基於 Flash 的分區進行管理、操作的抽象層,對上層統一了 Flash 及 分區操作的 API。並提供了將分區創建成 MTD 設備的 API
1.3、SFUD 組件
SFUD 是一款開源的串行 SPI Flash 通用驅動庫。現有市面的大部分串行 Flash,用戶只需要提供 SPI 或 QSPI 的讀寫接口,SFUD 就可以識別並驅動。同時 RT-Thread 提供了 FAL 針對 SFUD 的驅動移植,可以使兩個組件無縫連接
2、網絡通信相關組件和軟件包
2.1、SAL組件
SAL 套接字抽象層,通過它 RT-Thread 系統能夠適配下層不同的網絡協議棧,並提供給上層統一的網絡編程接口,方便不同協議棧的接入。套接字抽象層為上層應用層提供接口有:accept、connect、send、recv 等。具有如下特點:
抽象、統一多種網絡協議棧接口;
提供 Socket 層面的 TLS 加密傳輸特性;
支持標准 BSD Socket API;
統一的 FD 管理,便於使用 read/write poll/select 來操作網絡功能;
2.2、netdev組件
netdev 網卡層,主要作用是解決多網卡情況設備網絡連接和網絡管理相關問題,通過 netdev 網卡層用戶可以統一管理各個網卡信息和網絡連接狀態,並且可以使用統一的網卡調試命令接口
2.3、協議棧組件
協議棧層包括幾種常用的 TCP/IP 協議棧,例如嵌入式開發中常用的輕型 TCP/IP 協議棧 lwIP 以及 RT-Thread 自主研發的 AT Socket 網絡功能實現等。這些協議棧或網絡功能實現直接和硬件接觸,完成數據從網絡層到傳輸層的轉化。這里使用的是lwip
2.4、netutils工具集軟件包
netutils軟件包中匯集了 RT-Thread 可用的全部網絡小工具集合,這里主要使用TFTP小工具,TFTP (Trivial File Transfer Protocol, 簡單文件傳輸協議)是 TCP/IP 協議族中的一個用來在客戶機與服務器之間進行簡單文件傳輸的協議,提供不復雜、開銷不大的文件傳輸服務,端口號為 69 ,比傳統的 FTP 協議要輕量級很多,適用於小型的嵌入式產品上。在板卡上開啟TFTP Server后,就可以在PC上使用TFTP Client軟件將HTML網頁文件上傳到板卡的SPI FLASH中。
2.5、webnet軟件包
官網有很詳細的介紹,WebNet 軟件包功能特點:
支持 HTTP 1.0/1.1
支持 CGI 功能
支持 ASP 變量替換功能
支持 AUTH 基本認證功能
支持 INDEX 目錄文件顯示功能
支持 ALIAS 別名訪問功能
支持 SSI 文件嵌入功能
支持文件上傳功能
支持預壓縮功能
支持緩存功能
支持斷點續傳功能
瀏覽器訪問設備 IP 地址不顯示頁面信息
原因:設置的根目錄地址錯誤。
解決方法:確定設置的根目錄地址(/webnet)和設備文件系統上創建的目錄地址一致,確定根目錄下有頁面文件。也就是說必須先在塊設備上初始化文件系統,且在文件系統中有 /webnet 這個文件夾,同時頁面文件也已經上傳到了跟目錄下。
二、添加驅動和初始化代碼
1、SPI FLASH驅動
1.1、在spi_flash_init.c中添加如下內容,注冊softspi1總線,注冊softspi10設備並掛載到softspi1總線上;使能SFUD驅動W25Q64塊設備
#include <rtthread.h>
#include "spi_flash.h"
#include "spi_flash_sfud.h"
#include "drv_soft_spi.h"
#if defined(BSP_USING_SPI_FLASH)
static int rt_hw_spi_flash_init(void)
{
__HAL_RCC_GPIOG_CLK_ENABLE();
rt_soft_spi_device_attach("softspi1", "softspi10", GPIOG, GPIO_PIN_10);
if (RT_NULL == rt_sfud_flash_probe("W25Q64", "softspi10"))
{
return -RT_ERROR;
}
return RT_EOK;
}
INIT_COMPONENT_EXPORT(rt_hw_spi_flash_init);
#endif
1.2、在ENV中開啟模擬SPI,開啟BSP_USING_SOFT_SPI和BSP_USING_SOFT_SPI1宏定義,這樣在scons構建工程時,drv_soft_spi.c 就能自動添加進工程中
2、網卡驅動
2.1、網卡驅動部分rtthread已經在drv_eth.c/h中寫好了,唯一要改的就是在phy_reset.c中添加PHY網卡的復位,添加如下內容
#define ETH_RESET_IO GET_PIN(H, 3) //PHY RESET PIN
/* phy reset */
void phy_reset(void)
{
rt_pin_write(ETH_RESET_IO, PIN_HIGH);
rt_thread_mdelay(100);
rt_pin_write(ETH_RESET_IO, PIN_LOW);
rt_thread_mdelay(100);
}
int phy_init(void)
{
rt_pin_mode(ETH_RESET_IO, PIN_MODE_OUTPUT);
rt_pin_write(ETH_RESET_IO, PIN_LOW);
return RT_EOK;
}
INIT_BOARD_EXPORT(phy_init);
2.2、在ENV中選中網卡驅動,開啟 BSP_USING_ETH 和 PHY_USING_LAN8720A宏定義,這樣在scons構建工程時,drv_eth.c 和 phy_reset.c 就能自動添加進工程中
3、FAL配置
3.1、在 fal_cfg.h中定義 flash 設備、flash 設備表、flash 分區表。flash設備表中,nor_flash0是使用了SFUD接口實現片外SPI FLASH操作的fal_flash設備,具體實現在FAL針對 SFUD 的移植文件fal_flash_sfud_port.c中。stm32_onchip_flash_xx 是直接操作單片機片內FLASH的fal_flash設備,具體實現在 drv_flash_f4.c中
/* flash device table */
#define FAL_FLASH_DEV_TABLE \
{ \
&stm32_onchip_flash_16k, \
&stm32_onchip_flash_64k, \
&stm32_onchip_flash_128k, \
&nor_flash0, \
}
/* ====================== Partition Configuration ========================== */
#ifdef FAL_PART_HAS_TABLE_CFG
/* partition table */
#define FAL_PART_TABLE \
{ \
{FAL_PART_MAGIC_WROD, "bl", "onchip_flash_16k", 0 , FLASH_SIZE_GRANULARITY_16K , 0}, \
{FAL_PART_MAGIC_WROD, "easyflash", "onchip_flash_64k", 0 , FLASH_SIZE_GRANULARITY_64K , 0}, \
{FAL_PART_MAGIC_WROD, "app", "onchip_flash_128k", 0 , FLASH_SIZE_GRANULARITY_128K, 0}, \
{FAL_PART_MAGIC_WROD, "fs", FAL_USING_NOR_FLASH_DEV_NAME, 0 , 4 * 1024 * 1024, 0}, \
{FAL_PART_MAGIC_WROD, "tgfx", FAL_USING_NOR_FLASH_DEV_NAME, 4 * 1024 * 1024 , 4 * 1024 * 1024, 0}, \
}
3.2、在 spi_flash_init.c 中調用fal_init()初始化該組件
#if defined(PKG_USING_FAL)
int fs_init(void)
{
/* partition initialized */
fal_init();
return 0;
}
INIT_COMPONENT_EXPORT(fs_init);
#endif
4、格式化塊設備
4.1、將FAL的"fs"分區掛載到根目錄下,用於存儲靜態網頁,第一次掛載的時候可能會失敗,因為該分區還沒有文件系統,需要先在 "fs"分區創建elmFAT文件系統。此時可以在系統起來后,直接在shell中輸入 mkfs -t elm fs 命令對“fs”分區進行格式化
#if defined(RT_USING_DFS_ELMFAT)
#define FS_PARTITION_NAME "fs"
int elm_fatfs_init(void)
{
/* partition initialized */
// elm_init();
//dfs_mkfs("elm", "fs"); /* 在fs塊設備上創建elm文件系統*/
/* Create a block device on the "fs" partition of spi flash */
struct rt_device *flash_dev = fal_blk_device_create(FS_PARTITION_NAME);
if (flash_dev == NULL){
rt_kprintf("Can't create a block device on '%s' partition.\n", FS_PARTITION_NAME);
} else {
rt_kprintf("Create a block device on the %s partition of flash successful.\n", FS_PARTITION_NAME);
}
/* mount the file system from "fs" partition of spi flash. */
if (dfs_mount(FS_PARTITION_NAME, "/", "elm", 0, 0) == 0)
{
LOG_I("Filesystem initialized!");
}
else
{
LOG_E("Failed to initialize filesystem!");
LOG_D("You should create a filesystem on the block device first!");
}
return 0;
}
INIT_COMPONENT_EXPORT(elm_fatfs_init);
三、web服務器開發基礎
1、HTTP簡介
HTTP協議是Hyper Text Transfer Protocol(超文本傳輸協議)的縮寫,是用於從服務器傳輸超文本到本地瀏覽器的傳送協議。
1.1、工作原理
HTTP是一個基於TCP/IP通信協議來傳遞數據(HTML 文件, 圖片文件, 查詢結果等)HTTP協議工作於客戶端-服務端架構上。瀏覽器作為HTTP客戶端通過URL向HTTP服務端即WEB服務器發送所有請求。默認端口為80。HTTP使用統一資源標識符(Uniform Resource Identifiers, URI)來傳輸數據和建立連接。
HTTP三點注意事項:
HTTP是無連接:無連接的含義是限制每次連接只處理一個請求。服務器處理完客戶的請求,並收到客戶的應答后,即斷開連接。采用這種方式可以節省傳輸時間。
HTTP是媒體獨立的:這意味着,只要客戶端和服務器知道如何處理的數據內容,任何類型的數據都可以通過HTTP發送。客戶端以及服務器指定使用適合的MIME-type內容類型。
HTTP是無狀態:HTTP協議是無狀態協議。無狀態是指協議對於事務處理沒有記憶能力。缺少狀態意味着如果后續處理需要前面的信息,則它必須重傳,這樣可能導致每次連接傳送的數據量增大。另一方面,在服務器不需要先前信息時它的應答就較快。
1.2、消息結構
客戶端請求消息格式:由四個部分組成,分別是:請求行(request line)、請求頭部(header)、空行、請求數據
示例:
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
xxxxxxxxxxxxxx
服務器響應消息格式:也由四個部分組成分別是:狀態行、消息報頭、空行、響應正文
2、HTML簡介
超文本標記語言(HyperText Markup Language),是一種用於創建網頁的標准標記語言。HTML 運行在瀏覽器上,由瀏覽器來解析。
2.1、HTML 網頁結構
2.2、HTML 標簽
HTML 標記標簽通常被稱為 HTML 標簽 (HTML tag)。
HTML 標簽是由尖括號包圍的關鍵詞,比如 <html>
HTML 標簽通常是成對出現的,比如 <b> 和 </b>
標簽對中的第一個標簽是開始標簽,第二個標簽是結束標簽
開始和結束標簽也被稱為開放標簽和閉合標簽
2.3、HTML 屬性
HTML 標簽可以設置屬性
屬性可以在元素中添加附加信息
屬性一般描述於開始標簽
屬性總是以名稱/值對的形式出現,比如:name="value"
一個標簽中可以同時設置多個屬性,屬性之間需要用空格隔開
<a href="http://www.runoob.com">這是一個鏈接</a> <h1>這是<font color="red" size="10">一個</font>網頁</h1>
3、CSS簡介
CSS 指層疊樣式表 (Cascading Style Sheets),樣式表定義如何顯示 HTML 元素,就像 HTML 中的字體標簽和顏色屬性所起的作用那樣。樣式通常保存在外部的 .css 文件中。我們只需要編輯一個簡單的 CSS 文檔就可以改變所有頁面的布局和外觀。根據CSS樣式在HTML中被引用的方式可分為以下三種類型:
內部樣式表:包含含HTML文件的head標簽中,在body中的所以標簽的可以使用
<style type="text/css"> p{color:red;font-size:40px;}</style>
外部樣式表:外部獨立的.css文件中,通過在HTML文件中引用這個.css文件使用其樣式
<link rel="stylesheet" type="text/css" href="css文件路徑" />
內聯樣式:直接寫在標簽的開始標簽中,只對該標簽有效
<p style="color:red;font-size:40px;"> </p>
4、CGI技術簡介
公共網關接口(Common Gateway Interface),是外部應用程序與web服務器之間的接口標准。CGI規范允許web服務器執行外部程序,並將它們的輸出發送給web瀏覽器,CGI在物理上是一段程序,運行在服務器上,提供同客服端HTML頁面的接口。絕大多數的CGI程序被用來解釋處理來自表單的輸入信息,並在服務器產生相應的處理,或經相應的信息反饋給瀏覽器,CGI程序使網頁具有交互功能,比如通過web來處理瀏覽器提交的表單數據。如下圖,一個表單在瀏覽被提交,發送給web被處理的過程:
1、登錄網頁
2、提交表單(Checkbox, Radio button, selection list etc)
3、CGI程序處理
4、返回處理結果
5、SSI簡介
服務器側包含(server side include),是一種類似於ASP的基於服務器的網頁制作技術。是指將HTML內容發送到瀏覽器之前,先使用SSI指令將文本、圖形、或變量數據信息包含到網頁中,再發給瀏覽器解析。對於在多個文件中重復出現的文本或圖形,使用SSI包含技術是一種簡便的方法,只需將內容存入一個包含文件中即可,而不必將內容寫入到所有的文件,通過一個非常簡單的語句即可調用包含文件,此語句指示web服務器將內容插入到網頁的位置。
實現SSI功能的原理也很簡單,就是在發送網頁文件的時候遍歷整個文件,查找文件中的SSI指令,如果出現有<!--#include file="xxx" --> 指令,則將xxx文件的內容嵌入到網頁中;如果出現有<!--#include virtual="xxx" --> 指令,則匹配注冊好的xxx名稱,如果有注冊則執行其回調函數。想想如果發送每個網頁的時候都這么去查找,效率肯定很低,而且很多網頁並沒有文件或變量需要嵌入,所以使用SSI指令的網頁有特殊的擴展名,默認有 .stm、.shtm、.shtml,查找前會先匹配文件的擴展名,如果不是SSI默認的擴展名文件則不回配置SSI指令。如下圖,瀏覽器請求一個.shtml擴展名的文件:
1、用戶請求SSI頁面(后綴名是 .shtm .shtml .ssi .xml 的特殊的html網頁)
2、服務器根據頁面SSI標簽(<!--#include file="xxx" -->或<!--#include virtual="xxx" -->)動態插入數據。
3、服務器返回動態生成的頁面
四、web服務器應用程序設計
1、網頁制作
一個網頁主要由三部分構成:結構、表現、行為。結構:用於描述頁面的結構;表現:用於控制頁面中元素的樣式;行為:用於響應用戶的操作。
此次設計的有如下幾個網頁,網頁使用HBuilder編寫
1.1、效果圖
1.2、CGI類型與SSI標簽
1、在需要提交表單的頁面中,將表單的action屬性填寫成web服務器可識別的URL,表明向何處提交你的表單,這樣wen服務器在收到表單數據后,就會去執行跟action匹配的自定義注冊好的CGI執行函數。在rtthread的webnet中默認的CGI類型是/cgi-bin開頭的,需要注意!!
<form action="cgi-bin/ethip" method="post" target="nm_iframe">
...
</form>
2、在需要動態嵌入變量或文件的.shtml頁面中,需要通過注釋的方式定義好SSI標簽,這樣web服務器在發送這個頁面的時候就會去匹配頁面中的SSI標簽,然后去執行跟SSI標簽名稱匹配的自定義注冊好的SSI回調函數。如下,實時獲取傳感器數據的HTML
<table>
<tr>
<th height="50" colspan="2"><div align="center"><span class="font1">傳感器實時數據</span></div><hr /></th>
<tr>
<tr>
<td class="td_left">溫度:</td>
<td class="td_right"><span class="font2"><!--#include virtual="sensor_temp" --> ℃</span></td>
</tr>
<tr>
<td class="td_left">濕度:</td>
<td class="td_right"><span class="font2"><!--#include virtual="sensor_humit" --> %RH</span></td>
</tr>
<tr>
<td class="td_left">太陽能電壓:</td>
<td class="td_right"><span class="font2"><!--#include virtual="sensor_adc" --> mV</span></td>
</tr>
</table>
2、功能實現
2.1、自定義注冊CGI執行函數
需要在啟動web服務器之前注冊好你的CGI執行函數,我這里注冊的函數如下
以通過網頁遠程控制板卡為例,瀏覽器給web服務器發送的數據如下:
程序解析過程如下:
static void cgi_remotectl_handler(struct webnet_session* session)
{
/* defined the LED0 pin: PH2 */
#define LED0_PIN GET_PIN(H, 2)
/* defined the BEEP pin: PE3 */
#define BEEP_PIN GET_PIN(E, 3)
struct webnet_request* request;
const char* mimetype;
const char *led1,*beep,*relay;
char led1_value=0,beep_value=0,relay_value=0;
RT_ASSERT(session != RT_NULL);
/* get mimetype */
mimetype = mime_get_type(".html");
request = session->request;
/* set http header */
session->request->result_code = 200;
webnet_session_set_header(session, mimetype, 200, "Ok", -1);
/* 解析 */
led1 = webnet_request_get_query(request, "led1");
beep = webnet_request_get_query(request, "beep");
relay = webnet_request_get_query(request, "relay");
led1_value = atoi(led1);
beep_value = atoi(beep);
relay_value = atoi(relay);
if (led1_value) rt_pin_write(LED0_PIN, PIN_LOW);
else rt_pin_write(LED0_PIN, PIN_HIGH);
if (beep_value) rt_pin_write(BEEP_PIN, PIN_HIGH);
else rt_pin_write(BEEP_PIN, PIN_LOW);
}
2.2、自定義注冊SSI執行函數
rtthread實現的webnet中只實現了在網頁中嵌入文件的功能,即只識別 <!--#include file="xxx" --> 標簽,需要修改下 wn_module_ssi.c 實現 <!--#include virtual="xxx" --> 標簽的識別與處理。同理需要在啟動web服務器之前注冊好你的SSI執行函數,如上圖所示。我這里以獲取以太網IP地址為例,在瀏覽器請求 ethip.shtml 的時候,web服務器會執行注冊好的回調函數
在回調函數中直接將網卡的IP地址發送給瀏覽器
static void ssi_eth_ipaddr_value_handler(struct webnet_session* session)
{
struct netdev *netdev = netdev_get_by_name("e0");
webnet_session_printf(session, "%s", inet_ntoa(netdev->ip_addr));
}
3、頁面文件上傳
開啟RT-Thread 的設備虛擬文件系統后,需要將靜態頁面上傳到文件系統中服務器的根目錄下(這里根目錄為 /webnet),需要依次執行下面操作:
1、使用 mkdir webnet 命令創建 WebNet 軟件包根目錄 /webnet,並使用 cd webnet 命令進入該目錄;
2、使用 mkdir admin 、 mkdir upload 和 mkdir download命令創建 /webnet/admin 、/webnet/upload 和/webnet/download,用於 AUTH 功能、 upload 功能和download功能的測試
3、將設計的所有網頁依次上傳到設備 /webnet 目錄中。(可以使用 TFTP 工具上傳文件,具體操作方式參考 TFTP 使用說明)
4、啟動webnet
設備啟動,網絡連接成功,創建目錄和上傳文件成功之后,就可以啟動例程,測試 WebNet 軟件功能來了,過程如下:在 Shell 命令行輸入 webnet_test 命令即可啟動 WebNet 服務器
msh /webnet>webnet_test
[D/wn.log] server initialize success.
[I/wn] RT-Thread webnet package (V2.0.0) initialize success.
5、解決的問題
1、在瀏覽器獲取不到網頁的問題
獲取的網頁需要寫成絕對地址,比如獲取/webnet/frame_table.html頁面時,需要寫成 /frame_table.html,不能只寫成 frame_table.html
2、在提交完表單后會切換當前的頁面,有時候其實是不需要切換的,還停留在當前頁面。這時候可以巧妙運用 iframe,在頁面中放置一個面積為0的iframe,使其跳轉在iframe中完成,即實際跳轉了,只是不可見
<iframe id="id_iframe" name="nm_iframe" style="display:none;"></iframe>
<form action="cgi-bin/ethip" method="post" target="nm_iframe">
3、動態實時刷新頁面
<meta charset="utf-8" http-equiv="refresh" content="1">
4、登錄的時候,先判斷用戶名和密碼,不對的話打印提示信息
<script type="text/javascript">
function check(form)
{
var a = form.username.value;
var b = form.password.value;
if (a != "admin")
{
alert("User name error,Please input correct user name!");
}
else
{
if (b != "admin")
{
alert("Pass word error,Please input correct pass word!");
}
else
{
form.submit();
}
}
}
</script>
<input type="button" name="login" id="" style="position:absolute; left:48%;" value="登錄" onclick="check(form)"/>