【linux驱动开发】在linux内核中注册一个杂项设备与字符设备以及内核传参的详细教程
文章目录
- 注册杂项设备
- 驱动模块传参
- 注册字符设备
开发环境: windows + ubuntu18.04 + 迅为rk3568开发板
注册杂项设备
相较于字符设备,杂项设备有以下两个优点:
- 节省主设备号:杂项设备的主设备号固定为 10,在系统中注册多个 misc 设备驱动时,只需使用子设备号进行区分即可。
- 使用简单:相比如普通的字符设备驱动, misc驱动只需要将基本信息通过结构体传递给相应处理函数即可。
在linxu系统中可使用 cat /proc/misc
命令查看系统中的杂项设备。注册杂项设备的步骤:
-
1.填充设备操作集结构体
struct file_operations
; -
2.填充杂项设备结构体
struct miscdevice
; -
3.使用函数
misc_register
注册杂项设备; -
4.使用函数
misc_deregister
卸载杂项设备;
上面三步可使用下面函数直观用表达,即:
static struct file_operations xxx_fops{.owner = THIS_MODULE, .read = xxx_read, ....
};
struct miscdevice xxx_dev{.minor = MISC_DYNAMIC_MINOR, .name = "xxx", .fops = &xxx_fops
};
static int __init xxx_init(void) //驱动入口函数
{int ret;printk(KERN_EMERG "xxx_init\r\n");ret = misc_register(&xxx_dev);//注册杂项设备if(ret<0){printk( "misc_register failed\r\n");return -1;}printk( "misc_register ok\r\n");return 0;
}
static void __exit xxx_exit(void) //驱动出口函数
{printk(KERN_EMERG "xxx_exit\r\n");misc_deregister(&xxx_dev); //卸载杂项设备
}
module_init(xxx_init); //注册入口函数
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");
具体实现注册一个杂项设备的示例代码如下:
#include <linux/kernel.h>
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h> //注册杂项设备头文件
#include <linux/fs.h> //注册设备节点的文件结构体
#include <linux/uaccess.h>// 打开杂项设备
int _open(struct inode *inode,struct file*file)
{printk(KERN_EMERG"hello misc");return 0;
}// 关闭杂项设备
int close(struct inode * inode, struct file *file)
{printk(KERN_EMERG"close");return 0;
}// 读取杂项设备中的数据
ssize_t misc_read (struct file *file, char __user *buff, size_t size, loff_t *loff)
{char kbuff[32] = "kernel";if(copy_to_user(buff,kbuff,strlen(kbuff)) != 0) // 将内核中的数据给应用{printk("copy_to_user error\r\n");return -1;} printk(KERN_EMERG"copy_to_user is successful\r\n");return size;
}// 写入数据到杂项设备中
ssize_t misc_write (struct file *file, const char __user *buff, size_t size, loff_t *loff)
{char kbuff[32] ;if(copy_from_user(kbuff,buff,size)!= 0) // 从应用那儿获取数据{printk("copy_from_user error\r\n");return -1;} printk(KERN_EMERG"copy_from_user data:%s\r\n",kbuff);return size;
}// 设备文件描述集
struct file_operations misc_fops ={.owner = THIS_MODULE,.open = misc_open,.release = close,.read = misc_read,.write = misc_write
};struct miscdevice misc_dev = {.minor = MISC_DYNAMIC_MINOR,.name = "hello_misc", // 杂项设备名 注册成功后会在 /dev目录下显示.fops = &misc_fops
};// 驱动的入口函数
static int __init misc_init(void)
{int ret = 0;ret = misc_register(&misc_dev);if(ret < 0)printk(KERN_EMERG"misc register is error\r\n.");elseprintk(KERN_EMERG"misc register is seccussful\r\n.");return 0;
}//驱动的出口函数
static void __exit misc_exit(void)
{misc_deregister(&misc_dev);printk(KERN_EMERG"baibai\r\n");
}module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");
编译传送到开发板上后,先试用insmod +驱动名.ko
挂载驱动,其结果为:
上述驱动代码的测试代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main(int argc,char *argv[])
{char filePath[] = "/dev/hello_misc";// 打开文件int fd = open(filePath,O_RDWR);if(fd < 0){printf("opening is failed.\n");return -1;}elseprintf("opening is successful.\n");// 读取char buff1[32],buff2[32] = "hello this is app";read(fd,buff1,sizeof(buff1));printf("buff1 is %s.\n",buff1);// 写入write(fd,buff2,sizeof(buff2)); close(fd);return 0;
}
使用./+程序名
运行测试代码后,得结果如下:
驱动模块传参
总所周知,应用程序传参是通过shell终端传,只要将main函数按照下面格式书写即可完成传参操作
int main(int argc ,char *argv[])
{return 0;
}
相比之下,驱动模块传递参数需要借助其他函数完成传参操作:
1. 传递单个参数给内核
module_param(name, type, perm)
参数解释:
-
name:参数名,既是外部参数名,又是内部参数名。
-
type:参数的数据类型,可取int、charp等。
-
perm:访问权限。八进制,如:0777。0表示该参数在文件系统中不可见。
注意:传递字符作为参数时数据类为charp
,而不是char
.
2.传递数组给内核
module_param_array(name, type, nump, perm)
- module_param_array(name,type,nump,perm)
- name:数组参数名,既是外部参数,又是内部参数
- type:参数的数据类型
- nump:终端传给数组的实际元素个数(指针变量)
- perm:访问权限,0644。0表示该参数在文件系统中不可见
3.传递字符串给内核
module_param_string(name, string, len, perm)
- name:参数名,外部参数名
- string:内部参数名(内部字符数组名)
- len:数组长度
- perm:访问权限,0644。0表示该参数在文件系统中不可见
传递参给内核的作用:
- 1.设置驱动的相关参数,如:设备数量、设备缓冲区大小等等。
- 2.可进行安全校验,放置驱动被他人盗用。
说明:
参数传递的适用时机为 加载驱动到内核时,命令形式为:insmod +驱动名.ko +参数1名=参数值 参数2名=参数值 ……
。例如下面示例中使用insmod file.ko date=12
传递参数:
#include <linux/module.h>
#include <linux/init.h>// 保存参数
static int date;
static int date1;
static int data[5];
static int count;
static char str[32];
static char *strData;// 传递单个参数
module_param(date,int,S_IRUGO|S_IWUSR); // 可读可写
module_param(date1,int,S_IRUGO); // 可读可写
module_param(strData,charp,S_IRUGO); // 可读// 传递多个参数
module_param_array(data,int,&count,S_IRUGO); // 可读
module_param_string(str,str,sizeof(str),S_IRUGO); // 可读// 驱动的入口函数
static int __init dev_init(void)
{int i=0;if(strcmp(str,"myTest")!=0){printk("dev_init error\r\n");return -1;}printk("---------------------------------------\r\n");printk(KERN_EMERG"dev_init is successful!\r\n");for(i=0;i<count;++i)printk("data[%d] = %d \r\n",i,data[i]);printk("str:%s strData:%s\r\n",str,strData);printk("date:%d count:%d\r\n",date,count);data[1] = 111;date = 10;date1 = 111;return 0;
}//驱动的出口函数
static void __exit dev_exit(void)
{int i=0;for( i=0;i<count;++i)printk("data[%d] = %d \r\n",i,data[i]);printk("date:%d count:%d\r\n",date,count);printk(KERN_EMERG"dev_exit is successful\r\n");
}module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");
注册字符设备
步骤一:驱动初始化,需要申请设备号,初始化并且注册cdev结构体,初始化硬件;
可使用动态申请或静态申请设备号,其中动态申请设备号一般在235-255,静态申请则一般由用户手动输入。
静态申请的函数原型为:
int register_chrdev_region(dev_t, unsigned, const char *);
参数含义:
- from: 自定义的 dev_t 类型设备号。
- count: 申请设备的数量。
- name: 申请的设备名称。
函数返回值:申请成功返回 0,申请失败返回负数。
动态申请的函数原型为:
int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
参数含义:
- dev : 会将申请完成的设备号保存在 dev 变量中。
- baseminor: 次设备号可申请的最小值。
- count: 申请设备的数量。
- name: 申请的设备名称。
函数返回值:申请成功返回 0,申请失败返回负
Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备号、内核对象、文件操作 file_operations 结构体(设备的打开、读写、关闭等操作接口)等信息:
struct cdev {struct kobject kobj; //内嵌的内核对象.struct module *owner; //该字符设备所在的内核模块的对象指针. const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.struct list_head list; //用来将已经向内核注册的所有字符设备形成链表. dev_t dev; //字符设备的设备号,由主设备号和次设备号构成. unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
初始化设备描述集cdev结构体的函数原型为:
void cdev_init(struct cdev *, const struct file_operations *);
参数含义:
- 参数1:表示是抽象设备结构体;
- 参数2:表示文件操作集;
注册设备到内驱使用下面函数
int cdev_add(struct cdev *, dev_t, unsigned);
参数含义:
- 参数1:为要添加的 struct cdev 类型的结构体
- 参数2:为申请的字符设备号
- 参数3:为和该设备关联的设备编号的数量
若函数在内核中添加成功返回 0,添加失败返回负数。
步骤二:构建设备文件操作描述集file_operations
也就是read、write、open、close等函数。
步骤三:生成并且添加设备节点
- 手动添加设备节点:也就是在加载驱动到内核时添加,即
mknod + 路径/设备名 +设备类型 + 主设备号 + 次设备号
- 自动添加设备节点:初始化内核时,使用函数自动添加。即先试用函数
class_create
创建一个类,在使用函数device_create
创建并且添加设备节点。
class_create
函数说明:
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \ __class_create(owner, name, &__key); \
})
函数作用:
用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进 Linux 内核系统。
参数含义:
- owner:指向函数即将创建的这个 struct class 的模块。一般为 THIS_MODULE。
- name:代表即将创建的 struct class 变量的名字。
返回值:struct class * 类型的类。
device_create
函数说明:
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt,...);
函数作用:
用来在 class 类中下创建一个设备属性文件,udev 会自动识别从而进行设备节点的创建。
参数含义:
- cls:指定所要创建的设备所从属的类。
- parent:指定该设备的父设备,如果没有就指定为 NULL。
- devt:指定创建设备的设备号。
- drvdata:被添加到该设备回调的数据,没有则指定为 NULL。
- fmt:添加到系统的设备节点名称。
返回值:struct device * 类型结构体的设备
步骤四:注销字符设备驱动,
- 步骤一:使用函数
unregister_chrdev_region
释放设备号
void unregister_chrdev_region(dev_t, unsigned)
该函数只有一个参数,为要删除设备的设备号,并且函数无返回值。
- 步骤二:使用函数
cdev_del
步骤二:删除设备操作集卸载cdev
void cdev_del(struct cdev *);
该函数只有一个参数,为要删除的 struct cdev 类型的结构体,并且函数无返回值。
- 步骤三:使用函数
device_destroy
卸载设备
void device_destroy(struct class *cls, dev_t devt);
用来删除 cls 类中的devt设备属性文件,udev 会自动识别从而进行设备节点的删除。
- 步骤四:使用函数
class_destroy
删除类class
void class_destroy(struct class *cls);
该函数只有一个参数,为要删除的类,并且函数无返回值。
示例代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>#define DEVICE_NUMBER 1 // 设备数量
#define DEVICE_SNAME "schrdev" // 静态申请时设备名
#define DEVICE_ANAME "achrdev" // 动态申请时设备名
#define DEVICE_MINOR_NUM 0 // 次设备号起始地址
#define DEVICE_CLASS_NAME "myTestClass" // 类名
#define DEVICE_MYNAME "mytest"// 打开设备
int chrdev_open(struct inode*inode,struct file*file)
{printk("chrdev_open is opened\r\n");return 0;
}// 保存设备号 其中前12位为主设备号 后20位为次设备号
static dev_t dev_num;// 定义主设备号 次设备号
static int major_num,minor_num; // 设备信息描述集
struct cdev cdev;// 类描述集
struct class *cls;// 设备描述集
struct device*device;// 传递单个参数
module_param(major_num,int,S_IRUGO); // 可读
module_param(minor_num,int,S_IRUGO); // 可读// 文件操作集
struct file_operations chrdev_opr = {.owner = THIS_MODULE,.open = chrdev_open
};// 驱动的入口函数
static int __init dev_init(void)
{int ret;/* 步骤一:申请设备号 */// 有传递主设备号就静态申请if(major_num){ dev_num = MKDEV(major_num, minor_num);//将主设备号 次设备号合并成设备号//参数分别表示 设备号 设备数量 设备名称ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME); if(ret < 0){printk("dev_init error\r\n");return -1;}} // 否则就动态申请else{// 参数:设备号 次设备号起始地址 设备数量 设备名称ret = alloc_chrdev_region(&dev_num , DEVICE_MINOR_NUM, DEVICE_NUMBER, DEVICE_ANAME);if(ret < 0){printk("dev_init error\r\n");return -1;}// 获取主设备号 次设备号major_num = MAJOR(dev_num);minor_num = MINOR(dev_num);}printk("---------------------------------------\r\n");printk("dev_num:%d major_num:%d minor_num:%d\r\n",dev_num,major_num,minor_num);/* 步骤二:初始化设备 */// 初始化cdevcdev.owner = THIS_MODULE;// 初始化设备 cdev_init(&cdev, &chrdev_opr);/* 步骤三:注册设备到内核 *///添加(注册)到内核 参数: 设备 设备号 设备数量cdev_add(&cdev, dev_num, DEVICE_NUMBER);/* 步骤四:先创建类再自动创建添加设备名称*/// 创建类 参数1: 类的归属 参数2: 类名cls = class_create(THIS_MODULE,DEVICE_CLASS_NAME);// 创建设备 参数1: 归属到类 参数2:设备的父设备 参数3:设备号 参数4:添加到设备的回调数据 参数5:设备名device = device_create(cls,NULL,dev_num,NULL,DEVICE_MYNAME);printk("auto add device name\r\n");return 0;
}//驱动的出口函数
static void __exit dev_exit(void)
{/* 步骤一:注销设备号 参数:设备号 设备数量 */ unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);/* 步骤二:删除设备操作集 */cdev_del(&cdev);/* 步骤三:删除设备*/device_destroy(cls,dev_num);/* 步骤四: 删除类*/class_destroy( cls);printk(KERN_EMERG"dev_exit is successful\r\n");
}module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");
使用加载驱动到内核insmod file.ko命令,得结果
在测试驱动前需要使用mknod /dev/mytest c 236 0
创建设备文件,命令格式为:mknod + /路径/设备名称 +设备类型+主设备号 + 次设备号
。
然后就可写一个应用程序来测试我们的字符驱动是否成功注册,测试源码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main(int argc,char* argv[])
{char name[] = "/dev/mytest";int fd = open(name,O_RDONLY); // 仅读if(fd < 0){printf("open is failed\n");return -1;}close(fd);return 0;
}
执行测试程序后结果如下:
注意:在运行测试程序时,一定要先创建驱动文件,否则就会报段错误,具体如下:
杂项设备与字符设备的比较:
- 杂项设备的主设备号固定为10,而字符设备的主设备号需要创建。
- 杂项设备的创建相对简单,只需要填充设备操作集结构体、杂项设备结构体再注册即可。而字符设备需要经过 申请设备号、初始化并且注册cdev结构体、初始化硬件、构建设备文件操作描述集、生成并且添加设备节点。
- 杂项设备与字符设备都需要构建文件操作描述集。(应用层调用驱动的核心)
相关文章:

【linux驱动开发】在linux内核中注册一个杂项设备与字符设备以及内核传参的详细教程
文章目录 注册杂项设备驱动模块传参注册字符设备 开发环境: windows ubuntu18.04 迅为rk3568开发板 注册杂项设备 相较于字符设备,杂项设备有以下两个优点: 节省主设备号:杂项设备的主设备号固定为 10,在系统中注册多个 misc 设备驱动时&…...
Golang条件编译 | 获取系统的磁盘空间内存占用demo | gopsutil/disk库(跨平台方案)
文章目录 一、Golang条件编译1. 构建标签( Build tags)2. 文件后缀(File suffixes) 二、GO golang 获取磁盘空间 条件编译思路 三、【推荐】使用github.com/shirou/gopsutil/disk这个库,如何获取机器下不同磁盘分区的内容 一、Golang条件编译…...
22/76-池化
池化(最大池化层:选每个kernel中最大的数) 填充、步幅、多个通道: 池化层与卷积层类似,都具有填充和步幅。 没有可学习的参数。 在每个输入通道应用池化层以获得相应的输出通道。 输出通道数输入通道数。 平均池化层…...

江科大STM32 下
目录 ADC数模转换器DMA直接存储器存取USART串口9-2 串口发送接受9-3 串口收发HEX数据包 I2C(mpu6050陀螺仪和加速度计)SPI协议10.1 SPI简介W25Q64简介10.3 SPI软件读写W25Q6410.4 SPI硬件读写W25Q64 BKP、RTC11.0 Unix时间戳11.1 读写备份寄存器BKP11.2 RTC实时时钟 十二、PWR1…...

利用HTML和CSS实现的浮动布局
代码如下 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><style>*{m…...

2024年第十届控制、自动化与机器人国际会议(ICCAR 2024)即将召开!
2024年4月27~29日 新加披 会议官网:10th-ICCAR 2024https://iccar.org/index.html 第十届控制、自动化和机器人国际会议将于2024年4月27-29日在新加坡举办。本次会议由新加坡电子学会,IEEE机器人和自动控制协会和IEEE联合主办,并得到北京航空…...

基于python集成学习算法XGBoost农业数据可视化分析预测系统
文章目录 基于python集成学习算法XGBoost农业数据可视化分析预测系统一、项目简介二、开发环境三、项目技术四、功能结构五、功能实现模型构建封装类用于网格调参训练模型系统可视化数据请求接口模型评分 0.5*mse 六、系统实现七、总结 基于python集成学习算法XGBoost农业数据可…...

第29集《佛法修学概要》
丁三、声闻乘 分二:戊一、释义;戊二、四谛法;戊三、结示 请大家打开讲义第八十二页。我们看丁三,声闻乘。 在祖师大德的判教当中,把我们整个大乘的成佛之道分成了三个部分:第一个是安乐道,第…...

奥伦德光电耦合器5G通信领域及其相关领域推荐
光电耦合器是以光为媒介传输电信号的一种电-光-电转换器件。由于该器件使用寿命长、工作温度范围宽,所以在过程控制、工业通信、家用电器、医疗设备、通信设备、计算机以及精密仪器等方面有着广泛应用在当前工艺技术持续发展与提升的过程中,其工作速度、…...

机器学习算法 - 马尔可夫链
马尔可夫链(Markov Chain)可以说是机器学习和人工智能的基石,在强化学习、自然语言处理、金融领域、天气预测、语音识别方面都有着极其广泛的应用 > The future is independent of the past given the present 未来独立于过去ÿ…...
Linux下防火墙相关命令整理
目录 一.前言二.相关命令整理 一.前言 这篇文章简单整理一下Linux系统中防火墙相关命令。 二.相关命令整理 开启防火墙 systemctl start firewalld关闭防火墙 systemctl stop firewalld重启防火墙 systemctl restart firewalld开机启用防火墙 systemctl enable firewall…...
Python八股文总结
一. Python基本数据结构有哪四种?区别是什么? 列表(List)元组(Tuple)字典(Dictionary)集合(Set) 区别主要在于它们的可变性(是否可以修改&#x…...

计算机导论05-计算机网络
文章目录 计算机网络基础计算机网络概述计算机网络的概念计算机网络的功能计算机网络的组成 计算机网络的发展计算机网络的类型 网络体系结构网络互联模型OSI/RM结构与功能TCP/IP结构模型TCP/IP与OSI/RM的比较 网络地址与分配IP地址构成子网的划分IPv6 传输介质与网络设备网络传…...

sentinel熔断与限流
文章目录 一、sentinel简介Sentinel 是什么?Sentinel安装 二、sentinel整合工程新建cloudalibaba-sentinel-service8401微服务引入依赖yml配置主启动类添加EnableDiscoveryClient业务类测试 三、sentinel流控规则基本介绍流控模式直接(默认)关…...

vi/vim 编辑器 --基本命令
1 vi/vim编辑器介绍 vi 是visual interface 的简称,是Linux中最经典的文本编辑器 vim是vi的加强版。兼容了vi的所有指令,不仅能编辑文本,而且具有shell程序编辑的功能,可以通过不同颜色的字体辨别语法的正确性,极大…...
C++——STL标准模板库——容器详解——set
一、基本概念 set容器是一种具备自动排序功能的集合,默认递增排序;元素无法直接修改,且不能重复;另一个版本叫做multiset,允许存在重复元素,其他功能和性质一样。 set容器底层结构一般为自平衡二叉搜索树…...

Vim一键配置指南,打造高效率C++开发环境
文章目录 前言安装与卸载功能演示gcc/g升级问题 前言 Vim作为当下最受欢迎的文本编译器之一,不仅具有强大的文本编辑功能,还提供了高度的可定制性。用户可以根据自己的喜好自定义配置,并且通过自己编写插件或者使用现有的插件来扩展Vim的功能…...

新航向,新生态: Michael在出海业务圆桌会议分享HyperBDR全球业务拓展之道
1月15日-16日,以“领航新开局,共赢新生态”为主题的华为云生态大会2024在华为云贵安数据中心云上屯盛大举行。本次会议聚焦于华为云全国生态伙伴与开发者,旨在共同见证华为云生态战略的最新进展和伙伴政策的新升级。与会者将分享来自优秀生态…...
SpringBoot异步处理
Spring boot异步处理 业务场景: 如执行数据库备份任务,前端发起请求到后端,后端备份数据库的处理逻辑需要很长一段时间,此时前端会一直等待后端返回结果,给用户给等待时间过长,这是就要考虑异步处理了&…...

2024年甘肃省职业院校技能大赛信息安全管理与评估 样题一 模块二
竞赛需要完成三个阶段的任务,分别完成三个模块,总分共计 1000分。三个模块内容和分值分别是: 1.第一阶段:模块一 网络平台搭建与设备安全防护(180 分钟,300 分)。 2.第二阶段:模块二…...

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
数据链路层的主要功能是什么
数据链路层(OSI模型第2层)的核心功能是在相邻网络节点(如交换机、主机)间提供可靠的数据帧传输服务,主要职责包括: 🔑 核心功能详解: 帧封装与解封装 封装: 将网络层下发…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...

基于Springboot+Vue的办公管理系统
角色: 管理员、员工 技术: 后端: SpringBoot, Vue2, MySQL, Mybatis-Plus 前端: Vue2, Element-UI, Axios, Echarts, Vue-Router 核心功能: 该办公管理系统是一个综合性的企业内部管理平台,旨在提升企业运营效率和员工管理水…...

给网站添加live2d看板娘
给网站添加live2d看板娘 参考文献: stevenjoezhang/live2d-widget: 把萌萌哒的看板娘抱回家 (ノ≧∇≦)ノ | Live2D widget for web platformEikanya/Live2d-model: Live2d model collectionzenghongtu/live2d-model-assets 前言 网站环境如下,文章也主…...