Вступление


Сегодня герой нашего вечернего шоу — датчик абсолютного давления и температуры (последним сегодня уже никого не удивишь, их стали пихать абсолютно во все датчики, так или иначе связанные с embedded системами) Bosch BMP180. Датчик не новый и по его названию в любой момент можно нагуглить просто невероятное количество информации, включая примеры работы на всех возможных языках. Но как бы это не показалось странным, наша цель состоит вовсе не в том, чтобы разобраться, как именно он работает, нет. Мы будем работать над стилем программирования.

Пару слов о стиле программирования


Каждый начинающий программист постоянно совершенствует свой стиль написания кода. Этот процесс неизбежен и если вы его не ощущаете, это значит, что вы уже всему давно научились, или вам надо менять род занятий.

Итак, чего мы хотим? Что такое «красивый код»? Давайте разберемся в понятиях.

Красивый код:
— Оптимален (с точки зрения использования памяти и количества циклов, требуемых на его выполнение)
— Читаем (какой толк от кода, который написан так, что кроме компилятора его никто не понимает?)
— Кросс-платформен (сегодня ARM, завтра STM8, а вчера звонил один мужик, хотел слепить нечто подобное на PIC)

Как выбрать золотую середину? Можно ли убить всех зайцев одним выстрелом? Думаю, что кидаться на амбразуру мы не станем, но работать в этом направление будем точно.

Итак, давайте скорее переходить собственно к коду. Датчик этот имеет два возможных интерфейса — I2C и SPI. Хотелось бы, чтобы код был единым для обоих. Попробуем сделать это. А еще мы хотим, чтобы была возможность подключать не один датчик, а сколько угодно. Ну и для кучи подключаться они должны по своим интерфейсам. Т.е. мы вот прямо очень хотим, чтобы, к примеру, у нас была возможность работать с тремя датчиками — два подключены к разным I2C интерфейсам, а один вообще на SPI. И чтобы код при этом был один. Ну что же, попробуем.

Нам понадобится структура. В ней мы будем хранить всю информацию на каждый конкретный датчик — куда подключен, какой его адрес, какие настройки он будет иметь, список его калибровочных констант и, собственно, результат конвертирования:

typedef struct
{
	/* Data */
	float Temperature;
	long Pressure;
	
	/* Functions */
	char (*WriteReg)(char I2C_Adrs, char Reg, char Value);
	char (*ReadReg) (char I2C_Adrs, char Reg, char * buf, char size);
	void (*delay_func)(unsigned int ms);
	
	/* Settings */
	char I2C_Adrs;				//I2c address. Default value 0xEE
	BMP180_OversamplingEnumTypeDef P_Oversampling;
	
	/* Internal data */
`	short AC1;
	short AC2;
	short AC3;
	unsigned short AC4;
	unsigned short AC5;
	unsigned short AC6;
	short B1;
	short B2;
	short MB;
	short MC;`
	short MD;
	long UT;
	long UP;
}BMP180_StructTypeDef;


Что мы видим? Переменные Temperature и Pressure — собственно и есть результаты нашей работы с датчиком. Тут все говорит само за себя. Поле Functions представляет собой список указателей на функции, которыми мы будем пользоваться для работы с интерфейсом и реализация функции задержки. Поле Settings позволяет нам указать, какой именно адрес на шине I2C будет использовать наш датчик. Для интерфейса SPI (к слову сказать, эти датчики с интерфейсом SPI продаются только по особому заказу, но нам сейчас это не важно) мы будем указывать, какой номер датчика мы будем читать (мы создадим константный массив указателей на порт и пин CS. Поэтому номер элемента этого массива будет вести прямо к нужному нам пину порта для выбора нужного нам устройства на шине).

Параметр P_Oversampling будет представлять собой элемент типа enum, описанный заранее:
typedef enum
{
	BMP180_OV_Single = 0,
	BMP180_OversamplingX2,
	BMP180_OversamplingX4,
	BMP180_OversamplingX8,	
}BMP180_OversamplingEnumTypeDef;


Инитиализация.


Здесь мы будем читать калибровочные константы. Вот код:
char BMP180_Init (BMP180_StructTypeDef * BMP180_Struct)
{
	char buf[22], Result;
	BMP180_SW_Reset(BMP180_Struct);
	if ((Result = BMP180_Struct->ReadReg(BMP180_Struct->I2C_Adrs, AC1_Reg, buf, sizeof(buf))) != 0) return Result;
	BMP180_Struct->AC1 = (buf[0]<<8)  | buf[1];
	BMP180_Struct->AC2 = (buf[2]<<8)  | buf[3];
	BMP180_Struct->AC3 = (buf[4]<<8)  | buf[5];
	BMP180_Struct->AC4 = (buf[6]<<8)  | buf[7];
	BMP180_Struct->AC5 = (buf[8]<<8)  | buf[9];
	BMP180_Struct->AC6 = (buf[10]<<8) | buf[11];
	BMP180_Struct->B1  = (buf[12]<<8) | buf[13];
	BMP180_Struct->B2  = (buf[14]<<8) | buf[15];
	BMP180_Struct->MB  = (buf[16]<<8) | buf[17];
	BMP180_Struct->MC  = (buf[18]<<8) | buf[19];
	BMP180_Struct->MD  = (buf[20]<<8) | buf[21];
	return Result;
}

Что тут нужно отметить? Ну первое, что бросается в глаза — почему нельзя отправить адрес первого параметра в структуре и заполнить данными всю структуру на лету, без использования промежуточного буфера на 22 байта? Да потому, что порядок следования старшего и младшего байт у этого датчика обратный. (кто помнит big indian / little indian). Можно выкрутится командами свапа байтов на уровне ядра, но код не должен быть привязан к платформе. Поэтому так. По старой доброй традиции, любая ошибка имеет значение, отличное от нуля. Если функция вернула ноль — значит ошибки нет. Поэтому проверяю работу интерфейса лишь условием равенства с нулем. Если интерфейс не работает или датчика на шине не обнаружено, нет смысла писать значения в константы.

Читаем сырые данные.


void BMP180_Read_UT_Value (BMP180_StructTypeDef * BMP180_Struct)
{
	char buf[2];
	BMP180_Struct->WriteReg(BMP180_Struct->I2C_Adrs, ctrl_meas, 0x2E);
	BMP180_Struct->delay_func(50);
	BMP180_Struct->ReadReg(BMP180_Struct->I2C_Adrs, out_msb, buf, 2);
	BMP180_Struct->UT = (buf[0] << 8) + buf[1];
}

void BMP180_Read_UP_Value (BMP180_StructTypeDef * BMP180_Struct)
{
	char buf[3];
	BMP180_Struct->WriteReg(BMP180_Struct->I2C_Adrs, ctrl_meas, 0x34 + (BMP180_Struct->P_Oversampling << 6));
	BMP180_Struct->delay_func(100);
	BMP180_Struct->ReadReg(BMP180_Struct->I2C_Adrs, out_msb, buf, 3);
	BMP180_Struct->UP = ((buf[0] << 16) + (buf[1] << 8) + buf[2]) >> (8 - BMP180_Struct->P_Oversampling);
}

Эти функции читают сырые значения давления и температуры, чтобы потом мы могли при помощи умопомрачительной математики добиться результата в понятной человеку форме. Здесь-то нам и пригодилась функция задержки. Значения выбраны с запасом и причина тому легкая перестраховка на случай неточной работы функций задержки. В моем случае все это работает в отдельном потоке и пока функции ждут, с интерфейсом работают другие датчики. Так что я ничего не теряю.

Получаем результат


Чтобы в структуру сохранился результат, достаточно лишь вызвать функцию BMP180_Get_Result с указателем на нашу структуру в качестве параметра. Она сама опросит датчик и посчитает результат:

void BMP180_Get_Result (BMP180_StructTypeDef * BMP180_Struct)
{
	long X1, X2, B5, T;
	long B6, X3, B3;
	unsigned long B4, B7;
	BMP180_Read_UT_Value(BMP180_Struct);
	BMP180_Read_UP_Value(BMP180_Struct);
	
	/*Calculate temperature*/
	X1 = ((BMP180_Struct->UT - BMP180_Struct->AC6) * BMP180_Struct->AC5) >> 15;
	X2 = (BMP180_Struct->MC << 11) / (X1 + BMP180_Struct->MD);
	B5 = X1 + X2;
	T = (B5 + 8) >> 4;
	BMP180_Struct->Temperature = (float)T / 10;
	
	/*Calculate pressure*/
	B6 = B5 - 4000;
	X1 = (BMP180_Struct->B2 * ((B6 * B6) >> 12)) >> 11;
	X2 = (BMP180_Struct->AC2 * B6) >> 11;
	X3 = X1 + X2;
	B3 = (((BMP180_Struct->AC1 * 4 + X3) << BMP180_Struct->P_Oversampling) + 2) >> 2;
	X1 = (BMP180_Struct->AC3 * B6) >> 13;
	X2 = (BMP180_Struct->B1 * ((B6 * B6) >> 12)) >> 16;
	X3 = ((X1 + X2) + 2) >> 2;
	B4 = (BMP180_Struct->AC4 * (unsigned long)(X3 + 32768)) >> 15;
	B7 = ((unsigned long)BMP180_Struct->UP - B3) * (50000 >> BMP180_Struct->P_Oversampling);
	if (B7 < 0x80000000) BMP180_Struct->Pressure = (B7 * 2) / B4;
		else BMP180_Struct->Pressure = (B7 / B4) * 2;
	X1 = (BMP180_Struct->Pressure >> 8) * (BMP180_Struct->Pressure >> 8);
	X1 = (X1 * 3038) >> 16;
	X2 = (-7357 * (BMP180_Struct->Pressure)) >> 16;
	BMP180_Struct->Pressure = BMP180_Struct->Pressure + ((X1 + X2 + 3791) >> 4);
}

Математика, конечно, тут тяжелая. ARM ее щелкает быстро, а вот STM8 может и задуматься. В данном случае алгоритм был слизан с мануала, но слегка оптимизирован. Впрочем, сильно легче он от этого не стал. С другой стороны, вы еще не видели, что такое BMP280. Там используется 64-битная математика. Хотя там тоже есть варианты оптимизации с потерей точности в угоду скорости и объему кода.

Дополнительные возможности

.
Теперь перейдем к плюшкам. Зная давление, мы теоретически можем рассчитать высоту над уровнем моря. Честно говоря, никакого практического применения я пока не придумал, но возможность имеется. Значения получаются линейно-зависимые от реальной высоты, но все же требуют корректировки на атмосферное давление. Так же функция требует применения библиотеки <math.h>:

float Altitude (long Pressure)
{
	const float p0 = 101325;     // Pressure at sea level (Pa)
	return (float)44330 * (1 - pow(((float) Pressure/p0), 0.190295));
}

Эта функция возвращает миллиметры ртутного столба. Довольно точно работает. Обычному человеку это говорит на много больше, чем кПа.
unsigned short Pa_To_Hg (long Pressure_In_Pascals)
{
	return (unsigned long)(Pressure_In_Pascals * 760) / 101325;
}

Ну и остается функция проверки ID чипа. Я ей не пользуюсь т.к. если все настроено правильно, связь и так проверяется на этапе инитиализации:

char BMP180_Check_ID (BMP180_StructTypeDef * BMP180_Struct)
{
	char inbuff;
	BMP180_Struct->ReadReg(BMP180_Struct->I2C_Adrs, 0xD0, &inbuff, 1);
	if (inbuff == 0x55) return 0;
	return 1;
}

Так же добавлю функцию сброса чипа. От греха подальше лучше его сначала сбрасывать, а потом инитиализировать.
void BMP180_SW_Reset (BMP180_StructTypeDef * BMP180_Struct)
{
	BMP180_Struct->delay_func(100);
	BMP180_Struct->WriteReg(BMP180_Struct->I2C_Adrs, soft_reset, 0xB6);
	BMP180_Struct->delay_func(100);
}


Инитиализации


Декларируем структуру для одного датчика (их может быть сколько угодно):

BMP180_StructTypeDef BMP180_Struct;

Далее инитиализируем ее:

	BMP180_Struct.delay_func = vTaskDelay;
	BMP180_Struct.ReadReg = I2C_ReadReg;
	BMP180_Struct.WriteReg = I2C_WriteReg;
	BMP180_Struct.P_Oversampling = BMP180_OversamplingX8;
	BMP180_Struct.I2C_Adrs = 0xEE;
	Error.BMP180 = BMP180_Check_ID(&BMP180_Struct);

Первое, что хочу отметить — функция задержки выполняется средствами операционной системы. В моем случае это FreeRTOS. Вы же можете использовать стандартную библиотечную функцию delay_ms. Параметры будут совпадать.
Далее функции записи в регистр и чтения из регистра. Мы к ним придем позже. Пока надо просто подключить указатели на них.

Оверсэмплинг позволяет нам фильтровать значения средствами самого чипа. Это увеличивает время конвертирования, но в данном случае это значения не имеет. Никто не станет проверять давление 10 раз в секунду.

Адрес датчика на шине I2C будет стандартным из мануала. (я использую 8 бит адреса. Как-то так исторически сложилось)
В некую структуру Error, содержащую элемент char BMP180 мы записываем код ошибки, полученный после инитиализации. Если все хорошо, там будет ноль.

Получение результата тоже весьма примитивно:

BMP180_Get_Result(&BMP180_Struct);

После этого мы имеем значение температуры и давления, сохраненные в структуре BMP180_Struct. Ничего сложного.

Подключаем интерфейс.


Пришло время подключить функции работы с интерфейсом.
Здесь просто взять и скопировать уже не получится. Здесь надо включать голову.

char I2C_WriteReg (char I2C_Adrs, char Reg, char Value)
{
	unsigned char buf[1];
	char Result;
	if (xSemaphoreTake (xI2C_Semaphore, xSEMwTime) != pdTRUE)
		return 0xFF;
	buf[0] = Value;
	I2C_Struct.I2C_Address = I2C_Adrs;
	I2C_Struct.Reg_AddressOrLen = Reg;
	I2C_Struct.pBuffer = buf;
	I2C_Struct.pBufferSize = 1;
	Result = (char)SW_I2C_Write_Reg(&I2C_Struct);
	xSemaphoreGive (xI2C_Semaphore);
	return Result;
}

Сразу видим, что есть некий буфер, длинною в один байт, который мы будем использовать в качестве значения для регистра. По-сути, ваша задача здесь — обеспечить, чтобы значение Value записалось в регистр Reg устройства с адресом I2C_Adrs. Сделано так потому, что я никогда не пишу функцию для записи лишь одного байта. У меня там пишется буфер произвольной длинны. Поэтому так. Какой интерфейс для этого использовать — дело ваше. Но пример использования программной реализации I2C мы обязательно рассмотрим в следующей статье. А сейчас хорошо видно, что я использую мутексы для защиты функции от доступа из разных потоков и пользуюсь какой-то своей реализацией программной функции SW_I2C_Write_Reg. Так оно и есть, мы об этом еще поговорим, это платформозависимая часть, которую я здесь сознательно не хочу рассматривать. Общая идея понятная и так.

Скачать библиотеку BMP180 можно отсюда
Скачать программную реализацию I2C можно отсюда
Поделиться с друзьями
-->

Комментарии (43)


  1. gbg
    06.08.2016 17:34
    +14

    1. Код совершенно нечитаем. Вот почему:

            short AC1;
    	short AC2;
    	short AC3;
    	unsigned short AC4;
    	unsigned short AC5;
    	unsigned short AC6;
    	short B1;
    	short B2;
    	short MB;
    	short MC;
    	short MD;
    	long UT;
    	long UP;
    


    Что делают все эти буквы? В даташит лезть? А на какой странице искать?

    2. Классная простыня из индексов:
            BMP180_Struct->AC1 = (buf[0]<<8)  | buf[1];
    	BMP180_Struct->AC2 = (buf[2]<<8)  | buf[3];
    	BMP180_Struct->AC3 = (buf[4]<<8)  | buf[5];
    	BMP180_Struct->AC4 = (buf[6]<<8)  | buf[7];
    	BMP180_Struct->AC5 = (buf[8]<<8)  | buf[9];
    	BMP180_Struct->AC6 = (buf[10]<<8) | buf[11];
    	BMP180_Struct->B1  = (buf[12]<<8) | buf[13];
    	BMP180_Struct->B2  = (buf[14]<<8) | buf[15];
    	BMP180_Struct->MB  = (buf[16]<<8) | buf[17];
    	BMP180_Struct->MC  = (buf[18]<<8) | buf[19];
    	BMP180_Struct->MD  = (buf[20]<<8) | buf[21];
    


    Я в очередной раз сошлюсь на длинный список реальных багов, части которых бы не было, если бы некоторым программистам циклы выдавали не по талонам.

    Да, можно сказать «это такая оптимизация! Подлый компилятор наставит там условий, и копирование будет делаться на X тактов дольше».

    Ответ простой: А если мне количество тактов безразлично, но я попал в историю одного байта?

    Так вот, цикл решает обе эти проблемы:

    Он прикрывает нас от путаницы в индексах (и потенциальный читатель не будет проверять каждую строчку на предмет «A индексы точно везде единообразны?»)

    Он объясняет компилятору алгоритм. А опции оптимизации заставят компилятор либо раскрутить цикл, либо наоборот, максимально ужать его.

    3. Вместо гитхаба код лежит в архиве на облаке-диске

    И толку от этого? Лежал бы код в положенном месте, его можно было бы:

    -легко просматривать через встроенную смотрелку гитхаба, без необходимости скачивать и распаковывать
    -форкнуть, убрать помои и предложить пулл-реквест (который не примут все равно).
    -подключать к своим проектам через git-subtree и получать обновления по человечески (то есть автоматом), а не в зипчиках.


    1. Virviglaz
      06.08.2016 22:13
      -8

      Моя первая публикация. Гитхабом пользоваться не умею. Библиотека с рабочего проекта. Не знаю, чем вам еще помочь. Дефайны лежат в файле. И так старался как можно компактнее писать и не повторяться за мануалом. Впрочем, есть люди, которым всегда все не нравится. Поэтому оставлю как есть.


      1. kk86
        07.08.2016 00:59
        +10

        > Гитхабом пользоваться не умею.

        Это много говорит о квалификации.

        > Впрочем, есть люди, которым всегда все не нравится. Поэтому оставлю как есть.

        Впрочем, есть люди, которым всегда тяжело проходить pull request/code review и воспринимать критику кода.


        1. andrrrrr
          07.08.2016 08:38
          -3

          > Гитхабом пользоваться не умею.
          Это много говорит о квалификации.


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

          бегло пробежав по статье я мало что там понял, однако я пользуюсь гитхабом(точнее аналогом bitbucket),
          и что? это разве означает что что у меня квалификация выше?

          зато ваш отзыв о том кто не пользуется тем инструментом которым пользуетесь вы, многое говорит о вас как о человеке, и о вашем отношении к людям.

          ps лично мне вот это помогло разобраться с гитхабом. http://goloburdin.blogspot.ru/2013/11/git-bitbucket-20.html


          1. Virviglaz
            07.08.2016 08:38
            -3

            Спасибо, я пока действительно не вижу смысла пользоваться гитхабом. У меня настроен google drive и GIT для контроля версий. Зачем мне гитхаб?


            1. doom369
              07.08.2016 15:03
              +3

              • Очень удобный и приятный интерфейс
              • Некоторые изменения можно джае комитить из браузера
              • Легко вносить правки сторонним разработчикам
              • История коммитов онлайн
              • Возможность комментировать любую строчку в пул реквесте
              • readme.md
              • Публичность и индексируемость
              • Статистика посещений репозитория
              • Тикеты


              продолжать можно долго.


            1. mayorovp
              07.08.2016 15:33

              У меня настроен… GIT для контроля версий. Зачем мне гитхаб?

              Если вы уже используете гит — то неужели для вас написать в командной строке git push git@github.com:virviglaz/mics.git HEAD:refs/heads/{имя} сложнее чем то, что вы обычно делаете для публикации исходников?


              1. Virviglaz
                07.08.2016 15:36
                +2

                Спасибо, будем учиться.


          1. kk86
            07.08.2016 09:10
            +3

            … однако я пользуюсь гитхабом(точнее аналогом bitbucket), и что? это разве означает что что у меня квалификация выше?

            GitHub это de facto стандарт для open source проектов. Не пытаться разобраться с ним, чтобы сделать один простой push вместо того, чтобы делиться проектом через файлообменник или распределённую файловую систему, таки говорит о квалификации разработчика много.

            зато ваш отзыв о том кто не пользуется тем инструментом которым пользуетесь вы, многое говорит о вас как о человеке, и о вашем отношении к людям.

            Да, и я не против. Привык быть строгим ко всем и к себе.

            P.S. А ещё я не против того, чтобы меня считали мудаком, ага.


            1. Virviglaz
              07.08.2016 15:36
              -1

              Не надо быть мудаком!


              1. Dominikanez
                07.08.2016 17:51
                +1

                Быть и казаться — разные вещи.


            1. MacIn
              11.08.2016 18:52

              GitHub это de facto стандарт для open source проектов.

              А если человек не участвует в open-source проектах и 100% его кода — проприетарщина? Вот, вытащил кусочек для примера другим.
              Будет время — изучит, зачем оскорблять-то?


    1. Gryphon88
      08.08.2016 14:27
      -1

      Открыл для себя неплохой способ избавиться от простыней и повысить переносимость с помощью X macro или на шаблонах.

      Хотя препроцессор вроде не Тьюринг-полный, на нем все равно можно сделать очень и очень много. Теоретически на макросах можно изобразить внебрачного сына рефала, хаскеля и T-SQL, чтобы по заданному табличному описанию периферии генерировался код, но вот стоит ли? Сейчас смотрю на язык Order (вырос из проекта Chaos, который в свою очередь вырос из попытки довести до абсурда Boost.Preprocessor): могучий инструмент, да, но имеет ли смысл его использовать и не проще ли написать кодогенератор на питоне или перле?


  1. Genoik
    06.08.2016 19:20
    +1

    По мне так если этот пост рассматривать как пост про стиль кода, то сам стиль кода не очень.
    Взять хотя бы строку

    if ((Result = BMP180_Struct->ReadReg(BMP180_Struct->I2C_Adrs, AC1_Reg, buf, sizeof(buf))) != 0) return Result;

    тут в одной строке и вызов функции и проверка результата и возврат значения.
    Читать довольно тяжело.
    Имена переменных то с большой буквы, то с маленькой.

    Что мешает написать хотя бы так:
    
    Result = BMP180_Struct->ReadReg(BMP180_Struct->I2C_Adrs, AC1_Reg, buf, sizeof(buf));
    if (Result != 0) 
        return Result;
    


    1. Virviglaz
      06.08.2016 22:11
      -6

      Раньше именно так и было. Даже еще элегантнее:

      if (Result) 
          return Result;

      Ну а теперь вот так. Не вредничайте.


      1. MacIn
        11.08.2016 18:55
        -1

        Ну а теперь вот так. Не вредничайте.

        Выносить действие на следующую строку полезно — так можно BP поставить на положительный исход.


  1. Genoik
    06.08.2016 19:26
    +2

    И разницы между библиотеками, выложенными в интернете не увидел, кроме заворачивание всего действия в структуру.

    Можно еще пояснить вот эту строку, не совсем ее понял: «В моем случае все это работает в отдельном потоке и пока функции ждут, с интерфейсом работают другие датчики. Так что я ничего не теряю.»

    То есть если у меня три датчика подключены к SPI_1, каждый отпрашивается в своем независимом потоке? А за счет чего это реализовано?

    Был бы любопытно увидеть конкретную реализацию сего действа.


    1. Virviglaz
      06.08.2016 22:08
      -2

      Лично я не вижу смысла использовать ARM'ы без операционной системы, за исключением очень узкого круга задач, связанных с потоковой обработкой чего-либо. Да и то это больше удел FPGA. В качестве операционной системы я уже давно и успешно использую FreeRTOS. В данном случае код выглядит примерно так:

      if (!Error.BMP180)
      		xTaskCreate(BMP180_DataCollector, NULL, 70, NULL, tskIDLE_PRIORITY + 1, NULL);
      

      Т.е. в том случае, если датчик работает, создается задача, которая этот датчик читает. Т.к. таких задач может быть много, как и устройств на шине, наша задача реализовать защиту доступа. В простейшем случае на помощь приходит mutex. Тогда нам уже не важно, в какой момент задача обращается к интерфейсу. Если интерфейс занят, задача будет ждать его освобождения:
      void BMP180_DataCollector (void * pvArg)
      {
      	BMP180_Init(&BMP180_Struct);
      	
      	while(1)
      	{
      		/* Task delay */
      		vTaskDelay(LogConfig.BMP180_ReadInt * 1000);
      		/* Get data */
      		BMP180_Get_Result(&BMP180_Struct);
      		
      		SensList.intBMP180.Pressure = BMP180_Struct.Pressure;
      		SensList.intBMP180.Temperature = BMP180_Struct.Temperature;
      		SensList.intBMP180.Altitude = Altitude(BMP180_Struct.Pressure);
      		SensList.intBMP180.mmHg = Pa_To_Hg(BMP180_Struct.Pressure);
      
      		/* Update flags */
      		SensList.intBMP180.isDataNotLogged = 1;
      		SensList.intBMP180.isDataUpdated = 1;
      	}
      }
      


  1. Genoik
    06.08.2016 19:29
    +3

    «Зная давление, мы теоретически можем рассчитать высоту над уровнем моря. Честно говоря, никакого практического применения я пока не придумал, но возможность имеется.»
    Почему-то я всегда наивно думал что такие датчики давления как раз и используются для определения высоты.
    Как пример, в коптерах, текущая высота рассчитывается как разница давлений между давлением на точке старта и давлением в текущий момент времени. Ну или как в вашем примере разница высот над уровнем моря.


    1. Virviglaz
      06.08.2016 22:10

      Да, вы правы. С коптерами я не работал, скорее всего именно для них этот датчик будет очень интересен.


      1. zirix
        06.08.2016 22:16

        Есть еще такая штука как вариометр. Скорость подъема или спуска показывает.

        Вот там как раз и используется частый опрос датчика.


        1. Virviglaz
          06.08.2016 22:17

          Спасибо, будем знать!


  1. Genoik
    06.08.2016 19:32
    +6

    «Никто не станет проверять давление 10 раз в секунду.»
    Я бы поспорил с этим утверждением. Бывает и чаще нужно измерять, чтобы узнать вертикальную скорость.


    1. RoboShop
      07.08.2016 08:31

      Погрешность ~17см. Чтобы измерять чаще 10 раз в секунду, и при этом, иметь разницу в высоте при измерениях адекватно большую, чем погрешность (а иначе смысла такое измерение не имеет), нужно лететь на истребителе. Причем вертикально вниз лететь.


      1. Virviglaz
        07.08.2016 08:34
        -1

        А можно попробовать применить фильтр Калмана. Могу даже поделиться кодом (для float):

        typedef struct
        {
          float Result;
          float Value;
          float Previous;
          float K;
        }KalmanFloatStructTypeDef;

        void KalmanFloatCalc (KalmanFloatStructTypeDef * KalmanFloatStruct)
        {
          KalmanFloatStruct->Result = KalmanFloatStruct->K * KalmanFloatStruct->Value;
          KalmanFloatStruct->Value = 1.0 - KalmanFloatStruct->K;
          KalmanFloatStruct->Previous *= KalmanFloatStruct->Value;
          KalmanFloatStruct->Result += KalmanFloatStruct->Previous;
          KalmanFloatStruct->Previous = KalmanFloatStruct->Result;
        }

        Параметр К мы выбираем в интервале 0.1 — 0.5. Для 0.1 к результату мы придем через 10 итераций.
        Эта штука реально помогает замыливать перемещения и снижает шум. Лучше использовать Калмана, чем оверсамплинг.


        1. ProLimit
          07.08.2016 10:40
          +2

          Калман который фильтрует только один датчик??? Тогда в нем никакого смысла нет, используйте НЧ-фильтр. А Калман нужен для сведения разных датчиков которые выдают физически зависимые показания.


          1. Virviglaz
            07.08.2016 10:47
            -1

            Вы не правы. Фильтр Калмана можно изучить по этой статье. Я с нее код и писал.


            1. ProLimit
              07.08.2016 11:04
              +1

              Не верьте всему, что пишут в интернете. И уж точно фильтр Калмана нельзя назвать простым.


            1. Genoik
              08.08.2016 13:27
              +1

              Насколько я знаю, у Калмана немного другая задача, он не фильтрует показания какого-то одного датчика, он берет показания с нескольких датчиков и наиболее точно пытается предсказать текущее положение вещей. То есть если бы вы высоту измеряли бы своим барометром, плюс с помощью ультразвукового датчика плюс к ним, допустим, измерение по камере, получили бы 3 разные значения текущей высоты, отдали бы все эти 3 значения Калману, на выходе бы получили высоту, наиболее близкой к реальной. И то, только в том случае, если сам фильтр правильно настроен.
              То, о чем говорите вы, это по сути получается обычный НЧ-фильтр, как уже написали выше.


        1. Akon32
          07.08.2016 12:00
          +3

          Для его нужно Previous, если оно на выходе всегда равно Result?
          Почему функция модифицирует свои входные значения (Value)? Обычно это признак плохой архитектуры.

          Следующий код считает по сути то же, что и ваш, но выглядит понятнее. И компилируется чаще всего во что-то более короткое.

          void KalmanFloatCalc2 (KalmanFloatStructTypeDef * s)
          {
            s->Result = (1.0f - s->K) * s->Result + s->K * s->Value;
          }
          

          Для чего эта возня с присваиваниями? Она действительно оправдана?


          1. Virviglaz
            07.08.2016 12:01
            -3

            Может быть, все может быть. Я не математик, увы. Может быть вы и правы. Проверьте ваш код в железе. Если заработает, давайте вместе им пользоваться.


            1. mayorovp
              07.08.2016 14:01
              +2

              А при чем тут вообще математика? Ладно бы вам какое-то сложное упрощение формулы предложили, так ведь просто переменные переименовали и от лишних избавились. Обычный рефакторинг.


          1. Genoik
            08.08.2016 13:32

            Комменатрий удален


      1. Genoik
        08.08.2016 13:22

        Здесь я с вами не соглашусь, фильтрацию данных пока еще никто не отменял. Точно также как и интегрирование.
        Откройте исходный код любого автопилота коптера и посмотрите как там сделан основной цикл и его частоту. Также можете посмотреть как устроен ПИД-регулятор для удержания высоты и на какие данные он опирается. Там присутствуют обычно как показания акселлерометра, так и показания барометра, которые смешиваются между собой в определенной пропорции (самый простой случай), а уж дальше эти данные идут на регулятор.


  1. GarryC
    07.08.2016 15:06
    +4

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

    Если Вам потребовалось в тексте за фрагментом пояснять смысл полей, то это признак плохого (недостаточно откомментированого) кода.

    Если Вы применяете повсюду тип char, то это признак плохого кода, поскольку поля очевидно на символьные, хорошей альтернативой было бы u8.

    Если у Вас в структуре появляются поля с именем ххх1… ххх6, то это признак плохого стиля, поскольку скорее всего они будут использоваться как элементы массива (так оно дальше по коду и происходит).

    Если Вы пишете «Поле Settings позволяет нам указать», а в структуре нет такого поля, то это плохой стиль программирования (недостоверные комментарии).

    Если Вы пишете о значении адреса I2C_Adrs по умолчанию, то хотелось бы понять, как это обеспечивается — в дальнейшем выясняется, что никак не обеспечивается, и это признак плохого стиля.

    Если Вы собираетесь использовать обобщенные функции ввода/вывода, но в их сигнатуру прописываете адрес на I2C шине, то это плохой стиль программирования.

    Лично мне такого начала достаточно, чтобы предположить определенные недоработки в Вашем стиле программирования и предложить Вам сначала поработать над его совершенствованием, и лишь потом приступать к написанию столь обязывающих постов.

    Как пост на тему о взаимодействии с конкретным с устройством еще сойдет, хотя и не без огрехов, но как рекомендации по стилю… здесь точно не по адресу.

    Без обид, просто я этой темой плотно занимаюсь.


  1. sunman
    08.08.2016 11:16
    +2

    big indian / little indian — это вы хорошо написали — большой индусский/малый индусский — видимо код.
    Порядок байтов на английском звучит как big endian/little endian.


    1. d1f
      08.08.2016 12:04

      > Порядок байтов на английском звучит как big endian/little endian.

      big/little endian — это тупоконечники/остроконечники из «Путешествия Гулливера» Свифта.

      Применяется в IT для обозначения соответствующих отношений между сторонниками
      most/less significant bit first.


      1. Virviglaz
        08.08.2016 12:12

        Мне об этом уже раз 50 сообщили, спасибо. Пусть остается, как свидетельство моей неграмотности. Кстати, перед публикацией гуглил. Видимо, не я один плохо помню английский.


      1. mayorovp
        08.08.2016 12:25

        Что-то я ни разу не видел холиваров "тупоконечников" и "остроконечников". "big/little endian" давно уже стали просто терминами, обозначающими порядок байт.


        1. d1f
          08.08.2016 13:28

          > Что-то я ни разу не видел холиваров «тупоконечников» и «остроконечников».

          Тогда интернет не был столь распространён.


          1. mayorovp
            08.08.2016 13:38

            Вот именно, "тогда". Сейчас это уже термины.


  1. armature_current
    08.08.2016 11:55
    -1

    Для начала, неплохо было бы читателя «инитиализировать» в плане общей постановки задачи и выбора методов решения. Что, зачем и в строгой логической последовательности из 1 в 2, а затем в 3. А теперь на примерах:

    … порядок следования старшего и младшего байт у этого датчика обратный

    У меня с ходу два предложения для Вас (если весь этот эпос о стиле программирования не просто фарс):
    1. DMA или если его на АРМ-подобном МК нет, то
    2. Цикл через луковичку.

    Красивый код:
    — Оптимален (с точки зрения использования памяти и количества циклов, требуемых на его выполнение)

    Выпилить из std_periph_lib не особо нужные действия не означает сделать код оптимальнее. Рационализаторство, вот что Вы сделали.

    Математика, конечно, тут тяжелая

    Позвольте возразить, математика тут представляется на уровне 4 класса. Одни алгебраические операции. А вот код, через который они реализованы, надо еще прожевать… Попробуйте вставить предшествующую формулу и всем все сразу же станет понятно.

    Где тут многопоточность? И где тут оверсемплинг?

    ЗЫ Присоединяюсь ко всем вышеуказанным замечаниям.


  1. j8kin
    08.08.2016 11:55
    +2

    В копилку:
    Не понятно зачем писать на С? Для того же BME 280 есть на github библиотека.
    Чем не не нравится С, нормально отделить интерфейс от реализации нельзя.
    Например, нет смысла давать пользователю функции чтения calibration data и oversampling.
    Вообще с точки зрения пользователя должна быть 1 функция, которая возвратит значение температуры и давления в вашем случае, все остальное юзеру не нужно.
    В парадигме ООП, калибровочную информацию надо читать в конструкторе класса, там же писать в датчик oversampling.
    Что качается интерфейса, меня в бошевской библиотеке раздражал вызов функции по указателю, у вас тоже.
    И еще надежда на 3 датчика одновременно, мне кажется слишком оптимистично, например в моем BME 280 жестко прошит 0x76, и поменять его никак нельзя (в самом датчике можно, но этот контакт не разведен у меня на плате када впаян сенсор.
    Ну и наконец, я вытыкаю его в RPI, в котором либо SPI либо I2C, итого 1 датчик получается

    Ну и по коду, пишите коментарии в Doxygen, это дефакто стандарт. Таже бошевская библиотека везде имеет комментарии в doxygen формате.

    P.S> В связи с этим я велосипедирую BME 280 на C++.