SHTx Temperature and Humidity Sensor (Part 2)

 

Part 123

The trickiest part of the sensor is its communication protocol. SENSIRION makes it clear from the beginning in the SHT1x data sheet: “The serial interface of the SHTxx is optimized for sensor readout and power consumption and is not compatible with I2C interfaces”. Data is read or written from/to the sensor thanks to 9 bits blocks, containing 8 data bits and 1 acknowledgement bit. These blocks of data are used to:

  • Send commands to the sensor,
  • Read/write data to/from the internal register of the sensor,
  • Read physical measurements from the sensor. In this case data is sent in two consecutive blocks, MSB first, right aligned.

Initializing the connection between the sensor and its driving MCU is performed in the following way

sht11_01

The Data line is preset to a high level, while the Clock line is kept at a low level. Nine positive clock pulses are generated, followed by a “Transmission start” pattern.

sht11_02

The sensor is now ready for communication.

Sending a command to the sensor is quite easy

sht11_03

Start with a “Transmission start” pattern, followed by 8 data bits. Then the MCU reads the level of the data line while sending a ninth clock pulse. The previous figure illustrates a request for humidity measurement. Next to the measurement time, comes the reading time:

sht11_04

Note that this time, the MCU must acknowledge each data byte sent by the sensor, and that the sensor sends a additional byte which contains the CRC (Cyclic Redundancy Code).

Note: For laziness reasons, and because the data that I am using are not vital, I decided to skip the CRC checking in the first release of PlainSHT1x library.

For the sake of simplifying code, I also decided to standardize the signal settling times to 10µs. Signal management is performed by a simple routine which looks like:

void PlainSHTx::SetPinLevel(uint8_t pin, uint8_t level) 
{
	if (level == LVL_HIGH) {
		PORT(*_port) |= (0x01 << pin); 
	} else if (level == LVL_LOW) {
		PORT(*_port) &= ~(0x01 << pin); 
	}
	_delay_us(_signalSetUpTime);
};

In the same spirit two trivial routines will change pin modes (to INPUT or OUTPUT) and read the line levels

void PlainSHTx::SetPinMode(uint8_t pin, uint8_t mode)
{
	if (mode == MOD_INPUT) {
		DDR(*_port) &= ~(1 << pin);
	} else if (mode == MOD_OUTPUT) {
		DDR(*_port) |= (1 << pin);
	}
}; 

uint8_t PlainSHTx::GetPinLevel(uint8_t pin) 
{
	uint8_t result = ((PIN(*_port) >> pin) & 0X01);
	return(result);
};

Note: The _port variable is set in the initialization routine.

Here is an example of use of these basic routines which initialize the connection between the sensor and its driving MCU followed by the start transmission routine:

void PlainSHTx::ResetConnection(void)
/*
       _____________________________________________________         ________
 DATA:                                                      |_______|
          _    _    _    _    _    _    _    _    _        ___     ___
 SCK : __| |__| |__| |__| |__| |__| |__| |__| |__| |______|   |___|   |______
*/
{
	SetPinMode(_pinData, MOD_OUTPUT); 
	/* Set initial conditions */
	SetPinLevel(_pinData, LVL_HIGH);
	SetPinLevel(_pinClock, LVL_LOW);	
	for (uint8_t i = 0; i < 9; i++) {
		SetPinLevel(_pinClock, LVL_HIGH);
		SetPinLevel(_pinClock, LVL_LOW);	
	}
	StartTransmit();
};

void PlainSHTx::StartTransmit(void)
/* 
transmition start sequence 
       _____         ________
 DATA:      |_______|
           ___     ___
 SCK : ___|   |___|   |______
*/
{
	/* Make data pin an output pin */	
	SetPinMode(_pinData, MOD_OUTPUT); 
	/* preset initial conditions */
	SetPinLevel(_pinData, LVL_HIGH);
	SetPinLevel(_pinClock, LVL_LOW);
	/* Set clock line high */
	SetPinLevel(_pinClock, LVL_HIGH);
	/* set data line to low while clock line is high */
	SetPinLevel(_pinData, LVL_LOW);
	/* send negative clock pulse */
	SetPinLevel(_pinClock, LVL_LOW);	
	SetPinLevel(_pinClock, LVL_HIGH);
	/* raise data line while clock line is high */
	SetPinLevel(_pinData, LVL_HIGH);
	/* reset clock pîn to low */
	SetPinLevel(_pinClock, LVL_LOW);	
};

Reading or Writing from/to the sensor register is performed by this set of functions:

uint8_t PlainSHTx::ReadRegister(void)
{
	if (_errorCode != ERR_NONE) {
		return(0x00); /* exit and propagate error */
	}
	StartTransmit();
	/* query status value */
	WriteByte(CMD_STS_REG_READ);
	uint8_t result = ReadByte(ACK_YES);
	uint8_t crc = ReadByte(ACK_NO);
	/* Returned value */
	return(result);
};

void PlainSHTx::WriteRegister(uint8_t value) 
{
	if (_errorCode != ERR_NONE) {
		return; /* exit and propagate error */
	}
	StartTransmit();
	/* query status value */
	WriteByte(CMD_STS_REG_WRITE);
	WriteByte(value);
};

Note: CRC is read but not used.

These functions are featuring the sub functions for reading and writing bytes:

uint8_t PlainSHTx::ReadByte(uint8_t acknowledge)
/* Read single byte of data from sensor */
{
	if (_errorCode != ERR_NONE) {
		return(0); /* exit and propagate error */
	}
	/* Make data pin an input pin */
	SetPinMode(_pinData, MOD_INPUT); 	
	uint8_t byteValue = 0;
	/* Read bytes, MSB->LSB */
	for (uint8_t i = 0x80; i > 0; i >>= 1) {
		/* send clock pulse */
		SetPinLevel(_pinClock, 1);
		if (GetPinLevel(_pinData)) {
			byteValue |= i;
		}
		SetPinLevel(_pinClock, 0);	
	}
	/* Make data pin an output pin */	
	SetPinMode(_pinData, MOD_OUTPUT); 
	if (acknowledge == ACK_YES) {
		SetPinLevel(_pinData, LVL_LOW);	
	} else {
		SetPinLevel(_pinData, LVL_HIGH);	
	}
	/* Send 9th clock pulse */
	SetPinLevel(_pinClock, LVL_HIGH);
	SetPinLevel(_pinClock, LVL_LOW);	
	/* Make data pin an input pin */	
	SetPinMode(_pinData, MOD_INPUT);
	SetPinLevel(_pinData, LVL_HIGH);	
	/* Returned result */
	return(byteValue);
};

void PlainSHTx::WriteByte(uint8_t byteValue)
/* Write single byte of data to sensor */
{
	if (_errorCode != ERR_NONE) {
		return; /* exit and propagate error */
	}
	/* Make data pin an output pin */	
	SetPinMode(_pinData, MOD_OUTPUT); 
	/* send byte value */
	for (uint8_t i = 0x80; i > 0; i >>= 1) {
		uint8_t bitValue = (byteValue & i);
		if (bitValue) {
			SetPinLevel(_pinData, LVL_HIGH);
		} else {
			SetPinLevel(_pinData, LVL_LOW);
		}
		/* send positive clock pulse */
		SetPinLevel(_pinClock, LVL_HIGH);
		SetPinLevel(_pinClock, LVL_LOW);	
	}
	/* reset data line level */
	SetPinLevel(_pinData, LVL_HIGH);
	/* Set data pin to input mode */
	SetPinMode(_pinData, MOD_INPUT);
	_delay_ms(1);
	/* Send 9th clock pulse */
	SetPinLevel(_pinClock, LVL_HIGH);
	/* read the acknowledgment bit */
	if (GetPinLevel(_pinData)) {
		_errorCode = ERR_MIS_ACK_BIT; /* set error code */
	}
	SetPinLevel(_pinClock, LVL_LOW);
};

Note: Each critical function reads the initial error conditions (_errorCode is global to the library code) in a way which propagates the first occurrence of an error up to the final function in a sequence of functions. Error code are trigger in absence of acknowledgement bits, analogue to digital conversion error, CRC error (provisional), etc. The error code can be read and reset through public functions

uint8_t PlainSHTx::ErrorCode(void) 
{
	/* get and return the global and private error code */
	return(_errorCode);
};

uint8_t PlainSHTx::ErrorCode(uint8_t code)
{
	/* set the global and private error code */
	_errorCode = code;
	/* return the global and private error code */
	return(_errorCode);
};

Few words about the electrical wiring. I2C specifies that DATA and CLOCK lines are biased to VDD through approx  5kOhms resistors and signals are generated by switching the lines to ground (VSS). The PARALAX module includes the biasing for the data line while one should add an external resistor in order to bias the CLOCK line. This is true for micro controllers  such as the SAM3x8e from the Arduino DUE, which offer open collector (also known as open drain) outputs.

paralax_sensirion_SHT11

However, some other micro controllers such as the ATMEGA168/328 do not offer this feature. In this case, the output will be switched to VDD or VSS (GND). This may be a problem in case of inappropriate timing in the switching of the data line because this line acts as a receiver AND an emitter  Chances exist that the SHT11 sensor AND the micro controller try both to switch the data line to opposite voltages. Thus this clever little 330Ohm resistor which will absorb the current in case of such conflict (5V / 330Ohm = 15mA, which is very much acceptable as an output current for the ATMEGA series). In reality  the DATA line will read approx 0.33 V (5V / (4700+330)Ohm) when the controller switches the line to the LOW digital level. For those how really want to stick to I2C protocol, place a diode with the cathode attached to micro controller output and the anode to the CLOCK line. The 0.6 + 0.3 V drop across the diode will no effect on trigger levels. Do the same for the data line, except that you will have to add a 50k resistor in parallel with the diode so that the pin could “feel” the electrical level while reading from the sensor in INPUT mode.

 Next post on same subject

 

Leave a Reply

You must be logged in to post a comment.