编程技术分享

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

0%

简介:基于STM32G0的高精度TCXO VFD时钟

好的,作为一名高级嵌入式软件开发工程师,我将为您详细解析这款基于STM32G0的高精度TCXO VFD时钟项目,并提供相应的C代码实现方案。
关注微信公众号,提前获取相关推文

项目概述:基于STM32G0的高精度TCXO VFD时钟

本项目旨在设计并实现一款高精度、可靠且美观的VFD(真空荧光显示器)时钟。核心器件选用意法半导体(STMicroelectronics)的STM32G0系列微控制器,搭配高精度TCXO(温补晶体振荡器)作为时钟源,确保时间的准确性。显示部分采用VFD屏幕,以其高亮度、高对比度和广视角的特点,提供清晰且舒适的视觉体验。此外,项目还集成了温湿度传感器,实时监测环境温湿度,并在VFD屏幕上同步显示,提升了产品的实用性和附加价值。

系统设计架构

为了构建一个可靠、高效、可扩展的嵌入式系统平台,我将采用分层架构的设计思想,结合模块化编程方法,并融入事件驱动状态机的设计模式。这种架构能够有效地将系统功能分解成独立的、可管理的模块,降低系统的复杂性,提高代码的可读性、可维护性和可复用性。

1. 硬件层 (Hardware Layer)

硬件层是系统的基础,主要负责硬件资源的初始化和驱动。本项目硬件层主要包括以下模块:

  • STM32G0 微控制器: 核心处理单元,负责系统逻辑控制、数据处理和外设驱动。
  • TCXO 时钟源: 提供高精度的时间基准,保证时钟的准确性。
  • VFD 显示屏: 负责时间、温湿度等信息的显示输出。
  • 温湿度传感器: 采集环境温度和湿度数据。
  • 按键输入: 用户交互接口,用于设置时间、切换显示模式等。
  • 电源管理: 为系统提供稳定的电源供应。
  • 调试接口 (例如 UART): 用于程序调试和系统信息输出。

2. 硬件抽象层 (HAL - Hardware Abstraction Layer)

HAL层位于硬件层之上,是对底层硬件操作的封装,为上层软件提供统一的、与硬件无关的接口。HAL层的设计目标是屏蔽硬件差异,提高代码的可移植性。对于STM32G0,我们可以利用ST官方提供的HAL库,或者根据项目需求,自行编写更精简的HAL层。本项目HAL层主要包括以下模块:

  • GPIO 驱动: 控制GPIO端口的输入输出,用于按键检测、VFD控制信号输出、传感器控制等。
  • 定时器驱动: 配置和管理定时器,用于时间基准、PWM输出 (VFD亮度调节)、周期性任务触发等。
  • SPI/I2C 驱动: 如果VFD或传感器采用SPI或I2C接口,则需要相应的驱动。
  • UART 驱动: 用于串口通信,例如调试信息输出。
  • RTC 驱动: 如果使用STM32G0的内置RTC,需要RTC驱动进行时间管理。
  • ADC 驱动: 如果温湿度传感器是模拟输出,则需要ADC驱动。

3. 驱动层 (Driver Layer)

驱动层构建在HAL层之上,负责特定外围设备的驱动和管理。驱动层的功能更加具体,直接服务于应用层。本项目驱动层主要包括:

  • VFD 驱动: 控制VFD显示屏的显示内容,包括字符、数字、图标等。需要处理VFD的驱动时序、字库管理、显示缓存等。
  • 传感器驱动: 读取温湿度传感器的数据,进行数据解析和转换。
  • 按键驱动: 检测按键状态,处理按键事件,例如单击、双击、长按等。
  • 时间管理驱动: 基于TCXO和定时器,实现精确的时间管理,包括时间同步、时间校准、时间格式化等。

4. 服务层 (Service Layer)

服务层位于驱动层之上,提供更高层次的系统服务,为应用层提供功能模块。服务层是对驱动层功能的进一步封装和抽象,使得应用层可以更方便地调用系统功能。本项目服务层主要包括:

  • 显示服务: 管理VFD的显示内容,提供统一的显示接口,例如显示时间、显示温湿度、显示文本信息等。
  • 时间服务: 提供时间获取、时间设置、时间格式化等服务,供应用层调用。
  • 传感器服务: 提供温湿度数据获取服务,并进行数据滤波和校准。
  • 用户输入服务: 处理按键输入事件,并将其转换为应用层可理解的指令。
  • 配置服务: 管理系统配置参数,例如时间格式、显示模式、亮度设置等,可以存储在Flash或EEPROM中。

5. 应用层 (Application Layer)

应用层是系统的最高层,负责实现产品的具体功能和用户交互逻辑。应用层直接调用服务层提供的接口,完成系统的业务逻辑。本项目应用层主要包括:

  • 时钟应用: 核心应用模块,负责时间显示、时间更新、时间设置、闹钟功能 (可选) 等。
  • 温湿度显示应用: 负责温湿度数据的采集、处理和显示。
  • 用户界面管理: 处理用户交互逻辑,例如按键操作响应、菜单显示、参数设置等。

代码设计细节

  • 模块化设计: 每个层次和模块都独立设计和实现,模块之间通过清晰定义的接口进行通信,降低模块间的耦合度。
  • 事件驱动: 系统采用事件驱动的架构,例如定时器事件、按键事件、传感器数据就绪事件等,系统在空闲时进入低功耗模式,只有在事件发生时才被唤醒,提高系统的响应速度和效率。
  • 状态机: 对于复杂的系统流程和用户交互,例如时间设置、菜单切换等,采用状态机进行管理,清晰地定义系统的状态和状态转换逻辑。
  • 错误处理: 在代码中加入必要的错误处理机制,例如输入参数校验、硬件错误检测、异常处理等,提高系统的健壮性。
  • 代码注释: 编写清晰、详细的代码注释,提高代码的可读性和可维护性。
  • 版本控制: 使用版本控制系统 (例如 Git) 管理代码,方便代码的版本管理、协作开发和bug追踪。

C 代码实现 (部分关键模块示例)

由于篇幅限制,以下代码示例仅展示部分关键模块的核心代码,完整代码将超过3000行。以下代码基于STM32G0 HAL库,并假设VFD采用SPI接口,温湿度传感器采用I2C接口 (例如 DHT22 或 SHT30)。

1. main.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
#include "main.h"
#include "hal_init.h"
#include "vfd_driver.h"
#include "sensor_driver.h"
#include "time_service.h"
#include "display_service.h"
#include "input_service.h"
#include "config_service.h"

RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
float temperature, humidity;

void SystemClock_Config(void); // 系统时钟配置 (HAL库生成)
static void MX_GPIO_Init(void); // GPIO 初始化 (HAL库生成)
static void MX_SPI1_Init(void); // SPI1 初始化 (HAL库生成,用于VFD)
static void MX_I2C1_Init(void); // I2C1 初始化 (HAL库生成,用于传感器)
static void MX_TIM1_Init(void); // TIM1 初始化 (HAL库生成,例如 1Hz 定时器)
static void MX_RTC_Init(void); // RTC 初始化 (HAL库生成)
static void Error_Handler(void); // 错误处理函数 (HAL库生成)

int main(void)
{
HAL_Init(); // HAL库初始化
SystemClock_Config(); // 系统时钟配置
MX_GPIO_Init(); // GPIO 初始化
MX_SPI1_Init(); // SPI1 初始化
MX_I2C1_Init(); // I2C1 初始化
MX_TIM1_Init(); // TIM1 初始化
MX_RTC_Init(); // RTC 初始化

vfd_init(); // VFD 驱动初始化
sensor_init(); // 传感器驱动初始化
time_service_init(); // 时间服务初始化
display_service_init(); // 显示服务初始化
input_service_init(); // 输入服务初始化
config_service_init(); // 配置服务初始化

// 加载配置 (例如从Flash中)
config_load();

// 初始化显示内容
display_clear();
display_set_brightness(config_get_brightness()); // 从配置加载亮度

// 启动定时器,产生 1Hz 中断用于时间更新和显示刷新
HAL_TIM_Base_Start_IT(&htim1);

while (1)
{
// 处理按键输入事件
input_service_process();

// 应用层逻辑,例如根据当前状态更新显示内容
app_main_loop(); // 应用层主循环函数 (在 app 文件夹下实现)

// 可添加低功耗模式,例如 HAL_PWR_EnterSLEEPMode();
}
}

// ... (HAL库生成的初始化函数和错误处理函数) ...

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM1) // 1Hz 定时器中断
{
// 1. 更新时间 (从RTC或软件计时)
time_service_update_time();

// 2. 读取温湿度传感器数据 (周期性读取,例如每秒一次)
sensor_read_data(&temperature, &humidity);

// 3. 更新显示内容
display_service_update_display(time_service_get_time_string(), temperature, humidity);
}
}

