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"));
string_table
数组读取一个字节。该数组可能为20KB,并且永远无法容纳在内存中(即使是暂时的)。但是,您可以使用上述方法仅加载一个索引。