51单片机双向可控硅移相控制:调光调温实战与源程序解析

发布时间:2026/10/27 7:38:33
51单片机双向可控硅移相控制:调光调温实战与源程序解析

简介:一套面向单片机学习者的可控硅移相控制交流电实验资料,围绕220V市电过零检测与延时导通原理展开,说明如何在每次过零点后调整延时时间从而改变负载电压,并对比RC充放电直接触发与MOC3021光耦隔离触发两种方式下可控硅工作象限的差异。压缩包内含49个文件,以C源程序、Keil工程文件、hex烧录文件、原理图SchDoc和电路截图为主,另有若干演示zip包、外部中断上下沿测试程序与调试日志,整体约627KB,目录清晰便于按需查阅。已有179人学习下载。读者可获得可编译烧录的完整源程序、电路原理图与工程备份,既能对照理解移相触发相位控制的核心逻辑,也可直接借鉴到白炽灯无级调光、小功率电机调速等实际项目中作为改造基础。

1. 单片机可控硅移相控制交流电:能把调光调温一次做对

用单片机控制可控硅去切交流电,是做调光、调温、电机调速绕不开的路子。翻过车的人都知道,网上流传的源程序大多只给一个触发延时函数,过零检测写得稀碎,照着接上 BT134 之后不是打火花就是乱闪烁。这套资料难得在把原理和源程序放在一起讲,过零检测、移相延时、触发脉冲输出是一条完整链路,不是零散代码段。适合用 51/STC 系列单片机做调光器、调压器、电热毯控制器的人,也适合准备用 Proteus 仿真验证移相时序的学生。读完你能照着把电路搭出来,把代码烧进去,也知道示波器上该看哪些波形、低亮度区为什么会闪。

2. 先把原理吃透:双向可控硅怎么把交流电“切”出渐变量

2.1 双向可控硅的导通条件与四象限触发

先回答一个最基础的问题:双向可控硅凭什么能控制交流电?因为它是一个半控器件。门极只要拿到一个足够宽度的触发电流,可控硅就导通,导通之后门极就失去控制能力,你没法用撤掉触发的方式关断它,要等流过主回路的电流降到维持电流 IH 以下,器件才会自己关断。交流电有个天然优势:50Hz 正弦波每个周期要过零两次,电流每 10ms 归零一次,双向可控硅跟着自动关断,于是你只需要在每个半周期重新给它一个触发脉冲。

这个特性决定了移相控制的全部动作。以 BT134 为例,引脚排列是 MT1、MT2、G,触发电流的流向决定了器件工作在第几象限:MT2 为正、G 为正属于第 I 象限,MT2 为正、G 为负属于第 II 象限,MT2 为负、G 为负属于第 III 象限,MT2 为负、G 为正属于第 IV 象限。BT134 和 BCR1AM 都是三象限器件,只支持 I、II、III,第四象限不被允许。实际电路里最常见的用法是第 I 和第 III 象限对称触发:正半周 MT2 为正时给 G 正脉冲,负半周 MT2 为负时给 G 负脉冲,两半周导通角一致,负载上才不会叠加直流分量。

这里有个高频误判点:很多人把双向可控硅当成两个单向可控硅并联,觉得一个周期触发一次就够。实际不是这样。可控硅每半周期过零关断一次,如果你只在正半周触发,负半周就完全不导通,负载两端的波形变成半波整流效果,变压器类负载会发出明显的嗡嗡声,铁芯发热,严重的时候直接把负载烧掉。所以源程序里的过零中断必须无条件响应,正负半周一个都不能漏。判断代码是否合格的简单方法,就是数一数一秒内触发脉冲的数量:50Hz 交流电,双向可控硅应该每秒触发 100 次,每个半周期一次。

2.2 移相控制的量化:触发角与延时时间的换算

移相控制的核心参数是触发角 α。交流电频率 50Hz,一个完整周期 20ms,半周期 10ms 对应 180° 电角度。触发角表示过零之后你决定等待多久才给触发脉冲,延时时间 t = α / 180 × 10ms。α 等于 0° 表示过零瞬间立即触发,可控硅整个半周期都导通,负载上接近完整正弦波;α 等于 90° 时延时 5ms,输出只剩半周期的后半段,有效值明显下降;α 接近 180° 时,延时接近 10ms,可控硅几乎不导通,输出趋近于零。

源程序里用一个 8 位变量来存触发角,范围 0~255,线性映射到 0~180°。换算下来,每个 LSB 对应约 0.7° 导通角,半周期 10ms 对应 10000μs,触发角每变化 1 个 LSB,延时变化约 39.2μs。这个分辨率对调光、调温、小功率电机调速都足够用,没必要上更高精度的 PWM 思路。真正需要注意的是市电频率的漂移:电网频率一般稳定在 50Hz±0.2Hz 以内,对应半周期误差几十微秒,对移相控制没有实质影响,不需要做频率跟踪。

还有一个新手常踩的概念坑:触发角不等于导通角。触发角是“过零后等到什么时候触发”,导通角是“可控硅实际导通的时间占比”。对于电阻性负载,导通角等于 180° 减去触发角;对于电感性负载,由于电流滞后电压,实际导通角会比这个值大一点,触发角越大偏差越明显。这也是为什么感性负载的移相控制调试起来比阻性负载玄学,后面硬件章节会专门说 RC 吸收和维持电流的问题。

2.3 源程序里的控制变量分层设计

看过不少移相控制的工程,我认为源程序里最值得学的不是某个计算公式,而是变量分层。标准的写法是三层:主循环层、过零中断层、定时器中断层。主循环只负责根据需求更新一个全局触发角变量,比如按键调亮、温度传感器 PID 运算,这些慢速任务都在这里做。过零中断是硬实时入口,市电每 10ms 来一次,它负责把触发角换算好的定时器初值装入定时器,然后启动定时器。定时器中断负责在延时到达的时刻把触发引脚拉高,输出一个固定宽度的脉冲。

这个分层的意义在于把慢逻辑和硬实时彻底隔离。我见过初学者把过零检测写成轮询:主循环死等过零引脚变化,再用 delay 函数延时不精确的微秒数,最后触发。纯电阻负载、没有显示任务的情况下能跑,一旦主循环里加了数码管刷新、按键扫描、传感器滤波,循环周期就不确定,过零信号经常被漏掉,输出波形乱成一团。采用中断驱动之后,主循环跑多久都无所谓,过零到了自然会打断它。

从源程序的角度看,你只需要关注三个东西:过零中断服务函数、定时器初值换算函数、定时器中断服务函数。把这三个函数调通,移相控制的主干就通了,剩下的按键、显示、保护逻辑都是在这个骨架上加肉。下一章我就按这个顺序把三个函数逐个拆开讲。

3. 源程序解析:过零中断到定时器触发的完整链路

3.1 过零检测:用外部中断捕获市电过零点

过零检测电路的常见做法是:交流火线经过两个 100kΩ 电阻降压限流,接到光耦的输入侧,光耦输出侧接单片机的外部中断引脚,输出引脚加上拉电阻。交流电压过零时光耦的发光二极管两端电压接近零,输出侧关断,引脚电平翻转,触发 INT0 中断。这套电路没有变压器隔离,但光耦本身提供了强弱电隔离,单片机侧是安全的,前提是 PCB 上强弱电间距留够。

在 51 系列单片机里,INT0 中断服务函数要做得尽量短,只做定时器初值装载和启动,不做任何运算。触发角的换算放在主循环或定时器中断里提前算好。

