-
Notifications
You must be signed in to change notification settings - Fork 15
ChibiOS 线程编写
本文介绍如何使用 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
- 基础知识
- 基础配置
- 进阶与参考