Move I2CBus logic into I2C.cpp (without refactoring for devices)

This commit is contained in:
Pokechu22 2022-09-03 16:21:23 -07:00
parent 5f9648417c
commit 669cff6571
3 changed files with 334 additions and 317 deletions

View file

@ -4,25 +4,43 @@
#include "Common/I2C.h"
#include <algorithm>
#include <bitset>
#include <string_view>
#include <fmt/format.h>
#include "Common/Assert.h"
#include "Common/EnumFormatter.h"
#include "Common/Logging/Log.h"
#include "Core/Debugger/Debugger_SymbolMap.h"
namespace IOS
{
struct AVEState;
extern AVEState ave_state;
extern std::bitset<0x100> ave_ever_logged; // For logging only; not saved
std::string_view GetAVERegisterName(u8 address);
} // namespace IOS
namespace Common
{
void I2CBus::AddSlave(I2CSlave* slave)
void I2CBusOld::AddSlave(I2CSlave* slave)
{
m_slaves.emplace_back(slave);
}
void I2CBus::RemoveSlave(I2CSlave* slave)
void I2CBusOld::RemoveSlave(I2CSlave* slave)
{
m_slaves.erase(std::remove(m_slaves.begin(), m_slaves.end(), slave), m_slaves.end());
}
void I2CBus::Reset()
void I2CBusOld::Reset()
{
m_slaves.clear();
}
int I2CBus::BusRead(u8 slave_addr, u8 addr, int count, u8* data_out)
int I2CBusOld::BusRead(u8 slave_addr, u8 addr, int count, u8* data_out)
{
for (auto& slave : m_slaves)
{
@ -36,7 +54,7 @@ int I2CBus::BusRead(u8 slave_addr, u8 addr, int count, u8* data_out)
return 0;
}
int I2CBus::BusWrite(u8 slave_addr, u8 addr, int count, const u8* data_in)
int I2CBusOld::BusWrite(u8 slave_addr, u8 addr, int count, const u8* data_in)
{
for (auto& slave : m_slaves)
{
@ -50,4 +68,255 @@ int I2CBus::BusWrite(u8 slave_addr, u8 addr, int count, const u8* data_in)
return 0;
}
bool I2CBus::GetSCL() const
{
return true; // passive pullup - no clock stretching
}
bool I2CBus::GetSDA() const
{
switch (state)
{
case State::Inactive:
case State::Activating:
default:
return true; // passive pullup (or NACK)
case State::SetI2CAddress:
case State::WriteDeviceAddress:
case State::WriteToDevice:
if (bit_counter < 8)
return true; // passive pullup
else
return false; // ACK (if we need to NACK, we set the state to inactive)
case State::ReadFromDevice:
if (bit_counter < 8)
return ((current_byte << bit_counter) & 0x80) != 0;
else
return true; // passive pullup, receiver needs to ACK or NACK
}
}
void I2CBus::Start()
{
if (state != State::Inactive)
INFO_LOG_FMT(WII_IPC, "AVE: Re-start I2C");
else
INFO_LOG_FMT(WII_IPC, "AVE: Start I2C");
if (bit_counter != 0)
WARN_LOG_FMT(WII_IPC, "I2C: Start happened with a nonzero bit counter: {}", bit_counter);
state = State::Activating;
bit_counter = 0;
current_byte = 0;
i2c_address.reset();
// Note: don't reset device_address, as it's re-used for reads
}
void I2CBus::Stop()
{
INFO_LOG_FMT(WII_IPC, "AVE: Stop I2C");
state = State::Inactive;
bit_counter = 0;
current_byte = 0;
i2c_address.reset();
device_address.reset();
}
bool I2CBus::WriteExpected() const
{
// If we don't have an I²C address, it needs to be written (even if the address that is later
// written is a read).
// Otherwise, check the least significant bit; it being *clear* indicates a write.
const bool is_write = !i2c_address.has_value() || ((i2c_address.value() & 1) == 0);
// The device that is otherwise recieving instead transmits an acknowledge bit after each byte.
const bool acknowledge_expected = (bit_counter == 8);
return is_write ^ acknowledge_expected;
}
void I2CBus::Update(const bool old_scl, const bool new_scl, const bool old_sda, const bool new_sda)
{
if (old_scl != new_scl && old_sda != new_sda)
{
ERROR_LOG_FMT(WII_IPC, "Both SCL and SDA changed at the same time: SCL {} -> {} SDA {} -> {}",
old_scl, new_scl, old_sda, new_sda);
return;
}
if (old_scl == new_scl && old_sda == new_sda)
return; // Nothing changed
if (old_scl && new_scl)
{
// Check for changes to SDA while the clock is high.
if (old_sda && !new_sda)
{
// SDA falling edge (now pulled low) while SCL is high indicates I²C start
Start();
}
else if (!old_sda && new_sda)
{
// SDA rising edge (now passive pullup) while SCL is high indicates I²C stop
Stop();
}
}
else if (state != State::Inactive)
{
if (!old_scl && new_scl)
{
SCLRisingEdge(new_sda);
}
else if (old_scl && !new_scl)
{
SCLFallingEdge(new_sda);
}
}
}
void I2CBus::SCLRisingEdge(const bool sda)
{
// INFO_LOG_FMT(WII_IPC, "AVE: {} {} rising edge: {} (write expected: {})", bit_counter, state,
// sda, WriteExpected());
// SCL rising edge indicates data clocking. For reads, we set up data at this point.
// For writes, we instead process it on the falling edge, to better distinguish
// the start/stop condition.
if (state == State::ReadFromDevice && bit_counter == 0)
{
// Start of a read.
ASSERT(device_address.has_value()); // Implied by the state transition in falling edge
current_byte = reinterpret_cast<u8*>(&IOS::ave_state)[device_address.value()];
INFO_LOG_FMT(WII_IPC, "AVE: Read from {:02x} ({}) -> {:02x}", device_address.value(),
IOS::GetAVERegisterName(device_address.value()), current_byte);
}
// Dolphin_Debugger::PrintCallstack(Common::Log::LogType::WII_IPC, Common::Log::LogLevel::LINFO);
}
void I2CBus::SCLFallingEdge(const bool sda)
{
// INFO_LOG_FMT(WII_IPC, "AVE: {} {} falling edge: {} (write expected: {})", bit_counter, state,
// sda, WriteExpected());
// SCL falling edge is used to advance bit_counter/change states and process writes.
if (state == State::SetI2CAddress || state == State::WriteDeviceAddress ||
state == State::WriteToDevice)
{
if (bit_counter == 8)
{
// Acknowledge bit for *reads*.
if (sda)
{
WARN_LOG_FMT(WII_IPC, "Read NACK'd");
state = State::Inactive;
}
}
else
{
current_byte <<= 1;
if (sda)
current_byte |= 1;
if (bit_counter == 7)
{
INFO_LOG_FMT(WII_IPC, "AVE: Byte written: {:02x}", current_byte);
// Write finished.
if (state == State::SetI2CAddress)
{
if ((current_byte >> 1) != 0x70)
{
state = State::Inactive; // NACK
WARN_LOG_FMT(WII_IPC, "AVE: Unknown I2C address {:02x}", current_byte);
}
else
{
INFO_LOG_FMT(WII_IPC, "AVE: I2C address is {:02x}", current_byte);
}
}
else if (state == State::WriteDeviceAddress)
{
device_address = current_byte;
INFO_LOG_FMT(WII_IPC, "AVE: Device address is {:02x}", current_byte);
}
else
{
// Actual write
ASSERT(state == State::WriteToDevice);
ASSERT(device_address.has_value()); // implied by state transition
const u8 old_ave_value = reinterpret_cast<u8*>(&IOS::ave_state)[device_address.value()];
reinterpret_cast<u8*>(&IOS::ave_state)[device_address.value()] = current_byte;
if (!IOS::ave_ever_logged[device_address.value()] || old_ave_value != current_byte)
{
INFO_LOG_FMT(WII_IPC, "AVE: Wrote {:02x} to {:02x} ({})", current_byte,
device_address.value(), IOS::GetAVERegisterName(device_address.value()));
IOS::ave_ever_logged[device_address.value()] = true;
}
else
{
DEBUG_LOG_FMT(WII_IPC, "AVE: Wrote {:02x} to {:02x} ({})", current_byte,
device_address.value(), IOS::GetAVERegisterName(device_address.value()));
}
device_address = device_address.value() + 1;
}
}
}
}
if (state == State::Activating)
{
// This is triggered after the start condition.
state = State::SetI2CAddress;
bit_counter = 0;
}
else if (state != State::Inactive)
{
if (bit_counter >= 8)
{
// Finished a byte and the acknowledge signal.
bit_counter = 0;
switch (state)
{
case State::SetI2CAddress:
i2c_address = current_byte;
// Note: i2c_address is known to correspond to a valid device
if ((current_byte & 1) == 0)
{
state = State::WriteDeviceAddress;
device_address.reset();
}
else
{
if (device_address.has_value())
{
state = State::ReadFromDevice;
}
else
{
state = State::Inactive; // NACK - required for 8-bit internal addresses
ERROR_LOG_FMT(WII_IPC, "AVE: Attempted to read device without having a read address!");
}
}
break;
case State::WriteDeviceAddress:
state = State::WriteToDevice;
break;
}
}
else
{
bit_counter++;
}
}
// Dolphin_Debugger::PrintCallstack(Common::Log::LogType::WII_IPC, Common::Log::LogLevel::LINFO);
}
}; // namespace Common
template <>
struct fmt::formatter<Common::I2CBus::State> : EnumFormatter<Common::I2CBus::State::ReadFromDevice>
{
static constexpr array_type names = {"Inactive", "Activating",
"Set I2C Address", "Write Device Address",
"Write To Device", "Read From Device"};
constexpr formatter() : EnumFormatter(names) {}
};

View file

@ -4,17 +4,18 @@
#pragma once
#include <algorithm>
#include <optional>
#include <vector>
#include "Common/CommonTypes.h"
namespace Common
{
class I2CBus;
class I2CBusOld;
class I2CSlave
{
friend I2CBus;
friend I2CBusOld;
protected:
virtual ~I2CSlave() = default;
@ -55,7 +56,7 @@ protected:
}
};
class I2CBus
class I2CBusOld
{
public:
void AddSlave(I2CSlave* slave);
@ -72,4 +73,53 @@ private:
std::vector<I2CSlave*> m_slaves;
};
// An I²C bus implementation accessed via bit-banging.
// A few assumptions and limitations exist:
// - All devices support both writes and reads.
// - Timing is not implemented at all; the clock signal can be changed as fast as needed.
// - Devices are not allowed to stretch the clock signal. (Nintendo's write code does not seem to
// implement clock stretching in any case, though some homebrew does.)
// - All devices use a 1-byte auto-incrementing address which wraps around from 255 to 0.
// - The device address is handled by this I2CBus class, instead of the device itself.
// - The device address is set on writes, and re-used for reads; writing an address and data and
// then switching to reading uses the incremented address. Every write must specify the address.
// - Reading without setting the device address beforehand is disallowed; the I²C specification
// allows such reads but does not specify how they behave (or anything about the behavior of the
// device address).
// - Switching between multiple devices using a restart does not reset the device address; the
// device address is only reset on stopping. This means that a write to one device followed by a
// read from a different device would result in reading from the last used device address (without
// any warning).
// - 10-bit addressing and other reserved addressing modes are not implemented.
class I2CBus
{
public:
enum class State
{
Inactive,
Activating,
SetI2CAddress,
WriteDeviceAddress,
WriteToDevice,
ReadFromDevice,
};
State state;
u8 bit_counter;
u8 current_byte;
std::optional<u8> i2c_address; // Not shifted; includes the read flag
std::optional<u8> device_address;
void Update(const bool old_scl, const bool new_scl, const bool old_sda, const bool new_sda);
bool GetSCL() const;
bool GetSDA() const;
private:
void Start();
void Stop();
void SCLRisingEdge(const bool sda);
void SCLFallingEdge(const bool sda);
bool WriteExpected() const;
};
} // namespace Common

View file

@ -9,11 +9,10 @@
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/EnumFormatter.h"
#include "Common/I2C.h"
#include "Common/Logging/Log.h"
#include "Core/Core.h"
#include "Core/CoreTiming.h"
#include "Core/Debugger/Debugger_SymbolMap.h"
#include "Core/HW/DVD/DVDInterface.h"
#include "Core/HW/MMIO.h"
#include "Core/HW/ProcessorInterface.h"
@ -121,59 +120,10 @@ struct AVEState
};
#pragma pack()
static_assert(sizeof(AVEState) == 0x100);
static AVEState ave_state;
static std::bitset<sizeof(AVEState)> ave_ever_logged; // For logging only; not saved
AVEState ave_state;
std::bitset<sizeof(AVEState)> ave_ever_logged;
// An I²C bus implementation accessed via bit-banging.
// A few assumptions and limitations exist:
// - All devices support both writes and reads.
// - Timing is not implemented at all; the clock signal can be changed as fast as needed.
// - Devices are not allowed to stretch the clock signal. (Nintendo's write code does not seem to
// implement clock stretching in any case, though some homebrew does.)
// - All devices use a 1-byte auto-incrementing address which wraps around from 255 to 0.
// - The device address is handled by this I2CBus class, instead of the device itself.
// - The device address is set on writes, and re-used for reads; writing an address and data and
// then switching to reading uses the incremented address. Every write must specify the address.
// - Reading without setting the device address beforehand is disallowed; the I²C specification
// allows such reads but does not specify how they behave (or anything about the behavior of the
// device address).
// - Switching between multiple devices using a restart does not reset the device address; the
// device address is only reset on stopping. This means that a write to one device followed by a
// read from a different device would result in reading from the last used device address (without
// any warning).
// - 10-bit addressing and other reserved addressing modes are not implemented.
class I2CBus
{
public:
enum class State
{
Inactive,
Activating,
SetI2CAddress,
WriteDeviceAddress,
WriteToDevice,
ReadFromDevice,
};
State state;
u8 bit_counter;
u8 current_byte;
std::optional<u8> i2c_address; // Not shifted; includes the read flag
std::optional<u8> device_address;
void Update(Core::System& system, const bool old_scl, const bool new_scl, const bool old_sda,
const bool new_sda);
bool GetSCL() const;
bool GetSDA() const;
private:
void Start();
void Stop();
void SCLRisingEdge(const bool sda);
void SCLFallingEdge(const bool sda);
bool WriteExpected() const;
};
I2CBus i2c_state;
Common::I2CBus i2c_state;
static CoreTiming::EventType* updateInterrupts;
@ -247,7 +197,7 @@ void WiiIPC::Shutdown()
{
}
static std::string_view GetAVERegisterName(u8 address)
std::string_view GetAVERegisterName(u8 address)
{
if (address == 0x00)
return "A/V Timings";
@ -300,36 +250,6 @@ static u32 ReadGPIOIn(Core::System& system)
return gpio_in.m_hex;
}
bool I2CBus::GetSCL() const
{
return true; // passive pullup - no clock stretching
}
bool I2CBus::GetSDA() const
{
switch (state)
{
case State::Inactive:
case State::Activating:
default:
return true; // passive pullup (or NACK)
case State::SetI2CAddress:
case State::WriteDeviceAddress:
case State::WriteToDevice:
if (bit_counter < 8)
return true; // passive pullup
else
return false; // ACK (if we need to NACK, we set the state to inactive)
case State::ReadFromDevice:
if (bit_counter < 8)
return ((current_byte << bit_counter) & 0x80) != 0;
else
return true; // passive pullup, receiver needs to ACK or NACK
}
}
u32 WiiIPC::GetGPIOOut()
{
// In the direction field, a '1' bit for a pin indicates that it will behave as an output (drive),
@ -352,226 +272,13 @@ void WiiIPC::GPIOOutChanged(u32 old_value_hex)
}
// I²C logic for the audio/video encoder (AVE)
i2c_state.Update(m_system, old_value[GPIO::AVE_SCL], new_value[GPIO::AVE_SCL],
old_value[GPIO::AVE_SDA], new_value[GPIO::AVE_SDA]);
i2c_state.Update(old_value[GPIO::AVE_SCL], new_value[GPIO::AVE_SCL], old_value[GPIO::AVE_SDA],
new_value[GPIO::AVE_SDA]);
// SENSOR_BAR is checked by WiimoteEmu::CameraLogic
// TODO: SLOT_LED
}
void I2CBus::Start()
{
if (state != State::Inactive)
INFO_LOG_FMT(WII_IPC, "AVE: Re-start I2C");
else
INFO_LOG_FMT(WII_IPC, "AVE: Start I2C");
if (bit_counter != 0)
WARN_LOG_FMT(WII_IPC, "I2C: Start happened with a nonzero bit counter: {}", bit_counter);
state = State::Activating;
bit_counter = 0;
current_byte = 0;
i2c_address.reset();
// Note: don't reset device_address, as it's re-used for reads
}
void I2CBus::Stop()
{
INFO_LOG_FMT(WII_IPC, "AVE: Stop I2C");
state = State::Inactive;
bit_counter = 0;
current_byte = 0;
i2c_address.reset();
device_address.reset();
}
bool I2CBus::WriteExpected() const
{
// If we don't have an I²C address, it needs to be written (even if the address that is later
// written is a read).
// Otherwise, check the least significant bit; it being *clear* indicates a write.
const bool is_write = !i2c_address.has_value() || ((i2c_address.value() & 1) == 0);
// The device that is otherwise recieving instead transmits an acknowledge bit after each byte.
const bool acknowledge_expected = (bit_counter == 8);
return is_write ^ acknowledge_expected;
}
void I2CBus::Update(Core::System& system, const bool old_scl, const bool new_scl,
const bool old_sda, const bool new_sda)
{
if (old_scl != new_scl && old_sda != new_sda)
{
ERROR_LOG_FMT(WII_IPC, "Both SCL and SDA changed at the same time: SCL {} -> {} SDA {} -> {}",
old_scl, new_scl, old_sda, new_sda);
return;
}
if (old_scl == new_scl && old_sda == new_sda)
return; // Nothing changed
if (old_scl && new_scl)
{
// Check for changes to SDA while the clock is high.
if (old_sda && !new_sda)
{
// SDA falling edge (now pulled low) while SCL is high indicates I²C start
Start();
}
else if (!old_sda && new_sda)
{
// SDA rising edge (now passive pullup) while SCL is high indicates I²C stop
Stop();
}
}
else if (state != State::Inactive)
{
if (!old_scl && new_scl)
{
SCLRisingEdge(new_sda);
}
else if (old_scl && !new_scl)
{
SCLFallingEdge(new_sda);
}
}
}
void I2CBus::SCLRisingEdge(const bool sda)
{
// INFO_LOG_FMT(WII_IPC, "AVE: {} {} rising edge: {} (write expected: {})", bit_counter, state,
// sda, WriteExpected());
// SCL rising edge indicates data clocking. For reads, we set up data at this point.
// For writes, we instead process it on the falling edge, to better distinguish
// the start/stop condition.
if (state == State::ReadFromDevice && bit_counter == 0)
{
// Start of a read.
ASSERT(device_address.has_value()); // Implied by the state transition in falling edge
current_byte = reinterpret_cast<u8*>(&ave_state)[device_address.value()];
INFO_LOG_FMT(WII_IPC, "AVE: Read from {:02x} ({}) -> {:02x}", device_address.value(),
GetAVERegisterName(device_address.value()), current_byte);
}
// Dolphin_Debugger::PrintCallstack(Common::Log::LogType::WII_IPC, Common::Log::LogLevel::LINFO);
}
void I2CBus::SCLFallingEdge(const bool sda)
{
// INFO_LOG_FMT(WII_IPC, "AVE: {} {} falling edge: {} (write expected: {})", bit_counter, state,
// sda, WriteExpected());
// SCL falling edge is used to advance bit_counter/change states and process writes.
if (state == State::SetI2CAddress || state == State::WriteDeviceAddress ||
state == State::WriteToDevice)
{
if (bit_counter == 8)
{
// Acknowledge bit for *reads*.
if (sda)
{
WARN_LOG_FMT(WII_IPC, "Read NACK'd");
state = State::Inactive;
}
}
else
{
current_byte <<= 1;
if (sda)
current_byte |= 1;
if (bit_counter == 7)
{
INFO_LOG_FMT(WII_IPC, "AVE: Byte written: {:02x}", current_byte);
// Write finished.
if (state == State::SetI2CAddress)
{
if ((current_byte >> 1) != 0x70)
{
state = State::Inactive; // NACK
WARN_LOG_FMT(WII_IPC, "AVE: Unknown I2C address {:02x}", current_byte);
}
else
{
INFO_LOG_FMT(WII_IPC, "AVE: I2C address is {:02x}", current_byte);
}
}
else if (state == State::WriteDeviceAddress)
{
device_address = current_byte;
INFO_LOG_FMT(WII_IPC, "AVE: Device address is {:02x}", current_byte);
}
else
{
// Actual write
ASSERT(state == State::WriteToDevice);
ASSERT(device_address.has_value()); // implied by state transition
const u8 old_ave_value = reinterpret_cast<u8*>(&ave_state)[device_address.value()];
reinterpret_cast<u8*>(&ave_state)[device_address.value()] = current_byte;
if (!ave_ever_logged[device_address.value()] || old_ave_value != current_byte)
{
INFO_LOG_FMT(WII_IPC, "AVE: Wrote {:02x} to {:02x} ({})", current_byte,
device_address.value(), GetAVERegisterName(device_address.value()));
ave_ever_logged[device_address.value()] = true;
}
else
{
DEBUG_LOG_FMT(WII_IPC, "AVE: Wrote {:02x} to {:02x} ({})", current_byte,
device_address.value(), GetAVERegisterName(device_address.value()));
}
device_address = device_address.value() + 1;
}
}
}
}
if (state == State::Activating)
{
// This is triggered after the start condition.
state = State::SetI2CAddress;
bit_counter = 0;
}
else if (state != State::Inactive)
{
if (bit_counter >= 8)
{
// Finished a byte and the acknowledge signal.
bit_counter = 0;
switch (state)
{
case State::SetI2CAddress:
i2c_address = current_byte;
// Note: i2c_address is known to correspond to a valid device
if ((current_byte & 1) == 0)
{
state = State::WriteDeviceAddress;
device_address.reset();
}
else
{
if (device_address.has_value())
{
state = State::ReadFromDevice;
}
else
{
state = State::Inactive; // NACK - required for 8-bit internal addresses
ERROR_LOG_FMT(WII_IPC, "AVE: Attempted to read device without having a read address!");
}
}
break;
case State::WriteDeviceAddress:
state = State::WriteToDevice;
break;
}
}
else
{
bit_counter++;
}
}
// Dolphin_Debugger::PrintCallstack(Common::Log::LogType::WII_IPC, Common::Log::LogLevel::LINFO);
}
void WiiIPC::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
{
mmio->Register(base | IPC_PPCMSG, MMIO::InvalidRead<u32>(), MMIO::DirectWrite<u32>(&m_ppc_msg));
@ -746,12 +453,3 @@ bool WiiIPC::IsReady() const
((m_ppc_irq_flags & INT_CAUSE_IPC_BROADWAY) == 0));
}
} // namespace IOS
template <>
struct fmt::formatter<IOS::I2CBus::State> : EnumFormatter<IOS::I2CBus::State::ReadFromDevice>
{
static constexpr array_type names = {"Inactive", "Activating",
"Set I2C Address", "Write Device Address",
"Write To Device", "Read From Device"};
constexpr formatter() : EnumFormatter(names) {}
};