MarlinFirmware / Marlin

Marlin is an optimized firmware for RepRap 3D printers based on the Arduino platform. Many commercial 3D printers come with Marlin installed. Check with your vendor if you need source code for your specific machine.
https://marlinfw.org
GNU General Public License v3.0
16.27k stars 19.23k forks source link

Dual Serial Port #4776

Closed apballard closed 8 years ago

apballard commented 8 years ago

So I had this idea, not sure if its going to fly, but is it possible to have two serial ports working at the same time? They would mirror each other. Both would receive G-Code and both would respond. If you did a M503 on one, the info/response would be sent to both serial ports (may be unnecessary).

My idea is to completely disable LCD support and have another Arduino handling the display and communicating with Marlin using a second serial port using G-Code. However the USB port would still be used at the same time to print with (PC/Mac/OctoPi).

I know this has been done before, but I have not found something that allows for two serial ports. I can handle most of the programming of the Arduino and so forth, but after looking through Marlin's code, I realized I'm in over my head.

Maybe there is a better way to interface another Arduino?

Roxy-3D commented 8 years ago

There is plenty of room for different view points on this topic. But my thinking is it would be simpler to just move Marlin to a 32-Bit platform so we don't have to worry about splitting the work load.

If you really do want to (or need to) split the work load, wouldn't it make sense to use a higher bandwidth connection than a generic serial port?

apballard commented 8 years ago

@Roxy-3D I do agree. I'm personally very excited about the prospect of Marlin moving to 32bit.

I originally though serial, since the LCD itself does not need a lot of data to keep it up-to-date. The "protocol" is basically there with G-Code, so I thought it may be easier. Its the closest thing to an API Marlin has. Just need to write a parser on the Arduino and you halfway there.

I was thinking I2C or SPI, but I'm unsure how to interface it into Marlin. I have not given it much thought. Probably spend the weekend looking into it.

Roxy-3D commented 8 years ago

I originally though serial, since the LCD itself does not need a lot of data to keep it up-to-date. The "protocol" is basically there with G-Code, so I thought it may be easier. Its the closest thing to an API Marlin has. Just need to write a parser on the Arduino and you halfway there.

I haven't given this much thought, but this would be my first thoughts on the topic: If I was going to do this, I would try to identify all of the variables that the LCD Panel uses. And maybe even identify all the variables the LCD Panels uses on a per screen basis. With this work done, it maybe just a matter of sending a packet to the 2nd processor with the real LCD Panel that chunk of information to display.

And perhaps it is a 2-way protocol because as the LCD Panel changes things, those variables need to be feed back to the real machine.

I was thinking I2C or SPI, but I'm unsure how to interface it into Marlin. I have not given it much thought. Probably spend the weekend looking into it.

I^2C is already in Marlin. You can just use it. Going with the approach above, I^2C or SPI are probably not candidates. Or... The above approach is not a candidate. The reason is both of those capabilities are very CPU intensive because they are doing all the work (and timing) in software. The end result is you won't be off loading the CPU by doing this. In order to be viable that approach I mentioned up above would need a fast and efficient way to transfer small blocks of data. Unfortunately, we don't have any DMA type devices for the AVR. If we did, that would make that approach much more viable.

thinkyhead commented 8 years ago

I recently did a project where a second RAMPS board was used specifically just for the Z axis. The main board uses i2c to pass on commands selectively to the second board and to request the "current status" of those commands. I used a very light protocol so, for example, the master just sends "L" to initiate the bed leveling procedure and waits for a status of "L" to indicate successful completion. The slave board processes i2c in an interrupt, so I had to be cautious, initiating the commands and exiting quickly.

Of course a more robust solution would use either SPI or RS485, but the principle is the same. The main board sends off commands and periodically requests or receives a status from the second board. During that project I ended up making a lot of improvements in the i2c code in Marlin, so it's now quite solid.

If you'd like to try doing a project with a second board using i2c I would be happy to give you guidance on how to accomplish it. For now, do a search in the sources for EXPERIMENTAL_I2CBUS and check out the code that it enables.

Trackback: #4586, #4595, #4606

apballard commented 8 years ago

@thinkyhead I think I'm going to try your i2c approach. Need to sift through the code and see how to use i2c. Is there a specific way I should use it, esp to prevent contention? Or is it handled by your code?

I see I can use some methods/functions in the ultralcd file that can pop messages/commands onto the queue and I can read various variables like Roxy suggested and send them off to the LCD.

Should I just hijack the lcd_update function to do all this?

apballard commented 8 years ago

Ok so I think I can get this to work. Two questions though.

Does the twibus cause any blocking that would negatively impact the performance of Marlin?

How dependent is Marlin on ultralcd? Can it be cut out completely?

thinkyhead commented 8 years ago

Does the twibus cause any blocking

Not really. The TWIBus doesn't block for any significant amount of time. On the master side, you just call a Wire method to send bytes to one or more slaves. Each slave gets an interrupt and should receive the bytes. The slave can then do what it needs to do with the bytes and return from the interrupt. At that point the slave will just carry on with its main loop.

#if ENABLED(EXPERIMENTAL_I2CBUS) && I2C_SLAVE_ADDRESS > 0

  void i2c_on_receive(int bytes) { // just echo all bytes received to serial
    i2c.receive(bytes);
  }

  void i2c_on_request() {          // just send dummy data for now
    i2c.reply("Hello World!\n");
  }

#endif

How dependent is Marlin on ultralcd?

If no LCD is enabled you can delete all the LCD-oriented sources without issue.

thinkyhead commented 8 years ago

Here's the function I use to ask a slave for its "status." The status is just a single ASCII character, which on the slave is always set to something meaningful:

// Get the status of the remote
// If it fails to answer then it hung up
char get_remote_status() {
  for (uint8_t tries=5; tries--;) {
    if (i2c.request(1)) {             // Request a single byte
      char answer;                    // a buffer to store the reply
      i2c.capture(&answer, 1);        // Get the reply
      return answer;                  // Return it
    }
  }
  return 0;                           // 0 for total failure
}
thinkyhead commented 8 years ago

And here's the function I use to send a "command" which is again just a single ASCII character in my particular application:

bool remote_command(char command) {
  SERIAL_ECHO_START;
  SERIAL_ECHOLNPAIR("Sending command: ", command);
  i2c.address(REMOTE_I2C_ADDRESS);
  i2c.flush();                        // Clear stale data from the bus
  i2c.reset();
  char status;
  do {
    i2c.addbyte(command);
    i2c.send();
    status = get_remote_status();
    if (status == 'Q') safe_delay(20);
  } while (status == 'Q');
  return (status == command || remote_wait(command));
}

At the end it waits for the status to match the command letter, indicating that it was successful. When the status is 'Q' it means the command queue on the slave was full, so the command needs to be sent again. This allows the slave to return quickly, because if the slave were to wait around for its queue to become open it could cause Marlin to reboot (via the watchdog timer), or even crash (because i2c interrupts should never linger).

thinkyhead commented 8 years ago

For completeness, here's the function I use to wait for something from the slave:

// Wait for a response from the remote
bool remote_wait(char good_status, uint16_t wait=250) {
  bool command_done = false;
  while (!command_done) {             // Wait for the remote to finish
    safe_delay(wait);                 // Wait 1/4 second before checking
    char status = get_remote_status();
    switch (status) {
      case '?': break;                // ? = Busy (still homing, or whatever)

      case 0:
        SERIAL_ERROR_START;
        SERIAL_ERRORLNPGM("Remote Hung Up");

      case 'F':
        return false;                 // F = Failed

      default:                        // good reply = Operation Done
        if (status == good_status)
          command_done = true;
        else {
          SERIAL_ECHO_START;
          SERIAL_ECHOLNPAIR("Remote status: ", status);
        }
        break;
    }
  }
  return true;
}
thinkyhead commented 8 years ago

Meanwhile, here's how the slave replies to a status request from the master:

void i2c_on_request() {
  i2c.reset();
  i2c.addbyte(remote_status); // ?, F, or "success"
  i2c.reply();
}

The function i2c_on_request is automatically registered as the i2c request handler by setup() when I2C_SLAVE_ADDRESS is set non-zero.

#if ENABLED(EXPERIMENTAL_I2CBUS) && I2C_SLAVE_ADDRESS > 0
  i2c.onReceive(i2c_on_receive);
  i2c.onRequest(i2c_on_request);
#endif
thinkyhead commented 8 years ago

Finally, the magic function to handle i2c commands sent by the master controller:

FORCE_INLINE void _enqueue_master_command(char *cmd, const char pre_status='?') {
  remote_status = _enqueuecommand(cmd) ? pre_status : 'Q';
}

void i2c_on_receive(int bytes) {
  static char i2c_buffer[33];
  uint8_t len = i2c.capture(i2c_buffer, 32); // capture only up to 32 bytes
  i2c_buffer[len] = '\0';

  remote_status = '*';         // Standby

  switch (i2c_buffer[0]) {
    case 'S':                  // Synchronize
      _enqueue_master_command("M400", '$');
      break;
    case 'F':                  // Set feedrate in mm/m (G1 Fnnn)
    case 'Z':                  // Move Z (G1 Znnn)
      char cmd[32];
      sprintf_P(cmd, PSTR("G1 %s"), i2c_buffer);
      _enqueue_master_command(cmd, 'Z');
      break;
    case 'H':                  // Home Z
      _enqueue_master_command("G28 Z");
      break;
    case 'L':                  // Level Bed
      _enqueue_master_command("G29");
      break;
    default:
      if (len > 1) _enqueue_master_command(i2c_buffer);
      break;
  }
}

It returns as quickly as it can. If the queue is full it sets the status to Q. The master will try to re-send the command after a short delay, so it won't continually interrupt the slave. Fortunately the queue never gets full in my specific situation.

The GCode command handlers themselves will set the status to L or H or whatever is appropriate when they complete their work, with a simple assignment like remote_status = 'L'.

thinkyhead commented 8 years ago

So, for your concept of a remote with only a display attached, you would simply send the values that you wanted to update on the display. The slave would receive them and update its internal variables.

If you wanted to be able to use the controller wheel attached to the slave, then it gets a little trickier, but not too much so. Slaves can't interrupt the master; they can only wait for it to ask. So the slave would simply set the status to some letter that indicates a value has been changed, and the master would then request the value at its own convenience.

The slave would do something like this:

void i2c_on_request() {
  i2c.reset();
  if (remote_status == 'R') {    // I told you about this with 'V'
     i2c.addstring(value_to_send);
     remote_status = '?';        // nothing exciting here anymore
  }
  else {
    i2c.addbyte(remote_status);
    if (remote_status == 'V')    // I have a value for you...
      remote_status == 'R';      // you should read it next
  }
  i2c.reply();
}
apballard commented 8 years ago

@thinkyhead Wow, thanks for all the valuable info!

I've already for my first attempt managed to get ultralcd rooted out, just as a basic step to understand the dependencies, but there is some valuable info in there so more than likely will bring it back, also considering I want to minimize the number of changes to Marlin as a whole. But my understanding of the design has definatly improved over the weekend.

Will spend this week putting your examples into practice.

Thanks again! I'll let you know how it goes.

To elaborate a bit, I want to use a nextion display as a marlin interface. It uses serial and to reduce the overhead I want to use a lightweight protocol to communicate with an Arduino that will then communicate with the nextion display. Any communication to the lcd that's less than 1second should be fine, lcds hardly ever need high priority in the program loop. That's might change if it were to include sd card printing or an encoder, but that's another discussion. This screen has touch built in.

Roxy-3D commented 8 years ago

Wow, thanks for all the valuable info!

Yeah! I especially liked his last 3 posts!

apballard commented 8 years ago

@thinkyhead So I'm getting along quite nicely, managed to get things going. I'm still waiting for my nextion display from China, but working with a plain TFT display for now. I'm busy working on the protocol and what you have given me is really helping!

