SX1278 Module breakout board

sx1278breakout
I have been using some SX1278 modules for LoRa experiments. The pin spacing on these modules is 1.27mm – half of the pin spacing found on a breadboard. Interestingly, ribbon cable is exactly the right pitch and I previously managed to solder a length of ribbon cable to a module however, the wiring was prone to fatigue and soon became unreliable. Rummaging around my parts drawer I came across a 24 pin SOP24 to DIP adapter board. This is the sort of board you use with surface mount SSOP chips when you need to work with them on a breadboard. The spacing between pins on the SSOP pins is exactly 1.27mm. These adapter boards are very cheap ( < 10c) and it they can be used with the SX1278 as shown above to make it breadboard compatible

ESP32 and LoRa

ESP32_SX1278Module

Hardware

The ultimate intention of this project is to set up a LoRa to TCP/IP gateway. I decided that the ESP32 would make a good bridging device and found one here for about €6. The LoRa transceivers were obtained from Aliexpress also and are available here. Interfacing with the ESP32 was pretty straightforward as the LoRa module simply connects to its SPI interface. The really tricky part was connecting up to the 1/20 inch pitch connector on the LoRa module.

Software

I decided to develop my own LoRa driver module for the ESP32. Its API is based on the equivalent library for Arduino (I felt it would be easier to deal with just one API). Code was developed using the ESP development environment and is available here

Sender

Every conversation needs at least two parties so a device was needed to send some test data to the ESP32 board. A Seeduino was connected to an SX1278 LoRa module as shown here:
seeduino_lora
The great thing about the Seeduino is that you can switch its I/O to 3.3V which allows you directly connect it to the SX1278 module. The code (which makes use of the Arduino SX1278 library) is as follows:

#include <SPI.h>
#include <LoRa.h>

int counter = 0;

void setup() {
  Serial.begin(9600);
  delay(1000);
  //while (!Serial);

  Serial.println("LoRa Sender");
  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(7, 5, 6); // set CS, reset, IRQ pin
  
  

  if (!LoRa.begin(433123000)) {
    Serial.println("Starting LoRa failed!");
    while (1);
  }
  // The following settings should maximize reliability
  LoRa.setTxPower(20); // going beyond 10 is illegal
  LoRa.setSpreadingFactor(12);
  LoRa.setSignalBandwidth(125000);
  LoRa.setCodingRate4(5);
  pinMode(2,OUTPUT);
  Serial.println("Setup done");

}

void loop() {
  Serial.print("Sending packet: ");
  Serial.println(counter);
  digitalWrite(2,HIGH);
  // send packet
  LoRa.beginPacket();
  LoRa.print("hello ");
  LoRa.print(counter);
  LoRa.endPacket();
  digitalWrite(2,LOW);
  counter++;

  delay(4000);
}

Testing

The main performance measure I was concerned with was range as the data sizes were very small. The Arduino end of the radio link used the coiled quarter wavelength antenna supplied with the module. The ESP32 end used a straight quarter wavelength monopole. Range testing consisted of me walking around the neighborhood with the Arduino end in a bag while talking with my sons over the phone while they watched the ESP32 end. The ESP32 “base station” was located inside a house with foil backed insulation (which didn’t help). The testing was carried out in a suburban area with lots of houses, trees, cars and so on.

Results and discussion

The best range I managed to get was 770m (measured on Google maps). I suspect that this can be improved if I switch to dipole antennas and if I locate outside the foil envelope of the house. Watch this space 🙂

Another low cost dev kit from Aliexpress

stm32f103plusdebugger

STLink V2 clone debugger: $1.79
MCU breakout board : $1.68
Total : $3.47
Delivery time : 2 weeks

This development kit is based around the stm32f103C8T6MCU. This is a 72MHz Cortex M3 device with 64kB of flash memory and 20kB of RAM. The debugger is an ST-Link V2 clone which can be used to debug STM32 and STM8 devices. I used this with OpenOCD and needed to edit/creaqte the configuration files stm32f103_aliexpress.cfg and stm32f1x_64k.cfg (see below for contents of these files). Note: the register that tells OpenOCD the size of system flash incorrectly indicates 128kB so a value of 64kB is forced in the configuration file. The first test program was blinky of course but this time I decided NOT to write the device header file myself but instead make use of ST Microelectronics
System View Description file (SVD file) for this device. This can be downloaded from here: http://www.st.com/resource/en/svd/stm32f1_svd.zip. Contained in the zip file is STM32F103.svd : an XML description of the peripheral registers within the STM32F103. A separate utility called SVDConv.exe was used to convert this “svd” file to a header file suitable for use with GCC. The SVDConv utility was found in Keil’s ARM MDK (http://www2.keil.com/mdk5/). I’m working in Linux so the conversion command was:

wine ./SVDConv.exe STM32F103.svd --generate=header

This produced the header file STM32F103.h. The header uses structure definitions and pointers to structures to access peripherals and the registers within them. It also has a couple of dependencies include/core_cm3.h and include/system_ARMCM3.h. I started down the road of finding these only to realise that these too had further dependencies. Furthermore, they served to hide a lot of the startup code, interrupt vectors and so on that I like to keep an eye on. So, there are two choices: either remove the #include statements for the dependencies or create empty files with those names. I did the latter as it left the STM32F103.h file unchanged. An additional header file of my own (cortexm3.h) is needed to fix up a couple of missing symbols which are defined as shown below (other symbols are also defined):

#define __IO volatile
#define __IM volatile
#define __OM volatile

These symbols refer to Input/Output datatypes which should probably always be volatile – in short it works.
As an experiment, I tried using STM32Cube to generate code for a simple blinky project. First of all, I should say that it worked (more or less – I had to manually edit the output Makefile to point to the directory where arm-none-eabi-gcc was installed). This is probably a great tool for a company that is producing a range of products across different members of the ARM-Cortex family. From a teaching and learning perspective though the generated code is of limited use. It is littered with conditional compiles, helper functions that hide important details, and requires you place your code between various sets of comments. The generated code is also MUCH larger than the version below. In short it obscures the lower levels of the microcontroller that I’m interested in teaching. The approach I’ve taken here is to use the device’s SVD file for the peripherals and my own, simpler, device specific Cortex M3 cpu core file. I also used my own simplified initialization code to show exactly what goes on after reset.

Blinky

/* User LED is on PC13 */
#include <stdint.h>
#include "../include/cortexm3.h"
#include "../include/STM32F103.h"
void delay(uint32_t dly)
{
    while(dly--);
}
int main()
{
    // Turn on GPIO C
    RCC->APB2ENR |= BIT4;
    // Configure PC13 as an output
    GPIOC->CRH |= BIT20;
    GPIOC->CRH &= ~(BIT23 | BIT22 | BIT21);
    while(1)
    {
        GPIOC->ODR |= BIT13;
        delay(1000000);
        GPIOC->ODR &= ~BIT13;
        delay(1000000);
    }
}

This is built with a script file (or batch file) that executes the following commands:

arm-none-eabi-gcc -static -mthumb -g -mcpu=cortex-m3 *.c -T linker_script.ld -o main.elf -nostartfiles 
arm-none-eabi-objcopy -g -O binary main.elf main.bin

The second line of this is not necessary strictly speaking. I use the same script for mbed boards and the “bin” output format is useful there.

Running and debugging

I tried to write out all of the steps in this but decided that a video would be much better. Its over here:

Further examples

A number of other examples are to be found over here https://github.com/fduignan/stm32f103c8t6. These include Systick (at default 8MHz) speed, Systick at 72MHz and UART input/output.

Appendix: OpenOCD configuration files

stm32f103_aliexpress.cfg:

# FILE: stm32f103_aliexpress.cfg
# stm32f103 board and ST-Link v2 from Aliexpress
source [find interface/stlink-v2.cfg]
transport select hla_swd
set WORKAREASIZE 0x2000
source stm32f1x_64k.cfg
reset_config none

stm32f1x_64k.cfg

# FILE: stm32f1x_64k.cfg
# MODIFIED: script for stm32f1x family : forced Flash size to 64kB

#
# stm32 devices support both JTAG and SWD transports.
#
source [find target/swj-dp.tcl]
source [find mem_helper.tcl]

if { [info exists CHIPNAME] } {
   set _CHIPNAME $CHIPNAME
} else {
   set _CHIPNAME stm32f1x
}

set _ENDIAN little

# Work-area is a space in RAM used for flash programming
# By default use 4kB (as found on some STM32F100s)
if { [info exists WORKAREASIZE] } {
   set _WORKAREASIZE $WORKAREASIZE
} else {
   set _WORKAREASIZE 0x1000
}

#jtag scan chain
if { [info exists CPUTAPID] } {
   set _CPUTAPID $CPUTAPID
} else {+
   if { [using_jtag] } {
      # See STM Document RM0008 Section 26.6.3
      set _CPUTAPID 0x3ba00477
   } {
      # this is the SW-DP tap id not the jtag tap id
      set _CPUTAPID 0x1ba01477
   }
}

swj_newdap $_CHIPNAME cpu -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id $_CPUTAPID

if { [info exists BSTAPID] } {
   # FIXME this never gets used to override defaults...
   set _BSTAPID $BSTAPID
} else {
  # See STM Document RM0008
  # Section 29.6.2
  # Low density devices, Rev A
  set _BSTAPID1 0x06412041
  # Medium density devices, Rev A
  set _BSTAPID2 0x06410041
  # Medium density devices, Rev B and Rev Z
  set _BSTAPID3 0x16410041
  set _BSTAPID4 0x06420041
  # High density devices, Rev A
  set _BSTAPID5 0x06414041
  # Connectivity line devices, Rev A and Rev Z
  set _BSTAPID6 0x06418041
  # XL line devices, Rev A
  set _BSTAPID7 0x06430041
  # VL line devices, Rev A and Z In medium-density and high-density value line devices
  set _BSTAPID8 0x06420041
  # VL line devices, Rev A
  set _BSTAPID9 0x06428041
}

if {[using_jtag]} {
 swj_newdap $_CHIPNAME bs -irlen 5 -expected-id $_BSTAPID1 \
	-expected-id $_BSTAPID2 -expected-id $_BSTAPID3 \
	-expected-id $_BSTAPID4 -expected-id $_BSTAPID5 \
	-expected-id $_BSTAPID6 -expected-id $_BSTAPID7 \
	-expected-id $_BSTAPID8 -expected-id $_BSTAPID9
}

set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME cortex_m -endian $_ENDIAN -chain-position $_TARGETNAME

$_TARGETNAME configure -work-area-phys 0x20000000 -work-area-size $_WORKAREASIZE -work-area-backup 0

# flash size will NOT be probed: Force to 64k (error in chip config register)
set _FLASHNAME $_CHIPNAME.flash
flash bank $_FLASHNAME stm32f1x 0x08000000 0x10000 0 0 $_TARGETNAME

# JTAG speed should be <= F_CPU/6. F_CPU after reset is 8MHz, so use F_JTAG = 1MHz
adapter_khz 1000

adapter_nsrst_delay 100
if {[using_jtag]} {
 jtag_ntrst_delay 100
}

reset_config srst_nogate

if {![using_hla]} {
    # if srst is not fitted use SYSRESETREQ to
    # perform a soft reset
    cortex_m reset_config sysresetreq
}

$_TARGETNAME configure -event examine-end {
	# DBGMCU_CR |= DBG_WWDG_STOP | DBG_IWDG_STOP |
	#              DBG_STANDBY | DBG_STOP | DBG_SLEEP
	mmw 0xE0042004 0x00000307 0
}

$_TARGETNAME configure -event trace-config {
	# Set TRACE_IOEN; TRACE_MODE is set to async; when using sync
	# change this value accordingly to configure trace pins
	# assignment
	mmw 0xE0042004 0x00000020 0
}

Weather station part 3: putting it all together

Introduction

The previous two posts on this topic dealt with the BMP180 pressure/temperature sensor and the NRF905 radio transceiver separately. This post brings it all together and shows how energy consumption was reduced to prolong battery life. The original plan was to run this from two solar powered garden lights. These lights come with coin cell size NiMh batteries and a simple charging circuit. After a bit of fiddling I decided to put this to one side having concluded that the internal resistance of the coin cells was quite high resulting in a large voltage dip during transmission. Furthermore the capacity of the batteries is so small (40mAh printed on the case) and the quality so poor that they were unable to power the station through the night. For the moment I have switched to a pair of rechargeable 1300mAh AA batteries that are charged externally. I may revisit the solar power option later and replace the tiny NiMh batteries with some decent AA or AAA ones.

Reducing power consumption

The three main power consuming elements in the weather station are:
(1) BMP180 pressure/temperature sensor module
(2) NRF905 radio transceiver
(3) STM32F030 MCU

According to the datasheet, the BMP180 has an idle current of 5uA and an active current of up to 650uA. This device is mounted on a breakout board which has a voltage regulator (XC6206) with a quiescent current consumption of 3uA. A MHC5983 magnetometer is also on the board with a quiescent current of 2uA and an active current of 100uA. The total quiescent for this board should be the sum of these : around 10uA. Only the BMP180 is used so the peak current should get to around 650uA.
The NRF905 is powered down when not in use. The datashseet states that its current consumption should get down to 2.5uA when powered down, 32uA when in standby and 20mA when transmitting at +6dBm.
The STM32F030 is put in to deep sleep mode between transmissions. The LSI clock and RTC are left running and periodically wake the system up. It is a little difficult to determine what the current consumption should be in this mode. According to the datasheet, the current consumption should be between 2 and 5 uA. The power consumption of the RTC is of the order of 1uA according to ST literature. During active mode at 8MHz with code executing from flash, the current consumption should be around 4mA.
Adding all of the standby currents up we get to about 10 + 2.5 + 5 + 1 = 18.5uA.
Active mode current flow should be dominated by the STM32F030 and the NRF905. I would expect active mode currents of 5mA with no radio transmission and 25mA during transmission.
The main program loop is as follows:

while(1) {            
    Int2String(readTemperature(),&Msg[0]);    
    Msg[10]=',';
    Int2String(readPressure(),&Msg[11]);       
    TxPacket(Msg,0x20);
    low_power_mode(); // Sleep and wait for RTC interrupt
    resume_from_low_power();        
}

This transmits a packet with the following format:
0000000233,0000099804
This is interpreted as a temperature of 23.3 C and a pressure of 998.04 milibars
The RTC is configured to wake system once per minute (the weather does not change that quickly)
Low power mode is entered and exited with the following two functions:

void low_power_mode()
{					
    PwrLow(); // Put NRF905 into low power mode
    // Turn off GPIO B,A and F		
    RCC_AHBENR &= ~(BIT17+BIT18+BIT22);			
    RCC_APB1ENR &= ~BIT21;   // Turn off clock for I2C1
    RCC_APB2ENR &= ~BIT12;   // turn off SPI1 	
    RCC_CFGR |= 0xf0; // drop bus speed by a factor of 512
    cpu_sleep();      // stop cpu
}
void resume_from_low_power()
{	
    RCC_CFGR &= ~0xf0; // speed up to 8MHz		
    // Turn on GPIO B,A and F
    RCC_AHBENR |= BIT18+BIT17+BIT22;
    RCC_APB1ENR |= BIT21;   // Turn on clock for I2C1
    RCC_APB2ENR |= BIT12;   // turn on SPI1 	
    PwrHigh(); // power up the radio
}

Entering low power mode, the code shuts down the NRF905, disables clocks to the I/O port, I2C and SPI peripherals, slows the peripheral bus clock and stops the CPU. Resuming from low power reverses these steps. The CPU can be placed in shallow or deep sleep modes depending upon the setting of bit 2 in the system control register. The code below enables deep sleep mode which stops the CPU clock and powers down flash memory.

    SCR |= BIT2;  // enable Deep Sleep mode

On receipt of an RTC interrupt, the CPU exits sleep mode and resumes execution.

Results

CurrentConsumption

The measured quiescent current was a lot higher than expected; around 170uA @ 3.6V and 90uA at 2.4V. Why is this so much higher? Perhaps passive components on the breakout boards such as pull-down or pull-up resistors are raising the levels. All of the peripherals on the STM32F030 may not be powered off. A current drain of 90uA at 2.4V should allow the station to idle on 1300mAh for more than a year and I felt that this was probably good enough for now. The actual run time is likely to be a lot lower than this due to self discharge in the battery and the energy used during the brief transmission interval. I’ll see how it goes with an extended test.
The active mode current is lower than expected. The MCU current just before transmission is about 3mA. It would seem that the radio is only drawing (19-3) = 16mA during transmission. The datasheet suggests that this level of current should be typically 20mA. During experimentation I found that the current levels during transmission were consistently less than suggested by the datasheet – maybe the datasheet typical values are a little overstated.

Looking at the graph, the 5mA (max) region consists of three approx equal length periods: I2C read of temperature, I2C read of pressure and SPI transfer to NRF905. The NRF905 data packet consists of a preamble (10 bits), address (32 bits), data packet (32 bytes) and a CRC field (16 bits). In total this is 538 bits. The data rate if 50kbps which means the transmission time should be 10.76ms. This agrees with the graph.

Debugging issues

I developed this code using my ST-Link V2 debugger salvaged from a Nucleo board and reflashed with J-Link firmware. OpenOCD and GDB allowed me to debug and download code. A problem arises when the CPU is in a deep sleep : OpenOCD can not talk to it in this mode. It complains and times-out repeatedly. Occasionally, it catches the CPU when its awake allowing you halt it and to download code. If the sleep interval is long (e.g. a minute or more) this can be quite frustrating. There is a way around this however. If you pull the Boot0 pin high and power cycle the MCU it will boot to ISP mode. Openocd & GDB can happily attach to this allowing you to download fresh code. When download is complete, set Boot0 to zero and power cycle once more to run your code. You don’t need to stop OpenOCD or GDB when you do this power cycle (I just pulled a wire from the breadboard and put it back again)

Radio range

The NRF905 radio modules were supplied with some short antennas which seemed too short for 433MHz. I found that replacing them with quarter wave length wires slightly improved signal range. I settled on a power output level of 6dBm as it gave me an (urban) range of about 100m – enough for my needs. The radio module seemed to misbehave at the higher power level of +10dBm. This could be as a result of the breakout board or perhaps poor supply regulation. Running it a +6dBm produced stable results.

Code download

You can download the code for the weather station over here on github: https://github.com/fduignan/Weatherstation1

Weather station part 2: The radio link

Introduction

As mentioned in the previous post I’m hoping to put together a weather monitoring system. It will consist of an outstation in the garden which will be solar powered. The outstation will send data over a radio link to a base station located indoors. This post outlines the radio link code and setup.

The outstation radio wiring

stm32f030cct1
The figure above shows the connections between the outstation MCU, the BMP180 pressure/temperature sensor and the NRF905 radio module. There are lots of connections to the radio module and I’m not sure they are all strictly necessary at the moment but for now they stay.
The NRF905 can transmit at a number of different frequencies. After some trial and error I decided to go with a frequency in the 433 ISM band as it seemed to work best (the NRF905 is on a breakout board which already has inductors and capacitors in the antenna circuit which I suspect were tuned for this band). The actual module is supplied from dx.com (link). Overall I found this module much easier to use than the NRF24L01.
The code for the radio link is available on github over here. This code transmits an counter value once per second at maximum power (10mW). This current consumption while transmittiing is quite high (about 29mA) but this current burst is pretty short lived. Early indications are that the range is sufficient for my purposes though I may fiddle with the antenna later to see how far it can be pushed.
A (messy) prototype is shown in the photo below
Stm32f030Bmp180NRF905
You may notice the ST-Link debugger salvaged from a Nucleo board attached to the target. This debugger was reflashed with the Segger/J-Link firmware and behaved very well.

The base station

The base station consists of an STM32L011 Nucleo board attached to an NRF905 module. A photo is shown below (wiring details in the code only for the moment)
stm32l011nrf905
The Nucleo outputs any data it receives over its built-in USB/Serial converter. This is displayed on the host PC.
One problem arose with the STM32L011’s SPI interface which is covered in its errata sheet. There is a timing problem with the SPI pins and ST provide a simple fix for this (which I found out about after two days of head scratching :). Code for the base station is available here

Next post

The next post will deal with the issue of power consumption on the outstation. This needs to be kept as low as possible if the solar power cells I salvaged from cheap garden LED lights are to work out.

Weather station part 1: Pressure and temperature

Introduction

I’ve decided to try to put together a solar powered wireless weather station this summer. It will consist of two parts: an outstation comprising of a pressure/temperature sensor attached to an STM32F030 MCU; all powered by a cheap solar panel and a rechargeable battery. The MCU will use an NRF905 radio module to send data back to a base station within the house which will display it on screen. Power consumption in the outstation will have to be kept to a minimum.
The base station will consist of an STM32L011 Nucleo board and an NRF905 module. It will interface to the host PC using the built-in UART.

The pressure/temperature sensor

About a year ago I bought a BMP180+HMC5983 module from dx.com. The BMP180 component is a very sensitive atmospheric pressure and temperature sensor with an I2C interface. The HMC5983 is a digital magnetometer may have a future role to play in the weather station.

The outstation MCU

I have previously posted a description of how to mount the STM32F030 TSSOP-20 MCU on a breadboard friendly breakout board. These MCU’s can be configured to consume very little energy. I had several of these lying around so the seemed the logical choice.

Wiring

stm32f030_bmp180
The image above shows the wiring involved. The bmp180 module has built-in pull-up resistors so its quite simple. The code on the other hand is quite complicated. The BMP180 has a very strange conversion routine that takes the sensor outputs and converts them to pressure (in Pascals) and temperature (in degrees C x 10). The measurement results are output via the UART TX pin on PA9. I connected this to the host PC using a USB/Serial converter.
The code can be found here. It includes a lot of debugging code that was useful during development. This is very much version 0.1 as it does not take power consumption into account in any way. That will be the subject of a future post.