crazy stuff – Neil's Log Book https://nrqm.ca What could possibly go wrong? Wed, 23 Jan 2013 01:23:53 +0000 en-US hourly 1 https://wordpress.org/?v=5.4.1 AVR Code for the MS5541C Pressure Sensor https://nrqm.ca/2013/01/avr-code-for-the-ms5541c-pressure-sensor/ Wed, 23 Jan 2013 00:22:56 +0000 https://nrqm.ca/?p=903 I originally wrote code for the MS5541C pressure sensor using bit banging, and described the result of my investigation in the previous log entry.  At the end I mentioned that the sensor’s weird digital interface is kind of like SPI, and looks SPI-compatible, with several differences that could be worked around.  The most notable difference was that reading and writing data happen out of phase with respect to the serial clock edge. It wound up being a little squirrellier than that, but it worked out.  Below is some code for accessing the MS5541C with SPI. It runs on a Seeeduino Mega (which is compatible with the Arduino Mega and uses the ATmega1280 microcontroller), but I’ve eschewed the Arduino libraries for the sake of more control (and, for the ol’ bit-banging, better timing resolution). Some of the code, particularly the SPI stuff, could be replaced with Arduino library calls.

Let’s start, as always, with some definitions:

#define SPI_DDR DDRB	// DDR of SPI port
#define SPI_PORT PORTB	// SPI port
#define SPI_MOSI PB2	// MOSI pin (Master out, Slave in)
#define SPI_MISO PB3	// MISO pin (Master in, Slave out)
#define SPI_SCK PB1	// SCK pin (SPI clock)
#define SPI_SS PB0	// SS pin (Slave Select (not used))
// wait for an SPI read/write operation to complete
#define SPI_WAIT()              while ((SPSR & _BV(SPIF)) == 0);

// bit patterns for reading calib words and sensor data, and for sending reset
#define READ_W1 0b0001110101010000
#define READ_W2 0b0001110101100000
#define READ_W3 0b0001110110010000
#define READ_W4 0b0001110110100000

#define READ_D1 0b0000111101000000	// pressure read command
#define READ_D2 0b0000111100100000	// temperature read command

#define RESET   0b1010101010101010

The only thing worthy of comment is the read commands, which I had to pad out. The commands are only 10 or 12 bits, but we can left-pad them with zeros since the MS5541C will ignore anything it gets before the three-bit start sequence (111). When reading the four calibration words I also had to add an extra 0 after the three-bit stop sequence (000), because the sensor waits for one extra clock cycle before it starts sending back the calibration data. Same story with the commands to initiate pressure and temperature readings, except they wait for two cycles before starting the conversion so I padded them with two extra 0s after the stop sequence.

Function to send commands:

void send_bytes(uint16_t data)
{
	SPCR &= ~_BV(CPOL);
	SPDR = data >> 8;
	SPI_WAIT();
	SPDR = data & 0xFF;
	SPI_WAIT();
}

The commands are all 16 bits, just split them up and send them one byte at a time. SPI’s default is big-endian bit and byte ordering.  The first line of the function clears the SPI CPOL bit, which sets the polarity so that data are read (by the sensor) on the rising clock edge.

Function to read data back:

uint16_t get_bytes()
{
	uint16_t data;
	SPCR |= _BV(CPOL);
	SPDR = 0;
	SPI_WAIT();
	data = SPDR;
	data <<= 8;
	SPDR = 0;
	SPI_WAIT();
	data |= SPDR;
	return data;
}

Every piece of data is a 16-bit integer, so the program never has to read anything other than two bytes. Writing a 0 value to SPDR will initiate a transfer where the SPI module will send 0 on MOSI and read whatever the sensor sends back on MISO. Once SPI_WAIT() finishes, the incoming data will have replaced the outgoing data in SPDR and can be read back out. The second line of the function sets the CPOL bit so that data are read (by the microcontroller) on the falling clock edge.

Function to get calibration coefficients:

