Еще 1-Wire Master, теперь на прерываниях

AVR

Как ни странно, 1-Wire — довольно приятный протокол, особенно учитывая простоту и невысокую стоимость датчиков температуры DS18B20. Поискав в гугле рабочие либы для 1wire-master, я, к своему сожалению, увидел только «синхронные», «блокирующие» примеры, что, к сожалению, в моем случае было неприменимо (ага, я так думал). Пришлось писать свою, «неблокирующую» реализацию…


Зачем именно неблокирующую? Поясню — всего есть 6 1-wire каналов, на каждом — один или два DS18B20 + один DS2408 (8 — потому, что такие были), который читает состояние выключателя (чтобы свет включать и выключать). Если все это дело опрашивать «блокирующим» способом, то частота опроса состояния клавиш упадет в 6 раз (каналов-то 6, а при неблокирующем мы можем общаться по каждому каналу независимо). В принципе, не так уж и критично, 8 каналов оно все равно за десятую секунды опросить успеет, но вот захотелось…

Итак, что получился за девайс — atmega168 с кварцем на 16, сидящая на шине i2c, и 6 подтягивающих резисторов на 4.7к. Плюс 6 мини-платок из DS2408 + 4 подтягивающих резистора (для клавиш выключателей) + площадки для подпайки DS18B20. Питание не паразитное, так что из 4х подходящих к каждой платке жил используются 3.

Реализовано все крайне просто (код немного покоцан + оставлены только 4 датчика на PORTB для читабельности):
1. Написан класс, реализующий конечный автомат 1-wire мастера. Ему скармливается «что послать, сколько прочитать», по готовности он возводит внутри себя флаг.

class OneWireHW
{
...
    byte_t buff[MAX_1W_BUFFER_BYTES];
    bool presence;
...
    OneWireHW(ONEWIRE_PORT *_port, byte_t pin);
    bool IsReady();
    void StartTransaction(byte_t bytesToWrite, byte_t bytesToRead);
    void PrepareForNextSlot(byte_t currentPort);
};


2. Прерывание по таймеру — окно в 70мкс разбито на 4 тика таймера:
— в пины, которые нужно «опустить» — пишем 0; ставим таймер на 9 мксек;
— в пины, которые нужно «поднять» — пишем 1; ставим таймер на 6 мксек;
— читаем состояние пинов (мы де не только писать собрались...); для каждого пина (канала) вызываем экземпляр нашего класса-конечного автомата, чтобы он получил состояние своего пина и принял решение, надо ли его пин в следующем окне поднимать и\или опускать и\или «опускать после». Ставим таймер на 50 мксек.
— в пины, которые нужно «поднять после» — пишем 1; пишем 1 и ставим таймер на 5 мксек.

static ONEWIRE_PORT CURRENT_PORTB = {0,0,0};
ISR(TIMER2_COMPA_vect)
{
   if (TIMESLOT_STEP == STEP_STROBE)
   {
      SetMyTimerMkSec(9);
      TIMESLOT_STEP = STEP_RESTROBE;
      byte_t pgl = CURRENT_PORTB.pinsToStrobeLow; // i hope it will use a register
      PORTB  &= ~pgl;  // теперь опустим эти пины
      DDRB   |=  pgl;  // пины, которые "дергаем", переключаем как OUTPUT
      return;
   }

   if (TIMESLOT_STEP == STEP_RESTROBE)
   {
      SetMyTimerMkSec(6);
      TIMESLOT_STEP = STEP_AQUIRE;
      byte_t pgh = CURRENT_PORTB.pinsToStrobeHigh; // i hope it will use a register
      PORTB  |=  pgh;  // теперь поднимем эти пины
      DDRB   &= ~pgh;  // пины, которые "дергаем", переключаем как INPUT
      return;
   }

   if (TIMESLOT_STEP == STEP_AQUIRE)
   {
      SetMyTimerMkSec(50);
      TIMESLOT_STEP = STEP_PAUSE;
      byte_t readedbitsb = PINB;
      byte_t readedbitsd = PIND;

      // тут пускай нам все скажут, что ж мы будем делать в след тайм-слоте
      OneWireHW::ONEWIRE_PORTB.pinsToStrobeHigh = 0;
      OneWireHW::ONEWIRE_PORTB.pinsToStrobeHighAfter = 0;
      OneWireHW::ONEWIRE_PORTB.pinsToStrobeLow = 0;

      _ohw_b4.PrepareForNextSlot(readedbitsb);
      _ohw_b1.PrepareForNextSlot(readedbitsb);
      _ohw_b3.PrepareForNextSlot(readedbitsb);
      _ohw_b2.PrepareForNextSlot(readedbitsb);
      return;
   }
   if (TIMESLOT_STEP == STEP_PAUSE)
   {
	   OHW_CYCLES++;
	   SetMyTimerMkSec(5);
	   TIMESLOT_STEP = STEP_STROBE;
       byte_t pgh = CURRENT_PORTB.pinsToStrobeHighAfter; // тут по идее все, кто стробился
	   PORTB |=  pgh;
	   DDRB  |=  pgh;
	   CURRENT_PORTB = OneWireHW::ONEWIRE_PORTB;
	   return;
    }
}


ну, и, собственно, логика:


void OneWireHW::PrepareForNextSlot(byte_t currentPort)
{
    if (resetting)
    {
        if (resetting < 9)
        {
            // тут "начало" ресета.
            port->pinsToStrobeLow |= maskForInterrupt;
            resetting++;
            return;
        }
        port->pinsToStrobeHighAfter |= maskForInterrupt;
        if (resetting == 9)
        {
            presence = !(currentPort & maskForInterrupt);
            if (!presence)
                writing = reading = false; // ибо незачем читать или писать, если никого нет
            resetting++;
            return;
        }
        if (resetting == 16)
        {
            resetting = 0;
            return;
        }
        resetting++;
        return;
    }
    port->pinsToStrobeHighAfter |= maskForInterrupt;
    if (writing)
    {
        port->pinsToStrobeLow |= maskForInterrupt;

        byte_t b = buff[currentWriteBit >> 3] & (1 << (currentWriteBit & 0x07));
        if (b)
            port->pinsToStrobeHigh |= maskForInterrupt;

        currentWriteBit++;
        // объявим, что мы уже не пишем, если это был последний бит
        writing = currentWriteBit < bitsToWrite;
        return;
    }
    if (aquiring)
    {
        byte_t readbit = currentPort & maskForInterrupt;
        if (readbit)
            buff[currentReadBit >> 3] |= 1 << (currentReadBit & 0x07);
        else
            buff[currentReadBit >> 3] &= ~(1 << (currentReadBit & 0x07));

        currentReadBit++;
        // объявим, что мы уже не читаем, если это был последний бит
        aquiring = reading = currentReadBit < bitsToRead;
    }
    if (reading)
    {
        port->pinsToStrobeLow |= maskForInterrupt;
        port->pinsToStrobeHigh |= maskForInterrupt;
        aquiring = true;
        return;
    }
}


3. В main() крутятся еще 6 «высокоуровневых» конечных автомата, которые постоянно опрашивают DS2408 и периодически опрашивают DS18B20. Код приводить не буду, он уж слишком убогий — кому надо, тот выкачает из аттача…

4. Ну и отвечая на запросы по i2c мы, при помощи любезно предоставленной где-то здесь (по-моему — DIHALT'ом) библиотечки, отдаем считанные показания.

Код написан исключительно по описаниям 1wire (ага, и исходникам 1wire ардуины — первоначальные тайминги взяты оттуда), тайминги (9-6-50-5) в конечном счете подобраны по принципу «так реже ошибки приходят».

В конце концов оно заработало, причем стабильно (ага, очень, если не считать того, что 5 из 9 датчиков температуры всегда показывают 85 градусов (power on default) — питания, видимо, не хватает — провода длинные и тонкие)…

Не умеет оно искать устройства на шине 1wire — как-то не нужно было — с этим справился пример к ардуине :). Но при использованном подходе можно дописать в «логическую» часть.

Если у кому-нибудь показалось интересным, и есть вопросы — попробую ответить и сделать статейку более понятной ;)

Буду очень признателен, если кто увидит и укажет на криво изобретенные велосипеды и прочую несуразность.

PS
— почему atmega168, а не тот же stm8? а потому что только под него умею (и, видимо, зря);
— почему дорогой DS2408 а не atmega/attiny/другая мелочь? а потому что с ними я уже работать пробовал…
— зачем своя интерфейсная плата, а не DS2482-800? а она не работает с каналами параллельно, да и в I2C будет предоставлять слишком низкоуровневый интерфейс…
  • 0
  • 28 января 2012, 15:56
  • vlbuel
  • 1
Файлы в топике: 1wire8_interrupt_src.zip

Комментарии (30)

