物联网项目32代码(2)

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

172.7物联网项目32代码(2)

/* gizwits_product.c:以下按文件中的位置摘录本节新增或实际讲解的业务代码片段,不补入下一课的串口标志位。 */
#include "adc.h"                     /* 引入 CubeMX 生成的 ADC1 句柄与初始化接口声明,使本文件能使用 hadc1。 */
uint32_t ad_value;                    /* 保存 ADC1 单通道转换得到的 12 位原始码,正常范围为 0~4095。 */

/* gizwitsEventProcess() 中机智云生成框架已有的 EVENT_led1 分支,以及本节填入的实体灯与 OLED 操作。 */
case EVENT_led1:                      /* App 下发可写 led1 数据点时,机智云协议层进入这个事件分支。 */
    currentDataPoint.valueled1 = dataPointPtr->valueled1; /* 把本次下发的布尔值保存到设备当前数据点结构体。 */
    GIZWITS_LOG("Evt: EVENT_led1 %d \n", currentDataPoint.valueled1); /* 通过调试日志打印本次 led1 事件值。 */
    if(0x01 == currentDataPoint.valueled1) /* 判断 App 下发值是否为 1;课程把 1 定义为开灯。 */
    {                                    /* “开灯”分支开始。 */
		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET); /* 将 PB5 拉低;课程板载 LED 是低电平点亮。 */
		OLED_ShowString(0,6,"LED:ON ",16); /* 在第 4 个 16 像素行显示开灯状态,末尾空格用于覆盖旧字符串残留的 F。 */
		//user handle;这是生成框架原有的占位注释,本节没有在此调用 userHandle()。
    }                                    /* “开灯”分支结束。 */
    else                                 /* 下发值不是 1 时进入关灯分支。 */
    {                                    /* “关灯”分支开始。 */
		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET); /* 将 PB5 置高,切断板载 LED 的灌电流回路而熄灯。 */
		OLED_ShowString(0,6,"LED:OFF",16); /* 在同一位置显示关灯状态;该字符串比 LED:ON 多一个可见字符。 */
		//user handle;保留生成包原有注释,实际实体灯动作已经由上面的 GPIO 写操作完成。
    }                                    /* “关灯”分支结束。 */
    break;                               /* 结束 EVENT_led1 分支,防止继续落入下一个事件 case。 */

/* userHandle() 中本节新增的 ADC 采集、烟雾判断及屏幕显示语句。 */
HAL_ADC_Start(&hadc1);                   /* 软件启动 ADC1 的一次规则通道转换。 */
HAL_ADC_PollForConversion(&hadc1,10);    /* 最多轮询等待 10 ms;课程源码没有检查等待是否成功。 */
ad_value=HAL_ADC_GetValue(&hadc1);       /* 读取 ADC1 数据寄存器,把 PA1/ADC1_IN1 的转换码保存到 ad_value。 */
currentDataPoint.valuesmoke = HAL_GPIO_ReadPin(SMOKE_GPIO_Port, SMOKE_Pin); /* 把 PA4 上的原始高低电平直接写入 smoke 数据点。 */
currentDataPoint.valuesun =100 - ad_value/4095.0*100; /* 把 0~4095 反向映射到约 100%~0%,浮点结果再写入整数数据点。 */

if(HAL_GPIO_ReadPin(SMOKE_GPIO_Port, SMOKE_Pin)) /* 再读一次 PA4;非零即 GPIO_PIN_SET,课程实测将其解释为“无烟”。 */
{                                              /* PA4 为高电平的分支开始。 */
	OLED_ShowString(0,4,"Smog:N",16);          /* 在第 3 个 16 像素行左侧显示 N,表示课程当前模块未触发烟雾阈值。 */
	HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET); /* 将 PB8 拉低,关闭高电平驱动的板载蜂鸣器。 */
}else{                                         /* PA4 为低电平时,进入课程实测的“检测到烟雾”分支。 */
	OLED_ShowString(0,4,"Smog:Y",16);          /* 在相同位置显示 Y,表示模块比较器已经触发。 */
	HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_SET); /* 将 PB8 置高,经三极管驱动板载有源蜂鸣器发声。 */
}                                              /* 烟雾状态判断结束。 */

sprintf(OLED_BUFF,"Sun:%2d%%",currentDataPoint.valuesun); /* 把相对光照百分比写入 OLED 缓冲区;%% 输出字面量百分号。 */
OLED_ShowString(64,4,OLED_BUFF,16);             /* 从屏幕右半边 x=64、第 3 行显示光照文本。 */

/* userInit() 中本节新增的一次性 ADC 校准和 LED 初始状态显示。 */
OLED_ShowString(0,6,"LED:OFF",16);             /* 上电初始化时先显示 LED 关闭,与 PB5 默认高电平相对应。 */
HAL_ADCEx_Calibration_Start(&hadc1);            /* ADC1 初始化完成后执行一次自校准;课程源码没有检查返回状态。 */

源码范围说明:顶部只收录 172 视频实际讲解并写入的应用代码。case EVENT_led1 外层的机智云事件函数、CubeMX 自动生成的 ADC/GPIO 初始化和 HAL 库实现没有整段抄入;171 已经讲过的 DHT11/OLED 标题代码也不重复。课程完成工程后来把烟雾 if 整段注释掉,但本节视频结尾确实用这段逻辑完成了 OLED 与蜂鸣器测试,因此这里按本节画面保留为可执行语句。最终工程中的 led_on_cmd、led_off_cmd 等变量属于 173 的 ZigBee 串口交互,本节没有提前补入。

一、物联网项目编程控制

本节补齐 STM32 本地端的三组功能:

烟雾模块 DO ──► PA4 数字输入 ──► OLED 状态 + PB8 蜂鸣器 + 机智云 smoke 数据点
光敏模块 AO ──► PA1/ADC1_IN1 ──► 12 位 ADC ──► 相对百分比 + OLED + sun 数据点
手机 App led1 ──► 机智云 EVENT_led1 ──► PB5 板载 LED + OLED 状态

视频最后验证了本地温湿度、烟雾、相对光照和第一路灯。与 51 单片机之间的 ZigBee/USART3 指令收发尚未编写,留到下一节。

1.1 编程控制温室项目

原标题中的“温室项目”可理解为此处要实现的环境监测与控制部分。本节既有周期采集,也有事件驱动控制,两类代码位置不同:

  • DHT11、ADC 和烟雾输入属于设备主动采集,写在循环执行的 userHandle()。
  • App 下发的 LED 命令属于机智云事件,写在 gizwitsEventProcess() 的 EVENT_led1 分支,不是写在 userHandle()。
  • ADC 校准、OLED 初始状态等只需执行一次的动作放在 userInit()。
1.1.1 编程控制温室项目代码分析

1. HAL_GPIO_ReadPin() 的返回值

该函数返回类型是 GPIO_PinState,枚举中包含 GPIO_PIN_RESET 和 GPIO_PIN_SET。课程可把它们理解为逻辑 0 和 1,但更严谨的比较方式仍是直接与枚举常量比较,而不是假设所有平台都用任意整数表达:

if (HAL_GPIO_ReadPin(SMOKE_GPIO_Port, SMOKE_Pin) == GPIO_PIN_SET) /* 明确判断输入引脚当前是否为高电平。 */

顶部保持课程原代码 if(HAL_GPIO_ReadPin(...)),没有擅自改写。该判断只说明引脚是高还是低;“有烟/无烟”的业务含义还取决于模块输出有效电平。

2. 本课烟雾模块的实测逻辑

视频先承认不确定高低电平含义,再通过调节模块电位器跨过比较器阈值进行验证,最终代码采用:

PA4/SMOKE本课 OLEDPB8/BEEP本课解释
高电平Smog:N低,蜂鸣器关闭未检测到烟雾
低电平Smog:Y高,蜂鸣器发声检测到烟雾/比较器触发