// 过零检测信号接入 INT0 (P3.2),下降沿触发 void Init_ZeroCross() { IT0 = 1; // INT0 下降沿触发 EX0 = 1; // 开启 INT0 中断 EA = 1; // 开启总中断 } // INT0 中断服务函数:过零到达,启动移相定时器 void ZeroCross_ISR() interrupt 0 { TR0 = 0; // 先停定时器,清掉上一半周期未完成的延时 TH0 = TriggerHigh; // 装载触发角对应的定时器初值高 8 位 TL0 = TriggerLow; // 装载触发角对应的定时器初值低 8 位 TR0 = 1; // 启动定时器 0,开始移相延时 }

代码里最关键的一行是进入中断后先TR0 = 0。过零信号每 10ms 一次,如果上一个半周期触发角接近 180°,延时还没跑完下一次过零就到了,这时不先把定时器停掉,旧延时和新初值会混在一起,输出波形直接错位。TH0、TL0 这两个变量由主循环预先算好,中断里只做赋值,不做乘法除法,因为 C51 的 8 位除法会占用几十个机器周期,放在中断里会让过零响应产生不可忽略的抖动。

关于 INT0 的触发方式,边沿触发在进入中断后由硬件自动清标志,不需要软件干预;如果用了低电平触发,中断服务函数里必须显式清标志,否则会反复进中断,把定时器启动逻辑彻底打乱。源程序一般用下降沿触发,配合光耦输出端的信号特性,正好是过零瞬间的一次边沿。

3.2 定时器移相延时的初值计算

定时器 0 工作在方式 1,16 位计数模式。以 12MHz 晶振、标准 12T 的 STC89C52 为例,机器周期等于 12 个时钟周期,定时器每 1μs 计数一次。半周期 10ms 就是 10000 个计数单位,16 位定时器最大能数到 65536,余量充足。触发角 α 对应的延时时间 t = α / 180 × 10000μs,放到代码里就是 0~255 的变量线性映射到 0~10000μs。

#define FOSC 12000000UL // 晶振 12MHz,STC89C52 标准 12T 模式 #define TIMER_CLK (FOSC / 12) // 机器周期 1us 一次计数 // 计算定时器初值:触发角 0~255 映射到 0~10000us unsigned int CalcTimerValue(unsigned char angle) { unsigned long delay_us; unsigned int timer_value; delay_us = (unsigned long)angle * 10000UL / 255; // 延时微秒数 timer_value = 65536UL - delay_us; // 16 位定时器初值 return (unsigned int)timer_value; }

换算逻辑是这样的:angle 最大 255,乘 10000 后是 2550000,用 unsigned long 存不会溢出;除以 255 得到 0~10000 的微秒数;再用 65536 减去它,得到定时器要计多少个数才溢出。比如 angle 等于 128,延时约 5019μs,初值就是 65536 − 5019 = 60517。

这里有一个必须提醒的细节:如果你用的晶振是 11.0592MHz,机器周期是 12 / 11.0592 ≈ 1.0851μs,延时计算就不是整数,10000UL这个常量就不成立,需要按实际机器周期换算,或者干脆用浮点算初值再取整。还有一类芯片要格外小心:STC15 系列是 1T 模式,同样的 12MHz 晶振下机器周期只有 1/12μs,同一段代码的定时器初值会差 12 倍,第 5 章避坑里我会单独讲。

3.3 触发脉冲输出:宽度、位置与正负半周一致性

定时器计数溢出后进入定时器中断,此时触发角到了,需要把触发引脚拉高。但拉高不能保持太久,双向可控硅一旦导通门极就失效了,持续给门极电流没有意义,还会增加光耦和限流电阻的功耗,所以用一个短脉冲,宽度控制在 50~200μs 之间。

// 定时器 0 中断:到达触发角,输出触发脉冲 void Timer0_ISR() interrupt 1 { static unsigned char pulse_cnt; TR0 = 0; // 移相延时结束,停掉定时器 TRIAC_PIN = 1; // 触发引脚拉高,门极获得触发电流 // 软件延时约 100us,然后关断触发脉冲 for (pulse_cnt = 0; pulse_cnt < 12; pulse_cnt++); TRIAC_PIN = 0; // 触发脉冲结束,等待下一个过零 }

pulse_cnt 循环 12 次在 12MHz 晶振下大约 100μs,但这个值跟编译器的优化等级、指令执行周期有关,严格一点应该用示波器实测。触发脉冲太短的问题在纯电阻负载上不明显,白炽灯电流大,可控硅一旦触发导通就稳了;但换成电机、变压器这类感性负载,触发瞬间电流上升慢,脉冲太短撤掉时可控硅还没进入完全导通状态,会出现触发失败或半导通,表现为电机哼叫、转速不稳定。

还要注意正负半周的一致性。双向可控硅在正半周用第 I 象限触发、负半周用第 III 象限触发,两边的触发脉冲必须对称。程序中过零中断是通用入口,不管正负半周都做同样的动作,天然对称;但如果有人在源程序里为了省事加了半周判断,只在正半周触发,负半周靠可控硅的特性自己维持,那就是给自己埋雷。触发引脚在 P1^0,对应硬件连接是通过 MOC3021 驱动主回路可控硅的门极,这部分在硬件章节具体展开。

4. 硬件电路搭建:器件选型、触发回路与 RC 吸收

4.1 BT134、BCR1AM 怎么选:参数对比表

主回路器件就是双向可控硅,选型看三个硬指标:额定电流、耐压、门极触发电流。BT134 和 BCR1AM 是这份源程序里最常用的两只管子,参数差别挺大,对应用场景也不同,我列了一张实际选型时直接对照的表。

参数BT134-600EBCR1AM
额定电流4A1A
耐压600V 级600V 级,按后缀区分
门极触发电流约 5~10mA约 1~3mA
封装SOT-78(TO-220)TO-202
适用负载1000W 以下电热、调光小风扇、小功率调温

选型时至少留 1.5~2 倍电流裕量。4A 的 BT134 控制 800W 电暖器,负载电流大约 3.6A,已经逼近额定值,散热条件不好的话会热击穿;1A 的 BCR1AM 老老实实用在 100W 以下负载。很多照着电路图焊板子的人只盯型号,不盯封装引脚,BT134 和 BCR1AM 的引脚顺序完全不一样,前者从正面左到右是 MT1、MT2、G,后者引脚排列不同,焊反之后触发不了或者一上电就直通,这类问题用万用表二极管档量一遍就能避开。

4.2 触发回路:MOC3021 光耦与限流电阻怎么接

单片机 IO 口不能直接驱动主回路可控硅的门极,必须用光耦做强弱电隔离。移相控制要选 MOC3021 这种随机相位型光耦驱动器,它内部 LED 导通就让输出侧可控硅结构跟着导通,没有过零限制,适合任意相位触发。如果误用了 MOC3061 这类过零型光耦,它只允许在交流电压接近过零时输出,移相控制直接失效,输出波形变成每次都在过零附近乱触发,负载电压完全不可控。这个坑在实践中出现频率极高,我每次帮人看板子第一件事就是查光耦型号。

MOC3021 的接法:单片机 IO 口经过 330Ω~470Ω 限流电阻接到光耦输入 LED 的正极,LED 负极接 GND。输出侧是双向可控硅结构,一端接主回路可控硅 MT1,另一端经过 100Ω~220Ω 电阻接到 G 极,同时在输出侧并联一只 0.01μF 电容和电阻串联,限制触发电流的上升率。

// 触发引脚定义:接 MOC3021 输入侧 sbit TRIAC_PIN = P1^0; // 高电平点亮光耦 LED,触发主回路可控硅

主回路的接地布局值得专门强调:过零检测光耦的地、MOC3021 输入侧的地、单片机电源地要单点汇合,不要让强电部分的电流流过单片机的地线。可控硅导通关断瞬间的 dv/dt 很强,地线一旦形成环路,干扰就会串进过零检测,现象是过零中断乱触发、触发角忽大忽小、输出波形不规则抖动。我在第一版板子上吃过这个亏,后来把强电回路和弱电回路完全分开布地,问题才消失。

4.3 RC 吸收电路与感性负载的特殊处理

双向可控硅关断的瞬间,如果负载回路里有电感,电流不能突变,会在器件两端产生很高的电压变化率 dv/dt。dv/dt 超过器件允许值,可控硅会在没有门极触发的情况下自己导通,这就是误触发。抑制手段是在可控硅 MT1 和 MT2 之间并联 RC 吸收网络,用一个 47Ω~100Ω 的电阻串一个 0.01μF~0.1μF 的电容,电容耐压选 630V 以上,最好用无感电容。

RC 吸收的参数不是随便放的,电容太大有一个副作用:RC 回路的充放电电流可能超过可控硅的维持电流 IH,导致交流过零后可控硅不能可靠关断,输出一直维持在某个导通状态,表现为调低亮度时灯不灭,最小亮度降不下去。这时候要往小了调电容,往大了调电阻,让 RC 放电电流明显低于维持电流。RC 吸收的另一头连着触发回路,MOC3021 输出侧的 0.01μF 电容如果直接并接在 MT1 和 G 之间,也可能造成误触发,正确的接法是电容串一个电阻再接。

感性负载还有一个麻烦:变压器或电机的反电动势会让过零检测抓到的点和实际电压过零产生偏移。常见做法是在过零检测光耦输入端加 RC 滤波,输出端加施密特整形,把过零脉冲边沿的抖动滤掉。调试这类电路时示波器不能丢,同时看电网电压波形和可控硅两端波形,找到真实过零位置再谈触发角,靠万用表猜是调不出来的。

5. 单片机可控硅移相控制避坑:5 个最常见翻车点

移相控制这个东西,原理讲起来半小时,焊板子调试可能折腾两整天。下面五条踩坑记录我都实际遇到过,按过零检测、程序移植、干扰处理三个方向排开,每一条都按现象、原因、解决三步说清。

5.1 过零检测与触发环节的三个典型故障

现象一:输出波形只有半周,负载上叠加直流分量,变压器类负载嗡嗡响。

原因:过零中断只在上升沿或只在下降沿触发,或者代码里加了正负半周判断,导致负半周没有触发脉冲。

解决:把过零中断配置为边沿触发,并且让中断服务函数无条件装载初值、启动定时器,去掉任何“只在某一半周触发”的判断。用示波器数一秒内的触发脉冲数,50Hz 交流电应当每秒出现 100 个脉冲,少于这个数说明有半周被漏掉了。

现象二:白炽灯在低亮度区明显闪烁,像随机亮暗。

原因:触发角过大时导通角很小,负载电流接近可控硅的维持电流边界,导通不稳定;另一个常见原因是定时器初值计算用了 8 位乘法,低亮度区域的延时误差被放大。比如角度变量乘 10000 时,用 unsigned char 直接乘会发生溢出,初值错得离谱。

解决:程序里把最大触发角限制在 150°~160°,不要用满 180°;初值换算用 unsigned long 做中间量,把高字节和低字节分开装载。实测时逐点记录触发角对应的亮度,画出曲线,在非线性明显的区域单独做校正表,不要指望一路线性的映射覆盖全范围。

现象三:单片机还在复位,可控硅就已经全导通,负载直接满功率。

原因:单片机复位期间 IO 口呈高电平,MOC3021 输入侧 LED 被点亮,触发引脚还没被代码初始化就给出触发电流,可控硅在第一个半周期就导通。

解决:代码最开头先把触发引脚强制拉低,再初始化定时器和中断;硬件上在触发引脚到 GND 之间并一个 1kΩ 下拉电阻,复位期间分流掉光耦 LED 的电流。如果产品对启动安全要求高,还可以在过零中断里加一个软启动标志,前 200ms 只允许触发角从最大延时起步。

5.2 程序移植与干扰的两个坑

现象四:同一份源程序从 STC89C52 换到 STC15 系列,触发角完全乱套,输出波形像随机斩波。

原因:STC89C52 是 12T 模式,12MHz 晶振下定时器 1μs 计一次数;STC15 是 1T 模式,定时器以 12MHz 计数,同样的 10000 个计数单位实际只有 833μs,和理论延时差了 12 倍。Proteus 仿真里按理想时序跑没问题,实物一下台就翻车,因为仿真模型不区分 1T 和 12T 的差异。

解决:换芯片后要么把 STC15 的时钟配置为 12 分频,要么把定时器初值换算里的TIMER_CLK改成FOSC,重新计算。这个坑最隐蔽的地方在于:晶振没换、代码没改、负载没变,只有芯片换了,很多人会先怀疑可控硅和光耦,绕一大圈才发现是定时器时钟源的问题。

现象五:靠近主回路时单片机频繁复位,程序飞走。

原因:可控硅开关瞬间的 dv/dt 通过电源线或地线串入单片机复位电路,触发脉冲宽度不足时,可控硅在电流很小的点关断,di/dt 更大,干扰更强。还有一种隐蔽路径是触发引脚到 MOC3021 之间的连线太长,导线电感把干扰直接耦合进单片机。

解决:单片机电源处并 100μF 电解电容和 0.1μF 瓷片电容,复位引脚加 10μF 电容;过零检测光耦输出端到单片机之间串 1kΩ 电阻并对地并 10nF 电容,组成低通滤波;触发线和过零线尽量短,最好双绞。我不建议靠拉长延时来“躲”干扰,那是治标不治本,把干扰源的能量和耦合路径堵住才是正解。

6. 触发角标定与渐亮渐灭:输出波形的最后一公里

6.1 示波器标定:不靠眼睛靠波形

移相控制调到能亮、能灭,只算完成一半,另一半是让输出波形符合预期。我的标定习惯是把示波器探头接到负载两端,观察每个半周期被切掉的位置。触发角准确时,波形应该在每个半周期同一位置被切掉,后半段保留完整的正弦外形;如果切口位置在一个半周期内漂移,优先查过零检测抖动和触发脉冲宽度。把触发角固定在 30°、60°、90°、120°、150° 五个档位,逐个记录实际导通角,和理论值对比,误差超过 5° 就要回头查过零检测电路和定时器初值换算。

手头没有示波器时,可以用万用表测负载两端交流电压有效值,记录触发角 0~255 每个档位对应的电压,做一张校正表。主循环运行时查表输出,而不是实时计算延时,这样既省去浮点运算,又能避开非线性段的误差。这张表保存成 const 数组存在 Flash 里,不占 RAM,代码也简单。

6.2 渐亮渐灭:给触发角加一段软件斜坡

调光器最基本的使用体验是不能让灯“啪”地一下跳变。做法是在主循环里维护两个变量:当前触发角和目标触发角。主循环每跑一圈,把当前角度往目标方向移动 1~2 个 LSB,过零中断只读取当前角度对应的初值,不受主循环影响。

// 主循环中的渐亮渐灭逻辑 if (current_angle < target_angle) current_angle += 2; // 向目标角度逼近,每次约 0.7 度 else if (current_angle > target_angle) current_angle -= 2;

每次移动 2 个 LSB 大约对应 1.4° 导通角变化,主循环一圈几毫秒,整个渐变过程持续 2~3 秒,肉眼看起来就是线性渐亮。要缩短渐变时间就把步长改大,要更平滑就把步长改小,但不要小于 1,否则在低亮度区会停留太久,用户会以为卡住了。这个斜坡逻辑放在主循环里还有一个额外的好处:掉电保护也方便做,检测到断电时把目标角度直接归零,重新上电后从最大角度开始渐亮,避免每次通电都满功率冲击。

从那以后我每次焊完移相控制板,都强制走一遍固定动作:先量过零信号边沿,再量触发脉冲宽度,最后用示波器核对 90° 档位的导通角,三关全过才敢接负载。这套资料里的源程序和原理讲解正好可以照着做,省掉自己反复试错的时间,希望帮到你。

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