一、功能需求
使用定时器中断,实现数码管显示数值的递减
时间范围:10~0
递减步长:1000ms
二、数码管驱动原理
数码管显示的工作原理,其实不难,只要了解数码管的结构是由8个小的LED组成的。如下图所示,每一个数码(带1个小数点)管,都由8个led组成一个8字行,通过控制这些小灯的ON/OFF,就能组合成0-9各个数字。
而显示什么数字,其对应的8个灯管on/off的输出次序,就是编码表,如下图所示
这里就有一个问题:显示1个数字就要控制8个LED,如果用8个GPIO来控制,那每位数字8个IO口,显示8位数字就需要64个针脚,mcu哪有那么多针脚给数码管使用? 能不能用少数几个针脚,就能驱动多个数码管?
这就需要 74HC595 这个驱动芯片出场了。手册上对它的一句话描述:8位带有输出锁存功能的移位寄存器。该器件具有串行输入、移位功能,和带锁存器的并行输出功能。串行的输入移位,由移位时钟控制;并行的锁存输出,由输出时钟控制:两个时钟相互独立。
有了这个芯片,我们就可以通过一个GPIO引脚,每次输入1个LED灯的ON/Off状态,在移位时钟的驱动下每次移位1位;连续8个时钟,就将8个led的状态缓存在寄存器中。紧接着需要一次性输出的时候,检测到输出时钟上升沿,就将8个状态一次性通过锁存器输出,这样8位段数码管就能够一次收到8个led的控制信号,输出不同的数字。简单的说:一个个串行移入值,一次性并行输出值。所需的GPIO引脚也只有3个,1个数据针,2个时钟针。
多个数码管要连在一起显示,如上图8个数码管,如何驱动所有这8个管子?设计制造的时候,8个管子各有一个片选引脚CS,只有CS引脚本选中时,数码管才接受8个针脚高低电平的控制信号。这样,我们可以将2个74HC595芯片串联起来,1片的8针脚同时输出控制数据,另一片的8针脚输出8个片选信号,任何时候只有一个数码管被选中。通过单片机对8个数码管依次选中和输出控制信号,实现同时点亮8个数码管输出的功能。
三、数码管驱动封装
理解了8个数码管同时显示的驱动原理,我们可以将数码管外设的功能实现,并封装成*.h/.c文件,方便后续调用。
硬件接线上,使用3个GPIO口作为输出,1个数据位输出,2个为时钟输出(因为时钟和锁存输出时钟)。供电使用开发板上的3v3,共5根线其接法如下图
对3个GPIO输出,SysConfig工具中配置如下:
保存后编译无误。接着开始写代码封装 8个数码管的驱动显示函数,先看码表的定义:
//dat -- PA27 : 串行数据输入 //SCLK-- PA26 :移位时钟 //RCLK-- PA13 :并行寄存器锁存时钟 // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, b, C, d, E, F uint8_t Disp_DX[ 16 ]={0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E}; //段显 uint8_t Disp_PX[ 8 ]={1,2,3,4,5,6,7,8}; //片选
两个数组分别为 段选和片选,输入0-9的下标,可以获得要显示此数字需要的编码值;
紧接着,输出时候需要 输出时钟有一个上升沿,为了使用方便,定义一个函数 Display_Out()
//锁存输出到 并行寄存器 void Display_Out() { HC595_RCK(0); //delay_cycles(DELAYCYCLES); HC595_RCK(1); }
串行输入的时候,按照码表的值,一次GPIO针脚输出字节中的1位,然后移位时钟上升沿时完成移位,紧接着针脚输出第2位,等待移位上升沿。。。直到8个bit都移位完成。注意这里是大端模式,即首先引入字节的最高位。代码如下:
//串行写入一个字节,注意:大端模式 void HC595_WriteByte(uint8_t data) { for(int i=0; i<8; i++) { if( data&0x80 ) //最高位为1 { HC595_DAT(1); }else{ HC595_DAT(0); } //左移一位 data<<=1; //产生移位脉冲 HC595_CLK(0); HC595_CLK(1); } }
两个595芯片,一个输出8位的片选信号,另一个同时输出8位的led控制信号,所以点亮一个灯管的数码,其代码如下:
//发送一个数字(0-9)到指定位的数码管 //dis_num 发送的数字(0-9) //dis_bit 发送到第几个数码管,即:片选(0-7) void HC595_SEND_DATA(uint8_t dis_num, uint8_t dis_bit) { HC595_WriteByte( dis_num ); //写数据 HC595_WriteByte( 1<<dis_bit ); //写片选 Display_Out(); //锁存、输出 }
一个数码管能显示,8个数码管循环扫描8次,就全点亮了。封装代码如下:
//数码管同时显示8位数字 void Disp_8Num(uint16_t hiData, uint16_t loData) { uint16_t tmpH,tmpL; uint8_t num_q, num_b, num_s, num_g; //千、百、十、个位 数字 //显示高4位 tmpH = hiData; num_q = tmpH / 1000; num_b = tmpH / 100 % 10; num_s = tmpH / 10 % 10; num_g = tmpH % 10; HC595_SEND_DATA(Disp_DX[num_q],7); HC595_SEND_DATA(Disp_DX[num_b],6); HC595_SEND_DATA(Disp_DX[num_s],5); HC595_SEND_DATA(Disp_DX[num_g],4); //显示低4位 tmpL = loData; num_q = tmpL / 1000; num_b = tmpL / 100 % 10; num_s = tmpL / 10 % 10; num_g = tmpL % 10; HC595_SEND_DATA(Disp_DX[num_q],3); HC595_SEND_DATA(Disp_DX[num_b],2); HC595_SEND_DATA(Disp_DX[num_s],1); HC595_SEND_DATA(Disp_DX[num_g],0); }
同理,可以封装只显示最低4位数码管的函数 Disp_4Num(), 以及只显示最低2个数码管的函数Disp_2Num().
将以上所有函数封装到 HC595.h 头文件中,方便后面调用。
四、定时器实现
定时器可以通过SysConfig工具方便的配置。其需要配置的过程,先要选择时钟源,倍频除数,以及预分频系数PreScale,就得到了计数器的基频率。计数器的计数值设定后,又需要选择向上计数/向下计数还是同时计数。
要列算式估计比较复杂,还好有SysConfig工具。我们设置使用TimerG第一个计数器,32M的系统时钟源,倍频除数1,预分频256, 这样基频率就是 32M/256=125K,取倒数周期为8us。定时器如果设定为10ms,也即频率为100Hz,那么计数值就得取1250向下计数,也即每1250个基频率脉冲输出一次定时器中断。配置如下:
五、代码实现
Sysconfig工具自动生成了定时器的初始化代码,其中enable了 Tim0的中断。但是在代码中,还需要将Tim0 的全局中断在NVIC管理器中使能。其代码如下:
之后就会按固定的10ms一次,系统自动运行中断处理函数。我们需要将业务逻辑实现在中断处理函数中:
中断处理函数的名字是固定的,sysconfig产生的文件中已经重新define了函数名;函数的结构也是模板式的,先用switch细分是什么中断类型,然后各自处理不同的业务逻辑。图中方框里是真正的业务逻辑,因为是10ms周期,所以需要100次计数才是1秒的周期。每一秒只需要显示值减一,如果显示值小于0了则重新装载最大值MaxNum=10即可。
而最终的数码管显示代码,放在main中的while循环中,只有一句话,调用 Disp_2Num() 将递减的值显示出来即可。
六、下载验证
下载测试没有问题,b站视频如下: