[toc]
前言
小小的bb几句
这里我说明一下我个人在学习单片机的时候的一些心得体会,以及大模板的最新建立,会持续更新。
最基础的模版来自于B站Up Alice_西风的个人空间-Alice_西风个人主页-哔哩哔哩视频 (bilibili.com)
我的B站为左-岚的个人空间-左-岚个人主页-哔哩哔哩视频 (bilibili.com)
这篇文章对应着我B站的视频为 【蓝桥杯】【单片机】大模板构建喂饭教程
建议搭配着视频使用,不然可能有些地方比较迷糊。
对应的CSDN链接为【喂饭】【速成】蓝桥杯模版手把手建立-CSDN博客
对应的知乎链接为【喂饭】【速成】蓝桥杯模版手把手建立
这两个仓库可以删除后面的东西(一直删除到master,可以看到我的全部的东西)
Github的仓库为 lanqiaobei_study/模板/demo_zuolan at master · zuoliangyu/lanqiaobei_study (github.com)
Gitee的仓库为 模板/demo_zuolan · zuoliangyu/蓝桥杯学习 - 码云 - 开源中国 (gitee.com)
锁存器与三八译码器介绍
这里我们需要有一个基础知识,就是我们的锁存器和三八译码器介绍,这是一个比较关键的点,我们看到下面这两个图,可以看到我们的锁存器是用P25,P26,P27进行控制,即P2的高三位进行的控制,我们只需要按照如下的命令进行书写即可。
1
2
|
P2 = P2 & 0x1f | 0x?0;
P2 &= 0x1f;
|
其中的P2&=0x1f,表示我们将高三位置为0,| 0x?0的操作表明我们对高三位进行书写,比如Y4C,那么就代表我们的高三位要表示为4,即0100,我们将其放在高位,即1000_0000,翻译过来就是0x80


NOP延时函数
这里要注意一个问题,就是我们延时函数大部分都有NOP,包括Ds1302,IIC都使用了这个,这个函数存在于头文件intrins.h中,需要注意一下引用,一般来说,一个NOP在12MHz的单片机下是1us,其余的自己去换算一下。
LED,蜂鸣器,继电器,MOTOR模版建立
Led.c
这里虽然名称叫做Led.c,但是里面书写LED,蜂鸣器,继电器,MOTOR的代码
Led底层
我们可以看到Led的代码,我们可以注意到的是,这个地方使用了static这个关键词,因为我们需要关注内存的使用~~(这里不得不提C51的垃圾内存容量)~~,所以我们将其变为函数静态内存,可以有效节省。
传入的参数有两个,一个是addr,代表我们需要控制的Led的地址,(0-7)对应了(L1-L8),另一个是enable,用于控制选中的Led灯的亮灭。
1
|
void Led_Disp(unsigned char addr, unsigned char enable)
|
我们定义了两个参数,一个是temp,一个是temp_old,你们可能会好奇,为什么我要在这里用两个变量来进行编写,明明逻辑上只需要用一个变量对P0进行赋值就行了,这里我说明一下,我们一般情况下不会一直操控锁存器,否则有概率影响数码管的显示,当我们状况出现变化(temp!=temp_old)的时候,我们才对锁存器P2的高三位进行书写。当我们需要点亮的时候,就让相应的addr变为1;当我们需要熄灭的时候就让相应的addr变为0;
这里你可能会有疑问,因为我们在原理图上面如果想让Led亮,那么应该给0,这里做一个说明,我们后面会直接取反,这里用1是为了符合我们的直觉~~(因为正常人就是1亮0灭)~~
1
2
3
4
5
6
|
static unsigned char temp = 0x00;
static unsigned char temp_old = 0xff;
if (enable)
temp |= 0x01 << addr;
else
temp &= ~(0x01 << addr);
|

我们通过查看原理图可以看到,用于控制的Load为Y4C,我们通过计算二进制可以发现结果为1000_0000,即0x80,所以我们在这里代码中的P2设置为P2 = P2 & 0x1f | 0x80;在运行后别忘记使用P2 &= 0x1f来进行高位清零,但是在我们操作P2锁存器之前,我们需要先对P0进行赋值(要记得先取反)
1
2
3
4
5
6
|
if (temp != temp_old) {
P0 = ~temp;
P2 = P2 & 0x1f | 0x80;
P2 &= 0x1f;
temp_old = temp;
}
|
完整代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/// @brief Led扫描
/// @param addr 需要控制的Led的地址(0-7)
/// @param enable 控制该地址的Led是否点亮
void Led_Disp(unsigned char addr, unsigned char enable) {
static unsigned char temp = 0x00;
static unsigned char temp_old = 0xff;
if (enable)
temp |= 0x01 << addr;
else
temp &= ~(0x01 << addr);
if (temp != temp_old) {
P0 = ~temp;
P2 = P2 & 0x1f | 0x80;
P2 &= 0x1f;
temp_old = temp;
}
}
|
蜂鸣器,继电器,MOTOR底层
这里我就一起讲了,因为他们三是一个芯片的不同输出,和上方的Led类似,但是这几个需要一个全局变量(在函数外申明)
1
2
|
unsigned char temp_0 = 0x00;
unsigned char temp_old_0 = 0xff;
|
我们可以看到,BUZZ、MOTOR、RELAY为一个芯片的不同位的输出
RELAY对应I5->Q4->D4->0001_0000->0x10
MOTOR对应I6->Q5->D5->0010_0000->0x20
BUZZ对应I7->Q6->D6->0100_0000->0x80
这里后我们就知道如果我们需要对应的工作,那么就要给P0赋相应的值,并且下方的Y5C也告诉我们,我们需要给锁存器为1010_0000->0xa0,即P2 = P2 & 0x1f | 0xa0;这个之后也别忘了使用P2 &= 0x1f来进行高位清零。
1
2
3
4
5
6
|
if (temp_0 != temp_old_0) {
P0 = temp_0;
P2 = P2 & 0x1f | 0xa0;
P2 &= 0x1f;
temp_old_0 = temp_0;
}
|

总体代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
unsigned char temp_0 = 0x00;
unsigned char temp_old_0 = 0xff;
/// @brief 蜂鸣器控制
/// @param enable
void Beep(bit enable) {
if (enable)
temp_0 |= 0x40;
else
temp_0 &= ~(0x40);
if (temp_0 != temp_old_0) {
P0 = temp_0;
P2 = P2 & 0x1f | 0xa0;
P2 &= 0x1f;
temp_old_0 = temp_0;
}
}
/// @brief 继电器控制
/// @param enable
void Relay(bit enable) {
if (enable)
temp_0 |= 0x10;
else
temp_0 &= ~(0x10);
if (temp_0 != temp_old_0) {
P0 = temp_0;
P2 = P2 & 0x1f | 0xa0;
P2 &= 0x1f;
temp_old_0 = temp_0;
}
}
/// @brief MOTOR控制
/// @param enable
void MOTOR(bit enable) {
if (enable)
temp_0 |= 0x20;
else
temp_0 &= ~(0x20);
if (temp_0 != temp_old_0) {
P0 = temp_0;
P2 = P2 & 0x1f | 0xa0;
P2 &= 0x1f;
temp_old_0 = temp_0;
}
}
|
Led.h
申明代码如下
1
2
3
4
5
|
#include <STC15F2K60S2.H>
void Led_Disp(unsigned char addr, unsigned char enable);
void Beep(bit enable);
void Relay(bit enable);
void MOTOR(bit enable);
|
Key模版建立
key.c
基础代码
在这里我们可以看到原理图里面的行列显示,最新版的原理图把row和col单独拿出来了,所以可能对应有点麻烦~~(其实也不麻烦)~~,如下图所示,我们直接按照一列一列进行扫描,给列低电平后判断指定行是否为低电平,如果指定行为低电平,那么我们就认为这个按键被按下。并且我们在这里不需要实现消抖等操作,我们会在main.c里面实现我们的功能,这里只实现最基础的扫描。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
P44 = 0;P42 = 1;P35 = 1;P34 = 1;
if (P33 == 0) temp = 4;
if (P32 == 0) temp = 5;
if (P31 == 0) temp = 6;
if (P30 == 0) temp = 7;
P44 = 1;P42 = 0;P35 = 1;P34 = 1;
if (P33 == 0) temp = 8;
if (P32 == 0) temp = 9;
if (P31 == 0) temp = 10;
if (P30 == 0) temp = 11;
P44 = 1;P42 = 1;P35 = 0;P34 = 1;
if (P33 == 0) temp = 12;
if (P32 == 0) temp = 13;
if (P31 == 0) temp = 14;
if (P30 == 0) temp = 15;
P44 = 1;P42 = 1;P35 = 1;P34 = 0;
if (P33 == 0) temp = 16;
if (P32 == 0) temp = 17;
if (P31 == 0) temp = 18;
if (P30 == 0) temp = 19;
|


