Skip to content

《FreeRTOS实战快速入门》课程讲义

零、前言

FreeRTOS 实战快速入门(纯手撸版)

此教程不会讲过多理论,以动手实操为主,解决大伙学了半天 FreeRTOS 操作系统不知道干什么的问题。

为什么要学 FreeRTOS ?

更有钱途!!

  • 只会祼机开发的单片机工程师,薪资注定不会高于会 FreeRTOS 的工程师;

  • 有了 FreeRTOS 基础,对于将来学习 Linux 操作系统会更加有帮助;

如何学好 FreeRTOS ?

无它,多写代码,多做项目!

一定要把本课程里所有项目全部自己动手做一遍,加深理解。光看不练假把式!!

误区:不建议一上来就啃源码!!

学习本课程前置要求

  • C 语言熟练;
  • STM32 要熟练,没学过 STM32 的同学建议学一下《STM32实战快速入门》课程

本课程做了哪些升级?

  • 所有代码全程手敲,没有使用任何 CubeMX
  • 加入三个实战项目:排队控制系统、智能门禁系统、智能台灯
  • 项目代码以大厂要求开发,包括开发过程、流程图、代码风格等

准备好了吗?快乐启程~

本课程用到的硬件

  • 主要以 STM32F103C8T6 开发板为主
  • 做项目需要用到的硬件
    • ST-Link
    • USB转TTL
    • 杜邦线
    • 继电器
    • 蜂鸣器
    • 红外传感器
    • LCD1602
    • 矩阵键盘
    • OLED 屏幕
    • W25Q128
    • 蓝牙模块
    • 超声波传感器
    • 光敏电阻传感器
    • 高功率LED灯
    • KEY × 4

一、FreeRTOS简介

1. 什么是 FreeRTOS ?

Free即免费的,RTOS的全称是Real time operating system,中文就是实时操作系统。

注意:RTOS不是指某一个确定的系统,而是指一类操作系统。比如:uc/OS,FreeRTOS,RTX,RT-Thread等这些都是RTOS类操作系统。

FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。 由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行,其最新版本为10.4.4版。

(以上来自百度百科)

2. 为什么选择 FreeRTOS ?

  • FreeRTOS 是免费的,无潜在商业风险;

  • 很多半导体厂商产品的SDK(Software Development Kit)软件开发工具包,就使用FreeRTOS作为其操作系统,尤其是WIFI、蓝牙这些带有协议栈的芯片或模块。

  • 简单,因为FreeRTOS的文件数量很少。

3. 祼机开发与FreeRTOS

祼机开发:

while(1)
{
	太监();
	杨贵妃();
	张贵妃();
	if(time_flag == 1)
	{
		开会();
	}
	皇太后();
}

裸机又称为前后台系统,前台系统指的中断服务函数,后台系统指的大循环,即应用程序。

FreeRTOS:

void main(void) 
{ 
	xTaskCreate(太监);
	xTaskCreate(杨贵妃);
	xTaskCreate(张贵妃);
	xTaskCreate(皇太后);
}
void 太监(void)
{
	while(1)
	{
		汇报工作();
	}
}

void 杨贵妃(void)
{
	while(1);
	{
		唱歌();
	}
}

void 张贵妃(void)
{
	while(1);
	{
		跳舞();
	}
}

void 皇太后(void)
{
	while(1);
	{
		教训();
	}
}

RTOS全称为:Real Time OS,就是实时操作系统,强调的是:实时性

FreeRTOS 实现多任务的原理

系统将时间分割成很多时间片,然后轮流执行各个任务——分时复用

每个任务都是独立运行的,互不影响,由于切换的频率很快,就感觉像是同时运行的一样。

4. FreeRTOS相比裸机开发的优势

4.1 多任务实时调度

  • 裸机局限:裸机开发通常采用轮询或前后台系统,任务需顺序执行,紧急事件易被延迟处理(如任务1执行时无法响应任务2)。
  • FreeRTOS优势:支持抢占式调度和时间片轮转,高优先级任务可立即抢占低优先级任务,确保实时性;延时函数(如 vTaskDelay())释放 CPU 给其他任务,避免空等待。

4.2 资源管理高效化

  • 裸机局限:需手动处理中断堆栈、数据同步等问题,中断嵌套易导致堆栈溢出或数据混乱。

  • FreeRTOS优势:提供信号量、互斥锁、队列等同步机制,避免资源冲突;优先级继承机制防止死锁,提升系统稳定性。

4.3 代码结构与可维护性

  • 裸机局限:功能代码集中在 while(1) 循环中,结构臃肿,维护困难。
  • FreeRTOS优势:任务模块化设计,功能解耦,代码清晰易扩展;开发者可专注于业务逻辑,减少底层调度代码编写。

4.4 低资源占用与可移植性

  • 裸机局限:需针对特定硬件定制开发,移植成本高。

  • FreeRTOS优势:内核仅需 4–9KB RAM,支持多种处理器(如 ARM、RISC-V);源码采用 C 语言编写,移植层代码少,适配不同硬件便捷。

二、FreeRTOS移植

1. 移植前准备

  • 基础工程
  • FreeRTOS源码

https://www.freertos.org/

2. 手把手教你移植FreeRTOS

2.1 添加FreeRTOS源码

  • 创建FreeRTOS子文件夹

  • 拷备FreeRTOS源码

名称描述
include内包含了FreeRTOS的头文件
portable内包含了FreeRTOS的移植文件
croutine.c协程相关文件
event_groups.c事件相关文件
list.c列表相关文件
queue.c队列相关文件
stream_buffer.c流式缓冲区相关文件
tasks.c任务相关文件
timers.c软件定时器相关文件
  • 精简portable文件夹

名称描述
Keil指向RVDS文件夹
RVDS不同内核芯片的移植文件
MemMang内存管理文件

2.2 将FreeRTOS源码添加到基础工程

  • 创建两个文件分组

  • 将FreeRTOS源码添加到文件分组里

  • 添加头文件路径

2.3 添加FreeRTOSConfig.h文件

2.4. 修改工程代码

2.4.1 注释回调函数

stm32f1xx_it.c文件

c
//void SVC_Handler(void)
//{
//}

//void PendSV_Handler(void)
//{
//}

//void SysTick_Handler(void)
//{
//  HAL_IncTick();
//}
2.4.2 修改delay模块

使用「带操作系统的延时函数」里的delay模块。

c
//以下添加到delay.c
#include "FreeRTOS.h"
#include "task.h"

extern void xPortSysTickHandler(void);

void SysTick_Handler(void)
{
    HAL_IncTick();
    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) /* OS开始跑了,才执行正常的调度处理 */
    {
        xPortSysTickHandler();
    }
}

void delay_init(void)
{
    SysTick_Config(HAL_RCC_GetHCLKFreq()/configTICK_RATE_HZ);
}

//以下添加到delay.h
void delay_init(void);
2.4.3 修改main模块
c
#include "FreeRTOS.h"
#include "freertos_test.h"
#include "task.h"

int main(void)
{
    HAL_Init();                         /* 初始化HAL库 */
    stm32_clock_init(RCC_PLL_MUL9);     /* 设置时钟, 72Mhz */
    delay_init();
    led_init();                         /* 初始化LED灯 */
    uart1_init(115200);
    printf("hello world!\r\n");

    freertos_test();
    vTaskStartScheduler();
}
2.4.4 修改宏值
c
#define __NVIC_PRIO_BITS           4    //去掉尾巴的U

2.5 添加测试代码

3. 测试

硬件接线:

STM32ST-LinkUSB转TTL
CLKCLK
DIODIO
TX1RX
RX1TX
3V33V3
GNDGNDGND

实验现象:

led1以1秒的频率闪烁,led2以500ms频率闪烁。

三、任务

1. 任务的基本特性

1.1 任务定义与结构

每个任务是一个独立的函数,遵循 void TaskFunction(void *pvParameters) 的原型,通常以无限循环形式运行,通过 vTaskDelay() 等函数主动释放 CPU 资源。任务由**任务控制块(TCB)**描述,包含栈指针、优先级、状态等信息。

堆栈分配:每个任务需预分配独立栈空间(静态或动态),例如 STM32 中 configMINIMAL_STACK_SIZE 设置为 128 字(即 512 字节)。 • 优先级范围:0(最低)至 configMAX_PRIORITIES-1(最高),优先级越高越易抢占 CPU(数值越大优先级越高)。

1.2 任务类型

动态任务:通过 xTaskCreate() 创建,由内核自动分配内存,适用于灵活性要求高的场景。 • 静态任务:使用 xTaskCreateStatic() 创建,需手动预分配 TCB 和栈内存,适合资源受限系统。

2. 任务状态与转换

FreeRTOS 的任务包含五种状态,通过调度器动态切换:

1. 运行态(Running):当前占用 CPU 的任务,单核系统中同一时刻只能有一个任务处于运行态。 2. 就绪态(Ready):已准备就绪,等待调度器分配 CPU 时间。 3. 阻塞态(Blocked):等待事件(如信号量、队列消息或延时)触发,例如调用 vTaskDelay(1000) 进入 1 秒阻塞。 4. 挂起态(Suspended):通过 vTaskSuspend() 显式挂起,需调用 vTaskResume() 恢复,期间不参与调度。 5. 终止态/僵尸态(Deleted):任务被删除后进入终止态,由空闲任务清理资源。

1、仅就绪态可转变成运行态

2、其他状态的任务想运行,必须先转变成就绪态

3. 任务调度机制

3.1 调度概述

1. 抢占式调度高优先级抢占:当高优先级任务就绪时,立即抢占当前任务(如优先级 2 的任务抢占优先级 1 的任务)。 • 优先级继承:低优先级任务持有互斥锁时,临时继承高优先级任务的优先级,避免死锁。

2. 时间片轮转调度 • 同优先级任务按时间片轮流执行,每个时间片长度由 configTICK_RATE_HZ 定义(如 1ms Tick)。 • 示例:任务 A 和 B 优先级相同,各自运行 1 个时间片后切换。

3. 协程式调度 • 当前执行任务将会一直运行,同时高优先级的任务不会抢占低优先级任务 • FreeRTOS现在虽然还支持,但是官方已经表示不再更新协程式调度

3.2 抢占式调度详解

运行条件:

依次创建三个任务:task1、task2、task3,优先级分别为1、2、3

运行过程如下:

  1. task1优先被创建,优先进入运行态;当task2被创建并进入就绪态时,由于优先级比task1高,则抢占task1进入运行态;
  2. task3被创建并进入就绪态时,由于优先级比task2高,则抢占task2进入运行态;
  3. task3在运行过程中被阻塞了(如调用了vTaskDelay()函数),进入阻塞态,此时task2由就绪态转为运行态;
  4. task3解除了阻塞(如延时时间到),由阻塞态转为就绪态,由于优先级比task2高,由抢占task2转为运行态。

3.3 时间片轮转详解

任务运行固定时间片(由configTICK_RATE_HZ定义,如1ms)后自动切换至同优先级的下一个任务。

运行条件:

依次创建三个任务:task1、task2、task3,优先级均为1

运行过程如下:

  1. task1优先被创建,优先进入运行态;进行完一个时间片后,转为就绪态,此时task2进入运行态;
  2. task2运行完一个时间片后,转为就绪态,此时task3进入运行态;
  3. task3在运行过程中被阻塞了(如调用了vTaskDelay()函数),进入阻塞态,此时未用完的时间片被丢弃;
  4. 调度器直接调度task1进入运行态,task1运行完一个时间片后,切换至task2运行。

4. 任务创建函数

任务创建函数主要有两种:动态创建静态创建

二者有何区别?

动态创建: 通过 xTaskCreate() 函数实现,任务控制块(TCB)和任务堆栈内存由 FreeRTOS 从系统堆(Heap)中自动分配,需依赖内存管理算法(如 heap_4.c)。 • 优点:开发者无需手动管理内存,适合频繁创建/删除任务的场景。 • 缺点:可能因内存碎片导致长期运行后分配失败。

静态创建: 通过 xTaskCreateStatic() 函数实现,开发者需预先分配 TCB 和堆栈内存(如全局数组),并通过参数传递给函数。 • 优点:内存分配在编译时完成,无动态内存开销,适合对内存确定性要求高的系统(如汽车电子)。 • 缺点:需手动管理内存生命周期,灵活性较低。

4.1 动态(内存分配)创建任务:xTaskCreate()

函数原型

