跳转到内容

嵌入式系统/C 编程

来自维基教科书,开放世界中的开放书籍

C 编程语言可能是嵌入式系统编程中最流行的编程语言。(在之前嵌入式系统/嵌入式系统简介#本书将使用哪些编程语言?中我们提到了其他流行的编程语言)。

大多数 C 程序员都被宠坏了,因为他们是在有标准库实现的环境中编程,而且经常可以使用许多其他库。事实是,在嵌入式系统中,很少有程序员习惯使用的库,但有时嵌入式系统可能没有完整的标准库,如果有的話。很少有嵌入式系统具有动态链接功能,因此,如果要使用标准库函数,它们通常需要直接链接到可执行文件中。由于空间限制,通常不可能链接整个库文件,并且程序员经常被迫“自己编写”标准 C 库实现,如果他们想要使用它们。虽然有些库很笨重,不适合在微控制器上使用,但许多开发系统仍然包含 C 程序员最常用的标准库。

由于代码效率、降低的开销和开发时间,C 仍然是微控制器开发人员非常流行的语言。C 提供低级控制,被认为比汇编更容易阅读。许多免费的 C 编译器可用于各种开发平台。编译器是 IDE 的一部分,具有 ICD 支持、断点、单步执行和汇编窗口。近年来,C 编译器的性能已大大提高,据说它们与汇编一样好,具体取决于你问的是谁。大多数工具现在提供了用于自定义编译器优化的选项。此外,使用 C 可以提高可移植性,因为 C 代码可以为不同类型的处理器编译。

以下是一个使用 C 更改位的示例

清除位

 PORTH &=  0xF5;  // Changes bits 1 and 3 to zeros using C
 PORTH &= ~0x0A; // Same as above but using inverting the bit mask - easier to see which bits are cleared

设置位

 PORTH |= 0x0A;  // Set bits 1 and 3 to one using the OR  

在汇编中,这将是

清除位

 BCLR PORTH,$0A ;//Changes bits 1 and 3 to zeros using 68HC12 ASM

设置位

 BSET PORTH,$0A ;//Changes bits 1 and 3 to ones using 68HC12 ASM

特殊功能

[编辑 | 编辑源代码]

C 语言是标准化的,有一些每个人都知道和喜欢的运算符。然而,许多微处理器具有 C 编译器可能不会使用的功能。C 编译器可能产生的机器代码效率低于手工编写的汇编语言。例如,8051 和 PIC 微控制器都有用于直接设置和检查字节中单个位的汇编指令。C 程序可以编写为使用“位域”单独影响位,但编译器产生的机器代码输出可能不如某些微处理器上一次一位的机器操作快。

位域是一个很少有 C 程序员有经验的主题,虽然它在很长一段时间内一直是语言的标准化部分。位域允许程序员访问未对齐的部分的内存,甚至访问小于字节的部分。让我们创建一个示例

struct _bitfield {
   flagA : 1;
   flagB : 1;
   nybbA : 4;
   byteA : 8;
}

冒号将字段的名称与其大小以位为单位,而不是以字节为单位隔开。突然之间,了解什么数字可以放入什么长度的字段中变得非常重要。例如,flagA 和 flagB 字段都是 1 位,因此它们只能保存布尔值(1 或 0)。nybbA 字段可以保存 4 位,最大值为 15(一个十六进制数字)。

位域中的字段可以像普通结构一样被精确地寻址。例如,以下语句都是有效的

struct _bitfield field;
field.flagA = 1;
field.flagB = 0;
field.nybbA = 0x0A;
field.byteA = 255;

位域中的单个字段不使用存储类型,因为您手动定义每个字段占用的位数。另请参阅"在结构中声明和使用位域""允许的位域类型"

但是,位域中的字段可以用关键字“signed”或“unsigned”限定,即使未指定,也会隐含“signed”。

如果一个 1 位字段被标记为 signed,它有 +1 和 0 的值。请允许我引用c2:BitField:一个可以包含 1 的带符号 1 位位域是编译器中的错误。

重要的是要注意,不同的编译器可能会在位域中以不同的顺序排列字段,因此程序员永远不应该尝试将位域作为整型对象访问。如果不在您的个人编译器上进行试错测试,则不可能知道位域中的字段将以什么顺序排列。

此外,位域像任何其他数据对象一样,在给定机器上对齐到某个边界。


C 语言支持设置一个结构,该结构完全匹配内存映射 I/O 设备的字节和位级布局。[1]

变量声明中的“const”是编写它的程序员对程序不会更改变量值的承诺。

在嵌入式系统中使用“const”有两种略微不同的原因。

其中一个原因与桌面应用程序相同

通常,结构、数组或字符串使用指针传递给函数。当该参数被描述为“const”时,例如当头文件说

   void print_string( char const * the_string );

时,这是编写该函数的程序员对该函数不会修改结构、数组或字符串中任何项目的承诺。(如果该头文件在实现该函数的文件中被正确地 #include,那么当该实现被编译时,编译器将在编译时检查该承诺,如果该承诺被违反,则会给出错误)。

在桌面应用程序中,如果从源代码中删除所有“const”声明,这样的程序将编译成完全相同的可执行文件——但编译器将不会检查这些承诺。

当其他程序员有一个重要的数据想要传递给该函数时,他可以简单地通过阅读头文件来确保该函数不会修改这些数据。(如果沒有“const”,他要么必须查看函数实现的源代码以确保他的数据没有被修改(并担心下次更新该实现可能会修改该数据),要么创建一个数据的临时副本传递给该函数,保持原始版本不变)。

将数据存储在 ROM 中

[编辑 | 编辑源代码]

使用“const”的另一个原因是特定于嵌入式系统的

在许多嵌入式系统中,程序 Flash(或 ROM)比 RAM 多得多。使用如下定义的“.c”文件

   char * months[] = {
       "January", "February", "March",
       "April", "May", "June",
       "July", "August", "September",
       "October", "November", "December",
   };

强制编译器将所有这些字符串存储在程序 Flash 中,然后在启动时将这些值复制到 RAM 中的一个位置。如果程序实际上从未修改过这些字符串(如经常发生的那样),这会浪费宝贵的 RAM。通过将声明修改为

   char const * const months[] = { ... };

,我们告诉编译器,我们承诺永远不会修改这些字符串(或它们在数组中的顺序),因此编译器可以自由地将所有这些字符串存储在程序 Flash 中,并在需要时从 Flash 中获取原始值。这为确实会发生变化的变量节省了 RAM。

(如果您使用如下定义

   static char * months[] = { ... };

,一些编译器足够聪明,可以自己判断程序是否实际修改了这些字符串。如果程序确实修改了这些字符串,那么编译器当然必须将它们放在 RAM 中。但如果不是,编译器可以自由地在程序 Flash 中只存储这些字符串一次)。

在普林斯顿架构微控制器上将数据存储在 ROM 中

[编辑 | 编辑源代码]

普林斯顿架构微控制器使用完全相同的指令来访问 RAM 和程序 Flash。

此类架构的 C 编译器通常将所有声明为“const”的数据放入程序 Flash 中。函数既不知道也不关心它们是在处理来自 RAM 的数据还是程序 Flash 中的数据;相同的“读取”指令无论函数接收的是指向 RAM 的指针还是指向程序 Flash 的指针,都能正确地工作。

在哈佛架构微控制器上将数据存储到 ROM

[编辑 | 编辑源代码]

不幸的是,哈佛架构微控制器使用完全不同的指令来访问 RAM 和程序闪存(通常它们还有另一套指令来访问 EEPROM,以及另一套来访问外部存储芯片)。这使得编写一个可以从程序的某个部分调用以从 ROM 中打印出常量字符串(例如“November”)的子例程(例如 puts()),并且可以从程序的另一个部分调用以打印出 RAM 中的变量字符串,变得很困难。

不幸的是,不同的 C 编译器(即使针对同一芯片)也需要不同的、不兼容的技术,才能让 C 程序员告诉 C 编译器将数据放入 ROM 中。C 程序员至少有 3 种方法来告诉 C 编译器将数据放入 ROM 中。

(1) 有些人声称,使用“const”修饰符来表示某些数据应该存储在 ROM 中是滥用符号。 [2] 这些人通常建议使用一些非标准的属性或存储说明符,例如“PROGMEM”或“rom”[3],在变量定义和函数参数上,以表示“类型指针”类型为“值驻留在程序闪存中,而不是 RAM 中”。不幸的是,不同的编译器有不同的、不兼容的方法来指定数据可以放置在 ROM 中。通常这些人使用具有 2 个版本的函数库,每个函数都处理字符串(等等);一个版本用于 RAM 中的字符串,另一个版本用于 ROM 中的字符串。这种技术使用最少的 RAM,但通常需要比其他技术更多的 ROM。

(2) 有些函数库假设数据在 RAM 中。当程序员希望使用实际上在 ROM 中的数据调用这些函数时,程序员必须确保数据首先临时复制到 RAM 中的缓冲区,然后使用该缓冲区的地址调用该函数。这种技术使用最少的 ROM 来保存库,但在每次涉及 ROM 中数据的函数调用时,它使用比其他技术更多的 ROM 和 RAM。

(3) 有些函数库使用可以处理从一个位置用 RAM 中的字符串调用,从其他位置用 ROM 中的字符串调用的函数。这通常需要“胖指针”又名“通用指针”,这些指针具有额外的位,用于指示指针是指向 RAM 中的内容还是 ROM 中的内容。每次这样的库使用指针时,执行代码都会检查这些位,以查看是执行“从 RAM 读取”指令还是“从 ROM 读取”指令。 [4][5][6][7][8][9] 这是在其他系统中使用的“胖指针”和“标记指针”的特例,这些系统根据指向对象的类型执行不同的代码,其中“指针”包括类型信息和目标地址。 [10][11]

易变的

[编辑 | 编辑源代码]

在变量声明中,“易变的”告诉我们和编译器,该变量的值可能在任何时间改变,通过某种方式,在代码的这一部分的正常流程之外。这些变化可能是由硬件引起的,例如外设、多处理器系统中的另一个处理器或中断服务例程。

“易变的”关键字告诉编译器不要进行某些优化,这些优化只适用于存储在 RAM 或 ROM 中的“正常”变量,这些变量完全受此 C 程序的控制。

嵌入式编程的全部意义在于它与外部世界的通信——而输入和输出设备都需要“易变的”关键字。

至少有 3 种类型的优化,“易变的”会关闭它们

  • “读取”优化——如果没有“易变的”,C 编译器假设一旦程序将变量读入寄存器,它就不需要在每次源代码中提到该变量时重新读取该变量,而是可以使用寄存器中缓存的值。这对于 ROM 和 RAM 中的正常值非常有效,但对于输入外设来说却完全失败。外部世界以及内部计时器和计数器经常发生变化,使缓存的值变得陈旧和无关。
  • “写入”优化——如果没有“易变的”,C 编译器假设对不同变量进行写入的顺序并不重要,只有对特定变量的最后一次写入才是真正重要的。这对于 RAM 中的正常值非常有效,但对于典型输出外设来说却完全失败。通过串行端口发送“左转 90 度,前进 10 步,左转 90 度,前进 10 步”与“优化”为通过串行端口发送“0”完全不同。
  • 指令重新排序——如果没有“易变的”,C 编译器假设可以重新排序指令。编译器可能会决定更改变量分配的顺序,以更好地利用寄存器。这对于 I/O 外设来说可能会完全失败,例如,你写入一个位置来获取样本,然后从另一个位置读取该样本。重新排序这些指令将意味着读取旧的/陈旧的/未定义的样本,然后告诉外设获取新的样本(这将被忽略)。

根据你的硬件和编译器功能,其他优化(SIMD、循环展开、并行化、流水线)也可能会受到影响。

常量易变的

[编辑 | 编辑源代码]

很多人不理解“const”和“volatile”的组合。正如我们在之前的 嵌入式系统/内存 中讨论的那样,嵌入式系统有许多种内存。

许多输入外设——例如自由运行的计时器和键盘接口——必须声明为“const volatile”,因为它们既 (a) 通过此 C 程序之外的方式改变值,又 (b) 此 C 程序不应该向它们写入值(向 10 键键盘写入值没有任何意义)。

编译和交互式

[编辑 | 编辑源代码]

在绝大多数情况下,当人们用 C 语言编写代码时,他们会将该代码通过 C 编译器运行到某些个人计算机上,以获得本地可执行文件。使用嵌入式系统的人员然后将该本地可执行文件下载到嵌入式系统中,并运行它。

但是,少数使用嵌入式系统的人员做了一些不同的事情。

  • 有些人使用 C 解释器,例如 [5] 或交互式 C 或 可扩展交互式 C (EiC)。他们将 C 源代码下载到嵌入式系统中,然后在嵌入式系统本身运行解释器。(更多 C 解释器列在另一本维基教科书中,C 编程/C 编译器参考列表)。
  • 有些人有幸使用“大型”嵌入式系统,这些系统可以运行标准 C 编译器(它在 Linux 或 BSD 上运行标准 GCC;或者它在 FreeDos 上运行 DJGPP 移植的 GCC;或者它在 Windows 上运行 MinGW 移植的 GCC;或者它在 Linux 或 Windows 上运行 Tiny C 编译器;或者其他一些 C 编译器)。他们将 C 源代码下载到嵌入式系统中,然后在嵌入式系统本身运行编译器。

用于嵌入式系统的 C 编译器

[编辑 | 编辑源代码]

也许用于嵌入式系统的 C 编译器和用于台式计算机的 C 编译器之间最大的区别是“平台”和“目标”之间的区别。“平台”是 C 编译器运行的地方——也许是运行 Linux 的笔记本电脑或运行 Windows 的台式机。“目标”是 C 编译器生成的执行代码将运行的地方——嵌入式系统中的 CPU,通常没有任何底层操作系统。


GCC 编译器是[需要引用] 用于嵌入式系统的最流行的 C 编译器。GCC 最初是为 32 位普林斯顿架构 CPU 开发的。因此,它相对容易移植到目标 ARM 核心微控制器,例如 XScale 和 Atmel AT91RM9200;Atmel AVR32 AP7 系列;MIPS 核心微控制器,例如 Microchip PIC32;以及 Freescale 68k/ColdFire 处理器。

编写编译器的人员还(更困难地)将 GCC 移植到目标德州仪器 MSP430 16 位 MCU;Microchip PIC24 和 dsPIC 16 位微控制器;8 位 Atmel AVR 微控制器;8 位 Freescale 68HC11 微控制器。

其他微控制器与 32 位普林斯顿架构 CPU 有很大不同。许多编译器编写者认为,开发一个独立的 C 编译器会更好,而不是试图将 GCC 的圆柱形塞入 8 位哈佛架构微控制器目标的方形孔中。

SDCC——适用于英特尔 8051、Maxim 80DS390、Zilog Z80、摩托罗拉 68HC08、Microchip PIC16、Microchip PIC18 的小型设备 C 编译器 http://sdcc.sourceforge.net/

有一些备受尊敬的公司出售商业 C 编译器。你可以找到适用于几乎所有微控制器的商业 C 编译器,包括上面列出的微控制器。尚未列出的流行微控制器(即,唯一已知的 C 编译器是商业 C 编译器的微控制器)包括 Cypress M8C MCU;Microchip PIC10 和 Microchip PIC12 MCU;等等。

进一步阅读

[编辑 | 编辑源代码]

参考文献

[编辑 | 编辑源代码]
  1. 埃里克·S·雷蒙德。 "C 结构体打包的失落艺术".
  2. "程序空间中的数据: 关于 const 的说明"
  3. "面向 PICmicro 的 BoostC C 编译器参考手册"
  4. "Crossware C 编译器手册: 8051 特定功能: 通用指针" [1]
  5. 奥拉夫·菲弗。 "在 8051 C 编译器中使用指针、数组、结构体和联合体: 通用指针" [2]
  6. 以撒·马里诺·巴瓦雷斯科。 "面向 MPLAB-C18 编译器的通用指针"。 [3] [4]
  7. "SDCC 编译器用户指南"。第 "3.5.1.8 指向 MCS51/DS390 特定内存空间的指针" 节。第 "4.6.16 通用指针" 节。
  8. 约翰·哈特曼。 "Intel 8051: 3 字节通用指针".
  9. "Cx51 用户指南: 通用指针".
  10. 马克·S·米勒。 "胖指针".
  11. "真正简单的内存管理: 胖指针" 描述了一种简单、与 RTOS 实现 兼容的垃圾收集和内存碎片整理方案 - 它永远不会 "停止世界"。
华夏公益教科书