
Привет, Хабр! Несколько лет назад у меня был самодельный 48-вольтовый электромопед на свинцово-кислотных аккумуляторах, переделанный из Риги-12.
Для него был разработан специальный спидометр, смонтированный вместо крышки бензобака, переделанного в отсек для электроники. Во время движения прибор показывал скорость, а на остановке — пройденный путь.
На примере этой несложной поделки я расскажу и покажу вам последовательность разработки и воплощения любительского микроконтроллерного устройства.
История данного проекта начинается с ошибочного приобретения десяти микроконтроллеров PIC16F628A. Восемнадцатиногие, компактные (в отличие классических «сороконожек» PIC16F877A), они продавались по очень низкой цене и были куплены не глядя.
Мне хотелось собрать множество разных интересных устройств с аналого-цифровыми преобразователями. Однако именно последних, как выяснилось при чтении документации, у этих микроконтроллеров не имеется.

Поэтому чудесные микросхемы лежали в антистатическом пакетике и ждали своего часа, который однажды всё-таки наступил.
▍ Бедный старый мопед
Самая тяжёлая часть любого электробайка — это его аккумуляторы, особенно если они свинцово-кислотные. По сравнению с литиевыми, последние имеют малую ёмкость, но вместе с ней и гораздо более низкую стоимость. А ещё, они не горят и не взрываются.
Конечно же, можно ухитриться поджечь водород, выделяющийся при зарядке свинцового аккумулятора, и в гремучей смеси с кислородом произойдёт взрыв. Но даже это далеко не так опасно, как всепожирающий горящий литий.
Чтобы обеспечить устойчивость верного стального коня (или ослика, если говорить о маленьком двухскоростном мопеде), самые тяжёлые составляющие должны быть закреплены как можно ниже.
Итак, аккумуляторы в изготовленном из канистры алюминиевом кофре заняли место двигателя внутреннего сгорания, а бензобак превратился в корпус, где была установлена вся электроника.

Можете писать гневные комментарии об уничтожении памятника технической старины, и будете правы. В идеальном мире старые вещи должны сберегаться, реставрироваться и экспонироваться в музеях, а также иных публичных пространствах.
Однако реалии моей жизни были следующими. Денег на Omaks V1 или что-то подобное у меня не было, а нетяжёлый электромопед был необходим для поездок не только на рыбалку и сбора грибов, но и в магазин за продуктами.
В наличии имелись Рига-12 с непригодным для эксплуатации двигателем, небольшая мастерская в пристройке частного дома, средства для приобретения минимального комплекта электрификации двухколёсного аппарата и желание осуществить довольно основательный проект.
В итоге получился вполне функциональный электромопед с полным комплектом световых приборов и даже с полноценным односкоростным педальным приводом. Он полностью соответствовал тогдашней редакции Правил дорожного движения, являясь велосипедом со вспомогательным электрическим двигателем, оборудованным для езды по дорогам общего пользования в любое время суток.
▍ Откуда берутся идеи?
Узнав о том, что я переделываю свой старый байк в электрический, знакомый выпросил у меня ставшую ненужной крышку бензобака с логотипом Рижского мотозавода Sarkanā zvaigzne (Красная звезда). Это довольно редкая деталь, которую ценят коллекционеры и просто люди, продолжающие ездить на старой мототехнике.
Лучшим решением было бы изготовить кофр для электрооборудования, повторяющий форму и габариты старого бензобака, а последний передать ценителям за вознаграждение.
Однако в моём распоряжении не было таких технологий, поэтому бензобак был распилен пополам, а горловину нужно было как-то загерметизировать, чтобы защитить от атмосферных осадков это страшное сооружение.

Сварочный аппарат у меня был, причём даже полуавтомат, но заваривать отверстие не хотелось, так как это привело бы к необходимости перекрашивать деталь.
На моё счастье, тяжкие раздумья были прерваны попавшимся на глаза сломанным манометром от шинного компрессора. Его пластиковое стёклышко было как новенькое, и посадочный диаметр последнего чудесным образом подходил к обрезанной горловине бензобака, если воспользоваться уплотнительным резиновым кольцом.

Так было решено разместить на месте горловины спидометр — круглую плату с цифровым светодиодным индикатором, микроконтроллером и всей необходимой обвязкой.
▍ Постановка технического задания
Спидометр должен показывать скорость в километрах в час, а одометр — пройденный путь в километрах.
Для этого вполне можно обойтись одним трёхразрядным семисегментным индикатором, если во время движения показывать скорость, а на остановке — пройденный путь.
Получается, что одометр будет считать от 0 до 999 километров, после чего сбрасываться на ноль. Для стального ослика с менее чем двадцатикилометровым запасом хода на электротяге этого более чем достаточно.
Значение пройденного пути необходимо хранить в энергонезависимой памяти, причём желательно, чтобы квант пути равнялся не одному километру, а меньшей величине. Иначе 999 метров будут «округляться» до нуля.
Спидометр должен питаться от двенадцативольтового преобразователя, используемого для питания стоп-сигнала, заднего габаритного огня и указателей поворота.
Звуковой сигнал на моём электромопеде был запитан от отдельного преобразователя, так как это крайне прожорливый прибор. А 10-ваттная светодиодная фара питалась от тяговой батареи посредством самодельного драйвера.
Безредукторный мотор прямого привода позволяет обойтись без специального датчика. Источником входного сигнала для спидометра служит напряжение на любой из трёх фаз синхронного электродвигателя.

Если электромопед движется, то даже при отключённом контроллере мотор-колеса на фазах присутствует переменное напряжение, генерируемое двигателем. Его частота пропорциональна скорости вращения ведущего (заднего) колеса.
▍ Премудрости математики
Каждый оборот колеса соответствует 24 импульсам. Длина окружности шины Риги-12 равна 166 сантиметрам. Получается, что в одном километре 14458 импульсов.
Это очень удачное число, поскольку в одном часе 14400 четвертей секунды. Оставим три значащих цифры, отбросим 58, и получаем, что один километр в час соответствует четырём импульсам в секунду. А четыре — это два в квадрате.
Далее, 226 * 64 = 14464, что ещё ближе к 14458. Это значит, что каждый шестьдесят четвёртый импульс должен запускать приращение кванта пройденного пути, а каждый двести двадцать шестой квант — обнулять счётчик квантов и увеличивать содержимое счётчика километров на единицу.
Получается квант пройденного пути, равный 4.42 метра — просто прекрасная разрешающая способность для электромопедного одометра. И 226 меньше чем 256, что позволяет обойтись одним байтом беззнакового целого числа.
Вы спросите, зачем вообще нужны эти странные цифры? Дело в том, что они позволяют обойтись без операций деления, которые крайне сложно реализовать на простом аскетичном микроконтроллере, если только это не деление на степень числа 2.
Исходя из вышенаписанного, у меня нет никаких сомнений, что алгоритм работы спидометра и одометра уместится в памяти программ микроконтроллера. И оперативной памяти тоже хватит. Поэтому можно смело приступать к следующему шагу.
▍ Разработка схемы устройства
Математические чудеса продолжаются. У многоразрядного семисегментного индикатора восемь сегментов. Восьмой — это десятичная точка, но спидометру она не требуется, и мы её не будем использовать.
Каждый выход порта микроконтроллера PIC16F628A имеет нагрузочную способность, достаточную для зажигания сегмента маленького светодиодного индикатора. Однако цифра 1 означает два светящихся сегмента, а цифра 8 — все семь.
Трёхразрядный индикатор с общим катодом скоммутирован так, что на восемь ножек выведены аноды сегментов, и на ещё три ножки — катоды разрядов.

Последние зажигаются при помощи простого транзисторного ключа — логического инвертора, собранного по схеме с общим эмиттером или общим истоком.
В ультрапопулярной микросхеме ULN2003A таких инвертирующих ключей целых семь. Они собраны по схеме Дарлингтона, предоставляющей огромный коэффициент усиления по току.

И это ключи с открытым коллектором, что означает возможность коммутировать каждым из них полуамперную нагрузку с напряжением до 50 вольт, при том, что ключами управляет микроконтроллер с пятивольтовым питанием.
Три ключа микросхемы потребуются нам для зажигания трёх разрядов индикатора. А оставшиеся четыре целесообразно использовать для стоп-сигнала и указателей поворота: не пропадать же добру!

Ключи ULN2003A можно соединять параллельно, что я и сделаю: пара на стоп-сигнал и пара на указатели поворота.
Теперь самое время рисовать принципиальную схему параллельно с платой. Последняя должна быть односторонней и иметь определённый диаметр.
Нужно постараться уместить всё необходимое — микроконтроллер, ULN2003A, индикатор, семь резисторов ограничения тока его сегментов, а также формирователь импульсов и блокировочный конденсатор питания.
Для зажигания сегментов и управления ключами мы можем использовать любые выводы микроконтроллера, на которых имеется двухтактный (не с открытым коллектором) выход.
Однако вывод RB0/INT необходимо зарезервировать для импульсов с мотор-колеса, поскольку мы будем использовать внешнее прерывание.

Формирователь импульсов включает в себя однополупериодный выпрямитель D2, стабилитрон D1, ограничивающий напряжение на входе микроконтроллера до 4.7 вольт, и токоограничительный резистор R2.
Резистор R1 подтягивает вход к земле, а фильтрующий конденсатор С1 срезает высокочастотные помехи, чтобы микроконтроллер регистрировал фазные импульсы адекватно.
Также весьма желательно, чтобы все семь сегментов зажигались выводами одного порта. Тогда мы сможем выводить цифру на индикатор простой записью числа в этот порт.
Порт B годится, так как вышеупомянутый вывод RB0 работает только на вход, и запись бита в нулевой разряд данного порта ни на что не влияет.

Выход RA0 будет программно мигать указателями поворота, а RA7 — программно инвертировать сигнал Brake_Low (замыкание микровыключателя в любой из двух ручек тормоза на массу). При этом два ключа ULN2003A шунтируют 100-омный резистор, через который питается 12-вольтовый светодиодный задний фонарь. Яркость его свечения возрастает. Получается стоп-сигнал.
Проседание напряжения на RA6 до уровня логического нуля означает, что противоугонный замок отключил питание управляющих схем. Запаса электроэнергии в конденсаторе C2 достаточно, чтобы записать новые значения пройденного пути в энергонезависимую память. Так мы экономим её ресурс, производя запись только в момент отключения.
Нарисовалась такая плата.

А так она выглядит в реальности.

