巴中商城网站建设北京seo代理商
难点:
PCF8591同时测量两条通道数据
避免重复触发
采集触发时的时间数据存放
未采集的数据显示
清空数据
如果你系统的学习过C语言和数据结构与算法,解决本题还是很轻松的。
附件:第十六届蓝桥杯单片机组4T模拟赛二
一、基础知识
- PCF8591:如果你在读取通道 1 后立即读取通道 3,会导致以下问题:
1.电压参数未稳定:PCF8591 在切换通道后,需要一定的时间来稳定电压参数。
如果立即读取下一个通道,可能会导致读取的值不准确。
2.通道切换延迟:PCF8591 的通道切换需要时间,具体取决于芯片的采样速率和硬件设计。如果没有足够的延迟,读取的值可能仍然是上一个通道的值。
解决方法已经在另一篇文章给出了:PCF8591一次读取多条通道导致测量值不准确的原因及解决方法- 通过PCF8591采集的电压数据不稳定:可以使用以下方法:
1.均值滤波:对多次采样结果取平均值
2.中值滤波:对多次采样结果取中值
3.滑动平均滤波:维护一个滑动窗口,计算窗口内数据的平均值
4.卡尔曼滤波:适用于动态系统的高级滤波算法
本篇文章使用的是均值滤波,其他几种在单片机中有点大炮打蚊子的感觉,在以后的嵌入式系列再作介绍。string.h
中的两个常用函数
1.memset
memset
用于将一段内存区域的内容设置为指定的值
(1)函数原型:void* memset(void* ptr, int value, size_t num);
(2)参数:
ptr
:指向要设置的内存区域的指针
value
:要设置的值(以int
形式传递,但实际按unsigned char
处理
num
:要设置的字节数
(3)功能:
将ptr
指向的内存区域的前num
个字节设置为value
2.memcpy
memcpy 用于将一段内存区域的内容复制到另一段内存区域。
(1)函数原型:void* memcpy(void* dest, const void* src, size_t num);
(2)参数:
dest
:指向目标内存区域的指针
src
:指向源内存区域的指针
num
:要复制的字节数
(3)功能:
将src
指向的内存区域的前num
个字节复制到dest
指向的内存区域
二、板子现象演示
1.时间界面
上电显示时间界面,从23时59分50秒开始计时,同时LED1点亮,其余指示灯熄灭。
2.数据界面
按下S4切换成数据页面,数码管显示RB2和RD1的值,指示灯L2点亮。
3.历史查询界面
按下S4切换历史查询界面,未触发数据采集时显示-
,指示灯L3点亮,该页面按下S5切换历史查询子界面。
4.触发采集
任意界面光敏电阻电压<RB2电压时触发采集(持续处于该状态时不触发),由于手机拍照遮光触发三次。
触发采集时亮的灯是L8,下面这张图是错误的,后面的Led模块已经修改好了。
5.触发三次采集后的历史查询页面
6.清空记录时间
三、数码管模块
1.添加P
R
C
-
的段码
code unsigned char Seg_Table[] =
{
0xc0, //0
0xf9, //1
0xa4, //2
0xb0, //3
0x99, //4
0x92, //5
0x82, //6
0xf8, //7
0x80, //8
0x90, //9
0xff, //空
0xbf, //-
0x8c, //P
0xc1, //U
0x88, //A
0xc6, //C
};
void SegDisp(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_Table[dula];if(point)P0 &= 0x7f;P2 = P2 & 0x1f | 0xe0;P2 &= 0x1f;
}
2.时间页面
- 添加DS1302底层代码
#include <STC15F2K60S2.H>
#include <intrins.h>sbit SCK = P1^7;
sbit RST = P1^3;
sbit SDA = P2^3;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);
}code unsigned char DS1302_Arr[4] = {0x84,0x82,0x80,0x8E};void SetRtc(unsigned char *ucRtc)
{unsigned char i;Write_Ds1302_Byte(DS1302_Arr[3],0x00);for(i = 0; i < 3; i++)Write_Ds1302_Byte(DS1302_Arr[i],ucRtc[i]);Write_Ds1302_Byte(DS1302_Arr[3],0x80);
}void GetRtc(unsigned char *ucRtc)
{unsigned char i;for(i = 0; i < 3; i++)ucRtc[i] = Read_Ds1302_Byte(DS1302_Arr[i]+1);
}
- 在
main.c
函数中的DS1302
数据处理函数
pdata u8 ucRtc[3] = {0x23,0x59,0x50};void DS1302Proc()
{GetRtc(ucRtc);
}void main()
{SystemInit();Timer0_Init();SetRtc(ucRtc);//设定时间while(1){DS1302Proc();}
}
- 数码管显示
typedef unsigned char u8;
typedef unsigned int u16;
typedef unsigned long int u32;idata u8 SegPos;//数码管扫描位
idata u8 SegMode;//数码管显示页面pdata u8 SegBuf[8] = {10,10,10,10,10,10,10,10};//数码管数据缓存区
pdata u8 SegPoint[8] = {0,0,0,0,0,0,0,0};//数码管小数点使能位
pdata u8 ucRtc[3] = {0x23,0x59,0x50};//时钟void SegProc()
{unsigned char i;switch(SegMode){case 0:SegBuf[2] = SegBuf[5] = 11;//-for(i = 0; i < 3; i++){SegBuf[3*i] = ucRtc[i] / 16;SegBuf[3*i+1] = ucRtc[i] % 16;}break;}
}
3.数据页面
- iic底层
对AD处理时使用的方法是连续读取两次,丢弃第一次的值,并且使用均值滤波(不使用滤波也行)。
#include <STC15F2K60S2.H>
#include <intrins.h>#define DELAY_TIME 5sbit 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(unsigned char add)
{unsigned char temp;I2CStart();I2CSendByte(0x90);I2CWaitAck();I2CSendByte(add);I2CWaitAck();I2CStart();I2CSendByte(0x91);I2CWaitAck();temp = I2CReceiveByte();I2CSendAck(1);I2CStop();I2CStart();I2CSendByte(0x90);I2CWaitAck();I2CSendByte(add);I2CWaitAck();I2CStart();I2CSendByte(0x91);I2CWaitAck();temp = I2CReceiveByte();I2CSendAck(1);I2CStop();return temp;
}unsigned char AverageFilter(unsigned char add)
{unsigned char i;unsigned int sum = 0;for (i = 0; i < 10; i++) {sum += Ad(add);// 多次采样}return (unsigned char)(sum / 10);
}
main.c
中的AD数据处理函数ADProc()
idata u16 RD1_100x, RB2_100x;//RD1、RB2放大100倍void ADProc()
{RD1_100x = AverageFilter(0x01) / 51.0 * 100;RB2_100x = AverageFilter(0x03) / 51.0 * 100;
}
- 数码管显示
void SegProc()
{unsigned char i;if(!record){switch(SegMode){case 0://...break;case 1:SegBuf[0] = 12;//PSegBuf[1] = RD1_100x / 100;SegBuf[2] = RD1_100x / 10 % 10;SegBuf[3] = RD1_100x % 10;SegBuf[4] = 13;//USegBuf[5] = RB2_100x / 100;SegBuf[6] = RB2_100x / 10 % 10;SegBuf[7] = RB2_100x % 10;SegPoint[1] = SegPoint[5] = 1;break;}}
}
4.历史查询界面
由于历史查询界面中记录的是最近三次的触发时间,这边为了方便
使用二维数组pdata u8 rtc_record[3][3];
来记录三组时间
定义idata u8 rtc_record_x;
为二维数组的行索引
由于时钟的小时不可能为24,所以二维数组的初值赋值为0x24
,在数码管就可以通过判断二维数组的rtc_record[rtc_record_x][0]
是否为0x24进行判断是否为空。
idata u8 rtc_record_x;
pdata u8 rtc_record[3][3] = {0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24};void SegProc()
{unsigned char i;if(!record){switch(SegMode){case 0://...break;case 1://...break;case 2:SegBuf[0] = 14;SegBuf[1] = rtc_record_x + 1;SegPoint[1] = SegPoint[5] = 0;//如果该组数据未采集,则除了前两位其余显示-if(rtc_record[rtc_record_x][0] == 0x24){SegBuf[2] = 11;SegBuf[3] = 11;SegBuf[4] = 11;SegBuf[5] = 11;SegBuf[6] = 11;SegBuf[7] = 11;}//该组数据已采集,正常显示elsefor(i = 0; i < 3; i++){SegBuf[2*i+2] = rtc_record[rtc_record_x][i] / 16;SegBuf[2*i+3] = rtc_record[rtc_record_x][i] % 16;}break;}}
5.触发采集
/*变量定义*/
idata u8 rtc_record_x;//二维数组行索引
pdata u8 rtc_record[3][3] = {0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24};//二维数组存放记录的时间idata bit record;//触发标志位
idata bit has_record;//重复触发检测位
在实现该功能,应采取从大到小的方法来实现
- 当采集到光敏电压值 < RB2 电压值时,触发 1 次
void ADProc()
{RD1_100x = AverageFilter(0x01) / 51.0 * 100;RB2_100x = AverageFilter(0x03) / 51.0 * 100;//光敏电压:RD1 if(RD1_100x < RB2_100x && !record){record = 1;GetRtc(rtc_record[0]);//采集第一组时间}
}
- 触发后,立即切换到“触发界面”,显示本次触发时间, 3s 内不可重复触发。 3 秒后返回“原状态”
idata u16 Time_3s;void ADProc()
{RD1_100x = AverageFilter(0x01) / 51.0 * 100;RB2_100x = AverageFilter(0x03) / 51.0 * 100;//光敏电压:RD1 if(RD1_100x < RB2_100x && !record){record = 1;GetRtc(rtc_record[0]);//采集第一组时间}if(RD1_100x >= RB2_100x)has_record = 0;
}void Timer0_Isr(void) interrupt 1
{//systick++;if(++SegPos == 8) SegPos = 0;SegDisp(SegPos,SegBuf[SegPos],SegPoint[SegPos]);if(record)if(++Time_3s == 3000){Time_3s = 0;record = 0;}
}
- 接下来考虑不重复触发
idata bit has_record;//重复触发检测位
void ADProc()
{RD1_100x = AverageFilter(0x01) / 51.0 * 100;RB2_100x = AverageFilter(0x03) / 51.0 * 100;//光敏电压:RD1 if((RD1_100x < RB2_100x) && !record && !has_record){has_record = 1;record = 1;GetRtc(rtc_record[0]);//采集第一组时间}if(RD1_100x >= RB2_100x)has_record = 0;
}
- 接下来写一个函数,实现以下功能
触发后, “历史查询界面”,记录本次触发时间。越早记录的数据,索引值越大,最近一次触发时间索引值
为 1。历史查询界面可记录最近 3 次的触发时间。
(1)数据移动
for (i = 2; i > 0; i--)memcpy(rtc_record[i], rtc_record[i - 1], 3);
将rtc_record[1] 的数据复制到 rtc_record[2]
将rtc_record[0] 的数据复制到 rtc_record[1]
每次移动的数据长度为 3 个字节(时、分、秒)
(2)获取新数据
GetRtc(rtc_record[0]);
调用 GetRtc() 函数,将新数据存储到 rtc_record[0] 中。
(3)代码
void UpdateRtcRecord()
{unsigned char i;for(i = 2; i > 0; i--)memcpy(rtc_record[i],rtc_record[i-1],3);GetRtc(rtc_record[0]);
}
- 调用
UpdateRtcRecord()
void UpdateRtcRecord()
{unsigned char i;for(i = 2; i > 0; i--)memcpy(rtc_record[i],rtc_record[i-1],3);GetRtc(rtc_record[0]);
}void ADProc()
{RD1_100x = AverageFilter(0x01) / 51.0 * 100;RB2_100x = AverageFilter(0x03) / 51.0 * 100;//光敏电压:RD1 if((RD1_100x < RB2_100x) && !record && !has_record){has_record = 1;record = 1;UpdateRtcRecord();}if(RD1_100x >= RB2_100x)has_record = 0;
}
6.数码管完整代码
void SegProc()
{unsigned char i;if(!record)//没有处于记录页面{switch(SegMode){case 0:SegBuf[2] = SegBuf[5] = 11;for(i = 0; i < 3; i++){SegBuf[3*i] = ucRtc[i] / 16;SegBuf[3*i+1] = ucRtc[i] % 16;}break;case 1:SegBuf[0] = 12;SegBuf[1] = RD1_100x / 100;SegBuf[2] = RD1_100x / 10 % 10;SegBuf[3] = RD1_100x % 10;SegBuf[4] = 13;SegBuf[5] = RB2_100x / 100;SegBuf[6] = RB2_100x / 10 % 10;SegBuf[7] = RB2_100x % 10;SegPoint[1] = SegPoint[5] = 1;break;case 2:SegBuf[0] = 14;SegBuf[1] = rtc_record_x + 1;SegPoint[1] = SegPoint[5] = 0;if(rtc_record[rtc_record_x][0] == 0x24){SegBuf[2] = 11;SegBuf[3] = 11;SegBuf[4] = 11;SegBuf[5] = 11;SegBuf[6] = 11;SegBuf[7] = 11;}elsefor(i = 0; i < 3; i++){SegBuf[2*i+2] = rtc_record[rtc_record_x][i] / 16;SegBuf[2*i+3] = rtc_record[rtc_record_x][i] % 16;}break;}}else//处于记录页面{SegBuf[0] = 13;SegBuf[1] = 13;SegPoint[1] = SegPoint[5] = 0;for(i = 0; i < 3; i++){SegBuf[2*i+2] = rtc_record[0][i] / 16;SegBuf[2*i+3] = rtc_record[0][i] % 16;}}
}
四、按键模块
1.底层代码
#include <STC15F2K60S2.H>unsigned char KeyDisp()
{unsigned char temp = 0;P44 = 0;P42 = 1;P35 = 1;P34 = 1;if(P32 == 0)temp = 5;if(P33 == 0)temp = 4;P44 = 1;P42 = 0;P35 = 1;P34 = 1;if(P33 == 0)temp = 8;return temp;
}
2.功能处理
idata u8 rtc_record_x;//二维数组行索引
pdata u8 rtc_record[3][3] = {0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24};//二维数组存放记录的时间void KeyProc()
{KeyVal = KeyDisp();KeyDown = KeyVal & ~KeyOld;KeyUp = ~KeyVal & KeyOld;KeyOld = KeyVal;switch(KeyDown){case 4://页面流转if(++SegMode == 3) SegMode = 0;//每次进入历史查询页面时,索引值默认为1if(!SegMode) rtc_record_x = 0;break;case 5://历史查询页面子页面控制if(SegMode == 2)if(++rtc_record_x == 3)rtc_record_x = 0;break;case 8://清空记录的数据if(SegMode == 2){rtc_record_x = 0;//memset:将`rtc_record`指向的内存区域的前`9`个字节设置为`0x24`//在数码管中判断数据为空时比较数据是0x24memset(rtc_record,0x24,9);}break;}
}
五、Led模块
看到这种指定页面指定指示灯亮的依旧使用互斥点亮
pdata u8 ucLed[8] = {0,0,0,0,0,0,0,0};void LedProc()
{unsigned char i;for(i = 0; i < 3; i++)ucLed[i] = (SegMode == i);ucLed[7] = record;LedDisp(ucLed);
}
六、完整代码(各模块退出处理请自行添加)
#include <STC15F2K60S2.H>
#include <string.h>
#include "Init.h"
#include "Key.h"
#include "Seg.h"
#include "Led.h"
#include "iic.h"
#include "ds1302.h"typedef unsigned char u8;
typedef unsigned int u16;idata u8 SegPos;
idata u8 SegMode;
idata u8 KeyVal, KeyDown, KeyUp, KeyOld;
idata u8 rtc_record_x;idata u16 RD1_100x, RB2_100x;
idata u16 Time_3s;pdata u8 SegBuf[8] = {10,10,10,10,10,10,10,10};
pdata u8 SegPoint[8] = {0,0,0,0,0,0,0,0};
pdata u8 ucRtc[3] = {0x23,0x59,0x50};
pdata u8 rtc_record[3][3] = {0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24};
pdata u8 ucLed[8] = {0,0,0,0,0,0,0,0};idata bit record;//触发标志位
idata bit has_record;//重复触发检测位void UpdateRtcRecord()
{unsigned char i;for(i = 2; i > 0; i--)memcpy(rtc_record[i],rtc_record[i-1],3);GetRtc(rtc_record[0]);
}void KeyProc()
{KeyVal = KeyDisp();KeyDown = KeyVal & ~KeyOld;KeyUp = ~KeyVal & KeyOld;KeyOld = KeyVal;switch(KeyDown){case 4:if(++SegMode == 3) SegMode = 0;if(!SegMode) rtc_record_x = 0;break;case 5:if(SegMode == 2)if(++rtc_record_x == 3)rtc_record_x = 0;break;case 8:if(SegMode == 2){rtc_record_x = 0;memset(rtc_record,0x24,9);}break;}}void SegProc()
{unsigned char i;if(!record){switch(SegMode){case 0:SegBuf[2] = SegBuf[5] = 11;for(i = 0; i < 3; i++){SegBuf[3*i] = ucRtc[i] / 16;SegBuf[3*i+1] = ucRtc[i] % 16;}break;case 1:SegBuf[0] = 12;SegBuf[1] = RD1_100x / 100;SegBuf[2] = RD1_100x / 10 % 10;SegBuf[3] = RD1_100x % 10;SegBuf[4] = 13;SegBuf[5] = RB2_100x / 100;SegBuf[6] = RB2_100x / 10 % 10;SegBuf[7] = RB2_100x % 10;SegPoint[1] = SegPoint[5] = 1;break;case 2:SegBuf[0] = 14;SegBuf[1] = rtc_record_x + 1;SegPoint[1] = SegPoint[5] = 0;if(rtc_record[rtc_record_x][0] == 0x24){SegBuf[2] = 11;SegBuf[3] = 11;SegBuf[4] = 11;SegBuf[5] = 11;SegBuf[6] = 11;SegBuf[7] = 11;}elsefor(i = 0; i < 3; i++){SegBuf[2*i+2] = rtc_record[rtc_record_x][i] / 16;SegBuf[2*i+3] = rtc_record[rtc_record_x][i] % 16;}break;}}else{SegBuf[0] = 13;SegBuf[1] = 13;SegPoint[1] = SegPoint[5] = 0;for(i = 0; i < 3; i++){SegBuf[2*i+2] = rtc_record[0][i] / 16;SegBuf[2*i+3] = rtc_record[0][i] % 16;}}
}void LedProc()
{unsigned char i;for(i = 0; i < 3; i++)ucLed[i] = (SegMode == i);ucLed[7] = record;LedDisp(ucLed);
}void DS1302Proc()
{GetRtc(ucRtc);
}void ADProc()
{RD1_100x = AverageFilter(0x01) / 51.0 * 100;RB2_100x = AverageFilter(0x03) / 51.0 * 100;//光敏电压:RD1 if((RD1_100x < RB2_100x) && !record && !has_record){has_record = 1;record = 1;UpdateRtcRecord();}if(RD1_100x >= RB2_100x)has_record = 0;
}void Timer0_Init(void) //1毫秒@12.000MHz
{AUXR &= 0x7F; //定时器时钟12T模式TMOD &= 0xF0; //设置定时器模式TL0 = 0x18; //设置定时初始值TH0 = 0xFC; //设置定时初始值TF0 = 0; //清除TF0标志TR0 = 1; //定时器0开始计时ET0 = 1; //使能定时器0中断EA = 1;
}void Timer0_Isr(void) interrupt 1
{if(++SegPos == 8) SegPos = 0;SegDisp(SegPos,SegBuf[SegPos],SegPoint[SegPos]);if(record)if(++Time_3s == 3000){Time_3s = 0;record = 0;}
}void main()
{SystemInit();Timer0_Init();SetRtc(ucRtc);while(1){}
}