1. 程式人生 > >基於ARM的模組方式驅動程式

基於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(作業系統)通過使用驅動程式和其他元件獲得的資訊建立此樹,並且當新增或刪除裝置時更新此樹。