A Special "Thanks" to Myke Predko for submitting the following project/article.
About the Author
Myke Predko is the author of "Programming and Customizing the PIC Microcontroller", the "Handbook of Microcontrollers" and "Programming and Customizing the 8051 Microcontroller" as well as the soon to be released "PC PhD" and "PC Interfacing Pocketbook" which are all published by McGraw-Hill.
As well as writing books on electronics and programming, Myke works for Celestica, Inc. in the area of New Products Test Engineering. His wife, Patience, and he have three children, Joel, Elliot and Marya.
When I designed the "YAP" for the "YAP" PICMicro programmer presented in "Programming and Customizing the PIC Microcontroller", I felt that the serial interface provided in "execution mode" of the programmer would be very useful for people developing their own software. With this interface, the user could input new values into the PICMicro application or output temporary values to help with debugging. This feature was probably not considered that useful until I developed the "YAP Windows" interface software with a Terminal Emulator built into it for just this purpose. This Terminal Emulator interface is shown in the screen shot above.
In this article, I want to focus on how asynchronous Non Return to Zero ("NRZ") serial interface software is written for the low-end and mid-range PICMicros which do not have built in UARTs. There are many sources for information on creating the electrical side for this interface (and the YAP provides this as a matter of course), so I really haven't persued it in this article.
If you are looking for more information on the electrical side of things, check out the Maxim "MAX232" chip at http://www.maxim-ic.com (seach under "Interface Products"). The MAX232 is commonly used for this purpose because it only uses a single +5 Volt power supply, is widely available and has been second source (which means it is very cheap). When I design RS-232 interfaces, I normally create a "Three-wire" (TX, RX and Ground) interface and short CTS-RTS and DSR-DTR together to avoid having to worry about hardware "handshaking" in my applications.
Because the genesis for this topic was the "YAP", I used a YAP as the only hardware for the application. As can be seen in the picture below, Pin 5 of the YAP's debug connector (Serial Input) is connected to Pin 10 (the PICMicro's RA3) and Pin 6 (Serial Output) is connected to Pin 11 (the PICMicro's RA4 Pin).
The pins that were chosen are arbitrary, with RA4 being selected as an input simply because most applications do not require the "open-drain" capabilities of the pin. The serial signals used in the application are at TTL/CMOS levels with the YAP providing the electrical level conversion, eliminating the need for level conversion circuits to wire the PIC16F84 to the YAP's serial I/O.
The macros presented in this article are designed to use any of the PICMicro's I/O pins. If RA4 is used for serial output, make sure a 1K to 10K pullup is put on the line to allow it to be used to output digital signals.
Before going through the code for the article, I just want to review what I mean when I say "asynchronous Non-Return to Zero" serial communications. This method of communication has been around for many years; first used as part of the "Baudot" standard for teletypes. In modern communications, each byte is transferred in "8-N-1" format which means that eight data bits are sent with a single ("low") "Start Bit" and High "Stop Bit" to make up a data "packet". This is shown in the diagram below:
"8-N-1" is a very simple and convenient protocol. Early systems used different parameters, varying the number of data bits, whether or not "Parity" was to be included with the data and additional "Stop Bits", to allow the receiver to finish processing the data in the packet before the next packet had to be received.
The term "Non-Return to Zero" indicates that the packet always ends with a "1" being sent as a "Stop Bit" to allow the "0" "Start Bit" to be easily recognized.
Each bit is transmitted for a set period of time, the reciprocal of which is known as the "data rate". The normal units of the data rate is "Bits Per Second" ("bps"). This may also be referred to as "Baud". Some people get very uptight over the use of these terms, but for all intents and purposes they are interchangeable. Yes, I know they are different, but I don't see any reason to get into arguments or make somebody learning electronics and programming feel bad by berating them on it.
Typical electrical standards are "RS-232" and "RS-485". "RS-232" is a +12 Volt to -12 Volt point to point connection scheme and "RS-485" is a +5 Volt multi-drop network connection scheme. An excellent source for information on these standard's is Jan Axelson's "Serial Port Complete". As I indicated above, I only try to work with "Transmit" and "Receive" and avoid hardware handshaking unless it is absolutely necessary.
Common data rates are 300, 1200 (which is what the "YAP" uses), 2400, 9600 and 19200 bps. These speeds are somewhat unusual and were based on early teletypes. The speeds are based on the maximum mechanical speeds of the teletype printers. As equipment got faster, the data rates became multiples of these speeds which is why data is sent at strange data rates listed above.
In many PICMicros, a built in serial interface known as a "USART" for "Universal Synchronous/Asynchronous Receiver/Transmitter" is available. This hardware allows data to be received and sent without any software involvement (other than to transfer bytes between the USART and software). To send data, the USART simply shifts the data out (low byte first) to the Transmit I/O pin.
To receive data, the USART works by polling the serial data input line at sixteen times the data rate. When a "low" is detected, the hardware waits eight polling cycles to check to see if the line is still low half a bit period later. If it is, then a bit is read every sixteen polling clocks until all the whole byte has been read in. At the end of the byte, the "Stop" bit is checked to be high. If the byte is received without any problems, a "Byte Received" flag is set or the application software is interrupted.
The software and application I am going to present in this article will provide similar serial interface functions (although not carrying them out in identically the same way) while using the I/O pins and, optionally, the timer and interrupt controller built into the PICMicro. This type of interfacing is known as "Bit Banging" and, as I show in this article, can provide some useful enhancements to PICMicros which do not have the USART hardware capability.
To test out these interfaces, I created the serial.asm which sends an "A" out at 1200 bps to the YAP and then waits for a character to be received. if there is a problem or if the character is not received within two seconds, then a "-" character is sent back to the YAP. Otherwise, the character that is received, if it is from "a" to "z" will be converted to upper case and, sent back. At a high level (in "C" pseudo-code) this looks like:
// Load "Include" Files
The requirements for the two methods of serial communication is to place the "NRZSerialInit" invocation at address 0 of the application and the "NRZSerialRoutines" invocation where you want the routines (usually at the end of the application). Once they are in place, then simple calls to "NRZSend" and "NRZReceive" are all that is required to use them in the application. In "serial.asm", either set of macros can be used (they are selected by commenting one include file out of the source).
For the serial routines to be useful for me, I defined them as having the following basic parameters:
Speed - Processor Execution Speed (in Hertz)
These parameters are used within the Initialization and Send/Receive routines to transfer the data with what the user requires. Note that I have included a "Polarity" parameter; this allows you to specify whether or not the data is to be transferred with "1" as a "low" (known as a "space" in serial communications) and a "0" as a "high" (known as a "mark" in serial communications). This can be an advantage in some types of RS-232 level converters.
The actual number of instruction cycles between sending individual bits or polling the incoming data is mathematically calculated within the "NRZSerialInit" macro. This macro not only will initialize the I/O ports, but set up any other features (like the TMR0 timer and interrupt controller) needed for the serial data transfer.
A really nice feature of these macros is that when the bit data rate delay is calculated, the actual delay value is checked against the required value and if the error rate is greater than two percent, an "Error" is generated, stopping the assembly operation. The two macros have been checked for data rates from 110 bps to 19200 bps for PICMicros running anywhere from 32.768 kHZ to 20 MHz and this feature will either allow very accurate timings or prevent the application from being successfully assembled (and erroneously burned into a PICMicro).
The capabilities of these macros has really made me sit back and think about what is a macro's purpose in programming. As many people have heard, one of my first experiences with macros was very bad (a whole real time operating system for the PC written entirely with macros that I had to support), but when I look at how much easier the two sets of macros that I present in this article make writing applications with serial interfaces I feel like there is a real place for them in this type of application writing.
For this reason, I will not be spending a lot of time going through the actual mechanics of the macros in this article; instead, I will be concentrating on what the code in them does. In the next article, I will discuss this macro development and go through how they work and how to build checking and calculating features into them.
The first method of carrying out the serial functions is to send the data serially and poll the input line continuously before emulating the USART in software to receive data. The two macros (with NRZSend and NRZReceive) are contained in the nointser.inc. This file is included in the source file and the two macros ("NRZSerialInit" and "NRZSerialRoutines") are specified at the start of the application and the end, respectively.
The receive operation is shown in the diagram below:
To receive data, the input line is continuously polled. When the leading edge of the Start Bit is detected, a half bit delay is made and the input line is polled again. If the line is still low, then it is assumed to be a start bit and the data is shifted in, once per bit period. At the end of the packet, the Stop Bit is checked and the routine returns to its caller with the received byte.
There are three problems with this method of carrying out the serial transmit and receive. The first is, only one operation (Send, Receive or Data Process) can be done at any time. If data is coming in when the transmitter is active, then that byte will be lost. Similarly, if the PICMicro is processing data and a byte is coming in, then it can be lost as well.
The second problem is that while "NRZReceive" is active, nothing else can be going on. For this reason, I added a "TimeOut" parameter which will cause NRZReceive to return to allow the PICMicro to continue executing. If the "TimeOut" parameter is used, make sure that the processing that is done in between NRZReceive invocations is very short because data could be lost.
The last problem is, interrupts cannot be enabled during "NRZSend" or "NRZReceive" because if an interrupt is acknowledged while a packet is being transferred, the PICMicro timing will be lost, which will result in an invalid packet being transmitted from or received by the PICMicro.
Despite these problems, this is the only way in which the low-end PICMicros can communicate serially with another system. Ideally, this macro should only be used for communicating with humans, as there will be a fairly long space between characters, which allows time for processing the incoming data in between packets.
The second way of transferring data is exceedingly clever (I wish I could take credit for developing it) and avoids many of the probelms of the first method. For this method (which is found in intser.inc), the timer is set to interrupt the PICMicro at three times the bit rate. For example, for 1200 bps data transfers, the PICMicro is interrupted 3600 times per second.
Data bits are transmitted once every three interrupts. There is one important point to make on Data Transmit and that is to note that it is very carefully timed to make sure that bits are always sent during the same cycle within the interrupt handler and the Transmit portion of the interrupt handler always ends at the same cycle in the interrupt handler. This is important to make sure there is no "jitter" in the data transmitted and that the receive polling takes place at the same cycle in the interrupt handler as well.
The Data Receive is a bit more complicated and is shown in the diagram below:
As I indicated above, the incoming data line is polled at a rate of three times the bit speeed. After the Start Bit is detected, the line is polled again at the next interrupt handler invocation to make sure that it is a valid start bit and not a "glitch" on the line. At this point, the bit is being polled somewhere between 33% to 66% after the start of the bit, which is quite close to the middle of the bit. Each bit in the rest of the packet is polled every three interrupt invocations.
This method of serially transferring data does not have any of the deficiencies of the previous method and actually has quite a few advantages. These advantages include:
1. The actual cycles spent in the interrupt handler is very miniscule compared to the first method. When communicating at 1200 bps at 4 MHz, less than 9% of the cycles are lost to the serial handling.
2. The status of data coming in or going out can be checked by polling the "#XCount" flags (where # is "T" for sending and "R" for receiving). To see if a packet has been sent, the "TXCount" register is checked to see if it is equal to zero. To check to see if a packet has been received, the "RXCount" bit 1 is checked and after the byte is read, it is cleared. (Failing to clear "RXCount" after reading the byte in will result in the next received byte causing an "overrun" error.)
3. I have inserted hooks into the interrupt handler to allow buffered data to be sent (after each character is sent, a subroutine can be called to send the next character in a sequence) or to save received data in a buffer.
Other interrupt handlers should not be used with the "serint.inc" serial communications, because of the chance for data timing to be screwed up. To allow access to other interrupt requests, I have provided the "OtherInts" address parameter which will call the referenced subroutine (if the address of the subroutine is not zero).
When calling other routines, it is important to make sure that the total time in the interrupt handler never exceeds the time out interval. The easiest way to make sure this isn't a problem is to simulate your code and make sure that the timer interrupt doesn't end without the timer rolling over (to zero). Ideally, the interrupt handler should end under all circumstances with the counter at 0x0FC or less. This will give you eight or more instruction cycles for the mainline application.
This means that your mainline should not be very processor intensive, although very complex applications are still possible. My "YAP" PICMicro programmer is an example of what is possible.
I'm very pleased with the design of the serint handler in terms of overhead (in terms of instructions, cycles and memory). The data communication routines only require two file registers (the "RXCount" and "TXCount" variables) to support the data routines. As well, I have been using this macro and its routines for my own useage and I have been able to reduce the number of cycles required by the Transmit portion of the interrupt handler by four cycles. If you are looking to save the incoming data into a circular buffer, drop me a line and I can give you a routine which actually uses two less cycles than the original. The "intser.inc" file available for download incorporates the Transmit improvements
Looking at the two serial routines presented here, you're probably thinking there are other ways of implementing the serial I/O. For example, the serial receive routine could be enhanced by connecting the incoming serial data to the "RB0/Int" pin and respond to the request as an interrupt. This is very possible (such as putting in the no interrupt serial "NRZReceive" directly into the interrupt handler), but it is important to make sure that no other interrupts can cause either the Start Bit to be missed or their own interrupt requests to be ignored while the serial receive interrupt is active.
When you look at "serial.asm", you'll probably notice another include file vars.inc. This include file contains the macros "VarStart" and "VarAdd" which are used for allocating file registers for applications. These macros really replace the "CBLOCK" directive in MPASM and I like their functions better because they can be invoked from within a macro (as I do in "NRZSerialInit" for both different methods of serial interfacing).
These include files and asynchronous serial handler macros are very easy to use and take care of all the critical calculations for the application developer. As I have indicated above, I will be concentrating on complex macros such as these in my next article. I have tested the macros under a variety of conditions and they do seem to work well, or flag an assembly error if they cannot handle the serial streams with a low bit error rate.
If you try out these routines, please let me know what you think and if there is anything that I can do to improve them or if you find any problems with how they execute.
Click HERE to download all of the code for this article in ZIP format.
El Cheapo Updates and other News
I have received quite a few notes from people who have been building the "El Cheapo" programmer and for the most part, the programmer has worked well for them. For a few people, they have not been so lucky. To try and help these people, I have done two things.
The first is, at the end of Article 4 I have added the "El Debug" program that should help you debug your application. This little MS-DOS application is used with a voltmeter after the circuit is built to test out the connections. The application is designed to be used with only a voltmeter with the PICMicro pulled out of the test socket.
Along with "El Debug", I have also included a number of hints to what are the most likely problems people have had with the "El Cheapo". Before building your programmer, make sure that you consult with this list to help you avoid any potential problems.
The second change is that I modified the circuit to avoid using the eight data bits of the Parallel Port. Instead, I output RB7 data on an unused digital output and read back the bit using a digital input. This avoids the problems some people had with older PCs and non-EPP parallel ports. For the other parallel ports out there, I think many of their designers have changed the software to work around this problem. By changing the hardware, I'm hoping that I have come up with a PICMicro programmer that will work for all PCs, from 8088 based to the latest Pentium IIIs.
If you have any problems with your El Cheapo, or have any questions about it, please drop me a line.
For the other news, two exciting things have happened with "Programming and Customizing the PIC Microcontroller". In the next Rentron article I'll be announcing a much delayed and anticipated improvement to the book as well as a contest where your non-electronic/non-programming skills will be called to the forefront. All this and macros in a week or so.
Copyright and Warranty Statement
This article is presented on an "AS IS" basis. I have tested the circuit and code that I have presented here and I am confident that it works on the hardware that I have used. Different hardware may result in different results.
The information contained here cannot be reproduced without the author's permission.
Program the PIC in simple BASIC using the PicBasic Compiler. Visit: http://www.rentron.com/PicBasic2.htm