FreeRTOS入门与工程实践-基于STM32F103(一)(单片机程序设计模式,FreeRTOS源码概述,内存管理,任务管理,同步互斥与通信,队列,信号量)

裸机程序设计模式

裸机程序的设计模式可以分为:轮询、前后台、定时器驱动、基于状态机。前面三种方法都无法解决一个问题:假设有A、B两个都很耗时的函数,无法降低它们相互之间的影响。第4种方法可以解决这个问题,但是实践起来有难度。

状态机模式之所以能解决耗时函数相互影响的问题,关键在于它将程序的执行流程拆解为独立的状态片段,并通过事件驱动切换状态,从而实现“分时处理”。以下是通俗解释:

  1. ​前三种模式的缺陷​

    • ​轮询​​:像不断打电话问“轮到A/B了吗?”,两个函数必须互相等待对方完全执行完毕。
    • ​前后台​​:类似中断处理,但若A/B本身耗时,仍会长时间占用CPU(比如A执行时,B只能干等)。
    • ​定时器驱动​​:虽然能分时调度,但若A/B内部需要保持执行进度(比如解析数据包),定时器无法保存中间状态。
  2. ​状态机的解决原理​

    • ​拆解为状态​​:把A/B两个耗时函数分解成多个小步骤,例如:
      A步骤1 → 保存进度 → 切换执行B步骤1 → 保存进度 → 切换回A步骤2...
      每个步骤对应一个独立状态。
    • ​非阻塞切换​​:每次只执行当前状态对应的代码片段(比如处理一个数据块),完成后立即释放CPU,通过事件触发下一状态。这类似于“流水线工人轮流处理多道工序”。
    • ​状态记忆​​:通过变量记录当前进度(如解析到数据包第几位),下次切换回来时能继续执行。
  3. ​实践难点举例​

    • ​状态爆炸​​:若A/B各有10个步骤,组合后可能产生100种状态,需合理抽象(例如用“解析中”“等待响应”等通用状态代替具体步骤)。
    • ​事件管理​​:需明确触发状态切换的条件(如定时器到期、传感器信号),类似“红绿灯控制不同方向车流”。
    • ​调试复杂度​​:执行流程分散在不同状态中,需用工具跟踪状态跳转路径。

​类比理解​​:想象同时煮面和炒菜。传统方法(轮询/前后台)会先煮完面再炒菜,面可能煮糊;状态机则是:下面→切菜→搅拌面→炒菜→捞面→加调料… 通过快速切换保证两者同步进行且不烧焦。

多任务系统

2. ​​状态机模式的改进与局限​

状态机通过​​“拆分任务为小步骤”​​来缓解问题:

  • ​原理​​:将A和B拆解为多个小步骤(例如A拆成3步,B拆成3步),每次执行一个步骤后保存进度,下次继续执行后续步骤。例如:
    // A的步骤:舀饭 → 喂饭 → 舀菜 → 喂菜(循环)
    // B的步骤:查看信息 → 打字 → 发送(循环)
    每次执行时只完成一个步骤,然后切换任务。
  • ​优势​​:缩短单次执行时间,降低A和B之间的阻塞风险。
  • ​局限性​​:
    • ​拆分难度高​​:某些任务(如复杂的数据处理)难以拆分为独立步骤,强行拆分可能导致逻辑混乱。
    • ​状态爆炸​​:如果A和B各有10个步骤,组合后可能产生100种状态,代码复杂度剧增。
    • ​时间不可控​​:若某一步骤意外耗时(如等待硬件响应),仍可能影响其他任务。

​类比​​:妈妈把喂饭拆解为“舀饭、喂饭、舀菜、喂菜”,打字拆解为“看信息、打字、发送”,快速切换步骤。但如果某一步耗时太久(如喂饭时孩子不张嘴),整体流程仍会卡住。


3. ​​根本矛盾:裸机的单线程本质​

无论使用哪种模式,裸机程序最终依赖​​单核CPU轮流执行代码​​。即使通过状态机拆分任务,本质仍是“​​分时复用CPU​​”:

  • ​A和B的步骤交叉执行​​:例如先执行A的步骤1 → B的步骤1 → A的步骤2 → B的步骤2,但若总时间超出预期(如所有步骤耗时之和超过一个周期),系统仍会崩溃。
  • ​无法真正并行​​:裸机缺乏多任务调度机制,无法像操作系统那样为任务分配独立资源。

