65.9K
CodeProject 正在变化。 阅读更多。
Home

数字鸡蛋计时器...

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2018年8月2日

CPOL

8分钟阅读

viewsIcon

24225

downloadIcon

157

用一些有趣的...来最大化您的 Arduino...

引言

开始一个 Arduino 项目非常容易。例如点亮一些 LED。然而,您会很快耗尽可用的引脚。您可以查看本链接到原始“Arduino 夏季趣味”挑战 1 的文章:将 Arduino 连接到面包板以点亮 LED[^]。

如您所见,它根据数字的位数显示一串八个 LED。但是如果您想显示一个 16 位数字怎么办?在示例中使用的 Arduino UNO 上,您只有 14 个数字引脚...

虽然您可以选择其他板(如 MEGA),但当您选择更大的板/控制器时,总会遇到大小限制,并且成本也会增加。所以总而言之,了解如何使用额外的芯片来最大化微控制器的潜力会非常有帮助...

起点

最终项目将包含三个七段显示器和一个压电蜂鸣器。为了说明这个问题,我创建(至少尝试了)了这些组件连接到板上的图...

虽然显示器完美地显示了“生命、宇宙和万物的终极问题的答案”,但它有一个问题... 没有一个引脚可以连接蜂鸣器(更不用说第三个七段显示器)了。它还占用了 rx/tx 线,因此初始化串行通信可能会有问题(将 Arduino 连接到您的 PC/平板电脑并通过串行通信配置运行的应用程序是一个非常酷的选项——一种命令行解释器)。

我打算创建的项目不是一个大项目(比如驱动打印机或洗衣机),即使如此,我也成功地突破了 Arduino 的界限...

移位寄存器

为了解决这个问题,我想让您了解 74HC595移位寄存器芯片。移位寄存器是微控制器和设备之间的一种串行输入并行输出(也可以是串行输出,但这现在并不重要 :-)) 门。想法是您只使用 3 个引脚来写入一个 8 位值。它以逐位方式进行,移位寄存器在每次时钟脉冲时接收一个位,并将值推入某个内部存储器(满时会溢出)。当移位寄存器收到“传输结束”信号时,它会将内部存储器推送到引脚并点亮显示器...

595 的一个不错的附加功能是它有一个“溢出”引脚,它允许连接多个移位寄存器,从而节省更多 Arduino 引脚。所以当你写入 16 位(分成两组 8 位)时,前 8 个脉冲将设置 595 的 8 个存储引脚,而脉冲 9 到 19 将逐个将其推送到“溢出”引脚并占据它们的位置。您可以继续到 24 位或 32 位或更多位,将它们分成 8 位一组,但在同一个循环内。如果您将“溢出”引脚连接到另一个 595 的数据引脚,您实际上可以通过同一个 Arduino 引脚写入两个芯片。

让我们在板子上看看...

现在白色、黄色和青色的线连接到移位寄存器。所有移位寄存器的时钟和锁存线都连接到引脚 8 和 9,因为我们希望同步所有显示的写入和计时。引脚 13 连接到第一个移位寄存器(右侧)的数据线,而该寄存器的“溢出”引脚连接到第二个移位寄存器(中间)的数据线,而第二个移位寄存器的“溢出”引脚连接到最后一个移位寄存器(左侧)的数据线。

现在我有空余的引脚连接蜂鸣器(紫色),还剩下 10 个空余的引脚,包括用于串行通信的 rx/tx 对... 实际上发生的是,我可以使用仅 3 个 Arduino 引脚来驱动 3 个七段显示器(即 21 个 LED)(而且还可以继续,甚至可以进行疯狂的用移位寄存器驱动移位寄存器的操作)...

开始编码

驱动移位寄存器

