RSS

Archive for the ‘Electronics’ Category

Wind Speed and Ultraviolet Light Sensors

Saturday, April 2nd, 2016

Last time we looked at the I2C Sensors that are part of the weather station. At the end of the article it was noted that these were working and able to log data to the Particle ecosystem.

Now it is time for the analog sensors:

  1. Wind speed
  2. Wind direction
  3. Rainfall
  4. Ultraviolet light

The Ultraviolet light and wind direction sensor will present us with a problem as they are both analog sensors and the Oak only has one Analog-to-Digital Convertor (ADC).

The rainfall gauge and the wind speed sensor both use a similar technology to generate a signal, namely a magnet that will trigger a reed switch. These two sensors also present an issue; while they are simple enough there are a finite number of pins on the Oak and they are being consumed at a fair rate.

ML8511 – Ultraviolet Light Sensor

The ML8511 measures ultraviolet light at a wavelength of 365nM. This is at the top end of the UVB band, the band which is harmful to living tissue. The sensor generates a voltage that is linear and proportional to the intensity of the UV light. The intensity of the light measured is on the scale 0 to 15 mW/cm2. The following chart is taken from the data sheet:

ML8511 UV Plot

ML8511 UV Plot

The chart above is generated when the supply voltage is 3.0V but the system under development will be using a 3.3V supply. Some investigation is required to determine the output voltage when there is no UV present. After running the sensor under constant temperature conditions with no UV light present the ADC was generating a reading in the 316 – 320 range. This gives out output voltage for the sensor in the range 1.02V – 1.03V.

If we assume that the output characteristics of the sensor remain linear at 3.3V and the gradient remains the same then we can generate a formula for calculating the intensity of the UV light based upon the sensor output.

All linear graphs can be represented by the following formula:

y = mx + c

Where:

  • y is the y coordinate (in our case the voltage output of the sensor)
  • x is the x coordinate (in our case the intensity of the UV light)
  • m is the gradient of the line
  • c is a constant offset of the y coordinate

If we set x to 0 then the equation becomes y = c This represents the output of the sensor when there is no UV light present. As we have seen from the above experiment, this is in the range 1.02V – 1.03V. So as an approximation we will use the value c = 1.025V.

The gradient of any line is represented by the following formula:

m = deltaY / deltaX

When given two points on a line (x1, y1) and (x2, y2), then the gradient becomes:

m = (y2 – y1) / (x2 – x1)

Luckily the data sheet gives us two points on the line at 25C, namely the output voltage in shade and the output voltage at 10mW/cm2. So assuming shade represents no ultraviolet light then the gradient of the line becomes:

m = (2.2 – 1.0) / (10 – 0)

So m is 0.12. Plugging m and c back into the original equation gives:

y = 0.12x + 1.025

Solving for x gives:

x = (y – 1.025) / 0.12

Or to put it in context:

UV Intensity = (Sensor output in volts – 1.025) / 0.12 mW / cm2

There are some assumptions in the above work and the data sheet shows that the output voltage can vary but we have a method for calculating the intensity of UV light; accurate enough for a home project anyway.

Wind Speed

Wind speed is measured by an anemometer. The anemometer in the Weather Sensor kit is a cup anemometer:

Cup Anemometer

Cup Anemometer

This has a magnet on the spindle connected to the cups. The magnet closes (and opens) a reed switch each time it passes the switch. So one pulse is generated per full revolution of the spindle. Each full revolution of the spindle (per second) represents a wind speed of 1.492 miles per hour (mph) or 2.4 km/h.

For the experienced, this sounds simple but we all know there could be a nasty shock in store for us, namely switch bounce. An easy way to find out, hook the output up to an oscilloscope.

The circuit is simple enough, connect one switch contact to 3.3V, one to a resistor and the other end of the resistor to ground. Connect the scope to the resistor / switch junction.

The scope was set up to have a trigger voltage of about 1.5V and to trigger on the falling edge of a signal. The cups on the anemometer were then position so that the reed switch was closed (a high output on the scope) and the scope setup in single shot mode (to capture and hold the trace when triggered). Spinning the anemometer gave the following output:

Anemometer Switch Bounce

Anemometer Switch Bounce

As you can see, the switch does bounce. Switch debouncing is a well known and documented problem, in fact I have written about it here so we will not go too deep into the problem in this article. The solution that will be used is a simple RC circuit:

MSP430 Launchpad Debounce Circuit

MSP430 Launchpad Debounce Circuit

The principle is that the RC circuit resists change and so filters out the glitch in the above trace. So we need a filter that is resistive enough to filter out the glitch but fast enough to respond in the minimum time between pulses.

The weather station is going to be located in the mainland UK, about 30 miles from the coast. In this location the wind speed is unlikely to rise above 60-80 mph unless in extreme conditions (tornadoes are known to occur in the UK). So assuming the maximum wind speed in 149.2 mph (this number is based upon the fact that 1.492 mph gives one revolution per second) then we have a maximum number of rotations of 100 per second. This gives a revolution time of 10 milliseconds.

So we have a 10 millisecond window for the pulse from the sensor. The pulse will be low for the majority of the time as the switch can only close when the magnet is above the switch, so for most of the 10ms we will have a low pulse. You can see this in the following trace:

Anemometer Duty Cycle

Anemometer Duty Cycle

The signal appears to be high for 30% of the time. An accurate measurement could be made using the scope but it does not appear to be necessary. In our case, 10 milliseconds per rotation, the signal would be low for approximately 70% of the time, i.e. 7 milliseconds.

Going back to the first trace should the switch bounce it is observed that the switch bounce lasts for approximately 50-60 microseconds. Several observations of both the rising and falling edges showed this to be reasonable consistent. This final piece of information helps to define the parameters for this problem:

  • The frequency of the pulses should be 100Hz maximum (i.e. 10 milliseconds between pulses)
  • Duty cycle is 30% (high for 3 milliseconds, low for 7 milliseconds)
  • Switch bounce can last 50-60 microseconds (assume 100 microseconds as a worst case)
  • Trigger voltage is 2.3V with a supply voltage of 3.3V

There are plenty of online calculators for this type of circuit, Layada has one here that covers this type of scenario. We know the supply voltage (3.3V) and the trigger voltage (2.3V) so the only thing to do is look at the components available and calculate the delay time. After a few tries a 10K resistor and a 100nF capacitor were found to give a delay of 1.1939 milliseconds. This covers our case with plenty of margin for error.

Putting together the circuit above where the anemometer is the switch and triggering on the rising edge gives the following output on the oscilloscope:

Anemometer Gradual Rise

Anemometer Gradual Rise

The rise time looks to be acceptable, smooth and slow enough to iron out the glitches but fast enough to allow a 10 millisecond duration with a 30% duty cycle.

Conclusion

At the start of the article it was noted that four analog sensors are present in the kit. This article has concentrated on just two, the UV and Wind Speed sensors. The Rainfall and Wind Direction sensors will be covered in future articles.

The two sensors here will require one digital pin and one analog pin. As there is only one analog pin on the Oak then some creative thinking will be required in order to connect the Wind Direction sensor.

Assuming a maximum wind velocity of 149.2 mph then we will have 100 rotations of the anemometer per second. This should be something that could be measured by the Oak using an interrupt on one of the digital pins

At this point the hardware for six measurements can be put together on breadboard and some prototype software put together. This is the subject of the next article, Proof of Concept Software

Reading I2C Sensor Data with the Oak

Friday, April 1st, 2016

The weather station project will be bringing together a number of sensors, light, ultraviolet light, air pressure, humidity, wind speed, wind direction and rain fall. This collection of sensors falls into three groups:

  • Electronic sensors on an I2C bus
  • Mechanical sensors using switches
  • Analog sensors

The current plan is for the weather station to use the Oak as the microcontroller running the show. The data from the sensors can then be uploaded to the cloud, destination to be determined, but let’s start with Sparkfun’s data service.

The I2C sensors will require the least amount of work to get up and running so let’s start with those. The two sensors operating on the I2C bus are:

Oak and  2C Sensors on Breadboard

Oak and 2C Sensors on Breadboard

One of the great things about working with these two sensors is the fact that there are prebuilt drivers and example code for both breakout boards available from Github. What could be simpler, well head over to the Sketch – Include Library – Board Manager… menu in the Arduino IDE and you can download the library and have the IDE install it for you.

TSL2561 – Luminosity Sensor

This sensor allows the radiance of the light to be calculated in a way that approximates the response of the human eye. It does this by combining the input from two photodiodes, one infra-red only and one visible light and infra-red light combined. The output from the two sensors can be used to luminous emittance in lux (lumens per square metre).

The following table gives an idea of the lux values for typical scenarios:

Lux Typical Environment
0.0001 Moonless, overcast night sky
0.002 Moonless clear night sky
0.27–1.0 Full moon on a clear night
80 Office building hallway
320–500 Office lighting
1000 Overcast day
10000–25000 Daylight

As you can see from the table above, the lux values for a “normal” human day can vary dramatically. The sensor copes with this by allowing the use of a variable time window and sensitivity when taking a reading. Effectively the sensor accumulates the readings over the time window (integration interval) into a single 16-bit number which can then be used to calculate the lux reading.

BME280 – Air Pressure, Temperature and Humidity Sensor

This sensor is produced by Bosch and is packaged in both I2C and SPI configuration on the same board. The accuracy of the sensor appears good, pressure and temperature both to 1% and humidity to 3%.

Libraries

Both Sparkfun and Adafruit have provided libraries and example code for the boards. These were easy to add to the development environment.

One caveat, the BME280 requires the addition of the Adafruit sensor library as well as the BME280 library.

Once added it was a simple case of wiring up the sensor to 3.3V and the I2C bus and running the example code.

They both worked first time.

Some Code Modifications (for later)

The light sensor has been show to work in low light conditions but not to any degree of precision. A possible modification to the example code is to look at the sensitivity and integration window settings to see if the precision can be adjusted to make the sensor return better readings in low light.

Some of the values when calculated use the fractional part of a floating-point number, temperature and humidity spring to mind. This meant adding a method to convert a double into it’s string representation for debugging purposes. Trivially solved but an annoying omission from the implementation of sprintf.

Posting to Particle Dashboard

The Oak can also post to the Particle dashboard providing a second method of debugging your application. Statements such as Particle.publish(“Debug”, “Temperature data…”); will cause the string Temperature data… to be posted in a group/attribute Debug

So the readings from the office look like this:

Some Weather Data

Some Weather Data

Conclusion

Two simple to use sensors with good supported class libraries make these sensors quick and easy to hook up to the Oak. Merging the two examples was simple and sensor data is now appearing over the serial output from the Arduino.

Compiling the code gives some warnings about the I2C library being compiled for the ESP8266 whilst the target board is defined as an Oak. This can be ignored as the Oak is really a convenient wrapper around an ESP8266 module.

Next up, some analog sensors to measure ultraviolet light and wind and rain properties.

Setting Up the Oak – Flashing LED

Sunday, March 27th, 2016

The Oak microcontroller is new, Digistump only shipped version 1 firmware a few days ago (20th Match 2016). The hardware I have was shipped sometime in January. So the first thing to do is to upgrade the firmware and then try blinking an LED.

Upgrading the firmware

The Digistump Wiki contains a number of tutorials and troubleshooting guides. First stop the Connecting Your Oak for the First Time page. This shows how the Oak can be connected to your WiFi network and the firmware updated.

The initial over the air firmware update was problematic to say the least. Reading through the Digitsump forums it seems that I am not the only one having a problem with the first update. There are three methods for upgrading the firmware:

  • Over the Air using firmware from the internet
  • Over the air with a local server
  • Serial using pyserial or esptool

I started at the top of the list and slowly worked my way down. In the end the only method that worked for me was the serial update.

Flashing an LED

With the latest version of the firmware installed it is time to test the development environment. What could be simpler than flashing and LED. The on board LED is connected to pin 1 so lets try and use that.

Digistump offer two development environments:

At the time of writing there was a known issue with the Particle development environment which prevented an application being built and flashed successfully. This is an early release and so issues are expected.

This only leaves the Arduino environment, an IDE I really hate.

There are two methods for flashing an application to the Oak using the Arduino environment:

  • Over The Air (OTA)
  • Serial (requires Python)

Despite the earlier problems with the firmware update I decided to try the OTA method first. DigitStump have provided compehensive instructions in their WiKi on how to achieve this.

Following the example was easy and after only 15 minutes I have the LED on the Oak flashing at 1Hz. Just to prove it was not a fluke I then tried changing the frequency and reflashing the Oak.

Success!

Conclusion

My initial frustration with the firmware update was soon forgotten once I had an application successfully running on the Oak. I am hoping that the issues were caused by the fact I have an early release of the board with the original firmware installed.

Programming is easy enough and can be done over the air which is convenient.

Next up, talking to sensors.

Weather Station

Friday, March 25th, 2016

The first part of this year has seen me working on some photography projects, now time to get the soldering iron out. I have three projects I’m hoping to complete this year and one long term project that may take me a while, so pay attention 007.

Weather Station

A few weeks ago I received a couple of Oaks. These boards are based around the ESP12 module, a Kickstarter project I backed at the end of 2015. The project aimed to make working with the ESP12 easier by providing integration with particle.io. The latest firmware was release a few days ago so I think it is time to give this little device a go.

At the start of the year we came across a weather meter which allowed the measurement of rainfall, wind speed and wind direction. My wife mentioned that it might be nice to use one of these to record the weather in the garden.

And so the idea was born, weather meter, meet the Oak.

Mapping Out The Project

After a few hours the project started to become larger than just a couple of sensors, it currently looks something like the following:

Weather Station Mind Map

Weather Station Mind Map

I have coloured each block of concepts the same colour in order to give the project some structure. Let’s look at each block in turn and see how we can approach the problem.

Temperature, Pressure and Humidity

Starting with the green block and five o’clock on the diagram.

Temperature, Presure and Humidity Mind Map

Temperature, Presure and Humidity Mind Map

There are two temperatures we can measure here, air temperature and soil or ground level temperature. The air temperature will give an indication of how pleasant a day it is at the moment and the ground level sensor will give an indication of the progress of the seasons from the plants perspective. For this reason we are interested in both of these measurements.

Air pressure has long been used as a predictor for upcoming weather events.

Luckily we can get the air temperature, pressure and humidity sensor in one convenient package, the BME280. Ground level temperature will need a waterproof sensor. I think this would be best purchased as a unit rather than made so the DS18B20 with a 6 foot cable looks like it might be ideal.

Wind and Rain

Working clockwise, the next block are the wind and rain sensors:

Wind and Rain Sensor Mind Map

Wind and Rain Sensor Mind Map

All of these measurements come from the weather meter. The wind speed and rainfall sensors are simple switches that generate pulses whilst the wind direction is a resistor network.

Light Sensor

The next block shows the two light sensors:

Light Sensor Mind Map

Light Sensor Mind Map

The two light measurements are the overall light intensity (luminosity) and ultraviolet light intensity. Luckily there are a couple of sensors for these two measurements, the TSL2561 and the ML8511.

The ultraviolet light sensor is an analogue sensor and so we will have to consider the stability of the supply voltage when making the reading.

The luminosity sensor uses a measurement window and a sensitivity setting to take a reading. This means that for given settings the sensor may be overwhelmed and simply give a maximum reading. The work around for this is to make the measurement window and sensitivity dynamic. So long, sensitive windows at night and short less sensitive windows on bright sunny days.

Cases and Location

The next things we need to consider are cases and locations:

Case and Location Mind Map

Case and Location Mind Map

The microcontroller and power etc. will need to be located in a case of some form. This will need to be weather proof as water and electricity are not the best of friends. It would also be a good idea to keep any batteries in an environment with a reasonably stable temperature. A little research into how the professionals house weather Stations.

The sensors on the other hand need to be outdoors in a suitable location for the measurements being taken.

Power Supply

All of this equipment will need a stable power supply:

Power Supply Mind Map

Power Supply Mind Map

The initial work can be done using a bench power supply but when the project moves outdoors it will need to either be mains or battery powered. The long term aim is to use a solar cell and rechargeable battery, as they say on Kickstarter, a stretch goal.

Data Logging and Real Time Clock

Now we have collected all of this information we need to do store it somewhere:

Data Logging Mind Map

Data Logging Mind Map

The Oak is a WiFi enabled board so the most obvious place to put the data is the cloud. It might be an idea to also provide some local storage in case the WiFi network is unavailable.

The Real Time Clock (RTC) could have two uses, to wake the microcontroller and also provide a timestamp for the data items.

Conclusion

What started out as a simple wind and rain logging project has grown a little. The current specification is to measure and log the following readings:

  1. Rainfall
  2. Wind speed
  3. Wind direction
  4. Luminosity
  5. Ultraviolet light

The measurements should be collected and reliably logged either locally or preferably to the cloud.

There are several challenges with a number of interesting problems to overcome.

Let’s get this show on the road.

LED Board Has Arrived

Sunday, November 1st, 2015

A few weeks ago I started to work with KiCAD and put together a small LED board as a project to become familiar with the application. The boards were sent to manufacture and the the postman delivered the manufactured PCBs yesterday.

LED Board Front

LED Board Front

LED Board Front

LED Board Front

Time to add the components and test the boards.

Populated Board Right Angled Headers

Populated Board Right Angled Headers

And one with straight headers:

Populated Board Straight Headers

Populated Board Straight Headers

Everthing looks good, the boards work OK so time to move on to the big board for the cube.

Moving to KiCAD

Sunday, October 25th, 2015

I have been a long time supporter of DesignSpark PCB for laying out my PCBs. Moving to the Mac meant that I had to look for an alternative or to use the PC emulator. Enter KiCad.

I needed a project in order to evaluate KiCAD, something small and inexpensive to manufacture will fit the bill. One project that needs completing is the LED cube. The 74HC595s in this project were always going to restrict the power of the cube. They have a limit of 125mW which limits either the number of LEDs that can be illuminated or the brightness of the LEDs. This can be easily overcome by either using a higher power 74HC595 or by connecting each of the LEDs to a transistor wired as a switch. The transistor as a switch idea gives some flexibility for future experimentation and so this is the direction we will take. Making this board for the cube would be expensive as it would be large, over 15cm x 15cm. A smaller version would prove the concept as well as the software.

8 LED Indicator Board

The 8 LED Indicator Board is a simple PCB containing all of the components that would be used in a large powered board. The board is simple, it takes a signal for each of the 8 LEDs, power and ground. Any high signals turn the transistor on and illuminates the appropriate LED. The schematic for a single switch is as follows:

Simple Transistor Switch

Simple Transistor Switch

The board contains 8 of these switches along with connectors for the power and signals, adding seven more and some connectors gives the following:

8 Transistor Switches

8 Transistor Switches

The next step in the process is to convert this design to a PCB layout. Normally (with DesignSpark) I would just jump from the schematic straight to the PCB layout. With KiCAD it is necessary to perform two additional (for me) actions. The first is to annotate the components in the schematic. The second is to associate a footprint to each of the components. PCB layout can commence once this has been completed.

A few hours later:

8 LED Transistor Switch PCB Layout

8 LED Transistor Switch PCB Layout

There is even a 3D viewer with an impressive set of models built in:

8 LED Switch in 3D

8 LED Switch in 3D

So far, so good, the final step is to generate the gerbers, send for manufacture and wait.

Conclusion

Moving from one software suite to another is always an interesting process. Old habits such as key strokes, menu locations etc are difficult to break. I certainly found some aspects of KiCAD a little frustrating but as ever, there is always Google.

The gerber files have been sent to manufacture. The good news is that they were accepted straight away and have been through the manufacturing process. They are currently somewhere between China and the UK. I expect to see them in about 1 week (assuming the usual delay between ordering and shipping).

There are some issues I have found with the software:

  • The PCB Designer drains the battery on the Mac significantly faster than the Schematic layout tool.
  • Every now and then the PCB layout tool will start to zoom in (or out) seemingly at random.
  • Creating a new, empty component library is not as intuitive as it could be.

In fairness though there are some thing which I really did like:

  • Schematic editor is simple to use
  • Large component library
  • 3D Viewer
  • Easy to create new components
  • It only took a week to go from nothing to gerbers

All in all, this has been a positive experience although painful at times. The only thing remaining is to see how the boards turn out.

UDP on the ESP01 (ESP8266 Development Board)

Monday, June 1st, 2015

ESP8266 boards offer an amazing opportunity to explore the IoT world at a low cost. The boards start for as little as £5 here in the UK and come in a variety of form factors from one with only two GPIO pins to larger boards containing more GPIOs, PWM, I2C, SPI and analog capability. The boards started to become noticed around the middle of 2014 and have become more popular as the tools and documentation have matured.

Support for the boards is mainly through the online community forum. The latest documentation and SDKs can be found through the same forums.

The boards make low cost networks of sensors a possibility with the ESP8266 modules acting as either small servers or clients on your WiFi network.

The ESP8266 module is often supplied with one of two firmware version burned into the module:

  1. nodeMCU – Lua interpreter
  2. AT command interpreter

For the purposes of this exercise we will look at replacing the default firmware supplied with a custom application.

The SDK available from Expressif allows the developer to program the module using C. First thing to do is to install the tool chain for the module. The ESP8266 Wiki contains a host of information including instructions on setting up the tools on a variety of platforms. This whole process took about 2-3 hours on my Windows PC with another few hours to put together a template for a makefile application for Visual Studio.

Now that the development tools are in place we can start to think about the sensor network. Security is a major concern for Internet of Things (IoT) projects but this proof of concept will ignore this for the moment and rely on the WiFi encryption as the only mean of securing the data. This is a proof of concept afterall.

The applications developed here will allow data to be transmitted using UDL over the local area network.

UDP Receiver

Before starting on the ESP8266 application we should put together a simple application to receive the data from the local network. A simple .NET application on a Windows PC should do the trick:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;

namespace UDPConsoleClient
{
    class Program
    {
        static UdpClient _udpClient = new UdpClient(11000);

        static void Main(string[] args)
        {
            Console.WriteLine("UDP Client listening on port 11000");
            IPEndPoint _remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); 
            while (true)
            {
                byte[] data = _udpClient.Receive(ref _remoteEndPoint);
                string message = Encoding.ASCII.GetString(data);
                Console.WriteLine("Message ({0}): {1}", _remoteEndPoint.Address.ToString(), message);
            }
        }
    }
}

Create a new Windows console application on the PC and add the above code to the program.cs file. When run, this application will listen to the local network and display any UDP packets as a string to the console. Nothing special but good enough for the purposes of this experiment.

Basic ESP8266 Application

The basic ESP8266 application works in a similar way to an Arduino application, there is an initialisation method user_init and a loop method. The loop method is slightly different from the Arduino loop in that it is added to a task queue and the developer is free to select the name of the method. A basic application looks something like this:

#include "at_custom.h"
#include "ets_sys.h"
#include "osapi.h"
#include "gpio.h"
#include "os_type.h"
#include "mem.h"
#include "user_config.h"
#include "ip_addr.h"
#include "espconn.h"
#include "user_interface.h"

#define USER_TASK_PRIORITY          0
#define USER_TASK_QUEUE_LENGTH      1

//
//  Callbacks for the OS task queue.
//
os_event_t userTaskQueue[USER_TASK_QUEUE_LENGTH];

//
//  User function which will be added to the task queue.
//
static void ICACHE_FLASH_ATTR UserTask(os_event_t *events)
{
    os_delay_us(10);
}

//
//  Initialise the application and add the user task to the task queue.
//
void ICACHE_FLASH_ATTR user_init()
{
    //
    //  Start OS tasks.
    //
    system_os_task(UserTask, USER_TASK_PRIORITY, userTaskQueue, USER_TASK_QUEUE_LENGTH);
}

Compiling and deploying this application will result in a module which does nothing much other than wait.

Debugging

Unlike some of the other modules and boards discussed on this blog, the ESP8266 does not have much available in the way of debugging. Code will need to be added to the application to output debug information on GPIO pins in order to get feedback on what is going on inside the application.

The ESP01 module being used has two GPIO pins available, GPIO0 and GPIO2. We can use these to output serial data akin to SPI and then use a logic analyser to examine the application state. Adding the code below will allow us to do this.

#define BB_DATA                     BIT0
#define BB_CLOCK                    BIT2

//
//  Initialise the GPIO pins.
//
void InitialiseGPIO()
{
    gpio_init();
    //
    //  Set GPIO2 and GPIO0 to output mode.
    //
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2);
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0);
}

//
//  Bit bang the data out as serial (SPI) data.
//
void BitBang(uint8 byte)
{
    //
    //  Set the clock and data bits to be low.
    //
    gpio_output_set(0, BB_DATA, BB_DATA, 0);
    gpio_output_set(0, BB_CLOCK, BB_CLOCK, 0);
    //
    //  Output the data.
    //
    short bit;
    for (bit = 7; bit >= 0; bit--)
    {
        if (byte & (1 << bit))
        {
            gpio_output_set(BB_DATA, 0, BB_DATA, 0);
        }
        else
        {
            gpio_output_set(0, BB_DATA, BB_DATA, 0);
        }
        gpio_output_set(BB_CLOCK, 0, BB_CLOCK, 0);
        gpio_output_set(0, BB_CLOCK, BB_CLOCK, 0);
    }
    //
    //  Set the clock and data bits to be low.
    //
    gpio_output_set(0, BB_DATA, BB_DATA, 0);
    gpio_output_set(0, BB_CLOCK, BB_CLOCK, 0);
}

Timers

For the proof of concept the application can simply generate a regular packet of data emulating the presence of sensor data. The most cost effective (in terms of power) way of doing this will be to use a timer of some sort.

//
//  OS Timer structure holding information about the timer.
//
static volatile os_timer_t timerInformation;

//
//  Timer function called periodically - try communicating with the world.
//
void TimerCallback(void *arg)
{
    BitBang(0x01);
}

//
//  Initialise the application and add the user task to the task queue.
//
void ICACHE_FLASH_ATTR user_init()
{
    InitialiseGPIO();
    //
    //  Disarm timer.
    //
    os_timer_disarm(&timerInformation);
    //
    //  Set up timer to call the timer function.
    //
    os_timer_setfn(&timerInformation, (os_timer_func_t *) TimerCallback, NULL);
    //
    //  Arm the timer:
    //      - timerInformation is the pointer to the OS Timer data structure.
    //      - 1000 is the timer duration in milliseconds.
    //      - 0 fire once and 1 for repeating
    //
    os_timer_arm(&timerInformation, 1000, 1);
    //
    //  Start OS tasks.
    //
    system_os_task(UserTask, USER_TASK_PRIORITY, userTaskQueue, USER_TASK_QUEUE_LENGTH);
}

Adding the above code to the basic application should generate data on GPIO0 and GPIO2 every second.

UDP Communication & WiFi Connectivity

The last part of the puzzle is to connect the board to the local WiFi network and to start to send data across the network.

WiFi Connection

For a basic WiFi connection where the router is providing a DHCP service then we need to let the firmware know the SSID of the network we wish to connect to and the password for the network:

const char ssid[32] = "--- Your SSID Here ---";
const char password[32] = "--- Your Password Here ---";
struct station_config stationConf;

wifi_set_opmode(STATION_MODE);
os_memcpy(&stationConf.ssid, ssid, 32);
os_memcpy(&stationConf.password, password, 32);
wifi_station_set_config(&stationConf);

Executing the above code will set the network parameters. The network connection is not established immediately but can take some time to become active. Network connectivity can be detected by the following code snippet:

struct ip_info info;

wifi_get_ip_info(STATION_IF, &info);
if (wifi_station_get_connect_status() != STATION_GOT_IP || info.ip.addr == 0)
{
    // Not connected yet !!!
}

UDP Communication

Network communication needs a connection structure to be set up. This holds information about the network connection type, IP addresses and ports. For this exercise we are using the UDP protocol and will broadcast the data to any UDP listener on the network. The UDP broadcast address is x.y.z.255, in this case 192.168.1.255. The code to set this up is as follows:

struct espconn *_ptrUDPServer;
uint8 udpServerIP[] = { 192, 168, 1, 255 };

//
//  Allocate an "espconn" for the UDP connection.
///
_ptrUDPServer = (struct espconn *) os_zalloc(sizeof(struct espconn));
_ptrUDPServer->type = ESPCONN_UDP;
_ptrUDPServer->state = ESPCONN_NONE;
_ptrUDPServer->proto.udp = (esp_udp *) os_zalloc(sizeof(esp_udp));
_ptrUDPServer->proto.udp->local_port = espconn_port();
_ptrUDPServer->proto.udp->remote_port = 11000;
os_memcpy(_ptrUDPServer->proto.udp->remote_ip, udpServerIP, 4);

The application can now start to send data over the network:

#define USER_DATA                   "ESP8266 - Data"

espconn_create(_ptrUDPServer);
espconn_sent(_ptrUDPServer, (uint8 *) USER_DATA, (uint16) strlen(USER_DATA));
espconn_delete(_ptrUDPServer);

The application should really perform some diagnostics in the above code in order to detect any errors and clean up where necessary.

Putting it all Together

A little rearrangement of the snippets above is necessary in order to put together a mode elegant solution, namely:

  1. Output the results of function calls to the network to the GPIO pins
  2. check for network connectivity before calling the network functions

Adding these modifications to the code gives the following final solution:

// ***************************************************************************
//
//  Basic UDP broadcast application for the ESP8266.
//
#include "at_custom.h"
#include "ets_sys.h"
#include "osapi.h"
#include "gpio.h"
#include "os_type.h"
#include "mem.h"
#include "user_config.h"
#include "ip_addr.h"
#include "espconn.h"
#include "user_interface.h"

#define USER_TASK_PRIORITY          0
#define USER_TASK_QUEUE_LENGTH      1
#define USER_DATA                   "ESP8266 - Data"
#define BB_DATA                     BIT0
#define BB_CLOCK                    BIT2

//
//  Callbacks for the OS task queue.
//
os_event_t userTaskQueue[USER_TASK_QUEUE_LENGTH];
struct espconn *_ptrUDPServer;
uint8 udpServerIP[] = { 192, 168, 1, 255 };

// ***************************************************************************
//
//  Forward declarations.
//
static void UserTask(os_event_t *events);

//
//  OS Timer structure holding information about the timer.
//
static volatile os_timer_t timerInformation;

//
//  Initialise the GPIO subsystem.
//
void InitialiseGPIO()
{
    gpio_init();
    //
    //  Set GPIO2 and GPIO0 to output mode.
    //
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2);
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0);
}

//
//  Bit bang the data out as serial (SPI-like) data.
//
void BitBang(uint8 byte)
{
    //
    //  Set the clock and data bits to be low.
    //
    gpio_output_set(0, BB_DATA, BB_DATA, 0);
    gpio_output_set(0, BB_CLOCK, BB_CLOCK, 0);
    //
    //  Output the data.
    //
    short bit;
    for (bit = 7; bit >= 0; bit--)
    {
        if (byte & (1 << bit))
        {
            gpio_output_set(BB_DATA, 0, BB_DATA, 0);
        }
        else
        {
            gpio_output_set(0, BB_DATA, BB_DATA, 0);
        }
        gpio_output_set(BB_CLOCK, 0, BB_CLOCK, 0);
        gpio_output_set(0, BB_CLOCK, BB_CLOCK, 0);
    }
    //
    //  Set the clock and data bits to be low.
    //
    gpio_output_set(0, BB_DATA, BB_DATA, 0);
    gpio_output_set(0, BB_CLOCK, BB_CLOCK, 0);
}

//
//  Timer function called periodically - try communicating with the world.
//
void TimerCallback(void *arg)
{
    struct ip_info info;

    wifi_get_ip_info(STATION_IF, &info);
    if (wifi_station_get_connect_status() != STATION_GOT_IP || info.ip.addr == 0)
    {
        BitBang(0x01);
    }
    else
    {
        BitBang(0x02);
        BitBang(espconn_create(_ptrUDPServer));
        BitBang(0x03);
        BitBang(espconn_sent(_ptrUDPServer, (uint8 *) USER_DATA, (uint16) strlen(USER_DATA)));
        BitBang(0x04);
        BitBang(espconn_delete(_ptrUDPServer));
        BitBang(0x05);
    }
}

//
//  User function which will be added to the task queue.
//
static void ICACHE_FLASH_ATTR UserTask(os_event_t *events)
{
    os_delay_us(10);
}

//
//  Initialise the application and add the user task to the task queue.
//
void ICACHE_FLASH_ATTR user_init()
{
    at_init();
    InitialiseGPIO();
    //
    //  Disarm timer.
    //
    os_timer_disarm(&timerInformation);
    //
    //  Start the network connection.
    //
    const char ssid[32] = "--- Your SSID Here ---";
    const char password[32] = "--- Your Password Here ---";
    struct station_config stationConf;
    
    wifi_set_opmode(STATION_MODE);
    os_memcpy(&stationConf.ssid, ssid, 32);
    os_memcpy(&stationConf.password, password, 32);
    wifi_station_set_config(&stationConf);
    //
    //  Allocate an "espconn" for the UDP connection.
    ///
    _ptrUDPServer = (struct espconn *) os_zalloc(sizeof(struct espconn));
    _ptrUDPServer->type = ESPCONN_UDP;
    _ptrUDPServer->state = ESPCONN_NONE;
    _ptrUDPServer->proto.udp = (esp_udp *) os_zalloc(sizeof(esp_udp));
    _ptrUDPServer->proto.udp->local_port = espconn_port();
    _ptrUDPServer->proto.udp->remote_port = 11000;
    os_memcpy(_ptrUDPServer->proto.udp->remote_ip, udpServerIP, 4);
    //
    //  Setup timer to call the timer function.
    //
    os_timer_setfn(&timerInformation, (os_timer_func_t *) TimerCallback, NULL);
    //
    //  Arm the timer:
    //      - timerInformation is the pointer to the OS Timer data structure.
    //      - 1000 is the timer duration in milliseconds.
    //      - 0 fire once and 1 for repeating
    //
    os_timer_arm(&timerInformation, 1000, 1);
    //
    //  Start OS tasks.
    //
    system_os_task(UserTask, USER_TASK_PRIORITY, userTaskQueue, USER_TASK_QUEUE_LENGTH);
}

Compiling and deploying this application to the ESP01 generates regular (1 second intervals) messages on the console application at the start of this post.

Conclusion

The journey with the ESP8266 has just started but it looks promising. The initial set up was not a simple as I would have liked as there are a number of parts all of which need to be brought together in order for the development cycle to start. Once these are all in place then coding is relatively easy even though there is little documentation. The community which is developing is certainly helping in this process.

On the whole this chipset offers an interesting opportunity especially with the like of Adafruit developing the Huzzah DIP friendly ESP12 board which has now received the appropriate certification for commercial WiFi use.

STM8S I2C Slave Device

Sunday, May 24th, 2015

Having succeeded at getting a basic I2C master working on the STM8S it is not time to start to look at I2C slave devices on the STM8S.

The project will be a simple one, the STM8S will take a stream of bytes and perform simple addition. When requested, the STM8S will return the total as a 16-bit integer. Each new write request will clear the current total and start the whole process again.

Simple enough so let’s get started.

This post is a fairly long as it contains a lot of code so it might be a good time to grab a beer and settle down.

Simple Slave Adder

The slave adder is a simple device running over I2C. The devices has the following properties:

  1. Acts as an I2C slave device with address 0x50
  2. I2C bus speed is 50 KHz
  3. Upon receiving a valid start condition and address for a write operation the device will clear the current total.
  4. Bytes written to the device will be summed and a running total kept.
  5. A start condition with a valid address for a read operation will return the current total as a 16-bit integer, MSB first.

A Netduino 3 will be used as the I2C bus master device. This runs the .NET Microframework and has an implementation of the I2C protocol built into the framework. This gives a good reference point for the master device i.e. someone else has debugged that so we can assume that the device is working as per the protocol specification.

As mentioned in the previous article on I2C Master devices, there is a really good article about the I2C protocol on Wikipedia. If you want more information about the protocol then I suggest you head over there.

Netduino Code

The initial version of the I2C master device will simply send two bytes of data for the I2C slave device to sum. The code is simple enough:

using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;

namespace I2CMaster
{
    public class Program
    {
        public static void Main()
        {
            //
            //  Create a new I2C object on address 0x50 with the clock running at 50 KHz.
            //
            I2CDevice i2cBus = new I2CDevice(new I2CDevice.Configuration(0x50, 50));
            //
            //  Create a transaction to write two bytes of data to the I2C bus.
            //
            byte[] buffer = { 1, 2 };
            I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[1];
            transactions[0] = I2CDevice.CreateWriteTransaction(buffer);
            while (true)
            {
                int bytesRead = i2cBus.Execute(transactions, 100);
                Thread.Sleep(1000);
            }
        }
    }
}

The above application creates a new instance of the I2CDevice with a device address of )x50 and a clock frequency of 50 KHz. A single transaction is created and the master writes the same two bytes to the I2C bus every second.

STM8S Code

As with the I2C Master article, much of the action happens in two places:

  1. I2C device initialisation method
  2. I2C Interrupt Service Routine (ISR)

The initialisation method sets up the I2C peripheral on the STM8S and enters the waiting state, waiting for the master to put data onto the I2C bus.

The ISR will deal with the actual data processing and in a full application it will also deal with any error conditions that may arise.

I2C Initialisation

The initialisation method sets up the I2C bus by performing the following tasks:

  1. Setting the expected clock frequency
  2. Setting the device address
  3. Turning the interrupts on

The I2C peripheral must be disabled before configuring certain aspects of the peripheral, namely clock speeds:

I2C_CR1_PE = 0;

The peripheral needs to know the current master clock frequency, the I2C mode (Standard or Fast) and the clock divider values. These must all be configured whilst the peripheral is disabled.

I2C_FREQR = 16;                     //  Set the internal clock frequency (MHz).
I2C_CCRH_F_S = 0;                   //  I2C running is standard mode.
I2C_CCRL = 0xa0;                    //  SCL clock speed is 50 KHz.
I2C_CCRH_CCR = 0x00;

The device assumes that we will be using the standard 16 MHz clock speed which has been used in the other tutorials in this series. This is indicated by the I2C_FREQR register.

The values for the clock control register (I2C_CCRL and I2C_CCRH_CCR were simply taken from the STM8S programming reference manual. There is no coincidence that a bus speed of 50 KHz was chose, it is simply one of the reference values in the table. No point in creating work if you do not have to. If you want different speeds then you can either use one of the defined values in the remainder of the table or use the formulae provided.

The next step is to define the device address and addressing mode. I2C allows both 7 and 10 bit addressing modes. For simplicity this device will use a 7-bit device address:

I2C_OARH_ADDMODE = 0;               //  7 bit address mode.
I2C_OARH_ADD = 0;                   //  Set this device address to be 0x50.
I2C_OARL_ADD = 0x50;
I2C_OARH_ADDCONF = 1;               //  Docs say this must always be 1.

The device also allows for the maximum rise time to be configured. This example uses 17uS as the maximum time:

I2C_TRISER = 17;

The I2C peripheral allows for three different interrupt conditions to be defined, buffer interrupts, event interrupts (start condition etc.) and error interrupts. For convenience we will turn all of these on:

I2C_ITR_ITBUFEN = 1;                //  Buffer interrupt enabled.
I2C_ITR_ITEVTEN = 1;                //  Event interrupt enabled.
I2C_ITR_ITERREN = 1;

Now that the peripheral is configured we need to re-enable it:

I2C_CR1_PE = 1;

The last bit of initialisation is to configure the device to return an ACK after each byte:

I2C_CR2_ACK = 1;

At this point the I2C peripheral should be listening to the I2C bus for a start condition and the address 0x50.

I2C Interrupt Service Routine (ISR)

The ISR contains the code which processes the data and error conditions for the I2C peripheral. All of the I2C events share the same ISR and the ISR will need to interrogate the status registers in order to determine the exact reason for the interrupt.

Starting with an empty ISR we have the following code:

#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
}

There are several other vector names we could have chosen, they all contain the same value and the value I2C_RXNE_vector was chosen arbitrarily.

Assuming that there are no errors on the bus, the I2C master code above will cause the following events to be generated:

  1. Address detection
  2. Receive buffer not empty

This initial simple implementation can use these two events to clear the total when a new Start condition is received and add the current byte to the total when data is received.

if (I2C_SR1_ADDR)
{
    //
    //  In master mode, the address has been sent to the slave.
    //  Clear the status registers and wait for some data from the salve.
    //
    reg = I2C_SR1;
    reg = I2C_SR3;
    _total = 0;                 // New addition so clear the total.
    return;
}

I2C_SR1_ADDR should be set when an address is detected on the bus which matches the address currently in the address registers. As a starter application the code can simply assume that any address is going to be a write condition. The code can clear the totals and get ready to receive data. Note that this will be expanded later to take into consideration the fact that the master can perform both read and write operations.

The next event we need to consider is the receipt of data from the master. This will trigger the Receive Buffer Not Empty interrupt. This simple application should just read the buffer and add the current byte to the running total:

if (I2C_SR1_RXNE)
{
    //
    //  Received a new byte of data so add to the running total.
    //
    _total += I2C_DR;
    return;
}

As indicated earlier, the I2C ISR is a generic ISRT and is triggered for all I2C events. The initialisation code above has turned on the error interrupts as well as the data capture interrupts. Whilst none of the code in this article will handle error conditions, the ISR will output the status registers for diagnostic purposes:

PIN_ERROR = 1;
__no_operation();
PIN_ERROR = 0;
reg = I2C_SR1;
BitBang(reg);
reg = I2C_SR3;
BitBang(reg);

This will of course require suitable definitions and support methods.

Putting this all together gives an initial implementation of a slave device as:

//
//  This application demonstrates the principles behind developing an
//  I2C slave device on the STM8S microcontroller.  The application
//  will total the byte values written to it and then send the total
//  to the master when the device is read.
//
//  This software is provided under the CC BY-SA 3.0 licence.  A
//  copy of this licence can be found at:
//
//  http://creativecommons.org/licenses/by-sa/3.0/legalcode
//
#if defined DISCOVERY
    #include <iostm8S105c6.h>
#else
    #include <iostm8s103f3.h>
#endif
#include <intrinsics.h>

//
//  Define some pins to output diagnostic data.
//
#define PIN_BIT_BANG_DATA       PD_ODR_ODR4
#define PIN_BIT_BANG_CLOCK      PD_ODR_ODR5
#define PIN_ERROR               PD_ODR_ODR6

//
//  Somewhere to hold the sum.
//
int _total;

//
//  Bit bang data on the diagnostic pins.
//
void BitBang(unsigned char byte)
{
    for (short bit = 7; bit >= 0; bit--)
    {
        if (byte & (1 << bit))
        {
            PIN_BIT_BANG_DATA = 1;
        }
        else
        {
            PIN_BIT_BANG_DATA = 0;
        }
        PIN_BIT_BANG_CLOCK = 1;
        __no_operation();
        PIN_BIT_BANG_CLOCK = 0;
    }
    PIN_BIT_BANG_DATA = 0;
}

//
//  Set up the system clock to run at 16MHz using the internal oscillator.
//
void InitialiseSystemClock()
{
    CLK_ICKR = 0;                       //  Reset the Internal Clock Register.
    CLK_ICKR_HSIEN = 1;                 //  Enable the HSI.
    CLK_ECKR = 0;                       //  Disable the external clock.
    while (CLK_ICKR_HSIRDY == 0);       //  Wait for the HSI to be ready for use.
    CLK_CKDIVR = 0;                     //  Ensure the clocks are running at full speed.
    CLK_PCKENR1 = 0xff;                 //  Enable all peripheral clocks.
    CLK_PCKENR2 = 0xff;                 //  Ditto.
    CLK_CCOR = 0;                       //  Turn off CCO.
    CLK_HSITRIMR = 0;                   //  Turn off any HSIU trimming.
    CLK_SWIMCCR = 0;                    //  Set SWIM to run at clock / 2.
    CLK_SWR = 0xe1;                     //  Use HSI as the clock source.
    CLK_SWCR = 0;                       //  Reset the clock switch control register.
    CLK_SWCR_SWEN = 1;                  //  Enable switching.
    while (CLK_SWCR_SWBSY != 0);        //  Pause while the clock switch is busy.
}

//
//  Initialise the I2C system.
//
void InitialiseI2C()
{
    I2C_CR1_PE = 0;                     //  Disable I2C before configuration starts.
    //
    //  Set up the clock information.
    //
    I2C_FREQR = 16;                     //  Set the internal clock frequency (MHz).
    I2C_CCRH_F_S = 0;                   //  I2C running is standard mode.
    I2C_CCRL = 0xa0;                    //  SCL clock speed is 50 KHz.
    I2C_CCRH_CCR = 0x00;
    //
    //  Set the address of this device.
    //
    I2C_OARH_ADDMODE = 0;               //  7 bit address mode.
    I2C_OARH_ADD = 0;                   //  Set this device address to be 0x50.
    I2C_OARL_ADD = 0x50;
    I2C_OARH_ADDCONF = 1;               //  Docs say this must always be 1.
    //
    //  Set up the bus characteristics.
    //
    I2C_TRISER = 17;
    //
    //  Turn on the interrupts.
    //
    I2C_ITR_ITBUFEN = 1;                //  Buffer interrupt enabled.
    I2C_ITR_ITEVTEN = 1;                //  Event interrupt enabled.
    I2C_ITR_ITERREN = 1;
    //
    //  Configuration complete so turn the peripheral on.
    //
    I2C_CR1_PE = 1;
    //
    //  Acknowledge each byte with an ACK signal.
    //
    I2C_CR2_ACK = 1;
}

//
//  I2C interrupts all share the same handler.
//
#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
    unsigned char reg;

    if (I2C_SR1_ADDR)
    {
        //
        //  Clear the status registers and wait for some data from the salve.
        //
        reg = I2C_SR1;
        reg = I2C_SR3;
        _total = 0;                 // New addition so clear the total.
        return;
    }
    if (I2C_SR1_RXNE)
    {
        //
        //  Received a new byte of data so add to the running total.
        //
        _total += I2C_DR;
        return;
    }
    //
    //  Send a diagnostic signal to indicate we have cleared
    //  the error condition.
    //
    PIN_ERROR = 1;
    __no_operation();
    PIN_ERROR = 0;
    //
    //  If we get here then we have an error so clear
    //  the error, output the status registers and continue.
    //
    reg = I2C_SR1;
    BitBang(reg);
    reg = I2C_SR3;
    BitBang(reg);
}

//
//  Main program loop.
//
int main()
{
    _total = 0;
    __disable_interrupt();
    //
    //  Initialise Port D.
    //
    PD_ODR = 0;             //  All pins are turned off.
    PD_DDR_DDR4 = 1;        //  Port D, bit 4 is output.
    PD_CR1_C14 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C24 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR5 = 1;        //  Port D, bit 5 is output.
    PD_CR1_C15 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C25 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR6 = 1;        //  Port D, bit 6 is output.
    PD_CR1_C16 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C26 = 1;         //  Pin can run up to 10 MHz.
    //
    InitialiseSystemClock();
    InitialiseI2C();
    __enable_interrupt();
    while (1)
    {
        __wait_for_interrupt();
    }
}

Executing the Applications

At this stage a quick test with the two devices connected and the logic analyser will show the master device outputting data to the I2C bus. The correct operation of the I2C salve device can be verified by setting break points with in the two if statements in the ISR. Note that this will generate some errors but it is good enough to verify that the ISRs are being triggered correctly.

STM8 I2C Write Condition

STM8 I2C Write Condition

The above shows the output from the Saleae Logic Analyser when the write transaction is sent to the bus by the Netduino.

Reading and Writing with I2C Slaves

At this point the above code should be accepting data from the Netduino 3. It is now time to expand the code to take into account the requirement to read back data from the slave device.

Netduino Code

The Netduino code will need to be modified to generate both a write transaction to send data to the I2C slave device and a read transaction to retrieve the results from the calculation.

using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;

namespace I2CMaster
{
    public class Program
    {
        public static void Main()
        {
            //
            //  Create a new I2C object on address 0x50 with the clock running at 50 KHz.
            //
            I2CDevice i2cBus = new I2CDevice(new I2CDevice.Configuration(0x50, 50));
            //
            //  Create a transaction to write two bytes of data to the I2C bus.
            //
            byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            byte[] resultBuffer = new byte[2];
            I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[2];
            transactions[0] = I2CDevice.CreateWriteTransaction(buffer);
            transactions[1] = I2CDevice.CreateReadTransaction(resultBuffer);
            while (true)
            {
                int bytesRead = i2cBus.Execute(transactions, 100);
                Thread.Sleep(1000);
            }
        }
    }
}

The transaction array has been expanded to contain a second transaction to read the data from the device. The data to be summed has also been expanded to include a further eight elements.

STM8S Code

A state machine will be used to allow the STM8S to work out what action should be taken within the ISR. This is required as the i”c peripheral will generate an event for the address detection for both the write transaction and the read transaction. The application will need to be able to differentiate between the two conditions as one requires the total to be cleared, the other requires that the total is sent back to the master device.

The state machine contains the following states:

  1. Waiting for a write condition to start
  2. Adding data to the running total
  3. Sending the MSB of the total
  4. Sending the LSB of the total

This is represented in code by an enum and a global variable:

typedef enum
{
    ISWaiting,                    //  Waiting for comms to start.
    ISAdding,                     //  Adding bytes of data.
    ISSendingMSB,                 //  Sending MSB of total.
    ISSendingLSB                  //  Sending LSB of total.
} I2CStateType;
I2CStateType _i2cState;

The ISR will need to be changed in order to deal with the state machine and also handle the slave transmitting data to the master device. The first change is to the address detection. This assumes that the main program loop initialises the state to ISWaiting:

if (I2C_SR1_ADDR)
{
    //
    //  Slave address received, work out if to expect a read or a write.
    //
    if (_i2cState == ISWaiting)
    {
        _i2cState = ISAdding;
        _total = 0;             // New addition so clear the total.
    }
    reg = I2C_SR1;
    reg = I2C_SR3;
    return;
}

The code above works out if the application is waiting for the first write condition (IsWaiting) or if the address detection has been triggered by a read condition (any other state).

Next, any other data reception is assumed to be data to be added to the total. The state is changed to IsAdding just in case:

if (I2C_SR1_RXNE)
{
    //
    //  Receiving data from the master so we must be adding.
    //
    _total += I2C_DR;
    _i2cState = ISAdding;
    return;
}

Next we have two new conditions we have not considered so far, the first is the fact that we need to transmit the result to the master. The request from the client will trigger a Transmit Buffer Empty event:

if (I2C_SR1_TXE)
{
    if (_i2cState == ISAdding)
    {
        I2C_DR = (_total >> 8) & 0xff;
        _i2cState = ISSendingMSB;
    }
    else
    {
        I2C_DR = _total & 0xff;
        _i2cState = ISSendingLSB;
    }
    return;
}

The master is asking for two bytes and the application needs to track if we are transmitting the most significant byte (MSB) or least significant byte (LSB).

The next new condition is the acknowledge event. This is generated at the end of the master read operation. This will set the system back into a waiting condition:

if (I2C_SR2_AF)
{
    I2C_SR2_AF = 0;             //  End of slave transmission.
    _i2cState = ISWaiting;
    return;
}

The full application becomes:

//
//  This application demonstrates the principles behind developing an
//  I2C slave device on the STM8S microcontroller.  The application
//  will total the byte values written to it and then send the total
//  to the master when the device is read.
//
//  This software is provided under the CC BY-SA 3.0 licence.  A
//  copy of this licence can be found at:
//
//  http://creativecommons.org/licenses/by-sa/3.0/legalcode
//
#if defined DISCOVERY
    #include <iostm8S105c6.h>
#else
    #include <iostm8s103f3.h>
#endif
#include <intrinsics.h>

//
//  Define some pins to output diagnostic data.
//
#define PIN_BIT_BANG_DATA       PD_ODR_ODR4
#define PIN_BIT_BANG_CLOCK      PD_ODR_ODR5
#define PIN_ERROR               PD_ODR_ODR6

//
//  State machine for the I2C communications.
//
typedef enum
{
    ISWaiting,                    //  Waiting for comms to start.
    ISAdding,                     //  Adding bytes of data.
    ISSendingMSB,                 //  Sending MSB of total.
    ISSendingLSB                  //  Sending LSB of total.
} I2CStateType;
I2CStateType _i2cState;

//
//  Somewhere to hold the sum.
//
int _total;

//
//  Bit bang data on the diagnostic pins.
//
void BitBang(unsigned char byte)
{
    for (short bit = 7; bit >= 0; bit--)
    {
        if (byte & (1 << bit))
        {
            PIN_BIT_BANG_DATA = 1;
        }
        else
        {
            PIN_BIT_BANG_DATA = 0;
        }
        PIN_BIT_BANG_CLOCK = 1;
        __no_operation();
        PIN_BIT_BANG_CLOCK = 0;
    }
    PIN_BIT_BANG_DATA = 0;
}

//
//  Set up the system clock to run at 16MHz using the internal oscillator.
//
void InitialiseSystemClock()
{
    CLK_ICKR = 0;                       //  Reset the Internal Clock Register.
    CLK_ICKR_HSIEN = 1;                 //  Enable the HSI.
    CLK_ECKR = 0;                       //  Disable the external clock.
    while (CLK_ICKR_HSIRDY == 0);       //  Wait for the HSI to be ready for use.
    CLK_CKDIVR = 0;                     //  Ensure the clocks are running at full speed.
    CLK_PCKENR1 = 0xff;                 //  Enable all peripheral clocks.
    CLK_PCKENR2 = 0xff;                 //  Ditto.
    CLK_CCOR = 0;                       //  Turn off CCO.
    CLK_HSITRIMR = 0;                   //  Turn off any HSIU trimming.
    CLK_SWIMCCR = 0;                    //  Set SWIM to run at clock / 2.
    CLK_SWR = 0xe1;                     //  Use HSI as the clock source.
    CLK_SWCR = 0;                       //  Reset the clock switch control register.
    CLK_SWCR_SWEN = 1;                  //  Enable switching.
    while (CLK_SWCR_SWBSY != 0);        //  Pause while the clock switch is busy.
}

//
//  Set up Port D GPIO for diagnostics.
//
void InitialisePortD()
{
    PD_ODR = 0;             //  All pins are turned off.
    PD_DDR_DDR4 = 1;        //  Port D, bit 4 is output.
    PD_CR1_C14 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C24 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR5 = 1;        //  Port D, bit 5 is output.
    PD_CR1_C15 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C25 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR6 = 1;        //  Port D, bit 6 is output.
    PD_CR1_C16 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C26 = 1;         //  Pin can run up to 10 MHz.
}

//
//  Initialise the I2C system.
//
void InitialiseI2C()
{
    I2C_CR1_PE = 0;                     //  Disable I2C before configuration starts.
    //
    //  Set up the clock information.
    //
    I2C_FREQR = 16;                     //  Set the internal clock frequency (MHz).
    I2C_CCRH_F_S = 0;                   //  I2C running is standard mode.
    I2C_CCRL = 0xa0;                    //  SCL clock speed is 50 KHz.
    I2C_CCRH_CCR = 0x00;
    //
    //  Set the address of this device.
    //
    I2C_OARH_ADDMODE = 0;               //  7 bit address mode.
    I2C_OARH_ADD = 0;                   //  Set this device address to be 0x50.
    I2C_OARL_ADD = 0x50;
    I2C_OARH_ADDCONF = 1;               //  Docs say this must always be 1.
    //
    //  Set up the bus characteristics.
    //
    I2C_TRISER = 17;
    //
    //  Turn on the interrupts.
    //
    I2C_ITR_ITBUFEN = 1;                //  Buffer interrupt enabled.
    I2C_ITR_ITEVTEN = 1;                //  Event interrupt enabled.
    I2C_ITR_ITERREN = 1;
    //
    //  Configuration complete so turn the peripheral on.
    //
    I2C_CR1_PE = 1;
    //
    //  Set the acknowledge to be ACK.
    //
    I2C_CR2_ACK = 1;
}

//
//  I2C interrupts all share the same handler.
//
#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
    unsigned char reg;

    if (I2C_SR1_ADDR)
    {
        //
        //  Slave address received, work out if to expect a read or a write.
        //
        if (_i2cState == ISWaiting)
        {
            _i2cState = ISAdding;
            _total = 0;             // New addition so clear the total.
        }
        reg = I2C_SR1;
        reg = I2C_SR3;
        return;
    }
    if (I2C_SR1_RXNE)
    {
        //
        //  Receiving data from the master so we must be adding.
        //
        _total += I2C_DR;
        _i2cState = ISAdding;
        return;
    }
    if (I2C_SR1_TXE)
    {
        if (_i2cState == ISAdding)
        {
            I2C_DR = (_total >> 8) & 0xff;
            _i2cState = ISSendingMSB;
        }
        else
        {
            I2C_DR = _total & 0xff;
            _i2cState = ISSendingLSB;
        }
        return;
    }
    if (I2C_SR2_AF)
    {
        I2C_SR2_AF = 0;             //  End of slave transmission.
        _i2cState = ISWaiting;
        return;
    }
    //
    //  Send a diagnostic signal to indicate we have cleared
    //  the error condition.
    //
    PIN_ERROR = 1;
    __no_operation();
    PIN_ERROR = 0;
    //
    //  If we get here then we have an error so clear
    //  the error, output the status registers and continue.
    //
    reg = I2C_SR1;
    BitBang(reg);
    BitBang(I2C_SR2);
    reg = I2C_SR3;
    BitBang(reg);
}

//
//  Main program loop.
//
int main()
{
    _total = 0;
    _i2cState = ISWaiting;
    __disable_interrupt();
    InitialisePortD();
    InitialiseSystemClock();
    InitialiseI2C();
    __enable_interrupt();
    while (1)
    {
        __wait_for_interrupt();
    }
}

The above two pieces of code are fuller applications and if we execute these and hook up the logic anayser to the I2C bus we see the following output:

Adding 1 To 10

Adding 1 To 10

Reading From Multiple Devices

The I2C protocol allows for one or more devices with different slave addresses to be connected to the same I2C bus. The previous article used the TMP102 temperature sensor with slave address 0x48 and this article has created a slave device with address 0x50. It should therefore be possible to connect the two devices to the same bus and talk to each selectively.

Netduino Application

As with the previous examples, the Netduino 3 will be used as the I2C bus master. The applicaiton above will need to be merged with the code in the previous article.

using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;

