5. LED灯进阶——C语言实现

在汇编点亮LED灯章节我们在led.S文件里使用纯汇编语言操作i.MX6ULL的寄存器点亮了LED灯。 主要存在两个问题,第一,汇编语言编写效率低,编写繁琐。第二,程序编写完成后需要手动输入多条编译命令, 编译过程繁琐。本章的重点是解决上面两个问题。

本章主要内容:

  • 从汇编跳转到C语言

  • 使用makefile工具和链接脚本管理工程。

配套源码以及下载工具:

  • 路径:~/embed_linux_driver_tutorial_imx6_code/bare_metal/led_c

  • 野火裸机下载工具download_tool (路径:~/embed_linux_driver_tutorial_imx6_code/bare_metal/download-tool/download-tool.tar.bz2 )。

5.1. 软件设计

本章硬件与汇编点亮LED章节完全相同。软件编写主要包括三部分内容:

  • LED程序源码的编写,这部分内容是在汇编点亮LED灯的基础上增加C语言的部分。

  • 编写makefile工程管理文件。

  • 编写链接脚本文件。链接脚本文件用于指定代码运行时的存储结构,上电后程序被加载到哪里、那个文件放到最前面、字节对齐等等。

5.1.1. 点亮LED——汇编部分

LED灯源码程序包含两部分内容,第一部分在汇编文件中设置“栈地址”并执行跳转命令跳转到main函数执行C代码。 第二部分在C文件中使led灯不断闪烁。

将“汇编点亮led灯的源码led.S复制到”更名为start.S。 在start.S的基础上增加“栈”设置和执行跳转指令,如下所示。

极简启动文件(start.S)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 /***********************第一部分*********************/
  .text            //代码段
  .align 2         //设置字节对齐
  .global _start   //定义全局变量

  _start:          //程序的开始
    b reset      //跳转到reset标号处

      reset:
      mrc     p15, 0, r0, c1, c0, 0     /*读取CP15系统控制寄存器   */
      bic     r0,  r0, #(0x1 << 12)     /*  清除第12位(I位)禁用 I Cache  */
      bic     r0,  r0, #(0x1 <<  2)     /*  清除第 2位(C位)禁用 D Cache  */
      bic     r0,  r0, #0x2             /*  清除第 1位(A位)禁止严格对齐   */
      bic     r0,  r0, #(0x1 << 11)     /*  清除第11位(Z位)分支预测   */
      bic     r0,  r0, #0x1             /*  清除第 0位(M位)禁用 MMU   */
      mcr     p15, 0, r0, c1, c0, 0     /*  将修改后的值写回CP15寄存器   */

  /***********************第二部分*********************/
      ldr sp, =0x84000000   //设置栈地址64M
      b main                //跳转到main函数

  /***********************第三部分*******************/
    /*跳转到light_led函数*/
    //   bl light_led
    /*进入死循环*/
  loop:
      b loop

代码总共分为三部分,第一、三部分与“汇编点亮led灯”完全相同,下面重点说明第二部分代码。

  • ldr sp, =0x84000000指令,用于设置栈指针。野火i.MX6ULL开发板标配512M的DDR内存, 裸机开发用不了这么多。程序中我们将栈地址设置到DDR的64M地址处。 这个值也可以根据需要自行定义。

  • b main 指令,只用跳转指令跳转到main函数中执行。 指令“b”是“无返回”的跳转指令正常情况下,不会执行第三部分代码。

5.1.2. 点亮LED灯——C语言部分

C源码非常简单,只需把“汇编点亮led灯”例程中有关GPIO寄存器操作换成C语言即可,源码如下所示。

语言实现点亮LED灯
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//时钟控制寄存器
#define CCM_CCGR1 (volatile unsigned long*)0x20C406C
//GPIO1_04复用功能选择寄存器
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04 (volatile unsigned long*)0x20E006C
//PAD属性设置寄存器
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04 (volatile unsigned long*)0x20E02F8
//GPIO方向设置寄存器
#define GPIO1_GDIR (volatile unsigned long*)0x0209C004
//GPIO输出状态寄存器
#define GPIO1_DR (volatile unsigned long*)0x0209C000

#define uint32_t  unsigned int

/*简单延时函数*/
void delay(uint32_t count)
{
   volatile uint32_t i = 0;
   for (i = 0; i < count; ++i)
   {
      __asm("NOP"); /* 调用nop空指令 */
   }
}

int main()
{
   *(CCM_CCGR1) = 0xFFFFFFFF;                     //开启GPIO1的时钟
   *(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04) = 0x5;     //设置PAD复用功能为GPIO
   *(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04) = 0x1F838; //设置PAD属性
   *(GPIO1_GDIR) = 0x10;                          //设置GPIO为输出模式
   *(GPIO1_DR) = 0x0;                             //设置输出电平为低电平

   while(1)
   {
      *(GPIO1_DR) = 0x0;
      delay(0xFFFFF);
      *(GPIO1_DR) = 1<<4;
      delay(0xFFFFF);
   }
   return 0;
}

结合代码,各部分讲解如下:

  • 第1-10行,定义GPIO相关寄存器。这部分内容与“汇编点亮LED灯”中定义的寄存器相同, 只不过这里使用(volatile unsigned long*)将地址强制转化为指针。

  • 第12-22行,实现了简单的软件延时函数,在函数体中调用 __asm(“NOP”)嵌入汇编指令, 这条语句表示CPU什么都不做。

  • 第26-30行,向寄存器中写入值。在第一部分代码中我们将寄存器地址强制转换为指针, 这里使用使用“*”号再次“寻址”。“*(CCM_CCGR1) = 0xFFFFFFFF;” 代码表示将0xFFFFFFFF写入指针CCM_CCGR1所指定的地址中。其他寄存器类似。

  • 第32-38行,不断改变RGB红灯所在引脚的高低电平。

5.1.3. 编写链接脚本

什么是链接脚本

我们知道写好的代码(无论是汇编还是C语言)都要经过编译、汇编、链接等步骤生成二进 制文件或者可供下载的文件。在编译阶编译器会对每个源文件进行语法检查并生成对应的汇编语言, 汇编是将汇编文件转化为机器码。在上一章我们使用”arm-none-eabi-gcc -g -c led.S -o led.o”命令 完成了源码的编译、汇编工作,生成了.o文件。编译和汇编是针对单个源文件, 也就编译完成后一个源文件(.c,.S或.s)对应一个.o文件。程序链接阶段就会将这些.o链接成一个文件。

链接脚本的作用就是告诉编译器怎么链接这些文件,比如那个文件放在最前面,程序的代码段、数据段、bss段分别放在什么位置等等。

链接脚本介绍

链接器脚本主要由SECTIONS、段、“.”运算符以及变量组成,一个简单的链接脚本如下所示。 我们将基于它讲解连接脚本的基本用法。

链接脚本lds
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 ENTRY(_start)
 SECTIONS {
   . = 0x80000000;

   . = ALIGN(4);
   .text :
   {
   start.o (.text)
   *(.text)
   }

   . = ALIGN(4);
   .data :
   {
   *(.data)
   }

   . = ALIGN(4);
   .bss :
   {
   *(.bss)
   }
 }

结合代码各部分讲解如下:

  • 第1行,ENTRY(_start) 用于指定程序的入口,ENTRY( )是设置入口地址的命令, “_start”是程序的入口,本章的led程序的入口地址位于start.S的“_start”标号处。

  • 第2行,定义SECTIONS。SECTIONS可以理解为是一块区域,我们在这块区域排布我们的代码, 链接时链接器就会按照这里的指示链接我们的代码。

  • 第3行,“.”运算符代表当前位置。 我们在SECTION的最开始使用“.= 0x80000000”就是将链接起始地址设置为0x80000000。

  • 第5行,设置字节对齐。这里同样用到了“.”运算符,它表示从当前位置开始执行四字节对齐。 假设当前位置为0x80000001,执行该命令后当前地址将会空出三个字节转到0x80000004地址处。

  • 第6行,定义代码段。“.text :”用于定义代码段,固定的语法要求,我们按照要求写即可。 在“{}”中指定那些内容放在代码段。

  • 第8-9行,将start.o中的代码放到代码段的最前面。start.S是启动代码应当首先被执行, 所以通常情况下要把它放到代码段的最前面,其他源文件的代码按照系统默认的排放顺序即可, 通配符“*”在这里表示其他剩余所有的.o文件。

  • 第12-16行,设置数据段。同设置代码段类似,首先设置字节对齐,然后定义代码段。在代码段里使用“*”通配符, 将所有源文件中的代码添加到这个代码段中。

  • 第18-22行,设置BSS段。设置方法与设置数据段完全相同

连接器脚本编写完成后,在链接指令中加入链接脚本即可。编写Makefile章节将会介绍如何使用链接脚本。

5.1.4. 编写makefile文件

在“汇编点亮LED灯”章节,我们程序编写完成后需要依次输入编译、链接、格式转换 命令才能最终生成二进制文件。这种编译方式效率低、容易出错。本 小节讲解裸机下的makefile的编写。

点亮LED灯程序的makefile比较简单,仅实现了最基本的功能,后面使用到复杂功能是我们再进行修改,源码如下所示。

makefile文件实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 all: start.o led.o
    arm-none-eabi-ld -Tled.lds  $^ -o led.elf
    arm-none-eabi-objcopy -O binary -S -g led.elf led.bin

  %.o : %.S
    arm-none-eabi-gcc -g -c $^ -o start.o
  %.o : %.c
    arm-none-eabi-gcc -g -c $^ -o led.o


  .PHONY: clean
  clean:
    rm *.o *.elf *.bin

makefile文件很简短,因为是第一个裸机makefile,下面将详细分析每一行代码。

  • 第1行,添加最终目标以及依赖文件。

  • 第2行,添加链接命令。参数“-Tled.lds”表示使用led.lds链接脚本链接程序。 参数“$^”代表所有的依赖文件。“-o”指定输出文件名。

  • 第3行,添加格式转换命令。与“汇编点亮LED灯”相同,“-O binary”指定输出二进制文件。 “-S”选项,不从源文件中复制重定位信息和符号信息。“-g”选项,不从源文件中复制可调试信息。

  • 第5-6行,添加汇编文件编译命令。与“汇编点亮LED灯”编译命令大致相同,这里使用“$^”替代要编译的源文件。

  • 第7-8行,添加编译C文件的命令。编译C文件与编译汇编文件命令相同这里不再介绍。

  • 第11-13行,添加清理命令。“.PHONY”定义了伪目标“clean”。伪目标一般没 有依赖,并且“clean”伪目标一般放在Makefile文件的末尾。“clean”为目标用于删除make生成的文件。

5.1.5. 编译下载

makefile编写完成后程序的编译就变得非常简单,我们只需要在makefile 所在文件夹下执行make命令, makefile工具便会自动完成程序的编译、链接、格式转换等工作。 正常情况下我们可以在当前目录看到生成的一些中间文件以及我们期待的.bin文件。

在编译下载官方SDK程序到开发板章节我们详细讲解了如何将二进制文件烧写到SD卡(烧写工具自动实现为二进制文件添加头)。 这里再次说明下载步骤。

  • 将一张空SD卡(烧写一定会破坏SD卡中原有数据!!!烧写前请保存好SD卡中的数据), 接入电脑后在虚拟机的右下角状态栏找到对应的SD卡。将其链接到虚拟机。

  • 进入烧写工具目录,执行 ./mkimage.sh <烧写文件路径> 命令,例如要 烧写的led.bin位于home目录下,则烧写命令为 ./mkimage.sh /home/led.bin

  • 执行上一步后会列出linux下可烧写的磁盘,选择你插入的SD卡即可。这一步 非常危险!!!一定要确定选择的是你插入的SD卡!!,如果选错很可能破坏你电脑磁盘内容,造成数据损坏!!! 确定磁盘后SD卡以“sd”开头,选择“sd”后面的字符即可。例如要烧写的sd卡是“sdb”则输入“b”即可。

5.2. 实验现象

将开发板设置为SD卡启动,接入SD卡,开发板上电,可以看到开发板RGB红灯闪烁。