魏東山 Linuxドライバー入門実験教室(4) LEDドライバー

序文

(1) Hello ドライバーを学習した後、ドライバーの開発について少し理解しました。開発ボードは i.max6ull を使用していますが、STM32MP157 と Allwinner の D1H も説明します。
(2) hello ドライバーについてまだよくわからない場合は、
Wei Dongshan Linux ドライバー入門実験クラス (1) hello ドライバー
Wei Dongshan Linux ドライバー入門実験クラス (2) hello ドライバー - ドライバー層とアプリケーション層を参照してください。通信とデバイスノードの自動生成;
Wei Dongshan Linux ドライバーエントリー実験教室 (3) hello driver - 指定数のセカンダリデバイス番号を申請する
(3) 注: Wei Dongshan 氏のコードは独自のフレームワークで書かれているため、初心者が学び理解するのに便利なので、微調整を行い、不要な場所を削除および修正しました。

Linux ドライバーが GPIO を指す方法を理解する

概略図を表示

(1) ドライバプログラムを作成する際、点灯動作を実行する必要がある場合、まずどのピンを制御して実際に LED を動作させるかを知る必要があります。回路図を見ると、LED が GPIO5_3 によって制御されていることがわかりました。

ここに画像の説明を挿入

レジスタを通じて GPIO を直接操作する

(1) 入門ビ​​デオを学習していると、ioremap() 関数を使用してレジスタをマップし、レジスタを直接操作しているのをよく見かけます。このレジスタを使用しなくなった場合は、iounmap() 関数を呼び出してレジスタを解放します。
(2) この書き方は、51 個のシングルチップ マイコンのプログラムを書くような非常に原始的なものであることは間違いありません。ただし、51 個のシングルチップマイコンではレジスタの数がそれほど多くないため、レジスタを直接操作するのが面倒ではありません。しかし、i.max6ullレベルのチップではレジスタが多く、レジスタを直接操作するのは非常に面倒であることは間違いありません。
(3) GPIO をレジスタから直接操作するのは基本的に無意味なので説明は省略しますが、学びたい場合は Punctual Atom や Wei Dongshan の運転指導ビデオを見てください。ここでは Linux 統合インターフェースを使用して GPIO を操作する方法を説明します。

ピン番号で GPIO を操作する

ピン番号の概念

(1) 上の回路図から、LED が GPIO5_3 によって制御されていることがわかった後、操作を直接開始できますか?
(2) いいえ、Linux では、GPIO の識別と制御は通常、特定の GPIO ピンを一意に識別するために使用されるピン番号を通じて実行されます。
(3) stm32 や msp430 などのベアメタル開発の経験がある場合、チップごとに GPIO 名の定義が異なることがわかります。たとえば、STM32 ではピンを PA0 および PB4 として定義します。ただし、MSP430シングルチップマイコンの場合、ピンの定義はP2.3、P1.0などとなります。メーカーが異なれば、チップの GPIO 名も異なります。
(4) GPIO 名が異なる場合はどうなりますか? これにより、ドライバー開発者はさまざまなチップの命名規則を常に覚えておく必要があり、明らかに非常に面倒です。したがって、Linux では、どのような名前を付けても、チップのメーカーは気にしない、Linux を実行したい場合は、ピンを番号に変更する必要があると規定されており、この番号は ピン番号 と呼ばれますドライバー開発者は、このピンに対応するピン番号を知るだけで、操作できるようになります。このピンをピン番号に変換するプロセスは、元のチップ工場のエンジニアによって行われます。
(5) ピン番号を取得する最も簡単な方法は、製造元に直接問い合わせることです。たとえば、以下は Feiteng チップとそのピン マッピング テーブルです。(ここで、写真と案内を提供してくれたステーションC - 合肥のセカンドスキンと、本も持たずに漂流している上司の指導のためにコミュニケーショングループのIDに感謝したいと思います)

ここに画像の説明を挿入

i.max6ull ピン番号取得

(1)
<1> ただし、現時点で疑問に思っている人もいるかもしれませんが、i.max6ull 開発ボードは GPIO5_3 を制御する必要があり、ピン マッピング テーブルが見つからないので、ピン番号はいくつですか?
<2> マッピング テーブルが見つからない場合は、開発ボードに接続し、コマンドcat /sys/kernel/debug/gpioを入力して、GPIO マッピング テーブルとその開始アドレスを取得します。

ここに画像の説明を挿入

(2)
<1> このとき、ここで検索した gpiochip5 が GPIO5 だと思う人もいるかもしれません。答えは否定的です。
<2>なぜそんなことを言うのですか?上で述べたように、メーカーごとに GPIO の名前が異なり、そのメーカーのエンジニアは最終的にこれらの GPIO をピン番号に抽象化します。この抽象化プロセスでは、ドライバー内の名前が回路図上の名前と若干異なる場合があります。たとえば、i.max6ull 開発ボードの GPIO5 は、GPIO1 から計算を開始するため、gpiochip4 になります。ドライバープログラムはgipo0から始まります。
<3> これが事実であると判断するにはどうすればよいですか? まず上の図を見て、gpio0 のアドレスは 209c000 です。次にチップのマニュアルを直接開くと、GPIO1 の開始アドレスが 209c000 であり、正確に一致していることがわかります。

ここに画像の説明を挿入

(4) 次に、GPIO5 を gpio4 に対応するようにガイドします。その後、ターミナルからガイドできます。gpio4 の開始ピン番号は 128 で、GPIO5_3 のピン番号は 128+3=131 になります。

ここに画像の説明を挿入

STM32MP157_Pro ピン番号取得

(1) 同様に、ターミナルにcat /sys/kernel/debug/gpioコマンドを入力します。
(2) ST からのこの情報は依然として非常に友好的であり、GPIOA が gpiochip0 であると直接述べていることがわかります。PA10 を制御したい場合、ピン番号は 10 です。

ここに画像の説明を挿入

D1Hピン番号取得

Quanzhiが提供する関連情報は比較的ゴミなので、結論を直接言います。PC1 を制御したい場合、ピン番号は 2*32+1=65 になります。PA0 はピン番号 0 に対応し、PB0 はピン番号 32 に対応します。

Linux 用統一インターフェイス — GPIO サブシステム

統一インターフェースが必要な理由

(1) Linux の GPIO サブシステムを説明する前に、シングルチップ マイコンの開発を例に説明します。
(2) 大多数の人にとって、組み込み開発の学習は 51 個のシングルチップ マイクロコンピュータから始まります。STC89C52 は、51 個のシングルチップ マイコンの中でも古典的なシングルチップ マイコンであり、誰もが少しは理解しています。
(3) STC89C52 シングルチップマイコンのプログラムを書くときは、次のようなシリアルポート初期化プログラムのようにレジスタを直接操作します。

void UartInit(void)		//[email protected]
{
    
    
	SCON = 0x50;		//8位数据,可变波特率
	AUXR &= 0xBF;		//定时器1时钟为Fosc/12,即12T
	AUXR &= 0xFE;		//串口1选择定时器1为波特率发生器
	TMOD &= 0x0F;		//设定定时器1为16位自动重装方式
	TL1 = 0xE8;		//设定定时初值
	TH1 = 0xFF;		//设定定时初值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
}

(4) 51 シングルチップ マイクロコンピュータを学習した後、ほとんどの人は STM32F103 チップに進み始めます。STM32F103チップはレジスタが多いため、レジスタを使って直接開発するにはマニュアルを確認するのが非常に面倒です。そこで ST 社はいくつかのライブラリをカプセル化しました。以下は GPIO 操作部分のライブラリ関数です。

ここに画像の説明を挿入

(5) STM32 を学習した後、電子競技のため TI の MSP430 を学習する必要がある人もいるかもしれません。彼のライブラリ関数の一部は次のとおりです。

ここに画像の説明を挿入

(6) または、TI の TM4C123 チップの一部のライブラリ関数は次のとおりです

ここに画像の説明を挿入

(7) 異なるチップには異なるライブラリ関数があることがわかります。ビジネスプログラムを作成すると、STM32 上で問題なく動作します。何らかの理由でチップを変更することになり、そのチップのライブラリ機能が STM32 のライブラリ機能と異なる場合。最終的な結果はどうなるでしょうか? 明らかに、すべてのビジネス プログラムを書き直す必要があります。これはとても面倒なことです!
(8) この状況を防ぐために、Linux では、Linux を実行したい限り、どのようなチップであっても、統一されたインターフェイスを提供する必要があると規定しています。どのメーカーであっても、GPIO を出力として設定するチップの関数は int gpio_direction_output() と呼び出す必要があります。
(9) これを行うとどのようなメリットがありますか? もちろん、ビジネス コードを変更する必要はなく、チップを変更する場合は、最下層を少し変更するだけで済みます。Linux を実行しなければ、明確な階級が存在するが、Linux を実行すると、すべての存在は平等であると言う人がいるのはこのためです。

GPIOサブシステム機能の紹介

Linux の GPIO サブシステムでは、次の関数を通じて GPIO を設定できます。

int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);
int gpio_get_value(unsigned gpio);
void  gpio_set_value(unsigned gpio, int value);

gpio_request()

(1) 関数: Linux カーネルで GPIO ピンを要求するために使用される関数。ピンを操作したい場合は、最初に gpio_request() 関数を呼び出す必要があります。
(2) gpio: 要求される GPIO ピン番号。このピン番号は自分で直接指定できます (たとえば、上記の説明に非常に多くのスペースを費やしました)。of_get_named_gpio 関数を使用して、デバイス ツリーから指定した GPIO 属性情報を取得することもできます (デバイス ツリーの内容については後で説明します。ここでは影響を残すだけです)。
(3) label: GPIO に名前を付けます。直接のピン番号は読みにくいため、このピン番号に名前を付けることができます。好きな名前を付けても問題ありません。
(4) 戻り値: GPIO のアプリケーションが成功したことを示す 0 を返す方法。負の数値が返された場合は、GPIO の申請にエラーがあることを意味します。

/****** 函数介绍 ******/
/* 作用 :  向Linux 内核中用于请求申请一个 GPIO 引脚
 * 传入参数 : 
     * gpio : 要请求的 GPIO 引脚号
     * label : 给GPIO起一个名字
 * 返回参数 :  如何返回0,表示申请GPIO成功。如果返回负数,表示申请GPIO出现错误
*/
int gpio_request(unsigned gpio, const char *label);

gpio_free()

(1) 機能: 特定の GPIO が使用されていない場合、gpio_free 関数を呼び出して解放する必要があります。
(2) gpio: 解放する GPIO ピン番号。gpio_request の GPIO ピン番号と同じものです。
(3) 戻りパラメータ:なし

/****** 函数介绍 ******/
/* 作用 : 如果不使用某个GPIO了,那么就需要调用 gpio_free 函数进行释放
 * 传入参数 : 
     * gpio : 要释放的GPIO引脚号
 * 返回参数 :  无
*/
void gpio_free(unsigned gpio);

gpio_direction_input()

(1) 機能: GPIO を入力方向として設定します。GPIO を申請した後、要件に応じて入力または出力として設定する必要があります。この関数は GPIO を入力として設定できます
(2) gpio: 入力として設定する GPIO ピン番号
(3) 戻り値: 0 を返します。 GPIO は正常にインポートされ、ピンは入力モードに設定されます。GPIO ピンの設定のエラーまたは失敗を示す負の数を返します。

/****** 函数介绍 ******/
/* 作用 : 设置某个 GPIO 为输入
 * 传入参数 : 
     * gpio : 要设置为输入的GPIO 引脚号
 * 返回参数 : 设置成功返回 0; 设置失败返回负值
*/
int gpio_direction_input(unsigned gpio);

gpio_direction_output()

(1) 機能: GPIO を出力方向として設定し、デフォルトの出力値を設定します。GPIO を申請した後、要件に応じて入力または出力として設定する必要があります。この機能では、GPIO を出力として設定できます
(2) gpio: 出力として設定された GPIO ピン番号
(3) 値: GPIO のデフォルト出力値。GPIO の初期化が成功すると、デフォルトの出力電圧になります。
(4) 戻りパラメータ: 0 を返し、GPIO ピンが出力モードに正常に設定されたことを示します。GPIO ピンの設定のエラーまたは失敗を示す負の数を返します。

/****** 函数介绍 ******/
/* 作用 : 设置某个 GPIO 为输出,并且设置默认输出值
 * 传入参数 : 
     * gpio : 要设置为输出的GPIO 引脚号
     * value : GPIO 默认输出值
 * 返回参数 : 设置成功返回 0; 设置失败返回负值
*/
int gpio_direction_output(unsigned gpio, int value);

gpio_get_value()

(1) 機能: 指定した GPIO のレベル情報を取得します。
(2) gpio: レベル値を取得するための GPIO ラベル
(3) 戻り値: レベル情報の取得に成功し、高レベルの場合は 1 を返し、低レベルの場合は 0 を返します。 。GPIO レベルの取得に失敗した場合は、負の値が返されます。

/****** 函数介绍 ******/
/* 作用 : 获取指定GPIO的电平值
 * 传入参数 : 
     * gpio : 要获取电平值的GPIO标号
 * 返回参数 : 获取电平信息成功,高电平返回1,低电平返回0。GPIO电平获取失败返回负值
*/
int gpio_get_value(unsigned gpio);

gpio_set_value()

(1) 機能: 指定した GPIO のレベル値を設定します
(2) gpio: 指定した GPIO のレベル値を設定します
(3) 値: 設定するレベル値。0 が渡された場合は、GPIO が低電力フラットに設定されています。ゼロ以外の値を渡すことは、GPIO をハイ レベルに設定することを意味します
(4) 戻りパラメータ: なし

/****** 函数介绍 ******/
/* 作用 : 获取指定GPIO的电平值
 * 传入参数 : 
     * gpio : 要设置指定GPIO的电平值
     * value : 要获取电平值的GPIO标号
 * 返回参数 : 无
*/
void  gpio_set_value(unsigned gpio, int value);

コードの書き方

(1) 上記で多くのことを説明しました。ここで、コードを直接書き始めることができます。
(2) コードを記述する順序は、上から下に記述することをお勧めします。最初にアプリケーション層を記述し、大まかなフレームワークを作成します。次に、具体的な動作を実現するために以下のドライバを記述します。

アプリケーション層プログラミング

(1) 最初に要件を決定し、ターミナルで次のコマンドを入力する予定です。すると、下図のような結果が表示されます。

/* 可执行文件名   | 表示要操作哪一盏灯  | 灯状态  |    效果
 * ./led_test    |   <0|1|2|..>        | on     |硬件上开灯
 * ./led_test    |   <0|1|2|..>        | off    |硬件上关灯
 * ./led_test    |   <0|1|2|..>        |        |读取led状态,并且显示在终端
 */

ここに画像の説明を挿入

