Linux驅動開發筆記(十)——中斷

視頻:第13.1講 Linux中斷實驗-Linux內核中斷框架簡介_嗶哩嗶哩_bilibili
文檔:《【正點原子】I.MX6U嵌入式Linux驅動開發指南V1.81.pdf》五十一章


1. 中斷API函數

????????每個中斷都有一個中斷號,通過中斷號即可區分不同的中斷。在Linux 內核中使用一個 int 變量表示中斷號。

1.1 申請 request_irq

? ? ? ? 申請一個中斷。

request_irq(unsigned int irq,      // 要申請中斷的中斷號irq_handler_t handler, // 中斷處理函數unsigned long flags,   // 中斷標志,/linux/interrupt.h里面查看所有的中斷標志const char *name,      // 中斷名字。設置以后可以在/proc/interrupts文件中看到對應的中斷名字void *dev)             // 如果將flags設置為IRQF_SHARED的話,dev用來區分不同的中斷,// 一般情況下將dev設置為設備結構體,dev會傳遞給中斷處理函數irq_handler_t的第二個參數。 //  return : 0中斷申請成功,其他負值中斷申請失敗,返回-EBUSY表示中斷已經被申請

1.2 釋放 free_irq

? ? ? ? 使用完后釋放中斷。

void free_irq(unsigned int irq,  // 要釋放的中斷號void *dev)         // 如果中斷設置為共享(IRQF_SHARED)的話,此參數用來區分具體的中斷// 共享中斷只有在釋放最后中斷處理函數的時候才會被禁止掉

1.3 中斷處理函數

? ? ? ? 中斷處理函數格式:

irqreturn_t (*irq_handler_t) (int, void *)//第一個int參數是要中斷處理函數要相應的中斷號
//第二個void*參數是一個指向void的指針,也就是個通用指針,需要與request_irq函數的dev參數保持一致。用于區分共享中斷的不同設備,
//            dev也可以指向設備數據結構。中斷處理函數的返回值為irqreturn_t類型

1.4 中斷使能 / 禁止

? ? ? ? 使能一個中斷:

void enable_irq(unsigned int irq) 
// irq : 要使能的中斷號

? ? ? ? 禁止一個中斷:

void disable_irq(unsigned int irq) 
// irq : 要禁止的中斷號

? ? ? ?該函數要等到當前正在執行的中斷處理函數執行完才返回,因此使用者需要保證不會產生新的中斷,并且確保所有已經開始執行的中斷處理程序已經全部退出。

void disable_irq_nosync(unsigned int irq)
// irq : 要禁止的中斷號

????????函數調用以后立即返回,不會等待當前中斷處理程序執行完畢。

1.5 全局使能 / 禁止

? ? ? ? 開啟或關閉當前處理器的整個中斷系統/全局中斷:

local_irq_enable() 
local_irq_disable() 

?????? local_irq_enable用于使能當前處理器中斷系統,local_irq_disable用于禁止當前處理器中斷 系統。

?(來自《指南》的例子)A任務要關閉全局中斷10s,但關了2s后B任務要關閉全局中斷3s,3s后B又打開了全局中斷。此時就破壞了A任務關閉全局中斷10s的要求,可能會導致一些意外。因此B不能簡單地直接打開全局中斷,而是將中斷恢復到關閉時的狀態。就可以用這兩個函數:

local_irq_save(flags)      // 禁止中斷,并將中斷狀態保存在flags中
local_irq_restore(flags)   // 恢復中斷,并將中斷狀態恢復到flags狀態

1.6??獲取中斷號

(這部分應該放到下面的設備樹里面,為了整齊我扔到這里來了)

從設備樹中的interupts屬性中獲取中斷號:

// 定義在include/linux/of_irq.h
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
// dev   : 設備節點
// index : 索引號
// return: 中斷號

如果使用GPIO,可以直接獲取gpio對應的中斷號:

int gpio_to_irq(unsigned int gpio)
// gpio  : 要獲取的GPIO編號
// return: GPIO對應的中斷號

2. 上半部和下半部 / 頂半部和底半部

(直接把《指南》里的內容精簡一下貼過來了:)

????????中斷處理函數一定要快點執行完畢,越短越好,但是有些中斷處理過程就是比較費時間,必須要對其進行處理,縮短中斷處理函數的執行時間。

????????比如電容觸摸屏通過中斷通知SOC有觸摸事件發生,SOC響應中斷,然后通過IIC讀取觸摸坐標值并將其上報給系統。但是IIC的速度最高也只有400Kbit/s,中斷通過IIC讀取數據就會浪費時間。我們可以將IIC讀取觸摸數據的操作暫后執行,中斷處理函數僅僅響應中斷,然后清除中斷標志位即可。

這個時候中斷處理過程就分為了兩部分:

????????上半部:中斷處理函數,那些處理過程比較快、不會占用很長時間的處理就可以放在上半部完成。

????????下半部:如果中斷處理過程比較耗時,那么就將這些比較耗時的代碼交給下半部去執行。

????????因此,Linux內核將中斷分為上半部和下半部的主要目的就是實現中斷處理函數的快進快 出,至于哪些代碼屬于上半部,哪些代碼屬于下半部并沒有明確的規定, 一切根據實際使用情況去判斷,一些參考點:

????????①、如果要處理的內容不希望被其他中斷打斷,可以放到上半部。

????????②、如果要處理的任務對時間敏感,可以放到上半部。

????????③、如果要處理的任務與硬件有關,可以放到上半部?

????????④、除了上述三點以外的其他任務,優先考慮放到下半部。

上半部處理直接編寫中斷處理函數就可以。對于下半部,Linux內核提供了多種下半部機制。

2.1 軟中斷

? ? ? ? (基本不用,隨便看看就星)

? ? ? ? 結構體softirq_action表示軟中斷:

// 定義在include/linux/interrupt.h
struct softirq_action{void (*action)(struct softirq_action *); // action成員變量就是軟中斷的服務函數
};

? ? ? ? 共有10種軟中斷:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
// NR_SOFTIRQS為10,數組softirq_vec有10個元素

????????數組softirq_vec是全局數組,所有的CPU都可以訪問,每個CPU都有自己的觸發和控制機制,并且只執行自己所觸發的軟中斷。但是各個CPU所執行的軟中斷服務函數是相同的,都是數組softirq_vec中定義的action函數。

// 定義在include/linux/interrupt.h
enum{HI_SOFTIRQ=0,    // 高優先級軟中斷TIMER_SOFTIRQ,   // 定時器軟中斷NET_TX_SOFTIRQ,  // 網絡數據發送軟中斷NET_RX_SOFTIRQ,  // 網絡數據接收軟中斷BLOCK_SOFTIRQ,   // 塊設備軟中斷BLOCK_IOPOLL_SOFTIRQ, // 塊設備IO輪詢軟中斷TASKLET_SOFTIRQ, // Tasklet軟中斷SCHED_SOFTIRQ,   // 調度器軟中斷HRTIMER_SOFTIRQ, // 高精度定時器軟中斷RCU_SOFTIRQ,     // RCU 回調軟中斷/* Preferable RCU should always be the last softirq */NR_SOFTIRQS      // softirq的總數
};

2.2?tasklet

? ? ? ? 也需要用到上半部,只是上半部的中斷處理函數重點在調用tasklet_schedule。

// 定義在include/linux/interrupt.h
struct tasklet_struct { struct tasklet_struct *next;     /* 下一個tasklet */ unsigned long state;             /* tasklet狀態 */ atomic_t count;                  /* 計數器,記錄對tasklet的引用數 */ void (*func)(unsigned long);     /* tasklet執行的函數,相當于中斷處理函數 */ unsigned long data;              /* 函數func的參數 */ 
};

2.2.1 初始化

// 定義在kernel/softirq.c
void tasklet_init(struct tasklet_struct *t,    // 要初始化的taskletvoid (*func)(unsigned long), // tasklet的處理函數unsigned long data           // 要傳遞給func函數的參數)

也可以使用宏DECLARE_TASKLET來一次性完成tasklet的定義和初始化, DECLARE_TASKLET定義在include/linux/interrupt.h文件中,定義如下:

// 定義在include/linux/interrupt.h
#define DECLARE_TASKLET(name, func, data) \   // name要定義的tasklet,func要處理的函數,data要傳給處理函數的參數
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

2.2.2 示例代碼

《指南》中給出了示例代碼:

/* 定義taselet    */ 
struct tasklet_struct testtasklet; /* tasklet處理函數   */ 
void testtasklet_func(unsigned long data) { /* tasklet具體處理內容 */ 
} 
/* 中斷處理函數 */ 
irqreturn_t test_handler(int irq, void *dev_id) { ...... /* 調度tasklet   */ tasklet_schedule(&testtasklet); ...... 
} 
/* 驅動入口函數     */ 
static int __init xxxx_init(void) { ...... /* 初始化tasklet   */ tasklet_init(&testtasklet, testtasklet_func, data); /* 注冊中斷處理函數   */ request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); ...... 
} 

2.3?工作隊列

????????工作隊列將要推后的工作交給一個內核線程去執行。因為工作隊列工作在進程上下文,因此工作隊列允許睡眠或重新調度

????????因此如果你要推后的工作可以睡眠,那么就可以選擇工作隊列,否則的話就只能選擇軟中斷或tasklet。

? ? ? ? 工作結構體定義如下:

// 定義在include/linux/workqueue.h
struct work_struct {atomic_long_t data;struct list_head entry;work_func_t func;
#ifdef CONFIG_LOCKDEPstruct lockdep_map lockdep_map;
#endif
};

? ? ? ? 許多個工作(work_struct)組成工作隊列(workqueue_struct),內核使用工作者線程(worker)來處理工作隊列中的工作。在實際驅動開發中,只需要工作work_struct即可,工作隊列和工作者線程由系統完成,我們不用去管。

? ? ? ? 初始化:

#define INIT_WORK(_work, _func) 
// _work: 要初始化的工作
// _func: 工作對應的處理函數#define DECLARE_WORK(n, f) 
// n: 定義的工作。這個宏會聲明并定義一個struct work_struct類型的變量n并初始化它。
// f: 工作對應的處理函數示例:DECLARE_WORK(mywork, myfunc);等價于struct work_struct mywork;INIT_WORK(&mywork, myfunc);

????????調度:

bool schedule_work(struct work_struct *work)
// work  : 要調度的工作
// return: 0成功,else失敗

????????《指南》給出了工作隊列使用示例代碼51.1.2.11:

/* 定義工作(work)           */ 
struct  work_struct testwork; /* work處理函數          */
void testwork_func_t(struct work_struct *work); { /* work具體處理內容  */ 
} 
/* 中斷處理函數     */ 
irqreturn_t test_handler(int irq, void *dev_id) { ...... /* 調度work         */ schedule_work(&testwork); ...... 
} 
/* 驅動入口函數            */ 
static int __init xxxx_init(void) { ...... /* 初始化work        */ INIT_WORK(&testwork, testwork_func_t); /* 注冊中斷處理函數      */ request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); ...... 
} 

3. 設備樹中的中斷信息節點

????????中斷控制器的設備樹綁定信息參考文檔:linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek\ Documentation\devicetree\bindings\arm\gic.txt

- compatible : should be one of:
?? ?"arm,gic-400"
?? ?"arm,cortex-a15-gic"
?? ?"arm,cortex-a9-gic"
?? ?"arm,cortex-a7-gic"
?? ?"arm,arm11mp-gic"
?? ?"brcm,brahma-b15-gic"
?? ?"arm,arm1176jzf-devchip-gic"
?? ?"qcom,msm-8660-qgic"
?? ?"qcom,msm-qgic2"
- interrupt-controller : Identifies the node as an interrupt controller
- #interrupt-cells : Specifies the number of cells needed to encode an interrupt source. ?The type shall be a <u32> and the value shall be 3.

? The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI?interrupts.

? The 2nd cell contains the interrupt number for the interrupt type.
? SPI interrupts are in the range [0-987]. ?PPI interrupts are in the range [0-15].

? The 3rd cell is the flags, encoded as follows:
?? ?bits[3:0] trigger type and level flags.
?? ??? ?1 = low-to-high edge triggered?
?? ??? ?2 = high-to-low edge triggered (invalid for SPIs)
?? ??? ?4 = active high level-sensitive
?? ??? ?8 = active low level-sensitive (invalid for SPIs).

?? ?bits[15:8] PPI interrupt cpu mask. ?Each bit corresponds to each of
?? ?the 8 possible cpus attached to the GIC. ?A bit set to '1' indicated
?? ?the interrupt is wired to that CPU. ?Only valid for PPI interrupts.
?? ?Also note that the configurability of PPI interrupts is IMPLEMENTATION
?? ?DEFINED and as such not guaranteed to be present (most SoC available
?? ?in 2014 seem to ignore the setting of this flag and use the hardware
?? ?default value).

? ? ? ? 舉個例子,imx6ull.dtsi中的intc節點就是 I.MX6ULL的中斷控制器節點:

	intc: interrupt-controller@00a01000 {compatible = "arm,cortex-a7-gic";#interrupt-cells = <3>;    // 此中斷控制器下設備的cells大小為3interrupt-controller;      // 表示當前節點是中斷控制器reg = <0x00a01000 0x1000>,<0x00a02000 0x100>;};

????????gpio節點也可以作為中斷控制器。

????????如imx6ull.dtsi文件中的gpio5節點:

gpio5: gpio@020ac000 {compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";reg = <0x020ac000 0x4000>;interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;gpio-controller;    // 中斷控制器,用于控制gpio5所有IO的中斷#gpio-cells = <2>;interrupt-controller;#interrupt-cells = <2>;
};

? ? ? ? 該節點的interrupts這個屬性得細說啊。

? ? ? ? 其中GIC_SPI==0? (定義在include/dt-bindings/interrupt-controller/arm-gic.h),IRQ_TYPE_LEVEL_HIGH==4? (定義在include/dt-bindings/interrupt-controller/irq.h)
? ? ? ? 再參考上方引用塊綁定信息加粗的#interrupt-cells部分,這兩行表示SPI中斷、中斷號為74 75、高電平觸發。

? ? ? ? 至于這個74和75怎么來的,查?《IMX6ULL參考手冊》185頁可以找到下表,即GPIO5有兩個中斷號——74和75。74對應GPIO5_IO0015低16位IO,75對應GPIO5_IO1631高16位IO。

以下內容來自linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek\Documentation\devicetree \bindings\gpio\gpio-mxs.txt:

The Freescale MXS GPIO controller is part of MXS PIN controller. ?The
GPIOs are organized in port/bank. ?Each port consists of 32 GPIOs.

As the GPIO controller is embedded in the PIN controller and all the
GPIO ports share the same IO space with PIN controller, the GPIO node
will be represented as sub-nodes of MXS pinctrl node.

Required properties for GPIO node:
- compatible : Should be "fsl,<soc>-gpio". ?The supported SoCs include?imx23 and imx28.
- interrupts : Should be the port interrupt shared by all 32 pins.
- gpio-controller : Marks the device node as a gpio controller.
- #gpio-cells : Should be two. ?The first cell is the pin number and?the second cell is used to specify the gpio polarity:
? ? ? 0 = active high
? ? ? 1 = active low
- interrupt-controller: Marks the device node as an interrupt controller.
- #interrupt-cells : Should be 2. ?The first cell is the GPIO number.
? The second cell bits[3:0] is used to specify trigger type and level flags:
? ? ? 1 = low-to-high edge triggered.
? ? ? 2 = high-to-low edge triggered.
? ? ? 4 = active high level-sensitive.
? ? ? 8 = active low level-sensitive.

// imx6ull-alientek-emmc.dtsfxls8471@1e {compatible = "fsl,fxls8471";reg = <0x1e>;position = <0>;interrupt-parent = <&gpio5>; // 父中斷interrupts = <0 8>;   // 參照上面引用塊里的interrupts部分。表示GPIO5_IO00,低電平觸發};

4. 實驗:按鍵中斷+定時器消抖+tasklet下半部

視頻:第13.4講 Linux中斷實驗-按鍵中斷實驗驅動編寫(上)_嗶哩嗶哩_bilibili

手冊:《【正點原子】I.MX6U嵌入式Linux驅動開發指南V1.81.pdf》51.3節

4.1 設備樹

? ? ? ? 直接在上次按鍵實驗設備樹節點基礎上增加兩行中斷描述即可。將如下代碼添加到imx6ull-alientek-emmc.dts根節點最后:

	key{compatible = "alientek,key";pinctrl-names = "default";pinctrl-0 = <&pinctrl_key>;key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;  status = "okay";interrupt-parent = <&gpio1>;interrupts = <18 IRQ_TYPE_EDGE_BOTH>;};

如果需要配置多個中斷,則寫法為:

編譯為dtb并復制到tftproot,重啟開發板:

make dtbs
sudo cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /.../tftpboot/ -f

可以去開發板檢查一下,應當能看到key節點:

4.2 文件結構

? ? ? ? 和其他實驗一樣,直接貼過來改個文件名:

14_IRQ (工作區)
├── .vscode
│   ├── c_cpp_properties.json
│   └── settings.json
├── 14_irq.code-workspace
├── Makefile
├── irq.c
└── irqAPP.c

4.3 Makefile

CFLAGS_MODULE += -wKERNELDIR := /home/for/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek  # 內核路徑
# KERNELDIR改成自己的?linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek文件路徑(這個文件從正點原子“01、例程源碼”中直接搜,cp到虛擬機里面)CURRENT_PATH := $(shell pwd)	# 當前路徑obj-m := irq.o			# 編譯文件build: kernel_modules			# 編譯模塊kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean	

4.4 irq.c

按鍵→中斷處理函數→調用tasklet處理函數→tasklet啟動定時器→定時器處理函數

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <linux/atomic.h>#define DEV_CNT  1          /* 設備號數量 */
#define DEV_NAME "timer"    /* 設備名 */
#define KEY_NUM 1           /* 按鍵數量 */
#define KEY0_VALUE 0x01     /* 按鍵值(假定釋放為1,按下為0)*/
#define INVAKEY    0xFF     /* 按鍵無效值 *//* 按鍵結構體 */
struct irq_keydesc{int gpio;   /* io號 */int irqnum; /* 中斷號 */unsigned char value; /* 鍵值 (默認/空閑電平,比如 1) */char name[10];  /* 名字 */irqreturn_t (*handler) (int, void *);   /* 中斷處理函數*/struct tasklet_struct tasklet;
};/* 中斷設備結構體 */
struct irq{dev_t devid;    // 設備號int major;      // 主設備號int minor;      // 次struct cdev cdev;       // cdevstruct device *device;  // 設備struct class *class;    // 類struct device_node *nd; // 節點struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 個按鍵struct timer_list timer;// 定時器atomic_t releasekey;  // 0表示按鍵釋放,1表示按鍵按下atomic_t keyvalue;    // 第八位表示數據是否被讀取:每完成一次按鍵釋放,第八位就會置1,用戶態調用.read時會將其置0,表示數據被讀取// 其余七位表示按鍵的值
};
static struct irq key_irq;/* 打開/讀操作 */
static int irq_open(struct inode *inode, struct file *filp){filp->private_data = &key_irq;return 0;
}
static ssize_t irq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){int ret = 0;unsigned char keyvalue = 0;unsigned char releasekey = 0;struct irq* dev = filp->private_data;keyvalue = (unsigned char)atomic_read(&dev->keyvalue);releasekey = (unsigned char)atomic_read(&dev->releasekey);if(releasekey){ // 如果按鍵釋放,此時可以讀取數據if(keyvalue & 0x80){  // 如果最高位為1,表示此時數據還未被讀取unsigned char out = keyvalue & 0x7F; // 將最高位置0(最高位為有效位,低7位為實際value)if(copy_to_user(buf, &out, sizeof(out))){            // 返回按鍵值給用戶態return -EFAULT;}ret = sizeof(out);} else {goto data_error;}atomic_set(&dev->releasekey, 0); // 釋放標志清零return ret;} else {goto data_error;}data_error:return -EINVAL;
}/* 操作集 */
static const struct file_operations key_fops = {.owner = THIS_MODULE,.open = irq_open,.read = irq_read,
};/* 中斷處理函數 */
static irqreturn_t key0_handler(int irq, void *dev_id){struct irq *dev = (struct irq*)dev_id;int current_level;  // 保存中斷時的電平current_level = gpio_get_value(dev->irqkey[0].gpio);atomic_set(&dev->keyvalue, current_level);  // 保存基準電平tasklet_schedule(&dev->irqkey[0].tasklet); // 調度tasklet處理后續邏輯,避免中斷耗時return IRQ_HANDLED;
}/* 定時器處理函數 */
static void timer_func(unsigned long arg){struct irq* dev = (struct irq*)arg;int current_value; // 保存定時器延時后的電平/* 讀取當前按鍵電平 */current_value = gpio_get_value(dev->irqkey[0].gpio);/* 若當前電平與中斷發生時讀取到的電平一致,則有效 */if (current_value == atomic_read(&dev->keyvalue)) {if (current_value == 1) { // 釋放printk("KEY0 release!\r\n");atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value); // 最高位置1,表示有效atomic_set(&dev->releasekey, 1);  // 表示已釋放,此時用戶態可讀} else { // 按下printk("KEY0 Push!\r\n");atomic_set(&dev->keyvalue, current_value); // 這里0 表示按下// atomic_set(&dev->releasekey, 0);  // 表示未釋放}}
}/* tasklet處理函數 */
static void key_tasklet(unsigned long data){struct irq* dev = (struct irq*)data;printk("key_tasklet\r\n");dev->timer.data = data;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /* 20ms定時 */}/* 初始化按鍵 */
static int keyio_init(struct irq *dev){int ret = 0;int i = 0;/* 1. 按鍵初始化 */dev->nd = of_find_node_by_path("/key");  // 獲取設備節點if(dev->nd == NULL){ret = -EINVAL;goto fail_nd;}for(i=0; i<KEY_NUM; i++){dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 獲取設備樹中 gpioif (dev->irqkey[i].gpio < 0) {pr_err("get gpio %d failed\n", i);ret = -EINVAL;goto fail_nd;}}for(i=0; i<KEY_NUM; i++){memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));sprintf(dev->irqkey[i].name, "KEY%d", i);   // 命名ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申請gpioif (ret) {pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);goto fail_gpio_req;}gpio_direction_input(dev->irqkey[i].gpio);  // 設置io方向dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 獲取中斷號if (dev->irqkey[i].irqnum < 0) {pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);ret = dev->irqkey[i].irqnum;goto fail_gpio_req;}}dev->irqkey[0].handler = key0_handler; // 中斷處理函數dev->irqkey[0].value = KEY0_VALUE;     // 例如:1 表示 release(高電平),0 表示按下(低電平)/* 2. 中斷初始化 */for(i=0; i<KEY_NUM; i++){ret = request_irq(dev->irqkey[i].irqnum,dev->irqkey[i].handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,dev->irqkey[i].name,&key_irq); /* dev_id 傳入結構體指針 */if(ret < 0){printk("irq %d request failed! ret=%d\n", dev->irqkey[i].irqnum, ret);while (--i >= 0) {free_irq(dev->irqkey[i].irqnum, &key_irq);}goto fail_irq;}tasklet_init(&dev->irqkey[0].tasklet, key_tasklet, (unsigned long)dev);}return 0;fail_irq:
fail_gpio_req:for (i = 0; i < KEY_NUM; i++) {if (gpio_is_valid(dev->irqkey[i].gpio))gpio_free(dev->irqkey[i].gpio);}
fail_nd:return ret;
}/* 驅動入口 */
static int __init key_init(void){int ret = 0;/* 1. 注冊字符設備驅動 */key_irq.devid = 0;alloc_chrdev_region(&key_irq.devid, 0, DEV_CNT, DEV_NAME);key_irq.major = MAJOR(key_irq.devid);key_irq.minor = MINOR(key_irq.devid);/* 2. 初始化 cdev */key_irq.cdev.owner = THIS_MODULE;cdev_init(&key_irq.cdev, &key_fops);/* 3. 添加 cdev */ret = cdev_add(&key_irq.cdev, key_irq.devid, DEV_CNT);if (ret) {pr_err("cdev_add failed\n");goto fail_cdev;}/* 4. 創建類 */key_irq.class = class_create(THIS_MODULE, DEV_NAME);if(IS_ERR(key_irq.class)){ret = PTR_ERR(key_irq.class);goto fail_class;}/* 5. 創建設備 */key_irq.device = device_create(key_irq.class, NULL, key_irq.devid, NULL, DEV_NAME);if(IS_ERR(key_irq.device)){ret = PTR_ERR(key_irq.device);goto fail_device;}/* 6. 初始化IO*/ret = keyio_init(&key_irq);if(ret < 0){goto fail_keyinit;}/* 7. 初始化定時器 */init_timer(&key_irq.timer);key_irq.timer.function = timer_func;key_irq.timer.data = (unsigned long)&key_irq; /* 初始化 timer.data 指向設備結構體 *//* 8. 初始化原子變量 */atomic_set(&key_irq.keyvalue, INVAKEY);atomic_set(&key_irq.releasekey, 0);return 0;fail_keyinit:device_destroy(key_irq.class, key_irq.devid);
fail_device:class_destroy(key_irq.class);
fail_class:cdev_del(&key_irq.cdev);
fail_cdev:unregister_chrdev_region(key_irq.devid, DEV_CNT);return ret;
}/* 驅動出口 */
static void __exit key_exit(void){int i=0;/* 釋放中斷 */for(i=0; i<KEY_NUM; i++){free_irq(key_irq.irqkey[i].irqnum, &key_irq);}/* 釋放io */for(i=0; i<KEY_NUM; i++){if (gpio_is_valid(key_irq.irqkey[i].gpio))gpio_free(key_irq.irqkey[i].gpio);}/* 刪除定時器 */del_timer_sync(&key_irq.timer);/* 注銷字符設備驅動 */cdev_del(&key_irq.cdev);unregister_chrdev_region(key_irq.devid, DEV_CNT);device_destroy(key_irq.class, key_irq.devid);class_destroy(key_irq.class);
}module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");

4.5 irqAPP.c

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/ioctl.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>/* * @description    : main主程序 * @param - argc   : argv數組元素個數 * @param - argv   : 具體參數 * @return         : 0 成功; else失敗* 調用  ./irqAPP /dev/timer*/ #define LEDOFF 0
#define LEDON  1int main(int argc, char *argv[]){if(argc != 2){  // 判斷用法是否錯誤printf("Error Usage!\r\n");return -1;}char *filename;int fd = 0, ret = 0;unsigned char data;filename = argv[1];fd = open(filename, O_RDWR);  // 讀寫模式打開驅動文件filenameif(fd <0){printf("file %s open failed!\r\n");return -1;}while(1) {ret = read(fd, &data, sizeof(data));if (ret > 0) {printf("key value = %#x\r\n", data);} else {// usleep(10000); // 10ms 防止CPU占用過高}}printf("App finished!\r\n");close(fd);return 0;
}

4.6 測試

# VSCODE終端
make
arm-linux-gnueabihf-gcc irqAPP.c -o irqAPP
sudo cp irq.ko irqAPP /home/for/linux/nfs/rootfs/lib/modules/4.1.15/ -f# 串口
cd /lib/modules/4.1.15/
depmod
modprobe irq.ko
cat /proc/interrupts    # 此時在列表最右一列中應當能看到KEY0./irqAPP /dev/timer
# 然后按下按鍵/松開按鍵,串口應有對應的輸出rmmod irq.k
cat /proc/interrupts    # 此時應當看不到KEY0

5. 實驗:按鍵中斷+定時器消抖+工作隊列

有以下修改:
? ? ? ? 中斷設備結構體中增加了工作隊列結構體work_struct

????????中斷處理函數key0_handler中增加了對工作隊列的調用schedule_work()

? ? ? ? 初始化函數keyio_init()中增加了對工作隊列的初始化INIT_WORK

? ? ? ? 增加了work處理函數key_work()

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <linux/atomic.h>#define DEV_CNT  1          /* 設備號數量 */
#define DEV_NAME "timer"    /* 設備名 */
#define KEY_NUM 1           /* 按鍵數量 */
#define KEY0_VALUE 0x01     /* 按鍵值(假定釋放為1,按下為0)*/
#define INVAKEY    0xFF     /* 按鍵無效值 *//* 按鍵結構體 */
struct irq_keydesc{int gpio;   /* io號 */int irqnum; /* 中斷號 */unsigned char value; /* 鍵值 (默認/空閑電平,比如 1) */char name[10];  /* 名字 */irqreturn_t (*handler) (int, void *);   /* 中斷處理函數*/// struct tasklet_struct tasklet;struct work_struct work;
};/* 中斷設備結構體 */
struct irq{dev_t devid;    // 設備號int major;      // 主設備號int minor;      // 次struct cdev cdev;       // cdevstruct device *device;  // 設備struct class *class;    // 類struct device_node *nd; // 節點struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 個按鍵struct timer_list timer;// 定時器atomic_t releasekey;  // 0表示按鍵釋放,1表示按鍵按下atomic_t keyvalue;    // 第八位表示數據是否被讀取:每完成一次按鍵釋放,第八位就會置1,用戶態調用.read時會將其置0,表示數據被讀取// 其余七位表示按鍵的值
};
static struct irq key_irq;/* 打開/讀操作 */
static int irq_open(struct inode *inode, struct file *filp){filp->private_data = &key_irq;return 0;
}
static ssize_t irq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){int ret = 0;unsigned char keyvalue = 0;unsigned char releasekey = 0;struct irq* dev = filp->private_data;keyvalue = (unsigned char)atomic_read(&dev->keyvalue);releasekey = (unsigned char)atomic_read(&dev->releasekey);if(releasekey){ // 如果按鍵釋放,此時可以讀取數據if(keyvalue & 0x80){  // 如果最高位為1,表示此時數據還未被讀取unsigned char out = keyvalue & 0x7F; // 將最高位置0(最高位為有效位,低7位為實際value)if(copy_to_user(buf, &out, sizeof(out))){            // 返回按鍵值給用戶態return -EFAULT;}ret = sizeof(out);} else {goto data_error;}atomic_set(&dev->releasekey, 0); // 釋放標志清零return ret;} else {goto data_error;}data_error:return -EINVAL;
}/* 操作集 */
static const struct file_operations key_fops = {.owner = THIS_MODULE,.open = irq_open,.read = irq_read,
};/* 中斷處理函數 */
static irqreturn_t key0_handler(int irq, void *dev_id){struct irq *dev = (struct irq*)dev_id;int current_level;  // 保存中斷時的電平current_level = gpio_get_value(dev->irqkey[0].gpio);atomic_set(&dev->keyvalue, current_level);  // 保存基準電平// tasklet_schedule(&dev->irqkey[0].tasklet); // 調度tasklet處理后續邏輯,避免中斷耗時schedule_work(&dev->irqkey[0].work);return IRQ_HANDLED;
}/* 定時器處理函數 */
static void timer_func(unsigned long arg){struct irq* dev = (struct irq*)arg;int current_value; // 保存定時器延時后的電平/* 讀取當前按鍵電平 */current_value = gpio_get_value(dev->irqkey[0].gpio);/* 若當前電平與中斷發生時讀取到的電平一致,則有效 */if (current_value == atomic_read(&dev->keyvalue)) {if (current_value == 1) { // 釋放printk("KEY0 release!\r\n");atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value); // 最高位置1,表示有效atomic_set(&dev->releasekey, 1);  // 表示已釋放,此時用戶態可讀} else { // 按下printk("KEY0 Push!\r\n");atomic_set(&dev->keyvalue, current_value); // 這里0 表示按下// atomic_set(&dev->releasekey, 0);  // 表示未釋放}}
}/* tasklet處理函數 */
static void key_tasklet(unsigned long data){struct irq* dev = (struct irq*)data;printk("      key_tasklet\r\n");dev->timer.data = data;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /* 20ms定時 */}
/* 工作隊列處理函數 */
static void key_work(struct work_struct *work){printk("      key_work\r\n");key_irq.timer.data = (unsigned long)&key_irq;mod_timer(&key_irq.timer, jiffies + msecs_to_jiffies(20)); /* 20ms定時 */
}/* 初始化按鍵 */
static int keyio_init(struct irq *dev){int ret = 0;int i = 0;/* 1. 按鍵初始化 */dev->nd = of_find_node_by_path("/key");  // 獲取設備節點if(dev->nd == NULL){ret = -EINVAL;goto fail_nd;}for(i=0; i<KEY_NUM; i++){dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 獲取設備樹中 gpioif (dev->irqkey[i].gpio < 0) {pr_err("get gpio %d failed\n", i);ret = -EINVAL;goto fail_nd;}}for(i=0; i<KEY_NUM; i++){memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));sprintf(dev->irqkey[i].name, "KEY%d", i);   // 命名ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申請gpioif (ret) {pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);goto fail_gpio_req;}gpio_direction_input(dev->irqkey[i].gpio);  // 設置io方向dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 獲取中斷號if (dev->irqkey[i].irqnum < 0) {pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);ret = dev->irqkey[i].irqnum;goto fail_gpio_req;}}dev->irqkey[0].handler = key0_handler; // 中斷處理函數dev->irqkey[0].value = KEY0_VALUE;     // 例如:1 表示 release(高電平),0 表示按下(低電平)/* 2. 中斷初始化 */for(i=0; i<KEY_NUM; i++){ret = request_irq(dev->irqkey[i].irqnum,dev->irqkey[i].handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,dev->irqkey[i].name,&key_irq); /* dev_id 傳入結構體指針 */if(ret < 0){printk("irq %d request failed! ret=%d\n", dev->irqkey[i].irqnum, ret);while (--i >= 0) {free_irq(dev->irqkey[i].irqnum, &key_irq);}goto fail_irq;}// tasklet_init(&dev->irqkey[i].tasklet, key_tasklet, (unsigned long)dev);INIT_WORK(&dev->irqkey[i].work, key_work);}return 0;fail_irq:
fail_gpio_req:for (i = 0; i < KEY_NUM; i++) {if (gpio_is_valid(dev->irqkey[i].gpio))gpio_free(dev->irqkey[i].gpio);}
fail_nd:return ret;
}/* 驅動入口 */
static int __init key_init(void){int ret = 0;/* 1. 注冊字符設備驅動 */key_irq.devid = 0;alloc_chrdev_region(&key_irq.devid, 0, DEV_CNT, DEV_NAME);key_irq.major = MAJOR(key_irq.devid);key_irq.minor = MINOR(key_irq.devid);/* 2. 初始化 cdev */key_irq.cdev.owner = THIS_MODULE;cdev_init(&key_irq.cdev, &key_fops);/* 3. 添加 cdev */ret = cdev_add(&key_irq.cdev, key_irq.devid, DEV_CNT);if (ret) {pr_err("cdev_add failed\n");goto fail_cdev;}/* 4. 創建類 */key_irq.class = class_create(THIS_MODULE, DEV_NAME);if(IS_ERR(key_irq.class)){ret = PTR_ERR(key_irq.class);goto fail_class;}/* 5. 創建設備 */key_irq.device = device_create(key_irq.class, NULL, key_irq.devid, NULL, DEV_NAME);if(IS_ERR(key_irq.device)){ret = PTR_ERR(key_irq.device);goto fail_device;}/* 6. 初始化IO*/ret = keyio_init(&key_irq);if(ret < 0){goto fail_keyinit;}/* 7. 初始化定時器 */init_timer(&key_irq.timer);key_irq.timer.function = timer_func;key_irq.timer.data = (unsigned long)&key_irq; /* 初始化 timer.data 指向設備結構體 *//* 8. 初始化原子變量 */atomic_set(&key_irq.keyvalue, INVAKEY);atomic_set(&key_irq.releasekey, 0);return 0;fail_keyinit:device_destroy(key_irq.class, key_irq.devid);
fail_device:class_destroy(key_irq.class);
fail_class:cdev_del(&key_irq.cdev);
fail_cdev:unregister_chrdev_region(key_irq.devid, DEV_CNT);return ret;
}/* 驅動出口 */
static void __exit key_exit(void){int i=0;/* 釋放中斷 */for(i=0; i<KEY_NUM; i++){free_irq(key_irq.irqkey[i].irqnum, &key_irq);}/* 釋放io */for(i=0; i<KEY_NUM; i++){if (gpio_is_valid(key_irq.irqkey[i].gpio))gpio_free(key_irq.irqkey[i].gpio);}/* 刪除定時器 */del_timer_sync(&key_irq.timer);/* 注銷字符設備驅動 */cdev_del(&key_irq.cdev);unregister_chrdev_region(key_irq.devid, DEV_CNT);device_destroy(key_irq.class, key_irq.devid);class_destroy(key_irq.class);
}module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");

連著兩個key_work應該就是發生了抖動,不過都被消抖攔住了↓

6. 附錄

視頻第13.7講的后半部分講了一下container_of函數的使用方法。

????????container_of通過指向結構體中某個成員的指針,反推出整個結構體的指針,并返回任意成員的地址:

// 需要#include <linux/kernel.h>container_of(ptr, type, member)
// prt:    已知的成員指針
// type:   結構體的定義類型
// member: 要獲得地址的成員名
// return: member的地址

如本次實驗中的結構體定義:

/* 按鍵結構體 */
struct irq_keydesc{int gpio;   /* io號 */int irqnum; /* 中斷號 */unsigned char value; /* 鍵值 (默認/空閑電平,比如 1) */char name[10];  /* 名字 */irqreturn_t (*handler) (int, void *);   /* 中斷處理函數*/// struct tasklet_struct tasklet;struct work_struct work;
};/* 中斷設備結構體 */
struct irq{dev_t devid;    // 設備號int major;      // 主設備號int minor;      // 次struct cdev cdev;       // cdevstruct device *device;  // 設備struct class *class;    // 類struct device_node *nd; // 節點struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 個按鍵struct timer_list timer;// 定時器atomic_t releasekey;  // 0表示按鍵釋放,1表示按鍵按下atomic_t keyvalue;    // 第八位表示數據是否被讀取:每完成一次按鍵釋放,第八位就會置1,用戶態調用.read時會將其置0,表示數據被讀取// 其余七位表示按鍵的值
};

調用該函數就能直接獲得任意成員地址:

// 工作隊列處理函數
static void key_work(struct work_struct *work_){struct irq* dev = container_of(work_, struct irq, work); // 獲得工作隊列work成員的地址
}

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

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

相關文章

ubuntu18.04安裝PCL1.14

簡化版說明 1. 安裝依賴庫&#xff1a; (1) boost1.84 &#xff08;https://www.boost.org/releases/1.84.0/&#xff09; tar vxf boost_xxx.tar.gz ./bootstrap.sh --prefix/usr/local/ ./b2 sudo ./b2 install (2) vtk9.1.0 &#xff08;https://vtk.org/files/releas…

python將pdf轉txt,并切割ai

python將pdf轉txt&#xff0c;并切割ai step1:pdf轉換 from PIL import Image import pytesseract import os import tempfile from pdf2image import convert_from_path# 設置 Tesseract 路徑 pytesseract.pytesseract.tesseract_cmd rC:\Users\wangrusheng\AppData\Local\Pr…

Ubuntu22.04更換阿里鏡像源,ubuntu更換源

在 Ubuntu 22.04 上更換為阿里云鏡像源可以加速軟件包的下載和更新&#xff0c;大幅提升系統更新速度。以下是更換阿里云鏡像源的步驟&#xff1a;1. 備份現有源列表在更換鏡像源之前&#xff0c;建議先備份當前的源配置文件&#xff1a;bashsudo cp /etc/apt/sources.list /et…

Git版本控制工具+基礎命令

Git是什么&#xff1f;Git是目前世界上最先進的分布式版本控制系統代碼托管平臺&#xff1a;Gitlab/Github/Gitee&#xff08;碼云&#xff09;什么是版本控制系統&#xff1f;指對軟件開發過程中各種程序代碼、配置文件及說明文檔等文件變更的管理。版本控制最主要的功能就是追…

圖解設計模式【3】

本系列共分為三篇文章&#xff0c;其中包含的設計模式如下表&#xff1a; 名稱設計模式圖解設計模式【1】Iterator、Adapter、Template Method、Factory Method、Singleton、Prototype、 Builder、Abstract Factory、 Bridge、 Strategy圖解設計模式【2】Composite、 Decorato…

(純新手教學)計算機視覺(opencv)實戰十四——模板與多個對象匹配

圖片旋轉、圖片鏡像相關教學&#xff1a; &#xff08;純新手教學&#xff09;計算機視覺&#xff08;opencv&#xff09;實戰十三——圖片旋轉、圖片鏡像 的幾種常用方法-CSDN博客https://blog.csdn.net/2302_78022640/article/details/151356600?spm1011.2415.3001.5331 模板…

Java面試核心知識點總結:Redis與MySQL高可用、高并發解決方案

在分布式系統開發中&#xff0c;高并發場景下的數據一致性、系統可用性以及性能優化始終是核心挑戰。本文基于Java技術棧&#xff0c;結合Redis與MySQL的工程實踐&#xff0c;系統梳理分布式系統設計的關鍵技術要點。一、Redis集群架構演進與高可用實踐1.1 主從哨兵模式部署方案…

R 語言科研繪圖第 72 期 --- mantel檢驗圖

在發表科研論文的過程中&#xff0c;科研繪圖是必不可少的&#xff0c;一張好看的圖形會是文章很大的加分項。 為了便于使用&#xff0c;本系列文章介紹的所有繪圖都已收錄到了 sciRplot 項目中&#xff0c;獲取方式&#xff1a; R 語言科研繪圖模板 --- sciRplothttps://mp.…

4.2-中間件之MySQL

4.2.1MySQL的基本知識SQL語句用于存取數據以及查詢、更新和管理關系數據庫系統。包括&#xff1a;DQL&#xff08;select&#xff09;、DML&#xff08;insert,update,delete&#xff09;、DDL&#xff08;create,alter,drop&#xff09;、DCL&#xff08;grant,revoke&#xf…

LVS + Keepalived 高可用負載均衡集群

目錄 一、核心組件與作用 1. LVS&#xff08;Linux Virtual Server&#xff09; 2. Keepalived 二、DR 模式下的 LVS Keepalived 工作原理 1. 整體架構 2. 數據包流向&#xff08;DR 模式&#xff09; 三、部署步驟&#xff08;DR 模式&#xff09; 3.1 環境規劃 3.2…

知識沉淀過于碎片化如何形成體系化框架

要將過于碎片化的知識沉淀轉變為體系化的框架&#xff0c;必須采取一套自上而下設計與自下而上歸集相結合的系統性方法&#xff0c;其核心路徑在于首先進行戰略性診斷與頂層藍圖設計、其次構建統一且可擴展的知識架構&#xff08;分類與標簽體系&#xff09;、然后實施系統性的…

XLua教程之C#調用Lua

上一篇文章 XLua教程之入門篇-CSDN博客 在C#腳本中訪問lua全局數據&#xff0c;特別是table以及function&#xff0c;代價比較大&#xff0c;建議盡量少做相關操作。 LuaEnv.Global.Get 用于獲取一個全局變量&#xff0c;但是無法獲取局部變量(用local修飾) 全局基本類型變量…

C++ 標準庫中的哈希函數:從std::hash到自定義哈希器

C 標準庫中的哈希函數&#xff1a;從 std::hash 到自定義哈希器 1. 引言 在上一篇中&#xff0c;我們介紹了哈希表為什么能夠實現 O(1) 查找。 核心秘密在于&#xff1a;哈希函數。 在 C 標準庫中&#xff0c;哈希表容器&#xff08;如 unordered_map、unordered_set&#xff0…

在圖形 / 游戲開發中,為何 Pixels Per Unit(PPU)數值越小,物體在屏幕上顯示的尺寸越大?

1. 什么是 PPU&#xff1f; PPU&#xff08;Pixels Per Unit&#xff09;指的是 多少像素對應游戲世界中的一個單位&#xff08;Unit&#xff09;。 在 Unity 等游戲引擎中&#xff0c;1 Unit 通常被視為世界空間的基本長度&#xff0c;比如 1 米。2. PPU 與物體大小的關系PPU …

【ZYNQ開發篇】Petalinux和電腦端的靜態ip地址配置

使用Petalinux工具為ZYNQ板卡搭建嵌入式Linux操作系統&#xff0c;成功搭建后&#xff0c;用戶通常會使用客戶端軟件對ZYNQ板卡上的Linux系統進行訪問&#xff0c;軟件需要知道ZYNQ板卡的ip地址才能進行訪問&#xff0c;如果ip地址是動態變化的&#xff0c;軟件每次訪問都要重新…

AVL樹知識總結

AVL樹概念性質一顆AVL樹或是空樹&#xff0c;或者具有一下性質的二叉搜索樹&#xff1a;左右都是AVL樹&#xff0c;左右子樹高度差的絕對值不超過1AVL樹有n個結果&#xff0c;高度保持在O&#xff08;logN&#xff09; 搜索時間復雜度O(logN&#xff09;模擬實現插入定義&#…

返利app的跨域問題解決方案:CORS與反向代理在前后端分離架構中的應用

返利app的跨域問題解決方案&#xff1a;CORS與反向代理在前后端分離架構中的應用 大家好&#xff0c;我是阿可&#xff0c;微賺淘客系統及省賺客APP創始人&#xff0c;是個冬天不穿秋褲&#xff0c;天冷也要風度的程序猿&#xff01; 在返利APP的前后端分離架構中&#xff0c;跨…

【dl】python基礎 深度學習中需要用到的python基礎

直接在jupyter寫筆記然后導出md格式真的太好用了本文筆記來自小破站視頻BV1K14y1c75ePython 基礎 1. 變量 1.1 三種基本變量類型 # 字符串 str str_v "123"# 數字 int或float num_v 11 float_v 12.0# 布爾型 bool bool_v True1.1.1 字符串 f字符串&#xff1a;在…

Vue FullPage.js 完整使用指南:Vue 3 官方全屏滾動解決方案

概述 vue-fullpage.js 是 FullPage.js 的官方 Vue.js 3 包裝器&#xff0c;為 Vue 3 應用提供了強大的全屏滾動功能。該插件基于成熟的 FullPage.js 庫&#xff0c;支持多種滾動效果和豐富的配置選項&#xff0c;特別適用于企業級數據大屏、產品展示、單頁應用等場景。 官方信…

軟件工程實踐一:Git 使用教程(含分支與 Gitee)

文章目錄目標一、快速上手1. Windows 安裝 Git2. 初始化 / 克隆二、核心概念速覽三、常用命令清單1) 查看狀態與差異2) 添加與提交3) 歷史與回溯4) 撤銷與恢復&#xff08;Git 2.23 推薦新命令&#xff09;5) 忽略文件四、分支與合并&#xff08;Branch & Merge&#xff09…