嵌入式系统的 C 编程






3.84/5 (12投票s)
基于事件的任务调度。
引言
如今,嵌入式系统开发人员普遍使用操作系统来增加更多功能,同时缩短复杂系统的开发时间。本文旨在为初学者和中级程序员提供一个简单易懂的嵌入式系统编程调度技术概述。我将使用“C”作为编程语言,并考虑一个没有操作系统的、运行在专用硬件/设备上的应用程序。二进制输出(*.bin 文件)在上电后可以直接从设备运行。在这种情况下,时间调度是系统开发的重要组成部分。我们应该确保正确的任务在正确的时间执行。
什么是嵌入式系统?
嵌入式系统是计算机硬件和软件的某种组合,其功能固定或可编程,专门为特定类型的应用设备设计。简而言之,我们可以说,嵌入式系统是一种专用计算机系统,旨在执行一个或几个专门的功能。
并非所有嵌入式系统都必须是实时系统。实时系统是指及时性与输出的正确性同等重要的系统。性能估算和优化在实时系统中至关重要。根据定义,我们可以说实时系统是必须满足明确的(有界的)响应时间限制,否则可能面临严重后果(包括失败)的系统。
嵌入式系统在我们的日常生活中扮演着重要角色。全球大多数人高度依赖手机、iPod 等各种小工具。工业机器、汽车、医疗设备、飞机和自动售货机中使用的嵌入式系统必须是实时的。
我希望能分享我在嵌入式系统方面的经验。这是我在 CodeProject 上的第一篇文章,因此我期待您宝贵的反馈和建议以求改进。
背景
人们正在为复杂设备使用操作系统(RTOS),以使其更灵活、增加更多功能并缩短开发时间。但对于小型应用程序来说,这会增加设备的成本。因此,对于不使用操作系统的应用程序固件开发来说,是非常流行的。
时间调度是实时系统的一个重要方面。实时软件响应外部事件而执行。该事件可能是周期性的,在这种情况下,需要对事件和相关任务进行适当的调度以保证性能。调度策略还取决于所选 RTOS 提供的调度设施。本文重点介绍设备上没有操作系统时基于事件的技术。
调度
实时系统中使用两种调度技术
- 静态调度
- 动态调度
静态调度
这涉及静态分析任务并确定它们的时序特性。这些时序特性可用于创建固定的调度表,根据该表,任务将在运行时调度执行。因此,任务的执行顺序是固定的,并且假设它们的执行时间也是固定的。
轮转调度
基于时间片轮转调度是实现静态调度的方法之一。轮转调度是最简单也是最广泛使用的调度算法之一;其中定义了一个称为时间片的小时间单位。调度器会遍历就绪进程队列,并为每个这样的进程分配一个时间片。
带优先级的调度
优先级指示分配给任务的紧急程度或重要性。基于优先级的执行调度有两种方法——当处理器空闲时,选择就绪任务中优先级最高的任务执行;一旦被选中,任务将一直运行到完成。
抢占式调度
抢占式优先级执行是指当处理器空闲时,选择就绪任务中优先级最高的任务执行;在任何时候,如果出现优先级更高的任务就绪,则可以抢占当前任务的执行。因此,在任何时候,处理器要么空闲,要么正在执行就绪任务中优先级最高的任务。
动态调度
另一种调度机制称为动态调度——在这种情况下,实时程序需要在执行资源分配给事务的过程中做出连续的决策。在这里,每个决策都必须在不知道未来任务需求的情况下做出。动态调度不在本文的讨论范围之内,因此我在此不作详细讨论。或许我们可以在另一篇文章中讨论。
代码片段
让我们以一个主从通信系统为例。主系统通过串行端口(RS 485 网络)以多点配置连接到 n 个从系统。图 1 显示了该系统的典型配置。在这里,一次只有一个系统可以通信,其他系统处于监听模式。主系统控制通信。