namespace I2CMaster
{
    public class Program
    {
        public static void Main()
        {
            //
            //  Create a new I2C object and the configurations for the STM8S and the TMP102.
            //
            I2CDevice.Configuration stm8s = new I2CDevice.Configuration(0x50, 50);
            I2CDevice.Configuration tmp102 = new I2CDevice.Configuration(0x48, 50);
            I2CDevice i2cBus = new I2CDevice(stm8s);
            //
            //  Create a transaction to write several bytes of data to the I2C bus.
            //
            byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            byte[] resultBuffer = new byte[2];
            I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[2];
            transactions[0] = I2CDevice.CreateWriteTransaction(buffer);
            transactions[1] = I2CDevice.CreateReadTransaction(resultBuffer);
            //
            //  Create a transaction to read two bytes of data from the TMP102 sensor.
            //
            byte[] temperatureBuffer = new byte[2];
            I2CDevice.I2CTransaction[] reading = new I2CDevice.I2CTransaction[1];
            reading[0] = I2CDevice.CreateReadTransaction(temperatureBuffer);
            while (true)
            {
                //
                //  Read data from the I2C bus.
                //
                i2cBus.Config = stm8s;
                int bytesRead = i2cBus.Execute(transactions, 100);
                i2cBus.Config = tmp102;
                bytesRead = i2cBus.Execute(reading, 100);
                //
                //  Convert the reading into Centigrade and Fahrenheit.
                //
                int sensorReading = ((temperatureBuffer[0] << 8) | temperatureBuffer[1]) >> 4;
                double centigrade = sensorReading * 0.0625;
                double fahrenheit = centigrade * 1.8 + 32;
                //
                //  Display the readings in the debug window and pause before repeating.
                //
                Debug.Print(centigrade.ToString() + " C / " + fahrenheit.ToString() + " F");
                //
                //  Now display the results of the addition.
                //
                string message = "";
                for (int index = 0; index < buffer.Length; index++)
                {
                    message += buffer[index].ToString();
                    if (index == (buffer.Length - 1))
                    {
                        message += " = " + ((resultBuffer[0] * 256) + resultBuffer[1]).ToString();
                    }
                    else
                    {
                        message += " + ";
                    }
                }
                Debug.Print(message);
                Thread.Sleep(1000);
            }
        }
    }
}

The above code creates two I2C configuration object, one for the TMP102 and one for the STM8S device. The I2C bus object has the configuration changed depending upon which device is required.

Wiring up the Devices

Firstly, wire up the TMP102 sensor as described in the article on I2C master devices. The SDA and SCK lines should be connected to 3.3V via 4K7 resistors. Next connect the SDA and SCK lines from the STM8S device to the same point as SDA and SCK lines from the TMP102 sensor. Throw in the logic analyser connections and you get something like this:

TMP102 and STM8S I2C slave devices

TMP102 and STM8S I2C slave devices

Running the Application

Running the above two applications and starting the logic analyser generated the following output:

Reading From Two I2C Slaves

Reading From Two I2C Slaves

The write operation to the far left (W0x50) sends the 10 bytes to the STM8S device. This is followed by a read (R0x50) of two bytes from the same device, this can be seen about two thirds of the way from the left-hand side of the trace. The final operation is the read of the current temperature, R0x48, to the right of the trace.

Conclusion

The STM8S slave device above is a simple device. There are still plenty of modification which need to be made:

  1. Add error handling
  2. Allow for trapping conditions such as clock stretching

to name but a few. The code above should allow for simple devices to be put together is a short space of time.

STM8S I2C Master Devices

Saturday, May 23rd, 2015

I have finally managed to dig out the TMP102 temperature sensor from the back of the electronic breakout board cupboard. Why am I interested in this sensor, well it is about the only I2C device I actually own and I2C is one of the few areas I have not really looked at on the STM8S.

This article will explore the basics of creating a I2C master device using the STM8S as the bus master and the TMP102 as the slave device.

I2C Protocol

I2C (Inter-Integrated Circuit) is a communication protocol allowing bi-directional communication between two or more devices over two wires. The protocol allows a device to be in one of four modes:

  1. Master transmit
  2. Master receive
  3. Slave transmit
  4. Slave receive

The Wikipedia article contains a good description of the protocol and the various modes and the bus characteristics.

For the purposes of this post the STM8S will need to be in master mode as it will be controlling the communication flow with the temperature sensor which is essentially a dumb device.

Communicating with the TMP102

The TMP102 is a simple device which returns two bytes of data which represent the current reading from a temperature sensor. The process for reading the temperature is as follows:

  1. Master device transmits the slave address on the SDA line along with a bit indicating that it wishes to read data from the slave device
  2. Master device enters Master Receive mode
  3. Slave device transmits two bytes of data representing the temperature
  4. Master device closes down the communication sequence

The temperature can be calculated according to the following formula:

Temperature in centigrade = (((byte0 * 256) + byte1) / 16) * 0.0625

Wiring up the TMP102

The sensor breakout I have has 6 connections of which we will connect five:

Sensor Breakout Pin Connected to
SDA Microcontroller SDA line
SCK Microcontroller SCK
Vcc 3.3 V
GND Ground
ADR0 Ground

This should result in the breakout having an I2C address of 0x48.

Netduino Code

The Netduino 3 runs the .NET Microframework (NETMF). This has built in class for communicating over I2C. A simple application will give a reference point for how the protocol should work.

using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;

namespace TMP102
{
    public class Program
    {
        public static void Main()
        {
            //
            //  Create a new I2C device for the TMP102 on address 0x48 with the clock
            //  running at 50 KHz.
            //
            I2CDevice tmp102 = new I2CDevice(new I2CDevice.Configuration(0x48, 50));
            //
            //  Create a transaction to read two bytes of data from the TMP102 sensor.
            //
            byte[] buffer = new byte[2];
            I2CDevice.I2CTransaction[] reading = new I2CDevice.I2CTransaction[1];
            reading[0] = I2CDevice.CreateReadTransaction(buffer);
            while (true)
            {
                //
                //  Read the temperature.
                //
                int bytesRead = tmp102.Execute(reading, 100);
                //
                //  Convert the reading into Centigrade and Fahrenheit.
                //
                int sensorReading = ((buffer[0] << 8) | buffer[1]) >> 4;
                double centigrade = sensorReading * 0.0625;
                double fahrenheit = centigrade * 1.8 + 32;
                //
                //  Display the readings in the debug window and pause before repeating.
                //
                Debug.Print(centigrade.ToString() + " C / " + fahrenheit.ToString() + " F");
                Thread.Sleep(1000);
            }
        }
    }
}

Running this on the Netduino gives the following output in the debug window:

20.6875 C / 69.237500000000011 F
20.625 C / 69.125 F
20.6875 C / 69.237500000000011 F
20.6875 C / 69.237500000000011 F
20.625 C / 69.125 F
20.6875 C / 69.237500000000011 F
20.625 C / 69.125 F
20.625 C / 69.125 F
20.625 C / 69.125 F
20.625 C / 69.125 F
20.625 C / 69.125 F

Hooking up the Saleae Logic 16 gives the following output for a single reading:

I2C communication with a TMP102 and a Netduino 3

I2C communication with a TMP102 and a Netduino 3

STM8S Implementation

The NETMF class used above hides a lot of the low level work which the STM8S will have to manage. In order to communicate with the TMP102 the STM8S will have to perform the following:

  1. Enter master transmit mode
  2. Send the 7-bit address of the sensor
  3. Send a single bit indicating that we want to read from the sensor
  4. Wait for the slave to respond with an ACK
  5. Enter master receiver mode
  6. Read the bytes from the slave device. Send an ACK signal for all bytes except the last one.
  7. Send a NAK signal at the end of the sequence

The first task is to initialise the I2C system on the STM8S:

//
//  Initialise the I2C system.
//
void InitialiseI2C()
{
    I2C_CR1_PE = 0;                     //  Diable I2C before configuration starts.
    //
    //  Setup the clock information.
    //
    I2C_FREQR = 16;                     //  Set the internal clock frequency (MHz).
    I2C_CCRH_F_S = 0;                   //  I2C running is standard mode.
    I2C_CCRL = 0xa0;                    //  SCL clock speed is 50 KHz.
    I2C_CCRH_CCR = 0x00;
    //
    //  Set the address of this device.
    //
    I2C_OARH_ADDMODE = 0;               //  7 bit address mode.
    I2C_OARH_ADDCONF = 1;               //  Docs say this must always be 1.
    //
    //  Setup the bus characteristics.
    //
    I2C_TRISER = 17;
    //
    //  Turn on the interrupts.
    //
    I2C_ITR_ITBUFEN = 1;                //  Buffer interrupt enabled.
    I2C_ITR_ITEVTEN = 1;                //  Event interrupt enabled.
    I2C_ITR_ITERREN = 1;
    //
    //  Configuration complete so turn the peripheral on.
    //
    I2C_CR1_PE = 1;
    //
    //  Enter master mode.
    //
    I2C_CR2_ACK = 1;
    I2C_CR2_START = 1;
}

Some of the initialisation for the I2C bus needs to be performed whilst the peripheral is disabled, notably the setup of the clock speed. The method above diables the I2C bus, sets up the clock and addressing mode, turns on the interrupts for the peripheral and then enables the I2C bus. Finally, the method sets up the system to transmit ACKs following data reception and then sends the Start bit.

The communication with the slave device is handled by an Interrupt Service Routine (ISR). The initialisation method above will have taken control of the bus and set the start condition. An interrupt will be generated once the start condition has been set.

The master then needs to send the 7-bit address followed by a 1 to indicate the intention to read data from the bus. These two are normally combined into a single byte, the top 7-bits containing the device address and the lower bit indicating the mode (read – 1 or write – 0).

if (I2C_SR1_SB)
{
    //
    //  Master mode, send the address of the peripheral we
    //  are talking to.  Reading SR1 clears the start condition.
    //
    reg = I2C_SR1;
    //
    //  Send the slave address and the read bit.
    //
    I2C_DR = (DEVICE_ADDRESS << 1) | I2C_READ;
    //
    //  Clear the address registers.
    //
    I2C_OARL_ADD = 0;
    I2C_OARH_ADD = 0;
    return;
}

This above code checks the status registers to see if the interrupt has been generated because of a start condition. If it has then the STM8S is setup to send the address of the TMP102 along with the read bit.

Assuming that no error conditions are generated, the next interrupt generated will indicate that the address has been sent to the slave successfully. This condition is dealt with by reading two of the status registers and clearing the address registers:

if (I2C_SR1_ADDR)
{
    //
    //  In master mode, the address has been sent to the slave.
    //  Clear the status registers and wait for some data from the salve.
    //
    reg = I2C_SR1;
    reg = I2C_SR3;
    return;
}

At this point the address has been sent successfully and the I2C peripheral should be ready to start to receive data.

The I2C protocol requires that the data bytes are acknowledged by the master device with an ACK signal. All except for the last data byte, this must be acknowledged with a NAK signal. The I2C_CR2_ACK bit determines if an ACK or NAK is sent following each byte.

The master device can then continue to hold control of the bus or it can send a STOP signal indicating that the flow of communication has ended.

These two conditions are dealt with by resetting the I2C_CR2_ACK bit and setting the I2C_CR2_STOP bit.

The fulll ISR for the I2C bus looks like this:

//
//  I2C interrupts all share the same handler.
//
#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
    if (I2C_SR1_SB)
    {
        //
        //  Master mode, send the address of the peripheral we
        //  are talking to.  Reading SR1 clears the start condition.
        //
        reg = I2C_SR1;
        //
        //  Send the slave address and the read bit.
        //
        I2C_DR = (DEVICE_ADDRESS << 1) | I2C_READ;
        //
        //  Clear the address registers.
        //
        I2C_OARL_ADD = 0;
        I2C_OARH_ADD = 0;
        return;
    }
    if (I2C_SR1_ADDR)
    {
        //
        //  In master mode, the address has been sent to the slave.
        //  Clear the status registers and wait for some data from the salve.
        //
        reg = I2C_SR1;
        reg = I2C_SR3;
        return;
    }
    if (I2C_SR1_RXNE)
    {
        //
        //  The TMP102 temperature sensor returns two bytes of data
        //
        _buffer[_nextByte++] = I2C_DR;
        if (_nextByte == 1)
        {
            I2C_CR2_ACK = 0;
            I2C_CR2_STOP = 1;
        }
        return;
    }
}

Running the Application

The application needs to be rounded out a little in order to read and store the two data bytes in a buffer. Some diagnostics can also be provided by setting one of the ports on the STM8S to output and bit banging the data ready through this port. Add to this some initialisation code and the full application looks as follows:

//
//  This application demonstrates the principles behind developing an
//  I2C master device on the STM8S microcontroller.  The application
//  will read the temperature from a TMP102 I2C sensor.
//
//  This software is provided under the CC BY-SA 3.0 licence.  A
//  copy of this licence can be found at:
//
//  http://creativecommons.org/licenses/by-sa/3.0/legalcode
//
#if defined DISCOVERY
    #include <iostm8S105c6.h>
#else
    #include <iostm8s103f3.h>
#endif
#include <intrinsics.h>

//
//  Define some pins to output diagnostic data.
//
#define PIN_BIT_BANG_DATA       PD_ODR_ODR4
#define PIN_BIT_BANG_CLOCK      PD_ODR_ODR5
#define PIN_ERROR               PD_ODR_ODR6

//
//  I2C device related constants.
//
#define DEVICE_ADDRESS          0x48
#define I2C_READ                1
#define I2C_WRITE               0

//
//  Buffer to hold the I2C data.
//
unsigned char _buffer[2];
int _nextByte = 0;

//
//  Bit bang data on the diagnostic pins.
//
void BitBang(unsigned char byte)
{
    for (short bit = 7; bit >= 0; bit--)
    {
        if (byte & (1 << bit))
        {
            PIN_BIT_BANG_DATA = 1;
        }
        else
        {
            PIN_BIT_BANG_DATA = 0;
        }
        PIN_BIT_BANG_CLOCK = 1;
        __no_operation();
        PIN_BIT_BANG_CLOCK = 0;
    }
    PIN_BIT_BANG_DATA = 0;
}

//
//  Set up the system clock to run at 16MHz using the internal oscillator.
//
void InitialiseSystemClock()
{
    CLK_ICKR = 0;                       //  Reset the Internal Clock Register.
    CLK_ICKR_HSIEN = 1;                 //  Enable the HSI.
    CLK_ECKR = 0;                       //  Disable the external clock.
    while (CLK_ICKR_HSIRDY == 0);       //  Wait for the HSI to be ready for use.
    CLK_CKDIVR = 0;                     //  Ensure the clocks are running at full speed.
    CLK_PCKENR1 = 0xff;                 //  Enable all peripheral clocks.
    CLK_PCKENR2 = 0xff;                 //  Ditto.
    CLK_CCOR = 0;                       //  Turn off CCO.
    CLK_HSITRIMR = 0;                   //  Turn off any HSIU trimming.
    CLK_SWIMCCR = 0;                    //  Set SWIM to run at clock / 2.
    CLK_SWR = 0xe1;                     //  Use HSI as the clock source.
    CLK_SWCR = 0;                       //  Reset the clock switch control register.
    CLK_SWCR_SWEN = 1;                  //  Enable switching.
    while (CLK_SWCR_SWBSY != 0);        //  Pause while the clock switch is busy.
}

//
//  Initialise the I2C system.
//
void InitialiseI2C()
{
    I2C_CR1_PE = 0;                     //  Diable I2C before configuration starts.
    //
    //  Setup the clock information.
    //
    I2C_FREQR = 16;                     //  Set the internal clock frequency (MHz).
    I2C_CCRH_F_S = 0;                   //  I2C running is standard mode.
    I2C_CCRL = 0xa0;                    //  SCL clock speed is 50 KHz.
    I2C_CCRH_CCR = 0x00;
    //
    //  Set the address of this device.
    //
    I2C_OARH_ADDMODE = 0;               //  7 bit address mode.
    I2C_OARH_ADDCONF = 1;               //  Docs say this must always be 1.
    //
    //  Setup the bus characteristics.
    //
    I2C_TRISER = 17;
    //
    //  Turn on the interrupts.
    //
    I2C_ITR_ITBUFEN = 1;                //  Buffer interrupt enabled.
    I2C_ITR_ITEVTEN = 1;                //  Event interrupt enabled.
    I2C_ITR_ITERREN = 1;
    //
    //  Configuration complete so turn the peripheral on.
    //
    I2C_CR1_PE = 1;
    //
    //  Enter master mode.
    //
    I2C_CR2_ACK = 1;
    I2C_CR2_START = 1;
}

//
//  I2C interrupts all share the same handler.
//
#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
    if (I2C_SR1_SB)
    {
        //
        //  Master mode, send the address of the peripheral we
        //  are talking to.  Reading SR1 clears the start condition.
        //
        reg = I2C_SR1;
        //
        //  Send the slave address and the read bit.
        //
        I2C_DR = (DEVICE_ADDRESS << 1) | I2C_READ;
        //
        //  Clear the address registers.
        //
        I2C_OARL_ADD = 0;
        I2C_OARH_ADD = 0;
        return;
    }
    if (I2C_SR1_ADDR)
    {
        //
        //  In master mode, the address has been sent to the slave.
        //  Clear the status registers and wait for some data from the salve.
        //
        reg = I2C_SR1;
        reg = I2C_SR3;
        return;
    }
    if (I2C_SR1_RXNE)
    {
        //
        //  The TMP102 temperature sensor returns two bytes of data
        //
        _buffer[_nextByte++] = I2C_DR;
        if (_nextByte == 1)
        {
            I2C_CR2_ACK = 0;
            I2C_CR2_STOP = 1;
        }
        else
        {
            BitBang(_buffer[0]);
            BitBang(_buffer[1]);
        }
        return;
    }
    //
    //  If we get here then we have an error so clear
    //  the error and continue.
    //
    unsigned char reg = I2C_SR1;
    reg = I2C_SR3;
    //
    //  Send a diagnostic signal to indicate we have cleared 
    //  the error condition.
    //
    PIN_ERROR = 1;
    __no_operation();
    PIN_ERROR = 0;
}

//
//  Main program loop.
//
int main()
{
    __disable_interrupt();
    //
    //  Initialise Port D.
    //
    PD_ODR = 0;             //  All pins are turned off.
    PD_DDR_DDR4 = 1;        //  Port D, bit 4 is output.
    PD_CR1_C14 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C24 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR5 = 1;        //  Port D, bit 5 is output.
    PD_CR1_C15 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C25 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR6 = 1;        //  Port D, bit 6 is output.
    PD_CR1_C16 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C26 = 1;         //  Pin can run up to 10 MHz.
    //
    InitialiseSystemClock();
    InitialiseI2C();
    __enable_interrupt();
    while (1)
    {
        __wait_for_interrupt();
    }
}

Putting this in a project and running on the STM8S gives the following output on the Saleae logic analyser:

I2C Communication between STM8S and a TMP102

I2C Communication between STM8S and a TMP102

The output looks similar to that from the Netduino application above. Breaking out the calculator and using the readings in the above screen shot gives a temperature of 19.6 C which is right according to the thermometer in the room.

Conclusion

The above application shows the basics of a master I2C application. The code needs to be expanded to add some error handling to detect some of the errors that can occur (bus busy, acknowledge failures etc.) but the basics are there.

On to STM8S slave devices…

OpenIR PCB Revision B

Sunday, April 5th, 2015

Time for a small hardware update to the OpenIR project.

  1. Removed the single AND gate from the output as the pulses are now controlled purely by software.
  2. Repositioned the Power LED
  3. Corrected a mistake with the positioning of the battery connections. These were Off by one
  4. Added some additional markings to the board for clarification.

Over to the PCB manufacturer for new boards. In the meantime here are some images.

Schematic

OpenIR PCB Rev B Schematic

OpenIR PCB Rev B Schematic

PCB Layout

OpenIR PCB Rev B

OpenIR PCB Rev B

3d View

OpenIR PCB Rev B 3D Representation

OpenIR PCB Rev B 3D Representation