
Привет, Хабр!
Автономные системы становятся все более востребованными - от портативных приборов до сложных промышленных комплексов - надежное управление напряжением питания превращается в ключевой фактор их долговечности и эффективности. Сердце любой такой системы - аккумулятор, а его безопасность и срок службы, напрямую зависит от контроля напряжения.
В данной статье будет представлен пример контроля напряжения, над блоком питания - внутри которого (никель-металлгидридная аккумуляторная сборка NiMH 14.4В/12 банок по 1.2В(1.4В- при полной зарядке)).
В блоке питания уже есть палата управления над аккумулятором, которая выполняет задачи:
Работа с кнопкой;
Работа со светодиодом;
Работа с пъезоэлектрическим излучателем(звуковая индикация);
Контроль заряда/разряда аккумулятора(дает звуковой сигнал при напряжении менее 9 вольт и более 14).
В процессе анализа и статистики использования оборудования стало очевидно, что многие пользователи часто забывают своевременно отключать блоки питания. В результате аккумуляторные сборки продолжают разряжаться даже при отсутствии необходимости, напряжение падает до критических значений, и аккумулятор быстро теряет свою емкость, становясь непригодным для дальнейшей эксплуатации.
Помимо этого, подключенная система оказывается в неопределенном состоянии, на плату управления и различные датчики по-прежнему подается питание, что приводит к избыточной нагрузке. В качестве этого сокращается ресурс электронных компонентов и снижается надежность всего устройства.
Для решения данной проблемы я продемонстрирую пример системы контроля напряжения блока питания, в качестве микроконтроллера выбран STM32F103С8T6, который выполняет следующие задачи:
Непрерывный мониторинг напряжения аккумуляторной сборки, измерение производится через АЦП с использованием DMA;
Оповещение пользователя о низком заряде, при падении напряжения ниже установленного порога (в данном примере - 9.0В) система активизирует звуковой сигнал, время работы оповещения ограничено - звуковая индикация будет длится 5 минут;
Переход в энергосберегающий режим, если напряжение остается ниже порога и пользователь не предпринял действий в течении заданного времени, микроконтроллер переводит систему в режим сна, это сопровождается отключением тактирования и всей периферии, что минимизирует и предотвращает глубокий разряд аккумулятора.
Схема подключения NiMH АКБ, делителя напряжения к АЦП МК, кнопки вкл/выкл и пъезо-излучателя

Перечень компонентов
|
|
|
|
|
R1, R3, R4, R5, R9- (0805 - 10 кОм ± 5%) |
С1-0805 - X7R – 50 В – 0,1 мкФ |
FP1, FP2, FP3- MF-SM100/33-1.1А |
VT1, VT3-IRF7416, P-канал |
МК STM32F103C8T6 |
R2- (0805 - 20 кОм ± 5%) |
VT2, VT4-BC847C |
|||
R6-(0805 – 221 кОм ± 5%) |
AD4-BAT54S |
BA-1-Излучатель пьезоэлектрический HCM1206X |
||
R7-(0805 - 27 кОм ± 5%) |
Аккумуляторная сборка NiMh 14.4V |
|||
R8-Резистор подстроечный (3314G-1-502E, 5 кОм |
SA2- Выключатель 113-8748 |
Объяснение схемы
Узел[1]
Входной силовой ключ, исток подключен к +12V, сток идет к блоку питания(+12_АКК) через предохранители, а затвор подтянут к земле, применение (защищенная подача напряжения питания с аккумуляторного блока);
Узел[2]
Вторичный силовой ключ, обеспечивает управляемое включение/отключение напряжения питания основной нагрузки системы, управление происходит через МК, сигнал PWR_ON, после включения данного узла, на стоке напряжение питания +12ВК активизируется и передает напряжение другим частям схемы, в моем примере это (узел[4]-Звуковая индикация и узел[5]-lделитель напряжения), но также можно использовать данный узел и на включение преобразователей напряжения;
Узел[3]
Данный узел обеспечивает логику взаимодействия с кнопкой включения/выключения, кнопка sa2, при нажатии формирует управляющий сигнал, диод АD4 и конденсатор С1 обеспечивают фильтрацию и антидребезг.
Узел[4]
Данный узел обеспечивает звуковую индикацию, сигнал BEEP подключается к МК;
Узел[5]
Данный узел является делителем напряжения, делит напряжение до уровня, подходящего для измерения АЦП МК (обычно до 3.3V), подстроечный резистор R8, выставлен на 1.7кОм.
Настройка микроконтроллера STM32F103 в CubeIDE

Конфигурация TIM1(PA11)
Таймер TIM1 выполняет роль генератора для звуковой индикации.
Настройка таймера:
Предделитель (Prescaler) и период (Auto-Reload) выбраны так, чтобы на выходе формировался сигнал с частотой в диапазоне, воспринимаемом слухом (обычно 1–5 кГц);
Режим работы – PWM (широтно-импульсная модуляция);
Коэффициент заполнения (Pulse) определяет громкость и характер звучания.
Принцип работы:
Таймер генерирует ШИМ-сигнал, который подаётся на транзисторный ключ. Транзистор управляет пьезоизлучателем или динамиком. В результате получается слышимый звук.
Преимущества такого решения:
Микроконтроллеру не нужно вручную формировать частоту – этим занимается таймер;
Легко изменять тональность: достаточно переписать значения ARR/PSC;
Можно реализовать разные звуковые эффекты (короткие сигналы, мелодии) простым управлением таймером из программы.

Конфигурация ADC(PA1)
В проекте используется многоканальный режим работы АЦП с двумя каналами в последовательности (Regular conversion sequence).
Rank 1 – внешний канал (ADC Channel 1).
Этот вход подключён к делителю напряжения и используется для измерения напряжения аккумулятора.
Благодаря этому микроконтроллер может в реальном времени контролировать состояние питания устройства.
Rank 2 – внутренний канал (Vrefint).
Это встроенный источник опорного напряжения микроконтроллера. Он служит для автоматической калибровки и компенсации возможных изменений питающего напряжения. С его помощью можно более точно измерять значение внешних сигналов, в том числе напряжение аккумулятора.
Для повышения эффективности задействован DMA: результаты обоих измерений (Rank 1 и Rank 2) автоматически передаются в память, а процессор получает только готовые данные.

Для правильной настройки ADC я воспользовался данной информацией, там подробно расписано как работать с ADC МК-STM32.
Конфигурация пина для работы с кнопкой
Для работы с кнопкой выбран вывод PB11, сконфигурированный в режиме:
GPIO_EXTI – внешний прерывающий вход. Это значит, что нажатие кнопки обрабатывается не опросом в цикле, а через аппаратное прерывание;
Mode - External interrupt, Falling edge trigger – прерывание срабатывает по спаду сигнала (при замыкании кнопки на землю);
Pull-up – включен внутренний подтягивающий резистор, который удерживает вход в состоянии логической «1», пока кнопка не нажата.
Кнопка подключена так, что в обычном состоянии на входе PB11 присутствует логическая «1» благодаря встроенному подтягивающему резистору (Pull-up). При нажатии контакт замыкается на землю, формируется логический «0» и происходит спад сигнала. Этот спад фиксируется модулем EXTI, который вызывает прерывание.

Конфигурация пина для работы с сигналом PWR_ON
Сигнал PWR_ON играет роль электронного «выключателя питания».
В исходном состоянии (Low) нагрузка обесточена.
При активации (перевод вывода в High) силовой MOSFET открывается, и напряжение +12ВК подаётся на остальные узлы системы.
В примере данная линия питает:
узел [4] – звуковую индикацию,
узел [5] – делитель напряжения для мониторинга питания.
Аналогично этот узел можно использовать и для включения DC/DC-преобразователей или других модулей, требующих управляемого питания.

Конфигурация Clock

Реализация программного кода
Ссылка на скачивание исходного кода [ https://t.me/ChipCraft В закрепленном сообщении [ #исскуствомк_исходный_код -Исходный код для Adc_VoltageControl_STM32F103C8T6]
Заголовочный файл keys.h (работа с кнопками)
В данном файле определены:
Функции работы с кнопками;
Битовые маски состояний;
константы для различных сценариев нажатий.
keys.h
#ifndef INC_PROJECT_BU_KEYS_H_
#define INC_PROJECT_BU_KEYS_H_
unsigned short getKeyState(void);
//выдаёт состояние пинов кнопок в данный момент без учёта задержек для дребезга
unsigned short getKeyPinState_AtNow(void);
void keysDrv_Handler(void);//эту функцию нужно вызывать постоянно
#ifndef KEY1_Drv
#define KEY1_Drv 1u //кнопка нажата
#endif
#ifndef KEY1Double_Click_Drv
#define KEY1Double_Click_Drv
#endif
#ifndef KEY3_Drv
#define KEY3_Drv 4u
#endif
#ifndef KEY4_Drv
#define KEY4_Drv 8u
#endif
#ifndef KEY1Hold_Drv
#define KEY1Hold_Drv 16u // кнопка удерживается
#endif
#ifndef KEY2Hold_Drv
#define KEY2Hold_Drv 32u
#endif
#ifndef KEY3Hold_Drv
#define KEY3Hold_Drv 64u
#endif
#ifndef KEY4Hold_Drv
#define KEY4Hold_Drv 128u
#endif
#ifndef KEY1Release_Drv
#define KEY1Release_Drv 256u //Бит отпускания кнопки
#endif
#ifndef KEY2Release_Drv
#define KEY2Release_Drv 512u
#endif
#ifndef KEY3Release_Drv
#define KEY3Release_Drv 1024u
#endif
#ifndef KEY4Release_Drv
#define KEY4Release_Drv 2048u
#endif
#endif /* INC_PROJECT_BU_KEYS_H_ */
Реализация модуля keys.c (работа с кнопками)
Данный модуль включает в себя задачи:
Устранение дребезга контактов;
Различие между коротким и долгим нажатием;
Отслеживание событий нажатия, удержания и отпускания.
keysDrv_Handler()
Вызывается постоянно в основном цикле или из системного таймера.
Нажатие кнопки (первичное событие)
2. if(gl_kDrv_key1_blockEvent == 0 && ON_OFFB_state == KEY_PRESS) {
3. gl_kDrv_key1_blockEvent = 1;
4. gl_kDrv_time_key1_press = ms;
5. }
Сохраняется время нажатия, дальнейшие события блокируются до отпускания
Фильтр дребезга
else if(gl_kDrv_key1_blockEvent == 1 &&
ON_OFFB_state==KEY_PRESS &&
gl_kDrv_key1_short_state==0 &&
(ms - gl_kDrv_time_key1_press) > DELAY4TIMER){
//прошло время защиты от дребезга, кнопка нажата
gl_kDrv_key1_short_state=1;
keyState &= ~KEY1Release_Drv;//снимаем бит отпускания кнопки
keyState |= KEY1_Drv;
}
Если прошло больше DELAY4TIMER, считаем кнопку реально нажатой.
Определение удержания
else if(gl_kDrv_key1_blockEvent == 1 &&
ON_OFFB_state==KEY_PRESS &&
gl_kDrv_key1_short_state==1 &&
(ms - gl_kDrv_time_key1_press) > DELAY_HOLD_TIMER){
keyState |= KEY1Hold_Drv;
}
Если прошло больше DELAY_HOLD_TIMER, выставляем бит удержания.
Отпускание кнопки
else if(gl_kDrv_key1_blockEvent == 1 && ON_OFFB_state==KEY_UNPRESS && gl_kDrv_key1_short_state==1){
gl_kDrv_key1_blockEvent=0;
gl_kDrv_key1_short_state=0;
keyState |= KEY1Release_Drv;
keyState &= ~KEY1_Drv;//снимаем бит нажатия кнопки
keyState &= ~KEY1Hold_Drv;//снимаем бит удержания кнопки
}
При отпускании кнопки сбрасываются флаги удержания и нажатия, выставляется бит отпускания.
getKeyState()
Возвращает текущее состояние кнопок в виде битовой маски
Позволяет определить, была ли кнопка нажата, удержана или отпущена;
После считывания некоторые флаги (например, отпускание) сбрасываются ,чтобы событие не повторялось.
getKeyPinState_AtNow()
Возвращает моментальное состояние ножек GPIO, без учета дребезга
Полезно для отладки или когда нужно мгновенно узнать, нажата ли кнопка прямо сейчас
keys.c
#include "./Project/BU/keys.h"
#include "./Project/shared.h"
#include "main.h"
#include <stdlib.h>//abs
#include <string.h>//memset
#include <stdio.h>
//установки
#define DELAY4TIMER 20//задержка для таймера в миллисекундах (на дребезг)
//25u//больше чем COUNT_HOLD_PERIODS, то считается, что кнопка зажата (долгое нажатие)
#define COUNT_HOLD_PERIODS 40
#define DELAY_HOLD_TIMER 400//задержка для отслеживания удержания в миллисекундах
//E N D установки
//настройки
#define KEY1_GPIO_Port GPIOB //буква порта для кнопки (GPIOA, GPIOB, GPIOC, ...)
#define KEY1_Pin GPIO_PIN_11 //номер пина на порту для кнопки
//E N D настройки
volatile unsigned short keyState=0x0;//установка битов что кнопки отпущены
#define KEY_PRESS 1 //1=нажатая кнопка это логическая единица, иначе 0
#define KEY_UNPRESS 0//0=отпущенная кнопка это логический ноль, иначе 1
uint8_t gl_kDrv_key1_blockEvent = 0;//обрабатывать ли событие нажатия кнопки 1
uint32_t gl_kDrv_time_key1_press = 0;//время, когда была нажата кнопка
uint8_t gl_kDrv_key1_short_state = 0;//обработали ли короткое нажатие
void keysDrv_Handler(){
uint32_t ms = HAL_GetTick();
uint8_t ON_OFFB_state = HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin);
//кнопка 1
//если не заблокировано обработка события и кнопка нажата
if(gl_kDrv_key1_blockEvent == 0 && ON_OFFB_state==KEY_PRESS){
gl_kDrv_key1_blockEvent=1;
gl_kDrv_time_key1_press = ms;
}else if(gl_kDrv_key1_blockEvent == 1 && ON_OFFB_state==KEY_PRESS && gl_kDrv_key1_short_state==0
&& (ms - gl_kDrv_time_key1_press) > DELAY4TIMER){
//прошло время защиты от дребезга, кнопка нажата
gl_kDrv_key1_short_state=1;
keyState &= ~KEY1Release_Drv;//снимаем бит отпускания кнопки
keyState |= KEY1_Drv;
//кнопка удерживается
}else if(gl_kDrv_key1_blockEvent == 1 && ON_OFFB_state==KEY_PRESS
&& gl_kDrv_key1_short_state==1 && (ms - gl_kDrv_time_key1_press) > DELAY_HOLD_TIMER){
keyState |= KEY1Hold_Drv;
//кнопка отпущена
}else if(gl_kDrv_key1_blockEvent == 1 && ON_OFFB_state==KEY_UNPRESS && gl_kDrv_key1_short_state==1){
gl_kDrv_key1_blockEvent=0;
gl_kDrv_key1_short_state=0;
keyState |= KEY1Release_Drv;
keyState &= ~KEY1_Drv;//снимаем бит нажатия кнопки
keyState &= ~KEY1Hold_Drv;//снимаем бит удержания кнопки
//если сработало первое условие но не сработали остальные
}else if(gl_kDrv_key1_blockEvent == 1 && ON_OFFB_state==KEY_UNPRESS
&& (ms - gl_kDrv_time_key1_press) > COUNT_HOLD_PERIODS*2){
gl_kDrv_key1_blockEvent=0;
gl_kDrv_key1_short_state=0;
}
}
unsigned short getKeyState(void){
// данный блок служит для проверки срабатывает ли keyState никуда дальше не идет.
if(keyState){//-проверяет является ли выражение не нулевым
int i=0;
i++;
}
unsigned short ret=keyState;//здесь я получаю состояние кнопки
keyState &= 0xF0FF;//снимаем бит отпускания кнопки
return ret;
}
//выдаёт состояние пинов кнопок в данный момент без учёта задержек для дребезга
unsigned short getKeyPinState_AtNow(void)
{
unsigned short ret=0;
uint8_t key1_state = HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin);
if(key1_state==KEY_PRESS){//если кнопка нажата
ret |= KEY1_Drv;
}
return ret;
}
Реализация модуля ADC_Calc.c (работа с АЦП)
Данный модуль реализует контроль напряжения питания через АЦП микроконтроллера STM32F103C8T6, измерения выполняются с использованием:
DMA (циклическая запись данных в буфер);
Встроенного опорного напряжения Vrefint;
Собственного делителя напряжения на входе.
ADC_Calc_Handler() - главный обработчик вычислений
Проверяет, заполнена ли первая или вторая половина DMA-буфера;
Усредняет значения для канала измерения и Vrefint;
Конвертирует результат в напряжение вызовом adc_calcVoltage();
Если напряжение ниже порога critical_stress → возвращает команду отключения питания (FORCE_POWER_OFF);
Производит фильтрацию значений по диапазону 750 < val_input < 2800 (защита от шумов и выбросов).
HAL_ADC_ConvCpltCallback()
Вызывается по прерыванию DMA Transfer Complete → устанавливает флаг adcIRFullDone
HAL_ADC_ConvHalfCpltCallback()
Вызывается по прерыванию DMA Half Transfer Complete → устанавливает флаг adcIRHalfDone
adc_init()
Сброс флагов и буфера.
adc_start()
Запускает АЦП в режиме DMA, далее происходит циклическое заполнение adcDMAbuf без участия процессора
adc_stop()
Останавливает АЦП и DMA;
Сбрасывает флаги готовности
adc_calcVoltage()
Ключевая функция — переводит «сырые» значения АЦП в напряжение
adc_GetVoltage()
Геттер для получения последнего значения рассчитанного напряжения
Общий алгоритм работы:
DMA заполняет буфер парами значений (input, Vrefint);
При заполнении половины буфера → срабатывает прерывание, ставится флаг;
ADC_Calc_Handler считывает данные, фильтрует и усредняет;
Вызывается adc_calcVoltage, которая переводит вольты;
Значение доступно через adc_GetVoltage();
Если напряжение меньше 9 В (по critical_stress) → отрабатывает аварийное выключение.
ADC_Calc.c
#include "./Project/shared.h"
#include "./Project/BU/keys.h"
#include "./Project/BU/ADC_Calc.h"
#include "main.h"
#include <stdlib.h>//abs
#include <string.h>//memset
#include <stdio.h>
//у F103 нет калибр.значения, взято из datasheet
#define VREFINT_TYP 1.20
#define SIZE_DMABUF 400
volatile uint16_t adcDMAbuf[SIZE_DMABUF] = {0,};
volatile uint8_t adcIRFullDone=0; //сработало прерывание
volatile uint8_t adcIRHalfDone=0; //сработало прерывание
float gl_voltage=0;
void adc_calcVoltage(uint16_t avg_input, uint16_t avg_vref);
float vadc_ = 0.0f;
float vin = 0.0f;
float vdda = 0.0f;
float critical_stress =9.0f;
unsigned char ret = 0;
uint16_t avg_input = 0;
uint16_t avg_vref = 0;
uint16_t vrefint_cal_adr = 0;
unsigned char ADC_Calc_Handler()
{
uint32_t sum_input = 0;
uint32_t sum_vrefint = 0;
int count = 0;
if(adcIRHalfDone){//готова первая половина буфера
adcIRHalfDone = 0;
for (int i = 0; i < SIZE_DMABUF / 2; i += 2) {
uint16_t val_input = adcDMAbuf[i];
uint16_t val_vref = adcDMAbuf[i + 1];
if (val_input > 750 && val_input < 2800) {
sum_input += val_input;
sum_vrefint += val_vref;
count++;
}
}
}
if(adcIRFullDone){//готова вторая половина буфера
adcIRFullDone = 0;
for (int i = SIZE_DMABUF / 2; i < SIZE_DMABUF; i += 2) {
uint16_t val_input = adcDMAbuf[i];
uint16_t val_vref = adcDMAbuf[i + 1];
if (val_input > 750 && val_input < 2800) {
sum_input += val_input;
sum_vrefint += val_vref;
count++;
}
}
}
if (count > 0) {
avg_input = sum_input / count; //uint16_t
avg_vref = sum_vrefint / count;
adc_calcVoltage(avg_input, avg_vref);
}
if(gl_voltage && gl_voltage<=critical_stress && HAL_GetTick()>2000){
ret=FORCE_POWER_OFF;
}
return ret;
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if(hadc->Instance == ADC1){
adcIRFullDone=1;
}
}
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
if(hadc->Instance == ADC1){
adcIRHalfDone=1;
}
}
void adc_init(void)
{
adcIRFullDone = 0;
adcIRHalfDone = 0;
adcDMAbuf[0] = 0;
#if defined(S_T_M_32F1XX)
HAL_ADCEx_Calibration_Start(&hadc1);
#endif
}
void adc_start(void)
{
adcDMAbuf[0] = 0;
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adcDMAbuf, SIZE_DMABUF);
}
void adc_stop()
{
HAL_ADC_Stop_DMA(&hadc1);
adcIRFullDone = 0;
adcIRHalfDone = 0;
}
void adc_calcVoltage(uint16_t avg_input,uint16_t avg_vref)//переводит значение АЦП в напряжение
{
const float R1 = 221000.0f;
const float R2 = 27000.0f;
// считаем VDDA через Vrefint
float vdda = VREFINT_TYP * 4095.0f / (float)avg_vref;
// переводим значение канала в напряжение
float vadc = ((float)avg_input / 4095.0f) * vdda;
// напряжение на входе через делитель
vin = vadc * (R1 + R2) / R2 - 0.5f;
gl_voltage = vin;
// вывод значения для отладки
vadc_ = vadc;
}
float adc_GetVoltage(void)
{
return gl_voltage;
}
Пояснение к функции adc_calcVoltage()
Расчет VDDA, где:
VREFINT_TYP - калибровочное значение 1.20, взято из datasheet, у других МК на заводе производитель прошивает калибровочное значение в ПЗУ при изготовлении, пример получения значения с МК STM32F030CCTx [0x1FFFF7BA];
4095 - когда измеряется источник встроенным АЦП, результат выражается в единицах квантования, максимум которых равен 4095, поэтому в формуле используется множитель 4095 - это нормализация значения, чтобы связать измеренный АЦП с напряжением VDDA.
ADC_vref - значение встроенного источника опорного напряжения

Перевод значения канала в напряжение
ADC_in - напряжение измеренное с делителя напряжения (узел[5])

Коррекция через делитель напряжения
Вход подключен через делитель R1=221кОм и R2 = 27кОм, переводим в Ом,
добавил смещение (-0.5) для подстройки измерений.

Таблица замеров напряжения от 14.5 вольт до 7 вольт.
Напряжение от стационарного источника питания |
Рассчитанное напряжение через АЦП МК |
14,5 |
14.5474768 |
14 |
14.1426601 |
13,5 |
13.5746422 |
13 |
13.0305777 |
12,5 |
12.5156803 |
12 |
12.0447893 |
11,5 |
11.5500879 |
11 |
11.0325108 |
10,5 |
10.5149326 |
10 |
10.0049877 |
9,5 |
9.49503613 |
9 |
8.97779942 |
8,5 |
8.46920681 |
8 |
7.95231247 |
7,5 |
7.442698 |
7 |
6.93308401 |
Прикладываю видео-тестирования прохода по спаду напряжения, а также видео-тестирования, сброс напряжения и уход в сон микроконтроллера при низком напряжении ссылка [ https://t.me/ChipCraft В закрепленном сообщении [ #исскуствомк_тестирование_ Adc_VoltageControl]
Реализация модуля proj_main.c (Главный метод)
Данный модуль объединяет несколько подсистем:
Кнопка управления (одиночное и двойное нажатие);
Контроль напряжения питания через АЦП;
Звуковая индикация состояния;
Автоматический переход в сон при низком напряжении.
proj_main.c
#include "./Project/shared.h"
#include "./Project/proj_main.h"
#include "./Project/BU/ADC_Calc.h"
#include "./Project/BU/keys.h"
#include "main.h"
char gl_main_stateKey = 0; //отработали ли код по нажатию на кнопку
char count = 0;//используется для того чтобы пъезо не включался на повторное нажатие кнопки
// Глобальный флаг, установленный при прерывании
volatile uint8_t button_pressed = 0;
float adc_GetVoltage_ = 0;
typedef enum {
BUZZER_NONE = 0,
BUZZER_BEEP_1,
BUZZER_BEEP_2,
BUZZER_BEEP_3,
BUZZER_BEEP_4,
} buzzer_state_t;
volatile buzzer_state_t buzzer_state = BUZZER_NONE;
volatile uint32_t buzzer_timer = 0;
volatile uint8_t test_stop = 0;
volatile uint8_t test_stop_1 = 0;
uint8_t adc_ret=0;
#define START_DELAY 250
//для двойного нажатия
uint32_t btnPress_time_start = 0;//время, когда нажали кнопку
#define MIN_DelayDblClck 100 //100 было -50
#define MAX_DelayDblClck 600 //600 было-700
//E N D для двойного нажатия
//переменные для обработки логики при низком напряжении
uint32_t lowVoltageStart = 0;//время начала низкого уровня напряжения
uint8_t lowVoltageActive = 0;//флаг что система в режиме отсчета
#define SHUTDOWN_DELAY 15000
//E N D переменные для обработки логики при низком напряжении
//для звуковой индикации в режиме низкого напр.
uint32_t lowVoltageBeepTimer = 0; // время последнего писка
#define LOW_VOLTAGE_BEEP_PERIOD 2000 // каждые 2 секунды
//E N D для звуковой индикации в режиме низкого напр.
void SystemClock_Config(void);
void shutdownAndSleep();
void proj_main()
{
volatile const char *ch = ";V-F-BIN;ver: "VER_PROG(VER_a,VER_b,VER_c);(void)ch;//0x8008b00
unsigned short keysState=0;//состояние кнопки
HAL_Delay(1);//чтобы HAL_GetTick() не выдавал ноль
while (HAL_GetTick()<START_DELAY){;}//задержка, иначе иногда сразу уходит в сон
keysState=getKeyPinState_AtNow(); //
if((keysState & KEY1_Drv)==0){//защита от случайного нажатия
shutdownAndSleep();
}
uint32_t ms = 0;
ms = HAL_GetTick();
adc_init();
adc_start();
while (1){
//хэндлеры
keysDrv_Handler();//работа с кнопкой
// Работа с ADC
adc_ret = ADC_Calc_Handler();
// Работа со звуковой индикацией
buzzer_handler();
//E N D хэндлеры
ms = HAL_GetTick();
keysState=getKeyState();//получаю состояние кнопки
adc_GetVoltage_ = adc_GetVoltage();//получение измер.напряж.
// --- Проверка напряжения ---
if(adc_ret == FORCE_POWER_OFF) {
if(!lowVoltageActive) {
lowVoltageActive = 1;
lowVoltageStart = HAL_GetTick();
//внести обработку пищалки.
// первый писк сразу
buzzer_tripleBeep();
lowVoltageBeepTimer = HAL_GetTick();
}
// проверка тайм-аута //
if(lowVoltageActive && (HAL_GetTick() - lowVoltageStart >= SHUTDOWN_DELAY)) {
adc_ret = 0;
count = 0;
shutdownAndSleep();
}
// периодическая звуковая индикация
if (HAL_GetTick() - lowVoltageBeepTimer >= LOW_VOLTAGE_BEEP_PERIOD) {
buzzer_tripleBeep();
lowVoltageBeepTimer = HAL_GetTick();
}
if(adc_GetVoltage_ > 9.0f ){
// напряжение восстановилось → сброс
lowVoltageStart = 0;
lowVoltageActive = 0;
lowVoltageBeepTimer = 0;
buzzer_state = BUZZER_NONE;
buzzer_off();
}
}
if(keysState & KEY1Release_Drv){//если произошло короткое нажатие включение системы
//button_pressed =0;
if(test_stop == 1){
test_stop = 0;
adc_init();
adc_start();
}
if(gl_main_stateKey == 0){
gl_main_stateKey=1;
count ++;
if(count <= 1){
//запуск двойной звуковой индикации
buzzer_doubleBeep();
}
// — устанавливает пин в ЕДИНИЦУ, включение 3.3в и 12в
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_SET);
//действие для двойного нажатия кнопки
if(btnPress_time_start && (ms - btnPress_time_start) > MIN_DelayDblClck){
if(btnPress_time_start && (ms - btnPress_time_start) < MAX_DelayDblClck){
count = 0;
shutdownAndSleep();
}
}
btnPress_time_start=ms;
}
}
else{
if(gl_main_stateKey==1){
gl_main_stateKey=0;
}
}
//ss
}//while (1)
}
//Включение пъезо_излучателя
void buzzer_on()
{
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);
}
//Выключение пъезо_излучателя
void buzzer_off()
{
HAL_TIM_PWM_Stop(&htim1, TIM_CHANNEL_4);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_RESET);
}
// E N D
void buzzer_doubleBeep(void) {
buzzer_state = BUZZER_BEEP_1;
buzzer_timer = HAL_GetTick();
buzzer_on();
}
// Запуск тройного писка
void buzzer_tripleBeep(void) {
buzzer_state = BUZZER_BEEP_1;
buzzer_timer = HAL_GetTick();
buzzer_on();
}
void buzzer_handler(void) {
switch (buzzer_state) {
case BUZZER_NONE:
break;
case BUZZER_BEEP_1:
if (HAL_GetTick() - buzzer_timer > 100) {
buzzer_off();
buzzer_timer = HAL_GetTick();
buzzer_state = BUZZER_BEEP_2;
}
break;
case BUZZER_BEEP_2:
if (HAL_GetTick() - buzzer_timer > 100) {
buzzer_on();
buzzer_timer = HAL_GetTick();
buzzer_state = BUZZER_BEEP_3;
}
break;
case BUZZER_BEEP_3:
if (HAL_GetTick() - buzzer_timer > 100) {
buzzer_off();
buzzer_state = BUZZER_NONE;
}
break;
case BUZZER_BEEP_4:
if (HAL_GetTick() - buzzer_timer > 100) {
buzzer_on();
buzzer_timer = HAL_GetTick();
buzzer_state = BUZZER_NONE; // завершаем на третьем писке
}
break;
}
}
void shutdownAndSleep(){
adc_stop();
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_RESET);
test_stop =1;
//Сброс флага пробуждения
//__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_11);
__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
//Остановить SysTick, чтобы он не будил
HAL_SuspendTick();
//Переход в режим STOP
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
/* Configure the system clock */
//Восстановить тактирование после пробуждения
SystemClock_Config();
//Включить обратно
HAL_ResumeTick();
}
// Обработчик прерывания от кнопки (PB11 → EXTI11)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_11)
{
button_pressed = 1;
}
}
Вывод
Данная система контроля над блоком питания реализует:
Обработку кнопок с фильтрацией дребезга, с поддержкой короткого/долгого/двойного нажатия и отпускания;
Мониторинг напряжения через АЦП с защитой от просадок и автоматическим отключением при критическом низком уровне;
Звуковая индикация (короткие, двойные сигналы) для информирования пользователя о событиях;
Энергосбережение переход в режим STOP и пробуждение по прерыванию.
Систему можно использовать как основу для портативных приборов, автономных устройств и встраиваемых систем, где важно одновременно удобное управление и защита электроники.
Если статья показалась Вам интересной, буду рад выпустить для Вас еще множество статей исследований по всевозможным видам устройств, так что, если не хотите их пропустить – буду благодарен за подписку на мой ТГ-канал: https://t.me/ChipCraft.
Комментарии (7)
0xdead926e
25.08.2025 10:59а не лучше бы было поставить какую-нибудь микруху fuel gauge, чтоб хотя бы знать уровень заряда аккума/сколько времени осталось от него работать/вот это все?
DM_ChipCraft Автор
25.08.2025 10:59Вполне хорошее предложение по поводу микросхемы, но по поводу вывода информации сколько осталось...да это здорово, но смысла нету к сожалению в плане для моей системы, внимания обращать не будут, есть конечно сознательные ребята (которые стараются не разряжать в хлам блок питания), но таких единицы :) в основном внимания не обращают и продолжают дальше работать, а еще лучше когда разряжается блок, многие берут подключают систему вместе с блоком питания на зарядку, от того же самого авто и продолжат работы, а потом при каком нибудь скачке тока запредельном, происходит отказ вообще всей системы :) , поэтому в схеме и участвуют предохранители, и не только в этом узле но еще и в других узлах тоже.
5erG0
25.08.2025 10:59Почему не литий?
Поправьте описание узла[3]. Но схеме в нет транзистора, который усиливает сигнал для МК.
DM_ChipCraft Автор
25.08.2025 10:59Добрый день, большое спасибо за обнаружение ошибки, к сожалению смотрел на другую схему, когда описание составлял , попутал, там усиления никакого не нужно и соответственно транзистора тоже.
(почему не литий?)-Конкретно по моей позиции у меня используется такой тип аккумуляторов, это из разряда (что закупают с тем и работаем:), посмотрю есть ли литиевые аккумы, подключу к системе и дам знать тогда как она взаимодействует с литиевыми аккумами:)
Спасибо еще раз, за обнаружение ошибки.
ruomserg
Вы серьезно ? DMA для контроля напряжения аккумулятора ?! А компаратор поставить ? А почему в спящий режим только при снижении напряжения ниже предела ? А просто просыпаться раз в минуту на сотню-другую тактов для замера напряжения ? А поставить датчик активности (ток, свет, движение - я не знаю что у вас там за устройство) - и автоматически выключать источник если напряжение низкое, а активности нет ?
В целом, устройство с учетом заявленных функций реализовано максимально странно. :-(
DM_ChipCraft Автор
Для моей задачи DMA, вполне подходит, по поводу вход в спящий режим при снижении активности, это как один из вариантов, по мимо этого в моей системе есть еще графическое отображение индикации, так же есть анализ активности пользователя по в приложении(к примеру : нажимает на панель и производит настройку - значит активность есть), там уже активность я анализирую по uart пакетам, а выход из сна через какое - то время , это я уже проходил, все равно не помогает, некоторые пользователи все равно забывают выключать.
Естественно я не претендую на лучший метод анализа напряжения:)
Большое Вам спасибо за предложения и за комментарий:)