Глава 15 Вычисления в МК и использование АЦП

В давние, давние времена компьютеры занимались только своими прямыми обязанностями: они считали.

Вадим Житников «Компьютеры, математика и свобода»

Обычные операции сложения и вычитания для 8-разрядных чисел (которые на поверку все равно оказываются 16-разрядными операциями) мы уже «проходили» в главе 13. Здесь мы попытаемся понять, как в 8-разрядном МК можно работать с многоразрядными и дробными величинами, в том числе осуществлять операции деления и умножения.

Подумаем сначала, — ас какими числами приходится работать на практике? Если говорить о целых числах, то большинство реальных нужд вполне укладывается в трехбайтовое значение (224 или 16 777 216). Этот же диапазон дает достаточное для практики значение точности (7 десятичных разрядов), большие числа обычно округляют и записывают в виде «мантисса — порядок», с добавкой степени 10. То же касается и разрядности дробных чисел. При этом следует учесть, что любая арифметическая операция дает погрешности округления, которые могут накапливаться. Углубляться в этот достаточно сложный вопрос мы не будем, нам достаточно того факта, что оперируя с трехбайтовыми числами, как результатом обычных операций деления и умножения, мы не выходим за пределы погрешности в шестом знаке, что значительно превышает разрешение рядовых 10-разрядных АЦП — с числом градаций 1024, означающем ошибку уже в третьем, максимум (в реальности так не бывает) в четвертом знаке.

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

Процедуры умножения для многобайтовых чисел

Чтобы более четко разделить задачи с различной разрядностью, приводимые в «аппнотах» процедуры для наших целей придется творчески переработать, тем более, что в них имеются ошибки. Ошибки эти мы разбирать подробно не будем, да и вообще не будем останавливаться на внутреннем механизме работы таких процедур — к чему углубляться в теорию вычислений? Желающих я отсылаю к упоминавшемуся фундаментальному труду [10]. Здесь мы займемся лишь практическими алгоритмами.

На рис. 15.1 приведена блок-схема алгоритма перемножения двух 16-разрядных беззнаковых чисел (MPY16U), скопированная из pdf-файла Application note AVR200. Жирным выделено необходимое исправление, то же следует сделать в алгоритме перемножения двух 8-разрядных чисел (MPY8U).

Ассемблерный текст процедур умножения можно найти в той же «аппноте», только представленной в виде asm-файла (его можно скачать с сайта Atmel по адресу, приведенному в главе 13, если в таблице с перечнем Application notes щелкнуть по значку диска, а не PDF-документа). Для внесения изменений найдите в тексте процедуру mpy16u, переставьте метку m16u_1 вместо команды brcc noad8, перед которой она стоит, двумя командами ранее — перед командой lsr mp16uH. Аналогичную манипуляцию надо проделать в процедуре mpy8u с меткой m8u_1, если вы желаете воспользоваться 8-разрядным умножением.

Рассмотрите внимательно текст процедуры 16-разрядного умножения в «аппноте» 200 и обратите внимание на две особенности. Во-первых, для представления результата в общем случае требуется 4 байта (регистра), т. е. результат будет 32-разрядным числом, что понятно. Поэтому, во-вторых, разработчики используют в целях экономии одни и те же регистры для множителя и младших байтов результата, но называют их разными именами. Такой прием мы уже обсуждали, и ясно, для чего он применяется — для большей внятности текста процедуры. И тем не менее, вообще говоря, это недопустимо — слишком легко забыть про то, что под разными именами кроется один и тот же регистр, и использовать его еще где-то (не будете же вы, в самом деле, держать неиспользуемыми аж целых 7 регистров только для того, чтобы один раз где-то что-то перемножить, правда?). Потому мы в дальнейшем обойдемся без таких фокусов — лучше написать внятные комментарии, а имена оставить уникальные, даже если они и не окажутся «говорящими».

Рис. 15.1. Процедура перемножения двух 16-разрядных чисел из pdf-файла Atmel Application note AVR200 с исправленной ошибкой

На практике, как мы говорили ранее, 32-разрядное число (максимум 4 294 967 296, т. е. более 9 десятичных разрядов) с точки зрения точности в большинстве случаев избыточно. Если мы ограничимся 24 разрядами результата, то нам придется пожертвовать частью диапазона исходных чисел так, чтобы сумма двоичных разрядов сомножителей не превышала 24. Например, можно перемножать два 12-разрядных числа (в пределах 0—4095 каждое) или 10-разрядное (скажем, результат измерения АЦП) на 14-разрядный коэффициент (до 16 383). Так как при умножении точность не теряется, то этого оказывается более чем достаточным, чтобы обработать большинство практических величин.

Процедура перемножения двух таких величин в исходном 16-разрядном виде, с представлением результата в трехбайтовой форме может быть легко получена из исправленной нами процедуры MPY16U по «аппноте» 200, но я решил воспользоваться тем обстоятельством, что для контроллеров семейства Mega определены аппаратные операции умножения (в Приложении 4 я их не привожу). Тогда алгоритм сильно упрощается, причем он легко модифицируется как для 32-, так и для 24-разрядного результата. Таким образом, для Tuny и Classic по-прежнему следует пользоваться обычными процедурами из «аппноты» (исправленными), а алгоритм для Mega приведен в листинге 15.1 (в названиях исходных переменных отражен факт основного назначения такой процедуры — для умножения неких данных на некий коэффициент). Сокращения LSB и MSB, которые нам еще встретятся не раз, означают least (most) significant bit — младший (старший) значащий разряд, по-русски МЗР и СЗР соответственно.

Листинг 15.1

.def dataL = r4  ;multiplicand low byte

.def dataH = r5  ;multiplicand high byte

.def KoeffL = r2  ;multiplier low byte

.def koeffH = r3  ;multiplier high byte

.def temp = r16  ;result byte 0 (LSB — младший разряд)

.def temp2 = r17  ;result byte 1

.def temp3 = r18  ;result byte 2 (MSB — старший разряд)

;**********

;умножение двух 16-разрядных величин, только для Меда

;исходные величины dataH: dataL и KoeffH: KoeffL

;результат 3 байта temp2:temp1:temp;

;**********

Mu1616:

clr temp2  ;очистить старший

mul dataL,KoeffL  ;умножаем младшие

mov temp,r0  ;в r0 младший результата операции mu1

mov tempi,r1  ;в r01 старший результата операции mu1

mul dataH,KoeffL  ;умножаем старший на младший

add temp1,r1  ;в r0 младший результата операции mu1

adc temp2,r1  ;в r01 старший результата операции mu1

mul dataL,KoeffH  ;умножаем младший на старший

add temp1,r0  ;в r0 младший результата операции mu1

adc temp2,r01  ;в r01 старший результата операции mu1

mul dataH,KoeffH  ;умножаем старший на старший

add temp2,r0  ;4-й разряд нам тут не требуется, но он — в r01

ret

;**********

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

adc temp3,r01

Естественно, можно просто обозначить r01 через temp3, тогда и добавлять ничего не придется.

Процедуры деления для многобайтовых чисел

Деление — значительно более громоздкая процедура, чем умножение, требует больше регистров и занимает больше времени (MPY16U из «аппноты» занимает 153 такта, по уверению разработчиков, а аналогичная операция деления двух 16-разрядных чисел — от 235 до 251 тактов). Операции деления двух чисел (и для 8-, и для 16-разрядных) приведены в той же «аппноте» 200, и на этот раз без ошибок, но они не всегда удобны на практике: часто нам приходится делить результат какой-то ранее проведенной операции умножения или сложения, а он нередко выходит за пределы двух байтов.

Потому пришлось разрабатывать свои операции. Например, часто встречается необходимость вычислить среднее значение для уточнения результата по сумме отдельных измерений. Если даже само измерение укладывается в 16 разрядов, то сумма нескольких таких результатов уже должна занимать 3 байта. В то же время делитель — число измерений — может быть и относительно небольшим, и укладываться в один байт. В листинге 15.2 я привожу процедуру деления 32-разрядных чисел (на всякий случай) на однобайтное число, которая представляет собой модификацию оригинальной процедуры из Application notes 200. Как и ранее, названия переменных отражают назначение процедуры — деление состояния некоего 4-байтового счетчика на число циклов счета (определения регистров-переменных не приводятся, комментарии сохранены из оригинального текста «аппноты», они соответствуют блок-схеме алгоритма, размещенной в pdf-файле).

Листинг 15.2

;********

;div32x8» — 32/8 деление беззнаковых чисел

;Делимое и результат в count_HH (старший), countTH,

;countTM, countTL (младший)

;делитель в cikle

;требуется четыре временных регистра dremL — dremHH

;из диапазона r16-r31

;для хранения остатка

;********

div32x8:

        clr dremL  ;clear remainder Low byte

        clr dremM  ;clear remainder

        clr dremH  ;clear remainder

        sub dremHH,dremHH  ;clear remainder High byte and carry

        ldi cnt,33  ;init loop counter

d_1:  rol countTL  ;shift left dividend

        rol countTM

        rol countTH

        rol count_HH

        dec cnt  ;decrement counter

        brne d_2  ;if done

        ret  ;return

d_2:  rol dremL  ;shift dividend into remainder

        rol dremM

        rol dremH

        rol dremHH

        sub dremL,cikle  ;remainder = remainder — divisor

        sbci dremM,0

        sbci dremH,0

        sbci dremHH,0

        brcc d_3  ;if result negative

        add dremL,cikle  ;restore remainder

        clr temp

        adc dremM,temp

        adc dremH,temp

        adc dremHH,temp

       clc  ;clear carry to be shifted into result

       rjmp d_1  ;else

d_3: sec  ;set carry to be shifted into result rjmp d_1

;******** конец 32x8

Многие подобные задачи на деление удается решить значительно более простым и менее громоздким методом, если заранее подгадать так, чтобы делитель оказался кратным степени двойки. Тогда все деление сводится, как мы знаем, к сдвигу разрядов вправо столько раз, какова степень двойки. Для примера предположим, что мы некую величину измерили 64 раза, и хотим узнать среднее. Пусть сумма укладывается в 2 байта, тогда вся процедура деления будет такой:

;деление на 64

       clr count_data  ;счетчик до 6

div64L:

       lsr dataH  ;сдвинули старший

       ror dataL  ;сдвинули младший с переносом

       inc count_data

       cpi count_data,6

       brne div64L

He правда ли, гораздо изящнее и понятнее? Попробуем от радости решить задачку, которая на первый взгляд требует, по крайней мере, знания высшей алгебры — умножить некое число на дробный коэффициент (вещественное число с «плавающей запятой»). Теоретически для этого требуется представить исходные числа в виде «мантисса — порядок», сложить порядки и перемножить мантиссы (см. [10]). Нам же неохота возиться с этим представлением, т. к. мы не проектируем универсальный компьютер, и в подавляющем большинстве реальных задач все конечные результаты у нас представляют собой целые числа.

На самом деле эта задача решается очень просто, если ее свести к последовательному умножению и делению целых чисел, представив реальное число в виде целой дроби с оговоренной точностью. Например, число 0,48576 можно представить как 48 576/100 000. И если нам требуется на такой коэффициент умножить, к примеру, результат какого-то измерения, равный 976, то тогда можно действовать, не выходя за рамки диапазона целых чисел: сначала умножить 976 на 48 576 (получится заведомо целое число 47 410 176), а потом поделить результат на 105, чисто механически перенеся запятую на пять разрядов. Получится 474,10176 или, если отбросить дробную часть, 474. Большая точность нам и не требуется, т. к. и исходное число было трехразрядным.

Улавливаете, к чему я клоню? С числами в десятичном виде хорошо работать руками, просто отсчитывая разряды. Нам же делить на сто тысяч в 8-разрядном МК крайне неудобно — представляете, насколько громоздкая процедура получится? Наше ноу-хау будет состоять в том, что мы для того, чтобы «вогнать» дробное число в целый диапазон, будем использовать не десятичную дробь, а двоичную — деление тогда сведется к описанной «механической» процедуре сдвига, аналогичной переносу запятой в десятичном виде.

Итак, чтобы умножить 976 на коэффициент 0,48576, следует сначала последний вручную умножить, например, на 216 = 65 536, и тем самым получить числитель соответствующей двоичной дроби (у которой знаменатель равен 65 536) — он будет равен 31834,76736, или, с округлением до целого 31 835. Такой точности хватит, если исходные числа не выходят, как у нас, за пределы трех-четырех десятичных разрядов. Теперь мы в контроллере должны умножить исходную величину 976 на константу 31 835 и полученное число 31 070 960 (оно оказывается 4-байтовым — S01DA1AF0, потому нашу Mui6i6 придется чуть модифицировать, как сказано при ее описании ранее) сдвигаем на 16 разрядов вправо:

;в ddHH: ddH: ddM: ddL число $01DA1AF0,

;его надо сдвинуть на 16 разрядов

        сlrг cnt

div16L:  ;деление на 65536

        lsr ddHH  ;сдвинули старший

        ror ddH  ;сдвинули 3-й

        rоr ddM  ;сдвинули 2-й

        rоr ddL  ;сдвинули младший

        inc cnt

        cpi cnt,16

        brne divl6L  ;сдвинули-поделили на 2 в 16

В результате, как вы можете легко проверить, старшие байты будут нулевыми, а в ddM:ddL окажется число 474 — тот же самый результат. Но и это еще не все, такая процедура приведена скорее для иллюстрации общего принципа. Ее можно еще больше упростить, если обратить внимание на то, что сдвиг на восемь разрядов есть просто перенос значения одного байта в соседний (в старший, если сдвиг влево, и в младший — если вправо). Итого получится, что для сдвига на 16 разрядов вправо нам надо всего-навсего отбросить два младших байта, и взять из исходного числа два старших ddHH:ddH — это и будет результат. Проверьте — S01DA и есть 474. Никаких других действий вообще не требуется!

Если степень знаменателя дроби, как в данном случае, кратна 8, то действительно никакого деления, даже в виде сдвига, не требуется, но чаще всего это не так. Однако и тут приведенный принцип может помочь: например, при делении на 215 (что может потребоваться, если, например, в нашем примере константа больше единицы) вместо пятнадцати кратного сдвига вправо результат можно сдвинуть на один разряд влево (фактически умножив число на два), а потом уже выделить из него старшие два байта. Итого процедура будет состоять из четырех операций сдвига и займет четыре такта. А в виде циклического сдвига на 15, как ранее, нам требуется в каждом цикле сделать четыре операции сдвига и одну увеличения счетчика: 15 х 5 = 75 простых однотактных операций, и еще 15 операций сравнения, из которых 14 займут два такта — итого 104 такта. А решение «в лоб» на основе операций целочисленного деления в несколько раз превышало бы и эту величину. Существенная разница, правда? Вот такая специальная арифметика в МК.

Операции с числами в формате BCD

Это важная группа операций, ведь значительная часть устройств на основе МК предназначена для демонстрации чисел в том или ином виде. Это, естественно, можно делать только в десятичном формате, в то время как внутреннее представление чисел в регистрах двоичное. В некоторых микропроцессорных системах (в их число входит семейство x51 от Intel и, кстати, x86) даже имеется специальная инструкция для т. н. двоично-десятичной коррекции, которая позволяет получить верный результат при сложении двоично-десятичных чисел в упакованном формате (о BCD-форматах см. главу 7). Но в системе команд AVR такой инструкции нет, и, в общем-то, она все равно не очень-то полезна, т. к. математические операции в любом случае удобнее выполнять в «родной» двоичной форме, а для представления на дисплее числа так или иначе приходится «распаковывать». В ПК этим незаметно для пользователя занимаются процедуры на языках высокого уровня (да так успешно, что приходится скорее озадачиваться обратной проблемой — представлением десятичных чисел в двоичной/шестнадцатеричной форме), ну а на уровне ассемблера десятичные преобразования приходится делать, что называется, ручками.

Заметки на полях

Традиционная область использования команд двоично-десятичной коррекции, в том числе и в процессорах х86 — манипуляции со значением времени, полученным из микросхем RTC, в которых часы, минуты и секунды всегда хранятся в упакованном BCD-формате. Как вы увидите далее, такой формат хранения довольно удобен на практике. Однако область применения микроконтроллерных систем далеко не исчерпывается подсчетом и демонстрацией времени, потому нам придется выйти за рамки однобайтовых кодов, для которых, собственно, инструкция коррекции и создавалась. Уже для двухбайтовых чисел ее применение вызывает только лишние сложности.

В области BCD-преобразований есть три основные задачи:

• Преобразование двоичного/шестнадцатеричного числа в упакованный BCD-формат.

• Распаковка упакованного BCD-формата для непосредственного представления десятичных чисел с целью их вывода на дисплей.

• Обратное преобразование упакованного BCD-формата в двоичный/шестнадцатеричный для выполнения над ним, например, арифметических действий.

Некоторые процедуры для этой цели приведены в фирменной Application notes 204. При их использовании нужно учесть ряд моментов. Так, процедура bin2BCD8 для преобразования однобайтового числа в BCD работает только для чисел от 0 до 99 (для больших чисел нужен еще один байт, точнее, тетрада — в ней будет храниться старший разряд). В «аппноте» процедура представлена в универсальном виде, пригодном (при небольшой модификации) и для получения упакованного BCD, и для изначально распакованного (результат в двух отдельных байтах). Чтобы не путаться, приведу здесь ее вариант (листинг 15.3), который заодно более экономичный по количеству используемых регистров. Исходное hex-число содержится в регистре temp, распакованный результат — в temp1: temp. Как и в предыдущих случаях, комментарии сохранены из исходного текста.

Листинг 15.3

;преобразование 8-разрядного hex в неупакованный BCD

;вход hex= temp, выход BCD temp1-старший; temp — младший

;эта процедура работает только для исходного hex от 0 до 99

bin2bcd8:

       clr temp1  ;clear result MSD

bBCD8_1: subi temp,10  ;input = input — 10

       brcs bBCD8_2  ;abort if carry set

       inc temp1  ;inc MSD

       rjmp bBCD8_1  ;loop again

bBCD8_2:subi temp, — 10  ;compensate extra subtraction

ret

В листинге 15.4 приведено одно из решений обратной задачи — преобразования упакованного BCD (например, тех же значений часов, минут и секунд из RTC) в hex-число, после чего с ним можно производить арифметические действия. По сравнению с «фирменной» BCD2bin8 эта процедура хоть и немного длиннее, но понятнее и более предсказуема по времени выполнения («фирменная» может занимать от 3 до 48 тактов).

Листинг 15.4

;на входе в temp упакованное BCD-значение

;на выходе в temp hex-значение

;temp1 — вспомогательный регистр для промежуточного хранения temp

;действительна только для семейства Меgа

HEX_BCD:

         mov temp1, temp

         andi temp, 0b11110000  ; распаковываем — старший

         swap temp  ;старший в младшей тетраде

         mul temp,mult10  ;умножаем на 10, в r0 результат умножения

         mov temp,temp1  ;возвращаемся к исходному

         andi temp,0b00001111  ;младший

         add temp,r0  ;получили hex

ret

Более громоздкая задача — преобразование многоразрядных чисел. Преобразовывать BCD-числа, состоящие более чем из одного байта, обратно в НЕХ-формат приходится крайне редко, зато задача прямого преобразования возникает на каждом шагу. Я здесь приведу отсутствующую в «аппноте» 204 процедуру конвертации чисел, выходящих за рамки 16-разрядного диапазона. Например, такая задача может возникнуть при конструировании многоразрядных счетчиков. Ограничимся диапазоном в 7 десятичных знаков (9 999 999), тогда исходное число будет укладываться в 3 байта (24 разряда). В целях универсальности в процедуре, которая приводится далее в листинге 15.5, на выходе получается отдельно неупакованный (сразу для индикации) и упакованный десятичный формат. Сократить число необходимых регистров можно, если большую часть результатов сразу записывать в SRAM — в дальнейшем мы так и будем поступать, а здесь для наглядности работаем только с регистрами.

Отметим, что процедура bin2BCD24 сделана на основе «фирменной» bin2BCD16 и, как и последняя, использует хитрый прием с записью значений в регистры по адресам памяти: так можно производить над адресами разные манипуляции, меняя регистры (аналогично адресной арифметике в языке С). Как и в других случаях, сохранена часть оригинальных комментариев из исходной «фирменной» процедуры.

Листинг 15.5

;процедура преобразования 3-байтового hex в упакованный (4 регистра)

;и неупакованный (7 регистров) BCD

;исходное значение в регистрах

.def Count0 = r25

.def Count1 = r26

.def Count2 = r27

;на выходе упакованный BCD в регистрах

.def tBCD0 =r13  ;BCD value digits 1 and 0

.def tBCD1 =r14  ;BCD value digits 3 and 2

.def tBCD2 =r15  ;BCD value digit 4,5

.def tBCD3 =r16  ;BCD value digit 6

; на выходе неупакованный BCD в регистрах

.def N1 =r1  ;младший

.def N2 =r2

.def N3 =r3

.def N4 =r4

.def N5 =r5

.def N6 =r6

.def N7 =r7  ;старший

;вспомогательные регистры

.def cnt16a =r18  ;счетчик цикла

.def tmp16a =r19  ;временное значение

;адреса регистров в памяти

.equ AtBCD0 =13  ;address of tBCD0

.equ AtBCD3 =16  ;address of tBCD3

bin2BCD24:

         ldi cnt16a,24  ;Init loop counter clr tBCD3

         clr tBCD2  ;clear result (4 bytes)

         clr tBCD1

         clr tBCD0

         clr ZH  ;clear ZH (not needed for AT90Sxx0x)

bBCDx_1: lsl Count0  ;shift input value

         rol Count1  ;through all bytes

         rol Count2  ;through all bytes

         rol tBCD0

         rol tBCD1

         rol tBCD2

         rol tBCD3

         dec cnt16a  ;decrement loop counter

         brne bBCDx_2 ;if counter not zero

;распаковка

         ldi temp,0b00001111

         mov N1,tBCD0

         and N1,temp

         mov N2,tBCD0

         swap N2

         and N2,temp

         mov N3,tBCD1

         and N3,temp

         mov N4,tBCD1

         swap N4

         and N4,temp

         mov N5,tBCD2

         and N5,temp

         mov N6,tBCD2

         swap N6

         and N6,temp

         mov N7,tBCD3

         ret; return

bBCDx_2:ldi r30;AtBCD3+1  ;Z points to result MSB + 1

