417 lines
12 KiB
C

/*
* This file is part of open_agb_firm
* Copyright (C) 2023 derrek, profi200
*
* 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 3 of the License, 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. If not, see <http://www.gnu.org/licenses/>.
*/
#include <math.h>
#include "types.h"
#include "arm11/drivers/lcd.h"
#include "arm11/drivers/hw_cal.h"
#include "fs.h"
#include "debug.h"
#include "drivers/gfx.h"
#include "arm11/drivers/timer.h"
#include "fb_assert.h"
#include "arm11/drivers/i2c.h"
#define LCD_BL_TIMEOUT (10u)
// Track if the topscreen parallax barrier is on (3D mode).
static bool g_parallaxOn = false;
void LCD_init(u8 mcuLcdOnMask, const u32 lcdLum)
{
// Disable frame buffer output to prevent glitches.
LCD_setForceBlack(true, true);
// Setup PWM for the parallax barrier. Disabled by default.
LcdRegs *const lcd = getLcdRegs();
lcd->parallax_cnt = 0;
lcd->parallax_pwm = PARALLAX_PWM_TIMING(0xA39, 0xA39);
// TODO: Should we force both backlight PWM off here since we only modify them as needed below?
// Reset LCD drivers. Unknown for how long this must be low.
// gsp seems to rely on boot11/previous FIRM having set reset to active.
lcd->rst = LCD_RST_RST; // TODO: Not needed?
//lcd->signal_cnt = SIGNAL_CNT_BOTH_DIS; // Stop H-/V-sync control signals.
//TIMER_sleepMs(100);
// TODO: Does it even make sense to check the LCD on bit?
// LCDs must always be initialized anyway.
if(mcuLcdOnMask & MCU_LCD_PWR_ON)
{
// ---------------------------------------------------------------- //
// Timing critical block. Must be done within 4 frames.
// ---------------------------------------------------------------- //
// TODO: If we start using threading raise thread priority temporarily.
// Take LCD drivers out of reset.
lcd->rst = LCD_RST_NORST;
// At this point the output must be forced black or
// the LCD drivers will not sync. Already done above.
//LCD_setForceBlack(true, true);
// Route H-/V-sync signals to the LCDs.
lcd->signal_cnt = 0;
// Wait for power supply to stabilize and LCD drivers to finish resetting.
TIMER_sleepMs(10);
// Initialize LCD drivers.
LCDI2C_init();
// Power on LCDs (MCU --> PMIC).
MCU_setLcdPower(MCU_LCD_PWR_ON);
// ---------------------------------------------------------------- //
// Timing critical block end.
// ---------------------------------------------------------------- //
// Wait 50 µs for LCD sync. The MCU event wait will cover this.
if(MCU_waitIrqs(MCU_LCD_IRQ_MASK) != MCU_IRQ_LCD_POWER_ON) panic();
}
if(mcuLcdOnMask & ~(MCU_LCD_PWR_ON))
{
// Wait for the backlight on flag on both LCDs.
LCDI2C_waitBacklightsOn();
// TODO: ABL/other enhancements.
// Setup Backlight PWM. ABL is disabled for now.
// Note: PWM duty is set after PWM is enabled in gsp.
// There should be no harm if we set duty first.
lcd->abl0.cnt = 0; // ABL disabled for steady PWM output.
lcd->abl1.cnt = 0; // ABL disabled for steady PWM output.
LCD_setLuminance(lcdLum);
mcuLcdOnMask &= ~(MCU_LCD_PWR_ON);
LCD_setBacklightPower(mcuLcdOnMask);
// TODO: Wait for VBlank on both LCDs to prevent flickering.
// Turn the 3D LED off.
// TODO: What about the parallax barrier?
MCU_set3dLedState(0);
}
}
void LCD_deinit(const u8 mcuLcdOffMask)
{
// Disable the IR LED on N3DS (PIT)?
// Disable frame buffer output to prevent glitches.
LCD_setForceBlack(true, true);
// Disable parallax barrier and 3D LED.
//g_parallaxOn = false;
//LCD_setParallaxBarrier(false);
MCU_set3dLedState(0); // TODO: Move to GFX driver?
// Power down LCD backlights and disable backlight PWM.
// 2 separate steps in gsp.
LCD_setBacklightPower(mcuLcdOffMask & ~MCU_LCD_PWR_OFF);
// Power down LCDs.
LcdRegs *const lcd = getLcdRegs();
GFX_waitForVBlank0();
GFX_waitForVBlank0(); // Wait 2 VBlanks to make sure the LCDs are black.
lcd->rst = LCD_RST_RST;
lcd->signal_cnt = SIGNAL_CNT_BOTH_DIS;
if(mcuLcdOffMask & MCU_LCD_PWR_OFF)
{
MCU_setLcdPower(MCU_LCD_PWR_OFF);
if(MCU_waitIrqs(MCU_LCD_IRQ_MASK) != MCU_IRQ_LCD_POWER_OFF) panic();
}
}
void LCD_setBacklightPower(u8 mcuBlMask)
{
// Check if we have LCD power bits set.
// Also check if we have power on and off bits set at the same time.
fb_assert((mcuBlMask & (MCU_LCD_PWR_ON | MCU_LCD_PWR_OFF)) == 0);
fb_assert((mcuBlMask & (MCU_LCD_PWR_TOP_BL_ON | MCU_LCD_PWR_TOP_BL_OFF)) < (MCU_LCD_PWR_TOP_BL_ON | MCU_LCD_PWR_TOP_BL_OFF));
fb_assert((mcuBlMask & (MCU_LCD_PWR_BOT_BL_ON | MCU_LCD_PWR_BOT_BL_OFF)) < (MCU_LCD_PWR_BOT_BL_ON | MCU_LCD_PWR_BOT_BL_OFF));
// Ignore top LCD backlight bits on o2DS.
// On o2DS we have 1 big display panel and
// bottom LCD bits control the whole panel.
if(MCU_getSystemModel() == SYS_MODEL_2DS)
mcuBlMask &= ~(MCU_LCD_PWR_TOP_BL_ON | MCU_LCD_PWR_TOP_BL_OFF);
// Enable backlight PWM before we turn on backlights.
// This is necessary to avoid the open circuit protection from PMIC.
// TODO: With certain, very old MCU firmwares PWM prescaler is 6 and denominator 0xFF.
LcdRegs *const lcd = getLcdRegs();
if(mcuBlMask & MCU_LCD_PWR_TOP_BL_ON)
{
lcd->abl0.bl_pwm_cnt = BL_PWM_EN | BL_PWM_PRESCALER(0) | BL_PWM_DENOMINATOR(0x23E);
}
if(mcuBlMask & MCU_LCD_PWR_BOT_BL_ON)
{
lcd->abl1.bl_pwm_cnt = BL_PWM_EN | BL_PWM_PRESCALER(0) | BL_PWM_DENOMINATOR(0x23E);
}
MCU_setLcdPower(mcuBlMask);
if(MCU_waitIrqs(MCU_LCD_IRQ_MASK) != MCU_LCD_PWR_2_IRQ(mcuBlMask)) panic();
// Disable backligh PWM after turning off backlights.
// Same reason as for power on.
if(mcuBlMask & MCU_LCD_PWR_TOP_BL_OFF)
{
lcd->abl0.bl_pwm_cnt = 0;
}
if(mcuBlMask & MCU_LCD_PWR_BOT_BL_OFF)
{
lcd->abl1.bl_pwm_cnt = 0;
}
}
static u16 getPwmDenominator(const u8 lcd)
{
LcdRegs *const lcdRegs = getLcdRegs();
vu32 *const ptr = (lcd == 0 ? &lcdRegs->abl0.bl_pwm_cnt : &lcdRegs->abl1.bl_pwm_cnt);
const u32 reg = *ptr;
return (reg & BL_PWM_EN ? reg & BL_PWM_DENOMINATOR_MASK : 511u) + 1;
}
// Converts luminance (cd/m²) to PWM duty.
static u16 lum2Brightness(const u32 lum, const float coeffs[3], const float base, const u16 min)
{
// ax² + bx + c = y
const float l = (float)lum;
float y = coeffs[0] * l * l + coeffs[1] * l + coeffs[2];
y = (y <= min ? min : y) / base;
return (u16)(y + 0.5f);
}
// Converts PWM duty to luminance (cd/m²).
// Original calculation from gsp disassembly.
/*static u32 brightness2lum(const u16 bright, const float coeffs[3], const float base)
{
const float x = (float)bright * base;
const float bb = coeffs[1] * coeffs[1];
const float ac4 = coeffs[0] * (x - coeffs[2]) * 4.f;
const float l = (sqrtf(bb + ac4) - coeffs[1]) / (coeffs[0] + coeffs[0]);
return (u32)(l + 0.5f);
}
// Adjusted variant based on code from Luma3DS.
static u32 brightness2lum(const u16 bright, const float coeffs[3], const float base)
{
const float x = (float)bright * base;
const float bb = coeffs[1] * coeffs[1];
const float _4ac = 4.f * coeffs[0] * (coeffs[2] - x);
const float l = (sqrtf(bb - _4ac) - coeffs[1]) / (coeffs[0] + coeffs[0]);
return (u32)(l + 0.5f);
}*/
void LCD_setLuminance(u32 lum)
{
lum = (lum > g_blPwmCal.lumLevels[6] ? g_blPwmCal.lumLevels[6] : lum);
lum = (lum < g_blPwmCal.lumLevels[0] ? g_blPwmCal.lumLevels[0] : lum);
// TODO: ABL RS lookup table stuff.
// This calculation is currently a nop since we hardcoded ratio to 1.0.
/*const float lumRatio[2] = {1.f, 1.f};
u32 lumTop = (float)lum * lumRatio[0];
lumTop = (lumTop < g_blPwmCal.lumLevels[0] ? g_blPwmCal.lumLevels[0] : lumTop);
u32 lumBot = (float)lum * lumRatio[1];
lumBot = (lumBot < g_blPwmCal.lumLevels[0] ? g_blPwmCal.lumLevels[0] : lumBot);*/
const u32 lumTop = lum;
const u32 lumBot = lum;
LcdRegs *const lcd = getLcdRegs();
const float baseTop = (float)g_blPwmCal.hwBrightnessBase / getPwmDenominator(0);
const float baseBot = (float)g_blPwmCal.hwBrightnessBase / getPwmDenominator(1);
const u16 minBright = g_blPwmCal.hwBrightnessMin;
lcd->abl0.bl_pwm_duty = lum2Brightness(lumTop, g_blPwmCal.coeffs[g_parallaxOn ? 2 : 1], baseTop, minBright);
lcd->abl1.bl_pwm_duty = lum2Brightness(lumBot, g_blPwmCal.coeffs[0], baseBot, minBright);
}
void LCD_setLuminanceLevel(u8 level)
{
const u16 numLevels = g_blPwmCal.numLevels;
level = (level < 1 ? 1 : (level > numLevels ? numLevels : level));
// TODO: When the charger is plugged in gsp boosts brightness.
#if 0
if(isChargerPluggedIn)
{
static const u8 boostLut[] = {0, 1, 2, 3, 5, 6};
level = boostLut[level];
}
#endif
LCD_setLuminance(g_blPwmCal.lumLevels[level - 1]);
}
void LCD_setParallaxBarrier(const bool on)
{
LcdRegs *const lcd = getLcdRegs();
if(on)
{
// TODO: Parallax barrier on delay from HWCAL.
/*if(onDelay > 0)
{
// Update luminance for 3D mode.
// TODO
TIMER_sleepMs(onDelay);
}*/
lcd->parallax_cnt = PARALLAX_CNT_PWM1_EN | PARALLAX_CNT_PWM0_EN;
g_parallaxOn = true;
// TODO: Parallax barrier on delay from HWCAL.
// Why is there a delay before and after in gsp??
/*if(onDelay <= 0)
{
if(onDelay < 0) TIMER_sleepMs(-onDelay);
// Update luminance for 3D mode.
// TODO
}*/
}
else
{
// TODO: Parallax barrier off delay from HWCAL.
/*if(offDelay > 0)
{
// Update luminance for 2D mode.
// TODO
TIMER_sleepMs(offDelay);
}*/
lcd->parallax_cnt = 0;
g_parallaxOn = false;
// TODO: Parallax barrier off delay from HWCAL.
// Why is there a delay before and after in gsp??
/*if(offDelay <= 0)
{
if(offDelay < 0) TIMER_sleepMs(-offDelay);
// Update luminance for 2D mode.
// TODO
}*/
}
// Note: 3D LED is usually turned on/off after the barrier.
}
// ---------------------------------------------------------------- //
u8 LCDI2C_readReg(const u8 lcd, const LcdI2cReg reg)
{
u8 buf[2];
const I2cDevice dev = (lcd == 0 ? I2C_DEV_LCD1 : I2C_DEV_LCD2);
bool res = I2C_write(dev, LCD_I2C_REG_READ_ADDR, reg);
if(res)
{
res = I2C_readArray(dev, LCD_I2C_REG_READ_ADDR, buf, 2);
}
return (res ? buf[1] : 0xFF);
}
bool LCDI2C_writeReg(const u8 lcd, const LcdI2cReg reg, const u8 data)
{
const I2cDevice dev = (lcd == 0 ? I2C_DEV_LCD1 : I2C_DEV_LCD2);
return I2C_write(dev, reg, data);
}
void LCDI2C_init(void)
{
const u16 revs = LCDI2C_getRevisions();
// Top LCD.
if(revs & 0xFFu)
{
LCDI2C_writeReg(0, LCD_I2C_REG_RST_STATUS, LCD_REG_RST_STATUS_NONE);
}
else
{
LCDI2C_writeReg(0, LCD_I2C_REG_UNK11, LCD_REG_UNK11_UNK10);
LCDI2C_writeReg(0, LCD_I2C_REG_HS_SERIAL, LCD_REG_HS_SERIAL_ON);
}
// Bottom LCD.
if(revs>>8)
{
LCDI2C_writeReg(1, LCD_I2C_REG_RST_STATUS, LCD_REG_RST_STATUS_NONE);
}
else
{
LCDI2C_writeReg(1, LCD_I2C_REG_UNK11, LCD_REG_UNK11_UNK10);
}
LCDI2C_writeReg(0, LCD_I2C_REG_STATUS, LCD_REG_STATUS_OK); // Initialize status flag.
LCDI2C_writeReg(1, LCD_I2C_REG_STATUS, LCD_REG_STATUS_OK); // Initialize status flag.
LCDI2C_writeReg(0, LCD_I2C_REG_POWER, LCD_REG_POWER_ON); // Power on LCD.
LCDI2C_writeReg(1, LCD_I2C_REG_POWER, LCD_REG_POWER_ON); // Power on LCD.
}
void LCDI2C_waitBacklightsOn(void)
{
const u16 revs = LCDI2C_getRevisions();
if((revs & 0xFFu) == 0 || (revs>>8) == 0)
{
// Bug workaround for early LCD driver revisions?
TIMER_sleepMs(150);
}
else
{
u32 i = 0;
do
{
const u8 top = LCDI2C_readReg(0, LCD_I2C_REG_BL_STATUS);
const u8 bot = LCDI2C_readReg(1, LCD_I2C_REG_BL_STATUS);
if(top == LCD_REG_BL_STATUS_ON && bot == LCD_REG_BL_STATUS_ON) break;
TIMER_sleepUs(33333);
} while(++i < LCD_BL_TIMEOUT);
}
}
u16 LCDI2C_getRevisions(void)
{
static bool lcdRevsRead = false;
static u16 lcdRevsCache = 0;
if(!lcdRevsRead)
{
lcdRevsRead = true;
lcdRevsCache = LCDI2C_readReg(0, LCD_I2C_REG_REVISION) |
(u16)LCDI2C_readReg(1, LCD_I2C_REG_REVISION)<<8;
}
return lcdRevsCache;
}