嵌入式驱动初级-字符设备驱动基础

news/2024/5/7 19:30:14/文章来源:https://blog.csdn.net/weixin_44681745/article/details/128035813

文章目录

  • 前言
  • 一、驱动学习预备知识
    • 1.什么是设备驱动程序
    • 2.向内核添加新功能方法
      • 2.1新功能源码与Linux内核源码不在同目录下
      • 2.2在Ubuntu下加载和删除ko文件步骤
      • 2.3在开发板下加载和删除ko文件步骤
      • 2.4内核模块基础代码解析
  • 二、字符设备驱动框架
    • 2.1Linux内核对设备的分类
    • 2.2字符设备驱动开发步骤
    • 2.3字符设备驱动代码编写
    • 2.4字符设备应用层代码编写
    • 2.5字符设备驱动添加读写


前言

记录嵌入式驱动学习笔记

一、驱动学习预备知识

学习驱动前要了解的基础内容和命令。

1.什么是设备驱动程序

一种添加到操作系统中的特殊程序,主要作用是协助操作系统完成应用程序与对应硬件设备之间数据传送的功能。简言之,设备驱动程序就是操作系统中“驱动”对应硬件设备使之能正常工作的代码。

一个驱动程序主要完成如下工作:

  1. 初始化设备,让设备做好开始工作的准备
  2. 读数据:将设备产生的数据传递给上层应用程序
  3. 写数据:将上层应用程序交付过来的数据传递给设备
  4. 获取设备信息:协助上层应用程序获取设备的属性、状态信息
  5. 设置设备信息:让上层应用程序可以决定设备的一些工作属性、模式
  6. 其它相关操作:如休眠、唤醒、关闭设备等
    其中最核心的工作就是设备数据的输入和输出,因此计算机外部设备(外设)也被称为IO设备

2.向内核添加新功能方法

向内核添加新功能的方法分为两种,一种静态加载方法,一种动态加载方法。
静态加载方法思想:即新功能源码与内核其它代码一起编译进uImage文件内,在学习驱动的阶段我们经常采用动态加载方法因此不过多介绍该方法。
动态加载方法思想:即新功能源码与内核其它源码不一起编译,而是独立编译成内核的插件(被称为内核模块)文件.ko

2.1新功能源码与Linux内核源码不在同目录下

  1. cd ~/fs4412
  2. mkdir mydrivercode
  3. cd mydrivercode
  4. cp …/linux-3.14/drivers/char/myhello.c .
  5. vim Makefile
  6. make (生成的ko文件适用于主机ubuntu linux)
  7. make ARCH=arm (生成的ko文件适用于开发板linux,注意此命令执行前,开发板的内核源码已被编译)

#file命令可以查看指定ko文件适用于哪种平台,用法:
file ko文件
#结果带x86字样的适用于主机ubuntu linux,带arm字样的适用于开发板linux

2.2在Ubuntu下加载和删除ko文件步骤

sudo insmod ./???.ko  
#此处为内核模块文件名,将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod 
#查看已被插入的内核模块有哪些,显示的是插入内核后的模块名
sudo rmmod ??? 
#此处为插入内核后的模块名,此时将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
sudo dmesg -C  #清除内核已打印的信息
dmesg #查看内核的打印信息

2.3在开发板下加载和删除ko文件步骤

#先将生成的ko文件拷贝到/opt/4412/rootfs目录下:
cp ????/???.ko  /opt/4412/rootfs
#在串口终端界面开发板Linux命令行下执行
insmod ./???.ko  #将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod #查看已被插入的内核模块有哪些
rmmod ??? #将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
内核随时打印信息,我们可以在串口终端界面随时看到打印信息,不需要dmesg命令查看打印信息

2.4内核模块基础代码解析

Linux内核的插件机制——内核模块
类似于浏览器、eclipse这些软件的插件开发,Linux提供了一种可以向正在运行的内核中插入新的代码段、在代码段不需要继续运行时也可以从内核中移除的机制,这个可以被插入、移除的代码段被称为内核模块。

代码如下(示例):

#include <linux/module.h> //包含内核编程最常用的函数声明,如printk
#include <linux/kernel.h> //包含模块编程相关的宏定义,如:MODULE_LICENSE/*该函数在模块被插入进内核时调用,主要作用为新功能做好预备工作被称为模块的入口函数__init的作用 : 
1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text")))   实际是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .init.text区段
3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
*/
int __init myhello_init(void)
{/*内核是裸机程序,不可以调用C库中printf函数来打印程序信息,Linux内核源码自身实现了一个用法与printf差不多的函数,命名为printk (k-kernel)printk不支持浮点数打印*/printk("#####################################################\n");printk("#####################################################\n");printk("#####################################################\n");printk("#####################################################\n");printk("myhello is running\n");printk("#####################################################\n");printk("#####################################################\n");printk("#####################################################\n");printk("#####################################################\n");return 0;
}/*该函数在模块从内核中被移除时调用,主要作用做些init函数的反操作被称为模块的出口函数__exit的作用:
1.一个宏,展开后为:__attribute__ ((__section__ (".exit.text")))   实际也是gcc的一个特殊链接标记
2.指示链接器将该函数放置在 .exit.text区段
3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
*/
void __exit myhello_exit(void)
{printk("myhello will exit\n");
}/*
MODULE_LICENSE(字符串常量);
字符串常量内容为源码的许可证协议 可以是"GPL" "GPL v2"  "GPL and additional rights"  "Dual BSD/GPL"  "Dual MIT/GPL" "Dual MPL/GPL"等, "GPL"最常用其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息:myhello:module license 'unspecified' taints kernelDisabling lock debugging due to kernel taint
也会导致新模块没法使用一些内核其它模块提供的高级功能
*/
MODULE_LICENSE("GPL");/*
module_init 宏
1. 用法:module_init(模块入口函数名) 
2. 动态加载模块,对应函数被调用
3. 静态加载模块,内核启动过程中对应函数被调用
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
*/
module_init(myhello_init);/*
module_exit宏
1.用法:module_exit(模块出口函数名)
2.动态加载的模块在卸载时,对应函数被调用
3.静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
4.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
5.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
*/
module_exit(myhello_exit);

模块三要素:入口函数 出口函数 MODULE__LICENSE
Makefile文件用于生成ko模块,代码如下

ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR ?= 目标板linux内核源码顶层目录的绝对路径
ROOTFS ?= 目标板根文件系统顶层目录的绝对路径
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:rm -rf  *.o  *.ko  .*.cmd  *.mod.*  modules.order  Module.symvers   .tmp_versions
else
obj-m += hello.o
endif

内核接口头文件查询

  1. 首先在include/linux 查询指定函数:grep 名称 ./ -r -n
  2. 找不到则更大范围的include目录下查询,命令同上

二、字符设备驱动框架

2.1Linux内核对设备的分类

Linux内核按驱动程序实现模型框架的不同,将设备分为三类:

  1. 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
  2. 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节,随机访问,设缓存以提高效率。块设备不直接面向应用程序,一般使用一个文件系统去对接。
  3. 网络设备:针对网络数据收发的设备

在这里插入图片描述

2.2字符设备驱动开发步骤

A:设备号
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号。dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型,,其中高 12 位为主设备号,低 20 位为次设备号。应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备。
在这里插入图片描述
MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号,用法:

	dev_t devno;int major = 251;//主设备号int minor = 2;//次设备号devno = MKDEV(major,minor);

MAJOR宏用来从32位设备号中分离出主设备号,用法:

	dev_t devno = MKDEV(249,1);int major = MAJOR(devno);

MINOR宏用来从32位设备号中分离出次设备号,用法:

	dev_t devno = MKDEV(249,1);int minor = MINOR(devno);

如果已知一个设备的主次设备号,应用层指定好设备文件名,那么可以用mknod命令在/dev目录创建代表这个设备的文件,即此后应用程序对此文件的操作就是对其代表的设备操作,mknod用法如下:

//其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个
//字符设备,“200”是设备的主设备号,“0”是设备的次设备号。mknod /dev/chrdevbase c 200 0

B:申请和注销设备号
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数有两套如下所示:

	int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
参数:from:自己指定的设备号count:申请的设备数量name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号
返回值:成功为0,失败负数,绝对值为错误码int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name)
功能:动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
参数:dev:分配设备号成功后用来存放分配到的设备号baseminior:起始的次设备号,一般为0count:申请的设备数量name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号
返回值:成功为0,失败负数,绝对值为错误码分配成功后在/proc/devices 可以查看到申请到主设备号和对应的设备名,mknod时参数可以参考查到的此设备信息void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:from:已成功分配的设备号将被释放count:申请成功的设备数量释放后/proc/devices文件对应的记录消失

C:注册字符设备
在 Linux 中使用 cdev 结构体表示一个字符设备,在 cdev 中有两个重要的成员变量:ops 和 dev,这两个就是字符设备文件操作函数集合file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备。

1.定义 cdev 结构体变量

struct cdev test_cdev;

2.用 cdev_init 函数对其进行初始化
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,cdev_init 函数原型如下

//参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

3.cdev_add 函数用于向 Linux 系统添加字符设备
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数,完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。cdev_add 函数原型如下

//参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参
//数 count 是要添加的设备数量。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

4.使用 cdev_del 函数从 Linux 内核中删除相应的字符设备
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,cdev_del函数原型如下

//参数 p 就是要删除的字符设备。如果要删除字符设备
void cdev_del(struct cdev *p)

使用 cdev_init 函数初始化 cdev 变量的示例代码如下:

struct cdev testcdev;
/* 设备操作函数 */static struct file_operations test_fops = {.owner = THIS_MODULE,/* 其他具体的初始项 */};testcdev.owner = THIS_MODULE;cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */cdev_add(&testcdev, devid, 1);/* 添加字符设备 */

小结:

字符设备驱动开发步骤:

  1. 如果设备有自己的一些控制数据,则定义一个包含struct cdev cdev成员的结构体struct mydev,其它成员根据设备需求,设备简单则直接用struct cdev
  2. 定义一个struct mydev或struct cdev的全局变量来表示本设备;也可以定义一个struct mydev或struct cdev的全局指针(记得在init时动态分配)
  3. 定义三个全局变量分别来表示主设备号、次设备号、设备数
  4. 定义一个struct file_operations结构体变量,其owner成员置成THIS_MODULE
  5. module init函数流程:a. 申请设备号 b. 如果是全局设备指针则动态分配代表本设备的结构体元素 c. 初始化struct cdev成员 d. 设置struct cdev的owner成员为THIS_MODULE e. 添加字符设备到内核
  6. module exit函数:a. 注销设备号 b. 从内核中移除struct cdev c. 如果如果是全局设备指针则释放其指向空间
  7. 编写各个操作函数并将函数名初始化给struct file_operations结构体变量

验证操作步骤:

  1. 编写驱动代码mychar.c
  2. make生成ko文件
  3. insmod内核模块
  4. 查阅字符设备用到的设备号(主设备号):cat /proc/devices | grep 申请设备号时用的名字
  5. 创建设备文件(设备节点) : mknod /dev/??? c 上一步查询到的主设备号 代码中指定初始次设备号
  6. 编写app验证驱动(testmychar_app.c)
  7. 编译运行app,dmesg命令查看内核打印信息

2.3字符设备驱动代码编写

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
//主设备号
int major = 11;
//次设备号
int minor = 0;
//注册设备数量
int mychar_num = 1;
//设备名
char mycharname[] ="mychar";
//创建cdev类型的对象 cdevinit函数要用到
struct cdev mydev;
int mychar_open(struct inode *pnode, struct file *pfile) //打开设备
{printk("mychar_open is called\n");return 0;
}
int mychar_close(struct inode *pnode, struct file *pfile) //关闭设备
{printk("mychar_close is called\n");return 0;
}//内核驱动操作函数集合 cdevinit函数要用到
struct file_operations myops =
{.owner=THIS_MODULE,.open=mychar_open,.release=mychar_close,
};int __init mychar_init(void)
{//用来接收注册设备函数的返回值int ret = 0;//主设备号和次设备号合并dev_t devno = MKDEV(major,minor);/*1.申请设备号*///静态分配 手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号//ret = register_chrdev_region(devno,mychar_num,"mychar");ret = register_chrdev_region(devno,mychar_num,mycharname);//分配失败返回负数if(ret){//动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号//ret = alloc_chrdev_region(&devno,minor,mychar_num,"mychar");ret = alloc_chrdev_region(&devno,minor,mychar_num,mycharname);if(ret){printk("get devno failed\n");return -1;}//如果是动态分配说明自己设置的设备号被占用了 系统会更换主设备号 重新获取一下主设备号major = MAJOR(devno);}/*2.将struct cdev对象添加到内核对应的数据结构里*///初始化cdev 给struct cdev对象指定操作函数集cdev_init(&mydev,&myops);mydev.owner=THIS_MODULE;//将字符设备添加进内核 哈希管理列表cdev_add(&mydev,devno,1);return 0;
}void __exit mychar_exit(void)
{//主设备号和次设备号合并dev_t devno = MKDEV(major,minor);//注销字符设备驱动 入口参数:设备号 设备名称//unregister_chrdev(devno,"mychar");//将字符设备从内核移除cdev_del(&mydev);unregister_chrdev_region(devno,mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);

2.4字符设备应用层代码编写

#include <stdio.h>
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"int main(int argc, char *argv[])
{int fd = -1;if(argc < 2){printf("Error Usage!\r\n");return 1;}fd = open(argv[1],O_RDONLY);if(fd < 0){printf("Can't open file %s\r\n", argv[1]);return 2;}close(fd);fd = -1;return 0;
}

2.5字符设备驱动添加读写

ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
/*
完成功能:读取设备产生的数据
参数:filp:指向open产生的struct file类型的对象,表示本次read对应的那次openpbuf:指向用户空间一块内存,用来保存读到的数据count:用户期望读取的字节数ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置返回值:本次成功读取的字节数,失败返回-1
*/
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n)ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);  
/*
完成功能:向设备写入数据
参数:filp:指向open产生的struct file类型的对象,表示本次write对应的那次openpbuf:指向用户空间一块内存,用来保存被写的数据count:用户期望写入的字节数ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置返回值:本次成功写入的字节数,失败返回-1
*/
unsigned long copy_from_user (void * to, const void __user * from, unsigned long n)

添加读写的字符设备驱动代码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>#define BUF_LEN 100
//主设备号
int major = 11;
//次设备号
int minor = 0;
//注册设备数量
int mychar_num = 1;
//设备名
char mycharname[] ="mychar";
//创建cdev类型的对象 cdevinit函数要用到
struct cdev mydev;
//全局数组 字符设备产生的数据存放
char mydev_buf[BUF_LEN];
//数组下标从零开始
int curlen=0;
int mychar_open(struct inode *pnode, struct file *pfile) //打开设备
{printk("mychar_open is called\n");return 0;
}
int mychar_close(struct inode *pnode, struct file *pfile) //关闭设备
{printk("mychar_close is called\n");return 0;
}
//读数据是对于应用层来说 把内核数据拷贝到应用层来读取
/*
ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
完成功能:读取设备产生的数据
参数:filp:指向open产生的struct file类型的对象,表示本次read对应的那次openpbuf:指向用户空间一块内存,用来保存读到的数据count:用户期望读取的字节数ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置返回值:本次成功读取的字节数,失败返回-1
*/
ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{int ret = 0;//要读的数据大于数据的长度 修改要读数据的长度int size = 0;if(count > curlen){size = curlen;}else{size=count;}//将内核空间数据复制到用户空间 用户空间:puser 内核空间:mydev_bufret = copy_to_user(puser,mydev_buf,size);if(ret){printk("copy_to_user failed\n");return -1;}//将剩下的数据移动到数组起始 即删除已读数据memcpy(mydev_buf,mydev_buf + size,curlen - size);curlen = curlen - size;return size;
}
//写数据对于应用层来说 把数据写进驱动
/*
ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);  
完成功能:向设备写入数据
参数:filp:指向open产生的struct file类型的对象,表示本次write对应的那次openpbuf:指向用户空间一块内存,用来保存被写的数据count:用户期望写入的字节数ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置返回值:本次成功写入的字节数,失败返回-1
*/
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{int size = 0;int ret = 0;//如果剩余数组长度小于要写入的数据长度 则把要写入的数据修改 if(count > BUF_LEN - curlen){size = BUF_LEN - curlen;}else{size = count;}//从用户空间拷贝数据到内核空间ret = copy_from_user(mydev_buf + curlen,puser,size);if(ret){printk("copy_from_user failed\n");return -1;}curlen = curlen + size;return size;
}
//内核驱动操作函数集合 cdevinit函数要用到
struct file_operations myops =
{.owner=THIS_MODULE,.open=mychar_open,.release=mychar_close,.read=mychar_read,.write=mychar_write,
};int __init mychar_init(void)
{//用来接收注册设备函数的返回值int ret = 0;//主设备号和次设备号合并dev_t devno = MKDEV(major,minor);/*1.申请设备号*///静态分配 手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号//ret = register_chrdev_region(devno,mychar_num,"mychar");ret = register_chrdev_region(devno,mychar_num,mycharname);//分配失败返回负数if(ret){//动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号//ret = alloc_chrdev_region(&devno,minor,mychar_num,"mychar");ret = alloc_chrdev_region(&devno,minor,mychar_num,mycharname);if(ret){printk("get devno failed\n");return -1;}//如果是动态分配说明自己设置的设备号被占用了 系统会更换主设备号 重新获取一下主设备号major = MAJOR(devno);}/*2.将struct cdev对象添加到内核对应的数据结构里*///初始化cdev 给struct cdev对象指定操作函数集cdev_init(&mydev,&myops);mydev.owner=THIS_MODULE;//将字符设备添加进内核 哈希管理列表cdev_add(&mydev,devno,1);return 0;
}void __exit mychar_exit(void)
{//主设备号和次设备号合并dev_t devno = MKDEV(major,minor);//注销字符设备驱动 入口参数:设备号 设备名称//unregister_chrdev(devno,"mychar");//将字符设备从内核移除cdev_del(&mydev);unregister_chrdev_region(devno,mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_225707.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python(PyQt5)制作帮助文档查看器(可显示后缀名为md的文件)同时显示文本和图片

先看完整效果图: 帮助文档查看器是很多程序中必备要素,而利用Qt中的QTreeView组件可以很方便的查看文件,而QTextBrowser可以直接显示格式化的MarkDown文本。因此可以利用这两个组件制作一个帮助文件查看器。 未优化 效果图: 问题优化: 你会发现QT treeView列宽设置不成功问题…

2023年系统规划与设计管理师-第二章信息技术知识

1. 软件工程 2. 面向对象 3. 开发模型 4. 开发方法 4.1 敏捷开发方法 4.2 RUP 5. 数据仓库和网络技术 5.1 网络七层结构 5.2 各设备位于哪一次 5.3 各层的协议 5.4 TCP分层 5.5 IPv6 VS IPv4 5.6 IPv4 &#xff1a;A类、B类、C类地址的划分 A类地址的第一组数字为1&#xff5…

255-261BFC,媒体的类型,媒体的特性,浏览器前缀,媒体查询,逻辑操作符,

◼ 有时候可能会看到有些CSS属性名前面带有:-o-、-xv-、-ms-、mso-、-moz-、-webkit- ◼ 官方文档专业术语叫做:vendor-specific extensions(供应商特定扩展) ◼ 为什么需要浏览器前缀了?  CSS属性刚开始并没有成为标准,浏览器为了防止后续会修改名字给新的属性添加了浏…

软件测试面试,一定要准备的7个高频面试题(附答案,建议收藏)

收集了2022年最新的面试题后&#xff0c;负责就业的黑马讲师们整理出了7个高频出现的面试题&#xff0c;一起来看看。 高频问题1&#xff1a;请自我介绍下&#xff1f; 高频问题2&#xff1a;请介绍下最近做过的项目&#xff1f; 高频问题3&#xff1a;请介绍下你印象深刻的…

光学测量精度极限—光谱共焦位移传感器的六大行业应用

科技的不断发展&#xff0c;在半导体&#xff0c;高精密制造领域中都是采用微米及以上的加工工艺&#xff0c;并与之匹配高精度测量技术进行品质控制。光谱共焦的测量原理是一束白光经过镜头将不同的波长聚焦到光轴上&#xff0c;色散地形成一条彩虹状分布带&#xff0c;照射到…

力合精密装备科技:操纵盒按键说明

使用摇杆&#xff08;操纵盒&#xff09;&#xff1a; 不同的控制系统配备不同样式的操作杆&#xff0c;操作杆最常用的功能是用来手动移动机器来进行测量操作&#xff1b; 一般的操作杆上包含以下功能键&#xff1a; 急停按钮&#xff1a;紧急情况时按下急停按钮停止机器运…

ORB-SLAM2 ---- Tracking::TrackReferenceKeyFrame函数

目录 1.函数作用 2.步骤 3.code 4.函数解析 4.1 将当前帧的描述子转化为BoW向量 4.2 总体解释 1.函数作用 用参考关键帧的地图点来对当前普通帧进行跟踪。 2.步骤 Step 1&#xff1a;将当前普通帧的描述子转化为BoW向量 Step 2&#xff1a;通过词袋BoW加速当前帧与参考帧…

Go运行时的内存分配器以及消耗指定大小的内存(C语言)

对于go语言在运行时的一些内存分配&#xff0c;想要详细的了解&#xff0c;我们会用到自带的runtime.MemStats&#xff0c;有很多具体的细节实现&#xff0c;而不是简单的只看任务管理器中的内存分配。 我们先来看下这个记录内存分配器的结构体 type MemStats struct {Alloc …

一文了解 Go 中的指针和结构体

一文了解 Go 中的指针和结构体前言指针指针的定义获取和修改指针所指向变量的值结构体结构体定义结构体的创建方式小结耐心和持久胜过激烈和狂热。 前言 前面的两篇文章对 Go 语言的基础语法和基本数据类型以及几个复合数据类型进行介绍&#xff0c;本文将对 Go 里面的指针和结…

MySQL索引底层数据结构

索引简介 索引是一个排好序的数据结构&#xff0c;包含着对数据表里所有记录的引用指针&#xff0c;如下图所示。索引文件和数据文件一样都存储在磁盘中&#xff0c;数据库索引的目的是在检索数据库时&#xff0c;减少磁盘读取次数。 常见的索引数据结构包括二叉树、红黑树、…

跬智信息 (Kyligence) 荣获信创“大比武”重要奖项,坚持做大做实国产软件

近日&#xff0c;为期两个月的 2022 信创“大比武”活动圆满闭幕。经过层层筛选和考核&#xff0c;跬智信息 (Kyligence) 凭借“企业级智能多维数据分析解决方案”项目脱颖而出&#xff0c;在整体方案的技术架构、服务体系、安全架构、信创生态等方面得到了评委的高度认可&…

iptables应用大全

iptables四表五链&#xff1a; 1、“四表”是指 iptables 的功能 ——filter 表&#xff08;过滤规则表&#xff09;&#xff1a;控制数据包是否允许进出及转发 ——nat 表&#xff08;地址转换规则表&#xff09;&#xff1a;控制数据包中地址转换 ——mangle&#xff08;修改…

NDK 是什么 | FFmpeg 5.0 编译 so 库

前言 NDK 全称 Native Development Kit&#xff0c;也就是原生开发工具包 &#xff0c;官网对它有详细的 中文介绍 。可能一说到 NDK 或 JNI &#xff0c;大家脑子里第一反应就是集成 C/C 。其实 JNI 的含义是 Java Native Interface &#xff0c;这种接口允许 Java 和其他语言…

ovs vxlan 时延和吞吐

设计云时到底要不要用vxlan&#xff0c;如果用vxlan到底要不要购买比较贵的smart nic做offload&#xff0c;采用软件vxlan还是硬件交换机vxlan&#xff0c;很难决策&#xff0c;这儿简单测试一下&#xff0c;给个参考&#xff0c;资源终究是有限的&#xff0c;成本还是有考虑的…

【HDU No. 2586】 树上距离 How far away ?

【HDU No. 2586】 树上距离 How far away &#xff1f; 杭电 OJ 题目地址 【题意】 有n 栋房屋&#xff0c;由一些双向道路连接起来。 每两栋房屋之间都有一条独特的简单道路&#xff08;“简单”意味着不可以通过两条道路去一个地方&#xff09;。人们每天总是喜欢这样问&a…

Linux 软链接 与 硬链接 的区别

Linux 软链接 与 硬链接 的区别 1、概念 ​  链接文件&#xff1a;是 Linux 操作系统中的一种文件&#xff0c;主要用于解决文件的共享使用问题&#xff0c;而链接的方式分为两种——软链接和硬链接。 ​  inode&#xff1a;是文件系统中存储文件元信息&#xff08;文件的…

3.71 OrCAD新建原理图时,每一个类目的含义是什么?OrCAD软件怎么显示元器件的封装名称?

笔者电子信息专业硕士毕业&#xff0c;获得过多次电子设计大赛、大学生智能车、数学建模国奖&#xff0c;现就职于南京某半导体芯片公司&#xff0c;从事硬件研发&#xff0c;电路设计研究。对于学电子的小伙伴&#xff0c;深知入门的不易&#xff0c;特开次博客交流分享经验&a…

Word处理控件Aspose.Words功能演示:在 Python 中将 Word 文档转换为 PNG、JPEG 或 BMP

MS Word 文件到图像格式的转换让您可以将文档的页面嵌入到您的 Web 或桌面应用程序中。为了在 Python 应用程序中执行此转换&#xff0c;本文介绍了如何使用 Python 将 Word DOCX或DOC文件转换为PNG、JPEG或BMP图像。此外&#xff0c;您将学习如何使用不同的选项控制 Word 到图…

SpringBoot2.7.4整合Redis

目录 一、添加maven依赖 二、添加配置项 三、新增配置类 四、编辑实体类 五、编写接口 六、编写业务层 1.编写service层 2.编写service实现层 七、测试接口 一、添加maven依赖 <dependency><groupId>org.springframework.boot</groupId><artif…

Python测试框架之Pytest基础入门

Pytest简介 Pytest is a mature full-featured Python testing tool that helps you write better programs.The pytest framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries. 通过官方网站介绍…