新手入门STM32-FreeRTOS教程(上)

前言

本文只负责指导一些问题,学单片机MCU还是以下列视频为主:

  1. 正点原子HAL库视频:
  1. 正点原子电机专项视频:
  1. 正点原子FreeRTOS:
  1. 大疆开发板C板开发教程:大疆开发板C型嵌入式软件教程文档.pdf

  2. 【【中科大RM电控合集】手把手Keil+STM32CubeMX+VsCode环境配置-哔哩哔哩】 https://b23.tv/5mwveRt

  3. 【ARM与STM32啥关系?不来了解一下嘛?-哔哩哔哩】 https://b23.tv/VvcYgUD

stm32单片机

单片机介绍

①什么是单片机?

单片机又称单片微控制器,它不是完成某一个逻辑功能的芯片,而是把一个计算机系统集成到一个芯片上。相当于一个微型的计算机,和计算机相比,单片机只缺少了I/O设备。概括的讲:一块芯片就成了一台计算机。它的体积小、质量轻、价格便宜、为学习、应用和开发提供了便利条件。同时,学习使用单片机也是了解计算机原理与结构的最佳选择。

②单片机的应用?

  1. 物联网(※)

  2. 医用设备

  3. 工业控制

  4. 计算机网络通信(※)

  5. … …

③stm32单片机组成部分

  1. CPU(中央处理器)

    1. 芯片框图

    1. 处理器内核

      1. 介绍与作用 : CPU所有的计算、接收/存储命令、处理数据全部由内核执行。

      2. 指令集分类 : ARM架构、X86架构、LoongArch架构,RISC-V架构

        1. ARM架构指令集

          1. 应用:广泛应用于移动行业(手机、平板、工控机等)等需要很强的能耗比的场景中

          2. 架构分类: ARM32、AArch64(ARM64)等

          3. 内核分类:ARM Cortex-X(手机) 、ARM Cortex-A(手机) 、 ARM Cortex-R(嵌入式) 、 ARM Cortex-M(嵌入式)

          4. 了解CPU Soc和CPU内核的区别

            小米玄戒o1,华为麒麟9000,意法半导体STM32F407VET6这三个芯片都是基于ARM架构。他们的CPU的核心的前端设计都是由英国ARM公司设计好的,ARM的内核决定了这个CPU的性能,总线,浮点运算器等等。
            
            而小米,华为,意法半导体只对CPU核心进行后端设计,对CPU性能等影响不会太大,**CPU绝大部分特征都是由ARM的前端设计决定的**。
            
            **所以,我们在使用F407IG,F407VE的时候,因为他们的CPU内核都是Cortex-M4,所以CPU特征都一样,所以代码也几乎都一样的。**
            

        1. X86架构指令集
        1.  应用:广泛应用于电脑、软路由、工控机等需要高性能计算的场景中
        
        2.  分类:X86、AMD64(X86\_64)
        
        1. LoongArch架构
        1.  应用:政府单位采购、军工采购、个人电脑等
        
        2.  分类:龙架构32位、龙架构64位
        
        1. RISC-V架构

    1. GPIO(通用输入/输出端口General-Purpose IO ports)

      1. 定义:CPU与外部进行信息交换的端口(Input输入、Output输出)

      2. 理解:CPU上的“金手指”(注意和电路板PCB上的引脚区分)

      3. 所在位置:通常把CPU焊在电路板PCB上,一般地,GPIO端口在电路板上经过某些电路最终被引出来成为电路板引脚

      4. 数量:STM32F407系列具有上百GPIO端口,由于数量过多,将其分为7个组(A,B,C… …),每组共16个IO口(0,1,2,3…15)

      5. 命名:P+GPIO组+IO口号(比如PA2,PB6等)

      6. 应用

        1. 普通输出IO口:输出高电平,或低电平

        2. 普通输入IO口:读取外部高低电平

        3. 复用IO口:可变为通信IO口与电脑、电机、蓝牙模块等通信(定时器PWM、串口UART、CAN通信、SWD调试通信、晶振IO口)

        ④原理图

        1. 介绍:顾名思义就是表示电路板上各器件之间连接原理的图表。(各元件在原理图中是用整体形式来表示,进行二次接线的图)

        1. 组成部分:

          1. 元器件(包括元器件端口)

          2. 导线

          3. 网络标号

          4. 电源符号

          5. 等… …

⑤芯片手册

  1. 作用:查询各种芯片信息(比如CPU频率,IO定义,时钟树等等)

软件介绍

  1. STM32库

    1. 各种开发方式:寄存器,标准库,HAL库,LL库

      1. 寄存器功能简单了解:寄存器就是一个离CPU内核更近的存储结构,所以与CPU内核交换数据比内存(RAM)更快,每个寄存器都有不同的功能,在寄存器里存不同的值,CPU读取后都会实现对应的不同功能。

      2. 库:库是源文件+头文件。stm32的库是由汇编语言、C语言混合编译而成(HAL库、LL库兼容C++)。现有标准库、HAL库、LL库(标准库已淘汰,咱们实验室使用HAL库和LL库)

      3. 各种开发方式的优缺点:

        1. 寄存器:这种开发方式硬件执行效率高,但由于STM32寄存器过于多,用寄存器写可读性差,且麻烦繁琐,故不建议全用寄存器写,在某些场合下可以偶尔使用。

        2. 标准库:太老了,现在已经淘汰,该库使用汇编+C语言进行开发,代码可读性很高,但是由于初期对标准库设计有些问题以及一些专利上的问题,导致会出一些问题,且时钟配置过于麻烦繁琐,所以咱们于2021级开始就不再使用标准库。

        3. HAL库、LL库(力推):ARM公司与ST意法半导体力推的库,符合ARM CMSIS标准,该标准是当今嵌入式开发者都需要遵循的一个标准。该库由汇编语言+C语言进行开发,且兼容C++(头文件中有extern "C"条件编译),使用C++的OOP(面向对象)进行开发要方便一万倍。HAL库和LL库仍然被ST公司维护中,其解决掉了标准库的各种确定,比如硬件IIC无法正常使用,时钟配置及其容易。ARM CMSIS标准介绍:CMSIS – Arm®

  1. 开发软件介绍:

    1. 搭建环境教程:STM32 Windows开发环境软件安装教程

    2. ARM Keil MDK

      1. 介绍:可进行开发各种基于ARM Cortex系列内核开发的CPU的单片机(stm32),也可以开发其他类型单片机

      2. 作用:进行单片机的代码编辑(edit)、编译(compile)、构建(build)以及下载(download)与调试(debug)。

      3. 版本选择:

        1. MDK 5.3及以上:建议使用,但需要自己装ARMCC编译器。

        2. MDK 6及以上:详见下方的VScode

        3. 折中方案(Keil MDK5 + VScode +Keil Assistant): 详见下方的VScode

    3. STM32 CubeMX

      1. 介绍:用图形界面生成STM32 HAL库部分驱动层代码的软件,由ST公司开发,仅支持STM32系列单片机。

      2. 作用:后期开发使用,进行STM32单片机的驱动层的基本配置(比如时钟树、GPIO、各种外设通信、中断、嵌入式实时操作系统等的配置),前期新手禁止使用STM32 CubeMX这款软件**,不然就和没学一样。前期新手只可以用该软件生成时钟函数,其他的一切操作概不允许,可以了解一下,但不准作为主力开发工具。** (大概熟练掌握CAN通信,DMA等就可以使用该软件了)

    4. STM32 CubeIDE(选用,没有需求就不要去使用)

      1. 介绍:跨平台的STM32单片机开发平台,仅支持STM32系列单片机,且只能用ARM-GCC编译器(该编译器远远比不上MDK5和MDK6上的ARM-Clang编译器,甚至部分性能也比不上ARM-CC编译器)
    5. VScode

      1. 介绍:由微软开发的,开源的,世界第一的万能编辑器

      2. 作用:只是个编辑器(类似记事本),不自带编译器(比如GCC、MSVC,ARMCC(AC5),ARMCLANG(AC6),ARM-GCC),需要自己配置环境才能够正常开发C/C++,CMake,Python,ROS2,单片机等。

      3. 优点:①图形界面非常优美,②可跨平台,在Windows,Linux,MacOS上均能使用,③有非常多好用的插件。

      4. 缺点:①VSCode是使用Electron开发的,约等于塞了一个Google Chromium浏览器内核,非常占内存。②且环境难配置,但是这是必须要学的。

      5. 插件:

        1. Keil Studio Pack(Keil MDK 6,截止2024年1月2日,推荐熟练使用keil5后再使用) MDK6已经基本完善了,可以使用,但是不建议使用。MDK6学习成本比较高,对新手不友好,且MDK5还在更新维护,所以建议使用MDK5.3及以上。但MDK 6基于MS VScode编辑器开发,实现了跨平台,可在Windows,Linux,MacOS上进行开发,且界面非常优美,所以未来可期。ARM Keil MDK6使用教程

        1. Keil Assistant(后期开发建议使用,可以代替Keil MDK 5.3 完成代码编译(edit),但是编译,构建,下载,调试仍然建议在Keil MDK 5.3 上使用)在Windows上用MDK5软件配合Vscode的keil assistant插件进行开发。【VS Code开发stm32和51单片机的教程,vscode代替Keil-哔哩哔哩】 https://www.bilibili.com/video/BV18e4y1H7xX

时钟树

①使用CubeMX配置时钟的步骤

  1. 时钟配置介绍:这是每一个工程都需要做的事情,给予CPU正常的心跳。

  2. 作用:给予CPU正常的心跳,并且给予各个外设的心跳,让CPU和其各个外设正常工作。(比如延时函数的准确度,定时器PWM波形的准确度)

  3. STM32F1系列CPU时钟框图:

  1. 配置需要注意的事项:

    1. 注意电路板上HSE的真实晶振频率,填高了会导致超频,会出现比较严重的问题

    2. 配置时建议用CubeMX配置时钟函数,然后复制到正点原子模板工程中(因为手撸时钟函数太难了)

    3. CubeMX参考文档:大疆开发板C型嵌入式软件教程文档.pdf

  2. 配置步骤(这里只点出几个要注意的点,详细步骤请看大疆C板开发文档):

    1. 打开大疆C板开发文档

    2. 找到目录,点击0.4.2

    1. 按照0.4.2的步骤开始操作(每一步必须都得做,特别是Debug选Serial Wire,不选的话该工程代码会让板子假变砖)

      1. 需要注意板子型号,大疆板子是stm32f407igh6,咱们需要根据咱们实际的板子型号进行选择

      1. 配置时钟树时,需要注意HSE的时钟频率,按照实际原理图上的HSE频率来配置

      1. 代码路径必须全是英文,并且不能有连续两个空格,建议直接不要空格,单词之间用下划线(不可以放在桌面上)

      1. 解释

    2. 打开CubeMX生成的MDK 5工程

    1. 再复制一个并打开正点原子的模板工程

    1. 在CubeMX HAL库工程中的main.c中找到时钟函数void SystemClock_Config(void)的定义

    1. 复制整个void SystemClock_Config(void)函数的定义

    1. 然后打开正点原子的工程,在main函数中找到sys_stm32_clock_init(RCC_PLL_MUL9)函数,右键该函数,并go to definition of "sys_stm32_clock_init"找到这个函数的定义。

    1. 如果弹出下方的问题,请按照这个框框中的提示来解决,说的很明白。(如果看不懂英语,就去百度搜,锻炼下搜索能力)

    1. go to definition of "sys_stm32_clock_init"完后找到这个函数的定义,删掉整个函数,并把刚才复制的CubeMX HAL库里的时钟函数复制到这里。并将Error_Handler();直接删掉,或者替换成while(1);

    2. 找到sys_stm32_clock_init函数定义

    1. 框选后删掉

    1. 把复制的CubeMX HAL库里的时钟函数复制到这里。

    1. 用while(1);替换掉Error_Handler();或直接删掉。

    1. 找到void SystemClock_Config(void)函数所在的源文件sys.c对应的头文件sys.h

    1. 找到sys_stm32_clock_init(uint32_t plln)函数,删掉,替换成void SystemClock_Config(void)的声明。

    1. 回到主函数,找到sys_stm32_clock_init(RCC_PLL_MUL9);函数,删掉,并调用咱们新的时钟函数

    1. 修改HSE_VALUE
    2. 随便找个地方输入HSE_VALUE并go to definition(go to definition完毕后,就可以删掉这个自己写的HSE_VALUE)

    1. 修改HSE_VALUE的值(如果是8MHz就写8000000U,如果是12MHz就写12000000U)

    通过看原理图可知,该板子为8MHz。(具体填多少,看你板子HSE的原理图,对应OSCIN和OSCOUT这俩IO口)

    1. 删掉原来用来go to definition才写的HSE_VALUE

    2. 删掉多余的代码

    1. 第9行的delay_init的入口参数具体填什么值,先查看一下他的定义

    2. 查看delay_init的定义,得知其入口参数为sysclk(系统时钟)

    1. 查看CubeMX的时钟树框图,得知SYSCLK的值为72MHz

    1. 把delay_init的值改为时钟树中的SYSCLK的值

    1. 然后编译所有文件

    1. 零错误零警告即配置成功,有错误有警告请自行百度、谷歌

②查询某个外设时钟频率的方法(拿定时器来举例子)

  1. 打开tim.c

  1. 找到Msp初始化弱函数(看TIM的基句柄得知是哪个TIMx)

  1. 查找__HAL_RCC_XXX_CLK_ENABLE()的定义

  1. 根据函数定义,可以看出TIM1挂载在APB2上

  1. 查询时钟树,找APB2 Timer Clock可得TIM1的TCLK是168MHz

  1. 所以得知,TIM1的TCLK频率为168MHz

stm32程序组成

基本介绍(主函数等)

  1. 工程构成:stm32工程是由C语言和汇编语言的库组成的工程,所以有主函数,符合C/C++语言的结构。

  2. 程序运行顺序:除了预编译等,程序从主函数开始运行,而且非常符合C/C++运行顺序,从主函数开始会逐行运行代码,然后会进入死循环。

  3. 主函数内必须的组成部分:死循环[while(true)或者for(;;)],因为单片机要一直运行下去,所以有个死循环。

  4. HAL库 与 用户自定义库

    1. 库:

      1. .h文件声明函数

      2. .c/.cpp文件定义函数

      3. .c/.cpp文件调用函数

中断服务函数的介绍

  1. 特殊函数(中断服务函数):中断服务函数是由汇编定义的,与芯片硬件更紧密,是由芯片中断事件所触发,并不满足常规C/C++调用顺序。

    1. 中断服务函数的调用方式:由中断事件所触发。一旦满足某个中断事件,就立马从正在运行的地方切换到中断服务函数里开始运行,然后运行完中断服务函数后,再返回刚才运行的地方接着运行。

    2. 中断事件:比如说第X条线的外部中断事件、systick滴答定时器中断(普通延时函数的实现方式)、UART接收中断事件、UART发送中断事件、TIM定时器溢出更新中断、TIM定时器输入捕获中断、CAN通信发送中断事件、CAN通信接收中断事件、RTOS的PendSV中断等等。

    3. 中断服务函数处理过程:

      1. CPU检测到有中断事件的发生

      2. 保护现场,将当前位置的PC地址压栈(程序计数器(Program Counter));

      3. 跳转到中断服务函数,执行中断服务程序;

      4. 恢复现场,将栈顶的值回送给PC;

      5. 跳转到被中断的位置开始执行下一个指令。

    1. 中断优先级与分组

      1. 优先级:抢占优先级和子优先级

      2. 分组0-5

      1. 更改分组(在HAL_Init中更改)

      1. 中断服务函数内容:

        1. 先查询中断标志位,确定被触发的中断事件

        2. 清除对应标志位,防止中断一直被触发,好让下次中断正常运行

        3. 接收数据等(可选)

        4. 逻辑业务代码实现(可选,比如数据处理等)

      2. 中断服务函数的特点:

      1.  中断服务函数不能传入参数;
      
      2.  中断服务函数不能有返回值;
      
      3.  中断服务函数应该做到短小精悍;
      
      4.  迫不得已的情况下,不准在中断服务函数中使用延时函数,如果要使用延时,请设置好延时和中断的优先级,否则程序出卡死(除了外部中断为了软件消抖而设立的延时)
      
      5.  不要在中断服务函数中使用printf函数,会带来重入和性能问题。
      
      1. 举例:
      ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image107.webp)
      
      2.  USART1\_IRQHnadler函数
      
          1.  中文名:串口1\_中断服务函数
      
          2.  声明定义:由汇编声明,需要用户自己去定义(如果使能了,用户还不定义,程序将会卡在汇编代码中)
      
          3.  调用条件:由CPU中断事件调用
      
          4.  作用:被CPU调用,并调用紧急的中断程序(中断程序也就是中断服务函数里的内容)
      
      3.  HAL\_UART\_IRQHnadler(句柄)
      
          1.  中文名:串口\_中断**公共**服务函数(公共的意思指串口1,2,3,4,5......等所有的串口都共用这一个函数实现功能,由后面的句柄决定究竟是哪个函数被触发)
      
          2.  声明定义:ST公司编写的HAL库声明和定义
      
          3.  调用条件:由中断服务函数调用
      
          4.  作用:
      
              1.  先查询中断标志位,确定被触发的中断事件
      
              2.  清除对应标志位,防止中断一直被触发,好让下次中断正常运行
      
              3.  接收数据等(可选)
      
              4.  调用中断事件对应的中断回调函数
      
              5.  其他操作(比如特殊的,在串口接收中断里会disable失能中断,也就是关掉中断)
      
      4.  HAL\_UART\_RxCpltCallback(句柄)
      
          1.  中文名:串口\_中断回调函数(因为他被中断公共服务函数调用,所以句柄是由调用它的中断公共服务函数所决定)
      
          2.  声明定义:ST公司编写的HAL库声明为弱函数,需要用户自己去定义
      
          3.  调用条件:被中断公共服务函数调用
      
          4.  作用:先确定是哪个句柄调用的,再进行相应的业务逻辑实现(可选,比如数据处理等)
      
      ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image108.webp)
      
      ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image109.webp)
      

RTOS与ROS/ROS2简单了解

  1. 进阶(非裸机开发,基于RTOS系统开发)

    1. 常见的RTOS(嵌入式实时操作系统):FreeRTOS、Nuttx、RT-Thread、μC/OS-II、Xiaomi VelaOS

    2. FreeRTOS官网:FreeRTOS™ - FreeRTOS™

    1. FreeRTOS简单理解:拥有多线程库特性并兼容POSIX标准的操作系统

    2. 多线程:系统拥有多个任务(线程),每个任务(线程)独立并同时运行(可以理解成每个任务都是一个主函数,这些任务都是同时在执行的。具体实现方式以后再学,原理就是PendSV中断等)

  2. 进阶(非裸机开发,基于RTOS和ROS2_MicroROS)

    1. 使用方式:ESP32使用arduino库+FreeRTOS+MicroROS并通过串口与STM32进行通信。

    2. 主要作用之一:可通过WIFI远程与上位机(电脑、工控机)的ROS2进行更加安全、稳定的通信,对比直接用串口通信(rosserial),要好很多(DDS分布式)。

    3. MicroROS Vs ROSserial的详解链接: https://mp.weixin.qq.com/s/1lQXAA3sV-4GpXAzHiGChQ

寄存器

  1. 理解:是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。

  1. 实现的功能:

  1. 寄存器如何在基于C语言的HAL库中发挥作用的呢?

机器人队标准工程格式

英语

必须用 英语 ,工程文件名、函数名、变量名必须用英语!

正点原子HAL库工程标准:

机器人队STM32工程标准(Cube+C语言):

![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image120.webp)

1.  applications应用层

    ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image121.webp)

2.  bsp驱动层

![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image122.webp)

3.  Middlewares中间层

![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image123.webp)

4.  Core(主函数所在地,条件编译配置HAL库的头文件所在地)

![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image124.webp)

![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image125.webp)

机器人队STM32C/C++工程标准

    ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image126.webp)

    1.  应用层、驱动层等采用模块集成式,不再采用Src和Inc分离式

    ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image127.webp)

    ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image128.webp)

    2.  公共兼容层:

    3.  C++子main兼容库

        ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image129.webp)

        2.  作用:在.cpp文件中创建一个普通的函数,该函数调用C++的代码,然后被C语言main.c文件中的main函数所调用。

        ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image130.webp)

        ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image131.webp)

        5.  弱函数\_回调函数库(该文件的源文件全局要有extern "C",因为弱函数是C语言的东西,C++无法正常识别)

        ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image132.webp)

        ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image133.webp)

        ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image134.webp)

        ![](https://cdn.030204.xyz/tungwebsite/assets/images/2023/10/09/image135.webp)

(建议)机器人队STM32Cube C/C++工程标准(类Cube,试运行,建议):

首先打开CubeMX进行工程配置,比如我们这里用裸机开发使一个LED灯闪烁

然后选择OpenFolder打开文件夹

打开Github将一些必备文件进行克隆

仓库链接:

https://github.com/tungchiahui/CubeMX\_MDK5to6\_Template

或者直接打开terminal输入

git clone https://github.com/tungchiahui/CubeMX_MDK5to6_Template.git

打开克隆的模板与刚才CubeMX生成的工程

打开模板中的***工程文件移植(创建新模板请看这里)*** 文件夹,然后将里面的文件全部复制到CubeMX工程文件中。

移动后:

打开工程设置工程

  1. 打开MDK5工程

  1. 点击Options for Target

  1. 修改编译器为ARMClang[ARM Compiler6 (AC6)] 替换掉 ARMCC[ARM Compiler5 (AC5)]

  1. 添加头文件的路径(Include Path)

添加applications中的Inc文件夹

添加bsp/boards中的Inc文件夹

点击OK即可

  1. 添加源文件.c/.cpp等

打开Manage Project Items

创建两个分组

分组的名字分别叫

applications

bsp/boards

将Core/Src目录下的startup_main.cpp加入到Application/User/Core组中。

将bsp/boards/Src目录下的bsp_delay.cpp加入到bsp/boards组中。

可以看到工程里的文件都就绪了。

编译并配置一些必要代码

可以右键头文件,然后点Open Document "xxx.h"来打开头文件,用来检查头文件是否导入成功。

找到main.c文件,准备在主函数main()中调用C++的类主函数startup_main();

在USER CODE BEGIN Includes和USER CODE END Includes这两行注释中间 引用startup_main.h (因为不放在BEGIN和END之间的代码在CubeMX重新配置后,代码都会消失)

在USER CODE BEGIN和USER CODE END这两行注释中间 调用startup_main();(因为不放在BEGIN和END之间的代码在CubeMX重新配置后,代码都会消失)

打开startup_main.h

更改isRTOS宏的值,如果是裸机开发则为0,如果使用了FreeRTOS则改为1。

至此,你可以在startup_main()函数中随意调用C/C++库中的代码啦。

C++库的头文件格式

拿bsp_delay.h举例

#ifndef __BSP_DELAY_H_
#define __BSP_DELAY_H_

#ifdef __cplusplus
extern "C" 
{
#endif

#include "startup_main.h"

class BSP_Delay
{
        public:
                class F1
                {
                        public:
                                void Init(uint16_t sysclk);
                                void us(uint32_t nus);
                                void ms(uint16_t nms);
                }f1;
                class F4
                {
                        public:
                                void Init(uint16_t sysclk);
                                void us(uint32_t nus);
                                void ms(uint16_t nms);
                }f4;
                class FreeRTOS
                {
                        public:
                                void Init(void);
                }freertos;
};

extern BSP_Delay bsp_delay;

#ifdef __cplusplus
}
#endif

#endif

条件编译肯定不能少,一个是防止头文件重复引用的条件编译,一个是把C++链接为C语言的条件编译。

然后引用startup_main.h头文件

再创建该模块的类,比如说class BSP_LED等,我这里为延时的类,所以是class BSP_Delay。

然后写类里的声明。

注意:不要在.h文件里写任何代码实现,也就是不能写任何函数的定义。

然后第35行的extern BSP_Delay bsp_delay;是在主函数中进行了创建对象bsp_delay,我们要在这里声明(extern)一下对象(变量)。方便其他的源文件调用。

C++库的源文件格式

拿bsp_delay.cpp举例

#include "bsp_delay.h"

#if isRTOS == 1
    #include "cmsis_os.h"
#endif

static uint32_t g_fac_us = 0;       /* us延时倍乘数 */

BSP_Delay bsp_delay;

/**
 * @brief       初始化延迟函数
 * @param       sysclk: 系统时钟频率, 即CPU频率(HCLK)
 * @retval      无
 */
 void BSP_Delay::F1::Init(uint16_t sysclk)
{
    SysTick->CTRL = 0;                                          /* 清Systick状态,以便下一步重设,如果这里开了中断会关闭其中断 */
    HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);   /* SYSTICK使用内核时钟源8分频,因systick的计数器最大值只有2^24 */

    g_fac_us = sysclk / 8;                                      /* 不论是否使用OS,g_fac_us都需要使用,作为1us的基础时基 */
}

/**
 * @brief       延时nus
 * @param       nus: 要延时的us数.
 * @note        注意: nus的值,不要大于1864135us(最大值即2^24 / g_fac_us  @g_fac_us = 9)
 * @retval      无
 */
void BSP_Delay::F1::us(uint32_t nus)
{
    uint32_t temp;
    SysTick->LOAD = nus * g_fac_us; /* 时间加载 */
    SysTick->VAL = 0x00;            /* 清空计数器 */
    SysTick->CTRL |= 1 << 0 ;       /* 开始倒数 */

    do
    {
        temp = SysTick->CTRL;
    } while ((temp & 0x01) && !(temp & (1 << 16))); /* CTRL.ENABLE位必须为1, 并等待时间到达 */

    SysTick->CTRL &= ~(1 << 0) ;    /* 关闭SYSTICK */
    SysTick->VAL = 0X00;            /* 清空计数器 */
}

/**
 * @brief       延时nms
 * @param       nms: 要延时的ms数 (0< nms <= 65535)
 * @retval      无
 */
void BSP_Delay::F1::ms(uint16_t nms)
{
    uint32_t repeat = nms / 1000;   /*  这里用1000,是考虑到可能有超频应用,
                                     *  比如128Mhz的时候, delay_us最大只能延时1048576us左右了
                                     */
    uint32_t remain = nms % 1000;

    while (repeat)
    {
        us(1000 * 1000);      /* 利用delay_us 实现 1000ms 延时 */
        repeat--;
    }

    if (remain)
    {
        us(remain * 1000);    /* 利用delay_us, 把尾数延时(remain ms)给做了 */
    }
}

/**
 * @brief     初始化延迟函数
 * @param     sysclk: 系统时钟频率, 即CPU频率(rcc_c_ck), 168MHz
 * @retval    无
 */  
void BSP_Delay::F4::Init(uint16_t sysclk)
{
    HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);/* SYSTICK使用外部时钟源,频率为HCLK */
    g_fac_us = sysclk;                                  /* 不论是否使用OS,g_fac_us都需要使用 */
}

/**
 * @brief       延时nus
 * @param       nus: 要延时的us数.
 * @note        nus取值范围 : 0~190887435(最大值即 2^32 / fac_us @fac_us = 21)
 * @retval      无
 */
void BSP_Delay::F4::us(uint32_t nus)
{
    uint32_t ticks;
    uint32_t told, tnow, tcnt = 0;
    uint32_t reload = SysTick->LOAD;        /* LOAD的值 */
    ticks = nus * g_fac_us;                 /* 需要的节拍数 */
    told = SysTick->VAL;                    /* 刚进入时的计数器值 */
    while (1)
    {
        tnow = SysTick->VAL;
        if (tnow != told)
        {
            if (tnow < told)
            {
                tcnt += told - tnow;        /* 这里注意一下SYSTICK是一个递减的计数器就可以了 */
            }
            else 
            {
                tcnt += reload - tnow + told;
            }
            told = tnow;
            if (tcnt >= ticks)
            {
                break;                      /* 时间超过/等于要延迟的时间,则退出 */
            }
        }
    }
}

/**
 * @brief       延时nms
 * @param       nms: 要延时的ms数 (0< nms <= 65535)
 * @retval      无
 */
void BSP_Delay::F4::ms(uint16_t nms)
{
    uint32_t repeat = nms / 540;    /*  这里用540,是考虑到可能有超频应用, 比如248M的时候,delay_us最大只能延时541ms左右了 */
    uint32_t remain = nms % 540;

    while (repeat)
    {
        us(540 * 1000);        /* 利用delay_us 实现 540ms 延时 */
        repeat--;
    }

    if (remain)
    {
        us(remain * 1000);    /* 利用delay_us, 把尾数延时(remain ms)给做了 */
    }
}

void BSP_Delay::FreeRTOS::Init(void)
{
        //调用FreeRTOS自带的延时即可。
        //osDelay
        //vTaskDelay
        //vTaskDelayUntil
}

/**
  * @brief HAL库内部函数用到的延时
           HAL库的延时默认用Systick,如果我们没有开Systick的中断会导致调用这个延时后无法退出
  * @param Delay 要延时的毫秒数
  * @retval None
  */
void HAL_Delay(uint32_t Delay)
{
#if isRTOS==0   //如果是裸机开发

        #ifdef STM32F1  //如果是裸机开发且为F1
                        bsp_delay.f1.ms(Delay);
        #endif

        #ifdef STM32F4  //如果是裸机开发且为F4
                        bsp_delay.f4.ms(Delay);
        #endif

#elif isRTOS==1          //如果是FreeRTOS开发
                 osDelay(Delay);
#endif
}

刚上来肯定要引用自己的头文件。

这个条件编译不用管,因为延时在裸机开发和RTOS开发时有区别,所以我加了一行条件编译。

上来要先创建一下类对象bsp_delay;

然后把类里的函数都进行定义。

函数注释格式:

这一块是该函数的注释,以后尽量都这样写注释。(在以后MDK6中进行调用函数时,会提示该注释,一目了然)

这样写注释的好处,在调用时,会显示入口参数需要填什么,会显示返回值是什么。

brief 函数摘要

param 入口参数

retval 返回值

note或attention 注意事项

注意这里,有几个param入口参数,就写几个param

比如

/**
 * @brief       CAN1通信发送函数
 * @param       motor1: 第1个电机的相对电流值
 * @param       motor2: 第2个电机的相对电流值
 * @param       motor3: 第3个电机的相对电流值
 * @param       motor4: 第4个电机的相对电流值
 * @retval      bool是否发送成功
 * @note        无特殊注意事项
 */
 bool CAN_BUS::CAN1::CMD1(int16_t motor1,int16_t motor2,int16_t motor3,int16_t motor4)
 {
 // ... ...
 }

注意事项

  1. 在.cpp源文件中,弱函数的定义前面要加个extern “C” 因为__weak是C语言(汇编向量)特有的,所以必须把代码以C语言的形式链接。

  2. 代码要写在Begin和End之间,否则再次用CubeMX配置代码后,代码会消失。

1 个赞