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!