linux(第十二期)--裸机实验(C 语言版 LED 灯实验)-- Ubuntu20.04

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言

上一期的我们讲解了汇编版的点亮LED灯实验,但实际工作中是很少用到汇编去写嵌入式驱动的,毕竟汇编太难,而且写出来也不好理解,大部分情况下都 是使用 C 语言去编写的。只是在开始部分用汇编来初始化一下 C 语言环境,比如初始化 DDR、 设置堆栈指针 SP 等等,当这些工作都做完以后就可以进入 C 语言环境,也就是运行 C 语言代码,一般都是进入 main 函数。所以我们有两部分文件要做:
①、汇编文件
汇编文件只是用来完成 C 语言环境搭建。
②、 C 语言文件
C 语言文件就是完成我们的业务层代码的,其实就是我们实际例程要完成的功能。
注意:其实 STM32 也是这样的,只是我们在开发 STM32 的时候没有想到这一点,以 STM32F103 为 例,其启动文件 startup_stm32f10x_hd.s 这个汇编文件就是完成 C 语言环境搭建的,当然还有一些其他的处理,比如中断向量表等等。当 startup_stm32f10x_hd.s 把 C 语言环境初始化完成以后就会进入 C 语言环境。

一、硬件原理分析

还是和上一篇博客一样。

二、实验程序编写

现在可以新建一个工程,命名为:ledc

新建三个文件: start.S、main.c 和 main.h
start.S 是汇编文件
main.c 和 main.h 是 C 语言相关文件

1.汇编部分实验程序编写

I.MX6U 的汇编部分代码和 STM32 的启动文件startup_stm32f10x_hd.s 基本类似,初始化C环境,在start.s中输入如下代码:

复制代码
.global _start  		/* 全局标号 */

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

	/* 进入SVC模式 */
	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函数 		 */
  • 定义全局标号_start,作为代码入口。
  • 然后在设置处理器的模式:通过修改 CPSR 寄存器的 M [4:0] 位(设为 0X13),将处理器设为 SVC 模式,MRS将CPSR寄存器数据读出到通用寄存器里面,MSR指令将通用寄存器的值写入到CPSR寄存器里面去。
  • 再设置 SVC 模式下的 SP 指针为 0X80200000(基于 DDR3 地址范围,分配 2MB 栈空间)。
  • 最后:使用b指令,跳转到C语言函数,跳转到 C 语言的main函数。

2、c语言部分实验程序编写

C 语言部分有两个文件 main.c 和 main.h , main.h 里面主要是定义的寄存器地址。
在 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 中我们以宏定义的形式定义了要使用到的所有寄存器,后面的数字就是其地址,
比如 CCM_CCGR0 寄存器的地址就是 0X020C4068 ,这个很简单,很好理解。
接下看一下 main.c 文件,在 main.c 里面还是完成以下内容:
还是一样,先初始化LED

使能外设时钟

设置IO复用

配置GPIO的电气属性。
代码如下:

复制代码
#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_short(volatile unsigned int n)
{
	while(n--){}
}

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

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

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

		led_on();		/* 打开LED		 	*/
		delay(500);		/* 延时大约500ms 		*/
	}

	return 0;
}

main.c 包含 7 个简单函数,功能如下:

  • clk_enable:使能 CCGR0~CCGR6 控制的所有外设时钟。

  • led_init:初始化 LED 对应的 IO(配置复用功能、IO 属性、GPIO 功能),最终控制 GPIO 输出低电平点亮 LED。

  • led_on/led_off:控制 LED 的亮、灭。

  • delay_short:通过空循环实现延时;在 I.MX6U 主频 396MHz 时,delay_short(0x7ff)约实现 1ms 延时。

  • delay:封装delay_short,用于实现 ms 级延时。

  • main 函数是主函数:先调用clk_enable()led_init()完成时钟使能、LED 初始化,再通过while(1)循环实现 LED 约 500ms 间隔的循环亮灭。

  • 本实验的程序部分至此结束,接下来进行编译和测试。

三、编译下载

1、编写Makefile

新建 Makefile 文件,在 Makefile 文件里面输入如下内容:

复制代码
objs := start.o main.o            # 列出要生成的对象文件列表

ledc.bin:$(objs)                 # 目标:最终二进制文件 ledc.bin,依赖于 objs 列表
    arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^   # 使用链接脚本 imx6ul.lds 链接所有依赖($^),生成 ELF 文件 ledc.elf
    arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@ # 将 ELF 转换为裸二进制格式,输出到目标($@ 即 ledc.bin)
    arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis # 对 ELF 做反汇编,保存为 ledc.dis 便于查看

%.o:%.s                         # 模式规则:把 .s 汇编源编译成 .o 对象文件
    arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $< # 用交叉编译器编译($< 为源文件,$@ 为目标),启用警告、无标准库、优化等级 O2

%.o:%.S                         # 模式规则:把需要预处理的 .S 汇编源编译成 .o
    arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $< # 同上,用于 .S(预处理过的汇编)

%.o:%.c                         # 模式规则:把 .c C 源编译成 .o 对象文件
    arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $< # 交叉编译 C 源为对象文件,启用相同编译选项

clean:                           # 清理目标,用于删除构建生成物
    rm -rf *.o ledc.bin ledc.elf ledc.dis # 删除所有对象文件、二进制、ELF 和反汇编输出

第 1 行定义了一个变量 objs , objs 包含着要生成 ledc.bin 所需的材料: start.o 和 main.o ,也
就是当前工程下的 start.s 和 main.c 这两个文件编译后的 .o 文件。这里要注意 start.o 一定要放到
最前面!因为在后面链接的时候 start.o 要在最前面,因为 start.o 是最先要执行的文件!

第 3 行就是默认目标,目的是生成最终的可执行文件 ledc.bin , ledc.bin 依赖 start.o 和 main.o
如果当前工程没有 start.o 和 main.o 的时候就会找到相应的规则去生成 start.o 和 main.o 。比如
start.o 是 start.s 文件编译生成的,因此会执行第 8 行的规则。

2、编写链接脚本

创建一个imx6ul.lds文件

复制代码
带注释的脚本(行尾为中文注释):

SECTIONS{                         /* 定义链接脚本的节布局开始 /
. = 0X87800000;              / 将当前链接地址设置为 0x87800000 ,程序在内存中放置的起始地址 /
.text :                     / 定义 .text 节(代码段) /
{
start.o                 / 显式放入 start.o(通常包含启动代码和复位向量,放前面保证顺序) /
main.o                 / 显式放入 main.o(主程序) /
(.text)                 / 将所有未显式列出的输入文件的 .text 段都放进来(通配符匹配) /
}
.rodata ALIGN(4) : {(.rodata)}     /* 只读数据节,按 4 字节对齐;匹配 .rodata 及其变体(例如 .rodata.*) */
.data ALIGN(4) : { (.data) }         / 已初始化数据节,按 4 字节对齐,收集所有 .data 段 /
__bss_start = .;                     / 记录 bss 起始地址(用于运行时清零) */
.bss ALIGN(4) : { *(.bss) (COMMON) }     / 未初始化数据(BSS)和 COMMON 符号,按 4 字节对齐 /
__bss_end = .;                         / 记录 bss 结束地址(用于运行时清零的结束边界) /
}                                     / 链接脚本结束 */

功能:该文件是一个链接器脚本(供 ld 使用),定义了 ELF 输出文件中各节的内存布局与地址,控制代码、只读数据、已初始化数据和未初始化数据在最终镜像中的放置位置与顺序。

起始地址:. = 0x87800000; 将所有节放在物理/虚拟内存的该地址开始,这通常对应板子上可执行代码应驻留的地址。

顺序控制:显式列出 start.o 在最前面通常用于确保启动代码(复位向量、中断向量或初始化代码)位于预期位置,紧接着 main.o,然后把其余所有 .text 放入。

对齐与匹配:

ALIGN(4) 保证节按 4 字节边界对齐,避免未对齐访问。

*(.rodata*) 会匹配 .rodata 及所有以 .rodata 开头的节名。

BSS 符号:__bss_start 与 __bss_end 用于在启动代码中知道需要清零的内存范围(C 运行时通常在初始化时把 BSS 清为 0)。

链接时,ld 会按此脚本生成 ELF 文件并最终通过 objcopy 生成裸二进制镜像。

3、下载验证

在终端运行以下命令:

复制代码
chmod 777 imxdownload  //给予imxdownload可执行权限,一次即可
./imxdownload ledc.bin /dev/sdb

烧写的过程中可能会让你输入密码,输入你的 Ubuntu 密码即可完成烧写
烧写成功以后将 SD 卡插到开发板的 SD 卡槽中,然后复位开发板,如果代码运行正常的
话 LED0 就会以 500ms 的时间间隔亮灭。


总结

详细讲解了c语言版LED实验

相关推荐
HUGu RGIN8 小时前
Linux部署Redis集群
linux·运维·redis
先知后行。8 小时前
Linux 内核驱动 —— 锁机制
linux·运维·服务器
技术钱9 小时前
OutputParser输出解析器
linux·服务器·前端·python
先知后行。9 小时前
Liunx驱动 IO 模型
linux·运维·服务器
计算机安禾10 小时前
【Linux从入门到精通】第39篇:版本控制Git服务器搭建——Gitea/GitLab私有化部署
linux·服务器·git
浪客灿心10 小时前
Linux网络HTTP协议
linux
橙子也要努力变强10 小时前
volatile与信号
linux·服务器·c++
蜡笔小新拯救世界10 小时前
部分安全笔记总结
linux·网络·web安全
醇氧10 小时前
WSL 安装 Ubuntu 完整步骤(Windows 10/11 通用,极简无脑版)
linux·windows·ubuntu
leoufung10 小时前
LeetCode 30:Substring with Concatenation of All Words 题解(含 C 语言 uthash 实现)
c语言·leetcode·c#