RSS

Posts Tagged ‘Electronics’

1-Bit Full Adder With Carry

Saturday, November 16th, 2013

Last year I was given the Elektor FPGA board as a present. I had been wanting to work with FPGA as it presented a quick way of prototyping digital circuits and exploring the world of digital electronics. The board has been sitting in my parts collection for about 10 months. I have followed the tutorials in the magazine and decided that VHDL is probably the best way for me to start and use this board.

This post looks at my first attempt to simulate a 1-bit full adder with carry using binary logic implemented in VHDL.

I should also say that this is an early (i.e. inexperienced) project for me.

Software

Elektor recommended the Xilinx WebPack ISE software package for use with their board. It is free to use and all of their tutorials use this software. The version 14.7 download comes in at a heavy 6.17GB so make sure you have a good connection to the web or plenty of time to spare.

Project Specification

For the purpose of this exercise we will be implementing a 1-bit full adder with carry. For those not familiar with this circuit the schematic is as follows:

1-Bit Full Adder Schematic

1-Bit Full Adder Schematic

The circuit takes three bits, A, B and a carry bit. The three bits are added and two bits are output from the adder, the result bit and a carry bit. The logic table for the circuit is:

ABCarry InResultCarry Out
00000
01010
10010
11001
00110
01101
10101
11111

Time for start Webpack ISE.

ISE Project

First thing to do after starting Webpack ISE is to create a new project to hold the circuit. From the startup screen select New Project

ISE Project Navigator Start Up

ISE Project Navigator Start Up

Now give the project a name. I selected BitFullAdderWithCarry. I actually wanted 1-Bit but ISE does not seem to like projects containing non-alpha characters. I provided a short description of the project in the Descriptionbox. Finally ensure that the project source type is set to HDL:

New Project Name And Properties

New Project Name And Properties

The next step is hardware specific. Here we define the target hardware characteristics. For the Elektor board we need to change the following parameters:

  • Device: XC3S250E
  • Package: VQ100
  • Speed: -4
Device Properties

Device Properties

The next form simply shows the project details for review before the project is finally created.

New Project Summary

New Project Summary

The project will now be created and is ready for the addition of files.

Empty Hierarchy

Empty Hierarchy

Right clicking on the device in the Hierarchy brings up the context menu which will allow the addition of new or existing files:

New File Menu

New File Menu

Select New Source option and a wizard will appear. In the New Source Wizard select VHDL Module and add a file name which will hold the source code for the module:

Module Type

Module Type

The next step is to define the ports for the module. The adder has three inputs (a, b and carry in) and two output (result and carry out). This dialog gives the option of defining the ports and their direction:

Specify Ports

Specify Ports

Finally a summary dialog is displayed where you can confirm the module and port information.

New Source Summary

New Source Summary

After clicking on the Finish button the new file will be added to the project along with a template for the module. The new template contains the port definitions entered in the previous dialogs, some comments and space to start entering the code to implement the module.

New Project Source Code

New Project Source Code

I must admit I find it slightly annoying that although VHDL is case insensitive, the system uses mixed case and it not consistent in it’s use. You will see what I mean when we get to testing the code.

A little editing is required to get the code into a form I prefer. It is also time to add the code which implements the hardware for the full adder.

Adder Source Code

Adder Source Code

Once the code has been entered we need to perform a syntax check. Change the view in the top panel from Implementation to Simulation. Now turn your attention to the lower panel. Expand the ISIM Simulator, select Behavioural Check Syntax and click on the green triangle to start the syntax check. After a short while a green tick will appear to indicate that the the check was successful.

Simulate Selected

Simulate Selected

If you right click on the Simulate Behavioral Model entry the system will prepare the implementation for simulation and then launch the simulator.

Simulated Adder

Simulated Adder

At this point there is little to see as we have not defined any simulation parameters. Close the simulator. You will need to do this as there does not appear to be any bidirectional communication between the simulator and the designer. If you simply switch tasks and go back to the designer, make a change and then launch the simulator again you will receive an error message.

Once back in the Designer, add a new source module as before but this time select VHDL Test Bench for the source type.

New Test Module

New Test Module

Next select the source file test test code will be associated with. We only have one file in our project so you must select that.

Associate With Source File

Associate With Source File

As with the previous cases, the next dialog in the wizard presents a summary for us to confirm the options we have selected.

New Test Module Summary

New Test Module Summary

The Hierarchy will now show a new source file inserted between the device name and the source module we had previously created. This new file contains the test code for out module. The original code is also prefixed with uut which stands for Unit Under Test.

Project After Adding Test Module

Project After Adding Test Module

Select the file containing the source code for the test module. This will have the default code for a test module.

--------------------------------------------------------------------------------
-- Company: 
-- Engineer:
--
-- Create Date:   13:01:13 11/16/2013
-- Design Name:   
-- Module Name:   BitFullAdderWithCarry/FullAdderTextModule.vhd
-- Project Name:  BitFullAdderWithCarry
-- Target Device:  
-- Tool versions:  
-- Description:   
-- 
-- VHDL Test Bench Created by ISE for module: BitFullAdderWithCarry
-- 
-- Dependencies:
-- 
-- Revision:
-- Revision 0.01 - File Created
-- Additional Comments:
--
-- Notes: 
-- This testbench has been automatically generated using types std_logic and
-- std_logic_vector for the ports of the unit under test.  Xilinx recommends
-- that these types always be used for the top-level I/O of a design in order
-- to guarantee that the testbench will bind correctly to the post-implementation 
-- simulation model.
--------------------------------------------------------------------------------
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
 
-- Uncomment the following library declaration if using
-- arithmetic functions with Signed or Unsigned values
--USE ieee.numeric_std.ALL;
 
ENTITY FullAdderTextModule IS
END FullAdderTextModule;
 
ARCHITECTURE behavior OF FullAdderTextModule IS 
 
    -- Component Declaration for the Unit Under Test (UUT)
 
    COMPONENT BitFullAdderWithCarry
    PORT(
         a : IN  std_logic;
         b : IN  std_logic;
         cin : IN  std_logic;
         result : OUT  std_logic;
         cout : OUT  std_logic
        );
    END COMPONENT;
    

   --Inputs
   signal a : std_logic := '0';
   signal b : std_logic := '0';
   signal cin : std_logic := '0';

 	--Outputs
   signal result : std_logic;
   signal cout : std_logic;
   -- No clocks detected in port list. Replace <clock> below with 
   -- appropriate port name 
 
   constant <clock>_period : time := 10 ns;
 
BEGIN
 
	-- Instantiate the Unit Under Test (UUT)
   uut: BitFullAdderWithCarry PORT MAP (
          a => a,
          b => b,
          cin => cin,
          result => result,
          cout => cout
        );

   -- Clock process definitions
   <clock>_process :process
   begin
		<clock> <= '0';
		wait for <clock>_period/2;
		<clock> <= '1';
		wait for <clock>_period/2;
   end process;
 

   -- Stimulus process
   stim_proc: process
   begin		
      -- hold reset state for 100 ns.
      wait for 100 ns;	

      wait for <clock>_period*10;

      -- insert stimulus here 

      wait;
   end process;

END;

Remember I mentioned an issue I had with the consistency of the source code generated by the Designer. Check out the keywords in the test module and compare to the template source code for a module. Take the library keyword. In the template module code the keyword is in lower case. In the test module template it is upper case.

The test module contains the component definitions we require to test our module. Note that the module also contains a default clock for the circuits which need it. Editing the file above to remove unused items (and also correct the case to make it consistent with the source module).

--------------------------------------------------------------------------------
-- Company: 
-- Engineer:
--
-- Create Date:   13:01:13 11/16/2013
-- Design Name:   
-- Module Name:   BitFullAdderWithCarry/FullAdderTextModule.vhd
-- Project Name:  BitFullAdderWithCarry
-- Target Device:  
-- Tool versions:  
-- Description:   
-- 
-- VHDL Test Bench Created by ISE for module: BitFullAdderWithCarry
-- 
-- Dependencies:
-- 
-- Revision:
-- Revision 0.01 - File Created
-- Additional Comments:
--
-- Notes: 
-- This testbench has been automatically generated using types std_logic and
-- std_logic_vector for the ports of the unit under test.  Xilinx recommends
-- that these types always be used for the top-level I/O of a design in order
-- to guarantee that the testbench will bind correctly to the post-implementation 
-- simulation model.
--------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.ALL;
 
entity FullAdderTextModule is
end FullAdderTextModule;

architecture behavior of FullAdderTextModule is
    --
    --  Component Declaration for the Unit Under Test (UUT)
    --
    component BitFullAdderWithCarry
    port (
         a : in  std_logic;
         b : in  std_logic;
         cin : in  std_logic;
         result : out  std_logic;
         cout : out  std_logic
        );
    end component;
    --
    --  Inputs
    --
    signal a : std_logic := '0';
    signal b : std_logic := '0';
    signal cin : std_logic := '0';
    --
 	--  Outputs
    --
    signal result : std_logic;
    signal cout : std_logic;
begin
    --
	-- Instantiate the Unit Under Test (UUT)
    --
    uut: BitFullAdderWithCarry port map
        (
            a => a,
            b => b,
            cin => cin,
            result => result,
            cout => cout
        );
    --
    --  Stimulus process
    --
    stim_proc: process
    begin		
        -- hold reset state for 100 ns.
        wait for 100 ns;	
        --
        --  0 + 1, carry in = 0
        --  Result should be 1 with no carry out.
        --
        a <= '0';
        b <= '1';
        cin <= '0';
        wait for 5 ns;
        --
        --  1 + 0, carry in = 0
        --  Result should be 1 with no carry out.
        --
        a <= '1';
        b <= '0';
        cin <= '0';
        wait for 5 ns;
        --
        --  1 + 1, carry in = 0
        --  Result should be 0 with carry out set to 1.
        --
        a <= '1';
        b <= '1';
        cin <= '0';
        wait for 5 ns;
        --
        --  0 + 1, carry in = 1
        --  Result should be 0 with carry out set to 1.
        --
        a <= '0';
        b <= '1';
        cin <= '1';
        wait for 5 ns;
        --
        --  1 + 1, carry in = 1
        --  Result should be 1 with carry out set to 1.
        --
        a <= '1';
        b <= '1';
        cin <= '1';
        wait;
   end process;
end;

The main work in the above code is performed in the stim_proc process. The first thing that happens is to pause. This 100ns pause is recommended by Xilinx to allow the system to stabilise after a reset.

The next code blocks set the input signals to known levels and then pause for 5ns. The pause allows us to examine the outputs in the simulator.

Now we have a test module we should re-run the simulator as before (right click on the Simulate Behavioral Module and select ReRun All). The simulator will launch again.

Simulated Full Adder Zoomed Out

Simulated Full Adder Zoomed Out

Notice that this time the traces to the right showing the signals are green and not brown. This is because the various ports have values assigned to them by the default test template. Previously there were no values assigned to the pins and they were undefined.

The default setting for the simulator is to run the simulation for 1us (1,000,000ps). The trace shows the final part of the simulation. Click on the Zoom to Full icon in the toolbar

Zoom To Full View Icon

to see the results of the fill run.

Full View of Trace

Full View of Trace

Looking at the far left of the trace you can see that all of the inputs are set to zero and the corresponding output levels are also zero. At 100ns the traces start to change as the test code changes the input values to the adder circuit.

Click on the zoom icon to zoom in on the traces. You can also use the scrollbar at the bottom of the trace to move around the output.

Zooming in until you can clearly see the signals between 100ns and 130ns shows the output from the test circuit generated by the code entered in the test module.

Zoomed In View Of Trace

Zoomed In View Of Trace

To the far left of the output window is a yellow cursor line. Grabbing this and dragging the cursor over the traces changes the values displayed for the various pins in the model. So just before 100ns into the simulation the output shows that all of the inputs are zero and so are the corresponding outputs:

97ns Into Simulation

97ns Into Simulation

Moving the cursor through the simulation changes the values. So 102.081ns into the simulation we should have just completed execution of the following code:

--
--  0 + 1, carry in = 0
--  Result should be 1 with no carry out.
--
a <= '0';
b <= '1';
cin <= '0';
wait for 5 ns;

From the truth table presented at the start of this post we should be expecting result to be 1 and carry out to be 0. This is verified by the following output:

10ns Into Simulation

10ns Into Simulation

Similarly, at 107.081ns into the simulation we are executing the following code:

--
--  1 + 0, carry in = 0
--  Result should be 1 with no carry out.
--
a <= '1';
b <= '0';
cin <= '0';
wait for 5 ns;

Again, double checking with the truth table we see that we are expecting result to be 1 and carry out to be 0.

25-107nsIntoSimulation

Once again the trace verifies this.

Deploying to the Board

This article was supposed to end with the deployment of the design to the Elektor FPGA board. The board has two LEDs which can be used for output and we can use jumper leads for the three inputs to the board.

Xilinx Software Problems

All was going well until I started to map the signals to the pins on the board using the Xilinx PlanAhead tool. At this point PlanAhead displayed the banner and then simply quit. The output in the console reported the fact that a log file had been generated. Opening the log file I found this:

****** PlanAhead v14.7 (64-bit)
  **** Build 321239 by xbuild on Fri Sep 27 19:29:51 MDT 2013
    ** Copyright 1986-1999, 2001-2013 Xilinx, Inc. All Rights Reserved.

INFO: [Common 17-78] Attempting to get a license: PlanAhead
INFO: [Common 17-290] Got license for PlanAhead
INFO: [Device 21-36] Loading parts and site information from C:/Xilinx/14.7/ISE_DS/PlanAhead/data/parts/arch.xml
Parsing RTL primitives file [C:/Xilinx/14.7/ISE_DS/PlanAhead/data/parts/xilinx/rtl/prims/rtl_prims.xml]
Finished parsing RTL primitives file [C:/Xilinx/14.7/ISE_DS/PlanAhead/data/parts/xilinx/rtl/prims/rtl_prims.xml]
start_gui
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x000007fa87e21355, pid=64, tid=8124
#
# JRE version: 7.0_17-b02
# Java VM: Java HotSpot(TM) 64-Bit Server VM (23.7-b01 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# C  [msvcrt.dll+0x1355]  free_dbg+0x5
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# E:\MarksDocs\Circuits\Xilinx FPGA\BitFullAdderWithCarry\hs_err_pid64.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.sun.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#

Problems Solved

I have had the PlanAhead tool working in the past as I had completed the exercises in the Elektor magazines in the past. Time for some investigation. After several hours, and I do mean several hours, I uninstalled the 14.7 version of the tools and downloaded the 14.3 version from October 2012. Interestingly, this fixed the problem and PlanAhead launched as expected. So, on with the exercise.

On With the Exercise

The next step is to deploy the full-adder to the Elektor board connecting the ports to the pins on the board. We should be able to use jumper cables to represent the inputs to the full adder. The board contains two LEDs which we can use to show the output from the adder.

Firstly, we switch back to the Implementation view, expand Synthesise – XST and click on the green triangle to start the synthesis. This took a minute or so on my machine so expect to wait for the process to complete.

Getting Ready For Implementation

Getting Ready For Implementation

From the green tick in the processes panel above you can see that the synthesis completed with no errors.

The next step is to assign pins on the board to the input and output signals.

Double click on I/O Planning (PlanAhead) Post-Synthesis item in the User Constraints treeview. You will be presented with the following window:

User Constraints File Question

User Constraints File Question

Answer Yes to this question. After a while the PlanAhead tool should appear.

Plan Ahead Start Up

Plan Ahead Start Up

We now need to start assigning the pins on the board to the inputs/outputs in the design. So click on the Scalar Ports treeview item in the lower lefthand corner of the PlanAhead tool. PlanAhead will now show the inputs and outputs from the model.

Plan Ahead Left Hand Panel

Plan Ahead Left Hand Panel

We can not start to assign the ports used in the model to the physical pins on the chip (and hence the board). The Elektor board has two LEDs which are connected to pins 90 and 91 of the FPGA. So for the two output rows set the entry in the Site column to 90 and 91. The entry in the I/O Std column should be set to LVCMOS33 for all of the pins.

IO Ports Expanded

IO Ports Expanded

Once the pins have been selected and assigned, save the mapping in the PlanAhead tool and return to the Designer. Select Implement Design and either right click and select Run or click on the green triangle. After a short time the system should indicate that the implementation has completed without error:

Implement Design

Implement Design

The final step is to generate the file which will be used by the FPGA. This is achieved by selecting Generate Programming File in the Designer, right clicking and selecting Process properties

Process Properties

Process Properties

This will present the Process Properties dialog.

Process Properties Dialog

Process Properties Dialog

Ensure that the following options are checked in the Process Properties dialog:

CategoryOptionValue
General OptionsCreate Binary Configuration FileTick this option
Configuration OptionsConfiguration Pin ProgramFloat
Configuration OptionsUnused IOB PinsFloat
Startup OptionsDrive Done Pin HighTick this option

Select OK and return to the Designer. You can now right click on the Generate Programming File entry in the Designer and select Run.

At the end of the process you will find a new file has been placed in the project directory, in this case bitfulladderwithcarry.bin.

We are now ready to transfer the file to the FPGA board. According to the Elektor specs this is a simple process of connecting the board to the PC via USB and a new drive should appear in the file explorer. I must admit I have had intermittent success with this. I have found the most reliable way of transferring the file to the FPGA board is to insert the SD card into the PC and copy the bin file to the SD card. Once the file has been copied it must be renamed to config.bin. Next plug the SD card into the FPGA board and power the board (I used USB for this example).

Testing

In order to test the application you will need to power the board and then connect a,b and carry in to the appropriate signal levels and observe the LED output.

Grounding pins 7, 8 and 9 on the board should turn off both of the LEDs. In fact it turns both of the LEDs on.

a=0,b=0,cin=0

a=0,b=0,cin=0

Connecting all of the pins to a high signal should turn both LEDs on.

a=1,b=1,cin=1

a=1,b=1,cin=1

As you can see, it turns the LEDs off. I looks like the signals are being interpreted in reverse (negated). Testing with a=0, b=0, cin=1 gives the following:

a=0,b=0,cin=1

a=0,b=0,cin=1

The basic logic appears to be working although it is negated. A little more investigation is required.

Conclusion

It would have been nice to have completed the post with by deploying this to the FPGA board. Unfortunately to tools have let me down. Watch this space for future posts which will hopefully see this module deployed to the FPGA board.

The software swings between being amazing and buggy.

  • The simulator is quick and easy to use although this comment comes from my novice perspective.
  • The 64-bit editor quits every now and then when you try to copy text to the clip board. The 32-bit editor does not suffer from this problem.
  • I have had a problem every now and then adding a new file to a project using the 64-bit Designer. Again, the 32-bit version of the tools do not have this problem.
  • You cannot launch the simulator from the 32-bit tools. This works fine with the 64-bit tools.

This is the first step on the journey, a journey which I feel will take a while.

Update 17 Nov 2013

Version 14.7 of the software demonstrated the fact that new versions are not always better. In fact 14.7 proved to be a huge waste of time for me. I spent several hours trying to work out why PlanAhead didi not work only to revert back to version 14.3 of the software which looks to be working well.

Some future work:

  • Create a more convenient pin diagram for the Elektor board.
  • Follow up on the negated signals.

Work for another day I feel.

Nikon D70 remote Control PCB Assembly

Sunday, November 10th, 2013

A few days ago the bare PCBs arrived from China:

Nikon D70 Bare Boards

Nikon D70 Bare Boards

Work commitments meant that assembly has had to wait a while. I finally managed to get to put the boards together today. There is certainly a difference in size between the proto-board and the final (well nearly) PCB.

Proto-board and Assembled Board With Ruler

Proto-board and Assembled Board With Ruler

I’ve documented the assembly process of SMD boards in the past so in this post I’ll just be documenting the lessons learned from this assembly

Measure Twice, Cut Once

A carpenter friend of mine passed on this advice and it certainly rings true on this build. If you look at the back of the board there are a couple of options for connecting power to the board. The intention was to allow the board to be powered by 2 x AA batteries or a CR2032 coin cell. The connection points were supposed to be placed to allow the use of two battery holders I had purchased from Bitsbox. The measurements are a classic "off by one" case. The connections are both 2.54mm off.

Next time I’ll be double checking the footprint of the components.

Solder Paste is Opaque

I originally tried to apply solder paste to the pads for the STM8S and then position the chip on the pads. The theory is great but in practice the positioning is difficult as you cannot see the pads through the paste. Instead I found it easier to apply flux and then tack down one pin on he STM8S. This allowed the positioning of the pads and legs of the STM8S to be checked. Once I was happy with the alignment of the two I applied solder paste to the top of the legs on the chip and then heated the pins.

0402 Components are Small

A few of the components are small, very small. Most of these do not require any orientation but the LED indicating that the power is applied is small and does require a particular orientation. I found a cheap USB microscope useful to help ensure the orientation was correct.

Conclusion

I always forget how small some of these components are but with a little practice you can work with surface mount components. The current board looks like this:

Assembled Board With Ruler

Assembled Board With Ruler

The Nikon D70 infra-red remote control prototype still triggers the camera using the trigger button. The next stage is to work on the software allowing the trigger of the remote control using the UART either by the FTDI and the RedBearBLE mini.

Nikon D70 Remote Control with a Button and EEPROM

Thursday, September 12th, 2013

A remote control which can only generate a signal when initially powered up is of limited use. The addition of a switch will allow the user to determine when the camera is triggered.

Adding a Button

The initial versions of the infra-red remote control for the Nikon D70 which have been discussed so far are limited in that the control sequence is transmitted when the STM8S starts and never again until the next time the STM8S is restarted/reset. Adding a button to the remote control will allow the user to select when the control sequence is transmitted.

One of the main problems with switches is that they are subject to switch bounce. You can see an example of this in the post regarding External Interrupts on the STM8S. There are two approaches to switch debouncing, software and hardware. The button on the infra-red remote control will be triggering a sequence of pulses which will typically last several milliseconds. This means that it lends itself to software debouncing using a simple state machine.

The state machine will put the infra-red remote control into two states, waiting for user input and running. When waiting for user input the interrupt handler for the switch will accept user input. When in the running mode the system will be generating the infra-red output for the camera and it will ignore any input from the switch. At the end of the infra-red sequence the switch input will be re-enabled.

So much for the theory, lets have a look at the code. The first thing which the application will need to do is to setup the appropriate pin as an interrupt port:

//--------------------------------------------------------------------------------
//
//  Now set up the ports.
//
//  PD3 - IR Pulse signal.
//  PD4 - Input pin indicating that the user wishes to trigger the camera.
//
void SetupPorts()
{
    PD_ODR = 0;             //  All pins are turned off.
    //
    //  PD3 is the output for the IR control.
    //
    PD_DDR_DDR3 = 1;
    PD_CR1_C13 = 1;
    PD_CR2_C23 = 1;
    //
    //  Now configure the input pin.
    //
    PD_DDR_DDR4 = 0;        //  PD4 is input.
    PD_CR1_C14 = 1;         //  PD4 is floating input.
    PD_CR2_C24 = 1;
    //
    //  Set up the interrupt.
    //
    EXTI_CR1_PDIS = 1;      //  Interrupt on rising edge.
    EXTI_CR2_TLIS = 1;      //  Rising edge only.
}

The modifications to the SetupPorts method keeps the infra-red output on PD3 but adds an input on PD4 with a rising edge interrupt.

The next step is to deal with the interrupt on PD4. Here we need to work out if we are waiting for an interrupt and if we are then the application needs to start the generation of the infra-red signal. If we are not waiting for a button press then we should ignore the user request as it is likely to be a result of switch bounce. The code starts to look like this:

//--------------------------------------------------------------------------------
//
//  Process the interrupt generated by the pressing of the button.
//
//  This ISR makes the assumption that we only have on incoming interrupt on Port D.
//
#pragma vector = 8
__interrupt void EXTI_PORTD_IRQHandler(void)
{
    if (_currentState != STATE_RUNNING)
    {
        //
        //  Set everything up ready for the timers.
        //
        
        //  TODO!
        
        //
        //  Now we have everything ready we need to force the Timer 2 counters to
        //  reload and enable Timers 1 & 2.
        //
        TIM2_CR1_URS = 1;
        TIM2_EGR_UG = 1;
        TIM1_CR1_CEN = 1;
        TIM2_CR1_CEN = 1;
    }
}

The exact code required for setting up the timer will be revealed following the EEPROM section of this post.

Using EEPROM

In the post Storing Data in the EEPROM on the STM8S we saw how we could save data into the EEPROM of the STM8S for later retrieval. The data used in the example should look familiar if you have been following this series on the Nikon D70 Remote Control as it is the timing and signal data which triggers the Nikon D70.

The first thing we should do is to modify the order in which the data is written into the EEPROM. In the above post, the timing data is written into the EEPROM low byte followed by high byte. By swapping the order we can store the data in the order required by the interrupt for Timer 2. So the first task is to modify the application which wrote and verified the timing information to the following:

//
//  Write a series of bytes to the EEPROM of the STM8S105C6 and then
//  verify that the data has been written correctly.
//
//  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

//
//  Data to write into the EEPROM.
//
unsigned int _pulseLength[] = { 2000U, 27830U, 400U, 1580U, 400U, 3580U, 400U };
unsigned char _onOrOff[] =    {   1,      0,     1,     0,    1,     0,    1 };
char numberOfValues = 7;

//--------------------------------------------------------------------------------
//
//  Write the default values into EEPROM.
//
void SetDefaultValues()
{
    //
    //  Check if the EEPROM is write-protected.  If it is then unlock the EEPROM.
    //
    if (FLASH_IAPSR_DUL == 0)
    {
        FLASH_DUKR = 0xae;
        FLASH_DUKR = 0x56;
    }
    //
    //  Write the data to the EEPROM.
    //
    char *address = (char *) 0x4000;        //  EEPROM base address.
    *address++ = (char) numberOfValues;
    for (int index = 0; index < numberOfValues; index++)
    {
        *address++ = (char) ((_pulseLength[index] >> 8) & 0xff);
        *address++ = (char) (_pulseLength[index] & 0xff);
        *address++ = _onOrOff[index];
    }
    //
    //  Now write protect the EEPROM.
    //
    FLASH_IAPSR_DUL = 0;
}

