【入门级别】linux驱动的三种写法之前言——裸机程序

linux驱动的三种写法之前言——裸机程序

    • 引子
    • 软硬件平台
    • 裸板编程
      • 编程准备
      • 编程思路
    • 编译烧录及运行
    • 遇到问题及其解决

引子

最近想要系统地学习下linux 驱动程序的设备树的知识。韦东山老师提到驱动有如下三种写法:

驱动写法 优缺点
1.将硬件信息写在驱动程序中 简单,不易扩展,有硬件信息改动需要重新编译
2.使用platform总线设备驱动模型将驱动程序软硬件信息分离 稍复杂,易扩展,有冗余代码,有硬件信息改动需要重新编译
3.使用设备树文件,驱动程序解析设备树文件,读取相关硬件信息 稍复杂,易扩展,无冗余代码,有硬件信息改变,只需要更改dts文件,无需重新编译?

因为工作初次接触到dts文件,dts文件的语法规则等虽然看了写博客,但是工作的过程中还是云里雾里的,所以想着使用简单的点亮led灯的程序来学习追踪下以上驱动程序三种方法的演变过程中的硬件信息的变化。

提到单纯的硬件信息,也就是不管软件相关的,即使有没有linux系统也不会变的信息,于是就先从控制led等的裸板程序开始。

软硬件平台

  1. 开发板:S5P6818开发板
    【入门级别】linux驱动的三种写法之前言——裸机程序_第1张图片
  2. 主要参数:三星S5P6818开发板尺寸为:260*170mm。
    搭载Android5.1操作系统,标配2G内存+16G存储,配置10.1寸IPS屏(1920x1200分辨率)。支持5路USB HOST接口;一路OTG接口;支持一键USB启动;支持一键SD卡启动;支持MIPI LCD接口和LVDS LCD接口;支持MIPI、YUV接口摄像头;支持HDMI接口;支持1路RS485接口;2路2W喇叭接口;支持待机功能(电流小于0.1W,15mA)。支持4G模块、Wifi/蓝牙4.0/GPS模块、500W摄像头等。在配套的显示屏都支持7/10.1寸LVDS屏、10.1寸Mipi显示屏;都支持YUV格式的500W摄像头和MIPI格式摄像头,同样支持蓝牙4.0、Wifi、LAN等常用接口功能。

裸板编程

编程准备

  1. 实现开发板上的4个LED灯闪烁。
  2. 我们要查看硬件原理图,看看怎样让这四个LED灯亮灭。硬件原理图如下
    【入门级别】linux驱动的三种写法之前言——裸机程序_第2张图片
    由上图我们得知,D22,D23,D24,D25四个LED灯要是亮的话,对应的GPIO引脚应该输出为低电平,反之。而且,与这四个灯相连的gpio引脚的另一端与CPU相连,见下图(GPIOC12,GPIOC7,GPIOC11,GPIOB26)
    【入门级别】linux驱动的三种写法之前言——裸机程序_第3张图片
    由此可见,CPU来控制GPIO输出高/低电平来熄灭/点亮LED灯的。那么CPU是如何通过这些GPIO引脚控制对应的LED灯的呢?
    答:CPU核不是直接访问GPIO引脚来控制的,而是通过软件地址来访问GPIO控制器(CPU内部的硬件,实际上是一些寄存器),GPIO控制器来访问控制相应的GPIO引脚。(本质就是CPU给GPIO控制器发命令)见下图
    【入门级别】linux驱动的三种写法之前言——裸机程序_第4张图片

这里的地址指针具体细节是什么,还不太理解,裸板程序从单片机的角度来看,应该是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
编译步骤如下:

  1. arm-cortex_a9-linux-gnueabi-gcc -nostdlib -c -o led.o led.c
    说明:-nostdlib:告诉编译器,此程序不使用标准 C 库
    -c:将C语言编译成机器语言,但是不链接
  2. arm-cortex_a9-linux-gnueabi-ld -nostartfiles -nostdlib
    -Ttext=0x48000000 -o led.elf led.o

    说明:arm-cortex_a9-linux-gnueabi-ld为链接器
    -nostartfiles:告诉链接器,此代码无需启动文件
    -Ttext=0x48000000:告诉链接器,代码段的起始地址为 0x48000000(下位机的内存运行起始地址,是6818开发板设计规定的)
    -o led.elf:链接生成 ELF 格式的可执行文件
    切记:此时此刻 led.elf 不能在没有操作系统的环境中运行

3.arm-cortex_a9-linux-gnueabi-objcopy -O binary led.elf led.bin
说明:利用 arm…objcopy 工具将 ELF 格式的可执行文件再次获取到其中的真正的二进制文件信息

  1. cp led.bin /tftpboot //拷贝到下载目录
  2. 下位机测试
    重启下位机,进入 uboot 命令行模式,执行:
    ping 192.168.1.8 //是上位机的ip地址,确保上下位机的网络是连接的
    tftp 48000000 led.bin
    go 48000000

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
【入门级别】linux驱动的三种写法之前言——裸机程序_第5张图片
一番了解才知道,要将入口函数写在程序最开头,才能在编译为程序的入口函数,该错误中将delay函数放在了最开头,结果通过反编译手段查看,程序执行的入口函数是delay函数,所以执行不了,给了如上错误提示!见下图
【入门级别】linux驱动的三种写法之前言——裸机程序_第6张图片
反编译手段:arm-cortex_a9-linux-gnueabi-objdump -D led.elf > led.dis
然后使用vim查看led.dis文件。发现如上错误,将程序设计的入口函数led_test放到程序最开头的位置,再次编译运行执行,就OK了。
同样使用反编译手段查看,发现程序的入口函数OK了,见下图
【入门级别】linux驱动的三种写法之前言——裸机程序_第7张图片

你可能感兴趣的:(学习笔记,个人技术成长记录)