物联网项目介绍

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 2 Likes: 0 Comments: 0

165.1物联网项目介绍

/*
 * 本代码块只收录第 165 课在 Keil 中实际停留展示的业务/接口适配片段,内容来自视频版 gizwits_product.c。
 * 本课只是介绍完整项目,没有现场编写或讲解这些代码;以下片段也不是可独立编译的完整文件。
 * 视频快速滚动时没有完整展示 USART3 回调的结尾,因此这里只抄清晰可见且边界可确认的代码,不擅自补齐。
 * 课程配套目录中的最终工程与视频画面在少量变量名、定时器编号上存在差异,这里以本课视频画面为准。
 */

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) /* HAL 定时器周期到达后的统一回调;本项目在其中维护云协议节拍和 ZigBee 心跳计数。 */
{                                                           /* 回调函数体开始。 */
    if(htim==&htim2)                                        /* 判断本次回调是否由 TIM2 触发。 */
    {                                                       /* TIM2 分支开始。 */
        keyHandle();                                        /* 调用机智云工程的按键处理函数。 */
        gizTimerMs();                                       /* 将机智云协议使用的毫秒计数递增一次。 */
    }                                                       /* TIM2 分支结束。 */
    if(htim==&htim3)                                        /* 判断本次回调是否由 TIM3 触发。 */
    {                                                       /* TIM3 分支开始。 */
        zigbee_heart_count++;                               /* 增加 ZigBee 心跳超时计数;收到心跳帧时会在下方代码中清零。 */
        if(zigbee_heart_count>100)                           /* 计数超过 100 后进入本项目设置的保持区间。 */
            zigbee_heart_count=91;                          /* 回写 91,使掉线状态可持续被上层逻辑识别,而不是无界累加。 */
    }                                                       /* TIM3 分支结束。 */
}                                                           /* 定时器周期回调结束;具体一次回调代表多久,要由 TIM2/TIM3 配置决定。 */

void timerInit(void)                                        /* 启动本项目使用的两个基本定时器中断。 */
{                                                           /* 定时器启动函数开始。 */
    HAL_TIM_Base_Start_IT(&htim2);                           /* 启动 TIM2 基本定时器并打开更新中断。 */
    HAL_TIM_Base_Start_IT(&htim3);                           /* 启动 TIM3 基本定时器并打开更新中断,用于心跳计数。 */
}                                                           /* 定时器启动函数结束;课程本节没有展示两个定时器的周期参数。 */

/* HAL_UART_RxCpltCallback() 中视频清晰展示的 USART2 分支。 */
if(UartHandle->Instance == USART2)                           /* 判断收到 1 字节的串口是不是连接 Wi-Fi 模组的 USART2。 */
{                                                           /* USART2 接收完成分支开始。 */
    gizPutData((uint8_t *)&aRxBuffer, 1);                    /* 把刚收到的 1 字节送进机智云协议层的接收缓冲区。 */
    HAL_UART_Receive_IT(&huart2, (uint8_t *)&aRxBuffer, 1); /* 重新挂接下一次 1 字节中断接收。 */ //开启下一次接收中断
}                                                           /* USART2 接收完成分支结束。 */

/* HAL_UART_RxCpltCallback() 中视频清晰展示的 USART3 数据帧校验与分发片段。 */
if(0x55==recv_buff[0]&&recv_buff[0]+recv_buff[1]+recv_buff[2]==recv_buff[3]&&recv_buff[4]==0xAA) /* 同时检查帧头、三字节加和及帧尾。 */
{                                                           /* 只有格式与简单校验都符合时才处理这 5 字节帧。 */
    switch(recv_buff[1])                                    /* 第 2 字节作为命令码,决定怎样解释第 3 字节或清除哪个状态。 */
    {                                                       /* 命令分发表开始。 */
        case 0x5:                                           /* 收到命令码 0x05。 */
            led2_on_cmd=0;                                  /* 按视频源码把卧室灯“开”命令标志清零;不是直接给 LED 引脚写 0。 */
            break;                                          /* 结束 0x05 分支,避免继续落入下一个 case。 */
        case 0x6:                                           /* 收到命令码 0x06。 */
            led2_off_cmd=0;                                 /* 把卧室灯“关”命令标志清零。 */
            break;                                          /* 结束 0x06 分支。 */
        case 0x7:                                           /* 收到命令码 0x07。 */
            curtain_on_cmd=0;                               /* 把窗帘“开”命令标志清零。 */
            break;                                          /* 结束 0x07 分支。 */
        case 0x8:                                           /* 收到命令码 0x08。 */
            curtain_off_cmd=0;                              /* 把窗帘“关”命令标志清零。 */
            break;                                          /* 结束 0x08 分支。 */
        case 0x9:                                           /* 收到命令码 0x09,表示远端温度数据。 */
            currentDataPoint.valuetemp2 = recv_buff[2];      /* 把第 3 字节保存到机智云数据点 temp2。 */
            break;                                          /* 结束温度分支。 */
        case 0xa:                                           /* 收到命令码 0x0A,表示远端湿度数据。 */
            currentDataPoint.valuehumi2 = recv_buff[2];      /* 把第 3 字节保存到机智云数据点 humi2。 */
            break;                                          /* 结束湿度分支。 */
        case 0xb:                                           /* 收到命令码 0x0B,表示远端仍在线的心跳。 */
            zigbee_heart_count = 0;                         /* 心跳到达即清零超时计数,供屏幕显示 ZigBee OK。 */
            break;                                          /* 结束心跳分支。 */
        default:break;                                      /* 未识别命令不做处理,并离开 switch。 */
    }                                                       /* 命令分发表结束。 */
}                                                           /* 当前 5 字节帧处理结束;回调其余部分在本课视频中未完整展示。 */

void uartInit(void)                                         /* 初始化机智云协议使用的串口接收。 */
{                                                           /* 串口接收启动函数开始。 */
    HAL_UART_Receive_IT(&huart2, (uint8_t *)&aRxBuffer, 1); /* 首次挂接 USART2 的单字节中断接收。 */ //开启下一次接收中断
}                                                           /* 本课画面中的 uartInit() 到此结束,不补入其他串口初始化。 */

int32_t uartWrite(uint8_t *buf, uint32_t len)                /* 机智云协议层调用的发送适配函数:把 buf 中 len 字节送给 Wi-Fi 模组。 */
{                                                           /* 串口发送函数开始。 */
    uint8_t crc[1] = {0x55};                                /* 保存需要追加的 0x55 转义字节;变量名叫 crc,但这里并不是 CRC 计算。 */
    uint32_t i = 0;                                         /* 32 位循环下标,从缓冲区第 0 字节开始发送。 */

    if(NULL == buf)                                         /* 发送前检查调用者是否传入空指针。 */
    {                                                       /* 空指针错误分支开始。 */
        return -1;                                          /* 返回 -1 表示没有可发送的有效缓冲区。 */
    }                                                       /* 空指针错误分支结束。 */

    for(i=0; i<len; i++)                                    /* 从头到尾遍历 len 个原始协议字节。 */
    {                                                       /* 单字节发送循环开始。 */
        HAL_UART_Transmit_IT(&huart2, (uint8_t *)&buf[i], 1); /* 通过 USART2 中断方式启动当前 1 字节发送。 */
        while (huart2.gState != HAL_UART_STATE_READY);       /* 空循环等待句柄恢复 READY,保证下一字节不会过早启动。 */ //Loop until the end of transmission

        if(i >=2 && buf[i] == 0xFF)                          /* 跳过帧头两个 0xFF;若后续数据出现 0xFF,就执行协议转义。 */
        {                                                   /* 追加转义字节分支开始。 */
            HAL_UART_Transmit_IT(&huart2, (uint8_t *)&crc, 1); /* 紧跟原 0xFF 再发送一个 0x55。 */
            while (huart2.gState != HAL_UART_STATE_READY);   /* 等待追加的 0x55 发送完成。 */ //Loop until the end of transmission
        }                                                   /* 追加转义字节分支结束。 */
    }                                                       /* 原始缓冲区遍历结束。 */

#ifdef PROTOCOL_DEBUG                                      /* 只有定义协议调试宏时才编译下面的十六进制日志。 */
    GIZWITS_LOG("MCU2WiFi[%4d:%4d]: ", gizGetTimerCount(), len); /* 打印当前协议计时值和原始数据长度。 */
    for(i=0; i<len; i++)                                    /* 再遍历一遍原始缓冲区,用于输出调试日志。 */
    {                                                       /* 调试打印循环开始。 */
        GIZWITS_LOG("%02x ", buf[i]);                       /* 以两位十六进制打印当前原始字节。 */

        if(i >=2 && buf[i] == 0xFF)                          /* 日志也要反映真正在线路上追加的转义字节。 */
        {                                                   /* 日志转义分支开始。 */
            GIZWITS_LOG("%02x ", 0x55);                     /* 在日志中额外打印 0x55。 */
        }                                                   /* 日志转义分支结束。 */
    }                                                       /* 调试打印循环结束。 */
    GIZWITS_LOG("\n");                                    /* 一帧打印完成后换行。 */
#endif                                                      /* 协议调试条件编译结束。 */

    return len;                                             /* 返回调用者要求发送的原始数据长度,不包含额外插入的 0x55。 */
}                                                           /* 串口发送适配函数结束。 */

一、物联网项目

本课先给出项目全貌,再演示最终效果和硬件连接,并没有开始从零实现代码。完整系统不是“手机直接连单片机”,而是由卧室终端、客厅中控、无线串口链路、Wi-Fi 模组、云平台和手机 App 共同组成。理解每段链路负责什么,比一开始钻进整个工程更重要。

1.1 物联网项目框图

flowchart LR
    A[卧室温湿度] --> M51[51 单片机<br/>卧室终端]
    M51 --> B[舵机/窗帘]
    M51 --> C[卧室 LED]
    M51 --> D[卧室 OLED]
    M51 <--> Z1[透明串口模块]
    Z1 <-->|2.4 GHz ZigBee 无线链路| Z2[透明串口模块]
    Z2 <--> M32[STM32<br/>客厅中控]
    E[客厅温湿度] --> M32
    F[光敏传感器] --> M32
    G[烟雾传感器] --> M32
    M32 --> H[客厅 LED]
    M32 --> I[客厅 OLED]
    M32 <--> W[ESP8266 Wi-Fi 模组]
    W <--> Cloud[机智云]
    Cloud <--> App[手机 App]
  • 51 单片机模拟卧室终端:采集卧室温湿度,驱动舵机模拟窗帘,控制卧室灯,并在 OLED 上显示本地状态。
  • STM32 模拟客厅中控:采集客厅温湿度、光照和烟雾状态,控制客厅灯,在 OLED 上汇总显示本机数据与卧室端数据。
  • 两块 MCU 之间各接一个无线透明串口模块。MCU 看见的是 UART 字节流,模块负责把字节无线转发给另一端。
  • STM32 再通过 USART2 与带机智云固件的 ESP8266 模组交换协议帧。ESP8266 连接 2.4 GHz Wi-Fi,把数据送入机智云;手机 App 通过互联网访问云端。
  • 图中手机侧的“WiFi、5G”表示手机可经 Wi-Fi 或蜂窝网络访问云平台;它不表示 ESP8266 支持 5 GHz Wi-Fi,也不表示手机的 5G 蜂窝信号直接发给 ESP8266。

系统中有两条方向相反的数据链:

方向典型路径本课示例
状态上报传感器 → MCU → ZigBee/ESP8266 → 云平台 → App温湿度、光照、烟雾、灯和窗帘状态
远程控制App → 云平台 → ESP8266 → STM32 → 本地执行或 ZigBee → 51客厅灯、卧室灯、窗帘开关
  • 远程链路中的每一跳都有不同职责:传感器把物理量变成电信号,MCU 做采集和业务判断,透明串口模块转发本地帧,ESP8266 提供 IP 网络接入,云平台负责设备与 App 间的消息中转。
  • “ZigBee 本质上也是串口”是原笔记中过度简化的说法。准确地说,本课购买的成品模块向 MCU 暴露 UART 透明传输接口;其无线侧仍是基于 IEEE 802.15.4 的 2.4 GHz 无线通信,ZigBee 本身并不等同于 UART。
  • 本项目使用点对点透明传输,没有在课上实现通用 ZigBee Mesh 网络。成品模块已经封装无线协议,因此 MCU 代码主要面对自定义的 5 字节应用帧。

1.2 物联网项目演示

  • 视频中的“三个屏幕”是两块开发板的 OLED 加手机 App:STM32 OLED 代表“智能家居中控”,51 OLED 代表卧室终端,手机 App 是远程查看和控制界面。
  • STM32 端显示客厅温湿度、烟雾状态、光照百分比、LED 状态和 ZigBee OK/Error。画面中的 Sound 标签实际被课程代码拿来显示光照强度,不是声音传感器读数。
  • 51 端显示卧室温湿度与窗帘状态;讲师说明演示版界面漏掉了卧室灯状态显示,这不影响灯本身受控。
  • App 点击“客厅灯”后,STM32 板上的 LED 随之开关;点击“卧室灯”后,命令还要经过 STM32 和两个无线串口模块到达 51;点击窗帘开关后,51 控制舵机转到相应位置。
  • App 同时显示客厅温湿度、光照、烟雾以及卧室温湿度。这些是设备上报到云端后再由 App 读取的实时业务数据,并不是手机自身传感器测得的数据。

心跳不是无线模块自带的“通电检测”,而是两块 MCU 约定的应用层消息:

  1. 51 端周期发送心跳命令帧。
  2. STM32 的 USART3 收到命令码 0x0B 后,把 zigbee_heart_count 清零。
  3. TIM3 周期中断持续增加该计数;长时间没有新心跳时,计数进入超时区间。
  4. 上层显示逻辑据此显示 ZigBee OK 或 ZigBee Error。
  • 视频拔掉 51 开发板电源后,STM32 过几秒显示错误;重新上电并恢复帧交换后又显示正常。这说明检测的是“远端程序仍在通信”,而不只是两个模块是否亮灯。
  • 心跳只能证明最近收到过符合协议的远端帧,不能单独证明温湿度值一定正确,也不能定位故障究竟出在 51、UART、无线链路还是供电。
  • 源码中 0x55 是帧头、0xAA 是帧尾,第 4 字节等于前三个字节之和才继续处理。这只是课程自定义的简单加和校验,不是 CRC,也不具备强错误检测能力。

关于供电,视频中的现象是 ESP8266 经常掉线,改用外部手机充电头供电、并把所用模组板接到其标注的 5 V 供电端后更稳定。应把这个经验准确理解为:

  • ESP8266 发射时存在明显电流脉冲;稳压器能力不足、线材压降、接口接触、去耦不佳都可能让 3.3 V 电源瞬间下跌,引起复位或断线。
  • 电脑 USB 和手机充电头都通常提供 5 V,稳定性差异来自具体端口限流能力、线缆和板上稳压设计,不能概括成“电脑 USB 一定不稳定”。
  • 只有带板载稳压器且明确标出 5V/VIN 的 ESP8266 模组板才能从该端输入 5 V。ESP8266EX 芯片工作电压是 2.5~3.6 V,GPIO 也不耐 5 V;绝不能把 5 V 直接接到芯片、3V3 引脚或 UART IO。
  • 视频改电压的对象是课程使用的模组/转接板供电端,不是把 ESP8266 芯片供电改成 5 V。接线前必须以自己购买模块的原理图、丝印和数据手册为准。

1.3 讲课思路

  • 第一阶段先认识系统框图和硬件,明确每个模块的供电、接口、数据方向与作用。
  • 第二阶段只用 STM32、温湿度传感器、ESP8266 和机智云完成一个最小 Demo,把“传感器 → 云 → 手机”单链路先跑通。
  • 第三阶段再把客厅传感器、显示、灯控、51 卧室终端、ZigBee 链路、心跳和控制协议逐步合并成完整项目。
  • 这样的拆分能把故障限制在较小范围。若直接一次接上所有模块,任何一个串口波特率、供电、数据点映射或帧格式错误都可能表现为“App 没反应”。
  • 机智云代码不是全部手写:平台根据产品数据点生成协议框架,开发者再完成 UART、定时、复位等硬件适配,并把传感器值和控制事件接入 currentDataPoint。
  • 视频只是快速滚动 STM32 与 51 工程,用来说明完整项目代码量较大;没有逐段讲解,也没有要求此时照抄整个工程。因此代码块只保留画面中与架构直接相关的三个片段。

代码块能提前看出三个接口边界:

  • HAL_TIM_PeriodElapsedCallback() 给机智云协议时基和 ZigBee 心跳超时提供周期事件。
  • USART3 接收片段解析 51 与 STM32 之间的 5 字节帧,更新卧室温湿度、命令确认和心跳状态。
  • uartWrite() 把机智云协议帧经 USART2 发给 Wi-Fi 模组;从第 3 字节开始,数据中的 0xFF 后要附加 0x55,这是机智云协议要求的字节转义,源码变量名 crc 并不准确。
  • HAL_UART_Transmit_IT() 名义上是中断发送,但随后用空 while 等待 gState 回到 READY,所以本函数仍逐字节串行等待。精教版只解释现状,不在本节擅自改成 DMA、阻塞发送或队列方案。

1.4 硬件选择

1.4.1 ZigBee无线串口收发模块
  • 视频使用的是 CC2530 类 2.4 GHz 无线串口透明传输模块,页面标有 VCC、TX、RX、GND、四个状态 LED 和一个配置按键。
  • “透明传输”表示模块尽量按原顺序转发 MCU 送入 UART 的字节;它不理解 0x55、温湿度或灯控的业务意义,帧格式和重传/心跳等应用规则由两端 MCU 约定。
  • CC2530 是支持 2.4 GHz IEEE 802.15.4/ZigBee 的无线 SoC,并带有 USART 外设。成品模块把协议栈、射频和串口接口封装在一起,才形成“像无线串口一样使用”的体验。

两块 MCU 不使用无线模块时,也可以先用杜邦线直接验证 UART 协议:

STM32 TX  ─────────► 51 RX
STM32 RX  ◄───────── 51 TX
STM32 GND ────────── 51 GND
  • TX 必须接对方 RX,RX 必须接对方 TX,并且两块板必须共地。共地给双方提供相同的电压参考,否则“高/低电平”没有可靠共同基准。
  • 还要确认双方 UART IO 电平兼容。供电引脚允许 5 V 不自动代表 TX/RX 是 5 V 逻辑耐受;模块板是否做电平转换必须查其说明。
  • 两端设置相同的帧格式,包括数据位、校验位和停止位;每个 MCU 的波特率要与本地连接的模块一致。
  • 视频建议先用线缆完成协议和业务调试,再替换成无线模块。这样可以把“应用协议错误”和“无线配置/射频问题”分开排查。

烧写 51 程序时可能出现冲突,因为下载器和 STM32/ZigBee 都会占用 51 的同一组串口线:

  • PC 的 USB 转串口正在向 51 RX 发送下载数据时,如果 STM32 TX 也连接并发送,两个数据源会混在一起,下载协议无法正确识别。
  • 若两个推挽输出被直接并接且电平相反,不只是数据混乱,还可能产生硬件争用电流。
  • 处理方法是烧写前断开 STM32/ZigBee 到 51 串口的连接,只保留下载器;烧写完成后再断电恢复业务接线。
  • 更稳妥的硬件设计可以加入跳帽、串联电阻、模拟开关或使用不同 UART,把“下载通道”和“运行通信通道”隔离。
1.4.2 ZigBee无线串口收发模块配置

视频所示 DL-2X 系列页面给出的配置流程是:进入设置模式 → 设置波特率 → 设置信道 → 设置工作模式 → 确认设置。具体 LED 图案只适用于该型号,购买不同模块时不能照搬。

  1. 模块断电,按住配置键不松手,再给模块供电。
  2. 四个 LED 循环闪烁后松开,进入设置模式。
  3. 在波特率阶段短按按键轮换选项;页面列出 2400、4800、9600、14400、19200、38400、57600、115200,课程选择 9600 bit/s。
  4. 长按直到 LED 再次循环闪烁,松开进入信道设置;两个点对点模块必须选到同一信道。
  5. 再进入工作模式设置,一个模块选点对点 A 端,另一个选点对点 B 端;课程不使用广播模式。
  6. 按该型号说明完成确认并重新上电,然后先用简单串口数据验证双向通信,再接入完整业务帧。
  • “同信道”决定两个无线端能否在同一射频频道上通信;“UART 波特率”决定 MCU 与其本地模块能否正确交换字节。这是两层不同配置。
  • 页面提供 16 个 2.4 GHz 信道。若周围 2.4 GHz Wi-Fi 较拥挤,可以在两个模块保持同信道的前提下尝试其他信道,但课程没有展开频谱规划。
  • 页面宣传传输距离约 250 m,应理解为特定模块在理想测试条件下的标称值。墙体、天线方向、安装高度、干扰、发射功率和供电都会显著缩短实际距离,不能把 250 m 当成室内保证值。
  • 广播/多对一模式可以扩展为多个卧室或书房终端向一个 STM32 中控上报数据,但多个终端同时发送会产生冲突风险,还需要地址、时隙/退避、重试和去重等应用协议;本课只提出扩展设想,没有实现代码。

ESP8266 的 Wi-Fi 与 ZigBee 模块配置不要混淆:

  • ESP8266EX 支持 802.11 b/g/n 的 2.4 GHz Wi-Fi,不支持 5 GHz Wi-Fi。手机显示的 5G 蜂窝网络与路由器的 5 GHz Wi-Fi 是两件完全不同的事。
  • 双频路由器把 2.4 GHz 与 5 GHz 共用一个 SSID 时,ESP8266 理论上仍可能连接 2.4 GHz,但某些配网流程或频段引导会造成困难。把两个频段临时分成不同名称,能更明确地保证手机和模组在配网时使用 2.4 GHz。
  • 改 SSID、换充电头或改供电端后“更稳定”是课程实测经验,不等于所有掉线都由同一原因造成。还应检查 RSSI、路由器兼容性、模组固件、串口错误、稳压器余量和去耦电容。

参考资料