使用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 个函数,下面一个个的看下每个函数的具体功能:
- void clk_enable(void)函数使能CCM_CCGR0-CCM_CCGR6 的时钟(使能所有外设时钟)。
- void led_init(void)函数初始化LED 对应的IO(复用成GPIO,默认下拉,输出低电平)。
- void led_on(void)函数点亮LED(对应GPIO 输出低电平)。
- void led_off(void)函数关闭LED(对应GPIO 输出高电平)。
- void delay_short(volatile unsigned int n)延时函数。
- void mdelay(volatile unsigned int n)毫秒级延时函数。
- 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.elf”文件转换成“led.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灯的主要内容,如果未能解决你的问题,请参考以下文章