目录
前言
本系列文章意在帮助各位正在准备蓝桥杯单片机组的同学,提供一个参考与指南,但是所有指南的前提是,默认你已经有单片机基础,本系列文章会提供本人对蓝桥杯单片机组编程方面的一些源码实现。当然,或许与你写代码的style完全不想同,那咱们也可以彼此相互交流各自的优缺点,或许你可能只是一个小白,就是想来抄一些可供借鉴的代码,这些都不重要,重要的是能给正在准备单片机组的同学提供到一些帮助。
此外代码可能有写的不完善的地方,但是每一个代码都是经过测试可行之后才发出来的,不敢保证十全十美,但是跑起来应该没问题。
此外,比赛时会提供一个单片机资源数据包,里面的内容比较多,这里只放一个下载链接,是2023年省赛是提供的单片机资源数据包(今年才2024,已经是最新的资源数据包了),正文中会直接使用资源数据包内的资料:
单片机资源数据包_2023(点击下载)
关于此资源数据包还得补充两句,他的3-底层驱动代码参考与往些年不同(但其实差异也很小,这里不再赘述),而且还是快比赛时才公布的这个资源数据包。也就是说,在写底层驱动代码时,我都是以2023年的资源数据包为基础写的,如果你使用的不是2023年的资源数据包的话,可能会跟我使用的底层驱动不完全相同,不过影响也不大。
下图为单片机资源数据包_2023内的所有资源,其中最关键的就是3-底层代码驱动以及SCH_硬件原理图V30了,当然共阳数码管段码表也十分常用,不过一般也就新写一个项目时,会把里面的东西CV到main函数里,然后这个文件就用不着了。
一、关于iic总线
1.iic总线通信
IIC总线是一种串行通信协议,也被称为I2C(Inter-Integrated Circuit)总线。
IIC总线采用两根线进行通信,一根是时钟线(SCL,Serial Clock),另一根是数据线(SDA,Serial Data)。通过这两根线,多个设备可以在同一总线上进行通信,每个设备通过一个唯一的地址进行识别。
IIC总线具有以下特点:
- 串行通信:数据在时钟的控制下以位的方式传输,节约通信线路的数量。
- 主从结构:IIC总线中有一个主设备(Master)和多个从设备(Slave)。主设备负责发起通信请求,而从设备则被动应答。
- 多设备共享:多个设备可以在同一个总线上共享,通过设备地址进行识别和通信。
- 支持多速率:IIC总线支持不同的时钟速率,高速模式和标准模式之间可以进行切换。
- 双向传输:数据线上既可以传输主设备发送的数据,也可以传输从设备返回的数据。
IIC总线广泛应用于各种电子设备中,特别是在嵌入式系统中。它可以用于连接各种外围设备,如传感器、存储器、显示器等,实现设备之间的数据交换和通信。
在蓝桥杯比赛中,iic总线上挂载了两个设备,分别是PCF8591 AD/DA转化器,以及AT24C02 掉电不丢失数据存储器,也就是eeprom存储器。这就比第二篇讲的,onewire要复杂了,因为那个总线只挂载了一个简单的温度传感器,而且一般我们只需要从温度传感器读取数据;而iic总线上挂载的两个设备,都是我们不单需要读取,而且还需要写入数据的。不过在比赛时,一般也只会出其中的一小部分,不会两个外设的读写程序同时用上,下面提到时在详细介绍。
2.iic底层驱动代码
单片机资源数据包_2023中给出来iic驱动的底层代码:
- /* # I2C代码片段说明
- 1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
- 2. 参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
- 中对单片机时钟频率的要求,进行代码调试和修改。
- */
-
- #define DELAY_TIME 5
-
- //
- 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);
- }
-
当然,把这个代码复制到project里面之后,点击编译还是会报错。在正常使用之前,我们需要定义正确的管脚,以及引用头文件。此外,为了方面main函数引用,我们还需要手动添加一个iic.h的头文件,并将main.c和iic.c引用iic.h头文件。
从原理图可以看出,scl连接的是P20引脚,sda连接的是P21的引脚,再加上需要引用的头文件,因此我们需要再iic.c开头加上以下代码:
#include "iic.h"
#include
#include
sbit sda=P2^1;
sbit scl=P2^0;
并创建空的iic.h文件
#ifndef _IIC_H_
#define _IIC_H_
#endif
这样再次点击编译就不会报错了,但是仍旧会有许多警告,这些警告都是关于已经定义的函数未被调用的,不影响我们正常使用。
二、PCF8591 AD/DA转化器
1.关于PCF8591
PCF8591是一个8位的AD/DA转化器,8位的意思就是,它的精度是8位,也就是0到255.与onewire驱动的DS18B20温度传感器不同,PCF8591的采样速率取决于iic的速度,换言之,PCF8591的采样速度比iic总线的速度还要快,所以采样速度取决于iic的速度;而温度传感器因为需要进行温度转化,温度转化的速度往往很慢(最慢好像可以达到700ms,当然700ms的温度转换也就意味着温度的精度更高)。
PCF8591有四个通道,如果需要同时读取多个通道的话会出现意料之外的情况,因为比赛时也很少遇到需要读取两个甚至以上通道的情况,这里不再赘述。
2.AD/DA转化
这里简单介绍一下AD/DA转化:
AD/DA转换是指模拟信号与数字信号之间的相互转换过程。
AD转换(Analog-to-Digital)是将连续的模拟信号转换为离散的数字信号的过程。在AD转换中,模拟信号被采样并量化,然后通过编码转换为对应的数字信号。AD转换器的输入是模拟信号,输出是数字信号,在计算机和数字系统中广泛应用。AD转换器的输出通常是一个二进制代码,代表了模拟输入信号的离散化数值。
DA转换(Digital-to-Analog)是将离散的数字信号转换为连续的模拟信号的过程。在DA转换中,数字信号被解码并转换为对应的模拟信号。DA转换器的输入是数字信号,输出是模拟信号。DA转换器常用于将数字系统产生的数据转换为模拟信号,以便驱动模拟设备或输出到模拟电路中。
AD/DA转换在许多领域得到广泛应用,包括音频处理、信号采集与处理、测量仪器、通信系统等。通过AD转换,模拟信号可以被数字设备处理和分析;通过DA转换,数字信号可以被转换为模拟信号,以便输出到模拟设备中,例如扬声器、马达等。
刚才提到,PCF8591的精度是8位,也就是0到255。在使用AD/DA转化之前需要给它一个基准电压,也就是GND和一个标准的5V电压(蓝桥杯板子上的PCF8591的5V基准电压可能不太准,所以有时读出的数据跟实际的数据可能不太对照),PCF8591会把0V到5V拆分成255份,每份对应的电压值为5/255V。也就是说,0对应的是0V,255对应的是5V。
比如现在输入3V电压,使用AD转化,那3V对应的AD输出值就是(3/5)*255=153。
同理,对于DA转化就是AD转化的逆过程,DA转化是输入一个数字信号,输出一个模拟信号,比如输入了102(取值范围是0到255),那么输出电压就等于(102/255)*5等于2V。
3.ROM检测
iic总线上可能挂载的有多个设备,在使用PCF8591之前,需要进行正确的ROM检测。这个时候就不能像读取温度传感器时那样,直接跳过ROM检查了,因为这个iic总线上挂载了两个设备。
PCF8591的地址是一个8位二进制数,由三部分组成:一部分是固定地址,一部分是可变地址,一部分时读写位。
地址的高四位是固定部分,固定为1001
地址的低四位的高三位是可变部分,由芯片上的A2 A1 A0决定,我们再看一眼PCF8591的原理图:
可见,蓝桥杯板子上的A0 A1 A2都是接地的状态,也就是逻辑000
地址的低四位的低一位,是读写位:1代表读取数据,0代表写入数据
也就是说,如果我们想读取PCF8591,我们首先要进行ROM检测,发送数据1001000x,也就是0x90或者0x91。
4.PCF8591的控制字
进行ROM检测之后,我们就可以对PCF8591进行控制。
控制寄存器的高半字节用于允许模拟输出,和将模拟输入编程为单端或差分输入。低半字节选择一个由高半字节定义的
模拟输入通道。如果自动增量(auto-increment)标志置 1,每次 A/D 转换后通道号将自动增加。
如果自动增量(auto-increment)模式是使用内部振荡器的应用中所需要的,那么控制字中模拟输出允许标志应置1。这要求内部振荡器持续运行,因此要防止振荡器启动延时的转换错误结果。模拟输出允许标志可以在其他时候复位以减少静态功耗。
选择一个不存在的输入通道将导致分配最高可用的通道号。所以,如果自动增量(auto-increment)被置1,下一个被选择的通道将总是通道0。两个半字节的最高有效位(即bit 7 和bit 3)是留给未来的功能,必须设置为逻辑0。控制寄存器的所有位在上电复位后被复位为逻辑0。 D/A 转换器和振荡器在节能时被禁止。模拟输出被切换到高阻态。
PCF8591还是有许多功能的,这里只介绍比赛时会用到的一些。
对于第一位,固定为0
对于第二位,1:DA转化,0:AD转化
对于第三四位,是选择PCF8591的工作模式的,我们一般选择第一个模式即可,也就是第三位和第四位是00
对于第五位,固定为0
对于第六位,自增模式,我一般直接置0
对于第七八位,是选择通道的:
通道0对应的是00,蓝桥杯板子上连接的是一个空引脚,用于后续拓展或者DA输出
通道1对应01,蓝桥杯板子上连接的是光敏电阻
通道2对应10,蓝桥杯板子上连接的是LM324,没见使用过
通道3对应11,蓝桥杯板子上连接的是电位计
做个简单的小总结,对于AD转化控制字发送0000 0001(转化为十进制的话就是1),对应光敏电阻;控制字发送0000 0011(转化为十进制的话就是3),对应电位计。在使用是,刚好可以直接发送对应的十进制数选择它的通道,因为高位都是0.
对于DA转化,则只有一个控制字,也就是0100 0000(0x40),表示DA输出。
我们一般都是在写入PCF8591的状态下,对其进行控制,然后重新启动PCF8591,再读取其数据。
5.代码实现
对于读取PCF8591(AD转化)和写入PCF8591(DA输出),我们各需要一个函数,不过这里需要注意iic在通信时需要WaitAck,这里不再赘述。
//对于控制字节:
//0xxxx 0xxxx
//第一位:固定为0
//第二位:1:使能输出
//第三四位:00模式0,01模式1,10模式3,11模式4。。。。通常只用模式0
//
//第五位:固定为0
//第六位:1:开启自动增量,一般不开
//第七八位:00:通道0,01:通道1,10:通道2,11:通道3。。。
//对于蓝桥杯板子,DA输出对应的引脚是通道0
//光敏电阻输入电压对应通道1
//电位计输入电压对应通道3
void wirte_pcf8591(unsigned char dat)
{
I2CStart();
I2CSendByte(0x90);//发送地址位,写入数据
I2CWaitAck();
I2CSendByte(0x40);//发送控制字节:0100 0000;对应功能为:使能输出,通道0
I2CWaitAck();
I2CSendByte(dat);
I2CWaitAck();
I2CStop();
}//一般传递的参数为:0000 0001对应功能:读取光敏电子电压。或0000 0011 对应功能:读取电位计电压
unsigned char read_pcf8591(unsigned char dat)
{
unsigned char AD=0;I2CStart();
I2CSendByte(0x90);//发送地址位,写入数据
I2CWaitAck();
I2CSendByte(dat);//发送控制字节(同时选择通道)
I2CWaitAck();
I2CStop();
I2CStart();
I2CSendByte(0x91);//发送地址位,读取数据
I2CWaitAck();
AD=I2CReceiveByte();
I2CSendAck(1);
I2CStop();
return AD;
}
三、AT24C02掉电不丢失存储器
1.关于AT24C02
AT24C02是一种串行EEPROM(Electrically Erasable Programmable Read-Only Memory)芯片。它是Atmel公司生产的一种2K位(256字节)容量的存储器芯片,可通过I2C总线进行数据读写操作。
AT24C02采用8位地址寻址,总共有256个地址,每个地址存储一个字节的数据。它可以提供高达400kHz的I2C总线速度。AT24C02还具有写保护功能,可以通过设置写保护位来防止对芯片的非授权写入。
通过I2C总线,可以连接多个AT24C02芯片,实现扩展存储容量。它的低功耗特性和可靠的数据存储使得它在各种电子设备中都得到广泛应用。
AT24C02简单点说就是一个“内存卡”,他有256(地址取值0-255)个地址,每个可以存一个8位数据(数据取值0-255),断电之后,AT24C02里面的数据不会丢失。
此外,AT24C02的读写次数不是无限的,切勿放在while(1)循环内,快速连续读写AT24C02,容易对AT24C02造成损坏,除此之外,如果写入AT24C02之后立刻读取数据,是有可能读取不出来的,建议中间加个100ms的延时再读取。
2.ROM检查
与AT24C02与PCF8591挂载在同一个iic总线上,在正确使用AT24C02之前也需要进行正确的ROM检测。
与PCF8591类似,AT24C02的地址也是一个8位二进制数,也由三部分组成:一部分是固定地址,一部分是可变地址,一部分时读写位。
我们用的是AT24C02,也就是内部存储是2k的存储器,上边列举的是从1k到16k的 ,我们只需要看第一行关于2k的即可。
AT24C02的固定地址是高四位,固定位1010(Ah)
AT24C02的低四位的高三位是可变地址,再看一眼原理图:
AT24C02的A0 A1 A2跟PCF8591类似,也都全部接地,也就是逻辑0.
AT24C02地址的最后一位也是读写位:1读,0写。
所以我们在向AT24C02写入数据之前,需要发送1010 0000(0xA0)的地址进行检查,在读取数据之前要发生1010 0001(0xA1)的地址进行检查。
3.AT24C02的读写
不同于前边讲的温度传感器和AD/DA转化器,AT24C02不需要控制字即可直接使用。
如果是写数据,则需要先发送0xA0进行ROM检查,然后依次发送要写入AT24C02内部的哪个地址,以及要写入的数据。注意中间需要WaitAck。
如果是读数据,则需要先发送0xA0进行ROM检查,然后再发送我们需要读取AT24C02内部哪个地址的数据,然后重新启动总线发送0xA1,再然后读取数据。
4.代码实现
对于AT24C02,我们需要两个函数来实现其功能。分别是读取函数和写入函数:
/*AT24C02*/
//与pcf8591类似
//对于地址位:
//地址=1010 A2 A1 A0 R/W_
//A2 A1 A0为硬件地址,根据硬件电路确定
//R/W_为读写位,1:读取数据,0:写入数据
//对于蓝桥杯板子,A2 A1 A0 均接地,其值为0。
//所以发送地址1010 0000 对应发送地址+写入数据
//所以发送地址1010 0001 对应发送地址+读取数据void write_AT24C02(unsigned char add,unsigned char dat)
{
I2CStart();
I2CSendByte(0xA0);//发送地址位,写入数据
I2CWaitAck();
I2CSendByte(add);//发送待写入的地址
I2CWaitAck();
I2CSendByte(dat);//发送待写入的数据
I2CWaitAck();
I2CStop();
}unsigned char read_AT24C02(unsigned char add)
{
unsigned char at=0;
I2CStart();
I2CSendByte(0xA0);//发送地址位,写入数据
I2CWaitAck();
I2CSendByte(add);//发送待读取的地址
I2CWaitAck();
I2CStop();
I2CStart();
I2CSendByte(0xA1);//发送地址位,读取数据
I2CWaitAck();
at=I2CReceiveByte();//读取数据
I2CSendAck(1);
I2CStop();
return at;
}
四、代码总结
二三章提到的代码实现,只需要放在第一章修改之后的底层驱动的下边即可。
其实学到这里,已经具备了完成早些年部分省赛题目的能力(所以下编写的测试也会更加贴近于比赛)
这里写一个小代码,对刚才的功能进行测试与总结,主要完成以下功能:
1.上电之后所有LED灯熄灭
2.按下S7,蜂鸣器和继电器开启,松开S7,蜂鸣器和继电器关闭
3.数码管前三位显示PCF8591读取到的数据,后三位读取到的AT24C02的100地址的数据
4.LED灯第一位显示PCF8591的模式,共有两个模式,灭时表示模式1PCF8591正在读取光敏电阻的值,亮起表示模式2正在读取电位计的值。上电默认读取电位计的值。
5,定义按键S11为切换PCF8591的模式,每按下一次S11,切换一次PCF8591的模式
6.如果读取到的PCF8591的值高于60(这个值随意),则后四个led灯亮起,否则熄灭
7.定义S6为记录按键,按下S6之后,将当前PCF8591读取到的数据存储到AT24C02的100地址内。
main.c文件
- #include
- #include
- #include "iic.h"
-
- 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
- };//共阳极断码表,不用记,考试时会提供,直接CV过来即可
-
- unsigned char LED_Num=0xFF;//用来记录当前LED的状态
- unsigned char ULN=0x00;//用来记录当前ULN的值,ULN主要控制蜂鸣器和继电器,也会用来控制电机
-
- //LED有关的三个宏函数的思路都是,先更新LED_Num的值,然后将P0=LED_Num,再然后开关一次LED灯的锁
- //存器,以更新数据
- //点亮第x的LED灯,将LED_Num中第x位置为0(因为是共阳极LED灯)关闭同理,需将第x位置为1
- #define LED_ON(x) LED_Num&=~(0x01<
- #define LED_OFF(x) LED_Num|=0x01<
- #define LED_OFF_ALL() LED_Num=0xFF; P0=0xFF;P2|=0x80;P2&=0x9F;P2&=0x1F;
-
- //关于数码管的两个函数,因为需要从数组内读取待显示的数据,还要进行位选,所以这两个函数只起到开
- //关锁存器的功能,没有更新数据(会在其他地方实现数据更新和获取)
- #define NIXIE_CHECK() P2|=0xC0;P2&=0xDF;P2&=0x1F;
- #define NIXIE_ON() P2|=0xE0;P2&=0xFF;P2&=0x1F;
-
- //对于蜂鸣器的继电器,与LED类似,不过蜂鸣器是直接为特定的某一位置位。
- //其思路可以理解为通过宏函数打开或熄灭某一个LED灯
- //对BUZZER_ON就是把ULN第7位置为1,对于RELAY_ON就是吧第5位置为1
- #define BUZZER_ON() ULN|=0x40; P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
- #define BUZZER_OFF() ULN&=0xB0; P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
-
- #define RELAY_ON() ULN|=0x10; P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
- #define RELAY_OFF() ULN&=0xE0; P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
-
- void Timer0_Init(void); //1毫秒@11.0592MHz
- void Delay100ms(void); //@11.0592MHz
- void Delay5ms(void); //@11.0592MHz
- void get_key();//读取按键的函数
- void pcf_check(void);
-
- unsigned char location=0;//用于记录当前到哪一位数码管要显示数据了。是一个中间变量
- unsigned char Nixie_num[]={10,10,10,10,10,10,10,10};//用来存储数码管待显示的数据
- unsigned char key_value=0;
-
- bit pcf_mod=0;//pcf的模式,模式0,读取光敏电阻,模式1读取电位计
- bit pcf_is_greater_than=0;//记录上一次(或几次)pcf的值是否超过阈值,1:超过 0:没超过
-
- //设置PCF8591的阈值,当读取到的数据高于这个值,则LED灯亮,否则熄灭。
- //比赛时可以不设置这个变量,直接判断阈值时写阈值是多少即可,这里用变量仅方便读者修改
- unsigned char pcf_yuzhi=60;
-
- unsigned char ad=0;//记录读取到的PCF8591的值,也就是AD转化器的值
- unsigned char at=0;//记录读取到的AT24C02的值
- void main()
- {
- LED_OFF_ALL();//熄灭所以LED灯
- RELAY_OFF();//关闭继电器
- BUZZER_OFF();//关闭蜂鸣器
- at=read_AT24C02(100);//上电先读取一次AT24C02在100地址的值
- Timer0_Init();//定时器初始化
- EA=1;//开总中断
- Delay100ms();
- while(1)
- {
- get_key();//读取按键的值
-
- pcf_check();//读取pcf,并且对数据进行处理
- Nixie_num[0]=ad/100%10;//显示pcf的值
- Nixie_num[1]=ad/10%10;
- Nixie_num[2]=ad/1%10;
-
- Nixie_num[5]=at/100%10;//显示at的值
- Nixie_num[6]=at/10%10;
- Nixie_num[7]=at/1%10;
- Delay100ms();
- }
-
- }
- void Timer0_Isr(void) interrupt 1
- {
- P0=0x01<
NIXIE_CHECK();//选择要显示的某一位 - P0=Seg_Table[Nixie_num[location]];NIXIE_ON();//从数组中读取数据并显示
-
- if(++location==8)//位数+1,从而实现扫描
- location=0;//位数=8,重新清零(location取值是0到7八个数)
- }
-
- void Timer0_Init(void) //1毫秒@11.0592MHz
- {
- AUXR |= 0x80; //定时器时钟1T模式
- TMOD &= 0xF0; //设置定时器模式
- TL0 = 0xCD; //设置定时初始值
- TH0 = 0xD4; //设置定时初始值
- TF0 = 0; //清除TF0标志
- TR0 = 1; //定时器0开始计时
- ET0 = 1; //使能定时器0中断
- }
- void Delay100ms(void) //@11.0592MHz
- {
- unsigned char data i, j, k;
-
- _nop_();
- _nop_();
- i = 5;
- j = 52;
- k = 195;
- do
- {
- do
- {
- while (--k);
- } while (--j);
- } while (--i);
- }
- void Delay5ms(void) //@11.0592MHz
- {
- unsigned char data i, j;
-
- i = 54;
- j = 199;
- do
- {
- while (--j);
- } while (--i);
- }
-
- //Delay5ms()以及while(xxx);是为了按键消抖,避免按下一次按键重复处理
- void get_key()
- {
- unsigned char key_P3=P3;//记录当前P3的值,扫描完按键之后再将P3的值复位
- unsigned char key_P4=P4;//记录当前P4的值,扫描完按键之后再将P4的值复位
-
- //扫描第一列
- P44=0;
- //第一个if这样写只是为了方便看启动蜂鸣器和继电器,也只在测试时会这样写了。
- //一般所有对于按键的处理都在get_key()内实现。
- if(P30==0)
- {
- Delay5ms();//消抖
- BUZZER_ON();//测试蜂鸣器
- RELAY_ON();//测试继电器
- while(P30==0);
- BUZZER_OFF();//关闭蜂鸣器
- RELAY_OFF();//关闭继电器
- Delay5ms();
- key_value=7;
- }
- else if(P31==0){Delay5ms();while(P31==0);Delay5ms();key_value=6;}
- else if(P32==0){Delay5ms();while(P32==0);Delay5ms();key_value=5;}
- else if(P33==0){Delay5ms();while(P33==0);Delay5ms();key_value=4;}
-
- P42=0;
- if(P30==0){Delay5ms();while(P30==0);Delay5ms();key_value=11;}
- else if(P31==0){Delay5ms();while(P31==0);Delay5ms();key_value=10;}
- else if(P32==0){Delay5ms();while(P32==0);Delay5ms();key_value=9;}
- else if(P33==0){Delay5ms();while(P33==0);Delay5ms();key_value=8;}
-
- P35=0;
- if(P30==0){Delay5ms();while(P30==0);Delay5ms();key_value=15;}
- else if(P31==0){Delay5ms();while(P31==0);Delay5ms();key_value=14;}
- else if(P32==0){Delay5ms();while(P32==0);Delay5ms();key_value=13;}
- else if(P33==0){Delay5ms();while(P33==0);Delay5ms();key_value=12;}
-
- P34=0;
- if(P30==0){Delay5ms();while(P30==0);Delay5ms();key_value=19;}
- else if(P31==0){Delay5ms();while(P31==0);Delay5ms();key_value=18;}
- else if(P32==0){Delay5ms();while(P32==0);Delay5ms();key_value=17;}
- else if(P33==0){Delay5ms();while(P33==0);Delay5ms();key_value=16;}
-
- /*按下了s11,并且当前处在pcf的模式1*/
- if(key_value==11&&pcf_mod==1)
- {
- pcf_mod=0;
- LED_OFF(0);
- }
- else if(key_value==11&&pcf_mod==0)/*按下了s11,并且当前处在pcf的模式0*/
- {
- pcf_mod=1;
- LED_ON(0);
- }
- //如果s11仅用于切换pcf的模式,而不要进行其他操作的话,可以下成如下写法:
- // if(key_value==11)
- // {
- // pcf_mod=~pcf_mod;//pcf模式直接取反
- // }
-
- /*按下s6*/
- if(key_value==6)
- {
- //因为我们已经知道次数ad的值了,就不用先把ad存到at24c02在读取,而是直接at=ad,再将ad存到at24c02
- //可以省去关于at24c02的不必要的异常情况(不容易出bug)
- //如果真是先把ad存到at里在读,记得中间要加延时,不能存完立即读,会读不出来
- at=ad;
- write_AT24C02(100,ad);
- }
-
- //在以后大部分代码中,我都会加这一行,使得get_key函数读取到的键值仅仅在函数中作用一次,避免重复处理按键
- key_value=0;
- P3=key_P3;
- P4=key_P4;
-
- }
- void pcf_check(void)
- {
- if(pcf_mod==0)//如果pcf处在模式0,就读取光敏电阻
- ad=read_pcf8591(1);
- else if(pcf_mod==1)//如果pcf处在模式1,就读取电位计
- ad=read_pcf8591(3);
-
-
- //下面两个if判断,都是为了避免重复操作,引发异常
- //如果前几次ad的值没有超过阈值,而这次ad的值超过阈值了,则处理
- if(pcf_is_greater_than==0&&ad>pcf_yuzhi)
- {
- pcf_is_greater_than=1;//记录ad的值超过阈值
- LED_ON(4);//点亮led灯
- LED_ON(5);
- LED_ON(6);
- LED_ON(7);
- }
- //如果前几次ad的值超过阈值,而这次ad的值没有超过阈值了,则处理
- else if(pcf_is_greater_than==1&&ad<=pcf_yuzhi)
- {
- pcf_is_greater_than=0;//记录ad的值没有超过阈值
- LED_OFF(4);//熄灭led灯
- LED_OFF(5);
- LED_OFF(6);
- LED_OFF(7);
- }
-
- }
iic.c文件
- /* # I2C代码片段说明
- 1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
- 2. 参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
- 中对单片机时钟频率的要求,进行代码调试和修改。
- */
-
- #define DELAY_TIME 5
- #include
- #include "iic.h"
- #include
-
- sbit sda=P2^1;
- sbit scl=P2^0;
- //
- 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);
- }
- /*PCF8591*/
- //对于地址位:
- //地址=1001 A2 A1 A0 R/W_
- //A2 A1 A0为硬件地址,根据硬件电路确定
- //R/W_为读写位,1:读取数据,0:写入数据
- //对于蓝桥杯板子,A2 A1 A0 均接地,其值为0。
- //所以发送地址1001 0000 对应发送地址+写入数据
- //所以发送地址1001 0001 对应发送地址+读取数据
-
- //对于控制字节:
- //0xxxx 0xxxx
- //第一位:固定为0
- //第二位:1:使能输出
- //第三四位:00模式0,01模式1,10模式3,11模式4。。。。通常只用模式0
- //
- //第五位:固定为0
- //第六位:1:开启自动增量,一般不开
- //第七八位:00:通道0,01:通道1,10:通道2,11:通道3。。。
- //对于蓝桥杯板子,DA输出对应的引脚是通道0
- //光敏电阻输入电压对应通道1
- //电位计输入电压对应通道3
- void wirte_pcf8591(unsigned char dat)
- {
- I2CStart();
- I2CSendByte(0x90);//发送地址位,写入数据
- I2CWaitAck();
- I2CSendByte(0x40);//发送控制字节:0100 0000;对应功能为:使能输出,通道0
- I2CWaitAck();
- I2CSendByte(dat);
- I2CWaitAck();
- I2CStop();
- }
-
- //一般传递的参数为:0000 0001对应功能:读取光敏电子电压。或0000 0011 对应功能:读取电位计电压
- unsigned char read_pcf8591(unsigned char dat)
- {
- unsigned char AD=0;
-
- I2CStart();
- I2CSendByte(0x90);//发送地址位,写入数据
- I2CWaitAck();
- I2CSendByte(dat);//发送控制字节(同时选择通道)
- I2CWaitAck();
- I2CStop();
-
- I2CStart();
- I2CSendByte(0x91);//发送地址位,读取数据
- I2CWaitAck();
- AD=I2CReceiveByte();
- I2CSendAck(1);
- I2CStop();
-
- return AD;
- }
-
- /*AT24C02*/
- //与pcf8591类似
- //对于地址位:
- //地址=1010 A2 A1 A0 R/W_
- //A2 A1 A0为硬件地址,根据硬件电路确定
- //R/W_为读写位,1:读取数据,0:写入数据
- //对于蓝桥杯板子,A2 A1 A0 均接地,其值为0。
- //所以发送地址1010 0000 对应发送地址+写入数据
- //所以发送地址1010 0001 对应发送地址+读取数据
-
- void write_AT24C02(unsigned char add,unsigned char dat)
- {
- I2CStart();
- I2CSendByte(0xA0);//发送地址位,写入数据
- I2CWaitAck();
- I2CSendByte(add);//发送待写入的地址
- I2CWaitAck();
- I2CSendByte(dat);//发送待写入的数据
- I2CWaitAck();
- I2CStop();
- }
-
- unsigned char read_AT24C02(unsigned char add)
- {
- unsigned char at=0;
- I2CStart();
- I2CSendByte(0xA0);//发送地址位,写入数据
- I2CWaitAck();
- I2CSendByte(add);//发送待读取的地址
- I2CWaitAck();
- I2CStop();
-
- I2CStart();
- I2CSendByte(0xA1);//发送地址位,读取数据
- I2CWaitAck();
- at=I2CReceiveByte();//读取数据
- I2CSendAck(1);
- I2CStop();
-
- return at;
- }
iic.h文件
- #ifndef _IIC_H_
- #define _IIC_H_
-
- void wirte_pcf8591(unsigned char dat);
- unsigned char read_pcf8591(unsigned char dat);
-
- void write_AT24C02(unsigned char add,unsigned char dat);
- unsigned char read_AT24C02(unsigned char add);
- #endif
后续会带大家逐步深入的分享蓝桥杯比赛的代码
目录
前言
本系列文章意在帮助各位正在准备蓝桥杯单片机组的同学,提供一个参考与指南,但是所有指南的前提是,默认你已经有单片机基础,本系列文章会提供本人对蓝桥杯单片机组编程方面的一些源码实现。当然,或许与你写代码的style完全不想同,那咱们也可以彼此相互交流各自的优缺点,或许你可能只是一个小白,就是想来抄一些可供借鉴的代码,这些都不重要,重要的是能给正在准备单片机组的同学提供到一些帮助。
此外代码可能有写的不完善的地方,但是每一个代码都是经过测试可行之后才发出来的,不敢保证十全十美,但是跑起来应该没问题。
此外,比赛时会提供一个单片机资源数据包,里面的内容比较多,这里只放一个下载链接,是2023年省赛是提供的单片机资源数据包(今年才2024,已经是最新的资源数据包了),正文中会直接使用资源数据包内的资料:
单片机资源数据包_2023(点击下载)
关于此资源数据包还得补充两句,他的3-底层驱动代码参考与往些年不同(但其实差异也很小,这里不再赘述),而且还是快比赛时才公布的这个资源数据包。也就是说,在写底层驱动代码时,我都是以2023年的资源数据包为基础写的,如果你使用的不是2023年的资源数据包的话,可能会跟我使用的底层驱动不完全相同,不过影响也不大。
下图为单片机资源数据包_2023内的所有资源,其中最关键的就是3-底层代码驱动以及SCH_硬件原理图V30了,当然共阳数码管段码表也十分常用,不过一般也就新写一个项目时,会把里面的东西CV到main函数里,然后这个文件就用不着了。
一、关于iic总线
1.iic总线通信
IIC总线是一种串行通信协议,也被称为I2C(Inter-Integrated Circuit)总线。
IIC总线采用两根线进行通信,一根是时钟线(SCL,Serial Clock),另一根是数据线(SDA,Serial Data)。通过这两根线,多个设备可以在同一总线上进行通信,每个设备通过一个唯一的地址进行识别。
IIC总线具有以下特点:
- 串行通信:数据在时钟的控制下以位的方式传输,节约通信线路的数量。
- 主从结构:IIC总线中有一个主设备(Master)和多个从设备(Slave)。主设备负责发起通信请求,而从设备则被动应答。
- 多设备共享:多个设备可以在同一个总线上共享,通过设备地址进行识别和通信。
- 支持多速率:IIC总线支持不同的时钟速率,高速模式和标准模式之间可以进行切换。
- 双向传输:数据线上既可以传输主设备发送的数据,也可以传输从设备返回的数据。
IIC总线广泛应用于各种电子设备中,特别是在嵌入式系统中。它可以用于连接各种外围设备,如传感器、存储器、显示器等,实现设备之间的数据交换和通信。
在蓝桥杯比赛中,iic总线上挂载了两个设备,分别是PCF8591 AD/DA转化器,以及AT24C02 掉电不丢失数据存储器,也就是eeprom存储器。这就比第二篇讲的,onewire要复杂了,因为那个总线只挂载了一个简单的温度传感器,而且一般我们只需要从温度传感器读取数据;而iic总线上挂载的两个设备,都是我们不单需要读取,而且还需要写入数据的。不过在比赛时,一般也只会出其中的一小部分,不会两个外设的读写程序同时用上,下面提到时在详细介绍。
2.iic底层驱动代码
单片机资源数据包_2023中给出来iic驱动的底层代码:
- /* # I2C代码片段说明
- 1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
- 2. 参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
- 中对单片机时钟频率的要求,进行代码调试和修改。
- */
-
- #define DELAY_TIME 5
-
- //
- 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);
- }
-
当然,把这个代码复制到project里面之后,点击编译还是会报错。在正常使用之前,我们需要定义正确的管脚,以及引用头文件。此外,为了方面main函数引用,我们还需要手动添加一个iic.h的头文件,并将main.c和iic.c引用iic.h头文件。
从原理图可以看出,scl连接的是P20引脚,sda连接的是P21的引脚,再加上需要引用的头文件,因此我们需要再iic.c开头加上以下代码:
#include "iic.h"
#include
#include
sbit sda=P2^1;
sbit scl=P2^0;
并创建空的iic.h文件
#ifndef _IIC_H_
#define _IIC_H_
#endif
这样再次点击编译就不会报错了,但是仍旧会有许多警告,这些警告都是关于已经定义的函数未被调用的,不影响我们正常使用。
二、PCF8591 AD/DA转化器
1.关于PCF8591
PCF8591是一个8位的AD/DA转化器,8位的意思就是,它的精度是8位,也就是0到255.与onewire驱动的DS18B20温度传感器不同,PCF8591的采样速率取决于iic的速度,换言之,PCF8591的采样速度比iic总线的速度还要快,所以采样速度取决于iic的速度;而温度传感器因为需要进行温度转化,温度转化的速度往往很慢(最慢好像可以达到700ms,当然700ms的温度转换也就意味着温度的精度更高)。
PCF8591有四个通道,如果需要同时读取多个通道的话会出现意料之外的情况,因为比赛时也很少遇到需要读取两个甚至以上通道的情况,这里不再赘述。
2.AD/DA转化
这里简单介绍一下AD/DA转化:
AD/DA转换是指模拟信号与数字信号之间的相互转换过程。
AD转换(Analog-to-Digital)是将连续的模拟信号转换为离散的数字信号的过程。在AD转换中,模拟信号被采样并量化,然后通过编码转换为对应的数字信号。AD转换器的输入是模拟信号,输出是数字信号,在计算机和数字系统中广泛应用。AD转换器的输出通常是一个二进制代码,代表了模拟输入信号的离散化数值。
DA转换(Digital-to-Analog)是将离散的数字信号转换为连续的模拟信号的过程。在DA转换中,数字信号被解码并转换为对应的模拟信号。DA转换器的输入是数字信号,输出是模拟信号。DA转换器常用于将数字系统产生的数据转换为模拟信号,以便驱动模拟设备或输出到模拟电路中。
AD/DA转换在许多领域得到广泛应用,包括音频处理、信号采集与处理、测量仪器、通信系统等。通过AD转换,模拟信号可以被数字设备处理和分析;通过DA转换,数字信号可以被转换为模拟信号,以便输出到模拟设备中,例如扬声器、马达等。
刚才提到,PCF8591的精度是8位,也就是0到255。在使用AD/DA转化之前需要给它一个基准电压,也就是GND和一个标准的5V电压(蓝桥杯板子上的PCF8591的5V基准电压可能不太准,所以有时读出的数据跟实际的数据可能不太对照),PCF8591会把0V到5V拆分成255份,每份对应的电压值为5/255V。也就是说,0对应的是0V,255对应的是5V。
比如现在输入3V电压,使用AD转化,那3V对应的AD输出值就是(3/5)*255=153。
同理,对于DA转化就是AD转化的逆过程,DA转化是输入一个数字信号,输出一个模拟信号,比如输入了102(取值范围是0到255),那么输出电压就等于(102/255)*5等于2V。
3.ROM检测
iic总线上可能挂载的有多个设备,在使用PCF8591之前,需要进行正确的ROM检测。这个时候就不能像读取温度传感器时那样,直接跳过ROM检查了,因为这个iic总线上挂载了两个设备。
PCF8591的地址是一个8位二进制数,由三部分组成:一部分是固定地址,一部分是可变地址,一部分时读写位。
地址的高四位是固定部分,固定为1001
地址的低四位的高三位是可变部分,由芯片上的A2 A1 A0决定,我们再看一眼PCF8591的原理图:
可见,蓝桥杯板子上的A0 A1 A2都是接地的状态,也就是逻辑000
地址的低四位的低一位,是读写位:1代表读取数据,0代表写入数据
也就是说,如果我们想读取PCF8591,我们首先要进行ROM检测,发送数据1001000x,也就是0x90或者0x91。
4.PCF8591的控制字
进行ROM检测之后,我们就可以对PCF8591进行控制。
控制寄存器的高半字节用于允许模拟输出,和将模拟输入编程为单端或差分输入。低半字节选择一个由高半字节定义的
模拟输入通道。如果自动增量(auto-increment)标志置 1,每次 A/D 转换后通道号将自动增加。
如果自动增量(auto-increment)模式是使用内部振荡器的应用中所需要的,那么控制字中模拟输出允许标志应置1。这要求内部振荡器持续运行,因此要防止振荡器启动延时的转换错误结果。模拟输出允许标志可以在其他时候复位以减少静态功耗。
选择一个不存在的输入通道将导致分配最高可用的通道号。所以,如果自动增量(auto-increment)被置1,下一个被选择的通道将总是通道0。两个半字节的最高有效位(即bit 7 和bit 3)是留给未来的功能,必须设置为逻辑0。控制寄存器的所有位在上电复位后被复位为逻辑0。 D/A 转换器和振荡器在节能时被禁止。模拟输出被切换到高阻态。
PCF8591还是有许多功能的,这里只介绍比赛时会用到的一些。
对于第一位,固定为0
对于第二位,1:DA转化,0:AD转化
对于第三四位,是选择PCF8591的工作模式的,我们一般选择第一个模式即可,也就是第三位和第四位是00
对于第五位,固定为0
对于第六位,自增模式,我一般直接置0
对于第七八位,是选择通道的:
通道0对应的是00,蓝桥杯板子上连接的是一个空引脚,用于后续拓展或者DA输出
通道1对应01,蓝桥杯板子上连接的是光敏电阻
通道2对应10,蓝桥杯板子上连接的是LM324,没见使用过
通道3对应11,蓝桥杯板子上连接的是电位计
做个简单的小总结,对于AD转化控制字发送0000 0001(转化为十进制的话就是1),对应光敏电阻;控制字发送0000 0011(转化为十进制的话就是3),对应电位计。在使用是,刚好可以直接发送对应的十进制数选择它的通道,因为高位都是0.
对于DA转化,则只有一个控制字,也就是0100 0000(0x40),表示DA输出。
我们一般都是在写入PCF8591的状态下,对其进行控制,然后重新启动PCF8591,再读取其数据。
5.代码实现
对于读取PCF8591(AD转化)和写入PCF8591(DA输出),我们各需要一个函数,不过这里需要注意iic在通信时需要WaitAck,这里不再赘述。
//对于控制字节:
//0xxxx 0xxxx
//第一位:固定为0
//第二位:1:使能输出
//第三四位:00模式0,01模式1,10模式3,11模式4。。。。通常只用模式0
//
//第五位:固定为0
//第六位:1:开启自动增量,一般不开
//第七八位:00:通道0,01:通道1,10:通道2,11:通道3。。。
//对于蓝桥杯板子,DA输出对应的引脚是通道0
//光敏电阻输入电压对应通道1
//电位计输入电压对应通道3
void wirte_pcf8591(unsigned char dat)
{
I2CStart();
I2CSendByte(0x90);//发送地址位,写入数据
I2CWaitAck();
I2CSendByte(0x40);//发送控制字节:0100 0000;对应功能为:使能输出,通道0
I2CWaitAck();
I2CSendByte(dat);
I2CWaitAck();
I2CStop();
}//一般传递的参数为:0000 0001对应功能:读取光敏电子电压。或0000 0011 对应功能:读取电位计电压
unsigned char read_pcf8591(unsigned char dat)
{
unsigned char AD=0;I2CStart();
I2CSendByte(0x90);//发送地址位,写入数据
I2CWaitAck();
I2CSendByte(dat);//发送控制字节(同时选择通道)
I2CWaitAck();
I2CStop();
I2CStart();
I2CSendByte(0x91);//发送地址位,读取数据
I2CWaitAck();
AD=I2CReceiveByte();
I2CSendAck(1);
I2CStop();
return AD;
}
三、AT24C02掉电不丢失存储器
1.关于AT24C02
AT24C02是一种串行EEPROM(Electrically Erasable Programmable Read-Only Memory)芯片。它是Atmel公司生产的一种2K位(256字节)容量的存储器芯片,可通过I2C总线进行数据读写操作。
AT24C02采用8位地址寻址,总共有256个地址,每个地址存储一个字节的数据。它可以提供高达400kHz的I2C总线速度。AT24C02还具有写保护功能,可以通过设置写保护位来防止对芯片的非授权写入。
通过I2C总线,可以连接多个AT24C02芯片,实现扩展存储容量。它的低功耗特性和可靠的数据存储使得它在各种电子设备中都得到广泛应用。
AT24C02简单点说就是一个“内存卡”,他有256(地址取值0-255)个地址,每个可以存一个8位数据(数据取值0-255),断电之后,AT24C02里面的数据不会丢失。
此外,AT24C02的读写次数不是无限的,切勿放在while(1)循环内,快速连续读写AT24C02,容易对AT24C02造成损坏,除此之外,如果写入AT24C02之后立刻读取数据,是有可能读取不出来的,建议中间加个100ms的延时再读取。
2.ROM检查
与AT24C02与PCF8591挂载在同一个iic总线上,在正确使用AT24C02之前也需要进行正确的ROM检测。
与PCF8591类似,AT24C02的地址也是一个8位二进制数,也由三部分组成:一部分是固定地址,一部分是可变地址,一部分时读写位。
我们用的是AT24C02,也就是内部存储是2k的存储器,上边列举的是从1k到16k的 ,我们只需要看第一行关于2k的即可。
AT24C02的固定地址是高四位,固定位1010(Ah)
AT24C02的低四位的高三位是可变地址,再看一眼原理图:
AT24C02的A0 A1 A2跟PCF8591类似,也都全部接地,也就是逻辑0.
AT24C02地址的最后一位也是读写位:1读,0写。
所以我们在向AT24C02写入数据之前,需要发送1010 0000(0xA0)的地址进行检查,在读取数据之前要发生1010 0001(0xA1)的地址进行检查。
3.AT24C02的读写
不同于前边讲的温度传感器和AD/DA转化器,AT24C02不需要控制字即可直接使用。
如果是写数据,则需要先发送0xA0进行ROM检查,然后依次发送要写入AT24C02内部的哪个地址,以及要写入的数据。注意中间需要WaitAck。
如果是读数据,则需要先发送0xA0进行ROM检查,然后再发送我们需要读取AT24C02内部哪个地址的数据,然后重新启动总线发送0xA1,再然后读取数据。
4.代码实现
对于AT24C02,我们需要两个函数来实现其功能。分别是读取函数和写入函数:
/*AT24C02*/
//与pcf8591类似
//对于地址位:
//地址=1010 A2 A1 A0 R/W_
//A2 A1 A0为硬件地址,根据硬件电路确定
//R/W_为读写位,1:读取数据,0:写入数据
//对于蓝桥杯板子,A2 A1 A0 均接地,其值为0。
//所以发送地址1010 0000 对应发送地址+写入数据
//所以发送地址1010 0001 对应发送地址+读取数据void write_AT24C02(unsigned char add,unsigned char dat)
{
I2CStart();
I2CSendByte(0xA0);//发送地址位,写入数据
I2CWaitAck();
I2CSendByte(add);//发送待写入的地址
I2CWaitAck();
I2CSendByte(dat);//发送待写入的数据
I2CWaitAck();
I2CStop();
}unsigned char read_AT24C02(unsigned char add)
{
unsigned char at=0;
I2CStart();
I2CSendByte(0xA0);//发送地址位,写入数据
I2CWaitAck();
I2CSendByte(add);//发送待读取的地址
I2CWaitAck();
I2CStop();
I2CStart();
I2CSendByte(0xA1);//发送地址位,读取数据
I2CWaitAck();
at=I2CReceiveByte();//读取数据
I2CSendAck(1);
I2CStop();
return at;
}
四、代码总结
二三章提到的代码实现,只需要放在第一章修改之后的底层驱动的下边即可。
其实学到这里,已经具备了完成早些年部分省赛题目的能力(所以下编写的测试也会更加贴近于比赛)
这里写一个小代码,对刚才的功能进行测试与总结,主要完成以下功能:
1.上电之后所有LED灯熄灭
2.按下S7,蜂鸣器和继电器开启,松开S7,蜂鸣器和继电器关闭
3.数码管前三位显示PCF8591读取到的数据,后三位读取到的AT24C02的100地址的数据
4.LED灯第一位显示PCF8591的模式,共有两个模式,灭时表示模式1PCF8591正在读取光敏电阻的值,亮起表示模式2正在读取电位计的值。上电默认读取电位计的值。
5,定义按键S11为切换PCF8591的模式,每按下一次S11,切换一次PCF8591的模式
6.如果读取到的PCF8591的值高于60(这个值随意),则后四个led灯亮起,否则熄灭
7.定义S6为记录按键,按下S6之后,将当前PCF8591读取到的数据存储到AT24C02的100地址内。
main.c文件
- #include
- #include
- #include "iic.h"
-
- 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
- };//共阳极断码表,不用记,考试时会提供,直接CV过来即可
-
- unsigned char LED_Num=0xFF;//用来记录当前LED的状态
- unsigned char ULN=0x00;//用来记录当前ULN的值,ULN主要控制蜂鸣器和继电器,也会用来控制电机
-
- //LED有关的三个宏函数的思路都是,先更新LED_Num的值,然后将P0=LED_Num,再然后开关一次LED灯的锁
- //存器,以更新数据
- //点亮第x的LED灯,将LED_Num中第x位置为0(因为是共阳极LED灯)关闭同理,需将第x位置为1
- #define LED_ON(x) LED_Num&=~(0x01<
- #define LED_OFF(x) LED_Num|=0x01<
- #define LED_OFF_ALL() LED_Num=0xFF; P0=0xFF;P2|=0x80;P2&=0x9F;P2&=0x1F;
-
- //关于数码管的两个函数,因为需要从数组内读取待显示的数据,还要进行位选,所以这两个函数只起到开
- //关锁存器的功能,没有更新数据(会在其他地方实现数据更新和获取)
- #define NIXIE_CHECK() P2|=0xC0;P2&=0xDF;P2&=0x1F;
- #define NIXIE_ON() P2|=0xE0;P2&=0xFF;P2&=0x1F;
-
- //对于蜂鸣器的继电器,与LED类似,不过蜂鸣器是直接为特定的某一位置位。
- //其思路可以理解为通过宏函数打开或熄灭某一个LED灯
- //对BUZZER_ON就是把ULN第7位置为1,对于RELAY_ON就是吧第5位置为1
- #define BUZZER_ON() ULN|=0x40; P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
- #define BUZZER_OFF() ULN&=0xB0; P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
-
- #define RELAY_ON() ULN|=0x10; P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
- #define RELAY_OFF() ULN&=0xE0; P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
-
- void Timer0_Init(void); //1毫秒@11.0592MHz
- void Delay100ms(void); //@11.0592MHz
- void Delay5ms(void); //@11.0592MHz
- void get_key();//读取按键的函数
- void pcf_check(void);
-
- unsigned char location=0;//用于记录当前到哪一位数码管要显示数据了。是一个中间变量
- unsigned char Nixie_num[]={10,10,10,10,10,10,10,10};//用来存储数码管待显示的数据
- unsigned char key_value=0;
-
- bit pcf_mod=0;//pcf的模式,模式0,读取光敏电阻,模式1读取电位计
- bit pcf_is_greater_than=0;//记录上一次(或几次)pcf的值是否超过阈值,1:超过 0:没超过
-
- //设置PCF8591的阈值,当读取到的数据高于这个值,则LED灯亮,否则熄灭。
- //比赛时可以不设置这个变量,直接判断阈值时写阈值是多少即可,这里用变量仅方便读者修改
- unsigned char pcf_yuzhi=60;
-
- unsigned char ad=0;//记录读取到的PCF8591的值,也就是AD转化器的值
- unsigned char at=0;//记录读取到的AT24C02的值
- void main()
- {
- LED_OFF_ALL();//熄灭所以LED灯
- RELAY_OFF();//关闭继电器
- BUZZER_OFF();//关闭蜂鸣器
- at=read_AT24C02(100);//上电先读取一次AT24C02在100地址的值
- Timer0_Init();//定时器初始化
- EA=1;//开总中断
- Delay100ms();
- while(1)
- {
- get_key();//读取按键的值
-
- pcf_check();//读取pcf,并且对数据进行处理
- Nixie_num[0]=ad/100%10;//显示pcf的值
- Nixie_num[1]=ad/10%10;
- Nixie_num[2]=ad/1%10;
-
- Nixie_num[5]=at/100%10;//显示at的值
- Nixie_num[6]=at/10%10;
- Nixie_num[7]=at/1%10;
- Delay100ms();
- }
-
- }
- void Timer0_Isr(void) interrupt 1
- {
- P0=0x01<
NIXIE_CHECK();//选择要显示的某一位 - P0=Seg_Table[Nixie_num[location]];NIXIE_ON();//从数组中读取数据并显示
-
- if(++location==8)//位数+1,从而实现扫描
- location=0;//位数=8,重新清零(location取值是0到7八个数)
- }
-
- void Timer0_Init(void) //1毫秒@11.0592MHz
- {
- AUXR |= 0x80; //定时器时钟1T模式
- TMOD &= 0xF0; //设置定时器模式
- TL0 = 0xCD; //设置定时初始值
- TH0 = 0xD4; //设置定时初始值
- TF0 = 0; //清除TF0标志
- TR0 = 1; //定时器0开始计时
- ET0 = 1; //使能定时器0中断
- }
- void Delay100ms(void) //@11.0592MHz
- {
- unsigned char data i, j, k;
-
- _nop_();
- _nop_();
- i = 5;
- j = 52;
- k = 195;
- do
- {
- do
- {
- while (--k);
- } while (--j);
- } while (--i);
- }
- void Delay5ms(void) //@11.0592MHz
- {
- unsigned char data i, j;
-
- i = 54;
- j = 199;
- do
- {
- while (--j);
- } while (--i);
- }
-
- //Delay5ms()以及while(xxx);是为了按键消抖,避免按下一次按键重复处理
- void get_key()
- {
- unsigned char key_P3=P3;//记录当前P3的值,扫描完按键之后再将P3的值复位
- unsigned char key_P4=P4;//记录当前P4的值,扫描完按键之后再将P4的值复位
-
- //扫描第一列
- P44=0;
- //第一个if这样写只是为了方便看启动蜂鸣器和继电器,也只在测试时会这样写了。
- //一般所有对于按键的处理都在get_key()内实现。
- if(P30==0)
- {
- Delay5ms();//消抖
- BUZZER_ON();//测试蜂鸣器
- RELAY_ON();//测试继电器
- while(P30==0);
- BUZZER_OFF();//关闭蜂鸣器
- RELAY_OFF();//关闭继电器
- Delay5ms();
- key_value=7;
- }
- else if(P31==0){Delay5ms();while(P31==0);Delay5ms();key_value=6;}
- else if(P32==0){Delay5ms();while(P32==0);Delay5ms();key_value=5;}
- else if(P33==0){Delay5ms();while(P33==0);Delay5ms();key_value=4;}
-
- P42=0;
- if(P30==0){Delay5ms();while(P30==0);Delay5ms();key_value=11;}
- else if(P31==0){Delay5ms();while(P31==0);Delay5ms();key_value=10;}
- else if(P32==0){Delay5ms();while(P32==0);Delay5ms();key_value=9;}
- else if(P33==0){Delay5ms();while(P33==0);Delay5ms();key_value=8;}
-
- P35=0;
- if(P30==0){Delay5ms();while(P30==0);Delay5ms();key_value=15;}
- else if(P31==0){Delay5ms();while(P31==0);Delay5ms();key_value=14;}
- else if(P32==0){Delay5ms();while(P32==0);Delay5ms();key_value=13;}
- else if(P33==0){Delay5ms();while(P33==0);Delay5ms();key_value=12;}
-
- P34=0;
- if(P30==0){Delay5ms();while(P30==0);Delay5ms();key_value=19;}
- else if(P31==0){Delay5ms();while(P31==0);Delay5ms();key_value=18;}
- else if(P32==0){Delay5ms();while(P32==0);Delay5ms();key_value=17;}
- else if(P33==0){Delay5ms();while(P33==0);Delay5ms();key_value=16;}
-
- /*按下了s11,并且当前处在pcf的模式1*/
- if(key_value==11&&pcf_mod==1)
- {
- pcf_mod=0;
- LED_OFF(0);
- }
- else if(key_value==11&&pcf_mod==0)/*按下了s11,并且当前处在pcf的模式0*/
- {
- pcf_mod=1;
- LED_ON(0);
- }
- //如果s11仅用于切换pcf的模式,而不要进行其他操作的话,可以下成如下写法:
- // if(key_value==11)
- // {
- // pcf_mod=~pcf_mod;//pcf模式直接取反
- // }
-
- /*按下s6*/
- if(key_value==6)
- {
- //因为我们已经知道次数ad的值了,就不用先把ad存到at24c02在读取,而是直接at=ad,再将ad存到at24c02
- //可以省去关于at24c02的不必要的异常情况(不容易出bug)
- //如果真是先把ad存到at里在读,记得中间要加延时,不能存完立即读,会读不出来
- at=ad;
- write_AT24C02(100,ad);
- }
-
- //在以后大部分代码中,我都会加这一行,使得get_key函数读取到的键值仅仅在函数中作用一次,避免重复处理按键
- key_value=0;
- P3=key_P3;
- P4=key_P4;
-
- }
- void pcf_check(void)
- {
- if(pcf_mod==0)//如果pcf处在模式0,就读取光敏电阻
- ad=read_pcf8591(1);
- else if(pcf_mod==1)//如果pcf处在模式1,就读取电位计
- ad=read_pcf8591(3);
-
-
- //下面两个if判断,都是为了避免重复操作,引发异常
- //如果前几次ad的值没有超过阈值,而这次ad的值超过阈值了,则处理
- if(pcf_is_greater_than==0&&ad>pcf_yuzhi)
- {
- pcf_is_greater_than=1;//记录ad的值超过阈值
- LED_ON(4);//点亮led灯
- LED_ON(5);
- LED_ON(6);
- LED_ON(7);
- }
- //如果前几次ad的值超过阈值,而这次ad的值没有超过阈值了,则处理
- else if(pcf_is_greater_than==1&&ad<=pcf_yuzhi)
- {
- pcf_is_greater_than=0;//记录ad的值没有超过阈值
- LED_OFF(4);//熄灭led灯
- LED_OFF(5);
- LED_OFF(6);
- LED_OFF(7);
- }
-
- }
iic.c文件
- /* # I2C代码片段说明
- 1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
- 2. 参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
- 中对单片机时钟频率的要求,进行代码调试和修改。
- */
-
- #define DELAY_TIME 5
- #include
- #include "iic.h"
- #include
-
- sbit sda=P2^1;
- sbit scl=P2^0;
- //
- 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);
- }
- /*PCF8591*/
- //对于地址位:
- //地址=1001 A2 A1 A0 R/W_
- //A2 A1 A0为硬件地址,根据硬件电路确定
- //R/W_为读写位,1:读取数据,0:写入数据
- //对于蓝桥杯板子,A2 A1 A0 均接地,其值为0。
- //所以发送地址1001 0000 对应发送地址+写入数据
- //所以发送地址1001 0001 对应发送地址+读取数据
-
- //对于控制字节:
- //0xxxx 0xxxx
- //第一位:固定为0
- //第二位:1:使能输出
- //第三四位:00模式0,01模式1,10模式3,11模式4。。。。通常只用模式0
- //
- //第五位:固定为0
- //第六位:1:开启自动增量,一般不开
- //第七八位:00:通道0,01:通道1,10:通道2,11:通道3。。。
- //对于蓝桥杯板子,DA输出对应的引脚是通道0
- //光敏电阻输入电压对应通道1
- //电位计输入电压对应通道3
- void wirte_pcf8591(unsigned char dat)
- {
- I2CStart();
- I2CSendByte(0x90);//发送地址位,写入数据
- I2CWaitAck();
- I2CSendByte(0x40);//发送控制字节:0100 0000;对应功能为:使能输出,通道0
- I2CWaitAck();
- I2CSendByte(dat);
- I2CWaitAck();
- I2CStop();
- }
-
- //一般传递的参数为:0000 0001对应功能:读取光敏电子电压。或0000 0011 对应功能:读取电位计电压
- unsigned char read_pcf8591(unsigned char dat)
- {
- unsigned char AD=0;
-
- I2CStart();
- I2CSendByte(0x90);//发送地址位,写入数据
- I2CWaitAck();
- I2CSendByte(dat);//发送控制字节(同时选择通道)
- I2CWaitAck();
- I2CStop();
-
- I2CStart();
- I2CSendByte(0x91);//发送地址位,读取数据
- I2CWaitAck();
- AD=I2CReceiveByte();
- I2CSendAck(1);
- I2CStop();
-
- return AD;
- }
-
- /*AT24C02*/
- //与pcf8591类似
- //对于地址位:
- //地址=1010 A2 A1 A0 R/W_
- //A2 A1 A0为硬件地址,根据硬件电路确定
- //R/W_为读写位,1:读取数据,0:写入数据
- //对于蓝桥杯板子,A2 A1 A0 均接地,其值为0。
- //所以发送地址1010 0000 对应发送地址+写入数据
- //所以发送地址1010 0001 对应发送地址+读取数据
-
- void write_AT24C02(unsigned char add,unsigned char dat)
- {
- I2CStart();
- I2CSendByte(0xA0);//发送地址位,写入数据
- I2CWaitAck();
- I2CSendByte(add);//发送待写入的地址
- I2CWaitAck();
- I2CSendByte(dat);//发送待写入的数据
- I2CWaitAck();
- I2CStop();
- }
-
- unsigned char read_AT24C02(unsigned char add)
- {
- unsigned char at=0;
- I2CStart();
- I2CSendByte(0xA0);//发送地址位,写入数据
- I2CWaitAck();
- I2CSendByte(add);//发送待读取的地址
- I2CWaitAck();
- I2CStop();
-
- I2CStart();
- I2CSendByte(0xA1);//发送地址位,读取数据
- I2CWaitAck();
- at=I2CReceiveByte();//读取数据
- I2CSendAck(1);
- I2CStop();
-
- return at;
- }
iic.h文件
- #ifndef _IIC_H_
- #define _IIC_H_
-
- void wirte_pcf8591(unsigned char dat);
- unsigned char read_pcf8591(unsigned char dat);
-
- void write_AT24C02(unsigned char add,unsigned char dat);
- unsigned char read_AT24C02(unsigned char add);
- #endif
后续会带大家逐步深入的分享蓝桥杯比赛的代码
评论记录:
回复评论: