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

stm32单片机(重点)

驱动

  1. 驱动,驱动程序全称设备驱动程序,能够使计算机与相应的设备进行通信。驱动程序是硬件厂商根据操作系统编写的配置文件,可以说没有驱动程序,计算机中的硬件就无法工作。

  2. 普通模块的驱动:GPIO初始化程序+通信协议程序 数据协议处理程序

    1. 例子:LED灯,只需要GPIO初始化程序;蓝牙模块,需要写GPIO初始化程序和通信协议程序+数据处理程序

    1. GPIO程序:

    1. 通信协议程序:如图是串口的通信协议程序以及GPIO程序

    1. 数据解析程序:如图是PS2手柄的数据处理函数(见C++题库数据解析的题型,主要用二进制,十六进制,位操作符等)

      1. 数据单位变换:

        1. 1Mbps(比特率) = 1000 000 bit/s(比特/秒) 1 byte(字节) = 8 bit(比特)= 8位二进制 (非常重要) 1 kbyte(千字节) = 1024 byte(字节) 1Mbyte(兆字节) = 1024 kbyte(千字节) 1Gbyte (千兆字节) = 1024Mbyte(兆字节)

        2. 1个字符 = 1 byte(字节)

          1个阿拉伯数字 = 1个字符

          在GBK编码下,1个汉字 = 2个字符

          在Utf-8编码下,1个汉字 = 3个字符

      2. 数据命名格式(详细请见C++文档):

    1. 针对HAL库外设API的填参方法:
    ![](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2023/10/09/image180.webp)
    
    2.  查看对应的数据类型
    
    3.  查数据手册查函数的内容的注释
    

大疆电机控制(CAN)

###   ①CAN通信简介
  1. 什么是CAN通信?

CAN总线通信系统是串行通信的一种,要优于串口RS485总线。与I2C、SPI等具有时钟信号的同步通讯方式不同,CAN通讯并不是以时钟信号来进行同步的,它是一种异步****半双工通讯。(差分信号,半双工)

  1. 串口通信逻辑电平表示方法的分类

    1. TTL(单片机上引脚常用电平,串口全双工,芯片IO口为TTL电平的RX,TX;信号线为TTL电平的RX,TX)

    2. RS232(电压范围比TTL高的一种电平,抗干扰较好,串口全双工,芯片IO口为TTL电平的RX,TX;信号线为RS232电平的RX,TX)

    3. RS485(差分信号,抗干扰极好,Modbus协议,串口半双工,芯片IO口为TTL电平的RX,TX;信号线为A和B)

  2. CAN通信信号线

    1. 差分信号,抗干扰极好,半双工,芯片IO口为TTL电平的CAN_RX,CAN_TX;信号线为CAN_H和CAN_L;(类似RS485)

