本篇是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接口,編寫一個按鍵字符設備驅動和一個測試代碼,能夠在輸入./button命令後,按下按鍵時,在串口輸出按下了哪個按鍵。同時用戶不希望用戶線程佔用太多的cpu資源。
需求分析
對於按鍵字符設備驅動來說,測試代碼就是我們的用戶,因此我們可以通過分析測試代碼的邏輯來分析我們的需求。
首先梳理用戶的工作流程,用戶的工作流程如下:
1用戶調用open系統調用打開/dev/mybutton設備節點,獲取fd
2用戶拿到fd之後,調用read系統調用讀取fd中的值
3如果沒有數據,希望用戶進程睡眠,不希望大量佔用cpu資源
4如果有數據,直接返回數據
5如果按下或鬆開按鍵,在中斷中保存當前的數據,並喚醒用戶線程,用戶線程直接拿數據。
根據用戶的工作流程,我們可以梳理出驅動所要做的工作,下圖說明了驅動所要做的主體,跟用戶層是一一對應的。
梳理完用戶工作流程後,我們再來看下用戶進程和中斷之間的交互,這樣對於爲什麼編寫某些函數會有直觀的感受。
需求分解
根據《需求分析》和用戶的操作,我們需要做以下幾件工作。
1用戶調用open系統調用打開/dev/mybutton設備節點,獲取fd
1.3 需要提供一個設備節點/dv/mybutton,
1.4 需要提供一個操作設備節點的open函數
2用戶拿到fd之後,調用read系統調用讀取fd中的值
2.1 需要提供操作設備節點的read函數,用來將讀取的數據返回
3如果沒有數據,希望用戶進程睡眠,不希望大量佔用cpu資源
3.1 需要能判斷當前有無數據,需要在無數據時進入休眠狀態
4如果有數據,直接返回數據
4.1 需要能判斷當前有無數據,需要在有數據時能立即返回數據,並標記當前數據狀態爲無
4.2 需要一個有無數據的標誌,在產生數據時標記有,在讀取完畢後標記無
5如果按下或鬆開按鍵,在中斷中保存當前的數據,並喚醒用戶線程,用戶線程直接拿數據。
5.1 需要一箇中斷處理函數
5.2 需要能夠檢測當前按下的是哪個按鍵
5.3 需要在中斷中保存數據,並標記當前數據狀態爲有
5.4 需要喚醒用戶進程
5.5 在中斷函數中保存數據後,read函數能夠直接操作被保存的數據並返回給用戶
用一張圖來總結上述驅動所要做的工作,如下圖所示:
編寫思路
編寫思路主要用來搭建代碼框架,解決在宏觀上如何用代碼實現驅動的功能。
確定目標:實現需求分解中的所有函數
Init函數,exit函數,open函數,read函,close函數,中斷處理函數(在詳細步驟中編寫)
確定基本思路:
0搭建基礎框架
0.1編寫代碼框架,頭文件
0.2編寫空的出口函數
0.3編寫空的入口函數
0.4修飾出口函數,修飾入口函數,聲明LICENSE
在入口函數中所做的工作如下
1入口函數
1.1註冊一個字符設備,名字爲s3c_button
1.1.1定義file_operations結構體
1.1.2編寫空的打開函數,關閉函數,讀函數
1.2創建一個類,名字爲button-int,(決定了/sys/class下目錄的名字)
1.3創建一個設備節點,名字爲mybutton(決定了/dev下文件的名字)
1.4映射gpio的物理地址爲虛擬地址,一個是控制寄存器地址,一個是數據寄存器地址
在出口函數中所作的工作如下
2出口函數
2.1卸載字符設備
2.2刪除設備節點
2.3銷燬這個類
2.4取消地址映射
將上述步驟編寫完成就得到了《編寫框架》章節中的代碼
詳細步驟
詳細步驟主要用來在代碼框架裏填充必要的細節代碼,解決在微觀上如何用代碼實現驅動各個小功能。
0搭建基礎框架
0.1編寫代碼框架,頭文件
0.2編寫空的出口函數
0.3編寫空的入口函數
0.4修飾出口函數,修飾入口函數,聲明LICENSE
在入口函數中所做的工作如下
1入口函數
1.1註冊一個字符設備,名字爲s3c_button
1.1.1定義file_operations結構體
1.1.2編寫空的打開函數,關閉函數,讀函數
1.2創建一個類,名字爲button-int,(決定了/sys/class下目錄的名字)
1.3創建一個設備節點,名字爲mybutton(決定了/dev下文件的名字)
1.4映射gpio的物理地址爲虛擬地址,一個是控制寄存器地址,一個是數據寄存器地址
在出口函數中所作的工作如下
2出口函數
2.1卸載字符設備
2.2刪除設備節點
2.3銷燬這個類
2.4取消地址映射
3編寫file_operations裏的操作函數
3.1編寫打開函數,註冊中斷和中斷處理函數
3.2編寫關閉函數,釋放中斷
3.3編寫讀函數,讀取按下的鍵值
3.4編寫中斷處理函數,判斷當前是哪個按鍵按下,以及當前是按下還是鬆開
所有函數的基本流程圖如下圖示所示:
編寫框架
根據編寫思路,實現總體框架(實現編寫思路里主體框架,細節內容留在具體代碼裏編寫)。
/* 本文件名字爲button_drv_int_skel.c*/
/* 本文件是依照button-中斷法驅動<編寫思路>章節編寫,本文件
* 的目的是編寫代碼框架,不做具體細節的編寫
*/
/* 0.1編寫代碼框架,頭文件 */
#include <linux/module.h>
#include <linux/ioport.h>
#include <linux/io.h>
#include <linux/platform_device.h>
#include <linux/init.h>
#include <linux/serial_core.h>
#include <linux/serial.h>
#include <linux/irq.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <mach/hardware.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-fns.h>
#include <plat/regs-serial.h>
volatile unsigned long * gpfconf;
volatile unsigned long * gpfdat;
static struct class *my_button_cls;
static struct device * my_button_dev;
static int major;
/* 1.1.2編寫空的打開函數 */
static int button_open (struct inode * inode, struct file * filep)
{
return 0;
}
/* 1.1.2編寫空的關閉函數 */
static int button_release (struct inode * inode, struct file * filep)
{
return 0;
}
/* 1.1.2編寫空的讀函數 */
static ssize_t button_read (struct file * filep, char __user * buff, size_t size, loff_t * pos)
{
return 0;
}
/* 1.1.1定義file_operations結構體 */
static struct file_operations button_fops = {
.owner = THIS_MODULE,
.open = button_open,
.read = button_read,
.release = button_release,
};
/* 0.3編寫空的入口函數 */
static int my_button_init(void)
{
int ret = 0;
/* 1.1註冊一個字符設備,名字爲s3c_button */
major = register_chrdev(0,"s3c_button",&button_fops);
/* 1.2創建一個類,名字爲button-int,(決定了/sys/class下目錄的名字) */
my_button_cls = class_create(THIS_MODULE,"button-int");
/* 1.3創建一個設備節點,名字爲mybutton(決定了/dev下文件的名字) */
my_button_dev = device_create(my_button_cls,NULL,MKDEV(major,0),NULL,"mybutton");
/* 1.4映射gpio的物理地址爲虛擬地址,一個是控制寄存器地址,一個是數據寄存器地址 */
gpfconf = (volatile unsigned long *)ioremap(0x56000050,16);
//獲得數據寄存器地址
gpfdat = gpfconf + 1;
return ret;
}
/* 0.2編寫空的出口函數 */
static void my_button_exit(void)
{
/* 2.1卸載字符設備 */
unregister_chrdev(major," s3c_button");
/* 2.2刪除設備節點 */
device_unregister(my_button_dev);
/* 2.3銷燬這個類 */
class_destroy(my_button_cls);
/* 2.4取消地址映射 */
iounmap(gpfconf);
return;
}
/* 0.4修飾出口函數,修飾入口函數,聲明LICENSE */
module_init(my_button_init);
module_exit(my_button_exit);
MODULE_LICENSE("GPL");
驅動代碼
根據《編寫框架》《詳細步驟》章節,編寫每一個函數裏所需要實現的小功能。
/* 本文件名字爲button_drv_int.c*/
/* 本文件是依照button-中斷法驅動<詳細步驟>章節編寫,本文件
* 的目的是編寫具體操作函數,不做框架介紹
*/
/* 0.1編寫代碼框架,頭文件 */
#include <linux/module.h>
#include <linux/ioport.h>
#include <linux/io.h>
#include <linux/platform_device.h>
#include <linux/init.h>
#include <linux/serial_core.h>
#include <linux/serial.h>
#include <linux/irq.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <mach/hardware.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-fns.h>
#include <plat/regs-serial.h>
volatile unsigned long * gpfconf;
volatile unsigned long * gpfdat;
static struct class *my_button_cls;
static struct device * my_button_dev;
static int major;
DECLARE_WAIT_QUEUE_HEAD(button_waitq);
//引腳描述符
struct pin_desc{
int pin;//代表是哪個引腳
int key_val;//代表是按下還是鬆開
};
//引腳描述符,按引腳的不同定義不同的值,在中斷中進行處理
struct pin_desc pin_desc[4] =
{
{S3C2410_GPF4_EINT4,0x1},
{S3C2410_GPF5_EINT5,0x2},
{S3C2410_GPF6_EINT6,0x3},
{S3C2410_GPF7_EINT7,0x4},
};
//標誌是否進入中斷
static int do_press_loos;
/* 鍵值: 按下時, 0x01, 0x02, 0x03, 0x04 */
/* 鍵值: 鬆開時, 0x81, 0x82, 0x83, 0x84 */
/* 主要是爲了區分是按下還是鬆開 */
static unsigned char key_val;
irqreturn_t button_irq(int irq, void * devid)
{
int pin_val;
struct pin_desc * pin_readed;
/*
* 1 獲取當前引腳的電平值
* 2 根據電平值判斷當前是按下還是鬆開
* 鬆開爲高電平,返回0x8x
* 按下爲低電平,返回0x0x
* 3 標記已有數據
* 4 喚醒處在內核態的用戶進程
*/
pin_readed = (struct pin_desc *)devid;
//獲取某個引腳是高還是低
pin_val = s3c2410_gpio_getpin(pin_readed->pin);
if (pin_val) //鬆開是高電平
{
key_val = 0x80 | pin_readed->key_val;
}
else //按下爲低電平
{
//把當前按鍵的鍵值給一個全局靜態變量,在read函數裏給用戶
key_val = pin_readed->key_val;
}
//標記中斷已經觸發
do_press_loos = 1;
//喚醒用戶的讀進程
wake_up_interruptible(&button_waitq);
return IRQ_RETVAL(IRQ_HANDLED);
}
/* 1.1.2編寫空的打開函數 */
static int button_open (struct inode * inode, struct file * filep)
{
/* 3.1編寫打開函數,註冊中斷和中斷處理函數 */
//request_irq裏已經幫忙將GPF4-GPF7設置成中斷引腳了
int ret;
/* 註冊外部中斷4,類型爲下降沿中斷,名字爲S1,
中斷處理函數爲button_irq
傳入中斷處理函數中的參數爲pin_desc[0] */
ret = request_irq(IRQ_EINT4,button_irq,IRQ_TYPE_EDGE_FALLING,"S1",
(void *)&pin_desc[0]);
if (ret)
goto errout;
ret = request_irq(IRQ_EINT5,button_irq,IRQ_TYPE_EDGE_FALLING,"S2",
(void *)&pin_desc[1]);
if (ret)
goto errout;
ret = request_irq(IRQ_EINT6,button_irq,IRQ_TYPE_EDGE_FALLING,"S3",
(void *)&pin_desc[2]);
if (ret)
goto errout;
ret = request_irq(IRQ_EINT7,button_irq,IRQ_TYPE_EDGE_FALLING,"S4",
(void *)&pin_desc[3]);
if (ret)
goto errout;
return 0;
errout:
return ret;
}
/* 1.1.2編寫空的關閉函數 */
static int button_release (struct inode * inode, struct file * filep)
{
/* 3.2編寫關閉函數,釋放中斷 */
free_irq(IRQ_EINT4, &pin_desc[0]);
free_irq(IRQ_EINT5, &pin_desc[1]);
free_irq(IRQ_EINT6, &pin_desc[2]);
free_irq(IRQ_EINT7, &pin_desc[3]);
return 0;
}
/* 1.1.2編寫空的讀函數 */
static ssize_t button_read (struct file * filep, char __user * buff, size_t size, loff_t * pos)
{
int ret = 0;
/* 3.3編寫讀函數,讀取按下的鍵值,以及當前是按下還是鬆開 */
if (size != 1)
return -EINVAL;
//根據do_press_loos判斷,如果沒有中斷,直接進入休眠
wait_event_interruptible(button_waitq, do_press_loos);
//如果被喚醒拷貝數據到用戶空間
ret = copy_to_user(buff, &key_val, sizeof(key_val));
if (ret) {
return -EFAULT;
}
//讀取數據完畢後需要將標誌位清零,表示暫時無數據可讀
do_press_loos = 0;
return 1;
}
/* 1.1.1定義file_operations結構體 */
static struct file_operations button_fops = {
.owner = THIS_MODULE,
.open = button_open,
.read = button_read,
.release = button_release,
};
/* 0.3編寫空的入口函數 */
static int my_button_init(void)
{
int ret = 0;
/* 1.1註冊一個字符設備,名字爲s3c_button */
major = register_chrdev(0,"s3c_button",&button_fops);
/* 1.2創建一個類,名字爲button-int,(決定了/sys/class下目錄的名字) */
my_button_cls = class_create(THIS_MODULE,"button-int");
/* 1.3創建一個設備節點,名字爲mybutton(決定了/dev下文件的名字) */
my_button_dev = device_create(my_button_cls,NULL,MKDEV(major,0),NULL,"mybutton");
/* 1.4映射gpio的物理地址爲虛擬地址,一個是控制寄存器地址,一個是數據寄存器地址 */
gpfconf = (volatile unsigned long *)ioremap(0x56000050,16);
//獲得數據寄存器地址
gpfdat = gpfconf + 1;
return ret;
}
/* 0.2編寫空的出口函數 */
static void my_button_exit(void)
{
/* 2.1卸載字符設備 */
unregister_chrdev(major," s3c_button");
/* 2.2刪除設備節點 */
device_unregister(my_button_dev);
/* 2.3銷燬這個類 */
class_destroy(my_button_cls);
/* 2.4取消地址映射 */
iounmap(gpfconf);
return;
}
/* 0.4修飾出口函數,修飾入口函數,聲明LICENSE */
module_init(my_button_init);
module_exit(my_button_exit);
MODULE_LICENSE("GPL");
測試代碼
測試代碼編寫思路如下:
1打開/dev/mybutton文件
2讀取獲得的值
3打印獲得的值
測試代碼如下:
/* 本文件是button_test.c,是根據button-中斷法驅動的
* <測試代碼>章節編寫,主要任務是用來測試
* button 驅動
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
/* button 驅動,中斷法實驗
*/
int main(int argc, char **argv)
{
int fd;
unsigned char key_val;
int cnt;
fd = open("/dev/mybutton", O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
}
while (1)
{
read(fd, &key_val, 1);
printf("key_val = 0x%x\n", key_val);
}
return 0;
}
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 += button_drv_int.o
目錄結構
代碼編寫完成的目錄結構如下所示。直接執行make即可生成.ko文件。
.
├── button_drv_int.c(驅動代碼)
├── button_drv_int_skel.c(驅動框架代碼)
├── button_test.c(驅動測試代碼)
└── Makefile(用來編譯驅動代碼)
測試步驟
0 在linux下的makefile +180行處配置好arch爲arm,cross_compile爲arm-linux-(或者arm-angstrom-linux-gnueabi-)
1 在menuconfig中配置好內核源碼的目標系統爲s3c2440
2 在pc上將驅動程序編譯生成.ko,命令:make
3 在pc上將測試程序編譯生成elf可執行文件,生成的button就是我們所要使用的命令。
編譯:arm-angstrom-linux-gnueabi-gcc button_test.c -o button_test
4 掛載nfs,這樣就可以在開發板上看到pc端的.ko文件和測試文件
mount -t nfs -o nolock,vers=2 192.168.0.105:/home/linux/nfs_root /mnt/nfs
5 insmod button_drv.ko,加載按鍵的驅動模塊
6執行命令./button,依次按下四個按鍵,觀察串口的打印信息。
執行結果
執行完./button後,依次按下四個按鍵,串口的日誌如下,按下一個按鍵一次,會出現很多次打印。
[root@TX2440A 4-button-int]# insmod button_drv_int.ko
[root@TX2440A 4-button-int]# lsmod
button_drv_int 3356 0 - Live 0xbf000000
[root@TX2440A 4-button-int]# ./button_test
key_val = 0x4
key_val = 0x4
key_val = 0x3
key_val = 0x2
key_val = 0x2
key_val = 0x2
key_val = 0x2
key_val = 0x2
key_val = 0x2
key_val = 0x1
key_val = 0x1
key_val = 0x1
上述log表明,當前的驅動有按鍵抖動的現象,還需要繼續優化。
結果總結
在本篇文章中,中年潤跟讀者分享了按鍵字符設備驅動的編寫思路和方法,其中貫穿始終的有幾個函數和關鍵數據結構,它們分別是:
struct file_operations
struct class
struct class_device
register_chrdev
class_create
device_create
unregister_chrdev
device_destroy
class_destroy
ioremap
iounmap
DECLARE_WAIT_QUEUE_HEAD
request_irq
free_irq
請讀者盡力去了解這些函數的作用,入參,返回值。
問題彙總
暫無
實戰目標
1請讀者根據《需求描述》章節,獨立編寫需求分析和需求分解。
2請讀者根據需求分析和需求分解,獨立編寫編寫思路和詳細步驟。
3請讀者根據編寫思路,獨立寫出編寫框架。
4請讀者根據詳細步驟,獨立編寫驅動代碼和測試代碼。
5請讀者根據《Makefile》章節,獨立編寫Makefile。
6請讀者根據《測試步驟》章節,獨立進行測試。
7請讀者拋開上述練習,自頂向下從零開始再編寫一遍驅動代碼,測試代碼,makefile
8如果無法獨立寫出7,請重複練習1-6,直到能獨立寫出7。
參考資料
《linux設備驅動開發祥解》
《TX2440開發手冊及代碼》
《韋東山嵌入式教程》
《魚樹驅動筆記》
《s3c2440a》芯片手冊英文版和中文版
致謝
感謝在嵌入式領域深耕多年的前輩,感謝中年潤的家人,感謝讀者。沒有前輩們的開拓,我輩也不能站在巨人的肩膀上看世界;沒有家人的鼎力支持,我們也不能集中精力完成自己的工作;沒有讀者的關注和支持,我們也沒有充足的動力來編寫和完善文章。看完中年潤的文章,希望讀者學到的不僅僅是如何編寫代碼,更進一步能夠學到一種思路和一種方法。
爲了省去驅動開發者蒐集各種資料來寫驅動,中年潤後續有計劃按照本模板編寫linux下的常見驅動,敬請讀者關注。
聯繫方式
微信羣:自頂向下學嵌入式(可先加微信號:runzhiqingqing, 通過後會邀請入羣。)
微信訂閱號:自頂向下學嵌入式 公衆號:EmbeddedAIOT
CSDN博客:中年潤 網址:https://blog.csdn.net/chichi123137
QQ羣:766756075
更多原創文章請關注微信公衆號。另外,中年潤還代理銷售韋東山老師的視頻教程,歡迎讀者點擊下面二維碼購買。在中年潤這裏購買了韋東山老師的視頻教程,除了能得到韋東山官方的技術支持外,還能獲得中年潤細緻入微的技術和非技術的支持和幫助。歡迎選購哦。
公衆號二維碼如下圖
入羣小助手二維碼如下圖
中年潤代理銷售的韋東山視頻購買地址如下圖
如果略有所獲,歡迎讚賞,您的支持對中年潤無比重要