编程技术分享

分享编程知识,探讨技术创新

0%

简介:打造一套针对电子爱好者及学生学习数字示波器的基础项目,以单片机核心板与插件元器件结合起来,十分适合入门学习!

我将为您详细阐述针对电子爱好者和学生学习数字示波器的基础项目,从代码架构设计到具体C代码实现,并深入探讨项目中采用的各种经过实践验证的技术和方法。
关注微信公众号,提前获取相关推文

项目概述:

本项目旨在打造一款简易、实用、易于学习的数字示波器,采用单片机核心板作为主控,配合插件元器件,降低硬件门槛,使电子爱好者和学生能够轻松入门嵌入式系统开发和数字信号处理。该示波器将具备基本的波形采集、显示、测量功能,并预留扩展接口,方便后续的功能升级和二次开发。

代码设计架构:分层架构与模块化设计

为了构建可靠、高效、可扩展的系统平台,并考虑到学习的友好性,我将采用分层架构模块化设计相结合的方式。这种架构将系统划分为若干个独立的层次和模块,每个层次和模块负责特定的功能,层次之间通过清晰的接口进行通信,模块内部高内聚,模块之间低耦合。

分层架构:

  • 硬件抽象层 (HAL - Hardware Abstraction Layer): 这是最底层,直接与硬件打交道。HAL层封装了单片机具体的硬件操作,例如GPIO控制、ADC采集、定时器配置、SPI/I2C通信、LCD驱动等。HAL层的目的是屏蔽硬件差异,为上层提供统一的硬件访问接口,使得上层代码可以独立于具体的硬件平台。

  • 驱动层 (Driver Layer): 驱动层构建在HAL层之上,是对HAL层功能的进一步封装和抽象。驱动层提供更高级、更易用的接口,例如ADC驱动负责ADC的初始化、采样配置、数据读取;LCD驱动负责LCD的显示初始化、字符/图形显示;按键驱动负责按键扫描、去抖动、事件处理等。驱动层的目的是简化上层应用对硬件的操作,提高代码的可读性和可维护性。

  • 应用层 (Application Layer): 应用层是系统的核心逻辑层,负责实现示波器的具体功能,例如信号采集与处理、波形显示、参数测量、用户界面交互等。应用层调用驱动层提供的接口,实现业务逻辑。应用层应关注功能实现,而无需关心底层的硬件细节。

  • 系统层 (System Layer): 系统层负责系统的初始化、任务调度、资源管理、错误处理等系统级功能。系统层为整个系统提供运行环境和支撑。

模块化设计:

在每个层次内部,我们进一步采用模块化设计,将功能分解为独立的模块。例如,在应用层,可以划分为:

  • 数据采集模块 (Data Acquisition Module): 负责ADC采样、数据缓存、触发控制等。
  • 信号处理模块 (Signal Processing Module): 负责数据预处理、滤波、FFT分析(可选)、参数计算(频率、幅度、周期等)。
  • 显示模块 (Display Module): 负责波形绘制、界面元素显示、参数显示。
  • 用户界面模块 (User Interface Module): 负责按键/旋钮输入处理、菜单管理、参数设置。

代码组织结构 (示例):

1
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
DigitalOscilloscope/
├── Core/ // 单片机核心代码 (例如,基于STM32)
│ ├── Inc/ // 核心头文件
│ │ ├── stm32fxxx_hal.h
│ │ └── ...
│ └── Src/ // 核心源文件
│ ├── stm32fxxx_it.c
│ ├── system_stm32fxxx.c
│ └── ...
├── Drivers/ // 驱动层
│ ├── BSP/ // 板级支持包 (Board Support Package),针对具体硬件平台
│ │ ├── Inc/ // BSP 头文件
│ │ │ ├── bsp_led.h
│ │ │ ├── bsp_button.h
│ │ │ ├── bsp_lcd.h
│ │ │ ├── bsp_adc.h
│ │ │ └── ...
│ │ └── Src/ // BSP 源文件
│ │ ├── bsp_led.c
│ │ ├── bsp_button.c
│ │ ├── bsp_lcd.c
│ │ ├── bsp_adc.c
│ │ └── ...
│ ├── HAL/ // 硬件抽象层 (如果使用了HAL库,则可以精简)
│ │ ├── Inc/ // HAL 头文件 (根据需要自定义HAL层接口)
│ │ │ ├── hal_gpio.h
│ │ │ ├── hal_adc.h
│ │ │ ├── hal_timer.h
│ │ │ └── ...
│ │ └── Src/ // HAL 源文件 (根据需要自定义HAL层实现)
│ │ ├── hal_gpio.c
│ │ ├── hal_adc.c
│ │ ├── hal_timer.c
│ │ └── ...
│ └── Inc/ // 驱动层公共头文件 (可选)
│ └── Src/ // 驱动层公共源文件 (可选)
├── Application/ // 应用层
│ ├── DataAcquisition/ // 数据采集模块
│ │ ├── Inc/
│ │ │ ├── data_acquisition.h
│ │ │ └── ...
│ │ └── Src/
│ │ ├── data_acquisition.c
│ │ └── ...
│ ├── SignalProcessing/ // 信号处理模块
│ │ ├── Inc/
│ │ │ ├── signal_processing.h
│ │ │ └── ...
│ │ └── Src/
│ │ ├── signal_processing.c
│ │ └── ...
│ ├── Display/ // 显示模块
│ │ ├── Inc/
│ │ │ ├── display.h
│ │ │ └── ...
│ │ └── Src/
│ │ ├── display.c
│ │ └── ...
│ ├── UI/ // 用户界面模块
│ │ ├── Inc/
│ │ │ ├── ui.h
│ │ │ └── ...
│ │ └── Src/
│ │ ├── ui.c
│ │ └── ...
│ ├── Inc/ // 应用层公共头文件
│ ├── Src/ // 应用层公共源文件
│ └── app.c // 应用层入口,例如任务调度、主循环
├── System/ // 系统层 (可选)
│ ├── Inc/
│ │ ├── system_config.h
│ │ ├── system_error.h
│ │ └── ...
│ └── Src/
│ │ ├── system_init.c
│ │ ├── system_error.c
│ │ └── ...
├── Middlewares/ // 中间件 (例如,GUI库,文件系统,网络协议栈 - 可选)
├── Utilities/ // 工具库 (例如,环形缓冲区,数学库,字符串处理 - 可选)
├── User/ // 用户代码,通常 main.c 放在这里
│ ├── Inc/
│ │ ├── main.h
│ │ └── ...
│ └── Src/
│ ├── main.c
│ └── ...
├── Config/ // 配置文件,例如时钟配置,外设配置
├── Doc/ // 项目文档
├── Lib/ // 外部库 (例如,第三方算法库)
├── build/ // 编译输出目录
├── CMakeLists.txt // CMake 构建文件 (推荐使用 CMake 管理项目)
├── README.md // 项目说明文档
└── ...

C 代码实现 (核心模块示例):

由于代码量庞大,我将重点展示核心模块的代码实现,并提供详细的注释和说明。完整的代码实现会超出3000行,这里仅提供关键部分的代码框架和示例,以便您理解整体架构和实现思路。

1. HAL 层 (Hardware Abstraction Layer) 示例 (hal_adc.h, hal_adc.c):

  • hal_adc.h:
1
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
#ifndef HAL_ADC_H
#define HAL_ADC_H

#include "stdint.h"
#include "stdbool.h"

// ADC 通道定义 (根据具体硬件修改)
typedef enum {
ADC_CHANNEL_1,
ADC_CHANNEL_2,
// ... 其他通道
ADC_CHANNEL_COUNT
} HAL_ADC_ChannelTypeDef;

// ADC 分辨率定义 (根据具体硬件修改)
typedef enum {
ADC_RESOLUTION_8BIT,
ADC_RESOLUTION_10BIT,
ADC_RESOLUTION_12BIT
} HAL_ADC_ResolutionTypeDef;

// HAL ADC 初始化结构体
typedef struct {
HAL_ADC_ChannelTypeDef Channel; // ADC 通道
HAL_ADC_ResolutionTypeDef Resolution; // ADC 分辨率
// ... 其他 ADC 初始化参数 (例如采样率、时钟源等)
} HAL_ADC_InitTypeDef;

// 初始化 ADC
bool HAL_ADC_Init(HAL_ADC_InitTypeDef *init);

// 开始 ADC 转换
bool HAL_ADC_StartConversion(HAL_ADC_ChannelTypeDef channel);

// 获取 ADC 转换结果
uint16_t HAL_ADC_GetValue(HAL_ADC_ChannelTypeDef channel);

// 停止 ADC 转换
bool HAL_ADC_StopConversion(HAL_ADC_ChannelTypeDef channel);

#endif // HAL_ADC_H
  • hal_adc.c:
1
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
#include "hal_adc.h"
#include "stm32fxxx_hal.h" // 假设使用 STM32 HAL 库,根据实际单片机修改

// 静态函数,用于配置具体的 ADC 硬件寄存器 (示例,需要根据具体单片机手册实现)
static void ADC_Hardware_Config(HAL_ADC_InitTypeDef *init) {
// ... 根据 init 参数配置 ADC 寄存器,例如使能 ADC 时钟,配置 ADC 分辨率,选择 ADC 通道等
// ... 这里需要直接操作单片机的 ADC 寄存器或者使用单片机厂商提供的底层库函数 (例如 STM32 HAL 库)
// ... 示例代码 (仅为示意,需要根据具体硬件和库函数修改):
if (init->Channel == ADC_CHANNEL_1) {
// 使能 ADC 通道 1 对应的 GPIO 时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置 ADC 通道 1 对应的 GPIO 引脚为模拟输入
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0; // 假设 ADC_CHANNEL_1 对应 PA0
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
// ... 配置其他通道的 GPIO

// 使能 ADC 时钟
__HAL_RCC_ADC1_CLK_ENABLE();

// 配置 ADC 分辨率
ADC_HandleTypeDef hadc1; // 假设使用 ADC1
hadc1.Instance = ADC1;
if (init->Resolution == ADC_RESOLUTION_8BIT) {
hadc1.Init.Resolution = ADC_RESOLUTION_8B;
} else if (init->Resolution == ADC_RESOLUTION_10BIT) {
hadc1.Init.Resolution = ADC_RESOLUTION_10B;
} else if (init->Resolution == ADC_RESOLUTION_12BIT) {
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
}
// ... 其他 ADC 初始化配置 (例如数据对齐、扫描模式、连续转换模式等)
HAL_ADC_Init(&hadc1); // 使用 STM32 HAL 库初始化 ADC
}

// 初始化 ADC
bool HAL_ADC_Init(HAL_ADC_InitTypeDef *init) {
if (init == NULL) {
return false; // 参数错误
}
ADC_Hardware_Config(init); // 配置 ADC 硬件
return true;
}

// 开始 ADC 转换
bool HAL_ADC_StartConversion(HAL_ADC_ChannelTypeDef channel) {
// ... 根据 channel 参数选择 ADC 通道,并启动 ADC 转换
// ... 示例代码 (仅为示意,需要根据具体硬件和库函数修改):
ADC_HandleTypeDef hadc1; // 假设使用 ADC1
if (channel == ADC_CHANNEL_1) {
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_0; // 假设 ADC_CHANNEL_1 对应 ADC1 通道 0
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES_5; // 可根据需要调整采样时间
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
// ... 配置其他通道

HAL_ADC_Start(&hadc1); // 启动 ADC 转换
return true;
}

// 获取 ADC 转换结果
uint16_t HAL_ADC_GetValue(HAL_ADC_ChannelTypeDef channel) {
// ... 获取指定通道的 ADC 转换结果
// ... 示例代码 (仅为示意,需要根据具体硬件和库函数修改):
ADC_HandleTypeDef hadc1; // 假设使用 ADC1
HAL_ADC_PollForConversion(&hadc1, 10); // 等待转换完成 (超时时间 10ms)
if (HAL_ADC_GetState(&hadc1) == HAL_ADC_STATE_TIMEOUT) {
return 0; // 超时错误
}
if (HAL_ADC_GetError(&hadc1) != HAL_ADC_ERROR_NONE) {
return 0; // ADC 错误
}
return (uint16_t)HAL_ADC_GetValue(&hadc1); // 获取 ADC 值
}

// 停止 ADC 转换
bool HAL_ADC_StopConversion(HAL_ADC_ChannelTypeDef channel) {
// ... 停止 ADC 转换
// ... 示例代码 (仅为示意,需要根据具体硬件和库函数修改):
ADC_HandleTypeDef hadc1; // 假设使用 ADC1
HAL_ADC_Stop(&hadc1);
return true;
}

2. 驱动层 (Driver Layer) 示例 (adc_driver.h, adc_driver.c):

  • adc_driver.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef ADC_DRIVER_H
#define ADC_DRIVER_H

#include "stdint.h"
#include "stdbool.h"
#include "hal_adc.h" // 包含 HAL 层 ADC 头文件

// ADC 驱动初始化结构体
typedef struct {
HAL_ADC_ChannelTypeDef Channel; // ADC 通道 (使用 HAL 层定义的通道)
HAL_ADC_ResolutionTypeDef Resolution; // ADC 分辨率 (使用 HAL 层定义的分辨率)
// ... 其他驱动层配置参数 (例如采样频率,量程范围等)
} ADC_Driver_InitTypeDef;

// 初始化 ADC 驱动
bool ADC_Driver_Init(ADC_Driver_InitTypeDef *init);

// 获取 ADC 采样值 (驱动层接口,返回电压值,单位 mV)
uint16_t ADC_Driver_GetVoltage_mV(void);

// ... 其他 ADC 驱动接口 (例如设置采样频率,量程范围等)

#endif // ADC_DRIVER_H
  • adc_driver.c:
1
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 "adc_driver.h"
#include "hal_adc.h"

static ADC_Driver_InitTypeDef adc_config; // 保存 ADC 驱动配置

// 初始化 ADC 驱动
bool ADC_Driver_Init(ADC_Driver_InitTypeDef *init) {
if (init == NULL) {
return false; // 参数错误
}
adc_config = *init; // 保存配置

HAL_ADC_InitTypeDef hal_adc_init;
hal_adc_init.Channel = init->Channel;
hal_adc_init.Resolution = init->Resolution;
// ... 映射驱动层配置到 HAL 层配置

return HAL_ADC_Init(&hal_adc_init); // 调用 HAL 层 ADC 初始化函数
}

// 获取 ADC 采样值 (驱动层接口,返回电压值,单位 mV)
uint16_t ADC_Driver_GetVoltage_mV(void) {
HAL_ADC_StartConversion(adc_config.Channel); // 启动 ADC 转换
uint16_t adc_value = HAL_ADC_GetValue(adc_config.Channel); // 获取 ADC 原始值

// 将 ADC 原始值转换为电压值 (mV)
// 假设参考电压为 3.3V (3300mV),分辨率为 12 位 (4096)
uint32_t voltage_mV = (uint32_t)adc_value * 3300 / 4096;
return (uint16_t)voltage_mV; // 返回电压值 (mV)
}

// ... 其他 ADC 驱动接口实现

3. 应用层 (Application Layer) - 数据采集模块 (data_acquisition.h, data_acquisition.c):

  • data_acquisition.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef DATA_ACQUISITION_H
#define DATA_ACQUISITION_H

#include "stdint.h"
#include "stdbool.h"

#define ADC_BUFFER_SIZE 1024 // ADC 数据缓冲区大小

// 数据采集模块初始化
bool DataAcquisition_Init(void);

// 开始数据采集
bool DataAcquisition_Start(void);

// 停止数据采集
bool DataAcquisition_Stop(void);

// 获取 ADC 数据缓冲区
uint16_t* DataAcquisition_GetBuffer(void);

// 获取当前缓冲区数据量
uint16_t DataAcquisition_GetBufferSize(void);

#endif // DATA_ACQUISITION_H
  • data_acquisition.c:
1
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
#include "data_acquisition.h"
#include "adc_driver.h" // 包含 ADC 驱动层头文件
#include "circular_buffer.h" // 假设使用了环形缓冲区 (Utilities/circular_buffer.h)

static uint16_t adc_buffer[ADC_BUFFER_SIZE]; // ADC 数据缓冲区
static CircularBuffer_t adc_circular_buffer; // 环形缓冲区结构体

// 数据采集任务 (示例,使用简单轮询方式,实际应用中可以使用 DMA + 中断方式提高效率)
static void DataAcquisition_Task(void) {
uint16_t voltage_mV = ADC_Driver_GetVoltage_mV(); // 获取电压值 (mV)
CircularBuffer_Write(&adc_circular_buffer, &voltage_mV, sizeof(uint16_t)); // 写入环形缓冲区
}

// 数据采集模块初始化
bool DataAcquisition_Init(void) {
ADC_Driver_InitTypeDef adc_init;
adc_init.Channel = ADC_CHANNEL_1; // 选择 ADC 通道 1 (根据硬件连接修改)
adc_init.Resolution = ADC_RESOLUTION_12BIT; // 选择 12 位分辨率

if (!ADC_Driver_Init(&adc_init)) {
return false; // ADC 驱动初始化失败
}

CircularBuffer_Init(&adc_circular_buffer, adc_buffer, ADC_BUFFER_SIZE, sizeof(uint16_t)); // 初始化环形缓冲区

return true;
}

// 开始数据采集
bool DataAcquisition_Start(void) {
// ... 启动数据采集定时器或任务调度器 (如果使用定时器或任务调度)
// ... 这里使用简单轮询方式,直接在主循环中调用 DataAcquisition_Task()
return true;
}

// 停止数据采集
bool DataAcquisition_Stop(void) {
// ... 停止数据采集定时器或任务调度器
return true;
}

// 获取 ADC 数据缓冲区
uint16_t* DataAcquisition_GetBuffer(void) {
return adc_buffer; // 返回环形缓冲区首地址
}

// 获取当前缓冲区数据量
uint16_t DataAcquisition_GetBufferSize(void) {
return CircularBuffer_DataCount(&adc_circular_buffer); // 返回环形缓冲区数据量
}

// 应用层主循环中调用数据采集任务 (示例)
void App_MainLoop(void) {
while (1) {
DataAcquisition_Task(); // 轮询采集 ADC 数据
// ... 其他应用层任务 (例如信号处理,显示,UI 等)
// ... 可以添加适当的延时,降低 CPU 占用率
}
}

4. 应用层 (Application Layer) - 显示模块 (display.h, display.c):

  • display.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef DISPLAY_H
#define DISPLAY_H

#include "stdint.h"
#include "stdbool.h"

#define LCD_WIDTH 160 // LCD 宽度 (根据具体 LCD 屏幕参数修改)
#define LCD_HEIGHT 128 // LCD 高度 (根据具体 LCD 屏幕参数修改)

// 显示模块初始化
bool Display_Init(void);

// 清屏
void Display_ClearScreen(uint16_t color);

// 绘制波形
void Display_DrawWaveform(uint16_t *waveform_data, uint16_t data_length, uint16_t color);

// 显示文本
void Display_DrawText(uint16_t x, uint16_t y, const char *text, uint16_t color, uint16_t bgcolor);

// ... 其他显示接口 (例如绘制直线,矩形,圆形等)

#endif // DISPLAY_H
  • display.c:
1
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
#include "display.h"
#include "bsp_lcd.h" // 包含 BSP 层 LCD 驱动头文件

// 显示模块初始化
bool Display_Init(void) {
if (!BSP_LCD_Init()) {
return false; // LCD BSP 初始化失败
}
Display_ClearScreen(LCD_COLOR_BLACK); // 清屏为黑色
return true;
}

// 清屏
void Display_ClearScreen(uint16_t color) {
BSP_LCD_Clear(color); // 调用 BSP 层清屏函数
}

// 绘制波形
void Display_DrawWaveform(uint16_t *waveform_data, uint16_t data_length, uint16_t color) {
if (waveform_data == NULL || data_length == 0) {
return; // 参数错误
}

// 波形数据缩放和偏移 (根据 LCD 尺寸和波形幅度调整)
uint16_t y_scale = LCD_HEIGHT / 256; // 假设 ADC 值为 0-4095,电压范围 0-3.3V,归一化到 0-255 范围
uint16_t y_offset = LCD_HEIGHT / 2; // 波形垂直居中

// 绘制波形
for (uint16_t i = 0; i < data_length - 1 && i < LCD_WIDTH - 1; i++) {
uint16_t x1 = i;
uint16_t y1 = y_offset - waveform_data[i] / 16 * y_scale; // 假设 waveform_data 范围 0-4095,除以 16 缩小范围到 0-255
uint16_t x2 = i + 1;
uint16_t y2 = y_offset - waveform_data[i + 1] / 16 * y_scale;
BSP_LCD_DrawLine(x1, y1, x2, y2, color); // 调用 BSP 层画线函数
}
}

// 显示文本
void Display_DrawText(uint16_t x, uint16_t y, const char *text, uint16_t color, uint16_t bgcolor) {
BSP_LCD_SetTextColor(color);
BSP_LCD_SetBackColor(bgcolor);
BSP_LCD_DisplayStringAt(x, y, (uint8_t *)text, LEFT_MODE); // 调用 BSP 层显示字符串函数
}

// ... 其他显示接口实现

5. 应用层 (Application Layer) - 信号处理模块 (signal_processing.h, signal_processing.c):

  • signal_processing.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef SIGNAL_PROCESSING_H
#define SIGNAL_PROCESSING_H

#include "stdint.h"
#include "stdbool.h"

// 信号处理模块初始化
bool SignalProcessing_Init(void);

// 计算信号频率 (示例,简单过零点检测法)
float SignalProcessing_CalculateFrequency(uint16_t *waveform_data, uint16_t data_length, float sample_rate);

// 计算信号峰峰值
uint16_t SignalProcessing_CalculatePeakToPeakVoltage(uint16_t *waveform_data, uint16_t data_length);

// ... 其他信号处理接口 (例如滤波,FFT,均值,RMS值等)

#endif // SIGNAL_PROCESSING_H
  • signal_processing.c:
1
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
#include "signal_processing.h"
#include "math.h" // 需要包含 math.h 头文件

// 信号处理模块初始化
bool SignalProcessing_Init(void) {
// ... 初始化信号处理模块,例如配置滤波器参数,FFT 算法初始化等 (如果需要)
return true;
}

// 计算信号频率 (示例,简单过零点检测法)
float SignalProcessing_CalculateFrequency(uint16_t *waveform_data, uint16_t data_length, float sample_rate) {
if (waveform_data == NULL || data_length < 2 || sample_rate <= 0) {
return 0.0f; // 参数错误
}

uint16_t zero_crossings = 0;
for (uint16_t i = 1; i < data_length; i++) {
// 假设信号中心值为 2048 (12 位 ADC 的中间值)
if ((waveform_data[i - 1] < 2048 && waveform_data[i] >= 2048) || (waveform_data[i - 1] >= 2048 && waveform_data[i] < 2048)) {
zero_crossings++; // 检测过零点
}
}

if (zero_crossings < 2) {
return 0.0f; // 过零点太少,无法计算频率
}

// 频率 = 过零点次数 / (2 * 数据长度 / 采样率)
float frequency = (float)zero_crossings / 2.0f / ((float)data_length / sample_rate);
return frequency;
}

// 计算信号峰峰值
uint16_t SignalProcessing_CalculatePeakToPeakVoltage(uint16_t *waveform_data, uint16_t data_length) {
if (waveform_data == NULL || data_length == 0) {
return 0; // 参数错误
}

uint16_t min_value = 4095; // 假设 12 位 ADC 最大值为 4095
uint16_t max_value = 0;

for (uint16_t i = 0; i < data_length; i++) {
if (waveform_data[i] < min_value) {
min_value = waveform_data[i];
}
if (waveform_data[i] > max_value) {
max_value = waveform_data[i];
}
}

// 峰峰值 = 最大值 - 最小值 (原始 ADC 值)
return max_value - min_value;
}

// ... 其他信号处理接口实现

6. 应用层 (Application Layer) - 用户界面模块 (ui.h, ui.c):

  • ui.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef UI_H
#define UI_H

#include "stdint.h"
#include "stdbool.h"

// UI 模块初始化
bool UI_Init(void);

// UI 主循环 (处理用户输入,更新显示)
void UI_MainLoop(void);

// ... 其他 UI 接口 (例如显示菜单,设置参数,处理按键事件等)

#endif // UI_H
  • ui.c:
1
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
#include "ui.h"
#include "bsp_button.h" // 包含 BSP 层按键驱动头文件
#include "display.h" // 包含 显示模块头文件
#include "data_acquisition.h" // 包含 数据采集模块头文件
#include "signal_processing.h" // 包含 信号处理模块头文件
#include "stdio.h" // 包含 stdio.h 用于 sprintf

// UI 状态枚举 (示例)
typedef enum {
UI_STATE_WAVEFORM_DISPLAY, // 波形显示状态
UI_STATE_MENU, // 菜单状态
UI_STATE_SETTINGS // 设置状态
// ... 其他 UI 状态
} UI_StateTypeDef;

static UI_StateTypeDef current_ui_state = UI_STATE_WAVEFORM_DISPLAY; // 当前 UI 状态

// UI 模块初始化
bool UI_Init(void) {
if (!BSP_Button_Init()) {
return false; // 按键 BSP 初始化失败
}
if (!Display_Init()) {
return false; // 显示模块初始化失败
}
return true;
}

// UI 主循环 (处理用户输入,更新显示)
void UI_MainLoop(void) {
while (1) {
BSP_Button_Scan(); // 扫描按键

switch (current_ui_state) {
case UI_STATE_WAVEFORM_DISPLAY:
UI_WaveformDisplayState_Update(); // 更新波形显示状态
break;
case UI_STATE_MENU:
UI_MenuState_Update(); // 更新菜单状态
break;
case UI_STATE_SETTINGS:
UI_SettingsState_Update(); // 更新设置状态
break;
default:
break;
}

// ... 可以添加适当的延时,降低 CPU 占用率
}
}

// UI 波形显示状态更新 (示例)
void UI_WaveformDisplayState_Update(void) {
Display_ClearScreen(LCD_COLOR_BLACK); // 清屏

// 获取 ADC 数据缓冲区
uint16_t *waveform_data = DataAcquisition_GetBuffer();
uint16_t data_length = DataAcquisition_GetBufferSize();

if (waveform_data != NULL && data_length > 0) {
Display_DrawWaveform(waveform_data, data_length, LCD_COLOR_GREEN); // 绘制波形

// 计算频率和峰峰值 (示例)
float frequency = SignalProcessing_CalculateFrequency(waveform_data, data_length, 100000.0f); // 假设采样率为 100kHz
uint16_t peak_to_peak = SignalProcessing_CalculatePeakToPeakVoltage(waveform_data, data_length);

// 显示频率和峰峰值 (示例)
char text_buffer[50];
sprintf(text_buffer, "Freq: %.2f Hz", frequency);
Display_DrawText(10, 10, text_buffer, LCD_COLOR_WHITE, LCD_COLOR_BLACK);
sprintf(text_buffer, "Vpp: %d mV", peak_to_peak * 3300 / 4096); // 转换为 mV 显示
Display_DrawText(10, 30, text_buffer, LCD_COLOR_WHITE, LCD_COLOR_BLACK);
}

// ... 处理按键事件 (例如切换到菜单状态)
if (BSP_Button_GetState(BUTTON_KEY1) == BUTTON_PRESSED) {
current_ui_state = UI_STATE_MENU; // 切换到菜单状态
}
}

// UI 菜单状态更新 (示例)
void UI_MenuState_Update(void) {
Display_ClearScreen(LCD_COLOR_BLUE); // 清屏为蓝色
Display_DrawText(LCD_WIDTH / 2 - 30, LCD_HEIGHT / 2 - 10, "Menu", LCD_COLOR_WHITE, LCD_COLOR_BLUE);
// ... 显示菜单项,处理菜单选择,按键事件等
if (BSP_Button_GetState(BUTTON_KEY1) == BUTTON_PRESSED) {
current_ui_state = UI_STATE_WAVEFORM_DISPLAY; // 返回波形显示状态
}
}

// UI 设置状态更新 (示例)
void UI_SettingsState_Update(void) {
Display_ClearScreen(LCD_COLOR_YELLOW); // 清屏为黄色
Display_DrawText(LCD_WIDTH / 2 - 40, LCD_HEIGHT / 2 - 10, "Settings", LCD_COLOR_BLACK, LCD_COLOR_YELLOW);
// ... 显示设置项,处理设置修改,按键事件等
if (BSP_Button_GetState(BUTTON_KEY1) == BUTTON_PRESSED) {
current_ui_state = UI_STATE_WAVEFORM_DISPLAY; // 返回波形显示状态
}
}

// ... 其他 UI 接口实现

7. main.c (用户代码入口):

1
2
3
4
5
6
7
8
9
10
#include "main.h"
#include "system_init.h" // 包含系统初始化头文件
#include "app.h" // 包含应用层入口头文件

int main(void) {
System_Init(); // 系统初始化 (时钟,外设等)
App_Init(); // 应用层初始化 (模块初始化)
App_MainLoop(); // 进入应用层主循环
return 0;
}

8. system_init.c (系统层初始化):

1
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
#include "system_init.h"
#include "stm32fxxx_hal.h" // 假设使用 STM32 HAL 库

// 系统时钟配置 (示例,需要根据具体硬件和需求修改)
static void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 4;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 7;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK) {
Error_Handler();
}
}

// 系统初始化
void System_Init(void) {
HAL_Init(); // HAL 库初始化
SystemClock_Config(); // 系统时钟配置
// ... 其他系统级初始化 (例如中断配置,错误处理初始化等)
}

// 错误处理函数 (示例)
void Error_Handler(void) {
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1) {
}
/* USER CODE END Error_Handler_Debug */
}

9. app.c (应用层入口):

1
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
#include "app.h"
#include "data_acquisition.h"
#include "signal_processing.h"
#include "display.h"
#include "ui.h"

// 应用层初始化
bool App_Init(void) {
if (!DataAcquisition_Init()) {
return false; // 数据采集模块初始化失败
}
if (!SignalProcessing_Init()) {
return false; // 信号处理模块初始化失败
}
if (!Display_Init()) {
return false; // 显示模块初始化失败
}
if (!UI_Init()) {
return false; // UI 模块初始化失败
}
return true;
}

// 应用层主循环
void App_MainLoop(void) {
UI_MainLoop(); // 进入 UI 模块主循环,UI 模块负责调度其他应用层任务
}

项目中采用的技术和方法 (实践验证):

  • 分层架构与模块化设计: 提高代码可读性、可维护性、可扩展性,降低模块之间的耦合度,方便团队协作开发和后期功能升级。
  • 硬件抽象层 (HAL): 屏蔽硬件差异,提高代码的可移植性,方便更换不同的单片机平台。
  • 驱动层: 封装硬件操作,提供更高级、更易用的接口,简化应用层开发。
  • 环形缓冲区 (Circular Buffer): 高效管理 ADC 采集的数据,避免数据丢失,适用于实时数据流处理。
  • 定时器 (Timer) 触发 ADC 采样 (可选,更高效): 使用定时器定期触发 ADC 采样,实现精确的采样频率控制,提高采样精度和效率 (示例代码中使用的是轮询方式,实际项目中推荐使用定时器触发或 DMA + 中断方式)。
  • DMA (Direct Memory Access) 数据传输 (可选,更高效): 使用 DMA 将 ADC 采集的数据直接传输到内存缓冲区,无需 CPU 干预,进一步提高数据采集效率,降低 CPU 负载 (示例代码未使用 DMA,实际项目中可以考虑使用 DMA 方式)。
  • 中断 (Interrupt) 处理 (例如按键中断): 使用中断处理按键事件,提高系统实时响应性,避免轮询方式的 CPU 资源浪费。
  • 状态机 (State Machine) 设计 (UI 模块): 使用状态机管理 UI 的不同状态,例如波形显示状态、菜单状态、设置状态等,使 UI 逻辑清晰、易于维护。
  • 基本信号处理算法: 采用过零点检测法计算频率、峰峰值计算等基本信号处理算法,满足示波器的基本测量需求 (可以根据需求扩展更复杂的信号处理算法,例如 FFT 频谱分析,数字滤波器等)。
  • LCD 图形显示: 使用 LCD 图形库或底层驱动函数,实现波形绘制、文本显示、界面元素显示,提供直观的用户界面。
  • C 语言编程: 采用 C 语言作为开发语言,C 语言是嵌入式系统开发中最常用的语言,具有高效、灵活、可移植性强等优点。
  • 代码注释和文档: 代码中包含详细的注释,并编写项目文档,提高代码可读性和可维护性,方便学习和交流。
  • 版本控制 (例如 Git): 使用版本控制工具管理代码,方便代码版本管理、团队协作、错误回溯。
  • 调试工具和方法: 使用 JTAG/SWD 调试器进行硬件调试,使用串口打印、LCD 显示等方式进行软件调试,定位和解决问题。
  • 测试验证: 进行单元测试、集成测试、系统测试,验证代码的功能和性能,确保系统的可靠性和稳定性。

维护升级:

  • 模块化设计: 方便后续功能升级和扩展,例如添加新的测量功能、新的显示模式、新的通信接口等,只需在相应的模块中进行修改和添加,而无需修改整个系统。
  • 预留扩展接口: 硬件设计上预留扩展接口 (例如 SPI/I2C/UART 等),方便后续扩展外围模块,例如信号发生器、频谱分析仪等。
  • 固件升级机制 (OTA - Over-The-Air 可选): 设计固件在线升级机制,方便用户远程升级固件,修复 Bug,添加新功能。

总结:

本项目以分层架构和模块化设计为基础,采用经过实践验证的嵌入式系统开发技术和方法,旨在打造一款适合电子爱好者和学生学习的数字示波器基础项目。代码示例涵盖了 HAL 层、驱动层、应用层 (数据采集、信号处理、显示、UI) 的核心模块,并详细解释了代码结构和实现思路。通过学习和实践本项目,您可以掌握嵌入式系统开发的基本流程和关键技术,为后续更深入的学习和项目开发打下坚实的基础。

请注意:

  • 上述代码示例仅为框架和示意,实际项目中需要根据具体的单片机型号、硬件电路、LCD 屏幕、按键等进行详细的硬件配置和驱动代码编写。
  • 学习过程中遇到问题,建议参考单片机手册、HAL 库文档、LCD 驱动手册等技术资料,并进行实际的调试和验证。

希望这份详细的说明和代码示例能够帮助您成功构建您的数字示波器项目!

欢迎关注我的其它发布渠道