嵌入式知識-ARM裸機-學習筆記(2):利用GPIO來控制LED(附mkv210_image.c文件解析)

嵌入式知識-ARM裸機-學習筆記(2):利用GPIO來控制LED(附mkv210_image.c文件解析)

首先聲明該博客是針對朱有鵬老師的嵌入式課程進行筆記的總結。

一、通過GPIO控制點亮LED(通過彙編語言)

1. LED點亮原理

在這裏插入圖片描述
查閱原理圖,發現開發板上一共有5顆LED。其中一顆D26的接法是:正極接5V,負極接地。因此這顆LED只要上電就會常亮。因此我們分析這顆LED是電源指示燈。
剩下4顆LED的接法是:正極接3.3V,負極接了SoC上的一個引腳(GPIO),具體詳細接法是:
D22:GPJ0_3
D23:GPJ0_4
D24:GPJ0_5
D25:PWMTOUT1(GPD0_1)
我們知道LED點亮的原理是是其正負極兩端出現電壓差,這裏LED的正極爲3.3V已經固定,因此若想點亮LED秩序將負極電壓變爲0(共陽極接法)。負極接在了SoC的引腳上,可以通過SoC中編程來控制負極的電壓值,想點亮哪個引腳的LED就給這個引腳置0。

2. GPIO說明

GPIO(general purpose input output )通用輸入輸出:GPIO就是芯片的引腳(芯片上的引腳有些不是GPIO,只有一部分是),它的功能和特點是可以被編程控制它的工作模式,也可以編程控制他的電壓高低等

由於軟件操作硬件的接口是:寄存器。 我們當前要操作的硬件是LED,但是LED實際是通過GPIO來間接控制的,所以當前我們實際要操作的設備其實是SoC的GPIO,通過改變GPIO硬件的狀態間接影響LED的狀態。要操作這些GPIO,必須通過設置他們的寄存器
查閱數據手冊可知,GPJ0相關的寄存器有以下:
GPJ0CON, (GPJ0 control)GPJ0控制寄存器,用來配置各引腳的工作模式
GPJ0DAT, (GPJ0 data)當引腳配置爲input/output模式時,寄存器的相應位和引腳的電平高低相對應。
(這裏標黃的兩個寄存器起到主要功能)
GPJ0PUD, (pull up down)控制引腳內部弱上拉、下拉
GPJ0DRV, (driver)配置GPIO引腳的驅動能力
GPJ0CONPDN,(記得是低功耗模式下的控制寄存器)
GPJ0PUDPDN (記得是低功耗模式下的上下拉寄存器)

因此如果想點亮LED,需要:
1、操控GPJ0CON寄存器中,選中output模式
2、操控GPJ0DAT寄存器,相應的位設置爲0

3. 用匯編點亮LED

因此我們需要把相應的配置數據寫入相應的寄存器中,才能實現LED的點亮。即向GPJ0CON(0xE0200240)寄存器GPJ0DAT(0xE0200244)寄存器中寫入對應的信息,這裏要注意:寄存器名字是在數據手冊中體現的,而在寫代碼時,需要對應到該寄存器的地址才能進行控制。

點亮LED代碼如下:

//led.s文件(方式1:全點亮)
_start:
	// 第一步:把0x11111111寫入0xE0200240(GPJ0CON)位置
	ldr r0, =0x11111111			// 從後面的=可以看出用的是ldr僞指令,因爲需要編譯器來判斷這個數
	ldr r1, =0xE0200240			// 是合法立即數還是非法立即數。一般寫代碼都用ldr僞指令
	str r0, [r1]				// 寄存器間接尋址。功能是把r0中的數寫入到r1中的數爲地址的內存中去

	// 第二步:把0x0寫入0xE0200244(GPJ0DAT)位置
	ldr r0, =0x0				//如果要想改變LED點亮的狀態,即可修改0x0來控制哪個LED的亮滅
	ldr r1, =0xE0200244
	str r0, [r1]				// 把0寫入到GPJ0DAT寄存器中,引腳即輸出低電平,LED點亮

flag:							// 這兩行寫了一個死循環。因爲裸機程序是直接在CPU上運行的,CPU會
	b flag						// 逐行運行裸機程序直到CPU斷電關機。如果我們的程序所有的代碼都
								// 執行完了CPU就會跑飛(跑飛以後是未定義的,所以千萬不能讓CPU
								// 跑飛),不讓CPU跑飛的辦法就是在我們整個程序執行完後添加死循環

//led.s文件(方式2:指定某個點亮)
#define GPJ0CON	0xE0200240		//將數改爲宏定義的方式
#define GPJ0DAT	0xE0200244

.global _start					// 把_start鏈接屬性改爲外部,這樣其他文件就可以看見_start了
_start:
	// 第一步:把所有引腳都設置爲輸出模式,代碼不變
	ldr r0, =0x11111111			// 從後面的=可以看出用的是ldr僞指令,因爲需要編譯器來判斷這個數
	ldr r1, =GPJ0CON			// 是合法立即數還是非法立即數。一般寫代碼都用ldr僞指令
	str r0, [r1]				// 寄存器間接尋址。功能是把r0中的數寫入到r1中的數爲地址的內存中去

	// 第二步:把中間LED(GPJ0_4)的輸出設置爲0,其餘兩顆設置爲1,剩下的其他位不管
	ldr r0, =0x28
	ldr r1, =GPJ0DAT
	str r0, [r1]				// 把0寫入到GPJ0DAT寄存器中,引腳即輸出低電平,LED點亮

	b .							// .代表當前這一句指令的地址,這個就是高大上的死循環

//led.s文件(方式3:方式2的改版,通過位來進行操作)
#define GPJ0CON	0xE0200240
#define GPJ0DAT	0xE0200244

.global _start					// 把_start鏈接屬性改爲外部,這樣其他文件就可以看見_start了
_start:
	// 第一步:把所有引腳都設置爲輸出模式,代碼不變
	ldr r0, =0x11111111			// 從後面的=可以看出用的是ldr僞指令,因爲需要編譯器來判斷這個數
	ldr r1, =GPJ0CON			// 是合法立即數還是非法立即數。一般寫代碼都用ldr僞指令
	str r0, [r1]				// 寄存器間接尋址。功能是把r0中的數寫入到r1中的數爲地址的內存中去

	// 第二步:把中間LED(GPJ0_4)的輸出設置爲0,其餘兩顆設置爲1,剩下的其他位不管
	//ldr r0, =((1<<3) | (1<<5))	// 等效於0b00101000,即0x28
	ldr r0, =((1<<3) | (0<<4) | (1<<5))	// 清清楚楚的看到哪個滅,哪個是亮
	ldr r1, =GPJ0DAT
	str r0, [r1]				// 把0寫入到GPJ0DAT寄存器中,引腳即輸出低電平,LED點亮

	b .							// .代表當前這一句指令的地址,這個就是高大上的死循環

流水燈代碼如下:

#define GPJ0CON	0xE0200240
#define GPJ0DAT	0xE0200244

.global _start					// 把_start鏈接屬性改爲外部,這樣其他文件就可以看見_start了
_start:
	// 第一步:把所有引腳都設置爲輸出模式,代碼不變
	ldr r0, =0x11111111			// 從後面的=可以看出用的是ldr僞指令,因爲需要編譯器來判斷這個數
	ldr r1, =GPJ0CON			// 是合法立即數還是非法立即數。一般寫代碼都用ldr僞指令
	str r0, [r1]				// 寄存器間接尋址。功能是把r0中的數寫入到r1中的數爲地址的內存中去

	// 要實現流水燈,只要在主循環中實現1圈的流水顯示效果即可
flash:
	// 第1步:點亮LED1,其他熄滅
	ldr r0, =((0<<3) | (1<<4) | (1<<5))	// 清清楚楚的看到哪個滅,哪個是亮
	ldr r1, =GPJ0DAT
	str r0, [r1]				// 把0寫入到GPJ0DAT寄存器中,引腳即輸出低電平,LED點亮
	// 然後延時
	bl delay					// 使用bl進行函數調用
	
	// 第2步:點亮LED2,其他熄滅
	ldr r0, =((1<<3) | (0<<4) | (1<<5))	// 清清楚楚的看到哪個滅,哪個是亮
	ldr r1, =GPJ0DAT
	str r0, [r1]				// 把0寫入到GPJ0DAT寄存器中,引腳即輸出低電平,LED點亮
	// 然後延時
	bl delay	

	// 第3步:點亮LED3,其他熄滅
	ldr r0, =((1<<3) | (1<<4) | (0<<5))	// 清清楚楚的看到哪個滅,哪個是亮
	ldr r1, =GPJ0DAT
	str r0, [r1]				// 把0寫入到GPJ0DAT寄存器中,引腳即輸出低電平,LED點亮
	// 然後延時
	bl delay	//這裏bl可以實現delay函數調用	
	
	
	b flash		//跳轉到flash,相當於重新開始一遍,無限跳轉實現循環


// 延時函數:函數名:delay
delay:
	ldr r2, =9000000
	ldr r3, =0x0
delay_loop:	
	sub r2, r2, #1				//r2 = r2 -1
	cmp r2, r3					// cmp會影響Z標誌位,如果r2等於r3則Z=1,下一句中eq就會成立
	bne delay_loop
	mov pc, lr					// 函數調用返回

二、mkv210_image.c文件解讀

本實驗針對S5PV210板卡進行實驗。由於該板卡具有不同的啓動方式,因此需要製作不同的鏡像燒寫文件,這裏簡單描述USB鏡像文件和SD卡鏡像文件的製作。
由於SD卡啓動時,需要包含一個頭信息的校驗,因此需要通過mkv210_image.c文件來對USB啓動的鏡像文件進行加工,從而生成SD卡的鏡像文件。
在這裏插入圖片描述
分析啓動過程可知: 210啓動後先執行內部iROM中的BL0,BL0執行完後會根據OMpin的配置選擇一個外部設備來啓動(有很多,我們實際使用的有2個:usb啓動和SD卡啓動)。在usb啓動時內部BL0讀取到BL1後不做校驗,直接從BL1的實質內部0xd0020010開始執行,因此usb啓動的鏡像led.bin不需要頭信息,因此我們從usb啓動時直接將鏡像下載到0xd0020010去執行即可,不管頭信息了;從SD啓動時,BL0會首先讀取sd卡得到完整的鏡像(完整指的是led.bin和16字節的頭),然後BL0會自己根據你的實際鏡像(指led.bin)來計算一個校驗和checksum,然後和你完整鏡像的頭部中的checksum來比對。如果對應則執行BL1,如果不對應則啓動失敗。
在這裏插入圖片描述

//mkv210_image.c文件
/*
 * mkv210_image.c的主要作用就是由usb啓動時使用的led.bin製作得到由sd卡啓動的鏡像210.bin
 * 本文件來自於友善之臂的裸機教程,據友善之臂的文檔中講述,本文件是一個熱心網友提供,在此表示感謝。
 */
/* 在BL0階段,Irom內固化的代碼讀取nandflash或SD卡前16K的內容,
 * 並比對前16字節中的校驗和是否正確,正確則繼續,錯誤則停止。
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define BUFSIZE                 (16*1024)	//總長度16K大小
#define IMG_SIZE                (16*1024)
#define SPL_HEADER_SIZE         16			//頭長度16字節大小
//#define SPL_HEADER              "S5PC110 HEADER  "
#define SPL_HEADER              "****************"

int main (int argc, char *argv[])	//argc是用戶執行程序時傳遞參數的個數;argv是一個個的參數
{
	FILE		*fp;
	char		*Buf, *a;
	int		BufLen;
	int		nbytes, fileLen;
	unsigned int	checksum, count;
	int		i;
	
	//校驗3個參數(./mkx210  led.bin  210.bin)
	if (argc != 3)
	{
		printf("Usage: %s <source file> <destination file>\n", argv[0]);
		return -1;
	}

	//分配16K的buffer(其中包含實際內容和頭內容)
	BufLen = BUFSIZE;
	Buf = (char *)malloc(BufLen);	//這裏從堆上獲取空間
	if (!Buf)
	{
		printf("Alloc buffer failed!\n");
		return -1;
	}

	memset(Buf, 0x00, BufLen);	//清零

	//讀源bin到buffer
	//打開源bin,argv[1]對應led.bin文件
	fp = fopen(argv[1], "rb");
	if( fp == NULL)
	{
		printf("source file open error\n");
		free(Buf);
		return -1;
	}
	//獲取源bin長度
	fseek(fp, 0L, SEEK_END);								// 定位到文件尾
	fileLen = ftell(fp);									// 得到文件長度
	fseek(fp, 0L, SEEK_SET);								// 再次定位到文件頭
	//源bin長度不得超過16K-16byte
	count = (fileLen < (IMG_SIZE - SPL_HEADER_SIZE))
		? fileLen : (IMG_SIZE - SPL_HEADER_SIZE);
	//buffer[0~15]存放"S5PC110 HEADER  "
	memcpy(&Buf[0], SPL_HEADER, SPL_HEADER_SIZE);
	//讀源bin到buffer[16]
	nbytes = fread(Buf + SPL_HEADER_SIZE, 1, count, fp);
	if ( nbytes != count )
	{
		printf("source file read error\n");
		free(Buf);
		fclose(fp);
		return -1;
	}
	fclose(fp);

	//計算校驗和
 	//從第16byte開始統計buffer中共有幾個1
	//從第16byte開始計算,把buffer中所有的字節數據加和起來得到的結果
	a = Buf + SPL_HEADER_SIZE;
	for(i = 0, checksum = 0; i < IMG_SIZE - SPL_HEADER_SIZE; i++)
		checksum += (0x000000FF) & *a++;
	//將校驗和保存在buffer[8~15]
	a = Buf + 8;							// Buf是210.bin的起始地址,+8表示向後位移2個字,也就是說寫入到第3個字
	*( (unsigned int *)a ) = checksum;

	//拷貝buffer中的內容到目的bin
	//打開目的bin
	fp = fopen(argv[2], "wb");
	if (fp == NULL)
	{
		printf("destination file open error\n");
		free(Buf);
		return -1;
	}
	//將16k的buffer拷貝到目的bin中
	a = Buf;
	nbytes	= fwrite( a, 1, BufLen, fp);
	if ( nbytes != BufLen )
	{
		printf("destination file write error\n");
		free(Buf);
		fclose(fp);
		return -1;
	}

	free(Buf);
	fclose(fp);

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