Linux驱动开发-03字符设备驱动框架搭建
一、字符设备驱动开发步骤
- 驱动模块的加载和卸载(将驱动编译模块,insmod加载驱动运行)
- 字符设备注册与注销(我们的驱动实际上是去操作底层的硬件,所以需要向系统注册一个设备,告诉Linux系统,我有这个设备,当驱动模块加载的时候我们去注册设备,当驱动模块卸载的时候我们注销设备)
- 实现设备的具体操作函数(我们注册完设备后,需要对设备进行读写操作,是通过一个结构体来进行的)
- 添加LICENSE 和作者信息
通过上面的字符设备的驱动开发步骤的了解,其实我们只需要掌握每一种设备的驱动框架按照框架去进行开发就行
1.1 设备号
Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux提供了一个名为 dev_t的数据类型表示设备号
//设备号数据类型在内核中的定义
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
typedef unsigned int __u32;
//可以看到设备号就是一个u32的数据类型
32位的数据构成了主设备号和次设备号两部分,其中高12位为主设备号, 低 20位为次设备号。因此 Linux系统中主设备号范围为 0~4095
1.2 设备号的分配
(1)静态分配设备号
使用“ cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号,选择一个没有被使用的设备号进行使用即可
(2)动态分配设备号
设备号申请函数和设备号释放函数
/*
* alloc_chrdev_region() - register a range of char device numbers* @dev: output parameter for first assigned number* @baseminor: first of the requested range of minor numbers* @count: the number of minor numbers required* @name: the name of the associated device or driver
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
/** __register_chrdev() - create and register a cdev occupying a range of minors* @major: major device number or 0 for dynamic allocation* @baseminor: first of the requested range of minor numbers* @count: the number of minor numbers required* @name: name of this range of devices* @fops: file operations associated with this devices
*/
int __register_chrdev(unsigned int major, unsigned int baseminor,unsigned int count, const char *name,const struct file_operations *fops)
1.3 file_operations
file_operations结构体就是设备的具体操作函数
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iterate) (struct file *, struct dir_context *);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*mremap)(struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMUunsigned (*mmap_capabilities)(struct file *);
#endif
};
二、实战之驱动文件的编写
2.1 应用程序与驱动程序的交互方式
Linux 应用程序对驱动程序的调用
在Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
应用程序运行在用户空间,而Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。
应用程序运行在用户空间,驱动是属于内核的一部分;我们的应用程序想对内核空间进行操作,就需要通过系统调用的方式(例如下面用户空间的open就需要通过系统调用,来通过file_operations结构体里面的open对设备进行具体)
2.2 驱动中的open、close、read、release函数介绍
/** @description : 打开设备* @param - inode : 传递给驱动的inode* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量* 一般在open的时候将private_data指向设备结构体。* @return : 0 成功;其他 失败*/
static int chrdevbase_open(struct inode *inode, struct file *filp);/** @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符)* @param - buf : 返回给用户空间的数据缓冲区* @param - cnt : 要读取的数据长度* @param - offt : 相对于文件首地址的偏移* @return : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt);/** @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符* @param - buf : 要写给设备写入的数据* @param - cnt : 要写入的数据长度* @param - offt : 相对于文件首地址的偏移* @return : 写入的字节数,如果为负值,表示写入失败*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt);/** @description : 关闭/释放设备* @param - filp : 要关闭的设备文件(文件描述符)* @return : 0 成功;其他 失败*/
static int chrdevbase_release(struct inode *inode, struct file *filp);
2.3 内核区与用户区数据交互
驱动不能直接访问应用程序的数据,应用程序也不能直接访问内核数据,必须借助其他函数
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
//将用户空间的数据复制到内核空间unsigned long copy_to_user(void *to, const void *from, unsigned long n);
//将内核空间的数据复制到用户空间
所以,我们的内核read的时候,应该使用copy_to_user;内核write的时候,应该使用copy_from_user;
2.4 chrdevbase.c和 chrdevbaseAPP.c
chrdevbase.c
#include <linux/init.h> //包含宏定义的头文件(printk的头文件)
#include <linux/module.h> //包含初始化加载模块的头文件
#include <linux/fs.h>//注册设备和注销设备的头文件
#include<linux/kernel.h>
#include <linux/ide.h>
#include <linux/types.h>#define CHRDEVBASE_MAJOR 200 //主设备号
#define CHRDEVBASE_NAME "chrdevbase"//设备名字/*驱动不能直接访问应用程序的数据,应用程序也不能直接访问内核数据,必须借助其他函数(copy_from_user\copy_to_user)*//*我们的应用程序需要从驱动中去读取数据,并且还要向驱动写数据*/static char kenelreadbuf[100],kenelwritebuf[100];//我们驱动的读写缓冲区,file_operation结构体就是操作这两个static char keneldata[]={"keneldata data!"};//内核的数据,我们驱动read的时候就是把这个字符串返给应用程序//从设备读数据
static ssize_t chrdevbase_read(struct file *filp, __user char *buf, size_t count,loff_t *ppos){
/*参数解释:
filp:要打开的设备文件(文件描述符),也就是驱动文件的fd,因为我们是应用程序去read驱动的数据
buf:发送的数据就发送到buf里面去(我们的应用程序提供了一个buf,我们的驱动程序写到那个buf里面去)
count:驱动程序需要向应用程序发送多少个字节的数据
*/int ret=0;memcpy(kenelreadbuf,keneldata,sizeof(keneldata));//把keneldata中的数据拷贝到kenelreadbuf中,最后一个参数是拷贝的长度ret=copy_to_user(buf,kenelreadbuf,count);//把内核数据给应用程序,函数中的buf和count都是由应用程序决定,返回值为0表示成功if(ret == 0){printk("kernel send data :%s\r\n",kenelreadbuf);}else{printk("kernel send data failed!\r\n");}return 0;
}
//向设备写数据(应用程序把数据写给驱动程序)
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos){int ret=0;ret=copy_from_user(kenelwritebuf,buf,count);//把应用程序的数据给驱动,函数中的buf和count都是由应用程序决定if(ret == 0){printk("kernel receive data :%s\r\n",kenelwritebuf);}else{printk("kernel receive data failed!\r\n");}return 0;
}
//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp){return 0;
}
//关闭设备
static int chrdevbase_close(struct inode *inode, struct file *filp){return 0;
}
/*
字符设备:操作集合
*/
static const struct file_operations chrdevbase_fops = {.owner = THIS_MODULE,//owner拥有该结构体的模块的指针,一般设置为 THIS_MODULE.read = chrdevbase_read,.write = chrdevbase_write,.open = chrdevbase_open,.release= chrdevbase_close,
};//驱动入口函数
static int __init chrdevbase_init(void)
{int ret=0;/*注册字符设备驱动函数:主设备号、设备名字、file_operations结构体*/ret = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);//使用这个函数注册时,会把当前主设备号下的所有次设备号给占用if (ret<0){printk("chrdevbase init failed!\r\n");}printk("module_init!\r\n");return 0;
}
//驱动出口函数
static void __exit chrdevbase_exit(void)
{//注销字符设备驱动unregister_chrdev(CHRDEVBASE_MAJOR,CHRDEVBASE_NAME);printk("module_exit!\r\n");
};
/*模块的出口与入口*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);MODULE_AUTHOR("Chao");//作者是谁
MODULE_LICENSE("GPL");//开源协议
chrdevbaseAPP.c
int atoi(const char *nptr);把输入的字符串转化为整型变量,因为我们的最后一个字符串1、2分别代表了对驱动设备的读写
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"/*
/dev/chardevbase:驱动最终表现就是/dev/xxx文件,对文件的读写、打开关闭
argc:应用程序参数
argv[]:具体的参数内容,是以字符串形式
./chrdevbaseAPP <filename> <1:2>
./chrdevbaseAPP /dev/chardevbase 1表示向驱动里面读数据
./chrdevbaseAPP /dev/chardevbase 2表示向驱动里面写数据
*/
int main(int argc,char*argv[]){int ret=0;int fd=0;char*filename;//输入的字符串char readbuf[100],writebuf[100];static char usrdata[]={"usr data!"};if (argc!=3)//如果输入的参数不足三个打印错误{printf("Error usage!\r\n");return -1;}filename=argv[1];//驱动文件就是/dev/chardevbase,也就是输入的第二个字符串fd=open(filename,O_RDWR);//打开驱动文件if(fd < 0){printf("Can't open file %s\r\n", filename);return -1;}
if (atoi(argv[2])==1)//判断是不是读操作,atoi把字符串转化为整型变量
{//从驱动中读取数据ret = read(fd, readbuf, 50);//返回值是实际读取到的字节数/*这里我们从驱动中读取50个字节,把内容写到readbuf中*/if(ret < 0){printf("read file %s failed!\r\n", filename);}else{/* 读取成功,打印出读取成功的数据 */printf("read data:%s\r\n",readbuf);}}if (atoi(argv[2])==2){memcpy(writebuf,usrdata,sizeof(usrdata));//把usrdata内容拷贝到writebuf中//向驱动中写数据ret = write(fd, writebuf, 50);//返回值是实际写入的字节数/*这里我们从buf中的50个字节,把内容写到驱动中*/if(ret < 0){printf("write file %s failed!\r\n", filename);}else{/* 写入成功,打印出写入成功的数据 */printf("write data:%s\r\n",writebuf);}}/* 关闭设备 */ret = close(fd);if(ret < 0){printf("Can't close file %s\r\n", filename);return -1;}return 0;
}
三、测试运行
(1)编译chrdevbase.c生成chrdevbase.ko文件
(2)编译 chrdevbaseAPP.c使用交叉编译工具链编程成chrdevbaseAPP可执行文件
arm-linux-gnueabihf-gcc chrdevbaseAPP.c -o chrdevbaseAPP
(3)ls查看编译后的驱动文件和应用程序文件
(4)拷贝到/home/alientek/linux/nfs/rootfs/lib/modules/4.1.15文件下
这里存放着.ko文件
sudo cp chrdevbase.ko chrdevbaseAPP /home/alientek/linux/nfs/rootfs/lib/modules/4.1.15 -f
我们这里是使用nfs挂载根文件系统,所以我们使用串口终端去查看该目录下有没有对应的文件
(5)加载驱动modprobe chrdevbase.ko
(6)输入命令“ cat /proc/devices”可以注册的设备号、设备名字是否正确
我们在模块加载时,已经注册字符设备,我们使用这个命令来看看内核中是否有200这个设备号,设备名字是否正确
(7) 创建设备节点文件
在 Linux 内核中,当使用
register_chrdev()
函数,注册一个字符设备时,您实际上是在内核中注册了该设备的存在,并为其分配了一个主设备号;这个注册过程确保了内核知道如何处理对该设备的操作请求,但它并不在文件系统中创建任何可见的设备文件。设备文件(如
/dev/chrdevbase
)是用户空间与内核中设备驱动程序进行交互的接口。这些文件不是普通的文件,而是特殊文件,通常称为设备特殊文件或设备节点。当您在用户空间中对这些文件执行读写操作时,内核会将这些操作转换为对相应设备驱动程序的调用。
因此,即使已经在内核中注册了设备并为其分配了主设备号,仍然需要在文件系统中创建对应的设备文件,以便用户空间程序能够访问它。这就是为什么在注册字符设备之后,还需要用 mknod
命令来创建设备文件的原因。
创建设备文件节点
mknod /dev/chrdevbase c 200 0
其中“ mknod”是创建节点命令 ,“/dev/chrdevbase”是要创建的节点文件 c”表示这是个字符设备,“ 200”是设备的主设备号 0”是设备的次设备号。创建完成以后就会存在
查看/dev/目录下有没有对应的驱动文件(chrdevbase )
ls /dev/chrdevbase -l
结果
(8)运行chrdevbaseAPP可执行文件
./chrdevbaseAPP /dev/chrdevbase 1 (向驱动中去读数据)
./chrdevbaseAPP /dev/chrdevbase 2 (向驱动中去写数据)
至此我们的字符设备驱动开发流程结束
相关文章:

Linux驱动开发-03字符设备驱动框架搭建
一、字符设备驱动开发步骤 驱动模块的加载和卸载(将驱动编译模块,insmod加载驱动运行)字符设备注册与注销(我们的驱动实际上是去操作底层的硬件,所以需要向系统注册一个设备,告诉Linux系统,我有…...

Zynq系列FPGA实现SDI视频编解码+图像缩放+多路视频拼接,基于GTX高速接口,提供8套工程源码和技术支持
目录 1、前言工程概述免责声明 2、相关方案推荐本博已有的 SDI 编解码方案本博已有的FPGA图像缩放方案本方案的无缩放应用本方案在Xilinx--Kintex系列FPGA上的应用 3、详细设计方案设计原理框图SDI 输入设备Gv8601a 均衡器GTX 解串与串化SMPTE SD/HD/3G SDI IP核BT1120转RGB自研…...

VS2019使用C#写窗体程序技巧(1)
1、打开串口 private void button1_Click(object sender, EventArgs e){myPort cmb1.Text;mybaud Convert.ToInt32(cmb2.Text, 10);databit 8;parity Parity.None;stopBit StopBits.One;textBox9.Text "2";try{sp new SerialPort(myPort, mybaud, parity, dat…...
Python爬虫-requests模块
前戏: 1.你是否在夜深人静的时候,想看一些会让你更睡不着的图片却苦于没有资源... 2.你是否在节假日出行高峰的时候,想快速抢购火车票成功..。 3.你是否在网上购物的时候,想快速且精准的定位到口碑质量最好的商品. …...
适用于PyTorch 2.0.0的Ubuntu 22.04上CUDA v11.8和cuDNN 8.7安装指南
将下面内容保存为install.bash,直接用bash执行一把梭解决 #!/bin/bash### steps #### # verify the system has a cuda-capable gpu # download and install the nvidia cuda toolkit and cudnn # setup environmental variables # verify the installation ######…...
使用conda安装openturns
目录 1. 有效方法2. 整体分析使用pip安装使用conda安装验证安装安装过程中可能遇到的问题 1. 有效方法 conda install -c conda-forge openturns2. 整体分析 OpenTURNS是一个用于概率和统计分析的软件库,主要用于不确定性量化。你可以通过以下步骤在Python环境中安…...
Chameleon:动态UI框架使用详解
文章目录 引言Chameleon框架原理核心概念工作流程 基础使用安装与配置创建基础界面 高级使用自定义组件响应式布局数据流与状态管理 结论 引言 Chameleon,作为一种动态UI框架,旨在通过灵活、高效的方式帮助开发者构建跨平台、响应用户交互的图形用户界面…...
7.10飞书一面面经
问题描述 Redis为什么快? 这个问题我遇到过,但是没有好好总结,导致答得很乱。 答:Redis基于内存操作: 传统的磁盘文件操作相比减少了IO,提高了操作的速度。 Redis高效的数据结构:Redis专门设计…...

[数据结构] 归并排序快速排序 及非递归实现
()标题:[数据结构] 归并排序&&快速排序 及非递归实现 水墨不写bug (图片来源于网络) 目录 (一)快速排序 类比递归谋划非递归 快速排序的非递归实现: (二)归并排序 归…...
面试题 12. 矩阵中的路径
矩阵中的路径 题目描述示例 题解 题目描述 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。 单词必须按照字母顺序,通过相邻的单元格内的字母构成࿰…...

钉钉扫码登录第三方
钉钉文档 实现登录第三方网站 - 钉钉开放平台 (dingtalk.com) html页面 将html放在 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>登录</title>// jquery<script src"http://code.jqu…...

多GPU系统中的CUDA设备不可用问题
我们在使用多GPU系统时遇到了CUDA设备不可用的问题,详细情况如下: 问题描述: 我们在一台配备有8块NVIDIA GeForce RTX 3090显卡的服务器上运行CUDA程序时,遇到了如下错误: cudaErrorDevicesUnavailable: CUDA-capabl…...
python的列表推导式
文章目录 前言一、解释列表推导式二、在这句代码中的应用三、示例四、使用 for 循环的等价代码总结 前言 看看这一行代码:questions [q.strip() for q in examples["question"]] ,问题是最外层的 中括号是做什么的? 最外层的中括…...

类与对象(2)
我们在了解了类的简单创建后,需要对类的创建与销毁有进一步的了解,也就是对于类的构造函数与析构函数的了解。 目录 注意: 构造函数的特性: 析构函数: 注意: 该部分内容为重难点内容,在正常…...

迂回战术:“另类“全新安装 macOS 15 Sequoia beta2 的极简方法
概述 随着 WWDC 24 的胜利闭幕,Apple 平台上各种 beta 版的系统也都“跃跃欲出”,在 mac 上自然也不例外。 本次全新的 macOS 15 Sequoia(红杉)包含了诸多重磅升级,作为秃头开发者的我们怎么能不先睹为快呢࿱…...
如何设计一个秒杀系统,(高并发高可用分布式集群)
设计一个高并发、高可用的分布式秒杀系统是一个非常具有挑战性的任务,需要从架构、数据库、缓存、并发控制、降级限流等多个维度进行考虑。以下是一个典型的秒杀系统设计思路: 1. 系统架构 微服务架构 拆分服务:将系统功能拆分为多个微服务…...

深度优先搜索(所有可达路径)
参考题目:所有可达路径 题目描述 给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。 输入描述 第一行包含两个整数 N,M&…...

如何配置yolov10环境?
本文介绍如何快速搭建起yolov10环境,用于后续项目推理、模型训练。教程适用win、linux系统 yolo10是基于yolo8(ultralytics)的改进,环境配置跟yolo8几乎一模一样。 目录 第1章节:创建虚拟环境 第2章节:…...

『大模型笔记』GraphRAG:利用复杂信息进行发现的新方法!
GraphRAG:利用复杂信息进行发现的新方法! 文章目录 一. GraphRAG:利用复杂信息进行发现的新方法!1. 将RAG应用于私人数据集2. 整个数据集的推理3. 创建LLM生成的知识图谱4. 结果指标5. 下一步二. 参考文献微软官方推文:https://www.microsoft.com/en-us/research/blog/gra…...

数据结构1:C++实现变长数组
数组作为线性表的一种,具有内存连续这一特点,可以通过下标访问元素,并且下标访问的时间复杂的是O(1),在数组的末尾插入和删除元素的时间复杂度同样是O(1),我们使用C实现一个简单的边长数组。 数据结构定义 class Arr…...

国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
基于matlab策略迭代和值迭代法的动态规划
经典的基于策略迭代和值迭代法的动态规划matlab代码,实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...

STM32---外部32.768K晶振(LSE)无法起振问题
晶振是否起振主要就检查两个1、晶振与MCU是否兼容;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容(CL)与匹配电容(CL1、CL2)的关系 2. 如何选择 CL1 和 CL…...

【Linux系统】Linux环境变量:系统配置的隐形指挥官
。# Linux系列 文章目录 前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变量的生命周期 四、环境变量的组织方式五、C语言对环境变量的操作5.1 设置环境变量:setenv5.2 删除环境变量:unsetenv5.3 遍历所有环境…...

零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程
STM32F1 本教程使用零知标准板(STM32F103RBT6)通过I2C驱动ICM20948九轴传感器,实现姿态解算,并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化,适合嵌入式及物联网开发者。在基础驱动上新增…...

数据结构:递归的种类(Types of Recursion)
目录 尾递归(Tail Recursion) 什么是 Loop(循环)? 复杂度分析 头递归(Head Recursion) 树形递归(Tree Recursion) 线性递归(Linear Recursion)…...
Vue 3 + WebSocket 实战:公司通知实时推送功能详解
📢 Vue 3 WebSocket 实战:公司通知实时推送功能详解 📌 收藏 点赞 关注,项目中要用到推送功能时就不怕找不到了! 实时通知是企业系统中常见的功能,比如:管理员发布通知后,所有用户…...

密码学基础——SM4算法
博客主页:christine-rr-CSDN博客 专栏主页:密码学 📌 【今日更新】📌 对称密码算法——SM4 目录 一、国密SM系列算法概述 二、SM4算法 2.1算法背景 2.2算法特点 2.3 基本部件 2.3.1 S盒 2.3.2 非线性变换 编辑…...