portapack-mayhem/firmware/common/portapack_io.hpp
2024-02-17 18:44:06 -06:00

457 lines
13 KiB
C++

/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyleft (ɔ) 2024 zxkmm under GPL license
* Copyright (C) 2024 u-foka
* Copyright (C) 2024 Mark Thompson
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __PORTAPACK_IO_H__
#define __PORTAPACK_IO_H__
#include <cstdint>
#include <cstddef>
#include <array>
#include "platform.hpp"
#include "gpio.hpp"
#include "ui.hpp"
// #include "portapack_persistent_memory.hpp"
// Darkened pixel bit mask for each possible shift value.
static const uint16_t darken_mask[4] = {
0b1111111111111111, // RrrrrGgggggBbbbb
0b0111101111101111, // 0Rrrr0Ggggg0Bbbb
0b0011100111100111, // 00Rrr00Gggg00Bbb
0b0001100011100011 // 000Rr000Ggg000Bb
};
// To darken, dividing each color level R/G/B by 2^shift.
#define DARKENED_PIXEL(pixel, shift) ((pixel >> shift) & darken_mask[shift])
// To un-darken, multiply each color level by 2^shift (might still be darker that before since some bits may have been lost above).
// This function will only be called when the pixel has previously been darkened, so no masking is needed.
#define UNDARKENED_PIXEL(pixel, shift) (pixel << shift)
namespace portapack {
class IO {
public:
enum class TouchPinsConfig : uint8_t {
XN_BIT = (1 << 0),
XP_BIT = (1 << 1),
YN_BIT = (1 << 2),
YP_BIT = (1 << 3),
XN_OE = (1 << 4),
XP_OE = (1 << 5),
YN_OE = (1 << 6),
YP_OE = (1 << 7),
XN_IN = XN_BIT,
XN_OUT_1 = XN_OE | XN_BIT,
XN_OUT_0 = XN_OE,
XP_IN = XP_BIT,
XP_OUT_1 = XP_OE | XP_BIT,
XP_OUT_0 = XP_OE,
YN_IN = YN_BIT,
YN_OUT_1 = YN_OE | YN_BIT,
YN_OUT_0 = YN_OE,
YP_IN = YP_BIT,
YP_OUT_1 = YP_OE | YP_BIT,
YP_OUT_0 = YP_OE,
/* Allow pins to be pulled up by CPLD pull-ups. */
Float = XP_IN | XN_IN | YP_IN | YN_IN,
/* Drive one plane to 0V, other plane is pulled up. Watch for when pulled-up
* plane falls to ~0V.
*/
WaitTouch = XP_OUT_0 | XN_OUT_0 | YP_IN | YN_IN,
/* Create a voltage divider between X plane, touch resistance, Y plane. */
SensePressure = XP_IN | XN_OUT_0 | YP_OUT_1 | YN_IN,
/* Create a voltage divider across X plane, read voltage from Y plane. */
SenseX = XP_OUT_1 | XN_OUT_0 | YP_IN | YN_IN,
/* Create a voltage divider across Y plane, read voltage from X plane. */
SenseY = XP_IN | XN_IN | YP_OUT_1 | YN_OUT_0,
};
constexpr IO(
GPIO gpio_dir,
GPIO gpio_lcd_rdx,
GPIO gpio_lcd_wrx,
GPIO gpio_io_stbx,
GPIO gpio_addr,
GPIO gpio_rot_a,
GPIO gpio_rot_b)
: gpio_dir{gpio_dir},
gpio_lcd_rdx{gpio_lcd_rdx},
gpio_lcd_wrx{gpio_lcd_wrx},
gpio_io_stbx{gpio_io_stbx},
gpio_addr{gpio_addr},
gpio_rot_a{gpio_rot_a},
gpio_rot_b{gpio_rot_b} {};
void init();
void lcd_backlight(const bool value);
void lcd_reset_state(const bool active);
void audio_reset_state(const bool active);
void reference_oscillator(const bool enable);
void lcd_data_write_command_and_data(
const uint_fast8_t command,
const uint8_t* data,
const size_t data_count) {
lcd_command(command);
for (size_t i = 0; i < data_count; i++) {
lcd_write_data(data[i]);
}
}
void lcd_data_write_command_and_data(
const uint_fast8_t command,
const std::initializer_list<uint8_t>& data) {
lcd_command(command);
for (const auto d : data) {
lcd_write_data(d);
}
}
void lcd_data_read_command_and_data(
const uint_fast8_t command,
uint16_t* const data,
const size_t data_count) {
lcd_command(command);
for (size_t i = 0; i < data_count; i++) {
data[i] = lcd_read_data();
}
}
void lcd_write_word(const uint32_t w) {
lcd_write_data(w);
}
void lcd_write_words(const uint16_t* const w, size_t n) {
for (size_t i = 0; i < n; i++) {
lcd_write_data(w[i]);
}
}
void lcd_write_pixel(ui::Color pixel) {
if (get_dark_cover()) {
pixel.v = DARKENED_PIXEL(pixel.v, get_brightness());
}
lcd_write_data(pixel.v);
}
uint32_t lcd_read_word() {
return lcd_read_data();
}
void lcd_write_pixels(ui::Color pixel, size_t n) {
if (get_dark_cover()) {
pixel.v = DARKENED_PIXEL(pixel.v, get_brightness());
}
while (n--) {
lcd_write_data(pixel.v);
}
}
void lcd_write_pixels_unrolled8(ui::Color pixel, size_t n) {
if (get_dark_cover()) {
pixel.v = DARKENED_PIXEL(pixel.v, get_brightness());
}
auto v = pixel.v;
n >>= 3;
while (n--) {
lcd_write_data(v);
lcd_write_data(v);
lcd_write_data(v);
lcd_write_data(v);
lcd_write_data(v);
lcd_write_data(v);
lcd_write_data(v);
lcd_write_data(v);
}
}
void lcd_write_pixels(const ui::Color* const pixels, size_t n) {
for (size_t i = 0; i < n; i++) {
lcd_write_pixel(pixels[i]);
}
}
void lcd_read_bytes(uint8_t* byte, size_t byte_count) {
size_t word_count = byte_count / 2;
while (word_count) {
const auto word = lcd_read_data();
*(byte++) = word >> 8;
*(byte++) = word >> 0;
word_count--;
}
if (byte_count & 1) {
const auto word = lcd_read_data();
*(byte++) = word >> 8;
}
}
uint32_t io_read() {
io_stb_assert();
dir_read();
addr_0();
__asm__("nop");
__asm__("nop");
__asm__("nop");
const auto switches_raw = data_read();
io_stb_deassert();
return switches_raw;
}
bool get_dark_cover();
uint8_t get_brightness();
// TODO: cache the value ^^ & ^ to increaase performance, need a trigger cuz init doesn't work. And since the constructor is constexpr, we can't use with in class var to cache it. maybe cache from outside somewhere and pass it here as argument.
uint32_t io_update(const TouchPinsConfig write_value);
uint32_t lcd_te() {
return gpio_rot_a.read();
}
uint32_t dfu_read() {
return gpio_rot_b.read();
}
private:
const GPIO gpio_dir;
const GPIO gpio_lcd_rdx;
const GPIO gpio_lcd_wrx;
const GPIO gpio_io_stbx;
const GPIO gpio_addr;
const GPIO gpio_rot_a;
const GPIO gpio_rot_b;
static constexpr ioportid_t gpio_data_port_id = 3;
static constexpr size_t gpio_data_shift = 8;
static constexpr ioportmask_t gpio_data_mask = 0xffU << gpio_data_shift;
uint8_t io_reg{0x03};
void lcd_rd_assert() {
gpio_lcd_rdx.clear();
}
void lcd_rd_deassert() {
gpio_lcd_rdx.set();
}
void lcd_wr_assert() {
gpio_lcd_wrx.clear();
}
void lcd_wr_deassert() {
gpio_lcd_wrx.set();
}
void io_stb_assert() {
gpio_io_stbx.clear();
}
void io_stb_deassert() {
gpio_io_stbx.set();
}
void addr(const bool value) {
gpio_addr.write(value);
}
void addr_1() {
gpio_addr.set();
}
void addr_0() {
gpio_addr.clear();
}
void data_mask_set() {
LPC_GPIO->MASK[gpio_data_port_id] = ~gpio_data_mask;
}
void dir_write() {
gpio_dir.clear();
LPC_GPIO->DIR[gpio_data_port_id] |= gpio_data_mask;
/* TODO: Manipulating DIR[3] makes me queasy. The RFFC5072 DATA pin
* is also on port 3, and switches direction periodically...
* Time to resort to bit-banding to enforce atomicity? But then, how
* to change direction on eight bits efficiently? Or do I care, since
* the PortaPack data bus shouldn't change direction too frequently?
*/
}
void dir_read() {
LPC_GPIO->DIR[gpio_data_port_id] &= ~gpio_data_mask;
gpio_dir.set();
}
void data_write_low(const uint32_t value) {
LPC_GPIO->MPIN[gpio_data_port_id] = (value << gpio_data_shift);
}
void data_write_high(const uint32_t value) {
LPC_GPIO->MPIN[gpio_data_port_id] = value;
}
uint32_t data_read() {
return (LPC_GPIO->MPIN[gpio_data_port_id] >> gpio_data_shift) & 0xffU;
}
void lcd_command(const uint32_t value) {
data_write_high(0); /* Drive high byte (with zero -- don't care) */
dir_write(); /* Turn around data bus, MCU->CPLD */
addr(0); /* Indicate command */
__asm__("nop");
__asm__("nop");
__asm__("nop");
lcd_wr_assert(); /* Latch high byte */
data_write_low(value); /* Drive low byte (pass-through) */
__asm__("nop");
__asm__("nop");
__asm__("nop");
lcd_wr_deassert(); /* Complete write operation */
addr(1); /* Set up for data phase (most likely after a command) */
}
// void high_contrast(ui::Color& pixel, size_t contrast_level_shift) { // TODO
// uint16_t r = (pixel.v >> 11) & 0x1F;
// uint16_t g = (pixel.v >> 5) & 0x3F;
// uint16_t b = pixel.v & 0x1F;
//
// if ((r << contrast_level_shift) > 0x1F) { // should be slightly smaller, need more obverse...
// r = 0x1F;
// } else {
// r = r << contrast_level_shift;
// }
//
// if ((g << contrast_level_shift) > 0x3F) { // same as above
// g = 0x3F;
// } else {
// g = g << contrast_level_shift;
// }
//
// if ((b << contrast_level_shift) > 0x1F) { // same as above
// b = 0x1F;
// } else {
// b = b << contrast_level_shift;
// }
//
// pixel.v = (r << 11) | (g << 5) | b;
// }
//
// void gray_scale(ui::Color& pixel) { // TODO: the blackwhite not looks right....
// uint16_t r = (pixel.v >> 11) & 0x1F;
// uint16_t g = (pixel.v >> 5) & 0x3F;
// uint16_t b = pixel.v & 0x1F;
//
// uint16_t average = (r + g + b) / 3;
//
// pixel.v = (average << 11) | (average << 5) | average;
// }
void lcd_write_data(const uint32_t value) __attribute__((always_inline)) {
// NOTE: Assumes and DIR=0 and ADDR=1 from command phase.
data_write_high(value); /* Drive high byte */
__asm__("nop");
lcd_wr_assert(); /* Latch high byte */
data_write_low(value); /* Drive low byte (pass-through) */
__asm__("nop");
__asm__("nop");
__asm__("nop");
lcd_wr_deassert(); /* Complete write operation */
}
uint32_t lcd_read_data() {
// NOTE: Assumes ADDR=1 from command phase.
dir_read();
/* Start read operation */
lcd_rd_assert();
/* Wait for passthrough data(15:8) to settle -- ~16ns (3 cycles) typical */
/* Wait for read control L duration (355ns) */
halPolledDelay(71); // 355ns
const auto value_high = data_read();
/* Latch data[7:0] */
lcd_rd_deassert();
/* Wait for latched data[7:0] to settle -- ~26ns (5 cycles) typical */
/* Wait for read control H duration (90ns) */
halPolledDelay(18); // 90ns
const auto value_low = data_read();
uint32_t original_value = (value_high << 8) | value_low;
if (get_dark_cover()) {
original_value = UNDARKENED_PIXEL(original_value, get_brightness());
}
return original_value;
}
void io_write(const bool address, const uint_fast16_t value) {
data_write_low(value);
dir_write();
addr(address);
__asm__("nop");
__asm__("nop");
__asm__("nop");
io_stb_assert();
__asm__("nop");
__asm__("nop");
__asm__("nop");
io_stb_deassert();
}
/*
void lcd_data_write_command_and_data(
const uint_fast16_t command,
const uint8_t* const data,
const size_t count
) {
lcd_data_write_command(command);
for(size_t i=0; i<count; i++) {
lcd_data_write_data(data[i]);
}
}
*/
};
extern IO io;
} /* namespace portapack */
#endif /*__PORTAPACK_IO_H__*/