// rightmost bit is always 0 as we do not use the first pin
// of the shift registers (it is on the far side)
const byte ledValues[10] = {
  //GFEDCBA - pins
  0b01111110, // 0
  0b00001100, // 1
  0b10110110, // 2
  0b10011110, // 3
  0b11001100, // 4
  0b11011010, // 5
  0b11111010, // 6
  0b00001110, // 7
  0b11111110, // 8
  0b11011110, // 9
};

void registerWrite(byte left, byte middle, byte right) {
  digitalWrite(latchPin, LOW);

  shiftOut(dataPin, clockPin, MSBFIRST, left);
  shiftOut(dataPin, clockPin, MSBFIRST, middle);
  shiftOut(dataPin, clockPin, MSBFIRST, right);

  digitalWrite(latchPin, HIGH);
}

ledValues - 这是一个简单的数字到显示器上 7 个 LED 的映射(由于技术原因,第一个位位置未使用,我没有使用移位寄存器的 Q0 引脚)。因为我从最高有效位开始推送位,所以 A-G 的顺序在这里是颠倒的...

如果您查看 registerWrite 方法,您会发现我首先写出最左边的字节(保存百位的值),然后是中间的(保存十位的值),最后是最右边的(保存个位的值)。所以,如果我向显示器写入 42(即 042),我将按此顺序发送此位序列

0,1,1,1,1,1,1,0,1,1,0,0,1,1,0,0,1,0,1,1,0,1,1,0

您可能想知道为什么顺序看起来是颠倒的?您可能认为第一次写入应该是第一个显示器(最右边)的第一个 LED(A),但请记住我们使用的是移位寄存器!!!当数据线上出现新值时,它会推入先前的值(并可能通过溢出位输出)。所以我必须写入的第一个位是链中最左边那个显示器的 LED G 的值。

另一个有趣的事情是 shiftOut 方法(不是我的——它是 Arduino 的原生方法)只能写入 8 位——一个字节——所以我需要三次调用来写出三个数字。诀窍在于,只要移位寄存器的锁存线是低的,它就会被视为单个输入。一旦我将锁存线设置为高,进入的数据就会在 Q 引脚上输出... 这也解释了为什么我将所有移位寄存器的锁存线连接到同一个引脚——这使得我可以将它们作为一个整体来控制它们的状态。当然,对于时钟线也是如此,它将使用相同的频率为所有移位寄存器同步写入...

计算秒数

Arduino 没有内置的时钟来跟踪时间,但它有一个功能(对于微控制器来说很常见),称为看门狗。这个功能是一个滴答作响的时钟——与主循环的执行并行,主要用于跳出挂起。看门狗可以编程为周期性地触发中断,如果主程序在中断完成之前没有重置看门狗,它将启动控制器重启。

在我的例子中,我设置看门狗定时器每秒启动一次,然后我减少计数器并重置看门狗,以便在一秒后再次启动...

#include <avr/wdt.h> // you need this for easier access of the watchdog timer

// watchdog interrupt handler
ISR(WDT_vect) {
  if(counter > 0) {
    counter--;

    wdt_start();
  }
  else {
    alarm = true;
    wdt_stop();
  }
}

void wdt_start() {
  wdt_disable();

  WDTCSR |= 0b00011000;          // enable presale settings to set the interrupt time 
                                 // in the next line (bit 5)
  WDTCSR = 0b01001000 | WDTO_1S; // enable interrupt flag (bit 7) and set timing to 1s (bits 1, 2)
                                 // bit 4 is always high to enable writing to the 
                                 // watchdog control register

  wdt_reset();
}

void wdt_stop() {
  wdt_reset();
  wdt_disable();
}

void setup() {
  wdt_stop();
}

void loop() {
  wdt_start();
}

由于看门狗是低级的东西(而且我不想加载更多库并使我的 Arduino 臃肿),我使用一些位设置来初始化定时器...

可能最重要的事情是在 setup 方法中禁用看门狗,这样它就不会干扰软件/硬件初始化,而软件/硬件初始化可能需要超过 1 秒,在这种情况下,我们将陷入无限重启循环...

