__at() 是什么?
__at() 不是 C 语言标准库的一部分,而是由许多嵌入式 C 编译器(如 Keil MDK for ARM, IAR, Hi-Tech C 等)提供的一个扩展功能,它的主要作用是将一个变量或数据对象(如数组、结构体)放置在指定的内存地址上。

这个功能在嵌入式系统开发中至关重要,因为开发者需要精确地控制硬件资源,
- 将变量放在特定的 RAM 区域(如 DMA 缓冲区)。
- 将变量放在特定的硬件寄存器地址。
- 将代码或数据放置在 Flash 的特定扇区。
语法和用法
__at() 的语法通常如下:
data_type variable_name __at (address);
data_type: 变量的数据类型(如unsigned char,int,struct my_type等)。variable_name: 你定义的变量名。address: 你希望该变量存放的内存地址,通常是一个十六进制常量。
示例1:定义一个位于特定地址的变量
假设你的微控制器有一个外设控制寄存器,其地址是 0x40001000,你想通过一个 C 变量来访问它。
// 定义一个无符号32位整型变量,并将其放置在地址 0x40001000
volatile unsigned int * const GPIOA_MODER __at (0x40001000);
// 在代码中使用
void main(void) {
// 将 GPIOA 的模式设置为输出
// 这等同于直接向 0x40001000 这个地址写入值
*GPIOA_MODER = 0x00000001;
// ... 其他代码 ...
}
注意:这里使用
volatile关键字非常重要,因为它告诉编译器这个变量的值可能会被硬件本身改变,防止编译器进行“不安全”的优化。const表示这个指针本身的地址是固定的,不能修改。(图片来源网络,侵删)
示例2:定义一个位于特定地址的数组
假设你需要一个 256 字节的缓冲区,用于 DMA 传输,而这个缓冲区必须放在名为 AXI_SRAM 的特定 RAM 区域,起始地址为 0x20010000。
// 定义一个大小为 256 的 unsigned char 数组,并将其放置在 0x20010000
unsigned char dma_buffer[256] __at (0x20010000);
void process_dma_data(void) {
// 使用这个缓冲区
for (int i = 0; i < 256; i++) {
dma_buffer[i] = i; // 向缓冲区写入数据
}
// ... 启动 DMA 从这个地址读取数据 ...
}
示例3:定义一个结构体数组
有时候硬件寄存器是以结构体的形式组织的,这样更具可读性。
// 定义一个描述 GPIO 寄存器的结构体
typedef struct {
volatile unsigned int MODER; // 模式寄存器
volatile unsigned int OTYPER; // 输出类型寄存器
volatile unsigned int OSPEEDR; // 输出速度寄存器
volatile unsigned int PUPDR; // 上拉/下拉寄存器
volatile unsigned int IDR; // 输入数据寄存器
volatile unsigned int ODR; // 输出数据寄存器
} GPIO_TypeDef;
// 定义一个 GPIO 端口 A,并将其放置在硬件地址 0x40020000
volatile GPIO_TypeDef GPIOA __at (0x40020000);
void main(void) {
// 设置 GPIOA 的第 5 位为输出模式
GPIOA.MODER &= ~(0x3U << (5 * 2)); // 先清零
GPIOA.MODER |= (0x1U << (5 * 2)); // 再设置为输出
// 点亮连接到 PA5 的 LED
GPIOA.ODR |= (1 << 5);
}
工作原理
当编译器遇到 __at() 修饰符时,它会在编译和链接阶段执行以下操作:
- 编译阶段:编译器识别出这个变量需要被定位到特定地址。
- 链接阶段:链接器在生成最终的
.elf或.hex文件时,会在链接器脚本中为这个变量分配指定的地址,它会修改符号表,确保该变量的虚拟地址和加载地址都指向你指定的address。
__at() 是一个给链接器的“指令”,告诉它:“嘿,这个变量,别按常规给我分配地址了,就把它放在 XXX 这个地方!”

重要注意事项和局限性
-
非标准,可移植性差:这是最重要的一点。
__at()是编译器特有的,使用它的代码无法直接移植到不提供此功能的编译器(如 GCC、Clang、MSVC)上,在通用操作系统(如 Windows, Linux)的开发中,几乎用不到它。 -
地址必须是有效的:你指定的地址必须是目标硬件上合法且可用的内存或外设地址,如果地址无效,可能会导致程序运行时崩溃(Hard Fault)或不可预测的行为。
-
地址对齐:某些硬件要求数据必须按照特定字节边界对齐(32位数据必须放在4字节对齐的地址上),如果你的
__at()地址不满足对齐要求,可能会导致硬件访问错误,编译器有时会对此发出警告。 -
不能用于所有类型:
__at()可以用于全局变量、静态局部变量,它不能用于函数参数、自动局部变量(栈变量)或函数。 -
与
#pragma的关系:一些编译器也支持使用#pragma来实现类似功能。// IAR 编译器中的另一种写法 #pragma location = 0x20010000 unsigned char dma_buffer[256];
这种方式在功能上与
__at()类似,但语法不同,具体使用哪种取决于你的编译器。
现代 C 语言的替代方案(标准方法)
虽然 __at() 方便快捷,但为了提高代码的可移植性,现代 C 语言(C99及以后)提供了更标准的方法来处理绝对地址,即使用复合字面量和指向 void 的指针。
#include <stdint.h> // 使用标准整数类型,如 uint32_t
// 定义一个指向 uint32_t 类型的指针,并让它指向绝对地址 0x40001000
volatile uint32_t * const GPIOA_MODER = (volatile uint32_t * const)0x40001000;
void main(void) {
// 使用方式和 __at() 完全一样
*GPIOA_MODER = 0x00000001;
}
这种方法的优势:
- 标准且可移植:任何符合标准的 C 编译器都支持这种写法。
- 类型安全:编译器会进行类型检查,比裸地址更安全。
- 更灵活:可以轻松地创建指向不同地址的指针。
在需要绝对地址定位的嵌入式开发中,__at() 是一个非常方便且直观的工具,尤其是在处理硬件寄存器和固定内存缓冲区时,开发者必须清楚地认识到它的非标准特性,如果项目未来可能更换编译器,或者希望代码更具通用性,强烈推荐使用现代 C 标准提供的指针强制转换方法。

