c语言_at函数具体怎么用?

99ANYc3cd6
预计阅读时长 13 分钟
位置: 首页 C语言 正文

__at() 是什么?

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

c语言_at
(图片来源网络,侵删)

这个功能在嵌入式系统开发中至关重要,因为开发者需要精确地控制硬件资源,

  • 将变量放在特定的 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 表示这个指针本身的地址是固定的,不能修改。

c语言_at
(图片来源网络,侵删)

示例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() 修饰符时,它会在编译和链接阶段执行以下操作:

  1. 编译阶段:编译器识别出这个变量需要被定位到特定地址。
  2. 链接阶段:链接器在生成最终的 .elf.hex 文件时,会在链接器脚本中为这个变量分配指定的地址,它会修改符号表,确保该变量的虚拟地址和加载地址都指向你指定的 address

__at() 是一个给链接器的“指令”,告诉它:“嘿,这个变量,别按常规给我分配地址了,就把它放在 XXX 这个地方!”

c语言_at
(图片来源网络,侵删)

重要注意事项和局限性

  1. 非标准,可移植性差:这是最重要的一点。__at() 是编译器特有的,使用它的代码无法直接移植到不提供此功能的编译器(如 GCC、Clang、MSVC)上,在通用操作系统(如 Windows, Linux)的开发中,几乎用不到它。

  2. 地址必须是有效的:你指定的地址必须是目标硬件上合法且可用的内存或外设地址,如果地址无效,可能会导致程序运行时崩溃(Hard Fault)或不可预测的行为。

  3. 地址对齐:某些硬件要求数据必须按照特定字节边界对齐(32位数据必须放在4字节对齐的地址上),如果你的 __at() 地址不满足对齐要求,可能会导致硬件访问错误,编译器有时会对此发出警告。

  4. 不能用于所有类型__at() 可以用于全局变量、静态局部变量,它不能用于函数参数、自动局部变量(栈变量)或函数。

  5. #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 标准提供的指针强制转换方法。

-- 展开阅读全文 --
头像
织梦播放器插件教程,如何快速安装使用?
« 上一篇 04-21
织梦404页面文件是什么?如何自定义?
下一篇 » 04-21

相关文章

取消
微信二维码
支付宝二维码

目录[+]