蓝桥杯笔记
“免责声明” ( ?? ω ?? )?
代码未全部验证,也许存在BUG,如发现错误欢迎指正,不愿意指正那就当作没看见也行 所有说明文字仅代表笔者个人想法
CT107D硬件概况
首先是国信长天CT107D开发板的硬件概况,怎么说呢,一言难尽,268软妹币,血亏
其中虽然板上留有一些外设的拓展口,但实际上是不存在附带模块的,没错,268的板子连LED点阵都不带。 ̄へ ̄
程序
基础设备控制
由于开发板的设计,这块板上点个灯稍微有一丢丢复杂,根据电路结构,8颗LED需要通过74HC138去操作。
STC15
74HC138
74HC02
P2
STC15F2
Y0-Y7
P25+P26+P27
Y4C-Y7C
Y4-Y7
Y4C
LED灯
Y5C
SETP/RELAY...
Y6C
7SEG位选
Y7C
7SEG段选
P2 = (P2&0x1F)|0xA0;
P0 = ctrl;
P2 &= 0x1F;
[目录](# “免责声明” ( ?? ω ?? )?)
数码管驱动
位选为Y6C选定的575,段选则为Y7C,即P25-P27组成6和7;
即110和111,那P2就是C0H和E0H;
C0位选,E0段选;
void Display(void)
{
static dispcom;
P0 = 0xFF;
P2 = (P2&0x1F)|0xE0;
P0 = 0xFF;
P2 &= 0x1F;
P2 = (P2&0x1F)|0xC0;
P0 = 0x01<<dispcom;
P2 &= 0x1F;
P0 = 0xFF;
P2 = (P2&0x1F)|0xE0;
P0 = DispTab[DispBuf[dispcom]];
P2 &= 0x1F;
dispcom++;
if(dispcom==8)dispcom = 0;
}
按键
§ 独立按键
J5接至BTN,P3低4位控制4个按键;
unsigned char KeyValue = 0xFF;
------------------------------------------------------------
void BTN(void)
{
static unsigned char keyvalue;
static unsigned char keypress;
static bit keyfree = 1;
unsigned char temp;
P3 |= 0x0F;
temp = P3&0x0F;
if(temp!=0x0F)keypress++;
else keypress = 0;
if(keypress==5&&keyfree)
{
keypress = 0;
keyfree = 0;
switch(temp)
{
case 0x07:keyvalue = 4;break;
case 0x0B:keyvalue = 5;break;
case 0x0D:keyvalue = 6;break;
case 0x0E:keyvalue = 7;break;
}
}
if(temp==0x0F&&keyfree==0)
{
keyfree = 1;
KeyValue = keyvalue;
}
else Keyvalue = 0xFF;
}
§ 矩阵按键
J5接至KBD,P3(不存在P36、P37)和P42、P44共同控制按键;
unsigned char KeyValue = 0xFF;
------------------------------------------------------------
void KBD(void)
{
unsigned char S1=0x00,S2=0x00;
static unsigned char keyvalue;
static unsigned char keypress;
static bit keyfree = 1;
unsigned char temp = 0xFF;
P3 = 0x0F;
P42 = 0;P44 = 0;
temp = (P3&0x0F);
if(temp!=0x0F)keypress++;
else keypress = 0;
if(keypress==5&&keyfree)
{
keypress = 0;
keyfree = 0;
S1 = temp;
P3 = 0xF0;
P42 = 1;P44 = 1;
if(!P42) S2 = 0xB0;
else if(!P44) S2 = 0x70;
else S2 = temp;
switch(S1|S2)
{
case 0x77:keyvalue = 4;break;
case 0x7B:keyvalue = 5;break;
case 0x7D:keyvalue = 6;break;
case 0x7E:keyvalue = 7;break;
case 0xB7:keyvalue = 8;break;
case 0xBB:keyvalue = 9;break;
case 0xBD:keyvalue = 10;break;
case 0xBE:keyvalue = 11;break;
case 0xD7:keyvalue = 12;break;
case 0xDB:keyvalue = 13;break;
case 0xDD:keyvalue = 14;break;
case 0xDE:keyvalue = 15;break;
case 0xE7:keyvalue = 16;break;
case 0xEB:keyvalue = 17;break;
case 0xED:keyvalue = 18;break;
case 0xEE:keyvalue = 19;break;
}
}
if(temp==0x0F&&keyfree==0)
{
keyfree = 1;
KeyValue = keyvalue;
}
else Keyvalue = 0xFF;
}
IIC
§ AT24C02
不用从头写起,但是需要自己写最后使用的发送和接收函数;
数据包给出了启动停止应答等操作的函数,只需要知道IIC通信的时序或者步骤即可;
我们需要从数据手册中得到这个时序;
在AT24C02的数据手册中,我们可以在Read Operation下面找到上面这张图。乍一看看不出到底Byte Write,有多少个步骤,但实际上重点有两个图表,还包括上面那个;如下:
Figure 7表明:MSB,R/W,LSB都属于同一个字节,而在赛方给出的IIC参考程序中,有两种操作函数,电平变化和字节传输;
所以将Figure 8划分一下也变得非常简单:
易得它的顺序是
IIC_Start();
IIC_SendByte(?);
IIC_WaitAck();
IIC_SendByte(?);
IIC_WaitAck();
IIC_SendByte(?);
IIC_WaitAck();
IIC_Stop();
接下来需要知道“?”里填啥?
首先是DEVICE ADDRESS,其实甚至可以从图中出答案(当然,给出的数据里也有),图中的就是正确的(必须的呀),即0xA0;板上AT24C02芯片地址为000;
第二个发送的字节是WORD ADDRESS,即数据要写在AT24C02的哪里?这个位置是由使用情况决定的,于是设置一个输入参数,add,最后是发送的数据,自然,也是参数;
所以IIC写函数最后是:
void IIC_Write(unsigned char add,unsigned char data)
{
IIC_Start();
IIC_SendByte(0xA0);
IIC_WaitAck();
IIC_SendByte(add);
IIC_WaitAck();
IIC_SendByte(data);
IIC_WaitAck();
IIC_Stop();
}
那么读函数亦是如此啦;
当然,我们看到读取并不简单,它有三种模式;即 Current Address Read、 Random Read、 Sequential Read.
类型 | 描述 |
---|
Current Address Read | 未断电时,读上一次读的地址(即当前地址)。断电后,地址归为0x00 | Random Read | 指定地址读取(这是我们需要的) | Sequential Read | 连续读,先这样,再这样,再那样,就可以一个地址接下一个地址连续读 |
显然,传输步骤是:
IIC_Start();
IIC_SendByte(?);
IIC_WaitAck();
IIC_SendByte(?);
IIC_WaitAck();
IIC_Start();
IIC_SendByte(?);
IIC_WaitAck();
IIC_RecByte();
IIC_SendAck(?);
IIC_Stop();
所以,最后是:
unsigned char IIC_Write(unsigned char add)
{
unsigned char RecData;
IIC_Start();
IIC_SendByte(0xA0);
IIC_WaitAck();
IIC_SendByte(add);
IIC_WaitAck();
IIC_Start();
IIC_SendByte(0xA1);
IIC_WaitAck();
RecData = IIC_RecByte();
IIC_SendAck(1);
IIC_Stop();
return RecData;
}
AT24C02的读写差不多就是这样了。
§ AD/DA
对于DA过程,S直接是start;而对于AD(对应IIC的读)需要包含伪写;
首先看图中各个字节如何划分;
对于板上PCF8591来讲,地址位字节的高7位都是固定的,不固定的只有读写位,看图也可知写为低位有效。所以对写来说地址字节就是0x90;那么在上面的流程图中,ADDRESS和后面那个数字位肯定是在1个字节中,属于同一步操作;
控制字:
最低两位:通道,AD输入只能在0通道,DA输出可以选择0-3通道;
2号位:自动递增位,这里用不到,不管,设为0;
3号位:固定为0;(最高位同)
4,5号位:输入模式,AD输入时的输入模式,我们需要一一对应,直接设为00;
6号位:输出使能,DA输出时设为1;
了解这个之后就可以直接看最上面两张图写出大致步骤了;
IIC_Start(void);
IIC_Stop(void);
IIC_WaitAck(void);
IIC_SendAck(bit ackbit);
IIC_SendByte(unsigned char byt);
IIC_RecByte(void);
IIC_Start();
IIC_SendByte(0x90);
IIC_WaitAck();
IIC_SendByte(0x40);
IIC_WaitAck();
IIC_SendByte(DA_data);
IIC_WaitAck();
IIC_Stop();
同理也可得到AD转换的程序;
IIC_Start();
IIC_SendByte(0x90);
IIC_WaitAck();
IIC_SendByte(channal);
IIC_WaitAck();
IIC_Start();
IIC_SendByte(0x91);
IIC_WaitAck();
AD_data = IIC_RecByte();
IIC_SendAck(1);
IIC_Stop();
补全函数头和相关的变量定义可以得到完整的AD/DA 程序。
PS:调用AD/DA转换函数时,注意读取的是上一次转换的值。所以必要的时候要调用两遍。
18B20–One Wire
对于18B20,它使用的是onewire总线,由于只有一根线进行通信,其含义基本都是通过电平持续时间来表示的。所以对时间的把控相当严格。
这时要做一件非常重要的事,将原来资料中的延时函数改一下,否则时间就不对了;
------------------------------------------------------------------
void Delay_OneWire(unsigned int t)
{
t *= 12;
while(t--);
}
即“简单的硬件条件需要相对复杂的软件来补充”
所以软件会相对复杂;
给出的资料就是这么多了。接下来看数据手册;
温度在18B20中的数据存储位置等信息,但是这些对我们帮助不大,主要是相关的驱动程序都已经给出来了,我们需要知道的是它的读取步骤;
而在这个例子中,步骤就非常明显了;
我们只想让它做两件事,将温度转换为数据,读出数据;
所以在这个例子中有些东西是不需要的,比如Match ROM(匹配ROM),因为我们板上就只有一个18B20而已。自然,send DS18B20 ROM code也是不必要的,取而代之的是Skip ROM (跳过ROM);
于是有如下步骤:
init_ds18b20();
Write_DS18B20(0xCC);
Write_DS18B20(0x44);
Delay_OneWire(20);
init_ds18b20();
Write_DS18B20(0xCC);
Write_DS18B20(0xBE);
Temp_L = Read_DS18B20();
Temp_H = Read_DS18B20();
然后是对数据的处理了,前面说到的“用处不大”Figure2现在用处大了,18B20用多种温度的分辨率,默认的是12位分辨率的,也就是11位数据和其余的符号位;在下面这段话中有明确的表述
? The sign bits (S) indicate if the temperature is positive or negative: for positive numbers S = 0 and for negative numbers S = 1. If the DS18B20 is configured for 12-bit resolution, all bits in the temperature register will contain valid data.
? For 11-bit resolution, bit 0 is undefined. For 10-bit resolution, bits 1 and 0 are undefined, and for 9-bit resolution bits 2, 1, and 0 are undefined. ? Table 1 gives examples of digital output data and the corresponding temperature reading for 12-bit resolution conversions.
在默认12位情况下,数据值是实际温度的16倍;值得一提的是,温度数据在18B20中的存储可以理解为是以补码的形式存储的;
所以,最后,程序可以写成这个样子
unsigned int T_Value = 0x00;
bit T_Symbol = 0;
--------------------------------------------------------------------------
void rd_temperature(void)
{
unsigned char Temp_L,Temp_H;
unsigned int Temp = 0x00;
init_ds18b20();
Write_DS18B20(0xCC);
Write_DS18B20(0x44);
Delay_OneWire(20);
init_ds18b20();
Write_DS18B20(0xCC);
Write_DS18B20(0xBE);
Temp_L = Read_DS18B20();
Temp_H = Read_DS18B20();
Temp = Temp_H;
Temp <<= 8;
Temp += Temp_L;
if(Temp_H&0x80)
{
T_Symbol = 1;
Temp = ~Temp + 1;
}
else
{
T_Symbol = 0;
}
T_Value = Temp/16.0*100+0.5;
}
注意点:
- 温度值数据类型一定至少得是unsigned int,不要顺手写成了unsigned char。
- 读取温度数据时是先低位,再高位。
- …
PS:调用温度转换函数时,注意读取的是上一次转换的值。所以必要的时候要调用两遍。
DS1302
DS1302;先看给出的程序资料;
打开下面两个函数,就可以知道Write_Ds1302();是为Write_Ds1302_Byte()和Read_Ds1302_Byte()服务的,知道这点很重要,因为这样一来,我们只需要考虑怎么用这两个函数组成我们使用的函数;(两个函数而已,步骤一定不会太难,事实也正是如此)
实际上,直接用给出的函数读寄存器就可以了;
unsigned char SetRTC[3] = {0x12,0x50,0x59};
unsigned char ReadRTC[3] = {0x00,0x00,0x00};
------------------------------------------------------------------------
void Set_RTC(void)
{
Write_Ds1302_Byte(0x8E,0x00);
Write_Ds1302_Byte(0x84,SetRTC[0]);
Write_Ds1302_Byte(0x82,SetRTC[1]);
Write_Ds1302_Byte(0x80,SetRTC[2]);
Write_Ds1302_Byte(0x8E,0x80);
}
void Read_RTC(void)
{
SetRTC[0] = Read_Ds1302_Byte(0x85);
SetRTC[1] = Read_Ds1302_Byte(0x83);
SetRTC[2] = Read_Ds1302_Byte(0x81);
}
需要注意的是,需要在数码管上显示的时候,取位数得/16而不是/10;
并且,在使用时,初始化中要先写一次初始值,否则会有意想不到的惊喜效果。即DS1302使用起来应该是这样的:
Set_RTC();
while(1)
{
Read_RTC();
}
PS:
头文件声明数组时不需要带上数组长度,这样就可以了。
unsigned char SetRTCData[];
unsigned char ReadRTCData[];
Uart串口
串口使用
串口最简使用方法
- 初始化
- 中断服务函数
- 发送数据/接收数据
void UartInit(void)
{
SCON = 0x50;
AUXR |= 0x01;
AUXR |= 0x04;
T2L = 0xC7;
T2H = 0xFE;
AUXR |= 0x10;
ES = 1;
EA = 1;
}
void Uart() interrupt 4
{
uchar i = 0;
if (RI)
{
RI = 0;
i = SBUF;
}
}
void SendString(char *s)
{
while (*s != '\0')
{
SBUF = *s++;
while(TI==0);
TI = 0;
}
}
- 利用stc-isp软件生成初始化程序:
-
RI为接收中断标志位,即RI==1时表示有被数据写入SBUF。 -
TI为发送标志位,TI==1时表示数据已发送。(RI=1或者TI=1都可以触发串口中断,进入中断函数)
SCON寄存器详情:
| | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | |
---|
SCON | | SM0 | SM1 | SM2 | REN | TB8 | RB8 | TI | RI | 98H | | 位地址 | 9FH | 9EH | 9DH | 9CH | 9BH | 9AH | 99H | 98H | |
超声波
超声波测距过程:发送声波,计时,接收声波,计算距离;
没有关于超声波有用的信息给出,需要记住超声波模块大致的使用过程;
Measure()
TR0 = 1 开始计时/数
SendUltra()-发送声波
if(返回信号)
TR0 = 0 停止计时/数
读出TL0,TH0
1. 定义发送和接收管脚
2. 初始化Timer0
3. 定义发送8个40MHz信号函数
4. 定义测距函数
5. 发送信号,计时,接收信号,计算距离
#inlcude "intrins.h"
#define Nops {_nop_();_nop_();_nop_();_nop_();_nop_();\
_nop_();_nop_();_nop_();_nop_();_nop_();\
_nop_();_nop_();_nop_();_nop_();_nop_();}
sbit ULTX = P1^0;
sbit ULTX = P1^1;
unsigned int Distance = 0;
---------------------------------------------------------------
void Timer0Init(void)
{
}
void SendUltra()
{
unsigned char fre;
for(fre=0;fre<8;fre++)
{
ULTX = 1;
Nops;Nops;Nops;Nops;Nops;
Nops;Nops;Nops;Nops;Nops;
ULTX = 0;
Nops;Nops;Nops;Nops;Nops;
Nops;Nops;Nops;Nops;Nops;
}
}
void Measure(void)
{
unsigned int time = 0;
SendUltra();
TR0 = 1;
while(ULRX==1&&TF0==0);
TR0 = 0;
if(TF0)
{
TF0 = 0;
Distance = 9999;
}
else
{
time = TH0;
time <<= 8;
time |= TL0;
Distance = (uint)time*0.017;
}
TL0 = 0x00;
TH0 = 0x00;
}
程序中各种时间乱七八糟,要理解这些必须了解的一个东西就是:单片机的时间和速度到底是怎么样的?
时钟周期:又称为震荡周期,是为单片机提供定时信号的震荡源的周期,是单片机最基本的时间单位;
状态周期:CPU从一个状态转换到另一状态所需要的时间。简单地说每个状态周期分为两个震荡周期(时钟周期);
机器周期:一个机器周期包含六个状态例如,取指令、存储器读、存储器写等。机器周期 = 6个状态周期 = 12个时钟周期。
指令周期: 顾名思义,指令周期就是执行一条指令所需的全部时间。程序中用到的nop()函数就只需要一个指令周期;
① 产生信号
用其他语句来对发送的脉冲电平计时肯定不如nop()来得准确,于是,要产生40KHz的方波,就要确定发送信号引脚处于一个状态的时间;
对于STC15F2K60S2(1T高速芯片),一个指令周期就是一个时钟周期,即
T
n
o
p
=
1
/
12
M
T_{nop} = 1/12M
Tnop?=1/12M(s);
而
T
U
L
T
X
=
1
/
0
=
1
/
40
K
=
300
T
n
o
p
T_{ULTX = 1/0} = 1/40K = 300T_{nop}
TULTX=1/0?=1/40K=300Tnop? ;
即产生40KHz方波需要ULTX处于高电平150个NOP,低电平150个NOP;
② 计时
12T计时器每12个时钟周期计数+1;即T = 12time/12M (s) = time us;
频率测量
大致步骤:
① 设置计时器0;设置为P34触发的计数器;
② 用计时器0计算500ms内的P34脉冲数;
③ 计算频率
其中TMOD寄存器中
C
/
T
 ̄