bBCDx_3:

          ld tmp16a, — Z  ;get (Z) with pre-decrement

          subi tmp16a, — $03  ;add 0x03

          sbrc tmp16a,3  ;if bit 3 not clear

          st Z,tmp16a  ;store back

          ld tmp16a,Z  ;get (Z)

          subi tmp16a, — $30  ;add 0x30

          sbrc tmp16a,7  ;if bit 7 not clear

          st Z,tmp16a  ;store back

          cpi ZL,AtBCD0  ;done all three?

          brne bBCDx_3  ;loop again if not

          rjmp bBCDx_1

Использование встроенного АЦП

Встроенный АЦП последовательного приближения входит в состав почти всех МК семейства Mega и большинства МК семейства Tuny, кроме простейших младших моделей. В семействе Classic был только один тип МК со встроенным АЦП — AT90S8535 — несколько доработанный вариант популярного AT90S8515. На примере его Mega-версии под названием ATmega8535 мы в дальнейшем и разберем работу встроенного АЦП, но сначала стоит сделать несколько общих замечаний.

Все встроенные АЦП многоканальные и 10-разрядные (за небольшим исключением — например, в ATmega8 из 6 каналов только четыре имеют разрешение 10 разрядов, а оставшиеся два — 8). Многоканальность означает, что имеется только одно ядро преобразователя, которое по желанию программиста может подключаться к одному из входов через аналоговый мультиплексор, наподобие разобранного в главе 8 561КП2. Если вы, как чаще всего и бывает, задействуете лишь часть входов, то остальные могут использоваться как обычные порты ввода/вывода. В разных моделях число каналов колеблется от 4 до 16, причем в некоторых из них выводы АЦП можно коммутировать попарно так, чтобы получить АЦП с дифференциальным входом (тогда измеряется разность напряжений между этими входами, а не абсолютное значение относительно «земли»). Добавим еще, что в некоторых моделях все или часть входов в дифференциальном режиме могут иметь добавочный коэффициент усиления (10, 20 или 200).

Все эти «примочки» дополнительно снижают и без того не слишком высокую точность АЦП, которая номинально составляет для несимметричного (недифференциального) входа ±2 LSB, плюс еще 0,5 LSB за счет нелинейности по всей шкале. Фактически такой АЦП с точки зрения абсолютной точности соответствует 8-разрядному. При соблюдении всех условий эту точность, впрочем, можно повысить, правда, условия довольно жесткие и включают в себя как «правильную» разводку выводов АЦП, так и, например, требование остановки цифровых узлов на время измерения, чтобы исключить наводки (специальный режим ADC Noise Reduction). В дифференциальном режиме есть свои специальные приемы повышения точности. В общем, как и всегда в таких случаях, для получения хорошего результата аналого-цифрового преобразования требуются определенные усилия и некоторый опыт.

Чтобы не углубляться в детали этого процесса и не загромождать программу, мы в дальнейшем поступим проще — предпримем ряд мер, чтобы обеспечить стабильность результата, а абсолютную ошибку скомпенсируем за счет калибровки, которая все равно потребуется. Для начала давайте посчитаем, какие, собственно, ошибки нас могут устроить. Максимально достижимая точность с помощью 10-разрядного преобразователя составляет 0,1 % (1/1024, или ±0,5 LSB) приведенной погрешности (т. е. погрешности от всей шкалы измерения). Для бытовых измерений это достаточно высокая величина, например, большинство портативных мультиметров имеют точность раз в пять хуже, обладая погрешностью порядка 0,5 %. АЦП в 10 разрядов может, например, обеспечить точность измерения температуры 0,1° для стоградусной шкалы (от -50 до +50°).

На самом деле нам такая точность не требуется — все равно термометр, подвешенный за окном или на стенке комнаты, никогда не покажет точную температуру, насколько бы он ни был точным сам по себе. На него будут влиять сквозняки, солнечные лучи, осветительные приборы, конвекция воздуха по нагретой стенке, тепловое излучение от оконных проемов — одним словом, все то, что определяет т. н. методическую погрешность. И для большинства бытовых измерений абсолютной точности в 8 разрядов (~0,4 %) хватает, как говорится, «выше крыши». Это относится не только к температуре, но и к подавляющему большинству других бытовых измерений. В большинстве случаев нам важно обеспечить не абсолютную точность, а, во-первых, стабильность показаний (чтобы в одинаковых условиях прибор показывал Одно и то же, и показания можно было бы сравнивать между собой), и, во-вторых, достаточную разрешающую способность, т. е. оптимальную цену деления прибора.

Заметки на полях

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

После такого экскурса в теорию измерений мы можем сделать вывод, что погрешности встроенного АЦП нам в большинстве случаев хватит и без особых ухищрений, важно только, чтобы показания не «дребезжали». Цифровые помехи со стороны ядра МК, как показывает опыт, имеют значительно меньшее влияние на результат, чем внешние, потому режим Noise Reduction нам не потребуется. Уменьшение дребезга почти до нуля достигается тем, что, во-первых, на входе канала ставится фильтр низкой частоты для устранения неизбежных в совмещенных аналого-цифровых схемах наводок на внешние цепи. Обычно достаточно керамического конденсатора порядка 0,1–1 мкФ, хотя в критичных случаях фирменное руководство рекомендует еще последовательно с ним включать индуктивность (порядка 10 мкГн), которую, добавим, для простоты можно заменить на резистор (несколько единиц или десятков килоом). Во-вторых, мы будем измерять несколько раз, и значения отдельных измерений усреднять — это самый эффективный способ повышения стабильности показаний, который я рекомендую для всех случаев, даже и тогда, когда соблюдены все фирменные рекомендации по повышению точности измерений (и в этом случае — особенно!). Это хоть и загромождает программу, но полученный эффект оправдывает такое усложнение.

Наконец, остановимся на источнике опорного напряжения, который, как мы знаем из главы 10, влияет на точность АЦП напрямую. Встроенные АЦП в МК AVR могут использовать три источника опорного напряжения на выбор: внешний, встроенный и напряжение питания аналоговой части (оно всегда в таких случаях отдельное от питания цифровой, хотя в простейших случаях это может быть один и тот же источник).

Встроенным источником опорного напряжения 2,56 В я пользоваться не рекомендую, прежде всего потому, что его величина может «гулять» в значительных пределах (до ±0,3 В), и зависит к тому же от напряжения питания, что в достаточной степени обессмысливает его использование. Единственным аргументом «за» является сама величина 2,56 В, что позволяет без сложных арифметических преобразований получать на выходе число измеряемых милливольт. Выходное значение АЦП (для несимметричного входа) выражается формулой:

N = 1024∙(Uвх/Uon).

Поэтому при Uon = 256 мВ, выходная величина N будет представлять учетверенное значение входного напряжения в милливольтах. Его легко привести к целому числу милливольт, просто сдвинув результат на два разряда вправо.

Однако такое измерение будет достаточно неточным и с искусственно пониженным разрешением (мы «легким движением руки» зачем-то превращаем 10-разрядный АЦП в 8-разрядный). Поэтому во всех случаях, когда требуется обеспечить абсолютную точность (например, при работе АЦП в составе мультиметра, где нас интересуют именно абсолютные значения в вольтах), следует использовать внешний точный источник опорного напряжения, тем более что они вполне доступны, хотя и не всегда дешевы (так, один из самых дорогих — прецизионный МАХ873 с напряжением 2,5 В имеет разброс напряжения 1,5–3 мВ при температурной стабильности 2,5–7 мВ во всем диапазоне температур, и стоит порядка 10 долл.). Важным преимуществом такого способа служит возможность выбора опорного напряжения из более удобных величин (например, 2,048 В), что позволит не терять разрешение встроенного АЦП.

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