在我们把行列扫描写完后就可以完成工作了,但是,我们要注意一个问题,局部变量一定要注意初始化为0
1
|
unsigned char temp = 0;
|
注意事项
在这里要注意一个问题,我们的串口进行发送的时候会占用P30和P31口,这个是硬件的冲突,所以我们在进入这个扫描函数的时候,需要对其关掉轮询(就是在main函数里面用的什么来进行各个函数定时的定时器,叫做轮询定时器)。这里我用的是定时器1,所以我进入后就要使用ET1=0关掉,结束后用P3=0xff;ET1=1开启。
1
2
3
4
|
ET1 = 0;
----
P3 = 0xff;
ET1 = 1;
|
还有一个比较重要的问题,今年(2024)不管是省赛还是国赛都考了ne555的测量,我们ne555的测量要用到P34引脚,也就是我们的按键的最后一列,那么这里就要注意一下,这是一个硬件冲突,且无法解决,所以我们将所有P34删除,就当我们用的按键为4行3列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
P44 = 0;P42 = 1;P35 = 1;
if (P33 == 0) temp = 4;
if (P32 == 0) temp = 5;
if (P31 == 0) temp = 6;
if (P30 == 0) temp = 7;
P44 = 1;P42 = 0;P35 = 1;
if (P33 == 0) temp = 8;
if (P32 == 0) temp = 9;
if (P31 == 0) temp = 10;
if (P30 == 0) temp = 11;
P44 = 1;P42 = 1;P35 = 0;
if (P33 == 0) temp = 12;
if (P32 == 0) temp = 13;
if (P31 == 0) temp = 14;
if (P30 == 0) temp = 15;
|
整体代码如下(没有包括ne555,如果有ne555需要把按键扫描换成上面的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include "key.h"
unsigned char Key_Read() {
unsigned char temp = 0;
ET1 = 0;
P44 = 0;P42 = 1;P35 = 1;P34 = 1;
if (P33 == 0) temp = 4;
if (P32 == 0) temp = 5;
if (P31 == 0) temp = 6;
if (P30 == 0) temp = 7;
P44 = 1;P42 = 0;P35 = 1;P34 = 1;
if (P33 == 0) temp = 8;
if (P32 == 0) temp = 9;
if (P31 == 0) temp = 10;
if (P30 == 0) temp = 11;
P44 = 1;P42 = 1;P35 = 0;P34 = 1;
if (P33 == 0) temp = 12;
if (P32 == 0) temp = 13;
if (P31 == 0) temp = 14;
if (P30 == 0) temp = 15;
P44 = 1;P42 = 1;P35 = 1;P34 = 0;
if (P33 == 0) temp = 16;
if (P32 == 0) temp = 17;
if (P31 == 0) temp = 18;
if (P30 == 0) temp = 19;
P3 = 0xff;
ET1 = 1;
return temp;
}
|
key.h
1
2
|
#include <STC15F2K60S2.H>
unsigned char Key_Read();
|
数码管模板建立
Seg.c
数码管编码推导
这里我单独开一个标题来做数码管的编码推导,因为这个很重要,我们看到下方的截图

可以观察到每一段的数码管都被编号,这里由于我们用的是共阳极数码管,即0亮1灭,下面我给一个🌰来进行说明

我们标好后按照dp->g->…->a的顺序写出,即1010_0100->0xa4,这个值在数码管的显示中就代表着“2”
所以我们就可以根据这个规律来编写一个段选表
1
2
|
code unsigned char seg_dula[] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92,
0x82, 0xf8, 0x80, 0x90, 0xff, 0x88};
|
数码管代码编写
这是我们最最重要的玩意,因为这个涉及到我们的显示,我们看原理图可以看到,我们需要对数码管进行位选(选择哪一个数码管进行显示),段选(选择数码管显示的数据)。
其中ABCDEFG和DP为我们的段选,COM1-8为我们的位选
我们需要先对P0进行段选显示后,使用P2将锁存器打开(Y7C->1110_0000->0xe0);在对P0进行位选配置后,使用P2将锁存器打开(Y6C->1100_0000->0xc0)

经过我们的分析后我们就可以知道我们需要传入的数据为wela位选,dula段选,point是否显示小数点
1
|
void Seg_Disp(unsigned char wela, unsigned char dula, unsigned char point)
|
我们为了避免闪烁,所以我们需要先对数码管进行消隐,即
1
2
3
|
P0 = 0xff;
P2 = P2 & 0x1f | 0xe0;
P2 &= 0x1f;
|
然后我们对位置进行选择后使用P2打开相应锁存器
1
2
3
|
P0 = 0x01 << wela;
P2 = P2 & 0x1f | 0xc0;
P2 &= 0x1f;
|
由于我们可以知道,要显示小数点的话,我们就要让DP为0,其他不变,即&0x7f即可完成
1
2
3
4
|
P0 = seg_dula[dula];
if (point) P0 &= 0x7f;
P2 = P2 & 0x1f | 0xe0;
P2 &= 0x1f;
|
总体代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include "seg.h"
// 0-9 灭
// A
code unsigned char seg_dula[] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92,
0x82, 0xf8, 0x80, 0x90, 0xff, 0x88};
void Seg_Disp(unsigned char wela, unsigned char dula, unsigned char point) {
// 消隐
P0 = 0xff;
P2 = P2 & 0x1f | 0xe0;
P2 &= 0x1f;
// 位选
P0 = 0x01 << wela;
P2 = P2 & 0x1f | 0xc0;
P2 &= 0x1f;
// 段选
P0 = seg_dula[dula];
if (point) P0 &= 0x7f;
P2 = P2 & 0x1f | 0xe0;
P2 &= 0x1f;
}
|
Seg.h
1
2
|
#include <STC15F2K60S2.H>
void Seg_Disp(unsigned char wela, unsigned char dula, unsigned char point);
|
Ds1302时钟模版建立
Ds1302.c
这里有官方提供的参考代码,所以我们上面的时序可以不用书写,我们直接对下方的时间读取/写入进行操作
官方的参考代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
/* # DS1302代码片段说明
1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
2.
参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
中对单片机时钟频率的要求,进行代码调试和修改。
*/
//
void Write_Ds1302(unsigned char temp) {
unsigned char i;
for (i = 0; i < 8; i++) {
SCK = 0;
SDA = temp & 0x01;
temp >>= 1;
SCK = 1;
}
}
//
void Write_Ds1302_Byte(unsigned char address, unsigned char dat) {
RST = 0;
_nop_();
SCK = 0;
_nop_();
RST = 1;
_nop_();
Write_Ds1302(address);
Write_Ds1302(dat);
RST = 0;
}
//
unsigned char Read_Ds1302_Byte(unsigned char address) {
unsigned char i, temp = 0x00;
RST = 0;
_nop_();
SCK = 0;
_nop_();
RST = 1;
_nop_();
Write_Ds1302(address);
for (i = 0; i < 8; i++) {
SCK = 0;
temp >>= 1;
if (SDA) temp |= 0x80;
SCK = 1;
}
RST = 0;
_nop_();
SCK = 0;
_nop_();
SCK = 1;
_nop_();
SDA = 0;
_nop_();
SDA = 1;
_nop_();
return (temp);
}
|
我们需要补充的代码
首先是我们需要绑定SDA,RST,SCK的引脚,在原理图中可以看到,并且要注意一个问题,我们用到了NOP函数,所以需要引入头文件intrins.h

1
2
3
4
|
#include "intrins.h"
sbit SDA = P2 ^ 3;
sbit RST = P1 ^ 3;
sbit SCK = P1 ^ 7;
|
然后就是我们的读写时钟,观察芯片手册可以看到

