本帖最后由 正点原子运营 于 2023-7-13 10:49 编辑
第二十一章 通用定时器实验
1)实验平台:正点原子探索者STM32F407开发板
2) 章节摘自【正点原子】STM32F407开发指南 V1.1
6)STM32技术交流QQ群:151941872
本章我们主要来学习通用定时器,通过上一章的学习,我们知道STM32F407有10个通用定时器(TIM2~TIM5和TIM9~TIM14)。这些定时器彼此完全独立,不共享任何资源。本章我们将通过四个实验来学习通用定时器的各个功能,分别是通用定时器中断实验、通用定时器PWM输出实验、通用定时器输入捕获实验和通用定时器脉、冲计数实验。 本章分为如下几个小节: 21.1 通用定时器简介 21.2 通用定时器中断实验 21.3 通用定时器PWM输出实验 21.4 通用定时器输入捕获实验 21.5 通用定时器脉冲计数实验
21.1 通用定时器简介STM32F407的通用定时器有4个,为了更好的区别各个定时器的特性,我们列了一个表格,如下所示: 注:该表参考数据手册 《STM32F407ZGT6.pdf》的2.2.20小节Table 3(第29页)。
由上表知道:该STM32芯片的计数器都是16位的。通用定时器和高级定时器其实也就是在基本定时器的基础上,添加了一些其他功能,如:输入捕获、输出比较、输出PWM和单脉冲模式等。而通用定时器数量较多,其特性也有一些的差异,但是基本原理都一样。
通用定时器框图 下面先来学习通用定时器框图,通过学习通用定时器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。 如上图,通用定时器的框图比基本定时器的框图复杂许多,为了方便介绍,我们将其分成六个部分讲解:
① 时钟源 通用定时器时钟可以选择下面四类时钟源之一: 1)内部时钟(CK_INT) 2)外部时钟模式1:外部输入引脚(TIx),x=1,2(即只能来自于通道1或者通道2) 3)外部时钟模式2:外部触发输入(ETR) 4)内部触发输入(ITRx):使用一个定时器作为另一定时器的预分频器 通用定时器时钟源的设置方法如下表所示:
内部时钟(CK_INT) STM32F4系列的定时器TIM2/TIM3/TIM4/TIM5/ TIM6/TIM7/ TIM12/ TIM13/ TIM14都是挂载在APB1总线上,这些定时器的内部时钟(CK_INT)实际上来自于APB1总线提供的时钟。但是这些定时器时钟不是由APB1总线直接提供,而是要先经过一个倍频器。在HAL库版本例程源码的sys.c文件中,系统时钟初始化函数sys_stm32_clock_init已经设置APB1总线时钟频率为42MHz,APB1预分频器的预分频系数为2,所以这些定时器时钟源频率为84MHz。因为当APB1预分频器的预分频系数≥2分频时,挂载在APB1总线上的定时器时钟频率是该总线时钟频率的两倍。这个和基本定时器一样,可回顾基本定时器这部分内容。
APB2总线上挂载的通用定时器TIM9/TIM10/TIM11,以及高级定时器TIM1和 TIM8,它们的情况是上面的描述是一样的,不同点是:定时器挂载的总线变成了APB2,在系统时钟初始化函数sys_stm32_clock_init已经设置APB2总线时钟频率为84MHz,预分频器的预分频系数为2,所以上述的定时器时钟源频率为168MHz。
外部时钟模式1(TI1、TI2) 外部时钟模式1这类时钟源,顾名思义时钟信号来自芯片外部。时钟源进入定时器的流程如下:外部时钟源信号àIOàTIMx_CH1(或者TIMx_CH2),这里需要注意的是:外部时钟模式1下,时钟源信号只能从CH1或者CH2输入到定时器,CH3和CH4都是不可以的。从IO到TIMx_CH1(或者TIMx_CH2),就需要我们配置IO的复用功能,才能使IO和定时器通道相连通。
时钟源信号来到定时器CH1或CH2后,需要经过什么“关卡”才能到达计数器作为计数的时钟频率的,下面通过外部时钟模式1框图给大家解答。 图21.1.2中是以CH2(通道2)为例的,时钟源信号到达CH2后,那么这里我们把这个时钟源信号用TI2表示,因为它只是个信号,来到定时器内部,那我们就按定时器内部的信号来命名,所谓入乡随俗。
TI2首先经过一个滤波器,由ICF[3:0]位来设置滤波方式,也可以设置不使用滤波器。
接着经过边沿检测器,由CC2P位来设置检测的边沿,可以上升沿或者下降沿检测。
然后经过触发输入选择器,由TS[4:0]位来选择TRGI(触发输入信号)的来源。可以看到图21.1.2中框出了TI1F_ED、TI1FP1和TI2FP2三个触发输入信号(TRGI)。TI1F_ED表示来自于CH1,并且没有经过边沿检测器过滤的信号,所以它是CH1的双边沿信号,即上升沿或者下降沿都是有效的。TI1FP1表示来自CH1并经过边沿检测器后的信号,可以是上升沿或者下降沿。TI2FP2表示来自CH2并经过边沿检测器后的信号,可以是上升沿或者下降沿。这里以CH2为例,那只能选择TI2FP2。如果是CH1为例,那就可以选择TI1F_ED或者TI1FP1。
最后经过从模式选择器,由ECE位和SMS[2:0]位来选择定时器的时钟源。这里我们介绍的是外部时钟模式1,所以ECE位置0,SMS[2:0] = 111即可。CK_PSC需要经过定时器的预分频器分频后,最终就能到达计数器进行计数了。
外部时钟模式2(ETR) 外部时钟模式2,顾名思义时钟信号来自芯片外部。时钟源进入定时器的流程如下:外部时钟源信号àIOàTIMx_ETR。从IO到TIMx_ETR,就需要我们配置IO的复用功能,才能使IO和定时器相连通。
时钟源信号来到定时器TIMx_ETR后,需要经过什么“关卡”才能到达计数器作为计数的时钟频率的,下面通过外部时钟模式2框图给大家解答。 图21.1.3中,可以看到在外部时钟模式2下,定时器时钟信号首先从ETR引脚进来。
接着经过外部触发极性选择器,由ETP位来设置上升沿有效还是下降沿有效,选择下降沿有效的话,信号会经过反相器。
然后经过外部触发预分频器,由ETPS[1:0]位来设置预分频系数,系数范围:1、2、4、8。
紧接着经过滤波器器,由ETF[3:0]位来设置滤波方式,也可以设置不使用滤波器。fDTS由TIMx_CR1寄存器的CKD位设置。
最后经过从模式选择器,由ECE位和SMS[2:0]位来选择定时器的时钟源。这里我们介绍的是外部时钟模式2,直接把ECE位置1即可。CK_PSC需要经过定时器的预分频器分频后,最终就能到达计数器进行计数了。
内部触发输入(ITRx) 内部触发输入是使用一个定时器作为另一个定时器的预分频器,即实现定时器的级联。下面以TIM1作为TIM2的预分频器为例,给大家介绍。 图21.1.4 TIM1作为TIM2的预分频器框图 上图中,TIM1作为TIM2的预分频器,需要完成的配置步骤如下:
1,TIM1_CR2寄存器的MMS[2:0]位设置为010,即TIM1的主模式选择为更新(选择更新事件作为触发输出(TRGO))。
2,TIM2_SMCR寄存器的TS[2:0]位设置为000,即使用ITR1作为内部触发。TS[2:0]位用于配置触发选择,除了ITR1,还有其他的选择,详细描述如下图所示: 上图中的触发选择中,我们在讲解外部时钟模式1的时候说过TI1F_ED、TI1FP1和TI2FP2,以及外部时钟模式2讲的ETRF,它们都是属于外部的,其余的都是内部触发了。那么这内部触发都代表什么意思呢?大家打开《STM32F4xx参考手册_V4(中文版).pdf》的428页,就可以找下面这个表。
在步骤2中,TS[2:0]位设置为000,使用ITR0作为内部触发,这个ITR0什么意思?由表21.1.3可以知道,当从模式定时器为TIM2时,ITR0表示主模式定时器就是TIM1。这里只是TIM2~5的内部触发连接情况,其他定时器请查看参考手册的相应章节。
3,TIM2_SMCR寄存器的SMS[2:0]位设置为111,即从模式控制器选择外部时钟模式1。
4,TIM1和TIM2的CEN位都要置1,即启动计数器。
定时器的时钟源这部分内容是非常重要的,因为这计数器工作的基础。虽然定时器有四类时钟源之多,但是我们最常用的还是内部时钟。
② 控制器 控制器包括:从模式控制器、编码器接口和触发控制器(TRGO)。从模式控制器可以控制计数器复位、启动、递增/递减、计数。编码器接口针对编码器计数。触发控制器用来提供触发信号给别的外设,比如为其它定时器提供时钟或者为DAC/ADC的触发转换提供信号。
③ 时基单元 时基单元包括:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR)。这部分内容和基本定时器基本一样的,大家可以参考基本定时器的介绍。
不同点是:通用定时器的计数模式有三种:递增计数模式、递减计数模式和中心对齐模式;TIM2 和TIM5的计数器是32位的。递增计数模式在讲解基本定时器的时候已经讲过了,那么对应到递减计数模式就很好理解了。就是来了一个计数脉冲,计数器就减1,直到计数器寄存器的值减到0,减到0时定时器溢出,由于是递减计数,故而称为定时器下溢,定时器溢出就会伴随着更新事件的发生。然后计数器又从自动重载寄存器影子寄存器的值开始继续递减计数,如此循环。最后是中心对齐模式,字面上不太好理解。该模式下,计数器先从0开始递增计数,直到计数器的值等于自动重载寄存器影子寄存器的值减1时,定时器上溢,同时生成更新事件,然后从自动重载寄存器影子寄存器的值开始递减计算,直到计数值等于1时,定时器下溢,同时生成更新事件,然后又从0开始递增计数,依此循环。每次定时器上溢或下溢都会生成更新事件。计数器的计数模式的设置请参考TIMx_CR1寄存器的位CMS和位DIR。
下面通过一张图给大家展示定时器工作在不同计数模式下,更新事件发生的情况。 上图中,纵轴表示计数器的计数值,横轴表示时间,ARR表示自动重载寄存器的值,小红点就是更新事件发生的时间点。举个例子,递增计数模式下,当计数值等于ARR时,计数器的值被复位为0,定时器溢出,并伴随着更新事件的发生,后面继续递增计数。递减计数模式和中心对齐模式请参考前面的描述。
上表的描述属于硬件更新事件发生条件,我们还可以通过UG位产生软件更新事件。
关于影子寄存器和定时器溢出时间计算公式等内容可以参考基本定时器的相关内容。
④ 输入捕获 图21.1.1.1中的第④部分是输入捕获,一般应用是要和第⑤部分一起完成测量功能。TIMx_CH1~ TIMx_CH4表示定时器的4个通道,这4个通道都是可以独立工作的。IO端口通过复用功能与这些通道相连。配置好IO端口的复用功能后,将需要测量的信号输入到相应的IO端口,输入捕获部分可以对输入的信号的上升沿,下降沿或者双边沿进行捕获,常见的测量有:测量输入信号的脉冲宽度、测量 PWM 输入信号的频率和占空比等。后续有相应的实验。
下面简单说一下测量高电平脉冲宽度的工作原理,方便大家的理解:一般先要设置输入捕获的边沿检测极性,如:我们设置上升沿检测,那么当检测到上升沿时,定时器会把计数器CNT的值锁存到相应的捕获/比较寄存器TIMx_CCRy里,y=1~4。然后我们再设置边沿检测为下降沿检测,当检测到下降沿时,定时器会把计数器CNT的值再次锁存到相应的捕获/比较寄存器TIMx_CCRy里。最后,我们将前后两次锁存的CNT的值相减,就可以算出高电平脉冲期间内计数器的计数个数,再根据定时器的计数频率就可以计算出这个高电平脉冲的时间。如果要测量的高电平脉宽时间长度超过定时器的溢出时间周期,就会发生溢出,这时候我们还需要做定时器溢出的额外处理。低电平脉冲捕获同理。
上面的描述是第④部分输入捕获整体上的一个应用情况,下面我们来看第④部分的细节。当需要测量的信号进入通道后,需要经过哪些“关卡”?我们用图21.1.7给大家讲解。 图21.1.7是图21.1.1第④部分通道1的“放大版”,这里是以通道1输入捕获为例进行介绍,其他通道同理。
待测量信号到达TIMx_CH1后,那么这里我们把这个待测量信号用TI1表示,原因在讲解外部时钟模式1的时候说过,所谓“入乡随俗”。
TI1首先经过一个滤波器,由ICF[3:0]位来设置滤波方式,也可以设置不使用滤波器。fDTS由TIMx_CR1寄存器的CKD位设置。
接着经过边沿检测器,由CC1P位来设置检测的边沿,可以上升沿或者下降沿检测。CC1NP是配置互补通道的边沿检测的,在高级定时器才有,通用定时器没有。
然后经过输入捕获映射选择器,由CC1S[1:0]位来选择把IC1映射到TI1、TI2还是TRC。这里我们的待测量信号从通道1进来,所以选择IC1映射到TI1上即可。
紧接着经过输入捕获1预分频器,由ICPS[1:0]位来设置预分频系数,范围:1、2、4、8。
最后需要把CC1E位置1,使能输入捕获,IC1PS就是分频后的捕获信号。这个信号将会到达图21.1.1的第⑤部分。
下面我们接着看图21.1.1的第⑤部分的“放大版”,如下图所示: 图21.1.8 捕获/比较通道1主电路(输入捕获功能部分) 图21.1.8中,灰色阴影部分是输出比较功能部分,讲到第⑥部分输出比较的时候再介绍。左边没有阴影部分就是输入捕获功能部分了。
首先看到捕获/比较预装载寄存器,我们以通道1为例,那么它就是CCR1寄存器,通道2、通道3、通道4就分别对应CCR2、CCR3、CCR4。在图21.1.1中就可以看到CCR1~4是有影子寄存器的,所以这里就可以看到图21.1.8中有捕获/比较影子寄存器,该寄存器不可直接访问。
图21.1.8左下角的CC1G位可以产生软件捕获事件,那么硬件捕获事件如何产生的?这里我们还是以通道1输入为例,CC1S[1:0] = 01,即IC1映射到TI1上;CC1E位置1,使能输入捕获;比如不滤波、不分频,ICF[3:0] = 00,ICPS[1:0] = 00;比如检测上升沿,CC1P位置0;接着就是等待测量信号的上升沿到来。当上升沿到来时,IC1PS信号就会触发输入捕获事件发生,计数器的值就会被锁存到捕获/比较影子寄存器里。当CCR1寄存器没有被进行读操作的时候,捕获/比较影子寄存器里的值就会锁存到CCR1寄存器中,那么程序员就可以读取CCR1寄存器,得到计数器的计数值。检测下降沿同理。
⑤ 输入捕获和输出比较公用部分 该部分需要结合第④部分或者第⑥部分共同完成相应功能。
⑥ 输出比较 图21.1.1.1中的第⑥部分是输出比较,一般应用是要和第⑤部分一起完成定时器输出功能。TIMx_CH1~ TIMx_CH4表示定时器的4个通道,这4个通道都是可以独立工作的。IO端口通过复用功能与这些通道相连。
下面我们按照输出信号产生过程顺序给大家介绍定时器如何实现输出功能的?首先看到第⑤部分的“放大版”图,如下图所示: 图21.1.9 捕获/比较通道1主电路(输出比较功能部分) 图21.1.9中,灰色阴影部分是输入捕获功能部分,前面已经讲过。这里我们看到右边没有阴影部分就是输出比较功能部分了。下面以通道1输出比较功能为例给大家介绍定时器如何实现输出功能的。
首先程序员写CCR1寄存器,即写入比较值。这个比较值需要转移到对应的捕获/比较影子寄存器后才会真正生效。什么条件下才能转移?图21.1.9中可以看到compare_transfer旁边的与门,需要满足三个条件:CCR1不在写入操作期间、CC1S[1:0] = 0配置为输出、OC1PE位置0(或者OC1PE位置1,并且需要发生更新事件,这个更新事件可以软件产生或者硬件产生)。
当CCR1寄存器的值转移到其影子寄存器后,新的值就会和计数器的值进行比较,它们的比较结果将会通过第⑥部分影响定时器的输出。
下面来看看第⑥部分通道1的“放大版”,如下图所示: 上图中,可以看到输出模式控制器,由OC1M[2:0]位配置输出比较模式,该位的描述请参考《STM32F4xx参考手册_V4(中文版).pdf》相关定时器章节的TIMx_CCMR1寄存器。F4系列有8种输出比较模式之多,后面用到再来介绍。
oc1ref是输出参考信号,高电平有效,为高电平时称之为有效电平,为低电平时称之为无效电平。它的高低电平受到三个方面的影响:OC1M[3:0]位配置的输出比较模式、第⑤部分比较器的比较结果、还有就是OC1CE位配置的ETRF信号。ETRF信号可以将oc1ref电平强制清零,该信号来自IO外部。
一般来说,当计数器的值和捕获/比较寄存器的值相等时,输出参考信号oc1ref的极性就会根据我们选择的输出比较模式而改变。如果开启了比较中断,还会发生比较中断。
CC1P位用于选择通道输出极性。 CC1E位置1使能通道输出。 OC1信号就会从TIMx_CH1输出到IO端口,再到IO外部。 下面分别通过四个实验来学习通用定时器的功能。
21.2 通用定时器中断实验本小节我们来学习使用通用定时器中断,以定时器3中断为例,首先来了解相关的寄存器。
21.2.1 TIM2~TIM5寄存器下面介绍TIM2/TIM3/TIM4/TIM5的几个与定时器中断相关且重要的寄存器,其他通用定时器的寄存器可能会有一些差异,相关内容可以参考《STM32F4xx参考手册_V4(中文版).pdf》定时器相关章节。
l 控制寄存器 1(TIMx_CR1) TIM2/TIM3/TIM4/TIM5的控制寄存器1描述如图21.2.1.1所示: 上图中我们只列出了本实验需要用的一些位,其中:位7(ARPE)用于控制自动重载寄存器是否进行缓冲,如果ARPE位置1,ARR起缓冲作用,即只有在更新事件发生时才会把ARR的值写入其影子寄存器里;如果ARPE位置0,那么修改自动重载寄存器的值时,该值会马上被写入其影子寄存器中,从而立即生效。
CMS[1:0]位,用于设置边沿对齐模式还是中心对齐模式,本实验我们使用边沿对齐模式,所以设置为00即可。
DIR位,用于控制定时器的计数方向,我们使用递增计数模式,所以设置DIR位为0。
CEN位,用于使能计数器的工作,必须要设置该位为1,计数器才会开始计数。
l 从模式控制寄存器(TIMx_SMCR) TIM2/TIM3/TIM4/TIM5的从模式控制寄存器描述如图21.2.1.2所示: 该寄存器的SMS[2:0]位,用于从模式选择,其实就是选择计数器输入时钟的来源。比如通用定时器中断实验我们设置SMS[2:0]=000,禁止从模式,这样PSC预分频器的时钟就直接来自内部时钟(CK_INT),按照我们例程sys_stm32_clock_init函数的配置,频率为84Mhz(APB1总线时钟频率的2倍)。
l DMA/中断使能寄存器(TIMx_DIER) TIM2/TIM3/TIM4/TIM5的DMA/中断使能寄存器描述如图21.2.1.3所示: 该寄存器用于使能/失能触发DMA请求、捕获/比较中断以及更新中断。本实验只用到更新中断,所以把位0(UIE)置1即可。
l 状态寄存器(TIMx_SR) TIM2/TIM3/TIM4/TIM5的状态寄存器描述如图21.2.1.4所示: 该寄存器都是一些中断标志位,比如更新中断标志位、捕获/比较中断标志位等。在通用定时器中断实验我们用到更新中断标志位,当定时器更新中断到来后,位0(UIF)会由硬件置1,我们需要在中断服务函数里面把该位清零。
l 计数寄存器(TIMx_CNT) TIM2~TIM5的计数器寄存器描述如图21.2.1.5所示: TIM2/TIM5的计数寄存器是32位的,TIM3/TIM4的计数寄存器都是16位有效的,计数模式可以是递增计数模式、递减计数模式和中心对齐计数模式。其他定时器和基本定时器一样,可以直接写该寄存器设置计数的初始值,也可以读取该寄存器获取计数器值。
l 预分频寄存器(TIMx_PSC) TIM2/TIM3/TIM4/TIM5的预分频器寄存器描述如图21.2.1.6所示。 定时器的预分频寄存器都是16位的,即写入该寄存器的数值范围是0到65535,表示1到65536分频。比如我们要8400分频,就往该寄存器写入8399。
l 自动重载寄存器(TIMx_ARR) TIM3/TIM4的自动重载寄存器描述如图21.2.1.7所示。 在F4系列中,TIM2和TIM5的自动重装载寄存器是32位的,其他通用定时器自动重载寄存器是低16位有效。该寄存器可以由APRE位设置是否进行缓冲。计数器的值会和自动重装寄存器影子寄存器进行比较,当两者相等,定时器就会溢出,从而发生更新事件,如果打开了更新中断,还会发生更新中断。
21.2.2 硬件设计1. 例程功能 LED0用来指示程序正在运行,200ms翻转一次。LED1在定时器中断中翻转,500ms进入中断一次。
2. 硬件资源 1)LED灯 LED0 – PF9 LED1 – PF10 2)定时器3
3. 原理图 定时器属于STM32F407的内部资源,只需要软件设置好即可正常工作。我们通过LED1来指示STM32F407的定时器进入中断的频率,LED0则指示程序的运行状态。
21.2.3 程序设计本实验的相关HAL库驱动以及实验配置步骤请参考基本定时器相关内容,基本一样。不同点是基本定时器只能是递增计数模式,通用定时器可以递增计数模式、递减计数模式和中心对齐模式。
21.2.3.1 程序流程图 下面看看本实验的程序流程图,main函数中并没有对LED1的操作,我们把对LED1的操作放到定时器的中断中进行处理:
图21.2.3.1.1 通用定时器中断实验程序流程图 21.2.3.2 程序解析 这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。通用定时器驱动源码包括两个文件:gtim.c和gtim.h。
首先看gtim.h头文件的几个宏定义: - /* TIMX 中断定义
- * 默认是针对TIM2~TIM5
- * 注意: 通过修改这4个宏定义,可以支持TIM1~TIM8任意一个定时器.
- */
- #define GTIM_TIMX_INT TIM3
- #defineGTIM_TIMX_INT_IRQn TIM3_IRQn
- #defineGTIM_TIMX_INT_IRQHandler TIM3_IRQHandler
- /* TIM3 时钟使能 */
- #defineGTIM_TIMX_INT_CLK_ENABLE() do{__HAL_RCC_TIM3_CLK_ENABLE(); }while(0)
复制代码通过修改这4个宏定义,可以支持TIM1~TIM8任意一个定时器。
下面再来看一下gtim.c文件的代码,主要包括两个函数,先来看看通用定时器的初始化函数,其定义如下: - /**
- * @brief 通用定时器TIMX定时中断初始化函数
- * @note
- * 通用定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
- * 通用定时器的时钟为APB1时钟的2倍, 而APB1为42M, 所以定时器时钟 = 84Mhz
- * 定时器溢出时间计算方法: Tout = ((arr +1) * (psc + 1)) / Ft us.
- * Ft=定时器工作频率,单位:Mhz
- *
- * @param arr: 自动重装值。
- * @param psc: 时钟预分频数
- * @retval 无
- */
- void gtim_timx_int_init(uint16_t arr, uint16_t psc)
- {
- GTIM_TIMX_INT_CLK_ENABLE(); /* 使能TIMx时钟 */
-
- g_timx_handle.Instance = GTIM_TIMX_INT; /* 通用定时器x */
- g_timx_handle.Init.Prescaler = psc; /* 预分频系数 */
- g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 */
- g_timx_handle.Init.Period = arr; /* 自动装载值 */
- HAL_TIM_Base_Init(&g_timx_handle);
- /* 设置中断优先级,抢占优先级1,子优先级3 */
- HAL_NVIC_SetPriority(GTIM_TIMX_INT_IRQn, 1, 3);
- HAL_NVIC_EnableIRQ(GTIM_TIMX_INT_IRQn); /* 开启ITMx中断 */
- HAL_TIM_Base_Start_IT(&g_timx_handle); /* 使能定时器x和定时器x更新中断 */
- }
复制代码这里配置的参数和基本定时器中断实验的是一样的,只是这里没有使用到HAL库的HAL_TIM_Base_MspInit函数来存放NVIC和使能时钟的代码,而是全部存放到gtim_timx_int_init函数里。在一个项目中,用到多个定时器时,建议大家使用这种方式来处理代码,这样方便代码的管理。
下面再来看看定时器中断服务函数,其定义如下: - /**
- * @brief 定时器中断服务函数
- * @param 无
- * @retval 无
- */
- voidGTIM_TIMX_INT_IRQHandler(void)
- {
- /* 以下代码没有使用定时器HAL库共用处理函数来处理,而是直接通过判断中断标志位的方式 */
- if(__HAL_TIM_GET_FLAG(&g_timx_handle, TIM_FLAG_UPDATE) != RESET)
- {
- LED1_TOGGLE();
- /* 清除定时器溢出中断标志位 */
- __HAL_TIM_CLEAR_IT(&g_timx_handle, TIM_IT_UPDATE);
- }
- }
复制代码可以看到,这里我们没有使用HAL库的定时器公共处理函数来处理中断部分的代码,而是通过自行判断中断标志位的方式来处理。只不过获取标志位的方式还是使用HAL库的函数宏__HAL_TIM_GET_FLAG(),大家也可以直接使用寄存器的方式来操作。
通过__HAL_TIM_GET_FLAG()获取中断标志位并判断是否了中断,然后处理中断程序,最后通过__HAL_TIM_CLEAR_IT()将中断标志位清零,这样就完成了一次对中断的处理。这样的方式来处理中断,也是大家学习HAL库需要掌握的。在一个项目中,用到多个定时器相关中断时,建议大家使用这种方式来处理代码,这样方便代码的管理。
21.2.4 下载验证下载代码后,可以看到LED0不停闪烁(每400ms一个周期),而LED1也是不停的闪烁,但是闪烁时间较LED0慢(每1s一个周期)。
21.3 通用定时器PWM输出实验本小节我们来学习使用通用定时器的PWM输出模式。
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。我们可以让定时器产生PWM,在计数器频率固定时,PWM频率或者周期由自动重载寄存器(TIMx_ARR)的值决定,其占空比由捕获/比较寄存器(TIMx_CCRx)的值决定。PWM产生原理示意图如下图所示: 上图中,定时器工作在递增计数模式,纵轴是计数器的计数值CNT,横轴表示时。当CNT<CCRx时,IO输出低电平(逻辑0);当CNT>=CCRx时,IO输出高电平(逻辑1);当CNT=ARR时,定时器溢出,CNT的值被清零,然后继续递增,依次循环。在这个循环中,改变CCRx的值,就可以改变PWM的占空比,改变ARR的值,就可以改变PWM的频率,这就是PWM输出的原理。
定时器产生PWM的方式有许多种,下面我们以边沿对齐模式(即递增计数模式/递减计数模式)为例,PWM模式1或者PWM模式2产生PWM的示意图,如下图所示: STM32F407的定时器除了TIM6和TIM7,其他的定时器都可以用来产生PWM输出。其中高级定时器TIM1和TIM8可以同时产生多达7路的PWM输出。而通用定时器也能同时产生多达4路的PWM输出!本实验我们以使用TIM3的CH2产生一路PWM输出为例进行学习。
21.3.1 TIM10/TIM11/TIM13/TIM14寄存器要使STM32F407的通用定时器TIMx产生PWM输出,除了上一小节介绍的寄存器外,我们还会用到3个寄存器,来控制PWM。这三个寄存器分别是:捕获/比较模式寄存器(TIMx_CCMR1/2)、捕获/比较使能寄存器(TIMx_CCER)、捕获/比较寄存器(TIMx_CCR1~4)。接下来我们简单介绍一下这三个寄存器。
l 捕获/比较模式寄存器1(TIMx_CCMR1) TIM10/TIM11/TIM13/TIM14的捕获/比较模式寄存器,该寄存器只有1个:TIMx_CCMR1,TIMx_CCMR1控制CH1和CH2。TIMx_CCMR1寄存器描述如图21.3.1.1所示: 该寄存器的有些位在不同模式下,功能不一样,我们现在只用到输出比较,输入捕获后面的实验再讲解。关于该寄存器的详细说明,请参考《STM32F4xx参考手册_V4(中文版).pdf》第476页,16.4.7节。比如我们要让TIM14的CH1输出PWM波为例进行介绍,该寄存器的模式设置位OC1M[2:0]就是对应着通道1的模式设置,此部分由3位组成。总共可以配置成8种模式,我们使用的是PWM模式,所以这3位必须设置为110或者111,分别对应PWM模式1和PWM模式2。这两种PWM模式的区别就是输出有效电平的极性相反。
l 捕获/比较使能寄存器(TIMx_CCER) TIM10/TIM11/TIM13/TIM14的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图21.3.1.2所示: 该寄存器比较简单,要让TIM14的CH1输出PWM波,这里我们要使能CC1E位,该位是通道1输入/输出使能位,要想PWM从IO口输出,这个位必须设置为1。CC1P位是设置通道1的输出极性,我们默认设置0。
l 捕获/比较寄存器1(TIMx_CCR1) 捕获/比较寄存器(TIMx_CCR1),该寄存器只有1个,对应通道CH1。我们使用的是通道1,所以来看看TIMx_CCR1寄存器,描述如图21.3.1.3所示: 在输出模式下,捕获/比较寄存器影子寄存器的值与CNT的值比较,根据比较结果产生相应动作,利用这点,我们通过修改这个寄存器的值,就可以控制PWM的占空比了。
21.3.2 硬件设计1. 例程功能 使用TIM14通道1(由PF9复用)输出PWM,PF9引脚连接了LED0,从而实现PWM输出控制LED0亮度。
2. 硬件资源 1)LED灯 LED0 – PF9 2)定时器14输出通道1(由PF9复用)
3. 原理图 定时器属于STM32F407的内部资源,只需要软件设置好即可正常工作。我们通过LED0来间接指示定时器的PWM输出情况。
21.3.3 程序设计21.3.3.1 定时器的HAL库驱动 定时器在HAL库中的驱动代码在前面介绍基本定时器已经介绍了部分,这里我们再介绍几个本实验用到的函数。
1.HAL_TIM_PWM_Init函数 定时器的PWM输出模式初始化函数,其声明如下: - HAL_StatusTypeDefHAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
复制代码l 函数描述: 用于初始化定时器的PWM输出模式。
l 函数形参: 形参1是TIM_HandleTypeDef结构体类型指针变量,基本定时器的时候已经介绍。
l 函数返回值: HAL_StatusTypeDef枚举类型的值。
l 注意事项: 该函数实现的功能以及使用方法和HAL_TIM_Base_Init类似,作用都是初始化定时器的ARR和PSC等参数。为什么HAL库要提供这个函数而不直接让我们使用HAL_TIM_Base_Init函数呢?这是因为HAL库为定时器的针对PWM输出定义了单独的MSP回调函数HAL_TIM_PWM_MspInit,所以当我们调用HAL_TIM_PWM_Init进行PWM初始化之后,该函数内部会调用MSP回调函数HAL_TIM_PWM_MspInit。当我们使用HAL_TIM_Base_Init初始化定时器参数的时候,它内部调用的回调函数是HAL_TIM_Base_MspInit,这里大家注意区分。
2. HAL_TIM_PWM_ConfigChannel函数 定时器的PWM通道设置初始化函数。其声明如下: - HAL_StatusTypeDefHAL_TIM_PWM_ConfigChannel(TIM_HandleTypeDef *htim,TIM_OC_InitTypeDef *sConfig, uint32_t Channel);
复制代码l 函数描述: 该函数用于设置定时器的PWM通道。
l 函数形参: 形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。 形参2是TIM_OC_InitTypeDef结构体类型指针变量,用于配置定时器的输出比较参数。
重点了解一下TIM_OC_InitTypeDef结构体指针类型,其定义如下: - typedef struct
- {
- uint32_t OCMode; /* 输出比较模式选择,寄存器的时候说过了,共8种模式 */
- uint32_t Pulse; /* 设置比较值 */
- uint32_t OCPolarity; /* 设置输出比较极性 */
- uint32_t OCNPolarity; /* 设置互补输出比较极性 */
- uint32_t OCFastMode; /* 使能或失能输出比较快速模式 */
- uint32_t OCIdleState; /* 选择空闲状态下的非工作状态(OC1 输出) */
- uint32_t OCNIdleState; /* 设置空闲状态下的非工作状态(OC1N 输出) */
- } TIM_OC_InitTypeDef;
复制代码我们重点关注前三个结构体成员。成员变量OCMode用来设置模式,这里我们设置为PWM模式1。成员变量Pulse用来设置捕获比较值。成员变量TIM_OCPolarity用来设置输出极性。其他成员TIM_OutputNState,TIM_OCNPolarity,TIM_OCIdleState和TIM_OCNIdleState后面用到再介绍。
形参3是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。比如定时器14只有2个通道,那选择范围就只有TIM_CHANNEL_1~2,所以要根据具体情况选择。
l 函数返回值: HAL_StatusTypeDef枚举类型的值。
3.HAL_TIM_PWM_Start函数 定时器的PWM输出启动函数,其声明如下: - HAL_StatusTypeDefHAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
复制代码l 函数描述: 用于使能通道输出和启动计数器,即启动PWM输出。
l 函数形参: 形参1是TIM_HandleTypeDef结构体类型指针变量。 形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
l 函数返回值: HAL_StatusTypeDef枚举类型的值。
l 注意事项: 对于单独使能定时器的方法,在上一章定时器实验我们已经讲解。实际上,HAL库也同样提供了单独使能定时器的输出通道函数,函数为: - void TIM_CCxChannelCmd(TIM_TypeDef *TIMx, uint32_t Channel,uint32_t ChannelState);
复制代码HAL_TIM_PWM_Start函数内部也调用了该函数。
4.HAL_TIM_ConfigClockSource函数 配置定时器时钟源函数,其声明如下: - HAL_StatusTypeDefHAL_TIM_ConfigClockSource(TIM_HandleTypeDef *htim, TIM_ClockConfigTypeDef*sClockSourceConfig);
复制代码l 函数描述: 用于配置定时器时钟源。
l 函数形参: 形参1是TIM_HandleTypeDef结构体类型指针变量。 形参2是TIM_ClockConfigTypeDef结构体类型指针变量,用于配置定时器时钟源参数。 TIM_ClockConfigTypeDef定义如下: - typedef struct
- {
- uint32_t ClockSource; /* 时钟源 */
- uint32_t ClockPolarity; /* 时钟极性 */
- uint32_t ClockPrescaler; /* 定时器预分频器*/
- uint32_t ClockFilter; /* 时钟过滤器 */
- }TIM_ClockConfigTypeDef;
复制代码l 函数返回值: HAL_StatusTypeDef枚举类型的值。
l 注意事项: 该函数主要配置TIMx_SMCR寄存器。默认情况下,定时器的时钟源是内部时钟。本实验就是使用内部时钟的,所以我们不用对时钟源就行初始化,默认即可。这里只是让大家知道有这个函数可以设定时器的时钟源。比如用HAL_TIM_ConfigClockSource初始化选择内部时钟,方法如下: - TIM_HandleTypeDeftimx_handle; /* 定时器x句柄 */
- TIM_ClockConfigTypeDefsClockSourceConfig = {0};
- sClockSourceConfig.ClockSource =TIM_CLOCKSOURCE_INTERNAL; /* 选择内部时钟*/
- HAL_TIM_ConfigClockSource(&timx_handle, &sClockSourceConfig);
复制代码后面的定时器初始化凡是用到内部时钟我们都没有去初始化,系统默认即可。
定时器PWM输出模式配置步骤1) 开启TIMx和通道输出的GPIO时钟,配置该IO口的复用功能输出 首先开启TIMx的时钟,然后配置GPIO为复用功能输出。本实验我们默认用到定时器14通道1,对应IO是PF9,它们的时钟开启方法如下: - __HAL_RCC_TIM14_CLK_ENABLE(); /* 使能定时器14 */
- __HAL_RCC_GPIOF_CLK_ENABLE(); /* 开启GPIOF时钟 */
复制代码IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2) 初始化TIMx,设置TIMx的ARR和PSC等参数 使用定时器的PWM输出功能时,通过HAL_TIM_PWM_Init函数初始化定时器ARR和PSC等参数。 注意:该函数会调用:HAL_TIM_PWM_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO初始化、中断使能以及优先级设置等代码。
3) 设置TIMx_CHy的PWM模式,输出比较极性,比较值等参数 在HAL库中,通过HAL_TIM_PWM_ConfigChannel函数来设置定时器为PWM1模式或者PWM2模式,根据需求设置输出比较的极性,设置比较值(控制占空比)等。
4) 使能TIMx,使能TIMx的CHy输出 在HAL库中,通过调用HAL_TIM_PWM_Start函数来使能TIMx的某个通道输出PWM。
5) 修改TIM14_CCR1来控制占空比 在经过以上设置之后,PWM其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改比较值来控制PWM的输出占空比。HAL库中提供一个修改占空比的宏定义: - __HAL_TIM_SET_COMPARE (__HANDLE__,__CHANNEL__, __COMPARE__)
复制代码__HANDLE__是TIM_HandleTypeDef结构体类型指针变量,__CHANNEL__对应PWM的输出通道,__COMPARE__则是要写到捕获/比较寄存器(TIMx_CCR1/2/3/4)的值。实际上该宏定义最终还是往对应的捕获/比较寄存器写入比较值来控制PWM波的占空比,如下解析: 比如我们要修改定时器14通道1的输出比较值(控制占空比),寄存器操作方法: - TIM14->CCR1 =ledrpwmval; /* ledrpwmval是比较值,并且动态变化的,
- 所以我们要周期性调用这条语句,已到达及时修改PWM的占空比 */
复制代码__HAL_TIM_SET_COMPARE这个宏定义函数最终也是调用这个寄存器操作的,所以说我们使用HAL库的函数其实就是间接操作寄存器的。
21.3.3.2 程序流程图 图21.3.3.2.1 通用定时器PWM输出实验程序流程图 21.3.3.3 程序解析 这里我们这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。通用定时器驱动源码包括两个文件:gtim.c和gtim.h。
首先看gtim.h头文件的几个宏定义: - /* TIMX PWM输出定义
- * 这里输出的PWM控制LED0(RED)的亮度
- * 默认是针对TIM2~TIM5
- * 注意: 通过修改这几个宏定义,可以支持TIM1~TIM8任意一个定时器,任意一个IO口输出PWM
- */
- #define GTIM_TIMX_PWM_CHY_GPIO_PORT GPIOF
- #define GTIM_TIMX_PWM_CHY_GPIO_PIN GPIO_PIN_9
- #define GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOB_CLK_ENABLE();\
- }while(0) /* PB口时钟使能 */
- /* 端口复用到TIM14 */
- #define GTIM_TIMX_PWM_CHY_GPIO_AF GPIO_AF9_TIM14
- /* TIMX REMAP设置
- * 因为我们LED0接在PF9上,必须通过开启TIM14的部分重映射功能,才能将TIM14_CH1输出到PF9上
- */
- #define GTIM_TIMX_PWM TIM14
- #define GTIM_TIMX_PWM_CHY TIM_CHANNEL_1 /* 通道Y, 1<= Y <=4 */
- #define GTIM_TIMX_PWM_CHY_CCRX TIM14->CCR1 /* 通道Y的输出比较寄存器 */
- #define GTIM_TIMX_PWM_CHY_CLK_ENABLE() do{ __HAL_RCC_TIM14_CLK_ENABLE();\
- }while(0) /* TIM14 时钟使能 */
复制代码可以把上面的宏定义分成三部分,第一部分是定时器14输出通道1对应的IO口的宏定义。第二部分是定时器14的部分重映射功能的宏定义,第三部分则是定时器14输出通道1的相应宏定义。
下面看gtim.c的程序,首先是通用定时器PWM输出初始化函数。 - /**
- * @brief 通用定时器TIMX 通道Y PWM输出 初始化函数(使用PWM模式1)
- * @note
- * 通用定时器的时钟来自APB1,当D2PPRE1≥2分频的时候
- * 通用定时器的时钟为APB1时钟的2倍, 而APB1为42M, 所以定时器时钟= 84Mhz
- * 定时器溢出时间计算方法: Tout = ((arr +1) * (psc + 1)) / Ft us.
- * Ft=定时器工作频率,单位:Mhz
- * @param arr: 自动重装值。
- * @param psc: 时钟预分频数
- * @retval 无
- */
- voidgtim_timx_pwm_chy_init(uint16_t arr,uint16_t psc)
- {
- g_timx_pwm_chy_handle.Instance = GTIM_TIMX_PWM; /* 定时器x */
- g_timx_pwm_chy_handle.Init.Prescaler = psc; /* 定时器分频 */
- g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 递增计数模式*/
- g_timx_pwm_chy_handle.Init.Period = arr; /* 自动重装载值*/
- HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle); /* 初始化PWM */
-
- g_timx_oc_pwm_chy_handle.OCMode = TIM_OCMODE_PWM1; /* 模式选择PWM1 */
- /* 设置比较值,此值用来确定占空比,默认比较值为自动重装载值的一半,即占空比为50% */
- g_timx_oc_pwm_chy_handle.Pulse = arr/2;
- g_timx_oc_pwm_chy_handle.OCPolarity = TIM_OCPOLARITY_LOW;/* 输出比较极性为低*/
- HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, &g_timx_oc_pwm_chy_handle,
- GTIM_TIMX_PWM_CHY); /* 配置TIMx通道y */
- HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY);/*开启PWM通道*/
- }
复制代码HAL_TIM_PWM_Init初始化TIM14并设置TIM14的ARR和PSC等参数,其次通过调用函数HAL_TIM_PWM_ConfigChannel设置TIM14_CH1的PWM模式以及比较值等参数,最后通过调用函数HAL_TIM_PWM_Start来使能TIM14以及使能PWM通道TIM14_CH1输出。
本实验我们使用PWM的MSP初始化回调函数HAL_TIM_PWM_MspInit来存放时钟、GPIO的初始化代码,其定义如下: - /**
- * @brief 定时器底层驱动,时钟使能,引脚配置
- * 此函数会被HAL_TIM_PWM_Init()调用
- * @param htim:定时器句柄
- * @retval 无
- */
- voidHAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
- {
- if (htim->Instance == GTIM_TIMX_PWM)
- {
- GPIO_InitTypeDef gpio_init_struct;
- GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE(); /* 开启通道y的CPIO时钟 */
- GTIM_TIMX_PWM_CHY_CLK_ENABLE();
- gpio_init_struct.Pin =GTIM_TIMX_PWM_CHY_GPIO_PIN; /* 通道y的CPIO口 */
- gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推完输出 */
- gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
- gpio_init_struct.Speed =GPIO_SPEED_FREQ_HIGH; /* 高速 */
- /* IO口REMAP设置, 是否必要查看头文件配置的说明! */
- gpio_init_struct.Alternate =GTIM_TIMX_PWM_CHY_GPIO_AF;
- HAL_GPIO_Init(GTIM_TIMX_PWM_CHY_GPIO_PORT, &gpio_init_struct);
- }
- }
复制代码该函数首先判断定时器寄存器基地址,符合条件后,开启对应的GPIO时钟和定时器时钟,并且初始化GPIO。上面是使用HAL库标准的做法,我们亦可把HAL_TIM_PWM_MspInit函数里面的代码直接放到gtim_timx_pwm_chy_init函数里。这样做的好处是当一个项目中用到多个定时器时,代码的移植性、可读性好,方便管理。
在main.c里面编写如下代码: - int main(void)
- {
- uint16_t ledrpwmval = 0;
- uint8_t dir = 1;
- HAL_Init(); /* 初始化HAL库 */
- sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟, 168Mhz */
- delay_init(168); /* 延时初始化 */
- usart_init(115200); /* 串口初始化为115200 */
- led_init(); /* 初始化LED */
- /* 84M/84=1M的计数频率,自动重装载为500,那么PWM频率为1M/500=2kHz */
- gtim_timx_pwm_chy_init(500 - 1, 84 - 1);
- while (1)
- {
- delay_ms(10);
- if (dir)ledrpwmval++; /* dir==1 ledrpwmval递增 */
- else ledrpwmval--; /* dir==0ledrpwmval递减 */
- if (ledrpwmval > 300)dir = 0; /* ledrpwmval到达300后,方向为递减*/
- if (ledrpwmval == 0)dir = 1; /* ledrpwmval递减到0后,方向改为递增*/
- /* 修改比较值控制占空比 */
- __HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY,
- ledrpwmval);
- }
- }
复制代码本小节开头我们就说PWM波频率由自动重载寄存器(TIMx_ARR)的值决定,其占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定。下面结合实际看看具体怎么计算: 定时器14的时钟源频率为2倍APB1总线时钟频率,即频率为84MHz,而调用gtim_timx_pwm_chy_init初始化函数之后,就相当于写入预分频寄存器的值为83,写入自动重载寄存器的值为499。基本定时器讲的定时器溢出公式由公式得: Tout= ((arr+1)*(psc+1))/Tclk= ((499+1)*(83+1))/84000000=0.0005s 再由频率是周期的倒数关系得到PWM的频率为2000Hz。
占空比怎么计算的呢?结合图21.3.1,我们分两种情况分析,输出比较极性为低和输出比较极性为高,它们的情况正好相反。因为在main函数中的比较值是动态变化的,不利于我们计算占空比,我们假设比较值固定为200,在本实验中可以调用如下语句得到。 __HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY, 200);
因为LED0是低电平有效,所以我们在gtim_timx_pwm_chy_init函数中设置了输出比较极性为低,那么当比较值固定为200时,占空比 = ((arr+1)– CCR1) / (arr+1) = (500-200)/500=60%。其中arr是写入自动重载寄存器(TIMx_ARR)的值,CCR1就是写入捕获/比较寄存器1(TIMx_CCR1)的值。这里我们还需要提醒一下,占空比是指在一个周期内,高电平时间相对于总时间所占的比例。
另外一种情况:设置了输出比较极性为高,那么当比较值固定为200时,占空比 = CCR1/ (arr+1) =200/500=40%。可以看到输出比较极性为低和输出比较极性为高的占空比正好反过来。
在这里,我们也用了DS100示波器进行验证,效果图如图21.3.3.3.1所示: 这里把输出比较极性低和输出比较极性高的PWM波形都显示出来了。本实验默认设置PWM模式1、输出比较极性低,当CCR1寄存器的值设置为200时,对应的PWM波形如上图黄色的波形图。如果把输出比较极性设置为高,对应的波形图就是绿色的波形图了。 大家感兴趣也可以自行用示波器进行验证。
21.3.4 下载验证下载代码后,此时定时器14通道1输出PWM信号到PF9口。可以看到LED0不停的由暗变到亮,然后又从亮变到暗。 |