AVRILOS:AVR 微控制器的简单操作系统
一种轮询式操作系统(无抢占式多任务处理),
摘要
一个嵌入式系统简单操作系统框架,允许快速开发针对AVR系列的应用,但可以很容易地移植到其他架构。在本文中,我将描述这个操作系统的概念和结构,并提供一个示例应用程序,以便读者理解轻松构建新事物的简单性。
相关链接
引言
嵌入式系统是一个非常有趣的领域。你可以使用硬件和软件做一些让每个人都惊叹不已的事情,除非这种美被很好地隐藏起来。伴随着它们的问题、限制和特殊要求,如果你重复构建这样的系统,每个设计都会有一些共同点。例如,我总是使用系统滴答计时器和UART。这就是为什么我选择构建一个操作系统核心框架,它能让我更快地构建应用程序,而不会过于复杂。这个操作系统不进行抢占式多任务处理。相反,它是一个轮询协作系统,即每个任务要么做一些事情,要么在等待新数据时退出(不阻塞)。它非常简单,内存占用非常小,而且你可以非常容易地添加或删除组件,随时在你的下一个项目中使用它。有了这个操作系统,我可以非常快速地开发我的小型应用程序,因为当我已经拥有了基本运行条件时,唯一缺少的部分就是我的纯粹应用程序:我需要做什么,这通常是一个或两个页面的程序。我可以在几天内编写并使其运行起来……
背景
当我开始使用嵌入式系统时,我编写汇编语言,我们使用带 EPROM 作为程序内存、RAM 极少的微控制器。在每个项目中,我几乎都必须使用系统时钟计时器,并且经常使用 UART 与主机 PC 进行通信。大约在 90 年代末,随着 AVR 的引入,我转向了 ISP(系统内编程)。不再需要笨重的 EPROM 擦除器,几秒钟内即可轻松编程等。然而,那时内存仍然受限。在将 8051 代码移植到 AVR 的过程中,我需要一些东西。一个系统时钟计时器和一个 UART。我的下一个问题是调试。尽管 Atmel 有一个 AVR 模拟器,但当需要从外部环境获取输入时,我无法测试我的应用程序。因此,为了更容易调试,我构建了一个占用空间非常小的监视器:只需读/写端口、内存并操作外部外设。这个调试器是任何新构建的组成部分。后来我转向 C 语言,因此我再次将大部分代码用 C 重写。此外,随着我添加越来越多的外设,而程序内存又很宝贵,我开始配置我的微型内核,通过 #defines 来添加或删除组件。最终结果是拥有一个允许我快速构建应用程序的平台。我只需所有基础设施支持,然后专注于构建实际的应用程序。我只对特定代码片段使用模拟器(更多是为了查看实际的 C 语句做了什么)。在调试期间,我使用我的监视器、串口的 `printf`,当然还有万用表、示波器以及硬件方面的 LED(!)如果需要的话。
此外,由于我使用 GCC 进行编译,所以我不使用 IDE 进行 AVR 开发。我使用 makefile 来构建和配置我的构建,并使用我喜欢的编辑器进行编码。因此,这个框架理论上可以移植到任何微控制器(例如 PIC、ARM 等)。事实上,我已将变体移植到 ARM 和 ColdFire 处理器/控制器。当然,你可以自由使用你喜欢的 IDE。
由于内存有限,而且我不需要抢占式多任务的复杂性,所以这个操作系统的理念是每个任务检查是否有输入要处理,如果有要做的事情就执行,否则就返回到轮询主循环。优点是它非常可扩展,你无需担心复杂的事情,它对 RAM 非常高效,而且没有上下文切换,因此可以节省执行时间。缺点当然是,最坏情况下的执行(所有任务都执行)应该足够小(最好小于系统滴答周期),但这取决于你的应用程序!你可能会偶尔打破这个规则。但是,我避免这样做,并且我相信对于大多数项目来说,这个循环的时间不应该成为问题。
描述
项目目标
该项目的目标是提供一个可扩展并允许快速应用程序开发的平台。我最终用 C 语言编写了它,因此在某种程度上它是可移植的。你需要为每个新处理器编写主要外设,这是主要的难点。但是,在核心系统启动并运行后,你可以从这个结构中受益,将其作为基础项目来构建新的应用程序。由于我最初是为 AVR 构建的,所以我将其命名为“AVRILOS”:**AVR** **IL**ias **O**perating **S**ystem(AVR 伊利亚斯操作系统)。我假设你已经准备好 AVR 硬件。为了方便你,并且由于各种 I/O 映射到特定的端口(尽管你可以轻松更改它们),我提供了我的基本原理图,它或多或少地在各个项目中复制(像 AVRILOS 一样),并根据我的特定问题进行添加。
工具
我用于 AVRILOS 的工具是
- (软件) WinAVR (Windows 上的 AVR GCC)。
- (软件) Atmel AVR Studio (用于仿真)。
- (软件) 你偏好的编辑器。
- (软件) 终端程序 (例如 Terminal, PuTTY)。
- (软件) 编程器软件 (AVRDude 已包含在 WinAVR 中,但如果你愿意,可以使用 AVREAL32)。
- (硬件) 你的控制器所在地的硬件板!
- (硬件) 编程器加密狗。
- (硬件) USB/RS232-TTL 串口电平转换器,用于连接到显示器。
我选择性地使用了一些额外的工具
- (软件) CVTFPGA(用于将 Xilinx Spartan FPGA 的串行比特流集成到我的代码中,稍后详细介绍)
- (软件) Hexbin3
- GNUWIN32(如果我不使用 WinAVR,例如其他编译器包如 MPLAB,则用于 makefile)
- (软件) Python 和 Python Wx 用于构建主机应用程序。
- (硬件) 示波器(推荐)
- (硬件) 万用表(至少也要有!)
- 任何你能想象并适合的。
AVRILOS
目录结构
目录结构如下:
有两个主要目录,HW 和 SW。
- HW 是我所有硬件开发完成的目录。这包括板级原理图和 PCB 文件以及 FPGA 设计。
- SW 是软件目录,其中包含所用处理器的目录名,这样多年后我才知道每个项目使用了哪个处理器。此外,我可能会在这里放置主机软件(在另一个名为 host 的目录中)。
让我们专注于 avr16 的目录结构。AVR16 指的是 ATMega16 AVR。你可以随意命名。因为编译器倾向于生成许多中间文件,我不希望它们干扰我的源文件。因此,我特意为此包含了三个目录
- build.dep:这里放置 C 文件的依赖项。这些是由 makefile 自动生成的。
- build.err:我在这里指示编译器放置任何错误文件,以便在需要时进行跟踪。
- build.obj:我在这里放置每个模块的目标文件。还有最终的 .elf 文件。从 MAP 文件中我可以找到任何变量的内存位置,并将其用于监视器中进行检查。
- build.lst:每个模块的列表文件。
- build.rom:最终的编程文件,可用于设备编程。
根 makefile (名为 makefile) 位于 avr16 的根目录。
cfg 目录包含所有卫星 makefile。这些文件包含配置选项、编译器命令等。
src 目录是您所期望的。源文件。其中包含:
- Applic:主文件 Kernel.c,其中包含“调度器”。初始化和主循环都在这里。我的特定应用程序 C 文件也放在这里。
- peripheint:微控制器的内部外设。这些是定时器、UART 等。
- periphext:微控制器外部的外设。这些可能是智能卡、LPC 闪存、SPI 设备等。
- utils:包含许多类型转换工具(hex2bin、bin2hex)、延迟等。当然,如果内存空间足够,你也可以使用 stdlib 的
sprintf
。 - debug:这里有我的监视器调试器,此外还有一个扩展调试文件,其中我为可能使用的外部外设单独启用或禁用函数。如果我不使用它们,我就禁用相应的函数,并节省一些内存空间。
- include:这里有全局定义和设置。如果可编程,还包括每个使用的外设的引脚/端口分配。
内核描述
Kernel.c 包含初始化代码和主循环。在启动期间,内核执行每个模块/外设的各种初始化。没有一个初始化文件适用于所有模块。相反,每个模块通过读写修改指令修改其 I/O 寄存器中的位。这样,每个外设就不会相互干扰,除非存在端口冲突。这允许更模块化的设计以及轻松添加/移除模块。
调度器以轮询方式执行我们需要的任务。每个任务检查是否应因某个标志(如 SysTick 定时器(在 periphint/Timer0.c 中))而被触发
if ( ( v_SysStat & (1 << b_SysTick) ) != 0)
检查定时器中断是否已在 `v_SysStat` (变量) 中设置了 `b_SysTick` (位) 标志,如果没有则退出。如果设置了,则执行所有需要的定时器功能。
另一种情况是测试串口中是否有新数据(就像 `applic/serial/SerApp.c` 所做的那样),如果没有则退出。
我几乎总是包含的模块有:`SysTick`(执行毫秒级软件定时器、LED 状态指示等)、`debugger`(用于调试的监视器)、`SysADC`(按顺序捕获所有 8 个通道,因此应用程序只需读取 ADC 数据的内存位置)、`SerApp`(一个小型串行命令应用程序)。我们还可以选择让应用程序在每个主循环中运行,或者选择每 n 次运行一次应用程序(即 LED 闪烁时)。
if ( ( v_SysStat & (1 << b_AppTick) ) != 0)
这允许应用程序使用简单的计数器作为延迟或超时,因为修改(增/减)操作是在每个 SysTick 中完成的。当然,稍作修改,您可以为 LedAlive 和应用程序设置不同的步调。
最后,对于低功耗应用,我在循环末尾添加了一个睡眠命令。如果没有活动中断,系统将进入低功耗模式,直到发生事件。
因此,要创建新任务(应用程序、设备等),你需要知道新元素不应长时间阻塞系统。具体来说,所有函数的执行延迟不应超过 SysTick 定时。如果超过了,你有两种选择:要么增加 SysTick 间隔,要么减少阻塞时间。
根据我的经验,到目前为止,我还没有需要为任何项目调整这些时间。
我使用的另一个概念是我的中断例程是最小化的。例如,`timer0` 中断只是设置一个标志然后退出。主循环将执行 `SysTick`(延迟处理程序),后者将完成所有繁重的工作。当然,中断可能会更复杂,比如将数据放入 FIFO 的串行中断。但其思想是避免在中断级别进行任何主要处理。因此,中断阻塞的可能性将最小化。
此外,我在中断和延迟处理程序之间使用简单的生产者-消费者通信。我通过原子操作或单向动作(只写/只读变量)检查背景中每个变量的修改不会受到中断的影响。这些将在 UART 模块中可见。
模块描述
模块:SysTick
`SysTick` 模块执行所有主要的定时功能。它使“活跃”LED 闪烁,触发 ADC 开始新的转换,触发 LCM 延迟结束,触发键盘扫描功能,并且还包含软件定时器。
这些触发器是位于 `v_SysStat` 中的简单标志(位)。你可以通过修改文件开头的 *includes/ifc_time.h* 常量来轻松更改应用程序时间间隔。
// Alive Led Indications Set
#define c_ALIVEOK_ms 250 /* Alive LED When Ok */
#define c_ALIVESER_ms 500 /* Alive LED When Serial Error */
// Timed Tasks interval
#define c_AppInterval_ms 8
#define c_ADCInterval_ms 32
#define c_KEYInterval_ms 16
对应的激活位定义在 *includes/settings.h* 中。
/*************** SysStat Register ******************/
#define b_SysTick 0
#define b_DBG 1
#define b_AppTick 2
#define b_ADCTick 3
#define b_LCDBusy 4
我省略了未由 `Systick` 定时器激活的标志。标志的清除由 *Kernel.c* 主循环完成。键盘扫描和 LCD 操作直接由 Systick 调用,因此 `v_SysStat` 中没有这些的标志。
由于这些常量以毫秒为单位引用,你可以根据需要进行更改。CPU 时钟频率在 *cfg/hw.in* makefile 中定义,所有计时应立即符合,除非你需要不同的预分频器(稍后在同一文件中设置为 CLK/64)。
/* Select Clock Source for T0 */
#define c_T0CLK c_CLK64
#define c_T0DIV 64
此外,您可以使用 `f_SystickSetErrLevel` 函数提供动态错误指示。此函数修改闪烁 LED(Alive)的间隔(以滴答为单位),以便您可以通过更改 LED Alive 的闪烁间隔来通知用户出现问题/异常。例如,在串行通信错误的情况下,您可以从应用程序中调用
f_SystickSetErrLevel(c_ALIVESER_ticks);
`c_ALIVESER_ticks` 在 *include/ifc_time.h* 中稍后定义,并由同一文件开头的相应常量 `ALIVESER_ms` 派生。
那么,当您需要一个定时器间隔来执行通用超时或其他任何操作时,该怎么办?很简单!SysTick 提供了可编程的(`MAXSWTIMERS`)数量的软定时器,以及一些用于方便引用的附加定义。
// Maximum SW Timers (mS)
#define c_MAXSWTIMERS 4
#define c_SwTimer0 0 /* Timer Activation (0: Stop, >0: Run) */
#define c_SwTimer1 1
#define c_SwTimer2 2
#define c_SwTimer3 3
这些计数器以毫秒为单位计数。要启动一个定时器,你需要编写
buf_SwTimer[SwTimer0] = 10;
这会启动软定时器 0,超时值为 10 毫秒。
查看定时器是否到期
If (buf_SwTimer[SwTimer0] == 0) Action_Timer0_finished();
提前停止计时器buf_SwTimer[SwTimer0] = 0;
此外,您可能需要一些小的延迟函数(仅几微秒)。在这种情况下,您可以在 *utils/delay.c* 中找到一些阻塞函数。这些函数可以在初始化期间使用,当您等待外设启动时。例如,我在启动时进行 FPGA 代码配置时就使用这样的延迟。或者,您也可以在 SPI 组件上使用微秒延迟。
模块:UART
UART 是我最喜欢的外设。由于我的大多数应用程序不需要直接使用 UART,所以我的硬件中没有添加 TTL-RS232 电平转换器来连接到我的 PC。鉴于我经常手工制作电路板,我不想为仅在开发期间有用(用于我的监视器或应用程序)的功能浪费元件、导线和时间。所以我所做的,就是在标准(当然是我的标准)排针上放置 Tx/Rx 信号以及电源和地,然后使用适配器(加密狗)连接到我的 PC。这个加密狗可以是一个简单的电平转换器(这就是为什么我需要在排针上供电),或者现在,由于每台计算机都使用 USB 端口,我使用 USB-串口转换器(例如我的 Polulu AVR 编程器提供了这个附加功能)。现在,每个电路板都有这个带有 3-4 根线的排针,我就搞定了。
UART 具有简单的 I/O 功能。要进行初始化,请调用 f_ConfigSerial。波特率在 makefile (hw.in) 中定义。其余设置是标准的 8N1。如果需要更改它们,则必须更改 uart.c 中此函数的源代码。
在硬件层面,我们支持多种处理器,并且 Tx/Rx 都使用软件 FIFO。FIFO 的大小在 *includes/settings.h* 中定义
/*************** UART Buffer Sizes ******************/
#define c_RXBUFLEN 16
#define c_TXBUFLEN 64
如你所见,Tx FIFO 更大。我这样做是为了能够从应用程序响应一个较大的 Tx 字符串而不阻塞系统。如果 FIFO 小于发送到主机的较大字符串,那么 Put String 函数将等待(并阻塞在那里),直到它可以将所有数据写入 FIFO。
主要接口函数是:
bool f_Uart_PutChar(INT8 c);
INT16 f_Uart_GetChar(void);
bool f_Uart_PutStr(INT8 s[]);
f_Uart_PutChar
:将字符放入 TxFIFO 中,除非 FIFO 已满。返回代码表示成功(1:成功,0:失败)。它不会阻塞系统。
f_Uart_GetChar
:检查是否有可用字符,如果有,则将其从 Rx FIFO 中移除并返回给应用程序(-1 [0xFFFF]:失败,0x00XX:收到字符)。它不会阻塞系统。
f_Uart_PutStr
:向串口发送一个以空字符结尾的字符串。总是成功。如果 Tx FIFO 小于字符串,此函数可能会阻塞系统。
如果你需要一个 `printf` 函数,你可以使用 `sprintf` 函数将数据写入缓冲区,然后将其发送到 `f_Uart_PutStr`。或者你也可以构建自己的 `printf` 函数。
为了最小化占用空间,我使用位于 *utils/typeconv.c* 中的简单转换函数。
模块:调试器
虽然在本文中我称之为“调试器”,但实际上我指的是一个监视器。我构建的调试器/监视器不接管执行控制,不单步执行指令,也不做任何普通调试器会做的花哨事情。它更像一个监视器。你可以在运行时更改变量(不中断程序执行),你可以检查内存、端口、I/O 或写入这些外设。你还可以操作其他设备,如 SPI、LPC 或 FPGA,如果你在监视器中添加了此功能。
基本功能总结如下:
命令与格式 |
摘要 |
|
读取地址 XXXX 处的字节 |
|
将字节 YY 写入 XXXX |
|
查看从 XXXX 开始的 4 字节十六进制值 |
|
查看从 XXXX 开始的 4 字节 ASCII 值 |
|
PINA/B/C/D |
|
写入端口 PortX(01-04), DDRX(11-14) YY |
|
读取端口 PortX(01-04), DDRX(11-14) |
|
读取模拟端口 0X (X: 0-7) |
|
检查地址 XXXX 处的 EEPROM 数据 |
|
将字节 YY 写入地址 XXXX 处的 EEPROM |
|
用户命令 |
|
读取 LCM 地址 XX |
|
写入 LCM 命令 XX |
|
写入 LCM 数据 XX |
|
读取 LPC 总线地址 XXXXXXXX-X+4 处的 4 字节 (32 位) |
|
在地址 XXXXXXXX (32 位) 写入 LPC 字节 YY |
|
在 LPC FLASH SST49F020/A 写入一个字节(带写保护) |
|
恢复到串口应用。禁用调试器 |
|
读取 SPI DR, SR |
|
读取 FPGA 寄存器 XX(自定义命令,取决于 FPGA 代码) |
|
在寄存器 XX 写入 FPGA 字节 YY(自定义命令,取决于 FPGA 代码) |
**注意:**所有数字均为十六进制格式,并且应为大写(区分大小写)。
例如,要检查 RAM 地址 0x10 处的变量,请在串行终端中键入
R 0010
如果你需要将此内存位置的内容修改为 0x55 的值
W 0010 55
如果你需要检查 AVR 的 PIN A,只需键入“1”,端口 A 的引脚状态就会返回。
变量地址可以在 */build.lst/kernel.map* 文件中找到。例如,你可以搜索 `v_SysStat` 地址并找到类似以下内容:
*(COMMON)*
COMMON 0x00800160 0x2 build.obj/kernel.o
0x00800160 v_SysStat
0x00800161 v_StatReg
所以 AVR 的 RAM 地址是 0x160。
你可以在 `dbgext.c` 中提供自己的命令,但你还需要修改基础的 `debugger.c` 文件来获取输入并处理新命令。我更喜欢在单独的 `dbgext.c` 中实现额外命令,同时在 `debugger.c` 中添加命令识别和解析。然后,我根据 `HW.in` makefile 模块定义在需要时启用或禁用相应的命令。额外命令的功能始终存在于 `debugger.c` 中,它调用 `dbgext.c` 的函数。
此外,还有一个空的 user 命令 ('`U`'),你可以在每个应用程序中以不同方式实现它,而无需进行更复杂的 `dbgext.c` 修改。
调试器模块不使用 `sprintf` 或 `stdio` 库,因此占用空间最小。在内存更多的处理器上,你可以实现一个更好、功能更强大的监视器,但一个占用空间小的监视器可以在任何地方使用。
其他模块
我包含了 LCM 和 keymat 4x4 模块。然而,这些模块经过部分测试,可能并非总能正常工作。特别是 LCM_char 模块是基于 Joerg Wunsch 针对 HD44780 控制器编写的代码。在另一篇文章中,我们将讨论 FPGA SPI 编程以及如何从 *.bit* 文件生成 C 代码的流程。
系统设置
为了让 make 文件工作,你需要调整 *env.in* 文件。在这里你需要识别编译器可执行文件、编程器等。你需要将可执行文件放在你的路径中,否则你可能需要添加工具的绝对路径。
Cfg/Srcobj.in 根据 hw.in 文件的定义来确定将使用哪些源文件/目标文件。
Cfg/Srcdef.in 将 hw.in makefile 变量定义转换为 C 文件使用的 `#define` 预处理器变量。
最后,cfg/hw.in 配置所有硬件参数(即晶体时钟频率、波特率、活动模块等)。
要编译代码,请在 makefile 所在的根目录打开命令提示符。然后输入“`make`”或“`make all`”。要进行新的构建或修改 *hw.in* 文件时,需要先进行新的干净构建:“`make clean`”,然后“`make`”。
您可以在编译过程中看到各种消息。如果出现错误,编译器会停止并指出特定文件中存在问题的行。
当 rom 文件准备就绪后,你可以输入“`make prog`”或“`make progsp`”将程序下载到你的目标设备。prog 选项用于 AVReal 编程器,而 progsp 则使用 avr-dude,这是标准的 gcc 编程工具。
输入“`make size`”可以查看目标大小。
输入“`make list`”可以查看可用选项。
硬件引脚控制都设置在 *includes/settings.h* 文件中。我所有的硬件设备都使用引用此文件的宏定义。因此,我将所有 I/O 集中在一个地方,并且对于每个硬件,我只需要更改此文件。当然,对于 UART 或 SPI 等特定外设,由于功能是硬连接的,存在一些限制。
在下图中,您可以看到一个编译示例。
示例应用程序
我们的示例应用程序将实现一个锁。钥匙将是一个简单的智能卡,并控制一个舵机进行锁定。系统的输入是有效的智能卡(具有正确的序列号),输出是控制 R/C 舵机锁定/解锁的 PWM 信号。
有关智能卡的更多详细信息,请访问
http://support.gateway.com/s/Mobile/SHARED/FAQs/1014330Rfaq21.shtml
在此应用中,我使用了用于电话应用(卡式电话)的简单 PROM 型智能卡。在下图中,您可以看到读卡器(用于连接卡触点到 PCB 的无源元件)和智能卡的引脚。
在下面的图中,你可以看到舵机锁的机械配置。
下面的有限状态图展示了应用程序逻辑控制是如何实现的。
主要有四种状态。最初,系统进入 `IDLE` 状态。在插入有效的智能卡之前,系统一直等待。插入代码中预定义的有效卡后,系统激活舵机并等待预定时间,直到 R/C 舵机在 `WaitTO2Lock` 状态达到最终位置。然后,它进入 ARM 状态并再次等待有效的智能卡,以执行相反的操作,激活 R/C 舵机回到初始位置。之后(`WaitTO2UnLock`),它返回到初始的 `IDLE` 状态。
我们将使用调试器来找出舵机(锁定/解锁)的最佳启动和停止位置的 PWM 值。根据您实现锁的机械组件的方式,您将需要不同的 PWM 值来表示两个终端位置。通过使用调试器,我们将更改控制这些位置的两个变量,从而通过试错确定正确的 PWM 值。这个小例子将展示如何使用调试器/监视器的一小部分来开发您的应用程序。
下面显示了一个示例的启动屏幕

OS/APP 语句后面的零来自智能卡模块(我的硬件中没有连接读卡器)。
最小和最大 PWM 值存储在两个变量中
v_ServoLock
, v_ServoUnLock
我们转到文件 *build.lst/kernel.map*,然后搜索“`v_ServoLock`”
v_ServoLock 0x1 build.obj/applic.o
0x00800171 v_ServoLockv_ServoLock build.obj/serapp.o
build.obj/applic.o我们找到了三个实例。第一个是变量的大小。第二个是地址(偏移量为 0x800000)。最后一个是使用该变量的模块。
对我们来说重要的是第二个。所以 `v_ServoLock` 的地址是 0x171,它是一个字节。
我们通过在终端输入“* <Enter>”进入串行应用程序。然后我们尝试直接按键操作“n”和“m”来控制舵机的最小-最大值。然后我们通过“& <Enter>”进入调试器。
然后我们读取 `v_ServoLock` 的值
> R 0171
32
>
返回的值 32 是十六进制,即十进制 50,这是变量的初始值。现在我们把这个值减小到十进制 40 -> 0x28。
> W 0171 28
> R 0171
28
>
下面显示了一个类似的例子

重新进入应用程序,输入“* <Enter>”,然后重试“m”命令。舵机的最大终端位置应该改变!你可以玩转这些,找到可以硬编码到应用程序的最佳值。更好的是,你可以使用 EEPROM 来存储并在程序中用于此类设置。
您可以对 v_ServoUnlock 进行同样的操作,并调整其值。
这是一个小例子,展示了如何无需重新编程控制器即可进行实时测试。您可以测试引脚和端口 I/O,以查看控制器是否“看到”端口上的正确值,或者是否有任何端口似乎浮动(例如,当您触摸引脚时,某个位会随机切换)。
串行应用
串行应用程序是另一个用于直接控制应用程序的示例任务。例如,它与调试器无缝协作。在调试器中输入“*”会恢复到串行应用程序,反之亦然。在串行应用程序中输入“*”可以恢复到调试器。
所示的串行应用程序支持两种不同的模式。直接按键执行和 shell 执行。为简单起见,shell 命令是单个按键(类似于调试器命令),后面可能跟有参数,并以 <Enter> 键(0x0D)终止。
直接按键执行模式是指在终端上按下按键时执行的操作。例如,我们有一些命令可以控制舵机位置,这样您就可以看到您的硬件是否正常工作。例如,按下“c”键,舵机会缓慢地向一个方向旋转,而按下“v”键,舵机会向另一个方向旋转。控制按键操作的函数是“`f_ProcessAPPKey`”。
提供更类似 shell 界面的 shell 函数是“`f_ProcessAPPCMD`”。例如,输入“q <Enter>”与按“c”键效果相同。这样,您的应用程序就可以具有双重控制,并通过终端灵活地进行控制。
一个重要的方面是,进入串行应用程序主函数后,我们会检查调试器位是否已设置。如果已设置,则跳过执行,因为调试器处于活动状态。同时,“`f_ProcessAPPCMD`”必须支持星号“*”命令,以便恢复到调试器。如果您想盲目地跳到调试器模式而不了解之前的状态(使用“*”在调试器和应用程序之间切换),可以使用“&”字符返回到调试器。
结论
在本文中,我提出了一个简单的框架,用于更快、更容易地构建嵌入式系统。当然,开发嵌入式系统还有更简单的方法(例如使用基本解释器),但这是一个非常可扩展的系统,其便捷的开发不会牺牲性能或功能。我从 15 年前开始在 8051 汇编语言中构建这个系统,然后用 AVR 汇编语言编写,之后又用 C 语言重写了整个框架。我随着时间的推移进行了各种修改,现在它就在这里。我向公众提供源代码,以帮助更多人探索嵌入式系统的美妙之处。我也希望人们能贡献自己的外设和扩展,从而建立一个更大的组件库。这将使其他人开发新系统变得更加容易。
许可证
本文,以及任何相关的源代码和文件,均根据 CDDL <https://open-source.org.cn/licenses/cddl1.php> 许可。
历史
-
版本 1.0,初始发布。
-
版本 1.1,修正了 *ifc_time.h* 定义(第 77-81 行)中 `INT8U` 到 `INT16U` 的类型值。修正了文章中一些小的错别字。