2019-05-18

STM32F4 DISCOVERY board and MEMS microphone MP45DT02

This is a repost of a couple of submissions I made on the ST Community site. It seems they are not searchable on the site, nor on Google, so I will republish them here in the hope to make them more visible.
I wanted to configure the I2S peripheral for the microphone MP45DT02 mounted on the STM32F4DISCOVERY board using STM32CubeMX and HAL. When searching for information regarding this I came across a couple of errors in the ST documentation I would like to point out, and at the same time give some hints on how to correctly capture audio data from the microphone. If I have made any mistakes I hope to be corrected.

References:

In [1] the "Data and Frame Format" for the I2S peripheral is set to "16 Bits Data on 32 Bits Frame" which I think is wrong. With "16 Bits Data on 16 Bits Frame" and an audio frequency of 16KHz, the data capturing rate in the peripheral will be 2*16 bits @ 16KHz and the I2S clock will oscillate with 2*16*16000 Hz. When the frame is extended to 32 bits the I2S clock will double its frequency to 2*32*16000 Hz, but the data capturing rate in the peripheral will not change. It is still 2*16 bits @ 16KHz. This means every other 16 bits of data coming from the microphone will be dropped as the pheripheral assumes they are zero. This is not what you want. I decided to use "32 Bits Data on 32 Bits Frame" to get a data aquisition rate of 2*32 bits @ 16KHz, i.e. 64 times oversampling.

This leads us to the next documentation error in [2]. When capturing 16 bits of data per frame, regardless of frame size, the size parameter is the number of 16 bit halfwords that will be captured. So far so good. When capturing 24 or 32 bits of data per frame the size parameter is the number of 32 bit words that will be captured, i.e. twice the number of 16 bit halfwords. This is not clearly expressed in the documentation of the function.

Setting the DMA channel to Circular will automatically restart the capture at the end of the buffer. To be able to work on stable data at the same time as the capturing is ongoing I use both the HAL_I2S_RxHalfCpltCallback() and HAL_I2S_RxCpltCallback(), each working on the part of the capturing buffer that is not currently being filled.
~ ~ ~
I thought I needed DMA double buffering as well and investigated how to get it to work. The HAL DMA module actually supports double buffering, but the HAL I2S module has no function to start a double buffer DMA transfer. I modified HAL_I2S_Receive_DMA() to work with double buffers and got it to work, but then I realized that HAL_I2S_RxHalfCpltCallback() can be used to accomplish the same thing and reverted my changes.

Yes, it is a PDM mono microphone which means you can't use the data as PCM samples directly. The data have to be filtered and decimated 64 times to become 16kHz PCM.
~ ~ ~ 

2019-05-12

Version Numbers

A version number is used to identify different versions, revisions or editions of an artifact. It is common to start to count from 0 and then continue with 1, 2, 3 and so on. No implied meaning should be assigned to any specific number. They are all equally good at identifying a version of the artifact.

In software it is common to track the version of multiple artifacts at the same time using dot notation, e.g. 2.4. The first number tracks the version of the interface and the second tracks the version of the implementation. Semantic Versioning[1] is one example of a versioning scheme that does this.

The first version with a new major number, e.g. 1.0, 2.0 and so on, is not the end goal but the start of something new. If you treat it as the end goal you create a lot of practical problems for yourself. It is likely that 1.0 is less than perfect and you need to release a bugfix. You could call it 2.0 to replace 1.0 as the new perfect version with the high quality moniker .0, but it is better to step the first number only when the interface or other major things changes to not confuse your users. You settle for 1.1. Now what do you call the pre-release version leading up to 2.0? Not 1.2 because that is the next bugfix release of 1.0. And you do need to give those internal pre-release artifacts a version number to be able to relate test results, reviews and other feedback to something traceable. Therefore it is better to start off with version 2.0 and reserve the first few versions in the 2.x series as internal and later release whatever version of the 2.x series that happens to be stable enough to qualify as the first public version.

Sometimes it is necessary to depart from the main line and branch off a separate version of the artifact. An extra number can be added to indicate that it is a patched version, e.g. 2.4.1. This has its limitations though. It is for example not possible to make changes to the interface as the interface version number is fixed. If the branch shall live for a long time it is better to fork the artifact and give it a new name that separates it from the main line. The version numbers of the forked artifact can then be used freely without interfering with the parent.

