一、一步步点亮LED
1. 硬件工作原理及原理图查阅
LED 本身有 2 个接线点,一个是 LED 的正极,一个是 LED 的负极。LED 这个硬件的功能就是点亮或者不亮,物理上想要点亮一颗 LED 只需要给他的正负极上加正电压即可,要熄灭一颗 LED 只需要去掉电压即可。
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)
3. 分析如何点亮及熄灭LED(GPIO)
分析:LED点亮的要求是:正极和负极之间有正向电压差。
思考:在开发板上如何为LED制造这个电压差让它点亮呢?
解答:因为正极已经定了( 3.3V ),而负极接在了 SoC 的引脚上,可以通过 SoC 中编程来控制负极的电压值,因此我们可以通过程序控制负极输出低电平(0V),这样在正负极上就有了压差,LED 即可点亮。
二、 数据手册查阅及相关寄存器浏览
1. GPIO概念的引入
GPIO:general purpose input output 通用输入输出。
GPIO 就是芯片的引脚(芯片上的引脚有些不是 GPIO,只有一部分是),作为 GPIO 的这类引脚,他的功能和特点是可以被编程控制它的工作模式,也可以编程控制他的电压高低等。
通过之前的分析我们知道,我们设计电路时就把 LED 接在了一个 GPIO 上,这样我们就可以通过编程控制 GPIO 的模式和输入输出值来操控 LED 亮还是灭;如果你当时设计电路时把 LED 接在非 GPIO 上那就不可能了。
2. 阅读数据手册中有关部分
当我们想要通过编程操控 GPIO 来操作 LED 时,我们首先需要通读一下 S5PV210 的数据手册中有关于 GPIO 的部分,这部分在数据手册的 Section2.2 中。
3. GPIO相关的寄存器介绍
回忆下之前说过的,软件操作硬件的接口是:寄存器。
我们当前要操作的硬件是 LED,但是 LED 实际是通过 GPIO 来间接控制的,所以当前我们实际要操作的设备其实是 SoC 的 GPIO。要操作这些 GPIO,必须通过设置他们的寄存器。
Each Port Group has 2 types of control registers.
One works in normal mode, and the other works in power down mode (STOP, DEEP-STOP,
SLEEP mode) .
Normal registers (For example, GPA0CON, GPA0DAT, GPA0PUD, and GPA0DRV) are the former,
and power down registers (For example, GPA0CONPDN, and GPA0PUDPDN) are the latter.
If, S5PV210 enter the power down mode, all configurations and Pull-down controls
are selected by power down registers.
查阅数据手册可知,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
三、从零开始手写汇编点亮LED
1. GPxCON、GPxDAT寄存器分析
GPJ0 端口一共有 8 个引脚,分别记住:GPJ0_0 ~ GPJ0_7,相关重要寄存器就是GPJ0CON 、GPJ0DAT。
GPJ0CON 寄存器中设置 8 个引脚的工作模式(32/8=4,每个引脚可以分到 4 位,譬如 GPJ0_0 对应的 bit 位为 bit0~bit3 ,GPJ0_3 对应的位为 bit12~bit15。工作方法是:给相应的寄存器位写入相应的值,该引脚硬件就会按照相应的模式去工作。譬如给 bit12~bit15 写入 0b0001,GPJ0_3 引脚就成为输出模式了)。
2. 从零开始写代码操作寄存器
需要哪些先决条件才能写呢?
- 硬件接法和引脚:GPJ0_3 GPJ0_4 GPJ0_5 低电平亮/高电平灭
- GPJ0CON(0xE0200240)寄存器和 GPJ0DAT(0xE0200244)寄存器
- 工程管理:Makefile等
根据以上分析,我们就知道代码的写法了,代码所要完成的动作就是:
把相应的配置数据写入相应的寄存器即可。
root@ubuntu:/home/aston/workspace/git_xxx# cat led.S
/*
* 文件名: led.S
* 作者: xxx
* 描述: 这是我们一步步点亮 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:
b flag //这两行写了一个死循环。因为裸机程序是直接在 CPU 上运行的, CPU 会
//逐行运行裸机程序直到 CPU 断电关机。如果我们的程序所有的代码都
//执行完了 CPU 就会跑飞(跑飞以后是未定义的,所以千万不能让 CPU
//跑飞),不让 CPU 跑飞的办法就是在我们整个程序执行完后添加死循环
root@ubuntu:/home/aston/workspace/git_xxx# ls
Makefile led.S mkgcc.sh* mkv210_image.c readme.txt write2sd* 说明.txt
root@ubuntu:/home/aston/workspace/git_xxx# make
arm-linux-gcc -o led.o led.S -c
arm-linux-ld -Ttext 0x0 -o led.elf led.o
arm-linux-ld: warning: cannot find entry symbol _start; defaulting to 00000000
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
root@ubuntu:/home/aston/workspace/git_xxx#
3. 编译、下载、运行看结果
编译时用我们的工程管理,直接 make 编译得到 led.bin 和 210.bin。
下载运行可以用 usb 启动 dnw 下载;也可以用 sd 卡烧录下载,根据自己的情况用。
一般都用 usb 下载,因为方便。如果电脑主板插上 dnw 会死机没法解决,那只有 sd 卡下载启动了。
注意:开发板上按下电源键之后4颗 LED 默认都是半亮的,当我们下载程序后其中 3 颗变的很亮,这说明我们的程序已经运行了。
4. 总结和回顾
(软件控制硬件思想、寄存器意义、原理图数据手册的作用)
软件到底是怎么控制硬件的?为什么程序一运行硬件就能跟着动?
软件编程控制硬件的接口就是:寄存器
四、使用位运算实现复杂点亮要求
上节回顾:代码写的更漂亮一些
- 用宏定义来定义寄存器名字,再来操作。
- 用 b . 来实现死循环
- 用.global把_start链接属性改为外部,消除链接时的警告
1. 问题提出:如何只点亮中间1颗(两边是熄灭的)LED
分析:程序其实就是写了 GPJ0CON 和 GPJ0DAT 这 2 个寄存器而已,功能更改也要从这里下手。
GPJ0CON 寄存器不需要修改,GPJ0DAT 中设置相应的输出值即可。
2. 常用位运算:与、或、非、移位
位与(&) 位或(|) 位非(取反 ~) 移位(左移<< 右移>>)
使用位运算实现功能
1<<3 等于 0b1000
1<<5 等于 0b100000
(1<<3)|(1<<5) 等于 0b101000
扩展一下:如何只熄灭中间1颗而点亮旁边2颗
ldr r0, =((0<<3) | (1<<4) | (0<<5))
源代码
root@ubuntu:/home/aston/workspace/git_xxx# cat led.S
/*
* 文件名: led.S
* 作者: xxx
* 描述: 这是我们一步步点亮 LED 教程的自己写的第一个裸机程序
*/
#define GPJ0CON 0xE0200240
#define GPJ0DAT 0xE0200244
.global _start //解决 make 编译警告: arm-linux-ld: warning: cannot find entry symbol _start; defaulting to 00000000
// 把 _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) | (0 << 4) | (1 << 5))
ldr r1, =GPJ0DAT
str r0, [r1] //把 0 写入到 GPJ0DAT 寄存器中,引脚即输出低电平,LED 点亮
b . // . 代表当前这一句指令的地址,这个就是高大上的死循环
root@ubuntu:/home/aston/workspace/git_xxx# ls
led.S Makefile mkgcc.sh mkv210_image.c readme.txt write2sd 说明.txt
root@ubuntu:/home/aston/workspace/git_xxx# make
arm-linux-gcc -o led.o led.S -c
arm-linux-ld -Ttext 0x0 -o led.elf led.o
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
root@ubuntu:/home/aston/workspace/git_xxx#
五、汇编编写延时函数并实现LED闪烁效果
1. 延时函数原理
在汇编中实现延时的方法:用一些没有目的的代码来执行消耗时间,达到延时的效果。
2. 汇编编写延时函数
汇编编写延时函数的原理,用一个寄存器存放一个数字,然后在循环中每个循环里给数字减 1,然后再判断这个数字的值是否为0 .如果为 0 则停止循环,如果不为 0 则继续循环。
3. 汇编编写及调用函数的方式
汇编中整个汇编的主程序是一个死循环,这个死循环是我们汇编程序的主体,类似于 C 中的 main 函数。其他函数必须写在这个主死循环程序的后面(死循环外),不然会出错。
汇编编写 delay 延时函数时,要注意函数的初始化和函数体的位置,不能把初始化写在了循环体内。
汇编中调用函数用 bl 指令,子函数中最后用 mov pc, lr 来返回。
4. 源代码
root@ubuntu:/home/aston/workspace/git_xxx# ls
led.S Makefile mkgcc.sh mkv210_image.c readme.txt write2sd 说明.txt
root@ubuntu:/home/aston/workspace/git_xxx# cat led.S
/*
* 文件名: led.S
* 作者: xxxx
* 描述: 这是我们一步步点亮 LED 教程的自己写的第一个裸机程序
*/
#define GPJ0CON 0xE0200240
#define GPJ0DAT 0xE0200244
.global _start //解决 make 编译警告: arm-linux-ld: warning: cannot find entry symbol _start; defaulting to 00000000
// 把 _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 // 使用 bl 进行函数调用
b flash
b . // . 代表当前这一句指令的地址,这个就是高大上的死循环
//延时函数
delay:
ldr r2, =10000000
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 // 函数调用返回
root@ubuntu:/home/aston/workspace/git_xxx# make
arm-linux-gcc -o led.o led.S -c
arm-linux-ld -Ttext 0x0 -o led.elf led.o
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
root@ubuntu:/home/aston/workspace/git_xxx#
六、再难一点的流水灯效果
1. 流水灯原理分析
流水灯又叫跑马灯,实现的效果就是:挨着的LED一次点亮熄灭(同时只有1颗LED亮的)。
2. 源代码
root@ubuntu:/home/aston/workspace/git_xxx# ls
led.S Makefile mkgcc.sh mkv210_image.c readme.txt write2sd 说明.txt
root@ubuntu:/home/aston/workspace/git_xxx# cat led.S
/*
* 文件名: led.S
* 作者: xxx
* 描述: 流水灯
*/
#define GPJ0CON 0xE0200240
#define GPJ0DAT 0xE0200244
.global _start //解决 make 编译警告: arm-linux-ld: warning: cannot find entry symbol _start; defaulting to 00000000
// 把 _start 链接属性改为外部,这样其他文件就可以看见 _start 了
_start:
//第一步: 把所有引脚设置成输出模式
ldr r0, =0x11111111 //从后面的 = 可以看出用的是 ldr 伪指令,因为需要编译器来判断这个数
ldr r1, =GPJ0CON //是合法立即数还是非法立即数,一般写代码都用 ldr 伪指令
str r0, [r1] // 寄存器间接寻址。功能是把 r0 中的数写入到 r1 中的数为地址的内存中去
//要实现流水灯,只要在主循环中实现 1 圈的流水显示效果即可
flash:
//第 1 步: 点亮 LED1, 其他熄灭
ldr r0, =~(1 << 3)
ldr r1, =GPJ0DAT
str r0, [r1] //把 0 写入到 GPJ0DAT 寄存器中,引脚即输出低电平,LED 点亮
//然后延时
bl delay // 使用 bl 进行函数调用
//第 2 步: 点亮 LED2, 其他熄灭
ldr r0, =~(1 << 4)
ldr r1, =GPJ0DAT
str r0, [r1] //把 0 写入到 GPJ0DAT 寄存器中,引脚即输出低电平,LED 点亮
//然后延时
bl delay // 使用 bl 进行函数调用
//第 3 步: 点亮 LED3, 其他熄灭
ldr r0, =~(1 << 5)
ldr r1, =GPJ0DAT
str r0, [r1] //把 0 写入到 GPJ0DAT 寄存器中,引脚即输出低电平,LED 点亮
//然后延时
bl delay // 使用 bl 进行函数调用
b flash
b . // . 代表当前这一句指令的地址,这个就是高大上的死循环
//延时函数
delay:
ldr r2, =10000000
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 // 函数调用返回
root@ubuntu:/home/aston/workspace/git_xxx# make
arm-linux-gcc -o led.o led.S -c
arm-linux-ld -Ttext 0x0 -o led.elf led.o
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
root@ubuntu:/home/aston/workspace/git_xxx#