Еще 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

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

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
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.