物联网项目32代码(1)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

171.6物联网项目32代码(1)

/* gizwits_product.c:以下只摘录本节实际加入的头文件、变量和业务代码,不包含后续课程的 ADC、ZigBee、LED 等逻辑。 */
#include <stdio.h>                    /* 引入标准输入输出声明,使下面能够调用课程源码中的 sprintf()。 */
#include "dht11.h"                   /* 引入 DHT11 驱动接口及接收数组 Data[5] 的声明。 */
#include "oled.h"                    /* 引入 OLED 初始化、清屏、汉字和字符串显示接口。 */
uint8_t OLED_BUFF[30];                /* 申请 30 字节显示缓冲区,暂存格式化后的温湿度字符串。 */

void userHandle(void)                 /* 机智云框架周期调用的数据采集函数;本节在这里读取传感器并刷新屏幕。 */
{                                     /* userHandle() 函数体开始。 */
		DHT_Read();                      /* 按课程原代码读取一次 DHT11;本节没有检查该函数返回的成功/失败状态。 */
		currentDataPoint.valuesmoke = HAL_GPIO_ReadPin(SMOKE_GPIO_Port, SMOKE_Pin); /* 读取烟雾模块数字引脚的原始高/低电平,作为 smoke 数据点。 */
    currentDataPoint.valuetemp1 = Data[0];//Add Sensor Data Collection;课程原代码把湿度整数位 Data[0] 写入了名为 temp1 的字段。
    currentDataPoint.valuehumi1 = Data[2];//Add Sensor Data Collection;课程原代码把温度整数位 Data[2] 写入了名为 humi1 的字段。
		sprintf(OLED_BUFF,"H:%2d%%T:%2dC ",currentDataPoint.valuetemp1,currentDataPoint.valuehumi1); /* 依照上述“写反”的字段排布生成 H、T 显示文本;%% 才会输出一个百分号。 */
		OLED_ShowString(35,2,OLED_BUFF,16); /* 从 x=35、第 2 页开始,用 8×16 ASCII 字模显示缓冲区内容。 */
}                                     /* userHandle() 函数体结束。 */

void userInit(void)                   /* 机智云框架的用户初始化入口,本节把 OLED 初始化与固定标题集中到这里。 */
{                                     /* userInit() 函数体开始。 */
    memset((uint8_t*)&currentDataPoint, 0, sizeof(dataPoint_t)); /* 保留生成包原有语句,把全部数据点状态清零。 */
	OLED_Init();                        /* 向 OLED 控制器发送初始化命令,使显示屏进入可用状态。 */
	OLED_Clear();                       /* 清空显示 RAM,避免上电时残留随机像素。 */
    OLED_ShowCHinese(16,0,0);         /* 在 x=16、第 0 页显示 0 号 16×16 字模“智”。 */
	OLED_ShowCHinese(32,0,1);         /* 在 x=32、第 0 页显示 1 号字模“能”。 */
	OLED_ShowCHinese(48,0,2);         /* 在 x=48、第 0 页显示 2 号字模“家”。 */
	OLED_ShowCHinese(64,0,3);         /* 在 x=64、第 0 页显示 3 号字模“居”。 */
	OLED_ShowCHinese(80,0,4);         /* 在 x=80、第 0 页显示 4 号字模“中”。 */
	OLED_ShowCHinese(96,0,5);         /* 在 x=96、第 0 页显示 5 号字模“控”。 */
	OLED_ShowCHinese(0,2,6);          /* 在 x=0、第 2 页显示 6 号字模“客”。 */
	OLED_ShowCHinese(16,2,7);         /* 在 x=16、第 2 页显示 7 号字模“厅”。 */
}                                     /* userInit() 函数体结束;LED、ADC 等后续课程代码不属于本节,未抄入。 */

/* oled.h:这是本节为解决 uint8_t 未定义而实际补入 OLED 头文件的语句。 */
#include "stdint.h"                  /* 声明标准定宽整数类型 uint8_t、uint32_t;这不是用于 sprintf() 的 stdio.h。 */

源码范围说明:以上严格保留本节课在 gizwits_product.c 和 oled.h 中实际出现的语句,只增加逐行注释;未把后面课程才加入的 ADC、光照、烟雾判断、蜂鸣器、LED、ZigBee 和窗帘代码倒灌进来。oled.c/.h、oledfont.h 与 dht11.c/.h 是本节从课程既有工程原样迁移的驱动和字模文件,正文解释其接口和关键数据,不把整份库代码重复冒充本节业务代码。顶部两条温湿度赋值虽然语义写反,但这是课程配套源码的原样记录,因此没有暗改;其后果会在 2.1 中专门说明。

一、物联网项目代码编写

本节在上一课生成的机智云/CubeMX 工程上加入 I²C OLED 与 DHT11 驱动,先完成本地界面:首行显示“智能家居中控”,第二行左侧显示“客厅”,右侧显示温湿度。课程结尾只开始接入烟雾数字量,尚未完成报警和蜂鸣器控制。

整体调用关系如下:

CubeMX 生成 I2C1 初始化 ──► main() 调用 MX_I2C1_Init()
                                      │
                                      ▼
机智云 userInit() ──► OLED_Init/Clear ──► 固定汉字标题

机智云 userHandle() ──► DHT_Read() ──► Data[5]
          │                              │
          ├─► 写入 currentDataPoint ◄────┘
          ├─► sprintf() 生成一行文本 ──► OLED_ShowString()
          └─► HAL_GPIO_ReadPin() 读取 smoke 原始电平

1.1 任务信息

本节接线至少涉及电源、I²C、串口和传感器数字信号,不能只凭模块名字盲接:

模块/信号课程工程或原理图连接初学者要特别确认
OLEDPB6/I2C1_SCL、PB7/I2C1_SDA;课程板 OLED 插座的 VCC 标为 5 V只适用于这块板与相应 OLED 模块;换模块要重新核对允许电压和板载上拉电阻接到了几伏
DHT11课程驱动使用 PA5,并借助 TIM1 产生微秒延时数据线、电源和 GND 都要接对;本节没有独立的 DHT_Init()
烟雾数字输出PA4/SMOKE这里只读 0/1,不能得到真实烟雾浓度;报警有效电平还需查模块或实测
ZigBee 串口模块STM32 TX 接模块 RX,STM32 RX 接模块 TX,并共地转录中的“B 模块/蓝牙”实际是 ZigBee;TX 与 RX 要交叉
ESP8266本课程开发板插座 VCC 为 3.3 V不得把 5 V 直接送到本板插座或裸 ESP8266 的 3.3 V 电源脚

视频中“8266 接 5 V,让电流大一些”的口述不能脱离具体载板照做。只有明确标有 5 V 输入、且板上自带 3.3 V 稳压器的成品 ESP8266 开发板,才可能从该 5 V 输入端供电;课程原理图里的 ESP8266 插座直接接 3V3。供电能力不足应换用电流余量足够、去耦完善的稳定 3.3 V 电源,而不是提高裸模组供电电压。

I²C 的 SCL、SDA 是开漏信号,需要上拉电阻才能形成高电平。课程原理图没有在 OLED 插座附近明确画出独立上拉电阻,常见 OLED 小板可能自带上拉,但不能凭经验假定。使用不同 OLED 模块时,应检查模块原理图或测量板上电阻,并确认上拉电压不会超过 STM32 引脚允许范围。所有模块必须共地,否则“高/低电平”没有共同参考。

1.2 STM32CubeMX配置

上一课遗漏了 OLED 所需的 I²C,因此本节重新打开 .ioc,启用 I2C1 后生成代码并让 Keil 工程重新载入。完成工程可核对到以下配置:

项目配置含义
外设I2C1由 hi2c1 句柄供 OLED 驱动使用
SCLPB6I²C 时钟线,复用开漏输出
SDAPB7I²C 数据线,复用开漏输出
Clock Speed100000 Hz标准模式 100 kHz
Addressing Mode7-bit使用 7 位从地址

CubeMX 生成后,main() 会在进入 userInit() 之前调用 MX_I2C1_Init()。这个先后顺序很重要:若先执行 OLED_Init(),hi2c1 及 GPIO 尚未配置,屏幕发送就会失败。

课程 OLED 驱动把地址参数写为 0x78。在 STM32 HAL 这套调用约定中,它是 7 位地址 0x3C 左移一位后的形式,即 0x3C << 1 = 0x78;不能据此误判屏幕有一个不同的“7 位 0x78 地址”。驱动发送命令时先放控制字节 0x00,发送显示数据时先放 0x40。

本工程仍沿用平台模板的 STM32F103C8Tx 配置,而课程开发板原理图为 STM32F103ZET6。I²C1 的 PB6/PB7 在本节能够对应,但模板芯片、封装、Flash 容量和下载算法并未因此自动变成 ZE;正式项目仍应按实际芯片重新核对,不能只看屏幕已经点亮就认为型号配置完全正确。

1.3 初始化屏幕

课程选择先让 OLED 工作,是因为显示结果直观:I²C、驱动、字模和坐标只要有一项错误,屏幕就能很快暴露问题。移植顺序应是“复制文件 → 加入构建 → 配好 I²C → 解决类型声明 → 初始化 → 烧写验证”,而不是只把文件放进磁盘目录就认为工程已经引用。

1.3.1 粘贴头文件

课程从既有 CubeIDE/13IIC 示例中复制 OLED 驱动,文件安排为:

文件放入本工程作用
oled.cSrc/OLED 初始化、写命令/数据、清屏和显示接口实现
oled.hInc/OLED 对外函数、宏和类型声明
oledfont.hInc/ASCII 与 16×16 汉字点阵数据
bmp.h可选图片点阵;本节业务代码没有使用,缺少时不应为“凑齐文件”强行加入

复制驱动只是在复用已有源码,并不会自动让 Keil 编译 oled.c。头文件能被 #include 找到,也不等于其对应 .c 已被链接。

1.3.2 添加c文件

在 Keil 工程树中对 APP 分组执行 Add Existing Files to Group,选择刚复制的 oled.c;之后 DHT11 的 dht11.c 也要用相同方式加入。APP 是 Keil 的逻辑分组,不一定等同于磁盘上的真实目录。

常见现象可以这样区分:

  • 找不到 oled.h:通常是文件未放入包含路径,或 include path 配置不对。
  • 能编译调用语句但链接时报 undefined symbol OLED_Init:通常是 oled.c 没有加入当前 Target,或被排除编译。
  • 文件加到了另一个同名工程:当前工程仍不会得到实现,所以课程反复强调核对工程位置。
1.3.3 初始化屏幕代码

最初可以在 main.c 的 CubeMX 用户代码区测试 OLED,但课程随后把实际业务初始化移动到 gizwits_product.c 的 userInit() 中。两条规则要同时满足:

  1. CubeMX 管理的文件中,自写内容放进匹配的 /* USER CODE BEGIN ... */ 与 /* USER CODE END ... */ 之间,避免再次 Generate Code 时被覆盖。
  2. 面向机智云业务的 OLED 初始化最终只保留一处,由框架的 userInit() 统一执行,避免重复清屏和难以判断调用顺序。

复制的 oled.c 内部直接调用阻塞式 HAL_I2C_Master_Transmit(),并且几乎每写一个字节就调用一次。这样实现简单,适合课程验证,但刷新效率不高;源代码也没有检查 HAL 返回值,所以屏幕不亮时不会自动给出“地址错误”或“总线超时”的业务提示,需要结合调试器、逻辑分析仪或 I²C 扫描逐项排查。

