日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

树莓派高级开发之树莓派博通BCM2835芯片手册导读与及“相关IO口驱动代码的编写”

發布時間:2023/12/20 编程问答 53 豆豆
生活随笔 收集整理的這篇文章主要介紹了 树莓派高级开发之树莓派博通BCM2835芯片手册导读与及“相关IO口驱动代码的编写” 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

首先我們要知道,驅動的兩大利器:電路圖(通過電路圖去尋找寄存器)和芯片手冊

一、寄存器的介紹

芯片手冊第六章的89頁,GPIO有41個寄存器,所有訪問都是32位的。Description是寄存器的功能描述。GPFSEL0(寄存器名) GPIO Function Select 0(功能選擇:輸入或輸出);GPSET0 (寄存器名) GPIO Pin Output Set 0(將IO口置0);GPSET1(寄存器名) GPIO Pin Output Set 1(將IO口置1);GPCLR0(寄存器名) GPIO Pin Output Clear 0 (清0)下圖的地址是:總線地址(并不是真正的物理地址)

GPFSEL0是pin0 ~ pin9的配置寄存器,GPFSEL1是pin10 ~ pin19的配置寄存器,以此類推,GPFSEL5就是pin50~pin53的配置寄存器。

字段名描述用法
GPFSEL0GPIO Function select 0,功能選擇輸出/輸入以引腳9舉例:000 = GPIO Pin 9 is an input,001 = GPIO Pin 9 is an output
GPSET0GPIO Pin output Set 0,輸出00 = No effect ,1 = Set GPIO pin n
GPSET1GPIO Pin output set 1,輸出10 = No effect ,1 = Set GPIO pin n
GPCLR0GPIO Pin output clear 0,清00 = No effect ,1 = Clear GPIO pin n

在上面的文檔里已經說的很清楚了,000是引腳輸入,而001則是引腳輸出,在這里要注意每個寄存器都是32位的

  • FSELn表示GPIOn,下圖給出第九個引腳的功能選擇示例,對寄存器的29-27進行配置,進而設置相應的功能。根據圖片下方的register
    0表示0~9使用的是register 0(即GPFSEL0)這個寄存器。

  • 輸出集寄存器用于設置GPIO管腳。SET{n}字段定義,分別對GPIO引腳進行設置,將“0”寫入字段沒有作用。如果GPIO管腳為在輸入(默認情況下)中使用,那么SET{n}字段中的值將被忽略。然而,如果引腳隨后被定義為輸出,那么位將被設置根據上次的設置/清除操作。分離集和明確功能取消對讀-修改-寫操作的需要。GPSETn寄存器為了使IO口設置為1,set4位設置第四個引腳,也就是寄存器的第四位。
  • 輸出清除寄存器用于清除GPIO管腳。CLR{n}字段定義要清除各自的GPIO引腳,向字段寫入“0”沒有作用。如果在輸入(默認),然后在CLR{n}字段的值是忽略了。然而,如果引腳隨后被定義為輸出,那么位將被定義為輸出根據上次的設置/清除操作進行設置。分隔集與清函數消除了讀-修改-寫操作的需要。GPCLRn是清零功能寄存器。

把pin4引腳配置為輸出引腳:
FSEL4 14-12 001 我們把4引腳的14-12配置成001 GPIO Pin 4 is an output
詳細操作:
只需要將GPFSL0這個寄存器的14~12位設置為001就可以了。只需要將0x6(對應的2進制是110)左移12位·然后取反再與上GPFSL0就可以將13、14這兩位配置為0,然后再將0x6(對應2進制110)左移12位,然后或上GPFSL0即可將12位置1。

特別提示:進行取反后再進行按位與操作是為了不影響其他引腳

配置pin4引腳為輸出引腳 bit 12-14 配置成001

31 30 ······14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 0 ······0 0 1 0 0 0 0 0 0 0 0 0 0 0 //配置pin4引腳為輸出引腳 bit 12-14 配置成001 *GPFSEL0 &= ~(0x6 <<12); // 把bit13 、bit14置為0 //0x6是110 <<12左移12位 ~取反 &按位與*GPFSEL0 |= (0x1 <<12); //把12置為1 |按位或