②电机库代码解析(该库内容要求尽量全部看懂,尽量一行不差)

  1. 代码及其初始化

    1. 这部分正点原子都有讲,只需要把参数改为大疆电机的

    代码仓库链接:https://github.com/SDUTEMIS/SDUT\_VinciRobot/tree/main/1.Embedded\_STM32\_Driver%2FC%2F4.Motor\_Drivers%2F1.DJI\_CAN\_PID

  1. 大疆电机库开环代码解析:库由往届学长学姐对大疆官方库代码修改后的。

    1. CAN报文发送函数解析

    此函数是将电流值发送给大疆CAN1通信电机,CAN1通信每次只能发送8比特的数据,电流值是16比特的数据,所以把电流值向右移8位,然后再发送给电机。电机接收到电流值就开始转动(入口参数是电调ID为1-4的电机电流值)

    此函数是将电流值发送给大疆CAN1通信电机,CAN1通信每次只能发送8比特的数据,电流值是16比特的数据,所以把电流值向右移8位,然后再发送给电机。电机接收到电流值就开始转动(入口参数是电调ID为5-8的电机电流值)

    1. CAN报文发送函数调用
    int16_t Current_Motor_Target[1];
    
    void chassis_task(void const * argument)
    {
      //wait a time
      //空闲一段时间
      vTaskDelay(20);  //等待所有设备准备就绪
    
      while(1)   //可以在定时器中断里实现
      {
        Current_Motor_Target[0] = 1000;    //测试电机闭环是否可用的代码,正式使用时请注释该行代码
        CAN1_CMD_1(Current_Motor_Target[0],0,0,0);  //对电调ID为1的电机发送1000电流使其开环转动。
        //系统延时
        vTaskDelay(2);  //等同于osDelay(2);      
      }
    
    }
    
  2. 大疆电机库闭环代码解析:库由往届学长学姐对大疆官方库代码修改后的。(在开环基础上又加了CAN报文接收,以及一系列数据解析程序)

    1. CAN通信接收中断回调函数(CAN_RX0接收中断回调函数用来处理CAN通信电机发来的数据,也就是 电机的速度,角度,温度 等数据。)

    1. 数据解析函数

      1. 电机编码器分为增量式编码器和绝对值编码器

        1. 增量式编码器:上电时数据会丢失,角度从0开始

        2. 绝对值编码器:掉电后数据不会丢失。

        3. M3508和M2006编码器都为绝对值编码器,掉电后数据不会丢失,但是因为记录的是转子的角度,转子连接了一个减速器,所以导致数据是错误的,所以我们要用代码将绝对值编码器的数据转化为增量式编码器的数据来使用。(结构体里的total是绝对编码器的角度,而total_angle是我们处理后改为增量式编码器的总角度)

      2. 记录上电角度

      1. 计算总角度(经代码处理后,上电时总角度 = 圈数(0) *8192 + 当前绝对值编码器的角度(假设为A) - 上电时捕获到的上电角度(因为此时为上电时,所以也为A) = 0)

      1. 暂时用不着的函数(此函数没有被调用)

③PID控制器

  1. PID算法简介:

    1. MATLAB_PID控制器介绍:https://www.mathworks.com/help/control/pid-controller-design.html?s\_tid=CRUX\_lftnav
  2. PID算法原理

关于理解PID控制算法最典型的一个例子就是一个漏水的水缸的问题。

有个漏水的水缸,而且漏水的速度还不是恒定的。然后我们还有个水桶,我们可以控制往水缸里面加水或者从水缸里面舀水出来。另外我们可以检测水平面。现在我们的目的就是要控制水平面稳定在我们想要的任何一个平面上。

注意我们使用PID需要在一个闭环系统里面。什么叫闭环系统,就是有输入有反馈,输入就是能输入一个量去影响和控制我们的系统,反馈就是我们要能知道我们最终控制的东西的状态。在这个漏水的水缸系统中,输入就是这个水桶,我们能通过水桶往水缸里面加水或者从水缸里面舀水出来来影响我们水缸的水平面,反馈的话也就是说我们要能测量水平面,知道水平面是多少。

a, 比例控制理解

首先是比例控制。比例控制就好比是通过水桶往水缸加水或者从水缸舀水。假设我们需要把水平面稳定在A平面,而实际水平面在B平面,那么水平面差值Err=A-B,那这个时候我们需要往里面加水的量就是Kp*Err,Kp就是我们的比例控制系数。

如果A>B,Err为正,就往水缸里面加水;如果A<B,Err为负,就从水缸里面舀水出来。那么只要预期水平面和实际水平面有差值,我们都会通过水桶去加减水来调整系统。同时Kp的大小也有对系统的性能有影响。如果Kp的值比较大,优点是从B平面达到A平面的速度快,缺点是在B平面已经接近A平面的时候系统会产生比较大的震荡。如果Kp的值比较小,优点是B平面在接近A平面的时候系统震荡小,缺点是从B平面达到A平面的速度慢。

这里也许有人会有疑问,如果这里把比例控制系数Kp直接设置成1,然后加水的量直接为Err=A-B不就可以了。然而实际上很多系统是做不到这点的。比如温度控制系统,实际温度为10度,我要通过加热把温度提升到40度,这里难道我们能一次性准确的给系统加30度?显然这是做不到的。那么比例控制的最终结果是Err的值趋向于0。

b, 微分控制理解

然后我们先看看微分控制。在我们的比例控制的作用下,Err是开始减小的(假设一开始预期水平面A大于实际水平面B,也就是说Err是一个正值),那么也就是说Err随时间是一条斜率小于0的曲线,那么在周期时间内,Err越大,微分的绝对值越大,那么也就对Err的减小速度是起到抑制的作用的,直到最后斜率为0微分才会停止作用。

微分控制能反映输入信号的变化趋势,因此在输入信号的量值在变化太大之前可为系统引入一个有效的早期修正信号以增加系统的阻尼程度,从而提高系统的稳定性,但一阶微分的高通特性使得该控制器易于放大高频噪声

c, 积分控制理解

积分控制部分的作用主要是用来消除静差。那么积分是怎样来消除静差的呢?

比例控制只能尽量将Err调节到0,而微分的作用是将曲线的斜率控制到0则停止对其作用,但斜率为0的时候Err并不一定为0。

这个时候我们就需要积分来起作用了。我们知道曲线的积分相当于曲线与x轴围出来的面积。如下图,积分作用的目的是使红色部分的面积和蓝色部分的面积的和为0,那么即使系统在比例控制和微分控制部分已经趋于稳定,只要Err不为0就会存在静差,只要存在静差那么积分就会对系统产生影响,直到系统的Err值为0。那么这样我们的PID控制在理论上就可以达到一个非常精确的控制效果。

d, PID算法离散化

假设采样时间间隔为T,则在k时刻:

偏差为e(k);

积分为e(k)+e(k-1)+e(k-2)+…+e(0);

微分为(e(k)-e(k-1))/T;

从而公式离散化后如下:

比例系数:Kp,

积分系数:KpT/Ti,可以用Ki表示;

* 微分系数:Kp*Td/T,可以用Kd表示;

则公式可以写成如下形式:

PID算法的离散形式就是这样了,这就是我们平时说的位置式PID。

但是为什么还要增推算量式?

一个累加符号使得微机的内存可能不够用,一个字节八位最多存到255,第二点就是掉电之后产生的产生的影响非常大,之前存储的状态会全部丢失,所以要推算对状态记录要求不高的增量式。

接下来我们继续推算增量式PID,根据上面公式我们可以求得:

e,pid双环

f,pid前馈

  1. PID算法库

    1. 核心计算函数(非常成熟的控制器,数学算法)

    1. 初始化代码(将Kp,Ki,Kd三个参数与输出最大值,积分限赋值赋值给PID句柄pid_v_1或者其他的句柄)

    1. 反馈环代码

    1. 闭环代码调用

④C++库(建议)

简介

代码仓库链接:https://github.com/TungChiahuiMCURepos/CAN\_PID\_CPP

类比着C语言的库,

can.c是CubeMX自动生成的CAN通信初始化驱动文件,

bsp_can.cpp是需要自己写的开启CAN通信的代码文件(CubeMX没自动生成的部分,需要手动调用)

can_receive.cpp里是CANRX0接收中断回调函数的实现,该回调函数里用了一些电机信息数据处理函数,然后还有CAN的4个发送函数。

pid.cpp是pid控制系统核心的数学算法代码

pid_user.cpp里是调用pid核心代码并进行封装为PID控制器的初始化代码和一些闭环实现代码。

C++大疆电机库

CLASS的结构与简单介绍

下方图片中是CAN_BUS类,其中嵌套了3个类。

  1. CAN_BUS::BSP类,该类中包含两个方法:

    1. CAN_Start是开启CAN通信的函数;

    2. Filter_Init是CAN通信滤波的函数。

  2. CAN_BUS::DJI_ENCODER类,该类里包含三个方法(该类里的所有函数都由CAN_RX0接收中断回调函数调用):

    1. get_motor_measure是处理CAN通信接收到的大疆电机编码器数据,并处理得到 电机各个信息 函数;

    2. get_moto_offset是处理CAN通信接收到的大疆电机编码器数据,并处理得到 电机刚开始上电的角度初始值 函数;

    3. get_total_angle是处理CAN通信接收到的大疆电机编码器数据,并处理得到 电机角度值 函数。(暂时没被调用)

  3. CAN_BUS::CMD类,该类里包含四个方法:

    1. CAN1_Front是给CAN1 4个电机发送电流的函数;(对应电调ID:1-4)

    2. CAN1_Behind是给CAN1 4个电机发送电流的函数;(对应电调ID:5-8)

    3. CAN2_Front是是给CAN2 4个电机发送电流的函数;(对应电调ID:1-4)

    4. CAN2_Behind是是给CAN2 4个电机发送电流的函数。(对应电调ID:5-8)

CAN_BUS::BSP类的方法(函数) (在bsp_can.cpp中)
CAN_Start 开启CAN通信的函数

Filter_Init CAN通信滤波的函数

CAN_BUS::DJI_ENCODER类的方法(函数) (在can_receive.cpp中)
get_motor_measure 处理CAN通信接收到的大疆电机编码器数据,并处理得到 电机各个信息 函数

get_moto_offset 处理CAN通信接收到的大疆电机编码器数据,并处理得到 电机刚开始上电的角度初始值 函数

get_total_angle 处理CAN通信接收到的大疆电机编码器数据,并处理得到 电机角度值 函数。(暂时没被调用

CAN_BUS::CMD类的方法(函数) (在can_receive.cpp中)
CAN1_Front CAN1 4个电机发送电流的函数

CAN1_BehindCAN1 4个电机发送电流的函数

CAN2_FrontCAN2 4个电机发送电流的函数

CAN2_Behind CAN2 4个电机发送电流的函数

CAN_RX0接收中断回调函数 (在can_receive.cpp中)

C++PID库

CLASS的结构与简单介绍

下方图片中是PID_Controller类,其中嵌套了3个类和一个方法:

  1. PID_Controller类:

    1. All_Device_Init 将所有设备的PID控制器进行初始化
  2. PID_Controller::CORE核心类,该类中包含三个方法:

    1. PID_Init PID核心初始化函数;

    2. PID_Calc PID核心计算函数;

    3. PID_Clear PID清0函数。

  3. PID_Controller::CAN_MOTORcan电机类,该类中包含6个方法(因为上面3个方法和下面3个方法只是CAN通信不一样,所以只讲CAN1):

    1. CAN1_Velocity_Realize CAN1速度环实现函数;

    2. CAN1_Position_Realize CAN1位置环实现函数;

    3. CAN1_VP_Dual_Loop_Realize CAN1速度位置双环实现函数;

    4. CAN2_Velocity_Realize CAN2速度环实现函数;

    5. CAN2_Position_Realize CAN2位置环实现函数;

    6. CAN2_VP_Dual_Loop_Realize CAN2速度位置双环实现函数;

  4. PID_Controller::SENSORS传感器类,该类中包含三个方法:

    1. Yaw_Realize 陀螺仪IMU的航向角PID实现函数;

    2. Pos_X_Realize 码盘定位X坐标实现函数;

    3. Pos_Y_Realize 码盘定位Y坐标实现函数;

PID_Controller类的方法(函数) (在pid_user.cpp中)
All_Device_Init 将所有设备的PID控制器进行初始化
![](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2023/10/09/image229.webp)
PID_Controller::CORE类的方法(函数) (在pid.cpp中)
PID_Init PID核心初始化函数
![](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2023/10/09/image230.webp)
PID_Calc PID核心计算函数
![](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2023/10/09/image231.webp)
PID_Clear PID清0函数
![](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2023/10/09/image232.webp)
PID_Controller::CAN_MOTOR类的方法 (在pid_user.cpp中)(这里只讲CAN1的3个闭环函数)
  1. 注意:

    1. 一般电流值变量定义为一个数组形式,比如fp32 motor_current_target[8];这样就成功定义了8个电机要发送的电流值。

    速度和角度位置同理,fp32 motor_speed_target[8];和fp32 motor_position_target[8];。

    1. C++电机PID库与C语言的电机PID 库有些区别

      1. 因为电调ID的范围是1-8,而数组范围是0-7,

      2. 所以为了和数组序号一样,这个地方注意一下区别:

      3. C语言库中,i的值为电调ID的值。

      4. *C++库中,*i值为电调ID值-1。

CAN1_Velocity_Realize CAN1速度环实现函数

CAN1_Position_Realize CAN1位置环实现函数

CAN1_VP_Dual_Loop_Realize CAN1速度位置双环实现函数

PID_Controller::SENSORS传感器类的方法(函数) (在pid_user.cpp中)
Yaw_Realize 陀螺仪IMU的航向角PID实现函数(等你们完善好 陀螺仪IMU的C++库你们再补充)

Pos_X_Realize 码盘定位X坐标实现函数(等你们完善好 码盘OPS-9的C++库你们再补充)

Pos_Y_Realize 码盘定位Y坐标实现函数(等你们完善好 码盘OPS-9的C++库你们再补充)

如何调用?

我这里选择每隔1ms使用PID控制器进行一次负反馈回路的控制,并发送一次电流值。

可以选择在while(1)死循环中加个delay(1)进行发送;

也可以使用周期为1ms的定时器中断进行实现,更建议使用定时器中断。

⑤实物连接,详细的请看说明书

DMA(Direct Memory Access / 直接存储器访问)

FreeRTOS

理论知识

下方只会讲一些常用的操作和注意事项,更详细的FreeRTOS配置请看:(配合着学习)

大疆开发板C型嵌入式软件教程文档.pdf

STM32F1 FreeRTOS开发手册_V1.1.pdf

STM32F4 FreeRTOS开发手册_V1.1.pdf

常用的内容(下方教程着重讲CubeMX如何配置,理论知识请看正点原子)

系统配置

  1. 选择系统时基源(Timebase Source)

    1. 原因:因为FreeRTOS会占用systick,所以需要改时基源。

    2. 选择规则:优先选择功能少的定时器。(比如说F407ZGT6的tim6和tim7的功能比较少)

    1. 如何选择?(如图)

  1. 选择接口(Interface)

    1. 原因:FreeRTOS遵循ARM的CMSIS标准。

    2. 选择原则:优先选CMSIS v1,因为CMSIS v2还有些小问题没解决。

    3. 如何选择?

  2. 配置Include Parameters

    1. 功能:与hal_conf.h(用来开启HAL库的一些功能)一样,用来开启FreeRTOS的一些功能。

    2. Include Parameters的配置

      1. CubeMX配置(推荐)

      2. 需要什么功能就Enabled对应的功能即可。(常用的就是vTaskDelayUntil)

      1. 手动编辑头文件配置(不推荐)

创建任务

  1. CubeMX创建任务:

    1. 各参数介绍(详看大疆手册):

    1. 一般选择什么参数?

      1. Task Name(任务名):英文大写(与Entry Function对应)

      2. Priority(优先级):一般选择普通优先级即可(除非有特殊的逻辑)

      3. Stack Size(栈空间):128 Words即可

      4. Entry Function(入口函数名):英文小写(与Task Name对应)

      5. Code Generation Option(代码生成选项):无脑选择As weak(使FreeRTOS线程任务的入口函数以弱函数的形式生成)

      6. Parameter(参数):一般NULL即可,如果要用一些特殊功能(比如信号量),要填一些句柄(比如信号量的句柄)

      7. Allocation(份额):无脑选Dynamic,让FreeRTOS动态分配管理即可

  1. 注意事项:

    1. 任务创建太多会内存爆掉

延时

  1. 相对延时

    1. 函数:以下这俩函数作用相同,osDelay()和vTaskDelay()

    2. 时间:是从调用该函数才开始算,直到延时指定时间结束

    3. 调用方法:与HAL_Delay()方法一样

    extern "C" //若在C++中运行需要加上该行
    void green_led_task(void const * argument)
    { 
        for(;;) //等同于while(true) 
        { 
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); 
        osDelay(500); 
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); 
        osDelay(500); 
        }
    }
    
  2. 绝对延时

    1. 函数:

      1. 获取当前时间:osKernelSysTick()

      2. 绝对延时函数:osDelayUntil()

    2. 时间:从任务开始就开始算时间了,将整个任务运行周期看成一个整体,适用于按照一定频率运行的任务

    1. 调用方法:

任务状态转换

  1. FreeRTOS状态(详看大疆手册):

  1. 函数介绍:

  1. 如何调用:

队列

  1. 原因:全局变量在多线程里是不安全的,多个任务对该变量进行操作时,数据容易受损。

  2. 队列:队列是任务到任务、任务到中断、中断到任务数据交流的一种机制(消息传递)

  3. 具体内容:详看正点原子视频学习理论知识

  4. 调用

    1. CubeMX配置:(Queue Size选择你要传的数据的比特数(即二进制位数),Item Size选择数据的类型)

    1. 调用(详解请看正点原子):

信号量(队列的特殊形式)

  1. 原因:同队列

  2. 信号量:一种特殊的队列,是一种解决同步问题的机制,可以实现对共享资源的有序访问。

  3. 分类:二值信号量、计数型信号量(详见正点原子)

  4. 同步问题:A做完一个事情,通知B,B才可以做,这叫同步问题。

  1. 信号量简介(详见正点原子)

  1. 队列与信号量的对比

  1. 二值信号量介绍:

  1. CubeMX配置

要把创建的二值信号量的句柄传入任务的parameter参数里。

其实设置为NULL也可以。

  1. 调用(详细API函数作用请看正点原子)

    1. 释放信号量函数:xSemaphoreGive();

    2. 获取信号量函数:

内存管理

简介

栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。

堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于数据结构中的链表。

(详细请看Vinci机器人队C/C++资料)

修改stm32的栈区和堆区大小
stm32本身的堆区和栈区大小修改

如上图,

stm32一般内存总大小为20Kb。

Heap Size就是堆大小,为512byte = 0.5Kb。

Stack Size就是栈大小,为1024byte = 1Kb。

剩余的其他部分的内存,分配给剩余的区,其中大部分内存都分给了Static静态区。

使用CubeMX生成工程后,可以在启动文件中看到咱们设置的堆区和栈区大小地址。(当然也可以在这里进行修改,不过建议直接在CubeMX上进行修改,一般没啥需求也不用改。)

FreeRTOS的堆区大小修改(此堆区非彼堆区,请看下方介绍)

  1. TOTAL_HEAP_SIZE:如果使用了FreeRTOS,可以在这里修改FreeRTOS的堆区的大小。

  2. memory management scheme:可以修改动态分配内存的算法,一般都使用heap_4算法。

  3. FreeRTOS_HEAP这里的堆区,非彼堆区,而是FreeRTOS从stm32的ZI区中开辟的内存(可以这么理解,其实是FreeRTOS的内核在data,bss,heap,stack等中抢的内存),而并非从stm32的Heap堆区开辟的内存(在当你选择heap_1,2,4,5算法时)。如果你选择的是heap_3算法,那么将会使用C库的malloc()和free()函数进行开辟堆区内存,这个时候,FreeRTOS就是使用的为stm32的堆区(32的堆区比较小,所以不如heap_4算法)。但是咱们一般都使用heap_4算法进行内存管理,所以这里的FreeRTOS_HEAP是从stm32_ZI区分配的。(也就是FreeRTOS_HEAP并非直接从heap区申请,而是非常灵活的在RAM中进行申请,可以超过STM32_HEAP的大小)

  4. 因为我们使用的是heap_4算法,所以我们不用对stm32的heap和stack进行修改,只需要对FreeRTOS_HEAP进行修改即可。(也就是对FreeRTOS可操控的stm32的ZI区内存进行分配)

内存管理API介绍
C语言库的内存管理API(不建议)

正点原子分块式内存管理API

memx就是指内存块,内部的SRAM和外部的SRAM(外部的不一定有外部的SRAM)。

FreeRTOS内存管理API(建议)
  1. 介绍

  1. FreeRTOS内存管理算法(我们一般选择heap_4)

heap_4的first-fit算法是从堆区内存起始地址块开始找出第一个适合的内存大小。

  1. FreeRTOS内存管理API函数

可以通过看上面代码,就可以得知,申请完内存再释放掉内存后,空闲内存数会还原。

但是,可以看到,此时我们分配的是一个4字节的内存,但是他扣掉了16字节的内存,这是因为字节对齐的原因,FreeRTOS选择使用用空间换速度的方式进行字节对齐。

FPU浮点数计算加速

STM32由于主频比较低,所以运算浮点数运算会非常慢,目前有下列几种办法可以优化sin,cos这种大型浮点数运算。

检查是否支持

STM32 系列 CPU 内核 DSP 指令 FPU 类型 arm_cos_f32() 性能 适合的计算 建议使用的函数
STM32H7 Cortex-M7 :white_check_mark: 支持 :white_check_mark: 双精度 FPU (DP-FPU) :rocket: 最快(硬件加速) 高精度计算、机器人、滤波、导航 arm_cos_f32()
STM32H5 Cortex-M33 :white_check_mark: 支持 :white_check_mark: 双精度 FPU (DP-FPU) :rocket: 最快(硬件加速) 高精度计算、滤波、AI 计算 arm_cos_f32()
STM32F7 Cortex-M7 :white_check_mark: 支持 :white_check_mark: 单精度 FPU (SP-FPU) :fire: 很快(硬件加速) 机器人控制、导航、滤波 arm_cos_f32()
STM32F4 Cortex-M4 :white_check_mark: 支持 :white_check_mark: 单精度 FPU (SP-FPU) :fire: 很快(硬件加速) 机器人控制、数学运算 arm_cos_f32()
STM32G4 Cortex-M4 :white_check_mark: 支持 :white_check_mark: 单精度 FPU (SP-FPU) :fire: 很快(硬件加速) 电机控制、滤波 arm_cos_f32()
STM32L4 Cortex-M4 :white_check_mark: 支持 :white_check_mark: 单精度 FPU (SP-FPU) :fire: 很快(硬件加速) 低功耗计算 arm_cos_f32()
STM32U5 Cortex-M33 :white_check_mark: 支持 :white_check_mark: 单精度 FPU (SP-FPU) :fire: 很快(硬件加速) 低功耗 AI 计算 arm_cos_f32()
STM32F3 Cortex-M4 :white_check_mark: 支持 :x: 无 FPU :warning: 较慢(无 FPU,仅 DSP 加速) 电机控制、信号处理 arm_cos_q31()
STM32G0 Cortex-M0+/M4 :x: 部分支持 :x: 无 FPU(部分 M4 版有 SP-FPU) :warning: 较慢(软件计算) 基础控制 arm_cos_q31()
STM32F1 Cortex-M3 :x: 不支持 :x: 无 FPU :no_entry_sign: 最慢(纯软件计算) 不推荐做浮点计算 arm_cos_q31()
STM32F0 Cortex-M0 :x: 不支持 :x: 无 FPU :no_entry_sign: 最慢(纯软件计算) 不推荐做浮点计算 arm_cos_q31()
STM32L0 Cortex-M0+ :x: 不支持 :x: 无 FPU :no_entry_sign: 最慢(纯软件计算) 超低功耗应用 arm_cos_q31()

开启FPU

浮点运算单元(FPU)是一种用于执行浮点运算的结构,通常由电路实现,应用于计算机芯片中。ARM设计的M4内核及更高级的内核都支持FPU,也就是STM32F4系列及往上。(也就是STM32F1是不支持的)

STM32F4/F7一般有单精度FPU,而STM32H5/H7,一般有双精度FPU。

STM32F4开启FPU和不开启FPU往往会有数十倍甚至上百倍的差距。

使用STM32CubeMX生成工程,会默认开启FPU,如下图。

如果你使用F1的话,会压根都没有这个选项,代表M3内核不支持FPU。

下面这张图可以从源码看到开启了FPU。

DSP加速

DSP加速是指CMSIS-DSP库进行三角函数算法优化,使计算速度加快,但是误差会变大一些,不过对于99%的应用场景误差够用了,大概是1e-6单位的误差。

DSP库只适用于ARM的Cortex-A和Cortex-M的内核,也就是适应手机,ARM单片机,树莓派等等的设备。

对于STM32单片机来说,基本覆盖了所有STM32系列,所以都可以用。

假设你没有FPU,比如STM32F1系列的单片机,也可以通过DSP库来加速三角函数运算,这个DSP库的是通过查表+插值的数学运算方式进行优化的,计算也是比较快。

平台/库函数 CMSIS-DSP C++ std::cos C math.h
arm_cos_f32 std::__math::cos cosf() / cos()
Cortex-M4/M7(带FPU) :white_check_mark:最快(查表+插值) :white_check_mark:比较快(完整计算) :white_check_mark:比较快(和 std::cos 相近)
Cortex-M0/M3(无FPU) :warning:比较慢(查表+插值) :no_entry_sign:最慢(软件浮点) :no_entry_sign:最慢(和 std::cos 相近)
Cortex-A(如 Raspberry Pi) :white_check_mark:可能更快(查表方法) :white_check_mark:更快(用 SIMD/FPU) :white_check_mark:更快(glibc/libm,SIMD 优化)
x86/x86-64(PC 端) :x:不可用 :white_check_mark:最快(硬件加速) :white_check_mark:最快(使用 FPU 或 SIMD)

所以说在STM32上跑还是建议用dsp库的函数。

安装并使能DSP库:

  1. 方法一(推荐):使用CubeMX打开

然后使能DSP库

生成工程后,可以通过MDK5或者MDK6看到我们生成的lib。

  1. 方法二(不推荐):使用MDK5打开

这种方式会使编译时间增加至少200%.

函数介绍

ARM内核的CPU支持 CMSIS-DSP 库的三角函数,这比标准 math.hcmath 的函数更快。

  1. 普通的C/C++三角函数库:

下面是普通的重载三角函数,当我们开启了FPU后,只要传入的是fp32的类型,其实速度也是相当快的,可以不使用DSP库也可以。

#include <cmath>
// 更新机器人的位置(假设机器人沿着x轴移动)
this->x_position += this->vx * std::__math::cos(this->yaw) * this->dt;  
this->y_position += this->vy * std::__math::sin(this->yaw) * this->dt;
this->y_position = - y_position;
this->yaw += this->vw * this->dt;
  1. DSP库函数:

传入fp32的值。

    // 更新机器人的位置(假设机器人沿着x轴移动)
    this->x_position += this->vx * arm_cos_f32(this->yaw) * this->dt;  
    this->y_position += this->vy * arm_sin_f32(this->yaw) * this->dt;
    this->y_position = - y_position;
    this->yaw += this->vw * this->dt;

性能对比

:white_check_mark: 对于有FPU的单片机
函数
重载函数std::__math::cos(x)
float
cosf(x)
arm_cos_f32(x)
arm_cos_q31(x)
查表法(LUT)

除了arm_cos_f32,还有其他的一些arm_cos_q31函数,可能更加适配于F103这种低端芯片,可以进行自由选择。

:x: 对于无FPU的单片机
函数
arm_cos_f32(x)
arm_cos_q31(x)
重载函数std::__math::cos(x)
float
cosf(x)
查表法(LUT)

DMA+多通道adc(遥控器遥杆)

cubemx配置:

多通道adc大部分要开启扫描模式;

adc连续模式开启或者关闭,影响mian函数的相关代码,不开continuous则需在while中不断对adc进行开启

开连续模式(延时500可以去掉)

不开:

相比之下开连续更快,更建议连续

STM32常见问题

STM32 使用ST-link下载问题

  1. 原因:在使用CubeMX 配置文件时,忘记设置SYS选项里面的Debug选项

  1. 现象:下载完一次程序之后程序无法运行,且无法重新下载。

  2. stm32共有三种启动模式:

    1. 用户闪存:正常的工作模式。stm32内置的Flash,一般我们使用JTAG或者SWD模式下载程序时就是下载到这个里面,重启之后也是从这里启动程序。

    2. SRAM:芯片内置的RAM区,就是内存,没有程序储存的能力,这个模式一般用于调试。

    3. 系统储存器:系统储存器是芯片内部的一块特定的区域

    4. stm32厂商在这个区域内部设置了一段Bootloader。选用这种启动模式,是为了能够从串口下载程序,因为在商家提供的Bootloader中,提供了串口下载的固件,可以通过这个Bootloader将程序下载到系统的Flash中。

  3. 解决方法:

    1. 将BOOT0设置为1;BOOT1设置为0

    1. 连接电脑后按下复位键,使用keil5下载没有问题的正常程序,发现程序正常下载。

    2. 将BOOT引脚改为原来的状态,再次尝试下载程序发现一切正常。