Serial communication artifacts between Arduino UNO R4 and UNO R3

Hello! The project I'm making is a "smart workshop" system. Right now I have Arduino Uno R4 as a main brain and Arduino UNO R3 acting as a graphics card with TvOut library onboard.
The Arduino UNO R4 uses hardware Serial1 interface, and R3 uses Hardware Serial with lightweight pollSerial library. Both at 115200 baudrate. Through this Serial port, the R4 can send text and simple commands to add a new line, clear the screen and add simple graphics. The problem is that every transmission, on random positions there are appearing random characters (that aren't even recognized by TvOut library).

CODE:

//(void setup on Arduino R4)
  Serial.begin(115200);
  Serial1.begin(115200); 
  
  sendTvOutCommand("&clear");
  sendTvOutCommand("\n \n \n \n");
  sendTvOutCommand("    testing");
  sendTvOutCommand("&rect 20 30 50 30");
  while (!Serial) { }

  Serial.println("Initializing Sci-Fi Control System...");

sendTvOutCommand function:

/***************************************************************************
 * Helper function to send commands or text with automatic newline handling
 ***************************************************************************/
void sendTvOutCommand(const String& cmd) {
  String buffer = "";

  for (size_t i = 0; i < cmd.length(); i++) {
    char c = cmd[i];

    if (c == '\n') {
      // Send current buffer line if not empty
      if (buffer.length() > 0) {
        Serial1.print(buffer);
        Serial1.print("\n");
        buffer = "";
      }

      // Send &newline command
      Serial1.print("&newline\n");
    }
    else {
      buffer += c;
    }
  }

  // Send remaining buffer after loop
  if (buffer.length() > 0) {
    Serial1.print(buffer);
    Serial1.print("\n");
  }
}

Whole code for the "graphics card" (on R3):

/*
 * ===============================================================
 * Project: TvOutHostV3
 * Description:
 *   Arduino Uno-based graphics host using TVout library.
 *   Receives serial commands via pollserial and renders graphics
 *   to an NTSC screen, acting as a minimal microcontroller "graphics card".
 *
 * Commands:
 *   &clear or &clr
 *     Clears the entire screen.
 *
 *   &newline or &breakline
 *     Prints a newline (moves text cursor to next line).
 *
 *   &rect x y w h
 *     Draws an unfilled rectangle at (x,y) with width w and height h.
 *     Example: &rect 10 10 50 20
 *
 *   &fillrect x y w h
 *     Draws a filled rectangle at (x,y) with width w and height h.
 *     Example: &fillrect 10 10 50 20
 *
 *   &line x1 y1 x2 y2
 *     Draws a line from (x1,y1) to (x2,y2).
 *     Example: &line 0 0 50 50
 *
 *   &pixel x y
 *     Sets a single pixel at (x,y) to white.
 *     Example: &pixel 30 40
 *
 *   &circle x y r
 *     Draws an unfilled circle centered at (x,y) with radius r.
 *     Example: &circle 60 30 10
 *
 *   &fillcircle x y r
 *     Draws a filled circle centered at (x,y) with radius r.
 *     Example: &fillcircle 60 30 10
 *
 *   &triangle x1 y1 x2 y2 x3 y3
 *     Draws a triangle with corners at the specified coordinates.
 *     Example: &triangle 10 10 50 10 30 40
 *
 *   &invert
 *     Inverts all screen pixels (black <-> white).
 *
 *   <plain text>
 *     Any other input prints directly to the screen as text.
 *
 * Changelog:
 *   [v3.0]
 *     - Initial TvOut serial host with clear and text printing
 *   [v3.1]
 *     - Implemented line-based serial command parsing
 *   [v3.2]
 *     - Added draw_rect, fillrect, line, pixel, circle, fillcircle commands
 *   [v3.3]
 *     - Added triangle and invert commands
 *     - Replaced String parsing with strtok for RAM efficiency
 *     - Improved circle implementation using midpoint algorithm
 *   [v3.4]
 *     - Unknown command is now displayed fully on the screen for easier debugging
 *     - Added &newline and &breakline command
 *     - added ability to easily change baudrate and display it on the starting screen
 *
 * Author: Antoni Gzara & GPT-4o  (2025)
 * ===============================================================
 */
#define BAUDRATE 115200

#include <TVout.h>
#include <pollserial.h>
#include <fontALL.h>

TVout TV;
pollserial pserial;

char data[64];
int data_index = 0;

// Midpoint circle algorithm (outline)
void drawCircleMidpoint(int x0, int y0, int radius) {
  int x = radius;
  int y = 0;
  int err = 0;

  while (x >= y) {
    TV.set_pixel(x0 + x, y0 + y, 1);
    TV.set_pixel(x0 + y, y0 + x, 1);
    TV.set_pixel(x0 - y, y0 + x, 1);
    TV.set_pixel(x0 - x, y0 + y, 1);
    TV.set_pixel(x0 - x, y0 - y, 1);
    TV.set_pixel(x0 - y, y0 - x, 1);
    TV.set_pixel(x0 + y, y0 - x, 1);
    TV.set_pixel(x0 + x, y0 - y, 1);

    y += 1;
    if (err <= 0) {
      err += 2 * y + 1;
    }
    if (err > 0) {
      x -= 1;
      err -= 2 * x + 1;
    }
  }
}

// Filled circle using scanline filling of midpoint circle
void fillCircle(int x0, int y0, int r) {
  for (int y = -r; y <= r; y++) {
    int dx = (int)sqrt(r * r - y * y);
    for (int x = -dx; x <= dx; x++) {
      TV.set_pixel(x0 + x, y0 + y, 1);
    }
  }
}

// Draw triangle outline with 3 lines
void drawTriangle(int x1, int y1, int x2, int y2, int x3, int y3) {
  TV.draw_line(x1, y1, x2, y2, 1);
  TV.draw_line(x2, y2, x3, y3, 1);
  TV.draw_line(x3, y3, x1, y1, 1);
}

// Invert screen pixels
void invertScreen() {
  for (int y = 0; y < TV.vres(); y++) {
    for (int x = 0; x < TV.hres(); x++) {
      char pix = TV.get_pixel(x, y);
      TV.set_pixel(x, y, !pix);
    }
  }
}

void setup() {
  TV.begin(NTSC);
  TV.set_hbi_hook(pserial.begin(BAUDRATE));
  TV.select_font(font6x8);
  TV.println();
  TV.println(" TvOut Host");
  TV.println(" -- V3.4 --");
  TV.draw_rect(4, 6, 62, 18, WHITE);

  //drawTriangle(4, 63, 4, 34, 33, 34);
  //drawTriangle(8, 63, 37, 63, 37, 34);

  TV.println("\n\n\n");
  TV.println("    TvOut");
  int x = 3;
  int y = 4;
  drawCircleMidpoint(33 + x, 54+y, 21);
  TV.draw_line(15+ x, 54+y, 33+ x, 34+y, WHITE);
  TV.draw_line(33+ x,34+y,53+ x,54+y, WHITE);
  TV.draw_line(53+ x,54+y,33+ x,74+y, WHITE);
  TV.draw_line(15+ x,54+y,33+ x,74+y, WHITE);
  TV.draw_line(33+x,34+y, 28+x, 29+y, WHITE);
  TV.draw_line(33+x, 34+y, 38+x, 29+y, WHITE);
}

void loop() {
  while (pserial.available() > 0) {
    char c = (char)pserial.read();

    if (c == '\n') {
      data[data_index] = '\0';

      if (data[0] == '&') {
        if (strcmp(data, "&clear") == 0 || strcmp(data, "&clr") == 0) {
          TV.clear_screen();
        } else if (strcmp(data, "&newline") == 0 || strcmp(data, "&breakline") == 0) {
          TV.println();
        }
        
        else if (strncmp(data, "&rect", 5) == 0) {
          char *cmd = strtok(data, " ");
          char *sx = strtok(NULL, " ");
          char *sy = strtok(NULL, " ");
          char *sw = strtok(NULL, " ");
          char *sh = strtok(NULL, " ");
          if (sx && sy && sw && sh) {
            TV.draw_rect(atoi(sx), atoi(sy), atoi(sw), atoi(sh), 1);
          }
        }
        else if (strncmp(data, "&fillrect", 9) == 0) {
          char *cmd = strtok(data, " ");
          char *sx = strtok(NULL, " ");
          char *sy = strtok(NULL, " ");
          char *sw = strtok(NULL, " ");
          char *sh = strtok(NULL, " ");
          if (sx && sy && sw && sh) {
            TV.draw_rect(atoi(sx), atoi(sy), atoi(sw), atoi(sh), 1, 1);
          }
        }
        else if (strncmp(data, "&line", 5) == 0) {
          char *cmd = strtok(data, " ");
          char *sx1 = strtok(NULL, " ");
          char *sy1 = strtok(NULL, " ");
          char *sx2 = strtok(NULL, " ");
          char *sy2 = strtok(NULL, " ");
          if (sx1 && sy1 && sx2 && sy2) {
            TV.draw_line(atoi(sx1), atoi(sy1), atoi(sx2), atoi(sy2), 1);
          }
        }
        else if (strncmp(data, "&pixel", 6) == 0) {
          char *cmd = strtok(data, " ");
          char *sx = strtok(NULL, " ");
          char *sy = strtok(NULL, " ");
          if (sx && sy) {
            TV.set_pixel(atoi(sx), atoi(sy), 1);
          }
        }
        else if (strncmp(data, "&circle", 7) == 0) {
          char *cmd = strtok(data, " ");
          char *sx = strtok(NULL, " ");
          char *sy = strtok(NULL, " ");
          char *sr = strtok(NULL, " ");
          if (sx && sy && sr) {
            int x = atoi(sx);
            int y = atoi(sy);
            int r = atoi(sr);
            if (r > 0) {
              drawCircleMidpoint(x, y, r);
            }
          }
        }
        else if (strncmp(data, "&fillcircle", 11) == 0) {
          char *cmd = strtok(data, " ");
          char *sx = strtok(NULL, " ");
          char *sy = strtok(NULL, " ");
          char *sr = strtok(NULL, " ");
          if (sx && sy && sr) {
            int x = atoi(sx);
            int y = atoi(sy);
            int r = atoi(sr);
            if (r > 0) {
              fillCircle(x, y, r);
            }
          }
        }
        /*
        // Cursor command removed because TVout doesn't support public cursor setter
        else if (strncmp(data, "&cursor", 7) == 0) {
          // Not supported
        }
        */
        else if (strncmp(data, "&triangle", 9) == 0) {
          char *cmd = strtok(data, " ");
          char *sx1 = strtok(NULL, " ");
          char *sy1 = strtok(NULL, " ");
          char *sx2 = strtok(NULL, " ");
          char *sy2 = strtok(NULL, " ");
          char *sx3 = strtok(NULL, " ");
          char *sy3 = strtok(NULL, " ");
          if (sx1 && sy1 && sx2 && sy2 && sx3 && sy3) {
            drawTriangle(atoi(sx1), atoi(sy1), atoi(sx2), atoi(sy2), atoi(sx3), atoi(sy3));
          }
        }
        else if (strcmp(data, "&invert") == 0) {
          invertScreen();
        }
        else {
          TV.println("Unknown cmd: ");
          TV.println(data);
        }
      } else {
        TV.print(data);
      }

      data_index = 0;
    } else {
      if (data_index < sizeof(data) - 1) {
        data[data_index++] = c;
      }
    }
  }
}

whole board (pretty much only the arduino boards are used here, the rest is waiting for later development

Serial Connection:

Arduino Uno R4 is powered using USB-c only (I'm afraid that cheap 12V-5v converter will break and fry this expensive board)


Artifact inside "testing" text


When "graphics card" doesn't recognize the command, it displays the message "unknown cmd:", as you can see here, the artifact appeared in the middle of the new line command


Total mess, because the &clear command got corrupted and screen was not erased


This time it attacked the rectangle command


No comment

This is what boggles my mind this weekend, if you have any ideas or more questions (maybe something needs clarification), let me know!

My theory is that might be a timing issue between Arduino R4 and R3 as these have very different clock frequencies. Maybe the problem is with the pollSerial Library?

I also tried changing drastically the baudrate to 4800, but the artifacts changed to 'w' characters, Weird...

Thanks for reading!

TL;DR chatGPT summary:

:wrench: Serial artifacts in Arduino Uno R4 -> R3 communication (TvOut graphics host)

Post body:

Hello everyone!

I’m building a “smart workshop” system. Currently:

Arduino UNO R4 (Minima) acts as the main brain.

Arduino UNO R3 acts as a graphics card running TvOut library (via pollserial) and outputs to a small NTSC screen.

:white_check_mark: Goal: R4 sends text and drawing commands to R3 via Serial, and R3 renders them to the screen.
:warning: Problem

On every transmission, random corrupted characters appear in random positions, even inside commands or plain text. This breaks parsing on R3, resulting in:

Unknown command errors

Text artifacts

Screen not clearing when &clear command is corrupted

:wrench: Setup Details

Connection: R4 Serial1 TX → R3 RX

Baudrate: 115200 (tried lower as well)

R4 uses Serial1 hardware UART

R3 uses Hardware Serial with pollserial library

R4 powered via USB-C only (to protect it from cheap buck converter failure)

:desktop_computer: R4 (sender) snippet:

void sendTvOutCommand(const String& cmd) {
  String buffer = "";

  for (size_t i = 0; i < cmd.length(); i++) {
    char c = cmd[i];

    if (c == '\n') {
      if (buffer.length() > 0) {
        Serial1.print(buffer);
        Serial1.print("\n");
        buffer = "";
      }
      Serial1.print("&newline\n");
    }
    else {
      buffer += c;
    }
  }

  if (buffer.length() > 0) {
    Serial1.print(buffer);
    Serial1.print("\n");
  }
}

Usage example:

Serial.begin(115200);
Serial1.begin(115200);

sendTvOutCommand("&clear");
sendTvOutCommand("\n \n \n \n");
sendTvOutCommand("    testing");
sendTvOutCommand("&rect 20 30 50 30");

:artist_palette: R3 (graphics host)

Runs a TvOutHostV3 code that parses commands like:


    &clear

    &newline

    &rect x y w h

    &fillrect x y w h

    &line x1 y1 x2 y2

    &pixel x y

    &circle x y r

    &fillcircle x y r

    &triangle x1 y1 x2 y2 x3 y3

    &invert

If an unknown command is received, it displays Unknown cmd: ... for debugging.
:camera_with_flash: Observed artifacts

Artifacts inside text: e.g. random unreadable character in “testing”

Corrupted commands: e.g. &clear becomes unrecognized

Random w characters at 4800 baud

Commands fail silently or break parsing logic

:magnifying_glass_tilted_left: My Theory

Timing mismatch / clock drift between R4 (48 MHz) and R3 (16 MHz).

pollSerial library on R3 is lightweight but maybe unstable at higher baudrates.

Voltage levels should be okay (both are 5V logic).

Software bug unlikely since Serial works perfectly when connected to PC.

:red_question_mark: My questions

:white_check_mark: Has anyone used pollSerial with high baudrates successfully?
:white_check_mark: Should I switch to SoftwareSerial or another alternative?
:white_check_mark: Could interrupt priorities / clock speeds cause this level of corruption?
:white_check_mark: Is there a better approach to streaming text and commands to TvOut systems?
:wrench: Troubleshooting attempted

Lowering baudrate to 4800 (changed artifact characters, but still corrupted)

Verifying wiring and common ground

:light_bulb: Final thoughts

This issue is boggling my mind this weekend :sweat_smile:.
If you have any ideas, further questions, or need clarifications about the setup and wiring, please let me know!

Thanks for reading and helping me build this sci-fi inspired workshop control system.
Looking forward to your insights.

Is there a reason why the whole thing could not be run on the R4 ?

Yes, the TvOut library is pretty outdated and hard-coded for only a few Atmel AVR microcontrollers. Also it looks like the only library that fit my needs, and I don't feel like engineering a Arduino Uno R4 NTSC library from scratch (although it could be interesting). Another benefit is that the Uno R4 is not slowed down by heavy computing required to accurately send analog signals to the TV.

Understood

Have you considered other communication protocols between the boards such as I2C ?

Whilst I know nothing about it I am suspicious of the pollSerial library. What happens if you just use the normal hardware Serial library ?

I used the pollserial library because of this one feature that doesn't work with regular serial

TV.set_hbi_hook(pserial.begin(BAUDRATE));

hbi stands for horizontal blanking interval, and (from what I understand) by hooking the pollserial to it, Serial port operations don't block video generation (and vice versa). I know nothing about pollserial library too, this line was borrowed from TvOut library example here.
And here is full TvOut Library mirror (as the original was archived on google code)

Did you test the Uno R3 with the TVout by using the serial monitor instead of the R4? Simple sketch, read input from serial monitor and write to TV?

Note:
You should test the result of a strtok() operation before continuing. Something like

char *cmd = strtok(data, " ");
if(cmd == nullptr)
{
  // error
}

Yes and it's really weird, the sketch even in advanced form (with all the commands) works perfectly fine through serial monitor (even with long strings that take all the screen space), but for some reason the communication has errors only between two Arduinos.

Let’s start by eliminating the common errors (you’re new here, no offence intended but we have zero idea of your capabilities).

  • Is there a common ground between R3 and R4? If not, start there.
  • Otherwise, if you have a schematic, even ridiculously simple, that would help us. I think I know how you have it connected, but…

The use of the word “poll” implies to me that there is no interrupt involved in serial reception, rather the code needs to constantly check for character info. If that’s the case, reception at 115200 would be extremely flawed. I’d give that up, and try debugging the 4800 baud version.

edit - ugh, just saw your common ground comment. oops.

Sorry, I am semi-rusty with the Arduino R4 code base, and I have never
owned an R3, but have had earlier UNO boards...

When I last played with the R4, there were a lot of bugs and limitations
in the Serial code. I just did a quick check of the current code base
and it looks like some of the issues may be slightly addressed in a
PR about 8 months ago.

One of the major issues with their Serial objects is that they do not use
any Software buffering of TX output. That is, if Your code,
does something like: Serial1.write(buffer, 128), the code will sit there
in the code:

size_t  UART::write(uint8_t* c, size_t len) {
  if(init_ok) {
    tx_empty = false;
    tx_complete = false;
    R_SCI_UART_Write(&uart_ctrl, c, len);
    while (!tx_empty) {}
    return len;
  }
  else {
    return 0;
  }
}

Until all of the bytes have been output. With minor caveat, it will
actually return after the last byte has been put into the hardware FIFO
Which on some of the UARTS the fifo has a length of 1...
Note: The actual UART class actually has a tx_buffer (of 512), which is
totally unused, except to clear it.

The code above may have fixed one issue that it had when I last played with it. There was a timing window of when your call to write returns and if you
did one right after that, data could be lost...

A problem I have run into with no TX buffering, is for example if your code sits in a SerialX.write() a long enough time, and for example you are receiving data on an SerialX object, and if your stuck in the TX such that the RX received is larger than the software buffer, you will lose some data.

Side note: if by accident or with the assumption the system will handle it do something like: Serial1.write(buffer, 0);

It potentially could hang in that call, as you might not get a callback
to clear tx_empty or if you call Serial1.flush(); after that call
may not get the callback to clear the tx_complete flag.

Other potential issue, that I have run into, is that the member function
availableForWrite is not implemented, and defaults to the base class
which returns 0...

There are some example sketches out there that use this for sketches
like a USBToSerial adapter sketch, the echoes everything that comes
in from USB to a Serial port, and everything from the Serial port back
to USB...

And many of these do this as to not hangup in the write calls, so they
find out how many characters are available for RX on one device and how
many characters that can be output on the other take the min of those
...

Sorry not sure if any of these issues/limitations are ones are hitting, but
thought I would mention them

After a quick peek at the pictures, I see the "artifact" seems to be always the same (the same shape). It looks like something (?) is sending that byte thorugh the serial (the display tries to "interpret" it but probably it's a byte code outside the character map, pointing to an unwanted area), but I don't know either what is doing that or why. If it were a noise-generated artifact, it would be random.

I think so too.