一、前言
1、嵌入式Linux系统的组成:
2、简单驱动程序框架
| 层次 | open | read | write |
|---|---|---|---|
| APP | open | read | write |
| 驱动 | dev_open | dev_read | dev_write |
| 寄存器 | GPIO设置为输出 | 返回GPIO状态 | 写GPIO高低电平 |
驱动程序=软件框架+硬件操作
3、学习流程
单片机->BootLoader->Linux系统/驱动->App(纯C/C++无界面、QT/Android)
4、原理图
在原理图中,同名两个端口表示相互连接,如果名称以n开头,表示低电平有效。
二、嵌入式基础知识
- 1、基本门电路:
- 2、三极管:
- 3、上拉电阻和下拉电阻:
主要用于确定悬空引脚的状态。
三、JZ2440开发板
3.1、JZ2440简介
1、常用接口:
- JTAG口:用于烧写程序,但是速度非常慢。
- USB串口:用于查看调试信息。
- USB口:用于传输数据,也可以用于烧写程序,但前提是板子上运行了一个支持下载功能的程序。
USB串口配置参数:数据位:8bit,停止位:1bit,校验位:0bit,流量控制:None。
2、存储介质
JZ2440开发板上有以下4种存储介质:
| 介质 | 大小 | 特性 | 访问 |
|---|---|---|---|
| NOR Flash | 2M | 直接读,命令写 | CPU直接访问 |
| NAND Flash | 256M | 命令读写 | CPU通过Nand Flash控制器间接访问 |
| SDRAM | 64M(32M*2) | 直接读写 | CPU直接访问 |
| SRAM(片内4K内存) | 4K | 直接读写 | CPU直接访问 |
3、启动过程
大多数ARM芯片从0地址启动,但s3c2440可以通过 OM[1:0] 引脚来决定CPU的启动方式。不同启动方式的启动流程如下:
* 1. NOR启动:
NOR Flash基地址为0,片内RAM地址为0x4000,0000
CPU读取NOR上第一条指令(前4字节)执行
CPU继续读取其它指令执行
* 2. NAND启动:
片内4K RAM基地址为0,NOR Flash不可访问
2440硬件把NAND前4K内容复制到片内4K RAM中
然后CPU从0地址开始取指令执行
3.2、环境配置
1、配置gcc
下载arm-linux-gcc-3.4.5-glibc-2.3.6(尽量不要使用其余版本),并解压到本地
tar -xjvf arm-linux-gcc-3.4.5-glibc-2.3.6.tar.bz2
sudo mv ./gcc-3.4.5-glibc-2.3.6 ~/gcc-3.4.5-glibc-2.3.6
sudo vim ~/.bashrc
在.bashrc文件最后添加
export PATH=$PATH:~/gcc-3.4.5-glibc-2.3.6/bin
保存后,使用source ~/.bashrc命令使其生效,并通过arm-linux-gcc -v命令进行验证,如果出现版本号,表示配置成功。
如果出现以下错误:
/usr/local/arm/bin/arm-linux-gcc: 15: exec: /usr/local/arm/bin/.arm-none-linux-gnueabi-gcc: not found
这是因为ubuntu用的64位系统 而arm-linux-gcc是32位,所以需要一些32库才能通过编译,输入以下命令即可:
sudo apt-get install libc6-i386 lib32z1 gcc-multilib
2、解决虚拟机网络问题
pc机使用哪个网卡连接开发板,VMware就要使用同一个网卡作为桥接网卡。
同时,当开发板运行u-boot时,需要设置u-boot的ip地址。当开发板运行linux时,需要设置linux的ip地址。两者需要单独设置。
mount -t nfs -o nolock,vers=3 192.168.1.178:/home/pujh/workspace/imx6ull /mnt
3.3、程序烧写流程
1、使用JTAG口烧写裸板程序
JTAG口可以使用JLink、op/eop烧写工具进行裸板程序烧写。这两个工具功能是不同的:
- JLink只能烧写到NOR Flash,不能烧写到NAND Flash。
- 而op/eop能够烧写到NOR Flash和NAND Flash。
而我使用的是JLink,如果需要烧写裸板程序到nand flash中,则只能先使用JLink将u-boot烧写到nor flash中,然后使用u-boot烧写裸板程序到nand flash。
2、使用uboot烧写裸板程序
这里又分为2种方法:
- 2.1、通过网络烧写,如tftp
tftp led.bin 0x30000000 //将led.bin文件通过tftp下载到内存0x30000000(SDRAM空间)地址上。
nand erase 0 0x400 //nand flash烧写之前必须先擦除,这里选择擦除1KB的空间。
nand write 0x30000000 0 0x400 //将内存0x30000000上1KB的内容烧写到nand flash 0地址处。
- 2.2、使用usb口烧写
注意:要求u-boot必须带有USB下载功能。
1、使用`op/eop`把u-boot.bin烧到NOR Flash。
2、开发板设置为nor启动, 上电后马上在串口输入空格键,使板子进入UBOOT而不是启动板子上的内核。
3、连接PC和开发板的usb device口,并安装驱动。
4、在UBOOT的串口菜单中输入n (表示接收USB文件并烧写到NAND)。
5、使用`dnw_100ask.exe`发送bin文件。
6、uboot会自动接收、烧写bin文件。
7、断电、设为NAND启动、上电:运行nand上烧好的程序。
3.4、恢复出厂设置
- 1、烧写u-boot
- 2、烧写kernel:通过u-boot的usb下载功能烧写uImage文件
- 3、烧写根文件系统:通过uboot的usb下载功能烧写fs_qtopia.yaffs2文件
- 4、删除params分区:使用命令
nand erase params - 5、重启
如果重启后触摸屏校准有问题,使用命令rm /etc/pointercal删除校准
在uboot模式下,可以通过mtd命令查看NAND分区
OpenJTAG> mtd
device nand0 <nandflash0>, # parts = 4
#: name size offset mask_flags
0: bootloader 0x00040000 0x00000000 0
1: params 0x00020000 0x00040000 0
2: kernel 0x00200000 0x00060000 0
3: root 0x0fda0000 0x00260000 0
active partition: nand0,0 - (bootloader) 0x00040000 @ 0x00000000
defaults:
mtdids : nand0=nandflash0
mtdparts: mtdparts=nandflash0:256k@0(bootloader),128k(params),2m(kernel),-(root)
四、常用汇编指令
4.1、常用汇编指令
| 指令 | 含义 | 示例 |
|---|---|---|
| mov | 赋值 | mov r0, #0x100:把0x100赋值给r0mov r0, r1:把r1的值赋值给r0 |
| add | 加法 | add r0, r1, #4:r0 = r1 + 4add r0, r1, r2:r0 = r1 + r2 |
| sub | 减法 | sub r0, r1, #4:r0 = r1 - 4sub r0, r1, r2:r0 = r1 - r2 |
| ldr | 读内存 | ldr r0, [r1]:假设r1的值为X,读取地址X上的数据(4字节)保存到r0中ldr r0,=0x12345678 (伪指令) |
| str | 写内存 | str r0, [r1]:假设r1的值为X,把r0的值写到地址X上去(4字节) |
| b | 跳转 | |
| bl | 跳转(branch and link) | bl xxx:跳到xxx,并把返回地址(下一条指令地址)保存在lr寄存器中 |
4.2、LDM/STM指令
用于同时操作多个寄存器,可搭配的后缀有:
| 后缀 | 全称 | 含义 |
|---|---|---|
| IA | Increment After | 过后增加 |
| IB | Increment Before | 预先增加 |
| DA | Decrement After | 过后减少 |
| DB | Decrement Before | 预先减少 |
举例1:
stmdb sp!, (fp,ip,lr,pc)
假设sp=4096。db意思是先减后存,按高编号寄存器存在高地址存。
举例2:
ldmia sp, (fp,ip,pc)
五、点亮LED
LED的驱动方式,常见的有四种。
- 方式1:使用引脚输出3.3V点亮LED,输出0V熄灭LED。
- 方式2:使用引脚拉低到0V点亮LED,输出3.3V熄灭LED。
有的芯片为了省电等原因,其引脚驱动能力不足,这时可以使用三极管驱动。
- 方式3:使用引脚输出1.2V点亮LED,输出0V熄灭LED。
- 方式4:使用引脚输出0V点亮LED,输出1.2V熄灭LED。
查看JZ2440的LED电路图,可以知道LED_2连接在GPF5引脚上,低电平点亮LED,高电平熄灭LED。
查看芯片手册
需要设置
GPFCON[11:10]=01
GPFDAT[5]=0
5.1、汇编点亮LED
led_on.S
// 点亮LED:gpf5
.text
.global _start
_start:
/*
* 配置GPF5为输出引脚
* 把0x400写到地址0x56000050上
*/
ldr r1, =0x56000050
ldr r0, =0x400 /* mov r0, #0x400 */
str r0, [r1]
/*
* 设置GPF5输出高电平
* 把0x0写到地址0x56000054上
*/
ldr r1, =0x56000054
ldr r0, =0 /* mov r0, #0 */
str r0, [r1]
// 死循环
halt:
b halt
Makefile
all: led_on.S
arm-linux-gcc -c -o led_on.o led_on.S #编译
arm-linux-ld -Ttext 0 led_on.o -o led_on.elf #链接
arm-linux-objcopy -O binary -S led_on.elf led_on.bin
arm-linux-objdump -D -m arm led_on.elf > led_on.dis #反汇编
clean:
rm -f *.dis *.bin *.elf *.o
得到反汇编文件led_on.dis内容如下:
led_on.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
0: e59f1014 ldr r1, [pc, #20] ; 1c <.text+0x1c> //0x8+20=28=0x1c
4: e3a00b01 mov r0, #1024 ; 0x400
8: e5810000 str r0, [r1]
c: e59f100c ldr r1, [pc, #12] ; 20 <.text+0x20> //0x14+12=0x20
10: e3a00000 mov r0, #0 ; 0x0
14: e5810000 str r0, [r1]
00000018 <halt>:
18: eafffffe b 18 <halt>
1c: 56000050 undefined
20: 56000054 undefined
我们通过反汇编可以看到,伪指令ldr r1, =0x56000050,被编译器转换成了ldr r1, [pc, #20],即从pc+20的地址中读入数据到r1中。
pc是程序计数器(Program Counter),它的值等于当前指令+8。这是因为ARM芯片采用流水线方式读取指令:
- 当前执行地址A的指令
- 已经在对地址A+4的指令进行译码
- 已经在读取地址A+8的指令
lr Link Register 返回地址
sp Stack Pointer 栈指针
低位存在低地址上:小字节序(little endian)
高位存在低地址上:大字节序(big endian)
12位立即数可以拆分成高4位(rotate),低8位(immed)。
立即数等于immed_8循环右移2*rotate位。
5.2、C程序点亮LED
start.S
.text
.global _start
_start:
/* 将栈指针设为片内4k内存顶部 */
ldr sp, =0x40000000+4096 /* NOR启动 */
/*调用main*/
bl main
halt:
b halt
led.c
int main()
{
volatile unsigned int * pGPFCON = (unsigned int *)0x56000050;
volatile unsigned int * pGPFDAT = (unsigned int *)0x56000054;
// 配置GPF4为输出引脚
*pGPFCON = 0x100;
// 配置GPF4为输出0
*pGPFCON |= 0;
return 0;
}
Makefile
all:
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext 0 start.o led.o -o led.elf
arm-linux-objcopy -O binary -S led.elf led.bin
arm-linux-objdump -D -m arm led.elf > led.dis
clean:
rm -f *.dis *.bin *.elf *.o
5.3、跑马灯
start.S
.text
.global _start
_start:
/* 关闭看门狗 */
ldr r0, =0x53000000
mov r1, #0x0
str r1, [r0]
ldr sp, =0x40000000+4096 /* NOR启动 */
/* 调用main函数 */
bl main
halt:
b halt
led.c
void delay(){
volatile int i = 30000;
while(i--);
}
int main(){
volatile unsigned long * pGPFCON = (unsigned long *)0x56000050;
volatile unsigned long * pGPFDAT = (unsigned long *)0x56000054;
*pGPFCON &= ~0x3f00;//对第8~13位清零
*pGPFCON |= 0x1500;//对第8~13位赋值
int i = 4;
while(1) {
for(i=4; i<=6; i++) {
//对数据寄存器4~6位取反
*pGPFDAT ^= (1<<i);
delay();
}
}
}
5.4、按键控制LED
start.S与跑马灯程序一致。
led.c
#include "s3c2440_soc.h"
int main(){
// 设置GPFCON让GPF4/5/6配置为输出引脚
GPFCON &= ~((3<<8) | (3<<10) | (3<<12));
GPFCON |= ((1<<8) | (1<<10) | (1<<12));
/*
* 配置3个按键引脚为输入引脚:
* GPF0(S2),GPF2(S3),GPG3(S4)
*/
GPFCON &= ~((3<<0) | (3<<4)); /* gpf0,2 */
GPGCON &= ~((3<<6)); /* gpg3 */
while(1) {
if(GPFDAT & (1<<0)) {
GPFDAT |= (1<<6);
} else {
GPFDAT &= ~(1<<6);
}
if(GPFDAT & (1<<2)) {
GPFDAT |= (1<<5);
} else {
GPFDAT &= ~(1<<5);
}
if(GPGDAT & (1<<3)) {
GPFDAT |= (1<<4);
} else {
GPFDAT &= ~(1<<4);
}
}
return 0;
}
5.5、判断启动方式
start.S
.text
.global _start
_start:
// 分辨是nor/nand启动,不同的启动方式,片内4k内存基地址都不同
// 我们统一将栈指针设置到片内4k内存的顶部
// 如果0地址可写,表示nand启动,否则是nor启动
mov r1, #0
ldr r0, [r1] //读出原来的值备份
str r1, [r1] //0->[0]
ldr r2, [r1] //r2=[0]
cmp r1, r2 //r1==r2?如果相等表示nand启动
ldr sp, =0x40000000+4096 //先假设是nor启动
moveq sp, #4096 //nand启动
streq r0, [r1] //恢复原来的值
/*调用main*/
bl main
halt:
b halt
六、C语言内部机制
- 1、什么要设置栈?
因为C语言需要使用栈来保存局部变量,以及函数跳转时寄存器中的值,以便函数返回后继续执行。
- 2、调用者如何传参数给被调用者?
在arm中有个ATPCS规则,约定r0-r15寄存器的用途:
r0-r3:调用者和被调用者之间传参数;
r4-r11:函数可能被使用,所以在函数的入口保存它们,在函数的出口恢复它们;
- 3、被调用者如何传返回值给调用者?
被调用者返回时,将返回结果存入r0寄存器中。
- 4、怎么从栈中恢复那些寄存器?
从5.2、C程序点亮LED的反汇编文件来分析C语言的内部机制。
led.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
0: e3a0da01 mov sp, #4096 ; 0x1000
4: eb000000 bl c <main>
00000008 <halt>:
8: eafffffe b 8 <halt>
0000000c <main>:
c: e1a0c00d mov ip, sp
10: e92dd800 stmdb sp!, {fp, ip, lr, pc}
14: e24cb004 sub fp, ip, #4 ; 0x4
18: e24dd008 sub sp, sp, #8 ; 0x8
1c: e3a03456 mov r3, #1442840576 ; 0x56000000
20: e2833050 add r3, r3, #80 ; 0x50
24: e50b3010 str r3, [fp, #-16]
28: e3a03456 mov r3, #1442840576 ; 0x56000000
2c: e2833054 add r3, r3, #84 ; 0x54
30: e50b3014 str r3, [fp, #-20]
34: e51b2010 ldr r2, [fp, #-16]
38: e3a03c01 mov r3, #256 ; 0x100
3c: e5823000 str r3, [r2]
40: e51b2014 ldr r2, [fp, #-20]
44: e3a03000 mov r3, #0 ; 0x0
48: e5823000 str r3, [r2]
4c: e3a03000 mov r3, #0 ; 0x0
50: e1a00003 mov r0, r3
54: e24bd00c sub sp, fp, #12 ; 0xc
58: e89da800 ldmia sp, {fp, sp, pc}
Disassembly of section .comment:
00000000 <.comment>:
0: 43434700 cmpmi r3, #0 ; 0x0
4: 4728203a undefined
8: 2029554e eorcs r5, r9, lr, asr #10
c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1}
10: Address 0x10 is out of bounds.
分析上面的汇编代码:
开发板上电后,将从0地址开始执行,即开始执行
mov sp, #4096 //设置栈地址在4k RAM的最高处,sp=4096;
bl c <main> //调到c地址处的main函数,并保存下一行代码地址到lr,即lr=8;
mov ip, sp //把sp的值赋值给ip,ip=sp=4096
stmdb sp!, {fp, ip, lr, pc} //按高编号寄存器存在高地址,依次将pc、lr、ip、fp存入sp-4中;
sub fp, ip, #4 //fp的值为ip-4=4096-4=4092;
sub sp, sp, #8 //sp的值为sp-8=(4096-4x4)-8=4072;
mov r3, #1442840576 //r3赋值0x5600 0000;
add r3, r3, #80 //r3的值加0x50,即r3=0x5600 0050;
str r3, [fp, #-16] //r3存入[fp-16]所在的地址,即地址4076处存放0x5600 0050;
mov r3, #1442840576 //r3赋值0x5600 0000;
add r3, r3, #84 //r3的值加0x54,即r3=0x5600 0054;
str r3, [fp, #-20] //r3存入[fp-20]所在的地址,即地址4072处存放0x5600 0054;
ldr r2, [fp, #-16] //r2取[fp-16]地址处的值,即[4076]地址的值,r2=0x5600 0050;
mov r3, #256 //r3赋值为0x100;
str r3, [r2] //将r3写到r2内容所对应的地址,即0x5600 0050地址处的值为0x100;;对应c语言*pGPFCON = 0x100;;
ldr r2, [fp, #-20] //r2取[fp-20]地址处的值,即[4072]地址的值,r2=0x5600 0054;
mov r3, #0 //r3赋值为0x00;
str r3, [r2] //将r3写到r2内容所对应的地址,即0x5600 0054地址处的值为0x00;对应c语言*pGPFDAT = 0;
mov r3, #0 //r3赋值为0x00;
mov r0, r3 //r0=r3=0x00;
sub sp, fp, #12 //sp=fp-12=4092-12=4080;
ldmia sp, {fp, sp, pc} //从栈中恢复寄存器,fp=4080地址处的值=原来的fp,sp=4084地址处的值=4096,pc=4088地址处的值=8,随后调到0x08地址处继续执行。
整个流程的内存情况如下:
七、给C方法传递参数
start.S
.text
.global _start
_start:
/* 关闭看门狗 */
ldr r0, =0x53000000
mov r1, #0x0
str r1, [r0]
ldr sp, =0x40000000+4096 /* NOR启动 */
mov r0, #4
bl led_on
ldr r0, =100000
bl delay
mov r0, #5
bl led_on
halt:
b halt
led.c
void delay(volatile int count) {
while(count--);
}
int led_on(int which) {
volatile unsigned long * pGPFCON = (unsigned long *)0x56000050;
volatile unsigned long * pGPFDAT = (unsigned long *)0x56000054;
if(which == 4) {
*pGPFCON=0x100;
} else if(which == 5) {
*pGPFCON=0x400;
}
*pGPFDAT=0;
return 0;
}