Linux设备驱动之gpio-keys
Linux设备驱动之gpio-keys
前两个章节介绍了Linux字符设备和platform设备的注册,他们都是比较基础的,让大家理解Linux内核的设备驱动是如何注册、使用的。但在工作中,个人认为完全手写一个字符设备驱动的机会比较少,更多的都是基于前人的代码修修补补过三年。在内核驱动中,更多的会基于platform设备进行具体设备驱动的注册与使用,下面以内核原生的gpio-keys为例,向大家介绍一个简单的按键驱动是如何配置、工作的。
dts配置
前面有简单的介绍,设备树用于描述板端硬件信息,配合驱动进行使用的,Linux内核原生的gpio-keys驱动支持的dts如下:
gpio-keys {compatible = "gpio-keys"; /* 用于匹配内核gpio-keys驱动 */autorepeat; /* 标记是否自动重复该按键,想想常按的情况 */up {label = "GPIO Key UP"; /* 按键的标签 */linux,code = <103>; /* 按键的键值,理解为键盘上的字母类 */gpios = <&gpio1 0 1>; /* 使用的是哪个gpio */};down {label = "GPIO Key DOWN";linux,code = <108>;interrupts = <1 IRQ_TYPE_LEVEL_HIGH 7>; /* 中断配置 */};};
上面介绍到的dts配置,最后在驱动端都会进行解析,使用,那么驱动又具体是如何使用的呢,继续。
gpio-keys的platform驱动
Linux内核基本都可以看到类似以下的代码:
[drivers/input/keyboard/gpio_keys.c]/* 设备驱动匹配信息 */
static const struct of_device_id gpio_keys_of_match[] = {{ .compatible = "gpio-keys", },{ },
};
MODULE_DEVICE_TABLE(of, gpio_keys_of_match);static struct platform_driver gpio_keys_device_driver = {.probe = gpio_keys_probe, /* platform设备驱动匹配了,则调用probe函数 */.shutdown = gpio_keys_shutdown,.driver = {.name = "gpio-keys",.pm = &gpio_keys_pm_ops,.of_match_table = gpio_keys_of_match,.dev_groups = gpio_keys_groups,}
};static int __init gpio_keys_init(void)
{/* 注册platform driver */return platform_driver_register(&gpio_keys_device_driver);
}static void __exit gpio_keys_exit(void)
{platform_driver_unregister(&gpio_keys_device_driver);
}/* late_initcall与modeule_init的宏作用一样,* 都是将函数添加到驱动段,方便开机启动加载驱动,* late_initcall与modeule_init的差异是加载* 的优先级不一致*/
late_initcall(gpio_keys_init);
module_exit(gpio_keys_exit);
在platform和设备树章节已经介绍,当设备驱动匹配时,将会调用到probe函数。下面看看gpio-keys的probe是怎样获取dts信息的。PS:dts是怎么解析的,platform device是什么时候注册的,后面其他章节我们再进行介绍。
/** Handlers for alternative sources of platform_data*//** Translate properties into platform_data*/
static struct gpio_keys_platform_data *
gpio_keys_get_devtree_pdata(struct device *dev)
{struct gpio_keys_platform_data *pdata;struct gpio_keys_button *button;struct fwnode_handle *child;int nbuttons;/* 查看当前的gpio-keys节点有多少个子节点,每个节点代表一个按键 */nbuttons = device_get_child_node_count(dev);if (nbuttons == 0)return ERR_PTR(-ENODEV);pdata = devm_kzalloc(dev,sizeof(*pdata) + nbuttons * sizeof(*button),GFP_KERNEL);if (!pdata)return ERR_PTR(-ENOMEM);button = (struct gpio_keys_button *)(pdata + 1);pdata->buttons = button;pdata->nbuttons = nbuttons;/* 是否自动重复 */pdata->rep = device_property_read_bool(dev, "autorepeat");/* 按键的标签 */device_property_read_string(dev, "label", &pdata->name);device_for_each_child_node(dev, child) {if (is_of_node(child))button->irq =irq_of_parse_and_map(to_of_node(child), 0);/* 按键码值 */if (fwnode_property_read_u32(child, "linux,code",&button->code)) {dev_err(dev, "Button without keycode\n");fwnode_handle_put(child);return ERR_PTR(-EINVAL);}fwnode_property_read_string(child, "label", &button->desc);/* 输入的类型,一般设置为KEY */if (fwnode_property_read_u32(child, "linux,input-type",&button->type))button->type = EV_KEY;/* 该按键是否设置为唤醒源 */button->wakeup =fwnode_property_read_bool(child, "wakeup-source") ||/* legacy name */fwnode_property_read_bool(child, "gpio-key,wakeup");/* 唤醒的状态 */fwnode_property_read_u32(child, "wakeup-event-action",&button->wakeup_event_action);/* 是否可休眠 */button->can_disable =fwnode_property_read_bool(child, "linux,can-disable");/* 按键去抖配置 */if (fwnode_property_read_u32(child, "debounce-interval",&button->debounce_interval))button->debounce_interval = 5;button++;}return pdata;
}
在上面的代码可以看到,就是一个解析dts的操作,gpio_keys_get_devtree_pdata()将会进行以下操作:
- 确认gpio-keys节点存在多少个子节点,每个子节点代表一种按键功能;
- 获取”autorepeat“字段的信息赋值到pdata->rep,这个变量决定着是否允许重复发送事件;
- 获取各button的中断号、事件代码code、label、输入的类型、唤醒等信息;
之后驱动会依据这些信息进行按键驱动的适配以及操作。下面通过gpio-keys驱动学习Linux input子系统。
input子系统
Linux input子系统是linux内核用于管理各种输入设备的部分,内核将给用户导出一套固定的硬件无关的input API,供用户空间程序使用。
input系统分为三块:input core、input drivers和event handles。数据传输从底层硬件到input driver,再经过input core到event handles,最后到达用户空间。
input core
input子系统的core代码主要是input.c,该文件集成模块,模块的注册函数实现如下:
static int __init input_init(void)
{int err;/* 注册input类 */err = class_register(&input_class);if (err) {pr_err("unable to register input_dev class\n");return err;}/* 在/proc创建 bus/input/devices handlers */err = input_proc_init();if (err)goto fail1;/* 注册input字符设备 */err = register_chrdev_region(MKDEV(INPUT_MAJOR, 0),INPUT_MAX_CHAR_DEVICES, "input");if (err) {pr_err("unable to register char major %d", INPUT_MAJOR);goto fail2;}return 0;fail2: input_proc_exit();fail1: class_unregister(&input_class);return err;
}
在input.c是input core的核心文件,input core主要是承上启下,为input drivers提供输入设备注册和操作接口,如input_register_device()函数;通知event handles对事件进行处理;在/proc下产生相应的设备信息。input core将会负责将input drivers和event handles联通,具体是如何完成这个操作的呢,继续看input drivers和event handles。
相关结构体
[linux/input.h]
/** The event structure itself*/struct input_event {struct timeval time; /* 输入事件时间 */__u16 type; /* 类型 */__u16 code; /* 事件代码 [linux/input-event-codes.h] */__s32 value; /* 事件值(当type为EV_KEY时,value:0表示按键抬起,1表示按键按下) */
};[linux/input-event-codes.h]
/** Event types*/#define EV_SYN 0x00 /* 所有input设备都具备的同步事件,主要是与client同步事件队列 */
#define EV_KEY 0x01 /* 按键 */
#define EV_REL 0x02 /* 鼠标事件 相对坐标 */
#define EV_ABS 0x03 /* 手写板事件 绝对坐标 */
#define EV_MSC 0x04 /* 其他类型 */
#define EV_SW 0x05 /* 开关状态事件 */
#define EV_LED 0x11 /* LED事件 */
#define EV_SND 0x12 /* 音频事件 */
#define EV_REP 0x14 /* 用于指定自动重复事件 */
#define EV_FF 0x15 /* 用于初始化具有力反馈功能的设备并使该设备反馈 */
#define EV_PWR 0x16 /* 电源管理 */
#define EV_FF_STATUS 0x17 /* 用于接收力反馈设备状态 */
#define EV_MAX 0x1f
#define EV_CNT (EV_MAX+1)
gpio_keys_probe()
继续回到gpio_keys_probe()函数,从dts中获取到button信息之后,将会通过调用devm_input_allocate_device()([input.c])函数创建input_dev设备。接着是通过gpio_keys_setup_key()函数,为各个button申请相应的GPIO、中断资源等。最后,通过input_register_device()函数,注册input设备。至此,可以看到input drivers注册完成。
input handle
input handle的相关操作在evdev.c实现,该文件恰好自己构成一个模块,先分析模块的注册函数,注册函数只是调用了input_register_handler()([input.c]),该函数主要是为系统中的输入设备注册一个新的输入处理程序,并将其附加到与该处理程序兼容的所有输入设备上。该模块将在input dev注册时,将会通过input core完成与input dev完成相应的connect,当event发生时,又将会处理相应的event事件并上报。
input core、input drivers和event handles三块的简单介绍如上,下面,我们将带着问题去阅读代码,进一步了解Linux input子系统。
FAQ
input core是如何知道,当前有多少输入设备,而这些输入设备又分别是支持什么类型的事件?
在介绍上面的input drivers的时候就有提到,在driver的probe函数中,将会通过input_register_device()函数注册input device,下面来分析一下该函数的实现。
int input_register_device(struct input_dev *dev)
{/* 检查input dev支持的事件类型,注册device等 */...error = mutex_lock_interruptible(&input_mutex);if (error)goto err_device_del;/* 将该input device添加到input_dev_list链表 */list_add_tail(&dev->node, &input_dev_list);/* 在linux input子系统中,一个input device的输入事件* 将会发送到系统中所有的event handles,所以这里从保存* event handles的全局链表input_handler_list中,逐个获* 取event handles添加input device */list_for_each_entry(handler, &input_handler_list, node)input_attach_handler(dev, handler);/* 当注册input device成功,将会通过该函数唤醒profs的poll线程 */input_wakeup_procfs_readers();mutex_unlock(&input_mutex);...
}
然后我们再回到这个问题,input core是如何知道有多少的input devices,显然,是在通过input_register_device()注册input device的时候,同时会把input device添加到全局链表input_dev_list[input.c]中,这样,input core通过枚举input_dev_list链表,就可以得到相应的input device。
还剩一个问题,这些input device支持的事件类型和事件代码,又是从哪里填充的呢?
上面在介绍input device的时候,我们是以gpio-keys为例,同样的,在这里我们继续以它为例,也即gpio_keys.c。
在input device的probe()函数中,如上面介绍,将会通过gpio_keys_get_devtree_pdata()函数从dts中获取button的相应信息,接着通过gpio_keys_setup_key()将上面获取得到的信息填充到input dev,比如通过input_set_capability()函数设置input dev支持的事件类型等。
这样,linux内核就知道,当前的input dev支持什么事件type以及code。
当发生输入事件时,信息又是如何传递到应用层(用户空间)?
继续以gpio-keys作为input dev例子进行这个问题的解答。
回到gpio_keys_setup_key()函数,在该函数中,获取gpio之后,将会申请相应的中断,同时设置中断函数,中断函数有两个,分别是gpio_keys_gpio_isr()和gpio_keys_irq_isr(),将会根据gpio的信息相应的选择其中一个,以gpio_keys_gpio_isr()中断函数,当相应的gpio中断信号到来时,系统将会调用该函数,而在该函数中,又将存在以下代码:
static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
{...mod_delayed_work(system_wq,&bdata->work,msecs_to_jiffies(bdata->software_debounce));...
同时的,在gpio_keys_setup_key()函数中,是这样初始化bdata->work,所以,在中断函数中,设置延迟一段时间执行gpio_keys_gpio_work_func()函数。
INIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func);
而gpio_keys_gpio_work_func()函数也很简单,主要是通过gpio_keys_gpio_report_event()函数报告相应的事件信息。
static void gpio_keys_gpio_work_func(struct work_struct *work)
{struct gpio_button_data *bdata =container_of(work, struct gpio_button_data, work.work);gpio_keys_gpio_report_event(bdata);if (bdata->button->wakeup)pm_relax(bdata->input->dev.parent);
}static void gpio_keys_gpio_report_event(struct gpio_button_data *bdata)
{const struct gpio_keys_button *button = bdata->button;struct input_dev *input = bdata->input;unsigned int type = button->type ?: EV_KEY;int state;/* 获取gpio的状态,是高电平还是低电平 */state = gpiod_get_value_cansleep(bdata->gpiod);if (state < 0) {dev_err(input->dev.parent,"failed to get gpio state: %d\n", state);return;}/* 通过input_event()函数报告input事件 */if (type == EV_ABS) {if (state)input_event(input, type, button->code, button->value);} else {input_event(input, type, *bdata->code, state);}/* 同步事件 */input_sync(input);
}void input_event(struct input_dev *dev,unsigned int type, unsigned int code, int value)
{unsigned long flags;/* 先检查input dev是否支持该type,这个dev->evbit是在调用input_set_capability()时设置的 */if (is_event_supported(type, dev->evbit, EV_MAX)) {spin_lock_irqsave(&dev->event_lock, flags);input_handle_event(dev, type, code, value);spin_unlock_irqrestore(&dev->event_lock, flags);}
}
而在input_handle_event()函数中,将会先通过input_get_disposition()函数回去事件的相应信息。
#define INPUT_IGNORE_EVENT 0 /* 忽略该事件 */
#define INPUT_PASS_TO_HANDLERS 1 /* input handles处理该事件 */
#define INPUT_PASS_TO_DEVICE 2 /* input device处理该事件 */
#define INPUT_SLOT 4
#define INPUT_FLUSH 8
#define INPUT_PASS_TO_ALL (INPUT_PASS_TO_HANDLERS | INPUT_PASS_TO_DEVICE)
gpio-keys中,事件是由handles处理,所以在input_handle_event()函数中,将会将事件的信息填充到input dev的vals数组。至此,回到gpio_keys_gpio_report_event()函数,将事件信息填充到input dev的相应结构体之后,最后将会调用input_sync(input)函数同步事件。
static inline void input_sync(struct input_dev *dev)
{input_event(dev, EV_SYN, SYN_REPORT, 0);
}
通过input_sync()函数的代码我们可以知道,最终是发送EV_SYN事件,code为SYN_REPORT,这样的一个事件代码,将会在input_handle_event()函数中调用input_pass_values()函数,激发input handles处理事件。
而在input_pass_values()函数中,重要的也是以下部分:
static void input_pass_values(struct input_dev *dev,struct input_value *vals, unsigned int count)
{.../* 一般的,handle会为空,所以执行else部分代码 */handle = rcu_dereference(dev->grab);if (handle) {count = input_to_handler(handle, vals, count);} else {/* 从input dev的dev->h_list链表获取event handles,* 上面就有提到,一个input dev的事件,将会发送到所有的handles */list_for_each_entry_rcu(handle, &dev->h_list, d_node)if (handle->open) {count = input_to_handler(handle, vals, count);if (!count)break;}}...
}
代码跟踪到这里,又多了一个疑问:dev->h_list这个链表是什么时候填充的?
在input_register_device()函数中,通过调用input_attach_handler()函数,将event handles添加到dev->h_list,函数调用流程如下:
input_register_device()input_attach_handler()handler->connect(handler, dev, id)evdev_connect()input_register_handle()list_add_rcu(&handle->d_node, &dev->h_list)/list_add_tail_rcu(&handle->d_node, &dev->h_list)
了解到dev->h_list链表的填充过程之后,继续回到input_pass_values()函数,在该函数中,将会针对enable的event handle调用input_to_handler()函数,而在input_to_handler()函数重要的是调用handler的events函数—evdev_events()。
/** Pass incoming events to all connected clients.*/
static void evdev_events(struct input_handle *handle,const struct input_value *vals, unsigned int count)
{...client = rcu_dereference(evdev->grab);if (client)evdev_pass_values(client, vals, count, ev_time);else/* 主要是通过client_list链表获取handle client进行事件处理,* 可以将client理解为一个用户层的接收者,在open event时创建 */list_for_each_entry_rcu(client, &evdev->client_list, node)evdev_pass_values(client, vals, count, ev_time);...
}
在evdev_pass_values()函数中,将会通过__pass_event()将event信息填充到client的buffer缓冲区,如果code是SYN_REPORT,将会调用kill_fasync(&client->fasync, SIGIO, POLL_IN)异步通知应用层,这样,input event传递到应用层,接着,用户程序就可以通过read函数读取event的详细信息。
相关文章:
Linux设备驱动之gpio-keys
Linux设备驱动之gpio-keys 前两个章节介绍了Linux字符设备和platform设备的注册,他们都是比较基础的,让大家理解Linux内核的设备驱动是如何注册、使用的。但在工作中,个人认为完全手写一个字符设备驱动的机会比较少,更多的都是基…...
【vue3页面展示代码】展示代码codemirror插件
技术版本: vue 3.2.40、codemirror 6.0.1、less 4.1.3、vue-codemirror 6.1.1、 codemirror/lang-vue 0.1.2、codemirror/theme-one-dark 6.1.2 效果图: 1.安装插件 yarn add codemirror vue-codemirror codemirror/lang-vue codemirror/theme-one-dar…...
【面试必刷TOP101】链表相加 单链表的排序
目录 题目:链表相加(二)_牛客题霸_牛客网 (nowcoder.com) 题目的接口: 解题思路: 代码: 过啦!!! 题目:单链表的排序_牛客题霸_牛客网 (nowcoder.com) 题目的接口:…...
Visual Studio复制、拷贝C++项目与第三方库配置信息到新的项目中
本文介绍在Visual Studio软件中,复制一个已有的、配置过多种第三方库的C项目,将其拷贝为一个新的项目,同时使得新项目可以直接使用原有项目中配置好的各类**C**配置、第三方库等的方法。 在撰写C 代码时,如果需要用到他人撰写的第…...
rust迭代器
迭代器用来遍历容器。 迭代器就是把容器中的所有元素按照顺序一个接一个的传递给处理逻辑。 Rust中的迭代器 标准库中定义了Iterator特性 trait Iterator {type Item;fn next(&mut self) -> Option<Self::Item>; }实现了Iterator特性的结构体就是迭代器。 很多类…...
软件定制开发的优势与步骤|APP搭建|小程序
软件定制开发的优势与步骤|APP搭建|小程序 定制开发的优势: 1. 满足特定需求:定制开发可以根据客户的实际需求进行设计和开发,使得软件系统能够更好地满足客户的业务目标。 2. 优化用户体验:通过深入了解客户的需求,定…...
ERR_CONNECTION_REFUSED等非标准的HTTP错误状态码原因分析和解决办法
文章目录 一、DNS Resolution Failed1,DNS服务器故障2,DNS配置错误3,DNS劫持4,域名过期-5,其他网络问题 二、ERR_CONNECTION_REFUSED-"ERR_CONNECTION_REFUSED" 错误可能有多种原因 三、ERR_SSL_PROTOCOL_ER…...
瀑布流 - Vue3基于Grid布局简单实现一个瀑布流组件
瀑布流 - Vue3基于Grid布局简单实现一个瀑布流组件 前言 在学习Grid布局之时,我发现其是CSS中的一种强大的布局方案,它将网页划分成一个个网格,可以任意组合不同的网格,做出各种各样的布局,在刷某书和某宝首页时&…...
ES6面试题总结
1. 谈谈你对 ES6 的理解,为什么要学习es6? ES6是新一代的JS语言标准,对分JS语言核心内容做了升级优化,规范了JS使用标准,新增了JS原生方法,使得JS使用更加规范,更加优雅,更适合大型应用的开发。学习ES6是成…...
mybatisplus,jdbc 批量插入
1.测试用例 项目中遇到在做导入号码的时候我们会用到批量导入,提高入库的速度。接下来我们以10000条为测试用例。 1.1 批量执行sql语句 当需要成批插入或者更新记录时,可以采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库…...
如何使用IP归属地查询API来追踪网络活动
引言 在当今数字化世界中,了解网络活动的源头和位置对于网络安全、市场研究和用户体验至关重要。IP归属地查询API是一种强大的工具,可以帮助您追踪网络活动并获取有关IP地址的重要信息。本文将探讨如何使用IP归属地查询API来追踪网络活动,以…...
【SQL】S0 系列博文大纲
系列博文大纲 SQL 学习环境建议系列博文相关书籍系列博文大纲阶段进展 SQL 学习环境建议 对于 SQL 语言的学习,博主本地使用:MySQL DataGrip; MySQL 提供本地数据库服务; DataGrip IDE,承担编程运行测试任务…...
2023年8月体育用品行业数据分析(京东数据产品)
当前,亚运会临近,这也带动了国民对体育消费的热情,体育产品内销逐渐旺盛,“亚运经济”红利开始显现。鲸参谋数据显示,今年8月份,京东平台上体育用品行业的销量为185万,同比增长2%;销…...
国内高校镜像网站
国内各大高校开源镜像站 排名不分前后 清华大学:https://mirrors.tuna.tsinghua.edu.cn/ 北京大学:https://mirrors.pku.edu.cn/ 北京外国语大学:http:// https://mirrors.bfsu.edu.cn/ 北京理工大学:https://mirrors.bit.e…...
Linux安装kafka-manager
相关链接https://github.com/yahoo/kafka-manager/releases kafka-manager-2.0.0.2下载地址 百度云链接:https://pan.baidu.com/s/1XinGcwpXU9YBF46qkrKS_A 提取码:tzvg 一、安装部署 1.把kafka-manager-2.0.0.2.zip拷贝到目录 /opt/app/elk 2.解压…...
MYSQL索引——B+树讲解
B-/B树看 MySQL索引结构 B-树 B-树,这里的 B 表示 balance( 平衡的意思),B-树是一种多路自平衡的搜索树.它类似普通的平衡二叉树,不同的一点是B-树允许每个节点有更多的子节点。下图是 B-树的简化图. B-树有如下特点: 所有键值分布在整颗树中; 任何一…...
VB将十进制整数转换成16进制以内的任意进制数
VB将十进制整数转换成16进制以内的任意进制数 数值转换,能够将十进制整数转换成16进制以内的任意进制数 Private Function DecToN(ByVal x%, ByVal n%) As StringDim p() As String, y$, r%p Split("0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F", ",")I…...
基于SpringBoot+Vue的宠物领养饲养交流管理平台设计与实现
前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗 👇🏻…...
【图像去噪】【TGV 正则器的快速计算方法】通过FFT的总(广义)变化进行图像去噪(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
windbg调试句柄问题
这里写自定义目录标题 winform,句柄资源不够强,程序crash句柄主程序c程序,加载的插件是c# dll,这时候如何用windbg调试dll库如果查看句柄和对象的关系!handle 怎么能知道哪个句柄是Form对话框的句柄如何查看句柄对应的类对象 winf…...
eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)
说明: 想象一下,你正在用eNSP搭建一个虚拟的网络世界,里面有虚拟的路由器、交换机、电脑(PC)等等。这些设备都在你的电脑里面“运行”,它们之间可以互相通信,就像一个封闭的小王国。 但是&#…...
(十)学生端搭建
本次旨在将之前的已完成的部分功能进行拼装到学生端,同时完善学生端的构建。本次工作主要包括: 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要: 近期,在使用较新版本的OpenSSH客户端连接老旧SSH服务器时,会遇到 "no matching key exchange method found", "n…...
免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...
STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
Leetcode33( 搜索旋转排序数组)
题目表述 整数数组 nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前,nums 在预先未知的某个下标 k(0 < k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k1], …, nums[n-1], nums[0], nu…...
