lcd1602+STC12C5A60S2,实现电压0-20v读取---讲解细致

目录

一、了解器件性能,查询资料

   LCD1602

 stc12c5a60s2 (main.c文件)

二   结束语


一、了解器件性能,查询资料

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位。

 数据手册---宏晶科技http://www.stcmcudata.com/datasheet/stc/stc-ad-pdf/stc12c5a60s2.pdficon-default.png?t=N5F7http://www.stcmcudata.com/datasheet/stc/stc-ad-pdf/stc12c5a60s2.pdf

实际使用的时候,在头文件“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及时改变并打开相应挡位,如果有问题的话,可以考虑不必调整挡位;

--------------------------------------------------------------------------------------------------------------------------------

二   结束语

       将以上代码快速阅读,并加以使用,大概需要几个小时就能完成这样一个设计了;

       有出错的部分请不吝赐教,谢谢支持!;

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

弃梓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值