Pull to refresh

Приемопередатчик HC-12 и датчик температуры DS18b20 на AVR-ассемблере

Reading time26 min
Views16K
По мотивам содержания моей книжки «Программирование микроконтроллеров AVR: от Arduino к ассемблеру» была опубликована статья о подключении дисплеев к AVR с применением только «чистого» ассемблера. Под «чистым» имеется в виду наличие только простейшего редактора кода и программатора, и отсутствие необходимости во всяких навороченных инструментах вроде Atmel Studio. В конце я пообещал, что изложенный там материал продолжится изложением примеров, затронутых в книжке лишь вскользь или не вошедших в нее вовсе.

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

Для упрощения отладки проекта его составляющие сначала моделировались на Arduino, и затем воспроизводились на ассемблере на более адекватной элементной базе.

Одно предварительное замечание
К той статье я получил некоторое количество ожидаемых критических замечаний от знатоков программирования. Одно из них считаю необходимым прокомментировать отдельно: почему у меня «кидание из крайности в крайность» (имеется в виду от Arduino к ассемблеру, пропуская нормальный С). А потому, что Arduino — это платформа для начинающих, для ознакомления с предметом и удобнейший инструмент для быстрого макетирования. А все остальное — для продолжающих. И ассемблер должен предшествовать «нормальному С», а не завершать изучение. Как когда-то верно заметил DI HALT, после С вникать в ассемблер уже не захочется, и ничегошеньки вы про работу контроллера так и не узнаете. Хороший пример — утверждение в одном из комментариев, что «любая программа обработки прерываний должна…» — нет, дорогие оппоненты, совсем не «любая» и уж точно не «должна», все зависит от контекста, в котором это происходит.

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

Остальные соображения по этому поводу см. в книге.

Приемопередатчик HC-12


Сначала речь пойдет об организации беспроводной связи между двумя девайсами. Тема, можно сказать, топовая для наших дней, но при ближайшем рассмотрении приемлемое решение оказывается найти довольно сложно. Естественно, в простых устройствах целесообразно применение самых дешевых и компактных решений. Но дешевизна далеко не синоним простоты — малая цена и доступность описанного мной год назад RF-комплекта из приемника и передатчика ISM-диапазона 433 МГц вполне компенсируются сложностью библиотечного кода. Еще более усложнять задачу, переписывая его на ассемблере, просто глупо. И, кстати, за это время я выяснил, что качество модулей в продаже почему-то резко упало: даже из проверенных источников получить RF-комплект, не желающий работать в паре — совсем не исключение. Для проверки заказал десяток комплектов по дешевке у вроде бы солидного продавца на Ali, посмотрим на качество, когда доберутся.

Для ассемблерного подхода здесь бы идеально подошел какой-нибудь из приемопередатчиков, работающих через стандартные порты обмена (обычно это UART, иногда SPI). К сожалению, почти все они довольно дороги — в книге я ориентировал читателей на MBee-868, но самый дешевый из них находится поиском за цену порядка 1200 рублей, а для радиообмена ведь нужна их пара (и не ищите их на Ali — говорят, это чисто отечественная разработка). За такой порядок цен можно уже построить целую метеостанцию даже по ценам «Чипа-Дипа».

Остальные решения, такие, как LoRa, в основном еще дороже (если не рассматривать сомнительные и плохо документированные предложения с Ali). Потому я остановился на HC-12 на 433 МГц — пару таких модулей можно купить в более-менее надежном интернет-магазине рублей за 800 (или раза в полтора-два дешевле прямо на Ali). Смоделировав прием и передачу на Arduino, я убедился, что отзывы на форумах не врут: из коробки, без всяких настроек, модули работают просто отлично.


Обмен по умолчанию идет на скорости 9600. В качестве антенны использовался просто отрезок изолированной проволоки диаметром 0,5 мм и длиной 17 см (к модулям прилагается и спиральная антеннка, но, опять же по отзывам, она работает хуже). На дальность не проверял, но в пределах квартиры, включая передачу на лоджию через капитальную кирпичную стену под косым углом, никаких проблем не возникло.

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

Энергосбережение при передаче


Все хорошо, и меня устраивает — процедуры обмена простейшие и в полпинка переводятся на ассемблер. Есть только одно довольно большое «НО»: потребление. Модуль в умолчательном режиме на холостом ходу потребляет 15 мА, которые возрастают до 100 и более мА в момент передачи. Исследования с помощью осциллографа показали, что передача длится дольше, чем это должно быть при формально выставленной скорости 9600. При такой скорости байт передается примерно за миллисекунду, измерения же показали, что при передаче четырехбайтового числа потребление возрастает до 100 мА примерно на 15 мс. С этим может справиться обычный 100-миллиамперный стабилизатор (LM2931, LP2950 и пр.) с хорошим танталовым конденсатором на выходе, но для батарейного питания все это совершенно не годится.

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

Почитав стенания по этому поводу на различных форумах, я решил, что такой хоккей нам не нужен! Пусть разбираются въедливые перфекционисты, а мы поступим проще: раз в умолчательном режиме модуль нормально работает, просто будем отключать питание, а при необходимости передачи — включать. Экспериментально удалось установить, что после включения модулю достаточно 30-35 мс на успокоение, для гарантии будем выжидать 50 мс. Включать питание модуля будем маломощным MOSFET-ключом (транзистор BS-170), замыканием на общий провод. Не забудем выждать до выключения необходимое время для гарантированного окончания передачи. Эксперименты показали, что при снижении паузы до выключения модуля ниже 20-30 мс процесс обмена нарушается, потому будем с запасом выжидать также 50 мс. Это проще в осуществлении, чем подача AT-команд, страхует от странностей работы модуля и занимает не больше времени, чем управлением штатным способом в идеальном случае. Причем в перерывах модуль совсем ничего потреблять не будет и не надо экспериментировать с выбором из различных предлагаемых режимов.

Моделирование передачи-приема на Arduino


Скетч, моделирующий простую передачу двухбайтового числа (HC-12_proba_peredatchik) вы найдете в архиве по ссылке в конце статьи. Раз в 4 секунды передается число 273. Соответственно, в простейшем скетче для проверки приема (HC-12_proba_priemnik) при приеме этого числа светодиод по выводу 13 меняет состояние на противоположное. В текстовых скетчах намеренно использован не слишком удобный способ побайтной передачи двухбайтового числа и формированием его заново на приемном конце, примерно так (value — двухбайтовая переменная типа word или uint_16):

. . . . .
   byte bb = Serialpr.read(); //мл. байт
   value = Serialpr.read(); //ст. байт
   value = value*256+bb; //присланное число 
. . . . .

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

И для приемника и для передатчика можно использовать любой Ardiuno на основе ATmega328 (Uno, Nano, Mini или их аналоги, в том числе и с 3-вольтовым питанием). Схемы подключения и передатчика и приемника в тестовом варианте тривиальны: подключить оба вывода питания GND и Vcc модуля к питанию Arduino GND и +5V, и перекрестно соединить выводы RxD и TxD. В скетчах для связи через модули HC-12 использован программный UART: вывод 8 (RxD) и 9 (TxD) Arduino в передатчике, 2 (RxD) и 3 (TxD) в приемнике (см. исходный текст скетчей). Можно, конечно, использовать и штатный UART, но тогда необходимо каждый раз перед подключением USB отключать модуль и при проверке запитывать контроллер от автономного адаптера. Забывчивость мне уже стоила одного необратимо сожженного Uno и одного контроллера, лишившегося загрузчика, потому рисковать не советую (что интересно, в случае Xbee-модулей, которые также подключаются к UART, никаких таких катастроф не наблюдалось — просто Arduino IDE при подключении Xbee переставала получать отклик от порта). В ассемблерном варианте, который мы будем обсуждать далее, подключенный к порту UART адаптер, разумеется, не имеет значения, так как программирование через него не ведется. И мы там будем всегда использовать штатный порт, а на приемной стороне выводить полученные данные на дисплей.

Теперь соберем схему для испытаний модуля HC-12 на отключение и включение с помощью транзисторного ключа, согласно описанному ранее:


В соответствующем скетче (HC-12_proba_power_peredatchik) контроллер каждые 4 секунды отсылает в эфир возрастающее число. В перерывах питание модуля отключается. Управление включением и отключением питания модуля через ключ, соединяющий с модуль с «землей», осуществляется через вывод D5 Arduino (совпадает с выводом 5 порта D). Именно на такой схеме проводились измерения потребления модуля и проверялись необходимые величины задержек на установление питания.

Теперь отвлечемся от потребления и смоделируем расширенную передачу данных с выводом на дисплей. Усложненный скетч передатчика HC-12_peredatchik_array также имеется в архиве по ссылке в конце статьи. Контроллер передает в эфир целый массив, начинающийся с идентификатора передатчика (в данном случае трех символов «DAT»), после следуют два байта числа и затем символ «;» качестве конца данных. Опять же оформить подобную передачу можно средствами высокоуровневого языка более красиво и компактно, но мы остановимся на побайтной передаче массива, так как именно такой способ будем использовать в ассемблерном варианте. Текст скетча HC-12_peredatchik_array невелик и я привожу его здесь полностью:


#define Ledpin 13
#define RX  8  // * Определяем вывод RX (TX на модуле)
#define TX  9  // * Определяем вывод TX (RX на модуле)

#include <SoftwareSerial.h> // Библиотека программного последовательного порта
SoftwareSerial Serialpr(RX,TX); // Программный последовательный порт

word value=273; //условное 3-значное число (2-байтовое)
//передаваемые байты, "DAT"-идентификатор, ";" - разделитель (конец данных):
byte arr[6] = {'D','A','T',0,0,';'}; 

void setup() {
Serialpr.begin(9600);
pinMode(Ledpin,OUTPUT);
delay(100);
}

void loop() {
digitalWrite(Ledpin,HIGH);
//доформируем массив:
arr[3]=lowByte(value);  
arr[4]=highByte(value);  
for (byte i = 0; i < 6; i++) 
Serialpr.write(arr[i]); //передаем массив через программный порт 

delay(50);
digitalWrite(Ledpin,LOW);
delay(4000);
}

Может показаться, что накладные расходы такого способа слишком велики — четыре служебных байта на два байта передаваемого числа. Но во-первых, это тестовый макет (в реальности мы можем передавать сколько угодно чисел любой разрядности), во-вторых, таким способом мы можем повесить на один приемник любое количество датчиков, меняя их идентификатор (к примеру «DA1», «DA2» и так далее). А оконечный символ «;» служит здесь для проверки того факта, что массив передан и принят полностью (в Arduino без этого символа вполне можно обойтись, а в ассемблерном варианте он нам пригодится, чтобы не связываться со всякими таймаутами).

Для проверки можно просто принимать массив побайтно через программный порт, и потом отправлять принятое через обычный Serial на монитор порта для проверки. Для такого способа можно использовать слегка модифицированный скетч простейшего приемника (HC-12_proba_priemnik). Подробнее я останавливаться на этом не буду, так как мы тут сразу попробуем смоделировать отвязанный от USB приемник с дисплеем.

Приставим к Arduino обычный ЖК или OLED-дисплей 16х02, так как уже знаем (см. предыдущую статью), что потом перенести это в ассемблерный проект будет совсем несложно. Схема подключения дисплея и модуля HC-12 к Arduino показана на рисунке:


Соответствующий скетч носит название HC-12_priemnik_OLED16x02_Arduino, и также может быть найден в архиве по ссылке в конце статьи. Пятисекундная задержка в начале (в функции setup()) с тестовым выводом на дисплей необходима для проверки правильности подключения и инициализации дисплея. После этого программа начинает отслеживать прием массива через программный UART (штатный Serial при этом используется для отладки). Прием устроен довольно нестандартным способом:

. . . . .
    if (Serialpr.available()>0) { 
      i=0;
  while (Serialpr.available()){
  digitalWrite(Ledpin,HIGH); // Пришло! Считываем и анализируем
  bb=Serialpr.read();
//обязательная задержка, иначе вылетаем из while:  
  delay(2);
//  Serial.println(bb); //для отладки
//  Serial.print(' '); //для отладки
  {arr[i]=bb; i++;}
  } //конец while
. . . . .

После того, как мы убедились, что пришел первый байт передаваемого массива (Serialpr.available()>0), производится отслеживание по условию while (Serialpr.available()). Выход из этого цикла происходит автоматически по истечению таймаута на прием. По умолчанию в Arduino Serial.timeout равен 1 мс, что примерно равно времени передачи байта со скоростью 9600. Поэтому через пару-тройку принятых байт мы из цикла вылетаем, не закончив прием массива. Чтобы этого избежать, после приема очередного байта ставится небольшой delay, тогда выход из цикла будет только после приема последнего байта массива. Задержка необязательна, если раскомментировать отладочные строки посылки через обычный Serial – время их выполнения как раз обеспечит необходимую задержку. Повторю, что средствами Arduino можно обеспечить куда более компактный прием (применив всякие там readString и parseInt), но мы моделируем будущий ассемблерный вариант. Вы увидите, что там проблема приема целостного массива будет решаться другим способом, где как раз посылка разделителя «;» будет играть решающую роль.

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

Передача и прием на ассемблере




Испытание ассемблерной версии передатчика проводилось по схеме, показанной на рисунке выше (сразу с возможностью отключения питания модуля HC-12). Контроллер ATtiny2313 может быть заменен в данном случае на почти любой другой AVR (о том, что именно надо менять в исходном коде при такой замене — см. упомянутую книгу). Кварц (в данном случае на 4 МГц) — обязательная для этого случая деталь, при работе от встроенного генератора UART на скорости 9600 будет сбоить. Потребление в сравнении с ардуиновскими 16 МГц падает примерно вдвое, а кварц шустрее 4 МГц нужен лишь в отдельных задачах, связанных с измерением времени.

Управление ключом питания модуля HC-12 производится через вывод 5 порта D (вывод 9 микросхемы). Стандартный для Arduino вывод сигнального светодиода D13 (это вывод 19 ATmega328) здесь заменен на вывод 6 порта D (выв. 11 ATtiny2313, крайний в корпусе). Заметьте, что на схеме указан стандартный светодиод, который при резисторе 620 Ом будет потреблять около 5 мА. Если хотите немного сэкономить дополнительно, поставьте суперяркий (1000 мкд и более) и к нему резистор порядка 20-33 кОм, в зависимости от яркости свечения выбранного типа.

Предварительно необходимо установить fuse-биты. Да-да, а что вы думали? Это в Arduino вам их менять не только нельзя, но и невозможно. А все контроллеры выпускаются с фабрики с установками на работу от встроенного генератора (причем с конкретной частотой 1 МГц, за редким исключением). Потому их нужно поменять, как минимум, на работу от внешнего кварца. Для облегчения программирования чипа 2313 привожу наглядную картинку настроек фьюзов для данного случая. Как там и написано, нажатая кнопка соответствует нулевому (programmed) значению фьюза:



Систему BOD мы здесь держим выключенной (все биты BODELEVEL в единичном состоянии), потому что это 20-30 мкА дополнительного потребления. При выключенной системе отслеживания питания контроллер при истощении батарейки может совершать непредсказуемые операции, но в общем случае это опасно только для содержимого EEPROM. В данной схеме долговременная память не употребляется и BOD можно выключить.

Соответствующая тестовая ассемблерная программа передатчика сразу с использованием энергосбережения (H-12_peredatchik.asm) приведена полностью далее:

.include "tn2313def.inc"
	
.def temp =r16
.def Razr0 = r17 ;разряды задержки
.def Razr1 = r18
.def value = r19 ;очередной байт числа

;============ прерывания ============
rjmp RESET ;Reset Handle
.org WDTaddr
rjmp WDT_over ;WDT Interrupt Vector Address

;========== программа ============

out_com:  ;посылка байта из count с ожид. готовности
	sbis UCSRA,UDRE ;ждем готовности буфера передатчика
	rjmp out_com
	out	UDR,value ;value!!! а не temp
ret


;Число N для задержки T (с) при такт. частоте F (Гц) равно
;N = TF/4; F= 4 МГц 
;Для T = 50 mc N= 50 000, $C3 50 = 195 80
.macro Delay65 ;процедура задержки
ldi Razr1,@0 ;старший байт N
ldi Razr0,@1;младший байт N
R_sub: 
 subi Razr0,1 
 sbci Razr1,0
brcc R_sub
.endm

WDT_over: ;пробуждение по Watchdog
 sbi PortD,6 ;зажигаем светодиод
 sbi PortD,5 ;включаем модуль
 Delay65 195, 80 ;задержка 50 мс при 4 МГц 
	ldi value,Low(273) ;мл. байт числа 273
	rcall out_com ;посылаем во внешний мир 
	ldi value,High(273) ;ст. байт числа 273
	rcall out_com ;посылаем во внешний мир 
 Delay65 195, 80 ;задержка 50 мс при 4 МГц 
 cbi PortD,6 ;зажигаем светодиод
 cbi PortD,5 ;включаем модуль
reti ;конец прерывания WDT

Reset:
	ldi		temp,low(RAMEND)   ;устанавливаем указатель на стек  	
	out	SPL,temp ;для tiny2313 только SPL

	ldi temp,1<<ACD	 
	out ACSR,temp ;выкл. аналог. компаратор
	
ldi temp,0b01100000 ;порт D контакт PB5, PB6 на выход
out DDRD,temp

;=== UART
	ldi temp,25 ;9600 при 4 МГЦ + U2X
	out	UBRRL,temp ;скор. передачи
	ldi temp,(1<<RXEN|1<<TXEN)
	out	UCSRB,temp ;разреш. приема/передачи 8бит

;==== установка Watchdog, прерывания запрещены!
 wdr ;сбрасываем WDT
 ldi temp,(1<<WDCE)|(1<<WDE) ; разрешение изменений + запуск 
 out WDTCR,temp
 ldi temp, (1<<WDIE)|(1<< WDP3)  ; прерывание + время (WDP3:0 = 1000 =4 сек)
 out WDTCR,temp
	
ldi temp,(1<< SM1)|(1<<SE) ;разрешение Sleep, режим Power Down
out MCUCR,temp

sei ;разрешаем прерывания
	
Gcykle: ;главный цикл 
	sleep ;уходим в сон
 rjmp Gcykle

В главном цикле программы, как видите, совершается единственное действие — уход в энергосберегающее состояние. Все действия здесь реализованы в обработчике прерывания сторожевого таймера (WDT), настроенного на выход из сна каждые 4 секунды (максимальное значение 8 секунд). После выхода из сна через МОП-ключ BS-170 подключается передачик, дается пауза 50 мс для того, чтобы модуль «пришел в себя», и затем производится передача. Измерения показали, что в паузах вся схема потребляет не более 150 мкА. Активный период длится около 0,1 с, в это время потребление в среднем составляет около 50 мА. Напомним, что в ассемблерной программе подключение программатора ни на что не влияет, потому при отладке передатчика его можно не отключать (но не забывайте, что программатор питается от схемы и при измерении потребления отключать его все-таки необходимо!). Напомним, что в качестве светодиода Led1 можно поставить суперяркий с токоограничивающим резистором R2 величиной 20-30 кОм. Текст программы и скомпилированный hex-файл вы найдете в том же архиве в конце статьи.

Следует учесть, что WDT в контроллере ATtiny2313 расширенного типа, аналогичного встроенному в Arduino-контроллеры ATmega328. Сторожевой таймер в старых контроллерах, вроде ATmega8/16, не поддерживает режим выхода из сна в прерывание, только полный перезапуск. Это критично для программ, хранящих промежуточные результаты в памяти или регистрах, но в данном случае приведет лишь к необходимости в минимальной коррекции процедур инициализации сторожевого таймера и переноса всей функциональности в главный цикл (подробности см. книгу). Кроме того, старый WDT не поддерживает длинные выдержки в состоянии сна (более 2 с). Это следует помнить и проверять тип встроенного WDT, если вы захотите перенести программу на другой контроллер (кстати, популярный у ардуинщиков ATtiny13 также имеет расширенный WDT, но в нем отсутствует полноценный аппаратный UART, потому для наших целей его придется приспосабливать дополнительно).

Расширенную программу передатчика с посылкой массива, аналогично тому, как мы это делали на Arduino, вы также можете найти в архиве (H-12_peredatchik_2313_WDT.asm). Она отличается от приведенной выше только содержимым обработчика прерывания WDT:

. . . . .
WDT_over: ;пробуждение по Watchdog
 sbi PortD,6 ;зажигаем светодиод
 sbi PortD,5 ;включаем модуль
 Delay65 195, 80 ;задержка 50 мс при 4 МГц 
	ldi value,'D' ;символ D
	rcall out_com ;посылаем во внешний мир 
	ldi value,'A' ;символ A
	rcall out_com ;посылаем 
	ldi value,'T' ;символ T
	rcall out_com ;посылаем 
	ldi value,Low(273) ;мл. байт числа 273
	rcall out_com ;посылаем 
	ldi value,High(273) ;ст. байт числа 273
	rcall out_com ;посылаем 
	ldi value,';' ;символ ';', конец передачи
	rcall out_com ;посылаем 
 Delay65 195, 80 ;задержка 50 мс при 4 МГц 
 cbi PortD,6 ;зажигаем светодиод
 cbi PortD,5 ;включаем модуль
reti ;конец прерывания WDT
. . . . .

Программа каждые 4 секунды передает в эфир такой же массив, предваряемый идентификатором датчика, как и Arduino-скетч выше, потому, чтобы убедиться, что передатчик работает верно, проверить прием можно тем же скетчем приемника с дисплеем (HC-12_priemnik_OLED16x02_Arduino). Обратите внимание, что передаваемое число здесь 2-байтовое, но ограничено 3 десятичными знаками (0-999). Так в дальнейшем (при выводе на дисплей) удастся сократить процедуру BCD-преобразования — в задаче дистанционного термометра далее передаваемые величины температуры не будут выходить из этого диапазона.

Приемник на ассемблере


Для дальнейших действий нам потребуется приемник с дисплеем на основе ассемблера, потому давайте составим макет подобного устройства. За основу возьмем ATmega8, тогда схема может быть, например, такой:



Установка fuse-бит для Mega8 показана на рисунке (здесь питание от сетевого адаптера, экономия не требуется, потому систему BOD можно не отключать):



Программа, соответствующая этому рисунку (HC-12_priemnik_OLED16x02_proba.asm), получается довольно громоздкой, потому полностью здесь не приводится, и вы ее сможете посмотреть в архиве по адресу в конце статьи. Здесь мы остановимся на ключевых моментах. В начале программы, после всех необходимых установок, дисплей для проверки верности подключения и для образца заполняется тестовыми символами точно так же, как это делалось в программе OLED1602_proba.asm из предыдущей статьи. Затем программа переходит к замкнутому циклу, в котором принимает последовательно все элементы нашего массива, с проверкой трех символов идентификатора в начале и концевого символа «;» в конце:

Gcykle:
	rcall in_com ;ждем прихода байта в temp
	cpi temp,'D'
	sbi PortD,Led ;зажигаем Led
	brne Gcykle ;если не D, то в начало ожидания
	rcall in_com ;ждем прихода байта в temp
	cpi temp,'A'
	brne Gcykle ;если не A, то в начало ожидания
	rcall in_com ;ждем прихода байта в temp
	cpi temp,'T'
	brne Gcykle ;если не T, то в начало ожидания
;дальше два байта числа
	rcall in_com ;ждем прихода байта в temp
	mov valueL,temp ;получаем мл. разряд
	rcall in_com ;ждем прихода байта в temp
	mov valueH,temp ;получаем ст. разряд
	rcall in_com ;ждем прихода байта в temp
	cpi temp, ';' 
	brne Gcykle ;если не конец данных, то ничего не делаем
	;иначе зажигаем Led
	; переводим число valueH:valueL в BCD-форму и выводим на дисплей
	rcall bin2BCD10
	rcall unpack_bcd8 ;результат3 дес. разряда в ResH:valueH:valueL
	Set_cursor 0,0 ;курсор строка 0 позиция 0
mov temp,ResH ;выводим старший
subi temp,-$30
rcall LCD_data
mov temp,valueH ;выводим средний
subi temp,-$30
rcall LCD_data
mov temp,valueL ;выводим младший
subi temp,-$30
rcall LCD_data
	cbi PortD,Led ;гасим Led
rjmp Gcykle

Если эти служебные символы не совпадают с ожидаемыми, то происходит возврат в начало цикла ожидания. Если все совпало, то выполняем конвертацию полученного hex-числа в три отдельных десятичных цифры и выводим их дисплей в виде символов, для чего достаточно к каждой цифре прибавить число 0x30 = 48, соответствующее позиции нуля в таблице ASCII (во избежание недоумений: чтобы прибавить к регистру общего назначения константу, в AVR-ассемблере применяется команда вычитания отрицательного числа, команда сложения с константой имеется только для 16-битовых чисел).

О таком способе приема через UART
Обратите внимание на способ выполнения приема через UART. В данном случае мы не используем никаких прерываний, а банально зацикливаем программу в ожидании установления бита RXC в регистре UCSRA, что означает приход очередного байта:

in_com:   ;прием байта в temp с ожид. готовности
	sbis	UCSRA,RXC  ;ждем готовности буфера приемника
	rjmp	in_com
	in	temp,UDR ;принимаем
ret 

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

Датчик температуры DS18b20


В книге описана работа в основном с известным аналоговым полупроводниковым датчиком температуры TMP36 (под другими названиями его выпускает множество фирм). Вообще аналоговые датчики температуры не представляют никакой проблемы — у меня много лет отлично работал заоконный термометр с медным датчиком на основе 800-омной обмотки герметизированного реле РЭС-60. Сложнее с аналоговыми датчиками влажности, но и их приобрести отдельно не проблема, вопрос только в цене — дешевые наверняка будут вам показывать «погоду на Марсе», особенно вне комнатных условий. Впрочем, с цифровыми Arduino-датчиками ровно та же история, так что не будем отвлекаться.

Рассмотрим здесь один из самых приличных представителей цифровых датчиков температуры — DS18b20. Первые его аналоги появились на рынке более двух десятилетий назад, когда обо всем этом китайском ширпотребе, задающем сегодня тон на массовом рынке, еще речи не шло. Возможно, поэтому изделие по качеству получилось вполне на уровне: по моему опыту, точность 0,5 градуса «из коробки» датчик вполне выдерживает. Высокие метрологические качества подтверждаются фактом, что датчик DS18b20 внесен в Госреестр средств измерений (сертификат 44046-10 от 2010 года), и при соблюдении соответствующих условий может использоваться для официальных измерений. Цена на этот датчик зависит от диапазона, в котором гарантируется погрешность в 0,5 градуса, и совсем не кусается, причем оригинальный MAXIM даже в «Чип-дипе» можно приобрести менее, чем за сотню деревянных. Интерфейс OneWire позволяет длину линии в десятки метров, и потому китайские и отечественные магазины смогли предложить DS18b20 заделанным в герметичный кабель разной длины. Потребляет датчик копейки и вполне годится для энергосберегающих автономных устройств. В общем, удобная штуковина во всех отношениях, кроме, пожалуй, довольно навороченного протокола (что, очевидно, неизбежное следствие его высоких качеств).

А для нашей ассемблерной задачи DS18b20 удобен еще и потому, что, в отличие от всех этих Arduino-датчиков, выдает сразу калиброванные цифровые значения, соответствующие градусам Цельсия. То есть для вывода показаний не придется возиться с калибровкой и проводить громоздкие расчеты. Необходима только небольшая коррекция выходного кода, которую на ассемблере провести ненамного сложнее, чем на С.

Датчик подключим к выводу 0 порта С. На всякий случай приведем полную схему передатчика с датчиком DS18b20 на основе ATtiny2313:



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

Можно составить Arduino-модель, конечно, но это мало что даст: использование библиотеки DallasTemperature чрезвычайно простое, достаточно сделать по образцу прилагаемого к ней примера. Это ничуть нас не продвинет в изучении протокола, а составлять его самостоятельно сразу на ассемблере даже проще, чем на С. Процедуры чтения DS18b20, в том числе и на AVR-ассемблере, обсуждаются в Сети уже не первое десятилетие, потому останавливаться на этом я не буду. Толковое описание алгоритма можно найти, например, в серии статей вот тут, советую с ним ознакомиться (правда, в некоторых нюансах реализации моя программа отличается от приведенных в этих статьях примеров).

По поводу OneWire и датчика DS18b20
Уже 20 лет как несуществующая, фирма Dallas Semiconductor надолго задержалась в народной памяти целым рядом неустаревающих придумок. Например, она изобрела когда-то шину OneWire (1-Wire). Одним из самых массовых OneWire-устройств в России стали подъездные ключи-таблетки (iButton). На мой вкус, куда более удобное устройство, чем RFID-карточки, прижившиеся в качестве ключей в гостиницах. Кроме OneWire, самая, наверное, известная из ее придумок — ряд встраиваемых часов реального времени с различными интерфейсами (DS1307 и его потомки).

Всемирную известность Dallas когда-то принесла идея встраивать литиевые батарейки прямо в чип обычной статической памяти SRAM. Таким образом вы получали некую имитацию в то время еще очень дорогой и сложной в обращении EEPROM, причем имитацию гораздо более скоростную, чем даже современные флешки. Сохранность данных гарантировалась в течение 10 лет, и такое решение широко применялось даже в ранних ПК и лэптопах для хранения установок BIOS. По мере удешевления EEPROM это относительно дорогое и ненадежное (через 10 лет микросхему неизбежно нужно менять на новую) решение отмерло естественным путем и к началу тысячелетия фирма Dallas разорилась. Но ее остроумным придумкам умереть не дала издавна с ней сотрудничающая фирма Maxim, в 2001 году поглотившая Dallas со всеми патентами и товарными знаками, чему мы и обязаны сохранением торговой марки DS.

Добавим, что совсем недавно, в июле 2020 года, Maxim сама была поглощена своим главным конкурентом Analog Devices. Sic transit gloria mundi!.. Но стоит надеяться, что узнаваемая торговая марка DS все равно никуда не денется.

На простую шину 1-Wire можно сажать до десятков устройств на расстоянии до 30 м, или до 200-300 шт на расстояниях до 100-300 метров с применением специальных интерфейсных модулей и витой пары (подробности для интересующихся см. тут). Цифра 30 м относится к режиму «паразитного» питания, с отдельным питанием длина линии может быть больше даже без специальных мер. Но такие задачи возникают относительно редко — например, в публикациях упоминается распределенный контроль температуры в помещениях типа овощехранилищ. В любительской практике датчики, если их больше одного, обычно принципиально разнесены в пространстве (внешний и внутренний у метеостанции, два датчика для двух разных парников в огороде), и проще организовать беспроводную связь, чем тащить провода. Потому мы вместо 64-битного индивидуального номера DS18b20 и применяем самодельный идентификатор для беспроводных датчиков.

При ознакомлении с описанием DS18b20 поражаешься количеству наворотов, реализованных в таком простом и дешевом устройстве. Конечно, мы будем применять датчик в самом простом режиме однократного считывания температуры, но иметь в виду эти возможности следует.

Текст программы передатчика (HC-12_peredatchik_DS18b02.asm) можно найти все в том же архиве по адресу в конце статьи. Программа основана на приведенной выше программе посылки двухбайтового числа с идентификатором передатчика (H-12_peredatchik_2313_WDT.asm), но вместо произвольного числа один раз посылает считанный с датчика двухбайтовый код. В целях энергосбережения время между посылками увеличено до 8 секунд (его можно увеличить еще больше, если осуществлять вывод не в каждом прерывании WDT, а отсчитывать, например, каждое четвертое или даже восьмое). Обработку данных целесообразно производить на приемном конце, и на этом вопросе мы остановимся подробнее: — во-первых, это хорошая иллюстрация к ассемблерным методам обработки чисел на 8-разрядном контроллере, во-вторых, потому что этот вопрос применительно к датчику DS18b20 уже двадцать лет не сходит со страниц форумов по электронике и нередко ответы на него оказываются довольно причудливыми.

Формат данных о температуре, выдаваемых датчиком, устроен довольно остроумно. Содержимое двух байтов при умолчательном 12-битовом разрешении (остальные, очевидно, нужны для сокращения времени измерения при непрерывном преобразовании) представлено на стр. 6 фирменного руководства. На самом деле там не 12 значащих бит, а 11, 12-й (старший) представляет знак (1- минус, 0 — плюс), а максимальная температура равна 125 градусам и целая часть укладывается в 7 бит. Эти биты целой части распределены между двумя байтами по тетраде в каждом, так, что младшая тетрада старшего байта содержит старшие четыре бита температуры в градусах Цельсия (самый старший бит тетрады, напомню, несет только знак), а старшая тетрада младшего байта – младшие четыре бита. Оставшаяся младшая тетрада младшего байта несет десятичную дробную часть значения температуры, но выраженную в виде шестнадцатеричной дроби. Для тех, кто не понял, воспроизведу здесь картинку с указанной страницы руководства:

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

а) Определение знака (сравнением какого-нибудь из старших пяти бит старшего байта с единицей). При положительном знаке — сразу переход к пункту «в».

б) При отрицательном знаке производится дополнительное преобразование в положительный код: знак запоминается (и потом выводится), а для самого двухбайтового числа вычисляется дополнение до двух, которое и будет положительным числом. Дополнение до двух можно вычислять миллионом разных способов: например, в C-нотации это выглядит просто, как ~XXXX+1 (где XXXX — исходное отрицательное 2-байтовое число в дополнительном коде, а применяемый способ инвертирования битов определяет компилятор — можно, например, применить побитовое «исключающее ИЛИ» с числом 0xFFFF). Ту же самую операцию можно выполнить и простым вычитанием из максимального числа диапазона с прибавлением единицы: 0xFFFF – XXXX + 1. Обратите внимание, что разработчики из Dallas мудро заполнили все неиспользуемые старшие биты старшего разряда значением знака, потому мы можем совершать манипуляции с полным 16-разрядным числом, не заботясь о специальном выделении 12-разрядного результата.

В AVR-ассемблере все это делается побайтно и с помощью специальных команд com и neg (com — просто инвертирование битов, что равносильно операции вычитания из $FF, а neg — нахождение дополнительного кода, то есть инвертирования с прибавлением единицы, или вычитания из $(1)00):

com valueH ;инвертируем старший байт
neg valueL ;младший -> доп. код
sbci valueH,0xFF ;UPD - спасибо GarryC за указание на ошибку

Пояснение по команде sbci
Последняя операция учитывает в старшем байте перенос из младшего. Иначе, если в результате операции нахождения дополнительного кода в младшем байте образовалось число, большее $FF (что, очевидно, случится при valueL=0), возникнет ошибка. Простой пример, как мне справедливо указали — побайтная конвертация кода $FF00. Команда neg оставляет информацию о возникновении такой ситуации: она всегда устанавливает бит переноса C, отмечая заимствование из старшего разряда, за исключением случая, когда результирующий (и исходный) байт равен 0, т.е. заимствования не происходит. Команда sbci и учитывает эту ситуацию: она вычитает из полученного в старшем байте результата число $FF, заведомо добавляющее единицу, и одновременно вычитает бит переноса. Если заимствования не было, то добавленная единица в старшем разряде остается, если было, то уничтожается. Такая вот хитрая арифметика в 8-разрядном процессоре.

в) Усечение результата до одного десятичного знака после запятой. В принципе мы можем получить аж четыре знака после запятой (цена младшего разряда 0,0625 °С), и некоторые Arduino-библиотеки так и поступают. Что, конечно, полная фикция, так как при разрешении 1/16 °С даже два знака после запятой (т.е. 0,01 градус) в шесть с лишним раз перекрывают это разрешение, и в 50 раз — реальную погрешность датчика, равную 0,5 градуса. (Скажу по секрету, что погрешность в сотую градуса достигается только с платиновым термометром на лабораторном оборудовании). Так что не будем занимать лишнее место на фейковые разряды, ничего реального не отражающие, и обойдемся десятыми градуса.

Мы не будем даже городить строгое округление, так как десятая градуса все равно в пять раз меньше погрешности датчика, а алгоритм простого усечения получается куда менее громоздкий. Для поразрядного вывода на дисплей нам удобнее сразу иметь целое число, выраженное в десятых градуса, и потом установить точку на нужное место вручную. Иными словами, нам надо умножить полученное число на 10 и поделить на 16, или, что то же самое, умножить на 5 и поделить на 8.

В Tiny нет аппаратного умножения, потому в целях универсальности кода (чтобы он одинаково годился и для Tiny и для Mega) умножение на 5 составляем из нескольких действий согласно формуле X*4+X. Умножение на 4 на ассемблере естественно выполнять двойным сдвигом влево, но если разобраться поглубже, то окажется, что инструкция сложения регистра с самим собой не только делает то же самое, что и сдвиг влево на один разряд, но даже совпадает с ним по коду команды. Иными словами, это одна и та же команда, потому для единообразия употребим сложение, а вот деление на 8 будем делать тройным сдвигом вправо (здесь valueH:valueL – старший и младший байты полученного числа):

. . . . .
	mov ResL,valueL ;используем ResH, ResL  как временные переменные
	mov ResH,valueH
	add valueL,valueL ;умножили на 2	
	adc valueH,valueH	
	add valueL,valueL ;умножили на 4	
	adc valueH,valueH	
	add valueL,ResL ;прибавили один раз	
	adc valueH,ResH	
	; и делим на 8 - три раза сдвигаем вправо, мл. через перенос:
	lsr valueH
	ror valueL
	lsr valueH
	ror valueL
	lsr valueH
	ror valueL
 . . . . .

После этого полученное число осталось конвертировать в три отдельных десятичных разряда и вывести на дисплей. Заметим, что максимальная величина температуры у нас будет состоять из трех десятичных разрядов (по образцу «99,9»), но выше 99 градусов мы температуру все равно не меряем. Принцип устройства процедур bin2BCD10 и unpack_bcd8 описан в книге:

. . . . .	
; переводим число valueH:valueL в BCD-форму и выводим на дисплей
	rcall bin2BCD10
	rcall unpack_bcd8 ;результат 3 дес.разряда в ResH:valueH:valueL
	Set_cursor 0,5 ;курсор строка 0 позиция 5
;учитываем	знак
brtc out_plus ;если T=0 то на вывод +
	ldi temp,'-' ;выводим минус 
	rcall LCD_data
	rjmp out_val
out_plus:
	ldi temp,'+' ;выводим плюс 
	rcall LCD_data
out_val:
;ведущий 0 в ст. разряде не выводим
tst ResH
breq out_sp
	mov temp,ResH ;выводим старший
	subi temp,-$30
	rcall LCD_data
	rjmp out_rem
out_sp: ;если 0, выводим пробел
ldi temp, ' '
	rcall LCD_data
out_rem: ;выводим остальное
	mov temp,valueH ;выводим средний
	subi temp,-$30
	rcall LCD_data
	ldi temp,'.' ;выводим точку
	rcall LCD_data
	mov temp,valueL ;выводим младший
	subi temp,-$30
	rcall LCD_data
ldi temp,$01 ;рисованный значок градуса
rcall LCD_data
ldi temp,'C' ;C
rcall LCD_data
. . . . .

В этом выводе предусмотрено игнорирование ведущего нуля в разряде десятков градусов. Но если мы просто пропустим вывод в эту лишнюю позицию на экране, то вся строка градусов окажется короче на один символ, и при переходе от двухразрядного значения целых градусов («+10.0 ºС») к одноразрядному («+9,9 ºС») у вас, очевидно, два раза пропечатается конечный символ «С». Очистка всего экрана перед выводом строки, конечно, самый простой метод, но не самый лучший: процедура довольно долгая и экран будет заметно мигать, а если выводится еще что-то (во вторую строку, например), то придется без нужды обновлять и эти данные. Есть и много других способов, но мы применяем самый простой и кратчайший по времени, к тому же не затрагивающий остальные данные на экране: выдерживаем одну и ту же длину строки, независимо от величины градусов (она окажется равной 8 символов). Поэтому мы усложняем программу и выводим пробел вместо нуля в разряде десятков градусов.

Полностью программу HC-12_priemnik_OLED16x02_DS18b20.asm можно скачать из архива по адресу ниже. Результат совместной работы обеих ассемблерных программ с датчиком DS18b20 и модулем HC-12 показан на рисунке:



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

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

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

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

Архив программ из этой статьи можно скачать по этому адресу.
Tags:
Hubs:
+13
Comments28

Articles