PAYATHON 2026

Arduino 中如何正确读取 PROGMEM 字符串数组?

数据小陈

在经典 AVR Arduino 上,存放在 PROGMEM 中的指针表需要用 pgm_read_ptr() 读取,不能直接通过 myCharArray[x] 取出指针。原来的声明只将指针数组放进了 Flash,指针指向的字符串并没有一起放进去。

要把字符串也存入 Flash,需要分别用 PROGMEM 声明每个字符串。输出时先读取指针,再用 strcpy_P() 将字符串复制到 RAM 缓冲区。

为什么固定下标能用,循环读取却失败?

原声明中的 myCharArray 是字符串指针数组:

const char *const myCharArray[] PROGMEM = {
    "C/4/", "Y/1000/H/", "X/1000/H/", "D/-1000/-1000/L/", "Z/100/H/"
};

这里有两层数据:数组元素保存的指针,以及指针指向的字符。PROGMEM 只作用于声明的数组,不会把指针指向的字符串自动放进 Flash。AVR-LibC 的字符串数组说明中有对应示例。

在程序空间与数据空间分离的 AVR 上,直接读取 myCharArray[x] 会使用普通数据空间的访问方式,无法可靠地读出 Flash 中的指针。

固定下标看起来正常,可能是编译器在编译时直接替换了已知的指针值,绕过了实际读取指针表的过程。这只是对现象的推测;没有开发板型号、编译选项和反汇编结果,就不能确认具体的优化过程,也不能据此认为固定下标访问是正确写法。

只修正原声明的读取方式

如果暂时保留原来的数组声明,字符串仍按普通数据空间字符串处理,可以先这样读取指针:

for (uint8_t x = 1; x <= 3; ++x) {
    const char *p =
        static_cast<const char *>(pgm_read_ptr(&myCharArray[x]));
    Serial.println(p);
}

&myCharArray[x] 计算数组元素的地址,pgm_read_ptr() 从该 Flash 地址读取指针值。原声明中的字符串没有单独标记为 PROGMEM,因此不要对 p 使用 strcpy_P()。

将指针表和字符串都放进 PROGMEM

将 myCommand.h 改为:

#ifndef COMMAND_H
#define COMMAND_H

#include <avr/pgmspace.h>

const char command0[] PROGMEM = "C/4/";
const char command1[] PROGMEM = "Y/1000/H/";
const char command2[] PROGMEM = "X/1000/H/";
const char command3[] PROGMEM = "D/-1000/-1000/L/";
const char command4[] PROGMEM = "Z/100/H/";

const char *const myCharArray[] PROGMEM = {
    command0,
    command1,
    command2,
    command3,
    command4
};

#endif

主 sketch 改为:

#include "myCommand.h"

void setup() {
    Serial.begin(9600);

    // command3 is the longest string in the current table.
    char buffer[sizeof(command3)];

    for (uint8_t x = 1; x <= 3; ++x) {
        const char *p =
            static_cast<const char *>(pgm_read_ptr(&myCharArray[x]));

        strcpy_P(buffer, p);
        Serial.println(buffer);
    }
}

void loop() {
}

这段代码先用 pgm_read_ptr() 从 Flash 中取出字符串地址,再用 strcpy_P() 把该地址处的字符串复制到 RAM。随后,Serial.println(buffer) 输出 RAM 中的字符串。这与 AVR-LibC 的 PROGMEM 字符串读取方法一致。

按当前数据和循环范围,预期依次输出:

Y/1000/H/
X/1000/H/
D/-1000/-1000/L/

需要注意的细节

  • 原示例的 #include myCommand.h 缺少引号,应写成 #include "myCommand.h"。如果实际代码也是如此,会先出现预处理错误。
  • 数组下标从 0 开始,当前有效范围为 0 到 4。循环 1 到 3 会跳过首尾两项。
  • sizeof(command3) 包含末尾的 \0,足以容纳当前最长字符串。以后新增更长的命令时,需要同步调整缓冲区;strcpy_P() 不检查目标容量。
  • 上述解释以经典 AVR 的独立程序空间访问方式为前提。部分 AVR 和其他 Arduino 平台的内存映射不同,不能把这一读取限制直接推广到所有开发板。

备注:内容仅供参考。