Anatomy of a weather display

First let’s talk about the goal of the weather display. There are web services out there that you can call that will give you information about the weather in your area. No need to buy outside devices that for some reason seem to always go off line when you need them. Also I want to be able to read the display in the dark. Those inexpensive units are low powered LCD devices that are only readable in the light. I get up early in the morning before the sun shines and want to be able to just see the weather.

The display I am using for this project is from Adafruit 3.5inch TFT 320x480 Display. It is run by the HX8357 display controller. I was not familiar with this controller but Adafruit does a good job of building drivers for this display so I was not concerned about writing a driver for the Propeller.

First off the display can be driven in two ways. Either SPI or 8080 8 bit parallel mode. I chose the SPI mode since I was not looking for large display updates with graphics data. I only wanted to display the weather information.

Speaking of the weather display let’s talk about what I want to display:
1. Current temperature.
2. Humidity.
3. Wind and direction.
4. Weather conditions.
5. Inside Temperature.
6. Inside Humidity.
7. Sunset.

With this display when you power it up you get nothing. No lights no LED’s nothing so you don’t know if you have it connected correctly or not. So I tested the display with Adafruit’s driver and graphics library using an STM32L433 board. This board can be programmed using Arduinio so it was simple enough to setup. Just hookup the 9 wires from the display unit to the STM board. Wow that’s a lot of wires, I thought I was using SPI that only required 4 wires. Well you need to hookup these pins.
1. GND
2. 3-5V
3. CLK
4. MISO
5. MOSI
6. CS
7. D/C
8. RST
9. Lite

I found that you need to hookup Reset and Lite if you want to get the display to work properly and you want to control how bright the screen is to conserve some power.

With the STM board the display worked well and the graphics test was very responsive and looked like it would work just fine for what I was going to use it for.

Now I need to build a library to drive the display from the Propeller. Since I was writing this in C and not C++ I had to rewrite all the display code to make it work with the Propeller. I patterned the library after some of the other libraries that were created for the Propeller OLED displays. They had a lot of the basic functions to clear the display, write characters and draw lines.

After several trial and errors I got the display to come up and turn on. The startup code for this display is extensive. At first look the display had garbage on it and I didn’t know if this was normal or not. Now I needed to figure out how to clear the display. This display has only one display buffer and so as you write to the display you can see the pixels changing. To write to this display you have to give it a command that tells it what area on the screen you want to write to. This area is defined as a rectangle so you can just send all the data for that rectangle out and it just fills that area in with that data. This display does not have a clear screen command. So to clear the screen you define the entire screen, 320x480 as your area and send 153,600 words or 307,200 bytes with the color Black. This is because the display is in 16 bit mode, 5 bits Red, 6 bits Green, and 5 bits Blue. This gives you 65 thousand colors.

My first attempt at clearing the screen was successful but took 31 seconds to complete. Not very good. I apparently have an SPI performance problem with the Propeller code. With C code you have two options when it comes to building your code. CMM which is compact memory mode and LMM which is large memory mode. It’s not really large memory mode but just compiled code and runs at the processor speed which is the fastest you can get. For this setup I was using the LMM mode so I already had the display run as fast as it was going to go. At this point the display was not usable taking that long to just clear the screen. I had to look into the code used to drive the SPI interface.

With the Propeller chip you can capture the clock ticks and use it to determine the amount time used to perform a function. This told me how many instruction cycles it took to drive the display. My first test was just see how long it took to send it a command and return a display register. The tick count was 20,046. This is the time it took to lower DC, lower CS, and write 8 bits out and read 8 bits back in plus raise CS and DC. That’s a lot of ticks to do almost nothing.

It’s time to cut out some of the fat. The SPI function is generic in nature not knowing what you’re going to do with the function. It allowed sending the data with the highest bit first or sending it with the lowest bit first. This is determined every time the function is called. I also had to pass in the MOSI pin number and CLK pin. More wasted time.

Here is a copy of the shift out function:
void shift_out(int pinDat, int pinClk, int mode, int bits, int value)
{
  int vi, vf, inc;
  set_direction(pinDat, 1);
  if(mode == LSBFIRST)
  {
    vi = 0;
    vf = bits;
    inc = 1;
  }
  else
  {
    vi = bits - 1;
    vf = -1;
    inc = -1;
  }
  low(pinClk);
  int i;
  for(i = vi; i != vf; i += inc)
  {
    set_output(pinDat, (value >> i) & 1);
    toggle(pinClk);
    toggle(pinClk);
  }
}
Fortunately this is a simple protocol to do. Here is the updated function I came up with to replace it.
void __attribute__((fcache))spi_out(int bits, int value)
{
  unsigned long b;
  int i;
  
  b = 1 << (bits - 1);
  
  for(i = 0; i < bits; i++)
  {
    if ((value & b) != 0)
      OUTA |= _DMask;
    else
      OUTA &= _Dmask;
    OUTA &= _Cmask;
    OUTA |= _CMask;
    OUTA &= _Cmask;
    b = b >> 1;
  }
}
I know there’s some junk on the front of the function. This was added later because the LMM model used up the 32k of memory on the Propeller so the end program code did not fit. So I added the junk in the front so that I could use the CMM memory model and that junk makes it run as LMM code. That way I don’t lose all the speed I just picked up. I also removed the passing of the CLK, MOSI pins as they are known when the driver is started up and of course the Mode pin since that is also known.

With those changes my tick count is now 12,688 which is about 50% less ticks than I had before. Now let’s add this code to the clear screen function and see what we get. It took 2.8 seconds to clear the screen. Now were cooking with gas.

I still have one problem though. When writing characters to the screen it is still slow. I can see it paint the letters from top to bottom. For the character functions I use the plot function and only fill in one pixel at a time. After looking at the code I noticed that I use the command to tell it what rectangle area a want to write to. These means that I set one pixel for my rectangle and then paint it and so on. This is a waste of time. Since each character is in fact a rectangle I could just tell it this area ahead of time and then send it all the pixels for that area. That should save me 50% right there. So that was my next improvement.

With all these improvements in place the display was ready. Now it’s time to work on the inside sensor to read temperature and humidity that will be displayed. I like to use the BME280 sensor. Adafruit has a breakout board that uses this chip and I have used it in the past for an outside weather station.

The Bosch BME280 sensor is a little complicated to get setup. There is some major math going on in this sensor that wants floating point, but Bosch offers some integer math that will work as well. So even though temperature and humidity should be a no brainer with this chip is a little more complicated.

The Bosch sensor has several calibration registers that are used to calculate temperature and humidity and pressure and you need to calculate them in order to get it to work correctly. The driver for this sensor can be configured to do floating point as well as integer math but I prefer to use the integer math. Here is a sample of calculating temperature and humidity.
int BME280_getTemp(void)
{
int i;
int v1, v2;
_readBytes(BME280_ADDRESS, BME280_TD, 3, _Buffer);
i = _Buffer[0];
i = i << 8;
i = i | _Buffer[1];
i = i << 8;
i = i | _Buffer[2];
i = i >> 4;
// print("Raw Temperature: %d\n", i);
v1 = i / 8 - _cal.T1 * 2;
v1 = v1 * _cal.T2 / 2048;
v2 = i / 16 - _cal.T1;
v2 = v2 * v2 / 4096 * _cal.T3 / 16384;
v1 = v1 + v2;
_cal.fine = v1;
v1 = (v1 * 5 + 128) / 256;
return v1;
}

int BME280_getHumidity(void)
{
int i;
int v1, v2, v3, v4, v5;
_readBytes(BME280_ADDRESS, BME280_HD, 2, _Buffer);
i = _Buffer[0];
i = i << 8;
i = i | _Buffer[1];
// print("Raw Humidity: %d\n", i);
v1 = _cal.fine - 76800;
v2 = i * 16384;
v3 = (int)_cal.H4 * 1048576;
v4 = v1 * (int)_cal.H5;
v5 = (v2 - v3 - v4 + 16384)/32768;
v2 = v1 * (int)_cal.H6 / 1024;
v3 = v1 * _cal.H3 / 2048;
v4 = v2 * (v3 + 32768) / 1024 + 2097152;
v2 = (v4 * _cal.H2 + 8192) / 16384;
v3 = v5 * v2;
v4 = v3 / 32768;
v4 = v4 * v4 / 128;
v5 = v3 - v4 * _cal.H1 / 16;
if (v5 < 0)
v5 = 0;
if (v5 > 419430400)
v5 = 419430400;
i = v5 / 4096;

i = i*100 / 1024;
return i;
}
Well once you go through the pain of getting the integer math to work and you can verify that your calculation are correct which required firing up an Arduino program with the driver unmodified to see if the answer I got matches. This took several trial and errors but once I got it I was done. This sensor was definitely a head banger. While there are simpler sensors out there this one is rated as the most accurate at reading these values.

Now all that is left is the WiFi web service. This required getting a subscription from the weather service. I used OpenWeatherMap for my service. They provide a free service which gives you the current weather in your area. The only problem with this is that the data is returned in JSON format. That means now I need to write a JSON interpreter so that I can decode the weather data.

I tried looking at other JSON interpreters for other platforms but they were complicated and tried to do too much. I only need to decode a JSON packet and not encode one. So I ended up writing my own JSON decoder. I had worked with JSON several times before for other projects and had a good understanding of how it worked. I actually used it for my outside weather station to pass weather data to a Java application that has a JSON decoder built in.

The library that I came up with was a simple decoder and that you tell it what you want to find and it would return a string value of that item. From there if the item was a number you had to convert it on your own. I didn’t bother handling all the different data formats of the JSON object.

After several trial and error on decoding a test JSON packet I was able to get the JSON decoder working. I was now able to read the values coming back from the OpenWeatherMap web services.

Now I need to build some code to drive the WiFi module so I can generate a web request to the OpenWeatherMap service. I decided not to use the Parallax WiFi library and decided to write my own. This is a skinned down version that just does the web request. This took a little trial and error as talking to the Parallax WiFi module was not easy. It is set up like AT command to a modem. You send it a special command and then get an answers back. Well the commands are a little more complicated and the return values are not always just right there.

Anyway when you want to send it a command you need to precede the command with 0xFE and then follow it with what function you want to do. The other issue is that the commands functions can get turned off. This means that even though you send it a command it will be ignored and passed on to the serial port. The command mode is turned off every time you load a program. I guess loading a program might send command data so they turn it off to prevent loading issues.

So on startup you can hold a pin low for a short time to put the WiFi module back into command mode so that you can run your code to connect to the web service. You could also power the unit off and then back on to put it in command mode but that gets old after a while.

Now the web service doesn’t want you to continuously call there service otherwise they are going to shut you down. Also the data doesn’t change but once every 10 minutes and so calling it continuously is a waste of time. To get the weather in your area you need to provide the city name and country or the city ID. You also need to send them your ID that was given to you when you signed up for the service. There are also some other parameters that tell the service how you want the data returned. In english or metric units for one.

Here is what the JSON packet looks like when it is returned to you.
{"coord":{"lon":-87.62,"lat":41.88},"weather":[{"id":701,"main":"Mist","description":"mist","icon":"50d"},{"id":741,"main":"Fog","description":"fog","icon":"50d"}],"base":"stations","main":{"temp":42.78,"pressure":1019,"humidity":100,"temp_min":39,"temp_max":48},"visibility":2414,"wind":{"speed":5.44,"deg":69.303},"clouds":{"all":90},"dt":1556799357,"sys":{"type":1,"id":4309,"message":0.008,"country":"US","sunrise":1556793933,"sunset":1556844554},"id":4887398,"name":"Chicago","cod":200}]
Now you could just use a string find function and look for “temp” in the returned data and then copy out the 42.78. With the JSON library I wrote it does just that for you. You tell it what you’re looking for and it finds it and returns the string value for that item. You will also notice that there are arrays of values and sub values. The JSON library handles that. So for example the weather array contains id, main, description and icon. To find icon you would tell it you are looking for weather.icon. The period indicates sub item in the array that it needs to return. The JSON code remembers where it last looked so you need to find the values in order otherwise you need to tell it you want to start over. This speeds up the process of looking for values and to find multiple values in an array. Also the date/times are encode as an integer value from a start date so decoding the date requires a little conversion. Fortunately it is just a UNIX date/time minus or plus your time zone. C code has a function for that built in.

Comments

  • iseriesiseries Posts: 383
    edited 2019-05-10 - 19:01:51
    I also uncovered a bug in the WiFi module that I needed to fix. I noticed that after a while all of a sudden the weather stopped updating. After looking at the debug logs on the WiFi module and the errors return in the code I determined that the pooled connections that are setup by the unit became exhausted. What was happening was that if a connection that needed to be looked up in DNS would sometimes fail for some reason that would cause the WiFi module to return an error indicating that it could not make a connection. This error caused that connection pool entry to be left allocated and could then no longer be used. Since the WiFi unit had a pool of 5 connections at some point these connections would be used up.

    Looking into the WiFi code I could see that a connection was allocated from the pool and then would go through some checks. If it failed one of those checks it would return an error to you but would not free the connection back to the pool. Since the connection information was not returned to you, you had no way of freeing it up. I guess you could remember what connection you were using and then when you get an error you could just close that connection. Anyway I went into the Parallax WiFi code and added the necessary code to return the connection to the pool and now all is right with the world.

    Attached is a video of all the different stages of the project. You will see I used my Prop Mini Plug and Play board along with the BME280 and the display module. I also had to switch to the CMM memory model so that I could get the BME280 library, HX8357 library, JSON library, and the WiFi library to fit into 32k of memory.

    Now I can just look on my desk and get the current weather condition in my area. While the unit does use a lot of power and powering it by battery will not last very long a simple 7.5 volt power adapter should do the trick. Current draw is around 80 milliamps at 12v.

    Here is a list of the parts used in this project:
    • Parallax Propeller Mini
    • Parallax WiFi Module
    • BME280 sensor
    • Adafruit TFT display
    • 3D printed Bezel
    • 3.3 volt Buck converter
    Here is a link to the code in the Objects Exchange:
    BME280 Sensor Library
    HX8357 Display Library
    JSON decoder Library
    Timer Library
    ESP8266 Library



    Mike
  • PublisonPublison Posts: 10,853
    edited 2019-05-08 - 13:49:05
    Thanks for that most excellent write up! It's good to hear about the setbacks, knowing that perseverance pays off in the end.
    Infernal Machine
  • iseries,
    Well done on a very complicated and well documented project.
    I have to admit that I just gave up on that BME280 sensor.
    The maths was just horrible and I could not get it to work reliably and accurately.
  • @iseries - Looks like you are doing lots of great work! Nicely done and well documented.
    Whit+

    "We keep moving forward, opening new doors, and doing new things, because we're curious and curiosity keeps leading us down new paths." - Walt Disney
  • Nicely done!

    The project does require an esp8266 library (#include "esp8266.h")... Can you attach the one that you used, please?

    Thanks,
    dgately
    Livermore, CA (50 miles SE of San Francisco)
  • Knew I would miss something.

    Library is in the object exchange.

    Mike
  • Very cool project, I have a special fondness of weather station type projects. I just picked me up a BME280 to help a buddy with his first project. Good documentation and thanks for sharing.
    Joshua Donelson
    http://jdpresents.com/
Sign In or Register to comment.