Шаг 5.9. CRC, паразитное питание и всякая всячина
Итак, казалось, все было отлично. Датчик выдавал нам пусть не особо точную, но все же температуру, потягивал потихонечку питание со своего выхода на +5V, и все радовались жизни.
Однако мы-то легких путей не ищем, нам подавай максимальную точность и проверку на правильное считывание. Да будьте добры ещё использовать паразитный режим, чтобы вместо трехжильного провода использовать обычный двухжильный, который есть в любом хозяйственном - если нам вдруг вздумается выкинуть датчик подальше на улицу.
Ну что ж. Разберемся со всем по порядку.
Точность.
Эта часть несколько раз переписывалась, и мы решили изменить практически все. Ох.
Мы практически полностью разделили функции. Так, теперь getTemp у нас только считывает температуру в глобальную переменную, а convertTemp - отдельно преобразует её - тоже в глобальную переменную, и так далее.
Мы даже засунули датчик в холодильник (оооооооооо, это было чудесно-удивительно!!!) и долго плясали с переходом на темную отрицательную сторону)
Итак, основная задача - чтобы работало по следующей формуле:
Значит, нам потребуется вся память датчика... На самом деле мы пробовали сохранять и в отдельные переменные, и в массивчик, но самым удобным оказалось создать структуру памяти.
struct tempMemory //структура памяти датчика { int TEMP; //значение температуры byte EEPh; //старший байт энергонезав. памяти датчика byte EEPl; //младший байт энергонезав. памяти датчика byte reserve1; //два зарезервированных байта byte reserve2; byte countRemain; //сколько "шагов" осталось до целого градуса byte countPerC; //сколько всего "шагов" - в ds1820 16 byte CRC; //значение CRC } ourSensor;
Вот так.
Соответственно, поменяется и считка памяти. В функции getTemp после подачи всяких команд чтение уже 9 байт будет выглядеть так:
byte* currentByte; //указатель, куда записываем текущий байт ... currentByte = (char*)&ourSensor; //поставили указатель в начало структуры for(i = 0; i < TempMemoryCount; i++) //считываем память датчика в структуру. Умничка записывает int //правильно - сначала младший байт, потом старший, как и надо { *currentByte = OWRead(bPortTemp, bPinTemp); CRC = getCRC(CRC, *currentByte); //проверка CRC currentByte++; //перевели указатель }
Вот и отлично, все получается. Самое главное - что правильно записывается int, то есть в памяти он хранится как сначала младший байт, а потом - старший, как и в памяти датчика. Да-да, AVR являются "остроконечниками", как и вся x86 платформа. То есть порядок байт в памяти подчиняется простому мнемоническому правилу: старший адрес, старший байт.
Теперь о преобразовании температуры, нашей новой функции convertTemp. Во-первых, сначала надо отбросить значение "дробной части" - младшего бита нашего int-вого значения. Самое простое - это просто сдвинуть на один бит вправо, но нужно не забыть сохранить знак (помним о холодильнике!). А потом - просто использовать нашу формулу, и все будет хорошо. Напоминаю, что функция пишет все в глобальную переменную fTemp. (Сразу хочу предостеречь: Некоторые "умники" пытаются заменить сдвиг целочисленным делением на 2. Так делать нельзя! На отрицательных значениях результат такого сдвига делением ависит от реализации алгоритма и может "округлить" результат в неправильную сторону ><)
//перевод температуры в красивый вид //данные получаем из структуры ourSensor, запись - в fTemp void convertTemp() { //разбираемся с целой частью - убираем младший бит, но сохраняем знак fTemp = (int)((ourSensor.TEMP & 0x8000) | (ourSensor.TEMP >> 1)); //без конкретного указания типа не работает( //разбираемся с дробной частью - как по даташиту fTemp = fTemp - 0.25 + (float)(ourSensor.countPerC - ourSensor.countRemain) / ourSensor.countPerC; }
Теперь об ошибках. На данный момент у нас две возможные ошибки: устройство не найдено и неправильная считка. Определять отсутствие устройства мы умеем, а вот с ошибками при считке...
Краткий экскурс - crc.
CRC - циклический избыточный код; число, с помощью которого можно проверить целостность передачи или хранения информации. То есть после передачи мы подсчитываем по определённому алгоритму, использующему все прочитанные байты (иногда биты), и сравниваем с присланным значением. Ну или используем такое свойство crc, что при пропускании через алгоритм самого значения crc в качестве «нового байта» он выдает нам ноль.
//получение CRC //на вход - считаемое уже crc - его мы и изменяем, новый байт byte getCRC(byte CRC, byte NewByte) { const byte poli = 0b10001100; //полином, с помощью которого высчитываем CRC8 byte i; //счётчик for(i = 0; i < 8; i++) { if((CRC ˆ NewByte) & 0x01) CRC = (CRC >> 1) ˆ poli; //младший бит CRC не равен пришедшему биту // - исключающее ИЛИ else CRC = CRC >> 1; //равны - просто сдвиг NewByte = NewByte >> 1; } return CRC; }
Итак, подсчитали crc. Теперь о «сигнализации» ошибок: Можно, конечно, использовать байтовую переменную и обозначать ошибки типа «1», «2» и так далее. Но так же непонятно! На это есть перечисления - enum:
//перечисление ошибок enum err { ER_Not_errs, //нет ошибок ER_Not_found, //датчик не найден ER_Bad_crc //неверное значение CRC } errCurrent;
Таким образом, глобальная переменная errCurrent типа enum err (да-да, для перечислений нужно слово enum) имеет значения ER_Not_errs, ER_Not_found, ER_Bad_crc. При этом на самом деле ER_Not_errs соответствует 0, ER_Not_found - 1, а ER_Bad_crc - 2. Но так значительно удобнее) Итак, при измерении температуры мы сначала считаем, что ошибок нет - errCurrent равна 0, а в случае ошибок изменяем эту глобальную переменную. А уже в main-е определяем, печатаем ли мы температуру, или же вызываем функцию, обрабатывающую ошибки:
//функция передаёт сообщение об ошибке (errCurrent) через USART, который уже должен быть готов к работе void sendErr() { char sTemp[15]; switch(errCurrent) { case ER_Not_found: //датчик не найден { strcpy(sTemp, "Not found"); break; } case ER_Bad_crc: //crc не сходится { strcpy(sTemp, "Bad crc"); break; } } UART1_Write_Text(sTemp); }
А вот теперь о паразитном питании...
... нашем горе-печали. В попытках реализовать было, кстати, немножечко сведено с ума два датчика. Теперь они считают, что паразитное питание ВСЕГДА. ОМГ. Но это уже были пляски с транзистором - в даташите он присутствует на схеме. На самом деле он вообще не обязателен. Чтобы понять, откуда ноги растут, необходимо окунуться в историю. Хотя датчик DS1820 можно запитывать через линию 1-wire, тока, протекающего через резистор подтяжки, явно недостаточно. Казалось бы, простое решение: передавать в датчик энергию, просто переключая ножку процессора в логическую "1" и подавая, тем самым, положительный потенциал на линию 1-wire. Но это было не всегда приемлемо, ибо в старых микроконтроллерах и ток через ножку был ограничен, и напряжение логической "1" было около 4.5 вольт, а вовсе не 5. Именно из-за этого приходилось применять дополнительный транзистор, включенный как усилитель, подающий в нужный момент питание на вход 1-Wire полной мерой. Однако в современных контроллерах и нагрузочная способность выше, и напряжение ближе к напряжению питания. Поэтому-то при включении ds1820 по схеме с паразитным питанием можно обойтись и без транзистора. Достаточно просто подключить выход VCC датчика к земле (1 и 3 выводы закоротить на землю).
К сожалению, теоретическим выкладкам не суждено было стать реальностью. Проблема была в программе. А точнее, в библиотеке. После некоторого чесания маковки - то есть прыжков с осциллографом, переписывания нашей программки, ритуального питья чая со сгущенкой и прочих полезных действий - было выяснено что стандартная библиотека вместо состояния логической 1 переводила ножку в высокоимпедансное Z-состояние, когда микроконтроллер только "слушает", никак не влияя на линию. (ddrX = 0; portX = 0;) Логическая 1 в таком состоянии получалась исключительно из-за подтяжки линии резистором 4.7k к питанию... Поэтому датчики недополучали питания и все было печально(
Так уж получилось, что стандартная библиотека, строго соответствуя заветам 1-Wire, была предназначена исключительно для внешнего питания. В результате, было принято решение таки написать свою библиотечку 1-Wire, ибо встроенная, к несчастью, закрыта.
С другой стороны, чисто теоретически (хотя по-хорошему протокол на то и протокол, чтобы недопускать такие вещи), "мастер" может послать на линию эту самую "1", а датчик в это время - выдавать что-нибудь. И послать "0". И тогда ой-ей, короткое замыкание, быдыщ-быдыщ! Запомните, дети, нельзя соединять два выхода вместе, если на одном может быть установлена логическая 1, тогда как на другом логический 0. Иначе и ваша коллекция пополнится забавными датчиками, питающимися только per rectum - как у нас)
В общем, и пишем на свой страх и риск) Ну, на самом деле пришлось не писать вчистую, а переписать все с Паскаля - MayDay (мастер MayDay =ˆˆ=) уже давно написал вот тут. В результате получено следующее - 1wire.h:
//переименование типов typedef unsigned char byte; typedef unsigned int word; //состояние устройства const byte owPresent = 0; //устройство есть на шине const byte owNoPresent = 1; //устройства нет на шине const byte owShort = 2; //короткое замыкание - горе-печаль const byte maxWaiting = 30; //максимальное время ожидания ответа //задержечки const word dlReset = 500; //задержка для ресета (длина импульса сброса) const byte dlWait = 9; //задержка между проверками линии const byte dlShort = 1; //таймслот const byte dlCheck = 60; //передача данных const byte dlRead = 200; //задержка при чтении бита с устройства //"расстояние" между адресами const byte deltaDdrPort = 1; const byte deltaPinPort = 2; //уровень const byte isOff = 0; //на шине установлен ноль const byte isOn = 1; //на шине установлена единица //какие функции у нас есть в библиотечке byte OWReset(byte*, byte); void OWWrite(byte*, byte, byte); byte OWRead(byte*, byte); //ожидание ответа устройства. //выдает 1, если ответ совпал с ожидаемым до таймаута, 0 - иначе //port - указатель на порт, pin - номер вывода, где расположена шина //respond - ожидаемый ответ. byte OWCheckRespond(byte* portX, byte pin, byte respond) { byte* pinX; //порт входа byte bTime; //счетчик времени pinX = portX - deltaPinPort; //определили порт входа bTime = 0; //счетчик на ноль while(bTime < maxWaiting) //пока не превышен лимит ожидания { Delay_us(dlWait); //ждем if(((*pinX >> pin) & 1) == respond) break; //выходим из цикла bTime++; //прибавляем } return(bTime < maxWaiting); } //проверка наличия подчинённого устройства //port - указатель на порт, pin - номер вывода, где расположена шина //результат: состояние устройства (см. заголовок в константах) byte OWReset(byte* portX, byte pin) { //переменные byte* ddrX; //порт направления - вот как это называется! byte* pinX; //порт входа byte shift; //сдвиг 1 << pin ddrX = portX - deltaDdrPort; //а вот так! в памяти инфа о портах лежит так: pin-ddr-port. //Этим и пользуемся, переходя указателем pinX = portX - deltaPinPort; shift = 1 << pin; //подаем импульс сброса *ddrX = *ddrX | shift; //ногу на выход *portX = *portX | shift; //даем питалово Delay_us(dlReset); //задержка перед сбросом - будь готов! *portX = *portX & (˜shift); //переводим лапу на низкий уровень Delay_us(dlReset); //импульс сброса!!! *portX = *portX | shift; //даем питалово *ddrX = *ddrX & (˜shift); //ногу на вход с подтяжкой //ждем приветствия if(OWCheckRespond(portX, pin, isOff)) //проверяем, отвечают ли нам на сброс { if(OWCheckRespond(portX, pin, isOn)) //проверяем, возвращается ли потом уровень на единицу { return(owPresent); //ура, устройство есть на шине } else return(owShort); //короткое замыкание } else return(owNoPresent); //никто нам не ответил } // запись байта в устройство // Port, Pin - номер порта и вывода с подключенным устройством // Value - записываемый байт. void OWWrite(byte* portX, byte pin, byte value) { byte* ddrX; //порт назначения byte i; //счётчик byte sregOld; //хранит изначальное значение регистра состояния Sreg - мы запрещаем прерывания byte sendingBit; //передаваемый бит byte shift; //сдвиг 1 << pin byte lineIn1, lineIn0; //значение порта, когда по одноваре 1, когда 0 sregOld = Sreg; //Sreg - регистр состояния Sreg.B7 = 0; //7 разряд - разрешены(1)/запрещены(0) прерывания shift = 1 << pin; //вычисляем соответствующие порту адреса ddrX = portX - deltaDdrPort; //порт направления *ddrX = *ddrX | shift; //вывод на выход *portX = *portX | shift; //переводим в 1 delay_us(dlWait); //немного ждём lineIn1 = *portX | shift; //Port.pin = 1; lineIn0 = *portX & (˜shift); //Port.pin = 0; for(i = 0; i < 8; i++) //процесс передачи данных { sendingBit = (value >> i) & 1; //определяем записываемый бит *portX = lineIn0; //переводим линию в 0 - знак начала записи бита delay_us(dlShort); //ждем один таймслот //!!! if(sendingBit == 1) //если записываем единицу, то поднимаем линию { *portX = lineIn1; } delay_us(dlCheck); //передаем положенное время *portX = lineIn1; //поднимаем в 1 !!! delay_us(dlShort); //перерыв между битами } Sreg = sregOld; } //чтение байта из устройства //Port, Pin - номер порта и вывода с подключенным устройством //результат - считанный байт byte OWRead(byte* portX, byte pin) { byte *ddrX, *pinX; //соответствующий порт направления и порт входа byte i; //счётчик byte sregOld; //хранит изначальное значение регистра состояния Sreg - мы запрещаем прерывания byte lineIn1, lineIn0; //значение порта, когда по одноваре 1, когда 0 byte rez; //результат byte shift; //сдвиг 1 << pin sregOld = Sreg; //запоминаем текущее значение регистра состояния Sreg.B7 = 0; //запрещаем прерывания shift = 1 << pin; //вычисляем соответствующие порту адреса ddrX = portX - deltaDdrPort; //порт направления pinX = portX - deltaPinPort; //порт входов //устанавливаем соответствующий вывод в 1 на выход *portX = *portX | shift; *ddrX = *ddrX | shift; delay_us(dlRead); lineIn1 = *portX | (1 << pin); //portX.pin = 1; lineIn0 = *portX & (˜(1 << pin)); //portX.pin = 0; rez = 0; for(i = 0; i < 8; i++) //начинаем читать { *portX = lineIn1; *ddrX = *ddrX | shift; //переводим линию в состояние выхода, поднимаем в 1 delay_us(dlShort); *portX = lineIn0; //переводим линию в 0 - начало чтения delay_us(dlShort); //небольшая задержка *ddrX = *ddrX & (˜shift); //начинаем слушать линию !!! *portX = lineIn1; //с подтяжкой delay_us(dlWait); //ждем - устройство должно откликнуться rez = rez | (((*pinX >> pin) & 1) << i); //запоминаем таким хитрейшим образом результат delay_us(dlCheck); //ждем перед следующим чтением байта } Sreg = sregOld; return rez; }
Вот так вот. Зато теперь понятно, откуда рога растут) Этот файлик 1wire.h просто добавляем в проект - Project - Add File To Project А в самом начале нашего проектного файла .c пишем строчку #include "1wire.h" Получили следующее:
//считка показателей температуры с датчика DS18s20 (внешнее питание) по протоколу 1-Wire (к порту C.0)\ // и отсылка значения через UART (RXD - D.0, TXD - D.1). Используется микросхема ATmega32 #include "1wire.h" //подключили библиотечку //переименование типов - теперь в библиотеке //объявление констант byte ddrUART at ddrD; //порт, к которому подключён UART byte portUART at portD; const byte pinRXDNumber = 0; //номер вывода RXD const byte pinTXDNumber = 1; //номер вывода TXD const byte UARTFrequency = 19200; //частота передатчика const byte ddrUARTInit = (0xFF | (1 << pinTXDNumber)) & (˜(1 << pinRXDNumber)); const byte portUARTInit = 0xFF; //заодно подтяжка для RXD byte* bPortTemp = (byte*) &portC; //порт, к которому подключён датчик температуры sbit ddrTemp at ddrC.B0; //конкретный порт назначения sbit portTemp at portC.B0; //конкретный порт вывода const byte bPinTemp = 0; //номер вывода, к которому присоединяется датчик const byte TempMemoryCount = 9; //количество байт в памяти датчика struct tempMemory //структура памяти датчика { int TEMP; //значение температуры byte EEPh; //старший байт энергонезав. памяти датчика byte EEPl; //младший байт энергонезав. памяти датчика byte reserve1; //два зарезервированных байта byte reserve2; byte countRemain; //сколько "шагов" осталось до целого градуса byte countPerC; //сколько всего "шагов" - в ds1820 16 byte CRC; //значение CRC } ourSensor; float fTemp; //значение температуры //задержки const word ConvertDelay = 1000; //для конвертации - для нормальных датчиков достаточно и 1 мс за глаза const byte DelayTime = 400; //задержка перед тем, как снова считать температуру //const word WelcomeDelay = 1; //для приветствия - датчику же нужна энергия для начала работы! <- //прыжки с бубном у исходной библиотеки //перечисление ошибок enum err { ER_Not_errs, //нет ошибок ER_Not_found, //датчик не найден ER_Bad_crc //неверное значение CRC } errCurrent; //получение CRC //на вход - считаемое уже crc - его мы и изменяем, новый байт byte getCRC(byte CRC, byte NewByte) { const byte poli = 0b10001100; //полином, с помощью которого высчитываем CRC8 byte i; //счётчик for(i = 0; i < 8; i++) { if((CRC ˆ NewByte) & 0x01) CRC = (CRC >> 1) ˆ poli; //младший бит CRC не равен пришедшему биту // - исключающее ИЛИ else CRC = CRC >> 1; //равны - просто сдвиг NewByte = NewByte >> 1; } return CRC; } //перевод температуры в красивый вид //данные получаем из структуры ourSensor, запись - в fTemp void convertTemp() { //разбираемся с целой частью - убираем младший бит, но сохраняем знак fTemp = (int)((ourSensor.TEMP & 0x8000) | (ourSensor.TEMP >> 1)); //без конкретного указания типа не работает( //разбираемся с дробной частью - как по даташиту fTemp = fTemp - 0.25 + (float)(ourSensor.countPerC - ourSensor.countRemain) / ourSensor.countPerC; } //функция считывает память датчика, расположенного на выводе PortTempNumber порта bPortTemp, //в структуру ourSensor void getTemp() { byte bCheck; //проверка, считывается ли информация с датчика int i; //счётчик byte CRC = 0; //проверка CRC byte* currentByte; //указатель, куда записываем текущий байт //константы для работы с датчиком const byte SkipROM = 0xCC; //Команда ROM - "Пропуск ROM" const byte ConvertTemp = 0x44; //Функциональная команда - "Конвертировать температуру" const byte ReadMemory = 0xBE; //Функциональная команда - "Чтение памяти" bCheck = OWReset(bPortTemp, bPinTemp); if(bCheck) //если ошибка - не сумели прочитать { errCurrent = ER_Not_found; //отправим по UASRT ошибку return; } OWWrite(bPortTemp, bPinTemp, SkipROM); // Посылаем команду ROM - "Пропуск ROM" OWWrite(bPortTemp, bPinTemp, ConvertTemp); // Посылаем функциональную команду "Конвертировать температуру" VDelay_ms(ConvertDelay); //задержка для конвертации температуры bCheck = OwReset(bPortTemp, bPinTemp); // Перезагружаем OWWrite(bPortTemp, bPinTemp, SkipROM); // Посылаем команду ROM - "Пропуск ROM" OWWrite(bPortTemp, bPinTemp, ReadMemory); // Посылаем функциональную команду "Чтение памяти" currentByte = (char*)&ourSensor; //поставили указатель в начало структуры for(i = 0; i < TempMemoryCount; i++) //считываем память датчика в структуру. Умничка записывает int //правильно - сначала младший байт, потом старший, как и надо { *currentByte = OWRead(bPortTemp, bPinTemp); CRC = getCRC(CRC, *currentByte); //проверка CRC currentByte++; //перевели указатель } if(CRC) //если CRC не сошлось - ошибка! { errCurrent = ER_Bad_crc; return; } } //функция передаёт сообщение об ошибке (errCurrent) через USART, который уже должен быть готов к работе void sendErr() { char sTemp[15]; switch(errCurrent) { case ER_Not_found: //датчик не найден { strcpy(sTemp, "Not found"); break; } case ER_Bad_crc: //crc не сходится { strcpy(sTemp, "Bad crc"); break; } } UART1_Write_Text(sTemp); } void main() { //константы для непосредственной передачи значения с датчика по UART const byte TempLen = 15; //длина строки со значением температуры char sTemp[TempLen]; //строка со значением температуры char sTextBeforeTemp[] = "Temp = "; //текст-объяснение перед непосредственно самим значением температуры //инициализируем ошибку - нет ошибки errCurrent = ER_Not_errs; //инициализация портов ddrUART = ddrUARTInit; //TXD - на вывод, RXD - на вход, остальные - на выход portUART = portUARTInit; //инициализируем передатчик на определённой частоте UART1_Init(UARTFrequency); while(1) { errCurrent = ER_Not_errs; //сначала считаем, что без ошибок getTemp(); //записали в структуру ourSensor память датчика if(errCurrent == ER_Not_errs) //если без ошибок { convertTemp(); //конвертируем температуру UART1_Write_Text(sTextBeforeTemp); sprintf(sTemp, "%.3f", fTemp); //записываем в sTemp температуру с 3 знаками после . UART1_Write_Text(sTemp); } else sendErr(); UART1_Write(0x0A); //переходим на новую строку delay_ms(DelayTime); //небольшая задержка } }
Вот так. Зато работает и при внешнем, и при паразитном питании)
Вот программка на Си, а тут - прошивка.
Автор - Moriam совместно с котом MayDay
Обсудить на форуме