STM32 从 0 到 1 做智能家居(八):PWM 控制风扇、加热片与 SG90 窗帘

文章目录

    • [一、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 里看什么)
    • [四、启动:`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 最小验证步骤)

系列第八篇。前面两篇都在"读":读温湿度、读空气质量。这一篇开始"写"------让设备真的动起来。

我第一次调风扇,代码里明明写了 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

直接开关也能调速,但有两个问题:

  1. 冲击电流:电机从静止直接全压启动,启动电流是额定值的 3~7 倍,电源容易被拉垮;
  2. 只能全速:想"小风"做不到,只能靠人手动挡。

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() 干的事是:

  1. 使能定时器计数(CEN = 1);
  2. 使能对应通道输出(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);   // ❌ 本工程不这么写

它的问题:

  1. 关闭通道输出后,引脚电平不再由 CCR 决定,状态不好预期;
  2. 下次想开,必须重新 Start,多一处可能忘;
  3. 定时器还在跑,但通道关了------状态变成"半开半关",很难排查。

本工程统一用:

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% 余量的原因:

  1. 降噪:风扇全速噪音明显,80% 日常够用;
  2. 降流:启动和运行电流都小一点,电源更稳;
  3. 留调节空间:以后想"强风/弱风",改这个宏就行,不用动逻辑。

换成 1000 也不会坏,只是更吵、更费电。这属于策略参数,不是硬件限制。

5.3 风扇是感性负载,两个坑

  1. 反电动势 :电机断电瞬间线圈会产生高压尖峰,可能击穿 MOS。驱动端要有续流二极管(很多模块已集成,裸 MOS 要自己加)。
  2. 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 是开",有两种改法:

  1. 改宏(推荐):把两个宏的值对调;
  2. 改机械:把舵机臂拆下来转 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 最小验证步骤

  1. 只烧录,不改任何参数,上电后串口应打印 [MODE] Manual ...;
  2. 万用表量 PA6 对 GND,应为 0V(Pulse=0,未开);
  3. 按 KEY1,日志出现 [MANUAL] FAN ON,PA6 电压应变成 约 2.64V(80%);
  4. 再按 KEY1,日志 FAN OFF,电压回到 0V;
  5. 按 KEY3,舵机应转到一端;再按 KEY3 回另一端,日志对应 OPEN / CLOSE;
  6. 按 KEY2 进自动模式,用吹风机对着 DHT11 加热到 30℃ 以上,2 秒内风扇应启动、窗帘拉开;
  7. 拿开热源降到 29℃ 以下,加热通道打开(可用万用表量 PA7 电压约 2.64V),窗帘关闭。

七步全部通过,说明 PWM 这条链路(时钟 → 定时器 → 引脚 → 驱动 → 负载)是通的,可以去调按键状态机了。

相关推荐
简单电磁炮1 小时前
关于adc采集
stm32·单片机
HQB20262 小时前
EG1163 (S)|屹晶微电子 600V 高压同步降压半桥控制器
嵌入式硬件·硬件工程
木由里予2 小时前
UFS 4.1 HID(Host Initiated Defragmentation)功能详解
嵌入式硬件
意法半导体STM322 小时前
【官方原创】如何在STM32CubeMX2中使用UART
stm32·单片机·嵌入式硬件
辛迪聊物业数字化4 小时前
物业费收缴率从68%提升到97%落地复盘:中小物业收费系统选型避坑指南
人工智能·嵌入式硬件·物联网·网络协议·系统架构
白搞电子4 小时前
ESP-Mosaico 轮滑动画编排台:拍点吸附、完整动作编辑与工程保存
单片机·嵌入式硬件·电脑
玄芯散人5 小时前
【元婴·114】SD卡和FatFS文件系统:数据持久化
stm32·sd卡·fatfs
恒锐丰科技林技术员5 小时前
EG2106L 高压半桥驱动芯片解析:高性价比 600V 栅极驱动方案
经验分享·嵌入式硬件·硬件工程
cu1435 小时前
细谈IT6653的具体功能与其应用
c语言·c++·单片机·嵌入式硬件