Пятивольтовый стабилизатор, его внешний теплорассеивающий резистор и электролитический конденсатор не поместились на плату спидометра и размещены на плате с преобразователями напряжения.
▍ Пишем прошивку
Однажды мне довелось освоить не очень популярный, но вполне работоспособный инструмент разработки программного обеспечения для микроконтроллеров PIC — JAL v.2. Кстати, он поддерживается до сих пор.
Для начала, компилятор должен знать, с каким микроконтроллером имеет дело.
include 16f628a
Далее задаём желаемую тактовую частоту — 4 мегагерца. PIC18F628 выполняет одну команду за 4 такта. То есть, частота команд будет равна 1 мегагерцу.
pragma target clock 4_000_000
Нам вполне хватит точности встроенного RC-генератора, откалиброванного на заводе при изготовлении микроконтроллера. Выводить тактовые импульсы на RA6 не требуется.
pragma target OSC INTOSC_NOCLKOUT
Сторожевой таймер не нужен, так как в нашей программе нечему зависать. Зато нужно, чтобы микроконтроллер оставался в состоянии сброса после снижения питающего напряжения, когда разрядился электролитический конденсатор.
pragma target WDT DISABLED
pragma target BROWNOUT ENABLED
Низковольтное программирование и защиты от чтения исполняемого кода и содержимого EEPROM нам не требуются.
pragma target LVP DISABLED
pragma target CP DISABLED
pragma target CPD DISABLED
Задержка включения не помешает, а внутренний сброс — обязателен. Иначе программа не запустится, не получив внешнего сигнала.
pragma target PWRTE ENABLED
pragma target MCLR INTERNAL
▍ Алиасы
Алиасы — это удобные для восприятия имена, присваиваемые адресам в регистрах микроконтроллера.
alias blink is pin_A0 -- мигание поворотов
alias digit1 is pin_A1 -- первый разряд индикатора
alias digit2 is pin_A2 -- 2-й разряд индикатора
alias digit3 is pin_A3 -- 3-й разряд индикатора
alias brake_low is pin_A5 -- вход стоп-сигнала
alias pwr_on is pin_A6 -- сигнал отключения питания
alias brake_light is pin_A7 -- выход стоп-сигнала
▍ Константы
Теперь создадим массив с индексами от 0 до 9, определяющий сегменты, которые нужно зажечь для индикации соответствующей цифры. Это будут константы — значения, не изменяющиеся в процессе работы программы.
const byte segments_upside [10] = { -- если индикатор установлен десятичными точками вверх
-- -A-
-- F B
-- -G-
-- E C
-- -D-
--EDCGBFA- - соответствие битов сегментам
0b11101110, -- для цифры 0 зажечь все сегменты, кроме G
0b10000100, -- и так далее
0b11011010,
0b11010110,
0b10110100,
0b01110110,
0b01111110,
0b11000100,
0b11111110,
0b11110110
}
const byte segments_upright [10] = { -- если индикатор установлен нормально
0b11101110,
0b00101000,
0b11011010,
0b01111010,
0b00111100,
0b01110110,
0b11110110,
0b00101010,
0b11111110,
0b01111110
}
alias segments is segments_upright
▍ Переменные
Переменные — это по сути те же алиасы, указывающие на адреса в регистрах. Компилятор разместит их самостоятельно. Все переменные в программе спидометра будут беззнаковыми байтами, т.е числами от 0 до 255.
var volatile byte km1 -- единицы километров
var volatile byte km10 -- десятки километров
var volatile byte km100 -- сотни километров
var volatile byte meters -- кванты пройденного пути по 4.42 метра
var volatile byte old_meters -- старое значение квантов
var volatile byte imp_count = 0 -- счётчик импульсов
var volatile byte kmh1 = 0 -- отображение единиц км/ч
var volatile byte kmh10 = 0 -- отображение десятков км/ч
var volatile byte kmh1_tmp = 0 -- счётчик единиц км/ч
var volatile byte kmh10_tmp = 0 -- счётчик десятков км/ч
▍ Флаги
Программа спидометра реализует логику асинхронного автомата. Когда происходит определённое событие, оно поднимает флаг — однобитную булеву переменную.
Периодически запускается обработчик, выполняющий нужное действие при условии поднятого флага, и сбрасывает этот флаг.
В одном байте (восьмиразрядном регистре) можно хранить восемь таких флагов.
var volatile byte flags = 0b00001010
var bit indicate at flags:0 -- нужно переключить разряд динамической индикации
var bit stand at flags:1 -- мопед стоит на месте
var bit move at flags:2 -- мопед движется
var bit digit_1 at flags:3 -- нужно зажечь первый разряд индикатора
var bit digit_2 at flags:4 -- нужно зажечь второй разряд
▍ Обработчик прерываний
Обработчик прерываний — это подпрограмма, хранящаяся по определённому фиксированному адресу.
Микроконтроллер запускает её всякий раз, когда происходит прерывание по внутреннему сигналу с вывода или срабатыванию встроенного модуля периферии, независимой от ядра.
Программа спидометра использует внешние прерывания со входа INT, а также внутренние от двух таймеров — TIMER0, который будет переполняться 244 раза в секунду для динамической индикации, и TIMER1, переполняющегося 4 раза в секунду для расчёта скорости движения.
procedure interrupt is
pragma interrupt
if INTCON_T0IF then -- динамическая индикация
INTCON_T0IF = off -- сбрасываем флаг прерывания
indicate = on -- переключение разрядов индикации будет происходить в бесконечном цикле основной программы
end if
if INTCON_INTF then -- обработка фазного импульса
INTCON_INTF = off -- сбрасываем флаг прерывания
stand = 0 -- сбрасываем флаг неподвижности
move = 1 -- устанавливаем флаг движения
kmh1_tmp = kmh1_tmp + 1 -- прибавляем счётчик километров в час
if (kmh1_tmp >= 20) then -- прибавляем старший разряд и обнуляем младший
kmh1_tmp = 0
kmh10_tmp = kmh10_tmp + 1
if (kmh10_tmp >= 10) then
kmh10_tmp = 9 -- на случай ошибочных показаний выше 99 км/ч
end if
end if
if pwr_on then -- если МК проснулся от прерывания при выключенном питании, он заснёт вновь и ничего не будет делать
imp_count = imp_count + 1
end if
if (imp_count >= 64) then -- перевод импульсов в километры
imp_count = 0
meters = meters + 1
if (meters >= 226) then
meters = 0
km1 = km1 + 1
if (km1 >= 10) then
km1 = 0
km10 = km10 + 1
if (km10 >= 10) then
km10 = 0
km100 = km100 + 1
if (km100 >= 10) then
km100 = 0
end if -- если изменились сотни километров, производим запись в EEPROM
EEADR = 3
EEDATA = km100
EECON1_WREN = 1
EECON2 = 0x55
EECON2 = 0xAA
EECON1_WR = 1
while EECON1_WR loop
end loop
end if -- (km10 >= 10) -- если изменились десятки километров
EEADR = 2
EEDATA = km10
EECON1_WREN = 1
EECON2 = 0x55
EECON2 = 0xAA
EECON1_WR = 1
while EECON1_WR loop
end loop
end if -- (km1 >= 10) -- если изменились единицы километров
EEADR = 1
EEDATA = km1
EECON1_WREN = 1
EECON2 = 0x55
EECON2 = 0xAA
EECON1_WR = 1
while EECON1_WR loop
end loop
EECON1_WREN = 0
end if -- (meters >= 226)
end if -- конец перевода импульсов в километры
end if -- конец обработки фазного импульса
if PIR1_TMR1IF then -- два раза в секунду
PIR1_TMR1IF = off -- сбрасываем флаг прерывания
if move then -- если мопед движется
move = 0 -- сброс флага движения
kmh1 = kmh1_tmp / 2 -- просто побитный сдвиг вправо
kmh10 = kmh10_tmp
else
stand = 1 -- устанавливаем флаг неподвижности
end if
kmh1_tmp = 0
kmh10_tmp = 0
blink = !blink -- мигание указателя поворота
end if
end procedure
▍ Конфигурация периферии
Регистры TRISA и TRISB задают режим работы выводов микроконтроллера: 1 = Input, 0 = Output. Этот режим может меняться по ходу выполнения программы путём записи в данные регистры.
TRISA = 0b01110000 -- выходы: 3 разряда индикатора, указатель поворотов, стоп-сигнал, остальные входы
TRISB = 0b00000001 -- внешнее прерывание с фазы на вход, остальные выходы на сегменты индикатора
Далее выводим нули во все порты. Этого можно и не делать.
PORTB = 0b00000000
PORTA = 0b00000000
Теперь записываем в управляющие регистры микроконтроллера значения, соответствующие нужной нам конфигурации.
CMCON = 0b00000111 -- выключаем встроенные компараторы микроконтроллера.
OPTION_REG = 0b11000011 -- настройка TIMER0 на 244 Гц при тактовой частоте 4 МГц
PIE1 = 0b00000001 -- прерывания с периферии разрешены только от TIMER1
PIR1 = 0b00000000 -- обнуление регистра флагов прерываний с периферии
T1CON = 0b00110001 -- TIMER1 будет переполняться дважды в секунду
INTCON = 0b11110000 -- разрешение требуемых прерываниий
▍ Бесконечный цикл
forever loop
flags = 0b00001010 -- мопед стоит на месте, светит первый разряд индикатора
kmh1 = 0 -- обнуление всех счётчиков
kmh10 = 0
kmh1_tmp = 0
kmh10_tmp = 0
EEADR = 0 -- загрузка пройденного пути
EECON1_RD = 1
meters = EEDATA
old_meters = meters
EEADR = 1
EECON1_RD = 1
km1 = EEDATA
EEADR = 2
EECON1_RD = 1
km10 = EEDATA
EEADR = 3
EECON1_RD = 1
km100 = EEDATA
while pwr_on loop -- пока на спидометр поступает питание
brake_light = !brake_low -- включение стоп-сигнала
while (pwr_on&!indicate) loop -- ждём флага на переключение разряда индикации
end loop
indicate = off -- сбрасываем флаг
portb = 0
if pwr_on then -- если питание поступает
if digit_1 then -- очередь первого разряда
digit2 = 0 -- выключаем второй разряд
digit3 = 0 -- выключаем третий разряд
if stand then -- показ пройденного пути
portb = segments[km1] -- вывод единиц километров
else -- показ скорости
portb = segments[kmh1] -- вывод единиц км/ч
end if
digit1 = 1 -- зажигаем первый разряд
digit_1 = 0 -- при следующей смене разрядов эстафета перейдёт ко второму
digit_2 = 1
elsif digit_2 then -- очередь второго разряда
digit3 = 0 -- гашение третьего разряда
digit1 = 0 -- гашение второго разряда
if stand then -- показываем пройденный путь
if ((km10>0)|(km100>0)) then -- старшие разряды с нулями не показываем
portb = segments[km10] -- десятки километров
end if
elsif (kmh10>0) then -- отображение скорости
portb = segments[kmh10] -- десятки км/ч
end if
digit2 = 1 -- включаем второй разряд
digit_2 = 0 -- следующей будет очередь третьего разряда
digit_1 = 0
else -- третий - не первый и не второй
digit1 = 0 -- гашение первого разряда
digit2 = 0 -- гашение второго разряда
if stand then -- отображение пройденного пути
if (km100>0) then -- сотни километров
portb = segments[km100]
end if
end if
digit3 = 1 -- включаем третий разряд
digit_1 = 1 -- далее очередь первого разряда
digit_2 = 0
end if
end if -- pwr_on
end loop -- питание отключилось, работаем от конденсатора
INTCON_GIE = 0 -- запрет прерываний на время записи в EEPROM
PORTA = 0 -- выключение всей индикации
PORTB = 0 -- для экономии энергии
if (meters != old_meters) then -- если изменились кванты пройденного пути
while EECON1_WR loop -- дожидаемся завершения записи
end loop
EEADR = 0
EEDATA = meters
EECON1_WREN = 1
EECON2 = 0x55
EECON2 = 0xAA
EECON1_WR = 1
old_meters = meters
while EECON1_WR loop
end loop
EECON1_WREN = 0
end if
INTCON_GIE = 1 -- разрешение прерываний
asm sleep -- микроконтроллер засыпает до следующего прерывания
end loop
Инициализация программы с загрузкой пройденного пути помещена внутрь бесконечного цикла на маловероятный случай возобновления отключённого питания до наступления аппаратного сброса по BROWNOUT.
▍ Выводы по работе
Электромопед давно перешёл к другому владельцу. Дальнейшая судьба этого двухколёсного чуда мне неизвестна, и видео с работающим спидометром не сохранились. Зато есть фотографии.

Спидометр прекрасно выполнял свои функции и не был подвержен сбоям даже при проезде под линиями электропередачи.
Прошли годы, и сегодня я вряд ли возьмусь за такой «колхозный» проект. Однако в нём было своё неповторимое очарование. Напишите в комментариях, делали ли что-то подобное вы или ваши знакомые.
© 2025 ООО «МТ ФИНАНС»
Telegram-канал со скидками, розыгрышами призов и новостями IT 💻
