Один день програміста ембеддера. Написати драйвер для датчика вологості HTS221 від STM — це дуже просто?

image
Коли я оцінюю продуманість інтерфейсу користувача спеціалізованих мікросхем від STMicroelectronics, мене часом дивує той факт, що вони взагалі здатні працювати. Але ж працюють. І не просто працюють, а мають купу фішок і вкрай низьку ціну. В результаті доводиться вибирати їх знову і знову...

Черговий день обіцяв бути простим і приємним, наскільки це можливо коли в черговий раз рятуєш «гарячий» проект. За планом до вечора всього то треба було оживити інтегральний суміщений датчик температури і вологості. Крихітні розміри, займані ним на платі, мала кількість ніжок і відсутність дискретних компонентів «обв'язки» дозволяли сподіватися на те, що маєш справу з новітньою розробкою, а сучасні датчики, не дивлячись на маленькі розміри, відрізняються розумом і кмітливістю. Вони без зайвих питань видають на виході готовий результат. Найчастіше вони не просто виконують вимірювання, а виробляють дуже складну обробку сигналів, мають внутрішні буфери для зберігання даних, виходи переривань щоб під час розбудити мікроконтролер і багато інших приємних фішок. Все це сильно полегшує написання коду і скорочує вимоги до ресурсів керуючого мікроконтролера… Спілкуватися з ними легко і приємно. Правда іноді доводиться повозитися з великою кількістю налаштувань. Однак, сьогодні мені це не загрожувало, адже переді мною лише банальний ємнісний вимірювач вологості з функцією вимірювання температури.

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

Єдине, що на мене навивало неясне відчуття тривоги, це виробник: STMicroelectronics. Але відкинувши спогади про танці з бубном навколо їх мікросхем для електролічильника, PLC модему, просунутого інерційного датчика… я взявся за роботу.

Зав'язуємо знайомство
Побіжне ознайомлення з даташитом і схемою показало що на цей раз ні схемотехник, ні трасувальник не «накосячили» (звичайно блокувальний конденсатор поруч з корпусом варто було б поставити і харчування розвести трохи по іншому, але це вплине на точність вимірювань, а не на працездатність). Інтерфейс обміну між мікроконтролером і датчиком з I2C підозріло був схожий на стандартний.



Не буду описувати в подробицях стандартний I2C, розшифрую тільки основні терміни з даташита, які знадобляться трохи пізніше.

Домовимося про терміни. Для взаєморозуміння
ST — старт умова;
SR — операція повторної установки старт умови, вона служить для вирішення колізії читання з регістра. Ми не можемо зробити чисту операцію читання — нам спочатку необхідно передати приладу номер регістра, який читаємо;
SAD — адреса веденого пристрою — нашого датчика (молодший біт у ньому займає ознака операції -"+ W: або 0 для запису, "+R або 1 для читання. Таким чином «SAD+W=0xBE „SAD+R“=0xBF;
SAC — біт підтвердження „посланий“ відомим приладом, в нашому випадку датчиком;
MAK — біт підтвердження від мікроконтролера, який він виставляє у відповідь на факт зчитування даних NMAK його відсутність, яким характеризується останній байт;
SUB — це власне адреса регістра
DATA — власне дані
SP — стоп умова

Підтвердження здійснюється шляхом замикання шини SDA на землю

Важко повірити що витівники з STM в цей раз відмовилися від новацій, але раптом вони змінюють свою політику. Ну що ж, починаємо перевіряти.

Перші кроки. Перші проблеми
Нічого не віщувало складнощів при погляді на цю картинку. Всього-то делов — вважати пару 16 бітних регістрів!



Доробляю свій шаблон обміну з I2C і вже через годину на спробу запису в регістр мікросхема бадьоро відповідає бітами підтвердження. Ну що ж, навчити мікроконтролер читати дані з регістрів трохи складніше. Де тут регістри в яких зберігаються результати вимірювань? Так от вони, але видають суцільні нулі. Що ж, доведеться почитати детальніше про конфігурацію. Пів години за екраном монітора і чашкою чаю показують що за замовчуванням мікросхема знаходиться в неактивному стані, проте вивести її зі ступору можна змінивши лише один байт у регістр конфігурації. Заодно підштовхнемо ще парочку, щоб вимірювання відбувалися циклічно, благо суворою економією енергії в цьому проекті займатися не треба.

CTRL_REG1=0x86;

Ура, з'явилися дані в 16 бітних регістрах температури і вологості, але якось вони дивно виглядають. І в разі температури і вологості обидва байта рівні один одному. Це щонайменше підозріло. Треба розбиратися. Година йде на пошуки помилок в протоколі обміну і перевірку затримок. Нічого не допомагає, доводиться знову заглиблюватись в даташит. Ба..., та я не помилився. Ну не могли розробники з ЅТМ не приготувати подарунок для програміста. Регістрів в датчику мало і для їх адресації з надлишком вистачає одного байта. Коли читаєш або пишеш по одному байту все нормально. Але по специфікації I2C можна як читати, так і писати кілька байт за раз і я активно використовую цю можливість у своїй програмі. Головне, у першому випадку після кожного байта посилати біт підтвердження, а в другому перевіряти повернув його датчик. Але інженери від STM ввели своє ноу-хау.

Виявляється, для того щоб після читання або запису одного байта лічильник збільшується на одиницю і переходив на наступний байт необхідно в байті адреси регістра (SUB) встановити старший біт в 1! Інакше будеш до посиніння працювати взаємодіяти з одним регістром. Геніально, а головне як незбагненно. Навіщо це?
Додаємо милицю до стандартного протоколу.

if (ByteCount>1)
SUB[1]|=0x80;

Без поразок немає перемог
Лічені дані починають радувати своєю різноманітністю, ось тільки які ж папуг вони їх вимірюють? Дуже дивно виглядають лічені значення. Схоже поки не прочитаєш цей даташит від кірки до кірки, каші з цим датчиком не звариш.

Ось це засідка! Виявляється для того, щоб отримати відчутний результат у градусах і відсотках треба ще зайнятися розрахунками. Витягти калібрувальні дані з певних регістрів і скориставшись кусково-лінійною апроксимацією обчислити значення. Не те щоб на Сі складно написати шматок коду, але ретельного вивчення даташита явно уникнути не вийде. Обсяг коду зростає і як результат у мене намалювалася ще одна проблема — закінчується пам'ять мікроконтролера. Схемотехник спочатку заклав камінь всього з шістнадцятьма кілобайтами FLASH, а опитування датчика вологості тільки маленька дещиця завдань, покладених на нього. Оскільки мікроконтроллер 32 розрядний ми маємо всього 4К слів для коду і констант. Навіть з урахуванням того, що я не користуються ве великовагові HALовские дрова цього жахливо мало. Мені обіцяли купити нову версію мікроконтролерів з пам'яттю в два рази більшою. Як там справи? Дзвоню менеджеру проекту. Нові камені вже прийшли, але їх не запаяли в плати. Виявляється чекають, коли я отрапортую що помилок в платі не виявлено. Ну що ж, рапортую і отримую відповідь — до кінця тижня може бути сваяем тобі кам'яна квітка. Звичайно плата не складна, можна було б спаяти саомому, хоча чіпи дуже дрібні і вимагають кропіткої пайки феном. Однак у цьому проекті є спеціально навчені монтажу люди, навіщо залишати їх без роботи, тим більше що мій графік дуже щільний і нічого, крім витраченого часу, я з цього не поимею.

У сформованій ситуації без налагодження вперед не просунутися. Доводиться закоментіровать частина вже написаного коду, щоб звільнити місце для розрахунків.

Завдання лінійної апроксимації не виглядає надто складною,


але знаючи чарівників з STM краще пошукати готове рішення. 10 хвилин серфінгу приводить мене до Application з описом методики розрахунків і навіть шматками коду на Сі.

Навіщо робити те, що можна зробити складно?
Копіюю в свою програму. Ось воно і час обіду. З тугою дивлюся на смужку лісу на обрії, схоже не те що шашлики, але навіть пікнік на природі на сьогодні скасовується. Обід трохи піднімає настрій і з'являються сили для детального вивчення аппликэйшена. Розбираюся в регістрах з калібрувальними значеннями. Спочатку їх організація ставить мене в глухий кут, потім викликає сміх, майже істеричний. Навіть для продуктів STM відчувається перебір. На цьому моменті хочеться детальніше зупинитися.



Дивимось на малюнок і намагаємось оцінити політ думки. У першому стовпці таблиці адресу в HEX форматі, у другій назву, в третій тип даних, далі побітове опис.

Зі значеннями, позначеними як s16 все зрозуміло — 16 розрядні знакові, представлені в стандартному додатковому форматі.

З тими що за адресами 30 і 31 все веселіше. Вони восьмібітних. Але для того щоб використовувати їх в розрахунках необхідно розділити їх значення на два. Молодший біт незначащий. Невже важко було просто значущих 7 біт помістити в регістр, а старший в нулі залишити?

Але це були ще квіточки. Ягідки пішли з 8-бітовими значеннями за адресами 32 і 33. Виявляється, їм не вистачає ще двох бітів — дев'ятого та десятого. Вони чомусь зберігаються в регістрі з адресою 35. Він містить 2старших біта від одного регістра з номером 32 і два від іншого з номером 33. Навіщо? Чи Не простіше під кожен з них виділити по два байти і спокійно записати значення, адже все одно регістр з адресою 35 пустує!

Але і це ще не все. В результаті маніпуляцій ми отримуємо 10 бітові регістри, але виявляється для обчислень нам необхідні їх них тільки старші сім біт. Тому три молодших біта треба відкинути значення потрібно розділити на 8 і тільки потім підставляти у формулу.

Іншими словами, нам потрібно тільки сім біт для розрахунків. Навіщо питається були всі ці маніпуляції, коли вони легко вмістилися б у восьмибітний регістр!

Реально якийсь розрив головного мозку. Інформація в регістрах з адресами 30-31 могла б виглядати наприклад так:



Для вилучення інформації не довелося б робити неймовірних кульбітів, а в пам'яті регістрів ще і зайвий біт залишається.

Подорож на меркурій


Гаразд, перевіряю програму, наведену в апликейшине. Начебто все дуже схоже на опис. Однак радує передостанній рядок:


Якщо відносна вологість більше 1000 відсотків приймаємо її за 1000 відсотків.

Клас.

В результаті не проходить і двох годин, як я нарешті перевіряю результат вимірювань на практиці. Він мене лякає. Датчик показує що погодні умови в моїй кімнаті дуже близькі до западині на дні уявного меркуріанського океану. Температура вище 500 градусів за цельсієм, а вологість в районі 1000 відсотків. Схоже я зовсім запрацювався і пора включати кондиціонер.

Коли з'ясовується що на показання датчика і це впливає слабко, починаю перевіряти код. Я не втримався від доопрацювання грубувато написаного шаблону від ST, раптом накосячілі? Півгодинна перевірка не дає результатів. Знову заглиблююся в даташиты щоб зрозуміти звідки взялися цьому дивні множники „10“ у розрахунках за методом лінійної інтерполяції. Пів години уважною вичитки не прояснює причин їх появи.

Пробую выкинутьих з коду і раптово переміщаюся назад в середню смугу — 29 градусів і 20 відсотків вологості. Притискаю пальцем датчик і дуже швидко показання температури зростають до 34. Ура, нарешті можна завершувати.

Ось тільки за вікном вже смеркає і за тратуарам вервечками тягнуться бідолахи, що повертаються з роботи на московських електричках. Ну що ж, піду хоча б до парку прогуляюся, підготуюся подумки до завтрашнього ранкового розмови з генеральним з Казані. Їх фірма вирішила спробувати щастя в імпортозаміщення на пару з Башнефтью, потрібно подумати що вони від мене сподіваються отримати і чим власне я зможу їм допомогти.

Сухий залишок
В сухому залишку маємо — ST зробила все щоб наш брат розробник навіть для написання простого драйвера прочитав мануал від кірки до кірки та ще й з аппликейшином помучився:

  • При запису-читання декількох байт необхідно змінювати адреса регістра;
  • Реальні значення вимірюваних величин необхідно обчислювати з використанням каліброваних значень;
  • Самі калібрувальні значення розташовані в пам'яті дуже дивним чином і їх необхідно збирати звідти шматками проводячи циркові маніпуляції з бітами;
  • Текст програми з файлу application містить помилки;
  • Калібрувальні та резервні регістри відкриті для вільної запису. Випадковий запис в них нових значень призведе до спотворення результатів під час підрахунку;
  • Відносна вологість легко може зашкалювати за 100 відсотків і виробника це абсолютно не бентежить;
Вищеописані проблеми з якими я зіткнувся в процесі написання найпростішого драйвера відняли у мене більше половини робочого дня, а новачка їх рішення могло б і на кілька днів забезпечити роботою.

Що вже тут говорити про створення драйвера для якого-небудь PLC модему, коли до дивацтв чіпа і неточностей опису додаються ще схемотехнічні помилки, апаратні проблеми, нюанси протоколів обміну, якість ліній зв'язку. Коли мікроконтроллер має на льоту зчитувати і кодувати/декодувати дані і доводиться задіяти на повну силу механізм переривань і прямого доступу до пам'яті. При цьому код повинен виконуватися у „тіньовому режимі“ і надавати мінімальний вплив на перебіг основної програми.

Я вже не кажу про те, що про деяких не надто приємних нюанси виробники часом свідомо замовчують, або надають не надто достовірну інформацію.

В таких умовах без досвіду роботи, гарного знання схемотехніки і фізики роботи пристрою розробнику доводиться дуже туго.

Пора звітувати про результати
Ну і нарешті спробую полегшити життя тим, хто вирішить використовувати даний датчик у своїх розробках. Публікую нижче код відповідних програмних модулів. Відразу обмовлюся, що він не включає низькорівневих функцій обміну даних по I2C, оскільки він сильно залежить від конкретної реалізації — застосовується для опитування мікроконтролера, шини обміну і навіть номерів підключаються портів.

Обмін даними з датчиком здійснюється по інтерфейсу I2C. Використовується дві функції підтримують стандартний протокол, їх доведеться написати самому або пошукати в стандартних бібліотеках.

Одна для запису даних, відповідно до протоколу зображеному на малюнку нижче

int WRsubAddr(unsigned char SAD,unsigned char SUB, unsigned char *data,unsigned int ByteCount);

Друга для читання даних

int RDsubAddr(unsigned char SAD,unsigned char SUB, unsigned char *data,unsigned int ByteCount);

data — буфер даних
ByteCount — кількість лічених або записаних байт
Код програми досить простий і рясно забезпечений коментарями. При ініціалізації драйвера датчика з нього зчитуються константи для обчислень і поміщаються в окрему структуру. У неї ж поміщаються лічені і перетворені у зрозумілій вид результати вимірювань.

Код заголовкого файлу
#ifndef HTS221_H
#define HTS221_H

#include "i2c.h"

//визначення адрес декілька значень вологості і температури
#define adr_H_OUT 0x28
#define adr_T_OUT 0x2A

//визначення адрес регистровов калібрування
#define adr_H0_rH_x2 0x30
#define adr_H1_rH_x2 0x31
#define adr_T0_degC_x8 0x32
#define adr_degC_x8 0x33
#define T1_T0_msb 0x35
#define adr_H0_T0_OUT 0x36
#define adr_H1_T0_OUT 0x3A
#define adr_T0_OUT 0x3C
#define adr_T1_OUT 0x3E

//визначення адрес регистровов УПРАВЛІННЯ
#define adr_WHO_AM_I 0x0F
#define adr_AV_CONF 0x10
#define adr_CTRL_REG1 0x20
#define adr_CTRL_REG2 0x21
#define adr_CTRL_REG3 0x22
#define adr_STATUS_REG 0x27

//адреса мікросхеми HTS221 на шині I2C
#define addr_HTS221 0x5F

typedef struct { //структура, в якій будуть зберігатися дані калібрування для HTS221
//і самі дані

short H_OUT;//зчитане значення вологості
//дані калібрування для вологості
short H0_rH;
short H1_rH;
short H0_T0_OUT;
short H1_T0_OUT;

short T_OUT;//зчитане значення температури
//дані калібрування для температури
short T0_degC;
short T1_degC;
short T0_OUT;
short T1_OUT;

int Humidity;
int Temperature;

} THTS221str;


THTS221str HTS221str;

unsigned char DevCodeRead(void);
unsigned int InitHTS221CalibrTab(void);
int HTS221_Get_Humidity(void);
int HTS221_Get_Temp(void);

#endif

Код програми на СІ
#include "HTS221.h"

int WRHTS221reg(unsigned char addrREG, unsigned char *data,unsigned char ByteCount)
{//запис count байтів інформації в регістр
if (ByteCount>1)
addrREG|=0x80;//датчик вимагає для автоинкремента регістрів під час читання
return WRsubAddr(addr_HTS221,addrREG,data,ByteCount);
}

int RDHTS221regs(unsigned char addrREG, unsigned char *data,unsigned int ByteCount)
{//читання довільної кількості байт пам'яті з початковим адресою addrREG
if (ByteCount>1)
addrREG|=0x80;//датчик вимагає для автоинкремента регістрів під час читання
return RDsubAddr(addr_HTS221,addrREG,data,ByteCount);
}

unsigned char DevCodeRead(void)
{//считывае назва датчика, якщо там правильне значення видає 1 інакше нуль
unsigned char Code[2];

if (!RDHTS221regs(0xF,Code,1))
return 0;
if (Code[0]==0xBC)
return 1;
else
return 0;
}

unsigned int InitHTS221CalibrTab(void)
{
unsigned char buffer[4];
buffer[0]=0x86;//power down, OFF, 1 HZ read
if (!WRHTS221reg(adr_CTRL_REG1,buffer,1))
return 0;
if (!DevCodeRead())
return 0;
// 1. Read H0_rH and H1_rH coefficients
if (!RDHTS221regs(adr_H0_rH_x2,buffer,2))
return 0;
HTS221str.H0_rH = buffer[0]>>1;
HTS221str.H1_rH = buffer[1]>>1;
//2. Read H0_T0_OUT
if (!RDHTS221regs(adr_H0_T0_OUT,(unsigned char*)(&HTS221str.H0_T0_OUT),2))
return 0;
//3. Read H1_T0_OUT
if (!RDHTS221regs(adr_H1_T0_OUT,(unsigned char*)(&HTS221str.H1_T0_OUT),2))
return 0;

// 1. Read from 0x32 & 0x33 registers the value of coefficients T0_d egC_x8 and T1_de gC_x8
// 2. Read from 0x35 register the value of the MSB bits of T1_deg C and T0_deg C
if (!RDHTS221regs(adr_T0_degC_x8,buffer,4))
return 0;
HTS221str.T0_degC = (buffer[0]>>3)+(0x60&(buffer[3]<<5));
HTS221str.T1_degC = (buffer[1]>>3)+(0x60&(buffer[3]<<3));
//3. Read from 0x3C & 0x3D registers the value of T0_OUT
if (!RDHTS221regs(adr_T0_OUT,(unsigned char*)(&HTS221str.T0_OUT),2))
return 0;
//4. Read from 0x3E & 0x3F registers the value of T1_OUT
if (!RDHTS221regs(adr_T1_OUT,(unsigned char*)(&HTS221str.T1_OUT),2))
return 0;

return 1;
}

int HTS221_Get_Humidity(void)
{
unsigned int tmp;
//4. Read H_T_OUT
if (!RDHTS221regs(adr_H_OUT,(unsigned char*)(&HTS221str.H_OUT),2))
return 0;
/*5. Compute the RH [%] value by linea r interpolation */
tmp = ((unsigned int)(HTS221str.H_OUT - HTS221str.H0_T0_OUT)) * ((unsigned int)(HTS221str.H1_rH - HTS221str.H0_rH));
HTS221str.Humidity = tmp/(HTS221str.H1_T0_OUT - HTS221str.H0_T0_OUT) + HTS221str.H0_rH;

/* Saturation condition*/

return 1;
}

int HTS221_Get_Temp(void)
{
unsigned int tmp;
//4. Read H_T_OUT
if (!RDHTS221regs(adr_T_OUT,(unsigned char*)(&HTS221str.T_OUT),2))
return 0;
/*5. Compute the RH [%] value by linea r interpolation */
tmp = ((unsigned int)(HTS221str.T_OUT - HTS221str.T0_OUT)) * ((unsigned int)(HTS221str.T1_degC - HTS221str.T0_degC));
HTS221str.Temperature = tmp/(HTS221str.T1_OUT - HTS221str.T0_OUT)+ HTS221str.T0_degC;
/* Saturation cond ition*/
if(HTS221str.Temperature>1000 )
HTS221str.Temperature = 1000;

return 1;
}

Ініціалізація і зчитування даних
InitHTS221CalibrTab();//ініціалізація
HTS221_Get_Humidity();//визначаємо вологість
HTS221_Get_Temp();//вимірюємо температуру


Корисні матеріали
Слылка на сторінку датчика у виробника

Фінальний опитування
Виробник заявив, що датчик містить аж цифровий сигнальний процесор для обробки результатів. Тоді чому не можна було реалізувати найпростіші обчислення для надання даних у зручній для користувача формі. Ну добре, він дуже зайнятий, але чому принаймні хоча б калібрувальні регістри не розташувати нормальним чином? Чому в найпростішому демонстраційному коді містяться помилки? І ще купа чому.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.