☰
STM32F103C8T6实现220V可控硅调光与红外遥控解码实战
2026/11/5 2:59:44 网站建设 项目流程

简介:这份资源面向嵌入式初学者与电子爱好者,提供基于STM32F103C8T6的220V调光完整方案,涵盖PWM亮度调节、遥控解码与Flash掉电记忆三大功能。程序通过配置定时器PWM通道改变占空比实现细腻调光,利用GPIO捕获红外或射频遥控脉冲并软件解码,同时借助内部Flash保存上次亮度,重新上电自动恢复。压缩包共182个文件,约3.76MB,以41个c源文件与43个h头文件为核心,配合Keil工程文件、编译中间产物、hex烧录文件及原理图PDF,构成从代码到硬件的闭环参考。已有2110人学习,适合想掌握STM32定时器、外部中断与Flash读写的读者对照实践,也可作为智能照明类课程设计或DIY项目的起步模板。

1. 拿到一份 STM32F103C8T6 的 PWM 调光包,先别急着上电

220V 调光这四个字,很多人第一反应是“危险”,第二反应是“不就是个 PWM 占空比吗”。真拆开这份 STM32F103C8T6 的调光程序包,你会发现它把两件容易翻车的事捆在了一起:一边是单片机输出 PWM 去驱动可控硅,靠过零检测做相位切割来调 220V 灯的亮度;另一边是红外遥控解码,把遥控器按键映射成亮度档位。前者是强电时序问题,后者是弱电协议问题,中间还夹着一个“遥控调光和本地调光怎么不打架”的状态管理。这份资源适合已经摸过 STM32 定时器、想从点灯跨到强电控制的从业者,也适合做智能开关、调光模块 Demo 的课程设计场景。它解决的不是“会不会写 PWM”,而是“PWM 怎么和过零信号对齐、遥控码怎么稳定解出来”这两个落地问题。

2. 相位切割调光的原理与定时器配置:为什么不能只改占空比

2.1 可控硅调光的时序本质

市电是 50Hz 正弦波,一个周期 20ms,每 10ms 过一次零点。可控硅一旦被触发导通,会一直保持到电流过零才关断,所以调光不是靠“PWM 占空比”直接调电压,而是靠“过零之后延迟多久触发”来决定导通角。延迟越短,导通时间越长,灯越亮;延迟越长,灯越暗。这就是相位切割,也叫前沿调光。

STM32 在这里的角色是:过零检测电路给出一个下降沿或上升沿中断,单片机在中断里启动一个定时器,定时器计到设定值后输出一个窄脉冲去触发可控硅。所以真正决定亮度的是“延迟计数值”,PWM 只是触发脉冲的载体。很多人把这份程序当成普通 PWM 调光来理解,直接改占空比,结果灯要么全亮要么闪,根子就在这里。

常见做法是用 TIM 的一个通道输出单脉冲模式(One Pulse Mode),过零中断里重装载 ARR,计到后硬件自动翻转引脚,省去在中断里手动拉高拉低的抖动。程序包里如果用的是普通 PWM 模式,也能跑,但触发脉宽要控制在几十微秒,太宽会烧触发变压器,太窄可控硅触发不可靠。

2.2 定时器参数怎么算

以 72MHz 主频、72 分频为例,定时器计数频率是 1MHz,即 1 个计数 = 1μs。半周期 10ms 对应 10000 个计数。假设最小触发延迟 1ms(留出过零检测电路的响应余量),最大延迟 9ms,那么可调范围是 1000~9000。

// TIM3 时基配置:1MHz 计数,单脉冲模式 void TIM3_Pulse_Init(void) { TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); tb.TIM_Prescaler = 72 - 1; // 72MHz / 72 = 1MHz,1计数=1us tb.TIM_Period = 10000 - 1; // 默认半周期10ms tb.TIM_CounterMode = TIM_CounterMode_Up; tb.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, &tb); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 50; // 触发脉宽50us oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC2Init(TIM3, &oc); TIM_SelectOnePulseMode(TIM3, TIM_OPMode_Single); // 单脉冲,计到ARR自动停 TIM_CtrlPWMOutputs(TIM3, ENABLE); }

这段配置的关键在TIM_SelectOnePulseMode:单脉冲模式下,计数器计到 ARR 就停止并输出一次翻转,正好对应“延迟结束触发一次可控硅”。TIM_Pulse = 50是触发脉宽,50μs 对多数光耦可控硅(如 MOC3021)够用,如果你用的触发变压器要求更窄,改成 20~30μs。ARR 在过零中断里动态改,改的是延迟量,不是占空比。

提示:过零检测中断里只做“重装 ARR + 启动定时器”,不要在里面做亮度计算和遥控解析,中断里拖太久会错过下一个过零点,灯会抖。

2.3 亮度到延迟的映射

