本文将演示如何在一个 ESP-12F 模块上实现webserver,并且可以通过web请求对与模块连接的继电器进行控制。

0.写在前面

首先,假设本文的读者了解C语言、逻辑电路和HTTP协议。再次,本文适合物联网开发者和有意向涉及物联网项目的web开发者、移动开发者阅读 。最后,如果你只需要了解实现过程,你可以继续往下看,如果你想亲自体验这神奇的过程,除了常用的一些装备和动手能力以外你还要需要准备以下材料。

ESP-12F 是基于 Espressif ESP8266芯片开发的WIFI控制模块,支持802.11 b/g/n/e/i标准并集成了Tensilica L106 32位控制器、4 MB Flash 和 64 KB SRAM。

ESP-12F 模块

Espressif 为 ESP8266 已经移植好了操作系统并且在github 上开放了sdk,这个SDK已经实现了TCP/IP,只需要实现http协议就可以完成webserver的功能。

本例涉及的所有资料和代码在本文最后一节都提供了参考链接,由于笔者能力有限,本文内难免会有一些错误,也请各位读者积极纠正。

1.开发环境

ESP-12F在Linux或Mac OS 下开发并在Windows下烧录会更容易。 官网提供了安装好开发环境的虚拟机镜像。安装和配置开发环境不在本文讨论范围内,本文最后一章提供的链接会有很大帮助。

本文使用的开发环境是 CentOS7 / crosstool-NG / ESP8266_RTOS_SDK

注意: 如果不擅长自己配置开发环境,esp-open-sdk项目中的Readme会指导如何配置开发环境并创建项目。

2.硬件的连接和烧录

按照官方提供的描述连接线路即可,使用面包板和杜邦线连接可以有助于重复使用器件。本文尾提供的链接会很大有帮助。

注意:

烧录时需要更改连接到下载模式,否则无法写入程序。烧录以后需要更改连接到flash boot模式,否则将无法boot。
烧录过程中需要上电同步,可以给模块掉电在加电也可以把模块RST端接地超过一秒重启模块。
ESP-12F是3.3 V 电源供电,使用5V电源或USB供电的同学需要装备5V-3.3V 电源转换模块。

使用杜邦线连接以便重复利用模块

3.测试硬件状态并了解开发流程

在正式开发之前,需要测试硬件是否工作正常。由于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 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模块,否则会漏掉一些调试内容。
wifi链接创建好后在路由器管理界面就可以看到IP地址了。

4.创建Socket并等待连接

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; //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);//创建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);

5.处理request

当绑定端口成功以后 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 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) {// 获取请求方式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是否结束,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 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"));//关闭链接close(clientSock);
}

6.规划ROM文件系统

webserver 肯定是要能服务静态文件的,现在需要手动创建文件系统,考虑到存储器特点、片上资源和计算能力,文件系统被设计成只读ROM并且文件的MIME,大小,路径等信息被提前存到文件系统里。

ROM文件系统被分为两个区域,从ROM文件系统开始前64KB被划分为FAT区域,余下的区域都是文件数据存储区;FAT区域被分为512个128B大小的文件条目存储区,每个条目保存一条文件信息,其中前0x40 字节用于保存文件名,0x40-0x77 用于保存文件的MIME数据,0x78-0x7B 保存文件大小,0x7C-0x7F保存文件开头部分相对于ROM首字节的相对偏移量也可以称作文件的位置。

文件系统分配

注意

由于SPI Flash 读数据需要4B对齐,所以ROM 系统内所有文件开始位置必须是4B对齐的。

7.制作静态文件ROM

按照上节说到的文件系统,需要把一个特定目录下的所有文件转为一个单独的二进制文件才可以烧录到模块上。这个过程需要先扫描目录内所有文件并获取文件名,再根据名文件名获取文件相关属性将所有的文件信息写入ROM文件的FAT区,最后将文件二进制流附加在后面,并在文件开始位置4B对齐。

ROM创建过程

注意:

创建ROM的shell脚本可以在最后一章的链接里获得。
按照官方推荐的Flash布局,ROM建议烧录在Flash的0 x 0010 0000位置

8.读取ROM文件内容

我们需要根据文件名来读取文件,并不是直接读取文件,因此先要在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;}
}

9.处理动态请求

动态请求的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);
}

10.GPIO的控制

由于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 输出低电平

11.任务控制

由于使用了SDK内集成了FreeROTS操作系统,因此我们可以把整个Server启动等待链接和处理请求的过程分配成任务,这样在server运行过程中,模块的程序流不会被阻塞。关于FreeROTS的任务管理方面,在最后一节给出的链接里会有很大帮助。本例使用了创建任务 xTaskCreate,挂起任务 vTaskDelay和销毁任务 vTaskDelete 这三个任务API。

系统启动时先检查网络连接,当网络连接建立好后创建初始化WebServer的任务,当初始化完成后初始化任务会被删除并创建WebServer的主任务,当有请求进来时,主任务会创建worker任务去处理请求,当处理任务完成后,worker任务会自行删除。

任务控制

12.实现webserver

结合任务控制和其他的功能我们不难规划出一个webserver,具体项目代码在最后一章里有下载链接。

13.驱动5V继电器

由于GPIO输出电平为3.3V,不足以驱动5V的继电器模块,因此需要使用5V的逻辑门电路辅助驱动,本例使用的是CD4001 四或非门电路。

14.制作静态页面

现在我们已经有了一个可以控制继电器的Webserver ,再有一个前端也面就完美了。将制作好的静态页面写入ROM后烧录在Flash的0 x 0010 0000 位置上。完美收工。关于前端实现不在本文讨论范畴,前端代码随项目代码在最后一章的连接里一起给出。

15.接入调试

连接好线路,接通电源,进行最终调试。

最终调试

我的Webserver 工作正常,你的呢?

16.相关资源及项目代码

关于交叉编译器:
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 中国大陆 进行许可

【蒲公英技术征文】如何在 ESP-12F/ESP8266 上实现 webserver相关推荐

  1. 阿里一面 京东一面+二面 | 掘金技术征文

    阿里一面 简单说说在学校做过最有成就感的事情(和技术相关的) 你的项目用到了数据库,谈谈对事务的理解 假设你要做一个银行app,有可能碰到多个人同时向一个账户打钱的情况,有可能碰到什么问题,如何解决( ...

  2. 我是如何在天猫、蚂蚁金服、百度等大厂面试中被拒的 | 掘金技术征文

    本人16年毕业于普通二本院校网络相关专业,工作经验两年半,目前就职业于一家普通民营企业. 由于非985.211学历硬伤,校招进大厂的门槛远高于同届985.211的毕业生.于是乎,从毕业到现在经历了三家 ...

  3. 转载 一个渣硕iOS春招总结 | 掘金技术征文

    https://www.qingtingip.com/h_219584.html 地处北方一隅,今年很多公司春招没来现场,所以基本都是提前批的线上面试,整个三月都过的比较累,4月份的校招应该不参加了, ...

  4. Spring Boot干货系列:(六)静态资源和拦截器处理 | 掘金技术征文

    原本地址:Spring Boot干货系列:(六)静态资源和拦截器处理 博客地址:tengj.top/ 前言 本章我们来介绍下SpringBoot对静态资源的支持以及很重要的一个类WebMvcConfi ...

  5. 一起来学习 WebRTC (篇一)| 掘金技术征文

    前言 作为一个认为啥都想懂一点的小开发,一直都对WebRTC很感兴趣,这个兴趣来源于几年前公司希望做一个即时通讯的小功能在APP上,不过最终由于项目最终需求更改而搁置.虽然如此,但是我还是了解了一些关 ...

  6. Flutter 底部导航——BottomNavigationBar | 掘金技术征文

    前言 Google推出flutter这样一个新的高性能跨平台(Android,ios)快速开发框架之后,被业界许多开发者所关注.我在接触了flutter之后发现这个确实是一个好东西,好东西当然要和大家 ...

  7. Flutter实现动画卡片式Tab导航 | 掘金技术征文

    前言 本人接触Flutter不到一个月,深深感受到了这个平台的威力,于是不断学习,Flutter官方Example中的flutter_gallery,很好的展示了Flutter各个widget的功能 ...

  8. 9月,水了几个大中厂前端面试的一些总结分享 | 掘金技术征文

    写在前面 工作吧,我觉得就像谈恋爱,不一定是找高富帅或者白富美,互相确认过眼神是对的人就可以~而面试的自信和对工资的要求,源于你过硬的基础和平时的思考.积累以及总结~ 8月底离职,其实是裸辞,当然大概 ...

  9. 腾讯面试后续 | 掘金技术征文

    前言 在春招过程中,参加了不少公司的面试,这次就继续上次说的腾讯春招吧.之前提前批收到了一次面试,但后来就没有消息了,应该是妥妥的挂了.接着走正常渠道,参加笔试,笔试之后过几天,收到了通知,参加线路面 ...

最新文章

  1. HighNewTech:2019.08.08华为发布—面向2025十大趋势
  2. Qt修炼手册1_溢美之词和Designer设计
  3. 蚂蚁从飞机上掉下来的数学建模分析
  4. 短期记忆容量必需有限
  5. 自定义alert提示框
  6. 记一次自动提醒钉钉机器人的诞生
  7. 2020最新淘宝等级表图及商品发布限制数量类目表
  8. ardupilot 函数output_armed_stabilizing
  9. 【数据分享】我国地级市绿地利用现状数据(9个指标\Shp格式)
  10. 白嫖银行?普通人为数不多的机会
  11. 【极限精度】基于stm32f4xx的蜂鸣器音乐播放(生日快乐)及国际绝对音名标准频率定义(32位无符号整型精度、十二等律体系、A4=440.01000Hz)
  12. Mybatis传递单个参数
  13. Android内存优化总结
  14. HTML文件转JSP文件
  15. 2022年中国研究生数学建模竞赛F题-COVID-19疫情期间生活物资的科学管理问题
  16. 蚂蚁金服2019届【技术类】校园招聘
  17. Simple Java Mail的使用,发送qq邮件
  18. B.特定领域知识图谱知识推理方案[一]:基于表示学习的知识感知推理算法[对抗负采样、Logic Rule,链接预测任务]在关系预测、推荐场景下应用
  19. 阿拉伯数字大写转换(含小数)
  20. 计算机游戏13关gongl,密室逃脱17第13关怎么过 守护公寓第13关通关攻略

热门文章

  1. PostgreSql+PostGIS和uDig的安装
  2. Nginx 模块开发(1)—— 一个稍稍能说明问题模块开发 Step By Step 过程
  3. Ubuntu使用notify-send 与 crontab 实现定时提醒
  4. 关于nginx/lighttpd epoll高并发以及apache为何不采用epoll的的疑惑 不指定
  5. 转载:IBM红米连接wifi的方法
  6. Java @override报错的解决方法 .
  7. 10 分钟从零搭建个人博客
  8. 鸿蒙股票深度分析,本月华为鸿蒙概念股市回顾分析(3月31日)
  9. 软件问题造成的经济损失案例_公司印章管理使用哪些行为会造成法律风险隐患...
  10. php-fpm linux 权限,nginx/php-fpm及网站目录的权限设置