ADC相关算法以及热敏电阻测温

server/2025/1/1 3:43:45/

目录

前言

一、平均值滤波算法

二、快速排序算法的使用

三、中位值滤波算法

四、二分查找法

4.1 二分查找法查找某个元素是否存在

 4.2 二分查找法查找接近目标数值的元素的下标

五、NTC热敏电阻实现测温

5.1 分层设计

5.2 软件流程图

​编辑 5.3 API接口及数据结构

5.4 热敏电阻模块原理图

5.5 热敏电阻工作原理


前言

        本文章介绍在使用STM32F103片上外设ADC时,可能会用到的一些算法,平均值滤波算法、中位值滤波算法、快速排序算法、二分查找法。并且实现了使用热敏电阻测量温度,串口打印显示。使用到的资源如下:

PA0用于热敏电阻的ADC,PB10和PB11用于串口。

        相关的代码和资料请点击下方链接查看:

https://jcnwdt8hb184.feishu.cn/wiki/AUXQwtZ6AipW16kUAn0cIi0anMe?form_wx_login=1

一、平均值滤波算法

        ADC转换的数据是在动态变化的,这个范围可能不是很大,比如采集温度数据,ADC转换的值每次都可能浮动零点几,24.2、24.5、24.1、24.2。每次转换数字都在变化,这样的数字如果显示在LCD等显示屏幕上,会造成非常不好的体验,数字一会儿跳一会跳。所以就需要采用ADC滤波算法。这里介绍常用的平均值滤波算法,在我提供的资料里,有一份文档,上面写了近10种滤波算法,具体使用哪一种还要根据自己的项目确定。

        平均值滤波算法十分简单,就是把转换的数据放到数组里面,最终显示到屏幕上的数字是数组内的元素的平均值,这样显示的数据就会稳定。

static uint16_t ArithAvgFlter(uint16_t* arr, uint32_t len)
{uint32_t sum = 0;for (uint32_t i = 0; i < len; i++){sum += arr[i];}return (uint16_t)(sum / len);
}

        代码在调试的时候,遇到了一个问题,当代码里的波特率设置为9600(串口助手也为9600)时,打印显示的信息会错行,而当都设置为115200的时候,就不会错行,如下图所示。

        后来经过PN学堂各位老师和同学的指点,我才知道是串口助手里“超时时间”的问题。在波特率9600的时候把超时时间设置大一点就不会出错了。那么什么是超时时间呢?

在串口助手中,“超时时间”是指在接收串口数据时等待的最大时间。
比如说,设置超时时间为1000毫秒。当串口助手开始等待接收数据时,它会在1000毫秒内一直处于等待接收状态。如果在这1000毫秒内有数据到来,就正常接收处理;要是超过1000毫秒还没有接收到数据,就会停止等待,这种机制可以避免程序一直处于无期限的等待状态,从而提高通信效率和程序运行的稳定性。

判断串口助手超时时间设置是否合理,可以考虑以下几点:
通信速率
- 如果串口通信波特率高、数据传输快,超时时间可以设置得较短。例如,波特率为115200bps时,传输一个字节所需时间很短,超时时间设几十毫秒可能就足够了。
- 对于较低波特率(如9600bps),传输一个字节时间较长,超时时间可能需要设置到几百毫秒,以确保能完整接收数据。
 
数据量和数据发送频率
- 当每次传输的数据量较小且发送频率低时,较短超时时间(如50 - 100毫秒)或许就能满足需求。
- 若数据量较大或者数据发送很频繁,为避免接收不完整,需要适当延长超时时间,确保接收缓冲区能接收完整的数据块。
 
系统响应要求
- 如果系统对数据的实时性要求高,希望快速处理接收到的数据,超时时间不宜过长。比如工业控制的实时监控系统,超时时间过长可能会导致系统响应延迟。
- 对于对实时性要求不高的系统,可适当放宽超时时间设置。
 
设备性能
- 设备处理速度快、资源充足时,能快速处理接收的数据,可以设置较短超时时间。
- 对于性能较差的设备,适当延长超时时间可避免因处理不及时而丢失数据。
 
可以通过实际测试来验证设置是否合理。在不同的超时时间设置下,发送和接收一系列已知的数据,观察数据接收的完整性、准确性和系统响应情况。如果数据接收完整、系统响应及时,就说明当前超时时间设置比较合理。 

二、快速排序算法的使用

        在使用ADC的时候,不可避免的需要对数据进行排序,以前学过冒泡排序,但是在工作中,常使用的是快速排序,接下来就介绍快速排序的使用方法,具体如何实现的不做介绍,如果读者想要了解,可以去查看“数据结构与算法”相关的书籍,里面是有介绍的。

        在C标准库中提供了快速排序的接口函数,需要包含头文件stdlib.h。这个排序函数不仅可以排序普通数组,还可以排结构体数组。

         第一个参数是“数组首地址”,第二个参数是“数组元素个数”,第三个参数是“数组元素所占用内存空间大小”,第四个参数是“用户自己定义的回调函数的地址,即函数指针变量”。

        前三个参数都好理解,关键是第四个参数,这个参数需要我们自己定义一个回调函数,传入这个回调函数的地址的作用是告诉qsort排序规则是升序还是降序,函数中的两个参数是数组元素的地址,所以画红圈的部分表示,这两个参数的类型必须与你要排序的数组的类型一致,比如你要对uint32_t buf[10]排序,那么红圈部分就是uint32_t*。下图是升序规则,如果把if和else if里的内容互换就是降序规则。

        示例代码参考文章开头的链接。

三、中位值滤波算法

        所谓的中位值算法就是去掉数据当中的最大值和最小值,再对剩余数据取平均值,显而易见的,该算法需要联合使用平均值滤波算法和快速排序算法

#include "stm32f10x.h"                  // Device header
#include "stdio.h"
#include "stdlib.h"
#include "uart_drv.h"static int32_t g_dataBuf[6] = {-1, -4, 0, 12, 23, 4};static int32_t CompareCallBack(const void* _a, const void* _b)//升序规则
{int32_t* a = (int32_t*) _a;int32_t* b = (int32_t*) _b;int32_t val = 0;if (*a > *b){val = 1;}else if(*a < *b){val = -1;}else{val = 0;}return val;
}static int32_t ArithAvgFlter(int32_t* arr, uint32_t len)//平均值滤波算法
{int32_t sum = 0;for (uint32_t i = 0; i < len; i++){sum += arr[i];}return (int32_t)(sum / len);
}static int32_t MedianAvgFltr(int32_t* arr, uint32_t len)//中位值滤波算法
{qsort(arr, len, sizeof(arr[0]), CompareCallBack);//数据排序/*排序后的数据显示*/printf("after qsort :\n");for (uint8_t i = 0; i < len; i++){printf ("g_dataBuf[%d] = %d\n", i, arr[i]);}putchar ('\n');/*返回中位值滤波后的结果*/return ArithAvgFlter(&arr[1], len - 2);}int main(void)
{	UartDrvInit();int32_t result = 0;result = MedianAvgFltr(g_dataBuf, 6);printf("result =  %d\n", result);while(1){}
}

四、二分查找法

        二分查找法用于查找一个有序数组中某个目标值是否存在,或者查找到接近目标值的元素(下标);相比把整个数组遍历一次的O(n)复杂度,二分查找法可以把复杂度降低到O(log2^n),假如数组元素个数为1024,那么遍历法最多遍历1024次,而二分查找法最多10次搞定,2^10 = 1024嘛。

4.1 二分查找法查找某个元素是否存在

        假如现在有一个升序数组为{1,3,7,10,14,20},现要求查找元素20的下标,如果没有这个元素,返回下标为-1。

        我理解的二分查找法,一堆砝码随意的摆成一行,我需要找这其中是否有一个1KG的砝码a,首先我假设这行砝码里有我要的那个砝码a,那我把这行砝码分成两份,这个1KG的砝码a是不是不在这半边就在那半边?如果我能用一种简单的方法判断它究竟在那边,是不是直接就可以排除掉一半的砝码,大大减轻了筛选的工作量?二分查找法就是这样的思路。

        那么关键就在于我如何用一种较为简单的方法判断我要找的数字究竟在数据中的那一半呢?那就要思考数据的本质特征,是的,不管是砝码的质量还是数组中的数字,只要涉及数,都有一个最本质的特点——大小!!(这就是为什么二分查找法要求数据是有序排列的!

        有了上面的思路,我们就可以想,如果能把数据按照升序或降序排列,那么我只需要把我要找的那个数字,和数据排序好之后--下标位于数据中间的那个数字作比较,就可以轻而易举的知道,我要找的数字究竟在数据中的那一边,我不断的二分,不断的二分,最终就能找到那个数字,或者知道数据中并没有这个数字!!

        现在回到最开始提出的问题,结合下图进行讲解:首先要明确的一点就是,left, right, mid都是保存的元素的下标,现在left = 0, right = 5, 那么要找出中间数字的下标,很简单,mid = (right + left )/2!噫,可是图中并不是用的这种方法求取中间数字的下标,这是为什么呢?

        作为嵌入式工程师,做数值运算处理的时候,时刻都要问自己,是否有溢出的风险?如果left和right都是uint8_t数据类型,因为随着不断的二分、二分....,显而易见,left和right的值是会增大或减小的,假如现在left增大到100,而right又为234,那么234 + 100 = 334显然是超出了uint8_t类型的数值范围。而采用图中的方法就不会有溢出的风险。

        言归正传,mid = 0 + (5 - 0)/2 = 2,下标为2的数字是7,7是小于要找的数字20的,由于数据是升序排列,那么很显然要找的20一定处于右半部分,所以左半部分可以直接不找了。怎么个不找法呢?现在20处于下标3~5之间,因此把left = mid + 1 = 3;right = 5不变,再一次二分,mid = left +(right - left)/2 = 4; 那么下标为4的数字为14,仍然小于20,继续二分。。。。略

        那么如何用代码实现上述的逻辑呢?代码如下,只要C语言基本功扎实的朋友应该都可以看明白,我就不过多赘述了,只提一些比较容易忽略的、重要的点。

        函数IncBinarySearch和函数DecBinarySearch分别为查找升序数组和查找降序数组的二分查找法代码,在这两个函数里面,我把left, right, mid这三个代表数组下标的变量类型都设为了int32_t,这是为什么呢?明明数组的下标是不可能会有负数的,设置为无符号数据类型不就好了吗,为什么要设为有符号类型呢?

        如果想要知道答案的话,不妨可以自己试着这样思考一下,如果我把数据类型设为无符号的,会出错吗?然后去实验,脑子里预想各种数据情况,过一遍代码。如果把数据类型设为无符号,假如我要查找的数字并不在数组当中,就会出现这样一种情况,left会大于right然后退出循环,可是如果我要查找的数字不在数组里面,并且它小于数组中的任何一个数,那么就会出现right < left = 0的情况,也就是说right里面存的负数,可是无符号数据类型从0减1会直接溢出为数据有效范围的最大值,所以不会出现left > right退出循环的情况,代码就死在这个地方了。

#include "stm32f10x.h"                  // Device header
#include "stdio.h"
#include "stdlib.h"
#include "uart_drv.h"static uint16_t g_incDataBuf[6] = {0, 5, 7, 9, 11, 23};
static uint16_t g_decDataBuf[6] = {43, 32, 21, 13, 6, 3};static int32_t IncBinarySearch(uint16_t* arr, uint32_t size, uint16_t key)//查找升序数组
{int32_t left = 0;int32_t right = size - 1;int32_t mid = 0;while (left <= right){mid = left + (right - left) / 2;if (arr[mid] > key){right = mid - 1;}else if(arr[mid] < key){left = mid + 1;}else{return mid;}}printf ("error index!!!");return -1;
}static int32_t DecBinarySearch(uint16_t* arr, uint32_t size, uint16_t key)//查找降序数组
{int32_t left = 0;int32_t right = size - 1;int32_t mid = 0;while (left <= right){mid = left + (right - left) / 2;if (arr[mid] > key){left = mid + 1;}else if(arr[mid] < key){right = mid - 1;}else{return mid;}}printf ("error index!!!");return -1;
}
int main(void)
{	UartDrvInit();/*查找9在升序数组g_incDataBuf中的下标*/printf("find location of number 9 in the g_incDataBuf\n");int32_t incIndex = IncBinarySearch(g_incDataBuf, 6, 9);printf ("g_incDataBuf[%d] = %d\n", incIndex, 9);incIndex = IncBinarySearch(g_incDataBuf, 6, 88);printf ("g_incDataBuf[%d] = %d\n\n", incIndex, 88);/*查找21在降序数组g_decDataBuf中的下标*/printf("find location of number 9 in the g_decDataBuf\n");int32_t deccIndex = DecBinarySearch(g_decDataBuf, 6, 21);printf ("g_decDataBuf[%d] = %d\n", deccIndex, 21);deccIndex = DecBinarySearch(g_decDataBuf, 6, 99);printf ("g_decDataBuf[%d] = %d\n", deccIndex, 99);while(1){}
}

 4.2 二分查找法查找接近目标数值的元素的下标

        什么意思,也就是一堆砝码里面,我要找10KG的砝码,如果没有10KG的砝码,那就把最接近10KG质量的砝码找出来,嘿,找到一个9.9KG的砝码!!

        二分查找法查找接近目标的值有下图所示的两种情况,为什么这样我还没有想明白。

        升序查找代码图如下,图片资源来自郭天祥老师的PN学堂,ARM32位单片机教学课。

         降序代码如下:

五、NTC热敏电阻实现测温

5.1 分层设计

        软件架构框图如下:

5.2 软件流程图

 5.3 API接口及数据结构

        根据自己要实现的功能,先把各个.h文件中的API结构函数声明写好,再逐一实现。

sensor_app.h

/**
***********************************************************
* @brief    传感器任务处理函数
* @param
* @return 
***********************************************************
*/
void SensorTask(void);

sensor_drv.h

typedef struct 
{
    float temp;
    uint8_t humi;
}SensorDataInfo_t;//传感器数据类型成员

/**
***********************************************************
* @brief    传感器驱动初始化
* @param
* @return 
***********************************************************
*/
void SensorDrvInit(void);

/**
***********************************************************
* @brief    触发传感器转换数据
* @param
* @return 
***********************************************************
*/
void SensorDrvProc(void);

/**
***********************************************************
* @brief       获取温度数据
* @param    sensorData:结构体指针,带出数值
* @return 
***********************************************************
*/
void GetSensorData(SensorDataInfo_t* sensorData);

hmi_app.h

/**
***********************************************************
* @brief 人机交互任务处理函数
* @param 
* @return 
***********************************************************
*/
void HmiTask(void); 

5.4 热敏电阻模块原理图

         C1是芯片的退藕电容,C2应该是起到滤波作用;

        R1和热敏电阻N1分压,热敏电阻温度越高阻值越低,即采集到的电压越低;

        LM393是电压比较器,同相端INA+大于反向端INA-则OUTA输出高电平,反之输出低电平,调节R2电阻的值可以设置低于多少温度时D2亮;

        D1是电源指示灯,AC输出热敏电阻电压模拟量;

5.5 热敏电阻工作原理

        数据手册中给出了对应温度下的NTC电阻值,温度范围-55~125℃,电阻的精度是百1,温度精度是1℃。

         

        由于STM32的ADC是12位的,直接读取ADC得到的是电压的数字量0~4095,所以,我们可以根据手册中提供的温度对应的电阻值,自己计算把温度对应的热敏电阻电压数字量计算出来,列成表格。

        比如说0摄氏度下NTC的中心电阻值是32.116KΩ,那么计算公式就是:

                                32.116/(10+32.116)*4095 = 3122.685

        这样,ADC采集到一个数字量之后,可以利用二分查找法(注意由于热敏阻值随温度升高而降低,所以得到的电压表格天然降序排列)找到最接近这个数值的温度对应值,从而确定温度。 


http://www.ppmy.cn/server/154232.html

相关文章

售前笔试题总结

售前笔试题总结 List、Map、Set三个接口在存取元素时各有什么特点?Set里的元素是不能重复的&#xff0c;那么用什么方法来区分重复与否呢?是用“”还是equals()?它们有何区别? List&#xff1a;有先后顺序的集合 存元素&#xff1a;采用add&#xff08;&#xff09;方法在…

深入了解SpringIoc(续篇)

目录 注入 Bean 的方式有哪些? 构造函数注入还是 Setter 注入? Bean 的作用域有哪些? Bean 是线程安全的吗? Bean 的生命周期了解么? 注入 Bean 的方式有哪些? 依赖注入 (Dependency Injection, DI) 的常见方式&#xff1a; 构造函数注入&#xff1a;通过类的构造函…

Html——9 . 标题栏图标2

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>我要自学网</title><!--<link rel"icon" type"image/x-icon" href"img/favicon.ico"/>--><!--<link rel"icon…

Windows 安装 Jenkins 教程

Jenkins 简介 Jenkins 是一个开源的自动化服务器&#xff0c;主要用于持续集成&#xff08;CI&#xff09;和持续交付&#xff08;CD&#xff09;。它可以自动化软件开发生命周期中的许多任务&#xff0c;如构建、测试、部署和发布。Jenkins 最初是由 Kohsuke Kawaguchi 在 20…

springboot整合log4j2日志框架1

一 log4j基本知识 1.1 log4j的日志级别 Log4j定义了8个级别的log&#xff08;除去OFF和ALL&#xff0c;可以说分为6个级别&#xff09;&#xff0c;优先级从低到高依次为&#xff1a;All&#xff0c;trace&#xff0c;debug&#xff0c;info&#xff0c;warn&#xff0c;err…

【每日学点鸿蒙知识】List+Swipe滑动冲突、下拉刷新、编译错误定位、监听生命周期、上架应用市场要求

1、HarmonyOS ListSwipeweb滑动冲突&#xff1f; 在List中嵌套一个横向滑动的swipe&#xff0c;swipe嵌套一个web&#xff0c;此时设置手势优先的时&#xff0c;web无法和list进行联动交互 题原因可能是List组件嵌套Web组件产生了滑动冲突&#xff0c;这里可以使用触摸测试控…

RHCE(第二部分)-----第四章:流程控制之条件判断

第四章&#xff1a;流程控制之条件判断 ​ 条件判断语句是一种最简单的流程控制语句。该语句使得程序根据不同的条件来执行不同的程序分支。本节将介绍Shell程序设计中的简单的条件判断语句。 [rootlocalhost day03]# echo ${2:100} -bash: $2: cannot assign in this way4.1…

C++软件设计模式之类型模式和对象型模式

在 C 软件设计模式中&#xff0c;通常将设计模式分为两大类&#xff1a;类型模式&#xff08;Type Patterns&#xff09;和对象型模式&#xff08;Object Patterns&#xff09;。这两种模式在实现和应用上有不同的特点和目的。 类型模式&#xff08;Type Patterns&#xff09;…