c
BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,        // 任务函数指针(函数原型:void task(void *pvParams))
    const char *const pcName,        // 任务名称(调试用,长度≤configMAX_TASK_NAME_LEN)
    configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度(单位:字,如1024字=4KB for ARM)
    void *const pvParameters,         // 任务参数指针(传递给任务函数)
    UBaseType_t uxPriority,          // 优先级(0最低,configMAX_PRIORITIES-1最高)
    TaskHandle_t *const pxCreatedTask // 任务句柄(用于后续操作如删除、挂起)
);

返回值: • pdPASS:任务创建成功。 • errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:堆内存不足,创建失败。

注意:

需要将宏configSUPPORT_DYNAMIC_ALLOCATION 配置为 1

4.2 静态(内存分配)创建任务:xTaskCreateStatic()

函数原型

c
TaskHandle_t xTaskCreateStatic(
    TaskFunction_t pvTaskCode, 
    const char *const pcName, 
    uint32_t ulStackDepth, 
    void *const pvParameters, 
    UBaseType_t uxPriority, 
    StackType_t *const puxStackBuffer,  // 用户预分配的堆栈内存
    StaticTask_t *const pxTaskBuffer    // 用户预分配的TCB内存
);

返回值: • 成功:返回任务句柄。 • 失败:返回 NULL(参数错误或内存未对齐)。

使用流程:

1. 配置宏与预分配内存

• 在 FreeRTOSConfig.h 中启用静态内存分配:

c
#define configSUPPORT_STATIC_ALLOCATION 1  // 启用静态内存分配

• 预分配 TCB 和堆栈内存:

c
StaticTask_t xTaskTCB;              // 任务TCB
StackType_t xTaskStack[128];       // 任务堆栈

2. 实现空闲/定时器任务接口(必选)

• 定义空闲任务内存(否则编译报错):

c
//空闲任务配置
StaticTask_t idle_task_tcb;
StackType_t  idle_task_stack[configMINIMAL_STACK_SIZE];

//软件定时器任务配置
StaticTask_t timer_task_tcb;
StackType_t  timer_task_stack[configTIMER_TASK_STACK_DEPTH];

//空闲任务内存分配
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
                                    StackType_t ** ppxIdleTaskStackBuffer,
                                    configSTACK_DEPTH_TYPE * pulIdleTaskStackSize )
{
    * ppxIdleTaskTCBBuffer = &idle_task_tcb;
    * ppxIdleTaskStackBuffer = idle_task_stack;
    * pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}


//软件定时器内存分配
void vApplicationGetTimerTaskMemory( StaticTask_t ** ppxTimerTaskTCBBuffer,
                                     StackType_t ** ppxTimerTaskStackBuffer,
                                     configSTACK_DEPTH_TYPE * pulTimerTaskStackSize )
{
    * ppxTimerTaskTCBBuffer = &timer_task_tcb;
    * ppxTimerTaskStackBuffer = timer_task_stack;
    * pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;
}

3. 调用 xTaskCreateStatic 创建任务

c
TaskHandle_t xStaticTaskHandle = xTaskCreateStatic(
    vTaskDemo,          // 任务函数指针
    "StaticTask",       // 任务名称
    1024,               // 堆栈深度
    NULL,               // 任务参数
    3,                  // 优先级
    xTaskStack,         // 预分配的堆栈地址
    &xTaskTCB           // 预分配的 TCB 地址
);

返回值检查:若 xStaticTaskHandle != NULL 表示成功。

5. 任务删除函数

函数原型

c
void vTaskDelete(TaskHandle_t xTaskToDelete);

参数: • xTaskToDelete:任务句柄。若为 NULL,删除当前任务自身。

行为: • 删除任务后,任务进入 僵尸态,其TCB和堆栈由 空闲任务(Idle Task)自动回收。 • 若任务持有资源(如互斥锁、队列),需先手动释放,否则可能引发内存泄漏。

什么是空闲任务?

空闲任务的概述

空闲任务(Idle Task)是 FreeRTOS 调度器启动时自动创建的后台任务,具有以下特性:

  1. 最低优先级:固定为 0 优先级,仅在所有其他任务阻塞或挂起时运行。
  2. 必要性:确保系统始终有任务可运行,防止处理器“空转”。
  3. 资源占用:默认堆栈大小为 configMINIMAL_STACK_SIZE(通常较小,如 128 字)。

空闲任务的核心功能

自删除任务的内存释放: 当任务调用 vTaskDelete(NULL) 删除自身时,其任务控制块(TCB)和堆栈内存由空闲任务自动回收。 • 直接删除任务的资源处理: 若任务 A 删除任务 B,则任务 B 的资源立即释放,无需空闲任务介入。

小实验1

  1. 动态创建三个任务:task1、task2、task3,三个任务作用如下:
  • task1:以1000ms频率闪烁LED1;
  • task2:以500ms频率闪烁LED2;
  • task3:检测按键
  1. task2创建后即自我删除;
  2. 检测到按键1按下,则删除task1任务。

小实验2

使用静态任务创建方法完成小实验1。

6. 任务挂起与恢复函数

6.1 函数介绍

6.1.1 vTaskSuspend() - 任务挂起函数

功能:将指定任务置为挂起态,该任务将不再参与调度,直到被显式恢复。 • 参数: • xTaskToSuspend:目标任务的句柄(传 NULL 表示挂起自身)。 • 源码行为: • 从就绪/阻塞列表中移除任务,并插入挂起列表 xSuspendedTaskList。 • 若挂起自身,立即触发上下文切换 portYIELD_WITHIN_API()。 • 配置依赖:需在 FreeRTOSConfig.h 中启用宏 INCLUDE_vTaskSuspend=1

6.1.2 vTaskResume() - 任务恢复函数

功能:将挂起的任务恢复到就绪态。 • 参数: • xTaskToResume:目标任务的句柄。 • 限制:仅能恢复通过 vTaskSuspend() 挂起的任务,不支持恢复因事件阻塞的任务。 • 无返回值:直接修改任务状态至就绪列表。

6.1.3 xTaskResumeFromISR() - 中断中任务恢复函数

功能:在中断服务程序(ISR)中恢复挂起的任务。 • 参数: • xTaskToResume:目标任务的句柄。 • 返回值: • pdTRUE:恢复的任务优先级≥当前任务,需调用 portYIELD_FROM_ISR() 触发上下文切换。 • pdFALSE:恢复的任务优先级较低,无需立即切换。 • 配置依赖:需启用宏 INCLUDE_xTaskResumeFromISR=1。 • 注意: • NVIC中断优先级分组需设置为分组4; • 中断服务程序中要调用freeRTOS的API函数则中断优先级不能高于FreeRTOS所管理的最高优先级。

6.2 函数特性与设计规则

6.2.1 非嵌套性

• 无论调用多少次 vTaskSuspend() 挂起任务,只需调用一次 vTaskResume() 即可恢复。

6.2.2 中断安全性

挂起限制:禁止在 ISR 中调用 vTaskSuspend(),否则会导致程序异常。 • 恢复操作:在 ISR 中恢复任务时,必须处理返回值并决定是否触发上下文切换。

小实验3

基于小实验1,实现以下功能:

  1. 按下按键1,挂起task1;
  2. 按下按键2,在任务中恢复task1;

小实验4

基于小实验1,实现以下功能:

  1. 按下按键1,挂起task1;
  2. 按下按键2,在中断中恢复task1;

四、中断管理

1. 中断优先级管理

1.1 优先级分组与范围

分组方式:FreeRTOS 强制使用 NVIC_PriorityGroup_4,即所有中断仅通过抢占优先级区分(0-15级,数值越小优先级越高)。 • 管理范围: • 可管理中断:优先级 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY(默认5)的中断(即5-15)。在 ISR 中必须使用带 FromISR 的 API(如 xTaskResumeFromISR()),避免调用阻塞函数。 • 不可管理中断:优先级 <5 的中断(0-4),属于高优先级中断,不可调用 FreeRTOS API,即使系统处于临界区也不受影响。

中断优先级寄存器(IPR):

1.2 BASEPRI 寄存器机制

作用:通过设置 BASEPRI 寄存器的阈值(如0x50对应优先级5),屏蔽优先级低于该阈值的中断。 • 临界区操作: • portDISABLE_INTERRUPTS():写 BASEPRI 为 configMAX_SYSCALL_INTERRUPT_PRIORITY,屏蔽优先级5-15的中断。 • portENABLE_INTERRUPTS():写 BASEPRI 为0,解除屏蔽。

BASEPRI寄存器:

作用:屏蔽优先级低于某一个阈值的中断,当设置为0时,则不关闭任何中断。 比如: BASEPRI设置为0x50,代表中断优先级在5~15内的均被屏蔽,0~4的中断优先级正常执行。

2. 临界段代码保护

2.1 基本原理

临界段代码指必须完整执行且不可被打断的代码段(如硬件初始化、共享资源访问)。FreeRTOS 通过操作 BASEPRI 寄存器屏蔽中断,确保临界区的原子性。

什么可以打断当前程序的运行?

  • 中断
  • 任务调度

2.2 任务级临界区保护

函数: • taskENTER_CRITICAL():进入临界区,关闭优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断(默认优先级≥5)。 • taskEXIT_CRITICAL():退出临界区,恢复中断。 • 嵌套机制: 使用全局变量 uxCriticalNesting 记录嵌套次数,仅在最外层退出时重新使能中断。 • 代码示例

c
taskENTER_CRITICAL();
// 临界区操作(如修改全局变量)
taskEXIT_CRITICAL();

2.3 中断级临界区保护

函数: • taskENTER_CRITICAL_FROM_ISR():在 ISR 中进入临界区,保存当前 BASEPRI 值并屏蔽中断。 • taskEXIT_CRITICAL_FROM_ISR(status):退出时恢复原 BASEPRI 值。 • 使用场景: 用于中断服务程序中需要保护共享资源的场景,如 UART 接收中断中操作队列。

3. 调度器挂起与恢复

3.1 调度器挂起

函数vTaskSuspendAll()作用:挂起调度器,禁止任务切换(但不关闭中断)。 • 适用场景: 长耗时操作(如大块内存拷贝)需避免任务切换,但需允许中断响应。 • 代码示例

c
vTaskSuspendAll();
memcpy(buffer, source, LARGE_SIZE);  // 耗时操作
xTaskResumeAll();

3.2 调度器恢复

函数xTaskResumeAll()作用:恢复调度器,返回 pdTRUE 表示恢复期间有更高优先级任务就绪。 • 注意事项: 若挂起期间有任务解除阻塞,恢复时会立即触发任务切换。

3.3 临界区保护与调度器挂起的对比

特性临界区保护调度器挂起
中断状态关闭部分中断保持中断开启
任务切换禁止禁止
适用场景短耗时操作(<几微秒)长耗时操作(如文件系统访问)
资源消耗低(仅寄存器操作)低(无上下文切换)
嵌套支持支持支持

小实验5

在FreeRTOS中,多任务使用printf时,极易产生乱码的现象,请解决此问题。

五、延时函数

1. 延时函数概述

1.1 基本概念

延时函数是实时操作系统中用于控制任务执行时序的重要机制,使任务能够在指定时间内暂停执行,让处理器执行其他任务。在FreeRTOS中,延时函数是任务管理和时间管理的重要组成部分。

1.2 延时函数的作用

  • 任务调度控制:允许低优先级任务获得CPU执行时间
  • 时序控制:确保操作按照预定时间间隔执行
  • 降低功耗:在无需执行时进入低功耗状态
  • 软件定时:实现周期性操作和超时检测
  • 防止任务占用CPU:避免高优先级任务长时间占用处理器

2. FreeRTOS中的延时函数

2.1 相对延时函数

2.1.1 vTaskDelay
c
void vTaskDelay(TickType_t xTicksToDelay);
  • 功能:使当前任务延时指定的时钟节拍数
  • 参数xTicksToDelay - 延时的时钟节拍数
  • 特点
    • 相对延时(从调用时开始计时)
    • 不保证精确的时间间隔
    • 延时期间任务处于阻塞状态
    • 调用后立即让出CPU
2.1.2 宏定义pdMS_TO_TICKS
c
#define pdMS_TO_TICKS(xTimeInMs) ((TickType_t)(((uint32_t)(xTimeInMs) * (uint32_t)configTICK_RATE_HZ) / (uint32_t)1000))
  • 功能:将毫秒转换为系统时钟节拍数
  • 用法vTaskDelay(pdMS_TO_TICKS(100)); // 延时100毫秒

2.2 绝对延时函数

2.2.1 vTaskDelayUntil
c
void vTaskDelayUntil(TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement);
  • 功能:使任务延时到指定的绝对时间点
  • 参数
    • pxPreviousWakeTime - 上次唤醒时间的指针
    • xTimeIncrement - 周期间隔的时钟节拍数
  • 特点
    • 绝对延时(从指定时间点计时)
    • 适合周期性任务
    • 能保证固定执行频率

小实验6

请编程验证vTaskDelay与vTaskDelayUntil的区别。

六、队列

1. 队列简介

队列(Queue)是FreeRTOS中实现任务间通信的核心机制,用于在任务与任务、中断与任务之间传递数据。

本质:队列是一个先进先出(FIFO)的环形缓冲区,支持数据项的存储与按顺序读取。 • 数据传递方式:采用值传递(复制数据本身),而非指针传递,确保数据安全性和独立性。 • 应用场景: • 传感器数据采集与处理任务间的数据传输 • 按键事件通知 • 中断服务程序(ISR)向任务传递事件标志

关于队列的几个名词:

队列项目:队列中的每一个数据;

队列长度:队列能够存储队列项目的最大数量;

创建队列时,需要指定队列长度及队列项目大小。

2. 队列的核心特点

2.1 线程安全

• 通过临界区保护(关闭中断)实现多任务并发访问的原子性操作。

写队列:
xQueueSend( )
{
     // 进入临界区(关中断)
       写队列实际操作
     // 退出临界区(开中断)
}

读队列:
xQueueReceive( )
{
     // 进入临界区(关中断)
       读队列实际操作
     // 退出临界区(开中断)
}

• 支持任务在队列满/空时阻塞等待,并可通过优先级唤醒机制自动调度任务。

2.2 灵活的数据存储

• 支持FIFO(默认)和LIFO(通过xQueueSendToFront()实现)两种模式。 • 每个数据项大小固定,队列长度在创建时指定(例如:存储10个int型数据)。

2.3 动态与静态内存分配

动态创建xQueueCreate()从FreeRTOS堆分配内存,适合运行时灵活调整。 • 静态创建xQueueCreateStatic()需预分配内存,适用于资源受限或确定性要求高的场景。

2.4 中断安全

• 提供FromISR后缀的API(如xQueueSendFromISR()),确保中断服务程序安全操作队列。

3. 队列API详解

3.1 队列创建与删除

创建队列
c
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
  • uxQueueLength:队列可容纳的最大消息数
  • uxItemSize:每个消息的大小(字节)
  • 返回:成功返回队列句柄,失败返回NULL

静态创建方式(无需动态内存分配):

c
QueueHandle_t xQueueCreateStatic(
    UBaseType_t uxQueueLength,
    UBaseType_t uxItemSize,
    uint8_t *pucQueueStorageBuffer,
    StaticQueue_t *pxQueueBuffer
);
删除队列
c
void vQueueDelete(QueueHandle_t xQueue);
  • xQueue:要删除的队列句柄

3.2 发送数据到队列

基本发送
c
BaseType_t xQueueSend(
    QueueHandle_t xQueue,
    const void *pvItemToQueue,
    TickType_t xTicksToWait
);
  • xQueue:队列句柄
  • pvItemToQueue:指向要发送数据的指针
  • xTicksToWait:阻塞等待时间(如队列已满)
  • 返回:pdPASS表示成功,errQUEUE_FULL表示失败

xQueueSendToBack()xQueueSend()功能相同。

发送到队列前端
c
BaseType_t xQueueSendToFront(
    QueueHandle_t xQueue,
    const void *pvItemToQueue,
    TickType_t xTicksToWait
);
  • 发送数据到队列前端,使其成为下一个被读取的项目
强制发送(覆盖)
c
BaseType_t xQueueOverwrite(
    QueueHandle_t xQueue,
    const void *pvItemToQueue
);
  • 如果队列已满,覆盖最旧的数据
  • 主要用于长度为1的队列(邮箱)

3.3 从队列接收数据

基本接收
c
BaseType_t xQueueReceive(
    QueueHandle_t xQueue,
    void *pvBuffer,
    TickType_t xTicksToWait
);
  • xQueue:队列句柄
  • pvBuffer:接收数据的缓冲区指针
  • xTicksToWait:阻塞等待时间(如队列为空)
  • 返回:pdPASS表示成功,errQUEUE_EMPTY表示失败
  • 从队列头部读取消息,并删除消息
查看但不取出
c
BaseType_t xQueuePeek(
    QueueHandle_t xQueue,
    void *pvBuffer,
    TickType_t xTicksToWait
);
  • 查看队列中的数据但不从队列中移除

3.4 中断中使用队列

从中断发送
c
BaseType_t xQueueSendFromISR(
    QueueHandle_t xQueue,
    const void *pvItemToQueue,
    BaseType_t *pxHigherPriorityTaskWoken
);
c
BaseType_t xQueueSendToFrontFromISR(
    QueueHandle_t xQueue,
    const void *pvItemToQueue,
    BaseType_t *pxHigherPriorityTaskWoken
);
c
BaseType_t xQueueSendToBackFromISR(
    QueueHandle_t xQueue,
    const void *pvItemToQueue,
    BaseType_t *pxHigherPriorityTaskWoken
);
  • pxHigherPriorityTaskWoken:如设为pdTRUE,表示此操作唤醒了更高优先级任务,需要进行任务切换
从中断接收
c
BaseType_t xQueueReceiveFromISR(
    QueueHandle_t xQueue,
    void *pvBuffer,
    BaseType_t *pxHigherPriorityTaskWoken
);
中断中覆盖写入
c
BaseType_t xQueueOverwriteFromISR(
    QueueHandle_t xQueue,
    const void *pvItemToQueue,
    BaseType_t *pxHigherPriorityTaskWoken
);

3.5 队列信息查询

c
UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);  // 获取队列中消息数
UBaseType_t uxQueueSpacesAvailable(QueueHandle_t xQueue);  // 获取队列中空闲空间数
BaseType_t xQueueIsQueueEmptyFromISR(QueueHandle_t xQueue);  // 中断中判断队列是否为空
BaseType_t xQueueIsQueueFullFromISR(QueueHandle_t xQueue);   // 中断中判断队列是否已满

3.6 队列重置

c
BaseType_t xQueueReset(QueueHandle_t xQueue);  // 清空队列内的所有消息

小实验7

  1. 创建队列queue;
  2. 动态创建三个任务:task1、task2、task3,三个任务作用如下:
  • task1:以1000ms频率闪烁LED1,表示系统正常运行
  • task2:接收队列queue的数据;
  • task3:检测按键
  1. 检测到按键按下,则往队列queue发送当前按键值;

小实验8

与小实验7类似,不同之处在于:

检测到按键1按下,则往queue传递大数据。

七、信号量概述

在多任务嵌入式系统中,随着任务数量的增加,各个任务之间必然会存在着协作与竞争的关系。例如,多个任务可能需要访问同一个硬件资源,或者一个任务产生的数据需要被另一个任务消费。这就需要引入信号量这一重要的同步机制。

信号量是操作系统中一种经典的同步控制机制,它的本质是一种特殊的变量,用于在多任务环境中实现任务之间的同步和互斥。在FreeRTOS中,信号量是基于队列实现的一种特殊数据结构。与普通队列不同,信号量的队列项长度为零,也就是说,信号量不传递实际的数据,而只是用计数值表示资源的可用性或事件的发生次数。

1. 信号量的基本操作

信号量的基本操作包括"获取"(take)和"释放"(give)两种。当任务获取信号量时,信号量的计数值减1;当任务释放信号量时,计数值加1。如果计数值为0,任何试图获取信号量的任务都将被阻塞,直到其他任务或中断释放信号量,使计数值增加。这种简单而强大的机制可以解决多种多样的同步问题。

2. 信号量的应用场景

在现代嵌入式系统设计中,信号量的应用十分广泛。当多个任务需要共享访问某个硬件资源时,如串口或SPI总线,可以使用信号量确保同一时刻只有一个任务能够访问该资源,避免数据混乱。另一方面,当系统中的一个任务需要等待特定事件发生后才能继续执行时,可以利用信号量实现这种条件等待的逻辑。

FreeRTOS提供了多种类型的信号量,包括二值信号量、互斥量和计数信号量,以满足不同场景下的需求。二值信号量主要用于任务间的同步;互斥量带有优先级继承机制,主要用于防止优先级反转问题;而计数信号量则允许多个资源的管理或多个事件的计数。

八、二值信号量

1. 二值信号量概念

1.1 什么是二值信号量?

二值信号量是一种特殊的同步机制,只有 0(无效)1(有效) 两种状态。它类似于现实生活中的“通行证”,任务需要持有该“通行证”才能执行特定操作。 • 本质:长度为1、消息大小为0的特殊消息队列。 • 特点:不关心队列中的具体数据,只关注队列是否为空(0)或满(1)。

1.2 核心作用

实现 任务与任务任务与中断 之间的 同步(如事件触发、数据就绪通知)。 示例:串口接收数据时,任务无需轮询,通过信号量等待中断通知数据到达。

while(1)
{
	if(flag == 1)
		//do something
	else if(flag == 0)
		//do something else
}

2. 工作机制

2.1 同步流程(以串口接收为例)

任务A(接收任务)               中断(数据到达)
├─ 等待信号量(阻塞)            ──→ 释放信号量
└─ 收到信号量→处理数据           ←─ 触发中断(任务切换)

2.2 关键特性

优先级继承机制缺失:与互斥信号量不同,可能导致优先级翻转问题(需用户自行处理)。 • 非阻塞与阻塞: • 任务可设置阻塞时间(如xTicksToWait),超时后自动恢复。 • 中断中释放信号量时需使用xSemaphoreGiveFromISR,避免阻塞中断。

3. API函数详解

3.1 创建二值信号量

c
SemaphoreHandle_t xSemaphoreCreateBinary(void);

返回值:成功返回句柄,失败返回NULL(内存不足)。 • 初始化状态:默认无效(0),需手动释放。

3.2 释放信号量

c
// 任务级释放
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
// 中断级释放
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);

参数pxHigherPriorityTaskWoken标记是否需要任务切换。 • 返回值pdPASS(成功)或errQUEUE_FULL(失败)。

3.3 获取信号量

c
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime);

阻塞时间xBlockTime可设为portMAX_DELAY(无限等待)或具体节拍数。 • 返回值pdTRUE(成功获取)或pdFALSE(超时)。

小实验9

  1. 创建二值信号量semphore;
  2. 动态创建二个任务:task1、task2,两个任务作用如下:
  • task1:以1000ms频率闪烁LED1,表示系统正常运行
  • task2:检测按键
  1. 检测到按键1按下,则释放二值信号量;检测到按键2按下,则获取二值信号量。

九、计数型信号量

1. 计数型信号量概念

1.1 什么是计数型信号量?

计数型信号量是一种允许多任务共享资源的同步机制,其计数值范围为 0 到设定的最大值。 • 本质:长度为N(N≥1)、消息大小为0的特殊消息队列。 • 特点:计数值表示可用资源数量或事件积累次数,不同于二值信号量的0/1二元状态。

1.2 核心作用

资源管理:跟踪有限资源的可用数量(如停车场车位、内存块)。 • 事件计数:记录事件发生的次数,供任务批量处理(如多次按键触发)。

2. 工作机制

2.1 资源管理场景(以停车场为例)

初始化:创建最大计数值为100的计数信号量,初始值设为100(总车位数量)。 • 车辆进入:任务调用xSemaphoreTake获取信号量,计数值减1;若计数值为0,任务阻塞。 • 车辆离开:任务调用xSemaphoreGive释放信号量,计数值加1,唤醒等待任务。

2.2 事件计数场景(如按键触发)

中断触发:每次按键中断调用xSemaphoreGiveFromISR释放信号量,计数值加1。 • 任务处理:任务循环调用xSemaphoreTake获取信号量,批量处理累计事件(如处理10次按键事件)。

2.3 底层实现原理

• 基于消息队列结构,队列长度对应最大计数值,成员变量uxMessagesWaiting存储当前计数值。 • 获取信号量即“消费队列项”,释放信号量即“生产队列项”,但无需传递实际数据。

3. API函数详解

3.1 创建计数型信号量

c
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);

参数: • uxMaxCount:最大计数值(如停车场总车位)。 • uxInitialCount:初始值(资源管理设为总资源数,事件计数设为0)。 • 返回值:成功返回句柄,失败返回NULL(内存不足)。

3.2 获取信号量

c
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime);

行为:计数值减1,若为0则任务阻塞。 • 超时xBlockTime设为portMAX_DELAY表示无限等待。

3.3 释放信号量

c
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);  // 任务级
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);  // 中断级

限制:若计数值已达最大值,释放失败返回errQUEUE_FULL。 • 中断安全:使用xSemaphoreGiveFromISR避免阻塞中断,并通过portYIELD_FROM_ISR触发任务切换。

小实验10

  1. 创建二值信号量count_semphore_handle,最大计数值为5,初始值为0。
  2. 动态创建二个任务:task1、task2,两个任务作用如下:
  • task1:以1000ms频率闪烁LED1,表示系统正常运行
  • task2:检测按键
  1. 检测到按键1按下,则释放计数型信号量;检测到按键2按下,则获取计数型信号量。

十、互斥信号量

1. 优先级翻转

1.1 优先级翻转的定义

优先级翻转(Priority Inversion)是多任务实时操作系统中一种典型的资源竞争问题,表现为高优先级任务因等待低优先级任务占用的资源而被阻塞,而中等优先级任务在此期间抢占执行,导致系统任务调度顺序违背预设的优先级规则。这种现象可能严重破坏系统的实时性,尤其是在关键任务场景中(如工业控制、航空航天)。

1.2 典型场景与发生条件

假设存在三个任务,优先级排序为 H > M > L(H最高,L最低):

  1. 初始状态:任务L占用共享资源(如互斥锁、信号量),任务H和M处于挂起状态等待事件触发。
  2. 事件触发:任务H的事件发生,进入就绪态并抢占CPU。当任务H尝试获取资源时,因资源被任务L占用而阻塞
  3. 中等优先级任务介入:此时任务M的事件触发,因其优先级高于任务L,开始执行。任务M无需访问共享资源,持续占用CPU,导致任务H被迫等待任务M和任务L执行完毕。

最终结果:高优先级任务H的响应时间被中优先级任务M和低优先级任务L共同拉长,形成优先级翻转。

1.3 优先级翻转的危害

实时性破坏:高优先级任务无法及时响应,可能导致系统超时或功能失效(如传感器数据丢失)。 • 资源浪费:中等优先级任务无意义抢占CPU,降低系统效率。 • 死锁风险:若多个任务形成循环等待资源,可能引发死锁。

1.4 FreeRTOS的解决方案:优先级继承

FreeRTOS通过互斥信号量(Mutex)的优先级继承机制缓解优先级翻转问题: • 核心原理:当高优先级任务因资源被低优先级任务占用而阻塞时,低优先级任务的优先级被临时提升至高优先级任务的级别,使其尽快释放资源,避免被中等优先级任务抢占。

具体流程

  1. 任务L占用互斥锁。
  2. 任务H请求互斥锁时阻塞,触发优先级继承机制,任务L的优先级提升至任务H的级别。
  3. 任务L执行期间,任务M无法抢占,任务L释放锁后优先级恢复。
  4. 任务H获得锁并执行,任务M恢复原有调度顺序。

1.5 优先级继承的局限性

不完全解决翻转:仅缩短高优先级任务的阻塞时间,无法完全消除(如任务L本身执行时间过长)。 • 中断中不可用:互斥信号量不能在中断服务程序(ISR)中使用,需配合二值信号量或直接处理。

小实验11

编程演示优先级翻转现象。

2. 互斥信号量概念

2.1 什么是互斥信号量?

互斥信号量是一种特殊的二值信号量,用于保护共享资源,确保同一时刻仅有一个任务能访问临界资源。其核心特性包括: • 所有权机制:只有获取信号量的任务才能释放它,避免资源被错误释放。 • 优先级继承:当高优先级任务因资源被低优先级任务占用而阻塞时,系统会临时提升低优先级任务的优先级,缓解优先级翻转问题。

互斥量的核心设计原则是“谁获取,谁释放”。当一个任务获取互斥量后,它成为该互斥量的“持有者”(holder),理论上只有持有者能释放它。这种机制旨在保护临界资源,避免数据竞争或逻辑错误。FreeRTOS的互斥量在代码实现上未强制要求必须由持有者释放。即使任务A获取了互斥量,任务B仍可调用xSemaphoreGive()释放该互斥量,且操作会成功。

2.2 与二值信号量的区别

特性互斥信号量二值信号量
用途资源保护任务/中断同步
所有权(理认上)必须由获取者释放任何任务均可释放
优先级继承支持,防止优先级翻转不支持
适用场景硬件资源(如串口)、共享数据事件通知(如中断触发任务)

3. 工作机制

3.1 优先级翻转问题

场景:低优先级任务(L)占用资源 → 中优先级任务(M)抢占执行 → 高优先级任务(H)因资源被L占用而阻塞。 • 结果:H被迫等待L和M执行完成,违背实时性要求。

3.2 优先级继承机制

流程

  1. 当H请求被L占用的资源时,L的优先级临时提升至与H相同。
  2. L释放资源后,优先级恢复原状,H立即执行。 • 意义:将H的阻塞时间从“L+M执行时间”缩短为“L执行时间”,大幅降低优先级翻转的影响。

4. API函数详解

4.1 创建互斥信号量

c
SemaphoreHandle_t xSemaphoreCreateMutex(void);

要求:将宏configUSE_MUTEXES置1。 • 返回值:成功返回句柄,失败返回NULL(内存不足)。 • 初始化状态:默认有效(计数值1),无需手动释放。

4.2 获取信号量

c
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime);

阻塞时间:若资源被占用,任务进入阻塞态,最长等待xBlockTimeportMAX_DELAY表示无限等待)。 • 返回值pdTRUE(成功获取)或pdFALSE(超时)。

4.3 释放信号量

c
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);

限制:必须由获取信号量的任务释放,否则导致未定义行为。

4.4 删除信号量

c
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);

适用场景:动态创建的信号量在不再需要时需手动删除,释放内存。

5. 典型应用场景

5.1 硬件资源保护

案例:多个任务共享串口发送数据。

c
void UART_SendTask(void *pvParameters) {
    while (1) {
        if (xSemaphoreTake(uartMutex, portMAX_DELAY) == pdTRUE) {
            UART_SendData(buffer);  // 发送数据
            xSemaphoreGive(uartMutex);
        }
        vTaskDelay(100);
    }
}

5.2 共享数据结构访问

案例:多任务操作全局链表或计数器,防止数据竞争。

5.3 文件系统操作

案例:嵌入式文件系统中,确保同一时刻仅一个任务写入Flash。

小实验12

使用互斥量缓解优先级翻转现象。

十一、队列集

1. 队列集介绍

1.1 核心功能

队列集(Queue Set)是FreeRTOS中用于统一管理多个队列或信号量的机制,允许任务同时监听多个事件源(如队列数据到达、信号量触发等),任一事件触发即可唤醒任务。其核心作用包括:

减少任务轮询开销:任务无需逐个检查队列/信号量,通过事件驱动机制提升效率。 • 简化多事件处理逻辑:适用于需要响应多种异步事件的场景(如多设备输入、混合数据源同步)。 • 支持动态管理:可在运行时动态添加或移除队列/信号量。

1.2 与传统轮询的对比

对比维度传统轮询方式队列集方式
CPU资源消耗高(需循环检查每个队列)低(仅在事件触发时唤醒任务)
实时性延迟高(需遍历所有队列)延迟低(事件即时触发)
代码复杂度高(需手动管理多个队列的监听逻辑)低(统一监听接口)

1.3 适用场景

多设备输入整合:如同时监听触摸屏、按键、传感器数据。 • 混合事件处理:需同时处理异步消息(网络数据)和同步信号(中断触发)。 • 复杂同步需求:多个任务需协作时,通过队列集实现高效同步。

2. 队列集使用流程

2.1 启用队列集功能

FreeRTOSConfig.h中启用队列集:

c
#define configUSE_QUEUE_SETS 1  // 必须设置为1

2.2 创建队列集

c
QueueSetHandle_t xQueueSet = xQueueCreateSet(uxTotalLength);

参数规则uxTotalLength = 所有被监听队列/信号量的最大容量之和。 • 示例:若监听2个队列(长度分别为3、2)和1个信号量(长度1),则uxTotalLength = 3+2+1=6。 • 错误后果:长度不足会导致事件丢失或任务无法唤醒。

2.3 添加队列/信号量到队列集

c
xQueueAddToSet(xQueueHandle, xQueueSet);   // 添加队列
xQueueAddToSet(xSemaphoreHandle, xQueueSet); // 添加信号量

关键限制

  1. 队列必须为空:若队列已有数据,添加会失败(返回pdFAIL)。
  2. 仅属于一个队列集:同一队列/信号量不可同时加入多个队列集。

2.4 任务监听与处理

c
QueueSetMemberHandle_t xActivated = xQueueSelectFromSet(xQueueSet, xBlockTime);

阻塞机制:若队列集无事件,任务进入阻塞状态,最长等待xBlockTime(单位:Tick)。 • 返回值处理: • 返回NULL:超时或无事件。 • 返回有效句柄:需根据句柄类型(队列或信号量)执行对应操作。

3. 队列集API函数详解

3.1 创建队列集

函数原型

c
QueueSetHandle_t xQueueCreateSet(UBaseType_t uxEventQueueLength);

功能: 创建一个新的队列集,用于统一监听多个队列或信号量的事件。

参数: • uxEventQueueLength:队列集的总容量,必须等于所有被监听队列/信号量的最大长度之和示例:若监听2个队列(长度分别为3和5)和1个信号量(长度1),则 uxEventQueueLength = 3+5+1=9

返回值: • 成功:返回队列集句柄(QueueSetHandle_t)。 • 失败:返回 NULL(通常因内存不足或配置未启用队列集功能)。

注意事项

  1. 严格计算容量:容量不足会导致事件丢失,任务无法及时唤醒。
  2. 必须启用配置:在 FreeRTOSConfig.h 中设置 #define configUSE_QUEUE_SETS 1
  3. 内存分配:队列集占用内存由FreeRTOS自动分配,需确保系统有足够堆空间。

3.2 添加队列/信号量到队列集

函数原型

c
BaseType_t xQueueAddToSet(QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet);

功能: 将队列或信号量添加到队列集中,使其成为被监听的对象。

参数: • xQueueOrSemaphore:待添加的队列或信号量句柄。 • xQueueSet:目标队列集的句柄。

返回值: • pdPASS:添加成功。 • pdFAIL:添加失败(队列非空、已被其他队列集占用或句柄无效)。

注意事项

  1. 队列必须为空:若队列已有数据,需先调用 xQueueReceive() 清空才能添加。
  2. 独占性:同一队列/信号量不可同时加入多个队列集。
  3. 动态添加:可在运行时动态添加,但需注意实时性影响。

3.3 从队列集移除成员

函数原型

c
BaseType_t xQueueRemoveFromSet(QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet);

功能: 将队列或信号量从队列集中移除,停止对其监听。

参数: • xQueueOrSemaphore:待移除的队列或信号量句柄。 • xQueueSet:队列集句柄。

返回值: • pdPASS:移除成功。 • pdFAIL:移除失败(队列仍有未处理数据或句柄无效)。

注意事项

  1. 清空数据:移除前需确保队列/信号量无未处理事件。
  2. 中断安全:禁止在中断服务例程(ISR)中直接调用此函数。

3.4 监听队列集事件

函数原型

c
QueueSetMemberHandle_t xQueueSelectFromSet(QueueSetHandle_t xQueueSet, TickType_t xTicksToWait);

功能: 阻塞任务,直到队列集中任一成员触发事件(如队列收到数据或信号量被释放)。

参数: • xQueueSet:队列集句柄。 • xTicksToWait:最大阻塞时间(单位:Tick),设为 portMAX_DELAY 表示永久阻塞。

返回值: • 有效句柄:触发事件的队列或信号量句柄。 • NULL:超时或无事件发生。

注意事项

  1. 事件处理:返回句柄后需立即读取队列数据或获取信号量,否则可能被其他任务抢占。
  2. 超时处理:需检查返回值是否为 NULL,避免空指针操作。
  3. 任务优先级:高优先级任务可能抢占事件处理,需合理设计任务优先级。

示例

c
QueueSetMemberHandle_t xActivated = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
if (xActivated == xQueueA) {
    int data;
    xQueueReceive(xQueueA, &data, 0);  // 立即读取数据
} else if (xActivated == xSemaphore) {
    xSemaphoreTake(xSemaphore, 0);     // 立即获取信号量
}

3.5 中断安全版监听函数

函数原型

c
QueueSetMemberHandle_t xQueueSelectFromSetFromISR(QueueSetHandle_t xQueueSet);

功能: 在中断服务例程(ISR)中检查队列集是否有事件触发。

参数: • xQueueSet:队列集句柄。

返回值: • 有效句柄:触发事件的队列或信号量句柄。 • NULL:无事件发生。

注意事项

  1. 不可阻塞:ISR中禁止使用阻塞操作,此函数仅检查当前状态。
  2. 任务切换:若需要唤醒任务处理事件,需手动调用 portYIELD_FROM_ISR()
  3. 数据读取:ISR中无法直接处理队列数据,通常仅标记事件并唤醒任务处理。

小实验13

  1. 创建一个队列集queueset,一个队列queue,一个二值信号量semphr,并且将queue和semphr添加到queueset里;
  2. 动态创建两个任务:task1、task2,两个任务作用如下:
  • task1:检测按键,当按键1按下时,往queue写入数据;当按键2按下时,释放semphr。
  • task2:监听queueset。

十二、事件标志组

1. 事件标志组介绍

1.1 核心概念

事件标志组(Event Group)是FreeRTOS中用于多任务同步与通信的机制,通过位掩码(Bitmask)管理多个独立事件的状态: • 事件位(Event Bit):每个位(bit)表示一个事件是否发生,例如:

BIT0:按键K1按下

BIT1:传感器数据就绪

BIT2:网络连接成功

• 事件组(Event Group):由多个事件位组成的整数变量,FreeRTOS中默认支持24个事件位(低24位有效,高8位保留用于控制)。

1.2 核心作用

  1. 多事件同步:支持“与”(所有指定事件位触发)或“或”(任一事件位触发)逻辑。
  2. 高效资源利用:相比信号量或队列,事件标志组仅需少量内存(一个EventBits_t变量)。
  3. 中断与任务协作:可在中断中设置事件位,任务异步响应。

1.3 与信号量的对比

特性事件标志组信号量
同步逻辑支持多事件位逻辑组合单一计数器增减
资源占用低(仅需1个变量)较高(需独立数据结构)
适用场景多事件协同、复杂条件触发单一资源管理或简单同步

适用场景举例: • 多传感器数据就绪检测:需同时等待温度、湿度传感器数据。

• 任务链同步:任务A完成后触发任务B,任务B完成后触发任务C。

2. 事件标志组常用API详解

2.1 创建事件标志组

函数原型:

c
EventGroupHandle_t xEventGroupCreate(void);

功能:动态创建事件标志组,返回其句柄。 返回值: • 成功:事件标志组句柄(EventGroupHandle_t)。

• 失败:NULL(内存不足或未启用事件组功能)。

注意事项: • 需在FreeRTOSConfig.h中启用configUSE_EVENT_GROUPS = 1

2.2 设置事件位

函数原型:

c
EventBits_t xEventGroupSetBits(  
    EventGroupHandle_t xEventGroup,  
    const EventBits_t uxBitsToSet  
);

功能:在任务中设置指定事件位(置1)。 参数: • xEventGroup:事件标志组句柄。

uxBitsToSet:需设置的事件位掩码(如BIT0 | BIT2)。

返回值:设置后的事件组值(可用于调试)。 注意事项: • 若需在中断中设置事件位,需使用xEventGroupSetBitsFromISR()

2.3 等待事件位

函数原型:

c
EventBits_t xEventGroupWaitBits(  
    const EventGroupHandle_t xEventGroup,  
    const EventBits_t uxBitsToWaitFor,  
    const BaseType_t xClearOnExit,  
    const BaseType_t xWaitForAllBits,  
    TickType_t xTicksToWait  
);

功能:阻塞任务,直到指定事件位触发或超时。 参数: • uxBitsToWaitFor:等待的事件位掩码。

xClearOnExitpdTRUE表示触发后自动清零事件位。

xWaitForAllBitspdTRUE表示需所有位触发(“与”逻辑),pdFALSE表示任一触发(“或”逻辑)。

返回值:触发的事件位掩码(若超时返回当前事件组值)。 示例:

c
// 等待BIT0和BIT1均触发(“与”逻辑)  
EventBits_t bits = xEventGroupWaitBits(group, BIT0 | BIT1, pdTRUE, pdTRUE, portMAX_DELAY);

2.4 清除事件位

函数原型:

c
EventBits_t xEventGroupClearBits(  
    EventGroupHandle_t xEventGroup,  
    const EventBits_t uxBitsToClear  
);

功能:在任务中清除指定事件位(置0)。 参数: • uxBitsToClear:需清除的事件位掩码。

返回值:清除前的事件组值。 注意事项: • 中断中需使用xEventGroupClearBitsFromISR()

2.5 中断安全API

2.5.1 设置事件位:xEventGroupSetBitsFromISR() 函数原型:

c
BaseType_t xEventGroupSetBitsFromISR(
    EventGroupHandle_t xEventGroup,
    const EventBits_t uxBitsToSet,
    BaseType_t *pxHigherPriorityTaskWoken
);

功能: 在中断中设置指定事件位为1,通过延迟处理机制将操作转发给RTOS守护任务(Daemon Task),避免在ISR中直接操作事件组。

参数: • xEventGroup:事件组句柄。

uxBitsToSet:待设置的事件位掩码(如0x03表示设置bit0和bit1)。

pxHigherPriorityTaskWoken:记录是否需要触发任务切换(若守护任务优先级高于当前任务,则设为pdTRUE)。

返回值: • pdPASS:命令成功发送至守护任务队列。

pdFAIL:队列已满,操作失败。

注意事项:

  1. 非阻塞操作:不直接在ISR中修改事件组,而是通过定时器命令队列异步处理。
  2. 上下文切换:若pxHigherPriorityTaskWoken返回pdTRUE,需在中断退出前调用portYIELD_FROM_ISR()触发任务切换。
  3. 队列容量:确保定时器命令队列有足够空间,否则可能丢失事件。

2.5.2 清除事件位:xEventGroupClearBitsFromISR() 函数原型:

c
BaseType_t xEventGroupClearBitsFromISR(
    EventGroupHandle_t xEventGroup,
    const EventBits_t uxBitsToClear
);

功能: 在中断中清除指定事件位(置0),操作同样通过守护任务异步处理。

参数: • xEventGroup:事件组句柄。

uxBitsToClear:待清除的事件位掩码。

返回值: • pdPASS:命令成功发送至队列。

pdFAIL:队列已满或参数无效。

注意事项:

  1. 内存安全:与xEventGroupSetBitsFromISR类似,需依赖守护任务处理实际清除操作。
  2. 避免竞态条件:清除操作可能被其他任务或中断打断,需通过返回值判断操作结果。

2.5.3 获取事件位:xEventGroupGetBitsFromISR() 函数原型:

c
EventBits_t xEventGroupGetBitsFromISR(
    EventGroupHandle_t xEventGroup
);

功能: 在中断中直接读取当前事件组的值(无需通过守护任务)。

参数: • xEventGroup:事件组句柄。

返回值: 当前事件组的位掩码值(仅低24位有效)。

注意事项:

  1. 无阻塞风险:此函数仅读取当前状态,不修改事件组,可直接在ISR中使用。
  2. 实时性:返回值为调用时刻的瞬时值,可能因并发操作而后续变化。

小实验14

  1. 创建一个事件标志组eventgroup;
  2. 动态创建两个任务:task1、task2,两个任务作用如下:
  • task1:检测按键,当按键1按下时,将事件标志组的bit0位置1;当按键2按下时,将事件标志组的bit1位置1。
  • task2:监听事件标志组,当bit0和bit1同时为1时,打印log。

十三、任务通知

1. 任务通知简介

1.1 核心概念

任务通知是FreeRTOS中一种轻量级任务间通信机制,允许一个任务或中断直接向另一个任务发送事件或数据,无需创建队列、信号量等中间对象。每个任务内部自带一个32位的“通知值”(ulNotifiedValue)和一个“通知状态”(ucNotifyState),通过操作这两个变量实现快速同步。

通俗比喻: 任务通知就像直接给同事发微信消息,而传统队列/信号量则像通过公共邮箱传递信件。前者直接、快速,后者需要中间环节。

1.2 优势与劣势

优势劣势
速度快:比队列快45%,比信号量快15倍。单接收方:只能一对一通信,无法广播给多个任务。
省内存:每个任务仅需8字节额外内存。无数据缓冲:只能保存一个32位值,新数据可能覆盖旧值。
多功能:可模拟信号量、事件组、队列。中断限制:任务无法向中断发送通知。
直接操作:无需创建中间对象,代码更简洁。发送方无法阻塞:若接收方未处理通知,发送方只能立即返回失败。

适用场景举例: • 中断响应:ISR快速通知任务处理数据。

• 轻量同步:替代二值信号量(如按键检测)。

• 单次数据传递:传递32位整数或指针(如传感器读数)。

2. 任务通知值与通知状态

2.1 通知值(Notification Value)

• 定义:每个任务内部的32位无符号整数,用于存储通知内容。

• 操作方式:

• 按位操作:类似事件组(如设置bit0表示按键按下)。

• 数值操作:覆盖、递增或条件更新(如模拟计数器)。

• 示例:

c
#define DATA_READY_BIT (1 << 0)
xTaskNotify(xTaskHandle, DATA_READY_BIT, eSetBits); // 设置bit0

2.2 通知状态(Notification State)

• 三种状态:

  1. 未等待通知(taskNOT_WAITING_NOTIFICATION):初始状态,任务未等待通知。
  2. 等待通知(taskWAITING_NOTIFICATION):任务调用等待函数(如xTaskNotifyWait)进入阻塞。
  3. 已接收通知(taskNOTIFICATION_RECEIVED):通知到达但未被处理(类似“待处理邮件”)。

• 状态转换:

• 任务调用xTaskNotifyWait() → 进入“等待通知”状态。

• 其他任务发送通知 → 状态变为“已接收通知”,任务解除阻塞。

3. 任务通知常用API函数

3.1 发送通知函数

(1) 基础发送:xTaskNotify()

函数原型:

c
BaseType_t xTaskNotify(
    TaskHandle_t xTaskToNotify,  // 目标任务句柄
    uint32_t ulValue,            // 通知值
    eNotifyAction eAction        // 更新方式
);

参数详解: • xTaskToNotify:目标任务的句柄,可通过xTaskCreate()xTaskGetHandle()获取。

ulValue:根据eAction决定用途的32位值:

• 当eAction = eSetBits时,按位设置(如0x03表示设置bit0和bit1)。

• 当eAction = eSetValueWithOverwrite时,直接覆盖通知值为ulValue

• 其他模式下可能被忽略。

eAction:枚举类型,定义通知值的更新逻辑:

• eNoAction:仅触发通知,不修改通知值。

• eSetBits:按位或操作(类似事件组)。

• eIncrement:通知值自增1(模拟计数信号量)。

• eSetValueWithOverwrite:强制覆盖通知值。

• eSetValueWithoutOverwrite:仅在通知未被处理时覆盖。

返回值: • pdPASS:通知成功发送(默认返回值)。

pdFAIL:仅当eAction = eSetValueWithoutOverwrite且目标任务未处理上一次通知时返回。

(2) 简化发送:xTaskNotifyGive()

函数原型:

c
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);

参数: • xTaskToNotify:目标任务的句柄。

返回值: • pdPASS:始终返回成功(本质是xTaskNotify(..., eIncrement)的简化版)。

说明:专用于模拟计数信号量,通知值自增1,无数据传递需求时更高效。

(3) 中断安全版本:

xTaskNotifyFromISR() 函数原型:

c
BaseType_t xTaskNotifyFromISR(
    TaskHandle_t xTaskToNotify,
    uint32_t ulValue,
    eNotifyAction eAction,
    BaseType_t *pxHigherPriorityTaskWoken
);

新增参数: • pxHigherPriorityTaskWoken

• 输入前需初始化为pdFALSE

• 若发送通知导致目标任务解除阻塞且优先级更高,则设为pdTRUE,需在中断退出前调用portYIELD_FROM_ISR()触发上下文切换。

返回值:与xTaskNotify()一致。

vTaskNotifyGiveFromISR() 函数原型:

c
void vTaskNotifyGiveFromISR(
    TaskHandle_t xTaskToNotify,
    BaseType_t *pxHigherPriorityTaskWoken
);

参数与返回值: • 参数与xTaskNotifyFromISR()一致,但无返回值(专用于中断中递增通知值)。

3.2 接收通知函数

(1) 信号量模式:ulTaskNotifyTake()

函数原型:

c
uint32_t ulTaskNotifyTake(
    BaseType_t xClearCountOnExit,  // 退出时清零或减1
    TickType_t xTicksToWait        // 阻塞超时时间
);

参数详解: • xClearCountOnExit

• pdTRUE:清零通知值(模拟二值信号量)。

• pdFALSE:通知值减1(模拟计数信号量)。

xTicksToWait:最大阻塞时间,portMAX_DELAY表示无限等待。

返回值: • 非零值:接收成功,返回操作前的通知值。

0:超时或通知未到达。

(2) 通用模式:xTaskNotifyWait()

函数原型:

c
BaseType_t xTaskNotifyWait(
    uint32_t ulBitsToClearOnEntry,  // 等待前清除的位
    uint32_t ulBitsToClearOnExit,   // 退出前清除的位
    uint32_t *pulNotificationValue, // 接收到的通知值
    TickType_t xTicksToWait
);

参数详解: • ulBitsToClearOnEntry:进入等待前对通知值按位清零(如0xFF清空低8位)。

ulBitsToClearOnExit:退出前对通知值按位清零(如0x01清空bit0)。

pulNotificationValue:存储接收到的原始通知值(可传NULL忽略)。

返回值: • pdTRUE:成功接收到通知。

pdFALSE:超时或等待失败。

小实验15

使用任务通知模拟信号量

小实验16

使用任务通知模拟邮箱

小实验17

使用任务通知模拟事件标志组

十四、定时器

从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可自定义定时器的周期。

1. 软件定时器介绍

1.1 硬件定时器与软件定时器的核心差异

对比维度硬件定时器软件定时器
实现原理依赖芯片硬件模块(如STM32的TIMx)基于系统滴答中断(SysTick)和任务调度机制
精度高(可达纳秒级)较低(依赖系统节拍,通常毫秒级)
资源占用占用硬件外设,数量固定仅占用内存,可动态创建多个
触发方式中断触发,实时性强任务上下文触发(回调函数在守护任务中执行)
适用场景高精度控制(PWM、输入捕获)周期性任务、超时检测、事件通知等非实时场景

通俗理解:

硬件定时器类似机械闹钟,精准但数量有限;软件定时器像手机里的虚拟闹钟,灵活但精度稍低。

1.2 软件定时器的优缺点

优点:

• 省硬件资源:不占用硬件外设,仅需内存即可创建;

• 灵活扩展:数量仅受内存限制,可动态管理;

• 多功能性:支持单次、周期模式等。

缺点:

• 精度受限:依赖系统节拍(Tick),可能被高优先级任务打断;

• 回调限制:回调函数中不可调用阻塞API(如vTaskDelay());

• 无硬件中断:无法处理高实时性任务。

1.3 FreeRTOS软件定时器的核心特点

守护任务(Daemon Task)

系统自动创建的管理任务,优先级由configTIMER_TASK_PRIORITY配置,负责处理定时器命令队列和执行回调函数。守护任务类似“闹钟管理员”,统一管理所有定时器的触发逻辑。

• 守护任务优先级:需设为系统最高优先级之一(如configMAX_PRIORITIES-1),否则可能因任务调度延迟导致定时器超时回调延迟。

• 堆栈深度:通过configTIMER_TASK_STACK_DEPTH配置,需足够大以处理回调函数逻辑。

命令队列:

所有API操作(如启动、停止)通过队列发送命令,由守护任务统一处理,确保线程安全。

• 队列长度:由configTIMER_QUEUE_LENGTH定义,若队列满则新命令可能被阻塞或丢弃。

轻量化设计

每个定时器仅需约40字节内存,适合资源受限的嵌入式场景。

1.4 软件定时器的配置流程与注意事项

配置步骤:

  1. 启用定时器功能:在FreeRTOSConfig.h中设置:
    c
    #define configUSE_TIMERS 1                     // 启用软件定时器
    #define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1)  // 守护任务优先级
    #define configTIMER_QUEUE_LENGTH 10            // 命令队列长度
    #define configTIMER_TASK_STACK_DEPTH 1024      // 守护任务堆栈大小(单位:字)
  2. 创建定时器:通过xTimerCreate()指定周期、模式、回调函数等参数。

注意事项:

• 回调函数:必须短小精悍,避免阻塞操作;若需复杂操作,应通过任务通知或队列委托给其他任务。

• 中断安全:中断中操作定时器需使用FromISR版本API,并处理pxHigherPriorityTaskWoken标志触发任务切换。

1.5 软件定时器的状态与转换

休眠态(Dormant)

• 定时器创建后默认处于休眠态,需调用xTimerStart()激活。

• 单次定时器超时后自动进入休眠态,需手动重启。

运行态(Active)

• 定时器启动后开始计时,超时后根据模式决定是否自动重启。

• 状态转换示例:

◦ 单次定时器:`休眠态 → 启动 → 运行态 → 超时回调 → 休眠态`  

◦ 周期定时器:`休眠态 → 启动 → 运行态 → 超时回调 → 自动重启运行态`。

1.6 单次定时器与周期定时器的对比

类型触发逻辑典型场景
单次定时器触发一次后停止,需手动重启设备超时检测(如按键无操作自动休眠)
周期定时器自动重启计时,周期性触发回调函数心跳检测、LED闪烁、数据采集

示例:

• 单次定时器:用户设置10秒无操作进入休眠,超时后触发一次回调;

• 周期定时器:每1秒采集一次传感器数据并发送到云端。

2. 软件定时器常用API函数

2.1 创建定时器

核心作用:

动态分配内存并初始化定时器。

函数原型:

c
TimerHandle_t xTimerCreate(
    const char *pcTimerName,    // 定时器名称(调试用)
    TickType_t xTimerPeriod,    // 周期(单位:Tick)
    UBaseType_t uxAutoReload,   // 模式:pdTRUE(周期)/pdFALSE(单次)
    void *pvTimerID,            // 用户自定义ID(区分多个定时器)
    TimerCallbackFunction_t pxCallbackFunction  // 回调函数
);

参数详解:

pcTimerName:名称仅用于调试,不影响功能。

xTimerPeriod:可用pdMS_TO_TICKS(毫秒值)转换时间(如pdMS_TO_TICKS(1000)表示1秒)。

pvTimerID:用于多定时器共用回调函数时区分来源(如设置ID为1表示温度传感器,ID为2表示湿度传感器)。

返回值:

成功返回句柄,失败返回NULL(内存不足时可能发生)。

示例:

c
TimerHandle_t xTempTimer = xTimerCreate(
    "TempSensor", 
    pdMS_TO_TICKS(5000), 
    pdTRUE, 
    (void*)1, 
    vSensorCallback
);

2.2 启动定时器

核心作用:

启动或重启定时器。

函数原型:

c
BaseType_t xTimerStart(TimerHandle_t xTimer, TickType_t xTicksToWait);
BaseType_t xTimerStartFromISR(TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken);

参数说明:

xTicksToWait:命令队列满时的最大阻塞时间(通常设为0,非阻塞模式)。

pxHigherPriorityTaskWoken:中断版本需处理任务切换标志,若为pdTRUE需调用portYIELD_FROM_ISR()触发任务切换。

返回值:

pdPASS(成功)或pdFAIL(失败,如队列满且超时未发送)。

注意事项:

• 若定时器已在运行,xTimerStart()等效于xTimerReset(),即重新开始计时。

• 在启动调度器前调用xTimerStart(),定时器会在调度器启动后立即开始计时。

2.3 停止定时器

核心作用:

停止定时器运行。

函数原型:

c
BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xTicksToWait);
BaseType_t xTimerStopFromISR(TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken);

适用场景:

手动终止定时任务(如设备进入休眠模式)。

示例:

c
// 停止温度传感器定时器
if (xTimerStop(xTempTimer, 0) == pdPASS) {
    printf("定时器已停止\n");
}

2.4 修改定时周期

核心作用:

动态调整定时器周期。

函数原型:

c
BaseType_t xTimerChangePeriod(
    TimerHandle_t xTimer,       // 定时器句柄
    TickType_t xNewPeriod,      // 新周期(Tick)
    TickType_t xTicksToWait     // 阻塞时间
);

注意事项:

• 修改后立即生效,下一次计时从当前时刻开始。

• 若定时器正在运行,修改周期会重置当前计时。

示例:

将LED闪烁周期从1秒改为500ms:

c
xTimerChangePeriod(xLedTimer, pdMS_TO_TICKS(500), 0);

2.5 复位定时器

核心作用:

重新开始计时。

函数原型:

c
BaseType_t xTimerReset(TimerHandle_t xTimer, TickType_t xTicksToWait);
BaseType_t xTimerResetFromISR(TimerHandle_t xTimer, BaseType_t *pxHigherPriorityTaskWoken);

xTimerStart()的区别: • xTimerReset()强制从调用时刻开始重新计时,而xTimerStart()若定时器未运行则启动,若已运行则等效于复位。

适用场景:

按键操作后重置设备超时检测。

2.6 删除定时器

核心作用:

释放定时器内存。

函数原型:

c
void xTimerDelete(TimerHandle_t xTimer, TickType_t xTicksToWait);

注意事项:

删除后句柄失效,不可再操作。

2.7 查询定时器状态

核心作用:

查询软件定时器是否处于活动或休眠状态。

函数原型:

c
 BaseType_t xTimerIsTimerActive(TimerHandle_t xTimer);

参数:

  • xTimer

    被查询的定时器。

返回值:

  • 如果定时器处于休眠状态,将返回 pdFALSE。
  • 如果定时器处于活动状态,将返回 pdFALSE 以外的值。

3. 实战技巧与调试建议

3.1 回调函数设计原则

• 禁止阻塞:不可调用vTaskDelay()或访问带阻塞的队列/信号量。

• 快速执行:若需复杂操作(如发送大量数据),应通过任务通知或队列委托给其他任务。

错误示例:

c
void vBadCallback(TimerHandle_t xTimer) {
    vTaskDelay(100);  // 错误!阻塞操作会导致守护任务卡死
}

3.2 中断中操作定时器

示例:在按键中断中复位定时器:

c
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if (xTimerResetFromISR(xLedTimer, &xHigherPriorityTaskWoken) == pdPASS) {
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);  // 触发任务切换
    }
}

3.3 多定时器共享回调函数

c
// 创建两个定时器,共用回调函数
TimerHandle_t xTimer1 = xTimerCreate("Timer1", ..., (void*)1, vSharedCallback);
TimerHandle_t xTimer2 = xTimerCreate("Timer2", ..., (void*)2, vSharedCallback);

// 回调函数内区分ID
void vSharedCallback(TimerHandle_t xTimer) {
    uint32_t id = (uint32_t)pvTimerGetTimerID(xTimer);
    if (id == 1) {
        // 处理Timer1逻辑
    } else if (id == 2) {
        // 处理Timer2逻辑
    }
}

小实验18

  1. 创建两个定时器:timer1 -> 单次定时器;timer2 -> 周期定时器
  2. 创建一个任务task1,用于检测按键:
    • 按键1按下:如果timer1正在运行则停止timer1;若timer1停止则启动timer1;
    • 按键2按下:如果timer2正在运行则停止timer2;若timer2停止则启动timer2;

十五、低功耗

1. 低功耗模式简介

1.1 应用场景与硬件基础

低功耗设计是嵌入式系统的核心能力,直接影响设备续航与部署灵活性。以下场景需重点关注:

• 移动医疗设备(如血糖仪、心电图仪):需在待机时维持μA级电流,突发任务时快速响应。

• 工业无线传感器(如振动监测节点):部署在高温、密闭环境中,电池更换困难,需5年以上续航。

• 智能农业设备(如土壤墒情监测器):依赖太阳能+电池混合供电,需动态调整功耗策略。

1.2 STM32低功耗模式对比(以F4系列为例)

模式功耗范围唤醒时间数据保持适用场景
Sleep Mode1-3 mA<10 μs完整实时控制间隙(如PID调节)
Stop Mode10-50 μA200 μsSRAM保持周期性数据采集(如每5分钟)
Standby Mode0.5-2 μA5 ms仅备份域紧急事件监测(如烟雾报警)

硬件协同设计要点:

• 时钟树优化:进入低功耗前切换至HSI(内部RC时钟),关闭PLL可降低30%动态功耗

• 外设电源管理:通过RCC_AHB1PeriphClockCmd关闭未用外设时钟(如ADC/DMA)

• 引脚状态冻结:配置GPIO为模拟输入模式避免漏电流,STM32F4每个浮空引脚可能产生0.1μA漏电流

1.3 FreeRTOS适配策略

• 动态电压调节:配合STM32动态电压缩放(DVS)技术,在空闲时降低核心电压至1.2V

• 混合模式管理:将Sleep Mode与Tickless结合,实现多级功耗阶梯(如:短时空闲用Sleep,长时用Stop)

2. Tickless模式介绍

2.1 运行机制全解析

Tickless低功耗模式的本质是通过调用指令 WFI 实现睡眠模式!

任务运行时间统计实验中,可以看出,在整个系统的运行过程中,其实大部分时间是在执行空闲任务的。

空闲任务:是在系统中的所有其它任务都阻塞或被挂起时才运行的。

为了可以降低功耗,又不影响系统运行,该如何做?

可以在本该空闲任务执行的期间,让MCU 进入相应的低功耗模式;当其他任务准备运行的时候,唤醒MCU退出低功耗模式

  1. 空闲窗口预测: • 通过prvGetExpectedIdleTime()计算下一个任务唤醒时间xExpectedIdleTime

    • 算法核心:遍历所有阻塞任务的xTaskDelayUntil参数,找出最小剩余时间

  2. SysTick动态调整:

    c
    /* 计算重装载值 */
    ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG + 
                    ( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) );
    portNVIC_SYSTICK_LOAD_REG = ulReloadValue;

    • 启用LPTIM(低功耗定时器)突破SysTick 24位限制,支持长达36小时的休眠

  3. 中断管理策略: • 唤醒源分级:将RTC/Wakeup-Pin设为最高优先级(如抢占优先级0)

    • 虚假唤醒处理:通过eTaskConfirmSleepModeStatus()检测任务队列变化

2.2 时钟补偿黑科技

• 动态偏差修正:

c
if( ulCompleteTickPeriods > 0 ) {
    vTaskStepTick( ulCompleteTickPeriods );
}

• 采用RTC校准补偿晶振温漂(误差<±500ppm)

• 当实际休眠时间偏离预期10%时,触发自适应算法调整预测模型

2.3 性能实测数据

场景常规模式电流Tickless电流降幅
1秒周期任务15 mA3.2 mA78.6%
10分钟数据上报8.7 mA42 μA99.5%
(数据来源:STM32F429+FreeRTOS实测)

三、Tickless配置实战手册

3.1 FreeRTOSConfig.h关键配置

c
#define configUSE_TICKLESS_IDLE         1   // 启用低功耗处理
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2 // 最小休眠2个tick

3.2 预处理/后处理函数范例

c
#define configPRE_SLEEP_PROCESSING( x )         PreSleepProcessing()
#define configPOST_SLEEP_PROCESSING( x )        PostSleepProcessing()

// 进入低功耗前操作
void PreSleepProcessing(void) {
    HAL_RTCEx_DeactivateWakeUpTimer(&hrtc);  // 关闭原有RTC唤醒
    HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, ulExpectedIdleTime, RTC_WAKEUPCLOCK_CK_SPRE_16BITS);
    __HAL_RCC_GPIOA_CLK_DISABLE();  // 关闭GPIOA时钟
    HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);  // 进入Stop2模式
}

// 退出低功耗后恢复
void PostSleepProcessing(void) {
    SystemClock_Config();  // 重新配置主时钟
    HAL_RTCEx_ActivateWakeUpTimer(&hrtc); 
    __HAL_RCC_GPIOA_CLK_ENABLE();
}

小实验19

低功耗实验

十六、内存管理

1. FreeRTOS内存管理简介

1.1 核心功能与设计目标

FreeRTOS的内存管理模块是操作系统的核心组件,专为嵌入式实时系统优化,提供以下核心能力:

• 动态内存分配:允许任务在运行时按需申请内存(如创建任务、队列、信号量等)

• 静态内存分配:编译时预分配固定内存区域,避免运行时开销(适用于资源受限设备)

• 碎片控制:通过合并相邻空闲内存块降低碎片风险

1.2 为何不使用C库的malloc/free?

尽管C库函数通用,但在嵌入式场景中存在致命缺陷:

  1. 线程不安全:标准库未考虑多任务竞争,可能引发数据错乱(如两个任务同时申请内存)
  2. 确定性差:执行时间不可预测,影响实时性(例如某些实现可能因内存不足触发磁盘交换)
  3. 内存碎片:频繁分配释放导致内存被分割成小块,最终无法分配大块内存(例如连续申请不同大小的任务栈)
  4. 代码臃肿:标准库实现复杂,占用过多Flash空间(例如某些C库malloc代码量超过10KB)
  5. 硬件适配差:无法针对特定硬件优化(如STM32的CCM高速内存区)

2. FreeRTOS内存管理算法

2.1 五大算法对比

算法优点缺点
heap_1分配简单,时间确定只允许申请内存,不允许释放内存
heap_2允许申请和释放内存不能合并相邻的空闲内存块会产生碎片、时间不定
heap_3直接调用C库函数malloc()和 free() ,简单速度慢、时间不定
heap_4相邻空闲内存可合并,减少内存碎片的产生时间不定
heap_5能够管理多个非连续内存区域的 heap_4时间不定

2.2 FreeRTOS内存管理算法深度解析

1. Heap_1:单次分配,永不回收

核心机制

Heap_1是FreeRTOS最简单的内存管理算法,通过预定义的静态数组ucHeap[configTOTAL_HEAP_SIZE]实现线性分配。所有内存请求按顺序从数组起始地址递增分配,不支持内存释放,仅适用于初始化阶段创建对象后永不删除的场景。

实现细节

• 内存池初始化:在首次调用pvPortMalloc()时,根据portBYTE_ALIGNMENT对齐数组首地址,确保分配的内存满足处理器对齐要求(如STM32需8字节对齐)。

• 分配过程:记录当前分配位置xNextFreeByte,每次分配后直接递增指针。若剩余空间不足,返回NULL并触发vApplicationMallocFailedHook()钩子函数。

适用场景

• 工业控制器等静态系统,所有任务、队列、信号量在启动时创建且永不删除。

• 确定性要求高的场景,分配时间固定(无内存碎片影响)。

局限性

• 内存利用率低,无法回收已分配内存,长期运行可能导致内存耗尽。

2. Heap_2:最佳匹配算法,碎片化严重

核心机制

Heap_2在Heap_1基础上增加了内存释放功能,采用最佳匹配算法(Best Fit)。空闲块通过链表管理,分配时选择最小且足够的空闲块,但不合并相邻空闲块,导致外部碎片积累

实现细节

• 空闲块链表:使用BlockLink_t结构维护空闲块,包含指向下一块的指针和块大小。

• 分配策略:遍历链表找到最接近请求大小的块,分割剩余空间为新空闲块(若剩余空间足够)。

• 释放策略:仅将释放块标记为空闲,不进行合并。

适用场景

• 固定大小内存请求(如频繁创建/删除相同栈大小的任务)。

• 短期运行的嵌入式系统,对碎片容忍度较高。

局限性

• 长期运行后碎片化严重,无法分配连续大内存。

3. Heap_3:封装C库,兼容但低效

核心机制

Heap_3直接调用标准C库的malloc()free(),通过挂起调度器实现线程安全。内存堆大小由链接器配置(如STM32的启动文件定义),与configTOTAL_HEAP_SIZE无关。

实现细节

• 线程安全:在pvPortMalloc()vPortFree()中调用vTaskSuspendAll()暂停任务调度。

• 内存来源:依赖编译器的堆空间,可能分散在多个内存区域(如内部SRAM和外部SDRAM)。

适用场景

• 需要快速移植现有代码到FreeRTOS的复杂系统。

• 与标准库兼容性要求高的场景(如混合Linux和RTOS环境)。

局限性

• 非确定性,分配/释放时间不可预测。

• 标准库的碎片问题未解决,且代码体积大(如某些C库实现占用超10KB Flash)。

4. Heap_4:首次适应算法,碎片优化

核心机制

Heap_4采用首次适应算法(First Fit)和空闲块合并机制。分配时从链表头部查找第一个足够大的块,释放时检查相邻块是否空闲并合并,显著减少碎片。

实现细节

• 空闲块管理:链表按地址升序排列,释放时触发prvInsertBlockIntoFreeList()合并相邻块。

• 内存标记:通过块头部的xBlockSize最高位标记是否空闲(0为空闲,1为已分配)。

• 性能优化:支持动态调整空闲链表,减少遍历时间。

适用场景

• 通用嵌入式系统(STM32默认推荐),频繁分配/释放不同大小内存。

• 需要长期稳定运行的低碎片场景(如物联网网关)。

局限性

• 内存必须连续,无法管理非连续物理内存(如多块SRAM)。

5. Heap_5:多区域管理的Heap_4增强版

核心机制

Heap_5在Heap_4基础上扩展支持多块非连续内存区域。用户需预先定义HeapRegion_t数组描述各内存块起始地址和大小,初始化时通过vPortDefineHeapRegions()将其串联为逻辑连续空间。

实现细节

• 多区域配置:

c
const HeapRegion_t xHeapRegions[] = {
    { (uint8_t*)0x20000000, 0x10000 },  // 内部RAM 64KB
    { (uint8_t*)0x60000000, 0x80000 },  // 外部SDRAM 512KB
    { NULL, 0 }  // 结束标记
};
vPortDefineHeapRegions(xHeapRegions);

• 分配策略:与Heap_4相同,但支持跨区域分配。

适用场景

• 异构存储系统(如STM32H7+外部SDRAM)。

• 需要灵活管理分散内存的复杂应用(如视频处理设备)。

局限性

• 初始化复杂,需手动定义内存区域。

• 合并算法跨区域效率较低。

总结与选型建议

• 确定性优先:选择Heap_1(医疗设备、航天控制)。

• 低碎片需求:首选Heap_4(通用IoT设备)。

• 非连续内存:必须使用Heap_5(多核/大内存系统)。

• 兼容性要求:考虑Heap_3(混合开发环境)。

3. FreeRTOS内存管理API函数

3.1 内存申请

c
void *pvPortMalloc(size_t xWantedSize);

• 参数:xWantedSize为请求字节数(自动对齐,如8字节对齐)

• 返回值:成功返回内存首地址,失败返回NULL(可挂钩configUSE_MALLOC_FAILED_HOOK处理)

• 示例:创建任务时自动调用此函数分配TCB和栈空间

3.2 内存释放

c
void vPortFree(void *pv);

• 参数:pvpvPortMalloc返回的地址

• 注意:未释放内存会导致泄漏,需配合xPortGetFreeHeapSize监控

3.3 堆状态查询

c
size_t xPortGetFreeHeapSize(void);          // 当前空闲内存
size_t xPortGetMinimumEverFreeHeapSize(void); // 历史最小空闲值

小实验20

  1. 动态创建两个任务:task1、task2,两个任务作用如下:
  • task1:检测按键;
  • task2:打印剩余空闲内存大小;
  1. 检测到按键1按下,申请内存;检测到按键2按下,释放内存。

十七、其它常用API

1. 任务管理相关API

1.1 uxTaskPriorityGet

**作用:**获取指定任务的当前优先级。

原型:UBaseType_t uxTaskPriorityGet(TaskHandle_t xTask);

参数:

xTask:任务句柄,传入NULL表示获取当前任务优先级。

**返回值:**任务优先级(0configMAX_PRIORITIES-1)。

注意事项:

• 需在FreeRTOSConfig.h中启用INCLUDE_uxTaskPriorityGet宏。

1.2 vTaskPrioritySet

**作用:**动态修改任务的优先级。

原型:void vTaskPrioritySet(TaskHandle_t xTask, UBaseType_t uxNewPriority);

参数:

xTask:任务句柄,NULL表示修改当前任务。

