К вопросу об AVR и мировых рекордах

habr.png

Делай хорошо, плохо само получится


Поводом к посту послужила недавняя (когда я начинал писать этот пост, она действительно была недавней, но что-то пост долго пролежал в папке Неоконченное) публикация на Хабре относительно аспектов реализации программного UART на МК фирмы AVR. Сами по себе поднятые вопросы небезынтересны, но даны на них столь странные ответы, что посчитал своим долгом внести необходимые разъяснения. Тема обозначена, те, кто захочет прочитать о «королях, капусте и башмаках», то есть требованиях стандартов, чтении (правильном) технической документации и рекордах в программировании на ассемблере для AVR, могут нажать на кнопочку ниже.
Обозначим вопрос более подробно — возможна ли реализация ИРПС (привычное мне название интерфейса, в девичестве имеющего имя UART) на МК типа AVR (конкретно речь шла о Tiny13) при работе от внутреннего генератора. Дело в том, что данный генератор имеет не слишком хорошие показатели по точности удержания частоты, почему данный вопрос и возникает. Сразу оговорюсь, что не имеет значения, будем ли мы рассматривать программную реализацию (как было предложено в исходном посте) либо использовать аппаратные блоки МК. Результаты одного способа (в части точностных параметров по времени) практически полностью транслируются на другой.

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

Для начала найдем то, что найти проще (ну, я так думал) — требования к точности временных параметров интерфейса. Откроем стандарт на RS232 и все нужное сразу увидим. Оказалось, что «нельзя просто так взять и …», поскольку стандарт является платным и все лежащие в Сети копии нелегальны. Ладно, берем отечественную версию ГОСТ на стык С2 и не обнаруживаем там вообще никаких временных параметров, за исключением длительности фронта и среза импульса. Поначалу это вызвало легкую оторопь — как так может быть —, но потом пришло понимание, что стык С2 описывает только интерфейсную часть ИРПС и требования должны заключаться именно в последнем. В принципе, все логично, непонятно только, почему в ГОСТ это явно не описано, но, в конце концов, иногда можно и подумать самому, хотя все равно «как то не аккуратненько получается».

Конечно, зная протокол передачи, можно из общих соображений найти максимально допустимое рассогласование скоростей передатчика и приемника (0.5/9.5=5.2%), но это будет исследование сферического коня сами знаете где, поскольку:

  1. требования стандарта могут и должны быть жестче, чем подобный теоретический расчет предельно допустимого рассогласования;
  2. знание итоговой цифры рассогласования никак не даст нам бюджет передатчика и приемника.


Странствия по просторам Инета привели на AppNote от Atmel (ну раз мы все равно используем МК этой фирмы), где прямо говорится о допустимом рассогласовании в 2% с равным бюджетом, что приводит к требования точности поддержания частоты передатчика в 1%. Поверим уважаемой фирме и предположим, что они имеют доступ к секретным материалам и эта цифра верна, тем более, что выглядит она правдоподобно. Понимаю уязвимость подобной позиции, но мне, честно говоря, надоело искать точный ответ на такой простой вопрос, и не терпится перейти к следующей части.

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

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

Так что открываем документацию (Вы можете открыть файл в просмотрщике, а у меня есть типографская версия описания, отпечатанная самой фирмой-производителем — да, раньше такое бывало) и ищем соответствующий раздел. Интересующие нас параметры находятся в разделе «Calibrated Internal RC Oscillator», далее при необходимости идем по ссылкам. И вот тут нас (меня точно, насчет Вас не уверен) ждало первое разочарование — я давно (лет 15) работаю с продукцией Atmel, и всегда считал, что у них хорошая документация на МК. Как утверждают психиатры, «нет здоровых людей, есть не дообследованные» и внимательное изучение соответствующего раздела подтвердило эту истину, как я мог раньше не замечать таких провалов в документации. В свое оправдание могу только сказать, что:

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


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

Первая часть марлезонского балета-номинальная точность.

Сразу же находим нужный параметр — таблицу точности настройки генератора, в которой видим две строки «Factory Calibrated» с указанным значением ±10% и «Manual Calibrating» с аналогичным параметром ±2%.

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