(2) アプリケーション層のコードは以下のとおりです。
<1> strcmp()関数はCのライブラリ関数であり、2つの文字列が等しいかどうかを判定し、等しい場合は0を返し、等しくない場合は0以外の値を返す関数であることに注意してください。 。
<2> strtol()関数は文字を数値に変換する関数です。コマンドラインに入力した 1 は、実際には数字の 1 ではなく、文字 1 であるためです。データ型をドライバー層と統一するために、ここでこの関数を呼び出す必要があります。
(3) 注: アプリケーション層のコードは比較的単純であるため、説明は省略します。理解できない場合は、 Wei Dongshan の Linux ドライバー入門実験教室 (1) hello ドライバーのアプリケーション層コードの説明を参照してください。この部分を読んでも理解できない場合は、まず C 言語を学習してから Linux を学習することをお勧めします。

/* 说明 : 
 	*1,本代码是学习韦东山老师的驱动入门视频所写,增加了注释。
 	*2,采用的是UTF-8编码格式,如果注释是乱码,需要改一下。
 	*3,这是应用层代码
 	*4,TAB为4个空格
 * 作者 : CSDN风正豪
*/

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

static int fd;


//int led_on(int which);
//int led_off(int which);
//int led_status(int which);

/* 可执行文件名   | 表示要操作哪一盏灯  | 灯状态  |    效果
 * ./led_test    |   <0|1|2|..>        | on     |硬件上开灯
 * ./led_test    |   <0|1|2|..>        | off    |硬件上关灯
 * ./led_test    |   <0|1|2|..>        |        |读取led状态,并且显示在终端
 */
int main(int argc, char **argv)
{
    
    
	int ret;     //存放函数返回值,用于判断函数是否正常执行
	char buf[2]; //存放命令行的后两个字符(<0|1|2|...> [on | off])

	
	//如果传入参数少于两个,打印文件用法
	if (argc < 2) 
	{
    
    
		printf("Usage: %s <0|1|2|...> [on | off]\n", argv[0]);
		return -1;
	}


	//打开文件,因为在驱动层中,device_create()函数创建的设备节点名字叫做100ask_led,而设备节点都存放在/dev目录下,所以这里是/dev/100ask_led
	fd = open("/dev/100ask_led", O_RDWR);
	//如果无法打开,返回错误
	if (fd == -1)
	{
    
    
		printf("can not open file /dev/100ask_led\n");
		return -1;
	}
	//如果传入了三个参数,表示写入
	if (argc == 3)
	{
    
    
		/* write */
		/* 作用 : 将字符串转化为一个整数
		 * argv[1] :  要转换为长整数的字符串
		 * NULL :如果提供了 endptr 参数,则将指向解析结束位置的指针存储在 endptr 中。endptr 可以用于进一步处理字符串中的其他内容
		 * 0 : 设置为 0,则会根据字符串的前缀(如 "0x" 表示十六进制,"0" 表示八进制,没有前缀表示十进制)来自动判断进制
		*/
		buf[0] = strtol(argv[1], NULL, 0);

		//判断是否为打开
		if (strcmp(argv[2], "on") == 0)
			buf[1] = 0;  //因为LED外接3.3V,所以输出低电平才是开灯
		else
			buf[1] = 1;  //因为LED外接3.3V,所以输出高电平才是关灯
		//向字符驱动程序中写入
		ret = write(fd, buf, 2);
	}
	//否则表示读取电平信息
	else
	{
    
    
		/* read */
		/* 作用 : 将字符串转化为一个整数
		 * argv[1] :  要转换为长整数的字符串
		 * NULL :指向第一个不可转换的字符位置的指针
		 * 0 : 表示默认采用 10 进制转换
		*/
		buf[0] = strtol(argv[1], NULL, 0);
		//读取电平,从驱动层读取两个数据
		ret = read(fd, buf, 2);
		//如果返回值为2,表示正常读取到了电平。(为什么是2,看驱动程序的gpio_drv_read)
		if (ret == 2)
		{
    
    
			//打印引脚信息
			printf("led %d status is %s\n", buf[0], buf[1] == 0 ? "on" : "off");
		}
	}
	
	close(fd);
	
	return 0;
}

ドライバー層のコード記述

ドライバー層のコード

/* 说明 : 
 	*1,本代码是学习韦东山老师的驱动入门视频所写,增加了注释。
 	*2,采用的是UTF-8编码格式,如果注释是乱码,需要改一下。
 	*3,这是驱动层代码
 	*4,TAB为4个空格
 * 作者 : CSDN风正豪
*/

#include "asm-generic/errno-base.h"
#include "asm-generic/gpio.h"
#include "asm/uaccess.h"
#include <linux/module.h>
#include <linux/poll.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/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>

//描述一个引脚
struct gpio_desc{
    
    
	int gpio;   //引脚编号
    char *name; //名字
};

static struct gpio_desc gpios[] = {
    
    
    {
    
    131, "led0", },  //引脚编号,名字
};

/* 主设备号                                                                 */
static int major = 0;
static struct class *gpio_class;  //一个类,用于创建设备节点


/* 实现对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    
    
	char tmp_buf[2];  //存放驱动层和应用层交互的信息
	int err;   //没有使用,用于存放copy_from_user和copy_to_user的返回值,消除报错
  int count = sizeof(gpios)/sizeof(gpios[0]); //记录定义的最大引脚数量

	//应用程序读的时候,传入的值如果不是两个,那么返回一个错误
	if (size != 2)
		return -EINVAL;
	
	/* 作用 : 驱动层得到应用层数据
	 * tmp_buf : 驱动层数据
	 * buf : 应用层数据
	 * 1  :数据长度为1个字节(因为我只需要知道他控制的是那一盏灯,所以只需要传入一个字节数据)
	*/
	err = copy_from_user(tmp_buf, buf, 1);
	
	//第0项表示要操作哪一个LED,如果操作的LED超出,表示失败
	if (tmp_buf[0] >= count)
		return -EINVAL;
	
	//将引脚电平读取出来
	tmp_buf[1] = gpio_get_value(gpios[(int)tmp_buf[0]].gpio);
	
	/* 作用 : 驱动层发数据给应用层
	 * buf : 应用层数据
	 * tmp_buf : 驱动层数据
	 * 2  :数据长度为2个字节
	*/
	err = copy_to_user(buf, tmp_buf, 2);
	
	return 2;
}

static ssize_t gpio_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    
    
    unsigned char ker_buf[2];
    int err;
	//应用程序读的时候,传入的值如果不是两个,那么返回一个错误
    if (size != 2)
        return -EINVAL;
	
	/* 作用 : 驱动层得到应用层数据
	 * tmp_buf : 驱动层数据
	 * buf : 应用层数据
	 * size  :数据长度为size个字节
	*/
    err = copy_from_user(ker_buf, buf, size);

	//如果要操作的GPIO不在规定范围内,返回错误
    if (ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]))
        return -EINVAL;

	//设置指定引脚电平
    gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]);
    return 2;    
}



/* 定义自己的file_operations结构体                                              */
static struct file_operations gpio_led_drv= {
    
    
	.owner	 = THIS_MODULE,
	.read    = gpio_drv_read,
	.write   = gpio_drv_write,
};


/* 在入口函数 */
static int __init gpio_drv_init(void)
{
    
    
    int err;  //用于保存函数返回值,用于判断函数是否执行成功
    int i;    //因为存在多个GPIO可能要申请,所以建立一个i进行for循环
    int count = sizeof(gpios)/sizeof(gpios[0]);  //统计有多少个GPIO
    
	/*__FILE__ :表示文件
	 *__FUNCTION__ :当前函数名
	 *__LINE__ :在文件的哪一行
	*/
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	for (i = 0; i < count; i++)
	{
    
    		
		/* 设置为输出引脚 */
		//申请指定GPIO引脚,申请的时候需要用到名字
		err = gpio_request(gpios[i].gpio, gpios[i].name);
		//如果返回值小于0,表示申请失败
		if (err < 0) 
		{
    
    
			//如果GPIO申请失败,打印出是哪个GPIO申请出现问题
			printk("can not request gpio %s %d\n", gpios[i].name, gpios[i].gpio);
			return -ENODEV;
		}
		//如果GPIO申请成功,设置输出高电平
		gpio_direction_output(gpios[i].gpio, 1);
	}

	/* 注册file_operations 	*/
	//注册字符驱动程序
	major = register_chrdev(0, "100ask_led", &gpio_led_drv);  /* /dev/gpio_desc */
	
	/******这里相当于命令行输入 mknod  /dev/100ask_gpio c 240 0 创建设备节点*****/
	
	//创建类,为THIS_MODULE模块创建一个类,这个类叫做gpio_class
	gpio_class = class_create(THIS_MODULE, "100ask_led_class");
	if (IS_ERR(gpio_class))   //如果返回错误
	{
    
    
		/*__FILE__ :表示文件
		 *__FUNCTION__ :当前函数名
		 *__LINE__ :在文件的哪一行
		*/
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		//注销字符驱动程序
		unregister_chrdev(major, "100ask_led_class");
		//返回错误
		return PTR_ERR(gpio_class);
	}
	
	/*输入参数是逻辑设备的设备名,即在目录/dev目录下创建的设备名
	 *参数一 : 在gpio_class类下面创建设备
	 *参数二 : 无父设备的指针
	 *参数三 : 主设备号+次设备号
	 *参数四 : 没有私有数据
	*/
	device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_led"); /* /dev/100ask_gpio */
	
	//如果执行到这里了,说明LED驱动装载完成
	printk("LED driver loading is complete\n");
	return err;
}

/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
 */
static void __exit gpio_drv_exit(void)
{
    
    
    int i;
    int count = sizeof(gpios)/sizeof(gpios[0]);
	/*__FILE__ :表示文件
	 *__FUNCTION__ :当前函数名
	 *__LINE__ :在文件的哪一行
	*/
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//销毁gpio_class类下面的设备节点
	device_destroy(gpio_class, MKDEV(major, 0));
	//销毁gpio_class类
	class_destroy(gpio_class);
	//注销驱动
	unregister_chrdev(major, "100ask_led");

	for (i = 0; i < count; i++)
	{
    
    
		//将GPIO释放
		gpio_free(gpios[i].gpio);		
	}
	
	//如果执行到这里了,说明LED驱动卸载完成
	printk("The LED driver is uninstalled\n");
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(gpio_drv_init);  //确认入口函数
module_exit(gpio_drv_exit);  //确认出口函数

/*最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加
 *这个协议要求我们代码必须免费开源,Linux遵循GPL协议,他的源代码可以开放使用,那么你写的内核驱动程序也要遵循GPL协议才能使用内核函数
 *因为指定了这个协议,你的代码也需要开放给别人免费使用,同时可以根据这个协议要求很多厂商提供源代码
 *但是很多厂商为了规避这个协议,驱动源代码很简单,复杂的东西放在应用层
*/
MODULE_LICENSE("GPL"); //指定模块为GPL协议
MODULE_AUTHOR("CSDN:qq_63922192");  //表明作者,可以不写

ドライバー層のコード分析

(1) コードを見て、ソースコードをよく読むためには、まず関数のエントリが何であるかを知る必要があります。ベアメタル開発の場合、ほとんどのエントリ関数は main() 関数であるため、ベアメタル プログラムを読み取るときは、まずその main 関数を見つける必要があります。
(2) Linux ドライバは異なります。その関数エントリは module_init() マクロによって定義されており、コマンドラインに insmod led_drv.ko と入力してドライバをロードします。システムは module_init() マクロに含まれる関数を入力します。
(3)
<1> 下図に示すように、開発ボードに接続されたコマンドラインにinsmod led_drv.ko コマンドを入力すると、カーネルは 2 行の情報を出力します。
<2>この 2 行のデータが印刷されるのはなぜですか? module_init() マクロに含まれる関数 gpio_drv_init() には、次の 2 つの printk() ステートメントがあるためです。
<3> 注: ドライバーのロード時にカーネルによって出力されるデータがない場合は、開発ボードに接続されているコマンド ラインを入力する必要があります。echo “7 4 1 7” > /proc/sys/kernel/printk

	/*__FILE__ :表示文件
	 *__FUNCTION__ :当前函数名
	 *__LINE__ :在文件的哪一行
	*/
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	//如果执行到这里了,说明LED驱动装载完成
	printk("LED driver loading is complete\n");

ここに画像の説明を挿入

(4)
<1>関数エントリが何であるかがわかったので、実際にコードの分析を開始します。
<2> gpio_drv_init() 関数の上位 3 つの変数アプリケーションを見てみましょう。err と i は、次のコードを読むと理解しやすくなります。
<3> ここでのカウントの適用について少し混乱する人もいるかもしれません。プログラムの柔軟性を高め、最小限のコード変更で機能を実現するために、ここでカプセル化のための構造を作成するためです。
<4> ボード上に複数の LED がある場合、LED を追加したい場合は、追加する LED の GPIO ピン番号と名前を gpios[] 配列に書き込むだけです。その後、カウント変数は自動的に変更されます。
<5> Wei Dongshan 先生のビデオを見た後にここに来た方は、この記事の gpio_desc 構造が Wei Dongshan 先生の構造とは異なることがわかります。他のパラメータを使用していないためですが、初心者向けに干渉する項目を減らし、必要なパラメータのみを残す予定です。


int err;  //用于保存函数返回值,用于判断函数是否执行成功
int i;    //因为存在多个GPIO可能要申请,所以建立一个i进行for循环
int count = sizeof(gpios)/sizeof(gpios[0]);  //统计有多少个GPIO

//描述一个引脚
struct gpio_desc{
    
    
	int gpio;   //引脚编号
    char *name; //名字
};

static struct gpio_desc gpios[] = {
    
    
    {
    
    131, "led0", },  //引脚编号,名字
    //{132, "led1", },  //引脚编号,名字,如果需要增加GPIO
};

(5)
<1>LED ドライバーの拡張性を向上させたいため。したがって、上記では LED の GPIO の数を取得するために count 変数が使用されており、GPIO を順番に登録するために必要な for ステートメントは 1 つだけです。
<2> GPIO を入力または出力として設定する前に、まず GPIO を登録する必要がありますこれは、マイクロコントローラー プログラムと同様に、GPIO を構成し、最初にそのクロックをオンにする必要があります。
<3> GPIO を登録した後、GPIO が正常に登録されたかどうかを判断する必要がありますが、GPIO が正常に登録されていない場合に操作するとバグが発生します。
<4> GPIO の登録が成功したら、GPIO の方向を設定します。ここでは LED を駆動したいため、 gpio_direction_output() 関数を呼び出して GPIO を出力方向として設定する必要があります。
<5> 上の回路図から、GPIO がハイ レベルを出力すると LED がオフになり、GPIO がロー レベルを出力すると LED がオンになることがわかります。一般に、LED はデフォルトでオフになっています。したがって、 gpio_direction_output() の 2 番目のパラメーターは 1 で渡され、GPIO がデフォルトでハイ レベルを出力することを示します。

	for (i = 0; i < count; i++)
	{
    
    		
		/* 设置为输出引脚 */
		//申请指定GPIO引脚,申请的时候需要用到名字
		err = gpio_request(gpios[i].gpio, gpios[i].name);
		//如果返回值小于0,表示申请失败
		if (err < 0) 
		{
    
    
			//如果GPIO申请失败,打印出是哪个GPIO申请出现问题
			printk("can not request gpio %s %d\n", gpios[i].name, gpios[i].gpio);
			return -ENODEV;
		}
		//如果GPIO申请成功,设置输出高电平
		gpio_direction_output(gpios[i].gpio, 1);
	}

(6)
<1> 以下のコードについては、これ以上言うことはありません。理解できない場合は、前の 3 つの Hello Driver の説明を読んでください。
<2>もう一度強調する必要がある唯一のことは、 register_chrdev() 関数が file_operations 構造体を登録し、アプリケーション層にインターフェイスを提供するということです。たとえば、ここでは read() 関数と write() 関数のインターフェイスを提供し、アプリケーション層が read() 関数を呼び出し、ドライバー層が gpio_drv_read() 関数を呼び出します。アプリケーション層が write() 関数を呼び出す場合、ドライバー層は gpio_drv_write() 関数を呼び出します。
<3> このとき、疑問を抱く人もいるかもしれない。ドライバー層は、関数を開くおよび閉じるためのインターフェイスを提供しません。では、なぜアプリケーション層はオープン機能とクローズ機能を使用できるのでしょうか? なぜなら、file_operations 構造体で .open への関数ポインタを渡さない場合、アプリケーション層が open 関数を呼び出すと、最初にファイルを開く操作が実行され、次に .open で定義された関数が呼び出されるためです。ドライバー層のポインター。ただし、ドライバーのlayer.openには関数ポインターが定義されていないため、デフォルトでは空の関数になります
<4> 疑問に思う人もいるかもしれませんが、file_operations 構造体の .open と .release には一般的に何を書くのですか? (以前の記事で述べたように、アプリケーション層はドライバー層の file_operations 構造体の .release で指定された関数ポインターに対応する close 関数を呼び出します) <5> 非常に単純で、一般に .open は GPIO の初期化プログラムです
。 。たとえば、上記の gpio_drv_init() 関数では、GPIO を登録し、それを出力方向として設定しました。この部分は .open で記述できます。アプリケーション層で open() 関数を使用する場合は、GPIO を使用する必要があり、登録しても遅くないからです。
<6>.release は通常、GPIO ログアウトです。これは、アプリケーション層が close() 関数を呼び出すと、GPIO が使い果たされたことを意味するためです。ログアウト時に使用できます。(ここでの GPIO ログアウト プログラムはドライバー関数で記述されていることに注意してください)

/* 注册file_operations 	*/
major = register_chrdev(0, "100ask_led", &gpio_led_drv); 

static struct file_operations gpio_led_drv = {
    
    
	.owner	 = THIS_MODULE,
	.read    = gpio_drv_read,
	.write   = gpio_drv_write,
};

(7)
<1> これで、file_operations 構造体の登録が完了しました。そして、この構造では、読み取りおよび書き込み機能のインターフェイスが提供されます。そこで、最初に読み取り関数を分析しましょう。
<2> 理解を容易にするために、まずアプリケーション層に関連するコードを抽出します。
<3>まずアプリケーション層を見ると、buf[] が 2 つの文字を格納する配列であることがわかります。buf[0] はどの LED が制御されているかを格納する役割を果たし、buf[1] は LED レベル情報を格納する役割を果たします (制御 LED が使用されている場合は、LED のオン/オフ情報を格納します。 LED ステータス、LED のオン/オフ情報を保存します)。
<4> アプリケーション層では、strtol() 関数を使用してコマンドラインの文字列を数値に変換し、buf[0] に格納します。
<5> 次に、buf[] が読み取り関数を通じてドライバー層に渡されます。そして、ドライバー層に、2 バイトのデータを読み取るように伝えます。
<6> 次に、ドライバー層のコードを見てみましょう。アプリケーション層は buf[] 文字配列をドライバー層に渡しますが、ドライバー層は buf[0] のデータのみを知る必要がある (つまり、どの LED からデータを読み取るのかを知る必要があるだけ) ため、 copy_from_user() の最初の関数 3 つのパラメーターは 1 を渡すだけで済みます。つまり、ドライバー層は buf[0] のデータのみを知り、buf[0] のデータを tmp_buf[0] に渡します。
<7> どの LED を制御するかがわかったら、アプリケーション層から渡された LED が存在しないと障害が発生するため、逐次 if 判定を行う必要があります。
<8> gpio_get_value() 関数を呼び出すときに、gpios[(int)tmp_buf[0]].gpio を見て混乱する人も多いかもしれません。これを分解してみましょう。まず、最も内側の (int)tmp_buf[0] を見てください。制御する LED が tmp_buf[0] に格納されていることが分かります。たとえば、LED0 を制御したい場合、アプリケーション層から tmp_buf[0] によって取得されるデータは数値 0 です (これが、アプリケーション層が文字を数値に変換するために strtol 関数を呼び出す必要がある理由です)。ただし、 buf は文字配列であり、 gpio_get_value() 関数の 2 番目のパラメーターは整数データを渡す必要があるため、ここでは強制的な型変換が必要です。
<9> tmp_buf[0]=0 であることがわかり、gpios[(int)tmp_buf[0]].gpio は gpios[0].gpio になります。これでわかりましたか。gpios[0].gpio は LED0 のピン番号です。したがって、ここで LED0 のレベル情報を取得し、tmp_buf[1] に保存できます。
<10> 次に、copy_to_user() 関数を通じてドライバー層のデータをアプリケーション層に返します。
<11> 最終アプリケーション層はレベル情報を取得し、buf[1]に格納します。

/****************  应用层   ****************/

char buf[2]; //存放命令行的后两个字符(<0|1|2|...> [on | off])
/* 作用 : 将字符串转化为一个整数
 * argv[1] :  要转换为长整数的字符串
 * NULL :指向第一个不可转换的字符位置的指针
 * 0 : 表示默认采用 10 进制转换
*/
buf[0] = strtol(argv[1], NULL, 0);
//读取电平,从驱动层读取两个数据
ret = read(fd, buf, 2);

/****************  驱动层   ****************/
static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    
    
	char tmp_buf[2];  //存放驱动层和应用层交互的信息
	int err;   //没有使用,用于存放copy_from_user和copy_to_user的返回值,消除报错
	int count = sizeof(gpios)/sizeof(gpios[0]); //记录定义的最大引脚数量

	//应用程序读的时候,传入的值如果不是两个,那么返回一个错误
	if (size != 2)
		return -EINVAL;
	
	/* 作用 : 驱动层得到应用层数据
	 * tmp_buf : 驱动层数据
	 * buf : 应用层数据
	 * 1  :数据长度为1个字节(因为我只需要知道他控制的是那一盏灯,所以只需要传入一个字节数据)
	*/
	err = copy_from_user(tmp_buf, buf, 1);
	
	//第0项表示要操作哪一个LED,如果操作的LED超出,表示失败
	if (tmp_buf[0] >= count)
		return -EINVAL;
	
	//将引脚电平读取出来
	tmp_buf[1] = gpio_get_value(gpios[(int)tmp_buf[0]].gpio);
	
	/* 作用 : 驱动层发数据给应用层
	 * buf : 应用层数据
	 * tmp_buf : 驱动层数据
	 * 2  :数据长度为2个字节
	*/
	err = copy_to_user(buf, tmp_buf, 2);
	
	return 2;
}

(8)
<1> リード関数の解析後、ライト関数の解析を開始します。
<2> 読み取り関数を理解した後、書き込み関数は実際には非常に簡単です。自分で理解してください。それでも理解できない場合は、コメント欄で質問するか、ビデオを見てください。

/****************  应用层   ****************/

/* 作用 : 将字符串转化为一个整数
 * argv[1] :  要转换为长整数的字符串
 * NULL :如果提供了 endptr 参数,则将指向解析结束位置的指针存储在 endptr 中。endptr 可以用于进一步处理字符串中的其他内容
 * 0 : 设置为 0,则会根据字符串的前缀(如 "0x" 表示十六进制,"0" 表示八进制,没有前缀表示十进制)来自动判断进制
*/
buf[0] = strtol(argv[1], NULL, 0);

//判断是否为打开
if (strcmp(argv[2], "on") == 0)
	buf[1] = 0;  //因为LED外接3.3V,所以输出低电平才是开灯
else
	buf[1] = 1;  //因为LED外接3.3V,所以输出高电平才是关灯
//向字符驱动程序中写入
ret = write(fd, buf, 2);

/****************  驱动层   ****************/
static ssize_t gpio_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    
    
    unsigned char ker_buf[2];
    int err;
	//应用程序读的时候,传入的值如果不是两个,那么返回一个错误
    if (size != 2)
        return -EINVAL;
	
	/* 作用 : 驱动层得到应用层数据
	 * tmp_buf : 驱动层数据
	 * buf : 应用层数据
	 * size  :数据长度为size个字节
	*/
    err = copy_from_user(ker_buf, buf, size);

	//如果要操作的GPIO不在规定范围内,返回错误
    if (ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]))
        return -EINVAL;

	//设置指定引脚电平
    gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]);
    return 2;    
}

(9)
<1> 最後に、ドライバがなくなったら使用しないでください。rmmod led_drv.koコマンドを使用してドライバーをアンインストールできます。
<2> この命令を呼び出すと、 module_exit() マクロに含まれる関数に入ります。
<3>このドライバーは使用しなくなったので、クラス、デバイス ノード、ドライバー、GPIO をアンロードする必要があります。

ここに画像の説明を挿入

要約する

この記事の中心となるのは 6 つの GPIO サブシステム関数であり、GPIO サブシステム関数と以前の Hello ドライバー フレームワークをマスターしていれば、この記事は難しくありません。コードを直接見ても理解できます。

おすすめ

転載: blog.csdn.net/qq_63922192/article/details/130711556