diff --git a/src/ProjectConfig.cpp b/src/ProjectConfig.cpp index 935d6142..d7aeae7e 100644 --- a/src/ProjectConfig.cpp +++ b/src/ProjectConfig.cpp @@ -138,6 +138,10 @@ TargetConfig::TargetConfig(const YAML::Node& targetNode) { this->hardwareBreakpoints = targetNode["hardwareBreakpoints"].as(this->hardwareBreakpoints); } + if (targetNode["programMemoryCache"]) { + this->programMemoryCache = targetNode["programMemoryCache"].as(this->programMemoryCache); + } + this->targetNode = targetNode; } diff --git a/src/ProjectConfig.hpp b/src/ProjectConfig.hpp index afc7297b..bda583c0 100644 --- a/src/ProjectConfig.hpp +++ b/src/ProjectConfig.hpp @@ -56,6 +56,11 @@ struct TargetConfig */ bool hardwareBreakpoints = true; + /** + * Determines whether Bloom will employ a cache for the target's program memory. + */ + bool programMemoryCache = true; + /** * For extracting any target specific configuration. See Avr8TargetConfig::Avr8TargetConfig() and * Avr8::preActivationConfigure() for an example of this. diff --git a/src/TargetController/TargetControllerComponent.cpp b/src/TargetController/TargetControllerComponent.cpp index 8638e3b7..dcac939d 100644 --- a/src/TargetController/TargetControllerComponent.cpp +++ b/src/TargetController/TargetControllerComponent.cpp @@ -517,6 +517,10 @@ namespace TargetController ) ); } + + this->programMemoryCache = std::make_unique( + targetDescriptor.memoryDescriptorsByType.at(targetDescriptor.programMemoryType) + ); } void TargetControllerComponent::releaseHardware() { @@ -795,6 +799,42 @@ namespace TargetController } std::unique_ptr TargetControllerComponent::handleReadTargetMemory(ReadTargetMemory& command) { + const auto& targetDescriptor = this->getTargetDescriptor(); + if ( + command.memoryType == targetDescriptor.programMemoryType + && this->environmentConfig.targetConfig.programMemoryCache + ) { + assert(this->programMemoryCache); + + + if (!this->programMemoryCache->contains(command.startAddress, command.bytes)) { + Logger::debug( + "Program memory cache miss at 0x" + Services::StringService::toHex(command.startAddress) + ", " + + std::to_string(command.bytes) + " bytes" + ); + + /* + * TODO: We're currently ignoring command.excludedAddressRanges when populating the program + * memory cache. This isn't a big deal, so I'll sort it later. + */ + this->programMemoryCache->insert( + command.startAddress, + this->target->readMemory( + command.memoryType, + command.startAddress, + std::max( + command.bytes, + targetDescriptor.memoryDescriptorsByType.at(command.memoryType).pageSize.value_or(0) + ) + ) + ); + } + + return std::make_unique( + this->programMemoryCache->fetch(command.startAddress, command.bytes) + ); + } + return std::make_unique( command.bytes > 0 ? this->target->readMemory( @@ -819,6 +859,14 @@ namespace TargetController } this->target->writeMemory(command.memoryType, bufferStartAddress, buffer); + + if ( + command.memoryType == targetDescriptor.programMemoryType + && this->environmentConfig.targetConfig.programMemoryCache + ) { + this->programMemoryCache->insert(bufferStartAddress, buffer); + } + EventManager::triggerEvent( std::make_shared(command.memoryType, bufferStartAddress, bufferSize) ); @@ -866,11 +914,17 @@ namespace TargetController } std::unique_ptr TargetControllerComponent::handleEraseTargetMemory(EraseTargetMemory& command) { - if ( - command.memoryType == this->getTargetDescriptor().programMemoryType - && !this->target->programmingModeEnabled() - ) { - throw Exception("Cannot erase program memory - programming mode not enabled."); + if (command.memoryType == this->getTargetDescriptor().programMemoryType) { + if (!this->target->programmingModeEnabled()) { + throw Exception("Cannot erase program memory - programming mode not enabled."); + } + + if (this->environmentConfig.targetConfig.programMemoryCache) { + assert(this->programMemoryCache); + + Logger::debug("Clearing program memory cache"); + this->programMemoryCache->clear(); + } } this->target->eraseMemory(command.memoryType); diff --git a/src/TargetController/TargetControllerComponent.hpp b/src/TargetController/TargetControllerComponent.hpp index 7504e9c4..4f3074bf 100644 --- a/src/TargetController/TargetControllerComponent.hpp +++ b/src/TargetController/TargetControllerComponent.hpp @@ -62,6 +62,7 @@ #include "src/Targets/Targets.hpp" #include "src/Targets/TargetRegister.hpp" #include "src/Targets/TargetMemory.hpp" +#include "src/Targets/TargetMemoryCache.hpp" #include "src/EventManager/EventManager.hpp" #include "src/EventManager/EventListener.hpp" @@ -168,6 +169,16 @@ namespace TargetController std::map softwareBreakpointsByAddress; std::map hardwareBreakpointsByAddress; + /** + * The target's program memory cache. + * + * If program caching is enabled, all program memory reads will be serviced by the cache, if we have the data. + * + * We use std::unique_ptr here due to delayed construction (we construct this after activating the target + * and obtaining the target descriptor). + */ + std::unique_ptr programMemoryCache = nullptr; + /** * Registers a handler function for a particular command type. * Only one handler function can be registered per command type. diff --git a/src/Targets/CMakeLists.txt b/src/Targets/CMakeLists.txt index 6e8f8044..01432db5 100755 --- a/src/Targets/CMakeLists.txt +++ b/src/Targets/CMakeLists.txt @@ -3,6 +3,7 @@ target_sources( PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/TargetDescription/TargetDescriptionFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TargetRegister.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/TargetMemoryCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Microchip/AVR/AVR8/Avr8.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Microchip/AVR/AVR8/Avr8TargetConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Microchip/AVR/AVR8/PhysicalInterface.cpp diff --git a/src/Targets/TargetMemoryCache.cpp b/src/Targets/TargetMemoryCache.cpp new file mode 100644 index 00000000..3b059ea0 --- /dev/null +++ b/src/Targets/TargetMemoryCache.cpp @@ -0,0 +1,125 @@ +#include "TargetMemoryCache.hpp" + +#include + +#include "src/Exceptions/Exception.hpp" + +namespace Targets +{ + TargetMemoryCache::TargetMemoryCache(const TargetMemoryDescriptor& memoryDescriptor) + : memoryDescriptor(memoryDescriptor) + , data(TargetMemoryBuffer(memoryDescriptor.size(), 0x00)) + {} + + TargetMemoryBuffer TargetMemoryCache::fetch(TargetMemoryAddress startAddress, TargetMemorySize bytes) const { + const auto startIndex = startAddress - this->memoryDescriptor.addressRange.startAddress; + + if ( + startAddress < this->memoryDescriptor.addressRange.startAddress + || (startIndex + bytes) > this->data.size() + ) { + throw Exceptions::Exception("Invalid cache access"); + } + + return TargetMemoryBuffer(this->data.begin() + startIndex, this->data.begin() + startIndex + bytes); + } + + bool TargetMemoryCache::contains(TargetMemoryAddress startAddress, TargetMemorySize bytes) const { + const auto intersectingSegmentIt = this->intersectingSegment(startAddress); + + return + intersectingSegmentIt != this->populatedSegments.end() + && intersectingSegmentIt->first <= startAddress + && intersectingSegmentIt->second >= (startAddress + bytes - 1); + } + + void TargetMemoryCache::insert(TargetMemoryAddress startAddress, const TargetMemoryBuffer& data) { + const auto startIndex = startAddress - this->memoryDescriptor.addressRange.startAddress; + + std::copy(data.begin(), data.end(), this->data.begin() + startIndex); + + const auto endAddress = static_cast(startAddress + data.size() - 1); + + const auto intersectingStartSegmentIt = this->intersectingSegment(startAddress); + const auto intersectingEndSegmentIt = this->intersectingSegment(endAddress); + + if ( + intersectingStartSegmentIt == this->populatedSegments.end() + && intersectingEndSegmentIt == this->populatedSegments.end() + ) { + this->populatedSegments.insert(std::pair(startAddress, endAddress)); + return; + } + + if (intersectingStartSegmentIt == intersectingEndSegmentIt) { + // We already have a populated segment containing this address range. Nothing to do here. + return; + } + + auto newStartSegment = std::optionalpopulatedSegments)::value_type>(); + auto newEndSegment = std::optionalpopulatedSegments)::value_type>(); + + if (intersectingStartSegmentIt != this->populatedSegments.end()) { + newStartSegment.emplace(intersectingStartSegmentIt->first, endAddress); + this->populatedSegments.erase(intersectingStartSegmentIt); + } + + if (intersectingEndSegmentIt != this->populatedSegments.end()) { + newEndSegment.emplace(startAddress, intersectingEndSegmentIt->second); + this->populatedSegments.erase(intersectingEndSegmentIt); + } + + if (newStartSegment.has_value() && newEndSegment.has_value()) { + // The two new segments overlap. Merge them into one and remove any segments between them + newStartSegment.emplace(newStartSegment->first, newEndSegment->second); + newEndSegment.reset(); + + for ( + auto segmentIt = this->populatedSegments.upper_bound(newStartSegment->first); + segmentIt != this->populatedSegments.end(); + ) { + if (segmentIt->second > newStartSegment->second) { + break; + } + + this->populatedSegments.erase(segmentIt++); + } + } + + if (newStartSegment.has_value()) { + this->populatedSegments.insert(*newStartSegment); + } + + if (newEndSegment.has_value()) { + this->populatedSegments.insert(*newEndSegment); + } + } + + void TargetMemoryCache::clear() { + this->populatedSegments.clear(); + } + + TargetMemoryCache::SegmentIt TargetMemoryCache::intersectingSegment(TargetMemoryAddress address) const { + if (this->populatedSegments.empty()) { + return this->populatedSegments.end(); + } + + auto lowerBoundSegmentIt = this->populatedSegments.lower_bound(address); + + if (lowerBoundSegmentIt != this->populatedSegments.end()) { + if (lowerBoundSegmentIt->first != address && lowerBoundSegmentIt != this->populatedSegments.begin()) { + --lowerBoundSegmentIt; + } + + return lowerBoundSegmentIt->first <= address && lowerBoundSegmentIt->second >= address + ? lowerBoundSegmentIt + : this->populatedSegments.end(); + } + + // All the segments precede the given address. If the last segment doesn't intersect, none of them will. + const auto lastSegment = std::prev(this->populatedSegments.end()); + return lastSegment->first <= address && lastSegment->second >= address + ? lastSegment + : this->populatedSegments.end(); + } +} diff --git a/src/Targets/TargetMemoryCache.hpp b/src/Targets/TargetMemoryCache.hpp new file mode 100644 index 00000000..d97fa898 --- /dev/null +++ b/src/Targets/TargetMemoryCache.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include "TargetMemory.hpp" + +namespace Targets +{ + class TargetMemoryCache + { + public: + TargetMemoryCache(const TargetMemoryDescriptor& memoryDescriptor); + + /** + * Fetches data from the cache. + * + * @param startAddress + * @param bytes + * + * @return + */ + TargetMemoryBuffer fetch(TargetMemoryAddress startAddress, TargetMemorySize bytes) const; + + /** + * Checks if the cache currently holds data within the given address range. + * + * @param startAddress + * @param bytes + * + * @return + */ + bool contains(TargetMemoryAddress startAddress, TargetMemorySize bytes) const; + + /** + * Inserts data into the cache and performs any necessary bookkeeping. + * + * @param startAddress + * @param data + */ + void insert(TargetMemoryAddress startAddress, const TargetMemoryBuffer& data); + + /** + * Clears the cache. + */ + void clear(); + + private: + const TargetMemoryDescriptor& memoryDescriptor; + TargetMemoryBuffer data; + + /** + * A populated segment is just an address range in the cache that we know we've populated. + * + * populatedSegments::value_type::first = The start address (inclusive) of the populated range + * populatedSegments::value_type::second = The end address (inclusive) of the populated range + */ + std::map populatedSegments = {}; + + /** + * Finds the segment that intersects with the given address. Segments cannot overlap, so only one segment can + * intersect with the given address, at any given time. + * + * @param address + * + * @return + * An iterator to the intersecting segment, or populatedSegments::end() if none is found. + */ + using SegmentIt = decltype(TargetMemoryCache::populatedSegments)::const_iterator; + SegmentIt intersectingSegment(TargetMemoryAddress address) const; + }; +}