《4.ARM裸机第四部分-GPIO和LED》

ARM裸机 专栏收录该内容
14 篇文章 0 订阅

转自 https://edu.csdn.net/lecturer/505 朱老师物联网大讲堂
《4.ARM裸机第四部分-GPIO和LED》

第一部分、章节目录
1.4.1.裸机实验体验之usb启动配合dnw工具下载
1.4.2.裸机实验体验之SD卡下载
1.4.3.自己动手安装交叉编译工具链1
1.4.4.自己动手安装交叉编译工具链2
1.4.5.Makefile大侠隆重登场
1.4.6.mkv210_image.c文件详解1
1.4.7.mkv210_image.c文件详解2
1.4.8.一步步点亮LED1_硬件工作原理及原理图查阅
1.4.9.一步步点亮LED2_数据手册查阅及相关寄存器浏览
1.4.10.一步步点亮LED3_从零开始手写汇编点亮LED
1.4.11.一步步点亮LED4_使用位运算实现复杂点亮要求
1.4.12.一步步点亮LED5_汇编编写延时函数并实现LED闪烁效果
1.4.13.一步步点亮LED6_再难一点的流水灯效果
1.4.14.反汇编工具objdump的使用简介

第二部分、章节介绍
1.4.1.裸机实验体验之usb启动配合dnw工具下载
本节的主要目的是学会从usb启动,然后使用dnw工具下载裸机程序bin文件到开发板内部SRAM执行。学完本节要求大家能够使用usb启动方式进行裸机程序调试,以方便后面测试自己写的代码(本节会提供我编译好的led.bin)。
1.4.2.裸机实验体验之SD卡下载
本节的主要目的是学会设置开发板从SD2启动(需要事先将板载SD0通道的iNand上的bootloader破坏掉),并且学会在Windows中使用烧录软件、linux中使用dd命令2种方式来制作启动SD卡。学完本节要求大家能够使用SD卡启动方式进行裸机程序的调试,以方便后面测试自己写的代码(本节会提供我编译好的led.bin)。
1.4.3.自己动手安装交叉编译工具链1
本节首先介绍linux中装软件和windows中的不同,然后手把手教大家从零开始自己动手安装交叉编译工具链并作测试。
1.4.4.自己动手安装交叉编译工具链2
本节接上节讲解如何将安装的交叉编译工具链导出到环境变量,并且为工具链制作arm-linux-符号链接。
1.4.5.Makefile大侠隆重登场
本节给大家引入Makefile,并且ubuntu环境下简单介绍Makefile的书写,目标、依赖等基本概念,最后分析了我们在裸机程序中使用到的Makefile。本节学完要求大家理解Makefile的基本用法,会自己根据需要修改Makefile。
1.4.6.mkv210_image.c文件详解1
本节回顾第三部分中讲到的S5PV210启动知识,并且分析SD卡启动时头信息的技术要求,然后引入mkv210_image.c文件并作简单分析。
1.4.7.mkv210_image.c文件详解2
本节接上节继续详细分析mkv210_image.c文件的技术细节,目的是使大家深入理解本文件中C程序的工作原理和实现技巧。
1.4.8.一步步点亮LED1_硬件工作原理及原理图查阅
本节从LED工作原理讲起,通过查阅原理图分析点亮LED的原理和方法。
1.4.9.一步步点亮LED2_数据手册查阅及相关寄存器浏览
本节接上节内容,查阅SoC数据手册中GPIO寄存器部分,找到板子上LED对应的GPIO并分析操作方法。
1.4.10.一步步点亮LED3_从零开始手写汇编点亮LED
本节开始写我们第一个汇编程序,从零开始用最少的代码点亮LED,并且使用之前讲过的Makefile编译,然后使用之前实践过的下载方法下载运行测试。至此,本章点亮LED的目标已经实现。
1.4.11.一步步点亮LED4_使用位运算实现复杂点亮要求
本节以上节的汇编代码为基础,进行必要修改,使用位运算的技巧来实现一些复杂的电灯要求(譬如隔一个亮一个)。学完本节要求大家对位运算有一定掌握,并基本掌握使用位运算来操作SoC寄存器
1.4.12.一步步点亮LED5_汇编编写延时函数并实现LED闪烁效果
本节接上节并继续复杂化。引入汇编编写的延时函数,并通过延时达到LED闪烁显示的效果。本节学习的目的是加深大家对汇编编程的理解,学会用汇编来写函数并调用之,为以后编写复杂汇编程序打基础。
1.4.13.一步步点亮LED6_再难一点的流水灯效果
本节是点亮LED的最后一节了,实现的效果是流水灯(跑马灯)。有了以上章节的学习,本节任务应该不难实现。
1.4.14.反汇编工具objdump的使用简介
本节介绍交叉编译工具链中的反汇编工具objdump。该工具是我们后面理解和分析链接地址、链接脚本的利器,在这里给大家先认识下,方便以后引入使用。

第三部分、随堂记录
1.4.1.裸机实验体验之usb启动配合dnw工具下载
1.4.1.1、背景知识介绍
回顾S5PV210的启动方式,必须将OM5打到VCC,才能从USB启动。
S5PV210的启动过程:开机时先执行内部的iROM中的BL0,然后BL0做了一系列的初始化后,再读取外部OMpin的设置来确定用户选择了从哪里启动。当检测到我们设置的是USB启动时,S5PV210就会从USB OTG接口试图连接主机进行下载启动。
1.4.1.2、dnw工具介绍
dnw是一个软件,是三星公司编写的,这个软件的功能是通过USB线连接开发板和电脑主机,然后从主机下载文件镜像到开发板中去烧录系统。
dnw软件使用注意1:dnw是需要装usb驱动的,驱动在“X210光盘资料\A盘\tools\USB驱动”目录中
dnw软件使用注意2:dnw使用时通过usb线下载,所以一定要插USB线。
dnw软件使用注意3:dnw下载时需要设置dnw下载内存地址。在dnw软件的菜单“Configuration”中设置
Download Address为0xd0020010,确认即可。
1.4.1.3、dnw驱动安装
X210开发板使用了软开关,但是我们这里还没到操作系统没去处理开关,所以在整个裸机实验中必须手工按下POWER键才能保持开机,只要手一抬起来就关机了····
dnw驱动装好的标志是:开发板开机从usb启动后,设备管理器中显示已经安装的设备,并且关键是dnw工具中USB:OK
1.4.1.4、裸机程序下载地址设置
从usb启动做裸机实验时,因为不需要16字节的校验头,所以直接下载到0xd0020010
1.4.1.5、usb启动裸机实验总结
usb启动方式主要是用来调试程序的,其实分析S5PV210即可知道,我们这里是把裸机程序当作BL1来使用了。

扩充知识:Win7 X64版本驱动安装非常麻烦,因为微软启用了USB设备驱动签名政策。

1.4.2.裸机实验体验之SD卡下载
1.4.2.1、背景知识
一般情况下,用USB下载来调试裸机程序比较方便;但是有时候电脑使用dnw会频繁蓝屏,这时候用SD卡下载调试是不错选择。
把OM5打开GND,以从SD通道启动。
从SD启动时会先从iNand(SD0)启动执行,当iNand启动做校验和时失败才会转为启动SD2。而我们做裸机实验时是通过SD2来提供裸机程序镜像的,因此需要先破坏内部iNand的uboot才可以强迫开发板从SD2启动去执行我们的裸机程序。
1.4.2.2、擦除开发板iNand中的uboot的方法
在linux和android系统下,擦除uboot的方法:
busybox dd if=/dev/zero of=/dev/block/mmcblk0 bs=512 seek=1 count=1 conv=sync
sync
在uboot底下如何擦除uboot:movi write u-boot 0x30000000
1.4.2.2、Windows下制作启动SD卡
方法等同于我们第三部分讲过的SD卡刷机时的操作
1.4.2.3、linux下制作启动SD卡
后面章节再演示。

总结:SD卡启动和usb启动优劣势对比:如果你的电脑本身支持usb启动下载而且不蓝屏,建议以后做实验用usb下载调试;

1.4.3.自己动手安装交叉编译工具链1
1.4.3.1、Windows中装软件的特点
Windows中装软件使用安装包,安装包解压后有2种情况:一种是一个安装文件(.exe .msi),双击进行安装,下一步直到安装完毕。安装完毕后会在桌面上生成快捷方式,我们平时使用快捷方式来启动这些程序;另一种是所谓的绿色软件、免安装软件。这种不用安装,直接解压开里面就有exe可以直接双击执行。
1.4.3.2、linux中装软件的特点
linux中安装软件比windows中复杂。linux中安装软件一般有以下几种方法:
第一种:在线安装。譬如ubuntu中使用apt-get install vim来安装vim软件。
第二种:自己下载安装包来安装。这种方式的缺陷就是你不知道你下载的安装包和你的系统是否匹配。
第三种:最装逼的一种方式,就是源代码安装。
总结:我们安装交叉编译工具链(arm-linux-gcc)实际采用第二种安装方式。
1.4.3.3、交叉编译工具链的选择
我们选择交叉编译工具链的原则:和我们所使用的目标平台(给哪款SoC编程)尽量去匹配。譬如我们开发S5PV210的程序就是用arm-2009q3这个版本,因为三星官方在开发S5pv210时就使用这个版本的交叉编译工具链,这样可以最大限度的避免稀奇古怪的问题出现。
1.4.3.4、交叉编译工具链的安装
步骤1:打开虚拟机,在/usr/local/下创建/usr/local/arm文件夹
步骤2:先将安装包从Windows中弄到linux中去。可以用共享文件夹,也可以用Samba,也可以cuteftp。
步骤3:解压。tar -jxvf arm-2009q3.tar.bz2
到此相当于程序已经安装完毕,真正的应用程序安装在/usr/local/arm/arm-2009q3/bin目录下

注:linux中的目录管理方法。技术角度来讲,linux中所有目录性质都是一样的,所以技术角度来讲我们把软件安装到哪里都行。但是因为如果胡乱放置,将来程序可能不好找。所以久而久之大家就总结了一个文件放置的一般定义,譬如说/bin目录放置一些系统自带的用户使用的应用程序,/sbin目录下存放的是系统自带的系统管理方面的应用程序。
那我们装软件放在哪里?一般都在/usr目录下。我们安装arm-linux-gcc,就在/usr/local/底下创建一个arm文件夹,然后装到里面。
1.4.3.5、安装后的测试
到真正的应用程序的安装目录下(也就是/usr/local/arm/arm-2009q3/bin),去执行arm-linux-gcc -v
执行方法是:./arm-none-linux-gnueabi-gcc -v
执行后可以得到一长串输出,其中有“gcc version 4.4.1 ”字样,即表示安装成功。

1.4.4.自己动手安装交叉编译工具链2
1.4.3.1、环境变量的意义
环境变量就是操作系统的全局变量。每一个环境变量对操作系统来说都是唯一的,名字和所代表的意义都是唯一的。linux系统可以有很多个环境变量。其中有一部分是linux系统自带的,还有一些是我们自己来扩充的。我们这里涉及到的一个环境变量是
PATH。PATH这个环境变量是系统自带的,它的含义就是系统在查找可执行程序时会搜索的路径范围。
1.4.3.2、将工具链导出到环境变量
export PATH=/usr/local/arm/arm-2009q3/bin:(美元符号)PATH
在一个终端中执行以上命令后,该终端中就可以直接使用arm-linux-gcc了,但是只要关掉这个终端再另外打开一个立马就不行了。原因是我们本次终端中执行时的操作只是针对本终端,以后再打开的终端并未被执行过这个命令所以没导出。
解决方案是在~/.bashrc中,添加export PATH=/usr/local/arm/arm-2009q3/bin:$PATH 即可。
注意:我们导出这个环境变量是在当前用户,如果你登录时在其他用户下是没用的。

1.4.3.3、为工具链创建arm-linux-xxx符号链接
ln arm-none-linux-gnueabi-addr2line -s arm-linux-addr2line

1.4.5.Makefile大侠隆重登场
1.4.5.1、为什么需要Makefile
Makefile是用来管理工程的。
在一个正式的软件项目中,由很多个.c和.h文件构成,此时如果直接在命令行编译,就会像这样:gcc a.c b.c c.c d.c e.c f.c g.c -o exe 每次编译都要输入一堆东西很麻烦,这个问题严重影响工作效率,怎么办?Makefile来解决
1.4.5.2、一个简单的Makefile示例
见光盘下载文件夹下面的 随堂代码/1.4.5/Makefile1和Makefile2
1.4.5.3、Makefile中的一些基本概念
目标:目标定格写,后面是冒号(冒号后面是依赖)
依赖:依赖是用来产生目标的原材料。
命令:命令前面一定是Tab,不能是定格,也不能说多个空格。命令就是要生成那个目标需要做的动作。
1.4.5.4、Makefile的基本工作原理
其一,当我们执行 make xx 的时候,Makefile会自动执行xx这个目标下面的命令语句。
其二,当我们make xx的时候,是否执行命令是取决于依赖的。依赖如果成立就会执行命令,否则不执行。
其三,我们直接执行make 和make 第一个目标 效果是一样的。(第一个目标其实就是默认目标)
1.4.5.5、ARM裸机中用到的Makefile介绍

1.4.5.6、进一步学习Makefile的资料
我们学习Makefile的思路就是:先学会基本的概念和应用,先理解Makefile的概念和使用方法、工作原理。先自己会写简单的Makefile来管理工程。一般先学到这里就可以了,更深入的内容可以随同稍后的课程一起来学习,我们讲到课程的时候会再次提及并且逐步深入。
对于我们有一定基础的同学,同时还有时间,可以深入学习Makefile,看《跟我一起学Makefile》(作者:陈皓)

1.4.6.mkv210_image.c文件详解1
2.4.6.1、mkv210_image.c的使用演示
裸机程序中的Makefile(实际上真正的项目的Makefile都是这样的)是把程序的编译和链接过程分开的。(平时我们用gcc a.c -o exe这种方式来编译时,实际上把编译和链接过程一步完成了。在内部实际上编译和链接永远是分开独立进行的,编译要使用编译器gcc,链接要使用链接器ld)
链接器得到led.elf其实就是我们的可执行程序,(如果是在操作系统下,这个led.elf就可以执行了)但是在嵌入式裸机中我们需要的是可以烧写的文件(可烧写的文件就叫镜像image),因此我们需要用这个led.elf为原材料来制作镜像,制作工具是交叉编译工具链中的arm-linux-objcopy
我们使用arm-linux-objdump工具进行反编译(反汇编),反汇编其实就是把编译后的elf格式的可执行程序给反过来的到对应的汇编程序,的到它的汇编源代码。我们使用反汇编主要是用来学习,见本部分最后一节。
mkv210_image.c这个程序其实最终不是在开发板上执行的,而是在主机linux(就是用来执行make对整个项目进行编译的那个机器)中执行的,因此编译这个程序用gcc而不是用arm-linux-gcc。这个.c文件编译后得到一个可执行程序mkmini210,目的是通过执行这个mkmini210程序而由led.bin得到210.bin。(210.bin是通过SD卡启动时的裸机镜像,这个镜像需要由led.bin来加工的到,加工的具体方法和原理要看mkv210_image.c)

1.4.6.2、背景知识:S5PV210的启动过程回顾
分析启动过程可知;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,如果不对应则启动失败(会转入执行2st启动,即SD2启动。如果这里已经是2st启动了,这里校验通不过就死定了)。
1.4.6.3、mkv210_image.c的作用:为BL1添加校验头
我们编译链接时只得到了led.bin,这个210.bin的得到和交叉编译工具链是完全无关的。由led.bin得到210.bin的过程是三星的S5PV210所特有的,因此需要我们自己去完成,为此我们写了mkv210_image.c来完成。
1.4.6.4、整个程序工作流分析
整个程序中首先申请一个16KB大小的buffer,然后把所有内容按照各自的位置填充进去,最终把填充好的buffer写入到一个文件(名叫210.bin)就形成了我们想要的镜像。

//makefile
led.bin: start.o 
	arm-linux-ld -Ttext 0x0 -o led.elf $^
	arm-linux-objcopy -O binary led.elf led.bin
	arm-linux-objdump -D led.elf > led_elf.dis
	gcc mkv210_image.c -o mkx210
	./mkx210 led.bin 210.bin
	
%.o : %.S
	arm-linux-gcc -o $@ $< -c

%.o : %.c
	arm-linux-gcc -o $@ $< -c 

clean:
	rm *.o *.elf *.bin *.dis mkx210 -f	
#!/bin/sh
sudo dd iflag=dsync oflag=dsync if=210.bin of=/dev/sdb seek=1

.globl _start

_start:
	 设置GPJ2CON的bit[0:15],配置GPJ2_0/1/2/3引脚为输出功能
	// 设置GPJ0CON的bit[12:23],配置GPJ0_3/4/5引脚为输出功能
	ldr r1, =0xE0200240 					
	ldr r0, =0x00111000
	str r0, [r1]

	mov r2, #0x1000
	
led_blink:
	 设置GPJ2DAT的bit[0:3],使GPJ2_0/1/2/3引脚输出低电平,LED亮
	// 设置GPJ0DAT的bit[3:5],使GPJ0_3/4/5引脚输出低电平,LED亮
	ldr r1, =0xE0200244 					
	mov r0, #0
	str r0, [r1]

	// 延时
	bl delay							

	 设置GPJ2DAT的bit[0:3],使GPJ2_0/1/2/3引脚输出高电平,LED灭
	// 设置GPJ0DAT的bit[3:5],使GPJ0_3/4/5引脚输出高电平,LED灭
	ldr r1, =0xE0200244 					
	mov r0, #0x38
	str r0, [r1]

	// 延时
	bl delay	

	sub r2, r2, #1
	cmp r2,#0
	bne led_blink


halt:
	b halt


delay:
	mov r0, #0x900000
delay_loop:
	cmp r0, #0
	sub r0, r0, #1
	bne delay_loop
	mov pc, lr


/*
 * 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)
#define IMG_SIZE                (16*1024)
#define SPL_HEADER_SIZE         16
//#define SPL_HEADER              "S5PC110 HEADER  "
#define SPL_HEADER              "****************"

int main (int argc, char *argv[])
{
	FILE		*fp;
	char		*Buf, *a;
	int		BufLen;
	int		nbytes, fileLen;
	unsigned int	checksum, count;
	int		i;
	
	// 1. 3个参数
	if (argc != 3)
	{
		printf("Usage: %s <source file> <destination file>\n", argv[0]);
		return -1;
	}

	// 2. 分配16K的buffer
	BufLen = BUFSIZE;
	Buf = (char *)malloc(BufLen);
	if (!Buf)
	{
		printf("Alloc buffer failed!\n");
		return -1;
	}

	memset(Buf, 0x00, BufLen);

	// 3. 读源bin到buffer
	// 3.1 打开源bin
	fp = fopen(argv[1], "rb");
	if( fp == NULL)
	{
		printf("source file open error\n");
		free(Buf);
		return -1;
	}
	// 3.2 获取源bin长度
	fseek(fp, 0L, SEEK_END);								// 定位到文件尾
	fileLen = ftell(fp);									// 得到文件长度
	fseek(fp, 0L, SEEK_SET);								// 再次定位到文件头
	// 3.3 源bin长度不得超过16K-16byte
	count = (fileLen < (IMG_SIZE - SPL_HEADER_SIZE))
		? fileLen : (IMG_SIZE - SPL_HEADER_SIZE);
	// 3.4 buffer[0~15]存放"S5PC110 HEADER  "
	memcpy(&Buf[0], SPL_HEADER, SPL_HEADER_SIZE);
	// 3.5 读源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);

	// 4. 计算校验和
 	// 4.1 从第16byte开始统计buffer中共有几个1
	// 4.1 从第16byte开始计算,把buffer中所有的字节数据加和起来得到的结果
	a = Buf + SPL_HEADER_SIZE;
	for(i = 0, checksum = 0; i < IMG_SIZE - SPL_HEADER_SIZE; i++)
		checksum += (0x000000FF) & *a++;
	// 4.2 将校验和保存在buffer[8~15]
	a = Buf + 8;							// Buf是210.bin的起始地址,+8表示向后位移2个字,也就是说写入到第3个字
	*( (unsigned int *)a ) = checksum;

	// 5. 拷贝buffer中的内容到目的bin
	// 5.1 打开目的bin
	fp = fopen(argv[2], "wb");
	if (fp == NULL)
	{
		printf("destination file open error\n");
		free(Buf);
		return -1;
	}
	// 5.2 将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;
}

1.4.7.mkv210_image.c文件详解2
1.4.7.1、代码详解
第1步:检验用户传参是不是3个。
第2步:分配16K Bbuffer并且填充为0.
第3步:·········

1.4.7.2、main函数两个形参的作用
main函数接收2个形参:argc和argv。
argc是用户(通过命令行来)执行这个程序时,实际传递的参数个数。注意这个个数是包含程序执行本身的
argv是一个字符串数组,这个数组中存储的字符串就是一个个的传参。
譬如我们执行程序时使用./mkx210 led.bin 210.bin
则argc = 3
则argv[0] = “./mkx210” argv[1] = led.bin argv[2] = 210.bin
1.4.7.3、glibc读写文件接口
linux中要读取一个文件,可以使用fopen打开文件,fread读取文件,读完之后fclose关闭文件。
要写文件用fwrite来写。这些函数是glibc的库函数,在linux中用man 3 可以查找。
如果你本身就知道这些函数的用法,只是记不起来可以man查找;如果你本身根本就不会用这些接口,建议先去baidu。
1.4.7.4、校验和的计算方法
算法:校验和其实就是需要校验的内存区域中,所有内存中的内容按照字节为单位来进行相加,最终相加的和极为校验和。
实现时大家要注意指针的类型为char *

/*
 * 文件名:	led.s	
 * 作者:	朱老师
 * 描述:	这是我们一步步点亮LED教程的自己写的第一个裸机程序
 */

_start:
	// 第一步:把0x11111111写入0xE0200240(GPJ0CON)位置
	ldr r0, =0x11111111			// 从后面的=可以看出用的是ldr伪指令,因为需要编译器来判断这个数
	ldr r1, =0xE0200240			// 是合法立即数还是非法立即数。一般写代码都用ldr伪指令
	str r0, [r1]				// 寄存器间接寻址。功能是把r0中的数写入到r1中的数为地址的内存中去

	// 第二步:把0x0写入0xE0200244(GPJ0DAT)位置
	ldr r0, =0x0
	ldr r1, =0xE0200244
	str r0, [r1]				// 把0写入到GPJ0DAT寄存器中,引脚即输出低电平,LED点亮

flag:							// 这两行写了一个死循环。因为裸机程序是直接在CPU上运行的,CPU会
	b flag						// 逐行运行裸机程序直到CPU断电关机。如果我们的程序所有的代码都
	
led.bin: led.o 
	arm-linux-ld -Ttext 0x0 -o led.elf $^
	arm-linux-objcopy -O binary led.elf led.bin
	arm-linux-objdump -D led.elf > led_elf.dis
	gcc mkv210_image.c -o mkx210
	./mkx210 led.bin 210.bin
	
%.o : %.S
	arm-linux-gcc -o $@ $< -c

%.o : %.c
	arm-linux-gcc -o $@ $< -c 

.PHONY clean
clean:
	rm *.o *.elf *.bin *.dis mkx210 -f

	
	
/*
 * 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)
#define IMG_SIZE                (16*1024)
#define SPL_HEADER_SIZE         16
//#define SPL_HEADER              "S5PC110 HEADER  "
#define SPL_HEADER              "****************"

int main (int argc, char *argv[])
{
	FILE		*fp;
	char		*Buf, *a;
	int		BufLen;
	int		nbytes, fileLen;
	unsigned int	checksum, count;
	int		i;
	
	// 1. 3个参数
	if (argc != 3)
	{
		printf("Usage: %s <source file> <destination file>\n", argv[0]);
		return -1;
	}

	// 2. 分配16K的buffer
	BufLen = BUFSIZE;
	Buf = (char *)malloc(BufLen);
	if (!Buf)
	{
		printf("Alloc buffer failed!\n");
		return -1;
	}

	memset(Buf, 0x00, BufLen);

	// 3. 读源bin到buffer
	// 3.1 打开源bin
	fp = fopen(argv[1], "rb");
	if( fp == NULL)
	{
		printf("source file open error\n");
		free(Buf);
		return -1;
	}
	// 3.2 获取源bin长度
	fseek(fp, 0L, SEEK_END);								// 定位到文件尾
	fileLen = ftell(fp);									// 得到文件长度
	fseek(fp, 0L, SEEK_SET);								// 再次定位到文件头
	// 3.3 源bin长度不得超过16K-16byte
	count = (fileLen < (IMG_SIZE - SPL_HEADER_SIZE))
		? fileLen : (IMG_SIZE - SPL_HEADER_SIZE);
	// 3.4 buffer[0~15]存放"S5PC110 HEADER  "
	memcpy(&Buf[0], SPL_HEADER, SPL_HEADER_SIZE);
	// 3.5 读源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);

	// 4. 计算校验和
 	// 4.1 从第16byte开始统计buffer中共有几个1
	// 4.1 从第16byte开始计算,把buffer中所有的字节数据加和起来得到的结果
	a = Buf + SPL_HEADER_SIZE;
	for(i = 0, checksum = 0; i < IMG_SIZE - SPL_HEADER_SIZE; i++)
		checksum += (0x000000FF) & *a++;
	// 4.2 将校验和保存在buffer[8~15]
	a = Buf + 8;							// Buf是210.bin的起始地址,+8表示向后位移2个字,也就是说写入到第3个字
	*( (unsigned int *)a ) = checksum;

	// 5. 拷贝buffer中的内容到目的bin
	// 5.1 打开目的bin
	fp = fopen(argv[2], "wb");
	if (fp == NULL)
	{
		printf("destination file open error\n");
		free(Buf);
		return -1;
	}
	// 5.2 将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;
}

1.4.8.一步步点亮LED1_硬件工作原理及原理图查阅
1.4.8.1、LED物理特性介绍
LED本身有2个接线点,一个是LED的正极,一个是LED的负极。LED这个硬件的功能就是点亮或者不亮,物理上想要点亮一颗LED只需要给他的正负极上加正电压即可,要熄灭一颗LED只需要去掉电压即可。
1.4.8.2、查阅原理图了解板载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)
1.4.8.3、分析如何点亮及熄灭LED(GPIO)
分析:LED点亮的要求是:正极和负极之间有正向电压差。
思考:在开发板上如何为LED制造这个电压差让它点亮呢?
解答:因为正极已经定了(3.3V),而负极接在了SoC的引脚上,可以通过SoC中编程来控制负极的电压值,因此我们可以通过程序控制负极输出低电平(0V),这样在正负极上就有了压差,LED即可点亮。

/*
 * 文件名:	led.s	
 * 作者:	朱老师
 * 描述:	这是我们一步步点亮LED教程的自己写的第一个裸机程序
 */

_start:
	// 第一步:把0x11111111写入0xE0200240(GPJ0CON)位置
	ldr r0, =0x11111111			// 从后面的=可以看出用的是ldr伪指令,因为需要编译器来判断这个数
	ldr r1, =0xE0200240			// 是合法立即数还是非法立即数。一般写代码都用ldr伪指令
	str r0, [r1]				// 寄存器间接寻址。功能是把r0中的数写入到r1中的数为地址的内存中去

	// 第二步:把0x0写入0xE0200244(GPJ0DAT)位置
	ldr r0, =0x0
	ldr r1, =0xE0200244
	str r0, [r1]				// 把0写入到GPJ0DAT寄存器中,引脚即输出低电平,LED点亮

flag:							// 这两行写了一个死循环。因为裸机程序是直接在CPU上运行的,CPU会
	b flag						// 逐行运行裸机程序直到CPU断电关机。如果我们的程序所有的代码都
								// 执行完了CPU就会跑飞(跑飞以后是未定义的,所以千万不能让CPU
								// 跑飞),不让CPU跑飞的办法就是在我们整个程序执行完后添加死循环
//Makefile
led.bin: led.o 
	arm-linux-ld -Ttext 0x0 -o led.elf $^
	arm-linux-objcopy -O binary led.elf led.bin
	arm-linux-objdump -D led.elf > led_elf.dis
	gcc mkv210_image.c -o mkx210
	./mkx210 led.bin 210.bin
	
%.o : %.S
	arm-linux-gcc -o $@ $< -c

%.o : %.c
	arm-linux-gcc -o $@ $< -c 

.PHONY clean
clean:
	rm *.o *.elf *.bin *.dis mkx210 -f	

1.4.9.一步步点亮LED2_数据手册查阅及相关寄存器浏览
1.4.9.1、GPIO概念的引入
GPIO:general purpose input output 通用输入输出
GPIO就是芯片的引脚(芯片上的引脚有些不是GPIO,只有一部分是),作为GPIO的这类引脚,他的功能和特点是可以被编程控制它的工作模式,也可以编程控制他的电压高低等。
通过之前的分析我们知道,我们设计电路时就把LED接在了一个GPIO上,这样我们就可以通过编程控制GPIO的模式和输入输出值来操控LED亮还是灭;如果你当时设计电路时把LED接在非GPIO上那就不可能了。
1.4.9.2、阅读数据手册中有关部分
当我们想要通过编程操控GPIO来操作LED时,我们首先需要通读一下S5PV210的数据手册中有关于GPIO的部分,这部分在数据手册的Section2.2中。

1.4.9.3、GPIO相关的寄存器介绍
回忆下之前说过的,软件操作硬件的接口是:寄存器。
我们当前要操作的硬件是LED,但是LED实际是通过GPIO来间接控制的,所以当前我们实际要操作的设备其实是SoC的GPIO。要操作这些GPIO,必须通过设置他们的寄存器。

查阅数据手册可知,GPJ0相关的寄存器有以下:
GPJ0CON, (GPJ0 control)GPJ0控制寄存器,用来配置各引脚的工作模式	
GPJ0DAT, (GPJ0 data)当引脚配置为input/output模式时,寄存器的相应位和引脚的电平高低相对应。
GPJ0PUD, (pull up down)控制引脚内部弱上拉、下拉
GPJ0DRV, (driver)配置GPIO引脚的驱动能力
GPJ0CONPDN,(记得是低功耗模式下的控制寄存器)
GPJ0PUDPDN  (记得是低功耗模式下的上下拉寄存器)
注:在驱动LED点亮时,应该将GPIO配置为output模式。

实际上真正操控LED的硬件,主要的有:GPJ0CON, GPJ0DAT 这么2个。
如何点亮LED,编程的步骤是:
1、操控GPJ0CON寄存器中,选中output模式
2、操控GPJ0DAT寄存器,相应的位设置为0

1.4.10.一步步点亮LED3_从零开始手写汇编点亮LED
1.4.10.1、GPxCON、GPxDAT寄存器分析
GPJ0端口一共有8个引脚,分别记住:GPJ0_0 ~ GPJ0_7,相关重要寄存器就是GPJ0CON和GPJ0DAT
GPJ0CON寄存器中设置8个引脚的工作模式(32/8=4,每个引脚可以分到4位,譬如GPJ0_0对应的bit位为bit0bit3,GPJ0_3对应的位为bit12bit15。工作方法是:给相应的寄存器位写入相应的值,该引脚硬件就会按照相应的模式去工作。譬如给bit12~bit15写入0b0001,GPJ0_3引脚就成为输出模式了)
1.4.10.2、从零开始写代码操作寄存器
需要哪些先决条件才能写呢?
1. 硬件接法和引脚:GPJ0_3 GPJ0_4 GPJ0_5 低电平亮/高电平灭
2. GPJ0CON(0xE0200240)寄存器和GPJ0DAT(0xE0200244)寄存器
3. 工程管理:Makefile等
根据以上分析,我们就知道代码的写法了,代码所要完成的动作就是:
把相应的配置数据写入相应的寄存器即可。
1.4.10.3、编译、下载、运行看结果
编译时用我们的工程管理,直接make编译得到led.bin和210.bin
下载运行可以用usb启动dnw下载;也可以用sd卡烧录下载,根据自己的情况用
一般都用usb下载,因为方便。如果电脑主板插上dnw会死机没法解决,那只有sd卡下载启动了。
注意:开发板上按下电源键之后4颗LED默认都是半亮的,当我们下载程序后其中3颗变的很亮,这说明我们的程序已经运行了。
1.4.10.4、总结和回顾(软件控制硬件思想、寄存器意义、原理图数据手册的作用)
软件到底是怎么控制硬件的?为什么程序一运行硬件就能跟着动?
软件编程控制硬件的接口就是:寄存器

/*
 * 文件名:	led.s	
 * 作者:	朱老师
 * 描述:	这是我们一步步点亮LED教程的自己写的第一个裸机程序
 */
 
#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 .							// .代表当前这一句指令的地址,这个就是高大上的死循环

1.4.11.一步步点亮LED4_使用位运算实现复杂点亮要求
上节回顾:代码写的更漂亮一些
1. 用宏定义来定义寄存器名字,再来操作。
2. 用 b . 来实现死循环
3. 用.global把_start链接属性改为外部,消除链接时的警告
1.4.11.1、问题提出:如何只点亮中间1颗(两边是熄灭的)LED
分析:程序其实就是写了GPJ0CON和GPJ0DAT这2个寄存器而已,功能更改也要从这里下手。
GPJ0CON寄存器不需要修改,GPJ0DAT中设置相应的输出值即可。
1.4.11.2、直接解法(不使用位运算)和它的弊端
GPJ0DAT = 0x28
代码见<3.led_s>
总结:1. 这样写可以完成任务。
2. 这样写有缺陷。缺陷就是需要人为的去计算这个特定的设置值,而且看代码的也不容易看懂。
解决方案:在写代码时用位运算去让编译器帮我们计算这个特定值。
1.4.11.3、常用位运算:与、或、非、移位
位与(&) 位或(|) 位非(取反 ~) 移位(左移<< 右移>>)
1.4.11.4、使用位运算实现功能
1<<3 等于 0b1000
1<<5 等于 0b100000
(1<<3)|(1<<5) 等于 0b101000
1.4.11.5、扩展一下:如何只熄灭中间1颗而点亮旁边2颗
ldr r0, =((0<<3) | (1<<4) | (0<<5))

/*
 * 文件名:	led.s	
 * 作者:	朱老师
 * 描述:	这是我们一步步点亮LED教程的自己写的第一个裸机程序
 */
 
#define GPJ0CON	0xE0200240
#define GPJ0DAT	0xE0200244

.global _start					// 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
	// 第一步:把所有引脚都设置为输出模式,代码不变
	ldr r0, =0x11111111			// 从后面的=可以看出用的是ldr伪指令,因为需要编译器来判断这个数
	ldr r1, =GPJ0CON			// 是合法立即数还是非法立即数。一般写代码都用ldr伪指令
	str r0, [r1]				// 寄存器间接寻址。功能是把r0中的数写入到r1中的数为地址的内存中去

	// 第二步:全部点亮
	ldr r0, =((0<<3) | (0<<4) | (0<<5))	// 清清楚楚的看到哪个灭,哪个是亮
	ldr r1, =GPJ0DAT
	str r0, [r1]				// 把0写入到GPJ0DAT寄存器中,引脚即输出低电平,LED点亮

	// 第三步:延时
	bl delay					// 使用bl进行函数调用
	
	// 第四步:全部熄灭
	ldr r0, =((1<<3) | (1<<4) | (1<<5))	// 清清楚楚的看到哪个灭,哪个是亮
	ldr r1, =GPJ0DAT
	str r0, [r1]		
	
	// 第五步:延时
	bl delay
	
	// 第六步:点亮
	ldr r0, =((0<<3) | (0<<4) | (0<<5))	// 清清楚楚的看到哪个灭,哪个是亮
	ldr r1, =GPJ0DAT
	str r0, [r1]
	
	// 第七步:全部熄灭
	ldr r0, =((1<<3) | (1<<4) | (1<<5))	// 清清楚楚的看到哪个灭,哪个是亮
	ldr r1, =GPJ0DAT
	str r0, [r1]		
	
	// 第八步:延时
	bl delay
	
	b .							// .代表当前这一句指令的地址,这个就是高大上的死循环

// 延时函数:函数名: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					// 函数调用返回

1.4.12.一步步点亮LED5_汇编编写延时函数并实现LED闪烁效果
1.4.12.1、闪烁效果原理分析
闪烁 = 亮 + 延时 + 灭 + 延时 + 亮 + 延时 ······
1.4.12.2、延时函数原理
在汇编中实现延时的方法:用一些没有目的的代码来执行消耗时间,达到延时的效果。
1.4.12.3、汇编编写延时函数
汇编编写延时函数的原理,用一个寄存器存放一个数字,然后在循环中每个循环里给数字减1,然后再判断这个数字的值是否为0.如果为0则停止循环,如果不为0则继续循环。
1.4.12.4、汇编编写及调用函数的方式
汇编中整个汇编的主程序是一个死循环,这个死循环是我们汇编程序的主体,类似于C中的main函数。其他函数必须写在这个主死循环程序的后面(死循环外),不然会出错。
汇编编写delay延时函数时,要注意函数的初始化和函数体的位置,不能把初始化写在了循环体内。
汇编中调用函数用bl指令,子函数中最后用mov pc, lr来返回。

/*
 * 文件名:	led.s	
 * 作者:	朱老师
 * 描述:	这是我们一步步点亮LED教程的自己写的第一个裸机程序
 */
 
#define GPJ0CON	0xE0200240
#define GPJ0DAT	0xE0200244

.global _start					// 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
	// 第一步:把所有引脚都设置为输出模式,代码不变
	ldr r0, =0x11111111			// 从后面的=可以看出用的是ldr伪指令,因为需要编译器来判断这个数
	ldr r1, =GPJ0CON			// 是合法立即数还是非法立即数。一般写代码都用ldr伪指令
	str r0, [r1]				// 寄存器间接寻址。功能是把r0中的数写入到r1中的数为地址的内存中去

flash:
	// 第二步:全部点亮
	ldr r0, =((0<<3) | (0<<4) | (0<<5))	// 清清楚楚的看到哪个灭,哪个是亮
	ldr r1, =GPJ0DAT
	str r0, [r1]				// 把0写入到GPJ0DAT寄存器中,引脚即输出低电平,LED点亮

	// 第三步:延时
	bl delay					// 使用bl进行函数调用
	
	// 第四步:全部熄灭
	ldr r0, =((1<<3) | (1<<4) | (1<<5))	// 清清楚楚的看到哪个灭,哪个是亮
	ldr r1, =GPJ0DAT
	str r0, [r1]		
	
	// 第五步:延时
	bl delay
	
	b 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					// 函数调用返回


1.4.13.一步步点亮LED6_再难一点的流水灯效果
1.4.13.1、流水灯原理分析
流水灯又叫跑马灯,实现的效果就是:挨着的LED一次点亮熄灭(同时只有1颗LED亮的)
1.4.13.2、流水灯编写(使用循环)
LED1亮延时 + LED2亮延时 + LED3亮延时 + 循环
1.4.13.3、复杂点的实现
用位取反操作符来轻松愉快的实现单颗LED点亮流水效果
1.4.13.4、总结:一步步写,根本不难
从一步一步点亮LED1开始到6,写了8个示例代码,一步步的实现了更复杂的效果,其间夹杂使用了位运算来给LED赋值,以实现想要的点亮效果。如果按部就班实际上非常简单。
编程操控一个硬件的步骤:1 分析硬件工作原理 2 分析原理图 3 分析数据手册 4 找到相关的SFR 5 写代码设置寄存器得到想要的效果
1.4.13.5
作业:1、板子上有4颗LED的(还有个在GPD0_1),大家编程把LED4也点亮、熄灭
2、用4颗LED实现流水灯

/*
 * 文件名:	led.s	
 * 作者:	朱老师
 * 描述:	流水灯
 */
 
#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		
	
	
	b 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					// 函数调用返回

1.4.14.反汇编工具objdump的使用简介
1.4.14.1、反汇编的原理&为什么要反汇编
arm-linux-objdump -D led.elf > led_elf.dis
objdump是gcc工具链中的反汇编工具,作用是由编译链接好的elf格式的可执行程序反过来得到汇编源代码
-D表示反汇编 > 左边的是elf的可执行程序(反汇编时的原材料),>右边的是反汇编生成的反汇编程序

反汇编的原因有以下:
1.逆向破解。
2.调试程序时,反汇编代码可以帮助我们理解程序(我们学习时使用objdump主要目的是这个),尤其是在理解链接脚本、链接地址等概念时。
3. 把C语言源代码编译链接生成的可执行程序反汇编后得到对应的汇编代码,可以帮助我们理解C语言和汇编语言之间的对应关系。非常有助于深入理解C语言。

1.4.14.2、反汇编文件的格式和看法
(汇编 assembly 反汇编 dissembly)
标号地址、标号名字、指令地址、指令机器码、指令机器码反汇编到的指令
扩展:ARM汇编中用地址池方式来实现非法立即数

1.4.14.3、初识指令地址
下载烧录执行的bin文件,内部其实是一条一条的指令机器码。这些指令每一条都有一个指令地址,这个地址是连接的时候ld给指定的(ld根据我们写的链接脚本来指定)
1.4.14.4、展望:反汇编工具帮助我们分析链接脚本
反汇编的时候得到的指令地址是链接器考虑了链接脚本之后得到的地址,而我们写代码时通过指定连接脚本来让链接器给我们链接合适的地址。
但是有时候我们写的链接脚本有误(或者我们不知道这个链接脚本会怎么样),这时候可以通过看反汇编文件来分析这个链接脚本的效果,看是不是我们想要的,如果不是可以改了再看。

外链内容:C语言位操作

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值