Arduino笔记(2)

1. PWM概念: PWM( Pulse Width Modulation).简单来说,在arduino中我们可以理解为就是通过调节占空比来实现不同电压输出。

图片:

2. analogWrite()

描述

从一个引脚输出模拟值(PWM)。可用于让LED以不同的亮度点亮或驱动电机以不同的速度旋转。analogWrite()输出结束后,该引脚将产生一个稳定的特殊占空比方波,直到下次调用analogWrite()(或在同一引脚调用digitalRead()或digitalWrite())。PWM信号的频率大约是490赫兹。

在大多数arduino板(ATmega168或ATmega328),只有引脚3,5,6,9,10和11可以实现该功能。在aduino Mega上,引脚2到13可以实现该功能。老的Arduino板(ATmega8)的只有引脚9、10、11可以使用analogWrite()。在使用analogWrite()前,你不需要调用pinMode()来设置引脚为输出引脚。

analogWrite函数与模拟引脚、analogRead函数没有直接关系。

通过读取电位器的阻值控制LED的亮度

int ledPin = 9;  // LED连接到数字引脚9
int analogPin = 3;  //电位器连接到模拟引脚3
int val = 0;  //定义变量存以储读值
 
void setup()
 
{
pinMode(ledPin,OUTPUT);  //设置引脚为输出引脚
}
 
void loop()
{
val = analogRead(analogPin);  //从输入引脚读取数值
analogWrite(ledPin,val / 4);  // 以val / 4的数值点亮LED(因为analogRead读取的数值从0到1023,而analogWrite输出的数值从0到255)
}

3. 调节PWM值的程序:

int n=0;
void setup ()
{
  pinMode(4,INPUT);
  pinMode(6,OUTPUT);      //该端口需要选择有#号标识的数字口
  pinMode(10,INPUT);
}
 
void loop()
{
  int up =digitalRead(4);          //读取4号口的状态
  int down = digitalRead(10);      //读取10号口的状态   
  if (up==HIGH)                    //判断4号口目前是否是高电平
  { 
   n=n+5;                         //每次累加值为5
    if (n>=255) {
      n=255;
    }            //限定最大值为255   
analogWrite(6,n);               //使用PWM控制6号口输出,变量n的取值范围是0-255 
    delay (300);
  }
  if (down==HIGH)                    //减少亮度
  {
   n=n-5;
    if (n<=0) {
      n=0;
    }
 analogWrite(6,n);
    delay (300);
  }
}

需要选择#号标识的数字口是因为这些端口需要支持PWM功能。而后在loop()函数中,将修改后的n值输出到6号端口。

4. analogRead()

描述

从指定的模拟引脚读取数据值。 Arduino板包含一个6通道(Mini和Nano有8个通道,Mega有16个通道),10位模拟数字转换器。这意味着它将0至5伏特之间的输入电压映射到0至1023之间的整数值。这将产生读数之间的关系:5伏特/ 1024单位,或0.0049伏特(4.9 mV)每单位。输入范围和精度可以使用analogReference()改变。 它需要大约100微秒(0.0001)来读取模拟输入,所以最大的阅读速度是每秒10000次。

语法

analogRead(PIN)

数值的读取

引脚:从输入引脚(大部分板子从0到5,Mini和Nano从0到7,Mega从0到15)读取数值

返回

从0到1023的整数值

5. 实现呼吸灯:

void setup ()
{
  pinMode(11,OUTPUT);
}
 
void loop()
{
  for (int a=0; a<=255;a++)                //循环语句,控制PWM亮度的增加
  {
    analogWrite(11,a);
    delay(16);                             //当前亮度级别维持的时间,单位毫秒            
  }
    for (int a=255; a>=0;a--)             //循环语句,控制PWM亮度减小
  {
    analogWrite(11,a);
    delay(16);                             //当前亮度的维持的时间,单位毫秒  
  }
  delay(800);                             //完成一个循环后等待的时间,单位毫秒
}

或者:


int n = 0; // n 从 1 至 255,控制led亮度
int i = 5;  // 递进数

void setup()
{
  pinMode( 11, OUTPUT); //设置11口为PWM输出端
}

void loop()
{
  n += i;                               // n每次增加 i 

  

  if ( n == 255 || n == 0)
//在n升至255或者降至0时,i进行反转。这样led灯能在亮暗间转换
   i = -i; 

  analogWrite( 11, n );
  delay( 50 );       //延迟50ms,进行下一次亮度调整
  
   if( n == 0)
     delay(1800);
  
}

6. 实现温度计

主要器件LM315.


void setup() {
 
  Serial.begin(9600);         //使用9600速率进行串口通讯
}
 
void loop() {
 
  int n = analogRead(A0);    //读取A0口的电压值
 
  float vol = n * (5.0 / 1023.0*100);
//使用浮点数存储温度数据,温度数据由电压值换算得到
 
  Serial.println(vol);                   //串口输出温度数据
  delay(2000);                           //等待2秒,控制刷新速度
}

Serial.begin()

将串行数据传输速率设置为位/秒(波特)。与计算机进行通信时,可以使用这些波特率:300,1200,2400,4800,9600,14400,19200,28800,38400,57600或115200。当然,您也可以指定其他波特率- 例如,引脚0和1和一个元件进行通信,它需要一个特定的波特率。

Serial.println() 打印数据到串行端口,输出人们可识别的ASCII码文本并回车 (ASCII 13, 或 ‘\r’) 及换行(ASCII 10, 或 ‘\n’)。此命令采用的形式与Serial.print ()相同 。

和DS18b20有什么区别?

DS18b20是数字的,数字的出来的是方波,用脉冲方波和协议来通讯,模拟的出来的是电压,利用AD转换(ARDUINO的模拟脚可以理解为就是数字脚+AD/DA转换模块,如果你需要大量的模拟脚但是不要求数字脚,可以直接外接AD/DA转换器来实现)来得到测量值并换算成温度

0-100度 对应0-5v 模拟口返回数值0-1024 所以。模拟口的值 1=0.48828125

7. 光敏电阻的程序改动:

“达文西的手电筒”,有光才能亮,没光,绝对不会亮!

int a =300;     //此处需是环境基础亮度变量,请查看自己的亮度数值,
                //填写到此处数值要略大于所测得的数据但小于灯光下的数据
void setup ()
{
  Serial.begin(9600);
  pinMode(13,OUTPUT);
}
void loop()
{
  int n = analogRead(A0);            //读取模拟口A0数值
  Serial.println(n);
  if (n>= a )                   //对光线强度进行判断,如果比我们的预设值大
就点亮LED否则就关闭
  {
    digitalWrite(13,HIGH);
  }
  else
  {
    digitalWrite(13,LOW);
  }
}

修改为符合逻辑的光控电路:

/* 光强度小于临界值 */
if ( n < a)
  {
    digitalWrite(13,HIGH); 		// 点亮LED
  }
  else
  {
    digitalWrite(13,LOW);		// 超过临界值时,关闭
  }

光敏三极管有凸起的一边为发射极,此端接A0检测口,同时并联一个10K欧姆的分压电阻到地线以扩展光敏三极管的灵敏度(此处电阻越小灵敏度越高)。另一极使用5V输入。

Qemu快速保存和恢复状态

1. 启动镜像:

	$ qemu-system-i386 -hda hd.qcow2

2. 保存当前运行状态:

同时按下ctrl+alt+2切换到Qemu内建命令行,输入:

	(qemu) savevm booted

如果需要即时回复到保存时状态

	(qemu) loadvm booted

关闭Qemu运行窗口

3. 快速恢复到保存状态:

	$ qemu-system-i386 -hda hd.qcow2 -loadvm booted

用Qemu和GNU编译链研究ARM汇编(4)

1. 有关异常向量

前面的例子中存在一个大BUG,内存布局中的前8个全字是为异常向量而保留的。当异常发生时控制逻辑将转到这些位置以执行对应的异常处理代码。异常向量和它们的地址如下:

  • Exception Address
  • Reset 0x00
  • Undefined Instruction 0x04
  • Software Interrupt (SWI) 0x08
  • Prefetch Abort 0x0C
  • Data Abort 0x10
  • Reserved, not used 0x14
  • IRQ 0x18
  • FIQ 0x1C

