基於匯流排裝置驅動模型的按鍵讀取驅動程式

tstars發表於2024-05-08

本次實驗基於匯流排裝置驅動模型實現按鍵驅動程式的編寫,給上層應用程式提供檢測按鍵是否按下的操作介面,上層應用根據按鍵是否按下控制led的亮滅。所以上層應用程式會同時使用led和按鍵的驅動介面,但是對於下層驅動而言,這二者是分離的,因此只需要專注於編寫按鍵驅動程式就可以了。
在正點原子imx6ull開發板上按鍵原理圖如下:

按鍵KEY0接到的引腳名字是uart1_cts,查詢晶片參考手冊第28章(GPIO章節),找到下面這個表

從表中找到GPIO1_IO18這一項,寫著UART1_CTS_B,跟原理圖上引腳名一致,說明這是按鍵Key0對應的引腳。我們要讀取按鍵是否按下,因此我們需要把UART1_CTS_B這個引腳也即GPIO1_IO18設定為GPIO功能(GPIO功能代指普通的輸入輸出功能)。與led驅動程式的步驟類似,我們需要檢視GPIO章節中有關引腳的原理圖,其涉及到IOMUXC控制器、CMM時鐘控制器,以及相關的GPIO控制器。
(1)CCM時鐘控制器

在CCM控制器章節找到CCGR1暫存器的CG13位域控制GPIO1模組的時鐘使能,接著在CCM章節的暫存器對映小節找到暫存器地址(20C_406Ch)及各位域的配置方式

(2)IOMUC複用控制器
在IOMUXC控制器章節找到複用暫存器IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B(20E_008Ch)

(2)GPIO控制器
需要將GPIO設定為輸入功能,因此涉及GDIR和PSR暫存器,其中GDIR將GPIO配置為輸入,PSR暫存器用於讀取引腳輸入狀態

在GPIO章節找到上表可以檢視到GPIO1相關的暫存器

這裡插入話題GPIO是什麼,為什麼叫GPIO:
GPIO全稱是General Purpose Input / Output 通用目的輸入輸出。既然是通用目的(General Purpose),那說明它可以用於不同的功能,比如有些引腳輸出能力強,可以作為輸出,這時候它就被用作最一般的輸出功能,但是並不意味著它只能做普通輸入輸出引腳,它也可以複用為其它功能(具體檢視晶片手冊)。又比如本次實驗中按鍵接到的引腳名字叫UART1_CTS,說明它的主要功能是用於UART串列埠,但是不代表不能用於輸入,本實驗就用它作為按鍵輸入。晶片就是透過輸入/輸出資料來提供功能的,所有引腳都是輸入/輸出資料,也因此才叫IO,但是同一個引腳可能可以被複用為多種功能,比如UART,PWM,SPI,因此它是通用的,也即General Purpose,所以合起來才叫GPIO。更細緻地分類解釋的話,所有的引腳都具有輸入/輸出功能,它們都屬於GPIO,但是如果某一個引腳只是單純的用於輸入/輸出,比如LED的引腳GPIO1_IO03,這個引腳具有較強的輸出能力,一般就用於輸出,並且這個引腳屬於GPIO1組的第3個IO引腳,那麼這個引腳就命名為GPIO1_IO03;再比如按鍵Key0的引腳一般情況下輸出能力較弱,會作為UART串列埠功能,那麼這個引腳就命名UART1_CTS,但是它實際上是GPIO1組的第18個IO引腳,因此它是GPIO1_IO18,但是命名為UART1_CTS。

分析完硬體和暫存器部分後,開始寫驅動程式碼,分為三部分:裝置樹、下層驅動程式碼,上層驅動框架

1、裝置樹
在根節點下新增如下節點

點選檢視程式碼
mykey0 {
			compatible = "key0_driver";
			pin = <GROUP_PIN(1, 18)>;
			pin_reg = <
					0x20C406C	/*CCM_CCGR1*/
					0x20E008C	/*IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B*/
					0x209C004	/*GPIO1_GDIR*/
					0x209C008	/*GPIO1_PSR*/	
			>;
		};
