目录
一、了解器件性能,查询资料
STC12C5A60S2自带A/D转换器的使用_奔跑路上的小柱子的博客-CSDN博客在我们使用传统8051单片机的时候,常常使用某些需要进行A/D转换的模块,比如烟雾传感器(MQ-2)、超声波测距模块、人体红外传感器等等,这样会使我们的工作量增加不少。但是我们可以选择STC12C5A60S2单片机,它作为新一代8051单片机,内部集成了8路高速10位A/D转换器,可以省略外加A/D转换模块的步骤。提示:以下是本篇文章正文内容,下面案例可供参考https://blog.csdn.net/m0_57920090/article/details/127666918
使用STC12C5A60S2内部AD测量电压0-5V(单片机应使用5V电压供电)_stc单片机测量电压_小 安的博客-CSDN博客使用STC12C5A60S2内部AD测量电压0-5V(单片机应使用5V电压供电)main.c/***********************************************************程序功能:ADC采集电压信号(0-5V)+LCD1602液晶显示***********************************************************/#include "reg51.h"#include "intrins.h"//sfr P4https://blog.csdn.net/qq_30681939/article/details/107220885
LCD1602指令集解读_qq_25814297-npl的博客-CSDN博客LCD1602指令集解读LCD1602指令集(11个)1、清屏指令(clear display) RS=0 ,R/w=0, 01H功能:清除液晶显示器,即将DDRAM中的内容全部填入20H(空白字符) 光标撤回显示屏左上方 将地址计数器(AC)设为0, 光标移动方向为从左向右,并且DDRAM的自增量为1(...https://blog.csdn.net/qq_25814297/article/details/105287699参考以上两个博主的博客,能帮助我们更好的了解LCD1602和stc12c5a60s2的ADC模块。
这里我们依次介绍两个器件(针对新手),解决的主要问题是如何快速上手使用。
LCD1602
这里我们需要关注的内容主要是RS 、RW、E、D0-D7数据传输端口,其中RS=0,写指令;RS=1,写数据。
这里我们直接上函数,一律放在头文件lcd.h中,方便调用。读者可以自己新建头文件,然后放代码,具体电路中引脚是如何连接单片机,还请自己考虑后修改。
#ifndef _LCD1602_H
#define _LCD1602_H
sbit LCD_RS=P2^0;
sbit LCD_E=P2^2;
sbit LCD_RW=P2^1;
#define LCD_Data P0
#define BYTE unsigned char
#define Busy 0x80
unsigned char code table[]={48,49,50,51,52,53,54,55,56,57}; //0-9 ascii
void WriteDataLCD(BYTE WR_DATA);
void WriteCommandLCD(BYTE WCLCD);
void busy_check(void);
void LCDInit(void);
void DisplayOneChar(BYTE X, BYTE Y, BYTE DData);
void DisplayListChar(BYTE X, BYTE Y, BYTE code*DData);
sbit为寄存器定义,约等于#define;
code 定义的数组会放入ROM中,定义后,不建议修改,这里保存的是0-9的ASCII码,因为LCD1602中显示的数据根据ASCII码来显示;
LCD1602的地址有两行,每一行的地址范围并不同,第一行是0x00到0x27,第二行是0x40到0x67,但想要将数据显示在显示屏上,第一行只有0x00到0x0f,第二行只有0x40到0x4f;
在显示数据的时候,需要先写入一条指令指明要在哪个地址上写数据,然后才能在该地址上写入数据;
需要用到的指令格式为
我们能看到,写入这条指令需要设置RS=0,RW=0,DB6=1; 即二进制格式位为 01+地址
void LCDInit(void) //初始化部分,涉及的指令可以参考11条指令集
{
WriteCommandLCD(0x38);
WriteCommandLCD(0x08);
WriteCommandLCD(0x01);
WriteCommandLCD(0x06);
WriteCommandLCD(0x0C);
}
void DisplayListChar(BYTE X, BYTE Y, BYTE code *DData) //显示一串数据
{
BYTE ListLength;
ListLength = 0;
X &= 0xF;
Y &= 0x1;
while (DData[ListLength]>=0x20){
if (X <= 0xF){
DisplayOneChar(X, Y, DData[ListLength]);
ListLength++;
X++;
}}}
void DisplayOneChar(BYTE X, BYTE Y, BYTE DData) //在指定X,Y位置显示一个字符 注意20H是空白
{
X &= 0xF;
Y &= 0x1;
if (Y)
X |= 0x40;
X |= 0x80;
WriteCommandLCD(X);
WriteDataLCD(DData);
}
void WriteCommandLCD(BYTE WCLCD) //写入命令,需要先检查lcd1602是否忙碌中
{
busy_check();
LCD_Data = WCLCD;
LCD_RS = 0;
LCD_RW = 0;
LCD_E = 1;
LCD_E = 0;
}
void WriteDataLCD(BYTE WR_DATA) //写入命令后,再写入数据,注意RS、RW的值
{
busy_check();
LCD_Data = WR_DATA;
LCD_RS = 1;
LCD_RW = 0;
LCD_E = 1;
LCD_E = 0;
}
void busy_check(void) //检查是否忙碌
{
LCD_Data = 0xFF;
LCD_E = 0;
LCD_RS = 0;
LCD_RW = 1;
LCD_E = 1;
while (LCD_Data & Busy);
LCD_E = 0;
}
这里比较难以理解的部分,主要是X,Y如何转化为地址。X 为横坐标,Y为纵坐标,X的取值范围为0-F,Y的取值范围为0-1;
在函数DisplayOnechar()中,X与0XF相与,Y与0X1相与,这样做的意义是为了确保地址是否在合适范围内。
接下来判断Y的大小,Y=1,即在第2行的时候,还需要将X与0X40相或,得到第2行内的地址。(第2行地址在0X40-0X4F)
例如当X=2,Y=1时,2 | 0X40 =0000 0010 | 0010 0000 =0010 0010 =0X42
写入数据的时候,是先写入指令,在写入数据。
stc12c5a60s2 (main.c文件)
#include "reg51.h"
#include "lcd1602.h"
#include "intrins.h"
typedef unsigned int WORD;
sbit ctrl5v = P2^3;
sbit ctrl10v = P3^5;
sbit ctrl15v = P3^6;
sbit ctrl20v = P3^7; //µ²Î»µ÷Õû
sbit beep = P2^7;
sfr ADC_CONTR = 0xBC; //ADC control register
sfr ADC_RES = 0xBD; //ADC high 8-bit result register
sfr ADC_LOW2 = 0xBE; //ADC low 2-bit result register
sfr P1ASF = 0x9D; //P1 secondary function control register
#define ADC_POWER 0X80
#define ADC_FLAG 0X10
#define ADC_START 0X08
#define ADC_SPEEDLL 0X00
#define ADC_SPEEDL 0X20
#define ADC_SPEEDH 0X40
#define ADC_SPEEDHH 0X60
这是main.c文件,其中ADC_CONTER为ADC控制寄存器的地址,共8位。
实际使用的时候,在头文件“STC12C5A60S2.h”内已经定义,无需自己定义,请参考数据手册。
第一位二进制:
代表A/D转换器电源的开关,1表示开,0表示关。关闭A/D转换器表示 ADC_CONRT=0x00,开启表示为 ADC_CONRT=0x80。
第二、三位二进制:
代表A/D转换的速度,00代表540个时钟周期转换一次,01代表360个时钟周期转换一次,10代表180个时钟周期转换一次,11代表90个时钟周期转换一次。
第四位二进制:
代表A/D转换器的结束标志位,该标志位在A/D转换结束后自动置1,需要代码清零;
第五位二进制:
代表A/D转换器的启动控制位,置1的时候开始转换,转换结束为0.;
第六、七、八位二进制:
代表模拟输入通道的选择,000表示P1.0,001表示P1.1,以此类推。
这里的写法是define了很多参数,如#define ADC_POWER 0x80,实际需要打开ADC电源或者启动ADC时,只需要将各个参数相或,得到结果赋值给ADC_CONTER。上述所说的第一位,即是从左往右第一位。
main文件的函数包括这些:
void InitADC();
void ctrlinit(BYTE da);
void Delay(WORD n);
WORD GetADCResult(BYTE ch);
void DisplayADCResult(BYTE Channel);
float AD_average(BYTE Channel)
InitADC()函数,用于初始化ADC;
ctrlinit()函数,用于挡位初始化,这个具体如何初始化还得看电路图里的分压是如何处理的;因为单片机只能测试0-5V的范围,要想测试20V左右的电压,需要电阻分压处理后,将电压控制在5V以内,测出以后,再将数据乘以分压比,就能得到真实电压;
Delay(),延时函数;
GetADCResult(),获取ADC的输出结果;
DisplayADCResult,显示ADC的输出结果,这里需要调用LCD1602.h内的函数;
AD_average,多次采样后求平均值,因为并不可能十分迅速的读取数据显示数据,所以考虑将一段时间内的采样值处理后显示;
下面我们一一细看
--------------------------------------------------------------------------------------------------------------------------------
void InitADC()
{
P1ASF = 0XFF;
ADC_RES = 0;
ADC_CONTR = ADC_POWER|ADC_SPEEDLL;
Delay(2);
}
void Delay(WORD t)
{
WORD x;
while(t--)
{ x = 5000;
while(x--);}
}
InitADC()函数,主要打开P1口的ADC模拟功能,实际也可以像0x01这样打开P1.0口,将ADC_RES初始化为0;
void ctrlinit(unsigned char da)
{
switch (da)
{
case 0:ctrl5v=0;ctrl10v=1;ctrl15v=1;ctrl20v=1;break;
case 1:ctrl5v=1;ctrl10v=0;ctrl15v=1;ctrl20v=1;break;
case 2:ctrl5v=1;ctrl10v=1;ctrl15v=0;ctrl20v=1;break;
case 3:ctrl5v=1;ctrl10v=1;ctrl15v=1;ctrl20v=0;break;
}
}
对给定的通道进行判断处理,然后打开相连的挡位,0是打开,从0开始判断是因为在GetADCResult函数内,需要与通道ch相或,而P1.0口对应的是000,P1,0对应的是001。
WORD GetADCResult(BYTE ch)
{
ADC_CONTR=ADC_POWER|ADC_SPEEDLL|ch|ADC_START;
_nop_();
_nop_();
_nop_();
_nop_();
while(!(ADC_CONTR&ADC_FLAG));
ADC_CONTR&=~ADC_FLAG;
return ADC_RES*4+ADC_LOW2; }
这里的WORD,就是unsigned char,在前文已经被define过,不再多说;
nop()是头文件"intrins.h"头文件里的空隙处理,大概是一个空指令;
需要注意的是,ADC_FLAG在ADC转换完成后,会置为1,需要及时复位;
ADC_FLAG=0x10=0001 0000,相与的时候,如果转换完成,则while()循环内的结果为0,执行下一步,将ADC的标志位复位;
float AD_average(BYTE Channel)
{
float value=0;
BYTE count;
for(count=0;count<100;count++)
{
value+=GetADCResult(Channel); } //100
value/=100;
value=value*5.0/1024; //val/1024*5
return value;
}
这里对ADC采样一百次,然后除以100,最后根据公式计算出结果;公式为何这样计算有待探究;
void DisplayADCResult(BYTE Channel)
{
float ADResult;
WORD Result;
ADResult=AD_average(Channel);
switch (Channel)
{
case 0:break; //5Vµ²Î»
case 1:ADResult*=2;break;
case 2:ADResult*=4;break;
case 3:ADResult*=12;break;
}
if(ADResult>=20)
beep=0;
else
beep=1;
switch ((int)(ADResult/5)) //对测试出的数据进行辨别,属于哪个挡位
{
case 2:channel=2;break;
case 1:channel=1;break;
case 0:channel=0;break;
default:channel=3;
}
if(ADResult<10){ //小于10的话,就只有一个个位数
ADResult=ADResult*100;
Result=(int)ADResult;
bai=Result/100;
shi=Result%100/10;
ge=Result%10;
DisplayOneChar(5,0,20); //20
DisplayOneChar(6,0,table[bai]);
DisplayOneChar(7,0,'.');
DisplayOneChar(8,0,table[shi]);
DisplayOneChar(9,0,table[ge]);
DisplayOneChar(10,0,'V');}
else // ´大于等于10的话,有两位整数部分
{
ADResult=ADResult*100;
Result=(int)ADResult;
qian=Result/1000;
bai=(Result/100)%10; // 上下两者计算百位的方式不太一样
shi=Result%100/10;
ge=Result%10;
DisplayOneChar(5,0,table[qian]);
DisplayOneChar(6,0,table[bai]);
DisplayOneChar(7,0,'.');
DisplayOneChar(8,0,table[shi]);
DisplayOneChar(9,0,table[ge]);
DisplayOneChar(10,0,'V');}}
channel的选择根据你的挡位来选择,我采用的是1:2:3:6的电阻分压,对于ADResult,要得到真实的电压数据,还要根据分压情况将ADResult乘以比例,所以出现了对channel的switch语句;
beep是蜂鸣器,即不允许实际电压超过20v,超过就报警,当然其实也是可以测试的,因为1:2:3:4,最大的挡位测的是电阻比为1到地的电压差,真实电压要乘以12,那么最多测试的真实电压大概是5*12=60v(单片机最多测5v)
左边是四个挡位,对应的就是四个挡位开关,当三极管导通的时候,电磁开关吸合,挡位开关断开,就无法从P1引脚读入数据。这里的思路是开始时,就打开最大的挡位0-20v,先测试出一次数据,然后根据数值的大小来选择后续打开哪个挡位,这样测试的精度可能更好。当然需要考虑的一个问题是,我在InitADC内将P1口所有的端口都写成了ADC模拟功能,实际并不需要这么多;
第二个switch用来及时改变channel的值,当然并不影响本次电压的读取,只有在下一次读取电压的时候,channel的改变才会体现出来;
接下来准备显示数据的时候,需要判断电压的整数部分是有2位还是1位;可以看到代码中对于16.32这样的数据,有2位整数部分的时候,乘以100,得到1632,然后挨个取出数据;1632/1000=1; (1632/100)%10=6;1632%100/10=3;1632%10=2;
以此类推,我们也可以得到1位整数部分的时候,如何处理数据;这样就在我们想要的X,Y坐标上写入数据,并显示数据;
最后在main函数里我们只需要初始化并运行isplayADCResult(),注意channel值是一个全局变量,
void main()
{
WORD i=0;
InitADC();
LCDInit();
ctrlinit(3);
DisplayListChar(0, 0, "Volt:");
Delay(3);
while(1)
{
i++;
if(i==500)
{
ctrlinit(channel);
DisplayADCResult(channel);
i=0;
}
}
}
在调整挡位的时候,我们将channel及时改变并打开相应挡位,如果有问题的话,可以考虑不必调整挡位;
--------------------------------------------------------------------------------------------------------------------------------
二 结束语
将以上代码快速阅读,并加以使用,大概需要几个小时就能完成这样一个设计了;
有出错的部分请不吝赐教,谢谢支持!;