线性映射看着合理,实际观感很差。人眼对亮度是对数感知,可控硅的导通角和光通量也不是线性关系。程序里如果直接delay = max - brightness * step,低亮度段会跳变明显。我一般用查表或者平方映射:把 0~100 的亮度档先平方再归一化,低档位分得更细。

// 亮度档(0~100) 转 延迟计数(1000~9000) uint16_t Brightness_To_Delay(uint8_t level) { uint32_t norm; if (level > 100) level = 100; // 平方映射,低亮度段更细腻 norm = (uint32_t)level * level; // 0 ~ 10000 // 延迟 = 最大延迟 - 归一化后的可调区间 return 9000 - (norm * 8000) / 10000; // 9000 ~ 1000 }

level=0时返回 9000,接近过零点才触发,灯基本不亮;level=100返回 1000,导通角最大。这个映射不是唯一解,但比线性少很多“低档跳变”的抱怨。调这个函数的时候,拿照度计或者肉眼在暗环境下对比 10%、30%、50% 三档,能明显看出差别。

3. 红外遥控解码:NEC 协议在中断里的稳定实现

3.1 NEC 帧结构和采样策略

遥控部分常见的是 NEC 协议:9ms 低电平 + 4.5ms 高电平做引导码,后面 32 位数据,每位用 560μs 低电平开头,高电平 560μs 表示 0、1690μs 表示 1,最后 560μs 结束位。解码的难点不在协议本身,而在“用定时器捕获还是用外部中断 + 计时”。

程序包里如果用的是外部中断下降沿触发 + 定时器计高电平宽度,这是成本最低的做法,但抗干扰一般。更稳的是用定时器输入捕获,直接测两个下降沿之间的间隔,硬件记录时间戳,软件判 0/1。C8T6 资源紧,用 TIM2 的 CH1 输入捕获,配合 DMA 或者捕获中断都行。

// TIM2 输入捕获,测下降沿间隔 void IR_Capture_Init(void) { TIM_ICInitTypeDef ic; GPIO_InitTypeDef gpio; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_IPU; // 红外接收头默认高,上拉 GPIO_Init(GPIOA, &gpio); // 1MHz 计数,1计数=1us,方便直接比对560/1690 TIM_TimeBaseInitTypeDef tb; tb.TIM_Prescaler = 72 - 1; tb.TIM_Period = 0xFFFF; tb.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &tb); ic.TIM_Channel = TIM_Channel_1; ic.TIM_ICPolarity = TIM_ICPolarity_Falling; ic.TIM_ICSelection = TIM_ICSelection_DirectTI; ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; ic.TIM_ICFilter = 0x03; // 滤波,抑制毛刺 TIM_ICInit(TIM2, &ic); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM2, ENABLE); }

TIM_ICFilter = 0x03是数字滤波,能滤掉几个时钟周期的毛刺,红外接收头输出在强光或者劣质电源下会有尖峰,不加滤波偶尔会多解出一位。捕获中断里读TIM_GetCapture1得到时间戳,和上一次时间戳相减就是间隔,落在 400~800μs 判 0,落在 1400~1900μs 判 1,引导码单独判 9ms 和 4.5ms。

3.2 状态机比一堆 if 可靠

解码最容易翻车的地方是把判断写成嵌套 if,一旦丢一位后面全乱。用状态机把“等引导码 → 收 32 位 → 校验”拆开,丢位就复位回初始态,不会把半截数据当有效码。

typedef enum { IR_IDLE, IR_LEAD, IR_DATA, IR_DONE } ir_state_t; volatile ir_state_t ir_state = IR_IDLE; volatile uint32_t ir_code = 0; volatile uint8_t ir_bit_cnt = 0; static uint16_t last_cap = 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) { uint16_t now = TIM_GetCapture1(TIM2); uint16_t dt = now - last_cap; // 无符号相减,自动处理溢出 last_cap = now; switch (ir_state) { case IR_IDLE: if (dt > 8000 && dt < 10000) ir_state = IR_LEAD; // 9ms引导 break; case IR_LEAD: if (dt > 4000 && dt < 5000) { // 4.5ms ir_state = IR_DATA; ir_bit_cnt = 0; ir_code = 0; } else ir_state = IR_IDLE; break; case IR_DATA: if (dt > 400 && dt < 800) { // 位起始560us // 等下一个间隔判0/1,这里先占位 } else if (dt > 1400 && dt < 1900) { // 逻辑1 ir_code = (ir_code << 1) | 1; ir_bit_cnt++; } else if (dt > 400 && dt < 800) { // 逻辑0 ir_code = ir_code << 1; ir_bit_cnt++; } else { ir_state = IR_IDLE; break; } if (ir_bit_cnt >= 32) { ir_state = IR_DONE; } break; default: break; } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }

上面这段是骨架,实际判 0/1 要结合“位起始低电平 + 后续高电平”两段间隔,不同接收头的时序有几十微秒偏差,阈值留足余量。dt用无符号 16 位相减,计数器溢出也能算对,这是很多人忽略的细节。解出 32 位后取低 8 位做键值,再和地址码比对,防止别的遥控器误触发。

3.3 遥控键值到亮度档的映射

解出键值只是第一步,真正要落地的是“按一下加一档、长按连续调”。程序包里通常给的是单次按键映射,实际用起来手感一般。我一般加一个按键保持判断:同一个键值在 500ms 内重复出现,就进入连续调节,每次步进 5 档,松手停止。

// 遥控键值映射,返回亮度增量 int8_t Remote_To_Step(uint8_t key) { switch (key) { case 0x18: return +10; // 亮度+ case 0x52: return -10; // 亮度- case 0x45: return 0; // 开关,单独处理 default: return 0; } }

键值必须按你手上遥控器的实际解码结果改,不同厂家编码不一样,别照抄。映射表放在单独文件里,换遥控器只改这一处,不动解码逻辑。

4. 强电侧的避坑清单:五个真会烧东西的细节

4.1 过零检测光耦输出极性搞反

现象:灯完全不亮,或者一上电就全亮不可调。原因:过零检测电路用的光耦(常见 PC817 或 EL817)输出极性取决于接法,有的过零时输出低,有的输出高,程序里中断触发边沿配反了,定时器在错误的时刻启动。解决:用示波器看过零光耦输出和市电波形,确认过零点对应的是下降沿还是上升沿,把TIM_ICPolarity或外部中断的EXTI_Trigger改过来。没有示波器就用一个 LED 串电阻接在过零输出上,肉眼看过零瞬间的亮灭节奏。

4.2 触发脉宽过宽烧触发变压器

现象:调光模块用一会儿触发变压器发烫,严重时可控硅误触发。原因:TIM_Pulse设成了几百微秒甚至毫秒级,触发变压器初级长时间通流。解决:触发脉宽压到 20~50μs,用单脉冲模式保证只触发一次。改完用示波器量触发引脚,确认每次过零只有一个窄脉冲。

4.3 中断里做浮点运算导致过零丢失

现象:亮度调到中间某几档时灯闪,其他档正常。原因:过零中断里调用了带浮点的亮度映射函数,C8T6 没有 FPU,浮点运算耗时不定,偶尔超过下一个过零间隔。解决:亮度映射改成定点整数或者查表,中断里只做赋值和启动定时器。把Brightness_To_Delay里的乘除都用整型,提前算好表也行。

4.4 遥控和本地调光抢同一个亮度变量

现象:遥控调完亮度,按一下本地按键亮度跳回旧值。原因:两路输入各自维护一份亮度缓存,没有统一的状态源。解决:只保留一个全局brightness_level,遥控和按键都通过同一个Set_Brightness()函数改,函数内部做限幅和延迟重装。谁也别直接写延迟计数值。

4.5 强电和弱电共地没做隔离

现象:一接上 220V,单片机复位或者程序跑飞。原因:过零检测和可控硅触发没有光耦隔离,强电串进弱电地。解决:过零检测用光耦,触发用 MOC3021 这类光耦可控硅,强弱电地彻底分开,PCB 上爬电间距留够。这条是安全底线,不是优化项。

5. 验证与进阶:用示波器和分段测试把调光跑稳

调光程序写完,别直接接灯试。先断开 220V,只给单片机供电,用示波器看三路信号:过零检测输出、触发脉冲、遥控接收头输出。过零输出应该是 10ms 周期的方波,触发脉冲应该出现在过零之后的设定延迟处,脉宽几十微秒。这两路对齐了,再上强电。

上强电也分段来:先接一个白炽灯(阻性负载,对相位切割最友好),确认 0、50、100 三档亮度有明显区别且不闪;再换 LED 灯(容性/非线性负载),这时候可能出现低档闪烁或者关不死,原因是 LED 驱动电源在低导通角时维持不住。常见做法是把最低亮度档限制在 15%~20%,别追求“全黑到全亮”的线性范围。

测试项观察点合格标准
过零信号示波器看周期10ms±0.5ms,无毛刺
触发脉冲延迟与脉宽延迟随档位变化,脉宽 20~50μs
遥控解码串口打印键值连按 20 次无错码
白炽灯调光肉眼/照度计三档可区分,无频闪
LED 灯调光低档表现15% 以上不闪,关断彻底

进阶一点,可以把亮度档做成渐变而不是跳变:收到遥控加档后,用定时器每 20ms 把brightness_level加 1,直到目标档,这样调光有“呼吸”感,也不会因为一次大跨度改延迟导致灯闪。渐变过程同样只改全局亮度变量,延迟重装交给过零中断统一处理。

我自己的习惯是,每次改完映射函数或者中断优先级,都强制走一遍“断强电、看波形、接白炽灯、换 LED 灯”这四步,不跳步。强电项目没有后悔药,一次共地没做好,烧的不只是片子。希望这份拆解帮到你,把这份调光包跑成能用的东西,而不是躺在硬盘里的压缩包。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询