Формирование импульсов с помощью микроконтроллера (часть 2)


Просмотров: 14 366

В первой части мы рассмотрели как с помощью простой схемы можно формировать последовательности импульсов практически любой сложности. Пришло время применить эту идею к микроконтроллеру.

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

Итак, что нам нужно: раз в определённый интервал времени выбирать значение из таблицы и выдавать его на выход. На схеме есть три блока: тактовый генератор, счётчик и ПЗУ.

Тактовый генератор задаёт моменты времени, в которые должно выполняться некоторое действие. В МК для отсчёта времени есть штатное средство — таймер. Он и будет у нас выполнять функцию генератора.

Счётчик и ПЗУ образуют блок, по приходу очередного такта выбирающий следующее значение из таблицы и выдающий его на выход. Этот блок мы решим программно. Таблица может лежать как в памяти программ (Flash), в этом случае она неизменна или в ОЗУ (RAM) или ППЗУ (EEPROM), тогда она может изменяться в ходе работы алгоритма.

Таймер на каждый «такт» нашего виртуального генератора будет генерировать прерывание. Вся остальная программа будет выполняться в прерывании. Это, во-первых, даст жёсткую привязку к «тактам», а во-вторых, освободит ядро для выполнения других задач, некритичных ко времени исполнения. Подробнее о прерываниях читать здесь, здесь и здесь.

Что ж, приступим!

Рассмотрим решение задачи на примере макетика, который я сделал для одного проекта. Мне надо было сэмулировать одно устройство, которое выдавало данные по 3-проводному интерфейсу. Данные передавались по трём линиям:

STB — строб на слово. Импульс, пришедший по этой линии, фиксирует данные на выходе приёмника.
SCK — строб на бит. Во время импульса на этой линии приёмник считывает бит данных с линии SDT.
SDT — собственно данные.

Стробы в устройстве, которым я притворялся, всегда повторялись от цикла к циклу, а данные могли быть представлены в одном из пяти видов:

3 wire interface

Посмотрим на первую четвёрку импульсов SCK и сопутствующие им варианты SDT. Недолго пофтыкав, видим закономерность. Это же просто передача двоичной цифры от 1 (0001) до 5 (0101)! Во время второй четвёрки SCK данные повторяются.

Как видим, данная реализация интерфейса позволяет передавать до фига информации: аж 4 бита, число от 0 до 15!  🙂
Впрочем, для того устройства больше было и не надо: «данные» — это всего лишь код нажатой кнопки, а кнопок там было всего пять, причём в единицу времени могла быть нажата только одна 🙂

Такие интерфейсы были популярны в силу крайней простоты их аппаратной реализации. Для этого не нужны никакие микропроцессоры. Передатчик легко ваяется по схеме с ПЗУ, а приёмник — всего лишь одна микросхема 74HC595. Внутри у неё два 8-разрядных регистра: сдвиговый и параллельный:

3 wire interface

Сначала данные побитно заталкиваются в сдвиговый регистр, а потом (по стробу STB) всё накопленное богатство защёлкивается в параллельный регистр и попадает на выход.

Так как тактовый сигнал SCK передаётся вместе с данными, приёмник нечувствителен к нестабильности частоты передатчика: ему чётко объясняют когда забирать очередной бит данных.

А если на выходе поставить микросхему сравнения кодов типа 555СП1 (SN74LS85), которая будет сравнивать младшее полуслово (D0-D3) со старшим (D4-D7), то можно делать вывод о правильности передачи данных: половинки должны совпасть.

Что-то я отвлёкся 🙂

А нам, чтобы написать программу, надо решить последний вопрос: какой должна быть частота «генератора» (роль которого выполняет таймер). Точнее, для удобства, каким должен быть интервал между «тактовыми импульсами» (роль которых выполняют прерывания).

Если мы формируем одну последовательность, всё очевидно: интервал должен быть равен (или кратен) самому короткому импульсу или промежутку между импульсами нашей последовательности. Так как поднять выход в «1» или уронить его в «0» мы можем только один раз за цикл, то для формирования импульса нам надо минимум два вызова подпрограммы обработки прерывания. Всё как на физической модели.

Несколько сложнее случай параллельной выдачи разных последовательностей на разные выходы. Если на двух выходах у нас два одинаковых импульса, но чуть-чуть сдвинутых друг относительно друга, то это «чуть-чуть» и должно быть нашим интервалом. М… не совсем так! Мы должны найти такое значение, которое нацело укладывается как в длительность импульсов, так и промежутков между ними, а также в любое значение сдвигов между импульсами на разных выходах. Иллюстрацию чертить не буду, думаю, смысл понятен.

