使用C语言按照GPIO操作流程点亮LED灯

Posted 行稳方能走远

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用C语言按照GPIO操作流程点亮LED灯相关的知识,希望对你有一定的参考价值。

第九章我们使用汇编编写了LED 灯的实验,在实际开发过程中大部分还是使用C 语言,汇编只是用来完成C 语言环境的初始化,本章我们就来实现用汇编完成C 语言环境的初始化,然后用C 语言实现LED 的例程。

C程序版LED例程简介

汇编完成C 语言环境的初始化主要包括内存初始化,设置堆栈指针等等,当这些工作完成以后就可以跳转到C 语言,执行C 程序了,所以我们有两部分事情要做:

1.汇编文件,用来完成C 语言的环境初始化

2.C 语言文件,主要实现我们的业务功能,比如我们本章的点亮LED。

原理图分析

本章用到的硬件资源和第6 章的一样,可以参照第六章的硬件原理分析。

程序编写

本实验对应的例程在光盘资料的:i.MX6UL 终结者光盘资料\\04_裸机例程源码\\2_led_C program 目录下,我们在Ubuntu 系统建立“1_Led_C program”文件夹,然后在“Led_C program”文件夹下建立文件:start.S、main.c、main.h。其中start.S 是汇编文件,main.c 和main.h 是C 语言文件。

我们在前面新建的“strart.S”文件中输入下面的代码:

.global _start  		/* 全局标号 */

/*
 * 描述:	_start函数,程序从此函数开始执行,此函数主要功能是设置C
 *		 运行环境。
 */
_start:

	/* 进入SVC模式 设置CPU运行模式 也就是设置cpsr寄存器 手册7.1章节有讲*/ 
	mrs r0, cpsr
	bic r0, r0, #0x1f 	/* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4  这两个都是逻辑运算符	*/
	orr r0, r0, #0x13 	/* r0或上0x13,表示使用SVC模式					*/
	msr cpsr, r0		/* 将r0 的数据写入到cpsr_c中 					*/

	ldr sp, =0X80200000	/* 设置栈指针			 */
	b main				/* 跳转到main函数 		 */

第1 行定义了一个全局标号_start

第6 行是程序的入口、

第8 行到第11 行是设置处理器进入SVC 模式

第12 行通过ldr 指令设置SVC 模式下的sp 指针(0x80200000),i.MX6 ULL 终结者开发板上的内存地址范围是0X80000000~0X90000000(256MB),所以不论是512MB 版本还是256MB 版本的,其内存起始地址都是0X80000000,由于i.MX6ULL 的堆栈是向下增长的,所以SP 指针设置成0X80200000(0X80200000-0X80000000,是2MB 的栈空间,足够我们使用了)。

第13 行是跳转到main 函数(C 语言的入口函数)。

至此汇编部分的程序我们就完成了,主要用来设置处理器在SVC 模式下运行,然后初始化SP 指针,最后跳转到C 程序的main 入口函数。如果大家有接触过三星的S3C2440,S3c6410 或者S5PV210 的处理器,我们在使用内存之前必须先初始化CPU 的内存控制器,所以在他们的汇编文件中一定有内存控制器的初始化代码(比如Uboot 的汇编中)。大家可能会发现我们上面编写的start.S 文件中并没有发现初始化内存控制器的代码,但是却将SVC 模式下的SP 指针设置到了内存的地址范围里面,这样不是有问题吗?大家还记得在第六章我们在编译生成“led.bin”文件以后,通过create_imx 工具在“led.bin”文件添加了一些数据包头吗,也就是DCD 数据,在第六章我们已经讲过DCD 数据里面包含了内存控制器的参数配置了,i.MX6ULL内存固化的Boot ROM 程序会读取DCD 数据中的内存控制器参数,bin 完成内存控制器的初始化配置。

接下来我们开始实现C 语言部分,首先我们打开前面建立的“main.h”文件,然后输入下面的代码:

#ifndef __MAIN_H
#define __MAIN_H

/* 
 * CCM相关寄存器地址 
 */
#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)

/* 
 * IOMUX相关寄存器地址 
 */
#define SW_MUX_GPIO1_IO03 	*((volatile unsigned int *)0X020E0068)
#define SW_PAD_GPIO1_IO03 	*((volatile unsigned int *)0X020E02F4)

/* 
 * GPIO1相关寄存器地址 
 */
#define GPIO1_DR 			*((volatile unsigned int *)0X0209C000)
#define GPIO1_GDIR 			*((volatile unsigned int *)0X0209C004)
#define GPIO1_PSR 			*((volatile unsigned int *)0X0209C008)
#define GPIO1_ICR1 			*((volatile unsigned int *)0X0209C00C)
#define GPIO1_ICR2 			*((volatile unsigned int *)0X0209C010)
#define GPIO1_IMR 			*((volatile unsigned int *)0X0209C014)
#define GPIO1_ISR 			*((volatile unsigned int *)0X0209C018)
#define GPIO1_EDGE_SEL 		*((volatile unsigned int *)0X0209C01C)