忘了按位與和按位或的點這里

代碼實現:

*GPFSEL0 &=~(0x6 <<12); // 把13 、14置為0*GPFSEL0 |= (0x1 <<12); //把12置為1
  • 注意:我們配置的底層引腳對應得是BCM 寄存器第0組位FESL0–9, 這個就是在寄存器GPFSEL0里,寄存器已經分好組了
    寄存器第1組位FSEL10–19,這個在寄存器GPFSEL1里

更多的引腳對應的寄存器可以去樹莓派官網進行查看
樹莓派引腳查看官網

在上圖中我們可以點擊對應的引腳編號,就可以查看到對應的引腳的相關的信息

二、寄存器的地址問題

我們在編寫驅動程序的時候,IO口空間的起始地址是0x3f00 0000,加上GPIO的偏移量0x200 0000,所以GPIO的物理地址應該是0x3f20 0000開始的,然后在這個基礎上進行Linux系統的MMU內存虛擬化管理,映射到虛擬地址上。

上圖的尾部偏移是對的,根據GPIO的物理地址0x3f20 0000可以知道:

GPFSEL0 0x3f20 0000 //IO口的初始的物理地址,而并不是手冊里面的那個總線地址 GPSET0 0x3f20 001c //地址通過查找芯片手冊里面的對應的GPSET0 的總線地址的后兩位決定是1c GPCLR0 0x3f20 0028 //地址是查找GPCLR0在芯片手冊里的總線地址確定的28,所以地址后兩位是28
  • 在原來框架的基礎上,添加寄存器的定義
volatile unsigned int* GPFSEL0 = NULL; volatile unsigned int* GPSET0 = NULL; volatile unsigned int* GPCLR0 = NULL;

完成以上代碼需要搞清楚的幾點

  • 弄清楚寄存器的分組
    GPFSEL0是pin0 ~ pin9的配置寄存器,GPFSEL1是pin10 ~ pin19的配置寄存器,以此類推,GPFSEL5就是pin50~pin53的配置寄存器。這個由查閱芯片手冊可以得知

  • volatile關鍵字的使用(筆試可能會考)

    • 在此處的作用:防止編譯器優化(可能是省略,也可能是更改)這些寄存器地址變量,常見于在內核中對IO口進行操作

    • 作用:確保指令不會因編譯器的優化而省略,且要求每次直接讀值,在這里的意思就是確保地址不會被編譯器更換

  • 如何配置寄存器的地址
    首先是在1.的基礎上,在pin4_drv_init這個函數里面添加寄存器地址的配置
  • GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4); GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4); GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);

    寫出以上的代碼,要搞清楚以下幾點
    分別找到幾個IO寄存器的物理地址(非常易錯),弄清楚GPIO的物理地址(真實地址)
    記住并不是用下面這張圖的地址來對應GPIO功能選擇寄存器0的地址,否則編譯后運行會有段錯誤。

    IO口的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的實際物理地址應該是從0x3f200000開始的,然后在這個基礎上進行Linux系統的MMU內存虛擬化管理,映射到虛擬地址上,編程都是操作虛擬地址。

    然后我們可以根據這個偏移值來確定寄存器的物理地址(真實的地址
    可以看到寄存器GPSET0相對于GPIO物理地址的偏移值為1C。即0x3f20001C

    同樣的方法,寄存器GPCLR0的偏移值為28,即0x3f200028
    寄存器GPFSEL0的偏移值為0,即0x3f200000

  • 如何讓引腳拉高或拉低電平
    代碼實現:
  • if(userCmd == 1){printk("set 1\n");*GPSET0 |= (0x1 << 4); //這里的1左移4位的目的就是促使寄存器將電平拉高,即變為HIGH}else if(userCmd == 0){printk("set 0\n");*GPCLR0 |= (0x1 << 4); //這里的1左移4位也是一樣只是為了讓寄存器將電平拉低,即變為LOW}else{printk("nothing undo\n"); }

    引腳輸出高電平:
    *GPSET0 |= (0x1 << 4);
    左移4位, 這里無論什么寄存器都是寫1,寫1并不是為某個io口去寫1,而是1是驅動(SET)設置寄存器工作將bit4的電平拉高即變為高電平,為什么要進行或操作,是因為為了不影響其他引腳的狀態

    引腳輸出低電平:
    *GPCLR0 |= (0x1 << 4);
    同樣道理,左移4位,這里的1也并不是為了某個io口去寫1,而是1是驅動(CLR)清零寄存器將電平拉低,即變為低電平,進行或操作也一樣是為了不影響其他引腳的電平狀態

  • 在Linux內核的io.h頭文件中聲明了ioremap()函數,用來將IO內存資源映射到核心虛擬地址空間(3Gb~4GB)中,當然不用了可以將其取消映射iounmap()。這兩個函數在mm/ioremap.c文件中:
  • 開始映射:void* ioremap(unsigned long phys_addr , unsigned long size , unsigned long flags) //用map映射一個設備意味著使用戶空間的一段地址關聯到設備內存上,這使得只要程序在分配的地址范圍內進行讀取或寫入,實際上就是對設備的訪問。 第一個參數是映射的起始地址 第二個參數是映射的長度 第二個參數怎么定啊? ==================== 這個由你的硬件特性決定。 比如,你只是映射一個32位寄存器,那么長度為4就足夠了。 (這里樹莓派IO口功能設置寄存器、IO口設置寄存器都是32位寄存器,所以分配四個字節就夠了)比如:GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4); 這三行是設置寄存器的地址,volatile的作用是作為指令關鍵字 確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值 ioremap函數將物理地址轉換為虛擬地址,IO口寄存器映射成普通內存單元進行訪問。解除映射:void iounmap(void* addr)//取消ioremap所映射的IO地址 比如:iounmap(GPFSEL0);iounmap(GPSET0);iounmap(GPCLR0); //卸載驅動時釋放地址映射
  • 淺談一下copy_from_user和copy_to_user 函數的用法
    copy_from_user和copy_to_user這兩個函數相信做內核開發的人都非常熟悉,分別是將用戶空間的數據拷貝到內核空間以及將內核空間中的數據拷貝到用戶空間
  • 函數copy_from_user原型:copy_from_user(void *to, const void __user *from, unsigned long n)返回值:失敗返回沒有被拷貝成功的字節數,成功返回0 參數詳解: 1. to 將數據拷貝到內核的地址,即內核空間的數據目標地址指針 2. from 需要拷貝數據的地址,即用戶空間的數據源地址指針 3. n 拷貝數據的長度(字節) 也就是將@from地址中的數據拷貝到@to地址中去,拷貝長度是n

    詳細了解copy_from_user和copy_to_user

    三、驅動代碼與應用測試代碼

    3.1 相關代碼
    底層驅動代碼:

    #include <linux/fs.h> //file_operations聲明 #include <linux/module.h> //module_init module_exit聲明 #include <linux/init.h> //__init __exit 宏定義聲明 #include <linux/device.h> //class devise聲明 #include <linux/uaccess.h> //copy_from_user 的頭文件 #include <linux/types.h> //設備號 dev_t 類型聲明 #include <asm/io.h> //ioremap iounmap的頭文件static struct class *pin4_class; static struct device *pin4_class_dev;static dev_t devno; //設備號 static int major =231; //主設備號 static int minor =0; //次設備號 static char *module_name="pin4"; //模塊名--這個模塊名到時候是在樹莓派的/dev底下顯示相關驅動模塊的名字volatile unsigned int* GPFSEL0 = NULL; volatile unsigned int* GPSET0 = NULL; volatile unsigned int* GPCLR0 = NULL;//volatile關鍵字的作用:確保指令不會因編譯器的優化而省略,且要求每次直接讀值,在這里的意思就是確保地址不會被編譯器更換//led_open函數 static int pin4_open(struct inode *inode,struct file *file) {printk("pin4_open\n"); //內核的打印函數和printf類似 //由于pin4在 14-12位,所以將14-12位分別置為001即為輸出引腳,所以下面的那兩個步驟分別就是將14,13置為0,12置為1*GPFSEL0 &= ~(0x6 << 12); //把13,14位 置為0*GPFSEL0 |= (0x1 << 12); //把12位 置為1 return 0; }//led_write函數 static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) {int userCmd;int copy_cmd;printk("pin4_write\\n");//copy_from_user(void *to, const void __user *from, unsigned long n)copy_cmd = copy_from_user(&userCmd,buf,count); //函數的返回值是,如果成功的話返回0,失敗的話就是返回用戶空間的字節數if(copy_cmd != 0){printk("fail to copy from user\n");}if(userCmd == 1){printk("set 1\n");*GPSET0 |= (0x1 << 4); //這里的1左移4位的目的就是促使寄存器將電平拉高,即變為HIGH}else if(userCmd == 0){printk("set 0\n");*GPCLR0 |= (0x1 << 4); //這里的1左移4位也是一樣只是為了讓寄存器將電平拉低,即變為LOW}else{printk("nothing undo\n"); }return 0; }static ssize_t pin4_read(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {printk("pin4_read\n");return 0; }static struct file_operations pin4_fops = {.owner = THIS_MODULE,.open = pin4_open,.write = pin4_write,.read = pin4_read, };int __init pin4_drv_init(void) //設備驅動初始化函數(真實的驅動入口) {int ret;devno = MKDEV(major,minor); //創建設備號ret = register_chrdev(major, module_name,&pin4_fops); //注冊驅動 告訴內核,把這個驅動加入到內核驅動的鏈表中pin4_class=class_create(THIS_MODULE,"myfirstdemo"); //這個是讓代碼在/dev目錄底下自動生成設備,自己手動生成也是可以的pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //創建設備文件//由于以下的地址全是物理地址,所以我們要將物理地址轉換成虛擬地址 GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4); //由于寄存器是32位的,所以是映射4個字節,一個字節為8位GPSET0 = (volatile unsigned int *)ioremap(0x3f20001c,4);GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);return 0; }void __exit pin4_drv_exit(void) //卸載驅動,即將驅動從驅動鏈表中刪除掉 {iounmap(GPFSEL0);iounmap(GPSET0);iounmap(GPCLR0);device_destroy(pin4_class,devno);class_destroy(pin4_class);unregister_chrdev(major, module_name); //卸載驅動 }module_init(pin4_drv_init); //真正的入口 module_exit(pin4_drv_exit); //卸載驅動 MODULE_LICENSE("GPL v2");

    上層應用測試代碼:

    #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>int main() {int fd;int userCmd;fd = open("/dev/pin4",O_RDWR);if(fd < 0){printf("fail to open the pin4\n");perror("the reason:");}else{printf("success to open the pin4\n");}printf("please Input 1-HIGH,0-LOW \n");scanf("%d",&userCmd);write(fd,&userCmd,4); //這里userCmd是一個整型數,所以寫的是4個字節return 0; }

    至于怎么在虛擬機中編譯驅動文件以及如何將編譯好的文件發送至樹莓派,敬請關注以下博文
    如何將編譯好的文件發送至樹莓派底下

    3.2 在樹莓派底下進行代碼的測試與驗證
    相關的驅動的裝載與卸載也查看驅動裝載與卸載

    • 先來查看一下樹莓派4號引腳的初始狀態是什么

      輸入1,將引腳電平變為高電平

      輸入0,將引腳電平變為低電平

      到目前為止,我們經過那么多節的對驅動的學習的博文,現在終于自己終于實現了類似于wiringPi這樣的一個驅動文件,我們在這里做的是引腳4的驅動,那么我們就可以按著模樣來寫引腳5,引腳6,甚至其他引腳的驅動,在這里想說一句,驅動代碼的編寫,都是基于linux內核源碼來進行編寫的,linux內核源碼這個文件在前面的博文有,如有需要自行去下載。

    學習筆記,僅供參考

    樹莓派高級開發之IO口驅動代碼編寫 優秀博文參考一
    樹莓派高級開發之IO口驅動代碼編寫 優秀博文參考二

    總結

    以上是生活随笔為你收集整理的树莓派高级开发之树莓派博通BCM2835芯片手册导读与及“相关IO口驱动代码的编写”的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。