Implementing XY2-100 serial protocol on Teensy 4.1

built1n

Member
Hi,

I'd like to use a Teensy 4.1 to implement the XY2-100 laser scanning control protocol. The protocol is a fairly simple serial protocol with the following signals:

CLOCK: ~10 MHz, 50% duty cycle
SYNC: Pulse every 20 ticks of CLOCK.
X DATA, Y DATA: Transmits a 16-bit data frame in sync with CLOCK, and triggered by edges of SYNC.

See http://www.alaser.com.tw/db/upload/webdata4/6alaser_201412422541519318.pdf for a timing diagram, etc.

An existing project to do this exists for the 3.2 (https://github.com/Tuet/XY2_100), but the code uses timers that do not exist on the 4.1 -- but at least that means it's feasible.

I've got a couple questions:

1. How do I properly generate a ~10 MHz square wave? Until now I've been doing an analogWrite at a 50% duty PWM duty cycle and setting the frequency via analogWriteFrequency, but this seems hacky. Surely there must be a better way with a hardware timer or similar? (If a hardware timer is the best option, is there a good source of examples/documentation apart from the SoC datasheet?)

2. How do I trigger actions in sync with the CLOCK or SYNC signals? Looping the CLOCK signal back to another input pin and setting a pin change interrupt seems like an option, but is there a better way?

3. The existing protocol implementation for the Teensy 3.2 uses the DMAChannel library for signal generation. I don't mind reading the source code for that, but is there supplemental documentation/examples somewhere?

- Franklin
 
I would use one of the timer libraries to see the instructions that are different for T3.X and T4.X
 
I've made some progress on this, but I'm hitting a wall with DMA.

I've gotten a initial prototype working via the PIT timer (using the IntervalTimer library) that triggers an ISR running at twice the clock rate. This ISR acts as a state machine and handles all the signal generation logic (i.e. toggling CLOCK and setting data bits), but unfortunately it seems that IntervalTimer won't go faster than a 0.75 uS period, so I'll have to use DMA.

I've been reading through the processor manual about the eDMA module, and I've gathered the following:

1. eDMA basically acts as an asynchronous memcpy that copies a couple bytes from one memory address to another (i.e. an input buffer to the GPIO data output register) whenever it's triggered.
2. Triggering can be set to one of 128 sources by changing the bottom 7 bits of the CHCFG register.
3. These sources include the Quad Timer module, which looks interesting -- can I configure that to generate a periodic trigger at the data rate I need?

Is my understanding correct? If so, is there existing code that either wraps or uses the Quad Timers (and are they the right thing to use for what I want)?
 
Medium-speed clocked serial data output on T4.1

I need to implement a custom serial protocol on T4.1 (https://forum.pjrc.com/threads/62819-Implementing-XY2-100-serial-protocol-on-Teensy-4-1) that needs to output serial data synchronously with at least a 3.2 MHz clock (preferably higher).

Data transmission occurs in "frames" which are denoted with a high pulse of the "sync" line, followed by 20 bits of serial data, written on the rising edge of the clock.

I've gotten a bit-banging implementation working below that uses the PIT to trigger an ISR at 1.33 MHz, which then handles the changing the pin states on both the high and low clock edges. This gives an effective 667 kHz clock rate.

Code:
IntervalTimer t;

void setup() {
    pinMode(3, OUTPUT);
    pinMode(5, OUTPUT);
    pinMode(7, OUTPUT);
    pinMode(9, OUTPUT);
    t.begin(callback, .75);
}

static volatile int clk = LOW;
static volatile int sync = LOW;
static volatile int ctr = 0;
static volatile int transferring = 0;

#define TOGGLE(state) (((state) == HIGH) ? LOW : HIGH)
#define EXTRACT_BIT(data, idx) (((data) >> (idx)) & 1)

// get bit corresponding to clock according to XY2-100 protocol
static int xy2_bit(int ctr, uint16_t data) {
    int bit_to_write;
    switch(ctr) {
    case 0:
    case 1:
        bit_to_write = 0;
        break;
    case 2:
        bit_to_write = 1;
        break;
    default:
        bit_to_write = EXTRACT_BIT(data, 18 - ctr);
        break;
    }
    return bit_to_write;
}

static volatile uint16_t next_xdata, next_ydata;
static volatile int x_bit = 0, y_bit = 0;
void callback() {
    static volatile uint16_t x_data = 0x8000, y_data = 0x8000;
    clk = TOGGLE(clk);
    if(clk == HIGH) {
        if(ctr == 0 || ctr == 1) {
            if(ctr == 0) {
                // begin transfer
                transferring = 1;
                x_data = next_xdata;
                y_data = next_ydata;
            }
            sync = TOGGLE(sync);
        }

        if(ctr == 19)
            transferring = 0;

        if(transferring) {
            x_bit = xy2_bit(ctr, x_data);
            y_bit = xy2_bit(ctr, y_data);
        } else {
            x_bit = 0;
            y_bit = 0;
        }
    }
    if(clk == HIGH)
        ctr = (ctr + 1) % 20;
    digitalWriteFast(5, sync);
    digitalWriteFast(7, x_bit);
    digitalWriteFast(9, y_bit);
    digitalWriteFast(3, clk);
}

void loop() {
    while(1) {
        next_xdata = next_ydata = (sin(micros() / 1e6 * 2 * M_PI * 440) + 1) / 2 * 0xffff;
    }
}

I've tested this implementation and it works fine, but it appears that IntervalTimer can't go faster than a 0.75 us period by default. I need about a 5x improvement in speed to reach the 3.2 MHz target clock rate -- how could I achieve this? I've read that overclocking the PIT is possible, but I have a feeling that reducing CPU involvement through DMA or similar is the best way to go.

Thanks!
 
I have merged these 2 threads. Please do not create duplicate threads. Most of us use the "What's New" view, so you'll generally get better help if you just post again on the same thread where all the prior info exists.
 
I ran your program here on a Teensy 4.1. These are the waveforms I see. Is this correct?

file.png
 
Assuming this is the waveform you really want, and you really do intend for the clock to be continuous and the data to immediately repeat, this looks like a job for the SAI (digital audio) port.
 
Comparing these waveforms to the SS-III XY2-100-E documentation, I see the sync signal polarity is high for 19 of the 20 clocks. But the waveforms on msg #6 have it high only 1 of the 20 clocks.

sc.png
 
Last edited:
I did some fiddling. Here's a quick test using TDM output code copied from the audio library.

This probably isn't perfect, but hopefully it will give you a good start. It generates 10 MBit/sec output and calls isr() every 20 clocks as new data is needed. The 2 data streams transmit on pins 7 and 32. Sync is on pin 20 and bit clock is pin 21.

You'll probably want to edit some of the I2S1_TCR and I2S1_RCR register settings if the waveforms aren't quite right. Documentation is in the IMXRT1060 reference manual (rev 2) in the SAI chapter. Turn to page 1981 for the register defs.

Code:
void setup() {
  pinMode(3, OUTPUT);
  config_sai1();
  attachInterruptVector(IRQ_SAI1, isr);
  NVIC_ENABLE_IRQ(IRQ_SAI1);
  I2S1_TCSR |= 1<<8;  // start generating TX FIFO interrupts
}

volatile uint16_t nextval = 0x0FF0;

void isr() {
  digitalWriteFast(3, HIGH);
  delayNanoseconds(25);
  digitalWriteFast(3, LOW);
  
  uint32_t bits20 = 0x20000 | (nextval << 1) | 0x00;
  I2S1_TDR0 = bits20;
  I2S1_TDR1 = bits20;
}

void loop() {
  static elapsedMillis msec;
  if (msec > 100) {
    nextval++;
    msec = 0;
  }
}



FLASHMEM
void config_sai1()
{
  CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON);
  double fs = 39062.5;
  int n1 = 4; //SAI prescaler 4 => (n1*n2) = multiple of 4
  int n2 = 1 + (24000000 * 27) / (fs * 256 * n1);

  double C = ((double)fs * 256 * n1 * n2) / 24000000;
  int c0 = C;
  int c2 = 10000;
  int c1 = C * c2 - (c0 * c2);

  audioClock(c0, c1, c2);
  // clear SAI1_CLK register locations
  CCM_CSCMR1 = (CCM_CSCMR1 & ~(CCM_CSCMR1_SAI1_CLK_SEL_MASK))
               | CCM_CSCMR1_SAI1_CLK_SEL(2); // &0x03 // (0,1,2): PLL3PFD0, PLL5, PLL4

  n1 = n1 / 2; //Double Speed for TDM

  CCM_CS1CDR = (CCM_CS1CDR & ~(CCM_CS1CDR_SAI1_CLK_PRED_MASK | CCM_CS1CDR_SAI1_CLK_PODF_MASK))
               | CCM_CS1CDR_SAI1_CLK_PRED(n1 - 1) // &0x07
               | CCM_CS1CDR_SAI1_CLK_PODF(n2 - 1); // &0x3f

  IOMUXC_GPR_GPR1 = (IOMUXC_GPR_GPR1 & ~(IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL_MASK))
                    | (IOMUXC_GPR_GPR1_SAI1_MCLK_DIR | IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL(0));  //Select MCLK


  // configure transmitter
  int rsync = 0;
  int tsync = 1;

  I2S1_TMR = 0;
  I2S1_TCR1 = I2S_TCR1_RFW(4);
  I2S1_TCR2 = I2S_TCR2_SYNC(tsync) | I2S_TCR2_BCP | I2S_TCR2_MSEL(1)
              | I2S_TCR2_BCD | I2S_TCR2_DIV(0);
  I2S1_TCR3 = I2S_TCR3_TCE_2CH;
  I2S1_TCR4 = I2S_TCR4_FRSZ(0) | I2S_TCR4_SYWD(0) | I2S_TCR4_MF
              /*| I2S_TCR4_FSE*/ | I2S_TCR4_FSD | I2S_TCR4_FSP;
  I2S1_TCR5 = I2S_TCR5_WNW(19) | I2S_TCR5_W0W(19) | I2S_TCR5_FBT(19);

  I2S1_RMR = 0;
  I2S1_RCR1 = I2S_RCR1_RFW(4);
  I2S1_RCR2 = I2S_RCR2_SYNC(rsync) | I2S_TCR2_BCP | I2S_RCR2_MSEL(1)
              | I2S_RCR2_BCD | I2S_RCR2_DIV(0);
  I2S1_RCR3 = I2S_RCR3_RCE_2CH;
  I2S1_RCR4 = I2S_RCR4_FRSZ(0) | I2S_RCR4_SYWD(0) | I2S_RCR4_MF
              /*| I2S_RCR4_FSE*/ | I2S_RCR4_FSD | I2S_RCR4_FSP;
  I2S1_RCR5 = I2S_RCR5_WNW(19) | I2S_RCR5_W0W(19) | I2S_RCR5_FBT(19);

  //CORE_PIN23_CONFIG = 3;  // MCLK
  CORE_PIN21_CONFIG = 3;  // RX_BCLK
  CORE_PIN20_CONFIG = 3;  // RX_SYNC
  CORE_PIN7_CONFIG  = 3;  // TX_DATA0
  CORE_PIN32_CONFIG = 3;  // TX_DATA1
  I2S1_RCSR |= I2S_RCSR_RE | I2S_RCSR_BCE;
  I2S1_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE /* | I2S_TCSR_FRDE <-- not using DMA */;

}


/*
  (c) Frank B
*/

FLASHMEM
void audioClock(int nfact, int32_t nmult, uint32_t ndiv) // sets PLL4
{
  //if ((CCM_ANALOG_PLL_AUDIO & CCM_ANALOG_PLL_AUDIO_ENABLE)) return;

  CCM_ANALOG_PLL_AUDIO = CCM_ANALOG_PLL_AUDIO_BYPASS | CCM_ANALOG_PLL_AUDIO_ENABLE
                         | CCM_ANALOG_PLL_AUDIO_POST_DIV_SELECT(2) // 2: 1/4; 1: 1/2; 0: 1/1
                         | CCM_ANALOG_PLL_AUDIO_DIV_SELECT(nfact);

  CCM_ANALOG_PLL_AUDIO_NUM   = nmult & CCM_ANALOG_PLL_AUDIO_NUM_MASK;
  CCM_ANALOG_PLL_AUDIO_DENOM = ndiv & CCM_ANALOG_PLL_AUDIO_DENOM_MASK;

  CCM_ANALOG_PLL_AUDIO &= ~CCM_ANALOG_PLL_AUDIO_POWERDOWN;//Switch on PLL
  while (!(CCM_ANALOG_PLL_AUDIO & CCM_ANALOG_PLL_AUDIO_LOCK)) {}; //Wait for pll-lock

  const int div_post_pll = 1; // other values: 2,4
  CCM_ANALOG_MISC2 &= ~(CCM_ANALOG_MISC2_DIV_MSB | CCM_ANALOG_MISC2_DIV_LSB);
  if (div_post_pll > 1) CCM_ANALOG_MISC2 |= CCM_ANALOG_MISC2_DIV_LSB;
  if (div_post_pll > 3) CCM_ANALOG_MISC2 |= CCM_ANALOG_MISC2_DIV_MSB;

  CCM_ANALOG_PLL_AUDIO &= ~CCM_ANALOG_PLL_AUDIO_BYPASS;//Disable Bypass
}
 
The falling edge of the SYNC line is arbitrary -- it just has to be low before the next frame starts. It looks like your attachment links are invalid, however, so I can't see the waveforms you've produced. Can you re-upload?

Thanks!
 
Just one minor thing -- it looks like your version produces data waveforms that update on falling edge of the clock, not the rising edge as mine does (and the protocol requires) How would I modify things to produce this phase shift?
 
Maybe the BCP bit documented on page 1989 can help?

When/if this all works, hopefully you can share the final known-good code and some more info about the project? It might really help anyone else who later finds this thread and wants to communicate with this device.
 
I'm planning to build this into an open-source library with a modular interface which I'll post here once it's finished. I notice that your audioClock() function is copyrighted by a certain Frank B. -- how does this affect potential licensing?

Thanks for all the help!
 
One more thing -- there's a related protocol called FB4 to control laser scanners that's largely similar to XY2-100, but with the following changes:

1) The sync polarity is reversed, so it should go low at the start of each frame, and high before the end.
2) The two 16-bit data channels are serialized into one 32-bit word, transmitted across one data line.
3) The second data line is repurposed for device-to-host communication, sampled synchronously with CLOCK and SYNC just as the host-to-device signal.

