嵌入式入门1(基础知识)

1,139 阅读14分钟

一、前言

1、嵌入式Linux系统的组成:

image.png

2、简单驱动程序框架

层次openreadwrite
APPopenreadwrite
驱动dev_opendev_readdev_write
寄存器GPIO设置为输出返回GPIO状态写GPIO高低电平

驱动程序=软件框架+硬件操作

3、学习流程

单片机->BootLoader->Linux系统/驱动->App(纯C/C++无界面、QT/Android)

4、原理图

在原理图中,同名两个端口表示相互连接,如果名称以n开头,表示低电平有效。

二、嵌入式基础知识

  • 1、基本门电路:

基本门电路

  • 2、三极管:

image.png

  • 3、上拉电阻和下拉电阻:

image.png

主要用于确定悬空引脚的状态。

三、JZ2440开发板

JZ2440接口图

3.1、JZ2440简介

1、常用接口:

  1. JTAG口:用于烧写程序,但是速度非常慢。
  2. USB串口:用于查看调试信息。
  3. USB口:用于传输数据,也可以用于烧写程序,但前提是板子上运行了一个支持下载功能的程序。

USB串口配置参数:数据位:8bit,停止位:1bit,校验位:0bit,流量控制:None

2、存储介质

JZ2440开发板上有以下4种存储介质:

介质大小特性访问
NOR Flash2M直接读,命令写CPU直接访问
NAND Flash256M命令读写CPU通过Nand Flash控制器间接访问
SDRAM64M(32M*2)直接读写CPU直接访问
SRAM(片内4K内存)4K直接读写CPU直接访问

3、启动过程

image.png

大多数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)

JZ2440存储分区

四、常用汇编指令

4.1、常用汇编指令

指令含义示例
mov赋值mov r0, #0x100:把0x100赋值给r0
mov r0, r1:把r1的值赋值给r0
add加法add r0, r1, #4:r0 = r1 + 4
add r0, r1, r2:r0 = r1 + r2
sub减法sub r0, r1, #4:r0 = r1 - 4
sub 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指令

用于同时操作多个寄存器,可搭配的后缀有:

后缀全称含义
IAIncrement After过后增加
IBIncrement Before预先增加
DADecrement After过后减少
DBDecrement Before预先减少

举例1:

stmdb sp!, (fp,ip,lr,pc)

假设sp=4096。db意思是先减后存,按高编号寄存器存在高地址存。

image.png

举例2:

ldmia sp, (fp,ip,pc)

image.png

五、点亮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。

点亮LED

查看JZ2440的LED电路图,可以知道LED_2连接在GPF5引脚上,低电平点亮LED,高电平熄灭LED。

image.png

image.png

查看芯片手册

image.png

需要设置

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地址处继续执行。

整个流程的内存情况如下:

image.png

七、给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;
}