Introduction
Today, the hero of our evening show is a sensor of absolute pressure and temperature (the latter is no surprise to anyone today, they were shoved into absolutely all sensors related to embedded systems anyway) Bosch BMP180. The sensor is not new and by its name at any time you can google just an incredible amount of information, including examples of work in all possible languages. But no matter how strange it may seem, our goal is not at all to figure out exactly how it works, no. We will work on a programming style.
A few words about the programming style
Every novice programmer is constantly improving their style of writing code. This process is inevitable and if you do not feel it, it means that you have already learned everything long ago, or you need to change your occupation.
So what do we want? What is “beautiful code”? Let's understand the concepts.
Beautiful code:
- Optimal (in terms of memory usage and the number of cycles required for its execution)
- We read (what is the use of the code that is written in such a way that nobody understands it except the compiler?)
- Cross-platform (today ARM, tomorrow STM8, and yesterday one man called, wanted to make something like that on the PIC)
')
How to choose a middle ground? Can you kill all the hares with one shot? I think that we will not rush into the embrasure, but we will work in this direction for sure.
So let's move on to the actual code. This sensor has two possible interfaces - I2C and SPI. I would like the code to be the same for both. Let's try to do it. And we want to be able to connect not one sensor, but as many as you like. Well, for the heap, they must be connected by their interfaces. Those. we really really want to, for example, have the opportunity to work with three sensors - two are connected to different I2C interfaces, and one is generally on SPI. And so that the code was one. Well, let's try.
We will need a structure. In it, we will store all the information for each specific sensor - where it is connected, what its address, what settings it will have, its calibration constants list and, in fact, the result of converting:
typedef struct { float Temperature; long Pressure; 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); char I2C_Adrs;
What do we see? The
temperature and
pressure variables are actually the results of our work with the sensor. Everything speaks for itself. The field
Functions is a list of pointers to functions that we will use to work with the interface and the implementation of the delay function. The
Settings field allows us to specify which address on the I2C bus our sensor will use. For the SPI interface (by the way, these sensors with the SPI interface are sold only by special order, but this is not important for us now) we will indicate what sensor number we will read (we will create a constant array of pointers to the port and pin CS. So the number the element of this array will lead directly to the port we need to select the device we need on the bus).
The
P_Oversampling parameter will be an element of the
enum type, described in advance:
typedef enum { BMP180_OV_Single = 0, BMP180_OversamplingX2, BMP180_OversamplingX4, BMP180_OversamplingX8, }BMP180_OversamplingEnumTypeDef;
Initialization.
Here we will read the calibration constants. Here is the code:
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; }
What is there to note? Well, the first thing that catches your eye is why it is impossible to send the address of the first parameter in the structure and fill the entire structure with data on the fly, without using an intermediate buffer of 22 bytes? Yes, because the order of the high and low bytes of this sensor is reversed. (who remembers big indian / little indian). You can get out the commands byte swap at the kernel level, but the code should not be tied to the platform. Therefore, so. According to the good old tradition, any mistake has a nonzero value. If the function returns zero, then there is no error. Therefore, I check the operation of the interface only with the equality condition with zero. If the interface does not work or there is no sensor on the bus, it makes no sense to write values in constants.
We read the raw data.
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); }
These functions read the raw values of pressure and temperature, so that later we can achieve results in human-readable form with the help of mind-blowing mathematics. This is where the delay function came in handy. The values are chosen with a margin and the reason for this is a slight reassurance in case of inaccurate operation of the delay functions. In my case, it all works in a separate thread and while functions are waiting, other sensors work with the interface. So I do not lose anything.
We get the result
To preserve the result in the structure, it is enough just to call the function
BMP180_Get_Result with a pointer to our structure as a parameter. She herself polls the sensor and calculates the 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); 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; 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); }
Mathematics, of course, is hard. ARM clicks it quickly, but STM8 might think. In this case, the algorithm was lapped from the manual, but slightly optimized. However, he didn’t become much easier. On the other hand, you have not yet seen what a BMP280 is. It uses 64-bit mathematics. Although there are also optimization options with a loss of accuracy for the sake of speed and volume of code.
Additional features
.
We now turn to the buns. Knowing the pressure, we can theoretically calculate the height above sea level. To be honest, I haven’t yet come up with any practical application, but there is a possibility. Values are obtained linearly dependent on the actual height, but still require adjustment for atmospheric pressure. The same function requires the use of the library
<math.h> :
float Altitude (long Pressure) { const float p0 = 101325;
This function returns millimeters of mercury. Pretty accurate work. To an ordinary person, this speaks much more than kPa.
unsigned short Pa_To_Hg (long Pressure_In_Pascals) { return (unsigned long)(Pressure_In_Pascals * 760) / 101325; }
Well, it remains the function of checking the ID of the chip. I don't use it because if everything is set up correctly, the connection is checked at the initialization stage:
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; }
Just add the chip reset function. From sin away it is better to first dump it, and then intitialize.
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); }
Initiations
We declare the structure for one sensor (there can be as many as possible):
BMP180_StructTypeDef BMP180_Struct;
Next, we initialize it:
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);
The first thing I want to note is that the delay function is performed by means of the operating system. In my case, this is FreeRTOS. You can use the standard library function
delay_ms . The parameters will be the same.
Further functions of writing to the register and reading from the register. We will come to them later. For now, you just need to connect pointers to them.
Oversempling allows us to filter the values by means of the chip itself. This increases the conversion time, but in this case it does not matter. No one will check the pressure 10 times per second.
The address of the sensor on the I2C bus will be standard from the manual. (I use 8 bits of the address. Somehow it happened so historically)
In a certain structure
Error , containing the element
char BMP180, we write the error code obtained after initialization. If all is well, there will be zero.
Getting the result is also very primitive:
BMP180_Get_Result(&BMP180_Struct);
After that, we have the temperature and pressure values stored in the
BMP180_Struct structure. Nothing complicated.
We connect the interface.
It's time to connect the functions of working with the interface.
Here, just take and copy is no longer possible. Here it is necessary to include the head.
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; }
We immediately see that there is a buffer, one byte long, which we will use as a value for the register. In essence, your task here is to ensure that the
Value value is registered in the
Reg register of the device with the address
I2C_Adrs . This is done because I never write a function to write only one byte. I have a buffer of arbitrary length written there. Therefore, so. What interface to use is your business. But we will definitely consider the example of using the I2C software implementation in the next article. And now it's clear that I use mutexes to protect the function from access from different threads and use some of my own implementation of the
SW_I2C_Write_Reg software function. So it is, we'll talk more about this, this is a platform-specific part, which I deliberately do not want to consider here. The general idea is clear and true.
Download the BMP180 library from here.You can download the I2C software implementation from here.