Skip to content

Wiki

STM32 microcontroller (key focus)

About 145 min read

Existing English translations were reused. 23 current source blocks remain in Simplified Chinese, without triggering paid translation.

1.Introduction to Microcontrollers

1.1.① What is a microcontroller?

A microcontroller is also called asingle-chip microcontroller. It is not achipthat performs a single logic function; rather, it integrates a complete computer system onto one chip. It is equivalent to a microcomputer, and compared with a computer, a microcontroller lacks only the I/O. In summary: one chip becomes a computer. It is small in size, light in weight, and low in cost, providing convenient conditions for learning, application, and development. At the same time, learning to use a microcontroller is also the best choice for understandingcomputer principlesand architecture.

1.2.② Applications of microcontrollers?

  1. Internet of Things (※)

  2. Medical equipment

  3. Industrial control

  4. Computer network communication (※)

  5. ... ...

1.3.③ Components of the STM32 microcontroller

  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)
        

        11. LoongArch架构

        1.  应用:政府单位采购、军工采购、个人电脑等
        
        2.  分类:龙架构32位、龙架构64位
        

        12. 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口)

        2.      ④原理图

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

        1. 组成部分:

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

          2. 导线

          3. 网络标号

          4. 电源符号

          5. 等... ...

2.1.⑤ Chip manual

  1. Function: Query various chip information (such as CPU frequency, IO definitions, clock tree, etc.)

3.Software Introduction

  1. STM32 libraries

    1. Various development approaches: registers, standard peripheral library, HAL library, LL library

      1. Brief understanding of register functions: A register is a storage structure closer to the CPU core, so exchanging data with the CPU core is faster than memory (RAM). Each register has different functions; storing different values in a register and having the CPU read them will implement correspondingly different functions.

      2. Library: A library is source files + header files. STM32 libraries are compiled from a mix of assembly language and C language (HAL library and LL library are compatible with C++). Existing libraries include the standard peripheral library, HAL library, and LL library (the standard peripheral library has been eliminated; our laboratory uses the HAL library and LL library).

      3. Advantages and disadvantages of various development approaches:

        1. Registers: This development approach has high hardware execution efficiency, but because STM32 has too many registers, writing with registers has poor readability and is troublesome and cumbersome, so it is not recommended to write entirely with registers; it can occasionally be used in certain situations. (For example, in a running light, shift operations can be performed directly on registers; when adjusting PWM duty cycle, the CCR register can be assigned directly, etc.)

        2. Standard peripheral library: Too old and now eliminated. This library was developed using assembly + C language, and the code readability is very high. However, due to some problems in the initial design of the standard peripheral library and some patent issues, it can cause problems (such as IIC communication), and clock configuration is too troublesome and cumbersome, so starting with the class of 2021, we no longer use the standard peripheral library.

        3. HAL library, LL library (strongly recommended): Libraries strongly promoted by ARM and STMicroelectronics, compliant with the ARM CMSIS standard, which is a standard that embedded developers today all need to follow. This library is developed using assembly language + C language, and is compatible with C++ (the header files contain extern "C" conditional compilation). Using C++ OOP (object-oriented programming) for development is ten thousand times more convenient. The HAL library and LL library are still maintained by ST, and they have solved various certainties of the standard peripheral library, such as hardware IIC being unusable and clock configuration being extremely easy. Introduction to the ARM CMSIS standard:https://www.arm.com/technologies/cmsis

  1. 开发软件介绍:

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

    2. ARM Keil MDK

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

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

      3. 版本选择:

        1. MDK 5.3及以上:建议使用,但需要自己装ARMCC编译器。其只能在Windows平台进行开发,且图形界面过于丑陋,且没有黑暗模式,夜晚开发及其辣眼,但由于其使用ARMCC和ARMCLANG编译器,比ARM-GCC编译器生成的量要小很多,且因为其对ARM Cortex内核兼容性极好,所以仍选择用MDK 5.3版本。

        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

4.Clock tree

4.1.①Steps for configuring the clock using CubeMX

  1. Clock configuration introduction: This is something that every project needs to do, to give the CPU a normal heartbeat.

  2. Purpose: To give the CPU a normal heartbeat and to give each peripheral its heartbeat, so that the CPU and its peripherals can operate normally. (For example, the accuracy of delay functions and the accuracy of timer PWM waveforms.)

  3. STM32F1 series CPU clock block diagram:

  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. 零错误零警告即配置成功,有错误有警告请自行百度、谷歌

4.2.② Method for querying the clock frequency of a peripheral (using a timer as an example)

  1. Open tim.c

  1. Find the MSP initialization weak function (determine which TIMx it is from TIM's base handle)

  1. Find the definition of __HAL_RCC_XXX_CLK_ENABLE()

  1. According to the function definition, it can be seen that TIM1 is mounted on APB2.

  1. Query the clock tree and find APB2 Timer Clock to obtain that the TCLK of TIM1 is 168MHz

  1. Therefore, it is known that the TCLK frequency of TIM1 is 168MHz

5.STM32 program composition

5.1.Basic introduction (main functions, etc.)

  1. Project composition: an STM32 project is a project composed of C language and assembly language libraries, so it has a main function and conforms to the structure of the C/C++ language.

  2. Program execution order: apart from preprocessing and similar steps, the program starts running from the main function, and it conforms very closely to the C/C++ execution order: starting from the main function, the code runs line by line, and then enters an infinite loop.

  3. Required components within the main function: an infinite loopwhile(true)或者for(;;)while(true)或者for(;;)because the microcontroller must keep running, so there is an infinite loop.

  4. HAL library and user-defined libraries

    1. Libraries:

      1. .h files declare functions

      2. .c/.cpp files define functions

      3. .c/.cpp files call functions

5.2.Introduction to Interrupt Service Routines

  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中更改)

    2. 中断服务函数内容:

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

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

      3. 接收数据等(可选)

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

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

        1. 中断服务函数不能传入参数;

        2. 中断服务函数不能有返回值;

        3. 中断服务函数应该做到短小精悍;

        4. 迫不得已的情况下,不准在中断服务函数中使用延时函数,如果要使用延时,请设置好延时和中断的优先级,否则程序出卡死(除了外部中断为了软件消抖而设立的延时)

        5. 不要在中断服务函数中使用printf函数,会带来重入和性能问题。

      6. 举例:

        1. USART1_IRQHnadler函数

          1. 中文名:串口1_中断服务函数

          2. 声明定义:由汇编声明,需要用户自己去定义(如果使能了,用户还不定义,程序将会卡在汇编代码中)

          3. 调用条件:由CPU中断事件调用

          4. 作用:被CPU调用,并调用紧急的中断程序(中断程序也就是中断服务函数里的内容)

        2. HAL_UART_IRQHnadler(句柄)

          1. 中文名:串口_中断公共服务函数(公共的意思指串口1,2,3,4,5......等所有的串口都共用这一个函数实现功能,由后面的句柄决定究竟是哪个函数被触发)

          2. 声明定义:ST公司编写的HAL库声明和定义

          3. 调用条件:由中断服务函数调用

          4. 作用:

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

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

            3. 接收数据等(可选)

            4. 调用中断事件对应的中断回调函数

            5. 其他操作(比如特殊的,在串口接收中断里会disable失能中断,也就是关掉中断)

        3. HAL_UART_RxCpltCallback(句柄)

          1. 中文名:串口_中断回调函数(因为他被中断公共服务函数调用,所以句柄是由调用它的中断公共服务函数所决定)

          2. 声明定义:ST公司编写的HAL库声明为弱函数,需要用户自己去定义

          3. 调用条件:被中断公共服务函数调用

          4. 作用:先确定是哪个句柄调用的,再进行相应的业务逻辑实现(可选,比如数据处理等)

5.3.A Brief Introduction to RTOS and ROS/ROS2

  1. Advanced (non-bare-metal development, based on RTOS system development)

    1. Common RTOS (embedded real-time operating systems): FreeRTOS, Nuttx, RT-Thread, μC/OS-II, Xiaomi VelaOS

    2. FreeRTOS official website:https://www.freertos.org/zh-cn-cmn-s/

    1. Simple understanding of FreeRTOS: an operating system with multithreading library features and POSIX standard compatibility

    2. Multithreading: The system has multiple tasks (threads), each task (thread) runs independently and simultaneously (it can be understood as each task being a main function, and these tasks all execute at the same time. The specific implementation will be learned later; the principle is PendSV interrupts, etc.)

  2. Advanced (non-bare-metal development, based on RTOS and ROS2_MicroROS)

    1. Usage: ESP32 uses the Arduino library + FreeRTOS+MicroROS and communicates with STM32 through the serial port.

    2. One of the main functions: It can communicate more safely and stably with the ROS2 of the host computer (PC, industrial computer) remotely via WIFI. Compared with direct serial communication (rosserial), it is much better (DDS distributed).

    3. Detailed explanation link comparing MicroROS vs ROSserial:https://mp.weixin.qq.com/s/1lQXAA3sV-4GpXAzHiGChQ

6.register

  1. Understanding: These are small storage areas inside the CPU used to store data, employed to temporarily hold data involved in operations and the results of operations.

  1. Implemented functionality:

  1. How do registers function in a C-language-based HAL library? (Or rather, what is the principle by which a C-language HAL library implements control of an STM32 microcontroller?)

7.Vinci Robotics Team Standard Engineering Format

7.1.English

Must use English . Engineering file names, function names, and variable names must use English! (Step out of the Chinese comfort zone; at the very least, you need to recognize some technical English.)

7.2.ALIENTEK HAL library project standard:

7.3.Vinci Robot Team STM32 Project Standard (Cube+C language):

  1. applications application layer

  1. BSP driver layer

  1. Middlewares

  1. Core (where the main function is located, and where the header files for conditional compilation configuration of the HAL library are located)

7.4.Team Vinci Robotics STM32C/C++ Engineering Standard (similar to ALIENTEK, trial operation,not recommended,建议用下一节的类Cube_Cpp):

  1. The application layer, driver layer, etc. adopt a modular integrated approach, no longer using the Src and Inc separation approach

  1. Public compatibility layer:

  2. C++ sub-main compatibility library

  1. Function: Create a regular function in the .cpp file that calls the code of C++, and then is called by the main function in the C language main.c file.

  1. 弱函数_回调函数库:在 .cpp 中实现 HAL 库中以 C 链接方式提供的回调函数时,应保证实现也使用 C 链接方式,可通过包含带有 extern "C" 声明的头文件,或在函数定义前添加 extern "C" 来实现。这样可以避免 C++ 名称修饰导致符号不匹配,使用户实现能够覆盖原来的弱函数。弱函数并不是 C 语言独有的机制,也不是 C++ 无法识别。详细原因请看,C 和 C++ 混合时的弱函数这一节。

7.5.(Recommended) Vinci Robot Team STM32Cube C/C++ Engineering Standard (Cube-like, trial operation, recommended):

7.5.1.First, open CubeMX for project configuration. For example, here we use bare-metal development to make an LED blink.

Then select OpenFolder to open the folder

7.5.2.Open Github to clone some essential files

Repository link:

https://github.com/tungchiahui/CubeMX_MDK5to6_Template

Or directly open the terminal and enter

git clone https://github.com/tungchiahui/CubeMX_MDK5to6_Template.git
7.5.3.Open the cloned template and the project just generated by CubeMX

Open the *** in the template工程文件移植(创建新模板请看这里)*** folder, then copy all the files inside to the CubeMX project folder.

After moving:

7.5.4.Open Project Settings Project
  1. Open the MDK5 project

  1. Click Options for Target

  1. Change the compiler toARMClang[ARM Compiler6 (AC6)] replace ARMCC[ARM Compiler5 (AC5)]

  1. Add the header file path (Include Path)

Add the Inc folder in applications

Add the Inc folder in bsp/boards

Click OK to proceed

  1. Add source files .c/.cpp, etc.

Open Manage Project Items

Create two groups

The names of the groups are respectively called

applications

bsp/boards

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

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

You can see that all the files in the project are ready.

7.5.5.Compile and configure some necessary code

You can right-click the header file and then click Open Document "xxx.h" to open the header file, to check whether the header file was imported successfully.

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

Between the two comment lines USER CODE BEGIN Includes and USER CODE END Includes 引用startup_main.h (because code not placed between BEGIN and END will be lost after CubeMX reconfiguration)

Between the two comment lines USER CODE BEGIN and USER CODE END 调用startup_main();(because code not placed between BEGIN and END will disappear after CubeMX reconfiguration)

Open startup_main.h

Change the value of the isRTOS macro: if developing on bare metal, set it to 0; if FreeRTOS is used, change it to 1.

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

7.5.6.C++Library header file format

Take bsp_delay.h as an example

#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

Conditional compilation is definitely indispensable: one is conditional compilation to prevent repeated inclusion of header files, and the other is conditional compilation to link C++ as C language. (If you have forgotten, seeVinci Robot Team C/C++ materials).

Then include startup_main.h header file

Then create the class for this module, for example class BSP_LED, etc. Here it is a delay class, so it is class BSP_Delay.

Then write the declaration inside the class.

Note: Do not write any code implementation in the .h file, that is, you cannot write any function definition.

Then, at line 35, extern BSP_Delay bsp_delay; an object bsp_delay is created in the main function, and we need to declare (extern) the object (variable) here so that other source files can call it.

Theoretically, you should be able to understand what is said above. If you really cannot understand it, just imitate it; as you imitate, you will gradually understand.

7.5.7.C++ library source file format

Take bsp_delay.cpp as an example

#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
}

Of course, at the very beginning, you have to include your own header file.

You can ignore this conditional compilation, because there is a difference in delays between bare-metal development and RTOS development, so I added a line of conditional compilation.

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

Then define all the functions in the class.

Function comment format:

This part is the comment for the function; from now on, try to write comments in this way whenever possible. (When calling the function in MDK6 in the future, this comment will be displayed, making it clear at a glance.)

The advantage of writing comments this way is that, when called, it will display what entry parameters need to be filled in and what the return value is.

brief function summary

param entry parameter

retval return value

note or attention: Notes

Note that here, however many param entry parameters there are, write that many params

for example

/**
 * @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)
 {
 // ... ...
 }
7.5.8.Precautions
  1. 在 .cpp 源文件中覆盖原本以 C 链接方式提供的弱函数时,应保证新的函数定义也使用 C 链接方式:如果已包含带有该函数 extern "C" 声明的头文件,后续定义可以沿用该链接方式;否则需要在函数定义前添加 extern "C"。原因是要避免 C++ 名称修饰,使强定义与原弱定义对应同一个链接符号。__weak 并非 C 语言特有,是否需要 extern "C" 取决于原函数的语言链接方式,与弱函数机制本身没有直接关系。详细原因请看,C 和 C++ 混合时的弱函数这一节。

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

8.Driver

  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.  查数据手册查函数的内容的注释
    

9.DJI motor control (CAN)

###   ①CAN通信简介

3. 什么是CAN通信?

CAN busThe communication system isa type of serial communicationand is superior to the serial portRS485bus. Unlike synchronous communication methods such as I2C and SPI that have a clock signal, CAN communication is not synchronized by a clock signal; it is a type ofasynchronous **** half-duplexcommunication. (Differential signal, half-duplex)

  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)

9.1.② Analysis of the motor library code (the contents of this library are required to be understood as thoroughly as possible, ideally line by line without omission)

  1. Code and its initialization

    1. This part is covered by ALIENTEK; you only need to change the parameters to those for DJI motors.

    Code repository link:https://github.com/SDUTEMIS/SDUT_VinciRobot/tree/main/1.Embedded_STM32_Driver%2FC%2F4.Motor_Drivers%2F1.DJI_CAN_PID

  1. DJI Motor Library Open-Loop Code Analysis: The library is a modification of the official DJI library code by previous senior students.

    1. CAN Message Sending Function Analysis

       This function sends the current value to DJI motors communicating via CAN1. CAN1 communication can only send 8 bits of data at a time, while the current value is 16 bits of data, so the current value is shifted right by 8 bits and then sent to the motor. The motor starts rotating upon receiving the current value (the input parameter is the current value for motors with ESC IDs 1-4).

       This function sends the current value to DJI motors communicating via CAN1. CAN1 communication can only send 8 bits of data at a time, while the current value is 16 bits of data, so the current value is shifted right by 8 bits and then sent to the motor. The motor starts rotating upon receiving the current value (the input parameter is the current value for motors with ESC IDs 5-8).

    1. CAN Message Sending Function Call
    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. DJI Motor Library Closed-Loop Code Analysis: The library is a modification of the official DJI library code by previous senior students. (On top of the open-loop version, CAN message reception and a series of data parsing programs have been added.)

    1. CAN Communication Receive Interrupt Callback Function (CAN_RX0 The receive interrupt callback function is used to process the data sent by the CAN communication motor, that is, the motor's speed, angle, temperature and other data.)

    1. Data Parsing Function

      1. Motor encoders are divided into incremental encoders and absolute value encoders.

        1. Incremental encoder: data is lost upon power-up, and the angle starts from 0.

        2. Absolute value encoder: data is not lost after power-down.

        3. The encoders of the M3508 and M2006 are both absolute value encoders, and data is not lost after power-down. However, because what is recorded is the rotor's angle, and the rotor is connected to a reducer, the data is erroneous. Therefore, we need to use code to convert the absolute value encoder data into incremental encoder data for use. (The total in the struct is the absolute encoder's angle, and total_angle is the total angle we processed and changed to the incremental encoder's total angle.)

      2. Record the power-up angle

      1. Calculate the total angle (after code processing, at power-up the total angle = number of turns (0) \*8192 + the current absolute value encoder angle (assumed to be A) - the power-up angle captured at power-up (because this is at power-up, it is also A) = 0).

      1. Function not needed for now (this function is not called)

9.2.③ PID controller

  1. Introduction to the PID Algorithm:

    1. MATLAB_PID Controller Introduction:https://www.mathworks.com/help/control/pid-controller-design.html?s_tid=CRUX_lftnav
  2. PID Algorithm Principles

One of the most typical examples for understanding the PID control algorithm is the problem of a leaking water tank.

There is a leaking water tank, and the rate of leakage is not constant. In addition, we have a bucket, and we can control adding water to the tank or scooping water out of the tank. Also, we can detect the water level. Now our goal is to control the water level to be stable at any level we want.

Note that using PID requires us to be in a closed-loop system. What is a closed-loop system? It means there is an input and feedback. The input is the ability to input a quantity to affect and control our system, and the feedback is that we must be able to know the state of what we are ultimately controlling. In this leaking water tank system, the input is the bucket: we can add water to or scoop water out of the tank through the bucket to affect the water level in our tank. As for feedback, that means we must be able to measure the water level and know what the water level is.

a, Understanding proportional control

First is proportional control. Proportional control is like adding water to a tank through a bucket or scooping water out of the tank. Suppose we need to stabilize the water level at plane A, but the actual water level is at plane B; then the water level difference Err=A-B. At this time, the amount of water we need to add is Kp\*Err, and Kp is our proportional control coefficient.

If A>B, Err is positive, and water is added to the tank; if A<B, Err is negative, and water is scooped out of the tank. Therefore, as long as there is a difference between the expected water level and the actual water level, we will adjust the system by adding or removing water through the bucket. At the same time, the value of Kp also affects the system's performance. If the value of Kp is relatively large, the advantage is that the speed from plane B to plane A is fast, but the disadvantage is that when plane B is already close to plane A, the system will produce relatively large oscillations. If the value of Kp is relatively small, the advantage is that the system oscillates little when plane B is close to plane A, but the disadvantage is that the speed from plane B to plane A is slow.

Here, some may wonder: if the proportional control coefficient Kp is directly set to 1, and the amount of water added is directly Err=A-B, would that not suffice? However, in practice, many systems cannot achieve this. For example, in a temperature control system, if the actual temperature is 10 degrees and I want to raise it to 40 degrees by heating, can we accurately add 30 degrees to the system all at once? Obviously, this is impossible. Therefore, the final result of proportional control is that the value of Err tends toward 0.

b, Differential control understanding

Then let's first look at derivative control. Under the action of our proportional control, Err begins to decrease (assuming that initially the expected water level A is greater than the actual water level B, meaning that Err is a positive value); that is to say, Err is a curve with a slope less than 0 over time. Then within the cycle time, the larger Err is, the larger the absolute value of the derivative, and thus it acts to suppress the rate of decrease of Err, and only when the slope finally becomes 0 will the derivative action stop.

Differential control can reflect the trend of change in the input signal; therefore, before the magnitude of the input signal changes too much, it can introduce an effective early correction signal to the system to increase the degree of damping of the system, thereby improving the stability of the system. However, the high-pass characteristic of first-order differentiation makes this controller prone to amplifying high-frequency noise.

c, Integral control understanding

The role of the integral control part is mainly to eliminate steady-state error. So how does the integral eliminate the steady-state error?

Proportional control can only adjust Err toward 0 as much as possible, while the role of derivative control is to stop acting on it once the slope of the curve is controlled to 0; however, when the slope is 0, Err is not necessarily 0.

At this point, we need the integral to come into play. We know that the integral of a curve is equivalent to the area enclosed by the curve and the x-axis. As shown in the figure below, the purpose of the integral action is to make the sum of the area of the red part and the area of the blue part equal to 0. Thus, even if the system has already become stable in the proportional control and derivative control parts, as long as Err is not 0, a steady-state error will exist. As long as a steady-state error exists, the integral will affect the system until the Err value of the system becomes 0. In this way, our PID control can theoretically achieve a very precise control effect.

d, Discretization of the PID algorithm

Assume the sampling time interval is T, then at time k:

the deviation is e(k);

The integral is e(k)+e(k-1)+e(k-2)+…+e(0);

The differential is (e(k)-e(k-1))/T;

Thus, after discretizing the formula, it is as follows:

Proportional coefficient: Kp,

Integral coefficient: KpT/Ti, can be represented by Ki;

* Differential coefficient: Kp*Td/T, which can be denoted by Kd;

Then the formula can be written in the following form:

This is the discrete form of the PID algorithm, which is what we usually call the positional PID.

But why is there still a push inference quantity formula?

An accumulation symbol may cause the microcomputer's memory to be insufficient; one byte with eight bits can store at most up to 255. The second point is that the impact after power loss is very significant—all previously stored states will be completely lost. Therefore, an incremental type that has low requirements for state recording needs to be derived.

Next, we continue to derive the incremental PID. From the above formula, we can obtain:

e, PID dual-loop

f, pid feedforward

  1. PID algorithm library

    1. Core computation function (a very mature controller, mathematical algorithm)

    1. Initialization code (assign the three parameters Kp, Ki, Kd, the output maximum value, and the integral limit to the PID handle pid_v_1 or other handles)

    1. Feedback loop code

    1. Closed-loop code call

9.3.④C++lib (recommended)

9.3.1.Introduction

Code repository link:https://github.com/TungChiahuiMCURepos/CAN_PID_CPP

By analogy with C language libraries,

can.c is the CAN communication initialization driver file automatically generated by CubeMX,

bsp_can.cpp is the code file that you need to write yourself to enable CAN communication (the part not automatically generated by CubeMX, which needs to be called manually).

can_receive.cpp contains the implementation of the CANRX0 receive interrupt callback function. This callback function uses some motor information data processing functions, and also includes the 4 CAN transmit functions.

pid.cpp is the mathematical algorithm code at the core of the PID control system

pid_user.cpp contains the PID core code invocation and encapsulated initialization code for the PID controller, as well as some closed-loop implementation code.

9.3.2.C++ DJI Motor Library
9.3.2.1.Structure and Brief Introduction of CLASS

The image below shows the CAN_BUS class, which contains 3 nested classes.

  1. CAN_BUS::BSP class, which contains two methods:

    1. CAN_Start is the function for enabling CAN communication;

    2. Filter_Init is the function for CAN communication filtering.

  2. CAN_BUS::DJI_ENCODER class, which contains three methods (all functions in this class are called by the CAN_RX0 receive interrupt callback function):

    1. get_motor_measure is the function for processing the DJI motor encoder data received via CAN communication and deriving various motor information;

    2. get_moto_offset is the function for processing the DJI motor encoder data received via CAN communication and deriving the initial motor angle value at power-on;

    3. get_total_angle is the function for processing the DJI motor encoder data received via CAN communication and deriving the motor angle value. (Not called for now)

  3. CAN_BUS::CMD class, which contains four methods:

    1. CAN1_Front is the function for sending current to the front 4 motors on CAN1; (corresponding ESC IDs: 1-4)

    2. CAN1_Behind is the function for sending current to the rear 4 motors on CAN1; (corresponding ESC IDs: 5-8)

    3. CAN2_Front is the function for sending current to the front 4 motors on CAN2; (corresponding ESC IDs: 1-4)

    4. CAN2_Behind is the function for sending current to the rear 4 motors on CAN2. (corresponding ESC IDs: 5-8)

9.3.2.2.CAN_BUS::BSP类的方法(函数) (在bsp_can.cpp中)

####### CAN_Start Function to enable CAN communication

####### Filter_Init CAN communication filtering function

9.3.2.3.CAN_BUS::DJI_ENCODER methods (functions) of a class (in can_receive.cpp)

####### get_motor_measure Process the DJI motor encoder data received via CAN communication and process it to obtain the various motor information function

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

####### get_total_angle Process the DJI motor encoder data received via CAN communication, and process to obtain the motor angle value function. (Not currently called)

9.3.2.4.CAN_BUS::Methods (functions) of the CMD class (in can_receive.cpp)

####### CAN1_Front CAN1 front Function for sending current to the 4 motors

####### CAN1_BehindCAN1 After Function to send current to the 4 motors

####### CAN2_FrontCAN2 Front function for sending current to 4 motors

####### CAN2_Behind CAN2 After function for sending current to 4 motors

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

9.3.3.C++PID library
9.3.3.1.Structure and Brief Introduction of CLASS

The following image shows the PID_Controller class, which contains three nested classes and one method:

  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坐标实现函数;

9.3.3.2.PID_Controller类的方法(函数) (在pid_user.cpp中)

####### All_Device_Init Initialize the PID controllers of all devices

9.3.3.3.PID_Controller::Methods (functions) of the CORE class (in pid.cpp)

####### PID_Init PID core initialization function

####### PID_Calc PID core calculation function

####### PID_Clear PID zeroing function

9.3.3.4.PID_Controller::CAN_MOTOR class methods (in pid_user.cpp) (only the 3 closed-loop functions of CAN1 are discussed here)
  1. Note:

    1. Generally, the current value variables are defined as an array, for example, fp32 motor_current_target88; this successfully defines the current values to be sent to the 8 motors.

      The same applies to speed and angular position, fp32 motor_speed_target88; and fp32 motor_position_target88;.

    1. The C++ motor PID library differs somewhat from the C language motor PID library

      1. because the ESC ID range is 1-8, while the array range is 0-7,

      2. so in order to match the array index, note the difference here:

      3. In the C language library, the value of i is the ESC ID value.

      4. *In the C++ library,*the value of i is the ESC ID value - 1.

####### CAN1_Velocity_Realize CAN1 speed loop implementation function

####### CAN1_Position_Realize CAN1 position loop implementation function

####### CAN1_VP_Dual_Loop_Realize CAN1 speed-position dual-loop implementation function

9.3.3.5.PID_Controller::Methods (functions) of the SENSORS sensor class (in pid_user.cpp)

####### Yaw_Realize Heading angle PID implementation function for the gyroscope IMU (you can supplement it after you finish improving the gyroscope IMU C++ library)

####### Pos_X_Realize Encoder positioning X-coordinate implementation function (once you have perfected the encoder OPS-9 C++ library, you can then supplement it)

####### Pos_Y_Realize Encoder positioning Y-coordinate implementation function (you can supplement this after you have perfected the OPS-9 encoder C++ library)

9.3.4.How to call it?

Here, I choose to use the PID controller to perform negative feedback loop control every 1 ms and send a current value each time.

You can choose to add a delay(1) within the while(1) infinite loop for sending;

It can also be implemented using a timer interrupt with a period of 1 ms; using a timer interrupt is more recommended.

9.4.⑤ Physical connection; for details, please refer to the manual.

10.DMA (Direct Memory Access)

11.FreeRTOS

11.1.Theoretical Knowledge

https://www.bilibili.com/video/BV19g411p7UT

Below, only some commonly used operations and precautions will be discussed. For more detailed FreeRTOS configuration, see: (study alongside this)

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

STM32F1 FreeRTOS开发手册_V1.1.pdf

STM32F4 FreeRTOS开发手册_V1.1.pdf

11.2.Common content (the tutorial below focuses on how to configure CubeMX; for theoretical knowledge, please refer to ALIENTEK)

11.2.1.System Configuration
  1. Select the system timebase source

    1. Reason: Because FreeRTOS will occupy SysTick, the timebase source needs to be changed.

    2. Selection rule: Prefer timers with fewer functions. (For example, TIM6 and TIM7 on the F407ZGT6 have relatively few functions.)

    1. How to select? (As shown in the figure)

  1. Select the Interface

    1. Reason: FreeRTOS follows ARM's CMSIS standard.

    2. Selection principle: Prefer CMSIS v1, because CMSIS v2 still has some minor unresolved issues.

    3. How to choose?

  2. Configure Include Parameters

    1. Function: Same as hal_conf.h (used to enable certain features of the HAL library), used to enable certain features of FreeRTOS.

    2. Configuration of Include Parameters

      1. CubeMX configuration (recommended)

      2. Simply enable the corresponding feature as needed. (The commonly used one is vTaskDelayUntil.)

      1. Manual header file editing configuration (not recommended)
11.2.2.Create Task
  1. CubeMX task creation:

    1. Introduction to each parameter (see the DJI manual for details):

    1. What parameters are generally selected?

      1. Task Name: uppercase English letters (corresponds to Entry Function)

      2. Priority: generally, the normal priority is sufficient (unless there is special logic)

      3. Stack Size: 128 Words is sufficient

      4. Entry Function: lowercase English letters (corresponds to Task Name)

      5. Code Generation Option: just select As weak (generates the FreeRTOS thread task entry function as a weak function)

      6. Parameter: generally NULL is sufficient; if some special features are to be used (such as a semaphore), some handles must be filled in (such as the semaphore handle)

      7. Allocation: just select Dynamic, and let FreeRTOS handle dynamic allocation and management

  1. Notes:

    1. Creating too many tasks will cause memory to explode
11.2.3.Delay
  1. Relative delay

    1. Functions: The following two functions have the same effect, osDelay() and vTaskDelay()

    2. Timing: It starts counting only from when the function is called until the specified delay time ends

    3. Call method: Same as the HAL_Delay() method

    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. Absolute delay

    1. Functions:

      1. Get current time: osKernelSysTick()

      2. Absolute delay function: osDelayUntil()

    2. Timing: Time starts counting from the beginning of the task; the entire task execution cycle is treated as a whole, suitable for tasks that run at a certain frequency

    1. Call method:

11.2.4.Task state transition
  1. FreeRTOS states (see DJI manual for details):

  1. Function introduction:

  1. How to call:

11.2.5.Queue
  1. Reason: Global variables are not safe in multithreading; when multiple tasks operate on the same variable, data is easily corrupted.

  2. Queue: A queue is a mechanism for data exchange (message passing) from task to task, task to interrupt, and interrupt to task.

  3. Specific content: See the ALIENTEK video for learning the theoretical knowledge.

  4. Call

    1. CubeMX configuration: (Queue Size selects the number of bits of the data you want to transmit (i.e., the number of binary digits), Item Size selects the data type)

    1. Call (for details, see ALIENTEK):

11.2.6.Semaphore (a special form of queue)
  1. Reason: same queue

  2. Semaphore: a special kind of queue, a mechanism for solving synchronization problems, which can achieve orderly access to shared resources.

  3. Classification: binary semaphore, counting semaphore (see ALIENTEK for details)

  4. Synchronization problem: after A completes a task, it notifies B, and only then can B perform its task; this is called a synchronization problem.

  1. Introduction to Semaphores (see ALIENTEK for details)

  1. Comparison of queues and semaphores

  1. Introduction to binary semaphores:

  1. CubeMX configuration

Pass the handle of the created binary semaphore into the task's parameter argument.

Actually, setting it to NULL is also fine.

  1. Call (for detailed API function descriptions, see ALIENTEK)

    1. Semaphore release function: xSemaphoreGive();

    2. Semaphore acquire function:

11.2.7.Memory Management
11.2.7.1.Introduction

Stack area (stack):Automatically allocated and released by the compiler, it stores function parameter values, local variable values, etc. Its operation mode is similar to the stack in data structures.

Heap:Generally allocated and released by the programmer; if the programmer does not release it, it may be reclaimed by the operating system when the program ends. The allocation method is similar to a linked list in data structures.

(For details, seeVinci Robot Team C/C++ materials)

11.2.7.2.Modify the stack and heap size of STM32

####### Modifying the heap and stack size of the STM32 itself

As shown in the figure above,

The general total memory size of STM32 is 20Kb.

Heap Size is the heap size, which is 512 byte = 0.5 Kb.

Stack Size is the stack size, which is 1024 bytes = 1 KB.

The remaining portion of memory is allocated to the remaining regions, with most of that memory allocated to the Static region.

After generating the project with CubeMX, you can see the heap and stack size addresses we set in the startup file. (Of course, they can also be modified here, but it is recommended to modify them directly in CubeMX. Generally, there is no need to change them unless required.)

####### Modifying the FreeRTOS heap size (this heap is not the same as the other heap; see the introduction below)

  1. TOTAL_HEAP_SIZE: If FreeRTOS is used, the size of the FreeRTOS heap can be modified here.

  2. memory management scheme: The algorithm for dynamic memory allocation can be modified; generally, the heap_4 algorithm is used.

  3. FreeRTOS_HEAP The heap here is not the same heap; rather, it is memory that FreeRTOS has carved out from the STM32's ZI region (it can be understood this way: it is actually memory that the FreeRTOS kernel has seized from data, bss, heap, stack, etc.), and is not memory carved out from the STM32's Heap region (when you choose the heap_1, 2, 4, 5 algorithms). If you choose the heap_3 algorithm, then the C library's malloc() and free() functions will be used to allocate heap memory. At this time, FreeRTOS is using the STM32's heap region (the STM32's heap region is relatively small, so it is not as good as the heap_4 algorithm). However, we generally use the heap_4 algorithm for memory management, so here theFreeRTOS_HEAP is allocated from the stm32_ZI region. (That is, FreeRTOS_HEAP is not requested directly from the heap region, but is allocated very flexibly in RAM, and can exceed the size of STM32_HEAP.)

  4. Because we use the heap_4 algorithm, we do not need to modify the STM32's heap and stack; we only need to modify FreeRTOS_HEAP. (That is, allocate the STM32's ZI region memory that FreeRTOS can control.)

11.2.7.3.Introduction to Memory Management APIs

####### Memory management APIs in the C language library (not recommended)

####### ALIENTEK Block-Based Memory Management API

memx refers to a memory block, internal SRAM and external SRAM (there may not necessarily be external SRAM).

####### FreeRTOS Memory Management API (Recommended)

  1. Introduction

  1. FreeRTOS memory management algorithm (we generally choose heap_4)

heap_4's first-fit algorithm finds the first suitable memory size starting from the heap memory start address block.

  1. FreeRTOS Memory Management API Functions

By looking at the above code, you can see that after applying for memory and then releasing it, the amount of free memory will be restored.

However, it can be seen that at this point we allocated a 4-byte block of memory, but it deducted 16 bytes of memory. This is due to byte alignment: FreeRTOS chooses to trade space for speed by performing byte alignment.

12.FPU floating-point computation acceleration

Since the STM32 has a relatively low main frequency, floating-point operations are very slow. Currently, there are several methods available to optimize large floating-point operations such as sin and cos.

12.1.Check whether it is supported

STM32 Series CPU Core DSP Instructions FPU Type arm_cos_f32() Performance Suitable Computation Recommended Functions
STM32H7 Cortex-M7 ✅ Supported ✅ Double-Precision FPU (DP-FPU) 🚀 Fastest (Hardware Accelerated) High-Precision Computation, Robotics, Filtering, Navigation arm_cos_f32()
STM32H5 Cortex-M33 ✅ Supported ✅ Double-Precision FPU (DP-FPU) 🚀 Fastest (Hardware Accelerated) High-Precision Computation, Filtering, AI Computation arm_cos_f32()
STM32F7 Cortex-M7 ✅ Supported ✅ Single-Precision FPU (SP-FPU) 🔥 Very Fast (Hardware Accelerated) Robot Control, Navigation, Filtering arm_cos_f32()
STM32F4 Cortex-M4 ✅ Supported ✅ Single-Precision FPU (SP-FPU) 🔥 Very Fast (Hardware Accelerated) Robot Control, Mathematical Operations arm_cos_f32()
STM32G4 Cortex-M4 ✅ Supported ✅ Single-Precision FPU (SP-FPU) 🔥 Very Fast (Hardware Accelerated) Motor Control, Filtering arm_cos_f32()
STM32L4 Cortex-M4 ✅ Supported ✅ Single-Precision FPU (SP-FPU) 🔥 Very Fast (Hardware Accelerated) Low-Power Computation arm_cos_f32()
STM32U5 Cortex-M33 ✅ Supported ✅ Single-Precision FPU (SP-FPU) 🔥 Very Fast (Hardware Accelerated) Low-Power AI Computation arm_cos_f32()
STM32F3 Cortex-M4 ✅ Supported ❌ No FPU ⚠️ Slower (No FPU, DSP Acceleration Only) Motor Control, Signal Processing arm_cos_q31()
STM32G0 Cortex-M0+/M4 ❌ Partially Supported ❌ No FPU (Some M4 versions have SP-FPU) ⚠️ Slower (Software Computation) Basic Control arm_cos_q31()
STM32F1 Cortex-M3 ❌ Not Supported ❌ No FPU 🚫 Slowest (Pure Software Computation) Floating-Point Computation Not Recommended arm_cos_q31()
STM32F0 Cortex-M0 ❌ Not Supported ❌ No FPU 🚫 Slowest (Pure Software Computation) Floating-Point Computation Not Recommended arm_cos_q31()
STM32L0 Cortex-M0+ ❌ Not Supported ❌ No FPU 🚫 Slowest (Pure Software Computation) Ultra-Low-Power Applications arm_cos_q31()

12.2.Enable FPU

A floating-point unit (FPU) is a structure used to perform floating-point operations, typically implemented by circuits and applied in computer chips. The ARM-designed M4 core and higher cores all support the FPU, that is, the STM32F4 series and above.(That is, STM32F1 does not support it.)

STM32F4/F7 generally has a single-precision FPU, while STM32H5/H7 generally has a double-precision FPU.

Enabling the FPU on STM32F4 versus not enabling it often results ina difference of dozens or even hundreds of times.

When using STM32CubeMX to generate a project, the FPU is enabled by default, as shown in the figure below.

If you use F1, this option will not appear at all, which means the M3 core does not support the FPU.

From the source code in the image below, it can be seen that the FPU is enabled.

12.3.DSP acceleration

DSP acceleration refers to the optimization of trigonometric algorithms by the CMSIS-DSP library to speed up computation, but the error will become somewhat larger. However, for 99% of application scenarios, the error is sufficient, approximately an error of 1 e-6 unit.

The DSP library is only applicable to ARM Cortex-A and Cortex-M cores, that is, it is suitable for devices such as mobile phones, ARM microcontrollers, Raspberry Pi, and so on.

For STM32 microcontrollers, it basically covers all STM32 series, so all of them can be used.

Assuming you do not have an FPU, such as with STM32F1 series microcontrollers, you can still accelerate trigonometric function calculations through the DSP library. This DSP library is optimized using a mathematical operation method of table lookup plus interpolation, and the calculations are also relatively fast.

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

Therefore, when running on STM32, it is still recommended to use functions from the DSP library.

12.3.1.Install and enable the DSP library:
  1. Method 1 (recommended): Open using CubeMX

Then enable the DSP library

After the project is generated, you can see our generated lib through MDK5 or MDK6.

  1. Method 2 (not recommended): Open using MDK5

This approach increases compilation time by at least 200%.

12.3.2.Function Introduction

ARM core CPUs support trigonometric functions from the CMSIS-DSP library, which are faster than those of the standard math.h、cmath functions.

  1. Common C/C++ trigonometric function library:

The following are ordinary overloaded trigonometric functions. After we enable the FPU, as long as the argument passed in is of type fp32, the speed is actually quite fast, and the DSP library does not have to be used.

#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 library functions:

Pass in an fp32 value.

    // 更新机器人的位置(假设机器人沿着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;
12.3.3.Performance Comparison
✅ For microcontrollers with an FPU
Function
Overloaded function std::__math::cos(x)
float
cosf(x)
arm_cos_f32(x)
arm_cos_q31(x)
Lookup table method (LUT)

In addition toarm_cos_f32 there are some otherarm_cos_q31 functions that may be more suitable for low-end chips like the F103, allowing for free selection.

❌ For microcontrollers without FPU
Function
arm_cos_f32(x)
arm_cos_q31(x)
Overloaded function std::__math::cos(x)
float
cosf(x)
Lookup table method (LUT)

13.DMA + multi-channel ADC (remote controller joystick)

CubeMX configuration:

Most multi-channel ADCs need to enable scan mode;

Whether ADC continuous mode is enabled or disabled affects the related code in the main function. If continuous mode is not enabled, the ADC must be continuously started in the while loop.

Enable continuous mode (the 500 delay can be removed)

Off:

In comparison, continuous driving is faster, so continuous driving is more recommended.

14.STM32 Frequently Asked Questions

14.1.STM32 download issue using ST-link

  1. Reason: When using the CubeMX configuration file, the Debug option under the SYS section was forgotten to be set.

  1. Phenomenon: After downloading a program once, the program fails to run, and it cannot be downloaded again.

  2. STM32 has three boot modes in total:

    1. User Flash: Normal operating mode. In the STM32's built-in Flash. Generally, when we download programs using JTAG or SWD mode, they are downloaded into this. After a reset, the program also starts from here.

    2. SRAM: The chip's built-in RAM area, i.e., memory. It has no capability to store programs. This mode is generally used for debugging.

    3. System memory: The system memory is a specific area inside the chip.

    4. The STM32 manufacturer has placed a Bootloader inside this area. This boot mode is selected to enable downloading programs from the serial port, because the Bootloader provided by the vendor includes firmware for serial port downloading, and through this Bootloader, programs can be downloaded into the system Flash.

  3. Solution:

    1. Set BOOT0 to 1; set BOOT1 to 0.

    1. After connecting to the computer, press the reset button, and use Keil5 to download a known good, normal program. It is found that the program downloads normally.

    2. Change the BOOT pins back to their original state, and try downloading the program again. It is found that everything is normal.