基于ARM的模块方式驱动程序

在这里插入图片描述

基于ARM的模块方式驱动程序

作者:毛茏玮 / Saint
掘金:https://juejin.im/user/5aa1f89b6fb9a028bb18966a
微博:https://weibo.com/5458277467/profile?topnav=1&wvr=6&is_all=1
GitHub:github.com/saint-000
CSDN: https://me.csdn.net/qq_40531974

一、实验目的
1.掌握Linux系统下设备驱动程序的作用与编写技巧
2.掌握Linux驱动程序模块加载和卸载的方法
3.了解串口驱动的原理和工作方式
了解串口驱动的原理和工作方式

二、实验内容
1.基于ees331开发板编写led驱动程序。
2.编写led驱动,修改makefile,并将led驱动动态加载入linux内核中。
3.编写关于led的测试程序,交叉编译后运行,控制led灯的亮灭。
4.修改uart驱动,修改makefile,修改设备树,并将uart驱动动态加载入linux内核中。
5.编写关于uart的测试程序,交叉编译后运行,进行串口通信。

三、实验步骤
Led驱动

1.在vivado里搭建好硬件工程,包括:led、sw和uart模块。(详见实验2)
2.搭建好后生成比特流文件,利用SDK软件将fsbl文件、比特流文件、u-boot文件生成boot.bin启动文件。(详见实验2)
3.在driver_code/led目录下找到ees331_led.c源文件,双击打开源文件,也可以通过终端命令gedit 打开:编写led驱动源代码,然后添加控制函数的代码
4.打开Makefile 文件,修改其中部分,加入交叉编译器和内核源码树目录,在需要添加部分加入交叉编译器和内核源码树目录$(CROSS_COMPILE)gcc、/home/zynq/Downloads/2016.3/linux-xlnx-xilinx-v2016.3
5.虚拟机中打开一个终端,在led目录下执行make命令,生成.ko驱动模块文件。make
6.将测试文件ledtest.c进行交叉编译,(或者在开发板上执行gcc命令),生成可执行文件ledtest。arm-linux-gnueabihf-gcc ledtest.c -o ledtest
7.在win7系统里打开putty软件,选择对应的com口,波特率选择为115200。
8.putty的窗口里,进入到arm-linux系统里,将ees331_led.ko和 ledtest文件拷贝到SD卡根文件系统的root目录下。

scp [email protected]:/home/zynq/driver_code/led/ledtest    /root
scp [email protected]:/home/zynq/driver_code/led/ees331_led.ko /root

9.putty的窗口里,在/root(~)目录下执行insmod 命令,将驱动模块动态加载到内核中。
insmod ees331_led.ko
驱动加载成功后,在/dev/目录中用ls命令可以查看到相应的设备。
ls /dev/
10.putty的窗口里,返回到/root目录,在/root(~)目录下运行测试程序ledtest,putty的ARM-linux 操作系统的命令行输入:
./ledtest
11.putty的窗口里,程序会提示:please enter the led status,输入与希望显示的led 状态对应的ledstatus 值(输入十进制值即可),观察led 的显示情况。
12.putty的窗口里,卸载led驱动模块:
rmmod ees331_led

UART驱动
1.在vivado里搭建好硬件工程,包括:led、sw和uart模块。
2.搭建好后生成比特流文件,利用SDK软件将fsbl文件、比特流文件、u-boot文件生成boot.bin启动文件。
3.在driver_code/BOOT相应目录下找到devicetree.dts源文件,打开源文件(图中的目录是devicetree目录,实际是BOOT),修改设备树文件
4.在虚拟机终端中,将设备树文件dts转换成dtb格式,并通过读卡器将devicetree.dtb拷贝到SD里的BOOT分区里:dtc -I dts -O dtb -o devicetree.dtb devicetree.dts
5.在driver_code/uartlite目录下修改测试程序uart.c。
6.对提供的测试程序uart.c 进行交叉编译,生成可执行文件uart,在终端下输入以下命令:

cd /home/zynq/driver_code/uartlite
sudo su        (密码:1) 
source zynq-env.sh  
arm-linux-gnueabihf-gcc uart.c -o uart

7.对驱动文件进行编译,生成驱动模块uartlite.ko,在同一个终端下继续输入命令:
make
8.在win7系统里打开putty软件,选择对应的com口,波特率选择为115200。
9.putty的窗口里,将uartlite.ko和uart文件拷贝到SD卡的root目录下。