2. hal/hal_gpio.hhal/hal_gpio.c (HAL GPIO 驱动)

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
// hal/hal_gpio.h
#ifndef HAL_GPIO_H
#define HAL_GPIO_H

#include "stm32g0xx_hal.h"

// 定义 GPIO 引脚
#define VFD_CS_PIN GPIO_PIN_0
#define VFD_CS_GPIO_Port GPIOA

#define BTN_SET_PIN GPIO_PIN_1
#define BTN_SET_GPIO_Port GPIOB

// 初始化 GPIO
void hal_gpio_init(void);

// 设置 GPIO 输出高电平
void hal_gpio_set_high(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

// 设置 GPIO 输出低电平
void hal_gpio_set_low(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

// 读取 GPIO 输入电平
GPIO_PinState hal_gpio_read(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

#endif // HAL_GPIO_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
42
// hal/hal_gpio.c
#include "hal_gpio.h"

void hal_gpio_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};

// 使能 GPIO 时钟 (根据实际使用的 GPIO 端口使能)
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();

/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(VFD_CS_GPIO_Port, VFD_CS_PIN, GPIO_PIN_RESET); // 默认 CS 为高电平 (或根据VFD驱动芯片的要求设置)

/*Configure GPIO pins : VFD_CS_PIN */
GPIO_InitStruct.Pin = VFD_CS_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(VFD_CS_GPIO_Port, &GPIO_InitStruct);

/*Configure GPIO pins : BTN_SET_PIN */
GPIO_InitStruct.Pin = BTN_SET_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉输入,默认按键未按下时为高电平
HAL_GPIO_Init(BTN_SET_GPIO_Port, &GPIO_InitStruct);
}

void hal_gpio_set_high(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
HAL_GPIO_WritePin(GPIOx, GPIO_Pin, GPIO_PIN_SET);
}

void hal_gpio_set_low(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
HAL_GPIO_WritePin(GPIOx, GPIO_Pin, GPIO_PIN_RESET);
}

GPIO_PinState hal_gpio_read(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
return HAL_GPIO_ReadPin(GPIOx, GPIO_Pin);
}

3. bsp/vfd_driver.hbsp/vfd_driver.c (VFD 驱动)

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
// bsp/vfd_driver.h
#ifndef VFD_DRIVER_H
#define VFD_DRIVER_H

#include "stm32g0xx_hal.h"

// VFD 初始化
void vfd_init(void);

// 清空 VFD 显示
void vfd_clear(void);

// 显示字符
void vfd_display_char(uint8_t x, uint8_t y, char ch);

// 显示字符串
void vfd_display_string(uint8_t x, uint8_t y, const char *str);

// 显示数字 (整数)
void vfd_display_number(uint8_t x, uint8_t y, int32_t num);

// 显示浮点数 (需要根据VFD字库和显示方式进行调整)
void vfd_display_float(uint8_t x, uint8_t y, float num, uint8_t decimal_places);

// 设置 VFD 亮度 (如果VFD驱动芯片支持亮度调节)
void vfd_set_brightness(uint8_t brightness); // brightness 范围例如 0-100

#endif // VFD_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
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
// bsp/vfd_driver.c
#include "vfd_driver.h"
#include "hal_gpio.h"
#include "main.h" // 包含 SPI 初始化句柄等

// 假设 VFD 字库存储在 vfd_font.h 文件中
#include "vfd_font.h" // 需要自行生成或获取 VFD 字库

// SPI 发送字节函数 (基于 HAL SPI 库)
static void vfd_spi_send_byte(uint8_t data);

void vfd_init(void)
{
// VFD 初始化序列 (参考 VFD 驱动芯片的数据手册)
// 例如:设置显示模式、亮度、清屏等
vfd_clear(); // 清屏初始化
vfd_set_brightness(50); // 设置默认亮度
}

void vfd_clear(void)
{
// 清空显示缓存或直接发送清屏指令到 VFD 驱动芯片
// ... (根据 VFD 驱动芯片的指令集实现) ...
// 示例:假设发送清屏指令 0x01
vfd_spi_send_byte(0x01);
}

void vfd_display_char(uint8_t x, uint8_t y, char ch)
{
// 根据字符 ch 在字库中查找对应的字模数据
// ... (字库查找逻辑) ...
const uint8_t *font_data = get_font_data(ch); // 假设 get_font_data 函数返回字模数据指针

if (font_data != NULL)
{
// 将字模数据发送到 VFD 驱动芯片,控制指定位置 (x, y) 的显示
// ... (VFD 数据发送逻辑,根据 VFD 驱动芯片的协议实现) ...
// 示例:假设字模数据按列扫描,每列一个字节
for (uint8_t i = 0; i < FONT_WIDTH; i++) // FONT_WIDTH 是字模宽度,例如 8
{
// 设置显示位置 (根据 VFD 驱动芯片的指令集)
// ... (设置位置指令) ...

vfd_spi_send_byte(font_data[i]); // 发送字模数据
}
}
}

void vfd_display_string(uint8_t x, uint8_t y, const char *str)
{
uint8_t current_x = x;
while (*str != '\0')
{
vfd_display_char(current_x, y, *str);
current_x += FONT_WIDTH; // 移动到下一个字符的位置
str++;
}
}

void vfd_display_number(uint8_t x, uint8_t y, int32_t num)
{
char num_str[12]; // 足够存储 int32_t 的字符串
sprintf(num_str, "%ld", num); // 将数字转换为字符串
vfd_display_string(x, y, num_str);
}

void vfd_display_float(uint8_t x, uint8_t y, float num, uint8_t decimal_places)
{
char float_str[16]; // 足够存储浮点数的字符串
sprintf(float_str, "%.*f", decimal_places, num); // 格式化浮点数
vfd_display_string(x, y, float_str);
}

void vfd_set_brightness(uint8_t brightness)
{
// 设置 VFD 亮度 (具体指令参考 VFD 驱动芯片数据手册)
// ... (亮度设置指令) ...
// 示例:假设亮度设置指令为 0x02,亮度值范围 0-100
vfd_spi_send_byte(0x02);
vfd_spi_send_byte(brightness);
}

static void vfd_spi_send_byte(uint8_t data)
{
hal_gpio_set_low(VFD_CS_GPIO_Port, VFD_CS_PIN); // 片选使能 (低电平有效,假设)
HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY); // 使用 HAL SPI 发送数据
hal_gpio_set_high(VFD_CS_GPIO_Port, VFD_CS_PIN); // 片选失能
}

