单片机开发案例源码解析

小学期时单片机资料整理

Posted by Rigel on September 9, 2020

案例源码解析

0x00流水灯代码解析

#include<STC15F2K60S2.H>
#define uchar unsigned char
#define uint unsigned int
sbit led_sel=P2^3;//P2^3----E3   对引脚的声明,大小为一位!  用"^"来声明引脚是C51特定的,指明IO口的引脚位置
uchar led;      //一定要注意,因为P0是8个引脚,所以就8位,所以要定义成char!  不要定义成int,很容易因为移位等操作出错

void Init(){

	P0M1=0x00;
	P0M0=0xff;     //P0口的控制寄存器

	P2M1=0x00;
	P2M0=0x08;

	led_sel=1;	   //看电路图,P23有非门,置1才使二极管阴极为0
}

//STC-ISP“软件延时计算器”生成
void delay_ms(uint n)		
{
	while(n){
	uchar i, j;
	i = 11;
	j = 190;
	do
	{
		while (--j);
	} while (--i);
	n--;
}
}

void Delay200ms()		//@11.0592MHz
{
	unsigned char i, j, k;

	//_nop_();
	//_nop_();
	i = 9;
	j = 104;
	k = 139;
	do
	{
		do
		{
			while (--k);
		} while (--j);
	} while (--i);
}


void main()
{
	Init();
	led=0x01;
	while(1)                          //让程序一直跑,死循环不退出
	{
		P0=led;                     //对端口赋值,设置引脚电平
		//delay_ms(200);
		Delay200ms(); //延时200ms	
		if(led==0x80)                  //也可以用_crol_函数去循环左移,就不用判断0x80的情况了  汇编为rol指令   rotate left
			led=0x01;
		else
			led=led<<1;   //每次就亮一盏灯。所以用移位!
	}
	
}

对应的C51汇编代码:

$include(STC15F2K60S2.H)  

ORG   0000H  //伪指令
        LJMP   START  

;---------------------------------------主程序----------------------------
ORG   0100H          ;此指令用在原程序或数据块的开始,指明此语句后面目标程序或数据块存放的起始地址
//初始化,设置推挽输出
START:  MOV    P0M1, #00000000B      
        MOV    P0M0, #11111111B 
        MOV    P2M1, #00000000B
	    MOV    P2M0, #00001000B
		MOV    P2,   #00001000B
		
MAINLOOP:
        MOV    P0,   #00000001B
		CALL   DELAY  
        MOV    P0,   #00000010B
		CALL   DELAY
		MOV    P0,   #00000100B
		CALL   DELAY
		MOV    P0,   #00001000B
		CALL   DELAY
		MOV    P0,   #00010000B
		CALL   DELAY
		MOV    P0,   #00100000B
		CALL   DELAY
		MOV    P0,   #01000000B
		CALL   DELAY
		MOV    P0,   #10000000B
		CALL   DELAY
        JMP    MAINLOOP  

DELAY:  MOV    R0,   #0fFH          ;R0-R7是寄存器
L1:     MOV    R1,   #04FH
L2:		MOV    R2,   #20H
L3:     NOP
        DJNZ   R2,   L3      ;DJNZ RN,REL 是一条件转移指令,先将工作寄存器Rn中的数减“1”,判断结果是否为“0”,不为“0”程序就跳转到行标为REL的地方执行,否则,为“0”就不转移,继续执行下一条指令。
		DJNZ   R1,   L2
		DJNZ   R0,   L1
		RET        

        END          

IO口P0在头文件中被定义成sfr特殊功能寄存器,对应0x80地址。 因为导入了头文件,所以在汇编也能直接用P0

​ 单片机程序也有堆栈!!

0x01八位数码管动态扫描

工作原理

​ P0口的8位输出分别控制1个LED数码管的7段和一个小数点;而P2.3经反相器U4C控制74HC138的使能信号E3,结合P2.0、P2.1、P2.2这3个位选控制信号确定8个LED数码管中的哪个被点亮;电阻R15~R22为限流电阻。当段选为高、使能信号有效时,对应的LED管将会发光。通过以一定频率扫描位选信号,修改段选信号进行数码管点亮一段时间,从而给人视觉上几个数码管几乎同时显示的效果。

电路图与数码管引脚定义

img

中间是数码管,下面是个译码器,用于位选。

img

P0.0对应A,P0.7对应小数点

流程图

img

相关寄存器设置

P0(8位)和P2.3需要设置成推挽输出,以驱动电路正常发光。涉及寄存器及配置值如下:

​ P2M1=0x00;

​ P2M0=0xff;

​ P0M1=0x00;

​ P0M0=0xff;

源码

#include <STC15F2K60S2.h>
#define uint unsigned int
#define uchar unsigned char

uint i=0;
uchar duanxuan[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f}; //显示0-8
uchar weixuan[]={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07}; //数码管0-7,P2.3是数码管的位选,但是有反相器,所以P2.3置0,数码管才能工作

void Delay(int n)				
{
	while(n--);
}

void main()
{
	P2M0=0xff;				//设置推挽输出
   	P2M1=0x00;
    P0M0=0xff;
    P0M1=0x00;

	while(1)
	{
		for(i=0;i<8;i++)
		{
			P0=0;
			P2=weixuan[i];		//选择数码管位数
			P0=duanxuan[i+1];	//显示对应数值
			Delay(600);
		}
	}
}

P2.3引脚=0时,数码管工作;=1时,LED灯工作

0x02八位数码管动态扫描+流水灯

#include <STC15F2K60S2.h>
#define uint unsigned int
#define uchar unsigned char

uchar arrSeg7Select[] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f}; //显示0-8
uchar arrDigitSelect[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};   //数码管0-7

sbit sbtLedSel = P2 ^ 3;  //ÊýÂë¹ÜÓëLEDµÆÇл»Òý½Å

/*---------±äÁ¿¶¨Òå---------*/
uchar uiLed = 0x01;        //LED灯值寄存
uint  uiLedCnt = 0;        //LED累计计数器
uchar i = 0;               //数码管扫描循环显示

/*---------³õʼ»¯º¯Êý---------*/
void Init()
{
    P0M1 = 0x00;
    P0M0 = 0xff;
    P2M1 = 0x00;
    P2M0 = 0x08;
    sbtLedSel = 0;  //先选择数码管亮
}

//延时n毫秒
void delay_ms( uint n )
{
    while( n )
    {
        uchar i, j;
        i = 11;
        j = 190;
        do
        {
            while ( --j );
        }
        while ( --i );
        n--;
    }
}


void main()
{
    Init();
    while( 1 )
    {
        sbtLedSel = 0;
        for( i = 0; i < 8; i++ )
        {
            P0 = 0;
            P2 = arrDigitSelect[i];     //选择数码管位数
            P0 = arrSeg7Select[i + 1];  //显示对应数值
            delay_ms( 1 );
        }
        uiLedCnt++;
        sbtLedSel = 1;
        P0 = uiLed;                     //LED灯显示
        delay_ms( 1 );                  //延时1ms
        if( uiLedCnt == 50 )   //同一个灯亮50次,再换下一个灯
        {
            if( uiLed == 0x80 )        //rotate
                uiLed = 0x01;
            else
                uiLed = uiLed << 1;    //流水灯左移
            uiLedCnt = 0;
        }
    }
}

  • 在数码管和流水灯切换时,因为共用P0来设置要亮的东西,所以在切换开关后的那一瞬间,有可能会使上一个器件亮下一个器件的P0! 解决办法是:在切换开关之前,设置P0=0,此时两个器件都是不亮的状态,切换开关之后,再设置下一个器件的P0,那么就避免了两器件误亮对方数值的情况。 这其实就是初始化的道理。

0x03 三按键测试

若KEY1被按下,则LED灯L0发光,否则,LO不发光。

若KEY2被按下,则LED灯L1发光,否则,L1不发光。

若KEY3被按下,则LED灯L2发光,否则,L2不发光。

img

按键电路示意图

(三个按键分别是K1、K2、K3)

当按键被按下的时候,电路导通接地,I/O口为低电平;当按键未被下时,电路断开,I/O口保持高电平的。

源码

#include"STC15F2K60S2.H"

//引脚
sbit KEY1 = P3^2;
sbit KEY2 = P3^3;
sbit KEY3 = P1^7;
sbit led_sel = P2^3;

void main()
{
	//推挽输出
	P0M0 = 0XFF;
	P0M1 = 0X00;
	P2M0 = 0X08;
	P2M1 = 0X00;
	
	led_sel = 1;			//让LED发光
	P0 = 0;						//初始化P0,让灯全灭
	
	while(1)
	{
		if(KEY1 == 0)		//key1被按下
			P0 |= 0x01;		//L0发光
		else
			P0 &= ~0x01;	//L0熄灭
		
		if(KEY2 == 0)		
			P0 |= 0x02;		
		else
			P0 &= ~0x02;
		
		if(KEY3 == 0)	
			P0 |= 0x04;		
		else
			P0 &= ~0x04;	
	}
}

只要key被按下不松口开,引脚就一直是低电平,LED灯就一直亮。 while会一直扫描三个按键的情况,每次扫描,按键在按时,对应灯都会亮一下,而while循环很快,所以造成灯一直在亮的现象。

0x04 蜂鸣器测试

蜂鸣器初始状态是没有发声;按下按键1,则蜂鸣器开始发声。再次按下按键1,蜂鸣器停止发声。

​ 蜂鸣器分为有源蜂鸣器和无源蜂鸣器,这里的源特指振荡源;有源蜂鸣器直接加电就可以响起,无源蜂鸣器需要我们给提供振荡源。理想的振荡源为一定频率的方波。

​ 本实验板采用的是无源蜂鸣器,相比与有源蜂鸣器,无源蜂鸣器的优点在于价格便宜,可以通过控制其振动频率来改变发出的声音,因此,无源蜂鸣器可以用于音乐的播放。而有源蜂鸣器的优点在于使用简单,不需要编写“乐谱”。本实验板使用的无源蜂鸣器是电磁式蜂鸣器,电磁式蜂鸣器由振荡器、电磁线圈、磁铁、振动膜片及外壳等组成。接通电源后,接收到的音频信号电流通过电磁线圈,使电磁线圈产生磁场。振动膜片在电磁线圈和磁铁的相互作用下,周期性地振动发声。

无源蜂鸣器只需改变Beep端口的电平,产生一个周期性的方波即可使蜂鸣器发生声音,不同的频率发出的声音不同。其中,ULN2003是一个功放,用于放大电流。电阻R14和电容C21是用来保护电路的。若人为将Beep端口的电平一直置为高电平,在没有保护电路的情况下,容易烧毁电路,但即使有保护电路也应该注意不要将Beep端口长时间置于高电平,这对器件也是有一定损害的。

可通过改变驱动口电平翻转的时间来获得不同声调的声音, 还可通过改变高低电平在一个周期内的比例来获取不同音量的声音.

相关电路

img

​ 图1 无源蜂鸣器电路原图

按键控制电路

img

img

​ 图2 按键控制电路

源码

#include "STC15F2K60S2.H"	

#define uchar unsigned char		
#define uint  unsigned int

sbit beep = P3^4;					//蜂鸣器
sbit key1 = P3^2;					//按键1

bit flag;						   //蜂鸣器开关的标志

void init()
{
	//ÉèÖÃÍÆÍìģʽ
	P3M1=0x00;
	P3M0=0x10;					  //设置P3^4为推挽模式

	TMOD=0x00;					  //设置定时器0,工作模式0,16位自动重装定时器
	TH0=0xff;					  //定时器初值
	TL0=0x03;
	EA=1;						  //打开总中断
	ET0=1;						  //定时器0的中断允许位
	TR0=1;

	flag=0;						  //标志位置1
	P0=0x00;					  //关闭P0端口
	beep=0;						  //蜂鸣器引脚置0,保护蜂鸣器   因为板子复位时,所有引脚为高电平
}


void delay(uint xms)
{
	uchar i;
	for(; xms>0; xms--)						
		for(i=114; i>0; i--)
		{
			;
		}
}	 

void main()
{
	init();
	while(1)
	{
		if(key1==0)         //按下
		{	
			delay(10);						 //延时消抖
			if(key1==0)
			{									  
				while(!key1);       //key1松开才停止循环,去翻转蜂鸣器开关标志
				flag=~flag;		   //开关标志位翻转,用于中断函数内去翻转开关,产生方波
			}
		}
	}
}

/********************
	 定时器中断
********************/
void timer0() interrupt 1               //用定时器来定时产生方波
{
	if(flag)
	{
		beep=~beep;				   //产生方波使得蜂鸣器发声
	}
	else
	{
		beep=0;				      //flag=0时,说明已经再次按下了key1,那么蜂鸣器就该停止发声
	}
}

0x05 实时时钟

通过DS1302芯片、晶振、电池和数码管实现实时时钟的数码管显示。

程序运行效果说明:将程序下载至芯片,数码管会出现实时的时钟,断开USB端口,不给实验板外部供电,时钟依然走秒。

DS1302芯片介绍

​ DS1302是 DALLAS 公司推出的涓流充电时钟芯片内含有一个实时时钟/日历和 31 字节静态 RAM,外接32.768kHz晶振,为芯片提供计时脉冲,在电路板的纽扣电池(位于电路板左下方圆柱体)的持续供电下,实现DS1302的独立时间走动。我们可以直接对DS1302的寄存器进行读写,然后把时分秒等数据显示在实验板的数码管上面。

