欢迎光临
我们一直在努力

沁恒CH32·一键开关机功能设计·定时器版本(源码可直接移植)

目录

1.  前言

2.  按键

3.  工程创建

4.  一键开关机电路工作原理

4.1  关机状态(初始态)

4.2  长按开机

4.3  短按模式切换

4.4  长按关机

5.  代码实现

5.1  定时器功能实现

5.1.1  初始化

5.1.2  中断服务函数

5.2  按键功能实现

5.2.1  宏定义

 5.2.2  电源引脚初始化

5.2.3  按键检测引脚初始化

5.2.4  开机检测函数

5.2.5  按键扫描

5.2.6  获取开机状态

5.3  LED控制逻辑

5.3.1  初始化相关

5.3.2  LED模式运行

5.3.3  模式设置

5.4  主函数


1.  前言

        前面我们已经用延时函数做了一版一键开关机程序:

沁恒CH32·一键开关机功能设计·延时函数版本(源码可直接移植)-CSDN博客

        不过对于我们复杂的工程来说,如果靠延时函数进行一键开关机,可能会阻塞其他功能的运行,因此这里有优化了一版定时器版本。

        这里把上面文章的一些介绍直接拿来用了,不用拐过去看上方链接,直接看本篇文章即可。

2.  按键

        按键机械触点断开、闭合时,由于触点的弹性作用,按键开关不会马上稳定接通或一下子断开,使用按键时会产生图按键抖动说明图中的带波纹信号,需要用软件消抖处理滤波,不方便输入检测。可以利用电容充放电的延时,消除了波纹,从而简化软件的处理,软件只需要直接检测引脚的电平即可。

3.  工程创建

        这里基础配置直接参考:

沁恒CH32在MounRiver Studio上环境配置以及使用详细教程-CSDN博客

        分别按照链接6.2的步骤,创建按键、led灯和定时器的头文件和源文件:

        记得把文件路径添加一下:

4.  一键开关机电路工作原理

        电路图如下:

元件 型号 / 参数 功能描述
Q4 AO3401A (P-MOS) 主电源开关管,控制 VIN 到 VCC 的通断
Q6 S8050 (NPN) 驱动三极管,控制 Q4 的导通 / 关断
D1/D2 1N5819W S4 (肖特基二极管) 单向导通,实现长按开机、短按模式切换的逻辑隔离
U3 轻触按键 用户输入接口,实现长按 / 短按操作
R10/R11/R12 10K/1K/10K 限流、分压与下拉电阻,稳定电路状态
C5 100nF 按键消抖与滤波电容

        下面我们根据电路图来讲解一下工作原理。

4.1  关机状态(初始态)

        开始时,VCC 无输出,系统处于断电状态:

  • Q4(P-MOS)栅极通过 R10 被上拉至 VIN,因此 Q4 关断;
  • Q6(NPN)基极通过 R12 下拉至 GND,因此 Q6 关断;
  • 按键 U3 此时也未被按下处于断开状态。

        此时除了极短暂的电容 C5 充电电流外,没有持续的电流从 VBAT 流出,整个电路处于低功耗待机状态:

4.2  长按开机

        当我们长按按键 U3,按键闭合,此时电流的路径为VBAT → R10 → D1 → 按键 U3 → GND:

        此时 Q4 栅极被拉低至接近 GND,Q4 导通,VCC 输出,系统上电(注意:这里的PMOS的工作原理可以上网查阅,这里只讲解一键开关机的工作原理):

        此时系统上电,也就是单片机供上电了,我们可以在初始化阶段控制 PC3 引脚输出高电平,此时 Q6 导通:

        此时就算松开按键,由于 Q6 导通,Q4 栅极还会处于拉低至 GND 状态,因此自锁 Q4 的导通状态,系统仍会通电:

4.3  短按模式切换

        此时系统已开机,Q4 保持导通,此时因为二极管 D1 的原因电流流向如下:

        此时形成一个简单的电平检测电路,进行输入检测,检测 PC2 引脚高低电平变化,系统识别按键触发事件,执行模式切换逻辑。

4.4  长按关机

        当系统处于开机状态,我们可以长按按键 U3,系统通过 PC2 引脚持续检测到高电平,识别为长按关机指令,此时将 PC3 引脚拉低,当 PC3 引脚输出低电平,使 Q6 关断,不过此时由于U3 仍处于按下状态,所以此时 Q4 栅极仍被拉低至接近 GND,Q4 处于导通状态,给系统供电:

        当我们松开 U3,按键弹起,此时 Q4 栅极通过 R10 被上拉至 VBAT,Q4 关断,VCC 停止输出,系统断电:

5.  代码实现

5.1  定时器功能实现

5.1.1  初始化

        初始化通用定时器 TIM2:

void TIM2_INT_Init(u16 arr, u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);// 使能TIM2时钟

// 定时器时基配置
TIM_TimeBaseInitStructure.TIM_Period = arr; // 自动重装值
TIM_TimeBaseInitStructure.TIM_Prescaler = psc; // 预分频系数
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);

// 清除更新中断标志位
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);

// 配置NVIC中断优先级
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // TIM2中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能中断通道
NVIC_Init(&NVIC_InitStructure);

TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);// 使能TIM2更新中断

TIM_Cmd(TIM2, ENABLE);// 启动TIM2计数器
}

对于定时器的相关介绍,这里不再做过多描述,不熟悉可以参考: 沁恒CH32速通篇(二)·通用定时器点灯实验-CSDN博客

其实和STM32差不多,区别在于中断服务函数部分。

5.1.2  中断服务函数

        CH32V003 区别于 STM32 ,其在调用中断服务函数之前,需要先调用如下函数声明:

void TIM2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

  • TIM2_IRQHandler:TIM2 中断服务函数的固定名称(不能自定义),定时器触发中断时,CPU 会自动跳转到这个函数执行;
  • __attribute__((interrupt("WCH-Interrupt-fast"))):CH32V003 中断函数必须加这个属性!作用是告诉编译器这是 “快速中断函数”,适配沁恒 RISC-V 内核的中断机制,不加的话中断无法正常响应。

        然后调用中断服务函数,这里进行10ms的计时,没过10ms 后 g_timer_tick_ms 的值就会加10:

// 全局计时变量(10ms中断一次,每次+10)
uint32_t g_timer_tick_ms = 0;

void TIM2_IRQHandler(void)
{
// 检查更新中断标志位
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
// 每次中断累加10ms
g_timer_tick_ms += 10;

// 清除中断标志位
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}

5.2  按键功能实现

        首先我们先完善一下思路,我们想要实现,一个按键控制两种功能:模式切换和开关机。

        按照一键开关机电路的工作原理,我们实现:

  • 长按按键(≥1 秒):触发开机(拉高 PC3 引脚,开启电源自锁)
  • 短按按键(30ms):释放按键后切换 LED 工作模式
  • 长按按键(≥3 秒):触发关机(拉低 PC3 引脚,解除电源自锁)
  • 硬件层面:PC3 作为电源维持引脚(输出),PC2 作为按键检测引脚(输入)

5.2.1  宏定义

        首先我们对开关机的引脚进行一下宏定义,提高代码的可读性,方便后续移植:

// 按键/开关机引脚定义
#define POWER_ON_PORT GPIOC
#define POWER_ON_PIN GPIO_Pin_3 // PC3:维持供电
#define MODE_DET_PORT GPIOC
#define MODE_DET_PIN GPIO_Pin_2 // PC2:检测按键

        然后,对时间阈值进行一个进行宏定义,方便更改长按和短按的时间:

// 按键时间阈值
#define POWER_SHUTDOWN_TIME_MS 3000 // 长按关机(3秒)
#define POWER_ON_TIME_MS 1000 // 长按开机(1秒,可调整)
#define SHORT_PRESS_TIME_MS 30 // 短按判定(30ms)

 5.2.2  电源引脚初始化

        创建一个标志位记录系统开关机状态:

static uint8_t g_system_power_on = 0;//0=关机,1=开机

        初始化 PC3 为推挽输出模式,由于开始电源未触发,默认拉低,等待按键按下在将电平拉高:

void PowerOn_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; // GPIO初始化结构体
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

// 配置PC3为推挽输出
GPIO_InitStructure.GPIO_Pin = POWER_ON_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; // 低速:降低功耗,提高稳定性
GPIO_Init(POWER_ON_PORT, &GPIO_InitStructure);

//初始化时PC3默认拉低(关机状态)
GPIO_ResetBits(POWER_ON_PORT, POWER_ON_PIN);
g_system_power_on = 0; // 初始标记为关机
}

5.2.3  按键检测引脚初始化

        将 PC2 模式切换引脚配置为上拉输入,单片机内部提供上拉,按键按下时接地为低电平:

void ModeDet_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能GPIOC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
// 配置PC2为上拉输入
GPIO_InitStructure.GPIO_Pin = MODE_DET_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(MODE_DET_PORT, &GPIO_InitStructure);
}

5.2.4  开机检测函数

        我们通过 if() 语句判断,g_system_power_on是否等于0,若等于 0 则证明,此时系统处于关机状态,进入开机检测,通过 GPIO_ReadInputDataBit() 函数循环检测按键是否被按下,以及按下时长:

// 仅在关机状态下检测(避免开机后重复进入)
if (g_system_power_on == 0)
{
// 循环检测按键是否长按(低电平=按下)
while (GPIO_ReadInputDataBit(MODE_DET_PORT, MODE_DET_PIN) == 0)
{

}
}

        我们定义一个局部变量,用于检测首次按键按下的时间(按键按下当前定时器的时间),当按键按下,将此时定时器时间赋值给 press_start:

if (press_start == 0)
{
press_start = g_timer_tick_ms;
}

        随着按键按下,计时器继续计时,知道当前计时器的时间减去起始时间大于等于我们所设置的长按开机的时间后将  PC3 引脚拉高,自锁电源,同时将系统标记为开机状态,那么当我们处于开机状态时,就不会在进入电源检测函数:

// 长按达到1秒,触发开机
else if (g_timer_tick_ms – press_start >= POWER_ON_TIME_MS)
{
GPIO_SetBits(POWER_ON_PORT, POWER_ON_PIN); // PC3拉高,自锁电源
g_system_power_on = 1; // 标记系统开机
LED_ALL_OFF(); // 开机初始化LED
press_start = 0; // 重置起始时间
}

      判断逻辑,可以根据长按开机原理一起验证一下:

// 仅在关机状态下检测
if (g_system_power_on == 0)
{
// 检测按键是否按下(低电平)
if (GPIO_ReadInputDataBit(MODE_DET_PORT, MODE_DET_PIN) == 0)
{
// 首次按下:记录起始时间
if (press_start == 0)
{
press_start = g_timer_tick_ms;
}
// 长按达到1秒,触发开机
else if (g_timer_tick_ms – press_start >= POWER_ON_TIME_MS)
{
GPIO_SetBits(POWER_ON_PORT, POWER_ON_PIN); // PC3拉高,自锁电源
g_system_power_on = 1; // 标记系统开机
LED_ALL_OFF(); // 开机初始化LED
press_start = 0; // 重置起始时间
}
}
else
{
// 按键释放:重置起始时间
press_start = 0;
}
}

5.2.5  按键扫描

        第一步关机状态下,先检测是否开机:

if (g_system_power_on == 0)
{
Check_Power_On(); // 调用开机检测函数
return; // 未开机则跳过后续所有按键逻辑,直接返回
}

        第二步,如果正常开机,那说明此时已经形成如下电路模式:

        我们先读取 PC2 的引脚电平状态:

// 读取PC2引脚电平(上拉输入:高电平=未按,低电平=按下)
uint8_t key_level = GPIO_ReadInputDataBit(MODE_DET_PORT, MODE_DET_PIN);

        初始化两个变量,一个记录按键是否按下,一个累计按键按下的时长,方便对按键状态进行检测:

static uint8_t g_key_pressed = 0; // 按键是否按下(0=否,1=是)
static uint32_t g_key_press_ms = 0; // 按键累计按下时长(ms)

        首先如果此时处于低电平,也就是按键被按下,当第一次被按下,开始初始化计时,将按键电平状态设为 1,代表按键被按下,同时记录首次按键按下的时间:

if (g_key_pressed == 0) // 首次按下,记录起始时间
{
g_key_pressed = 1;
g_key_press_start = g_timer_tick_ms; // 记录按下时的定时器值
}

        同样的原理,当定时器值累加(也就是按键一直没有松手)时间减去第一次按下的时间,大于我们前面宏定义的触发关机的最大时长时,我们此时拉低电平,解除电源的自锁:

// 获取按键按下时长(基于定时器)
static uint32_t Get_Key_Press_Time(void)
{
return g_timer_tick_ms – g_key_press_start;
}

        此时松手电源就会停止供电:

// 长按达到3秒,触发关机
if (Get_Key_Press_Time() >= POWER_SHUTDOWN_TIME_MS)
{
GPIO_ResetBits(POWER_ON_PORT, POWER_ON_PIN); // PC3拉低,解除自锁
g_system_power_on = 0; // 标记系统关机
LED_ALL_OFF(); // 机关闭LED
while (1); // 死循环,等待电源断开
}

        上面是长按状态,下面是短按触发模式切换,由于无论我们长按还是短按,都是先标记为按下状态,也就是 g_key_pressed 此时状态为1,然后判断此时按下时长是否大于短按的时长,且小于长按的时长,说明此时是短按状态,可以在此进行一些如 LED 模式切换的功能,之后重置按键状态:

if (g_key_pressed == 1) // 之前按下过,现在释放
{
uint32_t press_time = Get_Key_Press_Time();
// 短按(30ms~3000ms),切换LED模式
if (press_time >= SHORT_PRESS_TIME_MS && press_time < POWER_SHUTDOWN_TIME_MS)
{
// 这里可以添加一些循环切换LED模式的代码,后面实现完LED功能后再做补充

}
// 重置按键状态,等待下一次按下
g_key_pressed = 0;
g_key_press_ms = 0;
}

5.2.6  获取开机状态

        由于我们上面什么开机状态使用了 static 因此主函数无法调用,这里通过函数返回值进行主函数调用,当然去掉 static,在同文件通过 extern 声明也是一个效果:

// 系统开机状态读取函数(对外提供访问接口)
uint8_t Get_System_Power_State(void)
{
return g_system_power_on; // 返回当前系统开关机状态
}

5.3  LED控制逻辑

5.3.1  初始化相关

        首先对LED控制引脚宏定义一下,方便后续更改:

// LED引脚宏定义:PC4/PC5/PC6(三个LED)
#define LED_PORT GPIOC
#define LED4_PIN GPIO_Pin_4
#define LED5_PIN GPIO_Pin_5
#define LED6_PIN GPIO_Pin_6
#define LED_ALL_PIN (LED4_PIN | LED5_PIN | LED6_PIN) // 所有LED引脚组合

// LED控制宏:低电平点亮(硬件设计决定)
#define LED4_ON() GPIO_ResetBits(LED_PORT, LED4_PIN) // 拉低=点亮
#define LED4_OFF() GPIO_SetBits(LED_PORT, LED4_PIN) // 拉高=熄灭
#define LED5_ON() GPIO_ResetBits(LED_PORT, LED5_PIN)
#define LED5_OFF() GPIO_SetBits(LED_PORT, LED5_PIN)
#define LED6_ON() GPIO_ResetBits(LED_PORT, LED6_PIN)
#define LED6_OFF() GPIO_SetBits(LED_PORT, LED6_PIN)
#define LED_ALL_ON() GPIO_ResetBits(LED_PORT, LED_ALL_PIN)
#define LED_ALL_OFF() GPIO_SetBits(LED_PORT, LED_ALL_PIN)

        然后通过枚举类型,声明一下LED的几种模式:

typedef enum {
LED_MODE_BLINK_ALL = 0, // 模式0:全灯闪烁
LED_MODE_FLOW, // 模式1:流水灯(4→5→6→全灭循环)
LED_MODE_SINGLE_BLINK, // 模式2:单灯循环闪(4→5→6循环)
LED_MODE_MAX // 模式总数(用于边界判断)
} LED_Mode_TypeDef;

        在声明一些变量,方便后续调用:

static LED_Mode_TypeDef g_led_mode = LED_MODE_BLINK_ALL; // 当前LED模式(默认全闪)
static uint32_t g_led_tick_ms = 0; // LED累计计时(ms),控制灯光切换频率
static uint8_t g_flow_step = 0; // 流水灯步骤
static uint8_t g_single_led = 0; // 单灯循环索引

        引脚初始化:

void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 使能GPIOC时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6; // 选择PC4/5/6
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 输出速率50MHz
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出模式
GPIO_Init(GPIOC, &GPIO_InitStructure); // 初始化引脚
GPIO_SetBits(GPIOC, GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6); // 默认拉高,LED熄灭
}

5.3.2  LED模式运行

        就是点个灯,可以按照自己的逻辑来写,这里不做过多介绍:

void LED_Run_Current_Mode(void)
{
// 每500ms更新一次LED状态
if (g_timer_tick_ms – g_led_start_tick >= 500)
{
g_led_start_tick = g_timer_tick_ms; // 重置起始时间

switch (g_led_mode)
{
case LED_MODE_BLINK_ALL:
// 翻转所有LED
if (GPIO_ReadOutputDataBit(LED_PORT, LED4_PIN) == 1) LED_ALL_ON();
else LED_ALL_OFF();
break;

case LED_MODE_FLOW:
// 流水灯:4→5→6→全灭
LED_ALL_OFF();
switch (g_flow_step)
{
case 0: LED4_ON(); break;
case 1: LED5_ON(); break;
case 2: LED6_ON(); break;
default: g_flow_step = -1; break;
}
g_flow_step = (g_flow_step + 1) % 4;
break;

case LED_MODE_SINGLE_BLINK:
// 单灯循环
LED_ALL_OFF();
switch (g_single_led)
{
case 0: LED4_ON(); break;
case 1: LED5_ON(); break;
case 2: LED6_ON(); break;
}
g_single_led = (g_single_led + 1) % 3;
break;

default:
g_led_mode = LED_MODE_BLINK_ALL;
break;
}
}
}

5.3.3  模式设置

        每次调用如下函数,通过改变mode的值,进行模式的切换:

void LED_Set_Mode(LED_Mode_TypeDef mode)
{
if (mode < LED_MODE_MAX)
{
g_led_mode = mode;
LED_ALL_OFF();
g_led_start_tick = g_timer_tick_ms; // 重置LED计时
g_flow_step = 0;
g_single_led = 0;
}
}

        例如,我们在按键扫描,中通过每次按键按下,计算mode的值,进行短按模式的切换:

// 短按(30ms~3000ms),切换LED模式
if (g_key_press_ms >= SHORT_PRESS_TIME_MS && g_key_press_ms < POWER_SHUTDOWN_TIME_MS)
{
static LED_Mode_TypeDef current_mode = LED_MODE_BLINK_ALL;
current_mode = (LED_Mode_TypeDef)((current_mode + 1) % LED_MODE_MAX);
LED_Set_Mode(current_mode);
}

5.4  主函数

        就是通过主函数调用按键检测函数和LED更新函数:

#include "ch32v00x.h"
#include "debug.h"
#include "led.h"
#include "key.h"
#include "Timer.h"

int main(void)
{
SystemCoreClockUpdate();
Delay_Init(); // 延时初始化

// 初始化定时器(10ms中断一次)
TIM2_INT_Init(99, 4799);

LED_Init();
ModeDet_Init();
PowerOn_Init();

while (1)
{
Key_Scan(); // 检测按键(基于定时器计时)
if (Get_System_Power_State() == 1)
{
LED_Run_Current_Mode();
}
}
}

        由于CSDN文章无法放视频,这里无法进行视频演示,可以自行根据第四小节的思路,移植看一下,主要就是通过 PC3 引脚状态的切换,实现硬件电路的通断,实现系统的开关电源,后续根据需求在完善一个定时器版本的。

完整工程:

基于CH32实现一键开关机设计(定时器版本)资源-CSDN下载

CH32学习笔记_时光の尘的博客-CSDN博客

赞(0)
未经允许不得转载:171主机测评 » 沁恒CH32·一键开关机功能设计·定时器版本(源码可直接移植)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址