按理说,这些个异常向量应该对应到异常处理程序中,既然我们代码中不会有异常发生,索性就用死循环来代替,如下:

        .section "vectors"
reset:  b     start
undef:  b     undef
swi:    b     swi
pabt:   b     pabt
dabt:   b     dabt
        nop
irq:    b     irq
fiq:    b     fiq

对应的,为了确保这些指令被放置在异常向量地址中,链接脚本也需要做相应的改动:

SECTIONS {
        . = 0x00000000;
        .text : {
                * (vectors);
                * (.text);
                ...
        }
        ...
}

异常向量需要放置在所有代码之前,这确保了代了向量是从0x0地址开始。

2. C启动代码

直接执行C代码会造成CPU直接重启,因为和汇编代码不同的是C语言需要初始化运行环境。

static int arr[] = { 1, 10, 4, 5, 6, 7 };
static int sum;
static const int n = sizeof(arr) / sizeof(arr[0]);

int main()
{
        int i;

        for (i = 0; i < n; i++)
                sum += arr[i];
}

C语言运行环境需要设置

  1. 全局变量: 已初始化的 && 未初始化的
  2. 只读数据

2.1 栈设置

栈被用来存储自动变量,传递函数变量,存储返回地址等等。ARM Architecture Procedure Call Standard (AAPCS)是ARM体系结构中用于生成栈的规则。r13被用于作栈指针。

对于特定的开发板来说,栈开始地址可能不同,对于connex开发板来说,地址可以用下面的代码来定义:

	ldr sp, =0xA4000000

2.2 全局变量

C代码在编译时会把已初始化的全局变量放在.data段中,因而在初始化的汇编代码中,需要把.data段从Flash搬移到RAM中。

C代码确保未初始化的全局变量被初始化成0. 当C程序被编译时,独立的.bss段被用作未初始化的变量。因为未初始化的值都是0,我们无需将其存储在FLASH中。只不过在搬移的时候,我们需要在程序中将它们初始化为0而已。

2.3 只读数据

const常量会被初始化为.rodata, .rodata也被用于存储字符常量。

.rodata在运行时不会被改变,所以它们可以被直接放置在FLASH中。

2.4 启动代码

Linker脚本需要做下面的事:

  1. .bss部分代替
  2. vectors部分代替
  3. .rodata部分代替
SECTIONS {
        . = 0x00000000;
        .text : {
              * (vectors);
              * (.text);
        }
        .rodata : {
              * (.rodata);
        }
        flash_sdata = .;

        . = 0xA0000000;
        ram_sdata = .;
        .data : AT (flash_sdata) {
              * (.data);
        }
        ram_edata = .;
        data_size = ram_edata - ram_sdata;

        sbss = .;
        .bss : {
             * (.bss);
        }
        ebss = .;
        bss_size = ebss - sbss;
}

启动代码需要完成下列任务:

  1. 中断向量设置
  2. 将.data部分从FLASH拷贝到RAM
  3. 将.bss置0后拷贝到RAM
  4. 设置栈指针(stack pointer)
  5. 分支程序到main函数
        .section "vectors"
reset:  b     start
undef:  b     undef
swi:    b     swi
pabt:   b     pabt
dabt:   b     dabt
        nop
irq:    b     irq
fiq:    b     fiq

        .text
start:
        @@ Copy data to RAM.
        ldr   r0, =flash_sdata
        ldr   r1, =ram_sdata
        ldr   r2, =data_size

        @@ Handle data_size == 0
        cmp   r2, #0
        beq   init_bss
copy:
        ldrb   r4, [r0], #1
        strb   r4, [r1], #1
        subs   r2, r2, #1
        bne    copy

init_bss:
        @@ Initialize .bss
        ldr   r0, =sbss
        ldr   r1, =ebss
        ldr   r2, =bss_size

        @@ Handle bss_size == 0
        cmp   r2, #0
        beq   init_stack

        mov   r4, #0
zero:
        strb  r4, [r0], #1
        subs  r2, r2, #1
        bne   zero

init_stack:
        @@ Initialize the stack pointer
        ldr   sp, =0xA4000000

        bl    main

stop:   b     stop

我们将直接用arm-none-eabi-gcc来编译所有程序: arm-none-eabi-gcc -nostdlib -o csum.elf -T csum.lds csum.c startup.s -nostdlib选项用于指定标准C不应该被链接。

查看符号信息:

	arm-none-eabi-nm -n csum.elf 
	00000000 t reset
	00000004 A bss_size
	00000004 t undef
	00000008 t swi
	0000000c t pabt
	00000010 t dabt
	00000018 A data_size
	00000018 t irq
	0000001c t fiq
	00000020 T main
	00000094 t start
	000000a8 t copy
	000000b8 t init_bss
	000000d0 t zero
	000000dc t init_stack
	000000e4 t stop
	00000100 r n
	00000104 R flash_sdata
	a0000000 d arr
	a0000000 D ram_sdata
	a0000018 D ram_edata
	a0000018 D sbss
	a0000018 b sum
	a000001c B ebss

可以看到: 中断向量从0x0开始; 汇编代码从8个全字后开始(0x20==32==84); 只读数据n放在代码之后; arr,初始化后的数据,放在RAM中; 未初始化的数据,sum放在6个int之后64==24==0x18

转化成.bin二进制格式后,在Qemu中运行之,检查结果:

	$ arm-none-eabi-objcopy -O binary csum.elf csum.bin	
	$ dd if=/dev/zero of=./flash.bin bs=4K count=4K
  	$ dd if=csum.bin of=flash.bin bs=4096 conv=notrunc
  	$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
	(qemu) xp /6dw 0xa0000000
	a0000000:          1         10          4          5
	a0000010:          6          7
	(qemu) xp /1dw 0xa0000018
	a0000018:         33

我们可以看到,1, 10, 4, 5, 6, 7 分别为数组元素,而结果为33, 储存在0x18的地址。如果感兴趣,我们大可查找出别的数据地址,这里就不一一述说了。

用Qemu和GNU编译链研究ARM汇编(2)

研究两个汇编程序, 通过研究这两个程序,初步了解ARM汇编的知识:

  • 用于求数组和的程序
  • 用于计算字符串长度的程序

1. 数组求和

        .text
entry:  b start                 @ Skip over the data
arr:    .byte 10, 20, 25        @ Read-only array of bytes
eoa:                            @ Address of end of array + 1

        .align
start:
        ldr   r0, =eoa          @ r0 = &eoa
        ldr   r1, =arr          @ r1 = &arr
        mov   r3, #0            @ r3 = 0
loop:   ldrb  r2, [r1], #1      @ r2 = *r1++
        add   r3, r2, r3        @ r3 += r2
        cmp   r1, r0            @ if (r1 != r2)
        bne   loop              @    goto loop
stop:   b stop

.byte声明:

.byte声明的变量在内存中以连续的比特存在,.2byte和.4byte与之类似,分别用于存储16位值和32位值。联想到C语言中的内建数据结构定义,不难想象char/int/long int 预编译完是哪一种类型。

通用语法结构如下:

	.byte   exp1, exp2, ...
	.2byte  exp1, exp2, ...
	.4byte  exp1, exp2, ...

你可以指定数据的格式,二进制用前缀0b/0B修饰,八进制以前缀0修饰,十进制/十六进制以0x/0X开头。整数也可以用字符常量来表示,加上单引号即可,在这种情况下ASCII码会被用到。

也可以用C表达式,包含文字和其他符号的组合,如下例:

	pattern:  .byte 0b01010101, 0b00110011, 0b00001111
	npattern: .byte npattern - pattern
	halpha:   .byte 'A', 'B', 'C', 'D', 'E', 'F'
	dummy:    .4byte 0xDEADBEEF
	nalpha:   .byte 'Z' - 'A' + 1

.align声明:

ARM指令需要32位对齐,指令的起始内存地址需要是4的倍数,所以用.align指令来插入无用的byte,来确保下一条指令的起始地址是4的倍数。在代码中存在byte或是半字(half words)的时候,需要用到这条指令。