А вот со второй строкой дела похуже — даны пределы изменения температуры и напряжения питания и утверждается, что применением некоей волшебной процедуры калибровки можно достичь существенно лучшего, нежели заводской, результата во всем диапазоне. У меня сразу же возникает вопрос — если этого можно добиться повсюду (в любой точке температуры и питания) и фирма-изготовитель знает, как это сделать, то почему она это не сделала сама при фабричной калибровке в конкретной точке условий? Обращаемся к описанию калибровочного байта и видим, что он принимает 128 значений и и этом перекрывает диапазон от 50% до 200% номинала, что соответствует 150/128~1.17% изменения частоты на единицу значения калибровки, что должно дать ожидаемую точность лучше, чем в 1%. Но дальше нам следует учесть, что регулировочная характеристика явно не линейна и в области больших значений калибровки имеем 60%/32~2% шага (данные взяты с графика, я неоднократно высказывал свое отношение к подобному методу представления технических параметров, но повторюсь — это неприемлемый метод, хотя, конечно, лучше, чем ничего), что дает точность в 1% и если мы учтем не монотонность регулировочной характеристики (да, именно так указано в документации, не нарисовано в графике, а явно указано в тексте. Я категорически отказываюсь понимать как, и главное зачем, фирма захотела сделать именно такой закон регулировки, но ей удалось), что явно указано в рекомендациях, то следует считать точность в 2% вполне достижимой. Мне не очень нравится, что пришлось смотреть график, но это не обязательно и табличные данные являются достаточными. В данной части следует считать документацию вполне понятной и непротиворечивой, критерий правильности лежит вне пределов нашей компетенции.

Вторая часть марлезонского балета. — влияние внешних условий.

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

Ладно, проехали, попытаемся извлечь хоть какую-то информацию и видим, что при изменении температуры от -40 до +80°С частота генератора изменяется на ±4%. Аналогичная картина и с напряжением питания — только типовые графики и результирующая погрешности в -6 +2% от 3.3 до 5.5. Данных же по старению генератора просто не дают, что, в общем, логично, поскольку на фоне уже приведенных параметров точность в единицу процента за 5 лет (характерная величина для кремния) уже никого не волнует.

Теперь у нас есть все данные для ответа на наш изначальный вопрос — при фабричной калибровке генератор не отвечает требованиям интерфейса по точности, при калибровке под конкретные условия применения — отвечает граничным требованиям, но не отвечает стандарту. Следует также учесть, что если калибровку под напряжение питания и конкретный МК можно сделать при изготовлении устройства и надеяться, что они не изменятся во времени, то учет температуры возможен только «на лету» и требует внешнего эталона времени соответствующей точности. Поскольку при разработке устройств следует руководствоваться правилом «в Бога мы верим, все остальное требует доказательств», а возможность соответствия требованиям мы не доказали, то правильный ответ — гарантировать реализацию ИРПС, отвечающего требованиям стандарта, в данном МК с внутренним генератором невозможно. Обратим внимание, что приведенный вывод мы сделали при анализе документации и сформулировали именно таким способом, чтобы подчеркнуть, что на конкретном экземпляре МК все может и получиться, если звезды встанут удачно. То есть наш вывод противоречит ранее упомянутому посту, как могло такое получится, ведь у человека все замечательно работает — давайте разбираться.

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

  1. Хороший способ — измерить критичные параметры интерфейса устройства и сравнить их с требованиями стандарта — это можно сделать с помощью универсальных приборов (в нашем случае осциллограф и длина битового интервала либо полной посылки), либо с помощью специализированного прибора, который сертифицирован на выполнение проверки данного интерфейса.
  2. Так себе способ — организовать взаимодействие с другим устройством, которое реализует ответную часть интерфейса и является доказанным (отвечает требованиям стандарта). Конечно, такая проверка совершенно недостаточна, и скорее, может быть применена более для подтверждения неисправности тестируемого устройства, но делает хоть что-то.
  3. Плохой способ — самостоятельно реализовать ответную часть интерфейса (в этом же устройстве или в ином) и взаимодействовать с ней. Поскольку оба устройства очевидно не доказаны, то польза от подобной проверки весьма и весьма сомнительно. Хорошим примером такого подхода является «эхо» на последовательном канале, которое ничего, кроме того факта, что устройство в принципе не сломано и способно что-то как-то передать, не доказывает и о скорости передачи сообщает ненамного больше, чем ничего.
  4. Ужасный способ — взять в качестве тестирующего вообще не отвечающее требованиям стандарта (а лучше противоречащее им) устройство и работать, как в предыдущем пункте.


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

