(Не) любителям protothreads посвящается: Высокоуровневые функции для работы с 1-Wire
В качестве примера рассмотрим задачу обслуживания устройств на шине 1-Wire. Реализацию асинхронных примитивов для нее можно посмотреть в моих предыдущих статьях здесь и здесь.
Для PnP-реализации необходимо, чтобы программа могла самостоятельно определять характеристики шины, такие как максимально допустимая скорость обмена, список идентификаторов подключенных в данный момент устройств и условия по их электропитанию.
Максимально допустимую скорость обмена мы определяем для того, чтобы впоследствии общаться с быстродействующими устройствами как можно более оперативно. При этом медленные устройства этот обмен «не заметят» и мешать нам не будут.
Условия по электропитанию надо знать для того, чтобы (при необходимости) после подачи команды на выполнение измерений (или программирования EEPROM) включить режим active pullup. В противном случае при использовании parasite power мы получим ошибку при попытке прочитать результат измерений (ну либо придется ставить низкоомные подтягивающие резисторы, что, наверное, не является красивым решением).
Ну, а уж список идентификаторов подключенных в данный момент устройств нам иметь просто жизненно необходимо. Иначе как мы собираемся к ним обращаться?
Алгоритм определения характеристик шины:
- Попытаемся выполнить процедуру RESET в режиме OVERDRIVE. Если при этом был обнаружен PRESENCE, то значит, как минимум, некоторые из подключенных устройств умеют работать на высокой скорости. В этом случае переходим к п.3.
- Попытаемся выполнить процедуру RESET в нормальном режиме. Если при этом был обнаружен PRESENCE, то на шине 1-Wire имеется, как минимум, одно подключенное устройство и мы переходим к п.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.
Список литературы:
- Код проекта на github
- Примитивы для реализации 1-Wire master при помощи PWM и ICP для STM8L и STM32
- Примитивы для реализации 1-Wire master при помощи PWM и ICP на микроконтроллерах AVR AtMega
- Protothreads from Adam Dunkels
- Хабр от ldir Многозадачность в микроконтроллерах на основе продолжений
- Хабр от LifeV Protothread и кооперативная многозадачность
- APPLICATION NOTE 187. 1-Wire Search Algorithm