十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32+OLED+ESP8266实现温度曲线手机监控系统

STM32+OLED+ESP8266实现温度曲线手机监控系统 简介基于STM32的毕业设计完整工程包面向电子、嵌入式相关专业学生及开发者。项目采用时间片轮询结构驱动OLED实时显示温度历史曲线借助ESP8266-01S模块实现物联网数据上传至手机APP并具备PI计算输出控制温度方案、温度上下限声光报警功能覆盖传感器采集、显示、无线通信、控制策略与报警等完整闭环。压缩包共242个文件约50.16MB以C语言源码.c/.h、Keil工程配置.uvprojx等、原理图与PCB设计.schdoc/.pcbdoc、说明文档.pdf/.txt/.md及编译调试过程文件为主文档包含设计说明、引脚连接、程序框架与调试记录便于对照学习与二次开发。已有119人学习浏览适合用于毕业设计参考、课程设计实践或嵌入式技能提升。工程包包含完整源码与硬件设计文档为作者整理的实战成果能帮助读者快速理清STM32项目开发流程并对ESP8266物联网通信、OLED绘图、PI温控策略形成工程化认知。1. 把 STM32 的 OLED 温度曲线送进手机 APP这道毕设题真正难在哪这道题初看是显示题OLED 上画一条温度历史曲线听着就是点几个像素的事。真正动手会发现难点恰好不在屏幕而在三段数据通路——传感器把温度变成数值数值在 128x64 的小屏上变成一条能看懂趋势的曲线再通过物联网协议穿过路由器落到手机 APP 里。STM32 是中间人OLED 是终端ESP8266 加 MQTT 是搬运工三条链路任何一段断了演示都会卡在「温度出不来」这种问题上。适合两类人一类是做毕业设计、需要一条完整可演示链路的本科生另一类是平时写后端或上位机、想搞清楚嵌入式侧数据怎么上行的工程师。驱动部分按 HAL 库展开联网部分按乐鑫 AT 2.x 指令集展开两套代码凑齐就是完整可演示链路。2. 把硬件链路先立住STM32、OLED、DS18B20、ESP8266 的选型与接线2.1 器件选型不是每一块 OLED 都走 I2C也不是所有 Wi-Fi 方案都叫 ESP8266标题里四个关键词对应四类器件选型时最容易踩的坑是「同名不同物」。OLED 模块里 0.96 寸、128x64、SSD1306 控制器是绝对主流走 I2C 或 SPI 都有而 1.44 寸的 OLED 常见是 SPI 接口、ST7789 控制器驱动方式和 SSD1306 完全不同。如果照搬网上的 SSD1306 代码去点 1.44 寸屏大概率花屏。这篇按最常见方案展开0.96 寸 SSD1306I2C 接口7 位地址 0x3C。温度传感器用 DS18B20 是毕业设计的经典选择单总线协议、12 位分辨率、精度 ±0.5°C一个 GPIO 引脚就能读完数据。比 NTC 做 ADC 校准省事比 SHT30 省 I2C 总线。通信模块选 ESP8266-01S原因是它便宜、资料多、出厂自带 AT 固件STM32 只需要通过串口发字符串指令就能完成 Wi-Fi 连接和 MQTT 收发不需要在 STM32 上移植协议栈。2.2 接线表与引脚分配先定死 I2C、单总线、串口三组外设用 STM32F103C8T6 最小系统板为例引脚分配可以参考下表。注意 ESP8266 的供电是重点它启动瞬态电流能到 300mA 以上直接用 STM32 板载 3.3V LDO 经常带不动表现为模块反复重启。功能器件关键参数接线位置主控STM32F103C8T672MHz64KB Flash-显示0.96 寸 SSD1306 OLED128x64I2C 0x3CSCLPB6SDAPB7测温DS18B20单总线12bitDQPB0加 4.7k 上拉到 3.3V联网ESP8266-01S串口 AT 固件TXPA10RXPA9供电AMS1117-3.35V 转 3.3V峰值 500mA单独给 ESP8266 供电DS18B20 的数据线是开漏结构上拉电阻一定要加否则温度值会随机跳成 85°C 或 -127°C。OLED 模块本身一般带 4.7k 上拉不用额外补。ESP8266 的 RX 是 3.3V 逻辑STM32 的 PA9 输出也是 3.3V可以直接连如果换 5V 单片机就要加分压。2.3 CubeMX 初始化里最容易被忽略的三个参数I2C1 开启后时钟频率建议直接设 400kHz不要停在默认的 100kHz。这两个速率整屏刷新 1KB 显存的时间差大约 80ms 对 20ms肉眼可见的卡顿区别。DS18B20 的 PB0 配置成开漏输出GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_0; gpio.Mode GPIO_MODE_OUTPUT_OD; // 开漏靠外部 4.7k 上拉拉高 gpio.Pull GPIO_NOPULL; // 上拉电阻放在外部不用内部上拉 gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, gpio);开漏加外部上拉是单总线协议的标准接法总线空闲时被上拉电阻拉高DS18B20 要应答时自己拉低。如果用内部上拉也能跑但线一长就容易误码。串口方面给 ESP8266 的 USART1 设成 115200、8N1和模块默认 AT 固件匹配如果模块被刷过其他波特率先用串口助手确认。2.4 等价方案ESP32-S3 单芯片替代 STM32ESP8266如果不想两片芯片来回调直接用 ESP32-S3 一颗芯片完成 OLED 驱动加 MQTT 上报是近年常见的毕设变体。ESP-IDF 或 Arduino 下驱动 0.91 寸 OLED128x32时SSD1306 寄存器序列和 STM32 版本完全同源只是 I2C 底层从 HAL 换成 IDF 的 i2c_master 接口。这个方案省掉 AT 指令桥接代价是代码从 C 裸机思维切到 RTOS 思维调试方式也不一样。做毕设求稳的话STM32 加 ESP8266 的资料量更大遇到问题更容易搜到答案。3. HAL 库点亮 OLED 全流程SSD1306 初始化、显存维护与 DS18B20 温度采集3.1 I2C 地址和控制字节0x3C 是地址0x00 与 0x40 是控制字节SSD1306 在 I2C 总线上是一个从机7 位地址默认 0x3C如果模块上 SA0 电阻焊到高电平则变成 0x3D。调用 HAL 的 I2C 传输函数时地址要左移一位拼上读写位所以代码里的 8 位地址是 0x78。每次传输的第一个字节是控制字节0x00 表示后面跟命令0x40 表示后面跟显示数据。很容易写错的点是直接把命令字节填进 HAL_I2C_Master_Transmit 的 pData漏掉控制字节结果屏幕完全没反应。#define OLED_ADDR 0x3C void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; // 0x00 控制字节 命令 HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR 1, buf, 2, 100); } void OLED_WriteData(const uint8_t *data, uint16_t len) { uint8_t buf[129]; buf[0] 0x40; // 0x40 控制字节 数据 memcpy(buf 1, data, len); HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR 1, buf, len 1, 100); }HAL_I2C_Master_Transmit最后一个参数是超时毫秒数I2C 总线上如果有设备没应答HAL 会等到超时返回错误码。在 while 循环里轮询显示时这个超时不要设太大100ms 足够否则总线异常时程序会卡在等待里。3.2 SSD1306 初始化序列16 条命令点亮关键命令逐个拆网上 SSD1306 的初始化代码版本很多核心命令其实就 16 条。注意不要把某宝卖家给的 25 条初始化原样照搬很多是给 51 单片机准备的里面重复设置或补了非必要命令在 HAL 库环境下也能跑但排查问题时多一个变量。static void OLED_Init(void) { uint8_t cfg[] { 0xAE, // 关闭显示初始化期间保持黑屏 0xD5, 0x80, // 显示时钟分频默认值即可 0xA8, 0x3F, // 复用比64 行128x64 屏必须 0xD3, 0x00, // 显示偏移 0 0x40, // 起始行 0 0x8D, 0x14, // 开启内部电荷泵不开启屏幕不亮 0x20, 0x02, // 页寻址模式后续刷屏按页操作 0xA1, // 段重映射常见模块用这个方向反了左右镜像 0xC8, // COM 扫描方向反了上下镜像 0xDA, 0x12, // COM 引脚配置128x64 用 0x12 0x81, 0xCF, // 对比度 207室内足够 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH 电压 0xA4, // 按 GDDRAM 内容显示 0xA6, // 正常显示0xA7 是反色 0xAF // 打开显示 }; for (int i 0; i sizeof(cfg); i) OLED_WriteCmd(cfg[i]); OLED_ClearBuffer(); OLED_Flush(); }0x8D 和 0x14 这条命令是「屏幕不亮」类问题的头号原因如果漏掉电荷泵开启屏幕即使在初始化后也只会显示模糊的残影。0x20 设置页寻址模式这决定了后续刷屏时数据是按 8 行一页连续写入的。3.3 显存 bufferSSD1306 不能读回必须在 MCU 侧维护SSD1306 的显存是 128x64 位也就是 1024 字节。I2C 接口只支持写入不支持读回所以任何想要「先读屏幕再改局部像素」的思路都行不通。常见做法是在 STM32 侧维护一个 8 页 x128 列的数组所有绘制操作先改数组最后整屏推送到屏幕。uint8_t disp_buf[8][128]; void OLED_ClearBuffer(void) { memset(disp_buf, 0, sizeof(disp_buf)); } void OLED_Flush(void) { for (uint8_t page 0; page 8; page) { uint8_t cmd[] {0xB0 page, 0x00, 0x10}; // 设置页地址和列地址 for (int i 0; i 3; i) OLED_WriteCmd(cmd[i]); OLED_WriteData(disp_buf[page], 128); } }页地址 0xB0 到 0xB7 对应屏幕的 8 个水平条带每个条带 8 像素高。列地址拆分两个命令0x00 到 0x0F 是低 4 位0x10 到 0x1F 是高 4 位合起来是 0 到 127。页寻址模式下写完 128 字节后列自动回绕到 0但页不会自动加一所以每页都要重新设置页地址。3.4 DS18B20 单总线读取用 GPIO 模拟时序比 I2C 更容易翻车DS18B20 走的是单总线协议所有时序都用微秒级延时实现。HAL_Delay 最小单位是 1ms不能用需要一个微秒延时函数。72MHz 主频下常见的空循环延时写法是for 循环每次约 3 个时钟周期校准后乘系数。void Delay_us(uint32_t us) { for (uint32_t i 0; i us * 8; i) { __NOP(); } }这个系数要在调试器里实测校准不同优化等级下循环周期会变。读取温度的核心流程分四步复位、跳过 ROM、启动转换、等待后读取暂存器。12 位分辨率下转换时间是 750ms这个等待直接 HAL_Delay。int16_t DS18B20_GetTempX10(void) // 返回值单位 0.1°C { uint8_t low, high; int16_t raw; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过 ROM总线上只有一颗 DS18B20 DS18B20_WriteByte(0x44); // 启动温度转换 HAL_Delay(750); // 12 位转换典型时间 750ms DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器先低字节后高字节 low DS18B20_ReadByte(); high DS18B20_ReadByte(); raw (high 8) | low; return (int16_t)(raw * 625 / 1000); // 0.0625 * 10 0.625 }raw 是 16 位带符号数低 4 位是小数部分。raw 乘 625 除 1000 是把温度转成 0.1°C 的整数这样后续给 OLED 画曲线时全程不需要浮点运算也方便通过串口打印调试。写字节和读字节的时序差异在采样点写时靠拉低时长区分 0 和 1读时要在拉低释放后约 15us 采样数据线电平。4. 温度历史曲线在 OLED 上的绘制环形缓存、整数映射与分页刷新4.1 历史数据为什么用环形缓冲128 个采样点对应 128 列像素OLED 横向 128 像素一列像素对应一个采样点这是最直观的滚屏方案。用环形缓冲而不是普通数组是因为温度曲线要持续滚动显示新数据不断进来最旧的数据要被覆盖环形缓冲正好用取模运算完成这件事不需要 memmove 移动整个数组。#define HISTORY_LEN 128 static int16_t hist[HISTORY_LEN]; static uint8_t hist_head 0; void Temp_Record(int16_t temp_x10) { hist[hist_head] temp_x10; // 写入当前点 hist_head (hist_head 1) % HISTORY_LEN; // 头指针前移满则回绕 }buffer 里存放的是 0.1°C 单位的整数温度范围从 -550 到 1250对应 DS18B20 的全量程。头指针指向下一个要写入的位置同时也是整个缓冲区里最旧的数据。绘制时从 hist_head 开始遍历就能保证画出来的是时间上连续的一段历史。4.2 坐标映射公式把 20.0~50.0°C 压进 OLED 的第 8 到第 56 行OLED 纵向 64 行不能全用上下留出边框才像「曲线图」。常见做法是让曲线区占据第 8 到第 56 行顶部和底部各留出标尺区。温度范围固定比自适应好自适应会让曲线频繁上下跳动看起来像传感器坏了。温度范围曲线区行号适用场景20.0~50.0°C8~56室内环境、水温变化毕设最常用-10.0~40.0°C8~56户外环境监测0.0~100.0°C8~56量程宽但分辨率低慎用映射公式本质是线性插值。温度越高行号越小像素点越靠近屏幕上方。整数运算是关键浮点格式化输出到 OLED 这种低密度屏上没有意义还会拖慢刷新。static uint8_t temp_to_row(int16_t temp_x10) { if (temp_x10 TEMP_MIN_X10) temp_x10 TEMP_MIN_X10; if (temp_x10 TEMP_MAX_X10) temp_x10 TEMP_MAX_X10; return (uint8_t)(56 - (temp_x10 - TEMP_MIN_X10) * 48 / (TEMP_MAX_X10 - TEMP_MIN_X10)); }48 是曲线区行号的跨度56 是下边界。温度等于下限时括号内为 0返回 56温度等于上限时括号内为 48返回 8。TEMP_MIN_X10 和 TEMP_MAX_X10 用宏定义成 200 和 500一眼能看出是 20.0°C 和 50.0°C。4.3 绘制与送显先画点再连线的取舍逐列画点的逻辑很简单清显存、按序取历史数据、计算行号、把对应像素点亮最后整屏送显。如果温度变化平缓点与点之间会自然相接如果刚开机或传感器跳变会有断点。断点多的时候可以在相邻两点之间补一条竖线。void OLED_DrawTempCurve(void) { OLED_ClearBuffer(); for (int i 0; i HISTORY_LEN; i) { int16_t t hist[(hist_head i) % HISTORY_LEN]; OLED_DrawPixel(i, temp_to_row(t), 1); } OLED_Flush(); }OLED_DrawPixel 维护的是 MCU 侧显存数组先将目标行所属页取出把对应 bit 置 1最后 Flush 时才整屏推给 SSD1306。连线算法如果有余力可以用 Bresenham 直线但 128x64 分辨率下相邻点之间的间距最多就是量程折算的几行竖线补齐视觉上完全够用。4.4 0.91 寸 128x32 屏怎么改两处参数互换如果手里是 0.91 寸 128x32 的 OLED不要硬套上面的代码。初始化数组里 0xA8 后跟的 0x3F64 行复用比改成 0x1F32 行0xDA 的 0x12 改成 0x02Flush 循环从 8 页改成 4 页。temp_to_row 里的曲线区也要改比如第 2 到第 30 行。这两处改完同样的 HAL 库代码在 ESP32 上用 IDF 跑 0.91 OLED 也一样适用SSD1306 寄存器序列是通用的。5. 把温度数据发送到手机 APPESP8266 MQTT 桥接与 Android 订阅消费5.1 链路选型MQTT、TCP socket、HTTP 轮询怎么选温度数据从 STM32 到手机 APP 有三条常见路径。最简单的是手机开热点STM32 通过 ESP8266 直连手机创建的 TCP Server适合教室演示但出了局域网就废。HTTP 轮询是 STM32 定时 POST 到服务器APP 再定时 GET实时性差还得多维护一台服务器。毕设和实际小型项目里最常见的是 MQTTSTM32 发布主题APP 订阅主题中间 broker 转发公网可访问。方案实时性公网访问开发量推荐场景MQTT 公共 broker秒级支持中毕设、远程监控手机热点 TCP socket秒级不支持小教室演示HTTP 轮询秒级到分钟级需要服务器中低频上报MQTT 的 QoS 等级建议用 0。温度数据是周期上报丢一帧下一帧马上补上不需要重传机制。毕设不要被「无源物联网」这类低功耗方向带偏思路STM32 加 ESP8266 的有源链路开发周期最短演示也稳定。5.2 STM32 与 ESP8266 的 AT 桥接控制指令与 MQTT 发布ESP8266 官方 AT 2.x 固件提供 MQTT 相关指令只需要按顺序执行四步设 Station 模式、连 Wi-Fi、配置 MQTT 参数、连接 broker。调试时先用串口助手手动把每步跑通再搬进 STM32 代码这样能把「模块问题」和「MCU 问题」分开。ATCWMODE1 # 设置 Wi-Fi 模式为 Station ATCWJAPSSID,PASSWORD # 连接路由器等 10 秒返回 WIFI GOT IP ATMQTTUSERCFG0,1,stm32_demo,user,pwd,0,0, ATMQTTCONN0,broker.emqx.io,1883,1 ATMQTTPUB0,dev/temp,{\t\:25.6},0,0broker 地址用公共测试 broker 即可客户端 ID 要保证唯一否则会把同一个 broker 上相同 ID 的连接踢掉。ATMQTTCONN 最后的参数是 keepalive 秒数设 1 表示开启心跳保活。发布指令里{\t\:25.6}的转义是 AT 固件的转义格式STM32 组包时要在字符串里写成\\\才能把这个反斜杠发出去。int ESP8266_PublishTemp(int16_t temp_x10) { char cmd[96]; int n sprintf(cmd, ATMQTTPUB0,\dev/temp\,\{\\\t\\\:%d.%d}\,0,0\r\n, temp_x10 / 10, temp_x10 % 10); HAL_UART_Transmit(huart1, (uint8_t*)cmd, n, 200); for (int i 0; i 50; i) { if (UART1_WaitOK()) return 0; // 收到 OK 即发布成功 HAL_Delay(20); } return -1; // 超时常见原因是模块没连上 broker }HAL_UART_Transmit 是阻塞发送200ms 超时对几十字节的 AT 指令完全够。UART1_WaitOK 的实现在串口接收中断里维护接收缓冲每次进入判断缓冲区最后几个字节是不是OK\r\n。注意不要用 HAL_UART_Receive 阻塞等 OK因为模块的响应可能分多段到达。5.3 载荷设计一个 JSON 字段也要注意逗号陷阱MQTT payload 用 JSON 格式方便 APP 端解析但 AT 指令是按逗号分隔参数的payload 里如果出现逗号整个 ATMQTTPUB 的参数解析就会错位。比如多发一个湿度字段{t:25.6,h:40}模块可能把h:40}当成下一个参数处理轻则发送失败重则指令直接报 ERROR。常见做法是载荷只放一个字段或者把多个字段做 URL 编码。毕设场景下温度单字段完全够用。APP 端解析时也不要引入 JSON 库字符串截取即可String payload new String(message.getPayload()); String t payload.substring(payload.indexOf(:) 1, payload.indexOf(})); float temp Float.parseFloat(t);5.4 Android 端订阅Paho 回调线程里不能直接改 UI手机 APP 用 Eclipse Paho 的 Android 客户端订阅 dev/temp 主题后在回调里拿到温度。回调运行在 MQTT 的网络线程不能直接修改 UI 控件必须切到主线程。try { MqttClient client new MqttClient( tcp://broker.emqx.io:1883, MqttClient.generateClientId(), new MemoryPersistence()); client.setCallback(new MqttCallback() { Override public void connectionLost(Throwable cause) { } Override public void messageArrived(String topic, MqttMessage message) throws Exception { String payload new String(message.getPayload()); float temp Float.parseFloat( payload.substring(payload.indexOf(:) 1, payload.indexOf(}))); runOnUiThread(() - tempText.setText(temp °C)); } Override public void deliveryComplete(IMqttDeliveryToken token) { } }); client.connect(); client.subscribe(dev/temp, 0); } catch (MqttException e) { Log.e(MQTT, connect failed, e); }代码里 subscribe 的第二个参数是 QoS与发布端保持一致用 0。generateClientId 生成随机客户端 ID避免多台手机同时订阅时客户端 ID 冲突导致互踢。subscribe 之后可以在测试工具里向同一主题发布消息验证 APP 显示也可以用 ESP8266 的串口直接发一条 MQTT 消息。6. 联调顺序与 OLED 刷新优化三级验证、I2C 提速、脏矩形刷新6.1 按「传感器→协议→APP」三级验证不要在链路末端猜问题拿到源码包或自己写完代码别直接盯着 APP 上的温度看到不对就改 APP。先串口打印 DS18B20 原始值确认室温读数合理再用串口助手手动执行 AT 指令确认模块能连上 Wi-Fi 和 broker最后用手机上的任意 MQTT 调试工具订阅 dev/temp确认能收到发布的数据。三级链路每一级都有独立验证手段哪一级过不去就只查哪一级不要拿着手机 APP 从头查到底。如果 DS18B20 读出来固定是 85.0°C那是复位时序问题OLED 花屏先查地址是 0x3C 还是 0x3DMQTT 发布超时查一下 ESP8266 是否拿到了 IP再查 broker 地址有没有拼错。这些问题的定位时间90% 花在怀疑错层上。6.2 I2C 400kHz 与脏矩形刷新OLED 整屏刷新 1KB 数据是显示性能的瓶颈。把 I2C 从 100kHz 提到 400kHz整屏刷新从约 80ms 降到约 20ms配合 128 列整屏重绘曲线刷新率可以达到 12fps 左右肉眼已经比较顺滑。如果还要更快就改成局部刷新曲线变化只影响新写入的那一列和上一次写入的那一列把这两列的显存字节单独推送到对应页地址数据量从 1KB 降到 2 字节。void OLED_FlushColumn(uint8_t x) { uint8_t page 0; uint8_t cmd[] {0xB0 page, (x 0x0F), 0x10 (x 4)}; // 只刷新曲线所在的一列 for (int i 0; i 3; i) OLED_WriteCmd(cmd[i]); OLED_WriteData((uint8_t*)disp_buf[page][x], 1); }SSD1306 支持按列地址写入通过设置低 4 位和高 4 位列地址命令可以只更新一列数据而不是整屏。这里需要注意页寻址模式下每次列地址设置会重置列指针因此局部刷新的前提是显存完整地在 STM32 侧维护。用 400kHz 的 I2C 加上单列刷新曲线刷新率可以稳定超过 30fps这个帧率下 OLED 的视觉余晖已经能掩盖点与点之间的间隙了。本文还有配套的精品资源点击获取
返回列表