基于FR8016与ESP8266的物联网温湿度时钟:主从协同与串口透传实践
1. 项目概述与核心思路拆解
这次折腾的是一个挺有意思的物联网小项目:用富芮坤的FR8016开发板做主控,搭配ESP8266 Wi-Fi模块和SHT30温湿度传感器,做了一个能联网显示时间、记录温湿度的桌面小摆件。最终的效果是,在本地的一块TFT屏幕上实时显示从网络获取的精确时间以及当前的温湿度,同时这些数据还能通过贝壳物联这个免费平台上传到云端,方便你在手机上随时查看历史记录。
这个项目的核心思路其实是一种在资源受限的MCU上实现复杂网络功能的经典架构: 主从协同 。FR8016作为主控MCU,负责核心的业务逻辑,比如驱动屏幕、读取传感器、处理数据;而ESP8266则扮演一个纯粹的“网络透传模块”,它只负责把FR8016通过串口发给它的数据原封不动地发送到贝壳物联的服务器,并把服务器返回的数据通过串口传回给FR8016。这样做的好处是,FR8016不需要处理复杂的TCP/IP协议栈和HTTP/WebSocket等应用层协议,大大降低了编程复杂度和对主控芯片资源(如RAM、Flash)的要求。整个系统的数据流非常清晰:传感器数据上行,网络时间下行,通过串口这个“桥梁”在FR8016和ESP8266之间交换。
选择贝壳物联平台也是经过考量的。对于个人开发者、学生或者物联网爱好者来说,自建服务器成本高、维护麻烦。贝壳物联提供了设备接入、数据存储和简单展示的免费服务,其基于JSON的通讯协议也足够轻量,非常适合通过串口透传来实现,几乎是为这类“MCU+Wi-Fi模块”的经典组合量身定做的快速开发方案。
2. 硬件选型与核心模块解析
2.1 主控芯片:富芮坤FR8016HA
FR8016是一款基于ARM Cortex-M0内核的蓝牙5.0低功耗SoC。在这个项目里,我们主要利用它的通用MCU特性。它的主频、Flash和RAM资源对于驱动屏幕、处理传感器数据和运行一个轻量级的实时操作系统(如FreeRTOS)或裸机程序来说是完全足够的。其丰富的外设,特别是UART串口,是实现与ESP8266通信的关键。需要注意的是,FR8016的官方SDK和开发环境是需要提前熟悉的部分,包括如何创建工程、配置时钟、初始化外设等。
2.2 网络模块:ESP8266-01/12系列
ESP8266以其极高的性价比和成熟的AT指令集/透传固件,成为了物联网项目的“万金油”。在这个项目中,我们将其设置为 透传模式 。这意味着一旦通过AT指令配置好目标服务器(贝壳物联)的IP和端口,ESP8266就会建立一个稳定的TCP连接,之后所有通过串口发送给它的数据,都会直接“透明传输”到服务器,反之亦然。这省去了在主控端实现Socket客户端的麻烦。选择ESP-01还是ESP-12F,主要区别在于GPIO数量和板载Flash大小,对于纯透传应用,ESP-01S就足够了。
2.3 传感器:SHT30温湿度传感器
SHT30是Sensirion出品的数字温湿度传感器,精度高、稳定性好,采用I2C接口通信。其功耗低,测量范围宽,完全符合室内环境监测的需求。与DHT11/22这类单总线传感器相比,SHT30的I2C接口时序更标准,与FR8016的I2C外设配合更顺畅,且数据更可靠。
2.4 显示模块:SPI TFT屏幕
选择一款小尺寸(如1.44寸或1.8寸)的SPI接口TFT屏幕。SPI接口占用MCU的引脚少,驱动简单,且有成熟的开源驱动库(如TFT_eSPI、U8g2的硬件SPI驱动)可以移植或参考。屏幕的分辨率(128x128或128x160)足以清晰显示时间、日期和温湿度信息。
注意:硬件连接规划 :在动手焊接或连接杜邦线之前,务必画一个简单的连接图。核心连接包括:FR8016的UART0_TX/RX连接ESP8266的RX/TX;FR8016的一组I2C_SCL/SDA连接SHT30;FR8016的一组SPI引脚(SCK, MOSI, CS, DC, RST)连接TFT屏幕。此外,别忘了给ESP8266和SHT30提供稳定的3.3V电源。
3. 软件架构设计与关键实现
整个项目的软件部分可以划分为几个相对独立的模块,在FR8016上以裸机或轻量级RTOS任务的方式组织起来。
3.1 贝壳物联通讯协议解析与封装
贝壳物联的协议是基于JSON的文本协议,通过TCP连接传输。这对于串口透传非常友好。我们需要实现三个核心功能:
- 设备登录(心跳) :协议规定设备需要每40-50秒发送一次登录(或心跳)报文,以维持在线状态。报文格式固定。
- 数据上报 :将温湿度数据按照指定格式打包上报。
- 时间查询 :向服务器请求当前网络时间,服务器会返回一个格式化的时间字符串。
在代码中,我们将这三个功能封装成三个函数,每个函数负责组装对应的JSON字符串,并通过串口发送函数 uart_write 发送出去。 这里的一个关键细节是JSON字符串的构造 。原始代码使用了 sprintf ,这在嵌入式开发中需要谨慎,因为可能造成栈溢出。更安全的做法是估算最大长度,使用静态缓冲区,或者分步组装。
// 示例:更安全的数据上报函数封装
void bigiot_report_data(float temperature, float humidity) {
// 预分配足够大的缓冲区,避免溢出
static char tx_buffer[128];
// 使用格式化的方式构造JSON,注意转义引号
int len = snprintf(tx_buffer, sizeof(tx_buffer),
"{\"M\":\"update\",\"ID\":\"%s\",\"V\":{\"%s\":\"%.1f\",\"%s\":\"%.1f\"}}\n",
DEVICE_ID, TEMP_INTERFACE_ID, temperature, HUMI_INTERFACE_ID, humidity);
if (len > 0 && len < sizeof(tx_buffer)) {
uart_write(UART0, (uint8_t*)tx_buffer, len);
}
}
3.2 定时任务调度设计
由于需要周期性地执行登录、查询时间、上报数据以及刷新屏幕等任务,一个简单的定时器调度机制是必不可少的。原始代码中使用了多个硬件定时器。这里我推荐一种更清晰、易于维护的 “软件定时器” 或 “时间片轮询” 架构。
在main函数的超级循环中,我们通过检查系统滴答计时器(SysTick)的溢出标志或直接读取毫秒计数器,来判定各个任务是否到了该执行的时间。
// 定义任务结构体和执行周期(单位:毫秒)
typedef struct {
uint32_t last_run_time;
uint32_t interval;
void (*task_func)(void);
} sched_task_t;
sched_task_t task_list[] = {
{0, 10000, task_bigiot_login}, // 每10秒登录/心跳
{0, 15000, task_report_sensor}, // 每15秒上报数据
{0, 25000, task_query_time}, // 每25秒查询时间
{0, 1000, task_update_display}, // 每1秒刷新显示
// ... 可以添加更多任务
};
void main_scheduler(void) {
uint32_t current_tick = get_system_tick(); // 获取当前系统毫秒数
for (int i = 0; i < ARRAY_SIZE(task_list); i++) {
if ((current_tick - task_list[i].last_run_time) >= task_list[i].interval) {
task_list[i].task_func();
task_list[i].last_run_time = current_tick;
}
}
}
int main(void) {
// 硬件初始化...
while (1) {
main_scheduler(); // 调度所有任务
// 其他低优先级或后台处理
}
}
这种方式的优点是结构清晰,添加或修改任务周期非常方便,且避免了多个硬件定时器中断可能带来的冲突和优先级管理问题。
3.3 串口数据接收与解析的优化
原始代码修改了串口中断服务函数(ISR),采用循环读取的方式直到硬件FIFO为空,并将数据存入一个缓冲区,直到遇到换行符 \n 才认为一帧数据接收完成。这是一个 正确且关键 的做法,避免了数据包被截断。
我们可以对此进行优化和强化:
- 环形缓冲区(Ring Buffer) :在中断服务程序(ISR)中,时间至关重要。应使用环形缓冲区来接收数据。ISR只做最简单的事情:将数据从UART数据寄存器放入环形缓冲区尾指针处。数据解析工作留给主循环或一个低优先级的任务来完成。这能极大减少中断阻塞时间。
- 协议解析器 :在主循环中,从环形缓冲区取出完整的一帧数据(以
\n结尾)后,需要解析其内容。原始代码使用了strstr查找”time”关键字,这适用于简单的场景。更健壮的做法是解析整个JSON对象。可以移植一个轻量级的JSON解析库(如cJSON),或者自己写一个简单的状态机来提取关键字段。例如,解析服务器返回的时间报文:
{"M":"time","T":"2023-10-27 15:30:45"}
我们需要解析出 ”T” 字段后面的时间字符串,并将其分解成年、月、日、时、分、秒的数值。
3.4 本地时钟的维护与同步
从服务器获取到时间后,我们得到了一个绝对的时间点(例如:2023-10-27 15:30:00)。之后,我们需要一个本地的“软时钟”来走时。原始代码使用一个1秒的定时器中断来递增秒、分、时等变量。
这里有几个 重要的细节和坑点 :
- 定时器精度 :用于产生1秒中断的硬件定时器(如RTC、通用定时器)的时钟源精度决定了本地时钟的走时精度。如果使用内部RC振荡器,误差可能较大(每天误差几秒到几十秒)。这就是为什么我们需要定期(如每25秒)从网络同步一次时间,以校准累积误差。
- 同步策略 :网络同步得到的时间是“权威时间”。当收到新的网络时间后,应直接覆盖本地的时、分、秒变量。但是,如果同步瞬间刚好在本地秒计数器自增的附近,可能会造成显示跳变或重复。一个更平滑的做法是,计算网络时间与本地时间的偏差,然后在接下来的若干秒内,微调每次“秒加一”的间隔,逐渐将偏差纠正到零,而不是瞬间覆盖。
- 时间变量类型 :处理年、月、日、时、分、秒的变量最好使用
uint8_t或uint16_t,并注意边界检查(如秒到60归零,月到12归零等)。可以考虑用一个结构体来组织,使代码更清晰。
typedef struct {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
} rtc_time_t;
rtc_time_t current_time;
4. 核心功能模块的深入实现
4.1 ESP8266透传模式配置的稳定性保障
虽然AT指令配置看起来简单,但在实际部署中,ESP8266的稳定性是项目成败的关键。不能仅仅在开发阶段配置一次就了事。
- 上电初始化序列 :FR8016上电后,不应立即发送AT指令。应等待几百毫秒,确保ESP8266已完成自身启动。然后发送
AT指令测试模块是否就绪,收到OK后再进行后续配置。 - 完整的配置流程 :
// 伪代码示例 uart_send_string("AT\r\n"); // 测试连接 wait_for_response("OK", 1000); uart_send_string("AT+CWMODE=1\r\n"); // 设置为Station模式 wait_for_response("OK", 1000); uart_send_string("AT+CWJAP=\"SSID\",\"PASSWORD\"\r\n"); // 连接Wi-Fi wait_for_response("WIFI CONNECTED", 5000); // 连接需要较长时间 wait_for_response("WIFI GOT IP", 5000); uart_send_string("AT+CIPSTART=\"TCP\",\"www.bigiot.net\",8181\r\n"); // 连接贝壳物联服务器 wait_for_response("CONNECT", 3000); uart_send_string("AT+CIPMODE=1\r\n"); // 开启透传模式 wait_for_response("OK", 1000); uart_send_string("AT+CIPSEND\r\n"); // 开始透传 wait_for_response(">", 1000); // 从此之后,所有通过该串口发送的数据都将直接发送到服务器 - 异常处理与重连机制 :必须在程序中加入对ESP8266响应超时的判断。如果任何一步失败(例如Wi-Fi密码错误、服务器连接失败),应该重置ESP8266(通过控制其EN或RST引脚)或重新发送AT指令序列。更健壮的做法是,在主循环中定期检查TCP连接是否还在(例如,通过心跳包的响应),如果发现连接断开,自动触发重连流程。
4.2 SHT30传感器数据读取与滤波
SHT30通过I2C通信,读取数据相对简单。但环境数据可能存在微小波动,直接显示原始数据可能会导致最后一位数字频繁跳动,影响观感。
- 基本的I2C读取 :按照SHT30数据手册,先发送测量命令(例如高重复性测量),等待测量完成(可以查询状态位或简单延时),然后读取6个字节的数据,计算得到温度和湿度值。
- 软件滤波 :一个简单有效的滤波方法是 移动平均滤波 。例如,维护一个最近10次温度读数的数组,每次显示时取平均值。
这样处理后的数据会平滑很多。对于湿度数据可以采用同样的方法。#define FILTER_SIZE 10 float temp_history[FILTER_SIZE]; uint8_t history_index = 0; float read_and_filter_temperature(void) { float raw_temp = sht30_read_temperature(); // 读取原始温度 temp_history[history_index] = raw_temp; history_index = (history_index + 1) % FILTER_SIZE; float sum = 0; for (int i = 0; i < FILTER_SIZE; i++) { sum += temp_history[i]; } return sum / FILTER_SIZE; }
4.3 TFT屏幕显示界面的优化
显示部分直接决定了产品的用户体验。除了简单地显示数字,我们可以做得更好。
- 字体与布局 :选择一款清晰易读的等宽字体来显示时间(如Digital-7风格字体),用稍小的字体显示日期和温湿度。合理布局,留出边距,避免拥挤。
- 局部刷新 :如果每次刷新都重绘整个屏幕(全屏刷新),可能会造成明显的闪烁。优化策略是 局部刷新 。例如:
- 只有秒位数字每秒变化,那么每秒只重绘秒位所在的区域。
- 分钟和小时只有在变化时才重绘。
- 温湿度数据可以设定一个变化阈值(如温度变化超过0.2度),只有当变化超过阈值时才更新显示。 这需要驱动库支持在指定坐标区域绘制,并且要管理好每个显示元素的位置和旧值。
- 视觉效果 :可以增加一些简单的UI元素,比如在时间下方画一条分隔线,为温湿度数据添加图标(一个小温度计和一滴水的符号),这些都能极大提升美观度。
5. 系统集成、调试与问题排查实录
当所有模块单独测试都正常工作后,将它们集成在一起时,常常会出现一些意想不到的问题。下面是我在实现过程中遇到的一些典型问题及解决方法。
5.1 串口数据混乱或丢失
现象 :屏幕显示的时间错乱,或者温湿度数据上传不稳定,有时能成功有时失败。 排查与解决 :
- 检查波特率 :首先确认FR8016的UART0和ESP8266的串口波特率是否严格一致(通常是115200)。哪怕有一个bit的误差,长时间通信也会导致错位。
- 检查电平 :确保FR8016(3.3V电平)和ESP8266(也是3.3V电平)的串口TX/RX线直接交叉连接即可,无需电平转换。如果ESP8266模块是5V tolerant的版本,连接3.3V MCU一般也没问题,但最好还是统一在3.3V下工作。
- 解决数据覆盖 :这是最可能的原因。原始代码中的
rev_buff是一个全局数组,在中断中填充。如果一帧数据还没被主循环处理完,下一帧数据又来了,就会发生覆盖。 必须使用环形缓冲区 。中断只负责写入环形缓冲区,主循环从中读取和解析。这能从根本上解决数据竞争问题。 - ESP8266的响应干扰 :在透传模式下,ESP8266接收到服务器数据会直接通过串口转发。但同时,如果你在透传模式下意外发送了
+++(在特定时间内且后面不跟其他字符),ESP8266会退出透传模式,回到AT指令状态,这会导致后续数据发送失败。确保你的程序不会意外产生这样的数据流。
5.2 设备在贝壳物联平台频繁掉线
现象 :设备状态时而在线时而不在线,数据上传断断续续。 排查与解决 :
- 严格遵守心跳间隔 :贝壳物联要求40-50秒内必须有一次通信。你的心跳定时任务必须 绝对保证 在这个时间间隔内执行。检查你的定时器是否准确,任务调度是否被其他长时间操作阻塞。将心跳间隔设置为45秒是一个安全的选择。
- 心跳包内容必须正确 :心跳包(即登录包
{"M":"checkin"...})中的设备ID和API Key必须与你在平台创建的一模一样,包括大小写。建议将这两个字符串定义为宏或常量,避免在代码中手误写错。 - 网络稳定性 :ESP8266的Wi-Fi连接可能不稳定。确保路由器信号良好。可以在代码中加入Wi-Fi信号强度(RSSI)的读取和显示,当信号弱时(如小于-70dBm),可以尝试重新连接Wi-Fi。
- 服务器连接保持 :TCP连接本身可能因为网络波动而断开。除了依赖心跳,还可以在程序中检测TCP连接状态。一个简单的方法是,如果连续发送几次心跳或数据都没有收到任何来自服务器的回应(注意,查询时间是有回应的,但心跳和上传数据可能没有直接回应),则可以认为连接已断,需要重新执行
AT+CIPSTART和AT+CIPSEND流程。
5.3 时间显示不同步或走时不准
现象 :网络同步后时间准确,但几小时后发现快了或慢了几分钟。 排查与解决 :
- 校准1秒定时器 :用于产生1秒中断的时钟源是问题的根源。如果使用FR8016的内部RC振荡器(HSI),其精度可能只有±1%甚至更差。这意味着一天的理论误差可能高达86400秒 * 1% = 864秒,即14分钟多! 解决方案是使用外部晶振(HSE)作为时钟源 ,其精度通常可达±20ppm(百万分之二十),一天误差约1.7秒,完全可以接受。
- 在FR8016中配置时钟树 ,确保系统时钟和定时器时钟都来源于高精度的HSE。
- 优化同步算法 :如前所述,采用“渐进式校准”而非“硬覆盖”。记录网络时间与本地时间的差值(秒级),然后在接下来的N次秒中断里,每次微调计数器的累加值,慢慢消化这个差值。
- 处理同步时刻的显示跳变 :在收到网络时间并更新本地时间变量的瞬间,可能刚好在屏幕刷新函数执行前后,导致显示出一秒的错乱。可以在时间变量更新时,设置一个“时间已更新”的标志位,屏幕刷新函数检测到这个标志位后,进行一次全屏时间重绘,之后清除标志位。这样能保证显示立刻更新到最新且完整的时间。
5.4 功耗优化考虑(如需电池供电)
如果希望这个时钟能脱离USB线,用电池供电,那么功耗就是必须考虑的问题。
- ESP8266的功耗控制 :ESP8266在连续透传模式下功耗较高(约70mA)。可以修改方案:仅在需要上传数据或同步时间时,才唤醒ESP8266并建立连接,完成后立即让其进入深度睡眠(Deep Sleep)。但这需要硬件上连接ESP8266的RST或EN引脚到FR8016的GPIO,以便FR8016能主动唤醒它。同时,心跳和数据上报的周期要拉长(例如每分钟甚至每5分钟一次)。
- FR8016的低功耗模式 :FR8016本身支持多种低功耗模式。在两次屏幕刷新和传感器读取的间隙,可以让CPU进入睡眠模式(Sleep),等待定时器中断唤醒。这能显著降低平均电流。
- 屏幕的功耗 :TFT屏幕是耗电大户。可以考虑使用OLED屏幕,其黑色像素不发光,在显示深色主题时更省电。或者,增加一个光敏传感器,在环境光暗时自动降低屏幕亮度或关闭屏幕。
6. 功能扩展与更多玩法
基础功能实现后,这个硬件平台还有很大的扩展空间。
6.1 添加物理按键进行交互
可以添加1-3个按键,实现以下功能:
- 模式切换 :按键在“显示时间”、“显示温湿度曲线”、“显示网络状态”等不同界面间切换。
- 手动同步时间 :按一下按键,立即触发一次网络时间查询。
- 亮度调节 :长按按键进入亮度调节模式,再短按增减亮度。 按键处理需要实现消抖(软件延时或状态机),并在主循环中定期扫描状态。
6.2 在贝壳物联平台实现反向控制
贝壳物联平台支持向设备发送指令。你可以定义一个简单的指令协议。例如,在平台上发送 LED:ON ,设备收到后解析指令,控制FR8016上的一个GPIO点亮LED;发送 LED:OFF 则熄灭。这需要你在串口接收解析部分,增加对平台下发指令的识别和处理逻辑。
6.3 本地数据记录与简单分析
如果FR8016的Flash空间有富余,可以开辟一小块区域作为存储区,每隔一段时间(如每小时)将温湿度数据连同时间戳存储起来。这样即使网络断开,数据也不会丢失。之后可以设计一个简单的界面,通过按键翻看历史数据,或者在联网后将这些历史数据批量上传到云端。
6.4 更换或增加传感器
SHT30可以更换为其他I2C或SPI接口的传感器,例如:
- BMP280 :测量大气压强和温度,可以做一个简易的气压计。
- VOC/Gas Sensor :检测空气质量。
- 光照传感器 :根据环境光自动调节屏幕亮度。 只需要修改相应的驱动读取函数,并在显示和数据上报部分增加对应的字段即可。
这个项目麻雀虽小,五脏俱全,涵盖了嵌入式开发中硬件选型、外设驱动、通信协议、定时调度、数据滤波、低功耗设计等多个核心环节。把它吃透,不仅能做出一个实用的桌面小工具,更能让你对物联网终端设备的开发有一个扎实而全面的理解。最重要的是,整个过程充满了动手和解决问题的乐趣,当看到自己做的设备稳定地显示着时间、记录着环境数据时,那种成就感是无可替代的。
更多推荐

所有评论(0)