

安装库libxcb-cursor.so.0,
sudo apt install libxcb-cursor0

安装库libxcb-cursor.so.0,
sudo apt install libxcb-cursor0

https://www.cnblogs.com/jiujiubashiyi/p/16429717.html
GLFW,GLAD,opengl是什么,有什么联系
GLFW、GLAD和OpenGL是计算机图形学中常用的三个库,它们之间存在着紧密的联系:
它们之间的联系:
综上,GLFW和GLAD都是帮助开发者更简单、更有效地使用OpenGL的辅助工具。通过它们,开发者可以专注于图形内容的创作,而不必担心底层的细节问题。
glfwMakeContextCurrent(window);是什么意思
glfwMakeContextCurrent(window); 是一行来自 GLFW(OpenGL Framework)库的代码。GLFW 是一个跨平台的库,用于窗口和输入处理,它使得创建 OpenGL 应用程序变得更加容易。
这句代码的作用是将 OpenGL 渲染上下文关联到由 window 参数指定的窗口。在 OpenGL 中,渲染上下文是用来进行图形绘制的环境,它定义了一系列可以用来配置和控制渲染行为的设置。
在创建窗口之后,必须调用 glfwMakeContextCurrent 函数来指定哪个窗口的上下文将被当前的线程使用。只有当前上下文中的 OpenGL 调用才会对指定的窗口产生影响。如果没有激活任何上下文,任何 OpenGL 调用都会导致错误。
简单来说,当你想要在特定的窗口上绘制图形时,你需要确保该窗口的 OpenGL 上下文被设置为当前上下文。这就是 glfwMakeContextCurrent(window); 这行代码的目的。
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress) 是什么意思
gladLoadGLLoader 是一个函数,通常在 C 或 C++ 程序中使用,用于初始化 OpenGL 函数指针。OpenGL 是一个用于渲染2D和3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。gladLoadGLLoader 函数是 GLAD 库的一部分,GLAD 是一个小型且易于使用的库,它负责在每次调用 OpenGL 函数之前,动态地加载和绑定正确的 OpenGL 函数指针。
glfwGetProcAddress 是另一个函数,属于 GLFW 库(一个用于创建窗口、输入处理等的前端库,常与OpenGL一起使用)。glfwGetProcAddress 的作用是获取指向特定OpenGL函数的指针,该函数在OpenGL核心或者扩展中定义。
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)
这表示在初始化GLAD库时,它将使用GLFW库提供的函数指针来加载OpenGL函数。这允许应用程序使用OpenGL函数,而不需要关心底层的具体实现细节。
这种机制的好处是,应用程序只需要链接到一个库(比如GLFW),而GLFW会负责查找和加载正确的OpenGL函数。这样做可以简化应用程序的编写和维护工作,同时也确保了应用程序可以轻松地与不同平台的OpenGL版本兼容。
双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素.3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。(图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出)
图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的

为了让OpenGL知道我们的坐标和颜色值构成的到底是什么,OpenGL需要你去指定这些数据所表示的渲染类型。我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出的这些提示叫做**图元(Primitive)**,任何一个绘制指令的调用都将把图元传递给OpenGL。这是其中的几个:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
图形渲染管线的第一个部分—-顶点着色器
把3D坐标转为另一种3D坐标
几何着色器
一组顶点作为输入,这些顶点形成图元,并且能够通过发出新的顶点来形成新的(或其他)图元来生成其他形状
图元装配
将顶点着色器(或几何着色器)输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定图元的形状
光栅化阶段
把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的**片段(Fragment)**。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据
片段着色器
片段着色器的主要目的是计算一个像素的最终颜色
Alpha测试和混合(Blending)阶段
这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同
GL_ARRAY_BUFFER目标用于表示顶点属性数据的缓冲区对象
在OpenGL中,VAO(Vertex Array Object)和VBO(Vertex Buffer Object)是用于管理顶点数据的重要概念,并且它们之间存在一定的关系。
关系:
总结来说,VAO用于管理顶点属性状态,而VBO用于存储实际的顶点数据。它们共同协作,使得在OpenGL中管理和使用顶点数据变得更加灵活和高效。
我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)
着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。
Uniform是另一种从我们的应用程序在 CPU 上传递数据到 GPU 上的着色器的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
在OpenGL中,VBO(Vertex Buffer Object)和VAO(Vertex Array Object)都是用于管理顶点数据的对象。它们之间的联系和区别如下:
联系:
区别:
总体来说,VBO和VAO在OpenGL中都扮演着非常重要的角色,它们都可以提高渲染效率,使得开发者可以更加方便地管理和操作顶点属性数据。其中,VBO主要用于存储和管理顶点数据,而VAO则是用于在绘制时快速激活和绑定多个顶点属性数组。
把两个角度都发送
试一下发后两个数据,看看是不是数据的问题
试试发5个
2024.6.3.15.52Matlab报错,遂改,无用!!!!
艺术家和程序员更喜欢使用纹理(Texture)。纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节.这样就可以让物体非常精细而不用指定额外的顶点
为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。
使用 Xlib 来获取窗口大小需要一些底层的操作,但可以通过以下步骤来实现:
首先,你需要安装 python-xlib 库。你可以使用以下命令在 Ubuntu 上安装:
sudo apt-get install python-xlib
然后,你可以使用下面的代码来获取当前活动窗口的大小:
from Xlib import display
def get_screen_size():
disp = display.Display()
screen = disp.screen()
root_win = screen.root
windowID = root_win.get_full_property(disp.intern_atom('_NET_ACTIVE_WINDOW'), 0).value[0]
window = disp.create_resource_object('window', windowID)
geometry = window.get_geometry()
return geometry.width, geometry.height
width, height = get_screen_size()
print("Window size: {} x {}".format(width, height))
这段代码中,我们首先创建了一个 Display 对象,然后获取了当前活动窗口的 ID。接着,我们使用这个窗口 ID 创建了一个 window 对象,并通过这个对象的 get_geometry 方法获取了窗口的宽度和高度。
请注意,使用 Xlib 需要对 X 窗口系统有一定的了解,因为它是一个底层的库,直接和 X 服务器进行交互。希望这个示例能够帮助你开始使用 Xlib 来获取窗口大小。
layout(location=0): 这是一个着色器布局限定符(layout qualifier),用于指定顶点属性在输入阶段的位置。在这里,location=0 表示顶点属性的位置索引为 0。这个位置索引将与顶点数组对象(VAO)中的对应属性绑定,以确保正确地将顶点数据传递给顶点着色器。in: 这是一个输入变量修饰符,用于指示这个变量是从外部传递给顶点着色器的。vec3: 这是指定变量类型的关键字,表示这个变量是一个三维向量。in_position: 这是变量的名称,用于在顶点着色器中引用这个输入变量。在这里,in_position 可能表示顶点的位置信息。在OpenGL中,gl_Position是一个内置的变量,用于表示顶点着色器(Vertex Shader)输出的顶点位置。它是一个四维向量(vec4),表示顶点的齐次坐标(Homogeneous Coordinates),通常用于表示三维空间中的点。齐次坐标是四维的,其中前三个分量表示点的位置,而第四个分量通常被用于表示点的类型或者进行透视除法(Perspective Division)。在顶点着色器中,对 gl_Position 的设置将影响后续的图元装配(Primitive Assembly)和光栅化(Rasterization)阶段,最终确定绘制的像素位置。因此,正确设置 gl_Position 是绘制正确图形的关键。
[[ 0.5 0.5 0. 0. 1. 0. ]
[-0.5 0.5 0. 1. 0. 0. ]
[-0.5 -0.5 0. 0. 0. 1. ]
[ 0.5 0.5 0. 0. 1. 0. ]
[-0.5 -0.5 0. 0. 0. 1. ]
[ 0.5 -0.5 0. 1. 0. 0. ]]
layout(location=0) in vec3 in_position;
layout(location=1) in vec3 in_color;
self.vbo_format = '3f 3f'
self.attrs = ('in_position', 'in_color')
vertex_data = np.hstack([vertices_array, colors_array])


大多数旋转函数需要用弧度制的角,但幸运的是角度制的角也可以很容易地转化为弧度制的:
角度 = 弧度 * (180.0f / PI)弧度 = 角度 * (PI / 180.0f)
我这一辈子,抠抠搜搜的花了很多钱,精精明明的上了很多当。骂骂咧咧的干了很多活,小心翼翼的闯了很多祸。精打细算的欠了一屁股帐。认认真真的范了很多错。掏心掏肺的结了很多仇。不明不白的吃了很多亏。窝窝囊囊的活了几十年。
glm::mat4 trans;:首先声明了一个4x4的矩阵trans,用于表示变换矩阵。trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));:这一行代码对trans进行了旋转变换。使用了glm库中的rotate函数,将trans矩阵绕Z轴旋转90度(使用radians函数将角度转换为弧度),并将结果赋值给trans本身。trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));:接着对trans进行了缩放变换。使用了glm库中的scale函数,将trans矩阵沿着X、Y、Z三个轴分别缩放0.5倍,并将结果再次赋值给trans本身。为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。我们的顶点坐标起始于局部空间(Local Space),在这里它称为局部坐标(Local Coordinate),它在之后会变为世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕
坐标(Screen Coordinate)的形式结束。下面的这张图展示了整个流程以及各个变换过程做了什么:

你可能已经大致了解了每个坐标空间的作用。我们之所以将顶点变换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当需要对物体进行修改的时候,在局部空间中来操作会更说得通;如果要对一个物体做出一个相对于其它物体位置的操作时,在世界坐标系中来做这个才更说得通,等等。如果我们愿意,我们也可以定义一个直接从局部空间变换到裁剪空间的变换矩阵,但那样会失去很多灵活性。






glm::LookAt函数需要一个位置、目标和上向量。它会创建一个观察矩阵。
为了改变摄像机方向

self.m_projection=glm.perspective(V_FOV,ASPECT_RATIO,NEARPLANE,FARPLANE)
使用 GLM 库中的 glm::perspective() 函数创建了一个投影矩阵(projection matrix).会根据给定的参数创建一个透视投影矩阵,并返回这个矩阵。这个投影矩阵描述了从摄像机位置观察场景时的投影效果,将三维场景转换为二维屏幕空间

对连续时间正弦信号考虑下面表示式:
x ( t ) = s i n ( 2 π f 0 t + φ )
可以按抽样频率 fs=1/Ts对 x(t)抽样来获得离散时间信号
x [ n ] = x ( t )|t =nTs = x ( t ) |t=n / fs = s i n ( 2 πf0 /fsn + φ ),
f0 =500Hz, fs 取 100Hz, 绘出 x[n]及其 DTFT




以 5000HZ 和 1000HZ 分别对其采样得到 x1(n), x2(n);画出它们的 DTFT 并比较


我们可以从第一个方程中直接得到 A 和 φ 的关系:

φ !=π/2+kπ
x(t)=2cos(π/3 *t)




现实中无法实现理想低通滤波器。然而,可以按下面的方法计算由理想低通滤波器产生的
波形:理想低通运算相当于信号频谱与频域的矩形函数相乘,这对应于信号与通过傅里叶逆变
换得到的时域 sinc 函数的卷积。当其应用于点样本时,卷积和为 sinc 函数内插:
xa(t)=sum_{n=-无穷}^{正无穷} [xa(nt) sin(π(t-nTs)/Ts)/(π(t-nTs)/Ts)]
(3.18)
其中,样本 xa(nt)取自 t= nTs处。
a. 假设只有有限数量的信号样本是非零值,且只需在有限时间区间上进行信号重建,写出
基于(3.18)式的 sinc 内插表示式。
syms t n Ts xa;
xa_t = symsum(xa * sin(pi*(t-n*Ts)/Ts)/(pi*(t-n*Ts)/Ts), n, -inf, inf);

C:
根据奈奎斯特采样定理,要求 fs≥2fbfs≥2fb 以避免混叠现象。因此,fb<fs2fb<2fs 是满足采样定理的条件。

45HZ,基本周期 T是 1/45


你可以看到,白色的阳光实际上是所有可见颜色的集合,物体吸收了其中的大部分颜色。它仅反射了代表物体颜色的部分,被反射颜色的组合就是我们所感知到的颜色(此例中为珊瑚红)。
这些颜色反射的定律被直接地运用在图形领域。当我们在OpenGL中创建一个光源时,我们希望给光源一个颜色。在上一段中我们有一个白色的太阳,所以我们也将光源设置为白色。当我们把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色)。让我们再次审视我们的玩具(这一次它还是珊瑚红),看看如何在图形学中计算出它的反射颜色。我们将这两个颜色向量作分量相乘,结果就是最终的颜色向量了:
glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);
我们可以看到玩具的颜色吸收了白色光源中很大一部分的颜色,但它根据自身的颜色值对红、绿、蓝三个分量都做出了一定的反射。这也表现了现实中颜色的工作原理。由此,我们可以定义物体的颜色为==物体从一个光源反射各个颜色分量的大小。==
**现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。因此OpenGL的光照使用的是简化的模型,对现实的情况进行近似,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子:

在漫反射光照部分,光照表现并没有问题,这是因为我们没有对物体进行任何缩放操作,所以我们并不真的需要使用一个法线矩阵,而是仅以模型矩阵乘以法线就可以。但是如果你会进行不等比缩放,使用法线矩阵去乘以法向量就是必须的了。




已知周期信号 x(t) = 0.75 + 3.4 cos 2πft + 2.7 cos 4π ft +1.5sin 3.5π ft + 2.5sin 7π ft ,其
中 25/16Hz,若截断时间长度分别为信号周期的 0.9 和 1.1 倍,试分别绘制这八种窗函数
提取的 x(t)的频谱。

根据下列指标采用窗函数法设计低通数字滤波器, 通带截止频率wp= 0.2π ,阻带截止频率
ws = 0.3π,通带最大衰减 0.25dB,阻带最小衰减 50dB。
(1) 分别利用汉明窗、布莱克曼窗和凯泽窗设计该滤波器,且滤波器具有线性相位。绘出脉冲响应 h(n)及滤波器的频率响应;
(2) 增加 N,观察过渡带和最大肩峰值的变化。
利用汉明窗设计数字微分器
Hd(e^jw)=
jw,0<w<π;
-jw,-π<w<0.
要求 N = 21,且滤波器具有线性相位。

PCIe传输速率比较慢




CPU启动核函数之后,由这个核函数在GPU设备里产生的所有的线程构成了一个grid(网格)
而一个grid又由多个线程块(block)组成,一个线程块里包含一组线程(thread)
进行CUDA编程时,要做的就是减少计算核心空闲的时间,让计算核心一直处于计算中
CPU,GPU在进行内存相互访问的时候,会很耗时



一维:


二维:

三维:


-arch和-code 都与GPU的兼容性有关,在指定计算能力的时候,GPU的真实架构计算能力一定要大于虚拟架构计算能力的





即时编译,增加兼容性:

两个都是compute_XY(虚拟)


在C++中,exit(-1) 和 return -1 都可以用来表示程序的异常退出或者返回一个错误码,但它们之间有一些重要的区别:
exit(-1) 是一个系统调用,它会立即终止整个程序的执行,并返回一个指定的退出码给操作系统。这会终止程序的执行并进行清理工作(如关闭文件、释放内存等),然后返回退出码。exit 函数是C标准库中的函数,定义在 <cstdlib> 头文件中。return -1 通常出现在函数中,用于从当前函数中返回一个指定的值。当函数的返回类型是整型时,return -1 将会将 -1 这个值返回给调用该函数的地方。如果 -1 是 main 函数的返回值,那么它会被返回给操作系统作为程序的退出码。因此,exit(-1) 会立即终止整个程序的执行,而 return -1 只是从当前函数中返回一个值。

双指针



cudaDeviceReset()函数用于重置当前设备上的所有状态信息。它会清除当前设备上的所有内存分配和设备端的运行时状态,释放所有CUDA资源,并将设备状态恢复到初始化时的状态。这个函数通常在程序结束前被调用,以确保释放所有CUDA资源并将GPU状态还原到初始状态。
调用cudaDeviceReset()函数可以帮助确保程序结束时释放了所有CUDA资源,从而避免内存泄漏和其他问题。
在CUDA中,核函数(kernel function)和设备函数(device function)是两个不同的概念。
__global__声明。它们可以被从CPU代码调用,并在GPU上并行执行。在CUDA中,核函数通常用于执行大规模数据并行计算。虽然它们都是在GPU上执行的函数,但核函数和设备函数在调用方式、用途和作用域上有明显的区别。核函数是CUDA程序中由CPU代码调用的入口点,而设备函数是为了在核函数内部使用而设计的。



__FILE__ 和 __LINE__ 是C/C++中的预定义宏,它们分别代表当前源文件的文件名和行号


%g 是 C++ 语言中的格式化输出控制符之一,用于打印浮点数。它根据浮点数的值自动选择 %f 或 %e 中较短的一个输出形式来打印。
具体来说:
%g 就会采用 %e 的输出形式,用科学计数法表示浮点数。%g 会采用 %f 的输出形式,用普通的小数形式表示浮点数。在 CUDA 编程中,cudaEventQuery(start) 表示查询事件 start 的状态。具体来说,它用于检查事件是否已经被记录。如果事件已经被记录,那么 cudaEventQuery 将立即返回。如果事件还没有被记录,那么 cudaEventQuery 将等待事件被记录后才返回。
在上述代码中,cudaEventQuery(start) 的目的可能是为了确保在记录 stop 事件之前,start 事件已经被成功记录。这样可以确保测量的时间间隔准确,避免了 start 事件尚未记录就立即记录 stop 事件的情况。

由于实际测试具有较大的局限性,因此我们考虑仅通过一些计算来评估算法的效率。这种估算方法被称为渐近复杂度分析(asymptotic complexity analysis),简称复杂度分析。
迭代(iteration)
递归(recursion),通过函数调用自身来解决问题:(“将问题分解为更小子问题”)
而从实现的角度看,递归代码主要包含三个要素。
虽然从计算角度看,迭代与递归可以得到相同的结果,但它们代表了两种完全不同的思考和解决问题的范式。
过深的递归可能导致栈溢出错误
有趣的是,如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空间效率上与迭代相当。这种情况被称为尾递归(tail recursion)。
普通递归:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下文。
尾递归:递归调用是函数返回前的最后一个操作,这意味着函数返回到上一层级后,无须继续执行其他操作,因此系统无须保存上一层函数的上下文。
例如:
/* 尾递归 */
int tailRecur(int n, int res) {
// 终止条件
if (n == 0)
return res;
// 尾递归调用
return tailRecur(n - 1, res + n);
}
普通递归:
python
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n - 1)在普通递归中,递归调用 factorial(n - 1) 发生在函数的末尾,并且返回值被乘以 n 后再返回。
尾递归:
python
def factorial_tail(n, accumulator=1):
if n == 0:
return accumulator
else:
return factorial_tail(n - 1, accumulator * n)以上述递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,这种工作机制与栈的“先入后出”原则异曲同工。
事实上,“调用栈”和“栈帧空间”这类递归术语已经暗示了递归与栈之间的密切关系。
时间复杂度分析统计的不是算法运行时间,而是算法运行时间随着数据量变大时的增长趋势。


在计算机科学中,”渐近上界” 是一种用于分析算法性能的概念,特别是与时间复杂度和空间复杂度相关。在时间复杂度分析中,渐近上界的目的在于确定算法在数据规模趋于无穷大时的最坏性能表现。为了更好地理解这一概念,可以从几个角度来探讨:
渐近上界指的是某个函数在趋于某个极限(例如,无限大)时,其增长速率的上限。它通常用于描述算法的最坏情况性能,表示随着输入规模增加,算法的运行时间或使用的资源上限。
在算法分析中,渐近上界最常用的表示法是大-O 表示法。用 O(f(n)) 表示某个算法的时间复杂度,意味着这个算法的运行时间在最坏情况下不会超过某个函数 f(n) 的增长速率。
例如,若算法的时间复杂度是 O(n^2),表示无论最坏情况下发生了什么,这个算法的运行时间最多是某个常数与 n^2 的乘积。这里 n 是输入数据的规模。
渐近上界有助于比较不同算法的性能,并帮助工程师选择适当的算法。在设计和优化算法时,了解渐近上界也有助于避免性能陷阱。
渐近上界在数学上是严格定义的。函数 T(n) 的渐近上界是 f(n),如果存在常数 c 和 n_0 使得对于所有 n ≥ n_0,都有 T(n) ≤ c * f(n)。这意味着当 n 足够大时,T(n) 不会超过 c * f(n),即使在最坏情况下。
考虑一个算法的时间复杂度是 3n^2 + 2n + 7。这个算法的渐近上界是 O(n^2),因为当 n 足够大时,3n^2 是增长最快的项,其他项的影响可以忽略。

/* 指数阶(递归实现) */
int expRecur(int n) {
if (n == 1)
return 1;
return expRecur(n - 1) + expRecur(n - 1) + 1;
}/*对数阶*/
int linearLogRecur(int n) {
if (n <= 1)
return 1;
int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);
return count;
}/* 线性对数阶 */
int linearLogRecur(int n) {
if (n <= 1)
return 1;
int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);
for (int i = 0; i < n; i++) {
count++;
}
return count;
}
算法在运行过程中使用的内存空间主要包括以下几种。
一般情况下,空间复杂度的统计范围是“暂存空间”加上“输出空间”。
struct Node {
int val;
Node *next;
Node(int x) : val(x), next(nullptr) {}
};Node(int x) : val(x), next(nullptr) 是一个构造函数的定义
int func() {
// 执行某些操作
return 0;
}
/* 循环的空间复杂度为 O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func();
}
}
/* 递归的空间复杂度为 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
}函数 loop() 和 recur() 的时间复杂度都为
,但空间复杂度不同。
loop() 在循环中调用了 次 function() ,每轮中的 function() 都返回并释放了栈帧空间,因此空间复杂度仍为
。
递归函数 recur() 在运行过程中会同时存在
个未返回的 recur() ,从而占用 的栈帧空间。
/* 线性阶 */
void linear(int n) {
// 长度为 n 的数组占用 O(n) 空间
vector<int> nums(n);
// 长度为 n 的列表占用 O(n) 空间
vector<ListNode> nodes;
for (int i = 0; i < n; i++) {
nodes.push_back(ListNode(i));
}
// 长度为 n 的哈希表占用 O(n) 空间
unordered_map<int, string> map;
for (int i = 0; i < n; i++) {
map[i] = to_string(i);
}
}
/* 线性阶(递归实现) */
void linearRecur(int n) {
cout << "递归 n = " << n << endl;
if (n == 1)
return;
linearRecur(n - 1);
}
Q:函数和方法这两个术语的区别是什么?
函数(function)可以被独立执行,所有参数都以显式传递。方法(method)与一个对象关联,被隐式传递给调用它的对象,能够对类的实例中包含的数据进行操作。
下面以几种常见的编程语言为例来说明。
常见的数据结构包括数组、链表、栈、队列、哈希表、树、堆、图,它们可以从“逻辑结构”和“物理结构”两个维度进行分类。
逻辑结构揭示了数据元素之间的逻辑关系。在数组和链表中,数据按照一定顺序排列,体现了数据之间的线性关系;而在树中,数据从顶部向下按层次排列,表现出“祖先”与“后代”之间的派生关系;图则由节点和边构成,反映了复杂的网络关系。
如图 3-1 所示,逻辑结构可分为“线性”和“非线性”两大类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列。
非线性数据结构可以进一步划分为树形结构和网状结构。
内存是所有程序的共享资源,当某块内存被某个程序占用时,则无法被其他程序同时使用了。因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素。比如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果缺少连续大块的内存空间,那么所选用的数据结构必须能够存储在分散的内存空间内。
如图 3-3 所示,物理结构反映了数据在计算机内存中的存储方式,可分为连续空间存储(数组)和分散空间存储(链表)。物理结构从底层决定了数据的访问、更新、增删等操作方法,两种物理结构在时间效率和空间效率方面呈现出互补的特点。

值得说明的是,所有数据结构都是基于数组、链表或二者的组合实现的。例如,栈和队列既可以使用数组实现,也可以使用链表实现;而哈希表的实现可能同时包含数组和链表。
链表在初始化后,仍可以在程序运行过程中对其长度进行调整,因此也称“动态数据结构”。数组在初始化后长度不可变,因此也称“静态数据结构”。值得注意的是,数组可通过重新分配内存实现长度变化,从而具备一定的“动态性”。
基本数据类型是 CPU 可以直接进行运算的类型,在算法中直接被使用,主要包括以下几种。
byte、short、int、long 。float、double ,用于表示小数。char ,用于表示各种语言的字母、标点符号甚至表情符号等。bool ,用于表示“是”与“否”判断。基本数据类型以二进制的形式存储在计算机中。一个二进制位即为 1比特。在绝大多数现代操作系统中,1字节(byte)由 8比特(bit)组成。
/* 初始化数组 */
// 存储在栈上
int arr[5];
int nums[5] = { 1, 3, 2, 5, 4 };
// 存储在堆上(需要手动释放空间)
int* arr1 = new int[5];
int* nums1 = new int[5] { 1, 3, 2, 5, 4 };在C++中,将数据存储在栈上和堆上有以下区别:
在实际编程中,选择栈上或堆上的存储方式取决于数据的生命周期、大小和访问方式等因素。
Q:把数组存储在堆,栈上,那堆栈是什么
A:在计算机科学中,”堆栈”通常指的是”栈”和”堆”这两种不同的内存分配区域,它们用于存储程序运行时所需的数据和内存。这两者虽然名字相似,但是在内存管理和使用方面有着不同的作用和特点:
new和delete操作符创建和释放的对象。总的来说,栈和堆是程序运行时两种不同的内存分配区域,它们有着不同的管理方式、分配速度和用途,程序员可以根据需要选择合适的内存分配方式来管理程序的数据和内存。
Q :存储在堆上,是要求定义为指针吗
A : 在C++中,使用new操作符分配内存时,返回的是指向分配内存的指针。因此,存储在堆上的数据需要定义为指针类型,以便程序员能够通过指针来访问和操作这些数据,并在适当的时候手动释放内存空间,以避免内存泄漏。
以下是一个简单的示例,演示如何声明和使用函数指针:
cpp
#include <iostream>
// 声明一个接受两个整数参数并返回它们之和的函数
int add(int a, int b) {
return a + b;
}
// 声明一个接受两个整数参数并返回它们之差的函数
int subtract(int a, int b) {
return a - b;
}
int main() {
// 声明一个指向函数的指针,该函数接受两个整数参数并返回一个整数
int (*operation)(int, int);
// 将函数 add 的地址赋值给指针变量 operation
operation = add;
// 使用指针调用函数 add
int result1 = operation(10, 5);
std::cout << "Result of add: " << result1 << std::endl;
// 将函数 subtract 的地址赋值给指针变量 operation
operation = subtract;
// 使用指针调用函数 subtract
int result2 = operation(10, 5);
std::cout << "Result of subtract: " << result2 << std::endl;
return 0;
}在这个示例中,int (*operation)(int, int); 声明了一个名为 operation 的函数指针,它指向一个接受两个整数参数并返回一个整数的函数。然后,通过将函数的地址赋值给指针变量 operation,可以使用该指针来调用不同的函数。
/* 扩展数组长度 */
int *extend(int *nums, int size, int enlarge) {
// 初始化一个扩展长度后的数组
int *res = new int[size + enlarge];
// 将原数组中的所有元素复制到新数组
for (int i = 0; i < size; i++) {
res[i] = nums[i];
}
// 释放内存
delete[] nums;
// 返回扩展后的新数组
return res;
}返回类型为 int *,意味着该函数返回的是一个指向整数类型的指针,即指向数组的指针。
数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来优化数据结构的操作效率。
空间效率高:数组为数据分配了连续的内存块,无须额外的结构开销。
支持随机访问:数组允许在
时间内访问任何元素。
缓存局部性:当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。
连续空间存储是一把双刃剑,其存在以下局限性。
插入与删除效率低:当数组中元素较多时,插入与删除操作需要移动大量的元素。
长度不可变:数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大。
空间浪费:如果数组分配的大小超过实际所需,那么多余的空间就被浪费了。
delete 用于释放通过 new 分配的单个对象的内存。
delete[] 用于释放通过 new[] 分配的数组的内存。
数组是一种基础且常见的数据结构,既频繁应用在各类算法之中,也可用于实现各种复杂数据结构。
存储数组的内存空间必须是连续的
链表的组成单位是节点(node)对象。每个节点都包含两项数据:节点的“值”和指向下一节点的“引用”。
null、nullptr 和 None 。/* 链表节点结构体 */
struct ListNode {
int val; // 节点值
ListNode *next; // 指向下一节点的指针
ListNode(int x) : val(x), next(nullptr) {} // 构造函数
};为什么结构体也有构造函数:
在C++中,结构体(struct)和类(class)都可以拥有构造函数。构造函数用于初始化对象的数据成员,在创建对象时自动调用。
单向链表通常用于实现栈、队列、哈希表和图等数据结构。
双向链表常用于需要快速查找前一个和后一个元素的场景。
环形链表常用于需要周期性操作的场景,比如操作系统的资源调度。
可以使用动态数组(dynamic array)来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。
实际上,许多编程语言中的标准库提供的列表是基于动态数组实现的,例如 Python 中的 list 、Java 中的 ArrayList 、C++ 中的 vector 和 C# 中的 List 等。接下来,我们将把“列表”和“动态数组”视为等同的概念。
缓存虽然在空间容量上远小于内存,但它比内存快得多,在程序执行速度上起着至关重要的作用。由于缓存的容量有限,只能存储一小部分频繁访问的数据,因此当 CPU 尝试访问的数据不在缓存中时,就会发生缓存未命中(cache miss),此时 CPU 不得不从速度较慢的内存中加载所需数据。
为了尽可能达到更高的效率,缓存会采取以下数据加载机制。
栈(stack)是一种遵循先入后出逻辑的线性数据结构。
如图 5-1 所示,我们把堆叠元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫作“入栈”,删除栈顶元素的操作叫作“出栈”。因此我们只能在栈顶添加或删除元素,然而,数组和链表都可以在任意位置添加和删除元素,因此栈可以视为一种受限制的数组或链表。
基于数组实现的栈
vector<int> stack;
/* 获取栈的长度 */
stack.size()
入栈
stack.push_back(num);
出栈
stack.pop_back();
访问栈顶元素
stack.back();
back就是栈顶的位置有专用的栈方法
stack<int> stack;
/* 元素入栈 */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* 访问栈顶元素 */
int top = stack.top();
/* 元素出栈 */
stack.pop(); // 无返回值
/* 获取栈的长度 */
int size = stack.size();
/* 判断是否为空 */
bool empty = stack.empty();队列(queue)是一种遵循先入先出规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。
/* 初始化队列 */
queue<int> queue;
/* 元素入队 */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
/* 访问队首元素 */
int front = queue.front();
/* 元素出队 */
queue.pop();
/* 获取队列的长度 */
int size = queue.size();
/* 判断队列是否为空 */
bool empty = queue.empty();添加队尾,删除队首—–先来后到
双向队列:
/* 初始化双向队列 */
deque<int> deque;
/* 元素入队 */
deque.push_back(2); // 添加至队尾
deque.push_back(5);
deque.push_back(4);
deque.push_front(3); // 添加至队首
deque.push_front(1);
/* 访问元素 */
int front = deque.front(); // 队首元素
int back = deque.back(); // 队尾元素
/* 元素出队 */
deque.pop_front(); // 队首元素出队
deque.pop_back(); // 队尾元素出队
/* 获取双向队列的长度 */
int size = deque.size();
/* 判断双向队列是否为空 */
bool empty = deque.empty();DoublyListNode *pre, *cur = front; 确实是一个比较容易产生误解的地方,但实际上这行代码并不是同时给 pre 和 cur 赋值为 front。这行代码实际上相当于两行分开的声明和初始化:
cpp
DoublyListNode *pre; // 声明一个指向 DoublyListNode 类型的指针 pre
DoublyListNode *cur = front; // 声明一个指向 DoublyListNode 类型的指针 cur,并将其初始化为 front这里的 pre 只是声明了一个指针,但并没有初始化,所以它的值是未定义的,你需要在后续代码中对其进行初始化。而 cur 在声明时已经被初始化为 front。
哈希表(hash table),又称散列表,它通过建立键 key 与值 value 之间的映射,实现高效的元素查询。具体而言,我们向哈希表中输入一个键 key ,则可以在 O(1)时间内获取对应的值 value 。
unordered_map 是 C++ 标准库中的一种关联容器(Associative Container),它提供了快速的键值对存储和检索功能。它是通过哈希表实现的,因此提供了高效的插入、删除和查找操作。
/* 初始化哈希表 */
unordered_map<int, string> map;
/* 添加操作 */
// 在哈希表中添加键值对 (key, value)
map[12836] = "小哈";
map[15937] = "小啰";
map[16750] = "小算";
map[13276] = "小法";
map[10583] = "小鸭";
/* 查询操作 */
// 向哈希表中输入键 key ,得到值 value
string name = map[15937];
/* 删除操作 */
// 在哈希表中删除键值对 (key, value)
map.erase(10583);
/* 遍历哈希表 */
// 遍历键值对 key->value
for (auto kv: map) {
cout << kv.first << " -> " << kv.second << endl;
}
// 使用迭代器遍历 key->value
for (auto iter = map.begin(); iter != map.end(); iter++) {
cout << iter->first << "->" << iter->second << endl;
}我们先考虑最简单的情况,仅用一个数组来实现哈希表。在哈希表中,我们将数组中的每个空位称为桶(bucket),每个桶可存储一个键值对。因此,查询操作就是找到 key 对应的桶,并在桶中获取 value 。
那么,如何基于 key 定位对应的桶呢?这是通过哈希函数(hash function)实现的。哈希函数的作用是将一个较大的输入空间映射到一个较小的输出空间。在哈希表中,输入空间是所有 key ,输出空间是所有桶(数组索引)。换句话说,输入一个 key ,我们可以通过哈希函数得到该 key 对应的键值对在数组中的存储位置。
输入一个 key ,哈希函数的计算过程分为以下两步。
hash() 计算得到哈希值。capacity 取模,从而获取该 key 对应的数组索引 index 。index = hash(key) % capacity<Pair *> 表示这个 vector 存储的是指向 Pair 类型对象的指针。
vector<Pair *> 创建了一个存储指向 Pair 结构的指针的动态数组,即每个元素都是指向 Pair 结构的指针。
我们将多个输入对应同一输出的情况称为哈希冲突(hash collision)。
我们可以通过扩容哈希表来减少哈希冲突。类似于数组扩容,哈希表扩容需将所有键值对从原哈希表迁移至新哈希表,非常耗时;并且由于哈希表容量 capacity 改变,我们需要通过哈希函数来重新计算所有键值对的存储位置,这进一步增加了扩容过程的计算开销。为此,编程语言通常会预留足够大的哈希表容量,防止频繁扩容。
负载因子(load factor)是哈希表的一个重要概念,其定义为哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度,也常作为哈希表扩容的触发条件。例如在 Java 中,当负载因子超过 时,系统会将哈希表扩容至原先的 2倍。
但此方法简单粗暴且有效,但效率太低,因为哈希表扩容需要进行大量的数据搬运与哈希值计算。为了提升效率,我们可以采用以下策略。
哈希表的结构改良方法主要包括“链式地址”和“开放寻址”。
在原始哈希表中,每个桶仅能存储一个键值对。链式地址(separate chaining)将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。图 6-5 展示了一个链式地址哈希表的例子。
图 6-5 链式地址哈希表
基于链式地址实现的哈希表的操作方法发生了以下变化。
key ,经过哈希函数得到桶索引,即可访问链表头节点,然后遍历链表并对比 key 以查找目标键值对。链式地址存在以下局限性。
开放寻址(open addressing)不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测和多次哈希等。
线性探测采用固定步长的线性搜索来进行探测,其操作方法与普通哈希表有所不同。
插入元素:通过哈希函数计算桶索引,若发现桶内已有元素,则从冲突位置向后线性遍历(步长通常为
),直至找到空桶,将元素插入其中。
查找元素:若发现哈希冲突,则使用相同步长向后进行线性遍历,直到找到对应元素,返回 value 即可;如果遇到空桶,说明目标元素不在哈希表中,返回 None 。
图 6-6 展示了开放寻址(线性探测)哈希表的键值对分布。根据此哈希函数,最后两位相同的 key 都会被映射到相同的桶。而通过线性探测,它们被依次存储在该桶以及之下的桶中。
值得注意的是,我们不能在开放寻址哈希表中直接删除元素。这是因为删除元素会在数组内产生一个空桶 None ,而当查询元素时,线性探测到该空桶就会返回,因此在该空桶之下的元素都无法再被访问到,程序可能误判这些元素不存在
为了解决该问题,我们可以采用懒删除(lazy deletion)机制:它不直接从哈希表中移除元素,而是利用一个常量 TOMBSTONE 来标记这个桶。在该机制下,None 和 TOMBSTONE 都代表空桶,都可以放置键值对。但不同的是,线性探测到 TOMBSTONE 时应该继续遍历,因为其之下可能还存在键值对。
然而,懒删除可能会加速哈希表的性能退化。这是因为每次删除操作都会产生一个删除标记,随着 TOMBSTONE 的增加,搜索时间也会增加,因为线性探测可能需要跳过多个 TOMBSTONE 才能找到目标元素。
为此,考虑在线性探测中记录遇到的首个 TOMBSTONE 的索引,并将搜索到的目标元素与该 TOMBSTONE 交换位置。这样做的好处是当每次查询或添加元素时,元素会被移动至距离理想位置(探测起始点)更近的桶,从而优化查询效率。
平方探测与线性探测类似,都是开放寻址的常见策略之一。当发生冲突时,平方探测不是简单地跳过一个固定的步数,而是跳过“探测次数的平方”的步数,即
步。
平方探测主要具有以下优势。
然而,平方探测并不是完美的。

二叉树的常用术语如图所示。
None 。
所有层的节点都被完全填满。

只有最底层的节点未被填满,且最底层节点尽量靠左填充。

除了叶节点之外,其余所有节点都有两个子节点。

任意节点的左子树和右子树的高度之差的绝对值不超过 1 。
[
理想结构与退化结构
二叉树的每层节点都被填满时,达到“完美二叉树”;而当所有节点都偏向一侧时,二叉树退化为“链表”。
完美二叉树是理想情况,可以充分发挥二叉树“分治”的优势。
链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 O(n)

从物理结构的角度来看,树是一种基于链表的数据结构,因此其遍历方式是通过指针逐个访问节点。然而,树是一种非线性数据结构,这使得遍历树比遍历链表更加复杂,需要借助搜索算法来实现。
二叉树常见的遍历方式包括层序遍历、前序遍历、中序遍历和后序遍历等。
层序遍历
从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。
层序遍历本质上属于广度优先遍历(breadth-first traversal),也称广度优先搜索(breadth-first search, BFS),它体现了一种“一圈一圈向外扩展”的逐层遍历方式。

广度优先遍历通常借助“队列”来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。
/* 层序遍历 */
vector<int> levelOrder(TreeNode *root) {
// 初始化队列,加入根节点
queue<TreeNode *> queue;
queue.push(root);
// 初始化一个列表,用于保存遍历序列
vector<int> vec;
while (!queue.empty()) {
TreeNode *node = queue.front();
queue.pop(); // 队列出队
vec.push_back(node->val); // 保存节点值
if (node->left != nullptr)
queue.push(node->left); // 左子节点入队
if (node->right != nullptr)
queue.push(node->right); // 右子节点入队
}
return vec;
}
前序、中序和后序遍历都属于深度优先遍历(depth-first traversal),也称深度优先搜索(depth-first search, DFS),它体现了一种“先走到尽头,再回溯继续”的遍历方式。

前序、中序和后序遍历是针对二叉树的三种不同的遍历方式,它们的区别在于遍历节点的顺序:
总的来说,这三种遍历方式主要区别在于根节点的访问顺序与左右子树的递归顺序。
深度优先搜索通常基于递归实现:
/* 前序遍历 */
void preOrder(TreeNode *root) {
if (root == nullptr)
return;
// 访问优先级:根节点 -> 左子树 -> 右子树
vec.push_back(root->val);
preOrder(root->left);
preOrder(root->right);
}
/* 中序遍历 */
void inOrder(TreeNode *root) {
if (root == nullptr)
return;
// 访问优先级:左子树 -> 根节点 -> 右子树
inOrder(root->left);
vec.push_back(root->val);
inOrder(root->right);
}
/* 后序遍历 */
void postOrder(TreeNode *root) {
if (root == nullptr)
return;
// 访问优先级:左子树 -> 右子树 -> 根节点
postOrder(root->left);
postOrder(root->right);
vec.push_back(root->val);
}
用数组来表示二叉树
给定一棵完美二叉树,我们将所有节点按照层序遍历的顺序存储在一个数组中,则每个节点都对应唯一的数组索引。
根据层序遍历的特性,我们可以推导出父节点索引与子节点索引之间的“映射公式”:若某节点的索引为i ,则该节点的左子节点索引为2i+1 ,右子节点索引为2i+2
映射公式的角色相当于链表中的节点引用(指针)。给定数组中的任意一个节点,我们都可以通过映射公式来访问它的左(右)子节点。
完美二叉树是一个特例,在二叉树的中间层通常存在许多 None 。由于层序遍历序列并不包含这些 None ,因此我们无法仅凭该序列来推测 None 的数量和分布位置。这意味着存在多种二叉树结构都符合该层序遍历序列。

为了解决此问题,我们可以考虑在层序遍历序列中显式地写出所有 None 。如图 7-14 所示,这样处理后,层序遍历序列就可以唯一表示二叉树了
/* 二叉树的数组表示 */
// 使用 int 最大值 INT_MAX 标记空位
vector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};
完全二叉树:只有最底层的节点未被填满,且最底层节点尽量靠左填充。
完全二叉树非常适合使用数组来表示。回顾完全二叉树的定义,None 只出现在最底层且靠右的位置,因此所有 None 一定出现在层序遍历序列的末尾。这意味着使用数组表示完全二叉树时,可以省略存储所有 None ,非常方便.

二叉树的数组表示主要有以下优点。
然而,数组表示也存在一些局限性。
None 时,数组中包含的节点数据比重较低,空间利用率较低。二叉搜索树(binary search tree)满足以下条件:
1.对于根节点,左子树中所有节点的值<根节点的值<右子树中所有节点的值。
2,任意节点的左、右子树也是二叉搜索树,即同样满足条件 1. 。

给定一个待插入元素 num ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,
num 的大小关系循环向下搜索,直到越过叶节点(遍历至 None )时跳出循环。num ,将该节点置于 None 的位置。
只能插在NONE节点处,即*pre = nullptr
pre 保存上一轮循环的节点。这样在遍历至 None 时,我们可以获取到其父节点,从而完成节点插入操作。删除节点
当待删除节点的度为2时,我们无法直接删除它,而需要使用一个节点替换该节点。由于要保持二叉搜索树“左子树 <根节点 <右子树”的性质,因此这个节点可以是右子树的最小节点或左子树的最大节点。
tmp 。tmp 的值覆盖待删除节点的值,并在树中递归删除节点 tmp 。


二叉树的中序遍历遵循“左 根 右”的遍历顺序,而二叉搜索树满足“左子节点 根节点 右子节点”的大小关系。
这意味着在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:二叉搜索树的中序遍历序列是升序的。
利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需O(n)时间,无须进行额外的排序操作,非常高效。

int val{}; 是C++中的变量声明语句,其中 int 表示变量的类型为整数类型,val 是变量名,{} 表示进行了值初始化。
在C++11及其之后的标准中,使用 {} 进行初始化被称为列表初始化或者统一初始化。对于内置类型(如 int、float、double 等),使用 {} 进行初始化时,如果未提供初始值,则会将变量初始化为零值,即 0。这种初始化方式也可以保证初始化的一致性,并且在某些情况下可以避免隐式类型转换带来的问题。
在多次插入和删除操作后,二叉搜索树可能退化为链表。在这种情况下,所有操作的时间复杂度将从
O(log n)劣化成O(n)
AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二叉树的所有性质,因此是一种平衡二叉搜索树(balanced binary search tree)。
节点的平衡因子(balance factor)定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为0 。
AVL 树旋转
AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新恢复平衡。换句话说,旋转操作既能保持“二叉搜索树”的性质,也能使树重新变为“平衡二叉树”。
我们将平衡因子绝对值>1的节点称为“失衡节点”。根据节点失衡情况的不同,旋转操作分为四种:右旋、左旋、先右旋后左旋、先左旋后右旋。



当节点 child 有右子节点(记为 grand_child )时,需要在右旋中添加一步:将 grand_child 作为 node 的左子节点。

右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的。基于对称性,我们只需将右旋的实现代码中的所有的 left 替换为 right ,将所有的 right 替换为 left ,即可得到左旋的实现代码




DFS(深度优先搜索)遍历二叉树是一种遍历或搜索算法,用来访问二叉树中的所有节点,其目的是尽可能深地访问树的分支。DFS在二叉树中常用的有三种遍历方式:前序遍历(Pre-order)、中序遍历(In-order)和后序遍历(Post-order)。下面详细解释这三种遍历方式:
DFS 遍历的核心在于使用递归(或显式使用栈)来实现持续深入每个分支直到达到叶子节点或满足某些条件后回溯到上一节点。这种方式非常适合处理具有层级关系的数据,如文件系统的目录结构、组织结构等。在二叉树的上下文中,DFS遍历可以帮助理解和操作树的结构。
visited.count(adjVet)count 是 unordered_set 提供的的一个成员函数,它返回集合中指定元素的数量。如果 adjVet 是指向图中的一个顶点的指针,并且这个顶点已经被添加到 visited 集合中,那么 visited.count(adjVet) 将返回 1,表示该顶点已经被访问过。如果 adjVet 不在 visited 集合中,那么返回 0,表示该顶点还没有被访问过。
visited.emplace(adjVet);emplace 是一个函数,它用于在容器中直接构造并插入元素,而不需要创建元素的副本
assign 关键字用于对线网(wire)或变量(var)进行连续赋值。连续赋值意味着一旦右侧的表达式发生变化,赋值就会立即更新左侧的值,这与过程赋值(在always块中)不同,后者在某种事件或条件发生时才更新值。
assign temp2 = {32{1’b0}}; 是什么意思?
在Verilog中,assign temp2 = {32{1'b0}}; 这行代码声明了一个连续赋值,将 temp2 这个线网的值设置为一个32位的全0值。
这里的 {32{1'b0}} 是一个重复拼接操作,含义如下:
1'b0 是一个二进制数,表示一个位宽为1的数值,值为0。{32{1'b0}} 表示将 1'b0 这个值重复32次。Verilog 最常用的 2 种数据类型就是线网(wire)与寄存器(reg),其余类型可以理解为这两种数据类型的扩展或辅助。
整数(integer) reg 型变量为无符号数,而 integer 型变量为有符号数
实数(real)
在Verilog中,real 和 integer 是数据类型关键字,分别用于声明实数类型和整数类型的变量。
real data1;
integer temp;
initial begin
data1 = 2e3;
data1 = 3.75;
end
initial begin
temp = data1; //temp 值的大小为3
end这段代码包含两个 initial 块,它们在仿真开始时执行一次。
第一个 initial 块中:
data1 被初始化为实数类型 real。data1 被赋值为 2e3,这意味着 data1 现在的值是2000.0。data1 被更新为 3.75。第二个 initial 块中:
temp 被初始化为整数类型 integer。temp 被赋值为 data1 的值。由于 data1 当前是 3.75,这个赋值会将实数转换为整数。在Verilog中,实数赋值给整数时,会进行取整操作,保留数值的整数部分,忽略小数部分。因此,temp 的值将是3。需要注意的是,您的注释 //temp 值的大小为3 是正确的,因为 data1 的值 3.75 在赋值给 temp 时会被取整为3。
时间(time)
Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间。例如:
time current_time;
initial begin
#100;
current_time = $time; //current_time 的大小为 100
end这段代码包含一个 initial 块,它在仿真开始时执行一次。
在 initial 块中:
current_time 被初始化为时间类型 time。#100; 是一个延迟语句,它会使仿真暂停100个时间单位。在Verilog中,# 后面跟一个数字表示延迟的时间量。存储器
parameter data_width = 10’d32 ;
字符串保存在 reg 类型的变量中,每个字符占用一个字节(8bit)。因此寄存器变量的宽度应该足够大,以保证不会溢出。
字符串不能多行书写,即字符串中不能包含回车符。如果寄存器变量的宽度大于字符串的大小,则使用 0 来填充左边的空余位;如果寄存器变量的宽度小于字符串大小,则会截去字符串左边多余的数据。例如,为存储字符串 “run.runoob.com”, 需要 14*8bit 的存储单元:
reg [0: 14*8-1] str ;
initial begin
str = “run.runoob.com”;
end





表达式由操作符和操作数构成,其目的是根据操作符的意义得到一个计算结果。
a^b ; //a与b进行异或操作
address[9:0] + 10’b1 ; //地址累加
flag1 && flag2 ; //逻辑与操作
always块里赋值对象不能是wire型
同类型操作符之间,除条件操作符从右往左关联,其余操作符都是自左向右关联。圆括号内表达式优先执行
//自右向左关联,两种写法等价
A+B-C ;
(A+B)-C ;
//自右向左关联,两种写法等价,结果为 B、D 或 F
A ? B : C ? D : F ;
A ? B : (C ? D : F) ;求幂(**)、取模(%)
b = 4'b100x;x 是一个表示未知或不可确定状态的字符。它用于在仿真中表示一个位的值是未知的,这通常发生在综合过程中,当某些逻辑路径没有被明确赋值时,或者在设计中的某些部分还没有完全定义时。
无符号数乘法时,结果变量位宽应该为 2 个操作数位宽之和
reg [3:0] mula ;
reg [1:0] mulb;
reg [5:0] res ;
mula = 4’he ;
mulb = 2’h3 ;
res = mula * mulb ; //结果为res=6’h2a, 数据结果没有丢失位数
逻辑操作符主要有 3 个:&&(逻辑与), ||(逻辑或),!(逻辑非)
按位操作符包括:取反(),与(&),或(|),异或(^),同或(^)
按位操作符对 2 个操作数的每 1bit 数据进行按位操作,如果 2 个操作数位宽不相等,则用 0 向左扩展补充较短的操作数。
归约操作符包括:归约与(&),归约与非(&),归约或(|),归约或非(|),归约异或(^),归约同或(~^)。
归约操作符只有一个操作数,它对这个向量操作数逐位进行操作,最终产生一个 1bit 结果。
逻辑操作符、按位操作符和归约操作符都使用相同的符号表示,因此有时候容易混淆。区分这些操作符的关键是分清操作数的数目,和计算结果的规则。
A = 4'b1010 ;
&A ; //结果为 1 & 0 & 1 & 0 = 1'b0,可用来判断变量A是否全1
~|A ; //结果为 ~(1 | 0 | 1 | 0) = 1'b0, 可用来判断变量A是否为全0
^A ; //结果为 1 ^ 0 ^ 1 ^ 0 = 1'b0移位操作符包括左移(<<),右移(>>),算术左移(<<<),算术右移(>>>)。
移位操作符是双目操作符,两个操作数分别表示要进行移位的向量信号(操作符左侧)与移动的位数(操作符右侧)。
算术左移和逻辑左移时,右边低位会补 0。
逻辑右移时,左边高位会补 0;而算术右移时,左边高位会补充符号位,以保证数据缩小后值的正确性。
A = 4’b1100 ;
B = 4’b0010 ;
A = A >> 2 ; //结果为 4’b0011
A = A << 1; *//结果为 4’b1000*
A = A <<< 1 ; *//结果为 4’b1000*
C = B + (A>>>2); //结果为 2 + (-4/4) = 1, 4’b0001
define, undef在编译阶段,`define 用于文本替换,类似于 C 语言中的 #define。
`undef 用来取消之前的宏定义
`ifdef MCU51
parameter DATA_DW = 8 ;
`elsif WINDOW
parameter DATA_DW = 64 ;
`else
parameter DATA_DW = 32 ;
`endif使用 `include 可以在编译时将一个 Verilog 文件内嵌到另一个 Verilog 文件中,作用类似于 C 语言中的 #include 结构。
在 Verilog 模型中,时延有具体的单位时间表述,并用 `timescale 编译指令将时间单位与实际时间相关联。
该指令用于定义时延、仿真的单位和精度,格式为:
`timescale time_unit / time_precisiontime_unit 表示时间单位,time_precision 表示时间精度,它们均是由数字以及单位 s(秒),ms(毫秒),us(微妙),ns(纳秒),ps(皮秒)和 fs(飞秒)组成。时间精度可以和时间单位一样,但是时间精度大小不能超过时间单位大小,例如下面例子中,输出端 Z 会延迟 5.21ns 输出 A&B 的结果。
timescale 1ns/100ps *//时间单位为1ns,精度为100ps,合法* *//timescale 100ps/1ns //不合法*
module AndFunc(Z, A, B);
output Z;
input A, B ;
assign #5.207 Z = A & B
endmodule
在编译过程中,timescale 指令会影响后面所有模块中的时延值,直至遇到另一个 timescale 指令或 `resetall 指令。
由于在 Verilog 中没有默认的 timescale,如果没有指定 timescale,Verilog 模块就有会继承前面编译模块的 `timescale 参数。有可能导致设计出错。
如果一个设计中的多个模块都带有 `timescale 时,模拟器总是定位在所有模块的最小时延精度上,并且所有时延都相应地换算为最小时延精度
该指令用于为隐式的线网变量指定为线网类型,即将没有被声明的连线定义为线网类型。
`default_nettype wand 该实例定义的缺省的线网为线与类型。因此,如果在此指令后面的任何模块中的连线没有说明,那么该线网被假定为线与类型。
`default_nettype none该实例定义后,将不再自动产生 wire 型变量。
celldefine, endcelldefine这两个程序指令用于将模块标记为单元模块,他们包含模块的定义。例如一些与、或、非门,一些 PLL 单元,PAD 模型,以及一些 Analog IP 等。
celldefine **module** ( **input** clk, **input** rst, **output** clk_pll, **output** flag); …… **endmodule** endcelldefine
unconnected_drive, nounconnected_drive在模块实例化中,出现在这两个编译指令间的任何未连接的输入端口,为正偏电路状态或者为反偏电路状态。
assign用于对 wire 型变量进行赋值,不对寄存器赋值
进位输出(Carry out,通常表示为Co或Cout)是全加器的一个输出,它表示在两个二进制位相加时是否产生了进位。在二进制加法中,当两个加数位(A和B)的和大于或等于2时,就会产生进位,因为二进制中的每一位只能表示0或1。进位输出就是用来表示这个进位的。
module full_adder1(
input Ai, Bi, Ci,
output So, Co);
assign So = Ai ^ Bi ^ Ci ;
assign Co = (Ai & Bi) | (Ci & (Ai | Bi));
endmodule
更简单的:
module full_adder1(
input Ai, Bi, Ci,
output So, Co);
assign {Co, So} = Ai + Bi + Ci;
endmodule//普通时延,A&B计算结果延时10个时间单位赋值给Z
wire Z, A, B ;
assign #10 Z = A & B ;
//隐式时延,声明一个wire型变量时对其进行包含一定时延的连续赋值。
wire A, B;
wire #10 Z = A & B;
//声明时延,声明一个wire型变量是指定一个时延。因此对该变量所有的连续赋值都会被推迟到指定的时间。除非门级建模中,一般不推荐使用此类方法建模。
wire A, B;
wire #10 Z ;
assign Z =A & B
过程结构语句有 2 种,initial 与 always 语句
一个模块中可以包含多个 initial 和 always 语句,但 2 种语句不能嵌套使用。
但是 initial 语句或 always 语句内部可以理解为是顺序执行的(非阻塞赋值除外)。
initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的。
如果 initial 块内包含多个语句,需要使用关键字 begin 和 end 组成一个块语句。
如果 initial 块内只要一条语句,关键字 begin 和 end 可使用也可不使用。
initial 理论上来讲是不可综合的,多用于初始化、信号检测等。
与 initial 语句相反,always 语句是重复执行的。always 语句块从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复。
由于循环执行的特点,always 语句多用于仿真时钟的产生,信号行为的检测等。
parameter 关键字用于定义模块的参数。参数是一种可以在模块实例化时或在模块内部使用,但不一定要在模块的所有复制中传递的常数。简单地说,参数类似于函数或算法中的变量,它们在模块的复制品之间共享。连续性赋值使用assign语句,而过程性赋值使用always块。
阻塞赋值属于顺序执行,即下一条语句执行前,当前语句一定会执行完毕。
阻塞赋值语句使用等号 = 作为赋值符。
非阻塞赋值属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。
非阻塞赋值语句使用小于等于号 <= 作为赋值符。
如下所示,2 个 always 块中语句并行执行,赋值操作右端操作数使用的是上一个时钟周期的旧值,此时 a<=b 与 b<=a 就可以相互不干扰的执行,达到交换寄存器值的目的。
always @(posedge clk) begin
a <= b ;
end
always @(posedge clk) begin
b <= a;
end
Verilog 提供了 2 大类时序控制方法:时延控制和事件控制。事件控制主要分为边沿触发事件控制与电平敏感事件控制
常规时延
reg value_test ;
reg value_general ;
#10 value_general = value_test ;或:
#10 ;
value_ single = value_test ;内嵌时延
遇到内嵌延时时,该语句先将计算结果保存,然后等待一定的时间后赋值给目标信号。
内嵌时延控制加在赋值号之后。例如:
reg value_test ;
reg value_embed ;
value_embed = #10 value_test ;需要说明的是,这 2 种时延控制方式的效果是有所不同的。
当延时语句的赋值符号右端是常量时,2 种时延控制都能达到相同的延时赋值效果。
当延时语句的赋值符号右端是变量时,2 种时延控制可能会产生不同的延时赋值效果。
在 Verilog 中,事件是指某一个 reg 或 wire 型变量发生了值的变化。事件控制用符号 @ 表示。
设计:根据需求编写硬件描述语言(如Verilog或VHDL)代码来描述设计的功能和行为
synthesize综合,合成:综合代码,检查语法是否有错误,将高级的逻辑描述代码转换为逻辑门级别的网表或等效的门级电路
FloorPlanner 是 FPGA 设计流程中的一个重要工具,用于执行布局(Place)阶段的子任务,即对设计中的逻辑电路进行布局安置。在 FPGA 设计流程中,FloorPlanner 的地位如下:
在 FPGA 设计流程中,FloorPlanner 位于布局(Place)阶段之前,它为后续的布线(Route)阶段提供了优化的布局结果,从而帮助实现设计的最终映射和部署。
Place&Route :
开发流程中的 Place & Route 是指在将设计映射到 FPGA 芯片时的一个重要步骤。下面解释一下它的含义和作用:
在 Verilog 中,reg 类型通常用于表示存储元素(如寄存器),而不是直接连接到模块的输出端口。输出端口通常使用 output 或 inout 声明,并且通常需要与 wire 类型一起使用。
reg 类型在 Verilog 中表示的是寄存器类型,它在 always 块中使用,存储状态或信号。而 output 端口应该使用 wire 类型来表示,因为它们不会存储状态,只是将信号传递给其他部件。
因此,你在模块顶层中使用 output reg 是不符合常规的 Verilog 设计习惯的,通常应该使用 output wire。

半加器(Half Adder)和全加器(Full Adder)是数字电路中用于执行二进制加法的基本组件。它们的主要区别在于它们处理的输入数量和功能。
半加器: 半加器是一个组合逻辑电路,它接受两个二进制位作为输入,并产生两个输出:和(Sum)和进位(Carry)。半加器只处理两个输入位的加法,不考虑来自较低位的进位。半加器的输出进位只能表示当前两个输入位相加是否产生了进位。
半加器的逻辑可以表示为:
•
和(Sum) = A XOR B
•
进位(Carry) = A AND B
其中,A和B是两个输入位,XOR表示异或门,AND表示与门。
全加器: 全加器也是一个组合逻辑电路,它接受三个二进制位作为输入,并产生两个输出:和(Sum)和进位(Carry)。全加器的三个输入包括两个加数位(A和B)以及来自较低位的进位(Carry-in)。全加器能够处理包括进位在内的三个位的加法。
全加器的逻辑可以表示为:
•
和(Sum) = (A XOR B) XOR Carry-in
•
进位(Carry) = (A AND B) OR (Carry-in AND (A XOR B))
其中,Carry-in是来自较低位的进位,OR表示或门。
区别:
输入数量:半加器有两个输入,全加器有三个输入。
功能:半加器只计算两个输入位的和和进位,而全加器计算三个输入位(包括来自较低位的进位)的和和进位。
应用:半加器通常用于构建更复杂的加法器电路,如全加器。全加器则用于实现多位二进制数的加法,因为它能够处理进位。
在实际的数字电路设计中,全加器更为常用,因为它可以级联(Cascade)起来构成多位加法器,如4位、8位、16位等,从而实现更复杂的算术运算。
在Verilog中,parameter是一个关键字,用于定义模块(module)的参数
parameter定义的值在模块的整个实例化过程中都是固定的,不能被重新赋值。这意味着一旦定义了parameter,它的值在整个模块中都是不变的。
Assignment 赋值
SP代表single port单端口模式,即同一时钟控制读写,同一时间只能写或者度
在您提供的 Verilog 代码片段中,ram_inst 是一个实例化语句,用于创建一个 RAM 模块的实例。这个实例使用了多个引脚(pins),这些引脚定义了模块的输入和输出接口。下面是每个引脚的详细解释:
•
.dout(dout_o):这是 RAM 模块的输出引脚,名为 dout,它是一个 8 位宽度的输出信号。在这个实例中,它被连接到了一个名为 dout_o 的信号。
•
.clk(clk):这是 RAM 模块的时钟输入引脚,用于同步数据写入和读取操作。它被连接到了一个名为 clk 的信号。
•
.oce(oce_i):这是 RAM 模块的输出使能输入引脚,用于控制 dout 信号是否输出数据。它被连接到了一个名为 oce_i 的信号。
•
.ce(ce_i):这是 RAM 模块的芯片使能输入引脚,用于控制 RAM 是否可以进行读写操作。它被连接到了一个名为 ce_i 的信号。
•
.reset(reset_i):这是 RAM 模块的复位输入引脚,用于将 RAM 恢复到初始状态。它被连接到了一个名为 reset_i 的信号。
•
.wre(wre_i):这是 RAM 模块的写使能输入引脚,用于控制是否可以写入数据到 RAM。它被连接到了一个名为 wre_i 的信号。
•
.ad(addr):这是 RAM 模块的地址输入引脚,用于指定要读取或写入的 RAM 单元的地址。它被连接到了一个名为 addr 的信号,该信号是 11 位宽度的。
•
.din(data_i):这是 RAM 模块的数据输入引脚,用于写入数据到 RAM。它被连接到了一个名为 data_i 的信号,该信号是 8 位宽度的。
每个引脚都是 RAM 模块与其外部接口之间的连接点,它们定义了模块如何与外部信号交互。在实际的设计中,您需要确保这些引脚被正确地连接到相应的信号,并且信号的类型和宽度与 RAM 模块的要求相匹配。
FPGA中BRAM和DRAM的区别
FPGA(现场可编程门阵列)中的BRAM(块RAM)和DRAM(动态RAM)是两种不同类型的存储器,它们在设计和使用上有着显著的区别:
在选择使用BRAM还是DRAM时,设计者需要根据应用需求、性能要求、成本考虑和功耗限制来做出决策。对于需要高速、小容量存储的应用,BRAM通常是更好的选择;而对于需要大容量存储的应用,DRAM可能是更合适的选择。
在FPGA中,BRAM(块RAM)可以被配置为单端口模式或双端口模式,这两种模式在数据访问方式上有所不同:
在某些FPGA中,BRAM还可以配置为更高级的端口模式,如四端口模式,这允许更多的并行访问。设计者根据具体应用的需求来选择最合适的端口模式,以优化性能和资源利用。
十六进制数系统中的每个数字代表4位二进制数
在Verilog中,defparam是一个编译器指令,用于在模块实例化时重定义参数的值。这条指令可以用来改变模块实例化时参数的默认值。
在Verilog中,localparam关键字用于声明一个模块内部的参数,这个参数在模块的整个作用域内都是常量。localparam声明的参数是不可变的,它们的值在编译时就已经确定了。
`class FirstActivity : AppCompatActivity() {`
`override fun onCreate(savedInstanceState: Bundle?) {`
`super.onCreate(savedInstanceState)`
`}`
`}`这段代码是使用 Kotlin 语言编写的 Android 应用程序中的一个活动(Activity)类。让我逐步解释其中的内容:
class FirstActivity : AppCompatActivity():这是一个类的声明,类名为 FirstActivity,并且继承自 AppCompatActivity 类。AppCompatActivity 是 Android 开发中常用的一个基类,用于支持应用程序在较旧的 Android 版本上提供向后兼容性。override fun onCreate(savedInstanceState: Bundle?):这是 FirstActivity 类中的一个方法 onCreate() 的重写。在 Android 中,onCreate() 方法是活动生命周期的一部分,在活动第一次创建时被调用。savedInstanceState 参数是一个 Bundle 对象,其中包含了之前保存的活动状态信息,可以用于在活动重新创建时恢复状态。super.onCreate(savedInstanceState):这是调用父类 AppCompatActivity 中的 onCreate() 方法,确保父类中的初始化工作得以执行。因此,这段代码定义了一个名为 FirstActivity 的活动类,当该活动第一次创建时,会调用父类的 onCreate() 方法进行初始化。
在声明 savedInstanceState 参数时,如果在类型后面添加了 ?,则表示该参数可以接受 null 值。
这样做的目的是为了允许 savedInstanceState 参数在某些情况下为 null,例如当活动或片段首次创建时,savedInstanceState 可能为 null,因为此时还没有保存任何状态信息。
在 Kotlin 中,如果没有使用 ?,则表示该参数不接受 null 值,如果在实际使用中传递了 null 值,编译器将会报错。而使用了 ? 后,表示该参数可以接受 null 值,编译器将不会报错。
因此,在 override fun onCreate(savedInstanceState: Bundle?) 中,Bundle? 表示 savedInstanceState 参数可以接受 null 值,即在某些情况下,savedInstanceState 可能为 null。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button 1"
/>
</LinearLayout>这段代码是一个简单的 Android 布局文件,使用 XML 格式描述了一个线性布局(LinearLayout)以及一个按钮(Button)的布局和属性。
让我解释其中的一些关键部分:
<LinearLayout>:这是一个布局容器,用于在垂直方向排列其子视图(例如按钮)。xmlns:android="http://schemas.android.com/apk/res/android" 是 XML 命名空间声明,它使得可以在 XML 文件中使用 Android 提供的属性和元素。android:orientation="vertical":这个属性指定了线性布局的方向,即垂直方向。这意味着其中的子视图(此处为按钮)将会按照垂直方向排列。android:layout_width="match_parent" 和 android:layout_height="match_parent":这两个属性指定了布局的宽度和高度。match_parent 表示该布局将会填充其父容器的宽度或高度,以占据尽可能多的空间。<Button>:这是一个按钮视图,在布局中用于响应用户的点击事件。android:id="@+id/button1":这个属性为按钮指定了一个唯一的标识符,可以在 Java 代码中使用这个标识符来查找和操作这个按钮。wrap_content 属性可以使得布局更加灵活,可以根据内容的大小动态调整视图的尺寸,而不是固定为特定的尺寸。android:text="Button 1":这个属性为按钮设置了显示的文本内容为 “Button 1”。因此,这段代码描述了一个垂直排列的线性布局,其中包含一个按钮,按钮显示文本为 “Button 1”。按钮的宽度会填充父容器的宽度,而高度则根据按钮文本的大小动态调整。
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout)
}
}首先调用了 super.onCreate(savedInstanceState),以确保调用了父类的 onCreate() 方法,以便执行必要的初始化操作。
接着调用了 setContentView(R.layout.first_layout) 方法,这个方法用于设置该活动所使用的布局文件。在这里,first_layout 是指定的布局资源文件的名称,这个布局文件定义了活动的界面布局结构。
因此,这段代码的作用是在 FirstActivity 中设置了一个布局文件 first_layout 作为活动的界面布局,并在活动创建时加载该布局。
R.layout 是一个自动生成的资源标识符类,它包含了项目中所有布局文件的引用。在 Android 开发中,资源文件(如布局文件、字符串、图像等)都需要通过资源标识符来访问和引用。
当你在项目中创建布局文件时,每个布局文件都会被编译成一个资源标识符,以便在代码中进行引用。这些资源标识符都会被统一放置在 R 类的内部静态类中,而 R.layout 则是其中用于引用布局文件的子类之一。
Intent—>用于通信的消息对象
在 Android 中,Intent 是一种用于在不同组件之间进行通信的对象。它可以用于启动活动(Activity)、启动服务(Service)、发送广播(Broadcast)以及执行其他各种操作。Intent 提供了一种在不同组件之间传递数据和执行操作的机制。
Intent 本质上是一个消息对象,用于指示想要执行的操作。它可以包含以下信息:
android.intent.action.VIEW(查看操作)、android.intent.action.SEND(发送操作)、android.intent.action.MAIN(主操作)等。android.intent.category.LAUNCHER(启动器类别)、android.intent.category.BROWSABLE(可浏览类别)等。Intent 可以分为两种类型:
通过使用 Intent,Android 应用程序可以实现各种功能,例如启动新的活动、执行后台任务、发送广播等,从而实现各种复杂的交互和功能。
Kotlin直接可以:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout)
button1.setOnClickListener {
Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
}
}在 Kotlin 中,函数声明的语法是:
fun 函数名(参数列表): 返回值类型 {
// 函数体
}其中,返回值类型在函数名和参数列表之后,使用冒号 : 来标识。在这个语法中,返回值类型是必须的,但在某些情况下,如果函数没有返回值,可以将返回值类型指定为 Unit,或者省略返回值类型(在这种情况下,编译器会自动推断返回值类型为 Unit)。例如:
kotlin
fun greet(name: String): Unit {
println("Hello, $name!")
}
// 或者省略返回值类型,编译器会自动推断为 Unit
fun greet(name: String) {
println("Hello, $name!")
}override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main, menu)
return true
}menuInflater 是 Android 开发中的一个类,用于从 XML 文件中创建菜单对象。在 Android 中,通常使用 XML 文件定义应用程序中的菜单,然后通过 MenuInflater 类将这些 XML 文件中定义的菜单加载到应用程序中的菜单对象中,以供在用户界面中显示和操作。
具体来说,menuInflater.inflate() 方法用于将一个 XML 文件中定义的菜单资源加载到一个 Menu 对象中,这样就可以在应用程序的用户界面中显示这个菜单。
在 Android 应用程序中,Activity 和 Fragment 是两种重要的组件,用于构建用户界面和处理用户交互。它们都可以包含用户界面的布局,并且可以响应用户的输入事件(如点击、滑动等)。
下面是关于 Activity 和 Fragment 的简要介绍:
onCreate()、onStart()、onResume()、onPause()、onStop() 和 onDestroy() 等,开发者可以根据需要重写这些方法来执行相应的操作。onCreate()、onStart()、onResume()、onPause()、onStop() 和 onDestroy() 等。总的来说,Activity 通常代表一个完整的屏幕,而 Fragment 则是 Activity 中一个可重用的组成部分,可以在一个 Activity 中组合多个 Fragment 来构建复杂的用户界面。在实际开发中,Activity 和 Fragment 经常一起使用,以实现灵活和高效的用户界面设计。
在 Kotlin 中,?. 是安全调用运算符,用于在对象为非空时调用其方法或访问其属性。如果对象为 null,则安全调用运算符会短路并返回 null,而不会抛出空指针异常。
在你提供的代码中,data?.getStringExtra("data_return") 的意思是,如果 data 不为 null,则调用 getStringExtra("data_return") 方法来获取名为 “data_return” 的额外数据,否则返回 null。这种写法可以避免在 data 为 null 时引发空指针异常。
registerForActivityResult(...):这是一个用于注册 Activity Result 的函数。它接受一个 ActivityResultContract 对象作为参数,并返回一个 ActivityResultLauncher 对象。ActivityResultContract 是一个接口,用于定义活动启动和结果处理之间的合同。
Activity类中定义了7个回调方法,覆盖了Activity生命周期的每一个环节:
onCreate() 在Activity第一次被创建时调用
onStart() 由不可见到可见时调用
onResume()。这个方法在Activity准备好和用户进行交互的时候调用。此时的Activity一
定位于返回栈的栈顶,并且处于运行状态。
onPause()。这个方法在系统准备去启动或者恢复另一个Activity的时候调用。我们通常
会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执
行速度一定要快,不然会影响到新的栈顶Activity的使用。
onStop()。这个方法在Activity完全不可见的时候调用。它和onPause()方法的主要区
别在于,如果启动的新Activity是一个对话框式的Activity,那么onPause()方法会得到执行,而onStop()方法并不会执行。
onDestroy()。这个方法在Activity被销毁之前调用,之后Activity的状态将变为销毁状态。
onRestart()。这个方法在Activity由停止状态变为运行状态之前调用,也就是Activity被重新启动了。
onPause() 方法是 Android Activity 生命周期中的一个回调方法,用于指示当前 Activity 正在失去焦点并即将暂停其可见性。当某些事件发生时,系统会调用 onPause() 方法,这些事件包括:
onPause() 方法。例如,当用户点击应用中的按钮,打开新的 Activity 时,当前 Activity 将暂停。onPause() 方法。onPause() 方法。onPause() 方法。总之,onPause() 方法的调用情况涵盖了当前 Activity 失去焦点并即将暂停可见性的各种情况。
onStart() 是 Android 活动生命周期中的一个方法,用于指示活动即将变为可见状态。当活动首次启动或从停止状态恢复到活动栈时,系统会调用 onStart() 方法。
具体情况下,onStart() 方法会在以下几种情况下被调用:
onStart() 方法。此时,活动将从不可见状态转变为可见状态。onPause() 状态恢复到前台状态,系统会先调用 onStart() 方法,然后调用 onResume() 方法。这种情况通常发生在用户按下返回按钮、活动恢复到前台、或者其他活动被移除而当前活动重新进入前台的情况下。在 onStart() 方法中,通常执行一些与界面相关的初始化操作,比如恢复 UI 状态、注册广播接收器或者启动一些后台任务。需要注意的是,尽管活动已经变为可见状态,但此时活动并未处于用户的焦点下,用户可能无法与其进行交互。
onResume() 是 Android 活动生命周期中的一个方法,用于指示活动即将成为用户焦点并开始与用户进行交互。当活动从不可见状态变为可见状态,并且用户可以开始与其进行交互时,系统会调用 onResume() 方法。
具体情况下,onResume() 方法会在以下几种情况下被调用:
onStart() 方法,然后调用 onResume() 方法。此时,活动将从不可见状态转变为可见状态,并且用户可以开始与其进行交互。onPause() 状态恢复到前台状态,系统会先调用 onStart() 方法,然后调用 onResume() 方法。这种情况通常发生在用户按下返回按钮、活动恢复到前台、或者其他活动被移除而当前活动重新进入前台的情况下。onStop() 状态恢复到前台状态,系统会依次调用 onRestart()、onStart() 和 onResume() 方法。这种情况通常发生在用户按下 Home 键后再次打开应用程序的情况下。在 onResume() 方法中,通常执行一些与活动生命周期相关的操作,比如恢复用户的输入状态、启动动画效果或者连接到一些外部服务。需要注意的是,当活动处于 onResume() 状态时,它已经成为了用户焦点下的活动,并且用户可以直接与其进行交互。

当将视图或布局的尺寸设置为 “wrap_content” 时,系统会根据其内容自动调整视图或布局的尺寸,以便刚好容纳其内
在 Android 的清单文件(AndroidManifest.xml)中声明活动(Activity)时,可以使用两种方式:
<activity>...</activity>:这是一种传统的 XML 标签方式,用于定义活动的属性和行为。在 <activity> 标签中,可以指定活动的各种属性,例如名称、图标、主题等,并且可以包含其他标签和属性以提供更详细的配置。通常情况下,你可以在 <activity> 标签内部设置更多的属性,包括活动的名称、图标、主题等,以及与活动相关的其他设置。<activity/>:这是一种自闭合的 XML 标签方式,用于简单地声明一个活动而不指定任何属性或配置。这种方式适用于简单的活动声明,当你不需要指定任何属性或配置时,可以使用这种方式。例如,如果你的活动只需要基本的声明,而不需要指定任何其他属性或配置,你可以使用 <activity/> 标签。因此,你应该根据活动的具体需求和配置选择适当的方式。如果需要指定活动的各种属性和配置,可以使用 <activity>...</activity> 标签;如果活动只需要基本的声明而不需要指定任何其他属性或配置,可以使用 <activity/> 标签。
@style/Theme.AppCompat.Dialog 是指在 Android 应用程序中使用的一个预定义的对话框主题样式。这个样式通常用于创建对话框,即弹出式窗口,以在应用程序中显示临时信息、接受用户输入或执行其他操作。
这个主题样式基于 AppCompat 库提供的默认对话框主题进行定义,并且与 AppCompat 库中其他主题一样,它提供了跨不同 Android 版本的一致外观和行为。这样,你的应用程序就可以在不同版本的 Android 上保持一致的外观和用户体验。
android:exported="false" 是 Android 清单文件中 <activity> 元素的一个属性,用于指定该活动是否能够被其他应用程序组件或者系统组件访问。具体含义如下:
android:exported="false",则表示该活动不会被其他应用程序或系统组件调用。这意味着该活动只能被声明它的应用程序内部的组件所访问,其他应用程序或系统组件无法直接启动或与其交互。android:exported="true",则表示该活动可以被其他应用程序或系统组件调用。这意味着其他应用程序可以使用显式或隐式意图启动该活动,并且与其进行交互。在你的示例中,android:exported="false" 意味着 DialogActivity 这个活动不会被其他应用程序或系统组件访问。它只能被声明它的应用程序内部的组件所访问,其他应用程序无法直接启动或与其交互。这通常用于内部使用的活动,例如只在应用程序内部使用的对话框或者配置界面。
在 Android 开发中,Intent(意图)是用于在不同组件之间传递数据或执行操作的对象。它是一种在 Android 应用程序中实现组件之间通信的重要机制。
Intent 主要有两种类型:显式 Intent 和隐式 Intent。
显式 Intent:用于启动应用程序内部的组件,例如启动另一个活动(Activity)、启动服务(Service)等。显式 Intent 通过指定目标组件的类名来明确指示要执行的操作。
示例:
Intent intent = new Intent(this, AnotherActivity.class);
startActivity(intent);隐式 Intent:用于启动应用程序内或其他应用程序中具有特定功能的组件,而不需要指定目标组件的类名。隐式 Intent 根据指定的动作(Action)、数据(Data)等信息来寻找合适的组件执行操作。
示例:
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.example.com"));
startActivity(intent);Intent 的主要作用包括但不限于:
总之,Intent 是 Android 应用程序中用于实现组件之间通信和执行操作的重要机制。
在 Android 中,Bundle 是一个用来存储键值对数据的容器类。它通常被用于在不同的组件(如活动、片段)之间传递数据,或者在组件销毁和重新创建时保存和恢复数据的状态。
在 onCreate() 方法中,Bundle 参数 savedInstanceState 表示上一个实例状态的数据。当活动被销毁并重新创建时(例如,由于设备旋转或内存紧张),Android 系统会调用 onSaveInstanceState() 方法来保存当前的状态数据,并将其存储在 Bundle 中。然后,在活动重新创建时,系统会将之前保存的状态数据传递给 onCreate() 方法的 savedInstanceState 参数。
Log.d("FirstActivity", this.toString())this.toString() 是调用当前对象的 toString() 方法。在 Java 中,几乎所有的对象都继承自 Object 类,而 Object 类中有一个 toString() 方法,用于返回对象的字符串表示形式
当你调用 this.toString() 时,它会返回当前对象的字符串表示形式
Activity的启动模式:
在 Kotlin 中,object 关键字用于定义单例对象。单例对象是指在程序运行期间只有一个实例存在的对象。在给定的作用域内,单例对象只能有一个实例,因此它们非常适合用于管理全局状态或提供全局访问点。
关闭所有活动并不等同于关闭应用程序的进程。在 Android 应用程序中,当你调用 finish() 方法结束一个活动时,这个活动会被销毁,但应用程序的进程仍然在运行。即使所有活动都被销毁,应用程序的进程仍然存在于系统中。
为了完全退出应用程序,有时需要调用 System.exit(0) 或者 Process.killProcess(Process.myPid()) 这样的方法来结束应用程序的进程。这样做可以确保应用程序的所有组件都被终止,包括活动、服务、广播接收器等,从而实现应用程序的完全退出。
然而,这种做法通常被视为不推荐的,因为它可能会导致用户体验上的问题,以及系统资源的浪费。通常情况下,Android 系统会自动管理应用程序的进程,并在必要时终止它们,而不需要手动干预。因此,大多数情况下,关闭所有活动即可满足应用程序退出的需求,而无需手动杀死进程。
在 Kotlin 中,companion object(伴生对象)具有以下作用:
总的来说,伴生对象提供了一种在 Kotlin 中实现类级别功能的方式,它可以用于静态成员访问、工厂方法、共享代码和扩展方法等场景。
StringBuilder是Java中的一个类,用于处理字符串。它允许你在不创建新的String对象的情况下进行字符串的修改和操作,这样可以提高性能并节省内存。
当你需要对字符串进行频繁的修改时,使用StringBuilder通常比直接使用String更高效。这是因为String是不可变的,每次对String进行修改都会创建一个新的String对象,而StringBuilder允许你在同一个对象上执行多次修改操作。
你可以使用StringBuilder的append()方法来添加字符串,使用delete()方法来删除字符串,使用insert()方法来在指定位置插入字符串,使用replace()方法来替换字符串等等。最后,你可以使用toString()方法将StringBuilder对象转换为String对象,以便进行进一步的操作或输出。
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") val builder = StringBuilder() builder.append("Start eating fruits.\n") for (fruit in list) { builder.append(fruit).append("\n") } builder.append("Ate all fruits.") val result = builder.toString() println(result) 为什么最后要调用builder.toString(),不能直接打印吗
在上面的代码中,builder是一个StringBuilder对象,而println函数通常接受String类型的参数来进行输出。而builder对象是一个StringBuilder类型的对象,不是String类型的对象,因此无法直接传递给println函数。
调用builder.toString()方法将StringBuilder对象转换为String类型的对象,这样就可以将其作为参数传递给println函数,从而实现输出。toString()方法会返回StringBuilder对象中包含的所有字符序列作为一个字符串。
因此,为了能够正确地输出builder对象中的内容,需要先调用toString()方法将其转换为String类型的对象,然后再传递给println函数进行打印。
with 是 Kotlin 的一个标准库函数,它提供了一种简洁的方式来执行针对特定对象的一系列操作,并返回最后一个表达式的结果。
AlertDialog.Builder(this).apply { setTitle("This is Dialog") setMessage("Something important.") setCancelable(false) setPositiveButton("OK") { dialog, which -> } setNegativeButton("Cancel") { dialog, which -> } show() 是什么意思
这段代码是使用 Kotlin 的标准库函数 apply 来构建一个 AlertDialog 对话框,并设置其标题、消息、按钮等属性。下面是代码的解释:
AlertDialog.Builder(this):创建了一个 AlertDialog.Builder 的实例,这个实例用于构建一个对话框。apply { ... }:apply 是一个 Kotlin 标准库函数,它接收一个 lambda 表达式作为参数,并在 lambda 表达式的作用域内执行一系列操作。在这个例子中,apply 函数被调用后,会将调用者对象(即 AlertDialog.Builder 的实例)作为参数传递给 lambda 表达式,并在 lambda 表达式内部执行一系列设置操作。setTitle("This is Dialog"):在 lambda 表达式中调用 setTitle 方法,设置对话框的标题为 “This is Dialog”。setMessage("Something important."):在 lambda 表达式中调用 setMessage 方法,设置对话框的消息内容为 “Something important.”。setCancelable(false):在 lambda 表达式中调用 setCancelable 方法,设置对话框为不可取消状态,即用户点击对话框外部或返回键时不会关闭对话框。setPositiveButton("OK") { dialog, which -> ... }:在 lambda 表达式中调用 setPositiveButton 方法,设置对话框的确定按钮文本为 “OK”,并设置点击事件处理器。在这里,{ dialog, which -> ... } 是一个 lambda 表达式,表示当用户点击确定按钮时执行的操作。在本例中,lambda 表达式为空,即不执行任何操作。setNegativeButton("Cancel") { dialog, which -> ... }:在 lambda 表达式中调用 setNegativeButton 方法,设置对话框的取消按钮文本为 “Cancel”,并设置点击事件处理器。同样,{ dialog, which -> ... } 是一个 lambda 表达式,表示当用户点击取消按钮时执行的操作。在本例中,lambda 表达式为空,即不执行任何操作。show():在所有设置完成后,调用 show 方法显示对话框。因此,这段代码的作用是创建一个带有标题、消息、确定按钮和取消按钮的对话框,并将其显示在界面上。
三种基本布局:

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view = LayoutInflater.from(context).inflate(resourceId, parent, false) val fruitImage: ImageView = view.findViewById(R.id.fruitImage) val fruitName: TextView = view.findViewById(R.id.fruitName) val fruit = getItem(position) // 获取当前项的Fruit实例 if (fruit != null) { fruitImage.setImageResource(fruit.imageId) fruitName.text = fruit.name } return view }
其中:val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
作用是从 XML 布局文件中创建一个视图对象,并将其添加到指定的父视图中。
解释每个参数的含义:
context: 表示当前的上下文环境,通常是一个 Activity 或 Fragment 的实例。resourceId: 表示要加载的布局文件的资源 ID,即 XML 文件的唯一标识符。parent: 表示要将创建的视图添加到的父视图,通常是一个 ViewGroup,比如一个 LinearLayout 或者一个 RecyclerView。false: 表示在加载布局文件时,不将其添加到父视图中。这个参数设置为 false 表示我们会手动将视图添加到父视图中,而不是在加载时自动添加。接口在编程中有多种作用,包括但不限于以下几个方面:
总的来说,接口是一种重要的编程工具,可以帮助程序员设计出更加灵活、可扩展和易于维护的代码结构。
interface接口
sealed class密封类

companion object 是 Kotlin 中的一个关键字,它用于创建一个伴随对象。伴随对象是类中的一个单例对象,可以通过类名直接访问其中的属性和方法
news_content_frag.xml——–>NewsContentFragment类———–>NewsContentActivity
news_item.xml——–>news_title_frag.xml———>NewsTitleFragment类———->
layout/activity_main.xml——->单页模式;
NewsTitleFragment类————>
layout-sw600dp/activity_main.xml——–>双页模式;
NewsContentFragment类——->
在NewsTitleFragment类中通过RecyclerView将新闻列表展示出来
在 Kotlin 中,open 关键字用于声明一个类、方法或属性是可以被继承或覆盖的。具体来说:
open 关键字,表示这个类是可以被其他类继承的。如果不加 open 关键字,则默认情况下类是 final 的,不能被继承。kotlin
open class BaseActivity : AppCompatActivity() {
// 可以被继承的类
}open 关键字,表示这个方法是可以被子类覆盖(重写)的。如果不加 open 关键字,则默认情况下方法是 final 的,不能被子类覆盖。kotlin
open fun someMethod() {
// 可以被子类重写的方法
}open 关键字,表示这个属性是可以被子类覆盖的。如果不加 open 关键字,则默认情况下属性是 final 的,不能被子类覆盖。kotlin
open val someProperty: Int = 0使用 open 关键字可以让类的设计更加灵活,允许其他类继承并重写其中的方法,或者覆盖属性的行为。
val square: (Int) -> Int = { x: Int -> x * x }
square是(Int) -> Int函数类型的变量,表示接受一个 Int 类型的参数,并返回一个 Int 类型的结果。
Lambda 表达式 { x: Int -> x * x } 被赋给了这个变量
fun save(inputText: String) {try {val output = openFileOutput("data", Context.MODE_PRIVATE)val writer = BufferedWriter(OutputStreamWriter(output))writer.use {it.write(inputText)}} catch (e: IOException) {e.printStackTrace()}}
这段代码定义了一个名为 save 的函数,它接受一个 inputText 参数,类型为 String。函数的作用是将输入的文本内容保存到文件中。让我解释一下代码的逻辑:
openFileOutput("data", Context.MODE_PRIVATE): 这行代码打开一个名为 “data” 的文件,并返回一个 FileOutputStream 对象。这个文件将会存储在应用的内部存储空间中,因为使用了 Context.MODE_PRIVATE 参数,表示只有当前应用可以访问这个文件,并且会覆盖掉同名的文件(如果存在)。val writer = BufferedWriter(OutputStreamWriter(output)): 这行代码创建了一个 BufferedWriter 对象,并将其包装在一个 OutputStreamWriter 中,以便将文本写入到 output 中。writer.use { it.write(inputText) }: 这行代码使用 Kotlin 的 use 函数,确保在使用结束后正确关闭 writer,以释放资源。在 use 函数的 lambda 表达式中,调用了 write 方法,将 inputText 写入到文件中。catch (e: IOException) { e.printStackTrace() }: 这是一个异常处理块,如果在保存文件时发生了 IOException 异常,程序将会打印异常的堆栈信息。将委托功能分为了两种:类委托和委托属性
PNNX
PNNX项目 PyTorch Neural Network eXchange(PNNX)是PyTorch模型互操作性的开放标准。PNNX为PyTorch提供了一种开源的模型格式,它定义了与Pytorch相匹配的数据流图和运算图,我们的框架在PNNX之上封装了一层更加易用和简单的计算图格式。pytorch训练好一个模型之后,然后模型需要转换到pnnx格式,然后pnnx格式我们再去读取,形成计算图.
pytorch到我们计算图?
PNNX帮我做了很多的图优化、算子融合的工作,所以底层的用它PNNX的话,我们可以吸收图优化的结果,后面推理更快.
但是不直接在项目中用PNNX,因为别人的工作和自己推理框架开发思路总是有不同的。所以在这上面封装,又快速又好用方便,符合自己的使用习惯。
我们只是去读取PNNX产物,然后构建自己一种易用的计算图结构。
产物:resnet18.pnnx.param PNNX graph definition 结构定义
resnet18.pnnx.bin PNNX model weight 权重
PNNX的格式定义:
PNNX由操作数operand(运算数)和operator(运算符号),PNNX::Graph用来管理和操作这两者。
操作数(operand),也可以通过操作数来方向访问到这个数字的产生者和使用者Customer
Operand有以下几个部分组成:
Producer: 类型是operator, 表示产生了这个操作数的运算符(operator). 也就是说这个操作数(operand)是Producer的输出. Producer这个操作符号产生了当前的Operand
Customer:类型是operator, 表示需要这个操作数下一个操作的的运算符(operator),也就是说这个操作数(operand)作为Customer的输入存在.
Name: 类型是std::string, 表示这个操作数的名称.Values
Shape: 类型是std::vector
this:指针,指向当前对象的地址。在类的成员函数中,this关键字指向当前对象。
->:成员访问运算符,用来访问类的成员变量或成员函数。
在 C++11 中,使用 new 创建动态分配的对象并不安全,因为需要手动管理资源的释放,容易出现内存泄漏或者忘记释放资源导致的内存泄漏。为了更加安全和方便地管理动态分配的对象,C++11 引入了智能指针,其中 std::unique_ptr 是一种独占所有权的智能指针,它负责在对象不再需要时释放所占用的内存。
图(graph) 节点(vertex) 边(edge)



一阶电路三要素法:
https://zhuanlan.zhihu.com/p/367671862
续流二极管的作用:当输入电压由变+VCC为0V时,三极管由饱和变为截止,这样继电器电感线圈中的电流突然失去了流通通路,若无续流二极管D将在线圈两端产生较大的反向电动势,极性为下正上负,电压值可达一百多伏,这个电压加上电源电压作用在三极管的集电极上足以损坏三极管。故续流二极管D的作用是将这个反向电动势通过图中箭头所指方向放电,使三极管集电极对地的电压最高不超过+VCC +0.7V。






衰减电阻网络需要搭配上电容来进行补偿是什么意思
在电子电路中,衰减电阻网络通常是指由电阻组成的网络,用于降低信号的电压或电流。而提到衰减电阻网络需要搭配电容来进行补偿,这通常是指在某些电路设计中,为了改善电路的性能,需要在不改变电路基本功能的前提下,通过添加电容元件来修正或改善电路的某些特性。
补偿电容的作用可能包括:
为什么添加电容可以补偿这种相位偏移
在电路中,电阻和电容都会引入相位偏移。电阻器的相位偏移通常与频率成正比,而电容器则与频率成反比.为了补偿这种相位偏移,电路设计者会在电路中添加电容器,以便在高频应用中提供额外的相位移动,从而抵消电阻引起的相位偏移。通过这种方式,电路可以在整个频率范围内保持稳定的相位响应,从而提高电路的整体性能。
``c make -DCMAKE_BUILD_TYPE=None `
-DCMAKE_INSTALL_PREFIX=/home/zgh/pcl_1.9.1_compile \
-DBUILD_GPU=ON \
-DBUILD_apps=ON \
-DBUILD_examples=ON
-DCUDA_TOOLKIT_ROOT_DIR=/usr/local/cuda-12.2 \
-DCUDA_ARCH_BIN=8.6 \
-DCUDA_ARCH_PTX=8.6 ..
if(NOT ${CUDA_VERSION_STRING} VERSION_LESS "10.0")
set(__cuda_arch_bin "3.0 3.5 5.0 5.2 5.3 6.0 6.1 7.0 7.2 7.5")
elseif(NOT ${CUDA_VERSION_STRING} VERSION_LESS "9.1")
set(__cuda_arch_bin "3.0 3.5 5.0 5.2 5.3 6.0 6.1 7.0 7.2")
elseif(NOT ${CUDA_VERSION_STRING} VERSION_LESS "9.0")
set(__cuda_arch_bin "3.0 3.5 5.0 5.2 5.3 6.0 6.1 7.0")
elseif(NOT ${CUDA_VERSION_STRING} VERSION_LESS "8.0")
set(__cuda_arch_bin "2.0 2.1(2.0) 3.0 3.5 5.0 5.2 5.3 6.0 6.1")
elseif(NOT ${CUDA_VERSION_STRING} VERSION_LESS "6.5")
set(__cuda_arch_bin "2.0 2.1(2.0) 3.0 3.5 5.0 5.2")
elseif(NOT ${CUDA_VERSION_STRING} VERSION_LESS "6.0")
set(__cuda_arch_bin "2.0 2.1(2.0) 3.0 3.5 5.0")
elseif(NOT ${CUDA_VERSION_STRING} VERSION_LESS "5.0")
set(__cuda_arch_bin "2.0 2.1(2.0) 3.0 3.5")
elseif(${CUDA_VERSION_STRING} VERSION_GREATER "4.1")
set(__cuda_arch_bin "2.0 2.1(2.0) 3.0")
else()
set(__cuda_arch_bin "2.0 2.1(2.0)")
endif()NXHAY2OW76-eyJsaWNlbnNlSWQiOiJOWEhBWTJPVzc2IiwibGljZW5zZWVOYW1lIjoiSHVuYW4gSW5zdGl0dXRlIG9mIFNjaWVuY2UgYW5kIFRlY2hub2xvZ3kiLCJsaWNlbnNlZVR5cGUiOiJDTEFTU1JPT00iLCJhc3NpZ25lZU5hbWUiOiJ0bXUgaXRtYW5hZ2VyIiwiYXNzaWduZWVFbWFpbCI6InRtdWl0bWFuYWdlckBvdXRsb29rLmNvbSIsImxpY2Vuc2VSZXN0cmljdGlvbiI6IkZvciBlZHVjYXRpb25hbCB1c2Ugb25seSIsImNoZWNrQ29uY3VycmVudFVzZSI6ZmFsc2UsInByb2R1Y3RzIjpbeyJjb2RlIjoiR08iLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IlJTMCIsInBhaWRVcFRvIjoiMjAyNC0wMi0wOSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiRE0iLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IkNMIiwicGFpZFVwVG8iOiIyMDI0LTAyLTA5IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJBQyIsInBhaWRVcFRvIjoiMjAyNC0wMi0wOSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUlNVIiwicGFpZFVwVG8iOiIyMDI0LTAyLTA5IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJSU0MiLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUEMiLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IkRTIiwicGFpZFVwVG8iOiIyMDI0LTAyLTA5IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJSRCIsInBhaWRVcFRvIjoiMjAyNC0wMi0wOSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUkMiLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IlJTRiIsInBhaWRVcFRvIjoiMjAyNC0wMi0wOSIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJSTSIsInBhaWRVcFRvIjoiMjAyNC0wMi0wOSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiSUkiLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IkRQTiIsInBhaWRVcFRvIjoiMjAyNC0wMi0wOSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiREIiLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IkRDIiwicGFpZFVwVG8iOiIyMDI0LTAyLTA5IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJQUyIsInBhaWRVcFRvIjoiMjAyNC0wMi0wOSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUlNWIiwicGFpZFVwVG8iOiIyMDI0LTAyLTA5IiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IldTIiwicGFpZFVwVG8iOiIyMDI0LTAyLTA5IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJQU0kiLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUENXTVAiLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUlMiLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiRFAiLCJwYWlkVXBUbyI6IjIwMjQtMDItMDkiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUERCIiwicGFpZFVwVG8iOiIyMDI0LTAyLTA5IiwiZXh0ZW5kZWQiOnRydWV9XSwibWV0YWRhdGEiOiIwMTIwMjMxMTA5TFBBQTAwOTAwNyIsImhhc2giOiI0MjU0OTY3OS8xMzU2NTA1OTotNDU5NTIyNTciLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwidHJpYWwiOmZhbHNlLCJhaUFsbG93ZWQiOnRydWV9-t3Mcnh+cc/DF5z54xXxQ1wh2DTHULDDp0xl9P1s3jRugff7MKaHhYu8MFuKB8smfDaUysLfs94WjunQJeCc4L/gMi024G9o/O2yBx4/Ho9yU7SeYOvnuMqPwwpczR/U2JcIZAhH9YPdOo5E7CEtPEW5cq774wN7MrXnHK+LPN6uE4asZ/Qk3g3TyqpD3R7ubTmtkAx8JF6iWrezyLNngezgq3NkSE+3LZFuOLep6EKsOJeuaPCKD1uSFJZK7yzUkcLq+H/AZDOPZ+Gk9ZTl+P8TlRw3DIA1WDMORmycx5Q4mG+y2cNJoj9r0gfZPiw8OcmcQI8icyF2yTLnWOopRUg==-MIIETDCCAjSgAwIBAgIBDzANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIyMTAxMDE2MDU0NFoXDTI0MTAxMTE2MDU0NFowHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMjEwMTAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/W3uCpU5M2y48rUR/3fFR6y4xj1nOm3rIuGp2brELVGzdgK2BezjnDXpAxVDw5657hBkAUMoyByiDs2MgmVi9IcqdAwpk988/Daaajq9xuU1of59jH9eQ9c3BmsEtdA4boN3VpenYKATwmpKYkJKVc07ZKoXL6kSyZuF7Jq7HoQZcclChbF75QJPGbri3cw9vDk/e46kuzfwpGftvl6+vKibpInO6Dv0ocwImDbOutyZC7E+BwpEm1TJZW4XovMBegHhWC04cJvpH1u98xoR94ichw0jKhdppywARe43rGU96163RckIuFmFDQKZV9SMUrwpQFu4Z2D5yTNqnlLRfAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQU5FZqQ4gnVc+inIeZF+o3ID+VhcEwSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBANLG1anEKid4W87vQkqWaQTkRtFKJ2GFtBeMhvLhIyM6Cg3FdQnMZr0qr9mlV0w289pf/+M14J7S7SgsfwxMJvFbw9gZlwHvhBl24N349GuthshGO9P9eKmNPgyTJzTtw6FedXrrHV99nC7spaY84e+DqfHGYOzMJDrg8xHDYLLHk5Q2z5TlrztXMbtLhjPKrc2+ZajFFshgE5eowfkutSYxeX8uA5czFNT1ZxmDwX1KIelbqhh6XkMQFJui8v8Eo396/sN3RAQSfvBd7Syhch2vlaMP4FAB11AlMKO2x/1hoKiHBU3oU3OKRTfoUTfy1uH3T+t03k1Qkr0dqgHLxiv6QU5WrarR9tx/dapqbsSmrYapmJ7S5+ghc4FTWxXJB1cjJRh3X+gwJIHjOVW+5ZVqXTG2s2Jwi2daDt6XYeigxgL2SlQpeL5kvXNCcuSJurJVcRZFYUkzVv85XfDauqGxYqaehPcK2TzmcXOUWPfxQxLJd2TrqSiO+mseqqkNTb3ZDiYS/ZqdQoGYIUwJqXo+EDgqlmuWUhkWwCkyo4rtTZeAj+nP00v3n8JmXtO30Fip+lxpfsVR3tO1hk4Vi2kmVjXyRkW2G7D7WAVt+91ahFoSeRWlKyb4KcvGvwUaa43fWLem2hyI4di2pZdr3fcYJ3xvL5ejL3m14bKsfoOv
pcl::PointCloud 和 pcl::PCLPointCloud2 是 PCL 中两种不同的点云数据结构,它们之间的主要区别在于数据的组织形式和灵活性:
pcl::PointCloud:pcl::PointCloud 是 PCL 中用于表示点云的模板类。pcl::PointCloud 可以更方便地访问和处理点云数据,因为它提供了成员函数来操作点云中的点,如添加、删除、修改等操作。pcl::PCLPointCloud2:pcl::PCLPointCloud2 是 PCL 中用于表示点云的更底层、更通用的数据结构。pcl::PCLPointCloud2 需要通过字段的偏移量和数据类型来手动解析和访问点云数据,相对复杂一些。总的来说,pcl::PointCloud 更适合处理已知结构和类型的点云数据,而 pcl::PCLPointCloud2 则更适合处理未知结构或需要更大灵活性的点云数据。在实际应用中,根据具体需求选择合适的数据结构进行点云数据的处理和操作。
std::cerr 通常用于输出程序运行时的错误信息、警告信息等
为什么原理图里的stm32f4的VCAP1引脚要接一个4.7微法的电容,再接地呢
2023/12/25 08:23:32
STM32F4芯片的VCAP1引脚是芯片内部电容的引出端,用于连接外部电容以提供给芯片内部振荡电路使用。在使用高速外设时,由于这些外设需要非常稳定的电源供应,因此需要在VCAP1引脚与地之间连接一个电容来提高芯片的稳定性。
具体来说,连接4.7微法的电容可以起到平滑电压、提高电源噪声抑制能力等作用,从而提高芯片的工作稳定性和可靠性。此外,需要注意电容的选型要符合芯片手册的要求,不能超出规定的范围。
2023/12/25 08:26:15
芯片内部还有电容吗
2023/12/25 08:26:20
是的,芯片内部通常会包含一些电容。这些内部电容主要用于稳定芯片的电源供应和进行滤波。它们的作用包括:
电源滤波:在芯片内部集成了一些小型电容器,用于过滤电源中的噪声和干扰,确保芯片正常运行所需的电源干净和稳定。
芯片电源稳定:芯片内部可能还包含一些供电相关的电容,用于提供瞬态响应和电源稳定性,以确保芯片在各种工作条件下都能正常工作。这些内部电容一般由芯片制造商在设计阶段根据具体需求添加,并在芯片手册或参考设计中进行了详细说明。在使用芯片时,我们不需要特别处理这些内部电容,只需按照芯片手册的要求合理连接外部电容即可。
2023/12/25 08:26:41
那为什么还要连接外部电容
2023/12/25 08:26:48
连接外部电容主要是为了进一步提高芯片的电源稳定性和抑制电磁干扰。虽然芯片内部已经包含了一些电容,但这些电容的容值较小且位置相对固定,可能无法满足某些特殊应用或高要求的电源稳定性。
通过在VCAP1引脚与地之间连接外部电容,可以实现以下效果:
改善电源噪声抑制能力:外部电容可以提供更大的容值,能够更好地滤除电源中的高频噪声和纹波,从而提供更稳定的电源供应。
提供额外的瞬态响应:外部电容能够快速响应芯片工作状态的变化,提供更好的瞬态响应能力,尤其在高速外设工作时,可以提供更稳定的电源给这些外设。
抑制电磁干扰:外部电容能够帮助减少电源线上的电磁辐射和接收到的外界干扰信号,提高系统的抗干扰能力,保证芯片的正常运行。需要注意的是,具体的外部电容选型和连接方式应根据芯片厂商提供的参考设计或芯片手册的建议进行,以确保电路的稳定性和可靠性。


配置通道 具有检测正反插…..等

边带使用实现…..





OSC32IN引脚,用于连接外部低速晶体振荡器(32.768 kHz)。这种振荡器通常用于实时时钟(RTC)的时基。
OSC_IN引脚,用于连接外部高速晶体振荡器(通常在4 MHz到26 MHz之间)。这种振荡器通常用于系统时钟(SYSCLK)的时基。
VBAT代表电池供电引脚(Voltage Battery)。VBAT引脚用于连接外部备用电池,通常是一个小型电池,以提供芯片的备份电源。
VCAP1是指STM32系列芯片上的一个电容滤波器引脚,用于为芯片内部提供稳定的电源环境
,用于去除芯片电源中的高频噪声和波动。

在电路设计中,加入0欧电阻(也称为短接电阻)有以下几个常见的目的:
需要注意的是,加入0欧电阻并不会引入电阻本身的阻尼效应。它的主要作用是提供连接和分配功能,而不会对电路的整体电性能产生实质性的影响。

TPS3823-33DBVR的MR引脚是干什么的
1/3/2024, 5:11:40 PM
TPS3823-33DBVR是一款超低功耗、微型化的复位监测器。MR引脚是器件的主复位引脚,用于手动地对器件进行复位操作。
当MR引脚被拉低时,TPS3823-33DBVR会立即进行复位操作,并将RST引脚拉低,使被监测的系统或芯片得到重新启动。此外,当MR引脚保持低电平时,TPS3823-33DBVR将始终保持在复位状态,直到MR引脚上升到高电平。
需要注意的是,MR引脚应该在正常工作时保持高电平,并通过一个合适的电阻接到正电源。当需要手动复位时,可以通过连接一个按钮或开关来将MR引脚拉低,从而触发复位操作。
总之,TPS3823-33DBVR的MR引脚是用于手动复位操作的引脚,当MR引脚被拉低时,TPS3823-33DBVR会进行复位操作,从而实现对被监测系统或芯片的重新启动。
1/3/2024, 5:12:51 PM
TPS3823-33DBVR的WDI引脚是干什么的
1/3/2024, 5:13:13 PM
TPS3823-33DBVR的WDI引脚是用于喂狗定时器(Watchdog Timer)的输入引脚。
喂狗定时器是一种用于监控系统运行状态的计时器。它需要周期性地接收一个喂狗脉冲来防止系统出现故障或死锁。如果在一定的时间内没有接收到喂狗脉冲,喂狗定时器会触发复位操作,使被监控的系统重新启动。
在TPS3823-33DBVR中,WDI引脚用于接收外部喂狗脉冲。当WDI引脚接收到一个高电平脉冲时,喂狗定时器会被重置,系统继续正常运行。如果在一定时间内未接收到喂狗脉冲,喂狗定时器将超时,并触发复位操作。
通过连接一个外部的喂狗脉冲源(例如微处理器的GPIO引脚),可以周期性地向TPS3823-33DBVR的WDI引脚提供喂狗脉冲。这样可以确保系统持续运行,并在出现故障或死锁时进行自动复位,提高系统的可靠性和稳定性。
总之,TPS3823-33DBVR的WDI引脚用于接收外部喂狗脉冲,用于喂狗定时器的重置操作,以确保系统正常运行并防止故障或死锁。
MPU6050
https://blog.csdn.net/qq_52251819/article/details/134957492

AHT21

Vdd_IO和Vdd是两个不同的电源引脚,常见于集成电路或芯片中。
区别:
在 STM32 HAL 库中,xx_MspInit() 和 xx_Init() 函数是针对特定外设的初始化函数。
HAL_ADC_ConfigChannel()是ADC常规通道配置,一次配置一个通道
HAL_ADC_PollForConversion()是轮询方式等待ADC常规通道转换完成
HAL_ADC_GetValue()读取常规通道转换结果寄存器的数据
HAL_ADC_Start()以软件方式启动ADC常规转换的通道,然后调用HAL_ADC_PollForConversion()来查询转换是否完成,如果转换完成,使用HAL_ADC_GetValue()读出常规结果寄存器的32位数据
stm32是32位的,所以指针总是32位的





工作模式:

低功耗模式:



stm32的SPI的Clock Phase(CPHA)设置为2 Edge是什么意思
2024/3/23 14:29:32
在 STM32 微控制器的 SPI(Serial Peripheral Interface)模块中,Clock Phase(时钟相位,通常用 CPHA 表示)参数用于定义数据采样的时机。当设置为 2 Edge 时,表示数据采样会在时钟的第二个边缘上进行。
在 SPI 通信中,数据传输的时机是由时钟信号决定的。CPHA 参数定义了数据采样的时机相对于时钟信号的边缘。具体来说,CPHA 可以设置为 0 或 1,分别表示:
因此,当将 SPI 的 CPHA 参数设置为 2 Edge 时,表示数据采样会在时钟的第二个边缘上进行,这种设置通常用于特定的 SPI 设备或者通信协议要求,确保数据的正确采样和传输。在实际应用中,需要根据外设设备的要求和通信协议的规范来选择合适的 CPHA 设置,以确保 SPI 数据传输的准确性和稳定性。





不使用预装载,设置的新ARR的值立即生效
使用,会在下一个UEV事件生效
PWM波:



HAL_TIM_IC_CaptureCallback(htim);
HAL_TIM_OC_DelayElapsedCallback(htim);
HAL_TIM_PWM_PulseFinishedCallback(htim);
HAL_TIM_PeriodElapsedCallback(htim);
HAL_TIM_TriggerCallback(htim);
HAL_TIMEx_CommutCallback(htim);


mpu_prototypes.h
mpu_wrappers.h内存保护单元 ,内存保护单元(memory protection unit),简称:MPU,使用MPU可以设置不同存储区域的存储访问特性(如只支持特权访问或全访问)和存储器属性(如可缓存、可共享),从而提高嵌入式系统的健壮性,使系统更加安全。优先执行mpu_prototypes.h,mpu_wrappers.h里的函数




MPU:内存保护单元
FPU:浮点数单元
钩子函数类似于回调函数






freertos中任何时候都要有一个任务占用CPU,所以就有了空闲任务
systick只有定时中断功能,1ms中断一次
systick定时器不仅可以产生滴答信号,还可以产生任务切换申请


所以:Freertos的任务优先级总是低于系统中断的优先级


xTaskCreate()创建一个任务,以动态方式来分配内存
xTaskCreateStatic()
vTaskDelete()可以删除当前任务或者另一个任务
vTaskSuspend()可以挂起当前任务或者另一个任务
vTaskResume()恢复另一个挂起的任务的运行
调度器管理的函数:
vTaskStartScheduler()开启任务调度器
vTaskSuspendAll()挂起任务调度器,但是不禁用中断,调度器被挂起后,不再进行上下文切换
vTaskResumeAll()恢复调度器的执行,但是不会恢复vTaskSuspend()单独挂起的任务
延时和调度的函数:
中央对齐模式下:
RCR = 0,更新事件没有延迟;
RCR = 1,更新事件延后了半个PWM周期;
RCR = 2, 更新事件延后了一个PWM周期;
RCR = 3,更新事件延后了 3 2 \cfrac{3}{2} 23个PWM周期;