1
\$\begingroup\$

This is a toy design analysis utility meant to support decisions around configuring a timer on a microcontroller. The initial configuration is an example meant to partially represent the possible configuration space for timer 0 on a PIC18F66K40.

Features

  • Architecture- and device-agnostic
  • Selection of a target timer expiry duration
  • Selection of the timer's counter bit width
  • Representation of multiple, fixed clock source frequencies
  • Representation of multiple scalers (potentially pre-scalers or post-scalers) showing how their factor product affects the configuration space
  • Scaler series in predefined, linear-range, or binary-exponential-range mode
  • Display of the feasible configuration space region as a projection of the actual bounding polytope
  • Mouse-over information including timer value and accuracy
  • Solution-culling by maximum absolute relative error

Math

In this system, the independent variables are:

  • bit width
  • clock frequency
  • pre-scale and post-scale factor
  • expiry time

The dependent variables are:

  • timer value
  • resolution
  • error

The fundamental equation being modelled is

value * scale = time * freq

under the following constraint:

1 <= value < 2^bits

Relative error is calculated as:

time_actual / time_ideal - 1

Reload integers

The timer reload value is represented as a modified two's-complement integer; with a 16-bit integer as an example:

  • Zero (0x0000) is identical
  • The negative portion (0x8000 through 0xFFFF representing -32768 through -1) is identical
  • The other half - 0x0001 through 0x7FFF - rather than representing positive numbers - represents the negative numbers from -65535 through -32769

This is to facilitate low-fuss, direct entry of an integer that will transparently convert to the correct machine code representation while still being easily understood as a timer value with no positive-negative discontinuity. This transparent conversion has worked in all of the compilers I've tried, most recently XC8; but of course you'll want to test this for yourself.

Review

The implementation is fairly strict in

  • pulling chart.js as its only dependency;
  • having no server dependencies other than the chart.js CDN;
  • having no "compiled bundles" like webpack;
  • (attempting to) use vanilla, modern JavaScript; and
  • avoiding dynamic DOM generation.

These goals are chosen

  • to promote simplicity;
  • for me to catch up on what has changed in JavaScript over the years; and
  • to gain an "untainted" view of what is possible in vanilla before pulling in other libraries and transpilers like TypeScript, which I would prefer if this were for work.

To that end, whereas I'm of course happy to hear any feedback, I'm particularly interested in cleanup of the JavaScript code, assuming the availability of modern, standards-adherent browsers.

Code

