本文将演示如何在一个 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 中国大陆 进行许可