avatar_Паяка

Таймеры+прерывания = спидометр+одометр

Автор Паяка, 11 Май 2018 в 20:11

« назад - далее »

0 Пользователи и 1 гость просматривают эту тему.

Паяка

Ценность рассказа о том, как для Вишенки спидометр делался, наверное, прежде всего, в том, что это простой и наглядный пример использования ресурсов микроконтроллера, а именно, прерываний и ПНЯ, - периферии независимой от ядра.

Многие коллеги, начавшие путь в информатику с Бейсика и Паскаля (или Си) для ПЭВМ, проявляют упорную верность линейному программированию и программной реализации всего, что только можно, игнорируя чудесные возможности верных друзей-жучков от Микрочипа и Атмела. Повествование прежде всего для них, а также начинающих. Попутно освоим динамическое управление 7-сегментным индикатором, достанем из древности ULN2003А, научим МК отслеживать момент отключения своего питания (?!), поработаем с энергонезависимой памятью. В общем, будет интересно.


Немного рассуждений
В наличии имелся небольшой (22.5x14x7.2мм, высота цифр 14мм) трёхразрядный индикатор красного цвета свечения с общим катодом SP420361 и 18-ногий, (т.е. имеющий два полных 8-разрядных порта, что для семисегментной индикации прекрасно) PIC16F628A. С общим катодом, значит, индикация по определению динамическая. Один полный порт удобно задействовать под сегменты, у второго выделить 3 вывода для разрядов. Число ножек позволяет, без всякого мультиплексирования.

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

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

Потому воспользуемся простейшим транзисторным инвертором на каждый сегмент. Но спидометр хочется разместить в горловине бывшего бензобака, и желательно на одной плате. Инвертор на биполярном транзисторе - это 3 детали: сам транзистор и 2 резистора, один ограничивает ток с ножки МК на базу, другой подтягивает базу к земле. Если транзистор полевой, и если ёмкость затвора и частота переключения невелики, можно, (хотя и не является безупречным), подключить затвор напрямую к выводу порта, сэкономив резистор, точнее, место под него. Наконец, есть специальные биполярные транзисторы как раз для управления светодиодами, реле и т.п. с микроконтроллеров, в которых два резистора рассчитаны под логические уровни и встроены в корпус.

Ещё есть микросхема ULN2003A, представляющая собой семь транзисторов Дарлингтона с упомянутыми резисторами и защитными диодами. Применяется чаще всего для мощных светодиодных индикаторов, реле, а также маломощных шаговых двигателей. Она была под рукой, её и поставим. Зачем такое громоздкое, избыточное решение, 4 лишних канала? - Затем, что они не лишние. Будут мигать указателями поворота и включать стоп-сигнал.

Мигалка светодиодом с ножки микроконтроллера - это "Привет, мир!", первое, с чего начинают применение однокристальной ЭВМ. Есть от неё ещё одна польза. Мигание указателей поворота свидетельствует о том, что микроконтроллер и программа работают. Точнее, о том, что работает микроконтроллер, так как прерывания с таймера обрабатываются и при зашедшей в тупик основной программе. Есть даже сторожевой таймер, перезагружающий МК, если программа вовремя не сбросила его счётчик, весьма полезная вещь для непростых алгоритмов. Но спидометр у нас простой, и отвлекаться не будем.

Почему не включать стоп-сигнал прямо с ручек тормоза (Brake High), как сделано на скутерах, зачем использовать велосипедный вариант сигнала Brake Low? - Мне захотелось, чтобы на органах управления присутствовало как можно меньше силовых и высоковольтных цепей.

Добавлено 11 Май 2018 в 20:15

Итак, нарисуем принципиальную схему и плату.
Схема простая и нарисована позже, специально для форума, мной рисовалась сразу плата. Ножки микроконтроллера PIC не идентичны, на разных присутствует разная периферия. Имеем это в виду, оптимизируя использование портов под трассировку!

Цепочка R14 C5 образует простейший фильтр низких частот, срезающий частоту ШИМ нашего синусного контроллера и пропускающий только частоту импульсов с фазы. D5 пропускает только положительную полуволну, а D3 ограничивает амплитуду подаваемого на вход МК импульса до безопасных для 5-вольтового 4.7В. Используется вход сигнала внешнего прерывания, реализованный как триггер Шмитта, прерывание по фронту сигнала.

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

Добавлено 11 Май 2018 в 20:16

Расчёты по геометрии колеса
Длина окружности шины 16-дюймового колеса двухскоростных Риг, с 4-й по Дельту, а также Верховин и Карпат, 166 см. Фаза Конхиса даёт 24 импульса на оборот. Получается 6.91(6) см на импульс, 14458 импульсов на километр. 1 км/ч - это 14458 импульсов в час, или 2 импульса за полсекунды. Итак, посчитав число импульсов за четверть секунды, получим значение скорости в километрах в час.

14458 примерно равно 226*64. Это удобные множители, т.к. оба меньше 256, а 64 - степень двойки, что поможет нам экономно и точно обрабатывать данные пройденного пути и хранить их в энергонезависимой памяти.

Длина окружности шины 19-дюймового колеса двухскоростных Риги-1 и Риги-3, а также односкоростных, начиная с 5-й, если не ошибаюсь, 188 см, что даёт 7.8(3) см на импульс, 12766 имп. на км, округлим до 256*50. 3.5461 имп. в cекунду, что даст число импульсов, равное километрам в час, при периоде отсчёта 0.282 секунды. По данным образцам можно рассчитать константы спидометра и одометра для своего транспортного средства.
GT TF1 60V 20Ah Chilwee DZF

Паяка

#1
Дальнейшие пояснения к схеме


Каждый из семи инверторов сборки ULN2003A - в данном приложении просто ключ, замыкающий выход на землю при высоком уровне на входе и размыкающий при низком. Т.е. для того, чтобы зажечь светодиод, подключенный на выход через токоограничивающий резистор к +5 или +12В, нужно подать логическую единицу на соответствующий вход, а чтобы погасить, - подать логический ноль.

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

Под выход TURN для указателей поворота задействованы целых 2 инвертора, их можно соединять параллельно, хоть все. МК переключает 1 на 0 и наоборот на своей ножке, подключенной к входам этих инверторов, раз в полсекунды. Соответственно, раз в полсекунды будет мигать указатель соответствующего поворота, если включить его с пульта на руле.

А вот выход REAR для заднего фонаря зашунтирован 100-омным резистором мощностью не менее 0,25 Вт. Зачем? - Затем, чтобы ток светодиода заднего фонаря при стабильных 12В питания на мопеде равнялся в обычном режиме порядка 40 мА, а когда нажат тормоз, поднимался до порядка 100 мА. Так работает стоп-сигнал.

Но сигнал Brake_low на рычагах тормоза притянут к массе при нажатом рычаге, а при отпущенном подтянут через резистор внутри контроллера к 5 вольтам, то есть наоборот от того, что нужно подавать на входы ULN2003. Для того чтобы проинвертировать сигнал ещё раз, можно воспользоваться, очевидно, инвертором, хоть сигнальным, хоть силовым, а можно задействовать пару ножек МК, что мы и сделаем. Микроконтроллер это не затруднит.

Питается МК от старого доброго 7805, между выходом которого и электролитическим конденсатором C1 стоит диод Шоттки, благодаря которому, при пропадании 12В питания, когда мы отключаем цепи управления противоугонным замком, C1 не разрядится иначе, чем питая МК. А высокий уровень сигнала на ножке RA5, подключенной до D2 через R2, пропадёт. Благодаря чему, МК узнает об отключении питания, когда в C1 ещё достаточно энергии, чтобы экономичный микропроцессор некоторое время поработал. Разумеется, при пропадании питания первым делом отключим семисегментный индикатор, что учтено в программе.

Добавлено 11 Май 2018 в 20:18

Исходник прошивки с комментариями
Вот и она. Очень удобным средством разработки под PIC является открытый и бесплатный JAL v.2, предоставляющий и компилятор языка, и среду разработки. Он не идеален, но позволяет воплощать собственно алгоритм, не заморачиваясь низкоуровневой спецификой там, где это не надо. А там, где надо, можно вставить строчку или блок на Ассемблере. Также можно посмотреть, как выглядит в Ассемблере то, что мы написали на JAL. Это язык семейства Паскаль.

Программа довольно проста, так что просто приведу её текст с комментариями.

include 16f628a
-- сообщаем компилятору, с каким МК работаем. Он берёт библиотеку с именем 16f628a, в которой находит имена, адреса и значения для всех констант, переменных и функций из даташита. Да, JAL V.2 - по сути, макроассемблер.
pragma target clock 4_000_000
-- тактовая частота 4 МГЦ, частота команд 1 МГц
pragma target OSC      INTOSC_NOCLKOUT
-- используем встроенный RC-генератор. Для показометра точности хватит. Если хотите, можно добавить и кварц.
pragma target WDT      DISABLED
-- сторожевой таймер не задействован
pragma target BROWNOUT ENABLED
-- активируем сброс при просадке питания
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 -- 1-й разряд индикатора
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 -- стоп-сигнал

-- Благодаря тому, что на сегменты выделили весь порт В, можно просто задать соответствие десятичной цифры двоичному числу, записываемому в регистр порта одним действием.

const byte segments_upside [10] = {
-- если индикатор повёрнут вверх ногами
0b11101110,
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 -- дробная часть
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
-- Флаги. Это однобитные переменные, хранящиеся в однобайтовой. Они могут быть равны 1 или 0.
var bit indicate at flags:0
-- 1, если пора переключить разряд индикации
var bit stand    at flags:1
-- 1, если мопед стоит на месте
var bit move     at flags:2
-- 1, если мопед движется. Казалось бы избыточные флаги, на самом деле, нет.
var bit digit_1  at flags:3
-- 1, если светит первый разряд
var bit digit_2  at flags:4
-- 1, если светит второй разряд

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
-- прибавляем старший разряд и обнуляем младший, прямо как в механических арифмометрах, шагомерах, одометрах, электросчётчиках и т.п. 20, а не 10, потому что 2, а не 1 импульс за полсекунды
   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
-- если МК проснулся от прерывания при выключенном питании, он не будет считать пройденный путь, а просто снова заснёт, не изменяя значений переменных и не записывая ничего в EEPROM
   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)
-- если изменились десятки километров, записываем в EEPROM
     EEADR = 2
     EEDATA = km10
     EECON1_WREN = 1
     EECON2 = 0x55
     EECON2 = 0xAA
     EECON1_WR = 1
     while EECON1_WR loop
     end loop
    end if -- (km1 >= 10)
-- если изменились единицы километров, записываем в EEPROM
    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 -- сбрасываем флаг движения
-- деление и умножение на 2 занимают мало ресурсов, ведь это просто сдвиг битов вправо или влево
   kmh1 = kmh1_tmp / 2
-- снова учли 2 импульса в полсекунды
   kmh10 = kmh10_tmp
  else
   stand = 1 -- устанавливаем флаг, что стоим на месте
  end if
  kmh1_tmp = 0
  kmh10_tmp = 0
  blink = !blink -- мигаем указателем поворота
end if
end procedure

-- 1 = Input, 0 = Output. Устанавливаем, какие ножки будут входами, какие - выходами. Их можно переназначать в любое время.
TRISB    =   0b00000001   
-- интегратор сигнала с фазы на вход, остальные выходы на сегменты
TRISA    =   0b01110000
-- выходы: 3 разряда, указатель поворотов, стоп-сигнал, остальные входы
PORTB    =   0b00000000
PORTA    =   0b00000000
-- выводим нули во все порты. Это делать необязательно.
CMCON    =   0b00000111 
-- Встроенные компараторы МК выключены. Они нам в спидометре не требуются.

OPTION_REG = 0b11000011
-- настроим TIMER0, исходя из тактовой частоты, на 244 Гц. Это будет частота смены разрядов динамической индикации
PIE1     =   0b00000001
-- разрешим прерывания с периферии только от TIMER1
PIR1     =   0b00000000
-- обнулим регистр флагов прерываний с периферии
T1CON    =   0b00110001
-- настроим TIMER1 на переполнение раз в полсекунды
INTCON   =   0b11110000
-- разрешим нужные нам прерывания

forever loop -- подготовка завершена, начинаем главный цикл
flags = 0b00001010
-- мопед стоит на месте, светит 1 разряд индикатора
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
-- ждём, пока обработчик прерывания даст команду сменить разряд индикации путём установки флага. Это происходит 224 раза в секунду.
  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
-- запретим прерывания на время записи в энергонезависимую память
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
GT TF1 60V 20Ah Chilwee DZF