嵌入式知识-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;
}