MarkF Senior Heliman Location: Palo Alto, CA
| I really am working on it...Hi, Gang!
I really am working hard to get this code published as soon as possible. As I’d mentioned earlier, documenting it properly can take a lot of time. To help get folks started with understanding this project, I’m giving a sneak preview by reproducing a copy of (by far) the largest single comment block in the project: the module header for rx.c:
“This module contains the high-level routines for the "Omega 16" PCM-FSK R/C receiver. It is responsible for processing received command frames, parsing the received commands, and driving the servos to the commanded positions.
The driving philosophy behind the design of this project is _speed_. It was created to deliver the fastest possible frame-based receiver design, and it is indeed quite fast: as currently configured, it _completes_ the first servo outputs less than 32 microseconds (millionths of a second) after the last input sample of an incoming frame is received. In addition, this receiver surpasses current receivers in another way: it drives all 16 of its servos simultaneously, so that all 16 servo outputs will be completed within one millisecond (1/1000th of a second) of the end of a frame (this millisecond is unavoidable with the current generation of servos, due to the fact that the range of command pulses varies by about a millisecond).
While future versions of this project will explore even faster alternatives, such as command word-based processing rather than frame-based processing, as well as alternative methods of communicating with the servos, this is a useful starting point.
Beyond speed, a second major driver behind this project is improved RF performance. Unlike current PCM-FSK R/C systems, which sample the RF input once to receive each bit that is transmitted, this receiver samples the RF input five times for every bit, enabling it to reject much of the noise that causes other systems to lose information.
The need to drive all 16 servo outputs in parallel leads to a very demanding timing environment - the receiver must be able to generate 16 pulses simultaneously that can vary in width by just one microsecond, which is easy to implement in hardware, but quite challenging to deliver in software. To handle this extreme level of performance, this receiver contains its own "compiler" that creates a new machine language subroutine every time a new frame is received - this custom routine drives each of the servo outputs with "perfect" timing precision. Furthermore, the compiler infrastructure is capable of much higher levels of performance, and it is also extensible for next-generation systems.
-------------
At the core of this receiver is the concept of "events", which are specific Inputs or Outputs that must occur at a precise point in time. Input events are used for sampling (reading) the RF input received from the transmitter, and Output events are used to drive the servo pulses that communicate position information to the servos.
Due to the use of 5X "oversampling" to improve RF performance, an Input event must occur every 31.250 microseconds, without exception. This means that each section of the code must complete its processing in less than about 31 microseconds (uS), or data will be lost. This is particularly challenging during the millisecond long period while the servo pulses are being driven.
To help solve this problem, the receiver introduces the concept of "event lists". Rather than attempting to perform single Input or Output events, event lists are constructed that contain both Input and Output events (and in fact, when two events happen to be coincident in time, the receiver also supports the concept of combined Input/Output events). To begin the process of understanding how this receiver works, let's first concentrate on the millisecond long time period during which the 16 servo output pulses are being turned off (all of the servo outputs are turned on while the frame is still being received, but that will be described later on).
During this millisecond time period, the receiver must not only end 16 different output pulses at different times, it must also sample the RF input 34 times. While the receiver initially doesn't know when each output must end, it knows with certainty the times it must sample the RF input – this is the case because all timing in the receiver is driven by the timing of each RF sample event. Since the receiver knows when it needs to sample the RF input, it starts with an event list that contains all of the Input sampling events that will need to occur during that critical millisecond.
As a frame is being received, the receiver is "looking forward" to that millisecond time period, planning how to handle the events that must occur during that interval. As servo commands are received, the receiver translates each command word into an appropriate pulsewidth for that channel, and it creates a new Output event for each channel that contains an Output value to be written, and a point in time when that output should occur. These Output events are recorded in the event list, so by the time that the last command has been received in a frame, the event list contains all of the Inputs and Outputs that must occur during that critical millisecond time period (this list is stored in time-sorted order, using an efficient binary search with an “insertion sort” – see add_event() for more information on these techniques).
Following the command words in each received frame are two Cyclic Redundancy Code (CRC) words. These words contain a special mathematical code that is very effective at determining if the data bits that comprise a frame contain any errors. While the CRC codes are being sent, before the receiver even knows if the commands it has received are valid, it speculatively compiles the event list that it has constructed into a new executable machine language subroutine. This "event routine" contains the processor instructions that are capable of executing all of the events that are contained in the event list with "perfect" timing precision.
Something else occurs while the last word of the CRC is being received. The primary rf_input() routine has been counting the number of samples it has taken since the start of this frame. When it determines that the time is one millisecond before the end of the frame, it will automatically drive all16 of the servo output pulses high simultaneously. During this millisecond, the receiver simply continues receiving the CRC - this millisecond is a necessary overhead when driving current-generation servos. By overlapping the first millisecond of the servo pulses with receipt of the incoming frame, however, it is eliminated from the receiver's response time to commands.
After the last sample of the last CRC word has been received, things start to get exciting. First, the receiver must rapidly determine whether the CRC code words indicate that the frame is valid. If so, the main() routine will execute the new event routine, called drive_servos(), to end each of the servo pulses at the appropriate point in time. If, however, the CRC indicates that the frame has been corrupted, then the event routine that's been created is useless, for it almost certainly contains invalid servo commands. When this happens, the receiver must fall back to repeating the last known valid servo position. It does this by calling a routine called drive_last_servos(), which is in fact a previously compiled drive_servos() routine!
Errors are a fact of life in RF communications: it isn't a question of whether or not errors will occur, it's a question of how often they will occur. As a result, the code in the receiver is structured to deal with "bad" frames as efficiently as possible. To do so, it maintains a pair of RX_STATE structures. One of these structures, pointed to by the 'rx->' variable, holds the current receiver state. This state information includes the servo commands as they are received, a pointer to an event list as it is built, and once the frame is received, a pointer to the compiled event routine. Each time a frame is successfully received, main() simply calls the swap_buffers() routine to swap the 'rx->' pointer with a pointer called ‘last_rx->’, thus saving all of the information necessary to reproduce the last good servo position. As you may have guessed, the event routine associated with the current RX_STATE is called drive_servos(), and the event routine associated with the last good RX_STATE is called drive_last_servos().
Whichever servo driver is called, just before returning, the event routine will store the RF samples that it has read into the 64-bit global "rf_sample" buffer (the number of valid samples in rf_sample is called "rf_samples"). As main() loops around to begin processing the next frame, these samples that were taken while driving the servos actually hold the start of the next frame, and so the whole cycle continues...
At this stage, you have a good general overview of how the receiver works, so the best next step is to dive directly into the different sections of the code and read the routine headers. The code in the receiver is rather heavily commented to make it more approachable, and while the code itself is reasonably modular, the comments intentionally are not: most of the routine headers attempt to explain the broader context which the routine operates in. To get started, scroll down to the bottom of this file, and start with the main() routine, for that's where things start happening.”
Hopefully, the rest will come along quickly!
Have Fun!
MarkF |