我们如果需要读取时钟的话,只需要读取地址0x85->小时 0x83->分钟 0x81->秒钟
我们如果需要写入时钟的话,需要先将0x8E位置的最高位WP置0,解除写保护后才能进行写入,写入的地址为0x84->小时 0x82->分钟 0x80->秒钟
所以我们的代码编写就很简单了,我们传入数据为数组的指针
1
2
|
void Set_Rtc(unsigned char *ucRtc)
void Read_Rtc(unsigned char *ucRtc)
|
这里给一个调用的示例
1
2
3
|
unsigned char time[3] = {11,12,13};//11:12:13
Set_Rtc(time);
Read_Rtc(time);
|
我们在写入之前需要进行解锁写保护,写完后就进行写保护
1
2
3
|
Write_Ds1302_Byte(0x8e, 0x00);
-----
Write_Ds1302_Byte(0x8e, 0x80);
|
我们在使用的过程中可以直接使用for循环进行读取/写入,由于我们为了方便,所以我们在这里使用十进制进行存储和读取(本质上存储和读取的都是二进制BCD码,比如0x11我们读取为11,写入为11,在里面内部使用变量进行转换)
举个🌰,我们假设要写入的时间为23:59:58,那么我们实际上要写入芯片的数据为0x23 0x59 0x58
但是我们为了方便(因为有时候需要我们是设置时钟,加减的那种,直接使用bcd码不方便,需要我们判断是否达到0xa0然后进行转换),这里我们可以在里面写一个十进制转为十进制bcd码的公式就行,以写入0x23为例,我们输入给我们Set_Rtc函数的数据为23,我们要先将其转换为0x23才能进行写入,那么我们可以观察一下,本质上就是把十位和个位分开,然后再存入即可,我们分开很简单,直接/10和%10就可以取出十位和个位,然后再使用«4来进行高位移位,将我们的十位移动到高四位去,然后|上我们的个位(低四位)就可以将其拼接完成。
23->2,3->0010,0011->0100_0011
1
|
temp = ucRtc[i] / 10 << 4 | ucRtc[i] % 10;
|
同理,我们读取的时候读取到的都是十进制的bcd码,那么我们将其转换后再读出
0x23->0x2,0x3->2*10+3->23
1
|
ucRtc[i] = temp >> 4 * 10 + temp & 0x0f;
|
读取和写入的代码如下
1
2
3
4
5
6
7
8
9
10
|
//写入
for (i = 0; i < 3; i++) {
temp = ucRtc[i] / 10 << 4 | ucRtc[i] % 10;
Write_Ds1302_Byte(0x84 - 2 * i, temp);
}
//读取
for (i = 0; i < 3; i++) {
temp = Read_Ds1302_Byte(0x85 - 2 * i);
ucRtc[i] = temp >> 4 * 10 + temp & 0x0f;
}
|
需要补充的完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 十进制 11 -> 0x11
void Set_Rtc(unsigned char *ucRtc) {
unsigned char i;
unsigned char temp;
Write_Ds1302_Byte(0x8e, 0x00);
for (i = 0; i < 3; i++) {
temp = ucRtc[i] / 10 << 4 | ucRtc[i] % 10;
Write_Ds1302_Byte(0x84 - 2 * i, temp);
}
Write_Ds1302_Byte(0x8e, 0x80);
}
// 十进制 0x11 -> 11
void Read_Rtc(unsigned char *ucRtc) {
unsigned char i;
unsigned char temp;
for (i = 0; i < 3; i++) {
temp = Read_Ds1302_Byte(0x85 - 2 * i);
ucRtc[i] = temp >> 4 * 10 + temp & 0x0f;
}
}
|
完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
/* # DS1302代码片段说明
1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
2.
参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
中对单片机时钟频率的要求,进行代码调试和修改。
*/
#include "ds1302.h"
#include "intrins.h"
sbit SDA = P2 ^ 3;
sbit RST = P1 ^ 3;
sbit SCK = P1 ^ 7;
//
void Write_Ds1302(unsigned char temp) {
unsigned char i;
for (i = 0; i < 8; i++) {
SCK = 0;
SDA = temp & 0x01;
temp >>= 1;
SCK = 1;
}
}
//
void Write_Ds1302_Byte(unsigned char address, unsigned char dat) {
RST = 0;
_nop_();
SCK = 0;
_nop_();
RST = 1;
_nop_();
Write_Ds1302(address);
Write_Ds1302(dat);
RST = 0;
}
//
unsigned char Read_Ds1302_Byte(unsigned char address) {
unsigned char i, temp = 0x00;
RST = 0;
_nop_();
SCK = 0;
_nop_();
RST = 1;
_nop_();
Write_Ds1302(address);
for (i = 0; i < 8; i++) {
SCK = 0;
temp >>= 1;
if (SDA) temp |= 0x80;
SCK = 1;
}
RST = 0;
_nop_();
SCK = 0;
_nop_();
SCK = 1;
_nop_();
SDA = 0;
_nop_();
SDA = 1;
_nop_();
return (temp);
}
// 十进制 11 -> 0x11
void Set_Rtc(unsigned char *ucRtc) {
unsigned char i;
unsigned char temp;
Write_Ds1302_Byte(0x8e, 0x00);
for (i = 0; i < 3; i++) {
temp = ucRtc[i] / 10 << 4 | ucRtc[i] % 10;
Write_Ds1302_Byte(0x84 - 2 * i, temp);
}
Write_Ds1302_Byte(0x8e, 0x80);
}
// 十进制 0x11 -> 11
void Read_Rtc(unsigned char *ucRtc) {
unsigned char i;
unsigned char temp;
for (i = 0; i < 3; i++) {
temp = Read_Ds1302_Byte(0x85 - 2 * i);
ucRtc[i] = temp >> 4 * 10 + temp & 0x0f;
}
}
|
Ds1302.h
1
2
3
|
#include <STC15F2K60S2.H>
void Set_Rtc(unsigned char *ucRtc);
void Read_Rtc(unsigned char *ucRtc);
|
Onewire温度模版建立
Onewire.c
官方的参考代码
这里有官方提供的参考代码,所以我们上面的时序可以不用书写,我们直接对下方的进行温度读取操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
/* # 单总线代码片段说明
1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
2.
参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
中对单片机时钟频率的要求,进行代码调试和修改。
*/
//
void Delay_OneWire(unsigned int t) {
unsigned char i;
while (t--) {
for (i = 0; i < 12; i++);
}
}
//
void Write_DS18B20(unsigned char dat) {
unsigned char i;
for (i = 0; i < 8; i++) {
DQ = 0;
DQ = dat & 0x01;
Delay_OneWire(5);
DQ = 1;
dat >>= 1;
}
Delay_OneWire(5);
}
//
unsigned char Read_DS18B20(void) {
unsigned char i;
unsigned char dat;
for (i = 0; i < 8; i++) {
DQ = 0;
dat >>= 1;
DQ = 1;
if (DQ) {
dat |= 0x80;
}
Delay_OneWire(5);
}
return dat;
}
//
bit init_ds18b20(void) {
bit initflag = 0;
DQ = 1;
Delay_OneWire(12);
DQ = 0;
Delay_OneWire(80);
DQ = 1;
Delay_OneWire(10);
initflag = DQ;
Delay_OneWire(5);
return initflag;
}
|
我们需要补充的代码
首先是我们需要绑定DQ的引脚,在原理图中可以看到

我们不需要管上方的时序的书写,只需要进行温度读取的代码书写就行,
下方是温度芯片的使用顺序(官方手册),首先我们需要运行init,然后再检查rom(可跳过),最后执行我们的功能函数,注意,一次只能使用一个功能。

我们可以在官方手册上找到需要运行的指令

当我们发送0xcc的时候,就会跳过ROM检查,并且这里也说明了,我们发送0x44就可以开启温度转换,发送0xbe就可以开始读取温度,但是要注意一个事情,我们同样可以在我们的官方的手册上面找到一段读取的语句,这里表明我们先读取低位,再读取高位,最后将其拼接后再*精度就是我们的数据了

温度的精度在文档中对应如下

精度为9->0.5;10->0.25;11->0.125;12(默认)->0.0625
我们就可以根据上面的描述来进行代码书写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
float rd_temperature() {
unsigned char low, high;
//开启温度转换
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();
return (float)(high << 8 | low) * 0.0625;
}
|
这里给一个调用示例
1
|
float temp = rd_temperature();
|
完整代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
/* # 单总线代码片段说明
1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
2.
参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
中对单片机时钟频率的要求,进行代码调试和修改。
*/
#include "onewire.h"
sbit DQ = P1 ^ 4;
//
void Delay_OneWire(unsigned int t) {
unsigned char i;
while (t--) {
for (i = 0; i < 12; i++);
}
}
//
void Write_DS18B20(unsigned char dat) {
unsigned char i;
for (i = 0; i < 8; i++) {
DQ = 0;
DQ = dat & 0x01;
Delay_OneWire(5);
DQ = 1;
dat >>= 1;
}
Delay_OneWire(5);
}
//
unsigned char Read_DS18B20(void) {
unsigned char i;
unsigned char dat;
for (i = 0; i < 8; i++) {
DQ = 0;
dat >>= 1;
DQ = 1;
if (DQ) {
dat |= 0x80;
}
Delay_OneWire(5);
}
return dat;
}
//
bit init_ds18b20(void) {
bit initflag = 0;
DQ = 1;
Delay_OneWire(12);
DQ = 0;
Delay_OneWire(80);
DQ = 1;
Delay_OneWire(10);
initflag = DQ;
Delay_OneWire(5);
return initflag;
}
float rd_temperature() {
unsigned char low, high;
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();
return (float)(high << 8 | low) * 0.0625;
}
|
Onewire.h
1
2
|
#include <STC15F2K60S2.H>
float rd_temperature();
|
IIC(PCF8591(AD/DA)与AT24C02(EEPROM))模版建立
iic.c
官方的参考代码
这里有官方提供的参考代码,所以我们上面的时序可以不用书写,我们直接进行AD读取与DA输出还有AT24C02的存储操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
/* # I2C代码片段说明
1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
2.
参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
中对单片机时钟频率的要求,进行代码调试和修改。
*/
#define DELAY_TIME 10
//
static void I2C_Delay(unsigned char n) {
do {
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
} while (n--);
}
//
void I2CStart(void) {
sda = 1;
scl = 1;
I2C_Delay(DELAY_TIME);
sda = 0;
I2C_Delay(DELAY_TIME);
scl = 0;
}
//
void I2CStop(void) {
sda = 0;
scl = 1;
I2C_Delay(DELAY_TIME);
sda = 1;
I2C_Delay(DELAY_TIME);
}
//
void I2CSendByte(unsigned char byt) {
unsigned char i;
for (i = 0; i < 8; i++) {
scl = 0;
I2C_Delay(DELAY_TIME);
if (byt & 0x80) {
sda = 1;
} else {
sda = 0;
}
I2C_Delay(DELAY_TIME);
scl = 1;
byt <<= 1;
I2C_Delay(DELAY_TIME);
}
scl = 0;
}
//
unsigned char I2CReceiveByte(void) {
unsigned char da;
unsigned char i;
for (i = 0; i < 8; i++) {
scl = 1;
I2C_Delay(DELAY_TIME);
da <<= 1;
if (sda) da |= 0x01;
scl = 0;
I2C_Delay(DELAY_TIME);
}
return da;
}
//
unsigned char I2CWaitAck(void) {
unsigned char ackbit;
scl = 1;
I2C_Delay(DELAY_TIME);
ackbit = sda;
scl = 0;
I2C_Delay(DELAY_TIME);
return ackbit;
}
//
void I2CSendAck(unsigned char ackbit) {
scl = 0;
sda = ackbit;
I2C_Delay(DELAY_TIME);
scl = 1;
I2C_Delay(DELAY_TIME);
scl = 0;
sda = 1;
I2C_Delay(DELAY_TIME);
}
|
我们需要补充的代码
首先是我们需要绑定sda和scl的引脚,在原理图中可以看到,并且要注意一个问题,我们用到了NOP函数,所以需要引入头文件intrins.h

1
2
3
|
#include "intrins.h"
sbit scl = P2 ^ 0;
sbit sda = P2 ^ 1;
|
我在这里需要说明一下IIC通信的方式,他是一个一个八位的数据,其中第一位是我们的发送地址,永远都是1,然后后面挂载了不同的外设,我们的蓝桥杯的板子挂了两个外设,一个是PCF8591,对应着AD/DA的功能;另一个是AT24C02,对应着EEPROM的存储功能。在我们板子上就长我们下面这个样子~~(我画的有点抽象)~~,所以我们就可以知道,如果我们要对AT24C02芯片写入数据的话,我们就需要使用地址为(1010_0000->0xa0),要读取数据的化,我们就要使用地址为(1010_0001->0xa1);要写入DA输出的数据的话,我们就需要使用地址为(1001_0000->0x90),要读取AD采样的数据的话,我们就需要使用地址为(1001_0001->0x91)

在我们使用IIC进行通信的时候,我们要遵循下面的运行逻辑
1
2
3
4
5
|
I2CStart();
I2CSendByte(地址);
I2CWaitAck();
I2CSendByte(功能);
I2CWaitAck();
|
AD与DA
从上面说明的内容的我们可以知道,如果我们要对PCF8591进行操作,那么就需要对地址0x90和0x91进行操作
我们现在先说一下AD的代码,通过观察原理图我们可以发现,AD有4个通道可以进行读取

其中AIN0通道是我们的外部测量通道(由于蓝桥杯现在还没有涉及到信号发生器,所以我们可以先暂时不用管这个通道)

AIN1通道很明显就是光明电阻的分压读取(光照越强,分压越高,采集到的结果越大),
AIN2通道为差分输入(我们也是不常用到的输入,因为没有信号发生器)
AIN3通道为滑动变阻器输入(我们测量滑动变阻器的分压,这个你左右旋钮看一下就知道哪边变大了)

我们查看一下PCF8591的手册可以看到,我们读取的地址的书写方式,这里我统一给一个说明
| 位次 |
情况说明 |
情况 |
| 第一位 |
固定位 |
0 |
| 第二位 |
是否需要开启DA输出 |
开启DA输出(1) 不开启DA输出(0) |
| 第三四位 |
选择输入通道的模式 差分输入具体要看下面的图 |
通道与编号对应输入(常用)(00) 通道差分输入(01,10,11) |
| 第五位 |
固定位 |
0 |
| 第六位 |
自动递增标志 (用于一个一个读取AD输入口) |
自我递增(没考过,因为我们一般都是单一读取)(1) 不自我递增(0) |
| 第七八位 |
用于指定我们读取的通道 |
通道0(00),在我们正常的对应输入下为外部信号输入 通道1(01),在我们正常的对应输入下为光明电阻分压输入 通道2(10),在我们正常的对应输入下为差分信号输入 通道3,在我们正常的对应输入下为滑动变阻器分压输入(11) |

我们那么这个时候常考的就是下面的形式,注意一下题目中是否需求DA输出,如果需要DA输出的话,这里使用地址的时候必须使用允许DA输出,
| 需要的数据 |
输入的地址 |
| 外部电压输入(开启DA) |
0100_0000->0x40 |
| 外部电压输入(不开启DA) |
0000_0000->0x00 |
| 光敏电阻分压输入(开启DA) |
0100_0001->0x41 |
| 光敏电阻分压输入(不开启DA) |
0000_0001->0x01 |
| 差分信号输入(开启DA) |
0100_0010->0x42 |
| 差分信号输入(不开启DA) |
0000_0010->0x02 |
| 滑动变阻器分压输入(开启DA) |
0100_0011->0x43 |
| 滑动变阻器分压输入(不开启DA) |
0000_0011->0x03 |
有了这样一个基础,我们就可以开始着手编写我们的AD代码了
我们只需要指定一下我们需要读取的通道的地址就行
1
|
unsigned char Ad_Read(unsigned char addr)
|
这里给一个调用示例,这里需要注意的是,如果你同时读取光敏电阻和滑动变阻器的值,他们会地址交换,你们可以自己去试试。
1
|
unsigned char temp = Ad_Read(0x01);//读取光敏电阻结果
|
首先我们需要选中我们的PCF芯片并且写入我们需要读取的地址,这里需要注意的就只有每次发送Byte后我们需要Wait一下
1
2
3
4
5
6
|
// 选择芯片为PCF
I2CStart();
I2CSendByte(0x90);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
|
然后重新开启一下通话(因为我们上面是进行写操作,现在要进行读操作),读取后我们需要发送一个Ack为1,表明我们这边接收数据完成,后续不用接受了,结束后就可以关闭对话Stop了
1
2
3
4
5
6
|
I2CStart();
I2CSendByte(0x91);
I2CWaitAck();
temp = I2CReceiveByte();
I2CSendAck(1);
I2CStop();
|
AD完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
unsigned char Ad_Read(unsigned char addr) {
unsigned char temp;
// 选择芯片为PCF
I2CStart();
I2CSendByte(0x90);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStart();
I2CSendByte(0x91);
I2CWaitAck();
temp = I2CReceiveByte();
I2CSendAck(1);
I2CStop();
return temp;
}
|
说完AD我们就要说到DA了
和AD一样,我们只需要传入我们需要写入的数据就行,但是要注意一下我们的数据为数字电压,即0-255,我们输出后的电压是模拟电压,即0-5V,所以我们是一一映射过去的
1
|
void Da_Write(unsigned char dat)
|
举个🌰,我们如果想要输出电压为2V,那么我们就应该
如果想要输出的电压为2.5V,那么我们就应该,这里不用关心他会出现不匹配的情况,C语言底层会自动转换为unsigned char类型的,这类有啊注意
首先我们需要选中我们的PCF芯片并且给他我们的地址信息(前面提到过如果要开启DA输出,那么我们应该写入的地址),这里需要注意的就只有每次发送Byte后我们需要Wait一下,这里的0x41可以更换为0x40-0x43的任何一个,但是最好和题目中要求AD需要读取的通道一致(这里也要注意在你读取AD的时候加上0x4)
1
2
3
4
5
6
7
8
9
10
|
void Da_Write(unsigned char dat) {
// 选择芯片为PCF
I2CStart();
I2CSendByte(0x90);
I2CWaitAck();
I2CSendByte(0x41);
I2CWaitAck();
I2CSendByte(dat);
I2CWaitAck();
}
|
AD与
EEPROM
从上面说明的内容的我们可以知道,如果我们要对AT24C02进行操作,那么就需要对地址0xa0和0xa1进行操作
本质上我们的EEPROM写入与读取和AD/DA是一致的,但是我们需要注意一个点,因为我们通常写入/读取的不止一个数据,我们一般是一个数组写进去和读取,所以我们就需要注意这个点,我们的操作如下,传递的数据都是数组的首地址(如果存单个数据只需要使用&name就行),传入参数为需要写入/读取数组首地址名称))str,需要写入/读取的数组地址addr,数组的大小num
1
2
|
void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned char num)
void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned char num)
|
这里给一个调用的示例,需要注意一下如果写入为unsigned int,可以不用分为高低位存储,但是需要分为高低位读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
unsigned char arr_input[5]={1,2,3,4,5};
unsigned char arr_output[5];
EEPROM_Write(arr_input,0,5);
EEPROM_Read(arr_output,0,5);
unsigned char input = 5;
unsigned char output;
EEPROM_Write(&input,0,1);
EEPROM_Read(&output,0,1);
unsigned int input = 5;
unsigned int output;
unsigned char high,low;
EEPROM_Write(&input,0,2);
EEPROM_Read(&high,0,1);
EEPROM_Read(&low,1,1);
output = high<<4 | low;
|
现在我来说一下实现细节
先说明一下EEPROM_Read函数,和上文的AD读取一样,我们需要先选中AT24C02,并且功能为写,然后进行位置的传输,确认我们需要读取的地址
1
2
3
4
5
|
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
|
在我们读取的时候,和上文AD一样,我们需要重新开启一个对话进行读取
1
2
3
|
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
|
但是在我们读取的时候需要注意一个点,我们使用的数组进行的数据读取,并且在我们读取的时候对指针进行++操作,当我们还没有读完数据的时候,我们发送Ack为0,代表我们需要继续读取,当我们读取数据结束后,我们发送Ack为1,表示读取结束,最后也别忘记需要进行Stop停止交流。
1
2
3
4
5
6
7
8
9
10
11
|
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
while (num--) {
*str++ = I2CReceiveByte();
if (num)
I2CSendAck(0);
else
I2CSendAck(1);
}
I2CStop();
|
在这里说明一下*str++代表的含义,这是c的一个写法,等价于下面的代码
1
2
3
4
5
6
7
8
9
|
unsigned char arr[5];
unsigned char i = 0;
unsigned char num = 5;
while(num--)
{
arr[i] = I2CReceiveByte();
i++;
-----
}
|
完整的代码EEPROM_READ如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned char num) {
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
while (num--) {
*str++ = I2CReceiveByte();
if (num)
I2CSendAck(0);
else
I2CSendAck(1);
}
I2CStop();
}
|
我们可以从上面看到,AD和EEPROM_READ可以进行类比;同理,我们的DA和EEPROM_Write也可以进行类比,但是EEPROM_Write有一个需要注意的点,我会在下面进行说明
我们第一步还是选择我们的芯片,选择我们需要写入的地址
1
2
3
4
5
|
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
|
第二步就是使用一个循环进行写入,这里需要注意一个问题,就是我们每写入一个数据,都需要I2C_Delay一下,注意不是Delay,而是I2C_Delay,经过我们测试,当这个值为200 的时候效果是可以的,并且这里有个最最重要的点,在我们Stop结束后,我们需要进行4个255时间的I2C_Delay~~(其实我在群里面说过,但你们一直觉得我在讲乐子)~~
1
2
3
4
5
6
7
8
9
10
|
while (num--) {
I2CSendByte(*str++);
I2CWaitAck();
I2C_Delay(200);
}
I2CStop();
I2C_Delay(255);
I2C_Delay(255);
I2C_Delay(255);
I2C_Delay(255);
|
完整的代码EEPROM_Write如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned char num) {
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
while (num--) {
I2CSendByte(*str++);
I2CWaitAck();
I2C_Delay(200);
}
I2CStop();
I2C_Delay(255);
I2C_Delay(255);
I2C_Delay(255);
I2C_Delay(255);
}
|
完整代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
|
/* # I2C代码片段说明
1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
2.
参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
中对单片机时钟频率的要求,进行代码调试和修改。
*/
#include "iic.h"
#include "intrins.h"
#define DELAY_TIME 5
sbit scl = P2 ^ 0;
sbit sda = P2 ^ 1;
//
static void I2C_Delay(unsigned char n) {
do {
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
} while (n--);
}
//
void I2CStart(void) {
sda = 1;
scl = 1;
I2C_Delay(DELAY_TIME);
sda = 0;
I2C_Delay(DELAY_TIME);
scl = 0;
}
//
void I2CStop(void) {
sda = 0;
scl = 1;
I2C_Delay(DELAY_TIME);
sda = 1;
I2C_Delay(DELAY_TIME);
}
//
void I2CSendByte(unsigned char byt) {
unsigned char i;
for (i = 0; i < 8; i++) {
scl = 0;
I2C_Delay(DELAY_TIME);
if (byt & 0x80) {
sda = 1;
} else {
sda = 0;
}
I2C_Delay(DELAY_TIME);
scl = 1;
byt <<= 1;
I2C_Delay(DELAY_TIME);
}
scl = 0;
}
//
unsigned char I2CReceiveByte(void) {
unsigned char da;
unsigned char i;
for (i = 0; i < 8; i++) {
scl = 1;
I2C_Delay(DELAY_TIME);
da <<= 1;
if (sda) da |= 0x01;
scl = 0;
I2C_Delay(DELAY_TIME);
}
return da;
}
//
unsigned char I2CWaitAck(void) {
unsigned char ackbit;
scl = 1;
I2C_Delay(DELAY_TIME);
ackbit = sda;
scl = 0;
I2C_Delay(DELAY_TIME);
return ackbit;
}
//
void I2CSendAck(unsigned char ackbit) {
scl = 0;
sda = ackbit;
I2C_Delay(DELAY_TIME);
scl = 1;
I2C_Delay(DELAY_TIME);
scl = 0;
sda = 1;
I2C_Delay(DELAY_TIME);
}
unsigned char Ad_Read(unsigned char addr) {
unsigned char temp;
// 选择芯片为PCF
I2CStart();
I2CSendByte(0x90);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStart();
I2CSendByte(0x91);
I2CWaitAck();
temp = I2CReceiveByte();
I2CSendAck(1);
I2CStop();
return temp;
}
// 数字电压255->5V
void Da_Write(unsigned char dat) {
// 选择芯片为PCF
I2CStart();
I2CSendByte(0x90);
I2CWaitAck();
I2CSendByte(0x41);
I2CWaitAck();
I2CSendByte(dat);
I2CWaitAck();
}
void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned char num) {
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
while (num--) {
I2CSendByte(*str++);
I2CWaitAck();
I2C_Delay(200);
}
I2CStop();
I2C_Delay(255);
I2C_Delay(255);
I2C_Delay(255);
I2C_Delay(255);
}
void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned char num) {
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
while (num--) {
*str++ = I2CReceiveByte();
if (num)
I2CSendAck(0);
else
I2CSendAck(1);
}
I2CStop();
}
|
iic.h
1
2
3
4
5
|
#include <STC15F2K60S2.H>
unsigned char Ad_Read(unsigned char addr);
void Da_Write(unsigned char dat);
void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned char num);
void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned char num);
|
Uart串口模版建立
uart.c
在这里我们需要注意,串口官方是没有给底层的,我们需要自己手搓,在这里我给的代码为重定向代码。
首先是使用isp生成一个用定时器2计时的串口1的代码,波特率为9600(比赛的时候可能不是9600,要根据题目来看),定时器时钟选用12T,数据位为8位

我们直接把他复制到我们代码里面就行,最后别忘记加一个EA=1来开启总中断,还有些isp版本可能比较老,没有使能中断的选项,所以需要自己加上ES=1
1
2
3
4
5
6
7
8
9
10
11
|
void Uart1_Init(void) // 9600bps@12.000MHz
{
SCON = 0x50; // 8位数据,可变波特率
AUXR |= 0x01; // 串口1选择定时器2为波特率发生器
AUXR |= 0x04; // 定时器时钟1T模式
T2L = 0xC7; // 设置定时初始值
T2H = 0xFE; // 设置定时初始值
AUXR |= 0x10; // 定时器2开始计时
ES = 1; // 使能串口1中断
EA = 1;
}
|
我们这里不需要实现复杂的函数,只需要实现一个重定向就行,使用printf直接将数据发送到串口就行,我现在说一下步骤
我们通过查看c51里面(注意是c51,不是标准的c语言函数库,直接在keil里面右键打开),我们可以看到下方有一个putchar的一个extern的函数定义,这就是我们需要重载的函数,因为我们printf本质上就是调用putchar的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
/*--------------------------------------------------------------------------
STDIO.H
Prototypes for standard I/O functions.
Copyright (c) 1988-2002 Keil Elektronik GmbH and Keil Software, Inc.
All rights reserved.
--------------------------------------------------------------------------*/
#ifndef __STDIO_H__
#define __STDIO_H__
#ifndef EOF
#define EOF -1
#endif
#ifndef NULL
#define NULL ((void *) 0)
#endif
#ifndef _SIZE_T
#define _SIZE_T
typedef unsigned int size_t;
#endif
#pragma SAVE
#pragma REGPARMS
extern char _getkey (void);
extern char getchar (void);
extern char ungetchar (char);
extern char putchar (char);
extern int printf (const char *, ...);
extern int sprintf (char *, const char *, ...);
extern int vprintf (const char *, char *);
extern int vsprintf (char *, const char *, char *);
extern char *gets (char *, int n);
extern int scanf (const char *, ...);
extern int sscanf (char *, const char *, ...);
extern int puts (const char *);
#pragma RESTORE
#endif
|
我们可以知道,串口在发送的时候,会将数据放进缓冲区SBUF(如果你用串口2那就是S2BUF,但是这里没涉及到,就不用管了),当我们正在发送的时候TI标志位为0,当我们发送完成后,他就会被置为0,然后我们需要将ch返回(这个是函数本身就需要返回的char)
1
2
3
4
5
6
|
extern char putchar(char ch) {
SBUF = ch;
while (TI == 0);
TI = 0;
return ch;
}
|
uart.h
1
2
3
4
|
#include <STC15F2K60S2.H>
#include "stdio.h"
void Uart1_Init(void);
|
Ul超声波模版建立
ul.c
在这里我们需要注意,串口官方是没有给底层的,我们需要自己手搓。我们通过查询原理图可以看到,超声波的T为P10,R为P11,所以我们需要定义一下
1
2
|
sbit Tx = P1 ^ 0;
sbit Rx = P1 ^ 1;
|

现在我们来进行逻辑的书写,我们首先要知道超声波测距的原理,我发送数据后,等待接收数据,那么我通过计算发送和接收中的间隔时间,就可以得到我们的距离,这里用数学公式可以进行计算,一般来说声速都是340m/s(也不排除题目里面和你说要对其进行变换啥的【比如第十四届国赛】,这里就按照声速为340m/s来进行说明)
$$
d = \frac{vt}{2} = \frac{340 \text{ m/s} \times t}{2} = 170 \text{ m/s} \times t = 1.7 \times 10^4 \text{ cm/s} \times t
$$
我们这里有一个单位换算需要进行注意,t的单位为us,也就是
$$
{10^{ - 6}}s
$$
我们进行完整的单位换算就可以知道d的距离了(单位为cm)
$$
d = 1.7*{10^4}*{10^{ - 6}}t = 0.017t
$$
图来自微信群友chmod-wrx绘制(我绘图太🌶︎🐔了)

根据我们上方的分析,我们可以知道我们首先需要进行发送,我们在这里发送8个40kHz的方波进行测量,那么周期就是
$$
{1 \over {40k}} = 25 \cdot {10^{ - 6}}s = 25us
$$
也就是我们需要
$
12us
$
将发送的电平进行反转
代码如下,我们首先使用isp生成一个12us的延时函数,这里需要注意的是要正确使用系统频率为12MHz;如果这里测量结果有问题,可以尝试将33修改为38或者其他的数据,因为软件延时不准。

1
2
3
4
5
6
7
8
9
|
void Delay12us(void) //@12.000MHz
{
unsigned char data i;
_nop_();
_nop_();
i = 33;//可以修改为其他数据,比如38
while (--i);
}
|
然后就是我们的超声波发送函数,发送8个4kHz的方波
1
2
3
4
5
6
7
8
9
|
void Ut_Wave_Init() {
unsigned char i;
for (i = 0; i < 8; i++) {
Tx = 1;
Delay12us();
Tx = 0;
Delay12us();
}
}
|
最后是我们最重要的数据接收与转换函数的书写,在这里我们使用的pca定时器,可以节省定时器来进行其他的操作(因为ne555强制占用定时器0,串口考到就会占用一个定时器,还有一个定时器需要用来进行函数轮询,所以我们就必须用其他的定时器进行超声波的操作),其实PCA本质上还是一个定时器,所以他也存在一些定时器的参数,比如TLx->CL;THx->CH;TMOD->CMOD;TRx->CR;TFx->CF
我们首先需要对定时器的CH和CL清零,为了我们计数做准备;然后将CMOD配置为0x00,十六位不重载定时器。
1
2
|
CH = CL = 0;
CMOD = 0x00;
|
在我们发送超声波的时候,我们需要将总中断关闭,在我们发送完毕后就关掉,在我们发送完成后,就打开CR开始计时,等待我们收到回应(收到回应后Rx为0)或者计数溢出(溢出后CF为1),我们就关闭CR开始读取数据
1
2
3
4
5
6
7
|
EA = 0;
Ut_Wave_Init();
EA = 1;
CR = 1;
while (Rx && !CF);
CR = 0;
|
如果我们是因为收到回应而结束(那么就代表我们测量到了数据),那么我们就进行时间读取,如下所示,你们可能会好奇为什么+3,这是因为我们实际测量发现,+3后在远程数据(测量结果>10的时候)误差会比较小,而近程数据(测量数据<10)满足误差范围,我们也不可能测量得到0这个数据(题目中一般都不会测到0)(所以我们才把他视为测量错误的返回结果)
1
2
|
time = CH << 8 | CL;
return (0.017 * time + 3);
|
如果我们是因为计数溢出(没有收到返回的信号,代表测量错误),我们就直接返回为0,表示错误结果
转换的完整代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
unsigned char Ut_Wave_Data() {
unsigned int time;
CH = CL = 0;
CMOD = 0x00;
EA = 0;
Ut_Wave_Init();
EA = 1;
CR = 1;
while (Rx && !CF);
CR = 0;
if (!CF) { // us -> s 10^(-6)
// m -> cm 10^2
// 10^(-4)
// L = V*T/2=340*time/2=170*10^(-4)*time=0.017*time
time = CH << 8 | CL;
return (0.017 * time + 3);
} else {
CF = 0;
return 0;
}
}
|
完整代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
#include "ul.h"
#include "intrins.h"
sbit Tx = P1 ^ 0;
sbit Rx = P1 ^ 1;
void Delay12us(void) //@12.000MHz
{
unsigned char data i;
_nop_();
_nop_();
i = 33; // 38
while (--i);
}
void Ut_Wave_Init() {
unsigned char i;
for (i = 0; i < 8; i++) {
Tx = 1;
Delay12us();
Tx = 0;
Delay12us();
}
}
unsigned char Ut_Wave_Data() {
unsigned int time;
CH = CL = 0;
CMOD = 0x00;
EA = 0;
Ut_Wave_Init();
EA = 1;
CR = 1;
while (Rx && !CF);
CR = 0;
if (!CF) { // us -> s 10^(-6)
// m -> cm 10^2
// 10^(-4)
// L = V*T/2=340*time/2=170*10^(-4)*time=0.017*time
time = CH << 8 | CL;
return (0.017 * time + 3);
} else {
CF = 0;
return 0;
}
}
|
ul.h
1
2
|
#include <STC15F2K60S2.H>
unsigned char Ut_Wave_Data();
|
init初始化模版建立
init.c
我们在启动开发板的时候最好对其进行初始化,关闭蜂鸣器和继电器,MOTOR和全部LED灯,避免上电情况有误
1
2
3
4
5
6
7
8
9
10
|
#include "init.h"
void System_Init() {
P0 = 0xff;
P2 = P2 & 0x1f | 0x80;
P2 &= 0x1f;
P0 = 0x00;
P2 = P2 & 0x1f | 0xa0;
P2 &= 0x1f;
}
|
init.h
1
2
|
#include <STC15F2K60S2.H>
void System_Init();
|
main主函数书写
main.c
现在我们来进行主函数的编写,我也拆分为模块,但是最后还是会给一个整体的代码
全部变量的归纳
这里我把全部的全局变量放上来
- 定义了Led的变量数组,用于控制8个灯的亮灭情况
- 定义了Seg_Pos的变量,用于控制数码管位选情况
- 定义了Seg_Buf的变量数组,用于控制每个数码管显示第数据
- 定义了Seg_Point的变量数组,用于控制某一位数码管是否显示小数点
- 定义了Uart_Buf的变量数组,用于获取串口接收的数据
- 定义了Uart_Rx_Index的变量,用于对串口接收后的数组进行索引
- 定义了Uart_flag的变量,用于判断当前是否正处于串口接收状态
- 定义了Sys_Tick的变量,用于嘀嗒计时,判断串口是否接收状态超过了10ms
- 定义了ucRtc的变量数组,用于存放时钟数据,时分秒
- 定义了int型的计时变量time_all_1s,用于全局定时
- 定义了int型的频率变量Freq,用于测频结果的存放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* LED与数码管 */
unsigned char ucLed[8] = {0, 0, 0, 0, 0, 0, 0, 0};
unsigned char Seg_Pos;
unsigned char Seg_Buf[8] = {10, 10, 10, 10, 10, 10, 10, 10};
unsigned char Seg_Point[8] = {0, 0, 0, 0, 0, 0, 0, 0};
/* 串口数据*/
unsigned char Uart_Buf[10];
unsigned char Uart_Rx_Index;
bit Uart_flag;
unsigned char Sys_Tick;
/* 时间*/
unsigned char ucRtc[3] = {11, 11, 11};
unsigned int time_all_1s;
/* 数据 */
unsigned int Freq;//ne555测频
|
数据读取模块
我们单独写一个模块对数据进行读取,方便我们进行不同数据的操作,这里使用%的操作来进行时间的定时,比如我们的时间读取,就每隔50ms(因为我定时器是1ms)进行一次;AD读取每隔100ms一次;温度读取每隔500ms一次(这里仅作参考,后续还是要自己搓一下试试)
1
2
3
4
5
6
7
8
9
10
11
|
void Data_Proc()
if (time_all_1s % 50 == 0) {
// 时间读取
}
if (time_all_1s % 100 == 0) {
// AD读取
}
if (time_all_1s % 500 == 0) {
// 温度读取
}
}
|
Key键盘模块
这个模块是重中之重,因为用到了一个很精妙的三行代码消抖方法,这个在我以前的视频蓝桥杯单片机按键考点里面提到过,点击这行字体就可以跳转过去,我这里简单说明一下,我们的逻辑如下,Key_Val,Key_Old,Key_Down,Key_Up,这几个变量都是下面的逻辑,我们这里对其的原理进行解析
瞬时值获取-> 获取按下的值,获取抬起的值->更新⼀下值
Down和Up都是瞬时值,Old比Val延后10ms,基于这个特性我们可以进行书写代码

