9

在某些架构上,可能需要为其他相同的对象使用不同的指针类型。特别是对于哈佛架构 CPU,您可能需要以下内容:

uint8_t const ram* data1;
uint8_t const rom* data2;

特别是对于 PIC 的 MPLAB C18(现已停产)中指向 ROM/RAM 的指针的定义是这样的。它甚至可以定义如下内容:

char const rom* ram* ram strdptr;

这意味着 RAM 中的指针指向 RAM 中指向 ROM 中的字符串的指针(使用ram不是必需的,因为默认情况下,此编译器在 RAM 中,只是为了清楚起见添加了所有内容)。

这种语法的好处是,当您尝试以不兼容的方式分配时,编译器能够提醒您,例如将 ROM 位置的地址指向 RAM 的指针(例如data1 = data2;,或将 ROM 指针传递给函数使用 RAM 指针会产生错误)。

与此相反,在 AVR-8 的 avr-gcc 中,没有这种类型安全性,因为它提供了访问 ROM 数据的功能。没有办法区分指向 RAM 的指针和指向 ROM 的指针。

在某些情况下,这种类型安全对于捕获编程错误非常有益。

有没有办法以某种方式向指针添加类似的修饰符(例如通过预处理器,扩展为可以模仿这种行为的东西)来达到这个目的?甚至是警告不正确访问的东西?(在 avr-gcc 的情况下,尝试在不使用 ROM 访问函数的情况下获取值)

4

4 回答 4

7

一个技巧是将指针包装在一个结构中。指向结构的指针比指向原始数据类型的指针具有更好的类型安全性。

typedef struct
{
  uint8_t ptr;
} a_t;

typedef struct
{
  uint8_t ptr;
} b_t;

const volatile a_t* a = (const volatile a_t*)0x1234;
const volatile b_t* b = (const volatile b_t*)0x5678;

a = b; // compiler error
b = a; // compiler error
于 2018-04-09T12:59:00.393 回答
4

您可以将指针封装在 RAM 和 ROM 的不同结构中,使类型不兼容,但包含相同类型的值。

struct romPtr {
    void *addr;
};

struct ramPtr {
    void *addr;
};

int main(int argc, char **argv) {
    struct romPtr data1 = {NULL};
    struct romPtr data3 = data1;
    struct ramPtr data2 = data1; // <-- gcc would throw a compilation error here
}

在编译期间:

$ cc struct_test.c
struct_test.c: In function ‘main’:
struct_test.c:12:24: error: invalid initializer
  struct ramPtr data2 = data1;
                    ^~~~~

为简洁起见,您当然可以typedef使用 struct

于 2018-04-09T12:57:39.083 回答
2

由于我收到了几个答案,它们在提供解决方案时提供了不同的折衷方案,因此我决定将它们合并为一个,概述每个答案的优缺点。因此,您可以选择最适合您的特定情况的

命名地址空间

对于解决这个问题的特殊问题,只有 AVR-8 微控制器上的 ROM 和 RAM 指针这种情况,最合适的解决方案是这个。

这是一个针对 C11 的提案,它没有成为最终标准,但是有 C 编译器支持它,包括用于 8 位 AVR 的 avr-gcc。

可以在此处访问相关文档(在线 GCC 手册的一部分,还包括使用此扩展的其他架构)。它优于其他解决方案(例如 AVR-8 的 pgmspace.h 中的类似函数的宏),因为这样,编译器可以进行适当的检查,否则访问指向的数据仍然清晰和简单。

特别是,如果您在从提供某种命名地址空间的编译器(如 MPLAB C18)中移植某些东西时遇到类似的问题,这可能是最快和最干净的方法。

从上面移植的指针如下所示:

uint8_t const* data1;
uint8_t const __flash* data2;
char const __flash** strdptr;

(如果可能,可以使用适当的预处理器定义来简化该过程)

(奥拉夫的原始答案)

struct封装,里面有指针

此方法旨在通过将指针包装在结构中来增强指针的类型。预期用途是跨接口传递结构本身,编译器可以通过这些接口对它们执行类型检查。

指向字节数据的“指针”类型可能如下所示:

typedef struct{
    uint8_t* ptr;
}bytebuffer_ptr;

可以按如下方式访问指向的数据:

bytebuffer_ptr bbuf;
(...)
bbuf.ptr = allocate_bbuf();
(...)
bbuf.ptr[index] = value;

接受这种类型并返回一个的函数原型可能如下所示:

bytebuffer_ptr encode_buffer(bytebuffer_ptr inbuf, size_t len);

(dvhh的原始答案)

struct封装,指针外接

与上面的方法类似,它旨在通过将指针包装在结构中来加强指针的类型化,但以不同的方式,提供更健壮的约束。要指向的数据类型是封装的。

指向字节数据的“指针”类型可能如下所示:

typedef struct{
    uint8_t val;
}byte_data;

可以按如下方式访问指向的数据:

byte_data* bbuf;
(...)
bbuf = allocate_bbuf();
(...)
bbuf[index].val = value;

接受这种类型并返回一个的函数原型可能如下所示:

byte_data* encode_buffer(byte_data* inbuf, size_t len);

(伦丁的原始答案)

我应该使用哪个?

在这方面,命名地址空间不需要太多讨论:如果您只想处理目标处理地址空间的特殊性,它们是最合适的解决方案。编译器将为您提供所需的编译时检查,您不必尝试进一步发明任何东西。

但是,如果由于其他原因您对结构包装感兴趣,那么您可能需要考虑以下事项:

  • 两种方法都可以优化得很好:至少 GCC 会从使用普通指针生成相同的代码。因此,您实际上不必考虑性能:它们应该可以工作。

  • 如果您有第三方接口来服务哪些需求指针,或者如果您正在重构无法一次性完成的大项目,则内部指针很有用。

  • 外部指针提供了更强大的类型安全性,因为您使用它来加强指向类型本身:您拥有一个真正的独特类型,您不能轻易(意外地)转换(隐式转换)。

  • 外部指针允许您在指针上使用修饰符,例如添加const,这对于创建健壮的接口很重要(您可以使数据仅供函数读取const)。

  • 请记住,有些人可能不喜欢其中任何一个,所以如果您在一个小组中工作,或者正在创建可能被已知方重用的代码,请先与他们讨论此事。

  • 应该很明显,但请记住,假设没有与方法一起使用命名地址空间,封装并不能解决需要特殊访问代码(例如通过 AVR-8 上的 pgmspace.h 宏)的问题。它仅提供一种方法来产生编译错误,如果您尝试通过在与它打算指向的地址空间不同的地址空间上操作的函数使用指针。

谢谢大家的回答!

于 2018-04-23T12:34:11.353 回答
-5

真正的哈佛架构使用不同的指令来访问不同类型的内存,例如代码(AVR 上的闪存)、数据 (RAM)、硬件外围寄存器 (IO) 以及可能的其他内存。范围内的地址通常重叠,即相同的值访问不同的内部设备,具体取决于指令。

回到C,如果要使用统一的指针,这意味着不仅要对指针值中的地址(值)进行编码,还要对访问类型(下文中的“地址空间”)进行编码。这可以使用指针值中的附加位来完成,也可以在运行时为每次访问选择适当的指令。这对生成的代码构成了很大的开销。此外,对于至少一些地址空间,通常在“自然”值中没有备用位(例如,指针的所有 16 位都已用于地址)。所以需要额外的位,至少一个字节。这也会增加内存使用量(主要是 RAM)。

两者在使用这种架构的典型 MCU 上通常是不可接受的,因为它们已经非常有限。幸运的是,对于大多数应用程序来说,在运行时确定地址空间是绝对没有必要的(或者至少很容易避免)。

为了解决这个问题,这种平台的所有编译器都支持某种方式来告诉编译器地址空间和对象所在的位置。即将推出的 C11 的标准草案 N1275 提出了一种使用“命名地址空间”的标准方法。不幸的是,它没有进入最终版本,所以我们留下了编译器扩展。

对于 gcc(请参阅其他编译器的文档),开发人员实现了原始标准提案。由于地址空间是特定于目标的,因此代码在不同的架构之间是不可移植的,但是对于裸机嵌入式代码来说通常是这样,没有什么真正丢失。

阅读 AVR 的文档,地址空间的使用类似于标准限定符。编译器会自动发出正确的指令来访问正确的空间。还有一个统一的地址空间,它在运行时确定区域,如上所述。

地址空间的工作方式类似于限定符,有更强的约束来确定兼容性,即在将不同地址空间的指针相互分配时。有关详细说明,请参阅提案第 5 章

结论:

命名地址空间是您想要的。他们解决了两个问题:

  • 确保指向不兼容地址空间的指针不能在不被注意的情况下相互分配。
  • 告诉编译器如何访问对象,即使用哪些指令。

关于提出structs 的其他答案,您必须在访问数据后指定地址空间(以及 的类型void *)。在声明中使用地址空间使其余代码保持干净,甚至允许稍后在源代码中的单个位置更改它。

如果您追求工具链之间的可移植性,请阅读他们的文档并使用宏。您很可能只需要采用地址空间的实际名称。

旁注:您引用的 PIC18 示例实际上使用了命名地址空间的语法。仅不推荐使用名称,因为实现应该为应用程序代码保留所有非标准名称。因此 gcc 中的下划线限定名称。

免责声明:我没有测试这些功能,但依赖于文档。评论中的有用反馈表示赞赏。

于 2018-04-10T13:05:09.890 回答