ISR 方法是中断处理程序,在那里完成了实际的计数... 每次中断后,我都会重新启动周期或在计数器达到零时停止定时器。

设置和重置

然后下一个主要部分是启动和停止计时器。为了使其完全自主,我本应添加一些按钮来设置/启动/停止计时器。然而,我决定使用 rx/tx 线(毕竟,我辛苦保存了它们)将 Arduino 连接到我的 PC,并从那里进行配置...

void loop() {
  bool endRead = false;
 
  while (Serial.available())
  {
    byte val = Serial.read();

    if (val > 47 && val < 58)
    {
      value = value * 10 + (val - 48);
    }
    else if (val == 's') {
      endRead = true;
    }
    else if (val == 'r' || val == 'R')
    {
      reset();
    }
  }

  if (endRead && value > 0 && value < 1000)
  {
    setValue(value);

    wdt_start();
    
    value = 0;
  }
}

这段代码读取 rx/tx 线上传送的所有内容(在 PC 端,它很可能显示为 COM 上的 USB 连接)。如果您仔细观察,您会注意到这段代码是构建在一个 IO 不会阻塞的环境中。在大多数开发领域(桌面、Web 或移动),IO 默认是阻塞的,您必须付出特殊的努力才能克服它。然而,在 Arduino 中,Serial.read 本质上是非阻塞的。实际这意味着发送的所有信息并不一定在 loop 方法的同一次迭代中被读取。

我的解决方案是使用单个字符来标记输入的结束。对我来说,我选择了 r (或 R)来重置计时器,以及 s(秒)来标记数字输入的结束。例如,发送 456s 将设置计时器为 456 秒并开始计数。另一方面,发送 23 将不会执行任何操作,只会等待更多输入。

惊悚的有趣

当计时器到期时,它会设置一个标志来启动警报阶段。压电蜂鸣器无法播放美妙的音乐,因为它只能以特定的频率播放特定持续时间的音符,而且音质并不好... 然而,这足以播放一段旋律作为警报而不是简单的蜂鸣声,而这正是代码的最后一部分...

// music notes
const int o = 0; // represents silence

const int a = 440;
const int f = 349;
const int cH = 523;
const int eH = 659;
const int fH = 698;
const int gS = 415;

const int len = 19;
const int notes[] = {a, a, a, f, cH, a, f, cH, a, o, eH, eH, fH, cH, gS, f, cH, a, o};
const int beats[] = {8, 8, 8, 6, 2, 8, 6, 2, 16, 8, 8, 8, 8, 6, 2, 8, 6, 2, 16, 8};
const int tempo = 70;

const int piezo = 2;

void play(int note, int duration)
{
  tone(piezo, note, duration);

  delay(duration);
  noTone(piezo);

  delay(tempo / 2); // fixed pause between notes
}

void setup() {
  pinMode(piezo, OUTPUT);
}

void loop() {
  if(alarm) {
    for (int i = 0; i < len; i++) {
      play(notes[i], beats[i] * tempo);
      }
  }
}

因为我对乐谱一无所知,我使用了这里的值(并进行了一些修改使其更有效):Arduino Star Wars Song for Piezo

它将以一种非常烦人的方式一遍又一遍地播放星球大战的帝国进行曲,直到您重置计时器...

硬件

仅为完成。

数量 组件 (Component)
1 Arduino UNO
3 七段显示器(阴极,但您可以切换)
3 8 位移位寄存器 (74HC595)
4 100 &#8486; 电阻
1 压电蜂鸣器
  各种颜色的电线

摘要

在操作现代计算机,甚至移动设备时,我们可能会觉得我们手头资源无限。在开始接触电子产品和使用微控制器时,我们很容易撞墙并崩溃。本项目展示了如何重新思考和解决资源短缺的问题。花少少的钱,但仍然有大大的想法...

© . All rights reserved.