您现在的位置是:首页 >其他 >【裸机驱动LED】使用C代码驱动LED(三)—— C代码编写篇网站首页其他
【裸机驱动LED】使用C代码驱动LED(三)—— C代码编写篇
前面只使用了汇编代码来驱动LED,但是对于后续一些比较复杂的逻辑,使用汇编代码编写驱动的难度太大,因此,这次我们要使用C语言代码来驱动LED。
除了C代码外,依然需要编写汇编代码,在没有OS的情况下,环境的初始化是需要通过汇编代码来完成的。
- 汇编文件:用于完成C代码的环境搭建
- C文件:实现驱动逻辑
目录
一、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 寄存器
参考链接:
初始化 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