FreeRTOS学习(3)——FreeRTOS的移植与剪裁

文章目录

获取资料

本文的资料可在gitee仓库(cnblog: 博客园演示的代码)中获取

概述

了解完FreeRTOS的任务调度机制,关于FreeRTOS的前置理论,我们就大体上搞定了。

虽然还有很多理论需要学习,但我们暂缓一下脚步,在这一章节中,我们先把FreeRTOS集成到我们的项目中。

先跑起来再说。

这里我们主要介绍:

基于SPL标准外设库模板工程,集成FreeRTOS,形成一个新的带FreeRTOS的标准库开发工程模板。

再顺便,我们来讲一讲HAL库的开发方式,也讲一讲在HAL库开发工程中集成FreeRTOS,当然这些内容了解即可。

我们后续还是以标准库开发方式为主要手段。

准备工作和下载FreeRTOS源码

首先,我们要准备好面包板,然后按照下面的方式完成接线:

在FreeRTOS的学习前几天,我们就使用这种简单的接线就足够了。

FreeRTOS是完全开源免费的,我们可以直接进入它的官方,直接下载FreeRTOS的源码。

FreeRTOS官网

直接点击下载按钮即可。

下载后将会得到一个压缩包,这个压缩包就是全部FreeRTOS的源码,

解压后大约只有几十MB,由此可见,FreeRTOS只是一个轻量级的小型实时操作系统。

这个FreeRTOS源码文件夹如何使用,我们放到下面再说。

首先,我们先来了解一下,什么叫做FreeRTOS的移植。

什么叫做FreeRTOS的移植

FreeRTOS 被广泛应用于各个领域,可适配的 MCU 芯片种类也各不相同。

然而,我们刚刚从官网上下载的 FreeRTOS 内核源码,却只有一份

那么问题就来了:

这一份代码,是如何同时适配如此多不同的处理器架构与平台的?

从源码层面来看,FreeRTOS 内核主要使用 C 语言编写。

整体上这份源码,可以分为两个部分:

  1. 绝大部分代码属于通用代码。这一部分与具体的 MCU 架构无关,各种平台可以共用同一份源码。
  2. 少量代码用于处理不同架构和平台之间的差异。针对不同 CPU 架构、编译器和硬件特性,FreeRTOS 官方提供了对应的适配代码。

因此,在实际使用 FreeRTOS 时,并不是将所有源码一股脑地拷贝进工程中,而是:

  1. 先引入 FreeRTOS 的通用核心代码
  2. 再根据所使用的 MCU 和编译器,选择对应的架构适配代码

通过这种方式,FreeRTOS 才能够在满足具体硬件条件的前提下,正常运行起来。

上述这一整套"选择并组合代码,使 FreeRTOS 能够在目标 MCU 上运行"的过程,就称为 FreeRTOS 的移植。

所以,关于什么是FreeRTOS的移植,这里给出一个定义:

FreeRTOS 的移植,

指的是让 FreeRTOS 操作系统内核能够在某一具体 CPU 架构和编译器环境下正确运行的适配过程。

大家可以小记一下这句话,起码也要理解移植的作用

基于SPL库的FreeRTOS移植

首先准备好一个Keil5下,基于SPL库的标准库模板工程。

只要你前面上过STM32的课程,这个模板你肯定不陌生。

下面我们先来介绍一下FreeRTOS源码文件夹下,各个文件夹的作用,了解一下。

根目录文件夹介绍

FreeRTOS源码文件夹,最外层根目录内容,如下图所示:

最核心的文件夹是其中的"FreeRTOS"文件夹,该文件夹下用于存放FreeRTOS的内核源码、演示示例以及各种文档。

2017年的时候,Amazon(亚马逊)收购了FreeRTOS项目组。

所以FreeRTOS 成为 AWS(亚马逊云) 官方 IoT 操作系统方案,这里根目录下放了一个"aws"的演示示例,实属正常。

当然亚马逊的收购不影响FreeRTOS开源免费的特点,我们仍然可以放心使用。

源码文件夹内容介绍

打开根目录下的FreeRTOS文件夹,这就是源码文件夹,其内容如下图所示:

在这个源码目录中,对我们当前学习和使用最直接相关的,只有 FreeRTOS-Kernel 文件夹。

它是FreeRTOS操作系统的核心部分,它为嵌入式系统提供了一个轻量级的、实时的操作系统内核。

我们在移植FreeRTOS时,我们只需要将 FreeRTOS-Kernel 目录下的相关源码引入工程即可。

关于 Kernel 文件夹内部的结构与作用,我们将在后面的内容中再进行详细介绍。

这里我们不妨来讲一下,其他的这么多文件夹,它们为什么没有用。

  1. backoffAlgorithm: 提供网络通信失败后的重试退避算法,通常用于需要网络重连机制的系统。
  2. coreHTTP:用于在 MCU 上实现 HTTP 协议通信,需要 MCU 具备 TCP/IP 网络通信能力。
  3. coreJSON:提供轻量级 JSON 数据解析功能,常用于与云端或网络服务进行数据交互的场景。
  4. coreMQTT:实现 MQTT 物联网通信协议,通常需要 MCU 具备网络接口并运行 TCP/IP 协议栈
  5. corePKCS11:提供密钥与证书管理接口,用于加密与安全通信,常见于支持 TLS/HTTPS 的系统。
  6. coreSNTP:用于通过网络进行时间同步(SNTP 协议),需要 MCU 具备网络通信能力。
  7. FreeRTOS-Cellular-Interface:面向蜂窝通信模块(如 4G/LTE)的统一接口层,通常需要 MCU 外接蜂窝通信模块。
  8. FreeRTOS-Plus-TCP:FreeRTOS 官方 TCP/IP 协议栈,通常用于具备以太网或 WiFi 硬件支持的 MCU 平台。

可以看到这些组件都是服务于网络通信的,那我们的MCU,即STM32F103C8T6,具备网络通信能力吗?

STM32F103C8T6可以实现串口通信、I2C通信、SPI通信等等,但是很显然,它不具备以太网、WiFi、蜂窝数据等网络通信能力。

既然我们使用的单片机不具备网络通信能力,那么上述的文件夹也就没有使用的必要了。

小Tips:

当然你可能会问,如果需要进入网络通信,那怎么办呢?

很简单,既然自身单片机不集成网络通信模块,那么外挂接入一个网络通信模块就好了。

我们使用的是ESP-01S模块,它是一款基于ESP8266芯片的Wi-Fi网络通信模块。

我们的单片机通过外接这个网络通信模块,实现网络通信。

在这种方案下:

  1. STM32 负责业务逻辑和实时控制
  2. ESP-01S 负责 Wi-Fi 协议栈和网络通信

STM32 只需要通过串口通信与ESP-01S模块交互数据,就可以了。

不需要自己在 STM32 上实现完整的 TCP/IP 或 Wi-Fi 协议栈。

所以,我们依然还是用不到上面这些文件夹。

Kernel文件夹介绍(重点)

Kernel文件夹是我们移植FreeRTOS的核心,首先该文件夹根目录下有很多无关的文件,我们直接删除:

  1. HTML文件,主要用于链接官方文档或相关网站,对编程本身没有作用。
  2. txt文件和md文件,用于说明、文档或版本信息,不参与代码编译。
  3. yaml文件,配置文件,不属于内核源码。
  4. ...

也就是说,根目录下所有文件夹先保留,仅保留 .c.h 源文件,其余文件直接删除。

如此,我们得到下面的文件夹内容:

逐一来解释一下这个文件夹内容:

  1. examples文件夹,示例代码。用于存放官方的演示示例,对FreeRTOS来说,暂时是无用的。
  2. include文件夹,include有包含的意思,所以该文件夹下存放所有的头文件,用于提供声明接口。
  3. portable文件夹,portable本身的含义是"可移植的",该文件夹下存放与 CPU 架构、编译器强相关的文件内容。
  4. 根目录下所有的".c"文件,都是FreeRTOS内核核心源代码文件。

整个FreeRTOS移植过程,我们将源码分为三部分加入工程中:

其一,通用代码,根目录下所有的源文件,以及include文件夹下的所有头文件。

其二,Cortex-M3架构适用的部分源代码。如下图所示:

其三,需要指定FreeRTOS内存管理方案。

portable/MemMang文件夹 下存在五个heap_x.c源文件,本质上代表五种FreeRTOS管理内存的策略方案。

其中第四种,也就是heap_4.c源文件代表的内存管理方案是最常用,最方便使用的。

在一般的学习和项目中,大多都直接选择这种内存管理策略。关于这个策略,我们之后再涉及,这里不增加大家负担。

所以大家直接选择heap_4.c源文件这个文件,加入工程即可。

如此,我们就已经挑选好了,要加入工程的FreeRTOS文件。

回顾标准库开发LED闪烁

现在我们的面包板上,PA0和PA4两个引脚上都接入了一盏LED。

现在我们打开标准库模板,只需要在main.c文件中写下面代码:

c 复制代码
#include "stm32f10x.h"

int main(void) {
    // 1. 开启 GPIOA 外设时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // 2. 配置 PA0 和 PA4 为推挽输出
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    // 3. 默认关闭 LED
    GPIO_ResetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_4);

    // 4. 主循环:两盏 LED 闪烁
    while (1) {
        // PA0 和 PA4 点亮
        GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_4);

        // for 循环延时
        for (uint32_t i = 0; i < 2000000; i++);

        // PA0 和 PA4 熄灭
        GPIO_ResetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_4);

        // for 循环延时
        for (uint32_t i = 0; i < 2000000; i++);
    }
}

这段代码非常简单,稍微需要注意的是:

从今天开始,丢掉我们之前使用的"Delay"函数。在使用FreeRTOS后,之前的Delay函数变得无法使用了。

关于这一点,在后续讲定时器时,我们再详谈。

在上述的基础上,在引入FreeRTOS之前,我们不妨思考两个问题:

问题1:FreeRTOS的使用一定基于标准库吗?基于寄存器开发可不可以?基于HAL库的开发可不可以?

答:

FreeRTOS 的运行基础是 MCU 硬件本身,而不是某一种具体的软件库。

至于如何对 MCU 进行开:

是直接使用寄存器、使用标准外设库(SPL),还是使用 HAL 库------属于用户的工程选择,与 FreeRTOS 本身无关。

无论采用哪种开发方式,都可以在工程中引入并使用 FreeRTOS。

问题2:现在使用两个FreeRTOS任务,分别控制两盏LED闪烁,

小细节:FreeRTOS的头文件问题

我们在 基于 SPL 库的 Keil5 开发环境 中,已经集成了 FreeRTOS 实时操作系统。

当程序需要使用 FreeRTOS 的任何功能时,都必须包含头文件:

c 复制代码
#include "FreeRTOS.h"

这个头文件是 FreeRTOS 的核心头文件。

并且,FreeRTOS 的所有功能模块,都是在这个头文件基础上建立的。

在实际开发中,通常还会包含其它 FreeRTOS 模块头文件,以使用FreeRTOS各种功能,例如:

c 复制代码
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

其中:

头文件 功能
task.h 任务创建与管理相关函数
queue.h 消息队列相关函数
semphr.h 信号量 / 互斥锁相关函数
event_groups.h 事件标志组相关函数
timers.h 软件定时器相关函数

需要特别注意的是:

FreeRTOS 其他模块的头文件,必须写在 FreeRTOS.h 之后。

例如,正确的写法是:

c 复制代码
#include "FreeRTOS.h"
#include "queue.h"

错误的写法是:

c 复制代码
#include "queue.h"
#include "FreeRTOS.h"

如果写错顺序,编译器会直接报错:

#error "include FreeRTOS.h must appear in source files before include queue.h"

之所以有这种限制,是因为:

FreeRTOS.h 中定义了很多基础类型和配置宏。

例如:

c 复制代码
BaseType_t
UBaseType_t
TickType_t

以及各种配置宏:

c 复制代码
configMAX_PRIORITIES
configTICK_RATE_HZ
configUSE_PREEMPTION

而其它模块头文件(例如 queue.htask.h),在声明函数时都会使用这些类型。

如果没有先包含 FreeRTOS.h,这些类型就不存在,编译器就无法解析这些函数声明。

如果你没听明白,举一个简单的例子:

c 复制代码
typedef int NumType;    // 这是FreeRTOS.h中的类型别名
NumType a;  // 这是其他头文件中,会使用FreeRTOS.h中的类型别名

显然写成下面的形式肯定是不对的:

c 复制代码
NumType a;  // 这是其他头文件中,会使用FreeRTOS.h中的类型别名
typedef int NumType;    // 这是FreeRTOS.h中的类型别名

因此必须记住一个简单的规则:FreeRTOS.h 必须最先包含。

随后再包含各个功能模块的头文件。

例如一个比较标准的写法是:

c 复制代码
#include "stm32f10x.h"

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

这种顺序在 FreeRTOS 项目中非常常见,也是比较规范的一种写法。

在工程中集成FreeRTOS

首先,我们需要在工程根目录下创建一个"FreeRTOS"文件夹,然后将上面移植选中的文件都复制到文件夹下。

这些FreeRTOS的源码文件,都不应该手动被修改,所以应当给它们设置为只读,也就是Keil5中会显示加锁。

如下图所示:

除此之外,我们还需要将一段代码,彻底覆盖到"main.c"文件中。如下所示:

c 复制代码
#include "stm32f10x.h"      // STM32F10x 标准外设库头文件

#include "FreeRTOS.h"      // FreeRTOS 核心头文件
#include "task.h"          // FreeRTOS 任务相关 API

// 任务1:控制 PA0 上的 LED 闪烁
void vTask1(void *arg) {
    while (1) {
        // 点亮 PA0 LED
        GPIO_SetBits(GPIOA, GPIO_Pin_0);

        // 延时 1000 个系统节拍,也就是1s
        vTaskDelay(1000);

        // 熄灭 PA0 LED
        GPIO_ResetBits(GPIOA, GPIO_Pin_0);

        // 再次延时 1000 个系统节拍,也就是1s
        vTaskDelay(1000);
    }
}

// 任务2:控制 PA4 上的 LED 闪烁
void vTask2(void *arg) {
    while (1) {
        // 点亮 PA4 LED
        GPIO_SetBits(GPIOA, GPIO_Pin_4);

        // 延时 1000 个系统节拍,也就是1s
        vTaskDelay(1000);

        // 熄灭 PA4 LED
        GPIO_ResetBits(GPIOA, GPIO_Pin_4);

        // 再次延时 1000 个系统节拍,也就是1s
        vTaskDelay(1000);
    }
}

int main(void) {
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

    // 两盏LED引脚初始化
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_4;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 3. 默认关闭 LED
    GPIO_ResetBits(GPIOA, GPIO_Pin_0);
    GPIO_ResetBits(GPIOA, GPIO_Pin_4);

    // 4. 创建任务1
    xTaskCreate(vTask1, "Task1", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);

    // 5. 创建任务2(与任务1优先级相同)
    xTaskCreate(vTask2, "Task2", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);

    // 6. 启动 FreeRTOS 调度器
    vTaskStartScheduler();

    // 理论上不会运行到这里
    while (1) {
    }
}

将这段代码,和FreeRTOS文件夹都添加到Keil5工程中,并且不要忘记将FreeRTOS文件夹添加到头文件包含目录。

如此,我们直接启动编译这个工程,会出现以下明显的报错:

这段报错非常清楚,意思就是:缺少了FreeRTOSConfig.h头文件。

这非常明显是一个配置文件,那么这个配置文件用来做什么呢?

简单介绍一下这个配置文件:

不同项目,对 FreeRTOS 的需求是完全不一样。

哪怕是同一个FreeRTOS,不同人使用,用出来的效果也是不一样的。

简单来说,FreeRTOSConfig.h 是 FreeRTOS 的"用户配置文件",用来告诉 FreeRTOS:这个系统该怎么跑。

FreeRTOS内核的功能很丰富,但不是所有功能你都用的上,甚至有些功能根本就是冲突的,只能多选一。

FreeRTOS内核是功能大全,FreeRTOSConfig.h配置文件是告诉它做哪些,怎么做。

没有FreeRTOSConfig.h配置文件,FreeRTOS 就不知道你要什么样的操作系统,也就无法运行了。

关于FreeRTOSConfig.h配置文件的使用,我们会在后续学习中再不断补充讲解,目前知道这些就足够了。

那么问题来了,这个配置文件去哪里找呢?

很简单,既然启动FreeRTOS,一定要这个配置文件。那么我们就去官方的Demo案例中抄一个过来就好了。

所以,我们只需要找到关于STM32F103C8T6相关的FreeRTOS官方示例,就可以拿到我们需要的配置文件了。

那么去哪里找STM32F103C8T6相关的FreeRTOS官方示例?

首先,我们下载的FreeRTOS源码中是没有的。

毕竟各种架构、各种MCU种类太多,示例也太多了,都放进去会导致源码包的大小增大。

所以需要我们自己去GitHub上下载示例。

在**"examples文件夹"**下,有一个"README.md"文件,其中就有这个链接:

打开这个网站后,找到下面的位置,找到配置文件。

