Samstag, 30. Dezember 2017

BME280 USB adapter: microsoldering practice

Motivated by the videos on Louis Rossman's YouTube channel 
I wanted to try a small scale project myself. I've chosen to build small size USB adapter for the Bosch Sensortec BME280 sensor. It measures pressure, temperature, and relative humidity in a super tiny package. For the USB connection I use an AVR ATMEGA8 microcontroller with the V-USB software-only USB implementation.
The sensor is connected to the MCU using the SPI interface. Schematics below
The firmware is based on the V-USB reference project called PowerSwitch.

Using the sensor requires to read the factory calibration coefficients from the sensor memory. Then read the raw data and apply the calibration. For the calibratoin, some functions are given in the BME280 datasheet. The calibrated output is transfered over USB to a program running on a PC.

The PCB was produced on https://oshpark.com/ for less than 3 EURO for 3 pieces. The USB connection is of mini-B type. The 6-pin programming header is only needed for firmware upload and can be removed once the firmware development is done. For the soldering of the QFN packaged ATMEGA chip a microscope is really helpul. The soldering itself might be possible without microscope, but the optical inspection of the joints to the pins is impossible with the naked eye. Passive components are 0603 size.

To get an impression of the data quality from this sensor, I took data for about 30 hours. Here is the result:
The large spike in temperature around 3-4 hours after starting the measurement was because the sensor was too close to the laptop and the warm air from the CPU fan reached the sensor. The dips in temperature or humidity occurred after opening the window. The straight upwards slope in the humidity curve between hours 12 and 22 was during the night and shows the water evaporation from a single adult person in a small room (~30 cubic meters of air, doors and window closed).

Montag, 23. Oktober 2017

gamma spectroscopy #3: Dynamic Time-over-Threshold (DTOT)

Introduction

In a previous post, I build a 4 channel data acquisition system for scintillation gamma spectroscopy using the Time-over-Threshold (TOT) technique. The results were promising, but one issue was that the resolution was getting bad if the threshold was too low. The reason for this is shown in the following picture.

There is always some noise on the signal (pink). The region where the trailing edge intersects the noise envelop is larger for thresholds that are closer to zero Volts. And this region directly creates the uncertainty in the Time-over-Threshold measurement. Note that this is not a problem for the leading edge because of its fast rising slope.
One could just put a higher threshold and the uncertainty will be less, but the disadvantage is that small signals below the threshold are not measured at all. 

Dynamic Time-over-Threshold

One possible solution is to start with a small threshold (just above the noise) and as soon as the leading edge is detected the threshold is increased dynamically.
The following picture shows a spice simulation including a small modification of the circuit. The dynamic threshold is created from the comparator output and is fed back into the previously unused second input of the differential amplifier.
Both lines in the graph show the inputs of the comparator. One can see that the threshold is dynamically increased and the trailing edge is intercepted at a higher slope. That effectively reduced the uncertainty of the trailing edge time measurement. I took the idea from this publication. My implementation is however different from what they mention in that publication. I've done the simplest possible extension to my existing circuit.

Measurements

I modified one of the channels of my PCB by adding the three components in the feedback path from the comparator output to the j-FET input.
After doing this I measured the comparator inputs with the oscilloscope and got the following picture (quite similar to the simulation).

Spectroscopy

Finally I made a setup with one single LYSO crystal and one 3x3 inch NaI detector. The detector signal was going into the modified channel of my acquisition box. I got an additional signal from a Silicon photomultiplier that was attached to the LYSO crystal. The LYSO setup can be seen in the following picture: the bare parts and the final assembly wrapped in Teflon tape, aluminum foil and black tape.

This signal was recorded with an unmodified channel (no dyamic ToT) of the acquisition box. The following is a pulse height histogram of the NaI detector for all signals that have a coincident signal in the Silicon photomultiplier (5 microseconds time window). 
The most important point here is the low energy limit of the spectrum as a result of the dynamic threshold behavior. The lowest visible peak is at about 28 keV and the spectrum extends down to half of this value (~20 keV). This was not possible without the dynamic threshold adjustment. The spectrum was calibrated using the known positions of the peaks. Labels on peaks show the decaying level and the FWHM resolution.


HPGe detector readout

At work, I got the chance to connect my device to the pre-amplifier output of a high-purity Germanium (HPGe) detector and a 60Co source. The count rate was around 400 Hz. I didn't have the time to do a lot of tuning, but the result looks promising

It is not so easy to estimate the actual resolution because of the nonlinear time-over-threshold values and the unknown threshold height. But I can see where the Compton edges are. Based on that I can guess that the threshold was quite high (maybe 400 keV). The resolution of the peaks in the histogram is 1% (FWHM/position). The conclusion is that the energy resolution of this setup was better than 1%.
It would be interesting to find how close I can get to the ~0.2% intrinsic HPGe resolution with a bit more tuning of the setup.
Another conclusion is that this method is definitely sufficient to read-out scintillation detectors.

Mittwoch, 27. September 2017

gamma spectroscopy #2: FPGA-based 4-channel data acquisition for gamma spectroscopy

Introduction

In a previous blog post I was showing how to do gamma spectroscopy using the sound card of a PC. This approach is very cheap and simple, but there are some disadvantages:

  • Limited time resolution: with a sampling rate of 192kHz the time resolution is only about 5 microseconds in a naive implementation. With some software interpolation, this value could be decreased to maybe 1 microsecond (I would guess... but I never tried it)
  • Limited rate: Because of the low sampling rate, the pulses have to be stretched to a length of about 50 microseconds in order to achieve a good energy resolution. Because of that, even with moderate count rates in the detector, there is already a lot of overlap in the pulses and that results in reduced energy resolution.
  • Limited number of acquisition channels. This is another severe point. Sound cards with more than 2 channels are very uncommon. They are for sure not standard in a PC and if one has to buy new hardware anyhow, one can also by something more specialized for gamma spectroscopy.
I want to present my first attempt to build an FPGA based data acquisition system for gamma spectroscopy with scintillation detectors.

Basic Idea

The system is based on the time over threshold (TOT) method, where the signals from a scintillator are converted into a digital signal by a threshold comparator and the length of the digital pulses is measured. A time resolution of 5 nanoseconds is easy to achieve using a 200MHz counter inside an FPGA and a rising/falling edge detector that sends the current counter value to the PC.
As can be seen on the picture, the duration for which the detector signal is over/under the threshold depends on the signal amplitude. The dependence is non-linear. For signals that have a fast rising edge and a relatively slow exponential decay the dependence is approximately logarithmic, i.e. TOT is proportional to log(Amplitude).

Implementation

The system is implemented using a Mojo Spartan6 FPGA development board from embedded micro and a self-made PCB with an 5x amplifier using BF862 JFet Transistors and a LT1720 comparator. The following picture shows the schematic for one of the four implemented channels. The signal SIG1 goes directly into an IO pin of the FPGA. The threshold can be adjusted with the potentiometer RV2 below the two transistors. The amplifier is a differential one with the second input tied to GND to compensate for temperature drifts. In addition to the signal amplification, the amplifier stage lifts both input signals for the comparator to a voltage below 3.3V and GND, allowing to use a single supply voltage. The signal path is entirely DC coupled, so even for high rates there will be no base line shift. The input impedance of the amplifier (22 kohm) determines the signal length. With a 5 ns resolution, it might be possible to use even 50 ohms which would imply no additional signal stretching at all (because the coaxial cable has also 50 ohms) and would have the additional advantage that reflections are avoided (this is however only relevant if the cables are longer).
Here are some pictures from the analog front-end PCB and the assembly with the Mojo board and the box that I used to mount everything inside.

FPGA architecture

The FPGA project is based on the VHDL port of the Mojo base project. The communication with the PC over USB (a virtual serial terminal) is already implemented in the base project. All I had to do was to implement four of the counter based TDCs. 

The Mojo base project runs at 50MHz clock speed. In order to have a high time resolution, the counter clock should be as fast as possible. Without any effort, I could go up to 200MHz. For higher counter frequencies, my design doesn't work reliable anymore. For now, I am quite happy with the resulting 5ns resolution.

Spectroscopy results

The first results are very promising. Below is a measurement over a few minutes with some LYSO crystals next to a 3x3'' NaI:Tl detector biased with -800V (purple) and a background measurement for the same time (green). The upper picture has linear scaling for the counts and labels for the 176Hf lines that originate from the decaying 176Lu as part of the LYSO material. The bottom picture has logarithmic scaling of the counts and shows labels for the two most prominent background lines and some cosmic muon events. A very nice thing about the ToT method is the lack of any practical amplitude limit. This is caused by the logarithmic dependency and the practically unlimited time measurement range (the 30-bit counter overflows only after about 5.3 seconds). For the same reason, calibration of the energy axis is more challenging because the relationship is not linear.

Outlook

There are things to improve, and I hope I can share them soon:
  • with 4 channels, I have the possibility to make more advanced setups employing gamma-gamma coincidences using more than 2 detectors. For example two NaI:Tl detectors and an active LYSO source (a LYSO crystal coupled to a small photomultiplier or silicon-photomultiplier).
  • with more channels there is need for more complex analysis software which I'm currently writing
  • The data with the Mojo board is rather limited. At the moment the total event rate of all 4 channels is about 2.5 kHz. Sending bare time-stamps to the PC is not the most efficient way of using weak data connection. In the future I want to send only time differences to the PC using a variable bit-width. 
  • If the threshold is decreased further (below the ~80 keV as it was in the test measurement) the energy resolution degrades. This is because of the faint slope of the leading edge of the signal at low amplituedes - a basic mathematical property of the exponential decay. This can be improved using a dynamic threshold dynamic threshold

Samstag, 1. Juli 2017

pt-100 temperature logger

Introduction

At work we have this unit to read out pt-100 temperature sensors for monitoring purposes. I was asking myself, if precise temperature logging really needs to be so expensive? I started to design a similar device with the aim of achieving similar performance and functionality.

Pt-100 sensor

Platinum pt-100 temperature sensors are standard devices for temperature measurement with positive temperature coefficient (PTC, resistance increases with temperature) and have 100 Ohm at 0 °C. Using such a sensor requires a precise resistance measurement. This can be done by sending a small current through the sensor and then measuring the voltage drop, but there are a few issues:
  • If current flows through the temperature sensor, there will be power dissipated (P=R*I^2). This causes the sensor to heat up and the resulting temperature measurement will be too high. In order to get precise readings even when there is not much thermal mass coupled to the sensor (for example if air temperature is measured), the current has to be very small. In addition, the current should only be switched on for a short time to take the measurement.
  • Small currents will lead to small voltage drops. A differential amplifier will be necessary in order to fill the dynamic range of the ADC.
  • The commercial unit mentioned in the introduction uses a 24-bit ADC, so I want to use a 24-bit ADC as well.
  • If the sensor is connected to the measurement device through a cable, the cable resistance will add to the sensor resistance and lead to wrong readings. This problem can be overcome by using four wires on the sensor: two for the supply current, and the other two for the voltage reading. This is known as 4-wire measurement. There is also 3-wire measurement, but I'll not support this to keep things simple.
  • The current source for the sensor has to be precise, because current errors will directly lead to errors in the resistance measurement. It may help if the current source is driven by the same reference that is used in the ADC. If that reference is varying (e.g. to higher values), the current will be higher and the measured voltage is higher, which is compensated by the higher reference value. Ideally, ADC and current source are in the same IC.

ADS1248

I found a nice IC that is designed for exactly this application: the ADS1248 from TI. It has four differential programmable gain amplifiers, a 24-bit sigma-delta ADC and two current sources (I only need one). It is controlled via SPI interface. The task is now reduced to interface that part with a microcontroller and a bit of coding. I've chosen the ATMEGA328p as controller and use the VUSB library for USB connectivity to a host computer running a command line tool to control the device.

Design files

The project is a bit more complex. Too much code to include in the blog. I've put the complete project (kicad files, firmware, and command line tool) on github. Feel free to copy, rebuild, improve, ...
https://github.com/miree/tempLoggerNG

Circuit

The circuit has the AVR controller, the ADS1248 and four MOSFETS to switch the current from the source to one of four sensors. For The sensor cables I'll abuse USB mini-B connectors. USB cables have 4 wires (for 4-wire measurements) and the mini-B type is robust, cheap, and small. 

The device

I made the first prototype myself with the toner transfer method
But later I decided to get a professionally manufactured PCB from https://oshpark.com/
Because of the abuse of USB mini-B connectors, it looks a bit like a USB hub... The single USB mini-B connector is for the host PC, the other mini-B connectors are for the sensor cables which need to be equipped with a pt-100 temperature sensor like this:
One lead of the sensor goes to USB-VCC and USB-D+, the other sensor lead goes to USB-GND and USB-D-. This allows a four-wire measurement where the cable resistance drops out. So in principle the cable can be very long and should result in the same measurement as with a short cable.

Calibration

The readings from the ADS1248 chip depend on the temperature of that chip. For really accurate measurements this has to be taken into account. The ADS1248 can measure the voltage drop on an on-board diode junction by connecting it to the ADC. So it is possible to correlate the measured value (of a fixed 100 ohms resistor) with the junction temperature and correct for that. In order to do this I heated up the chip with a heat gun and took some data of the 100 ohm resistor while it was cooling down back to room temperature.
It looks a bit messy but I think the point is clear: The fixed 100 ohm resistor has a lower value, depending on the chip temperature (i.e. the value of V_diode). Using the fitted line, I can correct the measured resistance. Better results might be achieved by using a quadratic correction, but remember that this is the temperature correction of the device. I believe that for values around room temperature (at which the device will be operated most of the time) the linear correction will be fine. The calibration coefficients can be stored into the EEPROM of the ATMEGA328p permanently and needs to be done only once.

Command line tool

Part of the project is a command line tool that communicates with the device using libusb http://libusb.org/
The tool can be used to read temperature value from the device in regular intervals (down to 8 times per second) and display on the terminal as well as saving them to a text file.

A measurement

letting the four probes hanging in the air and touching one sensor after the other with my fingertips results in the following measurement. The sensors react fast because of their low thermal mass. After being touched they approach room temperature again. There is a bit of spread in the different sensors. This is normal because I only have pt-100 of type B, and they are allowed to have a spread in absolute value of ±(0,30°C + 0,005 T).

How cold is my fridge?

How cold is it, and how does it vary with time? With a temperature logger, this question can be answered. I put one of the four sensor cables into the freezer of my fridge. The other three probes are inside the fridge at different locations.
The measurement went over 140 minutes and the compressor switched on two times (~min 40 and ~min 140). The temperature is lowest in the freezer (purple line). The freezer is located in the upper part of the fridge. It seems to be true that the coldest temperature (apart from the freezer) is on the bottom of the fridge (yellow line).

Sonntag, 16. April 2017

gamma spectroscopy #1: sound card gamma-ray coincidence spectroscopy

Introduction

People have built DIY gamma spectrometers and information can be found in the internet:
All these projects use the sound card of a PC as data acquisition system. The "gammaspectacular" is a small device that integrates a high voltage source and some signal conditioning to make the signals suitable for sound card acquisition.
Theremino and PRA are software tools to extract signal amplitudes from the recorded sound data and create a pulse-height histogram.

However, what I didn't see yet is a setup or software support for gamma coincidence spectroscopy. So I decided to make the software and hardware for this and try out if it is possible.

Update: I just saw this forum post: where it was done before.

Gamma radiation

All atoms have a positively charged core, consisting of protons an neutrons, called the atomic nucleus. Electromagnetic radiation emitted by an atomic nucleus is called gamma radiation.
This emission of gamma radiation can happen if a nucleus is in an excited state (e.g. it vibrates, rotates, or has protons/neutrons in higher orbits), and dissipates its excitation energy by releasing an electromagnetic wave quantum - a photon in the gamma energy range. This process is also known as gamma decay of the nucleus. The energy (inverse to its wavelength) of the released photon matches exactly the energy difference between energy of the nucleus before and after the decay. These excitation energies are very specific for each isotope and can therefore be used to identify gamma radioactive isotopes.
How does an atomic nucleus get into an excited state? In our environment it happens all the time if naturally occurring, unstable nuclei decay into a more stable isotope. There are many elements with unstable isotopes. One example is the isotope 40-K that has a natural abundance of 0.0117%. The number 40 refers to the sum of protons and neutrons inside the atomic nucleus. A sample of natural K (Potassium) contains 93.2581% 39-K (19 protons and 20 neutrons), 0.0117% 40-K (19 protons and 21 neutrons), and 6.7302% 41-K (19 protons and 22 neutrons). Only 40-K decays into 40-Ar (18 protons and 22 neutrons) with a half-life of 1.248 trillion years. (this data can be found here http://www.nndc.bnl.gov/)
during the decay of 40-K into 40-Ar energy is released. This energy goes partly into the kinetic energy of the beta particle that is emitted during the decay process, but partially into internal degrees of freedom of the 40-Ar nucleus.
The excited 40-Ar nucleus is this in an excited state and can emit the aforementioned gamma radiation. In this case it has an energy of 1.461 MeV (mega electron volts) which corresponds to a wavelength of 0.00085 nm (visible red light has a wavelength of approx. 650 nm), i.e. gamma radiation is much(!) higher in energy than visible light.

The gamma radiation can be detected in a Radiation detector, e.g. a scintillation detector or liquid nitrogen cooled high-purity Ge detector.
Potassium is a chemical component of concrete, thus gamma radiation from 40-Ar is all around us. But this is not the only energy component of natural radiation as can be seen in the following picture from an approx. 190 hours measurement inside the SPIRAL facility in France using HPGe detectors (I found it online some time ago, but cannot find the link anymore). It shows a pulse-height histogram of the detector signals, and the pulse-height is given in units of keV (kilo electron volts):
This gamma ray spectrum was measured with a high resolution high-purity Germanium detector (HPGe), so a lot of distinct energies can be seen. The most prominent lines come from the decay of 40-K and 208-Tl

Coincident ramma radiation

Some Isotopes decay into excited states which make multiple gamma emissions. One example for this is 176-Lu.

The excited state in 176-Hf decays into another excited state at lower energy, wich again decays into an excited state wich decays into the nuclear ground state. The half-life of all excited states is very(!) short. In the order of picoseconds or nanoseconds. Much shorter than a sound card acquisition system can resolve. For all practical purposes, these decays happen at the same time. If two detectors are used, this information can be used. the acquisition system only considers signals that appeared simultaneously in both detectors, there is a high chance that these two signals actually came from the sample that we want to investigate, and not from some background radiation source. It is very unlikely that a background 40-K decay happens at the exact same time together with the 176-Lu decay.
A common way of showing such data is a coincidence histogram, or coincidence matrix. It is a two dimensional histogram where the simultaneous detection of a pair of signal amplitudes is counted. Each pixel in the picture corresponds to a bin in the histogram, and the height of each bin is encoded in the color. The amplitude of the detectors is in arbitrary units.

Setup summary

The previous picture shows my first recorded gamma coincidence spectrum of some LYSO crystals using a DIY system at home. The sample source was a buch of LYSO scintillation crystals from Ebay. LYSO is a scintillation material that contains 176-Lu. As detectors I used tow 3x3'' NaI:Tl detector assemblies that I scored on Ebay over the years. For the high-voltage I use potted modules from Hamamatsu that I also got from Ebay. The signal shaping is done with high impedance MOSFET source followers that are directly integrated in the PMT sockets together with the voltage divider. From there, the signals are going directly into the microphone input of my PC. Histograms are produced with a small C++ program that uses the alsa API on Linux. My sound card has a stereo sampling rate of 192 kHz. It is essential that the sound card has true stereo recording, otherwise a coincidence setup is impossible.

Detectors

Gamma radiation can be detected using scintillation crystals, which absorb the gamma photon and convert the energy into a large number of photons in the visible spectral range. The quantity of visible light photons is proportional to the energy of the absorbed gamma photon. Typical photon numbers are a few 10000, which is still a very dim flash of light on a macroscopic scale.
A photomultiplier tube (PMT) can be used to detect these tiny amounts of light and convert it into a measurable electrical signal. Scintillation detectors have an energy resolution that is much worse than the one of HPGe detectors (in the room background picture above). But they are much easier to handle, because HPGe detectors need to be cooled to liquid nitrogen temperature to work. Typical resolution of a HPGe detector is ~0.2%, while a NaI:Tl detector can achieve ~7% - more than one order of magnitude worse.
Here are the detector modules that I use for my coincidence experiments. My PMT bases are open, and one has to be careful with the high voltage! I do not recommend to try this at home! If you make your own setup please make sure you know what you're doing. In any case you do this at your own risk!
On the picture the detectors are not connected to anything, but during a measurement it looks the same, only that some coaxial and HV cables are attached, and a radioactive sample is positioned between them. Because of the coincidence setup no shielding is needed to get clean spectra.

PMT base

The wiring of the PMT socket is shown in the following picture, where the numbers refer to the socket pin numbers (they are written on the socket). The resistor values are all identical. I chose 220k for one socket, and 920k for the other socket. They both work fine, so I guess that anything in that range is fine.

Here is a closeup of the source follower amplifier:
The source follower is very simple and also cheap (I would guess below 20 ct. component price), and sufficient for the purpose. Here is the schematic drawing


Note that there is nothing else connected to the PMT anode. The anode is discharged slowly to ground through the high impedance of the source follower input. The high impedance is needed to get the signals long enough to be captured by the relatively slow sampling rate of the sound card.

The voltage divider is a series of same value resistors... the actual value is not so important as long as your HV power supply can handle the current that is created. There are capacitors on the last three stages of the PMT. I didn't try if they are really needed or if it also works without them.

Software

For single detector setup it is the easiest to take one of the existing software tools for spectroscopy (see links in the introduction). These programs do not support coincidence setups, so I had to write my own.
My software is really simple. It continuously takes the digitized signal from the sound card, puts it through a moving average filter with a width of 10 samples. Then, the difference between the momentary filter signal and the delayed filter signal is calculated (delay is again 10 samples). The resulting signal has sharp dips and peaks. Each time a peak in height is reached, the last dip in height is subtracted and the result is taken as pulse height. If both sound card channels see an amplitude at the same time (same sample) the coincidence spectrum is incremented at the position given by the two amplitudes.
The signal processing is visualized (real data) in the following picture


It is very simple but works well enough. The program to read data from the soundcard, do the signal processing and create histograms can be seen below. It is a bit quick and dirty: global variables and not very modular structure and hard coded parameters (such as smaplingrate). I put some comments to make clear what happens. Feel free to copy, use, modify, improve, redistribute, ...
It can be compiled with this makefile:


1
2
3
4
CXXFLAGS = -O -std=c++11
LDFLAGS  = -pthread -lasound
all: soundcard_daq 
soundcard_daq: soundcard_daq.cpp 


source code highlighting by http://hilite.me/

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
#include <iostream>
#include <fstream>
#include <vector>
#include <cstdint>
#include <vector>
#include <cstdint>
#include <pthread.h>
#include <cstdlib>
#include <ctime>
#include <alsa/asoundlib.h>

const int sampling_rate = 192000; // set sampling rate here (in Hz)
//const int sampling_rate =  44100; // set sampling rate here (in Hz)

// store the recorded trace for one second
std::vector<int64_t> trace(sampling_rate,0);
std::vector<int64_t> mwatrace(sampling_rate,0);
std::vector<int64_t> filtertrace(sampling_rate,0);
// histograms for channel 1 and 2, and for the coincidence matrix
std::vector<int32_t> histogram1(8192,0);
std::vector<int32_t> histogram2(8192,0);
std::vector<std::vector<int32_t> > matrix(400,std::vector<int32_t>(400,0));

// Signal detection classes 
class Mwa // moving average window
{
public:
 Mwa(int l) : buffer(l,0), idx(0), sum(0)
 {}
 int64_t filter(int64_t value)
 {
  sum -= buffer[idx];
  buffer[idx] = value;
  sum += value;
  ++idx;
  if (idx >= buffer.size()) idx = 0;
  return sum;
 }
private:
 std::vector<int64_t> buffer;
 int idx;
 int sum;
};
class SignalFilter
{
public:
 SignalFilter(int l, int d, bool showtrace) : mwa1(l), mwa2(l), delay(d,0), idx(0), last_filter_value(0), direction(0), top(0), bottom(0), show_trace(showtrace)
 {}
 int64_t filter(int64_t value)
 {
  // we are recording with 32 bit, so first shift the value down 
  value >>= 16;
  static uint64_t sample_nr = 0;
  int64_t delayed_value = delay[idx];
  delay[idx] = value;
  ++idx;
  if (idx >= delay.size()) idx = 0;

  int64_t mwa1f = mwa1.filter(value);
  int64_t mwa2f = mwa2.filter(delayed_value);
  int64_t filter_value = mwa1f - mwa2f;

  // here we save the first second of the signal
  if (sample_nr < sampling_rate && show_trace)
  {
   trace[sample_nr]       = value;
   mwatrace[sample_nr]    = mwa1f;
   filtertrace[sample_nr] = filter_value;
   ++sample_nr;
  }

  // here is the peak detection and determination of the amplitude
  //   (amplitude = 0 means that no peak was detected)
  int64_t amplitude = 0;
  if (direction == 1) // going up before
  { 
   if (last_filter_value > filter_value) // we are suddenly going down
   {
    top = last_filter_value;
    amplitude = top-bottom;
   }
  }
  if (direction == -1) // going down before
  {
   if (last_filter_value < filter_value) // we are suddenly going up
   {
    bottom = last_filter_value;
   }
  }
  // remember if the signal was going up or down in this step
  if (last_filter_value < filter_value) direction =  1; 
  if (last_filter_value > filter_value) direction = -1; 

  last_filter_value = filter_value;
  return amplitude;
 }
private:
 Mwa mwa1, mwa2;
 std::vector<int64_t> delay;
 int idx;

 int64_t last_filter_value;
 int direction;
 int64_t top, bottom;

 bool show_trace;
};

// This function runs in a never ending thread. It receives the sound data
// in chunks of 32 samples per channel, sendst the data to the signal filters
// and determines peas amplitudes. If a peak amplitude was detected, the 
// value is put into the corresponding histogram.
void *pcm_thread(void* ptr)
{
 std::cerr << "start of data acquisition thread" << std::endl;
 snd_pcm_t               *handle;               // a handle to pcm device
 const snd_pcm_uframes_t  period_length = 32;    // 32 frames in one period
 const unsigned int       rate          = sampling_rate;
 const unsigned int       num_channels  = 2;     // stereo
 // the following buffer used to read out one period
 int32_t                  buffer[num_channels*period_length]; 
 SignalFilter             filter_ch1(10,10, true);
 SignalFilter             filter_ch2(10,10, false);
 int rc;

 // open pcm hardware
 rc = snd_pcm_open(&handle, "default", SND_PCM_STREAM_CAPTURE, 0);
 if (rc < 0) 
 {
  std::cerr << "unable to open pcm device: " << snd_strerror(rc) << "\n";
  return 0;
 }

 // get set of default hardware parameters
 snd_pcm_hw_params_t *params;
 snd_pcm_hw_params_alloca(&params);
 snd_pcm_hw_params_any(handle, params);
 
 // change hardware parameters:
 // Interleaved mode: right left right left right left right left ....
 rc = snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
 if (rc != 0) std::cerr << "error: snd_pcm_hw_params_set_access" << std::endl;
 // signed 32-bit little-endian format
 rc = snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S32_LE);
 if (rc != 0) std::cerr << "error: snd_pcm_hw_params_set_format" << std::endl;
 // two channels (stereo) 
 rc = snd_pcm_hw_params_set_channels(handle, params, 2);
 if (rc != 0) std::cerr << "error: snd_pcm_hw_params_set_channels" << std::endl;
 // set sample rate
 rc = snd_pcm_hw_params_set_rate(handle, params, rate, 0);
 if (rc != 0) std::cerr << "error: snd_pcm_hw_params_set_rate" << std::endl;
 // set period size 
 rc = snd_pcm_hw_params_set_period_size(handle, params, period_length, 0);
 if (rc != 0) std::cerr << "error: snd_pcm_hw_params_set_period_size" << std::endl;

 // write the parameters to the driver 
 rc = snd_pcm_hw_params(handle, params);
 if (rc < 0) 
 {
  std::cerr << "unable to set hw parameters: " << snd_strerror(rc) << "\n";
  return 0;
 }
 
 uint64_t time = 0;
 int samples = 0;
 int counts1 = 0;
 int counts2 = 0;
 int counts12 = 0;

 for(int i = 0;;++i)
 {
  rc = snd_pcm_readi(handle, (char*)buffer, period_length);
  if (rc < 0)
  {
   rc = snd_pcm_recover(handle, rc, 0);
   if (rc < 0)
    exit(1);
  }

  for (int i = 0; i < period_length; ++i)
  {
   ++samples;   
   ++time;
   uint64_t amplitude1 = filter_ch1.filter(buffer[2*i]);
   uint64_t amplitude2 = filter_ch2.filter(buffer[2*i+1]);

   // ratemeter output
   if (samples == sampling_rate)
   {
    std::cout << "ch1-rate | ch2-rate | coincidence-rate : " 
        << counts1 << " | " << counts2 << " | " 
        << counts12 << std::endl;
    samples  = 0;
    counts1  = 0;    
    counts2  = 0;
    counts12 = 0;
   }

   if (amplitude1) // fill histogram1
   { 
    // downscaling of amplitude to better fit in the histogram
    int ampl = amplitude1 / 100; 
    if (ampl > 0 && ampl < histogram1.size())
    {
     ++histogram1[ampl];
    }
    // ratemeter for channel1 (only amplitudes greater 60 are counted)
    if (ampl > 60)
    {
     ++counts1;
    }
   }
   if (amplitude2) // fill histogram2
   { 
    // downscaling of amplitude to better fit in the histogram
    int ampl = amplitude2 / 100;
    if (ampl > 0 && ampl < histogram2.size())
    {
     ++histogram2[ampl];
    }
    // ratemeter for channel1 (only amplitudes greater 60 are counted)
    if (ampl > 60)
    {
     ++counts2;
    }
   }
   if (amplitude1 && amplitude2) // fill coincidence matrix
   {
    // downscaling of amplitude to better fit into the matrix
    int ampl1 = amplitude1 / 1500;
    int ampl2 = amplitude2 / 1500;
    if (ampl1 < matrix.size() && ampl2 < matrix[0].size())
    {
     ++matrix[ampl1][ampl2];
    }
    if (ampl1 > 6 && ampl2 > 6)
    {
     ++counts12;
    }
   }
  }
 }
 std::cerr << "end of thread" << std::endl;
 return 0;
}

int delay_ms(unsigned long millis)
{
 struct timespec ts;
 int err;
 ts.tv_sec = millis / 1000;
 ts.tv_nsec = (millis % 1000) * 1000000L;
 err = nanosleep(&ts, (struct timespec *)NULL);
 return (err);
}

int main()
{
 // Start a never ending thread where the 
 //  recording and signal filtering is done.
 pthread_t recording_thread;
 if (pthread_create(&recording_thread, 0, &pcm_thread, 0))
 {
  std::cerr << "cannot create thread" << std::endl;
 }

 // ouptut updated histograms and coincidence matrix every 10 seconds
 for (int i = 0;;++i)
 {
  delay_ms(10000);
  std::cerr << "updating histograms"<< std::endl;
  std::ofstream histfile("histogram.dat");
  for (int i = 0; i < histogram1.size(); ++i)
   histfile << i << "\t" << ((i>10)?histogram1[i]:0) 
        << "\t" << ((i>10)?histogram2[i]:0)
        << std::endl;
  histfile.close();

  std::ofstream matrixfile("matrix.dat");
  for (int i = 0; i < matrix.size(); ++i)
  {
   for (int j = 0; j < matrix[i].size(); ++j)
   {
    matrixfile << matrix[i][j] <<  " ";
   }
   matrixfile << std::endl;
  }
  matrixfile.close();

  // the signal of channel 1 in the first second of recording
  if (i == 0)
  {
   std::ofstream tracefile("traces.dat");
   for (int i = 0; i < trace.size(); ++i)
    tracefile << i << " " << trace[i] 
            << " " << mwatrace[i] 
            << " " << filtertrace[i] << std::endl;
   tracefile.close();
  }
 }
 return 0;
}



Freitag, 6. Januar 2017

VDHL #1: ghdl setup and rotary encoder simulation

Introduction

There are some challenges for an "ordinary" programmer (like me) to start with VHDL development. This post is not intended to be a VHDL tutorial. In order to learn a language, I will always recommend to read a good book.
Instead, I would like to share some pieces of knowlege that I found important (like "milestones") during my VHDL learning experience. I write this for my past self and present information that I would have found uesful when I started.

In this first VHDL post I want to show a simple, yet not trivial example of VHDL code, presenting the structure of a simple project. I will discuss various aspects of the code in later posts.

Hardware description and simulation

The "HDL" in VHDL means "Hardware Description Language". So it is all about hardware development, which (at least for digital hardware) happens nowadays by creating configuration files for FPGAs (Field Programmable Gate Array) that are mounted on a circuit board. FPGAs are configurable logic ICs, used to create functionality for a special purpose for which no specialized ICs are available. The process of creating a configuration file from VHDL code is called "synthesis".
However, no serious FPGA development is possible without simulating the behavior of the described circuit. The first important point is to recognize that VHDL is a language for hardware description, as well as for the simulation of the described hardware. Only a subset of the VHDL language is useful for hardware synthesis. Many language features are only there to help writing simulations. Typically, a small VHDL project will consist of a synthesizable VHDL "module", and a VHDL "test bench" for this module. The test bench will simulate the environment where the module will be used in once it is sythesized.

Rotary encoder in VHDL

As a simple example, I'll show my implementation of a rotary encoder driver in VHDL. A rotary encoder is a digital input device (a knob) that can be turned left or right in steps. On each step, the two outputs will emit a characteristic wave form (encoder[0] and encoder[1] in the picture below). Both wave forms are similar, but one is ahead in time of the other one. Which one is which depends on the direction of the rotation (left/right). Each rotational step can be detected by driving a state machine with these two signals as input. The states with all transitions are shown in the following picture (high signal level is 1, low signal level is 0).


The special states (R00_emit and L00_emit) are only there to emit a pulse that indicates one step left or right of the encoder. This can be used for example in a counter track the position of the encoder knob. These states are left in the next clock cycle of the state machine (the state machine has a clock; it is a "synchronous" state machine).

GHDL

GHDL is a free VHDL simulator that allows to create hardware designs and simulate the behavior. It does not support hardware synthesis, because FPGA manufacturers usually don't release information to allow the open source community to develop free synthesizer tools. Almost any VHDL project will start as a pure simulation, so this restriction is no problem for now. I will not describe how to install GHDL because it depends on the system you are working on.

The project code consists of three files:
  • moduel.vhd        synthesizable module code
  • module_tb.vhd   test bench code
  • makefile
The makefile is useful to describe the build process of the project: first, the VHDL code is compiled into an executable file. This can be executed and runs a the simulation. The output of the simulation is a file that can be opened with a wave form wiever such as gtkwave.
I have a target "make view" that creates the simulation result and starts gtkwave. The default target creates the simulation result (if the sources were updated, of course) and tells a running instance of gtkwave to reload the wave form file.


%.o: %.vhd
 ghdl -a --ieee=synopsys $<

%.o: %.vhdl
 ghdl -a --ieee=synopsys $<

all: module_tb.ghw

view: module_tb.ghw
 gtkwave module_tb.ghw &

module_tb.ghw: module_tb makefile
 ./module_tb --stop-time=20000ns --wave=module_tb.ghw
 gconftool-2 --type string --set /com.geda.gtkwave/0/reload 0

module_tb.o: module.o

module_tb: module.o module_tb.o
 ghdl -e --ieee=synopsys module_tb

Here is a screen shot of the project in gtkwave.


The module code


library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity module is
  port (
    clk_i     : in  std_logic;
    rst_i     : in  std_logic;
    -- encoder input
    encoder_i : in std_logic_vector(1 downto 0);
    -- encoder output
    left_o    : out std_logic;
    right_o   : out std_logic
  );