RSS свернуть / развернуть
Главное чтобы другое прерывание не перебило, а то тайминги там очень мелкие, сшибить его можно на раз. Я бы таймслот обрабатывал напрямую, а биты отдельные по прерыванию. Ну 1-wire кошерней всех вешать на один провод, а не заводить четыре пина под них.
0
Там из других прерываний — только i2c, и обмен по ней — байт 100 в секунду, немного вроде. Но может и подгаживает — по 1wire где-то 0.1% транзакций битые приходят (crc рулез).
Вешать всех на 1 провод, а не на 6 (это в топике я остальные на портД опустил для краткости) — там и длина провода получится метров под 150 — датчики «звездой» расходятся — тоже не очень кошерно. Да и тогда огород городить не надо было — DS2482-800 или что-то похожее явно лучше справится когда нет требования работать параллельно ;)
Я бы таймслот обрабатывал напрямую, а биты отдельные по прерыванию
а вот тут можно мысль подробнее раскрыть?
0
Ну у тебя все временные интервалы тикаются прерываниями. А 1-wire он же полусинхронный ЕМНИП. Т.е. есть строб-дрыг и есть период когда надо считать ответ. Так вот чтобы не просрать таймслот мы его считываем блокирующим способом. А вот следующий можно и потом, по прерыванию. Слейв то никуда не денется.
0
Т.е. не на 15-ой микросекунде читаем состояние линии, а на 9-ой «сохраненный порт»=0xff, по PINCHANGE делаем «сохраненный порт» &= PORTB, и на микросекунде так 20-ой обрабатываем что наснимали? И типа делаем вид, что принять нолик от слэйва нам важней, чем сделать качественный по таймингам строб?
Мысль интересная, но, ИМХО, проверять надо. Попробую как-нибудь в mega168 под V-USB (там как раз жесть с прерываниями) код на один канал включить и посмотреть что будет с обоими вариантами — отпишусь обязательно.
Единственное что смущает — а оно не зафлудит своими PINCHANGE'ими?
0
Нет я не про это. А про то, что принял бит — отдал управление системе. Получил управление по таймеру — принял следующий бит, отдал управление системе. Получается, что передача по 1-wire идет медленней, но зато не тормозит систему.
0
Вообще — да. В STEP_AQUIRE с заблокированными прерываниями в OneWireHW::PrepareForNextSlot мы сидим достаточно долго — первая попытка даже одну линию впихнуть к V-USB показала, что ЮСБ отваливается нафиг очень быстро. Там, если что-то еще хочет нормально работать, придется сделать sei() после чтения порта и на следующем шаге повторять таймерное ожидание мсек 5-10 пока флаг готовности не установлен.
Таймслот в 1wire можно растягивать аж до 120мсек ж)
0
На один провод вешать хоть и кошерней, но бывают случаи когда нежелательно. Делал на заказ контроллер для инкубатора, так вот одним из требований было чтобы замыкание/обрыв одного сенсора не лишал систему всех остальных.
0
Классы то зачем?
0
Сказывается привычка — привык под плюсами и шарпами в прикладных вещах. Оверхеда нет, а выглядит привычней — почему бы и нет? Когда писал это, не думал, что буду выкладывать, и что этим может попробовать возпользоваться кто-то еще.
0
Таки да, 1-Wire полусинхронный. В тацмслоте тайминги жесткие, между таймслотами может пройти хоть столетие. Так что лучше всего именно блокировать прерывания на время таймслота, а потом отдавать управление.
0
  • avatar
  • _YS_
  • 28 января 2012, 19:29
Ну не знаю по поводу «столетия», проверил 32767 микросекунд дополнительно на каждый бит — скушало (протокол-то протоколом, а вот на девайсе проверить надо было), до столетия инта не хватило.
Честно говоря, не думал, что прокатит. Так что действительно стОит переписать слегка — займусь.
0
Вспомнилась цитата с Башорга
Snaky: Ассемблерщик в душе это тиран, сержант, рабовладелец и собаковод в одном лице. Каждый «бесполезный» такт МК воспринимается как личный вызов. Процессор должен любую команду пользователя выполнить за 3, нет лучше за 2 мкс. Остальные 399.998 миллисекунд до следующей команды он может махать хвостиком — «Смотри, хозяин, все готово!» :>>
0
Цитатка доставила))
0
Дёргать сам порт в таком стиле
PORTB &= ~pgl; // теперь опустим эти пины
PORTB |= pgh; // теперь поднимем эти пины
не нужно. Поскольку линия квазидвунаправленная с подтяжкой внешним резистором, то порт сбрасывается один раз в начале, а рулить можно одними только DDRx. Да! И при считывании пина желательно делать тройное сэмплирование с мажоритарным клапаном — на длинных и шумных линиях избавляет от кучи проблем. Проверено.
0
Здравствуйте. Применил эту уникальную библиотеку. И всё бы хорошо, но при подключении двух датчиков считывается только один. Причем, если подключен только один датчик, любой из двух, то всё считывается. Проверил с помощью другой библиотеки — опрашиваются оба датчика. Паразитное питание отсутствует. Не могу понять, что происходит. Помогите, пожалуйста)
0
Похоже тема давно померла. От себя скажу, что библиотека написана для ситуации один датчик — один пин. То есть для каждого датчика нужно выделять отдельную линию. Для опроса нескольких датчиков на одной линии нужна адресация по уникальному серийному номеру датчика, а реализации этого в данной библиотеке просто нет.
0
Отнюдь, коллега, смею не согласиться — в программе на каждом пине сидят по несколько датчиков и одной кнопке, опрашиваются с помощью функции Troller::TransGetTemp(uint8_t addr[]).
Всё, как полагается, опрашивается записью байта 0х55, затем 8 байт адреса, затем байт 0хBE, затем чтение 9 байт.
Всё, в принципе, по фен-шую, единственное — тайминги пришлось подрихтовать немного. И теперь беда с опросом двух датчиков. Отключение опроса одного из датчиков ни к чему не приводит.
0
День добрый.
Вообще — странно, по сей день трудятся в конфигурациях по одному\два ds18b20 + одна ds2408 на канал.
Подсоединено телефонным 4х-жильным проводом (длина в самой плохой части ~30м) и все нормально.
Разве что на питание пришлось керамический конденсатор повесить рядом с каждой ds18b20 [не помню какой, но не маленький] — а то при замере он напряжение на питании просаживал так, что всем плохо становилось.

Может и у вас с питанием проблема? У меня, если не изменяет память, измерение стартует одновременно на всех ds18b20, и это может быть отличием от «другой библиотеки». Тупо не хватает питания двум датчикам одновременно измерять (да, ds18b20 жрут просто неприлично много, на мой взгляд).
0
Здравствуйте. Спасибо, что ответили, а то я уж, грешным делом, начал Di Halta дергать.
Меня не покидает мысль, что второй датчик на паразитном питании, хотя опреедляется как на нормальном.
Добавил к вашей библиотеке запуск конвертации по адресу — та же песня — меняю в функции ns() StartConvert() на StartConvertAdress(Slaves_IDs[0]). В итоге первый датчик — 0, второй датчик — 261 (считывается, но не конвертится). Slaves_IDs-двухмерный массив uint8_t, формируемый функцией Search_ROM, которую тоже добавил к библиотеке. Менял значения ROM на константные массивы — та же канитель. Скетч DallasTemperatures из библиотек Ардуино работает без изъянов — оба датчика опрашиваются без ошибок, паразитное питание отсутствует.
0
единственное отличие датчиков — один в влагозащищенном корпусе, второй в TO-92, ерунда с влагозащищенным. Но это уже что-то про свет Венеры, отразившийся в болотном газе. ROM обоих датчиков начинаются с 0x28, поэтому, без сомнения — оба DS18B20.
0
Ну, зачем же сразу тяжелую артиллерию теребить :)
Но тут лучше код в студию \ в личку ( vlbuel@gmail.com ), со слов сложновато. Вдруг там какая-то досадная опечатка ;)
0
с удовольствием выложу код на всеощее обозрение, когда узнаю, как здесь это делается)))
а то пытался вставить сюда внутри тега CODE, но сайт пожурил, чтобы я не выкладывал всякую длинную каку)) кидаю на почту)
0
Я (теперь) делаю обычно вот так — bitbucket.org/dk866/avr8reletwi/src/default/
Правда уже с hg на git перешел, чего и всем советую. Удобно :)
0
0
Вообще, смущает меня в ns как идет переход между temp_state: в состоянии 1, когда подходит время «считать показания», мы даем команду «считать датчик 0». Тут же перескакиваем в состояние 2, и даем команду считать уже датчик 1. Мне кажется, что как и в StartConvert надо установить время, когда уже можно будет давать следующую команду на шину (да в той же to_convert), и делать «if (temp_state == 2 && to_convert < now)».
Учитывая, насколько быстро происходит перещелкивание, скорее всего исполняется команда только для slave_id[1].

Ну и проблема с питанием тоже может быть (но уже после исправления вышеописанного), и если будет иметь место — стОит расширить машину состояний, чтобы измерять \ читать по очереди.
0
Владимир, добрый вечер. В принципе, я как только не пробовал переписывать автомат ns, даже отдельно заставлял конвертиться и считываться
//delay(10);
		if (!hw || !hw->IsReady()) return;
		
		if (temp_state == 0) 
		{
			temp_state = 1;
			StartConvertAdress(Slaves_IDs[0]);
			return;
		}  
		if ((temp_state == 1)&&(to_convert < now)) 
		{
			temp_state = 2;
			TransGetTemp(Slaves_IDs[0]);
			return;
		}
		if (temp_state==2) 
		{
			temp_state = 3;
			temps[0] = Convert_temp(hw->buff);
			StartConvertAdress(Slaves_IDs[1]);
			return;
		}
		if ((temp_state==3)&&(to_convert < now)) 
		{
			temp_state = 4;
			TransGetTemp(Slaves_IDs[1]);
			return;
		}
		if (temp_state==4)
		{
			temp_state = 0;
			temps[1] = Convert_temp(hw->buff);
			return;
		}

переставлял местами адреса — меняются местами датчики — считывается второй, а не считывается первый. Возможно, я так подумал, загадка кроется в ROM устройтсв.
У меня следующие:{40, 185, 105, 84, 10, 0, 0, 209} и {40, 255, 127, 169, 208, 22, 5, 16}
Не считывается второй. Вижу у него в ROM число 255, 8 единиц- у вас таких нет. Может, теряется первый или последний бит при передаче?!
0
Вот смотрите.
Исполняется if ((temp_state == 1)&&(to_convert < now)) — вы говорите «шинному автомату», что надо считать измереную температуру с датчика[0]. Он только начинает это делать (а скорее всего, даже не успевает начать), тут же исполняется «if (temp_state==2)» — и «шинному автомату» говорится, что надо считать измеренную температуру с датчика [1].
TransGetTemp — он же возвращает управелние не после того, как «дело сделано», и сразу — как только дал указание. И в результате на шину НЕ послано «датчик 0, дай температуру», а сразу посылается «датчик 1, дай температуру»… Многопоточность, оно такое.

Попробуйте так:

Попробуйте поправить так:
dk@dkw:/tmp$ diff -C 5 onewirehw.cpp onewirehw.fixrd.cpp 
*** onewirehw.cpp	2020-05-01 00:01:23.052005399 +0300
--- onewirehw.fixed.cpp	2020-05-01 00:01:10.752220026 +0300
***************
*** 321,330 ****
--- 321,331 ----
      {
        hw->buff[0] = 0x55;
        memcpydk(addr, hw->buff+1, 8);
        hw->buff[9] = 0xbe;
        hw->StartTransaction(10,9);
+       to_convert = OHW_CYCLES + 1200*ONEWIRECYCLESINMS; 
      }
  	
  void Troller::StartConvert()
      {
        hw->buff[0] = OW_SKIP_ROM;
***************
*** 414,424 ****
  		{
  			temp_state = 2;
  			TransGetTemp(Slaves_IDs[0]);
  			return;
  		}
! 		if (temp_state==2) 
  		{
  			temp_state = 3;
  			
  			temps[0] = Convert_temp(hw->buff);
  			TransGetTemp(Slaves_IDs[1]);
--- 415,425 ----
  		{
  			temp_state = 2;
  			TransGetTemp(Slaves_IDs[0]);
  			return;
  		}
! 		if ((temp_state==2)&&(to_convert < now)) 
  		{
  			temp_state = 3;
  			
  			temps[0] = Convert_temp(hw->buff);
  			TransGetTemp(Slaves_IDs[1]);


оно перед тем, как посылать команду на «датчик 1, дай температуру» ждет, пока исполнится «датчик 0, дай температуру».

Это очень непривычно, но оно так работает.

PS: на самом деле, оно не совсем корректно, тайминг завышен, и надо бы в автомат 1wire делать «finished», но так, по идее, доожно работать в реальных кейзах ;)
0
и, да, это «разница» с тем, что было в репозитарии. Репозитарий стОит обновлять ;)
0
Доброго времени суток. Вечером обязательно попробую — длительный переезд на дачу. Спасибо большое
0
Добрый вечер. Надежда была, но все попытки остались тщетны. Всё больше думаю о какой-то проблеме с передачей адреса. Хотя по одному датчики опрашиваются адресно нормально — поэтому конфуз.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.