РадиоКот :: Термометр на микроконтроллере.
Например TDA7294

РадиоКот >Схемы >Цифровые устройства >Бытовая техника >

Теги статьи: Добавить тег

Термометр на микроконтроллере.

Автор: ARV
Опубликовано 06.06.2007

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

Итак, Обучалка прочитана, огоньки мигают, и лапы чешутся сварганить еще что-то, да не скопировать чужую прошивку, а наваять свою собственную. Идей в голове копошится много, но почему-то оказывается, что знаний не хватает. Обычное дело, просто надо продолжить обучение!
Предлагаю сделать это продолжение на следующем примере. В своих посылах будем ориентироваться на то, что 90% всех устройств на МК похожи по принципиальной схеме как близнецы-братья (не путать с Лениным и партией). Поэтому берем схему "Моддинга блока питания" за основу, безжалостно убираем из нее все, кроме индикаторов и связанных с ними резисторов, а в качестве компенсации добавляем датчик DS1820 (или аналогичный) и один резистор. В итоге схема приобретает следующий вид:

Схема градусника

Будем надеяться (и вскоре убедимся, что это так и есть), что это будет показывать температуру: То есть сделаем термометр. Пока что схема выглядит не самой красивой, но оставим ее в этом виде (думаю, те, кто уже повторил "моддинг" не станут выкусывать детали, а просто допаяют датчик, чтобы провести эксперименты). Теперь обдумаем, что и как должна делать наша программа, т.е. составим некое ТЗ (техническое задание).
Раз есть датчик с интерфейсом 1-Wire - программа должна уметь с ним работать. Температура у нас меняется, а значит надо производить периодические измерения и обновления информации на дисплее - значит, надо каким-то способом вести отсчет интервалов времени.
Раз есть индикаторы - программа должна ими управлять и (деваться некуда) делать это путем динамической индикации. Чтоб не было слишком просто реализуем сверхэкономичный по числу резисторов принцип посегментной индикации. Для простоты пока других функций не закладываем, но все-таки реализуем возможность работы нашей программы как с индикаторами с общим катодом, так и с общим анодом, а заодно с разным их количеством, как в устройстве-прототипе.
Для тех, у кого нетерпение превозмогает рассудок, имеются готовые прошивки архив proshivki.rar - они могут дальше не читать, а прямиком отправляться к программатору и паяльнику. Более терпеливые и обстоятельные изучают исходный код и продолжают набираться полезных навыков в программировании на ассемблере, читая статью. А нетерпеливые пусть бегут заниматься пайкой и прошивкой! К тому времени, когда мы с вами изучим статью и разберемся в программе, они как раз вернутся - спрашивать у нас, что там к чему.

Исходник состоит из нескольких файлов (архив source.rar):
1. termo.asm - основной модуль программы
2. 1-wire.asm - модуль для работы по интерфейсу 1-wire
3. ds1820.inc - описания команд для датчика DS1820
4. macros.inc - описание макросов
5. s-display.inc - описание констант для работы с индикаторами
Начинать изучение программы следует с основного модуля. Кстати, рекомендую для компиляции использовать AVR Studio 4 (при использовании других компиляторов может потребоваться небольшая правка программы).

Начинать изучение программы следует с основного модуля. Кстати, рекомендую для компиляции использовать AVR Studio 4 (при использовании других компиляторов может потребоваться небольшая правка программы).
Текст программы очень обильно прокомментирован, так что все должно быть понятно и так. Но, тем не менее, я считаю своим долгом обратить ваше внимание на некоторых нюансах написания программы, а так же характерных (почти типовых) приемах решения некоторых задач. Ведь, в конце концов, наша цель не содрать текст один в один, а понять, как и почему он сделан именно так, можно ли сделать иначе и т.п. - то есть научиться. Давайте же приступим.
Уверен, вы уже обратили внимание, что 70% текста программы - это комментарии. Не думайте, что мне было некуда девать время, и я писал многа букафф. Я так делаю всегда и призываю вас следовать этому примеру. Хорошие комментарии позволят уже в момент их написания лучше понимать, что же вы такое делаете, не говоря уже о том, что через месяц-другой вы сможете в написанном разобраться. Причем важно писать комментарии, поясняющие не то, что делает та или иная строка программы, а то, что позволит понять, почему эта строка написана, т.е. ход ваших мыслей. Что толку в комментарии "сложение r0 с константой"? А вот в "увеличим сигнал на величину поправки" куда больше смысла и пользы, не так ли?
Так же не следует пренебрегать описанием в комментариях характеристик аппаратуры, с которой программа работает, частоты кварцевого или иного генератора, настроек фьюзов и т.п.
Кроме комментариев существует еще один способ сделать программу понятнее - использование смысловых меток, констант, переменных и т.п. В ассемблере это делается с применением директив .EQU, .DEF и описанием меток в виде слов или аббревиатур. Например, увидев в тексте программы метку MainLoop трудно не догадаться, что тут начинается главный цикл программы - это куда понятнее, чем использовании ничего не говорящей о себе метки "m1".
Раз уж мы заговорили о директивах, познакомимся с еще некоторыми, которые встретятся вам буквально с первых строк. Рассмотрим участок нашей программы:

.equ VERS = 2 ; номер версии прошивки - см. ниже
.equ ON = 1
.equ OFF = 0

.IF VERS==3
    .equ INDS = 2
    .equ OK = ON
.ENDIF
.IF VERS==2
    .equ INDS = 1
    .equ OK = ON
.ENDIF

Первые три строчки вам знакомы - это директивы описания констант. А далее следуют директивы условной компиляции. Начинаются такие директивы с .IF, заканчиваются .ENDIF, а все, что между этими строчками - включается в текст программы только в том случае, если истинно выражение в строке .IF, если же выражение ложно - упомянутые строчки не участвуют в процессе компиляции. То есть в нашем случае при компиляции программы получится INDS=1 и OK=ON. А если мы изменим VERS=3, то при компиляции получится INDS=2 и OK=ON. Если вы внимательно посмотрите на весь код программы, вы найдете там немало директив условной компиляции - они помогают с минимальными усилиями осуществлять модификацию программы в зависимости от вашего желания. В частности, вышеупомянутые строки служат для того, чтобы упростить генерацию версий прошивки для разного количества индикаторов и типа индикаторов (с общим анодом или катодом). Как именно это происходит, мы вскоре разберемся.
Продвигаясь далее, мы встречаем еще одну интересную директиву:

.macro Send_1w
; вспомогательный макрос вывода в шину 1-Wire
    ldi Temp, @0    ; первый параметр
    mov r0, Temp
    rcall Send1w    ; выводим
.endmacro

Это директива макроопределения или просто макроса с именем Send_1w. Строки, находящиеся между .macro и .endmacro называются телом макроса. Это тело компилятор автоматически подставит в любое место программы, где будет ссылка на имя макроса. Директива эта как бы добавляет новую команду ассемблера Send_1w, только эта "команда" на самом деле может быть целой группой настоящих команд. Самое замечательное, что как и у обычных команд, у макроса могут быть параметры, причем не 1 или 2, а до 10-и. Параметры макроса нумеруются от 0 до 9, а в теле макроса можно использовать ссылку на параметр по его номеру - в нашем случае параметр только 1 (нулевой), а используем мы его в команде ldi Temp, @0 - @0 и есть ссылка на параметр. При компиляции тела макроса вместо @0 будет автоматически подставлен параметр, стоящий после имени макроса. То есть если мы в своей программе напишем так:

Send_1w CMD_1
Send_1w 0x0E

Компилятор сформирует на самом деле следующий код:

ldi Temp, CMD_1    ; первый параметр
mov r0, Temp
rcall Send1w
ldi Temp, 0x0E
mov r0, Temp
rcall Send1w

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

push Temp          ; сохраним вспомогательный регистр
in Temp, SREG   ; считаем содержимое SREG
push Temp          ; сохраним считанное значение

Чтобы не писать по три строки в начале каждого обработчика, а потом еще три в конце (ведь перед выходом из прерывания надо все восстановить в прежнем виде), можно оформить их в виде макроса - посмотрите файл macros.inc. Видите: теперь в начале обработчика вы пишите pushf, а в конце popf - и все! Программа стала красивее, понятнее и проще. Кстати, выделение частоиспользуемых макросов в отдельный файл - хорошая привычка, которая позволит вам сэкономить еще немного времени при разработке следующей программы: достаточно будет применить .include "macros.inc" (c указанием пути, где этот файл находится) - и однажды написанные макросы станут доступны в вашей программе.
Продвигаясь по тексту программы далее, вы увидите, что таблица векторов прерываний оформлена немного не так, как написано в обучалке. Не оспаривая допустимость "обучалочного" подхода, я все же настоятельно рекомендую использовать другой стиль - указание адреса вектора при помощи директивы .ORG. Несколько подряд идущих команд RJMP - не самое лучшее решение. Непременное условие корректного функционирования системы прерываний - это размещение каждого вектора по строго определенному адресу. Если вы используете не все, а только часть прерываний, разместить по нужным адресам команды RJMP не очень просто, если не применять директивы .ORG. Константы, которые я использовал для указания адресов обработчиков прерываний (OVF2addr и OVF0addr) определены в файле m8def.inc. Там же определены и адреса всех прочих прерываний, и наименования всех регистров МК, и все доступные биты и флаги - ну, вы уже в курсе. Как видите, я не использовал кучи RJMP на несуществующие обработчики "лишних" прерываний - и текст программы стал более наглядным и понятным. Но главное в том, что перепутать адрес вектора с предлагаемым подходом почти невозможно!

Теперь разберемся с интерфейсом 1-Wire. Подробности этого интерфейса я описывал в статье. Для работы с ним достаточно буквально нескольких базовых подпрограмм-функций: формирование сигнала RESET и получение PRESENCE PULSE, передача байта в шину и прием байта из шины. Дополнительно требуется уметь подсчитывать контрольную сумму. Так как устройств с этим интерфейсом много, логично все подпрограммы выделить в отдельный файл, чтобы в случае чего использовать его и в других программах и для других устройств. Так и сделано - см. файл 1-wire.asm. Благодаря обилию комментариев, разобраться с этим файлом для вас труда не составит. Обращу внимание на два аспекта: во-первых, интерфейс 1-wire очень зависим от точности временных интервалов, реализуемых программно, поэтому не при всех тактовых частотах МК он будет работоспособен. Во-вторых, применяя директивы условной компиляции, можно добиться либо максимального быстродействия вашей программы, либо минимального ее объема - делается это за счет разных вариантов реализации подпрограммы подсчета контрольной суммы. Все подпрограммы максимально универсальны, поэтому использовать их можно в программах для любых МК семейства AVR, а не только с ATMega8.

Для работы с устройствами 1-Wire требуется передавать им определенные команды и получать данные в ответ. Некоторые команды, характерные для нашего датчика DS1820 определены в файле ds1820.inc - их мы используем в нашей программе.
Общий алгоритм работы с датчиком следующий. Посылаем в шину импульс RESET и принимаем импульс PRESENCE. Если PRESENCE не получен - либо нет датчика, либо он не исправен - это ошибка. Если все хорошо - посылаем в датчик команды SKIP_ROM (датчик у нас один, и нам все равно, какой у него уникальный адрес) и CONVERT_T (старт измерения температуры). Измерение температуры длится долго, около 0,75 секунды, поэтому после необходимого ожидания начинаем получать измеренное значение следующим образом: снова выдаем RESET, SKIP_ROM, и посылаем команду считывания внутренних регистров датчика READ SCRATCHPAD, после чего мы должны принять от датчика 9 байтов: 8 его внутренних регистров и байт контрольной суммы. После приема надо проверить достоверность принятых данных: если принятый байт контрольной суммы равен тому, что вычислен в процессе приема - данные верны. Вычисление контрольной суммы выполняется прямо в подпрограмме чтения 9-и байтов ReadBytes1w, так что остается только после ее завершения проконтролировать вычисленное значение.
Датчик возвращает нам по команде READ SCRATCHPAD 9 байтов содержимого своих внутренних регистров, но собственно температура содержится в первых двух. Первый байт содержит код температуры в двоичном виде, причем самый младший бит соответствует 0,5 °С. Второй байт содержит либо 0хFF, если температура отрицательная, либо 0, если температура положительная. Так как индикатор у нас может состоять всего из 3-х разрядов, то десятые доли нам не потребуются, ограничимся только целыми градусами. Заметьте, что наш датчик способен измерять температуру от -55 до +125 °С, поэтому при выводе отрицательной температуры трех разрядов хватит и для индикации знака минус. Для вывода на "двойной" индикатор (6 разрядов) так же не будем использовать десятые доли, зато выведем символы "°С" правее температуры.
Чтобы из двухбайтного кода температуры получить однобайтное число, воспользуемся простейшим преобразованием - сдвигом вправо. Вот участок кода, который это делает:

  clt         ; сбросим флаг Т - знак числа
  lds Temp, Buffer+1     ; старший байт результата (знак)
  ror Temp        ; сдвинем его - теперь знак во флаге С
  brcc no_minus        ; если переноса не было - перейдем к метке
  set        ; установим знак принудительно
  clc        ; сбросим С, чтоб не мешал сдвигать байт
no_minus:
  lds Temp, Buffer    ; младший байт результата
  ror Temp    ; получим целое положительное число
            ; градусов сдвигом, а знак числа - во флаге Т

Таким способом мы сразу избавляемся от младшего бита, т.е. десятых долей, и разделяем результат на знак и модуль. Как же теперь вывести это число на индикатор? Воспользуемся вспомогательной подпрограммой PrintByteDec, которая организует преобразование двоичного числа в десятичное и одновременно выводит его в "экранную" область памяти (т.е. те ячейки, содержимое которых соответствует светящимся сегментам нашего индикатора).
Работа этой процедуры на удивление проста: выводимое число делится на 10, остаток (это число от 0 до 9) выводится в крайнюю правую позицию экранной области (т.е. в самый младший разряд индикатора), а частное проверяется на равенство нулю: если частное не ноль - оно снова делится на 10, остаток выводится в следующую справа позицию и т.д: То есть работа очень похожа на деление числа в "столбик". А вывод на индикатор осуществляется справа налево - так гораздо удобнее. В своей работе подпрограмма использует другую подпрограмму - ConvertToChar, которая преобразует число в символ, заносимый в экранную область. Под символом тут понимается некий байт, значение битов которого прямо соответствует включенным сегментам индикатора (см. файл s-display.inc). Для операции деления на 10 применяется примитивный алгоритм - метод вычитания (вспомните из арифметики за 1-й класс: умножение - это многократно повторенное сложение, а деление - многократно повторенное вычитание). Конечно, это далеко не самый оптимальный алгоритм, но в нашем случае вполне уместный. Замечу, что основной его недостаток - это низкая скорость выполнения, которая в нашем случае роли не играет. И еще одно: по алгоритму работы подпрограммы ConvertToChar можно выводить числа, состоящие более чем из одного байта - надо только использовать соответственно иную подпрограмму деления на 10 (и иметь достаточно места в экранной области).
После того, как модуль (то есть абсолютное значение) температуры выведен, остается только вывести знак минус (если температура у нас отрицательная). Для этого никаких специальных подпрограмм не требуется - просто в заносим нужный символ в нужное место экранной области. Наша динамическая индикация работает в "фоновом" режиме - по прерываниям от таймера, поэтому любое изменение содержимого экранной области немедленно отображается на индикаторе. Это гораздо удобнее, чем реализация динамической индикации в основном цикле программы - получается практически полная независимость процессов отображения информации и подготовки этой информации.
И вот настало время разобраться с самой динамической индикацией. Надеюсь, о том, как настраивать таймер, говорить не надо. Просто обратимся к обработчику прерывания по переполнению. Так как мы были заложниками ранее разработанной схемы, при которой общие выводы семисегментных индикаторов оказались разбросанными по разным портам и разным битам портов, то процедура динамической индикации вышла довольно путаной и некрасивой: Но что делать, будем вникать.
Логика работы обработчика прерывания довольно проста: сначала гасится то, что только что светилось, затем вычисляется новое состояние индикатора, после чего включается то, что должно светиться теперь - и это все, в следующем прерывании процесс повторяется. Для гашения и зажигания индикаторов применяем специальные макросы OffPort и OnPort, - первый гасит указанный бит заданного порта (через параметры), а второй зажигает. Зачем для этого применены макросы, когда все делается одной командой? А для универсальности. Посмотрите: в каждом макросе применена директива условной компиляции, которая в зависимости от типа примененных индикаторов (с общим анодом или катодом) включает в код нужную команду. Теперь понятно, для чего в самом начале программы была задана константа VERS?
Так как динамическая индикация подразумевает поочередное переключение разрядов индикатора, введена специальная ячейка-переменная IndPhase, содержимое которой определяет условный номер текущего разряда индикации. Сам процесс обновления отображаемого символа, в общем, не сложен: по значению IndPhase определяется адрес ячейки в экранной области, откуда берется очередной символ... Если б у нас была простая динамическая индикация, то мы просто выводили бы этот символ в наш порт D - и все дела. Но мы не ищем легких путей: у нас индикация посегментная, т.е. в каждом цикле индикации должен светиться только один сегмент из 8-и, значит, нам так просто не отделаться. Нужный символ дополнительно маскuруется перед выводом в порт D, то есть из него выделяется только один единственный бит, который соответствует очередному светящемуся сегменту, а все прочие биты имеют такое значение, что их сегменты не светятся. Для индикаторов с общим катодом свечению сегмента соответствует 1 в разряде порта, а для индикаторов с общим анодом - 0, поэтому снова применяем условную компиляцию для маскирования:

    ld TempT, Y    ; берем индицируемое значение
.if OK == ON
; для ОК сегменты выводятся так: 1 - горит
    and TempT, SegMask    ; выделяем (маскируем) индицируемый сегмент
    out PortD, TempT    ; выводим на индикатор
.else
; для ОА сегменты выводятся так: 0 - горит
    and TempT, SegMask    ; выделяем (маскируем) индицируемый сегмент
    ser Temp2
    eor TempT, Temp2
    out PortD, TempT    ; выводим на индикатор
.endif

Как видите, маскирование реализуется при помощи логических операций, разных для каждого типа индикаторов (для ОК это одна команда, а для ОА - целых три). После этого участка в порт D будет выведен байт, у которого только одни бит будет соответствовать светящемуся сегменту. Для выбора очередного сегмента использована дополнительная переменная SegMask, в которой содержится изначально 1, а по мере вызовов обработчика прерывания эта единичка сдвигается влево, таким образом, на каждом очередном цикле индикации мы имеем каждый раз новую маску сегмента.
После того, как все готово, остается снова зажечь индикатор - все очень похоже на то, как мы гасили его в начале. И завершает обработчик переустановка счетчика таймера.
Вот на чем я бы хотел задержать ваше внимание: для схем, у которых "общие" выводы индикаторов подключены к одному порту по порядку, следует использовать тот же принцип сдвига единички, чтобы выделить следующий рабочий разряд, а не огород из проверок значения IndPhase и многочисленных выводов в разные порты...
Остается только один пункт нашего ТЗ, о котором пока не сказано ни слова - организация счета времени и задержек в одну секунду. Для этих целей выделен таймер 1 микроконтроллера. В обработчике прерывания по его переполнению ведется счетчик прерываний - ячейка Delay. Как только эта ячейка обнулится - устанавливается специальный бит-флаг Ind_Flag во вспомогательном регистре Flags. Таймер настроен таким образом, чтобы прерывания происходили примерно с частотой 256 Гц. Почему "примерно"? Да потому, что особая точность нам не нужна (не часы делаем), а заносить в счетчик таймера какое-то начальное значение - трата лишних сил. Тем более что частота генератора 8 МГц довольно плохо делится до получения нужного нам периода переполнения таймера. В общем, получается, что флаг Ind_Flag будет устанавливаться каждую секунду. В основной программе в месте, где требуется подождать 1 секунду, мы должны дождаться, пока этот флаг установится, после чего немедленно его сбросить, вот так:

wait_loop:
  wdr
  sbrs Flags, Ind_flag    ; проверим, установлен ли флаг
  rjmp wait_loop    ; если не надо - ждем
  andi Flags, ~(1<<Ind_Flag)     ; сбросим флаг


Обратите внимание: в цикле ожидания необходимо сбрасывать WDT, иначе программа может быть перезапущена с начала еще до истечения необходимого интервала времени по переполнению WDT!
Таким очень простым способом реализуется задержка в 1 секунду. Но не только в этом красота такого подхода! Если перед тем, как начать ожидание флага Ind_Flag запретить прерывания, занести в ячейку Delay какое-то число, а потом разрешить прерывания - то наш флаг установится уже не через 1 секунду, а меньше (чем большее число занесем в Delay, тем меньше время до установки флага). То есть мы можем реализовать и другие задержки. Почему изменение Delay необходимо делать при запрещенных прерываниях, надеюсь, пояснять не надо?
Собственно, это и все: разве что еще несколько слов напоследок.
Рассмотренный пример - вполне работоспособная программа, более того, с минимальными усилиями ее можно перенести на МК другого типа, например, популярного ATTiny2313. Надеюсь, это с легкостью вам удастся (я буду огорчен, если у вас не получится). Пример демонстрирует технику использования директив условной компиляции: в нашем случае для получения возможности компиляции разных версий для разной аппаратной части устройства, но возможно и для иных целей, например для создания "демонстрационных", т.е. усеченных прошивок. Советую пользоваться ими при необходимости, а не переделывать весь код целиком. Экономить силы программиста призвана и другая примененная технология: включаемые файлы, содержащие некие "заготовки" для частого применения. Наконец, разумное использование макросов сделает ваши программы более читаемыми и универсальными. В качестве бесплатного бонуса пример содержит набор подпрограмм для работы с устройствами по интерфейсу 1-Wire.
Надеюсь, этот пример послужит вам не только очередным пособием в освоении программирования на ассемблере, но и станет неким фундаментом для постройки собственных проектов.

Файлы:
Исходники прошивок - source.rar
Прошивки - proshivki.rar

Все вопросы - сюда.




Как вам эта статья?

Заработало ли это устройство у вас?

8 4 2
0 0 1