• linux按键驱动设计(V3S开发板)


    1.前言

    本文描述了基于全志V3S开发板的按键驱动程序和测试应用程序的设计流程。
    在这里插入图片描述
    本次设计系统内核是基于linux3.4。
    在这里插入图片描述

    2.设计流程概述

    本次设计的步骤是:
    步骤一、编写一个driver_button.c的驱动程序

    步骤二、编写makefile文件,编译得到ko。

    步骤三、编写一个app_button.c的测试应用程序。

    步骤四、在V3S开发板中安装demo_driver驱动程序,并测试app_button应用程序。
    在这里插入图片描述

    3.编写驱动程序

    3.1硬件电路

    V3S开发板按键的硬件电路原理图如下:
    在这里插入图片描述
    根据原理图可知不同的按键按下对应不同的电压值,使用ADC采样LRADC0处的电压就可以识别是否有按键被按下,例如按键KEY3按下时LRADC0处的电压为0.6V。

    我们查看V3S芯片数据手册,看看如何操作V3S芯片LRADC,V3S芯片关于LRADC的关键信息如下(详细数据请参考数据手册):
    在这里插入图片描述
    在这里插入图片描述
    LRADC的控制寄存器信息如下(LRADC寄存器详细数据请参考数据手册):
    在这里插入图片描述
    我们只需要把LRADC配置成连续采样模式即可,具体配置如下:

    LRADC_CTRL = 0x00C00041
    LRADC_INTC = 0x00000013
    
    • 1
    • 2

    3.2设计要点

    本次按键驱动设计使用了以下两个知识点:

    1、内核定时器使用
    2、阻塞操作 (等待队列)

    我们先学习一下这两个知识点。
    在这里插入图片描述

    3.2.1内核定时器

    Linux内核定时器是一种基于时间点的计时方式,它以当前时刻为时间起点,以未来的某一时刻为终点,这种策略类似于闹钟。
    在这里插入图片描述
    Linux内核定时器的精度不高,不能作为高精度定时器使用。内核定时器并不是周期性运行,如果要想实现周期性的定时,就需要在定时中断处理函数中重新开启定时器。

    使用Linux内核定时器需要增加如下头文件:

    #include   		
    #include 
    
    • 1
    • 2

    Linux内核定时器使用分为以下4个步骤:

    1、声明定义一个定时器

    struct timer_list timer; 
    
    • 1

    2、初始化定时器

    /* 初始化定时器 */
    init_timer(&timer); 
    /* 设置定时处理函数 */
    timer.function = time_callback; 
    /* 设置定时时间 */
    timer.expires=jiffies + msecs_to_jiffies(100); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3、声明定义定时器中断回调函数
    在回调函数中再次设置定时时间,实现周期定时

    void time_callback(unsigned long arg)
    {
    	/* 设置定时器 */	
    	mod_timer(&timer, jiffies + msecs_to_jiffies(TIME_NUM));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4、启动定时器

    add_timer(&timer); 
    
    • 1

    通过以上的4个步骤,我们就可以正常使用Linux内核定时器了。
    在这里插入图片描述

    3.2.2阻塞操作

    相信大家去吃一些美味小食时都排队的经历吧,这种漫长的等待经历是不是还历历在目?除了在队伍里傻站着和刷刷手机,啥也干不了。
    在这里插入图片描述
    但是有些商场就有一种很好的等待机制:无线提示器。客户点餐后就可以在一定范围内自由活动,当你点的餐做好了,无线提示器会提示你去取餐。这种方式可以极大的减轻客户站立排队的低效和痛苦。
    回到上面排队的例子,第一种站立式排队模式,当你处在这种等待模式下时你必须一直保存这种排队等待的状态,直到你买到你需要的商品。这种等待方式属于非阻塞式等待。
    第二种无线提示器模式,当你完成商品付款后,就可以在一定范围内自由活动,直到收到无线提示器会提示你。这种等待方式属于阻塞式等待。
    非常明显在大多数情况下阻塞式等待会更加高效。

    我们使用linux等待队列实现阻塞操作,当事件未准备好时任务进入挂起状态,当事件准备好时唤醒任务。
    使用等待队列需要包含如下头文件:

    #include  		
    #include 
    
    • 1
    • 2

    linux等待队列操作分为以下4个步骤:
    1、定义等待队列和等待标志

    wait_queue_head_t button_wait; 
    bool  key_ready = false;
    
    • 1
    • 2

    2、初始化等待队列

    init_waitqueue_head(&button_wait);
    
    • 1

    3、进入等待

    wait_event(button_wait, key_ready);
    
    • 1

    4、唤醒任务
    需要在另外一处可以执行到的代码出执行唤醒代码

    key_ready = true;
    wake_up(&button_wait);
    
    • 1
    • 2

    通过以上的4个步骤,我们就可以正常使用Linux等待队列进行阻塞操作了。
    在这里插入图片描述

    3.3按键驱动实现

    描本次按键驱动程序策略如下:
    1、应用程序通过驱动程序的read函数读取按键值,当检测有按键被按键时read函数返回按键值,当无按键按下时read函数使用等待队列让应用程序挂起(不继续运行)。

    2、驱动程序中设置一个周期为10ms内核定时器,在定时器中断回调函数中检测按键是否被按下,并做去抖操作。如果在定时器中断回调函数中确定有按键被按下,此时唤醒因为等待按键被挂起的应用程序。

    策略框图如下:
    在这里插入图片描述

    3.4驱动代码

    编写一个demo_driver.c的驱动程序,驱动程序源码如下(代码不再作解释说明,可以参数代码注释):

    /**
    *********************************************************************************************************
    *                                        		driver_button
    *                                      (c) Copyright 2021-2031
    *                                         All Rights Reserved
    *
    * @File    : 
    * @By      : liwei
    * @Version : V0.01
    * 
    *********************************************************************************************************
    **/
    
    /**********************************************************************************************************
    Includes 
    **********************************************************************************************************/
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #include    		/*  io remap */
    #include 
    
    #include   		/* time */
    #include 
    
    #include 		/* copy */  
    
    #include  		/* wait */
    #include 
    /**********************************************************************************************************
    Define
    **********************************************************************************************************/
    #define    DRIVER_MAJOR     232
    #define    DEVICE_NAME     "driver_button"
    
    #define 	BASE_ADDRESS 0x01C22800
    
    #define   IOC_MAGIC  'w'
    #define   IOCTL_TEST_ON   _IO(IOC_MAGIC,0)
    #define   IOCTL_TEST_OFF   _IO(IOC_MAGIC,1)
    
    
    #define BUTTON_NULL 	(0X3F)
    #define BUTTON_HOME 	(0X04)
    #define BUTTON_NEXT 	(0X0B)
    #define BUTTON_PREV 	(0X11)
    #define BUTTON_ENTER 	(0X18)
    
    #define TIME_NUM 	(10)
    #define FILTER_NUM 	(5) 
    
    /**********************************************************************************************************
    Typedef
    **********************************************************************************************************/
    
    
    /**********************************************************************************************************
    Variables
    **********************************************************************************************************/
    /* gpio寄存器 */
    static volatile unsigned char __iomem		*base_res;    	
    static volatile unsigned char __iomem		*adc_base;
    static volatile unsigned char __iomem		*adc_data;
    static volatile unsigned char __iomem		*adc_ctl;
    static volatile unsigned char __iomem		*adc_irq;
    		
    struct timer_list timer; /* 定义定时器 */
    wait_queue_head_t button_wait; /* 等待队列 */
    bool  key_ready = false;
    
    static unsigned char user_key =  BUTTON_NULL;
    static unsigned char last_key = BUTTON_NULL;
    static unsigned char handle_key = BUTTON_NULL;
    
    /**********************************************************************************************************
    Function 
    **********************************************************************************************************/
    
    
    /***********************************************************************************************************
    * @描述	: 键值处理
    ***********************************************************************************************************/
    void get_key_value(unsigned char value)
    {
    	switch(value)
    	{
    		case BUTTON_HOME:
    			user_key = 1;
    		break;
    		case BUTTON_NEXT:
    			user_key = 2;
    		break;
    		case BUTTON_PREV:
    			user_key = 3;
    		break;
    		case BUTTON_ENTER:
    			user_key = 4;
    		break;
    		default :
    		break;		
    	}		
    }
    /***********************************************************************************************************
    * @描述	: 按键处理函数 
    ***********************************************************************************************************/
    void button_handle(void)
    {
    	static unsigned char null_key_num = 0;
    	static unsigned char valid_key_num = 0;
    	/*判断按键是否为空*/
    	if(*adc_data != BUTTON_NULL)
    	{
    		null_key_num = 0;
    		last_key =  *adc_data;
    		/*判断与上次有效键值是否一致*/
    		if(handle_key != last_key)
    		{	
    			/*多次判断,去抖操作*/
    			valid_key_num++;
    			if(valid_key_num > FILTER_NUM)
    			{
    				valid_key_num = 0;
    				handle_key = last_key;
    				/*获取键值*/
    				get_key_value(handle_key);
    				/*唤醒等待任务*/
    				key_ready = true;
    				wake_up(&button_wait);				
    			}			
    		}
    	}
    	else
    	{
    		/*多次判断,去抖操作*/
    		null_key_num++;
    		valid_key_num = 0;
    		if(null_key_num > FILTER_NUM)
    		{
    			/*按键为空*/
    			null_key_num = 0;
    			last_key = 	BUTTON_NULL;
    			handle_key =  BUTTON_NULL;
    			
    		}		
    	}		
    }
    /***********************************************************************************************************
    * @描述	: 定时器回调函数 
    ***********************************************************************************************************/
    void time_callback(unsigned long arg)
    {
    	/* 设置定时器 */	
    	mod_timer(&timer, jiffies + msecs_to_jiffies(TIME_NUM));
    	/* 根据ADC数值 获取按键值 */
    	button_handle();
    }
    /***********************************************************************************************************
    * @描述	:  初始化函数
    ***********************************************************************************************************/ 
    void time_init(void)
    {
    	/* 初始化定时器 */
    	init_timer(&timer); 
    	/* 设置定时处理函数 */
    	timer.function = time_callback; 
    	/*  超时时间 2 秒 */
    	timer.expires=jiffies + msecs_to_jiffies(TIME_NUM); 
    	/*  启动定时器 */
    	add_timer(&timer); 
    }
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    static int ioremap_init(void)
    {
        base_res = request_mem_region(BASE_ADDRESS , 0x10 , "GPIOE_MEM");
    	
    	if(base_res == NULL)
    	{
    		printk("request_mem_region BASE_ADDRESS,0x28 fail\n");		
    	}
        else
    	  printk(KERN_EMERG DEVICE_NAME " ======================request_mem_region  ok ======================\n");
    	
        /* IO映射 */
    	adc_base = (unsigned char*)ioremap(BASE_ADDRESS , 0x10);
    
    	if(adc_base == NULL)
    	{	
    		printk("ioremap BASE_ADDRESS,0x28 fail\n");	
    	}	
      	else
    	  printk(KERN_EMERG DEVICE_NAME " ======================ioremap  ok ======================\n");
    
        /* 寄存器地址映射 */
    	adc_ctl		=(unsigned int*)(adc_base + 0x00);
    	adc_irq		=(unsigned int*)(adc_base + 0x04);
    	adc_data 	=(unsigned int*)(adc_base + 0x0c);
    	return 0;	
    }
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    static int adc_init(void)
    {
    	*adc_ctl = 0x00C00041;
    	*adc_irq = 0x00000013;	
    	return 0;	
    }
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    static int button_open(struct inode *inode, struct file *file)
    {
    	printk(KERN_EMERG "======================open======================\n");
    	/* IO资源申请 */
    	ioremap_init();
    	/* 初始化adc  */
    	adc_init();
    	/* 初始化定时器  */
    	time_init();
    	/* 初始化等待队列  */	
    	init_waitqueue_head(&button_wait);
    	return 0;
    }
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    static ssize_t button_write(struct file *file, const char __user * buf, size_t count, loff_t *ppos)
    {
        printk(KERN_EMERG "======================write======================\n");
        return 0;
    }
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    static ssize_t button_read(struct file *file,  char __user * buf, size_t count, loff_t *ppos)
    {
    	char buff[20];
    	char num;
    	/* 判断按键是否有效 */	
    	if(user_key == BUTTON_NULL )	
    	{
    		/* 无有效按键时,任务进入等待 */
    		printk(KERN_EMERG "====================== wait : null key ======================\n");
    		wait_event(button_wait, key_ready);
    	}
    	key_ready = false;
    	/* 有效按键唤醒任务 */
    	printk(KERN_EMERG "====================== wake up :valid key ======================\n");
    	num = sprintf(buff,"%s%d","key = ", user_key);	
    	copy_to_user(buf , buff , num);
    	/* 清空按键数值 */
    	user_key = BUTTON_NULL;
    	memset(buff,0,20);
    
        return 0;
    }
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    static int button_close(struct inode *inode, struct file *file)
    {
    	/* 删除定时器 */
    	del_timer(&timer); 	
        printk(KERN_EMERG "======================close ======================\n");
        return 0;
    }
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    static int button_ioctl( struct file *file,  unsigned int  cmd, unsigned long arg)
    {
        printk(KERN_EMERG "======================ioctl ======================\n");	
        return 0;
    }
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    static struct file_operations driver_flops = 
    {
    	.owner  =   THIS_MODULE,
    	.open   =   button_open,     
    	.write  =   button_write,
    	.read 	=  	button_read,
    	.unlocked_ioctl = button_ioctl,
    	.release =  button_close,
    };
    static struct cdev dev;
    static     dev_t devno;
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    static int __init driver_button_init(void)
    {
        int ret;
        /* 申请设备号 */
       	ret = alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
    	if(ret < 0) 
    	{
    		pr_err("alloc_chrdev_region failed!");
    		return ret;
    	}
    	printk("MAJOR is %d\n", MAJOR(devno));
    	printk("MINOR is %d\n", MINOR(devno)); 
    	/* 注册设备 */
    	cdev_init(&dev,  &driver_flops);
    	ret = cdev_add(&dev ,  devno, 1);
    	if (ret < 0) 
    	{
    		pr_err("cdev_add failed!");
    		return ret;
    	}
        printk(KERN_EMERG DEVICE_NAME " ======================driver_button_init======================\n");
    	return 0;
    }
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    static void __exit driver_button_exit(void)
    {
        /* 注销设备 */
    	cdev_del(&dev);
        /* 释放设备号 */
    	unregister_chrdev_region(devno, 1);
        printk(KERN_EMERG DEVICE_NAME " ======================driver_button_exit======================\n");
    }
    
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    module_init(driver_button_init);
    module_exit(driver_button_exit);
    MODULE_LICENSE("GPL");
    /***********************************************END*******************************************************/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339

    4.编写makefile

    本次设计通过单编ko的方式得到驱动ko,单编ko的详细说明请参考我的另外一篇文章《如何编译linux驱动ko》。本次设计的Makefile代码如下(根据开发环境指定内核路径和编译工具路径):

    .PHONY:    main  clean
    
    KERNELDIR   :=   /home/liwei/v3_work/project/linux-3.4
    PWD   :=   $(shell pwd)
    CROSS_ARCH := /home/liwei/v3_work/tools/host/bin/arm-buildroot-linux-gnueabihf-gcc
    
    obj-m   +=   driver_button.o
    	
    main: 
    	$(MAKE) $(CROSS_ARCH) -C  $(KERNELDIR)   M=$(PWD)   modules 
    	
    clean: 
    	rm   -rf   *.o   *~   core   .depend   .*.cmd   *.ko   *.mod.c   .tmp_versions *.symvers *.d *.markers *.order
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    将driver_button.c和上述的Makefile文件放在同一个目录下(路径为任意路径,不需要一定放在内核目录中),执行make指令,最终我们得到了驱动ko 。
    在这里插入图片描述

    5.编写应用程序

    编写一个demo_app.c的应用程序,程序源码如下:

    /**
    *********************************************************************************************************
    *                                        		demo
    *                                      (c) Copyright 2021-2031
    *                                         All Rights Reserved
    *
    * @File    : 
    * @By      : liwei
    * @Version : V0.01
    * 
    *********************************************************************************************************
    **/
    
    /**********************************************************************************************************
    Includes 
    **********************************************************************************************************/
    #include  
    #include  
    #include 
    #include 
    #include  
    #include  
    #include 
    
    
    /***********************************************************************************************************
    * @描述	:  
    ***********************************************************************************************************/
    int main(int arvc, char *argv[])
    {
    	int fd;
    	int buff[20] ;
    	int num;
    	printf("==========button test==================\n");
    	//打开驱动 
    	fd = open("/dev/driver_button",O_RDWR);
    
    	while(1)
    	{
    		//执行驱动读操作
    		num= read(fd,buff,15);	
    		printf("run:%s\r\n",buff);
    		//延时等待串口发送数据 ,清发送BUFF
    		sleep(1);
    		memset(buff,0,20);
    	}
    	
    	return 0;
    }
    /***********************************************END*******************************************************/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    应用程序源码用于测试按键驱动功能,在测试程序在一直循环读取按键驱动数据,并将按键数据打印出来。

    我们将应用程序源码放在虚拟机的任意一个目录中,并在该目录下执行如下gcc编译指令:

    arm-buildroot-linux-gnueabihf-gcc app_button.c  -o app_button
    
    • 1

    于是我们得到一个可执行文件app_button
    在这里插入图片描述

    6.安装驱动及运行应用程序

    目前我们得到了driver_button.ko和app_button两个文件,我们使用SecureCRTPortable工具将这两个文件传输到V3开发板中。
    在这里插入图片描述
    执行安装驱动指令:

    insmod driver_button.ko 
    
    • 1

    在这里插入图片描述
    执行创建文件节点指令:

    mknod /dev/driver_button c 248 0
    
    • 1

    在这里插入图片描述
    执行修改app_button文件权限指令:

    chmod 777 app_button
    
    • 1

    执行运行demo_app指令:

    ./app_button
    
    • 1

    在这里插入图片描述
    测试按键结果如下:
    在这里插入图片描述

    7.总结

    本文本文描述了基于全志V3S开发板的按键驱动程序和测试应用程序的设计流程,本次设计细节如下:

    1、硬件电路通过ADC识别按键。
    2、使用内核定时器周期查询按键是否按下。
    3、使用等待队列休眠等待按键的应用。

    创作不易希望朋友们点赞,转发,评论,关注。
    您的点赞,转发,评论,关注将是我持续更新的动力
    作者:李巍
    Github:liyinuoman2017
    CSDN:liyinuo2017
    今日头条:程序猿李巍

    在这里插入图片描述

  • 相关阅读:
    Qt 设置CPU亲缘性,把进程和线程绑定到CPU核心上(Linux)
    学习react,复制一个civitai(C站)-更新3
    Redis安装与配置、centos虚拟机上配置自启动redis服务
    新版软考高项试题分析精选(二)
    NLP(文本处理技术)在数据分析中的应用实例
    R语言ggplot2可视化分面图(facet):gganimate包基于transition_time函数创建动态散点图动画(gif)
    MySQL 8.*版本 修改root密码报错
    rtsp转webrtc的其他几个项目
    Dubbo-Activate实现原理
    第二章、数据结构和算法9分
  • 原文地址:https://blog.csdn.net/li_man_man_man/article/details/126912214