对应的代码如下,我们将这四个变量设置为静态局部变量,可以有效节省内存空间,使用if语句那一段表明我们选择10ms跑一次代码(因为我们定时是1ms的)
1
2
3
4
5
6
7
8
|
void Key_Proc() {
static unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
if (time_all_1s % 10) return;
Key_Val = Key_Read();
Key_Down = Key_Val & (Key_Old ^ Key_Val);
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);
Key_Old = Key_Val;
}
|
如果理解不了上面的图和代码,我就拿一个🌰来进行验证,比如我们按下/抬起S4来做一下验证
| 键盘状态 |
Key_Old |
Key_Val |
Key_Old^Key_Val |
Key_Down |
Key_Up |
| 未按下 |
0000 |
0000 |
0000^0000=0000 |
0000 |
0000 |
| 按下过程中(10ms) |
0000 |
0100 |
0000^0100=0100 |
0100&0100=0100 |
1011&0100=0000 |
| 按下稳定(10ms后) |
0100 |
0100 |
0100^0100=0000 |
0100&0000=0000 |
1011&0000=0000 |
| 抬起过程(10ms) |
0100 |
0000 |
0100^0000=0100 |
0000&0100=0000 |
1111&0100=0100 |
我们可以从这里看出,当我们按下的时候,那一瞬间Down会是4(注意是一瞬间),其余时候都是0;同理Up也是一样,当我们抬起的瞬间Up会是4(注意是一瞬间),其余时候是0
那么我们如何在这个代码里面进行书写呢,其实很简单,我们一般判断按键都是判断的按键按下,即Down的数据,那么我们按照下面这样写就行,下面就是按下S4的一个示例
1
2
3
4
5
6
7
8
9
10
|
void Key_Proc() {
static unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
if (time_all_1s % 10) return;
Key_Val = Key_Read();
Key_Down = Key_Val & (Key_Old ^ Key_Val);
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);
Key_Old = Key_Val;
if(Key_Down == 4)
//执行按下S4的函数
}
|
Seg数码管模块
这个和V2模版不一样,他将数据读取放出去了,所以我们的数码管里面不过多涉及到数据读取,这里选择的是20ms进行一次数码管状态更新
1
2
3
|
void Seg_Proc() {
if (time_all_1s % 20) return;
}
|
如何使用呢,其实也非常简单,我们只需要按照下方的写法即可(因为一般来说我们都有多个界面进行切换,我们定义一个变量Seg_Show_Mode来进行书写,如果有子界面的话,拿我们就再定义一个mode就行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void Seg_Proc() {
if (time_all_1s % 20) return;
switch(Seg_Show_Mode)
{
case 0:
//xx界面
Seg_Buf[0] = xx;
Seg_Buf[1] = xx;
break;
case 1:
//xx界面
break;
}
}
|
Led灯模块
这里面我的话经常放的是Led,蜂鸣器,继电器,MOTOR口的相关逻辑,这里不需要设置定时进入
使用示例如下
1
2
3
4
5
6
7
8
9
10
|
void Led_Proc() {
ucLed[0] = 1;//L1点亮
ucLed[1] = 0;//L2熄灭
Relay(1);//打开继电器(L10亮)
Beep(1);//蜂鸣器开启
MOTOR(1);//MOTOR输出高电平
Relay(0);//关闭继电器(L10灭)
Beep(0);//蜂鸣器关闭
MOTOR(0);//MOTOR输出低电平
}
|
Uart串口模块
我们在这里使用了串口超时解析,即10ms内没有收集到串口的信息的时候我们才进入并进行解析(后续在定时器里面会提到这个Sys_Tick),在我们解析结束后使用memset函数将接收的数组置为0(为了防止后续解析被干扰),并将索引置为0,为下一次接收解析进行准备
1
2
3
4
5
6
7
8
9
|
void Uart_Proc() {
if (Uart_Rx_Index == 0) return;
if (Sys_Tick >= 10) {
Sys_Tick = Uart_flag = 0;
memset(Uart_Buf, 0, Uart_Rx_Index);
Uart_Rx_Index = 0;
}
}
|
使用示例,这里发送小写的ok返回hello,注意一下,判断的时候用的是单引号而不是双引号
1
2
3
4
5
6
7
8
9
10
|
void Uart_Proc() {
if (Uart_Rx_Index == 0) return;
if (Sys_Tick >= 10) {
Sys_Tick = Uart_flag = 0;
if(Uart_Buf[0]=='o'&&Uart_Buf[1]=='k')
printf("hello");
memset(Uart_Buf, 0, Uart_Rx_Index);
Uart_Rx_Index = 0;
}
}
|
定时器0初始化(NE555模块)
这个模块是最有意思的,首先我们需要注意一下,P34和singal的跳线帽要插在一起,并且按键方面需要删除P34,然后才能开始测,这里依然用isp生成一下初始化的定时器0,但是这里要注意,新版的不能生成0ms,我们就随便生成个时间,然后按照我这样勾选后复制粘贴就行

我们需要将TL0和TH0的值改为0,并且在TMOD &= 0xF0;下面对其进行配置 TMOD |= 0x05;将其变为十六位不自动重载(这里具体可以参考手册里面对于定时器的配置,我之前的视频有提到蓝桥杯单片机ne555模块,点击即可观看),这里别忘了EA开启总中断(这是我的习惯),频率的读取放在定时器1那里讲
1
2
3
4
5
6
7
8
9
10
11
|
void Timer0_Init(void) // 1毫秒@12.000MHz
{
AUXR &= 0x7F; // 定时器时钟12T模式
TMOD &= 0xF0; // 设置定时器模式
TMOD |= 0x05;
TL0 = 0; // 设置定时初始值
TH0 = 0; // 设置定时初始值
TF0 = 0; // 清除TF0标志
TR0 = 1; // 定时器0开始计时
EA = 1;
}
|
定时器1初始化
这是我们轮询的定时器的初始化代码,我们使用isp生成1ms定时器1的代码(记得勾选上使能),老版本没有使能,所以记得加上ET1 = 1开启

当然,不要忘记EA = 1,
1
2
3
4
5
6
7
8
9
10
11
|
void Timer1_Init(void) // 1毫秒@12.000MHz
{
AUXR &= 0xBF; // 定时器时钟12T模式
TMOD &= 0x0F; // 设置定时器模式
TL1 = 0x18; // 设置定时初始值
TH1 = 0xFC; // 设置定时初始值
TF1 = 0; // 清除TF1标志
TR1 = 1; // 定时器1开始计时
ET1 = 1; // 使能定时器1中断
EA = 1;
}
|
定时器1中断(重点)
我们在这里放了一堆东西,这里我做一下说明,我们每个中断都有对应的中断号(可以在官方的手册查到),这里定时器1的中断号是3
然后是我们的一个全局定时器(1s,注意time_all_1s的类型要是unsigned int,不然会溢出),在这个定时器里面我们对频率进行了读取,将TH0 « 8 | TL0的结果给了Freq(注意一下Freq的类型要是unsigned int,不然会溢出),在我们读取完数据后,将TH0和TL0进行了置0手动重载,方便下一次进行测量
接着就是一个和Sys_Tick相关的代码,当我们接收到数据的时候Uart_flag会置为1,然后将Sys_Tick置为0,也就是说我们一段时间不接受数据的话,那么Sys_Tick的值会一直累加,直到达到10ms(我们上面在Uart串口模块里面的设定),Uart_flag置为0,Sys_Tick的值置为0,为下一轮串口解析做准备
然后就是我们的一个位选的代码,因为我们数码管本质上是一位一位显示,所以我们需要写一个位选的变量来进行位选,这里使用的Seg_Pos = (++Seg_Pos) % 8将Seg_Pos这个变量在0~7中间进行循环
接着就是我们的数码管显示函数,这里不需要多说了,保持他不变就行,传入为位选、位选对应的段选显示、位选对应的小数点显示
最后就是我们的Led显示函数,这里我并没有选择和数码管同步,而是选择在中断里面写了个for循环来进行显示,因为它不涉及到扫描等操作,本质上就是可以直接一起显示。
1
2
3
4
5
6
7
8
9
10
11
12
|
void Timer1_Isr(void) interrupt 3 {
unsigned char i;
if (++time_all_1s == 1000) {
time_all_1s = 0;
Freq = TH0 << 8 | TL0;
TH0 = TL0 = 0;
}
if (Uart_flag) Sys_Tick++;
Seg_Pos = (++Seg_Pos) % 8;
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], Seg_Point[Seg_Pos]);
for (i = 0; i < 8; i++) Led_Disp(i, ucLed[i]);
}
|
串口中断(这里代码写死)
我们串口有一个接收中断,串口1的中断号查询后发现为4
我们第一行就是判断是否RI为1,因为RI为1则代表有信息过来了,当我们收集到信息时,我们将Uart_flag置为1,Sys_Tick置为0,开始嘀嗒计时,当我们10ms没有接收到新的数据的时候,我们就认为数据接收完成,可以开始进行串口处理,后面的接收就不需要多讲了,我们是一位一位进行接收的,而读写都是用的SBUF这个缓冲区(串口1,如果是串口2则是S2BUF,但是串口2不会考,所以这里不用管),我们接收完成后,将RI置为0,为下一次接收做准备,最后一行的>10清零,则是为了避免出现传入数据溢出,导致程序异常,这里的10取决于你的字符串数组的位数,我在最上面定的是10位,所以这里写的10
1
2
3
4
5
6
7
8
9
10
|
void Uart1_Isr(void) interrupt 4 {
if (RI) {
Uart_flag = 1;
Sys_Tick = 0;
Uart_Buf[Uart_Rx_Index] = SBUF;
Uart_Rx_Index++;
RI = 0;
}
if (Uart_Rx_Index > 10) Uart_Rx_Index = 0;
}
|
main主函数书写
这是我们的一个最初的函数,没什么好说明的,就是需要进行一些初始化而已,但是顺序不要写错了,首先是系统初始化,然后是定时器0(ne555),最后是定时器1(轮询)的初始化。
你可以发现,我在这里进行了一次温度的空读取,因为温度上电后会显示85(温度转换的时间),如果在后面才给他开始温度转换的话,那么很有可能跳85,导致测量结果错误,这里的延时函数也是用isp直接生成的,注意系统时间别选错了就行

我们写完初始化之后就需要写一下循环了,51单片机里面必须将循环写死,所以我们用了while(1),这里的顺序倒没什么特殊的要求,但是尽量按照我这个顺序就行,不会出问题的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void main() {
System_Init();
Timer0_Init();
Timer1_Init();
Set_Rtc(ucRtc);
rd_temperature();
Delay750ms();
while (1) {
Data_Proc();
Key_Proc();
Seg_Proc();
Uart_Proc();
Led_Proc();
}
}
|
完整的main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
|
#include "main.h"
/* LED与数码管 */
unsigned char ucLed[8] = {0, 0, 0, 0, 0, 0, 0, 0};
unsigned char Seg_Pos;
unsigned char Seg_Buf[8] = {10, 10, 10, 10, 10, 10, 10, 10};
unsigned char Seg_Point[8] = {0, 0, 0, 0, 0, 0, 0, 0};
/* 串口数据*/
unsigned char Uart_Buf[10];
unsigned char Uart_Rx_Index;
bit Uart_flag;
unsigned char Sys_Tick;
/* 时间*/
unsigned char ucRtc[3] = {11, 11, 11};
unsigned int time_all_1s;
/* 数据 */
unsigned int Freq;
void Data_Proc() {
if (time_all_1s % 50 == 0) {
// 时间读取
}
if (time_all_1s % 100 == 0) {
// AD读取
}
if (time_all_1s % 500 == 0) {
// 温度读取
}
}
/* 键盘处理*/
void Key_Proc() {
static unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
if (time_all_1s % 10) return;
Key_Val = Key_Read();
Key_Down = Key_Val & (Key_Old ^ Key_Val);
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);
Key_Old = Key_Val;
}
/* 数码管处理*/
void Seg_Proc() {
if (time_all_1s % 20) return;
}
void Led_Proc() {}
void Uart_Proc() {
if (Uart_Rx_Index == 0) return;
if (Sys_Tick >= 10) {
Sys_Tick = Uart_flag = 0;
memset(Uart_Buf, 0, Uart_Rx_Index);
Uart_Rx_Index = 0;
}
}
void Timer0_Init(void) // 1毫秒@12.000MHz
{
AUXR &= 0x7F; // 定时器时钟12T模式
TMOD &= 0xF0; // 设置定时器模式
TMOD |= 0x05;
TL0 = 0; // 设置定时初始值
TH0 = 0; // 设置定时初始值
TF0 = 0; // 清除TF0标志
TR0 = 1; // 定时器0开始计时
EA = 1;
}
void Timer1_Init(void) // 1毫秒@12.000MHz
{
AUXR &= 0xBF; // 定时器时钟12T模式
TMOD &= 0x0F; // 设置定时器模式
TL1 = 0x18; // 设置定时初始值
TH1 = 0xFC; // 设置定时初始值
TF1 = 0; // 清除TF1标志
TR1 = 1; // 定时器1开始计时
ET1 = 1; // 使能定时器1中断
EA = 1;
}
void Timer1_Isr(void) interrupt 3 {
unsigned char i;
if (++time_all_1s == 1000) {
time_all_1s = 0;
Freq = TH0 << 8 | TL0;
TH0 = TL0 = 0;
}
if (Uart_flag) Sys_Tick++;
Seg_Pos = (++Seg_Pos) % 8;
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], Seg_Point[Seg_Pos]);
for (i = 0; i < 8; i++) Led_Disp(i, ucLed[i]);
}
void Uart1_Isr(void) interrupt 4 {
if (RI) {
Uart_flag = 1;
Sys_Tick = 0;
Uart_Buf[Uart_Rx_Index] = SBUF;
Uart_Rx_Index++;
RI = 0;
}
if (Uart_Rx_Index > 10) Uart_Rx_Index = 0;
}
void Delay750ms(void) //@12.000MHz
{
unsigned char data i, j, k;
_nop_();
_nop_();
i = 35;
j = 51;
k = 182;
do {
do {
while (--k);
} while (--j);
} while (--i);
}
void main() {
System_Init();
Timer0_Init();
Timer1_Init();
Set_Rtc(ucRtc);
rd_temperature();
Delay750ms();
while (1) {
Data_Proc();
Key_Proc();
Seg_Proc();
Uart_Proc();
Led_Proc();
}
}
|
main.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <STC15F2K60S2.H>
#include "ds1302.h"
#include "iic.h"
#include "init.h"
#include "intrins.h"
#include "key.h"
#include "led.h"
#include "onewire.h"
#include "seg.h"
#include "stdio.h"
#include "string.h"
#include "uart.h"
#include "ul.h"
|