本文翻译自:http://www.playembedded.org/blog/demos-chibios-stm32/
发表于 2017年8月24日 更新了 2018年6月25日
在本文中,我们将深入了解ChibiOS默认演示,解释它们的工作原理。我们还将了解如何创建新项目以及如何修改它以创建我们自己的应用程序。
STM32 Nucleo F401默认演示的解剖结构
ChibiOS默认演示通常由一些不同的文件夹和文件组成。例如,在图中我们可以看到STM32 Nucleo F401RE的默认演示的资源。一般来说,所有ChibiOS的项目都具有相似的解剖结构。
所有演示都有一些文件夹,一些配置头,一个名为main.c的源文件和一个makefile。关于演示的附加说明通常在“自述”文件中报告,该文件实际上是纯文本文件(readme.txt)。
链接文件夹
我们项目中包含的一些文件夹实际上是链接。您可以注意到它们,因为左下角的图标上有一个小箭头。
这些文件夹通常在不同的演示和项目之间共享。这意味着在这些文件夹中编辑文件不仅会影响项目。一般来说:
编辑链接文件夹及其内容不是一个好主意。
让我们举一个例子来澄清这个概念。该操作系统文件夹链接到文件夹C:\ ChibiStudio \ chibios182 \ OS。 在我们的例子中,它链接到ChibiOS182,因为在上一篇文章(在STM32上开发:介绍ChibiStudio)中,我们从此版本导入了演示。无论如何,它包含ChibiOS的来源:如果我们编辑这段代码,我们可能会破坏代码,因此,每个项目都链接到此文件夹。
如果你破坏了ChibiOS意外编辑其代码,你总是可以恢复它从Sourceforge下载最新的版本并手动用新的文件夹替换损坏的文件夹,注意保留文件夹名称及其层次结构。
调试文件夹
在调试文件夹,而不是一个真正的文件夹。它与我们的项目特别相关,因为它包含一个.launch文件。我们之前已经使用过这个文件:你还记得flash和运行所需的步骤吗?刚刚启动OpenOCD后,我们必须从 Debug菜单中选择Launch Configuration。此.launch文件代表启动配置。
在 启动配置包含从调试器所需的闪光和运行过程的一些信息。它实际上是一个包含在debug文件夹中的XML文件。
配置标头
每个项目都带有一些配置标头。这些文件中包含的所有配置实际上都是预处理器指令(一系列常量定义和宏)。要对这些配置进行操作,我们通常必须编辑这些文件,保存它们并重新编译整个项目。
使用嵌入式系统时,处理文本配置标头非常常见。从这里到现在,我们经常谈论预处理器交换机。switch只是一个布尔常量定义,它包含/排除编译过程中的一段代码。例如,下面是两个开关:第一个开启,包括与串行驱动器相关的所有代码,第二个被禁用,并排除与PWM驱动器相关的所有代码。
/** * @brief Enables the SERIAL subsystem. */ #if !defined(HAL_USE_SERIAL) || defined(__DOXYGEN__) #define HAL_USE_SERIAL TRUE #endif
/** * @brief Enables the PWM subsystem. */ #if !defined(HAL_USE_PWM) || defined(__DOXYGEN__) #define HAL_USE_PWM FALSE #endif |
这些开关与HAL驱动器相关,通常称为驱动器开关。通常,预处理器开关非常重要,因为它们允许在我们不需要它们时从我们的二进制文件中删除整个子模块。
如果我们的目标是创建基于嵌入式系统的可销售产品,我们需要关注资源使用。如果我们的嵌入式软件使用更少的资源,我们可以使用最小的MCU,这意味着我们的产品将需要更小的存储器,更低的时钟频率,并且最有可能需要更少的能量:这意味着它将更便宜,更小,更薄,因此更多竞争的。
使用的资源越少,硬件就越便宜。更便宜的硬件意味着更低的最终价格和更高的利润 该软件是这一理念的关键点!
ChibiOS提供了许多机制来保持代码灵活和面向性能,但没有神奇的子弹:用户应用程序应正确设计以确保优化。
内核配置头:chconf.h
该chconf.h主要含有设置与内核:有相关的系统定时器和内核参数,很多交换机启用内核的功能和一些有趣的钩子一些设置。此文件配置为包含一般使用情况。除了调试功能外,我们很少编辑它,特别是在这些基本教程中。
请注意,即使ChibiOS / RT和ChibiOS / NIL的内核配置头以相同方式命名,配置也会有很大差异。
ChibiOS / RT具有一系列调试功能,这些功能在开发阶段非常有用。默认情况下,这些功能被禁用,因为当启用内核执行大量额外检查时:这会导致性能降低,以换取调试辅助工具。通常,在部署二进制文件之前,应在需要时启用这些功能并禁用它们。我们稍后会深入研究这一部分。
这里报告了一段chconf.h,其中包含一系列与调试选项相关的开关。
/*===========================================================================*/ /** * @name Debug options * @{ */ /*===========================================================================*/
/** * @brief Debug option, kernel statistics. * * @note The default is @p FALSE. */ #define CH_DBG_STATISTICS FALSE
/** * @brief Debug option, system state check. * @details If enabled the correct call protocol for system APIs is checked * at runtime. * * @note The default is @p FALSE. */ #define CH_DBG_SYSTEM_STATE_CHECK FALSE
/** * @brief Debug option, parameters checks. * @details If enabled then the checks on the API functions input * parameters are activated. * * @note The default is @p FALSE. */ #define CH_DBG_ENABLE_CHECKS FALSE
/** * @brief Debug option, consistency checks. * @details If enabled then all the assertions in the kernel code are * activated. This includes consistency checks inside the kernel, * runtime anomalies and port-defined checks. * * @note The default is @p FALSE. */ #define CH_DBG_ENABLE_ASSERTS FALSE
/** * @brief Debug option, trace buffer. * @details If enabled then the trace buffer is activated. * * @note The default is @p CH_DBG_TRACE_MASK_DISABLED. */ #define CH_DBG_TRACE_MASK CH_DBG_TRACE_MASK_DISABLED
/** * @brief Trace buffer entries. * @note The trace buffer is only allocated if @p CH_DBG_TRACE_MASK is * different from @p CH_DBG_TRACE_MASK_DISABLED. */ #define CH_DBG_TRACE_BUFFER_SIZE 128
/** * @brief Debug option, stack checks. * @details If enabled then a runtime stack check is performed. * * @note The default is @p FALSE. * @note The stack check is performed in a architecture/port dependent way. * It may not be implemented or some ports. * @note The default failure mode is to halt the system with the global * @p panic_msg variable set to @p NULL. */ #define CH_DBG_ENABLE_STACK_CHECK FALSE
/** * @brief Debug option, stacks initialization. * @details If enabled then the threads working area is filled with a byte * value when a thread is created. This can be useful for the * runtime measurement of the used stack. * * @note The default is @p FALSE. */ #define CH_DBG_FILL_THREADS FALSE
/** * @brief Debug option, threads profiling. * @details If enabled then a field is added to the @p thread_t structure that * counts the system ticks occurred while executing the thread. * * @note The default is @p FALSE. * @note This debug option is not currently compatible with the * tickless mode. */ #define CH_DBG_THREADS_PROFILING FALSE
/** @} */ |
HAL配置头:halconf.h
此halconf.h包含与ChibiOS / HAL驱动程序相关的所有配置。根据此文件,可以启用/禁用整个驱动程序:这通常会减小最终固件的大小,从而减小闪存占用的大小。
在部署固件之前,请记住在halconf.h中禁用未使用的驱动程序。
我们经常会在halconf.h上执行启用/禁用驱动程序。请注意,禁用实际使用构建过程的驱动程序将失败并出现大量错误:这是因为当驱动程序被禁用时,它将完全从编译中排除,并且其所有API都将变为不可用。
接下来是一段halconf.h。在此示例中,启用PAL驱动程序和CAN驱动程序 ,禁用ADC驱动程序 和DAC驱动程序。
/** * @brief Enables the PAL subsystem. */ #if !defined(HAL_USE_PAL) || defined(__DOXYGEN__) #define HAL_USE_PAL TRUE #endif
/** * @brief Enables the ADC subsystem. */ #if !defined(HAL_USE_ADC) || defined(__DOXYGEN__) #define HAL_USE_ADC FALSE #endif
/** * @brief Enables the CAN subsystem. */ #if !defined(HAL_USE_CAN) || defined(__DOXYGEN__) #define HAL_USE_CAN TRUE #endif
/** * @brief Enables the DAC subsystem. */ #if !defined(HAL_USE_DAC) || defined(__DOXYGEN__) #define HAL_USE_DAC FALSE #endif |
MCU配置头:mcuconf.h
MCU配置头包含与我们的MCU严格相关的所有配置。此文件非常依赖于硬件:属于同一子系列的STM32通常具有不同的mcuconf.h。
该文件包含整个时钟树的配置,以及与DMA,IRQ优先级,外设分配等相关的一系列配置。
我们经常采取外围任务:ChibiOS / HAL驱动程序依赖于硬件外设,而且通常不同的驱动程序在同一类硬件上进行中继,因此
要使用ChibiOS / HAL驱动程序,通常我们必须在halconf.h中启用它并在mcuconf.h中为其分配硬件外围设备
如果这个概念现在还不完全清楚,请不要担心:我们将在稍后的时刻澄清这些陈述,我们将正确介绍ChibiOS / HAL驱动程序。
makefile
我们已经介绍了makefile:它是一个脚本文件,其中包含GCC make用于构建代码的一系列指令。它的语法完全记录在GNU make手册中。出于我们的目的,我们将仅部分编辑它,所以不要太担心:我们将在适当的时候面对这个问题。
main.c
我们项目中最重要的部分绝对是main.c源文件。在此源文件中有应用程序入口点。我们将把大部分开发阶段都花在这个文件上。
只需注意,main.c文件包含一个同名函数int main()。在下文中,我将整个文件称为main.c,并将包含的函数称为main()。
在上一篇文章中,我们已经对STM32的ChibiOS演示进行了一瞥。我们使用了 STM32 Nucleo F401RE 和 STM32F3 Discovery套件,以及RT-STM32F401RE-NUCLEO64 和 RT-STM32F303-DISCOVERY等相关项目 。在闪存和运行之后,我们恢复了代码执行,并且一个或多个LED开始在我们的开发板上闪烁。
实际上,默认演示能够做的不仅仅是闪烁LED。实际上,该演示还执行了一个在串行端口上打印数据的测试套件。我们可以试试这个演示重新启动演示。简要步骤是:
让我们来看看我们使用STM32 Nucleo F401RE的这个简短视频:
简要说明
我们在上一个视频中看到的是ChibiOS测试套件,它被执行并通过虚拟COM端口打印出结果:这是可能的,因为ST-Link V2-1将STM32的一个UART桥接到PC模拟通过USB的COM端口:这称为 STLink虚拟COM端口。
在本系列的第一篇文章中,我们已经看到STM32配备了许多外围设备。一种非常常见的外设是通用同步/异步接收器/发送器,也称为 USART,一种能够实现称为RS232的标准串行协议通信的外设。该标准主要用于旧计算机,通常称为COM端口。
由于桥接器,它还可以在终端和STM32之间交换字符。在后面的文章中,我们将看到如何使用此机制在终端上打印转义字符串,就像我们通常使用C中的printf函数一样。
关于多线程的东西
该演示显然在同一时刻执行两个不同的任务:
使用RTOS可以轻松实现对显然同时执行的更多任务的管理。ChibiOS / RT最重要的特性之一是多线程。过度简化,线程可以被想象成一系列指令和多线程意味着内核可以独立管理多个指令序列,即使实际上内核在单个核心处理器上运行,也可以以并行方式执行它们。这是可能的,因为RTOS计划一个操作序列:此任务通常称为调度。
在ChibiOS / RT中,线程实际上是一个函数。启动时,线程与优先级 和工作区域相关联 。工作区域是专用于我们线程的一块内存,优先级是我们可以用来影响调度的相对数字。优先顺序遵循一条简单规则
在准备执行的所有线程中,具有最高优先级的线程是正在执行的线程,此规则没有例外。
如果您对此主题感兴趣,我还写了一篇关于ChibiOS / RT多线程的 详细文章。如果我们正在设计面向性能的复杂应用程序,那么理解ChibiOS / RT调度的工作原理就很重要。无论如何,在这个级别,我们的MCU未得到充分利用,我们几乎可以忽略这些细节,我们可以选择应用这个简单规则的优先级:
每个线程必须在其循环内具有睡眠或挂起功能。以这种方式,CPU所有权在线程之间切换,并且调度可以继续。
ChibiOS / RT提供不同的睡眠功能,但在许多情况下都适合
chThdSleepMilliseconds(<xxx>); |
在下文中我们将参考演示 RT-STM32F401RE-NUCLEO64, 但以下概念适用于STM32的每个演示,作为提示,可在文件夹\ demos \ STM32下找到。
浏览main.c我们可以很容易地看到它由两个主要部分组成:
闪光线程
此函数将线程挂起
/* * Green LED blinker thread, times are in milliseconds. */ static THD_WORKING_AREA(waThread1, 128); static THD_FUNCTION(Thread1, arg) {
(void)arg; chRegSetThreadName("blinker"); while (true) { palClearPad(GPIOA, GPIOA_LED_GREEN); chThdSleepMilliseconds(500); palSetPad(GPIOA, GPIOA_LED_GREEN); chThdSleepMilliseconds(500); } } |
在这种情况下,我们声明一个名为waThread1的工作区, 其大小为 128字节。我们还在其循环中声明了一个名为Thread1的函数:
注意这个功能
chRegSetThreadName("blinker"); |
在循环之前只执行一次并具有调试目的:使用此函数,线程为自己指定一个标识符。我们稍后会加深这一点。另请注意,这些行只是声明了空格内存和函数。简单来说,这段代码不足以创建一个线程。正如我们将看到线程创建发生在main()中。
简而言之,我们需要知道:
static THD_WORKING_AREA(<name>, <size>); |
分配一个工作区,其中
static THD_FUNCTION(<name>, <args>) { <code> } |
声明一个函数,其中是
它的实现。
主要的
在线程函数声明的正下方有main(),它是应用程序入口点:我们可以将其视为代码执行的起点。
/* * Application entry point. */ int main(void) { /* * System initializations. * - HAL initialization, this also initializes the configured device drivers * and performs the board-specific initializations. * - Kernel initialization, the main() function becomes a thread and the * RTOS is active. */ halInit(); chSysInit();
...
} |
当应用程序启动时,两个函数(halInit()和chSysInit())执行系统初始化:虽然 halInit()是ChibiOS / HAL的API并初始化HAL子系统,但 chSysInit()是ChibiOS / RT的API并初始化内核。请注意,基于ChibiOS / RT和ChibiOS / HAL的每个应用程序都以相同的方式开始。
在chSysInit()之后,main()成为一个线程本身,另一个名为idle的线程被创建:idle是一个虚拟线程,当所有其他线程都没有准备好运行时,它运行。
重要的是这些halInit()和chSysInit()在main()的开头执行。在使用之前使用任何ChibiOS API都会产生不必要的行为,可能会导致系统崩溃。
考虑到 RT-STM32F401RE-NUCLEO64 演示,在初始化之后我们有:
/* * Activates the serial driver 2 using the driver default configuration. */ sdStart(&SD2, NULL);
/* * Creates the blinker thread. */ chThdCreateStatic(waThread1, sizeof(waThread1), NORMALPRIO + 1, Thread1, NULL);
/* * Normal main() thread activity, in this demo it does nothing except * sleeping in a loop and check the button state. */ while (true) { if (!palReadPad(GPIOC, GPIOC_BUTTON)) test_execute((BaseSequentialStream *)&SD2); chThdSleepMilliseconds(500); } |
这段代码:
我们可以使用API chThdCreateStatic()在main中创建blinker线程
chThdCreateStatic(waThread1, sizeof(waThread1), NORMALPRIO + 1, Thread1, NULL); |
此API通常使用,需要5个参数:
执行此代码后,我们的内核必须管理三个线程:main,Thread1 和idle。
比较两个不同的演示
为了结束这段代码,让我们来看看演示RT-STM32F303-DISCOVERY的代码
#include "ch.h" #include "hal.h" #include "ch_test.h"
/* * Blinker thread #1. */ THD_WORKING_AREA(waThread1, 128); THD_FUNCTION(Thread1, arg) {
(void)arg;
chRegSetThreadName("blinker 1"); while (true) { palToggleLine(LINE_LED3_RED); chThdSleepMilliseconds(100); palToggleLine(LINE_LED7_GREEN); chThdSleepMilliseconds(100); palToggleLine(LINE_LED10_RED); chThdSleepMilliseconds(100); palToggleLine(LINE_LED6_GREEN); chThdSleepMilliseconds(100); } }
/* * Blinker thread #2. */ THD_WORKING_AREA(waThread2, 128); THD_FUNCTION(Thread2, arg) {
(void)arg;
chRegSetThreadName("blinker 2"); while (true) { chThdSleepMilliseconds(50); palToggleLine(LINE_LED5_ORANGE); chThdSleepMilliseconds(100); palToggleLine(LINE_LED9_BLUE); chThdSleepMilliseconds(100); palToggleLine(LINE_LED8_ORANGE); chThdSleepMilliseconds(100); palToggleLine(LINE_LED4_BLUE); chThdSleepMilliseconds(50); } }
/* * Application entry point. */ int main(void) {
/* * System initializations. * - HAL initialization, this also initializes the configured device drivers * and performs the board-specific initializations. * - Kernel initialization, the main() function becomes a thread and the * RTOS is active. */ halInit(); chSysInit();
/* * Activates the serial driver 1 using the driver default configuration. */ sdStart(&SD1, NULL);
/* * Creates the example threads. */ chThdCreateStatic(waThread1, sizeof(waThread1), NORMALPRIO+1, Thread1, NULL); chThdCreateStatic(waThread2, sizeof(waThread2), NORMALPRIO+1, Thread2, NULL);
/* * Normal main() thread activity, in this demo it does nothing except * sleeping in a loop and check the button state, when the button is * pressed the test procedure is launched. */ while (true) { if (palReadPad(GPIOA, GPIOA_BUTTON)) test_execute((BaseSequentialStream *)&SD1); chThdSleepMilliseconds(500); } }
|
主要区别在于Discovery有8个LED,这些LED通过2个线程进行管理。有趣的是,每个线程管理4个LED,尽管如此,游戏灯仍然保持同步。这是由于硬实时调度的可靠性。 ChibiOS / RT被认为是一种硬实时操作系统:这意味着它的代码执行具有确定性和可预测性。
到目前为止,我们已经看到了如何导入现有的演示以及如何闪存和运行它们。开发过程实际上需要创建新项目。最好的方法是复制开发板的默认演示。
这可以打开演示,复制并粘贴它(CTRL + C和CTRL + V)并选择一个新名称。要完成此过程,我们还要更改makefile中的相对路径,否则新项目将无法编译。
在 简短的解释是:在Makefile中,你必须改变这种
# Imported source files and paths CHIBIOS = ../../.. |
对此
# Imported source files and paths CHIBIOS = ../../chibios182 |
该长的解释是,CHIBIOS 是一个 相对路径。相对路径是一种指定目录相对于另一个目录的位置的方法。谈论符号的相对路径“ 。“(点)表示此文件夹,符号” .. “(双点)表示父文件夹。
在这种情况下,CHIBIOS代表ChibiOS主目录(C:\ ChibiStudio \ chibios182) 报告给项目路径(C:\ ChibiStudio \ chibios182 \ demos \ STM32 \ RT-STM32F401RE-NUCLEO64)的路径,因此在这种情况下绝对路径等于三步(../../ ..)。
复制后,新创建的项目将被放置在我们的工作区(C:\ ChibiStudio \ workspace_user \ <新项目的名称> \)中,因此新的相对路径将返回两步,然后输入文件夹 chibios182 (.. /../chibios182)。
复制默认项目我们必须编辑makefile并修复CHIBIOS相对路径,否则新项目将无法编译。
在此修改之后,我们将能够编译我们的新项目,但是如果不编辑启动配置,我们将无法进行闪存和运行。复制项目我们还复制了包含.launch文件的名为debug的文件夹:遗憾的是,此文件仍与旧项目相关联。要解决这个问题,我们必须打开启动配置文件,搜索旧项目的名称,并将其替换为新项目的名称:它应该替换三次。
请记住重命名 启动配置以避免项目之间的混淆。
复制默认项目,我们必须编辑启动配置,使其指向我们的新项目。使用新项目的名称重命名它以避免混淆也是一个好主意。
让我们恢复此程序所需的步骤:
以下视频将消除所有疑虑
如何终止和删除调试部分的最新实例
在上一篇文章中,我们已经讨论过启动OpenOCD以及flash和run过程。我想补充一点:当我们开发一个新项目时,每次编辑代码时,我们都需要使用更新版本的固件重新刷新我们的微控制器。如果我们每次都遵循闪存和运行程序,我们会尝试多次启动OpenOCD,但这是不可能的。
如果我们不终止前一个实例,我们甚至无法重新启动闪存并运行。那么正确的方法是什么?
您可以注意到在Debug窗口中有两个实例:OpenOCD和当前调试部分(具有用于开始调试的启动配置的名称)。
您不需要终止OpenOCD,除非:
您必须终止当前的调试部分:要在调试窗口中右键单击调试部分的上一个实例,然后选择终止和删除(如图2所示)。
现在您可以启动新的调试配置。