Bootstrap

【蓝桥杯】单片机教程

目录

零、写在前面

本文是自己在学习单片机过程中做的笔记,参考了许多教学视频以及书籍。只会记载个人认为重点以及难点内容,像一些最基础的内容这里就不会再阐述了。

一、准备介绍

1、所需要的软件

keil c51stc-isp

官方提供资料包链接:https://pan.baidu.com/s/1jorpInWbRNYQTuNd_VRGkg?pwd=ffb7
提取码:ffb7

2、所需要的硬件

蓝桥杯官方CT107-D开发板

注:软件的具体安装教程就不在这里一一赘述,可以去B站上参考一些up发布的教程

二、点亮LED灯

1、LED灯怎么才能发光?

满足如图所示阳极为1阴极为0即可(具体原理可以百度搜索)
在这里插入图片描述

2、M74HC573M1R锁存器基本介绍

跟其他开发板不同的,这块开发板控制数码管、LED、蜂鸣器和继电器的是锁存器和P0。所以在学习之前我们先介绍一下锁存器相关的概念。

2.1、RS锁存器(RS latch)

在这里插入图片描述
真值表:

R(赋零)S(置)Q解释
00Q保持
011
100
11X不允许这种情况

2.2、D锁存器(D latch)

原理图:
在这里插入图片描述
EN= 0时,输出状态保持不变。(锁住)
EN =1时,输出随输入状态而改变。当D=0,Q=0,当D=1,Q=1 。

2.3、M74HC573M1R

在这里插入图片描述

74HC573的八个锁存器都是透明的D型锁存器.当使能 (LE)为高 时,Q输出将 随数据(D)输入而变。当使能 (LE)为低时,输出将锁存在已建立的数据电平上。
74hc573的OE引脚在使用时 通常与GND连接在一起,通过LE来选择锁存器的锁存与使用状态。

2.4、M74HC573M1R的应用

原理图:
在这里插入图片描述
LE决定是否能透过去改变灯亮灭的状态,即Y4C决定(1透0不透)

3、Y4C如何控制

3.1、Y4控制Y4C

在这里插入图片描述
内部逻辑图:在这里插入图片描述
WR通过这个跳线帽默认相当于接地,再通过一个或非门对于Y4C没有影响

在这里插入图片描述
所以Y4CY4控制 ,它们两个是相反的关系。那么Y4又如何控制呢?

3.2 74HC138译码器控制Y4

在这里插入图片描述
在这里插入图片描述
被选中的Y为0其他的都为1。
在这里插入图片描述
总结:Y4CY4控制并且相反所以Y4C被选中时为1,即可以透过相应的锁存器改变值,没选选中的就被锁存器锁住,不能改变相应的值。可以得出结论可以通过P25 P26 P27 选择相应的锁存器工作状态。下表是总结的开启相应锁存器的值。
这张表需要背下来:

功能YP27P26P25对应十六进制值
LEDY41000X80
数码管-位选Y61100XC0
数码管-段选Y71110XE0
外设(继电器、蜂鸣器)Y51010XA0
关闭0000X00

3.3、 锁存器打开关闭代码

在蓝桥杯单片机实训板上操作上述4个外设必须满足两个条件:

  1. 锁存器选通控制某个外设的通道
  2. P0被赋予相应的值。
    操作顺序:对P0赋值->打开锁存器->关闭锁存器
    方法一:
	P0=0XFF; //关闭所有LED灯
	P2=0X80; //打开锁存器控制LED灯
	P2=0; //关闭锁存器

方法二(严谨写法):

  P0=0xFF; //准备的数据
  P2=P2 & 0x1F | 0x80;//选择的锁存器
  /*
  解释:&0x1F : & 0001 1111 将前三位清0其他未不变 
       (&有0为0常用来位清零 需要清零位为0其他不变位为1)
        |0x80 : | 1000 0000 将第一位置1其他未不变 
       (1有1为1常用来位置1 需要置1位为1其他不变位为0)
  */
  P2 &= 0x1F; //关闭锁存器

4、点亮不同亮度的LED灯

4.1什么是PWM

调整PWM可以使LED呈现不同的亮度。

那什么是PWM呢?
简单来说就是让改变高电平占整个周期的时间来产生一个模拟的电压。即通过一个周期里改变占空比(高电频的时间比上整个周期的时间)的方式来改变输出的有效电压。比方说占空比为50%那就是高电平时间一半,低电平时间一半,在一定的频率下,就可以得到模拟的2.5V输出电压 那么75%的占空比 得到的电压就是3.75V。

在这里插入图片描述

4.2利用PWM输出不同亮度的LED灯

我们可以改变一个周期内LED灯亮的时间和灭的时间来改变等的亮度

    for(i=0;i<9;i++) //9个亮度等级
    {
      for(j=0;j<200;j++) //在这个亮度等级的情况下执行一定次数 让肉眼能感觉到
      {
        Led_Disp(0xFF); //点亮LED
        Delay(i); //亮的时间 单位ms
        Led_Disp(0x00);  //熄灭LED
        Delay(9-i); //灭的时间 单位ms
      }
    }

5、让LED显示不同亮度代码集合

VID20220925115022

这里用的是模块化编程思想,不同操作放在不同文件里,自己可以把这些组合起来放在一个程序文件里面。
main.c

void main()
{
  unsigned char i,j;
  init_close(); //关闭外设

  while(1)
  {
    for(i=0;i<9;i++) //9个亮度等级
    {
      for(j=0;j<200;j++) //在这个亮度等级的情况下执行一定次数
      {
        Led_Disp(0xFF);
        Delay(i); //亮的时间
        Led_Disp(0x00);  
        Delay(9-i); //灭的时间
      }
    }
  }
}

bsp_init.c

/**
  * @brief  关闭LED、蜂鸣器和继电器
  * @param  无     
  * @retval 无
  */
void init_close()
{
  // LED
  P0=0xFF; //准备的数据
  P2=P2 & 0x1F | 0x80;//选择的锁存器
  /*
  解释:&0x1F : & 0001 1111 将前三位清0其他未不变 
       (&有0为0常用来位清零 需要清零位为0其他不变位为1)
        |0x80 : | 1000 0000 将第一位置1其他未不变 
       (1有1为1常用来位置1 需要置1位为1其他不变位为0)
  */
  P2 &= 0x1F; //关闭锁存器
  
  //关闭外设
  P0=0x00; 
  P2=P2 & 0x1F | 0xA0 ;
  P2 &= 0x1F  ;
}

bsp_led.c

/**
  * @brief  LED显示函数
  * @param  data 要显示的数据 1亮0灭
  * @retval 无
  */
void Led_Disp(unsigned char leds)
{
  P0 = ~leds ;
  P2 = P2 & 0x1F | 0x80 ;
  P2 &= 0x1F;
}

delays.c

/**
  * @brief  延时函数
  * @param  ms 延时函数的ms数 0-65536
  * @retval 无
  */

void Delay(unsigned int ms)
{
	unsigned int i;
	while(ms--)
	for(i=0; i<628; i++);
}

三、外部中断

在这里插入图片描述
由原理图可知, 外部中断由P32P33引脚控制即S5对应外部中断0S4对应外部中断1,J5默认3和2想连接,即默认接地。

1、外部中断的初始化

Int.c

/**
  * @brief  外部中断初始化
  * @brief  初始化外部中断0和外部中断1,配置成下降沿触发
  * @param  无      
  * @retval 无
  */
void Int_init(void)
{
  IT0 = 1; //配置成下降沿触发
  EX0 = 1; //开启外部中断0
  
  IT1 = 1; //配置成下降沿触发
  EX1 = 1; //开启外部中断1
  
  EA = 1; //打开总中断
}

2、使用外部中断

main.c
方法一:if判断方法取反

unsigned char leds;

void main()
{

  init_close(); //关闭外设
  Int_init(); //初始化中断
  while(1) ;
}

void Int0_isr() interrupt 0
{
  if((leds & 0x01) == 0) //如果leds最后一位为0 注:&需要取的位为1常用来取一个字节的某一位
    leds = leds | 0x01; //把led最后一位置1 | 1 常用来置1
  else
    leds = leds & 0xFE; //把led最后一位清0 & 0 常用来清0
  
  Led_Disp(leds);
}

void Int1_isr() interrupt 2
{
   if((leds & 0x02) == 0) //如果leds倒数第二位为0 注:&需要取的位为1常用来取一个字节的某一位
    leds = leds | 0x02; //把leds倒数第二位 置1 | 1 常用来置1
  else
    leds = leds & 0xFD; //把led倒数第二位 清0 & 0 常用来清0
  
  Led_Disp(leds); 
}

方法二: 用异或的方法取反

void Int0_isr() interrupt 0
{
  leds ^= 0x01; //相同为0,相异为1. ^ 1 常用来位取反
  Led_Disp(leds);
}

void Int1_isr() interrupt 2
{
  leds ^= 0x02; 
  Led_Disp(leds);
}

四、定时器

在这里插入图片描述

1、定时器的初始化

定时器常用的初始化步骤:

  1. TOMD 确定工作方式
    在这里插入图片描述

  2. AUXR 选择1T(1)或12T(0,默认)

  3. TH1 TL1 TH0TL0 设置定时初值
    初值的计算方式:
    M为16位时65536 八位时256
    计数方式: X = M − 要求计数的值 X = M -要求计数的值 X=M要求计数的值
    12T方式: X = M − 要求定时的值 ∗ 系统时钟频率 / 12 X =M -要求定时的值 *系统时钟频率 /12 X=M要求定时的值系统时钟频率/12
    1T方式: X = 6 M − 系统时钟频率 X = 6M -系统时钟频率 X=6M系统时钟频率

  4. TF0=0 TF1=0 清除定时器标志

  5. R0 =1TR1 =1 定时器开始计时

  6. ET0=1 ET1=1使能定时器中断

  7. EA=1 开启总中断

代码如下:
Timer.c


/**
  * @brief  定时器1初始化函数
  * @brief  12T 16 位自动重载 设置定时时长为 1ms
  * @param    无
  * @retval   无
  */

void Timer1_init(void)		//1毫秒@12.000MHz
{
	AUXR &= 0xBF;		//定时器时钟12T模式
	TMOD &= 0x0F;		//设置定时器模式
	TL1 = 0x18;		//设置定时初值
	TH1 = 0xFC;		//设置定时初值
	TF1 = 0;		//清除TF1标志
	TR1 = 1;		//定时器1开始计时
	
	ET1 = 1;		//使能定时器1中断
  if(!EA) EA=1;
}


/**
  * @brief  定时器0初始化函数
  * @brief  12T 16 位自动重载 设置定时时长为 10ms
  * @param    无
  * @retval   无
  */

void Timer0_init(void)		//10毫秒@12.000MHz
{
	AUXR &= 0x7F;		//定时器时钟12T模式
	TMOD &= 0xF0;		//设置定时器模式
	TL0 = 0xF0;		//设置定时初值
	TH0 = 0xD8;		//设置定时初值
	TF0 = 0;		//清除TF0标志
	TR0 = 1;		//定时器0开始计时
	
	ET0 = 1;		//使能定时器1中断	
  if(!EA) EA=1;
}

2、定时器中断服务子函数

void Timer0_isr() interrupt 1
{
  
}

void Timer1_isr() interrupt 3
{
  
}

3、led灯高四位每一秒闪烁一次,低四位每两秒闪烁一次

VID20220926092106

注:只有核心代码部分,之前写过的文件不再加上。
main.c


bit timer1_flag = 0;//本变量为布尔变量,进入定时器中断变为1
bit timer0_flag = 0;//本变量为布尔变量,进入定时器中断变为1

unsigned int one_trig = 0;//1s计数
unsigned int two_trig = 0;
unsigned char leds= 0;//要显示数据

void main()
{
	init_close();//关闭系统外设
	Timer1_init();//定时器1初始化,并开启定时器1中断
	Timer0_init();//定时器0初始化,并开启定时器1中断
	
	while(1)
	{
	
		if(timer1_flag)	//定时器1,每1ms触发一次
		{
			timer1_flag = 0;//复位标志
			one_trig++;	//毫秒计数
		}
		if(one_trig>=1000)//到达1s
		{
			one_trig = 0;
			leds ^= 0XF0;//反转高4位
		}
		
		if(timer0_flag) //定时器0,每10ms触发
		{
			timer0_flag = 0;//复位标志
			two_trig++;	//毫秒计数
		}
		if(two_trig >= 200)//到达10s
		{
			two_trig = 0;
			leds ^= 0X0F;//反转低4位
		}
		
		Led_Disp(leds);//将要显示的内容显示出来

	}	
}
void Timer0_isr() interrupt 1
{
  timer0_flag = 1;
}

void Timer1_isr() interrupt 3
{
  timer1_flag = 1;
}

五、数码管

1 、数码管的显示函数

数码管显示需要三步

  1. 位选 (选择哪一位)
  2. 清屏 (清楚上一次显示内容)
  3. 段选 (输送断码值)
/**
  * @brief    数码管显示及函数
  * @param    seg_buf -- 显示断码值数组
  * @param    pos -- 要显示的位数
  * @retval   无
  */
void Seg_Disp(unsigned char *seg_buf, unsigned char pos)
{
	//位选
	P0 = 1<<pos; 
	P2 = P2 & 0x1F | 0xC0; 
	P2 &= 0x1F; 
	//清空屏幕
	P0 = 0xff;
	P2 = P2 & 0x1F | 0xE0; 
	P2 &= 0x1F; 
	//断码值
	P0 = seg_buf[pos];
	P2 = P2 & 0x1F | 0xE0; 
	P2 &= 0x1F; 
}

2、将字符串转换为断码值函数

大题思路就是将取出每一位选择断码值,如果有小数点再加上即可。


/**
  * @brief    字符串转换为断码值
  * @param    seg_string -- 需要转换的字符串
  * @param    seg_buf -- 保存的数组
  * @retval   无
  */
void Seg_Tran(unsigned char *seg_string, unsigned char *seg_buf)
{
	unsigned char i,j,temp;
	
	for(i=0,j=0;i<=7;i++,j++)
	{
		switch(seg_string[j])
		{
			case '0':	temp = 0xc0; break;			
			case '1':	temp = 0xf9; break;			
			case '2':	temp = 0xa4; break;			
			case '3':	temp = 0xb0; break;		
			case '4':	temp = 0x99; break;			
			case '5':	temp = 0x92; break;		
			case '6':	temp = 0x82; break;			
			case '7':	temp = 0xf8; break;		
			case '8':	temp = 0x80; break;			
			case '9':	temp = 0x90; break;		
					
			case 'A': temp = 0x88; break;
			case 'B': temp = 0x83; break;
			case 'C': temp = 0xc6; break;
			case 'D': temp = 0xA1; break;
			case 'E': temp = 0x86; break;
			case 'F': temp = 0x8E; break;
			
			case 'H': temp = 0x89; break;
			case 'L': temp = 0xC7; break;
			case 'N': temp = 0xC8; break;
			case 'P': temp = 0x8c; break;
			case 'U': temp = 0xC1; break;
			
			case '-': temp = 0xbf; break;
			case ' ': temp = 0xff; break;		
			
			default : temp = 0xff; break;
		
		}
	
		if(seg_string[j+1] == '.')
		{
			temp &= 0x7f;
			j++;
		}
		
		seg_buf[i] = temp;
	}
}

3、让数码管显示"HA–CPDD"

main.c

unsigned char i;
unsigned char buf[10];

void main()
{
  init_close();
  Seg_Tran("HA--CPDD",buf);
  while(1)
  {
    for(i=0;i<8;i++)  //快速选择每一位 通过视觉残留让人感觉同时显示
    {
      Seg_Disp(buf,i);
      Delay(1);
    }
  }
}

六、矩阵键盘与独立键盘

1、矩阵键盘与独立键盘的切换

这个开发板矩阵键盘和独立键盘在一起,通过跳线帽来选择。
在这里插入图片描述

2、矩阵键盘的扫描函数

每一次选择一列扫描行即可。
在这里插入图片描述

/**
  * @brief  读取矩阵键盘
  * @param  无
  * @retval 对应的按键编号
  */
unsigned char Keys_Scan(void)
{
	unsigned int Key_New;//0x0000 存放每一列
	unsigned char Key_Value;//对应的按键编号
	
	P44 = 0; P42 = 1; P35 = 1; P34 = 1;  // 第一列扫描
	Key_New = P3 & 0X0F;  //P3高四位清0 第四位赋值给 Key_New 

	P44 = 1; P42 = 0; P35 = 1; P34 = 1;  // 第二列扫描
	Key_New = (Key_New << 4) | (P3 & 0X0F); //将原来的数值挪到次4位,本次数值放到最低4位,占用了8位

	P44 = 1; P42 = 1; P35 = 0; P34 = 1;  // 第三列扫描
	Key_New = (Key_New << 4) | (P3 & 0X0F); //将原来的数值挪到次次4位,本次数值放到最低4位,占用了12位
	
	P44 = 1; P42 = 1; P35 = 1; P34 = 0;  // 第四列扫描
	Key_New = (Key_New << 4) | (P3 & 0X0F); //将原来的数值挪到次次4位,本次数值放到最低4位,占用了16位

	switch(~Key_New)//Key_Value的数值对应按键的编号
	{
		case 0x8000: Key_Value = 4; break;
		case 0x4000: Key_Value = 5; break;
		case 0x2000: Key_Value = 6; break;
		case 0x1000: Key_Value = 7; break;		
	
		case 0x0800: Key_Value = 8; break;
		case 0x0400: Key_Value = 9; break;
		case 0x0200: Key_Value = 10; break;
		case 0x0100: Key_Value = 11; break;		
	
		case 0x0080: Key_Value = 12; break;
		case 0x0040: Key_Value = 13; break;
		case 0x0020: Key_Value = 14; break;
		case 0x0010: Key_Value = 15; break;			
	
		case 0x0008: Key_Value = 16; break;
		case 0x0004: Key_Value = 17; break;
		case 0x0002: Key_Value = 18; break;
		case 0x0001: Key_Value = 19; break;			
	
		default : Key_Value = 0;
	}
	
	return Key_Value;
}

3、独立键盘的扫描函数

/**
  * @brief  读取按键键盘
  * @param  无
  * @retval 对应的按键编号
  */
unsigned char Key_Scan(void)
{

	unsigned char Key_Value;//返回值

	if(P30 == 0)  Key_Value = 7;
	else if(P31 == 0) Key_Value = 6;
	else if(P32 == 0) Key_Value = 5;	
	else if(P33 == 0) Key_Value = 4;	
	else Key_Value = 0;
	
	return Key_Value;
}

七、DS1302时钟芯片

原理图:
在这里插入图片描述
时序图:
在这里插入图片描述
寄存器:
在这里插入图片描述

1、DS1302发送数据组成部分

DS1302数据分为两部分:控制字节+数据字节(低位到高位)

1.1、DS1302控制字节

DS1302 的一条指令一个字节共 8 位
第 7 位(即最高位)固定为 1,这一位如果是0 的话,那写进去也是无效的。
第 6 位是选择 RAM 还是 CLOCK 的,一般我们使用 CLOCK 时钟,第 6位是 0。
第 5 到第 1 位,决定了寄存器的 5 位地址。
第 0 位是读写位,如果要写,这一位就是 0,如果要读,这一位就是 1。
在这里插入图片描述

1.2、DS1302数据字节

就是你要发送的数据。
在这里插入图片描述

2、单片机对DS1302发送和读取字节

这一部分可以配合时序图理解,这里我们先初始化设置

sbit SCK=P1^7;		
sbit SDA=P2^3;		
sbit RST = P1^3;   

//寄存器写入地址/指令定义
#define DS1302_SECOND		0x80
#define DS1302_MINUTE		0x82
#define DS1302_HOUR			0x84
#define DS1302_DATE			0x86
#define DS1302_MONTH		0x88
#define DS1302_DAY			0x8A
#define DS1302_YEAR			0x8C
#define DS1302_WP			0x8E

//时间数组,索引0~6分别为年、月、日、时、分、秒、星期
unsigned char DS1302_Time[]={19,11,16,12,59,55,6};

2.1、发送一个字节

void Write_Ds1302(unsigned  char temp) 
{
	unsigned char i;
	for (i=0;i<8;i++)     	
	{ 
		SCK=0;	//先拉低SCK保证之后SCK等于1时产生上升沿
		SDA=temp&0x01; //取最低位
		temp>>=1;  //向右移动一位,让次第位变为最低为
		SCK=1; //产生上升沿让数据发送出去
	}
}

2.2、写数据

void Write_Ds1302_Byte( unsigned char address,unsigned char dat )     
{
	//保证RST和SCK初始等于0
 	RST=0;	_nop_();
 	SCK=0;	_nop_();
 	//开始
 	RST=1; 	_nop_();  
 	//写控制字节
 	Write_Ds1302(address);	
 	//写数据字节
 	Write_Ds1302(dat);		
 	//关闭RST
 	RST=0; 
}

2.3、读数据

unsigned char Read_Ds1302_Byte ( unsigned char address )
{
 	unsigned char i,temp=0x00;
 	//保证RST和SCK初始等于0
 	RST=0;	_nop_();
 	SCK=0;	_nop_();
 	//开始
 	RST=1;	_nop_();
 	//写控制字节
 	Write_Ds1302(address); 
 	//读数据,保存temp中
 	for (i=0;i<8;i++) 	
 	{		
		SCK=0; //先拉低SCK保证之后SCK等于1时产生上升沿
		temp>>=1;	//因为每次temp写在最高位,所以右移让出最高位给后面写
 		if(SDA) temp|=0x80;	 //如果SDA==1将temp最高位置1
 		SCK=1; //表示收到数据
	} 
	
 	RST=0;	_nop_();
 	SCK=0;	_nop_();
	SCK=1;	_nop_();
	SDA=0;	_nop_();
	
	SDA=1;	_nop_();
	return temp;			
}

3、设置和读取DS1302时间

3.1、设置时间

/**
  * @brief  DS1302设置时间,调用之后,DS1302_Time数组的数字会被设置到DS1302中
  * @param  无
  * @retval 无
  */
void Set_Rtc(unsigned char* DS1302_Time)
{
	Write_Ds1302_Byte(DS1302_WP,0x00);
	
	//十进制转BCD码后写入
	Write_Ds1302_Byte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);
	Write_Ds1302_Byte(DS1302_MONTH,DS1302_Time[1]/10*16+DS1302_Time[1]%10);
	Write_Ds1302_Byte(DS1302_DATE,DS1302_Time[2]/10*16+DS1302_Time[2]%10);
	Write_Ds1302_Byte(DS1302_HOUR,DS1302_Time[3]/10*16+DS1302_Time[3]%10);
	Write_Ds1302_Byte(DS1302_MINUTE,DS1302_Time[4]/10*16+DS1302_Time[4]%10);
	Write_Ds1302_Byte(DS1302_SECOND,DS1302_Time[5]/10*16+DS1302_Time[5]%10);
	Write_Ds1302_Byte(DS1302_DAY,DS1302_Time[6]/10*16+DS1302_Time[6]%10);
	
	Write_Ds1302_Byte(DS1302_WP,0x80);
}

3.2、读取时间

/**
  * @brief  DS1302读取时间,调用之后,DS1302中的数据会被读取到DS1302_Time数组中
  * @param  无
  * @retval 无
  */
void DS1302_ReadTime(void)
{
	unsigned char Temp;
	//BCD码转十进制后读取
	Temp=Read_Ds1302_Byte(DS1302_YEAR);
	DS1302_Time[0]=Temp/16*10+Temp%16;
	Temp=Read_Ds1302_Byte(DS1302_MONTH);
	DS1302_Time[1]=Temp/16*10+Temp%16;
	Temp=Read_Ds1302_Byte(DS1302_DATE);
	DS1302_Time[2]=Temp/16*10+Temp%16;
	Temp=Read_Ds1302_Byte(DS1302_HOUR);
	DS1302_Time[3]=Temp/16*10+Temp%16;
	Temp=Read_Ds1302_Byte(DS1302_MINUTE);
	DS1302_Time[4]=Temp/16*10+Temp%16;
	Temp=Read_Ds1302_Byte(DS1302_SECOND);
	DS1302_Time[5]=Temp/16*10+Temp%16;
	Temp=Read_Ds1302_Byte(DS1302_DAY);
	DS1302_Time[6]=Temp/16*10+Temp%16;
}

八、DS18b20温度传感器

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1、写时序和读时序

在这里插入图片描述

1.1、写时序

由两种写时序:写 1 时序和写 0 时序。总线控制器通过写 1 时序写逻辑 1 到
DS18B20,写 0 时序写逻辑 0 到 DS18B20。所有写时序必须最少持续 60us,包括
两个写周期之间至少 1us 的恢复时间。当总线控制器把数据线从逻辑高电平拉到 低电平的时候,写时序开始(见图 14)。总线控制器要生产一个写时序,必须把数据线拉到低电平然后释放在写时序开始后的 15us 释放总线。当总线被释放的时候,5K 的上拉电阻将拉高总线。总控制器要生成一个写 0 时序,必须把数据线拉到低电平并持续保持(至少 60us)。总线控制器初始化写时序后,DS18B20 在一个 15us 到 60us 的窗口内对 I/O 线采 样。如果线上是高电平,就是写 1。如果线上是低电平,就是写 0

void Write_DS18B20(unsigned char dat)
{
	unsigned char i;
	for(i=0;i<8;i++)
	{
		DQ = 0; //先拉低
		DQ = dat&0x01; //开始写 取最低位
		Delay_OneWire(5); //等待读完
		DQ = 1; //释放总线
		dat >>= 1; //把次第位移至最低位
	}
	Delay_OneWire(5); //一个字节写完再延时一会
}

1.2、读时序

总线控制器发起读时序时,DS18B20 仅被用来传输数据给控制器。因此,总线控 制器在发出读暂存器指令[BEh]或读电源模式指令[B4H]后必须立刻开始读时序,DS18B20可以提供请求信息。除此之外,总线控制器在发出发送温度转换指令[44h]或召回 EEPROM 指令[B8h]之后读时序,详见 DS18B20 功能指令节。 所有读时序必须最少 60us,包括两个读周期间至少 1us 的恢复时间。当总线控制器把数据线从高电平拉到低电平时,读时序开始,数据线必须至少保持 1us,然 后总线被释放(见图 14)。在总线控制器发出读时序后,DS18B20 通过拉高或拉低总线上来传输 1 或 0。当传输逻辑 0 结束后,总线将被释放,通过上拉电阻回 到上升沿状态。从 DS18B20 输出的数据在读时序的下降沿出现后 15us 内有效。因此,总线控制器在读时序开始后必须停止把 I/O 脚驱动为低电平 15us,以读取I/O 脚状态

unsigned char Read_DS18B20(void)
{
	unsigned char i;
	unsigned char dat;
  
	for(i=0;i<8;i++)
	{
		DQ = 0; //拉低
		dat >>= 1;
		DQ = 1; //释放
		if(DQ){dat |= 0x80;}	//保存数据    
		Delay_OneWire(5); //延迟一段时间
	}
	return dat;
}

2、操作步骤

2.1、 初始化

在初始化序列期间,总线控制器拉低总线并保持 480us 以发出(TX)一个复位脉 冲,然后释放总线,进入接收状态(RX)。单总线由 5K 上拉电阻拉到高电平。当DS18B20 探测到 I/O 引脚上的上升沿后,等待 15-60us,然后发出一个由 60-240us低电平信号构成的存在脉冲。
在这里插入图片描述

bit init_ds18b20(void)
{
  	bit initflag = 0;
  	
  	DQ = 1; //释放总线
  	Delay_OneWire(12); //保持一端时间
  	DQ = 0; //拉低总线
  	Delay_OneWire(80); //延时大于480us
  	DQ = 1; //释放总线
  	Delay_OneWire(10);  //等待一段时间
    initflag = DQ;    //读取DQ
  	Delay_OneWire(5);
  
  	return initflag; //0表示初始化成功
}

2.2. ROM操作指令

在这里插入图片描述

2.3. DS18B20功能指令

在这里插入图片描述 ### 2.4. DS18B20 读温度

unsigned int Read_Temp(void)
{
	unsigned char low,high;
	init_ds18b20();
	Write_DS18B20(0xcc); //跳过ROM
	Write_DS18B20(0x44);

	init_ds18b20();
	Write_DS18B20(0xcc); //跳过ROM
	Write_DS18B20(0xbe); //读暂存器
	low = Read_DS18B20();
	high = Read_DS18B20();

	return (high<<8)+low; //构成16位数据
	
}

九、PCF8951 DACDAC原理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1 、器件总地址

PCF8591 采用典型的 I2C 总线接口器件寻址方法,即总线地址由器件地址引脚地址方向位组成。飞利蒲公司规定 A/D 器件地址为 1001。引脚地址为 A2A1A0,其值由用户选择,因此 I2C 系统中最多可接 23=8 个具有 I2C 总线接口的 A/D 器件。地址的最后一位为方向位 R/ w ,当主控器对A/D 器件进行读操作时为 1进行写操作时为 0。总线操作时,由器件地址、引脚地址和方向位组成的从地址为主控器发送的第一字节。
在这里插入图片描述

2、 控制字节

控制字节用于实现器件的各种功能,如模拟信号由哪几个通道输入等。控制字节存放在控制寄存器中。总线操作时为主控器发送的第二字节。其格式如下所示:

其中:
D1、D0 两位是 A/D 通道编号:00 通道 0,01 通道 1,10 通道 2,11 通道 3
D2 自动增益选择(有效位为 1)
D5、D4 模拟量输入选择:00 为四路单数入、01 为三路差分输入、10 为单端与差分配合输入、11 为模拟输出允许有效
D6:模拟输出允许(1有效)

在这里插入图片描述

3、IIC通信

I2C总线是不同的IC或模块之间的双向两线通信。这两条线是串行数据线(SDA)和串行时钟线(SCL)。这两条线必须通过上拉电路连接至正电源。数据传输只能在总线不忙时启动

3.1、数据信息

一个数据位在每一个时钟脉冲期间传输。SDA线上的数据必须在时钟脉冲的高电压期间保持稳定这个期间数据线上的改变将被当作控制信号在这里插入图片描述
发送一个字节:

void IIC_SendByte(unsigned char byt)
{
    unsigned char i;

    for(i=0; i<8; i++)
    {
        SCL  = 0;
        IIC_Delay(DELAY_TIME);
        if(byt & 0x80) SDA  = 1;
        else SDA  = 0;
        IIC_Delay(DELAY_TIME);
        SCL = 1;
        byt <<= 1;
        IIC_Delay(DELAY_TIME);
    }
    SCL  = 0;  
}

接收一个字节:

unsigned char IIC_RecByte(void)
{
    unsigned char i, da;
    for(i=0; i<8; i++)
    {   
    	SCL = 1;
	IIC_Delay(DELAY_TIME);
	da <<= 1;
	if(SDA) da |= 1;
	SCL = 0;
	IIC_Delay(DELAY_TIME);
    }
    return da;    
}

3.2、开始信号和停止信号

数据和时钟线在总不忙时保持高电平。在时钟为高电平时,数据线上的一个由高到低的变化被定义为开始条件。时钟为高电平时,数据线上的一个由低到高的变化被定义为停止条件。
在这里插入图片描述
开始

void IIC_Start(void)
{
//起始同时为高电平
    SDA = 1;
    SCL = 1;
    //一段时间后
    IIC_Delay(DELAY_TIME);
    //拉低SDA
    SDA = 0;
    //再等待一段时间按
    IIC_Delay(DELAY_TIME);
    //拉低SCL
    SCL = 0;	
}

停止

void IIC_Stop(void)
{
	//其实SDA为低,SCL为高
    SDA = 0;
    SCL = 1;
    IIC_Delay(DELAY_TIME);
    SDA = 1;
    IIC_Delay(DELAY_TIME);
}

3.3、应答

在开始和停止条件之间从发送机传输到接收机的数据字节数是没有限制的。每个8位数据字节之后紧跟着一个应答位。应答位是由发送机放在总线的一个高电平,而主机也产生一个额外的与应答有关的时钟脉冲地址匹配的从接收机必须在接收每个字节后产生一个应答。然而主机在接收到每个已经被从发送机终止的字节后必须产生一个应答。在应答时钟脉冲期间,应答的器件必须将SDA线拉低,因此在应答相应的时钟脉冲的高电平期间,SDA线必须保持稳定的低电平。在由从机终止的最后一个字节主接收机必须通过产生一个低电平应答向发送机发出一个数据结束信号,这样发送机必须将数据线拉高以允许主机产生停止条件
在这里插入图片描述
发送应答:

void IIC_SendAck(bit ackbit)
{
    SCL = 0;
    SDA = ackbit;  					
    IIC_Delay(DELAY_TIME);
    SCL = 1;
    IIC_Delay(DELAY_TIME);
    SCL = 0; 
    SDA = 1;
    IIC_Delay(DELAY_TIME);
}

接收应答:

 bit IIC_WaitAck(void)
{
    bit ackbit;
	
    SCL  = 1;
    IIC_Delay(DELAY_TIME);
    ackbit = SDA;
    SCL = 0;
    IIC_Delay(DELAY_TIME);
    return ackbit;
}

3.4、空闲状态

I2C总线总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高

3.5、写模式的总线协议

在这里插入图片描述

3.6、写模式的总线协议

在这里插入图片描述

4、总代码

4.1、ADC

unsigned char Pcf8591_Adc(unsigned char channel_num_contrl)
{
	unsigned char temp;
	//写操作
	IIC_Start();//发送开启信号
	IIC_SendByte(0x90);//选择PCF8591芯片,确定写的模式
	IIC_WaitAck();//等待PCF8591反馈
	
	IIC_SendByte(channel_num_contrl);//确定要转换的通道(顺便,使能DA转换)
	IIC_WaitAck();//等待PCF8591反馈	
	//读操作
	IIC_Start();//发送开启信号
	IIC_SendByte(0x91);//选择PCF8591芯片,确定读的模式
	IIC_WaitAck();//等待PCF8591反馈	
		
	temp = IIC_RecByte();//接收数据
	IIC_SendAck(1);//选择不应答
	IIC_Stop();//停止发送
	
	return temp;

}

4.2、DAC

void Pcf8591_Dac(unsigned char trans_dat)
{
	IIC_Start();//发送开启信号
	IIC_SendByte(0x90);//选择PCF8591芯片,确定写的模式
	IIC_WaitAck();//等待PCF8591反馈
	
	IIC_SendByte(0x41);//使能DA转换(随便写通道编号,不影响,主要的功能是使能DA)
	IIC_WaitAck();//等待PCF8591反馈		

	IIC_SendByte(trans_dat);//将待转换的数据发送出去
	IIC_WaitAck();//等待PCF8591反馈	
	IIC_Stop();//停止发送	

}

十、EEPROM-AT24C02

24C01/02/04/08/16是低工作电压的1K/2K/4K/8K/16K 位串行电可擦除只读存储器,内部组织为128/256/512/1024/2048 个字节,每个字节8位,该芯片被广泛应用于低电压及低功耗的工商业领域。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1、写操作

1.1、写一个字节

AT24C02存储器写操作需要在给出开始态器件地址确认之后,紧跟着给出一个8位数据地址。一经收到该地址,EEPROM就通过 SDA发出确认信号并随时钟输入8位数据。在收到8位数据之后,EEPROM 将向SDA确认,数据传送设备必须用停止状态来终止写操作,这时,EEPROM进入一个内计时固定存储器写入周期在该写周期时,所有输入被禁止,EEPROM直到写完后才应答,写字节时序如图所示。
中文版:
在这里插入图片描述
英文版:在这里插入图片描述

1.2、写页面

1KB/2KB EEPROM能进行8字节页面写入,4KB、8KB和16KB设备能进行16字节页面写入。
激发写页面与激发写字节相同,只是数据传送设备无须在第一个字节随时钟输入之后,发出一个停止状态。在 EEPROM确认收到第一个数据之后,数据传送设备能再传送7个(1KB、2KB)或15个(4KB、8KB、16KB)数据每一个数据收到之后,EEPROM都将通过SDA回送一个确认信号,最后数据传送设备必须通过停止状态终止页面写序列

英文版:
在这里插入图片描述
中文版
在这里插入图片描述

2、读操作

除了器件地址码中读/写选择位置1,读操作与写操作是一样的。读操作有三种:立即地址读取随机地址读取顺序读取

2.1、立即地址读取

内部数据字地址指针保持在读写操作中最后访问的地址按“1”递增只要芯片保持上电,该地址在两个操作之间一直有效,如果最后一个操作是在地址n处读取,则立即地址是n+1;如果最后操作是在地址n处写入,则立即地址也是n+1。
在这里插入图片描述

2.1、随机地址读取

随机读取需要一个空字节写序列来载入数据地址一旦器件地址码数据码地址码时钟输入,并被EEPROM确认,数据传送设备就必须产生另一个开始条件。读/写选择位处于高电平时,通过送出一个器件地址,数据传送设备激发出一个立即寻址读取,EEPROM确认器件地址,并随时钟串行输出数据。器件读数据不通过确认(使SDA总线处于高电平)应答,而通过产生一个停止条件应答。
在这里插入图片描述

3.1、顺序地址读取

顺序读取由立即地址读取或随机地址读取激发,在读数据器件收到一数据码之后,通过确认应答,只要EEPROM收到确认之后,便会继续增加数据码地址及串行输出数据码。当达到存储器地址极限时,数据码地址将重复滚动,顺序读取将继续。当读数据器件不通过确认(使SDA总线处于高电平)应答,而通过产生一个停止条件应答时,顺序读取操作被终止。
在这里插入图片描述

3、读写代码

3.1写EEPROM函数

//函数名:
//入口参数:需要写入的字符串,写入的地址(务必为8的倍数),写入数量
//返回值:无
//函数功能:向EERPOM的某个地址写入字符串中特定数量的字符。
void EEPROM_Write(unsigned char* EEPROM_String, unsigned char addr, unsigned char num)
{
	IIC_Start();//发送开启信号
	IIC_SendByte(0xA0);//选择EEPROM芯片,确定写的模式
	IIC_WaitAck();//等待EEPROM反馈
	
	IIC_SendByte(addr);//写入要存储的数据地址
	IIC_WaitAck();//等待EEPROM反馈		

	while(num--)
	{
		IIC_SendByte(*EEPROM_String++);//将要写入的信息写入
		IIC_WaitAck();//等待EEPROM反馈		
		IIC_Delay(200);	
	}
	IIC_Stop();//停止发送	
}

3.2读EEPROM函数

//函数名:读EEPROM函数
//入口参数:读到的数据需要存储的字符串,读取的地址(务必为8的倍数),读取的数量
//返回值:无
//函数功能:读取EERPOM的某个地址中的数据,并存放在字符串数组中。
void EEPROM_Read(unsigned char* EEPROM_String, unsigned char addr, unsigned char num)
{
	IIC_Start();//发送开启信号
	IIC_SendByte(0xA0);//选择EEPROM芯片,确定写的模式
	IIC_WaitAck();//等待EEPROM反馈
	
	IIC_SendByte(addr);//写入要读取的数据地址
	IIC_WaitAck();//等待EEPROM反馈		

	IIC_Start();//发送开启信号
	IIC_SendByte(0xA1);//选择EEPROM芯片,确定读的模式
	IIC_WaitAck();//等待EEPROM反馈	
	
	while(num--)
	{
		*EEPROM_String++ = IIC_RecByte();//将要写入的信息写入
		if(num) IIC_SendAck(0);//发送应答
			else IIC_SendAck(1);//不应答
	}
	
	IIC_Stop();//停止发送	
}
;