文章目录
-
- [一、PWM 到底是什么](#一、PWM 到底是什么)
-
- [1.1 一句话版本](#1.1 一句话版本)
- [1.2 同样是 PWM,两种完全不同的用法](#1.2 同样是 PWM,两种完全不同的用法)
- [1.3 三个必须分清的参数](#1.3 三个必须分清的参数)
- [1.4 为什么风扇不用"开/关",要用 PWM](#1.4 为什么风扇不用"开/关",要用 PWM)
- [二、定时器是怎么"画"出 PWM 的](#二、定时器是怎么"画"出 PWM 的)
-
- [2.1 四个角色](#2.1 四个角色)
- [2.2 PWM mode 1 的规则](#2.2 PWM mode 1 的规则)
- [2.3 三个公式](#2.3 三个公式)
- [2.4 本工程两套参数是怎么来的](#2.4 本工程两套参数是怎么来的)
- [2.5 为什么 CCR 能直接当"微秒"用](#2.5 为什么 CCR 能直接当"微秒"用)
- [2.6 CCR 超过 ARR 会怎样](#2.6 CCR 超过 ARR 会怎样)
- [三、CubeMX 里那几个勾(回看第 4 章)](#三、CubeMX 里那几个勾(回看第 4 章))
-
- [3.1 通道模式要选 `PWM Generation CHx`](#3.1 通道模式要选
PWM Generation CHx) - [3.2 `Pulse = 0` 的意义](#3.2
Pulse = 0的意义) - [3.3 `PWM mode 1` + `Polarity High`](#3.3
PWM mode 1+Polarity High) - [3.4 `Auto-reload preload = Enable`](#3.4
Auto-reload preload = Enable) - [3.5 生成的 `tim.c` 里看什么](#3.5 生成的
tim.c里看什么)
- [3.1 通道模式要选 `PWM Generation CHx`](#3.1 通道模式要选
- [四、启动:`HAL_TIM_PWM_Start` 只调一次](#四、启动:
HAL_TIM_PWM_Start只调一次) -
- [4.1 本工程的四行](#4.1 本工程的四行)
- [4.2 不调用会怎样](#4.2 不调用会怎样)
- [4.3 运行时只改 CCR](#4.3 运行时只改 CCR)
- [4.4 "关设备"用 CCR=0,不要用 `HAL_TIM_PWM_Stop`](#4.4 "关设备"用 CCR=0,不要用
HAL_TIM_PWM_Stop) - [4.5 改 CCR 会不会有延迟](#4.5 改 CCR 会不会有延迟)
- [五、风扇与加热片:占空比 = 功率](#五、风扇与加热片:占空比 = 功率)
-
- [5.1 硬件链路长什么样](#5.1 硬件链路长什么样)
- [5.2 为什么是 800,而不是 1000](#5.2 为什么是 800,而不是 1000)
- [5.3 风扇是感性负载,两个坑](#5.3 风扇是感性负载,两个坑)
- [5.4 加热片是阻性负载,重点在"别烧"](#5.4 加热片是阻性负载,重点在"别烧")
- [5.5 手动模式的按键实现](#5.5 手动模式的按键实现)
- [5.6 为什么"标志位"必须和 CCR 一起改](#5.6 为什么"标志位"必须和 CCR 一起改)
- [六、SG90 舵机:脉宽 = 角度](#六、SG90 舵机:脉宽 = 角度)
-
- [6.1 它内部其实是一个闭环系统](#6.1 它内部其实是一个闭环系统)
- [6.2 为什么是 50Hz / 20ms](#6.2 为什么是 50Hz / 20ms)
- [6.3 标准量程 vs 本机实测量程](#6.3 标准量程 vs 本机实测量程)
- [6.4 为什么这里的 CCR 正好是微秒](#6.4 为什么这里的 CCR 正好是微秒)
- [6.5 用占空比算,其实只是"换个说法"](#6.5 用占空比算,其实只是"换个说法")
- [6.6 供电:这一节能救你的板子](#6.6 供电:这一节能救你的板子)
- [6.7 上电抖一下,是正常的](#6.7 上电抖一下,是正常的)
- [6.8 "两位置法":只走两个端点](#6.8 "两位置法":只走两个端点)
- [6.9 方向反了怎么办](#6.9 方向反了怎么办)
- 七、把执行器接进三模式状态机
-
- [7.1 切模式先全关:安全第一](#7.1 切模式先全关:安全第一)
- [7.2 手动模式:三键三设备](#7.2 手动模式:三键三设备)
- [7.3 自动模式:四个分支](#7.3 自动模式:四个分支)
- [7.4 1℃ 回差为什么必须有](#7.4 1℃ 回差为什么必须有)
- [7.5 为什么"正常温度且空气好"窗帘还是拉开](#7.5 为什么"正常温度且空气好"窗帘还是拉开)
- [7.6 远程模式:命令与 CCR 的映射](#7.6 远程模式:命令与 CCR 的映射)
- [7.7 风扇和加热同时开,会发生什么](#7.7 风扇和加热同时开,会发生什么)
- [八、没有示波器怎么验证 PWM](#八、没有示波器怎么验证 PWM)
-
- [8.1 先看日志,再看硬件](#8.1 先看日志,再看硬件)
- [8.2 万用表平均电压法(最实用)](#8.2 万用表平均电压法(最实用))
- [8.3 LED + 电阻试灯法](#8.3 LED + 电阻试灯法)
- [8.4 逻辑分析仪(最直接)](#8.4 逻辑分析仪(最直接))
- [8.5 "改了 CCR 没反应"排查表](#8.5 "改了 CCR 没反应"排查表)
- [8.6 舵机专项排查表](#8.6 舵机专项排查表)
- 九、本章易错点总结
- 十、小结与下一篇
- [附录 A:本章涉及的完整代码片段](#附录 A:本章涉及的完整代码片段)
-
- [A.1 参数宏(`main.c` 顶部)](#A.1 参数宏(
main.c顶部)) - [A.2 启动 PWM(`main()` 的 USER CODE 2 区)](#A.2 启动 PWM(
main()的 USER CODE 2 区)) - [A.3 手动模式:三个按键各自的处理](#A.3 手动模式:三个按键各自的处理)
- [A.4 自动模式:四分支控制](#A.4 自动模式:四分支控制)
- [A.5 远程命令:命令串 → CCR](#A.5 远程命令:命令串 → CCR)
- [A.6 模式切换前的安全归位](#A.6 模式切换前的安全归位)
- [A.7 最小验证步骤](#A.7 最小验证步骤)
- [A.1 参数宏(`main.c` 顶部)](#A.1 参数宏(
系列第八篇。前面两篇都在"读":读温湿度、读空气质量。这一篇开始"写"------让设备真的动起来。
我第一次调风扇,代码里明明写了
CCR = 800,风扇一动不动。查了半天,最后发现是根本没调用HAL_TIM_PWM_Start。后来调舵机又踩一次:写CCR = 2500,舵机一声不吭,我还以为买到了坏货。
一、PWM 到底是什么
1.1 一句话版本
PWM(Pulse Width Modulation,脉冲宽度调制)= 固定周期,改"高电平占多长时间"。
它输出的还是一根只会"高/低"的方波,但通过调整高电平的比例,就能让负载得到"介于全关和全开之间"的效果。
text
占空比 20%: ‾‾\________‾‾\________‾‾\________
占空比 50%: ‾‾‾‾‾\________‾‾‾‾‾\________
占空比 80%: ‾‾‾‾‾‾‾‾‾\__‾‾‾‾‾‾‾‾‾\__
←------ 一个周期 T ------→
1.2 同样是 PWM,两种完全不同的用法
这是本章最重要的一句话,搞混了就会写出"舵机不转"的代码:
| 用法 | 看什么 | 本工程的设备 | 单位 |
|---|---|---|---|
| 功率型 | 看占空比(高电平占比) | 风扇、加热片 | % |
| 角度型 | 看高电平持续多少微秒 | SG90 舵机 | µs |
- 风扇:占空比 80% ≈ 平均功率 80%,转得快;占空比 0% 停。
- 舵机:它根本不关心占空比是 2.5% 还是 10%,只关心"每个周期高电平是不是 0.5ms / 2.0ms"。占空比只是这个脉宽在 20ms 周期里算出来的结果。
所以你会看到一个反直觉的现象:舵机
CCR=500只占 2.5%,但它转得"很用力";风扇CCR=500只占 50%,转速却只有一半。因为两者读的不是同一个信息。
1.3 三个必须分清的参数
| 参数 | 含义 | 本工程取值 |
|---|---|---|
| 频率 | 一秒钟有多少个周期 | 风扇/加热 1kHz;舵机 50Hz |
| 周期 | 一个周期多长时间 | 1ms;20ms |
| 占空比 / 脉宽 | 高电平占多少 | 800/1000 = 80%;500~2000µs |
频率决定"变化有多快",脉宽决定"这一下给多少"。
1.4 为什么风扇不用"开/关",要用 PWM
直接开关也能调速,但有两个问题:
- 冲击电流:电机从静止直接全压启动,启动电流是额定值的 3~7 倍,电源容易被拉垮;
- 只能全速:想"小风"做不到,只能靠人手动挡。
PWM 相当于快速地开开关关。因为风扇有惯性、加热片有热惯性,只要开关足够快,宏观上就是"半速 / 半功率",而且启动过程是渐进的。
二、定时器是怎么"画"出 PWM 的
2.1 四个角色
STM32 的定时器输出 PWM,内部其实只有四个关键角色:
text
时钟源 ──▶ [PSC 预分频] ──▶ [CNT 计数器] ──▶ 与 [CCR] 比较 ──▶ 引脚输出
▲
[ARR 自动重装]
| 角色 | 作用 | 类比 |
|---|---|---|
| 时钟 | 定时器的"心跳" | 秒表的最小刻度 |
| PSC(预分频) | 把心跳变慢 | 每 100 下算 1 秒 |
| CNT | 一直在 0 → ARR → 0 循环计数 | 秒针 |
| ARR | 数到多少归零,决定周期 | 一圈有多少格 |
| CCR | 数到多少翻转电平,决定脉宽 | 这一圈亮几格 |
2.2 PWM mode 1 的规则
CubeMX 里默认选的是 PWM mode 1,规则只有一句:
CNT < CCR 时输出高电平,CNT ≥ CCR 时输出低电平。
text
CNT: 0 200 400 600 800 999 0
│ │ │ │ │ │
输出: ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\______________
└────── CCR = 800 ───────┘└─ 剩 200 ─┘
└──────────── ARR+1 = 1000 ──────────┘
所以改 CCR 就是改"高电平有多宽",不需要停定时器、不需要重新初始化。
2.3 三个公式
text
计数频率 = 定时器时钟 ÷ (PSC + 1)
PWM 频率 = 计数频率 ÷ (ARR + 1)
占空比 = CCR ÷ (ARR + 1)
注意都是 +1:因为 PSC/ARR 是从 0 开始数的,写 99 就是 100 分频。
2.4 本工程两套参数是怎么来的
本工程 SystemClock_Config() 里 APB1 = 50MHz。但有个容易忘的规则:
APB1 分频系数 ≠ 1 时,挂在 APB1 上的定时器时钟 = APB1 × 2。
所以 TIM3、TIM5 的时钟都是 100MHz,不是 50MHz。
TIM3(风扇 CH1 / 加热 CH2 / 预留 CH3)
| 项目 | 值 | 结果 |
|---|---|---|
| 定时器时钟 | 100MHz | --- |
| PSC | 99 | 100MHz ÷ 100 = 1MHz |
| ARR | 999 | 1MHz ÷ 1000 = 1kHz(周期 1ms) |
| CCR | 800 | 800 ÷ 1000 = 80% |
TIM5(SG90 CH1)
| 项目 | 值 | 结果 |
|---|---|---|
| 定时器时钟 | 100MHz | --- |
| PSC | 99 | 100MHz ÷ 100 = 1MHz |
| ARR | 19999 | 1MHz ÷ 20000 = 50Hz(周期 20ms) |
| CCR | 500 | 高电平 500µs |
2.5 为什么 CCR 能直接当"微秒"用
这是本工程故意凑出来的设计。
计数频率做到 1MHz 之后,计数器每走 1 格正好是 1µs。而 CCR 表示"数到第几格翻转",所以:
text
CCR = 500 → 高电平持续 500 格 → 500µs
CCR = 2000 → 高电平持续 2000 格 → 2000µs
如果 PSC 写成 9(10 分频),计数频率变成 10MHz,那 CCR=500 就只有 50µs,舵机会完全不动。这就是"参数算错 → 舵机不响应"最常见的原因。
2.6 CCR 超过 ARR 会怎样
PWM mode 1 下:
CCR = 0:CNT < 0永远不成立 → 一直低电平,0%;CCR = ARR+1(本工程 1000 / 20000):高电平占满整周期 → 一直高电平,100%;CCR > ARR+1:比较永不触发翻转 → 仍是 100% 高电平。
所以舵机写 CCR = 25000 不会"转更多",只会一直输出高电平 。舵机收到一个持续高电平,等于没有有效脉冲,表现就是不动、或者嗡嗡响。
三、CubeMX 里那几个勾(回看第 4 章)
这一节帮你把第 4 章的配置和本章的原理对上号。
3.1 通道模式要选 PWM Generation CHx
只选 Output Compare 不选 PWM,也能产生波形,但它是"比较输出",不是给你调占空比用的。看到 PWM Generation 才算对。
3.2 Pulse = 0 的意义
CubeMX 里 Pulse 就是初始 CCR。本工程全部填 0,意思是:
上电时先输出 0%,谁也别动。
这个细节很关键。如果 Pulse 填 800,那么 MX_TIM3_Init() 一执行完、HAL_TIM_PWM_Start() 一调用,风扇就立刻转起来------在你还没进入任何模式之前。放在智能家居里,这不是"功能正常",是"上电惊魂"。
3.3 PWM mode 1 + Polarity High
- 模式 1:CNT < CCR 输出高 → 占空比 = CCR/(ARR+1),符合直觉;
- 极性 High:高电平有效,不反相。
如果你的驱动板是"低电平导通"(有些 MOS 模块是低有效),可以选 Polarity Low,或者干脆在代码里把 800 改成 200(取反)。先确认硬件,再决定要不要反相,不要两边都反,最后变成"关的时候全速转"。
3.4 Auto-reload preload = Enable
CubeMX 的 Auto-reload preload 本工程是 Enable。它的作用是:运行中改 ARR 时,新值在下一个更新事件才生效,避免出现"半个周期"的毛刺。
顺带一个细节:输出比较也有预装载位(OCxPE)。如果它被置位,你写进 CCR 的新值同样会等到下一个更新事件才搬到"活动寄存器"。对 TIM5 来说最多等 20ms------人眼完全看不出来,但如果你在做"精确输出 N 个脉冲"的实验,就要把这个延迟算进去。
3.5 生成的 tim.c 里看什么
c
/* TIM3 */
htim3.Init.Prescaler = 100-1; // PSC = 99 → 1MHz 计数
htim3.Init.Period = 1000-1; // ARR = 999 → 1kHz
sConfigOC.OCMode = TIM_OCMODE_PWM1;// PWM mode 1
sConfigOC.Pulse = 0; // 初始 CCR = 0
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
/* TIM5 */
htim5.Init.Prescaler = 100-1; // 同样 1MHz 计数
htim5.Init.Period = 20000-1; // ARR = 19999 → 50Hz
还有一处容易忽略的:MX_TIM3_Init() 最后一行是 HAL_TIM_MspPostInit(&htim3);。PA6/PA7/PB0 复用成 AF2 就是在这里配的 。如果你手动改过引脚,却忘了改 MspPostInit,定时器照样在跑,但引脚还是普通 GPIO------示波器什么都没有。
四、启动:HAL_TIM_PWM_Start 只调一次
4.1 本工程的四行
在 main() 的 /* USER CODE BEGIN 2 */ 里:
c
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 风扇 PA6
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); // 加热 PA7
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); // 预留 PB0
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_1); // 舵机 PA0
只调一次,之后整个程序生命周期都在跑。
4.2 不调用会怎样
MX_TIM3_Init() 只是把定时器"配置好",没有让它开始计数 。HAL_TIM_PWM_Start() 干的事是:
- 使能定时器计数(
CEN = 1); - 使能对应通道输出(
CCxE = 1)。
少任何一步,引脚上都没有波形。这时候你改 CCR、改 ARR、甚至改 PSC,全都不会有任何反应。
排查口诀:"改了没反应,先看 Start 有没有调,再看引脚是不是 AF。" 这两条能解决 90% 的"PWM 不出波形"。
4.3 运行时只改 CCR
c
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 800); // 风扇 80%
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); // 风扇停
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, 500); // 窗帘拉开
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, 2000); // 窗帘关闭
这个宏展开后基本就是:
c
htim3.Instance->CCR1 = 800;
为什么用宏而不是直接写寄存器? 因为通道和寄存器的对应关系(CCR1/CCR2/CCR3/CCR4)容易写错,宏帮你做了映射。而且以后换芯片,宏还能用,寄存器名可能变。
想读回当前值,用 __HAL_TIM_GET_COMPARE()------调试时很好用:
c
printf("FAN CCR=%lu\r\n",
(unsigned long)__HAL_TIM_GET_COMPARE(&htim3, TIM_CHANNEL_1));
4.4 "关设备"用 CCR=0,不要用 HAL_TIM_PWM_Stop
新手常写:
c
HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_1); // ❌ 本工程不这么写
它的问题:
- 关闭通道输出后,引脚电平不再由 CCR 决定,状态不好预期;
- 下次想开,必须重新
Start,多一处可能忘; - 定时器还在跑,但通道关了------状态变成"半开半关",很难排查。
本工程统一用:
c
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); // ✅ 通道保持开启,输出恒低
CCR = 0 时 PWM mode 1 输出一直低电平,MOS 关断,风扇停。逻辑干净,而且随时改回去就能再转。
舵机尤其不能 Stop。 SG90 是靠持续的 50Hz 脉冲维持"保持力矩"的。你把 PWM 停了,它立刻失去力矩,窗帘会自己滑下来,或者被风一吹就动。"省电"和"窗帘不掉"在这里只能选一个。
4.5 改 CCR 会不会有延迟
有,但通常可以忽略:
- TIM3 周期 1ms → 最晚 1ms 生效;
- TIM5 周期 20ms → 最晚 20ms 生效。
这个延迟对风扇、窗帘完全无感。但如果你在做"发一个 500µs 脉冲就停"的精确实验,要意识到写入时刻和生效时刻不是同一时刻。
五、风扇与加热片:占空比 = 功率
5.1 硬件链路长什么样
text
STM32 PA6 ──▶ MOS 栅极(扩展板)──▶ 风扇(5V)
│
GND ── 与 STM32 共地
关键点:
- PA6 只出控制信号,不直接驱动风扇。风扇电流动辄几百 mA,IO 最多几十 mA,直接接必烧;
- 扩展板上已经有 MOS 管,PA6 接的是 MOS 的栅极;
- 必须共地。不共地时,MOS 的"高/低"没有参考点,现象就是"有时候转、有时候不转",非常难查。
5.2 为什么是 800,而不是 1000
c
#define FAN_PWM 800 // 800/1000 = 80%
#define HER_PWM 800
留 20% 余量的原因:
- 降噪:风扇全速噪音明显,80% 日常够用;
- 降流:启动和运行电流都小一点,电源更稳;
- 留调节空间:以后想"强风/弱风",改这个宏就行,不用动逻辑。
换成 1000 也不会坏,只是更吵、更费电。这属于策略参数,不是硬件限制。
5.3 风扇是感性负载,两个坑
- 反电动势 :电机断电瞬间线圈会产生高压尖峰,可能击穿 MOS。驱动端要有续流二极管(很多模块已集成,裸 MOS 要自己加)。
- 1kHz 会有轻微啸叫 :1kHz 正好在人耳敏感区,安静环境下能听到"吱------"。想彻底安静可以改成 20kHz 以上,但 MOS 开关损耗会上升,而且 ARR 要重新算。本工程优先保证"能调、好算",接受这点声音。
还有一个"看不见"的坑:风扇和 DHT11 抢电源。风扇一转,5V/3.3V 上会有纹波,DHT11 的微秒级时序就容易读错。这就是第 6 章里"DHT11 读失败要重试、要间隔 1s"的现实原因之一。
5.4 加热片是阻性负载,重点在"别烧"
- 纯电阻,没有反电动势,PWM 频率高低都行,1kHz 完全够;
- 电流大,供电线要粗,别用细杜邦线(会发热,甚至熔皮);
- 不要贴着塑料壳、杜邦线、电池;
- 不要长时间 100% 干烧。本工程用 80%,并且自动模式下高温时会同时关掉加热,算是双重保护;
- 加热片有热惯性,PWM 开关的瞬间温度不会跳变,所以 1kHz 不会造成"忽冷忽热"。
5.5 手动模式的按键实现
手动模式下,KEY1 管风扇、KEY4 管加热、KEY3 管窗帘。以风扇为例:
c
g_fan_on = !g_fan_on; // 翻转逻辑状态
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1,
g_fan_on ? FAN_PWM : 0); // 同步改硬件
printf("[MANUAL] FAN %s\r\n", g_fan_on ? "ON" : "OFF");
三行缺一不可。
5.6 为什么"标志位"必须和 CCR 一起改
g_fan_on 这类标志位记录的是逻辑状态 ,CCR 记录的是物理状态。两者一旦不同步,就会出现非常诡异的现象:
- 只改 CCR 不改标志:第一次按键"关"了风扇,但标志位还是 0,第二次按键又置 1 → 看起来像"按一下开、按一下还是开";
- 只改标志不改 CCR:日志显示
FAN ON,风扇却不转。
记住:标志位是给"下一次按键"看的,CCR 是给"硬件"看的,两边都要写。
六、SG90 舵机:脉宽 = 角度
6.1 它内部其实是一个闭环系统
SG90 不是"给电就转"的电机,里面有四样东西:
text
目标脉宽 ──▶ 控制电路 ──▶ 直流电机 ──▶ 减速齿轮 ──▶ 输出轴
▲ │
└──── 电位器(角度反馈)◀───────────┘
- 你给的脉宽 = 目标角度;
- 电位器测出当前角度;
- 控制电路比较两者,差得多就转快,差得少就转慢,到位就停。
所以它是位置伺服 :你给 500µs,它转到一端并保持 ;你给 2000µs,它转到另一端并保持。中途断电,它就松了。
6.2 为什么是 50Hz / 20ms
舵机的"行业习惯"就是 50Hz:
- 太快(比如 1kHz):老式模拟舵机内部电路来不及解析,会抖动甚至不响应;
- 太慢(比如 20Hz):保持力矩更新不及时,会一顿一顿的。
20ms 一个周期,其中 0.5~2.5ms 是有效脉宽,剩下 17.5~19.5ms 是"空档"。舵机只在这 0.5~2.5ms 里读信息。
6.3 标准量程 vs 本机实测量程
理论上常见范围是 500~2500µs 对应 0°~180° 。但本工程这颗 SG90 实测只认 500~2000µs:
| CCR | 实际现象 |
|---|---|
| 500 | 转到一端(本项目定义"拉开") |
| 2000 | 转到另一端("关闭") |
| 2500 | 完全不动,贴近听有轻微嗡嗡声 |
| 300 | 完全不动 |
原因不一定是"舵机坏了":不同批次、不同内部限位、不同齿轮比,都可能让有效区间变窄。所以这两个宏是"测试"出来的,不是抄来的:
c
#define CURTAIN_OPEN_PULSE 500 // 实测可动量程的一端
#define CURTAIN_CLOSE_PULSE 2000 // 另一端
换舵机必须重新标定。 方法很简单:写个循环,让 CCR 从 400 慢慢加到 2600,每次停 1 秒,记录"从哪开始动、到哪停止"。别直接拿别人的 500/2500 上电------超出量程轻则不动,重则堵转发热、扫齿。
6.4 为什么这里的 CCR 正好是微秒
因为 TIM5 也是 PSC=99 → 1MHz 计数:
text
1 格 = 1µs → CCR = 500 就是 500 格 = 500µs
这是设计上故意凑的,好处是代码里不用做任何换算:
c
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, 2000); // 就是 2000µs,不用乘不用除
如果哪天你把 PSC 改成 199(2MHz 计数),同样的 CCR=2000 就变成 1000µs,舵机会跑到中间去。改 PSC 之前,先想清楚所有依赖"CCR=微秒"的地方。
6.5 用占空比算,其实只是"换个说法"
CCR=500 的占空比是 500/20000 = 2.5% ,CCR=2000 是 10%。
你可以用占空比描述它,但不能用占空比思维去调它 。比如你觉得"2.5% 太小了,给 50% 吧",那 CCR 就是 10000 → 高电平 10ms → 舵机收到一个"超长脉冲",直接判为无效,不动。
这就是 1.2 节那句话的现实版:舵机读的是"绝对时间",不是"比例"。
6.6 供电:这一节能救你的板子
SG90 的参数:空载电流约 100mA,堵转电流可达 700mA 以上。
| 错误接法 | 现象 |
|---|---|
| 舵机 VCC 接板子 3.3V | 一动就复位,串口日志"从头开始" |
| 舵机 VCC 接板子 5V 但线细 | 电压跌落,舵机抖动、回弹 |
| 只接信号不接 GND | 完全不动,或乱抖 |
| 用独立 5V 但没共地 | 同上,信号没有参考点 |
正确接法:
text
独立 5V 电源 ──▶ SG90 VCC(红线)
独立 5V 电源 GND ──▶ SG90 GND(棕线)──▶ 同时接 STM32 GND
STM32 PA0 ──▶ SG90 信号(橙线)
再并一颗 470µF 以上电解电容在舵机电源附近,抖动会明显改善。
6.7 上电抖一下,是正常的
舵机上电瞬间通常会轻微抖一下,这是内部自检/找零点,不代表程序有问题。真正要担心的是:
| 现象 | 原因 |
|---|---|
| 一直嗡嗡响、发烫 | 脉宽超出量程,堵转 |
| 转到一半回弹 | 供电不足,电压跌落导致复位 |
| 抖动不止 | 电源纹波大 / 没共地 / 脉宽靠近极限 |
| 完全不动 | 没 PWM_Start、脉宽超范围、信号没接对 |
| 用手拧有"咔咔"声 | 齿轮在打滑,别硬拧,容易扫齿 |
6.8 "两位置法":只走两个端点
窗帘不需要"开 30%",只需要"拉开"和"关闭"两个状态。所以本工程只用两个 CCR 值,中间过程完全交给舵机自己完成。
好处:
- 代码简单,一个三目运算就够;
- 不会出现"停在半开"的中间态;
- 舵机自己会找位置,不需要你写延时等待。
c
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1,
g_curtain_open ? CURTAIN_OPEN_PULSE : CURTAIN_CLOSE_PULSE);
6.9 方向反了怎么办
如果实际装上去发现"500 是关、2000 是开",有两种改法:
- 改宏(推荐):把两个宏的值对调;
- 改机械:把舵机臂拆下来转 180° 再装。
不要 在代码里到处写 2000 - x 这种"取反",后面会看不懂。
七、把执行器接进三模式状态机
7.1 切模式先全关:安全第一
Switch_Mode() 的开头永远是这几行:
c
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); // 风扇关
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0); // 加热关
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, 0); // 预留通道关
HAL_GPIO_WritePin(DHT11_LED_GPIO_Port, DHT11_LED_Pin, GPIO_PIN_RESET);
g_fan_on = 0; g_curtain_open = 0; g_heat_on = 0; // 标志位同步清零
if(new_mode == MODE_MANUAL)
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, CURTAIN_CLOSE_PULSE);
为什么进手动模式还要单独让窗帘归位到"关闭"?
因为标志位被清成了 0(表示"关"),如果舵机实际还停在"开"的位置,就会出现:
按 KEY3 → 标志位 0 变 1 → 代码让舵机去"开" → 舵机本来就在开的位置,一动不动 → 你以为按键坏了。
把实物也归位到"关",第一次按 KEY3 就一定能看到动作。这是"状态一致"的典型处理。
7.2 手动模式:三键三设备
| 按键 | 设备 | 动作 |
|---|---|---|
| KEY1 | 风扇 | CCR1 在 0 / 800 间切换 |
| KEY3 | 窗帘 | CCR5 在 2000 / 500 间切换 |
| KEY4 | 加热 | CCR2 在 0 / 800 间切换 |
| KEY2 | --- | 退出手动,回自动模式 |
手动模式下不做任何自动判断,按一下变一下,这是"手动"的定义。
7.3 自动模式:四个分支
自动模式每 2 秒采集一次,然后按下面的优先级判断:
| 分支 | 条件 | 风扇 | 加热 | 窗帘 | 指示灯 |
|---|---|---|---|---|---|
| ① 高温 | temp >= 30 |
800 | 0 | 开(500) | 亮 |
| ② 低温 | temp <= 29 |
0 | 800 | 关(2000) | 灭 |
| ③ 空气差 | 温度正常 且 g_air_bad |
800 | 0 | 开(500) | 亮 |
| ④ 正常 | 温度正常 且 空气好 | 0 | 0 | 开(500) | 灭 |
注意判断顺序:温度优先。温度一旦越界,先保证"别热死/别冻着",空气质量的通风需求往后排。
第 ③ 个分支的 g_air_bad 来自上一章:
c
g_air_bad = (g_AQI >= AIR_AQI_BAD) || (g_ECO2 > AIR_ECO2_BAD) || (g_TVOC > AIR_TVOC_BAD);
三个指标任一超限就算"差"。
7.4 1℃ 回差为什么必须有
阈值是 30 / 29,不是 30 / 30。这不是写错,是故意的。
假设写成 30/30,室温刚好 30℃:
text
t=0s 30℃ → 高温 → 风扇开 → 降温到 29.9℃
t=2s 29.9℃ → 不满足>=30 → 风扇关 → 温度回升到 30.1℃
t=4s 30.1℃ → 高温 → 风扇开 → ...
结果:风扇每 2 秒开关一次。风扇、MOS、电源都在被反复冲击,噪音也很折磨人。
留 1℃ 回差后:
text
升到 30℃ → 开风扇
降到 29℃ 以下 → 才关风扇
中间 29~30℃ 这个"死区"里,保持上一次的状态不变,设备就稳定了。
这个技巧叫迟滞(hysteresis),是所有"阈值控制"的通用做法:空调、热水器、电池充电,全都在用。
7.5 为什么"正常温度且空气好"窗帘还是拉开
分支 ④ 里:
c
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, CURTAIN_OPEN_PULSE);
温度正常、空气也好,风扇和加热都关了,但窗帘保持拉开。
理由很简单:白天室内需要采光,窗帘默认就是开的。只有"低温"这个需要保温的场景才关窗帘。如果每次数据正常都把窗帘关上,体验会很怪。
这类"默认态"的取舍,代码里看不出对错,但用起来一眼就能感觉到。
7.6 远程模式:命令与 CCR 的映射
蓝牙和 MQTT 共用同一个函数 Remote_Cmd_Exec(),命令是整串匹配:
| 命令字符串 | 硬件动作 |
|---|---|
FAN_ON |
htim3 CH1 = 800 |
FAN_OFF |
htim3 CH1 = 0 |
HER_ON |
htim3 CH2 = 800 |
HER_OFF |
htim3 CH2 = 0 |
CURTAIN_ON |
htim5 CH1 = 500(拉开) |
CURTAIN_OFF |
htim5 CH1 = 2000(关闭) |
ALL_OFF |
风扇 0 + 加热 0 + 窗帘 2000 |
注意字面量是
CURTAIN_ON/CURTAIN_OFF,不是CURTAIN_OPEN/CURTAIN_CLOSE。H5 页面发的就是 ON/OFF。如果你在串口助手或 MQTT 客户端手打
CURTAIN_OPEN,strstr匹配不到,设备不会报错,也不会有任何反应------这是最容易被判成"程序死了"的一种情况。
7.7 风扇和加热同时开,会发生什么
自动模式下两者互斥(开了风扇就关加热),但手动模式和远程模式不互斥 ------你可以先 FAN_ON 再 HER_ON。
后果:
- 两个负载同时吃电流,5V 电源可能被拉跌;
- 一个在吹、一个在加热,物理上互相打架(但不会坏);
- 如果是电池供电,续航直接腰斩。
想避免的话,可以在 Remote_Cmd_Exec() 里加"开风扇前先关加热"的互锁。本工程没加,因为演示时偶尔需要"两个都开"来看效果。做产品必须加。
八、没有示波器怎么验证 PWM
大部分同学手边只有万用表。下面按"从易到难"给四种办法。
8.1 先看日志,再看硬件
c
printf("[MANUAL] FAN %s\r\n", g_fan_on ? "ON" : "OFF");
如果日志打印了 FAN ON,风扇不转,说明软件路径走通了 ,问题在硬件或 PWM 输出。反过来,日志都没打印,就先查按键和模式------不要一上来就怀疑 PWM 配置。
8.2 万用表平均电压法(最实用)
PWM 的直流平均值 = 高电平电压 × 占空比。
TIM3 风扇(3.3V 输出):
| CCR | 占空比 | 万用表直流电压(PA6 对 GND) |
|---|---|---|
| 0 | 0% | ≈ 0V |
| 500 | 50% | ≈ 1.65V |
| 800 | 80% | ≈ 2.64V |
| 1000 | 100% | ≈ 3.3V |
TIM5 舵机(50Hz,脉宽很短):
| CCR | 脉宽 | 平均电压 |
|---|---|---|
| 500 | 500µs | ≈ 0.083V |
| 2000 | 2000µs | ≈ 0.33V |
舵机这两行电压都很低,而且万用表在 50Hz 下可能跳字,只能定性看"有没有变化",不能当精确值。
这个办法的前提是万用表在直流档。用交流档会读到别的东西,别搞错。
8.3 LED + 电阻试灯法
手边有 LED 和 1kΩ 电阻的话:
text
PA6 ──▶ 1kΩ ──▶ LED 正极 ──▶ LED 负极 ──▶ GND
CCR=0:LED 灭;CCR=800:LED 亮,但比 100% 暗一点。
注意:LED 有正向压降,亮度变化不是严格线性的,只用来判断"有没有输出",别用来测占空比。
8.4 逻辑分析仪(最直接)
几十块钱的 8 通道逻辑分析仪就够用。接 PA6 或 PA0,看:
- 频率对不对(1kHz / 50Hz);
- 高电平宽度对不对(800µs / 500µs / 2000µs);
- 有没有波形(没有 = Start 没调 / 引脚不是 AF)。
这是唯一能一眼看出"参数算错"的工具。 比如你看到 10kHz,就知道 PSC 少写了一个 0。
8.5 "改了 CCR 没反应"排查表
按这个顺序查,基本不会绕圈:
| 顺序 | 检查项 | 怎么确认 |
|---|---|---|
| 1 | 有没有调 HAL_TIM_PWM_Start |
搜代码,四行都要有 |
| 2 | 改的是不是对的通道 | 风扇 CH1、加热 CH2、舵机 TIM5_CH1 |
| 3 | 引脚是不是 AF 复用 | 看 HAL_TIM_MspPostInit 有没有执行 |
| 4 | PSC/ARR 对不对 | TIM3 = 99/999,TIM5 = 99/19999 |
| 5 | 有没有被别处改回去 | 自动模式每 2 秒会覆盖 CCR |
| 6 | 硬件共地了吗 | 万用表通断档量 GND |
| 7 | MOS 是高有效还是低有效 | 看驱动板丝印/原理图 |
| 8 | 负载供电有没有 | 量风扇两端电压 |
第 5 条特别隐蔽 :你在手动模式下正调着,如果模式其实是自动模式,2 秒后自动逻辑就会把你的值覆盖掉。先确认 g_mode 再调。
8.6 舵机专项排查表
| 现象 | 优先怀疑 |
|---|---|
| 完全不动 | 脉宽超 500~2000;或没 Start;或信号线没接 PA0 |
| 一直抖 | 供电不足 / 没共地 / 电源纹波大 |
| 一动板子就复位 | 舵机从 3.3V 取电,启动电流拉垮电源 |
| 转到一半回弹 | 电压跌落,舵机内部复位 |
| 嗡嗡响且发烫 | 堵转(脉宽超量程或机械卡住),立刻断电 |
| 只能转很小一段 | 脉宽范围太窄,重新标定 |
| 上电抖一下 | 正常自检,不用管 |
九、本章易错点总结
| 现象 | 真正原因 | 处理 |
|---|---|---|
| 改了 CCR 风扇不动 | 没调用 HAL_TIM_PWM_Start |
四个通道都要 Start,只调一次 |
| 示波器完全没波形 | 引脚还是普通 GPIO,没进 AF 复用 | 检查 HAL_TIM_MspPostInit / CubeMX 引脚 |
| 风扇转速只有一点点 | PSC/ARR 算错,实际频率或分辨率不对 | TIM3 用 99/999,1kHz |
| 风扇"很猛但关不掉" | 驱动板低有效,代码按高有效写的 | 改 Polarity 或反相 CCR |
| 风扇时转时不转 | 大电流负载没和 MCU 共地 | 电源地、信号地全部连通 |
| 一上电风扇就转 | CubeMX 的 Pulse 填了非 0 |
初始 Pulse 一律填 0 |
| 自动模式下手动调 CCR 立刻被改回 | 当前其实是自动模式,2 秒覆盖一次 | 先确认 g_mode |
| 舵机写 2500 完全不动 | 超出本机实测量程 500~2000 | 只用 500 / 2000,换舵机重新标定 |
| 舵机完全不响应 | PSC 不是 99,CCR 不再等于微秒 | TIM5 用 99/19999,50Hz |
| 舵机抖动、回弹 | 供电不足 / 没共地 / 没加电容 | 独立 5V + 共地 + 470µF |
| 舵机一动,板子重启 | 舵机接了板载 3.3V | 独立 5V 供电,只共地 |
| 舵机断电后窗帘自己掉下来 | 用了 HAL_TIM_PWM_Stop |
保持 PWM 运行,用 CCR 控制位置 |
| 按键第一次"没反应" | 标志位和实物状态不一致 | 切手动时把窗帘归位到关闭 |
远程发 CURTAIN_OPEN 没反应 |
命令字面量是 CURTAIN_ON |
用 ON/OFF,别用 OPEN/CLOSE |
| 温度在 30℃ 附近风扇疯狂开关 | 阈值没留回差 | 用 30 / 29 的迟滞 |
改 htim3.Init.Period 来调速 |
改 ARR 会改频率,不是占空比 | 只改 CCR |
| 加热片线发热、变软 | 用细杜邦线过大电流 | 换粗线,别贴着塑料 |
| 风扇断电瞬间 MOS 击穿 | 感性负载没有续流二极管 | 加续流二极管(或换带保护的模块) |
在中断里调 HAL_TIM_PWM_Start |
带状态的操作不适合放中断 | 中断里只改 CCR,Start 放初始化 |
十、小结与下一篇
这一章记住五件事:
- PWM 有两种读法:风扇看占空比,舵机看脉宽(µs);
- 必须先 Start :
HAL_TIM_PWM_Start只调一次,之后只改 CCR; - CCR 就是微秒:因为两路定时器都做到了 1MHz 计数(PSC=99);
- 舵机只认 500~2000:这是实测标定值,超范围不动,换舵机要重标;
- 阈值要留回差:30/29 不是笔误,是防抖。
下一篇进入按键消抖与三模式状态机:四个按键怎么在三种模式里"一人多职"、为什么消抖不能用同一个时间戳、以及模式切换为什么必须在主循环里做而不能在中断里做。
如果这篇文章对你有帮助,欢迎点赞收藏。系列更新中,下一篇见。
附录 A:本章涉及的完整代码片段
A.1 参数宏(main.c 顶部)
c
// 功能2阈值定义
#define TEMP_HIGH 30 // 高温阈值
#define TEMP_LOW 29 // 低温阈值(与高温留 1℃ 回差,防止反复开关)
#define FAN_PWM 800 // 风扇占空比 800/1000 = 80%
#define HER_PWM 800 // 加热占空比
#define CUR_PWM 800 // 预留窗帘电机通道(PB0),本工程未使用
#define CURTAIN_OPEN_PULSE 500 // 舵机拉开:500µs(实测可动量程一端)
#define CURTAIN_CLOSE_PULSE 2000 // 舵机关闭:2000µs(另一端,超出 2000 不响应)
A.2 启动 PWM(main() 的 USER CODE 2 区)
c
// 启动 TIM3/TIM5 PWM:只调一次,之后运行期间一直输出波形
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // FAN PA6
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); // HER PA7
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); // CUR PB0(预留)
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_1); // SG90 PA0
A.3 手动模式:三个按键各自的处理
c
// KEY1:手动模式 = 风扇开/关
if(g_mode == MODE_MANUAL)
{
g_fan_on = !g_fan_on; // 翻转逻辑状态
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1,
g_fan_on ? FAN_PWM : 0); // 同步硬件
printf("[MANUAL] FAN %s\r\n", g_fan_on ? "ON" : "OFF");
}
// KEY3:手动模式 = 窗帘拉开/关闭
if(g_mode == MODE_MANUAL)
{
g_curtain_open = !g_curtain_open;
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1,
g_curtain_open ? CURTAIN_OPEN_PULSE : CURTAIN_CLOSE_PULSE);
printf("[MANUAL] CURTAIN %s\r\n", g_curtain_open ? "OPEN" : "CLOSE");
}
// KEY4:手动模式 = 加热开/关
if(g_mode == MODE_MANUAL)
{
g_heat_on = !g_heat_on;
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2,
g_heat_on ? HER_PWM : 0);
printf("[MANUAL] HEATER %s\r\n", g_heat_on ? "ON" : "OFF");
}
A.4 自动模式:四分支控制
c
if(temp >= TEMP_HIGH) // ① 高温:风扇 + 开窗帘
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, FAN_PWM);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0);
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, CURTAIN_OPEN_PULSE);
HAL_GPIO_WritePin(DHT11_LED_GPIO_Port, DHT11_LED_Pin, GPIO_PIN_SET);
}
else if(temp <= TEMP_LOW) // ② 低温:加热 + 关窗帘
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, HER_PWM);
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, CURTAIN_CLOSE_PULSE);
HAL_GPIO_WritePin(DHT11_LED_GPIO_Port, DHT11_LED_Pin, GPIO_PIN_RESET);
}
else if(g_air_bad) // ③ 温度正常 + 空气差:通风
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, FAN_PWM);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0);
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, CURTAIN_OPEN_PULSE);
HAL_GPIO_WritePin(DHT11_LED_GPIO_Port, DHT11_LED_Pin, GPIO_PIN_SET);
}
else // ④ 正常 + 空气好:全关,窗帘保持开
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0);
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, CURTAIN_OPEN_PULSE);
HAL_GPIO_WritePin(DHT11_LED_GPIO_Port, DHT11_LED_Pin, GPIO_PIN_RESET);
}
A.5 远程命令:命令串 → CCR
c
static void Remote_Cmd_Exec(const char *cmd, uint8_t echo_back)
{
if(strstr(cmd, "FAN_ON")) // 风扇开
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, FAN_PWM);
g_fan_on = 1;
}
else if(strstr(cmd, "FAN_OFF")) // 风扇关
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);
g_fan_on = 0;
}
else if(strstr(cmd, "HER_ON")) // 加热开
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, HER_PWM);
g_heat_on = 1;
}
else if(strstr(cmd, "HER_OFF")) // 加热关
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0);
g_heat_on = 0;
}
else if(strstr(cmd, "CURTAIN_ON")) // 窗帘拉开
{
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, CURTAIN_OPEN_PULSE);
g_curtain_open = 1;
}
else if(strstr(cmd, "CURTAIN_OFF")) // 窗帘关闭
{
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, CURTAIN_CLOSE_PULSE);
g_curtain_open = 0;
}
else if(strstr(cmd, "ALL_OFF")) // 一键全关
{
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0);
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, CURTAIN_CLOSE_PULSE);
g_fan_on = 0;
g_heat_on = 0;
g_curtain_open = 0;
}
/* 其余日志与回显略,见第 10、11 章 */
}
A.6 模式切换前的安全归位
c
void Switch_Mode(uint8_t new_mode)
{
g_mode = new_mode;
// 先关所有输出,安全第一
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); // 风扇关
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0); // 加热关
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, 0); // 预留通道关
HAL_GPIO_WritePin(DHT11_LED_GPIO_Port, DHT11_LED_Pin, GPIO_PIN_RESET);
// 标志位同步清零:任何模式都从"全关"开始
g_fan_on = 0; g_curtain_open = 0; g_heat_on = 0;
// 进手动模式时,让窗帘实物也归位到"关闭",保证第一次按 KEY3 一定能看到动作
if(new_mode == MODE_MANUAL)
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, CURTAIN_CLOSE_PULSE);
/* 模式日志略 */
}
A.7 最小验证步骤
- 只烧录,不改任何参数,上电后串口应打印
[MODE] Manual ...; - 万用表量 PA6 对 GND,应为 0V(Pulse=0,未开);
- 按 KEY1,日志出现
[MANUAL] FAN ON,PA6 电压应变成 约 2.64V(80%); - 再按 KEY1,日志
FAN OFF,电压回到 0V; - 按 KEY3,舵机应转到一端;再按 KEY3 回另一端,日志对应
OPEN/CLOSE; - 按 KEY2 进自动模式,用吹风机对着 DHT11 加热到 30℃ 以上,2 秒内风扇应启动、窗帘拉开;
- 拿开热源降到 29℃ 以下,加热通道打开(可用万用表量 PA7 电压约 2.64V),窗帘关闭。
七步全部通过,说明 PWM 这条链路(时钟 → 定时器 → 引脚 → 驱动 → 负载)是通的,可以去调按键状态机了。