Привет, Хабр!

Автономные системы становятся все более востребованными - от портативных приборов до сложных промышленных комплексов - надежное управление напряжением питания превращается в ключевой фактор их долговечности и эффективности. Сердце любой такой системы - аккумулятор, а его безопасность и срок службы, напрямую зависит от контроля напряжения.

В данной статье будет представлен пример контроля напряжения, над блоком питания - внутри которого (никель-металлгидридная аккумуляторная сборка 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)


  1. ruomserg
    25.08.2025 10:59

    Вы серьезно ? DMA для контроля напряжения аккумулятора ?! А компаратор поставить ? А почему в спящий режим только при снижении напряжения ниже предела ? А просто просыпаться раз в минуту на сотню-другую тактов для замера напряжения ? А поставить датчик активности (ток, свет, движение - я не знаю что у вас там за устройство) - и автоматически выключать источник если напряжение низкое, а активности нет ?

    В целом, устройство с учетом заявленных функций реализовано максимально странно. :-(


    1. DM_ChipCraft Автор
      25.08.2025 10:59

      Для моей задачи DMA, вполне подходит, по поводу вход в спящий режим при снижении активности, это как один из вариантов, по мимо этого в моей системе есть еще графическое отображение индикации, так же есть анализ активности пользователя по в приложении(к примеру : нажимает на панель и производит настройку - значит активность есть), там уже активность я анализирую по uart пакетам, а выход из сна через какое - то время , это я уже проходил, все равно не помогает, некоторые пользователи все равно забывают выключать.

      Естественно я не претендую на лучший метод анализа напряжения:)

      Большое Вам спасибо за предложения и за комментарий:)


  1. 0xdead926e
    25.08.2025 10:59

    а не лучше бы было поставить какую-нибудь микруху fuel gauge, чтоб хотя бы знать уровень заряда аккума/сколько времени осталось от него работать/вот это все?


    1. DM_ChipCraft Автор
      25.08.2025 10:59

      Вполне хорошее предложение по поводу микросхемы, но по поводу вывода информации сколько осталось...да это здорово, но смысла нету к сожалению в плане для моей системы, внимания обращать не будут, есть конечно сознательные ребята (которые стараются не разряжать в хлам блок питания), но таких единицы :) в основном внимания не обращают и продолжают дальше работать, а еще лучше когда разряжается блок, многие берут подключают систему вместе с блоком питания на зарядку, от того же самого авто и продолжат работы, а потом при каком нибудь скачке тока запредельном, происходит отказ вообще всей системы :) , поэтому в схеме и участвуют предохранители, и не только в этом узле но еще и в других узлах тоже.


  1. Luboff_sky
    25.08.2025 10:59

    NiMH??? Всё ещё?


  1. 5erG0
    25.08.2025 10:59

    Почему не литий?

    Поправьте описание узла[3]. Но схеме в нет транзистора, который усиливает сигнал для МК.


    1. DM_ChipCraft Автор
      25.08.2025 10:59

      Добрый день, большое спасибо за обнаружение ошибки, к сожалению смотрел на другую схему, когда описание составлял , попутал, там усиления никакого не нужно и соответственно транзистора тоже.

      (почему не литий?)-Конкретно по моей позиции у меня используется такой тип аккумуляторов, это из разряда (что закупают с тем и работаем:), посмотрю есть ли литиевые аккумы, подключу к системе и дам знать тогда как она взаимодействует с литиевыми аккумами:)

      Спасибо еще раз, за обнаружение ошибки.