Пару слов о самой организации измерений. АЦП последовательного приближения должен управляться определенной тактовой частотой, для чего в его состав входит делитель тактовой частоты самого МК, подобный предварительному делителю у таймеров. Устанавливать максимально возможную частоту (которая равна половине от тактовой) не рекомендуется, а лучше подбирать коэффициент деления так, чтобы тактовая частота АЦП укладывалась в промежуток от 50 до 200 кГц. Например, для тактовой частоты МК 4 МГц подойдет коэффициент деления 32, тогда частота АЦП составит 125 кГц. Преобразование может идти в непрерывном режиме (после окончания преобразования сразу начинается следующее), запускаться автоматически по некоторым прерываниям (не для всех типов AVR), или каждый раз запускаться по команде. Мы будем применять только последний «ручной» режим, т. к. нам для осреднения результатов тогда удобно точно отсчитывать число преобразований. В таком режиме на одно преобразование уходит 14 тактов, поэтому для приведенного примера с частотой 125 кГц время преобразования составит приблизительно 9 мс.

В любом случае по окончании процесса преобразования вызывается прерывание АЦП, и результат измерения читается из соответствующих регистров. Так как число 10-разрядное, то оно займет два байта, у которых старшие 6 разрядов равны нулю. Это удобно, т. к. мы можем без опасений суммировать до 64 (26) результатов, не привлекая дополнительных переменных, и затем простым сдвигом, как мы обсуждали ранее, вычислять среднее.

Измеритель температуры и давления на AVR

Для иллюстрации практического использования встроенного АЦП мы сконструируем измеритель температуры и атмосферного давления. Для измерения температуры мы заимствуем аналоговую часть схемы термометра из главы 10, перенеся ее сюда практически без изменений, за исключением того, что здесь мы запитаем схему от двуполярного источника ±5 В, чтобы обеспечить более удобный нам диапазон входных напряжений АЦП, начинающийся от 0 В в положительную сторону. Это позволит нам включить АЦП в несимметричном режиме, а не в дифференциальном, что упрощает схему и обеспечивает максимальное разрешение.

С датчиком атмосферного давления все еще проще — ряд фирм выпускают готовые датчики давления. Мы выберем барометрический датчик МРХ4115 фирмы Motorola, питающийся от напряжения 5 В и имеющий удобный диапазон выхода примерно от 0,2 до 4,6 В. Крупный недостаток таких датчиков с нашей точки зрения — то, что погрешность привязана к абсолютной шкале (в данном случае от 15 до 115 кПа, что составляет примерно 11 и 860 мм рт. ст. соответственно) и составляет не менее 1,5 %. Это без учета заводского разброса (устраняется калибровкой) и зависимости выходного напряжения от напряжения питания (устраняется путем относительных измерений — питанием АЦП и датчика от одного источника). Но даже при этих условиях 1,5 % от всей шкалы в 850 мм рт. ст. составит более 12 мм рт. ст. Это, конечно, недопустимо высокая погрешность для измерения атмосферного давления, которое на практике меняется в десятикратно меньших пределах — для большей части России, кроме горных местностей, можно выбирать диапазон от 700 до 800 мм рт. ст., даже с запасом. На самом деле это не должно нас пугать — как показал опыт, такой диапазон нас устраивает с точки зрения разрешения (одному мм рт. ст. будет соответствовать около одного разряда АЦП), а стабильность датчика оказывается вполне на высоте и обеспечивает при надлежащей калибровке разброс в пределах ±1 мм рт. ст.

При этом учтем, что большая абсолютная точность нам не требуется, как и в случае температуры — для небольших высот над уровнем моря можно считать, что при изменении высоты на каждые 10 м давление меняется примерно на 1 мм рт. ст., так что в пределах такого города, как Москва, с естественными перепадами высот 50 и более метров, оно само по себе будет «гулять» в пределах 5 мм рт. ст., даже без учета этажности зданий. И нам все равно целесообразно будет подогнать результат «по месту» так, чтобы не иметь крупных расхождений с прогнозом погоды по телевизору, иначе от показаний прибора будет мало пользы.

Схема

Схема такого прибора будет выглядеть так, как показано на рис. 15.2.

Рис. 15.2. Схема измерителя температуры и давления на МК ATmega8535

Чтобы не загромождать схему, здесь не показан узел индикации, т. к. он аналогичен тому, что используется в часах из главы 14, за исключением того, что должен содержать не четыре, а шесть разрядов (показания в формате «33,3»° и «760» мм рт. ст.). К ним можно добавить постоянно горящие индикаторы, показывающие единицы измерения (см. рис. 15.3, где они изготовлены на основе шестнадцатисегментных индикаторов типа PSA-05).

Рис. 15.3. Расположение индикаторов измерителя температуры и давления

Так как здесь выводов портов хватает, то можно назначить для управления разряды подряд (например, разряды порта С от РC0 до РС6 для управления сегментами и порта В от РВ0 до РВ5 для управления разрядами) и использовать для вывода цифры прием с формированием маски в виде констант (см. главу 13), что заметно сократит программу. Кроме того, надо не забыть знак температуры, который удобно изготовить из отдельного плоского светодиода. В остальном принцип индикации точно такой же, как в часах, и мы остановимся на подробностях чуть далее, когда будем разбирать программу.

Не показан на схеме и программирующий разъем, который полностью одинаков для любой схемы на AVR и показан на рис. 13.4 и 15.2 (соответствующие выводы для ATmega8535 подписаны на схеме рис. 15.2). То, что вывод MOSI (вывод 6) совпадает с выводом индикации единиц давления, вас смущать уже не должно. Однако незадействованные в других функциях выводы программирования (в данном случае MISO и SLK, выводы 7 и 8) следует подсоединить к питанию +5 Вц «подтягивающими» резисторами номиналом от 1 до 10 кОм (на схеме не показаны), так же, как и вывод Reset, только, естественно, без каких-либо конденсаторов (на схеме для вывода Reset указан номинал резистора 5,1 кОм). Как и RC-цепочка для Reset, «подтягивающие» резисторы для выводов программирования в принципе не требуются, однако их следует устанавливать. В тех случаях, когда схема представляет собой временный макет, без этих деталей можно обойтись, однако в работающей схеме без них могут быть неприятности, о чем мы уже говорили в главе 12. Если разъем программирования вообще не предусматривается, то устанавливать резисторы к выводам программирования не нужно.

Схема источника питания показана на рис. 15.4.

Рис. 15.4. Схема источника питания для измерителя температуры и давления

Измеритель имеет четыре питания (+5 Вц, ±5 Ва и +12 В для индикации) и три «земли», причем обычным значком «» здесь обозначена аналоговая «земля» CNDa. Линия цифровой «земли» обозначена GNDц, кроме этого, имеется еще общий провод индикаторов GNDи. Все три «земли» соединяются только на плате источника питания. Отмечу, что готовый трансформатор с характеристиками, указанными на схеме, вы можете не найти. Поэтому смело выбирайте тороидальный трансформатор мощностью порядка 10–15 Вт на напряжение вторичной обмотки 10–14 В (для индикаторов), измерьте на нем число витков на вольт (как описано в главе 4), и домотайте три одинаковых обмотки на 7–8 В каждая поверх существующих, проводом не меньше, чем 0,3 мм в диаметре. Удобнее всего их мотать одновременно сложенным втрое проводом заранее рассчитанной длины.

Теперь немного разберемся с температурой. Сопротивление датчика составляет 760 Ом при 0 °C (~=610 Ом при -50°) и имеет крутизну примерно 3 Ом/° (о датчике см. главу 10). Величины резисторов в аналоговой части измерителя подогнаны так, чтобы обеспечить ток через датчик 1,3 мА. Таким образом напряжение на датчике в диапазоне температур от -50° до +50 °C будет меняться на 400 мВ, т. е. на выходе дифференциального усилителя (с учетом его коэффициента усиления около 12) диапазон напряжений составит примерно 4,9 В. Таким образом мы будем использовать весь диапазон АЦП (от 0 до Uon) в полной мере с некоторым запасом. Резистор R4 устанавливает нижнюю границу диапазона, и здесь его нужно выбирать равным не сопротивлению датчика при 0°, как в схеме по рис. 10.8, а его сопротивлению при минимальной требуемой температуре. При указанных на схеме номиналах нижняя граница диапазона температур будет около -47°, а верхняя — около 55 °C. Для медного датчика с другим сопротивлением следует пересчитать коэффициент усиления усилителя (соответствующая формула приведена в главе 6, см. рис. 6.8). Это можно делать приблизительно — окончательную калибровку под реальный датчик мы будем производить путем изменения коэффициентов пересчета в программе МК.

Программа

Чтобы перейти к обсуждению непосредственно программы измерителя, нам нужно решить еще один принципиальный вопрос. Передаточная характеристика любого измерителя температуры, показывающего ее в градусах Цельсия, должна «ломаться» в нуле — ниже и выше абсолютные значения показаний возрастают. Так как мы тут действуем в области положительных напряжений, то этот вопрос придется решать самостоятельно (в АЦП типа 572ПВ2, напомним, определение абсолютной величины и индикация знака производилась автоматически).

Это несложно сделать, если представить формулу пересчета значений температуры в виде уравнения:

N = К∙(хZ),

где N — число на индикаторе, х — текущий код АЦП, Z — код АЦП, соответствующий нулю градусов Цельсия (при наших установках он должен соответствовать примерно середине диапазона).

Чтобы величина по данной формуле всегда получалась положительная, нам придется сначала определять, что больше — х или Z, и вычитать из большего меньшее. Заодно при этой операции сравнения мы определяем значение знака. Если мы предположим, что в регистрах AregH: AregL содержится значение текущего кода АЦП х, а в регистрах KoeffH: KoeffL значение коэффициента Z, то алгоритм будет выглядеть так, как иллюстрирует листинг 15.6.

Листинг 15.6

;вычисление знака:

         ср AregL,KoeffL  ;сравниваем х и Z

         срс AregH,KoeffH

         brsh Ь0

         ;если х меньше Z

         sub KoeffL,AregL

         sbc КоеffH,AregH

         mov AregL,KoeffL  ;меняем местами, чтобы температура

         mov AregH,KoeffH  ;оказалась опять в AregH: AregL

         sbi PortD,7  ;знак -

         rjmp m0

Ь0:  ;если x больше Z

         sub AregL,KoeffL

         sbc AregH,KоеffH

         cbi PortD,7  ;знак +

m0:

<умножение на коэффициент К>

Здесь разряд 7 порта D (вывод 21) управляет плоским светодиодом, который горит, если температура меньше нуля, и погашен в противном случае.

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

N = К∙(х + Z),

все величины будут находиться в положительной области.

Физический смысл коэффициента К — крутизна характеристики датчиков в координатах «входной код АЦП — число на индикаторах». Умножение на К мы будем производить описанным методом — через представление его в виде двоичной дроби (за основу берется 210 = 1024, этого будет достаточно). Вычисление ориентировочных значений коэффициентов К и Z поясняется далее, при описании процедуры калибровки.

Теперь можно окинуть взглядом собственно программу, которая целиком приведена в Приложении 5 (раздел «Программа измерителя температуры и давления»). Как вы видите из таблицы прерываний, здесь используется всего один, самый простой Timer 0, который срабатывает с частотой около 2000 раз в секунду. В его обработчике по метке TIM0 и заключена большая часть функциональности.

В каждом цикле сначала проверяется счетчик cRazr, который отсчитывает разряды индикаторов (от 0 до 5). В соответствии с его значением происходит формирование кода индицируемого знака (по алгоритму вызова константы-маски знака, описанному в главе 13) и затем на нужный разряд подается питание.

Заметки на полях

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

Интерес же представляет другой момент во всем этом — а нельзя ли было бы кардинально решить проблему, учитывая тот факт, что разряды считаются подряд (от нулевого до пятого), в регистре PortB они также расположены подряд, и ячейки SRAM, содержащие значения цифр, также идут подряд (начиная с TdH, см. секцию констант и определений)? Очень хочется как-то «свернуть» все шесть повторяющихся фрагментов в один, т. к. все равно все увязано со значением счетчика cRazr. Отсчитывать адрес, где хранится текущая цифра, несложно, просто прибавляя к начальному адресу (TdH) значение счетчика. В основном же это естественное желание упирается в тот факт, что невозможно простым способом перевести двоичное значение некоего регистра (в данном случае cRazr) в номер устанавливаемого бита в регистре PortB. Чтобы заменить «ручное» задание нужного бита в регистре PortB (см. пару команд ldi temp, i << RazrPdL/ out portb, temp) на автоматическое в соответствии со значением cRazr, понадобится двоично-десятичный дешифратор, подобный по функциям микросхеме 561ИД1 (см. главу 8), программная реализация которого будет еще более громоздкой, чем данный алгоритм. Мы еще вернемся к этому вопросу в главе 17, когда нам понадобится управлять большим количеством индикаторов.

После формирования цифры программа переходит к довольно запутанному, на первый взгляд, алгоритму работы АЦП. На самом деле он не так уж и сложен. Управляют этим процессом две переменных: счетчик циклов countCyk и счетчик преобразований count. Первый из них увеличивается на единицу каждый раз, когда происходит прерывание таймера. Когда его величина достигает 32 (т. е. когда устанавливается единица в бите 5, см. команду sbrs countCyk, 5), то значение счетчика сбрасывается для следующего цикла, и происходит запуск преобразования АЦП, причем для канала, соответствующего значению бита в регистре Flag, указывающего, что именно мы измеряем сейчас: температуру или давление. Таким образом измерения равномерно распределяются по времени.

Сами преобразования отсчитываются счетчиком count до 64 (т. е. цикл одного измерения занимает чуть более секунды: 32 х 64 = 2048 прерываний таймера, а в секунду их происходит примерно 1953). Когда это значение достигается, то мы переходим к обработке результатов по описанным ранее алгоритмам: сумма измерений делится на 64 (т. о. мы получаем среднее за секунду), затем вычитается или прибавляется значение «подставки», т. е. коэффициента Z, и полученная величина умножается на коэффициент К, точнее — на его целый эквивалент, полученный умножением на 1024. Произведение делится на это число и преобразуется к распакованному двоично-десятичному виду, отдельные цифры которого размещаются в памяти для последующей индикации. Как только очередной такой цикл заканчивается, меняется значение бита в регистре Flag, и таким образом давление и температура измеряются попеременно. В целом выходит, что значение каждой из величин меняется примерно раз в две секунды, и представляет собой среднее за половину этого периода.

Собственно результат измерения читается в прерывании АЦП (процедура по метке readADc), которое происходит автоматически по окончании каждого преобразования. В нем увеличивается значение счетчика count, извлекается из памяти предыдущее значение суммы показаний (в зависимости от регистра Flag — температуры или давления), считываются значения АЦП, суммируются и записываются обратно в память. Практически весь алгоритм мы описали — осталось только понять, как получить значения коэффициентов преобразования К и Z и затем выполнить точную калибровку.

Калибровка

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

Схема датчика температуры должна выдавать, как мы говорили, значение от 0 до 5 В в диапазоне температур примерно от -47 до 55 градусов. Следовательно, на 102 градуса у нас приходится 1024 градации АЦП, и крутизна характеристики составит 1020/1024 = 0,996 десятых градуса на единицу кода АЦП. Для вычислений в МК эту величину мы хотим умножить на 1024, так что можно было бы и не делить, ориентировочное значение коэффициента К так и будет 1020.

Величину Z, соответствующую 0 °C, вычислить также несложно. Мы полагаем, что нулю показаний соответствует температура -47°, тогда значение кода в нуле должно составить величину 470, поделенную на крутизну: 470/0,996 = 471.

Теперь разберемся с давлением. «Если повар нам не врет», то диапазон датчика, соответствующий изменению напряжения на его выходе от 0 до 4,6 В, составляет примерно 850 мм рт. ст. Это будет соответствовать изменению кодов примерно от 0 до 940 единиц, т. е. крутизна К равна 850/950 = 0,895 мм рт. ст. на единицу кода. В приведенном для наших расчетов виде это составит 0,895 х 1024 = 916. «Подставка» Z есть значение кода на нижней границе диапазона датчика, которая равна 11 мм рт. ст., соответственно, Z = 11/0,895 = 12 единиц. Полученные величины и «зашиваем» в программу.

После этого нужно сразу провести калибровку по температуре. Для этого следует запустить прибор и поместить датчик температуры в воду, записав для двух значений температур (как можно ближе к 0°, но не ниже его, и около 30–35 °C) показания датчика (/) и реальные значения температуры по образцовому термометру (О* Они, естественно, будут различаться. Для расчета новых (правильных) значений коэффициентов К' и Z' достаточно решить относительно них систему уравнений:

t'1 = К'(х1Z');

t1 = К(х1Z);

t'2 = К'(х2Z');

t2 = К(х2Z).

Здесь величины со штрихами относятся к правильным (новым) значениям, а без штрихов — к старым, причем значение коэффициента К нужно подставлять в изначальной форме (а не умноженным на 1024). Система четырех уравнений содержит четыре неизвестных, два из которых (величины кодов х1 и х2) вспомогательные. Если вы забыли, как решаются такие простые системы, купите любой справочник по математике для средней школы (или книжку по использованию Excel в алгебраических расчетах). Вычисленные значения (не забудьте К умножить на 1024) «забейте» в программу и перепрограммируйте контроллер.

Аналогично калибруется канал давления, только коэффициент Z в уравнениях не вычитается, а прибавляется к х. Но самое сложное здесь — получить действительные значения давления. Далеко не все научные лаборатории располагают образцовыми манометрами для измерения столь малых давлений с необходимой точностью. Поэтому самый простой, хотя и долгий метод — сравнивать показания датчика с данными по давлению, которые публикуются в Интернете. Есть сайты, которые публикуют погоду каждые 3 часа (это т. н. метеорологический интервал). Лучшие и наиболее популярные из них — weather.yandex.ru и gismeteo.ru. Причем лучше не ограничиваться данными одной какой-то службы, а обращаться к нескольким, отбрасывая явные ошибки и усредняя правдоподобные данные, с учетом того, что они публикуются с некоторым запаздыванием (отметьте показания прибора, например, в 9:00, а в Интернет лезьте примерно в 11:00). Данные радио и телевидения использовать нежелательно, т. к. текущие значения могут сообщаться с опозданием на полсуток, либо вообще отсутствовать, а по завтрашнему прогнозу, естественно, вы ничего не откалибруете.

Для получения двух точек дождитесь, пока давление на улице не станет достаточно низким, а затем, наоборот, высоким — экстремальные значения давления в регионе Москвы составляют примерно 720 и 770 мм рт. ст.[14] Чем дальше будут отстоять друг от друга значения, тем точнее калибровка. Для повышения точности можно усреднить коэффициенты, рассчитанные по нескольким парам значений давления, но это стоит делать только, если у вас хватит терпения вести наблюдения в течение нескольких месяцев, когда будет пройдено несколько минимумов и максимумов. Средние значения давления при калибровке лучше не учитывать, т. к. ошибка ее из-за узкого интервала и так достаточно велика.

Хранение констант в EEPROM

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

Логично придумать способ хранения констант, которые могут быть изменены в процессе эксплуатации, отдельно от программы. Для этой цели и служит энергонезависимая память данных, называемая EEPROM. Большинство МК семейства AVR имеют не менее 512 байт такой памяти, а младшие модели семейства Tuny — 64—128 байт. Для подавляющего большинства применений этого более чем достаточно. Не задерживаясь сейчас на вопросе, как осуществлять перезапись констант в процессе эксплуатации (этому вопросу будет посвящена глава 16), рассмотрим детали обращения с EEPROM.

Сохранность данных в EEPROM

Как мы уже говорили (см. главу 11), EEPROM и flash-память программ принципиально не отличаются, и предназначены для хранения данных в отсутствие питания. Однако между ними есть кардинальное различие: EEPROM может быть перезаписана в любой момент программой самого МК. В этом слабость всей системы: при снижении питания ниже определенных величин МК начинает совершать непредсказуемые операции, и EEPROM с большой вероятностью может быть повреждена. Для защиты от этой «напасти» (и вообще от выполнения каких-то операций, которые иногда могут навредить внешним устройствам) в AVR предусмотрена система BOD (см. главу 13), которая при снижении напряжения питания ниже определенного порога (4 или 2,7 В) «загоняет» МК в состояние сброса. Это помогает, но, как показывает опыт, для абсолютной защищенности данных в EEPROM, к сожалению, встроенной системы BOD недостаточно. Возможно, она недостаточно быстродействующая или в ней не слишком надежно фиксируется момент срабатывания, но факты свидетельствуют, что даже при включенной BOD данные все же могут быть повреждены.

Не исключено, что система BOD все время совершенствуется, но автор предпочитает не экспериментировать и использует самый надежный и проверенный способ с внешним монитором питания. Это небольшая микросхема (как правило, трехвыводная), которая при снижении питания ниже допустимого закорачивает свой выход на «землю». Если питание в пределах нормы, то выход находится в состоянии «разрыва» и никак не влияет на работу схемы. Присоединив этот выход к выводу Reset, мы получаем надежный предохранитель (рис. 15.5).

Рис. 15.5. Подсоединение внешнего монитора питания МС34064 к МК (схема из руководства фирмы Motorola)

Для напряжений питания 5 В один из самых популярных мониторов питания — микросхема МС34064, которая имеет встроенный порог срабатывания 4,6 В, выпускается в корпусе ТО-92 с гибкими выводами и обладает достаточно малым собственным потреблением (менее 0,5 мА). Время срабатывания у нее составляет при плавном снижении напряжения порядка 200 не, что достаточно для предотвращения выполнения «вредных» команд.

Если у вас питание автономное (от батарей), то к выбору монитора питания следует подходить довольно тщательно — так, чтобы не приводить схему в неработоспособное состояние тогда, когда батареи еще не исчерпали свой ресурс. При напряжении питания схемы 3,3 В пригодны приборы DS1816-10, MAX809S, при напряжении питания 3,0 В — DS1816-20 или MAX803R, а также некоторые другие.

Отметим, что рекомендуемый в фирменных описаниях способ защиты EEPROM вводом МК в состояние пониженного энергопотребления (см. главу 17) довольно сложно осуществить на практике. Если же вы все же умудритесь его задействовать, то следует учитывать, особенно в случае батарейного питания, что при резком снижении потребления напряжение источника немедленно повысится, что при относительно малом значении гистерезиса монитора питания (для МС34064 — 20 мВ) обязательно вызовет «дребезг» схемы. Увеличить гистерезис можно включением еще нескольких резисторов, но лучше обойтись вводом в режим сброса, как более простым и надежным способом.

Запись и чтение EEPROM

Запись и чтение данных в EEPROM осуществляется через специальные регистры ввода/вывода: регистр данных EEDR, регистр адреса EEAR (если объем EEPROM более 256 байт, то он делится на два — EEARH и EEARL) и регистр управления EECR. Основная особенность этого процесса — медленность процедуры записи, которая для разных моделей AVR может длиться от 2 до 9 мс, в тысячи раз дольше, чем выполнение обычных команд (обратим внимание, что в отличие от записи чтение осуществляется всего за один машинный цикл, даже быстрее, чем из обычной SRAM).

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

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

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

Листинг 15.7

WriteEEP:  ;в ZH:ZL — адрес EEPROM куда писать

;в temp — записываемый байт

       sbic EECR,EEWE  ;ждем очистки бита

       rjmp WriteEEP  ;разрешения записи EEWE

       out EEARH,ZH  ;старший адреса

       out EEARL,ZL  ;младший адреса

       out EEDR,temp  ;данные

           sbi EECR,EEMWE  ;установить флаг разрешения записи

           sbi EECR,EEWE  ;установить бит разрешения

ret  ;(конец WriteEEP)

Установленный нами бит разрешения EEWE в регистре управления сбросится автоматически, когда запись закончится — этого сброса мы и ожидаем в начале процедуры. Естественно, в самый первый раз никакого ожидания на самом деле не потребуется. На всякий случай то же самое рекомендуется делать и при чтении, но практически всегда (если только мы не читаем непосредственно после записи), это не будет задерживать программу дольше, чем на время выполнения команды sbic, т. е. на два машинных цикла. Так как при чтении не требуется устанавливать никаких флагов, то процедура получается несколько короче (листинг 15.8).

Листинг 15.8

ReadEEP:  ;в ZH: ZL — адрес откуда читать

;возврат temp — прочтенный байт

        sbic EECR,EEWE  ;ожидание очистки флага записи

        rjmp ReadEEP

        out EEARH,ZH  ;старший адреса

        out EEARL,ZL  ;младший адреса

        sbi EECR,EERE  ;бит чтения

        in temp,EEDR  ;чтение

ret  ;конец ReadEEP

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

Первичная запись констант в EEPROM

В принципе можно избежать процедуры записи вообще, если просто записать в EEPROM необходимые константы в процессе программирования. Это нужно делать отдельно от записи программы во Flash, с помощью специально подготовленного hex-файла. Но это ничем не будет отличаться от ситуации, когда константы хранятся в тексте программы, только программировать МК придется значительно дольше, особенно при отладке. Гораздо грамотнее будет не пожалеть труда и составить программу так, чтобы она сама записывала нужные константы «по умолчанию». Как это правильно сделать?

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

Для этого нам потребуется как-то узнавать, есть ли уже в EEPROM какие-то данные, или нет, и правильно ли они записаны. Можно учесть тот факт, что в пустой EEPROM всегда записаны одни единицы (любой считанный байт будет равен $FF), но в общем случае это ненадежно. Наиболее универсальный способ — выделить для этого один какой-то байт в EEPROM, и всегда придавать ему определенное значение, а при загрузке МК его проверять. Это не гарантирует 100 %-ной надежности при сбоях (т. к. данные в незащищенной EEPROM могут меняться произвольно, в том числе и с сохранением значения отдельных байтов), но мы будем считать, что от сбоев защищены «двойной броней» (из внешнего монитора питания и встроенной схемы BOD), и нам важно только распознать ситуацию, когда требуется первичная запись в еще не заполненную память. Приборы, которые я проектировал таким образом, работали, не выключаясь годами, без единого сбоя загруженных констант.

Итак, общая схема алгоритма такая: читаем контрольный байт из EEPROM, если он равен заданной величине (обычно я выбираю чередование единиц и нулей: $АА), то это значит, что коэффициенты уже записаны. Если же нет, то записывает значения «по умолчанию», в том числе и значение этого контрольного байта. Далее в любом случае переходим к процедуре чтения из EEPROM и перегрузки записанных констант в SRAM, откуда они при необходимости извлекаются точно так же, как ранее в процедурах расчета физических величин. Так мы сможем ничего не менять в основной программе, описанной ранее в этой главе, а лишь дописать некий текст в секции начальной загрузки.

Пусть значения коэффициентов записываются в EEPROM с самого начала (с адреса 0:0, в том же порядке, в котором они расположены в SRAM), а по адресу $10 записывается контрольный байт, равный $АА. Тогда в программе, приведенной в Приложении 5, в конце процедуры начальной загрузки по метке reset вместо всего фрагмента, начинающегося с заголовка «запись коэффициентов» до команды sei (обязательно перед ней, а не после) добавляется текст листинга 15.9.

Листинг 15.9

;чтение коэффициентов из EEPROM =====

       clr ZH  ;ст. адрес =0

       ldi ZL,$10  ;адрес контрольного байта

       rcall ReadEEP

       cpi temp,$AA  ;если он равен $AA

       breq mm_RK  ;то на чтение в ОЗУ

rcall ZapisK  ;иначе запись значений по умолчанию

mm_RK:  ;извлечение коэфф. из EEPROM в SRAM

        clr ZL  ;начальный адрес EEPROM 0:0

          ldi YL,tZH  ;начальный адрес SRAM, см. основной текст

LoopRK:

        rcall ReadEEP  ;читаем байт

        st Y+,temp  ;складываем в ОЗУ

        inc ZL  ;следующий адрес

        cpi ZL,8  ; всего 4 коэффициента, 8 байт

        brne LoopRK

Процедура записи коэффициентов по умолчанию, обозначенная как ZapisK (листинг 15.10), может быть вставлена в любом месте программы.

Листинг 15.10

ZapisK:

;запись предварительных коэффициентов по умолчанию

       clr ZH  ;с нулевого адреса в EEPR

       clr ZL

; Z tempr=471

       ldi temp,High(471)  ;ст.

       rcall WriteEEP

       inc ZL

       ldi temp,Low(471)  ;мл.

       rcall WriteEEP

       inc ZL

; К tempr=1020

       ldi temp,High(1020)  ;ст.

       rcall WriteEEP

       inc ZL

       ldi temp,Low(1020)  ;мл.

       rcall WriteEEP

       inc ZL

; Z press=12

       ldi temp,0x00  ;ст.

       rcall WriteEEP

       inc ZL

       ldi temp,12  ;мл.

       rcall WriteEEP

       inc ZL

; К prs=916

       ldi temp,High(916)  ;ct.

       rcall WriteEEP

       inc ZL

       ldi temp,Low(916)  ;мл.

       rcall WriteEEP

       ldi ZL,$10

       ldi temp,$AA  ;все Ok, записываем

       rcall WriteEEP  ;контрольный байт

ret

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

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

Кроме записи констант, наиболее часто EEPROM служит для хранения, например, заводского номера и названия прибора, фамилии конструктора-программиста или названия фирмы-изготовителя, и всякой другой полезной информации (ср. данные, которые извлекает операционная система ПК при подсоединении устройства plug&play, например, через USB). Можно заполнять различные поля, вплоть до серийного номера, и вести базу выпущенных экземпляров. Несложно сделать и так, чтобы эта информация выдавалась «наверх» автоматически при подсоединении прибора к компьютеру с загруженной программой, и текущие значения параметров выводились в отдельном окне — тогда можно обойтись без громоздкой и «прожорливой» индикации и получить компактный компьютерный «прибамбас». Только вот как все эти данные извлекать и при необходимости изменять, не затрагивая самой программы? Для этого существуют последовательные интерфейсы, к рассмотрению которых мы сейчас и перейдем.

Более 800 000 книг и аудиокниг! 📚

Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением

ПОЛУЧИТЬ ПОДАРОК