AVR微控制器ROM中数据的32K阈值

有什么会比拐杖更糟? 只记录不完整的拐杖。


图片


这是8位AVR微控制器最新的官方集成开发环境的截图,Atmel Studio 7是C编程语言。 从“值”列中可以看到,变量my_array包含数字0x8089。 换句话说,my_array数组位于内存中,从地址0x8089开始。


同时,“类型”列为我们提供了稍有不同的信息:my_array是位于ROM中的int16_t类型的4个元素的数组(由单词prog表示,与RAM的数据不同),从地址0x18089开始。 停止,但毕竟是0x8089!= 0x18089。 数组的实际地址是什么?


C语言和哈佛架构


由Atmel以及现在的Microchip制造的8位AVR微控制器之所以受欢迎,尤其是因为它们是根据哈佛架构构建的Arduino的基础,也就是说,代码和数据位于不同的地址空间中。 官方文档包含两种语言的代码示例:汇编程序和C。 以前,制造商提供了一个仅支持汇编程序的免费集成开发环境。 但是那些想用C甚至C ++编程的人呢? 有付费解决方案,例如IAR AVR和CodeVisionAVR。 就我个人而言,我从未使用过它,因为当我在2008年开始对AVR进行编程时,已经有免费的WinAVR,它能够与AVR Studio 4集成,并且它被简单地包含在当前的Atmel Studio 7中。


WinAVR项目基于GNU GCC编译器,该编译器是为von Neumann体系结构开发的,该体系结构意味着代码和数据的单个地址空间。 当使GCC适应AVR时,应用了以下拐杖:将地址0到0x007fffff分配给代码(ROM,闪存),将0x00800100分配给数据(RAM,SRAM)的0x0080ffff。 还有各种各样的其他技巧,例如,从0x00800000到0x008000ff的地址表示的寄存器可以由与RAM相同的操作码访问。 原则上,如果您是一个简单的程序员,例如初学者arduino,而不是黑客,并且在同一固件中混合了汇编程序和C / C ++,那么您不需要了解所有这一切。


除编译器外,WinAVR还以AVR Libc项目的形式包括各种库(标准C库和AVR特定模块的一部分)。 最新版本2.0.0大约在三年前发布,该文档不仅在项目本身的站点上可用,而且在微控制器制造商的站点上可用。 也有非官方的俄语翻译。


代码地址空间中的数据


有时,在微控制器中,您不仅需要放置很多数据,而且还需要放置大量数据:如此之多,以至于它们根本无法放入RAM。 而且,这些数据是不可变的,在固件时是已知的。 例如,光栅图片,旋律或某种表格。 同时,代码通常仅占用可用ROM的一小部分。 那么,为什么不使用剩余空间存储数据呢? 容易! avr-libc 2.0.0文档涵盖了程序空间中5数据的整个章节。 如果省略有关线条的部分,则一切都非常简单。 考虑一个例子。 对于RAM,我们这样写:


unsigned char array2d[2][3] = {...}; unsigned char element = array2d[i][j]; 

对于这样的ROM:


 #include <avr/pgmspace.h> const unsigned char array2d[2][3] PROGMEM = {...}; unsigned char element = pgm_read_byte(&(array2d[i][j])); 

如此简单,以至于即使在RuNet中也重复使用了该技术。


那怎么了


还记得640 KB足以满足所有人的说法吗? 还记得您是如何从16位体系结构转换为32位,以及从32位转换为64位的吗? Windows 98专为2 GB设计时,如何在超过512 MB的RAM上不稳定地工作? 您是否曾经更新过BIOS,以便主板可用于大于8 GB的硬盘? 还记得80 GB硬盘驱动器上的跳线,将它们的容量减少到32 GB吗?


当我尝试在ROM中创建至少32 KB的数组时,第一个问题使我不知所措。 为什么在ROM中而不在RAM中? 因为目前根本不存在RAM超过32 KB的8位AVR。 并且超过256 B-存在。 这可能就是为什么编译器的创建者为RAM中的指针选择16 b(2 B)(同时为int类型)选择16b(2 B)的原因,可以在阅读第11.14章的“数据类型”段落中找到它,C编译器使用哪些寄存器? AVR Libc文档。 哦,我们不打算黑客,但是这里是寄存器...但是回到阵列。 事实证明,您不能创建大于32,767 B(2 ^(16-1)-1 B)的对象。 我不知道为什么必须增加对象的长度,但这是事实:没有对象,即使是多维数组,都不能具有32,768 B或更大的长度。 有点像对64位OS中的32位应用程序(4 GB)的地址空间的限制,不是吗?


据我所知,这个问题没有解决办法。 如果要在ROM中放置长度为32,768的对象,请将其分成较小的对象。


我们再次转到段落数据类型:指针是16位。 我们将此知识应用于“程序空间数据”的第5章。 不,理论是必不可少的;需要实践。 我编写了一个测试程序,启动了调试器(不幸的是,不是软件,而是硬件),然后看到pgm_read_byte函数只能返回地址适合16位(64 KB;谢谢,不是15)的数据。 然后发生溢出,较旧的部分将被丢弃。 考虑到指针是16位的,这是合乎逻辑的。 但是出现了两个问题:为什么这不是第5章中写的(一个反问性的问题,但是是他促使我写这篇文章的原因)以及如何克服64 KB ROM的限制而不切换到汇编器。


幸运的是,除了第5章外,还有另一个25.18 pgmspace.h文件参考,从中我们了解到pgm_read_*系列函数只是对pgm_read_*_nearpgm_read_*_near ,该函数接受16位地址,并且还有pgm_read_*_far ,您可以提交32位地址 尤里卡!


我们编写代码:


 unsigned char element = pgm_read_byte_far(&(array2d[i][j])); 

它可以编译,但是不能如我们所愿的工作(如果array2d位于32 KB之后)。 怎么了 是的,因为&操作返回一个带符号的16位数字! 有趣的是, pgm_read_*_near家族pgm_read_*_near接受无符号的16位地址,也就是说,它能够处理64 KB的数据,而&操作仅对32 KB有用。


让我们继续前进。 除了pgm_read_*我们在pgmspace.h中还有pgm_read_*pgm_get_far_address(var)函数已经有半页的描述,并替换了&操作。


可能是正确的:


 unsigned char element = pgm_read_byte_far(pgm_get_far_address(array2d[i][j])); 

编译错误。 我们阅读了以下描述:链接时必须将'var'解析为现有符号,即简单的类型变量名称,数组名称(不是数组的索引元素,如果索引是常量,则编译器不会抱怨但如果启用了优化,则无法获取地址),结构名称或结构字段名称,函数标识符,链接器定义的标识符,...


我们放了另一个拐杖:我们从数组索引转到指针算术:


 unsigned char element = pgm_read_byte_far(pgm_get_far_address(array2d) + i*3*sizeof(unsigned char) + j*sizeof(unsigned char)); 

现在一切正常。


结论


如果您使用GCC编译器为8位AVR微控制器编写C / C ++并将数据存储在ROM中,则:


  • ROM大小不超过32 KB,仅通过读取程序空间中的第5章数据就不会遇到问题。
  • 对于大于32 KB的ROM,应该使用pgm_read_*_far系列函数,使用pgm_get_far_address函数代替& ,使用指针算术代替数组索引,并且任何对象的大小不能超过32,767B。

参考文献


Source: https://habr.com/ru/post/zh-CN436904/


All Articles