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

Arduino 的改进二进制计数器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (4投票s)

2018 年 6 月 25 日

CPOL

10分钟阅读

viewsIcon

15509

关于将 LED 连接到 Arduino 以及微控制器编程的通用注意事项,本文以改进型二进制计数器为例进行阐述。

引言

本文受到 CodeProject 上 Arduino 挑战赛的启发。我阅读了 Ryan 关于二进制计数器的入门文章,并认为其中有些地方可以改进。我对此进行了评论,Ryan 建议我可以将其写成一篇文章。 

因此,本文将介绍一个与 Ryan 的计数器一样,使用 8 个 LED 显示计数的二进制计数器,但我希望能展示一些更适合为 Arduino 等微控制器 (MCU) 设备编程的实践。我想探讨以下问题:

  • 如何正确地将 LED 连接到 Arduino 等微控制器 (MCU)
  • 使用合适的电阻值来驱动 LED
  • 在 MCU 程序中使用动态内存
  • 在 MCU 程序中使用 C++
  • 考虑 MCU 程序中的代码效率

LED 和 MCU 背景知识

我们需要将 8 个 LED 连接到 Arduino 的引脚。在 Ryan 的文章中,LED 的阳极(正极)连接到 Arduino 引脚,阴极(负极)通过一个单独的电阻连接到 GND。

这不是连接多个 LED 的正确方法。每颗 LED 都应该有一个独立的电阻。使用单个电阻的问题在于,电路中的电流始终是相同的,由电阻限制,并且这个电流会在给定时间点亮的 LED 之间分配。例如,如果电阻为 1000 欧姆 (1 k),电流约为 3 mA。如果只有一个 LED 点亮,则通过它的电流是 3 mA。如果有 2 个 LED 点亮,则每个 LED 的电流是 1.5 mA……如果所有 8 个 LED 都点亮,则每个 LED 的电流约为 0.4 mA——这对于 LED 来说太小了,不足以使其明亮。

为什么每颗 LED 都需要独立的电阻?

流过 LED 的电流取决于电压。如果电压低于某个阈值,电流几乎为零,LED 将熄灭。当电压超过阈值时,电流会迅速升高,我们需要通过串联 LED 的电阻来限制它。如果没有电阻,电流会过高;LED 会过热并损坏。

电压阈值取决于 LED 的类型,但通常在 2 V 左右。您可以在 LED 的数据手册中找到实际值(并且应该这样做)。您通常可以在购买 LED 的网店描述中看到这一点——它被称为LED 正向电压。如果不是,通常会有数据手册的链接。如果没有,可以根据 LED 的颜色进行猜测。

流过 LED 的电流也可以在数据手册中找到。对于“标准”LED,它是 20 mA;对于所谓的低电流或低功耗 LED,它是 2 mA。低功耗 LED 的价格与标准 LED 差不多,但功耗却低 10 倍,因此请尽可能使用它们。

如何计算电阻值?

现在我们有了使用 LED 的两个基本值——正向电压 (Vf) 和所需电流 (If)。我们可以计算电阻值 (R),以限制流过 LED 的电流 (If)

R = (Vdd – Vf) / If

Vdd 是我们使用的 MCU 输出引脚上的电压。对于 Arduino Uno,它是 5 V。

这里有一个例子。我们想连接一个红色 LED,其 Vf 为 1.9 V,所需电流 If 为 2 mA(但在公式中使用安培),所以 If = 0.002 A。

R = (5 – 1.9) / 0.002 = 3.1 / 0.002 = 1550 欧姆。

因此,理想情况下,我们应该使用 1550 欧姆的电阻。但是电阻只生产特定值,所以我们将使用一个接近的值,最好是更大的值——更大的值意味着电流会更小。我们可以使用例如 1800 欧姆 (1k8) 或 2200 欧姆 (2k2)。最接近的常用生产值为 1500 欧姆 (1k5),也足够了。

现在,您可能会看到有人将 LED 直接连接到 Arduino 引脚而不使用任何电阻——并且还能正常工作。并没有烧坏。嗯,您不应该习惯“只要东西能用就是对的”这种想法。如果您认真开发真正可靠的嵌入式系统,请尝试理解系统中每个组件的工作原理,并确信您正在正确使用它们。没有限流电阻的 LED 连接到 Arduino 不会立即损坏,是因为 MCU 无法提供足够的电流来使 LED 过热。MCU 引脚(对于 Arduino Uno)最多可以提供约 40 mA 的电流,因此 LED 不会过载。但 MCU 并不乐意这样被过载。您不应依赖 MCU 制造商来保护输出引脚免受过载。您很容易烧坏您的 Arduino。

关于 MCU 引脚电流的重要提示

通常,您可以在数据手册中找到确切的数字,但 Arduino Uno 中使用的 MCU 每个引脚最多可提供 40 mA 的电流,但并非所有引脚可以同时达到此值!整个 MCU 封装的总电流不应超过 200 mA。

MCU 软件背景知识

为 MCU 编写程序与为大型强大的计算机编写程序仍然有很大不同。在台式机和笔记本电脑上,我们习惯了千兆字节的 RAM 和千兆赫兹的处理器 (CPU) 速度,以及像 Java 或 Python 这样的编程语言,它们使编写复杂程序变得容易,但也消耗大量内存和 CPU 时间。这对大型计算机来说没问题——强大的硬件可以弥补低效的软件。在 MCU 中,情况不同。典型的 MCU 只有兆赫兹的 CPU 速度和千字节的内存。程序的效率在 MCU 中仍然很重要。

如果您问为什么,那是因为 MCU 被用于许多廉价的设备,如手电筒、玩具等。如果每件产品生产数百万件,那么每件价格上的一分钱都很重要。因此,硬件尽可能便宜,无法弥补低效的软件。

即使有一天硬件变得足够便宜,以至于没有速度和内存限制,仍然还有能源,也就是功耗。当您编写程序时,很容易忘记这一点,但程序是靠电力运行的。您的程序执行的操作越多,消耗的能量就越多。这在电池供电设备中尤其重要——它们无处不在,想想您电视机的遥控器。

因此,考虑到效率,Ryan 的二进制计数器中有两点我不喜欢——使用动态内存 (new 和 delete) 以及通过除以二将数字转换为二进制位。

使用 new 和 delete

在 MCU 编程中,动态内存分配除非在特殊情况下,否则不使用。由于 RAM 非常有限,您应该了解您实际使用了多少以及用于什么。不像大型计算机那样有虚拟的、几乎无限的内存。new 运算符并不能帮助您获得更多内存,它只会增加获取和释放您本可以静态分配的内存的开销。通常,您知道您需要多少内存。如果您需要从外部世界获取一些数据,但不知道它有多大,您仍然需要设计程序,使其在某个限制内工作,并在数据过大时优雅地失败。

处理位

在 C/C++ 中,很容易操作和测试单个位。在我们的二进制计数器中,我们有一个 8 位变量来存储当前计数,并且需要将其转换为单独的位来打开/关闭显示该值的 LED。没有必要使用昂贵的除法;变量以二进制形式存储在内存中。我们只需要查看每个位的值即可。在下面的代码中,我使用一个循环将计数器 mCount 中的数字转换为要输出的位。这不是最有效的方法,但希望它易于阅读。

uint8_t mask = 1;  // binary 0000 0001
uint8_t bitVal;   
for(uint8_t i = 0; i < mDigitCount; i++) {
    bitVal = (mCount & mask) ? 1 : 0;
    digitalWrite(mPins[i], bitVal );
    mask = mask << 1;   // shift left to test next bit
}

Using the Code

最后,我们来看代码。我提供了三个版本的二进制计数器。第一个版本是用经典的 C 语言编写的。另外两个是 C++ 版本。

为什么选择 C 语言?

如今,C 或 C++ 语言被用来编程 MCU。使用 C++ 有其优势,它是面向对象的,但也存在缺点——程序可能会更大、效率稍低,而且并非所有编译器(工具链)都支持 C++。即使您的编译器支持 C++,您也不能期望它与大型计算机上的 C++ 相同。您可能会得到一些人称之为“C++ 的奇怪子集”。

所以,如果您问我,我会说只有在您真正需要时才使用 C++——如果您的程序将从中受益。这里有三个在 MCU 程序中使用 C++ 的理由(当然,观点可能不同):

  • 创建大型程序,您觉得用 C++ 来管理会更容易
  • 创建库( nicely wrapped in a C++ class)
  • 如果您需要某种功能的多个实例——如下面的 C++ 二进制计数器所示。

计数器的硬件

我使用了连接方式与 Ryan 文章中描述相同的 LED——尽管这不是正确的方法。您可以在 Leonid Fofanov 的这篇文章中看到正确的方法,即使用独立的电阻。

第一个版本 - C 语言

这是我会用于简单二进制计数器项目的版本,没有任何将其制成库或使用更多计数器实例的计划。

// C - language version of binary counter.
// Uses 1006 B of flash, 20 B or RAM

// prototype
void CounterIncrement();

void setup()
{  
}

void loop()
{
  CounterIncrement();
  delay(500);
}

void CounterIncrement()
{   
   static const uint8_t digitCount = 8;
   static const uint8_t pins[digitCount] = {12, 11, 10, 9, 8, 7, 6, 5};
   static uint8_t count = 0;   
   static bool firstRun = true;

   if ( firstRun ) {
    firstRun = false;
    for(uint8_t i = 0; i < digitCount; i++) {
      pinMode(pins[i], OUTPUT);  
    }  
   }
   
    // Control the LEDs
    uint8_t mask = 0x80;  // binary 1000 0000
    uint8_t bitVal;   
    for(uint8_t i = 0; i < digitCount; i++) {
        bitVal = (count & mask) ? 1 : 0;
        digitalWrite(pins[i], bitVal );
        mask = mask >> 1;   // move to next bit
    }

    if(count == 255) {
        count = 0;
    } else {
        count++;  
    }
}

版本 2 - C++

这是用 C++ 编写的二进制计数器。除了摒弃动态内存和缓慢的二进制转换外,我还添加了在构造函数中设置使用引脚的选项,而不是将其硬编码到类中。因此,使用此类处理不同的硬件设置会更轻松。此外,我还添加了 begin 函数,这在 Arduino 中是用于初始化的标准函数(我希望他们称之为 init)。在这样的函数中设置引脚为输出模式比在构造函数中更好,因为如果我们创建一个类对象作为全局变量,构造函数会在程序启动早期执行,我不确定我们使用的 Arduino pinMode 函数是否已经初始化。所以为了安全起见,我使用一个将在 setup 中调用的初始化函数。

构造函数接受一个引脚编号数组作为输入参数,以及使用的位数。因此,也可以拥有少于 8 位计数器。

提示:尝试使用两个 4 位计数器而不是一个 8 位计数器——查看 counter2 中注释掉的代码。

// Alternative version of Binary counter
// Uses 1094 B of flash and 27 B of RAM with 1 instance of the counter.
// 2 instances: 1182 B flash and 37 B RAM
class BinaryCounter {
 
  private:    
    uint8_t mDigitCount;
    uint8_t mPins[8];  
    uint8_t mCount = 0;    

  public:
    BinaryCounter(uint8_t pins[], uint8_t count);
    void begin();
    void increment();      
};

// The pins should be in order from least significant to most significant,
// so pins[0] is bit 0, pins[1] is bit 1, etc.
BinaryCounter::BinaryCounter(uint8_t pins[], uint8_t count)
{
  //  we support max. 8 digits
  mDigitCount = ( count <= 8 ) ? count : 8;
  for(uint8_t i = 0; i < mDigitCount; i++) {
    mPins[i] = pins[i];
  }  
}

void BinaryCounter::begin() {
  //set LED all pins to output mode
  for(uint8_t i = 0; i < mDigitCount; i++) {
    pinMode(mPins[i], OUTPUT);  
  }
}

void BinaryCounter::increment() {

    // Maximum value of the counter depends on number of bits
    // For 8-bit counter the shift code can't be used
    // it would overflow 8-bit variable.  
    const uint8_t maxVal = (mDigitCount == 8) ? 255 : (1 << mDigitCount) - 1;
    
    // Control the LEDs
    uint8_t mask = 1;  // binary 0000 0001
    uint8_t bitVal;   
    for(uint8_t i = 0; i < mDigitCount; i++) {
      bitVal = (mCount & mask) ? 1 : 0;
      digitalWrite(mPins[i], bitVal );
      mask = mask << 1;   // shift left to test next bit
    }
 
    // Increment the counter
    if(mCount >= maxVal ) {
        mCount = 0;
    } else {
        mCount++;  
    }
}

// Create object of binary counter
BinaryCounter counter1( (uint8_t[]){5, 6, 7, 8, 9, 10, 11, 12}, 8 );
// Two counters at the same time
//BinaryCounter counter1( (uint8_t[]){5, 6, 7, 8}, 4 );
//BinaryCounter counter2( (uint8_t[]){9, 10, 11, 12}, 4 );

void setup()
{ 
  counter1.begin();
  //counter2.begin();
}

void loop()
{  
  counter1.increment();  
  //counter2.increment();  
  delay(500);
}

版本 3 - C++

关于上面二进制计数器版本 2 的一个缺点是使用了“普通”变量来存储引脚编号。我想使用 const 变量——但仍然能够在构造函数中设置引脚。为了实现这一点,我不得不使用 C++11 标准中添加的一个特性,以便在构造函数中初始化 const 数组。Arduino IDE 支持 C++11,所以这没问题,但我承认这对于 MCU 程序来说是一个相当严苛的要求。

因此,这个版本使用 const 变量来存储引脚编号,这节省了 8 字节 RAM,并且由于编译器知道引脚在运行时不会改变,因此可以带来更快速、更高效的代码。我还使用了构造函数中的默认参数值,以便更轻松地使用少于 8 个引脚的计数器。

// Alternative version of Binary counter
// version 3 with const pins.
// Requires C++11 support in compiler.
// Uses 1076 B of flash and 19 B of RAM with 1 instance of the counter.
class BinaryCounter {
 
  private:    
    uint8_t mDigitCount;
    const uint8_t mPins[8];  
    uint8_t mCount = 0;    

  public:
    //BinaryCounter(uint8_t pins[], uint8_t count);
    BinaryCounter::BinaryCounter(uint8_t count, uint8_t p0, uint8_t p1=0, uint8_t p2=0, 
                                  uint8_t p3=0, uint8_t p4=0, uint8_t p5=0, uint8_t p6=0, 
                                  uint8_t p7=0);
    void begin();
    void increment();      
};

BinaryCounter::BinaryCounter(uint8_t count, uint8_t p0, uint8_t p1, uint8_t p2, uint8_t p3,
                            uint8_t p4, uint8_t p5, uint8_t p6, uint8_t p7)
 : mPins{p0, p1, p2, p3, p4, p5, p6, p7}               
{
  //  we support max. 8 digits
  mDigitCount = ( count <= 8 ) ? count : 8;  
}

void BinaryCounter::begin() {
  //set LED all pins to output mode
  for(uint8_t i = 0; i < mDigitCount; i++) {
    pinMode(mPins[i], OUTPUT);  
  }
}

void BinaryCounter::increment() {

    // Maximum value of the counter depends on number of bits
    // For 8-bit counter the shift code can't be used
    // it would overflow 8-bit variable.  
    const uint8_t maxVal = (mDigitCount == 8) ? 255 : (1 << mDigitCount) - 1;
    
    // Control the LEDs
    uint8_t mask = 1;  // binary 0000 0001
    uint8_t bitVal;   
    for(uint8_t i = 0; i < mDigitCount; i++) {
      bitVal = (mCount & mask) ? 1 : 0;
      digitalWrite(mPins[i], bitVal );
      mask = mask << 1;   // shift left to test next bit
    }
 
    // Increment the counter
    if(mCount >= maxVal ) {
        mCount = 0;
    } else {
        mCount++;  
    }
}

// Create object of binary counter
BinaryCounter counter1( 8, 5, 6, 7, 8, 9, 10, 11, 12  );
// Two counters at the same time
//BinaryCounter counter1(4, 5, 6, 7, 8 );
//BinaryCounter counter2(4, 9, 10, 11, 12 );

void setup()
{  
  counter1.begin();
  //counter2.begin();
}

void loop()
{  
  counter1.increment();  
  //counter2.increment();  
  delay(500);
}

历史

  • 2018-06-25:第一个版本
© . All rights reserved.