主要是 记录下来关于蓝桥杯比赛板中的一些比较模块化和认为可在今后借鉴的内容。包括了自己的一丢丢理解和包括了广大互联网的智慧结晶。
2022 年第十三届单片机类赛点数据包,以供下载。相关介绍可以浏览 链接

首先是对开发板的一些介绍:

简介:实现多功能 IO 拓展的秘密——38 译码器 + 锁存器

首先我们可以通过下图看到 采用开发板可以实现的功能是不少的。

而开发板的模块基本上都是焊接在板子上的,51 引脚 io 资源并不像 STM32 之类的资源丰富,想要实现这些就要用到一些比较“有意思”的手段。
38 译码器 + 锁存器
通过不同的锁存器来将 P0 的值进行保存,这样实现了 P0 端口的复用,但因为需要不断的切换,也增添了一定的难度,需要大家去适应这样的编程方式。

38 译码器选择功能

输入:
38 译码器,通过 P2.5,P2.6,P2.7 三个引脚输入进行译码,理论上可以控制八种 功能
译码器打开了不同的功能,来改变不同寄存器里的值。
而开发板采用四组寄存器,可以保留四个不同的 P0 的状态,来供给不同的硬件模块,实现其功能。

输出:
对于三八译码器,我们主要使用的是 Y4,Y5,Y6,Y7 这四个输出引脚。
以下是帮助我们记忆的方式。
Y4 -> LED : 4->100 -> 1000 0000 -> 0x80
Y5 -> BEEP : 5->101 -> 1010 0000 -> 0xa0
Y6 -> 数码管位选 : 6->102 -> 1100 0000 -> 0xc0
Y7 -> 数码管端选 : 7->103 -> 1110 0000 -> 0xe0

P2 = 0xc0;//开数码管位 然后 放进去一个选管位的值,他就会保存到一个寄存器里面
然后在打开一个寄存器的开关(与此同时,其他的就被关闭了)
P2 = 0xe0;//开数码管段选锁存器
这样就分别存进去了位和段,他俩的值 并不是由 P0 直接给的,而是由 其各自的寄存器 进行赋值的

各个模块的学习

Hello,world -点灯

快速点亮开发板,是一项可以快速验证环境的事情。
对于如何安装环境和配置开发板,这里因为设计的太多,就不多介绍了详细的可以采用 链接中的介绍 去进行操作。

51 的点灯只需要寥寥几行就可以实现,在开发板上,我们也可以快速实现:

#include "STC15F2K60S2.h"
int main(){
    P0 = 0xfd; //点亮了L1,若全亮为0x00,全灭为0xff。
    P2 = 0x80; //开LED锁存器
    P2 = 0; //关锁存器
    while(1){
        
    }
}

对于点灯的理解:
低电平触发,则将8个LED依次排列,从左到右:1111 1111 恰好构成一个十六进制的数字,该值赋给 P0,来表示 LED 的亮灭。

先给 P0 赋值,这样写可以使得 led 等其他设备,在被开启锁存器之后,不会因为 P0 的值,而发生突然的变化,这种短暂波动可以被消除,对于数码管等模块,还是有一定作用的,所以建议保持这样的习惯:先赋值,再开锁存器,然后立刻关上寄存器。对于是否提前清零 P27 - P25 个人觉得用途不大~

在日后,Led 模块作为一个非常常用的模块,我们如果每次都需要写这样的内容,不仅使得我们的代码比较凌乱,不便于我们查找错误,对于我们的逻辑分析也造成了困扰,同时,这样的写法,也会让我们在遇到多 LED 进行亮灭变化时,或者说有中断控制的情况下,不易编写代码控制,所以采用宏定义的方式,会利于我们的代码编写。

//以下两种宏定义都会使得没有选中的 LED 熄灭
#define led_set(x) {EA =0;P0 = ~(0x01<<x-1);P2 = 0x80;P2 =0;EA =1;} //点亮某栈灯
#define LED_SET(x) {EA = 0;P0 = ~(0x01<<(x-1));P2 = (P2 & 0x1f)|0x80;P2 = P2 & 0x1f;EA = 1;} //更加讲究的写法
#define led_set_all(x) {EA =0;P0 = x;P2 = 0x80;P2 =0;EA =1;} //根据输入的十六进制数,进行点亮灯

当然,我们可以对于宏定义进行修改,使其在在不同的工程和情况下,更加适合我们的代码编写。
第一个宏定义可能不是很好记,可以在比赛时尝试一下,在开发板上运行成功了,再进行宏定义,点灯测试也很快的~

数码管

数码管基本上和LED是同时使用的,而且相对于数码管其能容纳更多的信息量,进而也经常是一个状态展示模块,重要程度不言而喻。

数码管也是有一定技巧的,比如官方虽然给了共阳数码管段码表,但是其给的是竖向的,而且只有数字,没有字母,资源不够丰富。但好在还有朴实无华的 STC-ISP 工具,在其范例程序中,我们可以轻易找到共阴级数码管的段码表(甚至还能顺走一个没有问题的位码),对于段码只需要求反就可以了。

这里给出一个完整的,可运行的代码。会在屏幕上显示1 - 8 ,刷新率是1ms一个符号,即8ms一组,接近 125Hz 的超级刷新率~

#include "STC15F2K60S2.H"
#include "intrins.h"

typedef unsigned char u8;
typedef unsigned int u16;

void Delay1ms();

u8 code t_display[]={                       //标准字库
//   0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,
//black  -     H    J    K    L    N    o   P    U     t    G    Q    r   M    y
    0x00,0x40,0x76,0x1E,0x70,0x38,0x37,0x5C,0x73,0x3E,0x78,0x3d,0x67,0x50,0x37,0x6e,
    0xBF,0x86,0xDB,0xCF,0xE6,0xED,0xFD,0x87,0xFF,0xEF,0x46};    //0. 1. 2. 3. 4. 5. 6. 7. 8. 9. -1

u8 code T_COM[]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};      //位码

int main()
{
	int i=0;
	while(1)
	{
		P0 = T_COM[i];
		P2 = 0xc0;
		P2 = 0;
		P0 = ~t_display[i+1];
		P2 = 0xe0;
		P2 = 0;
		Delay1ms();
		if(i++==7)i=0;
	}
}

void Delay1ms()		//@12.000MHz
{
	unsigned char i, j;
	i = 12;
	j = 169;
	do
	{
		while (--j);
	} while (--i);
}

但是,在单片机中,尤其是裸机开发,一般都是单线程运行,我们如果采用的软件延时,在延迟的过程中,单片机大量时间消耗在延迟上,造成了资源浪费,同时影响了其他的模组运行,故我们采用下一节的定时器来定时中断,进行刷新。

定时器

定时器的配置,如果要从理论上认真了解和学习,需要对寄存器进行配置,略微有些烦琐,但如果不求甚解,那么STC-ISP依旧是不二之选。通常我会选择 1ms 进行中断,这个时间对于数码管刚好,而且可以用计数。
这里以定时器0作为例子,通常我也会使用它作为自己的第一个定时器,用以显示数码管。

但需要注意
上图初始化并不完整,或者说需要我们手动打开定时器,即添加 ET0 = 1;EA = 1; 来打开定时器0中断和全局中断开关。通常添加到 Timer0Init() 函数的最后。
另,我们还需要写一个中断执行程序,来执行中断达到后的任务(也就是刷新数码管);但是数码管的内容通常也是变化的,很少情况是单一的 1234567,故我常用一个字符数组作为数码管的内容数组,每次刷新的时候,从该数组里进行刷新。因为没有对应的表,我们在smg_buf 里保存所需要显示的字符的序号即可。

#include "STC15F2K60S2.H"
#include "intrins.h"

typedef unsigned char u8;
typedef unsigned int u16;

u8 smgi = 0;
u8 smg_buf [8] = {0};
#define LED_SET(x) {EA = 0;P0 = ~(0x01<<(x-1));P2 = (P2 & 0x1f)|0x80;P2 = P2 & 0x1f;EA = 1;}

u8 code t_display[]={                       //标准字库
//   0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,
//black  -     H    J    K    L    N    o   P    U     t    G    Q    r   M    y
    0x00,0x40,0x76,0x1E,0x70,0x38,0x37,0x5C,0x73,0x3E,0x78,0x3d,0x67,0x50,0x37,0x6e,
    0xBF,0x86,0xDB,0xCF,0xE6,0xED,0xFD,0x87,0xFF,0xEF,0x46};    //0. 1. 2. 3. 4. 5. 6. 7. 8. 9. -1

u8 code T_COM[]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};      //位码

void Delay1ms()		//@12.000MHz
{
	unsigned char i, j;

	i = 12;
	j = 169;
	do
	{
		while (--j);
	} while (--i);
}

void Timer0Init(void)		//1毫秒@12.000MHz
{
	AUXR |= 0x80;		//定时器时钟1T模式
	TMOD &= 0xF0;		//设置定时器模式
	TL0 = 0x20;		//设置定时初始值
	TH0 = 0xD1;		//设置定时初始值
	TF0 = 0;		//清除TF0标志
	TR0 = 1;		//定时器0开始计时
	ET0 = 1;
	EA = 1;
}
void Timer0Handle() interrupt 1
{
	P0 = T_COM[smgi];
	P2 = 0xc0;
	P2 = 0;

	P0 = ~t_display[smg_buf[smgi]];
	P2 = 0xe0;
	P2 = 0;
	if(smgi++==7)smgi=0;
}
int main()
{
	int i=0;
	Timer0Init();
	while(1)
	{
		
	}
}

蜂鸣器与继电器

两个比较嘈杂的小玩意,所以在最新的第三代板子上特地给他们添加了开关,避免他们在烧录程序的时候滴滴滴滴,卡卡喳喳。我们也要注意,如果需要打开,就需要手动切换跳帽。

从原理图中可以清晰看出他们的关系。有时候题目会要求刚上电时候,蜂鸣器不会乱叫,我们最好在最开始初始化一下他俩。
这里给一个简略的代码,可以方便去第一次看,稍候会给出更加合理的代码:

#include "STC15F2K60S2.H"

typedef unsigned char u8;
typedef unsigned int u16;

void Beep_Relay_Init()
{
	P0 = 0;
	P2 = 0xa0;
	P2 = 0;
}
void Delay1ms()		//@12.000MHz
{
	unsigned char i, j;
	i = 12;
	j = 169;
	do
	{
		while (--j);
	} while (--i);
}
int main()
{
	int i=0;
	Beep_Relay_Init();
	while(1)
	{
		Delay1ms();
		i++;
		if(i==1){//继电器
			P0 = 0x10;
			P2 = 0xa0;
			P2 = 0;
		}
		if(i==500){
			P0 = 0;
			P2 = 0xa0;
			P2 = 0;
		}
		if(i==1000){//蜂鸣器
			P0 = 0x40;
			P2 = 0xa0;
			P2 = 0;
		}
		if(i==1500){
			P0 = 0;
			P2 = 0x10;
			P2 = 0;
			i=0;
		}
	}
}

而我们在使用的时候,常常我会将它和LED的处理方式类似,采用宏定义来简洁代码。
而且我们并不需要管其他的元器件,如果采用P0口直接对器操作那么就需要考虑其他元器件的功能,所以采用单个引脚操作最好不过了~同时采用两个状态量作为查看两者是否开启。

#include "STC15F2K60S2.H"

typedef unsigned char u8;
typedef unsigned int u16;
u8 buzzer_state=0;
u8 relay_state=0;
#define BUZZER_ON {EA = 0;P06 = 1;P2 = 0xa0;P2 = 0;buzzer_state=1;EA = 1;}
#define BUZZER_OFF {EA = 0;P06= 0;P2 = 0xa0;P2 = 0;buzzer_state=0;EA = 1;}

#define RELAY_ON {EA = 0;P04=1;P2 = 0xa0;P2 = 0;relay_state=1;EA = 1;}
#define RELAY_OFF {EA = 0;P04=0;P2 = 0xa0;P2 = 0;relay_state=0;EA = 1;}

void Delay1ms()		//@12.000MHz
{
	unsigned char i, j;
	i = 12;
	j = 169;
	do
	{
		while (--j);
	} while (--i);
}
int main()
{
	int i=0;
	BUZZER_OFF;
	RELAY_OFF;
	while(1)
	{
		Delay1ms();
		i++;
		if(i==1000)
		{
			if(buzzer_state == 0)BUZZER_ON
			else BUZZER_OFF;
			if(relay_state == 0)RELAY_ON
			else RELAY_OFF
			i=0;
		}
	}
}

按键/矩阵键盘

按键没什么好说的,一般直接写矩阵键盘,考察的一般也是矩阵键盘,因为矩阵键盘包括了按键~
这个没什么好说的,得背,理解也比较麻烦,可以参考链接
我们将读取键盘的时间为10ms一次,读取按键之后进行处理,改变一些状态量,然后再在Main函数里进行处理。
实际应用中, 我们也将这个read_key()放在另一个misc.c文件中,使得 main.c 相对简洁。

