学习裸机驱动开发也有一阵子,终于是把几种裸机驱动方法学完了,总体来说难度并不大。因为毕竟有基础在,能大概看懂数据手册和知道如何是控制寄存器。
驱动方法目前所学共有四种:
1.纯汇编驱动
2.汇编驱动头文件,剩下的使用C语言来驱动
3.模仿STM32的方式来进行驱动
4.移植NXP的官方SDK来进行驱动
四种方法有共同点和不同点,下面我来 一 一总结:
共同点:
1.无论怎么样,都必须使用汇编语言来启动头文件。选择了启动方式之后如果不使用汇编语言来启动头文件,任你后面的代码再怎么写你也驱动不了开发板。驱动代码对于接下来的所有方法都是相同的!
2.编译程序的步骤都是大同小异的。第一步先把软件代码写好,第二步将.C(源文件) .S(汇编文件)文件转换为.O(已编译不可执行)文件。第三步将所有的.O文件链接为elf格式的可执行文件。最后将.elf文件转换成.bin文件。这个.bin文件会和头文件一起被写入你所选择的启动方式里面去。我建议还可以在这里再加上一步,反汇编elf文件。为什么呢?因为反汇编最重要的就是可以看到你文件的开头文件,一定要是.S文件开头,只有汇编文件开头启动一些外设,其他文件才有写入的可能!
下面附上:个C语言代码源文件需要经过如图四个步骤,才能变成可以被计算机执行的文件。
3.Makefile。这个肯定是毋庸置疑很好用的make工具。使用它可以省去很多的操作步骤和少些很多的代码。而且如果需要修改一部分链接文件的代码,可以直接修改,然后编译。如果不使用,修改.C .S文件之后,又要重新走一遍编译程序的步骤,很繁琐不建议!而且最重要的一天,以后要学习的大头:系统移植和uboot都是要经常使用Makefile的!
CROSS_COMPILE ?= arm-linux-gnueabihf-
NAME ?= ledc //如果以后其他模块需要使用,只需要改名字就好了
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump
OBJS := start.o main.o
$(NAME).bin:$(OBJS)
$(LD) -Timx6u.lds -o $(NAME).elf $^
$(OBJCOPY) -O binary -S $(NAME).elf $@
$(OBJDUMP) -D -m arm $(NAME).elf > $(NAME).dis
%.o : %.c
$(CC) -Wall -nostdlib -c -O2 -o $@ $<
%.o : %.S
$(CC) -Wall -nostdlib -c -O2 -o $@ $<
clean:
rm -rf *.o $(NAME).bin $(NAME).elf $(NAME).dis
download:
./imxdownload $(NAME).bin /dev/sdb
4.链接脚本也是可以通用的。有了链接脚本我们就可以调控很多文件到我们指定的区域!我们是通过“-Ttext”来指定链接地址是 0X87800000 的,这样的话所有的文件都会链接到以0X87800000 为起始地址的区域。但是有时候我们很多文件需要链接到指定的区域,或者叫做段里面,比如在 Linux 里面初始化函数就会放到 init 段里面。因此我们需要能够自定义一些段,这些段的起始地址我们可以自由指定,同样的我们也可以指定一个文件或者函数应该存放到哪个段里面去。要完成这个功能我们就需要使用到链接脚本。
SECTIONS{
. = 0x87800000; //先将指定链接区域选中为0x87800000 .:定位计数器
.text : //设置.text段
{
start.o //start.o 里面包含着第一个要执行的指令,所以一定要链接到最开始的地方。
*(.text) //剩余的所有.text文件 通配符*
}
.rodate ALIGN(4) : {*(.rodata*)} //设置.rodata
.data ALIGN(4) : {*(.data)} //设置.data段,对“.data”这个段的起--
//--始地址做字节对齐的, ALIGN(4)表示 4 字节对齐
__bss_start=.;
.bss ALIGN(4) : {*(.bss) *(COMMON)}
__bss_end=.;
}
//.bss 段的起始地址和结束地址保存在“__bss_start”和“__bss_end”,.bss 段是定义了但是没有被初始化的变量
不同点:
1.纯汇编语言驱动代码是简洁一些,但是如果用它来来做一些复杂的控制,我认为局限性比较大,例如使用汇编语言来实现呼吸灯、流水灯,这些会很麻烦。
2.模仿STM32来驱动是很大不同的,我们需要将结构体抽象为外设,获得外设寄存器的基地址。添加寄存器结构体(在结构体中添加寄存器的时候一定要注意地址的连续性,如果不连续的话要添加占位。)由于编写寄存器结构体代码实在过于枯燥和繁琐,我只展示了一部分,剩下的按照方法来就行。
/***************************************************************
Copyright © godfatherlzq
文件名 : imx6ul.h
作者 : godfatherlzq
版本 : V1.0
描述 : IMX6UL相关寄存器定义,参考STM32寄存器定义方法
其他 : 无
日志 : 初版V1.0 2022.4.22 李柱奇创建
**************************************************************/
/*
* 外设寄存器组的基地址
*/
#define CCM_BASE (0X020C4000)
#define CCM_ANALOG_BASE (0X020C8000)
#define IOMUX_SW_MUX_BASE (0X020E0044)
#define IOMUX_SW_PAD_BASE (0X020E0204)
#define GPIO1_BASE (0x0209C000)
#define GPIO2_BASE (0x020A0000)
#define GPIO3_BASE (0x020A4000)
#define GPIO4_BASE (0x020A8000)
#define GPIO5_BASE (0x020AC000)
/*
* CCM寄存器结构体定义,分为CCM和CCM_ANALOG
*/
typedef struct
{
volatile unsigned int CCR;
volatile unsigned int CCDR;
volatile unsigned int CSR;
volatile unsigned int CCSR;
volatile unsigned int CACRR;
volatile unsigned int CBCDR;
volatile unsigned int CBCMR;
volatile unsigned int CSCMR1;
volatile unsigned int CSCMR2;
volatile unsigned int CSCDR1;
volatile unsigned int CS1CDR;
volatile unsigned int CS2CDR;
volatile unsigned int CDCDR;
volatile unsigned int CHSCCDR;
volatile unsigned int CSCDR2;
volatile unsigned int CSCDR3;
volatile unsigned int RESERVED_1[2];
volatile unsigned int CDHIPR;
volatile unsigned int RESERVED_2[2];
volatile unsigned int CLPCR;
volatile unsigned int CISR;
volatile unsigned int CIMR;
volatile unsigned int CCOSR;
volatile unsigned int CGPR;
volatile unsigned int CCGR0;
volatile unsigned int CCGR1;
volatile unsigned int CCGR2;
volatile unsigned int CCGR3;
volatile unsigned int CCGR4;
volatile unsigned int CCGR5;
volatile unsigned int CCGR6;
volatile unsigned int RESERVED_3[1];
volatile unsigned int CMEOR;
} CCM_Type;
只要将这个配置的头文件配置好之后,剩下的我们就可以模仿STM32调用底层寄存器的方法来编写驱动代码啦。熟悉的感觉又回来了哈哈哈!
#include "imx6u.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复用 */
IOMUX_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 低转换率
*/
IOMUX_SW_PAD->GPIO1_IO03 = 0X10B0;
/* 3、初始化GPIO */
GPIO1->GDIR = 0X0000008; /* GPIO1_IO03设置为输出 */
/* 4、设置GPIO1_IO03输出低电平,打开LED0 */
GPIO1->DR &= ~(1 << 3);
}
/*
* @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);
}
3.通过移植NXP的官方SDK包来写驱动代码。上面自己来编写寄存器结构体确实过于枯燥,在STM32中TI公司编写了这部分代码,那在我们的I.MX6U系列芯片里面有吗?当然是有的,就是官方的SDK包。只需要在官网下载完毕后通过FZ软件传给Ubuntu,装进去就可以使用了!但是因为我们在SDK包里会用到很多数据类型,我们就需要自己来设置一个头文件来定义一些常用的数据类型。
#ifndef __CC_H
#define __CC_H
#define __I volatile
#define __O volatile
#define __IO volatile
typedef signed char int8_t;
typedef signed short int16_t;
typedef signed int int32_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
typedef signed char s8;
typedef signed short s16;
typedef signed int s32;
typedef signed long long s64;
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef unsigned long long u64;
#endif
这部分和STM32中我们熟知的u8也差不多。有了配置好的SDK包编写代码会比以前的更简单
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03,0);
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03,0X10b0);
通过两个函数就可以解决所有配置的问题,很方便!
四种驱动方式,各有各的好处,我觉得应该都会一点。了解他们的优缺点,已便将来逻辑开发时能够快速的选择最好的驱动方法!总结一遍下来,感觉我对基础驱动的理解又深刻了一分,加油!