题目到官网看即可,有点久了有些细节记不清了,可能以前发的帖子解释详细一点。
这是我单片机初学的时候写的,像代码结构什么的肯定有可以提升的地方,多多包涵,将就看一下。
i2c文件使用官方的,pcf8591函数声明没有摆出来。
main.c
#include <system.H>
// 定义超声波测距相关参数,初始值为3.0和1.5
float hc = 3.0, lc = 1.5;
// 定义电压上下限,初始值为3.0和1.5
float highc = 3.0, lowc = 1.5;
// 定义变量v用于存储DAC输出的电压值,voltage用于存储ADC采集的电压值
float v, voltage = 0;
// sonicflag:超声波测距触发标志;adc:ADC采集结果;waveflag:测距启动标志;ledflag:LED闪烁标志
unsigned char sonicflag = 0, adc = 0, waveflag = 0, ledflag = 0;
// 定义距离变量,初始值为50
unsigned int distance = 50; // 该函数用于设置P2和P0端口的值
// p2:要设置到P2端口高5位的值
// p0:要设置到P0端口的值
void P2_P0(unsigned char p2, unsigned char p0)
{P0 = p0;// 清除P2端口高5位P2 = P2 & 0x1F; // 设置P2端口高5位P2 = p2 | P2; P2 = P2 & 0x1F;
}void Delay25us(void)
{unsigned char data i;_nop_();_nop_();i = 72;while (--i);
}void Timer1_Isr(void) interrupt 3
{// 静态变量,用于记录不同定时任务的计数static unsigned int T0 = 0, T1 = 0, T2 = 0, T3 = 0; T0++;T1++;T2++;T3++;// 每1ms执行一次数码管扫描if (T0 > 1) {T0 = 0;smg_loop();}// 每20ms执行一次按键扫描if (T1 > 20) {T1 = 0;key_loop();}// 每500ms触发一次超声波测距if (T2 > 500) {T2 = 0;sonicflag = 1;}// 每100ms改变一次LED闪烁标志if (T3 > 100) {T3 = 0;ledflag++;ledflag = ledflag % 2;}
}void Timer1_Init(void)
{// 定时器时钟1T模式AUXR |= 0x40; TMOD &= 0x0F; // 设置定时初始值TL1 = 0x20; // 设置定时初始值TH1 = 0xD1; // 清除TF1标志TF1 = 0; // 定时器1开始计时TR1 = 1; // 使能定时器1中断ET1 = 1; // 全局中断使能,两个定时器初始化有一个有这一句就行EA = 1;
}void Timer0_Init(void)//定时器0给超声波计时间用,不用在初始化的时候打开
{// 定时器时钟12T模式AUXR &= 0x7F; // 设置定时器模式TMOD &= 0xF0; // 设置定时初始值TL0 = 0; // 设置定时初始值TH0 = 0; // 清除TF0标志TF0 = 0;
}// 该函数用于根据不同的显示模式更新数码管显示内容
// i:显示模式,0表示电压显示,1表示测距显示,2表示参数显示
void view(unsigned char i)
{switch (i){case 0: // 电压显示模式{// 显示特定符号smg_set(0, 18); smg_set(1, 17);smg_set(2, 17);smg_set(3, 17);smg_set(4, 17);// 显示电压整数部分smg_set(5, (unsigned int)(voltage * 100) / 100 + 20); // 显示电压小数点后第一位smg_set(6, (unsigned int)(voltage * 100) % 100 / 10); // 显示电压小数点后第二位smg_set(7, (unsigned int)(voltage * 100) % 10); break;}case 2: // 参数显示模式{smg_set(0, 19); smg_set(1, 17);smg_set(2, 17);// 显示上限参数整数部分smg_set(3, (unsigned char)hc + 20); // 显示上限参数小数部分smg_set(4, (unsigned char)(hc * 10) % 10); smg_set(5, 17);// 显示下限参数整数部分smg_set(6, (unsigned char)lc + 20); // 显示下限参数小数部分smg_set(7, (unsigned char)(lc * 10) % 10); break;}case 1: // 测距显示模式{smg_set(0, 15); smg_set(1, 17);smg_set(2, 17);smg_set(3, 17);smg_set(4, 17);// 根据测距启动标志显示特定字符或距离数据smg_set(5, waveflag == 0 ? 10 : 17); smg_set(6, waveflag == 0 ? 10 : distance / 100);smg_set(7, waveflag == 0 ? 10 : distance % 100 / 10);break;}}
}// 超声波测距函数
void untrasonic()
{unsigned char i = 0;// 当超声波测距触发标志置位时if (sonicflag == 1) {// 清除触发标志sonicflag = 0; // 发送8个25us的超声波脉冲for (i = 0; i < 8; i++) {tx = 1;Delay25us();tx = 0;Delay25us();}// 启动定时器0开始计时TR0 = 1; // 等待回波信号或定时器0溢出while ((rx == 1) && (TF0 == 0)); // 停止定时器0计时TR0 = 0; // 如果定时器0未溢出if (TF0 == 0) {// 读取定时器0的高8位distance = TH0; // 左移8位distance <<= 8; // 读取定时器0的低8位distance |= TL0; // 根据定时器计数值计算距离distance = (unsigned int)(distance * 0.017); }else{// 清除定时器0溢出标志TF0 = 0; // 距离值置为0distance = 0; }}
}// DAC输出函数,根据测距结果设置DAC输出电压
void dac()
{// 当测距未启动时if (waveflag == 0) {// DAC输出0Vpcf8591_dac(0); v = 0;}else{// 当距离在20到80之间时if (distance > 20 && distance <= 80) {// 计算DAC输出电压v = distance / 15.0 - (1 / 3.0); // 设置DAC输出电压pcf8591_dac(v); }// 当距离在0到20之间时else if (distance > 0 && distance <= 20) {v = 1.0;pcf8591_dac(v);}else{v = 5.0;pcf8591_dac(v);}}
}void main()
{// 按键值unsigned char value = 0; // 显示模式unsigned char mode0 = 0; // 参数设置模式unsigned char mode1 = 0; // 临时变量,用于参数交换float vtemp = 0; Timer1_Init(); Timer0_Init(); // 初始化LED灯,所有灯熄灭,仅LED1点亮led.hex = 0xFF;led.b.b0 = 0;// 设置P2和P0端口,点亮LED1P2_P0(0x80, led.hex); // 关闭蜂鸣器和继电器P2_P0(0xA0, 0); while (1){// 获取按键值value = key_get(); // 按键4按下,切换显示界面if (value == 4) {mode0++;mode0 = mode0 % 3;if (mode0 == 1){// 测距界面,设置LED状态led.b.b0 = 1;led.b.b1 = 0;led.b.b2 = 1;}if (mode0 == 2){// 参数设置界面,设置LED状态led.b.b0 = 1;led.b.b1 = 1;led.b.b2 = 0;// 默认设置上限mode1 = 0; }if (mode0 == 0) // 退出参数设置,更新上下限参数{highc = hc;lowc = lc;// 电压显示界面,设置LED状态led.b.b0 = 0;led.b.b1 = 1;led.b.b2 = 1;}// 更新LED显示P2_P0(0x80, led.hex); }// 按键5按下,选择设置上限还是下限if (value == 5) {// 仅在参数设置界面有效if (mode0 == 2) {mode1++;mode1 = mode1 % 2;}}// 按键6按下,参数值加0.5if (value == 6) {// 上限设置if (mode1 == 0) {hc = hc + 0.5;if (hc > 5.0) hc = 0.5;// 越界后上下限交换if (hc < lc) {vtemp = hc;hc = lc;lc = vtemp;}}// 下限设置if (mode1 == 1) {lc = lc + 0.5;// 下限不能高于上限if (lc > hc) lc = hc; if (lc > 5.0) lc = 0.5;}}// 按键7按下,参数值减0.5if (value == 7) {// 上限设置if (mode1 == 0) {hc = hc - 0.5;if (hc < 0.5) hc = 5.0;}// 下限设置if (mode1 == 1) {lc = lc - 0.5;// 越界后上下限交换if (lc < 0.5) {lc = 5.0;if (hc < lc){vtemp = hc;hc = lc;lc = vtemp;}}// 下限不能低于上限if (hc < lc) hc = lc; }}// 根据显示模式更新数码管显示view(mode0); // 启动ADC采集通道3的电压值pcf8591_adc(0x03); // 读取ADC采集结果adc = pcf8591_read(); // 计算采集到的电压值voltage = 5.0 * (adc / 255.0); // 当电压值在上下限之间时if ((voltage < highc) && (voltage > lowc)) {// 启动测距waveflag = 1; // 执行超声波测距untrasonic(); // 设置DAC输出电压dac(); // 根据LED闪烁标志控制LED灯if (ledflag == 1) {led.b.b7 = 0;P2_P0(0x80, led.hex);}else{led.b.b7 = 1;P2_P0(0x80, led.hex);}}else{// 停止测距waveflag = 0; }}
}
主函数是程序的入口,主要完成以下工作:
- 初始化定时器 1 和定时器 0。
- 初始化 LED 灯和关闭蜂鸣器、继电器。
- 进入无限循环,不断读取按键值,根据按键值进行显示模式切换、参数设置等操作。
- 调用
view
函数更新数码管显示。 - 进行 ADC 电压采集,根据采集到的电压值判断是否启动超声波测距和 DAC 输出。
- 根据
ledflag
标志位控制 LED 灯的闪烁。
arrkeys.c
#include <STC15F2K60S2.H>// 该函数用于读取独立按键的状态,返回按下按键对应的编号
// 按键编号对应关系为:P30 -> 7, P31 -> 6, P32 -> 5, P33 -> 4
unsigned key_read()//独立按键
{// 用于存储检测到的按键编号,初始值为 0 表示无按键按下unsigned char key = 0;// 设置按键扫描的端口状态,P44 置 0,P42、P35、P34 置 1P44 = 0;P42 = 1;P35 = 1;P34 = 1;// 检测 P30 引脚是否为低电平,如果是则表示按键 7 被按下if (P30 == 0)key = 7;if (P31 == 0)key = 6;if (P32 == 0)key = 5;if (P33 == 0)key = 4;// 返回检测到的按键编号return key;
}// 该函数用于对按键进行消抖处理,并返回有效的按键编号
// 这个可能是状态机,通过比较当前和上一次按键状态来判断是否为有效按键按下
unsigned char key_loop()
{// 静态变量,用于记录上一次按键的状态,初始值为 0static unsigned char nowstate = 0, laststate = 0;// 用于存储最终有效的按键编号,初始值为 0 表示无有效按键按下unsigned char keynum = 0;// 将当前按键状态保存为上一次按键状态laststate = nowstate;// 调用 key_read 函数获取当前实际的按键状态nowstate = key_read();// 判断上一次无按键按下,且当前检测到按键 7 被按下,则认为按键 7 是有效按下if (laststate == 0 && nowstate == 7)keynum = 7;if (laststate == 0 && nowstate == 6)keynum = 6;if (laststate == 0 && nowstate == 5)keynum = 5;if (laststate == 0 && nowstate == 4)keynum = 4;// 返回有效的按键编号return keynum;
}// 该函数用于获取经过消抖处理后的有效按键编号
// 调用 key_loop 函数来完成按键消抖和判断,并将结果返回
unsigned char key_get()
{// 临时变量,用于存储 key_loop 函数返回的有效按键编号unsigned char temp = 0;// 调用 key_loop 函数获取有效按键编号temp = key_loop();// 返回有效按键编号return temp;
}
//这是一个必要的函数,好像是如果直接通过上面函数获得键值,就会一直返回按的结果好像一直在按
1. key_read 函数
该函数用于读取独立按键的状态。
首先对按键相关的端口进行初始化设置(P44 = 0; P42 = 1; P35 = 1; P34 = 1;)。
然后依次检测 P30 - P33 引脚的电平状态,如果相应引脚为低电平,则将对应的按键编号赋值给 key 变量。
最后返回按键编号。
2. key_loop 函数
该函数实现了按键消抖功能,使用状态机的思想。
定义两个静态变量 nowstate 和 laststate 分别记录当前按键状态和上一次按键状态。
调用 key_read 函数获取当前按键状态,并更新 nowstate。
通过比较 laststate 和 nowstate,当 laststate 为 0(无按键按下)且 nowstate 为特定按键编号时,将该按键编号赋值给 keynum。
最后返回有效按键编号。
3. key_get 函数
该函数调用 key_loop 函数获取有效按键编号,并将其返回。
smg.c
#include <STC15F2K60S2.H>
// 定义数码管段码表,存放在程序存储器中
// 依次对应 0 - 9、A、b、C、d、E、J、-、无显示、U、P 等字符的段码
// 后面还有一组是带小数点的数字
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
0x88, //A 10
0x83, //b 11
0xc6, //C 12
0xa1, //d 13
0x86, //E 14
0xc3, //J 15 11000011
0xBF, //- 16
0xFF, //none 17
0xC1, //U 18
0x8C,//P 19 1000,1100
0x40,0x79,0x24,0x30,0x19,0x12,0x02,0x78,0x00,0x10,0x08,0x03,0x46,0x21,0x06,0x0e
};
// 定义数码管显示缓冲区数组,初始值全为 17(对应无显示状态)
unsigned char smg_word[8]={17,17,17,17,17,17,17};// 该函数用于设置 P2 端口高 5 位和 P0 端口的值
// p2: P2 端口高 5 位要设置的值
// p0: P0 端口要设置的值
void P2andP0(unsigned char p2, unsigned char p0)//在main.c里解释过了
{P0 = p0;P2 = P2 & 0x1F;P2 = p2 | P2;P2 = P2 & 0x1F;
}// 该函数用于设置数码管显示缓冲区中指定位置的显示内容
// pos: 数码管的位置(0 - 7)
// word: 要显示的字符在段码表中的索引
void smg_set(unsigned char pos, unsigned char word)
{// 将指定位置的显示内容设置为 wordsmg_word[pos] = word;
}// 该函数用于实现数码管的动态扫描显示
void smg_loop()
{// 静态变量,用于记录当前扫描到的数码管位置,初始值为 0static unsigned char i = 0;// 选中第 i 个数码管进行显示,0xc0 用于选中数码管位选锁存器P2andP0(0xc0, 0x01 << i);// 将 smg_word[i] 对应的段码值通过段码表查找出来并显示P2andP0(0xe0, Seg_Table[smg_word[i]]);// 将当前位置的显示内容设置为无显示状态,避免余晖影响smg_word[i] = 17;// 扫描位置加 1i++;// 当扫描到第 8 个数码管后,将扫描位置重置为 0,开始下一轮扫描if (i >= 8) i = 0;
}
system.h
// 防止头文件被重复包含,这是一种常见的预处理器技巧
// 如果 _system_h_ 未被定义,则执行下面的代码并定义 _system_h_
#ifndef _system_h_
#define _system_h_#include <STC15F2K60S2.H>
#include <pcf8591.H>
#include <intrins.H>//结构体和联合体部分是给led和嗡鸣器用的,详细的解释在以前帖子有
// 定义一个位域结构体 bits
// 该结构体将一个字节拆分为 8 个独立的位,分别用 b0 - b7 表示
typedef struct
{unsigned char b0:1; // 第 0 位unsigned char b1:1; // 第 1 位unsigned char b2:1; // 第 2 位unsigned char b3:1; // 第 3 位unsigned char b4:1; // 第 4 位unsigned char b5:1; // 第 5 位unsigned char b6:1; // 第 6 位unsigned char b7:1; // 第 7 位
}bits;// 定义一个联合体 hextobin
// 联合体的特点是所有成员共享同一块内存空间
// 这里的成员包括前面定义的位域结构体 b 和一个无符号字符型变量 hex
typedef union
{bits b; unsigned char hex;
}hextobin;// 定义两个 hextobin 类型的变量 led 和 buzz
// led 控制 LED 灯,buzz 控制蜂鸣器
hextobin led,buzz;// 该函数用于获取按键的值
unsigned char key_get();
// 该函数可能用于按键扫描和消抖处理
unsigned char key_loop();// 该函数用于数码管的动态扫描显示
void smg_loop();// 该函数用于设置数码管指定位置显示的内容
// pos: 数码管的位置(0 - 7)
// word: 要显示的字符在段码表中的索引
void smg_set(unsigned char pos, unsigned char word);// 定义两个位变量 tx 和 rx
// tx 连接到 P1 端口的第 0 位,可能用于发送信号
// rx 连接到 P1 端口的第 1 位,可能用于接收信号
sbit tx=P1^0;
sbit rx=P1^1;// 该函数用于数码管的显示设置
// loc: 数码管的位置
// word: 要显示的内容
void smg_view(unsigned char loc, unsigned char word);#endif
pcf8591.h
#include <iic.H>
// 定义一个无符号字符型变量 ack,用于存储 I2C 通信中的应答信号
unsigned char ack = 0;// 该函数用于通过 I2C 总线向 PCF8591 芯片的 DAC(数模转换)通道写入数据
// word: 要写入 DAC 通道的数据,范围是 0 - 255
void pcf8591_dac(unsigned char word)
{I2CStart();// 发送 PCF8591 芯片的写地址(0x90),表示接下来要向芯片写入数据I2CSendByte(0x90);// 等待 PCF8591 芯片返回应答信号I2CWaitAck();// 发送控制字节 0x40,选择 DAC 输出功能I2CSendByte(0x40);// 等待 PCF8591 芯片返回应答信号I2CWaitAck();// 发送要写入 DAC 通道的具体数据I2CSendByte(word);// 等待 PCF8591 芯片返回应答信号I2CWaitAck();I2CStop();
}// 该函数用于通过 I2C 总线从 PCF8591 芯片读取 ADC(模数转换)数据
// 返回值: 读取到的 ADC 数据,范围是 0 - 255
unsigned char pcf8591_read()
{// 定义一个无符号字符型变量 datas,用于存储读取到的 ADC 数据unsigned char datas = 0;I2CStart();// 发送 PCF8591 芯片的读地址(0x91),表示接下来要从芯片读取数据I2CSendByte(0x91);// 等待 PCF8591 芯片返回应答信号,并将应答信号存储在 ack 变量中ack = I2CWaitAck();// 从 PCF8591 芯片接收 ADC 数据datas = I2CReceiveByte();// 向 PCF8591 芯片发送非应答信号(1),表示不再接收数据I2CSendAck(1);I2CStop();return datas;
}// 该函数用于通过 I2C 总线向 PCF8591 芯片发送 ADC 通道选择命令
// add: 要选择的 ADC 通道地址
void pcf8591_adc(unsigned char add)
{I2CStart();// 发送 PCF8591 芯片的写地址(0x90),表示接下来要向芯片写入数据I2CSendByte(0x90);// 等待 PCF8591 芯片返回应答信号,并将应答信号存储在 ack 变量中ack = I2CWaitAck();// 发送要选择的 ADC 通道地址I2CSendByte(add);// 等待 PCF8591 芯片返回应答信号,并将应答信号存储在 ack 变量中ack = I2CWaitAck();I2CStop();
}