基于STM32CubeIDE与FreeRTOS的HelloWorld多任务示例
我介绍了STM32CubeIDE这个强大的编程工具并基于STM32CubeMX方便地移植了FreeRTOS操作系统。不能免俗像大多数的编程教程在这一讲中我将写一个HelloWorld程序展示下FreeRTOS起到抛砖引玉的效果。本程序基于的开发版是stm32f4-discovery板载一个lcd屏幕和两个led灯。本程序的目的是建立两个任务同时运行其中第一个任务显示在lcd屏上滚动显示文字第二个任务是使一个led灯1s闪烁。一、示例下面是本项目的main程序限于篇幅还有着重点很多语句我不会过多介绍主要是让大家体验下如何方便地调用FreeRTOS的功能osThreadId Task1Handle; osThreadId Task2Handle; void HelloWorldTask(void const * argument); void BlinkTask(void const * argument); char str[] Hello World!; int TextPos 1;1.声明了两个线程在配置线程之前我们先来理解两个关键函数osThreadDef和osThreadCreate的作用。前者用于定义线程的属性后者则根据定义创建线程并返回线程句柄。osThreadDef 参数说明线程名name第一个参数如HelloWorld用于标识该线程定义后续通过osThread宏引用它。线程函数thread第二个参数如HelloWorldTask即线程实际执行的函数入口。优先级priority第三个参数如osPriorityNormal决定线程在调度器中的抢占优先级数值越高越优先执行。实例数instances第四个参数通常填 0表示使用默认实例数 1。堆栈大小stacksz第五个参数如2048单位为字节表示分配给该线程的栈空间大小需根据线程内局部变量和调用深度合理设置。osThreadCreate 参数说明线程定义thread_def第一个参数通过osThread(HelloWorld)宏引用前面定义的线程。参数argument第二个参数可向线程函数传入一个指针参数本例不需要传参填NULL。下面给出一个参数调整的示例将 HelloWorld 线程的优先级提升为osPriorityHigh并把堆栈空间增大到 4096 字节以容纳更复杂的显示逻辑/* 调整优先级为 High堆栈空间增大到 4096 字节 */ osThreadDef(HelloWorld, HelloWorldTask, osPriorityHigh, 0, 4096); Task1Handle osThreadCreate(osThread(HelloWorld), NULL);通过调整优先级和堆栈大小可以更好地匹配实际任务的实时性要求和资源占用这也是后续开发多线程项目时最常做的配置优化。下面给出 HelloWorldTask 和 BlinkTask 两个任务函数的完整实现。HelloWorldTask 负责在 LCD 屏幕上滚动显示文字BlinkTask 负责让 LED 以 1 秒为周期闪烁。/* HelloWorldTask在 LCD 屏幕上滚动显示 Hello World! */ void HelloWorldTask(void const * argument) { /* 初始化 LCD 屏幕 */ LCD_Init(); LCD_Clear(LCD_COLOR_BLACK); while (1) { /* 清屏并设置文字颜色 */ LCD_Clear(LCD_COLOR_BLACK); LCD_SetTextColor(LCD_COLOR_WHITE); /* 在当前位置显示文字TextPos 控制水平滚动位置 */ LCD_DisplayStringLine(Line0, (uint8_t *)str); /* 更新滚动位置超出屏幕宽度后回到起点 */ TextPos; if (TextPos 20) { TextPos 1; } /* 延时 200ms形成滚动效果 */ osDelay(200); } } /* BlinkTask控制 LED 以 1 秒为周期闪烁 */ void BlinkTask(void const * argument) { while (1) { /* 点亮 LED */ BSP_LED_On(LED3); /* 保持点亮 500ms */ osDelay(500); /* 熄灭 LED */ BSP_LED_Off(LED3); /* 保持熄灭 500ms合计 1s 一个闪烁周期 */ osDelay(500); } }其中HelloWorldTask通过不断更新TextPos并配合osDelay实现文字的滚动显示BlinkTask则通过交替点亮和熄灭 LED 并各延时 500ms实现 1 秒周期的闪烁效果。两个任务都由 FreeRTOS 调度器统一管理互不阻塞。在声明部分我们声明了两个线程Task1Handle和Task2Handle显示程序HelloWorldTask和Led闪烁程序BlinkTaskint main(void) { /*......之前程序是对单片机进行初始化和配置*/ osThreadDef(HelloWorld, HelloWorldTask, osPriorityNormal, 0, 2048); Task1Handle osThreadCreate(osThread(HelloWorld), NULL); osThreadDef(blink, BlinkTask, osPriorityNormal, 0, 2048); Task2Handle osThreadCreate(osThread(blink), NULL); /* USER CODE END RTOS_THREADS */ /* Start scheduler */ osKernelStart(); /* We should never get here as control is now taken by the scheduler */ while (1) { } }在main函数中首先的一系列函数是对单片机的资源进行配置初始化单片机rcc初始化lcd屏幕等等在这里我们就略过不讲了。然后当我们使用FreeRTOS的时候我们首先需要配置需要执行的线程程序osThreadDef(HelloWorld, HelloWorldTask, osPriorityNormal, 0, 2048); Task1Handle osThreadCreate(osThread(HelloWorld), NULL);声明了第一个线程Task1Handle并让它指向显示程序HelloWorldTaskosThreadDef(blink, BlinkTask, osPriorityNormal, 0, 2048); Task2Handle osThreadCreate(osThread(blink), NULL);其次了第二个线程Task2Handle并让它指向闪烁程序BlinkTask语句启动FreeRTOS的任务调度程序单片机就运行起来了我们可以看到一般开发FreeRTOS项目的流程首先是对单片机进行初始化然后是对需要执行的线程进行配置最后启动任务调度可以看到整个流程是十分清晰简洁的。同时各个线程之间耦合程度低日后对项目进行维护时只要对相关的线程程序进行修改就行了。二、常见问题与排查在运行本文示例时可能会遇到一些典型问题。下面针对常见现象给出原因分析和解决方法帮助读者快速定位并修复。1. LCD 显示乱码或滚动异常现象LCD 屏幕上出现乱码或者文字滚动位置错乱、显示不完整。原因滚动位置变量TextPos超出屏幕显示范围导致显示越界。osDelay延时设置过短滚动速度过快肉眼难以看清。字符串str内容或编码与 LCD 字库不匹配。解决方法检查TextPos的边界判断确保其值始终在屏幕有效显示范围内例如在超过 20 时重置为 1。适当增大osDelay的延时值如 200ms 或更大让滚动速度更平稳。确认str中只包含 LCD 字库支持的字符避免使用特殊符号或非 ASCII 字符。2. LED 不闪烁现象LED 一直保持点亮或熄灭没有按 1 秒周期闪烁。原因BSP_LED_On/BSP_LED_Off使用的引脚定义与板载 LED 实际引脚不一致。线程优先级设置过低导致BlinkTask长时间得不到调度。LED 初始化代码缺失或顺序错误。解决方法核对开发板原理图确认LED3对应的 GPIO 引脚并检查BSP_LED_On/BSP_LED_Off的引脚定义是否正确。适当提高BlinkTask的优先级例如使用osPriorityHigh确保其能被及时调度。在main函数初始化阶段调用BSP_LED_Init完成 LED 引脚配置。3. 任务不执行现象程序编译下载后两个任务都没有运行系统似乎卡死或没有任何输出。原因osThreadDef中堆栈大小设置过小任务启动时栈溢出导致崩溃。线程定义或创建语句书写错误例如线程名与函数名不匹配。未调用osKernelStart启动调度器或调用位置不正确。解决方法增大osThreadDef中的堆栈大小参数如从 2048 调整为 4096确保任务有足够的栈空间。检查osThreadDef和osThreadCreate的参数是否一一对应线程名、函数名和句柄保持一致。确认在完成所有线程配置后调用osKernelStart且该调用位于while(1)循环之前。三、任务间通信信号量前面的示例中两个任务各自独立运行互不干扰。但在实际项目中任务之间往往需要协作例如一个任务等待另一个任务完成某项工作后再继续执行。FreeRTOS 提供了多种任务间通信机制其中信号量Semaphore是最常用的一种。本节以二值信号量为例演示如何实现任务同步并说明信号量在资源保护中的作用。1. 二值信号量实现任务同步二值信号量只有 0 和 1 两种状态适合用于任务间的同步。下面给出一个示例BlinkTask每完成一次 LED 闪烁后释放信号量HelloWorldTask则等待该信号量后再更新 LCD 显示从而保证两个任务按节奏协同工作。/* 声明信号量句柄 */ osSemaphoreId BlinkSemHandle; /* BlinkTask闪烁 LED 并释放信号量 */ void BlinkTask(void const * argument) { while (1) { /* 点亮 LED */ BSP_LED_On(LED3); osDelay(500); /* 熄灭 LED */ BSP_LED_Off(LED3); osDelay(500); /* 完成一次闪烁后释放信号量通知 HelloWorldTask */ osSemaphoreRelease(BlinkSemHandle); } } /* HelloWorldTask等待信号量后更新 LCD 显示 */ void HelloWorldTask(void const * argument) { LCD_Init(); LCD_Clear(LCD_COLOR_BLACK); while (1) { /* 等待信号量获取到后才继续执行 */ osSemaphoreAcquire(BlinkSemHandle, osWaitForever); /* 更新滚动位置并显示文字 */ TextPos; if (TextPos 20) { TextPos 1; } LCD_DisplayStringLine(Line0, (uint8_t *)str); } }在main函数中需要先创建信号量再创建两个任务int main(void) { /*......之前程序是对单片机进行初始化和配置*/ /* 创建二值信号量初始计数值为 0 */ BlinkSemHandle osSemaphoreNew(1, 0, NULL); osThreadDef(HelloWorld, HelloWorldTask, osPriorityNormal, 0, 2048); Task1Handle osThreadCreate(osThread(HelloWorld), NULL); osThreadDef(blink, BlinkTask, osPriorityNormal, 0, 2048); Task2Handle osThreadCreate(osThread(blink), NULL); /* Start scheduler */ osKernelStart(); while (1) { } }2. 信号量相关函数说明下面分别介绍osSemaphoreNew、osSemaphoreAcquire和osSemaphoreRelease三个函数的用法。osSemaphoreNew 参数说明最大计数值max_count第一个参数如1表示信号量计数值的上限。二值信号量填 1计数信号量可填更大的值。初始计数值initial_count第二个参数如0表示信号量创建时的初始值。若希望任务一开始就能获取信号量可填 1。属性attr第三个参数通常填NULL使用默认属性。osSemaphoreAcquire 参数说明信号量句柄semaphore_id第一个参数传入osSemaphoreNew返回的句柄。超时时间timeout第二个参数如osWaitForever表示无限等待也可传入具体毫秒数超时后返回错误码。osSemaphoreRelease 参数说明信号量句柄semaphore_id唯一参数传入要释放的信号量句柄使计数值加 1。3. 信号量在资源保护中的作用除了任务同步信号量还常用于保护共享资源。当多个任务需要访问同一个外设或全局变量时如果不加保护可能出现数据竞争。使用二值信号量可以保证同一时刻只有一个任务访问该资源/* 创建互斥信号量初始值为 1表示资源可用 */ osSemaphoreId UartSemHandle osSemaphoreNew(1, 1, NULL); void TaskA(void const * argument) { while (1) { /* 获取信号量占用串口资源 */ osSemaphoreAcquire(UartSemHandle, osWaitForever); /* 独占访问串口打印任务 A 的数据 */ printf(Task A output\n); osDelay(100); /* 释放信号量让其他任务使用串口 */ osSemaphoreRelease(UartSemHandle); } } void TaskB(void const * argument) { while (1) { /* 获取信号量占用串口资源 */ osSemaphoreAcquire(UartSemHandle, osWaitForever); /* 独占访问串口打印任务 B 的数据 */ printf(Task B output\n); osDelay(100); /* 释放信号量让其他任务使用串口 */ osSemaphoreRelease(UartSemHandle); } }通过这种方式串口等共享资源在同一时刻只会被一个任务占用避免了输出交错和数据竞争这也是信号量在资源保护中的核心价值。四、总结本例的程序比较简单然而在实际的项目中有很多个线程要运行需要对每个线程进行的堆空间大小优先级进行配置同时要顾及到线程间的通讯问题还有对资源的保护避免互锁等这些都将是我们以后探讨的重点话题。FreeRTOS是非常强大的实时操作系统可以游刃有余地解决这些问题。我希望读者通过本篇文章对FreeRTOS能有个大致的思路如果对其中某些具体语句看不懂是正常的我将在之后的文章中详细介绍FreeRTOS的每个功能和使用案例并期待一起讨论共同提高技术。