#include "STC15F2K60S2.H"

typedef unsigned char u8;
typedef unsigned int u16;
u8 light_i=1;

#define LED_SET(x) {EA = 0;P0 = ~(0x01<<(x-1));P2 = (P2 & 0x1f)|0x80;P2 = P2 & 0x1f;EA = 1;}
//key_old 的初始化状态是根据具体的业务逻辑进行的
//初始化为我们想要的初始状态所采用的按键。
u8 key_old=1,key_down,key_up;
u8 read_key()
{
	u16 t;
	u8 key;
	/*这里为什么第一次将4个引脚置高,而之后置改变两个呢?
	因为第一次时候需要把其他引脚置高,来确保P44的准确性,算作初始化。
	而之后因为我们把P44拉低了,所以还需要置高,防止他影响其他的判断,
	而对于P34,P35我们并没有拉低他们,所以不需要改变。
	每次只需要把自己拉低的引脚重新置高就可了
	*/
	P44 =0 ;P42 = 1;P35 = 1;P34 =1;
	t = P3;
	P44 = 1;P42 = 0;
	t = t<<4 | (P3 & 0x0f);
	P42 = 1;P35 = 0;
	t = t<<4 | (P3 & 0x0f);
	P35 = 1; P34 = 0;
	t = t<<4 | (P3 & 0x0f);

	switch (~t)
	{
	case 0x8000:key = 4;break;
	case 0x4000:key = 5;break;
	case 0x2000:key = 6;break;
	case 0x1000:key = 7;break;
	case 0x0800:key = 8;break;
	case 0x0400:key = 9;break;
	case 0x0200:key = 10;break;
	case 0x0100:key = 11;break;
	case 0x0080:key = 12;break;
	case 0x0040:key = 13;break;
	case 0x0020:key = 14;break;
	case 0x0010:key = 15;break;
	case 0x0008:key = 16;break;
	case 0x0004:key = 17;break;
	case 0x0002:key = 18;break;
	case 0x0001:key = 19;break;
	default:key = 0;
	}
	return key;

}
//在里面尽量只改变状态,动作在Main函数或其他函数进行
void key_proc()
{
	u8 key = read_key();
	//Don't try to find out why,just feel and remember them :)
	key_down = key & (key ^ key_old);
	key_up = ~key & (key ^ key_old);
	key_old = key;

	if(key_down == 12) {
		light_i--;
		if(light_i<1)light_i=1;
	}
	if(key_down == 16){
		light_i++;
		if(light_i>8)light_i=8;
	}
	
}
void Delay10ms()		//@12.000MHz
{
	unsigned char i, j;

	i = 117;
	j = 184;
	do
	{
		while (--j);
	} while (--i);
}

int main()
{
	
	while(1)
	{
		key_proc();
		Delay10ms();
		LED_SET(light_i);
	}
	
}

DS18B20温度

基本组件,也就是最基础的东西已经熟悉了,下面就是外部拓展的组件:
对于 DS18b20 我们需要添加 onewire.h , onewire.c 文件到相关的文件夹中去。
同时,我们还需要修改 onewire.c 中的Delay_OneWire函数

void Delay_OneWire(unsigned int t)  
{
	t*=12;
	while(t--);
}

其中吧 t * 12 因为我们采用的是12T的单片机,添加即可。
后续主要的就是修改onewire.h头文件,添加函数的方法名就行,但需要完整的添加,不再赘述。
对于如何读取 温度值可以查询相关的芯片手册,然后阅读 u16 read_temperature() 部分即可。
对于浮点数,不便处理,故转出时采用u16 转出,需要保留几位,乘以10的倍数即可。
注意数据的转化和类型之间的切换。
#include "STC15F2K60S2.H"
#include "onewire.h"
typedef unsigned char u8;
typedef unsigned int u16;

u16 ms = 0;
u16 temper = 0;
u8 smg_cnt=0;
u8 smg_buf[8] = {0};
u8 code t_display[]={                       //标准字库
//   0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,
//black  -     H    J    K    L    N    o   P    U     t    G    Q    r   M    y
    0x00,0x40,0x76,0x1E,0x70,0x38,0x37,0x5C,0x73,0x3E,0x78,0x3d,0x67,0x50,0x37,0x6e,
    0xBF,0x86,0xDB,0xCF,0xE6,0xED,0xFD,0x87,0xFF,0xEF,0x46};    //0. 1. 2. 3. 4. 5. 6. 7. 8. 9. -1

u8 code T_COM[]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};      //位码


//return 298->29.8
u16 read_temperature()
{
	float temper;
	u8 high,low;
	u16 temp;

	init_ds18b20();
	Write_DS18B20(0xcc);
	Write_DS18B20(0x44);
	Delay_OneWire(200);

	init_ds18b20();
	Write_DS18B20(0xcc);
	Write_DS18B20(0xbe);

	low = Read_DS18B20();
	high = Read_DS18B20();

	temp = high<<8 | low;
	temper = temp*0.0625;
	return temper*10;
}

void Timer0Init(void)		//1毫秒@12.000MHz
{
	AUXR &= 0x7F;		//定时器时钟12T模式
	TMOD &= 0xF0;		//设置定时器模式
	TL0 = 0x18;		//设置定时初始值
	TH0 = 0xFC;		//设置定时初始值
	TF0 = 0;		//清除TF0标志
	TR0 = 1;		//定时器0开始计时
	ET0 = 1;
	EA = 1;
}
void Timer0Handle() interrupt 1
{
	ms++;
	P0 = T_COM[smg_cnt];
	P2 = 0xc0;
	P2 = 0;

	P0 = ~t_display[smg_buf[smg_cnt]];
	P2 = 0xe0;
	P2 = 0;
	if(smg_cnt++==7)smg_cnt=0;

	if(ms == 100000)ms=0;	
}

int main()
{
	Timer0Init();
	while(1)
	{
		if(ms % 500)temper = read_temperature();
		smg_buf[0] = 0;
		smg_buf[1] = 0;
		smg_buf[2] = 0;
		smg_buf[3] = 0;
		smg_buf[4] = 0;
		smg_buf[5] = temper / 100;
		smg_buf[6] = temper / 10 % 10 + 32;
		smg_buf[7] = temper % 10;
	}
	
}

DS1302时钟

对于时钟芯片,DS1302 应用也是颇为广泛,不仅在比赛中,实际应用中也不少。
也是相对复杂一些的内容,需要我们好好去记住并练习,才能在比赛中,熟练运用。
还好官方把驱动给了出来,要不真的难搞~,但如何写入和读取时间是我们需要完成的。

这个表在官方的芯片文档里面有,是需要我们打开看的,编写程序也是依托于他。

其实这个也是相对简单的,只是需要我们在写入或者读出的时候,把两位数的时间,分散成高位和低位的编码方式。即高四位表示 十位,低四位表示 个位。同时注意开关写入时刻的 wp 即可。

需要注意在c语言中,乘除的优先级高于 左移右移,所以在编写 读写代码时刻需要注意。
同时,由于定时器中断的影响,所以最好在使用DS18B20和DS1302这样的对时序有要求的模块时刻,在函数中采用中断的开启和关闭,不影响读取的完整操作。

而 对于 ds1302.h ds1302.c 两个文件,我们基本没有更改其内容,所以就不赘述了。

#include "STC15F2K60S2.H"
#include "ds1302.h"
typedef unsigned char u8;
typedef unsigned int u16;

u16 ms;
u8 HH,MM,SS;
u8 smg_cnt;
u8 smg_buf[8] = {0};
u8 code t_display[]={                       //标准字库
//   0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,
//black  -     H    J    K    L    N    o   P    U     t    G    Q    r   M    y
    0x00,0x40,0x76,0x1E,0x70,0x38,0x37,0x5C,0x73,0x3E,0x78,0x3d,0x67,0x50,0x37,0x6e,
    0xBF,0x86,0xDB,0xCF,0xE6,0xED,0xFD,0x87,0xFF,0xEF,0x46};    //0. 1. 2. 3. 4. 5. 6. 7. 8. 9. -1

u8 code T_COM[]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};      //位码


void Timer0Init(void)		//1毫秒@12.000MHz
{
	AUXR &= 0x7F;		//定时器时钟12T模式
	TMOD &= 0xF0;		//设置定时器模式
	TL0 = 0x18;		//设置定时初始值
	TH0 = 0xFC;		//设置定时初始值
	TF0 = 0;		//清除TF0标志
	TR0 = 1;		//定时器0开始计时
	ET0 = 1;
	EA = 1;
}
void Timer0Handle() interrupt 1
{
	P0 = T_COM[smg_cnt];
	P2 = 0xc0;
	P2 = 0;

	P0 = ~t_display[smg_buf[smg_cnt]];
	P2 = 0xe0;
	P2 = 0;
	if(smg_cnt++==7)smg_cnt=0;

	if(ms == 100000)ms=0;	
}
void read_time()
{
	u8 t;
	EA = 0;
	t = Read_Ds1302_Byte(0x81);
	SS = (t>>4)* 10 +(t & 0x0f);
	t = Read_Ds1302_Byte(0x83);
	MM = (t>>4)* 10 +(t & 0x0f);
	t = Read_Ds1302_Byte(0x85);
	HH = (t>>4)* 10 +(t & 0x0f);
	EA = 1;
}

void write_time(u8 hh,u8 mm,u8 ss)
{
	EA = 0;
	Write_Ds1302_Byte(0x8e,0);
	Write_Ds1302_Byte(0x80,(ss/10)<<4 | ss % 10);
	Write_Ds1302_Byte(0x82,(mm/10)<<4 | mm % 10);
	Write_Ds1302_Byte(0x84,(hh/10)<<4 | hh % 10);
	Write_Ds1302_Byte(0x8e,0x80);
	EA = 1;
}
int main()
{
	write_time(11,10,30);
	Timer0Init();
	
	while(1)
	{
		if(ms % 1000==0)read_time();
		smg_buf[0] = 0;
		smg_buf[1] = 0;
		smg_buf[2] = HH / 10;
		smg_buf[3] = HH % 10;
		smg_buf[4] = MM / 10;
		smg_buf[5] = MM % 10;
		smg_buf[6] = SS / 10;
		smg_buf[7] = SS % 10;
	}
}

ADC / DAC

可以说,只有 DAC 模块才需要我们去使用万用表来测量一下哈哈。
数模转换 是单片机一个比较重要的点,是真实世界与数字世界之间交换的桥梁。
采用的是模块 PCF8591 ,该模块采用IIC协议进行通讯,可以进行ADC和DAC的处理。
IIC设备地址见下图:即0x90

要注意,如果读取出来的DAC的值是0-255,我们转换到5v既然要小数点后两位,不妨就采用500这样的范围。但是500超过了 unsigned char 的最大范围,所以最好用 u16;
具体的原因于精妙的地方,因为时间原因就不再补充了。如果明天国赛比完后,还有心情的话,就来补充。
比完赛了,么有心情。

//iic.c
#define DELAY_TIME 50

unsigned char read_adc(unsigned char add)
{
    unsigned char t;
    EA = 0;
    IIC_Start();
    IIC_SendByte(0x90);
    IIC_WaitAck();
    IIC_SendByte(add);
    IIC_WaitAck();

    IIC_Start();
    IIC_SendByte(0x91);
    IIC_WaitAck();
    t = IIC_RecByte();
    IIC_WaitAck();
    IIC_Stop();
    EA = 1;
    return t;
}
void write_dac(unsigned char t)
{
    EA = 0;
    IIC_Start();
    IIC_SendByte(0x90);
    IIC_WaitAck();
    IIC_SendByte(0x40);
    IIC_WaitAck();
    IIC_SendByte(t);
    IIC_WaitAck();
    IIC_Stop();
    EA = 1;
}

//main.c
#include "STC15F2K60S2.H"
#include  "intrins.h"
#include "iic.h"
typedef unsigned char u8;
typedef unsigned int u16;

u16 ms;
u8 smg_cnt;
u8 smg_buf[8] = {0};
u8 code t_display[]={                       //标准字库
//   0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,
//black  -     H    J    K    L    N    o   P    U     t    G    Q    r   M    y
    0x00,0x40,0x76,0x1E,0x70,0x38,0x37,0x5C,0x73,0x3E,0x78,0x3d,0x67,0x50,0x37,0x6e,
    0xBF,0x86,0xDB,0xCF,0xE6,0xED,0xFD,0x87,0xFF,0xEF,0x46};    //0. 1. 2. 3. 4. 5. 6. 7. 8. 9. -1

u8 code T_COM[]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};      //位码


void Timer0Init(void)		//1毫秒@12.000MHz
{
	AUXR &= 0x7F;		//定时器时钟12T模式
	TMOD &= 0xF0;		//设置定时器模式
	TL0 = 0x18;		//设置定时初始值
	TH0 = 0xFC;		//设置定时初始值
	TF0 = 0;		//清除TF0标志
	TR0 = 1;		//定时器0开始计时
	ET0 = 1;
	EA = 1;
}
void Timer0Handle() interrupt 1
{
	P0 = T_COM[smg_cnt];
	P2 = 0xc0;
	P2 = 0;

	P0 = ~t_display[smg_buf[smg_cnt]];
	P2 = 0xe0;
	P2 = 0;
	if(smg_cnt++==7)smg_cnt=0;

	if(ms++ == 100000)ms=0;	
}
int main()
{
	u16 Light_value=0;
	u16 v_value=0;
	Timer0Init();
	write_dac(125);
	
	while(1)
	{
		if(ms%500 == 0)
		{
			v_value = (u16)(read_adc(1)*500.0/255);
			
			smg_buf[0] = v_value /100 +32;
			smg_buf[1] = v_value /10 %10;
			smg_buf[2] = v_value %10;

			Light_value = read_adc(3);
			smg_buf[4] = Light_value / 1000;
			smg_buf[5] = Light_value % 1000 /100;
			smg_buf[6] = Light_value % 100 /10;
			smg_buf[7] = Light_value % 10;
		}
	}
}

AT24C02-EEROM

超声波测距

这部分并不需要其他的驱动,但这也就意味着需要我们自己去完成全部的代码。
主要是发送波形,接受波形,如果对应的上,那么就可以根据飞行时间来确定距离。
发送的比较简单,需要发送八段脉冲波(40Khz),,意味着延迟可以这样计算:

#include "STC15F2K60S2.H"
#include    "intrins.h"
typedef unsigned char u8;
typedef unsigned int u16;

sbit TX = P1^0;
sbit RX = P1^1;

u16 ms,distance;
u8 smg_cnt;
u8 smg_buf[8] = {0};
u8 code t_display[]={                       //标准字库
//   0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,
//black  -     H    J    K    L    N    o   P    U     t    G    Q    r   M    y
    0x00,0x40,0x76,0x1E,0x70,0x38,0x37,0x5C,0x73,0x3E,0x78,0x3d,0x67,0x50,0x37,0x6e,
    0xBF,0x86,0xDB,0xCF,0xE6,0xED,0xFD,0x87,0xFF,0xEF,0x46};    //0. 1. 2. 3. 4. 5. 6. 7. 8. 9. -1

u8 code T_COM[]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};      //位码

// 13 = 1 000 000 / 40 000 / 2
// 所需的是8个40Khz的发送波
void Delay13us()		//@12.000MHz
{
	unsigned char i;

	_nop_();
	_nop_();
	i = 36;
	while (--i);
}

void send_wave()
{
	u8 i=8;
	while(i--)
	{
		TX = 1;
		Delay13us();
		TX = 0;
		Delay13us();
	}
}
void get_wave()
{
	u16 time;
	EA = 0;
	send_wave();
	EA = 1;

	TH1 = 0;
	TL1 = 0;
	TR1 = 1;
	while((RX == 1) && (TF1 == 0));
	TR1 = 0;
	if(TF1 == 1)TF1 =0;
	else{
		time = (TH1 << 8) | TL1;
		distance = (u16)(time * 34000 /1000000 / 2);
		//distance = (u16)(time * 0.017);//34 000 / 1000 000 / 2
	}
}
void Timer0Init(void)		//1毫秒@12.000MHz
{
	AUXR &= 0x7F;		//定时器时钟12T模式
	TMOD &= 0xF0;		//设置定时器模式
	TL0 = 0x18;		//设置定时初始值
	TH0 = 0xFC;		//设置定时初始值
	TF0 = 0;		//清除TF0标志
	TR0 = 1;		//定时器0开始计时
	ET0 = 1;
	EA = 1;
}
void Timer0Handle() interrupt 1
{
	P0 = T_COM[smg_cnt];
	P2 = 0xc0;
	P2 = 0;

	P0 = ~t_display[smg_buf[smg_cnt]];
	P2 = 0xe0;
	P2 = 0;
	if(smg_cnt++==7)smg_cnt=0;

	if(ms++ == 100000)ms=0;	
}
int main()
{

	Timer0Init();
	
	while(1)
	{
		if((ms % 200) ==0)
		{
			get_wave();
			smg_buf[0] = distance / 100;
			smg_buf[1] = distance / 10 % 10;
			smg_buf[2] = distance % 10;
		}
		
	}
}

致谢

https://baiblog.top/lq1/