​类比​​:妈妈虽然能快速切换步骤,但脑子只能想一件事。如果步骤太多或太复杂,切换速度跟不上,任务仍会积压。


4. ​​解决方案:从状态机到多任务系统​

当状态机也无法满足需求时,需转向​​多任务系统(如RTOS)​​:

  • ​核心思想​​:操作系统为每个任务分配独立“虚拟CPU”,通过快速切换(如毫秒级)模拟并行执行。
  • ​优势​​:
    • ​自动调度​​:无需手动拆分步骤,任务函数可保持完整逻辑。
    • ​优先级控制​​:紧急任务可抢占CPU资源(如喂饭优先于回信息)。
  • ​代价​​:需引入任务管理、资源互斥等复杂度。

​类比​​:妈妈请了一个助手,两人分工合作(一个喂饭,一个打字),助手负责协调两人节奏,确保任务看似同时进行。


总结

  • ​裸机极限​​:状态机是裸机的终极优化模式,但受限于单线程本质,无法彻底解决耗时任务相互影响的问题。
  • ​升级方向​​:当任务复杂度超过人工拆分能力时,需使用多任务系统(RTOS),通过操作系统调度实现“伪并行”。

互斥操作

在基于RTOS的多任务系统中,​​直接使用全局变量来实现串口互斥访问存在严重问题​​。以下是通俗解释和代码示例分析:


一、全局变量方案的致命缺陷

假设用全局变量 bool is_busy 作为互斥标志,代码逻辑可能是:

// 任务A和任务B共用的代码逻辑
if (is_busy == false) {  // 1.检查是否空闲
    is_busy = true;       // 2.标记为占用
    send_data_via_uart(); // 3.发送数据
    is_busy = false;      // 4.释放资源
}

​问题本质​​:

  1. ​原子性缺失​
    操作 检查变量 → 修改变量 不是原子操作。若任务A刚执行完第1步(检查为false),还未执行第2步时被高优先级任务B抢占,任务B同样会看到is_busy == false,导致两个任务同时操作串口。

  2. ​优先级反转风险​
    若低优先级任务A占用了串口,高优先级任务B将无限循环等待,导致系统响应异常。

  3. ​阻塞机制缺失​
    全局变量无法让任务在资源被占用时主动让出CPU,只能通过轮询消耗CPU资源。


二、正确的解决方案:互斥量(Mutex)

示例代码(基于FreeRTOS):
SemaphoreHandle_t uart_mutex = xSemaphoreCreateMutex(); // 创建互斥量

// 任务A和任务B的发送逻辑
void send_task(void *pvParameters) {
    for (;;) {
        if (xSemaphoreTake(uart_mutex, portMAX_DELAY) == pdTRUE) { // 加锁
            send_data_via_uart();  // 安全使用串口
            xSemaphoreGive(uart_mutex); // 解锁
        }
    }
}

​核心优势​​:

  1. ​原子性保障​
    互斥量的 Take 和 Give 是原子操作,不会被任务切换打断。
  2. ​自动阻塞​
    资源被占用时,任务会进入阻塞状态,释放CPU给其他任务。
  3. ​优先级继承​
    若低优先级任务占用资源,系统会临时提升其优先级,避免高优先级任务无限等待。

三、替代方案:队列(Queue)

若任务频繁发送数据,可用队列解耦操作:

QueueHandle_t uart_queue = xQueueCreate(10, sizeof(data_t)); // 创建队列

// 发送任务(任务A/B):
xQueueSend(uart_queue, &data, portMAX_DELAY); // 数据入队

// 专用串口发送任务:
void uart_sender_task(void *pvParameters) {
    data_t buf;
    for (;;) {
        if (xQueueReceive(uart_queue, &buf, portMAX_DELAY) == pdTRUE) {
            send_data_via_uart(&buf); // 从队列取出数据并发送
        }
    }
}

​优势​​:

  • 所有发送请求由队列统一管理,避免直接操作硬件冲突
  • 天然实现“先到先处理”的公平性

四、总结对比

方案 全局变量 互斥量 队列
线程安全 ❌ 易冲突 ✔️ 原子操作 ✔️ 无需锁
CPU利用率 ❌ 轮询耗资源 ✔️ 自动阻塞 ✔️ 自动阻塞
适用场景 低频独占操作 高频数据发送

​结论​​:
在RTOS中,​​绝对不要用全局变量实现互斥​​,优先选择互斥量或队列。

同步操作

问题本质

当任务B需要依赖任务A的结果时,如果任务B采用​​"轮询检查"​​的方式等待(比如不断循环查询某个变量),会导致以下问题:

  • ​CPU资源浪费​​:任务B在等待期间持续占用CPU,但实际没有做有意义的工作(类似员工不断打电话问同事“好了吗”)。
  • ​数据不一致风险​​:任务A可能尚未完成关键操作,任务B就读取了中间状态的脏数据(比如文件只写入了一半就被读取)。
  • ​优先级反转​​:若任务B优先级更高,它会不断抢占CPU,反而让任务A无法尽快完成任务,形成死循环。

错误代码示例(轮询浪费型)

// 全局变量作为标志位
volatile int task_a_done = 0;

void task_a() {
    // 执行耗时操作(如写入文件)
    write_data_to_file();
    task_a_done = 1; // 完成后设置标志
}

void task_b() {
    while (1) {
        if (task_a_done) {  // ❌ 不断循环检查标志
            process_file_data(); // 处理文件
            break;
        }
        // 空循环浪费CPU
    }
}

正确解决方案

方法1:事件通知(如FreeRTOS信号量)
SemaphoreHandle_t semaphore = xSemaphoreCreateBinary(); // 创建信号量

void task_a() {
    write_data_to_file();
    xSemaphoreGive(semaphore); // ✅ 完成后发送信号(类似按铃通知)
}

void task_b() {
    xSemaphoreTake(semaphore, portMAX_DELAY); // ✅ 阻塞等待信号
    process_file_data(); // 收到信号后才开始处理
}

​优势​​:

  • 任务B在等待

通俗解释:FreeRTOS信号量如何实现事件通知?

1. ​​什么是信号量?​

信号量就像是一个​​共享资源的计数器​​,用来管理多个任务(或中断)对资源的访问或事件的通知。比如:

  • ​停车场比喻(计数信号量)​​:假设停车场有100个车位(共享资源),每辆车进入时,剩余车位数量(信号量值)减1;离开时加1。当车位为0时,新来的车必须等待(任务阻塞),直到有车离开(信号量释放)。
  • ​公共电话比喻(二值信号量)​​:电话只能一人使用,状态为“占用”(信号量值0)或“空闲”(信号量值1)。当电话空闲时,你可以使用它;若被占用,你必须等待对方挂断(信号量释放)。
2. ​​信号量如何用于事件通知?​

假设场景:​​任务A需要等待某个事件(如按键按下)发生后再执行​​。

  • ​步骤​​:
    1. ​初始化信号量​​:创建一个初始值为0的二值信号量(例如xSemaphoreCreateBinary())。
    2. ​任务A等待事件​​:任务A调用xSemaphoreTake()获取信号量。由于初始值为0,任务A会进入阻塞状态(类似“睡觉”)。
    3. ​事件触发​​:当按键按下时(比如在中断服务函数中),调用xSemaphoreGiveFromISR()释放信号量。
    4. ​任务A被唤醒​​:信号量值变为1,任务A立即获取信号量并开始执行后续操作(如处理按键事件)。

​通俗类比​​:

  • 任务A像在等快递的人,快递(事件)没到时就睡觉(阻塞)。
  • 快递员(中断)送快递时按门铃(释放信号量),人醒来签收(处理事件)。
3. ​​信号量与其他机制的区别​
  • ​与队列的区别​​:信号量不传递数据,只传递事件发生的标志。比如队列是“传递包裹”,信号量是“按门铃通知”。
  • ​与任务通知的区别​​:任务通知更轻量(直接通过任务控制块通信),但信号量更适合多任务竞争的场景。
4. ​​实际应用场景​
  • ​中断与任务同步​​:传感器数据到达时,中断释放信号量,任务处理数据。
  • ​任务间协作​​:任务B完成任务后释放信号量,通知任务A开始下一步操作。
  • ​资源互斥​​:多任务共享打印机时,通过信号量确保同一时间只有一个任务使用。
5. ​​注意事项​
  • ​优先级继承(仅互斥信号量)​​:若高优先级任务等待低优先级任务释放信号量,系统会临时提升低优先级任务的优先级,避免“优先级反转”问题。
  • ​避免死锁​​:确保信号量释放逻辑正确,防止任务永远阻塞。

通过信号量,FreeRTOS可以高效协调任务与事件的关系,就像用“门铃”和“计数器”管理现实中的排队问题一样简单直观。

如果任务之间有依赖关系,比如任务A执行了某个操作之后,需要任务B进行后续的处理。如果代码如下编写的话,任务B大部分时间做的都是无用功。

优先级反转的本质是:​​高优先级任务需要低优先级任务生产的资源,但中优先级任务通过抢占CPU,导致低优先级任务无法完成资源生产,进而使高优先级任务无限等待​​。这与共享资源的使用场景密切相关,以下通过更贴切的例子说明:


1. ​​场景修正(资源生产视角)​
  • ​老板(任务A,高优先级)​​:需要一份由实习生(任务C)生成的报表(资源)才能继续工作。
  • ​实习生(任务C,低优先级)​​:正在生成报表(需要CPU时间),但尚未完成。
  • ​普通员工(任务B,中优先级)​​:执行与报表无关的报销单打印任务(占用CPU)。

​问题发生过程​​:

  1. ​任务C开始生产资源​​:实习生正在生成报表(占用CPU),但生成到一半时……
  2. ​任务B抢占CPU​​:普通员工(B)因优先级高于实习生(C),直接抢占CPU,开始打印报销单。
  3. ​任务A被阻塞​​:老板(A)需要等待实习生(C)完成报表,但C的CPU时间被B持续抢占,​​无法完成资源生产​​。
  4. ​死循环形成​​:
    • A等待C的报表 → C无法执行(被B抢占) → B无关任务持续运行 → A永远等不到资源。

​关键矛盾​​:

  • ​资源生产依赖低优先级任务​​:高优先级任务A需要C生产的资源,但C的CPU时间被中优先级任务B剥夺,导致资源无法产出。
  • ​系统规则漏洞​​:CPU调度仅根据优先级分配,未考虑资源生产的依赖性。

2. ​​为什么老板不能直接“帮”实习生生成报表?​
  • ​资源隔离原则​​:系统要求任务必须独立完成资源操作,否则会导致数据混乱(如报表生成一半被其他任务修改)。
  • ​优先级与资源的解耦​​:高优先级任务可抢占CPU,但无法直接操作其他任务未完成的资源(需等待完整性)。

3. ​​解决方案:打破中优先级的干扰​
方法1:​​优先级继承(Priority Inheritance)​
  • ​操作​​:当老板(A)等待实习生(C)时,系统​​将C的优先级临时提升到与A相同​​。
  • ​效果​​:中优先级任务B无法再抢占C的CPU时间,C快速生成报表,释放资源给A。
  • ​比喻​​:老板说:“实习生现在和我一样重要,其他人不许打断他!”
方法2:​​优先级天花板(Priority Ceiling)​
  • ​操作​​:规定“生成报表的任务自动获得最高优先级”。
  • ​效果​​:实习生(C)一开始生成报表,优先级直接超过B,B无法干扰。

FreeRTOS源码概述

1. 源码结构:像积木一样分层搭建​

FreeRTOS的源码像一套积木,分为三个大块:

  • ​核心积木(内核代码)​​:
    • tasks.c:管理所有任务(比如老板、员工的工作流程),负责创建、删除、调度任务。
    • queue.c:管“传纸条”,任务之间通过队列发送消息(比如老板让实习生交报表)。
    • list.c:内核的“任务清单”,用链表管理任务、定时器等对象的排队顺序。
    • timers.c:软件定时器,像闹钟一样提醒任务该做什么事。
  • ​硬件适配积木(移植层)​​:
    • port.cportmacro.h:针对不同芯片(如ARM、51单片机)写的适配代码,比如怎么切换任务、处理中断。
    • heap_x.c(如heap_4.c):管内存分配,像“分糖果”一样给任务分内存(x代表不同分配策略)。
  • ​配置文件​​:
    • FreeRTOSConfig.h:像“开关面板”,决定用哪些功能(比如要不要用信号量、任务最多几个优先级)。

​2. 核心机制:像公司管理一样分工明确​
  • ​任务管理​​:每个任务是一个独立员工
    • 每个员工(任务)有自己的工作清单(堆栈)和优先级(老板>员工>实习生)。
    • 调度器像“值班经理”,根据优先级决定谁先干活(高优先级任务能打断低优先级)。
  • ​通信机制​​:任务之间传话用“小纸条”
    • ​队列(Queue)​​:传数据纸条(比如传感器数据)。
    • ​信号量(Semaphore)​​:举个牌子表示资源是否可用(比如打印机是否空闲)。
    • ​事件组(Event Group)​​:挂个公告板,任务贴通知(比如“报表已生成”)。
  • ​内存管理​​:分糖果还是固定配额?
    • heap_1:简单粗暴,一次性分完所有内存,适合不删任务的系统。
    • heap_4:动态分配,像自动回收垃圾,避免内存碎片。

​3. 移植与配置:像换手机壳一样适配不同芯片​
  • ​移植步骤​​:
    1. 复制对应芯片的port.cportmacro.h(比如ARM芯片用ARM_CM3文件夹下的文件)。
    2. 选一个内存分配策略(如heap_4.c)。
    3. FreeRTOSConfig.h:调时钟频率、任务最大优先级数等参数。
  • ​配置示例​​:
    #define configTICK_RATE_HZ 1000    // 系统心跳=1毫秒一次
    #define configMAX_PRIORITIES 10    // 最多10个优先级(老板到实习生等级)
    #define configTOTAL_HEAP_SIZE 4096 // 内存池大小=4KB

​4. 代码规范:像写作文一样严格​

FreeRTOS的代码遵循​​MISRA-C​​安全规范:

  • ​变量命名​​:前缀表示类型,比如c是字符,x是自定义类型(如xTaskHandle)。
  • ​安全设计​​:
    • 任务切换用汇编实现,保证速度(见port.c里的汇编代码)。</
1.1 课程内容嵌入式软件工程师的学习路线般是:单片机、RTOS、Linux。当你掌握单片机开发后,如果要进步提升编程水平,建议学习RTOS(Real Time Operating System,实时操作系统)。有很多优秀的RTOS,比如FreeRTOS、RT-Thread、UCOS等等。FreeRTOS使用范围最广泛,RT-Thread生态丰富,UCOS是收费的并且很少使用了。对于初学者,建议先学习FreeRTOS。只要学会了任意款RTOS,肯定就会使用其他RTOS了。我们在2022年已经推出了“FreeRTOS快速入门”课程。为何还要重新制作“FreeRTOS入门工程实践”?“FreeRTOS快速入门”只是讲解FreeRTOS的各类API的理论、用法、示例,这些实验是基于Keil自带的STM32F103模拟器。没有使用更多的硬件模块、不能体现工作中的实际场景。在“FreeRTOS入门工程实践”,将引入更多的硬件模块,并展示实际工程示例中的用法。另外,基于RTOS的程序般都比较复杂,涉及的源文件非常多,在工作中般都基于“面向对象”的思想来写程序。所以,本课程会涉及如下内容:讲解FreeRTOS的常用API:理论、用法选择合适的硬件模块,展示这些API的实例实现合适的小项目,展示工作中的编程方法1.2 讲课方式对于每个实验,我们会精心设计:要解决什么问题;然后讲解FreeRTOS提供的解决方法。讲解FreeRTOS的API及内部原理(不深入讲解内部源码,只是进行原理性介绍)讲解实验过程使用的模块的接口函数(只讲使用,不讲内部实现,模块的源码实现单独开课讲解)讲解原理时,配合着文档、现场画图进行讲解,跟学校老师写黑板样最后现场从0编写程序并调试切都是现场操作,绝对不会照着PPT念,绝对不会照着现成的代码讲解。只有现场从0操作,学员才能身临其境地学习,跟着教程:碰到问题、解决问题。1.3 硬件平台本课程基于DshanMCU-103开发套件进行开发,它由3部分组成:STM32F103C8T6的最小系统板、扩展底板、各类模块。如下图所示:  上述硬件再加个ST-Link即可学完本课程所有内容。主板DshanMCU-103是基于STM32F103C8T6的最小系统板。之所以选择最小系统板,而不是把所有模块都放在个整体的电路板上,目的如下:低成本尝试:嵌入式软件开发并不定适合你,可以购买最小系统板进行体验、及时放弃按需购买:用到再买,讲究个性价比 
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值