嵌入式知识-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;
}
发布了56 篇原创文章 · 获赞 76 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_42826337/article/details/104430982