(function() {

"use strict";

// initialised on parse
var time, valueMax, availFreq, allScales, cullError,
    scaleFreqGraph, scaleValueGraph, valueFreqGraph;

// these references stay in-place and associated to the graph config;
// their contents are discarded and replaced on update
const
    feasiblePoints = [],
    implPoints = [];

function sortPredicate(a, b) { return a - b; }

function makePoint(freq, scale) {
    const valueIdeal = time * freq / scale,
          valueActual = Math.round(valueIdeal),
          timeActual =  valueActual * scale / freq
    return {
        scale: scale,
        freq: freq,
        valueIdeal: valueIdeal,
        valueActual: valueActual,
        timeActual: timeActual,
        error: timeActual/time - 1
    };
}

function fillFeasiblePoints() {
    feasiblePoints.length = 0;

    const
        freqBottom = Math.min(...availFreq), scaleLeft  = Math.min(...allScales),
        freqTop    = Math.max(...availFreq), scaleRight = Math.max(...allScales);

    const
        freqSW = scaleLeft/time,  freqNW = freqSW * valueMax,
        freqSE = scaleRight/time, freqNE = freqSE * valueMax;

    function fillSymmetric(
        sign,
        freqAcute, freqObtuse,
        freqCloseLimit, freqFarLimit,
        scaleCloseLimit, scaleFarLimit,
        growFactor, shrinkFactor
    ) {
        if (freqAcute > freqFarLimit)
            return false;  // No feasible region

        if (freqAcute < freqCloseLimit) {
            var scaleCorner = freqCloseLimit*time / shrinkFactor;
            if (scaleCorner < scaleCloseLimit) {
                scaleCorner = scaleCloseLimit;
                if (freqObtuse < freqFarLimit) {
                    feasiblePoints.push(
                        // First obtuse corner
                        makePoint(sign * scaleCloseLimit/time * shrinkFactor, sign * scaleCloseLimit)
                    );
                }
            }

            feasiblePoints.push(
                // Square corner
                makePoint(sign * freqCloseLimit, sign * scaleCorner),
                // Second obtuse corner
                makePoint(sign * freqCloseLimit, sign * Math.min(scaleFarLimit, freqCloseLimit*time / growFactor)),
            );
        }
        else {
            if (freqObtuse <= freqFarLimit) {
                feasiblePoints.push(
                    // Corner (obtuse)
                    makePoint(sign * freqObtuse, sign * scaleCloseLimit)
                );
            }
            feasiblePoints.push(
                // Corner (acute)
                makePoint(sign * freqAcute, sign * scaleCloseLimit),
            );
        }

        return true;
    }

    if (!fillSymmetric(
        1,                      // sign
        freqSW, freqNW,         // freqAcute, freqObtuse
        freqBottom, freqTop,    // freqCloseLimit, freqFarLimit
        scaleLeft, scaleRight,  // scaleCloseLimit, scaleFarLimit
        1, valueMax,            // growFactor, shrinkFactor
    )) return;

    if (!fillSymmetric(
        -1,                      // sign
        -freqNE, -freqSE,        // freqAcute, freqObtuse
        -freqTop, -freqBottom,   // freqCloseLimit, freqFarLimit
        -scaleRight, -scaleLeft, // scaleCloseLimit, scaleFarLimit
        valueMax, 1,             // growFactor, shrinkFactor
    )) {
        feasiblePoints.length = 0;
        return;
    }

    // close loop
    feasiblePoints.push(feasiblePoints[0]);
}

function fillImplPoints() {
    implPoints.length = 0;
    availFreq.forEach(
        freq => {
            // We could do a naive filter(), but that doesn't capture the fact that there will be:
            //   0 or more out-of-range,
            //   0 or more in-range, and then
            //   0 or more out-of-range, in that order.
            // The best approach to finding both bounds would be a bisection, but isn't built into JS.
            // Whatever.
            const
                scaleMax = time*freq,
                scaleMin = scaleMax / valueMax,
                start = allScales.findIndex(
                    scale => scale >= scaleMin
                );

            if (start == -1)
                return;
            
            allScales.slice(start).every(
                scale => {
                    if (scale > scaleMax)
                        return false;
                    
                    const point = makePoint(freq, scale);
                    if (Math.abs(point.error) <= cullError)
                        implPoints.push(point);
                    return true;
                }
            );
        }
    );
}

const tooltipCallback = (function() {
    const
        locale = undefined,
        freqFmt = new Intl.NumberFormat(
            locale, {
                notation: 'engineering', style: 'decimal', useGrouping: false,
            }
        ),
        timeFmt = new Intl.NumberFormat(
            locale, {
                notation: 'engineering', style: 'decimal', useGrouping: false,
            }
        ),
        scaleFmt = new Intl.NumberFormat(
            locale, {
                notation: 'standard', style: 'decimal', useGrouping: true,
            }
        ),
        timerFmt = scaleFmt,
        errorFmt = new Intl.NumberFormat(
            locale, {
                notation: 'scientific', style: 'decimal', useGrouping: true,
            }
        );

    return items => items.flatMap(
        item => {
            const raw = item.raw;
            if (item.dataset.label == 'Feasible region')
                return [
                    'Scale limit: ' + scaleFmt.format(raw.scale),
                    'Timer limit: ' + timerFmt.format(raw.valueIdeal),
                    'Freq limit: ' + freqFmt.format(raw.freq),
                ];
            
            return [
                'f = ' + freqFmt.format(raw.freq) + ' Hz',
                'scale = ' + scaleFmt.format(raw.scale),
                'tmr_idl = ' + timerFmt.format(-raw.valueIdeal),
                'tmr_act = ' + timerFmt.format(-raw.valueActual),
                't_idl = ' + timeFmt.format(time) + ' s',
                't_act = ' + timeFmt.format(raw.timeActual) + ' s',
                'rel_err = ' + errorFmt.format(raw.error),
            ];
        }
    );
})();

function updateGraphs() {
    fillFeasiblePoints();
    fillImplPoints();

    scaleFreqGraph.update();
    scaleValueGraph.update();
    valueFreqGraph.update();
}

function attachHandlers() {
    const
        timeInput = document.getElementById('time'),
        bitsInput = document.getElementById('bits'),
        freqInput = document.getElementById('freq'),
        errorInput = document.getElementById('cull_error'),
        specifiedScalerInputs = [
            '1', '2'
        ].map(id => document.getElementById('scale' + id)),
        rangeScaleInputs = [
            '3', '4'
        ].map(
            id => ({
                exp: document.getElementById('scale' + id + '_exp'),
                min: document.getElementById('scale' + id + '_min'),
                max: document.getElementById('scale' + id + '_max')
            })
        );

    function parseTime() {
        time = parseFloat(timeInput.value);
    }
    function parseBits() {
        const bits = parseInt(bitsInput.value);
        valueMax = 2**bits - 1;
    }
    function parseFreq() {
        availFreq = freqInput.value.split(',').map(parseFloat);
    }
    function parseError() {
        cullError = parseFloat(errorInput.value);
    }
    function parseScale() {
        const specified = specifiedScalerInputs.map(
            input => input.value.split(',').map(s => parseInt(s))
        );
        const ranges = rangeScaleInputs.map(
            inputs => {
                const
                    min = parseInt(inputs.min.value),
                    max = parseInt(inputs.max.value);
                const series = [
                    ...Array(max - min).keys()
                ].map(x => x + min);
                if (inputs.exp.checked)
                    return series.map(x => 1 << x);
                return series;
            }
        );
        allScales = [
            ...new Set(
                specified[0].flatMap(s1 =>
                    specified[1].flatMap(s2 =>
                        ranges[0].flatMap(s3 =>
                            ranges[1].map(s4 =>
                                s1 * s2 * s3 * s4
                            )
                        )
                    )
                )
            )
        ];
        allScales.sort(sortPredicate);
    }

    function attachChange(input, parse) {
        input.addEventListener(
            'change',
            () => {
                parse();
                updateGraphs();
            },
        );
        parse(); // first call for initialisation
    }

    attachChange(timeInput, parseTime);
    attachChange(bitsInput, parseBits);
    attachChange(freqInput, parseFreq);
    attachChange(errorInput, parseError);

    function attachScaleChange(input) {
        input.addEventListener(
            'change', () => {parseScale(); updateGraphs()}
        );
    }
    specifiedScalerInputs.forEach(attachScaleChange);
    rangeScaleInputs.forEach(
        rangeScale => Object.values(rangeScale).forEach(attachScaleChange)
    );
    parseScale();
}

function makeGraphs() {
    const dataConfig = {
        datasets: [
            {
                label: 'Feasible region',
                type: 'line',
                borderColor: '#cbeac9',
                data: feasiblePoints,
                order: 2,
            },
            {
                label: 'Implementable',
                type: 'scatter',
                borderColor: '#75a6d1',
                data: implPoints,
                order: 1,
            }
        ]
    };

    const options = {
        responsive: false,
        plugins: {
            tooltip: {
                callbacks: {
                    title: tooltipCallback
                }
            }
        },
        // https://www.chartjs.org/docs/latest/general/performance.html
        animation: false,
        parsing: false,
        spanGaps: true, 
    };

    const scaleFreqConfig = {
        options: {
            ...options,
            scales: {
                x: {
                    title: {
                        display: true,
                        text: 'scale'
                    },
                    type: 'logarithmic'
                },
                y: {
                    title: {
                        display: true,
                        text: 'freq (Hz)'
                    },
                    type: 'logarithmic'
                }
            },
            parsing: {
                xAxisKey: 'scale',
                yAxisKey: 'freq',
            }
        },
        data: dataConfig
    };

    const scaleValueConfig = {
        options: {
            ...options,
            scales: {
                x: {
                    title: {
                        display: true,
                        text: 'scale'
                    },
                    type: 'logarithmic'
                },
                y: {
                    title: {
                        display: true,
                        text: 'timer value (negated)'
                    },
                    type: 'logarithmic'
                }
            },
            parsing: {
                xAxisKey: 'scale',
                yAxisKey: 'valueIdeal',
            }
        },
        data: dataConfig
    };

    const valueFreqConfig = {
        options: {
            ...options,
            scales: {
                x: {
                    title: {
                        display: true,
                        text: 'timer value (negated)'
                    },
                    type: 'logarithmic'
                },
                y: {
                    title: {
                        display: true,
                        text: 'freq (Hz)'
                    },
                    type: 'logarithmic'
                }
            },
            parsing: {
                xAxisKey: 'valueIdeal',
                yAxisKey: 'freq',
            }
        },
        data: dataConfig
    };

    scaleFreqGraph = new Chart(
        document.getElementById('scaleFreqChart').getContext('2d'), 
        scaleFreqConfig);
    scaleValueGraph = new Chart(
        document.getElementById('scaleValueChart').getContext('2d'), 
        scaleValueConfig);
    valueFreqGraph = new Chart(
        document.getElementById('valueFreqChart').getContext('2d'), 
        valueFreqConfig);
}

window.onload = function () {
    makeGraphs();
    attachHandlers();
    updateGraphs();
};

})();
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <title>Embedded Timer Explorer</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>

  <style>
    body {
      font-family: 'Calibri', sans-serif;
    }
    canvas {
      width: 900px;
      height: 390px;
    }
    .parameters {
      padding-left: 100px;
    }
  </style>
