最近想要系统地学习下linux 驱动程序的设备树的知识。韦东山老师提到驱动有如下三种写法:
驱动写法 | 优缺点 |
---|---|
1.将硬件信息写在驱动程序中 | 简单,不易扩展,有硬件信息改动需要重新编译 |
2.使用platform总线设备驱动模型将驱动程序软硬件信息分离 | 稍复杂,易扩展,有冗余代码,有硬件信息改动需要重新编译 |
3.使用设备树文件,驱动程序解析设备树文件,读取相关硬件信息 | 稍复杂,易扩展,无冗余代码,有硬件信息改变,只需要更改dts文件,无需重新编译? |
因为工作初次接触到dts文件,dts文件的语法规则等虽然看了写博客,但是工作的过程中还是云里雾里的,所以想着使用简单的点亮led灯的程序来学习追踪下以上驱动程序三种方法的演变过程中的硬件信息的变化。
提到单纯的硬件信息,也就是不管软件相关的,即使有没有linux系统也不会变的信息,于是就先从控制led等的裸板程序开始。
这里的地址指针具体细节是什么,还不太理解,裸板程序从单片机的角度来看,应该是cpu直接根据物理地址访问相应的寄存器。那么如何访问呢?比如,我们在芯片手册中查找到设置S5P6818的C组GPIO功能的寄存器之一的GPIOCALTFN0的物理地址为 0xC001C020,那么CPU根据这个地址找到相应的寄存器后,如何读写这个寄存器存储的值呢?通过C语言中的* 符号来操作该物理地址里面的值进行写操作。(那么裸板开发中,cpu的读操作是?)
/*向该寄存器的bit[24:25]写01*/
*(unsigned long *)0XC001C020 &= ~(3 << 24);
*(unsigned long *)0XC001C020 |= (1 << 24);
好了,到这里我们就基本上理清楚了LED等点亮熄灭和CPU如何操作的原理了,接下来就参照芯片手册和原理图来编写裸机代码了。
1.将LED灯对应GPIO引脚设置为GPIO功能。
比如,D25 led灯对应的引脚为GPIOC12,复用功能有四种:引脚为:SA12/GPIOC12/SPITXD2/SDnRST2
SA12:此引脚可以作为地址线(例如:内存) ( A: addr)
GPIOC12:此引脚可以作为普通的输入或者输出引脚(LED1)
SPITXD2:此引脚可以作为 SPI 总线的发送数据的引脚
SDnRST2:SD 卡的 Reset 复位引脚 ( RST: RESET )
2.然后将GPIO引脚设置为输出功能
3.最后:开灯:配置 GPIO 输出寄存器为 0 关灯配置 GPIO 输出寄存器为 1
4.编码,先写头文件nakedboard_led.h
#ifndef __NAKEDBOARD_H__
#define __NAKEDBOARD_H__
/*声明寄存器的基地址信息*/
//定义设置管脚功能的寄存器(将设置为GPIO功能)
#define GPIOCALTFN0 *((unsigned long*)0XC001C020)
#define GPIOBALTFN1 *((unsigned long *)0XC001B020)
//定义设置GPIO管脚输入输出的寄存器
#define GPIOCOUTENB *((unsigned long *)0XC001C004)
#define GPIOBOUTENB *((unsigned long *)0XC001B004)
//定义GPIO管脚输出寄存器
#define GPIOCOUT *((unsigned long *)0XC001C000)
#define GPIOBOUT *((unsigned long *)0XC001B000)
/*声明操作函数*/
void led_init(void);
void led_on(void);
void led_off(void);
void delay(int);
#endif __NAKEDBOARD_H__
再写裸板程序nakedboard_led.c
/*led裸板程序*/
#include "nakedboard_led.h"
/*程序的入口函数的定义,裸板程序的入口函数不是main函数自己定义的,
但是一定要放在程序的最前面(紧挨着头文件),文章末尾有解释*/
void led_test(void){
//初始化led的硬件
led_init();
while(1){
led_on();
delay(0x1000000);
led_off();
delay(0x1000000);
}
return ;
}
//延迟函数的定义
void delay(int n){
int i;
for(i = n; i != 0; i--);
}
/*初始化控制led的GPIO相关配置*/
void led_init(void){
//配置led对应的管脚为GPIO模式
GPIOCALTFN0 &= ~(3 << 24);
GPIOCALTFN0 |= (1 << 24); /*GPIOC12*/
GPIOCALTFN0 &= ~(3 << 14);
GPIOCALTFN0 |= (1 << 14); /*GPIOC7*/
GPIOCALTFN0 &= ~(3 << 22);
GPIOCALTFN0 |= (1 << 22); /*GPIOC11*/
GPIOBALTFN1 &= ~(3 << 20);
GPIOBALTFN1 |= (1 << 20); /*GPIOB26*/
/*将所有的GPIO设置为输出模式*/
GPIOCOUTENB |= (1 << 12); //GPIOC12输出使能
GPIOCOUTENB |= (1 << 7); //GPIOC7输出使能
GPIOCOUTENB |= (1 << 11);//GPIOC11输出使能
GPIOBOUTENB |= (1 << 26);//GPIOB26输出使能
return ;
}
void led_on(void){
GPIOCOUT &= ~(1 << 12); // 向BIT[12]写0,开GPIOC12对应的灯
GPIOCOUT &= ~(1 << 7);
GPIOCOUT &= ~(1 << 11);
GPIOBOUT &= ~(1 << 26);
return ;
}
void led_off(void){
GPIOCOUT |= (1 << 12); //向BIT[12]写1,关GPIOC12对应的灯
GPIOCOUT |= (1 << 7);
GPIOCOUT |= (1 << 11);
GPIOBOUT |= (1 << 26);
return ;
}
至此,程序编写完毕,接下里就是使用对应的交叉编译器,编译为bin的二进制文件,然后使用tftp 软件烧写到开发板的内存里运行了。
这里使用的交叉编译器是arm-cortex_a9-linux-gnueabi-gcc
编译步骤如下:
3.arm-cortex_a9-linux-gnueabi-objcopy -O binary led.elf led.bin
说明:利用 arm…objcopy 工具将 ELF 格式的可执行文件再次获取到其中的真正的二进制文件信息
6.没有什么问题的话,就可以看到下位机的4个LED灯按照上面裸板程序写的逻辑那样一起闪烁起来了。
整个过程中,我遇到一个问题:就是第一次我把delay函数定义在led_test函数前面去了,即为如下代码演示
/*led裸板程序*/
#include "nakedboard_led.h"
//延迟函数的定义
void delay(int n){
int i;
for(i = n; i != 0; i--);
}
..............
/*程序的入口函数的定义*/
void led_test(void){
//初始化led的硬件
led_init();
while(1){
led_on();
delay(0x1000000);
led_off();
delay(0x1000000);
}
return ;
}
结果就是运行之后,发现下位机没有任何反应。上位机出现如下提示
Starting application at 0x30008000 …
Application terminated, rc = 0x1
一番了解才知道,要将入口函数写在程序最开头,才能在编译为程序的入口函数,该错误中将delay函数放在了最开头,结果通过反编译手段查看,程序执行的入口函数是delay函数,所以执行不了,给了如上错误提示!见下图
反编译手段:arm-cortex_a9-linux-gnueabi-objdump -D led.elf > led.dis
然后使用vim查看led.dis文件。发现如上错误,将程序设计的入口函数led_test放到程序最开头的位置,再次编译运行执行,就OK了。
同样使用反编译手段查看,发现程序的入口函数OK了,见下图