//Arduino智能交通灯演示程序
#include <SPI.h>           // 引入SPI通信库,用于串行外设接口通信
#include <LedControl.h>    // 引入MAX7219点阵屏控制库,用于驱动8x8点阵屏

/* ---------------------- 引脚定义 ---------------------- */
const int dataPin = A3;    // 数码管数据引脚(连接到移位寄存器的数据输入)
const int clockPin = A4;   // 数码管时钟引脚(控制数据移位时钟)
const int latchPin = A5;   // 数码管锁存引脚(锁存显示数据到寄存器)
int numToShow = 12;        // 倒计时显示数值(初始为红灯倒计时12秒)
int redLight = A0;         // 红灯引脚(连接红色LED)
int yellowLight = A1;      // 黄灯引脚(连接黄色LED)
int greenLight = A2;       // 绿灯引脚(连接绿色LED)
int buzzer = 2;            // 喇叭引脚(连接蜂鸣器)

// MAX7219点阵屏引脚定义(用于显示行人动画和禁止图标)
const int maxDinPin = 11;  // 点阵屏DIN引脚(串行数据输入)
const int maxCsPin = 10;   // 点阵屏CS引脚(片选信号)
const int maxClkPin = 13;  // 点阵屏CLK引脚(串行时钟)

// 创建MAX7219控制器对象(参数:DIN, CLK, CS, 设备数量)
LedControl lc = LedControl(maxDinPin, maxClkPin, maxCsPin, 1);

/* ---------------------- 点阵屏显示数据定义 ---------------------- */
// 行人走路动画帧(2帧循环,每帧8行数据,16进制表示点阵列)
const byte MAN_WALKING[2][8] = {
  {0x00, 0x1C, 0x1C, 0x1C, 0x28, 0x0A, 0x14, 0x22},   // 第1帧:左腿抬起(二进制0b00011100...)
  {0x00, 0x1C, 0x1C, 0x1C, 0x0A, 0x28, 0x08, 0x1C}    // 第2帧:右腿抬起(交换左右腿位置)
};

// 禁止通行图标(红色叉号,8行数据构成对角线交叉)
const byte NO_ENTRY[8] = {0x00,0x42,0x24,0x18,0x18,0x24,0x42,0x00};

// 数码管0-9显示编码(共阴数码管,LSB对应最低位LED)
const byte NUM[] = {    // 每个元素对应7段数码管的段码(D0-D6分别对应a-g段)
  0xFC,  // 0b11111100 → a/b/c/d/e/f段亮,g段灭(显示数字0)
  0x60,  // 0b01100000 → b/c段亮(显示数字1)
  0xDA,  // 0b11011010 → a/b/d/e/g段亮(显示数字2)
  0xF2,  // 0b11110010 → a/b/c/d/g段亮(显示数字3)
  0x66,  // 0b01100110 → b/c/f/g段亮(显示数字4)
  0xB6,  // 0b10110110 → a/c/d/f/g段亮(显示数字5)
  0xBE,  // 0b10111110 → a/c/d/e/f/g段亮(显示数字6)
  0xE0,  // 0b11100000 → a/b/c段亮(显示数字7)
  0xFE,  // 0b11111110 → 所有段亮(显示数字8)
  0xF6,  // 0b11110110 → a/b/c/d/f/g段亮(显示数字9)
  0x01   // 0b00000001 → 仅小数点亮(用于熄灭显示)
};

/* ---------------------- 全局变量定义 ---------------------- */
// 行人动画控制变量
unsigned long manTime = 0;       // 记录上一次动画更新时间
const unsigned long MAN_DELAY = 250; // 动画切换间隔(250毫秒)
int currentFrame = 0;            // 当前动画帧索引(0或1)

// 数码管动态扫描控制变量
unsigned long digitRefreshTime = 0; // 记录上一次数码管刷新时间
const unsigned long DIGIT_REFRESH_INTERVAL = 2; // 刷新间隔(2毫秒,确保视觉无闪烁)

void setup() {
  // 初始化数码管控制引脚为输出模式
  pinMode(dataPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(latchPin, OUTPUT);
  
  // 初始化交通灯引脚和蜂鸣器引脚为输出模式
  pinMode(redLight, OUTPUT);
  pinMode(yellowLight, OUTPUT);
  pinMode(greenLight, OUTPUT);
  pinMode(buzzer, OUTPUT);
  
  // 初始化为低电平(确保移位寄存器初始状态稳定)
  digitalWrite(clockPin, LOW);
  digitalWrite(latchPin, LOW);
  
  // 初始化MAX7219点阵屏
  lc.shutdown(0, false);      // 唤醒第0号设备(禁止掉电模式)
  lc.setIntensity(0, 8);      // 设置亮度等级(0-15,8为中等亮度)
  lc.clearDisplay(0);         // 清除点阵屏显示
  
  // 初始状态:启动蜂鸣器提示音,点亮红灯,显示禁止图标
  tone(buzzer, 200, 80);       // 发出200Hz声音,持续80毫秒
  digitalWrite(redLight, HIGH); // 点亮红灯
  showNoEntry();                // 显示禁止通行图标(点阵屏显示红色叉号)
  
  digitRefreshTime = millis(); // 初始化数码管刷新计时器
}

// 状态控制变量(用于记录当前激活的交通灯状态)
unsigned long digitTime = 0;   // 倒计时数值更新时间戳
unsigned long lightTime = 0;   // 灯光闪烁时间戳
unsigned long buzzerTime = 0;  // 蜂鸣器频率切换时间戳
bool On_Off = false;           // 灯光闪烁开关
bool buzzer_on_off = false;    // 蜂鸣器声音开关
bool red = true;               // 红灯激活状态(true表示当前为红灯模式)
bool green = false;            // 绿灯激活状态
bool yellow = false;           // 黄灯激活状态

int currentDigit = 0;          // 当前显示的数码管位(0为个位,1为十位)

void loop() {
  // 核心功能:定期刷新数码管显示(动态扫描实现双位显示)
  if (millis() - digitRefreshTime >= DIGIT_REFRESH_INTERVAL) {
    digitRefreshTime = millis(); // 更新时间戳
    refreshDisplay();             // 执行数码管刷新
  }

  // 根据当前激活状态执行对应逻辑
  if (red) {          // 红灯状态
    displayRedLight(); // 处理红灯倒计时和灯光控制
    showNoEntry();     // 点阵屏显示禁止图标
  } else if (green) { // 绿灯状态
    displayGreenLight(); // 处理绿灯倒计时和行人动画
    animateMan();      // 点阵屏显示行人走路动画
  } else if (yellow) { // 黄灯状态
    displayYellowLight(); // 处理黄灯倒计时和蜂鸣器提示
    showNoEntry();      // 点阵屏显示禁止图标
  }
}

/* ---------------------- 状态处理函数 ---------------------- */
// 红灯状态处理函数
void displayRedLight() {
  // 每秒更新一次倒计时数值
  if (millis() - digitTime >= 1000) {
    digitTime = millis(); // 更新时间戳
    numToShow--;          // 倒计时减1
    
    // 当剩余时间>6秒时,持续点亮红灯,蜂鸣器低频提示
    if (numToShow > 6) {
      tone(buzzer, 200, 80); // 200Hz声音
      digitalWrite(redLight, HIGH); // 红灯常亮
    } else { // 剩余时间≤6秒时,蜂鸣器高频提示
      tone(buzzer, 500, 80); // 500Hz声音
    }
  }

  // 每200毫秒切换红灯闪烁状态(仅在剩余时间≤6秒时闪烁)
  if (millis() - lightTime >= 200) {
    lightTime = millis(); // 更新时间戳
    On_Off = !On_Off;     // 切换开关状态
    
    if (numToShow <= 6) { // 剩余时间≤6秒时执行闪烁
      digitalWrite(redLight, On_Off ? HIGH : LOW); // 红灯闪烁
    }
  }

  // 倒计时结束后切换到绿灯状态
  if (numToShow < 0) {
    red = false;       // 关闭红灯状态
    green = true;      // 激活绿灯状态
    numToShow = 10;    // 绿灯倒计时初始值10秒
    tone(buzzer, 160, 80); // 切换状态提示音
    digitalWrite(greenLight, HIGH); // 点亮绿灯
    digitalWrite(redLight, LOW);    // 关闭红灯
    manTime = millis(); // 重置行人动画计时器
  }
}

// 绿灯状态处理函数
void displayGreenLight() {
  // 每秒更新一次倒计时数值
  if (millis() - digitTime >= 1000) {
    digitTime = millis(); // 更新时间戳
    numToShow--;          // 倒计时减1
  }

  // 根据剩余时间执行不同提示逻辑
  if (numToShow > 6) { // 剩余时间>6秒时,绿灯常亮,蜂鸣器短音提示
    digitalWrite(greenLight, HIGH); // 绿灯常亮
    
    if (millis() - lightTime >= 50) { // 每50毫秒切换一次提示音
      lightTime = millis(); // 更新时间戳
      On_Off = !On_Off;     // 切换开关状态
      if (On_Off) tone(buzzer, 160, 30); // 短促提示音
    }
  } else { // 剩余时间≤6秒时,绿灯开始闪烁,蜂鸣器长音提示
    if (millis() - lightTime >= 200) { // 每200毫秒切换闪烁状态
      lightTime = millis(); // 更新时间戳
      On_Off = !On_Off;     // 切换开关状态
      digitalWrite(greenLight, On_Off ? HIGH : LOW); // 绿灯闪烁
      if (On_Off) tone(buzzer, 160, 100); // 长提示音
    }
  }

  // 倒计时结束后切换到黄灯状态
  if (numToShow < 0) {
    yellow = true;       // 激活黄灯状态
    green = false;       // 关闭绿灯状态
    numToShow = 5;       // 黄灯倒计时初始值5秒
    tone(buzzer, 350, 80); // 切换状态提示音
    digitalWrite(yellowLight, HIGH); // 点亮黄灯
    digitalWrite(greenLight, LOW);   // 关闭绿灯
    lc.clearDisplay(0);  // 清除点阵屏
    showNoEntry();       // 显示禁止图标
  }
}

// 黄灯状态处理函数
void displayYellowLight() {
  // 每秒更新一次倒计时数值,黄灯常亮
  if (millis() - digitTime >= 1000) {
    digitTime = millis(); // 更新时间戳
    numToShow--;          // 倒计时减1
    digitalWrite(yellowLight, HIGH); // 黄灯常亮
  }

  // 每500毫秒切换蜂鸣器声音频率
  if (millis() - buzzerTime >= 500) {
    buzzerTime = millis(); // 更新时间戳
    buzzer_on_off = !buzzer_on_off; // 切换声音模式
    tone(buzzer, buzzer_on_off ? 300 : 100, 100); // 高低频交替
  }

  // 倒计时结束后切换回红灯状态
  if (numToShow < 0) {
    yellow = false;      // 关闭黄灯状态
    red = true;          // 激活红灯状态
    numToShow = 12;      // 红灯倒计时初始值12秒
    tone(buzzer, 200, 80); // 切换状态提示音
    digitalWrite(redLight, HIGH); // 点亮红灯
    digitalWrite(yellowLight, LOW); // 关闭黄灯
    showNoEntry();       // 显示禁止图标
  }
}

/* ---------------------- 显示控制函数 ---------------------- */
// 数码管动态扫描刷新函数(实现双位数码管显示)
void refreshDisplay() {
  // 分解数值为个位和十位(例如12→个位2,十位1)
  int digits[2] = {numToShow % 10, numToShow / 10}; // [个位, 十位]

  // 消隐处理:先关闭所有段显示,避免残影
  shiftOut(dataPin, clockPin, MSBFIRST, 0x00); // 位选信号全灭(0x00表示不选中任何位)
  shiftOut(dataPin, clockPin, LSBFIRST, 0x00); // 段选信号全灭(0x00表示所有段熄灭)
  digitalWrite(latchPin, HIGH); // 锁存消隐信号
  digitalWrite(latchPin, LOW);

  // 显示当前位(0为个位,1为十位)
  byte val = 0;
  bitSet(val, currentDigit); // 设置当前位选信号(例如currentDigit=0→0b00000001)
  
  // 发送位选信号(MSB优先,最高位对应高位数码管)
  shiftOut(dataPin, clockPin, MSBFIRST, val); // 选择显示位(个位或十位)
  
  // 发送段选信号(LSB优先,最低位对应数码管a段)
  shiftOut(dataPin, clockPin, LSBFIRST, NUM[digits[currentDigit]]); // 显示对应数字的段码
  
  // 锁存显示数据
  digitalWrite(latchPin, HIGH); // 上升沿锁存数据
  digitalWrite(latchPin, LOW);

  // 切换到下一位(个位→十位→个位循环)
  currentDigit = (currentDigit + 1) % 2;
}

// 行人走路动画函数(在点阵屏循环显示两帧动画)
void animateMan() {
  if (millis() - manTime >= MAN_DELAY) { // 达到动画切换间隔时更新
    manTime = millis(); // 更新时间戳
    
    lc.clearDisplay(0); // 清除点阵屏显示(避免残留上一帧)
    
    // 显示当前动画帧(逐行设置点阵数据)
    for (int row = 0; row < 8; row++) {
      lc.setRow(0, row, MAN_WALKING[currentFrame][row]); // 设置第0号设备的第row行数据
    }
    
    // 切换到下一帧(0→1→0循环)
    currentFrame = (currentFrame + 1) % 2;
  }
}

// 显示禁止图标函数(在点阵屏显示红色叉号)
void showNoEntry() {
  // 逐行设置禁止图标数据(8行组成对角线交叉图案)
  for (int row = 0; row < 8; row++) {
    lc.setRow(0, row, NO_ENTRY[row]); // 设置第0号设备的第row行数据为禁止图标对应行
  }
}