PROGMEM:是否必须将数据从闪存复制到RAM才能读取?


8

我在理解内存管理时遇到了一些困难。

Arduino文档说,可以在程序存储器中将常量(如字符串)或任何我不想更改的常量保留在程序存储器中。我认为它嵌入在代码段中的某个位置,这在von-Neumann体系结构内必须是完全可能的。我想以此来使我的UI菜单显示在LCD上。

但是,我对于那些仅从程序存储器中读取和打印数据的指令感到困惑:

strcpy_P(buffer, (char*)pgm_read_word(&(string_table[i]))); // Necessary casts and dereferencing, just copy. 
    Serial.println( buffer );

为什么在访问前我必须将该死的内容复制到RAM?如果这是真的,那么所有代码​​会怎样?它在执行之前也已加载到RAM吗?那么仅使用2kiB RAM时如何处理代码(32kiB)?那些拿着软盘的小妖精在哪里?

更有趣的是:像这样的表达式中的文字常量会发生什么:

a = 5*(10+7)

在将5、10和7加载到寄存器之前,它们真的被复制到RAM了吗?我简直不敢相信。


全局变量被加载到内存中,并且永远不会从中释放。上面的代码仅在需要时将数据复制到内存,并在完成后将其释放。另请注意,上面的代码仅从string_table数组读取一个字节。该数组可能为20KB,并且永远无法容纳在内存中(即使是暂时的)。但是,您可以使用上述方法仅加载一个索引。
Gerben 2014年

@Gerben:这是全局变量的真正缺点,我还没有考虑到这一点。我现在有点头疼。代码片段只是文档中的一个示例。我没有做某事。我自己先弄清楚概念。但是我现在有了一些见识。谢谢!
Ariser-恢复莫妮卡2014年

初次阅读时,我发现文档有些混乱。也尝试看一些现实生活中的例子(例如图书馆)。
Gerben 2014年

Answers:


10

AVR是改良的哈佛架构家族,因此代码仅存储在闪存中,而数据在被操纵时主要存在于RAM中。

考虑到这一点,让我们解决您的问题。

为什么在访问前我必须将该死的内容复制到RAM?

您本身并不需要,但是默认情况下,代码假定数据位于RAM中,除非修改了代码以专门在Flash中查找数据(例如使用strcpy_P())。

如果这是真的,那么所有代码​​会怎样?它在执行之前也已加载到RAM吗?

不。哈佛建筑。有关完整的详细信息,请参见Wikipedia页面。

那么仅使用2kiB RAM时如何处理代码(32kiB)?

在运行实际程序之前,由编译器生成的前同步码将应修改/修改的数据复制到SRAM中。

那些拿着软盘的小妖精在哪里?

不知道。但是,如果您碰巧看到它们,那么我无能为力。

...将5、10和7真正复制到RAM之后再加载到寄存器中吗?

没事 编译器在编译时评估表达式。无论发生什么情况,都取决于周围的代码行。


好的,我不知道AVR是哈佛。但是我对这个概念很熟悉。除了地精,我想我现在知道什么时候使用那些复制功能。我必须将PROGMEM的使用限制为很少用于节省CPU周期的数据。
Ariser-恢复莫妮卡2014年

或修改您的代码以直接从Flash使用它。
伊格纳西奥·巴斯克斯

但是这段代码看起来如何?假设我有几个uint8_t数组,它们表示要通过SPI放入LCD显示器的字符串。const uint8_t test1[5]= { 0x54, 0x65, 0x73, 0x74, 0x31 }; const uint8_t bla[9]= { 0x62, 0x6c, 0x61, 0x62, 0x6c, 0x61, 0x62, 0x6c, 0x62 }; const uint8_t Menu[4]= { 0x3d, 0x65, 0x6e, 0x75};我该如何使这些数据闪烁并随后进入SPI.transfer()函数,该函数每次调用需要一个uint8_t。
Ariser-恢复莫妮卡2014年


8

这是Print::print从Arduino库中程序存储器中打印的方式:

