本文將演示如何在一個 ESP-12F 模塊上實現webserver,而且能夠經過web請求對與模塊鏈接的繼電器進行控制。php
首先,假設本文的讀者瞭解C語言、邏輯電路和HTTP協議。再次,本文適合物聯網開發者和有意向涉及物聯網項目的web開發者、移動開發者閱讀 。最後,若是你只須要了解實現過程,你能夠繼續往下看,若是你想親自體驗這神奇的過程,除了經常使用的一些裝備和動手能力之外你還要須要準備如下材料。html
ESP-12F 是基於 Espressif ESP8266芯片開發的WIFI控制模塊,支持802.11 b/g/n/e/i標準並集成了Tensilica L106 32位控制器、4 MB Flash 和 64 KB SRAM。前端
ESP-12F 模塊git
Espressif 爲 ESP8266 已經移植好了操做系統而且在github 上開放了sdk,這個SDK已經實現了TCP/IP,只須要實現http協議就能夠完成webserver的功能。github
本例涉及的全部資料和代碼在本文最後一節都提供了參考連接,因爲筆者能力有限,本文內不免會有一些錯誤,也請各位讀者積極糾正。web
ESP-12F在Linux或Mac OS 下開發並在Windows下燒錄會更容易。 官網提供了安裝好開發環境的虛擬機鏡像。安裝和配置開發環境不在本文討論範圍內,本文最後一章提供的連接會有很大幫助。shell
本文使用的開發環境是 CentOS7 / crosstool-NG / ESP8266_RTOS_SDK網絡
注意: 若是不擅長本身配置開發環境,esp-open-sdk項目中的Readme會指導如何配置開發環境並建立項目。socket
按照官方提供的描述鏈接線路便可,使用麪包板和杜邦線鏈接能夠有助於重複使用器件。本文尾提供的連接會很大有幫助。ide
注意:
燒錄時須要更改鏈接到下載模式,不然沒法寫入程序。燒錄之後須要更改鏈接到flash boot模式,不然將沒法boot。
燒錄過程當中須要上電同步,能夠給模塊掉電在加電也能夠把模塊RST端接地超過一秒重啓模塊。
ESP-12F是3.3 V 電源供電,使用5V電源或USB供電的同窗須要裝備5V-3.3V 電源轉換模塊。
使用杜邦線鏈接以便重複利用模塊
在正式開發以前,須要測試硬件是否工做正常。因爲ESP-12F不具有任何顯示部件,所以調試須要藉助串口打印信息。咱們在 user/user_main.c 內寫入以下代碼初始化串口並向串口打印一條信息。同時你還須要連接wifi網絡。
代碼3-1: 初始化串口並打印調試信息
// 初始化UART 用戶須要按照相同的設置設置串口調試工具 UART_WaitTxFifoEmpty(UART0); UART_WaitTxFifoEmpty(UART1); UART_ConfigTypeDef uart_config; uart_config.baud_rate = BIT_RATE_115200; //波特率 uart_config.data_bits = UART_WordLength_8b; //字長度 uart_config.parity = USART_Parity_None; //校驗位 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 connection wifi_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模塊,不然會漏掉一些調試內容。
wifi連接建立好後在路由器管理界面就能夠看到IP地址了。
ESP8266_RTOS_SDK 提供了基於lwip 的Socket API,咱們只須要簡單調用便可實現建立Socket並綁定端口的過程。
代碼4-1:建立socket並綁定端口
int32 listenfd; int32 ret; struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; //IPV4 server_addr.sin_addr.s_addr = INADDR_ANY; //任意訪問IP server_addr.sin_len = sizeof(server_addr); server_addr.sin_port = htons(80); //綁定端口 do{ listenfd = socket(AF_INET, SOCK_STREAM, 0);//建立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); //開始監聽端口 } while (ret != 0);
當綁定端口成功之後 accept() 方法就會阻塞程序運行,直到有訪問請求。當有鏈接進入的時候(假設是沒有request body的GET請求),就能夠得到request的ID,而且經過 read() 獲取request header。當判斷request header完成後,便可經過 write() 方法向socket輸出response header和 response body,當這一切都完成的時候,就可使用close() 關閉鏈接。至此,一個request處理完成。
注意:
咱們沒法實現判斷request header的長度,而read()方法會阻塞程序運行,所以咱們須要判斷request header 是否完成以肯定是否開始向socket寫入response。
對與有 request body 的請求來講,須要解析request header 中的 content-length 字段以獲取request body的程度,從而判斷request body 是否結束以防止 read() 方法阻塞程序。
在獲取request header 的過程當中必需要獲取第一行報頭的內容以肯定請求類和須要訪問的資源位置
關於報頭標準請參照 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 finished int 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 data for(;;) { read(clientSock, &recieveBuffer, 1); if(recieveStatus == 0) { // 獲取請求方式 if(recieveBuffer != 32) { httpMethod[httpMethodLength] = recieveBuffer; httpMethodLength ++; } else { httpMethod[httpMethodLength] = 0; recieveStatus = 1; } continue; } if(recieveStatus == 1) { // 獲取URI if(recieveBuffer != 32) { httpRequestUri[httpRequestUriLength] = recieveBuffer; httpRequestUriLength ++; } else { httpRequestUri[httpRequestUriLength] = 0; recieveStatus = 2; } continue; } if(recieveStatus == 2) { //判斷header是否結束,header結束標記是一個空行 所以檢測header最後4個字符是不是連續的\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 header write(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 body write(clientSock, "Hello World", strlen("Hello World")); //關閉連接 close(clientSock); }
webserver 確定是要能服務靜態文件的,如今須要手動建立文件系統,考慮到存儲器特色、片上資源和計算能力,文件系統被設計成只讀ROM而且文件的MIME,大小,路徑等信息被提早存到文件系統裏。
ROM文件系統被分爲兩個區域,從ROM文件系統開始前64KB被劃分爲FAT區域,餘下的區域都是文件數據存儲區;FAT區域被分爲512個128B大小的文件條目存儲區,每一個條目保存一條文件信息,其中前0x40 字節用於保存文件名,0x40-0x77 用於保存文件的MIME數據,0x78-0x7B 保存文件大小,0x7C-0x7F保存文件開頭部分相對於ROM首字節的相對偏移量也能夠稱做文件的位置。
文件系統分配
注意
因爲SPI Flash 讀數據須要4B對齊,因此ROM 系統內全部文件開始位置必須是4B對齊的。
按照上節說到的文件系統,須要把一個特定目錄下的全部文件轉爲一個單獨的二進制文件才能夠燒錄到模塊上。這個過程須要先掃描目錄內全部文件並獲取文件名,再根據名文件名獲取文件相關屬性將全部的文件信息寫入ROM文件的FAT區,最後將文件二進制流附加在後面,並在文件開始位置4B對齊。
ROM建立過程
注意:
建立ROM的shell腳本能夠在最後一章的連接裏得到。
按照官方推薦的Flash佈局,ROM建議燒錄在Flash的0 x 0010 0000位置
咱們須要根據文件名來讀取文件,並非直接讀取文件,所以先要在ROM的FAT區裏查找對應文件名的存在位置、MIME、大小和存放區域,再去讀取文件內容,當讀到文件尾的時候不在讀取。官方的spi_flash_read接口只能讀取指定位置的指定長度的數據,這對咱們讀區文件很不方便。
代碼8-1:文件系統實現
// 所謂的文件句柄 保存已經打開文件的信息 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; //讀區文件FAT,匹配每一條文件條目是否於請求的文件名一致,一致則讀取信息並返回,不然返回空文件句柄。 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區域 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; } } // 有助於調試的調試信息 // 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 長度的數據並輸出到 data 裏,並設置 offset 到下一字節位置。若文件長度小於 offset + datalength 只讀區到文件末尾 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; // 若指針已經到達文件結尾不讀數據 if(currentOffset >= fileLength) { return false; } // 若超過文件結尾則只讀取到文件結尾 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; } }
動態請求的URI通常指向的不是一個真實存在的路徑,所以須要區分動態請求和靜態請求。本例會把URI由 /cgi/ 開頭的請求視爲動態請求。而且講動態請求傳入一個Router,有Router把請求轉發給每一個執行動態的請求的文件或函數,咱們稱之爲Controller。
router的工做過程
代碼9-1:router實現的代碼
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設置, 根據指定地址 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實現的代碼模版
void SOTCGI_PROG(char *para, int32 sock) { printf("GET CGI input: %s\n", para); }
因爲GPIO與普通IO不同,所以在使用前必須設置GPIO的功能,SDK爲每一個GPIO都設定了五種功能,使用前須要使用 PIN_FUNC_SELECT 宏函數進行設置,具體每一個GPIO口的功能,在最後一節給出的連接裏會有很大幫助。本例只使用了GPIO最基本的邏輯輸出的功能。具體GPOI功能設置能夠參照SDK的API參考文檔。
代碼10-1:邏輯輸出的實現
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 輸出低電平
因爲使用了SDK內集成了FreeROTS操做系統,所以咱們能夠把整個Server啓動等待連接和處理請求的過程分配成任務,這樣在server運行過程當中,模塊的程序流不會被阻塞。關於FreeROTS的任務管理方面,在最後一節給出的連接裏會有很大幫助。本例使用了建立任務 xTaskCreate,掛起任務 vTaskDelay和銷燬任務 vTaskDelete 這三個任務API。
系統啓動時先檢查網絡鏈接,當網絡鏈接創建好後建立初始化WebServer的任務,當初始化完成後初始化任務會被刪除並建立WebServer的主任務,當有請求進來時,主任務會建立worker任務去處理請求,當處理任務完成後,worker任務會自行刪除。
任務控制
結合任務控制和其餘的功能咱們不難規劃出一個webserver,具體項目代碼在最後一章裏有下載連接。
因爲GPIO輸出電平爲3.3V,不足以驅動5V的繼電器模塊,所以須要使用5V的邏輯門電路輔助驅動,本例使用的是CD4001 四或非門電路。
如今咱們已經有了一個能夠控制繼電器的Webserver ,再有一個前端也面就完美了。將製做好的靜態頁面寫入ROM後燒錄在Flash的0 x 0010 0000 位置上。完美收工。關於前端實現不在本文討論範疇,前端代碼隨項目代碼在最後一章的鏈接裏一塊兒給出。
鏈接好線路,接通電源,進行最終調試。
最終調試
個人Webserver 工做正常,你的呢?
關於交叉編譯器:
https://github.com/esp8266/es...
https://github.com/jcmvbkbc/c...
http://bbs.espressif.com/view...
關於燒寫工具:
https://github.com/esp8266/es...
http://bbs.espressif.com/view...
關於SDK:
https://github.com/espressif/...
https://github.com/pfalcon/es...
關於ESP8266的技術支持文檔:
http://espressif.com/en/suppo...
關於硬件的鏈接和燒錄
http://espressif.com/sites/de...
關於GPIO的功能的描述
http://espressif.com/sites/de...
關於FreeROTS的使用
http://www.freertos.org/FreeR...
本示例源代碼
https://github.com/cubicwork/...
SOTServer + SOTROM github項目( 代碼整理好之後會開放源代碼 )
https://github.com/cubicwork/...
做者:CarneyWu
本文來自【蒲公英技術徵文】,詳情連接:https://jinshuju.net/f/dGmewL
本活動用戶內容均採用 署名-非商業性使用-相同方式共享 3.0 中國大陸 進行許可