garfield

【原创】c语言还是汇编?

0
阅读(3124)

 

    c语言还是汇编?许多电子爱好者都问过这个问题。今天在这里谈一下我的浅见,欢迎批评指正。

    使用汇编语言还是C语言进行编程,到底哪种语言更好呢?这个问题长久以来一直争论不休,我很惊讶人们仍然还会时常提起它。每个人都有自己的观点,所以下面我要讲出我的观点。采用C语言的主要好处在于它的灵活性。而采用C语言的最大问题仍然在于它的灵活性。

     在中国的大学里,所有理工科学生会在大学第一年学习c语言程序设计,而电子信息类的学生会在大二或者大三时学习微型计算机原理或者嵌入式技术等课程,在那里他们将会接触到汇编语言,因为c语言独立于处理器之外,c语言的编程跟处理器类型,开发环境没有什么关系。而汇编语言是与处理器或者说处理器架构息息相关的,比如说大家熟悉的arm架构,x86架构,mips架构或者power架构等等,而每种架构随其发展而不断扩充或者更改其指令集。汇编语言就是跟处理器架构息息相关,每一条汇编指令都会指挥,这里就称指挥吧,处理器完成一种操作,汇编语言就是这样一种低级的编程语言,使用汇编语言,程序员就可以对自己所做的一切操作一目了然。

    C语言的最大特点之一便是它不与任何特定的硬件或系统联系在一起。这使它可以更加轻松地编写程序,实际上无需大幅更改代码就可以在任意机器上运行。当你使用微控制器(MCU)时,你会花费大量的时间来编写软件。个人认为MCU项目开发过程中至少有80%的时间会涉及到创建、测试和调试软件的任务。我们很少有人情愿将时间浪费在重新编写相同的软件这件事情上,仅仅为了让它可以运行在不同的MCU之上。将获得公认、可靠的算法迁移到另一个MCU,这种做法通常被称为“代码移植”,它比你所认为发生或者预测的次数更加频繁。这通常是你的项目需要不断发生演变的结果,同时尝试着重新使用软件,这些软件你已经了解了其运行方式。有时保留使用特定的MCU还有许多可靠的理由。但对代码迁移带来的不便之处来说,这并不是一个充足的理由——由于编写和测试软件的成本,这实际上涉及到了编写寿命这个事实。

    C语言通常被人们视为一种中层计算机语言,它将其他高层语言的抽象元素与汇编语言的功能结合在一起。C语言是一种相对紧凑、结构良好的语言,它有非常松散的数据类型定制,支持低层比特级数据操作。在编写需要操作MCU集成外设单独控制位的软件时,上述提到的最后一个特性绝对极为关键。

    从大学开始接触mcu,从八位到十六位再到三十二位,我感到十分习惯,有些技巧采用C语言编译器时可能会产生一定的损害。执行驻留在堆栈中的代码,或者将各个位在CPU的进位标记(Carry Flag)移进移出,这只是采用汇编语言比采用C语言更加容易的两个实例。

    汇编语言可以为你提供大量的自由度和控制性,这可以带来许多的乐趣。但采用汇编语言创建的程序很难轻松移植到不太类似的MCU平台之中——换句话说,即不易从8位MCU移植到32位MCU中。采用C语言编程不会让这一点非常明显,它只会让这个过程更加轻松。

与汇编语言编程相对比,采用C语言编写的代码可以:

  • 更加可靠
  • 更具扩展性
  • 在不同的平台之间更具可移植性
  • 更加便于维护
  • 更具生产效率

 

    C语言编译器可以和出色的汇编语言程序员一样高效。采用C语言创建软件可以使采用汇编语言编写的等效代码达到同样高效的内存利用率,这绝对可以实现。你只是需要了解你的代码通知编译器所要完成的任务。我定期使用编译器的分解工具,用肉眼检查它的汇编语言输出结果。这可以教我领会采用C语言编写高效软件的方式。我甚至已经学会了几招非常有趣的汇编语言编程技巧。它教我学会了如何像编译器一样思考。当你掌握了这种思考习惯的时候,你的C语言源代码从起点就会变得高效,而不是事后才会想到明智的做法。

    C编程语言保留了程序员编程过程中熟悉的基本理念。C语言只需要他们明确表述出其意图即可。无论采用的是何种编程语言,你的代码都应该做到清晰、简洁、正确和带有注释。

    似乎普遍存在这样一种误解,人们认为C语言可以自动添加注释。事实上并不是这样。我听到过许多不注释代码的观点——这样做并不是好的做法。不管是喜欢还是不喜欢,编写软件都需要规则的约束。采用C语言编写的功能和变量名称应该加以充分描述,以便能够合理表明它们的用途。例如,功能名称“call_me_a_taxi ()” 没有传达出任何有关目的的信息,而“toggle_GPIO_for_heater_elements()”却达到了这个目标。对于大部分情况,注释并不是用于解释代码所要完成的事情。注释应该用来解释代码为什么正在完成的任务。这可能需要花费额外的时间创建准确的注释,但从长远来看它会节省你的时间。注释可以反映出你在创建代码时的思考过程。它们可以帮助你防止在半年之后重新检查你的代码、需要更新特性或进行错误修正时,一直在自问“我当时在想什么呢?”这种情况的出现。

停止使用int!

    当我们第一次学习使用C语言编程时,我们的代码通常都在计算机中执行,在不具备实时概念的操作系统(OS)控制下实际上拥有无限的内存和硬盘空间。唯一重要的一件事情便是让软件运行起来——并且将它及时提交给教授给你打分。当你从台式机迁移到嵌入式MCU编程时,发生了显著的变化。

    首先,在MCU中一切都是有限的。你程序的内存数量是固定的,你一定要了解内存的数量。需求或你的应用将会确定完成这个任务所需的内部外设和I/O引脚的数量。计算性能由系统时钟脉冲频率和中央处理器(CPU)的大小——即8位、16位或32位——确定。你需要十分留意实时情况以及它的实施方法。在今后的博客文章中,我将会详细解释中断如何用来将你的软件与真实世界保持同步。

    C编程语言具备几种特有的数据类型,基础类型包括char、short、int、long、float和double。除了char以外,这些数据类型都默认为带符号。ANSI C99标准也支持布尔数据类型,通常它的名称为bool。标志为bool的数据是一种拥有两个值的类型——1为“真”,0为“假”。有趣的是,ANSI C定义每一种数据类型都有一个最小值,但没有指定它们的最大值。具体来说,ANSI C的大小可以表示如下:

sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long)

    因此,32位MCU的C编译器可以为bool类型的变量在RAM中存储32位内存位置。这个原因是由于C语言尝试支持“固有的”寄存器和处理器的存储容量。这就是说,我希望我们能够变量(或者为“真”或者为“假”)使用四个字节方面达成一致意见,这极为浪费RAM的空间。

    对于大多数8位和16位MCU而言,它们各自的C编译器将int视为16位数据类型。32位编译器通常会将int数据视为32位规格。当RAM属于有限资源,我们在创建MCU的软件时,这种含糊性不可接受。除此之外,如果嵌入式软件应用需要采用大量数学算法时,不同规格的int在算法上也会产生精度误差。当我将程序从8位MCU移植到32位平台时,我预计会看到内存利用率发生一些变化。但我不希望看到RAM使用率出现巨大的改变。

观察下列代码占位程序示例:

int n;
for (n = 0; n != 100, n++)
{

}

我们通过MCU软件处理的大部分数据往往都不带符号。长久以来形成了下列这些习惯,当我们需要指定不带符号数据的大小时,我们会采用下列备用的数据类型定义:

typedef unsigned char uint8_t;

typedef unsigned short int uint16_t;

typedef unsigned long   int uint32_t;

有些编译器可能会小幅修改这些类型定义,但最终结果都是相同的。数据类型标为uint8_t属于占用一字节内存的无符号整数。

这是我对上述软件循环修改的高效版本:

uint8_t n;
for (n = 100; n != 0, n--)
{

}

    注意,我们都知道n占用一个字节,无论目标MCU是哪一类,它都是无符号类型。我还改变了控制参数,从100开始倒数,而不是原来由0开始的顺序。我这样做原因在于,我的版本测试条件可以检查n的值何时等于零。如同任何优秀的汇编语言程序员了解的一样,所有CPU都可以测试零值。这就是之所以设置一个零标记(Zero Flag)的原因。没有标准的CPU可以测试100,因此值的测试需要多个指令,以便执行对照运算。

    飞思卡8位S08系列MCU采用汇编语言指令,特别是为了让这些通用软件循环变得更加高效——DBNZ。DBNZ操作码可以执行“如果非零便缩减和分支”(“Decrement and Branch if Not Zero”)的操作。DBNZ将会缩减为无符号的8位变量,如果为非零值则会执行比例分支,这恰恰是我修改过的软件循环所需要的功能。通常情况下,变量属于本地变量,这意味着它位于堆栈之中。DBNZ具有寻址模式,该模式可以支持变量相对访问到堆栈指针引用的位置。在单一、非中断指令中,DBNZ执行我的软件循环“n != 0, n--”部分。现在效率终于得到了提高!

     显而易见,这是一个非常简单的例子。但如果采用效率低下的方法,便会养成不断倍增的坏习惯,逐渐产生更大的问题。理解C语言源代码对汇编语言的影响,将有助于提高效率。

理解RAM

知道变量位于RAM 之中,这一点和使用正确(即尽可能最小)的变量规格一样重要。在C编程语言中,变量分为“存储”(“storage”)和“作用”(“scope”)两类,它们称为“全局”或者“本地”。“作用”这个术语表明变量对其他程序的可见方式。变量基本上分为四种不同的类型(可称“存储分类”):

  • extern – 是指带有全局存储和全局作用的变量
  • static – 是指带有全局存储和本地作用的变量
  • auto – 是指带有本地存储和本地作用的变量
  • register – 是指采用CPU寄存器存储的本地变量

    对于全局存储而言,每一个变量在RAM中都有永久的绝对地址位置。对于本地存储而言,每一个变量都驻留在堆栈之中,通过堆栈指针(Stack Pointer)的偏移被引用。当你将变量归类为“register”时,你正在请求编译器使用CPU内部寄存器之一,以便存储变量。但是,如果编译器发现这无法处理,变量则会被归为“auto”。

    堆栈是临时存储变量的场所。通过操作堆栈指针,当它启动并且释放直到停止,每一个功能都可以创建这一存储。因此,本地变量都属于临时性质,只要功能用到它们时才会激活,仅会占用内存。

    被归类为“extern”的变量通常称为全局变量。全局变量具有全局作用,可由所有软件模块访问。被归类为“auto”的变量通常称为本地变量。本地变量具有本地作用,它被定义为弯括号(即 { … })中补充集范围内的一切元素。它们的本地存储可以阻止其他功能,它们定义的弯括号范围以外元素将会被阻止对其的访问。弯括号集合范围内定义的变量和没有分类识别的变量将会默认为“auto”,这就可以说明在软件列表中我们很少看到“auto”术语的原因。

    静态变量似乎并未获得众多程序员的充分利用,因为他们似乎并未充分理解使用它们的方法。静态变量拥有全局存储,这意味着它们在RAM中的地址位置是永久的。编译器控制它们的本地作用,让它们只能在同类型范围内的功能或软件模块进行访问。换句话说:

  • 在同类型功能范围内的静态变量只能由该功能直接访问。再次说明,本地作用由弯括号的补充集定义。
  • 在同类型模块范围内的静态变量可由该模块范围内的所有功能直接访问。

注意我采用的是“直接访问”这个表述。我曾多次听说全局变量的使用通常都不可避免。这在有些情况下的确如此,但是我发现程序员通常都会忽视这一项强大的功能,以便操作并管理C语言的变量指针。如果变量指针可以依靠内部功能之一通过指针,同类型模块范围内的静态变量可以通过外部功能进行访问。明智地使用静态变量,将有助于防止各种功能损坏其他数据,提高整体代码的可靠性。

最后说一句,欢迎拍砖