第 4 節

stm32單片機(重點)

0瀏覽次數0訪問次數--跳出率--平均停留

單片機介紹

①什麼是單片機?

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

②單片機的應用?

  1. 物聯網(※)
  2. 醫用設備
  3. 工業控制
  4. 計算機網絡通信(※)
  5. ... ...

③stm32單片機組成部分

  1. CPU(中央處理器)
    1. 芯片框圖

    1. 處理器內核(內核這個東西瞭解一下,以後要根據內核的架構和操作系統來判斷下什麼版本的軟件)
      1. 介紹與作用 : CPU所有的計算、接收/存儲命令、處理數據全部由內核執行。
      2. 指令集分類 : ARM架構、X86架構、LoongArch架構,RISC-V架構
        1. ARM架構指令集
          1. 應用:廣泛應用於移動行業(手機、平板、工控機等)等需要很強的能耗比的場景中
          2. 架構分類: ARM32、AArch64(ARM64)等
          3. 內核分類:ARM Cortex-X(手機) 、ARM Cortex-A(手機) 、 ARM Cortex-R(嵌入式) 、 ARM Cortex-M(嵌入式)
          4. 瞭解CPU Soc和CPU內核的區別

                  小米玄戒o1,華為麒麟9000,意法半導體STM32F407VET6這三個芯片都是基於ARM架構。他們的CPU的核心的前端設計都是由英國ARM公司設計好的,ARM的內核決定了這個CPU的性能,總線,浮點運算器等等。
                  而小米,華為,意法半導體只對CPU核心進行後端設計,對CPU性能等影響不會太大,CPU絕大部分特徵都是由ARM的前端設計決定的
                  所以,我們在使用F407IG,F407VE的時候,因為他們的CPU內核都是Cortex-M4,所以CPU特徵都一樣,所以代碼也幾乎都一樣的。


        1. X86架構指令集
        1.  應用:廣泛應用於電腦、軟路由、工控機等需要高性能計算的場景中
        
        2.  分類:X86、AMD64(X86\_64)
        

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

              ④原理圖

        1. 介紹:顧名思義就是表示電路板上各器件之間連接原理的圖表。(各元件在原理圖中是用整體形式來表示,進行二次接線的圖)


        1. 組成部分:
          1. 元器件(包括元器件端口)
          2. 導線
          3. 網絡標號
          4. 電源符號
          5. 等... ...

⑤芯片手冊

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

軟件介紹

  1. STM32庫
    1. 各種開發方式:寄存器,標準庫,HAL庫,LL庫
      1. 寄存器功能簡單瞭解:寄存器就是一個離CPU內核更近的存儲結構,所以與CPU內核交換數據比內存(RAM)更快,每個寄存器都有不同的功能,在寄存器裏存不同的值,CPU讀取後都會實現對應的不同功能。
      2. 庫:庫是源文件+頭文件。stm32的庫是由彙編語言、C語言混合編譯而成(HAL庫、LL庫兼容C++)。現有標準庫、HAL庫、LL庫(標準庫已淘汰,咱們實驗室使用HAL庫和LL庫)
      3. 各種開發方式的優缺點:
        1. 寄存器:這種開發方式硬件執行效率高,但由於STM32寄存器過於多,用寄存器寫可讀性差,且麻煩繁瑣,故不建議全用寄存器寫,在某些場合下可以偶爾使用。(比如在流水燈可直接對寄存器進行移位操作、在調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

時鐘樹

①使用CubeMX配置時鐘的步驟

  1. 時鐘配置介紹:這是每一個工程都需要做的事情,給予CPU正常的心跳。
  2. 作用:給予CPU正常的心跳,並且給予各個外設的心跳,讓CPU和其各個外設正常工作。(比如延時函數的準確度,定時器PWM波形的準確度)
  3. STM32F1系列CPU時鐘框圖:

  1. 配置需要注意的事項:
    1. 注意電路板上HSE的真實晶振頻率,填高了會導致超頻,會出現比較嚴重的問題
    2. 配置時建議用CubeMX配置時鐘函數,然後複製到正點原子模板工程中(因為手擼時鐘函數太難了)
    3. CubeMX參考文檔:大疆開發板C型嵌入式軟件教程文檔.pdf
  2. 配置步驟(這裏只點出幾個要注意的點,詳細步驟請看大疆C板開發文檔):
    1. 打開大疆C板開發文檔
    2. 找到目錄,點擊0.4.2

    1. 按照0.4.2的步驟開始操作(每一步必須都得做,特別是Debug選Serial Wire,不選的話該工程代碼會讓板子假變磚)
      1. 需要注意板子型號,大疆板子是stm32f407igh6,咱們需要根據咱們實際的板子型號進行選擇

      1. 配置時鐘樹時,需要注意HSE的時鐘頻率,按照實際原理圖上的HSE頻率來配置


      1. 代碼路徑必須全是英文,並且不能有連續兩個空格,建議直接不要空格,單詞之間用下劃線(不可以放在桌面上)

      1. 解釋


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

    1. 再複製一個並打開正點原子的模板工程


    1. 在CubeMX HAL庫工程中的main.c中找到時鐘函數void SystemClock_Config(void)的定義

    1. 複製整個void SystemClock_Config(void)函數的定義

    1. 然後打開正點原子的工程,在main函數中找到sys_stm32_clock_init(RCC_PLL_MUL9)函數,右鍵該函數,並go to definition of "sys_stm32_clock_init"找到這個函數的定義。

    1. 如果彈出下方的問題,請按照這個框框中的提示來解決,説的很明白。(如果看不懂英語,就去百度搜,鍛鍊下搜索能力)

    1. go to definition of "sys_stm32_clock_init"完後找到這個函數的定義,刪掉整個函數,並把剛才複製的CubeMX HAL庫裏的時鐘函數複製到這裏。並將Error_Handler();直接刪掉,或者替換成while(1);
    2. 找到sys_stm32_clock_init函數定義

    1. 框選後刪掉

    1. 把複製的CubeMX HAL庫裏的時鐘函數複製到這裏。

    1. 用while(1);替換掉Error_Handler();或直接刪掉。


    1. 找到void SystemClock_Config(void)函數所在的源文件sys.c對應的頭文件sys.h

    1. 找到sys_stm32_clock_init(uint32_t plln)函數,刪掉,替換成void SystemClock_Config(void)的聲明。


    1. 回到主函數,找到sys_stm32_clock_init(RCC_PLL_MUL9);函數,刪掉,並調用咱們新的時鐘函數


    1. 修改HSE_VALUE
    2. 隨便找個地方輸入HSE_VALUE並go to definition(go to definition完畢後,就可以刪掉這個自己寫的HSE_VALUE)

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

    通過看原理圖可知,該板子為8MHz。(具體填多少,看你板子HSE的原理圖,對應OSCIN和OSCOUT這倆IO口)

    1. 刪掉原來用來go to definition才寫的HSE_VALUE
    2. 刪掉多餘的代碼

    1. 第9行的delay_init的入口參數具體填什麼值,先查看一下他的定義
    2. 查看delay_init的定義,得知其入口參數為sysclk(系統時鐘)

    1. 查看CubeMX的時鐘樹框圖,得知SYSCLK的值為72MHz

    1. 把delay_init的值改為時鐘樹中的SYSCLK的值

    1. 然後編譯所有文件

    1. 零錯誤零警告即配置成功,有錯誤有警告請自行百度、谷歌

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

  1. 打開tim.c

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

  1. 查找__HAL_RCC_XXX_CLK_ENABLE()的定義

  1. 根據函數定義,可以看出TIM1掛載在APB2上

  1. 查詢時鐘樹,找APB2 Timer Clock可得TIM1的TCLK是168MHz

  1. 所以得知,TIM1的TCLK頻率為168MHz

stm32程序組成

基本介紹(主函數等)

  1. 工程構成:stm32工程是由C語言和彙編語言的庫組成的工程,所以有主函數,符合C/C++語言的結構。
  2. 程序運行順序:除了預編譯等,程序從主函數開始運行,而且非常符合C/C++運行順序,從主函數開始會逐行運行代碼,然後會進入死循環。
  3. 主函數內必須的組成部分:死循環[while(true)或者for(;;)],因為單片機要一直運行下去,所以有個死循環。
  4. HAL庫 與 用户自定義庫
    1. 庫:
      1. .h文件聲明函數
      2. .c/.cpp文件定義函數
      3. .c/.cpp文件調用函數

中斷服務函數的介紹

  1. 特殊函數(中斷服務函數):中斷服務函數是由彙編定義的,與芯片硬件更緊密,是由芯片中斷事件所觸發,並不滿足常規C/C++調用順序。
    1. 中斷服務函數的調用方式:由中斷事件所觸發。一旦滿足某個中斷事件,就立馬從正在運行的地方切換到中斷服務函數里開始運行,然後運行完中斷服務函數後,再返回剛才運行的地方接着運行。
    2. 中斷事件:比如説第X條線的外部中斷事件、systick滴答定時器中斷(普通延時函數的實現方式)、UART接收中斷事件、UART發送中斷事件、TIM定時器溢出更新中斷、TIM定時器輸入捕獲中斷、CAN通信發送中斷事件、CAN通信接收中斷事件、RTOS的PendSV中斷等等。(每個事件對應的中斷服務函數一般都不相同,但是也有一些中斷事件會共用同一個中斷服務函數)
    3. 中斷服務函數處理過程:
      1. CPU檢測到有中斷事件的發生
      2. 保護現場,將當前位置的PC地址壓棧(程序計數器(Program Counter));
      3. 跳轉到中斷服務函數,執行中斷服務程序;
      4. 恢復現場,將棧頂的值回送給PC;
      5. 跳轉到被中斷的位置開始執行下一個指令。



    1. 中斷優先級與分組
      1. 優先級:搶佔優先級和子優先級
      2. 分組0-5

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




    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. 作用:先確定是哪個句柄調用的,再進行相應的業務邏輯實現(可選,比如數據處理等)


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

寄存器

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

  1. 實現的功能:

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

Vinci機器人隊標準工程格式

英語

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

正點原子HAL庫工程標準:

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

  1. applications應用層

  1. bsp驅動層

  1. Middlewares中間層

  1. Core(主函數所在地,條件編譯配置HAL庫的頭文件所在地)

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

  1. 應用層、驅動層等採用模塊集成式,不再採用Src和Inc分離式

  1. 公共兼容層:
  2. C++子main兼容庫

  1. 作用:在.cpp文件中創建一個普通的函數,該函數調用C++的代碼,然後被C語言main.c文件中的main函數所調用。

  1. 弱函數_回調函數庫(該文件的源文件全局要有extern "C",因為弱函數是C語言的東西,C++無法正常識別)

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

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

然後選擇OpenFolder打開文件夾

打開Github將一些必備文件進行克隆

倉庫鏈接:

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

或者直接打開terminal輸入

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

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

移動後:

打開工程設置工程
  1. 打開MDK5工程

  1. 點擊Options for Target

  1. 修改編譯器為ARMClang[ARM Compiler6 (AC6)] 替換掉 ARMCC[ARM Compiler5 (AC5)]

  1. 添加頭文件的路徑(Include Path)

添加applications中的Inc文件夾

添加bsp/boards中的Inc文件夾

點擊OK即可

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

打開Manage Project Items

創建兩個分組

分組的名字分別叫

applications

bsp/boards

將Core/Src目錄下的startup_main.cpp加入到Application/User/Core組中。

將bsp/boards/Src目錄下的bsp_delay.cpp加入到bsp/boards組中。

可以看到工程裏的文件都就緒了。

編譯並配置一些必要代碼

可以右鍵頭文件,然後點Open Document "xxx.h"來打開頭文件,用來檢查頭文件是否導入成功。

找到main.c文件,準備在主函數main()中調用C++的類主函數startup_main();

在USER CODE BEGIN Includes和USER CODE END Includes這兩行註釋中間 引用startup_main.h (因為不放在BEGIN和END之間的代碼在CubeMX重新配置後,代碼都會消失)

在USER CODE BEGIN和USER CODE END這兩行註釋中間 调用startup_main();(因為不放在BEGIN和END之間的代碼在CubeMX重新配置後,代碼都會消失)

打開startup_main.h

更改isRTOS宏的值,如果是裸機開發則為0,如果使用了FreeRTOS則改為1。

至此,你可以在startup_main()函數中隨意調用C/C++庫中的代碼啦。

C++庫的頭文件格式

拿bsp_delay.h舉例

#ifndef __BSP_DELAY_H_
#define __BSP_DELAY_H_

#ifdef __cplusplus
extern "C" 
{
#endif

#include "startup_main.h"

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

extern BSP_Delay bsp_delay;

#ifdef __cplusplus
}
#endif

#endif

條件編譯肯定不能少,一個是防止頭文件重複引用的條件編譯,一個是把C++鏈接為C語言的條件編譯。(如果忘了,請看Vinci機器人隊C/C++資料

然後引用startup_main.h頭文件

再創建該模塊的類,比如説class BSP_LED等,我這裏為延時的類,所以是class BSP_Delay。

然後寫類裏的聲明。

注意:不要在.h文件裏寫任何代碼實現,也就是不能寫任何函數的定義。

然後第35行的extern BSP_Delay bsp_delay;是在主函數中進行了創建對象bsp_delay,我們要在這裏聲明(extern)一下對象(變量)。方便其他的源文件調用。

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

C++庫的源文件格式

拿bsp_delay.cpp舉例

#include "bsp_delay.h"

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

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

BSP_Delay bsp_delay;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

剛上來肯定要引用自己的頭文件。

這個條件編譯不用管,因為延時在裸機開發和RTOS開發時有區別,所以我加了一行條件編譯。

上來要先創建一下類對象bsp_delay;

然後把類裏的函數都進行定義。

函數註釋格式:

這一塊是該函數的註釋,以後儘量都這樣寫註釋。(在以後MDK6中進行調用函數時,會提示該註釋,一目瞭然)

這樣寫註釋的好處,在調用時,會顯示入口參數需要填什麼,會顯示返回值是什麼。

brief 函數摘要

param 入口參數

retval 返回值

note或attention 注意事項

注意這裏,有幾個param入口參數,就寫幾個param

比如

/**
 * @brief       CAN1通信发送函数
 * @param       motor1: 第1个电机的相对电流值
 * @param       motor2: 第2个电机的相对电流值
 * @param       motor3: 第3个电机的相对电流值
 * @param       motor4: 第4个电机的相对电流值
 * @retval      bool是否发送成功
 * @note        无特殊注意事项
 */
 bool CAN_BUS::CAN1::CMD1(int16_t motor1,int16_t motor2,int16_t motor3,int16_t motor4)
 {
 // ... ...
 }
注意事項
  1. 在.cpp源文件中,弱函數的定義前面要加個extern "C" 因為__weak是C語言(彙編向量)特有的,所以必須把代碼以C語言的形式鏈接。
  2. 代碼要寫在Begin和End之間,否則再次用CubeMX配置代碼後,代碼會消失。

驅動

  1. 驅動,驅動程序全稱設備驅動程序,能夠使計算機與相應的設備進行通信。驅動程序是硬件廠商根據操作系統編寫的配置文件,可以説沒有驅動程序,計算機中的硬件就無法工作。
  2. 普通模塊的驅動:GPIO初始化程序+通信協議程序 數據協議處理程序
    1. 例子:LED燈,只需要GPIO初始化程序;藍牙模塊,需要寫GPIO初始化程序和通信協議程序+數據處理程序


    1. GPIO程序:

    1. 通信協議程序:如圖是串口的通信協議程序以及GPIO程序

    1. 數據解析程序:如圖是PS2手柄的數據處理函數(見C++題庫數據解析的題型,主要用二進制,十六進制,位操作符等)
      1. 數據單位變換:
        1. 1Mbps(比特率) = 1000 000 bit/s(比特/秒) 1 byte(字節) = 8 bit(比特)= 8位二進制 (非常重要) 1 kbyte(千字節) = 1024 byte(字節) 1Mbyte(兆字節) = 1024 kbyte(千字節) 1Gbyte (千兆字節) = 1024Mbyte(兆字節)
        2. 1個字符 = 1 byte(字節)

              1個阿拉伯數字 = 1個字符
              在GBK編碼下,1個漢字 = 2個字符
              在Utf-8編碼下,1個漢字 = 3個字符
      2. 數據命名格式(詳細請見C++文檔):



    1. 針對HAL庫外設API的填參方法:
    ![](https://cdn.tungchiahui.cn/tungwebsite/assets/images/2023/10/09/image180.webp)
    
    2.  查看對應的數據類型
    
    3.  查數據手冊查函數的內容的註釋
    

  大疆電機控制(CAN)

###   ①CAN通信簡介

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)

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

  1. 代碼及其初始化
    1. 這部分正點原子都有講,只需要把參數改為大疆電機的

    代碼倉庫鏈接:https://github.com/SDUTEMIS/SDUT\_VinciRobot/tree/main/1.Embedded\_STM32\_Driver%2FC%2F4.Motor\_Drivers%2F1.DJI\_CAN\_PID

  1. 大疆電機庫開環代碼解析:庫由往屆學長學姐對大疆官方庫代碼修改後的。
    1. CAN報文發送函數解析





       此函數是將電流值發送給大疆CAN1通信電機,CAN1通信每次只能發送8比特的數據,電流值是16比特的數據,所以把電流值向右移8位,然後再發送給電機。電機接收到電流值就開始轉動(入口參數是電調ID為1-4的電機電流值)


       此函數是將電流值發送給大疆CAN1通信電機,CAN1通信每次只能發送8比特的數據,電流值是16比特的數據,所以把電流值向右移8位,然後再發送給電機。電機接收到電流值就開始轉動(入口參數是電調ID為5-8的電機電流值)
    1. CAN報文發送函數調用
    int16_t Current_Motor_Target[1];
    
    void chassis_task(void const * argument)
    {
      //wait a time
      //空闲一段时间
      vTaskDelay(20);  //等待所有设备准备就绪
    
      while(1)   //可以在定时器中断里实现
      {
        Current_Motor_Target[0] = 1000;    //测试电机闭环是否可用的代码,正式使用时请注释该行代码
        CAN1_CMD_1(Current_Motor_Target[0],0,0,0);  //对电调ID为1的电机发送1000电流使其开环转动。
        //系统延时
        vTaskDelay(2);  //等同于osDelay(2);      
      }
    
    }
    
  2. 大疆電機庫閉環代碼解析:庫由往屆學長學姐對大疆官方庫代碼修改後的。(在開環基礎上又加了CAN報文接收,以及一系列數據解析程序)
    1. CAN通信接收中斷回調函數(CAN_RX0接收中斷回調函數用來處理CAN通信電機發來的數據,也就是 電機的速度,角度,温度 等數據。)



    1. 數據解析函數
      1. 電機編碼器分為增量式編碼器和絕對值編碼器
        1. 增量式編碼器:上電時數據會丟失,角度從0開始
        2. 絕對值編碼器:掉電後數據不會丟失。
        3. M3508和M2006編碼器都為絕對值編碼器,掉電後數據不會丟失,但是因為記錄的是轉子的角度,轉子連接了一個減速器,所以導致數據是錯誤的,所以我們要用代碼將絕對值編碼器的數據轉化為增量式編碼器的數據來使用。(結構體裏的total是絕對編碼器的角度,而total_angle是我們處理後改為增量式編碼器的總角度)
      2. 記錄上電角度

      1. 計算總角度(經代碼處理後,上電時總角度 = 圈數(0) *8192 + 當前絕對值編碼器的角度(假設為A) - 上電時捕獲到的上電角度(因為此時為上電時,所以也為A) = 0)

      1. 暫時用不着的函數(此函數沒有被調用)

③PID控制器

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

關於理解PID控制算法最典型的一個例子就是一個漏水的水缸的問題。

有個漏水的水缸,而且漏水的速度還不是恆定的。然後我們還有個水桶,我們可以控制往水缸裏面加水或者從水缸裏面舀水出來。另外我們可以檢測水平面。現在我們的目的就是要控制水平面穩定在我們想要的任何一個平面上。

注意我們使用PID需要在一個閉環系統裏面。什麼叫閉環系統,就是有輸入有反饋,輸入就是能輸入一個量去影響和控制我們的系統,反饋就是我們要能知道我們最終控制的東西的狀態。在這個漏水的水缸系統中,輸入就是這個水桶,我們能通過水桶往水缸裏面加水或者從水缸裏面舀水出來來影響我們水缸的水平面,反饋的話也就是説我們要能測量水平面,知道水平面是多少。

a, 比例控制理解

首先是比例控制。比例控制就好比是通過水桶往水缸加水或者從水缸舀水。假設我們需要把水平面穩定在A平面,而實際水平面在B平面,那麼水平面差值Err=A-B,那這個時候我們需要往裏面加水的量就是Kp*Err,Kp就是我們的比例控制係數。

如果A>B,Err為正,就往水缸裏面加水;如果A<B,Err為負,就從水缸裏面舀水出來。那麼只要預期水平面和實際水平面有差值,我們都會通過水桶去加減水來調整系統。同時Kp的大小也有對系統的性能有影響。如果Kp的值比較大,優點是從B平面達到A平面的速度快,缺點是在B平面已經接近A平面的時候系統會產生比較大的震盪。如果Kp的值比較小,優點是B平面在接近A平面的時候系統震盪小,缺點是從B平面達到A平面的速度慢。

這裏也許有人會有疑問,如果這裏把比例控制係數Kp直接設置成1,然後加水的量直接為Err=A-B不就可以了。然而實際上很多系統是做不到這點的。比如温度控制系統,實際温度為10度,我要通過加熱把温度提升到40度,這裏難道我們能一次性準確的給系統加30度?顯然這是做不到的。那麼比例控制的最終結果是Err的值趨向於0。

b, 微分控制理解

然後我們先看看微分控制。在我們的比例控制的作用下,Err是開始減小的(假設一開始預期水平面A大於實際水平面B,也就是説Err是一個正值),那麼也就是説Err隨時間是一條斜率小於0的曲線,那麼在週期時間內,Err越大,微分的絕對值越大,那麼也就對Err的減小速度是起到抑制的作用的,直到最後斜率為0微分才會停止作用。

微分控制能反映輸入信號的變化趨勢,因此在輸入信號的量值在變化太大之前可為系統引入一個有效的早期修正信號以增加系統的阻尼程度,從而提高系統的穩定性,但一階微分的高通特性使得該控制器易於放大高頻噪聲

c, 積分控制理解

積分控制部分的作用主要是用來消除靜差。那麼積分是怎樣來消除靜差的呢?

比例控制只能儘量將Err調節到0,而微分的作用是將曲線的斜率控制到0則停止對其作用,但斜率為0的時候Err並不一定為0。

這個時候我們就需要積分來起作用了。我們知道曲線的積分相當於曲線與x軸圍出來的面積。如下圖,積分作用的目的是使紅色部分的面積和藍色部分的面積的和為0,那麼即使系統在比例控制和微分控制部分已經趨於穩定,只要Err不為0就會存在靜差,只要存在靜差那麼積分就會對系統產生影響,直到系統的Err值為0。那麼這樣我們的PID控制在理論上就可以達到一個非常精確的控制效果。

d, PID算法離散化

假設採樣時間間隔為T,則在k時刻:

偏差為e(k);

積分為e(k)+e(k-1)+e(k-2)+…+e(0);

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

從而公式離散化後如下:

比例係數:Kp,

積分系數:KpT/Ti,可以用Ki表示;

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

則公式可以寫成如下形式:

PID算法的離散形式就是這樣了,這就是我們平時説的位置式PID。

但是為什麼還要增推算量式?

一個累加符號使得微機的內存可能不夠用,一個字節八位最多存到255,第二點就是掉電之後產生的產生的影響非常大,之前存儲的狀態會全部丟失,所以要推算對狀態記錄要求不高的增量式。

接下來我們繼續推算增量式PID,根據上面公式我們可以求得:

e,pid雙環

f,pid前饋

  1. PID算法庫
    1. 核心計算函數(非常成熟的控制器,數學算法)

    1. 初始化代碼(將Kp,Ki,Kd三個參數與輸出最大值,積分限賦值賦值給PID句柄pid_v_1或者其他的句柄)

    1. 反饋環代碼

    1. 閉環代碼調用

④C++庫(建議)

簡介

代碼倉庫鏈接:https://github.com/TungChiahuiMCURepos/CAN\_PID\_CPP

類比着C語言的庫,

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

bsp_can.cpp是需要自己寫的開啓CAN通信的代碼文件(CubeMX沒自動生成的部分,需要手動調用)

can_receive.cpp裏是CANRX0接收中斷回調函數的實現,該回調函數里用了一些電機信息數據處理函數,然後還有CAN的4個發送函數。

pid.cpp是pid控制系統核心的數學算法代碼

pid_user.cpp裏是調用pid核心代碼並進行封裝為PID控制器的初始化代碼和一些閉環實現代碼。

C++大疆電機庫
  CLASS的結構與簡單介紹

下方圖片中是CAN_BUS類,其中嵌套了3個類。

  1. CAN_BUS::BSP類,該類中包含兩個方法:
    1. CAN_Start是開啓CAN通信的函數;
    2. Filter_Init是CAN通信濾波的函數。
  2. CAN_BUS::DJI_ENCODER類,該類裏包含三個方法(該類裏的所有函數都由CAN_RX0接收中斷回調函數調用):
    1. get_motor_measure是處理CAN通信接收到的大疆電機編碼器數據,並處理得到 電機各個信息 函數;
    2. get_moto_offset是處理CAN通信接收到的大疆電機編碼器數據,並處理得到 電機剛開始上電的角度初始值 函數;
    3. get_total_angle是處理CAN通信接收到的大疆電機編碼器數據,並處理得到 電機角度值 函數。(暫時沒被調用)
  3. CAN_BUS::CMD類,該類裏包含四個方法:
    1. CAN1_Front是給CAN1 4個電機發送電流的函數;(對應電調ID:1-4)
    2. CAN1_Behind是給CAN1 4個電機發送電流的函數;(對應電調ID:5-8)
    3. CAN2_Front是是給CAN2 4個電機發送電流的函數;(對應電調ID:1-4)
    4. CAN2_Behind是是給CAN2 4個電機發送電流的函數。(對應電調ID:5-8)

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

#######   CAN_Start 開啓CAN通信的函數

####### Filter_Init CAN通信濾波的函數

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

####### get_motor_measure 處理CAN通信接收到的大疆電機編碼器數據,並處理得到 電機各個信息 函數

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

####### get_total_angle 處理CAN通信接收到的大疆電機編碼器數據,並處理得到 電機角度值 函數。(暫時沒被調用

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

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

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

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

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

CAN_RX0接收中斷回調函數 (在can_receive.cpp中)

C++PID庫
CLASS的結構與簡單介紹

下方圖片中是PID_Controller類,其中嵌套了3個類和一個方法:

  1. PID_Controller類:
    1. All_Device_Init 將所有設備的PID控制器進行初始化
  2. PID_Controller::CORE核心類,該類中包含三個方法:
    1. PID_Init PID核心初始化函數;
    2. PID_Calc PID核心計算函數;
    3. PID_Clear PID清0函數。
  3. PID_Controller::CAN_MOTORcan電機類,該類中包含6個方法(因為上面3個方法和下面3個方法只是CAN通信不一樣,所以只講CAN1):
    1. CAN1_Velocity_Realize CAN1速度環實現函數;
    2. CAN1_Position_Realize CAN1位置環實現函數;
    3. CAN1_VP_Dual_Loop_Realize CAN1速度位置雙環實現函數;
    4. CAN2_Velocity_Realize CAN2速度環實現函數;
    5. CAN2_Position_Realize CAN2位置環實現函數;
    6. CAN2_VP_Dual_Loop_Realize CAN2速度位置雙環實現函數;
  4. PID_Controller::SENSORS傳感器類,該類中包含三個方法:
    1. Yaw_Realize 陀螺儀IMU的航向角PID實現函數;
    2. Pos_X_Realize 碼盤定位X座標實現函數;
    3. Pos_Y_Realize 碼盤定位Y座標實現函數;

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

#######   All_Device_Init 將所有設備的PID控制器進行初始化

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

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

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

#######   PID_Clear PID清0函數

PID_Controller::CAN_MOTOR類的方法 (在pid_user.cpp中)(這裏只講CAN1的3個閉環函數)
  1. 注意:
    1. 一般電流值變量定義為一個數組形式,比如fp32 motor_current_target[8];這樣就成功定義了8個電機要發送的電流值。

      速度和角度位置同理,fp32 motor_speed_target[8];和fp32 motor_position_target[8];。
    1. C++電機PID庫與C語言的電機PID 庫有些區別
      1. 因為電調ID的範圍是1-8,而數組範圍是0-7,
      2. 所以為了和數組序號一樣,這個地方注意一下區別:
      3. C語言庫中,i的值為電調ID的值。
      4. *C++庫中,*i值為電調ID值-1。

####### CAN1_Velocity_Realize CAN1速度環實現函數

####### CAN1_Position_Realize CAN1位置環實現函數

####### CAN1_VP_Dual_Loop_Realize CAN1速度位置雙環實現函數

PID_Controller::SENSORS傳感器類的方法(函數) (在pid_user.cpp中)

####### Yaw_Realize 陀螺儀IMU的航向角PID實現函數(等你們完善好 陀螺儀IMU的C++庫你們再補充)

####### Pos_X_Realize 碼盤定位X座標實現函數(等你們完善好 碼盤OPS-9的C++庫你們再補充)

####### Pos_Y_Realize 碼盤定位Y座標實現函數(等你們完善好 碼盤OPS-9的C++庫你們再補充)

如何調用?

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

可以選擇在while(1)死循環中加個delay(1)進行發送;

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

⑤實物連接,詳細的請看説明書

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

FreeRTOS

理論知識

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

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

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

STM32F1 FreeRTOS開發手冊_V1.1.pdf

STM32F4 FreeRTOS開發手冊_V1.1.pdf

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

系統配置
  1. 選擇系統時基源(Timebase Source)
    1. 原因:因為FreeRTOS會佔用systick,所以需要改時基源。
    2. 選擇規則:優先選擇功能少的定時器。(比如説F407ZGT6的tim6和tim7的功能比較少)

    1. 如何選擇?(如圖)

  1. 選擇接口(Interface)
    1. 原因:FreeRTOS遵循ARM的CMSIS標準。
    2. 選擇原則:優先選CMSIS v1,因為CMSIS v2還有些小問題沒解決。
    3. 如何選擇?

  2. 配置Include Parameters
    1. 功能:與hal_conf.h(用來開啓HAL庫的一些功能)一樣,用來開啓FreeRTOS的一些功能。
    2. Include Parameters的配置
      1. CubeMX配置(推薦)
      2. 需要什麼功能就Enabled對應的功能即可。(常用的就是vTaskDelayUntil)

      1. 手動編輯頭文件配置(不推薦)
創建任務
  1. CubeMX創建任務:
    1. 各參數介紹(詳看大疆手冊):

    1. 一般選擇什麼參數?
      1. Task Name(任務名):英文大寫(與Entry Function對應)
      2. Priority(優先級):一般選擇普通優先級即可(除非有特殊的邏輯)
      3. Stack Size(棧空間):128 Words即可
      4. Entry Function(入口函數名):英文小寫(與Task Name對應)
      5. Code Generation Option(代碼生成選項):無腦選擇As weak(使FreeRTOS線程任務的入口函數以弱函數的形式生成)
      6. Parameter(參數):一般NULL即可,如果要用一些特殊功能(比如信號量),要填一些句柄(比如信號量的句柄)
      7. Allocation(份額):無腦選Dynamic,讓FreeRTOS動態分配管理即可

  1. 注意事項:
    1. 任務創建太多會內存爆掉
延時
  1. 相對延時
    1. 函數:以下這倆函數作用相同,osDelay()和vTaskDelay()
    2. 時間:是從調用該函數才開始算,直到延時指定時間結束
    3. 調用方法:與HAL_Delay()方法一樣
    extern "C" //若在C++中运行需要加上该行
    void green_led_task(void const * argument)
    { 
        for(;;) //等同于while(true) 
        { 
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); 
        osDelay(500); 
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); 
        osDelay(500); 
        }
    }
    
  2. 絕對延時
    1. 函數:
      1. 獲取當前時間:osKernelSysTick()
      2. 絕對延時函數:osDelayUntil()
    2. 時間:從任務開始就開始算時間了,將整個任務運行週期看成一個整體,適用於按照一定頻率運行的任務

    1. 調用方法:


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

  1. 函數介紹:

  1. 如何調用:

隊列
  1. 原因:全局變量在多線程裏是不安全的,多個任務對該變量進行操作時,數據容易受損。
  2. 隊列:隊列是任務到任務、任務到中斷、中斷到任務數據交流的一種機制(消息傳遞)
  3. 具體內容:詳看正點原子視頻學習理論知識
  4. 調用
    1. CubeMX配置:(Queue Size選擇你要傳的數據的比特數(即二進制位數),Item Size選擇數據的類型)

    1. 調用(詳解請看正點原子):

信號量(隊列的特殊形式)
  1. 原因:同隊列
  2. 信號量:一種特殊的隊列,是一種解決同步問題的機制,可以實現對共享資源的有序訪問。
  3. 分類:二值信號量、計數型信號量(詳見正點原子)
  4. 同步問題:A做完一個事情,通知B,B才可以做,這叫同步問題。

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

  1. 隊列與信號量的對比

  1. 二值信號量介紹:

  1. CubeMX配置

要把創建的二值信號量的句柄傳入任務的parameter參數裏。

其實設置為NULL也可以。

  1. 調用(詳細API函數作用請看正點原子)
    1. 釋放信號量函數:xSemaphoreGive();
    2. 獲取信號量函數:

內存管理
簡介

棧區(stack):由編譯器自動分配和釋放,存放函數的參數值、局部變量的值等,其操作方式類似於數據結構中的棧。

堆區(heap):一般由程序員分配和釋放,若程序員不釋放,程序結束時可能由操作系統回收。分配方式類似於數據結構中的鏈表。

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

修改stm32的棧區和堆區大小

#######   stm32本身的堆區和棧區大小修改

如上圖,

stm32一般內存總大小為20Kb。

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

Stack Size就是棧大小,為1024byte = 1Kb。

剩餘的其他部分的內存,分配給剩餘的區,其中大部分內存都分給了Static靜態區。

使用CubeMX生成工程後,可以在啓動文件中看到咱們設置的堆區和棧區大小地址。(當然也可以在這裏進行修改,不過建議直接在CubeMX上進行修改,一般沒啥需求也不用改。)

####### FreeRTOS的堆區大小修改(此堆區非彼堆區,請看下方介紹)

  1. TOTAL_HEAP_SIZE:如果使用了FreeRTOS,可以在這裏修改FreeRTOS的堆區的大小。
  2. memory management scheme:可以修改動態分配內存的算法,一般都使用heap_4算法。
  3. FreeRTOS_HEAP這裏的堆區,非彼堆區,而是FreeRTOS從stm32的ZI區中開闢的內存(可以這麼理解,其實是FreeRTOS的內核在data,bss,heap,stack等中搶的內存),而並非從stm32的Heap堆區開闢的內存(在當你選擇heap_1,2,4,5算法時)。如果你選擇的是heap_3算法,那麼將會使用C庫的malloc()和free()函數進行開闢堆區內存,這個時候,FreeRTOS就是使用的為stm32的堆區(32的堆區比較小,所以不如heap_4算法)。但是咱們一般都使用heap_4算法進行內存管理,所以這裏的FreeRTOS_HEAP是從stm32_ZI區分配的。(也就是FreeRTOS_HEAP並非直接從heap區申請,而是非常靈活的在RAM中進行申請,可以超過STM32_HEAP的大小)
  4. 因為我們使用的是heap_4算法,所以我們不用對stm32的heap和stack進行修改,只需要對FreeRTOS_HEAP進行修改即可。(也就是對FreeRTOS可操控的stm32的ZI區內存進行分配)

內存管理API介紹

####### C語言庫的內存管理API(不建議)

####### 正點原子分塊式內存管理API

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

####### FreeRTOS內存管理API(建議)

  1. 介紹

  1. FreeRTOS內存管理算法(我們一般選擇heap_4)

heap_4的first-fit算法是從堆區內存起始地址塊開始找出第一個適合的內存大小。

  1. FreeRTOS內存管理API函數

可以通過看上面代碼,就可以得知,申請完內存再釋放掉內存後,空閒內存數會還原。

但是,可以看到,此時我們分配的是一個4字節的內存,但是他扣掉了16字節的內存,這是因為字節對齊的原因,FreeRTOS選擇使用用空間換速度的方式進行字節對齊。

FPU浮點數計算加速

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

檢查是否支持

STM32 系列CPU 內核DSP 指令FPU 類型arm_cos_f32() 性能適合的計算建議使用的函數
STM32H7Cortex-M7✅ 支持✅ 雙精度 FPU (DP-FPU)🚀 最快(硬件加速)高精度計算、機器人、濾波、導航arm_cos_f32()
STM32H5Cortex-M33✅ 支持✅ 雙精度 FPU (DP-FPU)🚀 最快(硬件加速)高精度計算、濾波、AI 計算arm_cos_f32()
STM32F7Cortex-M7✅ 支持✅ 單精度 FPU (SP-FPU)🔥 很快(硬件加速)機器人控制、導航、濾波arm_cos_f32()
STM32F4Cortex-M4✅ 支持✅ 單精度 FPU (SP-FPU)🔥 很快(硬件加速)機器人控制、數學運算arm_cos_f32()
STM32G4Cortex-M4✅ 支持✅ 單精度 FPU (SP-FPU)🔥 很快(硬件加速)電機控制、濾波arm_cos_f32()
STM32L4Cortex-M4✅ 支持✅ 單精度 FPU (SP-FPU)🔥 很快(硬件加速)低功耗計算arm_cos_f32()
STM32U5Cortex-M33✅ 支持✅ 單精度 FPU (SP-FPU)🔥 很快(硬件加速)低功耗 AI 計算arm_cos_f32()
STM32F3Cortex-M4✅ 支持❌ 無 FPU⚠️ 較慢(無 FPU,僅 DSP 加速)電機控制、信號處理arm_cos_q31()
STM32G0Cortex-M0+/M4❌ 部分支持❌ 無 FPU(部分 M4 版有 SP-FPU)⚠️ 較慢(軟件計算)基礎控制arm_cos_q31()
STM32F1Cortex-M3❌ 不支持❌ 無 FPU🚫 最慢(純軟件計算)不推薦做浮點計算arm_cos_q31()
STM32F0Cortex-M0❌ 不支持❌ 無 FPU🚫 最慢(純軟件計算)不推薦做浮點計算arm_cos_q31()
STM32L0Cortex-M0+❌ 不支持❌ 無 FPU🚫 最慢(純軟件計算)超低功耗應用arm_cos_q31()

開啓FPU

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

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

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

使用STM32CubeMX生成工程,會默認開啓FPU,如下圖。

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

下面這張圖可以從源碼看到開啓了FPU。

DSP加速

DSP加速是指CMSIS-DSP庫進行三角函數算法優化,使計算速度加快,但是誤差會變大一些,不過對於99%的應用場景誤差夠用了,大概是1e-6單位的誤差。

DSP庫只適用於ARM的Cortex-A和Cortex-M的內核,也就是適應手機,ARM單片機,樹莓派等等的設備。

對於STM32單片機來説,基本覆蓋了所有STM32系列,所以都可以用。

假設你沒有FPU,比如STM32F1系列的單片機,也可以通過DSP庫來加速三角函數運算,這個DSP庫的是通過查表+插值的數學運算方式進行優化的,計算也是比較快。

平台/庫函數CMSIS-DSPC++ std::cosC math.h
arm_cos_f32std::__math::coscosf() / 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庫的函數。

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

然後使能DSP庫

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

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

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

函數介紹

ARM內核的CPU支持 CMSIS-DSP 庫的三角函數,這比標準 math.hcmath 的函數更快。

  1. 普通的C/C++三角函數庫:

下面是普通的重載三角函數,當我們開啓了FPU後,只要傳入的是fp32的類型,其實速度也是相當快的,可以不使用DSP庫也可以。

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

傳入fp32的值。

    // 更新机器人的位置(假设机器人沿着x轴移动)
    this->x_position += this->vx * arm_cos_f32(this->yaw) * this->dt;  
    this->y_position += this->vy * arm_sin_f32(this->yaw) * this->dt;
    this->y_position = - y_position;
    this->yaw += this->vw * this->dt;
性能對比
✅ 對於有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)

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

cubemx配置:

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

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

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

不開:

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

STM32常見問題

STM32 使用ST-link下載問題

  1. 原因:在使用CubeMX 配置文件時,忘記設置SYS選項裏面的Debug選項

  1. 現象:下載完一次程序之後程序無法運行,且無法重新下載。
  2. stm32共有三種啓動模式:
    1. 用户閃存:正常的工作模式。stm32內置的Flash,一般我們使用JTAG或者SWD模式下載程序時就是下載到這個裏面,重啓之後也是從這裏啓動程序。
    2. SRAM:芯片內置的RAM區,就是內存,沒有程序儲存的能力,這個模式一般用於調試。
    3. 系統儲存器:系統儲存器是芯片內部的一塊特定的區域
    4. stm32廠商在這個區域內部設置了一段Bootloader。選用這種啓動模式,是為了能夠從串口下載程序,因為在商家提供的Bootloader中,提供了串口下載的固件,可以通過這個Bootloader將程序下載到系統的Flash中。
  3. 解決方法:
    1. 將BOOT0設置為1;BOOT1設置為0

    1. 連接電腦後按下復位鍵,使用keil5下載沒有問題的正常程序,發現程序正常下載。
    2. 將BOOT引腳改為原來的狀態,再次嘗試下載程序發現一切正常。
音乐页