Bluetooth Controlled Led Driver
by Matthew Ford 10th May 2009 (revised 23rd
February 2010 – code updated)
© Forward Computing and Control Pty. Ltd. NSW Australia
All rights reserved.
NO SMD required
An Android Controlled LED Driver is also described here – No Android programming required.
In this tutorial you will see how to code an RS232 interface for the Atmel Attiny84, how to covert the previous ThreeLevelLedDriver to a multiple interrupt structure and how to integrate the RS232 code with the LedDriver. Finally you will see how to modify the driver so that the led is controlled from the computer over the bluetooth connection and the ADC reading, the led current, is sent back to the computer. If you are only interested in the RS232 software module, I have included a link to a circuit with the LED controller parts removed.
This first part of the tutorial covers the hardware and software for a basic RS232 interface from the Attiny84 to your computer. Part 2 of the tutorial will show you how to extend the Basic uC Led Driver to add Bluetooth Control. At the end of the tutorial you will have a Led Driver that you can both control from your computer (or other bluetooth terminal) and that can send you updates on its status.
Here is the completed project.
Adding Bluetooth to my torches was first suggested, in jest, by a work colleague. However then I came across the bluetooth modules sold by SparkFun (www.sparkfun.com) and realised how practical was. What would you use the bluetooth connection for?
Remote control of led lighting
Configuration/Programming of the torch modes without having to go through multiple complex button press sequences.
Monitoring of battery charge
Real time debugging of the led controller
Other uses include remote temperature sensing. The
Attiny 24/44/84 has a built-in temperature sensor so you can use the
bluetooth connection to remotely sense temperature. A more accurate
temperature sensor can be made using a NTC resistor (this may be the
subject of later tutorial if there is enough interest).
My initial use for the bluetooth connection will be for testing of the development of the battery charger I am building into my torch. The bluetooth connection will allow me to monitor, real time, the uC reading of battery temperature each 30sec while the uC is controlling the battery charging.
In addition to the parts and
programmer already used for the Basic
uC 3 Level Led Driver you need the following:-
1 x veroboard
4 x 1uF 25V tag or ceramic caps.
1 x MAX232, an RS232 driver
1 x 16pin IC socket
1 x female 9pin serial connector
1 x serial cable or usb-serial cable
1 x Multimeter with 8Mhz frequency measurement
1 x 6 pin 0.1" pitch, right angle header and matching plug (for the bluetooth module below)
1 x Bluetooth www.sparkfun.com BlueSMiRF-Gold module WRL-00582 (optional see text)
1 x Bluetooth www.sparkfun.com Modem - BlueDongle USB WRL-08180 (optional see text)
1 x USB miniB Cable (www.sparkfun.com CAB-00598 see text)
A multimeter with an accurate (<1%) frequency function capable of measuring up to >8Mhz is recommended to trim the uC clock to 8Mhz. Many cheap multimeters have this function. My circuit ran fine without trimming, so if you don't have this function it might still work for you.
The other parts (excluding the bluetooth modules) are only a few dollars (assuming you already have a serial cable). However the bluetooth modules are US$60 each. They provide a drop in replacement for the serial driver (MAX232) and cable, so you can complete all the coding and operations in this tutorial with out that additional expense. Also if you computer has bluetooth serial capability you won't need the Modem - BlueDongle USB. Sparkfun also offer cheaper USB modules and USB dongles you can use. Sparkfun also sell strips of right angle headers and strips of matching plugs, I used a locally sourced 6 pin header and plug.
Because the the RS232 interface is cheaper then the full bluetooth solution and still allows all the software to be developed and tested we will start with it. It also eliminates the bluetooth setup and connectivity as a possible source of problems.
Here is the completed RS232 version of the project.
If you are only building the basic RS232 module then you don't need to led driving parts. A basic RS232 circuit without the LED driver parts is here.
The circuit has three additions to the previous Led Driver circuit. (http://www.forward.com.au/uCLedDriver/build_basic_uC_led_driver.html) The supply is now 5V as the RS232 driver chip needs between 4.5 and 5.5V. My adjustable suppy had choices of 4.5V and 6V. Do not use 6V as this is too high for both the uC and the RS232 driver chip. I used the 4.5V setting.
This supply is connected to the LED via 3.3ohm 5W resistor. This resistor limits the current to the LED to less then 0.5A and is included as protection so that if the uC turns the controlling FET, U2, on hard for any length of time you do not burn out the LED. This can happen due to programming errors or in the normal course of single stepping through the program in debug mode.
On the right of the circuit, the RS232 driver chip, the MAX232, has been added. The MAX232 is mounted on its own piece of vero board and will be replaced later with a bluetooth module which is also powered by 5V. Finally there is a female 9pin serial connector for connection to the computer.
Take out the attiny84 chip, add the protection resistor and wire up the 16pin socket for the MAX232. If you are using polarised tag capacitors note carefully the polarity shown on the circuit.
Double check your wiring and then with both chips still unplugged, apply power and check that the following voltages. If you are using a 4.5V supply like me than the 5V readings below will be 4.5V instead
5V at the protection resistor with respect to GND,
+5V between pins 1(+) and 14(-) of the attiny84 socket
+5V between pins 2(+) and 6(-) of the 6pin programming header SV2
+5V between pins 16(+) and 15(-) of the MAX232 socket
0V between pins 9 and 15 and between 10 and 15 of the MAX232 socket, pins 9 and 10 should be wired to the attiny84
check that the voltage between all pins and pin 14(-) of the attiny84 socket is <= 5V
check that the voltage between all pins and pin 15(-) of the MAX232 socket is <=5V
Plug in the MAX232 only. Leave the attiny84 unplugged.
Apply power and check
-9 to -10V between pin 7 and GND (pin 15)
+5V between pin 9 and GND (pin 15)
To connect your PC to a serial device you need some software to talk to the COM port. I use TeraTerm (http://ttssh2.sourceforge.jp) but there are lots of other possiabilities, including Hyperterminal.
With the Attiny84 still unplugged, connect the Serial cable from your computer to RS232 9 pin socket and start your terminal software. When the software starts select the appropriate COM port.
Here I am using a USB to Serial Cable converter.
The default serial settings in Tera Term are 9600 baud, 1 start, 8 data, 1 stop and no parity which is just what we want. You can access this setup from the menu Setup → SerialPort...
Finally, click in the main TeraTerm window and hold down the 'a' key. You won't see any thing because the local echo option is off (Setup->Terminal, Local Echo tick box) but you should see some reading on your frequency meter connected between pin 9 and GND of the MAX232 chip or between pins 3 and 5 of the serial connector. I read about 120Khz.
This confirms your PC serial connection is working.
Now unplug the MAX232 chip and plug in the Attiny84 chip so we can program it without having to worry about the drive voltage from pin 9 of the MAX232.
Before getting into programming the attiny84, here is some background on the RS232 protocol we will be using.
RS232 is a standard for sending serial data between two devices. It can be either synchronous or asynchronous and can run at various speeds and with various number of data bits, stop bits and parity types. Each end must be pre-set to the particular settings used to transmit the data. For this project we will use 9600 baud, 1 start bit, 8 data bits, 1 stop bit and no parity.
RS232 uses +/- voltages up to +/-25V so never connect a serial cable directly to your uC. Always use a driver chip like the MAX232 which handles the level translation.
The following diagram (from wikipedia http://en.wikipedia.org/wiki/RS-232 ) shows the voltage levels for sending ASCII 'K' (0x4b).
The MAX232 inverts and shifts the level so that idle become high (+5V) and the start bit is low (0V).
As well as the send and receive lines, the RS232 protocol defines a number of hardware handshake and control lines. Although this tutorial does not use hardware handshaking, they cannot be completely ignored as we will see below.
For outgoing characters, the uC needs to output the required bits at the pre-configured baud rate. For 9600 baud this means the next bit is output 104uS after the start bit is output. With an 8Mhz uC clock this is 833 cycles.
The next figure (from Atmel's application note AVR304) shows the uC sampling points for an incoming character. Note the signal is inverted with idle now +5V. It shows the that the incoming bit stream is sampled at the middle of each bit. The timing of the sample is measured from the falling edge of the start bit. That is, skipping the start bit itself, the first sample is 156uS after the falling edge and then each sample is 104uS after that.
Quote from http://www.airborn.com.au/serial/rs232.html
“While the normal PC hardware might well run with just Tx, Rx and Ground connected, most driver software will wait forever for one of the handshaking lines to go to the correct level. Depending on the signal state it might sometimes work, other times it might not. The reliable solution is to loop back the handshake lines if they are not used. When the lines are handshake looped, the RTS output from the PC immediately activates the CTS input - so the PC effectively controls its own handshaking.”
These are the connections shown in the RS232 circuit diagram. That is the Request-To-Send (RTS) signal from the PC is looped back to the Clear-To-Send (CTS) input that indicates receiver is able to accept the data from the PC. Of course hard wiring these signals means that the PC will send data just as fast as it can. The Attiny84 needs to keep up or it will miss characters. (This will be discussed more later).
The other links on the 9pin Serial connector, pin 6 to pin 1 to pin 4, connect Data Set Ready to Data Carrier Detected to Data Terminal Ready. The PC drives Data Terminal Ready when it is ready for transmission and this in turn drives the Data Set Ready and Data Carrier Detected which the PC may look for to verify the other end it connected before sending data.
Some other Atmel devices have built-in UARTs (RS232) devices. The Attiny84 does not, so the sending and receiving has to be done by software.
RS232 uses a separate line for send and receive so it can be a full-duplex system (that is sending and receiving at the same time). However this implementation, for simplicity, is only a half-duplex system which is only either sending or receiving but not both at the same time.
Other alternative software implementations (for different Atmel devices) can be found in Atmel application notes AVR304 and AVR307 and at http://www.siwawi.arubi.uni-kl.de/avr_projects/#softuart in C code for the attiny84
As discussed above, the RS232 transmission needs to be sent and sampled at precise times. Once each 104uS or 833 uC clock cycles with an 8Mhz system clock. However from the data sheet of the Attiny84, the accuracy of the is only +/-10% (table 20-2) and varies by more then +/-2% with temperature. If the clock is slow by more then about 5.6% then after 1 start bit and 8 data bits it will completely miss sampling the last bit and sample the stop bit instead. Conversely if the clock is fast by more than 5.9%, the 7th data bit will be sampled when the uC should be sampling the 8th data bit.
To avoid this happening we are first going to adjust the uC clock frequency to 8Mhz +/-0.08Mhz. My chips initial clock frequency was 7.77Mhz which was within 3% of 8Mhz, so the serial transfer worked for me without calibration. If you don't have a multimeter that can measure 8Mhz, than the serial transfer might just work for you as well. Otherwise you need to follow the calibration procedure below.
Open AVRStudio and start a new project called OscCalibration (see the first tutorial Basic uC 3 Level Led Driver for details on doing this). Load the following code into the empty OscCalibartion.asm file. (This code can be downloaded from OscCalibration.asm)
.include "tn84def.inc" .def Temp = r16 .def Temp2 = r15 .cseg // INTERRUPT VECTORS .org 0x0000 rjmp RESET RESET: // set clock to 8Mhz i.e. remove div8 that is set by programmer ldi Temp, (1<<CLKPCE) out CLKPR, Temp // enable clock change for next 4 cycles clr Temp out CLKPR,Temp // set 8Mhz clock ldi Temp, 0xA5 // <<<<<<<<<<<<<<<< change this value out OSCCAL, Temp MAIN_LOOP: rjmp MAIN_LOOP
Before loading the program into the attiny84 first use AVR Dragon Fuses tab to turn on the CKOUT fuse. This directs the system clock frequency to PB2 (pin 5).
Connect your frequency meter between pin 5 and GND
Now goto the AVR Dragon Advance tab and read the clock calibration byte. Don't use the Write button, you only need to Read the byte.
Mine was 0x9E (158). Looking at Fig 21-42 in the Attiny24/44/84 datasheet you can see that this corresponds to more then 7Mhz and less then 8Mhz.
Change the following line in the code
ldi Temp, 0xA5 // <<<<<<<<<<< change this value
Replace the 0xA5 with the calibration value you have just read.
Compile and program the uC with this program. (Remember to change the Hex file to point to your compiled OscCalibration.hex file)
You can then read the actual system frequency corresponding to this calibration setting. If you don't get a frequency reading, check that you have written the fuses with the CKOUT set.
Change value in the program, up or down as necessary, to adjust the frequency to as close to 8Mhz as you can get. Use Fig 21-42 in the Attiny24/44/84 datasheet as a guide. NOTE: Each time the uC starts up it automatically loads the default calibration value, the one you initially read. You should not change from this initial value by more then 0x20 (32 decimal) in one step. If you need to shift the calibration byte value by more then 0x20, then do it in two steps each on less then 0x20. (see the downloaded code file for sample code)
Once you have found the value that gives you 8Mhz at 25degC, you will need to update the other programs in this tutorial with the value you have found.
Finally remember to un-program the CKOUT fuse, to restore the uC to normal operation.
As an alternative to calibrating the uC clock you could use an 8Mhz crystal or ceramic resonator. The ceramic resonator is has more than enough accuracy and starts up faster. The disadvantages of these is that they use up two pins on the chip and in this case the pins they need are already being used by the circuit, so you would need to re-arrange the circuit and change the code to refer to different pins. Two capacitors are also needed. 18pF to 22pF seems to be a good start but the Atmel application note, AVR042 AVR Hardware Design Considerations, goes into detail about the best choice of values for these capacitors.
In this part of the tutorial I will cover some programming tips and guidelines for programming interrupt driven programs. For a general guide to AVR assembler programming try http://www.avr-asm-tutorial.net/avr_en/beginner/index.html and of course read the data sheet for the particular device you are using.
The previous tutorial used a very simple and reliable programming approach. It did all the work in the interrupt routine. The main code merely initialised the uC and then went to sleep waiting for the ADC to finish reading the led current. When the ADC completed, it interrupted the main loop and all the current control and button press handling was done in the ADC interrupt routine.
This project will require a more difficult coding style. In this tutorial we need to handle multiple interrupts. These are the ADC conversion complete, the timer1 interrupt for button denounce and general timing, the timer0 interrupt for timing the sending and sampling of the RS232 signals and a pin change interrupt to detect the start of reception of the next RS232 byte.
This is the most important tip for programming. Start with the smallest program you can, say set an output low, and then get it to to compile and test it works. Then, most importantly, take a copy of this working program and re-name it version1. I actually prepend 1_ to the code file, e.g. 1_RS232.asm. I also put a comment at the top of the code saying what I changed in this version. Like
// 1 First working code, sets output low
Then I make a very small change to enhance the program and test it again, and so on. I am up to version 44 for the RS232.asm code.
One piece of software that I recommend you buy is BeyondCompare or some other diff program. This will let you easily compare your current code to a previous version so you can see what you have done to stop it working.
A lot of programmers also use version control software like Subversion. For these small projects with just one assemble file it is not necessary, but is very useful as your projects grow in size and complexity. I use the version numbering system while I am away from base and then put the versions into Subversion when I return.
By default all interrupts are disabled. To enable an interrupt you need to
enable the particular interrupt you want by writing to its control register and
enable the global interrupt flag (using SEI)
While the uC is executing the code in the interrupt routine it, usually, cannot handle any other interrupts. This is because the global interrupt flag is disabled automatically on entering the interrupt routine and turned on again by the RETI instruction at the end of the routine. Level triggered interrupts that come and go while you are handling some other interrupt are lost, other interrupts, such as edge triggered, are flagged and are processed when the code returns from the RETI instruction. So long interrupt routines mean you may miss some other signals altogether or deal with them after some delay. As we have seen above, the RS232 processing depends on sending the bits and sampling the inputs within a given time. This won't happen if you code spends too long in other interrupt tasks. In general in the interrupt handler just read the data and write it somewhere for the main program to process later.
The CLI instruction disables all interrupts and the SEI instruction re-enables the global interrupt flag. If you need to do something that cannot be interrupted, such as moving two bytes from one place to another, then surround the code with
CLI … SEI
to prevent some interrupt routine from changing the registers or global values while you are moving them. This code block should be kept as small as possible. As stated above, interrupt handlers also globally disable interrupts. Interrupts are disabled on entry to the interrupt handler automatically by the uC and re-enable them on exit (RETI). The reasons given in guideline 1 for keeping the code short also apply here.
Following guidelines 1 and 2 most of your code should be executed with interrupts enabled. This means that your main program can be interrupted between any two instructions. Because of this, you need to restore the previous values of all Registers and global variable, that do not transfer data, before returning from Interrupt.
If the interrupt changes any of the registers or storage (SRAM) you are working with, then the main program will appear to act strangely. For example, the main program can test some value is greater then 5 and then in the next instruction, after being interrupted, the value is suddenly less than 5. This is the most common problem with interrupt programming, and the hardest to debug. So take special care to guard against this using the programming approaches outlined below.
There are a number of basic ways to overcome this problem of an interrupt changing a register or SRAM value unexpectedly.
There are two basic issues here.
How to prevent interrupt routines from un-intentionally changing the values of registers and SRAM used by other parts of the program
How to main program can safely access registers and SRAM updated by an interrupt routine.
Possible solutions to 1) Preventing un-intentional changes to register and SRAM values, are:-
For SREG, this register MUST be saved and restored by all interrupt handlers.
For general purpose registers, either reserving specific registers for use by specific interrupt handlers, OR by saving and restoring registers used in the interrupt handler.
For I/O and control registers your need retain the values of the bits not being change by the interrupt.
For SRAM, reserving specific locations for use by specific interrupt handlers.
Possible solutions for 2) Safe access to registers and SRAM that is updated by interrupts, are:-
The uC disables interrupts when it processes the interrupt and calls the interrupt routine. D not enable it again until the main program has finished processing the data. When the interrupt routine returns the RETI will set the global interrupt flag to enable all the other interrupts that you have not disabled.
In the interrupt routine, save the data to an SRAM location and then in the main program copy the data to a different location for processing. If you need to copy more then one byte then surround the code with CLI and SEI to prevent any interrupt changing the data before you can save it.
Make sure your main program has enough time to process the data before the next interrupt can occur. An example of this is processing a denounced push button. The user cannot physically move the button in less then about 0.1 sec and the debounce code will not let the button program status change faster then 10mS (about 80,000 uC clock cycles, more then 40,000 instructions) which is more then enough time for the entire main programming loop to process the button press/release.
Do all the processing inside the interrupt routine itself. Only use this approach for small tasks that don't need to transfer any data to or from the main program. For example flashing a led.
Use 'guard' flags to control access to registers and SRAM locations. These flags let the main program know when it is safe to access and update values that are shared by interrupt handlers. Typically the flags indicate some or all interrupts have been disabled.
Let's look at some examples of applying each of these solutions.
This is needed for EVERY interrupt handler so I wrote two small macros.
// start interrupt routine .macro SAVE_SREG push Temp in Temp, SREG // save the SREG push Temp .ENDMACRO // end interrupt routine .macro RESTORE_SREG pop Temp out SREG, Temp // restore the SREG pop Temp .ENDMACRO
where Temp is defined as R15. The save macro is used at the very start of the interrupt handler. It first puts the current value of the Temp register on the stack then transfers the SREG to Temp and puts it on the stack as well. This leaves Temp free for use by the interrupt routine. At the end of the routine, just before the RETI statement, the restore macro gets the SREG value off the stack, into Temp, and restores it and then restores the value of Temp the existed before the interrupt routine was called.
My advice is to always save and restore every register you use in the interrupt routine, except of course if you are updating a register for access from the main program or another interrupt routine. You do this by pushing the registers at the start of interrupt routine and poping the in reverse order at the end of the interrupt routine. For example
.def RS232StatusReg = r17 // used in interrupt saved and restored .def RS232BitCount = r18 // used in interrupt saved and restored TIMER0_CMP_A_INT: SAVE_SREG // save Temp and SREG push RS232StatusReg push RS232BitCount ... pop RS232BitCount pop RS232StatusReg RESTORE_SREG // restore Temp and SREG and reti reti // return from interrupt
If on the other hand you decide to allocate specific registers for the sole use of an interrupt routine then you need to carefully document this in your code. I do not recommend this approach because as your code grows you will run out of registers and need to push and pop them as shown above.
I/O and control registers are shared by the whole program, so when an interrupt routine, or the main program, sets or clears a bit in one of them, you need to make sure the other bits are not changed. To do this you need to use one of two pairs of statements. Which pair you use depends on whether or not the i/o register is in the first 32, i.e. PRR to EEHRH (see the Register Summary in the datasheet).
If the i/o register is in the first 32 you can use SBI and CBI to set and clear individual bits in the register without changing the others. e.g;
sbi PORTA,SERIAL_OUT_A // Set transmit to 1
On the other hand if the register is outside the first 32 you need to load it into a temporary register first and then use SBR or CBR to set or clear the bits you want to change. Note: SBR and CBR are different from SBI and CBI. Where as SBI and CBI only need at bit number, 0 to 7, which you want to change, the SBR and CBR statements need a whole byte mask (0 to 254) to apply. e.g.
in Temp, GIFR sbr Temp,1<<PCIF1 out GIFR,Temp ; clear pending pin change 1 int. The 1<<PCIF1 translates to 1<<5 which shifts 1 five 5 places to the left, i.e. 0b00100000,
SBR then OR's this mask with the register to set the bit, while CBR AND's the mask's complement to clear the bit
To set or clear more than one bit at a time just OR (||) the masks, i.e.
cbr Temp, (1<<CS02)|(1<<CS01)|(1<<CS00)
Tip: Instead of trying to remember if the i/o register is in the first 32, I just try using SBI, or CBI, and then if the compiler complains I replace SBI, with SBR, 1<< and add the IN and OUT statements
SRAM is used for both stack and global volatile storage. Very early in the program you need to reserve some SRAM for use by globals by setting where the stack pointer starts (SP). e.g.
//***** Stack Pointer Setup Code ***** ldi Temp,high(RAMEND-(NO_GLOBAL_VARS+1)) ; Stack Pointer Setup out SPH,Temp ; Stack Pointer High Byte ldi Temp,low(RAMEND-(NO_GLOBAL_VARS+1)) ; Stack Pointer Setup out SPL,Temp ; Stack Pointer Low Byte There are two coding statements you need to
The RAMEND is defined in the include file for each AVR uC. The NO_GLOBAL_VARS is a variable I define at the top of the code. Note: the +1 to allow for global variable 0.
When I set the NO_GLOBAL_VARS, I also define a place holder for each one of them i.e.
... .equ GLOBAL_27 = RAMEND-27 .equ GLOBAL_28 = RAMEND-28 .equ GLOBAL_29 = RAMEND-29 .equ GLOBAL_30 = RAMEND-30
Then as I use them I replace the GLOBAL_.. with the name of the variable. If I run out, I increase NO_GLOBAL_VARS and add more place holders. Your code should clearly comment the use of each of these SRAM locations.
A slight variation of this approach is used in the LED_CONTROL processing in the BluetoothControlledLedDriver.asm (see below). In this case the interrupt routine does nothing (actually is not defined). The ADC is set for single conversions and stops when the conversion is complete. The main program looks for the conversion complete flag and processes the reading, before starting the next conversion. So there is no possibility of the ADC reading being changed while the main program is processing them. This is also an example of Polling. That is where instead of having an interrupt routine, the main program just loops check to see if something has happened. This approach is not suitable for time critical applications.
An example of this from RS232.asm (see below) is
lds Temp, RS232_STATUS sbrc Temp, RS232_STATUS_TX_COMPLETE
The Timer0 Compare A interrupt, and others, update the RS232_STATUS SRAM location. The main program reads this location it to a register and then tests for various statuses. Since only one statement is need to read the byte, you don't need to surround the statements with CLI, SEI.
On the other hand if we want to set/clear a bit in an SRAM location that is updated by an interrupt, you need to make sure the interrupt routine does not try the value while the main program is trying to change the value. So we surround the main program's update statement with a CLI and SEI statements to stop any interrupt handler from interrupting this group of statements.
// clear RS232_STATUS_DATA_RECEIVED // disable of interrupts to prevent some other interrupt change the RS232_STATUS // after we load it and before we save it. If so then that change would be lost. cli lds Temp, RS232_STATUS cbr Temp, (1<<RS232_STATUS_DATA_RECEIVED) sts RS232_STATUS, Temp sei
In the BluetoothControlledLedDriver.asm (see below), a timer interrupt checks the button position and updates the SwitchChanged flag. The main loop checks this flag each time round the loop and if it is set it clears the flag and then checks the button position. The code only works if the SwitchChanged flag and button position cannot change in the time it takes the program to do an entire loop. The SwitchChanged and SW_SWDown flags can only be updated once per 10mS. This is more then enough time for for the uC to execute and entire loop of the main program. So if the flags change just after this code is executed, they will still be valid next time the program loop comes around.
sbrs TRIGGER_Flags, TRIGGER_SwitchChanged rjmp Finished_SwitchChanged_TRIGGER_PROCESSING // else changed clear trigger now cbr Trigger_Flags, (1<<TRIGGER_SwitchChanged) // else switch changed state, check switch state // clear the trigger sbrc SW_Flags, SW_SWDown // skip rcall if switch is up rcall PROCESS_SWDown_TRIGGER // set level accordingly
The handling of the RS232_SEND_WORD in the Timer0 Compare A interrupt handler is an example of this (see RS232.asm below). This SRAM location is not used at all by the main program. All modifications are done in the interrupt handlers. So there is no possibility that its value can be changed by the main program.
This is a very common means to controlling access to shared registers and SRAM locations. In BluetoothControlledLedDriver.asm (see below), the RS232_STATUS location is used to communicate the current status of the RS232. For example when the RS232_STATUS_BUSY is set, the link is sending or receiving and it is not safe for the main program access or update the send/receive data. So the main program can load this status byte, using method 2) b), and then check the value of RS232_STATUS_BUSY and RS232_STATUS_SEND to see if it is safe to update the data to be sent. Note: that this code depends on RS232_STATUS_SEND remaining false at the end of sending until the main program starts another send, using 2) a) above.
lds Temp, RS232_STATUS sbrs Temp, RS232_STATUS_BUSY rjmp SEND_ADC_READING_TRANSMIT // not busy so can send // if busy see if sending or receiving sbrc Temp, RS232_STATUS_SEND rjmp END_SEND_ADC_READING // busy sending so keep trigger and try later // else not busy sending so send this reading // Note RS232 may be busy receiving. // If so this transmit will just terminate the receive // if it has not finished by the time RS232_TRANSMIT is called below SEND_ADC_READING_TRANSMIT:
Since we have not put these tests in a CLI, SEI block, an RS232 receive could start after we tested the RS232_STATUS_BUSY flag, or as noted in the code there could be a receive already in progress. In either case, as documented in the code, the transmit will take precedence and terminate any receive that is in progress.
Updating the register atomically means doing it in such a way that an interrupt can not disrupt the update. In practice this usually means doing the update in a single statement, such as
cbr Trigger_Flags, (1<<TRIGGER_Timer1)
which clears a bit in the Trigger_Flags register in one instruction. Interrupts never interrupt a single instruction and this instruction does not change any of the other bits in the register. Some Atmel uCs also provide atomic access or update to a pair of registers which guarentees that once you read/write the first register the second will not be changed before you read/write it. For example, in the Attiny84, when you read the low byte of the ADC result register it blocks the update of the result until the high byte has been read.
There are a number of ways you can code an RS232 interface on Atmel processors. For the larger processors, a hardware UART is provided. For the smaller processors, such as the Attiny84 used here, there is no hardware UART. The Attiny84 does have a USI hardware function for SPI communication. This can be also used as part of an RS232 interface (see Atmel application note “AVR307: Half Duplex UART Using the USI Module” for a C code version) However this restricts the pins you can use to the DI and DO pins. Atmel application note AVR305 provides a simple RS232 implementation, which can use any pins. Unfortunately the timer is implemented as a software loop so there are no cycles left for the processor to do anything else while it is sending or receiving. AVR274 provides a single wire timer based solution, where the send and receive bytes both use the same pin. This is not suitable for our application. One suitable alternative to the code presented here is a software based RS232 assembler code module from http://www.siwawi.arubi.uni-kl.de/avr_projects see the Software-UART (softuart_gittins) project on that page.
The code used here is a software base implementation using one of the hardware timers and providing half duplex RS232 on user selectable pins. The source code is in RS232.asm. (Mod2, updated 23rd February 2010) As provided, it is configured for 9600 baud 1 start, 8 data and 1 stop bit half duplex transmission using PA7 for receiving and PB2 for transmission. To use other pins change the defines at the top of the code. To ensure the baud rate is accurate you should set OSC_CAL_STEP_1 and OSC_CAL_STEP_2 to clock calibration setting you determined from running OscCalibration above.
code is designed to provide RS232 capabilities to your other
programs. There are three routines you need to call:-
RS232_INIT which initialises the pins and timer0 and the interrupts,
RS232_TRANSMIT which transmits the bytes previously saved in the RS232_BUFFER_1 to n, where the number of bytes to send has been stored in RS232_SEND_COUNT
RS232_SET_TO_RECEIVE which sets the software to detect a start bit and start receiving a byte.
To see how to use this module look at the test program that is included, starting at RESET. This test program waits for a character to arrive and then echos back the character in hex, prefixed with “Received “ and terminated by <cr><lf>
After setting the system clock, allocating space for the global SRAM variables and calibrating the system clock, the program loads the RS232_BUFFER locations with the fixed part of the return message. Then it calls RS232_INIT to initialize the RS232 module and then globally enables interrupts.
Then the program calls RS232_SET_TO_RECEIVE to set up for the receiving the first char and go into an infinite loop calling PROCESS_RS232.
In each call to PROCESS_RS232, it check the state of the RS232 module. This is called polling. If the RS232 module has just finished transmitting, i.e. RS232_STATUS_TX_COMPLETE is set, then we call RS232_SET_TO_RECEIVE to wait for the next character to arrive. Calling RS232_SET_TO_RECEIVE clears the RS232_STATUS_TX_COMPLETE for us.
If RS232_STATUS_TX_COMPLETE is not set then we test if RS232_STATUS_DATA_RECEIVED is set, if not we return to the main program which loops and calls PROCESS_RS232 again.
If RS232_STATUS_DATA_RECEIVED is set then we load the byte received and clear the flag to indicate we have copied the byte out of the buffer. Then we convert the byte to HEX and save the two numbers in the correct RS232_BUFFER locations, then set the RS232_SEND_COUNT and call RS232_TRANSMIT to send the all the bytes.
Of course in the background, while the statements in PROCESS_RS232 are being executed, the RS232 module's interrupts are interrupting the execution as necessary to send and receive the bytes.
The software RS232 Module presented here, while
suitable for its intended use, does have some limitations. These
i) It cannot send and receive at the same time. Not a big limitation and the software to overcome this would be messy.
ii) When you are transmitting you can miss incoming characters, or when you stop transmitting and call RS232_SET_TO_RECEIVE, you might start to receive a byte half way though and get the wrong character. You can use the additional drivers on the MAX232 chip and extra pins on the Attiny84 to implement hardware handshaking to stop the PC from sending characters until you are ready to receive them. Another alternative is to be tolerant of bad characters or check them with a CRC check sum or by echo them back to the sender for verification.
iii) Noise on the receive line can trigger the pin level change interrupt and cause the uC to think there has been a start bit and start sampling the input and return garbage for the received character. This could be a problem in noisy environments, but I have not had any problems like this with this circuit.
None of these limitations are serious problems for controlling the Led Driver using RS232 / Bluetooth
Mod1, updated 14th July 2009. Added cli to RS232_SET_TO_RECEIVE and RS232_TRANSMIT, to allow for this module being used in a program which has other interrupts happening.
Start a new project, RS232 (see the first tutorial Basic uC 3 Level Led Driver for details on doing this). Download the code from RS232.asm and load it into the empty RS232.asm file the project created. Compile the code. Because timing is critical to the correct transition and reception of bytes, you cannot debug this code using the Dragon in-circuit debugger. Instead this code is more easily debugged using the ARV Studio simulator. The debug example below shows how to trace through the code and check that the receive sample times are correct.
In AVR Studio select the menu items Debug, Select Platform and Device. In the dialog that appears, select the AVR Simulator for the Platform and select the Attiny84 for the device.
Then, after compiling, set a breakpoint at
line in the main program. Then start debugging using the Debug, Start Debugging menu item and then once debugging has started use Debug, Run to run to the break point. Now we are going to toggle the RS232 receive pin (PB2) to simulate a start bit arriving. This will trigger the pin level change interrupt. First take note of the current clock counter, 434, and stop watch 108.5uC, in the debugger.
Note that the Simulator uC frequency is set to 4Mhz, where as the circuit will actually be running at 8Mhz so all the times shown in the simulator need to be halved.
Now toggle the PB2 input pin in the PINB I/O view. As you move the mouse over the pins, AVR Studio will display their names.
Clicking on the square toggles the pin and changes the hex value (shown in red). Before continuing the run, set a breakpoint at the first line of the PIN_CHANGE_INT:, that is on the SAVE_SREG line. Then selecting Debug, Run again the simulator will stop at this point a few cycles later. So the pin change interrupt is working. Now set a breakpoint in the TIMER0_CMP_A_INT at the line that says
sbis SERIAL_IN_PORTIN,SERIAL_IN_PIN // <=== SAMPLE HERE
This is where the receive bit is sampled for each bit.
Choose Debug, Run again. The program will stop on your first breakpoint at rcall PROCESS_RS232. Remove this breakpoint and run again. When it stops at the breakpoint inside TIMER0_CMP_A_INT at the sample point, the cycle counter reads 1683 and the stop watch reads 420.75uS
i.e. there were 1683-434 =1249 clock cycles between changing the input receive pin and the first sample. At 8Mhz 1249 cycles is 156.125uS. One and half bits periods of 9600 baud is 1.5 x 1/9600 = 156.25uS or 1250 cycles. So we are very close to sampling in the middle of the first bit after the start bit. The difference in the stop watch is 312.25uS with a 4Mhz clock. With our two times faster 8Mhz clock, the time is half or 156.125uS. You can continue to run the software debug simulator and see when the next bits will be sampled and then to confirm when the send bits will be switched.
Now that you have checked that the software will sample the receive bits at the correct time and send the bits at the correct time you can load the RS232.asm code into the Attiny84. Open the AVR Dragon programming dialog and in the Main tab set the path for both the RS232.hex and RS232.eep files. This code has eprom data which contains the constant part of the return string, so you need to program both the HEX and EEPROM files.
Remember the MAX232 chip is still removed. You tested it previously. Now you will test the uC is receiving and sending its signals on the correct pins on the MAX232 socket.
To program the uC, Erase the Device and program both the Flash and EEPROM.
To test the uC receive and send, connect you Frequency meter (or volt meter if you don't have a frequency meter) between pin 10 of the MAX232 socket and ground. The touch pin 9 of the MAX232 socket with a grounded wire. This will trigger a pin change interrupt in the uC and in turn send a series of bytes back to pin 10 of the MAX232. Your multimeter should show some fluctuation as the bits toggle on and off.
Now that you have some indication that the uC software is actually working, you can turn off the power supply and fit the MAX232 chip and connect the serial cable to your computer. Start TeraTerm ,or whatever terminal software you are using, and configure it to talk to your serial port at 9600 baud, 1 start, 8 data, 1 stop, no parity as you did above in “RS232 connection to the PC Terminal Software”. Local echo is off by default in TeraTerm, so to see the characters you type, turn it on by selecting the menu items Setup, Terminal and checking Local Echo
Now press a key, say 1 and you should see
try some more keys, each time your uC should respond with hex value of the key you pressed.
As discussed above under “Limitations of this RS232 Module”, if you manage to send characters faster then your uC can handle them, you will get back corrupted hex values, indicating the uC did not receive the byte correctly.
Contact Forward Computing and Control by
©Copyright 1996-2017 Forward Computing and Control Pty. Ltd. ACN 003 669 994