优秀的编程知识分享平台

网站首页 > 技术文章 正文

在Arduino中使用内部或外部 EEPROM来保存设置及其它数据

nanyue 2024-07-23 13:22:13 技术文章 13 ℃

本文英文原创:DronebotWorkshop
翻译整理: DIY百事



目录

1 简介
2 了解 EEPROM
2.1非易失性存储器类型
2.2 EEPROM 限制

3 带有 Arduino 的 EEPROM – 两种类型
3.1内部 EEPROM
3.2外部 EEPROM

4 使用内部 EEPROM
4.1 Arduino EEPROM 库
4.1.1 EEPROM 更新
4.1.2 EEPROM 读取
4.1.3 EEPROM 清除

  • 5 使用外部 EEPROM
    5.1 AT24LC256 EEPROM
    5.1.1 AT24LC256 模块
    5.2 外部 EEPROM 连接
    5.3 外部 EEPROM Arduino 代码
    5.3.1运行外部 EEPROM 代码
  • 6 结论
  • 介绍

    计算机和微控制器需要内存来永久或临时存储数据,虽然这种内存可以有多种形式,但它可以分为两种基本类型——易失性和非易失性。

    易失性存储器通常采用 RAM 或随机存取存储器的形式。这是您设备的“工作”内存,它保存程序运行期间使用的临时数据。一旦断电,内存就会被清除。


    正如您现在可能已经猜到的那样,非易失性存储器即使在断电后也会保留其数据。有多种不同类型的非易失性存储器,今天我们将研究其中一种——可擦可编程只读存储器(EEPROM)

    具体来说,我们将研究如何在Arduino中使用 EEPROM。

    了解 EEPROM

    还有许多其他形式的非易失性存储器,包括闪存、SD 卡、USB 驱动器、硬盘驱动器和 SSD。那么 EEPROM 有什么特点呢?

    与上述存储器类型相比,EEPROM的存储量非常小,事实上,EEPROM 容量通常以比特而不是字节来衡量。由于它们只存储少量数据,因此不会消耗大量电流,因此非常适合电池和低功耗应用。

    EEPROM 是在 1970 年代初开发的,第一个 EEPROM 在 1975 年获得了 NEC 的专利。

    非易失性内存类型

    EEPROM 是使用浮栅晶体管阵列构成的,每个位有两个晶体管。它是 ROM 或只读存储器系列设备的一部分。

    EEPROM 与 闪存Flash Memory 类似,不同之处在于 闪存更大,使用更大的数据块。这是以牺牲寿命或重写或“写入周期”为代价的,闪存只能重写大约 10,000 次。

    Arduino 微控制器使用闪存来存储上传到其中的程序。Arduino 也有内部 EEPROM,我们很快就会看到。


    ROM 系列的其他成员包括:

    • ROM——只读存储器。这些芯片在制造过程中进行了编程,无法更改。
    • PROM——可编程只读存储器。这些芯片可以使用特殊设备进行编程,但是不能擦除和重新编程。
    • EPROM——可擦除可编程只读存储器。与 PROM 一样,EPROM 需要特殊的编程设备。这种类型的存储芯片可以使用紫外线擦除,然后重新使用。

    由于它不需要外部编程或“烧录”设备,因此 EEPROM 是这些设备中最容易使用的。

    EEPROM 限制

    在您的设计中使用 EEPROM 时还需要考虑一些限制。


    与闪存一样,EEPROM 的写入周期数有限。您可以随意读取它们,但您只能写入或重写数据给定的次数。

    普通 EEPROM 的写入周期限制约为 10万 到 200 万次写入周期。

    几十万甚至几百万的写入周期听起来可能很多,但考虑一下现代微控制器或微处理器写入数据的速度有多快,您很快就会意识到它可能会成为一个严重的限制。如果您每秒重写一次 EEPROM,并且它的写入次数为 10万 次,那么您将在一天多一点的时间内耗尽其寿命!

    使用 EEPROM 进行设计时,您会希望尽可能少地写入设备。另一种技术,我们稍后会讨论,是在写入之前先读取该位——如果它已经是正确的值,那么就没必要重写它。

    另一个 EEPROM 限制是数据保存时间。EEPROM 技术在不断改进,今天的 EEPROM 可以在室温下将数据保存大约 10 年。重写该数据则重新计算,从而延长数据保存时间。

    最后,一个明显的限制是 EEPROM 的存储容量,与其他存储设备相比,它非常小。但是,由于 EEPROM 最常见的用途是保留配置和校准数据,因此问题不大。

    Arduino 两种类型的 EEPROM

    在我们的 Arduino 设计中添加 EEPROM 可以让我们的项目在断电后保留数据。这对于需要校准或存储用户设置的应用程序非常有用。

    内部 EEPROM

    我们可以很容易地将 EEPROM 功能添加到我们的 Arduino 项目中。事实上,Arduino 已经有一些我们可以在程序中使用的内部 EEPROM。内存量取决于我们使用的 Arduino 模型。

    下面说明了一些流行的 Arduino 模型中内部 EEPROM 的数量:


    微控制器 EEPROM容量
    Atmega2560 (Arduino Mega 2560) 4096 字节
    ATmega328(Arduino Uno、Mini , Nano) 1024 字节
    ATmega168(Nano) 512 字节

    在许多设计中,这种低容量的非易失性存储器就足够了。

    外部 EEPROM

    如果您的设计需要更多 EEPROM 容量,那么您可以添加一个外部 EEPROM。

    由于 EEPROM 在位级别上运行,它们通常设计为使用串行数据,换句话说,一次传输一位的数据。这导致了许多基于 I2C 的 EEPROM 设备的开发。

    使用带有 Arduino 的 I2C EEPROM 设备非常简单,因为 Arduino 已经连接了 I2C 和库以使用它们。许多 I2C EEPROM 可以配置为唯一地址,允许您在同一电路中使用多个设备。

    外部 I2C EEPROM 的优点是它们具有比 Arduino 内部 EEPROM 限制的 10万 次写入更大的寿命。

    使用内部 EEPROM

    我们将使用 Arduino 中的内部 EEPROM 开始我们的 EEPROM 实验。对于我们的实验,我使用的是 Arduino Uno,但如果您愿意,可以替换为不同的 Arduino。

    为了演示内部 EEPROM,我们将在 Arduino 中添加一个电位器,将其连接到模拟输入端口之一。下面是接线图:


    连接后,将 Arduino 连接到运行 Arduino IDE 的计算机。

    Arduino EEPROM 库

    使用Arduino IDE 中已经包含 的Arduino EEPROM 库将大大简化我们的实验。

    该库带有许多简短的示例代码。我们可以使用它们来试验 Arduino 的内部 EEPROM。请注意,该库仅适用于内部 EEPROM,要使用外部设备将需要不同的库。

    为了在Arduino IDE中使用示例程序,请执行以下步骤:

    • 启动 Arduino IDE。
    • 单击屏幕顶部的文件菜单。
    • 选择示例。将出现一个子菜单。
    • 选择EEPROM。将出现一个额外的子菜单。
    • 您可以从子菜单中选择一个示例。

    该库包含八个示例,其中的代码将帮助您编写自己的代码以使用 Arduino 内置 EEPROM。这里有一些你可以尝试:

    EEPROM 更新

    虽然有一个EEPROM写入代码,但在将数据写入 EEPROM 时,使用更新方法是更好的选择。这是因为这种方法先读取 EEPROM 的值,如果不一样才更新,实际上它只是 Read 和 Write 方法的结合。

    通过这样做可以减少对 EEPROM 的写入次数。

    
    /***
       EEPROM Update method
     
       Stores values read from analog input 0 into the EEPROM.
       These values will stay in the EEPROM when the board is
       turned off and may be retrieved later by another sketch.
     
       If a value has not changed in the EEPROM, it is not overwritten
       which would reduce the life span of the EEPROM unnecessarily.
     
       Released using MIT licence.
     ***/
     
    #include <EEPROM.h>
     
    /** the current address in the EEPROM (i.e. which byte we're going to write to next) **/
    int address = 0;
     
    void setup() {
      /** EMpty setup **/
    }
     
    void loop() {
      /***
        need to divide by 4 because analog inputs range from
        0 to 1023 and each byte of the EEPROM can only hold a
    value from 0 to 255.
    需要将读入除4,因为EEPROM只能保存0-255的数据
      ***/
      int val = analogRead(0) / 4;
     
      /***
        Update the particular EEPROM cell.
        these values will remain there when the board is
        turned off.
      ***/
      EEPROM.update(address, val);
     
      /***
        The function EEPROM.update(address, val) is equivalent to the following:
     
        if( EEPROM.read(address) != val ){
          EEPROM.write(address, val);
        }
      ***/
     
     
      /***
        Advance to the next address, when at the end restart at the beginning.
     
        Larger AVR processors have larger EEPROM sizes, E.g:
        - Arduno Duemilanove: 512b EEPROM storage.
        - Arduino Uno:        1kb EEPROM storage.
        - Arduino Mega:       4kb EEPROM storage.
     
        Rather than hard-coding the length, you should use the pre-provided length function.
        This will make your code portable to all AVR processors.
      ***/
      address = address + 1;
      if (address == EEPROM.length()) {
        address = 0;
      }
     
      /***
        As the EEPROM sizes are powers of two, wrapping (preventing overflow) of an
        EEPROM address is also doable by a bitwise and of the length - 1.
     
        ++address &= EEPROM.length() - 1;
      ***/
     
      delay(100);
    }
    

    代码功能是接受来自模拟引脚 A0 的输入,这是我们连接电位器的地方。它接受输入并将其除以 4,使其处于 0 – 255 的范围内,可以用单个字节表示。

    然后将该值写入第一个 EEPROM 地址,但前提是数据与当前数据不同。显然,第一次运行时它总是会执行写操作,但在后续运行中,它只会在值与当前值不同时才会写。

    从示例中加载代码并将其发送到您的 Arduino。然后转动电位器,数据将被记录到 EEPROM。


    现在我们练习读取这些数据。

    EEPROM 读取

    /*
     * EEPROM Read
     *
     * Reads the value of each byte of the EEPROM and prints it
     * to the computer.
     * This example code is in the public domain.
     */
     
    #include <EEPROM.h>
     
    // start reading from the first byte (address 0) of the EEPROM
    int address = 0;
    byte value;
     
    void setup() {
      // initialize serial and wait for port to open:
      Serial.begin(9600);
    
      while (!Serial) {
        ; // 等待串口连接
      }
    }
     
    void loop() {
      // read a byte from the current address of the EEPROM
      value = EEPROM.read(address);
     
      Serial.print(address);
      Serial.print("\t");
      Serial.print(value, DEC);
      Serial.println();
     
      /***
        Advance to the next address, when at the end restart at the beginning.
     
        Larger AVR processors have larger EEPROM sizes, E.g:
        - Arduno Duemilanove: 512b EEPROM storage.
        - Arduino Uno:        1kb EEPROM storage.
        - Arduino Mega:       4kb EEPROM storage.
     
        Rather than hard-coding the length, you should use the pre-provided length function.
        This will make your code portable to all AVR processors.
      ***/
      address = address + 1;
      if (address == EEPROM.length()) {
        address = 0;
      }
     
      /***
        As the EEPROM sizes are powers of two, wrapping (preventing overflow) of an
        EEPROM address is also doable by a bitwise and of the length - 1.
     
        ++address &= EEPROM.length() - 1;
      ***/
     
      delay(500);
    }
    

    代码只是读取 EEPROM 并将数据打印到串行监视器。因此,如果您要在上一个代码之后运行它,您应该会看到电位器运动产生的值。

    该代码使用制表符(“\t”)很好地格式化显示,显示每个 EEPROM 位置的地址和数值。

    EEPROM 清除

    EEPROM 清除代码将 EEPROM 中的所有值重置为零

    /*
     * EEPROM Clear
     *
     * Sets all of the bytes of the EEPROM to 0.
     * Please see eeprom_iteration for a more in depth
     * look at how to traverse the EEPROM.
     *
     * This example code is in the public domain.
     */
     
    #include <EEPROM.h>
     
    void setup() {
      // initialize the LED pin as an output.
      pinMode(13, OUTPUT);
     
      /***
        Iterate through each byte of the EEPROM storage.
     
        Larger AVR processors have larger EEPROM sizes, E.g:
        - Arduno Duemilanove: 512b EEPROM storage.
        - Arduino Uno:        1kb EEPROM storage.
        - Arduino Mega:       4kb EEPROM storage.
     
        Rather than hard-coding the length, you should use the pre-provided length function.
        This will make your code portable to all AVR processors.
      ***/
     
      for (int i = 0 ; i < EEPROM.length() ; i++) {
        EEPROM.write(i, 0);
      }
     
      // turn the LED on when we're done
      digitalWrite(13, HIGH);
    }
     
    void loop() {
      /** Empty loop. **/
    }
    

    该代码通过使用 Write 方法遍历整个 EEPROM 并将每个值设置为零来工作。

    在使用上一个代码读取 EEPROM 值后尝试运行此代码。然后返回并再次使用 EEPROM 读取代码重新读取值。您应该会发现它们现在全为零。

    这是清除 EEPROM 的一种快速方法,但是当它写入每个位置时,它也会消耗一个有限的写入操作,因此请仅在您真正需要时运行它。


    使用外部 EEPROM

    如果 Arduino 中有限数量的非易失性存储不足以满足您的应用,那么您可以添加一个外部 EEPROM。使用 I2C 设备可以简化接线和代码。

    AT24LC256 EEPROM

    AT24LC256 是一个 256 K比特 的 EEPROM。由于一个字节中有 8 比特,这相当于 32 Kb 的非易失性存储器。

    该 I2C EEPROM 具有三个 I2C 地址线,允许您从八个可能的地址之一中进行选择。因此,如果您需要更多存储空间,您可以在设计中添加更多 AT24LC256 芯片。

    该设备也被命名为“AT24C256”(“L”代表流行的低功耗版本的芯片),具有超过 100 万次写入周期,因此它比 Arduino 中包含的 EEPROM 更强大。

    该器件提供多种封装,包括 8 引脚 DIP。该芯片的引脚排列如下:

    • A0-A2(引脚 1-3)——这些引脚决定了芯片的 I2C 地址。
    • WP(引脚 7)——这是写保护。将此引脚拉高将阻止写入 EEPROM。
    • SDA (pin 5) – Thi 是 I2C 连接的串行数据。
    • SCL(引脚 6)——这是 I2C 连接的串行时钟。

    还有一个分别用于电源和接地的 VCC(引脚 8)和 GND(引脚 4)连接。

    AT24LC256 模块

    您可以购买 8 针 DIP 的 AT24LC256,如果您正在使用电路板开发项目,这是最佳选择。但是对于面包板,还有另一种选择。

    EEPROM 也可用于几个方便的分线模块,使其更易于试验。这些模块具有 AT24LC256 芯片、用于设置 I2C 地址的跳线(或 DIP 开关)以及用于 I2C 总线的四个连接。一些模块还包含 I2C 线路所需的上拉电阻。

    在我的实验中,我将使用一个模块,因为它更容易使用,但是如果你愿意,你当然可以替换实际的芯片。

    外部 EEPROM 连接

    我们的实验将使用 Arduino Uno、AT24LC256 EEPROM 模块、电位器和小型伺服电机。我使用了一个 10K 线性锥度电位器,但 5K 以上的任何值都可以正常工作。我用的舵机是普通的SG90塑料舵机。

    请注意,在连接中,我为伺服电机使用了单独的 5 伏电源,这个方法避免了将电噪声引入 Arduino 电源线。但是,如果您愿意,可以使用 Arduino 提供的5 伏电源,但最好在电源线上并一个小型电解电容器以吸收任何噪声。

    您也可以使用 AT24LC256 8 针 DIP 代替模块,如果这样做,您可能需要在 SDA 和 SCL 线上添加几个上拉电阻。

    无论您是否使用仅一个芯片的模块,您都需要将所有 I2C 地址线接地,设置 50 位十六进制地址。您还需要将 WP(写保护)引脚接地,以便可以写入 EEPROM。

    外部 EEPROM Arduino 代码

    现在我们已经接好线,让我们看看代码。

    我们的代码将记录 EEPROM 中的伺服运动。大约一分钟后结束(如果你愿意,你可以延长它)。之后,它将等待五秒钟,然后重放动作。串行监视器将同时显示记录和回放。

    
    	/*
      External EEPROM Recording & Playback Demo
      ext_eeprom_demo.ino
      Uses AT24LC256 External I2C EEPROM
     
      DroneBot Workshop 2019
      https://dronebotworkshop.com
    */
     
    // Include the I2C Wire Library
    #include "Wire.h"
     
    // Include the Servo Library
    #include "Servo.h"
     
    // EEPROM I2C Address
    #define EEPROM_I2C_ADDRESS 0x50
     
    // Analog pin for potentiometer
    int analogPin = 0;
     
    // Integer to hold potentiometer value
    int val = 0;
     
    // Byte to hold data read from EEPROM
    int readVal = 0;
     
    // Integer to hold number of addresses to fill
    int maxaddress = 1500;
     
    // Create a Servo object
    Servo myservo;
     
     
    // Function to write to EEPROOM
    void writeEEPROM(int address, byte val, int i2c_address)
    {
      // Begin transmission to I2C EEPROM
      Wire.beginTransmission(i2c_address);
     
      // Send memory address as two 8-bit bytes
      Wire.write((int)(address >> 8));   // MSB
      Wire.write((int)(address & 0xFF)); // LSB
     
      // Send data to be stored
      Wire.write(val);
     
      // End the transmission
      Wire.endTransmission();
     
      // Add 5ms delay for EEPROM
      delay(5);
    }
     
    // Function to read from EEPROM
    byte readEEPROM(int address, int i2c_address)
    {
      // Define byte for received data
      byte rcvData = 0xFF;
     
      // Begin transmission to I2C EEPROM
      Wire.beginTransmission(i2c_address);
     
      // Send memory address as two 8-bit bytes
      Wire.write((int)(address >> 8));   // MSB
      Wire.write((int)(address & 0xFF)); // LSB
     
      // End the transmission
      Wire.endTransmission();
     
      // Request one byte of data at current memory address
      Wire.requestFrom(i2c_address, 1);
     
      // Read the data and assign to variable
      rcvData =  Wire.read();
     
      // Return the data as function output
      return rcvData;
    }
     
     
    void setup()
    {
      // Connect to I2C bus as master
      Wire.begin();
     
      // Setup Serial Monitor
      Serial.begin(9600);
     
      // Attach servo on pin 9 to the servo object
      myservo.attach(9);
     
      // Print to Serial Monitor
      Serial.println("Start Recording...");
     
      // Run until maximum address is reached
     
      for (int address = 0; address <= maxaddress; address++) {
     
        // Read pot and map to range of 0-180 for servo
        val = map(analogRead(analogPin), 0, 1023, 0, 180);
     
        // Write to the servo
        // Delay to allow servo to settle in position
        myservo.write(val);
        delay(15);
     
        // Record the position in the external EEPROM
        writeEEPROM(address, val, EEPROM_I2C_ADDRESS);
     
        // Print to Serial Monitor
        Serial.print("ADDR = ");
        Serial.print(address);
        Serial.print("\t");
        Serial.println(val);
     
      }
     
      // Print to Serial Monitor
      Serial.println("Recording Finished!");
     
      // Delay 5 Seconds
      delay(5000);
     
      // Print to Serial Monitor
      Serial.println("Begin Playback...");
     
      // Run until maximum address is reached
     
      for (int address = 0; address <= maxaddress; address++) {
     
        // Read value from EEPROM
        readVal = readEEPROM(address, EEPROM_I2C_ADDRESS);
     
     
        // Write to the servo
        // Delay to allow servo to settle in position
        // Convert value to integer for servo
        myservo.write(readVal);
        delay(15);
     
        // Print to Serial Monitor
        Serial.print("ADDR = ");
        Serial.print(address);
        Serial.print("\t");
        Serial.println(readVal);
     
      }
     
      // Print to Serial Monitor
      Serial.println("Playback Finished!");
     
    }
     
    void loop()
    {
     
      // Nothing in loop
     
    }
    

    与内部 EEPROM 不同,我们将使用I2C 的 Arduino Wire 库以及Servo 库。这两个库都已包含在您的 Arduino IDE 中。

    包含所需的库后,我们设置了一些常量和变量。

    • EEPROM_I2C_ADDRESS – 表示 EEPROM I2C 地址的常量,在我们的例子中是 十六进制50。
    • 模拟引脚——连接到电位器抽头的引脚,在本例中为 A0。
    • val – 一个整数,用于表示记录期间发送到伺服电机的值。
    • readVal – 一个整数,用于表示播放期间发送到伺服电机的值。
    • maxaddress – 我们要使用的最高地址位置。如果您希望可以增加此值,我使用 1500 来最小化运行演示所需的时间。

    然后我们创建一个名为myservo的伺服对象来表示电机。

    接下来,我们定义两个函数,writeEEPROMreadEEPROM,它们分别执行我们的 EEPROM 写入和读取。

    writeEEPROM函数将存储器地址、您希望写入该地址的数据和 EEPROM I2C 地址作为输入然后它连接到 EEPROM 并将存储器地址作为两个独立字节传递。这是因为 I2C 总线一次只允许您传输一个地址字节。

    接下来,我们将希望记录的值放到 I2C 总线上。之后,我们结束传输。

    我还在写入后添加了 5ms 的延迟,因为 EEPROM 需要在写入之间这样做。

    readEEPROM函数将存储器地址和 I2C 地址作为输入。然后它连接到 I2C 总线,传递地址信息并结束传输。这会导致 EEPROM 将指定地址的数据放入其输出缓冲区,以供主机读取。我们读取该值,然后将其输出以结束函数。

    setup是我们将所有内容放在一起的地方。

    我们首先连接到 I2C 总线作为主机,设置串行监视器并连接到引脚 9 上的伺服电机。

    打印到串行监视器后,我们进入 for-next 循环,遍历所有地址。在每个地址上,我们从连接电位器的模拟端口捕获值,并将其转换为伺服电机的 0-180 值。

    我们写入伺服器并允许它延迟 15ms 以使其就位。然后我们将值写入 EEPROM 并将其打印到串行监视器。

    循环浏览地址后,我们再次打印到串行监视器并等待五秒钟。

    然后我们再次遍历地址。这次我们读取每个值并将其写入串行监视器和伺服电机。我们再次为伺服提供延迟。

    最后,我们打印到串行监视器并结束设置。

    由于所有“动作”都发生在setup,因此loop中没有任何事情可做。

    运行外部 EEPROM 代码

    将代码加载到您的 Arduino 并开始转动电位器。您应该相应地观察伺服转动,以及串行监视器上显示的数据。

    大约一分钟后,录制将结束。延迟 5 秒后,电机将开始自行移动,与您记录的模式相同。


    您可以对此代码进行许多修改,甚至可以在基础上使用它作为记录器和播放单元,并添加几个按钮和一些状态 LED。

    或者,作为一个简单的实验,尝试在第一次运行后删除执行记录的代码部分。然后再次运行代码,仅使用播放功能。您应该观察电机以相同的模式转动。然后,您可以关闭所有电源并再次启动 Arduino,非易失性 EEPROM 存储器将使伺服器像以前一样完成它的步伐。

    结论

    在您的 Arduino 项目中拥有一些非易失性存储器确实可以增加一个新的维度。如果您只需要存储一些参数,您可以使用 Arduinos 内部 EEPROM。而对于大内存需求,可以使用外部 EEPROM。只要记住 EEPROM 的局限性,不要太频繁地写入它.

    Tags:

    最近发表
    标签列表