主程序
void main(void)
{
/* Initialise all register of processor and the peripheral devices */
InitMain();
/* Register the event handler */
RegisterTask(MainEventHandler);
RegisterTask(CheckDataIntegrity);
..............
/* Turn on all the leds for 1 sec as lamp test */
TurnOnLed(LED, 1000);
/* Call the application event manager - no return */
EventManager();
}
在上述情况下,RegisterTast()
和 EventManager()
是两个重要的函数。对于任何应用程序,我们都有许多任务,一个函数代表一个任务的入口点,例如 'CheckDataIntegrity
'。当设备收到完整数据包时,它会进行数据检查。RegisterTask()
函数创建一个函数指针的链表,其中每个节点代表一个单独的任务。这里我传递了函数指针 MainEventHandler
或 CheckDataIntegrity
作为参数。
Main.h 应包含以下行
/* Application event handler function pointer */
typedef void (*tEventHandler)(unsigned short *);
/* Link-list definition */
typedef struct TaskRecord
{
tEventHandler EventHandler;
struct TaskRecord *pNext;
}tTaskRecord;
static tTaskRecord *mpTaskList = NULL;
这里 mpTaskList
代表一个函数指针的链表。将链表的每个节点视为一个任务的入口点,在 EventManager()
函数中会逐个执行。下面是 RegisterTask()
函数的定义,它将函数指针添加到链表中。
void RegisterTask(tEventHandler EventHandlerFunc)
{
tTaskRecord *pNewTask;
/* Create a new task record */
pNewTask = malloc(sizeof(tTaskRecord));
if(pNewTask != NULL)
{
/* Assign the event handler function to the task */
pNewTask->EventHandler = EventHandlerFunc;
pNewTask->pNext = NULL;
if(mpTaskList == NULL)
{
/* Store the address of the first task in the task list */
mpTaskList = pNewTask;
}
else
{
/* Move to the last task in the list */
mpActiveTask = mpTaskList;
while(mpActiveTask->pNext != NULL)
{
mpActiveTask = mpActiveTask->pNext;
}
/* Add the new task to the end of the list */
mpActiveTask->pNext = pNewTask;
}
}
}
对于这种类型的应用程序,初始化后应该有一个无限循环以实现连续执行。main
末尾的 EventManager()
函数,它本质上是一个无限循环,始终检查活动任务或事件。如果发生任何事件,它会将该事件标志作为参数传递给已添加到 mpTaskList
的函数。因此,EventManager()
函数以 eventID
作为参数调用 MainEventHandler()
函数。MainEventHandler
将检查 eventId
并执行必要的操作或执行相应的代码。这里每个事件-处理函数都应该有唯一的事件,即两个事件-处理函数不应该检查相同的 eventID
。
EventManager 函数定义
void EventManager(void)
{
unsigned short AllEvents;
tTaskRecord pActiveTask
/* No return */
while(1)
{
/* Read application events */
AllEvents = mEventID;
/* Process any application events */
pActiveTask = mpTaskList;
while((AllEvents != 0) && (pActiveTask != NULL))
{
if(pActiveTask->EventHandler != NULL)
{
/* Call the task's event handler function */
(mpActiveTask->EventHandler)(&AllEvents);
/* Read application events */
AllEvents = mEventID;
}
/* Move to the next event handler */
pActiveTask = pActiveTask->pNext;
}
}
}
事件可以由中断服务程序生成,也可以通过轮询模式检查输入引脚的状态来生成。SerialReceiveISR
函数在接收到完整数据包后生成一个事件。由于变量 mEventID
在中断服务程序中被修改,建议在读取时禁用中断。
#pragma interrupt_level 0
void interrupt IService(void)
{
/* Receive bit is set when a byte is received */
if(Receivebit == 1)
{
SerialReceiveISR()
}
...........
/* code for other interrupt */
}
SerialReceiveISR 函数
#pragma inline SerialReceiveISR
void SerialReceiveISR(void)
{
static char RxMsgDataCount;
/* If a framing or overrun error occurs then clear the error */
if(Error == 1)
{
/* Indicate a problem was seen */
mEventID = mEventID | ATTENTION_REQ_FLG;
}
else if( RxMsgCount == DataLength)
{
/* Packet receive complete */
mEventID = mEventID | DATA_RECEIVE_COMPLETE;
}
else
{
/* Store data in memory */
Store(RxByte);
RxMsgCount++;
}
}
这里的 ATTENTION_REQ_FLAG
和 DATA_RECEIVE_COMPLETE
标志是全局变量 mEventID
的两个位,它是一个 16 位变量,每位在设置时触发相应的事件。
#define ATTENTION_REQ_FLAG 0x0008
#define DATA_RECEIVE_COMPLETE 0x0001
当标志被设置时,EventManager
将调用所有已注册的函数,并将 eventID
作为参数传递。
void MainEventHandler(unsigned short *Event)
{
if(*Event & DATA_RECEIVE_COMPLETE)
{
/* Do the corresponding action */
.........
/* Reset the flag */
*Event &= ~DATA_RECEIVE_COMPLETE;
}
}
现在我们可以更改变量类型以增加标志的数量。如果您想生成多个事件,请使用结构而不是单个变量。