编译&运行:

	# assemble
	$ arm-none-eabi-as -o sum.o sum.s 
	# link to elf file
	$ arm-none-eabi-ld -Ttext=0x0 -o sum.elf sum.o
	# form bin file
	$ arm-none-eabi-objcopy -O binary sum.elf sum.bin
	# form flash image
	$ dd if=/dev/zero of=flash.bin bs=4k count=4k
	$ dd if=sum.bin of=flash.bin bs=4K conv=notrunc
	# emulate with flash image
	$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
	# examine the result
	R00=00000007 R01=00000007 R02=00000019 R03=00000037
	R04=00000000 R05=00000000 R06=00000000 R07=00000000
	R08=00000000 R09=00000000 R10=00000000 R11=00000000
	R12=00000000 R13=00000000 R14=00000000 R15=00000024
	PSR=600001d3 -ZC- A svc32
	FPSCR: 00000000

R03的值正是我们求和的结果: 0x37==55==10+20+25, 可以用一张流程图来表示程序的运行过程

2. 字符串长度

        .text
        b start

str:    .asciz "Hello World"

        .equ   nul, 0

        .align
start:  ldr   r0, =str          @ r0 = &str
        mov   r1, #0

loop:   ldrb  r2, [r0], #1      @ r2 = *(r0++)
        add   r1, r1, #1        @ r1 += 1
        cmp   r2, #nul          @ if (r1 != nul)
        bne   loop              @    goto loop

        sub   r1, r1, #1        @ r1 -= 1
stop:   b stop

运行结果:

	(qemu) info registers
	R00=00000010 R01=0000000b R02=00000000 R03=00000000
	R04=00000000 R05=00000000 R06=00000000 R07=00000000
	R08=00000000 R09=00000000 R10=00000000 R11=00000000
	R12=00000000 R13=00000000 R14=00000000 R15=0000002c
	PSR=600001d3 -ZC- A svc32
	FPSCR: 00000000

r1是用来存储字符串长度的,计算结果为11。

.asciz声明: .asciz声明接受一个字符串作为参数,字符串以双引号修饰的字符表示。汇编器自动在字符串后面加nul字符(即\0字符)

.equ声明 汇编器维持一张符号表,符号表维持键->值的格式。当汇编器遇到一个标号时,汇编器将在符号表中自动建立一个条目。以后当汇编器遇到一个关于label的引用时,将自动替换为符号表中储存的label的地址。

使用汇编器指令.equ,我们可以手动在符号表中插入条目。

.equ通常这样定义: .equ name, expression

.equ不会分配任何内存,它们只是在符号表中插入条目罢了。

bne的意思是(!=) , b means bit. bit not equal. ble (<=), beq (==), bge (>=), bgt (>), and bne (!=).

用Qemu和GNU编译链研究ARM汇编(3)

在多个文件构成的程序中,源文件首先被编译成多个对象(object)文件(.o文件), 然后交由链接器生成最终的可执行文件,如下图所示:

{% img img /images/linker.png %}

在组建可执行文件时,链接器主要完成下列操作:

  • 解析符号
  • 重定位

1. 符号解析

在编译单个文件组成的程序时,所有标号的解析都可以由汇编器替代为对应的地址。而在多文件组成的程序中,如果有储存在其他文件中的符号引用,汇编器会将其标识为"unresolved”(未解析).当对象文件被传递给链接器时,链接器从这些文件中决定对应的值,并把code中的unresolved的值替代为正确的值。

我们用上一节的求和函数来演示链接器是如何进行符号解析的。 这两个文件汇编后,会在链接时被检查未被解析的引用。

main.s
        .text
        b start                 @ Skip over the data
arr:    .byte 10, 20, 25        @ Read-only array of bytes
eoa:                            @ Address of end of array + 1

        .align
start:
        ldr   r0, =arr          @ r0 = &arr
        ldr   r1, =eoa          @ r1 = &eoa

        bl    sum               @ Invoke the sum subroutine

stop:   b stop

more:

sum-sub.s
        @ Args
        @ r0: Start address of array
        @ r1: End address of array
        @
        @ Result
        @ r3: Sum of Array

        .global sum

sum:    mov   r3, #0            @ r3 = 0
loop:   ldrb  r2, [r0], #1      @ r2 = *r0++    ; Get array element
        add   r3, r2, r3        @ r3 += r2      ; Calculate sum
        cmp   r0, r1            @ if (r0 != r1) ; Check if hit end-of-array
        bne   loop              @    goto loop  ; Loop
        mov   pc, lr            @ pc = lr       ; Return when done

