跳到正文

Wiki

stm32微控制器(重點)

約 100 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

1.微控制器介紹

1.1.①什麼是微控制器?

微控制器又稱單片微控制器,它不是完成某一個邏輯功能的晶片,而是把一個計算機系統整合到一個晶片上。相當於一個微型的計算機,和計算機相比,微控制器只缺少了I/O裝置。概括的講:一塊晶片就成了一臺計算機。它的體積小、質量輕、價格便宜、為學習、應用和開發提供了便利條件。同時,學習使用微控制器也是瞭解計算機原理與結構的最佳選擇。

1.2.②微控制器的應用?

  1. 物聯網(※)

  2. 醫用裝置

  3. 工業控制

  4. 計算機網路通訊(※)

  5. ... ...

1.3.③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)
        

        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.⑤晶片手冊

  1. 作用:查詢各種晶片資訊(比如CPU頻率,IO定義,時鐘樹等等)

3.軟體介紹

  1. STM32庫

    1. 各種開發方式:暫存器,標準庫,HAL庫,LL庫

      1. 暫存器功能簡單瞭解:暫存器就是一個離CPU核心更近的儲存結構,所以與CPU核心交換資料比記憶體(RAM)更快,每個暫存器都有不同的功能,在暫存器裡存不同的值,CPU讀取後都會實現對應的不同功能。

      2. 庫:庫是原始檔+標頭檔案。stm32的庫是由組合語言、C語言混合編譯而成(HAL庫、LL庫相容C++)。現有標準庫、HAL庫、LL庫(標準庫已淘汰,咱們實驗室使用HAL庫和LL庫)

      3. 各種開發方式的優缺點:

        1. 暫存器:這種開發方式硬體執行效率高,但由於STM32暫存器過於多,用暫存器寫可讀性差,且麻煩繁瑣,故不建議全用暫存器寫,在某些場合下可以偶爾使用。(比如在流水燈可直接對暫存器進行移位操作、在調PWM佔空比時可直接對CCR暫存器進行賦值等)

        2. 標準庫:太老了,現在已經淘汰,該庫使用匯編+C語言進行開發,程式碼可讀性很高,但是由於初期對標準庫設計有些問題以及一些專利上的問題,導致會出一些問題(比如IIC通訊),且時鐘配置過於麻煩繁瑣,所以咱們於2021級開始就不再使用標準庫。

        3. HAL庫、LL庫(力推):ARM公司與ST意法半導體力推的庫,符合ARM CMSIS標準,該標準是當今嵌入式開發者都需要遵循的一個標準。該庫由組合語言+C語言進行開發,且相容C++(標頭檔案中有extern "C"條件編譯),使用C++的OOP(物件導向)進行開發要方便一萬倍。HAL庫和LL庫仍然被ST公司維護中,其解決掉了標準庫的各種確定,比如硬體IIC無法正常使用,時鐘配置及其容易。ARM CMSIS標準介紹: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.時鐘樹

4.1.①使用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. 零錯誤零警告即配置成功,有錯誤有警告請自行百度、谷歌

4.2.②查詢某個外設時脈頻率的方法(拿定時器來舉例子)

  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

5.stm32程式組成

5.1.基本介紹(主函式等)

  1. 工程構成:stm32工程是由C語言和組合語言的庫組成的工程,所以有主函式,符合C/C++語言的結構。

  2. 程式執行順序:除了預編譯等,程式從主函式開始執行,而且非常符合C/C++執行順序,從主函式開始會逐行執行程式碼,然後會進入死迴圈。

  3. 主函式內必須的組成部分:死迴圈[while(true)或者for(;;)],因為微控制器要一直執行下去,所以有個死迴圈。

  4. HAL庫 與 使用者自定義庫

    1. 庫:

      1. .h檔案宣告函式

      2. .c/.cpp檔案定義函式

      3. .c/.cpp檔案呼叫函式

5.2.中斷服務函式的介紹

  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.RTOS與ROS/ROS2簡單瞭解

  1. 進階(非裸機開發,基於RTOS系統開發)

    1. 常見的RTOS(嵌入式即時作業系統):FreeRTOS、Nuttx、RT-Thread、μC/OS-II、Xiaomi VelaOS

    2. FreeRTOS官網:https://www.freertos.org/zh-cn-cmn-s/

    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

6.暫存器

  1. 理解:是CPU內部用來存放資料的一些小型儲存區域,用來暫時存放參與運算的資料和運算結果。

  1. 實現的功能:

  1. 暫存器如何在基於C語言的HAL庫中發揮作用的呢?(應該說是 C語言HAL庫實現stm32微控制器控制的原理是什麼?)

7.Vinci機器人隊標準工程格式

7.1.英語

必須用 英語 ,工程檔名、函式名、變數名必須用英語!(走出中文舒適圈,最起碼一些專業英語你要認識)

7.2.正點原子HAL庫工程標準:

7.3.Vinci機器人隊STM32工程標準(Cube+C語言):

  1. applications應用層

  1. bsp驅動層

  1. Middlewares中間層

  1. Core(主函式所在地,條件編譯配置HAL庫的標頭檔案所在地)

7.4.  Vinci機器人隊STM32C/C++工程標準(類正點原子,試執行,不建議建议用下一节的类Cube_Cpp):

  1. 應用層、驅動層等採用模組整合式,不再採用Src和Inc分離式

  1. 公共相容層:

  2. C++子main相容庫

  1. 作用:在.cpp檔案中建立一個普通的函式,該函式呼叫C++的程式碼,然後被C語言main.c檔案中的main函式所呼叫。

  1. 弱函式_回撥函式庫(該檔案的原始檔全域性要有extern "C",因為弱函式是C語言的東西,C++無法正常識別)

7.5.(建議)Vinci機器人隊STM32Cube C/C++工程標準(類Cube,試執行,建議):

7.5.1.首先開啟CubeMX進行工程配置,比如我們這裡用裸機開發使一個LED燈閃爍

然後選擇OpenFolder開啟資料夾

7.5.2.開啟Github將一些必備檔案進行克隆

倉庫連結:

https://github.com/tungchiahui/CubeMX_MDK5to6_Template

或者直接開啟terminal輸入

git clone https://github.com/tungchiahui/CubeMX_MDK5to6_Template.git
7.5.3.開啟克隆的模板與剛才CubeMX生成的工程

開啟模板中的***工程文件移植(创建新模板请看这里)*** 資料夾,然後將裡面的檔案全部複製到CubeMX工程檔案中。

移動後:

7.5.4.開啟工程設定工程
  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組中。

可以看到工程裡的檔案都就緒了。

7.5.5.編譯並配置一些必要程式碼

可以右鍵標頭檔案,然後點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++庫中的程式碼啦。

7.5.6.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語言的條件編譯。(如果忘了,請看Vinci機器人隊C/C++資料

然後引用startup_main.h標頭檔案

再建立該模組的類,比如說class BSP_LED等,我這裡為延時的類,所以是class BSP_Delay。

然後寫類裡的宣告。

注意:不要在.h檔案裡寫任何程式碼實現,也就是不能寫任何函式的定義。

然後第35行的extern BSP_Delay bsp_delay;是在主函式中進行了建立物件bsp_delay,我們要在這裡宣告(extern)一下物件(變數)。方便其他的原始檔呼叫。

理論上你是可以看懂上面所說的的,如果你實在看不懂,就照葫蘆畫瓢,畫著畫著也就理解了。

7.5.7.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)
 {
 // ... ...
 }
7.5.8.注意事項
  1. 在.cpp原始檔中,弱函式的定義前面要加個extern "C" 因為__weak是C語言(彙編向量)特有的,所以必須把程式碼以C語言的形式連結。

  2. 程式碼要寫在Begin和End之間,否則再次用CubeMX配置程式碼後,程式碼會消失。

8.驅動

  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.  大疆電機控制(CAN)

###   ①CAN通信简介

3. 什麼是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)

9.1.②電機庫程式碼解析(該庫內容要求儘量全部看懂,儘量一行不差)

  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. 暫時用不著的函式(此函式沒有被呼叫)

9.2.③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,那這個時候我們需要往裡面加水的量就是KpErr,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. 閉環程式碼呼叫

9.3.④C++庫(建議)

9.3.1.簡介

程式碼倉庫連結: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控制器的初始化程式碼和一些閉環實現程式碼。

9.3.2.C++大疆電機庫
9.3.2.1.  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)

9.3.2.2.CAN_BUS::BSP類的方法(函式) (在bsp_can.cpp中)

#######   CAN_Start 開啟CAN通訊的函式

####### Filter_Init CAN通訊濾波的函式

9.3.2.3.CAN_BUS::DJI_ENCODER類的方法(函式) (在can_receive.cpp中)

####### get_motor_measure 處理CAN通訊接收到的大疆電機編碼器資料,並處理得到 電機各個資訊 函式

####### get_moto_offset 處理CAN通訊接收到的大疆電機編碼器資料,並處理得到 電機剛開始上電的角度初始值 函式

####### get_total_angle 處理CAN通訊接收到的大疆電機編碼器資料,並處理得到 電機角度值 函式。(暫時沒被呼叫

9.3.2.4.CAN_BUS::CMD類的方法(函式) (在can_receive.cpp中)

####### CAN1_Front CAN1 4個電機發送電流的函式

####### CAN1_BehindCAN1 4個電機發送電流的函式

####### CAN2_FrontCAN2 4個電機發送電流的函式

####### CAN2_Behind CAN2 4個電機發送電流的函式

9.3.2.5.CAN_RX0接收中斷回撥函式 (在can_receive.cpp中)

9.3.3.C++PID庫
9.3.3.1.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座標實現函式;

9.3.3.2.PID_Controller類的方法(函式) (在pid_user.cpp中)

#######   All_Device_Init 將所有裝置的PID控制器進行初始化

9.3.3.3.PID_Controller::CORE類的方法(函式) (在pid.cpp中)

#######   PID_Init PID核心初始化函式

#######   PID_Calc PID核心計算函式

#######   PID_Clear PID清0函式

9.3.3.4.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速度位置雙環實現函式

9.3.3.5.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++庫你們再補充)

9.3.4.如何呼叫?

我這裡選擇每隔1ms使用PID控制器進行一次負反饋迴路的控制,併發送一次電流值。

可以選擇在while(1)死迴圈中加個delay(1)進行傳送;

也可以使用週期為1ms的定時器中斷進行實現,更建議使用定時器中斷。

9.4.⑤實物連線,詳細的請看說明書

10.DMA(Direct Memory Access / 直接儲存器訪問)

11.FreeRTOS

11.1.理論知識

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

下方只會講一些常用的操作和注意事項,更詳細的FreeRTOS配置請看:(配合著學習)

大疆開發板C型嵌入式軟體教程文件.pdf

STM32F1 FreeRTOS開發手冊_V1.1.pdf

STM32F4 FreeRTOS開發手冊_V1.1.pdf

11.2.常用的內容(下方教程著重講CubeMX如何配置,理論知識請看正點原子)

11.2.1.系統配置
  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. 手動編輯標頭檔案配置(不推薦)
11.2.2.建立任務
  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. 任務建立太多會記憶體爆掉
11.2.3.延時
  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. 呼叫方法:

11.2.4.任務狀態轉換
  1. FreeRTOS狀態(詳看大疆手冊):

  1. 函式介紹:

  1. 如何呼叫:

11.2.5.佇列
  1. 原因:全域性變數在多執行緒裡是不安全的,多個任務對該變數進行操作時,資料容易受損。

  2. 佇列:佇列是任務到任務、任務到中斷、中斷到任務資料交流的一種機制(訊息傳遞)

  3. 具體內容:詳看正點原子影片學習理論知識

  4. 呼叫

    1. CubeMX配置:(Queue Size選擇你要傳的資料的位元數(即二進位制位數),Item Size選擇資料的型別)

    1. 呼叫(詳解請看正點原子):

11.2.6.訊號量(佇列的特殊形式)
  1. 原因:同隊列

  2. 訊號量:一種特殊的佇列,是一種解決同步問題的機制,可以實現對共享資源的有序訪問。

  3. 分類:二值訊號量、計數型訊號量(詳見正點原子)

  4. 同步問題:A做完一個事情,通知B,B才可以做,這叫同步問題。

  1. 訊號量簡介(詳見正點原子)

  1. 佇列與訊號量的對比

  1. 二值訊號量介紹:

  1. CubeMX配置

要把建立的二值訊號量的控制代碼傳入任務的parameter引數裡。

其實設定為NULL也可以。

  1. 呼叫(詳細API函式作用請看正點原子)

    1. 釋放訊號量函式:xSemaphoreGive();

    2. 獲取訊號量函式:

11.2.7.記憶體管理
11.2.7.1.簡介

棧區(stack):由編譯器自動分配和釋放,存放函式的引數值、區域性變數的值等,其操作方式類似於資料結構中的棧。

堆區(heap):一般由程式設計師分配和釋放,若程式設計師不釋放,程式結束時可能由作業系統回收。分配方式類似於資料結構中的連結串列。

(詳細請看Vinci機器人隊C/C++資料)

11.2.7.2.修改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區記憶體進行分配)

11.2.7.3.記憶體管理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選擇使用用空間換速度的方式進行位元組對齊。

12.FPU浮點數計算加速

STM32由於主頻比較低,所以運算浮點數運算會非常慢,目前有下列幾種辦法可以最佳化sin,cos這種大型浮點數運算。

12.1.檢查是否支援

STM32 系列 CPU 核心 DSP 指令 FPU 型別 arm_cos_f32() 效能 適合的計算 建議使用的函式
STM32H7 Cortex-M7 ✅ 支援 ✅ 雙精度 FPU (DP-FPU) 🚀 最快(硬體加速) 高精度計算、機器人、濾波、導航 arm_cos_f32()
STM32H5 Cortex-M33 ✅ 支援 ✅ 雙精度 FPU (DP-FPU) 🚀 最快(硬體加速) 高精度計算、濾波、AI 計算 arm_cos_f32()
STM32F7 Cortex-M7 ✅ 支援 ✅ 單精度 FPU (SP-FPU) 🔥 很快(硬體加速) 機器人控制、導航、濾波 arm_cos_f32()
STM32F4 Cortex-M4 ✅ 支援 ✅ 單精度 FPU (SP-FPU) 🔥 很快(硬體加速) 機器人控制、數學運算 arm_cos_f32()
STM32G4 Cortex-M4 ✅ 支援 ✅ 單精度 FPU (SP-FPU) 🔥 很快(硬體加速) 電機控制、濾波 arm_cos_f32()
STM32L4 Cortex-M4 ✅ 支援 ✅ 單精度 FPU (SP-FPU) 🔥 很快(硬體加速) 低功耗計算 arm_cos_f32()
STM32U5 Cortex-M33 ✅ 支援 ✅ 單精度 FPU (SP-FPU) 🔥 很快(硬體加速) 低功耗 AI 計算 arm_cos_f32()
STM32F3 Cortex-M4 ✅ 支援 ❌ 無 FPU ⚠️ 較慢(無 FPU,僅 DSP 加速) 電機控制、訊號處理 arm_cos_q31()
STM32G0 Cortex-M0+/M4 ❌ 部分支援 ❌ 無 FPU(部分 M4 版有 SP-FPU) ⚠️ 較慢(軟體計算) 基礎控制 arm_cos_q31()
STM32F1 Cortex-M3 ❌ 不支援 ❌ 無 FPU 🚫 最慢(純軟體計算) 不推薦做浮點計算 arm_cos_q31()
STM32F0 Cortex-M0 ❌ 不支援 ❌ 無 FPU 🚫 最慢(純軟體計算) 不推薦做浮點計算 arm_cos_q31()
STM32L0 Cortex-M0+ ❌ 不支援 ❌ 無 FPU 🚫 最慢(純軟體計算) 超低功耗應用 arm_cos_q31()

12.2.開啟FPU

浮點運算單元(FPU)是一種用於執行浮點運算的結構,通常由電路實現,應用於計算機晶片中。ARM設計的M4核心及更高階的核心都支援FPU,也就是STM32F4系列及往上。(也就是STM32F1是不支援的)

STM32F4/F7一般有單精度FPU,而STM32H5/H7,一般有雙精度FPU。

STM32F4開啟FPU和不開啟FPU往往會有數十倍甚至上百倍的差距。

使用STM32CubeMX生成工程,會預設開啟FPU,如下圖。

如果你使用F1的話,會壓根都沒有這個選項,代表M3核心不支援FPU。

下面這張圖可以從原始碼看到開啟了FPU。

12.3.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) ✅最快(查表+插值) ✅比較快(完整計算) ✅比較快(和 std::cos 相近)
Cortex-M0/M3(無FPU) ⚠️比較慢(查表+插值) 🚫最慢(軟體浮點) 🚫最慢(和 std::cos 相近)
Cortex-A(如 Raspberry Pi) ✅可能更快(查表方法) ✅更快(用 SIMD/FPU) ✅更快(glibc/libm,SIMD 最佳化)
x86/x86-64(PC 端) ❌不可用 ✅最快(硬體加速) ✅最快(使用 FPU 或 SIMD)

所以說在STM32上跑還是建議用dsp庫的函式。

12.3.1.安裝並使能DSP庫:
  1. 方法一(推薦):使用CubeMX開啟

然後使能DSP庫

生成工程後,可以通過MDK5或者MDK6看到我們生成的lib。

  1. 方法二(不推薦):使用MDK5開啟

這種方式會使編譯時間增加至少200%.

12.3.2.函式介紹

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;
12.3.3.效能對比
✅ 對於有FPU的微控制器
函式
過載函式std::__math::cos(x)
float
cosf(x)
arm_cos_f32(x)
arm_cos_q31(x)
查表法(LUT)

除了arm_cos_f32,還有其他的一些arm_cos_q31函式,可能更加適配於F103這種低端晶片,可以進行自由選擇。

❌ 對於無FPU的微控制器
函式
arm_cos_f32(x)
arm_cos_q31(x)
過載函式std::__math::cos(x)
float
cosf(x)
查表法(LUT)

13.DMA+多通道adc(遙控器遙杆)

cubemx配置:

多通道adc大部分要開啟掃描模式;

adc連續模式開啟或者關閉,影響mian函式的相關程式碼,不開continuous則需在while中不斷對adc進行開啟

開連續模式(延時500可以去掉)

不開:

相比之下開連續更快,更建議連續

14.STM32常見問題

14.1.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引腳改為原來的狀態,再次嘗試下載程式發現一切正常。