|
|||||||
![]() |
![]() |
||||||
|
|||||||
Forth 仍然适用于嵌入式应用尽管有时只是把 Forth 当成一个有趣历史遗留物,但 Forth 却仍然在今天的嵌入式系统中扮演了重要的角色。 原文 Tom Napier : Forth Still Suits Embedded Applications 编译 赵 宇 张文翠 嵌入式系统编程从来就没有一个单一的解决方案,项目的规模使得各种方案有着极大的差异。实时信号发生器需要对每个指令时钟记数,但软件只有几百行代码,对于这样的应用,汇编语言就是唯一的选择;另一些应用则需要可扩展的用户界面和成千上万行的代码,最经济的解决方案就是使用现成的单卡 PC 、运行一个操作系统并配合使用 C 语言编程。 对于需要几千行代码的中等规模应用,你就会发现你使用了昂贵的处理器和过多的程序存储器。这并不是因为你的程序需要这样,而是因为你的程序所运行的嵌入式操作系统和高级语言需要这样。在这种情况下,选择 Forth 可以使工程师快速地生成紧缩、可靠的代码,运行在一个廉价的处理器上,并可以在以后的应用中重用这些成果。 Forth 并不是一个新的程序设计语言。它的商业应用历史已经有 30 年了, ANSI 标准编号为 X3.215/1994 。但是它的应用不广,在美国大约仍有 100 多个全职的 Forth 程序员。而相反, C 是一个首选的语言。 程序员为什么学习和使用 C 语言呢?简单地说就是:别的人也在使用 C 语言。项目经理指定他所管理的项目用 C 语言编写,因为 C 程序员容易找到。不过,你如果能够远离程序员而去面对一下硬件工程师,你就会发现他们中的许多人不但熟悉而且热衷于使用 Forth 。在嵌入式系统中,喜爱 Forth 的理由是:通过 Forth 语言,懂硬件的工程师比能够编写编译程序的计算机专业毕业生工作得更有效。 Forth 是一个功能强大的语言,但它的语法比 BASIC 还要简单,比起 C 语言那就更是简单多了。虽然简单,程序员却可以根据自己的需要来扩展这个语言。你从来就不必局限于编译程序开发者想象的几种情况中。当使用汇编语言的时候,你必须关注硬件,除此之外, Forth 是一个特别容易学习和使用的语言。 如果 Forth 仅仅是一种计算机语言,那它和其它的语言也不会有什么大的区别。但是 Forth 和其它语言的概念完全不同的。 Forth 的核心具有解释器和编译器的双重功能。于是, Forth 程序运行得和编译过的程序一样快,而你却可以用交互式方式来编写和调试它们。 Forth 程序的任何一个子程序都可以在没有符号调试器的情况下通过键盘进行调试,这种紧密结合的编码 / 调试循环带给程序员的是极高的生产率。 Forth 通过省略分析器而奇迹般地实现了这些功能。源程序的每个字表示一个单一的操作(小括号表示注释,不表示优先级操作, Forth 中很少使用优先级操作)。每个字都知道从哪里得到操作数、如何处理它们、把结果放到什么地方。如果你在键盘上打入一个字,这个字就会被立即执行,于是你可以随意地指定参数来测试一个字,以确保它在任何情况下都能正常工作。 使用 Forth 编写程序,就是创建新的定义、扩展语言直到它可以处理你的任务。每个定义都是由其它的字来定义的。有些字可以来自于预定义的库、来自于 Forth 本身、或者你在以前的程序中字义的字。 Forth 不加区别地看待库和用户定义字,而最高级的字就是应用程序本身。这样, Forth 程序是自顶向下定义的,但却是自底向上编写和测试的,我们可以把 Forth 看成是一种可执行的程序设计语言。 你可以使用 Forth 做硬件能够胜任的任何事情,结果就是你编写了自己的应用语言。驱动机器人手的程序需要转动手臂的词汇,测试程序需要把字符写到显示器上的词汇。每个新的 Forth 字是一个自然的可再用模块,在你的下一个项目中,你可以重用这些字以稳定地提高你的生产率。 当你要把应用移到一个新的处理器体系结构时,重用字也是非常方便的。大约 80 个左右的 Forth 预定义字是用主处理器的机器代码写成的,其余的部分都是用 Forth 写的。这样语言和全部的定义字可以很容易地移植到一个新的微处理器上。只有 80 个机器字 – 其代码长度小于 2000 个字节 -- 需要重新编写。 这种程序设计生产率的提高也适应于整个项目。通过适当的划分,编码工作可以分配给大的程序员团队。一但低层的字被编写和测试通过,高层的字就几乎可以保证工作的正确性,代码的集成和维护也很简单。字的命名可以足够长以便于描述它们的功能,而好的 Forth 程序几乎是自解释的,几乎不需要在源程序文本之外的说明文档。由于定义通常很短,我们可以更容易追踪 Forth 的程序流。 Forth 程序具有高度的结构化,所有常用的操作如 IF 、 ,ELSE 、 WHILE 和 LOOP 也是 Forth 的字,但是它们的用法比其它语言更加明了。例如,比较和动作是两个不同的字。比较字产生一个标志,接着判断词使用这个标志。没有 GOTO ,除非你自己一定要定义一个。 当然, Forth 字需要知道这些标志和其它参数的位置。像大多数编译语言一样, Forth 在堆栈上传递参数。一个参数可以是你需要的任何东西:一个数值,一个布尔值,一个变量的地址,一个数据结构的指针,代码中的下一个字知道如何正确地解释它。 所有的算术和逻辑操作在参数栈上的数值之间完成。由于 Forth 通过调用连续的字来完成工作,它需要返回栈跟综这些字的地址,返回栈也存储循环变量和循环的终止值。 与其它语言不同的是, Forth 并没有把堆栈隐藏起来。程序员对堆栈有完全的访问能力,当然也应该明白堆栈上的值的意义。由于 Forth 把中间结果保存在堆栈上,它很少像大多数语言那样使用许多命名变量。编程的时候,程序员只需要关注堆栈而不必再关心一个命名的变量是否被覆盖。更好的消息是:一个程序员很少需要一次同时关注多于 3到4 个堆栈元素。 这种访问堆栈的方式是 Forth 语言一个强大的、也是非常危险的特性。由于你可以处理返回栈,所以你可以去掉一个返回地址以退出当前字的执行;你也可以把一个临时变量推进返回栈以便于稍后处理。然而,如果忘记弹出这个值,那结果肯定是系统崩溃。 通过使用堆栈编程方法, Forth 可以充分利用今天 32 位处理器的能力。早期的 Forth 标准把参数栈和返回栈都规定为 16 位宽,这适合第一代处理器的寻址范围,允许使用相同的子程序处理整数和地址( Forth 允许在地址上进行算术操作,对于可能仅使用一次的“数组”,不是定义一个数据结构,通常的办法是在变量的地址上增加一个偏移量以读取一个数组成员或者一个串的某个字符)。 早期的 Forth 甚至还有一些双精度( 32 位)操作符。每个双精度数占两个堆栈深度。现在除了最小的微处理器, Forth 都使用 32 位的单精度数。 数可以定义成整数或者浮点数,除此之外, Forth 很少或者没有数据类型。它假设程序员不是婴儿,为程序员提供灵活性比提供一些模糊的错误信息更重要。如果你有一个很好的理由指定一个数值为布尔值,编译器也不可能明白你的意思。这样,你就可以按你的希望匹配和混合数值运算,只有你自己关心它们。当然,你必须对你代码写上注释,否则就会激怒将来维护这些代码的程序员。 这种灵活性可以很容易地定义新的数据类型。我自己就有规律地使用 Forth 提供的 32 浮点数。当编写滤波分析程序时,我把一对这样的数定义成复数,然后编写自己的复数算法子程序库,结果差不多花了 20 分钟 -- 所花的时间比我找到一个预先编好代码的子程序库然后再去理解它们所花的时间要少。 熟悉 Forth 可能需要费一些时间,因为它的概念与其它语言是完全不同的。例如,一个变量名是一个字,它把变量的地址放到参数栈上。如果你希望得到变量的值或者把栈顶的值保存到变量中,你就必须在变量之后使用字进行“取”或者“存”,这些操作字分别简写成 @ 和 !,当然初看起来有些奇怪。如果你忘了 @ ,则你后面处理的都是变量的地址而不是它的内容。由于这样的操作在 Forth 中也是合法的,所以没有任何错误提示。 Forth 处理的另一个不同的数据类型是常数。一个常数也是一个字,它把预定义的数值放到堆栈顶上。但是,如果你不想把数定义为常数, Forth 也可以把它当作字面量来编译。 不像其它的编译器,一个 Forth 编译器不仅仅产生代码。它也解释用户的输入。 当处理代码的时候, Forth 的行为就像一个汇编器。它的全部行为就是在字典中查找源程序中的每一个字。许多 Forth 实现使用散列来提高查找速度。如果编译器没有找到这个字,就试着把它作为一个数来处理,如果不成功,就打印错误信息。如果在字典中找到了这个字,它就做下面两件事情之一:编译或者执行。做哪件事情依赖于当前处于一个定义的编译之中,还是结束了一个定义正等待另一个的开始。 当进行一个字编译的时候,编译器从字典中得到这个字的地址,然后把它加入到当前的目标代码中。如果编译器结束了一个定义,它就执行这个字。这就是你在键盘上打入一个字时发生的事情。为了执行一个字的代码,许多 Forth 版本使用一个短小的机器码子程序读出字代码中的每一个地址,然后跳转到其中执行。 有一些编译器控制字总是执行的,不论是不是处在编译处理过程中。其中之一就是“冒号”(:) , 它告诉编译器给字典增加一个新的定义。源代码中下一个字就变成了字义的名字,接下来是构成这个字的其它定义字。每个字依次得到编译,当源程序中遇到“分号”(;)时,定义结束。 一个字一但被定义,它就可以用来定义其它的字,但是字也可以是“向前引用”的,并允许递归。 以 Forth 执行程序的方法,“取下一个代码的地址并调用”,要比子程序调用或者执行在线编码的时间要长一些。 Forth 程序的执行速度理论上比其它的编译语言要慢(有些 Forth 编译器生成可执行的机器代码,当然就没有这些开销了)。但是实际上,由于没有比如垃圾收集等操作引起的系统开销, Forth 的算术逻辑操作可以执行得和预期一样快。它也能够非常平滑地处理中断,理由是 Forth 不需要保存上下文,因为上下文已经在堆栈上了。 Forth 的执行方法可能导致比其它编译语言慢,但是代码本身却是极其压缩的。一个 Forth 编译不管编译成 TOKEN 还是地址都只占 1 个或者 2 个字节。一个嵌入式 Forth 系统通常包含操作系统,尽管如此, Forth 和 Forth 程序仍然是非常紧缩的。 一个编译器可以用 1200 行代码写成,编译后占大约 6K 字节。一个运行时间内核所占的空间不多于 2K 字节。许多年来,我一直在运行 MSDOS 的 PC 机上使用商业化的 Forth 系统。它包含一个内建的源码编辑器,但只占用了 15K 字节的内存。每个 Forth 占 1-2 个字节,一行源代码大约含有 4 或 5 个字。一个有 64K 字节寻址空间的处理器可以运行 5000 行的程序。 只运行选项在编译和程序调试期间, Forth 需要保持定义字典可用。但在最后的产品中,字典名显然是浪费空间,而且它们会含有开发者不愿意公开的信息。幸运的是,许多 Forth 编译程序提供了创建只运行代码版本的选项。这种版本既不包含字典结构,也不包含未引用的标准字。这不仅可以缩短代码长度,也保护了工程师的劳动。 如果你准备用 Forth 编写你的嵌入式应用程序,你可以使用两种方法: 在 PC 机上编写和编译程序,使用 PC 的磁盘和编辑工具,然后把程序下载到目标系统的 PROM 、 RAM 或者 EEPROM 中; 可以在 PC 上模拟程序的执行。 第一种方法仅仅是把 PC 机当成一个哑终端来使用,而编写和编译程序都在目标机上进行。这种方法要求目标系统有串行口一类的通信装置和足够的 RAM 以容纳 Forth 编译器。这种方法给调试带来了极大的方便,因为代码是以正常速度运行的,并与硬件直接交互的。 第二种方式使 Forth 成为一个有用的原型硬件调试工具。我们可以很快地写一个字去访问一个接口的位,例如检查板上的信号是否丢失。我还发现在编写生产测试和调试程序时, Forth 也是一个可选的优秀语言。 让一个原型 Forth 系统运行是没有任何问题的。 Forth 已经在现有的每一个微处理上实现了。但是,它在具有两个堆栈、字长度与参数栈宽度匹配的处理器上运行将特别有效。有足够片上 RAM 以实现堆栈的微控制器运行 Forth 将特别快。 平台的灵活性又一次显现出来了。 1982 年,继 60 年代末发明 Forth 之后, Forth 语言的发明者 Charles Moore 使用门阵列在一个单片上实现了 Forth 机器(参见 Fast Processor Chip Takes Its Instructions Directly From Forth," electronic design, March 21, 1985, p. 127 )。这个设计后来被 Harris Semiconductor 公司发展成为 RTX2000 系列单片机。不幸的是,这个芯片在生产数量上一直没有达到应该达到的要求。 Forth 的应用也在许多嵌入式领域得到证明。麦哲伦探测器的金星雷达成像系统就使用了我设计的数据块处理器,它由一个运行在 Z80 上的 Forth 程序控制。我编写了它和操作系统,用了大约两个星期的时间教会了一个硬件工程师足够的 Forth 知识来编写 2000 行代码。无论什么时候客户希望新的测试,我们都可以用几分钟的时间把它们集成到程序中。 如果项目最后的应用程序要求必须用另一种语言编写,我就把 Forth 作为一种可执行的程序设计性语言( PDL )来使用。一但应用程序在 PC 机上调试完成,它就可以相对简单地使用汇编语言重新编码。例如,我最近就在 PIC 单片机上开发了一个 TOKEN 化的 Forth 。 另一个极端的情况是,我在一个窗口环境中使用 Forth 来进行通常的编程工作。它包括一个下拉菜单,一个参数的滑动输入。 Forth 在一个窗口中运行,而一个源代码编辑器在另一个窗口中运行,一个鼠标可以确保你在两个窗口之间移动。 尽管 Forth 提供了这些优点,不过你可能会发现如果想在下一个项目中就使用 Forth 是非常困难的。这完全是由于组织的惯性所造成的。对于项目经理来说,最困难的事情就是:眼下的项目由于设计者需要学习一种新的语言而花费更长的时间和更多的成本。在较短的时间编写和调试嵌入式程序是一件好事;使用更便宜的处理器和更少的 PROM 生产产品总是可以获利。但是,改变做事的方式也需要耗费时间和金钱。 我们可以这样来平衡费用和使用 Forth 语言的收益: Forth 语言是快速和交互式的,需要更少的编程时间,能够产生更短、更可靠的代码。 C 语言神话的背后有许多商业的原因,所以转换到 Forth 很难在短期收益。然而,如果你希望长期在嵌入式系统工作, Forth 的低开发成本和便宜的硬件将使你在未来受益。 一个 Forth 的例子这个程序片段是从一个实际的应用中截取的。它接受一个 0000-9999 之间的频率输入,利用一个 100 点输入项校正表插值为一个压控振荡器产生非线性的驱动电压。这里, Forth 代码用大写,注释用小写,注释放在括号中,或者由一个斜线开始直到行尾结束。 定义从“冒号”(:)开始至分号(;)结束,同时还给其它程序员一个“堆栈图释”。“图释”就是定义名之后括号中的注释,它显示了这个字将使用的堆栈参数(从下到上),以及这个字执行之后留在栈上的参数。 10000 CONSTANT MAX-INPUT \ 定义一个名为 MAX-INPUT 的常量 100 CONSTANT WIDTH \ The separation between table entries. CREATE TABLE1 MAX-INPUT WIDTH / CELL * ALLOT \ Generates TABLE1 having room for 100 stack elements. \ CELL is the stack element size in bytes, typically 4. \ This table will be filled when the system is calibrated. : INTERPOLATE ( point1 point2 fraction width --> value ) \ fraction/width = interpolation factor between point1 and point2 >R >R \ Move width and fraction to the return stack OVER - \ Size of interval = point2 - point1 R> R> \ Recover width and fraction */ \ Do size*fraction/width with double precision + \ Add result to point 1 ; \ End of definition : LOOK.UP ( user input --> equivalent value from table ) DUP 0 MAX-INPUT WITHIN? NOT \ Test user's input IF ERROR1 DROP 5000 \ Flag error and insert dummy THEN \ Some Forths use "ENDIF" WIDTH /MOD \ Get table index and fraction DUP TABLE1 + @ \ Look up lower table entry SWAP 1+ TABLE1 + @ \ Look up upper table entry ROT WIDTH \ Move fraction to top of stack and get width INTERPOLATE \ Call the interpolation routine ; \ End of definition 关于 Forth 的更多信息想得到更多关于 Forth 的信息,我们可以从非盈利的 Forth 兴趣组织( FIG )开始。 FIG 出版了一本杂志,名为“ Forth DIMENSIONS ”,同时销售图书和 Forth 的公共域版本。关于 Forth 方面的书籍,我推荐 Leo Brodie 的经典名著“Starting Forth” , 虽然这本书有些陈旧。如果买不到,可以通过 FIG 购买。该书作者的另一本书“ Forth 思维方式”也值得一读。 如果你需要使用为嵌入式系统开发的 Forth 编译器,则可以花 30 美元购买公共域版本,也可以花 2000 美元购买运行在 PC 机上的全功能的交叉编译器。 Forth INC. 公司是嵌入式 Forth 和其它 Forth 应用最大的供应商,而一些小的 Forth 开发商也在 Forth Dimensions 上打广告。如果熟悉 Forth 系统,还可以利用公共域得到的Forth 系统按自己的需要进行改写,这并不困难,我自己就编写了 4-6 个 Forth 编译器,使用的却是来自 1978 年的、过时的 FIG-Forth FOR 8088 。 Forth 研究相关文章 |
|||