diff --git a/src/Emulator/Peripherals/Peripherals/I2C/STM32F7_I2C_Master.cs b/src/Emulator/Peripherals/Peripherals/I2C/STM32F7_I2C_Master.cs new file mode 100644 index 000000000..b27d377b9 --- /dev/null +++ b/src/Emulator/Peripherals/Peripherals/I2C/STM32F7_I2C_Master.cs @@ -0,0 +1,550 @@ +// +// Copyright (c) 2010-2025 Antmicro +// +// This file is licensed under the MIT License. +// Full license text is available in 'licenses/MIT.txt'. +// April 2026: The driver has been updated to ensure proper functionality +// in master mode when an STM32H7xx workaround fixing +// "Transmission stalled after first byte transfer" is applied +// by I2C driver in the FW (zephyr/drivers/i2c/i2c_ll_stm32_v2.c) +using System.Collections.Generic; + +using Antmicro.Renode.Core; +using Antmicro.Renode.Core.Structure; +using Antmicro.Renode.Core.Structure.Registers; +using Antmicro.Renode.Logging; +using Antmicro.Renode.Peripherals.Bus; +using Antmicro.Renode.Utilities; + +namespace Antmicro.Renode.Peripherals.I2C +{ + public sealed class STM32F7_I2C_Master : SimpleContainer, II2CPeripheral, IDoubleWordPeripheral, IKnownSize + { + public STM32F7_I2C_Master(IMachine machine) : base(machine) + { + EventInterrupt = new GPIO(); + ErrorInterrupt = new GPIO(); + registers = CreateRegisters(); + Reset(); + } + + public uint ReadDoubleWord(long offset) + { + return registers.Read(offset); + } + + public void WriteDoubleWord(long offset, uint value) + { + registers.Write(offset, value); + } + + public override void Reset() + { + registers.Reset(); + txData = new Queue(); + rxData = new Queue(); + currentSlaveAddress = 0; + transferOutgoing = false; + EventInterrupt.Unset(); + ErrorInterrupt.Unset(); + masterMode = false; + ResetPendingData(); + } + + public void Write(byte[] data) + { + // RM0444 Rev 5, p.991/1390 + // "0: Write transfer, slave enters receiver mode." + transferOutgoing = false; + + rxData.EnqueueRange(data); + } + + public byte[] Read(int count = 1) + { + if(!addressMatched.Value) + { + // Note 1: + // RM0444 Rev 5, p.991/1390 + // "1: Read transfer, slave enters transmitter mode." + // Note 2: + // this is a workaround for the protocol not supporting start/stop bits + transferOutgoing = (count > 0); + bytesToTransfer.Value = (uint)count; + addressMatched.Value = true; + Update(); + } + + if(txData.Count >= (int)bytesToTransfer.Value) + { + // STOP condition + stopDetection.Value = true; + transmitInterruptStatus = false; + addressMatched.Value = false; + Update(); + } + else + { + // TODO: return partial results + return new byte[0]; + } + + var result = new byte[count]; + for(var i = 0; i < count; i++) + { + if(!txData.TryDequeue(out result[i])) + { + return new byte[0]; + } + } + return result; + } + + public void FinishTransmission() + { + } + + public long Size { get { return 0x400; } } + + public GPIO EventInterrupt { get; private set; } + + public GPIO ErrorInterrupt { get; private set; } + + public bool RxNotEmpty => rxData.Count > 0; + + public bool OwnAddress1Enabled => ownAddress1Enabled.Value; + + private DoubleWordRegisterCollection CreateRegisters() + { + var map = new Dictionary { { + (long)Registers.Control1, new DoubleWordRegister(this) + .WithFlag(0, writeCallback: PeripheralEnabledWrite, name: "PE") + .WithFlag(1, out transferInterruptEnabled, name: "TXIE") + .WithFlag(2, out receiveInterruptEnabled, name: "RXIE") + .WithFlag(3, out addressMatchedInterruptEnabled, name: "ADDRIE") + .WithFlag(4, out nackReceivedInterruptEnabled, name: "NACKIE") + .WithFlag(5, out stopDetectionInterruptEnabled, name: "STOPIE") + .WithFlag(6, out transferCompleteInterruptEnabled, name: "TCIE") + // Hush unsuppoted ERRIE (Error Interrupt Enable) for quiter logs + .WithIgnoredBits(7, 1) + .WithTag("DNF", 8, 4) + .WithTag("ANFOFF", 12, 1) + .WithReservedBits(13, 1) + .WithTag("TXDMAEN", 14, 1) + .WithTag("RXDMAEN", 15, 1) + .WithTag("SBC", 16, 1) + .WithFlag(17, out noStretch, name: "NOSTRETCH") + .WithTag("WUPEN", 18, 1) + .WithTag("GCEN", 19, 1) + .WithTag("SMBHEN", 20, 1) + .WithTag("SMBDEN", 21, 1) + .WithTag("ALERTEN", 22, 1) + .WithTag("PECEN", 23, 1) + .WithReservedBits(24, 8) + .WithChangeCallback((_,__) => Update()) + }, { + (long)Registers.Control2, + new DoubleWordRegister(this) + .WithValueField(0, 10, out slaveAddress, name: "SADD") //Changing this from a normal field to a callback requires a change in StartWrite + .WithFlag(10, out isReadTransfer, name: "RD_WRN") + .WithFlag(11, out use10BitAddressing, name: "ADD10") + .WithTag("HEAD10R", 12, 1) + .WithFlag(13, out start, name: "START") + .WithFlag(14, out stop, name: "STOP") + .WithTag("NACK", 15, 1) + .WithValueField(16, 8, out bytesToTransfer, name: "NBYTES") + .WithFlag(24, out reload, name: "RELOAD") + .WithFlag(25, out autoEnd, name: "AUTOEND") + .WithTag("PECBYTE", 26, 1) + .WithReservedBits(27, 5) + .WithWriteCallback((oldVal, newVal) => + { + uint oldStart = (oldVal >> 13) & 1; + uint oldBytesToTransfer = (oldVal >> 16) & 0xFF; + + if(start.Value && stop.Value) + { + this.Log(LogLevel.Warning, "Setting START and STOP at the same time, ignoring the transfer"); + } + else if(start.Value) + { + StartTransfer(); + } + else if(stop.Value) + { + StopTransfer(); + } + + if(!start.Value) + { + if(bytesToTransfer.Value > 0 && masterMode && transferCompleteReload.Value && currentSlave != null) + { + ExtendTransfer(); + } + } + + if(oldStart == 1) + { + if(oldBytesToTransfer != bytesToTransfer.Value) + { + this.Log(LogLevel.Error, "Changing NBYTES when START is set is not permitted"); + } + } + + start.Value = false; + stop.Value = false; + }) + .WithChangeCallback((_,__) => Update()) + }, { + (long)Registers.OwnAddress1, new DoubleWordRegister(this) + .WithValueField(0, 10, out ownAddress1, name: "OA1") + .WithFlag(10, out ownAddress1Mode, name: "OA1MODE") + .WithReservedBits(11, 4) + .WithFlag(15, out ownAddress1Enabled, name: "OA1EN") + .WithReservedBits(16, 16) + .WithWriteCallback((_, val) => + this.Log(LogLevel.Info, "Slave address 1: 0x{0:X}, mode: {1}, status: {2}", ownAddress1.Value, ownAddress1Mode.Value ? "10-bit" : "7-bit", ownAddress1Enabled.Value ? "enabled" : "disabled") + ) + }, { + (long)Registers.OwnAddress2, new DoubleWordRegister(this) + .WithReservedBits(0, 1) + .WithValueField(1, 7, out ownAddress2, name: "OA2") + .WithValueField(8, 3, out ownAddress2Mask, name: "OA2MSK") + .WithReservedBits(11, 4) + .WithFlag(15, out ownAddress2Enabled, name: "OA2EN") + .WithReservedBits(16, 16) + .WithWriteCallback((_, val) => + this.Log(LogLevel.Info, "Slave address 2: 0x{0:X}, mask: 0x{1:X}, status: {2}", ownAddress2.Value, ownAddress2Mask.Value, ownAddress2Enabled.Value ? "enabled" : "disabled") + ) + }, { + (long)Registers.Timing, new DoubleWordRegister(this) + .WithTag("SCLL", 0, 8) + .WithTag("SCLH", 8, 8) + .WithTag("SDADEL", 16, 4) + .WithTag("SCLDEL", 20, 4) + .WithReservedBits(24, 4) + .WithTag("PRESC", 28, 4) + }, { + (long)Registers.InterruptAndStatus, new DoubleWordRegister(this, 1) + .WithFlag(0, + valueProviderCallback: _ => txData.Count == 0, + writeCallback: (_, value)=> + { + if(value) + { + txData.Clear(); + } + }, name: "TXE") + .WithFlag(1, + valueProviderCallback: _ => transmitInterruptStatus, + writeCallback: (_, val) => + { + if(!noStretch.Value) + { + return; + } + transmitInterruptStatus = val && transferInterruptEnabled.Value; + } , name: "TXIS") + .WithFlag(2, FieldMode.Read, valueProviderCallback: _ => RxNotEmpty, name: "RXNE") + .WithFlag(3, out addressMatched, FieldMode.Read, name: "ADDR") + .WithTag("NACKF", 4, 1) + .WithFlag(5, out stopDetection, FieldMode.Read, name: "STOPF") + .WithFlag(6, out transferComplete, FieldMode.Read, name: "TC") + .WithFlag(7, out transferCompleteReload, FieldMode.Read, name: "TCR") + .WithTag("BERR", 8, 1) + .WithTag("ARLO", 9, 1) + .WithTag("OVR", 10, 1) + .WithTag("PECERR", 11, 1) + .WithTag("TIMEOUT", 12, 1) + .WithTag("ALERT", 13, 1) + .WithReservedBits(14, 1) + .WithTag("BUSY", 15, 1) + .WithFlag(16, FieldMode.Read, valueProviderCallback: _ => transferOutgoing, name: "DIR") + .WithTag("ADDCODE", 17, 7) + .WithReservedBits(24, 8) + .WithChangeCallback((_,__) => Update()) + }, { + (long)Registers.InterruptClear, new DoubleWordRegister(this, 0) + .WithReservedBits(0, 3) + .WithFlag(3, FieldMode.WriteOneToClear, + writeCallback: (_, value) => + { + if(value) + { + transmitInterruptStatus = transferOutgoing & (txData.Count == 0); + addressMatched.Value = false; + } + }, name: "ADDRCF") + .WithTag("NACKCF", 4, 1) + .WithFlag(5, FieldMode.WriteOneToClear, + writeCallback: (_, value) => + { + if(value) + { + stopDetection.Value = false; + } + }, name: "STOPCF") + .WithReservedBits(6, 2) + .WithTag("BERRCF", 8, 1) + .WithTag("ARLOCF", 9, 1) + .WithTag("OVRCF", 10, 1) + .WithTag("PECCF", 11, 1) + .WithTag("TIMOUTCF", 12, 1) + .WithTag("ALERTCF", 13, 1) + .WithReservedBits(14, 18) + .WithChangeCallback((_,__) => Update()) + }, { + (long)Registers.ReceiveData, new DoubleWordRegister(this, 0) + .WithValueField(0, 8, FieldMode.Read, valueProviderCallback: _ => ReceiveDataRead(), name: "RXDATA") + .WithReservedBits(9, 23) + }, { + (long)Registers.TransmitData, new DoubleWordRegister(this, 0) + .WithValueField(0, 8, writeCallback: (_, val) => HandleTransmitDataWrite((uint)val), name: "TXDATA") + .WithReservedBits(9, 23) + } + }; + + return new DoubleWordRegisterCollection(this, map); + } + + private void PeripheralEnabledWrite(bool oldValue, bool newValue) + { + if(newValue) + { + return; + } + stopDetection.Value = false; + transferComplete.Value = false; + transferCompleteReload.Value = false; + transmitInterruptStatus = false; + } + + private void ExtendTransfer() + { + //in case of reads we can fetch data from peripheral immediately, but in case of writes we have to wait until something is written to TXDATA + if(isReadTransfer.Value) + { + var data = currentSlave.Read((int)bytesToTransfer.Value); + foreach(var item in data) + { + rxData.Enqueue(item); + } + } + transferCompleteReload.Value = false; + Update(); + } + + private void StartTransfer() + { + masterMode = true; + transferComplete.Value = false; + + currentSlave = null; + + rxData.Clear(); + //This is kinda volatile. If we change slaveAddress setting to a callback action, it might not be set at this moment. + currentSlaveAddress = (int)(use10BitAddressing.Value ? slaveAddress.Value : ((slaveAddress.Value >> 1) & 0x7F)); + if(!TryGetByAddress(currentSlaveAddress, out currentSlave)) + { + this.Log(LogLevel.Warning, "Unknown slave at address {0}.", currentSlaveAddress); + return; + } + + if(isReadTransfer.Value) + { + transmitInterruptStatus = false; + var data = currentSlave.Read((int)bytesToTransfer.Value); + foreach(var item in data) + { + rxData.Enqueue(item); + } + } + else + { + transmitInterruptStatus = true; + } + Update(); + + // Handle pending Tx data (if actual) once Transfer is started + HandlePendingDataWrite(); + } + + private void StopTransfer() + { + masterMode = false; + stopDetection.Value = true; + ResetPendingData(); + currentSlave?.FinishTransmission(); + Update(); + } + + private uint ReceiveDataRead() + { + if(rxData.Count > 0) + { + var value = rxData.Dequeue(); + if(rxData.Count == 0) + { + SetTransferCompleteFlags(); //TC/TCR is set when NBYTES data have been transfered + } + return value; + } + this.Log(LogLevel.Warning, "Receive buffer underflow!"); + return 0; + } + + private void PendTransmitDataWrite(uint DataByte) + { + isPendingTxData = true; + pendingDataByte = DataByte; + } + + private void HandlePendingDataWrite() + { + if (isPendingTxData) + { + // Transfer pending data byte + MasterTransmitDataWrite(pendingDataByte); + ResetPendingData(); + } + } + + private void ResetPendingData() + { + // Clear pending data byte and pending flag + isPendingTxData = false; + pendingDataByte = 0; + } + + private void HandleTransmitDataWrite(uint newValue) + { + if(masterMode) + { + MasterTransmitDataWrite(newValue); + } + else + { + // only master mode is supported for now + // if the byte has been pushed for Tx before StartTransfer, it + // will be handled once transaction is initiated by master + PendTransmitDataWrite(newValue); + } + } + + private void MasterTransmitDataWrite(uint newValue) + { + if(currentSlave == null) + { + this.Log(LogLevel.Warning, "Trying to send byte {0} to an unknown slave with address {1}.", newValue, currentSlaveAddress); + return; + } + txData.Enqueue((byte)newValue); + if(txData.Count == (int)bytesToTransfer.Value) + { + currentSlave.Write(txData.ToArray()); + txData.Clear(); + SetTransferCompleteFlags(); + } + } + + // The API below is not used as of this version of the driver + // due to a fix for the FW I2C driver workaround that makes it hard to + // support slave mode simultaneously with master mode + private void SlaveTransmitDataWrite(uint newValue) + { + txData.Enqueue((byte)newValue); + } + + private void SetTransferCompleteFlags() + { + if(!autoEnd.Value && !reload.Value) + { + transferComplete.Value = true; + } + if(autoEnd.Value) + { + currentSlave.FinishTransmission(); + stopDetection.Value = true; + masterMode = false; + ResetPendingData(); + } + if(reload.Value) + { + transferCompleteReload.Value = true; + } + else + { + transmitInterruptStatus = false; //this is a guess based on a driver + } + Update(); + } + + private void Update() + { + var value = (transferCompleteInterruptEnabled.Value && (transferCompleteReload.Value || transferComplete.Value)) + || (transferInterruptEnabled.Value && transmitInterruptStatus) + || (receiveInterruptEnabled.Value && isReadTransfer.Value && rxData.Count > 0) //RXNE is calculated dynamically + || (stopDetectionInterruptEnabled.Value && stopDetection.Value) + || (nackReceivedInterruptEnabled.Value && false) //TODO: implement NACKF + || (addressMatchedInterruptEnabled.Value && addressMatched.Value); + EventInterrupt.Set(value); + } + + private II2CPeripheral currentSlave; + private Queue rxData; + private Queue txData; + private int currentSlaveAddress; + private bool transferOutgoing; + private bool transmitInterruptStatus; + private bool masterMode; + + // Required to store a pending Data byte pushed before start of Transfer + private uint pendingDataByte; + + // Required to indicate a Data byte pushed before start of Transfer + private bool isPendingTxData; + + private IValueRegisterField bytesToTransfer; + private IValueRegisterField slaveAddress; + private IValueRegisterField ownAddress1; + private IValueRegisterField ownAddress2; + private IValueRegisterField ownAddress2Mask; + private IFlagRegisterField transferInterruptEnabled; + private IFlagRegisterField receiveInterruptEnabled; + private IFlagRegisterField addressMatchedInterruptEnabled; + private IFlagRegisterField nackReceivedInterruptEnabled; + private IFlagRegisterField stopDetectionInterruptEnabled; + private IFlagRegisterField transferCompleteInterruptEnabled; + private IFlagRegisterField isReadTransfer; + private IFlagRegisterField use10BitAddressing; + private IFlagRegisterField reload; + private IFlagRegisterField autoEnd; + private IFlagRegisterField noStretch; + private IFlagRegisterField ownAddress1Mode; + private IFlagRegisterField ownAddress1Enabled; + private IFlagRegisterField ownAddress2Enabled; + private IFlagRegisterField transferComplete; + private IFlagRegisterField transferCompleteReload; + private IFlagRegisterField stopDetection; + private IFlagRegisterField addressMatched; + private IFlagRegisterField start; + private IFlagRegisterField stop; + + private readonly DoubleWordRegisterCollection registers; + + private enum Registers + { + Control1 = 0x00, + Control2 = 0x04, + OwnAddress1 = 0x08, + OwnAddress2 = 0x0C, + Timing = 0x10, + Timeout = 0x14, + InterruptAndStatus = 0x18, + InterruptClear = 0x1C, + PacketErrorChecking = 0x20, + ReceiveData = 0x24, + TransmitData = 0x28 + } + } +} \ No newline at end of file diff --git a/src/Infrastructure.csproj b/src/Infrastructure.csproj index c01441cda..fa1bd3e58 100644 --- a/src/Infrastructure.csproj +++ b/src/Infrastructure.csproj @@ -846,6 +846,7 @@ +