linux嵌入式第二課之GPIO

用匯編點亮LED

在這裏插入圖片描述
從原理圖可知讓GPF4=0時,led燈亮

控制IO口的步驟(操作寄存器,看芯片手冊)

  1. 配置功能:輸出/輸入、其他
  2. 設置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暫且不管

編譯程序是會經過的幾個步驟:

  1. 預處理(做一些語法分析)
  2. 編譯(.c文件編譯成.s文件)
  3. 彙編(將.s文件編譯成.o文件,即二進制文件)
  4. 鏈接(把多個.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啓動時

  1. 硬件上會強制將外部Nand Flash前面4k的內容拷貝到內部4k的SRAM,內部4k的SRAM起始地址爲0。
  2. CPU從0地址執行,因此代碼段會是從0地址開始的。該0地址處於CPU內。

Nor啓動又是怎麼回事呢?
CPU會外接NOR Flash ,NOR Flash能像正常內存那樣去訪問,但是不能像內存那樣直接寫數據,而Nand Flash不行。如果是NOR啓動的話,0地址就不會在CPU裏面,而是在NOR Flash那裏。

  1. 0地址是取向NOR Flash的
  2. CPU從0地址執行。

故在Nand 啓動的時候,會將led_on.S的內容複製到片面的SRAM中,然後根據代碼一次執行下來。

用C語言來點亮LED

用VC寫程序時一上來就用main函數來寫程序。,實際main函數也是要被其他的東西調用的,因爲main函數也是函數。
在主機上開發時肯定有庫,或者說是啓動文件,還有hello.c或者main函數

啓動文件的工作

在C函數之前,我們要做的事情:
a.自己寫啓動文件,啓動文件要做的事情:

  1. 設置main函數的返回地址。
  2. 調用main函數。
  3. 返回之後要去做什麼事情,所以要有些清理工作。
  4. C函數調用前要把返回地址和參數要壓棧,執行完再從棧裏面返回這些數據,因此要設置棧。設置棧就是要把棧指針指向某塊內存,這塊內存是片內SRAM,這樣就不用初始化還有可以使用,如果是SDRAM就要對其進行初始化。很多其他硬件也是需要初始化。

b.硬件方面的初始化(2440)

  1. 關看門狗
  2. 初始化時鐘(最快400MHZ)
  3. 初始化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

只編譯不鏈接時,執行的內容入下:

  1. 預處理(做一些語法分析)
  2. 編譯(.c文件編譯成.s文件)
  3. 彙編(將.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;
}

程序要點講解:

  1. #define GPF4_out (1<<(42))
    #define GPF5_out (1<<(5
    2))
    #define GPF6_out (1<<(6*2))

#define GPF4_msk (3<<(42))
#define GPF5_msk (3<<(5
2))
#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,那麼顯然這麼做是非常複雜的。

  1. dwDat = GPFDAT; // 讀取GPF管腳電平狀態
    if (dwDat & (1<<0)) // S2沒有按下,bit0爲1則表示沒按下按鍵
    GPFDAT |= (1<<4); // LED1熄滅
    在這裏插入圖片描述

總結
一、位操作:

  1. 清零:a &= ~(1<<3);
  2. 置一:a |= (1<<3);

二、用匯編寫程序和和用C語言寫程序的差別:

  1. 用C語言寫程序時,一開始就用main函數,而main函數需要啓動文件對其進行調用,因此用C函數寫的話除了.c文件之外還有crt0.s這個啓動文件,而彙編就只需要.S一個彙編文件。

三、啓動文件的工作主要有硬件初始化和軟件初始化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章