A few days ago I was asked for advice about pausing the STM8S for a long time period (in this case 30 seconds). I had to admit that I was not sure how to achieve this without using a timer and counting interrupts until the 30 time period had expired. A quick examination of the STM8S Programming Reference reveals that there is a simpler way of doing this. This post examines the Auto-Wakeup (AWU) feature of the STM8S and shows how this feature can be used to pause for a time period which can range from 15.625uS to 30.720s, assuming the clock source is accurate.
Auto-Wakeup (AWU) Feature
The AWU feature wakes the STM8S after a predefined time period following the microcontroller going into the active halt state. This feature can only be used when the microcontroller is halted and the accuracy is dependent upon the clock source.
The clock source is fed into a prescalar. The output from the prescalar is then used as a clock for a counter. The counter will cause an interrupt to be generated when the preset counter value has been reached.
It is also possible to determine the accuracy of the clock source by using the capture compare feature of TIM1 or TIM3 to measure the clock frequency.
Auto-Wakeup Registers
The function of the AWU is determined by the values in the AWU control registers.
Auto-Wakeup Enable – AWU_CSR1_AWUEN
Setting this bit to 1 enables the AWU function. Setting this to 0 disables the function.
These two registers together control the duration of the AWU. The exact duration of the wakeup period is determined by the AWU_APR_APR and AWU_TBR_AWUTB values from the following table.
fLS = f
fLS = 128 kHz
AWU_TBR_AWUTB
APRDIV Formula
AWU_APR_APR Range
2/f – 64/f
0.015625 ms – 0.5 ms
0001
APRDIV/fLS
2 to 64
2×32/f – 2x2x32/f
0.5 ms – 1.0 ms
0010
2 x APRDIV/fLS
32 to 64
2×64/f – 2x2x64/f
1 ms – 2 ms
0011
22 x APRDIV/fLS
32 to 64
22×64/f – 22×128/f
2 ms – 4ms
0100
23 x APRDIV/fLS
32 to 64
23×64/f – 23×128/f
4 ms – 8 ms
0101
24 x APRDIV/fLS
32 to 64
24×64/f – 24×128/f
8 ms – 16 ms
0110
25 x APRDIV/fLS
32 to 64
25×64/f – 25×128/f
16 ms – 32 ms
0111
26 x APRDIV/fLS
32 to 64
26×64/f – 26×128/f
32 ms – 64 ms
1000
27 x APRDIV/fLS
32 to 64
27×64/f – 27×128/f
64 ms – 128 ms
1001
28 x APRDIV/fLS
32 to 64
28×64/f – 28×128/f
128 ms – 256 ms
1010
29 x APRDIV/fLS
32 to 64
29×64/f – 29×128/f
256 ms – 512 ms
1011
210 x APRDIV/fLS
32 to 64
210×64/f – 210×128/f
512 ms – 1.024 s
1100
211 x APRDIV/fLS
32 to 64
211×64/f – 211×128/f
1.024 s – 2.048 s
1101
212 x APRDIV/fLS
32 to 64
211×130/f – 211×320/f
2.080 s – 5.120 s
1110
5 x 211 x APRDIV/fLS
26 to 64
211×330/f – 212×960/f
5.280 s – 30.720s
1111
30 x 211 x APRDIV/fLS
11 to 64
Where fLS = f is the formula which should be used when the inbuilt LSI is not being used.
It may not be possible to obtain an exact time period and the application may have to use the values which give the closest period.
When not in use, AWU_TBR_AWUB should be set to 0 in order to reduce power consumption.
Auto-Wakeup Flag – AWU_CSR1_AWUF
This flag is set when the AWU interrupt has been generated. The flag is cleared by reading the AWU control/status register (AWU_CSR1).
Auto-Wakeup Measurement Enable – AWU_CSR1_MSR
Setting this flag to 1 connects the output from the prescalar to one of the internal timers. This allows the application to accurately determine the clock frequency connected to the AWU prescalar. By measuring the clock frequency the application can adjust the values used for the prescalar divider and the timebase.
Software
Now we have the theory it is time to break out the STM8S Discovery board and the IAR compiler. The application starts pretty much the same as previous examples in this series, by setting the system clock and configuring a port for output:
//
// This program demonstrates how to use the Auto-Wakeup feature of the STM8S
// microcontroller.
//
// 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
//
#include <iostm8S105c6.h>
#include <intrinsics.h>
//--------------------------------------------------------------------------------
//
// Setup the system clock to run at 16MHz using the internal oscillator.
//
void InitialiseSystemClock()
{
CLK_ICKR = 0; // Reset the Internal Clock Register.
CLK_ICKR_HSIEN = 1; // Enable the HSI.
CLK_ECKR = 0; // Disable the external clock.
while (CLK_ICKR_HSIRDY == 0); // Wait for the HSI to be ready for use.
CLK_CKDIVR = 0; // Ensure the clocks are running at full speed.
CLK_PCKENR1 = 0xff; // Enable all peripheral clocks.
CLK_PCKENR2 = 0xff; // Ditto.
CLK_CCOR = 0; // Turn off CCO.
CLK_HSITRIMR = 0; // Turn off any HSIU trimming.
CLK_SWIMCCR = 0; // Set SWIM to run at clock / 2.
CLK_SWR = 0xe1; // Use HSI as the clock source.
CLK_SWCR = 0; // Reset the clock switch control register.
CLK_SWCR_SWEN = 1; // Enable switching.
while (CLK_SWCR_SWBSY != 0); // Pause while the clock switch is busy.
}
//--------------------------------------------------------------------------------
//
// Initialise the ports.
//
// Configure all of Port D for output.
//
void InitialisePorts()
{
PD_ODR = 0; // All pins are turned off.
PD_DDR = 0xff; // All pins are outputs.
PD_CR1 = 0xff; // Push-Pull outputs.
PD_CR2 = 0xff; // Output speeds up to 10 MHz.
}
The next step is to provide a method to initialise the AWU function:
//--------------------------------------------------------------------------------
//
// Initialise the Auto Wake-up feature.
//
//
//
void InitialiseAWU()
{
AWU_CSR1_AWUEN = 0; // Disable the Auto-wakeup feature.
AWU_APR_APR = 38;
AWU_TBR_AWUTB = 1;
AWU_CSR1_AWUEN = 1; // Enable the Auto-wakeup feature.
}
Firstly, the AWU function is disabled. The prescalar is set followed by the timebase. The final step is to re-enable the AWU.
//--------------------------------------------------------------------------------
//
// Auto Wakeup Interrupt Service Routine (ISR).
//
// Pulse PD0 for a short time. The NOP instructions generate a more stable
// pulse on the oscilloscope.
//
#pragma vector = AWU_vector
__interrupt void AWU_IRQHandler(void)
{
volatile unsigned char reg;
PD_ODR = 1;
asm("nop;");
asm("nop;");
PD_ODR = 0;
reg = AWU_CSR1; // Reading AWU_CSR1 register clears the interrupt flag.
}
This interrupt handler is triggered at the end of the AWU period. This handler simply pulses pin 0 on port D.
//--------------------------------------------------------------------------------
//
// Main program loop.
//
int main(void)
{
//
// Initialise the system.
//
__disable_interrupt();
InitialiseSystemClock();
InitialisePorts();
InitialiseAWU();
__enable_interrupt();
//
// Main program loop.
//
while (1)
{
__halt();
}
}
The AWU function is turned on when the microcontroller enters the active halt state. A halt instruction is generated by the __halt() method.
Running the above code on the STM8S Discovery board results in the following trace on the oscilloscope:
AWU 3208Hz Signal
and:
AWU Expanded Pulse
Examining the table above and the values used for the precalar and timebase we should be seeing a pulse with a frequency of approximately 3,368Hz. The actual value measured on the oscilloscope is 3,208Hz. The measurement on the oscilloscope will alway be slightly out due to the time it takes for the ISR to be setup and called. An ISR on the STM8S takes 9 clock cycles to be established, this equates to about 560nS on a system running at 16MHz. The actual difference is 6.25mS. The remainder of the difference comes from the fact that the LSI has an accuracy of +/-12.5%. A quick calculation shows the the LSI was running at about 121KHz at the time this post was written.
Conclusion
AWU offers a simple way of triggering processing at predetermined time periods. The active halt state puts the microcontroller into a low power mode whilst the microcontroller is waiting for the time period to elapse.
It should also be noted that using the LSI results in a slightly variable time period.
I’ve been playing with the TLC5940 for a few years now. I aim to eventually have it play a part in a larger project but I need to get a few things ironed out first. I’m currently on my second prototype board, well they are more proof of concept boards really.
This post is not really about the boards themselves but more about the mistakes I’ve made along the way. Hence the title of this post, Why do we prototype?
I think the simple answer is that we make mistakes.
Design Goals
The long term aim of this project is to use the TLC5940 to drive a grid of LEDs. The chip allows the connection of the LEDs directly to the chip but I want to use a transistor (or an equivalent) to turn the LEDs on and off and not the TLC5940 directly.
Breadboard
The breadboard circuit had all of the necessary components on the board and was pretty simple to get going. I started by connecting the LEDs directly to the TLC5940 and then built the software to run the circuit. The software runs on the STM8S103F3.
The next step is to connect one or more LEDs through a transistor. For this I used a PNP transistor and connected one of the LEDs using the transistor as a switch.
So far, so good. All is well with the world and I have some flashing LEDs.
The board (without the TLC5940s) looks like this:
Next step, prototype.
TLC5940 – Rev A
At this point I had designed a few boards and thought I’d push my SMD skills a little. I decided to use iTeads 5cm x 5cm manufacturing service and with the exception of the external connectors I would use SMD components only.
To give you an idea of the scale of this, the circuit requires 2 ICs plus 6 supporting discrete components. Each LED (and there are 16 of them) requires three discrete components for the driver plus the LED itself.
That is a pretty dense board for a beginner. Here is the 3D render of the board:
TLC5940 Rev A Prototype 3D Render
And the bare board itself:
TLC5940 Driver Board Rev A – Front
and the back:
TLC5940 Driver Board Rev A – Back
Lesson 1 – Track Routing
I’ve been using DesignSpark PCB for all of my designs and it’s a pretty good piece of software and I am very impressed. One feature I have only recently used in anger is the ability to turn off some of the layers. Have a look at the traces to the left of the board below:
TLC5940 Prototype Rev A Routed Traces
This did not really cause an issue as there was no routing error but I could have routed these tracks more elegantly. The problem was caused by me having both the top and bottom traces visible at the same time. In my mind I had to route these tracks around the traces on the top layer as well as the artefacts on the bottom layer. Hence I took the traces around the via when I could have taken them directly from the via on the right (as viewed from below).
I would have spotted this more logical routing had I turned off the top copper view as soon as the trace passed through to the bottom copper layer.
Not really an error, more of a cosmetic nicety.
Lesson 2 – Use the Same Parts
Between the breadboard stage and the PCB stage I changed the part used in the driver. Not really too much of an issue except…
I did not get an equivalent PTH part and test it on the breadboard first.
As it stands the transistor driver circuit does not function and needs some more attention.
To enable testing of the remainder of the circuit you can use the following work around. Abandon the driver and change the value of the IREF resistor to allow a small enough current through the LED.
Lesson 3 – 0603 Parts Are Small Enough For the Hobbyist
Some of the parts I used in the design are 0402. These are small parts and really difficult to solder. It is especially difficult to see markings on the components.
In future I think 0603 is as small as I’ll go.
TLC5940 – Rev B
The Rev A board was a bit of a disaster. I never really managed to get the board working so it was time to go back to first principles and build a simpler board. Enter Revision B.
Revision B of the board will have a reduced brief. This board will use a mixture of SMD and PTH components. The STM8S and supporting components will be surface mount but the TLC5940’s will be PTH. I’m getting reasonably confident that I can get the STM8S on a board and working even in surface mount format.
Boards ordered, arrived and assembled. Here’s is a photo of it working:
Lesson 3 – Vcc and Ground Are Not Interchangeable
In redesigning the circuit I had to replace the TLC5940 component and so added the new one and changed the resistor value for the LEDs I’d be working with. Except I connected the IREF resistor to Vcc instead of ground.
TLC5940 Prototype Rev B Working
Notice that the places for R1 and R2 are empty. That’s because they are on the bottom of the board:
TLC5940 Prototype Rev B Resistors
Lesson 4 – Plastic Melts At Low Temperatures
For this circuit I use a single AND gate to square off the CCO output from the STM8S. This small IC was placed onto the board after the connector for the ST-Link programmer. The only problem was that I was using a hot air rework station to fix this part to the board. The hot air from the rework station caused some bubbling on the plastic of the connector.
ST-Link Connector
I suppose this brings me to the next lesson.
Lesson 5 – Use the Board Luke
OK, so there’s been too much Star Wars on TV over Christmas.
What I really mean is, when prototyping, use all of the board available to you. The manufacturing process I used allowed for a 10cm x 5cm board (or anything up to that size) for a fixed price. For this board there is a lot of spare real estate and I could, with ease, have put that connector and the IC where they would not have interfered with each other.
The final board may have to be compact and fit into a location determined by the rest of the project but when testing you might as well use all of the space to your advantage.
Conclusion
I have a working Revision B board which mean I can free up the breadboard for other work but there is still some way to go before I have the final project completed. As with all things in life, this is a learning experience.
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
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
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.
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:
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.
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.
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
Clock signal:
38.4KHz Clock Signal
Combined output:
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
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
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
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.
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
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:
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
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.
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:
Pin
Connection
1
VDD (3.3-5V)
2
Data/Signal
3
Ground
4
Ground
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:
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:
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:
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.
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:
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.
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:
Send the start signal and wait for 1-10ms
Enter read mode and collect data
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:
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:
A quick test application (I promise, this will be last piece of the code in this article):
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.
So far in The Way of the Register series we have only looked at SPI from a slave device point of view as we have been working towards creating a Netduino GO! module. For every slave device there must be a master, here we will look at configuring the STM8S to operate in SPI master mode.
The project will look at controlling a TLC5940 in order to emulate the work described in the post TLC5940 16 Channel PWM Driver. We could simply bit-bang the data out to the chip but instead we will use the SPI interface to achieve this.
The project breaks down into the following steps:
Generate the grey scale clock and blank signals
Bit-Bang data out over GPIO pins to create an operational circuit
Convert the data transmission to SPI
See the quoted post for a description of how this chip works and for an explanation of the terminology used.
Generating the Grey Scale Clock and Blank Signals
The TLC5940 generated 4,096 grey scale values by using a PWM counter. Once the counter reaches 4096 pulses it stops until it is told to restart. The Blank pulse acts as a restart signal. This project will be controlling LEDs and so will want to continuously keep the counter running. If we did not keep the counter in the TLC5940 running then the LEDs would light for a short while and then simply turn off and remain off.
The greyscale clock is generated by using the Configurable Clock Output (CCO) pin on the STM8S. This pin simply outputs the clock pulses used to drive the STM8S. Reviewing the data sheet we find that the maximum value for the grey scale clock is 30MHz. Using out standard clock initialisation generates a clock with a frequency of 16MHz (approximately). This is well within the tolerances of the TLC5940. To output this we need to make a simple modification to our standard code, name change the line:
CLK_CCOR = 0; // Turn off CCO.
to:
CLK_CCOR = 1; // Turn on CCO.
The starting point for our application becomes:
#if defined DISCOVERY
#include <iostm8S105c6.h>
#elif defined PROTOMODULE
#include <iostm8s103k3.h>
#else
#include <iostm8s103f3.h>
#endif
#include <intrinsics.h>
//
// Setup the system clock to run at 16MHz using the internal oscillator.
//
void InitialiseSystemClock()
{
CLK_ICKR = 0; // Reset the Internal Clock Register.
CLK_ICKR_HSIEN = 1; // Enable the HSI.
CLK_ECKR = 0; // Disable the external clock.
while (CLK_ICKR_HSIRDY == 0); // Wait for the HSI to be ready for use.
CLK_CKDIVR = 0; // Ensure the clocks are running at full speed.
CLK_PCKENR1 = 0xff; // Enable all peripheral clocks.
CLK_PCKENR2 = 0xff; // Ditto.
CLK_CCOR = 1; // Turn on CCO.
CLK_HSITRIMR = 0; // Turn off any HSIU trimming.
CLK_SWIMCCR = 0; // Set SWIM to run at clock / 2.
CLK_SWR = 0xe1; // Use HSI as the clock source.
CLK_SWCR = 0; // Reset the clock switch control register.
CLK_SWCR_SWEN = 1; // Enable switching.
while (CLK_SWCR_SWBSY != 0); // Pause while the clock switch is busy.
}
//
// Main program loop.
//
void main()
{
//
// Initialise the system.
//
__disable_interrupt();
InitialiseSystemClock();
__enable_interrupt();
while (1)
{
__wait_for_interrupt();
}
}
Wiring up the STM8S and connecting the scope to PC4 (CCO output pin) gives the following trace on the scope:
CCO On Scope
The trace on the scope has a minimum value of around 680mV and a maximum of 2.48V. In an ideal world this signal should range from 0 to 3.3V (based upon a 3.3V supply). Adding an inverter from a 74HC04 and feeding the signal through one of the gates gives the following trace:
Inverter output on the scope
This is starting to look a lot better. The next task is to create the Blank signal. There are several ways of doing this. The most automatic way of doing this is to generate a very short PWM pulse using one of the timers in the STM8S. One drawback of this method is that it is more difficult to generate a Blank pulse on demand. Instead we will use the interrupt method described in the same article. Whilst not automatic it is still a trivial task to complete. We simply modify the code from the method to load the counters with 4,096. The code for the GPIO port, timer and interrupt becomes:
//
// Timer 2 Overflow handler.
//
#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
PD_ODR_ODR4 = 1;
PD_ODR_ODR4 = 0;
TIM2_SR1_UIF = 0; // Reset the interrupt otherwise it will fire again straight away.
}
//
// Setup Timer 2 to generate an interrupt every 4096 clock ticks.
//
void SetupTimer2()
{
TIM2_PSCR = 0x00; // Prescaler = 1.
TIM2_ARRH = 0x10; // High byte of 4096.
TIM2_ARRL = 0x00; // Low byte of 4096.
TIM2_IER_UIE = 1; // Turn on the interrupts.
TIM2_CR1_CEN = 1; // Finally enable the timer.
}
//
// Setup the output ports used to control the TLC5940.
//
void SetupOutputPorts()
{
PD_ODR = 0; // All pins are turned off.
PD_DDR_DDR4 = 1; // Port D, pin 4 is used for the Blank signal.
PD_CR1_C14 = 1; // Port D, pin 4 is Push-Pull
PD_CR2_C24 = 1; // Port D, Pin 4 is generating a pulse under 2 MHz.
}
Hooking up the scope to PD4 gives the following trace:
Blanking pulses
The single pulses are being generated at a frequency of approximately 3.9kHz. A little mental arithmetic dividing the 16MHz clock by 4,096 comes out to about 3,900.
Zooming in on the signal we see:
Single Blanking Pulse
This shows that the signal is 125nS wide. This is acceptable as the minimum pulse width given in the data sheet is 20nS.
So at this point we have the 16MHz grey scale clock signal and the Blank pulse being generated every 4,096 clock pulses.
Connecting the TLC5940
The next task is to connect the STM8S to the TLC5940. You should refer to the article TLC5940 16 Channel PWM Driver for more information on the pins and their meaning. For this exercise we will use the following mapping:
STM8S Pin
TLC5940 Pin
PD4
Blank (pin 23)
PD3
XLAT (pin 24)
PD2
VPRG (pin 27)
PD5
Serial data (pin 26)
PD6
Serial clock (pin 25)
PC4 (via inverter)
GSCLK (pin 18)
You will note that the serial data and clock are currently connected to PD5 and PD6 respectively. Whilst the eventual aim is to communicate with the TLC5940 via SPI, the initial communication will be using Bit-Banging. We will move on to using SPI once the operation of the circuit has been proven using tested technology.
The first changes we will have to make create some #define statements to make the code a little more readable. We also add some storage space for the grey scale and dot correction data.
//
// Define which pins on Port D will be used as control signals for the TLC5940.
//
// BLANK - A pulse from low to high causes the TLC5940 to restart the counter
// XLAT - A high pulse causes the data to be transferred into the DC or GS registers.
// VPRG - Determines which registers are being programmed, High = DC, Low = GS.
//
#define PIN_BLANK PD_ODR_ODR4
#define PIN_XLAT PD_ODR_ODR3
#define PIN_VPRG PD_ODR_ODR2
//
// Bit bang pins.
//
#define PIN_BB_DATA PD_ODR_ODR5
#define PIN_BB_CLK PD_ODR_ODR6
//
// Values representing the modes for the VPRG pin.
//
#define PROGRAMME_DC 1
#define PROGRAMME_GS 0
//
// TLC5940 related definitions.
//
#define TLC_NUMBER 1
#define TLC_DC_BYTES_PER_CHIP 12
#define TLC_DC_BYTES TLC_NUMBER * TLC_DC_BYTES_PER_CHIP
#define TLC_GS_BYTES_PER_CHIP 24
#define TLC_GS_BYTES TLC_NUMBER * TLC_GS_BYTES_PER_CHIP
//
// Next we need somewhere to hold the data.
//
unsigned char _greyScaleData[TLC_GS_BYTES];
unsigned char _dotCorrectionData[TLC_DC_BYTES];
The Bit-Banging methods should look familiar to anyone who has been reading any of the posts in The Way of the Register series.
//--------------------------------------------------------------------------------
//
// Bit bang data.
//
// TLC5940 expects the data to be shifted MSB first. The data
// is shifted in on the rising edge of the clock.
//
void BitBang(unsigned char byte)
{
for (short bit = 7; bit >= 0; bit--)
{
if (byte & (1 << bit))
{
PIN_BB_DATA = 1;
}
else
{
PIN_BB_DATA = 0;
}
PIN_BB_CLK = 1;
PIN_BB_CLK = 0;
}
PIN_BB_DATA = 0;
}
//--------------------------------------------------------------------------------
//
// Bit bang a buffer of data.
//
void BitBangBuffer(unsigned char *buffer, int size)
{
for (int index = 0; index < size; index++)
{
BitBang(buffer[index]);
}
}
Related to the Bit-Banging methods are the two methods which will send the grey scale and dot correction data:
//--------------------------------------------------------------------------------
//
// Send the grey scale data to the TLC5940.
//
void SendGreyScaleData(unsigned char *buffer, int length)
{
PIN_VPRG = PROGRAMME_GS;
BitBangBuffer(buffer, length);
PulseXLAT();
PulseBlank();
}
//--------------------------------------------------------------------------------
//
// Send the dot correction buffer to the TLC5940.
//
void SendDotCorrectionData(unsigned char *buffer, int length)
{
PIN_VPRG = PROGRAMME_DC;
BitBangBuffer(buffer, length);
PulseXLAT();
PulseBlank();
}
We also need a few methods to make the TLC5940 latch the data and also restart the counters:
//--------------------------------------------------------------------------------
//
// Pulse the Blank pin in order to make the TLC5940 reload the counters and
// restart timer.
//
void PulseBlank()
{
PIN_BLANK = 1;
PIN_BLANK = 0;
}
//--------------------------------------------------------------------------------
//
// Pulse the XLAT pin in order to make the TLC5940 transfer the
// data from the latches into the appropriate registers.
//
void PulseXLAT()
{
PIN_XLAT = 1;
PIN_XLAT = 0;
}
Next we need to set the initial condition. For this we set the TLC dot correction off and also turn all of the LEDs off:
//--------------------------------------------------------------------------------
//
// Initialise the TLC5940.
//
void InitialiseTLC5940()
{
for (int index = 0; index < TLC_DC_BYTES; index++)
{
_dotCorrectionData[index] = 0xff;
}
for (int index = 0; index < TLC_GS_BYTES; index++)
{
_greyScaleData[index] = 0;
}
SendDotCorrectionData(_dotCorrectionData, TLC_DC_BYTES);
SendGreyScaleData(_greyScaleData, TLC_GS_BYTES);
}
The final support method we need to add is the method which sets the brightness of an LED. The brightness is a 12-bit value (0-4095). This means each LED uses 1.5 bytes for the brightness value. The following methods breaks down the value and ensures that the correct bits are set in the grey scale buffer depending upon which LED is being changed:
//--------------------------------------------------------------------------------
//
// Set the brightness of an LED.
//
void SetLEDBrightness(int ledNumber, unsigned short brightness)
{
int offset = (ledNumber >> 1) * 3;
if (ledNumber & 0x01)
{
_greyScaleData[offset + 1] = (unsigned char) (_greyScaleData[offset + 1] & 0xf0) | ((brightness & 0x0f00) >> 8);
_greyScaleData[offset + 2] = (unsigned char) (brightness & 0xff);
}
else
{
_greyScaleData[offset] = (unsigned char) ((brightness & 0x0ff0) >> 4) & 0xff;
_greyScaleData[offset + 1] = (unsigned char) ((brightness & 0x0f) >> 4) | (_greyScaleData[offset + 1] & 0x0f);
}
}
We should also create a similar method for changing the dot correction value for an LED. This is left as an exercise for the reader as we will not be changing this value in this code.
Proving the concept
If we have connected the TLC5940 correctly and our code works we should be able to connect up some LEDs (common anode) to the TLC5940 and change the brightness under program control.
This main program loop slowly increases the brightness of the LEDs. When they are at full brightness they are turned off and the process starts again:
//--------------------------------------------------------------------------------
//
// Main program loop.
//
void main()
{
//
// Initialise the system.
//
__disable_interrupt();
InitialiseSystemClock();
SetupOutputPorts();
SetupTimer2();
InitialiseTLC5940();
__enable_interrupt();
//
// Main program loop.
//
int brightness = 0;
int counter = 0;
while (1)
{
__wait_for_interrupt();
counter++;
if (counter == 20)
{
TIM2_CR1_CEN = 0;
counter = 0;
for (int index = 0; index < 16; index++)
{
SetLEDBrightness(index, brightness);
}
SendGreyScaleData(_greyScaleData, TLC_GS_BYTES);
brightness++;
if (brightness == 4096)
{
brightness = 0;
}
TIM2_CR1_CEN = 1; // Finally re-enable the timer.
}
}
}
If you connect a scope to the cathode of one of the LEDs you will see that the wave form slowly changes over time. At the start, the LED is fully on and the trace on the scope shows a horizontal line, i.e. a constant voltage. As time moves on and the value in the dot correction buffer changes you start to see a PWM signal similar to the following:
PWM Output On Scope 1
This trace shows the signal when the LEDs are a little brighter:
PWM Output On Scope 2
Having arrived here we now know that the circuit has been connected correctly and that the control logic in the main method works. We can now move on to considering what we need to do in order to use SPI in master mode. The aim will be to simply remove the Bit-Banging methods and replace these with an interrupt driven SPI master algorithm.
SPI Master
So now we have a working circuit we need to look at SPI on the STM8S. Firstly let’s remind ourselves of the serial communication parameters for the TLC5940. This chip reads the data on the leading clock edge (CPHA = 1). We have also set the clock idle state to low (CPOL = 0).
It is also advisable to start off using the lowest clock speed for SPI in order to confirm correct operation of the software and circuit. Lower speed are less likely to be subject to interference.
SPI Registers
You should review the previous articles on SPI communication if you are not already familiar with the SPI registers we have used so far. In this post we will only discuss the new settings required to switch from being a SPI slave device to a SPI master device.
SPI_CR1_BR – Baud Rate Control
The SPI baud rate is determined by the master clock frequency and the value in this register. The divisor used to set the baud rate according to the following table:
SPI_CR1_BR
Divisor
000
2
001
4
010
8
011
16
100
32
101
64
110
128
111
256
The SPI baud rate is calculated as fmaster / divisor. So for our master clock speed of 16MHz we get the lowest clock speed of 16,000,000 / 256, or 62,500Hz.
SPI_CR1_MSTR – Master Selection
Setting this bit switches SPI into master mode (see also SPI_CR1_SPE).
Note that the reference for the STM8S also states that this bit (and SPI_CR1_SPE) will only remain set whilst NSS is high. It this therefore essential to connect NSS to Vcc if this device is not being used as a slave device.
Implementing SPI
Using SPI presents us with a small problem, namely the program will have to start to operate in a more asynchronous way. The code presented so far has only one interrupt to be concerned with, namely the timer used to control the Blank signal. Adding SPI to the mix means that we will have to also consider the SPI interrupt as well. It also adds the complication of sending dot correction data followed by grey scale data. This last problem will not be covered here and is left as an exercise for the reader.
The initialisation code merely sets things up for us:
//--------------------------------------------------------------------------------
//
// Initialise SPI to be SPI master.
//
void SetupSPIAsMaster()
{
SPI_CR1_SPE = 0; // Disable SPI.
SPI_CR1_CPOL = 0; // Clock is low when idle.
SPI_CR1_CPHA = 0; // Capture MSB on first edge.
SPI_ICR_TXIE = 1; // Enable the SPI TXE interrupt.
SPI_CR1_BR = 7; // fmaster / 256 (62,500 baud).
SPI_CR1_MSTR = 1; // Master device.
}
Much of the code should be familiar as it has been used in previous posts discussing SPI slave devices. Not however that we do not enable SPI at this point. We simply set the scene for us to use SPI later.
The SPI data transfers will be controlled by using an interrupt service routine:
//--------------------------------------------------------------------------------
//
// SPI Interrupt service routine.
//
#pragma vector = SPI_TXE_vector
__interrupt void SPI_IRQHandler(void)
{
//
// Check for an overflow error.
//
if (SPI_SR_OVR)
{
(void) SPI_DR; // These two reads clear the overflow
(void) SPI_SR; // error.
return;
}
if (SPI_SR_TXE)
{
//
// Check if we have more data to send.
//
if (_txBufferIndex == _txBufferSize)
{
while (SPI_SR_BSY);
SPI_CR1_SPE = 0;
_txBuffer = 0;
PulseXLAT();
PulseBlank();
TIM2_CR1_CEN = 1;
}
else
{
SPI_DR = _txBuffer[_txBufferIndex++];
}
}
}
The main works starts when we have established that the transmit buffer is empty (SPI_SR_TXE is set). If we have more data then we put the byte into the data register (SPI_DR). If we have transmitted all the data we have then we wait for the last byte to complete transmission (SPI_SR_BSY becomes false) before we start to terminate the end of the SPI communication.
In order to send some data we really just need to setup the pointers and counters correctly and then enable SPI. So the SendGreyScaleData method becomes:
//--------------------------------------------------------------------------------
//
// Send the grey scale data to the TLC5940.
//
void SendGreyScaleData(unsigned char *buffer, int length)
{
PIN_VPRG = PROGRAMME_GS;
_txBuffer = buffer;
_txBufferIndex = 0;
_txBufferSize = length;
TIM2_CR1_CEN = 0;
SPI_CR1_SPE = 1;
}
We also need to have a look at the main program loop as we use the __wait_for_interrupt() method in order to determine when we should start to process the next LED brightness value. We now need to ignore the interrupts when SPI is enabled otherwise the brightness will increase each time the transmit buffer is empty. A crude implementation eliminating the SPI interrupts is:
int brightness = 0;
int counter = 0;
while (1)
{
__wait_for_interrupt();
if (!SPI_CR1_SPE)
{
counter++;
if (counter == 20)
{
TIM2_CR1_CEN = 0;
counter = 0;
for (int index = 0; index < 16; index++)
{
SetLEDBrightness(index, brightness);
}
SendGreyScaleData(_greyScaleData, TLC_GS_BYTES);
brightness++;
if (brightness == 4096)
{
brightness = 0;
}
TIM2_CR1_CEN = 1; // Finally re-enable the timer.
}
}
}
Making these changes and running the code shows that the system operated as before.
Increasing the Baud Rate
As noted earlier, the baud rate has been set low in order to reduce the chance of any problems being experienced due to interference. Now we have established that using SPI communication is possible and the circuit works as before we can start to increase the baud rate. Using our 16MHz clock we find we have the following baud rates which are theoretically possible:
SPI_CR1_BR
Divisor
SPI Frequency
000
2
8 MHz
001
4
4 MHz
010
8
2 MHz
011
16
1 MHz
100
32
500 KHz
101
64
250 KHz
110
128
125 KHz
111
256
62.5 KHz
A little experimentation is called for. Being ambitious I started with a clock frequency of 1MHz. This resulted in a flickering effect on the LED display. So, 1MHz is too ambitious, let’s start to reduce the frequency. I finally settled on 250 KHz as this allowed the circuit to function as before.
Conclusion
Using SPI master for data transmission was not as difficult as I originally thought. To make this application complete there are a few tasks to follow up on, namely:
Receiving data over SPI
Create the method to allow setting the dot correction values
Transmitting buffers from a queue
Minor tidying up of the timer control
The use of SPI here actually increased the time taken (597uS Bit-Banging c.f. 795uS for 250 KHz SPI) to reliably send the grey scale data to the TLC5940. I suspect that the time can be decreased if the circuit was taken from breadboard and put onto a PCB manufactured for the purpose. The breadboard for this circuit currently looks like this:
Bread Board And Flying Leads
As you can see, there is a lot of opportunity for interference with all those flying leads.
While the time taken might have increased, the load on the microcontroller will have decreased as the SPI method is interrupt driven. The actual transmission is off-loaded to the microcontrollers dedicated circuitry.
Making the OutputExpander module has been an interesting journey. The original drawings started in August 2012 and then sat on the hard drive for about eight months. Much of the time following the original drawings were spent working out how the STM8S worked. You can find out more in The Way of the Register series (something I will pick up again soon, to my mind there are a few missing topics).
For those who do not know, I’m a software engineer and electronics is a hobby. The prospect of designing a board and using SMD components would have been unthinkable to me two years ago. Today I sit here with my first prototype PCB connected to a commercial board and the output looks reasonably professional – well I’ll let you decide.
Completed Board
Not looking too bad if I say so myself.
So let’s look at what I have learned and also how long the project took.
Lessons Learned
With all projects we should look back and learn from the experience, both good and bad. So here are a few things I have learned over the past few months.
Designing the Board
The original design started life in August 2012. I probably should have taken the plunge and developed the board a little quicker than I did although in truth, I did not get the major requirement of the board, namely GoBus 1.0 really sorted out until late November 2012.
Prototyping
This was probably the simplest bit of the project. I have all of the standard components in my toolbox already and I also have the tools required. This was really a case of getting the system working. The hardest part was getting to grips with the STM8S, a story I have documented in The Way of The Register series of posts.
Schematic
During this part of the design phase I tried several different packages. All of them had strengths and weaknesses. I finally settled on DesignSpark. For me this package had three major strengths:
It’s free
You can use it for commercial projects
It feels like a Windows application
In fairness it does have a few weaknesses. The most obvious for me was the lack of the ability to add images of any kind to the design. Come on, at version 5 you should have this one!
Nets – I discovered these when producing the final draft of the schematic. These allowed the separation of the nets into logical groups/functions/areas. It made the schematic a lot cleaner.
Schematic to Manufacture
For me this was the where I learned the most. The first thing I learned was that auto-routers are dreadful. They are slow and produce some interesting board layouts. This board is a simple board and yet the auto-router still took a long time to make a half-hearted attempt at routing the board. In the end I did this manually. This was not too much of a problem as the board was simple.
Next, you have to learn to think in three dimensions. You have two layers so use them.
The Netduino modules produced by Secret Labs have nice rounded corners – these are a devil to produce in DesignSpark. I think that the module I produced has one corner which is slightly different from the others.
The cost of prototyping is a lot lower than I thought. Ten boards including shipping costs about £18 and only took 10 days.
I now know what 0403 means. The ‘0’ stands for Ohhh my goodness that’s small. Seriously, the four digits should be split into two and thy give the dimensions of the component. So for a metric component an 0403 part is 0.4 x 0.3 mm – that’s small.
The STM8S part selected has a 0.65mm pitch for the pins. I originally found this a little worrying. Don’t be afraid – they are not that bad.
Get a USB microscope when soldering SMD components. This tools is cheap and allows the examination of joints for shorts. The quality will never be great, mine only runs at 640×480, but a 400x zoom means you can be sure that you have no problems.
Add test points. There came a point when I was making the board and I needed to see the data going through to the 74HC595’s. I did not have a suitable connection and so I had to solder a piece of wire to the board:
Improvised Test Point
A good test point would have made this easier.
How Long Did it Take
The original drawing started in August 2012 and the final board was put together and tested only yesterday. So in elapsed time that’s about nine months. In real working time this broke down as follows:
Activity
Duration (Hours)
Building Prototype circuit
2
Prototype software
3
Schematic
6
PCB layout
20
Assembly and testing
5
Enhanced software
4
Total
40
Something to bear in mind is that no production evaluation or component selection has been conducted as part of this project. It was supposed to be the final item on the list. I am still not sure if this should be taken through to manufacture – time will tell.
Another item to be considered is to achieve the Netduino GO! logo approval. At the time of writing this required the approval of the board by Secret Labs – this activity has not been completed.
Conclusion
Well, that was a hectic few weeks.
Did I enjoy it – YES!
Would I recommend that you try it – YES!
As for me, I’ll be taking a few days off of hardware development and blogging. Love doing it but it can take it’s toll.
I suppose you may be interested in some downloads…
If you use any of the code or techniques discussed in this series of posts then please let me know as I’m interested in what other people are doing with this work.
This website uses cookies to improve your experience and to gather page view statistics. This site does not collect user information. Accept & CloseRead More
Privacy & Cookies Policy
Privacy Overview
This website uses cookies to improve your experience while you navigate through the website. Out of these, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may affect your browsing experience.
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
Any cookies that may not be particularly necessary for the website to function and is used specifically to collect user personal data via analytics, ads, other embedded contents are termed as non-necessary cookies. It is mandatory to procure user consent prior to running these cookies on your website.