-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support RC receivers #1071
Comments
I've been casually thinking about this for about a year and the basic sketch I always return to is:
That's about as far as I usually get. It's critically important that these are designed with the controller pattern. |
I'm not sure we need I'll need to think through the |
That may very well be the case, I just figured I'd put it there and we can let the junk shake out :) |
I just purchased these, should receive them tomorrow |
The major issue we face is that
I like the idea of defining our own I2C backpacked peripheral, because we can control every aspect. I've come to realize that the design decisions made by many (not all) hardware creators is generally not very compatible with our async goals. |
Is there a way to handle PPM signal over firmata? |
@henricavalcante no, but it can be made available via I2C |
I assembled this gizmo as a basic "backpack" starting point. In the photos below, you'll notice that the intention is to plug this thing directly into the receiver unit's channel pins. The fritzing shows everything facing forward, but that's just only for illustrative purposes (and a limitation of fritzing) G: Ground I haven't added a label for channels, because my actual receiver won't arrive until later today, so I'm not sure which direction the channels are in. |
Nice. I'll try to build something similar. |
I cut them out and made it fit. Then I made a newer, less messy backpack. Pictures later |
That's the same Rx I have. FYI, the typical method for connecting such a receiver to your RC project (drone, truck, etc) is with a breakout cable, such as this Naze32 one: http://www.readymaderc.com/store/index.php?main_page=product_info&products_id=3062 That also gives you more flexibility in positioning components on your build. I'll probably make my backpack to accept something similar to that. Note that weight is also a factor for many uses of this, so I'd like to investigate the minimum backpack necessary once we get the design working. |
The second one I built significantly reduced materials:
But you won't be able to use such a thing if you're trying to read the pulse values from the receiver—right? You're effectively replacing this thing and adding a "middle man" to intercept the pulses to do something else with them. |
I'm testing the Pin Change Interrupts to decode pwm signal and it works, but I'm trying to figure out a way to send the information to johnny-five like firmata 'cause I'm using a crius board which uses atmega 2560 and has more sensors and I need them, when I upload firmata to board I cant read pwm signals, when I upload my code with Pin Change Interrupt I only access pwm signals from receiver. I have a quadcopter working well with this firmware and I'm trying to make a car controlled by johnny-five. There is a way to rewrite firmata with Pin Change Interrupts in some pins? something like:
I'm crazy or there is a way to make it possible? |
The reader code will go on a different microprocessor. I have some super basic firmware written that gets us 6 channels of PPM values. How are you using the pin change interrupts? I couldn't get them to do what I wanted. Can you share your work so far? Tomorrow I will push out what I have so far |
It's my code: #include <PinChangeInt.h> //https://github.com/GreyGnome/PinChangeInt
#define FIRST_PIN 62
volatile int pwm[] = {0,0,0,0,0,0,0,0,0,0};
volatile int last_up[] = {0,0,0,0,0,0,0,0,0,0};
void up()
{
uint8_t pin=PCintPort::arduinoPin;
PCintPort::attachInterrupt(pin, &down, FALLING);
last_up[pin - FIRST_PIN] = micros();
}
void down() {
uint8_t pin=PCintPort::arduinoPin;
PCintPort::attachInterrupt(pin, &up, RISING);
pwm[pin - FIRST_PIN] = micros()-last_up[pin - FIRST_PIN];
Serial.print(pin);
Serial.print(" - ");
Serial.println(pwm[pin - FIRST_PIN]);
}
void setup() {
pinMode(A8, INPUT); digitalWrite(A8, HIGH);
pinMode(A9, INPUT); digitalWrite(A9, HIGH);
pinMode(A10, INPUT); digitalWrite(A10, HIGH);
pinMode(A11, INPUT); digitalWrite(A11, HIGH);
pinMode(A12, INPUT); digitalWrite(A12, HIGH);
pinMode(A13, INPUT); digitalWrite(A13, HIGH);
pinMode(A14, INPUT); digitalWrite(A14, HIGH);
pinMode(A15, INPUT); digitalWrite(A15, HIGH);
Serial.begin(115200);
//start interrupts
PCintPort::attachInterrupt(A8, &up, RISING);
PCintPort::attachInterrupt(A9, &up, RISING);
PCintPort::attachInterrupt(A10, &up, RISING);
PCintPort::attachInterrupt(A11, &up, RISING);
PCintPort::attachInterrupt(A12, &up, RISING);
PCintPort::attachInterrupt(A13, &up, RISING);
PCintPort::attachInterrupt(A14, &up, RISING);
PCintPort::attachInterrupt(A15, &up, RISING);
}
void loop() { } And this is some serial output:
When I move the sticks on controller, values on serial output change between ~600 and ~1500 |
Are you getting receiver PPM signal by channels output? how? |
Yes, I was mistaken. Here is the crude firmware I wrote: #include <Wire.h>
#define DEBUG_MODE 1
// Address Pins
#define AD0 8
#define AD1 9
// I2C Defaults
#define I2C_DEFAULT_ADDRESS 0x0A
#define I2C_BUFFER_SIZE 12
byte buffer[I2C_BUFFER_SIZE];
int addressPins[] = { AD0, AD1 };
int address = I2C_DEFAULT_ADDRESS;
int first = 2;
int channels = 6;
void resetState() {
for (int i = first; i < first + channels; i++) {
pinMode(i, INPUT);
}
}
void setup() {
int offset = 0;
for (int i = 0; i < 2; i++) {
pinMode(addressPins[i], INPUT);
if (digitalRead(addressPins[i])) {
offset |= 1 << i;
}
}
address += offset;
#if DEBUG_MODE
Serial.begin(9600);
#endif
resetState();
Wire.begin(address);
Wire.onRequest(onRequest);
}
void loop() {
uint16_t pulses[channels];
for (int i = first; i < first + channels; i++) {
pulses[i - first] = pulseIn(i, HIGH, 35000);
}
#if DEBUG_MODE
for (int i = 0; i < channels; i++) {
Serial.print(i);
Serial.print(": ");
Serial.println(pulses[i]);
}
#endif
buffer[0] = pulses[0] >> 8;
buffer[1] = pulses[0] & 0xFF;
buffer[2] = pulses[1] >> 8;
buffer[3] = pulses[1] & 0xFF;
buffer[4] = pulses[2] >> 8;
buffer[5] = pulses[2] & 0xFF;
buffer[6] = pulses[3] >> 8;
buffer[7] = pulses[3] & 0xFF;
buffer[8] = pulses[4] >> 8;
buffer[9] = pulses[4] & 0xFF;
buffer[10] = pulses[5] >> 8;
buffer[11] = pulses[5] & 0xFF;
}
void onRequest() {
Wire.write(buffer, I2C_BUFFER_SIZE);
} I don't want to put this inside any version of Firmata, because that means it's not available on any other platform that Johnny-Five supports. The benefit of using the I2C slave backpack approach is that all we need is I2C support on other platforms and we're good to go. I'm going to try to combine your interrupt approach with my slave firmware (I will obviously cite you as co-author) |
I'm not sure we should use this: https://github.com/GreyGnome/PinChangeInt it's not available via the Arduino IDE "Manage Libraries" interface, which means people have to go through the hassle of adding it via the "Add .ZIP Library". Both are terrible. Also, there is a notice on the repo:
The version referenced does exist in the Arduino IDE's Library Manager. |
The interrupt version is giving me wildly inaccurate results and appears to be slower than the version with pulseIn... which is strange. |
What if we use EnableInterrupt instead? |
We'll have to either way. |
So... this works awesome when channels is set to 1. Once it I set back to 6 or 8, the values are garbage. #define EI_ARDUINO_INTERRUPTED_PIN
#include <EnableInterrupt.h>
#include <Wire.h>
#define DEBUG_MODE 1
// Address Pins
#define AD0 11
#define AD1 12
// I2C Defaults
#define I2C_DEFAULT_ADDRESS 0x0A
#define I2C_BUFFER_SIZE 16
byte buffer[I2C_BUFFER_SIZE];
int addressPins[] = { AD0, AD1 };
int address = I2C_DEFAULT_ADDRESS;
int first = 2;
int channels = 1;
volatile int pulses[8];
volatile int last_up[8];
void up() {
uint8_t pin = arduinoInterruptedPin;
enableInterrupt(pin, &down, FALLING);
#if DEBUG_MODE
Serial.print("pin up: ");
Serial.println(pin);
#endif
last_up[pin - first] = micros();
}
void down() {
uint8_t pin = arduinoInterruptedPin;
enableInterrupt(pin, &up, RISING);
// #if DEBUG_MODE
// Serial.print("pin down: ");
// Serial.println(pin);
// #endif
pulses[pin - first] = micros() - last_up[pin - first];
#if DEBUG_MODE
Serial.print(pin);
Serial.print(": ");
Serial.println(pulses[pin - first]);
#endif
}
void resetState() {
for (int i = first; i < first + channels; i++) {
pulses[i - first] = 0;
last_up[i - first] = 0;
pinMode(i, INPUT_PULLUP);
enableInterrupt(i, &up, RISING);
}
}
void setup() {
// First check if the address was set via pins 11 or 12
int offset = 0;
for (int i = 0; i < 2; i++) {
pinMode(addressPins[i], INPUT);
if (digitalRead(addressPins[i])) {
offset |= 1 << i;
}
}
address += offset;
resetState();
#if DEBUG_MODE
Serial.begin(9600);
#endif
// Initialize Slave
Wire.begin(address);
Wire.onRequest(onRequest);
Wire.onReceive(onReceive);
}
void loop() {
for (int i = 0; i < channels; i++) {
buffer[i * 2] = pulses[i] >> 8;
buffer[i * 2 + 1] = pulses[i] & 0xFF;
}
}
void onRequest() {
Wire.write(buffer, I2C_BUFFER_SIZE);
}
void onReceive(int count) {
while (Wire.available()) {
// Command 0x01 => Reset.
if (Wire.read() == 0x01) {
Serial.println("RESET");
resetState();
}
}
} ............ Can we discuss the pros and cons of pulseIn vs interrupts, given the context (which is: 100% dedicated microprocessor that must only write to I2C bus) |
Im going home now and I will make some tests about differences, interrupts should be better because is non blocking, but I'm thinking, if you want to make a hardware exclusive to convert signals why not convert pwm to analog signal with capacitors and use arduino ADC ports? |
Are you building a tessel 2 module? |
Check using without Serial output inside interrupt delegates like this: #define EI_ARDUINO_INTERRUPTED_PIN
#include <EnableInterrupt.h>
#include <Wire.h>
#define DEBUG_MODE 1
// Address Pins
#define AD0 11
#define AD1 12
// I2C Defaults
#define I2C_DEFAULT_ADDRESS 0x0A
#define I2C_BUFFER_SIZE 16
byte buffer[I2C_BUFFER_SIZE];
int addressPins[] = { AD0, AD1 };
int address = I2C_DEFAULT_ADDRESS;
int first = 2;
int channels = 8;
volatile int pulses[8];
volatile int last_up[8];
void up() {
volatile uint8_t pin = arduinoInterruptedPin;
enableInterrupt(pin, &down, FALLING);
last_up[pin - first] = micros();
}
void down() {
volatile uint8_t pin = arduinoInterruptedPin;
enableInterrupt(pin, &up, RISING);
pulses[pin - first] = micros() - last_up[pin - first];
}
void resetState() {
for (int i = first; i < first + channels; i++) {
pulses[i - first] = 0;
last_up[i - first] = 0;
pinMode(i, INPUT_PULLUP);
enableInterrupt(i, &up, RISING);
}
}
void setup() {
// First check if the address was set via pins 11 or 12
int offset = 0;
for (int i = 0; i < 2; i++) {
pinMode(addressPins[i], INPUT);
if (digitalRead(addressPins[i])) {
offset |= 1 << i;
}
}
address += offset;
resetState();
#if DEBUG_MODE
Serial.begin(9600);
#endif
// Initialize Slave
Wire.begin(address);
Wire.onRequest(onRequest);
Wire.onReceive(onReceive);
}
void loop() {
#if DEBUG_MODE
for (int i = first; i < first + channels - 1; i++) {
Serial.print(pulses[i - first]);
Serial.print(" - ");
}
Serial.println(pulses[channels - 1]);
#endif
for (int i = 0; i < channels; i++) {
buffer[i * 2] = pulses[i] >> 8;
buffer[i * 2 + 1] = pulses[i] & 0xFF;
}
}
void onRequest() {
Wire.write(buffer, I2C_BUFFER_SIZE);
}
void onReceive(int count) {
while (Wire.available()) {
// Command 0x01 => Reset.
if (Wire.read() == 0x01) {
Serial.println("RESET");
resetState();
}
}
} works like a charm here with arduino pro mini (atmega 328p 16MHZ) ;) |
Not specifically, but I plan to use this with Tessel 2 :D |
@henricavalcante 👏 this works perfectly |
Yeah typically you want to do as little as possible inside the interrupt service handler (ISR) - usually just toggle a variable (marked as "volatile") and then based on the state of that variable, do any actual reporting or heavy calculations in the main loop. Some architectures limit the size of the stack for the ISR to just a few bytes so you can't do much and really want to return as quickly as possible. |
I feel like I learned a lot today—thanks for providing that additional insight :) |
There are also scenarios where you want to disable the interrupts globally while executing a piece of code. You have to approach from the perspective of what happens if an interrupt is triggered while some block of code is executing and if so disable interrupts globally before that block and reenable after that block. You will see a lot of this if you dig into the code in Adafruit libraries and other well-written Arduino libraries that use interrupts. You'll see this in PJRC libs as well. |
here's an example where disabling and reenabling global interrupts is useful: https://github.com/PaulStoffregen/CapacitiveSensor/blob/master/CapacitiveSensor.cpp#L154-L183 |
One scenario where it's necessary to disable interrupts in the above code is when the pulses array is accessed in the loop function.
When the following code is executed
there is no guarantee that loading the values of the two bytes of the 16 bit integer The same applies to the following code
There is a similar issue with the following code
There may be an interrupt between the first and second line of code that changes the value of |
This should address that? int pulse = pulses[i];
buffer[i * 2] = pulse >> 8;
buffer[i * 2 + 1] = pulse & 0xFF; |
Per your suggestion, I tried using the |
I'm afraid not. The AVR 8-bit instructions set doesn't have an instruction for loading 16 bit values. This forces to the compiler to generate two instructions each of which loads a single byte. Because there are two instructions there can be an interrupt between the execution of the first and second. |
Well, then I guess it's a deal breaker. Can someone tell me why we need to use interrupts here? |
I think because reading pwm values with pulseIn will block everything, with interrupts the microprocessor will be able to work between pwm edges. I will try to figure out a way to make data atomic while i2c buffering. |
Thanks, I know why we might want to use interrupts in general, but that strategy is generally used in scenarios where the entire program, including all user application code is running on the same processor. We're only using this to read a maximum of 8 channels (maybe someone will want to use a 10 or 12 channel unit, but I'll worry about that when the day actually comes), and write the values to the I2C bus... |
Whoops! |
@rwaldron have you tried like this: for (int i = 0; i < channels; i++) {
noInterrupts();
buffer[i * 2] = pulses[i] >> 8;
buffer[i * 2 + 1] = pulses[i] & 0xFF;
interrupts();
} |
I thought I did and I thought I saw it crashing. It's not crashing now. |
I'm signing off for a bit. |
Presently, this is not crashing: #define EI_ARDUINO_INTERRUPTED_PIN
#include <EnableInterrupt.h>
#include <Wire.h>
#define DEBUG_MODE 1
// Address Pins
#define AD0 11
#define AD1 12
// I2C Defaults
#define I2C_DEFAULT_ADDRESS 0x0A
#define I2C_BUFFER_SIZE 16
byte buffer[I2C_BUFFER_SIZE];
int addressPins[] = { AD0, AD1 };
int address = I2C_DEFAULT_ADDRESS;
int first = 2;
int channels = 8;
volatile int pulses[8];
volatile int last_up[8];
void up() {
volatile uint8_t pin = arduinoInterruptedPin;
enableInterrupt(pin, &down, FALLING);
last_up[pin - first] = micros();
}
void down() {
volatile uint8_t pin = arduinoInterruptedPin;
enableInterrupt(pin, &up, RISING);
pulses[pin - first] = micros() - last_up[pin - first];
}
void resetState() {
for (int i = first; i < first + channels; i++) {
pulses[i - first] = 0;
last_up[i - first] = 0;
pinMode(i, INPUT_PULLUP);
enableInterrupt(i, &up, RISING);
}
}
void setup() {
// First check if the address was set via pins 11 or 12
int offset = 0;
for (int i = 0; i < 2; i++) {
pinMode(addressPins[i], INPUT);
if (digitalRead(addressPins[i])) {
offset |= 1 << i;
}
}
address += offset;
resetState();
#if DEBUG_MODE
Serial.begin(9600);
#endif
// Initialize Slave
Wire.begin(address);
Wire.onRequest(onRequest);
Wire.onReceive(onReceive);
}
void loop() {
#if DEBUG_MODE
for (int i = first; i < first + channels - 1; i++) {
Serial.print(pulses[i - first]);
Serial.print(" - ");
}
Serial.println(pulses[channels - 1]);
#endif
for (int i = 0; i < channels; i++) {
noInterrupts();
buffer[i * 2] = pulses[i] >> 8;
buffer[i * 2 + 1] = pulses[i] & 0xFF;
interrupts();
}
}
void onRequest() {
Wire.write(buffer, I2C_BUFFER_SIZE);
}
void onReceive(int count) {
while (Wire.available()) {
// Command 0x01 => Reset.
if (Wire.read() == 0x01) {
Serial.println("RESET");
resetState();
}
}
} |
It's not crashing here too, which node code are you using in tessel 2 to get values from I2C? |
I've been working on a plugin component |
ok I'll wait until tomorrow ;) const five = require('johnny-five');
const board = new five.Board();
board.on('ready', function(){
const opts = {
address: 0x0A
};
this.io.i2cConfig(opts);
this.io.i2cRead(opts.address, 16, function(data) {
console.log(data);
})
}); |
Looks like I was away for a few days and missed some fun discussion. Regarding that Naze cable:
I really only meant that I'll probably have male pins on my backpack so I could use such a cable since you can get something similar for most receivers. Then you don't have to worry about having female headers shaped a specific way to fit a particular receiver onto a backpack. |
@Resseguie got it! |
@henricavalcante @Resseguie here's what I've got so far... https://github.com/rwaldron/j5-rc-receiver |
I'll test asap. btw great work. =) |
Thanks! It's pretty fun :D |
This just landed in configurableFirmata https://github.com/git-developer/RCSwitchFirmata Seemed relevant. |
Hi @Resseguie, I'm working on cleaning up the J5 issues list (it was getting kind of hairy). I've identified a subset of issues that are requests for new classes, devices or features and have added them to an index of requested features. Hopefully it will be a place where motivated contributors can find cool things to work on. This request is listed under the new "Receiver" class. |
I'd like to add a class to support traditional hobby RC receivers to control nodebots. I happen to have a Spektrum radio and compatible receivers, but other popular Tx/Rx (transmitter/receiver - common abbreviations in RC world) combinations should be supported, too.
Details of my exact setup:
DX6 Radio: http://amzn.to/1Se2hjd
AR610 Rx: http://amzn.to/1UQfqVw
(There are Spektrum compatible Tx an Rx made by Orange that are much cheaper.)
It should support individual channels (like the AR610), but also the PPM (Pulse Position Modulation) varieties.
There are a number of examples of using these with Arduino that could get us started.
https://www.sparkfun.com/tutorials/348
http://rcarduino.blogspot.com/2012/01/how-to-read-rc-receiver-with.html
How generic should such a component be?
The text was updated successfully, but these errors were encountered: