Gimbal

I ordered these motors and this gimbal controller some time ago to make a gimbal for the Raspberry Pi camera. This weekend I managed to model, print and test it.

It weights ~65 grams, is very compact and fits perfectly on my new quad (more on this some other time).

Here’s a timelapse video of the modelling process:

And here’s a video with me mounting it:

And finally the gimbal working:


It works on 6-8.4V (2S), and is very quick to print.

The total cost is ~50e for the controller and motors. In the future I’ll model some extra motor mounts for some more micro gimbal motors.

 

The STL + Design Spark files will be very soon on github.

[Edit] The files are available here:

https://github.com/jeanleflambeur/silkopter/tree/master/printing/gimbal

Advertisements

Raspberry Pi Camera V2 & GPS

Last weekend I did some tests with a UBLOX Neo6 GPS running next to the V2 camera to see if it’s better than the V1 camera.

The GPS had a clear view of half of the sky with a building to one side so not necessarily ideal conditions. The GPS was powered from the Pi with the cable wrapped through a small ferrite bead.

Here is a picture of the setup:

IMG_0384.JPG

Here is the graph showing the Satelite number, PDOP, Position accuracy and velocity accuracy reported by the GPS:

Screenshot from 2016-07-17 11-16-49.png

There is a clear jump when turning on the camera where PAcc goes from ~2 to ~3.2. With the V1 camera this jumped to 30-40.

The jamming indicator showed 15-20% regardless if the camera was off or on. With V1 the indicator jumped to 100% when turning the camera on.

The number of satellites is the same, as is the PDOP and VAcc.

 

The spectrum measured with the SDR was crystal clear even when the antenna was almost touching the camera ribbon cable – which btw is unshielded.

 

So my conclusion is that the V2 interferes with the GPS way less than the V1 and with a bit of (optional) ribbon cable shielding it should be ok for a multirotor.

 

 

Binary Releases

I decided to start making binary releases available for everyone to download.

Here’s the first one – the GS and the brain binaries:

https://bintray.com/jeanleflambeur/silkopter/gs_brain/d779ff73ee330c82acc82171ddfdbb03701da784

I’ll write a new post later today or tomorrow about the install steps and how to get it working.

Note this is not finishes for flying, it’s an experimental release of the new version of Silkopter that can be used to debug sensors and test most of the nodes.

 

 

Silkopter FC

Finally, after some DHL delivery trouble the boards arrived:

pcb

After watching a few tutorials I immediately tried soldering the most difficult part – the MPU9250 chip. Turns out – if you don’t have the right flux type it’s impossible. I managed to ruin 2 boards in the process and broken 1 chip…

broken pcb

Notice the PCB changing color next to the IMU I2C pins and the ‘dry’ look. The pin header holes are also darker than the others due to the heat.

 

2 days later my new flux came: Chip Quick SMD291 ordered from farnell. This time it went much better as I was able to solder 2 chips in 5 minutes, from the first try. The trick is that tacky solder flux doesn’t evaporate from the hot air and keeps the chip floating while the solder melts (and prevents oxidation). When the solder flows, the chip kinda snaps into place and that’s it. In my first tries I used a flux pen that dried immediately and the friction between the chip and the PCB was greater than the solder surface tension – preventing the chip from snapping into place.

So this is the board almost complete, except for the ADS1115 ADC that didn’t arrive still:

final pcb

 

The last few days I spent trying to get the RF4463 chip to respond on SPI1. I expected surprises and I got them:

  • first of all, activating SPI1 on a RPI3B results in boot times of 2 minutes due to an interrupt issue with the UART driver. Solution: disable the bluetooth and use ttyAMA0 instead of ttyS0
  • next I simply couldn’t get the chip to respond at all. 2 days I spent on this until I went back to the schematic thinking the wiring is not ok. Then I realized that the chip select is connected to CS2 which is GPIO 16 but the SPI1 overlay I used uses GPIO 18 as CS. Duh! Solved by adding dtoverlay=spi1-1cs,cs0_pin=16 in /boot/config.txt
  • that’s it – it works now 🙂

 

In the last moment I added 3 WS2812B RGB LEDs thinking that I will be able to drive them with PIGPIO. I didn’t check the datasheet closely enough to see that they need PWM impulses in the order of 0.5ns whichare way below the resolution of the library.

So today after soldering the LEDs I realized that I cannot drive them. Desperate, I turned to the schematic and saw that by sheer luck I connected the LEDs to GPIO 13 – which happens to be the hardware PWM channel 1. Yaay – exactly what this library needs.

So I downloaded the lib, configured it on DMA channel 4 and PWM channel 1 and ran it and voila! It works with <1% CPU usage:

final pcb2

 

So now I have almost all the hardware working – the dual IMUs, the Baros, the RF chip and most importantly – vital for a 2016 multirotor – the LEDs.

 

I have 6 unpopulated left to give away (for free of course, shipping covered by you) if someone is interested!

boards

 

So now – back to software. The RC is coming along nicely.

Classic TX

I thought to give my Flysky TH9x TX a change to use it with silkopter so I made a new node called “CPPM Receiver”. This samples a CPPM stream on a GPIO and outputs the PWM streams that can be fed into the main node.

 

It uses the PIGPIO setAlertFunc which calls a user provided function for all level transitions on a certain gpio pin. The resolution is 5 microseconds which is waay more than needed for a standard CPPM stream. The gap between channels is 400 microseconds so way bigger than the minimum resolution of the library.

 

It started more of a proof of concept but in the end I think it’s perfectly usable.

Here’s a video with it in action, connected to a D8R-II receiver:

 

 

Schematic & PCB #2

I finally finished the schematic and PCB and will send them to DirtyPCB today.

It is in HAT format ready to be used with the new (when it’s released) Raspberry PI A3.

It supports either a RF4463F30 or a RFM22B (on the back side) connected to SPI2, a MPU9250 IMU and MS5611 baro connected to SPI1 and/or another set of MPU9250 & MS5611 connected to I2C.

On the same I2C there is an ADS1115 for current/voltage sensing and 2 extra ADC pins broken out on a separate controller.

PIGPIO is connected to 6 PWM pins – 4 for motors and 2 for a gimbal.

In the end I also had sufficient space to include 3 NeoPixels (or WS2812b) RGB LEDs to show various status info. They are all driven by only one GPIO using a serial-like protocol and I think I can make this work with the PIGPIO wave functions.

 

In a future revision I might add a buzzer for alerting comm errors – like link lost – and expose 2 more PWM outputs.

 

First I have to check if the PCB will be manufactured ok.

 

Some screenshots:

 Update July 14th: 

I made some tweaks to the SCH and PCB to include a diode for the leds, according to Adafruit.

In the previous version I was powering the LEDs from 3.3V which is below their minimum voltage. Datasheet here.

To fix this I routed the led VCC to 5V but through a diode.
The WS2812b leds expect the high input voltage to be within 0.7 VCC. With a 5V VCC this results in a 3.5V high level. The raspberry pi provides a reliable 3.3V on the GPIO and there was the risk of the leds failing to understand the PI. There are 2 solutions – a level converter to increase the high level of the PI to 5v or lowering the VCC of the leds a bit. I choose the second ’cause it’s way simpler. A diode drops the voltage from 5V to ~4.3V which in turn drops the expected high level to a convenient ~3V.

Two other changes that I did are:

  • Fixed some signal traces that were running parallel on the top and bottom sides – like the I2C and some PWM outputs. Not sure how important it is but just to avoid any nasty coupling
  • Broken out the I2C to a pin header to eventually connect a PX4Flow.

The board is in production now and I really hope it gets here by Friday.

 

In the meantime I’m investigating a PPM input solution in case I want to use a standard RC system with this board.

 

Schematic & PCB

The Silk FC schematic is finished and the PCB is 70% done.

Here are some screenshots:

 

There is a bit of wiring acrobatics with the PWM connector as I couldn’t find an eagle 6×3 header so I improvised one from 3 2×3 headers… Ugly but it works.

As soon as they are done I will put them on github.

The board is 2 layers only and has the specs from my last post.

I intend to add a footprint for a RFM22B as well as the RF4463F30 so I can choose which to use, in case one of them has issues.

The board should work in both single-imu-baro more and also in dual-imu-baro.

I got 2 MPU9250 and MS5611 from my previous Drotek 10 DOF boards with a bit of hot air.

 

I’m really curious how it will end up as this is my first SMD board and especially my first QFN package. The MPU is really really tiny – like 2 square millimeters.

 

 

Custom FC

In the past weeks I made progress on the software side. I have the node editor working again and the basic RC also.

To avoid getting bored I switched again to HW and started designing the FC PCB. It will include:

  • 2 x MPU9250 and MS5611 for redundancy and better noise filtering. One of the 9250 will use SPI and the other I2C, to have bus redundancy
  • 1 RF4463F30 transceiver.
  • 1 ADS1115 for ADC conversion
  • Several status LEDs

 

PWM will be handled by PIGPIO which works very nicely. Better than a PCA9685 that only allows 1 frequency for all the outputs.

It will have a HAT layout, without the EEPROM (still not very clear what good is it).

I already started designing it in Eagle (the free version) and will do the PCB using DirtyPCB.

 

 

 

RF Modules

I found and bought the RF modules I needed for the GS-brain comms: the RF4463F30

They have a 1W PA, the receiver has -122dBm sensitivity at low data rates and around -90-96dBm at 1Mbps max rate.

They are pretty small and light – under 2-3 grams I think.

IMG_0265

I managed to find a lot of documentation from SI and the RF chip works pretty differently than the RFM22B. It’s not programmed using registers but with SPI commands. You basically create API calls in a uint8_t buffer and send it through SPI.

The FIFO is only 64 bytes but it does have a CRC and a custom 4 byte header to pack various things – like a request id.

So far it seems very nice, I will try to make it work during the weekend.

 

The plan I have is to use the wifi only for video data – tx only, and the RF4463 for bidirectional commands (GS->Brain and back) and video packet confirmations.

This should allow me to use better the limited wifi bandwidth by using uni-directional comms – so the back packets don’t keep the channel busy – and also have a rock solid link for control.

I did some tests with the RFM22B at 0dBm and it can easily go through 4-5 walls and 20 meters  inside my house. With 20 or even 30dBm I should be able to have a solid 10Km link line of sight.

 

Definition Language

aaand it’s done!

The definition language for specifying node configuration is finished – at least in principle.

It ended up being a stand-alone library called def_lang (uninspired, I know)

Basically I can now define things like this:

namespace silk
{
  alias mass_t = ufloat;

  struct UAV_Config_Base
  {
    string name : [ui_name = "Name"];
    mass_t mass : [ui_name = "Mass (Kg)"];
    ufloat moment_of_inertia : [ui_name = "Moment Of Inertia"];
  };

  struct Multirotor_Config : public UAV_Config_Base
  {
    struct Motor
    {
      math::vec3f position;
      direction_t thrust_vector = {0.f, 0.f, 1.f};
      bool clockwise = false;
    };

    ufloat radius = 1.f; //m
    ufloat height = 1.f; //m
    ufloat motor_z_torque = 0.f; //Nm
    ufloat motor_thrust = 1.f; //N
    ufloat motor_acceleration = 10.f; //N/s
    ufloat motor_deceleration = 10.f; //N/s
    vector<Motor> motors;
  };
}

And use this definition in 2 possible ways:

    • Dynamically, through a type system:
ts::ast::Builder builder;

auto parse_result = builder.parse_file("definition_file.def");
if (parse_result != ts::success)
{
  std::cerr << parse_result.error().what();
  return;
}

//walk and display the AST
std::cout << builder.get_ast_root_node().to_string(0, true) << std::endl;

ts::Type_System ts;
ts.populate_builtin_types(); //this adds standard built-in types like float and double

auto compile_result = builder.compile(ts); //populate the typesystem from the AST
if (compile_result != ts::success)
{
  std::cerr << compile_result.error().what();
  return;
}

//now the type system is fully populated and all the types is it are instantiable
std::shared_ptr<ts::IStruct_Type> base_type = ts.find_specialized_symbol_by_path<ts::IStruct_Type>("silk::UAV_Config_Base");
std::shared_ptr<ts::IStruct_Type> type = ts.find_specialized_symbol_by_path<ts::IStruct_Type>("silk::Multirotor_Config");

assert(base_type->is_base_of(*type)); //inheritance is respected

std::shared_ptr<ts::IStruct_Value> multirotor_config_value = type->create_specialized_value(); //create a value of type Multirotor_Config
auto construct_result = multirotor_config_value->construct(); //construct it. This struct is default constructible. An alternative is to initialize it using an initializer list

//let's get (select) some members
//the full dot sintax is supported when selecting so things like motors[0].position works
std::shared_ptr<ts::IString_Value> name_value = multirotor_config_value->select_specialized<ts::IString_Value>("name");

//all setters might fail so I need to check the return value
auto result = name_value->set_value("silkopter");
TS_ASSERT(result == ts::success);

//values are serializable as well to an intermediary tree
auto serialize_result = multirotor_config_value->serialize();
TS_ASSERT(serialize_result == ts::success);

//the intermediary tree can be serialized to json or binary
std::string json = ts::serialization::to_json(serialize_result.payload(), true);

//a more complicated example:
//get the motors vector
std::shared_ptr<ts::IVector_Value> motors_vector_value = multirotor_config_value->select_specialized<ts::IVector_Value>("motors");

//create a new motor
std::shared_ptr<ts::IStruct_Value> motor_value = ts.create_value<ts::IStruct_Type>("silk::Multirotor_Config::Motor");

//change it's position
motor_value->select_specialized<ts::IVec3f_Value>("position")->set_value(ts::vec3f(0, 1, 0));

//add it to the vector
auto result = motors_vector_value->insert_value(0, motor_value);
assert(result == ts::success);

Basically  it’s a full type system implementation similar to the C++ one with type safety, templated types, aliases, values, casting, inheritance etc etc. But on top of the standard C++ features it also allows custom attributes on members and types (like ui_name for example), it supports reflection so everything can be enumerated and it’s serializable. This dynamic one will be used in the GS to generate UI for all the nodes. Like this the GS can work with new nodes without me having to do custom UI for each node I intend to add (or everytime I change the node config to add/remove a parameter, or change a type etc)

 

  • Statically through generated C++ code

From the definition above I can also generate C++ code for all the structs and work with them through getters and setters. Each type in the type system can be mapped to a native C++ type. So in this example, the above structure would be usable like this from generated code:

assert(std::is_base_of<silk::UAV_Config_Base, silk::Multirotor_Config>::value); //inheritance is respected

//create a value of type Multirotor_Config. This also default constructs it
silk::Multirotor_Config multirotor_config_value;

//all members have setters and getters generated for them
std::string name_value = multirotor_config_value.get_name();
multirotor_config.set_value("silkopter");

//values are serializable as well to an intermediary tree
auto serialize_result = silk::serialize(multirotor_config_value); //serialization methods are generated as well
TS_ASSERT(serialize_result == ts::success);

//the intermediary tree can be serialized to json or binary
std::string json = ts::serialization::to_json(serialize_result.payload(), true);

//a more complicated example:
//get the motors vector
std::vector<silk::Multirotor_Config::Motor> motors_vector_value = multirotor_config_value.get_motors();

//create a new motor
silk::Multirotor_Config::Motor motor_value;

//change it's position
motor_value.set_position(ts::vec3f(0, 1, 0));

//add it to the vector
motors_vector_value.insert(motors_vector_value.begin(), motor_value);

//set it back in the config
multirotor_config_value.set_motors(motors_vector_value);

The generated code is simpler to work with partly because it’s a lower level of abstraction (I use the build-in C++ typesystem and work in value space) but also because it’s familiar.

The interested thing is that the generated code keeps inside as a string the definition file contents that it was generated from. This is useful to implement the following flow:

  1. I write the definition file for the bran and all its nodes
  2. I generate the c++ code for the brain. Now all the nodes can use C++ structs for the parameters and config structs. Simple and type-safe as it’s impossible to access non-existing fields or use them with the wrong type. It’s standard, simple C++ structs
  3. When the brain connects to the GS, it sends the def file it was generated from
  4. The GS instantiates a dynamic Typesystem and parses the def file from the brain, so now it will understand and be able to (de)serialize the data it will receive from the brain – like node configs, etc

If I want to add a new node, I add in the def, regenerate the code and use the resulting config struct in the new node. The GS will automatically know how to handle the new node as it will receive the def for it next time it connects to the brain

 

Easy peasy