scp [email protected]:~/driver_code/uartlite/uart   /root
scp [email protected]:~/driver_code/uartlite/uartlite.ko  /root

10.putty的窗口里,进入到arm-linux系统里,在/root(~)目录下执行insmod 命令,将驱动模块动态加载到内核中。
insmod uartlite.ko
驱动加载成功后,在/dev/目录中用ls命令可以查看到相应的设备。
ls /dev/
11.用双头usb线将开发板上PL的串口(5上)与上位机连接,打开串口助手,选择对应的com口与波特率9600。
12…putty的窗口里,在/root(~)目录下运行测试程序uart,putty的ARM-linux 操作系统的命令行输入:
./uartlite

四、实验结果
1.基于ARM的模块方式驱动程序实验.在Linux虚拟机下编写实验程序。
(1)设置IP

1.sudo  du   (取得用户权限)
2.ifconfig    (查看ip)
3.ifconfig  ens33 192.168.1.12    (配置ip)

在这里插入图片描述
(2)在driver_code/led目录下找到ees331_led.c源文件,编写代码如下:

#include <linux/init.h> //包含对初始化函数和清除函数的定义
#include <linux/platform_device.h>
#include <linux/module.h>//包含可装载模块需要的大量符号和函数的定义
#include <linux/miscdevice.h>
#include <linux/ioport.h>
#include <linux/of.h>
#include <linux/fs.h>
#include <asm/io.h>
#define DEVICE_NAME         "ees331_led_dev"
#define ees331_led_PHY_ADDR 0x41200000   
#define ees331_led_REG_WIDTH   32
static void __iomem *GPIO_Regs;
static int ees331_led_open(struct inode * inode , struct file * filp)//初始化LED亮这个函数,加载时被调用
{
  printk("ees331_led_dev open\n");
  iowrite32(16, GPIO_Regs);//led端口操作函数
  return 0;
}

static int ees331_led_release(struct inode * inode, struct file *filp)
{
  printk("ees331_led_dev close\n");
  iowrite32(0, GPIO_Regs);
  return 0;
}

static int ees331_led_read(struct file *filp, char *buffer, size_t length, loff_t * offset)
{
  
  if (filp->f_flags & O_NONBLOCK)
    return -EAGAIN;

  *buffer=(char)ioread8(GPIO_Regs);
	
  return 0;
}
static int ees331_led_ioctl(struct file *filp, unsigned int reg_num, unsigned long arg)
{
/*------------Add your code here-----------------*/
//通过led地址给led寄存器赋值
iowrite32(arg, GPIO_Regs);
}
/*------------Add your code here-----------------*/  
  return 0;
}
static const struct file_operations ees331_led_fops =
{

  .owner = THIS_MODULE,

/*------------Add your code here-----------------*/

  .open =ees331_led_open ,// 打开设备函数
  .release =ees331_led_release ,//关闭设备函数
  .read =ees331_led_read,     //读函数
  .unlocked_ioctl =  ees331_led_ioctl ,    // IO控制函数
    
/*------------Add your code here-----------------*/
};
static struct miscdevice ees331_led_dev =
{
  .minor = MISC_DYNAMIC_MINOR,
  .name = DEVICE_NAME,
  .fops = &ees331_led_fops,
};

int __init ees331_led_init(void)
{
  int ret;
  //Map device to the virtual address of kernel
  GPIO_Regs = ioremap(ees331_led_PHY_ADDR, 32); /* Verify it's non-null! */

  if(GPIO_Regs == NULL)
  {
    printk("ees331_led:[ERROR] Access address is NULL!\n");
    return -EIO;
  }  
 ret = misc_register(&ees331_led_dev);
  if (ret)
  {
    printk("ees331_led:[ERROR] Misc device register failed\n");
    return ret;
  }
  
 printk("ees331_led: success! Module init complete\n");
 iowrite32(255, GPIO_Regs);
 return 0; /* Success */
}

void __exit ees331_led_exit(void)
{
  iounmap(GPIO_Regs);
  misc_deregister(&ees331_led_dev);
  
  printk("ees331_led: Module exit\n");
}
module_init(ees331_led_init);
module_exit(ees331_led_exit);
MODULE_AUTHOR("B243");
MODULE_ALIAS("ees331_led");
MODULE_DESCRIPTION("zedboard ees331_led module");
MODULE_LICENSE("GPL");