1.3.3.1 编译错误

视频中的报错实质是 oled.h 使用了 uint8_t、uint32_t,却没有包含声明这些定宽整数类型的头文件。本节实际修复是在 oled.h 中加入:

#include "stdint.h" /* 声明 uint8_t、uint32_t 等标准定宽整数类型。 */

这里应区分两个相似名字:

  • stdint.h:提供 uint8_t 等整数类型,解决本次类型未定义错误。
  • stdio.h:提供 sprintf() 等标准输入输出函数声明,gizwits_product.c 原本已经包含它。

因此转录中的“加 stdio 修复 u8”是听写混淆。修复编译错误时应看第一条真实错误及报错文件,而不是根据红线数量随意添加头文件。

1.3.3.2 烧写屏幕

课程把 Keil 调试器切换为 ST-Link,并在下载设置里启用 Reset and Run,使下载完成后目标板自动复位运行;视频还在 Pack/调试描述相关设置中取消了 Enable。不同 Keil、Device Pack 和调试器固件的界面文字可能略有差异,复现时以“当前 ST-Link 能识别芯片、下载算法匹配、下载后复位并从用户程序运行”为验证标准。

若下载成功但 OLED 不亮,建议按层次判断:

  1. Keil 是否真的使用 ST-Link,且能读到目标器件。
  2. Flash 下载算法与实际 STM32 型号/容量是否匹配。
  3. Reset and Run、复位方式和 NRST 接线是否正常。
  4. 程序是否已执行到 MX_I2C1_Init() 与 OLED_Init()。
  5. OLED 电源、共地、SCL/SDA、地址和上拉是否正确。

不要把“下载后没自动亮”全部归因于复位选项;下载失败、应用 HardFault、接线错误和 I²C 无应答也会表现为黑屏。

1.3.3.3 改字模

课程使用取模软件生成“智能家居中控客厅”的 16×16 汉字点阵,并选择能输出 C51/C 数组形式的模式。这里的“C51”只决定导出文本格式,不表示 STM32 在运行 51 指令;字模本质上仍是一串描述亮灭像素的字节。

一个 16×16 单色汉字有 256 个像素,每像素 1 bit,因此通常占:

$ 16\times16\div8=32\ \text{bytes} $

但这 32 字节如何对应上下半区、行列和位序,必须与 OLED_ShowCHinese() 的读取方式一致。若取模方式不匹配,数据可以正常编译,屏幕却会显示镜像、旋转或乱码。

1.3.3.4 粘贴字模文件

生成的数据粘贴到 oledfont.h 底部的汉字表。课程最终索引关系为:

0    1    2    3    4    5    6    7
智   能   家   居   中   控   客   厅

本驱动的 OLED_ShowCHinese(x, y, no) 会用 Hzk[2 * no] 和 Hzk[2 * no + 1] 取出一个汉字的上下两组 16 字节数据,所以表中每个汉字占两行数组。索引与点阵的排列次序必须同步;只改显示函数中的编号而不改字模表,显示的就会是另一个字。

字模数组属于复制进来的显示资源,顶部代码没有把几十行十六进制数据再次展开。复现时应原样使用课程文件,不能把网上不同扫描方向的字库数据随意混入。

1.3.3.5 修改字模内容并编译

字模文件改变后必须保存并重新编译,因为点阵数据会被编译进 MCU 的 Flash。若只改了取模软件窗口、没有把输出替换到 oledfont.h,或者烧写的仍是旧构建产物,屏幕不会变化。

可用三步确认修改真正生效:

  1. 在 oledfont.h 里核对新字节和索引注释已经保存。
  2. Rebuild 后确认构建时间和 0 Error(s),不要只看编辑器内容。
  3. 下载最新生成的程序,再观察汉字是否与索引一致。
