Когда я оцениваю продуманность интерфейса пользователя специализированных микросхем от 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 битных регистрах температуры и влажности, но как-то они странно выглядят. И в случае температуры, и влажности оба байта равны друг другу. Это по меньшей мере подозрительно. Надо разбираться. Час уходит на поиски ошибок в протоколе обмена и проверку задержек. Ничего не помогает, приходится снова углубляться в даташит. Ба..., да я не ошибся. Ну не могли разработчики из STМ не приготовить подарка для программиста. Регистров в датчике мало и для их адресации с избытком хватает одного байта. Когда читаешь или пишешь по одному байту всё нормально. Но по спецификации I2C можно как читать, так и писать несколько байт за раз и я активно использую эту возможность в своей программе. Главное, в первом случае после каждого байта посылать бит подтверждения, а во втором проверять вернул ли его датчик. Но инженеры от STM ввели своё ноухау.
Оказывается, для того чтобы после чтения или записи одного байта счётчик увеличивался на единицу и переходил на следующий байт необходимо в байте адреса регистра (SUB) установить старший бит в 1! Иначе будешь до посинения работать взаимодействовать с одним и тем же регистром. Гениально, а главное как непостижимо. Зачем это?
Добавляем костыль к стандартному протоколу.
if (ByteCount>1)
SUB[1]|=0x80;
Без поражений нет побед
Cчитанные данные начинают радовать своим разнообразием, вот только в каких же попугаях они их измеряют? Уж очень странно выглядят считанные значения. Похоже пока не прочитаешь этот даташит от корки до корки, каши с этим датчиком не сваришь.
Вот это засада! Оказывается для того, чтобы получить осязаемый результат в градусах и процентах надо ещё заняться расчётами. Извлечь калибровочные данные из определённых регистров и воспользовавшись кусочно-линейной аппроксимацией вычислить значения. Не то чтобы на Си сложно написать кусок кода, но тщательного изучения даташита явно избежать не получится. Объём кода возрастает и как результат у меня нарисовалась ещё одна проблема — заканчивается память микроконтроллера. Схемотехник изначально заложил камень всего с шестнадцатью килобайтами FLASH, а опрос датчика влажности только маленькая толика задач, на него возложенных. Поскольку микроконтроллер 32 разрядный мы имеем всего всего 4К слов для кода и констант. Даже с учётом того, что я не пользую тяжеловесные HALовские дрова этого чудовищно мало. Мне обещали закупить новую версию микроконтроллеров с памятью в два раза большей. Как там дела? Звоню менеджеру проекта. Новые камни уже пришли, но их не запаяли в платы. Оказывается ждут, когда я отрапортую что в ошибок в плате не обнаружено. Ну что ж, рапортую и получаю ответ — к концу недели может быть сваяем тебе каменный цветок. Конечно плата не сложная, можно было бы спаять саомому, хотя чипы очень мелкие и требуют кропотливой пайки феном. Однако в этом проекте есть специально обученные монтажу люди, зачем их оставлять без работы, тем более что мой график очень плотный и ничего, кроме потраченного времени, я с этого не поимею.
В сложившейся ситуации без отладки вперёд не продвинуться. Приходится закоментировать часть уже написанного кода, чтобы освободить место для расчётов.
Задача линейной апроксимации не выглядит слишком сложной,
но зная кудесников из STM лучше поискать готовое решение. 10 минут серфинга приводит меня к Application с описанием методики расчётов и даже кусками кода на Си.
Зачем делать просто то, что можно сделать сложно?
Копирую в свою программу. Вот оно и время обеда. С тоской гляжу на полоску леса на горизонте, похоже не то что шашлыки, но даже пикник на природе на сегодня отменяется. Обед немного поднимает настроение и появляются силы для детального изучения аппликэйшена. Разбираюсь в регистрах с калибровочными значениями. Сначала их организация ставит меня в тупик, потом вызывает смех, почти истерический. Даже для продуктов STM чувствуется перебор. На этом моменте хочется уже подробнее остановиться.
Смотрим на рисунок и пытаемся оценить полёт мысли. В первом столбце таблицы адрес в HEX формате, во второй название, в третьей тип данных, далее побитовое описание.
Со значениями, помеченными как s16 всё понятно — 16 битные знаковые, представленные в стандартном дополнительном формате.
С теми что по адресам 30 и 31 всё веселее. Они восьмибитные. Но для того чтобы использовать их в расчётах необходимо разделить их значение на два. Младший бит незначащий. Неужели трудно было просто значащих 7 бит поместить в регистр, а старший в нуле оставить?
Но это были ещё цветочки. Ягодки пошли с 8 битными значениями по адресам 32 и 33. Оказывается, им не хватает ещё двух битов — девятого и десятого. Они почему то хранятся в регистре с адресом 35. Он содержит 2старших бита от одного регистра с номером 32 и два от другого с номером 33. Зачем? Не проще ли под каждый из них выделить по два байта и спокойно записать значения, ведь всё равно регистр с адресом 35 пустует!
Но и это ещё не всё. В результате манипуляций мы получаем 10 битные регистры, но оказывается для вычислений нам необходимы их них только старшие семь бит. Поэтому три младших бита надо откинуть значение нужно разделить на 8 и только потом подставлять в формулы.
Другими словами, нам нужно только семь бит для расчётов. Зачем спрашивается были все эти манипуляции, когда они легко уместились бы в восьмибитный регистр!
Реально какой то разрыв головного
Для извлечения информации не пришлось бы делать невообразимых кульбитов, а в памяти регистров ещё и лишний бит остаётся.
Путешествие на меркурий
Ладно, проверяю программу, приведённую в апликейшине. Вроде всё очень похоже на описание. Однако радует предпоследняя строчка:
Если относительная влажность больше 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();//измеряем температуру
Полезные материалы
Слылка на страницу датчика у производителя
Финальный опрос
Производитель заявил что датчик содержит аж цифровой сигнальный процессор для обработки результатов. Тогда почему нельзя было реализовать простейшие вычисления для предоставления данных в удобной для пользователя формы. Ну хорошо, он слишком занят, но почему по крайней мере хотя бы калибровочные регистры не расположить нормальным образом? Почему в простейшем демонстрационном коде содержатся ошибки? И ешё куча почему.
Автор: progchip666