寫驅動實現LED閃爍及基礎概念查看

来源:https://www.cnblogs.com/moveddown/archive/2023/07/20/17569609.html
-Advertisement-
Play Games

> 2023/7/20 初學內核,記錄與分享,感嘆內核學了後真的感覺很多東西都通透了,但是難度太大,只能淺淺初探。 # 前提 內核五大功能 ➢ **進程管理**:進程的創建,銷毀,調度等功能 註:可中斷,不可中斷,就是是否被信號打斷。從運行狀態怎樣改到可中斷等待態,和不可中斷等待態操作系統開始會對每 ...


2023/7/20 初學內核,記錄與分享,感嘆內核學了後真的感覺很多東西都通透了,但是難度太大,只能淺淺初探。

前提

內核五大功能
進程管理:進程的創建,銷毀,調度等功能
註:可中斷,不可中斷,就是是否被信號打斷。從運行狀態怎樣改到可中斷等待態,和不可中斷等待態操作系統開始會對每個進程分配一個時間片,當進程裡面寫了sleep函數,進程由運行到休眠態,但是此時CPU不可能等著。有兩種方法,1:根據時間片,CPU自動跳轉,2:程式裡面自己寫能引起CPU調度的代碼就可以
文件管理:通過文件系統ext2/ext3/ext4 yaff jiffs等來組織管理文件
網路管理:通過網路協議棧(OSI,TCP)對數據進程封裝和拆解過程(數據發送和接收是通過網卡驅動完成的,網卡驅動不會產生文件(在Linux系統dev下麵沒有相應的文件),所以不能用open等函數,而是使用的socket)。
記憶體管理:通過記憶體管理器對用戶空間和內核空間記憶體的申請和釋放
設備管理: 設備驅動的管理(驅動工程師所對應的)
字元設備驅動: (led 滑鼠 鍵盤 lcd touchscreen(觸摸屏))
1.按照位元組為單位進行訪問,順序訪問(有先後順序去訪問)
2.會創建設備文件,open read write close來訪問
✧ **塊設備驅動 ** :(camera u盤 emmc)
1.按照塊(512位元組)(扇區)來訪問,可以順序訪問,可以無序訪問
2.會創建設備文件,open read write close來訪問
網卡設備驅動:(貓)
1.按照網路數據包來收發的。

驅動

三要素:入口,出口,許可證
● 入口:資源的申請
● 出口:資源的釋放
● 許可證:GPL(寫一個模塊需要開源,因為Linux系統是開源的,所以需要寫許可協議)

1.基礎模塊

驅動格式

#include <linux/init.h>
#include<linux/module.h>
//__init將hello_init放到.init.text段中
static int __init hello_init(void)
{
    return 0;
}

//__exit將hello_exit放到.exit.text段中
static void __exit hello_exit(void)
{

}

	//告訴內核驅動的入口地址(函數名為函數首地址)
module_init(hello_init);
		//告訴內核驅動的出口地址
module_exit(hello_exit);
    	//許可證
MODULE_LICENSE("GPL");

makefile格式

//板子內核路徑
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39
 //Ubuntu內核的路徑
KERNEL_PATH=/lib/modules/$(shell uname -r)/build  

PWD=$(shell pwd)  //驅動文件的路徑
all: //目標
	make -C $(KERNEL_PATH) M=$(PWD) modules //(-C:進入頂層目錄)
   /*註:進入內核目錄下執行make modules這條命令
如果不指定 M=$(PWD) 會把內核目錄下的.c文件編譯生成.ko*/

.PHONY:clean
clean:
	make -C $(KERNEL_PATH) M=$(PWD) clean

obj-m = hello.o   //指定編譯模塊的名字

命令的使用

	創建索引文件
		ctags -R
	在終端上
		vi -t xxx
	在代碼中跳轉
		ctrl + ]
		ctrl + t
	sudo insmod hello.ko   安裝驅動模塊
	sudo rmmod  hello      卸載驅動模塊
	lsmod                  查看模塊
	dmesg                  查看消息
	sudo dmesg -C          直接清空消息不回顯
	sudo dmesg -c          回顯後清空

列印函數

概念

1. #include <linux/printk.h>  //增加這個頭文件

2. printk(KERN_ERR "Fail%d",a);//列印函數,KERN_ERR對應的是內核列印級別

3. grep "printk" * -nR  檢索所有的列印函數
	vi -t  KERN_ERR(查看內核列印級別)
4. 
	#define KERN_EMERG  "<0>"   /* system is unusable        */(系統不用)
	#define KERN_ALERT  "<1>"   /* action must be taken immediately */(被立即處理)
	#define KERN_CRIT   "<2>"   /* critical conditions          */(臨界條件,臨界資源)
	#define KERN_ERR    "<3>"   /* error conditions         */(出錯)
	#define KERN_WARNING    "<4>"   /* warning conditions           */(警告)
	#define KERN_NOTICE "<5>"   /* normal but significant condition */(提示)
	#define KERN_INFO   "<6>"   /* informational            */(列印信息時候的級別)
	#define KERN_DEBUG  "<7>"   /* debug-level messages         */ (調試級別)
	0 ------ 7
	最高的  最低的

5. 列印顯示效果是有優先順序的
inux@ubuntu:~$ cat  /proc/sys/kernel/printk 查看優先順序,分佈為以下的四個級別
終端的級別   消息的預設級別  終端的最大級別 終端的最小級別
4                4             1              7
更改方式為   echo 4 3 1 7 > /pro/sys/kernel/printk(切換成su許可權) 
如果是更改開發板列印級別  vi  rootfs/etc/init.d/rcS
					echo 4 3 1 7 > /proc/sys/kernel/printk

驅動多文件編譯

概念

hello.c  add.c
	 Makefile
	 obj-m:=demo.o
	 demo-y+=hello.o add.o
(-y作用:將hello.o add.o放到demo.o中)
	最終生成demo.ko文件

舉例

#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39
KERNEL_PATH=/lib/modules/$(shell uname -r)/build

PWD=$(shell pwd)
all:
	make -C $(KERNEL_PATH) M=$(PWD) modules
.PHONY:clean
clean:
	make -C $(KERNEL_PATH) M=$(PWD) clean

obj-m = printkk.o
demo-y+=add.o
demo-y+=sub.o
demo-y+=printkk.o

模塊傳遞函數

概念

module_param(name, type, perm) 
功能:接收命令行傳遞的參數
	參數:
		@name :變數的名字
		@type :變數的類型
		@perm :許可權  0664  0775(其它用戶對我的只有讀和執行許可權,沒有寫的許可權)
	     modinfo hello.ko(查看變數情況)

MODULE_PARM_DESC(_parm, desc)
 功能:對變數的功能進行描述
	參數:
		@_parm:變數
		@desc :描述欄位
 只能傳十進位,不可以寫十六進位
          
module_param_array(name, type, nump, perm) 
功能:接收命令行傳遞的數組
	參數:
		@name :數組名
		@type :數組的類型
		@nump :參數的個數,變數的地址
		@perm :許可權 
          

舉例

#include <linux/init.h>
#include<linux/module.h>
#include<linux/printk.h>
#include"head.h"

//入口申請資源


int a=10;
module_param(a,int,0664);
MODULE_PARM_DESC(a,"this is lcd light");
short b=11;
module_param(b,short,0774);
MODULE_PARM_DESC(b,"this is rgb");
char ch='c';
module_param(ch,byte,0664);
MODULE_PARM_DESC(ch,"this is ch val");
char *p=NULL;
module_param(p,charp,0664);
MODULE_PARM_DESC(p,"this is ch *p");
int ww[10]={0};
int num;
module_param_array(ww,int,&num,0664);
MODULE_PARM_DESC(p,"this is int array [10]");

static int __init printk_init(void)
{
    int i;
    printk("a is val=%d\n",a);
    printk("b is val=%d\n",b);
    printk("c is val=%c\n",ch);
    printk("p is val=%s\n",p);
    for (i = 0; i < num; i++)
    {
        printk("ww[%d]==%d\n",i,ww[i]);
    }
    
    return 0;
}