It is tempting to add meaning to certain version numbers to indicate high quality, approval or review status, but this is not a good idea. The goal to uniquely identify a version of an artifact will come in conflict with the goal to communicate other properties of the artifact. Use a separate mechanism different from the version number for those things. The artifact can for example have a review status of Not Reviewed, Failed Review and Passed Review that is changed or set accordingly, whichever is more suitable. If the review status is carried within the artifact then a change of status results in a new version number. For example, vesion 7 is reviewed and results in version 8 that gets the status Passed Review.

[1] https://semver.org/

2019-02-20

Wifi Remote Control

I have started to automate my home and thought that a dedicated remote control would be useful as an alternative to web pages and phone apps. Pushing a button on the remote performs an HTTP request that in turn controls some equipment at home.

An ESP8266 acts as wifi modem. Out of the box, it implements a serial protocol that is used to control its networking functions. An Arduino Pro Mini is master in the system and is responsible for monitoring the buttons and communicating with the ESP8266 to perform the desired action.

When the system is idle the ESP2866 is turned off and the Arduino is put to sleep. It is woken when a button push generates an interrupt on pin 2 or 3 (interrupt 0 and 1). An internal pull up resistor prevents the interrupt from being triggered when the button is inactive.

#include <avr/sleep.h>

#define GREEN_COLUMN 2
#define RED_COLUMN   3

#define FIRST_ROW   A3
#define SECOND_ROW  A2
#define THIRD_ROW   A1


int activeButton = 0;

void green_isr() { ... }

void red_isr() { ... }

void setup()
{
  pinMode(FIRST_ROW, OUTPUT); 
  pinMode(SECOND_ROW, OUTPUT);
  pinMode(THIRD_ROW, OUTPUT);

  digitalWrite(FIRST_ROW, 0);
  digitalWrite(SECOND_ROW, 0);
  digitalWrite(THIRD_ROW, 0);

  pinMode(GREEN_COLUMN, INPUT_PULLUP);
  pinMode(RED_COLUMN, INPUT_PULLUP);
 

  // Low power mode, no ADC
  ADCSRA &= ~(1<<ADEN);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
 

}

void loop()
{
  if (activeButton == 0) {
    attachInterrupt(0, green_isr, LOW);
    attachInterrupt(1, red_isr, LOW);

    ...
    sleep_cpu();
    ...

  } 
  else {
    detachInterrupt(0);
    detachInterrupt(1);

    ...
  }
}

You may notice that I don't use CLI and SEI before SLEEP which is necessary to make sure I don't miss an interrupt. This would have been required with for example interrupt mode FALLING but with interrupt mode LOW new interrupts keep getting generated and it doesn't matter if one is missed. We need the button to be pushed for longer than one interrupt anyway as we need some time to figure out which button is pressed, which leads us to the next step.

The 6 LED buttons are connected to the Arduino in a 3x2 matrix to share the two interrupt enabled pins, one for each column. By setting the row outputs to 0 one by one, it is possible to figure out which button is currently active.

#define GREEN1 5
#define GREEN2 7
#define GREEN3 9
#define RED1   4
#define RED2   6
#define RED3   8
 


int greens[4] = {0, GREEN1, GREEN2, GREEN3};
int reds[4] = {0, RED1, RED2, RED3};
 

int scanRow = 0;

void green_isr() 
{
  if (scanRow != 0) {
    activeButton = greens[scanRow];
  }
}

void red_isr()
{
  if (scanRow != 0) {
    activeButton = reds[scanRow];
  }
}
 

void loop()
{
    ...
    sleep_cpu();

    digitalWrite(FIRST_ROW, 1);
    digitalWrite(SECOND_ROW, 1);
    digitalWrite(THIRD_ROW, 1);

    scanRow = 1;
    digitalWrite(FIRST_ROW, 0);
    digitalWrite(FIRST_ROW, 1);

    scanRow = 2;
    digitalWrite(SECOND_ROW, 0);
    digitalWrite(SECOND_ROW, 1);

    scanRow = 3;
    digitalWrite(THIRD_ROW, 0);
    digitalWrite(THIRD_ROW, 1);

    scanRow = 0;
    digitalWrite(FIRST_ROW, 0);
    digitalWrite(SECOND_ROW, 0);
    digitalWrite(THIRD_ROW, 0);
    ...
}

The interrupt will be triggered when the correct row output is toggled. This results in an assignment of activeButton and the program moves to the second phase where the ESP8266 is started and instructed to do an HTTP request.

The serial data received from ESP8266 is written to a small circular buffer. Whenever a carriage return ("\r") is detected the buffer is inspected to see if we have received a message that we are interested in. The following messages will trigger an action in the code.

ready
OK
GOT IP
CONNECT
CLOSED
FAIL
ERROR

Whenever "ready" or "OK" is received, a command will be sent to the ESP8266. A normal exchange looks something like this.

< ready\r
> AT+CIPMUX=1\r\n
< OK\r
> AT+CWJAP="SSID","password"\r\n
< GOT IP\r
< OK\r
> AT+CIPMUX=1\r\n
< OK\r
> AT+CIPSTART=0,"TCP","192.168.0.102",8081\r\n
< CONNECT\r
< OK\r
> AT+CIPSEND=0,31\r\n
< OK\r
> GET /on?group=Nere HTTP/1.1\r\n\r\n
< OK\r
< OK\r
> AT+CIPCLOSE\r\n
< CLOSED\r

Note the HTTP GET and the dual CRLF which executes the command on the web server. The second "OK" thereafter is part of the response from the web server.

The messages "CLOSED", "FAIL" and "ERROR" terminates the communication, turns off the ESP8266 and puts the Arduino back in sleep mode. There is also a timeout condition that does the same.

#define ESP32_CS 10
#define ESP32_RST 11
 

unsigned long start = 0;

byte next = 0;
#define BUFSZ 16
byte buffer[BUFSZ] = { 0 };

byte   ready[5] = { 'r', 'e', 'a', 'd', 'y' };
byte   gotip[6] = { 'G', 'O', 'T', ' ', 'I', 'P' };
byte      ok[2] = { 'O', 'K' };
byte connect[7] = { 'C', 'O', 'N', 'N', 'E', 'C', 'T' };
byte  closed[6] = { 'C', 'L', 'O', 'S', 'E', 'D' };
byte    fail[4] = { 'F', 'A', 'I', 'L' };
byte   error[5] = { 'E', 'R', 'R', 'O', 'R' };

byte cmd = 0;


void setup()
{
  Serial.begin(115200);


  pinMode(ESP32_CS, OUTPUT);
  pinMode(ESP32_RST, OUTPUT);
  ...

}

void allOff()
{
  digitalWrite(GREEN1, 1);
  digitalWrite(GREEN2, 1);
  digitalWrite(GREEN3, 1);
  digitalWrite(RED1, 1);
  digitalWrite(RED2, 1);
  digitalWrite(RED3, 1);
}

bool received(byte* msg, byte n)
{
  int i;
  for (i = 0; i < n; i++) {
    if (buffer[ ( BUFSZ + next - (n+1) + i ) % BUFSZ ] != msg[i]) {
      return 0;
    }
  }
  return 1;
}

bool activeIsGreen()
{
  return activeButton == GREEN1 || activeButton == GREEN2 || activeButton == GREEN3;
}


void loop()
{
  if (activeButton == 0) {
    ...
    digitalWrite(ESP32_CS, 0);
    digitalWrite(ESP32_RST, 0);

    ...
    start = millis();
  }
  else {

    ...
    digitalWrite(ESP32_CS, 1);
    digitalWrite(ESP32_RST, 1);

    digitalWrite(activeButton, 0);

    if (Serial.available())
    {
      buffer[ ( next++ ) % BUFSZ ] = Serial.read();
      if (buffer[ ( BUFSZ + next-1 ) % BUFSZ ] == '\r') {
        if (received(ok, sizeof(ok))) {
          if (cmd == 1) {
            Serial.print("AT+CWJAP=\"SSID\",\"password\"\r\n");
          }
          else if (cmd == 2) {
            cmd = 3;
            Serial.print("AT+CIPMUX=1\r\n");
          }
          else if (cmd == 3) {
            Serial.print("AT+CIPSTART=0,\"TCP\",\"192.168.0.102\",8081\r\n");
          }
          else if (cmd == 4) {
            cmd = 5;
            if (activeIsGreen()) {
              Serial.print("AT+CIPSEND=0,31\r\n");
            }
            else {
              Serial.print("AT+CIPSEND=0,32\r\n");
            }
          }
          else if (cmd == 5) {
            cmd = 6;
            if (activeIsGreen()) {
              Serial.print("GET /on?group=Nere HTTP/1.1\r\n\r\n");
            }
            else {
              Serial.print("GET /off?group=Nere HTTP/1.1\r\n\r\n");
            }
          }
          else if (cmd == 6) {
            cmd = 7;
            // Wait for HTTP GET 200 OK
          }
          else if (cmd == 7) {
            cmd = 8;
            Serial.print("AT+CIPCLOSE\r\n");
          }
        }
        else if (received(ready, sizeof(ready))) {
          cmd = 1;
          digitalWrite(activeIsGreen() ? RED1 : GREEN1, 0);
          Serial.print("AT+CIPMUX=1\r\n");
        }
        else if (received(gotip, sizeof(gotip))) {
          cmd = 2;
          digitalWrite(activeIsGreen() ? RED2 : GREEN2, 0);
          start = millis();
        }
        else if (received(connect, sizeof(connect))) {
          cmd = 4;
          digitalWrite(activeIsGreen() ? RED3 : GREEN3, 0);
          start = millis();
        }
        else if (received(closed, sizeof(closed)) ||
                 received(fail, sizeof(fail)) ||
                 received(error, sizeof(error))) {
          cmd = 0;
          allOff();
          activeButton = 0;

        }
      }
    }
    else if (millis() - start > 8000UL) {
      cmd = 0;
      allOff();
      activeButton = 0;

    }     
  }

}

Worth noting here is the timeout expression "millis() - start > 8000UL" which always works, especially when the number of millseconds rolls over to zero. A slightly different expression like "millis() > start + 8000UL" does not work as expected close to the rollover point.

As you can see, I have the same action on all green and red buttons, but this can easily be extended with a couple of if statements where the command is sent.

The three LED buttons of the opposite color of the one being pushed are used as a tiny progress bar.

To reduce the power consumption in idle mode I removed the power indicator LEDs from the Arduino and the ESP8266.

When debugging the serial communication I used my PC and the program socat to print a trace of the communication between ESP8266 and Arduino. I used two FTDI USB to Serial cables connected to the Arduino and an adapter board for the ESP8266 which besides the connector for FTDI also contains a 3.3V regulator and voltage dividers on RST, CS and RXT to allow it to be driven from the 5V Arduino. It is visible in the image, here connected directly to the Arduino.

$ socat -v /dev/ttyUSB0 /dev/ttyUSB1

It is highly recommended as a debugging tool.

2018-07-20

PM1 - Pro Memoria - For Memory

The written word is a powerful tool for knowledge transfer to colleagues and future self. Still, documentation is often neglected in favor of more tangible things like new products and features. One culprit could be an overly complicated process for writing and maintaining documents. Revising large documents and coordinate contributions from many authors can be daunting.The goal of the PM system described here is to simplify the task to share product designs, processes, utility guides, tool descriptions and ways of working within an organisation.

A PM is typically fairly short, like a blog post. It should be possible for one person to write a PM in a day or a few hours. Multiple PMs are required to describe all aspects of a large system.

A PM is assigned an unique identity, e.g. PM1, PM2, PM3 and so on.

A PM can be revised if it contains errors or needs to be improved. The first revision of PM27 is called PM27.0, the second is called PM27.1 and so on. (See also Version Numbers).

If the thing being described has changed then a new PM should be written. Old things will come back to haunt you and then it is good to have the documentation readily available. The underlying philosophy here is that a PM should be a stable value that doesn't change with the times. Let's assume you have described the build process for you product in a PM. Release 5 of the product has a new build system. You should keep the original PM and clarify that it covers product releases 1 to 4 and write a new PM for product release 5. (See also The Value of Values). The information that is allowed to continuously grow in a PM is the set of relations it has to other PMs.

A Wiki could be a suitable tool for your PMs.

If you need to organize related PMs in a larger structure, then do that in a PM. You are allowed to append to such a structural PM, but preferably not replace referenced PMs with superceeding ones. If this becomes necessary, then create a new structural PM to maintain the history intact. Don't start out by creating a lot of structural PMs with no content. Let the structure grow organically when you have PMs with actual content to organize.

For practical purposes it may also be necessary to keep a volatile list of the most up to date PMs in a well known location, e.g. the front page of a Wiki.

An alternative to the structural PM is to continuously revise PMs to contain cross references to related, superceeding and superceeded PMs.

2017-07-05

RF Temperature Sensor

This is the description of a sensor board that measures the temperature and transmits it over 433.92MHz radio. It uses the protocol for the weather1 sensor in pimatic/rfcontroljs [1]. Four separate temperature sensors can be connected to the same board.

Components

Arduino Pro Mini 5V [2]
TX433N Transmitter [3]
KTY84/150 Temperature Sensor [4]
1 kOhm Resistor
Coil Loaded Antenna [5]
9V Battery Connector
2x 1 MOhm Resistor

Assembly

The Arduino is the backbone of the construction. Connect the battery connector to RAW and GND.

Analog 4 is used to monitor the battery health. The two 1 MOhm resistors form a voltage divider to get below 5V on the analog input. Connect one resistor between RAW and Analog 4. Connect the other resistor between Analog 4 and GND.

The KTY84 and the 1 kOhm resistor forms a voltage divider. KTY84 connects to GND and one of Analog 0, 1, 2 or 3. The resistor connects to the same Analog and VCC.

TX433N is powered by VCC and GND on the Arduino. Connect TX433N Data to Digital 2.

Connect the coil loaded antenna to Ant.

Configuration

The board is configured with messages sent on the serial port (115200 bps). The following values can be set.

X0: Exact resistance of the 1 kOhm Resistor
X1: Exact resistance of KTY84 at zero degrees Celsius
02: Id 0-255 (weather1)
03: Measurement Interval 0-32767 (ms)
04: Low battery warning level 0-1023

where X selects one of the analog inputs 0, 1, 2 or 3. Each sensor transmit on separate weather1 channels 1, 2, 3 and 4. All sensors have the same id and measurement interval.

Example: To set the analog in 3 KTY84 zero degree resistance to 509 Ohm, send the following message, ending with newline.

31:509

The temperature measured by this KTY84 will transmit on channel 4.

Calibration

To calibrate the sensor, first measure the resistance of the 1 kOhm resistor and write the value to config X0. Then adjust config X1 until the measured temperature matches the value of a reference temperature. The sensor is disabled when X0 is zero.

References

[1] https://github.com/pimatic/rfcontroljs
[2] https://www.arduino.cc/en/Main/ArduinoBoardProMini
[3] https://www.velleman.eu/products/view/?id=350619
[4] http://www.nxp.com/docs/en/data-sheet/KTY84_SER.pdf
[5] http://www.instructables.com/id/433-MHz-Coil-loaded-antenna

Source Code

/*
  Copyright (C) 2017  Marcus Andersson

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see .
*/

#include "EEPROM.h"

// Digital
int ledPin = 13;
int txPin = 2;

// Analog
int thermoPin[4] = {0, 1, 2, 3};

// Pulse period time, 10 microsecond steps
#define PT 48

// Number of config values
#define NCONFIG 32

// EEPROM stored configuration data
struct Config {
  int data[NCONFIG];
};
Config config;

#define BUFLEN 100
byte packet[BUFLEN];
byte length = 0;

void printConfig() {
  for (int i = 0; i < NCONFIG; i++) {
    Serial.print(i);
    Serial.print(": ");
    Serial.println(config.data[i]);
  }
}

void setConfig(int i, int val) {
  config.data[i] = val;
  EEPROM.put(0, config);
}

void setup()
{
  pinMode(ledPin, OUTPUT);
  pinMode(txPin, OUTPUT);
  Serial.begin(115200);
  EEPROM.get(0, config);
  printConfig();
}

