1. VxWorks时间管理概述:系统时钟与Tick机制、时间片轮转调度、时间管理核心API介绍

各位同学,咱们今天聊聊VxWorks的时间管理。说实话,时间管理在嵌入式系统里是个老生常谈的话题,但也是最容易出问题的地方。我见过不少项目,功能逻辑写得漂漂亮亮,一上电跑起来就乱套——十有八九是时间管理没搞明白。

好,咱们直接进入正题。

1.1 系统时钟与Tick机制

VxWorks里最核心的时间概念是什么?是Tick。说白了,Tick就是系统的心跳。你想想看,一个操作系统要调度任务、要延时、要超时检测,总得有个统一的节拍器吧?这个节拍器就是系统时钟中断。

系统时钟每产生一次中断,我们就说走过了一个Tick。默认情况下,VxWorks的Tick频率是60Hz,也就是每秒60次中断。嗯,这里要注意——这个值是可以改的,我后面会讲。

核心概念:Tick是VxWorks时间管理的最小时间单位。所有与时间相关的操作,最终都会落到Tick上。

我在项目中遇到过这样一个坑:有个同事把Tick频率设成了1000Hz,觉得这样精度高。结果系统负载直接飙升,任务切换频繁到CPU都快被吃光了。为什么?因为每秒1000次中断,每次中断都要保存上下文、调度任务,开销太大了。

所以Tick频率怎么选?我个人习惯是:

  • 对实时性要求不高(比如数据采集、状态监控):用默认60Hz就够了
  • 需要较高精度(比如电机控制、PWM生成):可以提高到100-200Hz
  • 千万别超过500Hz,除非你的CPU非常强劲

1.2 时间片轮转调度

讲完Tick,咱们聊聊时间片轮转调度。这个机制其实很简单:多个同优先级的任务,每个任务轮流跑一小段时间,时间到了就换下一个。

这个「一小段时间」就是时间片。默认情况下,VxWorks的时间片长度是1个Tick。也就是说,如果Tick频率是60Hz,每个任务最多跑16.67毫秒就会被切换出去。

你可能会问:「那如果一个任务没跑完怎么办?」好问题。时间片轮转调度是抢占式的——时间一到,系统强制切换。任务下次再获得CPU时,从断点继续执行。

小技巧:我建议你在设计任务时,尽量让每个任务在一个时间片内完成主要工作。如果任务耗时太长,考虑拆分成多个小任务,或者用状态机实现。

我曾经接手过一个项目,里面有个任务要处理大量数据,每次运行都要几十毫秒。结果其他同优先级的任务根本抢不到CPU,系统响应慢得像蜗牛。后来我把那个任务拆成了几个阶段,每个阶段控制在5毫秒以内,问题就解决了。

时间片轮转调度的配置也很简单:

/* 开启时间片轮转调度 */
kernelTimeSlice(1);  /* 参数是Tick数,1表示每个任务跑1个Tick */

/* 也可以设置更长时间片 */
kernelTimeSlice(5);  /* 每个任务跑5个Tick */

嗯,这里要注意:kernelTimeSlice()的参数是Tick数,不是毫秒数。如果你想让每个任务跑10毫秒,而Tick频率是100Hz(每个Tick 10毫秒),那就传1。

1.3 时间管理核心API介绍

好了,前面铺垫了这么多,咱们来看看VxWorks到底提供了哪些时间管理API。我挑几个最常用的讲,这些你在实际项目中几乎天天都会用到。

1.3.1 获取系统时间

/* 获取系统启动以来的Tick数 */
ULONG tickGet(void);

/* 获取系统启动以来的秒数 */
ULONG sysClkRateGet(void);  /* 获取Tick频率 */
ULONG tickGet() / sysClkRateGet();  /* 手动计算秒数 */

我个人习惯用tickGet()来做性能测量。比如测量某段代码的执行时间:

ULONG startTick, endTick;
startTick = tickGet();
/* 执行要测量的代码 */
endTick = tickGet();
printf("执行耗时: %d Ticks\n", endTick - startTick);

注意:tickGet()返回的是32位无符号整数。如果系统运行时间很长,Tick数可能会溢出。比如60Hz频率下,大约828天才溢出一次。一般项目不用担心,但如果你做的是长期运行的系统,建议用tick64Get()

1.3.2 任务延时

任务延时是最常用的功能。VxWorks提供了两种方式:

/* 延时指定Tick数 */
STATUS taskDelay(ULONG ticks);

/* 延时指定秒数 */
STATUS taskDelay(sysClkRateGet() * seconds);  /* 手动转换 */

举个例子:

/* 延时100个Tick */
taskDelay(100);

/* 延时2秒(假设Tick频率60Hz) */
taskDelay(120);  /* 2 * 60 = 120 */

你可能会问:「为什么不用毫秒级的延时?」好问题。VxWorks确实提供了taskDelay()的毫秒版本——taskDelayMs(),但底层还是转换成Tick来实现的。所以精度受Tick频率限制。

避坑指南:我曾经在项目里用taskDelay(1)来实现「短暂等待」,结果发现实际等待时间远大于1个Tick。为什么?因为taskDelay(0)才是让出CPU但不等待,taskDelay(1)至少等待1个Tick。如果你想让任务立即让出CPU,用taskDelay(0)

1.3.3 超时等待

很多同步机制都支持超时等待。比如信号量:

/* 等待信号量,最多等100个Tick */
semTake(semId, 100);

/* 等待信号量,最多等500毫秒 */
semTake(semId, WAIT_FOREVER);  /* 无限等待 */

超时参数的单位也是Tick。我建议你养成一个好习惯:永远不要用WAIT_FOREVER。为什么?因为一旦信号量永远不来,你的任务就死锁了。我在项目中吃过这个亏,一个任务等另一个任务释放资源,结果对方挂了,这边就永远卡死了。

所以我的做法是:

/* 设置一个合理的超时时间 */
if (semTake(semId, 100) == OK) {
    /* 成功获取信号量 */
} else {
    /* 超时处理,比如重试或报错 */
    printf("等待信号量超时!\n");
}

1.3.4 系统时钟控制

最后,咱们看看怎么控制系统时钟:

/* 获取当前Tick频率 */
ULONG rate = sysClkRateGet();

/* 设置Tick频率(需要管理员权限) */
sysClkRateSet(100);  /* 设置为100Hz */

警告:修改Tick频率会影响所有与时间相关的操作。比如之前用taskDelay(60)延时1秒,改成100Hz后同样的代码只延时0.6秒。所以改频率前,一定要检查所有延时和超时参数。

我个人建议:项目初期就确定好Tick频率,后面尽量不改。如果实在要改,写个全局宏定义:

#define TICK_RATE   100   /* 系统Tick频率 */
#define MS_TO_TICKS(ms)  ((ms) * TICK_RATE / 1000)
#define SEC_TO_TICKS(s)  ((s) * TICK_RATE)

/* 使用宏来延时 */
taskDelay(MS_TO_TICKS(500));  /* 延时500毫秒 */

这样改频率时,只需要改TICK_RATE一个地方,所有延时自动适配。

小结

好了,这一章的内容就这些。咱们回顾一下:

  • Tick机制是VxWorks时间管理的基础,频率选择要权衡精度和开销
  • 时间片轮转调度让同优先级任务公平共享CPU,但要注意任务设计
  • 核心API包括tickGet()taskDelay()semTake()等,使用时注意单位是Tick

下一章咱们会深入讲定时器的使用,包括看门狗定时器和POSIX定时器。到时候我会分享一些实际项目中的定时器设计模式,保证让你少踩坑。

好,今天就到这里。有问题随时问我。