//出口函數釋放資源
static void __exit printk_exit(void)
{
            
  
}

//入口
module_init(printk_init);
//出口
module_exit(printk_exit);

MODULE_LICENSE("GPL");

image.png
image.png
image.png

2.字元設備驅動

概念理解

硬體與軟體的連接,需要驅動。
驅動是寫在內核層的。
應用層是調用內核層提供的介面實現的。

所以,這三者之間一定是有個東西作為聯繫的。那就是設備號。

以LED為例  字元設備的  步驟:
1.註冊字元設備驅動 - 得到一個字元設備驅動的框架,並且得到設備號
2.確定操作的硬體設備 - led燈(初始化燈)
3.初始化燈(先建立燈實際物理地址和虛擬地址之間的映射)- 基於操作系統開發,操作虛擬記憶體,
4.用戶空間數據拷貝到內核空間數據的交互(用戶使用的時候,驅動才會被真正運行,涉及數據交互)
5.在應用層創建一個設備文件(設備節點)

函數框架


int register_chrdev(unsigned int major, const char *name,
					  const struct file_operations *fops)
	功能:註冊一個字元設備驅動
	參數:@major:主設備號  
			  :如果你填寫的值大於0,它認為這個就是主設備號
			  :如果你填寫的值為0,操作系統給你分配一個主設備號
			@name :名字	cat /proc/devices 
			@fops :操作方法結構體
				返回值:major>0 ,成功返回0,失敗返回錯誤碼(負數) vi -t EIO
				major=0,成功主設備號,失敗返回錯誤碼(負數)


cat /proc/devices  查看系統自動分配的主設備號


void unregister_chrdev(unsigned int major, const char *name)
	功能:註銷一個字元設備驅動
	參數:
		@major:主設備號
		@name:名字
	返回值:無


sudo mknod led (路徑是任意) c/b  主設備號   次設備號   //手動創建設備文件


/-------數據傳遞,角度是占在用戶角度來說
int copy_from_user(void *to, const void __user *from, int n)
	功能:從用戶空間拷貝數據到內核空間(用戶需要寫數據的時候)
	參數:
		@to  :內核中記憶體的首地址
		@from:用戶空間的首地址
		@n   :拷貝數據的長度(位元組)
	返回值:成功返回0,失敗返回未拷貝的位元組的個數

int copy_to_user(void __user *to, const void *from, int n)
	功能:從內核空間拷貝數據到用戶空間(用戶開始讀數據)
	參數:
		@to  :用戶空間記憶體的首地址
		@from:內核空間的首地址
		@n   :拷貝數據的長度(位元組)
	返回值:成功返回0,失敗返回未拷貝的位元組的個數
----------/


/-----物理地址轉為虛擬地址
void * ioremap(phys_addr_t offset, unsigned long size)
功能:將物理地址映射成虛擬地址
		參數:
			@offset :要映射的物理的首地址
			@size   :大小(位元組)(映射是以業為單位,一頁為4K,就是當你小於4k的時候映射的區域都為4k)
		返回值:成功返回虛擬地址,失敗返回NULL((void *)0);


void iounmap(void  *addr)
		功能:取消映射
		參數:	
			@addr :虛擬地址
		返回值:無


----------------/

/-----設備節點自動創建
如果沒有寫節點自動創建,那麼就需要加
sudo mknod <名字> c <設備號> <子設備號>
設備號  就需要 cat /proc/devices 中查看到設備號
例如:  sudo mknod led c 230 0 
    這樣在本地創建了字元設備命名為led的字元設備文件。

struct class *cls;
cls = class_create(owner, name)	/void class_destroy(struct class *cls)//銷毀
	功能:向用戶空間提交目錄信息(內核目錄的創建)
	參數:
		@owner :THIS_MODULE(看到owner就添THIS_MODULE)
		@name  :目錄名字
	返回值:成功返回struct class *指針
			失敗返回錯誤碼指針 int (-5)	

struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)(內核文件的創建),每個文件對應一個外設(硬體設備)
			/void device_destroy(struct class *class, dev_t devt)//銷毀
	功能:向用戶空間提交文件信息
	參數:
		@class :目錄返回指針
		@parent:NULL
		@devt  :設備號 (major<<12 |0  < = > MKDEV(major,0))
		@drvdata :NULL
		@fmt   :文件的名字
	返回值:成功返回struct device *指針
			失敗返回錯誤碼指針 int (-5)


-------/

實例

實現控制LED閃爍

chrdev.c 文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/device.h>

#define NAME "chrdev_led"  //這個名字最終效果就是設備號旁的名字

//定義巨集表示實際物理首地址   這個地址是根據晶元手冊找到對應的首地址
#define RED_BASE 0xc001a000  //#GPIOA28   
#define GREE_BASE 0xc001e000 // #GPIOE13
#define BLUE_BASE 0xc001b000 //#GPIOB12

unsigned int major = 0;  //定義是為了接受創建的設備號

char kbuf[32] = {0};//實現拷貝,與應用層用戶進行數據交換的數組

//定義指針保存映射後的虛擬地址的首地址
unsigned int *red_addr = NULL;  //這樣寫的好處在於+1就是移動四個位元組,對應單片機一個寄存器的大小
unsigned int *gree_addr = NULL;
unsigned int *blue_addr = NULL;

struct class *cls = NULL;//是為了創建設備節點做的準備
struct device *dev = NULL;

int chrdev_open(struct inode *node_t, struct file *file_t)//當用戶使用fopen就會調用這句
{
    printk("%s %s %d\n", __FILE__, __func__, __LINE__);//測試用的
    return 0;
}
ssize_t chrdev_read(struct file *file_t, char __user *ubuf, size_t n, loff_t *off_t)//當用戶使用讀操作會調用這句   
{
    printk("%s %s %d\n", __FILE__, __func__, __LINE__);
    //將內核空間的數據拷貝到用戶空間
    if (sizeof(kbuf) < n)
        n = sizeof(kbuf);  //ubuf是用戶,應用層的數據
    if (copy_to_user(ubuf, kbuf, n) != 0)//當用戶使用讀操作會進入這個函數,然後再將內核數據給用戶
    {
        printk("copy_to_user err.");
        return -EINVAL;
    }
    return 0;
}
ssize_t chrdev_write(struct file *file_t, const char __user *ubuf, size_t n, loff_t *off_t)
{
    //當用戶執行寫操作驅動會進行這句操作
    printk("%s %s %d\n", __FILE__, __func__, __LINE__);
    if (sizeof(kbuf) < n)
        n = sizeof(kbuf);
    //將用戶空間的數據拷貝到內核空間
    if (copy_from_user(kbuf, ubuf, n) != 0)
    {
        printk("copy_from_user err.");
        return -EINVAL;
    }
    printk("kbuf=%s\n", kbuf);
    if (kbuf[0] == 1)//根據用戶寫的內容做出相應的操作
    {
        //紅燈亮
        *red_addr |= (1 << 28);//這就是控制對應寄存器的IO控制高低電平的
    }
    else if (kbuf[0] == 0)
    {
        //紅燈滅
        *red_addr &= (~(1 << 28));
    }
    if (kbuf[1] == 1)
    {
        //紅燈亮
        *gree_addr |= (1 << 13);
    }
    else if (kbuf[1] == 0)
    {
        //紅燈滅
        *gree_addr &= (~(1 << 13));
    }
    if (kbuf[2] == 1)
    {
        //紅燈亮
        *blue_addr |= (1 << 12);
    }
    else if (kbuf[2] == 0)
    {
        //紅燈滅
        *blue_addr &= (~(1 << 12));
    }
    return 0;
}
int chrdev_close(struct inode *node_t, struct file *file_t)//用戶執行close操作進入這句
{
    printk("%s %s %d\n", __FILE__, __func__, __LINE__);
    return 0;
}

struct file_operations fops = { //用戶之所以執行對應的操作就能跳轉對應的位置就是因為這個結構體的原因
  //這個結構體的名字就對應了驅動三模塊的申請中需要的參數
    .open = chrdev_open,
    .read = chrdev_read,
    .write = chrdev_write,
    .release = chrdev_close,
};
//入口函數-申請資源
static int __init printk_init(void)
{
    //註冊字元設備驅動
    major = register_chrdev(major, NAME, &fops);
    if (major < 0)
    {
        printk("register_chrdev err.");
        return -EINVAL;
    }
    //初始化燈-引腳功能(GPIO) 輸出功能  滅
    //1.基於操作系統開發。建立燈物理地址和虛擬地址之間映射
    //紅燈
    red_addr = (unsigned int *)ioremap(RED_BASE, 40);//基於地址開闢一定的範圍映射,實際上為4k大小
    if (red_addr == NULL)
    {
        printk("ioremap err.");
        return -EINVAL;
    }
    //gree
    gree_addr = (unsigned int *)ioremap(GREE_BASE, 40);
    if (gree_addr == NULL)
    {
        printk("ioremap gree err.");
        return -EINVAL;
    }
    blue_addr = (unsigned int *)ioremap(BLUE_BASE, 40);
    if (blue_addr == NULL)
    {
        printk("ioremap blue err.");
        return -EINVAL;
    }

    //通過虛擬地址操作實際物理地址向對應寄存器寫值
    //配置引腳GPIO
    //配置輸入輸出模式、復用選用、高點電平
    *(red_addr + 9) &= (~(3 << 24));
    *(red_addr + 1) |= (1 << 28); //輸出模式
    *red_addr &= (~(1 << 28));

    *(gree_addr + 8) &= (~(3 << 26));
    *(gree_addr + 1) |= (1 << 13);
    *gree_addr &= (~(1 << 13));

    *(blue_addr + 8) |= (1 << 25);
    *(blue_addr + 8) &= (~(1 << 24));
    *(blue_addr + 1) |= (1 << 12);
    *blue_addr &= (~(1 << 12));

    //設置自動創建設備節點
    //1.提交目錄信息
    cls = class_create(THIS_MODULE, NAME);//這個NAME為上面的巨集定義
    if (IS_ERR(cls))//判斷是否再這範圍內的函數
    {
        printk("class_create err.");
        return -EINVAL;
    }
    //2.提交文件信息
    dev = device_create(cls, NULL, MKDEV(major, 0), NULL, "led");
    if (IS_ERR(dev))
    {
        printk("class_create err.");
        return -EINVAL;
    }

    return 0;
}

//出口函數-釋放資源(先申請的後釋放,後申請先釋放)
static void __exit printk_exit(void)
{
    //銷毀創建的設備節點
    device_destroy(cls,MKDEV(major,0));
    class_destroy(cls);
    //取消映射
    iounmap(blue_addr);
    iounmap(gree_addr);
    iounmap(red_addr);
    //註銷設備驅動
    unregister_chrdev(major, NAME);
}

module_init(printk_init);
module_exit(printk_exit);
MODULE_LICENSE("GPL");
app.c文件
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
	int fd = open("/dev/led", O_RDWR);//這個是會生成在該文件

	char buf[32] = {1, 0, 0}; //buf[0]-red  buf[1]-gree  buf[2]-blue

	while (1) //紅燈亮一秒滅一秒
	{
		buf[0] = 0;
		buf[1] = 0;
		buf[2] = 0;
		write(fd, buf, sizeof(buf));
		sleep(1);
		buf[0] = 0;
		buf[1] = 0;
		buf[2] = 1;
		write(fd, buf, sizeof(buf));
		sleep(1);
		buf[0] = 0;
		buf[1] = 1;
		buf[2] = 0;
		write(fd, buf, sizeof(buf));
		sleep(1);
		buf[0] = 0;
		buf[1] = 1;
		buf[2] = 1;
		write(fd, buf, sizeof(buf));
		sleep(1);
		buf[0] = 1;
		buf[1] = 0;
		buf[2] = 0;
		write(fd, buf, sizeof(buf));
		sleep(1);
		buf[0] = 1;
		buf[1] = 0;
		buf[2] = 1;
		write(fd, buf, sizeof(buf));
		sleep(1);
		buf[0] = 1;
		buf[1] = 1;
		buf[2] = 0;
		write(fd, buf, sizeof(buf));
		sleep(1);
		buf[0] = 1;
		buf[1] = 1;
		buf[2] = 1;
		write(fd, buf, sizeof(buf));
		sleep(1);
	}

	close(fd);
	return 0;
}

Makefile文件

KERNEL_PATH=/home/hq/kernel/kernel-3.4.39   #開發板內核路徑
#KERNEL_PATH=/lib/modules/$(shell uname -r)/build
      #pc電腦上的路徑 gcc

PWD=$(shell pwd)
all:
	make -C $(KERNEL_PATH) M=$(PWD) modules       
	#在內核頂層目錄執行make modules才可以將hello.c生成驅動
	#-C 路徑:找到這個路徑執行這個命令
.PHONY:clean
clean:
	make -C $(KERNEL_PATH) M=$(PWD) clean

obj-m = chrdev.o

表現效果

a.out文件因為是在開發板運行,所以是交叉編譯工具編譯
make後會生成chrdev.ko驅動文件,進行安裝後
image.png
image.png
輸入cat proc/devices可以看到 發現,剛剛我強調的名字就是出現在這裡
image.png
對應的設備號,因為是是自動創建設備節點了,所以在輸入 cd dev
image.png就看到了led了,現在在執行./a.out 就可以看到效果了


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [toc] # 引入 - Microsoft.EntityFrameworkCore - Microsoft.EntityFrameworkCore.Design - Microsoft.EntityFrameworkCore.SqlServer - Microsoft.EntityFramewor ...
  • # 一、日誌記錄 日誌記錄是什麼?簡單而言,就是通過一些方式記錄應用程式運行中的某一時刻的狀態,保留應用程式當時的信息。這對於我們進行應用程式的分析、審計以及維護有很大的作用。 作為程式員,我們恐怕誰也不敢保證我們開發的軟體應用一定不存在BUG,一定不會出現故障,而當故障出現的時候,日誌就是我們排查 ...
  • # 基於Avalonia 11.0.0+ReactiveUI 的跨平臺項目開發2-功能開發 ![image-20230718225201652](https://www.raokun.top/upload/2023/07/image-20230718225201652.png) **項目簡介**:目 ...
  • 在使用DevExpress的GridView的時候,我們為了方便,往往使用一些擴展函數,動態創建GridView列的編輯控制項對象,然後我們可以靈活的對內容進行編輯或者使用一些彈出的對話框窗體進行處理內容的錄入,本篇隨筆就是介紹這一主題:在DevExpress的GridView的列中,動態創建列的時候... ...
  • ## 簡介 ##### IoC Ioc控制反轉,是一種設計模式和原則,旨在解耦組件之間的依賴關係,並將對象的創建和管理委托外部容器。是面向編程中一種重要的概念,用於提高代碼的可維護性. 核心思想:通過將控制權從高層轉移到底層模塊,實現對依賴關係的控制反轉,傳統上,一個對象通常負責自己的依賴項創建和管 ...
  • 本文介紹了值類型和引用類型在編程中的區別。值類型包括簡單類型、枚舉類型和結構體類型,通常被分配線上程的堆棧上,變數保存的是實例數據本身。引用類型實例則被分配在托管堆上,變數保存的是實例數據的記憶體地址。值類型由操作系統負責管理,而引用類型則由垃圾回收機制(GC)負責管理。本文還通過示例代碼展示了值類型... ...
  • ## 一:背景 ### 1. 講故事 這篇文章源自於分析一些疑難dump的思考而產生的靈感,在dump分析中經常要尋找的一個答案就是如何找到死亡線程的生前都做了一些什麼?參考如下輸出: ``` C# 0:001> !t ThreadCount: 22 UnstartedThread: 0 Backg ...
  • 本文介紹了 C# 中委托的定義、使用、為什麼引入委托以及委托的本質。同時,還介紹了委托鏈的使用,即將多個委托鏈接在一起,提高程式的可擴展性。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...