typedef struct {
	uint16_t SENST1;
	uint16_t OFFT1;
	uint16_t TCS;
	uint16_t TCO;
	uint16_t Tref;
	uint16_t TEMPSENS;
} coeff_t;

void get_coeffs(coeff_t* coefficients)
{
	uint16_t w1, w2, w3, w4;

	send_bytes(READ_W1);
	w1 = get_bytes();
	send_bytes(READ_W2);
	w2 = get_bytes();
	send_bytes(READ_W3);
	w3 = get_bytes();
	send_bytes(READ_W4);
	w4 = get_bytes();

	coefficients->SENST1 = w1 >> 3;
	coefficients->OFFT1 = (w1 & 0b111) << 10;
 	coefficients->OFFT1 |= w2 >> 6;
	coefficients->TCS = w3 >> 6;
	coefficients->TCO = w4 >> 7;
	coefficients->Tref = (w2 & 0b111111) << 6;
	coefficients->Tref |= w3 & 0b111111;
	coefficients->TEMPSENS = w4 & 0b1111111;
}

This function just reads the four 16-bit factory calibration words, and then unpacks them into the coefficients in the manner defined in the datasheet. The datasheet often refers to the coefficients as C1 through C6, they're in that order in the structure.

Reading the temperature:

int16_t get_temp_diff(coeff_t* coeffs)
{
	uint16_t T;
	uint16_t UT1;
	int16_t dT;
	int32_t dT_sq;
	send_bytes(READ_D2);
	_delay_ms(33);
	T = get_bytes();
	// find reference temperature
	UT1 = 8 * coeffs->Tref + 10000;	// maximum value is 42760
	dT = T - UT1;		// find difference between temperature reading and reference
	// calculate second-order temperature differential
	dT_sq = dT;
	dT_sq *= dT;	// dT squared
	dT_sq /= 128;
	dT_sq /= 128;	// looking for (dT/128) * (dT/128)
	if (dT >= 0) dT_sq /= 8;
	else dT_sq /= 2;
	// correct dT using second-order differential
	return dT - dT_sq;
	return 0;
}

int16_t get_temperature(int16_t dT, coeff_t* coeffs)
{
	int32_t acc = dT;
	acc *= (coeffs->TEMPSENS + 100);
	acc /= 2048;
	acc += 200;
	return (uint16_t)acc;
}

Temperature is obtained in two steps. First we get the temperature differential, which is the difference between the actual temperature and the reference temperature, Tref. Second we use that along with the TEMPSENS constant to calculate the actual temperature in units of 0.1°C (that is, a result of 200 corresponds to a temperature of 20.0°C).

I'm doing the second-order temperature differential correction as the datasheet suggests. At room temperature the second order differential is pretty close to 0.

Only the temperature differential is used to calculate the pressure. The actual temperature value isn't needed except for display and otherwise doesn't need to be calculated.

Calculating the pressure, in millibars:

uint16_t get_pressure(int16_t dT, coeff_t* coeffs)
{
	int16_t off;	// offset at temperature
	int32_t sens;	// sensitivity at temperature
	uint16_t P;		// pressure value
	int32_t pressure;	// actual pressure in mbar

	off = coeffs->TCO - 250;
	off *= dT;		// this shouldn't overflow, but watch out.
	off /= 4096;
	off += coeffs->OFFT1 + 10000;

	sens = coeffs->TCS + 200;
	sens *= dT;
	sens /= 8192;
	sens += coeffs->SENST1 / 2 + 3000;
	send_bytes(READ_D1);
	_delay_ms(33);
	P =  get_bytes();
	pressure = P;
	pressure -= off;
	pressure *= sens;
	pressure /= 2048;
	pressure += 1000;
	return (uint16_t)pressure;
}

This function just goes through the flow diagram in the datasheet. First it calculates the ADC offset and sensitivity, based on the temperature differential. Then it measures the actual pressure, and does math to it to convert the ADC units into millibars.

The datasheet specifies that each ADC conversion takes 33 ms to complete (I measured it to be around 31.3 ms). It actually outputs a trigger on the MISO pin whose falling edge indicates that the conversion is done. Unfortunately the SPI pins can't seem to be read without disabling the SPI module. One solution would be to tie the MISO line to an separate pin, and poll that while the conversion runs. The separate pin could also be an external interrupt that waits for a falling edge.  That's probably the right way to do it, but I just stuck in a busy wait.

The last software issue that needs to be considered is clock frequencies. The MS5541C has two clock inputs: MCLK, the master clock, and SCLK, the serial clock. The latter is generated by the SPI module, and can be at most 500 kHz. The former is intended to hook up to a 32,768 Hz clock source. It's unbuffered, so the clock voltage swing has to be the full TTL-compatible range. You could hook up a buffered watch crystal, I just generated a close-enough clock from the microcontroller as a PWM signal.

I've omitted the configuration code for the SPI, UART printing, and PWM signals. You can download the complete program here. The datasheet recommends averaging 8 samples to get an accurate result; I did not do so. The code assumes the following pin mapping to the digital pins on a Seeeduino Mega (AVR pin identifiers are noted in brackets):

MS5541C -> Seeeduino (AVR)
SCK -> D52 (PB1)
DOUT -> D50 (PB3, MISO)
DIN -> D51 (PB2, MOSI)
MCK -> D11 (PB5)
Vcc -> 3.3 V
GND -> GND

N.b. that the MS5541C is a 3.3 V device. The Seeeduino uses 5 V I/O, so the signal lines that go from the Seeeduino to the MS5541C (MOSI, SCLK, and MCLK) need to have voltage dividers or level shifters to step the voltage from 5 V down to 3.3 V. I used voltage dividers with resistor values of R1 = 22 kΩ and R2 = 43 kΩ.

]]>
Un-improving range on the infrared channel https://nrqm.ca/2011/10/un-improving-range-on-the-infrared-channel/ Tue, 04 Oct 2011 03:51:46 +0000 https://nrqm.ca/?p=668 It turned out doing software UART was a terrible idea.  The processor is way too slow to support a reasonable baud rate.  I did figure out how to use a comparator though: the key phrase I was missing was “rail-to-rail.”  That means that inputs can be in the full voltage range from ground to Vcc.  Another handy phrase is “push-pull,” which means that the comparator can output 0 and 1; in contrast, an “open collector” comparator can only output 0, and needs an external resistor to pull the output to 1.

I bought a rail-to-rail push-pull comparator, the MCP6541, and tried it with the receiver circuit, and sure enough it increased the maximum range significantly.  Unfortunately it also increased the minimum range significantly.

Here’s the circuit diagram for the modified receiver circuit:

Infrared receiver with comparator for amplification.

Infrared receiver with comparator for amplification.

The 1 kΩ and 10 kΩ resistors are in a voltage divider configuration to generate a 3.0 V reference against which the input is compared.  If the input is higher than 3.0 V, the comparator outputs 3.3 V, and otherwise the comparator outputs 0 V.

I guess there are a bunch of transistors inside the comparator, and putting them in a chain with the pair of external transistors messes things up.  I don’t know why, but it might be a gain-bandwidth thing (too much gain, lowers the maximum frequency the circuit can operate at).  This is what the output looks like at about 6.5 cm:

'U' character sent over infrared with infrared amplifier.

'U' character sent at 19200 bps over infrared with comparator amplifier.

The jagged bits aren’t there without the comparator.  I used the U character to test because its binary pattern is 01010101.  As you can see, the pattern is there but it’s kind of messed up and it’s dangerously close to 3.0 V.  (Now that I look at this again I see maybe reducing the 3.0 V threshold would improve the minimum range.)

I figured I could put an OPA2134 op-amp voltage follower in between the external transistors and the comparator.  This would de-couple the two sets of transistors, so the weird transistor chaining effect would disappear.  This worked perfectly, once, for no apparent reason at all.  Eventually, after spending a day trying to reproduce my success, I read the op-amp’s datasheet more closely and discovered that it’s not rail-to-rail and should never have worked (it can’t output the full 3.3 V signal, so the comparator’s input was never reaching 3.0 V).

I switched to a JFET op-amp that had rail-to-rail output, and it was able to transmit the ‘U’ character at a good set of ranges, both small and large.  Unfortunately this is the signal it was outputting, at 7.5 cm and at 2 cm:

'U' sent at 19200 bps over infrared with JFET voltage follower at 7.5 cm.

'U' sent at 19200 bps over infrared with JFET voltage follower at 7.5 cm.

'U' sent over infraret with JFET voltage follower at 2 cm.

'U' sent at 19200 bps over infrared with JFET voltage follower at 2 cm.

The observant among you, my imaginary audience, will notice two strange things from this screenshot:

  1. The signal is peaking at almost 4 V.
  2. It’s sending 9 bits at 7.5 cm and 19 bits at 2 cm.

Hoo boy, it didn’t work at all, and it was pure luck that the ‘U’ transmitted correctly at short range (followed, presumably, by a framing error that my system silently ignored).  No other character worked.

I found out that you can use an op-amp as a comparator, so I tried that with my OPA2134:

'U' sent at 19200 bps over infrared with op-amp comparator.

'U' sent at 19200 bps over infrared with op-amp comparator.

Okay!  You can see what it means that the amplifier is not rail-to-rail, it’s only going up to 2.6 V (and that only briefly), but if I remember correctly it received.  Unfortunately the high bits got narrower for some reason as the range decreased, so it didn’t actually do anything to solve the range problem.

I tried using the JFET amp as a comparator, but it didn’t output anything at all so I gave up and ignored the project for a few weeks.  My current solution is to pretend there isn’t a problem, and if I run into the  minimum range issue then I can just bend the receiver askew so that the signal is damped enough to receive.  In the meantime, the transmitter now has a wider field of view, which ought to make it easier to place several receivers in range of the control unit.

]]>
The Schmitt trigger https://nrqm.ca/2010/12/the-schmitt-trigger/ Fri, 03 Dec 2010 05:06:11 +0000 https://nrqm.ca/?p=327 Schmitt trigger example

Simulation of a Schmitt trigger. The yellow sine wave is the input, the purple square wave is the output.

I was looking for a way to convert an analogue signal into a digital signal, and came across the Schmitt trigger.  This is a great little circuit that you can build with a couple transistors or with an operational amplifier.  I tried to build a transistor-based simulation in Simulink, but it didn’t work right away.  My op-amp simulation did work, so I stuck with that.

The principle is simple: if the trigger input rises above a certain threshold, then the output saturates to the op-amp’s positive power supply voltage.  When the input falls below another threshold, the output saturates to the op-amp’s negative power supply voltage.  If the input lies between the thresholds then the output doesn’t change from whatever it was before.

This is incredibly useful.  To the left you can see a 0.3 V sine wave being converted into a 5 V binary signal (actually a 4.5 V signal, since I added a diode in series with the output to keep it from going down to -5 V).  It also effectively filters out any input jitter that doesn’t cross the threshold needed to change the output state.  Microcontrollers have Schmitt triggers on their digital inputs.  For example, an AVR microcontroller being powered by 5 V typically considers a 0 logic level to be under 1.5 V and a 1 logic level to be over 3 V.  If the input sits between 1.5 V and 3 V then the digital state remains whatever it was before the input entered that region.

Alas, it’s not a perfect circuit.  I used an online calculator to figure out what resistances I needed for my project.  Configurations that worked in the calculator and in simulation didn’t work in reality, either producing no output or something more like a sawtooth wave than the square wave I expected.  It’s probably a limitation in my op-amp—maybe the signal is too high-frequency, I didn’t investigate very deeply.

]]>