目录

  • 第二章 内核模块
    • 宏内核和微内核
    • 内核模块程序的初始化和退出函数原型
    • 内核模块的相关工具
    • 内核模块基本框架(内核最原始的结构)
    • 多个源文件编译生成一个内核模块
    • 内核模块参数(参数类型要注意)
    • 内核模块依赖
    • 内核模块和普通应用程序之间的差异(简答题)
  • 第三章 字符设备驱动
    • 设备驱动的种类
    • 不同设备驱动的特点
    • 字符设备驱动基础
      • 主设备号和次设备号
    • 字符设备驱动框架(编程题)
    • 虚拟串口设备操作
    • 一个驱动支持多个设备
  • 第四章 高级I/O操作
    • ioctl 设备操作
    • proc 文件操作
    • 非阻塞型I/O
    • 阻塞型I/O
    • I/O多路复用
    • 异步I/O
    • 对四种I/O模型的总结(问答题)
    • 异步通知
    • mmap 设备文件操作
  • 编程题

第二章 内核模块

宏内核和微内核

宏内核和微内核:linux是宏内核的代表,Windows是微内核的代表。区别是宏内核所有的内核功能被整体编译在一起,形成一个单独的内核镜像文件。

内核模块:linux引入了内核模块。内核模块是单独编译的一段内核代码,在需要的时候动态加载,不需要的时候动态卸载,动态增加内核功能。内核模块有助于减小内核镜像文件的体积,减少内核占用的内存空间。(当然内核模块不一定都是驱动程序,驱动程序也不一定都是内核模块

内核模块程序的初始化和退出函数原型

// 一个模块程序几乎都要直接或间接使用这三个头文件
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>// 模块的初始化函数原型
int init_module(void)
{printk("模块初始化了");return 0;
}
// 模块的清除函数原型
void cleanup_module(void)
{printk("模块卸载了");
}

内核模块的相关工具

1、模块加载
insmod vser.ko
insmod命令加载指定目录下的.ko文件到内核,需要指定路径,默认是在当前目录找。
depmod
modprobe vser
modprobe命令智能加载,只需要使用加载的模块名称,不需要加后缀,但是需要使用前用depmod命令更新依赖关系,这样可以把有依赖关系的模块一起加载上,而insmod不可以。但是使用这种方式必须将模块放置到指定目录下。

2、模块信息
modinfo vser
查看模块的信息。
3、模块卸载
rmmod vser
如果内核配置为允许卸载模块,将指定的模块从内核中卸载。

内核模块基本框架(内核最原始的结构)

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>// 使用static关键字,因为static关键字修饰的函数的链接属性为内部,解决了内核模块加载到内核之后函数名可能会重复定义的问题。
// 使用 _init 修饰,当函数调用成功后,模块的加载程序会释放掉这部分空间,节约内存
static int _init vser_init(void)
{printk("模块初始化了");return 0;
}
// 使用 _exit 修饰,如果模块不允许卸载,那么这段代码完全不需要加载
static void _exit vser_exit(void)
{printk("模块卸载了");
}
module_init(vser_init);
module_exit(vser_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("NAPO <2661273650@qq.com>");
MODULE_DESCRIPTION("A Module");
MODULE_ALIAS("napo_module");

模块加载可以使用别名,例如:modprobe virtual-serial

多个源文件编译生成一个内核模块

// foo.c文件
extern void bar(void); // 需要使用extern关键字声明该函数实现在外部,否则编译不通过
// .....// bar.c文件
// 在bar文件中实现bar函数
void bar(void)
{printk("我执行了");
}

需要在Makefile中进行修改

obj-m := vser.o # 依然是生成的模块
vser-objs = foo.o bar.o # 表示vser模块是由foo.o和bar.o两个目标文件共同生成的

内核模块参数(参数类型要注意)

已知模块初始化函数在模块加载时被调用,但是函数是不接受参数的,所以需要想办法通过命令行向其传递参数,这就产生了模块参数。(加载模块时通过命令行向其传递参数值

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>// 声明三个变量,其有默认值,我们的目标是模块加载时改变他们的值
static int baudrate = 9600;
static int port[4] = {0, 1, 2, 3}
static char* name = "vser";// 使用以下的宏代码表示可以在命令行中改变变量的内容
module_param(baudrate, int, S_IRUGO);   # S_IRUGO 表示 USER,GROUP,OTHER可读.
module_param_array(port, int, NULL, S_IRUGO);
module_param(name, charp, S_IRUGO);
module_param(name, type, perm); # 单个变量使用的宏.
module_param_array(name, type, nump,perm); # 数组使用的宏.
// name:变量名 type:变量类型 nump:数组元素个数的指针,可选 perm:sysfs文件系统中的文件权限属性

参数的类型有:bool、invbool(反转值bool)、charp(字符指针)、short、int、long、ushort(u表示无符号)、uint、ulong。以及它们对应的数组类型。

指定模块参数的值:modprobe vser baudrate=115200 port=1,2,3,4 name="virtual-serial"

dmesg:获得硬件信息和内核启动时的信息。

内核模块依赖

需要注意的是一个宏:EXPORT_SYMBOL(param) 如同其名字就是导出一个符号,这个符号可以是函数名或者是变量名,将其导出之后可以在别的模块中使用。

/*vser.c*/
....
extern int param;
extern int param_fun(void);
..../*dep.c*/
static int param = 5;
static void param_fun(void)
{printk("我执行了");
}
EXPORT_SYMBOL(param);
EXPORT_SYMBOL(param_fun);
// 两个模块需要一起编译,要不然的话vser.c并不知道dep.c的存在,所以依然找不到符号。
// 卸载时需要先卸载vser模块再卸载dep模块,要不然可能会因为vser依赖dep模块,而导致dep模块不能被卸载。

内核模块和普通应用程序之间的差异(简答题)

(1)内核模块是操作系统内核的一部分,运行在内核空间,应用程序运行在用户空间(主要不同)。

(2)内核模块中的函数是被动的被调用的,比如初始化函数和清除函数分别在模块加载和模块卸载时被调用。

(3)内核模块处于C函数库之下,不能调用C库函数,应用程序可以调用。

(4)内核模块需要做一些清除性的工作,而应用程序有些不需要

(5)内核模块如果产生了非法访问一般导致系统崩溃,应用程序一般只影响自己

第三章 字符设备驱动

设备驱动的种类

(1)字符设备驱动

(2)块设备驱动

(3)网络设备驱动

不同设备驱动的特点

(1)字符设备驱动对数据的处理是按照字节流形式进行的,而块设备驱动是按照若干个块进行的,每个块有固定的大小。

(2)字符设备驱动可以支持随机访问,也可以不支持,块设备驱动都支持随机访问。

(3)字符设备驱动数据流量通常不大,一般没有页高速缓存,而块设备驱动一般有。

(4)网络设备驱动针对网络设备,主要是进行网络数据的收发。

字符设备驱动基础

设备文件通常位于/dev目录下,b 表示块设备,c 表示字符设备

主设备号和次设备号

通常内核用主设备号区别一类设备,次设备号用于区分同一类设备的不同个体或不同分区。路径名则是用户层用于区别设备的。

比如:sda、sda1、sda2、sda3 是块设备 sda表示的整个硬盘,其余的表示的是不同的分区 tty0、tty1是终端设备

使用mknod命令创建一个设备文件:mknod /dev/vser0 c 256 0 指定了设备结点的路径,类型为字符设备,和主次设备号

驱动程序通过主次设备号与设备文件进行关联,通过操作设备结点文件,使用编写的驱动程序中的操作映射到对硬件的操作。

字符设备驱动框架(编程题)

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>#define VSER_MAJOR 256
#define VSER_MINOR 0
#define VSER_DEV_CNT 1
#define VSER_DEV_NAME "vser"static struct cdev vsdev; // 表示了一个具体的字符设备static struct file_operations vser_ops = {  // 表示了操作字符设备的一些方法.owner = THIS_MODULE
}static int _init vser_init(void)
{int ret;dev_t dev;// 使用宏MKDEV将主设备号和次设备号合并成一个设备号,高12位为主设备号,低20位为次设备号dev = MKDEV(VSER_MAJOR, VSER_MINOR);// 第一个参数指定起始设备号,第二个参数指定申请个数,第三个参数标记主设备号的名称// 为了解决设备号冲突的问题,动态分配设备号函数:alloc_chrdev_region(dev, baseminor, count, name)ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);if(ret) goto reg_err;// 初始化字符设备,将设备与操作关联,第一个参数为设备地址,第二个参数为操作结构体地址cdev_init(&vsdev, &vser_ops); vsdev.owner = THIS_MODULE;// 将设备添加到散列表中去,第一个参数为设备地址,第二个参数为设备号,第三个参数为cdev对象可以管理多少设备// 一个对象可以管理多个设备ret = cdev_add(&vsdev, dev, VSER_DEV_CNT);if(ret) goto add_err;return 0;
reg_err:return ret;
add_err:// 如果添加对象失败的话注销掉设备号unregister_chrdev_region(dev, VSER_DEV_CNT);
}static void _exit vser_exit(void)
{dev_t dev;dev = MKDEV(VSER_MAJOR, VSER_MINOR);// 模块卸载时删除掉设备对象cdev_del(&vsdev);// 注销掉设备号unregister_chrdev_region(dev, VSER_DEV_CNT);
}
module_init(vser_init);
module_exit(vser_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("NAPO <2661273650@qq.com>");
MODULE_DESCRIPTION("A Module");
MODULE_ALIAS("napo_module");

虚拟串口设备操作

内核中有一个结构体 struct kfifo

DEFINE_KFIFO(fifo, type, size);  // 初始化一个虚拟串口缓冲区
kfifo_from_user(fifo, from, len, copied);
kfifo_to_user(fifo, to, len, copied);

一个驱动支持多个设备

#define VSER_DEV_CNT 2   // 定义2个设备
static DEFINE_KFIFO(vsfifo0, char, 32);
static DEFINE_KFIFO(vsfifo1, char, 32);  // 申请2个空间struct vser_dev {struct kfifo *fifo;struct cdev cdev;
};static struct vser_dev vsdev[2];  // 创建2个对象并为其分配空间switch(MINOR(inode->i_rdev)){case 0:filp->private_data = &vsfifo0;break;case 1:filp->private_data = &vsfifo1;break;   }   // 根据次设备号的值确定保存哪个FIFO结构的地址到file结构中的private_data成员中。

第四章 高级I/O操作

ioctl 设备操作

ioctl函数原型:int ioctl(int fd, int cmd, ...); 第一个参数为文件描述符,第二个是要对设备操作的命令,第三个是可选的参数

一般在ioctl中使用switch语句对cmd命令进行条件分支,对不同的命令执行不同的操作。

proc 文件操作

proc文件系统是一种伪文件系统。它不存在于磁盘上,只存在于内存中,只有内核运行时才会动态生成里面的内容。

proc_mkdir("vser", NULL);  // 建一个vser目录
proc_create_data("info", 0, vsdev.pdir, &proc_ops, &vsdev);   // 建一个info文件

非阻塞型I/O

若应用程序以非阻塞型方式打开设备文件,当资源不可用时,驱动应立即返回并返回一个错误码。

int fd = open("/dev/vser0", O_RDWR | O_NONBLOCK); // O_NONBLOCK标志表示以非阻塞方式打开设备文件

阻塞型I/O

若应用程序以非阻塞型方式打开设备文件,当资源不可用时,则进程阻塞,让其他进程运行,有利于提高系统效率。

wait_event_interruptible() 等待阻塞

wake_up_interruptible() 唤醒阻塞

I/O多路复用

可以方便的实现一个进程同时对多个设备进行操作。

I/O多路复用有select、poll、epoll(Linux特有)三种方式

常见事件:

POLLIN:设备可以无阻塞读

POLLOUT:设备可以无阻塞写

异步I/O

异步I/O在提交完I/O操作请求后立即返回,程序不需等到I/O操作完成再去做别的事,具有非阻塞特性。

// 1
struct aiocb aiow, aior;         // 定义2个分别用于写/读的异步I/O控制块
memset(&aiow, 0, sizeof(aiow));
memset(&aior, 0, sizeof(aior));  // 初始化这2个控制块
aiow.aio_fildes = fd;
aiow.aio_buf = malloc(32);// 2
strcpy((char *)aiow.aio_buf, "aio test");
aiow.aio_nbytes = strlen((char *)aiow.aio_buf) + 1;
aiow.aio_offset = 0;// 3
aiow.aio_sigevent.sigev_notify = SIGEV_THREAD;  //(SIGEV_THREAD:通知信号)
aiow.aio_sigevent.sigev_notify_function = aior_completion_handler;
aiow.aio_sigevent.sigev_notify_attributes = NULL;// 4
aiow.aio_sigevent.sigev_value.sival_ptr = &aiow; // 指向I/O控制块aiow

对四种I/O模型的总结(问答题)

1、阻塞I/O:资源不可用时,进程阻塞,资源可用之后进程被唤醒,阻塞期间不占用CPU,时最常用的方式。(同步阻塞)

2、非阻塞I/O:不管资源是否可用,调用后立即返回,通过返回值判断I/O操作是否成功。(同步非阻塞)

3、I/O多路复用:可以同时监听多个设备的状态,如果所有被监听的设备都没有关心的事件发生,系统调用被阻塞,如果任何一个设备有关心的事件发生,就会唤醒系统调用,系统调用将会再次遍历所监听的设备,获取其事件信息,然后返回。之后可以对设备进行非阻塞的读或写。(异步阻塞)

4、异步I/O:调用者发起异步I/O操作请求,然后立即返回,I/O操作完成后程序会收到通知。(异步非阻塞)

异步通知

异步通知:当设备资源可获取时,由驱动主动通知应用程序,再由应用程序发起访问,类似于中断机制。

异步通知的应用程序实现步骤:

1,注册信号函数

2,打开设备文件

3,设置设备资源可用时驱动向进程发送的信号

4,设置文件的FASYNC标志,使能异步通知机制

mmap 设备文件操作

mmap做为系统调用实现了内存映射,可以实现用户空间和内核空间内存的映射。可以将文件映射到内存中,实现用内存读写代替I/O操作。

remap_pfn_range 函数是其中的核心函数。

编程题

写一个Linux驱动程序,要满足以下要求:

1) 内核中定义一个字符数组iBuf,用来贮存数据;

2) 打开设备时对iBuf清空;

3) 写设备时,把字符串存入iBuf;

4) 读设备时,从iBuf读出所有数据;

5) ioctl控制函数实现四个功能:

​ a) 命令代码1,无参数,把iBuf中的字母转成小写;

​ b) 命令代码2,无参数,把iBuf中的字母转成大写;

​ c) 命令代码3,无参数,按照ASCII码升序排列;

​ d) 命令代码4,无参数,按照ASCII码降序排列;

6)写出调用驱动示例程序

/*驱动*/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>#define VSER_MAJOR 256
#define VSER_MINOR 0
#define VSER_DEV_CNT 1
#define VSER_DEV_NAME "driver"#define LOWWERCASE 0        // 或者用宏 _IO('s', 1)
#define UPPERCASE 1         // _IO('s', 2)
#define ASC 2               // _IO('s', 3)
#define DESC 3              // _IO('s', 4)
#define array_size 128static struct cdev adev;  // 表示了一个具体的字符设备static char iBuf[array_size];        // 定义字符数组// 开启设备
static int dev_open(struct inode* inode, struct file* filp)
{memset(iBuf, 0, sizeof(iBuf) / sizeof(char));return 0;
}// 释放设备
static int dev_release(struct inode* inode, struct file* filp)
{return 0;
}// 读出设备函数
static ssize_t dev_read(struct file* filp, char* buf, size_t count, loff_t* pos)
{if(copy_to_user(buf, iBuf, count)) return -1;  // 将内核空间的连续内存中的内容复制到用户空间return 0;
}// 写入设备函数
static ssize_t dev_write(struct file* filp, const char* buf, size_t count, loff_t* pos)
{memset(iBuf, 0, sizeof(iBuf) / sizeof(char));if(copy_from_user(iBuf, buf, count)) return -1;   // 将用户空间的连续内存中的内容复制到内核空间return 0;
}// 通过冒泡排序为iBuf排序函数
static void bubble_sort(int cmd){int n = sizeof(iBuf);for (int i = 0; i < n - 1; i++){for (int j = 0; j < n - i - 1; j++){int flag = 0;if(iBuf[j] > iBuf[j + 1] && cmd == 1) flag = 1; // 升序if(iBuf[j] < iBuf[j + 1] && cmd == 2) flag = 2; // 降序if(flag){iBuf[j] = iBuf[j] ^ iBuf[j+1];iBuf[j+1] = iBuf[j] ^ iBuf[j+1];iBuf[j] = iBuf[j] ^ iBuf[j+1];}}}
}// 字母转化函数
static void caser(int cmd){for (int i = 0; i < array_size; i++){int flag = 0;if(iBuf[i] >= 'A' && iBuf[i] <= 'Z' && cmd == 1) flag = 1;   // 大转小if(iBuf[i] >= 'a' && iBuf[i] <= 'z' && cmd == 2) flag = 2; // 小转大if(flag == 1){iBuf[i] += 32;}if(flag == 2){iBuf[i] -= 32;}}
}// ioctl函数
static long dev_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
{if(_IOC_TYPE(cmd) != 's') return -ENOTTY;   // 可以不写switch(cmd){case LOWWERCASE:caser(1);break;case UPPERCASE:caser(2);break;case ASC:bubble_sort(1);break;case DESC:bubble_sort(2);break;}return 0;
}// 封装操作到 file_operations 结构体中
static struct file_operations some_ops = {.owner = THIS_MODULE,.open = dev_open,.release = dev_release,.read = dev_read,.write = dev_write,.unlocked_ioctl = dev_ioctl
};// 初始化模块时操作
static int _init dev_init(void)
{int ret;// 使用宏MKDEV将主设备号和次设备号合并成一个设备号,高12位为主设备号,低20位为次设备号dev_t dev = MKDEV(VSER_MAJOR, VSER_MINOR);// 第一个参数指定起始设备号,第二个参数指定申请个数,第三个参数标记主设备号的名称// 为了解决设备号冲突的问题,动态分配设备号函数:alloc_chrdev_region(dev, baseminor, count, name)ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);if(ret) goto reg_err;// 初始化字符设备,将设备与操作关联,第一个参数为设备地址,第二个参数为操作结构体地址cdev_init(&adev, &some_ops); // 将设备添加到散列表中去,第一个参数为设备地址,第二个参数为设备号,第三个参数为cdev对象可以管理多少设备// 一个对象可以管理多个设备ret = cdev_add(&adev, dev, VSER_DEV_CNT);if(ret) goto add_err;return 0;
reg_err:return ret;
add_err:// 如果添加对象失败的话注销掉设备号unregister_chrdev_region(dev, VSER_DEV_CNT);
}// 卸载模块时操作
static void _exit dev_exit(void)
{// 获取设备号dev_t dev = MKDEV(VSER_MAJOR, VSER_MINOR);// 模块卸载时删除掉设备对象cdev_del(&adev);// 注销掉设备号unregister_chrdev_region(dev, VSER_DEV_CNT);
}// 为初始化函数和退出函数起别名
module_init(dev_init);
module_exit(dev_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("NAPO <2661273650@qq.com>");
MODULE_DESCRIPTION("A Module");
MODULE_ALIAS("napo_module");
/*调用驱动事例程序*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>#include "dev.h" // 定义有ioctl的命令等等int main(int argc, char* argv){int fd = open("/dev/mydev", O_RDWR);if (fd == -1) return -1;char read_buf[16];char write_buf[16];if(read(fd, read_buf, sizeof(read_buf) / sizeof(char))) printf("读出失败");else printf("读出成功");if(write(fd, write_buf, sizeof(write_buf) / sizeof(char))) printf("写入失败");else printf("写入成功");ioctl(fd, LOWWERCASE); // 转换为小写ioctl(fd, UPPERCASE);   // 转换为大写ioctl(fd, ASC);         // 升序排列ioctl(fd, DESC);     // 降序排列close(fd);return 0;
}

嵌入式linux驱动开发教程相关推荐

  1. 《嵌入式Linux驱动开发教程》--内核模块

    内核模块 绝大多数的驱动都是以内核模块的形式实现. 宏内核和微内核 宏内核(Linux):所有的内核功能都被整体编译在一起,形成单独的内核镜像文件,内核中各功能模块的交互通过直接的函数调用进行. 微内 ...

  2. 《嵌入式Linux驱动开发教程》--高级I/O操作

    高级I/O操作 1.ioctl设备操作 2.proc文件系统 3.非阻塞式IO 4.阻塞式IO 5. IO多路复用 6.异步IO 7.异步通知(信号驱动IO模型) 8.相关代码 8.1非阻塞式IO用户 ...

  3. 嵌入式linux驱动开发实战教程,嵌入式Linux驱动开发实战视频教程

    嵌入式Linux驱动开发实战教程(内核驱动.看门狗技术.触摸屏.视频采集系统) 适合人群:高级 课时数量:109课时 用到技术:嵌入式 Linux 涉及项目:驱动开发.看门狗技术.触摸屏.视频采集 咨 ...

  4. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之Pinctrl子系统和GPIO子系统的使用

    文章目录 前言 1.Pinctrl子系统 1.1.为什么有Pinctrl子系统 1.2.重要的概念 1.3.代码中怎么引用pinctrl 2.GPIO子系统 2.1.为什么有GPIO子系统 2.2.在 ...

  5. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之按键驱动框架

    文章目录 前言 1.APP怎么读取按键值 1.1.查询方式 1.2.休眠-唤醒方式 1.3.poll方式 1.3.异步通知方式 1.5. 驱动程序提供能力,不提供策略 2.按键驱动程序框架--查询方式 ...

  6. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之LED模板驱动程序的改造:设备树

    文章目录 前言 1.驱动的三种编写方法 2.怎么使用设备树写驱动程序 2.1.设备树节点要与platform_driver能匹配 2.2.修改platform_driver的源码 3.实验和调试技巧 ...

  7. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之设备树模型

    文章目录 前言 1.设备树的作用 2.设备树的语法 2.1.设备树的逻辑图和dts文件.dtb文件 2.1.1.1Devicetree格式 1DTS文件的格式 node的格式 properties的格 ...

  8. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之总线设备驱动模型

    文章目录 前言 1.驱动编写的三种方法 1.1.传统写法 1.2.总线驱动模型 1.3.设备树驱动模型 2.Linux实现分离:Bus/Dev/Drv模型 2.1.Bus/Dev/Drv模型 2.2. ...

  9. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之驱动设计的思想:面向对象/分层/分离

    文章目录 前言 1.分离设计 驱动程序分析---程序分层 通用驱动程序---面向对象 个性化驱动程序---分离 APP 程序分析 前言 韦东山嵌入式Linux驱动开发基础知识学习笔记 文章中大多内容来 ...

最新文章

  1. 《BI项目笔记》创建标准维度、维度自定义层次结构
  2. (005) java后台开发之Mac终端命令运行java
  3. LeetCode Partition Equal Subset Sum(动态规划)
  4. php admin允许空密码登陆
  5. VS2008 ,TFS2008破解序列号
  6. %date:~0,10%用法
  7. Android中的Handler的具体用法
  8. JavaScript对象的理解
  9. SAP WM 工单完工入库,系统报错- No SU type could be determined -
  10. 《神经网络与深度学习》课程笔记(3)-- 神经网络基础之Python与向量化
  11. javascript无提示关闭窗口,兼容IE,Firefox
  12. 论文准备:基于区块链的一些设计IIoT的最新动向调查【已公开发表】
  13. 手把手教你做一个2048 上
  14. linux sdr 2832u软件无线电,使用R820T+RTL2832U玩软件无线电
  15. Endnote如何添加CAJ格式文件
  16. 央行降息后六大城市房价有望反弹(名单)
  17. 微信支付申请相关问题
  18. linux生成checksum,SF2281修改Lic授权ID生成新checksum生成器
  19. 细说ItemInfo
  20. intval()和int()

热门文章

  1. 新绝代双骄三 (张菁、何露)完美全攻略
  2. informix数据库unload导出数据Load导入数据
  3. SVN使用免费外网访问 实现远程办公
  4. 数字信号处理7——点到向量的距离
  5. TensorFlow2 -官方教程 :保存和恢复模型
  6. 【HTML】HTML 列表 ( 无序列表 | 有序列表 | 自定义列表 )
  7. Linux中cpio是什么?有几种操作模式?
  8. Joan Baez - Willie Moore
  9. IBM x3750 M4安装ESXi5.5
  10. Blood Cousins (dsu on tree + 求第k级祖先)