嵌入式LinuxLED驱动开发实验

目录:

  • 1. Linux下LED灯的驱动原理
    • 1.1. 地址映射
      • 1.1.1. 实际物理地址映射到虚拟地址的函数
      • 1.1.2. 内存访问函数
  • 2.硬件原理图分析
  • 3. 实验程序编写
    • 3.1. 驱动程序编写
    • 3.2. 应用程序编写
  • 4. 运行测试

1. Linux下LED灯的驱动原理

我们在裸机实验的时候,都是通过配置底层的寄存器来进行点亮LED灯的操作的。我们现在还没有学习到设备树的相关知识,所以,我们也是通过在字符设备驱动框架的基础上来配置底层寄存器来实现LED灯的点亮,但是,与之前不同的是,在Linux系统中会存在地址映射的方式,所以,我们在裸机实验操作的实际的物理地址和在Linux系统下操作的地址是不同的,所以我们需要用一个宏来找到映射的虚拟地址下的实际地址,从而完成对底层寄存器的配置。在I.MX6U-ALPHA开发板上的LED是连接到I.MX6ULL的GPIO1_IO03这个引脚上的。

1.1. 地址映射

首先我们需要简单了解一下MMU这个东西,MMU全称叫做Memory Manage Unit,就是内存管理单元。在老版本的Linux中要求处理器必须有MMU,但是现在的Linux内核已经支持没有MMU的处理器了,MMU主要完成一下两个功能:

  1. 完成虚拟空间到物理空间的映射。
  2. 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
    我们需要重点了解第1点,也就是虚拟空间到物理空间的映射,也叫地址映射。我们处理器是32位的,对于32位的处理器来说,虚拟地址的范围是2^32=4GB,但是我们使用的开发板上只有512MB的DDR3,这512MB的内存就是物理内存,经过MMU可以将其映射到整个4GB的虚拟空间。
    在这里插入图片描述
    物理地址512M,虚拟地址4GB,那么肯定会存在多个虚拟地址映射到同一个物理地址上的情况,但是这个情况MMU会自动处理。

1.1.1. 实际物理地址映射到虚拟地址的函数

Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址,如果我们开启了MMU,并且设置了内存映射,我们就不能直接向实际的物理地址进行写入数据,这就涉及到怎么在知道实际物理地址的前提下得到虚拟地址,并且向这个虚拟地址里面写入数据的问题。这个问题其实内核有向外提供两个函数的接口,通过这两个接口就可以实现知道物理地址的情况下得到虚拟地址。这两个函数接口就是ioremap和iounmap。

  • ioremap函数

首先我们先从ioremap函数开始,ioremap函数主要是用于获取指定物理地址空间对应的虚拟地址空间,ioremap其实是个宏,函数原型如下:

#define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE)void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,unsigned int mtype){return arch_ioremap_caller(phys_addr, size, mtype,__builtin_return_address(0));}

我们可以看到ioremap中有两个参数:cookie和size,但其实真正起作用的是__arm_ioremap,此函数有三个参数和一个返回值,具体解析如下:
phys_addr:要映射的物理起始地址。
size:要映射的内存空间大小。
mtype:ioremap的类型,可以选择MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED和MT_DEVICE_WC,ioremap函数默认选择MT_DEVICE。
关于这几个模式有什么区别,可以看下如下解释:这四个都是内存类型的枚举,MT_DEVICE表示设备内存,MT_DEVICE_NONSHARED表示设备非共享内存,MT_DEVICE_CACHED表示设备缓存内存,MT_DEVICE_WC表示设备写结合内存。它们的区别在于访问速度、可共享性和可写性等方面。
但是,其实上述内容也不是很重要,我们只需要知道,使用ioremap函数能够使物理地址映射到虚拟地址空间就可以了。

返回值:__iomem类型的指针,指向映射后的虚拟内存空间。
比如说我们要获取IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03这个寄存器对应的虚拟地址,可以使用如下代码:

#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);

其中,使用宏定义来定义实际的物理地址,然后定义一个__iomem*类型的指针来存放映射过后的虚拟地址,然后通过ioremap来从实际的物理地址得到虚拟地址,我们都知道对于32位的处理器来说,每个寄存器都对应4个字节,因此,我们的size参数就设置为4。

  • iounmap函数

卸载驱动的时候需要使用iounmap函数来释放掉ioremap函数所映射的虚拟内存空间,如果没有使用iounmap来释放掉ioremap映射的内存空间,可能会导致内存泄漏,最后导致系统崩溃,所以一定要在module_exit函数中,释放掉映射后的内存空间。

iounmap的函数原型如下:

void iounmap (volatile void __iomem *addr)

其中只有一个参数,这个参数就是使用ioremap映射后得到的虚拟内存空间的地址。

这次我们实现控制对开发板上的LED灯的控制,那么我们就需要控制LED灯配置的相关寄存器,LED灯的相关寄存器在裸机实验的时候就已经学过,包括时钟寄存器CCM_CCGR1,引脚复用寄存器SW_MUX_GPIO1_IO03,引脚电气属性寄存器SW_PAD_GPIO1_IO03,引脚数据寄存器GPIO1_DR,引脚方向寄存器GPIO1_GDIR。

以下代码就是通过宏定义和变量定义的方法来进行物理地址和虚拟地址的定义的:

/* 寄存器物理地址 */
#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)/* 映射后的寄存器虚拟地址指针变量,后续会通过ioremap函数将映射后的虚拟内存地址保存在以下变量中 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

我们在配置这些寄存器之前,需要在module_init的led_init中使用ioremap映射这些地址,代码如下:

/* 寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

1.1.2. 内存访问函数

我们在使用ioremap函数将实际物理地址映射到虚拟地址以后,也需要将虚拟地址中的数据进行读写操作。我们可以使用指针来直接操作这些地址,但是,Linux内核并不建议怎么做,所以Linux内核提供了一组操作函数来对映射后的内存进行读写的操作。

  • 读操作函数
    读操作函数有以下几个:
 u8 readb(const volatile void __iomem *addr)u16 readw(const volatile void __iomem *addr)u32 readl(const volatile void __iomem *addr)

上述三个函数分别是对8位、16位、32位数据进行操作,一般我们操作寄存器的时候都是进行整体的操作,因为i.mx6ull的处理器是32位的,所以我们一般使用readl函数来进行寄存器的操作。
readl函数中也只有一个参数,就是实际的物理地址对应的虚拟地址。

  • 写操作函数
    写操作函数主要有以下几个:
 void writeb(u8 value, volatile void __iomem *addr)void writew(u16 value, volatile void __iomem *addr)void writel(u32 value, volatile void __iomem *addr)

这些操作函数也分别对应的是对8位、16位、32位的写操作,这几个函数有两个参数,value参数对应的是往虚拟地址空间中要写的数据,addr参数对应的是虚拟内存空间地址。

在将物理地址映射成为虚拟地址以后,我们就可以对这些虚拟地址进行配置,从而实现使LED灯高电平熄灭,低电平点亮的功能,代码如下:

/* 使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);	/* 清楚以前的设置 */
val |= (3 << 26);	/* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);/* 设置GPIO1_IO03的复用功能,将其复用为*    GPIO1_IO03,最后设置IO属性。*/
writel(5, SW_MUX_GPIO1_IO03);/*寄存器SW_PAD_GPIO1_IO03设置IO属性*bit 16:0 HYS关闭*bit [15:14]: 00 默认下拉*bit [13]: 0 kepper功能*bit [12]: 1 pull/keeper使能*bit [11]: 0 关闭开路输出*bit [7:6]: 10 速度100Mhz*bit [5:3]: 110 R0/6驱动能力*bit [0]: 0 低转换率*/
writel(0x10B0, SW_PAD_GPIO1_IO03);/* 设置GPIO1_IO03为输出功能 */
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);	/* 清除以前的设置 */
val |= (1 << 3);	/* 设置为输出 */
writel(val, GPIO1_GDIR);/* 默认关闭LED */
val = readl(GPIO1_DR);
val |= (1 << 3);	
writel(val, GPIO1_DR);

2.硬件原理图分析

在这里插入图片描述
我们可以看到,LED灯的负极连接LED0这个引脚,如果给LED0低电平的话,这样就可以从VCC_3V3生成灌电流,点亮LED灯,所以就需要给LED0这个引脚置0。
我们可以从原理图中找到对应i.mx6ull芯片上的引脚。在这里插入图片描述
我们可以看到LED0接在了GPIO_3上,这个GPIO_3就是GPIO1_IO03,当GPIO1_IO03输出低电平的时候,LED灯就会点亮,所以LED0输出0就亮,LED0输出1就灭。

3. 实验程序编写

我们此次的实验的目标是使用应用程序来控制驱动程序,从而可以控制开发板上的LED灯的亮灭。我们在应用程序中输入1则LED灯亮,如果输入0以后则LED灯灭。

3.1. 驱动程序编写

驱动程序最主要的功能首先是字符设备驱动框架的搭建,以及物理地址映射,然后对虚拟地址的内容进行操作,最后通过write函数来实现应用程序对驱动程序的控制,最后实现LED灯亮灭的控制。
具体代码如下:(通过备注基本能看懂代码)

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>#define LED_NAME "led"           //注册的设备的名字
#define LED_MAJOR  249           //注册的主设备号#define LEDOFF 0
#define LEDON  1/* 寄存器物理地址 */
#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;/** @description		: LED打开/关闭* @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED* @return 			: 无*/
void led_switch(u8 sta)
{u32 val = 0;if(sta == LEDON) {val = readl(GPIO1_DR); //读取DR寄存器中的数据val &= ~(1 << 3);	   //关灯的数据赋值valwritel(val, GPIO1_DR); //将关灯数值写入DR寄存器中}else if(sta == LEDOFF) {val = readl(GPIO1_DR);val|= (1 << 3);	writel(val, GPIO1_DR);}	
}/*open,read,write,release函数函数原型就是这样,我们照着这个函数原型写就好了,后面会用到函数的部分参数*/
static int led_open(struct inode* inode,struct file *filp)
{return 0;
}/*read函数就是用户空间从内核空间读取,是从用户空间来考虑的,所以驱动程序的read函数其实是向用户空间进行write*/
static ssize_t led_read(struct file *filp,char __user* buf,size_t cnt,loff_t* offt)
{return 0;
}/*write函数就是用户空间向内核空间写数据,所以驱动程序的write函数其实是从用户空间进行read*/
static ssize_t led_write(struct file* filp,const char __user* buf,size_t cnt,loff_t* offt)
{int retvalue = 0;unsigned char databuf[1];unsigned char ledstat;retvalue = copy_from_user(databuf, buf, cnt); //从应用程序中读取写入的数值if(retvalue < 0) {printk("kernel write failed!\r\n");return -EFAULT;}ledstat = databuf[0];		/* 获取状态值 */if(ledstat == LEDON) {	led_switch(LEDON);		/* 打开LED灯 */} else if(ledstat == LEDOFF) {led_switch(LEDOFF);	/* 关闭LED灯 */}return 0;
}static int led_release(struct inode* inode,struct file *filp)
{return 0;
}static struct file_operations led_fops = {.owner = THIS_MODULE,       //.owner,一般都使用THIS_MODULE,具体我也不明白,但是都是使用THIS_MODULE.open = led_open,        //open函数,对应应用程序中的open函数.read = led_read,        //read函数,对应应用程序中的read函数.write = led_write,      //write函数,对应应用程序中的write函数.release = led_release,  //relaese函数,对应应用程序中的release函数
};static int __init led_init(void) //模块挂载以后运行的初始化函数
{int ret = 0;u32 val = 0;/* 初始化LED *//* 1、寄存器地址映射 */IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);/* 2、使能GPIO1时钟 */val = readl(IMX6U_CCM_CCGR1);val &= ~(3 << 26);	/* 清楚以前的设置 */val |= (3 << 26);	/* 设置新值 */writel(val, IMX6U_CCM_CCGR1);/* 3、设置GPIO1_IO03的复用功能,将其复用为*    GPIO1_IO03,最后设置IO属性。*/writel(5, SW_MUX_GPIO1_IO03);/*寄存器SW_PAD_GPIO1_IO03设置IO属性*bit 16:0 HYS关闭*bit [15:14]: 00 默认下拉*bit [13]: 0 kepper功能*bit [12]: 1 pull/keeper使能*bit [11]: 0 关闭开路输出*bit [7:6]: 10 速度100Mhz*bit [5:3]: 110 R0/6驱动能力*bit [0]: 0 低转换率*/writel(0x10B0, SW_PAD_GPIO1_IO03);/* 4、设置GPIO1_IO03为输出功能 */val = readl(GPIO1_GDIR);val &= ~(1 << 3);	/* 清除以前的设置 */val |= (1 << 3);	/* 设置为输出 */writel(val, GPIO1_GDIR);/* 5、默认关闭LED */val = readl(GPIO1_DR);val |= (1 << 3);	writel(val, GPIO1_DR);ret = register_chrdev(LED_MAJOR,LED_NAME,&led_fops);//注册字符设备驱动if(ret < 0){/*如果注册字符设备驱动的返回值小于0,则字符设备注册失败,如果等于0则注册成功*/printk("led driver register failed!!\r\n");return -1;}printk("led init success!!\r\n");//如果注册成功,打印出一行注册成功的信息return 0;
}static void __exit led_exit(void) //模块卸载以后运行的退出函数
{/* 取消映射 */iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO03);iounmap(SW_PAD_GPIO1_IO03);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);unregister_chrdev(LED_MAJOR,LED_NAME);//注销字符设备驱动printk("led exit success!!\r\n");
}module_init(led_init); //注册模块加载函数
module_exit(led_exit); //注册模块卸载函数
MODULE_LICENSE("GPL");  //添加许可证信息
MODULE_AUTHOR("kk");    //添加模块作者信息

3.2. 应用程序编写

应用程序主要实现的功能是通过ledAPP应用程序实现对/dev/led的控制,当输入1的时候,开发板上的LED灯打开,当输入0的时候,开发板上的LED灯关闭,具体代码如下所示:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"/**@description  : main主程序*@param - argc : argv数组的元素个数*@param - argv : main函数所要用到的具体的参数*@return       : 0 成功;其他:失败 */
int main(int argc,char *argv[])
{int fd,retvalue;    //定义文件描述符和返回值变量char *filename;     //用来存放打开后的文件名unsigned char databuf[1];/** 判断应用程序使用是否输入的三个参数,需要使用./chrdevAPP /dev/chrdev 1 或 ./chrdevAPP /dev/chrdev 0来进行读写操作* 如果使用的是1,则向内核中写入数据;如果输入为0,则从内核读取数据*/if(argc != 3){printf("ERROR usage!\r\n");return -1;}filename = argv[1];//使用命令的时候,/dev/chrdev就是文件名,文件名保存在argv[1]中,将argv[1]的参数赋值filenamefd = open(filename,O_RDWR);//使用读写方式打开驱动文件if(fd < 0)//错误处理{printf("can't open file %s\r\n",filename);return -1;}databuf[0] = atoi(argv[2]);	/* 要执行的操作:打开或关闭 *//* 向/dev/led文件写入数据 */retvalue = write(fd, databuf, sizeof(databuf));if(retvalue < 0){printf("LED Control Failed!\r\n");close(fd);return -1;}retvalue = close(fd); //close函数,如果要关闭某个文件,就直接用close函数关闭文件描述符即可if(retvalue < 0){printf("can't close file %s\r\n",filename);return -1;}return 0;}

当编写完成驱动代码和应用程序代码以后,需要将Makefile文件中生成的目标文件名进行修改
在这里插入图片描述
然后在终端中分别通过make命令和arm-linux-gnueabihf-gcc ledAPP.c -o ledAPP来生成led.ko模块和ledAPP应用程序,拷贝到rootfs下进行测试。

4. 运行测试

在开发板中打开/rootfs/lib/modules/4.1.15然后可以看到有led.ko和ledAPP两个文件,然后使用命令depmod(第一次加载驱动的时候需要执行此命令)和modprobe led.ko来加载驱动。
在这里插入图片描述
驱动加载成功以后,需要查看设备的设备号,然后手动创建设备节点,查看设备号的命令如下:
在这里插入图片描述
我们可以看到,我们的led设备的设备号为249
在这里插入图片描述
手动创建设备节点的命令如下:
在这里插入图片描述
这个时候就可以使用ledAPP来控制开发板上的LED等的亮灭了。
使用命令./ledAPP /dev/led 1可以打开LED灯,输入命令以后
在这里插入图片描述
在这里插入图片描述
可以看到开发板上的LED灯亮了。

使用命令./ledAPP /dev/led 0可以关闭LED灯,输入命令以后
在这里插入图片描述
在这里插入图片描述

可以看到LED灯灭了。

到此,LED灯的驱动就可以正常运行。

然后通过rmmod led.ko命令可以卸载led的模块,通过rmmod led.ko命令卸载模块以后,整个LED灯驱动的开发流程就已经彻底完成了。

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

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

相关文章

【数据结构】红黑树

文章目录 红黑树1. 红黑树的概念2. 红黑树的性质3. 红黑树节点的定义4. 红黑树的结构5. 红黑树的插入操作 红黑树 1. 红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个节点上增加一个存储位表示节点的颜色&#xff0c;可以是Red或者是Black。通过任何一…

【Docker】Docker的部署含服务和应用、多租环境、Linux内核的详细介绍

前言 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。 &#x1f4d5;作者简介&#xff1a;热…

简化生活之让AI以指定格式输出

原文合集地址如下&#xff0c;有需要的朋友可以关注 本文地址 合集地址 今天京东也宣布即将发布了自己的大模型&#xff0c;那么使用AI大模型进行工作或者生活将是必不可少的步骤。 建立命令 AI大模型是一种生成式聊天对话模型&#xff0c;我们可以通过预先定义命令的方式…

HTML的Input(type)的属性都有哪些

&#x1f607;作者介绍&#xff1a;一个有梦想、有理想、有目标的&#xff0c;且渴望能够学有所成的追梦人。 &#x1f386;学习格言&#xff1a;不读书的人,思想就会停止。——狄德罗 ⛪️个人主页&#xff1a;进入博主主页 &#x1f33c;欢迎小伙伴们访问到博主的文章内容&am…

flutter开发实战-Running Gradle task ‘assembleDebug‘ 的解决方法

flutter开发实战-Running Gradle task ‘assembleDebug‘ 的解决方法 使用Android studio经常出现Running Gradle task ‘assembleDebug‘问题&#xff0c;记录一下解决方法。 一、在Android目录下更改build.gradle 将repositories中的google(), mavenCentral() repositori…

linux_driver_day10

作业1 题目&#xff1a; 使用驱动代码实现如下要求 应用程序通过阻塞的io模型来读取number变量的值 number是内核驱动中的一个变量 number的值随着按键按下而改变&#xff08;按键中断&#xff09; 例如 numbero 按下按键 number1&#xff0c;再次按下按键 number0 在按下按…

未跟踪的文件: (使用 “git add <文件>...“ 以包含要提交的内容)怎么移除这些内容

有时候我们常常修改一些内容 手动就是&#xff1a;rm -rf system/core/healthd/images/.png 怎么丢弃呢&#xff1f; git clean -f . 删除这种文件

【学会动态规划】三步问题(2)

目录 动态规划怎么学&#xff1f; 1. 题目解析 2. 算法原理 1. 状态表示 2. 状态转移方程 3. 初始化 4. 填表顺序 5. 返回值 3. 代码编写 写在最后&#xff1a; 动态规划怎么学&#xff1f; 学习一个算法没有捷径&#xff0c;更何况是学习动态规划&#xff0c; 跟我…

数据库应用:MySQL索引

目录 一、理论 1.MySQL三层逻辑架构 2.索引结构 3.MyISAM与InnoDB对比 4.sql优化 5.MySQL 索引 6.MySQL索引原理 二、实验 1.创建索引 三、总结 一、理论 1.MySQL三层逻辑架构 MySQL的存储引擎架构将查询处理与数据的存储/提取相分离。 MySQL的逻辑架构图如下&…

PostgreSQL技术内幕(九)libpq通信协议

libpq通信协议是基于TCP/IP 协议的一套消息通信协议&#xff0c;它允许 psql、JDBC、PgAdmin等客户端程序传递查询给PostgreSQL后端服务器&#xff0c;并接收返回查询的结果。 在这次的直播中&#xff0c;我们为大家介绍了libpq通信协议的实现原理和执行机制&#xff0c;以下内…

springboot项目实战-API接口限流

1.简介 对接口限流的目的是通过对并发访问/请求进行限速&#xff0c;或者对一个时间窗口内的请求进行限速来保护系统&#xff0c;一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。 1.1.为什么需要限流? 大量正常用户高频访问导致服务器宕机恶意用户高频访问导致服…

stm32(时钟和中断事件知识点)

一、复位和时钟控制&#xff08;RCC&#xff09; 复位 系统复位 当发生以下任一事件时&#xff0c;产生一个系统复位&#xff1a; 1. NRST引脚上的低电平(外部复位) 2. 窗口看门狗计数终止(WWDG复位) 3. 独立看门狗计数终止(IWDG复位) 4. 软件复位(SW复位) 5. 低功耗管…