led驅動程式例項

tstars發表於2024-04-09

前言:
我們使用嵌入式裝置,使用任意一款晶片來完成、實現某些功能,本質就是操作GPIO引腳,而操作引指令碼質就是操作晶片內部的暫存器,當然了,要結合對應的電路原理圖來操作。因此,使用嵌入式晶片本質就是去操作晶片的暫存器。我們在學習的時候有自底向上和自頂向下兩種方法,並沒有好壞之分,只有合適與否。如果有一定基礎,可以考慮自頂向下,追根溯源的方法。還有些場合也應該是自頂向下的,比如看晶片手冊,我們就是了解某些引腳有什麼功能,怎麼用的,那麼就從引腳開始,看它是從晶片哪裡引出來的,找到了一個節點,再往內部看這個節點又來源哪裡,對應哪個暫存器,這樣一步步往內探究,操縱它的方法自然也就出來,具體參考韋東山led驅動程式的影片開頭部分。對於完全沒有基礎,沒有任何瞭解,那可能是自底向上更合適。總之,學會反饋調節,多數時候並不是一成不變的,而是自底向上和自頂向下相結合的。

要實現某一個功能,需要知道以下幾個方面內容:(1)硬體原理圖,裝置使用了哪些引腳;(2)檢視晶片手冊確定引腳GPIO的使用方法;(3)編寫程式。

1、硬體原理圖
我使用的板子是正點原子的imx6ull開發板,led的硬體原理圖如下,使用的引腳是GPIO1_IO03

補充知識:如何看電路原理圖上引腳編號及其對應參考手冊中的GPIO

在使用晶片時,如何根據原理圖去定位引腳:
根據引腳編號pin_num -> 去晶片資料手冊確定引腳名字pin_name -> 最後去參考手冊找到這個引腳對應的控制模組control module和這個模組對應的暫存器register
以上述LED0的引腳為例。引腳編號是J2

一般在電路原理圖中,每個引腳都會有一個編號,並且對應寫上一個名字,通常這個名字表示這個引腳的功能。有時候功能比較多,可能就會只寫一個功能,或者用/隔開表示還有其它功能。如果晶片引腳很大,單純的順序編號就不夠了,從而引入行列編號

2、檢視晶片手冊確定GPIO的使用方法
在正式進入驅動開發之前,要學會如何檢視晶片手冊確定GPIO的使用方法,所採用的方法是自頂向下、追根溯源的方法,也即根據GPIO引腳一步步往內檢視其來源並確定路徑上涉及的暫存器,下面是參考韋東山老師的驅動入門課程,結合本次實驗的閱讀imx6ull晶片手冊的過程

檢視參考手冊的GPIO章節(28章)可以看到一個圖,右邊的PAD1,PAD2就是GPIO引腳,晶片手冊的圖只列出了兩個。從右邊往左邊看,GPIO引腳透過IOMUX控制器輸出,再往左邊看,可以看到IOMUX有Block,GPIO,IOMUXC共同控制,其中Block表示GPIO的源頭可能來自其它模組,也可能來自GPIO模組,具體來自哪個模組由IOMUXC這個複用控制器來控制,同時GPIO模組又受到來自CCM時鐘模組的控制,因此我們要把某一個IO複用為GPIO,從右到左的路徑來看,涉及GPIO、IOMUXC、和CCM模組的多組暫存器

注意:在參考手冊原本的圖中,並沒有CCM這個模組,韋東山老師影片里加上了這個模組。為什麼需要這個模組,可以這樣理解,對於ARM晶片,每個控制器/模組都是需要時鐘進行同步的,這個時鐘通常來自最高頻率的分頻,不一定就是和CPU的時脈頻率一樣,但是總要有一個時鐘來進行同步,指導控制外設(外設分為soc內部外設和外部外設)何時更新狀態。可以理解為計算機就是在時鐘的指導下透過同步的方式有序執行指令進行工作的,所有的外設都需要時鐘。回顧數電裡面的內容,數電裡面從來就沒有離開過時鐘,晶片內部很大器件就是由觸發器組成,而觸發器只在時鐘的同步下更新狀態。一般情況下,外設的時脈頻率低於主頻,以GPIO輸出為例,CPU在主頻同步下執行一條指令將輸出值0/1寫入輸出暫存器,但是對應的GPIO引腳並不會馬上更新狀態,因為它的時鐘比較慢,可能再過幾個主頻時鐘週期,GPIO外設的時鐘週期才會完成一次,此時GPIO控制器才會從輸出暫存器中拿資料更新引腳的狀態。在以前學習51微控制器的時候,我們只需要一個晶振就可以,好像並不需要配置外設的時鐘,都是拿來就用,那是因為所有外設都是使用了和cpu一樣的時鐘。對於高階的晶片比如ARM,其所提供的內部外設(也就是各種介面、控制器,比如GPIO控制器,LCD控制器)資源通常是過剩的,如果所有外設的時鐘都開啟,那麼能耗會很大,因為考慮到嵌入式系統的低功耗需求,各個外設的時鐘是按需開啟,並且可以設定頻率。

找到了配置GPIO的路徑之後,我們自底向上(從左到右)再檢視晶片手冊確定如何確定具體使用哪些暫存器以及如何配置暫存器
在原理圖中,LED0接到了引腳GPIO1_IO03,
(1)先看CCM時鐘控制模組,找到晶片參考手冊的CCM章節,在第18章

然後電機system Clocks小節找到下面這個表

下拉找到GPIOn模組

從表中找到GPIOn模組中GPIO1_CLK_ENABLE對應暫存器CCGR1[CG13],因此GPIO1的時鐘門控使能對應時鐘控制模組中暫存器CCGR1的CG13位域

緊接著找到CCM Memory Map小節中的CCGR1暫存器

可以看到描述中CCRG暫存器的27-26位用於使能gpio1的時鐘,並且在這個暫存器描述的開頭可以找到暫存器的地址

(2)分析IOMUXC模組暫存器,IOMUXC暫存器用於確定GPIO複用為什麼功能,也配置GPIO的模式。檢視晶片參考手冊中GPIO那個圖,看到IOMUCX控制器涉及兩類暫存器,分別是SW_MUX_CTL_PAD_和SW_PAD_CTL_PAD_我們要複用為GPIO功能,因此去IOMUXC章節的IOMUXC Memory Map小節可以找到暫存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03和IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 ,可以檢視到如何配置GPIO1_IO03如何複用為GPIO功能以及如何配置模式。

IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03用於配置上拉/下拉,速率等模式,具體看實現需求

(3)繼續檢視晶片參考手冊中GPIO那個圖,可以看到GPIO模組涉及DR,GDIR,PSR,ICR1,ICR2,EDGE_SEL,IMR,ISR暫存器,去GPIO章節的GPIO Memory Map章節可以檢視到對應的暫存器如何使用

3、編寫程式
本章使用最傳統的方式編寫led驅動,後續會出新的部落格一步步深入,以led驅動程式為例深入學習現代驅動程式是如何編寫的
嵌入式軟體編寫是離不開硬體的,因此首先總結涉及到的暫存器,再編寫程式碼
(1)首先要使能CCM_CCGR1暫存器中的CG13位域,以使能GPIO模組的時鐘。再第2部分,已經列出了CCM_CCGR1的暫存器地址為20C_406Ch,並且預設是使能的,因此可以不用設定CCM暫存器了
(2)配置IOMUXC模組的複用暫存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03(地址為20E_0068h)中的MUX_MODE位域也即IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03[0, 3]為0101,將GPIO1_IO03複用為gpio1模式。再配置IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03(地址為20E_02F4h)暫存器配置gpio口的上下拉等模式,這裡可以先不考慮模式配置。
(3)配置GPIO模組的暫存器GPIO1_DR,GPIO1_GDIR,GPIO_PSR,GPIO_ICR1,GPIO_ICR2,GPIO_IMR,GPIO_ISR,GPIO_EDGE_SEL
查閱參考手冊得到GPIO1模組暫存器組的地址

DR暫存器用於設定GPIO的輸出值或者用於讀取GPIO作為輸入時的值,對於GPIO1_IO03,就是操作DR[3]

GDIR暫存器用於設定GPIO的方向,對於本次實驗,GPIO1_IO03用於控制LED0亮滅,因此設定為輸出,GDIR[3] = 1

PSR暫存器是read_only,當GPIO被設定為輸入時,可以讀取對應位獲取對應引腳的輸入狀態,具有鎖存功能。比DR暫存器讀取狀態準確

ICR、IMR、ISR暫存器是當引腳複用為中斷功能時用於配置中斷的,這裡暫時不涉及

總結一下,我們需要操作的暫存器也就IOMUXC模組的複用暫存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03(地址為20E_0068h),GPIO模組的暫存器GPIO1_DR,GPIO1_GDIR。下面用最傳統的方式編寫LED0驅動,而編寫這個驅動,本質就是GPIO1_IO03的引腳驅動。

根據上面的總結,結合前一篇關於hello驅動程式的程式碼編寫邏輯,一個簡易的led驅動的程式碼編寫邏輯可以如下組織:
(1)實現open, write,close函式並使用這三個函式構造operation函式
(2)在init函式進行暫存器地址對映,在exit函式撤銷對映
(3)在open函式配置GPIO1_IO03為輸出
(4)在write函式根據使用者空間傳入的引數設施GPIO1_IO03的輸出
(5)完善其它內容,比如建立裝置節點,將init函式、exit函式分別指定為入口函式、出口函式,列印核心資訊等。

led驅動程式碼如下:

點選檢視程式碼
//led_driver.c

#include "asm/memory.h"
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/kdev_t.h"
#include "linux/printk.h"
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/printk.h>
#include <linux/init.h>
#include <asm/io.h>

// (1)確定主裝置號,也可以讓核心分配
static int major = 0;
static int state;
static struct class *led_class;

/*
需要操作的暫存器也就IOMUXC模組的複用暫存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03(地址為20E_0068h),
GPIO模組的暫存器GPIO1_DR(209_C000h),
GPIO1_GDIR(209_C004h)
*/
static volatile unsigned int *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
static volatile unsigned int *GPIO1_DR;
static volatile unsigned int *GPIO1_GDIR;

#define MIN(a, b)   (a < b ? a : b)

// (3)實現對應的 drv_open/drv_read/drv_write 等函式,填入 file_operations 結構體
//為了不用宣告,將函式定義放到前面,但是邏輯順序應該是在後面
static ssize_t led_drv_write (struct file * file, const char __user * buf, size_t size, loff_t * offset)
{
    char val;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
   /*
   讀取從使用者空間傳入的資料,給DR暫存器寫0/1控制GPIO1_IO03輸出高/低電平以控制LED亮滅
   */
    copy_from_user(&val, buf, 1);
    if(val)
    {
        *GPIO1_DR &= ~(1 << 3);

        printk("led on\n");
    }
    else {
        *GPIO1_DR |= (1 << 3);
        printk("led off\n");
    }
    return 1;   //返回1,表示寫入1個資料

}


static int led_drv_open (struct inode * node, struct file * file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    /*
    開啟裝置,配置gpio為輸出
    因為載入驅動後不一定使用驅動,因此暫存器地址對映在open處進行而不是在init函式中進行更合理
    畢竟開啟裝置就意味著要使用裝置了
    */
    int val;
    val = *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
    val &= ~0xf;
    val |= 0x5;
    *IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = val;

    *GPIO1_GDIR |= (1 << 3);    //設定為輸出

    return 0;
}
static int led_drv_close (struct inode * node, struct file *file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    /*
    關閉裝置,撤銷暫存器地址對映

    */
    return 0;
}

// (2)定義自己的 file_operations
static struct file_operations led_drv = {
    .owner  = THIS_MODULE,
    .open   = led_drv_open,
    .write  = led_drv_write,
    .release    = led_drv_close,
};



// (4)把 file_operations 結構體告訴核心:register_chrdev 註冊驅動程式,因此先跳到步驟5


// (5)誰來註冊驅動程式啊?得有一個入口函式:安裝驅動程式時,就會去呼叫這個入口函式
static int __init led_init(void)
{
    int err;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    major = register_chrdev(0, "led_driver", &led_drv); //註冊驅動
    /*
    暫存器地址對映
    */
    /*
    需要操作的暫存器也就IOMUXC模組的複用暫存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03(地址為20E_0068h),
    GPIO模組的暫存器GPIO1_DR(209_C000h),
    GPIO1_GDIR(209_C004h)
    */
    IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = ioremap(0x020E0068, 4);
    GPIO1_DR = ioremap(0x0209C000, 4);
    GPIO1_GDIR = ioremap(0x0209C004, 4);


    led_class = class_create(THIS_MODULE, "led_driver");
    err = PTR_ERR(led_class);
    if (IS_ERR(led_class))
    {
        unregister_chrdev(major, "led_driver");
        return -1;
    }

    device_create(led_class, NULL, MKDEV(major, 0), NULL, "led_driver");

    return 0;

}

// (6)有入口函式就應該有出口函式:解除安裝驅動程式時,出口函式呼叫unregister_chrdev
static void __exit led_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    iounmap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);

    device_destroy(led_class, MKDEV(major, 0));
    class_destroy(led_class);


    unregister_chrdev(major, "led_driver");
    
}

// (7)其他完善:提供裝置資訊,自動建立裝置節點:class_create,device_create
module_init(led_init);    //憑什麼說上面的Init函式就是入口函式?這裡這個宏的作用就是告訴核心這是入口函式
module_exit(led_exit);
MODULE_LICENSE("GPL");

使用者空間中led應用程式如下:

點選檢視程式碼
//led_test.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>


int main(int argc, char **argv)
{
    int fd;
    char status;
    if(argc != 3)
    {
        printf("Usage: %s <dev> [on/off]\n", argv[0]);
    }
    fd = open(argv[1], O_RDWR);
    if(fd < 0)
    {
        printf("can not open\n");
        return -1;
    }
    if(strcmp(argv[2], "on") == 0)
    {
        status = 1;
        write(fd, &status, 1);
    }
    else {
        status = 0;
        write(fd, &status, 1);
    }
    return 0;

}


相關文章