查看.o文件符号信息:

	$ arm-none-eabi-nm main.o
	00000004 t arr
	00000007 t eoa
	00000008 t start
	00000014 t stop
	         U sum
	$ arm-none-eabi-nm sum-sub.o 
	00000004 t loop
	00000000 T sum

t代表符号已经被定义了, 而u则代表符号未被定义。大写字母表示该符号是全局变量。

从上面的输出结果看,sum是被定义在sum-sub.o的全局变量,而该变量在main.o中未被解析到。当linker被调用时,符号引用将被解析到,对应的可执行文件将被生成。

总结: as程序负责把.s文件编译成object文件,而生成最终的可执行文件时,ld负责把未被定位的符号定位到实际的库函数所在的位置。

2. 重定位.

重定位用于改变已经分配给标号的地址。它包括将所有符号引用映射到新分配的内存地址。

合并段后的符号列表情况,可以对比于上面的main.o和sum-sub.o来看:

	$ arm-none-eabi-ld -Ttext=0x0 -o sum.elf main.o sum-sub.o
	arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 00000000
	$ arm-none-eabi-nm sum.elf
	00000004 t arr
	00008038 T __bss_end__
	00008038 T _bss_end__
	00008038 T __bss_start
	00008038 T __bss_start__
	00008038 T __data_start
	00008038 T _edata
	00008038 T _end
	00008038 T __end__
	00000007 t eoa
	00000024 t loop
	00080000 T _stack
	00000008 t start
	         U _start
	00000014 t stop
	00000020 T sum

可以看到stop后面的sum已经被定位好了(之前是main.o中的U标记),而loop则相应延后,被定位到了再往后的00000024。

地址的变更: loop原本地址为00000004, 现在是00000024, 而sum原本为0x00000000,现在为00000020, 这是因为sum-sub.o中的.text和main.o中的.text部分一起组成了sum.elf中的.text部分。

整体移动某个段到指定内存位置, 注意在-Ttext中我们增加的0x100的偏移量,使得地址对比于上面的结果整体上移了0x100:

	$ arm-none-eabi-ld -Ttext=0x100 -o sum100.elf main.o sum-sub.o
	arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 00000100
	$ arm-none-eabi-nm sum100.elf 
	00000104 t arr
	00008138 T __bss_end__
	00008138 T _bss_end__
	00008138 T __bss_start
	00008138 T __bss_start__
	00008138 T __data_start
	00008138 T _edata
	00008138 T _end
	00008138 T __end__
	00000107 t eoa
	00000124 t loop
	00080000 T _stack
	00000108 t start
	         U _start
	00000114 t stop
	00000120 T sum

3. 重定位.data到RAM中。

我们可以通过撰写链接脚本,将程序的.data段放置在RAM中。这也是通常嵌入式系统所谓bootloader干的活儿,从Flash中加载启动代码到RAM中而后执行。

例程从RAM中加载两个数值,将两者相加而后将结果写回RAM,两个值和结果都放置在.data部分。

代码:

        .data
val1:   .4byte 10               @ First number
val2:   .4byte 30               @ Second number
result: .4byte 0                @ 4 byte space for result

        .text
        .align
start:
        ldr   r0, =val1         @ r0 = &val1
        ldr   r1, =val2         @ r1 = &val2

        ldr   r2, [r0]          @ r2 = *r0
        ldr   r3, [r1]          @ r3 = *r1

        add   r4, r2, r3        @ r4 = r2 + r3

        ldr   r0, =result       @ r0 = &result
        str   r4, [r0]          @ *r0 = r4

stop:   b stop

链接脚本:

SECTIONS {
        . = 0x00000000;
        .text : { * (.text); }

        . = 0xA0000000;
        .data : { * (.data); }
}

从connex的内存布局来看,内存地址为0xa000_0000到0xa400_0000,因而A0000000刚好在内存中。

查看链接后的内存符号地址:

	$ arm-none-eabi-as -o sum.o sum.s
	$ arm-none-eabi-ld -T sum_link.ld -o sum.elf sum.o 
	$ arm-none-eabi-nm -n sum.elf 
	00000000 t start
	0000001c t stop
	a0000000 d val1
	a0000004 d val2
	a0000008 d result

这样就完了?NO!!!!!!!!因为:RAM is Volatile! 内存是易变的!

RAM是易失性介质,怎可保证每次加电时就有代码洗干净PP在等着被运行?嵌入式系统里必然有非易失性存储,所有的代码和数据在加电前都需要放在这些非易失性存储介质中,例如在FLASH中。这样在加电后我们就可以利用一段启动代码把代码从FLASH搬到RAM中。

从这个设计思路出发,我们需要程序的.data有两个地址,一个是加载地址,另一个是运行地址。 这就是常说的:LMA(Load Memory Address) VS VMA(Virtual Memory Address)。

上面的代码需要做两个修改:

  1. 需要在.data中指定load地址和运行地址
  2. 需要写一段代码用于将数据从FLASH读取到RAM中, 从存储地址到运行地址。
SECTIONS {
        . = 0x00000000;
        .text : { * (.text); }
        etext = .;

        . = 0xA0000000;
        .data : AT (etext) { * (.data); }
}

etext包含了FLASH中放置完地址后的空白地址,记住这个地址以便在接下来将这个数值传送给.data部分,以便程序将.data部分从FLASH拷贝到RAM中。etext只是符号表中的一个,本身并不占据任何内存(可以回去翻上一篇日志)。

关于AT关键字: 它指定了.data部分的加载地址,一个地址或符号被传递给AT关键字,以便它从该地址拷贝数据。 在这里,我们传递etext符号给AT。

要把代码从FLASH拷贝到RAM中,下列信息需要被提供:

  1. Flash中数据地址(flash_sdata)
  2. RAM中数据地址(ram_sdata)
  3. .data部分大小(data_size)

拷贝代码:

        ldr   r0, =flash_sdata
        ldr   r1, =ram_sdata
        ldr   r2, =data_size

copy:
        ldrb  r4, [r0], #1
        strb  r4, [r1], #1
        subs  r2, r2, #1
        bne   copy

由此,我们需要在链接脚本中生成这三个数值:

SECTIONS {
        . = 0x00000000;
        .text : {
              * (.text);
        }
        flash_sdata = .;

        . = 0xA0000000;
        ram_sdata = .;
        .data : AT (flash_sdata) {* (.data); }
        ram_edata = .;
        data_size = ram_edata - ram_sdata;
}

ram_sdata为ram中数据开始地址,而ram_edata为结束地址,两者相减则为数据块大小。

改变后的带有copy数据的代码:

        .data
val1:   .4byte 10               @ First number
val2:   .4byte 30               @ Second number
result: .space 4                @ 1 byte space for result

        .text

        ;; Copy data to RAM.
start:
        ldr   r0, =flash_sdata
        ldr   r1, =ram_sdata
        ldr   r2, =data_size

copy:
        ldrb  r4, [r0], #1
        strb  r4, [r1], #1
        subs  r2, r2, #1
        bne   copy

        ;; Add and store result.
        ldr   r0, =val1         @ r0 = &val1
        ldr   r1, =val2         @ r1 = &val2

        ldr   r2, [r0]          @ r2 = *r0
        ldr   r3, [r1]          @ r3 = *r1

        add   r4, r2, r3        @ r4 = r2 + r3

        ldr   r0, =result       @ r0 = &result
        str   r4, [r0]          @ *r0 = r4

stop:   b stop

使用修改过的final_sum_ram.s和link脚本编译,并生成flash.bin后,就可以在qemu-system-arm中验证结果了。

$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
QEMU 1.4.2 monitor - type 'help' for more information
(qemu) info registers
R00=a0000008 R01=a0000004 R02=0000000a R03=0000001e
R04=00000028 R05=00000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=00000000 R14=00000000 R15=00000038
PSR=600001d3 -ZC- A svc32
FPSCR: 00000000

(qemu) xp /4dw 0xA0000000
00000000a0000000:         10         30         40          0

R04包含了我们相加后的结果, 为0x28=40, R02/R03则分别为操作数10/30. 而通过显示0xA0000000也显示了内存中的值分别为val1/val2/result的值。

接下来的章节中,我们将讲到C代码入口。