Опять возвращаясь к аппаратному решению отмечу что тактовую частоту часто увеличивали так чтобы на минимальный интервал пришлось несколько пар тактовых импульсов. Это делалось для того чтобы можно было «чуть-чуть» подвинуть какой-нибудь импульс или слегка изменить их взаимное положение не переделывая схему, а только меняя «прошивку» ПЗУ. Для программной реализации это не обязательно: ведь мы всегда можем перепрограммировать таймер.

Для своего макета я выбрал МК ATTiny2313. Так как никакая периферия, кроме той, что имеется в МК, мне не нужна, я использовал готовый процессорный модуль от платы PinBoard II. А поскольку не предполагалось превращение макета в полноценное устройство, я облегчил себе жизнь и не стал заморачиваться реализацией выбора какую «цифру» вывести в канал SDT. Вывел все! 🙂 Получилась не одна линия SDT, а пять, как на диаграмме выше. Выбор нужной «цифры» осуществлял подключая проводок к нужной ножке. Весь пакет сигналов выводил в порт B.

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

Ещё у ATTiny2313 есть интересная «фишка»: на выход PD2 можно «пробросить» системный clock, сигнал, от которого тактируется МК. Это можно использовать для контроля (и подстройки) внутреннего RC-генератора или чтобы подать тактовый сигнал на другие узлы схемы.

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

В шапке указываем что программа должна делать, распределение портов, а также настройку Fuses, при которых программа должна работать. Очень трудно всё это вспоминать по прошествии времени.

; 3-wire interface emulator
; for ATtiny2313
; written by Wan-Derer, oct-2013
; 
; Ports assignment:
; PB7 - STB, word srobe
; PB6 - SCK, bit strobe
; PB5...PB1 - SDT, data: PB1 outputs "1", PB2 outputs "2" etc
;
; PD6...PD3,PD1 - test out: 8 us period pulses (4 us "1", 4 us "0")
; PD2 - Master Clock return (for calibration)
; 
; fuse bytes: EXT=$FF, HIGH=$DF, LOW=$A4

Далее подключаем файл определений для выбранного МК:

 .include "tn2313Adef.inc"

Вводим свои собственные определения. Благодаря им будет возможно изменять какие-то параметры программы, не блуждая в её дебрях. Например, перекомпилировать под другую тактовую частоту:

; === Defines
 .def temp=R16
 .def FFmask=R17 ; mask for Inverse port D outputs
 .def DataEndL=R18 ; Constant = Data+DSize
 .def DataEndH=R19 ;
 .def CycEndL=R20 ; Constant = Data+CycleSize
 .def CycEndH=R21 ;
 .equ MCLK=8*1000000 ; Master Clock is 8 MHz
 .equ Data=Correct*2 ; Will output correct data
 .equ DSize=24 ; Data Table Size
 .equ CycleSize=8500 ; Cycle Size in 4us quantums (34ms=8500*4us)

Далее пошёл кодовый сегмент. Сначала настроим таблицу прерываний. Здесь важны строчки, определяющие точку старта по Reset и адрес подпрограммы прерывания по таймеру:

; === Code
 .cseg
; --- Interrupts Table
 .org 0
 rjmp reset

 .org OC0Aaddr ; Timer/Counter0 Compare Match A
 rjmp Handle4us

Старт программы. Настраиваем стек, отключаем лишнюю периферию:

reset: cli
; --- Stack Init
 ldi temp,RAMEND
 out SPL,temp

; --- Periferal Init
 ldi temp,$45 ; Internal CLK calibration
 out OSCCAL,temp ;

 ; --- Analog Comparator power off
 ldi temp,1<<ACD
 out ACSR,temp

Конфигурируем порты B и D как выходы и выводим в них нули.

; --- Ports Init
 ser temp ;
 out DDRB,temp ; Configure port B as output
 out DDRD,temp ; Configure port D as output
 clr temp ;
 out PORTB,temp ; set outputs B to "0"
 out PORTD,temp ; set outputs D to "0"

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

; --- Timer 0 Init
 ldi temp,1<<WGM01|0<<WGM00 ; CTC mode
 out TCCR0A,temp ;
 ldi temp,1<<CS00 ; Timer clock = Master clock
 out TCCR0B,temp ;
 ldi temp,4*MCLK/1000000-1 ; Interrupts period = 4 us
 out OCR0A,temp ;
 ldi temp,1<<OCIE0A ; Interrupts enable by Timer 0 Compare Match A
 out TIMSK,temp ;

Готовим регистры: константу, которую будем использовать для инверсии порта D, указатель на текущую позицию в массиве данных, размер массива, размер цикла.
Тут я схалтурил. Так как программа не была предназначена для чего-то другого кроме генерации моих последовательностей, я все данные храню в регистрах и не сохраняю их в стеке при обработке прерывания. Конечно, в «боевых» программах так делать нельзя!

; --- Regs/Mem Init
 ; --- mask for Inverse port D outputs
 ldi FFmask,$FF 
 ; --- Pointer
 ldi ZL,low(Data-1)
 ldi ZH,high(Data-1)
 ; --- Data+DSize
 ldi DataEndL,low(Data+DSize)
 ldi DataEndH,high(Data+DSize)
 ; --- Data+CycleSize
 ldi CycEndL,low(Data+CycleSize)
 ldi CycEndH,high(Data+CycleSize)

Главный цикл выглядит шедеврально! 🙂

; --- Start Program
 sei ; Interrupts Global Enable

loop: rjmp loop

Подпрограмма обработки прерываний. Вызывается по таймеру каждые 4 мкс.
Сначала инвертируем выходы порта D. Далее выдаём «цифру».
Если указатель находится внутри массива данных, на выход выдаётся очередное значение.
Если указатель дощёлкал до конца массива, начинают крутиться циклы ожидания. Так между выдачами «цифры» образуется пауза.
Когда пауза закончилась, стрелка переводится на начало массива и цикл повторяется.

; --- Interrupt Handle
Handle4us:
 out PIND,FFmask ; Inverse port D outputs

 adiw ZL,1 ; Inc Pointer

 cp ZL,DataEndL ; If Pointer <= DataEnd, Go to Data Output
 cpc ZH,DataEndH ;
 brcs DataOut ;

 cp ZL,CycEndL ; If Pointer <= DataEnd, DO a wait cycle (reti)
 cpc ZH,CycEndH ;
 brcs WaitCycle ;
 
 ldi ZL,low(Data-1) ; If Pointer > DataEnd, DO a new cycle
 ldi ZH,high(Data-1) ;

WaitCycle: reti

DataOut: lpm temp,Z
 out PORTB,temp
 reti

Массив данных хранится в памяти программ. Надо помнить что она состоит из двухбайтовых слов.
Благодаря двоичному представлению, чётко видно где будет импульс, а где промежуток.

Correct: 
 .db 0b01000000,0b00000000
 .db 0b00110000,0b01110000
 .db 0b00000000,0b00001100
 .db 0b01001100,0b00000000
 .db 0b00101010,0b01101010
 .db 0b00000000,0b01000000
 .db 0b00000000,0b00110000
 .db 0b01110000,0b00000000
 .db 0b00001100,0b01001100
 .db 0b00000000,0b00101010 
 .db 0b01101010,0b00000000
 .db 0b10000000,0b00000000

Компилируем, загружаем программу в МК, запускаем:

Эмуляция 3-проводного интерфейса на ATTiny2313

Эмуляция 3-проводного интерфейса

Ура, работает! Теперь можно поиздеваться над макетом. Например, подключиться на контрольные выходы и подстроить RC-генератор. Или понаблюдать явление джиттера: засинхронизироваться по какому-нибудь импульсу, потом «промотать» развёртку до следующей пачки и посмотреть как её мотыляет. Потом подключить кварцевый резонатор и посмотреть насколько улучшилась стабильность системы.

Время подытожить всю эту нуднятину 🙂 Я рассмотрел два способа формирования сложных последовательностей импульсов: «устаревший» аппаратный и «прогрессивный» программный (точнее аппаратно-программный). Какие есть достоинства и недостатки второго метода относительно первого.

Достоинства:

  • простота реализации: на фото видно: платка с «голым» МК плюс программатор и мы уже имеем программируемый генератор сложных последовательностей (конечно, если надо отдавать сигнал наружу, нужны буферные усилители, хоть та же ULN);
  • больше возможностей по изменению формируемых последовательностей без переделки схемы;
  • большая гибкость: помимо формирования импульсов на МК можно возложить другие задачи (опрос кнопок/датчиков, вывод информации на дисплей и пр.).

Недостатки:

  • недостаток один — низкая производительность. Я уже точно не помню, но, вроде, я запускал эту программу на тактовой частоте МК 4 МГц. Как будто, работала. Но это край. Надо взять калькулятор и посчитать такты в подпрограмме обработки прерывания.  При этом квант времени в 4 мкс соответствует частоте «тактового генератора» всего лишь в 250 кГц! Т.е. ни о каких мегагерцовых сложных генераторах речь не идёт. И если такой понадобится, придётся искать аппаратное решение (на ПЗУ или на ПЛИС) или брать МК с очень высокой производительностью.

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

 

Вложения: 3wire_Emulator.zip

Формирование импульсов с помощью микроконтроллера (часть 2): 2 комментария

  1. Ренат

    Добрый день!
    Спасибо Вам большое за материал, за статью, но немного плоховато объясняете: не совсем понятно, как работает счётчик импульсов.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *