STC15单片机实现ADC采集环境温度(NTC热敏电阻)
ADC获取环境温度
STC15L2K32S2 ADC结构
STC15系列单片机ADC由多路选择开关、比较器、逐次比较寄存器、10位DAC、转换结果寄存器(ADC_RES和ADC_RESL)以及ADC_CONTR构成。
STC15系列单片机的ADC是逐次比较型ADC。逐次比较型ADC由一个比较器和D/A转换器构成,通过逐次比较逻辑,从最高位(MSB)开始,顺序地对每一输入电压与内置D/A转换器输出进行比较,经过多次比较,使转换所得的数字量逐次逼近输入模拟量对应值。逐次比较型A/D转换器具有速度高,功耗低等优点。
ADC的使用也是通过配置寄存器来实现的,简单来说该结构对应的寄存器是ADC_CONTR,每一位对应的功能如下:
ADC_POWER:电源控制位,置1则打开A/D转换器电源,置0则关闭转换器电源
SPEED1和SPEED0:模数转换器转换速度选择位,两位对应4种选项,分别是90个时钟,180个时钟,360个时钟,540个时钟转换1次,速度可根据需要选择,本次实验选用180个时钟转换1次
ADC_FLAG:模数转换器转换结束标志位,当A/D转换完成后,ADC_FLAG = 1,要由软件清0。不管是A/D转换完成后由该位申请产生中断,还是由软件查询该标志位A/D转换是否结束,当A/D转换完成后,ADC_FLAG = 1,一定要软件清0
ADC_START:模数转换器(ADC)转换启动控制位,设置为“1”时,开始转换,转换结束后为0。
CHS2/CHS1/CHS0:模拟输入通道选择,因为该型号单片机的整个P1口都带有ADC的功能,那使用时总得选1个吧,所以这3位就是用来选择8个通道中的其中1个的
ADC相关寄存器
P1ASF也是用来选择通道的,ADC_CONTR上面有介绍,ADC_RES和ADC_RESL是用来存放转换结果的,CLK_DIV(PCON2)寄存器只用到了第5位ADRJ,
ADRJ置1时:ADC_RES[1:0]存放高2位ADC结果,ADC_RESL[7:0]存放低8位ADC结果;
ADRJ置0时:ADC_RES[7:0]存放高8位ADC结果,ADC_RESL[1:0]存放低2位ADC结果;
IE是控制ADC中断的,IP是设置ADC优先级的,这次实验中没有用到中断
更详细介绍只能看手册了
NTC热敏电阻
NTC是英文Negative Temperature Coefficient的缩写。其含义为负温度系数,它的电阻值随温度的升高而降低。利用这一特性既可制成测温、温度补偿和控温组件,又可以制成功率型组件,抑制电路的浪涌电流。这是由于NTC热敏电阻器有一个额定的零功率电阻值,当其串联在电源回路中时,就可以有效地抑制开机浪涌电流。并且在完成抑制浪涌电流作用以后,利用电流的持续作用,将NTC热敏电阻器的电阻值下降非常小的程度。
本次实验使用到的NTC热敏电阻电气特性,因为这涉及到了硬件选型,了解一下,主要注意B值,10千欧,3950,精度1%这几个标签
开发时特别要注意看热敏电阻温度阻值对照表,即多少阻值对应多少的温度值的表,一般在产品介绍手册中厂家会给出,如下面这只是一部分,不同的热敏电阻的测温范围也不同,最小值、中心值和最大值的意思是有误差,在最小和最大范围内的都可认为是同一温度
数据计算
NTC电阻值 ——> 输入到ADC电压值
温度特性表阻值对应的温度已经知道了,但光知道热敏电阻的阻值是没法进行ADC转换的,因为引脚输入的是电压,所以要把这些阻值转换为对应的引脚输入电压值,通过阻值计算电压就要根据实际的硬件电路来算,本次实验所用到的硬件电路图如下
输入到ADC的电压值 = (RT1/(RT1+10K)) * 3.3
RT1就是温度特性表中每一个温度值对应的电阻值,分别计算出最小、中间、最大的电压值
输入到ADC的电压值 ——> ADC转换结果(采集值)
然后就是经过ADC转换后的采集值,这个采集值是单片机计算出来的,放到了ADC_RES[1:0]和ADC_RES[7:0]中,数据手册中有给出计算公式,Vin就是上一步计算出的输入到ADC的电压值,VCC为单片机工作电压3.3V,因为该ADC是10位的,所以乘以1024
全部计算在一张execl表中
可将pdf的温度特性表转为execl表,然后用公式算出电压值,然后再根据电压值算出ADC的转换结果,也就是采集值,因为这些采集值是浮点型的,而单片机处理浮点型的数据是非常耗时间的,所以经过四舍五入后,得到整型的采集值;
后续在程序中,就要将整形的采集值做成一个二位数组,判断ADC的转换结果在哪一个范围内,对应的温度值就清楚了
程序
文件结构
main.c ->主函数文件,包含main函数等;
Public.c ->公共函数文件,包含Delay 延时函数等;
Sys_init.c ->系统初始化函数,包含GPIO初始化函数等;
ADC.c->ADC初始化,采集 ADC值等;
NTC.c ->NTC外设函数,包含查表,获取环境温度等;
实验结果
间隔1000ms 通过ADC获取NTC传感器的电压
通过查表,获取环境温度,并打印到串口上
ADC.h
主要是ADC转换速度宏定义,标志位,以及结构体类型
#ifndef __ADC_H_
#define __ADC_H_
//宏定义转换速度
#define ADC_SPEED_90 0x60
#define ADC_SPEED_180 0x40
#define ADC_SPEED_360 0x20
#define ADC_SPEED_540 0x00
#define ADC_POWER 0x80 //ADC电源控制位
#define ADC_FLAG 0X10 //ADC完成标志位,需要软件清零
#define ADC_START 0X08 //ADC启动标志位,置'1'开始转换
//定义结构体类型
typedef struct
{
uint16_t ADC_Value; //ADC采集值
void (*ADC_Init)(); //ADC初始化
uint16_t (*ADC_Result)(); //ADC转换结果
uint16_t (*ADC_Filter_Result)(); //ADC转换滤波后的结果
}ADC_t;
/* extern variables-----------------------------------------------------------*/
extern ADC_t idata ADC;
/* extern function prototypes-------------------------------------------------*/
#endif
/********************************************************
End Of File
********************************************************/
ADC.c
实现ADC初始化函数,实现转换并获取转换结果函数,为了确保获取数据的稳定性,实现一个多次读值取平均值的滤波算法
/* Includes ------------------------------------------------------------------*/
#include <main.h>
/* Private define-------------------------------------------------------------*/
#define NTC_CHAN 0x04 //NTC传感器输入通道(CHS2、CHS1、CHS0)
/* Private variables----------------------------------------------------------*/
static void ADC_Init();
static uint16_t ADC_Result();
static uint16_t ADC_Filter_Result();
/* Public variables-----------------------------------------------------------*/
ADC_t idata ADC =
{
0,
ADC_Init,
ADC_Result,
ADC_Filter_Result
};
/* Private function prototypes------------------------------------------------*/
/*
* @name ADC_Init
* @brief ADC初始化
* @param None
* @retval None
*/
static void ADC_Init()
{
//端口1模拟功能配置寄存器
P1ASF = BIT4; //将P1.4口设置为模拟功能A/D使用
//A/D转换控制寄存器 1100 0100
ADC_CONTR = ADC_POWER|ADC_SPEED_180|NTC_CHAN;
Public.Delay_ms(2); //等待ADC电源稳定
//ADRJ置‘1’,ADC_RES[1:0]存放高2位ADC结果,ADC_RESL[7:0]存放低8位ADC结果
CLK_DIV |= 0x20;
}
/*
* @name ADC_Result
* @brief ADC转换结果
* @param None
* @retval 返回ADC转换结果
*/
static uint16_t ADC_Result()
{
uint16_t ADC_Result = 0;
//清零转换结果
ADC_RES = 0;
ADC_RESL = 0;
//启动ADC
ADC_CONTR |= ADC_START;
_nop_(); //手册建议添加四个延时,等待ADC转换完成
_nop_();
_nop_();
_nop_();
while(!(ADC_CONTR & ADC_FLAG)); //查询方式等待ADC转换结果
//清除ADC转换完成标志位
ADC_CONTR &= (~ADC_FLAG);
//获取转换结果 :ADC_RES高2位+ADC_RESL低8位
ADC_Result = (ADC_RES & 0x03); //取出ADC_RES[1:0]存放的高2位数据
ADC_Result <<= 8; //左移8位,将取出的2位高位数据放到第9,第10位
ADC_Result += ADC_RESL; //加上低8位的数据,组合成一个16位的数据
return ADC_Result;
}
/*
* @name ADC_Filter_Result
* @brief ADC转换滤波后的结果(滤波函数,以后也可以借鉴使用)
* @param None
* @retval 返回ADC转换结果
*/
static uint16_t ADC_Filter_Result()
{
//经过滤波算法后返回的结果就会很准确,遵循产品的稳定性原则
/*循环读取AD值16次后求平均值并返回结果,而这16次里每一次又是8次读取后取的平均值*/
uint16_t ADC_min,ADC_max,ADC_temp,ADC_result,ADC_return;
uint8_t i,j;
ADC_return = 0;
for(i=0;i<16;i++)
{
ADC_result = 0;
ADC_min = ADC_max = ADC.ADC_Result();
for(j=0;j<8;j++)
{
ADC_temp = ADC.ADC_Result();
if(ADC_temp < ADC_min)
{
ADC_result += ADC_min;
ADC_min = ADC_temp;
}
else if(ADC_temp > ADC_max)
{
ADC_result += ADC_max;
ADC_max = ADC_temp;
}else
{
ADC_result += ADC_temp;
}
}
ADC_result /= 8;
ADC_return += ADC_result;
}
ADC_return /= 16;
Run_LED.Run_LED_Flip();
return ADC_return; //返回经过滤波后的采集值
}
/********************************************************
End Of File
********************************************************/
NTC.h
#ifndef __NTC_H_
#define __NTC_H_
//定义结构体类型
typedef struct
{
void (*Get_NTC_Voltage)(); //获取NTC电压值
float fNTC_Voltage; //NTC电压
void (*Get_Temperature_Value)(); //获取温度值
float fTemperature; //温度
}NTC_t;
/* extern variables-----------------------------------------------------------*/
extern NTC_t idata NTC;
/* extern function prototypes-------------------------------------------------*/
#endif
/********************************************************
End Of File
********************************************************/
NTC.c
主要是把上面execl表中的整型采集值做成一个二维数组,这需要点时间,而这里的数据又将之前的数据细分了,分出了0.5°来,二维数组里的数据只要大概在表中某一温度值对应的采集值范围内即可,不要超过最大采集值和最小采集值
示例代码到二位数组删减了,放不了这么多
而程序是直接得出转换后的采集值的,那如果得到NTC热敏电阻的电压呢,就要通过采集值倒推出电压值了,计算公式:电压值=(3.3 * ADC采集值)/1024;分别获取到ADC采集值和电压值后,就可打印到串口上显示
Get_Temperature_Value函数就是通过二分法,在二维数组中找采集值处于二维数组中的哪个下标位置,因为二位数组下标为0的对应温度是-30℃,下标为1是-29.5℃,依次类推,所以获取到下标值后,可以通过找数组下标与温度值的规律,得出计算公式,最后打印温度值
/* Includes ------------------------------------------------------------------*/
#include <main.h>
/* Private define-------------------------------------------------------------*/
#define SW_NTC P13 //MOS开关驱动引脚
#define SW_NTC_ON (bit)1 //打开MOS开关
#define SW_NTC_OFF (bit)0 //关闭MOS开关
/* Private variables----------------------------------------------------------*/
/*NTC型号:B3950 10K 1%
温度表,对应温度-30℃ ~ 70℃
Note:如果采集值 > 966,则温度低于-30℃;采集值 < 145,则温度高于70℃
*/
const uint16_t xdata NTC_Table[201][2] =
{
//温度 -30 -29.5 -29 -28.5 -28 -27.5 -27 -26.5 -26 -25.5
{964,966},{962,963},{960,961},{958,959},{957,957},{955,956},{953,954},{951,952},{949,950},{947,948},{945,946},{943,944},.......{169,170},{166,168},{164,165},{162,163},{160,161},{158,159},{155,157},{152,154},{147,151}
};
static void Get_NTC_Voltage();
static void Get_Temperature_Value();
/* Public variables-----------------------------------------------------------*/
NTC_t idata NTC =
{
Get_NTC_Voltage,
0.0,
Get_Temperature_Value,
0.0
};
/* Private function prototypes------------------------------------------------*/
/*
* @name Get_NTC_Voltage
* @brief 获取NTC电压值
* @param None
* @retval None
*/
static void Get_NTC_Voltage()
{
//打开MOS开关
SW_NTC = SW_NTC_ON;
//等待电压稳定
Public.Delay_ms(1);
//获取ADC采集值
ADC.ADC_Value = ADC.ADC_Filter_Result();
//条件编译
#ifdef Monitor_Run_Code
printf("The ADC Value is: %d\r\n",ADC.ADC_Value);
#endif
//通过获取到的ADC采集值,倒推出NTC的电压值
//VCC:3.3伏
//10位ADC结果:ADC.ADC_Value
//求Vin
NTC.fNTC_Voltage = (3.3 * ADC.ADC_Value)/1024;
#ifdef Monitor_Run_Code
printf("The NTC Voltage is: %.2fV\r\n",NTC.fNTC_Voltage);
#endif
//关闭MOS开关,NTC电源断开,停止工作
SW_NTC = SW_NTC_OFF;
}
/*
* @name Get_Temperature_Value
* @brief 获取温度值
* @param None
* @retval None
*/
static void Get_Temperature_Value()
{
static uint8_t Temp;
//获取采集值,计算温度等
NTC.Get_NTC_Voltage();
//临界温度处理
if(ADC.ADC_Value < 147) //温度最高70℃
{
ADC.ADC_Value = 147;
#ifdef Monitor_Run_Code
printf("Temperature is higher than 70℃\r\n");
#endif
}
if(ADC.ADC_Value > 966) //温度最低-30℃
{
ADC.ADC_Value = 966;
#ifdef Monitor_Run_Code
printf("Temperature is below than -30℃\r\n");
#endif
}
//二分法查表
if(ADC.ADC_Value > NTC_Table[101][1])
{
//查询数组下标0 - 100,对应-30℃至 20℃
for(Temp=0;Temp<=100;Temp++)
{
//如果采集值在这些一维数组的范围内,则表示找到对应温度值,用break跳出循环
if((ADC.ADC_Value >= NTC_Table[Temp][0]) && (ADC.ADC_Value <= NTC_Table[Temp][1]))
{
break;
}
}
}
else
{
//查询数组下标101 - 201,对应20℃至70℃
for(Temp=101;Temp<=200;Temp++)
{
if((ADC.ADC_Value >= NTC_Table[Temp][0]) && (ADC.ADC_Value <= NTC_Table[Temp][1]))
{
break;
}
}
}
//计算温度
//下标(Temp) 温度
// 0 -30
// 1 -29.5
// 2 -29
// 3 -28.5
// 4 -28
//通过比较数组和下标的关系,找出计算温度值的规律
NTC.fTemperature = (Temp/2.0)-30;
#ifdef Monitor_Run_Code
printf("Temperature is: %.1f ℃\r\n\r\n",NTC.fTemperature);
#endif
}
/********************************************************
End Of File
********************************************************/
main.c
程序中获取到数据的过程:最底层的ADC_Result()获取到采集值,ADC_Filter_Result()滤波算法得到更精确的采集值,Get_NTC_Voltage()通过采集值倒算出电压值,Get_Temperature_Value()在数组中找到采集值对应的温度,最后将温度通过串口打印
函数调用关系:main() -> Get_Temperature_Value() -> Get_NTC_Voltage() -> ADC_Filter_Result() -> ADC_Result()
/*
* @name main
* @brief 主函数
* @param void
* @retval int
*/
int main(void)
{
//系统初始化
Hradware.Sys_Init();
//串口1发送初始化信息
#ifdef Monitor_Run_Code
printf("Initialization completed,system startup!\r\n\r\n");
#endif
//系统主循环
while(1)
{
NTC.Get_Temperature_Value();
Public.Delay_ms(1000);
}
}
看教程时需要注意的点
NTC部分的CMFB103F3950FANT是热敏电阻,名称是NTC热敏电阻,25度情况下,阻值是10千欧
R6是一个分压电阻,阻值与RT1相同,为了让两者中间的采集电压不能太小,R6精度是1%
C7电容,起滤波作用,稳定电压,C8电容也是起稳定电压作用
因为要求低功耗,所以要有一个MOS开关U4,控制电源,关闭时后MCU_3V3就不会有电流流向RT1,热敏电阻不工作
文件结构中将ADC和NTC分开编程,因为ADC是芯片内部的资源,NTC是外部的,下次使用的可能不是NTC,可能是别的传感器,所以分开处理比较好,易于移植
需要根据手册制作execl表,选择-30度到70度,电压值计算公式:RT1/(RT1+10K)*3.3V,采集值计算公式:1024 *(Vin/Vcc),Vin就是前面计算的电压值,Vcc是3.3V,然后再将浮点型的采集值四舍五入转化为整型值,最后在程序中判断在这些整型值的哪个范围,就可以推出温度是多少
示例代码中将整型值写成一个二维数组,共201个,每个一维数组表示的温度范围是0.5度
上电串口输出初始化信息添加宏定义,调试时打开,出产品时注释掉
与硬件挂钩的定义,比如引脚定义,尽量用宏定义,方便移植更改
P13口设置为推挽输出,P14设置为高阻输入,Sys_Init()方法中记得加ADC初始化
可能出现的故障:
如果采集的电压一直是3.3V,那可能的故障是,MOS开关没有打开,或者虚焊,或者NTC传感器没接,导致一直是MCU_3.3V流进P14口
如果采集的一直是0V,那R6电阻可能虚焊
如果采集的电压不稳定,那R7电阻可能虚焊或没接,导致线路悬空不稳定
需要得到的数据:采集值((电压值*1024)/3.3)-> ADC_RES,ADC_RESL,电压值((电阻值/(电阻值+10)) * 3.3),温度
遇到的问题
问题1
编写好ADC和NTC的程序后,主函数调用NTC.Get_Temperature_Value()函数,里面有用 printf 函数重定向串口打印采集值、热敏电阻电压和温度,但打开串口却看不到任何信息
解决方法:
1.要在串口初始化函数Init()中,将串口切换寄存器AUXR1的S1_S1(BIT7)和S1_S0(BIT6)位都清零,才能将串口1切换到P3.0和P3.1口,因为串口的CH340芯片接的就是单片机P3.0和P3.1引脚
2.要在系统初始化文件Sys_Init.c的GPIO初始化函数中,将之前做RS-485通信时,P3.0和P3.1口初始化为0的操作删除
P3 &= (~(BIT0|BIT1|BIT4|BIT5)); //RS-485项目P3口初始化原来写法
P3 &= (~(BIT4|BIT5)); //修改为不对BIT0和BIT1初始化为0
问题2
解决第一个问题后,串口能输出ADC采集值、NTC电压和温度,在串口助手查看到单位℃出现乱码情况,而且输出一轮换行时本来两次 \r\n 就可实现空出一行,现在却要三次 \r\n 才能实现,如下图所示
printf("Temperature is: %.1f ℃\r\n\r\n\r\n",NTC.fTemperature); //三次\r\n才能实现空一行的效果,本来两次就能实现
℃单位乱码和两次 \r\n 无法空一行的情况:
解决思路及过程
一开始先看波特率是否对应,发现是正确的,再一想既然其他打印信息都能正确输出,那波特率是没有问题的;然后去查看代码是否有写错,串口通信的全部函数也检查了一遍,发现没有问题,并且也拿之前写过的串口通信程序对照着检查了一遍,程序一摸一样;但我拿教程的代码烧录后,发现串口通信是正常的,℃能正常打印,使用的是两次 \r\n ,而自己写的串口这一部分代码是与教程一样的,为什么代码一样,串口输出会有乱码现象呢?头一次遇到这种问题
然后想着去查看STC-ISP串口助手那一块是否有可以设置的地方,猜测自己的串口助手与教程的某项设置不一样,导致乱码,后来完全没发现STC-ISP烧录工具有可以设置串口相关参数的地方,除了波特率,校验位和停止位,其他没得设置,波特率对应了,乱码的问题与校验位和停止位关系应该也不大,如果校验位和停止位有错,应该整个通信都有问题,而不只是这一个℃单位
后来猜测到,既然烧录教程的代码没问题,烧录自己的代码有问题,而内容却一样,那我把教程代码串口打印输出这一块的全部内容复制到自己的代码中,是否还有乱码现象呢?若没有乱码,说明就是自己的代码还有问题;若还有乱码,那可想到这部分代码在教程的文件中没有问题,而把内容复制到我自己的文件中后却出现了问题,那这个文件是不是有点问题?
为了进一步验证是不是文件的锅,因为打印的信息都在NTC.c源文件里,就先把教程的NTC.c里的内容全部覆盖到自己本来的NTC.c中,然后编译烧录,发现仍然出现乱码,这就说明问题了,这些代码在教程的文件中没出问题,在我自己创建的文件中就出现乱码(因为是乱码现象,所以确定逻辑功能都是正确的);后续我把教程一整个NTC.c文件拷贝到自己的工程中,编译烧录,没有出现乱码现象,所以说明文件大概率是有问题的
既然文件可能有问题,那是不是刚开始创建时就不对了呢,然后突然意识到,我从面向对象的代码开始,用的编译器是VS Code,而新建文件也是用VC code创建的,不是用keil来创建文件,会不会与这个有关系呢?然后分别做了测试,把这次实验整个拷贝两份,一份备注为VS code ,另一份备注为 keil,然后分别把NTC.c文件删除,一份用VS code打开并新建NTC.c文件,粘贴自己写的代码,另一份用keil打开并新建NTC.c文件,也是粘贴自己的代码,然后都用keil来编译,最后烧录看效果
串口打印的关键代码就这样写,有℃单位,两次 \r\n
printf("Temperature is :%.1f℃\r\n\r\n",NTC.fTemperature);
VS code创建的NTC.c文件,烧录后串口输出情况:
Keil创建的NTC.c文件,烧录后串口输出情况:
可看到,用VS code新建NTC.c文件的是会出现乱码现象,而用keil新建的就不会,这两个工程代码完全一样,就是新建文件时用的软件不同,居然会出现这种结果
这下问题就解决了,原来是VS code编译器新建文件时出的问题,这种情况还真第一次遇到,用不同软件新建的文件都会导致最后运行结果出错;前面实验新建文件也是用的VS code软件,可能并没有用到串口或没太注意,直到这次串口输出信息乱码时才发现问题所在,至于为什么VS code新建文件会有问题,网上也没查到,以后还是用keil来新建文件保险一点
在后续串口通信实验中发现了问题的原因
原来VS code编写代码时新建的文件编码格式与keil软件的不同,导致串口发送的数据出现乱码,在用VS code编写代码前统一文件编码格式即可