【问题标题】:Is there any C standard for microcontrollers?微控制器是否有任何 C 标准?
【发布时间】:2010-07-23 14:22:50
【问题描述】:

微控制器有什么特殊的 C 标准吗?

我问是因为到目前为止,当我在 Windows 操作系统下编写某些东西时,我使用的编译器并不重要。如果我有一个C99 的编译器,我知道我可以用它做什么。

但最近我开始用 C 为微控制器编程,我很震惊,即使它的基础仍然是 C,比如循环、变量创建等等,有一些我从未见过的用于台式计算机的 C 语法类型.此外,语法随着版本的变化而变化。我用的是AVR-GCC编译器,在以前的版本中,你使用端口I/O的函数,现在你可以在新版本中像变量一样处理端口。

什么定义了哪些函数以及如何将它们实现到编译器中并且仍然将其称为 C?

【问题讨论】:

  • 建议标记为“嵌入式”

标签: c embedded standards microcontroller


【解决方案1】:

微控制器有什么特殊的 C 标准吗?

不,有 ISO C 标准。由于许多小型设备具有需要支持的特殊架构特性,因此许多编译器都支持语言扩展。例如,因为 8051 具有位可寻址 RAM,所以可以提供 _bit 数据类型。它还有一个Harvard architecture,因此提供了关键字来指定不同的内存地址空间,单独的地址无法解析这些地址空间,因为需要不同的指令来寻址这些空间。此类扩展将在编译器文档中明确指出。此外,符合标准的编译器中的扩展名应该带有下划线前缀。但是,许多为向后兼容提供了朴素的别名,应该不推荐使用它们。

...当我在 Windows 操作系统下编写程序时,我使用的编译器并不重要。

因为 Windows API 是标准化的(由 Microsoft),并且它只在 x86 上运行,所以不需要考虑架构变化。也就是说,您可能仍会在 API 中看到 FAR 和 NEAR 宏,这是对 16 位 x86 及其分段寻址的回归,这也需要编译器扩展来处理。

...即使它的基础仍然是 C,比如循环、变量创建等等,

我不确定这意味着什么。一个典型的微控制器应用程序没有操作系统或简单的内核,您应该期望看到更多的“裸机”或“系统级”代码,因为没有广泛的操作系统 API 和设备驱动程序接口来完成大量工作引擎盖给你。所有这些库调用就是这样;它们不是语言的一部分;它是相同的C语言;只是投入不同的工作。

...有一些我从未在桌面计算机的 C 语言中见过的语法类型。

例如...?

此外,语法随着版本的变化而变化。

我对此表示怀疑。再次;比如……?

我使用的是AVR-GCC编译器,在之前的版本中,你使用了一个端口I/O的函数,现在你可以像处理新版本中的变量一样处理端口了。

这不取决于语言或编译器的变化,而更可能是简单的“预处理器魔法”。在 AVR 上,所有 I/O 都是内存映射的,因此如果您包含设备支持标头,它可能有如下声明:

#define PORTA (*((volatile char*)0x0100))

然后你可以写:

PORTA = 0xFF;

将 0xFF 写入内存映射地址 0x100 的寄存器。你可以看看头文件,看看它到底是怎么做的。

GCC 文档描述了特定于目标的变体; AVR 在第 6.36.8 节和3.17.3 中专门处理了here。如果将其与 GCC 支持的其他目标进行比较,它的扩展很少,可能是因为 AVR 架构和指令集是专门为干净高效地实现没有扩展的 C 编译器而设计的。

什么定义了哪些函数以及如何将它们实现到编译器中并且仍然被称为 C?

重要的是要认识到 C 编程语言是与其库不同的实体,库提供的函数与您自己编写的函数没有什么不同——它们不是语言的一部分——所以它可以是C 没有任何库。最终,库函数是使用相同的基本语言元素编写的。您不能期望 Win32 API 中存在的抽象级别存在于用于微控制器的库中。在大多数情况下,您可以期望至少实现 C Standard Library 的一个子集,因为它被设计为具有很少目标硬件依赖关系的系统级库。

多年来,我一直在为嵌入式和桌面系统编写 C 和 C++,并没有认识到您似乎感知到的巨大差异,因此只能假设它们是对 C 语言构成的误解的结果。以下书籍可能会有所帮助。

【讨论】:

    【解决方案2】:

    嵌入式系统很奇怪,有时会出现“标准”C 的例外情况。

    从一个系统到另一个系统,您将有不同的方式来执行诸如声明中断、定义哪些变量存在于不同内存段中、运行“内在函数”(直接映射到汇编代码的伪函数)或内联执行等操作汇编代码。

    但是控制流(for/if/while/switch/case)以及变量和函数声明的基础知识应该是相同的。

    在以前的版本中,您使用端口 I/O 的函数,现在您可以在新版本中处理类似端口的变量。

    这不是 C 语言的一部分;这是设备支持库的一部分。这是每个制造商都必须记录的内容。

    【讨论】:

    • +1 如果您编写符合 C 标准的 C 代码,它应该使用任何 C 编译器进行编译,无论是嵌入式的还是其他的。话虽如此,大多数嵌入式设备供应商都会提供扩展或库,使他们的设备使用更容易。这些是非标准化的,并且因供应商和版本而异。如果您想让您的代码在平台之间轻松移植,请尽可能使用标准 C,并尽量将所有供应商特定的功能抽象出来。
    • 嵌入式系统并不“奇怪”,通常编译器会添加extensions而不是exceptions。
    • 那么,这是否意味着您可以在微控制器上运行 C11?我在 C11 中编写了一个基本库,我正在考虑为其添加线程支持,但我担心嵌入式使用......理论上我可以将 pthreads 编译到静态库中,对吧?
    【解决方案3】:

    C 语言采用von Neumann 架构(所有代码和数据的一个地址空间),并非所有架构实际上都有,但大多数桌面/服务器类机器确实有(或至少在操作系统的帮助下存在) .为了在不编写糟糕程序的情况下解决这个问题,C 编译器(在链接器的帮助下)通常支持一些有助于有效利用多个地址空间的扩展。所有这些都可以对程序员隐藏,但它通常会减慢和膨胀程序和数据。

    至于您访问设备寄存器的方式——在不同的桌面/服务器类机器上,这也是非常不同的,但是由于编写的程序是在这些机器的常见现代操作系统(Mac OS X、Windows、BSD 或Linux)通常不直接访问硬件,这不是问题。不过,有一些操作系统代码必须处理这些问题。这通常通过定义在不同架构上以不同方式实现的宏和/或函数来完成,甚至在单个系统上具有多个版本,以便驱动程序可以在特定设备(例如以太网芯片)上工作,无论它是在 @987654322 @ 卡或 USB 加密狗(可能插入插入 PCI 插槽的 USB 卡),或直接映射到处理器的地址空间。

    此外,C 标准库对托管使用它的程序的系统(the C standard library)做出了比编译器(和适当的语言)更多的假设。当没有通用操作系统或文件系统时,这些事情就没有意义了。 fopen 在没有文件系统的系统上毫无意义,甚至 printf 也可能不容易定义。

    就AVR-GCC 及其库所做的事情而言——有很多东西涉及到如何做到这一点。 AVR 是一个Harvard architecture,具有内存映射设备控制寄存器、特殊功能寄存器和通用寄存器(内存地址 0-31),以及用于代码和常量数据的不同地址空间。这已经超出了标准 C 的假设范围。一些寄存器(通用、特殊和设备控制)可以通过特殊指令访问,例如翻转单个位和读/写一些多字节寄存器(多指令操作)隐式阻止下一条指令的中断(所以操作的后半部分可能发生)。这些是桌面 C 程序不需要知道的东西,而且由于 AVR-GCC 来自常规的GCC,它最初也没有理解所有这些东西。这意味着编译器不会总是使用最好的指令来访问控制寄存器,所以:

    *(DEVICE_REG_ADDR) |= 1; // Set BIT0 of control register REG
    

    会变成:

    temp_reg = *DEVICE_REG_ADDR;
    temp_reg |= 1;
    *DEVICE_REG_ADDR = temp_reg;
    

    因为 AVR 通常必须在其通用寄存器中包含一些东西才能对它们进行位操作,尽管对于某些内存位置来说这不是真的。 AVR-GCC 必须进行更改,以识别当在某些操作中使用的变量的地址在编译时已知并且在某个范围内时,它可以使用不同的指令来执行这些操作。在此之前,AVR-GCC 只是为您提供了一些具有内联汇编的宏(看起来像函数)来执行此操作(并使用 GCC 现在使用的单指令 inplemenations)。如果它们不再提供这些操作的宏版本,那么这可能是一个糟糕的选择,因为它破坏了旧代码,但是一旦实现了高效和原子地执行此操作的能力很好,就允许您访问这些寄存器,就好像它们是普通变量一样。

    【讨论】:

    • 我认为任何 C 实现都不应该特别关心架构是否是冯诺依曼,因为函数指针和数据指针之间的直接转换是被禁止的,而且我认为没有任何要求它们的大小相同。可以肯定的是,在冯诺依曼机器上,人们可能能够做一些完全不可移植的技巧来动态生成代码,但 C 标准中没有任何东西假设这样的事情,而且许多现代平台无论如何都会禁止它。跨度>
    • @supercat:你说得对。冯诺依曼有点太严格了。哈佛应该严格符合标准 C,除了它通常能够在这些代码部分的代码部分中存储常量数据,并且指向这些的指针不能以与常规数据指针相同的方式使用。如果str 在代码区域中,printf("%s", str); 将不起作用。当堆栈和数据位于不同的地址空间时,更明显与 C 不兼容。
    • 一些 C 实现对不同的内存空间需要不同的指针类型,但几乎所有我见过的都提供只读的“通用”指针类型(有时它比其他指针类型大一个字节) .我不确定为什么 C 要求执行堆栈与数据位于同一地址空间;我认为如果不是这样,系统可能会更安全(可以肯定,递归需要数据空间中的堆栈,但我不知道会阻止返回地址存储在不同的堆栈上)。
    • @supercat:第一段的最后一句话解决了这个问题。这可以通过让通用指针类似于struct universal_pointer { enum data_area type; union { void * __stack stk; void * __data data; void * __code code; } ; 来完成,并且要么包含所有代码以访问每种类型,并且每次访问其中一个,调用其中包含该代码的函数,或者在重复访问的情况下操作(如 memcpy 或 strlen)确定要重复调用的代码。有些系统确实使用不同的数据并返回堆栈(至少部分返回)。
    【解决方案4】:

    我从未见过没有某些控制器特定扩展的微控制器的 C 编译器。一些编译器比其他编译器更接近于满足 ANSI 标准,但对于许多微控制器而言,需要在性能和 ANSI 合规性之间进行权衡。

    在许多 8 位微控制器上,甚至一些 16 位微控制器上,访问堆栈帧上的变量很慢。尽管需要额外的代码,一些编译器总是会在运行时堆栈上分配自动变量,一些会在编译时分配自动变量(允许从不同时存在的变量重叠),还有一些允许控制行为使用命令行选项或#pragma 指令。在为此类机器编码时,我有时喜欢#define 一个名为“auto”的宏,如果它有助于更​​快地工作,它会被重新定义为“static”。

    一些编译器有多种内存存储类。您可以通过将事物声明为合适的存储类来大大提高性能。例如,基于8051 的系统可能有 96 字节的“数据”内存、224 字节的“idata”内存(与前 96 字节重叠)和 4K 的“xdata”内存。

    • “数据”内存中的变量可以直接访问。

    • “idata”内存中的变量只能通过将其地址加载到单字节指针寄存器中来访问。在无论如何都需要访问它们的情况下没有额外的开销,因此 idata 内存非常适合数组。如果数组q 存储在idata 内存中,对q[i] 的引用将与在数据内存中一样快,尽管对q[0] 的引用会更慢(在数据内存中,编译器可以预先计算地址并在没有指针寄存器的情况下访问它;在 idata 内存中这是不可能的)。

    • xdata 内存中的变量的访问速度远慢于其他类型的变量,但可用的 xdata 内存要多得多。

    如果一个人告诉 8051 编译器默认将所有内容都放在“数据”中,那么如果一个人的变量总计超过 96 个字节并且一个人没有指示编译器将任何内容放在其他地方,那么一个人将“内存不足”。如果默认情况下将所有内容都放在“xdata”中,则可以使用更多内存而不会达到限制,但一切都会运行得更慢。最好的办法是将直接访问的常用变量放在“data”中,将间接访问的常用变量和数组放在“idata”中,将不常用的变量和数组放在“xdata”中。

    【讨论】:

      【解决方案5】:

      绝大多数标准 C 语言在微控制器中都很常见。中断的约定往往略有不同,但并非总是如此。

      将端口视为变量是因为寄存器映射到大多数微控制器上的内存中的位置,因此通过写入适当的内存位置(定义为在内存中具有预设位置的变量),您可以设置该端口上的值。

      【讨论】:

      • 中断处理没有“约定略有不同”的情况是什么?
      • 在某些系统上,“真正的”中断处理程序是由链接器和运行时库生成的。该中断处理程序将保存所有寄存器,然后调用一个指定的例程,其调用约定与任何其他例程相同。
      【解决方案6】:

      正如之前的贡献者所说,没有这样的标准,主要是由于架构不同。

      话虽如此,Dynamic C(由Rabbit Semiconductor 出售)被描述为“带有实时扩展的C”。据我所知,编译器仅针对 Rabbit 处理器,但还有一些有用的附加关键字(例如,costate、cofunc 和 waitfor)、一些真正的特性(例如,#use mylib.lib 而不是 #include mylib.h - 并且没有链接器),以及 ANSI C 中的一些遗漏(例如,没有文件范围的静态变量)。

      但它仍然被描述为“C”。

      【讨论】:

        【解决方案7】:

        Wiring 具有基于 C 语言的语法。也许您可能想看看是什么让它如此。

        【讨论】:

          猜你喜欢
          • 2010-11-08
          • 2011-04-18
          • 1970-01-01
          • 2014-08-24
          • 2016-11-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多