在上面的裝置樹節點中,compatible用於匹配驅動,其餘的pin和pin_reg是自定義屬性,儲存引腳的相關暫存器資訊,可以在驅動程式碼裡面的probe函式里讀取裝置樹中的屬性資訊來獲取暫存器地址。

2、包含platform_driver結構體的程式碼
這部分就是下層涉及具體硬體的操作函式,除了填充platform_driver結構體的內容外,還要向上層提供具體裝置的操作函式,比如硬體的初始化、控制,同時在probe函式中呼叫上層介面為每一個匹配的裝置註冊檔案檢視。具體的程式碼如下:

標頭檔案button_rsc.h

點選檢視程式碼
#ifndef __BUTTON__RSC__H
#define __BUTTON__RSC__H

#define GROUP_PIN(g, p) ((g << 16) | (p))   //轉換成引腳編號
#define PIN(x)  (x & 0xffff)    //獲取引腳屬於哪一個
#define GROUP(x)    ((x >> 16) & 0xffff)    //獲取引腳屬於哪一組

struct button_resource{
    int group_pin;  //[31:16]存放組,[15:0]存放
    //定義變數存放涉及到的暫存器的地址
    unsigned int CCM_CCGR1;
    unsigned int IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B;
    unsigned int GDIR;
    unsigned int DR; 
    unsigned int PSR;
};

struct button_registers_after_map{
    unsigned int* CCM_CCGR1;
    unsigned int* IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B;
    unsigned int* GDIR;
    unsigned int* DR; 
    unsigned int* PSR;
};

#endif

標頭檔案button_opr.h

點選檢視程式碼
#ifndef __BUTTON_OPR__H
#define __BUTTON_OPR__H

struct button_operations{
    int (*init) (unsigned int which);    //初始化引腳,which表示引腳編號
    int (*read) (unsigned int which);   //讀取引腳電平狀態,which表示引腳編號
};

#endif

標頭檔案button_drv.h

點選檢視程式碼
#ifndef __BUTTON_DRV_H__
#define __BUTTON_DRV_H__

#include "button_opr.h" //如果在board_button.c中button_drv標頭檔案的包含在button_opr前面的話,這裡就需要先包含button_opr.h,否則會有警告,因為struct button_operations定義在button_opr.h
void button_device_register(int minor);
void button_device_unregister(int minor);
void register_button_operations(struct button_operations *button_opr);

#endif

board_button.c檔案

點選檢視程式碼
/*
下層按鍵驅動程式,實現具體的硬體操作
(1)實現按鍵GPIO引腳的操作函式,包括初始化函式、讀取按鍵狀態函式,透過結構體指標形式向上層註冊
(2)實現platform_driver結構體,從裝置樹生成的platform_device獲取暫存器資源,並提供匹配和解除安裝時的probe和remove函式,還有其他屬性
(3)實現驅動入口/出口函式
*/

#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 "asm/delay.h"
#include "linux/ioport.h"
#include "linux/mod_devicetable.h"
#include "linux/of.h"
#include "linux/platform_device.h"
#include "asm/io.h"
#include <linux/delay.h>

#include "button_rsc.h"
#include "button_drv.h"
#include "button_opr.h"

int cur_counts = 0;
struct button_resource g_buttons[10];
struct button_registers_after_map g_buttons_map_addr[10];


/*
構造操作函式結構體,為上層驅動框架提供實體操作函式
*/
int button_init(unsigned int which)    //初始化引腳,which表示引腳編號
{
    /*根據which初始化GPIO,其中which是次裝置號,也是g_buttons中儲存資源結構體的下標*/
    unsigned int val;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    //地址對映
    if(!g_buttons_map_addr[which].CCM_CCGR1)
    {
        g_buttons_map_addr[which].CCM_CCGR1 = ioremap(g_buttons[which].CCM_CCGR1, 4);
        g_buttons_map_addr[which].IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B = ioremap(g_buttons[which].IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B, 4);
        g_buttons_map_addr[which].GDIR = ioremap(g_buttons[which].GDIR, 4);
        g_buttons_map_addr[which].PSR = ioremap(g_buttons[which].PSR, 4);
    }

    //使能時鐘
    val = *g_buttons_map_addr[which].CCM_CCGR1;
    val |= (0x3 << 26);
    *(g_buttons_map_addr[which].CCM_CCGR1) = val;

    /*
    配置GPIO1_IO18為GPIO模式
    */
    val = *g_buttons_map_addr[which].IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B;
    val &= (~0xf);  //低四位先清零
    val |= (0x5);   //設定為GPIO模式
    *(g_buttons_map_addr[which].IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B) = val;

    /*
    設定GPIO1_IO18為輸出
    */
    *(g_buttons_map_addr[which].GDIR) &= ~(0x1 << 18);
    return 0;
}

int button_read(unsigned int which)  //讀取引腳電平狀態,which表示引腳編號
{
    // printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    if(!(*(g_buttons_map_addr[which].PSR) & (1 << 18)))
    {
        //udelay(10000);  //延時10ms  引數太大,udelay不允許,改為mdelay
        mdelay(10);
        if(!(*(g_buttons_map_addr[which].PSR) & (1 << 18)))
        return 0;
    }
    return 1;
}


struct button_operations button_oprs = {
    .init = button_init,
    .read = button_read,
};

int board_button_probe(struct platform_device *pdev)
{
    struct device_node *np;
    int err;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    //獲取device_node結構體
    np = pdev->dev.of_node;
    if(!np)
    {
        printk("device_node error\n");
        return -1;
    }

    //獲取暫存器資訊
    err = of_property_read_u32(np, "pin", &g_buttons[cur_counts].group_pin);
    err = of_property_read_u32_index(np, "pin_reg", 0, &g_buttons[cur_counts].CCM_CCGR1);
    err = of_property_read_u32_index(np, "pin_reg", 1, &g_buttons[cur_counts].IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B);
    err = of_property_read_u32_index(np, "pin_reg", 2, &g_buttons[cur_counts].GDIR);
    err = of_property_read_u32_index(np, "pin_reg", 3, &g_buttons[cur_counts].PSR);

    //建立裝置節點,使用上層驅動提供的函式
    button_device_register(cur_counts);

    //節點數加1
    cur_counts++;

    return 0;
}

int board_button_remove(struct platform_device *pdev)
{
    int i;
    int err;
    struct device_node* np;
    int button_pin;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    //獲取device_node結構體
    np = pdev->dev.of_node;
    err = of_property_read_u32(np, "pin", &button_pin);
    for(i = 0; i < cur_counts; i++)
    {
        if(g_buttons[i].group_pin == button_pin)
        {
            button_device_unregister(i);
            g_buttons[i].group_pin = -1;
            break;
        }
    }

    for(i = 0; i < cur_counts; i++)
    {
        if(g_buttons[i].group_pin != -1)
        {
            break;
        }
    }

    if(i == cur_counts)
    {
        cur_counts = 0;
    }

    return 0;
}


static struct of_device_id mykeys[] = {
    {
        .compatible = "key0_driver"
    },
    {},
};

struct platform_driver button_drv = {
    .probe = board_button_probe,
    .remove = board_button_remove,
    .driver = {
        .name = "key",
        .of_match_table = mykeys,
    },
};

static int __init button_driver_init(void)
{
    int err;
    err = platform_driver_register(&button_drv);    //註冊平臺驅動
    //呼叫上層介面註冊、向上層驅動提供操作函式
    register_button_operations(&button_oprs);
    return 0;
}

static void __exit button_driver_exit(void)
{
    platform_driver_unregister(&button_drv);
}

module_init(button_driver_init);
module_exit(button_driver_exit);

MODULE_LICENSE("GPL");

3、上層驅動框架

點選檢視程式碼
/*
按鍵驅動程式上層框架,主要實現:
(1)file_operations結構體內容實現
(2)新增驅動入口/出口函式,在入口函式註冊驅動,在出口函式撤銷驅動
(3)提供下層驅動向上層註冊的介面:建立裝置節點、刪除裝置節點、下層向上層註冊按鍵相關的操作函式
*/

#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>


#include "button_opr.h"
int major;  //主裝置號
struct class* button_class; //裝置類
struct button_operations* p_button_opr;

void button_device_register(int minor)
{
    device_create(button_class, NULL, MKDEV(major, minor), NULL, "key_%d", minor);
}

void button_device_unregister(int minor)
{
    device_destroy(button_class, MKDEV(major, minor));
}


void register_button_operations(struct button_operations* button_opr)
{
    p_button_opr = button_opr;
}

EXPORT_SYMBOL(button_device_register);
EXPORT_SYMBOL(button_device_unregister);
EXPORT_SYMBOL(register_button_operations);

int button_open(struct inode * node, struct file * file)
{
    int which;
    printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
    which = iminor(node);
    p_button_opr->init(which);
    return 0;
};

ssize_t button_read (struct file *file, char __user *user, size_t count, loff_t *start)
{
    struct inode *node;
    unsigned int which;
    int ret;
    int err;
    // printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);

    node = file_inode(file);
    which = iminor(node);
    ret = p_button_opr->read(which);
    err = copy_to_user(user, &ret, 1);

    return ret;
};

int button_close (struct inode *node, struct file *file)
{
    printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);

    return 0;
}

struct file_operations button_opr = {
    .owner = THIS_MODULE,
    .open = button_open, 
    .read = button_read,
    .release = button_close
    
};

static int __init button_init(void)
{
    /*
    驅動入口函式
    (1)註冊驅動;(2)建立類
    */
    int err;
    printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
    major = register_chrdev(major, "button_driver", &button_opr);
    button_class = class_create(THIS_MODULE, "button_class");
    err = PTR_ERR(button_class);
    if (IS_ERR(button_class))
    {
        unregister_chrdev(major, "button_driver");
        return -1;
    }

    return 0;
}

static void __exit button_exit(void)
{
    printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
    class_destroy(button_class);
    unregister_chrdev(major, "button_driver");
}

module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");

4、應用程式,按鍵按下燈亮,再按下燈滅,如此反覆

點選檢視程式碼
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char** argv)
{
    int fd_button, fd_led;
    char val;
    char state;

    /*判斷引數*/
    if(argc != 3)
    {
        printf("Usage: %s <buton_dev> <led_dev>\n", argv[0]);
    }

    fd_button = open(argv[1], O_RDWR);
    fd_led = open(argv[2], O_RDWR);
    if(fd_button == -1 || fd_led == -1)
    {
        printf("can not open file %s\n", argv[1]);
        return -1;
    }

    /*讀檔案, 控制led燈*/
    state = 0;
    write(fd_led, &state, 1);
    while(1)
    {
        read(fd_button, &val, 1);
        if(val == 0)
        {
            usleep(10000);
            read(fd_button, &val, 1);
            if(val == 0)
            {
                state = ~state;
            }
            write(fd_led, &state, 1);
            // printf("led state: %d", state);
            sleep(1);
        }

    }
    close(fd_button);
    close(fd_led);


    return 0;
}

至此,按鍵驅動程式完成。本次驅動的編寫與led驅動程式類似,可以看作是一次鞏固。

使用按鍵控制led亮滅看似很簡單的功能,但是以此為基礎探究Linux的知識卻可以很深入。比如使用中斷方式判斷按鍵是否按下、燈和按鍵是兩個事件,可以引申出程序執行緒,進而基於此實現程序執行緒的通訊、同步,訊息機制、互斥等等。而且任何嵌入式裝置功能的開發,本質都是操作IO操作暫存器,和點燈、按鍵沒有本質的區別,只不過框架稍微複雜一些,關鍵在linux的那些知識。
接下來要深入學習pinctrl和gpio子系統,這部分在嵌入式linux開發中使用率很高,屬於提供工作效率的工具,應該要掌握,預計花費1-2個月的時間學習掌握。

相關文章