嵌入式开发实验

二、Linux GCC编译、交叉编译与GDB调试

实验平台

l VMware workstation虚拟机内嵌口袋云系统,快速创建多种操作系统的虚拟机:(操作系统:Ubuntu18.04

CentOS,Windows10或Windows11)

l 实验目的

l 学习Linux系统下编写c语言文件、编译文件、

l 学习编写shell程序实现系统功能

实验准备

l VMware workstation虚拟机+Ubuntu18.04系统,安装gcc编译器,交叉编译器安装包。

实验案例:

1. PC平台的gcc编译过程

首先打开终端,输入gcc -v,显示当前系统的gcc版本:

然后创建一个c语言源文件hello.c:

# include <stdio.h>

int main()

{

   printf("Hello Linux\n");

    return 0;

}

执行gcc hello.c -o hello,然后直接./hello

2. GCC创建和使用静态链接库

1)首先进入第二个实验目录second,然后创建4个文件add.c, sub.c,div.c,test.h

/*****add.c*****/

#include "test.h"

int add(int a,int b)

{

    return a + b;

}

/*****sub.c*****/

#include "test.h"

int sub(int a,int b)

{

    return a - b;

}

/*****div.c*****/

#include "test.h"

int div(int a,int b)

{

    return a / b;

}

/*****test.h*****/

#ifndef __TEST_H_

#define __TEST_H_

int add(int a,int b);

int sub(int a,int b);

int div(int a,int b);

#endif

2) 编译生成目标文件gcc -c *.c

*.c表示所有以.c结尾的文件,也即所有的源文件。执行完该命令,会发现 second 目录中多了三个目标文件,分别是 add.o、sub.o 和 div.o。

3)把所有目标文件打包成静态库文件:

ar rcs libtest.a *.o

*.o表示所有以.o结尾的文件,也即所有的目标文件。执行完该命令,发现 test 目录中多了一个静态库文件 libtest.a。

4)创建新的目录结构:在second目录下创建math目录,在math下创建include, lib, src三个目录,并将test.h移动到include下:

在src目录下创建一个新的测试文件main.c,在 main.c 中,可以像下面这样使用 libtest.a 中的函数::

/*****main.c*****/

#include <stdio.h>

#include "test.h"  //必须引入头文件

int main(void)

{

    int m, n;

    printf("Input two numbers: ");

    scanf("%d %d", &m, &n);

    printf("%d+%d=%d\n", m, n, add(m, n));

    printf("%d-%d=%d\n", m, n, sub(m, n));

    printf("%d÷%d=%d\n", m, n, div(m, n));

    return 0;

}

在编译 main.c 的时候,我们需要使用-I(大写的字母i)选项指明头文件的包含路径,使用-L选项指明静态库的包含路径,使用-l(小写字母L)选项指明静态库的名字。所以,main.c 的完整编译命令为:

gcc src/main.c -I include/ -L lib/ -l test -o math.out

注意,使用-l选项指明静态库的名字时,既不需要lib前缀,也不需要.a后缀,只能写 test,GCC 会自动加上前缀和后缀。

打开 math 目录,发现多了一个 math.out 可执行文件,使用./math.out命令就可以运行 math.out 进行数学计算。

3. GCC 生成和使用动态链接库

1)从文件中生成动态库

从单个源文件生成动态链接库:

$ gcc -fPIC -shared xxx.c -o libxxx.so

从目标文件生成动态链接库:

$ gcc -fPIC -c xxx.c -o xxx.o

$ gcc -shared xxx.c -o libxxx.so

从多个文件生成动态库:

gcc -fPIC -shared xxx1.c xxx2.c xxx3.c -o libxxx.so

或:gcc -fPIC -shared xxx1.o xxx2.o xxx3.o -o libxxx.so

2)接上面第2个实例,首先已经编译生成的目标文件不能再用,因为不是按照动态链接方式生成的,如果需要就还需要再次重新编译目标文件:

gcc -fPIC -c add.c -o add.o

3)在此我们直接用源文件来重新编译直接生成动态链接库文件:

gcc -fPIC -shared add.c sub.c div.c -o libtest.so

4)使用动态链接库:

重新对测试程序main.c编译,这次采用动态链接库:

gcc src/main.c lib/libtest.so -o math2

生成后执行math2:

4.安装交叉编译环境

1)修改系统设置

设置->Software&Updates

点击Download from的先择框,选择other

选择China中的任意一个源,可选择阿里云。输入用户密码之后,点击close,再选择Reload,等待下载完成后即可。

  1. 安装更新sudo apt-get update
  2. 安装依赖包

sudo apt-get install git gnupg flex bison gperf build-essential zip curl libc6-dev x11proto-core-dev u-boot-tools libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-glx:i386 libgl1-mesa-dev g++-multilib mingw32 tofrodos libncurses5-dev python-markdown libxml2-utils xsltproc zlib1g-dev:i386

若因电脑配置问题出现mingw32安装错误,可参考:

Ubuntu 16.04安装MinGW32-CSDN博客

4)复制交叉编译器并解压

把交叉编译器压缩包arm-2009q3.tar.gz拷贝至 Ubuntu 主机工作目录下,执行下面的命令安装到/usr/local/arm 目录下。

# sudo mkdir -p /usr/local/arm

# sudo tar -zxvf arm-2009q3.tar.gz -C /usr/local/arm

查看编译器安装情况表明已经正常解压完成:

配置环境变量

#sudo gedit /etc/bash.bashrc

在最后加上

export PATH=$PATH:/usr/local/arm/arm-2009q3/bin

export PATH

启动环境变量设置:

#source /root/.bashrc

如没有权限,则可换用

sudo –s

source /etc/bash.bashrc

检查是否设置成功:

#echo $PATH

#arm-none-linux-gcc –v

如果打印显示arm编译器版本信息,表示安装成功。

5)应用并检验交叉编译器

arm-none-linux-gnueabi-gcc -c add.c -o addarm

然后查看新生成的addarm文件属性:file addarm,可见其为ARM格式,在PC上直接执行会报错:bash: ./addarm: cannot execute binary file: Exec format error

思考与练习题

1.

2.

三、Linux maikefile脚本编写

实验平台

l VMware workstation虚拟机内嵌口袋云系统,快速创建多种操作系统的虚拟机:(操作系统:Ubuntu18.04

CentOS,Windows10或Windows11)

l实验目的

l 学习Linux系统下编写makefile文件

l 学习企业级项目中工程编译管理

实验准备

l VMware workstation虚拟机+Ubuntu18.04系统,安装gcc编译器。

实验案例:

  1. 按照makefile显式规则编写

对上一章中math目录下已经建立好目录结构的多个文件编写显式规则的makefile,首先将second目录下的所有文件复制到third目录下,然后在math目录下编写:

math3: src/main.o src/add.o src/sub.o src/div.o

gcc src/main.o src/add.o src/sub.o src/div.o -o math3

main.o:src/main.c include/test.h

gcc -c src/main.c -o src/main.o

add.o:add.c

gcc -c src/add.c -o src/add.o

sub.o:sub.c

gcc -c src/sub.c -o src/sub.o

div.o:div.c

gcc -c src/div.c -o src/div.o

clean:

