4. SWC开发流程
好,咱们进入 SWC 开发里最核心的部分——内部行为设计。说白了,就是让软件组件真正“动起来”的那套机制。你写好了接口,画好了端口,但代码在哪儿跑?什么时候跑?怎么被唤醒?这些都得靠 Runnable Entity(可运行实体)来搞定。
我刚开始接触 AUTOSAR 时,总觉得 Runnable 就是个函数嘛,有什么好讲的?后来踩了几个坑才明白——Runnable 的触发方式、执行周期、数据一致性,这些才是真正决定系统稳定性的关键。咱们一个一个来看。
4.1 Runnable Entity 是什么?
Runnable Entity,简称 Runnable,是 SWC 内部最小的可执行单元。你可以把它理解成一个“被调度器管理的函数”。它不自己创建线程,也不自己决定什么时候跑——这些都由 RTE(运行时环境)来管。
核心要点:Runnable 是 SWC 内部行为的执行载体。每个 Runnable 对应一段 C 函数代码,由 RTE 在特定事件触发时调用。
一个 SWC 可以包含多个 Runnable。比如一个控制策略组件,可能有初始化 Runnable、周期计算 Runnable、故障处理 Runnable。它们各自独立,互不阻塞。
我记得有个项目,同事把所有逻辑塞进一个 Runnable 里,结果周期抖动大得离谱。后来拆成三个 Runnable,各管各的事,问题就解决了。所以,Runnable 的粒度设计很重要。
4.2 Runnable 的三种类型
AUTOSAR 定义了三种基本的 Runnable 类型。我习惯按“执行时机”来区分它们:
| 类型 | 触发方式 | 典型用途 | 执行次数 |
|---|---|---|---|
| Init Runnable | SWC 初始化时触发 | 变量初始化、硬件配置、状态机复位 | 1 次(生命周期内) |
| Periodic Runnable | 按固定时间周期触发 | 周期性采样、控制算法、状态更新 | 无限次(按周期) |
| OnEvent Runnable | 由特定事件触发 | 数据接收、操作调用、模式切换 | 事件发生时执行 |
4.2.1 Init Runnable
Init Runnable 只在 SWC 启动时执行一次。它的任务很明确:把组件内部状态准备好。
举个例子,你写了一个电机控制 SWC,Init Runnable 里要做的可能是:
- 清零 PID 积分项
- 设置默认目标转速
- 读取 NVM 中保存的校准参数
/* 示例:Init Runnable 的典型实现 */
FUNC(void, SWC_MOTOR_CTRL_CODE) MotorCtrl_Init(void)
{
/* 清零内部状态 */
MotorCtrl_IntegralTerm = 0U;
MotorCtrl_PreviousError = 0U;
/* 设置默认值 */
MotorCtrl_TargetSpeed = 1000U; /* 默认 1000 RPM */
/* 读取 NVM 参数(如果有) */
(void)NvM_ReadBlock(NVM_BLOCK_MOTOR_CALIB, &MotorCtrl_CalibData);
/* 初始化完成标志 */
MotorCtrl_InitDone = TRUE;
}
我的习惯:Init Runnable 里尽量只做“一次性”的事情。别把周期性的逻辑放进来,也别做耗时操作。我曾经见过有人在 Init 里做 EEPROM 写入,结果启动时间超了 200ms,被客户投诉。
4.2.2 Periodic Runnable
Periodic Runnable 是最常用的类型。它按固定时间间隔被 RTE 调用,比如每 10ms、每 100ms。适合做那些需要“定时干活”的任务。
你想想看,一个传感器采样任务,每 10ms 读一次 ADC,然后滤波、换算、输出。这就是典型的 Periodic Runnable。
/* 示例:10ms 周期的 Periodic Runnable */
FUNC(void, SWC_SENSOR_CODE) Sensor_10ms(void)
{
uint16 adcValue;
float32 filteredValue;
/* 读取 ADC 原始值 */
adcValue = Adc_ReadGroup(ADC_GROUP_TEMP_SENSOR);
/* 一阶低通滤波 */
filteredValue = (float32)adcValue * 0.1f + Sensor_PrevFiltered * 0.9f;
Sensor_PrevFiltered = filteredValue;
/* 通过 S/R 接口输出 */
Rte_Write_TempSensor_FilteredValue(filteredValue);
}
注意:Periodic Runnable 的执行时间不能超过其周期。比如 10ms 周期的 Runnable,如果执行了 12ms,就会导致任务重叠,轻则数据错乱,重则 Watchdog 复位。我遇到过最夸张的一次,一个 5ms 周期的 Runnable 里做了浮点运算和 CAN 发送,结果跑了 8ms,系统直接崩了。
4.2.3 OnEvent Runnable
OnEvent Runnable 不按固定周期跑,而是“有事才跑”。触发它的可以是数据到达、操作调用、模式切换等事件。这种 Runnable 适合做“响应式”逻辑。
举个例子,你有一个 SWC 负责接收 CAN 报文,当收到某个特定信号时,立即更新控制参数。用 OnEvent Runnable 就很合适——不用轮询,事件来了自动触发。
/* 示例:OnEvent Runnable,由 DataReceivedEvent 触发 */
FUNC(void, SWC_CONTROL_CODE) Control_OnDataReceived(void)
{
uint8 newMode;
/* 读取接收到的数据 */
newMode = Rte_Read_ControlMode_ReceivedMode();
/* 根据新模式切换控制策略 */
switch (newMode)
{
case CONTROL_MODE_NORMAL:
Control_SetNormalMode();
break;
case CONTROL_MODE_LIMP_HOME:
Control_SetLimpHomeMode();
break;
default:
/* 无效模式,保持当前状态 */
break;
}
}
4.3 Runnable 的触发事件
Runnable 不会自己跑,它需要“被触发”。AUTOSAR 定义了多种触发事件,我挑三个最常用的来讲:
4.3.1 TimingEvent
TimingEvent 就是“定时器事件”。它和 Periodic Runnable 是绑定的——你在配置里设一个周期(比如 10ms),RTE 就会每隔 10ms 触发一次这个 Runnable。
配置 TimingEvent 时,你需要指定:
- 周期值:比如 10ms、50ms、100ms
- 偏移量:可以设置第一次触发的延迟时间,用于错开多个 Runnable 的执行时刻
避坑指南:我曾经在一个项目里,把三个 10ms Runnable 的偏移量都设成 0。结果它们全挤在同一个时间片执行,CPU 利用率瞬间飙到 90%。后来给每个 Runnable 加了 1ms、2ms、3ms 的偏移,负载就均匀了。所以,偏移量不是摆设,一定要用。
4.3.2 DataReceivedEvent
DataReceivedEvent 是“数据接收事件”。当某个 Sender-Receiver 接口的数据被更新时,RTE 会自动触发绑定的 Runnable。
这种机制的好处是“按需执行”——数据没变,Runnable 就不跑,节省 CPU。适合用在:
- 传感器数据更新处理
- 控制参数变更响应
- 状态机状态切换
4.3.3 OperationInvokedEvent
OperationInvokedEvent 是“操作调用事件”。当 Client-Server 接口的某个操作被调用时,RTE 会触发对应的 Runnable 来执行这个操作。
说白了,这就是 AUTOSAR 里的“函数调用”。一个 SWC 提供 Server Runnable,另一个 SWC 通过 RTE 调用它。调用方不需要知道 Server 在哪个 ECU 上,RTE 会帮你路由。
/* Server Runnable 示例:提供加法运算 */
FUNC(sint32, SWC_MATH_CODE) Math_Add(sint32 a, sint32 b)
{
return a + b;
}
/* Client 端调用(由 RTE 生成) */
/* Rte_Call_Math_Add(3, 5) 会返回 8 */
4.4 Runnable 内部逻辑实现
讲完了触发机制,咱们聊聊 Runnable 里面怎么写代码。嗯,这里要注意几个原则:
4.4.1 数据一致性保护
Runnable 之间可能共享数据。比如一个 Runnable 写数据,另一个 Runnable 读数据。如果它们被不同的任务调度,就可能出现数据不一致。
AUTOSAR 提供了两种保护机制:
- Explicit Access(显式访问):通过 Rte_Read/Rte_Write 接口,RTE 自动加锁
- Implicit Access(隐式访问):Runnable 执行开始时快照数据,结束时统一写回
我的建议:能用 Implicit 就用 Implicit。它把数据一致性问题交给 RTE 处理,你只管写业务逻辑。Explicit 虽然灵活,但容易漏掉锁保护,我见过好几次因为忘记加锁导致的数据竞争 bug。
4.4.2 执行时间控制
每个 Runnable 的执行时间都要可控。别在 Runnable 里做:
- 长循环(比如 while(1) 或 for 循环几万次)
- 阻塞等待(比如等待某个标志位被置位)
- 动态内存分配(malloc/free)
- 文件 I/O 或网络操作
这些操作会破坏实时性。如果确实需要,考虑拆成状态机,分多次执行。
4.4.3 状态机实现
很多 Runnable 的逻辑适合用状态机来表达。比如一个启动流程:
/* 状态机示例:启动控制 */
FUNC(void, SWC_STARTUP_CODE) Startup_10ms(void)
{
switch (Startup_State)
{
case STARTUP_IDLE:
/* 等待启动命令 */
if (Rte_Read_StartCmd() == TRUE)
{
Startup_State = STARTUP_PRECHARGE;
Startup_Timer = 0U;
}
break;
case STARTUP_PRECHARGE:
/* 预充电,持续 100ms */
Startup_Timer += 10U;
if (Startup_Timer >= 100U)
{
Startup_State = STARTUP_MAIN_CONTACTOR;
}
break;
case STARTUP_MAIN_CONTACTOR:
/* 闭合主接触器 */
Rte_Write_MainContactorCmd(TRUE);
Startup_State = STARTUP_RUNNING;
break;
case STARTUP_RUNNING:
/* 正常运行,不做特殊处理 */
break;
default:
/* 异常状态,复位 */
Startup_State = STARTUP_IDLE;
break;
}
}
4.5 知识结构总览
为了让你更直观地理解 Runnable 的整体设计,我画了一张结构图:
4.6 小结
Runnable Entity 是 SWC 开发的基石。你只要记住三点:
- 选对类型:Init 做一次性初始化,Periodic 做定时任务,OnEvent 做响应式处理
- 配好触发:TimingEvent 定周期,DataReceivedEvent 按需触发,OperationInvokedEvent 实现服务调用
- 写好内部逻辑:注意数据一致性,控制执行时间,多用状态机