继电器数据回环

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

052.6.8 继电器数据回环

/*
 * 整体目的:把UART收到的每个字节原样发回发送方,实现单片机软件数据回环。
 * 整体设计:UART方式1以9600波特接收;硬件置RI后进入串口中断,程序读接收SBUF,
 *           再把同一字节写入发送SBUF,等待TI置位并清TI后结束本次回环。
 * 时钟依据:11.0592MHz晶振、定时器1为12T、SMOD=0时,TH1/TL1=0xFD对应9600波特。
 * 硬件路径:电脑TX→单片机P3.0/RXD→接收SBUF→recv→发送SBUF→P3.1/TXD→电脑RX。
 * 课程上下文:继电器原理讲解之后,本程序删去上一节的switch外设控制,只保留字节回显;
 *             文件中LED、蜂鸣器、继电器别名和两个发送函数是沿用工程时留下的未使用代码。
 */

#include "reg52.h"   /* 引入8052特殊功能寄存器声明,使程序能使用SCON、SBUF、RI、TI等名称。 */
#include <intrins.h>  /* 引入Keil C51内部函数声明,软件延时需要调用_nop_()。 */

typedef unsigned char uchar;  /* 为8位无符号字符型建立简写,可完整保存一个UART数据字节。 */
typedef unsigned int uint;    /* 为C51的16位无符号整型建立简写,用作毫秒延时参数。 */

uint count = 0;  /* 保留自上一工程的全局计数器;当前回环程序没有读取或修改它。 */
uchar recv;      /* 保存从接收SBUF读出的一个字节,随后把它原样写回发送SBUF。 */

sbit LED1 = P1^0;  /* 保留LED1的P1.0别名;当前回环逻辑不再控制LED。 */
sbit LED2 = P1^1;  /* 保留LED2的P1.1别名;当前程序未使用。 */
sbit LED3 = P1^2;  /* 保留LED3的P1.2别名;当前程序未使用。 */
sbit LED4 = P1^3;  /* 保留LED4的P1.3别名;当前程序未使用。 */
sbit BEEP = P1^6;  /* 保留蜂鸣器控制端P1.6别名;当前程序未使用。 */
sbit JDQ1 = P2^0;  /* 保留继电器模块控制端P2.0别名;当前程序未使用。 */

/*
 * 函数目的:执行由xms指定的毫秒级软件忙等待。
 * 时间前提:计数值按11.0592MHz和当前Keil C51指令耗时生成;更改时钟或优化会影响精度。
 */
void Delay_xms(uint xms)                  /* 定义软件延时函数,xms表示计划等待的毫秒数。 */
{                                        /* 延时函数体开始,忙等期间CPU仍可被已允许的串口中断打断。 */
    uchar data i, j;                      /* 定义两个8位内层计数器,并放入片内直接寻址data RAM。 */

    while (xms)                           /* 每完成一轮约1ms等待,就把剩余毫秒数减1。 */
    {                                     /* 一毫秒延时循环开始。 */
        _nop_();                           /* 执行空操作指令,用于补齐延时生成器计算的时间。 */
        i = 2;                             /* 初始化外层8位计数器。 */
        j = 199;                           /* 初始化内层计数器,其递减过程构成主要延时。 */
        do                                /* 使用后判断循环,保证循环体至少执行一次。 */
        {                                 /* do循环体开始,此处只消耗CPU时间。 */
            while (--j);                  /* 先递减j,非0便继续空转;分号表示循环体为空。 */
        } while (--i);                    /* 一组j循环完成后递减i,非0时继续下一组。 */
        xms--;                             /* 一轮约1ms结束,将剩余等待次数减1。 */
    }                                     /* xms归零后退出延时。 */
}                                         /* 延时函数结束并返回调用者。 */

/* 函数目的:保留通用的单字节查询发送接口;本节ISR直接执行了同样的三步。 */
void UART_Send_Byte(uchar send_byte)       /* send_byte保存待发送的8位数据。 */
{                                         /* 单字节发送函数体开始。 */
    SBUF = send_byte;                      /* 写发送SBUF,启动UART硬件输出一帧。 */
    while (TI == 0);                       /* TI为0时原地等待;分号表示空循环。 */
    TI = 0;                                /* 硬件置位TI后由软件清0,为下一帧做准备。 */
}                                         /* 当前字节发送到规定时刻,函数返回。 */

/* 函数目的:保留通用字符串发送接口;当前回环只回送一个字节,没有调用该函数。 */
void UART_Send_Str(uchar *send_str)        /* send_str指向待发送C字符串的首字符。 */
{                                         /* 字符串发送函数体开始。 */
    while (*send_str != '\0')              /* 当前字符不是NUL结束标志时继续循环。 */
    {                                     /* 当前正文字符的发送分支开始。 */
        UART_Send_Byte(*send_str++);       /* 发送当前字符,再把指针移动到下一个字符。 */
    }                                     /* 遇到'\0'时结束,不发送结束标志本身。 */
}                                         /* 字符串发送完成并返回。 */

/* 函数目的:配置UART及波特率发生器,并保持前台运行以等待串口中断。 */
void main()                                /* 定义Keil C51程序入口。 */
{                                         /* 主函数体开始。 */
    SCON = 0x50;                           /* 选择UART方式1、允许接收,并把TI和RI初始化为0。 */
    PCON &= 0x7F;                          /* 清SMOD选择波特率不加倍,同时保留PCON其他位。 */
    TMOD &= 0x0F;                          /* 清除定时器1的高4位配置,保留定时器0低4位。 */
    TMOD |= 0x20;                          /* 设置定时器1为内部定时、模式2自动重装。 */
    TH1 = 0xFD;                            /* 保存自动重装值253,用于生成9600波特时基。 */
    TL1 = 0xFD;                            /* 设置第一次计数初值,使首轮周期与后续一致。 */
    TR1 = 1;                               /* 启动定时器1,开始给UART提供波特率时钟。 */
    ES = 1;                                /* 打开串口中断分开关,使RI或TI能提出中断请求。 */
    EA = 1;                                /* 打开CPU总中断门控,使串口请求可以被响应。 */

    while (1)                              /* 前台程序永久运行,回环工作由中断异步完成。 */
    {                                     /* 主循环体开始;这里没有主动发送或外设控制任务。 */
        Delay_xms(1000);                   /* 忙等待约1秒;串口中断仍可打断延时并完成回环。 */
    }                                     /* 延时结束后开始下一轮,持续保持程序运行。 */
}                                         /* 主函数语法边界;无限循环使其不会正常返回。 */

/*
 * 函数目的:接收一个字节后立即原样发送,实现软件回环。
 * 缓冲器区别:读SBUF选择内部接收缓冲器,写同名SBUF选择另一个发送缓冲器。
 * 执行代价:本例在中断内阻塞等待整帧发完,适合低速教学;复杂程序宜改用非阻塞发送。
 */
void UART_Routine(void) interrupt 4        /* 声明串口中断服务函数,对应向量地址0x0023。 */
{                                          /* 中断处理开始,TI和RI共用这个入口。 */
    if (RI == 1)                           /* 只在接收完成标志置位时执行回环,避免误读旧数据。 */
    {                                      /* 本次接收字节的回环分支开始。 */
        RI = 0;                            /* 按配套源码顺序先由软件清RI,为下一帧接收做准备。 */
        recv = SBUF;                       /* 读取接收缓冲器,把发送方送来的8位数据保存到recv。 */
        SBUF = recv;                       /* 把同一字节写入发送缓冲器,立即启动返回方向的一帧。 */
        while (TI == 0);                   /* 在中断中等待发送完成标志,保证回送字节已经推进完成。 */
        TI = 0;                            /* 软件清除TI,撤销发送请求并为下次回环准备。 */
    }                                      /* 当前接收事件处理结束;RI为0时不执行任何回送。 */
}                                          /* 中断服务程序结束并返回被打断的主循环。 */

一、继电器

1.1 继电器的原理

继电器本质上是一个由电信号控制的机械开关。单片机输出的是低压、小电流控制信号,继电器触点负责接通或切断另一条负载回路,因此可用小信号控制单片机引脚不能直接驱动的负载。

继电器模块应分成三个层次理解:

  1. 控制信号层:单片机的 P2.0 接模块的 IN,决定模块是否触发。
  2. 驱动与线圈层:模块上的限流、反相或放大电路驱动继电器线圈;单片机引脚不直接承担线圈电流。
  3. 触点与负载层:COM、NO、NC 是一组机械触点,它们接在独立的负载回路中。

视频中的单路模块采用低电平触发,功能链路如下。模块的具体晶体管接法可能随型号变化,但作用关系相同:

控制侧(低压)

单片机 P2.0
      │
      └── IN ──> 输入限流/反相电路 ──> 线圈驱动管 ──> 继电器线圈
                                              │            │
                                              ├─ 触发指示灯 │
                                              └────续流保护─┘

模块 VCC ──> 线圈和驱动电路供电
模块 GND ──> 与单片机 GND 相连,作为控制信号的共同参考点
  • 输入电阻限制 IN 端电流,保护单片机和驱动器件。
  • 驱动管完成电流放大或电平反相。继电器线圈通常需要的电流高于GPIO适合直接提供的电流,所以不能把裸继电器线圈直接接到 P2.0。
  • 续流二极管通常反向并在线圈两端。线圈断电时会产生反向感应电压,二极管为线圈电流提供衰减通路,避免高压尖峰损坏驱动管。
  • 电源指示灯只说明模块已经得到工作电源;触发指示灯说明驱动端进入触发状态。指示灯亮或听到吸合声,并不能完全证明外部负载回路接线正确、触点也一定完好。

视频所用模块的触发关系为:

P2.0/IN状态模块状态线圈状态导通触点
低电平,约 0~1.2 V触发得电吸合COM—NO
高电平未触发失电释放COM—NC

这里的 0~1.2 V 是视频中这块模块标示的低电平范围,不是所有继电器的统一参数。有些模块是高电平触发,有些带光耦或 JD-VCC 独立线圈电源,接线前应以模块丝印和说明书为准。

代码中的 sbit JDQ1 = P2^0; 只是给 P2.0 起了一个便于理解的名字。本讲的数据回环程序没有再给 JDQ1 赋值,因此这段程序本身不执行继电器开关动作。

1.1.1 电磁继电器百度百科

电磁继电器的核心部件包括线圈、铁芯、衔铁、复位弹簧和触点:

线圈失电(释放)                       线圈得电(吸合)

       复位弹簧                              电磁吸力
          ↓                                     ↓
NC ●──────● COM      NO ●              NC ●      ●──────● NO
         动触点                                   动触点
                                                COM

结果:COM 与 NC 导通                    结果:COM 与 NO 导通

工作过程可以分为四步:

  1. 线圈通电后产生磁场,铁芯成为电磁铁。
  2. 电磁力吸引衔铁,使与衔铁相连的动触点发生位移。
  3. 动触点离开 NC,转而接触 NO,负载回路随之切换。
  4. 线圈断电后磁力消失,复位弹簧使衔铁回到静止位置,COM 再次接回 NC。

“常开”和“常闭”中的“常”,是指线圈未通电、继电器未吸合的常态,不是指单片机输出高电平或低电平:

触点名称线圈未得电线圈得电
NO(Normally Open,常开)与 COM 断开与 COM 导通
NC(Normally Closed,常闭)与 COM 导通与 COM 断开
COM(Common,公共端)接向 NC切换到 NO

电磁继电器的线圈侧和机械触点侧在器件内部没有直接导线连接,因而能够实现控制回路与负载回路之间的电气隔离。不过,“继电器有隔离”不等于整块模块在任何条件下都绝对安全:模块是否带光耦、控制电源是否真正分开、PCB爬电距离和外壳防护是否合格,都要单独判断。

1.1.2 继电器插图

视频中的蓝色接线端子标有 NO、COM 和 NC。辨认端子时必须看模块丝印,不能只凭“中间一定是COM”来判断,因为不同模块的端子排列可能不同。

用低压灯理解负载侧接法

对初学者可先用安全的直流低压灯或LED灯模块验证,不要直接进行220V市电实验。例如希望继电器吸合后灯才亮,可使用 NO:

12 V 电源正极 ── 保险丝 ── COM
                              \
                               \  线圈吸合后,COM 与 NO 接通
                                NO ── 12 V 灯 ── 12 V 电源负极

单片机侧:P2.0 ── IN      继电器模块:VCC、GND
负载侧:  12 V电源与灯    只经过COM、NO触点

此时的电流通路为:

线圈未吸合:COM 与 NO 断开 → 负载回路无电流 → 灯灭
线圈已吸合:12V+ → 保险丝 → COM → NO → 灯 → 12V- → 灯亮

如果希望设备在继电器未触发时默认工作,触发后停止,则把负载回路改用 COM 和 NC。COM 只是机械开关的公共触点,并不天然等于电源正极、负极或地;它接什么电位由负载回路的设计决定。

控制侧与负载侧的边界

flowchart LR
    A[51单片机 P2.0] -->|低电平触发| B[模块输入与驱动电路]
    B --> C[继电器线圈]
    C -.磁力推动.-> D[机械触点 COM/NO/NC]
    E[独立的负载电源] --> D
    D --> F[灯或其他负载]
    F --> E

虚线表示磁力造成的机械动作,不是控制侧和负载侧之间的导线。继电器适合控制灯、风扇、电磁阀等负载,但选择时必须同时检查:

  • 线圈额定电压是否与模块供电匹配;
  • 触点的交流/直流电压、电流和负载类型是否在额定范围内;
  • 电机、灯丝等负载的启动电流可能远高于稳定工作电流;
  • 继电器有机械寿命和电气寿命,不适合无节制地高速开关。

安全提示: 220V市电可能造成致命触电和火灾。初学阶段只做隔离的低压直流实验。市电控制还涉及保险、接地、阻燃外壳、线径、爬电距离和接线端防触碰等要求,不应仅凭一块裸露继电器模块搭接。

二、数据回环

2.1 继续上节代码

数据回环是指:单片机从串口收到一个字节后,不改变其数值,立即把同一个字节从串口发回。完整路径是:

电脑串口助手
    │ 发送
    ▼
USB转TTL的 TXD ──> 单片机 RXD(P3.0)
                         │
                         ▼
                  接收移位寄存器
                         │ 收满1帧,RI=1
                         ▼
                       SBUF
                         │ 串口中断读取
                         ▼
                       recv
                         │ 再写入SBUF
                         ▼
                  发送移位寄存器
                         │ 发送完成,TI=1
                         ▼
USB转TTL的 RXD <── 单片机 TXD(P3.1)
    │ 接收
    ▼
电脑串口助手显示回传数据

初始化寄存器的作用如下:

语句关键位作用
SCON = 0x50SM0=0, SM1=1串口方式1,8位UART,可变波特率
SCON = 0x50REN=1允许串口接收
PCON &= 0x7FSMOD=0波特率不加倍
TMOD &= 0x0F清除高4位清除定时器1原来的模式设置,不影响定时器0
`TMOD= 0x20`M1=1, M0=0
TH1 = TL1 = 0xFD重装值为253配合11.0592 MHz晶振产生9600 bit/s
TR1 = 1TCON.TR1=1启动定时器1
ES = 1IE.ES=1允许串口中断
EA = 1IE.EA=1打开总中断开关

在串口方式1、SMOD=0、定时器1方式2时:

$ \text{Baud}=\frac{2^{SMOD}}{32}\times\frac{f_{osc}}{12\times(256-TH1)} $

代入 fosc = 11.0592 MHz、TH1 = 0xFD = 253:

$ \text{Baud}=\frac{1}{32}\times\frac{11059200}{12\times(256-253)}=9600\ \text{bit/s} $

串口中断入口 interrupt 4 由接收标志 RI 和发送标志 TI 共用。本程序的回环过程为:

/*
 * 片段目的:完整展示一次“收到什么就发回什么”的字节级软件回环。
 * 使用前提:UART已允许接收并开启串口中断,收发双方的波特率和帧格式一致。
 */
if (RI == 1)         /* RI为1说明接收SBUF中已有一个新字节。 */
{                    /* 本字节回环处理开始。 */
    RI = 0;          /* 软件清除接收完成标志,为下一帧重新置位做准备。 */
    recv = SBUF;     /* 读取刚收到的1字节;读操作选择内部接收缓冲器。 */
    SBUF = recv;     /* 把同一字节交给发送缓冲器,启动回送帧。 */
    while (TI == 0); /* TI仍为0时阻塞等待,避免过早结束本次发送处理。 */
    TI = 0;          /* 软件清除发送完成标志,为下一次回送做准备。 */
}                    /* 一个字节的接收、转存和回送全部完成。 */

SBUF在程序中只有一个名字,但硬件内部的接收缓冲器和发送缓冲器是分开的:读 SBUF 取得接收数据,写 SBUF 启动发送。RI、TI 都必须由软件清零。

按照常见的 8N1 格式,一个字节在线路上包含1个起始位、8个数据位和1个停止位,共10位。9600 bit/s时传完一帧约需:

$ t_{byte}=\frac{10}{9600}\approx1.04\ \text{ms} $

先接收一帧、再回发一帧,纯线路往返时间至少约为 2.08 ms,还要叠加中断响应、USB转换和串口助手显示延迟。

本例为了直观看到回环,在串口中断里一直等待 TI=1,约占用1 ms。低速、少量数据时可以工作,但工程中通常要求中断服务尽快结束;连续高速接收时,长时间停留在中断中可能造成后续字节来不及处理。更稳妥的方式是中断中只读取 SBUF 并放入缓冲区,再由主循环或发送中断逐字节发送。

还要注意,当前的 UART_Send_Byte()、UART_Send_Str() 没有被调用。如果以后在 ES=1 时从主程序使用轮询发送,发送完成也会触发串口中断;届时中断函数必须同时判断并处理 TI,否则可能反复进入串口中断。应统一采用“轮询发送”或“发送中断”中的一种完整策略。

2.2 程序文件打开

下载程序前要先关闭串口助手占用的COM口。一个串口通常不能同时被下载工具和串口助手独占打开;若提示端口打开失败或下载无响应,应先检查串口助手是否仍处于“打开”状态。

经过单片机的数据回环测试

  1. 下载程序后,打开对应COM口,设置为 9600、8位数据位、无校验、1位停止位。
  2. 串口助手的发送区和接收区都选择 ASCII,发送字符 a,接收区也应显示 a。
  3. 两边都选择 HEX,发送 61,接收区也应显示 61。因为ASCII字符 a 的字节值就是十六进制 0x61。
  4. 如果一边选ASCII、另一边选HEX,同一个字节会显示成不同外观;数据并没有因此改变。

视频所用软件以蓝色显示发送、绿色显示接收,这是该串口助手的界面规则,不是串口协议规定的颜色。

USB转TTL自身的硬件回环测试

把USB转TTL模块的 TXD 与 RXD 短接,不连接单片机,即可检查转换模块的基本收发通路:

电脑应用 ──USB──> USB转TTL发送器 ── TXD
                                      │
                                      └──短接线──> RXD
                                                   │
电脑应用 <─USB──── USB转TTL接收器 <────────────────┘

操作时应先确认引脚丝印,只短接 TXD 和 RXD,不要把电源脚与地脚短接。选择正确COM口并打开后,发送任意数据,若接收区出现完全相同的字节,说明USB枚举与驱动、串口助手、转换芯片的基本发送端和接收端大体正常。

在这种自身回环中,没有第二个独立设备使用另一套时钟来解释数据;同一个USB转TTL转换器按照当前配置发送,又按照同一配置接收,所以可在芯片支持范围内换用多种波特率进行测试。波特率仍然参与UART收发,并不是“回环时不需要波特率”。一旦连接单片机或其他串口设备,双方的波特率、数据位、校验位和停止位必须一致。

自身回环成功只能说明转换模块和电脑侧链路基本正常,不能证明以下部分没有问题:

  • 单片机程序与晶振频率;
  • 单片机 TXD/RXD 的交叉接线和共地;
  • 3.3 V、5 V等逻辑电平是否兼容;
  • 外部设备的波特率和帧格式;
  • 继电器模块及其负载回路。

因此排查串口故障时,可先做USB转TTL自身回环,再接入单片机测试。这样能把问题逐步缩小到电脑侧、转换模块、接线或单片机程序中的某一部分。