AlphaDecay Posted June 22, 2024 Posted June 22, 2024 (edited) I've made a Vertical Speed Indicator for my F-18 cockpit and the issue I'm having is that the dial is non-linear, where the DCS BIOS values of 0 to 65535 aren't just 1-to-1 across the dial (smaller steps in the -1000 to 1000 ft/min range). Does anyone know how the mapping should be done to account for this? Below is code that should work if the tick-marks on the indicator were a constant distance (excuse my long comments in it as its my first mechanical dial I'm making). void onVsiChange(unsigned int newValue) { /* Try and use the map function to translate the game values (0-65535) to the stepper position (0-4096). This approach requires the zero of the dial to match what it is in the game, so 0 -> -6000 ft/min on the dial. The maximum isn't a full circle, instead it is 340 degrees. That means the maximum position for the stepper is 227.556 or 228 steps less at 3868. */ unsigned int adjValue = 0; // this will hold the translated value for the stepper /* The map function in arduino takes one range of numbers and maps it to a new range. This allows us to translate the game values to stepper values. This mapping is linear. */ adjValue = map(change, 0, 65535, 0, 3868); verticalSpeedStepper.moveTo(adjValue); // send the stepper to the new location verticalSpeedStepper.setSpeed(MAX_SPEED); // move uses acceleration, to be constant and max speed we force it here } DcsBios::IntegerBuffer vsiBuffer(FA_18C_hornet_VSI, onVsiChange); Edited June 22, 2024 by AlphaDecay
Vinc_Vega Posted June 22, 2024 Posted June 22, 2024 (edited) @AlphaDecay If you are familiar with Arduino coding, you could use Rob Tillaart's MultiMap library for non linear mapping problems. You just need two arrays of data (input and output). They have to have the same size to get mapped. I suggest to multimap the VSI integer to the scale value of the gauge (motor steps). Regards, Vinc Edited June 22, 2024 by Vinc_Vega Link edited 1 Regards, Vinc real life: Royal Bavarian Airforce online: VJS-GermanKnights.de [sIGPIC][/sIGPIC]
AlphaDecay Posted June 22, 2024 Author Posted June 22, 2024 That was the approach @Vinc_Vega!! I've included my code below for anyone else trying to figure it out. #define DCSBIOS_IRQ_SERIAL #include "DcsBios.h" /* Need to map game values (0-65535) to stepper motor positions (0-4096), so map() for arduino doesn't work as some dials have non-linear regions like the F-18 vertical speed indicator and airspeed indicator. MultiMap tries to solve this problem with piece-wise linear interpolation provided a series of data points. */ #include "MultiMap.h" /* Need to create equally sized arrays representing the reference points. For this the datatype needs to be the same (integer in this case). inputArray[] *must have increasing values, this comes from the game 0-65535 *does not have to be equal distance between values outputArray[] *multiMap will constrain to the first/last values of this array which is good as that acts as physical stops for the stepper and won't try and send it past the ends of the physical dial (which would have physical limit stops). *The output array requires that you go into the game and place the plane into those specific values (e.g. 1500 ft/min climb) and then look at DCS Bort to see what value it is providing in the range it gives. *As the collected values will/should correspond to physical locations on the instrument, now you get the angle position for each of those collected values as measured on the Fusion design for the instrument and then convert each of those positions to a number of steps for your stepper motor. These steps are what go into the corresponding locations for MultiMap. E.g. in game at a -3500 ft/min descent, DCS Bort says 7063, in Fusion that is 57.5 degrees from the -6000 mark, and my stepper needs 654 steps to go that angle. Default multiMap() does an in-order search from beginning to end and then interpolates between values. So that means larger input values take longer to "find" for a long list. At about 10 entries it is parity to do a binary search, so that is what I use here since an instrument like the Vertical Speed Indicator has 41 entries. The search appears to work better with floats, will round to an int before use. */ float vsiInputArray[41] = { 0,1172,2535,4016,5460,7063,8712,10774,12846,16277,19707,20640,22036,23322,24114,25547,27025,28455,29720,31381,32767, 34153,35814,37079,38509,39987,41420,42212,43498,44894,45827,49257,52688,54760,56822,58471,60074,61518,62999,64362,65535 }; float vsiOutputArray[41] = { 0,85,228,370,512,654,796,939,1081,1223,1365,1422,1479,1536,1593,1650,1707,1764,1820,1877,1934, 1991,2048,2105,2162,2219,2276,2332,2389,2446,2503,2645,2788,2930,3072,3214,3356,3499,3641,3783,3868 }; float vsiNewPosition = 0; // this will hold the translated value for the stepper, it is zeroed with stepper to 0 /* Normally would include Stepper.h but the default stepper approach only does one stepper at a time, doesn't support acceleration, and when stepping a motor it blocks other things from happening. So we will use a better library. */ #include <AccelStepper.h> /* Running in constant speed mode, so we set the max and current speed to the same value. These values were experimentally determined. Any faster and it seems to jitter. We have a 4 wire motor, so the interface type is 4. We can run in half speed by setting it to 8 apparently. */ #define MAX_SPEED 850 #define MAX_ACCEL 850 #define MotorInterfaceType 8 /* Initialize the pins, just change the values to how it's connected myStepper(MotorInterfaceType, Blue, Yellow, Pink, Orange); Blue is IN1 Pink is IN2 Yellow is IN3 Orange is IN4 */ AccelStepper verticalSpeedStepper(MotorInterfaceType, 45, 49, 47, 51); // setup the photo-interrupter to determine where zero will be on the dial const int verticalPhotoInterrupter = 53; // pin number /* Next we setup some variables and constants for doing a zero and for moving the stepper to various locations. */ #define VERTICAL_ZERO_OFFSET 0 // this allows fine-tuning to get zero non-mechanically bool isVerticalZeroed = 0; // have to give stepper time to zero and do process only once void onVsiChange(unsigned int newValue) { // Use multiMapBS to convert the game value to a stepper position vsiNewPosition = multiMapBS<float>(newValue, vsiInputArray, vsiOutputArray, 41); int vsiMovePosition = round(vsiNewPosition); // multiMap was done as a float round it to int int vsiCurrPosition = verticalSpeedStepper.currentPosition(); // get where the stepper is now int relativeMove = vsiMovePosition - vsiCurrPosition; // how far to move, final - initial as CW is positive CCW is negative verticalSpeedStepper.move(relativeMove); // send the stepper to the new location via a relative move verticalSpeedStepper.setSpeed(MAX_SPEED); // move uses acceleration, to be constant and max speed we force it here } DcsBios::IntegerBuffer vsiBuffer(FA_18C_hornet_VSI, onVsiChange); void setup() { // Get DCS BIOS Setup DcsBios::setup(); // set the speed at which the stepper is to run verticalSpeedStepper.setMaxSpeed(MAX_SPEED); verticalSpeedStepper.setSpeed(MAX_SPEED); verticalSpeedStepper.setAcceleration(MAX_ACCEL); // check if in the photointerrupter, then move to the right edge while(!isVerticalZeroed) { // currently stopped in the photo-interrupter module, let's move out to get a good zero if (digitalRead(verticalPhotoInterrupter) == HIGH && verticalSpeedStepper.distanceToGo() == 0) { verticalSpeedStepper.move(80); verticalSpeedStepper.setSpeed(MAX_SPEED); verticalSpeedStepper.runSpeedToPosition(); }else { verticalSpeedStepper.runSpeedToPosition(); } // currently stopped but not in the module if (digitalRead(verticalPhotoInterrupter) == LOW && verticalSpeedStepper.distanceToGo() == 0) { while(digitalRead(verticalPhotoInterrupter) == LOW) { verticalSpeedStepper.setSpeed(MAX_SPEED); verticalSpeedStepper.runSpeed(); } verticalSpeedStepper.setCurrentPosition(0); isVerticalZeroed = 1; } } //give the steppers time to get to their zero position delay(5000); } void loop() { // Run the DCS BIOS Loop DcsBios::loop(); verticalSpeedStepper.runSpeedToPosition(); }
Vinc_Vega Posted June 22, 2024 Posted June 22, 2024 (edited) @AlphaDecay Congratulations, well done! Nevertheless, I want to give some hints to consider for better performance of stepper motor gauges. As the stepper has physical end stops, why the need for a photoresistor to find the zero position? It is used once after power up and than never again. It's not an Altimeter with endless moving needles. A simple way is to run the stepper against the lower or the high end stop. Tell the stepper its position and than run to your physical gauge zero marking. You have to find the right step amount by experimenting. Tell the stepper the new zero position or use offsets. Ensure that the total way of the needle than never reaches behind the end stops. That's also the manufacturer's recommendation for calibration. For that procedure you may use a lower speed as in the main sketch. So after calibration a new max speed may be set. You don't need to set the speed everytime the poisition changes in the "void onVsiChange(...)" part, it already should be defined in the Setup section and than never changes. To effectively reduce calculation efforts and maybe thereby introduced jitter, I recommend a lower resolution of the mapping. Don't use more than 10 to 15 values per array. That will speed up your calculations. All values (newValue and steps to go) are not higher than 65535, so integer data types would reduce memory and calculation efforts. Especially the arrays reserve much more memory when using floats. Anyway, you don't need floats to produce the stepper counts. Mapping even should produce rounded values (integers). I hope that further reduces the mentioned jitters. Regards, Vinc Edited June 22, 2024 by Vinc_Vega error corrections Regards, Vinc real life: Royal Bavarian Airforce online: VJS-GermanKnights.de [sIGPIC][/sIGPIC]
AlphaDecay Posted June 22, 2024 Author Posted June 22, 2024 Thanks again for the input. The steppers I have do full rotations - I teach aerospace engineering and had some laying around from some robotics projects. The downside is needing the photogate to zero it. So far in testing it seems to just work and doesn't take much time really to accomplish the task. On the max speed thing - the documentation on AccelStepper says to call a setSpeed after calling moveTo for constant speed motion or it will use acceleration. While I had concerns about abruptly changing direction (basically running it at max speed all of the time) - it seems to work so far in limited testing. I haven't done any longer play with it though so I don't know if there are errors that will accumulate. I'll also play with the number of elements in the array if it seems to get laggy as I add devices - it just seemed to make sense at the time to put an element for each known marked position on the dial to improve accuracy at least for those landmarks. That arduino should only be running 3 steppers (each with own power) and some LEDs. I'm doing all of the buttons and switches with a Bodnar card so I can maximize cross-game use of those.
Vinc_Vega Posted June 22, 2024 Posted June 22, 2024 (edited) Ah, okay. We usually use these cheap VID type automotive instrument steppers, including end stops, for direct connection to Arduinos. They don't pull much power and are good parts for Home Cockpit building Be careful if connecting other type of steppers without the use of additional driver boards. Arduino pins output 5V and are limited to max 50mA. But I think you already have that in mind for your 3 steppers project. If you want to display accurat values, than 41 element arrays make sense. But it will cost a lot of calculation power compared to only 10 or 15 elements. I don't believe that you can see a remarkable difference if you'd reduce the mapping resolution. If you want to drive more than one stepper from a single Arduino at the same time, there is a specific way to do so with the AccelStepper library. If you like, I'll than could post some code snippets here. I did that for two steppers at the same time. I think that driving 3 steppers may be possible. Regards, Vinc Edited June 22, 2024 by Vinc_Vega Regards, Vinc real life: Royal Bavarian Airforce online: VJS-GermanKnights.de [sIGPIC][/sIGPIC]
AlphaDecay Posted June 22, 2024 Author Posted June 22, 2024 Attached a video of it working. If anything, the stepper is on the slow side and while it lags under large deflections (end of video), even after hitting the limit stops and stuff it doesn't seem to lose accuracy. When flying that crazy one isn't looking at their VSI anyway. 20240622_143838.mp4 44 minutes ago, Vinc_Vega said: Ah, okay. We usually use these cheap VID type automotive instrument steppers, including end stops, for direct connection to Arduinos. They don't pull much power and are good parts for Home Cockpit building Be careful if connecting other type of steppers without the use of additional driver boards. Arduino pins output 5V and are limited to max 50mA. But I think you already have that in mind for your 3 steppers project. If you want to display accurat values, than 41 element arrays make sense. But it will cost a lot of calculation power compared to only 10 or 15 elements. I don't believe that you can see a remarkable difference if you'd reduce the mapping resolution. If you want to drive more than one stepper from a single Arduino at the same time, there is a specific way to do so with the AccelStepper library. If you like, I'll than could post some code snippets here. I did that for two steppers at the same time. I think that driving 3 steppers may be possible. Regards, Vinc Yup for sure.. Here's a picture of the assembly with the driver board (green) and the supplementary power at the back. Always happy to see code as it exposes new ideas to me as I'm not formally trained or anything. 1
No1sonuk Posted June 22, 2024 Posted June 22, 2024 You might find this of use. -- Vertical Speed Indicator AVU-53/A Variometer = CreateGauge() Variometer.arg_number = 225 Variometer.input = {-6000.0, -4000.0, -3000.0, -2000.0, -1000.0, -500.0, 0.0, 500.0, 1000.0, 2000.0, 3000.0, 4000.0, 6000.0} Variometer.output = { -1.0, -0.83, -0.73, -0.605, -0.40, -0.22, 0.0, 0.22, 0.40, 0.605, 0.73, 0.83, 1.0} Variometer.controller = controllers.Variometer This is how the game maps the VSI values to the cockpit model needle positions. Output -1 is needle fully anticlockwise, +1 is fully clockwise. Just 13 mapping points. This information, along with other non-linear gauge mapping info, comes from the "mainpanel_init.lua" file in the module's "cockpit/scripts" folder.
AlphaDecay Posted June 23, 2024 Author Posted June 23, 2024 (edited) 19 hours ago, No1sonuk said: You might find this of use. -- Vertical Speed Indicator AVU-53/A Variometer = CreateGauge() Variometer.arg_number = 225 Variometer.input = {-6000.0, -4000.0, -3000.0, -2000.0, -1000.0, -500.0, 0.0, 500.0, 1000.0, 2000.0, 3000.0, 4000.0, 6000.0} Variometer.output = { -1.0, -0.83, -0.73, -0.605, -0.40, -0.22, 0.0, 0.22, 0.40, 0.605, 0.73, 0.83, 1.0} Variometer.controller = controllers.Variometer This is how the game maps the VSI values to the cockpit model needle positions. Output -1 is needle fully anticlockwise, +1 is fully clockwise. Just 13 mapping points. This information, along with other non-linear gauge mapping info, comes from the "mainpanel_init.lua" file in the module's "cockpit/scripts" folder. I'll give that a try, thanks for the info. Better than trying to reverse engineer the values. EDIT: It's the next day - worked great on the airspeed indicator. I'll update my vertical speed once I can get around to it. I need to put some thought into the issue that DCS BIOS seems to output 0 to 65535, while the game uses the indicator as zero going up and down from there. Currently I just have the needle match DCS BIOS and start at -6000 ft/min and work from there as the 0. Edited June 23, 2024 by AlphaDecay
Recommended Posts