4. bsp/sensor_driver.hbsp/sensor_driver.c (传感器驱动,以 DHT22 为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
// bsp/sensor_driver.h
#ifndef SENSOR_DRIVER_H
#define SENSOR_DRIVER_H

#include "stm32g0xx_hal.h"

// 初始化传感器
void sensor_init(void);

// 读取温湿度数据
uint8_t sensor_read_data(float *temperature, float *humidity);

#endif // SENSOR_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
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
// bsp/sensor_driver.c
#include "sensor_driver.h"
#include "hal_gpio.h"
#include "main.h" // 包含 I2C 初始化句柄等
#include "delay.h" // 需要 delay 函数,可以使用 HAL_Delay 或自定义的 delay 函数

#define DHT22_DATA_PIN GPIO_PIN_2
#define DHT22_DATA_GPIO_Port GPIOC

#define DHT22_TIMEOUT 1000 // DHT22 通信超时时间 (ms)

void sensor_init(void)
{
// DHT22 初始化不需要特殊操作,只需配置数据引脚
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOC_CLK_ENABLE();

GPIO_InitStruct.Pin = DHT22_DATA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 初始设置为输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(DHT22_DATA_GPIO_Port, &GPIO_InitStruct);
HAL_GPIO_WritePin(DHT22_DATA_GPIO_Port, DHT22_DATA_PIN, GPIO_PIN_SET); // 初始输出高电平
}

uint8_t sensor_read_data(float *temperature, float *humidity)
{
uint8_t data[5] = {0};
uint8_t checksum;
uint16_t raw_humidity, raw_temperature;

// 1. 发送起始信号
HAL_GPIO_WritePin(DHT22_DATA_GPIO_Port, DHT22_DATA_PIN, GPIO_PIN_RESET); // 拉低数据线
delay_ms(1); // 保持低电平至少 1ms
HAL_GPIO_WritePin(DHT22_DATA_GPIO_Port, DHT22_DATA_PIN, GPIO_PIN_SET); // 拉高数据线
delay_us(30); // 保持高电平 20-40us

// 2. 切换为输入模式,准备接收数据
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DHT22_DATA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉输入,等待 DHT22 拉低
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(DHT22_DATA_GPIO_Port, &GPIO_InitStruct);

// 3. 等待 DHT22 响应信号 (先低电平,后高电平)
uint32_t timeout = HAL_GetTick() + DHT22_TIMEOUT;
while (HAL_GPIO_ReadPin(DHT22_DATA_GPIO_Port, DHT22_DATA_PIN) == GPIO_PIN_SET) // 等待低电平
{
if (HAL_GetTick() > timeout) return 1; // 超时错误
}
while (HAL_GPIO_ReadPin(DHT22_DATA_GPIO_Port, DHT22_DATA_PIN) == GPIO_PIN_RESET) // 等待高电平
{
if (HAL_GetTick() > timeout) return 1; // 超时错误
}
while (HAL_GPIO_ReadPin(DHT22_DATA_GPIO_Port, DHT22_DATA_PIN) == GPIO_PIN_SET) // 等待低电平,数据传输开始
{
if (HAL_GetTick() > timeout) return 1; // 超时错误
}

// 4. 接收 40 位数据 (5 字节)
for (uint8_t i = 0; i < 5; i++)
{
for (uint8_t j = 0; j < 8; j++)
{
timeout = HAL_GetTick() + DHT22_TIMEOUT;
while (HAL_GPIO_ReadPin(DHT22_DATA_GPIO_Port, DHT22_DATA_PIN) == GPIO_PIN_RESET) // 等待高电平开始
{
if (HAL_GetTick() > timeout) return 1; // 超时错误
}
delay_us(30); // 30us 后读取电平,判断是 0 还是 1 (DHT22 数据位宽编码)
if (HAL_GPIO_ReadPin(DHT22_DATA_GPIO_Port, DHT22_DATA_PIN) == GPIO_PIN_SET) // 高电平超过 30us 为 1
{
data[i] |= (1 << (7 - j));
}
timeout = HAL_GetTick() + DHT22_TIMEOUT;
while (HAL_GPIO_ReadPin(DHT22_DATA_GPIO_Port, DHT22_DATA_PIN) == GPIO_PIN_SET) // 等待低电平结束一个 bit 传输
{
if (HAL_GetTick() > timeout) return 1; // 超时错误
}
}
}

// 5. 切换回输出模式,恢复默认状态
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(DHT22_DATA_GPIO_Port, &GPIO_InitStruct);
HAL_GPIO_WritePin(DHT22_DATA_GPIO_Port, DHT22_DATA_PIN, GPIO_PIN_SET); // 输出高电平

// 6. 校验校验和
checksum = data[0] + data[1] + data[2] + data[3];
if (checksum != data[4]) return 2; // 校验和错误

// 7. 数据转换
raw_humidity = (data[0] << 8) | data[1];
raw_temperature = (data[2] << 8) | data[3];

*humidity = (float)raw_humidity / 10.0f;
*temperature = (float)raw_temperature / 10.0f;

return 0; // 成功
}

5. services/time_service.hservices/time_service.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
// services/time_service.h
#ifndef TIME_SERVICE_H
#define TIME_SERVICE_H

#include "stm32g0xx_hal.h"

// 初始化时间服务
void time_service_init(void);

// 更新时间 (基于 RTC 或软件计时)
void time_service_update_time(void);

// 获取当前时间字符串 (HH:MM:SS)
char* time_service_get_time_string(void);

// 设置时间 (例如从按键输入)
void time_service_set_time(uint8_t hour, uint8_t minute, uint8_t second);

// 获取当前时间 (RTC_TimeTypeDef 结构体)
RTC_TimeTypeDef time_service_get_current_time(void);

// 获取当前日期 (RTC_DateTypeDef 结构体)
RTC_DateTypeDef time_service_get_current_date(void);

#endif // TIME_SERVICE_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
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
// services/time_service.c
#include "time_service.h"
#include "main.h" // 包含 RTC 初始化句柄等
#include <stdio.h>
#include <string.h>

RTC_TimeTypeDef currentTime;
RTC_DateTypeDef currentDate;
char timeString[9]; // "HH:MM:SS\0"

void time_service_init(void)
{
// 从 RTC 读取初始时间 (如果使用 RTC)
HAL_RTC_GetTime(&hrtc, &currentTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &currentDate, RTC_FORMAT_BIN);

// 或者初始化为默认时间
// currentTime.Hours = 0;
// currentTime.Minutes = 0;
// currentTime.Seconds = 0;
// HAL_RTC_SetTime(&hrtc, &currentTime, RTC_FORMAT_BIN);
// currentDate.Year = 23; // 2023年
// currentDate.Month = RTC_MONTH_JANUARY;
// currentDate.Date = 1;
// HAL_RTC_SetDate(&hrtc, &currentDate, RTC_FORMAT_BIN);

time_service_update_time_string(); // 初始化时间字符串
}

void time_service_update_time(void)
{
// 从 RTC 读取时间
HAL_RTC_GetTime(&hrtc, &currentTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &currentDate, RTC_FORMAT_BIN);

time_service_update_time_string(); // 更新时间字符串
}

char* time_service_get_time_string(void)
{
return timeString;
}

void time_service_set_time(uint8_t hour, uint8_t minute, uint8_t second)
{
currentTime.Hours = hour;
currentTime.Minutes = minute;
currentTime.Seconds = second;
HAL_RTC_SetTime(&hrtc, &currentTime, RTC_FORMAT_BIN);
time_service_update_time_string(); // 更新时间字符串
}

RTC_TimeTypeDef time_service_get_current_time(void)
{
return currentTime;
}

RTC_DateTypeDef time_service_get_current_date(void)
{
return currentDate;
}


// 内部函数,更新时间字符串
static void time_service_update_time_string(void)
{
sprintf(timeString, "%02d:%02d:%02d", currentTime.Hours, currentTime.Minutes, currentTime.Seconds);
}

6. services/display_service.hservices/display_service.c (显示服务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// services/display_service.h
#ifndef DISPLAY_SERVICE_H
#define DISPLAY_SERVICE_H

#include "stm32g0xx_hal.h"

// 初始化显示服务
void display_service_init(void);

// 清空显示
void display_clear(void);

// 更新显示内容 (例如在定时器中断中调用)
void display_service_update_display(const char *time_str, float temperature, float humidity);

// 设置亮度
void display_set_brightness(uint8_t brightness);

#endif // DISPLAY_SERVICE_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
// services/display_service.c
#include "display_service.h"
#include "vfd_driver.h"

void display_service_init(void)
{
// 初始化显示服务 (如果需要)
}

void display_clear(void)
{
vfd_clear();
}

void display_service_update_display(const char *time_str, float temperature, float humidity)
{
display_clear(); // 先清空显示

// 显示时间 (假设显示在第一行)
vfd_display_string(0, 0, time_str);

// 显示温度 (假设显示在第二行,左侧)
char temp_str[16];
sprintf(temp_str, "%.1fC", temperature); // 显示一位小数的摄氏度温度
vfd_display_string(0, 1, temp_str);

// 显示湿度 (假设显示在第二行,右侧)
char hum_str[16];
sprintf(hum_str, "%.1f%%RH", humidity); // 显示一位小数的相对湿度
vfd_display_string(80, 1, hum_str); // 假设屏幕宽度足够,湿度信息显示在右侧
}

void display_set_brightness(uint8_t brightness)
{
vfd_set_brightness(brightness);
}

7. services/input_service.hservices/input_service.c (输入服务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// services/input_service.h
#ifndef INPUT_SERVICE_H
#define INPUT_SERVICE_H

#include "stm32g0xx_hal.h"

// 初始化输入服务
void input_service_init(void);

// 处理输入事件 (例如在主循环中周期性调用)
void input_service_process(void);

// 获取按键状态 (例如 SET 按钮)
uint8_t input_service_get_button_set_state(void);

#endif // INPUT_SERVICE_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
42
43
44
// services/input_service.c
#include "input_service.h"
#include "hal_gpio.h"
#include "delay.h" // 需要 delay 函数,用于按键消抖

#define BTN_DEBOUNCE_DELAY 50 // 按键消抖延时 (ms)

uint8_t button_set_state = 0; // 0: 未按下, 1: 按下

void input_service_init(void)
{
// 初始化输入服务 (例如按键引脚初始化已在 hal_gpio_init 中完成)
}

void input_service_process(void)
{
// 检测 SET 按钮
static GPIO_PinState last_btn_set_state = GPIO_PIN_SET; // 上次按键状态
GPIO_PinState current_btn_set_state = hal_gpio_read(BTN_SET_GPIO_Port, BTN_SET_PIN);

if (current_btn_set_state == GPIO_PIN_RESET && last_btn_set_state == GPIO_PIN_SET) // 检测到按键按下 (下降沿)
{
delay_ms(BTN_DEBOUNCE_DELAY); // 消抖延时
if (hal_gpio_read(BTN_SET_GPIO_Port, BTN_SET_PIN) == GPIO_PIN_RESET) // 再次确认按键按下
{
button_set_state = 1; // 设置按键按下标志
// 可以添加按键按下后的处理逻辑,例如进入设置模式
// ... (例如调用应用层的设置模式处理函数) ...
}
}
else if (current_btn_set_state == GPIO_PIN_SET && last_btn_set_state == GPIO_PIN_RESET) // 检测到按键释放 (上升沿)
{
button_set_state = 0; // 清除按键按下标志
// 可以添加按键释放后的处理逻辑
// ...
}

last_btn_set_state = current_btn_set_state; // 更新上次按键状态
}

uint8_t input_service_get_button_set_state(void)
{
return button_set_state;
}

8. config_service.hconfig_service.c (配置服务, 简易示例,实际应用可能需要更完善的Flash存储和管理)

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

#include "stm32g0xx_hal.h"

// 初始化配置服务
void config_service_init(void);

// 加载配置 (从 Flash 或 EEPROM)
void config_load(void);

// 保存配置 (到 Flash 或 EEPROM)
void config_save(void);

// 获取亮度设置
uint8_t config_get_brightness(void);

// 设置亮度设置
void config_set_brightness(uint8_t brightness);

// ... 可以添加其他配置项的获取和设置函数 ...

#endif // CONFIG_SERVICE_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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// config_service.c
#include "config_service.h"

#define DEFAULT_BRIGHTNESS 80 // 默认亮度值

uint8_t current_brightness = DEFAULT_BRIGHTNESS; // 当前亮度值 (存储在 RAM 中,实际应用需要 Flash 存储)

void config_service_init(void)
{
// 初始化配置服务 (例如 Flash 初始化,如果需要存储配置到 Flash)
// ...
}

void config_load(void)
{
// 从 Flash 或 EEPROM 加载配置
// ... (Flash 读取逻辑,读取亮度值等配置项) ...
// 示例:假设从 Flash 地址 0x08080000 读取亮度值
// uint8_t loaded_brightness = *(uint8_t*)0x08080000;
// if (loaded_brightness >= 0 && loaded_brightness <= 100) // 校验读取的值是否有效
// {
// current_brightness = loaded_brightness;
// }
// else
// {
// current_brightness = DEFAULT_BRIGHTNESS; // 使用默认值
// }

// 简易示例,直接使用默认值
current_brightness = DEFAULT_BRIGHTNESS;
}

void config_save(void)
{
// 保存配置到 Flash 或 EEPROM
// ... (Flash 写入逻辑,写入亮度值等配置项) ...
// 示例:假设写入亮度值到 Flash 地址 0x08080000
// HAL_FLASH_Unlock(); // 解锁 Flash
// FLASH_PageErase(0x08080000); // 擦除页 (需要根据 STM32G0 Flash 页大小调整)
// HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, 0x08080000, current_brightness); // 写入字节
// HAL_FLASH_Lock(); // 锁定 Flash

// 简易示例,不保存
}

uint8_t config_get_brightness(void)
{
return current_brightness;
}

void config_set_brightness(uint8_t brightness)
{
if (brightness >= 0 && brightness <= 100) // 亮度值范围校验
{
current_brightness = brightness;
config_save(); // 保存配置
display_set_brightness(current_brightness); // 立即更新显示亮度
}
}

9. app/clock_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
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
// app/clock_app.c
#include "clock_app.h"
#include "input_service.h"
#include "time_service.h"
#include "display_service.h"
#include "config_service.h"
#include <stdio.h>

// 系统状态枚举
typedef enum {
STATE_DISPLAY_TIME,
STATE_SET_TIME,
STATE_SET_BRIGHTNESS,
// ... 可以添加更多状态 ...
} AppState_t;

AppState_t current_state = STATE_DISPLAY_TIME; // 初始状态为显示时间

void app_main_loop(void)
{
switch (current_state)
{
case STATE_DISPLAY_TIME:
app_display_time_state_handler();
break;
case STATE_SET_TIME:
app_set_time_state_handler();
break;
case STATE_SET_BRIGHTNESS:
app_set_brightness_state_handler();
break;
default:
current_state = STATE_DISPLAY_TIME; // 默认状态
break;
}
}

// 显示时间状态处理函数
void app_display_time_state_handler(void)
{
if (input_service_get_button_set_state()) // 检测到 SET 按钮按下,进入设置时间状态
{
current_state = STATE_SET_TIME;
display_clear();
vfd_display_string(0, 0, "SET TIME"); // 显示提示信息
delay_ms(500); // 简单延时,避免快速状态切换
}
// 其他显示时间状态下的逻辑 ...
}

// 设置时间状态处理函数 (简易示例,实际应用需要更完善的UI交互)
void app_set_time_state_handler(void)
{
static uint8_t setting_hour = 0;
static uint8_t setting_minute = 0;
static uint8_t setting_second = 0;
static uint8_t setting_step = 0; // 0: 设置小时, 1: 设置分钟, 2: 设置秒

if (input_service_get_button_set_state()) // SET 按钮作为确认和下一步按钮
{
setting_step++;
if (setting_step > 2) // 设置完成,返回显示时间状态
{
time_service_set_time(setting_hour, setting_minute, setting_second); // 应用设置的时间
current_state = STATE_DISPLAY_TIME;
display_clear();
}
else
{
display_clear();
char prompt_str[16];
if (setting_step == 1) sprintf(prompt_str, "SET MINUTE");
else if (setting_step == 2) sprintf(prompt_str, "SET SECOND");
vfd_display_string(0, 0, prompt_str);
delay_ms(500); // 简单延时
}
}

// 可以添加其他按键用于调整小时、分钟、秒的值 (例如 UP/DOWN 按钮,此处省略)
// ... (按键检测和数值调整逻辑) ...

// 显示当前设置的值
char time_str[9];
sprintf(time_str, "%02d:%02d:%02d", setting_hour, setting_minute, setting_second);
vfd_display_string(0, 1, time_str);
}

// 设置亮度状态处理函数 (简易示例)
void app_set_brightness_state_handler(void)
{
static uint8_t current_brightness_setting = 0; // 临时亮度设置值

if (current_brightness_setting == 0) // 第一次进入设置亮度状态
{
current_brightness_setting = config_get_brightness(); // 从配置加载当前亮度值
display_clear();
vfd_display_string(0, 0, "SET BRIGHT");
}

if (input_service_get_button_set_state()) // SET 按钮作为确认和下一步按钮
{
config_set_brightness(current_brightness_setting); // 应用设置的亮度
current_state = STATE_DISPLAY_TIME; // 返回显示时间状态
display_clear();
}

// 可以添加其他按键用于调整亮度值 (例如 UP/DOWN 按钮,此处省略)
// ... (按键检测和亮度值调整逻辑) ...

// 显示当前亮度值
char brightness_str[16];
sprintf(brightness_str, "BRIGHT: %d", current_brightness_setting);
vfd_display_string(0, 1, brightness_str);
}

测试验证与维护升级

测试验证:

  • 单元测试: 对各个模块进行独立测试,例如VFD驱动、传感器驱动、时间服务等,验证模块功能的正确性。
  • 集成测试: 将各个模块组合起来进行测试,验证模块之间的协同工作是否正常,接口是否正确。
  • 系统测试: 对整个系统进行全面测试,包括功能测试、性能测试、稳定性测试、可靠性测试等。
  • 长时间运行测试: 进行72小时或更长时间的连续运行测试,验证系统的长期稳定性。
  • 精度测试: 使用高精度时间校准仪器,验证时钟的精度是否符合设计要求 (TCXO的精度)。
  • 用户体验测试: 邀请用户进行试用,收集用户反馈,改进用户体验。

维护升级:

  • 模块化设计: 模块化设计使得系统更容易维护和升级,可以单独修改和替换某个模块,而不会影响其他模块。
  • 清晰的接口: 模块之间通过清晰定义的接口进行通信,方便模块的替换和升级。
  • 版本控制: 使用版本控制系统 (例如 Git) 管理代码,方便代码的版本管理和回滚。
  • OTA (Over-The-Air) 升级 (可选): 如果产品需要远程升级,可以考虑添加OTA升级功能,通过网络远程更新固件。
  • 预留升级接口: 在硬件设计上预留必要的升级接口,例如JTAG/SWD接口,方便固件升级和调试。
  • 日志记录和错误诊断: 添加日志记录功能,记录系统运行状态和错误信息,方便问题排查和故障诊断。

总结

以上代码示例和架构设计方案,旨在为您提供一个完整的基于STM32G0高精度TCXO VFD时钟项目的软件开发框架。实际项目开发中,还需要根据具体的硬件选型、VFD型号、传感器型号以及功能需求,进行详细的设计和代码实现。代码量超过3000行是一个庞大的工程,以上示例代码只是一个基础框架,需要根据具体需求扩展和完善各个模块的功能,并进行充分的测试和验证,才能最终构建出一个稳定可靠、高效可扩展的嵌入式系统平台。

希望这份详细的解析和代码示例能够帮助您理解嵌入式系统开发的流程和关键技术,并为您的项目开发提供有价值的参考。

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