酷炫的数字变换

一、准备开发板

前段时间在网上碰巧看到一个视频(原文链接点击这里),很酷的数字变换时钟,代码还是开源的!于是我也想搞一个玩玩,淘宝搜LED显示屏,找了一圈找到一个尺寸320*160mm、型号P5、分辨率也是64*32的,但是卖家不提供相关资料,那我写个锤子的驱动啊。这时候拿出我封印已久的战舰板,满满的都是回忆啊!我打算用我的战舰板去移植代码,实现类似的效果。

二、先搞个框架

1.基本情况

  • LED矩阵显示屏分辨率:64*32
  • LED矩阵显示屏占用区域:59*15
  • 2.8寸TFT-LCD分辨率:320*240

2.分割区域

我打算用2.8寸显示屏实现上图的效果。首先分割区域,2.8寸TFT-LCD的宽度是足够的,以长度分割320/64=5,这样像素块长度为5,整个显示屏就被分割为了 64*48; 接下来在块与块之间加入间隙,由于块区域的对称性,间隙的点数必须是偶数个,对于5*5的区域,间隙设置为2是最适合的,显示像素点为 3*3 个,如下图所示;如果间隙设置4的话,对应显示的像素点个数为1*1个,5*5的区域只有一个点会亮?一家独秀是不行的。

相关宏定义如下:

//64*48代表5像素点分割后的分辨率,LOC_START代表了起始位置   		
#define LOC_START_X    		3  			//0-63
#define LOC_START_Y    		16 			//0-47

#define LEN_UNIT   			(5)  	 	//像素块长宽为5
#define LEN_DIS    			(3 - 1)  	//显示像素长宽为3(0~2)
#define LEN_GAP    			(2 - 1)  	//间隙像素为2(0~1)

#define AREA_X      		59       	//框架横向最大值59
#define AREA_Y      		15       	//框架纵向最大值15

for循环绘制块区域的测试结果如下图:

该区域的蓝点密度为 59*15,和LED矩阵用于显示的像素密度相同,达到了我想要的效果,很开心。有些东西用文字描述并没有用图表示来得直接,所以下面放张图表示一下宏定义的含义。测试代码并没有很大用处,这里就不贴了,画块区域的函数代码下面会讲。

三、由点到线

1.重定义点函数

此时的点已不是在单纯的点了,而是重新定义的5*5的块区域,因此重定义的画块函数需要调用正点原子LCD库中提供的矩形填充函数来实现,代码如下所示:

//画一个配置好的像素块
void DIGIT_DrawPixelBlock(u16 x, u16 y, u16 color)
{
	LCD_Fill((locX_now+x)*LEN_UNIT + LEN_GAP, 
		(locY_now+y)*LEN_UNIT + LEN_GAP, 
		(locX_now+x)*LEN_UNIT + LEN_GAP + LEN_DIS,
		(locY_now+y)*LEN_UNIT + LEN_GAP + LEN_DIS, color);
}

参数color表示块的颜色,为了更直观的编写和后期调试,这里的参数x和y被重定位了!蓝色显示框架的起点转换为了(0, 0)。关于这个locY_now,它的作用是移动显示的起点坐标。数字是多个位置显示的,函数中总要加入偏移量,此时此刻思考一下偏移量最好放在哪里,这时候有几种方法:第一种方法是把偏移量放在画块函数之中;第二种方法是在画线函数之中,将偏移量+初始位置作为实参传给画块函数;第三种方法是将偏移量+初始位置作为实参传给画线函数,这就有点扯淡了。。因为后面的动画切换会疯狂调用画线函数。第一种和第二种方法本质是相同的,但放在底层的画块函数还是好那么一丢丢。当时写的时候好像也没考虑那么多,只是感觉工程被我写得越来越复杂 T^T。

2.点到为止画线

画线函数很好写也很好理解,就是比较两个点的x和y坐标,判断是横线还是竖线,参数错误的情况可写可不写,这里就不多说了,一切尽在fucking source code中。

//根据设置好的像素块画线
void DIGIT_DrawLine(u16 x1, u16 y1, u16 x2, u16 y2, u16 color)		//x范围0-58,y范围0-14
{
	int i;
	if (x1 == x2) {
		/*
		 * 画竖线:当y1的坐标比y2小时,填充矩形函数起始位置用(x1,y1);
		 * 当y1的坐标比y2大时,填充矩形函数起始位置用(x1,y2).
		 */
		if (y1 <= y2)
			for (i  = 0; i <=  y2 - y1; i++)
				DIGIT_DrawPixelBlock(x1, y1+i, color);
		else if (y1 > y2)
			for (i  = 0; i <=  y1 - y2; i++)
				DIGIT_DrawPixelBlock(x1, y2+i, color);
	} else if (y1 == y2) { 
		/*
		 * 画横线:当x1的坐标比x2小时,填充矩形函数起始位置用(x1,y1);
		 * 当x1的坐标比x2大时,填充矩形函数起始位置用(x2,y1).
		 */
		if (x1 <= x2)
			for (i  = 0; i <=  x2 - x1; i++)
				DIGIT_DrawPixelBlock(x1+i, y1, color);
		else if (x1 > x2)
			for (i  = 0; i <=  x1 - x2; i++)
				DIGIT_DrawPixelBlock(x2+i, y1, color);
	}
}

四、显示数字

1.定义数组

定义unsigned char类型数组,存储0-9的十六进制表示。十六进制转化为二进制后,各个位代表了数码管各个段的亮灭情况,1代表亮,0代表灭。

const u8 digitBits[] = {
  0xFC, //B11111100, 0,  ABCDEF--
  0x60, //B01100000, 1,  -BC-----
  0xDA, //B11011010, 2,  AB-DE-G-
  0xF2, //B11110010, 3,  ABCD--G-
  0x66, //B01100110, 4,  -BC--FG-
  0xB6, //B10110110, 5,  A-CD-FG-
  0xBE, //B10111110, 6,  A-CDEFG-
  0xE0, //B11100000, 7,  ABC-----
  0xFE, //B11111110, 8,  ABCDEFG-
  0xF6, //B11110110, 9,  ABCD_FG-
};

2.数字的段

数字分为7段(点不算),seg为段号,段宽段高都设置为了6,和参考的效果一样。这里使用了宏定义,想要设置为其他的值也很方便。

#define SA 		 0
#define SB 		 1
#define SC 		 2
#define SD 		 3
#define SE 		 4
#define SF 		 5
#define SG 		 6

#define SEG_WIDTH     		6        	//段宽
#define SEG_HEIGHT    		6        	//段高

void DIGIT_DrawSeg(u8 seg)
{
  switch (seg) {
	  case SA: DIGIT_DrawLine(1, 0, SEG_WIDTH, 0, segColor); break;
	  case SB: DIGIT_DrawLine(SEG_WIDTH+1, 1, SEG_WIDTH+1, SEG_HEIGHT, segColor); break;
	  case SC: DIGIT_DrawLine(SEG_WIDTH+1, SEG_HEIGHT+2, SEG_WIDTH+1, 2*SEG_HEIGHT+1, segColor); break;
	  case SD: DIGIT_DrawLine(1, 2*SEG_HEIGHT+2, SEG_WIDTH, 2*SEG_HEIGHT+2, segColor); break;
	  case SE: DIGIT_DrawLine(0, SEG_HEIGHT+2, 0, 2*SEG_HEIGHT+1, segColor); break;
	  case SF: DIGIT_DrawLine(0, 1, 0, SEG_HEIGHT, segColor); break;
	  case SG: DIGIT_DrawLine(1, SEG_HEIGHT+1, SEG_WIDTH, SEG_HEIGHT+1, segColor); break;
	  default: break;
  }
}

3.显示数字

形参为u8类型的数字,函数效果是显示该数字。

//画数字
void DIGIT_DrawDigit(u8 num)  //参数范围范围0-9
{
	u8 value;
	if (num > 9)  //不在允许范围之内则退出
		return;
	value = digitBits[num];
	if (value & 0x80)	DIGIT_DrawSeg(SA);
	if (value & 0x40)	DIGIT_DrawSeg(SB);
	if (value & 0x20)	DIGIT_DrawSeg(SC);
	if (value & 0x10)	DIGIT_DrawSeg(SD);
	if (value & 0x08)	DIGIT_DrawSeg(SE);
	if (value & 0x04)	DIGIT_DrawSeg(SF);
	if (value & 0x02)	DIGIT_DrawSeg(SG);
}

五、实现动画

动画的实现靠的是循环,依次点亮指定块区域。我这里宏定义可能并不是那么好懂,手动去模拟一下就好了哈哈,一开始看到这个动画感觉好厉害呀,看了源码之后也就那么回事,但是人家就有这个创意和想法。说一下要注意的地方,动画是要考虑上一次的值的,由于变到2的情况只能是1变2,所以这里不需要去判断上一次的值;如果是变化到0,那么上一次的值可能是2、可能是3、还可能是5等,具体情况具体讨论。

//动画1变到2
void DIGIT_Morph2(void) 
{
	int i;
	for (i = 0; i <= SEG_WIDTH; i++) { 
		if (i < SEG_WIDTH) {
			DIGIT_DrawPixelBlock(SEG_WIDTH - i, 0, segColor);  //画A
			DIGIT_DrawPixelBlock(SEG_WIDTH - i, SEG_HEIGHT + 1, segColor);   //画G
			DIGIT_DrawPixelBlock(SEG_WIDTH - i, 2*SEG_HEIGHT + 2, segColor); //画D		
		}
		//左平移E
		DIGIT_DrawLine(SEG_WIDTH - i + 1, SEG_HEIGHT + 2, SEG_WIDTH - i + 1, 2*SEG_HEIGHT + 1, BLACK); 
		DIGIT_DrawLine(SEG_WIDTH - i, SEG_HEIGHT + 2, SEG_WIDTH - i, 2*SEG_HEIGHT + 1, segColor);
		delay_ms(ANIM_SPEED);
	}
}

封装成了函数之后:

/* 作用: 数字变换
 * 参数: value->当前的数字值
 *		 _value->上次的数字值
 * 返回值: none
 */
void DIGIT_Morph(u8 value, u8 _value) 
{
	switch (value) {
		case 0: DIGIT_Morph0(_value); break;
		case 1: DIGIT_Morph1(); break;
		case 2: DIGIT_Morph2(); break;
		case 3: DIGIT_Morph3(); break;
		case 4: DIGIT_Morph4(); break;
		case 5: DIGIT_Morph5(); break;
		case 6: DIGIT_Morph6(); break;
		case 7: DIGIT_Morph7(); break;
		case 8: DIGIT_Morph8(); break;
		case 9: DIGIT_Morph9(); break;
	}
}

六、为了好玩

颜色不只一种,当然显示的颜色也可以变换啦,颜色顺序太固定也没什么意思,唯有意想不到才最有趣。于是我给它加了一个随机显示颜色的功能,还可以吧。c语言提供的void srand(unsigned int seed)函数可以产生随机序列,但是!如果每次都提供相同的随机数种子,那么他产生的随机序列都是相同的,因此这个函数并不能达到我的要求。这时候我使用了AD采样的方式获取随机数,通过采集悬空引脚的电压值,适当处理后即可得到指定范围的随机数。

void ADC1_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	ADC_InitTypeDef ADC_InitTStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1|RCC_APB2Periph_GPIOC,ENABLE);	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);  //设置ADC分频因子6

	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;	
	GPIO_Init(GPIOC, &GPIO_InitStruct);

	ADC_InitTStruct.ADC_ContinuousConvMode = ENABLE;	//连续转换ADC_SoftwareStartConvCmd
	ADC_InitTStruct.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
	ADC_InitTStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //选择SWSATART作为触发事件
	ADC_InitTStruct.ADC_Mode = ADC_Mode_Independent;  //单次模式
	ADC_InitTStruct.ADC_NbrOfChannel = 1;	//顺序进行规则转换的ADC通道的数目
	ADC_InitTStruct.ADC_ScanConvMode = DISABLE; //设置为单通道模式
	ADC_Init(ADC1, &ADC_InitTStruct);	 
	
	ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_239Cycles5);//设置指定ADC的规则组通道,转换序列,采样时间	
	ADC_Cmd(ADC1, ENABLE); 
	ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使用SWSATART开始转换规则通道
	
	ADC_ResetCalibration(ADC1); //使能复位校准 
	while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
	
	ADC_StartCalibration(ADC1); //开启AD校准
	while(ADC_GetCalibrationStatus(ADC1));  //等待校准结束
	
}

//范围0-10
u16 Get_RandNum(void)
{
	u32 tem = 0;
	while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); //ADC_FLAG_EOC = 0 转换未完成
	tem = ADC_GetConversionValue(ADC1); //Returns the last ADCx conversion result data for regular channel
	return tem%10;
} 

七、上电初始化

核心的部分基本到这里都完成了,下面完善一下程序。上电之后我要显示一个给定的“时间值”,显示时分秒之间的冒号。先说明一下,我这里并没有加入DS1302之类的时钟芯片,只是单纯的显示动画,当然如果你有兴趣可以自己去加咯。

1.显示给定时间

上电之后的第一次显示并不需要动画,所以这里调用的是显示数字的函数,locX_now在上面也讲过了它的作用,给画块函数传递偏移量。locX[0]代表了秒的个位,locX[1]代表了秒的十位,以此类推。locX[]数组参数的值我是手动指定的,当然动态指定也是可以的咯。

//记录各个数字 X 轴的起始位置
const u8 locX_start[] = {54, 45, 33, 24, 12, 3};  //秒->分->时

/*
 * 作用: 画系统起始时间
 * 参数: 时分秒,locX[]存储各个数字的起始位置
 */
void DIGIT_DrawStartTime(u8 hour, u8 minute, u8 second, u8 const locX[])
{	
	locX_now = locX[0];			 //定位
	DIGIT_DrawDigit(second%10);  //画秒的个位
	locX_now = locX[1];			 //定位
	DIGIT_DrawDigit(second/10);  //画秒的十位
	locX_now = locX[2];			 //定位
	DIGIT_DrawDigit(minute%10);  //画分的个位
	locX_now = locX[3];		 	 //定位
	DIGIT_DrawDigit(minute/10);  //画分的十位
	locX_now = locX[4];			 //定位
	DIGIT_DrawDigit(hour%10);    //画时的个位
	locX_now = locX[5];			 //定位
	DIGIT_DrawDigit(hour/10);    //画时的十位	
}

2.显示冒号

这里有3种模式可以选择,对应了不同的绘制位置,用途是在随机切换颜色时,通过绘制指定位置的冒号作为一个过渡的动画,使最终效果看起来不那么违和。

/*记录四个点的左上角位置*/
const u8 locXY_dot[4][2] = {18, 4, 18, 9, 39, 4, 39, 9};

/*
 * 作用: 显示时与分,分与秒之间的冒号
 * 参数: locXY[][]: 存储冒号位置的数组
 * 		mode: 0:画两个冒号
 *   		  1:画右边的冒号
 *   		  2:画左边的冒号
 */
void DIGIT_DrawDot(u8 mode, u8 const locXY[][2])
{	
	u8 i, start, end;
	switch (mode)
	{
		case 0: start = 0; end = 4; break;
		case 1: start = 2; end = 4; break;
		case 2: start = 0; end = 2; break;
		default: break;
	}
	for (i = start; i < end; i++) {
		DIGIT_DrawPixelBlock(locXY[i][0], locXY[i][1], segColor);	
		DIGIT_DrawPixelBlock(locXY[i][0]+1, locXY[i][1], segColor);	
		DIGIT_DrawPixelBlock(locXY[i][0], locXY[i][1]+1, segColor);	
		DIGIT_DrawPixelBlock(locXY[i][0]+1, locXY[i][1]+1, segColor);			
	}	
}

3.main函数

#include "delay.h"
#include "sys.h"
#include "lcd.h"
#include "digit.h"
#include "timer.h"
#include "usart.h"
#include "adc.h"

//timer.c中定义
extern vu8 flag_timeupdate; 		//时间更新标志,1代表更新,0未更新
extern u8 hour, minute, second;  	//当前的时间值
extern u8 _hour, _minute, _second;  //上次的时间值
extern u16 srandnum; 				//AD产生随机数

//digit.c中定义
extern const vu16 Color[10];
extern vu16 segColor;
extern u8 locX_now; 				//动态赋值的
extern u8 locY_now;					//只需要赋值一次
extern const u8 locX_start[6]; 	    //记录各个数字 X 轴的起始位置
extern const u8 locXY_dot[4][2];    //记录四个点的左上角位置
	
int main(void)
{		
	delay_init();	    	 //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);	
	uart_init(115200);	 	 //串口初始化为115200
	LCD_Init();
	LCD_Clear(BLACK);

	My_TIM3_Init(9999, 7199);  //1s中断
	ADC1_Init();

	//获取随机数,赋值一个颜色
	srandnum = Get_RandNum();
	segColor = Color[srandnum];
	
	//画冒号
	DIGIT_DrawDot(0, locXY_dot); 

	//画起始时间
	DIGIT_DrawStartTime(hour, minute, second, locX_start);	
	_hour = hour;
	_minute = minute;
	_second = second;
	
  	while(1) 
	{
		//更新时间值
		if (flag_timeupdate == 1) {
			flag_timeupdate = 0;
			
			//画秒
			locX_now = locX_start[0];	
			DIGIT_Morph(second%10, _second%10);	 //秒个位的动画
			
			if (hour != _hour) {	//小时更改的时候,全部段的颜色都要更新
				DIGIT_DrawDigit(second%10);
			}
				
			if (second/10 !=  _second/10) {  
				locX_now = locX_start[1];		
				DIGIT_Morph(second/10, _second/10); //秒十位的动画
			}
			if (hour != _hour) {
				DIGIT_DrawDigit(second/10); //小时更改的时候,全部段的颜色都要更新
				locX_now = 3; 		   		//小时更改的时候,更新冒号的颜色
				DIGIT_DrawDot(1, locXY_dot);
			}
				
			//画分
			if (_minute != minute) {
				
				locX_now = locX_start[2];			//定位到分钟个位位置
				DIGIT_Morph(minute%10, _minute%10);	//更新分钟的个位
				
				if (hour != _hour) {
					DIGIT_DrawDigit(minute%10);
				}
				if (minute/10 != _minute/10) {	
					locX_now = locX_start[3];		
					DIGIT_Morph(minute/10, _minute/10);
				}
				if (hour != _hour) {
					DIGIT_DrawDigit(minute/10);
					locX_now = 3;
					DIGIT_DrawDot(2, locXY_dot);
				}
			}
			
			//画时
			if (_hour != hour) {
				locX_now = locX_start[4]; 		 //定位到小时个位位置
				DIGIT_Morph(hour%10, _hour%10);	 //更新小时的个位
				DIGIT_DrawDigit(hour%10);  		 //更新全部段的颜色			
				locX_now = locX_start[5];		 //定位到小时十位位置
				
				if (hour/10 != _hour/10) {
					DIGIT_Morph(hour/10, _hour/10);
				}
				DIGIT_DrawDigit(hour/10);
			}
			
			//储存本次时间
			_hour = hour;
			_minute = minute;
			_second = second;			
		}
	} 
}

八、最终的效果

颜色是随机变换的,动画还挺炫酷的吧!代码等有不足的地方还请不吝赐教。工程源码:https://github.com/astrozhenght/STM32-morph-num ,欢迎大家访问^_^

猜你喜欢

转载自blog.csdn.net/Meteor_s/article/details/83590258
今日推荐