Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
Plunger/rotarySensor.h
- Committer:
- arnoz
- Date:
- 2021-10-01
- Revision:
- 116:7a67265d7c19
- Parent:
- 106:e9e3b46132c1
File content as of revision 116:7a67265d7c19:
// Plunger sensor implementation for rotary absolute encoders // // This implements the plunger interfaces for rotary absolute encoders. A // rotary encoder measures the angle of a rotating shaft. An absolute encoder // is one where the microcontroller can ask the sensor for its current angular // position at any time. (As opposed to incremental encoders, which don't have // any notion of their current position, but can only signal the host on each // change in position.) // // // For plunger sensing, we can convert the plunger's linear motion into angular // motion using a mechanical link between the plunger rod and a rotating shaft // positioned at a fixed point, somewhere nearby, but away from the plunger's // axis of motion: // // =X=======================|=== <- plunger, X = connector attachment point // \ // \ <- connector between plunger and shaft // \ // * <- rotating shaft, at a fixed position // // As the plunger moves, the angle of the connector relative to the fixed // shaft position changes in a predictable way, so we can infer the plunger's // linear position at any given time by measuring the current rotational // angle of the shaft. // // The mechanical diagram above is, obviously, simplified for ASCII art's sake. // What's not shown is that the distance between the rotating shaft and the // "X" connection point on the plunger varies as the plunger moves, so the // mechanical linkage requires some way to accommodate that changing length. // If the connector is a rigid rod, it has to be able to slide at one or // the other connection points. Alternatively, rather than using a rigid // linkage, we can use a spring or elastic band. We leave these details up // to the mechanical design, since the software isn't affected by that, as // long as the basic relationship between linear and angular motion as shown // in the diagram is achieved. // // // Translating the angle to a linear position // // There are two complications to translating the angular reading back to // a linear plunger position. // // 1. We have to consider the sensor's zero point to be arbitrary, because // these sorts of sensors don't typically give the user a way to align the // zero point at a desired physical position. The zero point will just be // wherever it ends up after installation. The zero point could easily end // up being somewhere in the middle of the plunger's travel range, which // means that readings might "wrap" - e.g., we might see a series of readings // when the plunger is moving in one direction like this: 4050, 4070, 4090, // 14, 34 (note how we "wrapped" past some maximum angle reading for the // sensor and went back to zero, then continued from there). // // To deal with this, we have to make a couple of assumptions: // // - The park position is at about 1/6 of the overall travel range // - The total angular travel range is less than one full revolution // // With those assumptions in hand, we can bias the raw readings to the // park position, and then take them modulo the raw scale. That will // ensure that readings wrap properly, regardless of where the raw zero // point lies. // // 2. Going back to the original diagram, you can see that there's some // trigonometry required to interpret the sensor's angular reading as a // linear position on the plunger axis, which is of course what we need // to report to the PC software. // // Let's use the vertical line between the plunger and the rotation point // as the zero-degree reference point. To figure the plunger position, // we need to figure the difference between the raw angle reading and the // zero-degree point; call this theta. Let L be the position of the plunger // relative to the vertical reference point, let D be the length of the // vertical reference point line, and let H by the distance from the rotation // point to the plunger connection point. This is a right triangle with // hypotenuse H and sides L and D. D is a constant, because the rotation // point never moves, and the plunger never moves vertically. Thus we can // calculate D = H*cos(theta) and L = H*sin(theta). D is a constant, so // we can figure H = D/cos(theta) hence L = D*sin(theta)/cos(theta) or // D*tan(theta). If we wanted to know the true position in real-world // units, we'd have to know D, but only need arbitrary linear units, so // we can choose whatever value for D we find convenient: in particular, // a value that gives us the desired range and resolution for the final // result. // // Note that the tangent diverges at +/-90 degrees, but that's okay, // because the mechanical setup we've described is inherently constrained // to stay well within those limits. This would even be true for an // arbitrarily long range of motion along the travel axis, but we don't // even have to worry about that since we have such a well-defined range // of travel (of only about 3") to track. // // There's still one big piece missing here: we somehow have to know where // that vertical zero point lies. That's something we can only learn by // calibration. Unfortunately, we don't have a good way to detect this // directly. We *could* ask the user to look inside the cabinet and press // a button when the needle is straight up, but that seems too cumbersome // for the user, not to mention terribly imprecise. So we'll approach this // from the other direction: we'll assume a particular placement of the // rotation point relative to the travel range, and we'll provide // installation instructions to achieve that assumed alignment. // // The full range we actually have after calibration consists of the park // position and the maximum retracted position. We could in principle also // calibrate the maximum forward position, but that can't be read as reliably // as the other two, because the barrel spring makes it difficult for the // user to be sure they've pushed it all the way forward. Since we can // extract the information we need from the park and max retract positions, // it's better to rely on those alone and not ask for information that the // user can't as easily provide. Given these positions, AND the assumption // that the rotation point is at the midpoint of the plunger travel range, // we can do some grungy trig work to come up with a formula for the angle // between the park position and the vertical: // // let C1 = 1 1/32" (distance from midpoint to park), // C2 = 1 17/32" (distance from midpoint to max retract), // C = C2/C1 = 1.48484849, // alpha = angle from park to vertical, // beta = angle from max retract to vertical // theta = alpha + beta = angle from park to max retract, known from calibration, // T = tan(theta); // // then // alpha = atan(sqrt(4*T*T*C + C^2 + 2*C + 1) - C - 1)/(2*T*C)) // // Did I mention this was grungy? At any rate, everything going into that // last equation is either constant or known from the calibration, so we // can pre-compute alpha and store it after each calibration operation. // And once we've computed alpha, we can easily translate an angle reading // from the sensor to an angle relative to the vertical, which we can plug // into D*tan(angle) to convert to a linear position on the plunger axis. // // The final step is to scale that linear position into joystick reporting // units. Those units are arbitrary, so we don't have to relate this to any // real-world lengths. We can simply figure a scaling factor that maps the // physical range to map to roughly the full range of the joystick units. // // If you're wondering how we derived that ugly formula, read on. Start // with the basic relationships D*tan(alpha) = C1 and D*tan(beta) = C2. // This lets us write tan(beta) in terms of tan(alpha) as // C2/C1*tan(alpha) = C*tan(alpha). We can combine this with an identity // for the tan of a sum of angles: // // tan(alpha + beta) = (tan(alpha) + tan(beta))/(1 - tan(alpha)*tan(beta)) // // to obtain: // // tan(theta) = tan(alpha + beta) = (1 + C*tan(alpha))/(1 - C*tan^2(alpha)) // // Everything here except alpha is known, so we now have a quadratic equation // for tan(alpha). We can solve that by cranking through the normal algorithm // for solving a quadratic equation, arriving at the solution above. // // // Choosing an install position // // There are two competing factors in choosing the optimal "D". On the one // hand, you'd like D to be as large as possible, to maximum linearity of the // tan function used to translate angle to linear position. Higher linearity // gives us greater immunity to variations in the precise centering of the // rotation axis in the plunger travel range. tan() is pretty linear (that // is, tan(theta) is approximately proportional to theta) for small theta, // within about +/- 30 degrees. On the other hand, you'd like D to be as // small as possible so that we get the largest overall angle range. Our // sensor has a fixed angular resolution, so the more of the overall circle // we use, the more sensor increments we have over the range, and thus the // better effective linear resolution. // // Let's do some calculations for various "D" values (vertical distance // between rotation point and plunger rod). We'll base our calculations // on the AEAT-6012 sensor's 12-bit angular resolution. // // D theta(max) eff dpi theta(park) // ----------------------------------------------- // 1 17/32" 45 deg 341 34 deg // 2" 37 deg 280 27 deg // 2 21/32" 30 deg 228 21 deg // 3 1/4" 25 deg 190 17 deg // 4 3/16" 20 deg 152 14 deg // // I'd consider 50 dpi to be the minimum for acceptable performance, 100 dpi // to be excellent, and anything above 300 dpi to be diminishing returns. So // for a 12-bit sensor, 2" looks like the sweet spot. It doesn't take us far // outside of the +/-30 deg zone of tan() linearity, and it achieves almost // 300 dpi of effective linear resolution. I'd stop there are not try to // push the angular resolution higher with a shorter D; with the 45 deg // theta(max) at D = 1-17/32", we'd get a lovely DPI level of 341, but at // the cost of getting pretty non-linear around the ends of the plunger // travel. Our math corrects for the non-linearity, but the more of that // correction we need, the more sensitive the whole contraption becomes to // getting the sensor positioning exactly right. The closer we can stay to // the linear approximation, the more tolerant we are of inexact sensor // positioning. // // // Supported sensors // // * AEAT-6012-A06. This is a magnetic absolute encoder with 12-bit // resolution. It linearly encodes one full (360 degree) rotation in // 4096 increments, so each increment represents 360/4096 = .088 degrees. // // The base class doesn't actually care much about the sensor type; all it // needs from the sensor is an angle reading represented on an arbitrary // linear scale. ("Linear" in the angle, so that one increment represents // a fixed number of degrees of arc. The full scale can represent one full // turn but doesn't have to, as long as the scale is linear over the range // covered.) To add new sensor types, you just need to add the code to // interface to the physical sensor and return its reading on an arbitrary // linear scale. #ifndef _ROTARYSENSOR_H_ #define _ROTARYSENSOR_H_ #include "FastInterruptIn.h" #include "AEAT6012.h" // The conversion from raw sensor reading to linear position involves a // bunch of translations to different scales and unit systems. To help // keep things straight, let's give each scale a name: // // * "Raw" refers to the readings directly from the sensor. These are // unsigned ints in the range 0..maxRawAngle, and represent angles in a // unit system where one increment equals 360/maxRawAngle degrees. The // zero point is arbitrary, determined by the physical orientation // of the sensor. // // * "Biased" refers to angular units with a zero point equal to the // park position. This uses the same unit size as the "raw" system, but // the zero point is adjusted so that 0 always means the park position. // Negative values are forward of the park position. This scale is // also adjusted for wrapping, by ensuring that the value lies in the // range -(maximum forward excursion) to +(scale max - max fwd excursion). // Any values below or above the range are bumped up or down (respectively) // to wrap them back into the range. // // * "Linear" refers to the final linear results, in joystick units, on // the abstract integer scale from 0..65535 used by the generic plunger // base class. // class PlungerSensorRotary: public PlungerSensor { public: PlungerSensorRotary(int maxRawAngle, float radiansPerSensorUnit) : PlungerSensor(65535), maxRawAngle(maxRawAngle), radiansPerSensorUnit(radiansPerSensorUnit) { // start our sample timer with an arbitrary zero point of now timer.start(); // clear the timing statistics nReads = 0; totalReadTime = 0; // Pre-calculate the maximum forward excursion distance, in raw // units. For our reference mechanical setup with "D" in a likely // range, theta(max) is always about 10 degrees higher than // theta(park). 10 degrees is about 1/36 of the overall circle, // which is the same as 1/36 of the sensor scale. To be // conservative, allow for about 3X that, so allow 1/12 of scale // as the maximum forward excursion. For wrapping purposes, we'll // consider any reading outside of the range from -(excursion) // to +(maxRawAngle - excursion) to be wrapped. maxForwardExcursionRaw = maxRawAngle/12; // reset the calibration counters biasedMinObserved = biasedMaxObserved = 0; } // Restore the saved calibration at startup virtual void restoreCalibration(Config &cfg) { // only proceed if there's calibration data to retrieve if (cfg.plunger.cal.calibrated) { // we store the raw park angle in raw0 rawParkAngle = cfg.plunger.cal.raw0; // we store biased max angle in raw1 biasedMax = cfg.plunger.cal.raw1; } else { // Use the current sensor reading as the initial guess at the // park position. The system is usually powered up with the // plunger at the neutral position, so this is a good guess in // most cases. If the plunger has been calibrated, we'll restore // the better guess when we restore the configuration later on in // the initialization process. rawParkAngle = 0; readSensor(rawParkAngle); // Set an initial wild guess at a range equal to +/-35 degrees. // Note that this is in the "biased" coordinate system - raw // units, but relative to the park angle. The park angle is // about -25 degrees in this setup. biasedMax = (35 + 25) * maxRawAngle/360; } // recalculate the vertical angle updateAlpha(); } // Begin calibration virtual void beginCalibration(Config &) { // Calibration starts out with the plunger at the park position, so // we can take the current sensor reading to be the park position. rawParkAngle = 0; readSensor(rawParkAngle); // Reset the observed calibration counters biasedMinObserved = biasedMaxObserved = 0; } // End calibration virtual void endCalibration(Config &cfg) { // apply the observed maximum angle biasedMax = biasedMaxObserved; // recalculate the vertical angle updateAlpha(); // save our raw configuration data cfg.plunger.cal.raw0 = static_cast<uint16_t>(rawParkAngle); cfg.plunger.cal.raw1 = static_cast<uint16_t>(biasedMax); // Refigure the range for the generic code cfg.plunger.cal.min = biasedAngleToLinear(biasedMinObserved); cfg.plunger.cal.max = biasedAngleToLinear(biasedMaxObserved); cfg.plunger.cal.zero = biasedAngleToLinear(0); } // figure the average scan time in microseconds virtual uint32_t getAvgScanTime() { return nReads == 0 ? 0 : static_cast<uint32_t>(totalReadTime / nReads); } // read the sensor virtual bool readRaw(PlungerReading &r) { // note the starting time for the reading uint32_t t0 = timer.read_us(); // read the angular position int angle; if (!readSensor(angle)) return false; // Refigure the angle relative to the raw park position. This // is the "biased" angle. angle -= rawParkAngle; // Adjust for wrapping. // // An angular sensor reports the position on a circular scale, for // obvious reasons, so there's some point along the circle where the // angle is zero. One tick before that point reads as the maximum // angle on the scale, so we say that the scale "wraps" at that point. // // To correct for this, we can look to the layout of the mechanical // setup to constrain the values. Consider anything below the maximum // forward exclusion to be wrapped on the low side, and consider // anything outside of the complementary range on the high side to // be wrapped on the high side. if (angle < -maxForwardExcursionRaw) angle += maxRawAngle; else if (angle >= maxRawAngle - maxForwardExcursionRaw) angle -= maxRawAngle; // Note if this is the highest/lowest observed reading on the biased // scale since the last calibration started. if (angle > biasedMaxObserved) biasedMaxObserved = angle; if (angle < biasedMinObserved) biasedMinObserved = angle; // figure the linear result r.pos = biasedAngleToLinear(angle); // Set the timestamp on the reading to right now uint32_t now = timer.read_us(); r.t = now; // count the read statistics totalReadTime += now - t0; nReads += 1; // success return true; } private: // Read the underlying sensor - implemented by the hardware-specific // subclasses. Returns true on success, false if the sensor can't // be read. The angle is returned in raw sensor units. virtual bool readSensor(int &angle) = 0; // Convert a biased angle value to a linear reading int biasedAngleToLinear(int angle) { // Translate to an angle relative to the vertical, in sensor units float theta = static_cast<float>(angle)*radiansPerSensorUnit - alpha; // Calculate the linear position relative to the vertical. Zero // is right at the intersection of the vertical line from the // sensor rotation center to the plunger axis; positive numbers // are behind the vertical (more retracted). int linearPos = static_cast<int>(tanf(theta) * linearScaleFactor); // Finally, figure the offset. The vertical is the halfway point // of the plunger motion, so we want to put it at half of the raw // scale of 0..65535. return linearPos + 32767; } // Update the estimation of the vertical angle, based on the angle // between the park position and maximum retraction point. void updateAlpha() { // See the comments at the top of the file for details on this // formula. This figures the angle between the park position // and the vertical by applying the known constraints of the // mechanical setup: the known length of a standard plunger, // and the requirement that the rotation axis be placed at // roughly the midpoint of the plunger travel. const float C = 1.4848489f; // 1-17/32" / 1-1/32" float maxInRadians = static_cast<float>(biasedMax) * radiansPerSensorUnit; float T = tanf(maxInRadians); alpha = atanf((sqrtf(4*T*T*C + C*C + 2*C + 1) - C - 1)/(2*T*C)); // While we're at it, figure the linear conversion factor. Alpha // represents the angle from the park position to the midpoint, // which in the real world represents about 31/32", or just less // then 1/3 of the overall travel. We want to normalize this to // the corresponding fraction of our 0..65535 abstract linear unit // system. To avoid overflow, normalize to a slightly smaller // scale. const float safeMax = 60000.0f; const float alphaInLinearUnits = safeMax * .316327f; // 31/22" / 3-1/16" linearScaleFactor = static_cast<int>(alphaInLinearUnits / tanf(alpha)); } // Maximum raw angular reading from the sensor. The sensor's readings // will always be on a scale from 0..maxRawAngle. int maxRawAngle; // Radians per sensor unit. This is a constant for the sensor. float radiansPerSensorUnit; // Pre-calculated value of the maximum forward excursion, in raw units. int maxForwardExcursionRaw; // Raw reading at the park position. We use this to handle "wrapping", // if the sensor's raw zero reading position is within the plunger travel // range. All readings are taken to be within int rawParkAngle; // Biased maximum angle. This is the angle at the maximum retracted // position, in biased units (sensor units, relative to the park angle). int biasedMax; // Mininum and maximum angle observed since last calibration start, on // the biased scale int biasedMinObserved; int biasedMaxObserved; // The "alpha" angle - the angle between the park position and the // vertical line between the rotation axis and the plunger. This is // represented in radians. float alpha; // The linear scaling factor, applied in our trig calculation from // angle to linear position. This corresponds to the distance from // the rotation center to the plunger rod, but since the linear result // is in abstract joystick units, this distance is likewise in abstract // units. The value isn't chosen to correspond to any real-world // distance units, but rather to yield a joystick result that takes // advantage of most of the available axis range, to minimize rounding // errors when converting between scales. float linearScaleFactor; // timer for input timestamps and read timing measurements Timer timer; // read timing statistics uint64_t totalReadTime; uint64_t nReads; // Keep track of when calibration is in progress. The calibration // procedure is usually handled by the generic main loop code, but // in this case, we have to keep track of some of the raw sensor // data during calibration for our own internal purposes. bool calibrating; }; // Specialization for the AEAT-601X sensors template<int nDataBits> class PlungerSensorAEAT601X : public PlungerSensorRotary { public: PlungerSensorAEAT601X(PinName csPin, PinName clkPin, PinName doPin) : PlungerSensorRotary((1 << nDataBits) - 1, 6.283185f/((1 << nDataBits) - 1)), aeat(csPin, clkPin, doPin) { // Make sure the sensor has had time to finish initializing. // Power-up time (tCF) from the data sheet is 20ms for the 12-bit // version, 50ms for the 10-bit version. wait_ms(nDataBits == 12 ? 20 : nDataBits == 10 ? 50 : 50); } // read the angle virtual bool readSensor(int &angle) { angle = aeat.readAngle(); return true; } protected: // physical sensor interface AEAT601X<nDataBits> aeat; }; #endif