这个表是课程当前模块和阈值下的测试结果,不是所有烟雾模块的统一规则。电位器只是在改变比较器阈值,拧到触发状态只是模拟数字输出翻转,并不等于用真实、已知浓度的烟气完成了传感器校准。

还有一个语义细节:源码把 PA4 原始状态直接赋给 currentDataPoint.valuesmoke。按上表,1 实际表示“无烟”、0 表示“触发”。若机智云布尔数据点约定 1=有烟,云端含义就会反;只有平台显示规则也按 1=无烟、0=有烟 配置时才一致。业务代码最好把“原始电平”先转换成含义明确的告警布尔值,但本节没有这样改,所以顶部仍保留原赋值。

3. 板载蜂鸣器为什么是高电平响

课程开发板原理图中,蜂鸣器正端接 3.3 V,负端接 S8050 NPN 三极管 Q3 的集电极;PB8 的 BEEP 信号通过 1 kΩ 电阻 R27 驱动基极,R28(10 kΩ)把基极下拉:

3.3 V ──► 有源蜂鸣器 ──► Q3 集电极
                              Q3 发射极 ──► GND
PB8/BEEP ── R27 1 kΩ ──► Q3 基极
                         │
                      R28 10 kΩ
                         │
                        GND
  • PB8 为高电平:产生基极电流,Q3 导通,蜂鸣器电流回路闭合,蜂鸣器响。
  • PB8 为低电平:Q3 截止,蜂鸣器没有完整回路,蜂鸣器不响。
  • 三极管承担蜂鸣器电流,避免由 STM32 GPIO 直接驱动负载;R27 限制基极电流,R28 保证 MCU 复位或引脚悬空时 Q3 倾向于关闭。

因此视频前段的“先随便写一个,错了再换”只是调试过程;有原理图时可以在烧写前确定板载蜂鸣器为高电平开启。CubeMX 最终把 PB8 配成推挽输出,初始写 GPIO_PIN_RESET,所以上电默认静音。

4. 烟雾模块供电不能只看它输出数字量

视频说烟雾模块可优先接 5 V,因为读取的是高低电平。这个理由并不充分:模块供电为 5 V 时,数字输出高电平也可能接近 5 V,是否能直连 3.3 V STM32 必须查模块的输出级和上拉电压。课程仓库没有该烟雾模块的原理图或型号资料,因此精教版不能凭外观断言其 5 V 数字输出一定安全。

安全做法是先测量/查明:

  1. 模块允许的供电电压和加热器电流;
  2. DO 是推挽输出还是开集电极输出;
  3. 高电平由什么电压上拉;
  4. 接入 PA4 后的高电平是否落在 STM32 允许范围内。

若 DO 可能输出 5 V,应使用合适的分压或电平转换,或在确认模块允许的前提下以 3.3 V 供电。不能用“它只有 0 和 1”代替电压兼容性检查。

1.1.2 物联网项目思维导图分析

本节光敏模块使用 AO 模拟输出,而不是 DO 比较器输出。信号链为:

环境光变化
   ↓
光敏电阻阻值变化
   ↓
模块分压点 AO 电压变化
   ↓
PA1 / ADC1_IN1
   ↓
12 位 ADC 码 0~4095
   ↓
反向换算为 0~100 的相对百分比

课程配套的四线光敏模块资料说明:VCC 可用 3.3~5 V,模块同时提供 DO 和 AO;电位器调整的是 LM393 比较器的数字阈值,主要影响 DO。本项目读取的是 AO,因此 ADC 得到连续变化的分压电压。

根据模块电路图,光敏电阻在 AO 分压点与 GND 之间。光越强,光敏电阻阻值越小,AO 越被拉向 GND,所以原始 ADC 码反而越小;视频实测“手机灯照射后数值下降”与这张电路图一致。课程最终使用 100 - 原始比例,让显示值随光增强而增大。

ADC 配置与调用顺序

完成工程的 CubeMX 配置为:

项目值
输入PA1 / ADC1_IN1
分辨率12 bit,正常结果 0~4095
规则通道数1
连续转换Disable
触发Software Start
数据对齐Right
采样时间239.5 ADC cycles

因为连续转换关闭,所以 userHandle() 每次都要依次执行 HAL_ADC_Start()、HAL_ADC_PollForConversion()、HAL_ADC_GetValue()。HAL_ADCEx_Calibration_Start() 只需在 ADC 初始化完成后执行一次;main.c 的实际顺序是 MX_ADC1_Init() 在前、userInit() 在后,满足这个前提。

课程代码忽略了 Start、Poll 和 Calibration 的返回状态。如果轮询超时,随后仍读取数据寄存器,可能使用旧值。可靠程序应只在转换完成时更新数据点,但这属于后续健壮性改进,未添加到顶部原代码。

模拟输入电压安全

STM32F103ZET6 数据手册规定 ADC 转换输入范围为 0 V~VREF+,而本板 VREF+/VDDA 约为 3.3 V。光敏模块若由 5 V 供电,AO 分压点可能进入接近 5 V 的范围,超出正常 ADC 转换范围;因此课程要求在读取 AO 时给模块接 3.3 V 是正确且必要的。

模块说明书写“工作电压 3.3~5 V”,只代表模块本身可在这个范围工作,不代表它的 AO 在任何供电下都能安全接入任意 MCU。模块供电要求与 MCU 输入范围是两项必须同时满足的条件。

1.1.3 温室项目温室部分代码

1. 4095 与 4096 不是完全相同

12 位 ADC 一共有 $2^{12}=4096$ 个离散码,码值从 0 到 4095。若要求最大码 4095 显示为 100%,课程采用:

$ P_{raw}=\frac{ADC}{4095.0}\times100 $

若除以 4096.0,最大值会得到约 99.976%,而不是精确 100%。两者差异很小,但含义不同,不能简单说“都一样”。课程最后选择 4095.0,并用浮点常量避免整数除法先把绝大多数结果截成 0。

由于实物电路强光时 ADC 值变小,课程显示值为:

$ P_{sun}=100-\frac{ADC}{4095.0}\times100 $

valuesun 是 uint32_t,赋值时浮点小数部分会被截去。例如计算结果 63.8 最终保存为 63。这个数只是当前模块、供电和分压关系下的相对比例,不是经过标定的照度 lux,也不能跨不同光敏电阻直接比较绝对光强。

2. OLED 第三行布局

课程把同一行分成左右两块:

x=0                                  x=64                                  x=127
┌────────────────────────────────────┬────────────────────────────────────────┐
│ Smog:N / Smog:Y                    │ Sun:xx%                                │ y=4
└────────────────────────────────────┴────────────────────────────────────────┘

y=4 是 OLED 页地址,第 3 个 16 像素高显示行;x=64 是屏幕横向中点,不等于把字符串自身居中。

格式串 "Sun:%2d%%" 的 %2d 只是最小宽度 2:0~9 会补一个前导空格,10~99 占两位,100 会占三位。于是 Sun:100% 有 8 个字符,正好占满右侧 64 像素;数值从 100 变回两位数时,新字符串只有 7 个字符,末端可能残留旧像素。课程只处理了 LED 的长短字符串残留,没有处理这里;更稳妥的办法是固定宽度、先清局部区域或追加合适覆盖字符,但这些改法没有写进本节源码。

3. 烟雾状态与蜂鸣器

视频用电位器人为改变比较器阈值,观察到模块指示灯翻转、OLED 从 N/Y 切换并伴随蜂鸣器开关。这验证的是“数字比较器输出→GPIO→显示/蜂鸣器”的链路,不代表已经完成对真实烟雾浓度、响应时间和报警阈值的标定。

HAL_GPIO_ReadPin() 在本函数中被调用两次:一次赋给云端数据点,一次进入 if。如果信号恰在两次读取之间变化,两个结果理论上可能不一致。课程当前轮询场景一般不明显,但工程化实现可先读入一个局部变量,再统一用于上报、显示和报警;本节按源码原样保留,没有擅自重构。

1.1.4 温室项目LED部分代码

1. 控制代码应放在事件分支

led1 在机智云平台被定义为可写数据点,所以协议生成器创建 EVENT_led1。App 下发后,协议层解析数据并调用 gizwitsEventProcess(),本节只需在生成器预留的 case 中补充硬件动作:

App 开关
   ↓
ESP8266 / 机智云协议
   ↓
EVENT_led1
   ↓
currentDataPoint.valueled1
   ├─ 1:PB5=RESET → 灯亮 → OLED 显示 LED:ON
   └─ 0:PB5=SET   → 灯灭 → OLED 显示 LED:OFF

原笔记称其位于 userHandle(),不准确。userHandle() 是持续采集入口,EVENT_led1 是收到下发事件时才执行的控制入口;源码中的 //user handle 只是机智云模板留的注释文字,并不表示这里自动调用了同名函数。

2. 为什么 PB5 低电平点亮

开发板原理图中,PB5 连接网络 LED0,LED 和 1 kΩ 限流电阻的另一端接 3.3 V:

3.3 V ──► LED ──► 1 kΩ ──► PB5

PB5 输出低电平时,电流从 3.3 V 经 LED 和电阻流入 PB5,LED 点亮;输出高电平时两端压差不足,LED 熄灭。CubeMX 的用户标签把 PB5 命名为 LED1,所以代码里的 LED1_Pin 实际控制原理图网络名 LED0,不要被两个命名层级混淆。

CubeMX 初始化先把 PB5 写为 GPIO_PIN_SET,使灯上电默认熄灭;userInit() 再显示 LED:OFF,硬件状态与初始界面一致。

3. OLED 字符串残留

LED:OFF 有 7 个字符,LED:ON 只有 6 个。如果先显示 OFF,再在同一位置直接写 ON,最后一个 F 不会自动清除,屏幕可能显示成 LED:ONF。课程源码给较短文本写成 "LED:ON ",用尾部空格覆盖旧字符。这是局部刷新时常用的最小修复。

4. 为什么第二盏灯不能直接照搬

视频发现 led2 没有对应事件,并承认平台读写类型配置错了。它至少有两个独立问题:

  1. 机智云平台中的 led2 是只读数据点,因此生成代码没有 EVENT_led2;要接受 App 命令,必须先改为可写并重新生成/合并协议代码。
  2. 真实 ZE 开发板第二路用户 LED 接 PE5,但当前 CubeMX 工程目标是 LQFP48 的 STM32F103C8Tx,该封装没有 PE5。

所以不能只在 userInit() 中“重新初始化某个 E 口”就算修好。正确做法是按真实 STM32F103ZET6/LQFP144 重建或迁移工程,再配置 PE5,并同步修正机智云 led2 数据点为可写。课程口述的“E2/E8”是记忆不清,原理图给出的确切引脚是 PE5。

1.1.5 功能测试

课程测试结果应分层理解:

测试动作观察结果能证明什么不能证明什么
App 切换客厅灯PB5 灯亮灭,OLED 显示 ON/OFFEVENT_led1、GPIO 和显示链路可工作第二路灯和远端 51 节点也已可控
手机灯照光敏模块显示与 App 中相对光照值变化AO、ADC1 和反向百分比链路可工作数值就是准确 lux
调节烟雾模块电位器指示灯、OLED、蜂鸣器状态变化数字比较器、PA4 和 PB8 链路可工作对真实烟雾浓度已经标定
查看温湿度本地数值随环境变化DHT11 读取链路工作171 中云端 temp1/humi1 字段错位已经修复

视频最后说“除与 51 单片机交互外基本完成”,应限定为 STM32 本地功能已能演示。尚未完成的包括:USART3/ZigBee 命令帧、51 端执行与应答、心跳检测、第二路温湿度/灯、窗帘动作,以及 171 已指出的温湿度云端字段映射问题。

参考资料