实验截图:
在这里插入图片描述

(3)在drivr_code/led/目录下打开Makefile文件,代码如下:

ifneq ($(KERNELRELEASE),)
obj-m	:= ees331_led.o
else
ifeq ($(TARGET),)
TARGET := $(shell uname -r)
endif
PWD := $(shell pwd)
CC = $(CROSS_COMPILE)gcc
KDIR ?= /home/zynq/Downloads/2016.3/linux-xlnx-xilinx-v2016.3 
 default:
	@echo $(TARGET) > module.target
	$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

clean:
	@rm -f *.ko *.o modules.order Module.symvers *.mod.? .ees331_led*.* *~
	@rm -rf .tmp_versions module.target

install: ees331_led.ko
	install --mode 0644 ees331_led.ko /lib/modules/$(shell cat module.target)/kernel/drivers/char/
	/sbin/depmod -a $(shell cat module.target)
ees331_led.ko:
	$(MAKE)
endif

(4)对提供的测试程序ledtest.c 进行交叉编译,生成可执行文件ledtest,在终端下输入以下命令:
cd /home/zynq/driver_code/led
进入到led目录下

sudo su        (密码:1) 
source zynq-env.sh  
arm-linux-gnueabihf-gcc ledtest.c -o ledtest

对驱动文件进行编译,生成驱动模块ees331_led.ko,在同一个终端下继续输入命令:make
在这里插入图片描述
在这里插入图片描述

(5)putty的窗口里,进入到arm-linux系统里,将ees331_led.ko和 ledtest文件拷贝到SD卡根文件系统的root目录下。在/root(~)目录下执行insmod 命令,将驱动模块动态加载到内核中。实验截图如下:
在这里插入图片描述

驱动加载成功后,在/dev/目录中用ls命令可以查看到相应的设备:
在这里插入图片描述

./ledtest运行LED程序文件,实验截图如下:
在这里插入图片描述
键入1对应硬件上LD0亮灯
在这里插入图片描述
键入2,硬件上LD1亮灯对应10
在这里插入图片描述
键入5,硬件上LD0和LD1亮灯对应101

五、实验总结
1.知道编写led驱动程序以及在linux系统下编写led驱动,修改makefile,并将led驱动动态加载入linux内核中。
2.编写关于led的测试程序,知道可以交叉编译运行,控制led灯的亮灭。
3.理解了设备驱动的功能就是将系统提供的调用映射到作用于实际硬件的和设备相关的操作上。

实验心得:
(1)这次的实验是基于在linux操作系统下的驱动程序编写,事实上LED的程序比较好理解,令人比较烧脑的是linux系统是指令操作,习惯了Windows的图像化界面,用命令去执行一些操作还是有点不太适应。
(2)在LED这个实验中,我们需要先对板子和系统下的IP对应以便后面的文件拷贝,在后面的实验中,我发现如下几点要注意的:
在这里插入图片描述
(3)一定要主要这里,语句的作用是将ees331_led.ko和 ledtest文件拷贝到SD卡根文件系统的root目录下;然后,我们就要多关注一下这个SD卡的根目录了,因为我第一次做的时候发现编码什么的都没问题,保存之类的操作都没问题但是就是打不开ledtest,后来编译make然后更新了4次都没有变化,我就怀疑实验板的SD卡坏了,然后我重新换了一个开发板,发现突然好了,巨神奇!然后我又把所有代码又导到我之前用的那个开发板上,又发现之前那个开发板也没问题,这就奇怪了,最后我发现是makefile里面出了问题,原来是我一不小心打错了一个字母,后来发现终端要不是说打不开.KO驱动文件,要不就是我make生成的文件变少了很多。
在这里插入图片描述
CC = $(CROSS_COMPILE)gcc定义要使用的编译器,非交叉编译的场合,CROSS_COMPILE为空,所以使用的就是gcc,交叉编译时(如在x86 PC上编译在ARM上运行的软件),CROSS_COMPILE会定义为类似于arm_linux_gnu_的值,这时会使用交叉编译器(如arm_linux_gnu_gcc)。我们实验用的是 arm-linux-gnueabihf-gcc 交叉编译器。在编译执行时,将 led 驱动作为模块编译,编译出可装载的目标文件.ko 文件。MakefileMakefile 文件描述了整个工程的编译、连接等规则,因此我们一定要引用正确,不然代价很惨重。

实验思考题
1.驱动里led的地址是怎么查看的?
在硬件工程中查看led的地址。
在这里插入图片描述

  1. 模块化的最大优点是什么?
    可以在系统正在运行着的时候给内核增加模块提供的功能(也可以移除功能)。
    除此之外还有其他优点如下:
  2. 修改内核功能,不必重新全部编译整改内核,只需要编译相应模块即可。
    2.模块目标代码一旦被加载重定位到内核,其作用域和静态链接的代码完全等价。
    模块代码有两种运行的方式:编译成可动态加载的module,并通过insmod来动态加载,接着进行初始化。静态编译链接进内核,在系统启动过程中进行初始化。

3.printk()函数的作用是什么,怎么查看printk函数打印的消息?
Linux内核用函数printk打印调试信息,这个函数的用法与C库打印函数printf格式类似,但在内核使用。我们可在内核代码中的某位置加入函数printk,直接把要显示的信息打打印到屏幕上或日志文件中。
内核通过 printk() 输出的信息具有日志级别,日志级别是通过在 printk() 输出的字符串前加一个带尖括号的整数来控制的,如 printk("<6>Hello, world! ");。
内核中共提供了八种不同的日志级别。
查看printk函数打印的消息一般是在messages和虚拟终端下进行查看,可以在shell中使用dmesg指令。

2.基于ARM的串口驱动实验
(1)在driver_code/BOOT相应目录下找到devicetree.dts源文件,打开源文件,编写源代码如下:
在这里插入图片描述
(2)将设备树文件dts转换成dtb格式,并通过读卡器将devicetree.dtb拷贝到SD里的BOOT分区里,在driver_code/uartlite目录下编写测试程序uart.c

#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <termios.h>  
#include <unistd.h>  
  
int set_opt(int,int,int,char,int);  
int main(void)  
{  
    int fd,ret;  
/*  Add your code here  */
	
    char *uart = "/dev/ttyUL1";  
    char buffer_out[] = "2016031002009";  

	/*  Add your code here  */

char buffer_in[512];       
memset(buffer_in,0,512);  
fd = open(uart,O_RDWR | O_NOCTTY);  
if(fd == -1)  
{  
    printf("%s open failed\n",uart); 
return -1; 
}   

printf("%s open success\n",uart);  
ret = set_opt(fd,9600,8,'N',1);  
if(ret == -1)  
{  
    exit(-1);  
}  
ret = write(fd,buffer_out,strlen(buffer_out));
printf("your number sent  %d\n",ret); 
while(1)  
{  
/*  Add your code here  */
//1.主程序实现接收字符,并把接收到的字符发送出去
ret=read(fd,buffer_in,512);
if(ret>0)
{
	 printf("your number sent  %d\n",ret);//接收字符
	 ret = write(fd,buffer_in,strlen(buffer_in));//将读到的发送出去
}

/*  Add your code here  */
}  
close(fd);  
}  
  
int set_opt(int fd,int nSpeed,int nBits,char nEvent,int nStop)  
{  
    struct termios newtio,oldtio;  
    if(tcgetattr(fd,&oldtio)!=0)  
    {  
        perror("error:SetupSerial 3\n");  
        return -1;  
    }  
    bzero(&newtio,sizeof(newtio));  
    newtio.c_cflag |= CLOCAL | CREAD;  
    newtio.c_cflag &= ~CSIZE;  
  
newtio.c_lflag &=~ICANON;
                           
switch(nBits)  
{  
    case 7:  
        newtio.c_cflag |= CS7;  
        break;  
    case 8:  
        newtio.c_cflag |=CS8;  
        break;  
}  

switch(nEvent)  
  
{  
    case 'O':  
        newtio.c_cflag |= PARENB;  
        newtio.c_cflag |= PARODD;  
        newtio.c_iflag |= (INPCK | ISTRIP);  
        break;  
    case 'E':  
        newtio.c_iflag |= (INPCK | ISTRIP);  
        newtio.c_cflag |= PARENB;  
        newtio.c_cflag &= ~PARODD;  
        break;  
    case 'N':  
        newtio.c_cflag &=~PARENB;  
        break;  
}  
switch(nSpeed)  
{  
    case 2400:  
        cfsetispeed(&newtio,B2400);  
        cfsetospeed(&newtio,B2400);  
        break;  
    case 4800:  
        cfsetispeed(&newtio,B4800);  
        cfsetospeed(&newtio,B4800);  
        break;  
    case 9600:  
        cfsetispeed(&newtio,B9600);  
        cfsetospeed(&newtio,B9600);  
        break;  
    case 115200:  
        cfsetispeed(&newtio,B115200);  
        cfsetospeed(&newtio,B115200);  
        break;  
    case 460800:  
        cfsetispeed(&newtio,B460800);  
        cfsetospeed(&newtio,B460800);  
        break;  
    default:  
        cfsetispeed(&newtio,B9600);  
        cfsetospeed(&newtio,B9600);  
        break;  
}  
//ÉèÖÃֹͣλ  
if(nStop == 1)  
    newtio.c_cflag &= ~CSTOPB;  
else if(nStop == 2)  
    newtio.c_cflag |= CSTOPB;  
newtio.c_cc[VTIME] = 5;  
newtio.c_cc[VMIN] = 0;  
tcflush(fd,TCIFLUSH);  
  
if(tcsetattr(fd,TCSANOW,&newtio)!=0)  
{  
    perror("com set error\n");  
    return -1;  
}  
return 0;  
}  

