2. INTEGRITY驱动模型与内核对象
INTEGRITY是Green Hills Software面向高安全、高可靠嵌入式系统设计的实时操作系统,其驱动开发模型与Linux等通用操作系统有本质区别。理解INTEGRITY的驱动分层模型以及内核对象在驱动中的角色,是编写稳定、可认证驱动的前提。本节从分层结构入手,再逐一剖析任务、信号量、队列、内存区四类内核对象在驱动中的实际作用与交互方式。
一、驱动分层模型
INTEGRITY的驱动并不直接运行在“内核态”这一单一层次上,而是按照地址空间与权限划分为多个层次。最底层是内核(Kernel)与硬件抽象层(HAL),负责中断向量、MMU/MPU配置、时钟与底层调度;其上是内核服务层,提供任务、信号量、队列、内存区等对象的系统调用接口;再往上是驱动层,通常以“内核任务”或“用户任务+内核代理”的形式存在;最上层是应用任务与文件系统、协议栈等服务。
关键点在于:INTEGRITY采用微内核思想,驱动大多运行在独立的地址空间中,通过内核提供的对象进行受控通信,而不是像Linux那样把驱动直接链接进内核镜像。这种设计带来两个直接后果:一是驱动崩溃不会直接拖垮整个系统,二是驱动与内核、驱动与驱动之间的数据交换必须显式使用内核对象,不能随意共享指针。
在典型的INTEGRITY驱动工程中,一个设备驱动通常包含三部分:初始化任务(负责注册设备、申请内存区、创建信号量与队列)、中断服务例程(ISR,运行在中断上下文,只做最少的硬件操作并向队列投递事件)、以及一个或多个驱动任务(在任务上下文中处理队列事件、访问硬件寄存器、完成数据搬运)。这三者之间的协作,正是通过内核对象串联起来的。
二、任务:驱动的执行载体
在INTEGRITY中,任务(Task)是调度的基本单位,也是驱动代码实际运行的载体。与通用操作系统不同,INTEGRITY的任务具有明确的地址空间属性:每个任务可以拥有独立的地址空间,也可以与其它任务共享地址空间。驱动开发中通常创建两类任务:一类是初始化任务,在系统启动阶段运行一次,完成设备注册、内存区申请、信号量与队列创建;另一类是常驻驱动任务,以循环方式等待队列事件或信号量,处理实际I/O。
创建任务使用Task_Create系列接口,需要指定任务入口函数、优先级、栈大小、地址空间等参数。例如,一个串口驱动任务可以这样创建:
/* 示例:创建串口驱动任务 */
ErrorCode err;
Task_Id uart_task;
err = Task_Create(
&uart_task, /* 返回任务句柄 */
"UART_DRV", /* 任务名 */
uart_driver_main, /* 入口函数 */
120, /* 优先级,数值越大优先级越高 */
8192, /* 栈大小(字节) */
NULL, /* 地址空间,NULL表示使用默认 */
0 /* 标志位 */
);
if (err != NO_ERROR) {
/* 任务创建失败,记录错误并进入安全状态 */
}
驱动任务的主循环通常长这样:先等待一个队列事件或信号量,被唤醒后读取硬件状态,处理数据,再回到等待。这里的关键是:驱动任务不能忙等,必须使用内核对象进入阻塞态,把CPU让给其它任务。
三、信号量:互斥与同步
信号量(Semaphore)在驱动中有两种典型用途。第一种是互斥:多个任务或ISR与任务之间需要访问同一硬件寄存器或同一块DMA缓冲区时,用二值信号量保护临界区。第二种是同步:ISR完成一次数据接收后释放信号量,驱动任务获取信号量后开始处理,形成“中断触发—任务处理”的同步链。
INTEGRITY的信号量接口包括Semaphore_Create、Semaphore_Obtain、Semaphore_Release。需要注意的是,ISR中不能调用可能阻塞的Semaphore_Obtain,但可以调用Semaphore_Release来唤醒等待的任务。下面是一个典型的互斥保护示例:
/* 示例:用信号量保护寄存器访问 */
Semaphore_Id reg_lock;
void uart_write_reg(uint32_t offset, uint32_t value) {
Semaphore_Obtain(reg_lock, WAIT_FOREVER);
*(volatile uint32_t *)(UART_BASE + offset) = value;
Semaphore_Release(reg_lock);
}
如果信号量创建时初始计数为1,则表现为互斥锁;如果初始计数为0,则表现为同步信号。驱动初始化阶段必须明确信号量的初始计数,否则会出现任务永久阻塞或互斥失效。
四、队列:驱动中的事件与数据通道
队列(Queue)是INTEGRITY驱动中使用频率最高的对象之一。它本质上是一个内核维护的环形缓冲区,支持多任务或ISR向队列投递消息,也支持任务从队列接收消息。队列中的元素是固定大小的消息,通常是一个结构体或一个指针。
在驱动中,队列最常见的用法是“ISR投递、任务消费”。ISR检测到硬件事件后,把事件类型、状态寄存器值、时间戳等打包成一条消息,调用Queue_Send投递到队列;驱动任务调用Queue_Receive阻塞等待,收到消息后根据事件类型执行相应处理。这样做的原因是ISR运行在中断上下文,不能执行耗时操作,也不能调用可能阻塞的服务,而队列的发送操作是非阻塞的,适合在ISR中使用。
/* 示例:ISR向队列投递事件,驱动任务消费 */
typedef struct {
uint32_t event_type;
uint32_t status;
uint64_t timestamp;
} UartEvent;
Queue_Id uart_evt_q;
/* ISR 中 */
void uart_isr(void) {
UartEvent evt;
evt.event_type = EVT_RX_READY;
evt.status = read_status_reg();
evt.timestamp = get_tick();
Queue_Send(uart_evt_q, &evt, sizeof(evt), NO_WAIT);
}
/* 驱动任务中 */
void uart_driver_main(void) {
UartEvent evt;
while (1) {
Queue_Receive(uart_evt_q, &evt, sizeof(evt), WAIT_FOREVER);
switch (evt.event_type) {
case EVT_RX_READY:
handle_rx(&evt);
break;
case EVT_TX_DONE:
handle_tx_done(&evt);
break;
default:
break;
}
}
}
队列的深度和消息大小需要在创建时确定。深度过小会导致ISR投递失败(队列满),深度过大则浪费内存。工程中通常根据最坏情况下的中断频率和处理时间来估算队列深度。
五、内存区:DMA与共享数据的安全载体
内存区(Memory Region)是INTEGRITY中用于管理物理内存的对象。驱动开发中,内存区的核心用途是申请DMA缓冲区。由于DMA控制器直接访问物理内存,缓冲区必须是物理连续的,并且需要正确设置缓存属性(如非缓存或写穿透),否则会出现数据不一致。
创建内存区使用Memory_Region_Create,需要指定大小、对齐方式、物理地址约束和缓存属性。创建成功后,驱动通过Memory_Region_Get_Address获取虚拟地址和物理地址,虚拟地址供CPU访问,物理地址写入DMA控制器的源/目的寄存器。
/* 示例:申请DMA缓冲区 */
Memory_Region_Id dma_buf;
void *virt_addr;
uint64_t phys_addr;
ErrorCode err = Memory_Region_Create(
&dma_buf,
4096, /* 大小:4KB */
4096, /* 对齐:4KB */
MEM_REGION_PHYS_CONTIG | MEM_REGION_NON_CACHED,
NULL
);
if (err == NO_ERROR) {
Memory_Region_Get_Address(dma_buf, &virt_addr, &phys_addr);
/* 将 phys_addr 写入 DMA 控制器 */
}
内存区还常用于驱动与用户任务之间的共享数据。由于INTEGRITY的地址空间隔离机制,普通指针不能跨地址空间传递,必须通过内存区对象显式共享。共享时需要注意同步问题,通常配合信号量使用。
六、四类对象的协作关系
把四类对象放在一起看,一个完整的驱动数据通路通常是这样的:初始化任务创建内存区作为DMA缓冲区,创建信号量用于寄存器互斥,创建队列用于事件传递,创建驱动任务并启动。硬件中断到来时,ISR读取状态寄存器,向队列投递事件;驱动任务从队列接收事件,获取信号量后访问寄存器或DMA缓冲区,完成数据处理后释放信号量。整个过程由内核统一调度和仲裁,驱动各组成部分之间不直接共享可变状态,而是通过内核对象进行受控交互。
Semaphore_Obtain、Queue_Receive(带等待)、Task_Delay等。ISR只能使用非阻塞的发送、释放和状态读取接口。违反这一约束会导致系统在中断上下文中挂起,属于严重设计错误。