</head>

<body>
  <table>
    <tbody>
      <tr>
        <td>
          <canvas id="scaleFreqChart"></canvas>
        </td>
        <td>
          <canvas id="valueFreqChart"></canvas>
        </td>
      </tr>
      <tr>
        <td>
          <canvas id="scaleValueChart"></canvas>
        </td>

        <td class="parameters">
          <h2>Parameters</h2>

          <p>
            <label for="time">Timer expiry, seconds</label><br/>
            <input id="time" type="number" min="0" step="any" value="0.1" />
          </p>
        
          <p>
            <label for="bits">Timer width, bits</label><br/>
            <input id="bits" type="number" min="1" step="1" value="16" list="preferred_bits" />
            <datalist id="preferred_bits">
              <option value="8"></option>
              <option value="16"></option>
              <option value="32"></option>
              <option value="64"></option>
            </datalist>
          </p>
        
          <p>
            <label for="freq">Clock frequencies, Hz, comma-separated</label><br/>
            <input id="freq" type="text" value="31e3, 1e6, 2e6, 4e6, 8e6, 12e6, 16e6, 32e6, 48e6, 64e6" />
          </p>
        
          <p>
            <label for="scale1">Scaler A factors, comma-separated</label><br/>
            <input id="scale1" type="text" value="1" />
          </p>
        
          <p>
            <label for="scale2">Scaler B factors, comma-separated</label><br/>
            <input id="scale2" type="text" value="1" />
          </p>
        
          <div>Range Scalers C, D</div>
          <table>
            <thead>
              <tr>
                <th>2<sup>x</sup></th>
                <th>Min incl.</th>
                <th>Max excl.</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>
                  <input id="scale3_exp" type="checkbox" checked="checked" />
                </td>
                <td>
                  <input id="scale3_min" type="number" min="0" step="1" value="0" />
                </td>
                <td>
                  <input id="scale3_max" type="number" min="0" step="1" value="16" />
                </td>
              </tr>
              <tr>
                <td>
                  <input id="scale4_exp" type="checkbox" />
                </td>
                <td>
                  <input id="scale4_min" type="number" min="0" step="1" value="1" />
                </td>
                <td>
                  <input id="scale4_max" type="number" min="0" step="1" value="17" />
                </td>
              </tr>
            </tbody>
          </table>

          <p>
            <label for="cull_error">Maximum relative error</label><br/>
            <input id="cull_error" type="number" step="any" value="0.01" />
          </p>
        </td>
      </tr>
    </tbody>
  </table>

  <footer>
    <h2>Assumptions</h2>
    <ul>
      <li>The timer is represented as a non-positive integer that only counts up</li>
      <li>Timer expiry occurs on overflow from all-FF to all-00</li>
      <li>There is no latency or overhead in interrupt servicing, timer reconfiguration, etc.</li>
    </ul>
  </footer>
</body>
</html>

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.