实验截图:
在这里插入图片描述
(3)对提供的测试程序uart.c 进行交叉编译,生成可执行文件uart,在终端下输入以下命令:

cd /home/zynq/driver_code/uartlite
sudo su        (密码:1) 
source zynq-env.sh  
arm-linux-gnueabihf-gcc uart.c -o uart

(4)对驱动文件进行编译,生成驱动模块uartlite.ko,在同一个终端下继续输入命令:make在win7系统里打开putty软件,选择对应的com口,波特率选择为115200;将uartlite.ko和uart文件拷贝到SD卡的root目录下。

scp [email protected]:~/driver_code/uartlite/uart   /root
scp [email protected]:~/driver_code/uartlite/uartlite.ko  /root
insmod uartlite.ko

实验截图如下:
在这里插入图片描述
(5)用双头usb线将开发板上PL的串口(5上)与上位机连接,打开串口助手,选择对应的com口与波特率9600。./uart执行程序截图如下:
在这里插入图片描述

我们在串口点击发送,发现显示区出现2016031002009我的学号号码,然后在数值输入窗口输入1,putty的窗口里显示反馈1,实验截图如下:
在这里插入图片描述

我们在输入区输入1,点击发送发现串口调试助手显示区显示2,实验截图如下:
在这里插入图片描述

五.实验总结
1.知道修改uart驱动,了解设备树跟驱动程序之间的关系,并将uart驱动动态加载入linux内核中。
2.理解uart的测试程序,交叉编译后运行,进行串口通信。
3.知道串口收发通信的两个接口函数的使用

ret = read(fd,buffer_in,size);
ret = read(fd,buffer_in,size);

实验心得:
(1)在上一个LED的实验中,基本了解了putty与linux终端直接的make,更新以及文件拷贝等操作,主要遇到的问题出现在程序编写上:
在这里插入图片描述

我们要注意我们程序中定义的设备名称对不对:
在这里插入图片描述
如果设备名出错的话,终端会一直提示你不能打开串口文件

(2)应该关注的是WHILE循环里面的语句,事实上我们在理解收发数据的时候两个过程其实本质是一样的,只是两个的执行者不同,因此我们只需对原函数修改一下收发数据的执行者。
在这里插入图片描述
六.实验思考题

  1. 文件操作结构体struct file_operations的作用是什么?
    结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址。
    每一个设备文件都代表着内核中的一个file结构体。该结构体在头文件linux/fs.h定义。file结构体是内核空间的结构体,这意味着它不会在用户程序的代码中出现。它绝对不是在glibc中定义的FILE。FILE自己也从不在内核空间的函数中出现。

2.设备树的作用是什么,与驱动程序有什么联系?
Device Tree是一种描述硬件的数据结构,设备树源(Device Tree Source)文件(以.dts结尾)就是用来描述目标板硬件信息的。Device Tree由一系列被命名的结 点(node)和属性(property)组成,而结点本身可包含子结点。属性是成对出现的name和value。
在这里插入图片描述
设备树定义是保留着存在于系统中的设备信息,当机器引导时,OS(操作系统)通过使用驱动程序和其他组件获得的信息建立此树,并且当添加或删除设备时更新此树。

猜你喜欢

转载自blog.csdn.net/qq_40531974/article/details/84452834