定时器是微控制器中的关键外设,用于精确控制时间和事件。通过配置时钟源、预分频器、计数周期和比较值,可以实现各种时间控制任务,如定时中断、PWM生成和时间测量。理解定时器的工作原理和配置方法是嵌入式系统开发中的基本技能。 STM32F407 有众多的定时器,其中包括 2 个基本定时器(TIM6 和 TIM7)、10 个通用定时 器(TIM2~TIM5、TIM9~TIM14)、2 个高级控制定时器(TIM1 和 TIM8),这些定时器彼此完 全独立,不共享任何资源。本篇博客我们学习如何使用 STM32F407 的基本定时器中断。我们将使用 TIM6 的定时器中断来控制 LED0的翻转。
目录
一、TIM 简介
1.1 定时器的用途
1.2 STM32F4的定时器资源
1.3 定时器的理解
二、基本定时器
2.1 基本定时器简介
2.2 基本定时器功能框图
2.2.1 时钟源
2.2.2 控制器
2.2.3 时基单元(重点)
2.3 基本定时器的寄存器
2.3.1 控制寄存器 1(TIMx_CR1)
2.3.2 DMA/中断使能寄存器(TIMx_DIER)
2.3.3 状态寄存器(TIMx_SR)
2.3.4 计数器寄存器(TIMx_CNT)
2.4.5 预分频寄存器(TIMx_PSC)
2.4.6 自动重载寄存器(TIMx_ARR)
2.5 定时器周期的计算(定时时间计算)
2.6 定时器初始化结构体详解
2.7 基本定时器中断
2.7.1 基本定时器中断应用
2.7.2 基本定时器中断配置步骤
三、使用基本定时器实现定时3秒,触发中断。
一、TIM 简介
1.1 定时器的用途
定时器(Timer)最基本的功能就是定时了,比如定时发送 USART 数据、定时采集 AD 数据等等。如果把定时器与 GPIO 结合起来使用的话可以实现非常丰富的功能,可以测量输入信号的脉冲宽度,可以生产输出波形。定时器生产 PWM 控制电机状态是工业控制普遍方法,这方面知识非常有必要深入了解。下面对定时器的用途做个简单介绍:
STM32F4系列微控制器的定时器具有广泛的用途。以下是一些常见的用途:
1. 基本定时器(TIM6 和 TIM7)
- 简单计时:用于生成定时中断。
- DAC 触发:用于触发数模转换(DAC)。
2. 通用定时器(TIM2, TIM3, TIM4, TIM5)
- 输入捕获:捕获外部信号的时间戳,常用于测量脉冲宽度或频率。
- 输出比较:生成精确的PWM信号或用于定时事件的输出控制。
- PWM 生成:用于控制伺服电机、LED 调光等。
- 单脉冲模式:生成精确的单个脉冲信号。
- 编码器接口:用于连接旋转编码器,以测量旋转角度或速度。
3. 高级定时器(TIM1 和 TIM8)
- PWM 生成:生成多通道高精度的PWM信号。
- 死区控制:用于驱动电机的H桥,提供死区时间以避免短路。
- 复杂的捕获/比较功能:适用于高级运动控制应用。
- 三相互补PWM:用于控制三相电机。
具体用途示例
1. 延时和定时中断
利用定时器产生精确的时间延时和定时中断。例如,定时器可以设置为每隔1ms触发一次中断,用于实现操作系统的滴答时钟。
2. PWM 信号生成
PWM(脉宽调制)信号用于控制伺服电机、LED亮度等。通过调整PWM的占空比,可以精确控制电机的速度或LED的亮度。
3. 输入捕获
捕获外部信号的时间特征,如脉冲宽度和频率。比如,可以用来测量超声波传感器的回波时间,以确定距离。
4. 脉冲计数
用于计数外部脉冲信号,常用于旋转编码器以测量旋转角度或速度。
5. 单脉冲模式
生成精确的单个脉冲信号,可用于雷达系统中发送脉冲信号。
6. 编码器接口
与增量式旋转编码器接口,用于运动控制中精确测量位置和速度。
7. 触发ADC/DAC
定时器可以用来触发ADC(模数转换)和DAC(数模转换),实现定时采样和输出。
8. 定时器同步
多个定时器之间可以同步运行,适用于需要精确协调的控制系统。
9. 电机控制
通过生成复杂的PWM信号和控制死区时间,实现对三相电机的精确控制。
1.2 STM32F4的定时器资源
STM32F42xxx 系列控制器有 2 个高级定时器、10 个通用定时器和 2 个基本定时器, 还有 2 个看门狗定时器。看门狗定时器不在本次讨论范围。控制器上所有定时器都是彼此独立的,不共享任何资源。各个定时器特性参考表 如下所示。
1.3 定时器的理解
定时器 = 精准的时基 + 硬件电路。
- 分频模块(PSC):对外来的时钟进⾏分频。
- 计数模块(CNT):对来⾃分频模块输出的时钟脉冲进⾏计数。
- ⾃动重装载模块(ARR):它存储的是计数器的⽬标值,计数器每次累加就⽐较⼆者,⼆者相等计数器溢 出,当计数器溢出时,然后清零计数器寄存器。
二、基本定时器
2.1 基本定时器简介
STM32F407 有两个基本定时器 TIM6 和 TIM7,它们的功能完全相同,资源是完全独立的, 可以同时使用。其主要特性如下:16 位自动重载递增计数器,16 位可编程预分频器,预分频系数 1~65536,用于对计数器时钟频率进行分频。
- 可以发出⼀个触发信号,触发DAC进行数模转换的同步电路;
- 可以在事件更新时(计数器溢出),可以产生中断 和 DMA请求。
基本上定时器 TIM6 和 TIM7 是一个 16 位向上递增的定时器,当我在自动重载寄存器 (TIMx_ARR)添加一个计数值后并使能 TIMx,计数寄存器(TIMx_CNT)就会从 0 开始递增, 当 TIMx_CNT 的数值与 TIMx_ARR 值相同时就会生成事件并把 TIMx_CNT 寄存器清 0, 完成一次循环过程。如果没有停止定时器就循环执行上述过程。这些只是大概的流程,希望大家有个感性认识,下面会细讲整个过程。
2.2 基本定时器功能框图
STM32F407 基本定时器由时钟源、控制器、时基单元组成。下面先来学习基本定时器框图,通过学习基本定时器框图会有一个很好的整体掌握,同时 对之后的编程也会有一个清晰的思路。
首先先看最下面红色框内容,第一个是带有阴影的方框,方框内容一般是一个寄存器名称,比如图中主体部分的自动重载寄存器(TIMx_ARR)或 PSC 预分频器(TIMx_PSC), 这里要特别突出的是阴影这个标志的作用,它表示这个寄存器还自带有影子寄存器,在硬件结构上实际是有两个寄存器,源寄存器是我们可以进行读写操作,而影子寄存器我们是完全无法操作的,有内部硬件使用。影子寄存器是在程序运行时真正起到作用的,源寄存器只是给我们读写用的,只有在特定时候(特定事件发生时)才把源寄存器的值拷贝给它的影子寄存器。多个影子寄存器一起使用可以到达同步更新多个寄存器内容的目的。
接下来是一个指向右下角的图标,它表示一个事件,而一个指向右上角的图标表示中断和 DMA 输出。这个我们把它放在图中主体更好理解。图中的自动重载寄存器有影子寄存器,它左边有一个带有“U”字母的事件图标,表示在更新事件生成时就把自动重载寄存器内容拷贝到影子寄存器内,这个与上面分析是一致。寄存器右边的事件图标、中断和 DMA 输出图标表示在自动重载寄存器值与计数器寄存器值相等时生成事件、中断和 DMA 输出。
2.2.1 时钟源
定时器的核心就是计算器,要实现计数功能,首先要给它一个时钟源。基本定时器时钟挂载在 APB1 总线,所以它的时钟来自于 APB1 总线,但是基本定时器时钟不是直接由 APB1 总线直接提供,而是先经过一个倍频器。当 APB1 的预分频器系数为 1 时,这个倍频器系数为 1, 即定时器的时钟频率等于 APB1 总线时钟频率;当 APB1 的预分频器系数≥2 分频时,这个倍 频器系数就为 2 , 即定 时 器 的 时 钟 频 率 等于 APB1 总 线 时 钟 频率 的 两 倍 。回忆我们前面讲的时钟树简图如下所示:
我们知道APB1的分频系数为4,≥2,所以,倍频系数为2,基本定时器的时钟源为 42 x2=84MHZ。
【注解】:
1、为什么要在APB1总线和定时器之间加⼀个倍频器?
首先,我们的时钟是⼀个外设且挂载 在APB1总线上,但是我们总线上又挂载着其他外设。其他外设⼯作可能不要很⾼的时钟频率,所以设计 这个倍频器就可以在保证其他外设使⽤较低时钟频率时,定时器仍可以获得较⾼的时钟频率。
2、 定时器时钟源的选择
基本定时器时钟只能来自内部时钟,当 TIM6 和 TIM7 控制寄存器 1(TIMx_CR1)的 CEN 位 置 1 时,启动基本定时器,并且预分频器的时钟来源就是 CK_INT。高级控制定时器和通用定时器还可以选择外部时钟源或者直接来自其他定时器等待模式。我们可以通过 RCC 专用时钟配置寄存器(RCC_DCKCFGR)的 TIMPRE 位设置所有定时器的时钟频率, 我们一般设置该位为默认值 0,即 TIMxCLK 为总线时钟的两倍,使得表中可选的最大定时器时钟为 84MHz,即基本定时器的内部时钟(CK_INT)频率为 84MHz。
2.2.2 控制器
定时器控制器控制实现定时器功能,控制定时器复位、使能、计数是其基础功能,基本定时器还专门用于 DAC 转换触发。
2.2.3 时基单元(重点)
时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器 (TIMx_ARR) 。如下图所示:
基本定时器的这三个寄存器都是 16 位有效数字,即可设置值范围是 0~65535。 时基单元中的预分频器 PSC,它有一个输入和一个输出。输入 CK_PSC 来源于控制器部分, 实际上就是来自于内部时钟(CK_INT),即 2 倍的 APB1 总线时钟频率(84MHz)。输出 CK_CNT 是分频后的时钟,它是计数器实际的计数时钟,在不同应用场所,经常需要不同的定时频率,通 过设置预分频器 PSC 的值可以非常方便得到不同的 CK_CNT,计算公式如下:
上式中,PSC[15:0]是写入预分频器寄存器(TIMx_PSC)的值。另外:预分频器寄存器(TIMx_PSC)可以在运行过程中修改它的数值,新的预分频数值将在下一个更新事件时起作用。因为更新事件发生时,会把 TIMx_PSC 寄存器值更新到其影子寄存器中,这才会起作用。
1、什么是影子寄存器?
从框图上看,可以看到图中的预分频器PSC后面有一个影子, 自动重载寄存器也有个影子,这就表示这些寄存器有影子寄存器。影子寄存器是一个实际起作用的寄存器,不可直接访问。举个例子:我们可以把预分频系数写入预分频器寄存器(TIMx_PSC), 但是预分频器寄存器只是起到缓存数据的作用,只有等到更新事件发生时,预分频器寄存器的值才会被自动写入其影子寄存器中,这时才真正起作用。 自动重载寄存器及其影子寄存器的作用和上述同理。不同点在于自动重载寄存器是否具有缓冲作用还受到 ARPE 位的控制,当该位置 0 时,ARR 寄存器不进行缓冲,我们写入新的 ARR 值时,该值会马上被写入 ARR 影子寄存器中,从而直接生效;当该位置 1 时,ARR 寄存器进 行缓冲,我们写入新的 ARR 值时,该值不会马上被写入 ARR 影子寄存器中,而是要等到更新 事件发生才会被写入 ARR 影子寄存器,这时才生效。预分频器寄存器则没有这样相关的控制位,这就是它们不同点。
2、更新事件的产生有两种情况
一是由软件产生,将 TIMx_EGR 寄存器的位 UG 置 1,产生更新事件后,硬件会自动将 UG 位清零。二是由硬件产生,满足以下条件即可: 计数器的值等于自动重装载寄存器影子寄存器的值。下面来讨论一下硬件更新事件。
基本定时器的计数器(CNT)是一个递增的计数器,当寄存器(TIMx_CR1)的 CEN 位置 1,即使能定时器,每来一个 CK_CNT 脉冲,TIMx_CNT 的值就会递增加 1。当 TIMx_CNT 值 与 TIMx_ARR 的设定值相等时,TIMx_CNT 的值就会被自动清零并且会生成更新事件(如果开启相应的功能,就会产生 DMA 请求、产生中断信号或者触发 DAC 同步电路),然后下一个 CK_CNT 脉冲到来,TIMx_CNT 的值就会递增加 1,如此循环。在此过程中,TIMx_CNT 等于 TIMx_ARR 时,我们称之为定时器溢出,因为是递增计数,故而又称为定时器上溢。定时器溢出就伴随着更新事件的发生。
由上述可知,我们只要设置预分频寄存器和自动重载寄存器的值就可以控制定时器更新事 件发生的时间。自动重载寄存器(TIMx_ARR)是用于存放一个与计数器作比较的值,当计数器的值等于自动重载寄存器的值时就会生成更新事件,硬件自动置位相关更新事件的标志位,如: 更新中断标志位,此时只要判断这个中断标志位,就会去执行中断服务函数,处理相应的事件!下图是将预分频器 PSC 的值从 1 改为 4 时计数器时钟变化过程:通过这个例子理解一下分频:原来是 1 分频, CK_PSC 和 CK_CNT 频率相同。向 TIMx_PSC 寄存器写入新值时,并不会马上更新 CK_CNT 输出频率,而是等到更新事件发生时,把 TIMx_PSC 寄存器值更新到影子寄存器中,使其真正产生效果。更新为 4 分频后,在 CK_PSC 连续出现 4 个脉冲后 CK_CNT 才产 生一个脉冲。
2.3 基本定时器的寄存器
下面介绍 TIM6/TIM7 的几个重要的寄存器,具体如下:
2.3.1 控制寄存器 1(TIMx_CR1)
该寄存器,我们需要注意的是:位 0(CEN)用于使能或者禁止计数器,该位置 1 计数器开始工作,置 0 则停止。还有位 7(APRE)用于控制自动重载寄存器 ARR 是否具有缓冲作用, 如果 ARPE 位置 1,ARR 起缓冲作用,即只有在更新事件发生时才会把 ARR 的值写入其影子 寄存器里;如果 ARPE 位置 0,那么修改自动重载寄存器的值时,该值会马上被写入其影子寄 存器中,从而立即生效。
2.3.2 DMA/中断使能寄存器(TIMx_DIER)
该寄存器位 0(UIE)用于使能或者禁止更新中断,因为本实验我们用到中断,所以该位需 要置 1。位 8(UDE)用于使能或者禁止更新 DMA 请求,我们暂且用不到,置 0 即可。
2.3.3 状态寄存器(TIMx_SR)
该寄存器位 0(UIF)是中断更新的标志位,当发生中断时由硬件置 1,然后就会执行中断 服务函数,需要软件去清零,所以我们必须在中断服务函数里把该位清零。如果中断到来后, 不把该位清零,那么系统就会一直进入中断服务函数,这显然不是我们想要的。
2.3.4 计数器寄存器(TIMx_CNT)
该寄存器位[15:0]就是计数器的实时的计数值。
2.4.5 预分频寄存器(TIMx_PSC)
该寄存器是 TIM6/TIM7 的预分频寄存器,比如我们要 8400 分频,就往该寄存器写入 8399。 注意这是 16 位的寄存器,写入的数值范围是 0 到 65535,分频系数范围:1 到 65536。
2.4.6 自动重载寄存器(TIMx_ARR)
该寄存器可以由 APRE 位设置是否进行缓冲。计数器的值会和 ARR 寄存器影子寄存器进 行比较,当两者相等,定时器就会溢出,从而发生更新事件,如果打开更新中断,还会发生更 新中断。
2.5 定时器周期的计算(定时时间计算)
经过上面分析,我们知道定时事件生成时间主要由 TIMx_PSC 和 TIMx_ARR 两个寄存器值决定,这个也就是定时器的周期。比如我们需要一个 1s 周期的定时器,具体这两个寄 存器值该如何设置呢?
比如我们需要一个 500ms 周期的定时器更新中断?
一般思路是先设置预分频寄存器,然后才是自动重载寄存器。考虑到我们设置的 CK_INT 为 84MHz,我们把预分频系数设置为 8400(方便计算),即写入预分频寄存器的值为 8399,那么 fCK_CNT=84MHz/8400=10KHz。 这样就得到计数器的计数频率为 10KHz,即计数器 1 秒钟可以计 10000 个数(1毫秒可以计10个数)。我们需要 500ms 的中断周期,所以我们让计数器计数 5000 个数就能满足要求,即需要设置自动重载寄存器的值 为 4999,另外还要把定时器更新中断使能位 UIE 置 1,CEN 位也要置 1。
注意:
这里我们设定定时器自动重装载值为 5000-1,预分频系数为 8400-1,这里减 1 是因为定时器预分频器内部会自动加 1, 所以如果要进行 8400 分频的话,就传递 8399,而重装载值是从 0 开始计数的, 所以累积 5000 的话最终传递的参数是 5000-1。
2.6 定时器初始化结构体详解
标准库函数对定时器外设建立了四个初始化结构体,基本定时器只用到其中一个即 TIM_TimeBaseInitTypeDef,该结构体成员用于设置定时器基本工作参数,并由定时器基本初始化配置函数 TIM_TimeBaseInit 调用,这些设定参数将会设置定时器相应的寄存器,达到配置定时器工作环境的目的。这一节我们只介绍 TIM_TimeBaseInitTypeDef 结构体,其他结构体将在相关章节介绍。 初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个成员意义基本上就可以对该外设运用自如了。初始化结构体定义在 stm32f4xx_tim.h 文件中, 初始化库函数定义在 stm32f4xx_tim.c 文件中,编程时我们可以结合这两个文件内注释使用。
- TIM_Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器时钟,它设定 TIMx_PSC 寄存器的值。可设置范围为 0 至 65535,实现 1 至 65536 分频。
- TIM_CounterMode:定时器计数方式,可是在为向上计数、向下计数以及三种中心对 齐模式。基本定时器只能是向上计数,即 TIMx_CNT 只能从 0 开始递增,并且无需初 始化。
- TIM_Period:定时器周期,实际就是设定自动重载寄存器的值,在事件生成时更新到 影子寄存器。可设置范围为 0 至 65535。
- TIM_ClockDivision:时钟分频,设置定时器时钟 CK_INT 频率与数字滤波器采样时钟 频率分频比,基本定时器没有此功能,不用设置。
- TIM_RepetitionCounter:重复计数器,属于高级控制寄存器专用寄存器位,利用它可 以非常容易控制输出 PWM 的个数。这里不用设置。
虽然定时器基本初始化结构体有 5 个成员,但对于基本定时器 TIM6 和 TIM7 的使用只需初始化两个成员即可,想想使用基本定时器就是简单。
2.7 基本定时器中断
2.7.1 基本定时器中断应用
本实验,我们主要配置定时器产生周期性溢出,从而在定时器更新中断中做周期性的操作, 如周期性翻转 LED 灯。假设计数器计数模式为递增计数模式,那么实现周期性更新中断原理示 意图如下所示:
如图 20.1.3.1 所示,CNT 计数器从 0 开始计数,当 CNT 的值和 ARR 相等时(t1),产生一 个更新中断,然后 CNT 复位(清零),然后继续递增计数,依次循环。图中的 t1、t2、t3 就是定时器更新中断产生的时刻。 通过修改 ARR 的值,可以改变定时时间。另外,通过修改 PSC 的值,使用不同的计数频 率(改变图中 CNT 的斜率),也可以改变定时时间。
2.7.2 基本定时器中断配置步骤
接下来我们介绍下如何使用库函数对通用定时器进行配置。这个也是在编写 程序中必须要了解的。具体步骤如下
第一步:开启定时器时钟;
第二步:初始化定时器参数,设置自动重装值,分频系数,计数方式等;
第三步:使能定时器更新中断;
第四步:配置定时器中断优先级;
第五步:开启定时器计数;
第六步:编写中断服务函数;
三、使用基本定时器实现定时3秒,触发中断。
实现功能:我们使用基本定时器循环定时500毫秒并使能定时器中断,每到500毫秒 就在定时器中断服务函数翻转 RGB 彩灯,使得最终效果LED灯暗 0.5s,亮 0.5s,如此循环。
myled.h文件内容
#ifndef __MYLED_H
#define __MYLED_Hvoid LED_Init(void);#endif
myled.c内容
#include "stm32f4xx.h" // Device header
#include "myled.h"/*开时钟 打开外设对应的时钟(查看参考手册,该外设挂在哪个数据总线上),对应GPIO在哪条总线开哪条GPIOF外设 挂在AHB1总线上,所以要打开AHB1的时钟,双击函数,右键->go to definition*/void LED_Init(void)
{//第一步:使能GPIOF的时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能 GPIOF 时钟//第二步:GPIOF9,F10 初始化设置GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 ;//LED1对应 IO 口GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHzGPIO_Init(GPIOF, &GPIO_InitStructure);//初始化 GPIO//第三步:设置灯的初始状态GPIO_SetBits(GPIOF,GPIO_Pin_10 );//GPIOF10,设置高电平,灯灭
}
mytim.h内容
#ifndef __MY_TIM_H__
#define __MY_TIM_H__void TIM_Mode_Config(void);#endif
mytim.c内容
#include "stm32f4xx.h" // Device header
#include "mytim.h"/********定时器中断配置过程*********第一步:开启定时器时钟第二步:初始化定时器参数,设置自动重装值,分频系数,计数方式等第三步:使能定时器更新中断第四步:配置定时器中断优先级第五步:开启定时器计数第六步:编写中断服务函数************************************/void TIM_Mode_Config(void)
{//第一步:TIM时钟使能RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE); //第二步:初始化定时器参数,设置自动重装值,分频系数,计数方式等TIM_TimeBaseInitTypeDef Struct;Struct.TIM_Period = 5000-1; //自动重载寄存器ARR,定时500毫秒Struct.TIM_Prescaler = 8400-1; //预分频器PSCTIM_TimeBaseInit(TIM6,&Struct); //初始化时基单元//第三步:开启定时器更新中断TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE); //使能定时器中断 //第四步:配置定时器中断优先级,配置NVICNVIC_InitTypeDef Struct1;Struct1.NVIC_IRQChannel = TIM6_DAC_IRQn;Struct1.NVIC_IRQChannelCmd =ENABLE;Struct1.NVIC_IRQChannelPreemptionPriority = 0; //实验只有一个中断,对 NVIC 配置没什么具体要求。Struct1.NVIC_IRQChannelSubPriority = 1;NVIC_Init(&Struct1);//第五步:使能定时器TIM_Cmd(TIM6,ENABLE); //使能定时器}//第六步:编写中断服务函数
void TIM6_DAC_IRQHandler(void)
{if(TIM_GetITStatus(TIM6,TIM_IT_Update) == SET) //判断中断标志位{ GPIO_ToggleBits(GPIOF,GPIO_Pin_10 ); TIM_ClearITPendingBit(TIM6,TIM_IT_Update); //清除中断标志位}
}
在发生中断时,中断服务函数就得到运行。在服务函数内我们先调用定时器中断标志读取函数 TIM_GetITStatus 获取当 前定时器中断位状态,确定产生中断后才运行LED灯翻转动作,并使用定时器标志位清 除函数 TIM_ClearITPendingBit 清除中断标志位。
main.c内容
#include "stm32f4xx.h" // Device header
#include "mytim.h"
#include "myled.h"int main(void)
{LED_Init();/* 初始化基本定时器定时,3s 产生一次中断*/TIM_Mode_Config();while(1){}
}
将工程程序编译后下载到开发板内,可以看到 DS1 指示灯每隔 500ms 状态取反一次,实现 1 秒钟闪烁一次。
至此,我们的本次的学习就结束了。相信对基本定时器有了深入的理解,这一节我们就讲解到这里,希望能对大家的开发有帮助。 如有兴趣,感谢点赞、关注、收藏,若有不正地方,还请各位大佬多多指教!