Linux驱动开发-①pinctrl 和 gpio 子系统②并发和竞争③内核定时器

Linux驱动开发-①pinctrl 和 gpio 子系统②并发和竞争③内核定时器

一,pinctrl 和 gpio 子系统

1.pinctrl子系统

在linux内核中用于管理和配置引脚复用 (比如复用为GPIO,I2C,SPI,UART等)和引脚属性(电气属性:上拉,下拉,驱动强度等)的框架,通过设备树描述引脚配置。MX6UL_PAD_GPIO1_IO03__GPIO1_IO03宏定义格式 <mux_reg conf_reg input_reg mux_mode input_val>,mux_reg寄存器偏移地址,如果值为0x01,因为 iomux基地址为0x020e0000,因此MUX复用功能寄存器的地址为0x020e0001这样,同理conf_reg 和input_reg 。mux_mode即mux复用寄存器设置的值。

c 复制代码
&iomuxc {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_hog_1>;
	imx6ul-evk {
		pinctrl_hog_1: hoggrp-1 {
			fsl,pins = <
				MX6UL_PAD_UART1_RTS_B__GPIO1_IO19	0x17059 /* SD1 CD */
				MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT	0x17059 /* SD1 VSELECT */
				MX6UL_PAD_GPIO1_IO09__GPIO1_IO09        0x17059 /* SD1 RESET */
			>;
		};

		/*LED*/
		pinctrl_led: ledgrp{
			fsl,pins = <
				MX6UL_PAD_GPIO1_IO03__GPIO1_IO03  0X10B0  /*led设置复用为GOIO1_IO03,电气属性设置为0x10B0*/
			>;
		};

}

2.GPIO子系统

pinctrl将一个pin复用为GPIO的话,使用GPIO子系统控制,作用是不用操作寄存器就能配置和操作GPIO。首先在设备树中添加这个led的节点,让这个节点使用上面配置的属性并且连接好对应的引脚。在设备树的根目录下设置gpled节点,并且用pinctrl-0 = <&pinctrl_led>;使得复用和电器属性得到设置,led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>让这个驱动和GPIO1_IO03进行关联。

c 复制代码
/ {
	gpioled{
		#adress-cells = <1>;
		#size-cells = <1>;
		compatible = "atkalpha-gpioled";
		princtrl-names = "default";
		pinctrl-0 = <&pinctrl_led>;
		led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
		status = "okay";
	};
};

驱动实现主要利用到设置的这些内容:

c 复制代码
static ssize_t pgled_write(struct file *filp, const char __user *buf,size_t cnt,loff_t *offt)
{
    int ret;
    unsigned char databuf[1];
    unsigned char let_status;
    struct led_dev *pgled_dev = filp->private_data;
    ret = copy_from_user(databuf,buf,cnt);
    let_status = databuf[0];
    if(let_status == 0)
    {
        gpio_set_value(pgled_dev->led_gpio,0);//开灯

    }else if(let_status == 1) 
    {
        gpio_set_value(pgled_dev->led_gpio,1);//关灯
    }

    return 0;
}


static int __init pgled_init(void)
{
    int ret =0;
    
    led.nd = of_find_node_by_path("/gpioled");//在设备树中获取节点
    led.led_gpio = of_get_named_gpio(led.nd,"beep-gpio", 0);//得到gpio引脚
    ret = gpio_direction_output(led.led_gpio,1);//设置GPIO1-03为输出,并且设置为1,高点平关闭
    
    
    if(ret == 0)
    {
        printk("从设备树读取节点和gpio正确\r\n");
    }

    /*注册*/
    /*1.设备号*/
    if(led.major)
    {
        led.led_hao = MKDEV(led.major,0);
        register_chrdev_region(led.led_hao, 1, LED_NAME);//主动注册
    }else{
        alloc_chrdev_region(&led.led_hao, 0, 1, LED_NAME);//自动注册
    }
    printk("major = %d,minor = %d",MAJOR(led.led_hao),MINOR(led.led_hao));

    /*2.注册函数*/
    led.cdev.owner = THIS_MODULE;
    cdev_init(&led.cdev,&pgled_fops);
    cdev_add(&led.cdev,led.led_hao,1);

    /*3.节点申请*/ 
    led.class = class_create(THIS_MODULE,LED_NAME);
    led.device = device_create(led.class, NULL,led.led_hao, NULL,LED_NAME);

    /*4.具体实现*/  
    return 0;
}

二,并发和竞争

并发:指多个任务(线程、进程或协程)在同一时间段内交替执行的现象。这些任务可能是同时运行的(在多核 CPU 上),也可能是通过时间片轮转的方式交替运行的(在单核 CPU 上)。

竞争:指多个并发任务在访问共享资源时,由于执行顺序的不确定性,导致程序的最终结果依赖于任务的执行顺序。如果未正确同步,可能会导致数据不一致或程序行为异常。下面四种操作就是为了避免访问共享资源时候出现混乱。

1.原子操作

执行这一步,不被其他线程或者内核影响,相当于我在执行这个操作时候,让一个标准位置0,其他线程或者内核想执行这个操作,一看这个标志位为0,就执行不了,等到这个操作被我执行完后,把标志位置1,从而其他可以去执行。原子操作即不可分割的操作意思。

2.自旋锁

当一个线程或核心尝试获取锁时,如果锁已被其他线程或核心持有,则该线程或核心会一直"自旋"(即忙等待),直到锁被释放。由此可以看出,其他线程或核心在一直自旋,这个时候是浪费cpu的处理能力的,因此自旋锁适合持锁时间很短的操作。特点是①不进入休眠,即其它线程或核心要一直等待着,而不是先去执行其他操作,等这个锁解了再通知回来。②中断可以用,因为中断不能休眠。

3.信号量

信号量就相当于设置一个变量,初始值,我进行这个操作时,这个变量会设置为另一个值,其他线程或者内核看到这个变量不是初始值,不会在外面一直等待,而是去执行其他操作,等我执行完这个操作后,会将这个变量变回初始值,然后通知线程和内核来执行这个操作,适合锁持有时间较长的情况。

4.互斥体

互斥体是确保同一时间只有一个线程或进程可以访问共享资源,从而避免竞态条件,当一个线程或进程尝试获取互斥体时,如果互斥体已被其他线程或进程持有,则该线程或进程会进入睡眠状态,直到互斥体被释放,和信号量基本上一样,但是互斥体是两种情况即0 1,信号量是计数器,阔以大于1。

具体使用:

c 复制代码
struct led_dev{
    struct class *class;
    struct device *device;
    struct device_node *nd;
    struct cdev cdev;
    dev_t led_hao;
    int major;//主设备号
    int minor;//次设备号
    int led_gpio;//led的gpio

    atomic_t lock_yuan;//原子操作

    spinlock_t lock_spin;//自旋操作
    int dev_status;//0可用 1不可用

    struct semaphore sem;//信号量

    struct mutex lock_huci;//互斥体

    
};
struct led_dev led;
static int pgled_open(struct inode *innode,struct file *filp)
{
    /*信号量*/
     down(&led.sem);
     /*自旋锁*/
     unsigned long flags;
     spin_lock_irqsave(&led.lock_spin,flags);//上锁
     if(led.dev_status)
     {
         spin_unlock_irqrestore(&led.lock_spin,flags);
         return -EBUSY;
     }
     led.dev_status++;//为0,则让lock------spin为1标志设备已经被使用了
     spin_unlock_irqrestore(&led.lock_spin,flags);
    
     /*原子操作*/
     if(atomic_read(&led.lock_yuan)<=0)//被操作了
     {
        return -EBUSY;
     }else {
         atomic_dec(&led.lock_yuan);
     }

    /*互斥体*/
    if (mutex_lock_interruptible(&led.lock_huci)) {
        return -ERESTARTSYS;
     }

    filp->private_data = &led;//将led结构体数据设为私有数据
    return 0;
}
static int pgled_release(struct inode *innode,struct file *filp)
{   
    up(&led.sem);//信号量
    mutex_unlock(&led.lock_huci);//互斥体

    unsigned long flags;
    /*自旋锁*/
    spin_lock_irqsave(&led.lock_spin,flags);//上锁
    if(led.dev_status)
    {
         led.dev_status--;
    }
    spin_unlock_irqrestore(&led.lock_spin,flags);

    atomic_inc(&led.lock_yuan);//原子操作
   
    return 0;
}

static int __init pgled_init(void)
{

    atomic_set(&led.lock_yuan,1);//原子操作,锁设置 当为1可以操作,否则不可以操作
    spin_lock_init(&led.lock_spin);//自旋
    sema_init(&led.sem,1);//信号量
    mutex_init(&led.lock_huci);//互斥体
}

三,按键实验

驱动:

c 复制代码
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/fs.h>  // 包含 register_chrdev_region 的定义
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/slab.h>
#include <linux/semaphore.h>

#define KEY_NAME "key"
#define KEY_VALUE  0X01
#define KEY_NOVALUE 0X00



struct key_dev{
    struct class *class;
    struct device *device;
    struct device_node *nd;
    struct cdev cdev;
    dev_t key_hao;
    int major;//主设备号
    int minor;//次设备号
    int key_gpio;//key的gpio

     atomic_t key_value;//原子操作

    // spinlock_t lock_spin;//自旋操作
    // int dev_status;//0可用 1不可用

    // struct semaphore sem;//信号量

    // struct mutex lock_huci;//互斥体

    
};
struct key_dev key;

static void  keyio_init(void)
{
    int ret = 0;
    key.nd = of_find_node_by_path("/gpiokey");//在设备树中获取节点
    key.key_gpio = of_get_named_gpio(key.nd,"key-gpio", 0);//得到gpio引脚

    gpio_request(key.key_gpio,"key0");
    ret = gpio_direction_input(key.key_gpio);//设置为输入
    if(ret == 0)
    {
        printk("从设备树读取节点和gpio正确\r\n");
    }

}
static int pgkey_open(struct inode *innode,struct file *filp)
{

    keyio_init();//初始化IO
    filp->private_data = &key;//将key结构体数据设为私有数据
    return 0;
}
static int pgkey_release(struct inode *innode,struct file *filp)
{   
    // up(&key.sem);


    //mutex_unlock(&key.lock_huci);
    // unsigned long flags;
    // /*自旋锁*/
    // spin_lock_irqsave(&key.lock_spin,flags);//上锁
    // if(key.dev_status)
    // {
    //      key.dev_status--;
    // }
    // spin_unlock_irqrestore(&key.lock_spin,flags);


    // atomic_inc(&key.lock_yuan);
    // printk("打开后  关闭:close_file  lock_yuan = %d\r\n",key.lock_yuan);
    return 0;
}
static ssize_t pgkey_read(struct file *filp, char __user *buf,size_t cnt,loff_t *offt)
{
    unsigned char value = 0;
    struct key_dev *key1 = filp->private_data;
    if(gpio_get_value(key1->key_gpio)==0)//0按下,否则没按下
    {
        while(gpio_get_value(key1->key_gpio)==0);//等待按键释放,相当于判断上升沿触发
        atomic_set(&key1->key_value,KEY_VALUE);//原子操作 数据保护
    }else {                                    //未按下
        atomic_set(&key1->key_value,KEY_NOVALUE);
    }
    value = atomic_read(&key1->key_value);
    __copy_to_user(buf,&value,sizeof(value));
    return 0;
}
./APP /dev/pgled 1&

static struct file_operations pgkey_fops={
    .owner=THIS_MODULE,
    .read=pgkey_read,
    .open=pgkey_open,
    .release=pgkey_release,
};

static int __init pgkey_init(void)
{
   

    
    atomic_set(&key.key_value,KEY_NOVALUE);//原子操作,锁设置 值为0,说明未按下


    // spin_lock_init(&key.lock_spin);//自旋

    // sema_init(&key.sem,1);//信号量
    //mutex_init(&key.lock_huci);//互斥体

    /*注册*/
    /*1.设备号*/
    if(key.major)
    {
        key.key_hao = MKDEV(key.major,0);
        register_chrdev_region(key.key_hao, 1, KEY_NAME);//主动注册
    }else{
        alloc_chrdev_region(&key.key_hao, 0, 1, KEY_NAME);//自动注册
    }
    printk("major = %d,minor = %d",MAJOR(key.key_hao),MINOR(key.key_hao));

    /*2.注册函数*/
    key.cdev.owner = THIS_MODULE;
    cdev_init(&key.cdev,&pgkey_fops);
    cdev_add(&key.cdev,key.key_hao,1);

    /*3.节点申请*/ 
    key.class = class_create(THIS_MODULE,KEY_NAME);
    key.device = device_create(key.class, NULL,key.key_hao, NULL,KEY_NAME);

    /*4.具体实现*/  
    return 0;
}
static void  __exit pgkey_exit(void)
{
    gpio_free(key.key_gpio);//释放gpio
    cdev_del(&key.cdev);//先删除设备
    unregister_chrdev_region(key.key_hao,1);//删除设备号
    device_destroy(key.class,key.key_hao);//先删除和设备第关系
    class_destroy(key.class);//再删除类
   
}
./APP /dev/pgled 1&

/*驱动入口和出口*/
module_init(pgkey_init);
module_exit(pgkey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyt");

应用:

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(unsigned char argc,unsigned char *argv[])
{
    int rel = 0,fd = 0;
    unsigned char *filename,read_buff[1];
    filename = argv[1];
    if(argc != 2)
    {
        printf("WARNING!\r\n");
        return -1;
    }
    fd = open(filename,O_RDWR);
    if(fd<0) 
    {
        printf("open file error\r\n");
        return -1;
    }

    while(1)
    {
        read(fd,read_buff,1);
        if(read_buff[0]==1)
        {
            printf(" 按下了! \r\n");
        }
    }
    return 0;

}

四,内核定时器

1.关于定时器的有关概念

1.1 节拍HZ

硬件定时器提供时钟源,时钟源的频率可以设置,设置好以后就能周期性的产生中断,系统时钟定时中断来计时,中断的这个频率就是系统频率,称为节拍,单位是HZ,比如100HZ,就是一秒有100个中断产生。

1.2 jiffies

linux内核中使用全局变量jiffies来记录系统从启动以来的节拍数,系统启动的时候会将其初始化为0,然后系统运行,就开始计数。有32位的和64位的,内核中设置时间,比如想设置定时两秒,那么目标时间=jiffies(此时的节拍数)+(2*节拍数HZ),当现在的节拍数jiffies大于目标时间,就认为到达设定时间两秒。

2.定时器实验

能够用定时器控制led灯的闪烁频率,并且能够开关定时器,修改定时器周期。static void led_timer_function(unsigned long arg)定时器调回函数中,unsigned long arg是通用的参数传递机制,传递的是用户定义的数据,将结构体指针&led强制转化为unsigned long ,放到led.timer.data中,在放到arg中,意思就是arg传递的是led结构体的数据。

c 复制代码
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/fs.h>  
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/slab.h>
#include <linux/jiffies.h>
#include <linux/ioctl.h>

#define TIMER_NAME "led_timer"

#define CLOSE_TIMER_CMD _IO(0xef, 1)  //1设置为关闭
#define OPEN_TIMER_CMD _IO(0xef, 2)  //2设置为开
#define PERIOD_TIMER_CMD _IOW(0xef, 3,int)  //3设置修改周期


struct led_dev{
    struct class *class;
    struct device *device;
    struct device_node *nd;
    struct cdev cdev;
    dev_t led_hao;
    int major;//主设备号
    int minor;//次设备号
    int led_gpio;//led的gpio
    int timer_period;//定时器周期  利用原子操作保护
    atomic_t lock_yuan;//原子操作
    struct timer_list timer;//定义定时器
    
};
struct led_dev led;
static int pgled_open(struct inode *innode,struct file *filp)
{
    filp->private_data = &led;//将led结构体数据设为私有数据
    return 0;
}
static int pgled_release(struct inode *innode,struct file *filp)
{
    return 0;
}
static long timer_ioctl(struct file *filp, unsigned int cmd , unsigned long arg)
{
    int ret = 0;
    struct led_dev *led_ioctl = (struct led_dev*)filp->private_data;
    int period =led_ioctl->timer_period;
    switch (cmd)
    {
    case CLOSE_TIMER_CMD://关闭定时器
        del_timer_sync(&led_ioctl->timer);
        break;
    case OPEN_TIMER_CMD://打开定时器
        mod_timer(&led_ioctl->timer, jiffies + msecs_to_jiffies(led_ioctl->timer_period));
        break; 
    case PERIOD_TIMER_CMD://从新配置定时器时间
        ret = __copy_from_user(&period, (int *)arg, sizeof(int));//这个arg是输入第周期值。
        printk("111period =%d, led_ioctl->timer_period =%d,led.timer_period=%d\r\n",
        period,led_ioctl->timer_period,led.timer_period);
        led_ioctl->timer_period =  period;
        mod_timer(&led_ioctl->timer, jiffies + msecs_to_jiffies(led_ioctl->timer_period));
        printk("222period =%d, led_ioctl->timer_period =%d,led.timer_period=%d\r\n",
        period,led_ioctl->timer_period,led.timer_period);

        break;         
    default:
        break;
    }

    return 0;
}
static struct file_operations pgled_fops={
    .owner=THIS_MODULE,
    .unlocked_ioctl= timer_ioctl,
    .open=pgled_open,
    .release=pgled_release,

};
static void led_timer_function(unsigned long arg)//定时结束后会执行第操作,
{
    struct led_dev *led_timer = (struct led_dev*)arg;//这个arg和上面的不一样,上面是中端输入数据的首地址,这个数据
                                                    //是整形变量周期值,不是输入的第一个数据,而这个是结构体变量led首地址
    static int status = 1;
    status = !status;
    gpio_set_value(led_timer->led_gpio,status);
    mod_timer(&led_timer->timer, jiffies + msecs_to_jiffies(led_timer->timer_period));//让定时器再从新加载

}
static int __init pgled_init(void)
{
    int ret =0;
    led.nd = of_find_node_by_path("/gpioled");//在设备树中获取节点
    led.led_gpio = of_get_named_gpio(led.nd,"led-gpio", 0);//得到gpio引脚
    ret = gpio_direction_output(led.led_gpio,1);//设置GPIO1-03为输出,并且设置为1,高点平关闭
    if(ret == 0)
    {
        printk("从设备树读取节点和gpio正确\r\n");
    }

    /*注册*/
    /*1.设备号*/
    if(led.major)
    {
        led.led_hao = MKDEV(led.major,0);
        register_chrdev_region(led.led_hao, 1, TIMER_NAME);//主动注册
    }else{
        alloc_chrdev_region(&led.led_hao, 0, 1, TIMER_NAME);//自动注册
    }
    printk("major = %d,minor = %d",MAJOR(led.led_hao),MINOR(led.led_hao));

    /*2.注册函数*/
    led.cdev.owner = THIS_MODULE;
    cdev_init(&led.cdev,&pgled_fops);
    cdev_add(&led.cdev,led.led_hao,1);

    /*3.节点申请*/ 
    led.class = class_create(THIS_MODULE,TIMER_NAME);
    led.device = device_create(led.class, NULL,led.led_hao, NULL,TIMER_NAME);

    /*初始化定时器*/
    led.timer_period=1000;
    init_timer(&led.timer);//初始化定时器
    led.timer.function = led_timer_function;
    led.timer.expires = jiffies + msecs_to_jiffies(led.timer_period);
    led.timer.data = (unsigned long)&led;
    add_timer(&led.timer);//添加定时器
    return 0;
}

static void  __exit pgled_exit(void)
{
    gpio_set_value(led.led_gpio,1);
    del_timer(&led.timer);
    gpio_free(led.led_gpio);
    cdev_del(&led.cdev);//先删除设备
    unregister_chrdev_region(led.led_hao,1);//删除设备号
    device_destroy(led.class,led.led_hao);//先删除和设备第关系
    class_destroy(led.class);//再删除类
   
}


/*驱动入口和出口*/
module_init(pgled_init);
module_exit(pgled_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyt");

应用:

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define CLOSE_TIMER_CMD _IO(0xef, 1)  //1设置为关闭
#define OPEN_TIMER_CMD _IO(0xef, 2)  //2设置为开
#define PERIOD_TIMER_CMD _IOW(0xef, 3,int)  //3设置修改周期

int main(unsigned char argc,unsigned char *argv[])
{
    int rel = 0,fd = 0,arg = 0;
	int cmd = 0;

    unsigned char *filename,str[6];
    filename = argv[1];

    fd = open(filename,O_RDWR);
    if(fd<0) printf("open file error\r\n");
    while(1)
    {
        printf("INPUT CMD:\r\n");
        rel = scanf("%d",&cmd);
        if(rel!=1)
        {
            gets(str);
        }
        switch(cmd)
        {
            case 1:
                ioctl(fd,CLOSE_TIMER_CMD,&arg);
                break;
            case 2:
                ioctl(fd,OPEN_TIMER_CMD,&arg);
                break;
            case 3:
                printf("input period:");
                rel = scanf("%d",&arg);
                printf("arg1 = %d",arg);
                if(rel!=1)
                {
                    gets(str);
                }
                ioctl(fd,PERIOD_TIMER_CMD,&arg);
                break;
            case 4:
                return 0;
            default:
                break;            
        }
    }
    rel = close(fd);
    if(rel<0) printf("close in  APP error\r\n");
    return 0;

}

五,相关问题

1.pin和gpio

PIN 是芯片或电路板上的物理引脚,用于连接芯片与外部电路。GPIO 是一种通用的引脚功能,允许引脚通过软件配置为输入或输出模式。在输入模式下,GPIO 可以读取外部信号;在输出模式下,GPIO 可以驱动外部设备。pin是个大类,gpio是其中的一部分,比如pin可以设置为多种功能(如 GPIO、UART、I2C、SPI 等)。

2.depmod作用

depmod: Linux 系统中用于生成模块依赖关系文件的工具。会扫描 /lib/modules/<内核版本>/ 目录下的所有内核模块,分析它们之间的依赖关系,依赖关系包括模块之间的符号引用(如函数、变量等)。

3.得到gpio后,使用request申请的作用

request函数用于申请 GPIO 引脚的使用权,它的主要作用是确保 GPIO 引脚不会被多个驱动程序或模块同时使用,从而避免资源冲突,具体就是标记 GPIO 引脚为"已使用"状态,防止其他驱动程序或模块重复使用该引脚。

相关推荐
周Echo周10 分钟前
2、操作系统之软件基础
linux·运维·服务器·c++·windows·ubuntu·centos
niuTaylor12 分钟前
Linux驱动开发和FreeRTOS路线总体规划
linux·运维·服务器·驱动开发
norsd35 分钟前
一次Linux下 .net 调试经历
linux·.net
___波子 Pro Max.41 分钟前
Linux mount和SSD分区
linux·mount
二年级程序员1 小时前
51单片机的工作过程
单片机·嵌入式硬件·51单片机
云山工作室1 小时前
基于单片机的智能电表设计(论文+源码)
单片机·嵌入式硬件·毕业设计
用户2587141932632 小时前
深入浅出--Linux基础命令知识(总结,配图文解释)
linux·ubuntu
风正豪2 小时前
如何向 Linux 中加入一个 IO 扩展芯片
linux·运维·单片机
如果是君2 小时前
Ubuntu20.04安装运行DynaSLAM
linux·python·深度学习·神经网络·ubuntu
对你无可奈何3 小时前
高可用环境下Nginx服务管理脚本优化实践
linux·运维·nginx