#endif

在main.h 中,我们通过宏的方式定义了要使用到的所有寄存器名称,后面的十六进制数就是寄存器对应的地址,比如GPIO1_DR 寄存器的地址是0X0209C000。然后保存并退出“main.h”。

接下来我们打开“main.c”文件,在里面输入下面的代码:

#include "main.h"

/*
 * @description	: 使能I.MX6U所有外设时钟
 * @param 		: 无
 * @return 		: 无  这里全部使能 不考虑功耗了 和汇编步骤一样的
 */
void clk_enable(void)
{
	CCM_CCGR0 = 0xffffffff;
	CCM_CCGR1 = 0xffffffff;
	CCM_CCGR2 = 0xffffffff;
	CCM_CCGR3 = 0xffffffff;
	CCM_CCGR4 = 0xffffffff;
	CCM_CCGR5 = 0xffffffff;
	CCM_CCGR6 = 0xffffffff;
}

/*
 * @description	: 初始化LED对应的GPIO
 * @param 		: 无
 * @return 		: 无
 */
void led_init(void)
{
	/* 1、初始化IO复用 */
	SW_MUX_GPIO1_IO03 = 0x5;	/* 复用为GPIO1_IO03 */

	/* 2、、配置GPIO1_IO03的IO属性	
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
     */
	SW_PAD_GPIO1_IO03 = 0X10B0;		

	/* 3、初始化GPIO */
	GPIO1_GDIR = 0X0000008;	/* GPIO1_IO03设置为输出 */

	/* 4、设置GPIO1_IO03输出低电平,打开LED0 */
	GPIO1_DR = 0X0;
}

/*
 * @description	: 打开LED灯
 * @param 		: 无
 * @return 		: 无
 */
void led_on(void)
{
	/* 
	 * 将GPIO1_DR的bit3清零	 
	 */
	GPIO1_DR &= ~(1<<3); 
}

/*
 * @description	: 关闭LED灯
 * @param 		: 无
 * @return 		: 无
 */
void led_off(void)
{
	/*    
	 * 将GPIO1_DR的bit3置1
	 */
	GPIO1_DR |= (1<<3);
}

/*
 * @description	: 短时间延时函数
 * @param - n	: 要延时循环次数(空操作循环次数,模式延时)
 * @return 		: 无
 */
void delay(volatile unsigned int n)
{
	while(n--){}
}

/*
 * @description	: 延时函数,在396Mhz的主频下
 * 			  	  延时时间大约为1ms
 * @param - n	: 要延时的ms数
 * @return 		: 无
 */
void mdelay(volatile unsigned int n)
{
	while(n--)
	{
		delay(0x7ff);
	}
}

/*
 * @description	: mian函数
 * @param 	    : 无
 * @return 		: 无
 */
int main(void)
{
	clk_enable();		/* 使能所有的时钟		 	*/
	led_init();			/* 初始化led 			*/

	while(1)			/* 死循环 				*/
	{	
		led_off();		/* 关闭LED   			*/
		mdelay(300);		/* 延时大约500ms 		*/

		led_on();		/* 打开LED	闪灯	 	*/
		mdelay(300);		/* 延时大约500ms 		*/
	}

	return 0;
}

main.c 文件里面一共有7 个函数,下面一个个的看下每个函数的具体功能:

  1. void clk_enable(void)函数使能CCM_CCGR0-CCM_CCGR6 的时钟(使能所有外设时钟)。
  2. void led_init(void)函数初始化LED 对应的IO(复用成GPIO,默认下拉,输出低电平)。
  3. void led_on(void)函数点亮LED(对应GPIO 输出低电平)。
  4. void led_off(void)函数关闭LED(对应GPIO 输出高电平)。
  5. void delay_short(volatile unsigned int n)延时函数。
  6. void mdelay(volatile unsigned int n)毫秒级延时函数。
  7. int main(void),C 程序主函数。先调用clk_enable()函数完成外设时钟的使能,然后调用led_init()函数完成LED 对应的IO 的初始化,最后进入while(1)死循环,实现LED 每隔300 毫秒亮灭的状态切换。

编译及烧写测试

我们新建Makfile 文件,在里面输入下面的内容:

1 objs := start.o main.o

2 led.bin:$(objs)
3 arm-linux-gnueabihf-ld -Ttext 0X87800000 -o led.elf $^
4 arm-linux-gnueabihf-objcopy -O binary -S led.elf $@

5 %.o:%.s
6 arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<

7 %.o:%.S
8 arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<

9 %.o:%.c
10 arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<

11 clean:
12 rm -rf *.o led.bin led.elf

在本Makefile 文件中我们使用到了变量和自动变量,关于Makefile 的变量和自动变量可以参考《跟我一起写Makefile.pdf》文档(光盘资料的:i.MX6UL 终结者光盘资料\\10_其它参考资料目录下)

第1行我们定义了变量objs,该变量包含要生成的led.bin 文件所需要的start.o 和main.o,也就是我们工程的start.S 和main.c 编译生成的“.o”文件。需要我们注意的是start.o 一定要放到最前面,因为后面链接的时候start.o 要在最前面,这样才能保证start.o 在链接完成以后,运行的时候最先执行。

第2行就是默认最终生成的可执行文件“led.bin”,“led.bin”文件依赖于“start.o”和“main.o”两
个文件,如果当前工程目录下没有这两个文件,Make 命令就会在Makefile 中找到相应的规则生成“start.o”和“main.o”。比如“start.s”文件根据第5 行的规则生成,“start.S”根据规则中的第7 行生成,“main.o”根据规则中的第9 行生成。

第3 行使用arm-linux-gnueabihf-ld 进行连接,连接的起始地址是0x87800000,这一行用到了自动变量“$^”,它是指所有依赖文件的集合,也就是变量“objs”,即“start.o”和“main.o”,连接的时候汇编的start.o 要放在最前面,因为程序首先从汇编的“_start”标号处开始执行,因此这一行展开就是:

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

第4 行使用arm-linux-gnueabihf-objcopy -O binary -S ledc.elf @ 把 “ l e d . e l f ” 文 件 转 换 成 “ l e d . b i n ” 文 件 , 这 里 也 用 到 了 自 动 变 量 “ @把“led.elf”文件转换成“led.bin”文件,这里也用到了自动变量“ @led.elfled.bin@”,它的意思是目标集合,也就是“led.bin”,那么本行展开相当于:

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

第5 行到第10 行是针对不同后缀名的文件,将其编译成对应的“.o”文件。比如start.s 就会使用第5行的规则生成对应的“start.o”文件,第6 行就是具体执行的命令,这里用到了自动变量“ @ ” 和 “ @”和“ @<”,“$<”的意思是依赖目标集合的第一个文件,比如“start.s”编译成“start.o”,第5 行和第6 行的代码相当于:

start.o:start.s

arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o start.o start.s

第11 行是清理规则,通过“make clean”可以删除编译生成的中间文件以及目标文件。
Makefile 文件我们就分析到这里。

修改Makefile

在上一小节我们已经写好了Makefile 文件,在里面链接的时候我们使用的“arm-linux-gnueabihf-ld -Ttext0X87800000 -o ledc.elf $^”命令,在这条命令里面我们通过“-Ttext”来指定链接地址0x87800000,这样所有的文件都会连接到以0x87800000 为起始地址的区域。有时候我们工程中很多文件需要链接到指定的区域(段),比如Linux 里面初始化函数会放到init 的段里面,所以我们需要能够自定义一些段,这些段的起始地址可以自由指定,同样我们也可以指定一个文件或者一个函数应该存放到哪个段里面。要完成这个功能
我们需要使用链接脚本。链接脚本是用于描述文件如何被连接在一起生成最终的可执行文件。主要目的是描述输入文件中的段如何被映射到输出文件中,并且控制输出文件的内存排列,一般我们编译生成的文件都包括text 段,data 段等等。

链接脚本实际上是通过一系列的指令组成的,每个命令是带有参数的关键字或者是对符号的赋值,可以使用分号分隔命令。像文件名之类的字符串可以直接输入,也可以使用“*”通配符,我们可以使用“SECTIONS”来定义一个段,在里面描述文件的内存分配。我们的代码编译出来,一般都会包括在test、data、bss 和rodata 这四个段里面,比如我们的代码要连接到0x80000000 这个地址,数据连接到0x81000000 这个地址,我们可以编写连接脚本,如下:

1 SECTIONS{
2 . = 0X80000000;
3 .text : {*(.text)}
4 . = 0X81000000;
5 .data ALIGN(4) : { *(.data) }
6 .bss ALIGN(4) : { *(.bss) }
7 }

第1 行通过“SECTIONS”,后面加一个大括号,这个大括号和第7 行大括号的是一对。有点类似C 语言中的函数。

第2 行对“.”的特殊符号赋值,“.”在链接脚本里面叫做定位计数器,默认定位计数器是0。我们要求代码连接到以0x80000000 开始的地址,因此这一行给“.”赋值0x80000000,表示以0x80000000 地址开始,后面的文件或者段都会以0x80000000 位起始地址开始连接。

第3 行的“.text”是段名,后面的冒号是语法要求。冒号后面的大括号里面可以填上要链接到“.text”这个段里面的所有文件,“(.text)”中的“”是通配符,表示所有输入文件的.text 段都放到“.text”中。

第4 行,我们的要求是数据放到0X81000000 开始的地方,所以我们需要重新设置定位计数器“.”,将其改为0X81000000。如果不重新设置的话会怎么样?假设“.text”段大小为0X100,那么接下来的.data段开始地址就是0X80000000+0X100=0X80000100,所以我们必须调整定位计数器为0X81000000。

第5 行跟第3 行一样,定义了一个名为“.data”的段,然后所有文件的“.data”段都放到这里面。这一行多了一个“ALIGN(4)”,这是什么意思呢?这是用来对“.data”这个段的起始地址做字节对齐的,ALIGN(4)表示4 字节对齐。也就是说段“.data”的起始地址要能被4 整除,一般常见的都是ALIGN(4)或者ALIGN(8),也就是4 字节或者8 字节对齐。

第6 行定义了一个“.bss”段,所有文件中的“.bss”数据都会被放到这个里面,“.bss”数据就是那些定义了但是没有被初始化的变量。

上面这些就是连接脚本的基本语法格式,我们按照这个语法来编写我们本实验的链接脚本,我们要求链接起始地址为0x87800000,start.o 要被链接到最开始的位置(0x87800000),根据要求我们在工程目录下建立文件“imx6ull.lds”文件,然后在里面输入下面的脚本:

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

上面链接脚本首先第1 行通过“SECTIONS”定义一个关键字。

然后第2 行设置定位计数器为0x87800000(我们链接的起始地址),

第3 行是定义text 段,

第5 行是指定开始链接的第一个文件“start.o”(首先要执行改文件,所以连接在最开始处)。

第6 行是链接的main.o,其实main.o 链接的位置无所谓,所以可以不用写出来,由编译器自行决定链接位置。

第9 行,第10 行设置rodata、data 按照4 字节对齐。

第11、13 行有“__bss_start”和“__bss_end”两个符号,分别对两个符号进行赋值,其值为定位符“.”,这两个符号用来保存.bss 段的起始地址和结束地址。前面说了.bss 段是定义了但是没有被初始化的变量,我们需要手动对.bss 段的变量清零的,因此我们需要知道.bss 段的起始和结束地址,这样我们直接对这段内存赋0 即可完成清零。
通过第11、13 行代码,.bss 段的起始地址和结束地址就保存在了“__bss_start”和“__bss_end”中,我们就可以直接在汇编或者C 文件里面使用这两个符号。然后我们保存并退出imx6ul.lds 文件,如果使用这个链接文件,我们需要修改Makefile 文件,打开当前工程的Makefile 文件,将里面的“arm-linux-gnueabihf-ld -Ttext 0X87800000 -o led.elf $^ ”内容改成“arm-linux-gnueabihf-ld -Timx6ul.lds -o led.elf $^”。实际就是将-T 后面的0x87800000 改成imx6ul.lds,表示使用imx6ul.lds 这个链接脚本,修改完成以后保存并退出Makefile 文件。

编译烧写验证

在上一章节我们编写好了工程的所有源码以及Mkafile 文件、链接文件,然后我们在终端输入make 命令编译该工程,运行结果如图所示:
在这里插入图片描述
通过上图我们看到生成了“led.bin”文件,然后我们还需要给“led.bin”文件添加DCD 的数据包头,通过ssh 上传“create_imx”工具(光盘资料的“i.MX6UL 终结者光盘资料\\01_开发及烧写工具\\2.裸机镜像制作工具”)到本工程目录下,使用“chmod 777 create_imx”命令,修改“create_imx”具有执行权限,然后在当前目录下运行“./create_imx led.bin”命令,生成最终的镜像文件:bare.imx,如图所示:
在这里插入图片描述
然后把生成的“bare.imx”镜像文件通过ssh 传输到windows 系统下,然后使用MFG 烧写工具烧写“bare.imx”文件到开发板,烧写完成后,开发板设置成flash 启动,然后给开发板上电,我们可以看到开发板上的LED 开始闪烁了。

以上是关于使用C语言按照GPIO操作流程点亮LED灯的主要内容,如果未能解决你的问题,请参考以下文章

在一个 STM32 点亮 LED 的程序中,部分代码如下?

树莓派实验1:GPIO点亮led灯

GPIO接口及点亮第一个LED灯

BlinkLED 点亮第一个LED灯

GPIO输出—使用固件库点亮LED

STM32固件库点亮LED灯