void send(int temp, byte id, byte ch) {
  int i, j;
  unsigned long t0;
  unsigned long periods;
  byte data[36] = {
    0, 1, 0, 1,
    1, 1, 0, 1, 0, 0, 0, 0, // id
    1, 1, // batt
    0, 1, // channel
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // temp
    0, 0, 0, 0, 0, 0, 0, 0 // hum
  }; 
 
  // Write id
  for (i = 0; i < 8; i++) {
    data[4+i] = (id >> (7-i)) & 1;
  }
 
  // Write channel
  data[14] = (ch >> 1) & 1;
  data[15] = (ch >> 0) & 1;
 
  // Write temp
  data[16] = (temp < 0) ? 1 : 0;
  for (i = 1; i < 12; i++) {
    data[16+i] = (temp >> (11-i)) & 1;
  }
 
  digitalWrite(ledPin, HIGH);   // sets the LED on

  periods = 0;
  t0 = micros();
 
  digitalWrite(txPin, HIGH); periods += 8;
  delayMicroseconds(max(10000, t0 + periods*PT - micros()));
  digitalWrite(txPin, LOW); periods += 230;
  delayMicroseconds(max(10000, t0 + periods*PT - micros()));

  for (j = 0; j < 4; j++) {
    for (i = 0; i < 36; i++) {
      digitalWrite(txPin, HIGH); periods += 8;
      delayMicroseconds(max(10000, t0 + periods*PT - micros()));
      digitalWrite(txPin, LOW); periods += 4*10 + 4*11*data[i];
      delayMicroseconds(max(10000, t0 + periods*PT - micros()));
    }

    digitalWrite(txPin, HIGH); periods += 8;
    delayMicroseconds(max(10000, t0 + periods*PT - micros()));
    digitalWrite(txPin, LOW); periods += 230;
    delayMicroseconds(max(10000, t0 + periods*PT - micros()));
  }
  digitalWrite(ledPin, LOW);    // sets the LED off
}

void loop()
{
  static unsigned long sendTime = millis() - config.data[3];
 
  if (millis() - sendTime > config.data[3]) {
    sendTime += config.data[3];
    for (long ch = 0; ch < 4; ch++) {
      long r0 = config.data[ch*10 + 0];
      long r1 = config.data[ch*10 + 1];
      long id = config.data[2];
      long s = analogRead(thermoPin[ch]);
      long r = r0*s/(1023-s);
      int t = (r-r1)*2;
     
      if (r0 == 0) {
        continue;
      }
     
      send(t, id, ch);
   
      Serial.print(ch);
      Serial.print(" ");
      Serial.print(s);
      Serial.print(" ");
      Serial.println(t);
    }
  }

  while (Serial.available() > 0) {
    int d = Serial.read();
    packet[length++] = d;
    if (d == 10 || length >= BUFLEN) {
      int p = 0;
      int i = packet[p++] - '0';
      i = i*10 + packet[p++] - '0';
      if (i >= 0 && i < NCONFIG) {
        int v = 0;
        int d = packet[p++];
        d = packet[p++];
        while (d >= '0' && d <= '9') {
          v = v*10 + d - '0';
          d = packet[p++];
        }
        setConfig(i, v);
        printConfig();
      }
      length = 0;
    }
  } 
}


2017-06-26

ASK Baseband Decoder (433.92MHz)

/*
ASK Baseband Decoder  Copyright (C) 2017  Marcus Andersson

This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions; GPLv3.

This is a proof of concept 433MHz ASK baseband decoder for messages sent by
remote controls, weather stations, car keys and so on. It is written for Arduino,
but the algorithm can run on anything that has GPIO IN with interrupt and a
microsecond time function.

You need a 433Mhz receiver. There are plenty of cheap receivers on the market [1].
A good, compact antenna that I recommend is the DIY coil loaded antenna [2].

|   Receiver       Arduino
8  ----------     -----------
+--|Ant Data|-----|D2     TX|--- Serial Out
   |      V+|-----|5V    Raw|--- 5-12V
   |      V-|-----|Gnd   Gnd|--- Gnd
   ----------     -----------

Connect the data out from the 433MHz receiver to digital 2 on Arduino. Upload
this program using the Arduino IDE. Open a serial terminal set to 115200 bps
to start message reception. The output format is similar to homeduino [3],
with a list of microsecond intervals followed by the message which consists
of indexes referencing the list.

Without an ongoing transmission, the receiver will pick up noise. We want
to ignore noise and only detect proper messages. A proper message consists
of a sequence of high/low signal pairs. The signals varies between 1 to N
periods in length. A period is around 300 to 600 microseconds.

1_  _
0_ | |_.       = 11

1_  _
0_ | |_._.     = 12

1_  _._
0_ |   |_._._. = 23

...and so on.

A low signal that is longer than N periods is a sync. The high signal sets
the period time for the message. A sync is sent before and after a message.
The sync signal can be shared by two adjacent messages, which means it marks
both the end of the first message and the start of the next.

1_  _
0_ | |_._._._._._._._._._._._._._._._._._._._._._. = sync

When a sync signal is detected the message recording starts. As long as no signal
has a shorter duration than half a period, the reception continues until a new sync
signal is detected. There is a minimum length for a proper message and there is
also a minimum period time. This lowers the risk of interpreting noise as proper messages.

Incoming messages are written to a circular buffer by the interrupt routine. If
the reception buffer becomes full, the message being received is discarded. When
a complete message has been received the writer index is advanced to the position
after the message and the main loop can start to consume the message using the reader
index. The first datum in the buffer is the period time in microseconds. The following
data is the number of periods for all signals. The main loop transmits the message over
the serial port until (reader == writer) or until the number of periods of a datum is
larger than N, which means that a new message starts.

References
----------
[1] https://www.electrokit.com/rx433n-mottagarmodul-ask-433-9-mhz.45095
[2] https://arduinodiy.wordpress.com/2015/07/25/coil-loaded-433-mhz-antenna
[3] https://github.com/pimatic/homeduino
*/

// Circular buffer length
// Must be 256 for modulo arithmetic to work on byte index variables without using %.
#define BUF_LEN 256

// Noise filter
#define MIN_MSG_LEN 16

// Max length of data signal. Longer signals are treated as sync.
#define MAX_SIGNAL_PERIODS 20

// Minimum signal period time for a proper message
#define MIN_PERIOD_TIME 30 // * 4 microseconds

// Remembers the time of the last interrupt
volatile unsigned int lastTime;

// Signal period time of message being received
volatile unsigned int periodTime;

// Signal counter for message being received
volatile byte streak;

// Buffer pointer where the next message will be stored
volatile byte writer;

// Buffer pointer for the main loop reader
volatile byte reader;

// Circular message buffer
volatile unsigned int msgbuf[BUF_LEN];

void setup()
{
  Serial.begin(115200);
  // Scale down time by 4 to fit in 16 bit unsigned int
  lastTime = micros() / 4;
  periodTime = 1;
  writer = 0;
  reader = 0;
  streak = 0;
  pinMode(2, INPUT_PULLUP);
  attachInterrupt(0, isr, CHANGE);
}

void writeNum(unsigned long num, char* tail)
{
  char buf[10];
  String( num ).toCharArray(buf, 10);
  Serial.write(buf);
  Serial.write(tail);
}

unsigned int insertSort(unsigned int list[8], unsigned int val)
{
  byte i;
  for (i = 0; i < 8; i++) {
    if (list[i] == val) {
      // No duplicates
      val = list[7];
      break;
    }
    if (list[i] > val) {
      unsigned int tmp = list[i];
      list[i] = val;
      val = tmp;
    }
  }
  return val;
}

byte listpos(unsigned int list[8], unsigned int val)
{
  byte i;
  for (i = 0; i < 8; i++) {
    if (list[i] == val) {
      break;
    }
  }
  return i;
}

void loop()
{
  while (reader != writer) {
    unsigned int periodMap[8] = {-1, -1, -1, -1, -1, -1, -1, -1};
    unsigned int pt = msgbuf[reader++];
    byte prereader = reader;
    byte i;

    while (prereader != writer) {
      unsigned int periods = msgbuf[prereader++];
      if (insertSort(periodMap, periods) != -1) {
        // Too many different signals
        reader = writer;
        break;
      }
      if (periods > MAX_SIGNAL_PERIODS) {
        // End of message
        break;
      }
    }
  
    if (reader != writer) {
      for (i = 0; i < 8; i++) {
        if (periodMap[i] != -1) {
          writeNum(periodMap[i]*pt*4, " ");
        }
        else {
          writeNum(0, " ");
        }
      }
    }
  
    while (reader != writer) {
      unsigned int periods = msgbuf[reader++];
      writeNum(listpos(periodMap, periods), "");
      if (periods > MAX_SIGNAL_PERIODS) {
        // End of message
        Serial.write("\n");
        break;
      }
    }
  }
}

void isr()
{
  // Scale down time by 4 to fit in 16 bit unsigned int
  unsigned int now = micros() / 4;
  unsigned int signalTime = now - lastTime;
  unsigned int periods = (signalTime + periodTime/2) / periodTime;
  byte lowSignal = digitalRead(2);

  lastTime = now;
 
  if (periods == 0) {
    // Noise, ignore message
    streak = 0;
  }
  if (streak > 0) {
    // Receive message
    byte index = (writer + streak++); // % 256
    if (index == reader) {
      // Reception buffer is full, drop message
      streak = 0;
    }
    else {
      msgbuf[index] = periods;
    }
  }

  if (lowSignal) {
    if (periodTime > MIN_PERIOD_TIME && periods > MAX_SIGNAL_PERIODS) {
      // Sync detected
      if (streak > MIN_MSG_LEN) {
        // Message complete
        msgbuf[writer] = periodTime;
        writer = (writer + streak); // % 256
      }
      // Start new message
      streak = 1;
    }
  }
  else {
    // high signal
    if (periods > MAX_SIGNAL_PERIODS) {
      // Noise, ignore message
      streak = 0;
    }
    if (streak > 0) {
      if (periods == 1) {
        // Approximate average of single period high signals in message
        periodTime = (periodTime*streak + 2*signalTime) / (streak + 2);
      }
    }
    else {
      // Initiate search for new period time and sync
      periodTime = signalTime;
    }
  }
}

2017-04-15

Power Logger

The power meter I have in my basement has a LED that blinks once every Wh. With a consuption of 3600W it will blink once every second. By logging the clock time at every blink it is possible to get a good overview of the consuption, both momentarily and over time.

The logger I have built is based on the Raspberry Pi Zero W. The LED sensor is based on a photo resistor and an opamp. It is connected to GPIO on the Raspberry Pi. An optional 16x2 LCD display is used to display the IP address of the logger and the current power consuption. A button is used to turn on the LCD backlight. Blink timestamps are logged to file and the data can be viewed in a Web GUI.

Mechanical Design

There is not much to say about the mechanical design. The LCD is attached to the RPi with a single M4 bolt and nuts. The logger and the sensor is attached with blu-tac to the power meter. The photo resistor have long legs that are bendable and can be used to finetune its position in front of the LED.

Electrical Design

The LED sensor is designed using two GL5528 photo resistors, one LM324N operational amplifier and one 4.7kOhm potentiometer. Two photo resistors make the circuit more tolerant to varying ambient lighting conditions. The photo resistors form two voltage dividers with the left and the right side of the potentiometer. The center of each divider is connected to the two inputs, plus and minus, of the opamp. More light on a photo resistor will increase the voltage to the opamp. When the plus voltage is larger than the minus voltage the output of the opamp will go high. The circuit has two modes of operation. Use the photo resistor connected to the plus side to get a high signal when the LED blinks and use the photo resistor connected to the the minus side to get a low signal when the LED blinks. If you choose the positive option then you should use the plus side photo resistor to detect the LED and adjust the potentiometer to get a low signal when the LED is dark. A 1kOhm resistor in parallel with the minus photo resistor prevents it from activating the opamp in darkness when it goes towards infinite resistance. The circuit is powered with 5V from the RPi. The output of the opamp is connected to RPi GPIO 10.


The 16x2 LCD is powered by 5V from the RPi and connected to GPIO in the following way.

RS - GPIO 25
EN - GPIO 24
D4 - GPIO 23
D5 - GPIO 17
D6 - GPIO 21
D7 - GPIO 22
BACKLIGHT (K) - GPIO 4

More information about LCD and RPi can be found at [1].

A switch is connected to GPIO 16 and GND to be used to turn on LCD backlight.

Software Design

See the GitHub project [2].

References

[1] https://learn.adafruit.com/drive-a-16x2-lcd-directly-with-a-raspberry-pi/wiring
[2] https://github.com/TheOtherMarcus/PowerLogger

STM32F4 DISCOVERY board and MEMS microphone MP45DT02

This is a repost of a couple of submissions I made on the ST Community site. It seems they are not searchable on the site, nor on Google, so...