单片机秒表项目 – 数码管、按键和中断的综合单片机小程序

[title]单片机秒表程序效果[/title]

首先,我们先看下效果视频:

[title]思维导图(9个函数之间的关联)[/title]

本程序用到的基本模块有:按键、数码管

用到了MCU内部的中断和定时器

在写代码之前,先画一个思维导图,理清各个模块、函数之间的关联。还有各个状态变量。

[title]程序代码[/title]

/*!
 * 心得体会:
 * 1. 单片机由于没有硬件调试工具,所以debug显得十分棘手。因此我们只能通过观察硬件故障(与设计不相符)来确定是哪块函数出现了问题。
 *		需要用到中断的函数,在排查该函数之前先排除中断问题。
 *  	然后:例如数码管显示不正常,那就需要查看LedScan()函数。该函数是控制扫描的,如果该函数错误就会出现有的数码管显示有的不显示,频闪等问题。
 *		   		顺藤摸瓜,如果LedScan()函数没有问题,就查数码管显示函数。如果显示函数出现错误,就会导致显示数字诡异。
 *					又例如按键出现失效,比如只能按下一次,在程序运行过程中失效。那么大多是backup变量出现了问题。
 * 2. 再次强调模块化的重要性,试想下我们这个简单的程序就有9个函数。如果将它们都写在main函数中,那这个程序的调试基本完蛋。一定要降低耦合度。方便出现问题的时候按硬件对应的模块调试。
 * 3. 对于复杂程序,可以画一个思维导图来理清各个函数之间的调用关系。
 * 4. 主程序之外的全局变量一定要赋初值!
 * 5. 理解函数内static的作用范围。例如经常在函数内for循环经常用到的i,为什么有的加static有的不加?
 * 6. 模块化学习,各个硬件独立函数封装,以便于改进和记忆。
 */

#include <reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit KeyIn1 = P2^4;
sbit KeyIn2 = P2^5;
sbit KeyIn3 = P2^6;
sbit KeyIn4 = P2^7;

unsigned char code LedChar[] = {
	0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
	0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};

unsigned char LedBuff[6] = {
	0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};

unsigned char KeySta[4] = {
	1, 1, 1, 1				//KeyIn的默认状态
};

unsigned char DecimalPart = 0;
unsigned int IntegerPart = 0;
bit StopwatchRunning = 0;
bit StopwatchRefresh = 1;			//1为reset状态

void ConfigTimer0(unsigned int ms);
void KeyDriver();
void StopwatchDisplay();

unsigned char T0RH = 0;
unsigned char T0RL = 0;

void main()
{
	ENLED = 0;
	ADDR3 = 1;
	P2 = 0xFE;
	
	//开启中断
	EA = 1;
	ConfigTimer0(2);					//2ms
	
	while(1)
	{
		
		if (StopwatchRefresh == 1)
		{
			StopwatchRefresh = 0;
			StopwatchDisplay();
		}
		KeyDriver();
	}
}


void StopwatchCount()				//每10ms计数一次
{
	if (StopwatchRunning == 1)
	{
		DecimalPart++;
		if (DecimalPart >= 100)
		{
			DecimalPart = 0;
			IntegerPart++;
			if (IntegerPart >= 10000)
			{
				IntegerPart = 0;
			}
		}
		StopwatchRefresh = 1;
	}
}

void StopwatchDisplay()
{
	unsigned char buf[4];			//4的意思是整数部分占用4个数码管
	static signed char i;
	
	//确定每个数码管显示的数值,即buf[]
	//小数部分
	LedBuff[0] = LedChar[DecimalPart%10];
	LedBuff[1] = LedChar[DecimalPart/10];
	
	//整数部分
	buf[0] = IntegerPart%10;
	buf[1] = IntegerPart/10%10;
	buf[2] = IntegerPart/100%10;
	buf[3] = IntegerPart/1000%10;
	
	//从高位看,最高位如果是0则不显示
	for (i=3; i>=1; i--)
	{
		if (buf[i] == 0)
		{
			LedBuff[i+2] = 0xFF;			//+2的意思是:避开最后两个显示毫秒的数码管
		}
		else
		{
			break;
		}
	}
	for (; i>=0; i--)
	{
		LedBuff[i+2] = LedChar[buf[i]];
	}
	LedBuff[2] &= 0x7F;						//显示小数点,在指定的数码管
}

void LedScan()
{
	static unsigned char i = 0;
	P0 = 0xFF;							//消影
	P1 = (P1 & 0xF8) | i;
	P0 = LedBuff[i];				//最终在这里数码管被扫描刷新
	
	//[0, 5]循环
	if (i<5)
	{
		i++;
	}
	else
	{
		i = 0;
	}
}


/* 秒表启停函数 */
void StopwatchAction()
{
	if (StopwatchRunning)
	{
		StopwatchRunning = 0;
	}
	else
	{
		StopwatchRunning = 1;
	}
}

/* 秒表复位函数 */
void StopwatchReset()
{
	StopwatchRunning = 0;
	DecimalPart = 0;
	IntegerPart = 0;
	StopwatchRefresh = 1;
}


void KeyDriver()
{
	unsigned char i;
	static unsigned char backup[] = {
		1, 1, 1, 1
	};
	
	for (i=0; i<4;i++)
	{
		if (backup[i] != KeySta[i])
		{
			if (backup[i] != 0)					//按下
			{
				if (i == 1)
				{
					StopwatchReset();
				}
				else if (i == 2)
				{
					StopwatchAction();
				}	
				//backup[i] = KeySta[i];
			}
			backup[i] = KeySta[i];					//此句写错到上面的位置出现的症状是:按键只能在开始的时候使用一次,后面在程序运行过程中按键失效。
		}
	}
}


void KeyScan()
{
	unsigned char i;
	static unsigned char keybuf[] = {
		0xFF, 0xFF, 0xFF, 0xFF
	};
	
	keybuf[0] = (keybuf[0] << 1) | KeyIn1;
	keybuf[1] = (keybuf[1] << 1) | KeyIn2;
	keybuf[2] = (keybuf[2] << 1) | KeyIn3;
	keybuf[3] = (keybuf[3] << 1) | KeyIn4;
	for (i=0; i<4; i++)
	{
		if (keybuf[i] == 0x00)
		{
			KeySta[i] = 0;
		} else if (keybuf[i] == 0xFF)
		{
			KeySta[i] = 1;
		}
	}
}

/* 配置中断 */
void ConfigTimer0(unsigned int ms)
{
	static unsigned long tmp;		//初值
	tmp = 65536-(11059200/12*ms/1000);
	tmp = tmp + 18;
	
	//tmp共16位长(long)
	T0RH = (unsigned char)(tmp >> 8);			//取前8位
	T0RL = (unsigned char)tmp;						//截断前8位
	
	TH0 = T0RH;
	TL0 = T0RL;
	
	TMOD = TMOD & 0xF0;
	TMOD = TMOD | 0x01;

	ET0 = 1;
	TR0 = 1;
}

/* 中断 */
void InterruptTimer0() interrupt 1
{
	static unsigned char tmr10ms = 0;
	TH0 = T0RH;
	TL0 = T0RL;
	
	//提供2ms中断服务
	LedScan();
	KeyScan();
	
	//提供10ms终端服务
	tmr10ms++;
	if (tmr10ms >= 5)
	{
		tmr10ms = 0;
		StopwatchCount();
	}
}

 

[title]心得体会[/title]

心得体会:

  1. 单片机由于没有硬件调试工具,所以debug显得十分棘手。因此我们只能通过观察硬件故障(与设计不相符)来确定是哪块函数出现了问题。
    需要用到中断的函数,在排查该函数之前先排除中断问题。然后:例如数码管显示不正常,那就需要查看LedScan()函数。该函数是控制扫描的,如果该函数错误就会出现有的数码管显示有的不显示,频闪等问题。顺藤摸瓜,如果LedScan()函数没有问题,就查数码管显示函数。如果显示函数出现错误,就会导致显示数字诡异。
    又例如按键出现失效,比如只能按下一次,在程序运行过程中失效。那么大多是backup变量出现了问题。
  2. 再次强调模块化的重要性,试想下我们这个简单的程序就有9个函数。如果将它们都写在main函数中,那这个程序的调试基本完蛋。一定要降低耦合度。方便出现问题的时候按硬件对应的模块调试。
  3. 对于复杂程序,可以画一个思维导图来理清各个函数之间的调用关系。
  4. 主程序之外的全局变量一定要赋初值!
  5. 理解函数内static的作用范围。例如经常在函数内for循环经常用到的i,为什么有的加static有的不加?
  6.  模块化学习,各个硬件独立函数封装,以便于改进和记忆。

作者: 高志远

高志远,23岁,男生,毕业于上海杉达学院电子商务系。

发表评论

邮箱地址不会被公开。