Bridging the Telemetry from and APM Quadcopter Controller to a Spektrum Telemetry Module
I came across this post on the RCGroups forum describing how to interface into the Spektrum Telemetry Module and send your own telemetry to a Spektrum radio. I was building my own Quadcopter centered around the APM flight controller, which is based on an Arduino Mega 2560. One of the options on this controller is to put a radio transmitter on the Serial 1 port, and a radio receiver on a PC, and the controller can continuously transmit telemetry information like GPS coordinates, Battery Current and Voltage, Altitude, etc., to software running on the PC. Since the Spektrum Telemetry is and I2C master, and the APM is sending the telemetry out a serial USART, I thought it would be a fairly straight forward task to use an ATMega 328p to read the telemetry stream from the APM, reformat it and send it on to the Spektrum Telemetry module.
While I will also have a fondness for the Arduino, because it got me started work with microcontrollers, I’ve moved away from using the Arduino in my projects because even a small Arduino is big (compared to the atmega chip itself), expensive and the software is somewhat limited. In this case, the AVR had to respond to several I2C slave addresses (but not others), and the Arduino Wire interface is not well suited for that, but coding directly to the I2C interface made this possible.
The circuit board I made for this project consists of an ATMega 328p running at 16MHz and 5 V (because you need to run at 5 Volts to be in spec for 16MHz), a voltage level shifter to interface the 5 V I2C interface on the AVR to the 3.3 V Spektrum telemetry module, and connectors for the ISP interface, and a Bluetooth module I use to configure the Quadcopter with my phone. I used parts I had in my inventory, so some of the components, like the 1 Amp 3.3 V regulator and the 1 Amp diodes are not what I would us if I was ordering parts just for this project.
The circuit board is also designed for my home made PCB process (very large VIA pads), and would not be ideal for a professional PCB fabricator.
The raw Eagle files are located on my GitHub site along with the source code.
The serial stream that comes out of the APM is a protocol called Mavlink, and I spent a good bit of time on their site trying to figure out how to make this work. First, you have to download the Mavlink include files here, and generate them with a Python script. After running into some problems, I found that the structure __mavlink_request_data_stream_t in common/mavlink_msg_request_stream.h was incorrect (the order of the fields was wrong). It should look like:
typedef struct __mavlink_request_data_stream_t
uint8_t target_system; ///< The target requested to send the message stream.
uint8_t target_component; ///< The target requested to send the message stream.
uint8_t req_stream_id; ///< The ID of the requested data stream
uint16_t req_message_rate; ///< The requested interval between two messages of this type
uint8_t start_stop; ///< 1 to start sending, 0 to stop sending.
The other important Mavlink setting it know about when using a uC is at the top of main.c, before the include of mavlink.h:
// Save memory, MAVLINK by default allocates 4 256 byte buffers
#define MAVLINK_COMM_NUM_BUFFERS 1
If you don’t set the number of buffers to 1 before you include mavlink.h, you will waste 768 bytes of SRAM, which the AVR does not have to spare.
A most invaluable tool for debugging both the Serial connections and the I2C I’ve found is a logic analyzer. I use the Saleae Analyzer, which I am very happy with. It would have been very difficult to do this without being able to see exactly what was going on the wires.
Quad hooked up to a logic analyzer
On the Telemetry side of the process, when the unit is powered on, the Telemetry module requests data from the I2C slaves from address 0x00 to 0x7D twice (0,0,1,1,2,2,etc.). The bridge has to respond to the slave addresses it is going to fulfill (the RCGroups post above provide the list of slave addresses and functions) during this initialization, or that slave address will not be polled. The AVR cannot answer to addresses 0x00 and 0x01, because they are for internal use of some sort. The module Spektrum.c has a list of addresses the AVR will respond to, and, since during initialization the addresses are polled in numeric order, it sets the slave address of the AVR to the lowest address, and once that slave address has been called twice, the code resets the slave address to the next value, and so on util it has hit every address in the list. The AVR responds to the telemetry unit with the 16 byte response packet with 0 values.
After the initialization phase, the telemetry unit will only poll the slaves that responded during initialization, so the address mask, TWMAR, is set to 0xFF, which means the AVR will respond to any slave address. When any request for data is sent to the AVR after initialization, it looks at the address that is being requested, and sends the correct buffer back to the master. Another important thing to note is the telemetry unit does not poll the slave addresses until the radio it is bound to is up and ready to receive data, so if the radio is off, the initialization will occur, but no polling of the slave will occur until the raid is turned on.
One final thing to note is that the AVR does not have division natively built into the instruction set, so it is an expensive operation. Since the AVR needs to be available to handled the asynchronous requests from 2 outside feeds, it has to get the calculations done as quickly as possible . To get around this, a technique called fixed point arithmetic can be used. This article has an excellent description of the technique. For example, the following is a division by 10,000,000.
uint16_t divideBy1E7(uint32_t dividend)
return (uint16_t)((((int64_t)dividend * 0xD6E0LU) >> 16) >> 23);
//return (uint16_t)(dividend / 1E7);
In the source code there is a #define constant called TIMEIT which will include code to time the number of clock cycles the AVR takes to compute the GPS buffers (the most mathematically complex part of the code), and display that value in the Power Box 2nd battery capacity (which is normally 0). I tested the code with the straight-forward division and with fixed point, and compiled with full optimizations (-o3), the fixed point version of the setGPSBuf call took about 46,500 clock cycles while the division averaged 56,500 clock cycles. I’m not sure a 17.5% improvement was worth the effort of figuring out the fixed-point parameters, but it was interesting.
As I said above, all the code and eagle files are located on GitHub.