用匯編點亮LED
從原理圖可知讓GPF4=0時,led燈亮
控制IO口的步驟(操作寄存器,看芯片手冊)
- 配置功能:輸出/輸入、其他
- 設置IO口輸出高電平或低電平
GPFCON寄存器和GPFDAT寄存器的初始地址如下所示:
接下來先用匯編寫
.text
.global _start
_start:
LDR R0,=0x56000050 @ R0設爲GPBCON寄存器。此寄存器
@ 用於選擇端口B各引腳的功能:
@ 是輸出、是輸入、還是其他
MOV R1,#0x00000100
STR R1,[R0] @ 設置GPB5爲輸出口, 位[10:9]=0b01
LDR R0,=0x56000054 @ R0設爲GPBDAT寄存器。此寄存器
@ 用於讀/寫端口B各引腳的數據
MOV R1,#0x00000000 @ 此值改爲0x00000020,
@ 可讓LED1熄滅
STR R1,[R0] @ GPB5輸出0,LED1點亮
MAIN_LOOP:
B MAIN_LOOP
下面是makefile裏面的內容
led_on.bin : led_on.S
arm-linux-gcc -g -c -o led_on.o led_on.S @將.s文件轉換成.o文件
arm-linux-ld -Ttext 0x0000000 -g led_on.o -o led_on_elf @1.
arm-linux-objcopy -O binary -S led_on_elf led_on.bin @2.
clean:
rm -f led_on.bin led_on_elf *.o
@1.-o表示輸出,表示由led_on.o輸出led_on.elf文件。
-Ttext 0x00000000 表示把代碼段的地址指爲0,2440有兩種啓動方式,分別爲Nand啓動,一種Nor啓動,例子都用Nand啓動
@-g表示加入一些調試信息,所以加不加無所謂
@2.bin文件爲二進制的文件該文件會燒到開發板,-O binary表示輸出二進制,-S暫且不管
編譯程序是會經過的幾個步驟:
- 預處理(做一些語法分析)
- 編譯(.c文件編譯成.s文件)
- 彙編(將.s文件編譯成.o文件,即二進制文件)
- 鏈接(把多個.o連接成一個可執行文件,即.bin文件)
以Nand啓動時,會強制將外部NandFlash前面4k的內容拷貝到芯片內SRAM裏面去,起始地址爲0
由makefile推斷傳過去Ubuntu的只有led_on.s,所以這裏需要自己進行實驗,視頻是直接將整個已經弄好的文件夾傳過去的,根本不知道具體要用哪個文件。
由圖上表明,只需要led_on.s,並在ubuntu裏編寫makefile後,運行make便可以產生根據makefile裏面描述所增加的文件
爲什麼用-Ttext 0x0000000 將代碼段指向0地址呢?
2440有兩種啓動方式:1. Nand啓動 2. Nor啓動 (例子都是用Nand啓動)
我們的程序是燒到Nand Flash裏面去的,以Nand啓動時
- 硬件上會強制將外部Nand Flash前面4k的內容拷貝到內部4k的SRAM,內部4k的SRAM起始地址爲0。
- CPU從0地址執行,因此代碼段會是從0地址開始的。該0地址處於CPU內。
Nor啓動又是怎麼回事呢?
CPU會外接NOR Flash ,NOR Flash能像正常內存那樣去訪問,但是不能像內存那樣直接寫數據,而Nand Flash不行。如果是NOR啓動的話,0地址就不會在CPU裏面,而是在NOR Flash那裏。
- 0地址是取向NOR Flash的
- CPU從0地址執行。
故在Nand 啓動的時候,會將led_on.S的內容複製到片面的SRAM中,然後根據代碼一次執行下來。
用C語言來點亮LED
用VC寫程序時一上來就用main函數來寫程序。,實際main函數也是要被其他的東西調用的,因爲main函數也是函數。
在主機上開發時肯定有庫,或者說是啓動文件,還有hello.c或者main函數
啓動文件的工作
在C函數之前,我們要做的事情:
a.自己寫啓動文件,啓動文件要做的事情:
- 設置main函數的返回地址。
- 調用main函數。
- 返回之後要去做什麼事情,所以要有些清理工作。
- C函數調用前要把返回地址和參數要壓棧,執行完再從棧裏面返回這些數據,因此要設置棧。設置棧就是要把棧指針指向某塊內存,這塊內存是片內SRAM,這樣就不用初始化還有可以使用,如果是SDRAM就要對其進行初始化。很多其他硬件也是需要初始化。
b.硬件方面的初始化(2440)
- 關看門狗
- 初始化時鐘(最快400MHZ)
- 初始化SDRAM
根據上訴內容觀察下面的代碼,該程序在硬件方面只需要關看門狗,其他兩個暫時不需要
.text
.global _start
_start:
ldr r0, =0x53000000 @ WATCHDOG寄存器地址
mov r1, #0x0
str r1, [r0] @ 寫入0,禁止WATCHDOG,否則CPU會不斷重啓
@硬件初始化完畢,接下來進行軟件初始化
ldr sp, =1024*4 @ 設置堆棧,注意:不能大於4k, 因爲現在可用的內存只有4K
@ nand flash中的代碼在復位後會移到內部ram中,此ram只有4K
bl main @ 調用C程序中的main函數
halt_loop:
b halt_loop
sp放置的位置如下圖所示,上電後,外部Nand Flash會把內容複製進SRAM,將其全填滿,顯然該程序用不了這麼多,後面有大量空餘內存,故將sp放在此處。
bl main 該指令是指跳到main函數裏面去,同時把返回地址保存在lr裏面,因此完成了上訴兩項內容。
main函數執行完後就會跳回來不斷循環
main函數的內容
#define GPFCON (*(volatile unsigned long *)0x56000050)
#define GPFDAT (*(volatile unsigned long *)0x56000054)
int main()
{
GPFCON = 0x00000100; // 設置GPF4爲輸出口, 位[9:8]=0b01
GPFDAT = 0x00000000; // GPF4輸出0,LED1點亮
return 0;
}
#define GPFCON (*(volatile unsigned long *)0x56000050)這個是什麼意思?
p=(int *)0x56000050; //將指向整形數據的指針(即地址)賦給指針p
*p=0x100;
這兩個句子相當於:
*((int *)0x56000050)=0x100;
系統可能看你這個變量賦值後並沒有用到,所以會自作聰明的將它優化掉,因此volatile的作用就是不需要優化
軟件設置時爲什麼設置棧,看反彙編文件就知道了,.dis是反彙編文件
一下代碼是反彙編的內容(內容過多,所以只複製有涉及到的內容)
00000000 <_start>:
0: e3a00453 mov r0, #1392508928 ; 0x53000000
4: e3a01000 mov r1, #0 ; 0x0
8: e5801000 str r1, [r0]
c: e3a0da01 mov sp, #4096 ; 0x1000
10: eb000000 bl 18 <main>
00000014 <halt_loop>:
14: eafffffe b 14 <halt_loop>
00000018 <main>:
18: e1a0c00d mov ip, sp
1c: e92dd800 stmdb sp!, {fp, ip, lr, pc} @main函數一開始就將四個寄存器保存在棧裏面,因此在main函數調用之前一定要設置棧
20: e24cb004 sub fp, ip, #4 ; 0x4
24: e3a03456 mov r3, #1442840576 ; 0x56000000
28: e2833050 add r3, r3, #80 ; 0x50
2c: e3a02c01 mov r2, #256 ; 0x100
30: e5832000 str r2, [r3]
34: e3a03456 mov r3, #1442840576 ; 0x56000000
38: e2833054 add r3, r3, #84 ; 0x54
3c: e3a02000 mov r2, #0 ; 0x0
40: e5832000 str r2, [r3]
44: e3a03000 mov r3, #0 ; 0x0
48: e1a00003 mov r0, r3
4c: e89da800 ldmia sp, {fp, sp, pc} @又從棧裏面來回復四個寄存器
Disassembly of section .comment:
輪流點亮三個LED
啓動文件跟之前的相同
C文件內容如下:
#define GPFCON (*(volatile unsigned long *)0x56000050)
#define GPFDAT (*(volatile unsigned long *)0x56000054)
#define GPF4_out (1<<(4*2))
#define GPF5_out (1<<(5*2))
#define GPF6_out (1<<(6*2))
void wait(volatile unsigned long dly)
{
for(; dly > 0; dly--);
}
int main(void)
{
unsigned long i = 0;
GPFCON = GPF4_out|GPF5_out|GPF6_out; // 將LED1,2,4對應的GPF4/5/6三個引腳設爲輸出
while(1){
wait(30000);
GPFDAT = (~(i<<4)); // 根據i的值,點亮LED1,2,4
if(++i == 8)
i = 0;
}
return 0;
}
makefiele 文件內容如下:
CFLAGS := -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -ffreestanding
leds.bin : crt0.S leds.c
arm-linux-gcc $(CFLAGS) -c -o crt0.o crt0.S @只編譯不鏈接
arm-linux-gcc $(CFLAGS) -c -o leds.o leds.c
arm-linux-ld -Ttext 0x0000000 crt0.o leds.o -o leds_elf @把他們鏈接到一塊,併產生_elf文件
# arm-linux-ld -Tleds.lds crt0.o leds.o -o leds_elf
arm-linux-objcopy -O binary -S leds_elf leds.bin @轉換成二進制文件
arm-linux-objdump -D -m arm leds_elf > leds.dis @-objdump的意思就是把elf格式的可執行程序反彙編,-D是指將所有的段都反彙編一下,-m,m指machine,指ARM架構的機器
clean:
rm -f leds.dis leds.bin leds_elf *.o
只編譯不鏈接時,執行的內容入下:
- 預處理(做一些語法分析)
- 編譯(.c文件編譯成.s文件)
- 彙編(將.s文件編譯成.o文件,即二進制文件)
用按鍵控制LED燈
啓動文件跟前面的相同,C文件如下所示:
#define GPFCON (*(volatile unsigned long *)0x56000050)
#define GPFDAT (*(volatile unsigned long *)0x56000054)
#define GPGCON (*(volatile unsigned long *)0x56000060)
#define GPGDAT (*(volatile unsigned long *)0x56000064)
/*
* LED1,LED2,LED4對應GPF4、GPF5、GPF6
*/
#define GPF4_out (1<<(4*2))
#define GPF5_out (1<<(5*2))
#define GPF6_out (1<<(6*2))
#define GPF4_msk (3<<(4*2))
#define GPF5_msk (3<<(5*2))
#define GPF6_msk (3<<(6*2))
/*
* S2,S3,S4對應GPF0、GPF2、GPG3
*/
#define GPF0_in (0<<(0*2))
#define GPF2_in (0<<(2*2))
#define GPG3_in (0<<(3*2))
#define GPF0_msk (3<<(0*2))
#define GPF2_msk (3<<(2*2))
#define GPG3_msk (3<<(3*2))
int main()
{
unsigned long dwDat;
// LED1,LED2,LED4對應的3根引腳設爲輸出
GPFCON &= ~(GPF4_msk | GPF5_msk | GPF6_msk);
GPFCON |= GPF4_out | GPF5_out | GPF6_out;
// S2,S3對應的2根引腳設爲輸入
GPFCON &= ~(GPF0_msk | GPF2_msk);
GPFCON |= GPF0_in | GPF2_in;
// S4對應的引腳設爲輸入
GPGCON &= ~GPG3_msk;
GPGCON |= GPG3_in;
while(1){
//若Kn爲0(表示按下),則令LEDn爲0(表示點亮)
dwDat = GPFDAT; // 讀取GPF管腳電平狀態
if (dwDat & (1<<0)) // S2沒有按下
GPFDAT |= (1<<4); // LED1熄滅
else
GPFDAT &= ~(1<<4); // LED1點亮
if (dwDat & (1<<2)) // S3沒有按下
GPFDAT |= (1<<5); // LED2熄滅
else
GPFDAT &= ~(1<<5); // LED2點亮
dwDat = GPGDAT; // 讀取GPG管腳電平狀態
if (dwDat & (1<<3)) // S4沒有按下
GPFDAT |= (1<<6); // LED3熄滅
else
GPFDAT &= ~(1<<6); // LED3點亮
}
return 0;
}
程序要點講解:
- #define GPF4_out (1<<(42))
#define GPF5_out (1<<(52))
#define GPF6_out (1<<(6*2))
#define GPF4_msk (3<<(42))
#define GPF5_msk (3<<(52))
#define GPF6_msk (3<<(6*2))
上訴宏定義的意義是什麼?
將4引腳設置爲輸出引腳,不應該影響到其他的引腳,因此規範的做法是你涉及哪些位就改變哪些位就行了,具體的做法是將涉及的位清零,再或上具體的要修改的值。比如下方所示:
GPFCON &= ~(GPF4_msk | GPF5_msk | GPF6_msk); //將指定的位清零
GPFCON |= GPF4_out | GPF5_out | GPF6_out; //給指定的位賦值
不影響其他位的操作稱爲位操作,對寄存器操作時,該方法很重要
思考個問題:能否用 a |= (1<<4);或者a &= ~(1<<4); 進行寄存器位操作呢?
不行!對單獨一個位,這樣是可以的,但是倘若是多個位,而且有0有1,那麼顯然這麼做是非常複雜的。
- dwDat = GPFDAT; // 讀取GPF管腳電平狀態
if (dwDat & (1<<0)) // S2沒有按下,bit0爲1則表示沒按下按鍵
GPFDAT |= (1<<4); // LED1熄滅
總結
一、位操作:
- 清零:a &= ~(1<<3);
- 置一:a |= (1<<3);
二、用匯編寫程序和和用C語言寫程序的差別:
- 用C語言寫程序時,一開始就用main函數,而main函數需要啓動文件對其進行調用,因此用C函數寫的話除了.c文件之外還有crt0.s這個啓動文件,而彙編就只需要.S一個彙編文件。
三、啓動文件的工作主要有硬件初始化和軟件初始化。