Всё как в предыдуцщей разработке, только в качестве вычислителя использую микроконтроллер ф.WCH CH32V003A4M6. Преимущество тут в размерах, вместо Arduino Nano маленькая микросхемка SOP-16 – очень удобно для создания законченной конструкции маленького приборчика.
Добавлен режим счётчика. Теперь есть возможность просто считать импульсы, при этом функция делителя сохранена, устанавливается как и в предыдущей версии через кнопку SB1 (Установка). Режим счётчика бывает нужен, к примеру, при подсчёте витков катушки индуктивности в момент её намотки или что-то подобное.
2026-06-19
Схема
Датчик Холла используем такой же: A3144E, подключаем его к выводу 16 (PC0), а до этого в setup() назначаем прерывание на этот вывод.
Информацию выводим на тот же OLED SSD1306 128*64 I2C. Управление, как в предыдущем проекте, через кнопку SB1. Кнопка настройки на выводе 13 (PA2), что удобно для конструкции.
Pic 1. Схема электрическая принципиальная
Добавлен режим счётчика. Для этого пришлось добавить кнопку переключения режимов SB2: [Тахометр] или [Счётчик]. При длительном нажатии на кнопку SB2 происходит обнуление счётчика.
Pic 2. Схема электрическая принципиальная v1.4
Описание программы
Программа, как обычно, хорошо закомментирована, пояснять там вроде нечего. Однако в ядре WCH для Arduino IDE присутствует ошибка и поэтому вывод на дисплей OLED SSD1306 128*64 I2C не выходит. Пришлось устранять эту ошибку. Исправление ядра в Приложении в библиотеках SPI и Wire. Эти библиотеки скопировать в ядро WCH.
/***********************************************************
2025-10-18 Mr.ALB CH32V003F4P6 - блинк для проверки
2026-04-25 Mr.ALB CH32V003F4P6 - проверка внешнего прерывания
2026-04-26 Mr.ALB Подключение SSD1306...
2026-04-26 Mr.ALB ...Подключение SSD1306 - Успешно!
2026-04-27 Mr.ALB на CH32V003A4M6
2026-05-08 Mr.ALB Оптимизация
2026-06-19 Mr.ALB Добавлен режим счётчика
Кнопка1 - Удержание показания/Настройка делителя
Кнопка2 - переключение режимов: Тахометр/Счётчик
==================================================================================
WCH CH32V003A4M6
-----------------
+-\/-+
T1BKIN_/T2CH4_T2CH1ETR_/URX_/NSS/SDA/PC1 1| |16 PC0/T2CH3 NSS_/UTX_/T1CH3_
AETR_/T2CH2_/T1ETR_/URTS/T1BKIN/SCL/PC2 2| |15 VDD|VCC +3.3V
T1CH1N_/UCTS_/T1CH3/PC3 3| |14 VSS|GND
T1CH1CH2N_(2)/T1CH4/MCO/A2/PC4 4| |13 PA2/TICH2N/A0/OPP0/OSCO/AETR2_
T1CH1CH3N_(3)UCTS_/SDA_/MOSI/PC6 5| |12 PA1/T1CH2/A1/OPN0/OSCI
T1CH2_/URTS_/T2CH2_/MISO/PC7 6| |11 PD7/NRST/T2CH4/OPP1/UCK_
ПРОГ. - SWIO/T1CH3N/AETR2/SCL_/URX_/PD1 7| |10 PD6/URX/A6/T2CH3_/UTX_
UCK/C4ETR/OPA/C1ETR/A7/PD4 8|____|9 PD5/UTX/A5/T2CH4_/URX_
I2C A4M6 OLED SSD1306
--- --------- ------------
pin14 GND - GND
pin15 VCC - VCC
SCL - pin2 PC2/SCL - SCK
SDA - pin1 PC1/SDA - SDA
SENSOR(A3144E) A4M6
1 VCC - pin15 VCC/VDD
2 GND - pin14 GND/VSS
3 OUT - pin16 PC0
A4M6 Кнопка
---- ------
PA2 1 - OUT
GND 2 - GND
A4M6 Кнопка Тахометр/Счётчик
---- -----------------------
PA1 1 - OUT
GND 2 - GND
===============================================================
НАСТРОЙКА среды IDE
-------------------
Clock select: 48MHz Internal
Optimize: Smallest (-os) with LTO
C runtime Library: Newlib Nano (default)
с SERIAL_ON:
Скетч использует 15124 байт (92%) памяти устройства.
без SERIAL_ON:
Скетч использует 13928 байт (85%) памяти устройства.
Всего доступно 16384 байт.
**********************************************************************/#include<EEPROM.h>#include<Wire.h>#include"SSD1306Ascii.h"#include"SSD1306AsciiWire.h"//#define SERIAL_ON // Включение вывода сообщений в порт//=============ДИСПЛЕЙ SSD1306 Oled======================#define I2C_ADDRESS 0x3C // Адрес дисплея#define I2C_ADDRESS 0x3C
// Define proper RST_PIN if required.#define RST_PIN -1
SSD1306AsciiWire oled;
constuint8_t SCREEN_W = 128,// Ширина экрана в пикселях
SCREEN_H = 64,// Высота экрана в пикселях
h = 8,// Высота буквы size 1
w = 6; // Ширина буквы size 1//========================================================// Массив для заставкиconstchar* stitle[] =
{
"RPM/CNT meter",// 0"v1.4",// 1"Mr.ALB",// 2"2026-06-19",// 3
};
#define MAXN 30 // Максимальное число импульсов на оборот#define SENSOR PC0 // Выход IR модуля, или выход А3144#define BTN PA2 // Кнопка настройки#define BTN2 PA1 // Кнопка переключения режимов:Тахометр/Счётчик#define Z_PAUZA 3000 // Пауза после вывода заставки#define BTNPRES 3000 // Время удержания кнопки настройки#define TMDEBOUNCE 100 // Время антидребезгаint address = 16; // Адрес где хранится делитель nuint16_t rpm = 0,// Переменная оборотов
rpm_mem = 0; // Значение при удержанииbyte n = 1; // Количество импульсов датчика на 1 оборотbool flag_set =false,
btn_flag =false,// Флаг нажатия кнопки
flag_hold =false,// Флаг остановки измерения
flag_prn_hold =false,// Флаг вывода надписи Hold
flag_counter =false; // Режим счётчикаuint32_t tmr_prev,// Переменная для отслеживания секунды
btn_press,// Переменная времени нажатия кнопки
tmr_debounce; // Переменная антидребезгаvolatileuint16_t nn; // Переменная числа входных импульсов/*****************************
***** Функция настройки *****
*****************************/voidsetup()
{
#ifdef SERIAL_ON
Serial.begin(9600);
delay(300);
#endifpinMode(SENSOR,INPUT_PULLUP);
pinMode(BTN,INPUT_PULLUP);
pinMode(BTN2,INPUT_PULLUP); // Кнопка Тахометр/Счётчик/Обнуление// Инициализация дисплея
Wire.begin();
Wire.setClock(400000L);
#if RST_PIN >= 0
oled.begin(&Adafruit128x64, I2C_ADDRESS, RST_PIN);
#else// RST_PIN < 0
oled.begin(&Adafruit128x64, I2C_ADDRESS);
#endif
oled.setContrast(255); // Яркость дисплея
oled.setFont(Adafruit5x7);
fnZastavka();
// Подключаем прерывание:
// myISR — функция-обработчик
// FALLING — срабатывание по спадающему фронтуattachInterrupt(digitalPinToInterrupt(SENSOR), myISR,FALLING);
// initialize EEPROM objectEEPROM.begin();
// Чтение настройки делителя из EEPRON
n =EEPROM.read(address);
if (n > MAXN)n = 1;
btn_flag =false;
}
// end of setup()
/*****************************
***** Основная функция *****
*****************************/voidloop()
{
btn_press =millis();
// Если нажата кнопка режимовif (!digitalRead(BTN2) && btn_flag ==false)
{
btn_flag =true;
fnDebounce(TMDEBOUNCE); // Антидребезгwhile (!digitalRead(BTN2)); // Проверка удержания кнопки// Если кнопка нажата BTNPRES миллисекif ((millis() - btn_press) > BTNPRES)
nn = 0; // Обнуляем счётчикelse
flag_counter =!flag_counter; // Переключение режима работы
}
// Если нажата кнопка настройкиif (!digitalRead(BTN) && btn_flag ==false)
{
btn_flag =true;
while (!digitalRead(BTN)); // Проверка удержания кнопки#ifdef SERIAL_ON
Serial.print(F("Время удержания кнопки: "));
Serial.println(millis() - btn_press);
#endif// Если кнопка нажата BTNPRES миллисекif ((millis() - btn_press) > BTNPRES)
{
// Режим настройки#ifdef SERIAL_ON
Serial.println(F("Режим настройки"));
#endif
oled.invertDisplay(true); // Инверсия экрана
n = 1; // Установка начального значения
fnPrintN(); // Вывод делителя
btn_press =millis();
do// Настройка делителя
{
if (!digitalRead(BTN) && btn_flag ==false)
{
btn_flag =true; // Кнопка нажата
n++; // Новое значение кол-во импульсов на оборотif (n > 30)n = 1; // Ограничение значения
fnPrintN();
#ifdef SERIAL_ON
Serial.print(F("n = "));
Serial.println(n);
#endif
btn_press =millis();
}
// Сброс флага нажатия кнопкиif (digitalRead(BTN) && btn_flag ==true)
{
btn_flag =false;
fnDebounce(TMDEBOUNCE); // Антидребезг
}
}
while (millis() - btn_press < BTNPRES);
// Проверка значения делителя в EEPROM
// Если там другое значение, то обновляемif (EEPROM.read(address) != n)
{
#ifdef SERIAL_ON
Serial.println(F("Запись в EEPROM нового делителя..."));
#endifEEPROM.write(address, n); // Записываем в EEPROMEEPROM.commit(); // Сохранение EEPROM
}
#ifdef SERIAL_ON
Serial.print(F("В EEPROM n="));
Serial.println(EEPROM.read(address));
#endif
}
else// Кнопка нажата быстро
{
flag_hold =!flag_hold;
if (flag_hold)
{
rpm_mem = rpm; // Запомним текущее значение rpm#ifdef SERIAL_ON
Serial.println(F("Режим: Hold"));
Serial.print(F("flag_hold="));
Serial.println(flag_hold);
#endif
}
else
{
rpm = rpm_mem; // Вспомним значение rpm
nn = rpm * n; // Выставим значение импульсовif (!flag_counter) nn /= 60;
#ifdef SERIAL_ON
Serial.println(F("Режим: Измерение"));
Serial.print(F("flag_hold="));
Serial.println(flag_hold);
#endif
}
fnPrintN();
}
btn_press =millis();
oled.invertDisplay(false);
}
/*************************
Вычисление и индикация
*************************/if (flag_counter) // Режим счётчика
{
if (!flag_hold)
rpm = nn / n; // Число оборотов
}
else// Режим тахометра// Проверка времени 1 секундаif (millis() - tmr_prev > 1000)
{
#ifdef SERIAL_ON
Serial.print(F("nn="));
Serial.print(nn);
Serial.print(F("\tn="));
Serial.print(n);
#endifif (!flag_hold)
rpm = (nn / n) * 60; // Обороты в минуту
nn = 0; // Обнуляем счётчик
tmr_prev =millis(); // Обнуляем время#ifdef SERIAL_ON
Serial.print(F("\trpm="));
Serial.println(rpm);
#endif
}
fnPrintOLED(); // Вывод информации// Сброс флага нажатия кнопкиif ((digitalRead(BTN) ||digitalRead(BTN2)) && btn_flag ==true)
btn_flag =false;
delay(50);
}
// end of loop()
/************************************
Функция: Заставка
***********************************/void fnZastavka()
{
oled.clear();
oled.setScale(1);
byte j = 0;
for (byte i = 0; i < (sizeof(stitle) /sizeof(stitle[0])); i++)
{
oled.setCursor((SCREEN_W - w *strlen(stitle[i])) / 2, j);
j += 2;
oled.print(stitle[i]);
}
delay(Z_PAUZA); // Пауза
oled.clear(); // Очистить дисплей
}
// end of fnZastavka()
/*************************************
Функция: Вывод на дисплей
************************************/void fnPrintOLED()
{
//oled.set1X();
oled.setScale(1);
oled.home();
oled.print(stitle[0]);
oled.print(" ");
oled.print(stitle[1]);
//oled.set2X();
oled.setScale(2);
oled.setCursor(8, 3);
if (flag_counter)
oled.print(F("CNT:")); // Счётчикelse
oled.print(F("RPM:")); // Тахометр
oled.setCursor(56, 3);
oled.print(rpm); // Вывод оборотовif (rpm == 0)oled.print(F(" "));
else
{
if (rpm < 100)oled.print(F(" "));
if (rpm < 1000 && rpm >= 100)oled.print(F(" "));
else oled.print(F(" "));
}
// Вывод делителя
fnPrintN();
}
// end of fnPrintOLED()
/************************************
Функция: Вывод на экран делителя
***********************************/void fnPrintN()
{
oled.setScale(1);
oled.setCursor(8, 7);
oled.print(F("N:"));
oled.print(n); // Количество импульсов на 1 оборот
oled.print(F(" "));
if (flag_hold && flag_prn_hold ==false)
{
oled.setCursor(45, 7);
oled.print(F("Hold"));
flag_prn_hold =true;
#ifdef SERIAL_ON
Serial.println(F("flag_prn_hold = true"));
#endif
}
else
{
if (flag_prn_hold &&!flag_hold)
{
oled.setCursor(45, 7);
oled.print(F(" "));
flag_prn_hold =false;
#ifdef SERIAL_ON
Serial.println(F("flag_prn_hold = false"));
#endif
}
}
}
// end of fnPrintN()
/**************************************
Функция: Обработка прерывания
*************************************/void myISR()
{
nn++;
}
// end of count()
/********************************
Функция: Антидребезг кнопки
*******************************/void fnDebounce(uint16_t t)
{
// Антидребезг
tmr_debounce =millis();
while ((millis() - tmr_debounce) < t);
}
// end of fnDebounce()
Конструкция v1.2
Для реализации устройства использую кусочек макетной односторонней платы размером 50*25мм. Дисплей, кнопка, заъём для датчика, выключатель и плата-адаптер для микроконтроллера.
Pic 3. Конструкция. Разные виды
Всё смонтировано на плату. Для подключения дисплея использую панельку, можно легко снять дисплей, для доступа к микроконтроллеру. Для программирования использую контакты (SWDIO, VCC, GND) на плате слева от кнопки.
Pic 4. Конструкция. Разные виды
Вид на плату с нижней стороны. Монтаж объёмными проводками. Это устройство единичное, нет смысла делать печатную плату.
Pic 5. Конструкция. Разные виды
На плату установлен дисплей, подключен датчик и проводки питания для проверки работоспособности смонтированной схемы.
Pic 6. Конструкция. Разные виды
Плата вставлена в коробок, склееный из пластика ABS (традиция). Размеры корпуса 56,5*38,5*30 мм.
Включено питание. В начальный момент на экран выводится название, версия, автор, дата прошивки.
Pic 7. Конструкция. Разные виды
Далее на экран выводится название и версия. Частота оборотов и делитель. В данный момент сигнала с датчика нет.
Pic 8. Конструкция. Разные виды
С датчика поступают импульсы и на экране выводится частота оборотов в RPM(об/мин).
Pic 9. Конструкция. Разные виды
Устройство питается от аккумулятора Li-Ion 600 мАч. Для его подзарядки выведен разъём платы заряда с левой стороны корпуса.
Pic 10. Конструкция. Разные виды
С правой стороны корпуса выведен разъём для подключения датчика A3144E.
Pic 11. Конструкция. Разные виды
Снизу корпуса выведено индикаторное стекло. При зарядке аккумулятора, свет индикатора красный, при заряженном аккумуляторе – зелёный.
Pic 12. Конструкция. Разные виды
Для проверки работоспособности используется игрушечный мотор Минато v7.3.
Pic 13. Конструкция. Разные виды
Конструкция v1.4
Доработка – добавлена кнопка выбора режима работы: Тахометр / Счётчик. Как раз справа от первой кнопки было свободное место. Скетч промежуточной версии v1.3
Pic 14. Конструкция. Разные виды
Версия программы v1.4, в которой устранены неточности переходной версии. Плата тахометра установлена в корпус. На дисплее видно, что выбран режим счётчика.
Pic 15. Конструкция. Разные виды
На дисплее: выбран режим тахометра.
Pic 16. Конструкция. Разные виды
Сравнительные размеры приборчика со стандартным коробком спичек.
Pic 17. Конструкция. Разные видыPic 18. Конструкция. Разные виды
Удачно всё получилось: и программа, и конструкция. Сам приборчик совсем маленький. Если бы делать на Arduino Nano или Arduino Pro mini, то таких размеров не получить. Микроконтроллер CH32V003A4M6 имеет флэш 16384 байт, а вот его собрату CH32V006F8P6 доступно 63488 байт, что позволяет уже делать более продвинутые устройства с большими возможностями.
Приложение
Все, что необходимо для повторения конструкции, можно скачать по ссылкам: