蓝桥杯单片机——第十五届蓝桥杯省赛
A目录
前言
功能描述
功能概述
性能要求
显示功能
技术实现
硬件环境
原理图
代码实现
技术要点
1.LED部分
2.按键部分
3.频率测定
4.DA转换
问题记录
前言
本产品基于IAP15F2K61S2,PCF8591,DS1302,NE555,拥有频率测量,校准,报警,实时记录功能,并且能够显示时间的工程
实验环境:基于IAP15F2K60S2 211
实验配置:将IAP15F2K60S2内部振荡设置为12MHz
将键盘工作模式条线配置为KBD(矩阵键盘)模式
扩展方式跳线J3配置为IO模式
实验环境:IAP15F2K61S2国信长天开发板
实验时间:2025-3-4
功能描述
功能概述
- 通过单片机P34引脚测量NE555输出的脉冲信号频率
- 支持频率数据校准功能
- 支持频率超限报警功能
- 通过读取DS1302RTC(实时时钟)芯片,获取时间数据
- 通过数码管完成数据显示功能
- 通过键盘实现界面切换,参数设定等功能
- 通过PCF8591实现DAC输出功能
- 通过LED指示灯完成题目要求的输出和状态反馈功能
性能要求
- 频率测量精度: +8%
- 按键动作相应时间:<=0.1秒
- 指示灯动作响应时间:<=0.1秒
- 数码管动态扫描周期,位选通间隔均匀,显示效果清晰,稳定,无闪烁,过暗,亮度不均等明显缺陷
显示功能
声明:该部分图片来自四梯官网,此处作学习分享使用,无商业用途
技术实现
硬件环境
IAP15F2K61S2单片机
74HC573锁存器
74HC02高速硅栅CMOS器件
74HC138译码器
DS1302
NE555(LN555)
NE555是一种经典的8脚时基集成电路,由Signetics Corporation在1971年发布。它只需简单的电阻和电容,即可实现特定的振荡延时功能,延时范围极广。其操作电源范围为4.5-18V,可与TTL、CMOS等逻辑电路配合,输出电流可达225mA,计时精确度高,温度稳定度佳,且价格便宜。NE555内部包含电压比较器、RS触发器、反相器等,广泛应用于模拟和数字电路中,如多谐振荡器、单稳态触发器和RS触发器等。
PCF8591
ULN2003达林顿管
原理图
单片机部分
数码管部分
LED和蜂鸣器部分
锁存器选择电路
NE555部分
PCF8591部分
插针部分
DS1302时钟部分
代码实现
main.c
#include <STC15F2K60S2.H>
#include "Display.h"
#include "Delay.h"
#include "Interface.h"
#include "Pcf8591.h"#define BUZ(X) {P2=(P2&0X1F)|0XA0;P0 = X;P2&=0X1F;}
#define LED(X) {P2=(P2&0X1F)|0X80;P0 = X;P2&=0X1F;}bit flag = 0;extern unsigned char UI; //UI界面选择,初始为频率界面void main()
{ LED(0XFF);BUZ(0X00);DS1302_Init(); //DS1302初始化,写入初始数据且防止读取数据为固定数值InitTimer2();Timer0_Init();while(1){ adj = freq + CVP; //持续更新频率值FreqMaxTime_Get(); //实时更新最大频率发生的时间Ne555_CollFreq(); //循环读取输出频率Key_Detect(); //按键循环检测LED_F(); //LED指示灯,循环检测PCF8591_Out(); //PCF8591频率输出,应放在死循环中,使其持续输出switch(UI){case 0: Freq_UI(); //频率界面break;case 1:Param_UI();break;case 2:Time_UI();break;case 3:Echo_UI();break;default:break;}}
}
LED.h
#ifndef __LED_H__
#define __LED_H__#include <STC15F2K60S2.H>
#include "Delay.h"
#define LED(X) {P2=(P2&0X1F)|0X80;P0 = X;P2&=0X1F;}extern int adj;
extern bit flag_1;
extern bit flag_2;
extern unsigned int OLP;
extern unsigned char led;
extern unsigned int freq;void LED_F();#endif
LED.h
#include "LED.h"/*** @brief LED亮灭控制* @param dat:P0口接收到的数据,用于控制共阳极二极管的点亮与熄灭* @retval none
**/
void LED_F()
{static unsigned char led = 0xff;static unsigned char led_2 = 0xff;if(flag_2){if(flag_1){LED(0XFF);led ^= 0x01;LED(led);flag_2 = 0;Delay_ms(100);}else{led |= 0x01;LED(led);flag_2 = 0;}if(adj < 0){led_2 &= 0xfd;LED(led_2);flag_2 = 0;}else if(adj > OLP){led_2 ^= 0x02;LED(led_2);flag_2 = 0;Delay_ms(100);}else{ led_2 |= 0x02;LED(led_2);flag_2 = 0;}}
}
Delay.h
#ifndef __DELAY_H__
#define __DELAY_H__#include <STC15F2K60S2.H>
#include <intrins.h>void Delay_us(unsigned char us); //@12.000MHz
void Delay_ms(unsigned char ms);
void Delay_s(unsigned char s);#endif
Delay.c
#include "Delay.h"/*** @brief 微秒级延时* @param us:延时us时长,延时xus将其置为x * @retval none
**/
void Delay_us(unsigned char us) //@12.000MHz
{while(us--){_nop_();_nop_();_nop_();_nop_();}
}/*** @brief 毫秒级延时* @param ms:延时ms时长,延时xms将其置为x * @retval none
**/
void Delay_ms(unsigned char ms)
{while(ms--){Delay_us(1000);}
}
Key.h
#ifndef __KEY_H__
#define __KEY_H__#include <STC15F2K60S2.H>
#include "ui_ops.h"
typedef enum{WAIT_FOR_PRESS,KEY_PRESSED,KEY_RELEASED
}Status; //按键状态
/*问题:应使用extern声明全局变量声明在别处
*/
extern unsigned char key_value; //按键的值
extern Status key_status; //定义按键状态
extern bit flag;void Key_Detect(void); //按键检测#endif
Key.c
#include "Key.h"unsigned char key_value = 0x00; //按键的值
Status key_status = WAIT_FOR_PRESS; //定义按键状态并初始化/*** @brief 按键检测* @param none* @retval none
**/
void Key_Detect()
{unsigned char btn_h;switch(key_status){case WAIT_FOR_PRESS:P3 = 0x0f;P44 = 0;P42 = 0;if((P3&0X0F) != 0X0F)key_status = KEY_PRESSED;break;case KEY_PRESSED:P3 = 0x0f;P44 = 0;P42 = 0;if((P3&0X0F) != 0X0F){if(P32 == 0)btn_h=3;if(P33 == 0)btn_h=4;switch(btn_h){case 3:P32 = 0;P44 = 1;P42 = 1;if(!P44){/*在参数界面和回显界面切换子界面*/if(UI == 1) //参数父页面下切换子页面{ParamSub_UI++;if(ParamSub_UI == 2)ParamSub_UI = 0;while(!P44);}if(UI == 3){EchoSub_UI++;if(EchoSub_UI == 2)EchoSub_UI = 0;while(!P44);}key_status = KEY_RELEASED;}if(!P42){if((ParamSub_UI == 0) && (OLP > 1000) && flag){ //超限参数界面下OLP = OLP - 1000;while(!P42);}if((ParamSub_UI == 1) && (CVP > -900) && flag){//频率参数界面下CVP = CVP - 100;while(!P42);}key_status = KEY_RELEASED;} break;case 4:P33 = 0;P44 = 1;P42 = 1;if(!P44) /*父界面切换*/{UI++;if(UI == 4)UI = 0;while(!P44);key_status = KEY_RELEASED;} if(!P42){if((ParamSub_UI == 0) && (OLP < 9000) && flag){ //超限参数界面下OLP = OLP + 1000;while(!P42);}if((ParamSub_UI == 1) && (CVP < 900) && flag){ //频率参数界面下CVP = CVP + 100;while(!P42);}key_status = KEY_RELEASED;}break;default:break; }}elsekey_status = WAIT_FOR_PRESS;break;case KEY_RELEASED:P3 = 0X0F;P44 = 0;P45 = 0;/*问题:注意运算优先级*/if((P3&0X0F) == 0X0F){ key_status = WAIT_FOR_PRESS; }default :break;}
}
DS1302.h
#ifndef __DS1302_H__
#define __DS1302_H__#include <STC15F2K60S2.H>
#include "Delay.h"sbit SDA_CL = P2^3; //DS1302I/O口
sbit SCL_CL = P1^7; //DS1302时钟
sbit RST = P1^3; //DS1302使能端void DS1302_Init(void);
void DS1302_WriteByte(unsigned char dat);
unsigned char DS1302_Read(unsigned char addr);#endif
DS1302.c
#include "DS1302.h"
/*** @brief DS1302写字节* @param dat:要写入ds1302的字节* @retval none
**/
void DS1302_WriteByte(unsigned char dat)
{unsigned char mask;for(mask=0x01;mask!=0;mask<<=1){if((mask&dat) == 0)SDA_CL = 0;elseSDA_CL = 1;SCL_CL=0;Delay_us(10);SCL_CL=1;}SDA_CL = 1;
}/*** @brief DS1302向寄存器中写数据* @param addr:要写入数据的寄存器的地址* @param dat:要写入的数据* @retval none
**/
void DS1302_Write(unsigned char addr, unsigned char dat)
{RST = 0; _nop_(); SCL_CL=0; _nop_();RST = 1; _nop_(); DS1302_WriteByte(addr);DS1302_WriteByte(dat);RST = 0;
}
/*** @brief DS1302从寄存器中读数据* @param addr:要读取数据的寄存器* @retval dat:从DS1302中读取的数据
**/
unsigned char DS1302_Read ( unsigned char addr )
{unsigned char i,temp=0x00;RST=0;_nop_();SCL_CL=0;_nop_();RST=1;_nop_();DS1302_WriteByte((addr<<1)|0X81);for (i=0;i<8;i++) { SCL_CL=0;temp>>=1; if(SDA_CL)temp|=0x80; SCL_CL=1;} RST=0;_nop_();RST=0;SCL_CL=0;_nop_();SCL_CL=1;_nop_();SDA_CL=0;_nop_();SDA_CL=1;_nop_();return (temp);
}/*** @brief DS1302初始化,写入起始时间* @param none* @retval none
**/
void DS1302_Init()
{unsigned char i;DS1302_Write(0x8E,0x00); //取消写保护for(i=0;i<3;i++){DS1302_Write(((i<<1)|0x80),0x00);}DS1302_Write(0x8E,0x80); //启用写保护
}
Interface.h
/****************************************************UI调用API获取数据
****************************************************/
#ifndef __INTERFACE_H__
#define __INTERFACE_H__#include <STC15F2K60S2.H>
#include "ui_ops.h"extern bit flag;void OLP_UI(void);
void CVP_UI(void);
void Freq_UI(void);
void Time_UI(void);
void Echo_UI(void);
void Param_UI(void);
void UI_Switch(void);
void FreqEcho_UI(void);
void TimeEcho_UI(void);#endif
Interface.c
#include "Interface.h"/*** @brief 频率界面* @param none* @retval none
**/
void Freq_UI(void)
{flag = 0;Freq_API(); //调用API,获取频率
}/*** @brief 超限参数界面* @param none* @retval none
**/
void OLP_UI(void)
{OlpDis_API(); //调用API,获取超限参数
}/*** @brief 校准值参数界面* @param none* @retval none
**/
void CVP_UI(void)
{CailDis_API(); //调用API,获取校准参数
}/*** @brief 时间界面* @param none* @retval none
**/
void Time_UI()
{flag = 0;Time_API(); //API调用,获取时间数据
}/*** @brief 频率回显界面* @param none* @retval none
**/
void FreqEcho_UI(void)
{EchoFreq_API();
}/*** @brief 时间回显界面* @param none* @retval none
**/
void TimeEcho_UI(void)
{EchoTime_API();
}/*** @brief 参数界面跳转* @param none* @retval none
**/
void Param_UI(void)
{ flag = 1;switch(ParamSub_UI){case 0:OLP_UI();break;case 1:CVP_UI();break;default:break;}
}/*** @brief 回显界面跳转* @param none* @retval none
**/
void Echo_UI(void)
{ flag = 0;switch(EchoSub_UI){case 0:FreqEcho_UI();break;case 1:TimeEcho_UI();break;default:break;}
}
IIC.h
#ifndef __IIC_H__
#define __IIC_H__#include <STC15F2K60S2.H>
#include "Delay.h"//#define NAK 1
//#define ACK 0sbit SDA = P2^1;
sbit SCL = P2^0;void IIC_Start();
void IIC_Stop();
bit IIC_WriteData(unsigned char dat);#endif
IIC.c
#include "IIC.h"/*** @brief IIC起始信号* @param none * @retval none
**/
void IIC_Start()
{SCL = 1;SDA = 1;Delay_us(10);SDA = 0;Delay_us(10);SCL = 0;
}/*** @brief IIC终止信号* @param none * @retval none
**/
void IIC_Stop()
{SCL = 0;SDA = 0;Delay_us(10);SCL = 1;Delay_us(10);SDA = 1;
}/*** @brief IIC发送数据* @param none * @retval ack:从机应答位
**/
bit IIC_WriteData(unsigned char dat)
{unsigned char mask;bit ack;EA = 0; //数据传送时关闭总中断for(mask=0x80;mask!=0;mask>>=1){if((dat&mask) == 0)SDA = 0;elseSDA = 1;SCL = 1;Delay_us(10);SCL = 0;}EA = 1; //数据传送时打开总中断SDA = 1;SCL = 1;Delay_us(10);ack = SDA;Delay_us(10);SCL = 0;return ~ack;
}
ui_ops.h
/*****************************************数据API
*****************************************/
#ifndef __UI_OPS_H__
#define __UI_OPS_H__#include <STC15F2K60S2.H>
#include "Delay.h"
#include "DS1302.h"
#include "Key.h"
#include "LED.h"
#include "Display.h"
#include "Ne555.h"extern int adj;
extern unsigned char UI;
extern int CVP; //校准值参数,初始值为0,-900Hz~900Hz
extern unsigned int OLP; //超限参数,初始值为2000,1000Hz~9000Hz
extern bit flag_2;
extern unsigned char time[3]; //时间界面时间数据缓冲区
extern int Freq_MAX; //回显界面最大频率值
extern unsigned int debuff[8]; //显示缓冲区
extern unsigned char EchoSub_UI; //回显子界面选择,初始为频率回显界面
extern unsigned char Echo_time[3]; //时间回显界面时间数据缓冲
extern unsigned char ParamSub_UI;
extern unsigned char Echo_debuff[8]; //时间回显界面最大频率发生时间记录void Time_API(void);
void Cali_API(void);
void Freq_API(void);
void OlpDis_API(void);
void CailDis_API(void);
void EchoFreq_API(void);
void EchoTime_API(void);
void FreqMaxTime_Get(void);#endif
ui_ops.c
#include "ui_ops.h"unsigned char UI = 0; //主界面标签int adj = 0; //将其设为int型,方便存储频率与校准值的和int CVP = 0; //校准值参数,初始值为0,-900Hz~900Hz
unsigned int OLP = 2000; //超限参数,初始值为2000,1000Hz~9000Hzbit flag_1 = 1; //页面指示灯标志
unsigned char time[3] = {0,0,0}; //时间界面时间数据缓冲区int Freq_MAX = 0; //回显界面最大频率值
unsigned char Freq_Per; //用于存放
unsigned int debuff[8] = {10,10,10,10,10,10,10,10}; //显示缓冲区
unsigned char EchoSub_UI = 0; //存储最大频率发生的时间
unsigned char ParamSub_UI = 0; //参数子页面标签
unsigned char Echo_time[3]; //时间回显界面时间数据缓冲
unsigned char Echo_debuff[8] = {14,12,0,0,0,0,0,0}; //时间回显界面最大频率发生时间记录
unsigned char EchoMAX_debuff[5]; //最大频率/*** @brief 时间显示数据API* @param none* @retval none
**/
/*该部分原始版本存在问题,问题部分在DS1302_Read()函数部分,待单独验证,现版本无大问题。
*/
void Time_API(void)
{unsigned char i;flag_1 = 0;/*从DS1302寄存器中读取时间*/for(i=0;i<3;i++){time[i] = DS1302_Read(i);}/*将数据送入数据缓冲区*/debuff[0] = time[2]>>4; //时间界面-小时-第一位debuff[1] = time[2]&0x0F; //时间界面-小时-第二位debuff[2] = 11; //时间界面显示:-debuff[3] = time[1]>>4; //时间界面-分钟-第一位debuff[4] = time[1]&0x0F; //时间界面-分钟-第二位debuff[5] = 11; //时间界面显示:-debuff[6] = time[0]>>4; //时间界面-秒-第一位debuff[7] = time[0]&0x0F; //时间界面-秒-第二位}/*** @brief 频率显示数据API* @param none* @retval none
**/
/*该部分正常,未作修改
*/
void Freq_API(void)
{ flag_1 = 1;/*此处用静态局部变量,防止公共变量叠加导致数据出错*/ //校准值+测得的频率为最终频率if(adj >= 0){debuff[0] = 13; //Fdebuff[1] = 10; //熄灭debuff[2] = 10; //熄灭/*注意条件判断的对象*/(adj >= 10000) ? (debuff[3]=adj/10000) : (debuff[3]=10); //当前的频率值-第一位(adj >= 1000) ? (debuff[4]=adj%10000/1000) : (debuff[4]=10); //当前的频率值-第二位(adj >= 100) ? (debuff[5]=adj%10000%1000/100) : (debuff[5]=10); //当前的频率值-第三位(adj >= 10) ? (debuff[6]=adj%10000%1000%100/10) : (debuff[6]=10); //当前的频率值-第四位debuff[7] = adj%10000%1000%100%10; //当前的频率值-第四位}else //频率为负数时,显示错误状态{ debuff[0] = 13; //Fdebuff[1] = 10; //熄灭debuff[2] = 10; //熄灭debuff[3] = 10; //熄灭debuff[4] = 10; //熄灭debuff[5] = 10; //熄灭debuff[6] = 15; //Ldebuff[7] = 15; //L //L2点亮}
}/*** @brief 超限参数显示API* @param none* @retval none
**/
/*该部分对向缓冲区送数据时的比较大小删除,根据超限参数的取值范围且每次变化的大小为1000的整数倍
*/
void OlpDis_API(void)
{flag_1 = 0;debuff[0] = 16; //P debuff[1] = 1; //1debuff[2] = 10; //熄灭debuff[3] = 10; //熄灭debuff[4] = OLP / 1000; //千位debuff[5] = OLP % 1000 / 100; //百位debuff[6] = OLP % 1000 % 100 / 10; //十位debuff[7] = OLP % 1000 % 100 % 10; //个位
}/*** @brief 校准值参数显示API* @param none* @retval none
**/
/*该部分将向缓冲区送数据时判断数据大小删去,根据校准值参数的取值范围且变化的大小都是100的整数倍
*/
void CailDis_API(void)
{flag_1 = 0;debuff[0] = 16; //Pdebuff[1] = 2; //2debuff[2] = 10; //熄灭debuff[3] = 10; //熄灭if(CVP > 0) //校准值参数为整数{debuff[4] = 10; //熄灭debuff[5]=CVP/100; //校准值参数-第一位debuff[6]=CVP%100/10; //校准值参数-第二位debuff[7]=CVP%10; //校准值参数-第三位}else if(CVP == 0){debuff[4] = 10; //熄灭debuff[5] = 10; //校准值参数-第一位debuff[6] = 10; //校准值参数-第二位debuff[7] = 0; //校准值参数-第三位}else{debuff[4] = 11; //-debuff[5] = (0-CVP)/100; //校准值参数-第一位debuff[6] = (0-CVP)%100/10; //校准值参数-第二位debuff[7] = (0-CVP)%10; //校准值参数-第三位}
}/*** @brief 回显界面最大频率API* @param none* @retval none
**/
/*本部分做了如下修改添加缓冲数组发送数据部分删去中间数组存储最大频率,只需最大频率变量即可
*//*显示正常
*/
void EchoFreq_API(void)
{ flag_1 = 0;debuff[0] = 14;debuff[1] = 13;debuff[2] = 10;(Freq_MAX >= 10000) ? (debuff[3] = Freq_MAX / 10000):(debuff[3] = 10);(Freq_MAX >= 1000) ? (debuff[4] = Freq_MAX % 10000 / 1000):(debuff[4] = 10);(Freq_MAX >= 100) ? (debuff[5] = Freq_MAX % 10000 % 1000 / 100):(debuff[5] = 10);(Freq_MAX >= 10) ? (debuff[6] = Freq_MAX % 10000 % 1000 % 100 / 10):(debuff[6] = 10);debuff[7] = Freq_MAX % 10000 % 1000 % 100 % 10;}/*** @brief 回显界面最大频率发生时间API* @param none* @retval none
**/
/*由于时间显示部分为零的位置无需熄灭,故删除了对数据大小判断的部分,直接将最大频率发生的时间送给回显时间数组存储,再将该数据送至显示缓冲区*/
void EchoTime_API(void)
{ flag_1 = 0;/*将存储的最大频率发生时间送至数据缓冲区*/debuff[0] = Echo_debuff[0];debuff[1] = Echo_debuff[1];debuff[2] = Echo_debuff[2];debuff[3] = Echo_debuff[3];debuff[4] = Echo_debuff[4];debuff[5] = Echo_debuff[5];debuff[6] = Echo_debuff[6];debuff[7] = Echo_debuff[7];}/*** @brief 实时更新回显界面最大频率发生时间API* @param none* @retval none
**/
void FreqMaxTime_Get()
{unsigned char i;if(Freq_MAX < adj ){Freq_MAX = adj;for(i=0;i<3;i++){Echo_time[i] = DS1302_Read(i);}/*将数据送入数据缓冲区*/Echo_debuff[0] = 14; Echo_debuff[1] = 12; Echo_debuff[2] = Echo_time[2]>>4; //时间界面-小时-第一位Echo_debuff[3] = Echo_time[2]&0x0F; //时间界面-小时-第二位 //时间界面显示:-Echo_debuff[4] = Echo_time[1]>>4; //时间界面-分钟-第一位Echo_debuff[5] = Echo_time[1]&0x0F; //时间界面-分钟-第二位 //时间界面显示:-Echo_debuff[6] = Echo_time[0]>>4; //时间界面-秒-第一位Echo_debuff[7] = Echo_time[0]&0x0F; }
}
Display.h
#ifndef __DISPLAY_H__
#define __DISPLAY_H__#include <STC15F2K60S2.H>
#include "Delay.h"
#include "ui_ops.h"
#include "LED.h"extern unsigned char deposn;
extern unsigned int debuff[8];
extern unsigned int code table[];void InitTimer2();
void Timer0_Init();
void Digital_Tube_Display(void);#endif
Display.c
#include "Display.h"/*数码管显示缓冲区:数字0~9,数码管全灭,字符‘-’,字母A,F,H,L,P
*/
const unsigned int code table[] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0xff, 0xbf, 0x88, 0x8e, 0x89, 0xc7, 0x8c
};
unsigned char deposn=0;
unsigned char count = 0;
bit flag_2 = 0;
/*** @brief 数码管显示函数* @param none* @retval none
**/
void Digital_Tube_Display(void)
{P0 = 0XFF;P2 &= 0X1F;P2 |= 0XE0;P0 = 0XFF;P2 &= 0X1F;P0 = 0XFF;P2 &= 0x1F;P2 |= 0XC0;P0 = (1 << deposn);P2 &= 0X1F;P0 = 0XFF;P2 &= 0X1F;P2 |= 0XE0;P0 = table[debuff[deposn]];P2 &= 0X1F; if(++deposn == 8)deposn = 0;
}/*** @brief 定时器2初始化函数* @param none* @retval none
**/
void InitTimer2(void)
{AUXR |= 0x04;T2L = 0x20;T2H = 0xD1;AUXR |= 0x10;IE2 |= 0x04;EA = 1;
}/*** @brief 定时器1中断* @param none* @retval none
**/
void Timer2_Isr() interrupt 12
{ count++;if(count == 100){flag_2 = 1;count = 0;}Digital_Tube_Display();if(count_freq<1000)count_freq++;
}/*** @brief 计数器0初始化函数* @param none* @retval none
**/
void Timer0_Init(void)
{TMOD |= 0X04;AUXR &= 0X7f;TH0 = 0;TL0 = 0;TF0 = 0;TR0 = 1;
}
NE555.h
#ifndef __NE555_H__
#define __NE555_H__#include <STC15F2K60S2.H>extern int adj;
extern int CVP;
extern unsigned int freq;
extern unsigned int count_freq;void Ne555_CollFreq();
unsigned int Cal_Freq();#endif
Ne555.c
#include "Ne555.h"unsigned int freq = 0; //频率
unsigned int count_freq;
/*** @brief 获取频率* @param none* @retval none
**/
void Ne555_CollFreq()
{if(count_freq == 1000){count_freq = 0;freq = Cal_Freq();adj = freq + CVP;}
}/*** @brief 读取计数器的数值* @param none* @retval freq: NE555输出脉冲的频率
**/
unsigned int Cal_Freq()
{unsigned int fre = 0;TR0 = 0; //关闭计数器0/*freq为每秒统计的脉冲数,及NE555输出脉冲的频率*/fre = ((TH0 << 8) | TL0); //freq为16位变量,此操作将高八位与第八位均赋值给freq TH0 = 0;TL0 = 0;TR0 = 1;return fre;
}
PCF8591.h
#ifndef __PCF8591_H__
#define __PCF8591_H__#include <STC15F2K60S2.H>
#include "IIC.h"extern int adj;
extern unsigned int OLP;
extern unsigned int freq;
extern unsigned int Freq_Max;void PCF8591_Init();
void PCF8591_Out();
void PCF8591_DAC(unsigned char dat);#endif
PCF8591.c
#include "Pcf8591.h"/*** @brief PCF8591DAC输入* @param none * @retval none
**/
void PCF8591_DAC(unsigned char dat)
{IIC_Start();Delay_us(10);IIC_WriteData(0x90);Delay_us(10);IIC_WriteData(0x40); Delay_us(10);IIC_WriteData(dat); Delay_us(10);IIC_Stop();
}/*** @brief PCF8591根据测得频率输出电压* @param none * @retval none
**/
void PCF8591_Out()
{unsigned char digital = 0;if(adj > 0) //频率大于0{if(adj <= 500) //当输出频率低于500HZ{ PCF8591_DAC(255 * 0.2);}else if((500 < adj) && (adj < OLP)) //输出频率大于500但小于OLP{digital = 255.0*(4.0*(float)(freq-500)/(OLP-500)+1)/5.0;PCF8591_DAC(digital);}else //输出频率大于OLP{PCF8591_DAC(255);}}else //频率小于0PCF8591_DAC(0);}
技术要点
1.LED部分
static unsigned char led = 0xff;static unsigned char led_2 = 0xff;
此处使用两个变量分别控制LED1和LED2,防止同时点亮时造成数据混乱。
LED以0.2秒闪烁是由定时器控制的。
/*** @brief 定时器2初始化函数* @param none* @retval none
**/
void InitTimer2(void)
{AUXR |= 0x04;T2L = 0x20;T2H = 0xD1;AUXR |= 0x10;IE2 |= 0x04;EA = 1;
}
/*** @brief 定时器1中断* @param none* @retval none
**/
void Timer2_Isr() interrupt 12
{ count++;if(count == 100){flag_2 = 1;count = 0;}Digital_Tube_Display();if(count_freq<1000)count_freq++;
}
定时器2配置为1T模式,16位自动重载,定时1ms进行一次中断,使用公共变量count计数,当 count=200时,说明此时计时达到200ms,将LED控制标志位flag_2置1。
if(flag_2){if(flag_1){LED(0XFF);led ^= 0x01;LED(led);flag_2 = 0;Delay_ms(100);}else{led |= 0x01;LED(led);flag_2 = 0;}if(adj < 0){led_2 &= 0xfd;LED(led_2);flag_2 = 0;}else if(adj > OLP){led_2 ^= 0x02;LED(led_2);flag_2 = 0;Delay_ms(100);}else{ led_2 |= 0x02;LED(led_2);flag_2 = 0;}}
LED控制标志位允许后,再根据条件进行LED控制
if(flag_1)
{LED(0XFF);led ^= 0x01;LED(led);flag_2 = 0;Delay_ms(100);
}
else
{led |= 0x01;LED(led);flag_2 = 0;
}
此处flag_1为页面控制标志位,当页面进入频率界面时会将将标志位flag_1置为1,其他界面将其置为0.
void Time_API(void)
{unsigned char i;flag_1 = 0;
void Freq_API(void)
{ flag_1 = 1;
void OlpDis_API(void)
{flag_1 = 0;
void CailDis_API(void)
{flag_1 = 0;
void EchoFreq_API(void)
{ flag_1 = 0;
当flag_1为1时,即当前在频率显示页面
LED(0XFF);
led ^= 0x01;
LED(led);
flag_2 = 0;
Delay_ms(100);
先对LED进行消隐,通过异或语句只对LED的第一位进行操作,使其高低电平转换,达到闪烁的效果,对LED进行操作后需将标志位清零,使再次满足条件后再进行操作。此处加一个延时使LED的亮度正常,避免LED状态保持时间太短,电流无法正常驱动导致的亮度不够。
led |= 0x01;
LED(led);
flag_2 = 0;
这部分是当界面不在频率显示界面对LED的第一位单独操作,使用按位或将其置1,关闭LED,并将标志位置0。
if(adj < 0)
{led_2 &= 0xfd;LED(led_2);flag_2 = 0;
}
else if(adj > OLP)
{led_2 ^= 0x02;LED(led_2);flag_2 = 0;Delay_ms(100);
}
else
{ led_2 |= 0x02;LED(led_2);flag_2 = 0;
}
这部分是对LED2的操作。
使用if....else if....else对并列条件进行判断。adj为测得的NE555的频率值加上校准值,此校准值在主函数的死循环中,用于实时矫正频率。
adj = freq + CVP; //持续更新频率值
当adj小于0时,使用按位与操作将LED2电平置0(共阳极发光二极管),打开LED2,将200ms标志位清零,此处不用延时是因为LED保持一个状态,长时间的电流有能力驱动LED。
当校准后的频率值大于超限参数(用于限制频率的范围)时,使用异或语句,对LED2单独操作使其闪烁。同样清除标志位。
当上述条件均不满足时,即频率校准后的值大于0但小于超限参数时,使用按位或将LED2关闭,标志位清零。
2.按键部分
使用状态机检测按键,使用枚举类型设置按键状态
typedef enum{WAIT_FOR_PRESS,KEY_PRESSED,KEY_RELEASED
}Status;
注:在程序中应避免大量使用枚举类型,因为过多的枚举类型会让整个程序僵化,且在资源受限的嵌入式系统中,会占用大量内存 。
由于硬件设计的缘故,且要求使用矩阵键盘 。故每列送低电平,检测每一行是否有按键按下。
case WAIT_FOR_PRESS:P3 = 0x0f;P44 = 0;P42 = 0;if((P3&0X0F) != 0X0F)key_status = KEY_PRESSED;break;
给P3IO口的高四位送低电平,此处矩阵键盘只用到了P3口的低6位,由于整个工程未使用P36,P37,故直接将高四位置零,对P44和P42置低电平。然后屏蔽P3口的高四位,检测低四位,若检测到按键按下,将按键状态更新。
case KEY_PRESSED:P3 = 0x0f;P44 = 0;P42 = 0;if((P3&0X0F) != 0X0F){if(P32 == 0)btn_h=3;if(P33 == 0)btn_h=4;switch(btn_h){case 3:P32 = 0;P44 = 1;P42 = 1;if(!P44){/*在参数界面和回显界面切换子界面*/if(UI == 1) //参数父页面下切换子页面{ParamSub_UI++;if(ParamSub_UI == 2)ParamSub_UI = 0;while(!P44);}if(UI == 3){EchoSub_UI++;if(EchoSub_UI == 2)EchoSub_UI = 0;while(!P44);}key_status = KEY_RELEASED;}if(!P42){if((ParamSub_UI == 0) && (OLP > 1000) && flag){ //超限参数界面下OLP = OLP - 1000;while(!P42);}if((ParamSub_UI == 1) && (CVP > -900) && flag){//频率参数界面下CVP = CVP - 100;while(!P42);}key_status = KEY_RELEASED;} break;case 4:P33 = 0;P44 = 1;P42 = 1;if(!P44) /*父界面切换*/{UI++;if(UI == 4)UI = 0;while(!P44);key_status = KEY_RELEASED;} if(!P42){if((ParamSub_UI == 0) && (OLP < 9000) && flag){ //超限参数界面下OLP = OLP + 1000;while(!P42);}if((ParamSub_UI == 1) && (CVP < 900) && flag){ //频率参数界面下CVP = CVP + 100;while(!P42);}key_status = KEY_RELEASED;}break;default:break; }}elsekey_status = WAIT_FOR_PRESS;break;
按键进入下一个状态KEY_PRESSED后,再次给每一列送低电平,检测每一行。由于本程序只使用四个按键,故只需检测低两行,检测到哪一行的按键按下后,给该行送低电平,检测P44,P42引脚。当检测到任意一个引脚为低电平时,即可确定是哪一个按键。
由于基于状态机检测和程序内其他部分延时的缘故,按键检测无需延时消抖
本部分各按键通过界面标签、标志位以及某些变量的数值来限定其功能。
3.频率测定
将NE555的第三引脚通过母对母的杜邦线连接到插针的P34引脚,通过将定时器/计数器0配置为计数器,NE555每发送一次脉冲,计数器0计数一次。通过定时器2,每1ms中断一次,变量count_freq计数,将其设置为unsigned int型,其数值方便计数。当数值达到1000后,Ne555_CollFreq()函数检测到会进行下一步操作。
/*** @brief 获取频率* @param none* @retval none
**/
void Ne555_CollFreq()
{if(count_freq == 1000){count_freq = 0;freq = Cal_Freq();}
}
Ne555_CollFreq()将count_freq置0,调用Cal_Freq()函数读取计数器0统计的频率(此处用每秒NE555输出高电平的次数作为频率)。将频率和校准值参数均设置为int型方便计算,防止强制类型转换中数值的变动造成数值错误。
/*** @brief 读取计数器的数值* @param none* @retval freq: NE555输出脉冲的频率
**/
unsigned int Cal_Freq()
{unsigned int fre = 0;TR0 = 0; //关闭计数器0/*freq为每秒统计的脉冲数,及NE555输出脉冲的频率*/fre = ((TH0 << 8) | TL0); //freq为16位变量,此操作将高八位与第八位均赋值给freq TH0 = 0;TL0 = 0;TR0 = 1;return fre;
}
先将TR0(计数器0运行控制位)置0,关闭计数器。由于freq为int型,为16位变量,将计数器0的高八位左移8位后与计数器0的低八位相加即位NE555的频率。然后计数器清零,打开计数器0,返回频率值。
4.DA转换
使用PCF8591进行DA转换
当频率值大于0时,根据三角形相似计算图像中当频率大于500,小于OLP(超限参数)时要写入PCF8591的数值,进行模数转换输出相应的电压。
else if((500 < adj) && (adj < OLP)) //输出频率大于500但小于OLP{digital = 255.0*(4.0*(float)(freq-500)/(OLP-500)+1)/5.0;PCF8591_DAC(digital);}
当频率低于500Hz时,输出1V电压,V(AGND) = 0.V(REF) = 5.计算出要写入的数值为255 * 0.2
if(adj <= 500) //当输出频率低于500HZ{ PCF8591_DAC(255 * 0.2);}
当频率大于超限参数是直接输出5V
else //输出频率大于OLP{PCF8591_DAC(255);}
当频率值为负数时直接输出0V
else //频率小于0PCF8591_DAC(0);
/*** @brief PCF8591根据测得频率输出电压* @param none * @retval none **/ void PCF8591_Out() {unsigned char digital = 0;if(adj > 0) //频率大于0{if(adj <= 500) //当输出频率低于500HZ{ PCF8591_DAC(255 * 0.2);}else if((500 < adj) && (adj < OLP)) //输出频率大于500但小于OLP{digital = 255.0*(4.0*(float)(freq-500)/(OLP-500)+1)/5.0;PCF8591_DAC(digital);}else //输出频率大于OLP{PCF8591_DAC(255);}}else //频率小于0PCF8591_DAC(0);}
5.定时器2初始化配置
/*** @brief 定时器2初始化函数* @param none* @retval none
**/
void InitTimer2(void)
{AUXR |= 0x04;T2L = 0x20;T2H = 0xD1;AUXR |= 0x10;IE2 |= 0x04;EA = 1;
}
将定时器2配置为每1ms中断的定时器。
先配置AUXR寄存器
AUXR |= 0x04;
先设置定时器2的模式,此处值设置与定时器2运行模式有关的位。将Bit3置0,将定时器2用作定时器,将Bit2置1设置定时器2为1T模式。
T2L = 0x20;T2H = 0xD1;
给定时器2的高8位和低八位赋值5 .
AUXR |= 0x10;
将Bit4置1,允许定时器2运行
6.定时器0配置为计数器
/*** @brief 计数器0初始化函数* @param none* @retval none
**/
void Timer0_Init(void)
{TMOD |= 0X04;AUXR &= 0X7f;TH0 = 0;TL0 = 0;TF0 = 0;TR0 = 1;
}
先配置定时器/计数器工作模式寄存器TMOD,只配置定时器0的Bit2位,将其置1,将定时器0用作计数器 。
然后配置辅助寄存器AUXR
AUXR &= 0X7f;
此处只配置与定时器/计数器0有关的位,将Bit7置0,即设置定时器/计数器0的运行速度设置为为1T模式
TH0 = 0;
TL0 = 0;
将计数器的高低8位全部清零,用于计数 。
然后配置定时器/计数器0控制寄存器TCON
TF0 = 0;
TR0 = 1;
TCON可以位寻址,故可以对单独的某一位进行发位操作
将TF0置0,清除T0溢出中断标志,将TR0置1,允许T0开始计数。
问题记录
1.部分函数放置的位置不正确,例如频率校准部分,最大频率发生时间,pcf8591模拟量输出函数,LED状态检测函数。导致响应不及时,输出量或显示量错误。
2.条件判断时,条件交叉紊乱,导致逻辑出错,例如LED状态控制函数部分。应尽量简化条件判断和逻辑,清晰划分条件范围,使其正确执行。
3。按键检测部分,使用数值判断按键是否按下,但由于逻辑不够严谨导致存储按键值的变量时效性不强,容易被改变,导致程序无法正确地检测到按键,从而无法实现预想功能。
4.DS1302 部分,部分函数为简化忽略了某些引脚的未初始化会导致错误的出现,同时遗忘了关闭DS1302写保护同时向DS1302写入初始时间,最后关闭写保护的部分。
此部分代码较为固定,应加强熟练度
/*** @brief DS1302向寄存器中写数据* @param addr:要写入数据的寄存器的地址* @param dat:要写入的数据* @retval none
**/
void DS1302_Write(unsigned char addr, unsigned char dat)
{RST = 0; _nop_(); SCL_CL=0; _nop_();RST = 1; _nop_(); DS1302_WriteByte(addr);DS1302_WriteByte(dat);RST = 0;
}
/*** @brief DS1302从寄存器中读数据* @param addr:要读取数据的寄存器* @retval dat:从DS1302中读取的数据
**/
unsigned char DS1302_Read ( unsigned char addr )
{unsigned char i,temp=0x00;RST=0;_nop_();SCL_CL=0;_nop_();RST=1;_nop_();DS1302_WriteByte((addr<<1)|0X81);for (i=0;i<8;i++) { SCL_CL=0;temp>>=1; if(SDA_CL)temp|=0x80; SCL_CL=1;} RST=0;_nop_();RST=0;SCL_CL=0;_nop_();SCL_CL=1;_nop_();SDA_CL=0;_nop_();SDA_CL=1;_nop_();return (temp);
}
/*** @brief DS1302初始化,写入起始时间* @param none* @retval none
**/
void DS1302_Init()
{unsigned char i;DS1302_Write(0x8E,0x00); //取消写保护for(i=0;i<3;i++){DS1302_Write(((i<<1)|0x80),0x00);}DS1302_Write(0x8E,0x80); //启用写保护
}
5.在界面切换时使用常量即可,无需使用大量的枚举类型变量。因为大量的枚举类型不仅占用有限的单片机资源,同时还会使程序僵化。
6,注意变量的类型,避免因类型与预期的数值范围不匹配导致数据错误。例如本工程中的freq和CVP,若使用unsigned char 型会因为超出其数值范围导致数据上溢。CVP由于数值范围要求-900~900,故将其设置为int型。此处将freq设置为int型而不是unsigned int型是因为两者相加时避免强制类型转换导致的数值错误。
7.由于显示要求的限制,大部分界面的数据显示无需数值大小的判断。
8.部分要求存储固某些记录时,要使用单独的变量来存储
9。尽量不大量用长时间延时函数,会造成程序阻塞,程序空转。