linux驅動篇-Simple-char

Simple-char

本篇是linux下一個簡單的字符設備驅動,一起來動手吧。下面的話,老鳥可以跳過了直接從《需求描述》章節看起,新手可以試着看看。

 

前言

在嵌入式行業,有很多從業者。我們工作的主旋律是拿開源代碼,拿廠家代碼,完成產品的功能,提升產品的性能,進而解決各種各樣的問題。或者是維護一個模塊或方向,一搞就是好幾年。

 

時間長了,中年潤髮現我們對從零開始編寫驅動、應用、算法、系統、協議、文件系統等缺乏經驗。沒有該有的廣度和深度。中年潤也是這樣,工作了很多年,都是針對某個問題點修修補補或者某個模塊的局部刪刪改改。很少有機會去獨自從零開始編寫一整套完整的代碼。

 

當然,這種現狀對於企業來說是比較正常的,可以降低風險。但是對於員工本身,如果缺乏必要的規劃,很容易工作多年卻還是停留在單點的層面,而喪失了提升到較高層面的機會。隨着時間的增長很容易喪失競爭力。

 

另外,根據中年潤的經驗,絕大多數公司對於0-5年經驗從業者的定位主要是積極的問題解決者。而對於5-10經驗從業者的定位主要是積極的系統規劃者和引領者。在這種行業規則下,中年潤認爲,每個從業者都應該問自己一句,“5年後,我是否具備系統化把控軟件的能力呢?”。

 

當前的這種行業現狀,如果我們不做出一點改變,是沒有辦法突破的。有些東西,僅僅知道是不夠的,還需要深思熟慮的思考和必要的訓練,簡單來說就是要知行合一。

 

也許有讀者會有疑惑?這不就是重複造輪子麼?我們確實是在重複造輪子,因爲別人會造輪子那是別人的能力,我們自己會造輪子是我們自己的能力。在行業中,有太多的定製化需求是因爲輪子本身有原生性缺陷,我們無法直接使用,或者需要對其進行改進,或者需要抽取開源代碼的主體思想和框架,根據公司的需要定製自己的各項功能。設想,如果我們具備這種能力,必然會促使我們在行業中脫穎而出,而不是工作很多年一直在底層搬磚。底層搬磚沒什麼不好,問題是當有更廉價更激情的勞動力湧進來的時候,我們這些老的搬磚民工也就失去了價值。我們不會天天重複造輪子,我們需要通過造幾個輪子使得自己具備造輪子的能力,從而更好的適應這個環境,適應這個世界。

 

針對當前行業現狀,中年潤經過深思熟慮,想爲大家做點實實在在的事情,希望能夠幫助大家在鞏固基礎的同時提升系統化把控軟件的能力。當然,中年潤的水平也有限,有些觀點也只是一家之談,希望大家獨立思考,謹慎採用,如果寫的有錯誤或者不對的地方還請讀者們批評斧正,我們一起共同進步。

 

在這裏簡單介紹下中年潤,中年潤現在就職於一家大型國際化公司,工作經驗6年,碩士畢業。曾經擔任過組內的項目主管,項目經理,也曾經組建過新團隊,帶領大家衝鋒陷陣。在工作中,有做的不錯的地方,也有失誤的地方,有激情的時刻,也有失落的時刻。現在偏安一隅,專心搞技術,目前個人規劃的技術方向是嵌入式和AI基礎設施建設,以及嵌入式和AI的融合發展。

 

最後,說了這麼多,中年潤希望,在未來的日子裏和未知的領域裏,你我同行,爲我們的美好生活而努力奮鬥。

 

總體目標

本篇文章的目標是介紹如何從自頂向下從零編寫linux下的簡單的字符設備驅動。着力從總體思路,需求端,分析端,實現端,詳盡描述一個完整需求的開發流程,是中年潤多年經驗的提煉,希望讀者能夠有所收穫。最後的實戰目標,請讀者儘量完成,這樣讀者才能形成自己的思路。

本示例採用arm920架構,天祥電子生產的tx2440a開發板,核心爲三星的s3c2440。Linux版本爲2.6.31,是已經移植好的版本。編譯器爲arm920t-eabi-4.1.2.tar。

 

總體思路

總體思路是嚴格遵循需求的開發流程來,不遺漏任何思考環節。讀者在閱讀時請先跟中年潤的思路走一遍,然後再拋棄中年潤的思路,按照自己的思路走一遍,如果遇到困難請先自己思考,實在不會再來參考中年潤的思路和實現。

 

中年潤在寫代碼的的總體思路如下:

需求描述—能夠詳細完整的描述一個需求。

需求分析—根據需求描述,提取可供實現的功能,需要有定量或者定性的指標。(從宏觀上確定需要什麼功能)。

需求分解—根據需求分析,考慮要實現需求所需要做的工作(根據宏觀確定的功能,拆分成小的可單獨實現的功能)。

編寫思路—根據需求分解從總體上描述應該如何編寫代碼,(解決怎麼在宏觀上實現)。

詳細步驟—根據編寫思路,落實具體步驟,(解決怎麼在微觀上實現)。

編寫框架—根據編寫思路,實現總體框架(實現編寫思路里主體框架,細節內容留在具體代碼裏編寫)。

具體代碼—根據編寫框架和詳細步驟,編寫每一個函數裏所需要實現的小功能,主要是實現驅動代碼,測試代碼。

Makefile—用來編譯驅動代碼。

目錄結構—用來說明當完成編碼後的結果。

測試步驟—說明如何對驅動進行測試,主要是加載驅動模塊,執行測試代碼。

執行結果—觀察執行結果是否符合預期。

結果總結—回顧本節的思路,知識點,api,結構體。

實戰目標—說明如何根據本文檔訓練。

請大家儘量按照自頂向下的學習思路來學習和實戰,因爲我們所有工作的動力都是我們心中的需求。這些步驟僅僅是我們達到目標所要走過的路。目錄起到提綱挈領的重要作用,寫的時候要實時看下提綱,看有沒有偏離自己的方向。

 

需求描述

使用linux提供的字符設備api接口,編寫一個簡單的字符設備驅動,能夠在加載和卸載模塊時打印一句話,能夠在打開和關閉該字符設備時打印一句話,能夠在讀取和寫入該設備時打印一句話。試圖給初學linux下驅動編程的人們一種直觀的感受。

 

需求分析

根據《需求描述》,從宏觀上提取可供實現的功能,我們需要做以下幾件工作。

1需要使用linux提供的字符設備api。

2需要能夠加載模塊和卸載模塊。

3需要能打開、關閉、讀、寫一個字符設備。

4需要能在上述所有動作里加一句打印語句。

 

需求分解

根據《需求分析》的結果,將宏觀確定的功能拆分成小的可單獨實現的功能,我們需要做以下幾件工作。

1需要有跟加載動作配套的函數。

2需要有跟卸載動作配套的函數。

3需要有一個配套的字符設備。

4需要有跟字符配套的打開、關閉、讀、寫函數。

5需要一個能打印語句的函數。

 

編寫思路

編寫思路主要用來搭建代碼框架,解決在宏觀上如何用代碼實現驅動的功能。

 

0搭建基礎框架

0.1編寫代碼框架,頭文件,修飾出口函數,修飾入口函數,聲明LICENSE

 

編寫跟加載動作配套的入口函數,在入口函數中所做的工作如下

1入口函數

1.1註冊一個字符設備

1.1.1定義file_operations結構體

1.1.2編寫打開,關閉,讀,寫操作函數

1.2創建一個類

1.3創建一個設備節點

 

編寫跟卸載動作配套的出口函數,在出口函數中所作的工作如下

2出口函數

2.1卸載字符設備

2.2刪除設備節點

2.3銷燬這個類

 

詳細步驟

詳細步驟主要用來在代碼框架裏填充必要的細節代碼,解決在微觀上如何用代碼實現驅動各個小功能。

 

0搭建基礎框架

0.1編寫代碼框架,頭文件,修飾出口函數,修飾入口函數,聲明LICENSE

 

編寫跟加載動作配套的入口函數,在入口函數中所做的工作如下

1入口函數

1.1註冊一個字符設備

1.1.1定義file_operations結構體

1.1.2編寫操作函數

1.2創建一個類

1.3創建一個設備節點

 

編寫跟卸載動作配套的出口函數,在出口函數中所作的工作如下

2出口函數

2.1卸載字符設備

2.2刪除設備節點

2.3銷燬這個類

 

編寫操作函數

3編寫file_operations裏的操作函數

3.1編寫打開函數

3.2編寫關閉函數

3.3編寫讀函數

3.4編寫寫函數

3.5編寫ioctl函數

 

編寫框架

根據編寫思路,實現總體框架(實現編寫思路里主體框架,細節內容留在具體代碼裏編寫)。

/* 本文件名字爲simple_char_skel.c*/
/* 本文件是依照simple_char 驅動<編寫思路>章節編寫,本文件
  * 的目的是編寫代碼框架,不做具體細節的編寫
  */
 
/* 本頭文件是linux2.6.31內核所提供的,其他版本按需調整 */
 
 
/* 0.1編寫代碼框架,頭文件,修飾出口函數,修飾入口函數,聲明LICENSE */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/irq.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>
#include <linux/device.h>
#include <linux/gpio.h>
 
#define DEVICE_NAME "simple_char"
 
static int simple_char_major = 0;
static struct class *simple_char_class;
 
static int simple_char_open(struct inode *inode, struct file *file)
{
       return 0;
}
 
static int simple_char_release(struct inode *inode, struct file *file)
{
       return 0;
}
 
static int simple_char_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
       return 0;
}
 
static int simple_char_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
       return 0;
}
 
static int simple_char_ioctl( struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
       return 0;
}
 
/* 1.1.1定義file_operations結構體 */
static struct file_operations simple_char_fops =
{
       .owner  =   THIS_MODULE,
       .open   =   simple_char_open,
       .release =  simple_char_release,
       .read   =   simple_char_read,
       .write  =   simple_char_write,
       .ioctl  =   simple_char_ioctl,
 
};
 
/* 1入口函數 */
static int __init simple_char_init(void)
{
       /* 1.1註冊一個字符設備 */
       simple_char_major = register_chrdev(0, DEVICE_NAME, &simple_char_fops);
       if (simple_char_major < 0)
       {
              printk(DEVICE_NAME " can't register major number\n");
              return simple_char_major;
       }
       /* 1.2創建一個類 */
       /* 方便在/dev 目錄下面建立設備節點*/
       simple_char_class = class_create(THIS_MODULE, DEVICE_NAME);
       if(IS_ERR(simple_char_class))
       {
              printk("class_create failed\n");
              return -1;
       }
       /* 1.3創建一個設備節點 */
       /* 節點名爲DEVICE_NAME  */
       device_create(simple_char_class, NULL, MKDEV(simple_char_major, 0), NULL, DEVICE_NAME);
       printk(DEVICE_NAME" init ok, major = %d\n",simple_char_major);
       return 0;
}
 
/* 2出口函數 */
static void __exit simple_char_exit(void)
{
       printk(DEVICE_NAME"exit ok\n");
       /* 2.1卸載字符設備 */
       unregister_chrdev(simple_char_major, DEVICE_NAME);
       /* 2.2刪除設備節點 */
       device_destroy(simple_char_class, MKDEV(simple_char_major, 0));
       /* 2.3銷燬這個類 */
       class_destroy(simple_char_class);
}
 
/* 0.1編寫代碼框架,頭文件,修飾出口函數,修飾入口函數,聲明LICENSE */
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_LICENSE("GPL");

 

驅動代碼

根據《編寫框架》《詳細步驟》章節,編寫每一個函數裏所需要實現的小功能。

/* 本文件名字爲simple_char.c*/
/* 本文件是依照simple_char 驅動<詳細步驟>章節編寫,本文件
  * 的目的是編寫具體代碼,不介紹框架
  */
 
/* 本頭文件是linux2.6.31內核所提供的,其他版本按需調整 */
 
 
/* 0.1編寫代碼框架,頭文件,修飾出口函數,修飾入口函數,聲明LICENSE */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/irq.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>
#include <linux/device.h>
#include <linux/gpio.h>
 
#define PRINTK_DEBUG 0
#define DEVICE_NAME "simple_char"
 
static int simple_char_major = 0;
static struct class *simple_char_class;
 
/* 3.1編寫打開函數 */
static int simple_char_open(struct inode *inode, struct file *file)
{
       printk("simple_char_open called\n");
       return 0;
}
 
/* 3.2編寫關閉函數 */
static int simple_char_release(struct inode *inode, struct file *file)
{
       printk("simple_char_release called\n");
       return 0;
}
 
/* 3.3編寫讀函數 */
static int simple_char_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
       printk("simple_char_read called\n");
       return 10;
}
 
/* 3.4編寫寫函數 */
static int simple_char_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
#if PRINTK_DEBUG
       msleep(1);
#endif
       printk("simple_char_write called\n");
#if PRINTK_DEBUG
       msleep(1);
#endif
       return 10;
}
 
/* 3.5編寫ioctl函數 */
static int simple_char_ioctl( struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
       printk("simple_char_ioctl called\n");
       return 0;
}
 
/* 1.1.1定義file_operations結構體 */
static struct file_operations simple_char_fops =
{
       /* 3編寫file_operations裏的操作函數 */
       .owner  =   THIS_MODULE,
       .open   =   simple_char_open,
       .release =  simple_char_release,
       .read   =   simple_char_read,
       .write  =   simple_char_write,
       .ioctl  =   simple_char_ioctl,
 
};
 
/* 1入口函數 */
static int __init simple_char_init(void)
{
       /* 1.1註冊一個字符設備 */
       simple_char_major = register_chrdev(0, DEVICE_NAME, &simple_char_fops);
       if (simple_char_major < 0)
       {
              printk(DEVICE_NAME " can't register major number\n");
              return simple_char_major;
       }
       /* 1.2創建一個類 */
       /* 方便在/dev 目錄下面建立設備節點*/
       simple_char_class = class_create(THIS_MODULE, DEVICE_NAME);
       if(IS_ERR(simple_char_class))
       {
              printk("class_create failed\n");
              return -1;
       }
       /* 1.3創建一個設備節點 */
       /* 節點名爲DEVICE_NAME  */
       device_create(simple_char_class, NULL, MKDEV(simple_char_major, 0), NULL, DEVICE_NAME);
       printk(DEVICE_NAME" init ok, major = %d\n",simple_char_major);
       return 0;
}
 
/* 2出口函數 */
static void __exit simple_char_exit(void)
{
       printk(DEVICE_NAME" exit ok\n");
       /* 2.1卸載字符設備 */
       unregister_chrdev(simple_char_major, DEVICE_NAME);
       /* 2.2刪除設備節點 */
       device_destroy(simple_char_class, MKDEV(simple_char_major, 0));
       /* 2.3銷燬這個類 */
       class_destroy(simple_char_class);
}
 
/* 0.1編寫代碼框架,頭文件,修飾出口函數,修飾入口函數,聲明LICENSE */
module_init(simple_char_init);
module_exit(simple_char_exit);
MODULE_LICENSE("GPL");

 

測試代碼

測試代碼編寫思路如下:

1打開/dev/simple_char文件

2讀文件

3寫文件

4關閉文件

 

測試代碼如下:

/* 本文件是simple_char_test.c,是根據simple-char驅動的
  * <測試代碼>章節編寫,主要任務是用來測試
  * simple_char 驅動
  */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
 
#define DELAY_1000_US     0
 
int main(int argc, char **argv)
{
       int fd = -1;
       int ret = -1;
       char buf[10] = {0};
      
       fd = open("/dev/simple_char", O_RDWR,666);
       if (fd < 0) {
              printf("open device,%d\n",__LINE__);
              goto errout;
       }
      
       ret = read(fd,buf,sizeof(buf));
       if (ret == 10) {
              printf("read succeed \n");
#if DELAY_1000_US
              usleep(1000);
#endif
       } else {
              printf("read device failed at %d\n",__LINE__);
              goto errout;
       }
      
       ret = write(fd,buf,sizeof(buf));
       if (ret == 10) {
              printf("write succeed \n");
#if DELAY_1000_US
              usleep(1000);
#endif
       } else {
              printf("write device failed at %d\n",__LINE__);
              goto errout;
       }
      
       ret = close(fd);
       if (ret != 0) {
              printf("close device failed at %d\n",__LINE__);
              goto errout;
       }
       return 0;
 
errout:
       return -1;
}

 

 

Makefile

KERN_DIR = /home/linux/tools/linux-2.6.31_TX2440A
all:
       make -C $(KERN_DIR) M=`pwd` modules
clean:
       make -C $(KERN_DIR) M=`pwd` modules clean
       rm -rf modules.order
obj-m += simple_char.o

 

目錄結構

代碼編寫完成的目錄結構如下所示。直接執行make即可生成.ko文件。

.
├── Makefile(用來編譯驅動代碼)
├── simple_char.c(驅動代碼)
├── simple_char_skel.c(驅動框架代碼)
├── simple_char_test(測試驅動的可執行程序)
└── simple_char_test.c(驅動測試代碼)

 

測試步驟

0 在linux下的makefile +180行處配置好arch爲arm,cross_compile爲arm-linux-(或者arm-angstrom-linux-gnueabi-)

1 在menuconfig中配置好內核源碼的目標系統爲s3c2440

2 在pc上將驅動程序編譯生成.ko,命令:make

3 在pc上將測試程序編譯生成elf可執行文件,

編譯

arm-angstrom-linux-gnueabi-gcc -o simple_char_test simple_char_test.c

如果無法執行,修改可執行文件權限

chmod 777 simple_char_test

4 掛載nfs,這樣就可以在開發板上看到pc端的.ko文件和測試文件

mount -t nfs -o nolock,vers=2 192.168.0.105:/home/linux/nfs_root  /mnt/nfs

5 insmod simple_char.ko,觀察現象

6執行./simple_char_test,觀察現象

7打開或者關閉驅動代碼和測試代碼中的宏開關,觀察現象

 

執行結果

加載模塊的時候打印如下語句,這句話是在驅動的init函數中的。

[root@TX2440A 00simple-char]# insmod simple_char.ko

simple_char init ok, major = 253

加載模塊後會發現在/dev/下面有一個simple_char字符設備

[root@TX2440A 00simple-char]# ls /dev/simple_char

/dev/simple_char

 

關閉延時開關DELAY_1000_US後的log如下

[root@TX2440A 00simple-char]# ./simple_char_test

simple_char_open called

simple_char_read called

read succeed simple_char_write called

simple_char_release called

 

write succeed

可以發現調用流程跟simple_char_test.c中的使用流程相同,都是open—read—write—close。只是在驅動中打印的語句和應用層打印的語句的順序有些異常。

(異常流程)驅動層open—驅動層read—應用層read成功—驅動層write—驅動層關閉—應用層write成功

(正常流程)驅動層open—驅動層read—應用層read成功—驅動層write—應用層write成功—驅動層關閉

 

打開延時開關DELAY_1000_US後的log如下

[root@TX2440A 00simple-char]# ./simple_char_test

simple_char_open called

simple_char_read called

read succeed

simple_char_write called

write succeed

simple_char_release called

可以發現調用流程跟simple_char_test.c中的使用流程相同,都是open—read—write—close,而且是正常流程。

(正常流程)驅動層open—驅動層read—應用層read成功—驅動層write—應用層write成功—驅動層關閉

 

問題:爲什麼會出現異常流程呢?

請看《問題彙總》章節

 

卸載模塊時打印如下所示,這句話是在驅動的exit函數中的。

[root@TX2440A 00simple-char]# rmmod simple_char

simple_char exit ok

 

結果總結

在本篇文章中,中年潤跟讀者分享了簡單的字符設備驅動的編寫思路和方法,其中貫穿始終的有幾個函數和關鍵數據結構,它們分別是:

struct file_operations

register_chrdev

class_create

device_create

unregister_chrdev

device_destroy

class_destroy

 

請讀者盡力去了解這些函數的作用,入參,返回值。

 

問題彙總

問題:爲什麼會出現異常流程呢?

(異常流程)驅動層open—驅動層read—應用層read成功—驅動層write—驅動層關閉—應用層write成功

(正常流程)驅動層open—驅動層read—應用層read成功—驅動層write—應用層write成功—驅動層關閉

 

這個測試代碼中年潤執行了很多遍,依然是如《執行結果》章節一樣,爲什麼我們先write,而應用層的write反而在驅動層關閉函數之後被打印出來呢?首先我們要回答幾個問題。

1任何打印都需要緩衝區和串口硬件的參與,都需要時間

2如果在程序執行過程中,應用層已經將打印的內容放入緩衝區,等待被打印到硬件,此時printf函數已經返回,又執行到了驅動裏的printk,那麼是先打印應用層的數據還是先打印驅動中的數據呢?

3中年潤嘗試了幾種測試方法來說明這個問題

3.1嘗試只在simple_char_test.c應用層代碼里加延時,對應DELAY_1000_US開關

加上延時後相當於讓應用層打印的速度不那麼快,不那麼快的進入下一個動作,讓硬件有充分的時間反應。打印結果如下所示

[root@TX2440A 00simple-char]# ./simple_char_test

simple_char_open called

simple_char_read called

read succeed

simple_char_write called

write succeed

simple_char_release called

流程變得正常了

 

3.2嘗試只在simple_char.c驅動層代碼裏的write函數里加延時(對應PRINTK_DEBUG開關),

加上延時後相當於讓驅動的打印不那麼快的返回,打印結果如下所示:

[root@TX2440A 00simple-char]# ./simple_char_test

simple_char_open called

simple_char_read called

read succeed

simple_char_write called

write succeed

simple_char_release called

 

綜上所述只要一方等另一方完全輸出,那麼數據的打印流程就是對的。也說明了以下幾點:

1 linux2.6.31的內核只負責將緩衝區的數據輸出而不管其順序如何,且因爲只有一個串口硬件,所以當都要輸出的時候,應該是遵循誰先到誰輸出。

2所以,如果printf("write succeed \n");中的字符在printk("simple_char_write called\n");這句話之後到達硬件,則應該按照到達的先後順序輸出。

3因爲本篇文章的主要目的是講解linux下的簡單字符設備驅動,因此,上述問題在這裏不過分深究。關鍵還是中年潤精力有限。上述問題中年潤暫且記下,等到中年潤寫uart驅動的時候我們回過頭來再詳細分析。到那個時候,中年潤勢必要把printf、printk,linux下的tty、線路規程、串口驅動、uart驅動徹底給大家分析透徹。

 

實戰目標

1請讀者根據《需求描述》章節,獨立編寫需求分析和需求分解。

2請讀者根據需求分析和需求分解,獨立編寫編寫思路和詳細步驟。

3請讀者根據編寫思路,獨立寫出編寫框架。

4請讀者根據詳細步驟,獨立編寫驅動代碼和測試代碼。

5請讀者根據《Makefile》章節,獨立編寫Makefile。

6請讀者根據《測試步驟》章節,獨立進行測試。

7請讀者拋開上述練習,自頂向下從零開始再編寫一遍驅動代碼,測試代碼,makefile

8如果無法獨立寫出7,請重複練習1-6,直到能獨立寫出7。

 

參考資料

《linux設備驅動開發祥解》

《TX2440開發手冊及代碼》

《韋東山嵌入式教程》

《魚樹驅動筆記》

《s3c2440a》芯片手冊英文版和中文版

 

致謝

感謝在嵌入式領域深耕多年的前輩,感謝中年潤的家人,感謝讀者。沒有前輩們的開拓,我輩也不能站在巨人的肩膀上看世界;沒有家人的鼎力支持,我們也不能集中精力完成自己的工作;沒有讀者的關注和支持,我們也沒有充足的動力來編寫和完善文章。看完中年潤的文章,希望讀者學到的不僅僅是如何編寫代碼,更進一步能夠學到一種思路和一種方法。

 

爲了省去驅動開發者蒐集各種資料來寫驅動,中年潤後續有計劃按照本模板編寫linux下的常見驅動,敬請讀者關注。

 

聯繫方式

微信羣:見文章最底部,因微信羣有效期只有7天,感興趣的同學可以加下。微信羣裏主要是爲初學者答疑解惑,也可以進行技術和非技術的交流。如果微信羣失效了,大家可以加qq羣,我會在qq羣裏分享微信羣的二維碼。同時也歡迎和中年潤志同道合的中年人尤其是中年碼農的加入。

微信訂閱號:自頂向下學嵌入式

公衆號微信:EmbeddedAIOT

CSDN博客:chichi123137

CSDN博客網址:https://blog.csdn.net/chichi123137

QQ郵箱:[email protected]

QQ羣:766756075

更多原創文章請關注微信公衆號。另外,中年潤還代理銷售韋東山老師的視頻教程,歡迎讀者諮詢。在中年潤這裏購買了韋東山老師的視頻教程,除了能得到韋東山官方的技術支持外,還能獲得中年潤細緻入微的技術和非技術的支持和幫助。歡迎大家選購哦。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章