uxNewPriority:新优先级(自动限制在configMAX_PRIORITIES-1)。

注意事项:

• 若新优先级高于当前任务,会触发上下文切换。

• 需启用INCLUDE_vTaskPrioritySet宏。

1.3 xTaskGetCurrentTaskHandle

**作用:**获取当前任务的句柄。

原型:TaskHandle_t xTaskGetCurrentTaskHandle(void);

**返回值:**当前任务句柄。

注意事项:

• 需启用INCLUDE_xTaskGetCurrentTaskHandle宏。

1.4 xTaskGetHandle

**作用:**根据任务名称获取任务句柄。

原型:TaskHandle_t xTaskGetHandle(const char *pcNameToQuery);

参数:

pcNameToQuery:任务名称字符串。

返回值:任务句柄,NULL表示未找到。

1.5 vTaskList

**作用:**以字符串形式输出所有任务状态信息(调试用)。

原型:void vTaskList(char *pcWriteBuffer);

参数:

pcWriteBuffer:字符缓冲区(建议长度≥40×任务数)。

注意事项:

• 需启用configUSE_TRACE_FACILITYconfigUSE_STATS_FORMATTING_FUNCTIONS宏。

1.6 uxTaskGetSystemState

**作用:**获取系统中所有任务的详细状态信息。

原型:

c
UBaseType_t uxTaskGetSystemState(TaskStatus_t *pxTaskStatusArray,
                                UBaseType_t uxArraySize,
                                uint32_t *pulTotalRunTime);

参数:

pxTaskStatusArrayTaskStatus_t结构体数组。

uxArraySize:数组大小(建议通过uxTaskGetNumberOfTasks()获取)。

pulTotalRunTime:总运行时间(需启用configGENERATE_RUN_TIME_STATS)。

**返回值:**实际任务数量。

1.7 vTaskGetInfo

**作用:**获取单个任务的详细信息(栈使用、状态等)。

原型:

c
void vTaskGetInfo(TaskHandle_t xTask,
                TaskStatus_t *pxTaskStatus,
                BaseType_t xGetFreeStackSpace,
                eTaskState eState);

参数:

xTask:任务句柄。

pxTaskStatus:存储信息的结构体。

xGetFreeStackSpace:是否计算栈剩余空间(pdTRUE/pdFALSE)。

eState:直接指定任务状态或eInvalid自动获取。

2. 队列管理相关API

2.1 uxQueueSpacesAvailable

**作用:**获取队列剩余可存放数据项的数量。

原型:UBaseType_t uxQueueSpacesAvailable(QueueHandle_t xQueue);

**返回值:**剩余空间数(队列满时返回0)。

2.2 uxQueueMessagesWaiting

**作用:**获取队列中当前存储的数据项数量。

原型:UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);

2.3 xQueueReset

**作用:**清空队列。

原型:BaseType_t xQueueReset(QueueHandle_t xQueue);

返回值:pdPASS表示成功。

3. 信号量相关API

xSemaphoreGetMutexHolder

**作用:**获取当前持有互斥量的任务句柄。

原型:TaskHandle_t xSemaphoreGetMutexHolder(SemaphoreHandle_t xMutex);

**注意事项:**仅适用于互斥量,需启用INCLUDE_xSemaphoreGetMutexHolder宏。

4. 定时器相关API

4.1 xTaskGetTickCount

**作用:**获取系统启动以来的时钟节拍数。

原型:TickType_t xTaskGetTickCount(void);

返回值:当前系统节拍计数值(TickType_t 类型,通常为 uint32_t

4.2 xTimerGetExpiryTime

**作用:**获取定时器下次到期时间(tick单位)。

原型:TickType_t xTimerGetExpiryTime(TimerHandle_t xTimer);

4.3 xTimerGetPeriod

**作用:**获取定时器的周期。

原型:TickType_t xTimerGetPeriod(TimerHandle_t xTimer);

4.4 pvTimerGetTimerID

**作用:**获取定时器的用户自定义ID。

原型:void *pvTimerGetTimerID(TimerHandle_t xTimer);

5. 事件组相关API

5.1 xEventGroupGetBits

**作用:**获取当前事件组的值。

原型:EventBits_t xEventGroupGetBits(EventGroupHandle_t xEventGroup);

5.2 xEventGroupSync

**作用:**同步多个任务到事件组(原子操作设置位并等待)。

原型:

c
EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup,
                            const EventBits_t uxBitsToSet,
                            const EventBits_t uxBitsToWaitFor,
                            TickType_t xTicksToWait);

参数:

uxBitsToSet:要设置的位掩码。

uxBitsToWaitFor:等待的位掩码。

xTicksToWait:超时时间。

xEventGroupSetBits 与 xEventGroupSync 的区别详解

1. 核心功能对比

特性xEventGroupSetBitsxEventGroupSync
核心目的设置事件位(单次操作),通知其他任务事件发生原子化同步多任务(设置+等待),确保多任务同步完成
操作类型非原子操作(分步执行)原子操作(设置位与等待位一步完成)
适用场景单向事件通知(如传感器数据就绪、按键触发)多任务协作同步(如任务A、B、C需同时到达同步点)

2. 参数与行为差异​xEventGroupSetBits​参数:

xEventGroup:事件组句柄

uxBitsToSet:要设置的位掩码(如 0x03 表示置位 bit0 和 bit1)

行为:

• 设置指定事件位,触发等待这些位的任务解除阻塞

• 无阻塞,立即返回当前事件组的最新值(可能因其他任务修改而不同)

xEventGroupSync参数:

xEventGroup:事件组句柄

uxBitsToSet:要设置的位掩码

uxBitsToWaitFor:需等待的事件位掩码

xTicksToWait:超时时间(0表示非阻塞)

行为:

• 原子操作:先设置 uxBitsToSet,然后阻塞等待 uxBitsToWaitFor 的所有位被置位

• 若条件满足(所有等待位被置位),自动清除 uxBitsToWaitFor 指定的位

• 若超时,返回超时时刻的事件组值

3. 典型应用场景​xEventGroupSetBits 的适用场景​​ • 单向通知:任务A完成数据采集后,设置事件位通知任务B处理

c
// 任务A设置事件位
xEventGroupSetBits(xEventGroup, DATA_READY_BIT);

• 多事件广播:中断服务例程(通过 xEventGroupSetBitsFromISR)设置多个位,唤醒多个等待任务

xEventGroupSync 的适用场景 • 多任务同步:任务A、B、C需同时完成初始化后才能执行下一步操作

c
// 每个任务调用xEventGroupSync
EventBits_t bits = xEventGroupSync(xEventGroup, TASK_A_BIT, ALL_TASKS_BITS, portMAX_DELAY);

• 防止竞争条件:确保设置事件位与等待操作不可分割,避免其他任务中途修改事件组状态

4. 关键注意事项

  1. 原子性差异: • xEventGroupSetBits 仅设置位,需配合 xEventGroupWaitBits 使用,但两者非原子操作,可能导致同步失败

    xEventGroupSync 保证设置与等待的原子性,避免中间状态干扰

  2. 阻塞行为: • xEventGroupSetBits 无阻塞,适用于触发事件后无需等待响应的场景

    xEventGroupSync 会阻塞任务,直到所有指定位被设置或超时

  3. 返回值处理: • xEventGroupSetBits 返回操作后的即时事件组值(可能已被其他任务修改)

    xEventGroupSync 返回条件满足时的事件组值(自动清除前)或超时值,需检查是否满足同步条件

5. 性能与资源影响 • 资源占用:

xEventGroupSync 因涉及阻塞和原子操作,可能增加调度器负担,适用于高优先级同步场景

xEventGroupSetBits 轻量,适合高频事件触发

• 中断安全:

xEventGroupSetBitsFromISR 用于中断,但需通过守护任务间接操作

xEventGroupSync 不可在中断中使用,仅限任务上下文

总结选择依据:

• 单向通知 ➔ xEventGroupSetBits

• 多任务同步 ➔ xEventGroupSync

设计建议:

• 优先使用 xEventGroupSync 处理复杂同步逻辑,避免竞态条件

• 简单事件触发场景可结合 xEventGroupSetBitsxEventGroupWaitBits,但需注意非原子操作的潜在风险

6. 内存管理相关API

vPortGetHeapStats

**作用:**获取堆详细统计信息(空闲块、最大块等)。

原型:

c
void vPortGetHeapStats(HeapStats_t *pxHeapStats);

结构体:

c
typedef struct {
  size_t xAvailableHeapSpaceInBytes;            // 当前空闲内存
  size_t xSizeOfLargestFreeBlockInBytes;        // 最大连续空闲块
  size_t xSizeOfSmallestFreeBlockInBytes;       // 最小连续空闲块
  size_t xNumberOfFreeBlocks;                   // 空闲块总数
  size_t xMinimumEverFreeBytesRemaining;        // 历史最小剩余堆空间
  size_t xNumberOfSuccessfulAllocations;        // 分配次数
  size_t xNumberOfSuccessfulFrees;              // 释放次数
} HeapStats_t;

实战项目1:流量控制系统

原名:排队控制系统

项目需求

  1. 红外传感器检测有人通过并计数;
  2. 计数值显示在LCD1602
  3. 允许通过时,LED1闪烁,蜂鸣器不响,继电器不闭合;
  4. 不允许通过时,LED2闪烁,蜂鸣器响,继电器闭合;
  5. 每次允许通过5个人,之后转为不允许通过,3秒后再转为允许通过

硬件清单

  • 继电器(模拟匣机)
  • 蜂鸣器
  • 红外避障模块
  • LCD1602
  • STM32开发板
  • ST-Link

硬件接线

STM32LCD1602继电器蜂鸣器红外
GNDGND
5VVDD
GNDV0
B1RS
B2RW
B10E
A0D0
A1D1
A2D2
A3D3
A4D4
A5D5
A6D6
A7D7
3.3BLAVCCVCCVCC
GNDBLKGNDGNDGND
B4OUT
B5I/O
B6IN

项目框图

实战项目2:智能门禁

项目需求

  1. 矩阵键盘输入密码,正确则开锁,错误则提示,三次错误蜂鸣器响3秒;
  2. 按下#号确认输入,按下*号修改密码;
  3. 密码保存在 W25Q128 里;
  4. OLED 屏幕显示信息。

硬件清单

  • 矩阵键盘

  • OLED 屏幕

  • 蜂鸣器

  • W25Q128

  • 继电器

  • 杜邦线

  • STM32开发板

  • ST-Link

  • USB转TTL

硬件接线

STM32矩阵键盘OLED屏幕蜂鸣器W25Q128继电器
PB0R1
PB1R2
PB2R3
PB10R4
PB11C1
PB12C2
PB13C3
PB8SCL
PB9SDA
PC13I/O
PA4CS
PA5CLK
PA6DO
PA7DI
PB7I/O
5VVCC
3.3VVCCVCCVCC
GNDGNDGNDGNDGND

项目框图

实战项目3:智能台灯

项目需求

  1. 红外传感器检测是否有人,有人的话实时检测距离,过近则报警;同时计时,超过固定时间则报警;
  2. 按键 1 切换工作模式:智能模式、按键模式、远程模式;
  3. 智能模式下,根据光照强度自动调整光照档位(低亮、中亮、高亮),没人则自动关灯;
  4. 按键模式下,按键 2 可以手动调整光照档位;
  5. 远程模式下,可以通过蓝牙控制光照档位、计时等;
  6. 按键 3 暂停/开始计时,按键 4 清零计时;
  7. OLED 显示各项数据/状态。

硬件清单

  • 蓝牙模块
  • 超声波传感器
  • 红外传感器
  • 光敏电阻传感器
  • OLED
  • 高功率LED灯
  • 蜂鸣器
  • KEY × 4
  • 杜邦线
  • STM32开发板
  • ST-Link
  • USB转TTL

硬件接线

STM32OLEDLED灯蜂鸣器蓝牙超声波红外光敏电阻KEY1KEY2KEY3KEY4
PB6SCL
PB7SDA
PB0I/O
PB13I/O
PA3TX
PA2RX
PA11Trig
PA12Echo
PB12I/O
PA1I/O
PA4I/O
PA5I/O
PA6I/O
PA7I/O
3.3VVCCVCCVCCVCC
5VVCCVCCVCC
GNDGNDGNDGNDGNDGNDGNDGNDGNDGNDGNDGND

项目框图

完结,撒花🌸~~