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

硬件原理图

在开发板的资料里可以找到和LED相关的硬件原理图,如下图所示,最终的目的就是要控制GPIO1_3口的电平输出。



GPIO配置

教程中以STM32初始化GPIO的步骤作为参照,说明了I.MX6U的初始化步骤也与之相同:

    1.使能GPIO对应的时钟。

    2.设置寄存器IOMUXC_SW_MUX_CTL_PAD_XX_XX,设置IO的复用功能,使其复用为GPIO功能。

    3.设置寄存器IOMUXC_SW_PAD_CTL_PAD_XX_XX,设置IO的上下拉、速度等。

    4.配置GPIO,设置输入/输出,是否使能中断,默认输出电平等。

以下摘录I.MX6UL的参考手册里的一张图,可以看到和GPIO配置的相关的寄存器。左下角可以看到和IO复用相关的两个寄存器:SW_MUX_CTL_PAD_*SW_PAD_CTL_PAD_*。再往上可以看到GPIO相关的8个寄存器。关于

IO复用

IOMUXC_SW_MUX_CTL_PAD_<PAD NAME>是软件复用控制寄存器,IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME>是软件管脚控制寄存器。

以参考文档里的IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03寄存器为例,可见寄存器的地址为20E_0068h,这个寄存器是32位的,但是只用到了最低5位,其中bit0~bit3(MUX_MODE)指明复用功能。

以参考文档里的IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03寄存器为例,可见寄存器的地址为20E_02F4h,这个寄存器也是32为的,只用到其中的低17位。由于参数比较多,抄写了教程里的说明。

    HYS(bit16):用来使能迟滞比较器,当IO作为输入功能的时候有效,用于设置输入接收器的施密特触发器是否使能。如果需要对输入波形进行整形的话可以使能此位。此位为0的时候禁止迟滞比较器,为1的时候使能迟滞比较器。

    PUS(bit15:14):用来设置上下拉电阻。

    PUE(bit13):当IO作为输入的时候,这个位用来设置IO使用上下拉还是状态保持器。当为0的时候使用状态保持器,当为1的时候使用上下拉。状态保持器在IO作为输入的时候才有用,顾名思义,就是当外部电路断电以后此IO口可以保持住以前的状态。

    PKE(bit12):此位用来使能或者禁止上下拉/状态保持器功能,为0时禁止上下拉/状态保持器,为1时使能上下拉和状态保持器。

    ODE(bit11):当IO作为输出的时候,此位用来禁止或者使能开路输出,此位为0的时候禁止开路输出,为1的时候使能开路输出。

    SPEED(bit7:6):此位用来设置IO速度。

    DSE(bit5:3):当IO用作输出的时候用来设置IO的驱动能力。

    SRE(bit0):此位用来设置压摆率。当此位为0的时候是低压摆率,当为1的时候是高压摆率。这里的压摆率就是IO电平跳变所需要的时间,比如从0到1需要多少时间,时间越小波形就越抖,说明压摆率高;反之,时间越多波形就越缓,压摆率就越低。如果产品要过EMC的话就可以使用小的压摆率,因为波形缓和,如果当前所使用的IO做高速通信的话就可以使用高压摆率。


GPIO设置

当IO用作GPIO的时候需要设置的寄存器一共有八个,分别为DRGDIRPSRICR1ICR2EDGE_SELIMRISR

1. DR寄存器

DR寄存器是数据寄存器,结构如下图所示,此寄存器是32位,一个GPIO组最大只有32个IO,因此DR寄存器中的每个位都对应一个GPIO。当GPIO被配置为输出功能以后,向指定的位写入数据那么相应的IO就会输出相应的高低电平,比如要设置GPIO1_IO00输出高电平,那么就应该设置GPIO1.DR=1。当GPIO被配置为输入模式以后,此寄存器就保存着对应IO的电平值,每个位对应一个GPIO,例如,当GPIO1_IO00这个引脚接地的话,那么GPIO1.DR的bit0就是0。

2. GDIR寄存器

GDIR寄存器是是方向寄存器,用来设置某个GPIO的工作方向,即输入/输出。同样,每个IO对应一位,如果要设置GPIO为输入的话就设置相应的为0,如果要设置为输出的话就设置为1。比如要设置GPIO1_IO00为输入,那么GPIO1.GDIR=0。

3. PSR寄存器

PSR寄存器是GPIO状态寄存器。PSR寄存器也是一个GPIO对应一位,读取相应的位可获取对应的GPIO的状态,也就是GPIO的高低电平值。功能和输入状态下的DR寄存器一样。

4. ICR1寄存器;5. ICR2寄存器

ICR1和ICR2这两个寄存器,都是中断控制寄存器,ICR1用于配置低16个GPIO,ICR2用于配置高16个GPIO,ICR1寄存器如下图所示。

ICR寄存器中每个GPIO用两位来配置中断的触发方式,如要设置GPIO1_IO15为上升沿触发中断,那么GPIO1.ICR1=2<<30。

6. IMR寄存器

IMR寄存器是中断屏蔽寄存器。IMR寄存器也是一个GPIO对应一位,用来控制GPIO的中断禁止和使能,如果使能某个GPIO的中断,那么设置相应的位为1;反之,如果要禁止中断,那么就设置相应的位为0。例如,要使能GPIO1_IO00的中断,那么就可以设置GPIO1.IMR=1。

7. ISR寄存器

ISR寄存器是中断状态寄存器,一个GPIO对应一位,只要某个GPIO的中断发生,那么ISR中相应的位就会被置1。所以我们可以通过读取ISR寄存器来判断GPIO中断是否发生,相当于ISR中的这些位就是中断标志位。当我们处理完中断以后,必须清除中断标志位,清除方法就是向ISR中相应的位写1,也就是写1清零。

8. EDGE_SEL寄存器

EDGE_SEL寄存器是边沿选择寄存器,用来设置边沿中断,这个寄存器会覆盖ICR1和ICR2的设置,同样是一个GPIO对应一位。如果相应的位被置1,那么就相当与设置了对应的GPIO是上升和下降沿(双边沿)触发。例如,设置GPIO1.EDGE_SEL=1,那么就表示GPIO1_IO01是双边沿触发中断,无论CR1的设置是多少,都是双边沿触发。


GPIO时钟使能

I.MX6U的每个外设都有一个外设时钟,每个外设时钟都可以独立的使能或禁止,这样可以关闭掉不使用的外设时钟,从而起到省电的目的。教程手册中说I.MX6U的时钟系统很复杂,目前只看CCM里面的外设时钟使能寄存器。CCM有CCM_CCGR0~CCM_CCGR6这7个寄存器。这7个寄存器控制着I.MX6U的所有外设时钟开关,下面以CCM_CCGR0为例来看如何禁止或使能一个外设时钟:

CCM_CCGR0是个32位寄存器,其中每2位控制一个外设时钟,比如bit31:30控制着GPIO2的外设时钟。CCM_CCGR0~CCM_CCGR6的地址如下:

汇编代码编写和编译

参照上述的操作步骤以及寄存器地址,编写汇编代码。

  1. .global _start
  2.  
  3. _start:
  4. @ 使能所有时钟
  5. ldr r0,=0x020c4068     @ CCGR0
  6. ldr r1,=0xffffffff
  7. str r1,[r0]
  8.  
  9. ldr r0,=0x020c406c  @ CCGR1
  10. str r1,[r0]
  11.  
  12. ldr r0,=0x020c4070  @ CCGR2
  13. str r1,[r0]
  14.  
  15. ldr r0,=0x020c4074  @ CCGR3
  16. str r1,[r0]
  17.  
  18. ldr r0,=0x020c4078  @ CCGR4
  19. str r1,[r0]
  20.  
  21. ldr r0,=0x020c407c  @ CCGR5
  22. str r1,[r0]
  23.  
  24. ldr r0,=0x020c4080  @ CCGR6
  25. str r1,[r0]
  26.  
  27. @ 复用 - IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
  28. ldr r0,=0x20e0068
  29. ldr r1,=0x05
  30. str r1,[r0]
  31.  
  32. @ IO能力 - IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03
  33. ldr r0,=0x20e02f4
  34. @ bit [16] = 0  HYS关闭
  35. @ bit [15:14] = 00 默认下拉
  36. @ bit [13] = 0 kepper功能
  37. @ bit [12] = 1 使能kepper
  38. @ bit [11] = 0 关闭开路输出
  39. @ bit [7:6] = 10 速度100MHz
  40. @ bit [5:3] = 110 R0/6驱动能力
  41. @ bit [0] : 0 低转化率
  42. ldr r1,=0x10b0
  43. str r1,[r0]
  44.  
  45. @ 设置GPIO1_IO03为输出
  46. ldr r0,=0x209c004
  47. ldr r1,=0x08
  48. str r1,[r0]
  49.  
  50. @ GPIO1_IO03输出低电平
  51. ldr r0,=0x209c000
  52. ldr r1,=0
  53. str r1,[r0]
  54.  
  55. loop:
  56.     b loop
编译代码

1. 将上述代码保存为led.s,先将led.s编译为对应的.o文件,其中-g选项是产生调试信息,GDB能够使用这些调试信息进行代码调试;-c选项是编译源文件,但是不链接;-o选项指定编译产生的文件名。

  • arm-linux-gnueabihf-gcc -g -c led.s -o led.o

2. 教程中说选择0x87800000作为链接地址(因为后续Uboot也采用这个链接地址,不容易记混),之后将led.o链接到0x87800000这个地址,其中-Ttext选项就是指定链接地址;-o选项指定链接生成的elf文件。

  • arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf

3. 最后将生成的led.elf文件转化成bin文件供烧写用,其中-O选项指定以什么格式输出,后面的binary参数表示以二进制格式输出;-S选项表示不要复制源文件中的重定位信息和符号信息;-g选项表示不要复制源文件中的调试信息。

  • arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin

烧写和验证

烧写SD卡

I.MUX6U虽然内部有96K的ROM,但是这96K的ROM不向用户开发。为此,I.MUX6U支持从外置的NOR Flash、NAND FLASH、SD/EMMC、SPI NOR Flash和QSPI Flash这些介质中启动,所以我们可以将代码烧写到这些存储介质中。

之后的裸机实验都将代码烧写到SD卡中。编译出来的可执行文件怎么存到SD卡中,存放的位置是什么,这些NXP都有详细的规定,必须按照规定将代码烧写到SD卡中。

本篇笔记不涉及烧写格式规定,直接使用教程提供的imxdownload程序进行烧写:

  • imxdownload led.bin /dev/sdd
启动方式

将拨码开关拨到如下图的组合,指定从SD卡启动。


验证

启动电源,LED灯如预期点亮。之后对程序进行了增加,实现了亮灭交替。


思考和总结

之前了解过x86汇编,便试着增加了一个延迟函数,实现了LED交替闪灭。期间了解了ARM汇编中的条件跳转和函数调用,增加后的整体代码如下所示。

  1. .global _start
  2.  
  3. _start:
  4. @ 使能所有时钟
  5. ldr r0,=0x020c4068     @ CCGR0
  6. ldr r1,=0xffffffff
  7. str r1,[r0]
  8.  
  9. ldr r0,=0x020c406c  @ CCGR1
  10. str r1,[r0]
  11.  
  12. ldr r0,=0x020c4070  @ CCGR2
  13. str r1,[r0]
  14.  
  15. ldr r0,=0x020c4074  @ CCGR3
  16. str r1,[r0]
  17.  
  18. ldr r0,=0x020c4078  @ CCGR4
  19. str r1,[r0]
  20.  
  21. ldr r0,=0x020c407c  @ CCGR5
  22. str r1,[r0]
  23.  
  24. ldr r0,=0x020c4080  @ CCGR6
  25. str r1,[r0]
  26.  
  27. @ 复用 - IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
  28. ldr r0,=0x20e0068
  29. ldr r1,=0x05
  30. str r1,[r0]
  31.  
  32. @ IO能力 - IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03
  33. ldr r0,=0x20e02f4
  34. @ bit [16] = 0  HYS关闭
  35. @ bit [15:14] = 00 默认下拉
  36. @ bit [13] = 0 kepper功能
  37. @ bit [12] = 1 使能kepper
  38. @ bit [11] = 0 关闭开路输出
  39. @ bit [7:6] = 10 速度100MHz
  40. @ bit [5:3] = 110 R0/6驱动能力
  41. @ bit [0] : 0 低转化率
  42. ldr r1,=0x10b0
  43. str r1,[r0]
  44.  
  45. @ 设置GPIO1_IO03为输出
  46. ldr r0,=0x209c004
  47. ldr r1,=0x08
  48. str r1,[r0]
  49.  
  50. loop:
  51.     @ GPIO1_IO03输出低电平
  52.     ldr r0,=0x209c000
  53.     ldr r1,=0x08
  54.     str r1,[r0]
  55.  
  56.     bl delay
  57.  
  58.     ldr r1,=0
  59.     str r1,[r0]
  60.  
  61.     bl delay
  62. b loop
  63.  
  64. @delay 函数
  65. delay:
  66. mov r2,#0
  67. loop2:
  68. add r2,r2,#1
  69. cmp r2,#0x100000
  70. bne loop2
  71. mov r15,r14

在指定管脚输出的时候,因为是GPIO1_03,所以想要输出高电平,DR寄存器的值应该要是1<<3,之前直接写成了1,直到对照后续章节的C语言版本才找到错误。

整体实验下来,感觉也是对寄存器的操作,有些位代表的参数含义不太理解,也就直接按着教程上的内容进行设置了。点亮了一个LED,也算是初窥门径,但工作上遇到的问题,让我怀疑学了这些知识到底有多大帮助。但不管怎么样,先慢慢学吧,相信总会有“殊途同归”的一天。同样的道理,关于启动方式(拨码开关为什么要这么组合),烧写的格式又是什么?这些原理应该不影响整体学习(或许懂了也无关紧要),但还是决定在后续笔记里记录了解过程(WinHex直接复制是否可行待验证)。