Question: It seems from my tests that current_position[] is giving me the intended destination. Is there a variable that gives the head location at any given time? I'm not sure there will be a practical use for it, but it would be interesting to know.

thinkyhead commented 8 years ago

@apballard The current_position only tracks the position as moves are received and queued, and is always ahead of the planner when the steppers are in motion. (This is usually called the "logical" position in the code.) The planner.position gets updated as individual moves (or segments) are added to the planner, and so it's also ahead of the current stepper positions whenever the steppers are in motion. Note that the planner.position is stored in terms of steps, not millimeters, and will have had bed leveling compensation applied to it, if that is active. Finally, there's the stepper.count_position, which tracks the actual position of the steppers as they move. As with the planner.position, the stepper.count_position tracks stepper steps and not millimeters, and will have had bed leveling applied to it also.

So, if you want to get the latest XYZE position, you need to grab the stepper values, convert them to millimeters, and remove any bed leveling compensation from the resulting values. I've been working on adding more universal functions to do this, and have made some progress, but I still have some work to do on it. The PRs #4789 and #4814 are the first two in that process, and there will be one or two more coming soon which will move more of the bed leveling handling to the Planner class, where it will be more transparent to the "high level" code in Marlin_main.cpp.

The main function to pay attention to is set_current_from_steppers_for_axis. It does the necessary calls to convert the current stepper positions into cartesian positions in the current "working space" and then stores them in current_position. A function to display the current stepper positions in terms of the working space would do all the same stuff, except it would not store the result in current_position.

The working space defines where any given cartesian XYZ is actually located within the physical boundaries of the machine. Most of the time the working space is unshifted, so "home" is at X0 Y0 Z0. But it's possible to shift the space around using M206 to set Home Offsets, or by using G92 to change the current position (thus also changing the movement limits). The offset applied by G92 is cleared with every G28, but the home offsets are retained until changed, and so define what the current position is after G28.

apballard commented 8 years ago

I was wondering if I could make a feature request to implement M73? Was looking at the reprap.org Goode page and maker bot has this implemented and it made sense to me. So my problem with my new display I'm working on is there is no "formal" way to represent printing progress. Especially when printing via USB. It may be prudent to incorporate this into the SD progress bar so they all leverage a single progress variable. This will allow me to push the progress to the lcd and represent it any way I wish.

I will start looking at this, but to be honest, what may take me weeks may take someone days. Not sure if there is someone else willing to look into it?

Thoughts? Not sure if we are entertaining new features at the moment?

thinkyhead commented 8 years ago

Interesting. M73 would give hosts a way to set the percentage on the LCD, and for GCode files to specify their own percentage evaluation. It wouldn't be too difficult. But should it be yet another standard feature, or yet another optional feature? Either way it adds to our general bloat for a feature only so far implemented by and for MakerBot firmware.

apballard commented 8 years ago

@thinkyhead True, but I see that Simplify3d, Craftware and I think Cura support this, so maybe something worth looking into?... thanks for your comments. Not sure about the answer to optional or standard though.

thinkyhead commented 8 years ago

Interesting that some slicers support it, considering that only users of Makerbot would have ever been able to use it.

Blue-Marlin commented 8 years ago

A %-number is not useful without an explanation of - percent of what. "buinl percentage" can mean: % of the characters of the g-code file % of lines from total lines of the g-code file % of meters of the total meters of the file % of time calculated by meters/(m/s) calculated without accelerations % of time calculated by meters/(m/s) calculated with accelerations % of time calculated by meters/(m/s) calculated with accelerations and assumptions about heating times. % of time calculated by meters/(m/s) calculated with accelerations and assumptions about heating times and included subfiles. % of time calculated by meters/(m/s) calculated with accelerations and assumptions about heating times and included subfiles and estimations about the duration of G29, tool change, colour/material change, (or whatever) ...

Without the 'method' a %-number is ridicules. The host may send a estimation by M117 (like RH can do) but integrating this feature into Marlin would mead we believe in the number the host sends - or at least the concept of finding out this number.

For the same reasons our 'progress bar' is questionable. But here at least we know how it is calculated.

Predicting a reasonable end-time is much to complicated for Marlin and impossible when we get the g-code via USB. Without much more information about Marlin it is impossible to calculate a end-time for the host.

M73 is bloat!

apballard commented 8 years ago

Thanks for all the assistance! Great support! I've received my Nextion display and development is in progress! It's looking promising!

Will start sharing results but for now issue resolved!

thinkyhead commented 8 years ago

@apballard I just saw a video by @MagoKimbra (Alberto Cotronei) demonstrating touch-screen with Marlin. Check his branches to see if Nextion is the supported display.

MagoKimbra commented 8 years ago

Yes i use Nextion LCD... But this display uses a particular programming system. Each item has its own id, and sends and receives data on the serial that follow the logic of this type, are hexadecimal data: ID-type-content-FF FF FF. So I developed a new part called Nextion_LCD that sends out another serial data, and receiving information from the display on the keys pressed. https://www.youtube.com/watch?v=Q5XQ3llT1bY

This is a old video but you see the preview image object 3d on display. https://www.youtube.com/watch?v=VEPCqTN2B0k

CampbellFabrications commented 7 years ago

Well what do you know,

What are the chances that I was looking through the I2C related pages for slaving some sensor breakout devices, and came across a year old post about the very command I 'quick and dirty'-ly implemented. Spoiler: some cool i2c-based forks of marlin are coming soon that I think you will like @thinkyhead

crokatron commented 5 years ago

Hi, sorry if I'm beating a dead horse or asking what's already been asked.

I have minimal experience with Arduino and marlin but I'm learning more each day.

Currently, I'm running into issues with fitting all the features I want in marlin onto my melzi board, I understand that it already communicates with the LCD over i^2c. Is it possible to split the firmware over two boards? for example, I have a full graphics LCD (12864) and the u8glib take up a lot of Progmem so, I would like to detach the display from the master board and run it on a slave.

Would I be able to remove all the LCD related components of marlin from the master and offload it to the slave? Such that the master communicates its current state to the slave and the slave displays it and can send commands back to the master, like if I click home all axis on the display that it then sends the relevant command back to the master.

I would also like to use the extra pins on the slave to add peripherals like a bltouch / filament runout sensor / enclosure temperature / mosfets for turning the printer off etc. I understand I would be better suited to 32bit or an Arduino with more progmem and pins, but I would very much like to acheive this without replacing the stock board in my printer

Kind Regards Leras

thinkyhead commented 5 years ago

Would I be able to remove all the LCD related components of marlin from the master and offload it to the slave?

It would require a pile of work, but it's possible. I have already done this kind of thing for a system which needed 4 Z stepper motors and 4 Z endstops to be controlled on a separate board. You can see how this was done in these two branches:

And this thread explains how the i2c communication protocol was implemented.

To do what you want will require a boatload of work, so you may want to hire a smart developer in your local area to help you out. Be sure to pay them well for their time and skills!

jgoo9410 commented 5 years ago

@thinkyhead I implemented your code to control a separate Z controller. There seem to be some issues with the code. The command is sent (and received on a separate device) but at some point during the 'wait for a reply' procedure, marlin crashes and resets. Any suggestions on a remedy?

hitchcme commented 4 years ago

@thinkyhead So I'm getting along quite nicely, managed to get things going. I'm still waiting for my nextion display from China, but working with a plain TFT display for now. I'm busy working on the protocol and what you have given me is really helping!

Question: It seems from my tests that current_position[] is giving me the intended destination. Is there a variable that gives the head location at any given time? I'm not sure there will be a practical use for it, but it would be interesting to know.

I love this site here, for gcode reference. http://marlinfw.org/docs/gcode/M114.html

M114; # I use this for measuring Z-probe Z-offset, and just asking "Where do you think you're at?"

I imagine "grep"ing through the source for how M114 is implemented, you could probably find that variable.

github-actions[bot] commented 4 years ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

github-actions[bot] commented 4 years ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

github-actions[bot] commented 3 years ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

github-actions[bot] commented 2 years ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.