{"title":"Doug's Blog","link":[{"@attributes":{"rel":"self","type":"application\/atom+xml","href":"https:\/\/doug.lon.dev\/atom.xml"}},{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev"}}],"generator":"Zola","updated":"2024-03-05T00:00:00+00:00","id":"https:\/\/doug.lon.dev\/atom.xml","entry":[{"title":"Fourays: Updating Firmware & Debating Hardware Design","published":"2024-03-05T00:00:00+00:00","updated":"2024-03-05T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/fourays\/firmware-etc\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/fourays\/firmware-etc\/","content":"<h2 id=\"firmware\">Firmware<\/h2>\n<p>I've done quite a bit more work on the firmware now, and I believe it to be mostly functionally complete for producing music as I now have complete control over the AY chips. There is still some more work to do on the configuration management and screen user interface though.<\/p>\n<h3 id=\"ay-control\">AY Control<\/h3>\n<p>Up to this point not all of the AY's features have been made available via config or MIDI control. We had config control over the noise mixer, but to change this during performance using config is impractical, some other means would be better.<\/p>\n<p>A common method to enable or disable synth features during a performance is to use MIDI key switches. These switches are normally mapped to the lowest MIDI notes, which are out of range of the instrument. Using this method, we can implement the remaining features of the AY chips in a way which can be controlled during performance. In this instance I'll use a Note On message with velocity &gt; 63 to turn a feature on and a Note On message with a velocity &lt; 64 to turn a feature off.<\/p>\n<h4 id=\"envelope-generator\">Envelope Generator<\/h4>\n<p>The AY's envelope generator has 3 mix switches, 4 mode switches and a period parameter. When enabled for an oscillator, that oscillator's level will be affected by the envelope generator according to its mode and period.<\/p>\n<p>All 7 switches are mapped to MIDI key switches:<\/p>\n<table><thead><tr><th><strong>MIDI Note Number<\/strong><\/th><th><strong>MIDI Note Name<\/strong><\/th><th><strong>Switch<\/strong><\/th><\/tr><\/thead><tbody>\n<tr><td>1<\/td><td>C#-1<\/td><td>Enable for oscillator A<\/td><\/tr>\n<tr><td>2<\/td><td>D-1<\/td><td>Enable for oscillator B<\/td><\/tr>\n<tr><td>3<\/td><td>D#-1<\/td><td>Enable for oscillator C<\/td><\/tr>\n<tr><td>4<\/td><td>E-1<\/td><td>Hold mode enable<\/td><\/tr>\n<tr><td>5<\/td><td>F-1<\/td><td>Alternate mode enable<\/td><\/tr>\n<tr><td>6<\/td><td>F#-1<\/td><td>Attack mode enable<\/td><\/tr>\n<tr><td>7<\/td><td>G-1<\/td><td>Continue mode enable<\/td><\/tr>\n<\/tbody><\/table>\n<p>The period is controlled by CC messages, and the CC can be changed in the configuration. However, the longest period is probably too long for practical purposes, so the CC range is currently hard-coded to be mapped to the lowest 20% of the period range.<\/p>\n<p>The various modes can be combined to make the envelope generator behave in different ways:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;ay-env.a1f876be71095d2f.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>From the AY-3-8910 data sheet; Figure 1.<\/p>\n<\/blockquote>\n<h4 id=\"noise-generator\">Noise Generator<\/h4>\n<p>The noise generator period is set via CC messages, and the CC for each noise generator can be set in the configuration. The noise mixer configurations have been moved to key switches.<\/p>\n<p>MIDI key switch mapping:<\/p>\n<table><thead><tr><th><strong>MIDI Note Number<\/strong><\/th><th><strong>MIDI Note Name<\/strong><\/th><th><strong>Switch<\/strong><\/th><\/tr><\/thead><tbody>\n<tr><td>8<\/td><td>G#-1<\/td><td>Mix to output A enable<\/td><\/tr>\n<tr><td>9<\/td><td>A-1<\/td><td>Mix to output B enable<\/td><\/tr>\n<tr><td>10<\/td><td>A#-1<\/td><td>Mix to output C enable<\/td><\/tr>\n<\/tbody><\/table>\n<h4 id=\"pitchbend-transpose-and-detune\">Pitchbend, Transpose and Detune<\/h4>\n<p>Each MIDI note is mapped to an oscillator divisor value, which causes the oscillator to sound at something very close to the required note frequency.<\/p>\n<p>The actual oscillator frequency is calculated as:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>Frequency = Clock \/ 16 \/ Divisor\n<\/span><\/code><\/pre>\n<p>As such, it is not perfectly possible to match every note frequency across the entire MIDI note range because of the integer division.<\/p>\n<p>We are driving the <code>Clock<\/code> at 2.0 MHz, which means that the lowest 23 MIDI notes are unplayable. This is because these notes would require a <code>Divisor<\/code> value greater than would fit in the 12-bit divisor control register (4095). The available <code>Divisor<\/code> resolution also decreases as the MIDI note increases, which causes the frequency error to also increase as we go up the scale. We peak at about 4% frequency error in the very highest notes, but nobody wants to listen to 5kHz square waves anyway.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;ay-note-tuning-error.a6d4f68e4fe047bb.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>To give the performer some control over the exact frequency output of the oscillators we can implement some CC messages to affect the frequency.<\/p>\n<ul>\n<li>Transpose: offsets the MIDI note in semi-tone increments. This is mainly so that if two oscillators are mapped to the same MIDI channel, then they can independently be transposed; this means we can implement octave doubling, for example.<\/li>\n<li>Pitchbend: we implement the standard MIDI pitch bend commands which smoothly adjusts the <code>Divisor<\/code> between adjacent whole-tones. The <code>Divisor<\/code> difference between any two adjacent notes is not constant across the note range, so we have a separate map of <code>Divisor<\/code> deltas that we index into for applying pitch bend.<\/li>\n<li>Detune: this value directly adds\/subtracts from the <code>Divisor<\/code> value itself.<\/li>\n<\/ul>\n<p>The final <code>Divisor<\/code> is therefore calculated as:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>Divisor = divisor_note_map[Note + Transpose] + (divisor_delta_map[Note + Transpose] * Pitchbend) + Detune\n<\/span><\/code><\/pre>\n<h3 id=\"polyphony\">Polyphony<\/h3>\n<p>Using the Fourays synth with each oscillator mapped to different MIDI channels is cumbersome, and is not the way most MIDI music is written. It is more useful to be able to map multiple oscillators to the same MIDI channel write our music polyphonically, and have the firmware allocate Note On events to available oscillators.<\/p>\n<p>This was surprisingly straightforward to achieve, given the event driven architecture of the firmware using signals. In the monophonic firmware, we connected the incoming MIDI Note On and Note Off messages for each channel directly to the oscillator control handler.<\/p>\n<p><em>I'll illustrate this using simplified excerpts of the firmware C++ (ish) code. This code probably isn't strictly valid, I'm just trying to demonstrate the connections and the algorithm.<\/em><\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#65737e;\">\/\/ Create two arrays of Note message signals, to cover all the MIDI channels.\n<\/span><span>Signal&lt;<\/span><span style=\"color:#b48ead;\">int<\/span><span> note, <\/span><span style=\"color:#b48ead;\">int<\/span><span> velocity&gt;[<\/span><span style=\"color:#d08770;\">16<\/span><span>] channel_signals_note_on;\n<\/span><span>Signal&lt;<\/span><span style=\"color:#b48ead;\">int<\/span><span> note, <\/span><span style=\"color:#b48ead;\">int<\/span><span> velocity&gt;[<\/span><span style=\"color:#d08770;\">16<\/span><span>] channel_signals_note_off;\n<\/span><span>\n<\/span><span style=\"color:#65737e;\">\/\/ Re-configure all the oscillators\n<\/span><span style=\"color:#b48ead;\">for <\/span><span>(<\/span><span style=\"color:#b48ead;\">auto <\/span><span>&amp;oscillator : oscillators) {\n<\/span><span>    channel_signals_note_on[oscillator.<\/span><span style=\"color:#bf616a;\">config<\/span><span>.<\/span><span style=\"color:#bf616a;\">channel<\/span><span>].<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>        [](note, velocity) {\n<\/span><span>            oscillator.<\/span><span style=\"color:#bf616a;\">note_on<\/span><span>(note, velocity);\n<\/span><span>        }\n<\/span><span>    );\n<\/span><span>    channel_signals_note_off[oscillator.<\/span><span style=\"color:#bf616a;\">config<\/span><span>.<\/span><span style=\"color:#bf616a;\">channel<\/span><span>].<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>        [](note, velocity) {\n<\/span><span>            oscillator.<\/span><span style=\"color:#bf616a;\">note_off<\/span><span>(note, velocity);\n<\/span><span>        }\n<\/span><span>    );\n<\/span><span>}\n<\/span><span>\n<\/span><span style=\"color:#65737e;\">\/\/ When we receive any MIDI Note On message, dispatch it to the correct channel\n<\/span><span>midi_input.<\/span><span style=\"color:#bf616a;\">note_on<\/span><span>.<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>    [](channel, note, velocity) {\n<\/span><span>        channel_signals_note_on[channel].<\/span><span style=\"color:#bf616a;\">emit<\/span><span>(note, velocity);\n<\/span><span>    }\n<\/span><span>);\n<\/span><span>\n<\/span><span style=\"color:#65737e;\">\/\/ When we receive any MIDI Note Off message, dispatch it to the correct channel\n<\/span><span>midi_input.<\/span><span style=\"color:#bf616a;\">note_off<\/span><span>.<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>    [](channel, note, velocity) {\n<\/span><span>        channel_signals_note_off[channel].<\/span><span style=\"color:#bf616a;\">emit<\/span><span>(note, velocity);\n<\/span><span>    }\n<\/span><span>);\n<\/span><\/code><\/pre>\n<p>To implement polyphony, we need to upgrade the channel signals array to an array of some objects which know how many oscillators are available per channel and keeps track of which oscillators are playing which notes:<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">using <\/span><span>NoteSlot = std::function&lt;<\/span><span style=\"color:#b48ead;\">void<\/span><span>(<\/span><span style=\"color:#b48ead;\">int<\/span><span> note, <\/span><span style=\"color:#b48ead;\">int<\/span><span> velocity)&gt;;\n<\/span><span>\n<\/span><span style=\"color:#65737e;\">\/\/ This object replaces the Channel signals,\n<\/span><span style=\"color:#65737e;\">\/\/ and now holds references to the oscillator control functions.\n<\/span><span style=\"color:#b48ead;\">class <\/span><span style=\"color:#ebcb8b;\">ChannelManager\n<\/span><span style=\"color:#eff1f5;\">{\n<\/span><span style=\"color:#b48ead;\">public<\/span><span style=\"color:#eff1f5;\">:\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ Add the oscillator on\/off handlers to this manager\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">connect<\/span><span style=\"color:#eff1f5;\">(NoteSlot <\/span><span style=\"color:#bf616a;\">on_slot<\/span><span style=\"color:#eff1f5;\">, NoteSlot <\/span><span style=\"color:#bf616a;\">off_slot<\/span><span style=\"color:#eff1f5;\">)\n<\/span><span style=\"color:#eff1f5;\">    {\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#bf616a;\">m_slots<\/span><span style=\"color:#eff1f5;\">.<\/span><span style=\"color:#bf616a;\">push_back<\/span><span style=\"color:#eff1f5;\">({ .<\/span><span style=\"color:#bf616a;\">on<\/span><span>=<\/span><span style=\"color:#eff1f5;\">on_slot, .<\/span><span style=\"color:#bf616a;\">off<\/span><span>=<\/span><span style=\"color:#eff1f5;\">off_slot });\n<\/span><span style=\"color:#eff1f5;\">    }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ Decide what to do with each incoming note on message;\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ Find an available oscillator and activate it.\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">note_on<\/span><span style=\"color:#eff1f5;\">(<\/span><span style=\"color:#b48ead;\">int <\/span><span style=\"color:#bf616a;\">note<\/span><span style=\"color:#eff1f5;\">, <\/span><span style=\"color:#b48ead;\">int <\/span><span style=\"color:#bf616a;\">velocity<\/span><span style=\"color:#eff1f5;\">)\n<\/span><span style=\"color:#eff1f5;\">    {\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#b48ead;\">if <\/span><span style=\"color:#eff1f5;\">(<\/span><span style=\"color:#bf616a;\">m_active_notes<\/span><span style=\"color:#eff1f5;\">.<\/span><span style=\"color:#bf616a;\">contains<\/span><span style=\"color:#eff1f5;\">(note))\n<\/span><span style=\"color:#eff1f5;\">        {\n<\/span><span style=\"color:#eff1f5;\">            <\/span><span style=\"color:#b48ead;\">return<\/span><span style=\"color:#eff1f5;\">; <\/span><span style=\"color:#65737e;\">\/\/ note is already on\n<\/span><span style=\"color:#eff1f5;\">        }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#65737e;\">\/\/ find an available slot\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#b48ead;\">for <\/span><span style=\"color:#eff1f5;\">(size_t i <\/span><span>= <\/span><span style=\"color:#d08770;\">0<\/span><span style=\"color:#eff1f5;\">; i <\/span><span>&lt; <\/span><span style=\"color:#bf616a;\">m_slots<\/span><span style=\"color:#eff1f5;\">.<\/span><span style=\"color:#bf616a;\">size<\/span><span style=\"color:#eff1f5;\">(); <\/span><span>++<\/span><span style=\"color:#eff1f5;\">i)\n<\/span><span style=\"color:#eff1f5;\">        {\n<\/span><span style=\"color:#eff1f5;\">            <\/span><span style=\"color:#65737e;\">\/\/ if this slot is not in use\n<\/span><span style=\"color:#eff1f5;\">            <\/span><span style=\"color:#b48ead;\">if <\/span><span style=\"color:#eff1f5;\">(<\/span><span>!<\/span><span style=\"color:#bf616a;\">m_active_slots<\/span><span style=\"color:#eff1f5;\">.<\/span><span style=\"color:#bf616a;\">contains<\/span><span style=\"color:#eff1f5;\">(i))\n<\/span><span style=\"color:#eff1f5;\">            {\n<\/span><span style=\"color:#eff1f5;\">                <\/span><span style=\"color:#bf616a;\">m_active_slots<\/span><span style=\"color:#eff1f5;\">[i] <\/span><span>=<\/span><span style=\"color:#eff1f5;\"> note;      <\/span><span style=\"color:#65737e;\">\/\/ allocate the oscillator\n<\/span><span style=\"color:#eff1f5;\">                <\/span><span style=\"color:#bf616a;\">m_active_notes<\/span><span style=\"color:#eff1f5;\">[note] <\/span><span>=<\/span><span style=\"color:#eff1f5;\"> i;      <\/span><span style=\"color:#65737e;\">\/\/ allocate the note\n<\/span><span style=\"color:#eff1f5;\">                <\/span><span style=\"color:#bf616a;\">m_slots<\/span><span style=\"color:#eff1f5;\">[i].<\/span><span style=\"color:#bf616a;\">on<\/span><span style=\"color:#eff1f5;\">(note, velocity); <\/span><span style=\"color:#65737e;\">\/\/ forward the note to the oscillator\n<\/span><span style=\"color:#eff1f5;\">                <\/span><span style=\"color:#b48ead;\">return<\/span><span style=\"color:#eff1f5;\">;\n<\/span><span style=\"color:#eff1f5;\">            }\n<\/span><span style=\"color:#eff1f5;\">        }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#65737e;\">\/\/ If we reach here ... we can consider this later ...\n<\/span><span style=\"color:#eff1f5;\">    }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ Decide what to do with each incoming note off message;\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ If an oscillator is playing the note then end it and make\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ both the oscillator and note available for use again.\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">note_off<\/span><span style=\"color:#eff1f5;\">(<\/span><span style=\"color:#b48ead;\">int <\/span><span style=\"color:#bf616a;\">note<\/span><span style=\"color:#eff1f5;\">, <\/span><span style=\"color:#b48ead;\">int <\/span><span style=\"color:#bf616a;\">velocity<\/span><span style=\"color:#eff1f5;\">)\n<\/span><span style=\"color:#eff1f5;\">    {\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#b48ead;\">if <\/span><span style=\"color:#eff1f5;\">(<\/span><span>!<\/span><span style=\"color:#bf616a;\">m_active_notes<\/span><span style=\"color:#eff1f5;\">.<\/span><span style=\"color:#bf616a;\">contains<\/span><span style=\"color:#eff1f5;\">(note))\n<\/span><span style=\"color:#eff1f5;\">        {\n<\/span><span style=\"color:#eff1f5;\">            <\/span><span style=\"color:#b48ead;\">return<\/span><span style=\"color:#eff1f5;\">; <\/span><span style=\"color:#65737e;\">\/\/ note is not on\n<\/span><span style=\"color:#eff1f5;\">        }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#65737e;\">\/\/ find which oscillator is playing this note\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#b48ead;\">const auto<\/span><span style=\"color:#eff1f5;\"> i <\/span><span>= <\/span><span style=\"color:#bf616a;\">m_active_notes<\/span><span style=\"color:#eff1f5;\">[note];\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#bf616a;\">m_active_slots<\/span><span style=\"color:#eff1f5;\">.<\/span><span style=\"color:#bf616a;\">erase<\/span><span style=\"color:#eff1f5;\">(i);        <\/span><span style=\"color:#65737e;\">\/\/ free the oscillator\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#bf616a;\">m_active_notes<\/span><span style=\"color:#eff1f5;\">.<\/span><span style=\"color:#bf616a;\">erase<\/span><span style=\"color:#eff1f5;\">(note);     <\/span><span style=\"color:#65737e;\">\/\/ free the note\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#bf616a;\">m_slots<\/span><span style=\"color:#eff1f5;\">[i].<\/span><span style=\"color:#bf616a;\">off<\/span><span style=\"color:#eff1f5;\">(note, velocity); <\/span><span style=\"color:#65737e;\">\/\/ forward the note to the oscillator\n<\/span><span style=\"color:#eff1f5;\">    }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#b48ead;\">private<\/span><span style=\"color:#eff1f5;\">:\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ Array of available oscillator control functions\n<\/span><span style=\"color:#eff1f5;\">    std::vector&lt;<\/span><span style=\"color:#b48ead;\">struct <\/span><span style=\"color:#eff1f5;\">{ NoteSlot on; NoteSlot off; }&gt; <\/span><span style=\"color:#bf616a;\">m_slots<\/span><span style=\"color:#eff1f5;\">;\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ Somewhere to keep track of which oscillators are in use;\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ Useful for when we want to turn a note on\n<\/span><span style=\"color:#eff1f5;\">    std::unordered_map&lt;uint8_t, uint8_t&gt; <\/span><span style=\"color:#bf616a;\">m_active_slots<\/span><span style=\"color:#eff1f5;\">; <\/span><span style=\"color:#65737e;\">\/\/ maps &lt;slot index, note&gt;\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ Somewhere to keep track of which notes are currently on;\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#65737e;\">\/\/ Useful for when we want to turn a note off\n<\/span><span style=\"color:#eff1f5;\">    std::unordered_map&lt;uint8_t, uint8_t&gt; <\/span><span style=\"color:#bf616a;\">m_active_notes<\/span><span style=\"color:#eff1f5;\">; <\/span><span style=\"color:#65737e;\">\/\/ maps &lt;note, slot index&gt;\n<\/span><span style=\"color:#eff1f5;\">}<\/span><span>;\n<\/span><span>\n<\/span><span>ChannelManager[<\/span><span style=\"color:#d08770;\">16<\/span><span>] channel_managers;\n<\/span><span>\n<\/span><span style=\"color:#65737e;\">\/\/ Re-configure all the oscillators\n<\/span><span style=\"color:#b48ead;\">for <\/span><span>(<\/span><span style=\"color:#b48ead;\">auto <\/span><span>&amp;oscillator : oscillators) {\n<\/span><span>    channel_managers[oscillator.<\/span><span style=\"color:#bf616a;\">config<\/span><span>.<\/span><span style=\"color:#bf616a;\">channel<\/span><span>].<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>        [](note, velocity) {\n<\/span><span>            oscillator.<\/span><span style=\"color:#bf616a;\">note_on<\/span><span>(note, velocity);\n<\/span><span>        },\n<\/span><span>        [](note, velocity) {\n<\/span><span>            oscillator.<\/span><span style=\"color:#bf616a;\">note_off<\/span><span>(note, velocity);\n<\/span><span>        }\n<\/span><span>    )\n<\/span><span>}\n<\/span><span>\n<\/span><span style=\"color:#65737e;\">\/\/ When we receive any MIDI Note On message, dispatch it to the correct manager\n<\/span><span>midi_input.<\/span><span style=\"color:#bf616a;\">note_on<\/span><span>.<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>    [](channel, note, velocity) {\n<\/span><span>        channel_managers[channel].<\/span><span style=\"color:#bf616a;\">note_on<\/span><span>(note, velocity);\n<\/span><span>    }\n<\/span><span>);\n<\/span><span>\n<\/span><span style=\"color:#65737e;\">\/\/ When we receive any MIDI Note Off message, dispatch it to the correct manager\n<\/span><span>midi_input.<\/span><span style=\"color:#bf616a;\">note_off<\/span><span>.<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>    [](channel, note, velocity) {\n<\/span><span>        channel_managers[channel].<\/span><span style=\"color:#bf616a;\">note_off<\/span><span>(note, velocity);\n<\/span><span>    }\n<\/span><span>);\n<\/span><\/code><\/pre>\n<p>All of the polyphony handling was done in the middle layer without changing either the MIDI input driver or the oscillator controllers.<\/p>\n<p>There's further work we could do here as well, as indicated by the final comment in the <code>ChannelManager::note_on<\/code> method. At the moment, if there are not enough available oscillator slots for the next Note On message, then the message is ignored. We could however do something else. We could for example \"steal\" an oscillator and make it play the new note in place of some other note which was currently playing. There's quite a few different strategies we could employ to do this; either steal from the oldest or newest playing note, or the highest, or lowest, or something in the middle. I'm not going to implement any of these for the time being though.<\/p>\n<p>Finally, I think I will need to add a polyphony on\/off configuration somewhere, since it is at odds some of the other options available, particularly Transpose. By which I mean that if two AYs are on the same MIDI channel with polyphony enabled, you can just write your music in octaves, you don't need to transpose one of the oscillators. However, with polyphony turned off it makes sense, you can then double up the oscillators in octaves from a single note on one channel. I think that adds value to the firmware, and lets the performer decide how they want to use the oscillators.<\/p>\n<h3 id=\"configuration\">Configuration<\/h3>\n<p>Now that we support all of the AY functions and polyphony, the synth configuration needed attention to ensure that all of the note channels and control channels can be set up as desired. It's also worth at this point to explain also how the device manages configuration.<\/p>\n<p>Because of the limited number of available input pins on the ESP32-S3, I've minimised the number of button or touch inputs to just three:<\/p>\n<ul>\n<li><code>LEFT<\/code><\/li>\n<li><code>RIGHT<\/code><\/li>\n<li><code>SELECT<\/code><\/li>\n<\/ul>\n<p>We will use these inputs as events in a simple state machine:<\/p>\n<pre class=\"mermaid\">\n  flowchart LR\nA[NAVIGATE STATE]\nB[EDIT STATE]\n\n    A -- LEFT &lt;br&gt;&#x2F; RIGHT --&gt; A\n    B -- LEFT &lt;br&gt;&#x2F; RIGHT --&gt; B\n\n    A -- SELECT --&gt; B\n    B -- SELECT --&gt; A\n<\/pre>\n<p>Whilst we are in <code>NAVIGATE<\/code> state, pressing <code>LEFT<\/code> or <code>RIGHT<\/code> will update the state machine's context to cycle through the available configuration items. Pressing <code>SELECT<\/code> captures the current item for editing and enters <code>EDIT<\/code> state whereby the <code>LEFT<\/code> and <code>RIGHT<\/code> inputs will cycle through the item's values. Pressing <code>SELECT<\/code> again releases the item from editing and moves the state machine back to <code>NAVIGATE<\/code> state.<\/p>\n<p>For example, lets say we have a configuration consisting of three items:<\/p>\n<ol>\n<li>\"Oscillator A channel\" = 5<\/li>\n<li>\"Oscillator B channel\" = 8<\/li>\n<li>\"Oscillator C channel\" = 14<\/li>\n<\/ol>\n<p>Each of these items can hold a value from 1 to 16.<\/p>\n<p>At startup, the configuration state machine is in <code>NAVIGATE<\/code> state and the first item is selected. Lets say we want to change the \"Oscillator C channel\" to 1. We would input:<\/p>\n<ol>\n<li><code>RIGHT<\/code> - moves selection to \"Oscillator B channel\" item<\/li>\n<li><code>RIGHT<\/code> - moves selection to \"Oscillator C channel\" item<\/li>\n<li><code>SELECT<\/code> - selects \"Oscillator C channel\" for editing<\/li>\n<li><code>RIGHT<\/code> - increments \"Oscillator C channel\" to 15<\/li>\n<li><code>RIGHT<\/code> - increments \"Oscillator C channel\" to 16<\/li>\n<li><code>RIGHT<\/code> - wraps \"Oscillator C channel\" back to 1<\/li>\n<li><code>SELECT<\/code> - de-selects \"Oscillator C channel\"<\/li>\n<\/ol>\n<p>Navigation also wraps around, so instead of initially going <code>RIGHT<\/code> twice, we could have input <code>LEFT<\/code> just once.<\/p>\n<p>This probably sounds a lot more complicated than it is, but it's much the same as operating any simple electronic widget with a limited number of buttons. Showing the configuration items on screen with select and edit state markers also helps. But, I haven't completed the screen rendering code yet (<em>it's quite slow and tedious to write, I'm seriously considering writing a screen\/UI emulator for it to develop and test the config control code locally without using the ESP32-S3 to do it<\/em>).<\/p>\n<p>The actual Fourays configuration consists of 20 config items per AY chip:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>Global Control Channel\n<\/span><span>Envelope Period CC\n<\/span><span>Noise Control Mode\n<\/span><span>Noise Note Channel\n<\/span><span>Noise Period CC\n<\/span><span>\n<\/span><span>Oscillator A Note Channel               Oscillator B Note Channel               Oscillator C Note Channel\n<\/span><span>Oscillator A Control Channel            Oscillator B Control Channel            Oscillator C Control Channel\n<\/span><span>Oscillator A Filter Frequency CC        Oscillator B Filter Frequency CC        Oscillator C Filter Frequency CC\n<\/span><span>Oscillator A Transpose CC               Oscillator B Transpose CC               Oscillator C Transpose CC\n<\/span><span>Oscillator A Detune CC                  Oscillator B Detune CC                  Oscillator C Detune CC\n<\/span><\/code><\/pre>\n<p>You might have noticed that each oscillator can have different note and control channels. This is so that if you set up multiple oscillators per channel, so as to enable polyphony, that you can still allocate the CC controllers to distinct channels and control the oscillators' transpose, detune and filter frequency independently.<\/p>\n<p>The final part of the configuration implementation will be some means to save and load presets both on the device itself and by reading and dumping MIDI SysEx messages. To do this, we need a way to specify the \"identity\" of a configuration item. It is actually these ID objects that the configuration controller state machine iterates through for selection.<\/p>\n<p>The actual way I've identified config items in Fourays is using a struct called <code>ConfigItemId<\/code>:<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">enum class <\/span><span>ConfigItemType\n<\/span><span>{\n<\/span><span>    PSG,\n<\/span><span>    PSGN,\n<\/span><span>    ENVELOPE,\n<\/span><span>    PSGV,\n<\/span><span>};\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">enum class <\/span><span>ConfigItemPropType\n<\/span><span>{\n<\/span><span>    NONE,\n<\/span><span>    NOTECHANNEL,\n<\/span><span>    CTRLCHANNEL,\n<\/span><span>    MODE,\n<\/span><span>    CC0,\n<\/span><span>    CC1,\n<\/span><span>    CC2,\n<\/span><span>};\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">struct <\/span><span>ConfigItemId\n<\/span><span>{\n<\/span><span>    ConfigItemType type;\n<\/span><span>    int8_t majorIdx = -<\/span><span style=\"color:#d08770;\">1<\/span><span>;\n<\/span><span>    int8_t minorIdx = -<\/span><span style=\"color:#d08770;\">1<\/span><span>;\n<\/span><span>    ConfigItemPropType property = ConfigItemPropType::NONE;\n<\/span><span>};\n<\/span><\/code><\/pre>\n<p>Lets break down why this is necessary:<\/p>\n<ul>\n<li><code>ConfigItemType type<\/code> identifies the type of device we are configuring:\n<ul>\n<li><code>PSG<\/code> : an entire AY chip<\/li>\n<li><code>PSGN<\/code> : an AY noise generator (1 per chip)<\/li>\n<li><code>ENVELOPE<\/code> : an AY envelope generator (1 per chip)<\/li>\n<li><code>PSGV<\/code> : an AY oscillator (3 per chip)<\/li>\n<\/ul>\n<\/li>\n<li><code>int8_t majorIdx<\/code> : the index of the AY chip, so takes a value 0 - 3 inclusive.<\/li>\n<li><code>int8_t minorIdx<\/code> : the index of part of an AY chip, for example oscillator or filter, takes a value 0 - 2 inclusive, or -1 we only have one instance in a chip.<\/li>\n<li><code>ConfigItemPropType property<\/code> : indicates which property of the chip or chip part we are configuring. This is also essential for editing because it allows us to determine the range of possible values.<\/li>\n<\/ul>\n<p>So for example to construct the configuration items for all of the oscillator note channels looks like this:<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">for <\/span><span>(int8_t i = <\/span><span style=\"color:#d08770;\">0<\/span><span>; i &lt; <\/span><span style=\"color:#d08770;\">4<\/span><span>; ++i) <\/span><span style=\"color:#65737e;\">\/\/ chips\n<\/span><span>{\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">for <\/span><span>(int8_t k = <\/span><span style=\"color:#d08770;\">0<\/span><span>; k &lt; <\/span><span style=\"color:#d08770;\">3<\/span><span>; ++k) <\/span><span style=\"color:#65737e;\">\/\/ oscillators\n<\/span><span>    {\n<\/span><span>        config_item_ids.<\/span><span style=\"color:#bf616a;\">push_back<\/span><span>(<\/span><span style=\"color:#bf616a;\">ConfigItemId<\/span><span>{\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">type <\/span><span>= ConfigItemType::PSGV,\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">majorIdx <\/span><span>= i,\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">minorIdx <\/span><span>= k,\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">property <\/span><span>= ConfigItemPropType::NOTECHANNEL,\n<\/span><span>        });\n<\/span><span>    }\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>The act of changing the configuration values from the user inputs is delegated separately to the configuration manager object. It looks something like this:<\/p>\n<pre class=\"mermaid\">\n  flowchart TB\n\nA[Config IDs Array]\nB[Config Controller]\nC[Config Manager]\nD[Config Values]\nE[User]\n\nA -.-o B\nA -.-o C\n\nE -- INPUT&lt;br&gt;EVENTS --&gt; B\nB -- EDIT&lt;br&gt;EVENTS --&gt; C\n\nD -.-o C\n<\/pre>\n<p>The <code>INPUT EVENTS<\/code> are the same <code>LEFT<\/code>\/<code>RIGHT<\/code>\/<code>SELECT<\/code> that we met before. The <code>EDIT EVENTS<\/code> are actually calls to a method on the configuration manager which contains only the configuration ID and the direction of the edit (i.e. increment or decrement) since we are dealing only with numeric values. At no time does the configuration controller actually know the configuration values.<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span>config_controller-&gt;<\/span><span style=\"color:#bf616a;\">edit<\/span><span>().<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/**\n<\/span><span style=\"color:#65737e;\">      Two parameters supplied by the state machine:\n<\/span><span style=\"color:#65737e;\">      - ConfigItemId: is explained below;\n<\/span><span style=\"color:#65737e;\">      - dir: -1 for decrement, 1 for increment\n<\/span><span style=\"color:#65737e;\">\n<\/span><span style=\"color:#65737e;\">      nextVal(in, dir, min, max) is a simple helper function\n<\/span><span style=\"color:#65737e;\">      which implements the increment\/decrement with wrapping\n<\/span><span style=\"color:#65737e;\">     *\/\n<\/span><span>    [](<\/span><span style=\"color:#b48ead;\">const<\/span><span> ConfigItemId &amp;id, <\/span><span style=\"color:#b48ead;\">const int<\/span><span> dir)\n<\/span><span>    {\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">const auto<\/span><span> current = config.<\/span><span style=\"color:#bf616a;\">get_value<\/span><span>(id);\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">auto<\/span><span> next = current;\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">switch <\/span><span>(id.<\/span><span style=\"color:#bf616a;\">property<\/span><span>)\n<\/span><span>        {\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">case<\/span><span> ConfigItemPropType::NOTECHANNEL:\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">case<\/span><span> ConfigItemPropType::CTRLCHANNEL:\n<\/span><span>                next = <\/span><span style=\"color:#bf616a;\">nextVal<\/span><span>(current, dir, <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#d08770;\">16<\/span><span>);\n<\/span><span>                <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">case<\/span><span> ConfigItemPropType::CC0:\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">case<\/span><span> ConfigItemPropType::CC1:\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">case<\/span><span> ConfigItemPropType::CC2:\n<\/span><span>                next = <\/span><span style=\"color:#bf616a;\">nextVal<\/span><span>(current, dir, <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#d08770;\">127<\/span><span>);\n<\/span><span>                <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">case<\/span><span> ConfigItemPropType::MODE:\n<\/span><span>                next = <\/span><span style=\"color:#bf616a;\">nextVal<\/span><span>(current, dir, <\/span><span style=\"color:#d08770;\">0<\/span><span>, NUM_MODES);\n<\/span><span>                <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">default<\/span><span>:\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>        }\n<\/span><span>        config.<\/span><span style=\"color:#bf616a;\">put_value<\/span><span>(id, next);\n<\/span><span>    });\n<\/span><\/code><\/pre>\n<p>Using an array of <code>ConfigItemId<\/code> works in favour of saving and loading the configuration. All we need to do to save is output a list of <code>(ID, Value)<\/code> pairs, and the reverse for loading.\nSaving or loading the configuration to file then is almost trivial, we don't have to know in the save or load code much at all about what configuration items even exist:<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#65737e;\">\/\/ Just dump the config in ID order to a file, fixed width 5 bytes per item.\n<\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">save_config<\/span><span>()\n<\/span><span>{\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">auto<\/span><span> f = <\/span><span style=\"color:#bf616a;\">open_file<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">my.cfg<\/span><span>&quot;);\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">for <\/span><span>(<\/span><span style=\"color:#b48ead;\">auto <\/span><span>&amp;item_id : config_item_ids)\n<\/span><span>    {\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">const auto<\/span><span> value = config.<\/span><span style=\"color:#bf616a;\">get_value<\/span><span>(item_id);\n<\/span><span>        file.<\/span><span style=\"color:#bf616a;\">write<\/span><span>(\n<\/span><span>            &quot;<\/span><span style=\"color:#d08770;\">%d%d%d%d%d<\/span><span>&quot;,\n<\/span><span>            item_id.<\/span><span style=\"color:#bf616a;\">type<\/span><span>, item_id.<\/span><span style=\"color:#bf616a;\">majorIdx<\/span><span>, item_id.<\/span><span style=\"color:#bf616a;\">minorIdx<\/span><span>, item_id.<\/span><span style=\"color:#bf616a;\">property<\/span><span>,\n<\/span><span>            value\n<\/span><span>        );\n<\/span><span>    }\n<\/span><span>}\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">load_config<\/span><span>()\n<\/span><span>{\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">auto<\/span><span> f = <\/span><span style=\"color:#bf616a;\">open_file<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">my.cfg<\/span><span>&quot;);\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">while<\/span><span>(f.<\/span><span style=\"color:#bf616a;\">available<\/span><span>())\n<\/span><span>    {\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">const auto<\/span><span> id_type  = f.<\/span><span style=\"color:#bf616a;\">read<\/span><span>();\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">const auto<\/span><span> id_major = f.<\/span><span style=\"color:#bf616a;\">read<\/span><span>();\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">const auto<\/span><span> id_minor = f.<\/span><span style=\"color:#bf616a;\">read<\/span><span>();\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">const auto<\/span><span> id_prop  = f.<\/span><span style=\"color:#bf616a;\">read<\/span><span>();\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">const auto<\/span><span> value    = f.<\/span><span style=\"color:#bf616a;\">read<\/span><span>();\n<\/span><span>        config.<\/span><span style=\"color:#bf616a;\">put_value<\/span><span>(\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">ConfigItemId<\/span><span>{\n<\/span><span>                .<\/span><span style=\"color:#bf616a;\">type     <\/span><span>= id_type,\n<\/span><span>                .<\/span><span style=\"color:#bf616a;\">majorIdx <\/span><span>= id_major,\n<\/span><span>                .<\/span><span style=\"color:#bf616a;\">minorIdx <\/span><span>= id_minor,\n<\/span><span>                .<\/span><span style=\"color:#bf616a;\">property <\/span><span>= id_prop\n<\/span><span>            },\n<\/span><span>            value\n<\/span><span>        );\n<\/span><span>    }\n<\/span><span>}\n<\/span><\/code><\/pre>\n<h2 id=\"hardware\">Hardware<\/h2>\n<h3 id=\"new-chips-shipment\">New Chips Shipment<\/h3>\n<p>On the hardware side, I did receive another shipment of chips from China. This time I ordered 20 Yamaha <code>YM2149F<\/code> chips. These are electrically identical to <code>AY-3-8910<\/code>, they were just made by Yamaha rather than GI or Microchip. All of these arrived in a somewhat dirty, but original state. These new chips have not been re-labelled, all of them read \"YAMAHA JAPAN \/ YM2149F \/ 0517 GABG\" on the top side. And yes, some of them are faulty. As per the previous post on chip testing, here's the results of what I got:<\/p>\n<table><thead><tr><th><strong>Bottom<br>Print<\/strong><\/th><th><strong>Bottom<br>Mark Left<\/strong><\/th><th><strong>Bottom<br>Mark Right<\/strong><\/th><th><strong>Test<\/strong><\/th><th><strong>Notes<\/strong><\/th><\/tr><\/thead><tbody>\n<tr><td>-<\/td><td>5 E1<\/td><td>TAIWAN<\/td><td>PASS<\/td><td><\/td><\/tr>\n<tr><td>B7734M80 \/ 6766B<\/td><td>09 S3<\/td><td>SI<\/td><td>PASS<\/td><td>Cropped pins 1-20<\/td><\/tr>\n<tr><td>-<\/td><td>-<\/td><td>TN H2<\/td><td>FAIL<\/td><td>Cropped pins \/ Buzzes<\/td><\/tr>\n<tr><td>-<\/td><td>I1<\/td><td>I1<\/td><td>PASS<\/td><td>Cropped pin 40<\/td><\/tr>\n<tr><td>-<\/td><td>5 D1<\/td><td>TAIWAN<\/td><td>PASS<\/td><td><\/td><\/tr>\n<tr><td>C8833662C<\/td><td>-<\/td><td>TN M4<\/td><td>FAIL<\/td><td>Cropped pins \/ DEAD<\/td><\/tr>\n<tr><td>-<\/td><td>Z 5<\/td><td>CJ<\/td><td>PASS<\/td><td><\/td><\/tr>\n<tr><td>AA1KN51C<\/td><td>-<\/td><td>TN E2<\/td><td>PASS<\/td><td><\/td><\/tr>\n<tr><td>-<\/td><td>H 37<\/td><td>D4<\/td><td>FAIL<\/td><td>Cropped pins 1-20 \/ DEAD<\/td><\/tr>\n<tr><td>-<\/td><td>M 37<\/td><td>L5<\/td><td>FAIL<\/td><td>Cropped pins \/ Buzzes<\/td><\/tr>\n<tr><td>-<\/td><td>5 G5<\/td><td>TAIWAN<\/td><td>FAIL<\/td><td>A, B DEAD; C Buzzes only<\/td><\/tr>\n<tr><td>AH1UE1.1<\/td><td>M37<\/td><td>I1<\/td><td>PASS<\/td><td>PIN 20 DAMAGED<\/td><\/tr>\n<tr><td>AA1KN51C<\/td><td>-<\/td><td>TN C3<\/td><td>PASS<\/td><td><\/td><\/tr>\n<tr><td>A93TRK1C<\/td><td>-<\/td><td>TN K4<\/td><td>PASS<\/td><td><\/td><\/tr>\n<tr><td>-<\/td><td>5 L2<\/td><td>TAIWAN<\/td><td>PASS<\/td><td><\/td><\/tr>\n<tr><td>-<\/td><td>-<\/td><td>-<\/td><td>PASS<\/td><td><\/td><\/tr>\n<tr><td>AF4126.1<\/td><td>7 F5<\/td><td>TAIWAN<\/td><td>PASS<\/td><td><\/td><\/tr>\n<tr><td>-<\/td><td>5 T3<\/td><td>TAIWAN<\/td><td>FAIL<\/td><td>Only buzzes<\/td><\/tr>\n<tr><td>-<\/td><td>5 M5<\/td><td>TAIWAN<\/td><td>PASS<\/td><td><\/td><\/tr>\n<tr><td>A8A8B11C<\/td><td>-<\/td><td>TN H3<\/td><td>PASS<\/td><td>Cropped pins, hard to keep in socket<\/td><\/tr>\n<\/tbody><\/table>\n<p>Curiously, some of the chips bottom case markings match what is on some of the other re-labelled <code>AY-3-8910<\/code> chips from the first batch \ud83e\udd14<\/p>\n<p>Anyhow, I now have 24 working chips, which is enough to build 6 Fourays synths.<\/p>\n<h3 id=\"pcb-layouts\">PCB Layouts<\/h3>\n<p>I've done a lot of PCB layout clean up and made the boards a little smaller. I'm also still playing around with what to include in the Fourays synth. There's the choice that it is somewhat fully-featured and standalone (with filters) or it's a bare multi-oscillator module (without filters).<\/p>\n<h4 id=\"with-filters\">With Filters<\/h4>\n<p>The op-amps have been switched to surface mount. It's also just occurred to me that the large BC547 and BC557 transistors can also be converted to surface mount. Also still to do is change the footprints of the trim potentiometers, now that I have an idea of which ones I would use and these have 3 pins in-line rather than offset.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;pcb-cleanup.313055041442359e.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Each Fourays synth needs four of the left hand AY board and one of the right hand control board. That's five boards to assemble together which roughly fits in a desktop enclosure about 25 cm x 14 cm x 6 cm. I've done some preliminary mock ups for this form factor, but haven't settled on anything I like enough to show off yet.<\/p>\n<p>The filters in this design still need a +\/- 12V supply. I've bought some modules which apparently convert from 5V to +\/- 12V for this purpose, so that I can power this from the USB connection but not tested them yet, and neither do I have the filter circuits to see if they work with the converted voltage levels.<\/p>\n<h4 id=\"without-filters\">Without Filters<\/h4>\n<p>I've also been debating about not including the filters at all (and therefore also removing the mixer circuit and CV DACs). In this case, we can get all four AY chips and the ESP32-S3 on one board. On this board I have also re-instated the Gate outputs. The Gate outputs are simple signals which are active when an oscillator is playing, else inactive. These could be used in a modular synth setup to trigger and sync other modules in time with the oscillators. I could include the Gate outputs also on the version with filters.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;pcb-single-board.a00b81db1f41ccf0.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>This simpler design would be much more suitable to build as modular synth modules, for example in Eurorack format. This board design would require a module 16HP wide, but is currently significantly too tall, I'd probably have to re-configure this as a pair of boards stacked one behind the other.<\/p>\n<p>But, going down the modular module route presents me with a bit of a conundrum:<\/p>\n<blockquote>\n<p>Rather than building my own filter and mixer and power supply circuits, should I just make these simple combination boards only, in a Eurorack compatible format? I could then simply just start building a Eurorack synth for myself using the AYs as the core oscillators. That would allow me much more flexibility in how to process the oscillator outputs using any commercially available or DIY euro modules. I could also then just buy a euro case and power supply and the whole thing would be packaged very nicely in a standard format.<\/p>\n<p>But ... that leads to investing in a euro modular system (\u00a3\u00a3\u00a3 \ud83e\udd2e), when all I really wanted was some MIDI controlled AYs.<\/p>\n<\/blockquote>\n<p>I have in fact already reached the latter goal. I already hand made a combination board for myself on some prototype board. I've repurposed an old enclosure I had laying around which was already fitted with 1\/4\" jack sockets for the outputs:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;DSC08344.cd222815e4a52cf2.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I could stop there and do everything else in software even. <em>But it's not particularly attractive<\/em>. And, I now have a glut of other chips to build into units, and I know I'm going to either sell or give away those units and those units need to be usable by other musicians, who would probably much rather prefer them to be made to some standard form factor. And I still want to impart a little bit of visual design into this synth to reflect the 1980's era vibes of the old computers which contained these AY chips as their sound generators.<\/p>\n<h3 id=\"compiling-a-bill-of-materials\">Compiling a Bill of Materials<\/h3>\n<p>To see if cost comes into play to help guide me on the form factor decision I decided to do some calculations. Based on the with-filters boards, I submitted my boards to JLCPCB to find out how much they'd cost to manufacture. Also, I needed to work out what other components to purchase to complete each synth, made a list and added up the cost of all of those:<\/p>\n<p>To make all 6 Fourays synths with filters and all the panel controls:<\/p>\n<table><thead><tr><th><strong>Item<\/strong><\/th><th><strong>Cost<\/strong><\/th><th><strong>Qty<\/strong><\/th><th><strong>Cost\/Item<\/strong><\/th><th><strong>Qty\/Synth<\/strong><\/th><th><strong>Cost\/Synth<\/strong><\/th><\/tr><\/thead><tbody>\n<tr><td>AY boards<\/td><td>\u00a393.08<\/td><td>25<\/td><td>\u00a33.72<\/td><td>4<\/td><td>\u00a314.89<\/td><\/tr>\n<tr><td>Control board<\/td><td>\u00a321.88<\/td><td>10<\/td><td>\u00a32.19<\/td><td>1<\/td><td>\u00a32.19<\/td><\/tr>\n<tr><td>Non PCBA components<\/td><td>\u00a3152.96<\/td><td>6<\/td><td>\u00a325.49<\/td><td>1<\/td><td>\u00a325.49<\/td><\/tr>\n<tr><td>Enclosure (TBC)<\/td><td>\u00a35.00<\/td><td>1<\/td><td>\u00a35.00<\/td><td>1<\/td><td>\u00a35.00<\/td><\/tr>\n<\/tbody><\/table>\n<p><strong>TOTAL per synth \u00a347.57<\/strong><\/p>\n<p>That's not bad actually. But it would be significantly cheaper as a Eurorack module, even if the rest of the modular system is likely to be extremely expensive. If anyone has any tips on how to do euro modular on a strict budget, do let me know \ud83d\ude09<\/p>\n"},{"title":"Fourays: Testing Grey Market AY-3-8910 Chips","published":"2024-02-19T00:00:00+00:00","updated":"2024-02-19T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/fourays\/chip-testing\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/fourays\/chip-testing\/","content":"<h2 id=\"finding-out-exactly-what-i-received-from-china\">Finding out exactly what I received from China<\/h2>\n<p>Prior to today, I had done some cursory testing of a couple of the chips in order to prove a few points about how many I could integrate into a single system. But I still didn't know if all of the chips I have are real and\/or functional.<\/p>\n<p>Yesterday I did some further testing, but I realised my testing was not thorough enough and I came to the incorrect conclusion that the majority of the newly acquired chips were faulty. We'll fix that today.<\/p>\n<h2 id=\"reference-one-vintage-genuine-chip-i-already-had\">Reference: One vintage genuine chip I already had<\/h2>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;DSC08333.37952c64075f80e2.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;DSC08332.f79e477a5f06103d.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>This chip is characterised by:<\/p>\n<ul>\n<li>Fully glossy package<\/li>\n<li>Top: Printed \"SOUND\" \/ \"AY-3-8910\" \/ \"GI\" \/ \"8309\" \"P\"; No embossing<\/li>\n<li>Bottom: Printed \"C32033-100D\"; \"47\" mould embossing on left side<\/li>\n<\/ul>\n<p>This is a legitimate General Instruments device, probably manufactured in September 1983. It works perfectly with both a 5V and 3.3V supply.<\/p>\n<h2 id=\"china-shipment\">China shipment<\/h2>\n<blockquote>\n<p><em>Spoiler alert: I've written the test results on the bottom of each chip in pencil.<\/em><\/p>\n<\/blockquote>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;DSC08331.5f08ece30d055098.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Shown all here with my GI chip also on the lower right side for comparison.<\/p>\n<p>Top sides:<\/p>\n<ul>\n<li>All have been lasered or sanded and re-printed.<\/li>\n<li>None have any embossed mould markings.<\/li>\n<li>Three have an additional indent for pin 1.<\/li>\n<\/ul>\n<p>The bottom sides vary a lot and is best summarised thus:<\/p>\n<table><thead><tr><th>Chip #<\/th><th>Top side<br>Pin 1 mark<\/th><th>Bottom left<br>mould mark<\/th><th>Bottom right<br>mould mark<\/th><th>Bottom etching<\/th><\/tr><\/thead><tbody>\n<tr><td>1<\/td><td>No<\/td><td>-<\/td><td>-<\/td><td>-<\/td><\/tr>\n<tr><td>2<\/td><td>No<\/td><td>-<\/td><td>-<\/td><td>-<\/td><\/tr>\n<tr><td>3<\/td><td>No<\/td><td>-<\/td><td>-<\/td><td>-<\/td><\/tr>\n<tr><td>4<\/td><td>No<\/td><td>-<\/td><td>-<\/td><td>-<\/td><\/tr>\n<tr><td>5<\/td><td>No<\/td><td><code>8 14<\/code> <br> (Indistinct)<\/td><td>-<\/td><td>-<\/td><\/tr>\n<tr><td>6<\/td><td>No<\/td><td><code>C2<\/code><\/td><td><code>CJ<\/code><\/td><td><\/td><\/tr>\n<tr><td>7<\/td><td>No<\/td><td>Something <br> unreadable<\/td><td><code>G3<\/code><\/td><td><code>AH1PE1.1<\/code><\/td><\/tr>\n<tr><td>8<\/td><td>Yes<\/td><td>-<\/td><td><code>TN D6<\/code><\/td><td><code>AA1KN51C2<\/code><\/td><\/tr>\n<tr><td>9<\/td><td>Yes<\/td><td>-<\/td><td><code>TN K3<\/code><\/td><td><code>A98CE612C<\/code><\/td><\/tr>\n<tr><td>10<\/td><td>Yes<\/td><td><code>5 E3<\/code><\/td><td><code>TAIWAN<\/code><\/td><td>-<\/td><\/tr>\n<\/tbody><\/table>\n<p>All of these chips' pins were quite mucky or heavily tarnished.<\/p>\n<h2 id=\"yesterday-s-test-results\">Yesterday's test results<\/h2>\n<p>I tested all the chips yesterday with dirty pins and with only a 3.3V supply. In this setup, only one of the chips worked perfectly and another was mostly working but a bit intermittent. I pretty much accepted this as the result, given that I was comparing with my GI chip which also works just fine on 3.3V.<\/p>\n<p>However, today started looking to see if I could source a few more chips. I have in fact placed another order for a batch of chips earlier today <em>before<\/em> it occurred to me that my test method may have been <em>flawed<\/em>.<\/p>\n<p>Nevertheless, we can do better with testing the chips I have here, and I look forward to receiving perhaps a few more and testing those as well.<\/p>\n<h2 id=\"preparation-for-another-test-round\">Preparation for another test round<\/h2>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;DSC08328.b9548d7020e3a1b9.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;DSC08330.e4c0609b8bceadb8.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>Its not super clear to see in these photos, but this is a before and after of giving the legs a light filing to remove the tarnish and other dirt.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;DSC08319.7c8cdcc53637866d.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>After that, a quick leg straightening and alignment and each is ready to be put in the breadboard for testing.<\/p>\n<p>It was my intention to power the entire Fourays synth system from the 3.3V supply only. It turns out this is not entirely possible given yesterday's results. So, I also tested the chips with the recommended 5V supply.<\/p>\n<h2 id=\"results\">Results<\/h2>\n<p>The only chip which is does not fully work is #5. It makes some crackly clicking noises when I request tones, but it is not making pure tones like it ought to.<\/p>\n<p><strong>All of the others work on a 5V supply. Chip #10 additionally works on a 3.3V supply.<\/strong><\/p>\n<p>I have my suspicions that chip #10 <em>may<\/em> be a later <code>YM2149F<\/code> or <code>YM3439-D<\/code> (CMOS) if it works reliably on 3.3V. I could test this by poking pin 26 to see if it drops an octave. In fact, all of the chips could be submitted to this test, it may be insightful. (But that doesn't explain why my GI chip works on 3.3V, that one is most definitely an old AY).<\/p>\n<p>So, what do I do from here? I have to amend the schematic and PCB layouts a bit to send 5V to each AY chip, but I'm super happy that I do really have quite a few working chips now. This will be helpful if\/when I decide to actually get PCBs made, as economy of scale will kick in and I could perhaps sell a couple of complete Fourays for a profit. In fact, doing this may well incentivise me to put in the effort with physical design and assembly to truly make this project the tribute to the AY that it claims to be.<\/p>\n<p>The project continues ... \ud83d\ude04<\/p>\n"},{"title":"Fourays: A Tribute to the AY-3-8910","published":"2024-01-30T00:00:00+00:00","updated":"2024-01-30T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/fourays\/tribute\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/fourays\/tribute\/","content":"<blockquote>\n<p><em>UPDATE: this post was originally much shorter and published as \"Part 1\" on 30 Jan 2024, but I've since (10 Feb 2024) added more content to match the current state of the project as a whole. Further progress on this project will be made as new posts however.<\/em><\/p>\n<\/blockquote>\n<blockquote>\n<p><em>UPDATE: 18 &amp; 19 Feb 2024: I tested all of the AY chips.<\/em><\/p>\n<\/blockquote>\n<h2 id=\"what-is-the-ay-3-8910\">What is the AY-3-8910?<\/h2>\n<p>I'm quite a fan of the old <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/General_Instrument_AY-3-8910\">AY-3-8910<\/a> synthesizer chip. I've used one before in the <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/psc\/\">psc<\/a>, but wondered what a fully AY-only synth might be like.<\/p>\n<p>I only had 2 of these chips until recently, one for the <code>psc<\/code> and another ... spare? I didn't want to disassemble the <code>psc<\/code> to make the next project so looked at acquiring some more. I found a listing on AliExpress for a lot of 10 for just \u00a37. I wasn't sure if this was a legitimate listing, but decided the reasonable price was worth a gamble. A while later, a package arrived from China and inside was 10 chips correctly labelled as <code>AY-3-8910<\/code>.<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PXL_20240131_111954995.MP.f614cd3bf21ccb89.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PXL_20240131_112050546.adfcdd0fda29bcd5.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>This was a nice start, but I still wasn't convinced that these were real chips as they look <em>very<\/em> clean and new and I was sure these chips haven't been manufactured for a long while. These chips also all look somewhat different from each other. The print markings quality varies and the mouldings and markings on the undersides differ as well.<\/p>\n<p>So, I picked the first chip off the stack, rigged up a clone of the <code>psc<\/code> code with another ESP32-S3, hooked up the control lines and waited to see if the chip actually behaved like an AY. It turns out at least two of them are totally real but I haven't tested all of them yet. So, we are good to go with planning an entirely AY synthesizer with at least 2 chips.<\/p>\n<blockquote>\n<p><em>UPDATE 19 Feb 2024<\/em>: I've revised my testing and have some more details about the chips <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/fourays\/chip-testing\/\">here<\/a>.<\/p>\n<\/blockquote>\n<p>By comparison here's the new chips next to one of my old vintage ones, marked GI for General Instruments:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PXL_20240131_111654019.MP.098840dbb06450b8.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"limitations-of-the-ay\">Limitations of the AY<\/h2>\n<p>The AY chips produce only square waves. This is a good start for chip-tune vibes but I think this synth could do with a bit more flavour. For my analogue synth I have built some Voltage Controlled Filters and like how they can modify the AY sound, so I think we could make some more VCFs and maybe have one per AY output.<\/p>\n<p>Each AY has 3 oscillators and a noise generator; but it has only 3 audio outputs. Each oscillator is allocated to its own output, but the noise generator can be mixed into any or all of the outputs. So, we don't have as much control over the noise generator as we do the oscillators, but I think we can work with that. I think we should mix all of the audio outputs together into 1 or 2 outputs for the synth anyway.<\/p>\n<h2 id=\"the-multi-chip-chip-tune-synth\">The multi-chip chip-tune synth<\/h2>\n<p>Three oscillators is nice, but because I have loads of these chips now, it begs the question of exactly how many can be made to work together. Each AY chip requires the following input signals:<\/p>\n<ul>\n<li>1x clock<\/li>\n<li>8x data lines<\/li>\n<li>1x bus direction line<\/li>\n<li>1x bus control line<\/li>\n<\/ul>\n<p>It turns out that for multiple chips under the control of the same microcontroller, the clock, data lines, and bus direction line can be shared to all of the chips. Only the bus control line is unique per chip and acts like a traditional chip select line. So, the total number of microcontroller outputs we need for <code>N<\/code> AY chips is <code>10 + N<\/code>.<\/p>\n<p>With the idea of a filter per audio output, we will also need control voltages for each filter. We therefore need <code>3N<\/code> control analogue voltages. Most microcontrollers don't have very many usable DAC outputs, and usually they are quite low resolution, so we will probably need some dedicated DAC devices attached to the microcontroller. I've settled on using <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.analog.com\/en\/products\/ad5328.html\">AD5328<\/a> devices. These use a 3-wire SPI interface but provide 8 analogue voltages each with 12-bit resolution. Each additional DAC requires only one more chip select signal from the microcontroller.<\/p>\n<p>With the ESP32-S3 microcontroller acting as a USB device, it has <em>24<\/em> other usable output pins.<\/p>\n<p>So, lets see how many microcontroller pins we need for some <code>N<\/code> of AY chips:<\/p>\n<table><thead><tr><th>N<\/th><th>AY control<br>pins<\/th><th>Audio outputs\/<br>VCFs<\/th><th>DACs<\/th><th>DAC control<br>pins<\/th><th><strong>TOTAL<br>pins<\/strong><\/th><\/tr><\/thead><tbody>\n<tr><td>1<\/td><td>11<\/td><td>3<\/td><td>1<\/td><td>3<\/td><td>14<\/td><\/tr>\n<tr><td>2<\/td><td>12<\/td><td>6<\/td><td>1<\/td><td>3<\/td><td>15<\/td><\/tr>\n<tr><td>3<\/td><td>13<\/td><td>9<\/td><td>2<\/td><td>4<\/td><td>17<\/td><\/tr>\n<tr><td>4<\/td><td>14<\/td><td>12<\/td><td>2<\/td><td>4<\/td><td>18<\/td><\/tr>\n<tr><td>5<\/td><td>15<\/td><td>15<\/td><td>2<\/td><td>4<\/td><td>19<\/td><\/tr>\n<tr><td>6<\/td><td>16<\/td><td>18<\/td><td>3<\/td><td>5<\/td><td>21<\/td><\/tr>\n<tr><td>7<\/td><td>17<\/td><td>21<\/td><td>3<\/td><td>5<\/td><td>22<\/td><\/tr>\n<tr><td>8<\/td><td>18<\/td><td>24<\/td><td>3<\/td><td>5<\/td><td>23<\/td><\/tr>\n<\/tbody><\/table>\n<p>OK, so we could hook up 8 AY chips for a total of 24 oscillators and 8 noise generators and 24 VCFs. That's a tempting idea, but that's perhaps too many and leaves no scope for additional I\/O on the microcontroller.<\/p>\n<p>I like the idea of having a little screen for configuration and status display, and we also will need some touch or button inputs to control the user interface. I will use the same ST7735s type screen as I used for the <code>psc<\/code> and this shares the SPI bus with the DACs, but needs 3 additional control lines. For user interface inputs, I think we need a minimum of 3 touch or button inputs. That's a total of 6 more pins for user interface. If we subtract that from the 24 available, that leaves 18 remaining for the audio circuits.<\/p>\n<p>So, we are able to use 4 AYs ... four AYs ... <strong><em>Fourays<\/em><\/strong> ... <strong><em><span style=\"font-size: 2.6rem;\">FOURAYS!<\/span><\/em><\/strong><\/p>\n<p>And with that, we will have used <em>every usable pin<\/em> on the ESP32-S3 and found a name for the project!<\/p>\n<h3 id=\"microcontroller-hookup\">Microcontroller hookup<\/h3>\n<p>And with that lets start allocating pins on the ESP32-S3:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PXL_20240131_111449586.d841b6a24bd5079e.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;fourays-esp-hookup.7ff50836c566283b.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p><strong>NOTE:<\/strong> <em>Pin assignments have changed since this article was written, you should refer to the official <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/fourays.lon.dev\">Fourays documentation<\/a> or <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/fourays\/\">repository<\/a> for current references<\/em>.<\/p>\n<\/blockquote>\n<p>This consists of:<\/p>\n<ul>\n<li>3x touch inputs<\/li>\n<li>8x AY data bus lines<\/li>\n<li>1x AY clock<\/li>\n<li>1x AY bus direction line<\/li>\n<li>4x AY bus control \/ chip select lines<\/li>\n<li>2x SPI bus lines (clock &amp; data)<\/li>\n<li>2x DAC chip select lines<\/li>\n<li>3x screen control lines<\/li>\n<li>2x USB data lines<\/li>\n<\/ul>\n<blockquote>\n<p>There are technically 8 more pins exposed, but for various reasons we cannot use these. Four influence the ESP startup (\"strapping\") and these are best avoided. Three interface with a built in peripheral, the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/docs.espressif.com\/projects\/esp-idf\/en\/latest\/esp32\/api-guides\/external-ram.html\">PSRAM<\/a>. The last one interfaces with an on-board RGB LED, which we don't need.<\/p>\n<\/blockquote>\n<p>It should be noted as well that these pin choices are not arbitrary, this image comes from the future relative to this introductory post, having already tested and established which pins suit which functions. For example the touch inputs are only available on the first 14 GPIOs.<\/p>\n<h3 id=\"ay-hookup\">AY hookup<\/h3>\n<p>The AY chips were originally designed to be used with 11 control lines and 1 clock signal. However, it has been established that fewer than that is strictly necessary. I've used one reduced pin scheme in the <code>psc<\/code> design, but I've found a different one to use here, which saves another pin. I got this info from <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/dogemicrosystems.ca\/wiki\/Dual_AY-3-8910_MIDI_module\">dogemicrosystems<\/a>.<\/p>\n<p>Each AY chip hookup therefore looks like this:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;fourays-ay-hookup.52295b3e9cd7e070.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>One thing I haven't mentioned yet is that each AY also has two 8-bit GPIO ports incorporated. I haven't made use of these in the design so far, but it could be considered as an alternative way to control the filters if we run into limitations with the microcontroller or DACs.<\/p>\n<p>By setting these GPIO as outputs, we could implement an <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Resistor_ladder#R%E2%80%932R_resistor_ladder_network_(digital_to_analog_conversion)\">R-2R resistor ladder DAC<\/a>. If we want high precision we would have to sacrifice the number of VCFs, perhaps have one per AY chip instead of one per audio output, or we could divide the GPIOs into multiple DACs with lower resolution. If we want to maintain one VCF per audio output, we'd only have 5 bits available per VCF control voltage, reducing the resolution quite significantly (65536 levels to just 32 levels).<\/p>\n<p>Actually, driving the VCF control voltages from the AY itself would have the small advantage of ensuring that filter changes happen much closer in time as the note changes on the audio outputs, since the microcontroller would be setting both of these over the same bus in quick succession. I don't think the timing alignment will be as good using DAC devices, however I expect in practice the timing differences will not be audible at all, because we can drive either bus at MHz frequencies.<\/p>\n<p>If I don't use these GPIOs for anything else, I might just hook up some LEDs as blinkenlights.<\/p>\n<p>One final thing to note is that although the AY is specified as requiring a 5 volt supply, I have been able to power it successfully from the ESP32-S3 3.3 volt supply and it still outputs tones and at the correct frequencies. So, that should simplify the power requirements for the synth as well.<\/p>\n<h3 id=\"vcf-filter-design\">VCF filter design<\/h3>\n<p>The final circuit type to consider in this synth is the voltage controlled filter. I have simply lifted someone else's design for this, namely the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/kassu2000.blogspot.com\/2018\/07\/transistor-ladder-filter.html\">kassutronics transistor ladder filter<\/a>. This design is very close to the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.yusynth.net\/Modular\/EN\/MOOGVCF\/index.html\">YuSynth Minimoog VCF<\/a> which I made before for my analogue synth. I like the sound of that filter, but the kassutronics version has the advantage of not requiring CA3046 chips which are no longer available.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;kassutronics-vcf-schematic.4188196cd59e549b.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"setting-design-goals\">Setting Design Goals<\/h2>\n<p>I think all of the above makes a sensible plan for a synthesizer. We will be able to:<\/p>\n<ul>\n<li>use the ESP32-S3 as a USB MIDI device<\/li>\n<li>map MIDI channels\/notes to AY oscillators<\/li>\n<li>map MIDI CC controllers to filter cut-off frequencies<\/li>\n<\/ul>\n<p>We can mix the audio voices together and configure the synth to have either 1 output for mono or 2 outputs for stereo. We can decide later on the MIDI configuration and audio mixing strategy though.<\/p>\n<h2 id=\"esp32-s3-firmware\">ESP32-S3 Firmware<\/h2>\n<p>In order to have full control over the ESP32-S3 as a USB device, I am going to use <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/docs.espressif.com\/projects\/esp-idf\">ESP-IDF<\/a> framework.<\/p>\n<p>The firmware will be structured into several units which are mostly loosely coupled:<\/p>\n<ul>\n<li><code>board<\/code> : just a bunch of constants which define the peripheral pinouts.<\/li>\n<li><code>usb<\/code> : initialises the USB interface, device descriptors.<\/li>\n<li><code>midi<\/code> : initialises a MIDI interface which consists of bridging the ESP-IDF USB MIDI low level API to a MIDI packet interpreter. Exposes a set of <code>signal<\/code> objects which can be observed for different MIDI message types.<\/li>\n<li><code>config<\/code> : describes how to map MIDI messages to device peripherals, and an API to change these mappings.<\/li>\n<li><code>touch<\/code> : watches the touch input pins and exposes a touch <code>signal<\/code> when touch events occur.<\/li>\n<li><code>ay<\/code> : AY-3-8910 device driver.<\/li>\n<li><code>dac<\/code> : DAC device driver.<\/li>\n<li><code>tft<\/code> : LCD screen graphics rendering functions.<\/li>\n<\/ul>\n<h2 id=\"signals\">Signals<\/h2>\n<p>I'm able to loosely couple most of these units using the <code>signal<\/code> pattern. A <code>Signal<\/code> is an object which can be observed and which can be called upon to emit events.<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">#include <\/span><span>&lt;<\/span><span style=\"color:#a3be8c;\">signals.h<\/span><span>&gt;\n<\/span><span>\n<\/span><span>Signal&lt;&gt; demoSignal;\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">onSignal_1<\/span><span>() {}\n<\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">onSignal_2<\/span><span>() {}\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">int <\/span><span style=\"color:#8fa1b3;\">main<\/span><span>() {\n<\/span><span>  demoSignal.<\/span><span style=\"color:#bf616a;\">emit<\/span><span>(); <\/span><span style=\"color:#65737e;\">\/\/ No observers, nothing happens\n<\/span><span>\n<\/span><span>  demoSignal.<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(onSignal_1); <\/span><span style=\"color:#65737e;\">\/\/ Registered one callback for the signal\n<\/span><span>\n<\/span><span>  demoSignal.<\/span><span style=\"color:#bf616a;\">emit<\/span><span>(); <\/span><span style=\"color:#65737e;\">\/\/ onSignal_1 is called\n<\/span><span>\n<\/span><span>  demoSignal.<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(onSignal_2); <\/span><span style=\"color:#65737e;\">\/\/ Registered another callback for the signal\n<\/span><span>\n<\/span><span>  demoSignal.<\/span><span style=\"color:#bf616a;\">emit<\/span><span>(); <\/span><span style=\"color:#65737e;\">\/\/ onSignal_1 and onSignal_2 are called\n<\/span><span>\n<\/span><span>  <\/span><span style=\"color:#b48ead;\">return <\/span><span style=\"color:#d08770;\">0<\/span><span>;\n<\/span><span>}\n<\/span><\/code><\/pre>\n<h2 id=\"units\">Units<\/h2>\n<p>Lets look at some of the units in a bit more detail. But first, what do I mean by \"unit\"? Practically speaking each unit so far is just a pair of <code>.h<\/code>\/<code>.cpp<\/code> files (literally a C++ translation unit). Inside each unit I encapsulate the interface into a namespace. Most units expose a <code>setup()<\/code> function and perhaps some signals.<\/p>\n<p><code>example-unit.h<\/code><\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">namespace <\/span><span>fourays::example\n<\/span><span>{\n<\/span><span>  Signal&lt;&gt; &amp;<\/span><span style=\"color:#8fa1b3;\">exampleSignalGetter<\/span><span>();\n<\/span><span>\n<\/span><span>  <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">setup<\/span><span>();\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p><code>example-unit.cpp<\/code><\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">#include <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">example-unit.h<\/span><span>&quot;\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">namespace <\/span><span>fourays::example\n<\/span><span>{\n<\/span><span>  Signal&lt;&gt; s_example;\n<\/span><span>\n<\/span><span>  Signal&lt;&gt; &amp;<\/span><span style=\"color:#8fa1b3;\">exampleSignalGetter<\/span><span>()\n<\/span><span>  {\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">return<\/span><span> s_example;\n<\/span><span>  }\n<\/span><span>\n<\/span><span>  <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">setup<\/span><span>()\n<\/span><span>  {\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/\/ do some setup for example\n<\/span><span>  }\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>Note that in the descriptions of the following units I am by no means showing all of the code required, but it's worth showing some of the important parts which give the units their core functionality. A link to the actual code repo for fourays firmware will be published as open source in due course, once I've got all these units working nicely together (more on this later).<\/p>\n<h3 id=\"unit-usb\">Unit: usb<\/h3>\n<p>This is a minor unit which initialises the <code>tinyusb<\/code> stack with my own device descriptor information and to initialise the USB configuration as a MIDI device.<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">#include <\/span><span>&lt;<\/span><span style=\"color:#a3be8c;\">tinyusb.h<\/span><span>&gt;\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">#include <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">fourays-usb.h<\/span><span>&quot;\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">namespace <\/span><span>fourays::usb\n<\/span><span>{\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/** TinyUSB descriptors **\/\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/\/ Interface counter\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">enum <\/span><span>interface_count\n<\/span><span>    {\n<\/span><span style=\"color:#b48ead;\">#if<\/span><span> CFG_TUD_MIDI\n<\/span><span>        ITF_NUM_MIDI = <\/span><span style=\"color:#d08770;\">0<\/span><span>,\n<\/span><span>        ITF_NUM_MIDI_STREAMING,\n<\/span><span style=\"color:#b48ead;\">#endif\n<\/span><span>        ITF_COUNT\n<\/span><span>    };\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/\/ USB Endpoint numbers\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">enum <\/span><span>usb_endpoints\n<\/span><span>    {\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Available USB Endpoints: 5 IN\/OUT EPs and 1 IN EP\n<\/span><span>        EP_EMPTY = <\/span><span style=\"color:#d08770;\">0<\/span><span>,\n<\/span><span style=\"color:#b48ead;\">#if<\/span><span> CFG_TUD_MIDI\n<\/span><span>        EPNUM_MIDI,\n<\/span><span style=\"color:#b48ead;\">#endif\n<\/span><span>    };\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">#define <\/span><span>TUSB_DESCRIPTOR_TOTAL_LEN (TUD_CONFIG_DESC_LEN + CFG_TUD_MIDI * TUD_MIDI_DESC_LEN)\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/**\n<\/span><span style=\"color:#65737e;\">     * @brief String descriptor\n<\/span><span style=\"color:#65737e;\">     *\/\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">static const char <\/span><span>*s_str_desc[<\/span><span style=\"color:#d08770;\">5<\/span><span>] = {\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ array of pointer to string descriptors\n<\/span><span>        (<\/span><span style=\"color:#b48ead;\">char<\/span><span>[]){<\/span><span style=\"color:#d08770;\">0x09<\/span><span>, <\/span><span style=\"color:#d08770;\">0x04<\/span><span>}, <\/span><span style=\"color:#65737e;\">\/\/ 0: is supported language is English (0x0409)\n<\/span><span>        &quot;<\/span><span style=\"color:#a3be8c;\">lon.dev<\/span><span>&quot;,            <\/span><span style=\"color:#65737e;\">\/\/ 1: Manufacturer\n<\/span><span>        &quot;<\/span><span style=\"color:#a3be8c;\">fourays<\/span><span>&quot;,            <\/span><span style=\"color:#65737e;\">\/\/ 2: Product\n<\/span><span>        &quot;<\/span><span style=\"color:#a3be8c;\">123456<\/span><span>&quot;,             <\/span><span style=\"color:#65737e;\">\/\/ 3: Serials, should use chip ID\n<\/span><span>        &quot;<\/span><span style=\"color:#a3be8c;\">fourays<\/span><span>&quot;,            <\/span><span style=\"color:#65737e;\">\/\/ 4: MIDI\n<\/span><span>    };\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/**\n<\/span><span style=\"color:#65737e;\">     * @brief Configuration descriptor\n<\/span><span style=\"color:#65737e;\">     *\n<\/span><span style=\"color:#65737e;\">     * This is a simple configuration descriptor that defines 1 configuration and a MIDI interface\n<\/span><span style=\"color:#65737e;\">     *\/\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">static const <\/span><span>uint8_t s_midi_cfg_desc[] = {\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Configuration number, interface count, string index, total length, attribute, power in mA\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">TUD_CONFIG_DESCRIPTOR<\/span><span>(<\/span><span style=\"color:#d08770;\">1<\/span><span>, ITF_COUNT, <\/span><span style=\"color:#d08770;\">0<\/span><span>, TUSB_DESCRIPTOR_TOTAL_LEN, <\/span><span style=\"color:#d08770;\">0<\/span><span>, <\/span><span style=\"color:#d08770;\">100<\/span><span>),\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Interface number, string index, EP Out &amp; EP In address, EP size\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">TUD_MIDI_DESCRIPTOR<\/span><span>(ITF_NUM_MIDI, <\/span><span style=\"color:#d08770;\">4<\/span><span>, EPNUM_MIDI, (<\/span><span style=\"color:#d08770;\">0x80 <\/span><span>| EPNUM_MIDI), <\/span><span style=\"color:#d08770;\">64<\/span><span>),\n<\/span><span>    };\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">setup<\/span><span>()\n<\/span><span>    {\n<\/span><span>        tinyusb_config_t <\/span><span style=\"color:#b48ead;\">const<\/span><span> tusb_cfg = {\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">device_descriptor <\/span><span>= <\/span><span style=\"color:#d08770;\">NULL<\/span><span>, <\/span><span style=\"color:#65737e;\">\/\/ If device_descriptor is NULL, tinyusb_driver_install() will use Kconfig\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">string_descriptor <\/span><span>= s_str_desc,\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">string_descriptor_count <\/span><span>= sizeof(s_str_desc) \/ sizeof(s_str_desc[<\/span><span style=\"color:#d08770;\">0<\/span><span>]),\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">external_phy <\/span><span>= <\/span><span style=\"color:#d08770;\">false<\/span><span>,\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">configuration_descriptor <\/span><span>= s_midi_cfg_desc,\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">self_powered <\/span><span>= <\/span><span style=\"color:#d08770;\">false<\/span><span>,\n<\/span><span>            .<\/span><span style=\"color:#bf616a;\">vbus_monitor_io <\/span><span>= -<\/span><span style=\"color:#d08770;\">1<\/span><span>,\n<\/span><span>        };\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">ESP_ERROR_CHECK<\/span><span>(<\/span><span style=\"color:#bf616a;\">tinyusb_driver_install<\/span><span>(&amp;tusb_cfg));\n<\/span><span>    }\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>It seems like it is possible to define multiple distinct MIDI interfaces in the USB configuration should we need to, however for now I think we only need one and I also cannot find any way to know which interface received which data when we want to read it later on.<\/p>\n<h3 id=\"unit-midi\">Unit: midi<\/h3>\n<p>In this unit I have wrapped the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/FortySevenEffects\/arduino_midi_library\/tree\/master\/src\">Arduino MIDI Library<\/a>. This library is actually not Arduino framework specific, but is templated to allow integrating with components of other frameworks. In particular, we need to provide an implementation for something called <code>Transport<\/code> and something called a <code>Platform<\/code>.<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">template<\/span><span>&lt;<\/span><span style=\"color:#b48ead;\">class<\/span><span> Transport, <\/span><span style=\"color:#b48ead;\">class<\/span><span> _Settings = DefaultSettings, <\/span><span style=\"color:#b48ead;\">class<\/span><span> _Platform = DefaultPlatform&gt;\n<\/span><span style=\"color:#b48ead;\">class <\/span><span style=\"color:#ebcb8b;\">MidiInterface\n<\/span><span style=\"color:#eff1f5;\">{\n<\/span><span style=\"color:#65737e;\">\/\/ --- snip ---\n<\/span><\/code><\/pre>\n<p>The <code>Transport<\/code> implements the interface to read and write data to the physical interface, that is to say the <code>tinyusb<\/code> USB MIDI interface:<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">#include <\/span><span>&lt;<\/span><span style=\"color:#a3be8c;\">tinyusb.h<\/span><span>&gt;\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">class <\/span><span style=\"color:#ebcb8b;\">TUSBTransport\n<\/span><span style=\"color:#eff1f5;\">{\n<\/span><span style=\"color:#b48ead;\">public<\/span><span style=\"color:#eff1f5;\">:\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">begin<\/span><span style=\"color:#eff1f5;\">() {}\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#b48ead;\">unsigned <\/span><span style=\"color:#8fa1b3;\">available<\/span><span style=\"color:#eff1f5;\">()\n<\/span><span style=\"color:#eff1f5;\">    {\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#b48ead;\">return <\/span><span style=\"color:#bf616a;\">tud_midi_available<\/span><span style=\"color:#eff1f5;\">();\n<\/span><span style=\"color:#eff1f5;\">    }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">    byte <\/span><span style=\"color:#8fa1b3;\">read<\/span><span style=\"color:#eff1f5;\">()\n<\/span><span style=\"color:#eff1f5;\">    {\n<\/span><span style=\"color:#eff1f5;\">        uint8_t ch;\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#b48ead;\">return <\/span><span style=\"color:#bf616a;\">tud_midi_stream_read<\/span><span style=\"color:#eff1f5;\">(<\/span><span>&amp;<\/span><span style=\"color:#eff1f5;\">ch, <\/span><span style=\"color:#d08770;\">1<\/span><span style=\"color:#eff1f5;\">) <\/span><span>? <\/span><span style=\"color:#eff1f5;\">(<\/span><span style=\"color:#b48ead;\">int<\/span><span style=\"color:#eff1f5;\">)ch <\/span><span>: <\/span><span style=\"color:#eff1f5;\">(<\/span><span>-<\/span><span style=\"color:#d08770;\">1<\/span><span style=\"color:#eff1f5;\">);\n<\/span><span style=\"color:#eff1f5;\">    }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#b48ead;\">bool <\/span><span style=\"color:#8fa1b3;\">beginTransmission<\/span><span style=\"color:#eff1f5;\">(<\/span><span style=\"color:#b48ead;\">int <\/span><span style=\"color:#bf616a;\">val<\/span><span style=\"color:#eff1f5;\">)\n<\/span><span style=\"color:#eff1f5;\">    {\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#65737e;\">\/\/ nothing to do here\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#b48ead;\">return <\/span><span style=\"color:#d08770;\">false<\/span><span style=\"color:#eff1f5;\">;\n<\/span><span style=\"color:#eff1f5;\">    }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">write<\/span><span style=\"color:#eff1f5;\">(<\/span><span style=\"color:#b48ead;\">int <\/span><span style=\"color:#bf616a;\">val<\/span><span style=\"color:#eff1f5;\">)\n<\/span><span style=\"color:#eff1f5;\">    {\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#65737e;\">\/\/ nothing to do here\n<\/span><span style=\"color:#eff1f5;\">    }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">endTransmission<\/span><span style=\"color:#eff1f5;\">()\n<\/span><span style=\"color:#eff1f5;\">    {\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#65737e;\">\/\/ nothing to do here\n<\/span><span style=\"color:#eff1f5;\">    }\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#b48ead;\">public<\/span><span style=\"color:#eff1f5;\">:\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#b48ead;\">bool<\/span><span style=\"color:#eff1f5;\"> thruActivated <\/span><span>= <\/span><span style=\"color:#d08770;\">false<\/span><span style=\"color:#eff1f5;\">;\n<\/span><span style=\"color:#eff1f5;\">}<\/span><span>;\n<\/span><\/code><\/pre>\n<p><code>Platform<\/code> for some reason just needs a method to get the current timestamp:<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">#include <\/span><span>&lt;<\/span><span style=\"color:#a3be8c;\">esp_timer.h<\/span><span>&gt;\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">class <\/span><span style=\"color:#ebcb8b;\">ESPIDFPlatform\n<\/span><span style=\"color:#eff1f5;\">{\n<\/span><span style=\"color:#b48ead;\">public<\/span><span style=\"color:#eff1f5;\">:\n<\/span><span style=\"color:#eff1f5;\">    <\/span><span style=\"color:#b48ead;\">static unsigned long <\/span><span style=\"color:#8fa1b3;\">now<\/span><span style=\"color:#eff1f5;\">()\n<\/span><span style=\"color:#eff1f5;\">    {\n<\/span><span style=\"color:#eff1f5;\">        <\/span><span style=\"color:#b48ead;\">return <\/span><span style=\"color:#bf616a;\">esp_timer_get_time<\/span><span style=\"color:#eff1f5;\">() <\/span><span>\/ <\/span><span style=\"color:#d08770;\">1000<\/span><span style=\"color:#eff1f5;\">;\n<\/span><span style=\"color:#eff1f5;\">    }\n<\/span><span style=\"color:#eff1f5;\">}<\/span><span>;\n<\/span><\/code><\/pre>\n<p>Once we've got these defined, we can create our MIDI interface and use it as per the library documentation. I'm also using a FreeRTOS task to manage reading the MIDI data arriving and map it to some signals which this unit exposes.<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">namespace <\/span><span>fourays::midi\n<\/span><span>{\n<\/span><span>  <\/span><span style=\"color:#b48ead;\">static<\/span><span> TUSBTransport transport;\n<\/span><span>  <\/span><span style=\"color:#b48ead;\">static <\/span><span>::midi::MidiInterface&lt;TUSBTransport, ::midi::DefaultSettings, ESPIDFPlatform&gt; <\/span><span style=\"color:#8fa1b3;\">midi<\/span><span>(<\/span><span style=\"color:#bf616a;\">transport<\/span><span>);\n<\/span><span>\n<\/span><span>  <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">midi_task_read<\/span><span>(<\/span><span style=\"color:#b48ead;\">void <\/span><span>*<\/span><span style=\"color:#bf616a;\">arg<\/span><span>)\n<\/span><span>  {\n<\/span><span>      <\/span><span style=\"color:#b48ead;\">for <\/span><span>(;;)\n<\/span><span>      {\n<\/span><span>          <\/span><span style=\"color:#bf616a;\">vTaskDelay<\/span><span>(<\/span><span style=\"color:#d08770;\">1<\/span><span>);\n<\/span><span>          <\/span><span style=\"color:#b48ead;\">while <\/span><span>(midi.<\/span><span style=\"color:#bf616a;\">read<\/span><span>())\n<\/span><span>          {\n<\/span><span>              <\/span><span style=\"color:#b48ead;\">switch <\/span><span>(midi.<\/span><span style=\"color:#bf616a;\">getType<\/span><span>())\n<\/span><span>              {\n<\/span><span>              <\/span><span style=\"color:#b48ead;\">case <\/span><span>::midi::MidiType::Stop:\n<\/span><span>                  s_allNotesOff.<\/span><span style=\"color:#bf616a;\">emit<\/span><span>();\n<\/span><span>                  <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>              <\/span><span style=\"color:#b48ead;\">case <\/span><span>::midi::MidiType::SystemReset:\n<\/span><span>                  s_allNotesOff.<\/span><span style=\"color:#bf616a;\">emit<\/span><span>();\n<\/span><span>                  <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>              <\/span><span style=\"color:#b48ead;\">case <\/span><span>::midi::MidiType::NoteOn:\n<\/span><span>                  s_noteOn.<\/span><span style=\"color:#bf616a;\">emit<\/span><span>(midi.<\/span><span style=\"color:#bf616a;\">getChannel<\/span><span>(), midi.<\/span><span style=\"color:#bf616a;\">getData1<\/span><span>(), midi.<\/span><span style=\"color:#bf616a;\">getData2<\/span><span>());\n<\/span><span>                  <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>              <\/span><span style=\"color:#b48ead;\">case <\/span><span>::midi::MidiType::NoteOff:\n<\/span><span>                  s_noteOff.<\/span><span style=\"color:#bf616a;\">emit<\/span><span>(midi.<\/span><span style=\"color:#bf616a;\">getChannel<\/span><span>(), midi.<\/span><span style=\"color:#bf616a;\">getData1<\/span><span>(), midi.<\/span><span style=\"color:#bf616a;\">getData2<\/span><span>());\n<\/span><span>                  <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>              <\/span><span style=\"color:#b48ead;\">case <\/span><span>::midi::MidiType::ControlChange:\n<\/span><span>                  s_controlChange.<\/span><span style=\"color:#bf616a;\">emit<\/span><span>(midi.<\/span><span style=\"color:#bf616a;\">getChannel<\/span><span>(), midi.<\/span><span style=\"color:#bf616a;\">getData1<\/span><span>(), midi.<\/span><span style=\"color:#bf616a;\">getData2<\/span><span>());\n<\/span><span>                  <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>              <\/span><span style=\"color:#b48ead;\">default<\/span><span>:\n<\/span><span>                  <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>              }\n<\/span><span>          }\n<\/span><span>      }\n<\/span><span>  }\n<\/span><span>\n<\/span><span>  <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">setup<\/span><span>()\n<\/span><span>  {\n<\/span><span>    midi.<\/span><span style=\"color:#bf616a;\">begin<\/span><span>(MIDI_CHANNEL_OMNI);\n<\/span><span>    midi.<\/span><span style=\"color:#bf616a;\">turnThruOff<\/span><span>();\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">xTaskCreate<\/span><span>(midi_task_read, &quot;<\/span><span style=\"color:#a3be8c;\">midi_task_read<\/span><span>&quot;, <\/span><span style=\"color:#d08770;\">8 <\/span><span>* <\/span><span style=\"color:#d08770;\">1024<\/span><span>, <\/span><span style=\"color:#d08770;\">NULL<\/span><span>, <\/span><span style=\"color:#d08770;\">5<\/span><span>, <\/span><span style=\"color:#d08770;\">NULL<\/span><span>);\n<\/span><span>  }\n<\/span><span>}\n<\/span><\/code><\/pre>\n<h3 id=\"unit-touch\">Unit: touch<\/h3>\n<p>I found a good reference implementation of using touch inputs in the ESP-IDF documentation here:<\/p>\n<blockquote>\n<p><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/espressif\/esp-idf\/blob\/03414a15508036c8fc0f51642aed7a264e9527df\/examples\/peripherals\/touch_sensor\/touch_sensor_v2\/touch_pad_interrupt\/main\/tp_interrupt_main.c\">https:\/\/github.com\/espressif\/esp-idf\/blob\/03414a15508036c8fc0f51642aed7a264e9527df\/examples\/peripherals\/touch_sensor\/touch_sensor_v2\/touch_pad_interrupt\/main\/tp_interrupt_main.c<\/a><\/p>\n<\/blockquote>\n<p>I've used this almost verbatim, except for redefining my own buttons array and what happens inside the read task where I emit a signal when an input is activated:<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span>\n<\/span><span style=\"color:#b48ead;\">#define <\/span><span>TOUCH_BUTTON_NUM <\/span><span style=\"color:#d08770;\">3\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">const<\/span><span> touch_pad_t button[TOUCH_BUTTON_NUM] = {\n<\/span><span>        TOUCH_LEFT,\n<\/span><span>        TOUCH_RIGHT,\n<\/span><span>        TOUCH_SELECT,\n<\/span><span>    };\n<\/span><span>\n<\/span><span style=\"color:#65737e;\">\/\/ --- snip ---\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">using <\/span><span>TouchSignal = Signal&lt;touch_pad_t&gt;;\n<\/span><span>    TouchSignal s_touch;\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">touchsensor_read_task<\/span><span>(<\/span><span style=\"color:#b48ead;\">void <\/span><span>*<\/span><span style=\"color:#bf616a;\">pvParameter<\/span><span>)\n<\/span><span>    {\n<\/span><span style=\"color:#65737e;\">\/\/ --- snip ---\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">if <\/span><span>(evt.<\/span><span style=\"color:#bf616a;\">intr_mask <\/span><span>&amp; TOUCH_PAD_INTR_MASK_ACTIVE)\n<\/span><span>            {\n<\/span><span>                s_touch.<\/span><span style=\"color:#bf616a;\">emit<\/span><span>((touch_pad_t)evt.<\/span><span style=\"color:#bf616a;\">pad_num<\/span><span>);\n<\/span><span>            }\n<\/span><span style=\"color:#65737e;\">\/\/ --- snip ---\n<\/span><span>    }\n<\/span><\/code><\/pre>\n<h3 id=\"unit-ay\">Unit: ay<\/h3>\n<p>The AY unit exposes a simple interface for writing register values to an AY device:<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#65737e;\">\/\/ Libs\n<\/span><span style=\"color:#b48ead;\">#include <\/span><span>&lt;<\/span><span style=\"color:#a3be8c;\">AY38910.h<\/span><span>&gt;\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">namespace <\/span><span>fourays::ay\n<\/span><span>{\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">setup<\/span><span>();\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">writeRegisters<\/span><span>(<\/span><span style=\"color:#b48ead;\">const <\/span><span>uint8_t <\/span><span style=\"color:#bf616a;\">cs<\/span><span>, <\/span><span style=\"color:#b48ead;\">const<\/span><span> AY38910::DataFrame <\/span><span style=\"color:#bf616a;\">frm<\/span><span>);\n<\/span><span>\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>The <code>setup<\/code> function initialises the output pins:<\/p>\n<ul>\n<li>clock: using the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/docs.espressif.com\/projects\/esp-idf\/en\/v5.1\/esp32\/api-reference\/peripherals\/ledc.html\">LEDC PWM clock generator<\/a>.<\/li>\n<li>8-bit bus: using <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/docs.espressif.com\/projects\/esp-idf\/en\/v5.1\/esp32s2\/api-reference\/peripherals\/dedic_gpio.html\">Dedicated GPIO Bundle<\/a>. The advantage of doing this is that we can then write an 8-bit unsigned int to the bundle and the hardware will map each bit to the output pins in the correct order in a single function call. I think that's pretty neat.<\/li>\n<li>BDIR and chip select pins: as regular GPIO outputs.<\/li>\n<\/ul>\n<p>The <code>writeRegisters<\/code> function iterates over the <code>DataFrame<\/code> and writes each value to the corresponding AY register. The implementation of the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/dogemicrosystems.ca\/wiki\/Dual_AY-3-8910_MIDI_module#Code\">dogemicrosystems<\/a> addressing scheme looks like this:<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span>    <\/span><span style=\"color:#b48ead;\">enum class <\/span><span>BC2Control\n<\/span><span>    {\n<\/span><span>        INACTIVE = <\/span><span style=\"color:#d08770;\">0b00<\/span><span>,\n<\/span><span>        WRITE = <\/span><span style=\"color:#d08770;\">0b11<\/span><span>,\n<\/span><span>        LATCH = <\/span><span style=\"color:#d08770;\">0b10\n<\/span><span>    };\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">setControl<\/span><span>(uint8_t <\/span><span style=\"color:#bf616a;\">bc2<\/span><span>, BC2Control <\/span><span style=\"color:#bf616a;\">mode<\/span><span>)\n<\/span><span>    {\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ BC2 must be changed before BDIR\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">const auto<\/span><span> bc2val = (uint8_t)mode &amp; <\/span><span style=\"color:#d08770;\">0b01 <\/span><span>? <\/span><span style=\"color:#d08770;\">1 <\/span><span>: <\/span><span style=\"color:#d08770;\">0<\/span><span>;\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">gpio_set_level<\/span><span>((gpio_num_t)bc2, bc2val);\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">const auto<\/span><span> bdir = (uint8_t)mode &amp; <\/span><span style=\"color:#d08770;\">0b10 <\/span><span>? <\/span><span style=\"color:#d08770;\">1 <\/span><span>: <\/span><span style=\"color:#d08770;\">0<\/span><span>;\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">gpio_set_level<\/span><span>((gpio_num_t)AY_BDIR, bdir);\n<\/span><span>    }\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">setData<\/span><span>(<\/span><span style=\"color:#b48ead;\">unsigned char <\/span><span style=\"color:#bf616a;\">data<\/span><span>)\n<\/span><span>    {\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">dedic_gpio_bundle_write<\/span><span>(portD, <\/span><span style=\"color:#d08770;\">0xFF<\/span><span>, data);\n<\/span><span>    }\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">writeRegister<\/span><span>(uint8_t <\/span><span style=\"color:#bf616a;\">cs<\/span><span>, uint8_t <\/span><span style=\"color:#bf616a;\">reg<\/span><span>, uint8_t <\/span><span style=\"color:#bf616a;\">value<\/span><span>)\n<\/span><span>    {\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Ref: https:\/\/dogemicrosystems.ca\/wiki\/Dual_AY-3-8910_MIDI_module#Code\n<\/span><span>        uint8_t bc2 = <\/span><span style=\"color:#d08770;\">0<\/span><span>;\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">switch <\/span><span>(cs)\n<\/span><span>        {\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">case <\/span><span style=\"color:#d08770;\">0<\/span><span>:\n<\/span><span>            bc2 = AY_1_CS;\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">case <\/span><span style=\"color:#d08770;\">1<\/span><span>:\n<\/span><span>            bc2 = AY_2_CS;\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">case <\/span><span style=\"color:#d08770;\">2<\/span><span>:\n<\/span><span>            bc2 = AY_3_CS;\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">case <\/span><span style=\"color:#d08770;\">3<\/span><span>:\n<\/span><span>            bc2 = AY_4_CS;\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>        }\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Inactive (BDIR BC2 BC1 0 0 0)\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">setControl<\/span><span>(bc2, BC2Control::INACTIVE); <\/span><span style=\"color:#65737e;\">\/\/ BDIR, BC2\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Set register address\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">setData<\/span><span>(reg);\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ BAR (Latch) (BDIR BC2 BC1 1 0 0)\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">setControl<\/span><span>(bc2, BC2Control::LATCH); <\/span><span style=\"color:#65737e;\">\/\/ BDIR\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Inactive (BDIR BC2 BC1 0 0 0)\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">setControl<\/span><span>(bc2, BC2Control::INACTIVE); <\/span><span style=\"color:#65737e;\">\/\/ BDIR\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Set register contents\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">setData<\/span><span>(value);\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Write (BDIR BC2 BC1 1 1 0)\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">setControl<\/span><span>(bc2, BC2Control::WRITE); <\/span><span style=\"color:#65737e;\">\/\/ BC2, BDIR\n<\/span><span>\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Inactive (BDIR BC2 BC1 0 0 0)\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">setControl<\/span><span>(bc2, BC2Control::INACTIVE); <\/span><span style=\"color:#65737e;\">\/\/ BC2, BDIR\n<\/span><span>    }\n<\/span><\/code><\/pre>\n<p>What is not shown here is the <code>AY38910<\/code> mapper class which exposes a MIDI like interface but internally just updates the register states. We will use this later to receive events from the MIDI unit and update the state to send out to the AY chips.<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">class <\/span><span style=\"color:#ebcb8b;\">AY38910\n<\/span><span style=\"color:#eff1f5;\">{\n<\/span><span style=\"color:#b48ead;\">public<\/span><span style=\"color:#eff1f5;\">:\n<\/span><span style=\"color:#eff1f5;\">  <\/span><span style=\"color:#b48ead;\">bool <\/span><span style=\"color:#8fa1b3;\">allOff<\/span><span style=\"color:#eff1f5;\">();\n<\/span><span style=\"color:#eff1f5;\">  <\/span><span style=\"color:#b48ead;\">bool <\/span><span style=\"color:#8fa1b3;\">noteOn<\/span><span style=\"color:#eff1f5;\">(<\/span><span style=\"color:#b48ead;\">const <\/span><span style=\"color:#eff1f5;\">uint8_t <\/span><span style=\"color:#bf616a;\">channel<\/span><span style=\"color:#eff1f5;\">, <\/span><span style=\"color:#b48ead;\">const <\/span><span style=\"color:#eff1f5;\">uint8_t <\/span><span style=\"color:#bf616a;\">note<\/span><span style=\"color:#eff1f5;\">, <\/span><span style=\"color:#b48ead;\">const <\/span><span style=\"color:#eff1f5;\">uint8_t <\/span><span style=\"color:#bf616a;\">velocity<\/span><span style=\"color:#eff1f5;\">);\n<\/span><span style=\"color:#eff1f5;\">  <\/span><span style=\"color:#b48ead;\">bool <\/span><span style=\"color:#8fa1b3;\">noteOff<\/span><span style=\"color:#eff1f5;\">(<\/span><span style=\"color:#b48ead;\">const <\/span><span style=\"color:#eff1f5;\">uint8_t <\/span><span style=\"color:#bf616a;\">channel<\/span><span style=\"color:#eff1f5;\">);\n<\/span><span style=\"color:#eff1f5;\">  <\/span><span style=\"color:#b48ead;\">bool <\/span><span style=\"color:#8fa1b3;\">noiseOn<\/span><span style=\"color:#eff1f5;\">(<\/span><span style=\"color:#b48ead;\">const <\/span><span style=\"color:#eff1f5;\">uint8_t <\/span><span style=\"color:#bf616a;\">freq<\/span><span style=\"color:#eff1f5;\">, <\/span><span style=\"color:#b48ead;\">const <\/span><span style=\"color:#eff1f5;\">uint8_t <\/span><span style=\"color:#bf616a;\">output_mask<\/span><span style=\"color:#eff1f5;\">);\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#65737e;\">\/\/ --- snip ---\n<\/span><span style=\"color:#eff1f5;\">\n<\/span><span style=\"color:#b48ead;\">private<\/span><span style=\"color:#eff1f5;\">:\n<\/span><span style=\"color:#eff1f5;\">  DataFrame <\/span><span style=\"color:#bf616a;\">m_frame<\/span><span style=\"color:#eff1f5;\">;\n<\/span><span style=\"color:#eff1f5;\">}\n<\/span><\/code><\/pre>\n<h3 id=\"unit-dac\">Unit: dac<\/h3>\n<p>I could not find any existing libraries for interfacing with the AD5382 DACs. It turns out though that this is pretty simple, we combine the DAC channel number with the data and write it out over the SPI bus. Remember that we have 2 DAC devices (via <code>cs<\/code>) each with 8 channels (via <code>ch<\/code>), for a total of 16 outputs, of which we need only 12.<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">namespace <\/span><span>fourays::dac\n<\/span><span>{\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">writeDac<\/span><span>(<\/span><span style=\"color:#b48ead;\">const int <\/span><span style=\"color:#bf616a;\">cs<\/span><span>, <\/span><span style=\"color:#b48ead;\">const <\/span><span>uint16_t <\/span><span style=\"color:#bf616a;\">data<\/span><span>)\n<\/span><span>    {\n<\/span><span>        buf[<\/span><span style=\"color:#d08770;\">0<\/span><span>] = data &gt;&gt; <\/span><span style=\"color:#d08770;\">8<\/span><span>;\n<\/span><span>        buf[<\/span><span style=\"color:#d08770;\">1<\/span><span>] = data &amp; <\/span><span style=\"color:#d08770;\">0xFF<\/span><span>;\n<\/span><span>\n<\/span><span>        spi-&gt;<\/span><span style=\"color:#bf616a;\">mode<\/span><span>(<\/span><span style=\"color:#d08770;\">2<\/span><span>);\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">gpio_set_level<\/span><span>((gpio_num_t)cs, <\/span><span style=\"color:#d08770;\">0<\/span><span>); <\/span><span style=\"color:#65737e;\">\/\/ \/SYNC\n<\/span><span>        spi-&gt;<\/span><span style=\"color:#bf616a;\">beginTransaction<\/span><span>();\n<\/span><span>        spi-&gt;<\/span><span style=\"color:#bf616a;\">writeBytes<\/span><span>(buf, <\/span><span style=\"color:#d08770;\">2<\/span><span>, <\/span><span style=\"color:#d08770;\">false<\/span><span>, <\/span><span style=\"color:#d08770;\">true<\/span><span>);\n<\/span><span>        spi-&gt;<\/span><span style=\"color:#bf616a;\">endTransaction<\/span><span>();\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">gpio_set_level<\/span><span>((gpio_num_t)cs, <\/span><span style=\"color:#d08770;\">1<\/span><span>); <\/span><span style=\"color:#65737e;\">\/\/ \/SYNC\n<\/span><span>    }\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">writeDacOutput<\/span><span>(<\/span><span style=\"color:#b48ead;\">const int <\/span><span style=\"color:#bf616a;\">cs<\/span><span>, <\/span><span style=\"color:#b48ead;\">const <\/span><span>uint8_t <\/span><span style=\"color:#bf616a;\">ch<\/span><span>, <\/span><span style=\"color:#b48ead;\">const <\/span><span>uint16_t <\/span><span style=\"color:#bf616a;\">val<\/span><span>)\n<\/span><span>    {\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Page 16 &quot;DAC Write&quot; \/ Figure 34: Shift register contents\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ [RW, A2, A1, A0, d12...d0]\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ RW; 0=Write DAC value\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">const <\/span><span>uint16_t data = (ch &lt;&lt; <\/span><span style=\"color:#d08770;\">12<\/span><span>) | (val &amp; <\/span><span style=\"color:#d08770;\">0xFFF<\/span><span>);\n<\/span><span>        <\/span><span style=\"color:#bf616a;\">writeDac<\/span><span>(cs, data);\n<\/span><span>    }\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>This works because in the <code>setup<\/code> we configure the DACs for \"continuous update using SYNC\" mode<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">namespace <\/span><span>fourays::dac\n<\/span><span>{\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">setup<\/span><span>(psc::io::SPI_sptr <\/span><span style=\"color:#bf616a;\">bus<\/span><span>)\n<\/span><span>    {\n<\/span><span>        <\/span><span style=\"color:#65737e;\">\/\/ Manage the control pins manually\n<\/span><span>        <\/span><span style=\"color:#b48ead;\">for <\/span><span>(<\/span><span style=\"color:#b48ead;\">auto<\/span><span> pin : cs_gpios)\n<\/span><span>        {\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">gpio_set_level<\/span><span>((gpio_num_t)pin, <\/span><span style=\"color:#d08770;\">1<\/span><span>); <\/span><span style=\"color:#65737e;\">\/\/ CS \/SYNC is inverted\n<\/span><span>\n<\/span><span>            <\/span><span style=\"color:#65737e;\">\/\/ Page 18: Gain, BUF and Vdd config\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">const <\/span><span>uint16_t cfg = (<\/span><span style=\"color:#d08770;\">1 <\/span><span>&lt;&lt; <\/span><span style=\"color:#d08770;\">15<\/span><span>) | <\/span><span style=\"color:#d08770;\">0b00&#39;00&#39;11<\/span><span>;\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">writeDac<\/span><span>(pin, cfg);\n<\/span><span>\n<\/span><span>            <\/span><span style=\"color:#65737e;\">\/\/ Table 8: \/LDAC\n<\/span><span>            <\/span><span style=\"color:#65737e;\">\/\/ \/LDAC low mode; continuous update using SYNC;\n<\/span><span>            <\/span><span style=\"color:#65737e;\">\/\/ \/LDAC must be tied high\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">const <\/span><span>uint16_t ldacmode = (<\/span><span style=\"color:#d08770;\">1 <\/span><span>&lt;&lt; <\/span><span style=\"color:#d08770;\">15<\/span><span>) | (<\/span><span style=\"color:#d08770;\">1 <\/span><span>&lt;&lt; <\/span><span style=\"color:#d08770;\">13<\/span><span>) | <\/span><span style=\"color:#d08770;\">0x00<\/span><span>;\n<\/span><span>            <\/span><span style=\"color:#bf616a;\">writeDac<\/span><span>(pin, ldacmode);\n<\/span><span>        }\n<\/span><span>    }\n<\/span><span>}\n<\/span><\/code><\/pre>\n<h3 id=\"unit-tft\">Unit: tft<\/h3>\n<p>This unit initialises the SPI bus, and provides a function to display the MIDI mapping configuration on the screen. I have done this so far using the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/lovyan03\/LovyanGFX\">LovyanGFX<\/a> library. I really like this library for a few reasons:<\/p>\n<ul>\n<li>The API is very straightforward and mimics the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/Bodmer\/TFT_eSPI\">TFT_eSPI<\/a> library I have used before in the <code>psc<\/code>. I therefore was able to start the configuration rendering implementation mostly by copying what I'd already written for <code>psc<\/code>.<\/li>\n<li>It seems to configure the TFT LCD for good colour contrast and clarity. I have seen other libraries using different screen initialisation commands which ends up making the graphics look fuzzy and\/or washed out.<\/li>\n<li>Its SPI interface is <em>fast<\/em>. Really fast. I'm not sure what it does to achieve this (and we'll read later more about this), but other libraries don't seem to be able to do full screen updates anywhere near as fast as this library.<\/li>\n<\/ul>\n<p>The long list of graphics API calls for rendering the configuration isn't particularly enlightening to read, so I'll skip showing any code for this unit.<\/p>\n<h3 id=\"main\">main<\/h3>\n<p>To stitch all of the above together, we need to create an instance of each unit, call their <code>setup<\/code> functions and then start connecting signals:<\/p>\n<pre data-lang=\"c++\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c++ \"><code class=\"language-c++\" data-lang=\"c++\"><span style=\"color:#b48ead;\">using namespace<\/span><span> fourays;\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">namespace <\/span><span>state\n<\/span><span>{\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">static<\/span><span> config::Config cfg;\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">static<\/span><span> dac::DACState dac;\n<\/span><span>}\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">extern <\/span><span>&quot;<\/span><span style=\"color:#a3be8c;\">C<\/span><span>&quot; <\/span><span style=\"color:#b48ead;\">void <\/span><span style=\"color:#8fa1b3;\">app_main<\/span><span>(<\/span><span style=\"color:#b48ead;\">void<\/span><span>)\n<\/span><span>{\n<\/span><span>    usb::<\/span><span style=\"color:#bf616a;\">setup<\/span><span>();\n<\/span><span>    midi::<\/span><span style=\"color:#bf616a;\">setup<\/span><span>();\n<\/span><span>\n<\/span><span>    ay::<\/span><span style=\"color:#bf616a;\">setup<\/span><span>();\n<\/span><span>\n<\/span><span>    dac::<\/span><span style=\"color:#bf616a;\">setup<\/span><span>(io::<\/span><span style=\"color:#bf616a;\">SPI<\/span><span>());\n<\/span><span>    tft::<\/span><span style=\"color:#bf616a;\">setup<\/span><span>(io::<\/span><span style=\"color:#bf616a;\">SPI<\/span><span>());\n<\/span><span>\n<\/span><span>    midi::<\/span><span style=\"color:#bf616a;\">allNotesOff<\/span><span>().<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(handleAllNotesOff);\n<\/span><span>    midi::<\/span><span style=\"color:#bf616a;\">noteOn<\/span><span>().<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(handleNoteOn);\n<\/span><span>    midi::<\/span><span style=\"color:#bf616a;\">noteOff<\/span><span>().<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(handleNoteOff);\n<\/span><span>    midi::<\/span><span style=\"color:#bf616a;\">controlChange<\/span><span>().<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(handleControlChange);\n<\/span><span>\n<\/span><span>    <\/span><span style=\"color:#b48ead;\">auto<\/span><span> controller = state::cfg.<\/span><span style=\"color:#bf616a;\">controller<\/span><span>();\n<\/span><span>    touch::<\/span><span style=\"color:#bf616a;\">touch<\/span><span>().<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>        [](<\/span><span style=\"color:#b48ead;\">const<\/span><span> touch_pad_t &amp;pad)\n<\/span><span>        {\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">switch <\/span><span>(pad)\n<\/span><span>            {\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">case<\/span><span> TOUCH_LEFT:\n<\/span><span>                controller-&gt;<\/span><span style=\"color:#bf616a;\">next<\/span><span>({.<\/span><span style=\"color:#bf616a;\">type <\/span><span>= State::ControlEventType::LEFT});\n<\/span><span>                <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">case<\/span><span> TOUCH_RIGHT:\n<\/span><span>                controller-&gt;<\/span><span style=\"color:#bf616a;\">next<\/span><span>({.<\/span><span style=\"color:#bf616a;\">type <\/span><span>= State::ControlEventType::RIGHT});\n<\/span><span>                <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">case<\/span><span> TOUCH_SELECT:\n<\/span><span>                controller-&gt;<\/span><span style=\"color:#bf616a;\">next<\/span><span>({.<\/span><span style=\"color:#bf616a;\">type <\/span><span>= State::ControlEventType::SELECT});\n<\/span><span>                <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>\n<\/span><span>            <\/span><span style=\"color:#b48ead;\">default<\/span><span>:\n<\/span><span>                <\/span><span style=\"color:#b48ead;\">break<\/span><span>;\n<\/span><span>            }\n<\/span><span>        });\n<\/span><span>    touch::<\/span><span style=\"color:#bf616a;\">setup<\/span><span>();\n<\/span><span>\n<\/span><span>    state::cfg.<\/span><span style=\"color:#bf616a;\">changed<\/span><span>().<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>        []()\n<\/span><span>        {\n<\/span><span>            tft::<\/span><span style=\"color:#bf616a;\">displayConfig<\/span><span>(state::cfg);\n<\/span><span>        });\n<\/span><span>    controller-&gt;<\/span><span style=\"color:#bf616a;\">navigate<\/span><span>().<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(\n<\/span><span>        []()\n<\/span><span>        {\n<\/span><span>            tft::<\/span><span style=\"color:#bf616a;\">displayConfig<\/span><span>(state::cfg);\n<\/span><span>        });\n<\/span><span>\n<\/span><span>    config::<\/span><span style=\"color:#bf616a;\">setup<\/span><span>();\n<\/span><span>    state::cfg.<\/span><span style=\"color:#bf616a;\">load<\/span><span>(&quot;<\/span><span style=\"color:#a3be8c;\">default<\/span><span>&quot;);\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>The various <code>handle*<\/code> functions not shown here use the <code>cfg<\/code> to find out where to route the data and may eventually end up calling a <code>dac::update<\/code> or <code>ay::writeRegisters<\/code>.<\/p>\n<p>I also haven't described what the config <code>controller<\/code> is. That is worthy of its own article, and I will follow up with more details on that.<\/p>\n<h2 id=\"unit-compatibility-issues\">Unit Compatibility Issues<\/h2>\n<p>As it stands right now, each unit on its own works perfectly well. But there is a major problem I am still trying to solve. This revolves around the fact that the DAC and TFT share an SPI bus.<\/p>\n<p>I have established the following all works:<\/p>\n<ul>\n<li>Using DAC on its own works perfectly<\/li>\n<li>Using the display on its own works perfectly<\/li>\n<li>Using the DAC and then the display, both work<\/li>\n<li>Using the display and then the DAC - <em>the display is updated, but the DAC stops responding to any further commands<\/em><\/li>\n<\/ul>\n<p>I've tracked this down to the fact that whatever LovyanGFX is doing to make the SPI writes to the display so fast is very specific to writing to a single TFT device on the bus. The library expects no other devices will be using the bus, which kind of makes it not-a-bus. When the next DAC write occurs, the bus has been reconfigured into a state or mode which the DACs do not understand and therefore they no longer update.<\/p>\n<p>I have raised this as an issue with <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/lovyan03\/LovyanGFX\/issues\/491\">LovyanGFX (#491)<\/a> but I am still not convinced the author understands the issue and I have little hope that it'll be resolved any time soon.<\/p>\n<p>There's probably three ways I can go from here:<\/p>\n<ol>\n<li>Find another graphics library which doesn't mess around with the SPI bus so much. I would consider this a cop-out though, as I'm not keen on losing the display refresh performance nor the familiar graphics API.<\/li>\n<li>See if I can assign the DAC and TFT to <em>different<\/em> SPI busses, but I am technically out of available pins to use, and I'll need two more. I will need to do some further tests to see if I can use any of the \"unavailable\" pins. For example, using a \"strapping\" pin should be fine as long as I initialise and use it well after the ESP has started up. This glosses over the issues with the graphics library at the expense of more hardware complexity.<\/li>\n<li>Attach a logic analyser to the bus and find out what is being sent to the DACs before and after the display has been used. There's a chance with this method I can find out what has changed and trace down the code which has made that change, and possibly then reverse that when the DAC is used. This therefore would perhaps benefit not only myself but everyone else wanting to use this graphics library.<\/li>\n<\/ol>\n<h3 id=\"1-alternative-graphics-libraries\">1. Alternative Graphics Libraries<\/h3>\n<p>I've done a fair amount of searching for libraries to use already, prior to integrating LovyanGFX. There's not a lot to choose from. Most libraries which turn up in results assume you are using the Arduino framework, which I am not because it lacks decent support for using the ESP32-S3 as a USB device. Other libraries or examples I have found are for similar displays using interfaces which are not SPI.<\/p>\n<ul>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/lvgl.io\/developers\">LVGL<\/a> - looks very flashy but ESP32 support is very limited. The documentation is actually very sparse and lot of links on the website don't work or lead to 404s. When I did eventually get it to \"work\", the performance was terrible and the screen colours incorrect. I gave up trying to fix it.<\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/nopnop2002\/esp-idf-ili9340\">nopnop2002\/esp-idf-ili9340<\/a> - finding out that the ST7735s is mostly compatible with ILI9340 was news to me. This actually works but I had to make a few minor fixes. Performance is not great though and the initialisation commands make the display look kinda fuzzy. I couldn't get the initialisation commands from other libraries to work in this one.<\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/components.espressif.com\/components\/espressif\/esp_lcd_ili9341\">Official espressif ili9341 driver<\/a> - I haven't tried this one yet, I didn't know initially that this could be compatible with my ST7735s device.<\/li>\n<\/ul>\n<h3 id=\"2-split-the-spi-busses\">2. Split the SPI busses<\/h3>\n<p>I'm not sure why I didn't think of this before I started writing this article, perhaps I was too hung up on which ESP32-S3 pins should never be touched. However, less than one hour hacking proves this to actually work. Fortunately as well the ESP32-S3 has another available SPI host driver. So, we'll use <code>SPI2_HOST<\/code> on one set of pins for the TFT and <code>SPI3_HOST<\/code> with another set of pins for the DACs. At the moment I am still using the LovyanGFX SPI driver even for the DACs, as I assume I will get the same advantage of SPI speed\/efficiency that it implements, even though I have no idea how to measure that efficiency and test that hypothesis at the moment.<\/p>\n<pre data-lang=\"diff\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-diff \"><code class=\"language-diff\" data-lang=\"diff\"><span>diff --git a\/esp32-s3-idf\/src\/psc-dac.cpp b\/esp32-s3-idf\/src\/psc-dac.cpp\n<\/span><span>index b7d13dc..de92022 100644\n<\/span><span>--- a\/esp32-s3-idf\/src\/psc-dac.cpp\n<\/span><span>+++ b\/esp32-s3-idf\/src\/psc-dac.cpp\n<\/span><span>@@ -44,6 +44,10 @@ <\/span><span style=\"color:#8fa1b3;\">namespace psc::dac\n<\/span><span>         writeDac(idx, data);\n<\/span><span>     }\n<\/span><span>\n<\/span><span style=\"color:#a3be8c;\">+    psc::io::SPI_sptr bus = std::make_shared&lt;psc::io::PSCSPI&gt;(\n<\/span><span style=\"color:#a3be8c;\">+        SPI3_HOST, SPI_DAC_SCLK, SPI_DAC_MOSI, SPI_DAC_MISO, -1,\n<\/span><span style=\"color:#a3be8c;\">+        2, SPI_MASTER_FREQ_8M);\n<\/span><span style=\"color:#a3be8c;\">+\n<\/span><span>     void setup()\n<\/span><span>     {\n<\/span><span>         gpio_config_t io_cs_conf = {\n<\/span><span>@@ -70,7 +74,7 @@ <\/span><span style=\"color:#8fa1b3;\">namespace psc::dac\n<\/span><span>             .spics_io_num = DAC_1_CS,\n<\/span><span>             .queue_size = 1,\n<\/span><span>         };\n<\/span><span style=\"color:#bf616a;\">-        ret = spi_bus_add_device(SPI2_HOST, &amp;devcfg0, &amp;dacs[0]);\n<\/span><span style=\"color:#a3be8c;\">+        ret = spi_bus_add_device(bus-&gt;config().spi_host, &amp;devcfg0, &amp;dacs[0]);\n<\/span><span>         ESP_ERROR_CHECK(ret);\n<\/span><span>\n<\/span><span>         spi_device_interface_config_t devcfg1 = {\n<\/span><span>@@ -79,7 +83,7 @@ <\/span><span style=\"color:#8fa1b3;\">namespace psc::dac\n<\/span><span>             .spics_io_num = DAC_2_CS,\n<\/span><span>             .queue_size = 1,\n<\/span><span>         };\n<\/span><span style=\"color:#bf616a;\">-        ret = spi_bus_add_device(SPI2_HOST, &amp;devcfg1, &amp;dacs[1]);\n<\/span><span style=\"color:#a3be8c;\">+        ret = spi_bus_add_device(bus-&gt;config().spi_host, &amp;devcfg1, &amp;dacs[1]);\n<\/span><span>         ESP_ERROR_CHECK(ret);\n<\/span><span>\n<\/span><span>         \/\/ -----\n<\/span><span>@@ -108,11 +112,13 @@ <\/span><span style=\"color:#8fa1b3;\">namespace psc::dac\n<\/span><span>\n<\/span><span>     void beginTransaction(spi_device_handle_t dev)\n<\/span><span>     {\n<\/span><span style=\"color:#bf616a;\">-        uint32_t spi_port = (SPI2_HOST + 1);\n<\/span><span style=\"color:#a3be8c;\">+        auto cfg = bus-&gt;config();\n<\/span><span style=\"color:#a3be8c;\">+\n<\/span><span style=\"color:#a3be8c;\">+        uint32_t spi_port = (cfg.spi_host + 1);\n<\/span><span>         (void)spi_port;\n<\/span><span style=\"color:#bf616a;\">-        uint32_t clkdiv = lgfx::v1::FreqToClockDiv(lgfx::v1::getApbFrequency(), SPI_MASTER_FREQ_8M);\n<\/span><span style=\"color:#a3be8c;\">+        uint32_t clkdiv = lgfx::v1::FreqToClockDiv(lgfx::v1::getApbFrequency(), cfg.freq_write);\n<\/span><span>\n<\/span><span style=\"color:#bf616a;\">-        int spi_mode = 2;\n<\/span><span style=\"color:#a3be8c;\">+        int spi_mode = cfg.spi_mode;\n<\/span><span>         uint32_t user = SPI_USR_MOSI | SPI_USR_MISO | SPI_DOUTDIN;\n<\/span><span>         if (spi_mode == 1 || spi_mode == 2)\n<\/span><span>             user |= SPI_CK_OUT_EDGE;\n<\/span><span>@@ -147,8 +153,6 @@ <\/span><span style=\"color:#8fa1b3;\">namespace psc::dac\n<\/span><span>             return;\n<\/span><span>         }\n<\/span><span>\n<\/span><span>diff --git a\/esp32-s3-idf\/src\/psc-tft.cpp b\/esp32-s3-idf\/src\/psc-tft.cpp\n<\/span><span>index 2539604..9fb0790 100644\n<\/span><span>--- a\/esp32-s3-idf\/src\/psc-tft.cpp\n<\/span><span>+++ b\/esp32-s3-idf\/src\/psc-tft.cpp\n<\/span><span>@@ -4,6 +4,8 @@\n<\/span><span>\n<\/span><span> #include &lt;memory&gt;\n<\/span><span>\n<\/span><span style=\"color:#a3be8c;\">+#include &lt;driver\/spi_master.h&gt;\n<\/span><span style=\"color:#a3be8c;\">+\n<\/span><span> namespace psc::tft\n<\/span><span> {\n<\/span><span>     std::shared_ptr&lt;LGFX&gt; display;\n<\/span><span>@@ -18,9 +20,12 @@ <\/span><span style=\"color:#8fa1b3;\">namespace psc::tft\n<\/span><span>     \/\/ Cols offset +4px for single char column centred\n<\/span><span>     constexpr uint8_t COLSC[] = {5, 22, 39, 56, 73, 90, 107, 124, 141, 158};\n<\/span><span>\n<\/span><span style=\"color:#bf616a;\">-    void setup(psc::io::SPI_sptr bus)\n<\/span><span style=\"color:#a3be8c;\">+    psc::io::SPI_sptr bus = std::make_shared&lt;psc::io::PSCSPI&gt;(\n<\/span><span style=\"color:#a3be8c;\">+        SPI2_HOST, SPI_TFT_SCLK, SPI_TFT_MOSI, SPI_TFT_MISO, TFT_DC,\n<\/span><span style=\"color:#a3be8c;\">+        0, SPI_MASTER_FREQ_40M);\n<\/span><span style=\"color:#a3be8c;\">+\n<\/span><span style=\"color:#a3be8c;\">+    void setup()\n<\/span><span>     {\n<\/span><span>         display = std::make_shared&lt;LGFX&gt;(bus);\n<\/span><span>\n<\/span><span>         display-&gt;init();\n<\/span><span>diff --git a\/esp32-s3-idf\/src\/psc-tft.h b\/esp32-s3-idf\/src\/psc-tft.h\n<\/span><span>index 37cda32..7da7a3c 100644\n<\/span><span>--- a\/esp32-s3-idf\/src\/psc-tft.h\n<\/span><span>+++ b\/esp32-s3-idf\/src\/psc-tft.h\n<\/span><span>@@ -69,7 +69,7 @@ <\/span><span style=\"color:#8fa1b3;\">namespace psc::tft\n<\/span><span>         }\n<\/span><span>     };\n<\/span><span>\n<\/span><span style=\"color:#bf616a;\">-    void setup(psc::io::SPI_sptr bus);\n<\/span><span style=\"color:#a3be8c;\">+    void setup();\n<\/span><span>     void displayConfig(psc::config::Config &amp;cfg);\n<\/span><span>\n<\/span><span> } \/\/ namespace psc::tft\n<\/span><span>\n<\/span><\/code><\/pre>\n<h3 id=\"3-debug-the-spi-bus\">3. Debug the SPI bus<\/h3>\n<p>Well it turns out for the time being I don't need to do this. I'll continue to build this project out using dual SPI busses for now.<\/p>\n<h2 id=\"pcb-design\">PCB Design<\/h2>\n<p>Up to this point I have been prototyping this system entirely on breadboard. This test rig consists of two AY chips, one DAC and the TFT display. The DAC is hooked up to LEDs to check that the outputs are working.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;breadboard.112e9297039c180a.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I have also in parallel been laying out the schematic and PCBs in KiCAD.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;kicad-schematic-1.3fba1ab4176570d0.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>Top-level system hookup<\/p>\n<\/blockquote>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;kicad-schematic-2.d3a93f1ebd1f189c.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>AY chip hookup<\/p>\n<\/blockquote>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;kicad-schematic-3.1c3a8ccfdb2eec57.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>AY audio circuits<\/p>\n<\/blockquote>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;kicad-schematic-4.8c32407922915491.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>VCF filter<\/p>\n<\/blockquote>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;kicad-schematic-5.1ff4e61c9c8e057c.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>AY audio outputs mixer<\/p>\n<\/blockquote>\n<p>For the PCB layout, I will place one AY and three VCF filters on one board, and therefore need four instances of these boards. I have routed all the AY and VCF ins and outs to bus connectors, but each board has breakout pads between the bus connector and the devices so that each AY and VCF can be connected to a different part of the bus.<\/p>\n<div style=\"max-width: 33%; margin: 0 auto\">\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;kicad-schematic-6.887fad0ecdb70233.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/div>\n<blockquote>\n<p>AY\/VCF board bus connectors. The smaller connector is the shared AY control bus, the larger connector is the AY select and audio I\/O bus.<\/p>\n<\/blockquote>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;kicad-board-1.08e3061da1470226.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;kicad-board-3.c9cdd3d5917e913b.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<blockquote>\n<p>AY\/VCF board layout and KiCAD 3D render<\/p>\n<\/blockquote>\n<p>On each board I will have to manually connect each of the blue pads to the correct pins on the bus connectors. However, in this way it means that I can manufacture four identical boards.<\/p>\n<p>The bus connectors are also present in the same location on a controller board, where every connection is routed to the appropriate ESP32-S3 pin or audio circuit.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;kicad-board-2.9e40410b64a9780a.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>Controller board layout. The four devices on this board are the ESP32-S3, 2x AD5328 DACs, one op-amp audio mixer. The large connectors at the top are the board busses. The small pin headers on the bottom row are USB, TFT, Touch inputs respectively from left to right.<\/p>\n<\/blockquote>\n<h2 id=\"assembly-and-hardware-design\">Assembly and Hardware Design<\/h2>\n<h3 id=\"overall-dimensions\">Overall dimensions<\/h3>\n<p>By placing all of the bus connectors in the same location on every board, it suggests that all of the boards can be assembled together in a vertical stack, rather than side by side. At this time though I have not figured out any of the physical assembly of this thing. However, my gut feeling is that both a five board stack or a five board flat layout will lead to this being quite a large device overall. A vertical stack will end up being a chunky cube of boards. The flat layout will take over 475 cm<sup>2<\/sup> - equivalent to a square with 22 cm sides.<\/p>\n<h3 id=\"i-o-and-controls\">I\/O and controls<\/h3>\n<p>The device ideally will be entirely USB bus powered, however I think I still need additional power for the audio mixer, as these devices typically run on +\/- 12 volts, which is required to get enough headroom for the op-amps to do their thing. I'm not sure yet how to get that voltage, I most likely will need an external DC power supply and include some regulators as well. There's enough spare space on the controller board to include the power input and regulation.<\/p>\n<p>The synth will have three touch input controls for adjusting the configuration. These will likely either be hidden under the case and indicated by painted or drawn markings on top, or I could expose some small metal pads or buttons which would probably make them both more visible and more reliable to use.<\/p>\n<p>I could also provide some manual potentiometer controls for the filters. The filters' cut-off frequency is designed to be MIDI controlled, but these can be blended with manual controls as well. I have also allowed in the PCB design for the filter resonances to be either fixed by installing fixed resistors, or to be manually adjustable from potentiometers. A full set of manual filter controls then would consist of 24 potentiometers, which may be too many. I am not aware of any way to make the resonance MIDI controllable, even the filters I have built before for my analogue synth did not have CV control over resonance.<\/p>\n<p>I will do some 3D render mocks for the assembly and see what looks good and also serves the practical purpose of the synth.<\/p>\n<h2 id=\"if-you-feel-like-helping\">If you feel like helping ?<\/h2>\n<p>I am more than happy to try to complete this project with my own curiosity and determination, but I would also love to have some help on this - even if its just to drop a note about an idea you have or some comment which may be helpful to steer the project towards completion where there are still unanswered questions. Or if you've spotted bugs or issues in my firmware code by all means send a comment or PR.<\/p>\n<p>The repository for Fourays including all the firmware, library code and KiCAD files is here:<\/p>\n<blockquote>\n<p><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/fourays\/\">bitbucket\/doughammond\/fourays<\/a><\/p>\n<\/blockquote>\n<p>The entire repository is GPL 3 licensed, so you are also free to use anything from this project in your own as long as you follow the terms of the license.<\/p>\n"},{"title":"Programmable Synth Controller","published":"2024-01-24T00:00:00+00:00","updated":"2024-01-24T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/psc\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/psc\/","content":"<h2 id=\"an-interface-for-the-box-that-goes-bleep\">An interface for the box that goes bleep<\/h2>\n<p>As soon as I started thinking about building my own synth, I built into that project the requirement to interface the synth with a computer using MIDI. After all, that's what I know for generating notes, and I didn't want to start building analogue keyboards or sequencers to make the synth work standalone. This interface became known as the Programmable Synth Controller, or <code>psc<\/code> for short.<\/p>\n<h2 id=\"requirements\">Requirements<\/h2>\n<h3 id=\"synth-control\">Synth Control<\/h3>\n<p>To figure out what we need, lets work backwards from the synth itself. The interface to the synth modules is a simple voltage level, via the \"CV\" (Control Voltage) inputs on the modules. To have the computer control the modules, then what we need is some way to convert a MIDI message into a voltage. The voltage can be set using a DAC (Digital to Analogue Converter), and we will need some other electronics, namely a microcontroller, to read the computer data and instruct the DAC to set a specific voltage on its output(s). We will also need multiple different voltages changing at the same time, for example one for pitch and another for volume. A couple more wouldn't go amiss as well so that maybe we can change a filter cut-off frequency at the same time.<\/p>\n<h3 id=\"dac-selection\">DAC Selection<\/h3>\n<p>I settled on using an <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.microchip.com\/en-us\/product\/mcp4728\">MCP4728<\/a> DAC; this is a little device which can output 4 different voltages at once, all controlled by a single i2c input. This DAC is also desirable due to its resolution. I had heard that common and cheap 8-bit DACs can be a little limiting for controlling synths, as only 255 different voltage levels may not be enough. At 12-bit resolution we can have 4096 different levels, so we can represent frequency and volume levels a lot more accurately. We will still be a bit limited however, that the DAC cannot output a full range of voltages which may be useful for the synth. We might ideally want an output of -10V to +10V but given the low voltage nature of the <code>psc<\/code> system, we will only be able to achieve 0V to +3V or so. This should be enough for a start though.<\/p>\n<h3 id=\"additional-outputs\">Additional Outputs<\/h3>\n<p>As well as continuously variable voltages, it is useful when controlling a synth to have some digital style voltages available. These are commonly called gate and trigger signals. Gate signals will usually be fully off when nothing is happening and fully on when something is happening, e.g. +5V for the duration of a note and 0V otherwise. Triggers are similar but are usually just short pulses which are emitted at the start of an event.<\/p>\n<p>If we have four CV voltages available, then we should maybe also have four gate and four trigger signals each of which is linked to a CV signal. One \"channel\" therefore consists of one CV, one gate and one trigger all working together. This needs therefore 8 digital outputs from the microcontroller and I elected to use an <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.microchip.com\/en-us\/product\/mcp23017\">MCP23017<\/a> port expander chip for all of these. Also hanging off this expander is a \"master\" trigger output, which changes when any of the DAC values change, and an RGB LED.<\/p>\n<h3 id=\"computer-interface\">Computer Interface<\/h3>\n<p>On the computer side, I want the <code>psc<\/code> to show up as a MIDI device when connected to the computer in a plug-and-play manner.<\/p>\n<h3 id=\"added-bonuses\">Added Bonuses<\/h3>\n<p>I also had on hand an ancient <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/General_Instrument_AY-3-8910\">AY-3-8910<\/a> chip. Since we'll be using a microcontroller and accepting MIDI data, why not hook this up as well and have some extra noises available? This turned out to be possible, but the first iteration used more hardware to achieve this than may have been strictly necessary.<\/p>\n<p>Furthermore, if the microcontroller can run any code that I write for it, could it not also run autonomously without the connected computer and make the <code>psc<\/code> + synth combination work standalone? This is where the \"Programmable\" part of the name came from. This turned out to be an interesting avenue, but ultimately a massive distraction from the main goal. Even though in the end this thing is not really \"programmable\", I still continue to call this thing <code>psc<\/code>.<\/p>\n<p>The choice of microcontroller might also enable wireless control, this was explored but in the end discarded due to lack of any standard interface to use on the computer side and concerns over latency.<\/p>\n<blockquote>\n<p><em>Yes, <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.midi.org\/specifications-old\/item\/bluetooth-le-midi\">MIDI over Bluetooth<\/a> is a thing, but there's no sensible bridge available on the computer side to make it appear in any\/all DAW software automatically?<\/em><\/p>\n<\/blockquote>\n<h3 id=\"display\">Display<\/h3>\n<p>I figured it would be hard to know what the <code>psc<\/code> is doing, or how it is configured without a display. So, lets hook up a 1.8\" LCD. We could also use some visual feedback regarding note, tempo, etc. So lets also add one RGB LED.<\/p>\n<h1 id=\"first-iteration\">First iteration<\/h1>\n<p>Parts list:<\/p>\n<ul>\n<li>1x ESP8266 microcontroller<\/li>\n<li>1x MCP4728 4-channel 12-bit DAC with i2c<\/li>\n<li>1x MCP23017 16 output GPIO expander with i2c<\/li>\n<li>1x 1.8\" TFT LCD ST7735S<\/li>\n<li>1x RGB LED<\/li>\n<li>1x AY-3-8910<\/li>\n<li>1x Arduino Nano<\/li>\n<\/ul>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;psc_iter1_schematic.3ffafed59b3a5d9e.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>The reason we've included an Arduino as well should be apparent by looking at the schematic. The ESP8266 does not have enough available pins to connect the AY-3-8910. The AY uses an 8-bit parallel bus plus 2 bus control pins plus 1 clock signal, which already would be too many. So instead, we use the Arduino as a \"port expander\" which accepts AY register data over a UART serial link, and expands that into the signals required to write register data to the AY.<\/p>\n<p>We can just about squeeze all the other primary peripherals onto the other available GPIO pins on the ESP8266.<\/p>\n<p>Fortuitously I also didn't have to write any code for the Arduino, some kind soul on the internet had already done it. In fact, searching now reveals a few alternative to choose from, but I picked up and used <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/986-Studio\/AY-3-3910-Player\">this one<\/a>. This one comes with a small C program you can run on your PC to send \"mym\" format music to the Arduino, this was actually useful for testing the Arduino + AY combo part of the device in isolation.<\/p>\n<h2 id=\"esp8266-firmware\">ESP8266 firmware<\/h2>\n<p>We have a reasonably neat and capable system according to the schematic, but what exactly is it going to do?<\/p>\n<h3 id=\"ways-to-get-the-midi-notes-as-input\">Ways to get the MIDI notes as input<\/h3>\n<p>Primarily we want to get notes from the PC into it and let it translate those into commands for the peripherals. The ESP8266 is somewhat limited on inputs though:<\/p>\n<ol>\n<li>MIDI over Bluetooth<\/li>\n<li>MIDI over WiFi<\/li>\n<li>MIDI over USB serial<\/li>\n<\/ol>\n<h4 id=\"1-midi-over-bluetooth\">1. MIDI over Bluetooth<\/h4>\n<p>There are standards for this, but implementing anything over Bluetooth is an enormous headache. I think I did manage to make it work, but there's little to no support at all on the PC side. I tore this out before too long and forgot about it.<\/p>\n<p>The services definitions I implemented are <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/psc\/esp32-services\/\">here<\/a>.<\/p>\n<h4 id=\"2-midi-over-wifi\">2. MIDI over WiFi<\/h4>\n<p>Setting up the WiFi on the ESP is quite straightforward and we can open TCP sockets to accept MIDI bytes. Again though, this suffers from lack of support on the PC side. Also, we have some hurdles to get over to get the ESP on to the same WiFi network as well. With no keyboard, you can't simply type in WiFi network names and passwords. Pre-installing the password also doesn't really work, you don't want to be re-flashing the ESP just because you moved somewhere with a different network available. Since I started this project though, some newer ways of provisioning ESP WiFi credentials have appeared such as <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.improv-wifi.com\/\">Improv<\/a>, but they remain a little janky.<\/p>\n<p>There's the final nail in the coffin for transmitting timing-critical musical notes over TCP, and over WiFi is having terrible and unpredictable latency. The notes coming out will simply not be in time with what's happening on the PC. So, lets leave WiFi here and go no further.<\/p>\n<h4 id=\"3-midi-over-usb-serial\">3. MIDI over USB serial<\/h4>\n<p>This is pretty simple. By default the USB serial is already configured when using common frameworks for ESP devices. We can just sent MIDI bytes down the wire and interpret them one by one. This actually works, if you install and use some MIDI-Serial software on the PC side, such as <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/projectgus.github.io\/hairless-midiserial\/\">The Hairless MIDI to Serial Bridge<\/a>. We failed a little at the requirement of having the ESP itself be plug-and-play with the PC though.<\/p>\n<p>I stuck with this solution, given it's the only viable option really for ESP8266.<\/p>\n<h3 id=\"ways-to-get-midi-notes-autonomously\">Ways to get MIDI notes autonomously<\/h3>\n<p>Since I wasn't completely convinced about MIDI-over-serial I started looking at ways the device could just make music on its own. I invented a (very basic!) music programming language which resembles some sort of assembly language. That code could be compiled into a sort of compact byte-code and interpreted on the ESP. Note that this isn't really producing a \"machine code\" in the real sense that the operations and data directly map to control electrical signals in the CPU, it is simply a compact numerical representation of a sequence of instructions for the device firmware to follow.<\/p>\n<h4 id=\"pscs\"><code>pscs<\/code><\/h4>\n<p>The <code>psc<\/code> source program format is called <code>pscs<\/code>. The specification is <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/psc\/pscs-specification\/\">here<\/a>. The idea is that <code>pscs<\/code> defines a set of instructions which can operate on a 16-bit memory address space of 16-bit words, and a way to call \"interrupts\" which are interpreter implementation specific function calls which may for example accept a memory address and update the state of a hardware peripheral. In this way you can set up and store data and manipulate it with your <code>pscs<\/code> code, and when the time is right, send some of those values to the DACs.<\/p>\n<p>Note that <code>pscs<\/code> never defined how exactly any data is input from the MIDI stream or output to hardware devices. The former was implemented as a Bluetooth service working independently of the <code>pscs<\/code> program running in the interpreter, which required the PC host program to be sending data to <code>psc<\/code> memory addresses matching what the <code>pscs<\/code> program expected. To illustrate this, here is the <code>pscs<\/code> source for live-streaming data:<\/p>\n<blockquote>\n<p>keyboard.pscs<\/p>\n<pre data-lang=\"asm\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-asm \"><code class=\"language-asm\" data-lang=\"asm\"><span style=\"color:#65737e;\">;\n<\/span><span style=\"color:#65737e;\">; Realtime input driver for PSC.\n<\/span><span style=\"color:#65737e;\">;\n<\/span><span style=\"color:#65737e;\">; The data being input must be written in the following matrix:\n<\/span><span style=\"color:#65737e;\">;\n<\/span><span style=\"color:#65737e;\">;            _________DAC__________  _________PSG__________\n<\/span><span style=\"color:#65737e;\">; Output     DACA  DACB  DACC  DACD  PSGA  PSGB  PSGC  PSGN\n<\/span><span style=\"color:#65737e;\">; Input      0000  0010  0020  0030  0040  0050  0060  0070\n<\/span><span style=\"color:#65737e;\">;\n<\/span><span style=\"color:#65737e;\">; We must copy the input values to this contiguous memory\n<\/span><span style=\"color:#65737e;\">; segment, and use INT 00 to update the outputs\n<\/span><span style=\"color:#65737e;\">;\n<\/span><span style=\"color:#65737e;\">;                  _________DAC__________  _________PSG__________\n<\/span><span style=\"color:#65737e;\">; Output    Trig   DACA  DACB  DACC  DACD  PSGA  PSGB  PSGC  PSGN\n<\/span><span style=\"color:#65737e;\">;           00F0   00F1  00F2  00F3  00F4  00F5  00F6  00F7  00F8\n<\/span><span style=\"color:#65737e;\">;\n<\/span><span style=\"color:#65737e;\">; @see docs\/sequencer-data.md\n<\/span><span style=\"color:#65737e;\">;\n<\/span><span style=\"color:#8fa1b3;\">begin:\n<\/span><span style=\"color:#8fa1b3;\">subscribe:\n<\/span><span style=\"color:#8fa1b3;\">  INTM <\/span><span style=\"color:#d08770;\">00<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00F0<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00F8<\/span><span style=\"color:#65737e;\"> ; subscribe to current data\n<\/span><span style=\"color:#8fa1b3;\">input:\n<\/span><span style=\"color:#65737e;\">; compare inputs with outputs\n<\/span><span style=\"color:#8fa1b3;\">checkF1:\n<\/span><span style=\"color:#8fa1b3;\">  SUBR 00F1<\/span><span>, <\/span><span style=\"color:#d08770;\">0000\n<\/span><span style=\"color:#8fa1b3;\">  JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00F1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">trigger<\/span><span style=\"color:#65737e;\"> ; jump if 00 not equal to F1\n<\/span><span style=\"color:#8fa1b3;\">checkF2:\n<\/span><span style=\"color:#8fa1b3;\">  SUBR 00F2<\/span><span>, <\/span><span style=\"color:#d08770;\">0010\n<\/span><span style=\"color:#8fa1b3;\">  JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00F2<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">trigger<\/span><span style=\"color:#65737e;\"> ; jump if 10 not equal to F2\n<\/span><span style=\"color:#8fa1b3;\">checkF3:\n<\/span><span style=\"color:#8fa1b3;\">  SUBR 00F3<\/span><span>, <\/span><span style=\"color:#d08770;\">0020\n<\/span><span style=\"color:#8fa1b3;\">  JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00F3<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">trigger<\/span><span style=\"color:#65737e;\"> ; jump if 20 not equal to F3\n<\/span><span style=\"color:#8fa1b3;\">checkF4:\n<\/span><span style=\"color:#8fa1b3;\">  SUBR 00F4<\/span><span>, <\/span><span style=\"color:#d08770;\">0030\n<\/span><span style=\"color:#8fa1b3;\">  JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00F4<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">trigger<\/span><span style=\"color:#65737e;\"> ; jump if 30 not equal to F4\n<\/span><span style=\"color:#8fa1b3;\">checkF5:\n<\/span><span style=\"color:#8fa1b3;\">  SUBR 00F5<\/span><span>, <\/span><span style=\"color:#d08770;\">0040\n<\/span><span style=\"color:#8fa1b3;\">  JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00F5<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">trigger<\/span><span style=\"color:#65737e;\"> ; jump if 40 not equal to F5\n<\/span><span style=\"color:#8fa1b3;\">checkF6:\n<\/span><span style=\"color:#8fa1b3;\">  SUBR 00F6<\/span><span>, <\/span><span style=\"color:#d08770;\">0050\n<\/span><span style=\"color:#8fa1b3;\">  JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00F6<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">trigger<\/span><span style=\"color:#65737e;\"> ; jump if 50 not equal to F6\n<\/span><span style=\"color:#8fa1b3;\">checkF7:\n<\/span><span style=\"color:#8fa1b3;\">  SUBR 00F7<\/span><span>, <\/span><span style=\"color:#d08770;\">0060\n<\/span><span style=\"color:#8fa1b3;\">  JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00F7<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">trigger<\/span><span style=\"color:#65737e;\"> ; jump if 60 not equal to F7\n<\/span><span style=\"color:#8fa1b3;\">checkF8:\n<\/span><span style=\"color:#8fa1b3;\">  SUBR 00F8<\/span><span>, <\/span><span style=\"color:#d08770;\">0070\n<\/span><span style=\"color:#8fa1b3;\">  JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00F8<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">trigger<\/span><span style=\"color:#65737e;\"> ; jump if 70 not equal to F8\n<\/span><span>\n<\/span><span style=\"color:#65737e;\">; if we reached here there was no input, go back and try again\n<\/span><span style=\"color:#8fa1b3;\">noinput:\n<\/span><span style=\"color:#8fa1b3;\">  JCN F<\/span><span>, <\/span><span style=\"color:#d08770;\">0000<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">input\n<\/span><span>\n<\/span><span style=\"color:#8fa1b3;\">trigger:\n<\/span><span style=\"color:#65737e;\">  ; update output memory\n<\/span><span style=\"color:#8fa1b3;\">  COPY 00F1<\/span><span>, <\/span><span style=\"color:#d08770;\">0000<\/span><span>, <\/span><span style=\"color:#d08770;\">0001<\/span><span>, <\/span><span style=\"color:#d08770;\">0010<\/span><span>, <\/span><span style=\"color:#d08770;\">0008\n<\/span><span style=\"color:#65737e;\">  ; interrupt to set outputs\n<\/span><span style=\"color:#8fa1b3;\">  <\/span><span style=\"color:#b48ead;\">INT <\/span><span style=\"color:#d08770;\">00\n<\/span><span style=\"color:#65737e;\">  ; start again\n<\/span><span style=\"color:#8fa1b3;\">  JCN F<\/span><span>, <\/span><span style=\"color:#d08770;\">0000<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">input\n<\/span><span style=\"color:#8fa1b3;\">end:\n<\/span><\/code><\/pre>\n<\/blockquote>\n<p>As you can see here, it is the <code>pscs<\/code> program itself which defines how the memory is to be used. I never did figure out how to fully specify this in a way which would allow any arbitrary <code>pscs<\/code> program to function.<\/p>\n<h4 id=\"pscc\"><code>pscc<\/code><\/h4>\n<p>The byte-code code compiler for <code>pscs<\/code> is called <code>pscc<\/code> and was implemented in javascript. I guess at least in javascript the compiler can run on a web page and could therefore be available to everyone to use. Other than that, there was no other reason to use javascript particularly.<\/p>\n<p>The compiler is implemented as a <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/nearley.js.org\/\">nearley<\/a> grammar which quite simply turns the <code>pscs<\/code> string instructions into binary value equivalents. It also does some trickery with the code labels and jump instructions to figure out exactly where to jump to in the binary output. There's not a lot to it really. The resulting binary file is called <code>pscb<\/code>.<\/p>\n<p>The string opcode to binary value mapping is simply this:<\/p>\n<pre data-lang=\"typescript\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-typescript \"><code class=\"language-typescript\" data-lang=\"typescript\"><span style=\"color:#b48ead;\">export enum <\/span><span>Opcode {\n<\/span><span>  <\/span><span style=\"color:#65737e;\">\/\/ general\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">NOP <\/span><span>= <\/span><span style=\"color:#d08770;\">0x00<\/span><span>,\n<\/span><span>\n<\/span><span>  <\/span><span style=\"color:#65737e;\">\/\/ memory\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">MOVR <\/span><span>= <\/span><span style=\"color:#d08770;\">0x01<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">MOVL <\/span><span>= <\/span><span style=\"color:#d08770;\">0x02<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">FILL <\/span><span>= <\/span><span style=\"color:#d08770;\">0x03<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">COPY <\/span><span>= <\/span><span style=\"color:#d08770;\">0x04<\/span><span>,\n<\/span><span>\n<\/span><span>  <\/span><span style=\"color:#65737e;\">\/\/ interrupts\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">INT <\/span><span>= <\/span><span style=\"color:#d08770;\">0x05<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">INTM <\/span><span>= <\/span><span style=\"color:#d08770;\">0x06<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">INTR <\/span><span>= <\/span><span style=\"color:#d08770;\">0x07<\/span><span>,\n<\/span><span>\n<\/span><span>  <\/span><span style=\"color:#65737e;\">\/\/ arithmetic\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">INC <\/span><span>= <\/span><span style=\"color:#d08770;\">0x08<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">DEC <\/span><span>= <\/span><span style=\"color:#d08770;\">0x09<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">ADDL <\/span><span>= <\/span><span style=\"color:#d08770;\">0x0a<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">ADDR <\/span><span>= <\/span><span style=\"color:#d08770;\">0x0b<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">SUBL <\/span><span>= <\/span><span style=\"color:#d08770;\">0x0c<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">SUBR <\/span><span>= <\/span><span style=\"color:#d08770;\">0x0d<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">MULL <\/span><span>= <\/span><span style=\"color:#d08770;\">0x0e<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">MULR <\/span><span>= <\/span><span style=\"color:#d08770;\">0x0f<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">DIVL <\/span><span>= <\/span><span style=\"color:#d08770;\">0x10<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">DIVR <\/span><span>= <\/span><span style=\"color:#d08770;\">0x11<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">MODL <\/span><span>= <\/span><span style=\"color:#d08770;\">0x12<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">MODR <\/span><span>= <\/span><span style=\"color:#d08770;\">0x13<\/span><span>,\n<\/span><span>\n<\/span><span>  <\/span><span style=\"color:#65737e;\">\/\/ flow control\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">JCN <\/span><span>= <\/span><span style=\"color:#d08770;\">0x14<\/span><span>,\n<\/span><span>  <\/span><span style=\"color:#bf616a;\">HLT <\/span><span>= <\/span><span style=\"color:#d08770;\">0x15<\/span><span>,\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p>The resulting byte-code for the <code>keyboard.pscs<\/code> is thus<\/p>\n<blockquote>\n<p>keyboard.pscb<\/p>\n<pre data-lang=\"bash\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-bash \"><code class=\"language-bash\" data-lang=\"bash\"><span style=\"color:#bf616a;\">00000000:<\/span><span> 0600 00f0 00f8 0d00 f100 0014 0100 f15b  ...............[\n<\/span><span style=\"color:#bf616a;\">00000010:<\/span><span> 0d00 f200 1014 0100 f25b 0d00 f300 2014  .........[.... .\n<\/span><span style=\"color:#bf616a;\">00000020:<\/span><span> 0100 f35b 0d00 f400 3014 0100 f45b 0d00  ...[....0....[..\n<\/span><span style=\"color:#bf616a;\">00000030:<\/span><span> f500 4014 0100 f55b 0d00 f600 5014 0100  ..@....[....P...\n<\/span><span style=\"color:#bf616a;\">00000040:<\/span><span> f65b 0d00 f700 6014 0100 f75b 0d00 f800  .[....`<\/span><span style=\"color:#bf616a;\">....[....\n<\/span><span style=\"color:#bf616a;\">00000050:<\/span><span> 7014 0100 f85b 140f 0000 0604 00f1 0000  p....[..........\n<\/span><span style=\"color:#bf616a;\">00000060:<\/span><span> 0001 0010 0008 0500 140f 0000 06         .............\n<\/span><\/code><\/pre>\n<\/blockquote>\n<p><code>pscc<\/code> compiler sources are <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/psc\/src\/39ef00f742274c2ea98449bd7ea8cfc1eb084d93\/pscc\/src\/\">here<\/a>.<\/p>\n<h4 id=\"libpsc\"><code>libpsc<\/code><\/h4>\n<p>This is a c++ library which implements the <code>pscb<\/code> interpreter, primarily for running on the ESP device.<\/p>\n<p><code>libpsc<\/code> source code is <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/psc\/src\/39ef00f742274c2ea98449bd7ea8cfc1eb084d93\/libpsc\/\">here<\/a>.<\/p>\n<p>To help speed up development and testing of the <code>pscs<\/code>, <code>pscc<\/code>, <code>pscb<\/code> implementations, this lib when compiled on PC also outputs a disassembly and \"interactive\" debugger for <code>pscb<\/code> code. The disassembler is for checking that parsing the <code>psbc<\/code> binary produces the same (ish, since comments and original label names are lost) <code>pscs<\/code> as the original user program:<\/p>\n<blockquote>\n<p><code>$ .\/keyboard-disassemble<\/code><\/p>\n<pre data-lang=\"asm\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-asm \"><code class=\"language-asm\" data-lang=\"asm\"><span style=\"color:#65737e;\">; --------\n<\/span><span style=\"color:#8fa1b3;\">    INTM <\/span><span style=\"color:#d08770;\">00<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00f0<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00f8\n<\/span><span style=\"color:#8fa1b3;\">LBL0006:\n<\/span><span style=\"color:#8fa1b3;\">    SUBR 00f1<\/span><span>, <\/span><span style=\"color:#d08770;\">0000\n<\/span><span style=\"color:#8fa1b3;\">    JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00f1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">LBL5b\n<\/span><span style=\"color:#8fa1b3;\">    SUBR 00f2<\/span><span>, <\/span><span style=\"color:#d08770;\">0010\n<\/span><span style=\"color:#8fa1b3;\">    JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00f2<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">LBL5b\n<\/span><span style=\"color:#8fa1b3;\">    SUBR 00f3<\/span><span>, <\/span><span style=\"color:#d08770;\">0020\n<\/span><span style=\"color:#8fa1b3;\">    JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00f3<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">LBL5b\n<\/span><span style=\"color:#8fa1b3;\">    SUBR 00f4<\/span><span>, <\/span><span style=\"color:#d08770;\">0030\n<\/span><span style=\"color:#8fa1b3;\">    JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00f4<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">LBL5b\n<\/span><span style=\"color:#8fa1b3;\">    SUBR 00f5<\/span><span>, <\/span><span style=\"color:#d08770;\">0040\n<\/span><span style=\"color:#8fa1b3;\">    JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00f5<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">LBL5b\n<\/span><span style=\"color:#8fa1b3;\">    SUBR 00f6<\/span><span>, <\/span><span style=\"color:#d08770;\">0050\n<\/span><span style=\"color:#8fa1b3;\">    JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00f6<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">LBL5b\n<\/span><span style=\"color:#8fa1b3;\">    SUBR 00f7<\/span><span>, <\/span><span style=\"color:#d08770;\">0060\n<\/span><span style=\"color:#8fa1b3;\">    JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00f7<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">LBL5b\n<\/span><span style=\"color:#8fa1b3;\">    SUBR 00f8<\/span><span>, <\/span><span style=\"color:#d08770;\">0070\n<\/span><span style=\"color:#8fa1b3;\">    JCN <\/span><span style=\"color:#d08770;\">1<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">00f8<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">LBL5b\n<\/span><span style=\"color:#8fa1b3;\">    JCN f<\/span><span>, <\/span><span style=\"color:#d08770;\">0000<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">LBL06\n<\/span><span style=\"color:#8fa1b3;\">LBL005b:\n<\/span><span style=\"color:#8fa1b3;\">    COPY 00f1<\/span><span>, <\/span><span style=\"color:#d08770;\">0000<\/span><span>, <\/span><span style=\"color:#d08770;\">0001<\/span><span>, <\/span><span style=\"color:#d08770;\">0010<\/span><span>, <\/span><span style=\"color:#d08770;\">0008\n<\/span><span style=\"color:#8fa1b3;\">    <\/span><span style=\"color:#b48ead;\">INT <\/span><span style=\"color:#d08770;\">00\n<\/span><span style=\"color:#8fa1b3;\">    JCN f<\/span><span>, <\/span><span style=\"color:#d08770;\">0000<\/span><span>, <\/span><span style=\"color:#8fa1b3;\">LBL06\n<\/span><span style=\"color:#65737e;\">; --------\n<\/span><\/code><\/pre>\n<\/blockquote>\n<p>Viewing this program running in the \"debugger\" is not particularly interesting since it runs in a tight loop and expects the memory to be updated from an external process. Instead lets look at a 16-step sequencer running. This program reads step values from memory and copies them to the \"output registers\" for the interrupt handler to copy on to DAC outputs. The step value memory is all zero in this example though, but you can see the program keeping track of the current step number and the step delays:<\/p>\n<p><img src=\"https:\/\/doug.lon.dev\/blog\/2024\/psc\/assets\/16step.gif\" alt=\"\" \/><\/p>\n<h4 id=\"esp-pscb-interpreter-and-ble-services\">ESP <code>pscb<\/code> interpreter and BLE services<\/h4>\n<p>The ESP firmware implemented two BLE services; one for uploading a <code>pscb<\/code> program and another to write into the <code>psc<\/code> interpreter memory. The firmware implemented a <code>pscb<\/code> interpreter, with interrupt handlers defining <code>delay()<\/code> and <code>update_dac()<\/code> functions.<\/p>\n<p>Overall it worked, but I only ever wrote 2 <code>pscs<\/code> programs, the \"keyboard\" live MIDI note relay and the \"16step\" sequencer. It didn't solve my initial requirements of using the device as a MIDI instrument from my PC. I also realised I didn't want to interact with my synth by writing this weird <code>pscs<\/code> assembly language. Also this firmware did not provide any feedback on the display about what program is loaded\/running nor about the program status.<\/p>\n<p>It was an interesting adventure though, but did consume a lot of spare time which could well have been better spent on the primary requirements. I later deleted all of this code from the main branch and focussed on what I primarily wanted from the <code>psc<\/code>.<\/p>\n<h2 id=\"psc-configuration\"><code>psc<\/code> configuration<\/h2>\n<p>So far this article has discussed setting up the DAC voltage outputs from MIDI data, but has not established exactly how to map MIDI data to the DAC. There are several different ways we could do this, and also depending on the song and setup, we might want to be able to change the mapping in some way.<\/p>\n<p>To that end, I wrote up a SysEx data configuration protocol, documented <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/psc\/config-sysex\/\">here<\/a>.<\/p>\n<p>The upshot is that each DAC output and AY voice can be mapped to one of either a note value, note velocity, 7-bit CC value or 14-bit CC value. Each output can also be set enabled or disabled and also mapped to a MIDI channel, and the output's gate and trigger can separately be enabled and disabled.<\/p>\n<p>To date however, I have only actually set up the <code>psc<\/code> with the default mapping:<\/p>\n<pre data-lang=\"c\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-c \"><code class=\"language-c\" data-lang=\"c\"><span>    <\/span><span style=\"color:#65737e;\">\/\/ Set each output to sequential MIDI channels, 1-8:\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">parseProtocol0<\/span><span>({<\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x01<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x02<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x01<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x04<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x02<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x08<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x03<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x01<\/span><span>, <\/span><span style=\"color:#d08770;\">0x04<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x02<\/span><span>, <\/span><span style=\"color:#d08770;\">0x05<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x04<\/span><span>, <\/span><span style=\"color:#d08770;\">0x06<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x08<\/span><span>, <\/span><span style=\"color:#d08770;\">0x07<\/span><span>});\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/\/ Enable Value, Gate, Trigger for the DAC outputs A and B, Value only for DAC outputs C and D:\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">parseProtocol0<\/span><span>({<\/span><span style=\"color:#d08770;\">0x01<\/span><span>, <\/span><span style=\"color:#d08770;\">0x03<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x07<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x01<\/span><span>, <\/span><span style=\"color:#d08770;\">0x0C<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x01<\/span><span>});\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/\/ Set the same Min (G1) and Max (D7) for first two DAC outputs:\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">parseProtocol0<\/span><span>({<\/span><span style=\"color:#d08770;\">0x03<\/span><span>, <\/span><span style=\"color:#d08770;\">0x03<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">20<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x04<\/span><span>, <\/span><span style=\"color:#d08770;\">0x03<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">70<\/span><span>});\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/\/ Set the full ranage for second two DAC outputs:\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">parseProtocol0<\/span><span>({<\/span><span style=\"color:#d08770;\">0x03<\/span><span>, <\/span><span style=\"color:#d08770;\">0x0C<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x04<\/span><span>, <\/span><span style=\"color:#d08770;\">0x0C<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x7F<\/span><span>});\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/\/ Set third DAC output to CC7, forth DAC output to CC14\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">parseProtocol0<\/span><span>({<\/span><span style=\"color:#d08770;\">0x02<\/span><span>, <\/span><span style=\"color:#d08770;\">0x04<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x02<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x02<\/span><span>, <\/span><span style=\"color:#d08770;\">0x08<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">0x03<\/span><span>});\n<\/span><span>    <\/span><span style=\"color:#65737e;\">\/\/ Set third DAC output CC7 to {10}, fourth DAC output CC14 to {11, 12}\n<\/span><span>    <\/span><span style=\"color:#bf616a;\">parseProtocol0<\/span><span>({<\/span><span style=\"color:#d08770;\">0x05<\/span><span>, <\/span><span style=\"color:#d08770;\">0x04<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">10<\/span><span>,\n<\/span><span>                    <\/span><span style=\"color:#d08770;\">0x05<\/span><span>, <\/span><span style=\"color:#d08770;\">0x08<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">11<\/span><span>, <\/span><span style=\"color:#d08770;\">0x06<\/span><span>, <\/span><span style=\"color:#d08770;\">0x08<\/span><span>, <\/span><span style=\"color:#d08770;\">0x00<\/span><span>, <\/span><span style=\"color:#d08770;\">12<\/span><span>});\n<\/span><\/code><\/pre>\n<p>The main display of the <code>psc<\/code> at this point in time displays the configuration. So far, this has been the only useful use of the display at all.<\/p>\n<div style=\"margin:0 auto; max-width:300px\">\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;psc-config-display.cb1c6265d9deac58.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/div>\n<p>Changing the configuration via SysEx is an awkward process, and inserting SysEx messages into PC sequencer software is not the most straightforward thing either. I did also write a standalone desktop app to produce the SysEx messages and send them over the USB serial link:<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;psc-sysex-config-dac.b0438a30ffbb9a65.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;psc-sysex-config-psg.3437510776797121.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>This doesn't lend itself to a smooth workflow, so it has stagnated at this level and I therefore don't generally change the configuration at all.<\/p>\n<h1 id=\"second-iteration\">Second iteration<\/h1>\n<p>I felt that I was outgrowing the ESP8266 a bit in this application. ESP32 devices became available and I made a board revision to take the new microcontroller. However, every other aspect of the system remained the same.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;psc_iter2_schematic.2a113dc32ba529a1.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>It was my hope that the ESP32 could implement a proper USB MIDI device, but this never materialised. I was still constrained to be sending MIDI over the USB serial interface.<\/p>\n<h1 id=\"third-iteration\">Third iteration<\/h1>\n<p>Some time later, the ESP32-S3 chips became available and these promised to implement proper USB MIDI. So, the third and current revision of the <code>psc<\/code> uses one.<\/p>\n<p>Fortunately also, the ESP32-S3 also has enough GPIO pins to directly connect all of the other hardware peripherals, so I was able to get rid of the Arduino:<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;psc_iter3_schematic.c7d97c17b19653eb.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;psc_iter3_board.5f33341354f80138.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>The \"PCB\" layout here is not exactly complete since I don't intend to actually get a PCB made; this drawing is only to facilitate a one-off hand build circuit on a prototyping board.<\/p>\n<p>This simplifies the firmware a bit as well, I ported the AY register management code to a simple mapper class and that now runs directly in the ESP32-S3. Also in this process I ported the entire firmware to <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/docs.espressif.com\/projects\/esp-idf\/en\/latest\/esp32\/get-started\/\">ESP-IDF<\/a> instead of <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/docs.platformio.org\/en\/latest\/platforms\/espressif32.html\">PlatformIO Espressif32 Arduino<\/a>. This was necessary in order to get the USB interfaces working.<\/p>\n<p>Finally though, this still requires the SysEx configuration protocol, but again, I still have just left it at the defaults and it's been working just fine so far.<\/p>\n<p>This does however properly fulfil the original requirements that the <code>psc<\/code> should be plug-and-play MIDI device available on the PC. I can now plug it in via USB and use it directly with no further supporting software or protocol bridges.<\/p>\n<p>This iteration's firmware code is <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/psc\/src\/master\/\">here<\/a> and the hardware design is <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/psc\/src\/master\/hardware\/v2\/\">here<\/a>.<\/p>\n<h1 id=\"evolution-of-the-psc\">Evolution of the <code>psc<\/code><\/h1>\n<p>One aspect of this device that I actually really enjoy is getting to use the vintage AY chip as a synth in its own right. I recently found a listing to buy some more of these chips from a well known Chinese marketplace website. The listing was apparently for 10 AY-3-8910 chips for \u00a37. I thought that was worth a punt, despite the risk that I would be sent fake or relabelled chips that are not in fact the AY that I wanted. I added these to my next order and awaited to see what arrived.<\/p>\n<p>To my surprise, the chips are legit! So, I am now planning a new device build using lessons learned from this project. This will be another ESP32-S3 controlling the following:<\/p>\n<ul>\n<li>4x AY-3-8910<\/li>\n<li>2x 8-channel DACs<\/li>\n<li>1x 1.8\" TFT LCD display<\/li>\n<li>12x VCF circuits<\/li>\n<li>3x touch pad inputs<\/li>\n<\/ul>\n<p>All of these can be run from the ESP32-S3 GPIO pins without any expanders. There is a way to control an AY chip using only the 8-bit bus and just 1 other control line.<\/p>\n<p>Because each AY chip has 3 oscillators on board, I think it would be fun to have a voltage controlled filter for each. This would make a 12 voice polyphonic chip-tune synth. I've got some of the initial design already done and partially prototyped on a breadboard, but for the moment I am not allowing myself to develop this further until I have completed a little bit of project backlog, including writing these blog posts for things I've built over the past few years. There's probably at least one more post to be written before I continue with this new synth.<\/p>\n<p>Details of this build will be documented as I go <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/fourays\/\">here<\/a>.<\/p>\n<h1 id=\"wait-a-sec-what-did-i-learn-exactly\">Wait a sec, what did I learn exactly?<\/h1>\n<p>It is clear that I couldn't end up achieving everything I wanted on the original list of requirements. In fact, some of the requirements (e.g. autonomous playing) were seriously complicated to implement, and at the same time extremely hard to even use.<\/p>\n<p>It is unfortunate that I had to wait for the release of the ESP32-S3 to get USB MIDI working, but perhaps also I should not have been so hung up on using ESP devices at all (though I am a big fan of them), I could have found another microcontroller much earlier on that implements this reliably?<\/p>\n<p>Some aspects of the device remain unfinished as well; the configuration management and the usage of the display and RGB LED are under-developed, but as previously mentioned there is just enough there to be able to get on and use the thing. After all, if the device is perpetually in development, at what point exactly am I going to get to use it? There is also the case that using the device informs development - it's quite hard to specify, design and implement something without validating requirements in some way. It would have been better perhaps throughout to have a much tighter design-use-amend feedback loop.<\/p>\n"},{"title":"Programmable Synth Controller SysEx Configuration Specification","published":"2024-01-24T00:00:00+00:00","updated":"2024-01-24T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/psc\/config-sysex\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/psc\/config-sysex\/","content":"<h1 id=\"sysex-message-structure\">SysEx Message Structure<\/h1>\n<h2 id=\"envelope-manufacturer-device-and-protocol-identifiers\">Envelope: Manufacturer, device and protocol identifiers<\/h2>\n<p>Refs:<\/p>\n<ul>\n<li>http:\/\/midi.teragonaudio.com\/tech\/midispec\/sysex.htm<\/li>\n<li>http:\/\/midi.teragonaudio.com\/tech\/midispec\/id.htm<\/li>\n<li>https:\/\/www.midi.org\/specifications-old\/item\/manufacturer-id-numbers<\/li>\n<\/ul>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>    0xF0 &lt;MANF&gt; &lt;BYTE&gt;+ 0xF7\n<\/span><\/code><\/pre>\n<p>We need to use the first 3 bytes to identify the device manufacturer:<\/p>\n<ul>\n<li><code>MANF<\/code>: <code>0x00<\/code> - using 'extended' IDs<\/li>\n<li><code>BYTE[0]<\/code>: <code>0x60<\/code> - an unallocated ID block<\/li>\n<li><code>BYTE[1]<\/code>: <code>0x00<\/code> - first number in unallocated block<\/li>\n<\/ul>\n<p>The next byte will identify the device type:<\/p>\n<ul>\n<li><code>BYTE[2]<\/code>: <code>0x00<\/code> - the PSC<\/li>\n<\/ul>\n<p>The next byte will identify the protocol version:<\/p>\n<ul>\n<li><code>BYTE[3]<\/code>: <code>0x00<\/code> - the first protocol (as per this spec)<\/li>\n<\/ul>\n<p>Therefore, the overall SysEx message for this document will be in the form<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>    0xF0 0x00 0x60 0x00 0x00 0x00 &lt;MESSAGE BYTES&gt;[] 0xF7\n<\/span><\/code><\/pre>\n<p>(!) None of the <code>&lt;MESSAGE BYTES&gt;<\/code> can take the value <code>0xF7<\/code>, because that signals the end of the SysEx message.<\/p>\n<h2 id=\"protocol-0x00\">Protocol <code>0x00<\/code><\/h2>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>    ( &lt;Config Type&gt; &lt;DAC Channel Mask&gt; &lt;PSG Channel Mask&gt; &lt;Value&gt; ) +\n<\/span><\/code><\/pre>\n<p>The config strings can be repeated, in order to set multiple configuration values in the same SysEx message.<\/p>\n<h3 id=\"config-types\">Config Types:<\/h3>\n<ul>\n<li><code>0x00<\/code> - Channel<\/li>\n<li><code>0x01<\/code> - Enable<\/li>\n<li><code>0x02<\/code> - Mode<\/li>\n<li><code>0x03<\/code> - Min<\/li>\n<li><code>0x04<\/code> - Max<\/li>\n<li><code>0x05<\/code> - CC7<\/li>\n<li><code>0x06<\/code> - CC14<\/li>\n<\/ul>\n<h3 id=\"dac-channel-mask\">DAC Channel Mask:<\/h3>\n<p>Using LSB for each of the DAC channels (4):<\/p>\n<ul>\n<li><code>0b00000001<\/code> : Apply value to DAC output A<\/li>\n<li><code>0b00000010<\/code> : Apply value to DAC output B<\/li>\n<li><code>0b00000100<\/code> : Apply value to DAC output C<\/li>\n<li><code>0b00001000<\/code> : Apply value to DAC output D<\/li>\n<\/ul>\n<p>These values should be logically <code>OR<\/code>d together to specify which DAC channels the value applies to.<\/p>\n<h3 id=\"psg-channel-mask\">PSG Channel Mask:<\/h3>\n<p>Using LSB for each of the PSG channels (4):<\/p>\n<ul>\n<li><code>0b00000001<\/code> : Apply value to PSG Voice output A<\/li>\n<li><code>0b00000010<\/code> : Apply value to PSG Voice output B<\/li>\n<li><code>0b00000100<\/code> : Apply value to PSG Voice output C<\/li>\n<li><code>0b00001000<\/code> : Apply value to PSG Noise output<\/li>\n<\/ul>\n<p>These values should be logically <code>OR<\/code>d together to specify which PSG channels the value applies to.<\/p>\n<h3 id=\"config-values\">Config Values<\/h3>\n<h4 id=\"channel\">Channel<\/h4>\n<p>There are 16 MIDI channels, so send the channel number as-is, in range <code>0x00<\/code> - <code>0x0F<\/code>.<\/p>\n<h4 id=\"enable-for-dac-channels\">Enable (for DAC channels)<\/h4>\n<p>Using LSB as a mask for <code>Value<\/code>, <code>Gate<\/code>, <code>Trigger<\/code> respectively;<\/p>\n<ul>\n<li><code>0b00000001<\/code> : Enable Value output<\/li>\n<li><code>0b00000010<\/code> : Enable Gate output<\/li>\n<li><code>0b00000100<\/code> : Enable Trigger output<\/li>\n<\/ul>\n<h3 id=\"enable-for-psg-channels\">Enable (for PSG channels)<\/h3>\n<p>Using LSB as a mask for <code>Enable<\/code>, <code>Mix A<\/code>, <code>Mix B<\/code>, <code>Mix C<\/code> respectively;<\/p>\n<ul>\n<li><code>0b00000001<\/code> : Enable PSG Voice output<\/li>\n<li><code>0b00000010<\/code> : Enable PSG Noise output output to Voice A<\/li>\n<li><code>0b00000100<\/code> : Enable PSG Noise output output to Voice B<\/li>\n<li><code>0b00001000<\/code> : Enable PSG Noise output output to Voice C<\/li>\n<\/ul>\n<h3 id=\"mode\">Mode<\/h3>\n<p><strong>DAC and PSG Noise channels only; ignored for PSG Voice channels.<\/strong><\/p>\n<ul>\n<li><code>0x00<\/code> : Note value<\/li>\n<li><code>0x01<\/code> : Velocity value<\/li>\n<li><code>0x02<\/code> : CC (7 bit)<\/li>\n<li><code>0x03<\/code> :\n<ul>\n<li>For DAC: CC (14 bit) (interpreted as 12 bit using the 12 MSB)<\/li>\n<li>For PSGN: Not supported, reverts to Note mode<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h3 id=\"min\">Min<\/h3>\n<p><strong>DAC Channels only.<\/strong><\/p>\n<p>Set the MIDI note value which corresponds to minimum output, in range <code>0x00<\/code> - <code>0x7F<\/code>.<\/p>\n<h3 id=\"max\">Max<\/h3>\n<p><strong>DAC Channels only.<\/strong><\/p>\n<p>Set the MIDI note value which corresponds to maximum output, in range <code>0x00<\/code> - <code>0x7F<\/code>.<\/p>\n<h2 id=\"cc7\">CC7<\/h2>\n<p><strong>DAC channels:<\/strong><\/p>\n<p>Set the 7-bit CC number to use for the output value in CC7 mode, or the CC number to use for MSB in CC14 mode.\nIn range <code>0x00<\/code> - <code>0x7F<\/code>.<\/p>\n<p><strong>PSG Noise channel:<\/strong><\/p>\n<p>Set the 7-bit CC number to use for the output value, in range <code>0x00<\/code> - <code>0x7F<\/code>.<\/p>\n<h2 id=\"cc14\">CC14<\/h2>\n<p><strong>DAC channels only.<\/strong><\/p>\n<p>Set the 7-bit CC number to use for the output value in CC7 mode, or the CC number to use for MSB in CC14 mode.\nIn range <code>0x00<\/code> - <code>0x7F<\/code>.<\/p>\n<h3 id=\"examples\">Examples<\/h3>\n<p>These are examples of configuration strings, including the SysEx envelope.<\/p>\n<p><strong>1. Set each output to sequential MIDI channels, 1-8:<\/strong><\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>    0xF0 0x00 0x60 0x00 0x00 0x00\n<\/span><span>        0x00 0x01 0x00 0x00\n<\/span><span>        0x00 0x02 0x00 0x01\n<\/span><span>        0x00 0x04 0x00 0x02\n<\/span><span>        0x00 0x08 0x00 0x03\n<\/span><span>        0x00 0x00 0x01 0x04\n<\/span><span>        0x00 0x00 0x02 0x05\n<\/span><span>        0x00 0x00 0x04 0x06\n<\/span><span>        0x00 0x00 0x08 0x07\n<\/span><span>    0xF7\n<\/span><\/code><\/pre>\n<p><strong>2. Enable Value, Gate, Trigger for the DAC outputs A and B, Value only for DAC outputs C and D:<\/strong><\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>    0xF0 0x00 0x60 0x00 0x00 0x00\n<\/span><span>        0x01 0x03 0x00 0x07\n<\/span><span>        0x01 0x0C 0x00 0x01\n<\/span><span>    0xF7\n<\/span><\/code><\/pre>\n<p><strong>3. Set all DAC outputs to CC7 mode, all PSG to Note mode:<\/strong><\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>    0xF0 0x00 0x60 0x00 0x00 0x00\n<\/span><span>        0x02 0x0F 0x00 0x02\n<\/span><span>        0x02 0x00 0x0F 0x00\n<\/span><span>    0xF7\n<\/span><\/code><\/pre>\n<p><strong>4. Set the same Min (G1) and Max (D7) for all DAC outputs:<\/strong><\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>    0xF0 0x00 0x60 0x00 0x00 0x00\n<\/span><span>        0x03 0x0F 0x00 0x1F\n<\/span><span>        0x04 0x0F 0x00 0x62\n<\/span><span>    0xF7\n<\/span><\/code><\/pre>\n<p><strong>5. Assign incrementing CC number pairs to DAC outputs and then set to CC14 mode:<\/strong><\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>    0xF0 0x00 0x60 0x00 0x00 0x00\n<\/span><span>        0x05 0x01 0x00 0x14\n<\/span><span>        0x05 0x02 0x00 0x15\n<\/span><span>        0x05 0x04 0x00 0x16\n<\/span><span>        0x05 0x08 0x00 0x17\n<\/span><span>        0x06 0x01 0x00 0x32\n<\/span><span>        0x06 0x02 0x00 0x33\n<\/span><span>        0x06 0x04 0x00 0x34\n<\/span><span>        0x06 0x08 0x00 0x35\n<\/span><span>        0x02 0x0F 0x00 0x03\n<\/span><span>    0xF7\n<\/span><\/code><\/pre>\n"},{"title":"Programmable Synth Controller Bluetooth Services","published":"2024-01-24T00:00:00+00:00","updated":"2024-01-24T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/psc\/esp32-services\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/psc\/esp32-services\/","content":"<h1 id=\"esp32-services\">ESP32 Services<\/h1>\n<h2 id=\"ble\">BLE<\/h2>\n<h3 id=\"research\">Research<\/h3>\n<p>Relevant services:<\/p>\n<ul>\n<li>https:\/\/www.bluetooth.com\/wp-content\/uploads\/Sitecore-Media-Library\/Gatt\/Xml\/Services\/org.bluetooth.service.device_information.xml<\/li>\n<li>https:\/\/www.bluetooth.com\/wp-content\/uploads\/Sitecore-Media-Library\/Gatt\/Xml\/Services\/org.bluetooth.service.transport_discovery.xml<\/li>\n<\/ul>\n<p>Relevant characteristics:<\/p>\n<ul>\n<li>https:\/\/www.bluetooth.com\/wp-content\/uploads\/Sitecore-Media-Library\/Gatt\/Xml\/Characteristics\/org.bluetooth.characteristic.analog_output.xml<\/li>\n<li>https:\/\/www.bluetooth.com\/wp-content\/uploads\/Sitecore-Media-Library\/Gatt\/Xml\/Characteristics\/org.bluetooth.characteristic.digital_output.xml<\/li>\n<li>https:\/\/www.bluetooth.com\/wp-content\/uploads\/Sitecore-Media-Library\/Gatt\/Xml\/Characteristics\/org.bluetooth.characteristic.network_availability.xml<\/li>\n<\/ul>\n<h3 id=\"program-service\">Program service<\/h3>\n<p>UUID: <code>da3c76a3-adb8-4000-9000-d9f7fb4d934d<\/code><\/p>\n<p>Characteristics:<\/p>\n<ul>\n<li>\n<p>Program data:<\/p>\n<ul>\n<li>Description: Load a PSC bytecode program and run it<\/li>\n<li>UUID: <code>da3c76a3-adb8-4000-9001-d9f7fb4d934d<\/code><\/li>\n<li>Write only<\/li>\n<li>Data format: <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/psc\/pscs-specification\/\">PSCB bytecode<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h3 id=\"memory-service\">Memory service<\/h3>\n<p>UUID: <code>da3c76a3-adb8-4001-9000-d9f7fb4d934d<\/code><\/p>\n<p>Characterstics:<\/p>\n<ul>\n<li>\n<p>Memory data:<\/p>\n<ul>\n<li>Description: Write data into the PSC memory<\/li>\n<li>UUID: <code>da3c76a3-adb8-4001-9001-d9f7fb4d934d<\/code><\/li>\n<li>Write only<\/li>\n<li>Notify<\/li>\n<li>Data format: <code>AH AL DH DL [DH DL...]<\/code>\n<ul>\n<li>16-bit start address followed by 16 bit data<\/li>\n<li>Data overflowing max address will be ignored<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h3 id=\"outputs-service\">Outputs service<\/h3>\n<p>UUID: <code>da3c76a3-adb8-4002-9000-d9f7fb4d934d<\/code><\/p>\n<p>Characteristics:<\/p>\n<ul>\n<li>\n<p>Output data:<\/p>\n<ul>\n<li>Description: Immediately stop any PSC program and update outputs with the given values<\/li>\n<li>UUID: <code>da3c76a3-adb8-4002-9001-d9f7fb4d934d<\/code><\/li>\n<li>Write only, no response<\/li>\n<li>Data format: <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/psc\/live-play-protocol\/\">live-play-protocol<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h3 id=\"wifi-service\">WIFI Service<\/h3>\n<p>UUID: <code>da3c76a3-adb8-4003-9000-d9f7fb4d934d<\/code><\/p>\n<p>Characteristics:<\/p>\n<ul>\n<li>\n<p>Network credentials<\/p>\n<ul>\n<li>Description: Store an SSID\/Password pair for WiFi connection<\/li>\n<li>UUID: <code>da3c76a3-adb8-4003-9001-d9f7fb4d934d<\/code><\/li>\n<li>Write only<\/li>\n<li>Data format: <code>0x02 &lt;SSID bytes&gt; 0x1F &lt;Password bytes&gt; 0x03<\/code>\n<ul>\n<li>Writing <code>0x00<\/code> as the password will erase the SSID from the local store<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>Preferred network<\/p>\n<ul>\n<li>Description: Set the preferred SSID for WiFi auto connection<\/li>\n<li>UUID: <code>da3c76a3-adb8-4003-9002-d9f7fb4d934d<\/code><\/li>\n<li>Write only<\/li>\n<li>Data format: <code>0x02 &lt;SSID bytes&gt; 0x03<\/code>\n<ul>\n<li>Writing <code>0x02 0x03<\/code> will un-set the preferred network<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>Connection state<\/p>\n<ul>\n<li>Description: Return the current WiFi connection state<\/li>\n<li>UUID: <code>da3c76a3-adb8-4003-9003-d9f7fb4d934d<\/code><\/li>\n<li>Read only, Notify<\/li>\n<li>Data format: <code>0x02 &lt;Status byte&gt; 0x1F &lt;IP address&gt;? 0x03<\/code>\n<ul>\n<li>Status byte: ENUM\n<ul>\n<li>0x00: WL_IDLE_STATUS<\/li>\n<li>0x01: WL_NO_SSID_AVAIL<\/li>\n<li>0x02: WL_SCAN_COMPLETED<\/li>\n<li>0x03: WL_CONNECTED<\/li>\n<li>0x04: WL_CONNECT_FAILED<\/li>\n<li>0x05: WL_CONNECTION_LOST<\/li>\n<li>0x06: WL_DISCONNECTED<\/li>\n<\/ul>\n<\/li>\n<li>IP Address: string<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h2 id=\"network-services\">Network services<\/h2>\n<h3 id=\"outputs-service-1\">Outputs service<\/h3>\n<ul>\n<li>Description: Immediately stop any PSC program and update outputs with the given values<\/li>\n<li>Protocol: TCP<\/li>\n<li>Port: 8080<\/li>\n<li>Data format: <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/psc\/live-play-protocol\/\">live-play-protocol<\/a><\/li>\n<\/ul>\n"},{"title":"Programmable Synth Controller Live Play Protocol","published":"2024-01-24T00:00:00+00:00","updated":"2024-01-24T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/psc\/live-play-protocol\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/psc\/live-play-protocol\/","content":"<h1 id=\"immediate-live-interface\">Immediate (Live) interface<\/h1>\n<p><code>STX R DD [DD...] ETX<\/code><\/p>\n<p>R is the start register:<\/p>\n<ul>\n<li>0x11 = DACA<\/li>\n<li>0x12 = DACB<\/li>\n<li>0x13 = DACC<\/li>\n<li>0x14 = DACD<\/li>\n<li>0x15 = PSGA<\/li>\n<li>0x16 = PSGB<\/li>\n<li>0x17 = PSGC<\/li>\n<li>0x18 = PSGN<\/li>\n<\/ul>\n<p>DD is the 16 bit register data.\nData will continue to be read into each subsequent register until either:<\/p>\n<ul>\n<li>'aligned' ETX received<\/li>\n<li>R = 19; i.e. there's no more registers to write to. All data until the next aligned ETX will be ignored.<\/li>\n<\/ul>\n<h2 id=\"example-1-set-single-register\">Example 1: Set single register<\/h2>\n<p><code>02 12 EB 14 03<\/code><\/p>\n<p>Set DACB; message enabled, gate enabled, trig enabled, value 2836<\/p>\n<h2 id=\"example-2-set-multiple-adjacent-registers\">Example 2: Set multiple adjacent registers<\/h2>\n<p><code>02 15 C0 2B C0 2F C0 32 03<\/code><\/p>\n<p>Set all PSG tone generators; message enabled, velocity 64, chord of MIDI notes G2, B2, D3<\/p>\n<h2 id=\"example-3-set-multiple-adjacent-registers-superfluous-data\">Example 3: Set multiple adjacent registers, superfluous data<\/h2>\n<p><code>02 17 C0 32 C0 32 xx xx 03<\/code><\/p>\n<p>Set PSG tone generators B and C to MIDI notes D3. xx bytes will be ignored.<\/p>\n"},{"title":"Programmable Synth Controller Source Specification","published":"2024-01-24T00:00:00+00:00","updated":"2024-01-24T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/psc\/pscs-specification\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/psc\/pscs-specification\/","content":"<h1 id=\"nomenclature\">NOMENCLATURE<\/h1>\n<h2 id=\"addresses\">Addresses<\/h2>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>DST\n<\/span><span>SRC\n<\/span><span>RB\n<\/span><span>RE\n<\/span><span>TB\n<\/span><\/code><\/pre>\n<p>Addresses are 16 bit and written as 4 case insensitive hexadecimal characters with no spaces and no prefix or suffix. e.g.:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>0000\n<\/span><span>00f8\n<\/span><span>1B6F\n<\/span><span>F0e4\n<\/span><\/code><\/pre>\n<h2 id=\"literals\">Literals<\/h2>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>$LIT\n<\/span><span>$TSTEP\n<\/span><span>$RSTEP\n<\/span><span>$COUNT\n<\/span><\/code><\/pre>\n<p>Literal values are 16 bit and written as 4 case insensitive hexadecimal characters with no spaces and no prefix or suffix. (Examples same as per Addresses).<\/p>\n<h2 id=\"interrupts\">Interrupts<\/h2>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>I\n<\/span><\/code><\/pre>\n<p>Interrupts numbers are 8 bit and written as 2 case insensitive hexadecimal characters with no spaces and no prefix or suffix. e.g.:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>00\n<\/span><span>01\n<\/span><span>F4\n<\/span><\/code><\/pre>\n<p>Interrupt functions are Interpreter specific and not defined here.<\/p>\n<h1 id=\"opcodes\">OPCODES<\/h1>\n<h2 id=\"general\">GENERAL<\/h2>\n<p>No operation:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>NOP\n<\/span><\/code><\/pre>\n<h2 id=\"memory-management\">MEMORY MANAGEMENT<\/h2>\n<p>Copy literal value <code>$LIT<\/code> to address <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>MOVL    DST, $LIT\n<\/span><\/code><\/pre>\n<p>Copy value at address <code>SRC<\/code> to address <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>MOVR    DST, SRC\n<\/span><\/code><\/pre>\n<p>Fill a range of memory in address range <code>RB<\/code> to <code>RE<\/code> inclusive with value <code>$LIT<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>FILL     RB, RE, $LIT\n<\/span><\/code><\/pre>\n<p>Copy a <code>$COUNT<\/code> values of memory from address range starting <code>RB<\/code> stepping <code>$RSTEP<\/code> addresses at a time to range starting <code>TB<\/code> stepping <code>$TSTEP<\/code> addresses at a time:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>COPY    TB, RB, $TSTEP, $RSTEP, $COUNT\n<\/span><\/code><\/pre>\n<h2 id=\"interrupts-1\">INTERRUPTS<\/h2>\n<p>Raise interrupt number <code>I<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>INT     I\n<\/span><\/code><\/pre>\n<p>Interrupt data: when interrupt <code>I<\/code> is raised, attach memory address range <code>RB<\/code> to <code>RE<\/code> inclusive to the handler's data:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>INTM    I, RB, RE\n<\/span><\/code><\/pre>\n<p>Remove any attached data when interrupt <code>I<\/code> is raised:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>INTR    I\n<\/span><\/code><\/pre>\n<h2 id=\"arithmetic\">ARITHMETIC<\/h2>\n<p><em>Note that no effort is spent in the base interpreter for handling overflow or underflow. It will just happen without warning or notification.<\/em><\/p>\n<p><em>Note that in the base interpreter, division or modulus by zero will cause your program to halt without warning or notification.<\/em><\/p>\n<p>Increment value at address <code>DST<\/code> by 1:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>INC     DST\n<\/span><\/code><\/pre>\n<p>Decrement value at address <code>DST<\/code> by 1:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>DEC     DST\n<\/span><\/code><\/pre>\n<p>Add literal value <code>$LIT<\/code> to value at address <code>DST<\/code> and store result in <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>ADDL    DST, $LIT\n<\/span><\/code><\/pre>\n<p>Add value at address <code>SRC<\/code> to value at address <code>DST<\/code> and store result in <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>ADDR    DST, SRC\n<\/span><\/code><\/pre>\n<p>Subtract literal value <code>$LIT<\/code> from value at address <code>DST<\/code> and store result in <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>SUBL    DST, $LIT\n<\/span><\/code><\/pre>\n<p>Subtract value at address <code>SRC<\/code> from value at address <code>DST<\/code> and store result in <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>SUBR    DST, SRC\n<\/span><\/code><\/pre>\n<p>Multiply value at address <code>SRC<\/code> by $LIT and store result in <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>MULL    DST, $LIT\n<\/span><\/code><\/pre>\n<p>Multiply value at address <code>SRC<\/code> to value at address <code>DST<\/code> and store result in <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>MULR    DST, SRC\n<\/span><\/code><\/pre>\n<p>Divide value at address <code>DST<\/code> by $LIT and store result in <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>DIVL    DST, $LIT\n<\/span><\/code><\/pre>\n<p>Divide value at address <code>DST<\/code> by value at address <code>SRC<\/code> and store result in <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>DIVR    DST, SRC\n<\/span><\/code><\/pre>\n<p>Modulus value at address <code>DST<\/code> by $LIT and store result in <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>MODL    DST, $LIT\n<\/span><\/code><\/pre>\n<p>Modulus value at address <code>DST<\/code> by value at address <code>SRC<\/code> and store result in <code>DST<\/code>:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>MODR    DST, SRC\n<\/span><\/code><\/pre>\n<h2 id=\"flow-control\">FLOW CONTROL<\/h2>\n<p>Jump Conditional:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>JCN     COND, SRC, LABEL\n<\/span><\/code><\/pre>\n<p><code>COND<\/code>:<\/p>\n<p>This is a single hexadecimal digit:<\/p>\n<ul>\n<li>if F then Unconditional, <code>SRC<\/code> ignored, else<\/li>\n<li>if 0 then Jump if value at address <code>SRC<\/code> == 0, else<\/li>\n<li>if 1 then Jump if value at address <code>SRC<\/code> != 0, else<\/li>\n<li><code>NOP<\/code><\/li>\n<\/ul>\n<p>Halt execution:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>HLT\n<\/span><\/code><\/pre>\n<h1 id=\"memory-map\">MEMORY MAP<\/h1>\n<h2 id=\"mandatory\">Mandatory<\/h2>\n<p>The base Interpreter provides general purpose ram in the range 0x0000 - 0x00FF inclusive (256 16 bit words).\nThis is kept deliberately small since this is likely to be compiled into an embedded device by default.<\/p>\n<p>Extended interpreters may map additional memory immediately after this range (up to 0xAFFF).<\/p>\n<h2 id=\"psc-recommended\">PSC recommended<\/h2>\n<p>0xB000 - 0xFFFF (20k words) is reserved for 160x128x16 graphics memory.<\/p>\n"},{"title":"Analogue Synthesizer","published":"2024-01-16T00:00:00+00:00","updated":"2024-01-16T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/analogue-synth\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/analogue-synth\/","content":"<h2 id=\"a-box-that-goes-bleep\">A box that goes bleep<\/h2>\n<p>Ever since starting to make my own gadgets one project that stood out to me as a must-do project was to build a modular analogue synthesizer. Taking this on was no small feat, and took the best part of 5 to 6 years to complete enough to be a mostly usable instrument. Even then, interfacing the synth to a computer DAW took longer still and I have only just recently completed this component.<\/p>\n<p>I took a long time to consider the existing form factors for analogue synths and dismissed them all; mostly due to either cost or because components of those factors would be outside of my ability to make - for example the racks\/enclosures commonly used. Putting the final form aside, I started to figure out what modules I wanted and also the bill of materials required to start building them.<\/p>\n<p>I still have no idea about the electronics theory of synth circuits, I have clearly found circuit designs elsewhere to implement. I found a lot of great designs and information at <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.yusynth.net\/Modular\/index_en.html\">YuSynth<\/a> back in 2014 and rolled with it as a starting point.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;YuSynth_Schematics.3d5cfd8e66ca8eb7.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"getting-started\">Getting started<\/h2>\n<p>From 2014 - 2015 I started assessing which modules I wanted to build. I constructed a huge spreadsheet of modules and components in order to calculate exactly what I would need to purchase for any combination of modules in the synth.<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Module_Select_Sheet.6d23ef86aefae4b1.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Full_BOM_Sheet.b061325d9e7bd4a8.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<blockquote>\n<p>BOM Spreadsheets can be so much fun ?<\/p>\n<\/blockquote>\n<p>Using this sheet I reached the point of tedium counting up the number of every type of component in a few different modules and also going to find a source to buy them and figuring out ordering quantities and cost - at which point I elected that the minimum viable synth should consist of:<\/p>\n<ul>\n<li>One voltage controlled amplifier (VCA)<\/li>\n<li>One low frequency oscillator (LFO)<\/li>\n<li>One voltage controlled oscillator (VCO)<\/li>\n<li>One voltage controlled filter (VCF)<\/li>\n<li>One ADSR envelope generator (ADSR)<\/li>\n<\/ul>\n<p>Once I had decided on which modules I was going to build, I started taking YuSynth's circuits and re-making them in <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.kicad.org\/\">KiCAD<\/a>.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Schematic_LFO.eea5798c4d9671f4.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>YuSynth's VCLFO #2<\/p>\n<\/blockquote>\n<p>From there I painstakingly used KiCAD to make strip board layouts of the first circuits, since that was all I could comprehend myself doing - make the circuits by hand on strip board. Using a very kludgy workflow of exporting the KiCAD board layers as images and drawing a grid over them in <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.gimp.org\/\">GIMP<\/a> I eventually started assembling the first circuit.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180906_203712.f2c40cd17584fa75.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>This was pretty seriously tedious work, but I continued to make one circuit and not a lot else. I was getting bogged down with the hand-made-ness of it all and found it hard to keep going when I still had no idea how to assemble all the parts I was lining up to make.<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180908_191055.4ed79800acd03605.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180908_191104.971273cb70b70ac1.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>Meanwhile I was also thinking about a digital control module, because for sure want to control the synthesizer from my PC DAW software, as a MIDI device. I prototyped something using an ESP8622, Arduino Nano, MCP4728 DAC and some LEDs. This module will come to be known as <code>psc<\/code> - more about this later, as the story unfolds I have only recently rebuilt and completed this module to my satisfaction.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180830_092922.6628621614c4a1af.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>The project stagnated around this level of completion for a while in this state. A few other projects came and go in the intervening years, but I was keeping my mind and eyes open for a way to bring this all together.<\/p>\n<hr \/>\n<h2 id=\"enclosure-encounter\">Enclosure encounter<\/h2>\n<p>Fast forward to about 2019 and in the course of clearing out a whole load of household junk inherited from my parents, I came across a rather neat wooden box, with latching lid and carry handle which immediately sparked inspiration. It was about the right size to house a good number of synth modules and would also be nicely portable. I don't have any pictures of the box before I started the build, so spoiler alert, here it is with the front panel from the future being test-fitted:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190408_171638.2f409af8bb2b7283.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I also happened to find a stack of suitable power supplies, and one provides +12V, -12V, +5V from a mains input; perfect for powering the synth.<\/p>\n<p>Inspired by these finds, I went back to the drawing board to revisit how to construct the circuits.<\/p>\n<h2 id=\"pcbs-might-actually-be-feasible\">PCBs might actually be feasible?<\/h2>\n<p>I decided I was not going to go through with making all the circuits by hand on strip board. I then came to find out that services such as JLCPCB and PCBWay are actually pretty affordable to the hobbyist maker. Also, I already had the circuit schematics drawn up in KiCAD.<\/p>\n<p>I have no idea how long the process took, but I drew up 2-layer PCBs for all the modules I was going to build. By this time this consisted of:<\/p>\n<ul>\n<li>2x VCO<\/li>\n<li>2x ADSR<\/li>\n<li>2x LFO<\/li>\n<li>2x VCF<\/li>\n<li>4x VCA<\/li>\n<\/ul>\n<p>This would provide a 2 voice synth with enough options to get funky with some modulations and filtering. I figured also that more VCAs would be helpful for the <code>psc<\/code> MIDI control as well.<\/p>\n<p>I spent some considerable time adjusting the PCB layouts to be as compact as possible, since getting 12 circuits and a power supply and a <code>psc<\/code> into this box with through-hole components could end up being a bit of a squeeze.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PCB_VCO.4b46c4b8430cf44e.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>Compact VCO<\/p>\n<\/blockquote>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PCB_ADSR.9746a968ecac292a.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>Compact ADSR<\/p>\n<\/blockquote>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PCB_LFO.aa2116731495a562.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>Compact LFO<\/p>\n<\/blockquote>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PCB_VCF.341eb8729ee80f90.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>Compact VCF<\/p>\n<\/blockquote>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PCB_VCA.7b95824215238c9d.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>Compact VCA<\/p>\n<\/blockquote>\n<p>The next thing to do was figure out how to get these manufactured. I didn't really want to end up with a whole load of different sized boards, and I figured the board mounting would be easier if I could put multiple circuits on one board and have maybe 2 larger boards of the same size.<\/p>\n<p>That's actually what I did - I copied the PCB layouts for each module into 2 composite boards of the same size (200mm x 150mm), which would both fit side by side in the enclosure, leaving some space also for a power supply.<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PCB_Comp_A.c5f39225837dc0a6.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;PCB_Comp_B.e0ee1b2f7f96cb33.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>Each module brings the connections to its panel components to named holes, and I also put in some headers to distribute power from the PCB to each module individually. To keep the boards tidy and the circuits isolated I decided that each circuit would be powered with it own headers from flying leads rather than try to route power around the modules on the composite boards.<\/p>\n<p>I sent the files off to JLCPCB and a short while later <em>five<\/em> sets (the minimum order quantity) arrived even though I only needed 1 of each type. I still have the spares somewhere, let me know if you want a set, I don't plan to make any more of these up (<em>caveat: there's one error I know of on the physical boards. I corrected it in the CAD but I had to cut a track and re-arrange something in the build; I forget exactly what that was though<\/em>).<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190610_181712.13d65f870843ff31.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>A few days worth of soldering later (and still not completed in these photos):<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190624_220416.79565aee508c9935.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190617_203808.98af0944c1b34347.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190617_204000.261b448d4e69272f.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<h2 id=\"assembly-into-the-enclosure\">Assembly into the enclosure<\/h2>\n<p>During 2019 I was briefly a member of the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/southlondonmakerspace.org\/\">South London Maker Space<\/a> and during that time was able to use their laser cutter. I made use of this time to design both the front\npanel for the synth and also a set of frames to hold the internal components. The frames are designed to be cut from sheet plywood and slot together into a 3-dimensional structure to fill the box and support the other parts. The enclosure itself already had internal grooves cut into the sides, which I have used by leaving bumps on the frame to help keep the frame structure in place.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Synth_Box_Frame.c45731dc24ecab37.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>There are two halves to the internal framing. The first is arranged into two decks; the lower one holds the module PCBs and the upper holds the <code>psc<\/code> board. To the right of that the second frame holds the power supply in place.<\/p>\n<p>Here are the main boards on the lower deck alongside the power supply in the box:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190626_225652.a8d6f08ad23f478b.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"front-panel\">Front panel<\/h2>\n<p>Laying out the front panel was a lot of fun. This is the part of the project that lends itself to a bit of creativity and thinking about how the synth will actually be used. Given that this synth has at least 2 of everything, that suggested to me the idea of symmetry.<\/p>\n<p>I took that a little bit to the extreme and made the panel for the synth modules symmetrical down the middle. This means that one voice's worth of modules are on the left and the other on the right - they are mirror images of each other (<em>except for the VCAs<\/em>). I realise this is unconventional, no other user interface I have ever seen has done this, but on this musical instrument, it works.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Panel_CAD.ea8fee4ab7fc759f.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>The top row of the panel is not symmetrical exactly, as this contains some utility patch points, the <code>psc<\/code> interface and the power input. The middle row has the modules<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>[ VCF, LFO, VCA, VCA | VCA, VCA, LFO, VCF ]\n<\/span><\/code><\/pre>\n<p>and the bottom row has<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>[ VCO, ADSR | ADSR, VCO ]\n<\/span><\/code><\/pre>\n<p>Populating the front panel components you can see the symmetry emerging:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190518_190227.35a3637f33b79295.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>On the reverse, starting the panel wiring loom:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190605_215441.56708917e8824fe8.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>This wouldn't have been possible without colour coded drawings of what each component connects to:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Panel_Wiring_Guide.c8995e08f1430b78.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"final-assembly\">Final assembly<\/h2>\n<p>With the frames, PCBs and front panel all ready, it was time to bring it all together.<\/p>\n<p>Wiring panel loom into the boards, with power leads:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190907_171850.179cfd0fdb273ea4.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Completed module panel wiring and assembly into the box:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190911_213942.4f0a3010be3c25d0.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<blockquote>\n<p>If I recall correctly, there's something like over 30m of wire in this thing<\/p>\n<\/blockquote>\n<p>And finally with the front panel fitted with knobs:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20190911_215915.ec9c51343e465a6a.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"samples\">Samples?<\/h2>\n<p>Erm... OK you got me - to date I still haven't really used this thing for much at all. I will blame this on the lack of a working <code>psc<\/code> until recently. I also sort of have a history of building devices I don't really use. I should probably stop doing that and also spending 2 days worth of free time simply writing about each project. Or maybe not; a lot of the fun is in the making.<\/p>\n<h2 id=\"what-about-the-psc\">What about the <code>psc<\/code> ?<\/h2>\n<p>This post has got quite long already and <code>psc<\/code> deserves its own write-up since it went through several iterations both conceptually and physically. You can read more <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/psc\/\">here<\/a><\/p>\n"},{"title":"Connoise","published":"2024-01-13T00:00:00+00:00","updated":"2024-01-13T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/connoise\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/connoise\/","content":"<h2 id=\"connected-noise-machine\">Connected Noise Machine<\/h2>\n<p><code>CONNOISE<\/code> was an exercise in exploring software architecture for real-time audio synthesis. I wanted to take the concept of a modular synthesizer and replicate that in software, whereby each module has a number of inputs and outputs and performs a single function. This was also an exercise in learning some of the newer c++17 language features, but I also set myself the goal that the project should be as well documented as possible. The project builds with Doxygen docs and has a pretty complete <code>README.md<\/code> file. The documentation was a lot of extra work, but ensured that I took the project seriously and encouraged me to publish it all as open source.<\/p>\n<p>I stared this in January 2021 and worked on it as my primary spare time project for about 2 months.<\/p>\n<h2 id=\"architecture\">Architecture<\/h2>\n<p><code>CONNOISE<\/code> ended up being structured as a library (<code>libcno<\/code>) including all of the synthesizer implementation and a file format parser and serialiser, a CLI application (<code>connoise<\/code>) and a GUI application (<code>connoise-gui<\/code>).<\/p>\n<p>This makes the CLI application pretty trivial, it parses the arguments and instantiates the system from the library. Similarly for the GUI, the application is all GUI code and uses the library APIs to manage the synthesizer system.<\/p>\n<pre class=\"mermaid\">\n  graph LR\nconnoise &lt;--&gt; System\nconnoise-gui &lt;--&gt; System\n\nsubgraph C[libcno]\ndirection TB\nSystem\n\n    Parser -.-&gt; System\n    System &lt;-.-&gt; SystemModel\n    Transport -.-&gt; System\n    System -.-&gt; ModulesManager\n    ModulesManager -.- Registry\n    Registry -.- Inputs\n    Registry -.- Outputs\n    Registry -.- Modules\n\n    Inputs -.- I01[Constant]\n    Inputs -.- I02[MIDI Device]\n    Inputs -.- I03[Null]\n\n    Outputs -.- O01[Audio Device]\n    Outputs -.- O02[Null]\n    Outputs -.- O03[WAV File]\n\n    Modules -.- M01[ADSR]\n    Modules -.- M02[Delay]\n    Modules -.- M03[FIR Filter]\n    Modules -.- M04[Hold]\n    Modules -.- M05[Hz to MIDI Note]\n    Modules -.- M06[MIDI Note to Hz]\n    Modules -.- M07[Math Abs]\n    Modules -.- M08[Math Add]\n    Modules -.- M09[Math Differential]\n    Modules -.- M10[Math Multiply]\n    Modules -.- M11[Mixer]\n    Modules -.- M12[Oscillator Saw]\n    Modules -.- M13[Oscillator Sine]\n    Modules -.- M14[Oscillator Square]\n    Modules -.- M15[Random]\n    Modules -.- M16[Sequencer]\n\nend\n<\/pre>\n<h3 id=\"system-and-systemmodel\"><code>System<\/code> and <code>SystemModel<\/code><\/h3>\n<p>The interesting part here is the use of the <code>SystemModel<\/code> which implements the directed graph of <code>Module<\/code> <em>port connections<\/em> - this is used not only in the GUI for rendering the system layout, but also to figure out the graph dependencies in order to actually render the flow of signals in the correct order. The <code>SystemModel<\/code> is an abstract model representation of the actual <code>System<\/code>. The <code>System<\/code> owns all the objects which actually do any useful work, but the <code>SystemModel<\/code> contains the description of the <code>System<\/code> which makes it easier to inspect and work with. The <code>SystemModel<\/code> implements the algorithm for figuring out how to process the nodes in the graph in parallel. Given that all modern computers have multi-core processors, and real-time audio rendering requires the entire graph to be calculated with strict timing requirements, the algorithm is required in order to distribute the calculations amongst all CPU cores (using threads) and therefore allowing larger graphs to be rendered than would be possible if only one CPU core were available.<\/p>\n<p>The parallel calculation algorithm takes the system graph and performs a breadth first search across all nodes from inputs to outputs. As it does this, it records the distance of each node from the output, and groups the nodes by distance. Since the distance describes the dependency order of the nodes, we can process all nodes with the same distance value at once. This is how rendering proceeds - all nodes in each distance group are processed in parallel before moving over to the next group. The distance groups themselves are sorted highest to lowest, meaning the nodes furthest from the output node are processed first, as we would expect the signals to be flowing from inputs to outputs.<\/p>\n<h3 id=\"modules\">Modules<\/h3>\n<p>The <code>Module<\/code>s themselves have a fairly basic API, they can declare their own type, get allocated unique <code>ID<\/code>s and have accessors for any number of named input and output ports. The remaining methods allow the <code>System<\/code> to initialise and de-initialise the module's ports for rendering and call the ports themselves for each update. Notably, there is not a single <code>update<\/code> method in each <code>Module<\/code>, the processing actually happens at each input and output port. For example, in a module which implements a sine wave oscillator, the sine function would be part of the <code>v<\/code> output port and not the module itself.<\/p>\n<blockquote>\n<p>As an aside, in this manner actually <code>CONNOISE<\/code> only really needs one oscillator module type, because they all have mostly the same input parameters (frequency, phase) but each waveform type could be implemented on each of the output ports, we could have a port for sine, a port for square, etc. In practice though there are some variations on the parameters for each oscillator type, so it makes sense to have them as separate module types.<\/p>\n<\/blockquote>\n<h3 id=\"other-components\">Other components<\/h3>\n<p><code>Transport<\/code> controls the playing state of the system, much like the controls on any media player, it has a basic state machine to ensure only the correct transitions can happen (although in practice this is mostly <code>Stop<\/code> &lt;--&gt; <code>Play<\/code>). It is also responsible for preparing port rendering buffers before rendering starts, and for calling all of the port update functions in the rendering loop, and for cleaning up the buffers at the end of rendering.<\/p>\n<p><code>Parser<\/code> implements the grammar for the <code>cno<\/code> file format, and during parsing can populate the <code>System<\/code> with all of the valid items found in the file data.<\/p>\n<p>We needed <code>ModulesManager<\/code> and <code>Registry<\/code> to be able to find module implementations by name, this maps the names declared in the <code>cno<\/code> file description to code implementation.<\/p>\n<h2 id=\"gui\">GUI<\/h2>\n<p>I wrote the GUi using <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/ocornut\/imgui\">Dear Imgui<\/a> and <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/thedmd\/imgui-node-editor\">imgui-node-editor<\/a>. Dear Imgui is amazingly simple to use and extremely performant. Being a immediate mode renderer the GUI code ends up being just a big long list of things to draw. I find that this makes the code a lot easier to follow and there is not much in the way of framework or abstractions to get in the way. That said, I'm not sure I would want to use it for any application more complex than this, but in this case I was happy with the choices made.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Screenshot_20240113_153559.d61742b034b99c15.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>The current state of the GUI application is shown here; this model has a variety of nodes created and connected together. There is an inspector panel on the right for adjusting the static node parameters, and most numerical node parameters can receive inputs from other nodes.\nNodes can be fully created, configured and connected in the GUI, and the system saved to a <code>.cno<\/code> file. The colour of the connections between nodes changes according to the current value of the signal on that connection.<\/p>\n<h2 id=\"file-format\">File format<\/h2>\n<p><code>CONNOISE<\/code> implements its own file format for describing a synthesis system, a basic example shown here:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>Module Sine {\n<\/span><span>  id 1\n<\/span><span>  params {\n<\/span><span>    frequency 220.0\n<\/span><span>  }\n<\/span><span>}\n<\/span><span>\n<\/span><span>Module Square {\n<\/span><span>  id 2\n<\/span><span>  params {\n<\/span><span>    frequency 7.65\n<\/span><span>  }\n<\/span><span>}\n<\/span><span>\n<\/span><span>Module Multiply {\n<\/span><span>  id 3\n<\/span><span>}\n<\/span><span>\n<\/span><span>Connection {\n<\/span><span>  2 v\n<\/span><span>  3 a\n<\/span><span>}\n<\/span><span>\n<\/span><span>Connection {\n<\/span><span>  1 v\n<\/span><span>  3 b\n<\/span><span>}\n<\/span><span>\n<\/span><span>Output AudioDevice {\n<\/span><span>  id 4\n<\/span><span>}\n<\/span><span>\n<\/span><span>Connection {\n<\/span><span>  3 v\n<\/span><span>  4 L\n<\/span><span>}\n<\/span><\/code><\/pre>\n<p><code>Module<\/code>s, <code>Input<\/code>s, <code>Output<\/code>s, are all declared separately. Each of these has an <code>id<\/code> and a number of named ports. The <code>Connection<\/code>s declare which ports are connected together by referencing an <code>id<\/code> and a port name.<\/p>\n<p>The full file format specification is <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/connoise.lon.dev\/index.html#autotoc_md6\">here<\/a>.<\/p>\n<h2 id=\"links\">Links<\/h2>\n<ul>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/connoise\">Source code<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/connoise.lon.dev\/\">API documentation<\/a><\/li>\n<\/ul>\n"},{"title":"Watkins Copicat Mod","published":"2024-01-11T00:00:00+00:00","updated":"2024-01-11T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/copicat-mod\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/copicat-mod\/","content":"\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;cc-1.51a295b254dd6cb0.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I've had this Super IC Copicat for a long time, since way back in the '90s. It makes quite a pleasant lo-fi echo sound and is fun to play with. Unfortunately, after all of these years the heads have become worn out and needed to be replaced.<\/p>\n<p>In the process of replacing the heads, a thought occurred to me -<\/p>\n<blockquote>\n<p>why should the heads be fixed in place?<\/p>\n<\/blockquote>\n<p>The result here, is some 3d-printed parts; a curved rail and 5 head carriers which can slide on the rail. The first carrier here holds the record head and is fixed in place. The other 4 can move back and forth along the rail, which changes the time delay between the recorded sound and it reaching each head. I've also in this project modified one of the rear jack sockets to intercept the feedback control, so that I can plug in an expression pedal and control it in real time when performing.<\/p>\n<p>As far as I know, this is a unique modification, and allows to set up the delays in more interesting ways.<\/p>\n<p>If you want to attempt this mod yourself you can download the files:<\/p>\n<ul>\n<li>Source Fusion 360 design file : <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/copicat-mod\/assets\/downloads\/WEM_Head_Carrier.f3d\">Download<\/a><\/li>\n<li>3mf record and play head carrier : <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/copicat-mod\/assets\/downloads\/v22_Head_Carrier.3mf\">Download<\/a><\/li>\n<li>3mf slide track : <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/copicat-mod\/assets\/downloads\/v27_Slide_Rail.3mf\">Download<\/a><\/li>\n<li>3mf erase head carrier : <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/copicat-mod\/assets\/downloads\/v22_Erase_Carrier.3mf\">Download<\/a><\/li>\n<\/ul>\n<p>The erase head carrier is for adding an erase head to Copicats which never originally had one. Mine never did, it had a combined record\/erase head, which are now unobtanium. In replacing the heads I've had to fit an additional erase head as the replacement for recording is record only. The erase head carrier does not go on the slide rail, mount it directly to the top panel in a suitable place elsewhere.<\/p>\n<p>These files are made available under the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/creativecommons.org\/licenses\/by-nc-sa\/4.0\/\">CC-BY-NC-SA 4.0 license<\/a><\/p>\n"},{"title":"Site cleanup","published":"2024-01-10T00:00:00+00:00","updated":"2024-01-10T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2024\/site-cleanup\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2024\/site-cleanup\/","content":"<h2 id=\"happy-new-year\">Happy New Year<\/h2>\n<p>It's been 10 years since I wrote the first content for the first version of this site. Since then, the site has moved domain, and moved to a different framework and I've learned quite a bit about the art of my hobbies and how to write about them. And I'm still learning.<\/p>\n<p>Looking back though, it's clear I was initially heavily focussed on building my synth project and somewhat out of my depth. I had a good idea of what I wanted to build, but not much of an idea of how to realise it. I went around in circles a few times on both the electronics and digital and code design of the various parts, and there were some posts here which did a poor job of explaining what I was trying to do.<\/p>\n<p>To that end, I have removed the scruffy incomplete and sometimes incomprehensible posts about the early stages of that project, and kept only the posts which are stand alone and explain other projects I have worked on.<\/p>\n<p>I did end up completing the analogue part of the synth in 2018 and the digital part finally at the end of 2023. And, I still haven't written much about it. I think it deserves a write-up and it's on my TODO list to at least make a basic post about it and show some photos and samples. Maybe this year. Maybe in another 10 years.<\/p>\n<p>I've also committed an Internet Sin and changed the URL of some of the remaining posts. They had strange category names in the URLs, and I prefer the date URLs. And now all the post URLs are uniform. Apologies if you had to navigate around to find something you were linked to, but it's my site and I'll organise it as I want.<\/p>\n<p>Finally, coming up this year is probably another synthesizer build in the chip-tune genre. There's a fair amount of design work done already, and the project is probably a bit too big to document properly in one post, so I will try to break that down into a series so that it's more manageable to write.<\/p>\n<p>I'm also considering changing this site theme ... there are some other projects I've started along the way which have (or will have) their own web presence and I'm looking for a way to link them all up.<\/p>\n<p><em>EDIT: I've now also changed the site generator, theme and canonical URLs for all posts. As much as possible I've created URL aliases for all existing posts.<\/em><\/p>\n"},{"title":"My Astrophotography Camera Mount","published":"2023-09-19T00:00:00+00:00","updated":"2023-09-19T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2023\/astro-camera-mount\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2023\/astro-camera-mount\/","content":"<p><em>Originally authored 19 September 2023; subsequently updated:<\/em><\/p>\n<ul>\n<li><em>6 October 2023: details of adding motor with gearbox<\/em><\/li>\n<li><em>7 October 2023: minor edits for consistency<\/em><\/li>\n<\/ul>\n<h2 id=\"intro\">Intro<\/h2>\n<p>In <a href=\"https:\/\/doug.lon.dev\/blog\/2023\/astrophotography\/\">my previous blog post<\/a> I've talked about my journey into doing astrophotography and how a suitable camera mount is necessary in order to get any sort of reasonable results.<\/p>\n<p>This blog post goes into a fair amount of technical detail of how I went about designing and building my own motorised camera mount for astrophotography.<\/p>\n<h2 id=\"what-is-this-thing\">What is this thing?<\/h2>\n<p>I got half way through writing this blog post without actually describing in detail what this mount thing is. So, I've come back up to the top to clarify, the details of how it came to be can follow.<\/p>\n<p>To take pictures of faint objects in the sky, it is necessary to take long exposures. But, because the Earth is rotating, then the camera also needs to rotate so that the motion is cancelled out and the stars remain static points on the image. A second axis of rotation is also required in order to be able to point the camera at any point in the sky.<\/p>\n<p>So, I was going to design a device to which I can attach a camera + lens assembly, which can rotate in 2 perpendicular axes. Initially, it made sense to design the first axis as a turntable going \"around\" (i.e. parallel to the table top, like a lazy susan) and the 2nd axis on top of that going \"up and down\" like a swing. So, I am going to make an <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Altazimuth_mount\">Altazimuth mount<\/a>.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Sketch-1.ad1f6a87198372ba.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>The first \"turntable\" or \"pan\" axis is called \"Azimuth\" and the other is called \"Altitude\" or sometimes \"Elevation\". When referring to the mount rotating around these vertical and horizontal axes relative to the Earth's surface I will call them Az\/Alt. Later on, when improving how the mount tracks, the mount will be converted into an <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Equatorial_mount\">Equatorial mount<\/a> and then these axes will be called \"Right Ascension\" and \"Declination\" or RA\/Dec for short. None of the mount's assembly has changed when we do this, only the names of the directions of rotation.<\/p>\n<p>Given that the mount needs to move by itself, it will need at least one motorised axis, so we might as well motorise both of them. Actually, in an Az\/Alt configuration it is necessary to move both axes at the same time to compensate for the Earth's rotation. Whereas in an Equatorial configuration only one axis (RA) needs to be moving. To compute these axes rotations, the mount will be connected to a computer. This allows for both slowly tracking objects in the sky and also to direct the camera to an absolute position, so that we don't have to manually locate the objects we want to look at.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Sketch-2.147ae1ab6dc1b083.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Because I am writing this post retro-actively regarding the design and implementation of the mount, you may notice that the images included here are not strictly chronological with regards to how the development happened, but are used to illustrate the points of the text surrounding them.<\/p>\n<h2 id=\"inspiration\">Inspiration<\/h2>\n<p>At first, I didn't really want to have to design anything at all and there are two routes into this:<\/p>\n<ol>\n<li>\n<p>Just buy something off the shelf, plug in and play<\/p>\n<\/li>\n<li>\n<p>Take a look if anyone else on the internet has designed, built and published details of their mounts or something similar.<\/p>\n<\/li>\n<\/ol>\n<p>The first approach is valid, but at heart I'm an engineer and if you've ever looked at doing anything astronomy related, you'll know that decent kit is really really expensive. By the time you get to reading the end of this post though, you might think I'm mad for not just paying up and having something which performs perfectly for far less investment in time and mental energy. I'll counter that argument by stating that throughout all of this, the journey itself was also a goal I wanted to complete. Plus, (spoiler alert) in the end I actually spent less than an off the shelf mount would have cost and I learned a few things. So, I think I'm still winning.<\/p>\n<p>So, I took the second approach. There are quite a few engineers\/makers out there who have designed and built motorised camera mounts. There are some open source designs even for the exact thing I want to make here<sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup>. In the end, I was most interested in <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/isaac879\/Pan-Tilt-Mount\">the mount system designed by \"isaac879\"<\/a>. This mount was designed specifically for more regular photography and film production, but I felt it most closely matched what I wanted to achieve.<\/p>\n<p>I did look at the CAD and model files provided by Isaac, but realised that he'd designed it around his own camera, and the cameras I have are a different size\/ I also didn't need any of the track sliding mechanism either. So in the end I didn't simply use his models and print them out, I went ahead and started my own design but using much of the same concepts.<\/p>\n<h2 id=\"design-constraints\">Design constraints<\/h2>\n<p>We're not going to get anywhere on this project without knowing what we want to achieve, and exploring any constraints which need to be considered in the design. Here's some which I am retrospectively listing for the purposes of this blog - to be honest, I can't remember if I knew about all of these when I started.<\/p>\n<h3 id=\"physical\">Physical<\/h3>\n<ol>\n<li>\n<p>Camera properties - obviously the mount needs to be able to hold my cameras, and the cameras need to fit within whatever part of the device supports them. The mount also needs to be strong enough to support the weight of the camera + lens in a variety of orientations without significant sagging or stressing any parts. There will always be some degree of flex however, because it will be made primarily of semi-hollow plastic.<\/p>\n<\/li>\n<li>\n<p>3D Printer dimensions - I only have one printer, which can print parts with dimensions no larger than 220 x 220 x 270 mm.<\/p>\n<\/li>\n<li>\n<p>Positional accuracy - the positional changes required to track the stars across the sky are quite small. I knew that standard stepper motors operate at 1.8 degrees per step, which is far too coarse for use in this project, so the motors should not be directly connected to the axes of rotation. Some gearing is required in the design so that each 1.8 degree step of the motor causes a smaller degree of movement of the mount's axes. The largest acceptable increment on the axes will depend on the lens focal length and the exposure time, so for the time being it is not really possible to put a value on the gear ratios, but it is possible to consider that it needs to be as low as practically possible.<\/p>\n<\/li>\n<li>\n<p>Torque - moving a potentially quite heavy camera and lens assembly around is going to require some physical power. The more torque that is required, then the bigger the motors are required to be and the greater the electrical power is needed to supply to them. Direct driving the axes would be a bad idea not only for accuracy, but using gears to reduce the motion actually proportionally increases the torque applied to the driven part. It might still be able to use small motors, if the drive ratio is low enough. Note that one axis also needs to move the weight of <em>both motors<\/em>, so bigger motors also requires bigger motors.<\/p>\n<\/li>\n<li>\n<p>Positional stability - as the mount is required to hold the camera in position, the weight of the camera must not be enough to back-drive the mechanism and overcome the available torque of the motors. Even with low drive ratios, it can still be possible to back-drive the motors in this way. Worm gears are less susceptible to being back-driven beyond a certain lead angle; this might be useful for one or more of the axes. Since there will be gears in the mechanisms, backlash or any \"play\" in the system should also be minimised. This can achieved by ensuring accurate part dimensions, part stiffness and also considering perhaps double-helical or herringbone gears.<\/p>\n<\/li>\n<li>\n<p>Optical alignment - this one is actually not so important for looking at the sky, since any parallax effects do not need to be considered; however if the mount is to be useful perhaps also for normal photography or film making, weird parallax effects can be reduced during rotation by making sure that both axes of rotation of the camera are around a point known as the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Entrance_pupil\">entrance pupil<\/a>. I've seen these effects before<sup class=\"footnote-reference\"><a href=\"#2\">2<\/a><\/sup>.<\/p>\n<\/li>\n<\/ol>\n<p>From a practical point of view, I also wanted the mount to not require too many components which I would have to buy pre-made or would require manufacturing using methods not currently at my disposal; so, no metalwork, woodwork etc.<\/p>\n<h3 id=\"software\">Software<\/h3>\n<p>I was really keen that this project not be a multi-month long software authoring effort. I leave that for the day job - and previously when hobby projects start to get heavy on the software side I tend to get quite tired of the process and that greatly reduces the chances that the project will get finished.<\/p>\n<p>That said, I knew there would need to be some and that would involve also learning about how to drive stepper motors and also how <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Astronomical_coordinate_systems\">astronomical coordinate systems<\/a> work.<\/p>\n<p>For the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/GoTo_(telescopes)\">\"Go-to\"<\/a> part of the design, where the mount can be told exactly where to point, I wanted to integrate the motor controller with astronomy software. This would allow the astrophotography shooting to be somewhat automated.<\/p>\n<h2 id=\"finding-good-cad-tools\">Finding good CAD tools<\/h2>\n<p>I'm a big fan of using open source software where I can. However, before I started this project I wasn't up to speed on doing 3D engineering CAD at all, let alone with any open source software. I went around the loop trying out what I could find available;<\/p>\n<ol>\n<li>\n<p><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.blender.org\/\">Blender<\/a> - great for artistic modelling, but good luck doing CAD or engineering design with it<sup class=\"footnote-reference\"><a href=\"#3\">3<\/a><\/sup>.<\/p>\n<\/li>\n<li>\n<p><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.freecad.org\/\">FreeCAD<\/a> - I tried and tried and tried to get into this. It promises so much, but there are two fatal flaws in my opinion. First, the UI is a nightmare. I have no idea which \"workbench\" I am supposed to be using, and there are so many similar choices available, each with subtly different tools and ... I gave up trying to make sense of it. Secondly, even when following tutorials to get some basic modelling done, I found lack of sensible keyboard control and having to click almost everything a real distraction. Not a good experience.<\/p>\n<\/li>\n<li>\n<p><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/openscad.org\/\">OpenSCAD<\/a> - Is an extremely appealing concept; to be able to model by writing code, as if I'm writing software ... but in practice it's quite cumbersome and as stated in the design goals of this project, I wanted to focus on making rather than programming. I have <a href=\"https:\/\/doug.lon.dev\/blog\/2020\/guitarix-pedal\/\">used this before though<\/a> for some simple box modelling, and even then the code for the model was quite hard to manage.<\/p>\n<\/li>\n<\/ol>\n<p>There are others out there, but by this point I had got fed up with yak shaving and just wanted to get on with the project. So, after all of that, I recalled that I had used <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.autodesk.co.uk\/products\/fusion-360\/overview\">Fusion 360<\/a> for my <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/analogue-synth\/\">modular synth build<\/a> and it was free to use for personal projects at the time. I thought I had heard at some point they had discontinued the free for personal use tier, but I seem to have been wrong about that. I found my old account, reset password and got back into it. I found my old synth design files in my account still; nice.<\/p>\n<p>I find Fusion 360 very pleasant to use, so I spent a week or so of spare time getting to grips with it and set about drafting up parts for the design.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Shot-0.58c24d762f4afe04.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"physical-design-details\">Physical design details<\/h2>\n<p>Following suit from Isaac's design, I established that I should be able to assemble the mount using entirely M3 nuts and bolts. After a few tests, I established the correct bolt and nut sizes to use for my 3D printer and then it is simply the case of making connecting holes in adjacent parts for assembly. I wanted all bolt heads and nuts recessed into parts so that all outer surfaces are flush.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Shot-1.9e2b4b49d9018035.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>For the Alt\/Dec axis I chose a worm drive design. I thought that this axis would be most susceptible to being back-driven due to the weight of the camera and lens. As it turns out, both axes can be susceptible to this, depending on the position. However, the worm drive on this axis does provide a decent reduction ratio for positional accuracy. The design and print tolerance for this gear set though is not so great in the current version, the gears do not mesh completely and there is some backlash on this axis. It is mostly not a problem in practice as the weight of the camera keeps the axis biased to one side, but as it tips over the centre of mass of the camera it introduces positional error. I may come back to re-print one of the gears in this set to improve the meshing, but there also is not a lot of available clearance to make either one much larger.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Shot-2.523f576f41d80b5d.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I settled on using herringbone gears for the Az\/RA axis. This required a plug-in for Fusion 360 in order to generate the models. I used a modulo of 1 mm for all gears in the design. This proved correct the theorem that herringbone gears reduce backlash, or maybe the design and print tolerances played in my favour here as the Az\/RA axis is pretty snug and works well.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Shot-3.279fc4145dedbcb2.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>It is worth looking at the actual design for the Az\/RA axis and how this part of the mount moves and is held together. I wanted to implement something approximating a large <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Thrust_bearing\">thrust bearing<\/a> or <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Slewing_bearing\">slew bearing<\/a>, and not have to buy one as at this size that can get expensive and I honestly don't need the precision of machined metal parts. This type of bearing is designed to support axial load, which is what the design calls for in the \"turntable\" part. I drew up 3 parts which implement a slewing bearing. The first is the outer base part containing a <code>&lt;<\/code> shaped slot around the inner circumference. The second two parts combine to form the other matching <code>&gt;<\/code> slot, and in the slot I fill in with steel ball bearings. These parts are split in order to allow assembly of the bearing, and form the \"plate\" of the \"turntable\".<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Shot-4.2045b10e5f6d8c35.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Shot-5.15ca802ba7889d00.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>The end result turns well enough, but obviously not as smoothly as a quality metal bearing. There are some inconsistencies in the bearing surfaces which causes some points in the rotation to stick slightly. Some work with a file or sandpaper may improve this, as well as slightly loosening the bolts holding the two inner parts together.<\/p>\n<p>The base itself is printed in two parts to allow for easier assembly of the big bearing and reduce risk in 3D printing such a large part in case of warping or other failure - not to mention reduce the individual part print time, the whole base would otherwise have taken in excess of 15 hours to print in one part.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Shot-6.514a82932e2447af.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"learning-and-using-3d-printing\">Learning and using 3D printing<\/h2>\n<p>I got my 3D printer back in May 2023, with the intention of making practical useful things with it. I got a Creality Ender 3 S1 Pro. It's a nice machine, mid-range hobbyist type. The first thing I did with it was <a href=\"https:\/\/doug.lon.dev\/blog\/2024\/copicat-mod\/\">make some modifications to my Watkins Copicat<\/a> but this project would be the first \"big one\".<\/p>\n<p>There are some additional steps needed in order to get the design from CAD and into the real world. It is not possible to directly send the CAD model to the printer, it has to be processed it into something the machine understands, and this process is called \"slicing\". There are quite a few different <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Slicer_(3D_printing)\">slicing software<\/a> packages available (and some of them share a common history). I tried out just about all that I could find and settled on using <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.prusa3d.com\/page\/prusaslicer_424\/\">PrusaSlicer<\/a>. Even though this is provided by Prusa primarily for use with their printers, it is open source and has a long lineage in open source 3D printing. There are configurations available out of the box for Creality printers, and I find the software itself nice to use - it exposes settings when you need them, but otherwise keeps out of the way and has sensible defaults. There's a few concepts to get to grips with being successful with slicing, but that's not what this post is really about; just know that I spent also a good few weeks of spare time researching and testing out what the different settings and features are and how they affect the final print.<\/p>\n<p>Throughout this project, I had to ensure that details of the design would work when 3D printed. I was also initially unsure about tolerances required for parts which need to fit inside each other, or mesh against each other. I was also mindful that I didn't want to waste material or printing time (did you know that <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Fused_filament_fabrication\">FFF 3D printing<\/a> is a somewhat painfully slow process?) by simply committing to print all the parts as-designed and hoping that it would turn out OK. So, for certain aspects of the design such as the bolt holes, captive nut recesses and bearings, I sliced out small sections of parts and printed just those as tests, so that I could establish how good the tolerances are. With some parameter tweaking later, I was confident to go ahead and print the full parts knowing that they would assemble correctly.<\/p>\n<h2 id=\"learning-and-using-stepper-motors\">Learning and using stepper motors<\/h2>\n<p>Considering the requirements for accurate and absolute position of the axes, stepper motors seem to be a good place to start for this project. Stepper motors have a known and accurate amount of rotation per step, and a microcontroller can be used to count out and keep track of the steps provided to the motor. In this way, the mount can always know how many degrees of rotation has been applied to each axis - in other words, no other sensors are required to know where the camera will be pointing.<\/p>\n<p>There is a down-side however - the motors can <em>only<\/em> move in discrete steps and not in a continuous motion. This presents a bit of an issue for tracking stars with long exposures, the point light of the star will appear to jump in steps across the image instead of being perfectly aligned on one spot. This can be mitigated using a low enough gear drive ratio that each step is within a tolerable limit and also combining with <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.linearmotiontips.com\/microstepping-basics\/\">micro-stepping<\/a>, where the motor can be somewhat held in positions between steps. Using micro-stepping however can reduce the torque provided by the motor, so there are some trade-offs to be made here.<\/p>\n<p>The Az\/RA axis needs a very fine resolution of motion in order to ensure that the star points are not spread across the image due to the discrete motion of the stepper motor. To track the Earth's rotation, this axis needs to move at a rate of 0.25 degrees per minute. It is therefore necessary to have quite a large gear reduction ratio on this axis, such that one single step of the motor produces a movement so small that it is not detectable on the image. Therefore, as the motor is periodically stepped, at a high enough reduction ratio the axis is effectively moving continuously. This is technically not true, but in practice is good enough over the duration of a typical exposure. The initial reduction ratio was 63:144 (0.4375) but this was not good enough. I iterated again on the motor for this axis and put in a motor with a 139:1 reduction gearbox integrated; this gives a final drive ratio of 1:139 x 63:144 (0.003147). I also changed out the motor driver chips for slightly better ones which provide 64x micro-stepping. This gives a final resolution of nearly 11300 micro-steps per degree of rotation on the Az\/RA axis.<\/p>\n<p>I also didn't want to have to go low-level on the microcontroller software and implement the motor control myself. Taking inspiration from my 3D printer, I realised that the same kind of controller board as is in my printer could be used for the task of controlling this mount. There are already well-established products and control protocols for machines with stepper motors, such as 3D printers or CNC machines.<\/p>\n<p>A good place to start is to get a pre-built CNC controller circuit board and make a mental shift to interpret commands such as \"move the Y-axis 30 mm at 2 mm\/s\" to really mean \"rotate the RA-axis 30 degrees at 2 degrees\/s\". It's a simple case of scaling the \"dimensions\" to suit the gear ratios and using different units.<\/p>\n<h3 id=\"using-grbl-arduino-and-cnc-shields\">Using GRBL, Arduino and CNC shields<\/h3>\n<p>The programming language used for CNC machine control is called G-code. 3D printers use some of the same commands, and extend this language for printer-specific features (the job of the slicing software mentioned above is actually to translate a CAD model into G-code). One flavour of CNC G-code language is called <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/gnea\/grbl\">grbl<\/a> and this implementation runs on Arduino devices.<\/p>\n<p>There are also quite a lot of Arduino and grbl compatible motor controller circuit boards available. Some are quite bare-bones, that you have to separately buy an Arduino and the motor driver circuits to plug into it. I tried a couple of different ones, but settled on something very cheap in the end - the advantage of using a plug-in Arduino is that I could be able to modify the grbl software running on it, should I need to. Luckily, so far I have not needed to.<\/p>\n<p>Also, if I goofed up and fried some of the electronics, then each part is separately replaceable and at low cost.<\/p>\n<p>The final electronic module involved is a Bluetooth serial module, which connects to the controller board via power and UART serial pins. Once configured to match the UART speed of the Arduino it can be connected to from a computer at any serial baud rate and it forwards the serial data in both directions.<\/p>\n<pre class=\"mermaid\">\n  graph LR\nsubgraph C[Controller Board]\nA --&gt; M1[Motor Driver&lt;br&gt;TMC2209] &amp; M2[Motor Driver&lt;br&gt;TMC2209]\nend\nBT[Bluetooth&lt;br&gt;Module] &lt;--&gt; A[Arduino&lt;br&gt;Nano]\nM1 --&gt; S1[Stepper Motor]\nM2 --&gt; S2[Stepper Motor]\n<\/pre>\n<h3 id=\"grbl-configuration\">GRBL configuration<\/h3>\n<p>Grbl requires a little bit of configuration, with regards to knowing how many steps to apply to a motor in order to reach a certain distance travelled. By starting with the degrees per step and the gear ratios of the axes, I could derive the scaling factors necessary such that the grbl dimensions match the rotation of the axes in degrees.<\/p>\n<table><thead><tr><th><strong>Tilt\/Alt\/Dec<\/strong><\/th><th><\/th><th><\/th><th><\/th><th><\/th><\/tr><\/thead><tbody>\n<tr><td><\/td><td>5<\/td><td>threads<\/td><td>worm<\/td><td><\/td><\/tr>\n<tr><td>\u00f7<\/td><td>90<\/td><td>teeth<\/td><td>driven gear<\/td><td><\/td><\/tr>\n<tr><td>=<\/td><td>0.056<\/td><td>ratio<\/td><td>worm : driven<\/td><td>(1\/ratio = 18.000)<\/td><\/tr>\n<tr><td>x<\/td><td>1.80<\/td><td>deg\/step<\/td><td>stepper input<\/td><td><\/td><\/tr>\n<tr><td>=<\/td><td><em>0.100<\/em><\/td><td><em>deg\/step<\/em><\/td><td><em>driven gear<\/em><\/td><td><\/td><\/tr>\n<tr><td>1\/ =<\/td><td>10.00<\/td><td>step\/deg<\/td><td><\/td><td><\/td><\/tr>\n<tr><td>x<\/td><td>64.00<\/td><td>micro-step<\/td><td><\/td><td><\/td><\/tr>\n<tr><td>=<\/td><td><strong>640.000<\/strong><\/td><td><strong>step\/deg<\/strong><\/td><td><\/td><td><\/td><\/tr>\n<\/tbody><\/table>\n<table><thead><tr><th><strong>Pan\/Az\/RA<\/strong><\/th><th><\/th><th><\/th><th><\/th><th><\/th><\/tr><\/thead><tbody>\n<tr><td><\/td><td>63<\/td><td>teeth<\/td><td>drive<\/td><td><\/td><\/tr>\n<tr><td>\u00f7<\/td><td>144<\/td><td>teeth<\/td><td>driven<\/td><td><\/td><\/tr>\n<tr><td>=<\/td><td>0.438<\/td><td>ratio<\/td><td>drive : driven<\/td><td>(1\/ratio = 2.286)<\/td><\/tr>\n<tr><td>x<\/td><td>1.80<\/td><td>deg\/step<\/td><td>stepper input<\/td><td><\/td><\/tr>\n<tr><td>=<\/td><td><em>0.788<\/em><\/td><td><em>deg\/step<\/em><\/td><td><em>drive ratio<\/em><\/td><td><\/td><\/tr>\n<tr><td>1\/ =<\/td><td>1.27<\/td><td>step\/deg<\/td><td><\/td><td><\/td><\/tr>\n<tr><td>x<\/td><td>64.00<\/td><td>micro-step<\/td><td><\/td><td><\/td><\/tr>\n<tr><td>=<\/td><td><em>81.2698<\/em><\/td><td><em>step\/deg<\/em><\/td><td><\/td><td>without gearbox<\/td><\/tr>\n<tr><td><\/td><td>1<\/td><td>gearbox<\/td><td>drive<\/td><td><\/td><\/tr>\n<tr><td>\u00f7<\/td><td>139<\/td><td>gearbox<\/td><td>driven<\/td><td><\/td><\/tr>\n<tr><td>=<\/td><td>0.0072<\/td><td>gearbox<\/td><td>ratio<\/td><td><\/td><\/tr>\n<tr><td>x<\/td><td><em>81.2698<\/em><\/td><td><em>step\/deg<\/em><\/td><td><\/td><td>without gearbox<\/td><\/tr>\n<tr><td>=<\/td><td><em>11,296.5079<\/em><\/td><td><em>steps\/degree<\/em><\/td><td><\/td><td>with gearbox<\/td><\/tr>\n<\/tbody><\/table>\n<p>These final scaling values need to be configured on the grbl Arduino as the \"steps\/mm\" parameters <code>$100<\/code> and <code>$101<\/code> respectively. The \"Max rate mm\/s\" also needs to be set in <code>$110<\/code> and <code>$111<\/code> registers so that the mount moves at a sensible rate when moving between targets. If this rate is too fast, the motors can skip steps either because the mass of the motor stator alone cannot move that fast, or because the mass of the mount and camera and lens cannot be accelerated fast enough. If the motors skip steps, then the mount loses positional accuracy and is no longer pointing in the direction the controller thinks it is. If the motors move too slowly, then the mount would take too long to reach the next target. It is also possible to change the acceleration of the motors in mm\/s<sup>2<\/sup> in registers <code>$120<\/code> and <code>$121<\/code> so that the motors do not try to exceed the available drive power.<\/p>\n<h2 id=\"ascom-and-custom-driver\">ASCOM and custom driver<\/h2>\n<p>The next step is to interface the motor controller with the controlling computer. How will the computer tell the camera where to point?<\/p>\n<p>In the first instance, I looked at manually extracting viewing coordinates from Stellarium, as I initially thought that would make the overall system simpler. However, Stellarium does not directly provide the coordinates I required at the time (Az\/Alt) over its remote control API. I persevered with this for a while, but it became clear that I would need to implement complicated coordinate conversions myself and that I would also not really be integrating properly with any astronomy software.<\/p>\n<p>During this initial investigation into integration I became aware of <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/ascom-standards.org\/\">ASCOM<\/a>, which is a standard set of interfaces for astronomy equipment. I also actively avoided going all-in on starting an implementation of an ASCOM Telescope driver (\"telescope\" is synonymous with \"mount\" in this context) as I thought that would be another software authoring black hole I'd disappear into.<\/p>\n<p>It quickly transpired that I would not be able to avoid writing this ASCOM Telescope driver; and that the benefits would be quite large. My custom driver would mean that my mount will then work with any astronomy software which also interfaces with ASCOM standards, and I would not be limited to just using only e.g. Stellarium and the equivalent of a string-and-sticky-tape solution.<\/p>\n<p>Writing the driver turned out to be fairly straightforward after studying the templates, documentation and other open source examples. I've not programmed in C# either before, so some learning on that front was also required. Within a week or so I had something which worked, and the ASCOM driver infrastructure took care of all the coordinate conversions using simple API and it is successful in hiding away a lot of complexity.<\/p>\n<p>My driver implements an ASCOM Telescope driver which outputs G-code over a serial connection. The driver is open-source and can be viewed\/cloned\/forked from here:<\/p>\n<p><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/ASCOM.PTMount\">https:\/\/bitbucket.org\/doughammond\/ASCOM.PTMount<\/a><\/p>\n<h3 id=\"further-driver-development\">Further driver development<\/h3>\n<p>As I understand it, there is another driver standard called <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.indilib.org\/\">INDI<\/a> which is supported on platforms other than Windows. I may in the future also implement an INDI driver for this mount, but it is not a priority at the moment.<\/p>\n<h2 id=\"testing-and-issues\">Testing and Issues<\/h2>\n<p>I started using the mount in Az\/Alt mode and became quite pleased with a working Go-To and tracking system. The mount keeps the target close enough to the centre of the image such that stacking software is able to combine multiple exposures successfully.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Photo-1.ad47900a4122e0f8.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>At this point in time, the controller board attachment was still temporary and I was using the first set of motors, the small 26 mm ones. These motors would later struggle with larger lenses and in the Equatorial configuration and I have upgraded them currently to 60 mm motors which are able to provide a much higher torque.<\/p>\n<p>The images I have taken in this configuration did work, but it became apparent that the issue of field rotation in Az\/Alt tracking would have an impact on my final images, by reducing the quality of the image away from the centre. The effect of this is that the stars in the image will appear to rotate about the centre over time, and this is a consequence of the Earth's axial tilt.<\/p>\n<p>A minor point related to this also is that these rotation-corrected images have no standard orientation, or rather the final stacked image would have to be manually rotated to match what other astronomers would expect to see - compared to shooting with an Equatorial mount where the \"up\" direction is always the same and does not need any correction.<\/p>\n<p>I briefly considered how to add a third axis to the mount, which would rotate the camera to compensate, but it turns out there is a much simpler solution.<\/p>\n<p>Still, during this time I got to learn a bit about astronomy software and I have settled on using <a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/stellarium.org\/\">Stellarium<\/a> and <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/nighttime-imaging.eu\/\">N.I.N.A.<\/a> together.<\/p>\n<h2 id=\"conversion-from-az-alt-to-equatorial\">Conversion from Az\/Alt to Equatorial<\/h2>\n<p>To deal with the issue of the field rotation and also to simplify the tracking to require only one moving axis, it would be best to use an Equatorial mount which aligns to the Earth's rotation. It is possible to make a simple change to the mount to convert it from Az\/Alt configuration to Equatorial. This is one of those techniques that if you don't know possible, then you'd not easily be able to discover. I was given a hint to look into the \"wedge\" solution.<\/p>\n<p>What this entails is to simply place the mount on an inclined wedge platform. The angle of the wedge is related to the observation location's latitude, <code>90 - latitude<\/code>. In my case, observing near London, I need a wedge platform angled at about 38.5\u00b0. I've 3D printed some supports which assemble to this angle and I'm able to attach the mount using the existing base assembly bolt holes.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Photo-2.3b65225d6e0afb34.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I didn't anticipate the fact that at some positions, the camera and lens centre of mass is over or beyond the lower edge of the mount and it is prone to tipping over. The temporary solution to this is to fix the supports to some Lego which prevents the whole thing tipping. There's a couple of things I could do as a more permanent solution, perhaps:<\/p>\n<ol>\n<li>Convert the \"feet\" into fixings with holes so that I can screw\/bolt the supports to something fixed; either a plank of wood, or some aluminium extrusion or a table\/portable workbench.<\/li>\n<li>Design and 3D print a better support frame which extends out further to prevent the tipping.<\/li>\n<\/ol>\n<p>Update: I have indeed now mounted the wedge feet onto aluminium extrusion, this provides a good stable base for the system.<\/p>\n<h2 id=\"full-sky-visibility\">Full sky visibility<\/h2>\n<p>With the mount in Equatorial mode and the camera cradle attached in the original \"underslung\" configuration, and using the original ~100\u00b0 driven gear for the Dec axis I was not able to position the camera to any part of the sky less than 0\u00b0 declination. This was a symptom of assuming that I'd be using the mount in Az\/Alt originally, where the Altitude axis is only needed to go from 0 to 90 degrees. This is a problem because that rules out actually being able to view a large portion of the sky to the south at this latitude.<\/p>\n<p>Even if I were to print another gear which works in the range of e.g. -50 to +90 degrees almost any negative rotation here would cause the lens to collide with the top of the mount. The cradle would need to be higher in order to provide the necessary clearance.<\/p>\n<p>In the first instance, with the new gear printed, I simply also inverted the camera cradle to an \"over-slung\" configuration. This was OK to prove a point that it allows the mount to see any part of the sky, but this is not a stable configuration with the original cradle dimensions. More about this below.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Shot-7.4402d175283e86d4.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"torque\">Torque<\/h2>\n<p>You might have gathered from reading all of the above that I underestimated the power required to move even a moderately sized camera and lens assembly. I initially bought NEMA 17 26 mm motors thinking that the small and lightweight size would be beneficial. It turns out they are more than a bit on the wimpy side. Especially in Equatorial mode, the angles at which the camera is held seem to put more of a strain on the axes and the only solution is more power.<\/p>\n<h3 id=\"motor-iterations-current-heat\">Motor iterations &amp; current &amp; heat<\/h3>\n<p>As of the time of writing I have replaced the Alt\/Dec 26 mm motor with a 60 mm motor and the Az\/RA motor with a 40 mm motor + 139:1 reduction gearbox. The alt\/Dec axis has enough torque now to keep the axis in place, and there's enough reduction on the Az\/RA axis that it is not possible to back-drive it at all. This seems to provide enough power now to move all the camera and lens combinations I have tried so far. The largest being a Nikon 80-200 mm f\/2.8 zoom lens which is really pretty bulky.<\/p>\n<p>There is a downside to this though. As the motors output more power, then the motor drivers and power supply must also pass more power into the motors. This caused the drivers to get quite hot, but I believe they are still operating within the designed limits. I further adjusted the driver configurations to reduce the current supplied to the motors, enough that I get the torque that I need, but not so much that everything gets hot.<\/p>\n<h3 id=\"cradle-height\">Cradle height<\/h3>\n<p>One factor affecting the torque requirements is how far the camera and lens centre of mass is away from the axes of rotation. In the first adaptation of the mount from Az\/Alt to Equatorial I simply inverted the camera cradle component, which elevates the camera some distance away from the axes.<\/p>\n<p>Since torque is defined as force times distance, then any excess distance results in increased torque requirements. I went back to the CAD and made a second iteration of the camera cradle which reduces the off-axis distance and therefore reduces the torque required to keep the camera and lens in position. I made this cradle just high enough to be able to level off at the horizon when facing south at my latitude when mounted in the \"over-slung\" configuration.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Shot-8.362ba321b8f36d4f.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"make-one-yourself\">Make one yourself?<\/h2>\n<p>I have published the 3D models for all the parts of this build here in the same repository as the ASCOM driver code:<\/p>\n<p><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/ascom.ptmount\/src\/master\/3D%20Print%20Parts\/\">https:\/\/bitbucket.org\/doughammond\/ascom.ptmount\/src\/master\/3D%20Print%20Parts\/<\/a><\/p>\n<p>I would advise however to measure the parts and make sure it's suitable for your camera and 3D printer before starting. You may well find that you will need to make adjustments for your own requirements.<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>This project is still very much a work in progress if I'm honest, since I'm relatively new to astronomy and I haven't mastered at all the image exposure and stacking process. I've done a few tests but I wasn't happy with the results, and a lot of the exposures were not usable.<\/p>\n<p>This means that at present, there is no satisfying pay off of being able to show lots of pretty pictures of space objects. But I do have just one faint image of the North America Nebula:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;north_america_nebula_2023-08-16_small.7e826c2abb81f229.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I am keen to keep on with this though, I hope that in the near future the focus will change from fixing and improving the mount to spending a lot more time actually using it for astrophotography. I plan to follow up with further posts on this subject if I can get to a point where I can produce good results.<\/p>\n<h2 id=\"bill-of-materials\">Bill of Materials<\/h2>\n<p>Here's a complete list of materials used in both the design and implementation of this project. Not all of these are needed in the final build, but even so including the parts I did not finally use the total cost was way less than an off the shelf product.<\/p>\n<p>In this table, \"Total Qty\" is the total quantity of items I actually ordered, and \"Parts Cost\" is the sub-total for that quantity. \"Build Qty\" is the quantity actually used in the current design, and \"Build Cost\" is the sub-total for the quantity used. So, you can see I over-ordered most components (some only come in large multiples), and also iterated on some parts to find ones with better performance (e.g. stepper motors). I originally thought I would implement hall sensors and magnets for automatic homing, but this has not made it into the design at this point.<\/p>\n<p>The counts of nuts and bolts used here is a bit approximate since I don't want to disassemble the mount to make exact counts. Some bolts were also trimmed down to be flush from their original length. Given the bulk nature of the quantities of these, it doesn't affect the build cost if I say I used 10 or 40. Its worth having a range of sizes on hand if you're a hobbyist engineer in any case and you can select the correct bolt lengths during assembly.<\/p>\n<p>The quantity of printer filament used also reflects usage for test parts and re-designed parts that were discarded and are not part of the working mount. This may have been a little less than stated, I haven't weighed what is left, but it looks like much less than half of the spool.<\/p>\n<table><thead><tr><th>Item<\/th><th>Qty\/Pk<\/th><th>Pks<\/th><th>Total Qty<\/th><th>Price\/Pk<\/th><th>Parts Cost<\/th><th>Build Qty<\/th><th>Build Cost<\/th><\/tr><\/thead><tbody>\n<tr><td>F6233ZZ flange bearing<\/td><td>10<\/td><td>1<\/td><td>10<\/td><td>\u00a32.87<\/td><td>\u00a32.87<\/td><td>4<\/td><td>\u00a31.15<\/td><\/tr>\n<tr><td>Arduino Nano<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>\u00a32.82<\/td><td>\u00a32.82<\/td><td>1<\/td><td>\u00a32.82<\/td><\/tr>\n<tr><td>HC-06 BT module<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>\u00a32.49<\/td><td>\u00a32.49<\/td><td>1<\/td><td>\u00a32.49<\/td><\/tr>\n<tr><td>CNC Shield<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>\u00a32.30<\/td><td>\u00a32.30<\/td><td>1<\/td><td>\u00a32.30<\/td><\/tr>\n<tr><td>NEMA 17 x 60mm Motor<\/td><td>1<\/td><td>2<\/td><td>2<\/td><td>\u00a311.79<\/td><td>\u00a323.58<\/td><td>1<\/td><td>\u00a311.79<\/td><\/tr>\n<tr><td>NEMA 17 x 40mm + Gearbox<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>\u00a37.97<\/td><td>\u00a37.97<\/td><td>1<\/td><td>\u00a37.97<\/td><\/tr>\n<tr><td>TMC2209 stepper drivers<\/td><td>5<\/td><td>1<\/td><td>5<\/td><td>\u00a312.37<\/td><td>\u00a312.37<\/td><td>2<\/td><td>\u00a34.95<\/td><\/tr>\n<tr><td>6.35mm Ball Bearings<\/td><td>1000<\/td><td>1<\/td><td>1000<\/td><td>\u00a39.99<\/td><td>\u00a39.99<\/td><td>150<\/td><td>\u00a31.50<\/td><\/tr>\n<tr><td>M3 Nylock nuts<\/td><td>1000<\/td><td>1<\/td><td>1000<\/td><td>\u00a312.63<\/td><td>\u00a312.63<\/td><td>30<\/td><td>\u00a30.38<\/td><\/tr>\n<tr><td>M3 square nuts<\/td><td>50<\/td><td>1<\/td><td>50<\/td><td>\u00a30.91<\/td><td>\u00a30.91<\/td><td>6<\/td><td>\u00a30.11<\/td><\/tr>\n<tr><td>M3 x 40mm bolts<\/td><td>50<\/td><td>2<\/td><td>100<\/td><td>\u00a32.79<\/td><td>\u00a35.58<\/td><td>17<\/td><td>\u00a30.95<\/td><\/tr>\n<tr><td>M3 x 20mm bolts<\/td><td>50<\/td><td>2<\/td><td>100<\/td><td>\u00a31.76<\/td><td>\u00a33.52<\/td><td>8<\/td><td>\u00a30.28<\/td><\/tr>\n<tr><td>M3 x 16mm bolts<\/td><td>50<\/td><td>2<\/td><td>100<\/td><td>\u00a31.59<\/td><td>\u00a33.18<\/td><td>6<\/td><td>\u00a30.19<\/td><\/tr>\n<tr><td>M3 x 12mm bolts<\/td><td>50<\/td><td>2<\/td><td>100<\/td><td>\u00a31.30<\/td><td>\u00a32.60<\/td><td>1<\/td><td>\u00a30.03<\/td><\/tr>\n<tr><td>M3 x 8mm bolts<\/td><td>50<\/td><td>2<\/td><td>100<\/td><td>\u00a30.96<\/td><td>\u00a31.92<\/td><td>7<\/td><td>\u00a30.13<\/td><\/tr>\n<tr><td>3D Printer filament<\/td><td>1000 (g)<\/td><td>1<\/td><td>1000 (g)<\/td><td>\u00a320.00<\/td><td>\u00a320.00<\/td><td>600 (g)<\/td><td>\u00a312.00<\/td><\/tr>\n<tr><td>M3 x 6mm bolts<\/td><td>50<\/td><td>2<\/td><td>100<\/td><td>\u00a30.90<\/td><td>\u00a31.80<\/td><td>0<\/td><td>\u00a30.00<\/td><\/tr>\n<tr><td>M3 x 4mm bolts<\/td><td>50<\/td><td>2<\/td><td>100<\/td><td>\u00a30.96<\/td><td>\u00a31.92<\/td><td>0<\/td><td>\u00a30.00<\/td><\/tr>\n<tr><td>6mm Ball Bearings<\/td><td>50<\/td><td>1<\/td><td>50<\/td><td>\u00a32.61<\/td><td>\u00a32.61<\/td><td>0<\/td><td>\u00a30.00<\/td><\/tr>\n<tr><td>3x1.5mm magnets<\/td><td>100<\/td><td>1<\/td><td>100<\/td><td>\u00a31.55<\/td><td>\u00a31.55<\/td><td>0<\/td><td>\u00a30.00<\/td><\/tr>\n<tr><td>6x1.5 mm magnets<\/td><td>50<\/td><td>1<\/td><td>50<\/td><td>\u00a32.91<\/td><td>\u00a32.91<\/td><td>0<\/td><td>\u00a30.00<\/td><\/tr>\n<tr><td>NEMA 17 x 26mm Motor<\/td><td>1<\/td><td>3<\/td><td>3<\/td><td>\u00a36.01<\/td><td>\u00a318.03<\/td><td>0<\/td><td>\u00a30.00<\/td><\/tr>\n<tr><td>HC-05 BT module<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>\u00a31.45<\/td><td>\u00a31.45<\/td><td>0<\/td><td>\u00a30.00<\/td><\/tr>\n<tr><td>GRBL Shield + Motor drivers<\/td><td>1<\/td><td>1<\/td><td>1<\/td><td>\u00a325.66<\/td><td>\u00a325.66<\/td><td>0<\/td><td>\u00a30.00<\/td><\/tr>\n<tr><td>A3144E Hall Effect Sensor<\/td><td>10<\/td><td>1<\/td><td>10<\/td><td>\u00a30.57<\/td><td>\u00a30.57<\/td><td>0<\/td><td>\u00a30.00<\/td><\/tr>\n<tr><td><strong>TOTALS<\/strong><\/td><td><\/td><td><\/td><td><\/td><td><\/td><td><strong>\u00a3158.86<\/strong><\/td><td><\/td><td><strong>\u00a349.04<\/strong><\/td><\/tr>\n<\/tbody><\/table>\n<hr \/>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>For example <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/wiki.openastrotech.com\/OpenAstroTracker\">OpenAstroTracker<\/a> - this looks awesome BTW, and I may one day actually make one of these. But, as a project it is quite ambitious, and a bit more than I wanted to take on at this point. However, having completed this project, I will take another serious look at this.<\/p>\n<\/div>\n<p>\n<div class=\"footnote-definition\" id=\"2\"><sup class=\"footnote-definition-label\">2<\/sup>\n<p>I happen to know this from experimenting with shooting and stitching large panoramas on DSLR some years ago, and I have a manually operated Az\/Alt mount for cameras which assists in doing this with the proper entrance pupil alignment - this ensures that when you stitch images together, that all objects in adjacent shots are in the same place and have not moved relative to each other due to the rotation. This type of mount isn't useful for astrophotography though.<\/p>\n<\/div>\n<p>\n<div class=\"footnote-definition\" id=\"3\"><sup class=\"footnote-definition-label\">3<\/sup>\n<p>I only became aware of <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.cadsketcher.com\/\">CAD Sketcher<\/a> after I completed this project. I will try it out, but for now, I have committed to using something else.<\/p>\n<\/div>\n"},{"title":"Astrophotography","published":"2023-09-17T00:00:00+00:00","updated":"2023-09-17T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2023\/astrophotography\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2023\/astrophotography\/","content":"<h2 id=\"intro\">Intro<\/h2>\n<p>I've always been interested in looking at the night sky, and also the technical aspects of photography. I've built a good understanding of what makes digital photography work, and how to choose the correct settings for a given environment and shot. However, up until recently I haven't made much of an effort to learn much about astronomy, what we can see in the sky with our own eyes and what is possible to observe with the help of some optics and modern cameras.<\/p>\n<p>I bought myself a telescope a few years ago, and have enjoyed using it to explore the night sky, and the basic model I have is OK for looking at the larger and brighter objects, such as the moon, Jupiter, and Saturn. It is also astonishing how it enables one to see many more stars in the sky, even if they are only visible as tiny point lights. Somehow, simply applying some optical magnification also brings into focus a lot more than can be seen with the naked eye.<\/p>\n<p>But that really begs the question:<\/p>\n<blockquote>\nWhat else could we see in the sky, if we had some more help from technology?\n<\/blockquote>\n<p>Specifically, modern digital cameras can be set up to be more sensitive than our own eyes, and collect photons of light from objects too faint to be seen otherwise. There are other techniques we can apply to these digital images to further enhance the view, such as combining multiple exposures together in software in order to detect and enhance the view of objects which are barely even visible to the camera, let alone our eyes. With these techniques, we can create images of faint objects such as galaxies and nebulae.<\/p>\n<p>There are also some practical problems with trying to take long exposure photographs of the sky, such as the fact that the earth is constantly rotating. In this blog post I am going to explore some solutions to the issues involved in collecting enough photons from distant objects to create a viewable image on a practical engineering level rather than a theoretical or mathematical level.<\/p>\n<h2 id=\"light-and-visibility\">Light and visibility<\/h2>\n<p>In order for us to see anything, our eyes need to receive a certain amount of light which has been emitted by, or reflected off an object. Our own eyes are calibrated to roughly deal with the levels of light present in environments found on Earth, meaning we can see clearly during the day when there is light coming from the Sun, and more or less just enough at night depending on the moon phase or other more terrestrial light sources.<\/p>\n<p>But if we turn our head to look at the sky, we can see some things - the Sun itself (NOTE: it is not advisable to ever look directly at the Sun, it is in fact <em>too<\/em> bright for our eyes. Precautions must be taken when pointing anything at the Sun itself, it can cause blindness, damage to equipment and start fires), the Sun's light reflecting off the moon and large planets, and some stars which happen to be emitting enough light to be visible.<\/p>\n<p>But that is not the full story. There is a lot more going on in the space around us; objects which are simply not emitting (or reflecting) enough photons of light for us to see. If we were able to somehow adjust or adapt our eyes to be much more sensitive, we would in fact see that the night sky is almost completely filled with various light sources, coming from a vast array of interesting objects. Our eyes unfortunately have a fixed and limited range of sensitivity, so we will have to substitute in some technology to help us see further into the sky.<\/p>\n<h2 id=\"deep-space-objects\">Deep Space Objects<\/h2>\n<p>My aim with this hobby so far has been to try and take photos of both galaxies and nebulae. Without getting into requiring powerful optics (telescopes and lenses), and making use of what I already have, there are 2 objects which are obvious targets for getting started: The <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Andromeda_Galaxy\">Andromeda Galaxy<\/a> and the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/North_America_Nebula\">North America Nebula<\/a>. The reasons for choosing these targets are that<\/p>\n<ol>\n<li>\n<p>They are relatively large in the night sky. They are obviously absolutely huge in absolute terms, but also due to their distance from earth, they make quite large targets in the sky, so can be seen with relatively small and low power optics, such as a medium (135mm) focal length camera lens.<\/p>\n<\/li>\n<li>\n<p>They are relatively bright. Despite this however, they can only be seen with camera equipment and are not generally visible to the naked eye.<\/p>\n<\/li>\n<li>\n<p>Both of these appear to the East-to-South direction, which is convenient for viewing from my south-facing garden.<\/p>\n<\/li>\n<\/ol>\n<p>I will be trying to explore other similar objects in due course, however most of them are too far away (too small) and\/or too dim to be detectable with the equipment I have currently on hand.<\/p>\n<h2 id=\"digital-cameras\">Digital Cameras<\/h2>\n<p>Digital cameras work on the principle of using sensors which convert incoming photons of light into a small electrical charges. The typical <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Charge-coupled_device\">image sensor<\/a> consists of a 2D plane of individual \"cells\", each of which can build up its own electrical charge depending on the number of photons which have hit it during the exposure. By reading back these electrical charge values as digital numbers, we can interpret the result as an image. We can also control how long we allow each cell to charge for, and therefore how many photons it may be able to capture before we declare the image ready to read. By taking longer exposures, we can capture more photons, and it is possible now to capture more photons in a digital exposure in this way than our own eyes are able to detect.<\/p>\n<p>There is also another parameter in play with digital cameras, that of the \"ISO\" value, or sensor gain. By using a higher ISO\/gain value, we are essentially amplifying the electrical charge signal in the sensor, meaning that the resulting digital value we read out is higher for given number of photons which have arrived. However, this is not a magical solution to being able to see anything we want - as we increase the gain, we also increase the amount of random noise in the sensor. So, whilst we may be able to start to see fainter objects, as the gain increases, the image itself becomes covered in random noise which will eventually completely take over and destroy the image.<\/p>\n<p>We can work around this though. Because the photons we are collecting are arriving in a constant stream at a constant intensity and the noise is random and constantly changing, by averaging many high gain images together, then the noise will start to cancel out, but the incoming photon signal remains constant.<\/p>\n<p>The goal then, for astrophotography is to take:<\/p>\n<ol>\n<li>Long exposures - so that we can collect enough photons from dim objects that we are able to measure some degree of \"constant\" signal from that object<\/li>\n<li>Many exposures - so that the random noise in each exposure can be averaged out<\/li>\n<\/ol>\n<h2 id=\"problems-with-long-exposure\">Problems with long exposure<\/h2>\n<p>Long exposures are only going to work if the camera and the thing you are looking at are not moving relative to each other. If there is any movement, then the light coming from the object is going to be moving across the image sensor as time passes, turning the image in either a series of streaks of light, or just a big blurred mess.<\/p>\n<p>Unfortunately for astrophotography, the Earth is spinning. This means that if we have a camera in a fixed position on Earth, and point it at the sky, then taking a long exposure is going to result in \"star trail\" images, and we won't be able to see any details in the objects we are looking at. This is a nice artistic effect, but it is not what we want if we want to marvel at the fine structures present in galaxies and nebulae. Thankfully however, the stars, galaxies and nebulae we can see in the sky are far enough away that we won't have to deal with any perspective or parallax issues, in effect the night sky on any given night is a flat image we can pan around. Note that this is not true for the Sun, moon and planets - these are moving relative to the background image of the stars. Nor is it true if we take exposures at different times of year when the Earth itself has moved significantly in space.<\/p>\n<p>How do we solve this for astrophotography? We need to first tilt the camera at an angle so that we can remove the tilt offset of the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Axial_tilt\">Earth's axis of rotation<\/a>. Then we need to slowly rotate the camera around that same axis, at the same rate as the Earth is spinning.<\/p>\n<h2 id=\"mounts\">Mounts<\/h2>\n<h3 id=\"equatorial\">Equatorial<\/h3>\n<p>The recommended solution for the first problem of the tilt, is to use an <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Equatorial_mount\">equatorial mount<\/a>. The amount of tilt we apply on the mount is related to the latitude position we are observing from on Earth. I have an equatorial mount for my telescope, it was included in the package I bought along with its tripod. However, it is manual only, meaning in order to locate and view an object in the sky, I have to move the axes of the mount by hand. The second issue can be solved using electric motors and gears set up to control the equatorial axis of the mount, so that the telescope or camera tracks the apparent motion of the stars in the sky. The other main attraction of using an equatorial mount is that it allows us to reference the positions of objects in the sky using <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Astronomical_coordinate_systems\">astronomical coordinates<\/a> which are independent of the observer's location and current date and time.<\/p>\n<p>I could very well have bought an off the shelf motorised equatorial mount for my camera, but I wanted to both avoid the expense (most astronomy equipment worth having is <em>really<\/em> expensive) and also to really understand how these work and how to locate objects in the sky using astronomical coordinates.<\/p>\n<h3 id=\"altazimuth\">Altazimuth<\/h3>\n<p>An alternative to equatorial mount, is an <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Altazimuth_mount\">Altazimuth mount<\/a> - this is what you'd come up with if you were to think about panning and tilting a camera relative to the Earth's surface, such as you would do for film-making or CCTV cameras perhaps. This has the advantage of being somewhat simpler to make, as it consists of a flat turntable (Azimuth) upon which you mount a cradle which can tilt (Altitude, also sometimes called Elevation).<\/p>\n<p>It is possible to convert astronomical coordinates to local Az\/Alt coordinates so that we can locate space objects in this coordinate system relative to the Earth's surface at the observer's position. However, as we track an object across the sky, the image will be seen to rotate in the frame, because we are not aligning the camera to the Earth's axis of rotation - the camera is aligned to the Earth's surface.<\/p>\n<p>Fortunately, there is a solution also to the frame rotation problem, we can incline the Altazimuth mount itself by an amount proportional to the observer's latitude and point the Azimuth axis of rotation towards the celestial pole (\"North\") - this effectively converts the mount into an equatorial mount! The Azimuth axis becomes Right Ascension and the Altitude axis becomes Declination. This works well for most of the positions we might want to photograph, but there may still be alignment and tracking issues with this arrangement when looking at objects close to the celestial poles.<\/p>\n<h2 id=\"my-camera-mount\">My camera mount<\/h2>\n<p>I started out looking at designs for camera mounts which I would be able to make at home, using 3D printed parts and commonly available electronic components.<\/p>\n<p>I was inspired particularly by the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/isaac879\/Pan-Tilt-Mount\">mount system designed by \"isaac879\"<\/a>. However, in the spirit of wanting to understand every detail of the design and working of the device, I actually made my own design from scratch. Also, the design of Isaac's mount is too small for my camera.<\/p>\n<p>I also didn't want to spend too much time writing software for the mount, both on the embedded motor control side and also on the PC side. It is worth noting also at this point that the motors in this design are not only for compensating for the Earth's rotation, but we have control over the absolute direction we wish to point the camera - so we can use PC <a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/stellarium.org\/\">planetarium<\/a> and <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/nighttime-imaging.eu\/\">astrophotography<\/a> software to automatically point the camera at objects of interest.<\/p>\n<p>Designing and building this mount has been a multi-month spare-time endeavour and is documented in much more technical detail <a href=\"https:\/\/doug.lon.dev\/blog\/2023\/astro-camera-mount\/\">in the next blog post<\/a>.<\/p>\n<h2 id=\"setting-up-a-shoot\">Setting up a shoot<\/h2>\n<p>For an astro shoot, I currently need the following equipment:<\/p>\n<ol>\n<li>Camera - Sony a7r2 or Nikon D700<\/li>\n<li>Lens - Nikon 135mm f\/3.5 or Nikon 500m f\/8<\/li>\n<li>Mount<\/li>\n<li>Laptop<\/li>\n<li>Mains power (for mount power and laptop)<\/li>\n<\/ol>\n<p>The lens gets mounted on the camera, and the camera mounted on the mount. The mount is oriented such that the RA axis is pointing North (here in the northern hemisphere). The mount is set to its home position.<\/p>\n<h3 id=\"connect-equipment\">Connect equipment<\/h3>\n<p>The PC connects to the mount over Bluetooth serial and the camera is connected via USB. Everything is then powered on.<\/p>\n<h3 id=\"alignment\">Alignment<\/h3>\n<p>I use Stellarium to find the object I am going to shoot, and then using the Telescope Remote control, tell the mount to rotate the camera to point to it.<\/p>\n<p>Next, using N.I.N.A. I take an exposure, and using <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Astrometric_solving\">plate solving<\/a> it tells me how far off the camera alignment is. I can now re-orient the mount on the table, or turn its axes by hand to get as close as possible. It is not strictly necessary to get the target object <em>exactly<\/em> in the centre of the image - my camera has enough resolution to be able to crop the final image to centre the object. Once tracking is enabled, the object should at least be in the same place in the image for the duration of the shoot.<\/p>\n<h3 id=\"starting-the-shoot\">Starting the shoot<\/h3>\n<p>I enable <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Sidereal_time\">sidereal<\/a> tracking for the mount, and set up a sequence in N.I.N.A. to take multiple long exposures. I am still trying to find the ideal exposure time and gain settings for shooting, depending on my camera and lens combinations.<\/p>\n<h2 id=\"image-processing\">Image Processing<\/h2>\n<p>Once we have taken all of our exposures, properly aligned and tracked, we need to combine them into a single image, hopefully bringing out the details of the object so that we can marvel at its beauty.<\/p>\n<p>There are a great number of software applications designed to do this and as of right now I am no master in using any of them. I am still trying to capture the ideal exposures to feed into these apps and also still trying to find the ideal processing parameters to get the best results.<\/p>\n<p>Some options for stacking using free software include:<\/p>\n<ul>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.hnsky.org\/astap.htm\">ASTAP<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.autostakkert.com\/\">Autostakkert<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/deepskystacker.free.fr\/english\/index.html\">DeepSkyStacker<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/siril.org\/\">Siril<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/sites.google.com\/view\/sequator\/\">Sequator<\/a><\/li>\n<\/ul>\n<h2 id=\"next-steps\">Next Steps<\/h2>\n<p>There isn't much of a conclusion to this post, because this is the start of a journey to find out what works and what does not. I also haven't really been rigorous in documenting the processing methods I've tried so far and the results those methods produced.<\/p>\n<p>I will follow up with some photos and explanation of how they were made if I can get some good results that I am happy with. For now, I'm letting this all sink in and planning to put in the hours required to get good at this.<\/p>\n"},{"title":"Louper","published":"2022-03-01T00:00:00+00:00","updated":"2022-03-01T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2022\/louper\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2022\/louper\/","content":"<blockquote>\nA journey in discovering complexity in a relatively simple software application.\n<\/blockquote>\n<h2 id=\"building-a-live-looping-application\">Building a live looping application<\/h2>\n<p>I wanted to play around with a live looping pedal, but for no cost. So, what to do?<\/p>\n<p>I tried some of the existing (Linux) loop recorder applications, and none of them really grabbed me as a delight to use. I had my own ideas and pre-conceptions about how it should work, maybe that got in the way of using existing software - so maybe I should just proceed to make my own?<\/p>\n<h2 id=\"getting-started\">Getting Started<\/h2>\n<p>By building upon the platform I made for my <a href=\"https:\/\/doug.lon.dev\/blog\/2020\/guitarix-pedal\/\">Guitarix Pedal<\/a>:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200719_123943.768abccecbf54194.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>To be honest, I hadn't really made much use of this pedal. I prefer analogue hardware for my guitar tones, and I had also perpetually put off the slightly tedious task of setting up my own presets for Guitarix.<\/p>\n<p>But, underneath, it's just a Raspberry Pi running Linux. I can run other applications on it, including my own.<\/p>\n<h2 id=\"application-ux-prototype\">Application UX Prototype<\/h2>\n<p>To get a grip on my ideas of how this would work, I made a mock-up of what I'd want to see on screen and what the foot-switch buttons should do. In a spreadsheet.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Screenshot_20220224_221821.01a6af4422a4d517.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>The screen shows the following components:<\/p>\n<ul>\n<li>Top: Output signal level meter<\/li>\n<li>Middle: Tempo &amp; Time signature \/ Beat clock \/ Current mode<\/li>\n<li>Bottom: 3 loop recorder channels, each showing:\n<ul>\n<li>Mute \/ Solo state<\/li>\n<li>Cue \/ Play state<\/li>\n<li>Loop length in bars<\/li>\n<li>Loop progress bar<\/li>\n<li>Loop audio play signal level<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>The 6 buttons in yellow are annotated with their primary function, secondary function and effects of those functions.<\/p>\n<p>I also noted in the spreadsheet (not shown in the screenshot) some key behaviours and unanswered questions of the application. Some of these statements are inconsistent with the terminology on the mock-up, because I changed some of the terms during the application development. Not all of these requirements and questions have yet been addressed, but those that are are ticked here:<\/p>\n<ul>\n<li><em>Define \u201ccycle\u201d as end of current bar or end of longest loop?<\/em><\/li>\n<li>\u2705 <em>Every action happens at the end of the current cycle<\/em><\/li>\n<li>\u2705 <em>End of REC\/OD immediately starts PLAY on next cycle<\/em><\/li>\n<li><em>Cannot record into 2 channels at once; last REC stops previous<\/em><\/li>\n<li>\u2705 <em>There are no channel input\/output gain controls<\/em><\/li>\n<li>\u2705 <em>There is no master gain control<\/em><\/li>\n<li><em>Mono input<\/em><\/li>\n<li>\u2705 <em>Output 1: loops<\/em><\/li>\n<li>\u2705 <em>Output 2: click<\/em><\/li>\n<li>\u2705 <em>Click on pre-roll and first loop take only<\/em><\/li>\n<li><em>Configurable pre-roll?<\/em><\/li>\n<li>\u2705 <em>Loops extend in whole bar increments<\/em><\/li>\n<li><em>Change of tempo\/sig\/sync when 1 or more channels is not empty?<\/em><\/li>\n<li><em>Mute 1 channel == Solo 2 channels<\/em><\/li>\n<li><em>Solo 1 channel == Mute 2 channels<\/em><\/li>\n<li><em>There can be additional toggle buttons on screen, but not designed to be used mid-performance<\/em><\/li>\n<\/ul>\n<p>But what do these specifications mean? How <em>does<\/em> this thing work?<\/p>\n<h2 id=\"louper-user-manual\">Louper User Manual<\/h2>\n<h3 id=\"the-beat-clock\">The Beat Clock<\/h3>\n<p>There is a constantly running \"beat clock\" which ticks at the configured tempo and within the selected time signature. The current beat is shown prominently at the top of the display.<\/p>\n<p>One of the foot-switch buttons is dedicated to tempo and time control. Short press tap for tap-tempo to set the tempo. Long-press to cycle through the preset time signatures. (Actually, none of the long press functionality is working yet).<\/p>\n<p>The first press of the Tap Tempo button will also start the Metronome, which emits a short beep on the monitor (left) output channel only on each beat. The Metronome is silenced once one of the Loop Recorders enters a <code>PLAY<\/code> state.<\/p>\n<h3 id=\"loop-recorders\">Loop Recorders<\/h3>\n<p>There are also 3x loop recorder channels, which operate synchronised to the clock, but in <em>bar<\/em> intervals.<\/p>\n<p>All of the 3 recorders are attached to the same audio input.<\/p>\n<p>Each recorder can be in one of several states:<\/p>\n<ul>\n<li><code>CLEAR<\/code> - contains no audio, the default state.<\/li>\n<li><code>REPLACE<\/code>- when the recorder is recording new audio into its loop - the loop length increases in bar length increments.<\/li>\n<li><code>OVERDUB<\/code> - when the recorder is recording new audio into its loop, but over the top of any existing recording - the loop length does not change.<\/li>\n<li><code>PLAY<\/code> - when the recorder is playing out its recorded loop.<\/li>\n<\/ul>\n<p>Once in a given state, the recorder will continue in that state unless told otherwise. However, the recorder can only change its state at the start\/end of a bar - otherwise the application would require super-human timing to control.<\/p>\n<p>To facilitate a smooth state change, each recorder also has a \"cue\" state. This cue state can be freely-selected at any time during a bar, but only becomes active at the end of the current bar. If there is no cued state, the loop recorder will continue on with its current state.<\/p>\n<p>The state which will be used to \"cue\" a recorder is selected by pressing the \"mode\" button until the desired cue state is shown on screen, and then pressing the \"cue\" button for the desired loop recorder channel.<\/p>\n<p>Generally speaking, the states are arranged such that a recorder can be \"un-cued\" by selecting the same cue state a 2nd time. For instance, to change your mind about entering <code>REPLACE<\/code>, you can select <code>REPLACE<\/code> again, and the recorder will not change state at the end of the bar.<\/p>\n<p>You might also notice <code>MUTE<\/code> and <code>SOLO<\/code> \"states\" on the mode selector - these are actually not Loop Recorder states as such, they are actually Mixer states. As of today, this is implemented internally, and via the remote-control interface, but isn't possible to select with the foot-switches.<\/p>\n<h3 id=\"other-internal-components\">Other Internal Components<\/h3>\n<p>There also exists in the application a Metronome, as described above. This is fairly basic and emits short A-note beeps on the beat. There is also a Pass-through channel and an audio Mixer, so that the input can always be heard, and mixed with all the other channels before reaching the output.<\/p>\n<p>The application is also running an OSC server for remote-control. The inputs from the hardware buttons are implemented as a separate process which sends messages to this server.<\/p>\n<h3 id=\"make-it-stop\">Make it stop!!!<\/h3>\n<p>There is a dedicated foot-switch button to \"clear all\" of the loop recorders, to make the noise stop at the end of the current bar.<\/p>\n<h2 id=\"system-design\">System Design<\/h2>\n<p>I wrote this application in C++ since we require maximum performance, to aim for low latency \"real-time\" audio processing. It's a fairly standard <code>cmake<\/code> project outputting a library and several binaries.<\/p>\n<p>There were some interesting challenges in getting the above to work though.<\/p>\n<h3 id=\"loop-recorder-states\">Loop Recorder States<\/h3>\n<p>It is probably apparent if you read the above \"manual\" that the Loop Recorder state control could easily get out of hand and not work correctly.<\/p>\n<p>Each Loop Recorder actually implements <em>two<\/em> linked state machines.<\/p>\n<p>The state machines are a little tricky to describe, given that the state names and events have the same names, and also that both machines also deal with the same states and events. Remember that the Loop Recorder \"state\" is what the recorder is currently doing. The \"cue state\" is what you want it to do <em>next<\/em>. The events describe what you want the \"cue state\" or \"state\" to change to <em>now<\/em>. Put another way, the \"cue state\" is used as an <em>event<\/em> for the next \"state\".<\/p>\n<p>This is made worse by me now describing State Machines to change two different \"states\". Clear? OK, here's the state machine definitions...<\/p>\n<h4 id=\"state-machine-1-cue-state\">State Machine 1 - Cue State<\/h4>\n<p>This is an interesting machine to construct. I didn't expect all these transitions and conditions when I started, but when playing with the state changes, it quickly emerges that this is required, just to make the behaviour feel intuitive and correct.<\/p>\n<p>And unfortunately, I have to describe this monstrosity first, else the other state machine makes no sense.<\/p>\n<ul>\n<li>\n<p>Cue States : <code>PLAY<\/code> - <code>REPLACE<\/code> - <code>OVERDUB<\/code> - <code>CLEAR<\/code><\/p>\n<\/li>\n<li>\n<p>Events : <code>REPLACE<\/code> - <code>OVERDUB<\/code> - <code>CLEAR<\/code><\/p>\n<\/li>\n<li>\n<p>Transitions :<\/p>\n<ul>\n<li>\n<p>Cue State <code>PLAY<\/code> + Event <code>REPLACE<\/code> =&gt; Cue State <code>REPLACE<\/code><\/p>\n<\/li>\n<li>\n<p>Cue State <code>PLAY<\/code> + Event <code>OVERDUB<\/code> =&gt; Cue State <code>OVERDUB<\/code><\/p>\n<\/li>\n<li>\n<p>Cue State <code>PLAY<\/code> + Event <code>CLEAR<\/code> =&gt; Cue State <code>CLEAR<\/code><\/p>\n<\/li>\n<li>\n<p>Cue State <code>REPLACE<\/code> + Event <code>REPLACE<\/code><\/p>\n<ul>\n<li>=&gt; Cue State <code>PLAY<\/code> <em>IF the recorder is not empty<\/em><\/li>\n<li>=&gt; Cue State <code>CLEAR<\/code> <em>IF the recorder is empty<\/em><\/li>\n<\/ul>\n<\/li>\n<li>\n<p>Cue State <code>REPLACE<\/code> + Event <code>OVERDUB<\/code><\/p>\n<ul>\n<li>=&gt; Cue State <code>OVERDUB<\/code> <em>IF the recorder is not empty<\/em><\/li>\n<li>=&gt; Cue State <code>CLEAR<\/code> <em>IF the recorder is empty<\/em><\/li>\n<\/ul>\n<\/li>\n<li>\n<p>Cue State <code>REPLACE<\/code> + Event <code>CLEAR<\/code> =&gt; Cue State <code>CLEAR<\/code><\/p>\n<\/li>\n<li>\n<p>Cue State <code>OVERDUB<\/code> + Event <code>REPLACE<\/code> =&gt; Cue State <code>REPLACE<\/code><\/p>\n<\/li>\n<li>\n<p>Cue State <code>OVERDUB<\/code> + Event <code>OVERDUB<\/code><\/p>\n<ul>\n<li>=&gt; Cue State <code>PLAY<\/code> <em>IF the recorder is not empty<\/em><\/li>\n<li>=&gt; Cue State <code>CLEAR<\/code> <em>IF the recorder is empty<\/em><\/li>\n<\/ul>\n<\/li>\n<li>\n<p>Cue State <code>OVERDUB<\/code> + Event <code>CLEAR<\/code> =&gt; Cue State <code>CLEAR<\/code><\/p>\n<\/li>\n<li>\n<p>Cue State <code>CLEAR<\/code> + Event <code>REPLACE<\/code> =&gt; Cue State <code>REPLACE<\/code><\/p>\n<\/li>\n<li>\n<p>Cue State <code>CLEAR<\/code> + Event <code>OVERDUB<\/code> =&gt; Cue State <code>REPLACE<\/code><\/p>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;cue-state-machine.65b76e9d0456bf93.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>You might notice that this can automatically cycle through many Cue States if you are inputting <code>OVERDUB<\/code> events and the recorder itself is already in a certain state; e.g. <code>OVERDUB<\/code> =&gt; <code>REPLACE<\/code> =&gt; <code>CLEAR<\/code> =&gt; <code>REPLACE<\/code> =&gt; ...<\/p>\n<p>This probably isn't desirable and needs some testing and tweaking.<\/p>\n<h4 id=\"state-machine-2-state\">State Machine 2 - State<\/h4>\n<p>This one is a little more straightforward, it didn't require any conditions.<\/p>\n<ul>\n<li>\n<p>States : <code>PLAY<\/code> - <code>REPLACE<\/code> - <code>OVERDUB<\/code> - <code>CLEAR<\/code><\/p>\n<\/li>\n<li>\n<p>Events : <code>PLAY<\/code> - <code>REPLACE<\/code> - <code>OVERDUB<\/code> - <code>CLEAR<\/code> (These are actually \"Cue States\" !)<\/p>\n<\/li>\n<li>\n<p>Transitions :<\/p>\n<ul>\n<li>\n<p>State <code>PLAY<\/code> + Cue State <code>REPLACE<\/code> =&gt; State <code>REPLACE<\/code><\/p>\n<\/li>\n<li>\n<p>State <code>PLAY<\/code> + Cue State <code>OVERDUB<\/code> =&gt; State <code>OVERDUB<\/code><\/p>\n<\/li>\n<li>\n<p>State <code>PLAY<\/code> + Cue State <code>CLEAR<\/code> =&gt; State <code>CLEAR<\/code><\/p>\n<\/li>\n<li>\n<p>State <code>REPLACE<\/code> + Cue State <code>PLAY<\/code> =&gt; State <code>PLAY<\/code><\/p>\n<\/li>\n<li>\n<p>State <code>REPLACE<\/code> + Cue State <code>OVERDUB<\/code> =&gt; State <code>OVERDUB<\/code><\/p>\n<\/li>\n<li>\n<p>State <code>REPLACE<\/code> + Cue State <code>CLEAR<\/code> =&gt; State <code>CLEAR<\/code><\/p>\n<\/li>\n<li>\n<p>State <code>OVERDUB<\/code> + Cue State <code>PLAY<\/code> =&gt; State <code>PLAY<\/code><\/p>\n<\/li>\n<li>\n<p>State <code>OVERDUB<\/code> + Cue State <code>REPLACE<\/code> =&gt; State <code>REPLACE<\/code><\/p>\n<\/li>\n<li>\n<p>State <code>OVERDUB<\/code> + Cue State <code>CLEAR<\/code> =&gt; State <code>CLEAR<\/code><\/p>\n<\/li>\n<li>\n<p>State <code>CLEAR<\/code> + Cue State <code>REPLACE<\/code> =&gt; State <code>REPLACE<\/code><\/p>\n<\/li>\n<li>\n<p>State <code>CLEAR<\/code> + Cue State <code>OVERDUB<\/code> =&gt; State <code>REPLACE<\/code><\/p>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;state-state-machine.93fcae6850611c34.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>All of this basically just describes basic tape recorder machine mechanics, that would be familiar to anyone who used or played with a real hardware tape recorder. e.g. \"You cannot play if there's no tape in the machine\", \"Overdubbing onto an empty tape is the same as recording (replacing) on to it\".<\/p>\n<h3 id=\"other-components\">Other Components<\/h3>\n<p>Most of the other components are nowhere near as complex as this, they are fairly boring and exist to support the Loop Recorders:<\/p>\n<ul>\n<li><code>AudioDrv<\/code> : Interfaces with the audio subsystem. I chose the <code>rtaudio<\/code> library to drive this, as it had a straightforward method of doing duplex audio. Once started, the library calls us back at regular intervals with input and output buffers. All we have to do is read the input, \"perform some processing\" and write back to the output buffer.<\/li>\n<li><code>Control<\/code> : Starts an OSC server for remote control. The inputs from the foot-switches are actually handled by an entirely different process, which sends messages to the main application via OSC.<\/li>\n<li><code>Meter<\/code> : Derives peak and RMS values for audio signals.<\/li>\n<li><code>Mixer<\/code> : Mixes multiple audio signals together. I'll explain which signals below.<\/li>\n<li><code>TapTempo<\/code> : Tap tempo calculator. Receives events and calculates the (average) duration between them as beats-per-minute.<\/li>\n<li><code>Transport<\/code> : Derives bar and beat clock values from tempo, time signature, sample rate and buffer sizes. I may have lied about all of these components being simple. This one was not entirely straightforward either.<\/li>\n<li>Other channel types:\n<ul>\n<li><code>Passthrough<\/code> : Simply copies input to output, used to provide a monitor of the input, so you can hear what you're playing.<\/li>\n<li><code>Metronome<\/code> : Generates beeps on the beat, driven by <code>Transport<\/code>.<\/li>\n<\/ul>\n<\/li>\n<li><code>Looper<\/code> : The main application controller which sets up and glues together all of the above.<\/li>\n<\/ul>\n<p>At least two of these deserve a bit more explanation;<\/p>\n<h4 id=\"mixer\">Mixer<\/h4>\n<p>Perhaps a block diagram of the audio signal flow might explain why we need this:<\/p>\n<pre style=\"background-color:#2b303b;color:#c0c5ce;\"><code><span>\n<\/span><span>  [Audio Input] ----+---- [Loop Recorder 1] ----\\\n<\/span><span>                    +---- [Loop Recorder 2] -----+\n<\/span><span>                    +---- [Loop Recorder 3] -----+[Mixer] ---- [Audio Output]\n<\/span><span>                    \\---- [Passthrough] ---------+\n<\/span><span>                                                 |\n<\/span><span>                          [Metronome] ----------\/\n<\/span><span>\n<\/span><\/code><\/pre>\n<p>Whilst it is simple to provide all the channels with the same input buffer to process (they literally all read the same buffer in turn), in order to hear all the signals at once we must add up the values from their outputs to provide to the audio output. Hence, we mix them together.<\/p>\n<p>Note that the Metronome doesn't use the audio input, it generates its own signal.<\/p>\n<h4 id=\"transport\">Transport<\/h4>\n<p>Since we are wanting to operate in musical time increments of bars and beats, we need something which can tell us where we are in the music. This component exists to translate the running time of the application in to bars and beats values.<\/p>\n<p>This became surprisingly tricky, given that the application is actually timed from the <code>AudioDrv<\/code> callbacks. The audio driver gives us a callback with buffers of fixed a length, which typically are a power of or multiple of two. The intervals between these callbacks depend on the sample rate and the buffer size we chose to work with when we started the system. Everything else in the application is synchronised to these callbacks.<\/p>\n<p>For example, I found that the Raspberry Pi likes to operate reliably using a sample rate of 32000 samples per second and a buffer size of 96. We therefore get 1000 callbacks every 3 seconds. This happens regardless of the tempo we want to use, and never changes whilst the application is running.<\/p>\n<blockquote>\nAside: it also determines our processing latency, which in this case is 3 milliseconds - low enough to use live and not notice - that also means that the all processing we do inside the callback must be completed within this time period.\n<\/blockquote>\n<p>What we want, however, is events to be fired inside the application <em>exactly<\/em> when a bar end or beat is occurring. e.g. to drive the Metronome, or to change the Loop Recorder channel states. These events almost never will happen in between audio callbacks, but most likely part way through, and never always in the same place. This also depends on the chosen tempo.<\/p>\n<p>What the Transport does, is to count each callback and accumulate a total of the number of samples elapsed. It also knows how many samples long a bar and beat are, so for a given callback it can calculate the indices of the buffer at which these events should occur. This involves a nasty algorithm which I discovered via test-driven-development and haven't really fully analysed yet. It has to deal with the fact that the bar or beat length may be less than, the same as or greater than the audio driver buffer length - it makes no assumptions. Therefore, in any given callback, there may be zero, one, or many indices at which these events occur.<\/p>\n<p>During the audio callback, we can then emit events part way through the buffer processing for bar and beat at the indices given to us by Transport, thus ensuring every state change in the application is accurately timed according to the musical time.<\/p>\n<h2 id=\"gui\">GUI<\/h2>\n<p>Yes, I implemented GUI for this, using <code>Dear ImGui<\/code>:<\/p>\n<p><img src=\"https:\/\/doug.lon.dev\/blog\/2022\/louper\/louper-gui.gif\" alt=\"\" \/><\/p>\n<p>Its designed to fit nicely on the pedal's 800x480 screen and be readable from the floor. It could do with a couple of tweaks already, e.g. the time signature is not displayed.<\/p>\n<h2 id=\"the-code\">The Code<\/h2>\n<p>The code is GPL 3 licensed and available on my bitbucket: <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/louper\">doughammond\/louper<\/a><\/p>\n<h2 id=\"why-louper\">Why \"Louper\" ?<\/h2>\n<p>For my wife, Louisa, I set this name on Valentine's day 2022 \u2764\ufe0f<\/p>\n"},{"title":"Guitarix Pedal","published":"2020-07-26T00:00:00+00:00","updated":"2020-07-26T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2020\/guitarix-pedal\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2020\/guitarix-pedal\/","content":"\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200719_123943.273149a872927366.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"motivation\">Motivation<\/h2>\n<p>I'd recently been looking to acquire a guitar pedal multi-effect\/amp modeller.\nThere's various models available, ranging in cost from about \u00a3150 to over \u00a31200.\nI was really tempted to go for something in the upper-mid-range, but could not overall justify the expense.<\/p>\n<p>Looking at the devices, however, they mostly nowadays consist of various types and numbers of the following components:<\/p>\n<ul>\n<li>Foot switches; at least 3<\/li>\n<li>Indicators for foot switches; either built-in RGB LEDs, or adjacent LEDs or even small LCD screens<\/li>\n<li>A main LCD screen, usually now full colour graphical and sometimes touchscreen<\/li>\n<li>An expression pedal or two<\/li>\n<li>Some number of audio inputs and outputs; usually at least 1 guitar input and a stereo output<\/li>\n<li>Some other knobs for volume or parameter control<\/li>\n<li>Some other buttons for setup, load\/save functions etc<\/li>\n<\/ul>\n<p>Obviously internally these devices now are running a reasonably powerful CPU\/DSP of some sort with a nice fully graphical interface to control it. It occurred to me that a Raspberry Pi 4 would likely be capable of performing such duties...<\/p>\n<p>I also knew from \"The Task Switch\" project that wiring up several push buttons to a Raspberry Pi is not particularly difficult, and neither is driving some kind of display - either dot-matrix LCD or touchscreen etc.<\/p>\n<h2 id=\"hardware\">Hardware<\/h2>\n<p>I set about looking for suitable components to start building my own pedal, although going for the full compliment of possible components does indeed start to add up in cost to the point where if you want a pedal with all the bells and whistles, you may as well just buy one off the shelf.<\/p>\n<p>So, I limited the scope of my design to just the following:<\/p>\n<table><thead><tr><th><strong>Part<\/strong><\/th><th><strong>Cost Per Item<\/strong><\/th><th><strong>Quantity<\/strong><\/th><th><strong>Line Cost<\/strong><\/th><\/tr><\/thead><tbody>\n<tr><td>Raspberry Pi 4, 2GB + PSU + SD Card<\/td><td>\u00a359.00<\/td><td>1<\/td><td>\u00a359.00<\/td><\/tr>\n<tr><td>5 inch HDMI display with touch<\/td><td>\u00a342.99<\/td><td>1<\/td><td>\u00a342.99<\/td><\/tr>\n<tr><td>Foot-switch FS5700SPMT2B2M1QEH<\/td><td>\u00a34.61<\/td><td>6<\/td><td>\u00a327.66<\/td><\/tr>\n<tr><td>Enclosure BUD AC-403 + bottom plate + feet<\/td><td>\u00a321.77<\/td><td>1<\/td><td>\u00a321.77<\/td><\/tr>\n<tr><td>Audio Injector Ultra 2<\/td><td>\u00a362.05<\/td><td>1<\/td><td>\u00a362.05<\/td><\/tr>\n<tr><td>Other various cables, connectors, hardware<\/td><td>\u00a329.05<\/td><td>1<\/td><td>\u00a329.05<\/td><\/tr>\n<\/tbody><\/table>\n<p>Total cost of parts: <strong>\u00a3242.51<\/strong><\/p>\n<p>The Audio Injector interface turned out to be a large part of the cost, however it runs up to 24bit\/192kHz at very low latency - which is important for good sound quality and playing live.<\/p>\n<p>My intention is to use the 6 buttons for the following functions:<\/p>\n<ul>\n<li>\"page change\"<\/li>\n<li>\"Previous\"<\/li>\n<li>\"Select\" 1, 2, and 3<\/li>\n<li>\"Next\"<\/li>\n<\/ul>\n<p>The select buttons may activate a preset bank, a preset or toggle an individual effect on\/off depending on the current page. The \"page change\" button cycles between banks, presets, units. \"Previous\"\/\"Next\" scroll through the items on the current page and \"Select\" activates an option on the current page.<\/p>\n<p>I created an <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.openscad.org\/\">OpenSCAD<\/a> model of the components and the enclosure, in order to figure out where to place everything and check that it will all fit in the enclosure:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Screenshot_20200726_192814.c8e8058b26559ee8.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>This turned out to be a good exercise, but in the end I didn't use all of the CAD dimensions to actually make the enclosure.<\/p>\n<h2 id=\"software\">Software<\/h2>\n<p>I have set up the Raspberry Pi to run the latest Raspbian operating system (i.e. the default choice).<\/p>\n<p>On top of that there are actually many options for the DSP; <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/guitarix.org\/\">Guitarix<\/a> and <a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/rakarrack.sourceforge.net\/\">Rackarack<\/a> to name but two. I could even set up my own chain of plugins using a plugin host for JACK, for example.<\/p>\n<p>I opted to go with Guitarix, since it seems very well written, the UI is well thought out, looks good and has quite a few publicly published banks of presets. It also exposes a JSONRPC API for remote control.<\/p>\n<p>To bring this all up on the Raspberry Pi, I configured the OS to auto login and run <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/i3wm.org\/\">i3wm<\/a> window manager, so that the application start with a predictable position and layout on the small screen - I will later also run my own remote control app which interfaces with the foot switches.<\/p>\n<p>When i3 starts, it is given a screen layout to use, creating placeholder regions for both Guitarix and the guitarix-foot-remote apps. It also starts up JACK in the background.<\/p>\n<h2 id=\"guitarix-foot-remote\">guitarix-foot-remote<\/h2>\n<p>The only custom software in this build is a small app which reads the foot-switch states, and uses the inputs to control Guitarix via its JSONRPC interface.<\/p>\n<p>I have open sourced the code, it is published here <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/guitarix-foot-remote\">on my bitbucket<\/a>.<\/p>\n<p>The repo also contains some of the hardware reference drawing, and enclosure design drawings.<\/p>\n<p>The app is designed to display 3 \"tabs\", corresponding to the \"Select\" foot-switches. The tabs cab be scrolled with the \"Previous\"\/\"Next\" buttons and the page cycled between Banks, Presets and Units with the \"Page\" button<\/p>\n<p>Banks page:\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Screenshot_20200726_192102.0d6cba12699322c0.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\nSelecting a bank changes the UI to the Presets page.<\/p>\n<p>Presets page:\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Screenshot_20200726_192135.feadb050a114fd60.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\nSelecting a Preset changes the page to the Units page.<\/p>\n<p>Units page:\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Screenshot_20200726_192157.cdad1dc8eecbc232.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\nSelecting a unit will toggle its on\/off state.<\/p>\n<p>Pressing the Page button will return back to the Banks page.<\/p>\n<p>Other acknowledgements:<\/p>\n<ul>\n<li>Qt<\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/nlohmann\/json\">nlohmann::json<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/bwalter\/qt-promise\">qt-promise<\/a><\/li>\n<\/ul>\n<h2 id=\"assembly\">Assembly<\/h2>\n<p>First test of the audio interface, to prove that this configuration is viable:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200625_112520.5e98f2e43505dfd0.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Marking out the enclosure, drilling and cutting holes:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200714_174611.509cf3f75e389474.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200714_174620.83ae4ef7ea187155.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200715_145722.3cdab2d3a3f4d6ab.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Aligning the Raspberry Pi etc on the bottom plate, against the other internal components:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200715_174057.b703fab08e685e53.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Full system assembly testing (without enclosure):<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200718_170055.dfcb547465d8788f.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200718_180404.426840686fb69d92.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Painting the enclosure:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200718_180414.e57f3343512a3cdb.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Final assembly:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200719_123943.273149a872927366.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20200719_124005.789ff1cb9ce216ec.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"problems-encountered-along-the-way\">Problems encountered along the way<\/h2>\n<ul>\n<li>\n<p>The CAD printouts for the enclosure holes came out at the wrong size. I really should have measured these before starting to cut. As a result, the initial holes I drilled for the screen mounting screws were in completely the wrong place. I had to subsequently fill these in before painting.<\/p>\n<\/li>\n<li>\n<p>The 40-pin flat cable for the audio interface was not long enough, I replaced it with a few jumper wires instead.<\/p>\n<\/li>\n<li>\n<p>Some of the internal cables and wiring took up too much space, such that I could not completely close up the unit. I had to replace the other 40-pin flat cable for the buttons interface with much smaller\/slimmer wire-wrap wires.<\/p>\n<\/li>\n<li>\n<p>In the process of multiple assembly\/disassembly whilst making the enclosure I broke the micro USB socket on the display. This was supposed to be used as the main power input to the entire system. I tried to repair it but to no avail. I elected instead to use the GPIO block to connect the screen power. However, the jumper pins to the screen also took up too much space, preventing the unit from closing. I ended up cutting the ends off the jumper wires and soldering them directly to the display circuit board - fortunately solder pads were provided.<\/p>\n<\/li>\n<li>\n<p>I had to perform a similar adaptation with the audio interface, using jumper wires was not good enough of a connection which caused a lot of noise and crackling on the audio output. I ended up again cutting the ends off the jumper wires and soldering them directly to the audio interface PCBs.<\/p>\n<\/li>\n<\/ul>\n"},{"title":"The Task Switch","published":"2018-06-24T00:00:00+00:00","updated":"2018-06-24T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2018\/the-task-switch\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2018\/the-task-switch\/","content":"\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;00000IMG_00000_BURST20180519182428_COVER.636eca0f34380ea1.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h2 id=\"what-is-this\">What is this ?<\/h2>\n<p>I have built a little box of buttons which I can use to record the time I spend\non each activity at work. Mostly just for my own amusement. I've open sourced\nthe entire concept, software and physical designs (links at the bottom).<\/p>\n<p><em>This blog post doesn't really go into a lot of the gritty detail of the build of\nthis project, unfortunately trying to achieve that might just take longer than the\nbuild already did. So, apologies if this skims over some of the interesting stuff,\nbut you could always have a dig through the repos later.<\/em><\/p>\n<h2 id=\"background-motivation\">Background &amp; Motivation<\/h2>\n<p>We are all from time to time either curious about or get asked to record the\ntime we spend on activities. There is a particularly long running practice of\nthis at my work, and in fact I have for nearly 6 years now been working in a\nteam which\u00a0 provides the company's Time and Attendance solution (amongst other\nbusiness tools). Because of this, I am very aware of the benefits of such\npractices, in that it allows you to use that data for several quite important\npurposes, for example:<\/p>\n<ul>\n<li>Figuring out the total cost of a project,<\/li>\n<li>Being able to more accurately estimate projects,<\/li>\n<li>Being able to report and push back on activities which are a \"waste of time\" or \"not what I should be doing\".<\/li>\n<\/ul>\n<p>I first started recording my distractions using a notebook and pen, as at that\ntime there was quite a few happening and management wanted to know why our team\nwas always going over our estimates. My opinion was that we were being badly\nmanaged, and that there was some difference of expectations of what each other\nshould be doing. That all eventually got resolved, but the practice of habitually\nrecording all of my activity remained with me.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180619_184939.e1b02224c883bfd6.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Naturally as part of that process I needed to analyse the data I recorded. This\nstarted off using Excel sheets; I copied the notebook entries into the sheet,\nand started to classify each item according to importance\/urgency (see\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Time_management#The_Eisenhower_Method\">The Eisenhower Method<\/a>).\nIn this step I could also calculate the duration of each activity. The end result\nbeing a nice pie chart per week of time spent in each \"Quadrant\". Looking at this\ndata, I could then try to establish how to spend more time in \"Q2\" and less in\nthe others. Mostly by the reduction in interruptions and distractions.<\/p>\n<p>Of course, doing all of this by hand was very time consuming. It could take 60\nto 90 minutes at the end of each week to input all the data. Also, wrangling\nnumbers in the same way in a repeated and consistent fashion in Excel is extremely\nlaborious. I eventually wrote a little web app which could ingest the data in\nYAML format and do the calculation and presentation for me. However, I still had\nto type out the YAML data by hand.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Screenshot_20180619_222911.9d6e3387a21ce4ed.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Eventually I stopped putting the data into the computer as the work situation\nimproved significantly, but I still kept the habit of writing activities down in\na notebook. I found in fact the act of writing down each context-change a useful\nmental tool, to ensure I stayed on the task at hand and did not wander off doing\nsomething else.<\/p>\n<p>Just over a year passed of notebook logging, and then it struck me that I would\nlike to perform the reporting again, but mostly out of curiosity than necessity.\nUnfortunately the year's worth of hand written notes is not going to get typed\ninto the system, I will have a big gap in my data.<\/p>\n<h2 id=\"initial-concept\">Initial concept<\/h2>\n<p>On 4th March 2018, near the end of an idle Sunday at home, almost out of nowhere,\nI sketched the first concept for the design of something I wanted to build.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180619_212619.6bd191d55447be81.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I realised that I wanted a way to input quickly into a computer my current\nactivity and I eventually realised that in fact it does not need to be incredibly\ndetailed. All I need to be able to do is record enough information to derive an\nEisenhower Quadrant number and a duration for each activity.<\/p>\n<blockquote style=\"text-align: center;\">\n<em>My number one UX requirement is that the input should be as quick, or quicker\nthan writing a few words with a pen.<\/em>\n<\/blockquote>\n<p>This would also generally require that the activity logger should not be running\non my main workstation. My workstation is a protected environment where the act\nof recording a context-switch is in itself too much of a context-switch. Add to\nthat the constraint that our workplace workstations have no internet access and\nis extremely difficult to install and run arbitrary software and I quickly came\nto the former conclusion. The final justification for this solution is that I am\nnot always at my desk during a context-switch (e.g. back to back meetings), then\nsomething separate and potentially portable makes sense.<\/p>\n<p>I came up with the idea of using \"category tags\" as the input method. This\nconsists of a short list of tags which can be used in combinations to describe\nthe current activity:<\/p>\n<ul>\n<li><code>DEV<\/code> : Development<\/li>\n<li><code>SUPP<\/code> : Support<\/li>\n<li><code>PRJ<\/code> : Project<\/li>\n<li><code>EML<\/code> : Email<\/li>\n<li><code>SCRM<\/code> : Scrum<\/li>\n<li><code>MEET<\/code> : Meeting<\/li>\n<li><code>INTR<\/code> : Interrupt<\/li>\n<li><code>PHN<\/code> : Phone call<\/li>\n<li><code>IMP<\/code> : Eisenhower's \"Important\"<\/li>\n<li><code>URG<\/code> : Eisenhower's \"Urgent\"<\/li>\n<\/ul>\n<p>For example, generally the first thing I do in the morning is check and respond\nto emails, this activity is generally just tagged using <code>{EML, IMP}<\/code>. Inevitably\nduring the morning, my phone will ring and it will be either my boss or another\nmanager asking for some information, I may tag this activity as <code>{INTR, PHN, URG}<\/code>.\nAlso every morning we have a team Scrum meeting; I made a shortcut tag for this,\nbut it could also maybe be described using <code>{DEV, MEET, IMP}<\/code>.<\/p>\n<p>As you can see, there are quite a few interesting tag combinations which can be\nused to describe developer and developer-manager activities. I can also\nautomatically derive the Eisenhower quadrants according to whether the <code>IMP<\/code>\nand\/or <code>URG<\/code> tags were specified.<\/p>\n<p>I originally also thought a numeric keypad would be nice in order to input\nspecific task codes, however I couldn't really justify this in the end as it adds\na lot of complexity and a number alone is not sufficient to identify tasks in the\nlong term. That and the fact that I do not really do very many development tasks\nwith actual code reference numbers any more. It was a bit of a relief to drop\nthis feature and arrive at a solution which is much simpler. Having the keypad\ninterface also made the physical size of the device uncomfortably large. By\ndropping this feature though, I would lose the ability to track time spent on\nindividual projects, but as a developer-manager now I rarely contribute any direct\ndevelopment input into projects any more, and I would rather focus on tracking\nmy own overall efficiency across all of the activities I have to perform.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;front-panel-large-e1529525260235.54a179d4a1d60f21.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>One thing I needed to replace though was a way to \"accept\" the tag inputs. I\noriginally set out to use the keypad's \"#\" and \"*\" buttons for \"accept\" and\n\"cancel\" respectively, but now I needed something else.<\/p>\n<p>I knew, however, from the start that I wanted physical buttons for the tag inputs,\nand a colour-changing LCD screen as the display. I've used such a screen before\nand like the retro style. This alone led me on to a sort of cool, retro\narcade-machine look for the final physical product.<\/p>\n<p>Ah, an arcade machine. How about a comically big illuminated push button on the\ntop for \"accept\" ? Perfect. As it turns out, I also could happily remove the\n\"cancel\" button altogether without affecting functionality.<\/p>\n<p>The original \"specification\" sketch above also calls for a Raspberry Pi, and LED\nilluminated push-buttons. I chose the Raspberry Pi Zero W as the heart of the\napplication, which also necessitated a couple of other accessories to support\nthe other hardware; 2x MCP23017 GPIO port expanders and a MAX7219 LED driver.<\/p>\n<h2 id=\"prototyping\">Prototyping<\/h2>\n<p>Being familiar with Python and Qt, I quickly threw together a PyQT mock up of\nthe physical interface.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Screenshot_20180620_145038.fdfc7f681f3e61de.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I decided to reduce the number of inputs from that shown on my initial sketch.\nI did not need dedicated accept\/cancel buttons, and I could only come up with\n10 useful tag buttons. The only other interface features are a single action\nbutton and a 16x2 character \"LCD Screen\". It didn't take very long at all to\nwrite the application logic and have something which I could test!<\/p>\n<p>I had to build the prototype application in an\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Model%E2%80%93view%E2%80%93controller\">MVC<\/a> pattern\n(naturally), knowing that I could simply implement a second <code>V<\/code> for the physical\nhardware interface. This turned out to be a good thing to always keep in mind,\nas I was always conscious of exactly which parts of the application code and\nlogic belong where, I definitely did not want to have to duplicate any code when\nI started on the hardware interface \"View\". In the end, I actually allowed the\n<code>C<\/code> to plug into multiple <code>V<\/code> at the same time, this was invaluable for testing\nthe hardware, as I could click a button on screen and see the same state\nreflected on the hardware (and vice versa) - that was a good way to ensure I had\ncorrectly set up the hardware button inputs and that they all worked correctly.\nThe <code>M<\/code> in the system is an output-only class writing the activity log to YAML\nfile. This was later extended to be able to read back the last logged item when\nthe system restarts, and also to read the entire log for web syncing much later.<\/p>\n<p>However, before embarking on any hardware build, I wanted to ensure that I had\nthe interface and UX as per my primary goal. I needed to get my prototype\napplication on to my desk to start using immediately. I have a small x86 tablet\n(Viewsonic Viewpad) kicking around from some years ago, so I thought it would be\na good idea to test the app on it on my desk as I developed the rest of the\nphysical object. I installed a fresh Linux (Kubuntu) on it, cloned my app and\nwas up and running.<\/p>\n<blockquote style=\"text-align: center;\"><em>\nWithin a week of having the idea for this project, I had something already working\nand on my desk to replace my notepad and pen.\n<\/em> <\/blockquote>\n<p>My last real notebook entry was on 8th March 2018, and the note for the following\nday reads \"time logged digitally :)\". Indeed the first records in my YAML log\nfiles start on 9th March 2018.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180619_133327.960cba0217061579.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Note that I have only been working on this project during some lunch times at\nwork, and a few evenings or weekend afternoons at home. The rest of the journey\nbelow to make the device real took around 3.5 months.<\/p>\n<h2 id=\"the-software\">The Software<\/h2>\n<h3 id=\"operation\">Operation<\/h3>\n<p>So, how does the software work exactly? The primary function is to input a\ncombination of tags.<\/p>\n<p>The initial state is that none of the buttons are lit up, and the LCD screen\ndisplays the current time.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;application-state-flow-diagram.7bee52671b7a1521.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>One just has to press the relevant tag buttons (each one lights up as you press it)\nand then the \"action\" button to start the timer. During this selection phase, the\naction button turns green to signal as \"accept\". Once the activity has been accepted,\nthe selected tag names and the activity elapsed time is shown on the LCD screen and\nthe action button is red, to signal as a stop function. The LEDs in the tag buttons\nalso turn off. The timer keeps going until you press the action button again.<\/p>\n<blockquote style=\"text-align:center;\"><em>\nMy number two UX requirement is that it needs to be <b>not<\/b> distracting at <b>all<\/b> times.\n<\/em><\/blockquote>\n<p>Therefore, most LEDs are normally off and only used during the time of input.\nWhilst the activity is running, the action button's red colour intensity is\nactually lower than normal.<\/p>\n<p>If you did not select any more tags before pressing the action button (i.e. you\npress it in the red state) then the activity is ended.<\/p>\n<p>If you selected more tags before pressing the action button, it first turns green\n(to signal a start action) and then the timer starts on the new activity.<\/p>\n<p>If there is no current activity, the timer displays the idle time as a negative\nvalue, and the action button is not illuminated.<\/p>\n<p>The LCD screen also flashes up \"OK\" (in green) or error messages (in red) on input.\nThere is only a small amount of validation built into the software - you cannot\nselect only <code>IMP<\/code> and\/or <code>URG<\/code> tags, you have to also specify at least one other\ntag. It will also display an error if the internal YAML file could not be written.<\/p>\n<p>That is it for the human interaction part. The only other function which the\ndevice performs additional to the original specification is a periodic sync to a\nweb service, during which time the LCD screen turns blue (and can flash a red\nerror message if this fails).<\/p>\n<h3 id=\"structure\">Structure<\/h3>\n<p>The code is written in Python, and I structured the project as a single python\npackage called <code>tts<\/code>. There are three sub-packages in <code>tts<\/code>:<\/p>\n<ul>\n<li><code>tts.core<\/code> : contains the controller, models, state management and validation<\/li>\n<li><code>tts.ui<\/code> : contains the views;\n_ <code>tts.ui.desktop<\/code> with the PyQt widget interface,\n_ <code>tts.ui.hardware<\/code> with the hardware interface and device drivers<\/li>\n<li><code>tts.net<\/code> : contains the web API sync code<\/li>\n<\/ul>\n<p>The controller receives events from the UI view(s) and uses those to update the\napplication state model. The state model emits its own events, and these are\ndispatched out to the storage model or back to the UI view(s) as needed.<\/p>\n<h4 id=\"redux-state-model\">Redux state model<\/h4>\n<p>Given that I am primarily a web application developer(-manager), I have recent\nexperience using <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/reactjs.org\/\">React<\/a> and <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/redux.js.org\/\">Redux<\/a>.\nI have really like using Redux for application state management, the strictly\nfunctional programming approach to this makes a lot of sense and I have seen it\nused effectively to provide a robust way to manage application state. I wanted\nto make use of this in this application instead of having some ad-hoc states in\nthe controller.<\/p>\n<p>Making a python (or any) clone implementation of this is remarkably simple. The\nmain <code>Store<\/code> class is but 30 lines of code; including support for middleware.<\/p>\n<pre data-lang=\"python\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-python \"><code class=\"language-python\" data-lang=\"python\"><span style=\"color:#65737e;\">## https:\/\/bitbucket.org\/doughammond\/the-task-switch\/src\/08ee4e427055129b4cc39d4f9219dd9e75834a90\/tts\/core\/redux.py#lines-60\n<\/span><span>\n<\/span><span style=\"color:#b48ead;\">class <\/span><span style=\"color:#ebcb8b;\">Store<\/span><span style=\"color:#eff1f5;\">(<\/span><span style=\"color:#a3be8c;\">_Qt.QObject<\/span><span style=\"color:#eff1f5;\">):\n<\/span><span>\tnewstate = <\/span><span style=\"color:#bf616a;\">_signal<\/span><span>(object)\n<\/span><span>\n<\/span><span>\t<\/span><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#96b5b4;\">__init__<\/span><span>(<\/span><span style=\"color:#bf616a;\">self<\/span><span>, <\/span><span style=\"color:#bf616a;\">reducer<\/span><span>, *<\/span><span style=\"color:#bf616a;\">args<\/span><span>):\n<\/span><span>\t\t<\/span><span style=\"color:#96b5b4;\">super<\/span><span>(Store, <\/span><span style=\"color:#bf616a;\">self<\/span><span>).<\/span><span style=\"color:#96b5b4;\">__init__<\/span><span>(*args)\n<\/span><span>\t\t<\/span><span style=\"color:#bf616a;\">self<\/span><span>.__reducer = reducer\n<\/span><span>\t\t<\/span><span style=\"color:#bf616a;\">self<\/span><span>.__state = <\/span><span style=\"color:#d08770;\">None\n<\/span><span>\n<\/span><span>\t\t<\/span><span style=\"color:#bf616a;\">self<\/span><span>.__dispatcher = <\/span><span style=\"color:#b48ead;\">lambda <\/span><span style=\"color:#bf616a;\">action<\/span><span>: <\/span><span style=\"color:#bf616a;\">self<\/span><span>.<\/span><span style=\"color:#bf616a;\">__single_dispatch<\/span><span>(action)\n<\/span><span>\n<\/span><span>\t\t<\/span><span style=\"color:#65737e;\"># ensure we have a default state\n<\/span><span>\t\t<\/span><span style=\"color:#bf616a;\">self<\/span><span>.<\/span><span style=\"color:#bf616a;\">dispatch<\/span><span>({ &#39;<\/span><span style=\"color:#a3be8c;\">type<\/span><span>&#39;: <\/span><span style=\"color:#d08770;\">None <\/span><span>})\n<\/span><span>\n<\/span><span>\t<\/span><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#8fa1b3;\">apply_middleware<\/span><span>(<\/span><span style=\"color:#bf616a;\">self<\/span><span>, <\/span><span style=\"color:#bf616a;\">middlewares<\/span><span>):\n<\/span><span>\t\td = <\/span><span style=\"color:#bf616a;\">self<\/span><span>.__dispatcher\n<\/span><span>\t\t<\/span><span style=\"color:#b48ead;\">for <\/span><span>m <\/span><span style=\"color:#b48ead;\">in <\/span><span style=\"color:#96b5b4;\">reversed<\/span><span>(middlewares):\n<\/span><span>\t\t\td = <\/span><span style=\"color:#bf616a;\">m<\/span><span>(<\/span><span style=\"color:#bf616a;\">self<\/span><span>)(d)\n<\/span><span>\t\t<\/span><span style=\"color:#bf616a;\">self<\/span><span>.__dispatcher = d\n<\/span><span>\n<\/span><span>\t@<\/span><span style=\"color:#96b5b4;\">property\n<\/span><span>\t<\/span><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#8fa1b3;\">dispatch<\/span><span>(<\/span><span style=\"color:#bf616a;\">self<\/span><span>):\n<\/span><span>\t\t<\/span><span style=\"color:#b48ead;\">return <\/span><span style=\"color:#bf616a;\">self<\/span><span>.__dispatcher\n<\/span><span>\n<\/span><span>\t<\/span><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#8fa1b3;\">__single_dispatch<\/span><span>(<\/span><span style=\"color:#bf616a;\">self<\/span><span>, <\/span><span style=\"color:#bf616a;\">action<\/span><span>):\n<\/span><span>\t\t<\/span><span style=\"color:#bf616a;\">self<\/span><span>.__state = <\/span><span style=\"color:#bf616a;\">self<\/span><span>.<\/span><span style=\"color:#bf616a;\">__reducer<\/span><span>(action, <\/span><span style=\"color:#bf616a;\">self<\/span><span>.__state)\n<\/span><span>\t\t<\/span><span style=\"color:#bf616a;\">self<\/span><span>.newstate.<\/span><span style=\"color:#bf616a;\">emit<\/span><span>(<\/span><span style=\"color:#bf616a;\">self<\/span><span>.__state)\n<\/span><span>\n<\/span><span>\t<\/span><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#8fa1b3;\">subscribe<\/span><span>(<\/span><span style=\"color:#bf616a;\">self<\/span><span>, <\/span><span style=\"color:#bf616a;\">slot<\/span><span>):\n<\/span><span>\t\t<\/span><span style=\"color:#bf616a;\">self<\/span><span>.newstate.<\/span><span style=\"color:#bf616a;\">connect<\/span><span>(slot)\n<\/span><span>\n<\/span><span>\t<\/span><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#8fa1b3;\">getState<\/span><span>(<\/span><span style=\"color:#bf616a;\">self<\/span><span>):\n<\/span><span>\t\t<\/span><span style=\"color:#b48ead;\">return <\/span><span style=\"color:#bf616a;\">self<\/span><span>.__state\n<\/span><\/code><\/pre>\n<p>I am certain that my implementation is not as thorough as the canonical javascript\none, but it uses the same principles and works well enough for this application.\nHere's an example usage:<\/p>\n<pre data-lang=\"python\" style=\"background-color:#2b303b;color:#c0c5ce;\" class=\"language-python \"><code class=\"language-python\" data-lang=\"python\"><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#8fa1b3;\">reducer<\/span><span>(<\/span><span style=\"color:#bf616a;\">action<\/span><span>, <\/span><span style=\"color:#bf616a;\">state<\/span><span>=<\/span><span style=\"color:#d08770;\">None<\/span><span>):\n<\/span><span>\tstate = state or {}\n<\/span><span>\t<\/span><span style=\"color:#b48ead;\">if <\/span><span>action[&#39;<\/span><span style=\"color:#a3be8c;\">type<\/span><span>&#39;] == &#39;<\/span><span style=\"color:#a3be8c;\">SET_FOO<\/span><span>&#39;:\n<\/span><span>\t  state[&#39;<\/span><span style=\"color:#a3be8c;\">foo<\/span><span>&#39;] = action[&#39;<\/span><span style=\"color:#a3be8c;\">value<\/span><span>&#39;]\n<\/span><span>\t<\/span><span style=\"color:#b48ead;\">return <\/span><span>state\n<\/span><span>\n<\/span><span>store = <\/span><span style=\"color:#bf616a;\">Store<\/span><span>(reducer)\n<\/span><span>\n<\/span><span>@<\/span><span style=\"color:#bf616a;\">_slot<\/span><span>()\n<\/span><span style=\"color:#b48ead;\">def <\/span><span style=\"color:#8fa1b3;\">state_handler<\/span><span>(<\/span><span style=\"color:#bf616a;\">state<\/span><span>):\n<\/span><span>\t<\/span><span style=\"color:#96b5b4;\">print<\/span><span>(&#39;<\/span><span style=\"color:#a3be8c;\">foo is: <\/span><span style=\"color:#d08770;\">%r<\/span><span>&#39; % (state.<\/span><span style=\"color:#bf616a;\">get<\/span><span>(&#39;<\/span><span style=\"color:#a3be8c;\">foo<\/span><span>&#39;),))\n<\/span><span>\n<\/span><span>store.<\/span><span style=\"color:#bf616a;\">subscribe<\/span><span>(state_handler)\n<\/span><span>\n<\/span><span>store.<\/span><span style=\"color:#bf616a;\">dispatch<\/span><span>({ &#39;<\/span><span style=\"color:#a3be8c;\">type<\/span><span>&#39;: &#39;<\/span><span style=\"color:#a3be8c;\">SET_FOO<\/span><span>&#39;, &#39;<\/span><span style=\"color:#a3be8c;\">value<\/span><span>&#39;: &#39;<\/span><span style=\"color:#a3be8c;\">bar<\/span><span>&#39; })\n<\/span><\/code><\/pre>\n<h4 id=\"hardware-device-interfaces\">Hardware device interfaces<\/h4>\n<p>I didn't want to get too far down into the nuts and bolts of IO devices, so I\nopted to make use of an existing library, <a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/wiringpi.com\/\">WiringPi<\/a>. There\nis a pretty good Python binding for it, and also includes an interface for the\nMCP23017 and LCD, so I only really had to make quite thin wrapper classes for the\ndevices I wanted to use. The implementation for the MAX7219 is a bit more manual,\nwhere I am 'bit-banging' the serial protocol myself.<\/p>\n<ul>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch\/src\/08ee4e427055129b4cc39d4f9219dd9e75834a90\/tts\/ui\/hardware\/devices.py#lines-151\"><code>class MCP23017()<\/code><\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch\/src\/08ee4e427055129b4cc39d4f9219dd9e75834a90\/tts\/ui\/hardware\/devices.py#lines-317\"><code>class RGBCharLCD()<\/code><\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch\/src\/08ee4e427055129b4cc39d4f9219dd9e75834a90\/tts\/ui\/hardware\/devices.py#lines-388\"><code>class MAX7219()<\/code><\/a><\/li>\n<\/ul>\n<p>I wanted to make use of the device interrupts on the MCP23017, so that I could\nefficiently detect button pushes. Unfortunately, the particular combination of\nWiringPi interrupt handling code bound through Python running a Qt event loop\nmake this more or less impossible to get working. The obvious way of hooking this\nup ended up <code>SEGFAULT<\/code>ing Python :(<\/p>\n<p>I resorted in this instance to polling the chip's port state and using the\nsoftware to report when the state changes; hence\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch\/src\/08ee4e427055129b4cc39d4f9219dd9e75834a90\/tts\/ui\/hardware\/devices.py#lines-114\">StateScanner<\/a>.\nThe StateScanner is actually generic, it can periodically call any function to get\na value to detect for changes. It is all a bit inefficient still, but the Python\nversion of the application is still somewhat in the territory of being a prototype.\nIt is good enough to prove that the electronics is wired up correctly and watch\nthe device work.<\/p>\n<h3 id=\"repo\">Repo<\/h3>\n<p>The code for this version of the software can be found here:<\/p>\n<blockquote style=\"text-align: center;\">\n<a href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch\/src\/master\/\" target=\"_blank\">bitbucket\/doughammond\/the-task-switch<\/a>\n<\/blockquote>\n<h2 id=\"building-the-electronics\">Building the Electronics<\/h2>\n<p>The first step into the real world involved figuring out how the components work\nand how to interface them. Bring out the breadboard!<\/p>\n<p><em>(Apologies that this section reads a bit like a holiday photo album, but\nfortunately I did take quite a lot of photos of various stages of the build, and\nI think that they mostly speak for themselves).<\/em><\/p>\n<p>Before that though, I had to decide which GPIO pins on the Raspberry Pi I can use\nfor each device. I used <a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/kicad-pcb.org\/\">KiCad<\/a> to make a\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch\/src\/master\/hardware-dwgs\/circuit-board\/\">schematic<\/a>.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;circuit-board.7646d9c14c9d9db9.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Knowing that I may have to change a few things around as I went along (that and\nthe fact that I cannot manufacture PCBs), I went with a\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/en.wikipedia.org\/wiki\/Wire_wrap\">wire-wrap<\/a> approach to connecting things\ntogether. This is a somewhat old technology for semi-prototyping connections,\nand it also so happens that I have a large stock of old wire-wrap wire and the\nright tools to use it.<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180320_230843.688682f460271233.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180321_221445.777919dfd73b43f4.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>Eventually I had most of the system hooked up on the breadboard. It stayed this\nway for some time whilst I wrote the software View to drive it, there were quite\na few iterations of device drivers and interfaces before I settled on something\nwhich seemed to be working.<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180619_212652.59a79a09f3867c5b.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180321_230550.4e5574006ac20be2.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>Before long though, the components had to move on to a \"real\" board. More\nwire-wrap. This was the first \"layer\" applied to hook up the LED driver chip:<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180408_175217.8c1c5d6284daba1c.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180408_175223.a8a060dec9e10de6.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>And after a couple more \"layers\" and with with with the LCD attached. If I were\nto do this again I would perhaps not permanently attach the LCD wires to the board\nbut use a connector like I did for the other peripherals.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180408_230457_1.90270da1d899a3b4.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I actually got quite a way along the hardware build before I decided to drop\nthe keypad feature. It was only really at this point that I could really tell\nthat it would be both cumbersome to mount properly and that it was not worth the\neffort. At this stage I also thought the Pi would be mounted on the board, that\nturned out to be impossible if I wanted to have the minimum size cubic box, though\nmostly because the USB lead connecting to the Pi would collide with the side. The\nPi itself would fit on the board within a 114mm square.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180414_180654.8aec4119d200474c.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>Connecting up the first illuminated button:<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180414_180701.ecb3ab7dad45392b.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180414_180709.8828ca49fe7e727f.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p><a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/hobbycomponents.com\/switches\/631-45mm-arcade-style-big-round-push-button-red\">The arcade button I sourced<\/a>\nhad originally just a plain white LED mounted inside. I knew my application\nrequired a colour changing status indicator, so I pulled the button apart and\nreplaced the LED with an RGB one. I removed also the metal contacts which hold\nin the original LED, and threaded the legs of the RGB LED down through the holes\nleft in the casing. The microswitch clipped into the bottom of the housing clamps\nthe LED legs in place. Here I was testing the same LED on the bench at\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/southlondonmakerspace.org\/\">South London Makerspace<\/a>:<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180426_215750.7719fbaa43d37489.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180426_215800.e94b5a5cf4ca98cf.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>By this point I had build the electronics and proved it worked. Time to box it\nup into a usable device.<\/p>\n<h2 id=\"building-the-box\">Building the Box<\/h2>\n<p>I started out with CAD of the front panel, by now trimmed down to the bare essentials:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;front-panel-small-01-e1529525237558.876b51f1b39270da.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I originally just left some space above the buttons to stick on some printed\nlabels. I later revised this idea and make the labels out of wood and fixed them\non with some cute shiny 8BA bolts.<\/p>\n<p>This set the minimum possible size at around 114mm square at the front. My next\nthought was to make the box a perfect cube, though that took quite some time and\na lot more drawing to figure out if that would even be possible. Turns out is was.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;front-panel-small-02-e1529525209420.b8bf9b61df5b1bcf.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I verified the CAD drawing dimensions by making an actual 3D model of the box\npanels and internal components. Yes, I did have to really measure all of the\ncomponents and the circuit board, and yes I left a little margin in the\nmeasurements to be better safe than sorry. The components were proven to fit:<\/p>\n<p>\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180619_212642.bdbd5a3d9c949bf6.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180619_212647.2242903e6e410e84.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;Screenshot_20180620_210345.7a87a5f4750d96f4.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<\/p>\n<p>The next step was to cut out the wooden panels somehow from the CAD drawings to\nmake a box. Being a recent new member of\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/southlondonmakerspace.org\/\">South London Makerspace<\/a> I gained access to\ntheir Trotec laser cutter, which makes light work of cutting out and putting\nholes in ply wood. It also can engrave, which was a handy solution to the button\nlabelling problem. I had to arrange some thin wooden strips inside the box to\nhold the sides together, and after some quite intricate glueing and clamping, I\nhave now a quite pretty little box.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180518_001111.a452ff3cebf483b2.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>I have to admit though that it is not perfect. The CAD drawings assumed the ply\nwood to be exactly 4mm, but in reality the material is a little thinner. I did\nnot know this before cutting commenced, so some of the sides do not meet exactly.\nI accept that as the first attempt at building something like this, I just will\nnot look too hard at the left side whilst using it :)<\/p>\n<p>After sanding the burn marks off, and oiling the wood in the sunshine, the box\nactually started to look quite attractive:<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180519_135143.147fc1be8a8cdadd.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<p>And finally with the components installed, except for the rubber feet on the bottom.\nNever mind the gap on the top left edge.<\/p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;IMG_20180519_174801.22a85ae680d62231.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h3 id=\"cad-files\">CAD Files<\/h3>\n<blockquote style=\"text-align: center;\">\n<a href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch\/src\/master\/hardware-dwgs\/\" target=\"_blank\">bitbucket\/doughammond\/the-task-switch\/src\/master\/hardware-dwgs<\/a>\n<\/blockquote>\n<h2 id=\"software-rewrite\">Software Rewrite<\/h2>\n<p>Now that I was ready to run the software with a full set of real hardware, I\nnaturally updated the Python\/PyQt code on to the Raspberry Pi and tried it out.\nI was rather disappointed, even more so than expected because I knew that the\nPython hardware driver code was somewhat inefficient.<\/p>\n<p>There was a massive latency between pushing a button and having the application\nrespond. Latency in the range of seconds. I tried quite a few tricks to try and\nget to the bottom of this, but eventually came to the conclusion that perhaps a\ntiny Raspberry Pi is not the best platform for running Python code, or that my\nchoice of using C\/C++ libraries via Python bindings in a single threaded runtime\nenvironment was just the wrong choice. I am not at all trying to say anything\nbad about Python though - it had served me very well in figuring out the prototype\nin a short space of time and to prove the worth of the project. It was just time\nto try something else.<\/p>\n<p>I decided to re-write the entire application software in C++ Qt.<\/p>\n<p>This way, I could very closely follow the Python implementation, but take\nadvantage of using Qt and WiringPi in their natural environments. It took a fair\nbit longer than expected, I was a bit rusty with C++ and there were some\nroad-blocks also in actually compiling the application for the Raspberry Pi.<\/p>\n<p>At first I was not satisfied with having to compile on the Pi, it really is not\nfast enough. Therefore, I had to get hold of a cross-compiling environment on my\nlaptop to speed up the process. Thankfully Qt Creator makes setting this up\nactually very easy from the IDE's point of view, but there was still some work\nto do to get a working compiler set up with a cross-built Qt and a basic sysroot\nfrom the Pi.<\/p>\n<p>Here's a bunch of resources I found useful:<\/p>\n<ul>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/medium.com\/@amirmann\/how-to-cross-compile-qt-for-raspberry-pi-3-on-linux-ubuntu-for-beginners-75acf2a078c\">Cross compiling Qt for Raspberry Pi<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.raspberrypi.org\/forums\/viewtopic.php?t=204529\">Cross-compilation guide for Qt 5.9.4 and RPi<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/thebugfreeblog.blogspot.com\/2013\/05\/cross-building-icu-for-applications-on.html\">Overcoming a compilation issue regarding ICU library<\/a><\/li>\n<\/ul>\n<h3 id=\"structure-1\">Structure<\/h3>\n<p>This version of the application became structured into 3 library and 2\napplication projects, tied together using a Qt <code>subdirs<\/code> master project;<\/p>\n<ul>\n<li><code>redux<\/code> : (Library) Another simple Redux implementation<\/li>\n<li><code>tts-core<\/code> : (Library) Contains much the same stuff as the Python implementation packages <code>tts.core<\/code> + <code>tts.net<\/code><\/li>\n<li><code>tts-devices<\/code> : (Library) Device driver classes<\/li>\n<li><code>tts-gui<\/code> : (App) Desktop UI View and application<\/li>\n<li><code>tts-pi<\/code> : (App) Hardware UI View and application<\/li>\n<\/ul>\n<p>I also wrote two small test projects to help get the implementations working:<\/p>\n<ul>\n<li><code>redux-test<\/code> : Test suite for <code>redux<\/code>, using QtTest<\/li>\n<li><code>devices-test<\/code> : Test suite for <code>tts-devices<\/code>, not really a test suite, but more of a harness to be able to call functions on the device driver classes<\/li>\n<\/ul>\n<p>This implementation does still support using more than one View class at a time\nwith the Controller, but I never yet actually had to. I used <code>tts-gui<\/code> to develop\nthe Controller implementation on the desktop and <code>tts-pi<\/code> could make use of that\nwhilst I developed the device drivers.<\/p>\n<h4 id=\"redux-again\">Redux again<\/h4>\n<p>The implementation of\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch-qt\/src\/master\/redux\/\">Redux in C++<\/a>\nended up a bit more verbose than in Python. Writing it again though was an\nexperience worth having, having a strong type system made me really think about\ninterfaces and how to make the Store, Dispatchers (middleware) and Reducers\ncomposable in a way that works.<\/p>\n<p>The <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch-qt\/src\/b484cd1867b1990e498a5054f26cf0f55704503e\/redux\/types.h#lines-16\">state of the application<\/a>,\nhowever, is implemented using <code>QVariantMap<\/code>. This is still a compromise in the\ndesign, I am not a big fan of this data type, but I concede that it does work.<\/p>\n<h4 id=\"hardware-interfaces-again\">Hardware interfaces again<\/h4>\n<p>I'm still using WiringPi in this project, but this time it's C interface. Thus,\nthe device driver classes in C++ are much the same wrappers as I had in Python.<\/p>\n<p>One key difference is the interrupt handling. By this point I had actually\nrealised I can use a slightly different approach. I hooked up the MCP23017 INTA\npin to a Raspberry Pi GPIO and I instead poll that state instead of polling over\nthe I2C bus. Thus, the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch-qt\/src\/b484cd1867b1990e498a5054f26cf0f55704503e\/tts-devices\/tts-mcp23017.h#lines-14\">InterruptWorker<\/a>\nstill polls a state, but rather more quickly. I am still aware of a bug in this\nimplementation, but not exactly what the cause is. Sometimes, the code or the\ndevice does not read and reset the interrupt state and gets stuck. I\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch-qt\/src\/b484cd1867b1990e498a5054f26cf0f55704503e\/tts-devices\/tts-mcp23017.h#lines-37\">implemented a work-around<\/a>\nusing a timeout and reset condition to force this to happen if it detects it\ngets stuck.<\/p>\n<p>The end result though is much reduced input latency. The push buttons appear to\nrespond instantly to touch :)<\/p>\n<h3 id=\"repo-1\">Repo<\/h3>\n<p>This version of code can be found here:<\/p>\n<blockquote style=\"text-align: center;\">\n<a href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch-qt\/src\/master\/\" target=\"_blank\">bitbucket\/doughammond\/the-task-switch-qt<\/a>\n<\/blockquote>\n<h2 id=\"unexpected-issues\">Unexpected Issues<\/h2>\n<p>There are still a few bugs in the system that I know of, and probably a few more\nthat I am not.<\/p>\n<p>I notice that some of the push button LEDs (actually, mostly one in particular, <code>INTR<\/code>)\ncomes on and off and flashes randomly. I don't know the exact cause of this yet,\nbut must be something to do with the MAX7219 since there is no activity in the\nsoftware when it happens. There is probably a floating logic line or noise on\nthe serial bus perhaps.<\/p>\n<p>I've also noticed that some of the\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch-qt\/src\/b484cd1867b1990e498a5054f26cf0f55704503e\/tts-core\/ttscontroller.cpp#lines-595\">\"temporary status\"<\/a>\nmessages, particularly those shown during the web sync process do not display\nfor long enough. I suspect multiple status changes are happening within the status\ntimeout period, there is no code yet to queue these to allow the user to read\nevery status.<\/p>\n<h2 id=\"web-services\">Web services<\/h2>\n<p>Having the application keep a record of activities locally in a YAML file is fun\nfor a short while, but once the device is all boxed up and working there is no\neasy way to extract it for processing and reporting. Also, why should I have to do that ?<\/p>\n<p>The Raspberry Pi Zero W was chosen as the platform for a good reason. \"W\" stands\nfor \"WiFi\". Once configured with access point details, the application can just\nupload the data itself to a web service.<\/p>\n<p>So, how did I go about doing this ? Bring out the Python again !<\/p>\n<p>The library of choice this time is <a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/flask.pocoo.org\/\">Flask<\/a>, a self-proclaimed\nmicro-framework for building web apps (and also APIs). Since I was to build an\nAPI only, I also made use of\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/flask-restful.readthedocs.io\/en\/latest\/\">Flask-RESTful<\/a> as the Resource interface.<\/p>\n<p>I still need some storage for the data, so bring in\n<a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/flask-sqlalchemy.pocoo.org\/2.3\/\">Flask-SQLAlchemy<\/a> and\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/flask-migrate.readthedocs.io\/en\/latest\/\">Flask-Migrate<\/a>. The API needs\nalso to be secured, so also bring in\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/pythonhosted.org\/Flask-Security\/\">Flask-Security<\/a>.<\/p>\n<p>Overall, I am impressed by the community of libraries surrounding Flask, however\nI'm not that impressed at the default way of structuring a Flask project. Almost\nevery example usage shows the entire application in one file, without any real\nseparation of concerns. Even if one is to try and split the project up into\nmodules, you'll also quickly run into a situation where you are required to use\ncircular imports. Quite a lot of fiddling is required to structure the project\ncorrectly and with a chain of dependencies\/imports which makes sense.<\/p>\n<p>Even after all of that, I still did not have Flask-SQLAlchemy and Flask-Migrate\nworking correctly together. There was some trickery with patching in something\ncalled a <code>metadata<\/code> object on the various interfaces which allow them to share\na confguration. That was really not intuitive at all.<\/p>\n<h3 id=\"data-model\">Data Model<\/h3>\n<p>The database only has a handful of tables. A couple for <code>user<\/code> and <code>role<\/code>\nrequired by the security implementation (which also, helpfully, enables a hosted\ninstance of the API and report to be eventually be useful to more than one\nperson at a time). The other 4 tables are:<\/p>\n<ul>\n<li><code>timesheet_item<\/code> : A time sheet line-item; in other words a single activity<\/li>\n<li><code>timesheet_item_category<\/code> : Normalised table for storing tags used by <code>timesheet_item<\/code>s<\/li>\n<li><code>timesheet_item_category_timesheet_item<\/code> : Many-many pivot table between <code>timesheet_item<\/code> and <code>timesheet_item_category<\/code><\/li>\n<li><code>timesheet<\/code> : A grouping of <code>timesheet_item<\/code>s into a daily \"time sheet\".<\/li>\n<\/ul>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;mysql-schema.81f20e49b0547ef2.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n<h3 id=\"rest-api\">REST API<\/h3>\n<p>Due to the relative simplicity of the above model, the REST API is correspondingly\nquite simple. All that is really required is to make a request to <code>PUT timesheet_item<\/code>\nand the code will:<\/p>\n<ul>\n<li>Check if the item being sent already exists and re-use or update it and the\nrelated categories as necessary,<\/li>\n<li>Generate or re-use an existing <code>timesheet<\/code> group for it<\/li>\n<\/ul>\n<p>And that it about it. There are a couple of <code>GET<\/code> methods to return your data for\nthe purposes of the Reporting application.<\/p>\n<h3 id=\"repo-2\">Repo<\/h3>\n<blockquote style=\"text-align: center;\">\n<a href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch-db\/src\/master\/\" target=\"_blank\">bitbucket\/doughammond\/the-task-switch-db<\/a>\n<\/blockquote>\n<h2 id=\"reporting\">Reporting<\/h2>\n<p>The Reporting app is actually the first part of this endeavour that I created.\nIt was originally built to analyse my manually input YAML files. It is built using\n<a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/reactjs.org\/\">React<\/a> and <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/redux.js.org\/\">Redux<\/a>.<\/p>\n<p>The original structure of the YAML data was a little simpler than the current data\nmodel. Each timesheet item originally only had one category, however I extended\nthat to multiple. Because of this, the report app now is still incomplete because\nI need to change some of the aggregation and widgets to support multiple categories.\nAs a result of this, there is also some legacy in the data model throughout this\nproject which can eventually be cleaned up (e.g. timesheet item description).<\/p>\n<p>The application does work though to produce a pie chart and daily breakdown of\nEisenhower Quadrant percentages. The detailed timesheet listing also works fine.<\/p>\n<p>I would like to replace the initial pivot table with something a bit more time-line\nbased, which can show the individual tag active periods over time, probably with\ncolour coding for the Q numbers.<\/p>\n<h3 id=\"repo-3\">Repo<\/h3>\n<blockquote style=\"text-align: center;\">\n<a href=\"https:\/\/bitbucket.org\/doughammond\/timesheet-report\/src\/master\/\" target=\"_blank\">bitbucket\/doughammond\/timesheet-report<\/a>\n<\/blockquote>\n<h2 id=\"finished-project\">Finished Project<\/h2>\n<p>So, we've arrived at the bottom of the page, I suppose that warrants a conclusion\nof some sort.<\/p>\n<p>I'm considering this project more or less done for me for now. I will aim to fix\nup the Report a little, but I personally don't require much more.<\/p>\n<p>I learned a fair bit in the process, mostly in the world of the hardware, which\nis what I set out to achieve. Particularly with regards to doing CAD for manufacture,\neven if that manufacture is of a somewhat simple automated laser-cutting variety.\nI may revisit the box at a later date as well, to fix a few issues:<\/p>\n<ol>\n<li>The current box fits together properly, but is not exactly a neat job<\/li>\n<li>The LCD viewing angle is not correct when the device is on my desk. It would\nbe better to be tilted upwards nearer 45 degrees.<\/li>\n<li>The top panel of the box is a different wood &amp; colour than the rest. Though\nyou probably wouldn't have noticed if I had not mentioned it, it makes the\nbox just a bit more less perfect than it could be.<\/li>\n<\/ol>\n<p>At some point it might also be fun to make a proper PCB for the board, instead\nof using wire-wrap. That might be the cause of the random-flashing-bug if I'm\nperfectly honest.<\/p>\n<h2 id=\"it-s-open-source\">It's Open Source !<\/h2>\n<p>If you think you'd like a toy like this one, then you're in luck. All of the code\nand design material is open source. You can clone, copy, adapt and even submit\npatches back to me if you wish.<\/p>\n<ul>\n<li>Python software implementation <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch\/src\">the-task-switch<\/a>\n<ul>\n<li>(This also contains the CAD drawings and schematics.)<\/li>\n<\/ul>\n<\/li>\n<li>Qt software implementation <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch-qt\/src\">the-task-switch-qt<\/a><\/li>\n<li>Web service <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/the-task-switch-db\/src\">the-task-switch-db<\/a><\/li>\n<li>Report application <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/bitbucket.org\/doughammond\/timesheet-report\/src\/master\/\">timesheet-report<\/a><\/li>\n<\/ul>\n<h2 id=\"acknowledgements\">Acknowledgements<\/h2>\n<p>I couldn't have done all of this so quickly without all of the below. Thanks all :)<\/p>\n<ul>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.qt.io\/\">Qt<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/reactjs.org\/\">React<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/redux.js.org\/\">Redux<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.python.org\/\">Python<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/flask.pocoo.org\/\">Flask<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/flask-restful.readthedocs.io\/en\/latest\/\">Flask-RESTful<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/flask-sqlalchemy.pocoo.org\/2.3\/\">Flask-SQLAlchemy<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/flask-migrate.readthedocs.io\/en\/latest\/\">Flask-Migrate<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/pythonhosted.org\/Flask-Security\/\">Flask-Security<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.raspberrypi.org\/\">Raspberry Pi<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/wiringpi.com\/\">WiringPi<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"http:\/\/kicad-pcb.org\/\">KiCad<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/www.3ds.com\/products-services\/draftsight-cad-software\/\">DraftSight<\/a><\/li>\n<li><a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/southlondonmakerspace.org\/\">South London Makerspace<\/a><\/li>\n<\/ul>\n"},{"title":"Digital Aquarium Thermometer","published":"2016-04-03T00:00:00+00:00","updated":"2016-04-03T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2016\/digital-aquarium-thermometer\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2016\/digital-aquarium-thermometer\/","content":"<p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20150926_161013-1024x576.62c457f214bd958b.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>I really enjoy keeping fish as pets. At current count we have ten in two aquariums. Shown above is Ziggy, our largest fantail, and a few of our smaller (and faster moving) ones.<\/p>\n<p>Unfortunately, over the years we have also lost a few - and when this happens, one is left wanting to know exactly why this happens. Particularly frustrating is when a fish is lost, yet the others in the same aquarium appear completely unaffected.<\/p>\n<p>The obvious thing to do, though is to ensure that one knows the full details of the conditions in which the fish are living. There are various standard chemical tests that can be done, and at times I've been regularly testing the water for ammonia, nitrates and nitrites to ensure that the fish are not living in a poisonous environment. These chemical tests are however completely 'analogue' and have to be done by hand. There are (very) expensive digital versions of some of these tests, though the probes and sensors are not really worth using on small domestic aquariums.<\/p>\n<p>An obvious and easy thing to keep an eye on is the water temperature. The type of fish we keep are relatively tolerant to a range of temperatures, however any sudden changes can be harmful.<\/p>\n<p>To this end, I went and bought a DS18B20 one-wire digital temperature sensor. My initial thought was to hook it up to a Raspberry Pi, and have the temperature logged for viewing on something like a web dashboard. I had this up and running for a while, using <a href=\"http:\/\/rpi-experiences.blogspot.co.uk\/p\/rpi-monitor.html\" target=\"_blank\">RPiMonitor<\/a>. The issue with this was that the network connection to the Raspberry Pi over WiFi proved unreliable, and even when it was working, I had to use another device even to see the current temperature reading. The benefit though was having a full time-series history of the aquarium temperature.<\/p>\n<p>More useful would be to have an immediate live view of the current temperature, and whether that temperature has changed rapidly recently.<\/p>\n<p>It was then that I remembered that I had an FRDM-KL25Z MCU board and an Adafruit RGB LCD shield both sitting in a drawer, just waiting for an application. I acquired these boards as part of my on-going modular synth project, hoping that they'd be useful as a PC or digital interface to the instrument. Realistically, the KL25Z is unsuitable for real-time audio processing. The LCD could still be useful, but the RGB backlight gave me an idea...<\/p>\n<p>To pull off this idea, I would have to get the LCD working with the KL25Z, a task that I had previously failed. This time around, I went to the <a href=\"https:\/\/developer.mbed.org\/compiler\" target=\"_blank\">mbed online compiler<\/a> and started looking for libraries that other people had already written for working with the LCD (and, while I was at it, the DS18B20). I got lucky, and found a few libraries that did in fact work.<\/p>\n<p>Now, all I needed to do was clean the code up a bit, and invent a <code>main()<\/code> loop that does what I want. With limited RAM, and no persistent storage at all on the KL25Z I needed to figure out how long a time-series I can keep active for the purposes of averaging the temperature readings.<\/p>\n<p>Just for fun, I also wanted to program a few custom graphics for the LCD and make a little swimming fishy animation to show that the unit is in fact still alive and recording the temperature.<\/p>\n<p>The <code>main()<\/code> loop therefore does this:<\/p>\n<ul>\n<li>Maintain a counter for the current position of the fish animating across the top row of the screen<\/li>\n<li>Draw the fish graphic at the appropriate location<\/li>\n<li>As the fish exits the right-hand side of the screen, take a temperature reading<\/li>\n<li>Store the temperature reading in a circular buffer<\/li>\n<li>Take the average of the values in the circular buffer<\/li>\n<li>Display the current reading and the average on the bottom row of the screen<\/li>\n<li>Change the back-light colour to reflect the relationship between the current reading and the average:\n<ul>\n<li>If they are within 1 degree celsius, display green (safe)<\/li>\n<li>Else, if they are within 2.5 degrees celsius:\n<ul>\n<li>If current &gt; average, display magenta (warm)<\/li>\n<li>If current &lt; average, display cyan (cool)<\/li>\n<\/ul>\n<\/li>\n<li>Else, if the difference is greater than 2.5 degrees celsius:\n<ul>\n<li>If current &gt; average, display red (hot)<\/li>\n<li>If current &lt; average, display blue (cold)<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>By making use of the LED colour like this, I can actually see at a glance if there's anything to even worry about on the temperature front without even having to read the temperature numbers.<\/p>\n<p>\n\n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160324_001124.3995889ef429d9e2.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>The whole thing is powered via a USB cable, and it fits snugly in the space between the back of the large aquarium and the wall. Thankfully, the aquarium being made of glass, allows the screen to shine right through. Above, the display is showing a 25 hour average of 21.2 degrees celsius and a current reading of 20.2 degrees celsius. Since this is not more than one degree from the average, the light is showing a nice safe green colour.<\/p>\n<p>During the first few days of operation, I did notice the magenta colour a couple of times, but recently the temperature is largely stable and green.<\/p>\n<p>All of the code is on my bitbucket: <a href=\"https:\/\/bitbucket.org\/doughammond\/fishy-monitor\" target=\"_blank\">fishy-monitor<\/a> - and this does also include all of the library code written by other people.<\/p>\n"},{"title":"Tascam DM24 Screen Repair","published":"2016-03-28T00:00:00+00:00","updated":"2016-03-28T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2016\/tascam-dm24-screen-repair\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2016\/tascam-dm24-screen-repair\/","content":"<h2>Background<\/h2>\n<p>\n  The DM24 is notorious for having its LCD screen fail after a few years use.\n  Apparently this is due to inadequate thermal design around the screen housing,\n  so the screen ends up exposed to higher than optimal temperatures which after\n  a while causes the LCD to degrade. The end result is that the screen ends up\n  unreadable due to either permanently on or off lines running across the\n  screen. Over time, more and more lines will fail.\n<\/p>\n<h2>Solutions<\/h2>\n<p>\n  Up until a few years ago it was still possible to get an OEM replacement\n  screen. This would have been the optimal solution for the problem, albeit also\n  rather costly. Tascam wanted upwards of $200 for the part alone. Taking the\n  unit to a repair centre to have them do the work for you could easily double\n  or triple that cost (for reasons that I now completely understand).\n<\/p>\n<p>\n  Unfortunately though, it appears that original parts are no longer available.\n  For several years, I left my DM24 in a mostly unusable state due to this\n  situation.\n<\/p>\n<p>\n  However, thanks to a\n  <a\n    href=\"http:\/\/www.tascamforums.com\/threads\/dm24-screen-repair.10\"\n    target=\"_blank\"\n    >tascamforums thread<\/a\n  >\u00a0that I started several years ago, some helpful souls have managed to\n  identify some solutions:\n<\/p>\n<p>1. Dismantle and clean and repair the screen unit yourself<\/p>\n<p>\n  At first, with not a lot to lose, I chose this approach. I removed the LCD\n  assembly from the mixer, and took it apart. I cleaned down all of the\n  rubber-glass contacts and tried to re-assemble the best I could. The end\n  result was that the screen became <em>worse<\/em> that it was before. I\n  repeated the procedure a couple of times, but I could never get it to improve.\n  At this point, the mixer was largely unusable unless you were expert in using\n  the buttons + knobs to navigate around without reference to the screen itself.\n  I could have managed that, but the screen interface makes life so much easier.\n<\/p>\n<p>2. Find an alternative compatible screen unit<\/p>\n<p>\n  Compatible parts are apparently available at an affordable price from Chinese\n  suppliers on eBay. The part number is something like \"MG3224C3-SBF PC3224C3-2\n  STN\" - though you'd better either research exactly which variant on this\n  screen you need, or trust advice and links given by people on the\n  aforementioned tascamforums thread. I'll explain why below.\n<\/p>\n<h2>Buying the replacement screen unit<\/h2>\n<p>\n  I didn't use the same supplier as mentioned on the tascamforums thread. He was\n  out of stock at the point I wanted to order. I should have waited, or at least\n  contacted the supplier to try and find out more. We'll find out why below.\n<\/p>\n<p>\n  Instead, I went and bought a \"5.7\" SNT MG3224C3-SBF PC3224C3-2\" from some\n  other supplier. That's the same thing, right? I figured it was worth a punt\n  for about $60 including shipping.\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_142316.4ddd7c8864277324.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  At this point, I was already aware that I might have to modify or change a\n  component on the screen's PCB in order to get an adequate contrast on the\n  display.\n<\/p>\n<h2>The replacement process<\/h2>\n<h3>Opening the mixer<\/h3>\n<p>\n  Due to my previous repair attempts, I'm no stranger to taking the DM24 apart.\n  There's about 15-20 screws around the outside of the case, then the whole\n  thing opens up like a clam-shell. I usually put something soft on top of my\n  workbench (a multiple folded blanket will do), then place the mixer upside\n  down on it (the blanket prevents the faders and knobs etc getting damaged\n  through contact with the hard bench surface). The bottom of the mixer then\n  opens upwards, so that I can work on the inside of the top of the mixer\n  horizontally.\n<\/p>\n<p>\n  The large PCB on the inside of the top needs to come more or less out. There\n  is just one cable attached which cannot be removed, else I would have taken it\n  out completely. In its removed state, the buttons keep falling off if you\n  touch it at all. It's a bit annoying, but try to park it out of the way and\n  avoid touching it.\n<\/p>\n<p>\n  This time, I also unscrewed the screen housing casing, as I knew I might need\n  to partially re-assemble the mixer for testing the screen, and I thought it\n  easier to leave a hole in the top casing so that I could get easy access to\n  hooking up the screen to test it.\n<\/p>\n<h3>Getting the new screen in<\/h3>\n<p>\n  My first goal was to simply hook up the new screen and see if it works at all.\n<\/p>\n<p>\n  <strong\n    >Problem #1 - the screen's flat data ribbon cable is way too short.<\/strong\n  >\n<\/p>\n<p>\n  The cable on the new screen is soldered directly to the PCB. The cable on the\n  old screen is detachable at both ends. A simple solution comes to mind; I just\n  need to attach the old cable to the end of the new, to get a combined cable\n  which is even longer than the original. That would certainly facilitate having\n  to test the screen with it hanging outside the front of the mixer.\n<\/p>\n<p>\n  This proved to be way more tricky than you might think. The contacts on the\n  ends of the cables are basically just thin metal foil, and it also seems that\n  solder does not readily take to it. In trying to do so, the plastic ribbon\n  starts to melt and burn. In the melting process the plastic also contracts,\n  meaning that the metal contacts get closer together.\n<\/p>\n<p>\n  I tried several ways to strip back the plastic; scalpel, burning, fine metal\n  file. During the course of these trials, I ended up losing about 30mm off the\n  end of the old cable as I had to cut off the failed attempts to expose the\n  contacts.\n<\/p>\n<p>\n  The best success came from using the file to wear away the plastic and expose\n  the metal foil contacts. However, this did leave plastic ribbon in place\n  between the contacts which would later cause a bit of an issue due to\n  shrinkage and soldering surface contamination.\n<\/p>\n<p>\n  I ended up getting the contact surfaces tinned, but a bit heavy handedly. Upon\n  trying to solder the contact pairs together, the surplus solder was squeezed\n  out and shorted many of them together. Also, mixing with the surplus plastic,\n  the contacts got closer together and mis-aligned, causing most of the contacts\n  to be shorted together. I knew this by testing with a multimeter, and then by\n  inspection under a microscope.\n<\/p>\n<p>\n  I made a better graft by using solder wick to remove the excess solder, and\n  then repeating the soldering process under the microscope with soldering iron\n  in one hand and the scalpel in the other to push, pull and hold each\n  individual contact in the required location. In this process I also used the\n  scalpel to scrape away the excess melted plastic and other surface\n  contamination.\n<\/p>\n<p>\n  Finally, the ribbon cable was grafted, and tested OK, and I wrapped it all up\n  with some electrical tape.\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_175245.110356c58efec454.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<h3>Testing the new screen<\/h3>\n<p><strong>Problem #2 - the screen's display contrast<\/strong><\/p>\n<p>\n  With a data cable that is long enough, I lashed up the screen and powered up\n  the mixer. At this point I hadn't considered hooking up the back-light power,\n  more on that later.\n<\/p>\n<p>\n  When powered on, even without back-lighting, it was obvious the screen was\n  lacking contrast. It looks like I would have to examine the resistors on the\n  PCB and attempt a modification there.\n<\/p>\n<p>\n  I removed the new screen and sat down to compare the components on the back of\n  the PCB with the old one. I was trying to figure out the similarities and\n  differences, and whether I would need to replace the \"R3\" resistor - or\n  indeed, if the resistors marked \"R3\" on each board even perform the same\n  function.\n<\/p>\n<p>Here's the table of resistor values surrounding the \"LA6324N\" chip:<\/p>\n<table width=\"100%\">\n  <tbody>\n    <tr>\n      <th>Resistor Number<\/th>\n      <th>Old screen value<\/th>\n      <th>New screen value<\/th>\n    <\/tr>\n    <tr>\n      <td>R1<\/td>\n      <td>2k2 (222)<\/td>\n      <td>2k (202)<\/td>\n    <\/tr>\n    <tr>\n      <td>R2<\/td>\n      <td>2k2 (222)<\/td>\n      <td>2k (202)<\/td>\n    <\/tr>\n    <tr>\n      <td>R3<\/td>\n      <td>27k (273)<\/td>\n      <td>20k (203)<\/td>\n    <\/tr>\n    <tr>\n      <td>R4<\/td>\n      <td>2k2 (222)<\/td>\n      <td>2k (202)<\/td>\n    <\/tr>\n    <tr>\n      <td>R5<\/td>\n      <td>2k2 (222)<\/td>\n      <td>2k (202)<\/td>\n    <\/tr>\n    <tr>\n      <td>R6<\/td>\n      <td>0 (000)<\/td>\n      <td>15 (150)<\/td>\n    <\/tr>\n    <tr>\n      <td>R7<\/td>\n      <td>0 (000)<\/td>\n      <td>15 (150)<\/td>\n    <\/tr>\n    <tr>\n      <td>R8<\/td>\n      <td>0 (000)<\/td>\n      <td>15 (150)<\/td>\n    <\/tr>\n    <tr>\n      <td>R9<\/td>\n      <td>0 (000)<\/td>\n      <td>15 (150)<\/td>\n    <\/tr>\n  <\/tbody>\n<\/table>\n<p>\n  It seems like we're in luck. The numbering is consistent! The resistors with\n  the same number have similar values on both screens.\n<\/p>\n<p>\n  Again soldering under the microscope I removed the R3 from the old PCB and the\n  new, then soldered in the old 273 R3 onto the new board.\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_173815-e1459185379146.9fb2ecadd0a3849c.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_173735-e1459185356689.d56f8b63b6761851.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  This leaves me with the old screen missing a resistor, and a spare 202 SMD\n  resistor. Both of these will likely end up in the bin at some point.\n<\/p>\n<h3>The back-light power<\/h3>\n<p>\n  Next step was to test the back-light. I quickly discovered that the new screen\n  had the wrong type of molex connector fitted. Never mind, using a small blade\n  I could remove the pins from the connector housing and swap them over.\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_174528-e1459185680472.4c9f116e8545b548.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  Unfortunately the crimped pin \"sockets\" on the new screen are of a slightly\n  larger type and kept falling out of the connector casing, but when plugged in\n  they would actually hold.\n<\/p>\n<h3>Testing the screen again<\/h3>\n<p>\n  This time on power up the screen looks perfect, and the back-light works\n  great.\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_174911.460eefa3cb0fae85.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<h3>Re-assembly<\/h3>\n<p>\n  Happy that the screen will function, all I need to do now is get the thing\n  fitted back in to the casing and put the mixer back together.\n<\/p>\n<p>\n  Oh dear. The mounting holes are in the wrong place. Not only that, but it\n  doesn't seem to fit in the screen housing either.\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_175933.9911ea927f914f55.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_180203.6a81a67bd3f022e7.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  However, by putting in the two screws that I can, and picking up the assembly,\n  the slight flex in the plastic actually let the screen drop in\n  <em>almost<\/em>\u00a0all the way in. There is no way that I can get the other two\n  screws in.\u00a0However, despite the fact that the screen is not quite flat, the\n  appearance from the outside is actually completely acceptable.\n<\/p>\n<p>Next problem - the damn back-light power wires are way too short.<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_180843.57c51bf9938c33ab.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  OK, easy solution here. I'll cut the wires off the old screen. This way I can\n  extend the wires and use the original connector assembly. A quick graft later;\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_182411-e1459185790659.b3e9faaeae623f60.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_180911-e1459186002310.2cb71b3e71fe7a48.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  30 or so screws later and the mixer is closed up and back in a nice working\n  state. The repair is not perfect, but it's good enough for me. No doubt the\n  repair will last long enough - until either this LCD fails again or something\n  else does.\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;20160326_184040.97ae17663664098b.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>:)<\/p>\n"},{"title":"Interrupt driven LCD buttons","published":"2014-01-18T00:00:00+00:00","updated":"2014-01-18T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2014\/interrupt-driven-lcd-buttons\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2014\/interrupt-driven-lcd-buttons\/","content":"<h2>What do I mean by 'interrupt driven' ?<\/h2>\n<p>\n  By design, the LCD board I have provides the state of the buttons to the\n  application only when the application asks for it. This means that the\n  application has to run a loop which continually asks the LCD board for which\n  buttons are pressed. In an interrupt-driven world, the application will sit\n  idle until a button is pressed, and then the LCD board will tell the\n  application what has happened.\n<\/p>\n<p>\n  An analogy might be wanting to know when a particular time of day occurs. You\n  could ring up the talking clock service every minute to check yourself. Or,\n  you could set an alarm clock and let it tell you when it's the time you\n  require, making you free to do other things in the mean time.\n<\/p>\n<h2>Why would I want 'interrupt driven' ?<\/h2>\n<p>\n  It should be pretty clear already that having the application continuously\n  polling the button state is wasteful of resources. Not only does it take up\n  quite a bit of CPU time for the software, but it also occupies a significant\n  portion of the available bandwidth on the i2c bus between the RPi and the LCD\n  board. This is a problem for my project, since I also want to share that bus\n  with one or more other devices which are more important than the LCD board and\n  which also need to operate in (near to) real-time.\n<\/p>\n<h2>How did I make it work ?<\/h2>\n<p>\n  The LCD board uses an i2c GPIO expander chip (an MCP23017) to interface\n  between the RPi and the LCD itself. The buttons are set up as GPIO inputs on\n  the expander chip. The board has been designed this way to minimise the number\n  of connections required to the RPi. The MCP23017 actually has an interrupt\n  facility for the GPIO inputs but due to the connections constraint they are\n  not used. However, there's nothing stopping us hooking it up. The MCP23017 can\n  be configured to raise an interrupt pin high when any of its GPIO inputs\n  change.\u00a0By connecting this signal to an RPi GPIO, we can detect when a button\n  has\u00a0been pressed.\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;wpid-wp-1390064840863.5feb3a5f951c52f2.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  In the above picture, I have connected the INTA pin from the MCP23017 to one\n  of the plate's unused pins; note that this pin I'm using was chosen simply as\n  being the closest to the INTA pin, no other reason. It also makes this plate\n  pin-<strong>in<\/strong>compatible with the Arduino it was designed for, but\n  that's fine by me since I plug it into a breadboard anyway:\n<\/p>\n<p>\n  \n<img\n  src=\"https:&#x2F;&#x2F;doug.lon.dev&#x2F;processed_images&#x2F;wpid-wp-1390065332700.cf529da110816303.webp\"\n  width=\"960\"\n  style=\"max-width: 99%; margin: 0; padding: 0\"\n\/>\n\n<\/p>\n<p>\n  Here's the complete hookup on the breadboard with the Pi cobbler cable going\n  to to the RPi. I've connected the INTA pin through a small circuit\n  <a href=\"http:\/\/tansi.info\/rp\/interfacing5v.html\" target=\"_blank\">described here<\/a> which\n  limits the 5V MCP23017 output to something safe for the RPi's 3V3 GPIO\n  inputs.\u00a0Here, I am using GPIO23. You can also see the i2c lines in orange at\n  the top, as well as the red\/black 5V supply. The blue and yellow wires are\n  hooked up to my oscilloscope so that I can check the GPIO voltages on the\n  interrupt line.\n<\/p>\n<p>\n  Now, even though I am claiming the buttons are now interrupt driven, this\n  still isn't strictly true unfortunately. I still have to keep asking the GPIO\n  interrupt signal if it is high, and then ask the MCP23017 why the interrupt\n  was raised. So why did I bother? The RPi can read the interrupt signal from\n  the GPIO pin much faster than it can read the button states from the MCP23017\n  over i2c. This means that I save i2c bandwidth for my DAC device(s).\n<\/p>\n<p>\n  Below is the code which I use to poll the GPIO interrupt signal and turn it\n  into qt signals:\n<\/p>\n<pre>\n\t\/**\n\t * @brief pollInterrupt; poll the GPIO pin to see if it is high\n\t * meaning that there is button information to be read from the\n\t * MCP23017\n\t *\/\n\tvoid pollInterrupt() {\n\t\tint btnIntr = m_pGPIO->digitalRead();\n\n\t\tif (btnIntr > 0) {\n\t\t\treadInterrupt();\n\t\t}\n\t}\n\n\t\/**\n\t * @brief readInterrupt; read the button interrupt registers from\n\t * the MCP23017 and emit appropriate button down\/up signals\n\t *\/\n\tvoid readInterrupt() {\n\t\tuint8_t _in[] = { 0x00, 0x00 };\n\t\tm_pI2C->receive(MCP23017::INTFA, _in, 1);\n\t\t_logI2CFail();\n\n\t\tm_pI2C->receive(MCP23017::INTCAPA, _in + 1, 1);\n\t\t_logI2CFail();\n\n\t\t\/\/ work out what changed\n\t\t\/\/ INTFA is the address of the button which caused the interrupt\n\t\t\/\/ INTCAPA is the bitmask of currently pressed buttons;\n\t\t\/\/ therefore logical AND of these two tells us the current state\n\t\t\/\/ of the button which caused the interrupt.\n\n\t\tif (_in[0] & _in[1]) {\n\t\t\t\/\/ button down event\n\t\t\temit buttonDown(_in[0]);\n\t\t} else if (_in[0] != 0) {\n\t\t\t\/\/ button up event\n\t\t\temit buttonUp(_in[0]);\n\t\t}\n\t}\n<\/pre>\n<p>\n  I also had to change the initialisation code to set up the MCP23017. The data\n  sheet explained how to enable the INTA pin for the button inputs. Here is the\n  new code:\n<\/p>\n<pre>\n\t\/\/ Brute force reload ALL registers to known state.  This also\n\t\/\/ sets up all the input pins, pull-ups, etc. for the Pi Plate.\n\tuint8_t _registerInit[] = {\n\t\t0x3F,    \/\/ IODIRA    R+G LEDs=outputs, buttons=inputs\n\t\tm_ddrb,  \/\/ IODIRB    LCD D7=input, Blue LED=output\n\t\t0x3F,    \/\/ IPOLA     Invert polarity on button inputs\n\t\t0x00,    \/\/ IPOLB\n\t\t0x3F,    \/\/ GPINTENA  interrupt-on-change\n\t\t0x00,    \/\/ GPINTENB\n\t\t0x00,    \/\/ DEFVALA\n\t\t0x00,    \/\/ DEFVALB\n\t\t0x00,    \/\/ INTCONA\n\t\t0x00,    \/\/ INTCONB\n\t\t0x02,    \/\/ IOCON     BANK, MIRROR, SEQOP, DISSLW, HAEN, ODR, INTPOL, -\n\t\t0x02,    \/\/ IOCON\n\t\t0x3F,    \/\/ GPPUA     Enable pull-ups on buttons\n\t\t0x00,    \/\/ GPPUB\n\t\t0x00,    \/\/ INTFA\n\t\t0x00,    \/\/ INTFB\n\t\t0x00,    \/\/ INTCAPA\n\t\t0x00,    \/\/ INTCAPB\n\t\tm_portA, \/\/ GPIOA    Initial states for GPIO ports\n\t\tm_portB, \/\/ GPIOB\n\t\tm_portA, \/\/ OLATA\n\t\tm_portB  \/\/ OLATB\n\t};\n\tm_pI2C->send(0x00, _registerInit, 22);\n\t_logI2CFail();\n\n\t\/\/ Switch to Bank 1 and disable sequential operation.\n\t\/\/ From this point forward, the register addresses do NOT match\n\t\/\/ the list immediately above.  Instead, use the constants defined\n\t\/\/ at the start of the class.  Also, the address register will no\n\t\/\/ longer increment automatically after this -- multi-byte\n\t\/\/ operations must be broken down into single-byte calls.\n\tm_pI2C->send(MCP23017::IOCON_BANK0, 0xA2); \/\/ Originally 0xA0\n\t_logI2CFail();\n<\/pre>\n"},{"title":"Hello world!","published":"2013-12-31T00:00:00+00:00","updated":"2013-12-31T00:00:00+00:00","author":{"name":"\n            \n              Unknown\n            \n          "},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/doug.lon.dev\/blog\/2013\/hello-world\/"}},"id":"https:\/\/doug.lon.dev\/blog\/2013\/hello-world\/","content":"<p>Hello!<\/p>\n<p>\n  Recently I've been playing around with some electronics, Raspberry Pi,\n  software and OS stuff which I hope to bring together into what could be an\n  interesting project. It's something I've been thinking about for a couple of\n  years, and bit by bit I'm actually getting there.\n<\/p>\n<p>\n  Since I've already spent a lot of time just getting started with all the\n  above, and learned a lot of lessons about how to go about doing something like\n  this, I thought it also worth blogging about the experience. I'm sure there's\n  some useful hints, hacks and help I can disclose back to the world as I go;\n  seems only fair since I've borrowed a very large chunk of open source and\n  public domain knowledge and code.\n<\/p>\n<p>The first real technical post will appear soon.... :)<\/p>\n<p>&nbsp;<\/p>\n<p>d.<\/p>\n"}]}