Tuesday, July 11, 2017

RC Joystick with FrSky DHT Module and USB Host



I've wanted to try controlling my Rover and Quadcopter with a Joystick, at first I thought I can use the simple potentiometer based joysticks like these.




Attach them to ADC and transmit the values over to the Controller.


Then I've decided its a good opportunity to try one of the USB Host I had



The USB Host module is based on the Maxim MAX3421E (Datasheet), which is a USB Host controller to SPI, accessible to Arduino programming. Oleg Mazurov wrote a very nice library for it and he has some very nice articles on how to use it, as well as explanations on how to patch the module to get 5 volts supplied to the USB device, which I needed for this project.




You will need to cut the wire where the red circle is and attach that wire to the pin marked in green. Otherwise the USB device will get 3.3v and its not enough for most devices.


Another thing to point out is that these modules work on 3.3v and need 3.3v signal, so a level converter might be needed if you're using 5v Arduino, if you're planning to risk it, then from my experience its not going to burn it, but you'll get an unreliable connection.



I've then attached it to a real joystick to see how if its working




As I wrote, Oleg did a very good job and I got a demo up and running in no time.


I've then thought it might be a good idea to send this data directly to a standard RC receiver, I remember I've found FrSky DHT transmitter (Manual) while looking up FrSky protocol.





The FrSky DHT is a DIY module which uses PPM as input and transmit the values to a standard FrSky receiver, it also has a serial port for getting telemetry back but I didn't use it at this stage.


Many people use it to modify their transmitters to FrSky protocol and you can find many guides on how to convert your own transmitter.


I've also ordered RX-F802 but as some people pointed out in the forums, it died quickly.




I've had another receiver which worked perfectly so I didn't get overly excited about it.


So now we have all the hardware we need and the software for reading the Joystick values, we're almost ready.


I've wanted an easy way to see the values as its being decoded, but keeping the serial port connected all the time is not always useful. 


I've used LCD1602, but it has a parallel interface and I didn't want to connect so many pins between it and the Arduino.




To that end there's I2C to Parallel interface module based on PCF8574T (Datasheet), Frank de Brabander wrote a library for controlling LCD1602 with it, but it can be used for other purposes as well.


The interface is pretty simple, you instantiate it with the I2C address and the number of columns and rows and it does the rest.


You can switch the backlight on and off with

- backlight()
- noBacklight()

You can update the same location by:

- setCursor(uint8_t, uint8_t);
- print(value);

And you can even set 8 custom characters!


Pretty useful for such a low cost device!


So now we have all the components for our USB Joystick to PPM, I've hooked it up, added a switch and printed a box and then went ahead to test it.






I've set it up so each axis is displayed as -99 to 99 for 1000-2000μs and M0-M6 as controller mode matching ArduCopter flight mode values. The rest is not working yet, but the plan is to control channels 7-8 with the 'hat' on the joystick.


But having the numeric values in Arduino is not enough, we need to transfer these values to the FrSky transmitter, this is done with PPM, the basics are, there are 8 pulses, each one variable by width (or time), each one represents a channel and then there is a pause and it starts all over again, this is done about 30-50 times per second, so this thing is pretty fast, Joshua Bardwell have some numbers for you.


I've researched a few libraries but none of them were accurate or consistent until I've found ArduinoRCLib, I've used this library for other things and its very consistent. 


You can find my source code here:

https://github.com/drorgl/USBJoystickPPM


Now that we have a working transmitter, we need to check the receiver and see the values actually match.



I've created a simple PPM Display, it listens for PPM on pin 3 and using LCD to display the 8 channels received.




The design is pretty basic, ATMEGA168, 3310 LCD, based on PCD8544 (Datasheet).


Note that the LCD VCC must not exceed 3.3v, so you have to regulate the voltage, I've used A1117 3.3v for that, but its inputs are 5v tolerant (at least from experience) so no need for a logic converter like the USB Host module.

You can find the source code here:
https://github.com/drorgl/PPMDisplay

I've hooked it up to a Mini FrSky Receiver and here are the results:



What happens is, 
- First, monitor/receiver is turned on without a transmitter, you can see the values are in the middle (~1500)
- Transmitter is turned on, values are what the transmitter sends, note channel 6 is ~950, which means a flight mode wasn't set yet (M0).
- Roll is tested
- Pitch is tested
- Yaw is tested
- Throttle is tested
- Flight modes are tested
- USB Disconnected Failsafe is tested
- Transmitter power off failsafe is tested

I didn't have the guts to test it on a quad yet, I'll continue testing it on my new rover until I have enough confidence its working properly :-)

Todo:
- Joystick 'Hat' to channels 7-8
- FrSky Telemetry Monitoring, a source code for the telemetry protocol, RSSI Configuration and hookup:
Source https://www.rcgroups.com/forums/showpost.php?p=26378136&postcount=465

Saturday, July 8, 2017

C Closures

I've started writing this article about C Closures while doing research for a project that needed them, eventually I've failed to provide a working code on Visual Studio and decided to keep the article as a lesson learned rather than a how to.


Callbacks are great language construct, no matter which language, but adding data to each callback is sometimes necessary. While in C++ you can provide a std::function callback which can include user data and even use lambda with captured variables, in C its a bit different.

#include <functional>

int test_function(int n1)
{
 printf("value %d", n1);
 return n1;
}

int main()
{
 auto f1 = std::bind(&test_function, 42 );
 f1();

 int n1 = 42;

 auto f2 = [=]() {return n1; };

 f2();

    return 0;
}


Since you can't create a function at runtime, Many C APIs provide a way to include data pointer and this pointer is passed to the callback.

typedef void (fn)(int value, void* data);
int function(fn* f, void* v) {
 f(1, v);
 return 0;
}

APIs that do not provide userdata void * pointer are a bit trickier to call with user data. To overcome this problem, the developer can use something called Closures. C closures are not part of the language but are still possible. Two of the common libraries that provide this functionality are libffcall and libffi, both of these libraries generate a function on the fly and provide the new function's address.

To actually generate these functions, these libraries needs to know the CPU architecture and compiler used because they need to implement a compatible call.

Lets start with libffcall, to compile it you can start by cloning https://github.com/libffcall/libffcall
You can find the documentation here: https://www.gnu.org/software/libffcall/

If you're compiling for linux, you should read the readme file, if you're compiling for windows, you should read readme.win32.

For some reason, the compilation failed on my machine and I've had to add _WIN32 and _WIN64 to the #ifdef __i386__ and #ifdef __x86_64__ like so:

#if defined(__i386__ ) || defined(_WIN32)
#define TRAMP_LENGTH 15
#define TRAMP_ALIGN 16  /* 4 for a i386, 16 for a i486 */
#endif

and

#if defined(__x86_64__) || defined(_WIN64)
#define TRAMP_LENGTH 32
#define TRAMP_ALIGN 16
#endif

In the end, I couldn't get the project working on Visual Studio, I've then proceeded to try libffi with Visual Studio as well and after fixing and workarounding more than a dozen errors I gave up.

I have no doubt that these two projects work in more than one environment, but perhaps because its not very simple to build and use might point to a weak link, it works by creating assembly code that encapsulates the userdata and the function pointer, completely ignoring the compiler (though it should use the same calling conventions though a provided generator). On top of that, because its not using the compiler directly, its cross-platform-ness is not as robust across compilers and CPU architectures as portable C should be. To strengthen my point, libffi for example, supports only 64bit visual c++ builds according to the build scripts.

I'm not sure if its my own fault for not being able to compile these libraries successfully on Windows/Visual Studio but in any case I see it as an important lesson about C API Design, no matter how ridiculous it might look at first, if you're expecting a callback, a void * user data should be provided as well.

I'm including my build batch for libffi/Windows, if anyone is successful using any of these libraries, share your knowledge, if you find a different method to implement closures in a cross-platform way, even better.

set CYG_ROOT=%CD%/cygwin
set CYG_CACHE=%CD%/cygwin/var/cache/setup
set CYG_MIRROR=http://mirrors.kernel.org/sourceware/cygwin/

rem libffi is not supported on x64/visual c++
rem set VCVARS_PLATFORM=x86
rem set BUILD=x86-pc-cygwin
rem set HOST=x86-pc-winnt

set VCVARS_PLATFORM=amd64
set BUILD=x86_64-pc-cygwin
set HOST=x86_64-pc-winnt

curl -O http://cygwin.com/setup-x86.exe

setup-x86.exe -qnNdO -R "%CYG_ROOT%" -s "%CYG_MIRROR%" -l "%CYG_CACHE%" -P dejagnu
setup-x86.exe -qnNdO -R "%CYG_ROOT%" -s "%CYG_MIRROR%" -l "%CYG_CACHE%" -P Devel,autoconf,automake,make,libtool

%CYG_ROOT%/bin/bash -lc "cygcheck -dc cygwin"

rem %comspec% /k ""C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"" x86
%comspec% /k ""C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"" amd64

%CYG_ROOT%\bin\sh -lc "(cd $OLDPWD; ./autogen.sh;)"
%CYG_ROOT%\bin\sh -lc "(cd $OLDPWD; ./configure CC=''$PWD'/msvcc.sh' CXX=''$PWD'/msvcc.sh' CXXCPP=''$PWD'/msvcc.sh' LD=link CPP='cl -nologo -EP' --build=$BUILD --host=$HOST; cp src/x86/ffitarget.h include; make;)"

rem %CYG_ROOT%\bin\sh -lc "(cd $OLDPWD; ./configure CC='./msvcc.sh -m64' CXX='./msvcc.sh -m64' LD=link CPP='cl -nologo -EP' --build=$BUILD --host=$HOST; cp src/x86/ffitarget.h include; make;)"