Commit c9556d44 authored by Bjarne Wintermann's avatar Bjarne Wintermann
Browse files

Updated readme, introduction of Detector Component. Needs to have buffer...

Updated readme, introduction of Detector Component. Needs to have buffer because receiver is faster than it
parent eabccd19
# Bus Monitor Component for LiteX
The litebusmonitor component enables supervision on a SoCs Bus, for purposes of recoding accesses, detecting anomalies and analyzing a chips behaviour.
The component is distrbuted into smaller LiteX-Modules:
* _MonitorSender_: Receives the bus(masters) as a parameter. Those are supervised and the output is buffered. The modules source member exposes a litex-stream like interface, with ready, valid and data signals. The data queues data gets concatted into a long string and split into 32-bit words and output to the source member
* _MonitorReceiver_: This modules is connected to the MonitorSender (either directly per SoC defintion, or per external port, etc.). It appends a DMA (direct memory access) and it's master to the SoCs chip / bus and is capable of writing the data. When the data has filled >50% of the DMA allocated space, an interrupt is triggered, and the module stops writing data. It is programmed to stop in the right cycle, in order to preserve entries in memory (since the data is simply one long bit-string, it could potentially be cut in the wrong spot, rendering the decoded data from the resumed next cycle useless). The software (BIOS) now has the responsibility to clear the interrupt and consume the data written (by moving it onto a harddisk, analyzing it, sending it over network, etc.). As soon as the interrupt is cleared, the DMA resets its internal offset and continues data writing.
* _MonitorDetector_
There are two modes of operation for the combination. Either the software (in most cases the BIOS) explicitly allocates a block of memory and sends the starting address pointer to the DMA, allowing it so start writing. The advantage is that at any point the address can be changed and the space be as big as needed.
However this bears the risk of loosing out on data: In case the buffer is filled more quickly than the data can be read (and thus the buffer cleared), bus access entries get discarded because the buffer is full. This happens when the software has to assign the address to write to to the DMA, because at that point the amount of instructions is already far larger than the buffer has space. One possible fix, which works for large chips is to simply increase the buffer size. However in smaller devices this is not possible and thus there is a second solution: When inintializing the MonitorReceiver, you can pass it the address and length to write to, if this is known at build time. As a result, when the SoC gets started, the software does not have to wait and initialize memory itself, but can start writing as soon as the buffer is filled. And because it can be emptied from the first moment and the write-out is fast enough, there are no dropped entries anymore.
## Building
py basys3 --build --l2-size 0 --integrated-main-ram-size 41384 --integrated-rom-size=64000 --integrated-sram-size=11000 --uart-baudrate 1000000
......@@ -11,11 +11,11 @@ from typing import Dict, List, Tuple, Union
import sys
from litebusmonitor import lbm_misc
from litebusmonitor.lbm_misc import MigenValue, MigenValueList
from litex.soc.interconnect.axi import *
MigenValue = Union[Signal, _Value, _Statement, _Operator]
MigenValueList = [Signal, _Value, _Statement, _Operator]
class SignalMonitor(Module):
......@@ -28,6 +28,7 @@ from crg import _CRG
#from receiver import LiteBusReceiver
from litex.soc.integration.builder import soc_directory, Builder
from lbm_misc import simple_connect
from litebusmonitor.modules import MonitorDetector
from reworked_monitor import MonitorSender, MonitorReceiver
class BaseSoC(SoCCore):
......@@ -54,17 +55,29 @@ class BaseSoC(SoCCore):
#self.add_constant("ROM_BOOT_ADDRESS", 0x30000000)
def add_monitor(self, fifodepth):
# Sanity test condition: Sel can never be 16 because there are only 4 bits so this should never trigger
sel_test = lambda entry: entry["sel"] == 16
self.submodules.sender = sender = MonitorSender(self.bus, fifodepth)
self.submodules.receiver = receiver = MonitorReceiver(self, fifodepth, sender.bits_per_entry, self.bus)
#self.submodules.detector = detector = MonitorDetector([sel_test], sender.layout, sender.bits_per_entry)
#violation_detected_led = self.platform.request("user_led", 7)
#self.comb += violation_detected_led.eq(detector.violation_detected)
self.add_interrupt("receiver", use_loc_if_exists=False)
self.comb += [
sender.source.ready.eq(receiver.sink.ready), #& detector.sink.ready),
# Build --------------------------------------------------------------------------------------------
......@@ -2,11 +2,17 @@ from termcolor import colored # type: ignore
from migen import * # type: ignore
from import Endpoint # type: ignore
from migen.fhdl.structure import _Assign # type: ignore
from typing import List, Optional, Union, Callable
from typing import List, Optional, Union, Callable, Tuple
import logging
import datetime
#from monitor import MonitoringComponentSender
import os
from migen.fhdl.structure import _Operator, _Statement, _Value
Layout = List[Tuple[str, int]] # Type annotation for a migen component layout
MigenValue = Union[Signal, _Value, _Statement, _Operator]
MigenValueList = [Signal, _Value, _Statement, _Operator]
OUTPUT_FOLDER = "build_data"
from migen import *
from migen.fhdl.structure import _Operator
from import Endpoint, SyncFIFO
from typing import List, Tuple
from litebusmonitor.lbm_misc import Layout
from import *
class ParityBitAdder(Module):
def __init__(self, layout, input_fifo : SyncFIFO) -> None:
def __init__(self, layout : Layout, input_fifo : SyncFIFO) -> None:
XOR = lambda x,y: _Operator("^", [x,y])
self.out_layout = out_layout = layout + [("parity", 1)]
......@@ -27,4 +30,55 @@ class ParityBitAdder(Module):
self.comb += internal_queue.sink.parity.eq(reduce(XOR, input_fifo.sink.raw_bits()))
self.source = internal_queue.source
\ No newline at end of file
self.source = internal_queue.source
class MonitorDetector(Module):
def __init__(self, detector_conditions, unpacked_layout : Layout, bits_per_entry : int, input_data_width : int = 32) -> None:
"""Creates a module that detects violations of given policies. An example for a condition/policy: The first master is not allowed to write into the first address space:
condition = lambda entry: entry["adr"] < 0x500 & entry["we"] == 1 & entry["acknowledged_master"] == 0
detector_conditions (Callable[[Dict[str, Signal]], MigenValue]): The conditions - these represent what is NOT allowed. If a condition is fulfilled it triggers the violation detection
unpacked_layout (Layout): The layout of the original queue. This has to be taken from the sender component to reconstruct the original data
bits_per_entry (int): How many bits are in a complete entry. Needed to initialize the UpConverter
input_data_width (int, optional): How many bits are given per cycle. Needed to initialize the UpConverter. Defaults to 32.
print(f"[DETECTOR] Unpacking {input_data_width} to {bits_per_entry}")
unpack_converter = Converter(input_data_width, bits_per_entry)
self.submodules += unpack_converter
# Always be ready to accept new entry data - checking always only takes one cycle
self.comb += unpack_converter.source.ready.eq(1)
self.entry = Signal(bits_per_entry)
self.entry_dict = {}
# Make entry data accessible via a dictionary
idx = 0
for name, length in unpacked_layout:
self.entry_dict[name] = Signal(length)
self.comb += self.entry_dict[name].eq(self.entry[idx:idx+length])
idx += length
self.sync += [
# Set a flag to 1 as soon as a violation was detected
self.violation_detected = Signal(reset=0)
for condition in detector_conditions:
self.sync += [
self.sink = unpack_converter.sink
\ No newline at end of file
......@@ -36,11 +36,14 @@ class MonitorSender(Connectable, AutoCSR):
# Monitor = bm = BusMasterMonitor(list(target.masters.values()) if type(target) == SoCBusHandler else target, entries=buffered_entries, select_by_ack=select_by_ack)
self.submodules +=
self.bits_per_entry = bits_per_entry = bm.get_complete_layout_width()
tmp_bits_per_entry = bits_per_entry = bm.get_complete_layout_width()
# Pad the queue data to the nearest multiple of 32 and fill rest with 0s
padded_length = self.bits_per_entry + (32 - (self.bits_per_entry % 32))
padded_length = tmp_bits_per_entry + (32 - (tmp_bits_per_entry % 32))
# Bits per entry attribute contains the padding!
self.bits_per_entry = padded_length
entry_signal = Signal(padded_length)
say(f"Original Entry Length: {bits_per_entry}\nWith padding: {padded_length}")
......@@ -64,6 +67,8 @@ class MonitorSender(Connectable, AutoCSR):
# Write config out for the decoder to read
self.write_layout_config_files(padded_length - bits_per_entry)
self.layout : Layout = bm.get_recording_layout()
def write_layout_config_files(self, leadingZeros, foldername="build"):
layout = {"padding": leadingZeros}
......@@ -104,10 +109,10 @@ class MonitorReceiver(Connectable, AutoCSR):
assert dma_bus.data_width == 32
# Calculate where interrupts are supposed to happen
padded_entry_length = bits_per_entry + (32 - (bits_per_entry % 32))
self.words_needed = int(ceil(dma_entries * padded_entry_length / 32)) # Assuming 32 bit words
#padded_entry_length = bits_per_entry + (32 - (bits_per_entry % 32))
self.words_needed = int(ceil(dma_entries * bits_per_entry / 32)) # Assuming 32 bit words
break_every = padded_entry_length / 32
break_every = bits_per_entry / 32
break_at_word = int(int(ceil(((self.words_needed/2) / break_every) + 1)) * break_every) # The next multiple of break_every above the half way mark of the DMA
say(f"The DMA will interrupt at {break_at_word} 32 bit words")
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment