[Из песочницы] Датчик абсолютного давления BMP180

Вступление


Сегодня герой нашего вечернего шоу — датчик абсолютного давления и температуры (последним сегодня уже никого не удивишь, их стали пихать абсолютно во все датчики, так или иначе связанные с 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-битная математика. Хотя там тоже есть варианты оптимизации с потерей точности в угоду скорости и объему кода.

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

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

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

  • 6 августа 2016 в 17:34 (комментарий был изменён)

    0

    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 и получать обновления по человечески (то есть автоматом), а не в зипчиках.

© Habrahabr.ru