3. RTA-OS工程结构解析

在把 RTA-OS 集成进一个 ECU 项目时,最先遇到的并不是 API 怎么调用,而是“文件该放哪、哪些能改、哪些一改就被覆盖”。RTA-OS 的工程结构本质上是一条从配置到生成、再到编译链接的单向流水线:工程师只维护少量输入文件,其余大量 C 代码、头文件和汇编桩由生成器产出。理解这条边界,是后续所有集成工作的前提。

一、工程目录组织

RTA-OS 本身以“安装目录 + 工程目录”两层存在。安装目录(例如 C:\ETAS\RTA-OS3.x)存放编译器适配、目标端口、模板和工具链,属于只读资源;工程目录才是每个 ECU 项目独立维护的部分。一个典型的工程目录组织如下:

MyEcuProject/
├── config/
│   ├── MyEcu.oil              # OIL 配置输入(手写)
│   └── MyEcu.arxml            # 可选:ARXML 导入源
├── generated/
│   ├── Os_Cfg.c / Os_Cfg.h    # 生成:配置数据
│   ├── Os_Lcfg.c / Os_Lcfg.h  # 生成:链接期配置
│   ├── Os_Callout.c           # 生成:回调桩(可被用户实现覆盖)
│   └── Os_MemMap.h            # 生成:内存段映射
├── src/
│   ├── Os_Callout_User.c      # 手写:回调实现
│   ├── Os_App_Task.c          # 手写:任务体
│   └── Os_Hook_Error.c        # 手写:错误钩子
├── build/
│   ├── Makefile               # 构建脚本
│   └── rtaos.mk               # 生成器调用规则
└── out/
    └── MyEcu.elf              # 链接产物

关键约定是:config/ 与 src/ 属于“人写的”,generated/ 属于“机器写的”,out/ 属于“可丢弃的”。把生成目录纳入版本管理通常没有必要,但把生成目录与手写目录混放,会导致清理构建时误删手写代码,这是最常见的工程事故之一。

二、配置文件类型

RTA-OS 的配置输入主要有三类。第一类是 OIL 文件(OSEK Implementation Language),它是文本形式的声明式配置,描述 OS 对象及其属性,例如:

CPU MyCpu {
  OS MyOs {
    STATUS = EXTENDED;
    STARTUPHOOK = TRUE;
    ERRORHOOK = TRUE;
    PRETASKHOOK = FALSE;
  };
  TASK Task_10ms {
    PRIORITY = 10;
    ACTIVATION = 1;
    SCHEDULE = FULL;
    AUTOSTART = FALSE;
  };
  ALARM Alarm_10ms {
    COUNTER = SystemCounter;
    ACTION = ACTIVATETASK { TASK = Task_10ms; };
  };
  COUNTER SystemCounter {
    MINCYCLE = 1;
    MAXALLOWEDVALUE = 65535;
    TICKSPERBASE = 1;
  };
};

第二类是 ARXML 文件,用于从 AUTOSAR 系统描述中导入 OS 相关配置,适合与上层 ECU 配置工具链对接。第三类是 生成器选项文件(如 .rtaos 或命令行参数),控制目标编译器、内存模型、是否生成调试信息等。OIL 决定“系统里有什么”,选项文件决定“生成成什么样”,两者职责不能混淆。

三、生成代码与手写代码的边界

RTA-OS 生成器读取 OIL 后,会产出配置数据、链接期常量、回调桩和内存映射。这些文件顶部通常带有“generated, do not edit”标记。边界规则可以概括为:凡是生成器能推导出来的,都不要手写;凡是生成器留出接口的,才由用户实现。

以回调为例,OIL 中声明 ERRORHOOK = TRUE 后,生成器会在 Os_Callout.c 中给出弱符号桩,用户应在 src/Os_Hook_Error.c 中提供强符号实现:

/* src/Os_Hook_Error.c —— 手写,覆盖生成桩 */
#include "Os.h"

FUNC(void, OS_CODE) ErrorHook(StatusType Error)
{
    /* 记录错误码,点亮故障灯,进入安全状态 */
    (void)Error;
    Dem_SetEventStatus(DEM_EVENT_OS_ERROR, DEM_EVENT_STATUS_FAILED);
}

如果误把实现写进 generated/Os_Callout.c,下一次重新生成就会被覆盖,且编译可能因重复定义失败。判断某个文件能否修改,最可靠的方法是看它是否位于生成目录、是否带生成标记、是否在构建脚本中被列为生成目标。

RTA-OS 工程数据流与代码边界 手写输入区(可编辑) MyEcu.oil(OS 对象声明) MyEcu.arxml(可选导入) Os_Callout_User.c / 任务体 RTA-OS 生成器 rtaosgen / 构建脚本调用 读取 OIL + 选项文件 生成输出区(只读) Os_Cfg.c / Os_Cfg.h Os_Lcfg.c / Os_Lcfg.h Os_Callout.c(弱桩) 输入 产出 用户实现覆盖弱桩(强符号) 构建脚本组成(Makefile 关键规则) 1. 变量定义 RTAOS_DIR / CC / OIL CFLAGS / INCLUDE 2. 生成规则 OIL → generated/*.c 依赖 OIL 时间戳 3. 编译规则 src + generated → .o 统一包含路径 4. 链接与清理 .o + OS 库 → .elf clean 只删 out/generated

四、构建脚本的组成

构建脚本把上述目录和文件串成可重复执行的流水线。一个最小可用的 Makefile 通常包含四段:变量定义、生成规则、编译规则、链接与清理。下面给出与上图对应的示例片段:

RTAOS_DIR := C:/ETAS/RTA-OS3.x
CC        := tricore-gcc
OIL       := config/MyEcu.oil
GEN_DIR   := generated
SRC_DIR   := src
OUT_DIR   := out

INCLUDES  := -I$(GEN_DIR) -I$(RTAOS_DIR)/include -I$(RTAOS_DIR)/target/tc2xx

# 1) 生成规则:OIL 变化才重新生成
$(GEN_DIR)/Os_Cfg.c: $(OIL)
	rtaosgen -o $(GEN_DIR) -t tc2xx $(OIL)

# 2) 编译规则:手写与生成代码统一编译
$(OUT_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(INCLUDES) -c $< -o $@

$(OUT_DIR)/%.o: $(GEN_DIR)/%.c
	$(CC) $(INCLUDES) -c $< -o $@

# 3) 链接
$(OUT_DIR)/MyEcu.elf: $(OBJS)
	$(CC) $(OBJS) -L$(RTAOS_DIR)/lib -lrtaos_tc2xx -o $@

# 4) 清理:只删产物,不碰手写源码
clean:
	rm -rf $(OUT_DIR) $(GEN_DIR)

这里有两个容易踩的坑。第一,生成规则必须把 OIL 作为依赖,否则改了配置却不会重新生成,现象是“配置明明改了,行为没变”。第二,clean 只能删除 out/ 和 generated/,绝不能把 src/ 或 config/ 纳入删除范围。建议在脚本中显式列出生成文件清单,而不是用通配符删除整个工程目录。

判断一个文件属于哪一侧,可以问三个问题:它是否由 OIL 推导而来?它是否带“do not edit”标记?重新生成后它是否会被覆盖?三个答案都是“是”,就属于生成侧,任何修改都应转移到手写侧的回调或配置中完成。
不要为了“省事”直接修改 generated/Os_Cfg.c 中的优先级或激活次数。这些值来自 OIL,下次生成即被覆盖,且会造成配置与代码不一致,排查时极难定位。正确做法是回到 OIL 修改并重新生成。

把目录、配置、代码边界和构建脚本四者对齐之后,RTA-OS 的集成就从“一堆看不懂的文件”变成了“输入—生成—编译—链接”的可控流程。后续无论是增加任务、调整调度还是接入诊断,都可以沿着这条链路定位改动点:改 OIL、重新生成、编译链接、观察运行结果。