(Не) любителям protothreads посвящается: Высокоуровневые функции для работы с 1-Wire

    Подразумевается, что мы будем писать прошивку под «голое железо». В противном случае применение protothreads смысла не имеет, т.к. мультизадачность должна обеспечиваться средствами ОС. Подразумевается также, что нам необходимо реализовать несколько более-менее сложных алгоритмов, связанных с операциями ввода-вывода. Ну и, как всегда в микроконтроллерах, очевидные требования по экономии RAM и энергопотребления.

    В качестве примера рассмотрим задачу обслуживания устройств на шине 1-Wire. Реализацию асинхронных примитивов для нее можно посмотреть в моих предыдущих статьях здесь и здесь.

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

    Условия по электропитанию надо знать для того, чтобы (при необходимости) после подачи команды на выполнение измерений (или программирования EEPROM) включить режим active pullup. В противном случае при использовании parasite power мы получим ошибку при попытке прочитать результат измерений (ну либо придется ставить низкоомные подтягивающие резисторы, что, наверное, не является красивым решением).

    Ну а уж список идентификаторов подключенных в данный момент устройств нам иметь просто жизненно необходимо. Иначе как мы собираемся к ним обращаться?

    Алгоритм определения характеристик шины:
    1. Попытаемся выполнить процедуру RESET в режиме OVERDRIVE. Если при этом был обнаружен PRESENCE, то значит, как минимум, некоторые из подключенных устройств умеют работать на высокой скорости. В этом случае переходим к п.3.
    2. Попытаемся выполнить процедуру RESET в нормальном режиме. Если при этом был обнаружен PRESENCE, то на шине 1-Wire имеется, как минимум, одно подключенное устройство и мы переходим к п.3. В противном случае подключенных к шине устройств в данный момент нет.
    3. Передаем на шину команду «Адресовать все устройства» и затем команду «Прочитать условия электропитания». В случае, если хотя бы одно из подключенных устройств использует режим parasite power,
      установим соответствующий флаг.

    А вот алгоритм определения идентификаторов подключенных устройств достаточно громоздкий. Его синхронная реализация приведена в APPLICATION NOTE 187, я просто переделал ее в асихнронную.

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

    Далее предполагается, что у читателя есть минимальные знания о protothreads. Кому тяжело понимать англоязычные тексты, может, для начала, почитать здесь и здесь.

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

    Основная программа
        PT_INIT(&ptSearchContext.pt);
        
        /* Определение максимальной скорости обмена и условий электропитания подключенных устройств */
        while(PT_SCHEDULE(c = ptOneWireProbeBus(&ptSearchContext.pt, &nested))) {
            if(PT_WAITING == c) {
                /* Операция в процессе выполнения, можно заняться чем-либо еще */
                waitComplete();
                
                continue;
            }
        }
        
        /* Инициализируем контекст перед первым вызовом */
        ptOneWireInitWalkROM(&ptSearchContext);
        
        /* Выполнение задачи определения подключенных к 1-Wire устройств */
        while(PT_SCHEDULE(c = ptOneWireWalkROM(&ptSearchContext))) {
            if(PT_WAITING == c) {
                /* Операция в процессе выполнения, можно заняться чем-либо еще */
                waitComplete();
                
                continue;
            }
            
            /* На шине 1-Wire обнаружено очередное устройство.
             * Его S/N находится в массиве ptSearchContext.romid
             */
            __no_operation();
        }
        
        /* Все устройства просканированы */
        __no_operation();
    


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

    Макрокоманды, используемые в реализации
    #define OVERDRIVE()                                                     \
        drv_onewire_context.overdrive
            
    #define PRESENCE_DETECTED()                                             \
        drv_onewire_context.presence
    
    #define PARASITE_POWER                                                  \
        drv_onewire_context.parasite
            
    #define STATUS                                                          \
        drv_onewire_context.status
            
    #define PT_WAIT_IO_COMPLETE()                                           \
        PT_WAIT_WHILE(TASK_CONTEXT, ONEWIRE_STATUS_PROGRESS == (dummy = drvOneWireStatus()))
    
    #define IO_SUCCESS()                                                    \
        (ONEWIRE_STATUS_COMPLETE == dummy)
    
    #define PT_TX_BITS(_v,_n) do {                                          \
        if(drvOneWireTxBits((_v),(_n))) {                                   \
            PT_WAIT_IO_COMPLETE(); } else {                                 \
            dummy = ONEWIRE_STATUS_ERROR; } } while(0)
            
    #define PT_TX_BYTE(_v)                                                  \
        PT_TX_BITS((_v), 8)
            
    #define PT_RX_BITS(_n)                                                  \
        PT_TX_BITS(~0,(_n))
            
    #define PT_TX_BYTE_CONST(_v) do {                                       \
        PT_TX_BYTE((_v));                                                   \
        if(!IO_SUCCESS() || ((_v) != drvOneWireRxBits(8))) {                \
            STATUS = ONEWIRE_STATUS_ERROR; } } while(0)
    


    Краткое описание:

    PT_WAIT_IO_COMPLETE()
    Ожидание завершения операции ввода/вывода. Предназначена для применения только внутри protothread.

    PT_TX_BITS(_v,_n)
    Передача _n бит из значения _v на шину с ожиданием завершения операции ввода/вывода. Предназначена для применения только внутри protothread.

    PT_TX_BYTE(_v)
    Передача байта _v на шину с ожиданием завершения операции ввода/вывода. Предназначена для применения только внутри protothread.

    PT_RX_BITS(_n)
    Прием _n бит с ожиданием завершения операции ввода/вывода. Предназначена для применения только внутри protothread.

    PT_TX_BYTE_CONST(_v)
    Передача байта команды (констатны) на шину с проверкой отсутствия искажений передаваемых данных и ожиданием завершения операции ввода/вывода. Предназначена для применения только внутри protothread.

    Следует отметить, что «ожидание завершения ввода вывода» в данном случае обозначает не глухой цикл с проверкой какого-либо условия, а прерывание текущей protothread со статусом PT_WAITING. Это позволяет выполнять другие protothreads с периодической проверкой текущей до момента завершения активированной операции ввода/вывода.

    Передача команды адресации всех устройств
    PT_THREAD(ptOneWireTargetAll(struct pt * _pt)) {
        uint8_t dummy;
        
        PT_BEGIN(TASK_CONTEXT);
    
        PT_TX_BYTE_CONST(OP_SKIP_ROM);
        
        PT_END(TASK_CONTEXT);
    }
    


    Операция адресации всех устройств может быть использована и с другими командами шины 1-Wire, поэтому она была оформлена в виде отдельной protothreads.

    Процедура определения параметров шины
    PT_THREAD(ptOneWireProbeBus(struct pt * _pt, struct pt * _nested)) {
        uint8_t dummy;
        
        PT_BEGIN(TASK_CONTEXT);
        
        /* Parasite power not detected */
        PARASITE_POWER = 0;
        
        /* Try overdrive procedure first */
        if(drvOneWireReset(1)) {
            PT_WAIT_IO_COMPLETE();
            
            if(!IO_SUCCESS() || !PRESENCE_DETECTED()) {
                /* Overdrive RESET procedure failed */
                if(drvOneWireReset(0)) {
                    PT_WAIT_IO_COMPLETE();
            
                    if(!IO_SUCCESS() || !PRESENCE_DETECTED()) {
                        /* No devices on the bus */
                        PT_EXIT(TASK_CONTEXT);
                    }
                } else {
                    /* Hardware BUSY */
                    PT_EXIT(TASK_CONTEXT);
                }
            }
        } else {
            /* Hardware BUSY */
            PT_EXIT(TASK_CONTEXT);
        }
        
        PT_SPAWN(TASK_CONTEXT, _nested, ptOneWireTargetAll(_nested));
    
        if(ONEWIRE_STATUS_COMPLETE == STATUS) {
            PT_TX_BYTE_CONST(OP_READ_POWER_SUPPLY);
    
            if(IO_SUCCESS()) {
                /* Read one bit after command */
                PT_RX_BITS(1);
        
                if(IO_SUCCESS()) {
                    /* Fetch bit value */
                    int16_t value = drvOneWireRxBits(1);
                
                    if(value < 0) {
                        /* Rx bit decode failed */
                        STATUS = ONEWIRE_STATUS_ERROR;
                    } else {
                        /* If any device sent "0" then it used parasite power */
                        PARASITE_POWER = value ? 0 : 1;
                    }
                }
            }
        }
        
        PT_END(TASK_CONTEXT);
    }
    


    Код достаточно простой и реализует описанный выше алгоритм.

    Процедура определения идентификаторов подключенных устройств
    PT_THREAD(ptOneWireWalkROM(pt_onewire_search_context_t * _ctx)) {
        PT_BEGIN(TASK_CONTEXT);
        
        while(!LAST_DEVICE_FLAG) {
            int16_t dummy;
            
            /* initialize for search */
            ID_BIT_NUMBER = 1;
            LAST_ZERO = 0;
            ROM_BYTE_NUMBER = 0;
            ROM_BYTE_MASK = 1;
            
            /* 1-Wire reset (dependent on OVERDRIVE flag) */
            PT_ONEWIRE_RESET();
            
            if(!IO_SUCCESS() || !PRESENCE_DETECTED()) {
                // reset the search
                LAST_DISCREPANCY = 0;
                LAST_DEVICE_FLAG = 0;
                LAST_FAMILY_DISCREPANCY = 0;
    
                /* If presence not detected then no devices on the bus */
                PT_EXIT(TASK_CONTEXT);
            }
    
            /* issue the search command */
            PT_TX_BYTE(OP_SEARCH_ROM);
            
            if(!IO_SUCCESS() || (OP_SEARCH_ROM != drvOneWireRxBits(8))) {
                /* Send command error, repeat procedure from RESET point */
                
                /* Other solution is abort search procedure */
                
                continue;
            }
            
            // loop to do the search
            do {
                // read a bit and its complement
                PT_RX_BITS(2);
                
                if(!IO_SUCCESS()) {
                    /* Error while receiving 2 bits.
                     * As ID_BIT_NUMBER less than 65 search procedure
                     * resumed from state such as original task entry.
                     */
                    break;
                }
    
                if((RX_VALUE = drvOneWireRxBits(2)) < 0) {
                    __no_operation();
                    
                    break;
                }
                
                uint8_t id_bit = (RX_VALUE & 0x01) ? 1 : 0;
                uint8_t cmp_id_bit = (RX_VALUE & 0x02) ? 1 : 0;
    
                uint8_t search_direction;
                
                /* check for no devices on 1-wire */
                if ((id_bit == 1) && (cmp_id_bit == 1)) {
                    /* Same bit values equ "1" indicate no devices on the bus */
                    break;
                } else {
                    /* all devices coupled have 0 or 1 */
                    if (id_bit != cmp_id_bit) {
                        search_direction = id_bit;  /* bit write value for search */
                    } else {
                        /* if this discrepancy if before the LAST_DISCREPANCY
                         on a previous next then pick the same as last time */
                        if (ID_BIT_NUMBER < LAST_DISCREPANCY) {
                            search_direction = (ROMID_BYTE_REF(ROM_BYTE_NUMBER) & ROM_BYTE_MASK) ? 1 : 0;
                        } else {
                            /* if equal to last pick 1, if not then pick 0 */
                            search_direction = (ID_BIT_NUMBER == LAST_DISCREPANCY) ? 1 : 0;
                        }
    
                        /* if 0 was picked then record its position in LAST_ZERO */
                        if (search_direction == 0) {
                            LAST_ZERO = ID_BIT_NUMBER;
                        }
    
                        /* check for LAST_FAMILY_DISCREPANCY in family */
                        if (LAST_ZERO < 9) {
                            LAST_FAMILY_DISCREPANCY = LAST_ZERO;
                        }
                        
                    }
                }
    
                /* set or clear the bit in the ROM byte ROM_BYTE_NUMBER
                   with mask rom_byte_mask */
                if (search_direction == 1) {
                    ROMID_BYTE_REF(ROM_BYTE_NUMBER) |= ROM_BYTE_MASK;
                } else {
                    ROMID_BYTE_REF(ROM_BYTE_NUMBER) &= ~ROM_BYTE_MASK;
                }
    
                /* serial number search direction write bit */
                PT_TX_BITS(search_direction, 1);
                
                /* search_direction not stored, therefore we can't check echo */
    
                if(!IO_SUCCESS()) {
                    /* Sending direction failed.
                     * As ID_BIT_NUMBER less than 65 search procedure
                     * resumed from state such as original task entry.
                     */
                    break;
                }
    
                /* increment the byte counter ID_BIT_NUMBER
                   and shift the mask rom_byte_mask */
                ID_BIT_NUMBER++;
                ROM_BYTE_MASK <<= 1;
    
                /* if the mask is 0 then go to new SerialNum byte ROM_BYTE_NUMBER and reset mask */
                if (ROM_BYTE_MASK == 0) {
                    ROM_BYTE_NUMBER++;
                    ROM_BYTE_MASK = 1;
                }
            } while(ROM_BYTE_NUMBER < 8);  /* loop until through all ROM bytes 0-7 */
    
            /* if the search was successful then */
            if(!(ID_BIT_NUMBER < 65)) {
                uint8_t i;
                
                /* Calculate CRC */
                uint8_t crc = 0;
                for(i = 0;i < sizeof(ROMID);i++) {
                    crc = modOneWireUpdateCRC(crc, ROMID_BYTE_REF(i));
                }
                
                if(crc) {
                    /* CRC error.
                     * Repeat procedure from original point
                     */
                    continue;
                }
                
                /* search successful so set LAST_DISCREPANCY and LAST_DEVICE_FLAG */
                LAST_DISCREPANCY = LAST_ZERO;
    
                // check for last device
                if (LAST_DISCREPANCY == 0) {
                    LAST_DEVICE_FLAG = 1;
                }
    
                /* Next device detection complete */
            } else {
                /* I/O error.
                 * Retry procedure from original point
                 */
                continue;
            }
            
            if(!ROMID.id.familyCode) {
                /* familyCode не должен быть равен 0! */
                break;
            }
            
            /* Return detected device S/N */
            PT_YIELD(TASK_CONTEXT);
        }
    
        /* Reset state for next scan loop (if need) */
        ptOneWireInitWalkROM(CONTEXT);
        
        PT_END(TASK_CONTEXT);
    }
    
    /*
     * Initialize device search procedure
     */
    void ptOneWireInitWalkROM(pt_onewire_search_context_t * _ctx) {
        /* Prepare ptOneWireWalkROM() for first call  */
        LAST_DISCREPANCY = 0;
        LAST_DEVICE_FLAG = 0;
        LAST_FAMILY_DISCREPANCY = 0;
    
        /* Initialize protothreads data */
        PT_INIT(TASK_CONTEXT);
    }
    


    Я просто взял синхронную реализацию от Maxim и заменил обращения к процедурам ввода/вывода на асинхронные макрокоманды. Все это вместе с прогонкой тестовых примеров заняло у меня порядка получаса. Интересно, сколько пришлось бы возиться без применения обертки protothreads?

    Полный исходный код проекта для STM8L-Discovery board размещен на github. Для создания сборки с вышеприведенными примерами при компиляции необходимо определить символ HIGH_LEVEL.

    Список литературы:

    1. Код проекта на github
    2. Примитивы для реализации 1-Wire master при помощи PWM и ICP для STM8L и STM32
    3. Примитивы для реализации 1-Wire master при помощи PWM и ICP на микроконтроллерах AVR AtMega
    4. Protothreads from Adam Dunkels
    5. Хабр от ldir Многозадачность в микроконтроллерах на основе продолжений
    6. Хабр от LifeV Protothread и кооперативная многозадачность
    7. APPLICATION NOTE 187. 1-Wire Search Algorithm
    Метки:
    • +15
    • 2,7k
    • 1
    Поделиться публикацией
    Комментарии 1
    • 0
      Все это вместе с прогонкой тестовых примеров заняло у меня порядка получаса. Интересно, сколько пришлось бы возиться без применения обертки protothreads?

      Час, при наличии другой обертки?


      Я не отношу себя к противникам protothreads. т.к. читал статьи Адама. Он вполне обосновано утверждает, что protothreads понятны и прозрачны, т.к. код подобен использованию RTOS с вытесняющей многозадачностью и т. п.


      Но зачем всё оборачивать в макросы? Например, STATUS — это делает код только хуже.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.