rm src/*.o math3

保存后执行:

  1. 采用隐含规则和自动化变量编写企业级项目管理makefile文件

1)首先复制math目录,并重命名为math4,在其中创建一个目录output用于保存生成文件,创建一个makefile文件,输入以下内容:

VERSION = 1.0.0     #程序版本号  

SOURCE = $(wildcard ./src/*.c)  #获取所有的.c文件  

OBJ = $(patsubst %.c, %.o, $(SOURCE))   #将.c文件转为.o文件  

INCLUDES = -I./include    #头文件路径  

LIBS = -libtest.so      #库文件名字  

LIB_PATH = -L./lib  #库文件地址  

DEBUG = -D_MACRO    #宏定义  

CFLAGS = -Wall -c   #编译标志位  

TARGET = math4  

CC = gcc  

$(TARGET): $(OBJ)     

    #mkdir -p output/   #创建一个目录,用于存放已编译的目标  

    $(CC) $(OBJ) -o output/$(TARGET)

%.o: %.c  

    $(CC) $(INCLUDES) $(DEBUG) $(CFLAGS) $< -o $@  

.PHONY: clean  

clean:  

rm -rf $(OBJ) output/

2)说明:

版本:软件开发过程中,会产生多个版本程序,通常会在程序末尾加上版本号后缀。

VERSION = 1.0.0 #定义  

$(CC) $(OBJ) $(LIB_PATH) $(LIBS) -o output/$(TARGET).$(VERSION) #使用  

头文件:由于.c文件与.h文件分开在不同目录下,所以应指定头文件路径。INCLUDES = -I ./include  

宏定义:在代码调试的过程中,我们通常会加个宏定义来控制此段代码是否被编译,比如:#ifdef _MACRO  

    printf("macro test\n");  

#endif  

具体的宏我们可不定义在代码里,可在Makefile里指定,比如:

DEBUG = -D_MACRO    #定义  

$(CC) $(INCLUDES) $(DEBUG) $(CFLAGS) $< -o $@    #使用  

编译选项:当编译选项较多时,我们通常会把它单独拿出来,比如:

CFLAGS = -Wall -c       #定义  

$(CC) $(INCLUDES) $(DEBUG) $(CFLAGS) $< -o $@    #使用  

:代码里如果要使用到库,我们可以将库名字和路径分别拿出来,比如:

LIBS = -libtest          #库文件名字  

LIB_PATH = -L./lib      #库文件地址  

$(CC) $(OBJ) $(LIB_PATH) $(LIBS) -o output/$(TARGET).$(VERSION) #使用  

output目录:如果不想把生成的程序与源文件混在一起,可将生成的程序单独放在一个output目录

思考与练习题

1. 

2.


四、Linux字符设备驱动程序开发

实验平台

l VMware workstation虚拟机内嵌口袋云系统,快速创建多种操作系统的虚拟机:(操作系统:Ubuntu18.04

CentOS,Windows10或Windows11)

l 实验目的

l 学习Linux系统下编写标准设备驱动程序

l 学习Linux系统下编写设备树类型驱动程序

实验准备

l VMware workstation虚拟机+Ubuntu18.04系统,安装gcc编译器和交叉编译器,连接开发板。

实验案例:

  1. 编写Linux 2.6内核版本以下标准设备驱动程序

1)gedit hello.c

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <linux/init.h>

#include <linux/delay.h>

#define    HELLO_MAJOR     231

#define    DEVICE_NAME     "HelloModule"

static int hello_open(struct inode *inode, struct file *file){

    printk(KERN_EMERG "hello open.\n");

    return 0;

}

static ssize_t hello_write(struct file *file, const char __user * buf, size_t count, loff_t *ppos){

    printk(KERN_EMERG "hello write.\n");

    return 0;

}

static struct file_operations hello_flops = {

    .owner  =   THIS_MODULE,

    .open   =   hello_open,     

    .write  =   hello_write,

};

static int __init hello_init(void){

    int ret;

    

    ret = register_chrdev(HELLO_MAJOR,DEVICE_NAME, &hello_flops);

    if (ret < 0) {

      printk(KERN_EMERG DEVICE_NAME " can't register major number.\n");

      return ret;

    }

    printk(KERN_EMERG DEVICE_NAME " initialized.\n");

    return 0;

}

static void __exit hello_exit(void){

    unregister_chrdev(HELLO_MAJOR, DEVICE_NAME);

    printk(KERN_EMERG DEVICE_NAME " removed.\n");

}

module_init(hello_init);

module_exit(hello_exit);

MODULE_LICENSE("GPL");

驱动文件主要包括函数hello_open、hello_write、hello_init、hello_exit,测试案例中并没有赋予驱动模块具有实际意义的功能,只是通过打印日志的方式告知控制台一些调试信息,这样我们就可以把握驱动程序的执行过程。

  在使用printk打印的时候,在参数中加上了“KERN_EMERG”可以确保待打印信息输出到控制台上。由于printk打印分8个等级,等级高的被打印到控制台上,而等级低的却输出到日志文件中。

2) 编写驱动所需的Makefile(注意M要大写,不要小写)

ifneq ($(KERNELRELEASE),)

MODULE_NAME = hellomodule

$(MODULE_NAME)-objs := hello.o

obj-m := $(MODULE_NAME).o

else

KERNEL_DIR = /lib/modules/`uname -r`/build

MODULEDIR := $(shell pwd)

.PHONY: modules

default: modules

modules:

    make -C $(KERNEL_DIR) M=$(MODULEDIR) modules

clean distclean:

    rm -f *.o *.mod.c .*.*.cmd *.ko

    rm -rf .tmp_versions

endif

说明:

uname -r用于输出当前系统内核版本

3)编写测试文件应用代码hellotest.c

#include <fcntl.h>

#include <stdio.h>

int main(void)

{

    int fd;

    int val = 1;

    fd = open("/dev/hellodev", O_RDWR);

    if(fd < 0){

        printf("can't open!\n");

    }

    write(fd, &val, 4);

    return 0;

}

首先打开设备文件,然后向设备中写入数据。如此,则会调用驱动中对应的xxx_open和xxx_write函数,通过驱动程序的打印信息可以判断是否真的如愿执行了对应的函数。

  1. 编译和测试

测试的方法整体来说就是,编译驱动和上层测试应用;加载驱动,通过上层应用调用驱动;最后,卸载驱动。

4.1)编译驱动

#make

make命令,直接调用Makefile编译hello.c,最后会生成“hellomodule.ko”。

如果出现错误:arch/x86/Makefile:168: CONFIG_X86_X32 enabled but no binutils support,

则gedit ~/.bashrc把交叉编译环境暂时注释掉,然后关闭终端再重新打开即可使用PC上自带的原生gcc编译器。

4.2)编译上层应用

#gcc hellotest.c -o hellotest

通过这条命令,就能编译出一个上层应用hellotest。

4.3)加载驱动

#insmod hellomodule.ko

insmod加载驱动的时候,会调用函数hello_init(),可在终端上打印出调试信息。

如果看不到调试信息,有两种可能性:

  1. 一般这种情况出现在发行版Linux(如Ubuntu)驱动开发中,出现这个问题的原因是:printk默认的输出设备是/dev/console,而这个设备只能在内核中访问。我们用户使用的控制台,也叫终端,对应到的是/dev/tty。可以通过tty命令来查看当前使用的是哪个终端设备:echo "hello"> /dev/tty来查看这个终端设备的打印作用。

由于发行版Linux操作系统基本都是使用grub来进行磁盘引导,而默认的grub中并没有指定console的映射终端,这就导致很多人在网络上面搜索到的B)那样的解决方法,通过修改printk控制台输出信息级别没有任何效果。

此时可在终端中输入dmesg命令打开日志查看内核打印信息。

B)则是因为内核打印是需要按照级别设置的。

使用printk时,Linux内核根据日志级别,可能把消息打印到当前控制台上,这个控制台是一个字符设备。这些消息从终端输出的前提是日志输出级别小于console_loglevel(越小级别越高)。日志级别有八个:0-7,

通过读写/proc/sys/kernel/printk文件可以读取、修改控制台的日志级别。查看这个文件:

cat /proc/sys/kernel/printk

上面显示的数字:6、7、1、6分别对应控制台日志级别、默认的消息日志级别、最低的控制台日志级别和默认的控制台日志级别。可以使用下面命令设置当前日志级别:

echo 8 > /proc/sys/kernel/printk

这样所有级别小于8的都可以打印在控制台终端上了

Insmod加载后在"/proc/devices"中可以看到已经加载的模块。

4.4)创建节点

  虽然已经加载了驱动hellomodule.ko,而且在/proc/devices文件中也看到了已经加载的模块HelloModule,但是这个模块仍然不能被使用,因为在设备目录/dev目录下还没有它对应的设备文件。所以,需要创建一个设备节点。

#mknod /dev/hellodev c 231 0

  在/proc/devices中看到HelloModule模块的主设备号为231,创建节点的时候就是将设备文件/dev/hellodev与主设备号建立连接。这样在应用程序操作文件/dev/hellodev的时候,就会定位到模块HelloModule。

/proc/devices 与 /dev的区别

  1. /proc/devices中的设备是驱动程序生成的,它可产生一个major供mknod作为参数。这个文件中的内容显示的是当前挂载在系统的模块。当加载驱动HelloModule的时候,并没有生成一个对应的设备文件来对这个设备进行抽象封装,以供上层应用访问。
  2. /dev下的设备是通过mknod加上去的,用户通过此设备名来访问驱动。我以为可以将/dev下的文件看做是硬件模块的一个抽象封装,Linux下所有的设备都以文件的形式进行封装。

4.5)上层应用调用驱动

#./hellotest

hellotest应用程序先打开文件“/dev/hellodev”,然后向此文件中写入一个变量val。期间会调用底层驱动中的hello_open和hello_write函数,hellotest的运行结果同样可通过命令dmesg查看,如下所示,看到打印hello_open和hello_write表示运行成功。

4.6) 卸载驱动

#rmmod hellomodule

insmod卸载驱动的时候,会调用函数hello_exit(),再次运行dmesg可以看到退出时的打印调试信息。

总结:

一个模块的操作流程:

  (1)通过insmod命令注册module

  (2)通过mknod命令在/dev目录下建立一个设备文件"xxx",并通过主设备号与module建立连接

(3)应用程序层通过设备文件/dev/xxx对底层module进行操作

注:上述所有驱动程序编译时调用的是x86的gcc编译器,也可以换成交叉编译器编译,并在编译成功后可下载到开发板上运行。

  1. 编写Linux 2.6内核版本以上设备树驱动程序

从 Linux 2.6 起引入了一套新的驱动管理和注册机制: Platform_device和 Platform_driver。从 Linux 2.6 起引入了一套新的驱动管理和注册机制:Platform_device 和 Platform_driver。Linux中大部分的设备驱动,都可以使用这套机制, 设备用Platform_device 表示,驱动用 Platform_driver进行注册。

platform 是一个虚拟的地址总线,相比 PCI、USB,它主要用于描述 SOC 上的片上资源。platform 所描述的资源有一个共同点,在 CPU 的总线上直接取址。Linux platform device driver 机制和传统的 device driver 机制(通过 device_register driver_register 函数进行注册)相比,一个十分明显的优势在于 platform 机制将设备本 身的资源注册进内核,由内核统一管理,在驱动程序中使用这些资源时通过 platform device 提供的 标准接口进行申请并使用。这样提高了驱动和资源管理的独立性,并且拥有较好的可移植性和安 全性(这些标准接口是安全的)。

Platform 机制的本身使用并不复杂,由两部分组成:platform_device 和 platfrom_driver。通过platform 机制开发底层驱动的大致流程为:

定义 platform_deviece -->注册 platform_device -->定义 platform_driver -->  注册

platform_driver。

其中设备树选项默认没有开启:

思考与练习题:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值