|
前面的帖子写了点灯,也胡说了一通中断,这一次,咱们主要实验一下中断,具体用定时器的定时中断功能来控制LED定时闪烁。 这个实验里,主要用到了三样东西:端口,定时器,中断控制器。 端口和中断,我们前面已经讲了,在这里,咱们首先弄清楚单片机的定时器的工作原理以及如何操作它。 这事简单,拿出你的手机,一步一步跟我来做,看看手机的定时器是怎么操控的: 第一步,点击手机上正在显示的时间,看到屏幕下部的计时器没有?点开它; 第二步,拨动数字拨盘到你想要定时的计数值; 第三步,按一下启动三角按钮,启动计数器。 操作完成,然后你就看到计时数据在稳步倒退,然后你就可以不管它了,等退到0的时候,手机自然会发出计时结束的提示音,然后你就听到了这个声音,然后你要手动关掉这个提示音,然后你就应该去做先前计划的事了。 如果你要再次启用这个定时,就得手动再按一下启动按钮。 上面所提到的三步,就是对手机定时器的操作。 单片机的定时器的操作和工作过程,底子就这一套东西,一样的,初始化的配置的第一步,就是调出定时器——给定时器模块上电;第二步就是输入计数值。这两步就算是配置完成。第三步就是启动这个定时器让它开始计数,也就是让它开始工作了。 所以,让单片机的定时器工作起来,就是这么简单,是不是很出乎你的意料之外?但它就是这样的。 让定时器工作起来不是目的,目的是用它来定时提醒你时间到了该做某件事了,否则它就只能是个摆设。 前面提到手机定时器计数结束会发出提示音,单片机也用了这个机制,不过,它不是搞什么提示音,在芯片设计上,它是在相关寄存器中做一个标记位,这个标记位复位时为0,一旦定时器的计数结束,这个位就会被置位为1,也就是说,这个位,就是计数结束标志位。 那我们怎么知道这个位已经变成1了呢?用查询方法过一会来看一下过一会来看一下?本来就是为了准确计时,结果你把这个过一会加进来,那哪还有即时效果?这不误事么? 那就一直守着这个位等着它变成1?那你还做不做其它的事了?除非你没其它的事,否则别用这一套,比如说如果我们只有一件事只是闪个灯,这个也不是不可以用,哦对了,这个叫等候机制。 象人一样手脚在做事而耳朵在听着?对,就是这个思路,单片机就是这样的,CPU在做事的同时也没断了跟定时器的联系,这个联系机构就是中断控制器。定时器计数结束时,会置位计数结束标志位,中断控制器会一直盯着这个位,一旦这个位变成1,它就会放下手中的事来处理计数结束后约定的事,这个机制,就叫中断处理机制。至于究竟约定的是什么事,则全看你程序员在相应的中断函数里编的是些什么,你编什么它就执行什么,你不编的话,那它空转一下就出来了。 注意啊,在单片机中,这个中断处理机制是可以有针对性地关掉的,而且,单片机复位之后对于可屏蔽全局中断都是处于关闭状态的,而定时器的中断就属于可屏蔽中断,所以,我们还得打开这个中断。注意认真看哈,这个计数结束,在手册中叫做事件,CPU允许这个事件产生中断,就称为中断事件,在我的那个中断机制帖子中称为局部中断中的组成,这个计数结束,只是定时器中断事件之中的一种,这个事件在相应的寄存器中有专门的一位来控制其开关,但定时器还另外设计有一个专门位用于定时器全局中断允许的开关,所以,我们还得打开这个定时器的全局中断开关才行。定时器全局中断开关的寄存器的说明,在手册中断一章中;定时器局部中断也就是中断事件开关所在寄存器的说明,在手册该外设的章节之中。 前面说到计数重复的事,我们在这里既然是用于定时闪灯,那肯定得是不断重复啊,不断重复才能闪灯嘛。怎么重复?肯定不能让你来手动啊,所以,单片机的计数设计中还有一个计数结束自动重复开关。 还有一个问题,计数结束之后,计数器之中的数值已经归零了吧?那你从零开始再次启动么?那不等于没动么?所以,在设计中,这个计数初始值得找一个地方存着,然后计数归零后,定时器将会将这个计数值自动重装入定时器的计数器中。其实手机的计时器就是这样的,计时结束并关闭提示音之后,计时拨盘上的数值自动显示为上一次的值。 单片机定时器的初始化的项目,也就这么多,下面我们开始实验。 板子还是用的之前的CH32L103评估板,板上有两颗LED,其正极均已接至电源正极并串有1K电阻。这里我们让PB的低八位全可以用于闪灯,于是板载的LDE负极可以接在PB的低八位的任意一脚上。当然,你用CH32的其它芯片也是一样的做法,看手册就是了。 首先确定我们要用到的外设,这里就两个:定时器TIM2和输入输出端口PB。 一、先对定时器进行初始化,前面已经讲到了,它的初始化的项目如下: 1. 调出定时器,也就是给定时器这个外设上电; 2. 输入计数初始值; 3. 打开重复计数功能开关; 4. 打开自动重装计数初始值功能开关; 5. 配置定时器全局中断优先级,包括打断优先级和排队优先级; 6. 打开定时器计数结束中断使能开关; 7. 打开定时器全局中断使能开关; 8. 启动定时器。 下面我们就一个一个地讲解如何配置: 1. 给TIM2这个定时器上电 手册RCC章节里找相应的使能寄存器3.4.8 PB1 外设时钟使能寄存器(RCC_PB1PCENR),位 0 ,定时器 2 模块时钟使能位,1为模块时钟开启,0为模块时钟关闭。 于是给这这个外设供上电的语句就有了: RCC->PB1PCENR|=(1<<0);//开启TIM2的电源开关 2. 输入计数初始值 这个稍微有点麻烦,我们一是要知道在已定传输波特率的条件下,这个单片机是怎么计算计数初始值的;二是既然要重复计数,那这个初始值是放在哪个寄存器中。 这个就要细看手册了,15.2.1 概述中:通用定时器的时钟可以来自于 HB 总线时钟(CK_INT),可以来自外部时钟输入引脚(TIMx_ETR)......通用定时器的核心是一个 16 位计数器(CNT)。CK_PSC 经过预分频器(PSC)分频后,成为 CK_CNT 再最终输给 CNT,CNT支持增计数模式、减计数模式和增减计数模式,并有一个自动重装值寄存器(ATRLR)在每个计数周期结束后为 CNT 重装载初始化值。 这就很清楚了,这个初始值由两个数值决定,一个是预分频数,另一个才是用于计数器的初始计数值,这两个数据都要我们来决定;而相关的寄存器则有三个,一个是预分频值寄存存器,一个是计数器寄存器,第三个是自动重装值寄存器。看看,这三个寄存器,是不是都属于单片机运行参数存储寄存器? 这里我们的时钟来源自然选用HB总线的时钟,TIM2挂在桥1上,而桥1的总线时钟默认值为96MHz(可以查3.4.2 时钟配置寄存器 0(RCC_CFGR0以知道桥1的频率如何设置)。分频器是16位的,也就是说它最大可以分频65535,那我们这里分频数选多少呢?选9600吧,于是分频后计数器的工作频率就成了10KHz。如果我们的计数时间周期设定为1秒,那计数初始值自然就是10000了。 查手册找寄存器的英文名字,15.4.10 通用定时器的计数器(TIMx_CNT)(x=2/3) ,15.4.12 计数时钟预分频器(TIMx_PSC)(x=2/3/4),15.4.13 自动重装值寄存器(TIMx_ATRLR)(x=2/3)。 该输入的数值确定了,该用的寄存器名字也弄清楚了,现在就可以写出向这些寄存器灌数据的程序了,不过嘛,因为是自动重装,所以,计数器的数据灌不灌的,也都差不多,反正我是省了的: TIM2->PSC=(9600-1); TIM2->ATRLR=(10000-1); 为什么要减1?因为0也算,它是从0开始计算的。 3. 打开重复计数功能开关 手册15.4.1 控制寄存器 1(TIMx_CTLR1)(x=2/3/4) ,位3,单脉冲模式,为1在发生下一次更新事件(清除 CEN 位)时,计数器停止,为0在发生下一次更新事件时,计数器不停止。复位值0。 既然复位值为0,那这一条不写程序也能过。 4. 打开自动重装计数初始值功能开关 手册15.4.1 控制寄存器 1(TIMx_CTLR1)(x=2/3/4),位7 自动重装预装使能位,1为使能自动重装值寄存器(ATRLR),0为禁止自动重装值寄存器(ATRLR)。 写成程序: TIM2->CTLR1|=(1<<7); //使能自动重装值寄存器(ATRLR) 5. 配置定时器全局中断优先级,包括打断优先级和排队优先级 第 9 章 中断和事件(PFIC)开头说了,CH32L103最大嵌套深度2级,9.5.3.1 中断系统控制寄存器(INTSYSCR)中,位1 中断嵌套使能,0为中断嵌套功能关闭,1为中断嵌套功能使能。 按说我们若要设置2级中断嵌套的话,得操作这个寄存器来打开中断嵌套功能,但这个寄存器并不在此单片机的存储器序列之中也就是说他的地址并不在存储器4G地址之列,无法用一般的地址操作方法对其进行操作,它只能用汇编进行操作,好在CH32的启动文件中已打开这个嵌套功能,所以,有关对嵌套功能寄存的操作,我们就略过。当然,如果你会汇编的话,你照样可以操作它。 接下来就是排队优先级的设置,9.5.2.30 PFIC 中断优先级配置寄存器(PFIC_IPRIORx)(x=0-17) 控制器支持 256 个中断(0-255),每个中断使用 8bit 来设置控制优先级。 也就是说,因为寄存器为32位,所以每个寄存器要控制4个全局中断,PFIC_IPRIOR0控制0#~3#中断,依此类推。 查9.3 中断和异常的向量表,TIM2的中断号为44#,于是推算出控制TIM2中断优先级的段位在寄存器PFIC_IPRIOR11的[0:7]位。这里要注意的是,厂家的头文件里对这个寄存器的命名的序号是用下标的方式,从0开始编号,也就是它的名字为PFIC->IPRIOR[11]。 表中这八位用来控制优先级的方式是这样表述的: [7:5]:优先级控制位。 若配置无嵌套,无抢占位; 若配置 2 级嵌套,bit7 为抢占位; 优先级数值越小则优先级越高,同一抢占优先级中断若同时挂起,优先执行优先级高的中断。 [4:0]:保留,固定为 0,写无效。 前面说了,CH32的启动文件已经打开了中断嵌套,所以我们要设置打断优先级和排队优先级,这里我们设置打断优先级为1,排队优先级2,则这个八位字段就是11000000B,编程时偷个懒就只写高3位,反正低五位写无效,则优先级设置语句如下: PFIC->IPRIOR[13]&= ~(7 <<(5+0*8));//清零[7:5] PFIC->IPRIOR[13]|=((6<<(5+0*8)));//打断级1,排队级3 6. 打开定时器计数结束中断使能开关 这个相关的寄存器在手册的定时器章节之中,15.4.4 DMA/中断使能寄存器(TIMx_DMAINTENR)(x=2/3/4),这个寄存器控制着该TIM的15个事件的中断允许,其中位0为更新中断使能位,也就是更新类事件的中断使能位。 15.4.1 控制寄存器 1(TIMx_CTLR1)(x=2/3/4)中的位2( URS)栏表述如下: 更新请求源,软件通过该位选择 UEV 事件的源。 1:如果使能了更新中断或 DMA 请求,则只有计数器 溢出/下溢才产生更新中断或 DMA 请求; 0:如果使能了更新中断或 DMA 请求,则下述任一事 件产生更新中断或 DMA 请求: -计数器溢出/下溢 -设置 UG 位 -从模式控制器产生的更新 这就很清楚地定义了哪些事件为更新事件,而我们的计数结束就是其中所述的计数器溢出事件。 于是打开计数结束中断允许开关的语句如下: TIM2->DMAINTENR|=(1<<0); //允许更新中断 7. 打开定时器TIM2的全局中断使能开关 这个相关的寄存器在手册的中断章节之中。CH32L103有三个中断使能设置寄存器,寄存器的每一位控制一个全局中断,9.5.2.16 PFIC 中断使能设置寄存器 2(PFIC_IENR2)控制32#-63#这32个全局中断,TIM2的中断号为44#,44#中断由其位12控制。这里同样要注意的是,厂家的头文件里对这个寄存器的命名的序号也是用下标的方式,从0开始编号,也就是我们要用到的寄存器的名字为PFIC->IENR[1]。 于是该语句如下: PFIC->IENR[1]|=(1<<12); //使能TIM2的全局中断,中断号44 8. 启动定时器 这个相关的寄存器在手册的定时器章节之中,15.4.1 控制寄存器 1(TIMx_CTLR1)(x=2/3/4),位0 (CEN)表述如下: 使能计数器 (Counter enable) 1:使能计数器; 0:禁止计数器。
于是启动计数器的语句如下:
TIM2_CTLR1|=(1<<0); //使能计数器即启动计数器 这个一开启,计数器便开始计数,计数结束就置位计数结束标志位发出中断信号、将自动重装值寄存器中的值装入计数器并再次开始计数,周而复始。 好了,定时器的初始化内容算是弄完了,但是,严格说来,这只能说是基本够用,里面的寄存器还有很多项目按说是应该进行设置的,但是我们用它的复位值就可以通过了,所以就不再介绍了。 二、接下来初始化IO端口。 首先当然是开启它们的电源开关,手册RCC章节里找相应的使能寄存器:3.4.7 PB2 外设时钟使能寄存器(RCC_PB2PCENR)中找到了位3,PB 端口模块时钟使能位,1为模块时钟开启, 0为模块时钟关闭。 RCC->PB2PCENR|=(1<<3);//开启PB端口的电源开关 然后是设置PB低八位引脚的输入输出模式和电路模式,不用说,这里自然是用非复用的推挽输出模式,在第 10 章 GPIO 及其复用功能(GPIO/AFIO) 之10.3.1.1 GPIO 配置寄存器低位(GPIOx_CFGLR)(x=A/B/C/D),每个引脚的四位控制位全设置成0011B,于是配置这个寄存器的语句就这样了: GPIOB->CFGLR=0x33333333;//配置端口PB的低八位引脚为通用推挽输出模式 端口的配置就算完成了。 三、再就是定时器中断函数了,四件事: 1. 进入该中断函数后,首先要做的是,判断是定时器的什么事件发生了。我们这里是根据计数溢出来计划要做的事的,所以,我们要判断是不是计数溢出了,即判断计数结束标志位是不是1。手册15.4.5 中断状态寄存(R16_TIMx_INTFR)(x=2/3/4),位0,UIF。 if(TIM2->INTFR&(1<<0)) //如果更新中断标志位为1 2. 再就是翻转端口PB低八位的电位 GPIOB->OUTDR=~GPIOB->OUTDR;//翻转低八位八个引脚电位 3. 15.4.5 中断状态寄存(R16_TIMx_INTFR)(x=2/3/4),表中这样表述的: 名称UIF ,读写RW0 更新中断标志位,当产生更新事件时该位由硬件置位,由软件清零。 1:更新中断产生; 0:无更新事件产生。 以下情形会产生更新事件: 若 UDIS=0,当重复计数器数值上溢或下溢时; 若 URS=0、UDIS=0,当置 UG 位时,或当通过软件对计数器核心计数器重新初始化时; 若 URS=0、UDIS=0,当计数器 CNT 被触发事件重新初始化时; 所以,我们在中断函数中还得给它手动清零: TIM2->INTFR&=~(1<<0); //清除更新中断标志位。 如果此位一直为1而不作清零,则中断控制器会不停地进出这个中断函数,然后你就看到LED一直亮着,因为这时LED翻转频率太高,我们就看不见闪烁了。还有,注意一下上面的表述,这个位,在这个单片机中称为更新中断标志位,也就是说它不仅用于计数结束标志。 4. 注意要加一个硬件压栈声明语句,至于硬件压栈功能开关,CH32的启动文件也是已经打开了,就不用我们动手了: void TIM2_IRQHandler(void)__attribute__((interrupt("WCH-Interrupt-fast"))); 四、再就是主函数,调用初始化函数、等待,没什么好讲的。 五、全部程序如下,就这点东西,所以没做多C: #include "ch32l103.h" //厂家的头文件,里面包含对各寄存器之名称的定义 void TIM2_Init(void) //TIM2初始化 { RCC->PB1PCENR|=(1<<0);//开启TIM2的电源开关 TIM2->PSC=(48000-1);//预分频值48000 TIM2->ATRLR=(2000-1);//自动重装值1000 TIM2->CTLR1|=(1<<7); //使能自动重装值寄存器(ATRLR) PFIC->IPRIOR[13]&= ~(7 <<(5+0*8));//清零[7:5] PFIC->IPRIOR[13]|=((6<<(5+0*8)));//打断级1,排队级3 TIM2->DMAINTENR|=(1<<0); //允许更新中断,即允许计数结束发出中断信号 PFIC->IENR[1]|=(1<<12);//使能TIM2的全局中断,中断号44 TIM2->CTLR1|=(1<<0); //使能计数器即启动计数器 } void LED_Init(void) //端口PB初始化 { RCC->PB2PCENR|=(1<<3);//开启PB端口的电源开关 GPIOB->CFGLR=0x33333333;//配置端口PB的低八位引脚为通用推挽输出模式 } int main(void) { LED_Init(); TIM2_Init(); while(1); } void TIM2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); void TIM2_IRQHandler(void) { if(TIM2->INTFR&(1<<0)) //如果更新中断标志位为1 { GPIOB->OUTDR=~GPIOB->OUTDR;//翻转低八位八个引脚电位 TIM2->INTFR&=~(1<<0); //清除更新中断标志位 } } 编译、下载,看灯是不是在闪烁?用手机的秒表观察一下是不是每2秒闪一下?
|