C语言裸机GPIO控制—基于I.MX6UL嵌入式SoC
1、前言
在前面的文章《汇编裸机GPIO控制—基于I.MX6UL嵌入式SoC》中,链接如下:
描述了I.MX6UL这款SoC中IOMUX控制器复用GPIO的基本机制以及GPIO的控制原理,最后使用了汇编代码实现了一个GPIO的简单控制,但是在实际嵌入式的开发过程中,直接使用汇编代码对目标板上的外设进行控制是比较少的,例如用来启动Linux内核的Uboot,最开始运行的汇编代码一般也只是用来完成一些前期工作而已,比如ARM内核时钟开启、初始化DDR以及完成C语言运行环境等工作,当这些前期环境都准备好后,将会跳转到C语言的函数去执行。
2、C语言GPIO控制代码
接下来,学习如何使用C语言进行简单的裸机GPIO控制,在前面也提到了,要想运行C语言的代码,肯定是先要初始化好C语言的运行环境,比如说初始化DDR、初始化堆栈指针SP等,对于I.MX6UL这款SoC而言,DDR的初始化是通过Boot ROM中的代码读取可编程镜像中的DCD数据结构来初始化,有的SoC就要需要自己使用汇编代码进行初始化了。
先来看看,初始化C语言运行环境的汇编代码,新创建start.S汇编文件,代码如下:
.global _start /** * _start函数,程序先在这开始执行,用来设置C语言运行环境 */ _start: /* SoC进入SVC运行模式 */ mrs r0, cpsr bic r0, r0, #0x1f /* 将cpsr寄存器的M[4:0]清0 */ orr r0, r0, #0x13 /* SoC使用SVC运行模式 */ msr cpsr, r0 /* 设置C语言运行环境 */ ldr sp, =0x80200000 /* 设置栈指针 */ b app /* 跳转到C语言的app()函数执行 */
汇编代码首先定义了一个全局标号_start,然后定义了_start函数,该函数调后,先将SoC的运行模式初始化为SVC运行模式,对于运行模式的配置是通过CPSR寄存器的M[4:0]来完成的,接下来就是设置SP堆栈指针的值为0x80200000,在我当前的目标板CoM-P6UL中,DDR是256MB的,内存地址的范围为0x80000000~0x90000000,对于ARM v7架构的处理器堆栈是向下生长的,因此在这里定义的栈空间为2MB,最后,则是使用b指令进行跳转,也就是跳转到C语言的app()函数处执行。
对于DDR的初始化,就不需要使用汇编代码区实现了,I.MX6UL芯片内部的Boot ROM启动代码将会读取可编程镜像文件中DCD数据结构来完成DDR的初始化。
接下来,新创建app.h文件,将需要用到的寄存器地址进行定义,如下:
#ifndef __APP_H #define __APP_H /** * SoC中CCM_CCGR寄存器地址 */ #define CCM_CCGR0 *((volatile unsigned int *)0x020c4068) #define CCM_CCGR1 *((volatile unsigned int *)0x020c406c) #define CCM_CCGR2 *((volatile unsigned int *)0x020c4070) #define CCM_CCGR3 *((volatile unsigned int *)0x020c4074) #define CCM_CCGR4 *((volatile unsigned int *)0x020c4078) #define CCM_CCGR5 *((volatile unsigned int *)0x020c407c) #define CCM_CCGR6 *((volatile unsigned int *)0x020c4080) /** * SoC中IOMUXC中CSI_DATA00引脚复用配置寄存器地址 */ #define SW_MUX_CTL_PAD_CSI_DATA00 *((volatile unsigned int *)0x020e01e4) #define SW_PAD_CTL_PAD_CSI_DATA00 *((volatile unsigned int *)0x020e0470) /** * SoC中GPIO4寄存器相关地址 */ #define GPIO4_DR *((volatile unsigned int *)0x020a8000) #define GPIO4_GDIR *((volatile unsigned int *)0x020a8004) #define GPIO4_PSR *((volatile unsigned int *)0x020a8008) #define GPIO4_ICR1 *((volatile unsigned int *)0x020a800c) #define GPIO4_ICR2 *((volatile unsigned int *)0x020a8010) #define GPIO4_IMR *((volatile unsigned int *)0x020a8014) #define GPIO4_ISR *((volatile unsigned int *)0x020a8018) #define GPIO4_EDGE_SEL *((volatile unsigned int *)0x020a801c) #endif
相关的寄存器地址可以在I.MX6UL芯片的参考手册中找到。
接下来,新创建app.c文件,实现完成GPIO控制的myapp()函数,实现的流程和使用汇编代码进行GPIO控制的流程一样,先使能相关的外设时钟,然后将IO口复用为GPIO复用模式,设置GPIO的方向为输出,通过写内部GPIO_DR寄存器来实现GPIO引脚的电平控制,app.c文件代码如下:
#include "app.h" /** * system_clk_enable() - 使能SoC上所有外设时钟 */ void system_clk_enable(void) { CCM_CCGR0 = 0xffffffff; CCM_CCGR1 = 0xffffffff; CCM_CCGR2 = 0xffffffff; CCM_CCGR3 = 0xffffffff; CCM_CCGR4 = 0xffffffff; CCM_CCGR5 = 0xffffffff; CCM_CCGR6 = 0xffffffff; } /** * gpio_init() - 初始化控制的GPIO * * @param: 无 * @return: 无 */ void gpio_init(void) { /* 设置CSI_DATA00引脚IO复用为GPIO4_IO21 */ SW_MUX_CTL_PAD_CSI_DATA00 = 0x5; /** * 配置GPIO4_IO21引脚电气属性 * bit[16]: 0 关闭HYS * bit[15:14]: 00 默认下拉 * bit[13]: 0 keeper * bit[12]: 1 pull/keeper使能 * bit[11]: 0 禁止开路输出 * bit[10:8]: 000 reserved * bit[7:6]: 10 速度为100MHz * bit[5:3]: 110 驱动能力为R0/6 * bit[2:1]: 00 reserved * bit[0]: 0 低摆率 */ SW_PAD_CTL_PAD_CSI_DATA00 = 0x10b0; /* 设置GPIO4_IO21的方向为输出 */ GPIO4_GDIR = 0x00200000; /* 设置GPIO4_IO21引脚输出高电平 */ GPIO4_DR = 0x00200000; } /** * gpio_output_low() - GPIO4_IO21输出低电平 * * @param: 无 * @return: 无 */ void gpio_output_low(void) { GPIO4_DR &= ~(1 << 21); } /** * gpio_output_hight() - GPIO4_IO21输出高电平 * * @param: 无 * @return: 无 */ void gpio_output_hight(void) { GPIO4_DR |= (1 << 21); } /** * delay_short() - 短时间延时函数 * * @n: 要循环的次数 * * @return: 无 */ void delay_short(volatile unsigned int n) { while(n--); } /** * delay() - 延时函数,SoC在396MHz大概延时1ms * * @n: 要延时的ms数 * * @return: 无 */ void delay(volatile unsigned int n) { while(n--) { delay_short(0x07ff); } } /** * app() - 主函数 */ void app(void) { system_clk_enable(); /* 外设时钟使能 */ gpio_init(); /* GPIO初始化 */ while (1) { gpio_output_hight(); delay(1000); gpio_output_low(); delay(1000); } }
函数实现都比较简单,GPIO控制的思路和汇编实现的是一样的。
接下来,需要为我们的工程新创建一个Makefile文件,要不然怎么编译链接得到我们需要的.bin文件呢?链接的地址还是0x87800000,Makefile的代码如下:
objs := start.o app.o gpioctrl.bin:$(objs) arm-linux-gnueabihf-ld -Ttext 0x87800000 -o gpioctrl.elf $^ arm-linux-gnueabihf-objcopy -O binary -S gpioctrl.elf $@ arm-linux-gnueabihf-objdump -D -m arm gpioctrl.elf > gpioctrl.dis %.o:%.S arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $< %.o:%.c arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $< clean: rm -rf *.o *.bin *.elf *.dis
在Makefile文件中使用到了一些变量,首先,Makefile文件开头定义了一个变量objs,该objs变量包含了编译生成目标文件gpioctrl.bin所需要的文件:start.o和app.o文件,需要注意的是,start.o要放到最前面,因为在后面链接生成gpioctrl.bin文件的时候,start.o要在前面,程序也是先从start.S里面的代码先运行的,在使用arm-linux-gnueabihf-ld命令链接的时候,使用了Makefile的自动变量"$^",表示所有依赖文件的集合,也就是objs这个变量,如果当前的工程目录下,没有start.o和app.o文件的话,就会寻找相应的规则去编译生成start.o和app.o文件,比如app.o是app.c文件编译生成的,就会执行"%.o:%.c"这个规则编译,将app.c文件编译成app.o文件,自动变量"$@"表示目标集合,例如app.o,自动变量"$<"表示依赖目标集合的第一个文件,例如app.c源文件,最后就是整个工程生成文件的清理规则clean。
在这里,使用的链接地址仍然是DDR的地址0x87800000,编译链接生成gpioctrl.bin文件如下:
最后,使用mkimage工具在gpioctrl.bin文件的基础上添加IVT、Boot Data和DCD数据生成gpioctrl.imx文件,使用MfgTools工具将可编程镜像gpioctrl.imx文件烧写到目标板的启动设备就可以进行上电测试了。
3、小结
本文主要简单介绍了基于I.MX6UL芯片平台上使用C语言进行裸机GPIO控制的基本流程。