🐱作者:一只大喵咪1201
🐱專欄:《理解ARM架構》
🔥格言:你只管努力,剩下的交給時間!
目錄
- 🍠操作寄存器實現UART
- 🍟UART原理
- 🍟編程
- 🍠段的概念
- 🍠IDE背后的命令
- 🍠總結
🍠操作寄存器實現UART
🍟UART原理
UART的全稱是Universal Asynchronous Receiver and Transmitter,即異步發送和接收。
串口在嵌入式中用途非常的廣泛,主要的用途有:
- 打印調試信息;
- 外接各種模塊:GPS、藍牙;
串口因為結構簡單、穩定可靠,廣受歡迎。
如上圖所示,串口通信只需要三根線,發送(TXD)、接收(RXD)、地線(GND)。
- 通信雙方的TXD與對方的RXD相連。
串口發送數據是以幀格式一幀一幀來發的,幀格式由1bit起始位,8或9bit數據位,1或1.5或2bit校驗位,1bit停止位組成。
- 通常情況下都使用8bit數據位,不適用校驗位,這樣的一幀數據有10個bit。
校驗位又叫奇偶校驗位,如果8個數據位加校驗位中比特為位1的個數是奇數,校驗位就是1,否則就是0。
由于現在電子技術的逐漸成熟,串口通信很少出錯,所以校驗位使用的不多。
如上圖所示是一幀數據傳送時的邏輯電平示意圖。
- 發送方將自己的TXD線從高電平拉到低電平,保持一段時間,接收方讀取到自己的RXD線由高到底以后就知道要接收數據了。
- 發送方按照自己發送的這個字節,從低位開始,改變TXD線的電平,每改變一次保持一段時間,如此反復8次完成一字節數據的發送。
- 接收方在自己RXD線上的電平保持期間的中間時刻,根據電平狀態記錄該比特位的值,最后組合成一字節數據。
- 發送方將一字節數據發送完畢后,將自己的TXD線拉高方便下次發送數據,接收方在接收到8bit數據以后,并且檢測到自己RXD線是高電平,就知道這一幀數據傳送完畢了。
上面描述數據發送過程中電平維持的時間,就是根據波特率來確定的,一般選波特率都會有9600,19200,115200等選項。
- 波特率:可以簡單理解為,串口通信過程中1秒鐘能發送的比特位個數。
- 波特率是通信雙方約定好的,一個按照這個速度發送數據,另一個按照這個速度接收數據。
邏輯電平:
如上圖所示是本喵使用的ARM開發板串口發出的電平信號,在xV至5V之間,就認為是邏輯1,在0V至yV之間就為邏輯0,這叫做TTL/CMOS邏輯電平。
如上圖所示是RS-232邏輯電平,在-12V至-3V之間,就認為是邏輯1,在+3V至+12V之間就為邏輯0,RS-232的電平比TTL/CMOS高,能傳輸更遠的距離,在工業上用得比較多。
可以看到,RS-232與TTL/CMOS相同邏輯電平對應的真實電壓正負是相反的。
如上圖所示,ARM芯片上的串口都是TTL電平的,通過板子上或者外接的電平轉換芯片,轉成RS232接口,連接到電腦的RS232串口上,實現兩者的數據傳輸。
如上圖所示,現在的電腦越來越少有RS232串口的接口,但USB是幾乎都有的。因此使用USB串口芯片將ARM芯片上的TTL電平轉換成USB串口協議,即可通過USB與電腦數據傳輸。
- 無論那種接口,板子上的芯片IO口輸出的都是TTL/CMOS電平,我們在寫程序時僅需要關心輸出的邏輯電平即可。
🍟編程
一款ARM芯片上會有多個USART串口,一般UART1用來輸出調試信息,這里本喵也使用USART1。
確定引腳:
如上圖,本喵使用的STMF103ZET6芯片上,USART1的USART1_RX、USART1_TX,接到了PA10、PA9。
將引腳配置為UART功能:
- 使能GPIOA/USART1模塊
如上圖是,RCC_APB2ENR
寄存器,GPIOA模塊、USART1模塊的使能都是在這一個寄存器里實現。
如上圖,從芯片手冊中查看Reset and clock control RCC
寄存器的基地址是0x40021000
,再根據RCC_APB2ENR
的偏移地址0x18
得到該寄存器的絕對地址是0x40021000 + 0x18
。
將該寄存器的bit2和bit14寫一,此時就使能了GPIOA和USART1模塊。
- 配置引腳功能
從上面的芯片原理圖可以知道,PA9、PA10有三種功能:GPIO、USART1、TIMER1,所以這里要將其配置為USAT1功能。
如上圖所示GPIOx_CRH
寄存器,該寄存器的絕對地址是0x40010800 + 0x04
,PA9配置為輸出,所以將MODE9
代表的bit4和bit5配置成01
,將CNF9
代表的bit6和bit7配置為10
。
PA10配置為輸入,將MODE10
代表的bit8和bit9配置為00
,再將CNF10
代表的bit10和bit11配置成01
。
由于這里僅使能了USART1,沒有使能定時器,所以PA9和PA10的默認復用功能就是USART1,使用默認值即可。
設置串口參數:
- 設置波特率
如上圖所示是波特率的計算公式,USARTDIV由整數部分、小數部分組成,USARTDIV = DIV_Mantissa + (DIV_Fraction / 16)
。fck是內部時鐘頻率,這里就使用默認值,是8MHZ。
如上圖USART_BRR
寄存器,DIV_Mantissa表示整數部分,占用該寄存器的bit4~bit15
,DIV_Fraction表示小數部分,占用該寄存器的bit0~bit3
。
以常用的波特率115200為例,來計算該寄存器的值:
設置波特率* 115200 = 8000000/16/USARTDIV* USARTDIV = 4.34* DIV_Mantissa = 4* DIV_Fraction / 16 = 0.34* DIV_Fraction = 16*0.34 = 5
所以給USART_BRR
寄存器的bit4~bit15
賦值4,bit0~bit3
賦值5,根據這兩個值再來倒推一下真實的波特率:
真實波特率:* DIV_Fraction / 16 = 5/16=0.3125* USARTDIV = DIV_Mantissa + DIV_Fraction / 16 = 4.3125* baudrate = 8000000/16/4.3125 = 115942
可以看到,雖然和115200有點差距,但是并不影響。
- 設置數據格式
如上圖所示USART1_CR1
寄存器,本喵將幀格式設置為1個起始位,8個數據位,無校驗位,1個停止位,所以將bit13
設置1,bit12
設置為0,bit10
設置為0,bit3
設置為1,bit2
設置為1。
但是此時并沒有設置幾個停止位,還需要設置另一個寄存器:
如上圖所示USART_CR2
寄存器,將bit12~bit13
設置為00,表示1個停止位。
根據狀態寄存器讀寫數據:
如上圖所示串口模塊結構圖,發送有一個發送數據寄存器和發送移位寄存器,接收有一個接收數據寄存器和接收移位寄存器。
發送數據時,CPU將數據寫入到發送數據寄存器,然后由發送移位寄存器一位一位將數據通過TXD線發送出去。
接收數據時,RXD線上的數據一位一位放入接收移位寄存器,該寄存器接收完畢后將整個字節數據放入到接收數據寄存器,CPU從接收數據寄存器中可以直接讀取數據。
- 狀態寄存器
如上圖所示USART_SR
狀態寄存器,TXE
表示發送數據寄存器是否為空,該位并不能說明數據已經發送完了,因為真正發送數據的是移位寄存器,只能說發送數據寄存器將數據給了移位寄存器,CPU可以再向數據寄存器中寫數據了。
TC
表示發送數據完成,即發送數據寄存器和移位寄存器中的數據都發送完畢了。RXNE
表示接收數據寄存器中有數據了,說明已經接收到了數據,CPU可以來讀取了。
- 數據寄存器
如上圖所示USART_DR
寄存器,寫、讀這個寄存器,就可以發送、讀取串口數據。
在配置完引腳和功能選擇以后,本喵在介紹USART_XXX
寄存器的時候并沒有說它的地址,因為無論是設置波特率的USART_BRR
,還是設置數據格式的USART_CR1
,再或者狀態寄存器USART_SR
,以及數據寄存器USART_DR
這些都是以USART
為基地址的。
如上圖所示USART1
的基地址是0x40013800
,上面本喵提到的這些寄存器都是在這個基地址的基礎上進行偏移,也就是說它們都屬于USART1
模塊中的寄存器。
typedef unsigned int uint32_t;
typedef struct
{volatile uint32_t SR; /*!< USART Status register, Address offset: 0x00 */volatile uint32_t DR; /*!< USART Data register, Address offset: 0x04 */volatile uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */volatile uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */volatile uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */volatile uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */volatile uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;
如上面代碼所示,用一個結構體來表示USART
模塊,里面的成員變量表示各個寄存器,讓它們在結構體中的偏移量和寄存器相對于USART
模塊的偏移量相對應,此時就可以通過訪問這個結構體訪問到各個寄存器。
如上圖所示是整個串口的初始化代碼,其中配置波特率等參數使用的是結構體訪問的寄存器,串口結構體是一個局部變量。
如上圖所示,定義發送一個字符和接收一個字符的函數,通過判斷狀態寄存器的值,進而讀寫DR
寄存器,也是通過結構體訪問的寄存器,結構體是一個局部變量。
如上圖,此時將程序燒錄到開發板以后,會通過串口發送Hello
字符串,在PC端發送一個字符,板子接收到以后返回該字符及下一個字符,此時我們的串口是配置好了。
問題:為什么每個函數中都得創建一個uart1結構體局部變量,而不是創建全局變量供這些函數使用呢?
🍠段的概念
如上圖所示,增加三個函數,用來打印字符串及變量的地址。
如上圖所示,創建四個全局變量,g_ConstChar
被const修飾,然后在mymain
中分別打印四個變量的地址及它們的值。
將程序編譯后燒錄到開發板中,通過串口工具來觀察輸出的內容。
如上圖所示,來看這四個變量的地址,只有g_ConstChar
這個被const修飾的變量地址是位于Flash中的,其他幾個變量都是位于RAM中。
如上圖,keil中只能了Flash和RAM的起始地址,根據這兩個參數很容易判斷出這四個變量所處的位置。
如上圖,再來看輸出的這四個變量的值,可以看到,只有const修飾的g_ConstChar
變量輸出了B
,其他幾個變量都沒有輸出對應的則,而是奇怪的東西。
- 其他變量輸出的奇怪值表明,這幾個變量地址處的值是亂碼。
g_ConstChar
變量位于Flash,也就是ROM,ROM是只讀的,不能寫,而其他三個變量位于RAM,RAM是可讀可寫的。
在編譯的時候,編譯器進行了判斷處理,g_ConstChar
是只讀的,不會寫,所以把它放在Flash就可以。
- Flash上存放這種只讀數據的區域叫做只讀數據段。
其他三個變量會進行讀和寫的操作,所以編譯器給了它們一個鏈接地址,這個地址對應在RAM上,方便CPU進行讀寫。
- RAM上存放這種可讀可寫全局變量的區域叫做可讀可寫數據段。
無論有沒有被const修飾的變量,它們都有初始值A
或者B
,這個兩個數值是不會變的,只是用來使用的,所以編譯器將這兩個值放在這兩個變量位于Flash上的地址處(加載地址)。
- 有幾個有初始值的全局變量,Flash中就會保存幾個初始值。
- Flash以及內存中并沒有變量名,只會在變量的地址處直接存放數值。
像char g_A = 0
這種初始值為0的全局變量,以及char g_B
這種沒有初始值的全局變量,Flash上就沒有必要存放它們的初始值。
假設初始值為0的變量有一萬個,Flash中難道要存放1萬個0嗎?肯定不會的,這樣浪費內存不說,還沒有任何意義。對于沒有初始值的全局變量Flash中更不會存放它的初始值了。
所以編譯器在編譯的時候,直接給這種初始值為0或者沒有初始值的全局變量分配一個鏈接地址,位于RAM中,CPU直接去鏈接地址讀寫就可以了。
- 這種存放初始值為0或者沒有初始值所在的RAM區域被叫做BSS段或者ZI段。
我們寫的代碼經過編譯鏈接以后,會生成一個二進制可執行文件,里面全部都是機器碼,這部分代碼并不會改變,所以也存放到Flash上。
- 存放代碼的Flash區域被叫做代碼段。
至于棧以及堆本喵在前面的文章中就詳細講解過,這里就不再說了,有興趣的小伙伴可以移步單片機中的C語言。
所以,程序分為這幾個段:
- 代碼段(RO-CODE):就是程序本身,不會被修改
- 可讀可寫的數據段(RW-DATA):有初始值的全局變量、靜態變量,需要從ROM上復制到內存
- 只讀的數據段(RO-DATA):可以放在ROM上,不需要復制到內存
- BSS段或ZI段:
- 初始值為0的全局變量或靜態變量,沒必要放在ROM上,使用之前清零就可以
- 未初始化的全局變量或靜態變量,沒必要放在ROM上,使用之前清零就可以
- 局部變量:保存在棧中,運行時生成
- 堆:一塊空閑空間,使用malloc函數來管理它,malloc函數可以自己寫
🍠IDE背后的命令
IDE指集成開發環境(Integrated Development Environment)。我們開發STM32F103等單片機程序時使用是keil5就是一種IDE。
使用IDE,很容易操作,點點鼠標就可完成,添加文件,指定文件路徑(頭文件路徑、庫文件路徑),指定鏈接庫,編譯、鏈接,下載、調試等功能。
其實在我們點下某一個按鈕以后,IDE的背后會執行一系列指令:
如上圖,在keil5的Output選擇中勾選Create Batch File
,然后重新全部編譯。
如上圖,此時在當前工程的Objects
目錄下會多出上面紅色框中的四個文件。
如上圖所示分別是這幾個文件中的內容,都是一系列的命令行指令,用來編譯和鏈接文件的指令,具體怎么用不用管,只需要知道有這些東西。
start._ia
中的命令行就是在讓start.s
匯編文件編譯成start.o
目標文件。main._i
中的命令行就是在讓main,c
源文件編譯成main.o
目標文件。uart._i
中的命令行就是在讓uart.c
源文件編譯成uart.o
目標文件。led.linp
中的命令行就是把這幾個.o
目標文件鏈接在一起形成一個二進制可執行文件led.axf
,我們燒錄的就是這個文件。
當我們點下IDE上的編譯選項時,IDE會自動執行上面四個文件中的內容,最后生成我們需要的東西。
🍠總結
雖然配置串口已經是一個老生常談的問題了,但是相信大家很少直接使用寄存器地址來配置吧,這個過程中可以加深對ARM架構的理解。
串口配好后通過打印數據過程中出現的問題介紹了段的概念,編譯器不同類型的變量放在內存中不同的位置。
要意識到,編譯一個工程的背后沒有那么簡單。