One day embedder programmer. Writing a driver for the STM HTS221 humidity sensor - is it very simple?

    When I evaluate the reasonableness of the user interface of specialized microchips from STMicroelectronics, I am sometimes surprised by the fact that they are generally capable of working. But they work the same. And they do not just work, but have a bunch of chips and an extremely low price. As a result, you have to choose them again and again ... The

    next day promised to be as simple and enjoyable as possible when once again you save a “burning” project. According to the plan, until the evening all that was needed was to revive the integrated combined temperature and humidity sensor. The tiny sizes occupied by it on the board, the small number of legs and the lack of discrete components of the “harness” made it possible to hope that you are dealing with the latest development, and modern sensors, despite the small sizes, are smart and quick-witted. They without any questions give out the finished result. Often they do not just take measurements, but produce very complex signal processing, have internal buffers for storing data, interrupt outputs to wake the microcontroller duringand many other nice features. All this greatly facilitates the task of writing code and reduces the resource requirements of the control microcontroller ... It is easy and pleasant to communicate with them. True, sometimes you have to tinker with a lot of settings. However, this did not threaten me today, because in front of me is just a banal capacitive humidity meter with a temperature measurement function.

    “A couple of hours for screwing the interface, a couple more for register configuration and half a day ahead are free,” I thought. It will be possible to spend dinner in the forest, if not on barbecue, then at least for a modest picnic.

    The only thing that inspired me with a vague sense of anxiety was the manufacturer: STMicroelectronics. But casting aside memories of dancing with a tambourine around their microcircuits for an electric meter, PLC modem, an advanced inertial sensor ... I set to work.

    We make an acquaintance

    A quick acquaintance with the datasheet and the circuit showed that this time neither the circuit technician nor the tracer was “messed up” (of course, the blocking capacitor next to the case should be installed and the power parted a little differently, but this will affect the accuracy of measurements, and not the performance). The exchange interface between the microcontroller and the sensor via I2C was suspiciously similar to the standard one.

    I will not describe in detail the standard I2C, I will only decipher the basic terms from the datasheet, which will be required a little later.

    We agree on the terms. For mutual understanding

    ST - start condition;
    SR is the operation of reinstalling the start of a condition; it serves to solve the conflict of reading from the register. We cannot perform a clean read operation — we must first pass the register number we are reading to the device;
    SAD is the address of the slave device - our sensor (the least significant bit in it occupies the sign of operation - ”+ W: or 0 for writing,“ + R ”or 1 for reading. Thus,“ SAD + W ”= 0xBE“ SAD + R ”= 0xBF;
    SAC is the confirmation bit “sent” by the slave device, in our case, the sensor;
    MAK is the confirmation bit from the microcontroller, which it sets in response to the fact that NMAK data was read, its absence, which characterizes the last byte;
    SUB is the register address itself
    DATA - actually the data
    SP - stop condition

    Confirmation is made by shorting the SDA bus to ground

    It is hard to believe that STM venturers this time refused innovations, but suddenly they change their policy. Well, let's start checking.

    First steps. First problems

    There were no signs of difficulty in looking at this picture. In total, it’s business - to count a pair of 16 bit registers!

    I’m finalizing my I2C exchange template and within an hour the microcircuit cheerfully responds with confirmation bits to an attempt to write to the register. Well, to teach a microcontroller to read data from registers is a little more difficult. Where are the registers in which the measurement results are stored? Yes there they are, but they give out solid zeros. Well, you have to read more about the configuration. Half an hour behind the monitor screen and a cup of tea show that by default the microcircuit is inactive, however, you can get it out of the stupor by changing only one byte in the configuration register. At the same time, we will push a couple more so that the measurements take place cyclically, since there is no need to deal with severe energy savings in this project.


    Hooray, data appeared in 16 bit temperature and humidity registers, but somehow they look strange. And in the case of temperature and humidity, both bytes are equal to each other. This is at least suspicious. It’s necessary to understand. An hour is spent searching for errors in the exchange protocol and checking for delays. Nothing helps, you have to go deep into datasheet again. Bah ... yes, I was not mistaken. Well, STM developers could not prepare a gift for the programmer. There are few registers in the sensor and one byte is enough for their addressing in excess. When you read or write one byte everything is fine. But according to the I2C specification, you can both read and write a few bytes at a time, and I actively use this feature in my program. The main thing is, in the first case, after each byte, send a confirmation bit, and in the second, check whether the sensor returned it.

    It turns out that after reading or writing one byte, the counter should increase by one and move to the next byte, it is necessary to set the most significant bit to 1 in the register address byte! Otherwise, you will work to interact with the same register until you turn blue. Brilliant, and most importantly, how incomprehensible. Why is this?
    Add a crutch to the standard protocol.

    if (ByteCount>1)

    There are no victories without defeat

    The read data begins to please with its variety, but only in what parrots do they measure them? The read values ​​look very strange. It looks like until you read this datasheet from cover to cover, you won’t cook porridge with this sensor.

    This is an ambush! It turns out that in order to get a tangible result in degrees and percent, you still have to do the calculations. Extract calibration data from specific registers and use the piecewise linear approximation to calculate the values. It’s not that it is difficult to write a piece of code in C, but a thorough study of the datasheet cannot be avoided. The code volume grows and as a result I got another problem - the memory of the microcontroller is running out. The circuit designer originally laid the stone with only sixteen kilobytes of FLASH, and polling a humidity sensor is only a small fraction of the tasks assigned to it. Since the 32-bit microcontroller, we have only 4K words for code and constants. Even taking into account the fact that I do not use heavy HAL firewood, this is monstrously small. I was promised to purchase a new version of microcontrollers with memory twice as large. How's it going? I’m calling the project manager. New stones have already arrived, but they have not been soldered to the boards. It turns out they are waiting for me to report that no errors were found in the board. Well, I’ll report and get an answer - by the end of the week a stone flower may be piled to you. Of course, the board is not complicated, you could solder it yourself, although the chips are very small and require painstaking soldering with a hairdryer. However, there are people specially trained in the installation in this project, why leave them without work, especially since my schedule is very tight and there’s nothing I can do except spend time. I will report and get an answer - by the end of the week a stone flower may be piled to you. Of course, the board is not complicated, you could solder it yourself, although the chips are very small and require painstaking soldering with a hairdryer. However, there are people specially trained in the installation in this project, why leave them without work, especially since my schedule is very tight and there’s nothing I can do except spend time. I will report and get an answer - by the end of the week a stone flower may be piled to you. Of course, the board is not complicated, you could solder it yourself, although the chips are very small and require painstaking soldering with a hairdryer. However, there are people specially trained in the installation in this project, why leave them without work, especially since my schedule is very tight and there’s nothing I can do except spend time.

    In this situation, without debugging, one cannot advance. It is necessary to comment out a part of the already written code in order to free up space for calculations.

    The linear approximation task doesn't look too complicated,

    but knowing the magicians from STM is better to look for a turnkey solution. 10 minutes of surfing leads me to Application with a description of the calculation method and even pieces of C code .

    Why do just what is difficult to do?

    I copy to my program. Here it is lunch time. With a longing look at a strip of forest on the horizon, it seems not like shish kebabs, but even a picnic in nature is being canceled for today. Dinner is a little uplifting and you get strength for a detailed study of application. I understand the registers with calibration values. At first, their organization baffles me, then it causes a laugh, almost hysterical. Even for STM products, overkill is felt. At this point, I want to dwell in more detail.

    We look at the picture and try to evaluate the flight of thought. In the first column of the table, the address is in HEX format, in the second name, in the third data type, followed by a bit description.

    With the values ​​marked as s16, everything is clear - 16 bit signed, presented in the standard additional format.

    With those at addresses 30 and 31 all the more fun. They are eight-bit. But in order to use them in the calculations, it is necessary to divide their value into two. The least significant bit. Was it really difficult to just put the significant 7 bits into the register, and leave the oldest at zero?

    But these were still flowers. Berries went with 8 bit values ​​to addresses 32 and 33. It turns out that they lack two more bits - the ninth and tenth. For some reason, they are stored in the register with the address 35. It contains 2 high bits from one register with the number 32 and two from the other with the number 33. Why? Isn’t it easier to allocate two bytes for each of them and calmly write the values, because all the same, the register with address 35 is empty!

    But that is not all. As a result of manipulations, we get 10 bit registers, but it turns out that for the calculations we need them only the highest seven bits. Therefore, the three least significant bits must be discarded; the value must be divided by 8 and only then substituted into the formulas.

    In other words, we only need seven bits for calculations. Why are all these manipulations being asked when they could easily fit into an eight-bit register!

    Really some kind of brain rupture. Information in registers with addresses 30-31 could look like this:

    To extract information, one would not have to make unimaginable somersaults, and in the memory of the registers there is still an extra bit left.

    Travel to Mercury

    Okay, I’m checking the program given in the upstream. Everything seems to be very similar to the description. However, the penultimate line pleases:

    If the relative humidity is more than 1000 percent, we take it for 1000 percent.


    As a result, not even two hours pass, as I finally check the measurement result in practice. He scares me. The sensor shows that the weather conditions in my room are very close to the depression at the bottom of the imaginary Mercury ocean. The temperature is above 500 degrees Celsius, and humidity in the region of 1000 percent. It seems I’ve completely earned and it's time to turn on the air conditioning.

    When it turns out that the sensor readings are affected slightly, I start checking the code. I could not refrain from finalizing the roughly written template from ST, suddenly messed up? A half-hour check does not produce results. Once again, I delve deeper into datasheets to understand where these strange multipliers of “10” came from in calculations by the linear interpolation method. Half an hour of attentive proofreading gave the result - it turns out it was a move aimed at improving the accuracy of integer division. True, at the output we got the results no longer in percentages, but in ppm for humidity and in degrees Celsius multiplied by 10 for temperature.

    I try to throw them out of the code and suddenly move back to the middle lane - 29 degrees and 58 percent humidity. I press the sensor with my finger and very quickly the temperature readings increase to 34. Hurray, you can finally round off.

    It’s just that it’s getting dark outside the window and poor people are returning along the sidewalks, returning from work on Moscow electric trains. Well, I’ll go at least to the park for a walk, prepare myself mentally for tomorrow morning conversation with the general from Kazan. Their company decided to try their luck in import substitution for a couple with Bashneft, you need to think about what they hope to get from me and how actually I can help them.

    Dry residue

    The bottom line is - ST did everything so that our brother, the developer, even to write a simple driver, read the manual from cover to cover, and even tormented with application:

    • When writing-reading several bytes, it is necessary to change the register address;
    • Actual measured values ​​must be calculated using calibration values;
    • The calibration values ​​themselves are located in memory in a very strange way and they need to be collected from there by pieces carrying out circus manipulations with bits;
    • The program text from the application file contains errors;
    • Calibration and backup registers are open for free recording. Accidental recording of new values ​​in them will lead to distortion of the results during the calculation;
    • Relative humidity can easily go over 100 percent and this does not bother the manufacturer at all;

    The above-described problems that I encountered in the process of writing the simplest driver took me more than half a working day, and a newcomer could provide them with work for several days.

    What can I say about creating a driver for some kind of PLC modem, when circuitry errors, hardware problems, nuances of communication protocols, and the quality of communication lines are added to the strangeness of the chip and inaccuracies in the description. When the microcontroller must read and encode / decode data on the fly and you have to use the interrupt mechanism and direct memory access to the fullest. Moreover, the code should be executed in “shadow mode” and have minimal impact on the course of the main program.

    I'm not talking about the fact that manufacturers sometimes deliberately keep silent about some not-so-pleasant nuances, or provide not-so-reliable information.

    In such conditions, without experience, good knowledge of circuitry and physics of the device, the developer has to be very tight.

    It's time to report on the results

    And finally, I’ll try to make life easier for those who decide to use this sensor in their designs. I will publish below the code of the corresponding software modules. I must say right away that it does not include low-level I2C data exchange functions, since it strongly depends on the particular implementation — the microcontroller, the exchange bus, and even the numbers of connected ports used for polling.

    Data exchange with the sensor is carried out via the I2C interface. Two functions are used that support the standard protocol; you have to write them yourself or search in standard libraries.

    One for recording data, according to the protocol shown in the figure below

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

    Second to read data

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

    data - data buffer
    ByteCount - number of bytes read or written
    The program code is very simple and plentifully supplied with comments. When the sensor driver is initialized, the constants for calculations are read from it and placed in a separate structure. It also contains readings that are read and converted into a readable form.
    This program code has been modified after a hectic two-day discussion process. I tried to increase the accuracy of transformations by using the low-order bits of the registers H0_rH_x2, H1_rH_x2, H0_T0_OUT, H1_T0_OUT. In the case of calculating the temperature, this did not significantly affect the result; in the case of humidity, it led to inadequate results. It was possible to significantly bring the readings closer to the real ones by using the float type for calculations. For 32-bit microcontrollers, the calculation of floating-point numbers is not a very demanding task, and high speed calculations when working with this sensor are not required. I bring to the attention of readers the program in this particular version.
    I express gratitude to all the users who participated in the discussion, especially mdn-tech , olartamonovand lorc . In addition, I express my gratitude to the Habr team for the fact that, among the innovations, they did not cover the possibility of editing articles after they were written. This allows you to change the content according to the results of the discussion in the comments. I try to use this opportunity - I correct errors and sometimes add interesting information. After several iterations, the article becomes more interesting, more fully reflects the topic and most importantly does not contain errors.

    Header file code

    #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
    //и самые данные
    	float H_OUT;//считанное значение влажности
    	//данные калибровки для влажности
    	float H0_rH;
    	float H1_rH;
    	float H0_T0_OUT;
    	float H1_T0_OUT;
    	float T_OUT;//считанное значение температуры
    	//данные калибровки для температуры
    	float T0_degC;
    	float T1_degC;
    	float T0_OUT;
    	float T1_OUT;
    	float Humidity;
    	float Temperature;
    } THTS221str;
    THTS221str HTS221str;
    unsigned char DevCodeRead(void);
    unsigned int InitHTS221CalibrTab(void);
    int HTS221_Get_Humidity(void);
    int HTS221_Get_Temp(void);

    SI program code

    #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;
    		return 0;
    unsigned int InitHTS221CalibrTab(void)
     unsigned char buffer[4];
     short tmpi;
     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]/2);
    	HTS221str.H1_rH = (buffer[1]/2);
    	//2. Read H0_T0_OUT
    	if (!RDHTS221regs(adr_H0_T0_OUT,(unsigned char*)(&tmpi),2))
    		return 0;
    	//3. Read H1_T0_OUT
    	if (!RDHTS221regs(adr_H1_T0_OUT,(unsigned char*)(&tmpi),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*)(&tmpi),2))
    		return 0;
    	//4. Read from 0x3E & 0x3F registers the value of T1_OUT
    	if (!RDHTS221regs(adr_T1_OUT,(unsigned char*)(&tmpi),2))
    		return 0;
    	return 1;
    int HTS221_Get_Humidity(void)
     short tmp;
     //4. Read H_T_OUT
     if (!RDHTS221regs(adr_H_OUT,(unsigned char*)(&tmp),2))
     	return 0;
    /*5. Compute the RH [%] value by linea r interpolation */
     HTS221str.Humidity = ((HTS221str.H_OUT - HTS221str.H0_T0_OUT) * (HTS221str.H1_rH - HTS221str.H0_rH))/(HTS221str.H1_T0_OUT - HTS221str.H0_T0_OUT) + HTS221str.H0_rH;
    /* Saturation condition*/
     if (HTS221str.Humidity>100)
      HTS221str.Humidity =100;
    return 1;
    int HTS221_Get_Temp(void)
     short tmp;
     //4. Read H_T_OUT
     if (!RDHTS221regs(adr_T_OUT,(unsigned char*)(&tmp),2))
     	return 0;
    /*5. Compute the RH [%] value by linea r interpolation */
     HTS221str.Temperature = ((HTS221str.T_OUT - HTS221str.T0_OUT) * (HTS221str.T1_degC - HTS221str.T0_degC))/(HTS221str.T1_OUT - HTS221str.T0_OUT)+ HTS221str.T0_degC;
    return 1;

    Useful materials

    Link to the manufacturer’s sensor page

    Final survey

    The manufacturer said that the sensor already contains a digital signal processor for processing results. Then why it was impossible to implement the simplest calculations to provide data in a user-friendly form. Well, he’s too busy, but why not at least the calibration registers are arranged in a normal way? Why are there simple errors in the simplest demo code? And a lot more why.

    Only registered users can participate in the survey. Please come in.

    Why does STM pay so little attention to the usability of developers with their products?

    • 42.4% Low price and rich functionality compensate for all interface shortcomings 70
    • 27.8% Individual development teams are composed of low-skilled specialists 46
    • 32.1% They don’t even think of this issue 53
    • 37.5% Too in a hurry to release a new product and do not pay attention to such trifles 62
    • 34.5% Savings on interface optimization significantly reduce the cost of developing new products 57
    • 18.1% It’s just nice for them to realize that programmers are forced to read their manuals from cover to cover 30
    • 3% Other, note in comments 5

    Also popular now: