ARM Cortex-M嵌入式C基礎編程(上)
ARM Cortex-M Embedded C Fundamentals/Tutorial -Aviral Mittal
此技術是關於從編寫簡單的嵌入式C代碼到執行的過程。
這項技術試圖不使用行話,並針對任何人誰有興趣知道如何開始編寫一個嵌入式C程序或ARM Cortex-M系列處理器的彙編語言程序。
世界上充斥着信息,然而這些信息的存在方式使得所有的信息對於一個來自其他背景的人來說都是垃圾。例如,如果您來自硬件背景,則軟件信息看起來很神祕。例如,在軟件世界中,他們使用“圖像”一詞來表示從相應的C程序獲得的二進制文件。說真的,如果你不知道“圖像文件”是什麼,那麼jpeg或gif就會出現在你的想象中。我不知道爲什麼它叫“形象”。這項技術是一種試圖保持它非常簡單,而不使用’什麼軟件假設每個人都知道出生’之類的行話。
這都是關於ARM架構的,所以它關注的是嵌入式c,也就是爲基於ARM的微控制器編寫的c程序。
C代碼是人類可讀的,處理器無法執行它。它必須轉換成0和1,因爲這是處理器可以執行的。
因此,在執行C代碼之前,必須將此C代碼轉換爲二進制格式的處理器指令,然後將此二進制格式的指令放入內存中,然後處理器將開始從該內存中獲取並執行指令(請參閱軟件術語“圖像文件”的使用)
但這聽起來很簡單。在幕後發生了很多事情,很多事情都要考慮。
The very basic Embedded C program:
本節描述了一個非常簡單的嵌入式C程序,並給出了一些解釋。
讓我們考慮一個非常簡單的嵌入式C程序:
typedef unsigned long uint32_t;
int main ()
{
int ii;
for(ii=0;ii<305419896;ii++) {
*((uint32_t *)0x40E00018) = 0x87654321;
asm(“NOP”);
}
while(1){}
}
如果您之前不知道嵌入式C,那麼上面的代碼已經很神祕了,但是讓我逐行解釋一下:
第一行是數據類型的定義,程序將使用。定義了一種新的數據類型,稱爲“uint32”。這被定義爲“長”long’是C語言中預定義的數據類型,表示32位寬的二進制數。
第二行是“main”函數調用。對每一個C程序都是必不可少的。這是用戶寫他們想做的事情的地方。
“int ii”是一個不言而喻的整數聲明,它將在“for循環”中使用。在C語言中,需要顯式聲明所有變量,然後才能使用它們。
for(ii……)同樣是不言而喻的,一個for循環被啓動,它將執行305419896次。
然後你就有了這行命令:
*((uint32_t *)0x40E00018) = 0x87654321;
看起來像是第二次世界大戰的加密密碼,用來指示某人發射魚雷!。
讓我解釋一下:
C語言使用所謂的“指針”。指針是指向內存位置的地址。“星號”或“星號”用於定義/聲明指針。
“0x”:這意味着“0x”後面的值是十六進制格式。
現在,上面的代碼行簡單地表示,用戶希望將0x8765_4321的十六進制值發送到內存位置0x40E0_0018。很簡單。
上面用大括號寫的’(unit32_t*)表示用戶打算將“0x40E0_0018”的常量值轉換爲另一種類型的數據,即指針,以便它可以用作內存位置的地址。指針指向簡單的內存位置。
這裏我們剛剛解釋瞭如何將常量數據(0x40E0_0018)轉換爲另一種類型的數據,稱爲指針數據類型。軟件人員稱之爲“類型轉換”。也就是,把一種數據轉換成另一種數據。或者將一種類型的數據更改爲另一種類型的數據。所以這裏是“類型選擇”行話。
這裏我們剛剛解釋瞭如何將常量數據(0x40E0_0018)轉換爲另一種類型的數據,稱爲指針數據類型。軟件人員稱之爲“類型轉換”。也就是,把一種數據轉換成另一種數據。或者將一種類型的數據更改爲另一種類型的數據。所以這裏是“類型選擇”行話。
那麼“((uint32_t)0x40E00018)=0x87654321”總體上意味着用戶現在希望將0x8765_4321的值寫入內存位置0x40E0_0018。指針的“星”表示指針指向的位置處的值。因此,在上面的行中,用戶將0x8765_4321分配給(uint32_t*)0x40E00018的“star”。記住,’ (uint32_t*)0x40E00018’是指向內存位置0x4E0_0018的指針。
asm(“NOP”)是彙編語言中的“no operation”指令,在這裏使用,因爲我不知道C語言中有什麼替代方法。要使用C語言中的彙編指令,請使用asm(“彙編指令”)。
現在是while循環:
while (1) {}。
這段代碼將出現在大多數嵌入式C程序中。在嵌入式世界,只要處理器有電,它就可以運行。它就像一個瓶子裏的金妮,它必須一直做些什麼。如果處理器的電源沒有關閉,或者處理器沒有進入睡眠狀態,它將繼續執行“某些操作”。所以上面的while(1)do nothing循環的作用完全相同。
爲某目標微控制器編寫了嵌入式C程序。典型的微控制器至少有微處理器、存儲器、外圍設備和時鐘源。上述C程序寫入一個內存位置該內存位置可能屬於外圍設備中的“寄存器”。
The Compile Flow:
技術的這一部分解釋編譯器/鏈接器生成的機器代碼(彙編代碼)的位。
程序集代碼的部分已顯示/描述。
本文還描述了編譯過程中的幕後操作。
好吧,但什麼是“編譯”?
處理器只能執行二進制指令。編譯器將用高級語言(如C)編寫的可讀程序轉換成二進制指令。然後將這些二進制指令放入內存。處理器啓動時,從內存中獲取這些二進制指令並執行它們。將人類可讀代碼轉換爲二進制代碼的過程稱爲“編譯”
下面是前面介紹的簡單C程序:
typedef unsigned long
uint32_t;
int main ()
{
int ii;
for(ii=0;ii<305419896;ii++) {
*((uint32_t *)0x40E00018) = 0x87654321;
asm(“NOP”);
}
while(1){}
}
Compile it using KEIL
uVision: Click Here to go to a Very simple Quick Tutorial
編譯上述C代碼和’statup.s’文件會生成一個名爲’axf’的可執行文件。axf是一個人類無法讀取的二進制文件,但是可以生成這個“axf”文件的人類可讀取版本,它將以“彙編語言”顯示指令:這是由Keil提供的名爲“fromelf”實用程序完成的。Keil教程演示瞭如何使用這個實用程序和精確的命令語法來完成這個轉換
下面是“axf”文件的可讀版本的一節。可以看到C語言中的“main()”是如何轉換爲彙編指令的。
下面截取的代碼還顯示了每個指令的地址,即在內存中存儲該段代碼的位置。例如,“main()”的第一條指令存儲在位置0x0000_0134,這是指令MOVS r0,#0。這意味着在寄存器r0中移動值“0”。
還可以注意到,上述“C”程序中的所有“常量”值都存儲在從0x0000_014C開始的完全獨立的內存位置。有3個這樣的常量,如下所示。
注意,上面的main()中的指令不是從地址0x0000_0000開始的,而是從0x0000_0134開始的。
然後,如果對“axf”文件的全文版本進行分析,它將顯示出許多情況。上面的“main”代碼只有幾行。
“axf”文件中的額外內容是什麼。
“axf”文件包含許多調試信息。當代碼下載到目標設備上,並且設備仍連接到主機PC時,此調試信息有助於調試代碼。當目標代碼加載到目標本身時,代碼和調試信息都加載到開發主機PC的內存中。
當使用某些編譯時選項刪除調試信息時,axf文件將如下所示:
這又是很多代碼,這是多餘的’主要’代碼。
將二進制axf文件轉換成ARM體系結構能夠執行的格式需要多餘的信息。
在執行用戶“main()”之前,將調用以下函數。
__main -> this is not the user main(), but a function called at
the start of the binary executable, which calls other functions.
__scatterload
__rt_entry
__rt_entry in turn will call
__rt_lib_init
User
Code (your code inside main)
exit()
主程序是用戶程序的入口點。此主函數是預定義的(儘管用戶可以編寫自己的主函數)。請注意,這個main與用戶的C程序中的main()不同。如果用戶打算編寫自己的’uuu main’,他們可以使用自己的代碼和名稱。但是,用戶必須更新鏈接器的默認“–startup”選項,例如“–startup my\u main”,因爲默認鏈接器選項是以下“–startup=\u main”。如果用戶願意,也可以使用“–no_startup”。但是,這樣做的後果超出了本教程的範圍。
__ __main then calls __scatterload。
對於understand __scatterload,重要的是要進一步瞭解代碼如何存儲在內存中以及如何執行。
典型的微控制器系統通常有幾種類型的存儲器。例如,閃存、ROM、RAM等。
這意味着,同一代碼可能在不執行時駐留在一個內存中,然後在執行時移動到另一個內存中。例如,代碼及其數據在不執行時可以駐留在ROM中,然後將其移動到RAM中執行。
在另一個例子中,代碼可以直接從ROM執行,但是它的變量必須複製到RAM,因爲這些變量可能需要由運行的代碼更新。Keil教程2展示瞭如何將變量的初始值存儲在只讀存儲器中,而變量本身存儲在讀寫存儲器中,然後在程序執行之前將這些變量的初始值複製到讀寫存儲器中。在Keil教程2中,可以看到C程序有兩個整數數組變量,即avar[10]和bvar[10],它們有一些初始值。avar[10]的初始值存儲在地址0x0000_015c到0x0000_0180處。這可能是ROM地址。
然而,當程序執行時,變量avar[10]和bvar[10]存儲在堆棧存儲器的某處堆棧存儲器是從0x2000_0000開始的區域中的讀/寫存儲器。在執行用戶的main()之前,這些變量的初始值已經在堆棧中可用。這意味着在執行用戶的main()之前,這些初始值是如何從區域0x0000_0xxx(只讀區域)複製到0x2000_0yyy(讀寫區域)的。