Skip to content

ChibiOS 线程编写

liuzikai edited this page Jan 17, 2022 · 5 revisions

本文介绍如何使用 ChibiOS C++ Wrapper 创建新的线程。ChibiOS C++ 接口还较新,本文基于 Meta-Embedded 目前使用的 ChibiOS 版本,新版本的 ChibiOS 可能不同。

什么是线程

简单来说,一个 CPU 在同一时间只能运行一条指令,而代码是顺序执行的,因此,理论上来讲,CPU 只能运行一段代码,直到结束。

但在很多情况下,我们希望多段代码同时执行,例如云台的控制和底盘的控制。操作系统提供了“线程”(非嵌入式操作系统一般有“进程”和“线程”,在此不展开).

每个线程都是一段独立运行的代码,操作系统负责给每个线程分配 CPU 时间,一个线程运行了一段时间(或者更多情况是,运行结束主动让出 CPU ),操作系统会暂停它,并切换到另一个线程。

暂停对于线程本身是无感知的(当与其他线程或中断有关联时并非如此,详见 ChibiOS 关键区与锁),因此形成了多个线程同时运行的“假象”。

线程有优先级,当两个线程要同时运行时,高优先级的线程会优先运行,甚至中途打断低优先级的线程。

ChibiOS 提供两种类型的线程:Static Thread 和 Dynamic Thread,前者的线程内存空间(栈)是固定的、代码静态分配的,后者则是动态的。

STM32F4 有足够大的 RAM,一般而言,我们只需要用到 Static Thread,分配足够大的空间即可。

ChibiOS 提供了 C API 用于创建线程,在其官方教程:ChibiOS free embedded RTOS - How to create a thread 中有详细介绍。

本文则主要介绍如何使用 ChibiOS C++ API 创建线程,以实现更好的封装。官方教程依然推荐阅读,其中介绍了一些基础概念,以及空闲线程 idle thread、主线程 main thread 等。

Meta-Embedded 大量使用 class static variables (详见程序架构演进史,本文也继续沿用这种风格,将线程定义为某个 class 中的一个 static variable。

如何编写一个无限循环的的线程

首先,定义一个新类,继承 chibios_rt::BaseStaticThread。BaseStaticThread 带一个模板参数,指定 Static Thread 的栈空间大小,单位为 byte

在我们的工程中,栈空间一般只用于线程代码的局部变量,变量越多,则需要更大的栈空间。STM32F4 有足够大的 RAM,因此一般分配足够大的栈空间,栈空间不足可能导致 crash。线程剩余的栈空间大小可在运行时通过 Shell 终端查看,详见ChibiOS 性能与资源占用。在这里,我们分配 512 bytes。

线程 class 重载 main() 函数(final 关键字不是必须的)。

class GimbalSKD : public GimbalBase, public PIDControllerBase {

    // ...

    class SKDThread : public chibios_rt::BaseStaticThread<512> {
        void main() final;
    };

    static constexpr unsigned SKD_THREAD_INTERVAL = 1;  // thread interval [ms]

    static SKDThread skd_thread;
};

声明了 static variable 后,不要忘记在 cpp 文件中加上实际的 definition。

GimbalSKD::SKDThread GimbalSKD::skd_thread;

在 cpp 中,使用以下模板定义线程主体:

void GimbalSKD::SKDThread::main() {
    setName("GimbalSKD");
    while (!shouldTerminate()) {
        // Do something
        sleep(TIME_MS2I(SKD_THREAD_INTERVAL));
    }
}

可以看到,线程首先为当前线程设置了一个名字,使用 Shell 终端查看线程时会看到。

然后,线程进入一个循环。虽然我们使用 shouldTerminate() 作为退出条件,但这个条件一般都不会被触发,因此可以当作是个死循环。如果线程运行到 main() 的结尾,则线程退出。

sleep(TIME_MS2I(SKD_THREAD_INTERVAL)) 让线程在执行一段代码后暂停一段时间,sleep 的参数为系统间隔,TIME_MS2I 将毫秒转化为系统间隔,SKD_THREAD_INTERVAL 则是线程运行间隔,以毫秒为单位。

一个不断执行代码而不 sleep 的线程会占用所有的 CPU 资源,只有优先级比它高的线程可以运行。

由于执行代码也需要时间,该线程并不是严格的 1000Hz,但考虑到我们使用 120MHz 的时钟(每秒执行 120M 条指令),一般代码运行时间也几乎可以忽略。

在模块初始化处,启动线程:

void GimbalSKD::start(/* others */ tprio_t thread_prio /* others */ ) {
    // ...
    skd_thread.start(thread_prio);
}

thread_prio 是线程优先级,当两个线程要同时运行时,高优先级的线程会优先运行,甚至中途打断低优先级的线程。

如何编写一个可暂停的线程

和无限循环的线程类似,首先,定义一个新类。不同的是,在 public 域中增加一个 bool,用于记录当前线程是否启动。另外,除了线程实例以外,定义一个 chibios_rt::ThreadReference,用于记录线程状态。

class GimbalLG : public GimbalBase {

    // ...

    class VisionControlThread : public chibios_rt::BaseStaticThread<512> {
    public:
        bool started = false;
    private:
        void main() final;
        static constexpr unsigned VISION_THREAD_INTERVAL = 4;  // [ms]
    };

    static VisionControlThread vision_control_thread;
    static chibios_rt::ThreadReference vision_control_thread_reference;
    // Remember definitions in the cpp file
};

然后,使用以下基本格式,定义线程主体:

void GimbalLG::VisionControlThread::main() {
    setName("GimbalIF_Vision");

    while(!shouldTerminate()) {

        chSysLock();  /// --- ENTER S-Locked state. DO NOT use LOG, printf, non S/I-Class functions or return ---
        {
            if (action != VISION_MODE) {  // action is a static variable outside
                started = false;
                chSchGoSleepS(CH_STATE_SUSPENDED);
            }
        }
        chSysUnlock();  /// --- EXIT S-Locked state ---

        // Something else

        sleep(TIME_MS2I(VISION_THREAD_INTERVAL));
    }
}

可以看到,与无限循环的线程相比,可暂停的线程多了一段,用于暂停当前线程的代码。在线程检测到退出条件满足时,调用 chSchGoSleepS 将当前线程暂停。

在模块初始化时,启动线程并记录 reference:

void GimbalLG::init(tprio_t vision_control_thread_prio) {
    vision_control_thread.started = true;
    vision_control_thread_reference = vision_control_thread.start(vision_control_thread_prio);
}

在需要启动线程的地方,使用 chSchWakeupS 唤醒线程:

void GimbalLG::set_action(GimbalLG::action_t value) {
    
    // ...
    else if (action == VISION_MODE) {
        // Resume the thread
        chSysLock();  /// --- ENTER S-Locked state. DO NOT use LOG, printf, non S/I-Class functions or return ---
        {
            if (!vision_control_thread.started) {
                vision_control_thread.started = true;
                chSchWakeupS(vision_control_thread_reference.getInner(), 0);
            }
        }
        chSysUnlock();  /// --- EXIT S-Locked state ---
    }
}

关于 chSysLock 和 chSysUnlock 的详细说明,参见ChibiOS 关键区与锁

更新历史

  • 2021.07.09 初始版本。liuzikai
  • 2021.07.12 小幅更新。liuzikai
  • 2022.01.17 将 Meta-Infantry 修改为 Meta-Embedded。liuzikai
Clone this wiki locally