【工具使用】STM32CubeMX-FreeRTOS操作系统-信号标志、互斥锁、信号量篇
一、概述
无论是新手还是大佬,基于STM32单片机的开发,使用STM32CubeMX都是可以极大提升开发效率的,并且其界面化的开发,也大大降低了新手对STM32单片机的开发门槛。
本文主要讲述STM32芯片FreeRTOS
信号标志、互斥锁和信号量的配置及其相关知识。
二、软件说明
STM32CubeMX是ST官方出的一款针对ST的MCU/MPU跨平台的图形化工具,支持在Linux、MacOS、Window系统下开发,其对接的底层接口是HAL库,另外习惯于寄存器开发的同学们,也可以使用LL库。STM32CubeMX除了集成MCU/MPU的硬件抽象层,另外还集成了像RTOS,文件系统,USB,网络,显示,嵌入式AI等中间件,这样开发者就能够很轻松的完成MCU/MPU的底层驱动的配置,留出更多精力开发上层功能逻辑,能够更进一步提高了嵌入式开发效率。
演示版本 6.1.0
三、FreeRTOS功能简介
嵌入式初学者一般使用的是裸机开发,而大多数所谓的进阶课程,就是使用操作系统开发。其实两者并不存在很大的差距,使用操作系统,更多是在裸机开发的基础上,限制在操作系统要求的框架下进行开发,同时需要留意操作系统的一些特性,以防止出现问题。具体操作系统开发与裸机开发的区别如下:
维度 | FreeRTOS(RTOS 开发) | 裸机开发(轮询 / 中断驱动) |
---|---|---|
任务管理 | 多任务并行(抢占式 / 协作式),自动调度 | 单线程轮询 + 中断处理,手动协调任务优先级 |
实时性 | 高(可精确控制任务执行顺序和响应时间) | 依赖中断优先级和轮询顺序,复杂场景易卡顿 |
系统复杂度 | 适合复杂逻辑(如多外设、通信协议、用户界面) | 适合简单逻辑(如单一传感器采集、LED 控制) |
代码结构 | 模块化(任务独立,通过 IPC 通信) | 线性代码 + 全局变量,耦合度高 |
资源占用 | 需额外内存(栈空间、内核数据结构) | 资源占用极小(仅代码和必要数据) |
开发成本 | 学习成本较高(需理解 RTOS 概念) | 门槛低,适合快速实现简单功能 |
可维护性 | 任务隔离性好,扩展新功能更方便 | 功能扩展可能需修改全局逻辑,维护困难 |
提到操作系统,第一反应也是大家最早接触的,应该就是Windows系统了(当然新生代可能第一接触的是苹果的IOS系统或华为的鸿蒙系统),但由于Windows的交互友好性,让大家很难感知到它的存在;而学习了嵌入式后,又知道了Linux这个天花板般的操作系统存在,虽然是个开源系统,但其体量也让大多数人忘而生畏,从而使操作系统蒙上一层神秘的面纱。而今天的主角FreeRTOS,作为小体量且开源的操作系统,正是敲开操作系统神秘大门的砖头,带着我们了解其中的奥妙。
FreeRTOS 是一款开源实时操作系统(RTOS),专为嵌入式系统设计,尤其适用于资源受限的微控制器(MCU)。由 Richard Barry 开发并发布首个版本(V1.0),最初名为 FreeRTOS Kernel,旨在提供轻量级、可移植的多任务处理能力。早期版本以代码简洁、易于移植为特点,迅速在嵌入式社区流行。2012 年,成立 Real Time Engineers Ltd 公司,推动 FreeRTOS 商业化,推出付费技术支持和扩展组件(如文件系统、TCP/IP 栈)。逐步支持更多硬件平台(如 ARM Cortex-M、ESP32、RISC-V 等),并构建生态系统,包括中间件和工具链。2016 年,亚马逊(AWS)收购 FreeRTOS,将其纳入 IoT 战略,推出 AWS IoT Greengrass for FreeRTOS,强化物联网(IoT)连接能力(如 MQTT、OTA 升级)。最新版本(截至 2025 年)为 V202212.00,持续优化实时性能、安全性和云集成,并提供免费的认证服务(如功能安全认证 IEC 61508)。
FreeRTOS 以实时性、轻量级、可配置性为核心,功能模块包括:
- 任务调度器
- 抢占式调度:支持多任务按优先级运行,高优先级任务可中断低优先级任务,确保实时响应。
- 协作式调度(可选):任务主动释放控制权,适合对实时性要求不高的场景。
- 支持 任务优先级(最多 32 级,可配置)和 时间片轮转(同优先级任务分时执行)。
- 任务间通信(IPC)
- 队列(Queue):任务 / 中断间传递数据,支持先进先出(FIFO)或优先级队列。
- 信号量(Semaphore):包括二进制信号量、计数信号量,用于资源同步与互斥(如 mutex 互斥信号量)。
- 事件组(Event Group):实现任务间多事件同步。
- 内存管理
- 提供多种内存分配策略:
– 静态分配:编译时分配固定内存,适合关键任务,避免内存碎片。
– 动态分配:运行时动态申请内存(类似 C 语言 malloc),需注意碎片问题。 - 支持用户自定义内存管理方案。
- 定时器与中断管理
- 软件定时器:基于系统时钟的周期性或一次性定时器,支持回调函数。
- 中断安全接口:允许在中断服务程序(ISR)中安全访问 RTOS 资源(如队列、信号量)。
- 可配置性与移植性
- 通过头文件(FreeRTOSConfig.h)配置内核参数(如任务数量、栈大小、调度器行为),灵活适配不同硬件。
- 提供标准接口,移植到新平台只需实现少量汇编代码(如上下文切换)。
- 生态与扩展组件
- 中间件:集成文件系统(如 FATFS)、TCP/IP 栈(LwIP)、USB 协议栈、图形界面(GUI)等。
- 物联网支持:通过 AWS IoT 组件实现设备与云端通信,支持 MQTT、TLS 加密、设备管理等。
- 安全与认证:提供功能安全版本(如 FreeRTOS-Safety),通过 IEC 61508、ISO 26262 等认证,适合工业、汽车电子等场景。
除了FreeRTOS,其他还有很多其他优秀的嵌入式操作系统,其中就包括很多人学校里会学到的uC/OS-II,FreeRTOS与其他常见的嵌入式操作系统对比如下:
维度 | FreeRTOS | uC/OS-II | RTX(ARM) | RIOT OS |
---|---|---|---|---|
许可证 | 开源(GPLv2,修改需开源)/ 商业许可 | 开源(需购买商业许可用于产品) | 商业许可(需授权) | 开源(BSD-2-Clause) |
实时性 | 抢占式调度,微秒级响应 | 抢占式调度,支持优先级继承 | 抢占式调度,支持 CMSIS-RTOS 标准 | 抢占式 + 协作式,适合 IoT 低功耗 |
代码复杂度 | 简洁,核心代码约 10k 行 C 语言 | 模块化设计,代码量较大 | 集成于 Keil 工具链,抽象层完善 | 面向 IoT,轻量级(<10KB 内存) |
生态系统 | 丰富(AWS IoT、中间件、社区支持) | 成熟(工业、汽车领域案例多) | 与 ARM 工具链深度整合(Keil、IAR) | 专注 IoT,支持传感器网络和低功耗协议 |
资源占用 | 极小(ROM: ~4KB,RAM: ~1KB) | 中等(ROM: ~10KB,RAM: ~2KB) | 中等(依赖组件数量) | 极轻量(适合 8/16 位 MCU) |
典型应用 | IoT 设备、消费电子、工业控制 | 医疗设备、航空航天、汽车电子 | 嵌入式系统开发(ARM Cortex-M 系列) | 物联网边缘设备、传感器节点 |
FreeRTOS 凭借轻量、开源、易移植的特性,成为嵌入式领域最流行的 RTOS 之一,尤其适合 IoT 和中小型实时系统。其与 AWS 的深度整合进一步强化了物联网能力,而功能安全认证版本则拓展了工业和汽车电子市场。相比裸机开发,它能显著提升复杂系统的设计效率和实时性,但需权衡资源占用和学习成本。对于开发者而言,若项目需要多任务调度、实时响应或未来扩展,FreeRTOS 是理想选择;若需求简单或资源受限,裸机开发仍具优势。
四、FreeRTOS配置及应用
4.1 FreeRTOS配置说明
具体参考《【工具使用】STM32CubeMX-FreeRTOS操作系统-任务、延时、定时器篇》,这里就不再赘述了。
4.2 接口说明
使用CubeMX生成的工程,会将FreeRTOS的接口再封装一层统一接口CMSIS-RTOS,这是 ARM 定义的一套 RTOS 抽象层标准,旨在通过统一接口屏蔽不同 RTOS 的差异。这里我们先来认识几个比较常用的接口。
4.2.1 信号标志
1. osSignalSet - 设置线程的信号标志
int32_t osSignalSet(osThreadId thread_id, int32_t signals);
向指定线程(thread_id)的信号标志寄存器中 按位设置 一个或多个信号标志(signals)。信号标志是线程的私有资源,每个线程有一组独立的标志位(通常是 32 位整数),用于表示异步事件的发生。
参数:
thread_id:目标线程的 ID(通过 osThreadCreate 或 osThreadGetId 获取)。
signals:要设置的信号标志(如 0x01、0x02 等,可通过按位或组合多个标志)。
返回值:
osOK:设置成功。
osErrorOS:操作失败(如线程 ID 无效)。
2. osSignalClear - 清除线程的信号标志
int32_t osSignalClear(osThreadId thread_id, int32_t signals);
从指定线程的信号标志寄存器中 按位清除 一个或多个信号标志(signals)。返回值为清除前的标志位状态,可用于判断哪些标志被清除前已设置。
参数:
thread_id:目标线程的 ID。
signals:要清除的信号标志(与 osSignalSet 中的标志位对应)。
返回值:
清除前的信号标志值(若成功)。
0x80000000:参数无效(如线程 ID 不存在)。
注:通常在目标线程处理完信号后调用,避免重复处理同一事件。清除操作是线程安全的,由RTOS内核保证原子性。
3. osSignalWait - 等待线程的信号标志
osEvent osSignalWait(int32_t signals, uint32_t millisec);
使当前线程进入 阻塞状态,等待自身的信号标志满足指定条件。可指定等待所有标志位均被设置(signals 非零,按位与匹配)或任意一个标志位被设置(signals 为 0)。
参数:
signals:
非零值:等待所有指定位均被设置(如 signals=0x03 表示等待标志 0 和 1 同时置位)。
0:等待任意一个标志位被设置。
millisec:超时时间(单位:毫秒),0 表示永不超时。
返回值:
osEvent 结构体:
-status:状态码(如 osEventSignal 表示信号触发,osEventTimeout 表示超时)。
-value.signals:实际触发的信号标志值(仅当 .status 为 osEventSignal 时有效)。
使用流程
- 当前线程调用 osSignalWait 阻塞等待信号。
- 其他线程通过 osSignalSet 触发信号。
- 若满足条件或超时,当前线程恢复运行,通过返回值判断事件类型并处理。
4.2.2 互斥锁
1. 创建互斥锁(osMutexCreate)
osMutexCreate(const osMutexDef_t *mutex_def)
创建并初始化一个互斥锁。
参数:
mutex_def:互斥锁定义(通过 osMutex(name) 获取地址)。
返回值:
osMutexId:互斥锁 ID(非 NULL 表示成功);
NULL:创建失败(如内存不足)。
2. 等待获取互斥锁(osMutexWait)
osMutexWait(osMutexId mutex_id, uint32_t millisec)
等待获取互斥锁(阻塞当前任务直到获取成功或超时)。
参数:
mutex_id:互斥锁 ID(由 osMutexCreate 返回);
millisec:超时时间(单位:毫秒):
0:不等待,立即返回;
osWaitForever:无限等待(需包含头文件 cmsis_os.h)。
返回值:
osOK:成功获取锁;
osErrorTimeout:超时未获取;
osErrorParameter:参数无效(如 mutex_id 为 NULL)。
注:优先级继承:若 RTOS 支持(如 FreeRTOS),低优先级任务持有锁时,高优先级任务等待会临时提升前者优先级,避免优先级反转。递归锁:同一任务可多次调用 osMutexWait,但需对应次数的 osMutexRelease 释放。
3. 释放互斥锁(osMutexRelease)
osMutexRelease(osMutexId mutex_id)
释放已持有的互斥锁。
参数:
mutex_id:互斥锁 ID。
返回值:
osOK:释放成功;
osErrorResource:调用者未持有该锁(非法释放);
osErrorParameter:参数无效。
注:必须由持有锁的任务调用,中断服务程序(ISR)不可调用(需用信号量替代)。释放后,等待该锁的任务中优先级最高的任务将被唤醒。
4. 删除互斥锁(osMutexDelete)
osMutexDelete(osMutexId mutex_id)
删除一个不再使用的互斥锁,释放相关资源。
参数:
mutex_id:互斥锁 ID。
返回值:
osOK:删除成功;
osErrorResource:锁仍被持有(无法删除);
osErrorParameter:参数无效。
注:删除前需确保 没有任务持有该锁 且 无任务在等待该锁,否则会导致不可预测行为。动态分配的互斥锁需删除以释放内存;静态分配的锁可省略删除(由用户手动管理内存)。
特性 | 互斥锁(Mutex) | 信号量(Semaphore) |
---|---|---|
用途 | 保护独占资源(一对一) | 控制资源计数(一对多) |
递归性 | 支持(同一任务可重入) | 不支持 |
优先级继承 | 支持(部分 RTOS) | 不支持 |
中断安全 | 否(需用信号量替代) | 是(通过 xxxFromISR 函数) |
4.2.3 信号量
1. 信号量对象定义宏(osSemaphoreDef 和 osSemaphore)
#if defined (osObjectsExternal) // 外部定义(如在其他文件中声明)
#define osSemaphoreDef(name) \
extern const osSemaphoreDef_t os_semaphore_def_##name
#else // 本地定义#if (configSUPPORT_STATIC_ALLOCATION == 1) // 静态内存分配#define osSemaphoreDef(name) \const osSemaphoreDef_t os_semaphore_def_##name = { 0, NULL }#define osSemaphoreStaticDef(name, control) \const osSemaphoreDef_t os_semaphore_def_##name = { 0, (control) }#else // 动态内存分配(默认)#define osSemaphoreDef(name) \const osSemaphoreDef_t os_semaphore_def_##name = { 0 }#endif
#endif#define osSemaphore(name) \
&os_semaphore_def_##name // 获取信号量对象地址
声明或定义信号量对象的结构体(osSemaphoreDef_t)。
关键细节:
静态分配(configSUPPORT_STATIC_ALLOCATION=1):需手动指定内存(如 osSemaphoreStaticDef 传入控制块地址),适合内存受限场景。
动态分配:RTOS 自动管理内存,使用更灵活。
命名规则:信号量对象名为 os_semaphore_def_,通过 osSemaphore(name) 访问其地址。
2. 创建信号量(osSemaphoreCreate)
osSemaphoreId osSemaphoreCreate(const osSemaphoreDef_t *semaphore_def, int32_t count);
初始化信号量对象,指定初始可用资源数 count。
参数:
semaphore_def:信号量定义结构体指针(通过 osSemaphore(name) 获取)。
count:初始可用资源数:
二进制信号量:count = 1(初始可用)。
计数信号量:count = N(N 为资源总数)。
返回值:
成功:信号量 ID(非 NULL)。
失败:NULL(如内存不足)。
3. 等待信号量(osSemaphoreWait)
int32_t osSemaphoreWait(osSemaphoreId semaphore_id, uint32_t millisec);
获取信号量(消耗一个令牌),若资源不可用则阻塞等待。
参数:
semaphore_id:信号量 ID(由 osSemaphoreCreate 返回)。
millisec:超时时间(单位:毫秒):
0:不等待,立即返回当前状态。
osWaitForever:无限等待(需 RTOS 支持)。
返回值:
osOK:成功。
其他:其他超时等异常,具体参考源码的定义
typedef enum {osOK = 0, ///< function completed; no error or event occurred.osEventSignal = 0x08, ///< function completed; signal event occurred.osEventMessage = 0x10, ///< function completed; message event occurred.osEventMail = 0x20, ///< function completed; mail event occurred.osEventTimeout = 0x40, ///< function completed; timeout occurred.osErrorParameter = 0x80, ///< parameter error: a mandatory parameter was missing or specified an incorrect object.osErrorResource = 0x81, ///< resource not available: a specified resource was not available.osErrorTimeoutResource = 0xC1, ///< resource not available within given time: a specified resource was not available within the timeout period.osErrorISR = 0x82, ///< not allowed in ISR context: the function cannot be called from interrupt service routines.osErrorISRRecursive = 0x83, ///< function called multiple times from ISR with same object.osErrorPriority = 0x84, ///< system cannot determine priority or thread has illegal priority.osErrorNoMemory = 0x85, ///< system is out of memory: it was impossible to allocate or reserve memory for the operation.osErrorValue = 0x86, ///< value of a parameter is out of range.osErrorOS = 0xFF, ///< unspecified RTOS error: run-time error but no other error message fits.os_status_reserved = 0x7FFFFFFF ///< prevent from enum down-size compiler optimization.
} osStatus;
4. 释放信号量(osSemaphoreRelease)
osStatus osSemaphoreRelease(osSemaphoreId semaphore_id);
释放信号量(归还一个令牌),唤醒等待该信号量的线程。
参数:
semaphore_id:信号量 ID。
返回值:
osOK:成功。
其他同等待信号量
5. 删除信号量(osSemaphoreDelete)
osStatus osSemaphoreDelete(osSemaphoreId semaphore_id);
销毁信号量对象,释放其占用的内存。
参数:
semaphore_id:信号量 ID。
返回值:
osOK:成功(需确保无线程等待该信号量)。
其他同等待信号量
注:删除前需确保没有线程正在等待该信号量,否则可能导致不可预测行为。动态分配的信号量需删除以释放内存,静态分配的信号量需手动管理生命周期。
特性 | 信号量 | 互斥锁 |
---|---|---|
典型用途 | 资源计数、线程同步 | 互斥访问(需优先级继承) |
计数支持 | 支持计数(N ≥ 1) | 仅二进制(N = 1) |
优先级继承 | 不支持 | 支持(避免优先级反转) |
释放者 | 任意线程可释放 | 仅持有锁的线程可释放 |
4.3 应用配置
在之前线程、延时、定时器篇中配置的基础上,再使能信号标志、互斥锁、信号量的功能。并且因为信号量、互斥锁这些更多应用在两个或多个线程之间,为了演示其一般的应用方式,这里需要多配置几个线程。因为这里用到信号量的资源计数功能,所以要在CubeMX中使能USE_COUNTING_SEMAPHORES
的功能,启用信号量计数。互斥锁需要使能USE_MUTEXES
,这里一般默认是使能的。
4.4 代码实现
4.4.1 信号标志
信号标志实现一个功能:响应线程B和C的按键信号,在线程A中显示不同的灯。这里让按下KEY_R
键闪LED_ACT
,按下KEY_W
键闪LED_READY
。(因为按键在不同的线程里,而闪灯在同一个线程中,所以实际显示效果是按下按键后会延迟闪灯)
/* USER CODE BEGIN 4 */
#define LEDACT_EVENT (1 << 0)
#define LEDREADY_EVENT (1 << 1)
osThreadId LEDThreadID;/* USER CODE END 4 *//* USER CODE BEGIN Header_StartDefaultTask */
/*** @brief Function implementing the defaultTask thread.* @param argument: Not used* @retval None*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{/* USER CODE BEGIN 5 *//* 获取当前线程ID */LEDThreadID = osThreadGetId();/* Infinite loop */for(;;){osEvent ret = osSignalWait(LEDACT_EVENT | LEDREADY_EVENT, 500);if (ret.value.signals == LEDACT_EVENT){HAL_GPIO_WritePin(LED_ACT_GPIO_Port, LED_ACT_Pin, GPIO_PIN_RESET);osDelay(500);HAL_GPIO_WritePin(LED_ACT_GPIO_Port, LED_ACT_Pin, GPIO_PIN_SET);osDelay(500);}if (ret.value.signals == LEDREADY_EVENT){HAL_GPIO_WritePin(LED_READY_GPIO_Port, LED_READY_Pin, GPIO_PIN_RESET);osDelay(500);HAL_GPIO_WritePin(LED_READY_GPIO_Port, LED_READY_Pin, GPIO_PIN_SET);osDelay(500);}}/* USER CODE END 5 */
}/* USER CODE BEGIN Header_StartTask03 */
/**
* @brief Function implementing the myTask03 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask03 */
void StartTask03(void const * argument)
{/* USER CODE BEGIN StartTask03 *//* Infinite loop */for(;;){if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(KEY_R_GPIO_Port, KEY_R_Pin)){/* 经典5ms消抖 */osDelay(5);if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(KEY_R_GPIO_Port, KEY_R_Pin)){/* 直到放开才算完成一次按键 */while (GPIO_PIN_RESET == HAL_GPIO_ReadPin(KEY_R_GPIO_Port, KEY_R_Pin));osSignalSet(LEDThreadID, LEDACT_EVENT);}}}/* USER CODE END StartTask03 */
}/* USER CODE BEGIN Header_StartTask04 */
/**
* @brief Function implementing the myTask04 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask04 */
void StartTask04(void const * argument)
{/* USER CODE BEGIN StartTask04 *//* Infinite loop */for(;;){if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(KEY_W_GPIO_Port, KEY_W_Pin)){/* 经典5ms消抖 */osDelay(5);if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(KEY_W_GPIO_Port, KEY_W_Pin)){/* 直到放开才算完成一次按键 */while (GPIO_PIN_RESET == HAL_GPIO_ReadPin(KEY_W_GPIO_Port, KEY_W_Pin));osSignalSet(LEDThreadID, LEDREADY_EVENT);}}}/* USER CODE END StartTask04 */
}
4.4.2 互斥锁
互斥锁实现一个功能:保护一个临界资源的使用(即保护共享资源,防止重入的问题)。这里为了直观演示且增大复现的概率,把单片机的主频调成1MHz,让循环的时间拉长,然后对比下加互斥锁和不加互斥锁的区别。
/* USER CODE BEGIN Header_StartTask02 */
/**
* @brief Function implementing the myTask02 thread.
* @param argument: Not used
* @retval None
*/
#define BUFFSIZE (2000)
uint32_t ShareBuff[BUFFSIZE];/* 互斥锁相关定义 */
osMutexDef(FlashLEDMute);
osMutexId FlashLEDMuteID;/* USER CODE END Header_StartTask02 */
void StartTask02(void const * argument)
{/* USER CODE BEGIN StartTask02 */uint32_t i = 0;FlashLEDMuteID = osMutexCreate(osMutex(FlashLEDMute));/* Infinite loop */for(;;){//if (osOK == osMutexWait(FlashLEDMuteID, 500)){for (i = 0; i < BUFFSIZE; i++){ShareBuff[i] = 1;}for (i = 0; i < BUFFSIZE; i++){if (1 != ShareBuff[i])break;}if (i == BUFFSIZE){HAL_GPIO_WritePin(LED_READY_GPIO_Port, LED_READY_Pin, GPIO_PIN_RESET);}else{HAL_GPIO_WritePin(LED_READY_GPIO_Port, LED_READY_Pin, GPIO_PIN_SET);}//osMutexRelease(FlashLEDMuteID);}osDelay(1);}/* USER CODE END StartTask02 */
}/* USER CODE BEGIN Header_StartTask03 */
/**
* @brief Function implementing the myTask03 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask03 */
void StartTask03(void const * argument)
{/* USER CODE BEGIN StartTask03 *//* Infinite loop */for(;;){//if (osOK == osMutexWait(FlashLEDMuteID, 500)){for (uint32_t i = 0; i < BUFFSIZE; i++){ShareBuff[i] = 2;}//osMutexRelease(FlashLEDMuteID);}osDelay(1);}/* USER CODE END StartTask03 */
}
下面是加了互斥锁后
/* USER CODE BEGIN Header_StartTask02 */
/**
* @brief Function implementing the myTask02 thread.
* @param argument: Not used
* @retval None
*/
#define BUFFSIZE (2000)
uint32_t ShareBuff[BUFFSIZE];/* 互斥锁相关定义 */
osMutexDef(FlashLEDMute);
osMutexId FlashLEDMuteID;/* USER CODE END Header_StartTask02 */
void StartTask02(void const * argument)
{/* USER CODE BEGIN StartTask02 */uint32_t i = 0;FlashLEDMuteID = osMutexCreate(osMutex(FlashLEDMute));/* Infinite loop */for(;;){if (osOK == osMutexWait(FlashLEDMuteID, 500)){for (i = 0; i < BUFFSIZE; i++){ShareBuff[i] = 1;}for (i = 0; i < BUFFSIZE; i++){if (1 != ShareBuff[i])break;}if (i == BUFFSIZE){HAL_GPIO_WritePin(LED_READY_GPIO_Port, LED_READY_Pin, GPIO_PIN_RESET);}else{HAL_GPIO_WritePin(LED_READY_GPIO_Port, LED_READY_Pin, GPIO_PIN_SET);}osMutexRelease(FlashLEDMuteID);}osDelay(1);}/* USER CODE END StartTask02 */
}/* USER CODE BEGIN Header_StartTask03 */
/**
* @brief Function implementing the myTask03 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask03 */
void StartTask03(void const * argument)
{/* USER CODE BEGIN StartTask03 *//* Infinite loop */for(;;){if (osOK == osMutexWait(FlashLEDMuteID, 500)){for (uint32_t i = 0; i < BUFFSIZE; i++){ShareBuff[i] = 2;}osMutexRelease(FlashLEDMuteID);}osDelay(1);}/* USER CODE END StartTask03 */
}
如果这里单看StartTask02
任务里的操作,是完全看不出有什么问题的,这就是使用操作系统与裸机开发最大的区别。操作系统是存在多线程的,线程之间的轮转方式,除了通过一些osDelay
延时接口,或信号量等等待接口对当前任务进行让出外,还有固定的时间片轮转机制,也就是当一个线程运行达到一个时间片的长度时(一个时间片一般就是操作系统设置的时基,这里是1ms),会跳出查找是否有同等优先级的其他线程需要运行,如果有会执行轮转。因此当线程中执行大量操作,且同其他线程共用同一个资源时,就容易在资源使用的边界被其他线程误修改,且当中的时序不可控,最终表现出来就是现象很随机,问题概率性出现。这就是嵌入式经常讲的重入问题。所以在操作系统中,为了防止这种共享资源应用异常,可以使用互斥锁对该资源进行保护,防止使用过程中异常切出。
4.4.3 信号量
信号量实现一个功能:按照按键按下的次数闪烁对应的次数。这里设置按多少次KEY_W
键亮LED_READY
灯多少次,最大次数为5次。不过这个应用方式用得不是很恰当,因为创建信号量的时候,初始就存在5个信号量,需要消耗完再等按键新释放的信号量,跟一开始的设计想法不大一样,不过这里用来学习接口的用法也足够了。
/* USER CODE BEGIN Header_StartTask02 */
/**
* @brief Function implementing the myTask02 thread.
* @param argument: Not used
* @retval None
*/
osSemaphoreDef(FlashLED);
osSemaphoreId FlashLEDID;/* USER CODE END Header_StartTask02 */
void StartTask02(void const * argument)
{/* USER CODE BEGIN StartTask02 *//* Infinite loop */for(;;){/* 一直等,直到有接收到信号量再执行 */if (osOK == osSemaphoreWait(FlashLEDID, osWaitForever)){/* 1s消耗一个信号量 */HAL_GPIO_WritePin(LED_READY_GPIO_Port, LED_READY_Pin, GPIO_PIN_RESET);osDelay(500);HAL_GPIO_WritePin(LED_READY_GPIO_Port, LED_READY_Pin, GPIO_PIN_SET);osDelay(500);}}/* USER CODE END StartTask02 */
}/* USER CODE BEGIN Header_StartTask03 */
/**
* @brief Function implementing the myTask03 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask03 */
void StartTask03(void const * argument)
{/* USER CODE BEGIN StartTask03 *//* 最多闪5下 */FlashLEDID = osSemaphoreCreate(osSemaphore(FlashLED), 5);/* Infinite loop */for(;;){if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(KEY_W_GPIO_Port, KEY_W_Pin)){/* 经典5ms消抖 */osDelay(5);if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(KEY_W_GPIO_Port, KEY_W_Pin)){/* 直到放开才算完成一次按键 */while (GPIO_PIN_RESET == HAL_GPIO_ReadPin(KEY_W_GPIO_Port, KEY_W_Pin));osSemaphoreRelease(FlashLEDID);}}}/* USER CODE END StartTask03 */
}
五、注意事项
1、使用操作系统要注意共享资源的使用,如果存在两个任务同时读写同一个共用内存时,要留意是否存在重入问题,如果需要保护该临界资源,可以通过互斥锁的方式进行保护。
2、信号量在创建的时候设置最大资源个数时,会同步创建同样多的资源数。
六、相关链接
对于刚入门的小伙伴可以先看下STM32CubeMX的基础使用及Keil的基础使用。
【工具使用】STM32CubeMX-基础使用篇
【工具使用】Keil5软件使用-基础使用篇
【工具使用】STM32CubeMX-FreeRTOS操作系统-任务、延时、定时器篇
相关文章:

【工具使用】STM32CubeMX-FreeRTOS操作系统-信号标志、互斥锁、信号量篇
一、概述 无论是新手还是大佬,基于STM32单片机的开发,使用STM32CubeMX都是可以极大提升开发效率的,并且其界面化的开发,也大大降低了新手对STM32单片机的开发门槛。 本文主要讲述STM32芯片FreeRTOS信号标志、互斥锁和信号…...
[P2P]并发模式
设备可以同时作为 P2P Client 监听其他P2P请求,需要硬件和驱动支持。 //某些高级Wi-Fi芯片(如高通、博通)支持 Concurrent Mode(并发模式 GO 如果GO已经有一个client,大多数支持接受新的P2P Discovery。默认情况下会…...
Cloudflare 免费域名邮箱 支持 Catch-all 无限别名收件
本文首发于只抄博客,欢迎点击原文链接了解更多内容。 前言 与自建 Poste.io 还有 Serv00 邮局不同,Cloudflare 的域名邮箱并不需要 VPS,也没有复杂的配置。只要有一个托管在 Cloudflare 的域名就可以部署,像是常见的免费域名 eu.org 或者 dpdns.org 都是可以使用的。 需要…...

大数据Spark(六十一):Spark基于Standalone提交任务流程
文章目录 Spark基于Standalone提交任务流程 一、Standalone-Client模式 1、提交命令 2、任务执行流程 二、Standalone-Cluster模式 1、提交命令 2、任务执行流程 Spark基于Standalone提交任务流程 在Standalone模式下,Spark的任务提交根据Driver程序运行的位…...
学习记录:DAY32
Electron 开发之旅:从入门到实践 前言 接续上一篇 blog,这篇的内容主要和 Electron 有关。 课设不是特别想做下去了,实际核心代码大概只有 3,4 百行左右,比较水…… 或许会把 Docker 的部署也做一做(权当是…...
next,react封装axios,http请求
import axios from axios;//声明一个基础接口变量1 let base_url; //配置开发环境 if (process.env.NODE_ENV development) {base_url "http://127.0.0.1/"; } // 配置生产环境 if (process.env.NODE_ENV production) {base_url "http://127.0.0.1/"; …...
元图CAD:一键解锁PDF转CAD,OCR技术赋能高效转换
在建筑、工程与制造领域,图纸的精准性与高效协作是项目成功的关键。然而,传统PDF文件中的文字和图形往往难以直接编辑,手动输入不仅耗时易错,还可能因格式问题导致信息丢失。元图CAD凭借创新的OCR文字识别技术,重新定义…...

Android 平台RTSP/RTMP播放器SDK接入说明
一、技术背景 自2015年起,大牛直播SDK持续深耕音视频直播领域,自主研发并迭代推出跨平台 RTSP/RTMP 播放模块,具备如下核心优势: 全平台兼容:支持 Android/iOS/Windows/Linux 等主流系统; 超低延迟&#…...

Nodejs工程化实践:构建高性能前后端交互系统
一、工程架构设计 1.1 现代化项目初始化 采用多包管理架构: mkdir content-platform && cd content-platform npm init -y npx lerna init mkdir -p {packages/client,packages/server,packages/shared} 关键模块划分: client/: 基于Next.js…...

STM32什么是寄存器
提示:文章 文章目录 前言一、背景二、2.12.2 三、3.1 总结 前言 前期疑问: 1、什么是寄存器? 答:在4GB的地址空间中,512MB的block2上,每4个字节组成32位,这个32位为一个单元,控制&a…...
Linux 的 find 命令使用指南
精通 Linux 的 find 命令:终极使用指南 在 Linux 系统中,find 命令是文件搜索的瑞士军刀,它能基于多种条件在目录树中精准定位文件。无论你是系统管理员还是开发者,掌握 find 都能极大提升工作效率。本文将深入解析 find 的核心用法,并附赠实用示例! 一、基础语法结构 …...

第六个微信小程序:教师工具集
源于工作需要,下面开始。 安装及使用 | Taro 文档 vscode 代码管理 git 辅助 开发技术如上: 1.开始创建模板 taro4.1.1 $ taro init teachers-tools 2.用vsocde开始吧。 选择 第二个文件夹找一。 (base) PS D:\react\teachers-tools> pnpm…...

记录一个用了很久的git提交到github和gitee比较方便的方法
在当前git init后,在隐藏的git文件夹中找到config文件 [user]name thels [remote "github"]url your github repository urlfetch refs/heads/*:refs/remotes/origin/* [remote "gitee"]url your gitee repository urlfetch refs/heads/*:…...

Qt Qml模块功能及功能解析
QtQml 是 Qt 6.0 中用于声明式 UI 开发和应用程序逻辑的核心模块,它提供了 QML 语言的支持和运行时环境。 一、主要功能 1. QML 语言支持 QML 语法解析:支持 QML (Qt Meta-Object Language 或 Qt Modeling Language) 的完整语法 JavaScript 集成&…...
前端八股之JS的原型链
1.原型的定义 每一个对象从被创建开始就和另一个对象关联,从另一个对象上继承其属性,这个另一个对象就是 原型。 当访问一个对象的属性时,先在对象的本身找,找不到就去对象的原型上找,如果还是找不到,就去…...

NLP学习路线图(二十九):BERT及其变体
在自然语言处理(NLP)领域,一场静默的革命始于2017年。当谷歌研究者发表《Attention is All You Need》时,很少有人预料到其中提出的Transformer架构会彻底颠覆NLP的发展轨迹,更催生了以GPT系列为代表的语言模型风暴,重新定义了人类与机器的交互方式。 一、传统NLP的瓶颈:…...
机器翻译模型笔记
机器翻译学习笔记(简体中文) 1. 任务概述 目标:将英文句子翻译成简体中文。 示例: 输入:Tom is a student. 输出:汤姆是一个学生。 框架:Seq2Seq(序列到序列)模型。…...
Ref vs. Reactive:Vue 3 响应式变量的最佳选择指南
Ref vs. Reactive:Vue 3 响应式变量的最佳选择指南 在 Vue 3 的 Composition API 中,ref 和 reactive 是创建响应式数据的两种主要方式。许多开发者经常困惑于何时使用哪种方式。本文将深入对比两者的差异,帮助您做出最佳选择。 核心概念解…...
让视觉基础模型(VFMs)像大语言模型(LLMs)一样“会思考”
视觉检测器的演进:从 DETR 到 Grounding-DINO DINO-R1 的基础是 Grounding-DINO,而 Grounding-DINO 本身是一系列视觉检测器演进的结果。理解这个发展过程对掌握 DINO-R1 的核心技术至关重要。 DETR:用 Transformer 革新目标检测 在 DETR&…...
现代前端框架的发展与演进
现代前端框架的发展与演进是一个非常值得关注的话题,反映了整个前端生态系统的不断演化与技术深度的提升。以下是这一趋势的详细解析: 📈 现代前端框架的发展与演进 🔹 第一阶段:jQuery 时代(2006-2013&am…...

【LLM-Agent】智能体的记忆缓存设计
note 实践:https://modelscope-agent.readthedocs.io/zh-cn/latest/modules/memory.html 文章目录 note一、Agent的记忆实现二、相关综述三、记忆体的构建四、cursor的记忆设计1. 记忆生成提示词2. 记忆评估提示词 五、记忆相关的MCPReference 一、Agent的记忆实现…...

一起学Spring AI:核心概念
人工智能概念 本节描述了 Spring AI 使用的核心概念。我们建议您仔细阅读,以理解 Spring AI 实现背后的思想。 模型(Models) 人工智能模型是设计用来处理和生成信息的算法,通常模仿人类的认知功能。通过从大型数据集中学习模式…...
Oracle业务用户的存储过程个数及行数统计
Oracle业务用户的存储过程个数及行数统计 统计所有业务用户存储过程的个数独立定义的存储过程定义在包里的存储过程统计所有业务用户存储过程的总行数独立定义的存储过程定义在包里的存储过程📖 对存储过程进行统计主要用到以下三个系统视图: dba_objects:记录了所有独立创…...

PicSharp(图片压缩工具) v1.1.6
PicSharp 一个简单、高效、灵活的跨平台桌面图像压缩应用程序。软件基于Rust实现,高性能低资源,能快速扫描文件或目录,批处理图像。软件还具备组合压缩策略,TinyPNG提供最佳压缩比,但需要互联网连接,对大量…...

前端文件下载常用方式详解
在前端开发中,实现文件下载是常见的需求。根据不同的场景,我们可以选择不同的方法来实现文件流的下载。本文介绍三种常用的文件下载方式: 使用 axios 发送 JSON 请求下载文件流使用 axios 发送 FormData 请求下载文件流使用原生 form 表单提…...

【DAY42】Grad-CAM与Hook函数
内容来自浙大疏锦行python打卡训练营 浙大疏锦行 知识点: 回调函数lambda函数hook函数的模块钩子和张量钩子Grad-CAM的示例 作业:理解下今天的代码即可 在深度学习中,我们经常需要查看或修改模型中间层的输出或梯度。然而,标准的前向传播和反…...

如何生成和制作PDF文件
在数字化办公的今天,PDF文件已经成为我们工作和学习中不可或缺的一部分。无论是合同、报告、简历,还是电子书、表单,PDF格式都以其跨平台兼容性、不可编辑性和清晰的排版而被广泛使用。但你是否知道,生成和制作PDF文件其实并不复杂…...

【K8S系列】Kubernetes 中 Pod(Java服务)启动缓慢的深度分析与解决方案
本文针对 Kubernetes 中 Java 服务启动时间慢的深度分析与解决方案文章,结合了底层原理、常见原因及具体优化策略: Kubernetes 中 Java 服务启动缓慢的深度分析与高效解决方案 在 Kubernetes 上部署 Java 应用时,启动时间过长是常见痛点,尤其在需要快速扩缩容或滚动更新的…...

【Java学习笔记】StringBuilder类(重点)
StringBuilder(重点) 1. 基本介绍 是一个可变的字符串序列。该类提供一个与 StringBuffer 兼容的 API,但不保证同步(StringBuilder 不是线程安全的) 该类被设计用作 StringBuffer 的一个简易替换,用在字符…...
JavaScript ES6 解构:优雅提取数据的艺术
JavaScript ES6 解构:优雅提取数据的艺术 在 JavaScript 的世界中,ES6(ECMAScript 2015)的推出为开发者带来了许多革命性的特性,其中“解构赋值”(Destructuring Assignment)无疑是最受欢迎的功…...