1. 程式人生 > 實用技巧 >9. 從0學ARM Cortex-A9 LED彙編、C語言驅動編寫

9. 從0學ARM Cortex-A9 LED彙編、C語言驅動編寫

0. 前言

一般我們購買一個開發板,廠家都會給出對應的電路圖檔案,我們可以通過搜尋對應名稱來查詢到對應的外設。對於驅動工程師來說,我們只需要知道外設與SOC互動的一些資料線和訊號線即可。

用主控晶片控制這些外設的一般步驟:

  1. 看電路原理圖,弄明白主控晶片和外設是怎麼連線的,對於驅動工程師來說,主要是看外設的一些clk、資料引腳、控制引腳是如何連線的;
  2. 外設一般都會連線到SOC的1個或者多個控制器上,比如i2c、spi、gpio等,有的是資料線有的是訊號線,中斷線等;
  3. 根據電路連線和需求對主控晶片進行設定,往往對外設的設定都是通過暫存器操作實現;
  4. 書寫相應程式碼,實現功能,不同型別的外設,程式碼結構也不盡相同,比如按鍵,我們既可以通過輪詢方式讀取按鍵資訊,也可以通過中斷方式來讀取。

下面我們就以華清遠見的fs4412開發板為例來看如何編寫led的裸機程式。
SOC exynos 4412 datahseet 下載地址:
https://download.csdn.net/download/daocaokafei/12533438

一、LED燈電路圖

通過後續的章節的一些常用外設的分析,相信大家會掌握如何查閱電路圖。

首先看下led電路圖:

  1. 該板子有4個LED,是發光二極體,有電流是為藍色;
  2. led都接了上拉電阻;
  3. 三極體的基極接了SOC的某個GPIO引腳;
  4. 比如GPX1_0,當該引腳為高電平是,三極體pn結導通,於是LED3兩側就有了電勢差,LED3被點亮,如果該引腳為低電平,pn結截止,LED3兩側就沒有了電勢差,LED3熄滅。

下面是CPU核訪問GPIO控制器的資料通路:

  1. AHB:高速匯流排
  2. APB Bridge:APB匯流排橋
  3. APB:外設匯流排,低速匯流排
  4. GPIO掛載在APB總線上


由上圖可知,cpu要訪問GPIO的暫存器需要經過的路徑。

二、GPIO

GPIO(General Purpose I/O Ports)意思為通用輸入/輸出埠,通俗地說,就是一些引腳,可以通過它們輸出高低電平或者通過它們讀入引腳的狀態-是高電平或是低電平。

使用者可以通過GPIO口和硬體進行資料互動(如UART),控制硬體工作(如LED、蜂鳴器等),讀取硬體的工作狀態訊號(如中斷訊號)等。GPIO口的使用非常廣泛。

1. GPIO的優點

  • 低功耗:GPIO具有更低的功率損耗(大約1µA,µC的工作電流則為100µA)。
  • 整合I²C從機介面:GPIO內建I²C從機介面,即使在待機模式下也能夠全速工作。
  • 小封裝:GPIO器件提供最小的封裝尺寸—3mm x 3mm QFN!
  • 低成本:您不用為沒有使用的功能買單!
  • 快速上市:不需要編寫額外的程式碼、文件,不需要任何維護工作!
  • 靈活的燈光控制:內建多路高解析度的PWM輸出。
  • 可預先確定響應時間:縮短或確定外部事件與中斷之間的響應時間。
  • 更好的燈光效果:匹配的電流輸出確保均勻的顯示亮度。
  • 佈線簡單:僅需使用2條I²C匯流排或3條SPI匯流排。

2. exynos4412 GPIO特性

  1. 172 個外部中斷
  2. 32個外部可喚醒中斷
  3. 252個多功能 input/output ports
  4. 在休眠模式下也可以控制GPIO引腳,但不包括 GPX0, GPX1, GPX2, and GPX3

3. 6 General Purpose Input/Output (GPIO) Control

Exynos 4412 SCP 包括304個多功能 input/output埠引腳和164 儲存埠引腳. 總共 37
個埠分組和兩個儲存埠分組.。

下圖為GPIO模組圖:

三、如何操作GPIO?

主要通過暫存器來操作GPIO引腳。

GPxCON用於選擇引腳功能,GPxDAT用於讀/寫引腳資料;另外,GPxUP用於確定是否使用內部上拉電阻。其中x為A、B…..H、J等。

1. GPxCON暫存器

從暫存器的名字可以看出,它用於配置(Configure)-選擇引腳功能。

LED3是連線到GPX1_0,該引腳說明如下:
由上圖所示,

  1. GPX1CON地址為0x1100C20;
  2. LED3是輸出裝置,所以需要將GPX1CON[3:0]設定為0x1,但是能修改其他的bite。

2. GPxDAT暫存器

GPxDAT用於讀/寫引腳;當引腳被設為輸入時,讀此暫存器可知相應引腳的電平狀態是高還是低;當引腳被設為輸出時,寫此暫存器相應位可以令此引腳輸出高電平或是低電平。

  1. GPX1DAT的地址是0x1100C24
  2. LED3對應的輸出引腳是GPX1DAT[0],點燈只需要將該引腳設定為1即可,滅燈將bite0置0。

3. GPxUP暫存器

GPxUP:某位為1時,相應引腳無內部上拉電阻;為0時,相應引腳使用內部上拉電阻。

上拉電阻的作用在於:當GPIO引腳處於第三態(即不是輸出高電平,也不是輸出低電平,而是呈高阻態,即相當於沒接晶片)時,它的電平狀態由上拉電阻、下拉電阻確定。

本例不用設定。

四、驅動編寫

下面我們分別用匯編和C語言來給LED編寫驅動程式。

1. 彙編程式碼

大家如果掌握了我之前講解的彙編指令的知識點,那麼這個程式碼很容易就能看明白:

.globl _start
.arm
_start:
	LDR R0,=0x11000C20 @將配置暫存器GPX1CON的地址寫入到R0
	LDR R1,[R0]  @讀取暫存器GPX1CON的值儲存到R1
	BIC R1,R1,#0x0000000f @將R1的3:0位清0,目的是不覆蓋到其他bit的值
	ORR R1,R1,#0x00000001 @將R1的3:0位置1
	STR R1,[R0]  @將R1的值寫回暫存器GPX1CON
loop:
	LDR R0,=0x11000C24 @將data暫存器GPX1DAT的地址寫入到R0
	LDR R1,[R0] @讀取暫存器GPX1DAT的值儲存到R1
	ORR R1,R1,#0x01 @將R1的值bite0 設定為1,即拉高,點燈
	STR R1,[R0]  @將R1的值寫回暫存器GPX1DAT
	BL delay  @呼叫延時函式
	LDR R1,[R0] 
	BIC R1,R1,#0x01 @將R1的值bite0 設定為0,即拉低,滅燈
	STR R1,[R0]
	BL delay
	B loop
delay:     @delay延時函式
	LDR R2,=0xfffffff
loop1:
	SUB R2,R2,#0x1
	CMP R2,#0x0
	BNE loop1
	MOV PC,LR @返回
.end 

Makefile

TARGET=gcd
all:
	arm-none-linux-gnueabi-gcc -O0 -g -c -o $(TARGET).o $(TARGET).s
	arm-none-linux-gnueabi-ld	 	$(TARGET).o -Ttext 0x40008000 -N -o $(TARGET).elf
	arm-none-linux-gnueabi-objcopy -O binary -S $(TARGET).elf $(TARGET).bin
clean:
	rm -rf *.o *.elf *.dis *.bin

程式功能很簡單,就是讓LED3呈現一閃一閃的效果。

執行make,最終生成的gcd.bin檔案。

2. c語言實現

如果要進入C語言執行環境,那麼就必須為設定棧空間,函式呼叫引數和返回值會壓棧。

start.s

.text
.global _start
_start:
		ldr		sp,=0x70000000         /*get stack top pointer*/
		b		main

main.c

/* GPX1 */
typedef struct {
				unsigned int CON;
				unsigned int DAT;
				unsigned int PUD;
				unsigned int DRV;
}gpx1;
#define GPX1 (* (volatile gpx1 *)0x11000C20 )

void led_init(void)
{
	GPX1.CON = GPX1.CON & (~(0x0000000f)) | 0x00000001;
}
void led_on(int n)
{	
	GPX1.DAT = GPX1.DAT|0x01;
}
void led_off()
{	
	GPX1.DAT = GPX1.DAT&(~(0x01));		
}
void  delay_ms(unsigned int num)
{   int i,j;
     for(i=num; i>0;i--)
     	for(j=1000;j>0;j--)
         ;
 }
int main(void)
{
	led_init ();
	while (1) {
		led_on();
		delay_ms(500);
		led_off();
		delay_ms(500);
	}
	while(1);
    return 0;
}   

map.lds

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
	. = 0x40008000;     ;從該地址開始
	. = ALIGN(4);
	.text      :       ;指定程式碼段
	{
		gcd.o(.text)  ;程式碼的第一個部分,絕對不能錯
		*(.text)
	}
	. = ALIGN(4);
    .rodata :             ;只讀資料段
	{ *(.rodata) }
    . = ALIGN(4);
    .data :              ;讀寫資料段
	{ *(.data) }
    . = ALIGN(4);
    .bss :              
     { *(.bss) }
}

Makefile

TARGET=gcd
TARGETC=main
all:
	arm-none-eabi-gcc -O0 -g -c -o $(TARGETC).o  $(TARGETC).c
	arm-none-eabi-gcc -O0 -g -c -o $(TARGET).o $(TARGET).s
	arm-none-eabi-gcc -O0 -g -S -o $(TARGETC).s  $(TARGETC).c
	arm-none-eabi-ld	$(TARGETC).o	$(TARGET).o -Tmap.lds -o  $(TARGET).elf
	arm-none-eabi-objcopy -O binary -S $(TARGET).elf $(TARGET).bin	
clean:
	rm -rf *.o *.elf *.dis *.bin

執行make命令,最終生成的gcd.bin檔案。

這段程式碼中,讀者可能不能理解的是下面的定義:

typedef struct {
				unsigned int CON;
				unsigned int DAT;
				unsigned int PUD;
				unsigned int DRV;
}gpx1;
#define GPX1 (* (volatile gpx1 *)0x11000C20 )

由上圖所示:

  1. (volatile gpx1 *)0x11000C20 ) :將常量0x11000C20 強轉成struct gpx1型別指標
  2. (* (volatile gpx1 *)0x11000C20 ):查詢指標對應的記憶體驅動,即對應整個結構體變數,結構體變數地址為0x11000C20
  3. define GPX1 (* (volatile gpx1 *)0x11000C20 ) :GPX1等價於地址為0x11000C20的結構體變數

這樣我們要想操作GPX1的暫存器,就可以像結構體變數一樣操作即可。

3. 測試

採用UBOOT自帶的命令loadb,通過串列埠以baud速率下載binary(.bin)至SDRAM中某一地址中,然後用go 命令從某地址處開始執行程式。

該命令使用了kermit protocol,嵌入式系統通常使用該協議與pc傳送檔案。

操作步驟如下:

  1. 串列埠連線開發板,開發板啟動後在讀秒階段,立即按下回車,進入uboot命令介面
  2. 執行loadb 40008000 【該地址與Makefile 和map.lds檔案中的地址保持一致】
  3. 選擇選單transfer->send Kermit,
  4. 然後選擇我們編譯好的gcd.bin檔案,
  5. 點選OK,出現"Staring kermit transfer."字樣,
  6. 執行 go 40008000,執行程式


執行結果:
可以看到LED閃爍的現象。

5. 注意

該種測試方法需要bootloader選用uboot,並且需要串列埠工具支援Kermit協議,一口君使用的是SecureCRT7.3.3版本【其他低一些的版本可能不支援該協議】,該軟體的下載和安裝方法【安裝方法有點繁瑣】可以公眾號後臺回覆【SecureCRT】。