C/\overline{T}
C/T就是控制计数器和定时器切换的“开关”;
和STC-ISP软件中的定时器代码唯一的区别就是打开了这个开关,并且计数槽清零。
void Timer0Init(void)
{
AUXR &= 0x7F;
TMOD &= 0xF0;
TMOD |= 0x04;
TL0 = 0x00;
TH0 = 0x00;
TF0 = 0;
TR0 = 0;
}
void FreMeasure(void)
{
if(G_Time_1ms%1000==0)
{
Timer0Init();
TR0 = 1;
}
else if(G_Time_1ms%1000==500)
{
TR0 = 0;
Frequency = TH0;
Frequency <<= 8;
Frequency += TL0;
Frequency *= 2;
TH0 = 0;
TL0 = 0;
}
}
注意:测量函数放在计时器1的中断函数中;不然可能在还没执行到测量函数时时间点就过去了;
系统结构
Project ψ(._. )>
小项目来说,笔者倾向于尽量放在同一个源文件,一是自己写着省时省力,不容易出错。二是分太多文件没有必要,很容易出现一个c文件和对应的头文件加起来都没几行代码,另外,如果系统中功能相互勾连,一个c文件中的函数要用到另一个c文件中的变量,头文件包含来包含去,整个项目结构更加混乱。
相反,如果在一个c文件中,你可以将各部分写得模块分明,那也是相当漂亮的。
而对于一个文件显得冗长,则可以在coding过程中将暂时不用管的函数收起(绝大部分编辑器都可以做到这点)。如此一来,coding高效且代码漂亮。
下面给出个人习惯的代码结构参考:
#include
#define
uchar
uint
void Func(void);
void main(void)
{
}
void Func(void);
Main函数 ψ(._. )>
个人认为主函数是一个项目代码结构的体现,一个好的结构其主函数一定是层次分明,一目了然的。(也可能是我个人执念吧o((>ω< ))o)
在下面的主函数控制代码中,以定时器1作为系统的时间管理,利用一系列代表不同时间的变量作为时间标志,类似于定了一个闹钟,闹钟响的时候就去做该做的事,否则就休息(空转while(1));
void main()
{
Init();
while(1)
{
if(TimeFlag_10ms)
{
TimeFlag_10ms = 0;
Function();
if(TimeFlag_200ms)
{
TimeFlag_200ms = 0;
Function();
}
}
}
}
时间控制 ψ(._. )>
void Timer1Sr(void) interrupt 3
{
Time_1ms++;
if(Time_1ms%10==0)
{
TimeFlag_10ms = 1;
if(Time_1ms%20==0)
{
TimeFlag_20ms = 1;
if(Time_1ms%200==0)
{
TimeFlag_200ms = 1;
}
}
}
Display();
}
|