
简介面向嵌入式初学者的STM32F1物联网实战项目资源通过ESP8266 Wi-Fi模块获取心知天气API的天气与温度数据并在0.96寸OLED屏上显示。资源基于Keil5标准库开发包含完整工程源码、硬件接线说明ESP8266接串口3、OLED接PB13/PB15、按键接PA0与手机热点配网流程适合学习STM32串口通信、ESP8266 AT指令解析、OLED驱动及JSON数据解析。压缩包共191个文件以.c/.h源码为主辅以Keil工程配置uvprojx/uvoptx、编译中间文件o/d/crf及hex烧录文件等整体仅3.54MB轻量易部署。已有264人学习项目结构清晰附有readme与基础脚本可帮助开发者从硬件连接到Wi-Fi联网快速跑通整套流程同时通过天气API的调用与数据解析为后续物联网设备开发提供可复用的工程模板。 做这个项目之前我建议你先有一个心理准备这不是接三根线、烧个程序就能跑通的东西。中间最磨人的一段不是STM32也不是OLED而是让ESP8266在陌生网络环境里稳定地把天气数据拉回来。我在调这个项目时光在ESP8266的AT指令交互上就耗了快一天——第一次上电乱码第二次连不上路由器第三次数据收到一半就断后来才逐个排查清楚。这篇文章把整个链路拆开来讲从硬件接线到AT指令交互从JSON解析到OLED显示最后再把实测踩过的坑一并列出来适合刚学完STM32基础外设、想接触设备上云的读者也适合那些明明照着例程抄却跑不通、想搞明白问题出在哪的人。1. 整机框架一块Cortex-M3如何完成上网这件大事1.1 系统各模块之间的关系很多新人第一次接触这类项目容易把它想成ESP8266把天气数据直接传给OLED。实际上ESP8266只是一个无线透传模块真正干活的是STM32F1。整条数据链路是这样的STM32通过串口给ESP8266发AT指令ESP8266联网后向天气API服务器发起HTTP请求服务器返回一段JSON数据ESP8266再把这段数据通过串口原样丢给STM32STM32解析出城市、天气现象、温度等字段最后通过I2C接口把内容绘制到0.96寸OLED屏幕上。也就是说STM32既是指挥官也是翻译官。它负责指挥ESP8266去连网、去请求也负责把收到的JSON拆开、提取有效信息再转换成OLED能懂的像素点。ESP8266本身不解析数据OLED也不懂网络协议所有逻辑都集中在STM32的固件里。1.2 硬件选型明细与接线方式我用的主控是STM32F103C8T6蓝色Pill板ESP8266模块选择的是ESP-01SOLED是0.96寸I2C接口的SSD1306。三者的接线看起来简单但有两处容易被忽略。ESP8266引脚STM32引脚说明VCC3.3V需单独供电后面细说电源GNDGND共地TXPA3USART2_RXESP8266发送到STM32RXPA2USART2_TXSTM32发送到ESP8266CH_PD/EN3.3V使能脚必须拉高GPIO0悬空悬空为运行模式OLED引脚STM32引脚VCC3.3VGNDGNDSCLPB6软件I2C可自定义SDAPB7这里我要特别提醒ESP8266的供电问题。ESP-01S在发射WiFi信号的瞬间电流能冲到300mA甚至更高STM32开发板上的AMS1117-3.3稳压器虽然标称能输出1A但实际在面包板环境下带着ESP8266和OLED一起跑压降会很明显。最稳的做法是给ESP8266单独配一个3.3V稳压模块或者用AMS1117-3.3模块从5V转出来独立供电并且所有模块共地。如果偷懒直接用开发板的3.3V十有八九会出现模块反复重启、串口输出乱码这类诡异现象。1.3 软件分层思路固件代码我没有一上来就写业务逻辑而是先分了层底层驱动层OLED的I2C驱动、USART2的收发驱动中间封装层ESP8266的AT指令封装连接WiFi、建立TCP、发送HTTP请求、OLED的显示封装显示中文、显示数字、清屏应用层主循环里的获取天气数据→解析→刷新显示状态机分层的好处是排障方便。OLED显示乱了只需要查显示层网络不通只需要查ESP8266层不用在几百行代码里大海捞针。哪怕是临时写的小demo我也建议至少把ESP8266的AT交互独立成一个文件后续想换成4G模块或NB-IoT模块时只改这一层就够了。2. 让ESP8266开口说话AT指令与HTTP请求的完整交互2.1 连接前的必要检查ESP8266出厂时一般自带AT固件。把模块通过USB转TTL连到电脑打开串口助手波特率选择115200发送AT并回车如果返回OK说明模块和固件都没问题。这一步我强烈建议不要跳过——很多人把ESP8266直接接到STM32上就开始调出了故障根本分不清是模块问题、接线问题还是代码问题。模块没有反应时先检查这几个点CH_PD有没有接3.3V不接的话模块根本不会启动TX和RX有没有交叉接STM32的TX接ESP8266的RX反之亦然波特率是不是115200部分老固件是9600需要用AT指令重新配置2.2 核心AT指令序列与预期返回如果是在STM32代码里通过串口发指令整个流程会有严格的时序依赖。我列一下典型的交互过程。AT # 期望返回OK ATE0 # 关闭回显期望返回OK后续收到的回复更干净 ATCWMODE1 # 设置为Station模式期望返回OK ATCWJAP你的WiFi名,你的WiFi密码 # 连接路由器期望返回WIFI DISCONNECT若有然后 WIFI GOT IP # 最后返回OK连接WiFi这步最容易出问题。有些路由器开了MAC地址过滤或隐藏SSID会导致反复返回FAIL。另外需要留意WIFI GOT IP这个返回值它才是真正拿到IP地址、可以对外通信的标志。我当时的做法是在代码里判断收到WIFI GOT IP才继续往下走而不是看到OK就往下走因为OK只代表AT指令被接受了不代表联网成功。接下来的TCP连接和HTTP请求ATCIPSTARTTCP,api.seniverse.com,80 # 期望返回CONNECT OK ATCIPSEND87 # 期望返回 大于号提示符意思是可以发送数据了 # 然后立刻发送HTTP请求报文报文长度必须和CIPSEND后的数字一致HTTP请求报文长这样GET /v3/weather/now.json?key你的API密钥locationbeijinglanguagezh-Hansunitc HTTP/1.1 Host: api.seniverse.com Connection: close注意最后有两个回车换行。Connection: close这个字段也很关键它告诉服务器返回完数据就断开连接这样ESP8266会收到连接关闭的通知STM32就知道数据收完了可以安全地开始解析。如果没有这个字段TCP连接会一直挂着接收逻辑就得靠超时判断容易出问题。2.3 CIPSEND发送长度的坑我特意把CIPSEND这一行单独拿出来说。ATCIPSEND87这里的87是后面HTTP请求报文的字节数必须精确匹配。差一个字节模块就会把数据一直留在缓冲区里不发给服务器多一个字节会把错误的字符也发出去服务器返回400错误。实战里我从来不用手数字节数。做法是先在串口助手里把HTTP请求报文拼好然后让串口助手的状态栏显示字符数或者借助十六进制显示来数。也可以在代码里预先定义好请求字符串用strlen()算出来拼接成指令再通过STM32发送char http_request[256]; sprintf(http_request, GET /v3/weather/now.json?key%slocationbeijinglanguagezh-Hansunitc HTTP/1.1\r\n Host: api.seniverse.com\r\n Connection: close\r\n\r\n, weather_api_key); char at_cmd[300]; sprintf(at_cmd, ATCIPSEND%d\r\n, strlen(http_request)); esp8266_send_string(at_cmd); delay_ms(200); esp8266_send_string(http_request);这样长度就不会出错。等服务器返回完数据ESP8266会自己给出SEND OK然后因为服务器关闭了连接串口会收到大量JSON数据末尾跟着CLOSED。2.4 天气API怎么选我项目里用的是心知天气。选它的原因很简单国内访问快、返回的JSON结构清晰、免费版足够这个项目用。下面是我用来对比的几个方案API服务是否需要Key国内访问速度返回格式复杂度备注心知天气是快JSON字段较少免费版每日有调用次数限制和风天气是快JSON结构略复杂新用户送体验套餐OpenWeatherMap是一般JSON字段较多英文为主wttr.in否取决于网络支持JSON模式适合快速测试心知天气的免费版每日请求次数有限我自己的显示刷新策略是每小时刷一次完全够用。如果你要高频刷新建议申请付费或者用本地测试数据代替。3. 小内存MCU上的JSON解析cJSON实战3.1 为什么说JSON解析是这项目的隐形门槛很多初学者在把天气数据成功拿到手之后会发现自己高兴得太早。因为HTTP响应的原始内容里除了有用字段还包裹着HTTP状态行、响应头然后才是JSON正文。你要的不是整包数据而是其中的温度数字和天气现象文字。从长这样的一坨东西里提取数据HTTP/1.1 200 OK\r\n Content-Type: application/json; charsetutf-8\r\n Transfer-Encoding: chunked\r\n ...\r\n \r\n 65\r\n {results:[{location:{name:北京...},now:{text:晴,temperature:25}}]}\r\n 0\r\n \r\nSTM32F103C8T6的RAM只有20KB不能像PC那样随便用正则表达式或库函数。解析JSON的正确姿势是引入轻量级的cJSON库它专门为嵌入式环境设计核心代码只有两个文件内存占用可控。3.2 cJSON的标准解析套路拿到数据后我建议先做一步预处理把缓冲区里的数据转成以{开头、以一个完整}结尾的字符串去头的HTTP响应头和尾部的chunked分块信息。简单做法是定位到第一个{和最后一个}这两者之间的子串就是可以喂给cJSON_Parse的内容。解析代码大致如下#include cJSON.h typedef struct { char city[16]; char text[16]; int temperature; } WeatherData; void parse_weather(char *json, WeatherData *wx) { cJSON *root cJSON_Parse(json); if (root NULL) { return; } cJSON *results cJSON_GetObjectItem(root, results); cJSON *first cJSON_GetArrayItem(results, 0); cJSON *location cJSON_GetObjectItem(first, location); cJSON *now cJSON_GetObjectItem(first, now); strcpy(wx-city, cJSON_GetObjectItem(location, name)-valuestring); strcpy(wx-text, cJSON_GetObjectItem(now, text)-valuestring); wx-temperature atoi(cJSON_GetObjectItem(now, temperature)-valuestring); cJSON_Delete(root); }3.3 几个必须注意的内存与健壮性细节cJSON_Parse失败时返回NULL一定先判空再继续取字段解析完必须调用cJSON_Delete(root)释放内存否则20KB RAM很快被吃光第二次解析直接崩溃cJSON库内部使用malloc分配堆内存需要在启动文件里确认Heap足够大。一般默认的0x200够用了如果解析大JSON时报错可以适当调大Heap_Size取字段时如果不确定字段是否存在先判断cJSON_GetObjectItem返回的指针是否为NULL再取valuestring防止空指针异常我在调试时就遇到过解析两次就死机的情况后来一查是cJSON_Delete漏写了第二次cJSON_Parse时堆空间不足直接返回NULL而我没判空就往下执行程序跑飞。这种问题在PC上很难遇到但在小内存单片机上非常致命。3.4 使用缓冲区时的接收策略接收HTTP响应数据时我用的策略是在串口接收中断里把每个字节存进一个全局环形缓冲区主循环里检查缓冲区尾部是否出现了CLOSED字样说明ESP8266那边TCP连接已经关闭数据接收完毕或者把连接关闭标志位作为结束条件。整个原始数据可能长达1000字节以上缓冲区要开够大比如1024字节。如果收满了还没出现结束标志就清空缓冲区重新收避免旧数据污染新一轮请求。4. 0.96寸OLED显示从初始化到中文/天气图标4.1 I2C驱动与SSD1306初始化0.96寸OLED屏的核心驱动芯片是SSD1306分辨率128x64。它支持I2C和SPI两种接口我选的这块是I2C版本7位地址一般是0x3C。I2C总线只需要两根线接线少非常适合这种传感器/屏幕挂载场景。关于用硬件I2C还是软件模拟I2C我个人的建议是省事就用软件模拟。STM32F1的硬件I2C久负盛名的难用其实主要在于很多人不太会处理错误标志而软件I2C只要GPIO配置对了拉高拉低时序完全由你掌控移植性也好。OLED刷新对时序不敏感软件模拟完全够用。#define OLED_I2C_ADDR 0x78 // 0x3C左移一位的写地址 void OLED_WriteCmd(uint8_t cmd) { I2C_Start(); I2C_SendByte(OLED_I2C_ADDR); I2C_SendByte(0x00); // 控制字节0x00表示后续是命令 I2C_SendByte(cmd); I2C_Stop(); } void OLED_WriteData(uint8_t dat) { I2C_Start(); I2C_SendByte(OLED_I2C_ADDR); I2C_SendByte(0x40); // 控制字节0x40表示后续是数据 I2C_SendByte(dat); I2C_Stop(); }SSD1306上电后需要初始化命令序列核心配置包括打开电荷泵、设置显示时钟分频、设置复用率、设置段重映射方向、设置COM扫描方向、设置对比度、取消显示偏移、打开显示等。这些命令基本是固定的可以直接抄datasheet里的参考序列。我贴一段自己整理的完整初始化函数几个关键配置都加了注释void OLED_Init(void) { delay_ms(100); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x20); // 设置内存寻址模式 OLED_WriteCmd(0x02); // 页寻址模式适合逐页刷新 OLED_WriteCmd(0xB0); // 设置页地址为第0页 OLED_WriteCmd(0xC8); // COM扫描方向反向 OLED_WriteCmd(0x00); // 低列起始地址 OLED_WriteCmd(0x10); // 高列起始地址 OLED_WriteCmd(0x40); // 设置显示起始行 OLED_WriteCmd(0x81); // 设置对比度 OLED_WriteCmd(0xCF); OLED_WriteCmd(0xA1); // 段重映射左右反置 OLED_WriteCmd(0xA6); // 正常显示非反显 OLED_WriteCmd(0xA8); // 设置多路复用率 OLED_WriteCmd(0x3F); // 64路 OLED_WriteCmd(0xA4); // 从显存恢复显示内容 OLED_WriteCmd(0xD3); // 设置显示偏移 OLED_WriteCmd(0x00); OLED_WriteCmd(0xD5); // 设置时钟分频因子 OLED_WriteCmd(0x80); OLED_WriteCmd(0xD9); // 设置预充电周期 OLED_WriteCmd(0xF1); OLED_WriteCmd(0xDA); // 设置COM引脚配置 OLED_WriteCmd(0x12); OLED_WriteCmd(0xDB); // 设置VCOMH取消选择电平 OLED_WriteCmd(0x40); OLED_WriteCmd(0x8D); // 电荷泵设置 OLED_WriteCmd(0x14); // 开启电荷泵 OLED_WriteCmd(0xAF); // 打开显示 OLED_Clear(); }这段初始化里最容易出错的是0xA1和0xC8。如果显示出来左右镜像或上下颠倒就是这两个参数跟你屏幕的物理方向不匹配颠倒一下或换成0xA0/0xC0就能解决。4.2 SSD1306的工作原理与字符显示理解OLED显示原理很重要。SSD1306的显示GRAM是128x64位分成8页Page0到Page7每页纵向8个像素。写入数据时你先通过命令设置当前页地址和列地址然后连续写数据字节每个数据字节的8个bit对应当前页的8行像素。这种页列的寻址方式决定了显示函数的写法。显示一个字符本质上是把字符点阵数据逐字节写入GRAM。ASCII字符集我使用8x16点阵也就是宽8像素、高16像素。显示一个字符时高8位放在上页低8位放在下页void OLED_ShowChar(uint8_t x, uint8_t y, char ch) { uint8_t i; for (i 0; i 8; i) { OLED_SetPos(x i, y); OLED_WriteData(OLED_ASCII8x16[(ch - ) * 16 i]); } for (i 0; i 8; i) { OLED_SetPos(x i, y 1); OLED_WriteData(OLED_ASCII8x16[(ch - ) * 16 8 i]); } }这里的y是页编号0到7不是像素坐标。OLED_SetPos函数设置列起始地址和页地址通过组合命令来实现。4.3 中文字模的准备与天气图标显示OLED屏不能直接显示晴云雨这些汉字需要用取模工具把汉字转成点阵数组。我用的工具是PCtoLCD2002参数设置这几项非常关键取模方式逐行式取模走向顺向输出格式C语言数组阴码还是阳码阴码点亮的像素为1一个16x16的汉字点阵在逐行式取模下会生成32字节数据。前16字节是左上区域逐行扫描后16字节是左下区域逐行扫描正好对应SSD1306的页0和页1。如果你取模后显示出来的汉字上下颠倒或者左右反转优先检查是不是取模方式选错了或者显示函数里扫描顺序写反了。天气图标我用了几个32x32的位图数组。显示时同样要以页为单位写入只是每列要拆成4页来写也就是32像素高恰好占4页。图标素材可以从一些图标网站下载png再用取模工具转成数组也可以直接用画图工具手绘。对于晴多云小雨这三种天气用简单的太阳、云、雨滴图形就够了不用追求复杂。4.4 显示布局与数据刷新关于页面布局我最终采用的是四行结构第一行显示城市名16x16中文比如北京第二行显示天气现象16x16中文比如晴第三行显示温度用8x16 ASCII字体显示数字和℃字符第四行显示更新时间或湿度信息。这里有个实用的技巧因为天气现象只有有限的几个汉字我提前做了字库数组而不是运行时动态取模。比如定义了晴云雨雪等十几个汉字的点阵数组按索引取值。这样显示函数简单字库也小完全够用。温度数据我是用字符串拼接的方式char temp_str[16]; sprintf(temp_str, %d℃, wx.temperature); OLED_ShowString(8, 3, temp_str); // (8,3)表示第8列、第3页每次刷新显示时先OLED_Clear()清屏再重新绘制虽然多花几毫秒但能避免新旧数据重叠导致的重影。5. 实测踩坑记录那些让你怀疑人生的细节5.1 ESP8266反复重启或串口乱码先从供电查起我第一次给ESP-01S上电串口打印出来的是一堆乱码偶尔夹杂着ready。查了半天发现是面包板供电问题——模块发射瞬间拉低了电压导致芯片复位。两个解决方向一是外接独立的3.3V稳压模块二是在ESP8266的VCC和GND之间并一个大电容我用的是470uF电解电容。加上电容之后模块稳定多了没有再出现随机重启。这款模块本身的VCC走线很细瞬时电流一大就容易掉压所以如果你在别的项目里也用到它优先处理供电。5.2 串口收到数据不完整或粘包HTTP响应有时候一两千字节串口缓冲和接收方式直接决定数据完整性。我用的是USART2中断接收每收到一个字节就存入环形缓冲区。这里有两个坑一是缓冲区太小导致旧数据被覆盖。解决办法是把缓冲区设成1024字节并且在处理完后清空或调整读写索引。二是HTTP/1.1的chunked编码。心知天气返回的数据用了Transfer-Encoding: chunked也就是说响应体被拆成好几段每段前面有一个十六进制数字表示这段长度。cJSON解析前要先把chunked的分块信息剥离掉。我前面说的定位到第一个{和最后一个}这个方法就能顺便解决这个问题因为{和}之间的内容就是完整的JSON结构中间不会夹杂分块长度描述符。5.3 TCP连接偶尔超时或AT指令无响应ESP8266的AT指令在处理网络请求时是阻塞式的如果服务器响应慢模块可能几秒钟都不回复任何东西。刚开始我在代码里发完ATCIPSTART就死等CONNECT OK结果有时能连上有时超时程序就卡死了。解决方法是给每个AT指令加超时和重试机制。具体做法是用一个状态机发送指令后轮询接收缓冲区如果在规定时间内比如3秒没等到预期回复就重发指令最多重试3次全部失败则进入错误处理比如OLED显示NET ERROR。这样即使网络抖动系统也不会永久卡死。5.4 OLED花屏、残影和汉字颠倒OLED显示乱的排查顺序我每次都是先确认I2C地址是否正确0x3C还是0x3D有的屏是0x3D再确认初始化序列是否完整最后检查取模和写入方向。一个很隐蔽的问题出在OLED_SetPos函数上。SSD1306在页寻址模式下每次设置列地址时需要同时写低列地址和高列地址两个命令如果只写了低列没写高列显示内容会错位到奇怪的列上去。我在调试时曾经因为漏了0x10这条高列地址命令导致字符全部显示在屏幕右半部分看起来像花屏。另一个常见问题是刷新速度快于I2C传输速度。软件I2C如果时钟太快在某些屏上会丢数据。建议软件I2C的SCL频率控制在100kHz以下GPIO翻转之间加一点延时稳定压倒一切。6. 按我的经验再给你三条建议项目做完我最大的感受是这类单片机无线模块云服务的项目最容易出问题的环节往往不是C语言代码而是通信链路上某个不起眼的细节。最后分享三条我总结的经验。第一先把ESP8266单独在电脑上用串口助手调通确认能拿到天气数据再接STM32。这样可以隔离变量——网络问题归网络代码问题归代码别让它们混在一起。第二程序里把AT指令的原始回复全部通过串口打印出来。不要觉得乱开发阶段信息越全越好。你看到的可能是ATCWJAP返回FAIL或者WIFI DISCONNECT这些关键字是定位问题的第一手线索。等一切稳定了再把这些调试输出关掉。第三API密钥不要写死在代码里然后到处发。这个项目跑通之后如果你想传到GitHub或者分享给朋友记得把密钥单独定义成宏或者配置文件里上传时剔除掉。虽然免费版密钥泄露风险不大但养成这个习惯对以后接触物联网平台的项目很有帮助。这个项目本身可以做很多扩展加上DHT11做成网络天气本地环境温湿度双路显示加一个按键切换城市或者把刷新间隔改成可配置。核心的数据链路和处理思路都是相通的把这篇里的细节吃透后面再玩其他云平台或者网络模块都会顺手很多。本文还有配套的精品资源点击获取