linux 設備驅動的分層思想

一、? ? ? ? 概述

????????

? ? ? ? ? ? ? ? 像這樣的分層設計在linux的input、RTC、MTD、I2c、SPI、tty、USB等諸多類型設備驅動中屢見不鮮,下面對這些驅動進行詳細的分析。

二、????????輸入設備驅動

? ? ? ? 輸入設備(如按鍵、鍵盤、觸摸屏、鼠標等)是典型的字符設備,其一般的工作機理是底層在按鍵、觸摸等動作發送時產生一個中斷(或驅動通過Timer定時查詢),然后通過CPU通過SPI、I2c或外部存儲器讀取鍵值、坐標等數據,并將它們放入一個緩沖區,字符設備驅動管理該緩沖區,而驅動的read()接口讓用戶可以讀取鍵值、坐標等數據。

? ? ? ? 顯然,在這些工作中,只是中斷、讀鍵值/坐標值是與設備相關的,而輸入事件的緩沖區管理以及字符設備驅動的file_operations接口則對輸入設備是通用的。基于此,內核設計了輸入子系統,由核心層處理公共的工作。Linux內核輸入子系統如圖所示:

????????

? ? ? ? 輸入核心提供了底層輸入設備驅動程序所需要的API,如分配/釋放一個輸入設備:

?????????

?????????

? ? ? ? input_allocate_device()返回的是1個input_dev的結構體,此結構體用于表征1個輸入設備。注冊/注銷輸入設備用的接口如下:

?????????

? ? ? ? 報告輸入事件用的接口如下:? ? ? ? ? ? ?

? ? ? ? 而對于所有的輸入事件,內核都用統一都數據結構來描述,這個數據結構是input_event,如代碼:? ??

????????drivers/input/keyboard/gpio_keys.c基于input架構實現了通用的GPIO按鍵驅動。該驅動是基于platform_driver架構的,名為”gpio-keys“它將硬件相關的信息(如使用GPIO號,按下和抬起時的電平等)屏蔽在板文件platform_device的platform_data中,因此該驅動應用于各個處理器,具有良好的跨平臺性。以下是該驅動的probe()函數。

 851 static int gpio_keys_probe(struct platform_device *pdev)852 {853     struct device *dev = &pdev->dev;854     const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);855     struct fwnode_handle *child = NULL;856     struct gpio_keys_drvdata *ddata;857     struct input_dev *input;858     int i, error;859     int wakeup = 0;860 861     if (!pdata) {862         pdata = gpio_keys_get_devtree_pdata(dev);863         if (IS_ERR(pdata))864             return PTR_ERR(pdata);865     }866 867     ddata = devm_kzalloc(dev, struct_size(ddata, data, pdata->nbuttons),868                  GFP_KERNEL);869     if (!ddata) {870         dev_err(dev, "failed to allocate state\n");871         return -ENOMEM;872     }873 874     ddata->keymap = devm_kcalloc(dev,875                      pdata->nbuttons, sizeof(ddata->keymap[0]),876                      GFP_KERNEL);877     if (!ddata->keymap)878         return -ENOMEM;879 880     input = devm_input_allocate_device(dev);881     if (!input) {882         dev_err(dev, "failed to allocate input device\n");883         return -ENOMEM;884     }885 886     ddata->pdata = pdata;887     ddata->input = input;888     mutex_init(&ddata->disable_lock);889 890     platform_set_drvdata(pdev, ddata);891     input_set_drvdata(input, ddata);892 893     input->name = pdata->name ? : pdev->name;894     input->phys = "gpio-keys/input0";895     input->dev.parent = dev;896     input->open = gpio_keys_open;897     input->close = gpio_keys_close;898 899     input->id.bustype = BUS_HOST;900     input->id.vendor = 0x0001;901     input->id.product = 0x0001;902     input->id.version = 0x0100;903 904     input->keycode = ddata->keymap;905     input->keycodesize = sizeof(ddata->keymap[0]);906     input->keycodemax = pdata->nbuttons;907 908     /* Enable auto repeat feature of Linux input subsystem */909     if (pdata->rep)910         __set_bit(EV_REP, input->evbit);911 912     for (i = 0; i < pdata->nbuttons; i++) {913         const struct gpio_keys_button *button = &pdata->buttons[i];914 915         if (!dev_get_platdata(dev)) {916             child = device_get_next_child_node(dev, child);917             if (!child) {918                 dev_err(dev,919                     "missing child device node for entry %d\n",920                     i);921                 return -EINVAL;922             }923         }924 925         error = gpio_keys_setup_key(pdev, input, ddata,926                         button, i, child);927         if (error) {928             fwnode_handle_put(child);929             return error;930         }931 932         if (button->wakeup)933             wakeup = 1;934     }935 936     fwnode_handle_put(child);937 938     error = input_register_device(input);939     if (error) {940         dev_err(dev, "Unable to register input device, error: %d\n",941             error);942         return error;943     }944 945     device_init_wakeup(dev, wakeup);946 947     return 0;948 }
 880     input = devm_input_allocate_device(dev);886     ddata->pdata = pdata;887     ddata->input = input;888     mutex_init(&ddata->disable_lock);889 890     platform_set_drvdata(pdev, ddata);891     input_set_drvdata(input, ddata);892 893     input->name = pdata->name ? : pdev->name;894     input->phys = "gpio-keys/input0";895     input->dev.parent = dev;896     input->open = gpio_keys_open;897     input->close = gpio_keys_close;898 899     input->id.bustype = BUS_HOST;900     input->id.vendor = 0x0001;901     input->id.product = 0x0001;902     input->id.version = 0x0100;903 904     input->keycode = ddata->keymap;905     input->keycodesize = sizeof(ddata->keymap[0]);906     input->keycodemax = pdata->nbuttons;907 925         error = gpio_keys_setup_key(pdev, input, ddata,926                         button, i, child);912     for (i = 0; i < pdata->nbuttons; i++) {913         const struct gpio_keys_button *button = &pdata->buttons[i];914 915         if (!dev_get_platdata(dev)) {916             child = device_get_next_child_node(dev, child);917             if (!child) {918                 dev_err(dev,919                     "missing child device node for entry %d\n",920                     i);921                 return -EINVAL;922             }923         }924 925         error = gpio_keys_setup_key(pdev, input, ddata,926                         button, i, child);927         if (error) {928             fwnode_handle_put(child);929             return error;930         }931 932         if (button->wakeup)933             wakeup = 1;934     }938     error = input_register_device(input);

? ? ? ? 上訴代碼的第880行分配了1個輸入設備,第886~907行初始化了該input_dev的一些屬性,第925行注冊了這個輸入設備。第912~934行則初始化了所用到的GPIO,第23行完成了這個輸入設備的注冊?。

? ? ? ? 在注冊輸入設備后,底層輸入設備驅動的核心工作只剩下在按鍵、觸摸等人為動作發送時報告事件。下方展示了GPIO按鍵中斷發送時的事件報告代碼。

  1. GPIO指通用輸入輸出接口(General-purpose input/output)
  2. Keys表示按鍵設備
  3. IRQ是中斷請求(Interrupt Request)的縮寫
  4. ISR即中斷服務程序(Interrupt Service Routine)
 469 static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id)470 {471     struct gpio_button_data *bdata = dev_id;472     struct input_dev *input = bdata->input;473     unsigned long flags;474 475     BUG_ON(irq != bdata->irq);476 477     spin_lock_irqsave(&bdata->lock, flags);478 479     if (!bdata->key_pressed) {480         if (bdata->button->wakeup)481             pm_wakeup_event(bdata->input->dev.parent, 0);482 483         input_report_key(input, *bdata->code, 1);484         input_sync(input);485 486         if (!bdata->release_delay) {487             input_report_key(input, *bdata->code, 0);488             input_sync(input);489             goto out;490         }491 492         bdata->key_pressed = true;493     }494 495     if (bdata->release_delay)496         hrtimer_start(&bdata->release_timer,497                   ms_to_ktime(bdata->release_delay),498                   HRTIMER_MODE_REL_HARD);499 out:500     spin_unlock_irqrestore(&bdata->lock, flags);501     return IRQ_HANDLED;502 }
 483         input_report_key(input, *bdata->code, 1);484         input_sync(input);

? ? ? ? GPIO按鍵驅動通過input_report_key()、input_sync()這樣的函數來匯報按鍵事件以及同步事件。從底層的GPIO按鍵驅動可以看出,該驅動沒有任何file_operations的動作,也沒有各種I/O模式,注冊進入系統也用的是input_register_device()這樣與input相關的API。這是由于與linux VFS接口的這一部分代碼全部都在drivers/input/evdev.c中實現了。

 558 static ssize_t evdev_read(struct file *file, char __user *buffer,559               size_t count, loff_t *ppos)560 {561     struct evdev_client *client = file->private_data;562     struct evdev *evdev = client->evdev;563     struct input_event event;564     size_t read = 0;565     int error;566 567     if (count != 0 && count < input_event_size())568         return -EINVAL;569 570     for (;;) {571         if (!evdev->exist || client->revoked)572             return -ENODEV;573 574         if (client->packet_head == client->tail &&575             (file->f_flags & O_NONBLOCK))576             return -EAGAIN;577 578         /*579          * count == 0 is special - no IO is done but we check580          * for error conditions (see above).581          */582         if (count == 0)583             break;584 585         while (read + input_event_size() <= count &&586                evdev_fetch_next_event(client, &event)) {587 588             if (input_event_to_user(buffer + read, &event))589                 return -EFAULT;590 591             read += input_event_size();592         }593 594         if (read)595             break;596 597         if (!(file->f_flags & O_NONBLOCK)) {598             error = wait_event_interruptible(client->wait,599                     client->packet_head != client->tail ||600                     !evdev->exist || client->revoked);601             if (error)602                 return error;603         }604     }605 606     return read;607 }1292 static const struct file_operations evdev_fops = {
1293     .owner      = THIS_MODULE,
1294     .read       = evdev_read,
1295     .write      = evdev_write,
1296     .poll       = evdev_poll,
1297     .open       = evdev_open,
1298     .release    = evdev_release,
1299     .unlocked_ioctl = evdev_ioctl,
1300 #ifdef CONFIG_COMPAT
1301     .compat_ioctl   = evdev_ioctl_compat,
1302 #endif
1303     .fasync     = evdev_fasync,
1304     .llseek     = no_llseek,
1305 };

? ? ? ? 上訴代碼574-576行在檢查出是非阻塞訪問后,立即返回EAGAIM錯誤,?而586和598~600行都代碼則處理了阻塞的睡眠情況。回過頭來想,其實gpio_keys驅動里面調用的input_event()、input_sync()有間接喚醒這個等待隊列evdev->wait的功能,只不過這些代碼都隱藏在其內部實現里了。

?三、? ? ? ? RTC設備驅動

? ? ? ? RTC(實時鐘)借助電池供電,在系統掉電都情況下依然可以正常計時。它通常還具有產生周期性中斷以及鬧鐘(Alarm)中斷的能力,是一種典型的字符設備。作為一種字符設備驅動,RTC需要有file_operations中接口函數的實現,如open()、release()、read()、poll()、ioctl()等,而典型的IOCTL包括RTC_SET_TIME、RTC_ALM_READ、RTC_ALM_SET、RTC_IRQP_SET、RTC_IRQP_READ等,這些對于所有的RTC是通用的,只用底層的具體實現是與設備相關的。

? ? ? ??

四、? ? ? ? Framebuffer設備驅動

? ? ? ? Framebuff(幀緩沖)是Linux系統為顯示設備提供設備的一個接口,它將顯示緩沖器抽象,屏蔽圖像硬件的底層差異,允許上層應用程序在圖形模式下直接對顯示緩沖區進行讀寫操作。對于幀緩沖設備而言,只要在顯示緩沖區中與顯示點對應的區域內寫入區域顏色值,對應的顏色會自動在屏幕上顯示。

? ? ? ? 下圖為Linux幀緩沖設備驅動的主要結構,幀緩沖設備提供給用戶空間file_operations結構體由

drivers/video/fbdev/core/fbmem.c中的file_operations提供,而特定幀緩沖設備fb_info結構的注冊、注銷以及其中成員的維護,尤其是fb_ops中成員函數的實現則由對應的xxxfb.c文件實現,fb_ops中的函數最終會操作LCD控制其硬件寄存器。

? ? ? ? 多數顯存的操作方法都是規范的,可以按照像素點格式的要求順序寫幀緩沖區。但是有少量LCD的顯存寫法可能比較特殊,這時候,在核心層drivers/video/fbdev/core/fb_chrdev.c實現的fb_write()中,實際上可以給底層提供一個重寫自己的機會,如下:

 46 static ssize_t fb_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)47 {48     struct fb_info *info = file_fb_info(file);49 50     if (!info)51         return -ENODEV;52 53     if (info->state != FBINFO_STATE_RUNNING)54         return -EPERM;55 56     if (info->fbops->fb_write)57         return info->fbops->fb_write(info, buf, count, ppos);58 59     return fb_io_write(info, buf, count, ppos);60 }

? ? ? ? 第56~57行是一個檢查底層LCD有沒有實現自己特殊顯存寫法的代碼,如果有,直接調底層的;如果沒有,用中間層標準的顯存寫法就搞定了底層的那個不特殊的LCD。

五、? ? ? ? 終端設備驅動

? ? ? ? 在linux系統中,終端是一種字符型設備,它有多種類型,通常使用tty來簡稱各種類型的終端設備。對于嵌入式系統而言,最普通采用的是UART(Univer Asynchronous Receiver/Transmitter)串行端口,日常生活中簡稱串口。

? ? ? ? Linux內核中tty的層次結構如圖所示,它包含tty核心tty_io.c、tty線路規程n_tty.c(實現N_TTY線路規程)和tty驅動實例,tty線路規程的工作是以特殊的方式格式化從一個用戶或者硬件收到的數據,這個格式化常常采用一個協議轉換的形勢。

? ? ? ? tty_io.c本身是一個標準的字符設備驅動,它對上字符設備的職責,實現file_operations成員函數。但是tty核心層對下又定義了tty_driver的架構,這樣tty設備驅動的主體工作就變成了填充tty_driver結構體中的成員,實現其中的tty_operations的成員函數,而不再是去實現file_operations這一級的工作。tty_driver結構體和tty_operations的定義分別如下:?

????????

????????

433 struct tty_driver {
434     struct kref kref;
435     struct cdev **cdevs;
436     struct module   *owner;
437     const char  *driver_name;
438     const char  *name;
439     int name_base;
440     int major;
441     int minor_start;
442     unsigned int    num;
443     short   type;
444     short   subtype;
445     struct ktermios init_termios;
446     unsigned long   flags;
447     struct proc_dir_entry *proc_entry;
448     struct tty_driver *other;
449 
450     /*
451      * Pointer to the tty data structures
452      */
453     struct tty_struct **ttys;
454     struct tty_port **ports;
455     struct ktermios **termios;
456     void *driver_state;
457 
458     /*
459      * Driver methods
460      */
461 
462     const struct tty_operations *ops;
463     struct list_head tty_drivers;
464 } __randomize_layout;
350 struct tty_operations {
351     struct tty_struct * (*lookup)(struct tty_driver *driver,
352             struct file *filp, int idx);
353     int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
354     void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
355     int  (*open)(struct tty_struct * tty, struct file * filp);
356     void (*close)(struct tty_struct * tty, struct file * filp);
357     void (*shutdown)(struct tty_struct *tty);
358     void (*cleanup)(struct tty_struct *tty);
359     ssize_t (*write)(struct tty_struct *tty, const u8 *buf, size_t count);
360     int  (*put_char)(struct tty_struct *tty, u8 ch);
361     void (*flush_chars)(struct tty_struct *tty);
362     unsigned int (*write_room)(struct tty_struct *tty);
363     unsigned int (*chars_in_buffer)(struct tty_struct *tty);
364     int  (*ioctl)(struct tty_struct *tty,
365             unsigned int cmd, unsigned long arg);
366     long (*compat_ioctl)(struct tty_struct *tty,
367                  unsigned int cmd, unsigned long arg);
368     void (*set_termios)(struct tty_struct *tty, const struct ktermios *old);
369     void (*throttle)(struct tty_struct * tty);
370     void (*unthrottle)(struct tty_struct * tty);
371     void (*stop)(struct tty_struct *tty);
372     void (*start)(struct tty_struct *tty);
373     void (*hangup)(struct tty_struct *tty);
374     int (*break_ctl)(struct tty_struct *tty, int state);
375     void (*flush_buffer)(struct tty_struct *tty);
376     void (*set_ldisc)(struct tty_struct *tty);
377     void (*wait_until_sent)(struct tty_struct *tty, int timeout);
378     void (*send_xchar)(struct tty_struct *tty, char ch);
379     int (*tiocmget)(struct tty_struct *tty);
380     int (*tiocmset)(struct tty_struct *tty,
381             unsigned int set, unsigned int clear);
382     int (*resize)(struct tty_struct *tty, struct winsize *ws);
383     int (*get_icount)(struct tty_struct *tty,
384                 struct serial_icounter_struct *icount);
385     int  (*get_serial)(struct tty_struct *tty, struct serial_struct *p);
386     int  (*set_serial)(struct tty_struct *tty, struct serial_struct *p);
387     void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
388 #ifdef CONFIG_CONSOLE_POLL
389     int (*poll_init)(struct tty_driver *driver, int line, char *options);
390     int (*poll_get_char)(struct tty_driver *driver, int line);
391     void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
392 #endif
393     int (*proc_show)(struct seq_file *m, void *driver);
394 } __randomize_layout;

? ? ? ? tty設備發送數據的流程為:tty核心從一個用戶獲取將要發送給一個tty設備的數據,tty核心將數據傳遞給tty線路規程驅動,接著數據被傳遞到tty驅動,tty驅動將數據轉化為可以發送給硬件的格式。接收數據的流程為:從tty硬件接收到的數據向上交給tty驅動,接著進入tty線程驅動,再進入tty核心,在這里它被一個用戶獲取。?

? ? ? ? 盡管一個特定的底層UART設備驅動完全可以遵循上述tty_driver的方法來設計,即定義tty_driver并實現tty_operations中的成員函數,但是鑒于串口之間的共性,Linux考慮在文件drivers/tty/serial/serial_core.c中實現了UART設備的通用tty驅動層(我們也可以稱其為串口核心層)。這樣,UART驅動的主要任務就進一步演變成了serial-core.c中定義的一組uart_xxx接口而不是tty_xxx接口,如下圖所示,按照面向對象的思想,可以認為tty_driver是字符設備的泛華、serial-core是tty_driver的泛化,而具體的串口驅動又是serial-core的泛化。

?????????

? ? ? ? 串口核心層又定義了新的uart_driver結構體和其操作集uart_ops。一個底層的UART驅動需要創建和通過uart_register_driver注冊一個uart_driver而不是tty_driver,代碼給出uart_driver的定義。

 732 struct uart_driver {733     struct module       *owner;734     const char      *driver_name;735     const char      *dev_name;736     int          major;737     int          minor;738     int          nr;739     struct console      *cons;740 741     /*742      * these are private; the low level driver should not743      * touch these; they should be initialised to NULL744      */745     struct uart_state   *state;746     struct tty_driver   *tty_driver;747 };

? ? ? ? uart_driver結構體在本質上是派生uart_driver結構體,因此,它的第746行也包含了一個tty_driver的結構體成員。tty_operations在UART這個層面也被進一步泛化為了uart_ops,其定義如代碼如下:

 374 struct uart_ops {375     unsigned int    (*tx_empty)(struct uart_port *);376     void        (*set_mctrl)(struct uart_port *, unsigned int mctrl);377     unsigned int    (*get_mctrl)(struct uart_port *);378     void        (*stop_tx)(struct uart_port *);379     void        (*start_tx)(struct uart_port *);380     void        (*throttle)(struct uart_port *);381     void        (*unthrottle)(struct uart_port *);382     void        (*send_xchar)(struct uart_port *, char ch);383     void        (*stop_rx)(struct uart_port *);384     void        (*start_rx)(struct uart_port *);385     void        (*enable_ms)(struct uart_port *);386     void        (*break_ctl)(struct uart_port *, int ctl);387     int     (*startup)(struct uart_port *);388     void        (*shutdown)(struct uart_port *);389     void        (*flush_buffer)(struct uart_port *);390     void        (*set_termios)(struct uart_port *, struct ktermios *new,391                        const struct ktermios *old);392     void        (*set_ldisc)(struct uart_port *, struct ktermios *);393     void        (*pm)(struct uart_port *, unsigned int state,394                   unsigned int oldstate);395     const char  *(*type)(struct uart_port *);396     void        (*release_port)(struct uart_port *);397     int     (*request_port)(struct uart_port *);398     void        (*config_port)(struct uart_port *, int);399     int     (*verify_port)(struct uart_port *, struct serial_struct *);400     int     (*ioctl)(struct uart_port *, unsigned int, unsigned long);401 #ifdef CONFIG_CONSOLE_POLL402     int     (*poll_init)(struct uart_port *);403     void        (*poll_put_char)(struct uart_port *, unsigned char);404     int     (*poll_get_char)(struct uart_port *);405 #endif406 };

? ? ? ? 由于drivers/tty/serial/serial_core.c是一個tty_driver,因此在serial_core.c中,存在一個tty_operations的實例,這個實例的成員函數會進一步調用struct uart_ops的成員函數,這樣就把file_operations里的成員函數、tty_operations的成員函數和uart_ops的成員函數串起來。

六、? ? ? ? misc設備驅動

? ? ? ? 由于Linux驅動傾向與分層設計,所有各個具體的設備都可以找到它歸屬都類型,從而套到它相應的架構里面去,并且只需要實現最底層的那?一部分。但是,也有部分類似globalmem、globalfifo的字符設備,確實不知道它屬于什么類型,一般推薦大家采用miscdevice框架結構。miscdevice本質上也是字符設備,只是在miscdevice核心層的misc_init()函數中,通過register_chrdev(MISC_MAJOR,"misc",&misc_fops)注冊了字符設備,而具體miscdevice實例調用misc_register()的時候又自動完成了device_create()、獲取動態次設備號的動作。

? ? ? ? miscdevice的主設備號是固定的,MISC_MAJOR定義為10,在linux內核中,大概可以找到200多處使用miscdevice框架結構的驅動。

? ? ? ? miscdevice的結構體定義如下,第82行,指向了一個file_operations的結構體。miscdevice結構體內file_operations中的成員函數實際上是由dirvers/char/misc.c中的misc驅動的核心層的misc_ops成員函數間接調用的,比如misc_open()就會間接調用底層注冊的miscdevice的fops->open。

 79 struct miscdevice  {80     int minor;81     const char *name;82     const struct file_operations *fops;83     struct list_head list;84     struct device *parent;85     struct device *this_device;86     const struct attribute_group **groups;87     const char *nodename;88     umode_t mode;89 };

? ? ? ? ?如果上訴代碼第80行的minor為MISC_DYNAMIC_MINOR,miscdevice核心層會自動找一個空閑的次設備號,否則用minor指定的次設備號。第81行的name是設備的名稱。

? ? ? ? miscdevice驅動的注冊和注銷分別用下面兩個API:

????????

? ? ? ? 因此miscdevice驅動的一般結構形如:

?????????

? ? ? ? 在調用misc_register(&xxx_dev)時,該函數內部會自動調用device_create(),而device_create會以xxx_dev作為drvdata參數。其次,在miscdevice核心層misc_open()函數的幫助下,在file_operations的成員函數中,xxx_dev會自動生成為file的private_data(misc_open會完成file->private_data的賦值操作)。

? ? ? ? 如果我們用面向對象的封裝思想把一個設備的屬性、自旋鎖、互斥體、等待隊列、miscdevice等封裝在一個結構體里面:

?????????

? ? ? ? 在file_operations的成員函數中,就可以通過container_of()和file->private_data反推出xxx_dev的實例。??

?????????

七、? ? ? ? 驅動核心層

? ? ? ? 分析了上訴多個實例,我們可以歸納出核心層肩負的3大職責:

? ? ? ? 1)對上提供接口。file_operations的讀、寫、ioctl都被中間層搞定,各種I/O模型也被處理掉了。

? ? ? ? 2)中間層實現通用邏輯。可以被底層各種實例共享都代碼都被中間層搞定,避免底層重復實現。

? ? ? ? 3)對下定義框架。底層的驅動不再需要關心linux內核VFS的接口和各種可能的I/O模型,而只需處理與具體硬件相關的訪問。

? ? ? ? 這種分層有時候還不是兩層,可以有更多層,在軟件上呈現為面向對象里類繼承和多態的狀態。上面介紹的終端設備驅動類似下圖一樣的結果。

?????????

??

?????????

?????????

????????

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/93283.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/93283.shtml
英文地址,請注明出處:http://en.pswp.cn/web/93283.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【嵌入式硬件實例】-555定時器驅動直流無刷電機

555定時器驅動直流無刷電機 文章目錄 555定時器驅動直流無刷電機 1、555定時器介紹 2、BLDC,無刷直流電機 3、DRV10866 驅動器 4、硬件準備與接線 5、電路工作原理 在這個項目中,我們將使用 555 定時器 IC 和 DRV10866 驅動器 IC 制作 BLDC、無刷直流電機驅動電路。無刷電機可…

Helm 常用命令 + Bitnami 中間件部署速查表

文章目錄一、Helm 常用命令速查表1.1. 倉庫管理1.2. Chart 搜索1.3. 應用部署1.4. 應用管理二、Bitnami 常用中間件部署示例三、常用自定義參數&#xff08;values.yaml 配置項&#xff09;四、安裝后的訪問方式五、一鍵安裝腳本 install-middleware.sh5.1. 完整腳本5.2. 使用方…

Ansible 自動化運維實戰系列(六):Valut詳解

Ansible 自動化運維實戰系列&#xff08;六&#xff09;&#xff1a;Valut詳解&#x1f4da; 系列導航一&#xff1a;概述二&#xff1a;命令1&#xff09;創建加密文件2&#xff09;加密已有文件3&#xff09;查看加密文件4&#xff09;編輯加密文件5&#xff09;解密文件6&am…

《探秘瀏覽器Web Bluetooth API設備發現流程》

網頁若需與藍牙設備通信,往往需依賴本地客戶端或專用驅動程序作為中介,不僅增加了用戶操作成本,也限制了Web應用在跨設備場景中的拓展。而Web Bluetooth API的出現,直接賦予了網頁與低功耗藍牙(BLE)設備對話的能力,從智能手環的健康數據同步,到智能家居設備的遠程控制,…

Jenkins+Python自動化持續集成詳細教程

Python接口自動化測試零基礎入門到精通&#xff08;2025最新版&#xff09;Jenkins安裝 ? Jenkins是一個開源的軟件項目&#xff0c;是基于java開發的一種持續集成工具&#xff0c;用于監控持續重復的工作&#xff0c;旨在提供一個開放易用的軟件平臺&#xff0c;使軟件的持續…

C++面試——內存

一、簡述堆和棧的區別維度棧&#xff08;Stack&#xff09;堆&#xff08;Heap&#xff09;生命周期隨函數調用自動創建/銷毀由程序員或垃圾回收器控制分配速度極快&#xff08;僅移動指針&#xff09;慢&#xff08;需查找空閑塊、維護元數據&#xff09;空間大小較小&#xf…

UVM驗證(三)—UVM機制(1)

目錄 &#xff08;一&#xff09;Factory工廠機制 1. 工廠機制核心邏輯&#xff1a;“注冊 - 創建 - 覆蓋” 2. 代碼映射&#xff1a;從概念到實現 3. 實驗目標&#xff1a;用 dadd_fixen_driver 固定 data_en1 4. 工廠機制的價值&#xff1a;“靈活驗證的基石” 5. 常見…

前往中世紀 送修改器(Going Medieval)免安裝中文版

網盤鏈接&#xff1a; 前往中世紀 免安裝中文版 名稱&#xff1a;前往中世紀 送修改器&#xff08;Going Medieval&#xff09;免安裝中文版 描述&#xff1a; 在Going Medieval的世界中&#xff0c;黑暗時代的社會已瀕臨崩潰。14世紀末瘟疫肆虐&#xff0c;全球95%的人口因…

Font Awesome 參考手冊

Font Awesome 參考手冊 引言 Font Awesome 是一個功能強大的圖標庫,它允許開發者通過簡單的 CSS 類來添加圖標到網頁中。本手冊旨在為開發者提供全面的 Font Awesome 使用指南,包括圖標選擇、樣式定制以及常見問題解答。 圖標選擇 圖標分類 Font Awesome 提供了多種類別…

源網荷儲一體化零碳智慧工業園區建設

針對傳統工業園區等電力消納大戶存在的供電模式單一、能源管理錯雜、園區人員設備安全統籌不到位等諸多問題&#xff0c;通過AI分析及物聯網等新技術和自研交直流關鍵設備的應用&#xff0c;在三維場景中構建集智慧能源、智慧安防、碳排放管理及智慧運營等功能于一體的新型零碳…

MySQL表操作(DDL)

MySQL表操作創建表查看表結構修改表結構增加一列刪除一列修改某一列的屬性修改某一列的名字修改某一列的屬性和名字插入幾條信息刪除表創建表 語法&#xff1a; CREATE TABLE table_name ( field1 datatype, field2 datatype, field3 datatype ) character set 字符集 collat…

【總結】Python多線程

【總結】Python多線程備注一、基本概念二、備注 2025/08/15 星期五 最近用到了python的多線程發現和其他語言有點不同記錄一下 一、基本概念 首先要理解一下線程、進程和協程的概念 線程&#xff08;Thread&#xff09;&#xff1a;是計算機能夠調度的最小計算單位 進程&…

【c++深入系列】:萬字詳解模版(下)

&#x1f525; 本文專欄&#xff1a;c &#x1f338;作者主頁&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客勵志語錄&#xff1a; 成功沒有標準答案&#xff0c;但堅持永遠是必選項 ★★★ 本文前置知識&#xff1a; 模版(上&#xff09; 那么在之前的文章中我們展示…

Docker部署美化SunPanel導航頁

使用Cloudflare Tunnels穿透的地址:星霜導航 由于是使用的iStore里面的SunPanel導航頁,只是基本的功能 頁腳配置 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" conte…

支持向量機的原理和案例解析

支持向量機的原理和案例解析一、支持向量機的核心目標&#xff1a;間隔最大化步驟1&#xff1a;定義分離超平面步驟2&#xff1a;定義樣本到超平面的距離&#xff08;間隔&#xff09;步驟3&#xff1a;間隔最大化的目標步驟4&#xff1a;簡化目標函數二、通過拉格朗日乘子法求…

【教程】Nginx 源碼安裝

開發環境&#xff1a;VMWare 操作系統&#xff1a;紅帽 Linux 8 ? 前言 以離線環境安裝為前提&#xff0c;需準備以下 rmp 包內容&#xff1a; gccmakepcre-develzlib-developenssl-devel 如何準備可參考【教程】準備離線可用的 RPM 包 ? 流程 準備離線包 # 安裝 rpm yu…

俄羅斯信封套娃問題-二維最長遞增子序列

354. 俄羅斯套娃信封問題 - 力扣&#xff08;LeetCode&#xff09; Solution 對一個維度從小到大排序&#xff0c;然后對另外一個維度求最長上升子序列即可。 class Solution { public:struct node {int w, h;node(int w, int h) {this->w w;this->h h;}};static bool…

區塊鏈:用數學重構信任的數字文明基石

在數字經濟浪潮席卷全球的今天&#xff0c;虛擬與現實的融合正面臨一個根本性挑戰——如何讓數字世界的"承諾"擁有與現實世界同等的可信度&#xff1f; 當我們在電商平臺下單時&#xff0c;如何確保商品質量與描述一致&#xff1f;當企業簽署電子合同時&#xff0c;如…

Go語言defer機制詳解與應用

一、defer作用Go語言的defer關鍵字提供了一種延遲執行機制&#xff0c;它能確保指定的函數調用在當前函數返回前被執行。這一特性常用于資源釋放和異常處理場景。二、defer基本特性&#xff08;1&#xff09;執行時機&#xff1a;defer 語句會在外層函數返回前執行&#xff0c;…

服務器安全防護詳細介紹

一、方案概述隨著信息技術的飛速發展&#xff0c;服務器作為企業數據存儲、業務運行的核心載體&#xff0c;其安全性至關重要。本服務器安全防護方案旨在通過多層次、全方位的安全防護策略&#xff0c;構建一個完整的服務器安全防護體系&#xff0c;有效抵御各類安全威脅&#…