The first two differences should hopefully be trivial to implement, but how about the third? The Teensy would need to sample one of the GPIOs on the falling edge of the clock signal and trigger an ISR to notify application code of the reception of a 32-bit input word from the device.

EDIT: FB4 timing diagrams can be found on page 14 of https://fwei.tk/MachDSP_Help.pdf.
 
OK, for future reference here's a bare-bones working implementation of XY2-100 over SAI.

Code:
void setup() {
    pinMode(3, OUTPUT);
    config_sai1();
    attachInterruptVector(IRQ_SAI1, isr);
    NVIC_ENABLE_IRQ(IRQ_SAI1);
    I2S1_TCSR |= 1<<8;  // start generating TX FIFO interrupts
}

volatile uint16_t nextval = 0x0FF0;

void isr() {
    digitalWriteFast(3, HIGH);
    delayNanoseconds(25);
    digitalWriteFast(3, LOW);

    uint32_t bits20 = 0x10000 | (nextval << 0) | 0x00;
    I2S1_TDR0 = bits20;
    I2S1_TDR1 = bits20;
}

void loop() {
    nextval = (sin(micros() / 1e6 * 2 * M_PI * 440) + 1) / 2 * 0xffff;
}



FLASHMEM
void config_sai1()
{
    CCM_CCGR5 |= CCM_CCGR5_SAI1(CCM_CCGR_ON);
    double fs = 4000;
    int n1 = 4; //SAI prescaler 4 => (n1*n2) = multiple of 4
    int n2 = 1 + (24000000 * 27) / (fs * 256 * n1);

    double C = ((double)fs * 256 * n1 * n2) / 24000000;
    int c0 = C;
    int c2 = 10000;
    int c1 = C * c2 - (c0 * c2);

    audioClock(c0, c1, c2);
    // clear SAI1_CLK register locations
    CCM_CSCMR1 = (CCM_CSCMR1 & ~(CCM_CSCMR1_SAI1_CLK_SEL_MASK))
        | CCM_CSCMR1_SAI1_CLK_SEL(2); // &0x03 // (0,1,2): PLL3PFD0, PLL5, PLL4

    n1 = n1 / 2; //Double Speed for TDM

    CCM_CS1CDR = (CCM_CS1CDR & ~(CCM_CS1CDR_SAI1_CLK_PRED_MASK | CCM_CS1CDR_SAI1_CLK_PODF_MASK))
        | CCM_CS1CDR_SAI1_CLK_PRED(n1 - 1) // &0x07
        | CCM_CS1CDR_SAI1_CLK_PODF(n2 - 1); // &0x3f

    IOMUXC_GPR_GPR1 = (IOMUXC_GPR_GPR1 & ~(IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL_MASK))
        | (IOMUXC_GPR_GPR1_SAI1_MCLK_DIR | IOMUXC_GPR_GPR1_SAI1_MCLK1_SEL(0));  //Select MCLK


    // configure transmitter
    int rsync = 0;
    int tsync = 1;

    I2S1_TMR = 0;
    I2S1_TCR1 = I2S_TCR1_RFW(4);
    I2S1_TCR2 = I2S_TCR2_SYNC(tsync) | /*I2S_TCR2_BCP |*/ I2S_TCR2_MSEL(1)
        | I2S_TCR2_BCD | I2S_TCR2_DIV(0);
    I2S1_TCR3 = I2S_TCR3_TCE_2CH;
    I2S1_TCR4 = I2S_TCR4_FRSZ(0) | I2S_TCR4_SYWD(0) | I2S_TCR4_MF
        /*| I2S_TCR4_FSE*/ | I2S_TCR4_FSD | I2S_TCR4_FSP;
    I2S1_TCR5 = I2S_TCR5_WNW(19) | I2S_TCR5_W0W(19) | I2S_TCR5_FBT(19);

    I2S1_RMR = 0;
    I2S1_RCR1 = I2S_RCR1_RFW(4);
    I2S1_RCR2 = I2S_RCR2_SYNC(rsync) | /*I2S_TCR2_BCP |*/ I2S_RCR2_MSEL(1)
        | I2S_RCR2_BCD | I2S_RCR2_DIV(0);
    I2S1_RCR3 = I2S_RCR3_RCE_2CH;
    I2S1_RCR4 = I2S_RCR4_FRSZ(0) | I2S_RCR4_SYWD(0) | I2S_RCR4_MF
        /*| I2S_RCR4_FSE*/ | I2S_RCR4_FSD | I2S_RCR4_FSP;
    I2S1_RCR5 = I2S_RCR5_WNW(19) | I2S_RCR5_W0W(19) | I2S_RCR5_FBT(19);

    //CORE_PIN23_CONFIG = 3;  // MCLK
    CORE_PIN21_CONFIG = 3;  // RX_BCLK
    CORE_PIN20_CONFIG = 3;  // RX_SYNC
    CORE_PIN7_CONFIG  = 3;  // TX_DATA0
    CORE_PIN32_CONFIG = 3;  // TX_DATA1
    I2S1_RCSR |= I2S_RCSR_RE | I2S_RCSR_BCE;
    I2S1_TCSR = I2S_TCSR_TE | I2S_TCSR_BCE /* | I2S_TCSR_FRDE <-- not using DMA */;

}


/*
  (c) Frank B
*/

FLASHMEM
void audioClock(int nfact, int32_t nmult, uint32_t ndiv) // sets PLL4
{
    //if ((CCM_ANALOG_PLL_AUDIO & CCM_ANALOG_PLL_AUDIO_ENABLE)) return;

    CCM_ANALOG_PLL_AUDIO = CCM_ANALOG_PLL_AUDIO_BYPASS | CCM_ANALOG_PLL_AUDIO_ENABLE
        | CCM_ANALOG_PLL_AUDIO_POST_DIV_SELECT(2) // 2: 1/4; 1: 1/2; 0: 1/1
        | CCM_ANALOG_PLL_AUDIO_DIV_SELECT(nfact);

    CCM_ANALOG_PLL_AUDIO_NUM   = nmult & CCM_ANALOG_PLL_AUDIO_NUM_MASK;
    CCM_ANALOG_PLL_AUDIO_DENOM = ndiv & CCM_ANALOG_PLL_AUDIO_DENOM_MASK;

    CCM_ANALOG_PLL_AUDIO &= ~CCM_ANALOG_PLL_AUDIO_POWERDOWN;//Switch on PLL
    while (!(CCM_ANALOG_PLL_AUDIO & CCM_ANALOG_PLL_AUDIO_LOCK)) {}; //Wait for pll-lock

    const int div_post_pll = 1; // other values: 2,4
    CCM_ANALOG_MISC2 &= ~(CCM_ANALOG_MISC2_DIV_MSB | CCM_ANALOG_MISC2_DIV_LSB);
    if (div_post_pll > 1) CCM_ANALOG_MISC2 |= CCM_ANALOG_MISC2_DIV_LSB;
    if (div_post_pll > 3) CCM_ANALOG_MISC2 |= CCM_ANALOG_MISC2_DIV_MSB;

    CCM_ANALOG_PLL_AUDIO &= ~CCM_ANALOG_PLL_AUDIO_BYPASS;//Disable Bypass
}

I only had to make a couple minor tweaks to implement the protocol correctly -- namely changing the clock polarity and the transmitted bit pattern.
 
It almost seems like a moot point with a fancy SAI implementation, but I have found why the PIT timer cannot be shorter than a 0.75 uS period. In the PIT library, there is a minimum 17 clock cycle value for the timer presumably to not lock up your CPU with interrupt calls for Teensy 3.x hardware. However, with the Teensy 4.x, the clock source is currently set to a 24Mhz clock instead of the CPU clock so the minimum cycle check is not needed.

Code:
class IntervalTimer {
...
bool begin(void (*funct)(), float microseconds) {
	if (microseconds <= 0 || microseconds > MAX_PERIOD) return false;
	uint32_t cycles = (float)(24000000 / 1000000) * microseconds - 0.5f;
	if (cycles < 17) return false; // <-- HERE: Limits the interrupt interval to 1.5Mhz max
	return beginCycles(funct, cycles);
}
 
Can you clarify why the minimum cycle check isn't needed for the PIT if the clock source is external? The CPU would still be overwhelmed by interrupts no matter what the clock source is, no?
 
At a 24 Mhz interrupt rate, there would be barely enough cycles for just the interrupts to complete without additional code, so likely the more practical answer would be to just reduce the count. As for how the numbers fall out, the interrupt latency is >=12 clock cycles(of the 600MHz CPU clock) just to get to the start of the handler. Then there is a similar number of cycles to resume the program where it left off. Now at least 24 cycles of the 25 cycles(600Mhz/24Mhz) are accounted for so that's not very helpful as far as a program is concerned...

At speeds under 10Mhz, however, there should be enough cycles available for simple operations. It still will tax the system a bit to have so many interrupts, but it would be well within the realm of possibility.

If you really needed parallelized high speed pin control, you can tie together a PIT to a DMA in the DMAMUX registry. The DMA will do a single transfer every time the PIT triggers. With this configuration, the DMA can then periodically transfer values into the port registers. You would no longer need an interrupt callback, just a new buffer to swap in at the end of the DMA transfer.
 
Are you still chasing the DMA route for this? I am running a XY2-100 galvo from a Teensy 4.0. Its working flawlessly using QT1 and bitshifting! I made a Arduino Lib out of it and will be publishing it soon on OpenGalvo on GitHub.
 
I am running a XY2-100 galvo from a Teensy 4.0. Its working flawlessly using QT1 and bitshifting! I made a Arduino Lib out of it and will be publishing it soon on OpenGalvo on GitHub.

Any chance you might share some photos of the hardware in action & info about the build? Would love to be able to show it on the website blog.
 
OpenGalvo

Any chance you might share some photos of the hardware in action & info about the build? Would love to be able to show it on the website blog.

Yeah, love to! Just let me get it cleaned up first :p

I am building a GCode based controller as its first implementation. This might be ready to in a couple of days so... but I will share when ready!
 
Hi DanielO,

This project has fallen a bit on the back burner recently, but I'm still interested in driving galvos from a Teensy. Specifically, I'm working with the ScannerMAX Saturn series of galvos, which communicate using the FB4 protocol (which is is a full-duplex protocol, unlike XY2-100). I've actually gotten full-duplex FB4 working at many hundreds of kHz on a Teensy 4.1 through SAI already, but it'll probably be some time before I polish it up into a usable state.
 
OPAL FW -Released!

Well - I released my take on XY2-100 implementation OPAL FW on GitHub today! https://github.com/opengalvo/OPAL It is far from the speeds I was seeing before all GCode interpretation but hopefully some smart genious tags along and helps out to clean up the mess i made :) It is not for public use but more of a starting point. Still leaks memmory etc... @PaulStoffregen: Pictures maybe tomorrow... (Yawn) gotta ZZZzzz...
 
Back
Top