一、设计背景
ADC(Analog-to-Digital Converter,即模拟-数字转换器)是电子系统中不可或缺的关键组件,它将连续的模拟信号转换为数字信号,为数字处理和分析提供了可能。ADC在信号转换、测量与数据采集、控制系统输入以及通信与信号处理等方面发挥着重要作用,其广泛的应用促进了各行业电子设备的智能化和精确控制,是推动现代科技进步的关键因素之一。
数字电压电流表结合了ADC的技术与电路测量原理,能够精确地将模拟的电压电流信号转换为数字显示,便于电子工程师直观读取和分析。这种设备不仅提高了电路测量的准确性和效率,还帮助工程师更好地理解电路行为,是进行电子设计和故障排查的得力助手,对电子工程师的工作具有重要的辅助作用。在产品应用上,数字电压电流表确保了电路设计的准确性和安全性,同时也为产品的质量控制和后期维护提供了有力支持。
学习设计和制作一个数字电压电流表对于个人专业技能的提升是非常有益的。数字电压电流表项目涵盖了微控制器电路的设计与实现、信号采集与处理电路的设计、用户界面的开发与优化以及产品外观的设计等多个方面,融合了电子技术、微控制器编程、电路设计以及工业设计等多领域知识。考虑到初学者的学习进度与知识吸收能力,我们特别推出了这一入门级的数字电压电流表项目,非常适合电子技术的初学者以及想要深入学习微控制器应用的人群。该项目具备以下几个亮点:
- 采用核心板加扩展板设计理念,采用插件器件设计,让学习更能简单,让探索能更深入;
- 项目综合程度高,实用性强,设计完成后可作为桌面日常仪表使用;
- 项目学习资料丰富,包括电路设计教学、PCB设计、代码编程的学习以及工程师调试能力的培养。
二、硬件设计
详细请查看地奇星电流电压表
三、软件设计
3.1. 软件开发概述
3.1.1. 嵌入式必备知识点
嵌入式软件开发作为计算机科学和电子工程的交叉领域,要求开发人员具备一系列的专业知识和技能。而基于地奇星的嵌入式软件开发必备知识包括以下部分:
- 编程语言:
- 熟练掌握C(C++)语言,这是嵌入式系统中最常用的编程语言,因为它们提供了直接访问硬件的能力,并且代码执行效率高。
- 了解汇编语言,用于编写底层驱动、中断处理程序以及性能要求极高的代码段。
- 对其他编程语言如Python、Java等有一定了解,以便在特定情况下使用。
- 了解瑞萨的 FSP 库的用法。
- 数据结构与算法:
- 熟悉各种数据结构,如数组、链表、栈、队列等,以及常用的算法,如排序、查找、递归等。
- 能够根据嵌入式系统的资源限制选择合适的数据结构和算法。
- 计算机体系结构:
- 了解处理器架构,如ARM、x86等,以及指令集和内存管理。
- 熟悉嵌入式系统的硬件组成,如微控制器、FPGA、DSP等。
- 可以很熟练的根据嵌入式芯片的数据手册及用户手册,查找所需外设工作原理。
- 嵌入式操作系统(深入知识点):
- 掌握常用的嵌入式操作系统,如μC/OS、FREERTOS等,了解其内核、进程管理、内存管理、设备管理和文件系统等基本原理。
- 能够进行操作系统的任务设计、任务管理等,以满足特定应用的需求。
- 硬件接口与外设:
- 熟悉常用的硬件接口,如GPIO、串口、SPI、I2C等,并能编写相应的驱动程序。
- 了解嵌入式系统常用的硬件设备,如传感器、执行器、通信模块等,并能够与之进行交互。
- 开发工具与环境:
- 熟练使用集成开发环境 E2studio 进行软件开发和调试。
- 掌握交叉编译器的使用,以便在开发计算机上编译出能在目标硬件上运行的程序。
- 熟悉调试器的使用,能够进行软件的断点调试、单步执行、变量查看等。
- 系统分析与设计:
- 能够进行项目需求分析,将需求转化为软件功能需求。
- 掌握软件架构设计的原则和方法,能够设计出高效、可维护的软件系统。
- 了解嵌入式系统的实时性要求,能够设计出满足实时性要求的软件系统。
- 测试与验证:
掌握单元测试、集成测试和系统测试的方法和技术,能够对软件进行全面的测试,确保软件功能的正确性和稳定性。
了解嵌入式系统的可靠性要求,能够进行软件的可靠性测试和验证。
只有全面掌握这些知识并具备持续学习的能力,才能成为一名优秀的嵌入式软件开发人员。
3.1.2 入手准备
调试器建议使用EZ-CUBE3 调试器进行DeBug调试,若是没有 可以采用串口下载,打印数据。
电压电流表程序下载
注:voltammeter.hex 是综合测试固件,voltammeter.7z 是多个源码,方便用户一步步掌握。
3.3. 实验一:LED灯驱动
3.3.1 LED灯驱动原理
LED 驱动指的是通过稳定的电源为 LED 提供合适的电流和电压,使其正常工作点亮。LED 驱动方式主要有恒流和恒压两种。限定电流的恒流驱动是最常见的方式,因为 LED 灯对电流敏感,电流大于其额定值可能导致损坏。恒流驱动保证了稳定的电流,从而确保了 LED 安全。
LED 灯的驱动比较简单,只需要给将对应的正负极接到单片机的正负极即可驱动。LED的接法也分有两种,灌入电流和输出电流。
- 灌入电流指的是LED的供电电流是由外部提供电流,将电流灌入我们的MCU;风险是当外部电源出现变化时,会导致MCU的引脚烧坏。
- 输出电流指的是由MCU提供电压电流,将电流输出给LED;如果使用 MCU的GPIO 直接驱动 LED,则驱动能力较弱,可能无法提供足够的电流驱动 LED。
需要注意的 是 LED 灯的颜色不同,对应的电压也不同。电流不可过大,通常需要接入220欧姆到10K欧姆左右的限流电阻,限流电阻的阻值越大,LED的亮度越暗。
3.3.2 LED灯原理图
地奇星电压电流表中关于LED灯的原理图如图所示:
3.3.3 LED灯驱动流程
LED1的正极经限流电阻 R10 接到电源正极,LED1 的负极连接到单片机的 GPIO 口上,通过 LED 灯的驱动原理,只需要将相应 GPIO(P402)配置为低电平即可点亮LED1。如图所示:
3.3.3.1 配置流程
一般我们使用GPIO的端口,都需要有以下几个步骤。
- 开启GPIO的端口时钟
- 配置GPIO的模式
- 配置GPIO的输出
从开发板原理图了解到 LED1 接的是单片机的 GPIO(P402)。我们要使能 LED 就需要配置 LED 端口。下面我们就以LED1 接的 P402 进行介绍。
工程配置:
代码如下:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_02, BSP_IO_LEVEL_LOW);
R_BSP_SoftwareDelay (1, BSP_DELAY_UNITS_SECONDS);
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_02, BSP_IO_LEVEL_HIGH);
R_BSP_SoftwareDelay (1, BSP_DELAY_UNITS_SECONDS);
2
3
4
5
3.4 实验二:按键检测
3.4.1 独立按键原理图
地奇星电压电流表算上核心板一共有四个按键,一个复位和三个用户按键,复位作为单片机的特殊功能,不可以作为按键使用,故只有用户按键可以作为按键使用。
地奇星电压电流表关于用户按键的原理图如图所示。
3.4.2 独立按键驱动流程
通过上面的原理图可以了解到,按键的一端接到了地,另一端接到单片机的GPIO口上。通过检测GPIO引脚的电平状态,判断按键是否按下。当按键松开的时候,GPIO检测到的电平为低电平,当按键按下的时候,GPIO检测到的电平为高电平。
外部电路不含上下拉电阻,对IO而言是浮空输入,因此需要使用单片机内部的上下拉电阻;电路不含消抖电容,故编程上需要对按键进行软件消抖,在本次实验中我们使用 K3 按键来控制LED1的亮灭。
3.4.2.1 配置流程
一般我们使用GPIO的输入功能,都需要有以下几个步骤。
- 开启GPIO的端口时钟
- 配置GPIO的模式
- 配置GPIO的输入
- 编写消抖函数
从开发板原理图了解到K3按键接的是地奇星的P013。我们要使能按键就需要端口。
工程配置:
3.4.2.2 读取IO状态
读取IO状态,并翻转LED的状态,逻辑代码如下(注:hal_entry.c文件需要添加app.h路径与Run() 函数,后面就不展示其步骤):
#include "hal_data.h"
#include "Apply\app.h"
FSP_CPP_HEADER
void R_BSP_WarmStart(bsp_warm_start_event_t event);
FSP_CPP_FOOTER
/*******************************************************************************************************************//**
* main() is generated by the RA Configuration editor and is used to generate threads if an RTOS is used. This function
* is called by main() when no RTOS is used.
**********************************************************************************************************************/
void hal_entry(void)
{
/* TODO: add your own code here */
Run();
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "Apply\app.h" // 应用程序
#include "key\bsp_key.h" //按键驱动
#include "led\bsp_led.h" //LED驱动
/**
* @brief 运用函数
* @details 运行系统逻辑
*/
void Run(void)
{
while(1)
{
Key_Scan(); //按键读取
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "bsp_key.h"
#include "led\bsp_led.h"
uint8_t Flag_Key = 0; //按键标志位
/**
* @brief 按键状态扫描函数
* @details 读取指定引脚的电平状态,判断按键是否按下(支持按键释放检测,防抖动)。
* @param key 按键对应的引脚(bsp_io_port_pin_t类型)
*/
void Key_Scan(void)
{
bsp_io_level_t state;
R_IOPORT_PinRead(&g_ioport_ctrl, BSP_IO_PORT_00_PIN_13, &state);// 读取按键引脚电平
if(state == BSP_IO_LEVEL_LOW)
{
Flag_Key = 1;
}
if(Flag_Key) //接着判断标志位
{
R_IOPORT_PinRead(&g_ioport_ctrl, BSP_IO_PORT_00_PIN_13, &state);
if(state == BSP_IO_LEVEL_HIGH)//如果按键已经松开
{
Flag_Key = 0; //清零标志位,等待下一次按键检测
P402_Toggle;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#ifndef __SBP_LED_H
#define __SBP_LED_H
#define P402_Toggle R_PORT4->PODR ^= 1<<(BSP_IO_PORT_04_PIN_02 & 0xFF);
#endif
2
3
4
5
6
3.5 实验三:数码管显示数字
3.5.1 数码管显示原理
注:以前没有了解过数码管的同学,可以去B站学习一下基础原理。
数码管的显示原理是由多个发光的二极管共阴极或者共阳极组成的成“8”字形的显示器件。数码管通过不同的组合可用来显示数字0~9、字符A ~ F及小数点“.”。数码管的工作原理是通过控制外部的I/O端口进行驱动数码管的各个段码,使用不同的段码从而形成字符显示出我们要的数字。数码管实际上是由七个发光管组成8字形构成的,加上小数点就是8个。这些段分别由字母A、B、C、D、E、F、G、DP来表示。
当数码管特定的引脚加上高电平后,这些特定的发光二极管就会发亮,以形成我们眼睛看到的字样了。如:在一个共阴极数码管上显示一个“8”字,那么就对A、B、C、D、E、F、G对应的引脚置高电平。发光二极管的阳极共同连接至电源的正极称为共阳极数码管,这种类型的数码管点亮需要对引脚置低电平;发光二极管的阴极共同连接到电源的负极称为共阴极数码管,点亮共阴极数码管需要对相应的引脚置高电平。常用LED数码管显示的数字和字符是0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F。
共阳极数码管的8个发光二极管的阳极(二极管正端)连接在一起。通常,公共阳极接高电平(一般接电源),其它管脚接段驱动电路输出端。当某段驱动电路的输出端为低电平时,则该端所连接的字段导通并点亮。根据发光字段的不同组合可显示出各种数字或字符。此时,要求段驱动电路能吸收额定的段导通电流,还需根据外接电源及额定段导通电流来确定相应的限流电阻。
共阴极数码管的8个发光二极管的阴极(二极管负端)连接在一起。通常,公共阴极接低电平(一般接地),其它管脚接段驱动电路输出端。当某段驱动电路的输出端为高电平时,则该端所连接的字段导通并点亮,根据发光字段的不同组合可显示出各种数字或字符。此时,要求段驱动电路能提供额定的段导通电流,还需根据外接电源及额定段导通电流来确定相应的限流电阻。
3.5.2 数码管原理图与实物图
如果数码管可以显示多位数字,如我们的电压电流表所示。那么除了控制段码来选择要显示的内容,还要选择位码来控制某一个数码管的亮灭。
数码管的原理图如下,可以看出除了上述的段码引脚之外,还有COM1、COM2、COM3的位码引脚,三个位码引脚分别控制三个数码管的亮灭情况,且低电平有效。
3.5.3 数码管驱动显示
驱动显示数码管的思路是:先将A、B、C、D、E、F、G所代表的引脚从低到高编号,列出数码要显示数字的段码值。比如要显示数字5,则段码值为0x6d,二进制表示为01101101,这说明G置1,F置1,E置0,D置1,C置1,B置0,A置1,最高位则是DP的值。将要显示的数字以段码值的方式储存在数组里以供调用,可以简化程序。接着以循环的方式结合switch语句对A、B、C、D、E、F、G的亮灭情况进行单独计算,先将段码值确定后再进行位码的选择,可以避免因单片机执行程序的时间而造成显示效果的不足。
GPIO的配置请参考LED项目。
具体程序如下,将所有与数码管显示相关的函数保存在新建的 bsp_seg.c 文件中:
#include "Apply/app.h" // 应用层
#include "seg/bsp_seg.h" // 数码管驱动
/**
* @brief 运用函数
* @details 运行系统逻辑
*/
void Run(void)
{
Close_Com();
Seg_Dis(1,6);
while(1)
{
;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <seg/bsp_seg.h>
#include "hal_data.h"
/* 共阴数码管编码表(每个字节对应数码管的8段LED状态):
低7位对应A~G段,第7位(最高位)对应小数点(DP)
0x3f: 0 0x06: 1 0x5b: 2 0x4f: 3 0x66: 4 0x6d: 5 0x7d: 6 0x07: 7 0x7f: 8 0x6f: 9
0xbf: 0.(带小数点) 0x86: 1. 0xdb: 2. 0xcf: 3. 0xe6: 4. 0xed: 5. 0xfd: 6. 0x87: 7. 0xff: 8. 0xef: 9.
*/
uint8_t Seg_Table[21] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f,
0xbf, 0x86, 0xdb, 0xcf, 0xe6, 0xed, 0xfd, 0x87, 0xff, 0xef,0xF7};
/**
* @brief 数码管单一位显示函数
* @details 根据指定位置和数值,控制对应数码管的段选和位选引脚,显示相应字符
* @param Pos 数码管位置(0~5,对应6个数码管的位选端)
* @param Num 待显示数值索引(对应Seg_Table中的索引,如0对应数字0,10对应0.)
*/
void Seg_Dis(uint8_t Pos,uint8_t Num)
{
int i;
uint8_t Dis_Value; // 从编码表中获取的段选数据
Dis_Value = Seg_Table[Num]; // 根据数值索引获取段选编码
for(i = 0; i < 8; i++)
{
switch(i)
{
case 0:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_03_PIN_02, (Dis_Value >> i) & 0x01);//P,A
break;
case 1:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_02_PIN_07, (Dis_Value >> i) & 0x01);//P,B
break;
case 2:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_02, (Dis_Value >> i) & 0x01);//P,C
break;
case 3:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_04, (Dis_Value >> i) & 0x01);//P,D
break;
case 4:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_03_PIN_01, (Dis_Value >> i) & 0x01);//P,E
break;
case 5:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_02_PIN_06, (Dis_Value >> i) & 0x01);//P,F
break;
case 6:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_12, (Dis_Value >> i) & 0x01);//P,G
break;
case 7:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_03, (Dis_Value >> i) & 0x01);//P,DP
break;
default:
break;
}
}
switch(Pos)
{
case 0:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_09, BSP_IO_LEVEL_LOW); //P,COM1
break;
case 1:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_10,BSP_IO_LEVEL_LOW); //P,COM2
break;
case 2:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_11,BSP_IO_LEVEL_LOW); //P,COM3
break;
case 3:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_08,BSP_IO_LEVEL_LOW); //P,COM4
break;
case 4:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_09,BSP_IO_LEVEL_LOW); //P,COM5
break;
case 5:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_03,BSP_IO_LEVEL_LOW); //P,COM6
break;
default:
break;
}
}
/**
* @brief 关闭所有数码管的位选端
* @details 将所有数码管的公共端(COM)置为高电平,使其暂时不显示,防止扫描时出现重影
*/
void Close_Com(void)
{
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_09, BSP_IO_LEVEL_HIGH); //P,COM1
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_10,BSP_IO_LEVEL_HIGH); //P,COM2
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_11,BSP_IO_LEVEL_HIGH); //P,COM3
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_08,BSP_IO_LEVEL_HIGH); //P,COM4
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_09,BSP_IO_LEVEL_HIGH); //P,COM5
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_03,BSP_IO_LEVEL_HIGH); //P,COM6
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
3.5.4 实验效果
最终的实验效果如下图所示:
3.6 实验四:数码管动态显示
3.6.1 数码管动态显示原理
所谓动态扫描显示即轮流向各位数码管送出段码和位码,利用发光管的余辉和人眼视觉暂留作用,使人眼的感觉好像各位数码管同时都在显示。明确了原理,我们要使电压电流表的三个位同时显示不同的值需要用到地奇星的定时器功能,在定时器的中断服务程序里面执行显示刷新的动作。有关地奇星的定时器和中断的相关知识请查看链接:【地奇星】通用GPT定时器教程。本文只讲述如何配置定时器中断并执行数码管刷新函数。
3.6.2 定时器中断配置
在本次实验中,我们使用定时器GPT0进行刷新,代码如下:
#include "Apply/app.h" // 应用层
#include "seg/bsp_seg.h" // 数码管驱动
#include "gpt/bsp_gpt.h" // 通用定时器驱动
#include "led/bsp_led.h" // led驱动
uint32_t ledcount=0;
/**
* @brief 运用函数
* @details 运行逻辑
*/
void Run(void)
{
GPT_Init(); // 初始化通用定时器
while(1)
{
DisplayValue(123456);
ledcount++; //工作指示灯
if(ledcount >= 1000000)
{
ledcount = 0;
P402_Toggle;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "bsp_gpt.h"
#include "seg/bsp_seg.h" // 数码管驱动
/**
* @brief GPT定时器初始化函数
* @details 初始化GPT定时器模块,配置定时器参数并启动计数
* 用于定时触发数码管刷新和按键扫描等周期性任务
* @return 无
*/
void GPT_Init(void)
{
/* 初始化 GPT 模块 */
R_GPT_Open(&g_timer0_ctrl, &g_timer0_cfg);
/* 启动 GPT 定时器 */
R_GPT_Start(&g_timer0_ctrl);
}
/**
* @brief GPT定时器中断回调函数
* @details 当GPT定时器达到设定周期时触发此函数,执行周期性任务
* @param p_args 定时器回调参数,包含事件类型信息
* @return 无
*/
void gpt0_callback(timer_callback_args_t *p_args)
{
if (p_args->event == TIMER_EVENT_CYCLE_END)
{
Dis_Refresh(); //数码管扫描显示
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <seg/bsp_seg.h>
#include "hal_data.h"
/* 共阴数码管编码表(每个字节对应数码管的8段LED状态):
低7位对应A~G段,第7位(最高位)对应小数点(DP)
0x3f: 0 0x06: 1 0x5b: 2 0x4f: 3 0x66: 4 0x6d: 5 0x7d: 6 0x07: 7 0x7f: 8 0x6f: 9
0xbf: 0.(带小数点) 0x86: 1. 0xdb: 2. 0xcf: 3. 0xe6: 4. 0xed: 5. 0xfd: 6. 0x87: 7. 0xff: 8. 0xef: 9.
*/
uint8_t Seg_Table[21] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f,
0xbf, 0x86, 0xdb, 0xcf, 0xe6, 0xed, 0xfd, 0x87, 0xff, 0xef,0xF7};
uint8_t Seg_Reg[6] = {1,2,3,4,5,6};
/**
* @brief 数码管单一位显示函数
* @details 根据指定位置和数值,控制对应数码管的段选和位选引脚,显示相应字符
* @param Pos 数码管位置(0~5,对应6个数码管的位选端)
* @param Num 待显示数值索引(对应Seg_Table中的索引,如0对应数字0,10对应0.)
*/
void Seg_Dis(uint8_t Pos,uint8_t Num)
{
int i;
uint8_t Dis_Value; // 从编码表中获取的段选数据
Dis_Value = Seg_Table[Num]; // 根据数值索引获取段选编码
for(i = 0; i < 8; i++)
{
switch(i)
{
case 0:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_03_PIN_02, (Dis_Value >> i) & 0x01);//P,A
break;
case 1:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_02_PIN_07, (Dis_Value >> i) & 0x01);//P,B
break;
case 2:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_02, (Dis_Value >> i) & 0x01);//P,C
break;
case 3:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_04, (Dis_Value >> i) & 0x01);//P,D
break;
case 4:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_03_PIN_01, (Dis_Value >> i) & 0x01);//P,E
break;
case 5:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_02_PIN_06, (Dis_Value >> i) & 0x01);//P,F
break;
case 6:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_12, (Dis_Value >> i) & 0x01);//P,G
break;
case 7:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_03, (Dis_Value >> i) & 0x01);//P,DP
break;
default:
break;
}
}
switch(Pos)
{
case 0:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_09, BSP_IO_LEVEL_LOW); //P,COM1
break;
case 1:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_10,BSP_IO_LEVEL_LOW); //P,COM2
break;
case 2:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_11,BSP_IO_LEVEL_LOW); //P,COM3
break;
case 3:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_08,BSP_IO_LEVEL_LOW); //P,COM4
break;
case 4:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_09,BSP_IO_LEVEL_LOW); //P,COM5
break;
case 5:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_03,BSP_IO_LEVEL_LOW); //P,COM6
break;
default:
break;
}
}
/**
* @brief 关闭所有数码管的位选端
* @details 将所有数码管的公共端(COM)置为高电平,使其暂时不显示,防止扫描时出现重影
*/
void Close_Com(void)
{
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_09, BSP_IO_LEVEL_HIGH); //P,COM1
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_10,BSP_IO_LEVEL_HIGH); //P,COM2
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_11,BSP_IO_LEVEL_HIGH); //P,COM3
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_08,BSP_IO_LEVEL_HIGH); //P,COM4
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_09,BSP_IO_LEVEL_HIGH); //P,COM5
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_03,BSP_IO_LEVEL_HIGH); //P,COM6
}
/**
* @brief 六位显示函数
* @details 将值格式化后存入显示寄存器,控制数码管显示不带小数点的实际电流值
* @param value 待显示的值
*/
void DisplayValue(uint32_t value)
{
uint8_t SWwei;
uint8_t Wwei;
uint8_t Thousands;
uint8_t Hundreds;
uint8_t Tens;
uint8_t Units; // 个位数
SWwei = value / 100000;
Wwei = value / 10000 %10;
Thousands = value / 1000 % 10;
Hundreds = value / 100 % 10;
Tens = value / 10 % 10;
Units = value % 10;
Seg_Reg[0] =SWwei;
Seg_Reg[1] =Wwei;
Seg_Reg[2] =Thousands;
Seg_Reg[3] =Hundreds;
Seg_Reg[4] =Tens;
Seg_Reg[5] =Units;
}
/**
* @brief 数码管扫描显示函数
* @details 由定时器周期性调用(如1ms一次),依次刷新6个数码管,利用人眼视觉暂留实现全屏显示
* 每次只点亮一个数码管,避免多个数码管同时点亮导致的段选冲突和重影
*/
void Dis_Refresh(void)
{
static uint8_t num = 0; // 静态变量,记录当前扫描的数码管位置(0~5循环)
Close_Com(); //先关闭公共端,防止重影
Seg_Dis(num,Seg_Reg[num]); // 显示当前位置的数码管
num++; // 切换显示到下一个位置
if(num > 6)
{
num = 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
3.6.4 实验效果
最终的实验效果如下图所示,人眼已经看不出闪烁,但实际上数码管是依次刷新显示。
3.7 实验五:ADC采样及显示
3.7.1 什么是ADC
模拟数字转换器即A/D转换器,或简称ADC,通常是指一个将模拟信号转变为数字信号的电子元件。通常的模数转换器是将一个输入电压信号转换为一个输出的数字信号。由于数字信号本身不具有实际意义,仅仅表示一个相对大小。故任何一个模数转换器都需要一个参考模拟量作为转换的标准,比较常见的参考标准为最大的可转换信号大小。而输出的数字量则表示输入信号相对于参考信号的大小。
3.7.2 地奇星的ADC介绍
地奇星只有一个模数转换器(ADC),其中A/D转换精度支持从12位,10位,8位转换中选择,从而可以优化权衡在产生数字值的速度和分辨率之间。支持以下工作模式:单扫描模式、连续扫描模式与 组扫描模式。
3.7.3 ADC基本参数
分辨率: 表示ADC转换器的输出精度,通常以位数(bit)表示,比如8位、10位、12位等,位数越高,精度越高。
采样率: 表示ADC对模拟输入信号进行采样的速率,通常以每秒采样次数(samples per second,SPS)表示,也称为转换速率,表示ADC能够进行多少次模拟到数字的转换。
采样范围: 指ADC可以采集到的模拟输入信号的电压范围,范围见下:
VREFH0 ≥ ADC ≥ VREFL0
注:其中VREFL0为V(SSA)等于地0V,VREFH0 等于V(DDA),而立创·地奇星开发板在原理图设计的时候VDDA接入了3.3V 。
3.7.4 基本原理
地奇星采用的是逐次逼近型的12位ADC,逐次逼近型ADC是一种常见的ADC工作原理,它的思想是通过比较模拟信号与参考电压之间的大小关系来逐步逼近输入信号的数字表示。在逐次逼近型ADC中,输入信号和参考电压被加入一个差分放大器中,产生一个差分电压。然后,这个差分电压被输入到一个逐步逼近的数字量化器中,该量化器以逐步递减的方式将其与一系列参考电压进行比较。具体来说,在每个逼近阶段,量化器将输入信号与一个中间电压点进行比较,将该电压点上方或下方的参考电压作为下一个逼近阶段的参考电压。这个过程一直持续到量化器逼近到最终的数字输出值为止。
以一个采样电路原理图举例:
3.7.5 ADC采样显示
地奇星将采样得到的值输入数码管显示,配置代码如下:
注:详细的配置教程请查看地奇星ADC
#include "Apply/app.h" // 应用层
#include "seg/bsp_seg.h" // 数码管驱动
#include "gpt/bsp_gpt.h" // 通用定时器驱动
#include "adc/bsp_adc.h" // ADC(模数转换)驱动
#include "uart/bsp_uart.h" // UART(串口)驱动
#include "led/bsp_led.h" // LED 驱动
extern uint8_t Seg_Reg[6]; // 数码管显示寄存器
unsigned int timecount=0; // 主循环计时计数器
uint16_t V_Buffer=0; // 缓存电压值
/**
* @brief 硬件初始化函数
* @details 初始化系统所需的外设
* 读取历史校准数据并计算校准斜率,为系统运行做准备。
*/
void bsp_Init(void)
{
UART0_Init(); // 初始化串口0
GPT_Init(); // 初始化通用定时器
ADC_Init(); // 初始化ADC模块
}
/**
* @brief 运用函数
* @details 运行逻辑
*/
void Run(void)
{
bsp_Init(); // 初始化硬件外设
DisplayI(000); //最下面显示 000
while(1)
{
V_Buffer = ADC_read_Value() * 3.3 / 40.96; //将采集的数据扩大100倍,能显示小数点后两位
if( timecount >= 500)
{
printf("temperature=%d\r\n",V_Buffer);
Display(V_Buffer); //更新数码管显示
P402_Toggle; //工作指示灯
timecount=0;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "bsp_adc.h"
#include "r_adc_api.h"
#include "hal_data.h"
#include <stdio.h>
volatile bool adc_flag = false;
/**
* @brief ADC模块初始化函数
* @details 初始化ADC硬件,配置ADC控制器和扫描参数,为ADC数据采集做准备
* @return 无
*/
void ADC_Init(void)
{
fsp_err_t err;
// 打开ADC设备
err = R_ADC_Open(&g_adc0_ctrl, &g_adc0_cfg);
if (FSP_SUCCESS != err) {
printf("ADC初始化失败! \n");
return;
}
// 配置ADC扫描
err = R_ADC_ScanCfg(&g_adc0_ctrl, &g_adc0_channel_cfg);
if (FSP_SUCCESS != err) {
printf("ADC扫描配置失败! \n");
R_ADC_Close(&g_adc0_ctrl); // 关闭已打开的ADC
return;
}
printf("ADC初始化成功!\n");
}
/**
* @brief ADC转换完成回调函数
* @details 当ADC完成一次扫描转换后,由硬件中断触发此函数
* @param p_args ADC回调参数
* @return 无
*/
void adc_callback(adc_callback_args_t *p_args)
{
FSP_PARAMETER_NOT_USED(p_args);
adc_flag = true;
}
/**
* @brief 读取ADC通道数据
* @details 启动ADC扫描,等待转换完成后,读取指定通道的ADC原始值
* @return 读取到的ADC值(0,4095)
*/
uint16_t ADC_read_Value(void)
{
uint16_t adc_data = 0;
// 启动ADC扫描
fsp_err_t err = R_ADC_ScanStart(&g_adc0_ctrl);
if (FSP_SUCCESS != err) {
printf("ADC扫描启动失败! \n");
return 9999; // 返回错误值
}
// 等待ADC转换完成
while (!adc_flag);
adc_flag = false; // 清除标志位
// 读取ADC数据
err = R_ADC_Read(&g_adc0_ctrl, ADC_CHANNEL_7, &adc_data);
if (FSP_SUCCESS != err) {
printf("ADC数据读取失败!\n");
return 9999; // 返回错误值
}
return adc_data;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include "bsp_gpt.h"
extern bool adc_flag; // 全局定义ADC转换完成标志位
extern unsigned int timecount;
/**
* @brief GPT定时器初始化函数
* @details 初始化GPT定时器模块,配置定时器参数并启动计数
* 用于定时触发数码管刷新和按键扫描等周期性任务
* @return 无
*/
void GPT_Init(void)
{
/* 初始化 GPT 模块 */
R_GPT_Open(&g_timer0_ctrl, &g_timer0_cfg);
/* 启动 GPT 定时器 */
R_GPT_Start(&g_timer0_ctrl);
}
/**
* @brief GPT定时器中断回调函数
* @details 当GPT定时器达到设定周期时触发此函数,执行周期性任务
* @param p_args 定时器回调参数,包含事件类型信息
* @return 无
*/
void gpt0_callback(timer_callback_args_t *p_args)
{
if (p_args->event == TIMER_EVENT_CYCLE_END)
{
Dis_Refresh();//数码管扫描显示
timecount++;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <seg/bsp_seg.h>
#include "hal_data.h"
/* 共阴数码管编码表(每个字节对应数码管的8段LED状态):
低7位对应A~G段,第7位(最高位)对应小数点(DP)
0x3f: 0 0x06: 1 0x5b: 2 0x4f: 3 0x66: 4 0x6d: 5 0x7d: 6 0x07: 7 0x7f: 8 0x6f: 9
0xbf: 0.(带小数点) 0x86: 1. 0xdb: 2. 0xcf: 3. 0xe6: 4. 0xed: 5. 0xfd: 6. 0x87: 7. 0xff: 8. 0xef: 9.
*/
uint8_t Seg_Table[21] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f,
0xbf, 0x86, 0xdb, 0xcf, 0xe6, 0xed, 0xfd, 0x87, 0xff, 0xef,0xF7};
uint8_t Seg_Reg[6] = {1,2,3,4,5,6};
/**
* @brief 数码管单一位显示函数
* @details 根据指定位置和数值,控制对应数码管的段选和位选引脚,显示相应字符
* @param Pos 数码管位置(0~5,对应6个数码管的位选端)
* @param Num 待显示数值索引(对应Seg_Table中的索引,如0对应数字0,10对应0.)
*/
void Seg_Dis(uint8_t Pos,uint8_t Num)
{
int i;
uint8_t Dis_Value; // 从编码表中获取的段选数据
Dis_Value = Seg_Table[Num]; // 根据数值索引获取段选编码
for(i = 0; i < 8; i++)
{
switch(i)
{
case 0:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_03_PIN_02, (Dis_Value >> i) & 0x01);//P,A
break;
case 1:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_02_PIN_07, (Dis_Value >> i) & 0x01);//P,B
break;
case 2:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_02, (Dis_Value >> i) & 0x01);//P,C
break;
case 3:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_04, (Dis_Value >> i) & 0x01);//P,D
break;
case 4:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_03_PIN_01, (Dis_Value >> i) & 0x01);//P,E
break;
case 5:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_02_PIN_06, (Dis_Value >> i) & 0x01);//P,F
break;
case 6:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_12, (Dis_Value >> i) & 0x01);//P,G
break;
case 7:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_03, (Dis_Value >> i) & 0x01);//P,DP
break;
default:
break;
}
}
switch(Pos)
{
case 0:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_09, BSP_IO_LEVEL_LOW); //P,COM1
break;
case 1:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_10,BSP_IO_LEVEL_LOW); //P,COM2
break;
case 2:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_11,BSP_IO_LEVEL_LOW); //P,COM3
break;
case 3:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_08,BSP_IO_LEVEL_LOW); //P,COM4
break;
case 4:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_09,BSP_IO_LEVEL_LOW); //P,COM5
break;
case 5:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_03,BSP_IO_LEVEL_LOW); //P,COM6
break;
default:
break;
}
}
/**
* @brief 关闭所有数码管的位选端
* @details 将所有数码管的公共端(COM)置为高电平,使其暂时不显示,防止扫描时出现重影
*/
void Close_Com(void)
{
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_09, BSP_IO_LEVEL_HIGH); //P,COM1
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_10,BSP_IO_LEVEL_HIGH); //P,COM2
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_11,BSP_IO_LEVEL_HIGH); //P,COM3
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_08,BSP_IO_LEVEL_HIGH); //P,COM4
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_09,BSP_IO_LEVEL_HIGH); //P,COM5
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_03,BSP_IO_LEVEL_HIGH); //P,COM6
}
/**
* @brief 电压校准模式显示函数
* @details 将电压校准值格式化后存入显示寄存器,控制数码管显示带小数点的电压校准数据
* @param value 待显示的电压校准原始值(ADC采样相关数值)
*/
void DisplaySETV(uint32_t value)
{
uint8_t Thousands; // 千位
uint8_t Hundreds; // 百位
uint8_t Tens; // 十位
uint8_t Units; // 个位数
Thousands = value / 1000; // 判断是否有千位数值
if(Thousands > 0)
{
Units = value % 10;
value = Units > 5 ? (value + 10) : value; // 根据后一位四舍五入
Thousands = value / 1000 % 10;
Hundreds = value / 100 % 10;
Tens = value / 10 % 10;
// 显示xx.x伏
Seg_Reg[3] = Thousands;
Seg_Reg[4] = Hundreds + 10; // 加dp显示
Seg_Reg[5] = Tens;
}
else
{
Units = value % 10;
Tens = value / 10 % 10;
Hundreds = value / 100 % 10;
// 显示x.xx伏
Seg_Reg[3] = Hundreds + 10; // 加dp显示
Seg_Reg[4] = Tens;
Seg_Reg[5] = Units;
}
}
/**
* @brief 常规电压显示函数
* @details 将电压值格式化后存入显示寄存器,控制数码管显示带小数点的实际电压值
* @param value 待显示的电压值(经校准后的数值,单位:10mV)
*/
void Display(uint32_t value)
{
uint8_t Thousands;
uint8_t Hundreds;
uint8_t Tens;
uint8_t Units; // 个位数
Thousands = value / 1000;
if(Thousands > 0)
{
Units = value % 10;
value = Units > 5 ? (value + 10) : value; // 根据后一位四舍五入
Thousands = value / 1000 % 10;
Hundreds = value / 100 % 10;
Tens = value / 10 % 10;
// 显示xx.x伏
Seg_Reg[0] = Thousands;
Seg_Reg[1] = Hundreds + 10; // 加dp显示
Seg_Reg[2] = Tens;
}
else
{
Units = value % 10;
Tens = value / 10 % 10;
Hundreds = value / 100 % 10;
// 显示x.xx伏
Seg_Reg[0] = Hundreds + 10; // 加dp显示
Seg_Reg[1] = Tens;
Seg_Reg[2] = Units;
}
}
/**
* @brief 电流显示函数
* @details 将电流值格式化后存入显示寄存器,控制数码管显示带小数点的实际电流值
* @param value 待显示的电流值(经校准后的数值,单位:mA)
*/
void DisplayI(uint32_t value)
{
uint8_t Thousands;
uint8_t Hundreds;
uint8_t Tens;
uint8_t Units; // 个位数
Seg_Reg[3] = value/100 + 10;// 加dp显示
Seg_Reg[4] = value%100/10;
Seg_Reg[5] = value%10;
}
/**
* @brief 数码管扫描显示函数
* @details 由定时器周期性调用(如1ms一次),依次刷新6个数码管,利用人眼视觉暂留实现全屏显示
* 每次只点亮一个数码管,避免多个数码管同时点亮导致的段选冲突和重影
*/
void Dis_Refresh(void)
{
static uint8_t num = 0; // 静态变量,记录当前扫描的数码管位置(0~5循环)
Close_Com(); //先关闭公共端,防止重影
Seg_Dis(num,Seg_Reg[num]); // 显示当前位置的数码管
num++; // 切换显示到下一个位置
if(num > 6)
{
num = 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
理论上我们采集到的TL431的电压是2.5V,而实际上采集到来显示的电压是2.49V(这个因为实际采样的误差与显示位数导致的),最终的结果展示如下:
不同的器件,所输出的电压有差异,所以显示的数据还存在一定的偏差,以实际测试为准。
3.8 实验六:均值滤波
3.8.1 常见的滤波算法
在嵌入式软件开发中,常用的软件滤波信号算法多种多样,每种算法都有其特定的应用场景和优缺点。以下是一些常见的软件滤波算法:
- 限幅滤波法(又称程序判断滤波法)
- 方法:根据经验判断,确定两次采样允许的最大偏差值(设为A)。每次检测到新值时判断,如果本次值与上次值之差小于等于A,则本次值有效;如果大于A,则本次值无效,放弃本次值,用上次值代替本次值。
- 优点:能有效克服因偶然因素引起的脉冲干扰。
- 缺点:无法抑制周期性干扰,平滑度差。
- 中位值滤波法
- 方法:连续采样N次(N取奇数),把N次采样值按大小排列,取中间值为本次有效值。
- 优点:能有效克服因偶然因素引起的波动干扰,对温度、液位等变化缓慢的被测参数有良好的滤波效果。
- 缺点:对流量、速度等快速变化的参数不宜。
- 算术平均滤波法
- 方法:连续取N个采样值进行算术平均运算。N值较大时,信号平滑度较高但灵敏度较低;N值较小时,信号平滑度较低但灵敏度较高。
- 优点:适用于对一般具有随机干扰的信号进行滤波。
- 缺点:对于测量速度较慢或要求数据计算速度较快的实时控制不适用,且比较浪费RAM。
- 递推平均滤波法(又称滑动平均滤波法)
- 方法:把连续取N个采样值看成一个队列,队列的长度固定为N。每次采样到一个新数据放入队尾,并扔掉原来队首的一次数据(先进先出原则)。把队列中的N个数据进行算术平均运算,即可获得新的滤波结果。
- 优点:对周期性干扰有良好的抑制作用,平滑度高,适用于高频振荡的系统。
- 缺点:灵敏度低,对偶然出现的脉冲性干扰的抑制作用较差,不适用于脉冲干扰比较严重的场合,且比较浪费RAM。
- 中位值平均滤波法(又称防脉冲干扰平均滤波法)
- 方法:相当于“中位值滤波法”+“算术平均滤波法”。连续采样N个数据,去掉一个最大值和一个最小值,然后计算N-2个数据的算术平均值。
- 优点:融合了两种滤波法的优点,对于偶然出现的脉冲性干扰,可消除由于脉冲干扰所引起的采样值偏差。
- 缺点:测量速度较慢,比较浪费RAM。
- 限幅平均滤波法
- 方法:相当于“限幅滤波法”+“递推平均滤波法”。每次采样到的新数据先进行限幅处理,再送入队列进行递推平均滤波处理。
- 优点:融合了两种滤波法的优点,对于偶然出现的脉冲性干扰,可消除由于脉冲干扰所引起的采样值偏差。
- 缺点:比较浪费RAM。
- 一阶滞后滤波法
- 方法:取a=0~1,本次滤波结果=(1-a)本次采样值+a上次滤波结果。
- 优点:对周期性干扰具有良好的抑制作用,适用于波动频率较高的场合。
- 缺点:相位滞后,灵敏度低,滞后程度取决于a值大小,不能消除滤波频率高于采样频率的1/2的干扰信号。
- 加权递推平均滤波法
- 方法:是对递推平均滤波法的改进,即不同时刻的数据加以不同的权。通常是越接近现时刻的数据,权取得越大。
- 优点:适用于有较大纯滞后时间常数的对象和采样周期较短的系统。
- 缺点:对于纯滞后时间常数较小、采样周期较长、变化缓慢的信号,不能迅速反应系统当前所受干扰的严重程度,滤波效果差。
- 消抖滤波法
- 方法:设置一个滤波计数器,将每次采样值与当前有效值比较。如果采样值等于当前有效值,则计数器清零;如果不等,则计数器加1,并判断计数器是否大于等于上限N(溢出)。如果计数器溢出,则将本次值替换当前有效值,并清计数器。
- 优点:对于变化缓慢的被测参数有较好的滤波效果,可避免在临界值附近控制器的反复开/关跳动或显示器上数值抖动。
- 缺点:对于快速变化的参数不宜,如果在计数器溢出的那一次采样到的值恰好是干扰值,则会将干扰值当作有效值导入系统。
- 其他滤波方法
除了上述常见的滤波方法外,还有如移动平均滤波、卡尔曼滤波等算法。
3.8.2 均值滤波
均值滤波也称为线性滤波,其采用的主要方法为邻域平均法。线性滤波的基本原理是用均值代替原图像中的各个像素值,即对待处理的当前像素点(x,y),选择一个模板,该模板由其近邻的若干像素组成,求模板中所有像素的均值,再把该均值赋予当前像素点(x,y),作为处理后图像在该点上的灰度g(x,y),即g(x,y)=∑f(x,y)/m,m为该模板中包含当前像素在内的像素总个数。
这本是数字图像处理的一种方法,但也可以用在我们数字电压电流表的ADC采样数据上。我们选取二十次的ADC采样值存储在数组 Volt_Buffer 中,然后去除掉数组中的最大值和最小值后再取平均,得到的值作为结果显示在数码管上,这样可以较大程度获得准确的、不易波动的数据。
程序在实验五的基础上略作修改即可:
#include "Apply/app.h" // 应用层
#include "seg/bsp_seg.h" // 数码管驱动
#include "gpt/bsp_gpt.h" // 通用定时器驱动
#include "adc/bsp_adc.h" // ADC(模数转换)驱动
#include "uart/bsp_uart.h" // UART(串口)驱动
#include "led/bsp_led.h" // LED驱动
// ADC采样相关宏定义
#define ADC_SAMPLE_SIZE (100) // ADC采样点数,用于均值滤波(多次采样减少噪声影响)
uint16_t Volt_Buffer[ADC_SAMPLE_SIZE];
extern uint16_t Volt_Buffer[ADC_SAMPLE_SIZE];
extern uint8_t Seg_Reg[6]; // 数码管显示寄存器
unsigned int timecount=0; // 主循环计时计数器
uint32_t led_count=0; // LED闪烁计数器
uint16_t V_Buffer=0;
uint16_t Voltage = 0;
/**
* @brief 运用函数
* @details 运行逻辑
*/
void Run(void)
{
bsp_Init(); // 初始化硬件外设
DisplayI(000); //最下面显示 000
while(1)
{
Get_ADC_Value();
if( timecount >= 500)
{
Voltage = V_Buffer*3.3/40.96; //将采集的数据扩大100倍,能显示小数点后两位
Display(Voltage); //更新数码管显示,只要前3位数据
P402_Toggle; //工作指示灯
timecount=0;
}
}
}
/**
* @brief 硬件初始化函数
* @details 初始化系统所需的外设
* 读取历史校准数据并计算校准斜率,为系统运行做准备。
*/
void bsp_Init(void)
{
UART0_Init(); // 初始化串口0
GPT_Init(); // 初始化通用定时器
ADC_Init(); // 初始化ADC模块
}
/**
* @brief 获取ADC采样值
* @details 循环采集电压和电流的ADC原始值,填充采样缓冲区,
* 当缓冲区填满后重置计数器,实现循环采样。
*/
void Get_ADC_Value(void)
{
static uint8_t cnt;
Volt_Buffer[cnt] = ADC_read_Value() ;
cnt++;
if(cnt >= ADC_SAMPLE_SIZE)
{
V_Buffer = Mean_Value_Filter(Volt_Buffer,ADC_SAMPLE_SIZE);
cnt = 0;
}
}
/**
* @brief 均值滤波函数
* @details 对一组采样值进行滤波处理,去除最大值和最小值后求平均,
* 减少极端值对结果的影响,提高数据稳定性。
* @param value 采样值数组(待滤波数据)
* @param size 采样值数量(数组长度)
* @return 滤波后的平均值(uint32_t类型)
*/
uint32_t Mean_Value_Filter(uint16_t *value, uint32_t size) //均值滤波
{
uint32_t sum = 0;
uint16_t max = 0;
uint16_t min = 0xffff;
int i;
for(i = 0; i < size; i++)
{
sum += value[i];
if(value[i] > max)
{
max = value[i];
}
if(value[i] < min)
{
min = value[i];
}
printf("value[%2d]=%d",i,value[i]);
}
sum -= max + min;
sum = sum / (size - 2);
printf("\nsum=%ld\n",sum);
printf("\r\n");
return sum;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
最终的结果展示如下,与之前的结果相比,数码管显示的值更加稳定,滤除了干扰对 MCU 工作的影响。
注:这里是用串口打印数据,方便查看,建议直接用我们给的工程代码进行验证。
3.9 实验七:六位数显的电流通道采集电压显示
3.9.1 实验目的
简单来说就是用两个数码管显示电压,这个电压是通过电流通道采集而来。
当我们需要检测MCU AD采样精度时,往往从MCU AD引脚上输入一个确定的可调电压,再通过编程对引脚电压进行采集计算,与实际进行比较,可以观察ADC转换的性能。 在开发板中,我们可以通过电流通道采集电压进行显示,来判断AD转换性能。
3.9.2 实验分析
电流采样电路如下:
电流采样通过ADC_IN0 通道完成。
我们可以使用PR2电位器调节模拟I+输入的电压值。该电压值的实际值,可使用万用表测试TI+端与GND端的电压值可以得到。数码管可以显示MCU测量计算的电压值。实际值与测理值做比较即分析Ad转换处理的配置或性能是否需要改进。
注意:该实验中,R0不焊。JP2跳线短接。
使用RP2提供的电压范围为:在0~0.238V(5V÷210K*10K)范围内的电压值(理想值,实际可能由电阻精度决定),经由I﹢网络,接入到芯片用于电流采样的引脚上。
3.9.3 重要代码讲解
#include "Apply/app.h" // 应用层
#include "seg/bsp_seg.h" // 数码管驱动
#include "gpt/bsp_gpt.h" // 通用定时器驱动
#include "adc/bsp_adc.h" // ADC(模数转换)驱动
#include "uart/bsp_uart.h" // UART(串口)驱动
#include "led/bsp_led.h" // LED驱动
// ADC采样相关宏定义
#define ADC_SAMPLE_SIZE (100) // ADC采样点数,用于均值滤波(多次采样减少噪声影响)
uint16_t Volt_Buffer[ADC_SAMPLE_SIZE];
extern uint16_t Volt_Buffer[ADC_SAMPLE_SIZE];
extern uint8_t Seg_Reg[6];
unsigned int timecount=0;
uint32_t led_count=0;
uint16_t V_Buffer=0;
uint32_t mameter = 0;
void Run(void)
{
UART0_Init();
GPT_Init();
ADC_Init();
while(1)
{
Get_ADC_Value(); //ADC采集
if( timecount >= 500)
{
mameter = V_Buffer * 3.3 * 10 /4096 * 1000; //将采集的数据扩大1000倍,尽量保持与万用表测试出来一致
DisplayValue(mameter); //数码管显示
P402_Toggle; //工作指示灯
timecount=0;
}
}
}
/**
* @brief 获取ADC采样值
* @details 循环采集电压和电流的ADC原始值,填充采样缓冲区,
* 当缓冲区填满后重置计数器,实现循环采样。
*/
void Get_ADC_Value(void)
{
static uint8_t cnt;
Volt_Buffer[cnt] = ADC_read_Value() ;
cnt++;
if(cnt >= ADC_SAMPLE_SIZE)
{
V_Buffer = Mean_Value_Filter(Volt_Buffer,ADC_SAMPLE_SIZE);
cnt = 0;
}
}
/**
* @brief 均值滤波函数
* @details 对一组采样值进行滤波处理,去除最大值和最小值后求平均,
* 减少极端值对结果的影响,提高数据稳定性。
* @param value 采样值数组(待滤波数据)
* @param size 采样值数量(数组长度)
* @return 滤波后的平均值(uint32_t类型)
*/
uint32_t Mean_Value_Filter(uint16_t *value, uint32_t size) //均值滤波
{
uint32_t sum = 0;
uint16_t max = 0;
uint16_t min = 0xffff;
int i;
for(i = 0; i < size; i++)
{
sum += value[i];
if(value[i] > max)
{
max = value[i];
}
if(value[i] < min)
{
min = value[i];
}
printf("value[%2d]=%d",i,value[i]);
}
sum -= max + min;
sum = sum / (size - 2);
printf("\nsum=%ld\n",sum);
printf("\r\n");
return sum;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include "bsp_adc.h"
#include "r_adc_api.h"
#include "hal_data.h"
#include <stdio.h>
volatile bool adc_flag = false;
/**
* @brief ADC模块初始化函数
* @details 初始化ADC硬件,配置ADC控制器和扫描参数,为ADC数据采集做准备
* @return 无
*/
void ADC_Init(void)
{
fsp_err_t err;
// 打开ADC设备
err = R_ADC_Open(&g_adc0_ctrl, &g_adc0_cfg);
if (FSP_SUCCESS != err) {
printf("ADC初始化失败! \n");
return;
}
// 配置ADC扫描
err = R_ADC_ScanCfg(&g_adc0_ctrl, &g_adc0_channel_cfg);
if (FSP_SUCCESS != err) {
printf("ADC扫描配置失败! \n");
R_ADC_Close(&g_adc0_ctrl); // 关闭已打开的ADC
return;
}
printf("ADC初始化成功!\n");
}
/**
* @brief ADC转换完成回调函数
* @details 当ADC完成一次扫描转换后,由硬件中断触发此函数
* @param p_args ADC回调参数
* @return 无
*/
void adc_callback(adc_callback_args_t *p_args)
{
FSP_PARAMETER_NOT_USED(p_args);
adc_flag = true;
}
/**
* @brief 读取ADC通道数据
* @details 启动ADC扫描,等待转换完成后,读取指定通道的ADC原始值
* @return 读取到的ADC值(0,4095)
*/
uint16_t ADC_read_Value(void)
{
uint16_t adc_data = 0;
// 启动ADC扫描
fsp_err_t err = R_ADC_ScanStart(&g_adc0_ctrl);
if (FSP_SUCCESS != err) {
printf("ADC扫描启动失败! \n");
return 9999; // 返回错误值
}
// 等待ADC转换完成
while (!adc_flag);
adc_flag = false; // 清除标志位
// 读取ADC数据
err = R_ADC_Read(&g_adc0_ctrl, ADC_CHANNEL_0, &adc_data);
if (FSP_SUCCESS != err) {
printf("ADC数据读取失败!\n");
return 9999; // 返回错误值
}
return adc_data;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <seg/bsp_seg.h>
#include "hal_data.h"
/* 共阴数码管编码表(每个字节对应数码管的8段LED状态):
低7位对应A~G段,第7位(最高位)对应小数点(DP)
0x3f: 0 0x06: 1 0x5b: 2 0x4f: 3 0x66: 4 0x6d: 5 0x7d: 6 0x07: 7 0x7f: 8 0x6f: 9
0xbf: 0.(带小数点) 0x86: 1. 0xdb: 2. 0xcf: 3. 0xe6: 4. 0xed: 5. 0xfd: 6. 0x87: 7. 0xff: 8. 0xef: 9.
*/
uint8_t Seg_Table[21] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f,
0xbf, 0x86, 0xdb, 0xcf, 0xe6, 0xed, 0xfd, 0x87, 0xff, 0xef,0xF7};
uint8_t Seg_Reg[6] = {1,2,3,4,5,6};
/**
* @brief 数码管单一位显示函数
* @details 根据指定位置和数值,控制对应数码管的段选和位选引脚,显示相应字符
* @param Pos 数码管位置(0~5,对应6个数码管的位选端)
* @param Num 待显示数值索引(对应Seg_Table中的索引,如0对应数字0,10对应0.)
*/
void Seg_Dis(uint8_t Pos,uint8_t Num)
{
int i;
uint8_t Dis_Value; // 从编码表中获取的段选数据
Dis_Value = Seg_Table[Num]; // 根据数值索引获取段选编码
for(i = 0; i < 8; i++)
{
switch(i)
{
case 0:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_03_PIN_02, (Dis_Value >> i) & 0x01);//P,A
break;
case 1:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_02_PIN_07, (Dis_Value >> i) & 0x01);//P,B
break;
case 2:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_02, (Dis_Value >> i) & 0x01);//P,C
break;
case 3:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_04, (Dis_Value >> i) & 0x01);//P,D
break;
case 4:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_03_PIN_01, (Dis_Value >> i) & 0x01);//P,E
break;
case 5:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_02_PIN_06, (Dis_Value >> i) & 0x01);//P,F
break;
case 6:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_12, (Dis_Value >> i) & 0x01);//P,G
break;
case 7:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_03, (Dis_Value >> i) & 0x01);//P,DP
break;
default:
break;
}
}
switch(Pos)
{
case 0:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_09, BSP_IO_LEVEL_LOW); //P,COM1
break;
case 1:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_10,BSP_IO_LEVEL_LOW); //P,COM2
break;
case 2:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_11,BSP_IO_LEVEL_LOW); //P,COM3
break;
case 3:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_08,BSP_IO_LEVEL_LOW); //P,COM4
break;
case 4:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_09,BSP_IO_LEVEL_LOW); //P,COM5
break;
case 5:
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_03,BSP_IO_LEVEL_LOW); //P,COM6
break;
default:
break;
}
}
/**
* @brief 关闭所有数码管的位选端
* @details 将所有数码管的公共端(COM)置为高电平,使其暂时不显示,防止扫描时出现重影
*/
void Close_Com(void)
{
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_09, BSP_IO_LEVEL_HIGH); //P,COM1
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_10,BSP_IO_LEVEL_HIGH); //P,COM2
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_01_PIN_11,BSP_IO_LEVEL_HIGH); //P,COM3
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_08,BSP_IO_LEVEL_HIGH); //P,COM4
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_09,BSP_IO_LEVEL_HIGH); //P,COM5
R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_04_PIN_03,BSP_IO_LEVEL_HIGH); //P,COM6
}
/**
* @brief 电压校准模式显示函数
* @details 将电压校准值格式化后存入显示寄存器,控制数码管显示带小数点的电压校准数据
* @param value 待显示的电压校准原始值(ADC采样相关数值)
*/
void DisplaySETV(uint32_t value)
{
uint8_t Thousands; // 千位
uint8_t Hundreds; // 百位
uint8_t Tens; // 十位
uint8_t Units; // 个位数
Thousands = value / 1000; // 判断是否有千位数值
if(Thousands > 0)
{
Units = value % 10;
value = Units > 5 ? (value + 10) : value; // 根据后一位四舍五入
Thousands = value / 1000 % 10;
Hundreds = value / 100 % 10;
Tens = value / 10 % 10;
// 显示xx.x伏
Seg_Reg[3] = Thousands;
Seg_Reg[4] = Hundreds + 10; // 加dp显示
Seg_Reg[5] = Tens;
}
else
{
Units = value % 10;
Tens = value / 10 % 10;
Hundreds = value / 100 % 10;
// 显示x.xx伏
Seg_Reg[3] = Hundreds + 10; // 加dp显示
Seg_Reg[4] = Tens;
Seg_Reg[5] = Units;
}
}
/**
* @brief 常规电压显示函数
* @details 将电压值格式化后存入显示寄存器,控制数码管显示带小数点的实际电压值
* @param value 待显示的电压值(经校准后的数值,单位:10mV)
*/
void Display(uint32_t value)
{
uint8_t Thousands;
uint8_t Hundreds;
uint8_t Tens;
uint8_t Units; // 个位数
Thousands = value / 1000;
if(Thousands > 0)
{
Units = value % 10;
value = Units > 5 ? (value + 10) : value; // 根据后一位四舍五入
Thousands = value / 1000 % 10;
Hundreds = value / 100 % 10;
Tens = value / 10 % 10;
// 显示xx.x伏
Seg_Reg[0] = Thousands;
Seg_Reg[1] = Hundreds + 10; // 加dp显示
Seg_Reg[2] = Tens;
}
else
{
Units = value % 10;
Tens = value / 10 % 10;
Hundreds = value / 100 % 10;
// 显示x.xx伏
Seg_Reg[0] = Hundreds + 10; // 加dp显示
Seg_Reg[1] = Tens;
Seg_Reg[2] = Units;
}
}
/**
* @brief 电流显示函数
* @details 将电流值格式化后存入显示寄存器,控制数码管显示带小数点的实际电流值
* @param value 待显示的电流值(经校准后的数值,单位:mA)
*/
void DisplayI(uint32_t value)
{
uint8_t Thousands;
uint8_t Hundreds;
uint8_t Tens;
uint8_t Units; // 个位数
Seg_Reg[3] = value/100 + 10;// 加dp显示
Seg_Reg[4] = value%100/10;
Seg_Reg[5] = value%10;
}
/**
* @brief 六位显示函数
* @details 将值格式化后存入显示寄存器,控制数码管显示不带小数点的实际电流值
* @param value 待显示的值
*/
void DisplayValue(uint32_t value)
{
uint8_t SWwei;
uint8_t Wwei;
uint8_t Thousands;
uint8_t Hundreds;
uint8_t Tens;
uint8_t Units; // 个位数
SWwei = value / 100000;
Wwei = value / 10000 %10+10;
Thousands = value / 1000 % 10;
Hundreds = value / 100 % 10;
Tens = value / 10 % 10;
Units = value % 10;
Seg_Reg[0] =SWwei;
Seg_Reg[1] =Wwei;
Seg_Reg[2] =Thousands;
Seg_Reg[3] =Hundreds;
Seg_Reg[4] =Tens;
Seg_Reg[5] =Units;
}
/**
* @brief 数码管扫描显示函数
* @details 由定时器周期性调用(如1ms一次),依次刷新6个数码管,利用人眼视觉暂留实现全屏显示
* 每次只点亮一个数码管,避免多个数码管同时点亮导致的段选冲突和重影
*/
void Dis_Refresh(void)
{
static uint8_t num = 0; // 静态变量,记录当前扫描的数码管位置(0~5循环)
Close_Com(); //先关闭公共端,防止重影
Seg_Dis(num,Seg_Reg[num]); // 显示当前位置的数码管
num++; // 切换显示到下一个位置
if(num > 6)
{
num = 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
工程运行效果图如下。显示数据表示进入引脚电压为0.2441V,后面加上校准就更加接近。
3.10 实验八:六位数显的电压通道采集电压显示
3.10.1 电流采样电路分析与计算
在实验七基础上,修改为电压显示。
如果使用3.3V作为参考电压,根据R8和R7的阻值配比可以得到最高采样电压为:
注:只允许输入采集电压为0-30V。
注:只允许输入采集电压为0-30V。
注:只允许输入采集电压为0-30V。
重要的事情说三遍。
3.10.2 重要代码讲解
其中:ADC初始化代码如下。该代码主要修改为电压通道ADCH_IN2的初始化配置。配置为内部3.3V为参考。
工程运行效果图如下。显示数据表示进入电压被测端电压为4.853V,万用表4.98V。后面加上校准就更加接近。
3.11 实验九:带有标定功能的数字电压电流表
3.11.1 标定的概念
标定是通过测量标准器的偏差来补偿仪器系统误差,从而改善仪器或系统准确度、精度的操作。为了提高电压电流表在测量时的测量精度和准确度,需要对电压电流进行标定校准。
常见的标定原理如下:
假设一个采样系统,AD部分可以得到数字量,对应的物理量为电压(或电流);
- 若在“零点”标定一个AD值点Xmin,在“最大处”标定一个AD值点Xmax,根据“两点成一条直线”的原理,可以得到一条由零点和最大点连起来的一条直线,这条直线的斜率k很容易求得,然后套如直线方程求解每一个点X(AD采样值),可以得到该AD值对应的物理量(电压值):
上图中的斜率k:
(因为第一点为“零点”,故上面的Ymin = 0)
所以,上图中任一点的AD值对应的物理量:
- 上面的算法只是在“零点”和“最大点”之间做了标定,如果使用中间的AD采样值会带来很大的对应物理量的误差,解决的办法是多插入一些标定点。
如下图,分别插入了标定点(x1,y1)、(x2,y2)、(x3,y3)、(x4,y4) 四个点:
这样将获得不再是一条直线,而是一条“折现”(相当于分段处理),若欲求解落在x1和x2之间一点Xad值对应的电压值:
由上看出,中间插入的“标定点”越多,得到物理值“精度”越高。
在电压电流表测量可以使用“电压电流标定板”“万用表”等配合适合,对采集的电压电流进行标定处理。标定点越多,测量越精确。
3.11.2 重要代码讲解
参考例程中,使用了3点标定。其中,电压标定点为0V、5V、15V。电流标定点为0A、0.5A、1.5A。
主程序代码如下:
#include "Apply/app.h" // 应用层
#include "seg/bsp_seg.h" // 数码管驱动
#include "gpt/bsp_gpt.h" // 通用定时器驱动
#include "adc/bsp_adc.h" // ADC(模数转换)驱动
#include "uart/bsp_uart.h" // UART(串口)驱动
#include "led/bsp_led.h" // LED驱动
#include "flash/bsp_flash.h" // FLASH(闪存)驱动
// ADC采样相关宏定义
#define ADC_SAMPLE_SIZE (100) // ADC采样点数,用于均值滤波(多次采样减少噪声影响)
#define ADC_REF_VALUE (3300) // ADC参考电压,单位:mV
// 电阻参数(用于电压/电流检测电路)
#define R2 (220) // 分压电阻R2,单位:KΩ
#define R1 (10) // 分压电阻R1,单位:KΩ
#define R3 (0.1) // 采样电阻R3,单位:Ω
/* 按键状态宏定义 */
#define KEY_ON 1 // 按键按下状态
#define KEY_OFF 0 // 按键未按下状态
// 电压/电流数据转换系数(用于单位换算)
#define V_data 10 // 电压计算除数
#define I_data 10 // 电流计算除数
// 外部引用声明
extern uint16_t Volt_Buffer[ADC_SAMPLE_SIZE];
extern uint16_t Curr_Buffer[ADC_SAMPLE_SIZE];
extern uint8_t Seg_Reg[6]; // 数码管显示寄存器
extern bool Scan_count; // 扫描计数标志
extern bool Collect_flang=0; // ADC采样完成标志
// ADC采样缓冲区(存储原始采样值)
uint16_t Volt_Buffer[ADC_SAMPLE_SIZE]; // 电压采样值缓冲区
uint16_t Curr_Buffer[ADC_SAMPLE_SIZE]; // 电流采样值缓冲区
uint16_t V_Buffer,I_Buffer; // 校准后的电压/电流值
unsigned char BrushFlag=0; // 数码管刷新标志
unsigned int timecount=0; // 主循环计时计数器
uint32_t ledcount=0; // LED闪烁计数器
// 电压校准参数(5V和15V校准点)
unsigned int X05=0;
unsigned int X15=0;
unsigned int Y15=15;
unsigned int Y05=5;
float K; // 电压校准曲线斜率
//0.5A与1.5A 校准
unsigned int IX05=0;
unsigned int IX15=0;
unsigned int IY15=150;
unsigned int IY05=50;
float KI; //斜率
//定义模式
unsigned char Mode=0;
//mode0 :电压电流常规显示模式
//mode1 :电压5V校准
//mode2 :电压15V校准
//mode3 :电流0.5A校准
//mode4 :电流1.5A校准
/**
* @brief 硬件初始化函数
* @details 初始化系统所需的外设
* 读取历史校准数据并计算校准斜率,为系统运行做准备。
*/
void bsp_Init(void)
{
UART0_Init(); // 初始化串口0
FLASH_Init(); // 初始化FLASH(用于存储校准参数)
GPT_Init(); // 初始化通用定时器
ADC_Init(); // 初始化ADC模块
}
/**
* @brief 数据校准
* @details 读取历史校准数据并计算校准斜率
*/
void Proofread(void)
{
read_vol_cur_calibration(); // 读取电压/电流校准参数
ComputeK(); // 根据校准参数计算电压/电流转换斜率
}
void Run(void)
{
bsp_Init(); // 初始化硬件外设
Proofread(); // 数据校准
while(1)
{
Get_ADC_Value(); //ADC采集
if(Scan_count) // 若触发功能切换标志
{
Scan(); // 按键扫描及模式切换
}
if(BrushFlag==1) //数码管刷新
{
DisplayBuff(); // 更新数码管显示内容
BrushFlag=0; // 清除刷新标志
}
if( Collect_flang == 1 && timecount>= 50000)//计算值
{
timecount=0;
Collect_flang=0;
Volt_Cal(); // 计算校准后的电压/电流值
BrushFlag=1;
}
ledcount++;
if(ledcount >= 500000)
{
ledcount = 0;
P402_Toggle; //工作指示灯
}
}
}
/**
* @brief 获取ADC采样值
* @details 循环采集电压和电流的ADC原始值,填充采样缓冲区,
* 当缓冲区填满后重置计数器,实现循环采样。
*/
void Get_ADC_Value(void)
{
static uint8_t cnt;
ADC_read_Value(&Curr_Buffer[cnt], &Volt_Buffer[cnt]); // 读取ADC模块的电流和电压采样值,存入缓冲区对应位置
cnt++;
if(cnt >= ADC_SAMPLE_SIZE) // 若缓冲区已满(达到设定采样点数)
{
Collect_flang = 1;
cnt = 0;
}
}
/**
* @brief 均值滤波函数
* @details 对一组采样值进行滤波处理,去除最大值和最小值后求平均,
* 减少极端值对结果的影响,提高数据稳定性。
* @param value 采样值数组(待滤波数据)
* @param size 采样值数量(数组长度)
* @return 滤波后的平均值(uint32_t类型)
*/
uint32_t Mean_Value_Filter(uint16_t *value, uint32_t size) //均值滤波
{
uint32_t sum = 0; // 采样值总和
uint16_t max = 0; // 最大值(初始化为最小值)
uint16_t min = 0xffff; // 最小值(初始化为最大值)
int i;
for(i = 0; i < size; i++)
{
sum += value[i]; // 累加总和
if(value[i] > max)
{
max = value[i];
}
if(value[i] < min)
{
min = value[i];
}
}
sum -= max + min; // 总和减去最大值和最小值(剔除极端值)
sum = sum / (size - 2); // 求剩余值的平均值
// printf("sum=%d \n",sum);
//if(sum>1)sum+=4; 后期校准
return sum; // 返回滤波后的平均值
}
/**
* @brief 电压电流校准计算
* @details 对滤波后的ADC值进行校准转换,校准斜率计算实际电压和电流值,
* 并通过四舍五入优化结果精度,最终存储到显示缓冲区。
*/
void Volt_Cal(void)
{
float t,KT1;
uint32_t V_data_Buff; // 滤波后的电压ADC值
uint32_t I_data_Buff; // 滤波后的电流ADC值
V_data_Buff = Mean_Value_Filter(Volt_Buffer,ADC_SAMPLE_SIZE);//使用均值滤波
I_data_Buff = Mean_Value_Filter(Curr_Buffer,ADC_SAMPLE_SIZE); //使用均值滤波
// 电压校准计算(将ADC值转换为实际电压)
if(V_data_Buff>=X05)
{
t=V_data_Buff-X05;
V_data_Buff=(K*t+Y05)*1000;
}
else
{
KT1=5000;
KT1=KT1/X05;
V_data_Buff=KT1*V_data_Buff;
}
if(V_data_Buff % 10 >= 5)
{
V_Buffer = V_data_Buff / V_data + 1; //10mV为单位
}
else
{
V_Buffer = V_data_Buff / V_data;
}
if(I_data_Buff>=IX05)
{
t=I_data_Buff-IX05;
I_data_Buff=(KI*t+IY05)*10;
}
else
{
KT1=500;
KT1=KT1/IX05;
I_data_Buff=KT1*I_data_Buff;
}
if(I_data_Buff % 10 >= 5)
{
I_Buffer = I_data_Buff / I_data + 1;
}
else
{
I_Buffer = I_data_Buff / I_data;
}
}
/**
* @brief 数码管显示缓冲区更新
* @details 根据当前工作模式,更新数码管寄存器的内容,
* 控制数码管显示常规电压电流或校准模式信息。
*/
void DisplayBuff(void)
{
if(Mode==0) //正常显示
{
Display(V_Buffer);
if(I_Buffer>400)I_Buffer=400;
DisplayI(I_Buffer);
}
else if(Mode==1) // 电压5V校准模式(显示"S.05."标识)
{
Seg_Reg[0] =5+10;
Seg_Reg[1] =0;
Seg_Reg[2]=5+10;
DisplaySETV(V_Buffer);
}
else if(Mode==2) // 电压15V校准模式(显示"S.15."标识)
{
Seg_Reg[0] =5+10;
Seg_Reg[1] =1;
Seg_Reg[2]=5+10;
DisplaySETV(V_Buffer);
}
else if(Mode==3) // 电流0.5A校准模式(显示"A.0.5"标识)
{
Seg_Reg[0] =20;
Seg_Reg[1] =0+10;
Seg_Reg[2]=5;
DisplayI(I_Buffer);
}
else if(Mode==4) // 电流1.5A校准模式(显示"A.1.5"标识)
{
Seg_Reg[0] =20;
Seg_Reg[1] =1+10;
Seg_Reg[2]=5;
DisplayI(I_Buffer);
}
}
/**
* @brief 计算校准斜率
* @details 根据电压和电流的校准点(已知实际值和对应ADC值),
* 计算转换斜率K(电压)和KI(电流),用于ADC值到实际值的线性转换。
*/
void ComputeK(void)
{
K=(Y15-Y05);
K=K/(X15-X05);
KI=(IY15-IY05);
KI=KI/(IX15-IX05);
}
/**
* @brief 保存校准参数到FLASH
* @details 将电压和电流的校准点参数(X05、X15、IX05、IX15)存储到FLASH,
* 并添加校验值(0xbb)用于判断是否已校准。
*/
void save_calibration(void)
{
uint32_t da[5];
da[0]=0xbb; // 校验值
da[1]=X05;
da[2]=X15;
da[3]=IX05;
da[4]=IX15;
FLASH_Write_Data(&da,sizeof(da)); // 将数据写入FLASH
}
/**
* @brief 上电前,读取电压电流校准参数,避免上电校准。
* @details 从FLASH读取校准参数,若未校准(无校验值),则计算理论初始值并保存,
* 确保系统有可用的校准参数。
*/
void read_vol_cur_calibration(void)
{
uint32_t da1[5];
FLASH_read_Data(&da1,sizeof(da1)); // 读取FLASH中的校准数据
if(da1[0]!=0xbb) //还没校准过时,计算理论值,并存储
{
X15=15.0/((R2+R1)/R1+1)/3.3*4096;
X05=5.0/((R2+R1)/R1+1)/3.3*4096;
IX05=0.5*R3/3.3*4096;
IX15=1.5*R3/3.3*4096;
save_calibration();
}
else
{
X05=da1[1];
X15=da1[2];
IX05=da1[3];
IX15=da1[4];
}
}
/**
* @brief 按键扫描与功能处理
* @details 检测按键状态,实现工作模式切换和校准参数保存,
* 支持在非常规模式下长按按键自动返回常规模式。
*/
void Scan(void)
{
static uint32_t keytime3=0; // 按键长按计时(静态变量,记录长按时间)
timecount++;
// 模式切换按键
if(Key_Scan(BSP_IO_PORT_00_PIN_15) == KEY_ON)
{
Mode++;
if(Mode>=5)
{
Mode=0;
}
BrushFlag=1; //更新数码管
}
// 按键校准保存
if((Key_Scan(BSP_IO_PORT_00_PIN_14) == KEY_ON ) && Mode!=0)
{
keytime3=0;
if(Mode==1) // 电压5V校准模式:保存当前电压ADC值为X05
{
X05=Mean_Value_Filter(Volt_Buffer,ADC_SAMPLE_SIZE);
save_calibration();ComputeK();Volt_Cal();BrushFlag=1;
}
if(Mode==2) // 电压15V校准模式:保存当前电压ADC值为X15
{
X15=Mean_Value_Filter(Volt_Buffer,ADC_SAMPLE_SIZE);
save_calibration();ComputeK();Volt_Cal();BrushFlag=1;
}
if(Mode==3) // 电流0.5A校准模式:保存当前电流ADC值为IX05
{
IX05=Mean_Value_Filter(Curr_Buffer,ADC_SAMPLE_SIZE);
save_calibration();ComputeK();Volt_Cal();BrushFlag=1;
}
if(Mode==4) // 电流1.5A校准模式:保存当前电流ADC值为IX15
{
IX15=Mean_Value_Filter(Curr_Buffer,ADC_SAMPLE_SIZE);
save_calibration();ComputeK();Volt_Cal();BrushFlag=1;
}
}
if((Key_Scan(BSP_IO_PORT_00_PIN_14) != KEY_ON ) && Mode != 0)
{
keytime3++;
if(keytime3>=5000000 )
{
keytime3=0; //切换模式
Mode=0;
}
}
}
/**
* @brief 按键状态扫描函数
* @details 读取指定引脚的电平状态,判断按键是否按下(支持按键释放检测,防抖动)。
* @param key 按键对应的引脚(bsp_io_port_pin_t类型)
* @return KEY_ON:按键按下;KEY_OFF:按键未按下
*/
uint32_t Key_Scan(bsp_io_port_pin_t key)
{
bsp_io_level_t state; // 引脚电平状态
R_IOPORT_PinRead(&g_ioport_ctrl, key, &state);// 读取按键引脚电平
if (BSP_IO_LEVEL_HIGH == state)
{
return KEY_OFF; //按键没有被按下
}
else // 若电平为低(按键按下,引脚被拉低)
{
do //等待按键释放
{
R_IOPORT_PinRead(&g_ioport_ctrl, key, &state);
} while (BSP_IO_LEVEL_LOW == state);
}
return KEY_ON; //按键被按下了
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
注:如果接 JP2 跳线帽的话,R0电阻不能焊接,若是想测试外部电流则需要焊接R0。
3.11.3 本实验的标定操作方法
该例程使用按键操作来标定。具体操作方法如下:
定义5个工作模式,K1键用于切换显示模式。K2键设置对应模式下的参数值,并保存到FLASH。K3键返回到模式0。
模式0: 显示正常的电压电流值(上一排数码管显示电压值*.V或.*V自动切换,下一排显示电流值,_.**A)
模式1: 电压5V标定值设置。上一排数码管显示5.05. 。下一排显示当前电压值_.V或._V。在该模式下,应将万用表测量被测位,调到5.00V。 按下K2键后,将当前值标定为5V电压值。
模式2: 电压15V标定值设置。上一排数码管显示5.15. 。下一排显示当前电压值_.V或._V。在该模式下,应将万用表测量被测位,调到15.0V。 按下K2键后,将当前值标定为15V电压值。
模式3: 电流0.5A标定值设置。上一排数码管显示A.0.5 。下一排显示当前电流值_.**A。按下K2键后,将当前值标定为0.5A电流值。
模式4: 电流1.5A标定值设置。上一排数码管显示A.1.5 。下一排显示当前电流值*.**A。按下K2键后,将当前值标定为1.5A电流值。