Общий вывод из вышесказанного:

  1. при проектировании устройств следует обращать основное внимание на документацию (RTFM),
  2. необходимо изучать документацию и правильно интерпретировать прочитанное (RTFMF),
  3. иметь в виду, что в документации в наше время могут быть недоговоренности, неточности (и даже ошибки), поэтому
  4. проверять полученную информацию на непротиворечивость и правдоподобность, и
  5. использовать экспериментально полученную информацию только для подтверждения полученных из анализа документации выводов, при этом
  6. особо тщательно выбирать методы экспериментов по тестированию аппаратуры для получения достоверного результата.


Ну и в заключение, как и было обещано, немного ассемблера. Я позволил себе переписать приведенный автором фрагмент кода в нормальном виде, поскольку встроенный в GCC ассемблер ничем иным, кроме как издевательством на программистом, назвать не могу. Нет, я, конечно, понимаю, что разработчики компилятора руководствовались вескими соображениями, но результат до боли напоминает фразу «ну работает же».

.equ delay=15
TX_Byte:
        cli
;       ld      r18,Z+
;       cp      r18,r1
;       breq    Exit_Transmit
;       dec     r1
        cbi     port, TX_line
Delay_TX:
        ldi     r16,delay
Do_Delay_TX:
        nop
        dec     r16
        brne    Do_Delay_TX     
TX_Bit:
        sbrc    r18,0
        sbi     port,TX_line
        sbrs    r18,0
        cbi     port,TX_line
        lsr     r18
        lsr     r17
        brcs    Delay_TX
        sbi     port,   TX_line
        ldi     r16,delay
Stop_Bit_TX:
        nop
        dec     r16
        brne    Stop_Bit_TX
        Sei


И сразу в глаза бросается ошибка в программе — в строке 3 (закоментированна) значение регистра 1 должно быть нулевым, но явно в функции присвоение не прописано. После выполнения цикла передачи одного байта данное значение гарантируется строкой 12, но при первом проходе — нет. Поэтому должна быть добавлена инициализация, что потребует увеличения размера кода.

Второй недостаток — собственно формирование уровня в строках 4–7, поскольку принятая автором методика выдачи очередного бита приведет к джитированию фронта на 2 такта при различных переходах (0–1 и 1–0), что повлечет за собой повышение требований к точности удержания частоты. Не то, чтобы это давало очень сильное влияние, но, если можно исправить недостаток, не удлиняя программу, то почему бы и нет — смотри эпиграф. Первоначальный вариант занимал 4 слова и исполнялся за 4 такта, новый занимает 4 слова и исполняется за те же 4 такта. Да, исправленный вариант требует более глубокого изучения архитектуры МК, но кто говорил, что будет легко. С другой стороны, в первом варианте модификация порта атомарна, а во втором — нет, в данном случае это неважно (мы явно запретили прерывания), но осадок остается. Если бы в рассматриваемом МК был настоящий битовый процессор, как в архитектуре 51, то мы могли бы написать идеальный фрагмент, сочетающий все преимущества обоих подходов (и даже был бы чуть короче), но что мечтать о несбыточном…

Третий недостаток — вопрос более стиля. Я неоднократно высказывал свое отношение к магическим константам, которые мы видим в преамбуле данной программы. Еще раз подчеркну — от того, что автор задает константу в преамбуле программы, а не непосредственно в операторе, «обычная уличная магия» никуда не девается. Дело в том, что мы должны явно предъявить читателю метод формирования конкретного значения, а не создать синоним для значения, полученного неизвестным путем. Можно, конечно, написать комментарий к строке со значением, в котором указать расчетную формулу, но лучше явно использовать расчетную формулу при формировании константы и тогда комментарий просто не понадобится (разумеется, при говорящих именах применяемых констант). Так и сделано в нижеприведенном тексте, причем обратите внимание, что мы проводим преобразование в целое число только в последний момент и правильно округляем, что позволяет не потерять точность результата.

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

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

.equ delay=15
TX_Byte:
        cli
        sec             ; потребуется для стоп-бита
        clt             ; устанавливаем старт-бит
TransBit:               ; собственно формирование бита
        in      r17,port
        bld     r17,Tx_line
        out     port,r17
Delay_TX:               ; формируем длину битового интервала
        ldi     r17,delay
Do_Delay_TX:
        nop
        dec     r17
        brne    Do_Delay_TX     
TX_Bit:
        bst     r16,0
        ror     r16
        clc             
        brne  TransBit ; есть что передавать
        brcs    TransBit ; передаем стоп-бит
Exit_Transmit:
        Sei


Обратим внимание, как мы используем передаваемый байт совместно с битом переноса в качестве счетчика битов, красивое решение, но обладает одним недостатком — длительность последнего бита данных будет несколько (на 2 такта) длиннее остальных, за счет задержки перехода. Если бы речь шла о стоповом бите, то «наплевать и забыть», поскольку минимальный интервал между передачами нам не задан, но это значащий бит, да мы еще только что критиковали исходную программу за подобное поведение. Не будем уподобляться библейскому персонажу из притчи о соринке в чужом глазу и примем меры к устранению. Данное явление легко можно было бы компенсировать введением задержки на 2 такта, но увеличится длина кода, а это ключевой параметр. Поэтому пойдем классическим путем и поменяем время на память — используем отдельный регистр для организации счетчика передаваемых битов, и получим совершенно одинаковые битовые интервалы при том же размере кода.

Следующее улучшение связано с формированием длительности битового интервала, которое в исходной программе выполняется на 4х тактовом цикле. Если мы сделаем его 3х тактовым (минимально возможным в данном МК), то сможем сэкономить один байт кода и потенциально можем улучшить точностные параметры, поскольку дискретность задержки станет меньше (отклонение не превосходит половины размера дискрета при правильном округлении). Но следует иметь в виду, что в конкретном случае мы можем и потерять точность, все зависит от исходных данных. Еще одно обстоятельство, которое могло повлиять на выбор именно такой длительности цикла — максимальный размер задержки при байтовом счетчике составляет 256 значений — для имеющегося варианта можно использовать скорости от 9600 бод и выше, а вот с 3х цикловой задержкой это невозможно. Было бы весьма неплохо отразить данное обстоятельство (минимально допустимая скорость порта) в комментариях к программе и заодно вывести предупреждающее сообщение в случае нарушения данного требования. Ну и внести соответствующие модификации в макросы формирования параметра для формирования задержки, не забывая использовать «говорящие» имена для обозначения переменных.

.equ Freq = 8000000
.equ BaudRate = 115200
.equ PayLoad = 9  ; количество тактов вне цикла
.equ CycleTime = 3 ;количество тактов в цикле
.equ delay=((Freq*2/BaudRate - PayLoad*2)+CycleTime)/(CycleTime*2)
TX_Byte:
        cli
        ldi r18,10
        sec             ; потребуется для стоп-бита
        clt             ; устанавливаем старт-бит
TransBit:
        in      r17,port
        bld     r17,Tx_line
        out     port,r17
Delay_TX:
        ldi     r17,delay
Do_Delay_TX:
        dec     r17
        brne    Do_Delay_TX     
TX_Bit:
        bst     r16,0
        ror     r16
        dec r18
        brne    TransBit
Exit_Transmit:
        sei


Теперь посмотрим на результат — размер кода уменьшился с 20 до 16 слов (если учитывать только собственно передачу, то еще более разительно — с 18 до 14, джиттер фронтов исчез (конечно, только та компонента джиттера, которая обусловлена особенностями программы, на аппаратную составляющую мы не посягаем), точность выдерживания временных интервалов улучшилась, программа стала нагляднее и проще в понимании (за счет комментариев, поскольку даже хорошо написанная программа на ассемблере само-документируемой, как правило, не является).

Вывод из последней части — если мы собираемся ставить мировые рекорды в программировании на ассемблере, то нам надлежит весьма глубоко изучить архитектуру конкретного МК и применять полученные знания для получения идеального результата, обращая внимание на все тонкости.

Ну и в заключение — задача написания кода минимального размера в наше время выглядит несколько надуманной, но, совершенно неожиданно, получает подтверждение своей жизненности. В конце прошлого года (2016, вот как долго ждал своей очереди этот пост) анонсирован новый МК из семейства MSP430, который наряду с уникально низкой ценой (26 центов — ждем появления китайских устройств на его основе) имеет и уникально малый размер памяти программ — 512 байт (нет, я не ошибся, буквы «к» сразу после цифры нет). Так что размер кода может оказаться критичным при использовании данного прибора, да и вообще написание подобных экстремальных программ требует углубленного изучения МК, а «труд уже сам по себе есть благо».

© Habrahabr.ru