前面只使用了汇编代码来驱动LED,但是对于后续一些比较复杂的逻辑,使用汇编代码编写驱动的难度太大,因此,这次我们要使用C语言代码来驱动LED。
除了C代码外,依然需要编写汇编代码,在没有OS的情况下,环境的初始化是需要通过汇编代码来完成的。
- 汇编文件:用于完成C代码的环境搭建
- C文件:实现驱动逻辑
目录
一、start.s 汇编文件
1、编写思路
2、具体实现
3、完整汇编实现
二、头文件 register.h
1、C代码访问寄存器的方式
2、register.h 文件
三、C 文件 led.c
1、初始化时钟源
2、设置IO复用、初始化GPIO
3、GPIO 输出(LED 亮灭控制)
4、延时函数
5、完整 led.c 文件
四、Makefile文件
一、start.s 汇编文件
1、编写思路
这里的汇编代码仅用于C代码的环境搭建,初始化环境的方式和 stm32 类似,这里我们可以先参考stm32 的初始化步骤:
- 设置栈大小
- 初始化SP指针
- 设置堆大小
- 中断复位
- 其他初始化(如DDR初始化)
- 跳转到 main 函数
但是对于 I.MX6U 而言,无需这么复杂,也不需要进行DDR的初始化,因为在 bin 文件的头部信息中,DCD部分已经初始化了DDR。基本步骤如下:
- 设置为 SVC 模式
- 初始化 SP 指针
- 跳转到 main 函数
2、具体实现
切换 SVC 模式
切换到SVC模式要用到控制寄存器 CPSR,其中控制模式切换的是第 4-0 bit。因此,切换到 SVC 模式的步骤如下:
- 取出 cpsr 寄存器中的值,保存到 r0。(目的是防止因为误操作而改变 cpsr 其他位的值)
- r0 寄存器将低五位清零。(低五位用于控制模式切换)
- r0 的低五位或上 10011(0x13)
- 将修改以后的值写入到 cpsr
工作模式 | 取值(左边高位,右边低位) |
User | 10000 |
FIQ | 10001 |
IRQ | 10010 |
SVC | 10011 |
Abort | 10111 |
Undef | 11011 |
System | 11111 |
Monitor | 10110 |
/* CPSR 比较特殊,不能使用mov或者ldr访问 */
/* 必须使用 mrs指令访问 */
mrs r0, cpsr @ 将 cpsr 寄存器的内容读取到 r0 寄存器
bic r0, r0, #0x1f @ 将 r0 的低五位清零,运算结果放到 r0
orr r0, r0, #0x13 @ 让 r0 的低五位或上 10011(0x13),结果保存到 r0
msr cpsr, r0 @ 将 r0 的值写入到 cpsr 寄存器
参考链接:
CPSR 寄存器详解
访问(读写)CPSR寄存器
数据处理指令 —— 位清零(bic)、或(orr)
初始化 SP 指针
C 语言调用main函数时,会为main函数创建堆栈,其中 SP指针表示栈顶的地址。那么SP 地址应该指向多少呢?对此我们需要知道如下几点:
- 栈区大小分配 2M。2 M的栈空间只要不出现无限递归,一般没什么大问题
- 代码的运行在DDR,DDR的范围为 0x80000000 ~ 0xA0000000(512M)
- ARM 处理器是满减栈,即由高地址向低地址存储。也就是说,每存一个数据,地址自减
栈是反向增长的,而且栈大小为 2M,因此,SP 一开始应指向 0x80200000
/* 初始化栈指针 */
ldr sp,=0x80200000 @ 设置栈指针
b main @ 跳转到 main 函数
3、完整汇编实现
.global _start
_start:
/* 切换到 SVC 模式 */
mrs r0, cpsr @ 将 cpsr 寄存器的内容读取到 r0 寄存器
bic r0, r0, #0x1f @ 将 r0 的低五位清零,运算结果放到 r0
orr r0, r0, #0x13 @ 让 r0 的低五位或上 10011(0x13),结果保存到 r0
msr cpsr, r0 @ 将 r0 的值写入到 cpsr 寄存器
/* 初始化栈指针 */
ldr sp,=0x80200000 @ 设置栈指针
b main @ 跳转到 main 函数
二、头文件 register.h
这里我们依然是通过地址来访问寄存器,只不过直接使用地址可读性太差,我们可以将要用到的寄存器地址保存到头文件,并声明成宏。
1、C代码访问寄存器的方式
之前在使用汇编驱动LED的时候,我们是通过访问寄存器的地址来写入内容的,C代码也可以访问地址,因为C语言中的指针,某种意义上就可以看做是一个地址。
比如我们要访问 0x20C4068,如果我们直接 *(0x20C4068),那么解引用的时候,应该访问多少个字节呢?指针类型决定了每次访问的字节数。一个寄存器的大小是4个字节,所以应该对这个地址强制类型转换成 unsigned int
*((unsigned int*)0x20C4068)
这里最好再加一个 volatile 关键字,volatile提醒编译器它后面所定义的变量随时都有可能改变,每次使用该变量时都去读取内存地址。(因为编译器可能图省事,直接把内容保存到CPU的寄存器,每次读的是CPU寄存器的内容)
*((volatile unsigned int*)0x20C4068)
2、register.h 文件
比如我们要初始化 CCR0 时钟源,CCR0 寄存器的地址为 0x20C4068,那就是
*((volatile unsigned int*)0x20C4068) = 0xffffffff;
这样的可读性实在太差,所以我们打算给等号左边起个好懂一点的名字,下面就是 register.h 文件中的内容(关于寄存器的地址,参考这篇:裸机驱动LED —— 寄存器解析篇)
#ifndef _register_h
#define _register_h
typedef unsigned int uint32_t;
/*
* 时钟相关寄存器地址
*/
#define CCM_CCGR0 *((volatile uint32_t*)0x20C4068)
#define CCM_CCGR1 *((volatile uint32_t*)0x20C406C)
#define CCM_CCGR2 *((volatile uint32_t*)0x20C4070)
#define CCM_CCGR3 *((volatile uint32_t*)0x20C4074)
#define CCM_CCGR4 *((volatile uint32_t*)0x20C4078)
#define CCM_CCGR5 *((volatile uint32_t*)0x20C407C)
#define CCM_CCGR6 *((volatile uint32_t*)0x20C4080)
/*
* IOMUX 相关寄存器地址
*/
#define SW_MUX_GPIO1_IO03 *((volatile uint32_t*)0x020E0068) // 设置IO复用
#define SW_PAD_GPIO1_IO03 *((volatile uint32_t*)0x020E02F4) // 设置电气属性
/*
* 设置GPIO输出相关寄存器地址
*/
#define GPIO1_DR *((volatile uint32_t*)0x0209C000) // GPIO输出
#define GPIO1_GDIR *((volatile uint32_t*)0x0209C004) // 设置输入还是输出
#endif
三、C 文件 led.c
C 代码的实现就可以参考之前使用汇编代码驱动LED的步骤了。
- 初始化时钟源
- 设置IO复用为GPIO
- 初始化GPIO(设置电气属性)
- 设置GPIO输出
初始化寄存器的值,在之前的博客已经介绍了,可以参考:裸机驱动LED —— 寄存器解析篇
1、初始化时钟源
void clk_enable()
{
GPIO_CCGR0 = 0xffffffff;
GPIO_CCGR1 = 0xffffffff;
GPIO_CCGR2 = 0xffffffff;
GPIO_CCGR3 = 0xffffffff;
GPIO_CCGR4 = 0xffffffff;
GPIO_CCGR5 = 0xffffffff;
GPIO_CCGR6 = 0xffffffff;
}
2、设置IO复用、初始化GPIO
void led_init()
{
/* 1、设置GPIO复用 */
SW_MUX_GPIO1_IO03 = 0x5;
/* 2、设置GPIO电气属性 */
SW_PAD_GPIO1_IO03 = 0x10B0;
/* 3、GPIO 设为输出 */
GPIO1_GDIR = 0x00000008;
}
3、GPIO 输出(LED 亮灭控制)
LED低电平亮,高电平灭。0x08 代表第 3 bit的位置为高电平,其实简单粗暴一点,GPIO1_DR = 0 也是可以的,GPIO1_DR |= 0x08 的目的是不影响其他位置。
void led_on()
{
GPIO1_DR &= (~0x08);
}
void led_off()
{
GPIO1_DR |= 0x08;
}
4、延时函数
因为本次的目的不止是让LED亮,还要让LED闪烁,这就需要用到延时函数
void delay(unsigned int n)
{
while (n--)
{
delay_short(0x7ff);
}
}
void delay_short(unsigned int n)
{
while(n--) {}
}
5、完整 led.c 文件
#include "register.h"
void clk_enable(); // 时钟源初始化
void led_init(); // 设置IO复用为GPIO、初始化GPIO
void delay_short(unsigned int n); // 短时延时
void delay(unsigned int n); // 延时
void led_on(); // 点灯
void led_off(); // 熄灯
int main(void)
{
/* 1、初始化时钟源 */
clk_enable();
/* 2、初始化LED */
led_init();
led_on();
while (1)
{
led_on();
delay(500);
led_off();
delay(500);
}
return 0;
}
void clk_enable()
{
CCM_CCGR0 = 0xffffffff;
CCM_CCGR1 = 0xffffffff;
CCM_CCGR2 = 0xffffffff;
CCM_CCGR3 = 0xffffffff;
CCM_CCGR4 = 0xffffffff;
CCM_CCGR5 = 0xffffffff;
CCM_CCGR6 = 0xffffffff;
}
void led_init()
{
/* 1、设置GPIO复用 */
SW_MUX_GPIO1_IO03 = 0x5;
/* 2、设置GPIO电气属性 */
SW_PAD_GPIO1_IO03 = 0x10B0;
/* 3、GPIO 设为输出 */
GPIO1_GDIR = 0x00000008;
}
void delay(unsigned int n)
{
while (n--)
{
delay_short(0x7ff);
}
}
void delay_short(unsigned int n)
{
while(n--) {}
}
void led_on()
{
GPIO1_DR &= (~0x08);
}
void led_off()
{
GPIO1_DR |= 0x08;
}
四、Makefile文件
之前只有 .s 文件,现在既有 .s 文件,也有 .c 文件,因此我们需要分别将 .s 文件和 .c 文件先转化为 .o 文件,然后再转化为 elf 文件
注意下面的 OBJS 变量,start.o 必须放在其他 .o 文件的前面,因为 start.s 作用是搭建C环境,所以必须在运行 led.c 之前运行。下面就是 start.o 没有放在最开始的后果:
TOOLCHAIN_PATH := /home/pigeon/workspace/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
CC := $(TOOLCHAIN_PATH)/arm-linux-gnueabihf-gcc
LD := $(TOOLCHAIN_PATH)/arm-linux-gnueabihf-ld
OBJCOPY := $(TOOLCHAIN_PATH)/arm-linux-gnueabihf-objcopy
OBJDUMP := $(TOOLCHAIN_PATH)/arm-linux-gnueabihf-objdump
OBJS := start.o led.o
led.bin: $(OBJS)
$(LD) -Ttext 0x87800000 -o led.elf $^
$(OBJCOPY) -O binary -S -g led.elf $@
$(OBJDUMP) -D led.elf > led.dis
# 将 .s 转化为 .o
%.o: %.s
$(CC) -c -Wall -nostdlib -o $@ $<
# 将 .c 转化为 .o
%.o: %.c
$(CC) -c -Wall -nostdlib -o $@ $<
.PHONY:clean
clean:
rm -rf *.o *.elf *.imx *.dis *.bin