【蒲公英技术征文】如何在 ESP-12F/ESP8266 上实现 webserver
本文將演示如何在一個(gè) ESP-12F 模塊上實(shí)現(xiàn)webserver,并且可以通過web請(qǐng)求對(duì)與模塊連接的繼電器進(jìn)行控制。
0.寫在前面
首先,假設(shè)本文的讀者了解C語(yǔ)言、邏輯電路和HTTP協(xié)議。再次,本文適合物聯(lián)網(wǎng)開發(fā)者和有意向涉及物聯(lián)網(wǎng)項(xiàng)目的web開發(fā)者、移動(dòng)開發(fā)者閱讀 。最后,如果你只需要了解實(shí)現(xiàn)過程,你可以繼續(xù)往下看,如果你想親自體驗(yàn)這神奇的過程,除了常用的一些裝備和動(dòng)手能力以外你還要需要準(zhǔn)備以下材料。
ESP-12F 是基于 Espressif ESP8266芯片開發(fā)的WIFI控制模塊,支持802.11 b/g/n/e/i標(biāo)準(zhǔn)并集成了Tensilica L106 32位控制器、4 MB Flash 和 64 KB SRAM。
ESP-12F 模塊
Espressif 為 ESP8266 已經(jīng)移植好了操作系統(tǒng)并且在github 上開放了sdk,這個(gè)SDK已經(jīng)實(shí)現(xiàn)了TCP/IP,只需要實(shí)現(xiàn)http協(xié)議就可以完成webserver的功能。
本例涉及的所有資料和代碼在本文最后一節(jié)都提供了參考鏈接,由于筆者能力有限,本文內(nèi)難免會(huì)有一些錯(cuò)誤,也請(qǐng)各位讀者積極糾正。
1.開發(fā)環(huán)境
ESP-12F在Linux或Mac OS 下開發(fā)并在Windows下燒錄會(huì)更容易。 官網(wǎng)提供了安裝好開發(fā)環(huán)境的虛擬機(jī)鏡像。安裝和配置開發(fā)環(huán)境不在本文討論范圍內(nèi),本文最后一章提供的鏈接會(huì)有很大幫助。
本文使用的開發(fā)環(huán)境是 CentOS7 / crosstool-NG / ESP8266_RTOS_SDK
注意: 如果不擅長(zhǎng)自己配置開發(fā)環(huán)境,esp-open-sdk項(xiàng)目中的Readme會(huì)指導(dǎo)如何配置開發(fā)環(huán)境并創(chuàng)建項(xiàng)目。
2.硬件的連接和燒錄
按照官方提供的描述連接線路即可,使用面包板和杜邦線連接可以有助于重復(fù)使用器件。本文尾提供的鏈接會(huì)很大有幫助。
注意:
燒錄時(shí)需要更改連接到下載模式,否則無法寫入程序。燒錄以后需要更改連接到flash boot模式,否則將無法boot。
燒錄過程中需要上電同步,可以給模塊掉電在加電也可以把模塊RST端接地超過一秒重啟模塊。
ESP-12F是3.3 V 電源供電,使用5V電源或USB供電的同學(xué)需要裝備5V-3.3V 電源轉(zhuǎn)換模塊。
使用杜邦線連接以便重復(fù)利用模塊
3.測(cè)試硬件狀態(tài)并了解開發(fā)流程
在正式開發(fā)之前,需要測(cè)試硬件是否工作正常。由于ESP-12F不具備任何顯示部件,因此調(diào)試需要借助串口打印信息。我們?cè)?user/user_main.c 內(nèi)寫入如下代碼初始化串口并向串口打印一條信息。同時(shí)你還需要鏈接wifi網(wǎng)絡(luò)。
代碼3-1: 初始化串口并打印調(diào)試信息
// 初始化UART 用戶需要按照相同的設(shè)置設(shè)置串口調(diào)試工具UART_WaitTxFifoEmpty(UART0); UART_WaitTxFifoEmpty(UART1);UART_ConfigTypeDef uart_config;uart_config.baud_rate = BIT_RATE_115200; //波特率uart_config.data_bits = UART_WordLength_8b; //字長(zhǎng)度uart_config.parity = USART_Parity_None; //校驗(yàn)位uart_config.stop_bits = USART_StopBits_1; //停止位uart_config.flow_ctrl = USART_HardwareFlowControl_None;uart_config.UART_RxFlowThresh = 120;uart_config.UART_InverseMask = UART_None_Inverse;UART_ParamConfig(UART0, &uart_config);UART_SetPrintPort(UART0);// 向串口輸出一條信息printf("Hello World");代碼3-2:初始化wifi連接
// init wifi connectionwifi_set_opmode(STATION_MODE);struct station_config * wifi_config = (struct station_config *) zalloc(sizeof(struct station_config));sprintf(wifi_config->ssid, "your wifi ssid");sprintf(wifi_config->password, "your wifi password");wifi_station_set_config(wifi_config);free(wifi_config);wifi_station_connect();注意:
需要先打開串口工具再boot模塊,否則會(huì)漏掉一些調(diào)試內(nèi)容。
wifi鏈接創(chuàng)建好后在路由器管理界面就可以看到IP地址了。
4.創(chuàng)建Socket并等待連接
ESP8266_RTOS_SDK 提供了基于lwip 的Socket API,我們只需要簡(jiǎn)單調(diào)用即可實(shí)現(xiàn)創(chuàng)建Socket并綁定端口的過程。
代碼4-1:創(chuàng)建socket并綁定端口
int32 listenfd; int32 ret; struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET; //IPV4server_addr.sin_addr.s_addr = INADDR_ANY; //任意訪問IPserver_addr.sin_len = sizeof(server_addr);server_addr.sin_port = htons(80); //綁定端口 do{listenfd = socket(AF_INET, SOCK_STREAM, 0);//創(chuàng)建socket } while (listenfd == -1); do{ret = bind(listenfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); //綁定端口 } while (ret != 0); do{ret = listen(listenfd, SOT_SERVER_MAX_CONNECTIONS); //開始監(jiān)聽端口 } while (ret != 0);5.處理request
當(dāng)綁定端口成功以后 accept() 方法就會(huì)阻塞程序運(yùn)行,直到有訪問請(qǐng)求。當(dāng)有連接進(jìn)入的時(shí)候(假設(shè)是沒有request body的GET請(qǐng)求),就可以獲得request的ID,并且通過 read() 獲取request header。當(dāng)判斷request header完成后,即可通過 write() 方法向socket輸出response header和 response body,當(dāng)這一切都完成的時(shí)候,就可以使用close() 關(guān)閉連接。至此,一個(gè)request處理完成。
注意:
我們無法實(shí)現(xiàn)判斷request header的長(zhǎng)度,而read()方法會(huì)阻塞程序運(yùn)行,因此我們需要判斷request header 是否完成以確定是否開始向socket寫入response。
對(duì)與有 request body 的請(qǐng)求來說,需要解析request header 中的 content-length 字段以獲取request body的程度,從而判斷request body 是否結(jié)束以防止 read() 方法阻塞程序。
在獲取request header 的過程中必須要獲取第一行報(bào)頭的內(nèi)容以確定請(qǐng)求類和需要訪問的資源位置
關(guān)于報(bào)頭標(biāo)準(zhǔn)請(qǐng)參照 http://www.ietf.org/rfc/rfc26...
處理 request 的過程
代碼5-1:處理request
while((client_sock = accept(listenfd, (struct sockaddr *)&remote_addr, (socklen_t *)&len)) >= 0) {// recieveStatus 的含義 0. watting, 1. method get, 2. request URI get 3. finish recive 4. start send 5.send finishedint recieveStatus = 0;bool cgiRequest = true;char recieveBuffer;char *httpMethod = (char *)zalloc(8);int httpMethodLength = 0;char *httpRequestUri = (char *)zalloc(64);int httpRequestUriLength = 0;char *httpStopFlag = (char *)zalloc(4);int httpStopFlagLength = 0;httpMethod[0] = 0;httpRequestUri[0] = 0;httpStopFlag[0] = 0;// loop for recieve datafor(;;) {read(clientSock, &recieveBuffer, 1);if(recieveStatus == 0) {// 獲取請(qǐng)求方式if(recieveBuffer != 32) {httpMethod[httpMethodLength] = recieveBuffer;httpMethodLength ++;} else {httpMethod[httpMethodLength] = 0;recieveStatus = 1;}continue;}if(recieveStatus == 1) {// 獲取URIif(recieveBuffer != 32) {httpRequestUri[httpRequestUriLength] = recieveBuffer;httpRequestUriLength ++;} else {httpRequestUri[httpRequestUriLength] = 0;recieveStatus = 2;}continue;}if(recieveStatus == 2) {//判斷header是否結(jié)束,header結(jié)束標(biāo)記是一個(gè)空行 因此檢測(cè)header最后4個(gè)字符是否是連續(xù)的\r\n\r\n即可if(recieveBuffer == 10 || recieveBuffer == 13) {httpStopFlag[httpStopFlagLength] = recieveBuffer;httpStopFlagLength ++;httpStopFlag[httpStopFlagLength] = 0;if(httpStopFlag[0] == 13 && httpStopFlag[1] == 10 && httpStopFlag[2] == 13 && httpStopFlag[3] == 10) {recieveStatus == 3;break;}} else {httpStopFlagLength = 0;httpStopFlag[httpStopFlagLength] = 0;}continue;}}// 向串口打印獲取的信息 可以判斷訪問是否正確printf("Method=%s SOCK=%d\n", httpMethod, clientSock);printf("URI=%s SOCK=%d\n", httpRequestUri, clientSock);printf("CGIRequestFlag=%d SOCK=%d\n", cgiRequest, clientSock);//輸出response headerwrite(clientSock, "HTTP/1.1 200 OK\r\n", strlen("HTTP/1.1 200 OK\r\n"));write(clientSock, "Server: SOTServer\r\n", strlen("Server: SOTServer\r\n"));write(clientSock, "Content-Type: text/plain; charset=utf-8\r\n", strlen("Content-Type: text/plain; charset=utf-8\r\n"));write(clientSock, "\r\n", 2);//輸出 respose bodywrite(clientSock, "Hello World", strlen("Hello World"));//關(guān)閉鏈接close(clientSock); }6.規(guī)劃ROM文件系統(tǒng)
webserver 肯定是要能服務(wù)靜態(tài)文件的,現(xiàn)在需要手動(dòng)創(chuàng)建文件系統(tǒng),考慮到存儲(chǔ)器特點(diǎn)、片上資源和計(jì)算能力,文件系統(tǒng)被設(shè)計(jì)成只讀ROM并且文件的MIME,大小,路徑等信息被提前存到文件系統(tǒng)里。
ROM文件系統(tǒng)被分為兩個(gè)區(qū)域,從ROM文件系統(tǒng)開始前64KB被劃分為FAT區(qū)域,余下的區(qū)域都是文件數(shù)據(jù)存儲(chǔ)區(qū);FAT區(qū)域被分為512個(gè)128B大小的文件條目存儲(chǔ)區(qū),每個(gè)條目保存一條文件信息,其中前0x40 字節(jié)用于保存文件名,0x40-0x77 用于保存文件的MIME數(shù)據(jù),0x78-0x7B 保存文件大小,0x7C-0x7F保存文件開頭部分相對(duì)于ROM首字節(jié)的相對(duì)偏移量也可以稱作文件的位置。
文件系統(tǒng)分配
注意
由于SPI Flash 讀數(shù)據(jù)需要4B對(duì)齊,所以ROM 系統(tǒng)內(nèi)所有文件開始位置必須是4B對(duì)齊的。
7.制作靜態(tài)文件ROM
按照上節(jié)說到的文件系統(tǒng),需要把一個(gè)特定目錄下的所有文件轉(zhuǎn)為一個(gè)單獨(dú)的二進(jìn)制文件才可以燒錄到模塊上。這個(gè)過程需要先掃描目錄內(nèi)所有文件并獲取文件名,再根據(jù)名文件名獲取文件相關(guān)屬性將所有的文件信息寫入ROM文件的FAT區(qū),最后將文件二進(jìn)制流附加在后面,并在文件開始位置4B對(duì)齊。
ROM創(chuàng)建過程
注意:
創(chuàng)建ROM的shell腳本可以在最后一章的鏈接里獲得。
按照官方推薦的Flash布局,ROM建議燒錄在Flash的0 x 0010 0000位置
8.讀取ROM文件內(nèi)容
我們需要根據(jù)文件名來讀取文件,并不是直接讀取文件,因此先要在ROM的FAT區(qū)里查找對(duì)應(yīng)文件名的存在位置、MIME、大小和存放區(qū)域,再去讀取文件內(nèi)容,當(dāng)讀到文件尾的時(shí)候不在讀取。官方的spi_flash_read接口只能讀取指定位置的指定長(zhǎng)度的數(shù)據(jù),這對(duì)我們讀區(qū)文件很不方便。
代碼8-1:文件系統(tǒng)實(shí)現(xiàn)
// 所謂的文件句柄 保存已經(jīng)打開文件的信息 struct SOTROM_filePointer {uint32 location;uint32 offset;uint32 fileSize;bool fileExsit;char *mime; }; typedef struct SOTROM_filePointer SOTROM_file; define SOT_ROM_ORG 0x00100000; define SOT_ROM_FAT_SIZE 0x00010000; //讀區(qū)文件FAT,匹配每一條文件條目是否于請(qǐng)求的文件名一致,一致則讀取信息并返回,否則返回空文件句柄。 SOTROM_file *SOTROM_fopen(char* fileName) {SOTROM_file *openedFile;openedFile = malloc(70);openedFile->location = 0;openedFile->offset = 0;openedFile->fileSize = 0;openedFile->fileExsit = false;// 查找FAT區(qū)域char *pointerFilename = (char *)zalloc(64);uint32 currentFATPointer = SOT_ROM_ORG;uint32 maxFATPointer = SOT_ROM_ORG + SOT_ROM_FAT_SIZE;SpiFlashOpResult res;while(currentFATPointer < maxFATPointer) {// 獲得文件名res = spi_flash_read(currentFATPointer, (uint32* )pointerFilename, 64);if(res == SPI_FLASH_RESULT_OK) {if(strlen(pointerFilename) > 0) {if(strcmp(fileName, pointerFilename) == 0) {char *pointerFilename = (char *)zalloc(56);uint32 fileSize;uint32 location;res |= spi_flash_read(currentFATPointer + 64, (uint32* )pointerFilename, 56);res |= spi_flash_read(currentFATPointer + 120, (uint32* )&fileSize, 4);res |= spi_flash_read(currentFATPointer + 124, (uint32* )&location, 4);if(res == SPI_FLASH_RESULT_OK) {openedFile->fileExsit = true;openedFile->mime = pointerFilename;openedFile->fileSize = fileSize;openedFile->location = location;openedFile->location += maxFATPointer;break;}}currentFATPointer += 128;} else {break;}} else {break;}}// 有助于調(diào)試的調(diào)試信息// printf("file found: %d\n", openedFile->fileExsit);// printf("file mime: %s\n", openedFile->mime);// printf("file length: %d\n", openedFile->fileSize);// printf("file location: %d\n", openedFile->location);// printf("file offset: %d\n", openedFile->offset);return openedFile; } // 從 SOTROM_fopen 打開的文件里 獲取在offset指針處讀取 datalength 長(zhǎng)度的數(shù)據(jù)并輸出到 data 里,并設(shè)置 offset 到下一字節(jié)位置。若文件長(zhǎng)度小于 offset + datalength 只讀區(qū)到文件末尾 bool SOTROM_fread(SOTROM_file *file, uint32 *data, int32 datalength) {// 檢查文件是否存在if(!file->fileExsit) {return false;}int32 fileLength = file->fileSize;int32 currentOffset = file->offset;int32 startReadLocation = file->location + currentOffset;// 若指針已經(jīng)到達(dá)文件結(jié)尾不讀數(shù)據(jù)if(currentOffset >= fileLength) {return false;}// 若超過文件結(jié)尾則只讀取到文件結(jié)尾if(currentOffset + datalength > fileLength) {datalength = fileLength - currentOffset;}SpiFlashOpResult res;res = spi_flash_read(startReadLocation, data, datalength);if(res == SPI_FLASH_RESULT_OK) {file->offset = currentOffset + datalength;char *tmpDataPtr = (char *)data;tmpDataPtr[datalength] = 0;return true;} else {return false;} }9.處理動(dòng)態(tài)請(qǐng)求
動(dòng)態(tài)請(qǐng)求的URI一般指向的不是一個(gè)真實(shí)存在的路徑,因此需要區(qū)分動(dòng)態(tài)請(qǐng)求和靜態(tài)請(qǐng)求。本例會(huì)把URI由 /cgi/ 開頭的請(qǐng)求視為動(dòng)態(tài)請(qǐng)求。并且講動(dòng)態(tài)請(qǐng)求傳入一個(gè)Router,有Router把請(qǐng)求轉(zhuǎn)發(fā)給每個(gè)執(zhí)行動(dòng)態(tài)的請(qǐng)求的文件或函數(shù),我們稱之為Controller。
router的工作過程
代碼9-1:router實(shí)現(xiàn)的代碼
void SOTCGI_PROG(char *para, int32 sock) // CGI入口文件,傳socket連接ID和URL即可 void SOTCGI_handler(char * cgiURI, int32 sock) {char *response = (char *)zalloc(64);SOTCGI_route("/cgi/demo0/", cgiURI, sock, SOTCGI_PROG);SOTCGI_route("/cgi/demo1/", cgiURI, sock, SOTCGI_PROG); } // CGI Router設(shè)置, 根據(jù)指定地址 route 綁定指定控制器 callback。 void SOTCGI_route(char *route, char *cgiURI, int32 sock, void (* callback)()) {if(strncmp(route, cgiURI, strlen(route)) == 0) {char *para = substr(cgiURI, strlen(route), strlen(cgiURI));(* callback)(para, sock);free(para);} }代碼9-2:controller實(shí)現(xiàn)的代碼模版
void SOTCGI_PROG(char *para, int32 sock) {printf("GET CGI input: %s\n", para); }10.GPIO的控制
由于GPIO與普通IO不一樣,因此在使用前必須設(shè)置GPIO的功能,SDK為每個(gè)GPIO都設(shè)定了五種功能,使用前需要使用 PIN_FUNC_SELECT 宏函數(shù)進(jìn)行設(shè)置,具體每個(gè)GPIO口的功能,在最后一節(jié)給出的鏈接里會(huì)有很大幫助。本例只使用了GPIO最基本的邏輯輸出的功能。具體GPOI功能設(shè)置可以參照SDK的API參考文檔。
代碼10-1:邏輯輸出的實(shí)現(xiàn)
PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12);//將 PERIPHS_IO_MUX_MTDI_U 接口綁定為 FUNC_GPIO12 輸出功能 gpio_output_set(BIT12, 0, BIT12, 0); // GPIO12 輸出高電平 gpio_output_set(0, BIT12, BIT12, 0); // GPIO12 輸出低電平11.任務(wù)控制
由于使用了SDK內(nèi)集成了FreeROTS操作系統(tǒng),因此我們可以把整個(gè)Server啟動(dòng)等待鏈接和處理請(qǐng)求的過程分配成任務(wù),這樣在server運(yùn)行過程中,模塊的程序流不會(huì)被阻塞。關(guān)于FreeROTS的任務(wù)管理方面,在最后一節(jié)給出的鏈接里會(huì)有很大幫助。本例使用了創(chuàng)建任務(wù) xTaskCreate,掛起任務(wù) vTaskDelay和銷毀任務(wù) vTaskDelete 這三個(gè)任務(wù)API。
系統(tǒng)啟動(dòng)時(shí)先檢查網(wǎng)絡(luò)連接,當(dāng)網(wǎng)絡(luò)連接建立好后創(chuàng)建初始化WebServer的任務(wù),當(dāng)初始化完成后初始化任務(wù)會(huì)被刪除并創(chuàng)建WebServer的主任務(wù),當(dāng)有請(qǐng)求進(jìn)來時(shí),主任務(wù)會(huì)創(chuàng)建worker任務(wù)去處理請(qǐng)求,當(dāng)處理任務(wù)完成后,worker任務(wù)會(huì)自行刪除。
任務(wù)控制
12.實(shí)現(xiàn)webserver
結(jié)合任務(wù)控制和其他的功能我們不難規(guī)劃出一個(gè)webserver,具體項(xiàng)目代碼在最后一章里有下載鏈接。
13.驅(qū)動(dòng)5V繼電器
由于GPIO輸出電平為3.3V,不足以驅(qū)動(dòng)5V的繼電器模塊,因此需要使用5V的邏輯門電路輔助驅(qū)動(dòng),本例使用的是CD4001 四或非門電路。
14.制作靜態(tài)頁(yè)面
現(xiàn)在我們已經(jīng)有了一個(gè)可以控制繼電器的Webserver ,再有一個(gè)前端也面就完美了。將制作好的靜態(tài)頁(yè)面寫入ROM后燒錄在Flash的0 x 0010 0000 位置上。完美收工。關(guān)于前端實(shí)現(xiàn)不在本文討論范疇,前端代碼隨項(xiàng)目代碼在最后一章的連接里一起給出。
15.接入調(diào)試
連接好線路,接通電源,進(jìn)行最終調(diào)試。
最終調(diào)試
我的Webserver 工作正常,你的呢?
16.相關(guān)資源及項(xiàng)目代碼
關(guān)于交叉編譯器:
https://github.com/esp8266/es...
https://github.com/jcmvbkbc/c...
http://bbs.espressif.com/view...
關(guān)于燒寫工具:
https://github.com/esp8266/es...
http://bbs.espressif.com/view...
關(guān)于SDK:
https://github.com/espressif/...
https://github.com/pfalcon/es...
關(guān)于ESP8266的技術(shù)支持文檔:
http://espressif.com/en/suppo...
關(guān)于硬件的連接和燒錄
http://espressif.com/sites/de...
關(guān)于GPIO的功能的描述
http://espressif.com/sites/de...
關(guān)于FreeROTS的使用
http://www.freertos.org/FreeR...
本示例源代碼
https://github.com/cubicwork/...
SOTServer + SOTROM github項(xiàng)目( 代碼整理好以后會(huì)開放源代碼 )
https://github.com/cubicwork/...
作者:CarneyWu
本文來自【蒲公英技術(shù)征文】,詳情鏈接:https://jinshuju.net/f/dGmewL
本活動(dòng)用戶內(nèi)容均采用?署名-非商業(yè)性使用-相同方式共享 3.0 中國(guó)大陸 進(jìn)行許可
總結(jié)
以上是生活随笔為你收集整理的【蒲公英技术征文】如何在 ESP-12F/ESP8266 上实现 webserver的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PV 和 UV IP
- 下一篇: YII2操作mongodb笔记