//--------------------------------------------------------------------------------
//
//  Verify that the data in the EEPROM is the same as the data we
//  wrote originally.
//
void VerifyEEPROMData()
{
    PD_ODR_ODR2 = 1;            //  Checking the data
    PD_ODR_ODR3 = 0;            //  No errors.
    //
    char *address = (char *) 0x4000;        //  EEPROM base address.
    if (*address++ != numberOfValues)
    {
        PD_ODR_ODR3 = 1;
    }
    else
    {
        for (int index = 0; index < numberOfValues; index++)
        {
            unsigned int value = (*address++ << 8);
            value += *address++;
            if (value != _pulseLength[index])
            {
                PD_ODR_ODR3 = 1;
            }
            if (*address++ != _onOrOff[index])
            {
                PD_ODR_ODR3 = 1;
            }
        }
    }
    PD_ODR_ODR2 = 0;        // Finished processing.
}

//--------------------------------------------------------------------------------
//
//  Setup port D for data output.
//
void SetupPorts()
{
    //
    //  Initialise Port D.
    //
    PD_ODR = 0;             //  All pins are turned off.
    PD_DDR = 0xff;          //  All bits are output.
    PD_CR1 = 0xff;          //  All pins are Push-Pull mode.
    PD_CR2 = 0xff;          //  Pins can run up to 10 MHz.
}

//--------------------------------------------------------------------------------
//
//  Main program loop.
//
void main()
{
    SetupPorts();
    SetDefaultValues();
    VerifyEEPROMData();
}

Create a new project for the above and execute the program on the STM8S Discovery board. This should set up the timing data ready for use to use.

Using the EEPROM Data

The above application has been tailored to write the byte data into the EEPROM in the order in which the bytes are required by the remote control application. Using this data should be a simple case of setting a pointer to the first byte and then consuming the bytes one after another. To do this we will need some pointers and a counter:

//
//  Define where we will be working in the EEPROM.
//
#define EEPROM_BASE_ADDRESS         0x4000
#define EEPROM_INITIAL_OFFSET       0x0000
#define EEPROM_DATA_START           (EEPROM_BASE_ADDRESS + EEPROM_INITIAL_OFFSET)

//
//  Data ready for the pulse timer ISR's to use.
//
int _numberOfPulses = 0;
int _currentPulse = 0;
char *_pulseDataAddress = NULL;

As part of the initialisation process we will need to set the pointer and also the number of pulses we have data for:

_pulseDataAddress = (char *) EEPROM_DATA_START;
_numberOfPulses = *_pulseDataAddress++;

So now we should have the initial state configured and we should revisited the button handler method. This method kicks off the timers by consuming the first three bytes of data:

//--------------------------------------------------------------------------------
//
//  Process the interrupt generated by the pressing of the button.
//
//  This ISR makes the assumption that we only have on incoming interrupt on Port D.
//
#pragma vector = 8
__interrupt void EXTI_PORTD_IRQHandler(void)
{
    if (_currentState != STATE_RUNNING)
    {
        //
        //  Set everything up ready for the timers.
        //
        _currentState = STATE_RUNNING;
        _currentPulse = 0;
        _pulseDataAddress = (char *) (EEPROM_DATA_START + 1);
        TIM2_ARRH = *_pulseDataAddress++;
        TIM2_ARRL = *_pulseDataAddress++;
        PD_ODR_ODR3 = *_pulseDataAddress++;
        //
        //  Now we have everything ready we need to force the Timer 2 counters to
        //  reload and enable Timers 1 & 2.
        //
        TIM2_CR1_URS = 1;
        TIM2_EGR_UG = 1;
        TIM1_CR1_CEN = 1;
        TIM2_CR1_CEN = 1;
    }
}

Finally, the interrupt for the Timer 2 interrupt needs to be modified in order to continue to process the data three bytes at a time until we have reached the total number of pulses:

//--------------------------------------------------------------------------------
//
//  Timer 2 Overflow handler.
//
#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
    _currentPulse++;
    if (_currentPulse == _numberOfPulses)
    {
        //
        //  We have processed the pulse data so stop now.
        //
        PD_ODR_ODR3 = 0;
        TIM2_CR1_CEN = 0;
        TIM1_CR1_CEN = 0;           //  Stop Timer 1.
        _currentState = STATE_WAITING_FOR_USER;
    }
    else
    {
        TIM2_ARRH = *_pulseDataAddress++;
        TIM2_ARRL = *_pulseDataAddress++;
        PD_ODR_ODR3 = *_pulseDataAddress++;
        TIM2_CR1_URS = 1;
        TIM2_EGR_UG = 1;
    }
    TIM2_SR1_UIF = 0;               //  Reset the interrupt otherwise it will fire again straight away.
}

Conclusion

The addition of the button certainly makes the remote control more usable as the user can elect when to trigger the camera. At this stage, the EEPROM does not offer too many advantages over the use of the static data but it can allow the remote control to be fine-tuned at a later stage.

The acid test, does it still trigger the camera – Yes it does.

Storing Data in EEPROM on the STM8S

Thursday, September 12th, 2013

During a recent project it became desirable to store a small amount of data in some non-volatile memory in order that the system state could be restored following loss of power. This article demonstrates how to achieve this by writing a small amount of data to the data EEPROM of the STM8S105Cr micro-controller on the STM8S Discovery board.

Memory Layout, Access and Protection

The Data EEPROM area of the STM8S series of micro-controllers varies depending upon the specific unit being used. For this article the specific micro-controller being used is the STM8S105C6 on the STM8S Discovery board. This micro-controller has 1KByte of EEPROM in the address range 0x4000 – 0x43ff.

By default the data area is write protected and cannot be modified by the main program. The write protection is removed by using a key to unlock the EEPROM data area. The flash program area is similarly protected but we will only consider the data EEPROM area. In order to write to the EEPROM area the application will need to write two security keys called Memory Access Security System (MASS) keys to the FLASH_DUKR register. These keys will unlock the EEPROM area and allow the application to write data to the EEPROM until the application turns write protection back on.

The MASS keys for the data EEPROM area are:

  • 0xae
  • 0x56
  • The algorithm for enabling write access to the EEPROM data area is as follows:

    • Check the DUL bit of FLASH_IASPR. If this bit is set then the data area is writeable and no further action is required.
    • Write the first MASS key (0xae) to the FLASH_DUKR register.
    • Write the second MASS key (0x56) to the FLASH_DUKR register.

    At the end of the process of successfully writing the MASS keys the DUL bit of the FLASH_IASPR register will be set. This bit will remain set until either the application changes the bit or the micro-controller is reset. Resetting the DUL bit programatically reinstates the write protection for the EEPROM memory.

    Read-while-write (RWW)

    This feature is not available on all of the STM8S family of processors and you should consult the data sheet for the unit being used if you are interested in using this feature. The RWW feature allows the program memory to be read whilst the EEPROM memory is being written to.

    Byte Programming

    Byte level programming is available for both the program memory and the EEPROM memory. To use this feature the application simply needs to unlock the EEPROM using the MASS keys and then write individual bytes into the EEPROM memory. To erase a byte simply write 0x00 into the memory location.

    Word and Page Programming

    The STM8S also allows word (4 bytes) and block programming. Both of these are faster than byte programming and block programming is faster than word programming. These features will not be discussed further here and are mentioned simply for awareness.

    Interrupts

    The system can be configured to generate an interrupt for the following event:

    • Successful write operation.
    • Successful erase operation.
    • Illegal operation (writing to protected pages).

    Interrupts are enabled by setting FLASH_CR1_IE to 1. When this bit is set, an interrupt will be generated when the FLASH_IASPR_EOP or FLASH_IASPR_WR_PG_DIS bits are set.

    Software

    The original aim of the software was to write a small amount of data to the data EEPROM of the STM8S and use the ST Visual Programmer to verify that the memory contents had changed. As the project progressed it became apparent that we would also need some code to verify the data.

    Writing the Data

    The application to write data to the EEPROM is relatively simple:

    //
    //  Write a series of bytes to the EEPROM of the STM8S105C6.
    //
    //  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
    
    //
    //  Data to write into the EEPROM.
    //
    unsigned int _pulseLength[] = { 2000U, 27830U, 400U, 1580U, 400U, 3580U, 400U };
    unsigned char _onOrOff[] =    {   1,      0,     1,     0,    1,     0,    1 };
    char numberOfValues = 7;
    
    //--------------------------------------------------------------------------------
    //
    //  Write the default values into EEPROM.
    //
    void SetDefaultValues()
    {
        //
        //  Check if the EEPROM is write-protected.  If it is then unlock the EEPROM.
        //
        if (FLASH_IAPSR_DUL == 0)
        {
            FLASH_DUKR = 0xae;
            FLASH_DUKR = 0x56;
        }
        //
        //  Write the data to the EEPROM.
        //
        char *address = (char *) 0x4000;        //  EEPROM base address.
        *address++ = (char) numberOfValues;
        for (int index = 0; index < numberOfValues; index++)
        {
            *address++ = (char) (_pulseLength[index] & 0xff);
            *address++ = (char) ((_pulseLength[index] >> 8) & 0xff);
            *address++ = _onOrOff[index];
        }
        //
        //  Now write protect the EEPROM.
        //
        FLASH_IAPSR_DUL = 0;
    }
    
    //--------------------------------------------------------------------------------
    //
    //  Main program loop.
    //
    void main()
    {
        SetDefaultValues();
    }
    

    The application simply enables writing to the EEPROM and then writes data to the memory. It also re-enables the write protection at the end of the write operation.

    Verifying the Data

    Testing this application should simply be a case of creating a new project, putting the above in main.c, setting some options and then running the code. The EEPROM data can then be read by ST Visual Develop and verified by hand. After compiling and executing the above code, start ST visual Programmer, connect it to the STM8S Discovery board and download the contents of the EEPROM:

    This does not look correct. Double checking the code against RM0016 – Reference Manual all looks good with the application. So try downloading the EEPROM data again:

    This time the data looks good and the values appear to be correct.

    Downloading the EEPROM data again gave the first set of results. Trying for a fourth thime gave the second set of results. It appears that the correct data is only retrieved every second attempt (for reference, I am using ST Visual Develop version 3.2.8 on Windows 8).

    At this point I decided that the only way to ensure that the data is in fact correct is to write a verification method into the code. The new application becomes:

    //
    //  Write a series of bytes to the EEPROM of the STM8S105C6 and then
    //  verify that the data has been written correctly.
    //
    //  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
    
    //
    //  Data to write into the EEPROM.
    //
    unsigned int _pulseLength[] = { 2000U, 27830U, 400U, 1580U, 400U, 3580U, 400U };
    unsigned char _onOrOff[] =    {   1,      0,     1,     0,    1,     0,    1 };
    char numberOfValues = 7;
    
    //--------------------------------------------------------------------------------
    //
    //  Write the default values into EEPROM.
    //
    void SetDefaultValues()
    {
        //
        //  Check if the EEPROM is write-protected.  If it is then unlock the EEPROM.
        //
        if (FLASH_IAPSR_DUL == 0)
        {
            FLASH_DUKR = 0xae;
            FLASH_DUKR = 0x56;
        }
        //
        //  Write the data to the EEPROM.
        //
        char *address = (char *) 0x4000;        //  EEPROM base address.
        *address++ = (char) numberOfValues;
        for (int index = 0; index < numberOfValues; index++)
        {
            *address++ = (char) (_pulseLength[index] & 0xff);
            *address++ = (char) ((_pulseLength[index] >> 8) & 0xff);
            *address++ = _onOrOff[index];
        }
        //
        //  Now write protect the EEPROM.
        //
        FLASH_IAPSR_DUL = 0;
    }
    
    //--------------------------------------------------------------------------------
    //
    //  Verify that the data in the EEPROM is the same as the data we
    //  wrote originally.
    //
    void VerifyEEPROMData()
    {
        PD_ODR_ODR2 = 1;            //  Checking the data
        PD_ODR_ODR3 = 0;            //  No errors.
        //
        char *address = (char *) 0x4000;        //  EEPROM base address.
        if (*address++ != numberOfValues)
        {
            PD_ODR_ODR3 = 1;
        }
        else
        {
            for (int index = 0; index < numberOfValues; index++)
            {
                unsigned int value = *address++;
                value += (*address++ << 8);
                if (value != _pulseLength[index])
                {
                    PD_ODR_ODR3 = 1;
                }
                if (*address++ != _onOrOff[index])
                {
                    PD_ODR_ODR3 = 1;
                }
            }
        }
        PD_ODR_ODR2 = 0;        // Finished processing.
    }
    
    //--------------------------------------------------------------------------------
    //
    //  Setup port D for data output.
    //
    void SetupPorts()
    {
        //
        //  Initialise Port D.
        //
        PD_ODR = 0;             //  All pins are turned off.
        PD_DDR = 0xff;          //  All bits are output.
        PD_CR1 = 0xff;          //  All pins are Push-Pull mode.
        PD_CR2 = 0xff;          //  Pins can run up to 10 MHz.
    }
    
    //--------------------------------------------------------------------------------
    //
    //  Main program loop.
    //
    void main()
    {
        SetupPorts();
        SetDefaultValues();
        VerifyEEPROMData();
    }
    

    The application uses Port D, pins 2 and 3 to indicate how the verification is proceeding. Pin D2 goes high when the application is verifying the data. Pin D3 is used to indicate if an error is found. Compiling the above and connecting up a scope gives the following output:

    Pin D2 is connected to the yellow channel and pin D3 is connected to the blue channel. The above shows that the verification process starts and no errors are generated.

    Conclusion

    There may only be a small amount of EEPROM storage space available on the STM8S (640 bytes to be precise) but this offers a quick and simple method of storing data which may be needed between system resets/power loses.

    In it’s simplest form, the code required to store the data is trivial only requiring the developer to enable the write operations and then disable after the data has been written successfully.

    In addition to the above I would recommend that some form of checksum value is written into the EEPROM as it is possible that the power is lost as the data is being written into the EEPROM. In this case there are two arrays being written and we may only have written half of the data when the power is lost. This is left as an exercise for the reader.

Modulated Nikon D70 Remote Control Signal

Sunday, September 8th, 2013

A quiet Sunday here in the North of England so I decided to add the 38.4KHz modulation to the Nikon D70 Infra-red Remote Control.

I considered two methods of modulating the signal:

  • Software implementation using a timer
  • Hardware implementation using PWM

The software implementation is attractive as it does not require the addition of any additional components to the circuit and hence reduces the cost of the remote control. On the downside, this requires a slightly more complex implementation and may cause some issues due to the timing of the interrupts.

Using PWM is a much simpler software solution as it only requires that the timers are setup correctly and turned on at the right time.

How Does Modulation Work?

Modulation works by combining a clock frequency (in the case of infra-red this is normally around 38KHz – 40 KHz) with a digital signal. When the digital signal is supposed to be at logic 1 then the clock signal is output rather than a stable logic level 1. When the signal is at logic level 0 then no signal is generated. The following illustrates this:

Digital signal:

Digital Signal

Digital Signal

Clock signal:

38.4KHz Clock Signal

38.4KHz Clock Signal

Combined output:

Digital Signal And Clock

Digital Signal And Clock

In the final image above, the top trace shows the clock signal, the middle trace shows the digital signal we wish to generate and the lower trace shows the signal which should be output by the circuit.

Hardware Changes

The hardware solution requires the combination of a digital signal with a PWM signal. The easiest way to do this is to use a single AND gate taking input from the digital output required and a clock signal.

Nikon Remote Circuit With Added Modulation

Nikon Remote Circuit With Added Modulation

Searching the RS Components web site lead to a single and gate component for a small price. This would be ideal for solving this problem.

Software Changes

The software changes are minimal as we simply need to configure a timer and turning it on and off as required. We will be using Timer 1, Channel 4 configured to generate a 38.4KHz PWM signal.

Setup

A 38.4KHz signal has a peak to peak duration of 26uS The system is running at 2MHz and so we would need a count value of 52 clock pulses (with no prescalar applied). Using Timer 2, Channel 4 results in the following setup code:

//--------------------------------------------------------------------------------
//
//  Set up Timer 1, channel 4 to output a single pulse lasting 240 uS.
//
void SetupTimer1()
{
    TIM1_ARRH = 0x00;       //  Reload counter = 51
    TIM1_ARRL = 0x33;
    TIM1_PSCRH = 0;         //  Prescalar = 0 (i.e. 1)
    TIM1_PSCRL = 0;
    //
    //  Now configure Timer 1, channel 4.
    //
    TIM1_CCMR4_OC4M = 7;    //  Set up to use PWM mode 2.
    TIM1_CCER2_CC4E = 1;    //  Output is enabled.
    TIM1_CCER2_CC4P = 0;    //  Active is defined as high.
    TIM1_CCR4H = 0x00;      //  26 = 50% duty cycle (based on TIM1_ARR).
    TIM1_CCR4L = 0x1a;
    TIM1_BKR_MOE = 1;       //  Enable the main output.
}

Timer 2 Interrupt Handler

A minor change to the Timer 2 interrupt handler is required to turn off Timer 1 when the signal is no longer being generated:

//--------------------------------------------------------------------------------
//
//  Timer 2 Overflow handler.
//
#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
    _currentPulse++;
    if (_currentPulse == _numberOfPulses)
    {
        //
        //  We have processed the pulse data so stop now.
        //
        PD_ODR_ODR3 = 0;
        TIM2_CR1_CEN = 0;
        TIM1_CR1_CEN = 0;           //  Stop Timer 1.
    }
    else
    {
        TIM2_ARRH = _counterHighBytes[_currentPulse];
        TIM2_ARRL = _counterLowBytes[_currentPulse];
        PD_ODR_ODR3 = _outputValue[_currentPulse];
        TIM2_CR1_URS = 1;
        TIM2_EGR_UG = 1;
    }
    TIM2_SR1_UIF = 0;               //  Reset the interrupt otherwise it will fire again straight away.
}

Main Loop

The final change is to the main program loop. This needs to start Timer 1 when the application starts to output data:

//--------------------------------------------------------------------------------
//
//  Main program loop.
//
void main()
{
    unsigned int pulseLength[] = { 2000U, 27830U, 400U, 1580U, 400U, 3580U, 400U };
    unsigned char onOrOff[] =    {   1,      0,     1,     0,    1,     0,    1 };
    PrepareCounterData(pulseLength, onOrOff, 7);
    __disable_interrupt();
    SetupTimer2();
    SetupTimer1();
    SetupOutputPorts();
    __enable_interrupt();
    PD_ODR_ODR3 = _outputValue[0];
    //
    //  Now we have everything ready we need to force the Timer 2 counters to
    //  reload and enable Timer 2.
    //
    TIM2_CR1_URS = 1;
    TIM2_EGR_UG = 1;
    TIM2_CR1_CEN = 1;
    TIM1_CR1_CEN = 1;       // Start Timer 1
    while (1)
    {
        __wait_for_interrupt();
    }
}

Conclusion

Connecting the logic analyser to the circuit will allow the examination of the three signals, namely, the digital signal required (centre trace), the clock (upper trace) and the modulated output (lower trace):

Modulated IR Signal

Modulated IR Signal

The solid white blocks in the clock and modulated traces show a high density of signals. Zooming in on the right hand side of the capture shows the following:

Modulated IR Signal Zoom View

Modulated IR Signal Zoom View

As you can see, the modulated output is composed of a series of 38.4KHz clock pulses which are only generated when the digital signal should be high (logic 1). The remainder of the time the trace shows no output.

The final test is to see if this will trigger the camera, and yes, it still does.

Nikon D70 Infra-red Control

Saturday, September 7th, 2013

A few weeks ago I started to investigate infra-red transmitters with the intention of looking at implementing a remote control for my DSLR. The first post established the fundamentals by creating a low power transmitter and a receiver. This post takes this one step further and attempts to trigger the Nikon D70 DSLR under the control of a microcontroller.

Background

The Nikon D70 uses the ML-L3 infra-red remote control to trigger the camera. I chose this experiment as a first step to a more advanced remote control for my camera. The experiment builds upon the previous work with the STM8S and infra-red signals to trigger the DSLR.

The remote sequence required to trigger the Nikon D70 has been investigated before. I found the post by Michelle Bighignoli to be the most helpful for this particular exercise.

According to Michelle’s web site, the sequence of pulses required to trigger the camera is as follows:

  • High pulse for 2000uS
  • Low for 27830uS
  • High for 400uS
  • Low for 1580us
  • High for 400uS
  • Low for 3580uS
  • High for 400uS

The pulse is also modulated at a frequency of 38.4KHz.

Hardware

The hardware setup is going to be relatively simple. A basic STM8S circuit is connected to the IR Transmitter circuit using port D3 on the STM8S:

Nikon Infra-red Control Circuit

Nikon Infra-red Control Circuit

You can find out more information about both of these circuits by having a look at the following posts:

All that needs to be done is to put this together on breadboard and setup the STM8S ready for programming.

Software

The initial version of the software will simply emit the infra-red pulse sequence as soon as it powers up as I am only looking at a proof of concept at this stage. We will not consider the topic of modulation at this stage.

Looking at the pulse sequence, the application will need to be able to generate infra-red pulses with control down to the micro-second level. The pulse widths are reasonably large, the smallest is 400uS. Taking this into consideration, the default clock speed of 2MHz will be used for the initial version of the application.

The most obvious way of controlling the pulse widths is to use one of the built in system timers. The most obvious choice is to use Timer 2 as we are going to be using the default clock speed and only require an accuracy of a micro-second or so. This will give a count in the range 0-65535 implying that the maximum pulse width will be 65,535uS assuming we use a prescalar of 2 to divide down the clock frequency used by Timer 2 to 1MHz.

The timers use two eight bit values to control the counting. This will require that the 16-bit values we have for the pulse durations will need to be broken down into two parts either before the sequence starts or as the sequence is being generated (i.e. in the timer Interrupt Service Routine (ISR)). The method chosen here is to perform this operation before the first pulse is generated. This will allow for a quicker ISR.

Design decisions over, let’s start to look at the code.

Pulse and Timer Data

The data for the timers will be broken down into the high and low byte values for the pulse duration. Along with this we will need to store the pulse type, namely high or low.

The pulse data is stored as a sequence of on/off values along with a duration in microseconds:

unsigned int pulseLength[] = { 2000U, 27830U, 400U, 1580U, 400U, 3580U, 400U };
unsigned char onOrOff[] =    {   1,      0,     1,     0,    1,     0,    1 };

This makes the pulse sequence more readable for the programmer. This data needs to be stored as a sequence of 8-bit values representing the high and low bytes of the pulse durations to speed up the ISR.

//
//  Data ready for the pulse timer ISR's to use.
//
unsigned char *_counterHighBytes = NULL;
unsigned char *_counterLowBytes = NULL;
unsigned char *_outputValue = NULL;
int _numberOfPulses = 0;
int _currentPulse = 0;

And to encode the data we need a small helper method:

//--------------------------------------------------------------------------------
//
//  Prepare the data for the timer ISRs.
//
void PrepareCounterData(unsigned int *pulseDuration, unsigned char *pulseValue, unsigned int numberOfPulses)
{
    _numberOfPulses = numberOfPulses;
    if (_counterHighBytes != NULL)
    {
        free(_counterHighBytes);
        free(_counterLowBytes);
        free(_outputValue);
    }
    _counterHighBytes = (unsigned char *) malloc(numberOfPulses);
    _counterLowBytes = (unsigned char *) malloc(numberOfPulses);
    _outputValue = (unsigned char *) malloc(numberOfPulses);
    for (int index = 0; index < numberOfPulses; index++)
    {
        _counterLowBytes[index] = (unsigned char) (pulseDuration[index] & 0xff);
        _counterHighBytes[index] = (unsigned char) (((pulseDuration[index] & 0xff00) >> 8) & 0xff);
        _outputValue[index] = pulseValue[index];
    }
    _currentPulse = 0;
}

Now we have a method of converting the pulses into a format ready for the Timer and the ISR we need to setup the Timer and implement the ISR.

Timer Setup and ISR

Setup is simple as we just need to load the timer values for the first pulse, load the prescalar and enable the interrupts:

//--------------------------------------------------------------------------------
//
//  Setup Timer 2 ready to process the pulse data.
//
void SetupTimer2()
{
    TIM2_ARRH = _counterHighBytes[0];
    TIM2_ARRL = _counterLowBytes[0];
    TIM2_PSCR = _prescalar;
    TIM2_IER_UIE = 1;       //  Enable the update interrupts.
}

The ISR is relatively simple, we need to work out if there is any more pulse data to process. If there is then we load the new data into the counter, set the pulse output and exit the ISR. If there is no more data then we simply set the pulse output and disable the timer.

//--------------------------------------------------------------------------------
//
//  Timer 2 Overflow handler.
//
#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
    _currentPulse++;
    if (_currentPulse == _numberOfPulses)
    {
        //
        //  We have processed the pulse data so stop now.
        //
        PD_ODR_ODR3 = 0;
        TIM2_CR1_CEN = 0;
    }
    else
    {
        TIM2_ARRH = _counterHighBytes[_currentPulse];
        TIM2_ARRL = _counterLowBytes[_currentPulse];
        PD_ODR_ODR3 = _outputValue[_currentPulse];
        TIM2_CR1_URS = 1;
        TIM2_EGR_UG = 1;
    }
    TIM2_SR1_UIF = 0;               //  Reset the interrupt otherwise it will fire again straight away.
}

Output Port

As noted, the application is using Port D3 to output the signal. A small method is required to setup the port accordingly:

//--------------------------------------------------------------------------------
//
//  Now set up the output ports.
//
//  PD3 - IR Pulse signal.
//
void SetupOutputPorts()
{
    PD_ODR = 0;             //  All pins are turned off.
    //
    //  PD4 is the output for the IR control.
    //
    PD_DDR_DDR3 = 1;
    PD_CR1_C13 = 1;
    PD_CR2_C23 = 1;
}

Main program Loop

The main program loop merely sets the stage by initialising the data, output port and timer before wiating for wny interrupts:

//--------------------------------------------------------------------------------
//
//  Main program loop.
//
void main()
{
    unsigned int pulseLength[] = { 2000U, 27830U, 400U, 1580U, 400U, 3580U, 400U };
    unsigned char onOrOff[] =    {   1,      0,     1,     0,    1,     0,    1 };
    PrepareCounterData(pulseLength, onOrOff, 7);
    __disable_interrupt();
    SetupTimer2();
    SetupOutputPorts();
    __enable_interrupt();
    PD_ODR_ODR3 = _outputValue[0];
    //
    //  Now we have everything ready we need to force the Timer 2 counters to
    //  reload and enable Timer 2.
    //
    TIM2_CR1_URS = 1;
    TIM2_EGR_UG = 1;
    TIM2_CR1_CEN = 1;
    while (1)
    {
        __wait_for_interrupt();
    }
}

Conclusion

Putting this all together and wiring up the oscilloscope we find the following generated when the circuit is turned on:

Output From Nikon Infra-red Control Circuit

Output From Nikon Infra-red Control Circuit

Examination of the timings of the pulses reveals that the pulse widths match those in the original specification above. The small spike at the start of the pulse sequence only appears when the circuit is first turned on.

At the moment we are only generating the raw pulses, we have not started to modulate the signal using the 38.4KHz carrier. This is left for a future experiment or indeed as a exercise for the reader.

One thing I could not resist was trying this with the Nikon camera, even though the specification stated that a 38.4KHz carrier was required. Running the code with the camera set to manual mode resulted in the camera being triggered.

So some future work:

  • Add the modulation
  • Increase the power of the infra-red output

Netduino, iPhone and Low Energy Bluetooth

Saturday, August 24th, 2013

A while ago I wrote a small library for the RN-42 Bluetooth module. This small module acted as a serial port when connected to the Netduino. Times have moved on with Bluetooth 4.0 being available on the iPhone I thought I would take another look at Bluetooth.

Bluetooth Module

The Bluetooth module we will be using is the RedBear Bluetooth Mini. This little module is a Bluetooth 4.0 Low Energy (BLE) module. In it’s simplest form it offers the ability to act as a serial port for your project. This is how we will be using this module for this project.

Bluetooth Device

To test this a BLE compatible device is required. Enter the iPhone 5 and the BLE Arduino application which RedBear have thoughtfully provided. The application may be written for Arduino but it is sending simple serial commands to the Arduino. Let’s see what it is doing and if we can use this application.

Serial Port Settings

First job is to figure out the serial port settings. Scanning the source code for the Arduino I came across the following line in the setup method:

BleFirmata.begin(57600);

My guess is that this is setting up the Arduino serial port to run at 57,600 baud using standard data bits, parity and top bit settings. Time to break out the logic analyser.

Connections between the Netduino and the RedBear module is simple:

Netduino PinRedbear BLE Module Pin
D0 (Rx)Tx
D1 (Tx)Rx
3.3VVin
GNDGND

Additionally, the logic analyser is connected to the Rx and Tx pins of the Netduino. An Async Serial analyser was added to the two pins through the Saleae software. This analyser has an Autobaud feature. Turn this on just in case we get the baud rate incorrect.

Next step is to install the BLE Arduino application on the iPhone 5. Start the application:

ReadBear BLE Software Opening Screen

ReadBear BLE Software Opening Screen

Press the Click button to connect to the ReadBear module.

Select Arduino Board

Select Arduino Board

Select the Arduino to connect to. The Arduino Uno has a similar pin out so select the Uno option.

Change a Pin Output Using BLE Software

Change a Pin Output Using BLE Software

So far there has been no output on the logic analyser. Change the state of an output pin (pressing H or L). This generates serial data:

Serial Data On the Logic Analyser

Serial Data On the Logic Analyser

A quick check in the Async Serial analysers properties shows that the data logic analyser thinks that the data is being sent at 60,000 baud, pretty close to the 57,600 which was found in the code earlier.

Netduino Code

The next step is to try to connect some code on the Netduino to the iPhone application. The simplest application we can use simply captures the incoming data on the serial port and displays the bytes received in the debug window of Visual Studio. The following should do the trick:

public class Program
{
    const int BUFFER_SIZE = 1024;

    public static void Main()
    {
        SerialPort sp = new SerialPort("COM1", 57600, Parity.None, 8, StopBits.One);
        sp.DataReceived += sp_DataReceived;
        sp.Open();
        Thread.Sleep(Timeout.Infinite);
    }

    static void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        string str;
        str = "";
        if (e.EventType == SerialData.Chars)
        {
            int amount;
            byte[] buffer;

            buffer = new byte[BUFFER_SIZE];
            amount = ((SerialPort) sender).Read(buffer, 0, BUFFER_SIZE);
            if (amount > 0)
            {
                for (int index = 0; index < amount; index++)
                {
                    str += buffer[index].ToString();
                    str += " ";
                }
                Debug.Print("Data: " + str);
            }
        }
    }
}

Deploying the above code to my Netduino Plus 2 and running the application gives the following output:

Data: 144 4 0
Data: 144
Data: 12 0
Data: 144
Data: 4 0

Pressing the H at the side of Pin 2 in the application generates the sequence 144 4 0. Following this up by pressing the H at the side of Pin 3 results in the sequence 144 12 0. Checking the Arduino code shows that the digital commands are prefixed by 0x90, i.e. 144.

The iPhone Application

Now that we know the iPhone and the Netduino can communicate (at least from the phone to the Netduino) let’s have a look at creating our own iPhone application. The simplest application would entail toggling a digital pin, so let’s do that. We’ll turn an LED on and off.

Starting XCode we will create a new project (Single View Application) for the iPhone and set the application be targeted at the iPhone only and to use Automatic Reference Counting (ARC):

New iPhone Project Options

New iPhone Project Options

Next we need to add the CoreBluetooth.Framework references to the application. So scroll down the frameworks and click on the + button under the Linked Frameworks section. Type bluetooth in order to search the installed frameworks.

Next, we need to add the RedBear BLE framework. I downloaded this and put it in my stored libraries. This was then added by right clicking on the Frameworks folder of the project, selecting Add Files to… and then browsing to the folder which contained the files.

Next we head over to the Storyboard and add a few controls to our interface:

iPhone Application in XCode

iPhone Application in XCode

ControlDescription
btnConnectToNetduinoButton to connect/disconnect to/from the Netduino.
lblStatusLabel to indicate the status of the application.
lblLEDStatusLabel containing the text LED Status
swLEDSwitch which is used to turn the LED on/off.
aiBusyIndicator which shows when the application is trying to detect the Bluetooth module.

First thing to do is to connect the controls to the code in the header file for the view controller:

#import <UIKit/UIKit.h>
#import "BLE.h"

//
//  Define the various modes for the state machine.
//
#define MODE_DISCONNECTED       0
#define MODE_CONNECTING         1
#define MODE_CONNECTED          2
#define MODE_DISCONNECTING      3

//
//  Timeout values.
//
#define TIMEOUT_BLE_TIMEOUT         2
#define TIMEOUT_BUSY_CONNECTING     3.0

@interface SNCViewController : UIViewController <BLEDelegate>
{
    IBOutlet UILabel *lblStatusMessage;
    IBOutlet UILabel *lblLEDStatus;
    IBOutlet UISwitch *swLED;
}

- (IBAction) btnConnect_TouchUpInside:(UIButton *) sender;
- (IBAction) swLED_Changed:(UISwitch *)sender;

@property (strong, nonatomic) BLE *ble;
@property (strong, nonatomic) UIActivityIndicatorView *aiBusy;
@property (strong, nonatomic) IBOutlet UIButton *btnConnectToNetduino;

@end

Now we have the controls defined in the header file we can start to work on the methods.

Definitions

We need some simple definitions at the head of our implementation file:

#import "SNCViewController.h"
#import "BLE.h"

@interface SNCViewController ()

@end

@implementation SNCViewController

@synthesize ble;
@synthesize btnConnectToNetduino;

//
//  Private local variables.
//
int mode = MODE_DISCONNECTED;

These definitions create the Bluetooth object and he variables required for state control.

Supporting Methods

The Connected and Disconnected methods change the interface and state variables when we have successfully connected or disconnected to/from the Bluetooth module:

//
//  Set up the interface to show that the Bluetooth module is connected
//
- (void) Connected
{
    [lblStatusMessage setText:@"Connected"];
    lblLEDStatus.hidden = false;
    swLED.hidden = false;
    [btnConnectToNetduino setTitle:@"Disconnect from Netduino" forState:UIControlStateNormal];
    btnConnectToNetduino.enabled = true;
    [self.aiBusy stopAnimating];
    mode = MODE_CONNECTED;
}

//
//  Set up the interface to show that the Bluetooth module is disconnected
//
- (void) Disconnected
{
    [lblStatusMessage setText:@"Disconnected"];
    lblLEDStatus.hidden = true;
    swLED.hidden = true;
    [btnConnectToNetduino setTitle:@"Connect to Netduino" forState:UIControlStateNormal];
    btnConnectToNetduino.enabled = true;
    [self.aiBusy stopAnimating];
    mode = MODE_DISCONNECTED;
}

Standard Methods (viewDidLoad and didReceiveMemoryWarning)

These methods are generated by default when the application is first created. For viewDidLoad we will set up the application for initial use. We will not be modifying the didReceiveMemoryWarning method in this application.

//
//  Setup the view.
//
- (void) viewDidLoad
{
    [super viewDidLoad];
    //
    //  Setup the display.
    //
    lblLEDStatus.hidden = true;
    [swLED setOn:NO];
    swLED.hidden = true;
    [lblStatusMessage setText:@"Ready"];
    self.aiBusy.hidesWhenStopped = true;
    //
    //  Create the Bluetooth objects.
    //
    ble = [[BLE alloc] init];
    [ble controlSetup:1];
    ble.delegate = self;
    //
    //  Setp the simple properties.
    //
    mode = MODE_DISCONNECTED;
}

//
//  Process the memory warning event.
//
- (void) didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

The main point to note in the viewDidLoad method is that the Bluetooth object is initialised.

Button Press Event

The btnConnect_TouchUpInside event initiates the connection/disconnection of the application to/from the Bluetooth module.

//
//  User has pressed the Connect button so try to connect/disconnect from the Netduino.
//
- (IBAction) btnConnect_TouchUpInside:(UIButton *) sender
{
    if (ble.activePeripheral)
    {
        if (ble.activePeripheral.isConnected)
        {
            [[ble CM] cancelPeripheralConnection:[ble activePeripheral]];
        }
    }
    ble.peripherals = nil;
    switch (mode)
    {
        case MODE_DISCONNECTED:
            [lblStatusMessage setText:@"Connecting..."];
            [NSTimer scheduledTimerWithTimeInterval:(float) TIMEOUT_BUSY_CONNECTING target:self selector:@selector(connectionTimer:) userInfo:nil repeats:NO];
            [ble findBLEPeripherals:TIMEOUT_BLE_TIMEOUT];
            [self.aiBusy startAnimating];
            mode = MODE_CONNECTING;
            break;
        case MODE_CONNECTED:
            [lblStatusMessage setText:@"Disconnecting..."];
            [self.aiBusy startAnimating];
            mode = MODE_DISCONNECTING;
            break;
        default:
            break;
    }
    btnConnectToNetduino.enabled = false;
}

Timer

The btnConnect_TouchUpInside event starts a timer when it is trying to connect to a Bluetooth module. This timer prevents the application from locking when searching for a Bluetooth module which does not exist. The timer callback stops the search process after 3 seconds:

//
//  The timer started by the Connect button has triggered.  See if we have any
//  Bluetooth modules nearby.  If we have then connect to the first one in the
//  list.
//
-(void) connectionTimer:(NSTimer *) timer
{
    if (ble.peripherals.count > 0)
    {
        [ble connectPeripheral:[ble.peripherals objectAtIndex:0]];
    }
    else
    {
        [lblStatusMessage setText:@"Cannot find Netduino"];
        mode = MODE_DISCONNECTED;
    }
    [self.aiBusy stopAnimating];
}

Switching the LED on/off

The application assumes only one LED is connected to the Netduino. The data packet sent to the Netduino therefore contains a 1 or 0 to indicate the status of the LED:

//
//  User has changed the value of the LED.
//
- (IBAction) swLED_Changed:(UISwitch *) sender
{
    UInt8 buffer[1];
    buffer[0] = sender.isOn == YES ? 1 : 0;
    NSData *data = [[NSData alloc] initWithBytes:buffer length:1];
    [ble write:data];
}

Bluetooth Module Events

The final two events are generated by the Bluetooth library. These are fired when the module connects/disconnects to/from the module:

//
//  Connected to a Bluetooth module successfully.
//
-(void) bleDidConnect
{
    [self Connected];
}

//
//  Application has disconnected from a Bluetooth module.
//
- (void) bleDidDisconnect
{
    [self Disconnected];
}

//
//  RSSI has been updated.
//
- (void) bleDidDisconnect:(NSNumber *) rssi
{
}

Netduino Application

The final task is to modify the Netduino Application to process the data. An OutputPort is added to the application and this is connected to a digital IO pin. This pin is then turned on/off when serial data is received from the Bluetooth module:

public class Program
{
    const int BUFFER_SIZE = 1024;
    private static OutputPort led = new OutputPort(Pins.GPIO_PIN_D8, false);

    public static void Main()
    {
        SerialPort sp = new SerialPort("COM1", 57600, Parity.None, 8, StopBits.One);
        sp.DataReceived += sp_DataReceived;
        sp.Open();
        Thread.Sleep(Timeout.Infinite);
    }

    static void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        string str;
        str = "";
        if (e.EventType == SerialData.Chars)
        {
            int amount;
            byte[] buffer;

            buffer = new byte[BUFFER_SIZE];
            amount = ((SerialPort) sender).Read(buffer, 0, BUFFER_SIZE);
            if (amount > 0)
            {
                for (int index = 0; index < amount; index++)
                {
                    str += buffer[index].ToString();
                    str += "";
                }
                if (buffer[0] == 1)
                {
                    led.Write(true);
                }
                else
                {
                    led.Write(false);
                }
                Debug.Print("Data: " + str);
            }
        }
    }
}

Does it Work?

Well of course it does and here’s the video to prove it:

First we connect to the module. Once connected we can start to turn the LED on and off using the iPhone.

Conclusion

The library provided by RedBear is a great starting point for using the iPhone to communicate with a BLE module. It is simple and easy to use for this type of application. This whole experiment only took a few hours to put together an execute.

Let’s see where this takes us 🙂

Infra-red Transmitter and Receiver

Sunday, August 18th, 2013

In this post we will use Mr Maxwell’s new fangled electrons to generate invisible light and transmit a signal through the luminiferous aether.

Or to put it another way, I’m learning how to use IR remote controls in microcontroller circuits. The problem with using infra-red LEDs is that you cannot really see if they are on or off. You can see if a signal is applied to the LED but this does not mean it’s generating an infra-red signal. Let’s admit it, we’ve all burnt out an LED or two in our time. So in this post I’m going to put together a couple of circuits to generate and detect infra-red signals.

Detecting IR Signals

For this circuit I am going to be using the L-7113P3C phototransistor. This component looks like an LED but is in fact a transistor:

Photo Transistor

Photo Transistor

The phototransistor has two conventional connections which allow the collector and emitter to be connected to the electrical circuit but the third connector (the base) has been made sensitive to infra-red light and then exposed via a lens. The transistor becomes conductive when infra-red light hits the photosensitive base of the transistor.

The schematic for the basic detector circuit is as follows:

IR Receiver Schematic

IR Receiver Schematic

Should be simple enough. Out comes the breadboard, oscilloscope and a couple of remote controls I have to hand.

Simple IR Receiver

Simple IR Receiver

Connecting the Vout to the oscilloscope and pointing a WD TV remote control at the phototransistor I saw the following trace:

WD TV Remote Signal

WD TV Remote Signal

So, it looks like the detector circuit is detecting the infra-red signal from these remote controls.

Infra-red Transmitter

The transmitter circuit is simply an infra-red LED with visible LED replaced by an infra-red LED. For this circuit I used the TSUS5400 as this is matched with the L-7113P3C phototransistor I used in the detector circuit. Dropping this into a simple LED/transistor circuit gives the following schematic:

IR Transmitter Schematic

IR Transmitter Schematic


R2 was selected to allow 20mA of current through the infra-red LED (IRLED). The resistor on the base of the transistor should allow the transistor to become saturated. Dropping this on breadboard gives a circuit which looks like this:

IR Transmitter and Receiver

IR Transmitter and Receiver

The top part of the breadboard contains the transmitter circuit (Blue LED) and the lower part of the breadboard contains the receiver.

Testing

To test the circuit a 1KHz signal generator was connected to the base of the transmitter circuit. Channel 1 (yellow) of the oscilloscope is connected to Vout of the receiver with channel 2 (blue) connected to the signal generator. Turning on the signal generator produced the following output on the oscilloscope:

1 KHz Signal

1 KHz Signal

Looking good. The signals show a good match.

Conclusion

The main aim of this exercise was to create an infra-red receiver circuit which can be used to verify that an equivalent transmitter circuit is working. The receiver circuit gives a value for Vout of 3.12V. This is an acceptable value for logic 1 for the microcontrollers which I am using. Their nominal input value for logic 1 is the supply voltage +/- 0.3V.

The transmitter circuit should allow the use of the same microcontroller to generate a signal which can be used to control a device such as a TV etc.

Designspark Feature Tutorial – ModelSource

Sunday, July 21st, 2013

I have been a great fan of Designspark for a while. Many hobbyists swear by Eagle. Personally, I find the interface counter intuitive. Designspark on the other hand, feels like a Windows application.

Shortly after Designspark version 5 was released, I noticed a new toolbar in the bottom lefthand corner of the interface. This toolbar was labelled ModelSource. So like all good software users I went to the manual to find out more <COUGH>, OK, I simply started to play with the feature and here’s a summary of what I found.

What Is ModelSource?

ModelSource is Designspark’s free on-line database of component models. The library is by no means complete but it does provide a large on-line library of components to choose from. At the time of writing, there are over 80,000 components in the library. The library is linked to RS Components but this is to be expected as Deisgnspark seems to have been originated/sponsored by RS.

Interestingly, the library is also compatible with other software packages.

Let’s Use It

The quickest way to become familiar with a software feature is to use it. So let’s add a component I’ve never used before to a schematic. For this example we will use the 74HC164.

We start by opening the ModelSource toolbar. This is can be found in the bottom left corner of the application window and looks like this:

ModelSource Toolbar Collapsed

ModelSource Toolbar Collapsed

If you cannot see the ModelSource toolbar then you can check if it is visible. by looking at the View menu. Alternatively, you can use the hotkey Ctrl-M to make it visible.

The first time you make ModelSource visible you will have to log in using your Designspark credentials. Once logged in, you can access ModelSource.

Clicking on ModelSource opens the toolbar. This can appear as required or you can permanently pin this in the open state to the interface. When first opened ModelSource will be ready to accept search requests from the user:

ModelSource

ModelSource

There are really three distinct areas to the interface:

  • Search Parameters (Red Rectangle)
  • Component Detail (Green Rectangle)
  • Component List (Blue Rectangle)

Search Parameters

The Search Parameters section of the interface is in the upper left part of ModelSource and it allows you to search ModelSource by component family or for a specific component using the RS part number.

Component Detail

Component Detail sows the schematic and PCB trace for a selected component along with summary information. This can be found to the right of the Search Parameters. This section also allows you to go to the component data sheet and add the component to your library.

Component List

Component List contains a grid of components which match your search criteria.

Searching For A Component

The component we are searching for is the 74HC164. This is a 8-bit shift register from the 74HC family of components.

The search process requires the user to select from a list of possible family groups/characteristics slowly narrowing the search down to a group of components which you can then browse through. So for the 74HC164 we start by selecting Semiconductors from the first drop down list. The system will then consult the ModelSource database and construct sensible options for the second drop down list which will then be populated.

This component is a shift register and so it probably best fits into the Standard Logic family of components. So we select this in the second drop down list. Again, Designspark will select a new set of options for the third drop down list.

The final list now contains the family of components we are looking for, namely Counters & Shift Registers. Selecting this option now allows Designspark to populate the component list from the ModelSource database and we are presented with the following:

Semiconductor search for Counters and Shift Registers

Semiconductor search for Counters and Shift Registers

As you can see, you can use the drop down lists above each column to filter the information presented in the component list should you require to refine the results further. A quick look down the list and we can see our component, the 74HC164. So let’s select the 74HC164D component in the grid:

74HC164D Ready to load preview

74HC164D Ready to load preview

And now press the Load Preview button. Designspark will then load the component preview information from ModelSource:

74HC164 Preview Complete

74HC164 Preview Complete

At this point we have two options, we can use this component immediately and add it to the schematic or we can view the datasheet. Clicking on View Datasheet takes you to the RS web site and presents the page for the component. Here you can add the component to your basket or scroll down the page to the datasheet links:

74HC164 Web page

74HC164 Web page

By selecting Use Component you make the component available through the library installed on your computer. Whilst in the schematic editor press the Add Component Button

Add Component button

Add Component button

and type in the component name into the Component text box in the Add Component dialog:

Add 74HC164 component to schematic

Add 74HC164 component to schematic

You can now add the component to the schematic:

74HC164 on schematic

74HC164 on schematic

Searching for a Component by Part Number

It is sometimes simpler to use the RS Components web site to locate the part you are looking for and then use the Part Number Search option to locate the component. For example, consider the TLC5940 shift register. It is difficult to work out from the list of search options in the drop down list boxes exactly what you should be selecting.

In comparison, go to the RS Components web site and enter TLC5940 in the search text box on the home page and press the Find button. You will be taken to a page with three components, all variations on the TLC5940 16 channel PWM driver. Now make a note of the RS part number (I have the 389-344 variation of this component).

So go back to ModelSource and press the Clear button. Now enter the component part number into the Part Number Quick Search text box and press the Go button:

TLC5940 search for part number

TLC5940 search for part number

Selecting the part and pressing Load Preview shows the part information:

TLC5940 ModelSource Preview

TLC5940 ModelSource Preview

Conclusion

ModelSource is not a complete database but with 80,000 components it offers a very good chance of finding the component you need for the circuit you are building. I have used this on several occasions to find models for components which do not exist in the standard installation.

There is no question that this could be improved. For instance, why can you not search by component name as you can in the web interface?

Overall this is a good product feature and whilst it is linked through to the RS Components web site it does not necessarily mean you have to order from there.

Temperature and Humidity Sensor Module for the Netduino GO!

Tuesday, June 25th, 2013

A while ago (forgive the pun), Arron Chapman and I started to collaborate on building a temperature and humidity sensor based upon the DHT22 sensor. One of the original posts discussing the module can be found here in the Netduino forums. From the very start we agreed that both the hardware and software would be open source. This post will discuss the basic hardware requirements and the software required to create a Temperature and Humidity Module for the Netduino GO!.

This post has a software bias given the relatively simple nature of the hardware being developed. Here is a flavour of what was achieved:

The design work for this module is the combined effort of Arron Chapman of Variable Labs and myself.

DHT22 Temperature and Humidity Sensor

The DHT22 is a four pin package capable of measuring temperature (+/- 0.5C) and humidity (+/- 5%). The package uses a single wire interface for communication and can be powered by 3.3-5V. The single wire protocol used is not compatible with Dallas single wire protocol.

The four pins should be connected as follows:

PinConnection
1VDD (3.3-5V)
2Data/Signal
3Ground
4Ground

Pin 2 (Data/Signal) should be connected to the microcontroller with a pull-up resistor to VDD.

The microcontroller sends a start signal to the sensor which then responds with the data representing the temperature and humidity. The data is terminated with a check sum. The sensor can only be read at most once every 2 seconds. The trace for a full start, transmit and end signal looks like this:

CondensedTrace

The communication starts with the microcontroller sending the start signal. The microcontroller pulls the signal line low for at least 1-10ms. This ensures that the sensor can detect the microcontrollers signal. The microcontroller then pulls up the signal line and then waits for 20-40us for the sensor to respond.

Zooming in on the start packet we would see something like:

StartSignal

As you can see, in this case the signal line was pulled low by the microcontroller for about 6.25mS.

The sensor then pulls the signal line low for 80us followed by pulling up the signal line for a further 80us. At this point the sensor is ready to start to transmit the temperature and humidity data.

The data is transmitted by varying the length of time the signal pin is held high. Transmission of a single bit starts by pulling the signal line low. A 0 bit is indicated by the sensor pulling the signal line high for 26-28us. pulling the signal high for 70us indicates a 1.

The temperature and humidity data is transmitted in a 40-bit packet. The first 16 bits hold the humidity information, the next 16 bits hold the temperature information and the final 8 bits contains the checksum. The following shows the full data packet from the sensor:

DHT22FullDataPacket

Both the temperature and the humidity are represented as an integer. The actual value is obtained by converting the binary number to decimal and then dividing by 10. If the high bit of the temperature reading is 1 then the value represents a negative temperature.

The final 8 bits of the data packet contain the checksum. The checksum is the result of adding the four bytes of the temperature and humidity data.

Schematic

Aside from the components required to make a basic module, the board really only required two parts, a single pull-up resistor and the DHT22 Temperature and Humidity sensor itself.

DHT22Schematic

The components to the left of the diagram should be familiar if you have read the previous posts on making a module. The only additional parts can be seen to the right of the schematic.

Breadboard and PCB Prototypes

The original work for this module was completed on breadboard with an additional LM35 temperature sensor at the side of the DHT22. This second sensor was used as a reference to confirm the readings being generated by the DHT22. The simplicity of the design meant that moving to a PCB prototype was relatively simple and the iTeadStudio prototyping service made this affordable. A few weeks after ordering the prototype modules arrived:

TemperatureSensorBarePC

One of the tests I do on any PCBs I have made is a connectivity test. I do this when the board is unpopulated and simply walk through the list of connections and verify that there are no problems. I also take the software I have written during prototyping on the breadboard and check that the pins on the STM8S are connected to the correct points on the PCB. It was during this test that I found that a connection had been missed off of the original schematic, namely the connection from the GO! connector and the GPIO pin which is used to signal that data is ready for the GO! to consume. A quick fix once the board had been populated.

TemperatureModulePCBPopulated

Notice the yellow wire – this goes to show the value of prototyping even for the smallest and seemingly simplest of projects.

Software

The one wire protocol means that the microcontroller will be both a master and a slave device as will the DHT22. We also have to allow for the fact that we also have the leave a 2 second gap between readings. The ideal way to implement this is to use a finite state machine. The cycle of events is as follows:

  1. Send the start signal and wait for 1-10ms
  2. Enter read mode and collect data
  3. Pause for at least 2 seconds

The state machine relies upon the timers to change the state based upon the minimum values for the time periods set by the sensor.

For the start signal, the signal line is set low and the timer started.

When the timer interrupt is triggered, the timer is turned off and reset. The signal pin is then switched from being an output pin to input with interrupts enabled. The timer is then restarted with a time period slightly larger than that required to ready all 40 bits of data from the sensor.

In the final stage, the interrupts are turned off, the data is processed and the system put to sleep (from a reading point of view) for more than 2 seconds.

In read mode, the system merely waits for interrupts to be generated by the sensor changing the state of the signal line. When an interrupt occurs, the time stamp (from the currently running timer) is read and recorded. The duration of the signal can then be calculated later (in stage three, pause mode) and the bit stream reconstructed from the timings.

Functionality

From a high level, the temperature and humidity module should provide the ability to read the current temperature and humidity (given the restrictions on the sensors delay of 2 seconds between readings). In addition, it would be desirable to allow the system to generate alarms for readings which are out of range.

As with all Netduino GO! modules, this functionality is split between the module and the module driver running on the Netduino GO!. The code on the module takes care of all the communication with the sensor. It takes this information and then responds to requests from the module driver on the Netdunio GO!.

STM8S Module

The software on the STM8S started life as the basic STM8S module software which has been used in previous posts.

The first modification needed is to add the state machine. The STM8S periodically reads the values from the sensor and store them for later retrieval by the Netduino GO! The regular nature of this update means that there is always a “current” reading available to the Netduiino GO! So the first thing we need is to setup a timer:

//--------------------------------------------------------------------------------
//
//  Setup Timer 2 to pause for 3.2 seconds following power up.
//
void SetupTimer2()
{
    TIM2_PSCR = 0x0a;       //  Prescaler = 1024
    TIM2_ARRH = 0xc3;       //  High byte of 50,000.
    TIM2_ARRL = 0x50;       //  Low byte of 50,000.
    TIM2_IER_UIE = 1;       //  Enable the update interrupts.
    TIM2_CR1_CEN = 1;
}

This initialisation code means that a reading is not available immediately. To go with this we will also need a variable to store the current mode along with some definitions:

//
//  Define the various modes for the state machine.
//
#define MODE_PAUSE                  0
#define MODE_SENDING_START_SIGNAL   1
#define MODE_READING_DATA           2
//
//  Current sensing mode.
//
int _mode = MODE_PAUSE;

Now for the critical part of the operation, we need to change to the correct state when the timer is triggered:

//--------------------------------------------------------------------------------
//
//  Timer 2 Overflow handler.
//
//  Note:   Normally we want the ISR to operate as quickly as possible but in
//          this case "as quickly as possible" just needs to be quick enough
//          for this sensor.  This means we have milliseconds for this ISR.
//
#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
    TIM2_CR1_CEN = 0;
    switch (_mode)
    {
        case MODE_PAUSE:
            //
            //  Any pause has now completed, we need to start the
            //  read process.
            //
            PIN_DHT22_DATA = 0;
            TIM2_ARRH = 0xc3;       //  High byte of 50,000.
            TIM2_ARRL = 0x50;       //  Low byte of 50,000.
            TIM2_PSCR = 1;          //  Prescalar = 2 => count = 100,000 = 6.25mS
            TIM2_EGR_UG = 1;        //  Force counter update.
            _mode = MODE_SENDING_START_SIGNAL;
            break;
        case MODE_SENDING_START_SIGNAL:
            //
            //  At this point the start signal period has elapsed and we
            //  want to start to read data from the sensor.
            //
            PIN_DHT22_DATA = 1;
            PIN_DHT22_DIRECTION = 0;//  DHT22 pin is input.
            PIN_DHT22_MODE = 0;     //  DHT22 pin is floating input.
            EXTI_CR1_PDIS = 1;      //  Interrupt on rising edge.
            //
            //  We will get another interrupt after 5ms.  This should be
            //  enough time for the sensor to have generated the data and
            //  for us to process it.
            //
            TIM2_ARRH = 0x13;       //  High byte of 5,000.
            TIM2_ARRL = 0x88;       //  Low byte of 5,000.
            TIM2_PSCR = 4;          //  Prescalar = 4 => count = 5,000 (5 mS)
            TIM2_EGR_UG = 1;        //  Force counter update.
            _currentTiming = 0;
            _mode = MODE_READING_DATA;
            break;
        case MODE_READING_DATA:
            //
            //  At this point we should have read all of the data.  We
            //  now need to calculate the values and wait for 2 seconds
            //  before reading the next value.
            //
            PIN_DHT22_DATA = 1;     //  Set the output high after the data has been read.
            PIN_DHT22_DIRECTION = 1;//  DHT22 data pin is output.
            //
            //  We cannot read the sensor again for at least 2 seconds (32,800 with a prescalar
            //  of 11 should result in a ~4 seconds delay).
            //
            TIM2_ARRH = 0x80;
            TIM2_ARRL = 0x20;
            TIM2_PSCR = 0x0b;
            TIM2_EGR_UG = 1;        //  Force counter update.
            if (_currentTiming == 42)
            {
                DecodeData();
                if (_alarmsEnabled != 0)
                {
                    CheckAlarms();
                }
            }
            else
            {
                OutputStatusCode(SC_TOO_LITTLE_DATA);
            }
            _mode = MODE_PAUSE;
            break;
    }
    TIM2_CR1_CEN = 1;               //  Re-enable Timer 2.
    TIM2_SR1_UIF = 0;               //  Reset the interrupt otherwise it will fire again straight away.
}

Much of the code in the Interrupt Service Routine (ISR) above is concerned with recording the current state and resetting the timer ready to move into the next state. A key point to notice here is the change of use for the GPIO pin connected to the sensor.

When the ISR is entered and we are in pause mode (case MODE_PAUSE), we reset the timer and drop the signal line connected to the DHT22 (PIN_DHT22_DATA = 0). We then enter the next mode, MODE_SENDING_START_SIGNAL.

The next time the ISR is entered we should have just completed the time needed to keep the signal line low and for the sensor to be ready to send the data on the signal line. So we pull the signal line high and then change the direction of the GPIO line and the line becomes an input line which generates and interrupt. We then record the fact that we have changed to the next mode (MODE_READING_DATA), reset the timers and start to wait again.

In the final state, we should have received a full data packet from the sensor so we decode and process the data and then enter the pause mode once more. At this point the whole cycle repeats.

The state machine is now complete and we now need to start to look at recording the data. A little early experimentation showed that the sensor would generate 42 pulses. It was decided that the best way to record the pulses was to simply record the time at which they occurred. These timings could later be used to workout the pulse duration and hence if we had a 0 or 1. As we only needed the pulse duration then the actual time of the pulse was not needed, simply a reliable way of recording the duration. Timer 2 came to our aid. This timer was already running as it was required to control the state machine. We could simply record the value in this timer whenever an interrupt occurred. So starting with some definitions to support the storage of the data:

//
//  How many readings will we take?
//
#define MAX_READINGS                50
//
//  Somewhere to put the data.
//
int _sensorTimings[MAX_READINGS];
//
//  Which reading are we expecting next?
//
int _currentTiming = 0;
int _reading;

Next we need to capture the value in the timer when an interrupt occurred:

//--------------------------------------------------------------------------------
//
//  Process the interrupt generated by the DHT22.
//
#pragma vector = 8
__interrupt void EXTI_PORTD_IRQHandler(void)
{
    unsigned char high = TIM2_CNTRH;
    unsigned char low = TIM2_CNTRL;
    if (_currentTiming < MAX_READINGS)
    {
        _reading = (high << 8);
        _reading += low;
        _sensorTimings[_currentTiming++] = _reading;
    }
}

Now we have captured the data we need to decode the timings to obtain the temperature, humidity and the checksum. This method is called from the Timer 2 ISR above.

//--------------------------------------------------------------------------------
//
//  Decode the temperature and humidity data.
//
void DecodeData()
{
    unsigned short multiplier;
    //
    //  Extract the humidity.
    //
    unsigned short humidity = 0;
    multiplier = 32768;
    for (int index = 2; index < 18; index++)
    {
        if ((_sensorTimings[index] - _sensorTimings[index - 1]) > LOGIC_BOUNDARY)
        {
            humidity += multiplier;
        }
        multiplier >>= 1;
    }
    //
    //  Extract the temperature.
    //
    unsigned short temperature = 0;
    multiplier = 32768;
    for (int index = 18; index < 34; index++)
    {
        if ((_sensorTimings[index] - _sensorTimings[index - 1]) > LOGIC_BOUNDARY)
        {
            temperature += multiplier;
        }
        multiplier >>= 1;
    }
    //
    //  Extract the checksum.
    //
    unsigned short checksum = 0;
    multiplier = 128;
    for (int index = 34; index < 42; index++)
    {
        if ((_sensorTimings[index] - _sensorTimings[index - 1]) > LOGIC_BOUNDARY)
        {
            checksum += multiplier;
        }
        multiplier >>= 1;
    }
    //
    //  If the checksum is OK then overwrite the data.
    //
    unsigned short calcChecksum = 0;
    calcChecksum += humidity & 0xff;
    calcChecksum += ((humidity >> 8) & 0xff);
    calcChecksum += temperature & 0xff;
    calcChecksum += ((temperature >> 8) & 0xff);
    if ((calcChecksum & 0xff) == checksum)
    {
        _lastTemperature = temperature;
        _lastHumidity = humidity;
        _lastChecksum = checksum;
        OutputStatusCode(SC_OK);
        OutputDebugData(humidity, temperature, checksum);
    }
    else
    {
        OutputStatusCode(SC_CHECKSUM_ERROR);
    }
}

The LOGIC_BOUNDARY definition is simply the number of clock pulses which separates the 0 and the 1. A little empirical research gave this value as 100 clock pulses. This is not exactly as defined in the data sheet but it does give a reasonable working value.

As with all GoBus 1.0 modules, the definition of the data within the data packets (with the exception of the first byte) is left to the implementation. For this module the packets will be formatted as follows:

  • 0x80 – Mandatory first byte
  • ACK or NACK
  • Type of data in the packet (for an ACK packet)
  • Data (for ACK)

The type of data in the packet allows the system to respond to requests for information and also raise alarms.

Responding to the requests for readings is simply a case of copying the last set of readings from the global variable and putting them into the packet. The module then raises an interrupt (via NotifyGOBoard()) to indicate to the Netduino GO! that there is some data ready for processing:

//--------------------------------------------------------------------------------
//
//  Copy the sensor readings into the _txBuffer.
//
void CopySensorReadingsToTxBuffer()
{
    _txBuffer[3] = ((_lastHumidity >> 8) & 0xff);
    _txBuffer[4] = (_lastHumidity & 0xff);
    _txBuffer[5] = ((_lastTemperature >> 8) & 0xff);
    _txBuffer[6] = (_lastTemperature & 0xff);
    _txBuffer[7] = _lastChecksum;
}

//--------------------------------------------------------------------------------
//
//  Notify the GO! main board that there is some data ready for collection.
//
void GetReadings()
{
    _txBuffer[0] = 0x80;
    _txBuffer[1] = DHT22_ACK;
    _txBuffer[2] = DHT22_GET_READINGS;
    CopySensorReadingsToTxBuffer();
    NotifyGOBoard();
}

One desirable feature mentioned earlier was the ability to set alarms and have the module signal to the Netduino GO! that an alarm has been raised. The alarm system allows the user to set an alarm for the following events:

  • Low temperature
  • High temperature
  • Low humidity
  • High humidity

Setting an alarm is a simple operation as it only requires that the module records the alarms which have been set and the values associated with the alarm:

//--------------------------------------------------------------------------------
//
//  Set the alarm thresholds for the temperature and humidity.
//
//  Note that the alarm uses "special" out of range values to indicate that a
//  specified limit is not required.
//
void SetAlarms()
{
    //
    //  Start by turning everything off.
    //
    _alarmsEnabled = 0;
    _lowerTemperatureAlarm = SENSOR_MIN_TEMPERATURE;
    _upperTemperatureAlarm = SENSOR_MAX_TEMPERATURE;
    _lowerHumidityAlarm = SENSOR_MIN_HUMIDITY;
    _upperHumidityAlarm = SENSOR_MAX_HUMIDITY;
    //
    //  Now work out what we are looking at.
    //
    if (_rxBuffer[2] & ALARM_LOW_TEMPERATURE)
    {
       _lowerTemperatureAlarm = (_rxBuffer[3] * 256) + _rxBuffer[4];
       if (_lowerTemperatureAlarm < SENSOR_MIN_TEMPERATURE)
       {
           _alarmsEnabled = 0;
           RaiseNAK();
           return;
       }
       _alarmsEnabled |= ALARM_LOW_TEMPERATURE;
    }
    if (_rxBuffer[2] & ALARM_HIGH_TEMPERATURE)
    {
        _upperTemperatureAlarm = (_rxBuffer[5] * 256) + _rxBuffer[6];
       if (_upperTemperatureAlarm > SENSOR_MAX_TEMPERATURE)
       {
           _alarmsEnabled = 0;
           RaiseNAK();
           return;
       }
        _alarmsEnabled |= ALARM_HIGH_TEMPERATURE;
    }
    if (_rxBuffer[2] & ALARM_LOW_HUMIDITY)
    {
       _lowerHumidityAlarm = (_rxBuffer[7] * 256) + _rxBuffer[8];
       if (_lowerHumidityAlarm < SENSOR_MIN_HUMIDITY)
       {
           _alarmsEnabled = 0;
           RaiseNAK();
           return;
       }
       _alarmsEnabled |= ALARM_LOW_HUMIDITY;
    }
    if (_rxBuffer[2] & ALARM_HIGH_HUMIDITY)
    {
        _upperHumidityAlarm = (_rxBuffer[9] * 256) + _rxBuffer[10];
       if (_upperHumidityAlarm > SENSOR_MAX_HUMIDITY)
       {
           _alarmsEnabled = 0;
           RaiseNAK();
           return;
       }
        _alarmsEnabled |= ALARM_HIGH_HUMIDITY;
    }
    //
    //  Tell the Go board which alarms have been enabled (as an acknowledgement).
    //
    _txBuffer[0] = 0x80;
    _txBuffer[1] = DHT22_ACK;
    _txBuffer[2] = DHT22_SET_ALARMS;
    _txBuffer[3] = _alarmsEnabled;
    NotifyGOBoard();
}

The actual process of raising the alarm is performed in the Timer 2 ISR when we have decoded the data. Remember this code:

DecodeData();
if (_alarmsEnabled != 0)
{
    CheckAlarms();
}

When checking the alarms the application will compare the values for all of the alarms and then raise a single interrupt back to the Netduino GO! if one or more alarms need to be raised. The data packet sent back to the Netduino GO! also contains the current sensor readings. This means that the Netduino GO! does not have to request the sensor data to find out which alarm has been raised.

//--------------------------------------------------------------------------------
//
//  Check to see if the temperature/humidity is outside of any ranges and raise
//  an alarm if needed.
//
void CheckAlarms()
{
    unsigned char alarm = 0;
    if ((_alarmsEnabled & ALARM_LOW_TEMPERATURE) && (_lastTemperature < _lowerTemperatureAlarm))
    {
        alarm |= ALARM_LOW_TEMPERATURE;
    }
    if ((_alarmsEnabled & ALARM_HIGH_TEMPERATURE) && (_lastTemperature > _upperTemperatureAlarm))
    {
        alarm |= ALARM_HIGH_TEMPERATURE;
    }
    if ((_alarmsEnabled & ALARM_LOW_HUMIDITY) && (_lastHumidity < _lowerHumidityAlarm))
    {
        alarm |= ALARM_LOW_HUMIDITY;
    }
    if ((_alarmsEnabled & ALARM_HIGH_HUMIDITY) && (_lastHumidity > _upperHumidityAlarm))
    {
        alarm |= ALARM_HIGH_HUMIDITY;
    }
    if (alarm != 0)
    {
        _txBuffer[0] = 0x80;
        _txBuffer[1] = DHT22_ACK;
        _txBuffer[2] = DHT22_ALARM;
        CopySensorReadingsToTxBuffer();
        _txBuffer[8] = alarm;
        NotifyGOBoard();
    }
}

At this point the majority of the STM8S code is complete.

Netduino GO!

As with the STM8S code, the Netduino GO! driver is based upon the code developed previously and used as the basis for the Output Expander module. Much of this should be familiar and so we will only be considering the methods and supporting structures which implement specific features in this module. The class diagram for our module driver looks like this:

DHT22ModuleNETMFClassDiagram

As you can see the module driver is not overly complex as it provides a few methods and supports a single interrupt.

The STM8S module returns temperature data on two occasions, the first is the explicit request by the Netduino GO! for the current readings, the second is when an alarm is raised. It therefore makes sense to abstract this code into a method:

/// <summary>
/// Extract the temperature and humidity data from the data which has been
/// generated by the STM8S.
/// </summary>
/// <param name="temperature">Temperature extracted from the buffer.</param>
/// <param name="humidity">Humidity extracted from the buffer.</param>
private void ExtractSensorReadings(out float temperature, out float humidity)
{
    //
    //  Verify the checksum before extracting the data.
    //
    int sum = 0;
    for (int index = 4; index < 8; index++)
    {
        sum += _readFrameBuffer[index];
    }
    if ((sum & 0xff) == _readFrameBuffer[8])
    {
        //
        //  Checksum good so extract the temperature and humidity data.
        //
        humidity = ((float) ((_readFrameBuffer[4] * 256) + _readFrameBuffer[5])) / 10;
        int sign = 1;
        byte highByte = _readFrameBuffer[6];
        if ((highByte & 0x80) == 0x80)
        {
            sign = -1;
            highByte &= (byte) 0x7f;
        }
        temperature = sign * ((float) ((highByte * 256) + _readFrameBuffer[7])) / 10;
    }
    else
    {
        throw new Exception("ExtractSensorData: Checksum error, discarding data.");
    }
}

The code which gets the current readings is relatively trivial:

/// <summary>
/// This method calls the AddOne method on the GO! module and then waits for the
/// module to indicate that there is a response ready.  The response is then read
/// from the module and the resulting value is returned to the caller.
/// </summary>
public void GetReadings()
{
    _writeFrameBuffer[0] = GO_BUS10_COMMAND_RESPONSE;
    _writeFrameBuffer[1] = (int) Action.GetReadings;
    WriteDataToModule();
    if (!WaitForResponse())
    {
        throw new Exception("GetReadings: Cannot communicate with the DHT22 module");
    }
}

Setting an alarm is also trivial, although the method is longer, with much of the work involving extracting the bytes of data and storing them in the transmit buffer:

/// <summary>
/// Set the alarms for the low/high temperature/humidity alarms.
/// </summary>
/// <remarks>
/// To turn an alarm off set the value to float.MinValue.
/// 
/// The alarms are triggered when the temperature/humidity goes below the low threshold
/// or above the high threshold.
/// </remarks>
/// <param name="lowTemperature">Low temperature alarm value.</param>
/// <param name="highTemperature">High temperature alarm value.</param>
/// <param name="lowHumidity">Low humidity alarm value.</param>
/// <param name="highHumidity">High humidity alarm value.</param>
public void SetAlarms(float lowTemperature, float highTemperature, float lowHumidity, float highHumidity)
{
    Alarms alarms = 0;

    if (lowTemperature != float.MinValue)
    {
        if ((lowTemperature < MIN_TEMPERATURE) || (lowTemperature > MAX_TEMPERATURE))
        {
            throw new ArgumentOutOfRangeException("SetAlarms: lowTemperature out of range.");
        }
        short lt = (short) (lowTemperature * 10);
        _writeFrameBuffer[3] = (byte) ((lt & 0xff00) >> 8);
        _writeFrameBuffer[4] = (byte) (lt & 0xff);
        alarms |= Alarms.LowTemperature;
    }
    if (highTemperature != float.MinValue)
    {
        if ((highTemperature > MAX_TEMPERATURE) || (highTemperature < MIN_TEMPERATURE))
        {
            throw new ArgumentOutOfRangeException("SetAlarms: highTemperature out of range.");
        }
        short ht = (short) (highTemperature * 10);
        _writeFrameBuffer[5] = (byte) ((ht & 0xff00) >> 8);
        _writeFrameBuffer[6] = (byte) (ht & 0xff);
        alarms |= Alarms.HighTemperature;
    }
    if (lowHumidity != float.MinValue)
    {
        if ((lowHumidity < MIN_HUMIDITY) || (lowHumidity > MAX_HUMIDITY))
        {
            throw new ArgumentOutOfRangeException("SetAlarms: lowHumidity out of range");
        }
        short lh = (short) (lowHumidity * 10);
        _writeFrameBuffer[7] = (byte) ((lh & 0xff00) >> 8);
        _writeFrameBuffer[8] = (byte) (lh & 0xff);
        alarms |= Alarms.LowHumidity;
    }
    if (highHumidity != float.MinValue)
    {
        if ((highHumidity < MIN_HUMIDITY) || (highHumidity > MAX_HUMIDITY))
        {
            throw new ArgumentOutOfRangeException("SetAlarms: highHumidity out of range");
        }
        short hh = (short) (highHumidity * 10);
        _writeFrameBuffer[9] = (byte) ((hh & 0xff00) >> 8);
        _writeFrameBuffer[10] = (byte) (hh & 0xff);
        alarms |= Alarms.HighHumidity;
    }
    _writeFrameBuffer[0] = GO_BUS10_COMMAND_RESPONSE;
    _writeFrameBuffer[1] = (int) Action.SetAlarms;
    _writeFrameBuffer[2] = (byte) alarms;
    WriteDataToModule();
    if (!WaitForResponse())
    {
        throw new Exception("SetAlarms: Cannot communicate with the DHT22 module");
    }
}

The really interesting work involves the receipt of data from the module. This is triggered by the interrupt being raised on the GPIO pin of the Netduino GO! connector.

/// <summary>
/// Handle the IRQ events generated by the GO! module.
/// </summary>
/// <remarks>
/// The module raises an interrupt when a command has been processed or when there
/// is data ready for the module.  The first task for this method is to retrieve 
/// the buffer from the module and then work out what action should be taken.  The
/// module will have placed any relevant data into the write buffer prior to raising 
/// the interrupt.
/// </remarks>
private void _irqPort_OnInterrupt(uint data1, uint data2, DateTime time)
{
    _writeFrameBuffer[0] = GO_BUS10_COMMAND_RESPONSE;
    _writeFrameBuffer[1] = (int) Action.GetBuffer;
    WriteDataToModule();
    if ((_readFrameBuffer[1] == GO_BUS10_COMMAND_RESPONSE) && (_readFrameBuffer[2] == DHT22_ACK) && ReadBufferCRCCheckOK())
    {
        float t, h;

        switch ((Action) _readFrameBuffer[3])
        {
            case Action.GetReadings:
                ExtractSensorReadings(out t, out h);
                Temperature = t;
                Humidity = h;
                _irqPortInterruptEvent.Set();
                break;
            case Action.SetAlarms:
                _irqPortInterruptEvent.Set();
                break;
            case Action.Alarm:
                if ((SensorAlarm != null) && (AlarmInterruptsEnabled == AlarmState.InterruptsEnabled))
                {
                    ExtractSensorReadings(out t, out h);
                    SensorAlarm(this, new AlarmEventArgs(t, h, (Alarms) _readFrameBuffer[9]));
                }
                break;
            default:
                throw new ArgumentException("Interrupt: Unknown action " + _readFrameBuffer[3].ToString());
                break;
        }
    }
}

This event allows the main program to set an alarm and then leave the module to work out when it needs to communicate with the Netduino GO!. This is achieved by the application setting the delegate SensorAlarm

Testing

At this point we have the hardware and software complete. All that is needed now is to put the two together:

TemperatureModuleConnectedToGOAndSevenSegment

A quick test application (I promise, this will be last piece of the code in this article):

private static DHT22 module;

/// <summary>
/// Main program loop.
/// </summary>
public static void Main()
{
    SevenSegmentDisplay display = new SevenSegmentDisplay();
    display.SetBrightness(0.5F);
    display.SetValue("----");
    module = new DHT22();
    module.AlarmInterruptsEnabled = DHT22.AlarmState.InterruptsDisabled;
    module.SensorAlarm += new DHT22.SensorAlarmHandler(module_SensorAlarm);
    module.SetAlarms(35.0f, float.MinValue, float.MinValue, 10.0f);
    module.AlarmInterruptsEnabled = DHT22.AlarmState.InterruptsEnabled;
    Thread.Sleep(5000);
    int counter = 0;
    while (true)
    {
        try
        {
            module.GetReadings();
            Debug.Print("Reading: " + counter.ToString() + ", temperature: " + module.Temperature.ToString("f2") + "C, Humidity: " + module.Humidity.ToString("f2") + "%");
            string text = "Read  Temp  Hum " + (counter.ToString() + "    ").Substring(0, 5) + (module.Temperature.ToString("f1") + "C     ").Substring(0, 6) + (module.Humidity.ToString("f1") + "%     ").Substring(0, 5);
            display.SetValue(module.Temperature, 1);
            Thread.Sleep(2000);
            display.SetValue(module.Humidity, 1);
            Thread.Sleep(2000);
        }
        catch (Exception ex)
        {
            Debug.Print(ex.Message);
            display.SetValue("EEEE");
        }
        counter++;
        if (counter > 9999)
        {
            counter = 0;
        }
    }
}

/// <summary>
/// Catch the interrupt generated when one of the temperature/humidity (or both)
/// exceed the alarm values.
/// </summary>
private static void module_SensorAlarm(object sender, DHT22.AlarmEventArgs args)
{
    string message = "";
    if ((args.AlarmsRaised & DHT22.Alarms.HighHumidity) != 0)
    {
        message += "High humidity,";
    }
    if ((args.AlarmsRaised & DHT22.Alarms.HighTemperature) != 0)
    {
        message += "High Temperature,";
    }
    if ((args.AlarmsRaised & DHT22.Alarms.LowHumidity) != 0)
    {
        message += "Low Humidity, ";
    }
    if ((args.AlarmsRaised & DHT22.Alarms.LowTemperature) != 0)
    {
        message += "Low Temperature,";
    }
    message += " alarm raised: temperature = " + args.Temperature.ToString() + ", humidity = " + args.Humidity.ToString();
    Debug.Print(message);
}

The image above shows the DHT22 module connected to the Netduino GO!. A Komodex Labs Seven Segment Display is used to display the temperature and humidity.

And here is a video showing the module working. The system shows the temperature for a short while (16.8C) followed by the humidity (46.7%).

We were lucky last year as we had a particularly cold winter (lucky for testing). This allowed the module to be test outdoors in a reasonably cold environment. The sensor and the Netduino GO! performed as expected down to -10C when compared to an off the shelf digital thermometer and humidity unit. I would have waited for the temperature to drop further by my desire to prove the module has limits and I discovered that standing around outside with a coffee waiting for the module to read lower and lower temperatures had an interest threshold of about 10 minutes.

Conclusion

This module shows the power of combining microcontrollers to provide a combined system. The Netduino GO! would have found it difficult to have achieved the work completed by the STM8S. Similarly, the Netduino GO! provides the application developer with the simplicity and power of NETMF, something the STM8S could not achieve.

This module has yet to move from prototype into production. Maybe it will someday, just not today.