一、數據傳輸
1.1 APP和驅動?
APP和驅動之間的數據訪問是不能通過直接訪問對方的內存地址來操作的,這里涉及Linux系統中的MMU(內存管理單元)。在驅動程序中通過這兩個函數來獲得APP和傳給APP數據:
- copy_to_user
- copy_from_user
簡單來講,應用程序與內核/驅動程序在物理空間上是隔離開的,應用程序和驅動程序是不可能互相訪問到的。驅動程序里的copy_from_user得到應用層傳來的數據,驅動程序可以使用copy_to_user把數據發給應用程序,即應用程序和驅動程序通過這兩個函數交換數據。
1.2 驅動和硬件
- 各個子系統函數
- 通過ioremap映射寄存器地址后,直接訪問寄存器
驅動程序操作硬件可以通過子系統的方式(調用函數)來操作硬件;或者用最原始的辦法ioremap,映射寄存器的地址(不是直接操作寄存器地址),這樣在驅動程序里就可以訪問寄存器了。
二、APP使用驅動的4種方式
驅動程序:提供能力,不提供策略(驅動程序提供各種作用的函數,供應用程序抉擇并使用)。
2.1 非阻塞(查詢)
如果在應用程序里open這個argv[1](設備節點)時,指定了非阻塞,表示讀數據時,如果沒有數據并且這個文件的flag是非阻塞,則立刻返回一個錯誤。APP指定了非阻塞方式,驅動程序是否判斷它的flag完全由用戶決定。
//應用程序
//O_RDWR可讀可寫,O_NONBLOCK非阻塞方式
fd = open(argv[1], O_RDWR | O_NONBLOCK);
//驅動程序的read
static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{int err;int key;if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))return -EAGAIN;wait_event_interruptible(gpio_wait, !is_key_buf_empty());key = get_key();err = copy_to_user(buf, &key, 4); return 4;
}
2.2 阻塞(休眠+喚醒)
如果一開始buf里沒有數據,APP調用讀函數,驅動程序讀函數會進入wait_event_interruptible里休眠(放棄運行,不是死等),等待被喚醒。所以我們經常看到read函數很久沒有返回,是因為在驅動程序里休眠了。該事件會記錄在gpio_wait隊列中。
//應用程序
//O_RDWR可讀可寫,不設置非阻塞
fd = open(argv[1], O_RDWR);
//驅動程序的read
static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{int err;int key;if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))return -EAGAIN;wait_event_interruptible(gpio_wait, !is_key_buf_empty());key = get_key();err = copy_to_user(buf, &key, 4); return 4;
}
通常配合中斷+定時器的方式來喚醒該隊列里面等待喚醒的進程/線程。
//中斷函數
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{struct gpio_desc *gpio_desc = dev_id;printk("gpio_key_isr key %d irq happened\n", gpio_desc->gpio);//定時器 用來消除抖動//修改定時器的超時時間= jiffies(當前時間) + 赫茲/5mod_timer(&gpio_desc->key_timer, jiffies + HZ/5);return IRQ_HANDLED;//成功處理
}
//定時器超時函數
static void key_timer_expire(unsigned long data)
{struct gpio_desc *gpio_desc = (struct gpio_desc *)data;int val;int key;val = gpio_get_value(gpio_desc->gpio);key = (gpio_desc->key) | (val<<8);put_key(key);//按鍵值放入環形緩沖區//喚醒隊列中的進程/線程wake_up_interruptible(&gpio_wait);kill_fasync(&button_fasync, SIGIO, POLL_IN);
}
2.3 POLL(休眠+喚醒+超時時間)
2.3.1?POLL機制流程
使用休眠-喚醒的方式等待某個事件發生時,有一個缺點: 等待的時間可能很久。我們可以加上一個超時時間,這時就可以使用 poll 機制。poll機制流程如下6步:
①APP不知道驅動程序中是否有數據,可以先調用poll函數查詢一下,poll函數可以傳入超時時間;
②APP進入內核態,調用到驅動程序的poll函數,如果有數據的話立刻返回;
③如果發現沒有數據時就休眠一段時間;
④當有數據時,比如當按下按鍵時,驅動程序的中斷服務程序和定時器超時函數被調用,它會記錄數據、喚醒APP;
⑤當超時時間到了之后,內核也會喚醒APP;
⑥APP根據poll函數的返回值就可以知道是否有數據,如果有數據就調用read得到數據。
2.3.2?POLL執行流程
?????????????????????????????????????????????????????????????????圖1 poll機制
函數執行流程如上圖①~⑧所示,重點從③開始看。假設一開始無按鍵數據:
③APP調用poll之后,進入內核態;
④在循環中執行程序,致驅動程序的drv_poll被調用;注意,drv_poll要把自己這個線程掛入等待隊列 wq 中!,并沒有休眠,且無數據返回0,有數據返回POLLIN;
⑤當前沒有數據,則在內核態中休眠一會,等待超時內核喚醒或中斷+定時器喚醒;
中斷+定時器喚醒情況:
⑥過程中,按下了按鍵,發生了中斷+定時器超時函數,在定時器超時函數里記錄了按鍵值,并且從gpio_wait隊列中把線程喚醒了;
⑦從休眠中被喚醒,繼續執行 for 循環,再次調用 drv_poll,在drv_poll中返回數據狀態(POLLIN);
⑧有數據返回到內核態,內核態返回到應用態;
⑨APP調用read函數讀數據。
超時內核喚醒情況:接著上面的⑤
⑥在休眠過程中,一直沒有按下了按鍵,超時時間到,內核把這個線程喚醒;
⑦線程從休眠中被喚醒,繼續執行 for 循環,再次調用 drv_poll,drv_poll返回數據狀態
⑧還是沒有數據,但是超時時間到了,那從內核態返回到應用態;
⑨APP不能調用 read 函數讀數據。
需要注意一下幾點!!!
- drv_poll 要把線程掛入隊列gpio_wait,但是并不是在 drv_poll 中進入休眠,而是在調用 drv_poll 之后休眠
- drv_poll 要返回數據狀態
- APP 調用一次 poll,有可能會導致 drv_poll 被調用 2 次
- 線程被喚醒的原因有 2個:中斷(+定時器)發生了去隊列gpio_wait中把它喚醒,超時時間到了內核把它喚醒
- APP 要判斷 poll 返回的原因:有數據,還是超時。有數據時再去調用read函數
2.3.3?POLL應用和驅動編程?
驅動程序中的poll代碼
static unsigned int gpio_drv_poll(struct file *fp, poll_table * wait)
{poll_wait(fp, &gpio_wait, wait);return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
?驅動程序中的中斷觸發函數:按鍵消抖+修改了定時器超時時間
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{struct gpio_desc *gpio_desc = dev_id;printk("gpio_key_isr key %d irq happened\n", gpio_desc->gpio);//定時器 用來消除抖動mod_timer(&gpio_desc->key_timer, jiffies + HZ/5);//修改定時器的超時時間= jiffies(當前時間) + 赫茲/5return IRQ_HANDLED;//成功處理
}
?定時器超時函數:獲取按鍵值+儲存按鍵值+喚醒線程
static void key_timer_expire(unsigned long data)
{struct gpio_desc *gpio_desc = (struct gpio_desc *)data;int val;int key;val = gpio_get_value(gpio_desc->gpio);key = (gpio_desc->key) | (val<<8);put_key(key);//按鍵值放入環形緩沖區wake_up_interruptible(&gpio_wait);//喚醒隊列里的線程kill_fasync(&button_fasync, SIGIO, POLL_IN);
}
應用程序代碼:
struct pollfd fds[1]
int timeout_ms = 5000;
int ret;
int fd;
fds[0].fd = fd; //查詢fd這個文件
fds[0].events = POLLIN; //POLLIN表示查詢這個文件有沒有數據讓我讀進來 fd = open(argv[1], O_RDWR);
if(fd == -1)
{printf("can not open file %s\n", argv[1]);
}while(1)
{ret = poll(fds, 1, timeout_ms);//ret為1表示fds結構體中有文件滿足返回條件,且返回的事件是這個文件有數據讓我讀進來POLLINif((ret == 1) && (fds[0].revents & POLLIN)){read(fd, &val, 4);printf("get button : 0x%x\n", val);}else{printf("timeout\n");}}
?
2.4 異步通知
2.4.1 異步通知流程
使用休眠-喚醒、POLL機制時,都需要休眠等待某個事件發生時,它們的差別在于后者可以指定休眠的時長。如果APP不想休眠怎么辦?也有類似的方法:驅動程序有數據時主動通知APP,APP收到信號后執行信息處理函數,這就是異步通知。
圖2 異步通知的信號流程
重點從②開始:
② APP 給 SIGIO 這個信號注冊信號處理函數 func,以后 APP 收到 SIGIO信號時,這個函數會被自動調用;
③ 把 APP 的 PID(進程 ID)告訴驅動程序,這個調用不涉及驅動程序,在內核的文件系統層次記錄 PID;
④ 讀取驅動程序文件 Flag;
⑤ 設置 Flag 里面的 FASYNC 位為 1:當 FASYNC 位發生變化時,會導致驅動程序的 fasync 被調用;
⑥⑦ 調 用 faync_helper , 它會根據FAYSNC的值決定是否設置button_async->fa_file=驅動文件 filp:驅動文件 filp 結構體里面含有之前設置的 PID。
⑧ APP 可以做其他事;
⑨⑩ 按下按鍵,發生中斷,驅動程序的中斷服務程序被調用,里面調用kill_fasync 發信號;
??? APP 收到信號后,它的信號處理函數被自動調用,可以在里面調用read 函數讀取按鍵。
2.4.1 異步通知應用和驅動編程
應用程序:信號處理函數+注冊信號處理函數+打開驅動+把進程ID告訴驅動+使能驅動的FASYNC功能
static void sig_func(int sig)
{int val;read(fd, &val, 4);printf("get button : 0x%x\n", val);
}signal(SIGIO, sig_func);fd = open(argv[1], O_RDWR);
if(fd == -1)
{printf("can not open file %s\n", argv[1]);
}fcntl(fd, F_SETOWN, getpid()); //告訴驅動程序,要給誰發信號
flags = fcntl(fd, F_GETFL); //獲得之前的flags
fcntl(fd, F_SETFL, flags | FASYNC); //這是新的flags并使能驅動的FASYNC功能(使能異步通知)
驅動程序中的fasync被調用:使能異步通知后會調用這個輔助函數來構造結構體,結構體里存放進程id
//構造button_fasync結構體,結構體里存放進程id
static int gpio_drv_fasync(int fd, struct file *file, int on)
{if (fasync_helper(fd, file, on, &button_fasync) >= 0)return 0;elsereturn -EIO;
}
?驅動程序中的中斷觸發函數:按鍵消抖+修改了定時器超時時間
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{struct gpio_desc *gpio_desc = dev_id;printk("gpio_key_isr key %d irq happened\n", gpio_desc->gpio);//定時器 用來消除抖動mod_timer(&gpio_desc->key_timer, jiffies + HZ/5);//修改定時器的超時時間= jiffies(當前時間) + 赫茲/5return IRQ_HANDLED;//成功處理
}
?定時器超時函數:最后一行發送信號SIGIO給進程,button_fasync結構體中有進程信息,發送信號后,應用程序中收到信號會打斷while循環并先執行對應的信號處理函數,再回到while循環。
static void key_timer_expire(unsigned long data)
{struct gpio_desc *gpio_desc = (struct gpio_desc *)data;int val;int key;val = gpio_get_value(gpio_desc->gpio);key = (gpio_desc->key) | (val<<8);put_key(key);//按鍵值放入環形緩沖區wake_up_interruptible(&gpio_wait);//喚醒隊列里的線程kill_fasync(&button_fasync, SIGIO, POLL_IN);
}