[ARM裸机开发] C语言LED灯实验

在之前的笔记 汇编语言LED灯实验 中,记录了汇编语言控制LED灯的流程。这篇笔记将介绍如何使用C语言编写相同的功能。使用C语言环境让我想起来之前看的Linux0.12的引导部分的代码:从实模式进入到保护模式、初始化最基本的外设等操作都是用汇编语言写的,尤其是和CPU相关的指令是没有对应C语言实现的,待上述内容一切就绪之后,就jumpmain函数,这边的main函数这里就是C语言的天地了。

1. C语言环境设置

本次试验的思路和上述引导部分的思路也是类似的,并且非常精简,只做两部分工作:一是进入SVC模式,二是设置栈指针。

1.1 设置处理器运行模式

如下图所示,Cortex-A有九个运行模式,这里我们设置处理器运行在SVC模式下。CPSR寄存器中的M[4:0]这5位就是用来设置处理器运行模式的,像SVC模式对应的10011

1.2 设置栈指针

实验的开发板上的DDR3的地址范围从0x80000000开始,由于栈是向下生长的,所以设置SP指针为0x80200000,即预留0x200000=2MB的空间给栈。

1.3 代码

汇编初始部分的代码很简洁,先进入SVC模式,然后设置栈指针,最后跳转到后续要写的main函数中。

  1. .global _start
  2.  
  3. _start:
  4.  
  5. mrs r0cpsr
  6. bic r0r0#0x1f
  7. orr r0r0#0x13
  8. msr cpsrr0
  9.  
  10. ldr sp, =0x80200000
  11. b main

2. C语言点灯程序编写

汇编语言LED灯实验 里所讲,控制点灯是一些相关寄存器的操作,所以这边先将这些寄存器都表示出来:

  1. #ifndef __MAIN_H
  2. #define __MAIN_H
  3. /*
  4.  * CCM相关寄存器地址
  5.  */
  6. #define CCM_CCGR0 *((volatile unsigned int *)0X020C4068)
  7. #define CCM_CCGR1 *((volatile unsigned int *)0X020C406C)
  8.  
  9. #define CCM_CCGR2 *((volatile unsigned int *)0X020C4070)
  10. #define CCM_CCGR3 *((volatile unsigned int *)0X020C4074)
  11. #define CCM_CCGR4 *((volatile unsigned int *)0X020C4078)
  12. #define CCM_CCGR5 *((volatile unsigned int *)0X020C407C)
  13. #define CCM_CCGR6 *((volatile unsigned int *)0X020C4080)
  14.  
  15. /*
  16.  * IOMUX相关寄存器地址
  17.  */
  18. #define SW_MUX_GPIO1_IO03 *((volatile unsigned int *)0X020E0068)
  19. #define SW_PAD_GPIO1_IO03 *((volatile unsigned int *)0X020E02F4)
  20.  
  21. /*
  22.  * GPIO1相关寄存器地址
  23.  */
  24. #define GPIO1_DR *((volatile unsigned int *)0X0209C000)
  25. #define GPIO1_GDIR *((volatile unsigned int *)0X0209C004)
  26. #define GPIO1_PSR *((volatile unsigned int *)0X0209C008)
  27. #define GPIO1_ICR1 *((volatile unsigned int *)0X0209C00C)
  28. #define GPIO1_ICR2 *((volatile unsigned int *)0X0209C010)
  29. #define GPIO1_IMR *((volatile unsigned int *)0X0209C014)
  30. #define GPIO1_ISR *((volatile unsigned int *)0X0209C018)
  31. #define GPIO1_EDGE_SEL *((volatile unsigned int *)0X0209C01C)
  32.  
  33. #endif

接着C语言的操作就看着相当方便,但步骤和之前写的汇编代码是一样的:clk_enable()函数通过设置CCM_CCGR寄存器使能所有外设时钟;led_init()函数初始化IO复用和属性;led_on()led_off()函数通过控制GPIO1_DR寄存器达到输出高低电平的作用;delay()函数直接通过两层循环实现延迟,相比汇编来说方便许多。

  1. #include "main.h"
  2.  
  3. void clk_enable()
  4. {
  5.     CCM_CCGR0 = 0xffffffff;
  6.     CCM_CCGR1 = 0xffffffff;
  7.     CCM_CCGR2 = 0xffffffff;
  8.     CCM_CCGR3 = 0xffffffff;
  9.     CCM_CCGR4 = 0xffffffff;
  10.     CCM_CCGR5 = 0xffffffff;
  11.     CCM_CCGR6 = 0xffffffff;
  12. }
  13.  
  14. void led_init()
  15. {
  16.     SW_MUX_GPIO1_IO03 = 0x05;
  17.  
  18.     SW_PAD_GPIO1_IO03 = 0x10b0;
  19.  
  20.     GPIO1_GDIR = 0x08;
  21.  
  22.     GPIO1_DR = 0x00;
  23. }
  24.  
  25. void led_on()
  26. {
  27.     GPIO1_DR &= ~(1 << 3);
  28. }
  29.  
  30. void led_off()
  31. {
  32.     GPIO1_DR |= (1 << 3);
  33. }
  34.  
  35. void delay_short(volatile unsigned int n)
  36. {
  37.     while (n--)
  38.     {
  39.  
  40.     }
  41. }
  42.  
  43. void delay(volatile unsigned int n)
  44. {
  45.     while (n--)
  46.     {
  47.         delay_short(0x7ff);
  48.     }
  49. }
  50.  
  51. int main()
  52. {
  53.     clk_enable();
  54.     led_init();
  55.  
  56.     while (1)
  57.     {
  58.         led_off();
  59.         delay(500);
  60.  
  61.         led_on();
  62.         delay(500);
  63.     }
  64.  
  65.     return 0;
  66. }

3. 编译和烧写

编写makefile

  1. objs := start.o main.o
  2.  
  3. ledc.bin:$(objs)
  4.     arm-linux-gnueabihf-ld -Ttext 0x87800000 -o ledc.elf $^
  5.     arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
  6.     arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
  7.  
  8. %.o:%.s
  9.     arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
  10.  
  11. %.o:%.c
  12.     arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<

?:$@代表目标文件;$^代表所有依赖文件;$<代表第一个依赖文件。

也可以使用链接脚本来指定链接参数,链接脚本的基本知识可以见 附录。下面链接脚本的意思大概是:链接的地址从0x87800000开始。首先是text段,依次存放start.omain.o和其他文件的text段。紧接着按4字节地址对齐存放各个文件的rodatadatabss段。脚本中还记录了bss段的开始地址__bss_satrt和结束地址__bss_end,这两个符号都可用于后续的程序中。

  1. SECTIONS
  2. {
  3.     . = 0x87800000;
  4.     .text :
  5.     {
  6.         start.o
  7.         main.o
  8.         *(.text)
  9.     }
  10.     .rodata ALIGN(4) : {*(.rodata)}
  11.     .data ALIGN(4) : {*(.data)}
  12.     __bss_satrt = .;
  13.     .bss ALIGN(4) : {*(.bss) *(COMMON)}
  14.     __bss_end = .;
  15. }

写好链接脚本之后,只需将原先makefile中的下述语句

  •     arm-linux-gnueabihf-ld -Ttext 0x87800000 -o ledc.elf $^

中的-T选项更改成写好的链接脚本文件名即可(这边实验的名称为imx.lds):

  •     arm-linux-gnueabihf-ld -Timx.lds -o ledc.elf $^

总结和思考

之前没有听过链接脚本的概念,借着这次学习机会了解到了。C语言环境的设置因为之前了解过linux的启动设置,所以看起来比较顺畅。


附录 GNU链接脚本[1]

链接器负责将多个目标文件链接在一起,组合成一个可执行文件,称为执行映像。链接器的工作需要链接脚本的指示。如果用户未定义链接脚本,链接器会使用自带的缺省脚本。链接脚本是由链接器命令语言(Linker Command Language)写成的文本文件,是提供给链接器的指示或命令,主要用于控制链接器的链接方式,比如:如何将输入文件中的节链接成输出文件中的节;如何将输出节组合成加载段(一个加载段表示可执行文件中的一段代码或数据,由一个程序头描述);如何定义输出文件的内存布局等。

链接器命令语言中有许多命令,在Linux链接脚本中主要使用如下命令:

    (1)OUTPUT_FORMAT(default,big,little),表示链接后的可执行文件格式。如OUTPUT_FORMAT(elf32-i386,elf32-i386,elf32-i386)表示链接后的可执行文件是32位的ELF格式。

    (2)OUTPUT_ARCH(bfdarch),表示将要运行该可执行文件的处理器结构。如OUTPUT_ARCH(i386)表示链接后的可执行文件将要运行在IA-32系列处理器上。

    (3)ENTRY(symbol),表示可执行程序的入口点,即第一条指令的地址。如ENTRY(startup_32)表示可执行程序的入口点是startup_32(一个标号)。

    (4) 符号定义与赋值命令。格式为symbol = expression,用于定义符号symbol并将该符号的值设为表达式expression。在链接脚本中定义的符号可以在源程序中使用。最特殊的符号是.,它表示输出节中的当前逻辑地址(或者说是线性地址)。如:

  • . = _KERNEL_START;  // 当前逻辑地址是_KERNEL_START
  • _text = .;  // 在符号_text中记录当前逻辑地址

    (5)SECTIONS,告诉链接器如何将输入节链接到输出节以及如何在内存中摆放输出节。SECTIONS命令是链接脚本的主体部分,其格式如下:

  • SECTIONS
  • {
  •     sections-command
  •     sections-command
  •     ...
  • }

其中的sections-command可以是ENTRY命令、符号赋值、输出节描述等。常用的输出节描述命令的格式如下:

  • output-section-name: AT(lma)
  • {
  •     output-section-command
  •     output-section-command
  •     ...
  • }[:phdf:phdr ...][=fillexp]

其中AT(lma)声明输出节的装入地址为lma,如一个物理地址。

输出节描述的主体部分是output-section-command,这些命令可以是符号赋值、输入节描述等。

常见的输入节描述的格式是:*(input-section-command),意思是将所有输入文件中名为input-section-command的输入节的内容全部拼接在一起,而后输出到输出文件的output-section-command节中。

:phdf表示该节所属的加载段,由PHDRS命令定义。

    (6) PHDRS,用于定义可执行文件中的程序头。一个程序头描述可执行文件中的一段程序或数据。PHDRS命令的格式如下:

  • PHDRS
  • {
  •     name type [FLAGS(flags)];
  • }

其中name是加载段的名称,type是加载段的类型(如PT_LOAD是需要加载的段、PT_NOTE是注释信息段),FLAGS是加载段的标志(可读、可写、可执行)。

一个加载段中可以有多个节。通常情况下,只读的节被组织在.text段中,可读可写的节被组织在.data段中,符号表、串表、Debug信息等节不需要加载,因而不在任何段中。

    (7) 内建函数ADDR(secname)的返回值是节secname的绝对地址,也就是它的开始逻辑地址。

    (8) 内建函数ALIGN(align)的返回值是当前逻辑地址的向上规约值,是align的倍数。

下面是某链接脚本文件的一个片段:

  1. SECTIONS
  2. {
  3.     . = 0xC0000000 + 0x100000;      // 开始逻辑地址
  4.     _start = .;     //程序的开始地址
  5.     .text: AT(ADDR(.text)-0xC0000000) {     //输出代码节,装入到0x100000
  6.         *(.text)        //整合所有文件中的.text节
  7.     }
  8.     . = ALIGN(32);      //将地址调整为32的倍数
  9.     .data: AT(ADDR(.data)-0xC0000000) {     //输出数据节
  10.         *(.data)        //整合所有输出文件的.data节
  11.     }
  12.     _end = .;       //程序的终止地址
  13. }

在源代码中可以使用上述脚本中的符号_start_end等确定可执行程序在逻辑地址空间或线性地址空间中的位置。