项目概述
本项目为宠物腹背理疗仪配套驱动板,基于PY32F002B单片机开发,核心功能是为理疗仪提供稳定的供电管理、理疗灯(红光+850nm红外)控制、人机交互(按键+数码管显示)及低功耗控制,适配中小型犬、成年猫的腹背理疗需求,可搭配充电宝或锂电池供电,实现居家便捷理疗。
项目核心适配硬件:118数码管(带闪电+%符号,0.25寸)、双路呼吸灯(对应660nm红光+850nm红外理疗灯)、Type-C充电接口、3.7V锂电池、2个实体按键(开关键+设置键),软件层面实现供电切换、电量检测、模式控制、低功耗唤醒等全流程功能,代码遵循模块化设计,兼顾稳定性与可维护性。

项目核心适配硬件:118数码管(带闪电+%符号,0.25寸)、双路呼吸灯(对应660nm红光+850nm红外理疗灯)、Type-C充电接口、3.7V锂电池、2个实体按键(开关键+设置键),软件层面实现供电切换、电量检测、模式控制、低功耗唤醒等全流程功能,代码遵循模块化设计,兼顾稳定性与可维护性。
前面详细解说了这个项目的头文件,本问主要重点在于功能代码的分析。按软件功能模块拆分,详细说明各模块的代码逻辑、外设调用方式、核心函数实现及关键注意点,结合代码片段帮助理解,适配开发者调试、修改代码需求。
一、软件整体架构说明
本项目软件采用"主循环+中断驱动"的架构,核心逻辑如下:
-
系统初始化:main函数入口执行所有外设初始化(时钟、GPIO、TIM、ADC等),读取Flash保存的参数,完成系统启动准备。
-
中断驱动:TIM14定时器产生1ms中断,在中断服务函数中执行数码管刷新、系统计时、按键扫描触发等高频任务。
-
主循环:执行低频次任务,包括电池电压检测、充电状态检测、呼吸灯控制、低功耗判断、参数保存等,逻辑清晰、占用资源少。
核心设计优势:高频任务(如数码管刷新)放入中断(只置位标志,在主循环中处理,防止阻塞),确保响应速度;低频任务放入主循环,降低CPU占用率,兼顾功能稳定性与低功耗需求。
二、核心功能模块代码解析(按执行流程排序)
模块1:系统初始化模块(代码入口,外设初始化核心)
1.1 初始化流程(main函数开头)
代码入口为main()函数,首先执行系统初始化序列,顺序不可随意调整(需保证外设依赖的时钟先开启),具体初始化函数及功能如下:
// main函数中初始化序列
APP_SystemClockConfig(); // 配置系统时钟为24MHz
APP_GPIO_Init(); // 初始化所有GPIO(按键、数码管、呼吸灯、充电检测)
LL_GPIO_SetOutputPin(PWR_ADC_PORT, PWR_ADC_PIN);
APP_TIM1_Init(); // 初始化TIM1用于PWM呼吸灯输出
APP_TIM14_Init(); // 初始化TIM14用于1ms系统定时
APP_ADC_Init(); // 初始化ADC用于电池电压检测
APP_LoadFromFlash(); // 从Flash读取保存的参数(工作时间、模式)
1.2 核心初始化函数解析
1.2.1 系统时钟配置(APP_SystemClockConfig)
核心功能:配置PY32F002B的系统时钟为24MHz(HSI内部时钟),为所有外设提供稳定时钟源,代码实现及解析如下:
void APP_SystemClockConfig(void)
{
/* 开启HSI时钟 */
LL_RCC_HSI_Enable();
while(LL_RCC_HSI_IsReady() != 1); // 等待HSI时钟稳定
/* 配置AHB总线时钟(不分频,与HSI时钟一致) */
LL_RCC_SetAHBPrescaler(LL_RCC_AHB_DIV_1);
/* 配置APB1总线时钟(不分频) */
LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);
/* 配置系统时钟为HSI,24MHz */
LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_HSI);
while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_HSI);
/* 更新系统时钟频率变量 */
SystemCoreClockUpdate();
}
关键注意点:
-
HSI时钟是单片机内置高速时钟,无需外部晶振,适合低成本项目,本项目配置为最高24MHz,兼顾性能与功耗。
-
while循环用于等待时钟稳定,若缺少此步骤,后续外设配置可能失败(时钟未就绪)。
1.2.2 GPIO初始化(APP_GPIO_Init)
核心功能:初始化所有GPIO引脚,包括按键(输入)、数码管(输出)、呼吸灯(复用为PWM输出)、充电状态检测(输入)、ADC供电控制(输出),代码实现及解析如下(核心片段):
void APP_GPIO_Init(void)
{
/* 开启GPIOA、GPIOB、GPIOC时钟(使用外设前必须开启对应时钟) */
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA | LL_IOP_GRP1_PERIPH_GPIOB | LL_IOP_GRP1_PERIPH_GPIOC);
/* 1. 按键引脚初始化(PA0、PA1,输入模式,上拉) */
// 开关键 K1(PA0)
LL_GPIO_SetPinMode(KEY_PWR_PORT, KEY_PWR_PIN, LL_GPIO_MODE_INPUT);
LL_GPIO_SetPinPull(KEY_PWR_PORT, KEY_PWR_PIN, LL_GPIO_PULL_UP);
// 设置键 K2(PA1)
LL_GPIO_SetPinMode(KEY_SET_PORT, KEY_SET_PIN, LL_GPIO_MODE_INPUT);
LL_GPIO_SetPinPull(KEY_SET_PORT, KEY_SET_PIN, LL_GPIO_PULL_UP);
/* 2. 数码管引脚初始化(PB0~PB4,输出模式,推挽输出) */
LL_GPIO_SetPinMode(ALL_SEG_PORTS, ALL_SEG_PINS, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType(ALL_SEG_PORTS, ALL_SEG_PINS, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinSpeed(ALL_SEG_PORTS, ALL_SEG_PINS, LL_GPIO_SPEED_FREQ_LOW);
APP_AllSegOff(); // 初始化时关闭所有数码管段
/* 3. 呼吸灯PWM引脚初始化(PA3、PA4,复用为TIM1输出) */
LL_GPIO_SetPinMode(LED_PWM1_PORT, LED_PWM1_PIN, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetPinMode(LED_PWM2_PORT, LED_PWM2_PIN, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetPinAlternateConfig(LED_PWM1_PORT, LED_PWM1_PIN, LL_GPIO_AF_2); // PA4对应TIM1_CH3
LL_GPIO_SetPinAlternateConfig(LED_PWM2_PORT, LED_PWM2_PIN, LL_GPIO_AF_2); // PA3对应TIM1_CH2
/* 4. 充电状态检测引脚初始化(PC1、PB7,输入模式,无上下拉) */
LL_GPIO_SetPinMode(CHRG_PORT, CHRG_PIN, LL_GPIO_MODE_INPUT);
LL_GPIO_SetPinMode(FULL_PORT, FULL_PIN, LL_GPIO_MODE_INPUT);
/* 5. ADC供电控制引脚初始化(PB5,输出模式,推挽输出) */
LL_GPIO_SetPinMode(PWR_ADC_PORT, PWR_ADC_PIN, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType(PWR_ADC_PORT, PWR_ADC_PIN, LL_GPIO_PULL_UP);
}
关键注意点:
-
使用任何GPIO外设前,必须先开启对应GPIO端口的时钟(LL_IOP_GRP1_EnableClock),否则引脚配置无效。
-
呼吸灯引脚需配置为"复用模式"(LL_GPIO_MODE_ALTERNATE),并指定复用功能为TIM1(LL_GPIO_AF_2),否则无法输出PWM。
-
按键引脚配置为"上拉输入",可避免引脚悬空导致的误触发(未按下时引脚为高电平,按下时为低电平)。
1.2.3 TIM1初始化(APP_TIM1_Init,呼吸灯PWM控制)
核心功能:初始化TIM1定时器,输出双路PWM信号,用于控制两个呼吸灯(红光+红外)的亮度,代码实现及解析如下:
void APP_TIM1_Init(void)
{
/* 开启TIM1时钟 */
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_TIM1);
/* 配置TIM1基本参数:预分频、自动重装值(决定PWM频率) */
LL_TIM_SetPrescaler(TIM1, 23); // 预分频系数=23,时钟频率=24MHz/(23+1)=1MHz
LL_TIM_SetAutoReload(TIM1, 999); // 自动重装值=999,PWM频率=1MHz/(999+1)=1kHz
LL_TIM_EnableARRPreload(TIM1); // 开启自动重装预加载
/* 配置TIM1通道3(PA4,LED1)为PWM模式1 */
LL_TIM_OC_SetMode(TIM1, LED_PWM_CH1, LL_TIM_OCMODE_PWM1);
LL_TIM_OC_SetPolarity(TIM1, LED_PWM_CH1, LL_TIM_OCPOLARITY_HIGH);
LL_TIM_OC_EnablePreload(TIM1, LED_PWM_CH1); // 开启通道预加载
LL_TIM_OC_SetCompareCH3(TIM1, 0); // 初始占空比为0(呼吸灯熄灭)
/* 配置TIM1通道2(PA3,LED2)为PWM模式1 */
LL_TIM_OC_SetMode(TIM1, LED_PWM_CH2, LL_TIM_OCMODE_PWM1);
LL_TIM_OC_SetPolarity(TIM1, LED_PWM_CH2, LL_TIM_OCPOLARITY_HIGH);
LL_TIM_OC_EnablePreload(TIM1, LED_PWM_CH2);
LL_TIM_OC_SetCompareCH2(TIM1, 0); // 初始占空比为0
/* 开启TIM1主输出,启动定时器 */
LL_TIM_CC_EnableChannel(TIM1, LED_PWM_CH1 | LED_PWM_CH2);
LL_TIM_EnableCounter(TIM1);
LL_TIM_EnableAllOutputs(TIM1);
}
关键解析:
-
PWM频率计算:时钟频率(24MHz)÷(预分频系数+1)÷(自动重装值+1)= 24MHz÷24÷1000 = 1kHz,适合呼吸灯控制(无频闪)。
-
占空比控制:通过LL_TIM_OC_SetCompareCHx()函数设置比较值,比较值范围0~999,对应占空比0%~100%,比较值越大,呼吸灯越亮。
-
初始占空比设为0,确保系统启动时呼吸灯处于熄灭状态,符合用户操作逻辑。
1.2.4 TIM14初始化(APP_TIM14_Init,系统1ms定时)
核心功能:初始化TIM14定时器,产生1ms中断,用于系统计时(g_SysTick)、数码管刷新、按键扫描触发等高频任务,代码实现及解析如下
void APP_TIM14_Init(void)
{
/* 开启TIM14时钟 */
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_TIM14);
/* 配置TIM14基本参数:1ms中断 */
LL_TIM_SetPrescaler(TIM14, 23); // 预分频系数=23,时钟频率=24MHz/(23+1)=1MHz
LL_TIM_SetAutoReload(TIM14, 999); // 自动重装值=999,中断周期=1MHz/(999+1)=1ms
LL_TIM_EnableARRPreload(TIM14);
/* 开启TIM14更新中断(溢出中断) */
LL_TIM_EnableIT_UPDATE(TIM14);
/* 配置中断优先级(抢占优先级0,响应优先级0,最高优先级) */
NVIC_SetPriority(TIM14_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 0, 0));
NVIC_EnableIRQ(TIM14_IRQn); // 使能TIM14中断
/* 启动TIM14定时器 */
LL_TIM_EnableCounter(TIM14);
}
// TIM14中断服务函数(1ms执行一次)
void TIM14_IRQHandler(void)
{
if(LL_TIM_IsActiveFlag_UPDATE(TIM14) == 1) // 判断是否是更新中断
{
LL_TIM_ClearFlag_UPDATE(TIM14); // 清除中断标志
g_SysTick++; // 系统计时+1(每1ms+1,用于计算时间)
APP_Disp_Refresh(); // 1ms刷新一次数码管(避免闪烁)
// 每10ms扫描一次按键(降低CPU占用)
static uint8_t key_scan_cnt = 0;
if(++key_scan_cnt >= 10)
{
key_scan_cnt = 0;
APP_Key_Scan(); // 按键扫描
}
}
}
关键解析:
-
中断周期计算:与TIM1一致,1ms中断一次,确保高频任务的响应速度。
-
g_SysTick变量:系统毫秒计时器,所有时间相关逻辑(长按判定、自动休眠、倒计时)均依赖此变量。
-
按键扫描频率:每10ms扫描一次,而非1ms,可降低CPU占用率(避免频繁扫描按键)。
-
中断标志清除:必须在中断服务函数中清除中断标志(LL_TIM_ClearFlag_UPDATE),否则会持续触发中断。
1.2.5 ADC初始化(APP_ADC_Init,电池电压采样)
核心功能:初始化ADC模块,配置为单次采样模式,用于采集电池电压(PA7引脚),代码实现及解析如下:
void APP_ADC_Init(void)
{
/* 开启ADC时钟 */
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_ADC1);
/* 配置ADC基本参数:内部1.5V基准,单次采样,右对齐 */
LL_ADC_SetResolution(ADC1, LL_ADC_RESOLUTION_12B); // 12位精度
LL_ADC_SetDataAlignment(ADC1, LL_ADC_DATA_ALIGN_RIGHT); // 右对齐
LL_ADC_SetClock(ADC1, LL_ADC_CLOCK_SYNC_PCLK_DIV4); // ADC时钟=PCLK/4=6MHz
LL_ADC_SetSamplingTime(ADC1, LL_ADC_SAMPLINGTIME_239CYCLES_5); // 采样时间239.5个周期,提高采样精度
/* 配置ADC采样通道:PA7(通道4) */
LL_ADC_REG_SetSequencerLength(ADC1, LL_ADC_REG_SEQ_SCAN_DISABLE); // 关闭扫描模式(单次采样)
LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_1, VBAT_ADC_CH); // 通道4为第1优先级
/* 配置ADC基准电压:内部1.5V */
LL_ADC_SetCommonPathInternalCh(LL_ADC_COMMON_INSTANCE, LL_ADC_PATH_INTERNAL_VREFINT);
/* 开启ADC */
LL_ADC_Enable(ADC1);
// 等待ADC就绪
while(LL_ADC_IsActiveFlag_ADRDY(ADC1) == 0);
}
// 获取一次ADC采样值(带滤波)
uint16_t APP_ADC_GetValue(void)
{
uint32_t adc_sum = 0;
uint8_t i;
// 采样16次,取平均值(滤波,降低噪声)
for(i = 0; i < ADC_FILTER_WINDOW; i++)
{
LL_ADC_REG_StartConversion(ADC1); // 启动一次采样
while(LL_ADC_REG_IsConversionComplete(ADC1) == 0); // 等待采样完成
adc_sum += LL_ADC_REG_ReadConversionData12(ADC1); // 读取采样值(12位)
}
return (uint16_t)(adc_sum / ADC_FILTER_WINDOW); // 返回平均值
}
关键注意点:
-
采样精度:12位ADC,采样值范围0~4095,结合内部1.5V基准,可计算实际电压(电压=采样值×1.5V÷4095)。
-
滤波处理:采样16次取平均值(ADC_FILTER_WINDOW=16),减少采样噪声,使电池电压检测更稳定。
-
采样流程:启动采样→等待采样完成→读取数据,缺一不可,否则会读取到错误的采样值。
1.2.6 Flash参数读取(APP_LoadFromFlash)
核心功能:从Flash指定地址(FLASH_SAVE_ADDR)读取保存的用户参数(呼吸灯工作时间、模式),实现掉电记忆功能,代码实现及解析如下:
void APP_LoadFromFlash(void)
{
SaveData_t *p_save_data = (SaveData_t *)FLASH_SAVE_ADDR; // 指向Flash存储地址
// 校验数据(crc校验,防止数据出错)
if(p_save_data->crc == (p_save_data->work_time + p_save_data->led_mode))
{
// 数据有效,读取参数
g_WorkTime = p_save_data->work_time;
g_LedMode = p_save_data->led_mode;
}
else
{
// 数据无效(首次启动或数据损坏),设置默认参数
g_WorkTime = 15; // 默认定时15分钟
g_LedMode = LED_MODE_50_PERCENT; // 默认50%亮度模式
APP_SaveToFlash(); // 保存默认参数到Flash
}
}
模块2:人机交互模块(按键+数码管)
核心功能:处理按键操作(开关键、设置键),控制数码管显示(电量、模式、定时时间),是用户与设备交互的核心,分为按键扫描和数码管显示两个子模块。
2.1 按键扫描模块(APP_Key_Scan)
核心功能:扫描两个实体按键(开关键K1、设置键K2),识别短按、长按操作,触发对应功能(呼吸灯开关、模式切换、定时调整、休眠/唤醒),代码实现基于按键状态机(Key_Handle_t结构体),核心代码及解析如下:
// 全局变量(按键状态机)
Key_Handle_t g_KeyPwr = {0}; // 开关键(K1)
Key_Handle_t s_keySet = {0}; // 设置键(K2)
// 按键状态机处理函数(通用,可复用)
static void Key_Process(Key_Handle_t *key, uint8_t pin_state)
{
switch(key->state)
{
case 0: // 空闲状态
if(pin_state == 0) // 按键按下(低电平,因按键配置为上拉输入)
{
key->state = 1; // 切换到按下状态
key->press_tick = g_SysTick; // 记录按键按下的时间点(关联系统1ms计时)
key->click_count++; // 点击次数+1(用于双击判定,本项目暂未启用)
}
break;
case 1: // 按下状态
if(pin_state == 1) // 按键松开(高电平)
{
key->state = 2; // 切换到等待双击状态
key->release_tick = g_SysTick; // 记录按键松开的时间点
// 判断是否为短按(按下时间 < 长按判定时间)
if(g_SysTick - key->press_tick < KEY_LONG_PRESS_MS)
{
// 短按处理:根据按键类型(开关键/设置键)执行不同功能
if(key == &g_KeyPwr)
{
// 开关键短按:切换呼吸灯开关状态
g_LedIsOn = !g_LedIsOn;
if(!g_LedIsOn)
{
// 关闭呼吸灯:将两路PWM占空比设为0
LL_TIM_OC_SetCompareCH2(LED_PWM_TIM, 0); // LED2(PA3)占空比0
LL_TIM_OC_SetCompareCH3(LED_PWM_TIM, 0); // LED1(PA4)占空比0
}
else
{
// 开启呼吸灯:根据当前选中的模式设置对应占空比
APP_LedControl();
}
}
else if(key == &s_keySet)
{
// 设置键短按:切换数码管显示模式(循环切换4种模式)
g_DispMode = (g_DispMode + 1) % 4;
APP_Disp_Update(); // 立即更新数码管显示内容,同步显示模式切换
}
}
}
else // 按键仍处于按下状态
{
// 判断是否触发长按(按下时间 ≥ 长按判定时间,且未触发过长按)
if(g_SysTick - key->press_tick >= KEY_LONG_PRESS_MS && !key->long_press)
{
key->long_press = true; // 标记为已触发长按,避免重复触发
// 长按处理:仅开关键支持长按,实现休眠/唤醒切换
if(key == &g_KeyPwr)
{
if(g_SystemState == SYS_STATE_ACTIVE)
{
// 当前为正常工作状态,触发进入低功耗休眠
APP_EnterSleep();
}
else
{
// 当前为休眠状态,触发退出休眠,恢复正常工作
APP_ExitSleep();
}
}
}
}
break;
case 2: // 等待双击状态(本项目暂未启用双击功能,仅用于重置状态)
// 超过双击判定时间(300ms)未再次按下,判定为单次点击,重置按键状态
if(g_SysTick - key->release_tick >= KEY_DOUBLE_CLICK_MS)
{
key->state = 0; // 重置为空闲状态
key->click_count = 0; // 重置点击次数
key->long_press = false; // 重置长按标记
}
break;
}
}
// 按键扫描主函数(每10ms调用一次,由TIM14中断触发)
void APP_Key_Scan(void)
{
// 读取两个按键的实时状态(LL_GPIO_ReadInputPin返回1=高电平(未按下),0=低电平(按下))
uint8_t key_pwr_state = LL_GPIO_ReadInputPin(KEY_PWR_PORT, KEY_PWR_PIN);
uint8_t key_set_state = LL_GPIO_ReadInputPin(KEY_SET_PORT, KEY_SET_PIN);
// 处理开关键(K1)的状态变化,传入按键状态机
Key_Process(&g_KeyPwr, key_pwr_state);
// 处理设置键(K2)的状态变化,传入按键状态机
Key_Process(&s_keySet, key_set_state);
}
关键解析(结合硬件配置与代码逻辑):
-
按键状态机逻辑:通过
Key_Handle_t结构体记录按键的完整状态(空闲、按下、等待双击),避免按键抖动、误触发,确保操作响应稳定,这是单片机按键处理的常用高效方式。 -
按键电平判断:因按键引脚配置为"上拉输入"(APP_GPIO_Init中设置),未按下时引脚为高电平(1),按下时为低电平(0),代码中通过
pin_state == 0判定按键按下。 -
短按/长按区分:通过
g_SysTick(系统1ms计时)计算按键按下的持续时间,小于KEY_LONG_PRESS_MS(1000ms)为短按,大于等于则为长按,逻辑清晰且可通过宏定义灵活调整。 -
功能触发逻辑:
-
开关键(K1):短按切换呼吸灯开关,长按切换系统休眠/唤醒状态,符合用户日常操作习惯。
-
设置键(K2):短按切换数码管显示模式(电量→灯模式→定时时间→倒计时),循环切换,无需多余操作。
-
-
调用时机:APP_Key_Scan每10ms调用一次(由TIM14中断中的计数器触发),既保证了按键响应的及时性,又避免了1ms高频扫描导致的CPU资源浪费。
2.2 数码管显示模块(核心函数:APP_Disp_Refresh、APP_Disp_Update)
核心功能:控制0.25寸118数码管(带闪电+%符号),根据当前显示模式(g_DispMode),显示电池电量、呼吸灯模式、定时时间、倒计时,通过1ms刷新避免闪烁,核心代码及解析如下:
2.2.1 数码管驱动基础(Seg_t结构体与段定义)
本项目采用5线数码管,每次仅点亮一段(1个正极+1个负极),通过Seg_t结构体定义每一段的正负极引脚,结合段码定义实现数字、符号显示,核心定义如下:
// 数码管段定义(正极a:1~5对应S1~S5;负极c:1~5对应S1~S5,低电平有效)
// 数字0~9、%符号、闪电符号(充电提示)的段码
const Seg_t SEG_DIG0_B = {1,2}; // 数字0(对应数码管B位)
const Seg_t SEG_DIG1_B = {2,2}; // 数字1(对应数码管B位)
const Seg_t SEG_DIG2_B = {3,2}; // 数字2(对应数码管B位)
const Seg_t SEG_DIG3_B = {4,2}; // 数字3(对应数码管B位)
const Seg_t SEG_DIG4_B = {5,2}; // 数字4(对应数码管B位)
const Seg_t SEG_DIG5_B = {1,3}; // 数字5(对应数码管B位)
const Seg_t SEG_DIG6_B = {2,3}; // 数字6(对应数码管B位)
const Seg_t SEG_DIG7_B = {3,3}; // 数字7(对应数码管B位)
const Seg_t SEG_DIG8_B = {4,3}; // 数字8(对应数码管B位)
const Seg_t SEG_DIG9_B = {5,3}; // 数字9(对应数码管B位)
const Seg_t SEG_PERCENT = {1,4}; // %符号(电量显示用)
const Seg_t SEG_LIGHTNING = {2,4};// 闪电符号(充电状态显示用)
// 数码管显示缓存(存储当前要显示的段码,共5段,对应数码管5个显示位置)
static Seg_t s_disp_buf[5] = {0};
// 数码管刷新计数器(用于循环刷新5段)
static uint8_t s_disp_cnt = 0;
2.2.2 数码管刷新函数(APP_Disp_Refresh)
核心功能:每1ms调用一次(由TIM14中断触发),循环刷新数码管的5个段,通过"快速切换点亮"实现视觉上的稳定显示(人眼视觉暂留效应),代码实现及解析如下:
// 驱动数码管的某一段(核心辅助函数)
void APP_DriveSeg(Seg_t seg)
{
APP_AllSegOff(); // 先关闭所有段,避免段间串扰(防止多个段同时点亮)
// 控制数码管正极(S1~S5):对应seg.a(1~5),高电平有效
switch(seg.a)
{
case 1: LL_GPIO_SetOutputPin(ALL_SEG_PORTS, S1_PIN); break;
case 2: LL_GPIO_SetOutputPin(ALL_SEG_PORTS, S2_PIN); break;
case 3: LL_GPIO_SetOutputPin(ALL_SEG_PORTS, S3_PIN); break;
case 4: LL_GPIO_SetOutputPin(ALL_SEG_PORTS, S4_PIN); break;
case 5: LL_GPIO_SetOutputPin(ALL_SEG_PORTS, S5_PIN); break;
}
// 控制数码管负极(S1~S5):对应seg.c(1~5),低电平有效
switch(seg.c)
{
case 1: LL_GPIO_ResetOutputPin(ALL_SEG_PORTS, S1_PIN); break;
case 2: LL_GPIO_ResetOutputPin(ALL_SEG_PORTS, S2_PIN); break;
case 3: LL_GPIO_ResetOutputPin(ALL_SEG_PORTS, S3_PIN); break;
case 4: LL_GPIO_ResetOutputPin(ALL_SEG_PORTS, S4_PIN); break;
case 5: LL_GPIO_ResetOutputPin(ALL_SEG_PORTS, S5_PIN); break;
}
}
// 关闭所有数码管段(休眠时调用,降低功耗)
void APP_AllSegOff(void)
{
// 所有正极置低,所有负极置高,确保所有段均熄灭
LL_GPIO_ResetOutputPin(ALL_SEG_PORTS, ALL_SEG_PINS);
}
// 数码管刷新函数(1ms调用一次,循环刷新5段)
void APP_Disp_Refresh(void)
{
// 系统休眠时,关闭所有数码管,直接返回
if(g_SystemState == SYS_STATE_SLEEP)
{
APP_AllSegOff();
return;
}
// 循环刷新5个段(s_disp_cnt从0~4,对应s_disp_buf[0]~s_disp_buf[4])
APP_DriveSeg(s_disp_buf[s_disp_cnt]);
s_disp_cnt = (s_disp_cnt + 1) % 5; // 计数器循环递增,实现5段轮流点亮
}
关键解析:
-
刷新原理:利用人眼视觉暂留效应(约100ms),每1ms点亮数码管的一个段,5ms完成一次完整的5段刷新,视觉上呈现"所有段同时点亮"的效果,避免闪烁。
-
段间串扰避免:每次驱动某一段前,先调用
APP_AllSegOff()关闭所有段,防止前一段未熄灭导致的显示混乱。 -
休眠适配:系统进入休眠模式(SYS_STATE_SLEEP)时,关闭所有数码管,降低待机功耗,符合低功耗设计需求。
2.2.3 数码管显示内容更新函数(APP_Disp_Update)
核心功能:根据当前显示模式(g_DispMode),更新数码管显示缓存(s_disp_buf),决定显示电量、模式、定时时间还是倒计时,代码实现及解析如下:
// 根据数字获取对应的数码管段码(辅助函数)
static Seg_t Get_Digit_Seg(uint8_t digit, uint8_t pos)
{
// pos=0:数码管高位;pos=1:数码管低位;pos=2:符号位(%/闪电)
switch(pos)
{
case 0: // 高位数字(0~9)
switch(digit)
{
case 0: return SEG_DIG0_B; case 1: return SEG_DIG1_B; case 2: return SEG_DIG2_B;
case 3: return SEG_DIG3_B; case 4: return SEG_DIG4_B; case 5: return SEG_DIG5_B;
case 6: return SEG_DIG6_B; case 7: return SEG_DIG7_B; case 8: return SEG_DIG8_B;
case 9: return SEG_DIG9_B; default: return SEG_DIG0_B;
}
case 1: // 低位数字(0~9)
switch(digit)
{
case 0: return SEG_DIG0_B; case 1: return SEG_DIG1_B; case 2: return SEG_DIG2_B;
case 3: return SEG_DIG3_B; case 4: return SEG_DIG4_B; case 5: return SEG_DIG5_B;
case 6: return SEG_DIG6_B; case 7: return SEG_DIG7_B; case 8: return SEG_DIG8_B;
case 9: return SEG_DIG9_B; default: return SEG_DIG0_B;
}
case 2: // 符号位(根据显示模式切换)
if(g_DispMode == DISP_MODE_BATTERY)
return SEG_PERCENT; // 电量模式显示%符号
else if(g_IsCharging)
return SEG_LIGHTNING; // 充电状态显示闪电符号
else
return (Seg_t){0,0}; // 其他情况不显示符号
}
return (Seg_t){0,0};
}
// 更新数码管显示内容(根据当前显示模式)
void APP_Disp_Update(void)
{
uint8_t high_digit = 0, low_digit = 0; // 高位数字、低位数字
switch(g_DispMode)
{
case DISP_MODE_BATTERY: // 显示模式0:电池电量百分比(0~100%)
high_digit = g_BatPercent / 10; // 电量高位(如60% → 高位6)
low_digit = g_BatPercent % 10; // 电量低位(如60% → 低位0)
break;
case DISP_MODE_LED_MODE: // 显示模式1:呼吸灯当前模式(L1~L4)
// 模式0=L1,1=L2,2=L3,3=L4,对应显示01~04
high_digit = 0;
low_digit = g_LedMode + 1;
break;
case DISP_MODE_TIMER_SET: // 显示模式2:呼吸灯定时时间(分钟,默认为15分钟)
high_digit = g_WorkTime / 10; // 定时时间高位(如15分钟 → 高位1)
low_digit = g_WorkTime % 10; // 定时时间低位(如15分钟 → 低位5)
break;
case DISP_MODE_COUNTDOWN: // 显示模式3:呼吸灯剩余工作时间(分钟)
// g_CountDown为剩余时间(秒),转换为分钟(保留整数)
uint16_t countdown_min = g_CountDown / 60;
high_digit = countdown_min / 10;
low_digit = countdown_min % 10;
break;
}
// 更新显示缓存(s_disp_buf[0]~s_disp_buf[4]对应数码管5个段)
s_disp_buf[0] = Get_Digit_Seg(high_digit, 0); // 高位数字
s_disp_buf[1] = Get_Digit_Seg(low_digit, 1); // 低位数字
s_disp_buf[2] = Get_Digit_Seg(0, 2); // 符号位(%/闪电)
s_disp_buf[3] = (Seg_t){0,0}; // 预留段(不显示)
s_disp_buf[4] = (Seg_t){0,0}; // 预留段(不显示)
}
关键解析:
-
显示逻辑:通过
g_DispMode(显示模式变量)区分4种显示场景,将对应的数值(电量、模式、时间)拆分为高位和低位,再通过Get_Digit_Seg函数获取对应的数码管段码。 -
符号显示适配:电量模式显示"%"符号,充电状态(g_IsCharging为true)显示"闪电"符号,直观反馈设备状态,提升用户体验。
-
调用时机:设置键短按切换显示模式后、电量更新后、定时时间调整后、倒计时变化后,均需调用
APP_Disp_Update(),确保数码管显示内容同步更新。
2.3 人机交互模块关键注意事项
-
数码管刷新频率:必须保持1ms一次刷新,若刷新频率过低(如10ms一次),会出现明显闪烁;频率过高则会增加CPU占用率。
-
按键消抖:代码中通过10ms扫描一次按键,间接实现消抖(避免按键接触抖动导致的误触发),无需额外添加消抖延时(避免阻塞主循环)。
-
显示缓存同步:
s_disp_buf是数码管显示的核心缓存,所有显示内容的更新均需操作此缓存,不可直接驱动数码管段,否则会导致显示混乱。 -
休眠状态适配:休眠时必须关闭数码管和呼吸灯,既降低功耗,也避免休眠状态下的无效显示。