Compare commits
5 Commits
bc1fdfd34b
...
a49143de8f
Author | SHA1 | Date |
---|---|---|
giomba | a49143de8f | |
giomba | a3fc600996 | |
giomba | d645c1ada2 | |
giomba | 11c051cfab | |
giomba | b84d15e7f3 |
|
@ -3,4 +3,4 @@ datasheet/
|
|||
output/
|
||||
frame.S
|
||||
a.out
|
||||
|
||||
frame
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Linux",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**",
|
||||
"/usr/avr/include"
|
||||
],
|
||||
"defines": [
|
||||
"__AVR_ATmega328P__"
|
||||
],
|
||||
"compilerPath": "/usr/bin/clang",
|
||||
"cStandard": "c17",
|
||||
"intelliSenseMode": "linux-clang-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
14
Makefile
14
Makefile
|
@ -24,7 +24,6 @@ OUTPUT_DIR = output
|
|||
SRCS += main.c
|
||||
|
||||
# asm files
|
||||
# S maiuscola! Invoca prima il compilatore gcc che interpreta macro e altro
|
||||
ASRC += main.S
|
||||
ASRC += frame.S
|
||||
|
||||
|
@ -68,6 +67,8 @@ GDB = avr-gdb
|
|||
AS = avr-as
|
||||
SIZE = avr-size
|
||||
AVRDUDE = avrdude
|
||||
HOSTCC = gcc
|
||||
HOSTCXX = g++
|
||||
# Custom script for enum parsing
|
||||
# PACK2STRING = python3 bin/enum2string.py
|
||||
|
||||
|
@ -108,6 +109,10 @@ CFLAGS += -flto
|
|||
CXXFLAGS = $(CFLAGS)
|
||||
CXXFLAGS += -std=c++11
|
||||
|
||||
# Host CXX flags
|
||||
HOSTCXXFLAGS := `pkgconf -cflags spdlog`
|
||||
HOSTLDFLAGS := `pkgconf -libs spdlog`
|
||||
|
||||
# Linker flags
|
||||
LFLAGS = -mmcu=$(MCU)
|
||||
LFLAGS += $(addprefix -I,$(INC_DIR))
|
||||
|
@ -139,6 +144,13 @@ all: $(OUTPUT_DIR)/$(PROJ_NAME).hex
|
|||
|
||||
-include $(DEPS)
|
||||
|
||||
# frame
|
||||
frame.S: frame
|
||||
./frame samples/retrofficina.pbm
|
||||
|
||||
frame: frame.cpp
|
||||
$(HOSTCXX) $(HOSTCXXFLAGS) $< $(HOSTLDFLAGS) -o $@
|
||||
|
||||
# invokes CC compiler before assemblying
|
||||
$(BUILD_DIR)/%.S.o : %.S
|
||||
@echo -e "\033[1;33m[Assembling ]\033[0m AS $<"
|
||||
|
|
187
frame.cpp
187
frame.cpp
|
@ -1,151 +1,216 @@
|
|||
/* frame.cpp
|
||||
* Takes a black/white (1bpp) PNM ASCII-armored image as input,
|
||||
* and produces the frame.S assembly code to draw it
|
||||
* as an analog video signal for this project's kernel.
|
||||
* */
|
||||
/**
|
||||
* @file frame.cpp
|
||||
* @author giomba@glgprograms.it
|
||||
* @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,
|
||||
* and produces the frame.S assembly code to draw it
|
||||
* as an analog video signal for this project's kernel.
|
||||
*
|
||||
* @copyright Copyright retrofficina.glgprograms.it (c) 2021-2022
|
||||
*
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
enum ExitStatus {
|
||||
enum ExitStatus
|
||||
{
|
||||
BAD_ARGUMENT = 1,
|
||||
FILE_NOT_OPEN = 2,
|
||||
FILE_BAD_FORMAT = 3
|
||||
};
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
outfile.open("frame.S", std::ios::out);
|
||||
if (! outfile) {
|
||||
if (!outfile)
|
||||
return FILE_NOT_OPEN;
|
||||
}
|
||||
|
||||
// include headers
|
||||
jump_table << "#include \"macro.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;
|
||||
|
||||
infile.open(argv[1 + current_image], std::ios::in);
|
||||
if (! infile) {
|
||||
if (!infile)
|
||||
return FILE_NOT_OPEN;
|
||||
}
|
||||
|
||||
int width, height;
|
||||
std::string format;
|
||||
char c;
|
||||
unsigned int width, height; // width and height of image
|
||||
std::string format; // image format
|
||||
char c; // current pixel
|
||||
|
||||
getline(infile, format);
|
||||
|
||||
std::cout << format << std::endl;
|
||||
|
||||
if (format != "P1") {
|
||||
spdlog::debug("format: {}", format);
|
||||
if (format != "P1")
|
||||
return FILE_BAD_FORMAT;
|
||||
}
|
||||
|
||||
// skip one line TODO
|
||||
// skip commented line / TODO in a proper way
|
||||
getline(infile, format);
|
||||
std::cout << format << std::endl;
|
||||
spdlog::debug("comment: {}", format);
|
||||
|
||||
// read width and 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];
|
||||
|
||||
for (int y = 0; y < height; ) {
|
||||
for (int x = 0; x < width; ) {
|
||||
for (int y = 0; y < height;)
|
||||
{
|
||||
for (int x = 0; x < width;)
|
||||
{
|
||||
c = infile.get();
|
||||
if (! infile.good())
|
||||
|
||||
// file is truncated
|
||||
if (!infile.good())
|
||||
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;
|
||||
x++;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
else // ... else ignore current char
|
||||
continue;
|
||||
}
|
||||
y++;
|
||||
}
|
||||
|
||||
infile.close();
|
||||
|
||||
// add new jump table for current image
|
||||
jump_table << ".global 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;
|
||||
|
||||
for (int other_image = 0; other_image <= current_image; ++other_image) {
|
||||
for (int other_y = 0; other_y < height && other_y < y; ++other_y) {
|
||||
// Check if one of the previously inspected images has an identical line.
|
||||
// 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);
|
||||
if (diff == 0) {
|
||||
if (diff == 0)
|
||||
{
|
||||
jump_table << '\t' << "jmp line_" << other_image << "_" << other_y << std::endl;
|
||||
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)
|
||||
continue;
|
||||
|
||||
// Add jump in the current image table.
|
||||
jump_table << '\t' << "jmp line_" << current_image << "_" << y << std::endl;
|
||||
|
||||
// Add line description.
|
||||
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;
|
||||
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];
|
||||
std::cout << "current " << current << " last " << last << std::endl;
|
||||
if (current == last) {
|
||||
// If current pixel is the same as previous one,
|
||||
// then just count++
|
||||
if (current == last)
|
||||
++count;
|
||||
} else {
|
||||
std::cout << "detected change" << std::endl;
|
||||
if (last == '0') {
|
||||
std::cout << "clear" << std::endl;
|
||||
asm_code << '\t' << "cbi IO(PORTB), 4" << std::endl;
|
||||
} else if (last == '1') {
|
||||
std::cout << "set" << std::endl;
|
||||
asm_code << '\t' << "sbi IO(PORTB), 4" << std::endl;
|
||||
else // current pixel is different from previous one
|
||||
{
|
||||
// Each pixel is 4 clock cycles long,
|
||||
// because It Just Works out perfectly with the instruction set.
|
||||
|
||||
// Two clock cycles are spent to set/clear the output port.
|
||||
if (last == '0')
|
||||
{
|
||||
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) {
|
||||
asm_code << '\t' << "ldi r31, " << count - 1 << std::endl;
|
||||
asm_code << "1:" << std::endl;
|
||||
asm_code << '\t' << "dec r31" << std::endl;
|
||||
asm_code << '\t' << "nop" << std::endl;
|
||||
asm_code << '\t' << "brne 1b" << std::endl;
|
||||
// Loop is entered only if RLE is needed,
|
||||
// eg. at least 2 subsequent pixels of the same color.
|
||||
if (count > 1)
|
||||
{ // clock cycles
|
||||
asm_code << '\t' << "ldi r31, " << count - 1 << std::endl; // +1
|
||||
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;
|
||||
|
||||
last = current;
|
||||
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' << "jmp jump_table_return_address" << std::endl;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
delete[] image;
|
||||
|
||||
// save the generated code on disk
|
||||
outfile << jump_table.str() << asm_code.str();
|
||||
|
||||
outfile.close();
|
||||
}
|
||||
|
|
53
main.S
53
main.S
|
@ -1,12 +1,14 @@
|
|||
; main.S
|
||||
;
|
||||
; This file is part of OSDY VideoCharGen
|
||||
; a do-it-yourself on-screen-display image generator
|
||||
; for superimposition of analog PAL signals
|
||||
;
|
||||
#include <avr/io.h>
|
||||
#include "macro.h"
|
||||
#include "const.h"
|
||||
|
||||
|
||||
.data
|
||||
image:
|
||||
.byte 0xff
|
||||
|
||||
show_image:
|
||||
.byte 0x00
|
||||
|
||||
|
@ -18,48 +20,19 @@ current_jump_table:
|
|||
|
||||
.text
|
||||
|
||||
.global main
|
||||
main:
|
||||
ldi r16, 0x30 ; port B, pin 4 and 5 as output, others as input
|
||||
sts DDRB, r16
|
||||
|
||||
; set interrupt vectors at address 0x0, not bootloader
|
||||
; timing is important, see atmel datasheet
|
||||
ldi r16, (1 << IVCE)
|
||||
ldi r17, 0
|
||||
out IO(MCUCR), r16
|
||||
out IO(MCUCR), r17
|
||||
|
||||
ldi r16, 0xa ; external interrupt 0 and 1, falling edge
|
||||
sts EICRA, r16
|
||||
|
||||
ldi r16, 0x3 ; external interrupt 0 and 1, mask enable
|
||||
sts EIMSK, r16
|
||||
|
||||
ldi r16, 0x02 ; don't connect output pins to timer, CTC[1:0] mode
|
||||
sts TCCR0A, r16
|
||||
ldi r16, 0x01 ; CTC[2] mode, no prescaler
|
||||
sts TCCR0B, r16
|
||||
|
||||
.global main_asm
|
||||
main_asm:
|
||||
; init variables
|
||||
ldi r16, 0
|
||||
sts frame, r16
|
||||
sts frame + 1, r16
|
||||
sts line, r16
|
||||
ldi r16, 0xfe
|
||||
sts line + 1, r16
|
||||
ldi r16, 0xff
|
||||
sts image, r16
|
||||
ldi r16, 0
|
||||
sts show_image, r16
|
||||
|
||||
; r0 always holds 0
|
||||
clr r0
|
||||
|
||||
call setup_c
|
||||
|
||||
sei ; global interrupt enable
|
||||
; global interrupt enable
|
||||
sei
|
||||
|
||||
; endless loop
|
||||
1:
|
||||
rjmp 1b
|
||||
|
||||
|
@ -81,7 +54,7 @@ int_horizontal_sync: ; +3
|
|||
jmp int_horizontal_sync_end
|
||||
|
||||
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
|
||||
ldi r31, 0
|
||||
sts TCNT0, r31
|
||||
|
@ -182,7 +155,7 @@ check_if_released:
|
|||
in r31, IO(PINB)
|
||||
andi r31, 0x04
|
||||
; if transition mode == 1, then always show image
|
||||
; TODO an huge shitty spaghetti code
|
||||
; Nice to have: maybe this can be written better.
|
||||
breq 1f
|
||||
ldi r31, 0
|
||||
sts show_image, r31
|
||||
|
|
51
main.c
51
main.c
|
@ -1,21 +1,54 @@
|
|||
#include <avr/io.h>
|
||||
#include <avr/interrupt.h>
|
||||
/**
|
||||
* @file main.c
|
||||
* @author giomba@glgprograms.it
|
||||
* @brief OSDY main.
|
||||
*
|
||||
* @copyright Copyright RetrOfficina GLG Programs (c) 2022
|
||||
*
|
||||
* Video image generator superimposition for analog PAL signals,
|
||||
* with Atmega328, for your retro OSD titles.
|
||||
*
|
||||
*/
|
||||
#include "const.h"
|
||||
#include <avr/interrupt.h>
|
||||
#include <avr/io.h>
|
||||
#include <string.h>
|
||||
|
||||
volatile uint16_t frame;
|
||||
volatile uint16_t line;
|
||||
volatile uint16_t frame = 0;
|
||||
volatile uint16_t line = 0xfe;
|
||||
volatile uint8_t image = 0xff;
|
||||
|
||||
ISR(INT0_vect, ISR_NAKED) {
|
||||
asm("jmp int_vertical_sync");
|
||||
// vertical sync interrupt
|
||||
asm("jmp int_vertical_sync");
|
||||
}
|
||||
ISR(INT1_vect, ISR_NAKED) {
|
||||
asm("jmp int_horizontal_sync");
|
||||
// horizontal sync interrupt
|
||||
asm("jmp int_horizontal_sync");
|
||||
}
|
||||
ISR(TIMER0_COMPA_vect, ISR_NAKED) {
|
||||
asm("jmp int_timer_0");
|
||||
// back porch timer interrupt
|
||||
asm("jmp int_timer_0");
|
||||
}
|
||||
|
||||
void setup_c() {
|
||||
}
|
||||
// external assembly main loop
|
||||
void main_asm(void);
|
||||
|
||||
int main() {
|
||||
// port B, pin 4 and 5 as output, others as input
|
||||
DDRB |= 0x30;
|
||||
|
||||
// set interrupt vectors at address 0x0, not bootloader
|
||||
// timing is important, see atmel datasheet
|
||||
_SFR_IO8(MCUCR) = (1 << IVCE);
|
||||
_SFR_IO8(MCUCR) = 0;
|
||||
|
||||
EICRA = 0x0a; // external interrupt 0 and 1, falling edge
|
||||
EIMSK = 0x03; // external interrupt 0 and 1, mask enable
|
||||
|
||||
TCCR0A = 0x02; // don't connect output pins to timer, CTC[1:0] mode
|
||||
|
||||
TCCR0B = 0x01; // CTC[2] mode, no prescaler
|
||||
|
||||
main_asm();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue