Add documentation.

This commit is contained in:
giomba 2022-10-01 22:43:18 +02:00
parent 11c051cfab
commit d645c1ada2
2 changed files with 117 additions and 61 deletions

176
frame.cpp
View File

@ -2,159 +2,215 @@
* @file frame.cpp * @file frame.cpp
* @author giomba@glgprograms.it * @author giomba@glgprograms.it
* @brief Transform a black and white PNM image into the assembly code for the video generator. * @brief Transform a black and white PNM image into the assembly code for the video generator.
* *
* Takes a black/white (1bpp) PNM ASCII-armored image as input, * Takes a black/white (1bpp) PNM ASCII-armored image as input,
* and produces the frame.S assembly code to draw it * and produces the frame.S assembly code to draw it
* as an analog video signal for this project's kernel. * as an analog video signal for this project's kernel.
* *
* @copyright Copyright retrofficina.glgprograms.it (c) 2021-2022 * @copyright Copyright retrofficina.glgprograms.it (c) 2021-2022
* *
*/ */
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <cstring> #include <cstring>
#include <spdlog/spdlog.h>
enum ExitStatus { enum ExitStatus
{
BAD_ARGUMENT = 1, BAD_ARGUMENT = 1,
FILE_NOT_OPEN = 2, FILE_NOT_OPEN = 2,
FILE_BAD_FORMAT = 3 FILE_BAD_FORMAT = 3
}; };
int main(int argc, char** argv) { int main(int argc, char **argv)
if (argc < 2) { {
std::cerr << "Usage: " << argv[0] << " <FILE>" << std::endl; // enable debug messages
spdlog::set_level(spdlog::level::debug);
// arguments parsing and sanity check
if (argc < 2)
{
spdlog::error("Usage: {} <FILE> ...", argv[0]);
return BAD_ARGUMENT; return BAD_ARGUMENT;
} }
const int images = argc - 1; const int images = argc - 1;
std::stringstream jump_table;
std::stringstream asm_code;
// Output assembly file content.
// Content is buffered into these stringstreams,
// and then dumped to a file on disk.
// Code is organized into two parts.
std::stringstream jump_table; // Assembly jump table for images.
// Each image is described by a jump table with 256 entries.
// Each entry is a jump to the code to produce a particular line on the screen.
std::stringstream asm_code; // Assembly code to describe lines.
// Each line is a sequence of a set and/or clear bit,
// conveniently timed to mangle the video signal at the right moment.
// Output file.
std::fstream outfile; std::fstream outfile;
outfile.open("frame.S", std::ios::out); outfile.open("frame.S", std::ios::out);
if (! outfile) { if (!outfile)
return FILE_NOT_OPEN; return FILE_NOT_OPEN;
}
// include headers
jump_table << "#include \"macro.h\"" << std::endl; jump_table << "#include \"macro.h\"" << std::endl;
jump_table << "#include <avr/io.h>" << std::endl; jump_table << "#include <avr/io.h>" << std::endl;
char** image = new char*[images]; // Pointer to images
char **image = new char *[images];
for (int current_image = 0; current_image < images; ++current_image) { // Parse images and convert in assembly code
for (int current_image = 0; current_image < images; ++current_image)
{
std::fstream infile; std::fstream infile;
infile.open(argv[1 + current_image], std::ios::in); infile.open(argv[1 + current_image], std::ios::in);
if (! infile) { if (!infile)
return FILE_NOT_OPEN; return FILE_NOT_OPEN;
}
int width, height; unsigned int width, height; // width and height of image
std::string format; std::string format; // image format
char c; char c; // current pixel
getline(infile, format); getline(infile, format);
spdlog::debug("format: {}", format);
std::cout << format << std::endl; if (format != "P1")
if (format != "P1") {
return FILE_BAD_FORMAT; return FILE_BAD_FORMAT;
}
// skip one line TODO // skip commented line / TODO in a proper way
getline(infile, format); getline(infile, format);
std::cout << format << std::endl; spdlog::debug("comment: {}", format);
// read width and height
infile >> width >> height; infile >> width >> height;
std::cout << width << " x " << height << std::endl; spdlog::debug("width x height = {} x {}", width, height);
// read full image from disk into memory
// in an organized row by column array
image[current_image] = new char[height * width]; image[current_image] = new char[height * width];
for (int y = 0; y < height;)
for (int y = 0; y < height; ) { {
for (int x = 0; x < width; ) { for (int x = 0; x < width;)
{
c = infile.get(); c = infile.get();
if (! infile.good())
// file is truncated
if (!infile.good())
return FILE_BAD_FORMAT; return FILE_BAD_FORMAT;
if (c == '0' || c == '1') {
// if current character represents a pixel, add to array...
if (c == '0' || c == '1')
{
image[current_image][y * width + x] = c; image[current_image][y * width + x] = c;
x++; x++;
} else {
continue;
} }
else // ... else ignore current char
continue;
} }
y++; y++;
} }
infile.close(); infile.close();
// add new jump table for current image
jump_table << ".global line_jump_table_" << current_image << std::endl; jump_table << ".global line_jump_table_" << current_image << std::endl;
jump_table << "line_jump_table_" << current_image << ":" << std::endl; jump_table << "line_jump_table_" << current_image << ":" << std::endl;
for (int y = 0; y < height; ++y) { // Inspect each line one by one
for (int y = 0; y < height; ++y)
{
int diff = 1; int diff = 1;
for (int other_image = 0; other_image <= current_image; ++other_image) { // Check if one of the previously inspected images has an identical line.
for (int other_y = 0; other_y < height && other_y < y; ++other_y) { // If it has an identical line, just add it in the current image jump table,
// instead of generating again an identical assembly code.
// This allows us to save a lot of program memory space.
// Note: previous lines of current image are good candidates to check too!
for (int other_image = 0; other_image <= current_image; ++other_image)
{
for (int other_y = 0; other_y < height && other_y < y; ++other_y)
{
diff = memcmp(&(image[current_image][y * width + 0]), &(image[other_image][other_y * width + 0]), width); diff = memcmp(&(image[current_image][y * width + 0]), &(image[other_image][other_y * width + 0]), width);
if (diff == 0) { if (diff == 0)
{
jump_table << '\t' << "jmp line_" << other_image << "_" << other_y << std::endl; jump_table << '\t' << "jmp line_" << other_image << "_" << other_y << std::endl;
goto nested_loop_end; goto nested_loop_end;
} }
} }
} }
nested_loop_end: nested_loop_end:
// Current line has already been found to be identical
// to one of the previous ones: then skip to next one.
if (diff == 0) if (diff == 0)
continue; continue;
// Add jump in the current image table.
jump_table << '\t' << "jmp line_" << current_image << "_" << y << std::endl; jump_table << '\t' << "jmp line_" << current_image << "_" << y << std::endl;
// Add line description.
asm_code << "line_" << current_image << "_" << y << ":" << std::endl; asm_code << "line_" << current_image << "_" << y << ":" << std::endl;
// Line description is a sequence of assembly code conveniently built
// in order to set/clear an output port bit at the right moment,
// and mangle the video signal in order to display the desired image.
// Code simulates an RLE scheme, exploting small conveniently timed loops,
// in order to save program space.
int count = 0; int count = 0;
char last = image[current_image][y * width + 0]; char last = image[current_image][y * width + 0];
for (int x = 0; x < width; ++x) { for (int x = 0; x < width; ++x)
{
char current = image[current_image][y * width + x]; char current = image[current_image][y * width + x];
std::cout << "current " << current << " last " << last << std::endl; // If current pixel is the same as previous one,
if (current == last) { // then just count++
if (current == last)
++count; ++count;
} else { else // current pixel is different from previous one
std::cout << "detected change" << std::endl; {
if (last == '0') { // Each pixel is 4 clock cycles long,
std::cout << "clear" << std::endl; // because It Just Works out perfectly with the instruction set.
asm_code << '\t' << "cbi IO(PORTB), 4" << std::endl;
} else if (last == '1') { // Two clock cycles are spent to set/clear the output port.
std::cout << "set" << std::endl; if (last == '0')
asm_code << '\t' << "sbi IO(PORTB), 4" << std::endl; {
asm_code << '\t' << "cbi IO(PORTB), 4" << std::endl; // +2
}
else if (last == '1')
{
asm_code << '\t' << "sbi IO(PORTB), 4" << std::endl; // +2
} }
if (count > 1) { // Loop is entered only if RLE is needed,
asm_code << '\t' << "ldi r31, " << count - 1 << std::endl; // eg. at least 2 subsequent pixels of the same color.
asm_code << "1:" << std::endl; if (count > 1)
asm_code << '\t' << "dec r31" << std::endl; { // clock cycles
asm_code << '\t' << "nop" << std::endl; asm_code << '\t' << "ldi r31, " << count - 1 << std::endl; // +1
asm_code << '\t' << "brne 1b" << std::endl; asm_code << "1:" << std::endl; // +0
asm_code << '\t' << "dec r31" << std::endl; // +1
asm_code << '\t' << "nop" << std::endl; // +1
asm_code << '\t' << "brne 1b" << std::endl; // +2 if taken, +1 if not
} }
asm_code << '\t' << "rjmp 2f" << std::endl; // Add 2 more dummy clock cycles.
asm_code << '\t' << "rjmp 2f" << std::endl; // +2
asm_code << "2:" << std::endl; asm_code << "2:" << std::endl;
last = current; last = current;
count = 1; count = 1;
} }
} }
// At the end of the line, clear output bit, and return from interrupt.
asm_code << '\t' << "cbi IO(PORTB), 4" << std::endl; asm_code << '\t' << "cbi IO(PORTB), 4" << std::endl;
asm_code << '\t' << "jmp jump_table_return_address" << std::endl; asm_code << '\t' << "jmp jump_table_return_address" << std::endl;
} }
} }
delete[] image; delete[] image;
// save the generated code on disk
outfile << jump_table.str() << asm_code.str(); outfile << jump_table.str() << asm_code.str();
outfile.close(); outfile.close();
} }

2
main.S
View File

@ -81,7 +81,7 @@ int_horizontal_sync: ; +3
jmp int_horizontal_sync_end jmp int_horizontal_sync_end
enter: enter:
; here, +23 or +24 cycles have passed since horizontal sync ; here, +23 cycles have passed since horizontal sync
; so, there are still ~168 cycles before first useful data ; so, there are still ~168 cycles before first useful data
ldi r31, 0 ldi r31, 0
sts TCNT0, r31 sts TCNT0, r31