size_t Print::print(const __FlashStringHelper *ifsh)
{
  const char PROGMEM *p = (const char PROGMEM *)ifsh;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

__FlashStringHelper*是一个空类,它允许诸如print之类的重载函数将指向程序存储器的指针从一个指向普通存储器的指针区分开来,因为两者都被const char*编译器视为(请参见/programming/16597437/arduino-f-实际上是做什么的

因此,您可以在printLCD显示器上重载该函数,以便它接受一个__FlashStringHelper*参数,让它调用它LCD::print,然后使用lcd.print(F("this is a string in progmem"));' to call it.F()`是确保该字符串在程序存储器中的宏。

为了预定义字符串(与内置的Arduino打印兼容),我使用了:

const char firmware_version_s[] PROGMEM = {"1.0.2"};
__FlashStringHelper* firmware_version = (__FlashStringHelper*) firmware_version_s;
...
Serial.println(firmware_version);

我认为另一种选择是

size_t LCD::print_from_flash(const char *pgms)
{
  const char PROGMEM *p = (const char PROGMEM *) pgms;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

这样可以避免__FlashStringHelper演员


2

Arduino文档说,可以在程序存储器中将常量(如字符串)或任何我不想更改的常量保留在程序存储器中。

所有常量最初都在程序存储器中。当电源关闭时,它们还会在哪里?

我认为它嵌入在代码段中的某个位置,这在von-Neumann体系结构内必须是完全可能的。

它实际上是哈佛建筑

为什么在访问前我必须将该死的内容复制到RAM?

你不知道 实际上,有一条硬件指令(LPM-加载程序存储器)将数据直接从程序存储器移到寄存器中。

我在Arduino Uno输出到VGA监视器中有此技术的示例。在该代码中,有一个位图字体存储在程序存储器中。它是从即时读取的,并复制到输出中,如下所示:

  // blit pixel data to screen    
  while (i--)
    UDR0 = pgm_read_byte (linePtr + (* messagePtr++));

这些行的反汇编显示了(部分):

  f1a:  e4 91           lpm r30, Z+
  f1c:  e0 93 c6 00     sts 0x00C6, r30

您可以看到程序存储器的一个字节已复制到R30,然后立即存储到USART寄存器UDR0中。不涉及RAM。


但是,这很复杂。对于普通字符串,编译器希望在RAM中找到数据,而不是PROGMEM。它们是不同的地址空间,因此RAM中的0x200与PROGMEM中的0x200有所不同。因此,编译器会在程序启动时遇到将常量(如字符串)复制到RAM的麻烦,因此不必担心以后会知道它们之间的区别。

那么仅使用2kiB RAM时如何处理代码(32kiB)?

好问题。拥有超过2 KB的常量字符串将无处可逃,因为没有空间可以全部复制它们。

这就是为什么编写菜单和其他冗杂内容之类的人会采取额外的步骤为字符串提供PROGMEM属性,从而禁止将它们复制到RAM中的原因。

但是,我对于那些仅从程序存储器中读取和打印数据的指令感到困惑:

如果添加PROGMEM属性,则必须采取措施使编译器知道这些字符串在不同的地址空间中。制作完整(临时)副本是一种方法。或者只是直接从PROGMEM打印,一次打印一个字节。一个例子是:

// Print a string from Program Memory directly to save RAM 
void printProgStr (const char * str)
{
  char c;
  if (!str) 
    return;
  while ((c = pgm_read_byte(str++)))
    Serial.print (c);
} // end of printProgStr

如果将此函数的指针传递给PROGMEM中的字符串,它将执行“特殊读取”(pgm_read_byte)以从PROGMEM中而不是RAM中提取数据,并进行打印。注意,这每个字节需要一个额外的时钟周期。

甚至更有趣的是:像这样的表达式a = 5*(10+7)中的文字常量会在将它们加载到寄存器之前将5、10和7真正复制到RAM中发生什么情况?我简直不敢相信。

不,因为它们不一定必须如此。这将编译为“将文字加载到寄存器”指令。该指令已存在于PROGMEM中,因此现在处理字面量。无需将其复制到RAM,然后再读回。


我在将常量数据放入程序存储器(PROGMEM)页面上对这些事情有很长的描述。上面有示例代码,可以很容易地设置字符串和字符串数组。

它还提到了F()宏,这是从PROGMEM中简单打印的一种简便方法:

Serial.println (F("Hello, world"));

一点点预处理器的复杂性使它可以编译成一个辅助函数,该函数一次将一个字节从PROGMEM中拉出字符串中的字节。无需中间使用RAM。

通过从Print类派生LCD打印,可以很容易地将该技术用于串行以外的其他事情(例如LCD)。

例如,在我编写的一个LCD库中,我正是这样做的:

class I2C_graphical_LCD_display : public Print
{
...
    size_t write(uint8_t c);
};

这里的关键点是从Print派生,并覆盖“ write”功能。现在,您的重写函数将执行输出字符所需的任何操作。由于它是从Print派生的,因此您现在可以使用F()宏。例如。

lcd.println (F("Hello, world"));
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.