首页 > 代码库 > B001-Atmega16-数码管
B001-Atmega16-数码管
一步步完成数码管
-------------------------------------------------------------------------------------------------------------------------------------
开发环境:AVR Studio 4.19 + avr-toolchain-installer-3.4.1.1195-win32.win32.x86
芯片型号:ATmega16
芯片主频:8MHz
-------------------------------------------------------------------------------------------------------------------------------------
第一步: 产生1ms的时基
说明:1、使用定时器0的CTC中断产生1ms的时基信号,CTC模式下时自动重装初值、比较方便。
2、使用OCF0中断、不需要OC0引脚输出波形。
代码:
Drv_Timer.h中的相关定义:// ---------------------------------------------------------------------------------------------------------- // 定时器中断模式 typedef enum { INT_MODE_TOV = 0, INT_MODE_OCF = 1, INT_MODE_ICF = 2, INT_MODE_OCF1A = 3, INT_MODE_OCF1B = 4 } TIMER_INT_MODE; // 定时器比较匹配引脚输出模式 typedef enum { COM_MODE_NONE = 0, COM_MODE_TOGGLE = 1, COM_MODE_CLEAR = 2, COM_MODE_SET = 3, } TIMER_COM_MODE; // 定时器0 typedef enum { T0_WGM_NOMAL = 0, T0_WGM_PHASE_PWM = 1, T0_WGM_CTC = 2, T0_WGM_FAST_PWM = 3, T0_CLK_SOURCE_NONE = 0, T0_CLK_SOURCE_CLK_1 = 1, T0_CLK_SOURCE_CLK_8 = 2, T0_CLK_SOURCE_CLK_64 = 3, T0_CLK_SOURCE_CLK_256 = 4, T0_CLK_SOURCE_CLK_1024 = 5, T0_CLK_SOURCE_T0_FALL = 6, T0_CLK_SOURCE_T0_RAISE = 7 } TIMER0_MODE;Drv_Timer.c中的操作函数:
// ========================================================================================================== // TIMER0 初始化 // // 参数:wave_mode 工作模式/波形产生模式选择 // OC_mode 比较匹配/PWM输出模式选择 // clk_source 时钟源和预分频选择 // // 写TCCR0时需要清除bit7=FOC0 // // 定时器溢出周期 T = ((1.0 / 8000000) * 1000000) * clk_source * 256 ( @ 8MHz ) // ========================================================================================================== void Drv_Timer0_init(const uint8_t wave_mode, const uint8_t com_mode, const uint8_t clk_source) { uint8_t wgm00,wgm01; wgm00 = wave_mode & 0x01; wgm01 = (wave_mode & 0x02) >> 1; // 写TCCR0时需要将bit7=FOC0清0 TCCR0 = (wgm00 << 6)| // 工作模式/波形产生模式选择 (wgm01 << 3)| ((com_mode & 0x03) << 4)| // 比较匹配/PWM输出模式选择 ((clk_source & 0x07) << 0); // 时钟源和预分频选择 } // ========================================================================================================== // TIMER0 中断使能 // // 参数:int_mode = INT_MODE_TOV 或 INT_MODE_OCF 或 INT_MODE_ICF // enable = ENABLE 或 DISABLE // // 说明: // 1、OC0引脚要先配置成比较匹配引脚、再修改数据方向寄存器DDB3 // 2、可以单独使能/禁止一种模式的中断 // // ========================================================================================================== void Drv_Timer0_INT_Enable(const uint8_t int_mode, const uint8_t enable) { if(INT_MODE_TOV == int_mode) { if(DISABLE == enable) { TIMSK &= ~(1 << TOIE0); } else { TIMSK |= (1 << TOIE0); } TIFR |= (1 << TOV0); return ; } if(INT_MODE_OCF == int_mode) { if(DISABLE == enable) { TIMSK &= ~(1 << OCIE0); } else { TIMSK |= (1 << OCIE0); } TIFR |= (1 << OCF0); } } // ========================================================================================================== // 设置TCNT0和OCR0的值 // // (1). 在比较匹配下、OCR0需要在TCNT0被设置之后设置 // ========================================================================================================== void Drv_Timer0_set_TCNT0_OCR0(const uint8_t tcnt0, const uint8_t ocr0) { TCNT0 = tcnt0; OCR0 = ocr0; }sys_timer.c中设置定时器0,并在OCF0中断中使用PA1测试定时时间:
#include <avr/interrupt.h> #include "Drv_Timer.h" #include "sys_timer.h" // ========================================================================================================== // 系统任务定时器 // // (1). 使用Timer0产生1ms的时标 // 定时周期 T = ((1.0/8000000)*1000000)*64*(124+1) = 1000us = 1ms // // ========================================================================================================== void sys_timer_init(void) { // 定时器0初始化:CTC模式、OC0引脚不连接、64预分频 Drv_Timer0_init(T0_WGM_CTC, COM_MODE_NONE, T0_CLK_SOURCE_CLK_64); // 设置初值:TCNT0=0、OCR0=122 Drv_Timer0_set_TCNT0_OCR0(0, 122); // 使能OCF0中断 Drv_Timer0_INT_Enable(INT_MODE_OCF, ENABLE); } // ========================================================================================================== // 系统定时器中断 // // (1). 使用Timer0的CTC中断调度各个任务 // // ========================================================================================================== ISR(TIMER0_COMP_vect) { PORTA ^= (1 << PA1); // 使用PA1测试定时周期 }main.c中完成初始化,并设置IO:
// ========================================================================================================== // 主函数 // ========================================================================================================== #include <avr/io.h> #include "Drv_Timer.h" #include "system.h" #include "sys_timer.h" #include "config.h" // ========================================================================================================== // main函数 // ========================================================================================================== int main(void) { // ------------------------------------------------------------------------------------------------------ // 关全局中断 cli(); // 系统初始化 ( 包含sys_timer_init() ) sys_init(); DDRA |= (1 << DDA0) | (1 << DDA1); PORTA &= ~((1 << PA0 ) | (1 << PA1 )); // OC0/PB3初始化为输出0 DDRB |= (1 << DDB3); PORTB &= ~((1 << PB3 )); // 开全局中断 sei(); // ------------------------------------------------------------------------------------------------------ while(1) { } return 0; }
测试结果:
示波器输出如下:1、PA1引脚输出方波,周期是2*1.0ms,引脚电平每隔1.0ms翻转一次。
使用OCR0=124、计算得到精确的1.0ms,但进入中断函数是需要花费时间的。
所以这里使用稍小的OCR0=122,让从中断产生到进入中断函数为止的时间更精确为1.0ms
有些计时功能会积累时基的误差、越到后面积累的误差越大,所以这里能精确就尽量做的精确些。
到此、1.0ms定时完成。
2、OC0引脚没有波形输出,我们也不需要用到这个引脚,就让他保持普通IO的特性吧。
-------------------------------------------------------------------------------------------------------------------------------------
第二步: 静态显示
说明:1、这一步需要根据电路图、在指定的数码管上显示指定的符号。
1、数码管驱动电路图:
电路中使用的是共阴极数码管:
1个数码管有8个LED,称为8段数码管。
共阴的意思是:
1、8个LED的负极都连接在com引脚
2、8个LED的正极对应8个引脚,编号分别为[Dp,g,f,e,d,c,b,a]
数码管的电压和电流:
点亮1个数码管所需的电流需要查看厂家给的数码管数据手册才知道。
如果没有数据手册,可以预估为10-20mA,电压预估为2V。
和普通LED一样、必要的时候需要加限流电阻。
在动态扫描中、每个数码管都是点亮几毫秒熄灭几十毫秒,并非一直点亮,所以不加限流电阻也行,除非是大电流数码管。
数码管的驱动芯片是74HC138和74HC573,他们的驱动电流足够点亮1个数码管:
2、共阴极数码管的点亮方式:
1、在引脚a输入高电平,引脚b-Dp输入低电平,在com引脚输入低电平。那么,段[a]被点亮,段[Dp,g,f,e,d,c,b]都不亮。
也就是说、只有输入状态为高电平的段会被点亮。
2、在引脚b、c输入高电平,引脚a、d-Dp输入低电平,在com引脚输入低电平。
那么,段[b,c]被点亮,段[a,d,e,f,g,Dp]都不亮。
这时得到的图像就是数字‘1‘的图像,对应的段码[Dp,g,f,e,d,c,b,a]=0b00000110=0x06。
3、在引脚a、b、g、e、d输入高电平,引脚c、f、Dp输入低电平,在com引脚输入低电平。
那么,段[a,b,g,e,d]被点亮,段[c,f,Dp]都不亮。
这时得到的图像就是数字‘2‘的图像,对应的段码[Dp,g,f,e,d,c,b,a]=0b01011011=0x5B。
4、也就是说、com口为低电平的数码管被使能,如果它的段选中有不为0的段,这个段就会被点亮。
3、段码:
1、16个十六进制数字[0-9,A-F]的段码如下:static const uint8_t segment_code[17]= { 0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f, // 0 - 9 0x77,0x7c,0x39,0x5e,0x79,0x71, // A - F 0x00 // 全部熄灭 };最后一个值0x00就是全部LED段都不亮,用来熄灭数码管上所有LED段。
在电路中、74HC573的输出和输入保持一致,同时输出电流。
将这些段码赋予PORTB口就可以在74HC573的输出同样的电平数据,使得段[Dp,g,f,e,d,c,b,a]上得到对应的电平,对应的LED段就被点亮。
4、位选:
1、通过拉低某个数码管的com口、来使能这个数码管,这称为位选。8个com口都连接在74HC138上,输出为低电平的引脚上连接的数码管将被选中。
74HC138的输入输出表如下:
在电路中,使用PORTA[2:0]来对应74HC138的输入[A2,A0]。
74HC138的输入[A2,A0]映射到PORTA[2:0]就是下面的数组,用来分别使能第0位到第7位数码管:
static const uint8_t segment_index[8]= { 0,1,2,3,4,5,6,7 };
5、代码:
下面的代码用来在第0号数码管上、显示数字"5"。
Mod_LED_display.c:
#include <avr/interrupt.h> #include "Mod_LED_Displayer.h" // 段码(共阴 == 高电平点亮) static const uint8_t segment_code[17]= { 0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f, // 0 - 9 0x77,0x7c,0x39,0x5e,0x79,0x71, // A - F 0x00 // 全部熄灭 }; // 位码(低电平使能)(使用74HC138选通位选) // 分别使能第0号到第7号数码管(与之相与(&)来使能) static const uint8_t segment_index[8]= { 0,1,2,3,4,5,6,7 }; // ========================================================================================================== // LED数码管硬件初始化 // // ========================================================================================================== void Mod_LED_display_init(void) { // 数码位选选使能(74HC138芯片使能) DDRC |= (1 << DDC7); PORTC |= (1 << PC7 ); // 段选控制:PORTB初始化为:输出低电平 DDRB = 0xFF; PORTB = 0x00; // 位选控制:PORTA[2:0]初始化为:输出低电平(选中第0号数码管) DDRA |= (1 << DDA0) |(1 << DDA1) |(1 << DDA2); PORTA &= ~((1 << PA0 ) |(1 << PA1 ) |(1 << PA2 )); // LED数码管显示固定的数据 ------------------------------------------------------------------------------ // 清除数码管显示 PORTB = segment_code[16]; // 修改位选 PORTA |= segment_index[7]; PORTA &= segment_index[0]; // 第0号数码管 // 修改显示 PORTB = segment_code[5]; // 数字'5' }
main.c:
// ========================================================================================================== // 主函数 // ========================================================================================================== #include <avr/io.h> #include "Mod_LED_Displayer.h" #include "sys_timer.h" #include "system.h" #include "config.h" // ========================================================================================================== // main函数 // ========================================================================================================== int main(void) { // ------------------------------------------------------------------------------------------------------ // 关全局中断 cli(); // 系统初始化 ( 包含Mod_LED_display_init() ) sys_init(); // 开全局中断 sei(); // ------------------------------------------------------------------------------------------------------ while(1) { } return 0; }
测试结果:
1、现在可以在任一个数码管上、显示任一个十六进制数了。
-------------------------------------------------------------------------------------------------------------------------------------
第三步: 动态扫描
说明:1、由静态显示到动态扫描经过了以下步骤:
(1). 在第一步的1ms定时中断中、每隔1000ms在第0号数码管上显示1个数字,
并依次循环显示16个十六进制数,检验所有段码。
(2). 在第一步的1ms定时中断中、每隔1000ms切换到下一个数码管,去显示一个固定的数字,
并依次循环切换这8个数码管,检验所有位码。(3). 让8位数码管动态显示数值01234567。
也就是切换到第0号数码管时、显示数字‘0‘,后面的一次类推,每隔1ms切换一个数码管。
(4). 最后、让让8位数码管动态显示任意数值,超过8位的数值当然只能显示低8位(十进制)。
最终的代码经过了几次构造,以方便修改用于控制数码管的IO口。
代码如下:
Mod_LED_display.c:
#include <avr/interrupt.h> #include "Mod_LED_Displayer.h" // 段码(共阴 == 高电平点亮) static const uint8_t segment_code[17]= { 0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f, // 0 - 9 0x77,0x7c,0x39,0x5e,0x79,0x71, // A - F 0x00 // 全部熄灭 }; // 位码(低电平使能)(使用74HC138选通位选) // 分别使能第0号到第7号数码管(与之相与(&)来使能) static const uint8_t segment_index[8]= { 0,1,2,3,4,5,6,7 }; // 数码管控制结构 typedef struct { uint8_t *seg_index; // 位选端口的数据 uint8_t *seg_code; // 段选端口的数据 uint8_t index; // 使能第index数码管 uint8_t data[sizeof(segment_index)]; // 送给[0:7]号数码管去显示的8个数据 }T_SEG_LED_DISPLAY_CTRL,*pT_SEG_LED_DISPLAY_CTRL; static T_SEG_LED_DISPLAY_CTRL LED_display_ctrl = { .seg_index = (uint8_t *)(&PORTA), .seg_code = (uint8_t *)(&PORTB), .index = 0, .data = http://www.mamicode.com/{ 0,1,2,3,4,5,6,7 }>在sys_timer.c中,每隔1ms刷新一次数码管:
// ========================================================================================================== // 系统定时器中断(中断周期=1ms) // // (1). 使用Timer0的CTC中断调度各个任务 // // ========================================================================================================== ISR(TIMER0_COMP_vect) { Mod_LED_display_update(); }main.c如下:// ========================================================================================================== // 主函数 // ========================================================================================================== #include <avr/io.h> #include "Mod_LED_Displayer.h" #include "sys_timer.h" #include "system.h" #include "config.h" // ========================================================================================================== // main函数 // ========================================================================================================== int main(void) { // ------------------------------------------------------------------------------------------------------ // 关全局中断 cli(); // 系统初始化 sys_init(); // 开全局中断 sei(); Mod_LED_display(123456789); // Mod_LED_display_hex(65536, 15); // ------------------------------------------------------------------------------------------------------ while(1) { } return 0; }测试结果:
1、显示中没有余晖(重影),不亮的段都是白色。
2、操作函数中还提供了一个根据指定的进制来保存和显示数字的函数Mod_LED_display_hex()。
比如Mod_LED_display_hex(65536, 15);就是将十进制数65536按照15进制保存、并显示在数码管上。
数码管上将显示15进制数14641,可以在线进行进制转换、来验证结果:http://tool.oschina.net/hexconvert。
在线转换的结果:
3、这里对数码管的操作是一种三段式的操作:初始化 + 前台更新 + 后台API。
-------------------------------------------------------------------------------------------------------------------------------------
第四步: 余晖 (重影)
余晖的来源:
1、如果现在正在第0号数码管上显示数字‘E‘(‘E‘的段码是0x79),然后切换到第1号数码管上去显示数字‘1‘,中间不先将第0号数码管熄灭。
其过程是:修改位码、从第0号数码管切换到第1号数码管,修改段码、将段码由‘E‘的段码0x79改为‘1‘的段码0x06。
切换到第1号数码管只修改了位码,此时的段码依然是‘E‘的段码0x79,此时第1号数码管就会显示数字‘E‘。
接着把段码改为‘1‘的段码0x06,此时、第1号数码管才显示数字‘1‘。
结果是、第1号数码管会先显示一小段时间(几个时钟周期)的数字‘E‘、然后才显示数字‘1‘(1ms)。
显示数字‘1‘的时候、只有段[b,c]被点亮。
显示数字‘E‘的时候、只有段[a,d,e,f,g]会被点亮。
所以、第1号数码管显示数字‘1‘之前,段[a,d,e,f,g]会被点亮一段时间,然后才去点亮段[b,c],这里有一先一后的顺序。
但对于眼睛来说,由于时间太短,眼睛会认为显示几个时钟周期的数字‘E‘和显示1ms的数字‘1‘是同时发生的。
看的的结果就是、第1号数码管显示数字‘1‘的时候、其他的段[a,d,e,f,g]是微亮的,这就是余晖。
显示数字‘E‘的时间越久、第1号数码管来自第0号数码管的余晖就越明显。
另外、驱动电流越大,数码管越量,余晖也会越亮。
2、上面先修改位选、再修改段选。
如果先修改段选、再修改位选、一样会有余晖产生,如果不先熄灭数码管、让段码为0x00的话。
只是、第1号数码管的余晖来自第2号数码管,而不是第0号数码管。
因为数码管还没有从第1号切换到第2号之前,段码要先改成了第2号数码管要显示的段码。
3、所以在切换数码管之前,需要熄灭数码管,将段码先修改为熄灭显示的段码 0x00。
4、或者、可以关闭数码管,也就是将所有数码管的com口都拉高,熄灭所有数码管。
在段码修改完毕后,再设置下一个数码管的位码,来使能下一个数码管。
上面的电路中、74HC138不能禁止所有数码管,所以只能使用熄灭显示的段码0x00。
使用三极管控制数码管的com口的电路,可以禁止所有数码管。
测试代码:
说明:1、如果屏蔽下面用于避免余晖的代码,就会有余晖。2、但为了让余晖更明显,可以在切换到下一个数码管之前,延时几百微妙。3、下面的代码使用#if 0来使能余晖的产生,并加入延时:// ========================================================================================================== // LED数码管显示数据的刷新 // // (1). 在系统定时器中每隔1ms刷新1次 // // ========================================================================================================== void Mod_LED_display_update(void) { #if 0 // 熄灭当前数码管、并保持3个时钟周期的熄灭,用来避免余晖 *p_LED_display_ctrl->seg_code = segment_code[sizeof(segment_code) - 1]; #endif // 切换到下1个数码管 p_LED_display_ctrl->index++; if(p_LED_display_ctrl->index > (sizeof(segment_index) - 1)) { p_LED_display_ctrl->index = 0; } // 修改位选、修改显示 *p_LED_display_ctrl->seg_index |= segment_index[sizeof(segment_index) - 1]; *p_LED_display_ctrl->seg_index &= segment_index[p_LED_display_ctrl->index]; // 切换数码管后,延时一段时间,看看余晖,在修改段码 for(volatile int j = 0; j < 20; j++) {} // 修改段码 *p_LED_display_ctrl->seg_code = segment_code[p_LED_display_ctrl->data[segment_index[p_LED_display_ctrl->index]]]; }测试结果:
0、从左到右分别是第0号数码管到第7号数码管。1、在第3号数码管上、可以明显的看到来自第2号数码管(显示数字‘0‘)的余晖。在第5、6、7号数码管上、也有明显的余晖。2、第0号数码管上没有余晖,因为它的上一个数码管(第7号)显示的是数字‘1‘,其余晖被第0号数码管自己显示的数字‘0‘覆盖了。同样的、第1、2、4号数码管上的余晖也是被覆盖了。3、延时用的for循环中、有个volatile,用来避免编译器优化掉变量j。如果被优化掉,这段代码就没了,因为这段代码对AVR-GCC编译器来说、没有什么意义。-------------------------------------------------------------------------------------------------------------------------------------第五步: 改进后的代码
B001-Atmega16-数码管