參考
http://t.csdnimg.cn/P9H6x
一、sys文件夾介紹
在上述介紹的 sys 文件夾中,涉及了一些與系統控制、中斷管理、低功耗模式、棧頂地址設置、系統時鐘初始化以及緩存配置等相關的函數。以下是對每個功能的簡要分析:
1.中斷類函數:
sys_nvic_set_vector_table(): 設置中斷向量表地址,中斷向量表是一個包含中斷服務程序入口地址的表。
sys_intx_enable(): 開啟所有中斷,通過設置相關標志位允許中斷發生。
sys_intx_disable(): 關閉所有中斷,通過清除相關標志位禁止中斷發生,但不包括fault和NMI中斷。
2.低功耗類函數:
sys_wfi_set(): 執行 WFI 指令,該指令使處理器進入等待低功耗狀態,等待外部中斷喚醒。
sys_standby(): 進入待機模式,是一種低功耗模式,主要特點是CPU停止運行。
sys_soft_reset(): 系統軟復位,通過配置相關寄存器實現對整個系統的復位。
3.設置棧頂地址函數:
sys_msr_msp(): 設置棧頂地址,通過修改主堆棧指針(Main Stack Pointer,MSP)的值來設置棧頂地址。
4.系統時鐘初始化函數:
sys_stm32_clock_init(): 設置系統時鐘,這個函數通常在啟動代碼中調用,用于配置時鐘源、PLL、分頻等,確保系統時鐘的正確初始化。
5.Cache 配置函數(F7/H7):
sys_cache_enable(): 使能 I-Cache 和 D-Cache,開啟 D-Cache 強制透寫。在某些處理器架構(如F7和H7)中,Cache的配置和使能可以提高程序的執行效率。
這些函數涵蓋了系統的基本控制、中斷管理、低功耗模式的配置、棧頂地址的設置、系統時鐘的初始化以及緩存的配置等方面。在嵌入式系統中,這些功能對于系統的運行、時鐘配置和低功耗管理非常重要。
二、deley文件夾介紹
2.1、deley文件夾函數簡介
在 delay 文件夾中,涉及了一些延時函數,主要用于在嵌入式系統中實現微秒和毫秒級別的延時。以下是對每個功能的簡要分析:
1.不使用OS:
在嵌入式系統中,有時候不使用操作系統(OS)是常見的情況,因為一些簡單的應用可能不需要復雜的操作系統支持。
對于這種情況,提供了 delay_init() 函數,用于初始化系統滴答定時器。滴答定時器是一種在嵌入式系統中常用的計時器,用于生成精確定時的延時。
2.delay_us():
delay_us() 函數使用系統滴答定時器實現微秒級別的延時。通過讀取滴答定時器的計數值,并進行一定的計算,實現對微秒級別的精確延時。
3.delay_ms():
delay_ms() 函數則是使用微秒級別的延時函數 delay_us() 實現毫秒級別的延時。通過在 delay_us() 的基礎上進行倍乘,實現對毫秒級別的延時。
這些函數主要用于在嵌入式系統中提供簡單的延時功能。在沒有操作系統支持的情況下,使用滴答定時器是一種常見的方式來實現延時。這對于需要進行時間控制的嵌入式應用,例如控制器的初始化、通信時序等,都是有用的。在使用這些函數時,需要注意系統的時鐘配置,以確保延時的準確性。
2.2、SysTick工作原理
SysTick 是 ARM Cortex-M 系列微控制器內置的一個系統定時器。下面對 SysTick 的工作原理進行分析:
1.遞減計數器:
SysTick 包含一個 24 位的遞減計數器。這意味著計數器的值從加載的初始值開始遞減,直到達到零。
遞減計數器的寬度決定了 SysTick 可以提供的最大定時周期。
2.時鐘源:
SysTick 的時鐘源可以是 HCLK(對于 F1/F4/F7 等系列)或 SYS_d1cpre_ck(對于 H7 等系列),具體取決于芯片型號和配置。
HCLK 是核心時鐘,而 SYS_d1cpre_ck 是 H7 系列中的一個時鐘源。
3.分頻器:
時鐘源可以通過一個可編程分頻器進行分頻,以確定 SysTick 計數器的工作頻率。
分頻可以用于調整 SysTick 的計數速率,使其適應不同的應用場景。
4.重裝載值(LOAD):
SysTick 包含一個可以設置的重裝載值 LOAD。當遞減計數器達到零時,它可以自動重新加載為 LOAD 的值。
這就意味著 SysTick 在每次計數完成后可以自動重新開始新一輪的遞減計數。
5.計數完成標志(COUNTFLAG):
當遞減計數器的值達到零時,會產生一個計數完成標志 COUNTFLAG。
軟件可以檢查這個標志來判斷是否達到了設定的定時周期。
6.遞減計數和重裝載機制:
SysTick 的基本工作機制是遞減計數,每次計數完成都會檢查 COUNTFLAG。
當 COUNTFLAG 為 1 時,表示計數完成,可以執行相應的操作。然后遞減計數器被重新加載為 LOAD。
總體來說,SysTick 是一種簡單而強大的定時器,適用于許多嵌入式系統中的定時和延時操作。通過調整時鐘源、分頻因子和重裝載值,可以實現不同精度和范圍的定時需求。SysTick 經常被用于創建精確的延時函數、定時任務等。
2.3、SysTick寄存器介紹
SysTick 寄存器包括 SysTick 控制及狀態寄存器(CTRL)、SysTick 重裝載數值寄存器(LOAD)和 SysTick 當前數值寄存器(VAL)。以下是對每個寄存器的簡要分析:
1.SysTick 控制及狀態寄存器 (CTRL):
CTRL 寄存器是 SysTick 的控制和狀態寄存器,用于配置 SysTick 的工作方式和獲取狀態信息。
CTRL 寄存器的位字段包括:
Bit[0] - ENABLE:用于啟用(1)或禁用(0)SysTick 計數器。
Bit[1] - TICKINT:用于啟用(1)或禁用(0)SysTick 定時中斷。
Bit[2] - CLKSOURCE:用于選擇時鐘源,0 表示外部時鐘源(通常是處理器時鐘),1 表示處理器時鐘(通常是時鐘源除以 8)。
Bit[16] - COUNTFLAG:表示 SysTick 計數器是否歸零,當計數器值變為 0 時,該位會置 1。
2.SysTick 重裝載數值寄存器 (LOAD):
LOAD 寄存器用于設置 SysTick 的重裝載值,即計數器遞減到 0 后自動重新加載的值。
當 CTRL 寄存器的 ENABLE 位被置 1 時,LOAD 寄存器的值會被加載到 SysTick 計數器,從而決定了 SysTick 的定時周期。
3.SysTick 當前數值寄存器 (VAL):
VAL 寄存器用于讀取當前的 SysTick 計數器的值。
當 CTRL 寄存器的 ENABLE 位被置 1 時,VAL 寄存器的值表示當前剩余的計數器值,可以通過讀取該寄存器來獲取 SysTick 的當前計數狀態。
這三個寄存器協同工作,實現了 SysTick 定時器的基本功能。CTRL 寄存器用于配置 SysTick 的工作方式,LOAD 寄存器用于設置定時周期,而 VAL 寄存器用于讀取當前計數器的值。通過這些寄存器的設置和讀取,可以對 SysTick 進行靈活的配置和使用。
2.4、delay_init()函數
用于初始化延時函數的 delay_init() 函數。以下是對這個函數的簡要分析:
void delay_init(uint16_t sysclk)
{SysTick->CTRL = 0;HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);g_fac_us = sysclk / 8;
}
1.SysTick->CTRL = 0;:
將 SysTick 控制寄存器 (CTRL) 的值設為 0,即清零。這是為了確保在初始化之前 SysTick 定時器被禁用。
2.HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);:
通過調用 HAL_SYSTICK_CLKSourceConfig 函數配置 SysTick 的時鐘源。在這里,時鐘源被配置為 HCLK(處理器時鐘)除以 8。
這里選擇 HCLK 除以 8 作為 SysTick 的時鐘源,這是為了降低 SysTick 的時鐘頻率,從而延長計數器的溢出周期,提高延時的范圍。
3.g_fac_us = sysclk / 8;:
計算一個全局變量 g_fac_us 的值,該變量用于后續的延時計算。
sysclk / 8 表示實際的 SysTick 時鐘頻率,這個值將用于后續的微秒級延時計算。
該函數的主要目的是對 SysTick 定時器進行初始化配置,包括清零控制寄存器、配置時鐘源為 HCLK 除以 8,以及計算用于延時的全局變量的值。這樣,通過調用 delay_us() 和 delay_ms() 函數,可以實現微秒級和毫秒級的延時。
2.5、delay_us()函數
用于實現微秒級延時的 delay_us() 函數。以下是對這個函數的簡要分析:
void delay_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))); /* 等待計時完成 */SysTick->CTRL &= ~(1 << 0); /* 關閉 SysTick 定時器 */SysTick->VAL = 0X00; /* 清空計數器 */
}
1.SysTick->LOAD = nus * g_fac_us;:
將 SysTick->LOAD 寄存器的值設置為延時的微秒數乘以全局變量 g_fac_us。這個值決定了 SysTick 的計時周期,即延時的實際時間。
2.SysTick->VAL = 0x00;:
清空 SysTick 的當前計數器值,確保計數器從零開始。
3.SysTick->CTRL |= 1 << 0;:
設置 SysTick->CTRL 寄存器的 ENABLE 位(位 0)為 1,開始倒計時。
4.do…while 循環:
在循環中,使用 temp = SysTick->CTRL 不斷讀取 SysTick->CTRL 寄存器的值,判斷條件是 temp & 0x01 為 1(即 ENABLE 位為 1),同時 temp & (1 << 16) 為 0(即倒計時未結束)。
循環等待計時完成,當計時完成時,temp & 0x01 為 0,循環退出。
5.SysTick->CTRL &= ~(1 << 0);:
在倒計時完成后,將 SysTick->CTRL 寄存器的 ENABLE 位清零,關閉 SysTick 定時器。
6.SysTick->VAL = 0X00;:
最后,再次清空 SysTick 的當前計數器值。
該函數的主要目的是通過 SysTick 定時器實現微秒級的延時。在 SysTick->LOAD 寄存器中設置適當的值,然后通過控制 SysTick->CTRL 寄存器的 ENABLE 位實現定時器的啟動和關閉。循環等待計時完成以保證準確的延時。
2.6、delay_ms()函數
用于實現毫秒級延時的 delay_ms() 函數。以下是對這個函數的簡要分析:
void delay_ms(uint16_t nms)
{uint32_t repeat = nms / 1000; /* 計算整秒數,每秒使用 delay_us 實現 1000 毫秒延時 */uint32_t remain = nms % 1000; /* 計算余數,即剩余的毫秒數 */while (repeat){delay_us(1000 * 1000); /* 利用 delay_us 函數實現 1000 毫秒延時 */repeat--;}if (remain){delay_us(remain * 1000); /* 利用 delay_us 函數,把尾數延時(remain 毫秒)給做了 */}
}
1.repeat = nms / 1000;:
計算整秒數,即通過將毫秒數除以 1000 得到整數部分。這里采用整數除法,以得到需要通過 delay_us 實現的整秒數。
2.remain = nms % 1000;:
計算余數,即剩余的毫秒數。這是因為可能存在不足 1000 毫秒的部分,需要通過 delay_us 函數進行延時。
3.while (repeat):
使用循環,通過 delay_us(1000 * 1000) 實現整秒數的延時。這是因為 delay_us 函數實現的是微秒級延時,因此需要乘以 1000 得到毫秒級延時。
4.if (remain):
判斷是否存在不足 1000 毫秒的剩余部分,如果存在,則通過 delay_us(remain * 1000) 實現剩余毫秒數的延時。
該函數的主要目的是通過調用 delay_us() 函數實現毫秒級的延時。通過循環整秒數的部分和處理剩余毫秒數的部分,實現了毫秒級的延時功能。這樣,在整數秒數的延時和剩余毫秒數的延時之間,可以實現相對較為準確的毫秒級延時。
三、USART 文件夾介紹
在嵌入式系統中,usart 文件夾通常包含與 USART(通用同步異步收發器)通信相關的函數和文件。以下是可能在 usart 文件夾中找到的一些常見文件和功能的簡要介紹:
1.USART 配置文件(例如 usart_config.h):
這個文件可能包含 USART 相關的宏定義、常量和配置參數。通常,你可以在這里設置 USART 的波特率、數據位、停止位等參數。
2.USART 初始化函數(例如 usart_init.c/.h):
提供 USART 的初始化函數,用于配置 USART 模塊并啟用相應的硬件資源。這通常包括對寄存器的設置以及時鐘和引腳的配置。
3.USART 發送函數(例如 usart_send.c/.h):
包含用于向 USART 發送數據的函數。這些函數通常涉及到數據緩沖區的管理、數據格式的處理以及通過 USART 寄存器的寫操作來發送數據。
4.USART 接收函數(例如 usart_receive.c/.h):
包含用于從 USART 接收數據的函數。這些函數通常涉及到數據緩沖區的管理、接收中斷的處理以及通過 USART 寄存器的讀操作來接收數據。
5.USART 中斷處理函數(例如 usart_interrupt.c/.h):
提供用于處理 USART 中斷的函數。這些函數通常包括處理接收和發送中斷,并根據需要執行相應的操作。
6.USART 外設驅動(例如 usart_driver.c/.h):
可能包含更高層次的函數,用于處理 USART 的一些常見操作,如發送字符串、接收字符串等。
7.USART 示例應用程序(例如 usart_example.c):
提供一些示例代碼,演示如何在具體的應用中使用 USART。這可以作為用戶的參考,幫助理解和使用 USART 相關功能。
這些文件通常組成了一個完整的 USART 驅動庫,為用戶提供了方便的接口,使其能夠輕松地在應用程序中集成和使用 USART 通信。具體的文件結構和功能可能因不同的開發環境和硬件平臺而有所不同。
3.1、printf函數輸出流程
printf函數輸出流程如下:
1.用戶調用 printf() 函數:
在程序中,用戶調用 printf() 函數來格式化輸出字符串到標準輸出流。
2.C 標準庫(printf 部分):
printf() 函數屬于 C 標準庫(stdio.h 頭文件)。該頭文件中包含了一系列用于輸入輸出操作的標準函數,其中就包括 printf。
3.printf 函數由編譯器提供的 stdio.h 解析:
在編譯階段,編譯器會將 printf 函數解析為與標準輸出流相關的一系列函數調用。
4.標準庫的輸出函數(如 fputc()):
在 printf 的內部,它會調用標準庫的輸出函數,比如 fputc。
fputc 是一個通用的字符輸出函數,其功能是將一個字符寫入指定的輸出流(例如標準輸出流)。
5.用戶根據最終輸出的硬件重新定義輸出函數:printf 重定向:
最終的輸出是通過底層的輸出函數實現的,比如 fputc。用戶可以根據最終輸出的硬件重新定義這些函數,這個過程被稱為 “printf 重定向”。
通過重新定義輸出函數,用戶可以將輸出重定向到不同的設備,比如串口、文件等。這對于嵌入式系統和特殊硬件平臺很有用。
總體而言,printf 函數是一個高層次的接口,它在底層調用標準庫的輸出函數,而用戶可以通過重定向這些輸出函數來適應不同的硬件或輸出設備。這種機制允許程序員將標準的輸入輸出函數與特定硬件進行適配,提高了代碼的可移植性。
3.2、printf的使用
printf 是 C 語言標準庫中用于格式化輸出的函數。下面是 printf 的基本用法和如何輸出特殊字符的方法:
1.輸出字符串:
printf(“字符串\r\n”);:直接輸出指定的字符串,\r\n 表示回車和換行,通常用于換行輸出。
2.使用輸出控制符和輸出參數:
printf(“輸出控制符”, 輸出參數);:通過輸出控制符指定輸出參數的格式,例如 %d 表示輸出整數。
printf(“輸出控制符1 輸出控制符2 …”, 輸出參數1, 輸出參數2, …);:可以同時輸出多個參數,并通過多個輸出控制符指定它們的格式。
3.混合輸出非輸出控制符和輸出控制符:
printf(“非輸出控制符 輸出控制符 非輸出控制符”, 輸出參數);:可以混合輸出非輸出控制符和輸出控制符,只有輸出控制符會影響參數的格式。
4.輸出特殊字符 %、\ 和雙引號:
若要在 printf 中輸出特殊字符 %、\ 或雙引號,需要使用轉義字符 \:
%:printf(“輸出 %% 字符”);
\:printf(“輸出 \ 字符”);
雙引號:printf(“輸出 " 字符”);
#include <stdio.h>int main() {// 示例1:輸出字符串printf("Hello, World!\r\n");// 示例2:使用輸出控制符和輸出參數int num = 42;printf("The number is %d\r\n", num);// 示例3:混合輸出非輸出控制符和輸出控制符char ch = 'A';printf("Character: %c, ASCII code: %d\r\n", ch, ch);// 示例4:輸出特殊字符 %、\ 和雙引號printf("Output %% character: %%\r\n");printf("Output \\ character: \\\r\n");printf("Output \" character: \"\r\n");return 0;
}
3.3、printf函數支持
1.避免使用半主機模式:
半主機模式是指將 printf 函數的輸出重定向到開發環境的終端窗口,通常通過串口或仿真器實現。在嵌入式系統中,由于沒有終端窗口,半主機模式可能導致編譯后的代碼過大,因此需要避免使用半主機模式。
兩種常見的方法:
微庫法:
在編譯時使用 -specs=nano.specs 選項,該選項會使用微庫(nano.specs)來減小代碼大小。
示例編譯命令:arm-none-eabi-gcc -o output.elf input.c -specs=nano.specs
代碼法:
在代碼中添加如下語句,關閉緩沖區并禁用半主機模式:
setvbuf(stdout, NULL, _IONBF, 0);
2. 實現 fputc 函數:
在嵌入式系統中,需要用戶自己實現 fputc 函數,以便 printf 函數正確輸出字符。下面是一個簡單的例子:
#include <stdio.h>// 實現 fputc 函數
int fputc(int c, FILE *stream) {// 在這里實現將字符 c 輸出到相應的硬件設備,例如串口// 例如,如果使用串口輸出,可以使用類似于 UART_SendChar(c) 的函數// 返回輸出的字符或者 EOF(表示錯誤)// 這里僅為示例,沒有具體的輸出操作return c;
}int main() {// 避免使用半主機模式的兩種方法// 方法1:微庫法// arm-none-eabi-gcc -o output.elf input.c -specs=nano.specs// 方法2:代碼法setvbuf(stdout, NULL, _IONBF, 0);// 使用 printf 函數printf("Hello, World!\n");return 0;
}
在上述代碼中,fputc 函數用于實現字符的輸出操作,具體的輸出動作應該根據實際的硬件設備進行實現。main 函數中使用 printf 輸出字符串,而通過設置緩沖區為無緩沖 (_IONBF),可以避免緩沖區的影響。
半主機模式簡介
半主機模式是一種在嵌入式系統中用于將輸入/輸出(I/O)請求傳送到運行調試器的主機的機制。這種模式通常通過調試工具、仿真器或者開發板連接到主機來實現。半主機模式的目的是方便在嵌入式系統中進行調試和開發。
在半主機模式中,開發者可以在嵌入式系統中使用標準的輸入輸出函數(如printf和scanf)進行調試。這些函數的輸出被傳送到主機,而主機上的終端模擬器則模擬了嵌入式系統的輸入和輸出。這使得在嵌入式系統中進行調試時能夠使用類似于在主機上調試的方式。
然而,半主機模式在一些嵌入式系統開發中可能存在一些問題和限制,例如:
1.代碼大小: 啟用半主機模式可能導致生成的代碼變得較大,因為需要包含一些用于與主機通信的額外代碼。
2.實時性: 半主機模式可能引入一些額外的延遲,特別是在需要頻繁進行輸入輸出的應用中。
3.硬件依賴性: 半主機模式通常依賴于特定的仿真器或調試工具,因此在更換硬件平臺時可能需要重新調整。
正如你所提到的,一般情況下,在嵌入式系統的實際應用中,不使用半主機模式是一個較為常見的選擇。在實際產品中,通常會使用其他手段,如串口通信或者其他特定的調試接口來進行調試和信息輸出,以避免半主機模式可能引入的一些問題。
方法一:微庫法
方法二:代碼法
1.#pragma import(__use_no_semihosting):
這個 #pragma 指令用于告訴編譯器不要使用半主機函數。半主機函數通常用于與調試器進行交互,包括輸入輸出操作。通過使用這個 #pragma,你告訴編譯器在編譯時不要鏈接半主機函數,從而避免在嵌入式系統中引入不必要的代碼。
2.定義 __FILE 結構體:
__FILE 是一個預定義的宏,用于在編譯時傳遞當前源文件的名稱。在某些情況下,特別是使用 HAL 庫時,可能會遇到對 __FILE 結構體的要求。通過定義 __FILE 結構體,你確保了在編譯時正確傳遞源文件的信息。
3.定義 FILE __stdout:
在使用 printf 函數等輸出函數時,通常需要定義一個輸出流,這個輸出流可以是標準輸出流。通過定義 FILE __stdout,你為 printf 函數提供了一個輸出流,從而使得輸出函數知道要輸出到哪里。
4.實現 _ttywrch、_sys_exit 和 _sys_command_string:
這三個函數通常是與半主機相關的函數,用于處理底層的輸入輸出和系統命令。在沒有使用半主機模式的情況下,你可能需要提供這些函數的實現,以滿足編譯器的鏈接要求。具體的實現可能根據你的嵌入式系統和開發環境而有所不同。
在使用 AC5 和 AC6 時,確保正確定義和實現這些關鍵的結構體和函數,以滿足 HAL 庫和編譯器的要求,同時避免引入不必要的半主機函數。這樣可以確保在嵌入式系統中編寫和調試代碼時,不會受到半主機模式可能帶來的一些問題。
實現fputc函數
實現 fputc 函數,該函數用于將單個字符發送到 USART(串口)。下面是代碼的簡要分析:
#define USART_UX USART1/* 重定義 fputc 函數, printf 函數最終會通過調用 fputc 輸出字符串到串口 */
int fputc(int ch, FILE *f) {while ((USART_UX->SR & 0X40) == 0); /* 等待上一個字符發送完成 */USART_UX->DR = (uint8_t)ch; /* 將要發送的字符 ch 寫入到 DR 寄存器 */return ch;
}
1.#define USART_UX USART1:
定義了一個宏 USART_UX,表示使用的 USART 模塊,這里定義為 USART1。你可以根據實際硬件連接選擇合適的 USART 模塊。
*2.int fputc(int ch, FILE f) {…}:
實現了 fputc 函數,該函數會被 printf 調用來將字符輸出到串口。
int ch 是要輸出的字符。
FILE *f 是文件指針,由于 fputc 函數是標準庫函數,所以需要有這個參數,但實際上在這個實現中沒有使用。
3.while ((USART_UX->SR & 0X40) == 0);:
這是一個忙等待循環,用于等待上一個字符發送完成。USART_UX->SR 表示 USART 狀態寄存器,0X40 表示 USART 的發送緩沖區空標志位(TXE)。
4.USART_UX->DR = (uint8_t)ch;:
將要發送的字符 ch 寫入 USART 數據寄存器(DR)。USART 數據寄存器用于存儲要發送或接收的數據。
5.return ch;:
返回發送的字符。在 printf 中,該返回值并不會被使用,因此可以簡單地返回發送的字符。
這段代碼實際上是一個典型的 USART 發送字符的實現,用于在嵌入式系統中通過串口輸出。確保 USART 的初始化和配置在此代碼之前完成,以便正確地將字符發送到 USART。