end entity;

architecture rtl of module is
  type state_t is (s11,s10,r00,r00_emit,l00,l00_emit,s01,s_unknown);
  signal state, next_state : state_t;
  signal sync_encoder      : std_logic_vector(1 downto 0);
begin
  -- sync. process for state transition, input synchronization
  p_state_switch: process (clk_i)
  begin
    if rising_edge(clk_i) then
      -- synchronous reset
      if rst_i = '1' then 
        state      <= s_unknown;
      end if;
      -- update state
      state <= next_state;
    end if;  
  end process;

  p_sync: process (clk_i)
  begin
    if rising_edge(clk_i) then
      -- synchronize input
      sync_encoder <= encoder_i;
    end if;
  end process;

  -- conditonal assignment for output generation
right_o <= '1' when state = r00_emit else '0'; left_o <= '1' when state = l00_emit else '0'; -- async. process for update of state p_state_analysis: process (state, sync_encoder) begin case state is when s_unknown => if sync_encoder = "11" then next_state <= s11; elsif sync_encoder = "10" then next_state <= s10; elsif sync_encoder = "00" then next_state <= r00; elsif sync_encoder = "01" then next_state <= s01; else next_state <= s_unknown; end if; when s11 => if sync_encoder = "10" then next_state <= s10; elsif sync_encoder = "01" then next_state <= s01; elsif sync_encoder = "11" then next_state <= s11; else next_state <= s_unknown; end if; when s10 => if sync_encoder = "00" then next_state <= r00; elsif sync_encoder = "11" then next_state <= s11; elsif sync_encoder = "10" then next_state <= s10; else next_state <= s_unknown; end if; when r00 => if sync_encoder = "01" then next_state <= r00_emit; elsif sync_encoder = "10" then next_state <= s10; elsif sync_encoder = "00" then next_state <= r00; else next_state <= s_unknown; end if; when r00_emit => next_state <= s01; when l00 => if sync_encoder = "10" then next_state <= l00_emit; elsif sync_encoder = "01" then next_state <= s01; elsif sync_encoder = "00" then next_state <= l00; else next_state <= s_unknown; end if; when l00_emit => next_state <= s10; when s01 => if sync_encoder = "11" then next_state <= s11; elsif sync_encoder = "00" then next_state <= l00; elsif sync_encoder = "01" then next_state <= s01; else next_state <= s_unknown; end if; when others => next_state <= s_unknown; end case; end process; end architecture;

There is one process "p_state_switch" that updates the current state of the state machine for each clock cycle. It also does a synchronous reset.

The most important practical thing I learned here, was the synchronization of the input. The determinism of almost all FPGA configurations depends on all signals being synchronized to the clock of the flip flops in the device. That is why the first thing to do with the input signals "encoder_i[1:0]" is to sample it inside the process "p_sync" and create a synchronized version of it "sync_encoder[1:0]" it was very striking when I synthesized the project into an FPGA without that process (directly using "encoder_i" for the state machine) and see how the system misses steps occasionally. Synchronization is not only needed for external inputs, but also for internal signals that run with a different clock speed.

The reset of the code is for output generation and the state machine transition logic. 

The test bench code

The test bench creates all input signals for the module: a clock signal and the two signals from the rotary encoder. The edges of these input signals are not synchronized to the clock, just like in the real world. It also has a counter process that tracks the number of right and left steps the knob was turned.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity module_tb is
end entity;

architecture simulation of module_tb is
  signal   clk, rst    : std_logic;
  constant clk_period  : time := 20 ns;
  
  signal   a, b        : std_logic; -- simulating the input form quadrature rotary encoder
  signal   left, right : std_logic;
  
  signal   counter     : unsigned(7 downto 0) := (others => '0');
 
begin
  p_clock: process 
  begin
    clk <= '0';
    wait for clk_period / 2;
    clk <= '1';
    wait for clk_period / 2;
  end process;
  
  p_reset: process
  begin
    rst <= '1';
    wait for clk_period * 20;
    rst <= '0';
    wait;
  end process;
  
  p_rot_encoder: process
  begin
    A <= '1';
    B <= '1';
    -- counting up
    for i in 1 to 4 loop
      wait for clk_period * 50;
      A <= '0';
      wait for clk_period * 14.4;
      B <= '0';
      wait for clk_period * 11.4;
      A <= '1';
      wait for clk_period * 7.4;
      B <= '1';
      wait for clk_period * 50;
    end loop;
    -- counting down
    for i in 1 to 4 loop
      wait for clk_period * 50;
      B <= '0';
      wait for clk_period * 6.4;
      A <= '0';
      wait for clk_period * 8.4;
      B <= '1';
      wait for clk_period * 13.4;
      A <= '1';
      wait for clk_period * 50;
    end loop;

  end process;
  
  
  -- instantiate module 
  encoder : entity work.module 
  port map (
    clk_i        => clk,
    rst_i        => rst, 
    encoder_i(0) => a,
    encoder_i(1) => b,
    left_o       => left,
    right_o      => right
  );
  
  p_count: process (clk)
  begin
    if rising_edge(clk) then
      if right = '1' then
        counter <= counter + 1;
      end if;
      if left = '1' then 
        counter <= counter - 1;
      end if;
    end if;
  end process;
  
end architecture;