通过ADC转换电路采集电位器AD值,将采集的AD值转换成电压值,通过数码管显示出来;
采集光敏电阻的AD值;
采集热敏电阻的AD值;
采集外部通道AIN3的电压值;
为了方便显示,我们可以通过独立按键k1-k4来切换显示哪种数据;(k1:电位器电压;k2:外部通道AIN3电压;k3:光敏电阻AD值;k4:热敏电阻AD值)
ADC(analog to digital converter)也称为模数转换器,是指一个将模拟信号转变为数字信号的器件;
单片机在采集模拟信号时,通常都需要在前端加上A/D芯片;
首先我们知道51单片机系统内部运算时用的全部是数字量,即0和1,因此对单片机系统而言,无法直接操作模拟量,必须将模拟量转换成数字量才能操作;
所谓数字量,就是用一系列0和1组成的二进制代码表示某个信号大小的量;
用数字量表示同一个模拟量时,数字位数可以多也可以少,位数越多则表示的精度越高,位数越少表示的精度就越低;
ADC的分辨率是指对于允许范围内的模拟信号,它能输出离散数字信号值的个数;
这些信号值通常用二进制数来存储,因此分辨率经常用比特作为单位,且这些离散值的个数是2的幂指数;
例如:12位ADC的分辨率就是12位,或者说分辨率为满刻度的1/(2^12);
一个10V满刻度的12位ADC能分辨输入电压变化最小值是10V×1/(2^12)=2.4mV;
转换误差通常是以输出误差的最大值形式给出,它表示A/D转换器实际输出的数字量和理论上的输出数字量之间的差别,常用最低有效位的倍数表示;
例如给出相对误差≤±LSB/2,这就表明实际输出的数字量和理论上应得到的输出数字量之间的误差小于最低位的半个字;
ADC的转换速率是能够重复进行数据转换的速度,即每秒转换的次数;
完成一次A/D转换所需的时间(包括稳定时间),则是转换速率的倒数;
AD转换器(ADC)将模拟量转换为数字量通常要经过4个步骤:采样、保持、量化和编码;
采样:所谓采样即是将一个时间上连续变化的模拟量转换为时间上离散变化的模拟量;
如下图所示:
保持:将采样结果存储起来,直到下次采样,这个过程叫做保持;一般采样器和保持电路一起总称为采样保持电路;
量化:将采样电平归化为与之接近的离散数字电平,这个过程叫做量化;
编码:将量化后的结果按照一定数制形式表示就是编码;
将采样电平(模拟值)转换为数字值时,主要有两类方法:直接比较型与间接比较型;
就是将输入模拟信号直接与标准的参考电压比较,从而得到数字量;
常见的有并行ADC和逐次比较型ADC;
输入模拟量不是直接与参考电压比较,而是将二者变为中间的某种物理量在进行比较,然后将比较所得的结果进行数字编码;
常见的有双积分型ADC;
比较常用的为逐次比较型ADC和双积分型ADC:
采用逐次比较法的AD转换器是有一个比较器、DA 转换器、缓冲寄存器和控制逻辑电路组成;
如下图所示:
基本原理是:从高位到低位逐次试探比较,就像用天平秤物体,从重到轻逐级增减砝码进行试探;
逐次比较法的转换过程是:
1 初始化时将逐次比较寄存器各位清零,转换开始时,先将逐次比较寄存器最高位置1,送入DA转换器,经DA转换后生成的模拟量送入比较器,称为U0,与送入比较器的待转换的模拟量Ux进行比较,若U0 < Ux,该位1被保留,否则被清除;
2 然后再将逐次比较寄存器次高位置1,将寄存器中新的数字量送DA转换器,输出的U0再与Ux比较,若U0 < Ux,该位1被保留,否则被清除;
3 重复此过程,直至比较寄存器最低位;转换结束后,将逐次比较寄存器中的数字量送入缓冲寄存器,得到数字量的输出;
4 逐次比较的操作过程是在一个控制电路的控制下进行的;
采用双积分法的AD转换器由电子开关、积分器、比较器和控制逻辑等部件组成;
基本原理是:将输入电压变换成与其平均值成正比的时间间隔,再把此时间间隔转换成数字量,属于间接转换;
双积分法AD转换的过程是:
1 先将开关接通待转换的模拟量Vi,Vi采样输入到积分器,积分器从零开始进行固定时间T的正向积分,时间T到后,开关再接通与Vi极性相反的基准电压Vref,将Vref输入到积分器,进行反向积分,直到输出为0V时停止积分;
2 Vi越大,积分器输出电压越大,反向积分时间也越长;
3 计数器在反向积分时间内所计的数值,就是输入模拟电压Vi所对应的数字量,实现了AD转换;
XPT2046是一款4线制电阻式触摸屏控制器,内含12位分辨率、125KHz转换速率、逐步比较型的A/D转换器;
XPT2046支持从1.5V到5.25V的低电压I/O接口;
XPT2046能通过执行两次A/D转换查出被按的屏幕位置,除此之外,还可以测量加在触摸屏上的压力;
内部自带2.5V参考电压,可以作为辅助输入、温度测量和电池监测之用,电池监测的电压范围可以从0V到6V;
XPT2046片内集成有一个温度传感器;
在2.7V的典型工作状态下,关闭参考电压,功耗可小于0.75mW;
工作温度范围为-40℃~+85℃,与ADS7846、TSC2046、AK4182A完全兼容;
XPT2046是一种典型的逐次比较型模数转换器(SAR ADC),包含了采样/保持、模数转换、串口数据、输出等功能;
同时芯片集成有一个2.5V的内部参考电压源、温度检测电路,工作时使用外部时钟;
XPT2046可以单电源供电,电源电压范围为2.7V~5.5V;
参考电压值直接决定ADC的输入范围,参考电压可以使用内部参考电压,也可以从外部直接输入1V~VCC范围内的参考电压(要求外部参考电压源输出阻抗低);
X、Y、Z、VBAT、Temp和AUX模拟信号经过片内的控制寄存器选择后进入ADC,ADC可以配置为单端或差分模式;
选择VBAT、Temp和AUX时应该配置为单端模式;作为触摸屏应用时,应该配置为差分模式,这可有效消除由于驱动开关的寄生电阻及外部的干扰带来的测量误差,提高转换精度;
在对XPT2046进行控制时,控制字节由DIN输入的控制;
(SPI读写时序)
XPT2046数据接口是串行接口,其典型工作时序如下图所示:(图中展示的信号来自带有基本串行接口的单片机或数据信号处理器)
处理器和转换器之间的的通信需要8个时钟周期,可采用SPI、SSI和Microwire等同步串行接口;
一次完整的转换需要24个串行同步时钟(DCLK)来完成;
前8个时钟用来通过DIN引脚输入控制字节;
当转换器获取有关下一次转换的足够信息后,接着根据获得的信息设置输入多路选择器和参考源输入,并进入采样模式,如果需要,将启动触摸面板驱动器;
3个多时钟周期后,控制字节设置完成,转换器进入转换状态;这时,输入采样-保持器进入保持状态,触摸面板驱动器停止工作(单端工作模式);接着的12个时钟周期将完成真正的模数转换;
如果是度量比率转换方式(SER/DFR=0),驱动器在转换过程中将一直工作,第13个时钟将输出转换结果的最后一位;
剩下的3个多时钟周期将用来完成被转换器忽略的最后字节(DOUT置低);
由图可知,控制管脚DIN、CS、DCLK、DOUT分别连接单片机p34、p35、p36、p37口;
其中ADC的输入转换通道分别接入了AD1电位器(AIN0)、NTC1热敏传感器(AIN1)、GR1光敏传感器(AIN2),还有一个外接通道AIN3接在DAC(PWM)(数模转换)模块的J52端子上,供外部模拟信号检测;
按照工作时序(SPI通信)编写读写程序;
根据模式控制的配置图和命令格式,如果要检测AD1电位器,即AIN0/X+管脚输入的模拟信号,则命令配置为(0x94=1001 0100)
得到AD值后,可按照如下公式计算电压值:Vref*ADC_Value/分辨率;
其中Vref是参考电压,XPT2046接入的是5V,ADC_Value是读取的AD值,分辨率是ADC的位数(2^12=4096);
然后将该值放大10倍,即保留小数后一位;
最后将得到的电压值转换为数码管段码数据显示;
main.c
/*
* @Description: 采集电位器的电压值
*/
#include "public.h"
#include "smg.h"
#include "xpt2046.h"
void main()
{
u16 adc_value = 0;
float adc_vol; // ADC电压值
u8 adc_buf[3];
while (1)
{
adc_value = xpt2046_read_adc_value(0x94); // 测量电位器
adc_vol = 5.0 * adc_value / 4096; // 将读取的AD值转换为电压
adc_value = adc_vol * 10; // 放大10倍,即保留小数点后一位
adc_buf[0] = gsmg_code[adc_value / 10] | 0x80;
adc_buf[1] = gsmg_code[adc_value % 10];
adc_buf[2] = 0x3e; // 显示单位V
smg_display(adc_buf, 6);
}
}
public.h
#ifndef _public_H
#define _public_H
#include "reg52.h"
typedef unsigned int u16; // 对系统默认数据类型进行重定义
typedef unsigned char u8;
void delay_10us(u16 ten_us);
void delay_ms(u16 ms);
#endif
public.c
#include "public.h"
/**
* @description: 延时函数,ten_us=1时,大约延时10us
* @param {u16} ten_us 延时倍数
* @return {*}
*/
void delay_10us(u16 ten_us)
{
while (ten_us--)
;
}
/**ms延时函数,ms=1时,大约延时1ms***
* @param {u16} ms 延时倍数
* @return {*}
*/
void delay_ms(u16 ms)
{
u16 i, j;
for (i = ms; i > 0; i--)
for (j = 110; j > 0; j--)
;
}
xpt2046.h
#ifndef _xpt2046_H
#define _xpt2046_H
#include "public.h"
// 管脚定义
sbit DOUT = P3 ^ 7; // 输出
sbit CLK = P3 ^ 6; // 时钟
sbit DIN = P3 ^ 4; // 输入
sbit CS = P3 ^ 5; // 片选
// 函数声明
u16 xpt2046_read_adc_value(u8 cmd);
#endif
xpt2046.c
#include "xpt2046.h"
#include "intrins.h"
/**
* @description: XPT2046写数据
* @param {u8} dat 要写入的数据
* @return {*}
*/
void xpt2046_wirte_data(u8 dat)
{
u8 i;
CLK = 0;
_nop_();
for (i = 0; i < 8; i++) // 循环8次,每次传输一位,共一个字节
{
DIN = dat >> 7; // 先传高位再传低位
dat <<= 1; // 将低位移到高位
CLK = 0; // CLK由低到高产生一个上升沿,从而写入数据
_nop_();
CLK = 1;
_nop_();
}
}
/**
* @description: XPT2046读数据
* @return {u16} XPT2046返回12位数据
*/
u16 xpt2046_read_data(void)
{
u8 i;
u16 dat = 0;
CLK = 0;
_nop_();
for (i = 0; i < 12; i++) // 循环12次,每次读取一位,大于一个字节数,所以返回值类型是u16
{
dat <<= 1;
CLK = 1;
_nop_();
CLK = 0; // CLK由高到低产生一个下降沿,从而读取数据
_nop_();
dat |= DOUT; // 先读取高位,再读取低位
}
return dat;
}
/**
* @description: XPT2046读AD数据
* @param {u8} cmd 命令
* @return {u16} XPT2046的返回AD值
*/
u16 xpt2046_read_adc_value(u8 cmd)
{
u8 i;
u16 adc_value = 0;
CLK = 0; // 先拉低时钟
CS = 0; // 使能XPT2046
xpt2046_wirte_data(cmd); // 发送命令字
for (i = 6; i > 0; i--)
; // 延时等待转换结果
CLK = 1;
_nop_();
CLK = 0; // 发送一个时钟,清除BUSY
_nop_();
adc_value = xpt2046_read_data();
CS = 1; // 关闭XPT2046
return adc_value;
}
smg.h
#ifndef _smg_H
#define _smg_H
#include "public.h"
#define SMG_A_DP_PORT P0 // 使用宏定义数码管段码口
// 定义数码管位选信号控制脚
sbit LSA = P2 ^ 2;
sbit LSB = P2 ^ 3;
sbit LSC = P2 ^ 4;
extern u8 gsmg_code[17]; // 使“共阴极数码管显示0~F的段码数据”这个变量定义为外部可用
void smg_display(u8 dat[], u8 pos);
#endif
smg.c
#include "smg.h"
// 共阴极数码管显示0~F的段码数据
u8 gsmg_code[17] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71};
/**
* @description: 动态数码管显示函数
* @param {u8} dat 要显示的数据
* @param {u8} pos 从左开始第几个位置开始显示,范围1-8
* @return {*}
*/
void smg_display(u8 dat[], u8 pos)
{
u8 i = 0;
u8 pos_temp = pos - 1;
for (i = pos_temp; i < 8; i++)
{
switch (i) // 位选
{
case 0:
LSC = 1;
LSB = 1;
LSA = 1;
break;
case 1:
LSC = 1;
LSB = 1;
LSA = 0;
break;
case 2:
LSC = 1;
LSB = 0;
LSA = 1;
break;
case 3:
LSC = 1;
LSB = 0;
LSA = 0;
break;
case 4:
LSC = 0;
LSB = 1;
LSA = 1;
break;
case 5:
LSC = 0;
LSB = 1;
LSA = 0;
break;
case 6:
LSC = 0;
LSB = 0;
LSA = 1;
break;
case 7:
LSC = 0;
LSB = 0;
LSA = 0;
break;
}
SMG_A_DP_PORT = dat[i - pos_temp]; // 传送段选数据
delay_10us(100); // 延时一段时间,等待显示稳定
SMG_A_DP_PORT = 0x00; // 消影
}
}
按F7编译,无错误,生成.hex文件,使用pz-isp将hex文件下载到单片机
结果:成功采集电位器电压值并显示出来;
与采集电位器电压类似,不过不需要转换AD值了,输入口不同,所以命令也不同,为0xA4
只有main.c文件不同
main.c
/*
* @Description: 采集电位器的AD值
*/
#include "public.h"
#include "smg.h"
#include "xpt2046.h"
void main()
{
u16 adc_value = 0;
u8 adc_buf[4];
while (1)
{
adc_value = xpt2046_read_adc_value(0xA4); // 测量光敏电阻
adc_buf[0] = gsmg_code[adc_value / 1000];
adc_buf[1] = gsmg_code[adc_value % 1000 / 100];
adc_buf[2] = gsmg_code[adc_value % 1000 % 100 / 10];
adc_buf[3] = gsmg_code[adc_value % 1000 % 100 % 10];
smg_display(adc_buf, 5);
}
}
按F7编译,无错误,生成.hex文件,使用pz-isp将hex文件下载到单片机
与采集光敏电阻AD值类似,命令为0xD4
只有main.c文件不同
main.c
/*
* @Description: 采集热敏电阻的AD值
*/
#include "public.h"
#include "smg.h"
#include "xpt2046.h"
void main()
{
u16 adc_value = 0;
u8 adc_buf[4];
while (1)
{
adc_value = xpt2046_read_adc_value(0xD4); // 测量热敏电阻
adc_buf[0] = gsmg_code[adc_value / 1000];
adc_buf[1] = gsmg_code[adc_value % 1000 / 100];
adc_buf[2] = gsmg_code[adc_value % 1000 % 100 / 10];
adc_buf[3] = gsmg_code[adc_value % 1000 % 100 % 10];
smg_display(adc_buf, 5);
}
}
按F7编译,无错误,生成.hex文件,使用pz-isp将hex文件下载到单片机
与采集电位器电压值类似,命令为0xE4
只有main.c文件不同
main.c
/*
* @Description: 采集外部通道AIN3的电压值
*/
#include "public.h"
#include "smg.h"
#include "xpt2046.h"
void main()
{
u16 adc_value = 0;
float adc_vol; // ADC电压值
u8 adc_buf[3];
while (1)
{
adc_value = xpt2046_read_adc_value(0xE4); // 测量外部输入通道AIN3
adc_vol = 5.0 * adc_value / 4096; // 将读取的AD值转换为电压
adc_value = adc_vol * 10; // 放大10倍,即保留小数点后一位
adc_buf[0] = gsmg_code[adc_value / 10] | 0x80;
adc_buf[1] = gsmg_code[adc_value % 10];
adc_buf[2] = 0x3e; // 显示单位V
smg_display(adc_buf, 6);
}
}
按F7编译,无错误,生成.hex文件,使用pz-isp将hex文件下载到单片机
我们只需要让按键按下时切换显示内容,因此我们需要中断函数来实现需求;
我这里选择外部中断0实现,下降沿触发方式,在中断函数里实现切换的功能;
定义静态全局变量key,确保能记录上次按下的值;
因为我们需要使用k3键连接的p32端口来实现外部中断,所以需要在中断函数里先消除k3键的影响;
由于进入中断函数,数码管控制程序可能来不及消影,所以要在中断函数再次消影确保显示正确;
退出中断函数时也要消除k3键的影响,否则可能会连续进入中断函数;
额外加上独立按键检测程序;
main.c
/*
* @Description: 使用独立按键来切换显示内容:先按下k3(相当于设置键),再按下k1-k4,显示需要的内容
*/
#include "public.h"
#include "smg.h"
#include "key.h"
#include "xpt2046.h"
static u8 key = KEY1_PRESS; // 定义全局变量,记录上次按下按键key的值,默认为key1的值
/**
* @description: 外部中断0配置函数
* @return {*}
*/
void exti0_init(void)
{
IT0 = 1; // 跳变沿触发方式(下降沿)
EX0 = 1; // 打开INT0的中断允许
EA = 1; // 打开总中断
}
/**
* @description: 外部中断0中断函数
* @return {*}
*/
void exti0() interrupt 0 // 中断号必须对应上
{
delay_10us(1000); // 消抖
if (KEY3 == 0) // 再次判断K3键是否按下
{
SMG_A_DP_PORT = 0x00; // 数码管消影
delay_ms(300); // 等待0.3s,消除key3的影响
while (1) // 设置死循环,只要没按键按下就一直循环
{
key = key_scan(1);
delay_10us(1000); // 消抖
if (key)
{
delay_ms(300); // 等待0.3s,消除按下按键的影响
return; // 确认有按键按下后就返回,继续执行主函数
}
}
}
}
void main()
{
u16 adc_value = 0;
float adc_vol; // ADC电压值
u8 adc_buf[4];
exti0_init(); // 外部中断0配置
while (1)
{
switch (key)
{
case KEY1_PRESS:
{
adc_value = xpt2046_read_adc_value(0x94); // 测量电位器
adc_vol = 5.0 * adc_value / 4096; // 将读取的AD值转换为电压
adc_value = adc_vol * 10; // 放大10倍,即保留小数点后一位
adc_buf[0] = gsmg_code[adc_value / 10] | 0x80;
adc_buf[1] = gsmg_code[adc_value % 10];
adc_buf[2] = 0x3e; // 显示单位V
smg_display(adc_buf, 6);
}
break;
case KEY2_PRESS:
{
adc_value = xpt2046_read_adc_value(0xE4); // 测量外部输入通道AIN3
adc_vol = 5.0 * adc_value / 4096; // 将读取的AD值转换为电压
adc_value = adc_vol * 10; // 放大10倍,即保留小数点后一位
adc_buf[0] = gsmg_code[adc_value / 10] | 0x80;
adc_buf[1] = gsmg_code[adc_value % 10];
adc_buf[2] = 0x3e; // 显示单位V
smg_display(adc_buf, 6);
}
break;
case KEY3_PRESS:
{
adc_value = xpt2046_read_adc_value(0xA4); // 测量光敏电阻
adc_buf[0] = gsmg_code[adc_value / 1000];
adc_buf[1] = gsmg_code[adc_value % 1000 / 100];
adc_buf[2] = gsmg_code[adc_value % 1000 % 100 / 10];
adc_buf[3] = gsmg_code[adc_value % 1000 % 100 % 10];
smg_display(adc_buf, 5);
}
break;
case KEY4_PRESS:
{
adc_value = xpt2046_read_adc_value(0xD4); // 测量热敏电阻
adc_buf[0] = gsmg_code[adc_value / 1000];
adc_buf[1] = gsmg_code[adc_value % 1000 / 100];
adc_buf[2] = gsmg_code[adc_value % 1000 % 100 / 10];
adc_buf[3] = gsmg_code[adc_value % 1000 % 100 % 10];
smg_display(adc_buf, 5);
}
break;
default:
{
adc_value = xpt2046_read_adc_value(0x94); // 测量电位器
adc_vol = 5.0 * adc_value / 4096; // 将读取的AD值转换为电压
adc_value = adc_vol * 10; // 放大10倍,即保留小数点后一位
adc_buf[0] = gsmg_code[adc_value / 10] | 0x80;
adc_buf[1] = gsmg_code[adc_value % 10];
adc_buf[2] = 0x3e; // 显示单位V
smg_display(adc_buf, 6);
}
break;
}
}
}
key.h
#ifndef _key_H
#define _key_H
#include "public.h"
// 定义独立按键控制脚
sbit KEY1 = P3 ^ 1;
sbit KEY2 = P3 ^ 0;
sbit KEY3 = P3 ^ 2;
sbit KEY4 = P3 ^ 3;
// 使用宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0
u8 key_scan(u8 mode);
#endif
key.c
#include "key.h"
/**
* @description: 检测独立按键是否按下,按下则返回对应键值
* @param {u8} mode mode=0:单次扫描按键,mode=1:连续扫描按键
* @return {u8} k1到k5的键值1-5,0表示没有按键按下
*/
u8 key_scan(u8 mode)
{
static u8 key = 1;
if (mode)
key = 1; // 连续扫描按键
if (key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0)) // 任意按键按下
{
delay_10us(1000); // 消抖
key = 0;
if (KEY1 == 0)
return KEY1_PRESS;
else if (KEY2 == 0)
return KEY2_PRESS;
else if (KEY3 == 0)
return KEY3_PRESS;
else if (KEY4 == 0)
return KEY4_PRESS;
}
else if (KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1) // 无按键按下
{
key = 1;
}
return KEY_UNPRESS;
}
按F7编译,无错误,生成.hex文件,使用pz-isp将hex文件下载到单片机