当然,如果你无法打开这个网站下载,在仓库中,我也准备好了这个文件,你直接去拿过来就可以了。

当然你可以直接自己创建这个文件,然后完整复制下面的内容:

c 复制代码
/*
 * FreeRTOS V202212.00
 * Copyright (C) 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * https://www.FreeRTOS.org
 * https://github.com/FreeRTOS
 *
 */

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/*-----------------------------------------------------------
 * Application specific definitions.
 *
 * These definitions should be adjusted for your particular hardware and
 * application requirements.
 *
 * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
 * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE. 
 *
 * See http://www.freertos.org/a00110.html
 *----------------------------------------------------------*/

#define configUSE_PREEMPTION        1
#define configUSE_IDLE_HOOK         0
#define configUSE_TICK_HOOK         0
#define configCPU_CLOCK_HZ          ( ( unsigned long ) 72000000 )  
#define configTICK_RATE_HZ          ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES        ( 5 )
#define configMINIMAL_STACK_SIZE    ( ( unsigned short ) 128 )
#define configTOTAL_HEAP_SIZE       ( ( size_t ) ( 17 * 1024 ) )
#define configMAX_TASK_NAME_LEN     ( 16 )
#define configUSE_TRACE_FACILITY    0
#define configUSE_16_BIT_TICKS      0
#define configIDLE_SHOULD_YIELD     1

/* Co-routine definitions. */
#define configUSE_CO_ROUTINES       0
#define configMAX_CO_ROUTINE_PRIORITIES ( 2 )

/* Set the following definitions to 1 to include the API function, or zero
to exclude the API function. */

#define INCLUDE_vTaskPrioritySet        1
#define INCLUDE_uxTaskPriorityGet       1
#define INCLUDE_vTaskDelete             1
#define INCLUDE_vTaskCleanUpResources   0
#define INCLUDE_vTaskSuspend            1
#define INCLUDE_vTaskDelayUntil         1
#define INCLUDE_vTaskDelay              1

/* This is the raw value as per the Cortex-M3 NVIC.  Values can be 255
(lowest) to 0 (1?) (highest). */
#define configKERNEL_INTERRUPT_PRIORITY         255
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY    191 /* equivalent to 0xb0, or priority 11. */


/* This is the value being used as per the ST library which permits 16
priority values, 0 to 15.  This must correspond to the
configKERNEL_INTERRUPT_PRIORITY setting.  Here 15 corresponds to the lowest
NVIC value of 255. */
#define configLIBRARY_KERNEL_INTERRUPT_PRIORITY 15

#endif /* FREERTOS_CONFIG_H */

注意,既然这个文件是一个配置文件,那么它存在的目的就是为了被程序员修改。

所以我们把它放入"User"目录下。

如此就完成了吗?

我们再次编译工程,发现编译通过了。

烧录一下,看看能不能执行成功呢?

发现无法成功。

这是正常的,因为我们还缺一步重要的步骤没有完成。

首先,我们打开"stm32f10x.c"文件,如果你的标准库工程和之前讲的一样配置,那么它就在User目录下。

然后将SVC、PendSV、以及SysTick这三个中断服务程序注释掉。

如下一段代码所示:

c 复制代码
/**
  * @brief  This function handles SVCall exception.
  * @param  None
  * @retval None
  */
//void SVC_Handler(void)
//{
//}

/**
  * @brief  This function handles Debug Monitor exception.
  * @param  None
  * @retval None
  */
void DebugMon_Handler(void)
{
}

/**
  * @brief  This function handles PendSVC exception.
  * @param  None
  * @retval None
  */
//void PendSV_Handler(void)
//{
//}

/**
  * @brief  This function handles SysTick Handler.
  * @param  None
  * @retval None
  */
//void SysTick_Handler(void)
//{
//}

然后再在FreeRTOSConfig.h配置文件的下面,加上这一段内容:

c 复制代码
// --------------- 个人修改FreeRTOS配置文件  ---------------------

//  让FreeRTOS接管这三个中断服务程序,使得FreeRTOS可以正常运行
#define xPortPendSVHandler PendSV_Handler
#define vPortSVCHandler SVC_Handler
#define xPortSysTickHandler SysTick_Handler


// --------------- 个人修改FreeRTOS配置文件  ---------------------

注意,不要把上面的内容放到"头文件保护"下面去了。

最终的效果如下图所示:

这一连串操作的目的只有一个:

让FreeRTOS接管SVC、PendSV以及SysTick三个系统中断的处理。

实际上之前它们的默认处理是空壳,这会导致FreeRTOS调度卡死,任务永远不会开始执行。

关于这一部分,FreeRTOS是如何启动调度的,详细内容仍然放到后面讲,目前大家先这么操作就好了。

如此,再编译代码烧录,你就会发现两盏LED做到了和之前不使用FreeRTOS一样的效果:一起闪烁。

这是什么原因导致的呢?

结合我们上一个章节,讲过的任务调度机制,分析结果如下:

  1. vTaskDelay() 会让任务进入阻塞态,期间不占用 CPU,而不是原地空转。
  2. 两个任务优先级相同,被调度器公平对待,谁进入就绪状态,谁就能运行。
  3. 任务每次延时结束,进入就绪状态,只做极短的 GPIO 操作,随后立刻再次 vTaskDelay()进入阻塞状态。
  4. 所以两个任务,轮流就绪上CPU点灯熄灯,轮流阻塞下CPU。
  5. 点灯熄灯操作的速度非常快,再加上任务之间的切换非常快,人肉眼察觉不到这种差异。
  6. 所以人的感觉就是两盏灯都同时在闪烁。

思考一下:让两个任务的优先级不一致,LED闪烁的现象会改变吗?

现在把两个任务的执行函数,改成下面的形式:

c 复制代码
// 任务1:控制 PA0 上的 LED 闪烁
void task1(void *arg) {
    while (1) {
        // 点亮 PA0 LED
        GPIO_SetBits(GPIOA, GPIO_Pin_0);

        // 使用for循环延时
        for(uint32_t i = 1000000; i > 0; i--);

        // 熄灭 PA0 LED
        GPIO_ResetBits(GPIOA, GPIO_Pin_0);

        // 使用for循环延时
        for(uint32_t i = 1000000; i > 0; i--);
    }
}

// 任务2:控制 PA4 上的 LED 闪烁
void task2(void *arg) {
    while (1) {
        // 点亮 PA4 LED
        GPIO_SetBits(GPIOA, GPIO_Pin_4);

        // 使用for循环延时
        for(uint32_t i = 1000000; i > 0; i--);

        // 熄灭 PA4 LED
        GPIO_ResetBits(GPIOA, GPIO_Pin_4);

        // 使用for循环延时
        for(uint32_t i = 1000000; i > 0; i--);
    }
}

在两个任务优先级一致、开启时间片轮转的情况下:

此时,这两个LED又是什么情况呢?

和之前的状态是一致的:仍然是两盏LED同时闪烁。

虽然现象一致,但实际情况已经大不相同了:

  1. 此时两个任务优先级是一致的
  2. 两个任务中都没有主动进入阻塞的Delay函数。
  3. 所以此时两个任务采用时间片轮转,轮流上CPU,并发执行。
  4. 人肉眼察觉不出这种时间差距,所以人的感觉是两个LED同时闪烁。

再思考一下:让两个任务的优先级不一致,LED闪烁的现象会改变吗?

创建FreeRTOS模板工程

在你完成上述FreeRTOS移植,并完成两个LED闪烁实验后,可以将main.c文件中的多余内容删除。

main.c文件内容如下:

c 复制代码
#include "stm32f10x.h"      // STM32F10x 标准外设库头文件

#include "FreeRTOS.h"      // FreeRTOS 核心头文件
#include "task.h"          // FreeRTOS 任务相关 API

// 任务1:
void vTask1(void *arg) {
    while (1) {
        vTaskDelay(1);
        // 任务不能空转,所以延时1ms
    }
}

// 任务2:
void vTask2(void *arg) {
    while (1) {
        vTaskDelay(1);
        // 任务不能空转,所以延时1ms
    }
}

int main(void) {
    // 1. 创建任务1
    xTaskCreate(vTask1, "Task1", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);

    // 2. 创建任务2(与任务1优先级相同)
    xTaskCreate(vTask2, "Task2", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);

    // 3. 启动 FreeRTOS 调度器
    vTaskStartScheduler();

    // 理论上不会运行到这里
    while (1) {
    }
}

然后打开FreeRTOS配置文件,保留一些个人配置。FreeRTOSConfig.h文件内容如下:

c 复制代码
/*
 * FreeRTOS V202212.00
 * Copyright (C) 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * https://www.FreeRTOS.org
 * https://github.com/FreeRTOS
 *
 */

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/*-----------------------------------------------------------
 * Application specific definitions.
 *
 * These definitions should be adjusted for your particular hardware and
 * application requirements.
 *
 * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
 * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE. 
 *
 * See http://www.freertos.org/a00110.html
 *----------------------------------------------------------*/

#define configUSE_PREEMPTION        1
#define configUSE_IDLE_HOOK         0
#define configUSE_TICK_HOOK         0
#define configCPU_CLOCK_HZ          ( ( unsigned long ) 72000000 )  
#define configTICK_RATE_HZ          ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES        ( 5 )
#define configMINIMAL_STACK_SIZE    ( ( unsigned short ) 128 )
#define configTOTAL_HEAP_SIZE       ( ( size_t ) ( 17 * 1024 ) )
#define configMAX_TASK_NAME_LEN     ( 16 )
#define configUSE_TRACE_FACILITY    0
#define configUSE_16_BIT_TICKS      0
#define configIDLE_SHOULD_YIELD     1

/* Co-routine definitions. */
#define configUSE_CO_ROUTINES       0
#define configMAX_CO_ROUTINE_PRIORITIES ( 2 )

/* Set the following definitions to 1 to include the API function, or zero
to exclude the API function. */

#define INCLUDE_vTaskPrioritySet        1
#define INCLUDE_uxTaskPriorityGet       1
#define INCLUDE_vTaskDelete             1
#define INCLUDE_vTaskCleanUpResources   0
#define INCLUDE_vTaskSuspend            1
#define INCLUDE_vTaskDelayUntil         1
#define INCLUDE_vTaskDelay              1

/* This is the raw value as per the Cortex-M3 NVIC.  Values can be 255
(lowest) to 0 (1?) (highest). */
#define configKERNEL_INTERRUPT_PRIORITY         255
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY    191 /* equivalent to 0xb0, or priority 11. */


/* This is the value being used as per the ST library which permits 16
priority values, 0 to 15.  This must correspond to the
configKERNEL_INTERRUPT_PRIORITY setting.  Here 15 corresponds to the lowest
NVIC value of 255. */
#define configLIBRARY_KERNEL_INTERRUPT_PRIORITY 15

// --------------- 个人修改FreeRTOS配置文件  ---------------------

// 让FreeRTOS接管这三个中断服务程序,使得FreeRTOS可以正常运行
#define xPortPendSVHandler PendSV_Handler
#define vPortSVCHandler SVC_Handler
#define xPortSysTickHandler SysTick_Handler

// 显式配置,启用时间片轮转机制
#define configUSE_TIME_SLICING  1

// --------------- 个人修改FreeRTOS配置文件  ---------------------


#endif /* FREERTOS_CONFIG_H */

如此,这样的一个SPL库集成FreeRTOS的Keil5工程,就可以称得上是一个FreeRTOS标准模板工程了。

当然,后续随着我们对FreeRTOS的深入学习,我们还会在这个标准模版下面,再增加一些内容,以适应我们的需求。

FreeRTOS的剪裁

关于 FreeRTOS 的移植,上面已经讲清楚了。

说白了,所谓的移植,解决的是 FreeRTOS 能不能在这颗芯片上跑起来。

那什么叫FreeRTOS的剪裁呢?

FreeRTOS 本身提供的功能非常多,但具体到某一个项目中:

  1. 并不是所有功能都会用到,用不到的功能就没有必要保留。
  2. 有些功能存在多种实现方式,这些实现方式之间可能互斥,需要选择其中一种。
  3. 有些功能默认是关闭的,如果项目需要,就必须手动开启。
  4. ...

为了解决这些问题,在使用 FreeRTOS 时,必须通过配置FreeRTOSConfig.h 这个头文件,来明确:

  1. 哪些功能需要使用,让它们参与编译
  2. 哪些功能被关闭
  3. 采用哪一种功能实现方式
  4. ...

这种通过配置宏来决定功能取舍的过程,就称为 FreeRTOS 的剪裁。

说白了,FreeRTOS 原本给你的是一整块布,剪裁就是把这块布按你的身材剪一剪,做成一件能穿、合身、而且还不浪费布料的衣服。

移植解决的是能不能跑的问题,那么剪裁解决的是 "跑的时候,用哪些功能"。

几个剪裁的案例

举几个例子:

第一,是否要启动抢占式调度:

c 复制代码
#define configUSE_PREEMPTION    1

1:开启抢占式调度(高优先级任务可立即抢 CPU)

0:关闭抢占,只能协作式调度

第二,是否启用时间片轮转:

c 复制代码
#define configUSE_TIME_SLICING  1

FreeRTOS默认开启时间片轮转机制。

1:开启时间片轮转,同优先级任务轮流运行

0:关闭时间片轮转,同优先级任务不轮转,谁先跑谁一直跑到阻塞

第三,配置创建任务的方式(动态任务):

FreeRTOS默认开启动态创建任务的方式,类似时间片轮转。

但也可以手动配置开启与否。

c 复制代码
#define configSUPPORT_DYNAMIC_ALLOCATION   1

1:允许使用动态内存方式 创建任务,可以用xTaskCreate()函数创建任务

0:禁止动态创建任务,xTaskCreate() 禁止使用。

第四,配置创建任务的方式(静态任务):

FreeRTOS默认关闭静态任务的创建方式。

xTaskCreateStatic()函数,在默认情况下是无法使用的。

如果需要这种方式创建任务,可以手动开启:

c 复制代码
#define configSUPPORT_STATIC_ALLOCATION    1

1:允许使用静态内存方式 创建任务,可以用xTaskCreateStatic()函数创建任务

0:禁止静态创建任务,xTaskCreateStatic() 禁止使用。

关于这两种创建任务的方式,是后续的内容,这里大家先知道一下即可。

关于裁剪的使用建议

首先我们要明确一点:

在 FreeRTOS 的剪裁中,最重要、最直观的一件事,就是决定:哪些 API 要参与编译并使用,哪些 API 会直接被移除。

实际上,在FreeRTOS中:

几乎所有的对外开放的API,即用户可以直接调用的函数,都可以在配置文件中配置保留使用或直接移除。

因此,在后续学习 FreeRTOS 各类 API 时,我们可以先暂时不去关心该函数"具体是干什么的",

而是优先关注与剪裁相关的几个问题:

  1. 该函数是不是默认开启,或者默认关闭
  2. 如果需要使用该函数,建议在配置文件中显式配置对应宏,即便它默认是开启的。
  3. 如果不使用该函数,且该函数默认就是关闭的,通常不需要进行额外的宏配置。

也就是说:

我们实际使用了哪些 API,建议通过宏配置的方式明确标注出来。

这样的操作在功能上来说,有可能毫无意义,毕竟有些API可能是默认开启的。

但从工程的角度出发,这增强了软件系统的确定性,清晰地界定了工程中 API 的使用边界。

剪裁的好处

那么,在 FreeRTOS 剪裁中,将不需要的函数/代码片段移除,具体有哪些好处呢?

需要明确的是:

通过宏配置去掉某些函数/代码片段,这个操作发生在预处理阶段。

这意味着,一旦某一段代码被裁剪掉:

  1. 对应的源码片段不会参与后续的编译与链接,相应参与构建目标文件的有效代码规模就随之减少。
  2. 最终生成的可执行程序体积也会相应减少,从而降低 Flash 占用。
  3. 除了降低 Flash 占用,源码中还可能包含全局/静态变量或扩展结构体成员,裁剪这些功能也可能减少运行时 RAM 的占用。

除了减少资源占用外,还有以下两点对工程而言,更加重要的收益:

  1. 被裁剪的API不会被编译,即便你想误调用也不可能,因为会编译报错。
  2. 通过剪裁明确了工程使用API的边界,哪些可用,哪些不可用,系统的确定性更强,更加利于维护管理工程。

基于HAL库的FreeRTOS移植

上面讲完了,基于标准库的FreeRTOS移植。

而基于HAL库的STM32开发方式,我们早就有所耳闻了,但从来没有实践过。

因此,在这一小节,我们不妨手动实践一下, HAL 库的基本开发方式,并进一步完成 在 HAL 工程中集成 FreeRTOS

创建基于HAL库的开发工程

在正式动手之前,我们还是先简单回顾一下 HAL 库开发方式的优缺点。

HAL库的开发方式有以下优点:

  1. 硬件抽象程度高,可移植性强。
    1. 通俗的说,HAL库开发方式将底层细节都进行了深度封装,使得不同型号的STM32单片机几乎可以采用相同的API进行开发。
    2. 你在 F1、F4、F7等不同型号的STM32单片机上写 HAL库 开发代码,写法几乎一样,换芯片时不需要大张旗鼓的重新开发。
  2. 官方主推,生态完善。
    1. 通俗的说,HAL库是官方的亲儿子,主推的开发方式,生态完善成熟。
    2. 实际上,标准库的开发方式,早在2016年左右就停止更新了。
  3. 开发简单易上手,开发效率高。
  4. STM32的F0、F3、F4、F7等系列,这些较新款的单片机,没有SPL库支持,只能使用HAL库进行开发。
  5. HAL库与FreeRTOS这样的中间层实时操作系统天然适配,HAL 在设计时就考虑了 RTOS 场景,已经进行了统一封装。

HAL库的开发方式有以下缺点:

  1. 封装层次太高,导致代码执行效率低下。不适用于追求性能的场景。
  2. 封装层次太高,中间层过多,导致代码体积臃肿。不适用于资源紧张的场景。
  3. 封装层次太高,隐藏了底层细节,降低了开发者对硬件细节的感知。
    1. 开发者无需关注硬件细节,在正常使用HAL库时是一件好事。
    2. 但一旦出现了Bug,定位Bug,修正Bug,将会是一场灾难。

了解了HAL库开发方式的优缺点,下面我们来实践一下创建基于HAL库的开发工程。

首先,我们需要使用一款软件------STM32CubeMX

它是 ST 官方围绕 HAL 库开发体系 打造的一款核心配置与代码生成工具。

STM32CubeMX 采用 图形化配置 + 自动代码生成 的方式:

通过引脚分配、外设参数及时钟系统的可视化配置,就可以快速生成一个 基于 HAL 库的工程框架。

简单来说,只需要"点点点",就能把一个 HAL 工程的骨架先搭起来,是一款在实际工程中非常实用的工具。

安装STM32CubeMX

你可以再git仓库中找到,整个安装过程下一步即可,傻瓜式安装

创建新项目

安装好软件后,开始创建新项目。如下图所示:

点击New创建后,第一次使用,会要求你下载资源。

这个过程可能比较慢,耐心等待即可。

如果出现下载不动的情况,可能就是IP地址被ST官网服务器限流了:

  1. 要么就等明天再试试
  2. 要么换个网络环境试一试
  3. 要么考虑用一些科技手段

后续的过程中,如果还需要下载资源,都有可能出现这种下载不动的情况,如果遇到了,可以自己尝试排除一下。

加载好资源,会进入下图中的界面,会让你选择使用的单片机。

请按照下图的顺序进行选择即可:

选择好单片机后,进入下一步。

首先,我们对"SYS"系统选项卡进行选择,如下图所示:

紧接着,我们要对"RCC"选项卡,进行选择。按照下图的方式进行选择:

首先,RCC选项卡用于确定单片机是否使用外部时钟信号源。

关于时钟信号源,早在STM32基本概述时,我们就讲过了。到了今天,我们已经对时钟信号有了一定的了解。

所以我们这里详细讲一下**"最小系统板的四种时钟信号源"**。

用一张表格来简单描述一下这四种时钟信号源:

时钟源 全称 中文全称 来源说明 时钟源频率 主要用途 说明一句话
HSI High Speed Internal 内部高速时钟源 内部 RC 振荡器 8 MHz 上电启动、系统备用时钟 上电默认使用,能跑但不够准
LSI Low Speed Internal 内部低速时钟源 内部 RC 振荡器 40 kHz 独立看门狗(IWDG) 精度低但可靠,用于系统保底
HSE High Speed External 外部高速时钟源 外部石英晶振 8 MHz 主系统时钟源 精度高、稳定,日常主时钟源
LSE Low Speed External 外部低速时钟源 外部陶瓷谐振器 32.768 kHz RTC 实时时钟 低功耗,用于长时间计时

你可以先记住以下结论,这些内容我们会在后续专门讲解时钟系统时再详细展开。

  1. HSI,内部高速时钟源,主要靠内部电路中的电阻和电容产生时钟源频率。时钟源频率通常是8MHz。
    1. 优点是体积小,启动迅速,上电即用。启动时间通常在微秒级别。
    2. 缺点是极其不准,强烈受温度、电压等外部条件影响。时钟频率的误差通常在1% ~ 3%之间。
    3. 基于上述特点,HSI 一般只用于系统上电启动阶段 ,通常是在 main 函数执行之前,为单片机提供最初的运行时钟。
    4. 在系统初始化完成后,通常会切换到精度和稳定性更高的外部时钟源。
    5. 这也是最小系统板,通常都需要集成外部晶振电路的根本原因。
  2. HSE,外部高速时钟信号源,通常是石英材质的晶振,时钟源频率通常也为8MHz。
    1. 优点是准度高,稳定性强。时钟频率的误差通常只有十万分之一到十万分之五。
    2. 缺点是体积较大,启动比较慢。启动时间通常在毫秒级别。

因此,在 STM32 等单片机中,系统的启动流程通常是:

先使用 HSI 完成上电启动和基础初始化

再等待 HSE 稳定后,将系统主时钟切换到 HSE。

这种设计方式兼顾了启动速度与运行精度,是嵌入式开发中非常经典,常用的时钟使用策略。


随后,假如我们想要点亮/熄灭PC13指示灯,就需要配置"GPIO"选项卡。

如下图所示:

如此,完成GPIO的相关配置。

这些配置,在随后创建工程时,都会自动生成相关的代码,这就是使用STM32CubeMX的便利之处。


随后对系统时钟进行相关的配置,如下图所示:

此界面展示了一种叫做"时钟树"的知识点概念,暂且先不赘述了。


随后配置一下工程的管理属性,如下图所示:

最后选择生成代码,如下图所示:

第一次使用STM32CubeMX生成代码时,会要求你从官方下载HAL库开发包,直接点击是即可。

如下图所示:

等待下载成功后,会出现下列选项框:

如此,你将打开这个HAL库开发工程,这样整个创建HAL库工程的过程就完成了。

如下图所示:

编写代码与调试程序

到了这一步,就是我们比较熟悉的Keil5的操作环节了。

但毕竟开发的方式从SPL标准库,变成了HAL库,所以使用的函数不一样了。

但很多代码,我们仍然能够认出来。比如下面的代码:

c 复制代码
/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    /* USER CODE BEGIN MX_GPIO_Init_1 */
    /* USER CODE END MX_GPIO_Init_1 */

    /* GPIO Ports Clock Enable */
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOD_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    /*Configure GPIO pin Output Level */
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);

    /*Configure GPIO pin : PC13 */
    GPIO_InitStruct.Pin = GPIO_PIN_13;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

    /* USER CODE BEGIN MX_GPIO_Init_2 */
    /* USER CODE END MX_GPIO_Init_2 */
}

这段代码就是我们熟悉的GPIO引脚初始化,这段代码完成:

  1. 初始化PC13引脚
  2. PC13引脚的工作模式,设置为通用开漏输出模式
  3. PC13引脚的引脚输出速度采用低速比
  4. PC13引脚,设置默认输出低电平,PC13指示灯点亮

上述代码,都是使用STM32CubeMX自动生成代码的。

我们直接编译链接,然后烧录。(这里就可以发现HAL库工程的编译明显较慢)

这时大概率PC13指示灯并不会点亮,这是因为HAL库工程默认没有开启"烧录自动复位"功能。

此时你可以将"烧录自动复位"功能打开,或者手动按最小系统板的复位按键进行复位。

除此之外,还需要注意所有程序员的代码,都必须写在User Begin和User End之间,否则后续CubeMX自动生成代码就会将你的代码覆盖掉。

c 复制代码
/* USER CODE BEGIN MX_GPIO_Init_2 */

/* USER CODE END MX_GPIO_Init_2 */

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

其他的,关于HAL代码究竟如何编写,这不是我们目前需要探究的问题。

大家如果以后需要用到,自行学习一下API即可,难度并不大。

HAL库集成FreeRTOS

首先,我们已经有了一个基于HAL库开发方式的Keil5工程。

既然已经有了Keil5工程,那么我们再进行手动的挑选FreeRTOS源码文件,手动进行FreeRTOS的移植,可不可以呢?

答:当然是可以的。

但这样做,显然有点侮辱STM32CubeMX这个工具了。

STM32CubeMX完全可以采用自动化配置"点点点"的方式,来完成创建一个集成FreeRTOS的HAL库工程。

流程如下:

其他流程保持一致,只有SYS选项卡稍作变动。为了更好演示任务,我们使用PA1和PA2两个引脚来驱动LED:

注意几点细节:

  1. STM32CubeMX软件通常是自带集成FreeRTOS的,不需要再次手动下载。
  2. 在使用FreeRTOS时,有两个版本可选择:
    1. CMSIS_V1,老版本。兼容老项目,但可读性和扩展性一般,建议只在维护老项目时使用。
    2. CMSIS_V1,新版本。ST公司的主推版本,可读性和扩展性都更好,推荐学习和做新项目时使用。
  3. 注意,不管是选择上面的哪一个版本。HAL库都对FreeRTOS源码做了一层封装,使其更符合HAL库的API风格。
  4. 所以,使用HAL库集成FreeRTOS进行开发,和标准库集成FreeRTOS进行开发,使用的函数也完全不同:
    1. 标准库是直接基于FreeRTOS源码开发,直接使用FreeRTOS提供的函数。
    2. HAL则是对FreeRTOS源码进行了二次封装,使其用起来更简便。
  5. 推荐使用CMSIS_V1版本,它使用起来更简单,V2版本使用起来配置很复杂,很可能跑不起来。

主时钟频率要选择成72MHz,不要忘记了:

其余设置保持不变,点击生成代码,并打开工程。

如下图所示:

直接点击编译,此时应当可以成功编译。

两个引脚PA1和PA2的配置如下:

两个任务执行的函数,代码如下:

随后,我们将下面的代码复制到任务1的,Begin和End之间(死循环内部)。

c 复制代码
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
osDelay(1000);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
osDelay(1000);

如下图所示:

随后,再把下面的代码复制到任务2的,Begin和End之间(死循环内部)。

c 复制代码
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
osDelay(1000);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);
osDelay(1000);

如下图所示:

如此,代码写完后,我们编译工程,然后烧录代码,查看实验的现象。

注意,还是不要忘记了,HAL库的工程并不会默认开启"烧录自动复位"功能。

相关推荐
嵌入式×边缘AI:打怪升级日志5 小时前
硬件清单与学习进度存档
学习
Engineer邓祥浩7 小时前
软件设计师备考 第0章 题型分布、示例、学习路线
学习·职场和发展
楷哥爱开发7 小时前
Facebook解封指南:4种封禁类型及其原因(附对应申诉方法)
网络·学习·安全
吃好睡好便好7 小时前
矩阵的乘法运算
数据结构·人工智能·学习·线性代数·算法·matlab·矩阵
水木流年追梦8 小时前
大模型入门-大模型优化方法1
人工智能·学习·算法·机器学习·正则表达式
摇滚侠8 小时前
IDEA 新建 Java 项目 学习 Java SE
java·学习·intellij-idea
叶~小兮9 小时前
K8s常用组件学习笔记
笔记·学习·kubernetes
星恒随风9 小时前
从零开始理解 ResNet(上):为什么 CNN 需要“残差连接”?
人工智能·笔记·神经网络·学习·cnn
z小猫不吃鱼9 小时前
08 BERT 论文精读:双向 Transformer 如何学习语言表示?
学习·bert·transformer