第十六集 DS18B20测温 学习笔记
一. DS18B20简介
DS18B20是常用的数字温度传感器,其输出的是数字信号,具有体积小,硬件开销低,抗干扰能力强,精度高的特点。DS18B20数字温度传感器接线方便,封装成后可应用于多种场合,如管道式,螺纹式,磁铁吸附式,不锈钢封装式,型号多种多样。主要根据应用场合的不同而改变其外观。封装后的DS18B20可用于电缆沟测温,高炉水循环测温,锅炉测温,机房测温,农业大棚测温,洁净室测温,弹药库测温等各种非极限温度场合。耐磨耐碰,体积小,使用方便,封装形式多样,适用于各种狭小空间设备数字测温和控制领域
①、独特的单线接口方式,DS18B20在与微处理器连接时仅需要一条口线即可实现微处理器与DS18B20的双向通讯。
②、测温范围 -55℃~+125℃,固有测温误差1℃。
③、支持多点组网功能,多个DS18B20可以并联在唯一的三线上,最多只能并联8个,实现多点测温,如果数量过多,会使供电电源电压过低,从而造成信号传输的不稳定。
④、工作电源: 3.0~5.5V/DC (可以数据线寄生电源)
⑤、在使用中不需要任何外围元件
⑥、测量结果以9~12位数字量方式串行传送
二.硬件连接
用1k的上拉电阻
三.功能描述
正数补码:本身不变
负数补码:按位取反再+1,加个-号
四.代码编写
1)复位和存在
复位(输出0保持480us,输出1保持60us,读取当前电平,延时420us)
2)写0,写1
写0(输出0保持60us+,输出1保持1us+)
写1(输出0保持1us+,输出1保持60us+)
3)读0,读1
读0/1(输出0保持1us+,输出1保持1us+,读取当前电平,延时60us)
4.1 底层驱动
复位(输出0保持480us,输出1保持60us,读取当前电平,延时420us)
写0(输出0保持60us+,输出1保持1us+)
写1(输出0保持1us+,输出1保持60us+)
读0/1(输出0保持1us+,输出1保持1us+,读取当前电平,延时60us)
4.2 接口函数
写1字节(先输出低位,在输出高位)
读1字节(先读到的是低位,后读到的是高位
4.3 用户功能函数
温度读取换算函数
(复位-CCH-44H-等待-复位-CCH-BEH-读取2字节温度数据-换算)
在main.c中写入
#include "18b20.h"
SEG_Show_U32(Temp_18b20); //在USB初始化后
生成新文件18B20.c,并写入
#include "18b20.h"
u8MinusFlag = 0; //如果等于0 ,正数;等于1,负数
u32 Temp_18b20; //最终的温度,0.0625
void Delay480us(void) //@24.000MHz
{
unsigned long edata i;
_nop_();
_nop_();
_nop_();
i = 2878UL;
while (i) i--;
}
void Delay60us(void) //@24.000MHz
{
unsigned long edata i;
_nop_();
_nop_();
_nop_();
i = 358UL;
while (i) i--;
}
void Delay1us(void) //@24.000MHz
{
unsigned long edata i;
_nop_();
_nop_();
_nop_();
i = 4UL;
while (i) i--;
}
//复位(输出0保持480us,输出1保持60us,读取当前电平,延时420us)
void DS18B20_Reset(void)
{
u8 flag = 1;
while( flag ) //只要括号里的变量大于0,就会一直执行
{
DQ = 0;
Delay480us();
DQ = 1;
Delay60us();
flag = DQ; //设备存在,会拉低总线
Delay480us();
}
}
//写逻辑0(输出0保持60us+,输出1保持1us+)
void DS18B20_Write_0(void)
{
DQ = 0;
Delay60us();
DQ = 1;
Delay1us();
Delay1us();
}
//写逻辑1(输出0保持1us+,输出1保持60us+)
void DS18B20_Write_1(void)
{
DQ = 0;
Delay1us();
Delay1us();
DQ = 1;
Delay60us();
}
//读逻辑0/1(输出0保持1us+,输出1保持1us+,读取当前电平,延时60us)
bit DS18B20_Read(void)
{
bit state = 0;
DQ = 0;
Delay1us();
Delay1us();
DQ = 1;
Delay1us();
Delay1us();
state = DQ; //暂时保存这个DQ的数值
Delay60us();
return state;
}
//写1字节(先输出低位,在输出高位)
void DS18B20_WriteByte( u8 dat )
{
u8 i;
for(i=0;i<8;i++)
{
if( dat & 0x01 ) //最低位是1.发逻辑1电平
{
DS18B20_Write_1();
}
else //否则.发逻辑0电平
{
DS18B20_Write_0();
}
dat >>= 1; //dat = (dat>>1);
}
}
//读1字节(先读到的是低位,后读到的是高位)
u8 DS18B20_ReadByte(void )
{
u8 i;
u8 dat=0; //数据暂存,
for(i=0;i<8;i++) //循环读取八次
{
dat >>= 1;
if( DS18B20_Read() ) //如果读回来的是逻辑1 //0000 0000->1000 0000 -> 0100 0000
{
dat |= 0x80;
}
else
{
}
}
return dat;
}
//复位-CCH-44H-等待-复位-CCH-BEH-读取2字节温度数据-换算)
void DS18B20_ReadTemp(void)
{
u8 TempH = 0;
u8 TempL = 0;
u16 Temp = 0;
//---------------------发送检测命令---------------------
DS18B20_Reset(); //1.发送复位命令
DS18B20_WriteByte(0xcc); //2.跳过ROM命令
DS18B20_WriteByte(0x44); //3.开始转化命令
while( !DQ ); //4.等待这个引脚变成高电平
//---------------------发送读取命令---------------------
DS18B20_Reset(); //1.发送复位命令
DS18B20_WriteByte(0xcc); //2.跳过ROM命令
DS18B20_WriteByte(0xBE); //3.开始转化命令
TempL = DS18B20_ReadByte(); //读取低字节温度
TempH = DS18B20_ReadByte(); //读取高字节温度
if( TempH & 0x80 ) //如果最高位是1,这个就是负数
{
MinusFlag = 1;
Temp = (((u16)TempH << 8) | ((u16)TempL << 0));
Temp = (~Temp) +1;
Temp_18b20 = (u32)Temp*625;
}
else
{
MinusFlag = 0;
Temp = (((u16)TempH << 8) | ((u16)TempL << 0));
Temp_18b20 = (u32)Temp*625;
}
}
在18B20.h中写入
#ifndef __18B20_H
#define __18B20_H
#include "config.h" //调用头文件
#define DQP33
extern u8MinusFlag ; //如果等于0 ,正数;等于1,负数
extern u32 Temp_18b20; //最终的温度,放大了10000倍
void DS18B20_ReadTemp(void);
#endif
在task.c中写入两个任务
#include "18b20.h"
{0, 800, 800, DS18B20_ReadTemp}, //温度读取
{0,1, 1, SEG_Task}, //显示数码管
在io.c中写入
void SEG_Show_U32(u32 num)
{
u8 i;
for(i=0;i<8;i++)
{
passward = num%10;
num /= 10;
}
}
在io.h中写入
void SEG_Show_U32(u32 num);
至此,18B20的读取与显示就完成了
课后小练
测温计:
1.按下开关机按钮开机
2.开机后显示三个横杠和单位 “- - - C”/“- - - F”
3.在2秒内显示出当前温度
4.有一个按键可以切换摄氏和华氏
5.按下开关机按钮后关机,或者无操作30秒自动关机
第十七集 串口的简单应用 学习笔记
一.串口通信的基础知识
通信指设备之间通过一定的协议进行的信息交换
串口1/2和3/4的主要区别就在工作模式和工作方式上,这节课主要讲异步串口。
每次发送一位数据的称为串行通信,多位一起传输的称为并行通信。
今天要讲的串口通信是串行通信的其中的一种!
①异步串口通信不需要统一的时钟信号,每个数据字节的传输都是独立的。所以需要发送和接收设备的时钟比较接近
②每个数据字节通常会被起始位和停止位包围,起始位用于通知接收端数据传输的开始,停止位用于标志数据字节的结束。
③由于没有统一的时钟信号,异步串口的传输速率相对较低,且需要额外的起始位和停止位,因此效率较低
①同步串口通信依赖于一个统一的时钟信号来同步发送端和接收端的数据传输。
②发送端和接收端共享同一个时钟信号,或者通过某种方式(如曼彻斯特编码)在数据流中嵌入时钟信息。
③由于有时钟信号的同步,数据传输的速率可以较高,且不需要起始位和停止位来界定每个数据字节,因此效率较高
二.串口通信的寄存器配置
全双工,波特率,数据位,校验位,停止位
传输距离:RS232,15米 / RS422,1200米 /RS485(半双工)1200米
传输速度:20k/s 10M/s 10M/s
串口转以太网,WIFI,蓝牙,zigbee
三.串口通信的硬件连接
四.使用ISP软件自动生成串口初始化代码
在main.c中
#include “usart.h”
Uart2_Init(); //在EA = 1;之前,串口2初始化
生成usart.c,在其中输入(ISP生成的)
#include “usart.h”
#include “io.h”
u8 Rec_Dat; //接收缓冲区
u8 Rec_Num = 0; //接收计数
bit B_TX2_Busy = 0;
void Uart2_Isr(void) interrupt 8
{
if (S2CON & 0x02) //检测串口2发送中断
{
S2CON &= ~0x02; //清除串口2发送中断请求位
B_TX2_Busy = 0;
}
if (S2CON & 0x01) //检测串口2接收中断
{
S2CON &= ~0x01; //清除串口2接收中断请求位
Rec_Dat = S2BUF;
}
}
void Uart2_Init(void) //9600bps@24.000MHz
{
P_SW2 |= 0x01; //功能脚切换,UART2/USART2: RxD2(P4.2), TxD2(P4.3)
S2CON = 0x50; //8位数据,可变波特率
AUXR |= 0x04; //定时器时钟1T模式
T2L = 0x8F; //设置定时初始值
T2H = 0xFD; //设置定时初始值
AUXR |= 0x10; //定时器2开始计时
IE2 |= 0x01; //使能串口2中断
}
void Uart2_SendStr( u8 *puts ) //串口数据发送函数
{
for (; *puts != 0;puts++) //遇到停止符0结束
{
S2BUF = *puts;
B_TX2_Busy = 1;
while(B_TX2_Busy);
}
}
void Usart2_RunTask(void)
{
if( Rec_Num >= 6 ) //是否接收到了6位以上的数据
{
if(( Rec_Dat == '\n' ) && ( Rec_Dat == '\r' ) ) //末尾判断
{ //1.发送OPEN\r\n打开数码管,数码管显示“- - - -” 11 22 OPEN\r\n
if( ( Rec_Dat == 'O' ) &&
( Rec_Dat == 'P' ) &&
( Rec_Dat == 'E' ) &&
( Rec_Dat == 'N' ) )
{
passward = 16;
passward = 16;
passward = 16;
passward = 16;
Uart2_SendStr( "打开成功!\r\n" );
}
//2.发送CLOSE\r\n打开数码管,数码管全部熄灭
else if( ( Rec_Dat == 'C' ) &&
( Rec_Dat == 'L' ) &&
( Rec_Dat == 'O' ) &&
( Rec_Dat == 'S' ) &&
( Rec_Dat == 'E' ) )
{
passward = 17;
passward = 17;
passward = 17;
passward = 17;
Uart2_SendStr( "关闭成功!\r\n" );
}
//3.再打开的情况下,串口发送DAT+123\r\n,数码管显示数值“123”
else if( ( Rec_Dat == 'D' ) &&
( Rec_Dat == 'A' ) &&
( Rec_Dat == 'T' ) &&
( Rec_Dat == '+' ) &&
( Rec_Dat == '1' ) &&
( Rec_Dat == '2' ) &&
( Rec_Dat == '3' ) )
{
passward = 17;
passward = 1;
passward = 2;
passward = 3;
}
Rec_Num = 0;
}
}
}
生成usart.h,在其中输入
#ifndef __USART_H
#define __USART_H
#include "config.h" //调用头文件
void Uart2_Init(void); //9600bps@24.000MHz
void Usart2_RunTask(void);
#endif
在Task.c中增加1个任务
#include “usart.h”
{0, 1, 1, Usart2_RunTask}, //串口2运行任务
至此,任务1,任务2,任务3,课后小练都已完成
课后小练
智能数码管显示屏
1.发送OPEN\r\n打开数码管,数码管显示“- - - -”
2.发送CLOSE\r\n打开数码管,数码管全部熄灭
3.再打开的情况下,串口发送DAT+123\r\n,数码管显示数值“123”
(前面已完成)
第十八集 串口高级应用 学习笔记
一.串口通信的奇偶校验
最常用的通信格式为8-N-1(1包为10位数据)
8:代表数据位为8位
N:代表None,无校验 (ODD 奇 ;EVEN 偶)
1:代表1位停止位
偶校验(even parity):让传输的数据(包含校验位)中1的个数为偶数。真0,假1。
即:如果传输字节中1的个数是偶数,则校验位为“0”,奇数相反。
奇校验(odd parity):让传输的数据(包含校验位)中1的个数为奇数。真0,假1。
即:如果传输字节中1的个数是奇数,则校验位为“0”,偶数相反。
为什么需要加校验呢?
传输过程是单向的,可能存在出错的可能!
奇偶校验的优缺点?
优点1:可以减少数据出错的可能
优点2:使用简单便捷
缺点1:奇偶校验的检错率只有50%,因为只有奇数个数据位发生变化能检测到,如果偶数个数据位发生变化则无能为力了
缺点2:奇偶校验每传输一个字节都需要加一位校验位,对传输效率影响很大
程序如何实现?使用9位数据位(8位数据+1位校验)
方案1:可以利用二进制数相加的特点:
0+0=0、1+0=1、1+1=0
可以看出,如果我们将一个字节的所有位相加
• 有奇数个“1”的字节的和为1
• 有偶数个“1”的字节的和为0
方案2:可以利用ACC(累加器)和P(奇偶校验位)
• 将需要运算的数值存入ACC寄存器
• 打印读取P位用来表示结果中“1”的个数是奇数还是偶数。结果等于1为奇数!
• ACC = dat;
• if(P) //奇数个1
• else //偶数个1
任务1:使用奇校验,8位数据位,1位停止位的数据,发送OPEN\r\n打开数码管,数码管显示“- - - -“
在main.c中
#include “usart.h”
Uart2_Init(); //在EA = 1;之前,串口2初始化
生成usart.c,在其中输入(ISP生成的)
#include “usart.h”
#include “io.h”
u8 Rec_Dat; //接收缓冲区
u8 Rec_Num = 0; //接收计数
bit B_TX2_Busy = 0;
void Uart2_Isr(void) interrupt 8
{
u8 dat;
if (S2CON & 0x02) //检测串口2发送中断
{
S2CON &= ~0x02; //清除串口2发送中断请求位
B_TX2_Busy = 0;
}
if (S2CON & 0x01) //检测串口2接收中断
{
S2CON &= ~0x01; //清除串口2接收中断请求位
dat = S2BUF;
ACC = dat;
if(S2RB8) = (!P) ) //奇校验
{
Rec_Dat = dat;
}
}
}
void Uart2_Init(void) //9600bps@24.000MHz
{
P_SW2 |= 0x01; //功能脚切换,UART2/USART2: RxD2(P4.2), TxD2(P4.3)
S2CON = 0xD0; //9位数据,可变波特率
AUXR |= 0x04; //定时器时钟1T模式
T2L = 0x8F; //设置定时初始值
T2H = 0xFD; //设置定时初始值
AUXR |= 0x10; //定时器2开始计时
IE2 |= 0x01; //使能串口2中断
}
void Uart2_SendStr( u8 *puts ) //串口数据发送函数
{
u8 dat;
for (; *puts != 0;puts++) //遇到停止符0结束
{
dat = *puts;
ACC = dat;
if(P)
S2RB8 = 0;
else
S2RB8 = 1;
S2BUF = *puts;
B_TX2_Busy = 1;
while(B_TX2_Busy);
}
}
void Usart2_RunTask(void)
{
if( Rec_Num >= 6 ) //是否接收到了6位以上的数据
{
if(( Rec_Dat == '\n' ) && ( Rec_Dat == '\r' ) ) //末尾判断
{
//1.发送OPEN\r\n打开数码管,数码管显示“- - - -” 11 22 OPEN\r\n
if( ( Rec_Dat == 'O' ) &&
( Rec_Dat == 'P' ) &&
( Rec_Dat == 'E' ) &&
( Rec_Dat == 'N' ) )
{
passward = 16;
passward = 16;
passward = 16;
passward = 16;
Uart2_SendStr( "打开成功!\r\n" );
}
//2.发送CLOSE\r\n打开数码管,数码管全部熄灭
else if( ( Rec_Dat == 'C' ) &&
( Rec_Dat == 'L' ) &&
( Rec_Dat == 'O' ) &&
( Rec_Dat == 'S' ) &&
( Rec_Dat == 'E' ) )
{
passward = 17;
passward = 17;
passward = 17;
passward = 17;
Uart2_SendStr( "关闭成功!\r\n" );
}
//3.再打开的情况下,串口发送DAT+123\r\n,数码管显示数值“123”
else if( ( Rec_Dat == 'D' ) &&
( Rec_Dat == 'A' ) &&
( Rec_Dat == 'T' ) &&
( Rec_Dat == '+' ) &&
( Rec_Dat == '1' ) &&
( Rec_Dat == '2' ) &&
( Rec_Dat == '3' ) )
{
passward = 17;
passward = 1;
passward = 2;
passward = 3;
}
Rec_Num = 0;
}
}
}
生成usart.h,在其中输入
#ifndef __USART_H
#define __USART_H
#include "config.h" //调用头文件
void Uart2_Init(void); //9600bps@24.000MHz
void Usart2_RunTask(void);
#endif
在Task.c中增加1个任务
#include “usart.h”
{0, 1, 1, Usart2_RunTask}, //串口2运行任务
编译,下载
先点无校验,无响应,
加上奇校验,OPEN和CLOSE反应都正确
课后可以试试偶校验。
二.串口通信的超时中断
当我们在做串口接收的程序的时候,往往需要当接收到一串数据的时候需要及时的响应。但是响应的第一步就是我们得先判断这一包数据是否已经接收完成,必须要接收完成了才能进行下一步动作;
案例一:常见的AT指令集,带固定帧尾的数据。假设我们用单片机模拟一个ESP的模块,是不是当串口发来一个"AT\r\n"的时候需要回应一个"OK\r\n";这个可以串口接收函数里判断结尾是不是"\r\n",监测到这个才是结尾。
案例二:串口接收定长输出,每一包的数据都是固定的字节,接受到这个长度的数据就是这一包结束了。
案例三:每一帧的数据里包含数据长度,比如每一包数据的第一个字节是长度,长度多长后面就跟几个字节的数据。
上面的几种常见案例虽然都能实现数据的完整接收,但是在实际使用的时候数据包里有没有长度,有没有固定帧尾都不是我们做从机的时候能决定的,那我们怎么样才能用最好的办法来判断一个数据包也没有接收完成呢!答案是有的————串口超时中断。
UR2TOCR这个寄存器其实在我们接收的时候再打开即可,ENTO不用说坑定需要使能,ENTOI是中断,可以及时响应。SCALE的话这里为了时钟相对准,我们选择系统时钟即可。这里需要注意如果选择1us时钟的话务必要设置IAP_TPS寄存器
这里三个寄存器共同组成了一个计时器,最终的计时时间 = 1/系统时钟(单位mhz)*计时器数值,假设计时器数值为44000(0XABE0),那么UR1TOTL = 0XE0;UR1TOTH = 0XAB;UR1TOTE = 0X00;需要注意的是这里一定要从低位开始写,且这三个寄存器一定都要写,哪怕这个寄存器的数值是0也必须要写一次。需要注意的是这个计时器的数值不能全为0!
这个寄存器就很简单了,只有一个位,触发的时候这个位会置1,我们手动清0即可。需要注意的是这个和串口发送和接收中断共用同一个中断向量号!串口1的中断号是4,那么串口1的超时中断号也是4.
串口通信的超时中断
没有固定帧尾的时候,虽然数据可能是定长的,但是如果数据是错误的呢?
在modbus rtu协议中,假设帧1是主机发送的,帧2是从机的回复命令,按照标准的modbus rtu协议来说,中间应该要间隔3.5个字符周期
有校验位:假设 1个字符=1(起始位)+8(数据位)+1(奇偶校验位)+1(停止位)= 11位
3.5个字符=3.5*11=38.5位,如果波特率=9600bps,则3.5个字符间隔时间为38.5/9.6=4.0104167毫秒
无校验位:假设 1个字符=1(起始位)+8(数据位)+0(无校验位)+1(停止位)=10位
3.5个字符=3.5*10=35位
9600波特率下,8N1的空闲时间就是 1/9600*35 ≈3.646ms;带入公式3646= 1/22.1184*计时器数值,得出计时器数值 = 87504 = 0x013b04;
1.初始化配置
UR2TOCR = 0x00;
UR2TOTL = 0x04;
UR2TOTH = 0x3b;
UR2TOTE = 0x01;
2.中断接收+
UR2TOCR = 0xe0; //开启超时中断,使用系统时钟
3.超时中断处理
if(UR2TOSR & 0x01) //串口超时中断
{
B_RX2_OK = 1; //接收完成标志位
UR2TOSR = 0x00; //清除超时标志位
UR2TOCR = 0x00; //关闭超时中断
}
具体的软件实现如下:
在main.c中
#include “usart.h”
Uart2_Init(); //在EA = 1;之前,串口2初始化
while(1)
{
Usart2_RunTask(); // 在USB之前
…………
}
生成usart.c,在其中输入(ISP生成的)
#include “usart.h”
#include “io.h”
u8 Rec_Dat; //接收缓冲区
u8 Rec_Num = 0; //接收计数
bit B_TX2_Busy = 0;
u8 B_RX2_OK = 0; //串口接收完成标志位
void Uart2_Isr(void) interrupt 8
{
u8 dat;
if (S2CON & 0x02) //检测串口2发送中断
{
S2CON &= ~0x02; //清除串口2发送中断请求位
B_TX2_Busy = 0;
}
if (S2CON & 0x01) //检测串口2接收中断
{
S2CON &= ~0x01; //清除串口2接收中断请求位
UR2TOCR = 0xe0; //开启超时中断,使用系统时钟
Rec_Dat = S2BUF;
}
if(UR2TOSR & 0x01) //串口超时中断
{
B_RX2_OK = 1; //接收完成标志位
UR2TOSR = 0x00; //清除超时标志位
UR2TOCR = 0x00; //关闭超时中断
}
}
void Uart2_Init(void) //9600bps@24.000MHz
{
P_SW2 |= 0x01; //功能脚切换,UART2/USART2: RxD2(P4.2), TxD2(P4.3)
S2CON = 0xD0; //9位数据,可变波特率
AUXR |= 0x04; //定时器时钟1T 模式
T2L = 0x8F; //设置定时初始值
T2H = 0xFD; //设置定时初始值
AUXR |= 0x10; //定时器2开始计时
IE2 |= 0x01; //使能串口2中断
UR2TOCR = 0x00; //先关闭超时中断
UR2TOTL = 0x04; //9600,8N1下的超时时间
UR2TOTH = 0x3b;
UR2TOTE = 0x01;
Rec_Num = 0;
B_TX2_Busy = 0;
B_RX2_OK = 0;
}
void Uart2_SendStr( u8 *puts ) //串口数据发送函数
{
u8 dat;
for (; *puts != 0;puts++) //遇到停止符0结束
{
dat = *puts;
ACC = dat;
if(P)
S2RB8 = 0;
else
S2RB8 = 1;
S2BUF = *puts;
B_TX2_Busy = 1;
while(B_TX2_Busy);
}
}
void Usart2_RunTask(void)
{
if (B_RX2_OK==1)
{
B_RX2_OK = 0;
Uart2_SendStr( "接收成功!\r\n" );
}
}
生成usart.h,在其中输入
#ifndef __USART_H
#define __USART_H
#include "config.h" //调用头文件
void Uart2_Init(void); //9600bps@24.000MHz
void Usart2_RunTask(void);
#endif
在Task.c中增加1个任务
#include “usart.h”
{0, 1, 1, Usart2_RunTask}, //串口2运行任务
编译,下载,运行OK
课后小练
智能数码管显示屏(MODBUS RTU版本,使用9600,8-N-1通信)
1.发送01 06 00 00 00 01 48 0A,数码管显示数值1, 返回01 06 00 00 00 01 48 0A
2.发送01 06 00 00 03 E8 89 74,数码管显示数值1000,返回01 06 00 00 03 E8 89 74
3.发送01 03 00 03 00 01 74 0A,即可读取当前显示的数值,
返回数值为1000时,返回:01 03 02 03 E8 B8 FA
返回数值为1 时,返回:01 03 02 00 01 79 84
第十九集 ADC 学习笔记
一.数转换器(ADC)是什么
模数转换器即A/D转换器,或简称ADC(Analog-to-digital converter),通常是指一个将模拟信号转变为数字信号的电子元件。
如右图所示,采集一个模拟电压得到一个量化的数值的过程,就叫做ADC。
模拟信号->电压
数字信号->电压的具体数值
当然,我们的单片机内也带有ADC的功能,也可以轻松的实现如右图所示的电压检测的功能!具体是怎么实现的呢?我们一起看一段视频。
(视频来源于网络,暂未联系上原作者,如有侵权请联系我删除)
ADC的必要因素:
1.基准(总长度)
2.分度(1/2,1/4的分度)
二.Ai8051U单片机ADC使用原理
三.编写最简单的ADC采集代码
注意:ADC引脚需要设置为高阻输入
任务1:编写ADC读取函数,
并在数码管显示当前ADC数值
新建两个文件adc.c,adc.h,保存到user下
在main.c中输入
#include ”adc.h”
Sys_init(); //系统初始化
usb_init(); //USB CDC接口配置
IE2 |= 0x80; //使能USB中断
Timer0_Init(); //定时器初始化
Init_595();
Timer1_Init();
ADC_Init(); //在EA= 1之前
在 while中增加
Task_Pro_Handle_Callback(); //执行功能函数
SEG_Show_U32(ADC_Read(0)); //P1.0,且设为高阻状态
在adc.c中,输入
(由ISP生成)
#include "adc.h"
void AdcSetRate (void) //50KSPS@24.000MHz
{
ADCCFG &= ~0x0f;
ADCCFG |= 0x04; //SPEED(4)
ADCTIM = 0xbf; //CSSETUP(1), CSHOLD(1), SMPDUTY(31)
}
void ADC_Init (void)
{
//1.初始化IO为高阻输入
P1M0 &= ~0x01;
P1M1 |= 0x01;
//2.初始化ADC速度
AdcSetRate();
//3.对齐模式,右对齐
ADCCFG |= 0x20;
//4.打开ADC电源
ADC_POWER = 1;
}
u16 ADC_Read(u8 no)
{//参考手册
u16 adcval = 0;
ADC_CONTR &= 0Xf0; //清空低四位
ADC_CONTR |= no;
ADC_START = 1; //启动ADC转化
_nop_();
_nop_();
while( !ADC_FLAG ); //等待采集完成
ADC_FLAG = 0; //手动清空
adcval = (((u16)ADC_RES) << 8) + (ADC_RESL); //获取到最终的ADC数值
return adcval;
}
在adc.h中输入
#ifndef __ADC_H
#define __ADC_H
#include "config.h" //调用头文件
void ADC_Init(void);
u16 ADC_Read(u8 no);
u8 ADC_KEY_READ( u16 adc );
#endif
在task.c中增加一个任务
(0,1,1,SEG-Task());
编译下载,运行正常,至此,任务1完成
任务2:编写ADC按键读取函数,
并在数码管上显示当前按键号
在main.c,while循环中
SEG_Show_U32(ADC_KEY_READ(ADC_Read(0)));
在uart.c中增加
#define ADC_OFFSET 64
u8ADC_KEY_READ ( u16 adc) // 分别对应1-16个键值
{
//1.判断当前有没有键按下
if (adc < ( 254 - ADC_OFFSET )
{
return 0;
}
else
{
for( i = 0; i < 16; i++)
{
if ( ( adc>= ( i * 254 -ADC_OFFSET) && adc>= ( i * 254 +ADC_OFFSET))
{
return i;
}
}
}
return 0;
}
在adc.h添加一个申明
u8ADC_KEY_READ ( u16 adc);
编译正确,下载,运行OK
至此,任务2就完成了
课后小练:
简易电压表:
1.用四位数码管显示当前P10引脚的电压
第二十集 ADC_NTC测温,学习笔记
一.ADC的用途分析
虽然ADC只能测量电压,但是通过配合外部的传感器,就可以实现多种的信号检测!
NTC(Negative Temperature Coefficient)是指随温度上升电阻呈指数关系减小、具有负温度系数的热敏电阻现象和材料。该材料是利用锰、铜、硅、钴、铁、镍、锌等两种或两种以上的金属氧化物进行充分混合、成型、烧结等工艺而成的半导体陶瓷,可制成具有负温度系数(NTC)的热敏电阻。其电阻率和材料常数随材料成分比例、烧结气氛、烧结温度和结构状态不同而变化。现在还出现了以碳化硅、硒化锡、氮化钽等为代表的非氧化物系NTC热敏电阻材料。
Rt = RT0*EXP(Bn*(1/T-1/T0))
式中RT、RT0分别为温度T、T0时的电阻值,Bn为材料常数.陶瓷晶粒本身由于温度变化而使电阻率发生变化,这是由半导体特性决定的。
要想做一个电压表,就必须要有电压基准,如果直接用锂电池供电,电压基准会变化。如果加额外的电源芯片会浪费成本,故而可以用内部1.19V直接反推出电源电压!
二.ADC采集NTC换算内温度
P5.1推挽输出,10K电阻,加NTC
用P1.3引脚进行采集,设置为高阻输入
R2/(R1+R2)
端口初始化,用ISP的端口配置功能,
在main.c中输入
#include ”adc.h”
Sys_init(); //系统初始化
usb_init(); //USB CDC接口配置
IE2 |= 0x80; //使能USB中断
Timer0_Init(); //定时器初始化
Init_595();
Timer1_Init();
ADC_Init(); //在EA= 1之前
在 while中增加
Task_Pro_Handle_Callback(); //执行功能函数
SEG_Show_U32(Temp+Cal(ADC_Read(3))); //P1.0,且设为高阻状态
在adc.c中,输入
(由ISP生成)
#include "adc.h"
void AdcSetRate (void) //50KSPS@24.000MHz
{
ADCCFG &= ~0x0f;
ADCCFG |= 0x04; //SPEED(4)
ADCTIM = 0xbf; //CSSETUP(1), CSHOLD(1), SMPDUTY(31)
}
void ADC_Init (void)
{
P5M0 |= 0x02; P5M1 &= ~0x02; //P51推挽输出
P1M0 &= ~0x08; P1M1 |= 0x08; //P13高阻输入
P51 = 1;
//1.初始化IO为高阻输入
P1M0 &= ~0x01;
P1M1 |= 0x01;
//2.初始化ADC速度
AdcSetRate();
//3.对齐模式,右对齐
ADCCFG |= 0x20;
//4.打开ADC电源
ADC_POWER = 1;
}
u16 code Temp_Table[]=
{
140,
149,
159,
168,
178,
188,
199,
210,
222,
233,
246,
259,
272,
286,
。。。。,
};
u16 Temp+Cal(u16 adc) //返回结果是放大了10倍后的数值
{
u8 j = 0;
u16 k = 0;
u16 min; //最小值
u16 max; //最大值
u16 i; //温度
adc = 4096 - adc; //得到当前的adc数值
if( adc < Temp_Tab ) //温度最小值检测
return 0xfffe;
if( adc > Temp_Tab ) //温度最大值检测
return 0xfffF;
min = 0;
max = 160;
for( j=0;j<5;j++ ) //实现5次二分法查询
{
k = (min + max)/2;
if( adc <= Temp_Tab )
max = k;
else
min = k;
}
if( adc == Temp_Tab )
i = min *10; //(20*10 - 400)/10 =-20.0
else if( adc == Temp_Tab )
i = max * 10;
else //50 -51之间
{
while(min <= max )
{
min++;
if( Temp_Tab == adc )
{
i = min * 10;
break;
}
else if( adc < Temp_Tab) //超过这一档的温度的adc
{
min --;
i = Temp_Tab; //上一档的adc数值记录下来
j = Temp_Tab -Temp_Tab ;//两档之前的差值2-8
j = ( adc - i )*10/j;
i = min*10+j;
break;
}
}
}
return i;
}
u16 ADC_Read(u8 no)
{//参考手册
u16 adcval = 0;
ADC_CONTR &= 0Xf0; //清空低四位
ADC_CONTR |= no;
ADC_START = 1; //启动ADC转化
_nop_();
_nop_();
while( !ADC_FLAG ); //等待采集完成
ADC_FLAG = 0; //手动清空
adcval = (((u16)ADC_RES) << 8) + (ADC_RESL); //获取到最终的ADC数值
return adcval;
}
在adc.h中输入
#ifndef __ADC_H
#define __ADC_H
#include "config.h" //调用头文件
void ADC_Init(void);
u16 ADC_Read(u8 no);
u8 ADC_KEY_READ( u16 adc );
u16 Temp+Cal(u16 adc);
#endif
在task.c中增加一个任务
(0,1,1,SEG-Task());
编译下载,运行正常,至此,任务1完成,实际温度=显示值-40
三.使用ADC15测量内部1.19V信号源,反推电源电压
锂电池越来越低,此时就要用到15通道的基准值
仅把main.c中SEG_Show_U32改一下
SEG_Show_U32 ( (4096 * 119 / (u32) ADC+Read ( 15 ) ) ); //实际显示330,正确
课后小练
简易温度检测器:
1.前位数码管显示当前的电压
2.后四位数码管显示当前温度
3.按下设置键设置报警温度,后四位以1秒的闪烁频率显示当前的高温预警温度数值,可通过几个按键修改这个数值,按确认键退出
4.如当前温度超过这个预警温度,后四位数码管显示“- - - -”,恢复正常后显示当前温度
第二十一集 Flash模拟EEPROM,学习笔记
一. FLASH和EEPROM是什么
通常,单片机里的Flash都用于存放运行代码,在运行过程中不能改;EEPROM是用来保存用户数据,运行过程中可以改变,比如我们在程序运行中需要保存密码等参数,就需要存在EEPROM里。但现在单片机没EEPROM了,该怎么存储我们的掉电保存数据呢?
关于 IAP 技术,做过 bootloader 的想必很熟悉 (IAP全称 In Application Programming,即应用编程),和 ISP (全称 In System Programming,即系统编程)不同,ISP 一般都是通过专业的调试器或者下载器对单片机内部的 Flash 存储器进程编程(如JTAG等),而 IAP 技术是从结构上将 Flash 储存器映射分为两个或者多个分区,在一个分区中对其他分区进行编程,这个分区通常称为 bootloader。
注意事项:
IAP_TPS,22.1184MHz注意事项:
1.擦除必须整个扇区
2.只能往1写0,不能往0写1
3.数据必须按扇区写入
4.在电源稳定时操作
5.避免频繁读写
二.内部EEPROM介绍
IAP_TPS,22.1184MHz,取值23
同一时间只能有一个操作
三.内部EEPROM的简单使用
EEPROM
数据寄存器IAP_DATA,读写数据
地址寄存器IAP_ADDR(E,H,L),数据地址,高中低
命令寄存器IAP_CMD:001读,010写,011擦除扇区(512字节)
触发寄存器IAP_TRIG:触发命令,先写入5A A5,相当于解锁
控制寄存器IAP_CONRE:先写入0X80,IAPEN:0禁止,1使能。CMD_FAIL:0正确,1失败
时间控制寄存器IAP_TPS:工作频率/1M,向上进位
SWBS/SWBS2:00复位后从用户程序区执行,IAP_CONTR = 0x20
01复位后从用户系统区执行,IAP_CONTR = 0x28
1x复位后从系统ISP区执行,IAP_CONTR = 0x60
SWRST:为1触发软件复位
示例代码里也有三个非常经典的案例
任务1:实现手册的EEPROM的基本操作,并用数码管显示当前的数值。并实现每次重新开机数值+1
生成两个文件
eeprom.c和eeprom.h
在eeprom.c中输入
#include "eeprom.h"
#define MAIN_Fosc24000000UL
#define IAP_STANDBY() IAP_CMD = 0 //IAP空闭命令(禁止)
#define IAP_READ() IAP_CMD = 1 //IAP读出命令
#define IAP_WRITE() IAP_CMD = 2 //IAP写入命令
#define IAP_ERASE() IAP_CMD = 3 //IAP擦除命令
#define IAP_ENABLE() IAP_CONTR = IAP_EN; IAP_TPS = MAIN_Fosc / 1000000
#define IAP_DISABLE() IAP_CONTR = 0; IAP_CMD = 0; IAP_TRIG = 0; IAP_ADDRE = 0xff; IAP_ADDRH = 0xff; IAP_ADDRL = 0xff
#define IAP_EN (1<<7)
#define IAP_SWBS (1<<6)
#define IAP_SWRST (1<<5)
#define IAP_CMD_FAIL (1<<4)
//========================================================================
// 函数: void DisableEEPROM(void)
// 描述:禁止EEPROM.
// 参数: none.
// 返回: none.
// 版本: V1.0, 2014-6-30
//========================================================================
void DisableEEPROM(void) //禁止访问EEPROM
{
IAP_CONTR = 0; //关闭 IAP 功能
IAP_CMD = 0; //清除命令寄存器
IAP_TRIG = 0; //清除触发寄存器
IAP_ADDRE = 0xff; //将地址设置到非IAP 区域
IAP_ADDRH = 0xff; //将地址设置到非IAP 区域
IAP_ADDRL = 0xff;
}
//========================================================================
// 函数: void EEPROM_Trig(void)
// 描述: 触发EEPROM操作
// 参数: none.
// 返回: none.
// 版本: V1.0, 2014-6-30
//========================================================================
void EEPROM_Trig(void)
{
F0 = EA; //保存全局中断
EA = 0; //禁止中断,避免触发命令无效
IAP_TRIG = 0x5A;
IAP_TRIG = 0xA5; //先送5AH,再送A5H到IAP触发寄存器,每次都需要这样
//送完A5后,IAP命令立即被触发启动
//CPU等待IAP完成后,都会继续执行程序
_nop_(); //多级流水线的,触发后建议加四个NOP,保证IAP_DATA的数据完成准备
_nop_();
_nop_();
_nop_();
EA = F0; //恢复全局中断
}
//========================================================================
//函数: void EEPROM_SectorErase(u32 EE_address)
//描述: 擦除一个扇区
//参数: EE_address:要擦除的EEPROM的扇区中的一个字节地址
//返回: none.
//版本: V1.0, 2014-6-30
//========================================================================
void EEPROM_SectorErase(u32 EE_address)
{
IAP_ENABLE(); //设置等待时间,允许IAP操作,送一次就够
IAP_ERASE(); //宏调用,送扇区操作命令,命令不需改变时,不需重送命令
//只有扇区擦除,没有字节擦除,512字节/扇区
//扇区中任意一个字节地址都是扇区地址
IAP_ADDRE = (u8)(EE_address >> 16); //送扇区地址高字节(仅地址需要改变时才需重送地址)
IAP_ADDRH = (u8)(EE_address >> 8);//送扇区地址中字节(仅地址需要改变时才需重送地址)
IAP_ADDRL = (u8)EE_address; //送扇区地址低字节(仅地址需要改变时才需重送地址)
EEPROM_Trig(); //触发EEPROM操作
DisableEEPROM(); //禁止EEPROM操作
}
//========================================================================
//函数: void EEPROM_read_n(u32 EE_address,u8 *DataAddress,u8 lenth)
//描述:读N个字节函数
//参数: EE_address:要读出的EEPROM地址
// DataAddress: 要读出的数据指针
// length: 要读出的数据长度
//返回: 0: 写入正确1: 写入长度为0错误.2: 写入数据错误
//版本: V1.0, 2014-6-30
//========================================================================
void EEPROM_read_n(u32 EE_address,u8 *DataAddress,u8 length)
{
IAP_ENABLE(); //设置等待时间,允许IAP操作,送一次就够
IAP_READ(); //送字节读命令,命令不需改变时,不需重送命令
do
{
IAP_ADDRE = (u8)(EE_address >> 16); //送地址高字节(地址要变时才需重新送地址)
IAP_ADDRH = (u8)(EE_address >> 8);//送地址中字节(地址要变时才需重新送地址)
IAP_ADDRL = (u8)EE_address; //送地址低字节(地址要变时才需重新送地址)
EEPROM_Trig(); //触发EEPROM操作
*DataAddress = IAP_DATA; //读出的数据送往
EE_address++;
DataAddress++;
}while(--length);
DisableEEPROM();
}
//========================================================================
//函数: u8 EEPROM_write_n(u32 EE_address,u8 *DataAddress,u8 length)
//描述:写N个字节函数
//参数: EE_address:要写入的EEPROM的首地址.
// DataAddress: 要写入数据的指针
// length: 要写入的长度
//返回: 0:写入正确1:写入长度为0错误2:写入数据错误
//版本: V1.0, 2014-6-30
//========================================================================
u8 EEPROM_write_n(u32 EE_address,u8 *DataAddress,u8 length)
{
u8i;
u16 j;
u8*p;
if(length == 0) return 1; //长度为0错误
IAP_ENABLE(); //设置等待时间,允许IAP操作
i = length;
j = EE_address;
p = DataAddress;
IAP_WRITE(); //宏调用,送字节写命令
do
{
IAP_ADDRE = (u8)(EE_address >> 16); //送地址高字节(地址要变时才需重新送地址)
IAP_ADDRH = (u8)(EE_address >> 8);//送地址中字节(地址要变时才需重新送地址)
IAP_ADDRL = (u8)EE_address; //送地址低字节(地址要变时才需重新送地址)
IAP_DATA= *DataAddress; //送数据到IAP DATA,只有数据改变时才需重新送
EEPROM_Trig(); //触发EEPROM操作
EE_address++; //下一个地址
DataAddress++; //下一个数据
}while(--length); //直到结束
EE_address = j;
length = i;
DataAddress = p;
i = 0;
IAP_READ(); //送N个字节并比较
do
{
IAP_ADDRE = (u8)(EE_address >> 16); //送地址高字节(地址要变时才需重新送地址)
IAP_ADDRH = (u8)(EE_address >> 8);//送地址中字节(地址要变时才需重新送地址)
IAP_ADDRL = (u8)EE_address; //送地址低字节(地址要变时才需重新送地址)
EEPROM_Trig(); //´触发EEPROM操作
if(*DataAddress != IAP_DATA) //读出的数据与源数据比较
{
i = 2;
break;
}
EE_address++;
DataAddress++;
}while(--length);
DisableEEPROM();
return i;
}
#define E_ADDR10
#define E_PWR 0XFE
u8 SYS_Run_times = 0;
//任务1:实现手册的EEPROM的基本操作,并用数码管显示当前的数值,并实现每次重新开机数值+1
void Parm_Init(void)
{
u8 dat; //0:操作码 1:开机次数 2:校验码(累加校验)
EEPROM_read_n( E_ADDR,dat,3 );
if( dat != E_PWR )//如果读取的数据不为操作码,就是第一次上电,此时可以初始化
{
EEPROM_SectorErase(E_ADDR); //清空地址里的数据,擦除扇区
dat = E_PWR;
dat = 0;
dat = (dat+dat);
EEPROM_write_n(E_ADDR,dat,3);
SYS_Run_times = 0;
}
else //第二次及其之后的上电
{
if( dat == (u8)(dat+dat) ) //校验通过,数据有效
{
SYS_Run_times = dat;
SYS_Run_times +=1;
EEPROM_SectorErase(E_ADDR); //清空地址里的数据,擦除扇区
dat = E_PWR;
dat = SYS_Run_times;
dat = (dat+dat);
EEPROM_write_n(E_ADDR,dat,3);
}
else
{
//如果数据校验出错是,执行什么
}
}
}
在eeprom.h中输入
#ifndef __EEPROM_H
#define __EEPROM_H
#include "config.h" //调用头文件
extern u8 SYS_Run_times ;
void DisableEEPROM(void); //禁止访问EEPROM
void EEPROM_Trig(void);
void EEPROM_SectorErase(u32 EE_address);
void EEPROM_read_n(u32 EE_address,u8 *DataAddress,u8 length);
u8 EEPROM_write_n(u32 EE_address,u8 *DataAddress,u8 length);
void Parm_Init(void);
#endif
在main.c中写入
#include "eeprom.h"
Parm_Init(); //在USB配置之后
SEG_Show_U32(SYS_Run_time); //在while中
在task.c中
{0,1,1, SEG_Task}, //显示数码管
编译0错0警告,下载运行,正确。
课后小练
密码锁
1.没有输入时,显示“- - - - - - - -”
2.有输入时,按下一个按键,开始按顺序写入
例如,第一个按下1,显示“1 - - - - - - -”
例如,第二个按下3,显示“1 3 - - - - - -”
3.当按下的密码为“ 1 2 3 4 5 6 7 0”时,数码管显示open的字 符,否则,还是显示“- - - - - - - -”
4.看门狗,超时1秒自动复位
5.增加开机版本号,开机显示三秒的U 1.00 版本号
6.增加手动复位,P33按钮按下时重启(方便查看版本号和清除密码)
新增:
1.开锁后长按“#” 持续3秒后进入管理员模式,可以修改密码,输入完成后再次按下保存,之后需要输入这个修改后的密码才能开锁!