• I got it all working - thanks!

    Here is my code in case it's useful to anyone...

    var DEFS={
      TIM1: {
        base: 0x40010000,
        pin: A8
      },
      TIM3: {
        base: 0x40000400,
        pin: B4
      },
      TIM4: {
        base: 0x40000800,
        pin: B6
      }
    };
    
    function init(tim) {
      const def=DEFS[tim];
      if ( !def ) {
        throw "Invalid timer!";
      }
      const BASE=def.base;
      
      // slave mode control register
      var SMCR = BASE+0x0008;
      // event generation register
      var EGR = BASE+0x0014;
      // Capture compare mode register
      var CCMR1 = BASE+0x0018;
      // Capture/compare enable register
      var CCER = BASE+0x0020;
      // counter
      var CNT = BASE+0x0024;
      // prescaler
      var PSC = BASE+0x0028;
      // auto reload register
      var ARR = BASE+0x002C;
    
      // enable PWM on A8 (TIM1 CH1)
      analogWrite(def.pin,0.5,{freq:10});
      // CC1E = 0 (Turn channel 1 off)
      poke16(CCER, peek16(CCER) & ~1);
      // CC1S[1:0]=01 (rising edge), IC1F[7:4]=0 (no filter)
      poke16(CCMR1, (peek16(CCMR1) & ~0b11110011) | (0b00000001));
      // CC1P=0, CC1NP=0 (detect rising edge), CC1E[0] = 1 (Turn channel 1 on)
      poke16(CCER, peek16(CCER) & ~(0b1011) | (0b0001));
      // SMS[2:0]=111 (ext clock), TS[6:4]=101 (CH1 as trigger)
      poke16(SMCR, (peek16(SMCR) & ~0b1110111) | 0b1010111);
      // Prescaler to 0 - use every transition
      poke16(PSC, 0);
      // auto-reload with the full range of values
      poke16(ARR, 65535);
      // poke the UG[0] bit to reset the counter and update the prescaler
      poke16(EGR, 1);
    
      return {
        get: function() {
          return peek16(CNT);
        },
        reset: function() {
          poke16(CNT, 0);
        }
      };
    }
    
    const TIM1=init("TIM1");
    const TIM4=init("TIM4");
    const TIM3=init("TIM3");
    
    function update(){
      g.clear();
      g.drawString("Steps X: "+TIM1.get(), 2, 2);
      g.drawString("Steps Y: "+TIM4.get(), 2, 20);
      g.drawString("Steps Z: "+TIM3.get(), 2, 38);
      g.flip(); 
    }
    
    // SPI
    var s = new SPI();
    s.setup({mosi: B15 /* D1 */, sck:B13 /* D0 */});
    var g = require("SH1106").connectSPI(s, B14 /* DC */, B10 /* RST - can leave as undefined */, function() {
      setInterval(update, 100);
    });
    
    setWatch(function(e) {
      TIM1.reset();
      TIM4.reset();
      TIM3.reset();
    }, BTN, { repeat: true });
    
About