一、项目速览项目内容主控AT89C528052 内核8KB Flash / 256B RAM / 3×16 位定时器仿真晶振12 MHzProteus 中 MCU 的CLOCK属性开发环境Keil µVision Proteus 8资源占用code 884 bytesdata 24 bytesFlash 占用约 10.8%功能双向信号灯自动配时、双向秒级倒计时显示、三按键紧急干预整个系统的骨架只有三件事两个定时器各自维护一个方向的相位状态机一个显示函数用动态扫描把 4 位倒计时刷到数码管上一个按键扫描在需要时冻结时间、强制放行。二、硬件设计2.1 元件清单AT89C52× 1 —— 主控CRYSTALCAP× 2 —— 时钟电路CAP-ELECRES—— 上电复位电路RESPACK-8RP1——P0 口上拉排阻关键见 2.37SEG-MPX2-CC× 2 —— 两组两位共阴数码管合计 4 位LED-RED/LED-YELLOW/LED-GREEN× 各 2 —— 编号 D10 ~ D15BUTTON× 3 —— 紧急控制按键2.2 引脚分配sbit g1P1^0; sbit y1P1^1; sbit l1P1^2; // 东西向 绿/黄/红 sbit g2P1^3; sbit y2P1^4; sbit l2P1^5; // 南北向 绿/黄/红 #define ON 0 #define OFF 1引脚功能电平逻辑P1.0 / P1.1 / P1.2东西向 绿 / 黄 / 红低电平点亮P1.3 / P1.4 / P1.5南北向 绿 / 黄 / 红低电平点亮P0.0 ~ P0.7数码管段码 a~g、dp高电平点亮共阴P2.0 ~ P2.3数码管位选低电平选通P3.0东西方向强制放行按下为低P3.1南北方向强制放行按下为低P3.2恢复自动运行按下为低ON被定义成0—— LED 阳极经限流电阻接 VCC阴极接 IO 口靠单片机灌电流点亮。这是 51 的标准接法8051 的 IO 口拉电流能力只有几百微安灌电流却能到 20 mA 左右。2.3 为什么必须有 RP1 上拉排阻P0 口是准双向口中唯一的开漏结构内部没有上拉 FET。输出1时它只是高阻不是高电平驱动不了共阴数码管的段。所以 P0 作为段码口时必须外接 10kΩ 排阻上拉到 VCC。三、配时方案50 秒一个完整周期程序把一个信号周期切成四个相位。整理成表格是这样时间区间东西向南北向东西倒计时南北倒计时0 – 20 s 绿灯 红灯20 → 125 → 620 – 25 s 黄灯 红灯5 → 15 → 125 – 45 s 红灯 绿灯25 → 620 → 145 – 50 s 红灯 黄灯5 → 15 → 1四、代码剖析4.1 定时器初始化一次配置两个定时器TMOD 0x11; // 0001 0001 → T1、T0 均为方式 116 位定时 TH0 (65536-50000)/256; TL0 (65536-50000)%256; // 装入 15536还需 50000 个机器周期溢出 TH1 (65536-50000)/256; TL1 (65536-50000)%256; EA 1; ET0 1; ET1 1; // 开总中断 两个定时器中断 TR0 1; TR1 1; // 启动TMOD高 4 位管 T1、低 4 位管 T0写0x11就是一句话把两个定时器都配成方式 1。定时长度怎么算12 MHz 晶振 → 机器周期 12 / 12 MHz 1 μs 初值 65536 - 50000 15536 溢出所需 50000 × 1 μs 50 ms然后在中断里数 20 次凑成 1 秒t0; if (t0 % 20 0) { // 20 × 50 ms 1 s ... }于是t0每 1 代表 50 mst0从 0 数到 1000 正好是50 秒与配时周期完全对应。四个相位的边界400 / 500 / 1000换算过来就是 20 s / 25 s / 50 s。以东西向定时器 0为例void timer0() interrupt 1 { TH0 (65536-50000)/256; TL0 (65536-50000)%256; // 方式 1 不自动重装必须手动重载 t0; if (t0 % 20 0) // 每秒执行一次 { if (t0 400) // 绿灯期 { num--; if (num 0) { num 5; g1OFF; y1ON; l1OFF; } // 转黄 } if (t0 400 t0 500) // 黄灯期 { num--; if (num 0) { num 25; g1OFF; y1OFF; l1ON; } // 转红 } if (t0 500 t0 1000) // 红灯期 { num--; if (num 0) { num 20; g1ON; y1OFF; l1OFF; } // 转绿 if (t0 1000) t0 0; // 周期回绕 } a num/10; b num%10; // 拆成十位、个位供显示 } }4.3 数码管动态扫描uchar code table[] {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f, ...}; void display(uchar a, uchar b, uchar c, uchar d) { P0 table[a]; P2 0xfe; delay(1); P2 0xff; P0 table[b]; P2 0xfd; delay(1); P2 0xff; P0 table[c]; P2 0xfb; delay(1); P2 0xff; P0 table[d]; P2 0xf7; delay(1); P2 0xff; }table[]是共阴段码表0x3f 0011 1111点亮 a~f 六段正好显示0。扫描节奏是先送段码 → 再开位选 → 延时 → 关位选。注意这里有个容易被忽略的细节P0换段码的时候P2是0xff全部位选关闭所以不会出现上一位的余晖串到下一位上的鬼影。很多教材代码在这里图省事直接连续写 P0/P2结果数码管边缘发虚。delay(1)内层循环 110 次在 12 MHz 下约 1 ms四位扫完约 4 ms刷新率约 250 Hz远高于人眼临界闪烁频率视觉上是稳定常亮的。code关键字把段码表放进 FlashCODE段而非 RAM对只有 256 字节 RAM 的 8052 来说是必须的。4.4 按键与紧急放行三个按键接在 P3 低三位switch (temp) { case 0xfe: key 1; break; // P3.0 按下 → 东西放行 case 0xfd: key 2; break; // P3.1 按下 → 南北放行 case 0xfb: key 0; break; // P3.2 按下 → 恢复自动 }主循环中的处理if (key) { TR0 0; TR1 0; // 停表冻结时间 P0 table[9]; P2 0; // 四位全部选通显示 9999 if (key 1) P1 0xde; // 1101 1110 → 东西绿 南北红 if (key 2) P1 0xf3; // 1111 0011 → 东西红 南北绿 while (key) Keyscan(); // 死等直到按下 P3.2 ... TR0 1; TR1 1; // 恢复计时 }P1 0xde这种整字节直写的技巧值得学一下1101 1110bit00 点亮东西绿灯bit50 点亮南北红灯其余全灭一条指令搞定六个灯。五、运行截图