# 简单的例子 ## 1. 新建带FreeRTOS的项目 ### 1.1. 建立项目 File->New->STM32 Project ![image-20231025101713280](./img/image-20231025101713280.png) ![image-20231025101816533](./img/image-20231025101816533.png) ### 1.2. 配置项目 首先启用FreeRTOS支持,使用CMSIS_V2接口。 ![image-20231025102033130](./img/image-20231025102033130.png) 在Advanced settings 中打开 NEWLIB 的支持。 ![image-20231025102129858](./img/image-20231025102129858.png) 设置系统滴答的时钟源以及调试方式。 ![image-20231025102501142](./img/image-20231025102501142.png) 因为代码量比较多,最好为每个外设设置独立的文件。 ![image-20231025102612621](./img/image-20231025102612621.png) 保存后生成代码框架 最后设置使用printf可以使用浮点。 Project->Properties ![image-20231025103836426](./img/image-20231025103836426.png) ## 2. 事件标志组的使用 请参考书上333页的讲解。 这里使用两个按钮都同时按过后才能点亮灯作为例子;如果只按了一个按钮不会点亮灯;两个按钮的先后顺序没关系;也就是说这是一个AND关系。 为了简单,我们并不做太多的操作,只是在一个按钮按下的时候设置信号而已。当点灯的任务执行完点灯后,清除标志。 ### 2.1. 环境配置 这里的配置都在IDE中配置完成,并不涉及代码 #### 2.1.1. 按键线程 建立两个线程,定时检查按钮。 ![image-20231025104626443](./img/image-20231025104626443.png)![image-20231025114518323](./img/image-20231025114518323.png) #### 2.1.2. 按键配置 设置按键方式为输入,设置label为 KEY1 和 KEY2。需要在GPIO中设置两个端口的上拉(其中有个端口有外部上拉,我记不住了,就两个都设置了,也不影响)。 ![image-20231025110118570](./img/image-20231025110118570.png) #### 2.1.3. LED灯配置 使用红色灯PB0端口设置成输出,并设置label为LED。 ![image-20231025114717324](./img/image-20231025114717324.png) #### 2.1.4. LED控制线程 建立一个LED控制线程 ![image-20231025114751740](./img/image-20231025114751740.png) #### 2.1.5. 事件标志组 书上是通过代码的方式建立事件标志组的,代码有些问题。这里直接使用配置的方式建立事件标志组。 ![image-20231025114942291](./img/image-20231025114942291.png) ### 2.2. 代码解析 IDE已经为我们生成了代码框架,接下来看看主要的代码块在什么位置。 #### 2.2.1. 代码框架 GPIO的端口常量定义在main.h 的大约 60 行: ```c #define LED_Pin GPIO_PIN_0 #define LED_GPIO_Port GPIOB #define KEY1_Pin GPIO_PIN_12 #define KEY1_GPIO_Port GPIOB #define KEY2_Pin GPIO_PIN_13 #define KEY2_GPIO_Port GPIOB ``` 这里定义了我们的三个IO的port和pin,到时候需要使用。 在 freertos.c 的大约67行定义了事件标志组的变量: ```c /* Definitions for ledEvent */ osEventFlagsId_t ledEventHandle; const osEventFlagsAttr_t ledEvent_attributes = { .name = "ledEvent" }; ``` 在大约87行的**MX_FREERTOS_Init** 初始化函数中对线程以及事件标志进行初始化: ```c void MX_FREERTOS_Init(void) { /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* USER CODE BEGIN RTOS_MUTEX */ /* add mutexes, ... */ /* USER CODE END RTOS_MUTEX */ /* USER CODE BEGIN RTOS_SEMAPHORES */ /* add semaphores, ... */ /* USER CODE END RTOS_SEMAPHORES */ /* USER CODE BEGIN RTOS_TIMERS */ /* start timers, add new ones, ... */ /* USER CODE END RTOS_TIMERS */ /* USER CODE BEGIN RTOS_QUEUES */ /* add queues, ... */ /* USER CODE END RTOS_QUEUES */ /* Create the thread(s) */ /* creation of defaultTask */ defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); /* creation of taskKey1 */ taskKey1Handle = osThreadNew(StartTaskKey1, NULL, &taskKey1_attributes); /* creation of taskKey2 */ taskKey2Handle = osThreadNew(StartTaskKey2, NULL, &taskKey2_attributes); /* creation of taskLed */ taskLedHandle = osThreadNew(StartTaskLed, NULL, &taskLed_attributes); /* USER CODE BEGIN RTOS_THREADS */ /* add threads, ... */ /* USER CODE END RTOS_THREADS */ /* Create the event(s) */ /* creation of ledEvent */ ledEventHandle = osEventFlagsNew(&ledEvent_attributes); /* USER CODE BEGIN RTOS_EVENTS */ /* add events, ... */ /* USER CODE END RTOS_EVENTS */ } ``` 在freertos.c中有三个自定义的线程,可以在IDE的Outline中比较方便的看到: ![image-20231025115651677](./img/image-20231025115651677.png) 这三个函数就是具体的线程任务,我们看看其中的一个,其他三个都是类似的。 ```c /* USER CODE BEGIN Header_StartTaskKey1 */ /** * @brief Function implementing the taskKey1 thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_StartTaskKey1 */ void StartTaskKey1(void *argument) { /* USER CODE BEGIN StartTaskKey1 */ /* Infinite loop */ for (;;) { osDelay(1); } /* USER CODE END StartTaskKey1 */ } ``` #### 2.2.2. 添加代码 我们需要在两个按键的线程中读取按键的状态,当按钮被按下后,设置相应的事件标志位;在后在LED线程中等待相应的标志位,等待选项设置成 osFlagsWaitAll,表示当所有的事件被设置后才相应。 实验中采用第0位和第1位作为事件标志。 两个按键线程的代码分别是: ```c /* USER CODE BEGIN Header_StartTaskKey1 */ /** * @brief Function implementing the taskKey1 thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_StartTaskKey1 */ void StartTaskKey1(void *argument) { /* USER CODE BEGIN StartTaskKey1 */ /* Infinite loop */ for (;;) { osDelay(10); if (!HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin)) osEventFlagsSet(ledEventHandle, 0x01); } /* USER CODE END StartTaskKey1 */ } ``` ```c /* USER CODE BEGIN Header_StartTaskKey2 */ /** * @brief Function implementing the taskKey2 thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_StartTaskKey2 */ void StartTaskKey2(void *argument) { /* USER CODE BEGIN StartTaskKey2 */ /* Infinite loop */ for (;;) { osDelay(10); if (!HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin)) osEventFlagsSet(ledEventHandle, 0x02); } /* USER CODE END StartTaskKey2 */ } ``` 使用一个死循环去循环检测按钮,每次检测的间隔是10个系统滴答(10ms,因为系统滴答的频率是1000)。注意,这里必须使用osDelay函数! 如果检测到按键被按下,设置事件标志组的标志。按键1设置第0位,因此是 0x01;按键2设置第1位,因此是 0x02。 LED线程等待标志位: ```c /* USER CODE BEGIN Header_StartTaskLed */ /** * @brief Function implementing the taskLed thread. * @param argument: Not used * @retval None */ /* USER CODE END Header_StartTaskLed */ void StartTaskLed(void *argument) { /* USER CODE BEGIN StartTaskLed */ /* Infinite loop */ for (;;) { osEventFlagsWait(ledEventHandle, 0x03, osFlagsWaitAll, osWaitForever); HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } /* USER CODE END StartTaskLed */ } ``` 因为第0位和第1位都设置后的位掩码对应的数字是 0x03,这样当检测到两个标志都设置后才对LED进行翻转。如果osEventFlagsWait等待到事件满足条件后,会清空相应的事件标志。 #### 2.2.3. 实验效果 单按一个按钮LED并不会进行状态转换,按一个后(可以释放),再按第二个后,LED的状态翻转。 # 实验3设计 ![Alt text](img/design.drawio.png) 注意以下几点: 1. 从最小的代码量一步步完善。如果一开始就是很多代码,那么一旦错误很难定位是哪里出现问题;从最少的代码逐步完善,那么可以基本保证前面代码的正确性。 2. 当遇到问题的时候,需要仔细看错误提示;善用跟踪调试工具,并在过程中结合自己的知识,网上的资料和合理的猜想。 3. 一条路走不通,换条路走,代码的实现方式有很多种。