物联网项目32代码(1)
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*)¤tDataPoint, 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、串口和传感器数字信号,不能只凭模块名字盲接:
| 模块/信号 | 课程工程或原理图连接 | 初学者要特别确认 |
|---|---|---|
| OLED | PB6/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 驱动使用 |
| SCL | PB6 | I²C 时钟线,复用开漏输出 |
| SDA | PB7 | I²C 数据线,复用开漏输出 |
| Clock Speed | 100000 Hz | 标准模式 100 kHz |
| Addressing Mode | 7-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.c | Src/ | OLED 初始化、写命令/数据、清屏和显示接口实现 |
oled.h | Inc/ | OLED 对外函数、宏和类型声明 |
oledfont.h | Inc/ | 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() 中。两条规则要同时满足:
- CubeMX 管理的文件中,自写内容放进匹配的
/* USER CODE BEGIN ... */与/* USER CODE END ... */之间,避免再次 Generate Code 时被覆盖。 - 面向机智云业务的 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 不亮,建议按层次判断:
- Keil 是否真的使用 ST-Link,且能读到目标器件。
- Flash 下载算法与实际 STM32 型号/容量是否匹配。
Reset and Run、复位方式和 NRST 接线是否正常。- 程序是否已执行到
MX_I2C1_Init()与OLED_Init()。 - 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,或者烧写的仍是旧构建产物,屏幕不会变化。
可用三步确认修改真正生效:
- 在
oledfont.h里核对新字节和索引注释已经保存。 - Rebuild 后确认构建时间和
0 Error(s),不要只看编辑器内容。 - 下载最新生成的程序,再观察汉字是否与索引一致。
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 判断并联动蜂鸣器,但在本课结束前没有完成相关代码。故本精教版不从后续最终工程补入烟雾提示、蜂鸣器动作或有效电平假设;这些应留给真正实现它们的后续课次。
参考资料
- 本地课程配套 STM32F103ZE 开发板原理图:核对 OLED 的 PB6/PB7 与 5 V 插座、ESP8266 插座的 3.3 V 供电以及板上电源关系。
- 本节落地工程中的 gizwits_product.c、oled.c、oled.h、oledfont.h、dht11.c 与 STM32F103C8x.ioc:核对本节源码、字模索引、DHT11 数据排布和 I²C1 配置;最终工程含后续课程逻辑,正文只截取本节边界内的内容。
- ST:STM32F103x8/xB 官方文档页:核对 STM32F103 外设与数据手册。
- Solomon Systech 产品检索页:可查到 SSD1306 的 128×64 单色 OLED 驱动规格;课程未展示实物模组芯片丝印,因此不能只凭相似命令断定模组一定采用该芯片,实际电压、地址和上拉仍以所用模块资料为准。