AVR芯片编程时如何定义内存指针?

How to define memory pointers when programming AVR chips?

序言:作为应用程序开发人员工作了几年后,软件工程的世界变得比以前更加晦涩难懂。原因是真正的东西隐藏在无数层抽象之下:OS、框架等。年轻一代被剥夺了使用 PDP-like 机器工作的乐趣,其中所有编程都是通过电气开关完成的切换。另一个问题是现代编程语言的短暂性。曾经有 Python 2.x,现在已弃用,Python 3.x 将在几个月后弃用。其他语言同上。 ANSI C 看起来像胡夫金字塔:它在 70 年代就在那里,我不怀疑它会在太阳变成红矮星之后出现。

看来现在要想了解硬件和软件的交互,就只有玩嵌入式开发了。从教学的角度来看,物理芯片非常方便,因为它们可以解决 C 语言中最困难的部分,即指针。在 OS 环境中编码时, */& 符号仍然很混乱,因为它指的是虚拟内存内部某处的某个位置。在你理解什么是虚拟内存之前,你必须阅读几本关于 OS 开发等的专着。你可能会觉得它很愚蠢,但我真的很想知道哪个晶体管在控制我的位置马上。至少,我可以将物理引脚电压连接到编程抽象。

由于大量教科书和可访问的硬件,目前我正在使用 Atmel 芯片和 WinAVR 软件包。尽管所有书籍都承诺使用普通 C 来教授 AVR 编码,但现实是所有指针都隐藏在 PORTA、DDRB 等宏的后面。所有代码示例都包含 header 文件 'io.h',而 'io.h' 又指向其他 header 特定于给定芯片的文件,例如 'iomx8.h'。到目前为止,我在这些 header 中找不到任何宏定义。增加 Atmega168 物理引脚 14 电压的代码看起来像

DDRB = 0x01;
PORTB = 0x01;

幸运的是,Microchip 网站提供了一些基本文档,其中指出,例如,如果我想提高物理引脚 14 上的电压,我需要按照以下步骤操作:

unsigned char *ddrB;
ddrB = (unsigned char*)0x24; // the address of ddrB is 0x24
*ddrB |= 0x01; // set up low impedance/ high current state for the transistor 0 

unsigned char *portB;
portB = (unsigned char*)0x25;
*portB |= 0x01; // voltage on
*portB &= ~(0x01); // voltage off

很遗憾,这是我潜伏一周后得到的唯一信息。现在我正在进行 USART 编程,所有这些 UBRR0H、UCSR0C 使事情变得更加复杂。由于提供的 header 文件不包含任何寄存器的宏定义,我还能在哪里找到它?

几年前有人问过类似的问题:accessing AVR registers with C?。然而,提供的答案有些无用,除了 GCC 本身可以将一些神秘的 PORTB 映射到真实物理位置的线索之外。有人可以描述映射背后的机制吗?

从memory-mapping的角度来看:通用寄存器、特殊功能+I/O寄存器和SRAM共享non-overlapping范围单个地址space,如 AVR 系列中各种处理器的数据表中所述。您的所有指针都将引用此内存 space,除非注释为指向 PROGMEM 的指针(这将导致发出不同的指令)。将在没有任何类型的虚拟内存映射的情况下进行引用。

例如,ATtiny 25/45/85 在第 18 页显示了以下地图:

您的链接器知道此内存映射并将相应地放置变量。例如,在您的一个编译单元中声明的全局变量将在上述示例设备中以 0x0060 以上的地址结束,因此它最终在 SRAM 中。

从指令编码的角度来看:虽然只有一个地址space,但为某些重要区域保留了特殊功能。例如,IN 和 OUT 指令在其指令编码中有六位,可用于直接引用 [0x20, 0x5F).

中的 64 个地址之一。

IN 和 OUT 指令的独特之处在于它们能够加载和存储到直接编码在指令中的固定地址,因为正常的加载和存储指令需要间接加载 'Z' 寄存器首先.

因此,当编译器看到对固定 I/O 寄存器的内存操作时,它 可能 生成这些更有效的指令。然而,通过指针的正常 load/store 将具有相同的效果(尽管需要不同数量的时钟周期)。对于不适合前 64 个的扩展 I/O 寄存器(例如 atmega328p 上的 OSCCAL),将始终生成正常的 load/store 指令。

简短回答 - 隐藏在 Atmel 随附的 headers 中的是一个 collection 宏,它们创建指向寄存器位置的指针。如果您想查看任何来源,以及其他必要的 headers,例如 interrupt.h,它们位于 WinAVR-20100110/avr/include/

以下是该过程的简要概述:

您的 Makefile 定义要使用的设备,然后将其定义传递给编译器。

DEVICE = atmega2560
...
-D__$(DEVICE)__

然后您包含 io.h,它会根据您的设备自动包含必要的 headers:

// In main source file
#include <io.h>    

// In io.h
#include <avr/sfr_defs.h>
// ...
#elif defined (__AVR_ATmega2560__)
    #  include <avr/iom2560.h>

// In sfr_defs.h
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
#define __SFR_OFFSET 0x20
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)

// In iom2560.h
#include <avr/iomxx0_1.h>
// Other device specific definitions

// Om iomxx0_1.h
#define PINA    _SFR_IO8(0X00)
// Other device family shared definitions

所以如果你展开所有这些,你得到的是一个指向寄存器地址的可变指针。当您在代码中使用 PINA 时,预处理器会将其替换为所有扩展的宏:

PINA
_SFR_IO8(0X00)
_MMIO_BYTE((0X00) + __SFR_OFFSET)
(*(volatile uint8_t *)((0X00) + 0x20))

指定 PINA 是指向 0x20 的易失性 8 位内存地址的指针。内部芯片架构然后将该地址映射到适当的外设寄存器,只要它被访问。

不同的设备有不同的寄存器地址和偏移量。如果您想自己定义,则需要查看相关的数据表。对于大多数 AVR 芯片,末尾有一个标题为“寄存器摘要”的部分,其中列出了所有寄存器地址和各个控制位的名称。根据我的经验(至少对于 AVR,数据表中的寄存器和位的名称与 io.h 文件中的定义完全相同。

另请注意使用“uint8_t”而不是“char”。在适当的时候使用 中的 bit-width 特定定义来指定 signed/unsigned 和 8/16/32 位变量是很常见的(并且强烈鼓励)。由于 AVR 是 8 位的,任何 16 位或 32 位(或浮点数)变量的使用都需要多个时钟周期用于每个操作。在这种情况下,stdint.h 应该有:

typedef unsigned char uint8_t