​ 实时时钟的核心是晶振,晶振频率为32768 Hz 。它为分频计数器提供精确的与低功耗的实基信号。它可以用于产生秒、分、时、日等信息。为了确保时钟长期的准确性,晶振必须正常工作,不能够收到干扰。

​ 那实时时钟的晶振频率为什么是32768Hz? 实时时钟时间是以振荡频率来计算的。故它不是一个时间器而是一个计数器。而一般的计数器都是16位的。又因为时间的准确性很重要,故震荡次数越低,时间的准确性越低。所以必定是个高次数。32768 Hz = 2^15 即分频15次后为1Hz,周期 = 1s;经过工程师的经验总结32768 Hz,时钟最准确。

img

与单片机相连的参考电路如上所示。可见,DS1302作为外部设备,与CPU通过IO口传输数据。很明显与单片机相连只需要三条线。

CE复位引脚(RTC_/RST引脚):输入信号,在读写数据期间,必须为高,因此该引脚有两个功能,一是CE开始控制字访问移位寄存器的控制逻辑,二是提供结束单字节或多字节数据传输的方法。

I/O引脚:三线接口时的双向数据线。 只能通过该IO引脚来往ds1302芯片内读写数据,每次1位

SCLK:串行时钟,输入,控制数据的输入与输出。

img

DS1302对应的时序:单个字节读:在前8个SCLK时钟周期内,上升沿写入控制字,在后8个SCLK时钟周期内,下降沿读取数据字;均从最低位开始。

img

单个字节写:在前8 个 SCLK 时钟周期,上升沿写入控制字,在后 8 个 SCLK 时钟周期,上升沿写入数据字;均从最低位开始。

img

DS1302引脚

sbit RTC_sclk=P1^5; //时钟线引脚

sbitRTC_rst=P1^6; // CE线引脚

sbit RTC_io=P5^4; //实时时钟的数据线引脚

DS1302相关寄存器

img

DS1302内部一共有12个寄存器,其中七个寄存器与时钟,日历有关,就是上表中的前七个,寄存器中的数据是二—-十进制的BCD码。

其中:

①命令字:

位7:必须是逻辑1,如果它为0,则不能把数据写入到DS1302中。

位6:如果为0,则表示存取日历时钟数据,为1表示存取RAM数据;此程序中没有涉及RAM存取数据,所以位6置为0.

位5~位1(A4~A0) :指示操作单元的地址; 这里的地址是ds1302芯片内部的ram,共31字节,用于存储时间信息,控制信息等 与单片机的ram无关 所以源码中的define的0x80,0x81等,就是对应功能的控制字!而不是单片机的ram地址!! 可以算一下,0x80满足控制字的格式,指向ds1302的0x00ram地址

位0 :为0写操作,为1读操作。

img

​ 命令字始终从LSB(位0)开始输入(最低有效位)。

​ 讲一下为什么命令字的D7一定是1,。大家都知道单片机端口结构吧,端口置1后,灌电流的通道关闭,引脚上若进入数据,数据会保持不变的,也就是说数据会不会变化,不管是写还是读,都是正确的。若端口置0,则灌电流通道打开,引脚上进入的数据若是1,则通过该通道流走,变成0,举个例子,置0后,DS1302输出数据到该引脚,则该引脚读到的数字全是0。

②秒寄存器:

位7:时间暂停位,为1时钟振荡器停止工作,为0时,时钟振荡器启动;

初始化时,要启动时钟振荡器,在禁止写保护的情况下通过如下语句实现:

temp=ReadDS1302(DS1302_SECOND_READ)&0x7f;

WriteDS1302(0x80,temp);//晶振开始工作

③小时寄存器:

位7:12或24小时工作模式选择位,为1时12小时工作模式,此时位5为AM/PM位,低电平对应 AM,高电平对应PM;在 24 小时模式下,位5为第二个10小时位表示(20~23 时);

④写保护:

位7: WP 是写保护位,工作时除WP 外的其他位都置为0, 写操作之前WP必须为0,当WP 为1时不能进行写操作。

WriteDS1302(0x8E,0x00); //禁止写保护位

WriteDS1302(0x8E,0x80); //写保护位置1 是往ds1302芯片的ram对应位置写1!!

 ●控制寄存器的位7是写保护位。  ●前七个位(位0到6)被强制为0,并且在读取时始终为0。  ●在对时钟或RAM进行任何写操作之前,位7必须为0。  ●为高电平时,写保护位可防止对其他任何寄存器的写操作。  ●初始上电状态未定义。 因此,在尝试写入设备之前,应先清除WP位。

疑惑

命令字写在哪?? 写在控制寄存器还是ds1302ram?? 和wp所在寄存器重叠?

wp应该是防止对其他寄存器的写入,0是能写,1是不能写 写命令字受写保护的影响吗?

​ 如果不受影响,那么说得通,wp就是防止对时钟即ram数据进行写入。

查阅ds1302数据手册:

image-20200907211801741

可见,命令字8位会传到command and control logic模块去进行译码等,并没有进行写入操作,所以wp是指对ram和时钟寄存器等的写入,不包含命令字!!

源码

img

/**********************
进行实时时钟模块测试的例程
采用定时器0,方式1通过数码管对时间进行显示
***********************/

/**********************
基于STC15F2K60S2系列单片机C语言编程实现
使用如下头文件,不用另外再包含"REG51.H"
***********************/
#include"STC15F2K60S2.H" //头文件
#include"intrins.H" //头文件

#define uchar unsigned char 	   //宏定义
#define uint unsigned int

/***********时分秒写寄存器**************/
#define DS1302_SECOND_WRITE 0x80	//不是sfr,是ds1302对应功能的控制字
#define DS1302_MINUTE_WRITE 0x82
#define DS1302_HOUR_WRITE   0x84 
#define DS1302_WEEK_WRITE   0x8A
#define DS1302_DAY_WRITE    0x86
#define DS1302_MONTH_WRITE  0x88
#define DS1302_YEAR_WRITE   0x8C

/***********时分秒读寄存器**************/
#define DS1302_SECOND_READ  0x81
#define DS1302_MINUTE_READ  0x83
#define DS1302_HOUR_READ    0x85 
#define DS1302_WEEK_READ    0x8B
#define DS1302_DAY_READ     0x87
#define DS1302_MONTH_READ   0x89
#define DS1302_YEAR_READ    0x8D

/**********************
引脚别名定义
***********************/
/********DS1302*******/
sbit Rtc_sclk = P1^5;			//时钟线引脚,控制数据的输入与输出
sbit Rtc_rst  = P1^6;			//CE线引脚,读、写数据时必须置为高电平
sbit Rtc_io   = P5^4;			//实时时钟的数据线引脚
/********数码管显示******/
sbit Led_sel = P2^3;		   //流水灯和数码管选通引脚
sbit Sel0    = P2^0;		   //Sel0、Sel1、Sel2三位二进制进行数码管位选0-7  	
sbit Sel1    = P2^1;
sbit Sel2    = P2^2;

/**********************
变量定义
***********************/
uchar duanxuan[]={ 					  //数码管显示所要用的段码
 
                0x3F,  //"0"
                0x06,  //"1"
                0x5B,  //"2"
                0x4F,  //"3"
                0x66,  //"4"
                0x6D,  //"5"
                0x7D,  //"6"
                0x07,  //"7"
                0x7F,  //"8"
                0x6F,  //"9"
                0x77,  //"A"
                0x7C,  //"B"
                0x39,  //"C"
                0x5E,  //"D"
                0x79,  //"E"
                0x71,  //"F"
                0x76,  //"H"
                0x38,  //"L"
                0x37,  //"n"
                0x3E,  //"u"
                0x73,  //"P"
                0x5C,  //"o"
                0x40,  //"-"
                0x00,  //熄灭
                0x00  //自定义
 
                };

uchar weixuan[]={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07};	   //数码管位选数组
uchar flag;					 //所选择点亮的数码管0-7标志位
uchar temp;					 //要写入到DS1302的数据

/**********************
定义时间结构体
***********************/
typedef struct __SYSTEMTIME__
{
	uchar Second;
	uchar Minute;
	uchar Hour;
	uchar Week;
	uchar Day;
	uchar Month;
	uchar Year;
}SYSTEMTIME; 			//定义的时间类型

SYSTEMTIME t;			

/*******************************
 * 函数名:Delayms
 * 描述  :毫秒延时程序
 * 输入  :延时i毫秒
 * 输出  :无
 *******************************/
void Delayms(char i) 			
{
	while(i--)
	{	
		int n=500;
		while (n--)
	    {
	        _nop_();
	        _nop_();
	        _nop_();
	        _nop_();
	        _nop_();
			_nop_();
	    }
	}
}

/**********************
函数名称:Ds1302_write
功能描述:Ds1302写入一字节
入口参数:uchar 要写入的数据 
***********************/
//单个字节写:在前 8 个 SCLK 时钟周期,上升沿写入控制字,在后 8 个 SCLK 时钟周期,上升沿写入数据字;均从最低位开始。   在这里由代码生成sclk的波形,来控制数据写入
void Ds1302_write(uchar temp) 		 //temp:要写入的数据
{
	uchar i;
	for(i=0;i<8;i++)				 //循环8次写入一个字节
	{
		Rtc_sclk=0;					//时钟脉冲拉低
		Rtc_io=(bit)(temp&0x01);	 //取最低位数据
		temp>>=1;					 //右移一位
		Rtc_sclk=1;					  //上升沿输入 	
	}
}
/**********************
函数名称:Ds1302_read
功能描述:Ds1302读取一字节
入口参数:无
出口参数:返回uchar 读出的数据
***********************/
uchar Ds1302_read()	 
{
	 uchar i, dat;
	 for(i=0;i<8;i++)			
	 {
	 	Rtc_sclk=0;				//时钟脉冲拉低	 下降沿时读取
	 	dat>>=1;				//要返回的数据右移一位	 
		if(Rtc_io==1)			//数据线为高,证明该位数据为1
		dat|=0x80;				//要传输数据的当前值置为1,若不是,则为0
		Rtc_sclk=1;				//拉高时钟线 
	 }
	 return dat;				//返回读取出的数据
}


/**********************
函数名称:WriteDS1302
功能描述:向Addr对应的寄存器中写入数据Data
入口参数:uchar Addr 寄存器地址, uchar Data 要写入寄存器的数据
***********************/
void WriteDS1302(uchar Addr, uchar Data) 	   //Addr: DS1302地址,Data: 要写的数据
{
    Rtc_rst = 0;						 //初始CE线置0
    Rtc_sclk = 0;						 //初始时钟线置0
    Rtc_rst = 1;						 //初始CE线置为1,传输开始
	Ds1302_write(Addr);					 // 地址,传输命令字   即控制字,写到控制字寄存器
	Ds1302_write(Data);			   		 // 写1Byte数据 
	Rtc_rst = 0;   						 //读取结束,CE置0,结束数据的传输
    Rtc_sclk = 1;						 //时钟线拉高
}

/**********************
函数名称:ReadDS1302
功能描述:读出Addr对应的寄存器中的数据
入口参数:uchar cmd 寄存器地址
***********************/
uchar ReadDS1302(uchar cmd)
{
    uchar Data;
    Rtc_rst = 0;			  //初始CE线置为0
    Rtc_sclk = 0;			  //初始时钟线置为0     就算之前sclk为高电平,产生下降沿,io口读数据也没事   因为读数据只是ds1302把数据传到io口,我们并没有进行接收,所以没事
    Rtc_rst = 1;			  //初始CE置为1,传输开始
    Ds1302_write(cmd);        // 地址,传输命令字 
    Data =Ds1302_read();      // 读1Byte数据	  
	Rtc_rst = 0;			  //读取结束,CE置为0,结束数据的传输
    Rtc_sclk = 1; 			  //时钟线拉高       write/read完sclk都是=1
    return Data;			  //返回得到的数据
}

/**********************
函数名称:DS1302_GetTime_BCD
功能描述:读出DS1302中时、分、秒、年、月、日寄存器中对应的数据
***********************/

SYSTEMTIME DS1302_GetTime()
{
	SYSTEMTIME Time;
 	uchar ReadValue;
 	ReadValue = ReadDS1302(DS1302_SECOND_READ);       //1字节
 	//将BCD码转换成十进制
 	Time.Second=((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);		  //读取秒寄存器中的数据
	ReadValue=ReadDS1302(DS1302_MINUTE_READ);
 	Time.Minute = ((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);		  //读取分寄存器中的数据
 	ReadValue = ReadDS1302(DS1302_HOUR_READ);
 	Time.Hour = ((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);		  //读取时寄存器中的数据
	ReadValue = ReadDS1302(DS1302_DAY_READ);
 	Time.Day = ((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F); 		  //读取日寄存器中的数据
 	ReadValue = ReadDS1302(DS1302_WEEK_READ);
 	Time.Week = ((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);		  //读取周寄存器中的数据
 	ReadValue = ReadDS1302(DS1302_MONTH_READ);
 	Time.Month = ((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F);		  //读取月寄存器中的数据
 	ReadValue = ReadDS1302(DS1302_YEAR_READ);
 	Time.Year=((ReadValue&0x70)>>4)*10 + (ReadValue&0x0F); 			  //读取年寄存器中的数据

	return Time;
}

/**********************
函数名称:Init
功能描述:完成各部分功能模块的初始化
入口参数:无 
出口参数:无
备注:
***********************/
void Init()
{
 	P2M0=0xff;				 //设置推挽模式
   	P2M1=0x00;
    P0M0=0xff;
    P0M1=0x00;

	Led_sel=0;				 //选通数码管

 	TMOD=0x01;				//定时器0,方式1
	EA=1;					//打开总的中断
	ET0=1;					//开启定时器中断	
 	TH0=(65535-1000)/256;	//设置定时初值
	TL0=(65535-1000)%256;
	TR0=1;					//启动定时器
}

void Initial_DS1302(void)	
{
 	WriteDS1302(0x8E,0x00);			 //禁止写保护位,因为初始化要进行写操作	
	temp=ReadDS1302(DS1302_SECOND_READ)&0x7f ;   //第7位置0,启动晶振
	WriteDS1302(0x80,temp);		   	//晶振开始工作
	WriteDS1302(0x8E,0x80);			 //写保护位置1,现在开始不能写了  
}	

/********************************定时器中断处理程序************************************************/
void timer0() interrupt 1	  //把数码管的显示提到中断里面来了
 {
 	TH0=(65535-1000)/256;	  //重新填充初值  定时1ms
	TL0=(65535-1000)%256;

	/********显示程序*******/
	flag++;						  //初值为0
	if(flag==8)					  
	flag=0;
	P0=0x00;					//先初始化,什么都不亮		   
	P2=weixuan[flag];
	switch(flag)
	{
		case 0:P0=duanxuan[t.Hour/10];break; 
	    case 1:P0=duanxuan[t.Hour%10];break; 
		case 3:P0=duanxuan[t.Minute/10];break; 
		case 4:P0=duanxuan[t.Minute%10];break; 
		case 6:P0=duanxuan[t.Second/10];break; 
		case 7:P0=duanxuan[t.Second%10];break; 
		default:P0=duanxuan[22];break;					  //duanxuan[22]所对应的字符为“-”
	}	
}

void main()
{
	Init();
	if (ReadDS1302(DS1302_SECOND_READ)&0x80)			  //判断掉电之后时钟是否还在运行
	Initial_DS1302();									  //若没有运行,则进行DS1302初始化
	while(1)
	{
		Delayms(300);
		t=DS1302_GetTime();								 //取时间各数据放到结构体t中
	}	

}

​ main函数的while循环每300ms去读取ds1302内的时间数据,然后通过定时器,每1ms发生中断去让数码管显示t中的时间数据。

​ 在该程序中,只在Initial_DS1302函数中用到了writeDS1302,去启动时钟。

0x06导航按键测试

​ 利用STC15F2K60S2芯片的ADC口对来自导航按键不同方向的电压值进行采集,并将采集后的转换结果用数码管显示。程序主要是对ADC进行操作,并将寄存器相应位取出分别用8位二极管和数码管显示。第一位数码管显示8位转换结果中高三位值,最后两位数码管显示后低位值。数码管下方的发光二极管与数码管对应显示。

​ 程序运行效果说明:根据用户对导航按键的操作情况,相应产生的ADC转换结果,在数码管最高位(命名Seg0)显示转换结果高三位,点亮对应的发光二极管L7—L5。数码管后两位(命名Seg6-Seg7)显示转换结果低五位,点亮对应的发光二极管L4—L0。具体显示情况如表1所示。注:实际数值如果有一点误差,是由于电阻的工艺使得电阻会有一定的误差,关系不大,我们在做导航按键的判断时,都只是取高三位的值,也就是数码管Seg0的值。

image-20200907222352600

导航按键电路及工作原理说明

img

导航按键在上图的标注为MINI_KEY5,首先可以看到左边有多个不同阻值的电阻,然后是分别是1、2、3、4、5、6共6个接口。导航键的开关接到不同接口上由于总阻值不一样,所以加在这两点的电压肯定不同。从而可以根据这个原理,与A/D转换器配合,可以判断哪个方位被按下,获取按下后A/D转换的结果。

图片右上角KEY3是按键的电压输出,是模拟信号

这张图标注了STC板子上每一个接口的名称,当你想用这个某个端口的时候就在这张图上面找对应的名字就好。

image-20200907234150860

发现前面还有一些字母,如ADC7,说明这个端口可以作为A/D转换的输入。

AD数据采集电路及采集步骤说明

image-20200907235259904

image-20200908002518306

ADC数据采集的步骤:

l 将ADC0~7的模拟量送到比较器中,用DAC(数/模转换器)转换的模拟量与输入的模拟量通过比较器进行比较

l 转换结束后,将比较结果放入转换结果寄存器(ADC_RES和ADC_RESL)

l 同时,需要将ADC_FLAG软件清零

l 注意硬件会自动将ADC_START清零,如果需要进行下一次转换,则需要将ADC_START置位

特别说明:

(1)数码管所显示的ADC转换结果并不是电压值,而是电压进行转换后所得的一个值。如果需要实际的电压值可以参照STC15F2K60S2数据手册的760页上面的公式进行计算得出。

(2)ADC转换结果是一个10位数据,若ADRJ=0,则ADC_RES存放高八位,ADC_RESL存放低两位。若ADRJ=1,则ADC_RESL存放高八位,ADC_RES存放低两位。本案例采用的是ADRJ=0,而且只取了高八位结果。

image-20200908000942111

如图是AD转换需要用到的寄存器:

image-20200908110849997

首先配置好我们AD采集信息的输入口,那么要写入信息,就要用到与此相关的寄存器,也就是P1ASF。它有八个口可以作为输入。而我们是采集导航键的,刚刚说了导航键的输出对应的是P1.7口。因为P1ASF可设置为:P1ASF = 0x80(1000 0000)。如图:(B7-B0哪位为1就表示选用哪个端口作为数据输入源)

image-20200908110931448

信息源有了,接下来是打开我们的AD转换器。

跟打开定时器类似,也要用到控制寄存器,也就是ADC_CONTR啦。先来看看它的结构:

image-20200908111145802

image-20200908111228714

所以这个ADC_CONTR可以设置为:ADC_CONTR = 0x87 (1000 0111) ADC_FLAG需要特别注意,一定要软件清0,否则就只能转换一次。ADC_STATR此处不需要设置,你可以手动开启。ADC_CONTR也是可为寻址的,因此可以单独设置某一位的值。

那么转换后的结果放在哪呢?寄存器呗!AD转换有用来存储结果的寄存器,称为结果寄存器。如图:

image-20200908111512243

最后是中断相关的寄存器

image-20200908111637404

和定时器一样,因为AD转换器在CPU外部,所以AD转换完成会发生中断告知CPU转换完成!

image-20200908114247756

查数据手册,可以知道中断表。 学校的板子是51的增强版,有3个定时器。

AD中断号为5

源码

/**************************
说明:导航按键的例程
修改:消除数码管显示中的残影
**************************/

/**************************
使用头文件如下:
**************************/

#include "STC15F2K60S2.H" 
#include <intrins.h>			  //_cror_(); 

#define uint unsigned int
#define ulint unsigned long
#define uchar unsigned char
#define ADC_FLAG 0x10

/**************************
引脚别名定义如下:
**************************/

sbit SEL0 = P2^0;
sbit SEL1 = P2^1;
sbit SEL2 = P2^2;
sbit LED_SEL = P2^3;

/**************************
定义变量如下:
**************************/

uint global = 0;
uint date_h = 0;   
ulint sum = 0;
uint x = 0;         //全局变量存储AD转换值
uint y = 0;
uchar seg_flag;
//0123456789- 
char duanxuan[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
//选择哪一位数码管
uchar weixuan[] = {0x00,0x06,0x07};

//初始化系统
void InitSYS(){
	
	//P0口推挽输出,用于给数码管和发光二极管增加电流
	P0M1 = 0x00;
	P0M0 = 0xff;
	//P2.3(即LED_SEL),P2.2,P2.1为推挽输出
	P2M1 = 0x00;
	P2M0 = 0x0E;

	seg_flag = 0;
	
	TMOD = 0x00;		//定时器0,工作方式1;
	EA = 1;				//打开总中断;
 	TH0 = (65535-1000)/256;	   	//定时器初值      1ms
	TL0 = (65535-1000)%256;
	ET0 = 1;			//打开定时中断;
	TR0	= 1;			//启动定时器;	
}

//初始化ADC
void InitADC()
{
	P1ASF = 0x80;			//P1.7作为A/D使用
	ADC_RES = 0;			//清零ADC结果寄存器
	ADC_CONTR = 0X8F;		//打开ADC电源  540个时钟周期转换一次 
	CLK_DIV = 0X00;			//ADRJ = 0	  ADC_RES存放高八位结果
	EADC = 1;				//允许ADC中断
	PADC = 1;				//ADC中断优先级为1	
}

//显示高三位和低五位
void HighThree()
{
	 x = date_h&0xE0;					//将八位转换结果的低五位清零
	 x = _cror_(x,5);					//将高三位移到低三位
	 y = date_h&0x1F;					//对原值取低5位,将八位转换结果的高三位清零
}

void timer0() interrupt 1 {	
	
	seg_flag++;	                          //每1ms扫描一次,各位轮流显示
	if(seg_flag==4)
		seg_flag = 0;
	P0 = 0x00;	              //每次首先初始化,什么都不亮
	switch(seg_flag) {
		case 0:
			LED_SEL = 0;				//选择数码管显示
			P2 = weixuan[seg_flag];
			P0 = duanxuan[x];			//显示ADC转换结果高三位的十进制值
			break;	
		case 1:
			LED_SEL = 0;				//选择数码管显示
			P2 = weixuan[seg_flag];
			P0 = duanxuan[y/10];		//显示ADC转换结果低五位的十进制值的高位
			break;
		case 2:
			LED_SEL = 0;				//选择数码管显示
			P2 = weixuan[seg_flag];
			P0 = duanxuan[y%10];		//显示ADC转换结果低五位的十进制值的低位
			break;
		case 3:
			LED_SEL = 1;				//选择发光二极管显示
			P0=date_h;                 //LED灯直接显示整个8位
			break;
	}
	
}

//AD中断
void adc_isr() interrupt 5    //只要ADC_CONTR的ADC_POWER=1(即打开AD电源),且每次AD中断中设置ADC_START=1,AD转换是一直在转换的,就算不按导航按键,数码管也会显示当前电压值。所以几乎是实时刷新当前电压值,刷新频率由控制寄存器的speed设置。 若想让AD停止,把Power位=0即可。
{
	IE=0x00;				  //关闭所有中断,防止数据没有采集完就进入新的中断(包括定时器的中断! 反正AD中断函数不长,耽误不了多少数码管显示时间)
	ADC_CONTR&=~0X10;		  //将ADC_FLAG位清零
	
	global++;					
	if(global>1000)			  //将采集1000次后的数据求出平均值
	{
		date_h=(sum+500)/1000;//四舍五入

		HighThree();
		global=0;
		sum=0;
	}
	sum+=ADC_RES;           //每次中断,累加
	ADC_CONTR|=0X08;		 //将ADC_START进行软件置位,开启下一次转换
	IE=0xa2;				 //再次开启中断
}

void main()
{
	InitSYS();
	InitADC();

	while(1)     //直接让定时器和AD跑,不断扫描
		;
}

x,y等的类型应该改成uchar,不然int直接赋给P0 sfr,大小不符,虽然也能跑

0x07 非易失性存储

本程序是对24C02存储页面的0x00地址写入可变化的数据,然后读取数据,并显示在数码管上。

位数码管默认显示0。按下key3,要写入数据的地址加1。按下key2要写入的数据加1。按下key1,向存储器写入数据并读取数据,并显示在数码管上。数码管左边2位(第一、第二位)是写入的地址,数码管中间两位(第四、第五位)是写入的数据,数码管右边两位(第七、第八位)是显示从非易失存储器读取的数据。

工作原理

非易失性存储器(nonvolatile memory)是掉电后数据能够保存的存储器,它不用定期地刷新存储器内容。这包括所有形式的只读存储器(ROM),像是可编程只读存储器(PROM)、可擦可编程只读存储器(EPROM)、电可擦除只读存储器(EEPROM)和闪存。在许多常见的应用中,微处理器要求非易失存储器来存放其可执行代码、变量和其他暂态数据(例如采集到的温度、光照等数据)。

24C02工作电路及其工作原理

img

​ 24C02模块电路

本实验采用24C02芯片, 24C02通过IIC_SCL和IIC_SDA与单片机相连,单片机以IIC总线的方式对24C02进行读写。24C02是一个2K位(即256字节)串行E2PROM,内部含有256个8位字节。

(1)管脚配置

图片1

(2)管脚描述

图片1

A0A1A2是选择24c02芯片的序号

(3)寻址方式

寻址信号由一个字节构成,高7位为地址位,最低位为方向位,用以表明主机与从器件的数据传送方向。方向位为0,表明主机接下来对从器件进行写操作;方向位位1,表明主机接下来对从器件进行读操作。

img

A0,A1和A2对应器件的管脚1,2和3;

a8,a9和a10对应存储阵列地址字地址;

(4)读/写时序

写一个字节时序

img

读一个字节时序

img

如图,写一个字节时序,第一个DEV SEL是器件选择信号(即上面寻址方式描述的),器件选择的范围为(000~111),总共可以选择8个24C02芯片器件。但是本实验只用到了1个24C02芯片,所以对应的器件管脚地址A2A1A0为000。第二个信号BYTEADDR是地址信号,表示要对哪一个地址进行操作,第三个DATA IN则是写入的数据。而读操作则是多了一步,DEV SEL和BYTE ADDR后,还有一个DEV SEL,但此信号的最后一位为高,表示是读操作,随后从机会把相应地址的数据发送给主机。

image-20200908235503993

​ 24CXX支持I2C总线传输协议

I2C总线介绍

I2C(Inter-Integrated Circuit)总线是由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备。是微电子通信控制领域广泛采用的一种总线标准。它是同步通信的一种特殊形式,具有接口线少,控制方式简单,器件封装形式小,通信速率较高等优点。I2C总线硬件结构图如下:

img

SCL是时钟线,SDA是数据线

I2C总线信号包括有,启始信号,停止信号和应答信号,在程序用分别用函数void start()、void stop()、void respons()表示。24C02的存储空间为2K,每一次写和读操作都只能操作已选定的对应24C02芯片的地址数据。要切换操作的芯片,需要重新发送寻址信号,在void write_add(uchar addr,uchar date)函数中,第一个寻址信号writebyte(0xa0),已经固定了本程序只能在第0个芯片进行操作(注:0xa0化为二进制为1010000,其中,前4位1010是固定不能改变的,最后一位0代表写操作,1代表读操作,而中间三位则是代表不同芯片地址的编号),若要改变需要操作的芯片,则只需改变中间三位即可。

(1)I2C位传输

数据传输:SCL为高电平时,SDA线若保持稳定,那么SDA上是在传输数据bit;若SDA发生跳变,则用来表示一个会话的开始或结束

数据改变:SCL为低电平时,SDA线才能改变传输的bit 因为SCL为高电平时,SDA跳变表示开始和结束信号

I2C学习笔记 - dp - dp: 生活的脚步,进步的点滴...

(2)I2C开始和结束信号

开始信号:SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。

结束信号:SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。

I2C学习笔记 - dp - dp: 生活的脚步,进步的点滴...

(3)I2C应答信号

主设备每发送完8bit数据后等待从设备的ACK。

即在第9个clock,从IC发ACK,SDA会被拉低。 此时SCL为高电平 注意:ACK是IC发给Master!! 所以不会误判为开始信号!! 下一个字节的传输会在SCL为低电平的时候进行SDA转换!!

若没有ACK,SDA会被置高,这会引起Master发生RESTART或STOP流程。

image-20200908230415267

程序流程图

img

源码

#include <STC15F2K60S2.h>
#define uint unsigned int
#define uchar unsigned char
#define FOSC 11059200L	//晶振频率
#define T_ms 0.1	 	//定时时间为0.1ms
#define NMAX_KEY 10 	
/*位声明*/
sbit Key1=P3^2;		  	//按下key1,向存储器写入数据并读取该地址的数据,显示在数码管上。
sbit Key2=P3^3;		  	//按下key2,要写入的数据加1
sbit Key3=P1^7;		 	//按下key3,要写入的地址加1					  
sbit led=P2^3;	      	//LED灯与数码管切换引脚
sbit SDA=P4^0;		  	//I2C总线的数据线
sbit SCL=P5^5;		  	//I2C总线的时钟线
/*变量定义*/
uchar duan[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; //数码管段选,显示0-f
uchar wei[]={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07};	  //数码管位选
uchar flag1;				//数码管循环扫描变量
uchar write_addr;			//写入地址
uchar write_date; 			//写入数据
uchar read_date;  	 		//读出数据
/*按键计数消抖变量*/
uchar G_count;				//定时器0中断计数值
uint Key1_count;			//KEY1键在1ms内达到低电平的次数
uint Key2_count;			//KEY2键在1ms内达到低电平的次数
uint Key3_count;			//KEY3键在1ms内达到低电平的次数
uint Key_count;				//由100递减到0
bit flg_1ms;				//表示1ms时间到
bit Key1_C;/*key1当前的状态*/
bit Key1_P;/*key1前一个状态*/
bit Key2_C;/*key2当前的状态*/
bit Key2_P;/*key2前一个状态*/
bit Key3_C;/*key3当前的状态*/
bit Key3_P;/*key3前一个状态*/

void SMG1(uchar date1,uchar date2,uchar date3) //数码管显示函数
{	
	flag1++;
	if(flag1==8)
	{
		flag1=0;
	}
	P0=0x00;
	P2=wei[flag1];						
	switch(flag1)							
	{									    
		case 0:P0=duan[date1/16];break; 
	    case 1:P0=duan[date1%16];break; 	
		case 2:P0=0x40;break;
		case 3:P0=duan[date2/16];break; 
		case 4:P0=duan[date2%16];break; 		
		case 5:P0=0x40;break; 		
		case 6:P0=duan[date3/16];break; 
		default:P0=duan[date3%16];break; 
	}
} 
void KEY_init()	//按键消抖模块初始化
{	
	G_count=0;
	flg_1ms=0;	
	Key1_C=1;	/*key1当前的状态*/
	Key1_P=1;	/*key1前一个状态*/
	Key2_C=1;	/*key2当前的状态*/
	Key2_P=1;	/*key2前一个状态*/
	Key3_C=1;	/*key3当前的状态*/
	Key3_P=1;	/*key3前一个状态*/
	Key1_count=0x80+NMAX_KEY/3*2;
	Key2_count=0x80+NMAX_KEY/3*2;
	Key3_count=0x80+NMAX_KEY/3*2;
	Key_count=NMAX_KEY;
}
void delay() 		//延时4us
{
	;;	
}
void IIC_init()		//I2C总线初始化
{
	SCL=1;
	delay();		//让SCL稳定
	SDA=1;			//为开始信号做准备, 因为开始信号是SDA高到低
	delay();	
}

void start()	 	//主机启动信号
{
 	SDA=1;
	delay();		//让电平稳定
	SCL=1;
	delay();
	SDA=0;	//重点是在start函数内造成的SDA高到低! 之前的SDA是高是低都无关,因为这里是开始信号,这里声明开始传输
	delay(); 	
}
void stop()		 	//停止信号
{
 	SDA=0;
	delay();
	SCL=1; 			//先让SDA稳定,再把SCL置1
	delay();
	SDA=1;
	delay();
}
void respons()	 	//从机应答信号
{
	uchar i=0;
	SCL=1;
	delay();
	while(SDA==1&&(i<255))  //表示若在一段时间内没有收到从器件的应答则
		i++;				//主器件默认从期间已经收到数据而不再等待应答信号。 这里没有考虑从机未接收到字节的情况
	SCL=0;
	delay();
}
void writebyte(uchar date) //对24C16写一个字节数据
{
	uchar i,temp;
	temp=date;
	for(i=0;i<8;i++)
	{
		temp=temp<<1;
		SCL=0;
		delay();
		SDA=CY;        //进位标志,即左移出来的位
		delay();
		SCL=1;
		delay();
	}
	SCL=0;
	delay();
	SDA=1;
	delay();	
}
uchar readbyte()  			//从24C16读一个字节数据
{
	uchar i,k;
	SCL=0;
	delay();
	SDA=1;
	delay();
	for(i=0;i<8;i++)
	{
		SCL=1;			//接受过程中SCL=1,  不然SCL=0时IC不工作
		delay();
		k=(k<<1)|SDA;   //SDA就1位     这里直接接收IC从SDA线传过来的数据
		delay();
		SCL=0;
		delay();
	}
	delay();
	return k;	
}
void write_add(uchar addr,uchar date)  //对24C16的地址addr,写入一个数据date
{
	start();				//发生开始信号
	writebyte(0xa0);		// 10100000   最后一位=0,表示本次是写操作  A3A2A1=0				
	respons();				//接收ACK
	writebyte(addr);
	respons();
	writebyte(date);
	respons();
	stop();							
}
uchar read_add(uchar addr) //从24C16的addr地址,读一个字节数据
{	
	uchar date; //读字节为了和写字节工作模式统一,所以都要传输3个字节过去,之后SDA才会把数据传输给CPU
	start();
	writebyte(0xa0);	//读和写的第一个DEV SEL最低位都是0,表示把地址写进去
	respons();
	writebyte(addr);
	respons();	
	start();			//需要再一个start信号,表示要输入DEV SEL
	writebyte(0xa1);
	respons();
	date=readbyte();
	stop();
	return date;
}
void IO_init()		 //IO口初始化,变量初始化
{	
	P2M1=0x00;		//设置P0口和P2^3推挽输出
	P2M0=0x08;
	P0M1=0x00;
	P0M0=0xff;		//按键无需推挽
	led=0;		   	//关LED显示,开启数码管
	P0=0x00;		//什么都不显示
	write_addr=0x00;		//写入地址初始化
	write_date=0x00;		//写入数据初始化	
}
void Timer0_Init()		 //计时器0初始化
{
	TMOD=0x00;   		//计时器0工作方式0,16位自动重装计数
	AUXR=0x80;   		//1T模式,T0x12=1,
	EA=1;		 		//开总中断
	ET0=1;				//开定时器0中断
	TH0=(65536-T_ms*FOSC/1000)/256;//给定时器赋初值
	TL0=(65536-T_ms*FOSC/1000);
	TR0=1;			   	//启动定时器
}    

void Timer_T0() interrupt 1	 //定时器0中断函数		
{	
	G_count++;
	if(G_count==10)
	{
		flg_1ms=1;		//1ms时间到,fla_1ms=1     此方法也可用于数码管单位闪烁
		G_count=0x00;
	}
	SMG1(write_addr,write_date,read_date); //在定时器中短中调用数码管显示函数						  
}
int main()				//主函数
{
	IO_init();		 	//IO口初始化
	Timer0_Init();		//定时器0初始化
	KEY_init();			//按键消抖模块初始化
	IIC_init();		    //IIC总线初始化
	while(1)
	{
		while(flg_1ms)  //1ms时间到 判断按键状态           计数消抖法
 		{
			flg_1ms=0;
			read_date=read_add(write_addr);	//读出地址为write_addr的数据
			if(Key1==0)
			Key1_count--;
			if(Key2==0)
			Key2_count--;
			if(Key3==0)    //按键是按下状态
			Key3_count--;
			Key_count--;   //总的次数直接减1,10ms减完  就是用作计数,每1ms进行一次扫描		
		 	if(Key_count==0)//10次完了   10ms
			{
				if(Key1_count<0x80)    //即“按下”次数超出抖动最大范围,那么说明确实按下了
				{
					Key1_C=0;
					if(Key1_P==1)	//下降沿(按键做动作)  排除一直按下没松开的情况
					{
						Key1_P=0;
						write_add(write_addr,write_date);//向地址write_addr写入数据		
					}
				}
				if(Key1_count>=0x80)      
				{
					Key1_C=1;
					if(Key1_P==0)      //因为每10ms就会来判断一下,所以在按键松开后,会在这里把上一个key状态置1
					   Key1_P=1;	//上升沿(假设不做动作那就继续)
				}

				if(Key2_count<0x80)
				{
					Key2_C=0;
					if(Key2_P==1)	//下降沿(按键做动作)
					{
						Key2_P=0;
						write_date++;		   	//按键数据加1
						if(write_date==0xff) 	//假如输入的数据大于0xff,则变为0x00。
						write_date=0x00;				
						}
					}

				
				if(Key2_count>=0x80)
				{
					Key2_C=1;
					if(Key2_P==0)
					   Key2_P=1;	//上升沿(假设不做动作那就继续)
				}


				if(Key3_count<0x80)
				{
					Key3_C=0;
					if(Key3_P==1)	//下降沿(按键做动作)
					{
						Key3_P=0;
						write_addr++;			//按键写入地址+1
						if(write_addr==0xff)
						write_addr=0;
					}

				}
				if(Key3_count>=0x80)
				{
					Key3_C=1;
					if(Key3_P==0)
					   Key3_P=1;	//上升沿(假设不做动作那就继续)
				}
								
				Key1_count=0x80+NMAX_KEY/3*2;    //新一轮的判断   key_cnt=0时才重置次数
				Key2_count=0x80+NMAX_KEY/3*2;
				Key3_count=0x80+NMAX_KEY/3*2;

				Key_count=NMAX_KEY;
			}
		}//read_date=read_add(read_addr); 读数据函数放在此处数码管会无法及时显示
	}
}									                                 

用了计数消抖法,代码要多看一会才理解。

注意点

  • 若想数码管显示二位以上的数,要把整个数拆成各个位,用/和%处理。

image-20200906122730528

  • 另外,把IO口寄存器赋值后,只要不改变,那么IO口就一直是那个值!! 也就是说,就算正在执行delay函数,数码管依然显示进入delay之前的数值,不会熄灭!!

  • 动态扫描就是每一位持续亮一段时间,再换成下一位,一轮之后再亮这一位,那么就是一位亮一段时间再停一段时间,再亮一段时间,让人觉得是一直亮的。 一般来讲,数码管显示都要通过delay来维持一段时间,保证亮度

  • 重复性工作要写函数!

image-20200906152522326

  • 初始化时,要把所有变量都初始化,避免错误