четверг, 15 июля 2021 г.

DS18B20 + STM32, комментарии к коду

Тут будет уныло. 
Во-первых, определимся с электрической схемой подключения. Шина данных адаптирована под схему с открытым коллектором, подтянутой к +питания пятью килоомами, хотя в реальности разницы между 5кОм и 20кОм встроенной я не заметил. И схема с ОК и подтяжка поддерживается контроллером, потому подключение датчика не требует внешних элементов.
Теперь о структуре ПО. Структура ПО для 300 строчного файла, звучит как отчет в универе, самое обидное что писать такое дольше чем сам код.
  1. низкоуровневые функции взаимодействия с ресурсами МК (используется порт ввода-вывода и самый простой таймер-счетчик). Работа с регистрами микроконтроллера.
  2. функции передачи и считывания битов и байт.
  3. подфункции протокола (конвертор температуры и проверка контрольной суммы)
  4. команды протокола DS18B20 
  5. ну почти пользовательский интерфейс, те две функции, которые действительно нужны

1. Периферия микроконтроллера.

dsPortConfig() Инициализация порта. Итак, комментарии построчно через запятую: режим работы порта - выход, тип выход с открытым коллектором, тактирование с наибольшей частотой (меньшие частоты нужны для экономии батарейки), подтяжка к питанию, порт выставить в единицу (транзистор закрыт).

GPIOA_MODER |= GPIO_MODE_OUTPUT << (DS_PIN*2); GPIOA_OTYPER |= GPIO_OTYPE_OD << (DS_PIN); GPIOA_OSPEEDR |= GPIO_OSPEED_100MHZ << (DS_PIN*2); GPIOA_PUPDR |= GPIO_PUPD_PULLUP << (DS_PIN*2); GPIOA_BSRR |= 1 << DS_PIN;

dsTimerConfig() Инициализация таймера счетчика. В случае с программной реализацией нужно как то отслеживать тайминги. Для точности можно запустить таймер счетчик. Нужно будет отсчитывать не больше ста микросекунд. Поэтому предделитель таймера счетчика выставим так, что бы счетчик инкрементировался каждую микросекунду. Тогда функция запроса прошедшего времени будет просто оберткой регистра счетчика. Ну вот и инициализация: источник тактов - системный генератор, таймер включен, предделитель на отсчет каждую микросекунду (48мГц / (47+1) = 1мГц), считаем до бесконечности (ну почти).

TIM16_CR1 = (uint32_t) TIM_CR1_CKD_CK_INT; TIM16_CR1 |= (uint32_t) TIM_CR1_CEN; TIM16_PSC = (uint32_t) 47; TIM16_ARR = (uint32_t) 65535;

dsTimerStart() Обнулить таймер (что бы потом запрашивать прошедшее время с момента обнуления). Обнуляется переменная таймаута (на случай если включите задержку, а счетчик не работает). И запускается команда обновления таймера (обнуление и предделителя таймера и самого таймера).

timeoutGl = 1e6; TIM16_EGR |= TIM_EGR_UG;

dsElapsedTime() Прошедшее время. Сперва таймаут, рассчитаный на то, что эта функция будет опрашиваться в бесконечном цикле, возвращает максимум если количество вызовов функции превышено (что бы от не работающего таймера весь контроллер не повис целиком). Ну и как обещал, возвращает регистр, т.е. это оберточная функция регистра таймера счетчика.

if(--timeoutGl < 5) { return 65000; } return TIM16_CNT;

dsDelayUs(uint16_t us) Микросекундная задержка, тут все как объяснял раньше.

TIM16_EGR |= TIM_EGR_UG; timeoutGl = 1e6; while((TIM16_CNT < us) && (--timeoutGl > 1));

uint8_t dsReadPin() Чтение состояния порта ввода вывода (раз уж порт исользуется один, то и функция всего одна и без параметра).

if((GPIOA_IDR & (1 << DS_PIN)) > 0) { return 1; } return 0;

dsSetPin() Установить единицу (выключить транзистор в схеме с ОК)

GPIOA_BSRR |= 1 << DS_PIN;

dsResetPin() Установить ноль (включить транзистор в схеме с ОК)

GPIOA_BRR |= 1 << DS_PIN;

2. Функции чтения и записи

uint8_t dsTxBit(uint8_t bit) Передача одного бита. Здесь реализованы все те тайминги, что вы видели в предыдущем посте. Функция одновременно и считывает и записывает бит (т.к. это похожие операции), причем считывает только если пытаться записать единицу. Стало быть запись нуля это просто щелканье лапкой с нужными задержками, обрабатывается в отдельном исключении.

uint8_t ret = 0; dsResetPin(); if(bit == 0) { dsDelayUs(WRITE0T); dsSetPin(); dsDelayUs(PULSE_DELAY); return 0; }

Чтение операция посложнее, сперва выжидаем, пока отреагирует датчик, а после ждем, когда датчик сбросит порт. Если рано, то считали единицу, если позже, то ноль. В любом случае ждем необходимое для чтения/записи одного бита время. А вот ошибку, связанную с отстутвием датчика не обрабатываем, узнаем как нибудь потом когда CRC не совпадет.

dsDelayUs(WRITE1T1); dsSetPin(); dsTimerStart(); dsDelayUs(PULSE_DELAY); while( (dsElapsedTime() < TIMESLOT) && (dsReadPin() == 0) ); uint16_t time = dsElapsedTime(); if(time > READT) { ret = 0; } else { ret = 1; } while(dsElapsedTime() < WRITE1T2); return ret;

uint8_t dsTxByte(uint8_t byte) Чтение и запись байта состоит из последовательного чтения и записи восьми бит.

uint8_t ret = 0; for(int i=0 ; i<8 ; ++i) { if( dsTxBit(byte & (1<<i)) == 1 ) { ret |= 1<<i; } // I don't know why dsDelayUs(20); } return ret;

int dsInitSequence() И чуть не забыл про инициализационную последовательность. Это особая последовательность с которой начинается любой запрос датчика. Ни смотря на то, что необходимо только выдать определенные тайминги на шине.

dsResetPin(); dsDelayUs(RESET_PULSE); dsSetPin(); dsDelayUs(RESET_WAIT);

Моя функция имеет обратную связь и возвращает -1 если датчик отреагировал слишком рано или поздно.

dsTimerStart(); while((dsReadPin() == 0) && (dsElapsedTime() < INIT_PULSE_MAX)); uint16_t time = dsElapsedTime(); if( (time > INIT_PULSE_MAX) || (time < INIT_PULSE_MIN) ) { return -1; } while( dsElapsedTime() < RESET_PULSE ); return 0;

3. Функции DS18B20

uint8_t dsCrc(uint8_t *data, uint8_t size) Контрольная сумма проверяется вот таким вот полиномом CRC = X 8 + X 5 + X 4 + 1. Не поверишь, он вычислен за нас, нужно только скопировать таблицу с сайта.

const uint8_t table[256] = {0, 94, 188, 226, 97, 63, 221, 131, 194, 156, 126, 32, 163, 253, 31, 65, 157, 195, 33, 127, 252, 162, 64, 30, 95, 1, 227, 189, 62, 96, 130, 220, 35, 125, 159, 193, 66, 28, 254, 160, 225, 191, 93, 3, 128, 222, 60, 98, 190, 224, 2, 92, 223, 129, 99, 61, 124, 34, 192, 158, 29, 67, 161, 255, 70, 24, 250, 164, 39, 121, 155, 197, 132, 218, 56, 102, 229, 187, 89, 7, 219, 133, 103, 57, 186, 228, 6, 88, 25, 71, 165, 251, 120, 38, 196, 154, 101, 59, 217, 135, 4, 90, 184, 230, 167, 249, 27, 69, 198, 152, 122, 36, 248, 166, 68, 26, 153, 199, 37, 123, 58, 100, 134, 216, 91, 5, 231, 185, 140, 210, 48, 110, 237, 179, 81, 15, 78, 16, 242, 172, 47, 113, 147, 205, 17, 79, 173, 243, 112, 46, 204, 146, 211, 141, 111, 49, 178, 236, 14, 80, 175, 241, 19, 77, 206, 144, 114, 44, 109, 51, 209, 143, 12, 82, 176, 238, 50, 108, 142, 208, 83, 13, 239, 177, 240, 174, 76, 18, 145, 207, 45, 115, 202, 148, 118, 40, 171, 245, 23, 73, 8, 86, 180, 234, 105, 55, 213, 139, 87, 9, 235, 181, 54, 104, 138, 212, 149, 203, 41, 119, 244, 170, 72, 22, 233, 183, 85, 11, 136, 214, 52, 106, 43, 117, 151, 201, 74, 20, 246, 168, 116, 42, 200, 150, 21, 75, 169, 247, 182, 232, 10, 84, 215, 137, 107, 53}; uint8_t crc = 0; for(int i=0 ; i<size ; ++i) { crc = table[crc ^ data[i]]; } return crc;

int32_t dsTransTemp(uint8_t templ, uint8_t temph, uint8_t conf) Функция перевода температуры в градусы. В нее передаются регистры датчика, а она возвращает температуру в градусах цельсия, умноженную на 1000 (целочисленная имитация чисел с плавающей точкой). В коде сперва "склеиваем" два байта (старший и младший) и выясняем, отрицательная ли температура. И ориентируясь на регистр настроек датчика, умножаем полученный результат на нужный коэффициент в зависимости от выбранной разрядности. 

uint32_t tempRaw = ((uint32_t)templ + ((uint32_t)temph << 8)) & 0x07ff; int32_t sign = 1; if( (temph & 0x80) > 0 ) { sign = -1; } switch(conf) { case RESOL_9BIT : return (int32_t)((tempRaw >> 3) * 500) * sign; break; case RESOL_10BIT : return (int32_t)((tempRaw >> 2) * 250) * sign; break; case RESOL_11BIT : return (int32_t)((tempRaw >> 1) * 125) * sign; break; case RESOL_12BIT : return (int32_t)((tempRaw * 625)/10) * sign; break; } return -1;

4. Команды DS18B20

Если коротко, то это проктол датчика.
int dsReadRomCmd() Команда Read rom, довольно бесполезная команда с которой начинается работа с датчиком. Описание операций по порядку: инициализационная последовательность с проверкой, отправка байта команды, чтение и проверка кода устройства (FAMILY CODE), чтение серийного номера датчика и проверка контрольной суммы. Если все три операции прошли успешно, команда вернет 0. Family code это код любого из датчиков DS18B20, серийник используется как адрес в случае, если датчиков на шине несколько. У меня библиотека для одного датчика, потому не нужен. 

uint8_t buffer[7]; int ret; if( dsInitSequence() < 0 ) { return -1; } dsTxByte(READ_ROM); buffer[0] = dsTxByte(0xff); if( buffer[0] != FAMILY_CODE ) { return -1; } // ds ROM CODE not needed for(int i=0 ; i<6 ; ++i) { buffer[i+1] = dsTxByte(0xff); } uint8_t crc = dsTxByte(0xff); if( crc != dsCrc(buffer,7) ) { return -1; } return 0;

int32_t dsReadScratchpad() А вот эта команда уже интереснее, начинается она не с инициализационной последовательности, а с команды read rom. Называется она read scratchpad, чтение eeprom датчика, который хранит и температуру, измеренную ранее, и настройки и несколько бесполезных параметров, которые здесь не используются. Как видно из кода, функция возвращает либо код ошибки, если что то пошло не так. Либо температуру как результат преобразования только что  полученных данных функцией, описанной в пункте 3.

dsTimerUpdate(); if( dsReadRomCmd() < 0 ) { return -1; } dsTxByte(READ_SCRATCHPAD); uint8_t scratchpad[9]; uint8_t crc; for(int i=0 ; i<9 ; ++i) { scratchpad[i] = dsTxByte(0xff); } if( scratchpad[8] != dsCrc(scratchpad, 8) ) { return -1; } return dsTransTemp(scratchpad[0], scratchpad[1], scratchpad[4]);

dsWriteScratchpad(uint16_t Tpar, uint8_t configByte) Запись настроек в датчик, записывает тот самый не нужный параметр (Tpar, на самом деле это порог для компаратора) и регистр настроек, который вычисляется макросами.

if( dsReadRomCmd() < 0 ) { return -1; } dsTxByte(WRITE_SCRATCHPAD); dsTxByte((uint8_t)((Tpar >> 8) & 0x00ff)); dsTxByte((uint8_t)(Tpar & 0x00ff)); dsTxByte(configByte); if( dsReadRomCmd() < 0 ) { return -1; }

В этой же функции следом идет команда сохранения только что переданных настроек copy scratchpad. Она постоянно опрашивает порт до тех пор, пока датчик не вернет ноль. Это нужно для того, чтобы подождать пока датчик сохранит в eeprom полученные настройки.

dsTxByte(COPY_SCRATCHPAD); uint32_t timeout = 1e6; while( (dsTxBit(1) == 0) && (--timeout > 0) ); if(timeout < 2) { return -1; }

5. Пользовательские функции -

Для работы с этой библиотекой сперва  вызовите функцию инициализации dsInit(), а после можно запрашивать температуру либо последовательностью команд dsStart() и dsReadScratchpad(), вызванных через промежуток времени. Либо одной функцией tempBlocking().
dsInit() Инициализация всего, тут порт, таймер, применение настроек, запрос на измерение. 

dsPortConfig(); dsTimerConfig(); rough_delay_us(100); dsWriteScratchpad(0x7ff, DEFAULT_RESOL); dsStart();

int32_t tempBlocking() Если нужно узнать температуру, вызови эту функцию. Сперва запрашивает температуру, потом ждет пока она измерится , потом считывает. Недостаток во времени выполнения (немного притормозит МК).

dsTimerUpdate(); uint32_t timeout = 1e6; if( dsStart() < 0 ) { return -1; } while( (dsTxBit(1) == 0) && (--timeout > 0) ); if(timeout > 0) { return dsReadScratchpad(); } return -1;

int dsStart() Отдельный запрос на измерение температуры, функция команды convert t.

dsTimerUpdate(); if( dsReadRomCmd() < 0 ) { return -1; } dsTxByte(CONVERT_T); return 0;


И наконец, те самые настройки, которые вам понадобятся. Можете указать нужный вам порт и пин, а также выбрать точность (9-12 бит). Вот и все, теперь вы знаете как использовать библиотеку в вашем проекте, основанном на libopencm3 и stm32f0xx микроконтроллере.

/* settings */ // mcu related #define DS_PORT GPIOA #define DS_PIN 2 // set the resolution #define DEFAULT_RESOL RESOL_9BIT

Еще раз оставлю ссылки ds18b20.c и ds18b20.h.

Комментариев нет :

Отправить комментарий