1.3.3.6 修改字模内容并烧写

课程屏幕横向为 128 像素,首行 6 个 16 像素宽汉字共占 96 像素。居中时左侧起点为:

$ x_0=\frac{128-6\times16}{2}=16 $

所以六个字的 x 坐标依次为 16、32、48、64、80、96。“客厅”放到下一行左侧,坐标为 (0,2) 与 (16,2)。

这里的 y 不是普通像素坐标,而是 OLED 的 8 像素高页地址。16×16 汉字会占连续两页,因此四行 16 像素高内容的起始 y 应为 0、2、4、6,而不是 0、16、32、48。这也解释了课程为何把“客厅”的 y 从 0 改成 2。

1.3.3.7 粘贴温湿度传感器代码

课程继续从既有温湿度示例复制:

  • dht11.c 放入 Src/,并加入 Keil 当前 Target 的 APP 分组;
  • dht11.h 放入 Inc/;
  • gizwits_product.c 包含 dht11.h 后调用 DHT_Read()。

DHT11 驱动使用 PA5 的时序切换以及 TIM1 微秒延时,依赖上一课已经生成的 GPIOA 和 TIM1 初始化。它没有单独的 DHT_Init(),不代表传感器“不需要任何底层条件”。

1.3.3.8 添加私有文件

原笔记中的“私有文件”应理解为把项目自己的驱动文件纳入当前工程,而不是 C 语言的一种特殊文件类型。实际要同时满足:

磁盘中存在 .c/.h
   + Keil 编译当前 .c
   + 编译器能搜索到 .h
   + 业务文件正确 #include
   = 接口可被正常调用和链接

若后续再次用 CubeMX 生成代码,自建驱动文件通常不会被删除,但 Keil 工程分组和包含路径仍要复核;尤其不要在多个相似的“物联网32”目录之间复制后误编译旧版本。

1.3.3.9 产品文件夹头文件包装

gizwits_product.c 是机智云生成框架留给产品业务对接的文件。本节在其中包含 dht11.h 和 oled.h,让 userInit()、userHandle() 能直接调用两套驱动。

头文件负责“声明接口”,.c 文件负责“实现接口”。#include "oled.h" 并不会把 oled.c 自动编译进工程;反过来,只加入 oled.c 却不包含声明,也会造成函数原型缺失。课程口述的“包装”就是用头文件把实现的对外接口集中暴露给产品代码。

1.3.3.10 userInit函数位置问题

机智云框架在底层外设初始化完成后调用 userInit(),因此课程把 OLED 初始化和固定标题都移入这个函数。关键是运行时调用顺序,不是 userInit() 在源文件文本中必须排在另一个函数的“上面”。

本节正确职责划分为:

  • userInit():只执行一次,清零数据点、初始化 OLED、清屏并显示固定标题。
  • userHandle():周期执行,读取 DHT11、烟雾引脚并刷新变化数据。

原转录把 DHT_Read() 说成放进 userInit(),与视频成品代码不符。若只在初始化阶段读一次,之后显示值就不会继续随环境变化。

完成工程的主循环紧接着调用 userHandle() 和 gizwitsHandle(),中间没有采样周期控制;而课程 DHT_Read() 每次启动通信时仅 HAL_Delay(18) 就会阻塞至少约 18 ms。也就是说,当前写法会频繁读取并占住前台循环,机智云协议处理也只能等它返回。实际产品应按传感器手册设置采样周期,并把“到期采样”与“每圈处理通信”分开;本节没有实现调度,所以顶部不补写定时器或延时逻辑。

1.3.3.11 复制数据结构体数组名称

dht11.h 暴露 extern uint8_t Data[5];,DHT11 一帧有效数据的课程驱动排布为:

下标含义
Data[0]湿度整数部分
Data[1]湿度小数部分
Data[2]温度整数部分
Data[3]温度小数部分
Data[4]校验和

顶部原代码却把 Data[0] 赋给 valuetemp1,把 Data[2] 赋给 valuehumi1。本精教版依照要求原样保留,不能为了“看起来正确”暗中交换;但学习时必须知道,变量名和真实物理量已经错位。

此外,课程直接忽略了 DHT_Read() 的返回值。读取或校验失败时,应用仍可能继续使用旧值或无效值。可靠产品应先判断成功状态再更新数据点,不过这属于改进方向,不是本节已经写出的源码,因此没有补进顶部代码。

1.3.3.12 屏幕显示字符串接口的调用

“客厅”两个 16×16 汉字覆盖 x=0~31,课程把温湿度字符串放在 x=35,是给汉字右侧留出约 3 像素间隔;y=2 与“客厅”位于同一 16 像素高行。

格式串必须准确理解:

"H:%2d%%T:%2dC "
   │  │    │ └─ C 只是摄氏度文字标记,没有显示“°”符号
   │  │    └── %2d:最小字段宽度 2,短数值左侧补空格,并不限制最多两位
   │  └─────── %%:输出一个字面量百分号
   └────────── %2d:同样以至少两个字符显示整数

原笔记写成 %02d 不准确:%02d 才会补前导零,例如 08;课程实际 %2d 显示空格加 8。末尾空格可在新文本比旧文本短时帮助覆盖残留字符。

OLED_ShowString(..., 16) 使用约 8×16 的 ASCII 字模,每显示一个字符 x 增加 8。课程缓冲区 30 字节足以容纳预期的小整数文本,但 sprintf() 本身没有长度上限;同时机智云数据点字段是 uint32_t,用 %d 严格来说也与无符号类型不匹配。更健壮的写法应考虑 snprintf()、缓冲区大小和 %lu/显式类型转换,但为遵守“原样抄源码”,顶部没有擅自改写。

二、物联网项目

2.1 温室度和湿度检测

本标题原文中的“温室度”应理解为“温度”,但为保持原有标题结构没有改名。视频通过向 DHT11 哈气观察数值变化:呼气含有较多水汽,湿度通常会明显上升;温度可能随传感器初温、呼气距离、气流和响应速度变化,不能把“温度一定下降”当作判定规则。

本节出现了一个容易被屏幕现象掩盖的“双重错位”:

Data[0](真实湿度) ──► valuetemp1 ──► 格式串的 H 位置
Data[2](真实温度) ──► valuehumi1 ──► 格式串的 T 位置

因此 OLED 上的 H、T 数字碰巧看起来正确,但机智云上报时会按字段名解释:temp1 实际携带湿度,humi1 实际携带温度。只看本地屏幕不能证明数据映射正确。

概念上的正确对应应当是 valuetemp1 ← Data[2]、valuehumi1 ← Data[0],然后格式化时让 H 使用 valuehumi1、T 使用 valuetemp1。这里仅说明正确关系,没有替换顶部课程源码;后续若课程专门修复,应在对应课次记录实际修改。

2.2 初始化下一个数据

课程结尾只完成了烟雾数据点的第一步:

currentDataPoint.valuesmoke = HAL_GPIO_ReadPin(SMOKE_GPIO_Port, SMOKE_Pin); /* 读取 PA4/SMOKE 当前数字电平并保存。 */

它得到的是 GPIO_PIN_RESET 或 GPIO_PIN_SET 对应的原始逻辑状态,不是烟雾浓度。究竟高电平还是低电平代表报警,要根据所用烟雾模块比较器输出电路、可调阈值和实测结果确认,不能看到变量名 smoke 就直接认定 1 为有烟。

视频末尾开始口述要用 if 判断并联动蜂鸣器,但在本课结束前没有完成相关代码。故本精教版不从后续最终工程补入烟雾提示、蜂鸣器动作或有效电平假设;这些应留给真正实现它们的后续课次。

参考资料