Return to Home Page

DmaManager

The DMA Manager is the main class in charge of IBUS communications into and out of the Pico.

It’s a bit of a misnomer since it doesn’t actually do any DMA. Originally I wanted to write a PIO routine to DMA bytes from the UART read buffers into some kind of place. Instead, I ended up an interrupt service routine (ISR) to fetch the available bytes and put them in a queue_t. The Pico SDK offers an ISR-safe queue type, which I used here.

Then, a tight loop on CPU0 checks to see incrementally if there’s a complete packet from the queue of bytes. If so, they get assembled into a IBusPacket object, and placed onto a queue there.

Bus Topology

The DMA manager is responsible for setting up the bus topology.

Topologies

These exist in the PicoBusTopology.h file.

Each of these are made for different use-cases.

The USB port on the Pico is often used as an stdout. I used that to start debugging the pico before I had really gotten the DMA Manager all set up.

The Pico Debug Probe has the useful property that it integrates a UART as well as a SWD link. So, if I got the Pico under test to output it’s stdout printlogs to the uart hooked up to the debug probe, then I wouldn’t have to keep switching serial ports on the laptop when debugging - I could use one device stably.

CAR_WITH_PI

/**
 *
 *
 *                                  +--------+
 *             ---------swd-------->|  Pico  |
 *            /                     |  Debug |
 *           /                      |  Probe |
 *          |                       +--------+
 *          |                            ^
 *          |                            | (swd)
 *          v                            v
 *      +--------+                    +--------+       +---------+
 *      | Laptop |<-(usb stdio logs)->|  Pico  |<=====>| MCP2020 | (UART0, 9600-8E1, IBUS raw)
 *      +--------+                    +--------+       +---------+
 *                                      ^^
 *                                      ||<........................(UART1, 9600-8E1, IBUS raw)
 *                                      ||
 *                       (AdaFruit 954 Serial <-> USB Adapter)
 *                                      ||
 *                                      vv
 *                                  +--------+
 *                                  |  Rpi  |
 *                                  +-------+
 *
 * We can't use UART1 to talk to the Rpi directly because when the Rpi is in display parallel mode
 * to get VGA-out, there aren't enough GPIO pins we can use (and they're all disabled for SPI
 * too). So, we run another USB cable from the mainboard to the pi.
 */

SLED_NO_PI

/**
 *                    .....(SWD, 115200-8N1. Debug log + Raw IBUS)
 *                   .
 *                  .               +--------+
 *             ====================>|  Pico  |
 *            //                    |  Debug |
 *           //                     |  Probe |
 *          ||                      +--------+
 *          ||                           ^^
 *          ||                           ||<.....................(UART1, 115200-8N1. Debug log + Raw IBUS)
 *          vv                           vv
 *      +--------+                  +--------+       +---------+
 *      | Laptop |<-(usb pwr only)->|  Pico  |<=====>| MCP2020 | (UART0, 9600-8E1 IBUS raw)
 *      +--------+                  +--------+       +---------+
 *
 *                                  +--------+
 *                                  |  Rpi   |
 *                                  +--------+
 *
 */

SLED_LAPTOP_HMI

/**
 * Same as SLED_NO_PI, but UART1 is 115200 8-N-1, Raw IBUS. It's so that I can use the picoprobe
 * straight into e39-rpi on the laptop without having to hook up the Adafruit separately
 *
 *                    .....(SWD, 115200-8N1. Raw IBUS)
 *                   .
 *                  .               +--------+
 *             ====================>|  Pico  |
 *            //                    |  Debug |
 *           //                     |  Probe |
 *          ||                      +--------+
 *          ||                           ^^
 *          ||                           ||<.....................(UART1, 115200-8N1. Raw IBUS)
 *          vv                           vv
 *      +--------+                  +--------+       +---------+
 *      | Laptop |<-(usb pwr only)->|  Pico  |<=====>| MCP2020 | (UART0, 9600-8E1 IBUS raw)
 *      +--------+                  +--------+       +---------+
 *
 *                                  +--------+
 *                                  |  Rpi   |
 *                                  +--------+
 *
 */

Queues

This picture shows an overview of how the DMA Manager uses Queues to shuffle messages and bytes around.

The interop between the UartForwarderObserver and the UartForwarderWriter is discussed in Output Writers

flowchart LR subgraph Car IBusNetwork end subgraph Pico uart0@{shape: flag, label: "uart0" } uart1@{shape: flag, label: "uart1" } subgraph DmaManager uart0rxByteQ[("`_Byte_ uart0rxByteQ`")] uart1rxByteQ[("`_Byte_ uart1rxByteQ`")] toPiQ[("`_IBusPacket_ toPiQ`")] toCarQ[("`_IBusPacket_ toCarQ`")] fromPiQ[("`_IBusPacket_ fromPiQ`")] fromCarQ[("`_IBusPacket_ fromCarQ`")] end subgraph writers writerClasses@{ shape: processes, label: "Writers"} writerClasses --> toCarQ writerClasses --> toPiQ UartForwarderWriter UartForwarderWriter --> toCarQ UartForwarderWriter --> toPiQ end subgraph observers observerRegistry@{ shape: processes, label: "Observer Regsitry"} UartForwarderObserver observerRegistry --> UartForwarderObserver end end subgraph RaspberryPi e39Rpi end Car <--> uart0 RaspberryPi <--> uart1 uart0 -->|Interrupt Service Routine| uart0rxByteQ toCarQ --> uart0 uart0rxByteQ --->|Packetizer| fromCarQ fromCarQ --->|Set origin from car| observerRegistry toPiQ --> uart1 uart1 -->|Interrupt Service Routine| uart1rxByteQ uart1rxByteQ -->|Packetizer| fromPiQ fromPiQ -->|Set origin from pi| observerRegistry %% UartForwarderObserver --------> UartForwarderWriter

Incoming Bytes to Observers

This section details how incoming bytes on a UART are then doled out to the Observers.

Interrupt Service Routines

ISRs are run on UART0 rx of bytes, and on UART1 rx of bytes. Each ISR reads all the available characters and puts them onto a queue of bytes. This is one area where DMA would actually improve the DMAmanager :D

    void SingleCoreDmaManager::on_uart0_rx() {
        handleRxInterruptServiceRoutine(uart0, &uart0rxByteQ);
    }

    void SingleCoreDmaManager::on_uart1_rx() {
        handleRxInterruptServiceRoutine(uart1, &uart1rxByteQ);
    }
    
       void SingleCoreDmaManager::handleRxInterruptServiceRoutine(
            uart_inst_t *uart,
            queue_t *toQ
    ) {
        //https://github.com/raspberrypi/pico-examples/blob/master/uart/uart_advanced/uart_advanced.c
        while (uart_is_readable(uart)) {
            uint8_t ch = uart_getc(uart);
            if (!queue_try_add(toQ, &ch)) {
                //We couldn't write the queue?
                queue_free(toQ);
            }
        }
    }

Packetizers

The packetizers are build up an ibus packet, byte-by-byte. They’re populated from a tight loop on CPU 0:

    void SingleCoreDmaManager::onCpu0Loop() {
        ...
        flushUart0ByteBufferToPacketizer();
        flushUart1ByteBufferToPacketizer();
        flushFromPiQToLogic();
        flushFromCarQToLogic();
        
        flushToPiQToUart();
        flushToCarQToUart();
        ...
    }

which drains the Q, by feeding it into a packetizer.

After the byte is added to the packetizer, the flush routine then checks to see if the packet is complete, and pops the completed packet onto another queue for further processing.

    void SingleCoreDmaManager::flushUartByteBufferToPacketizer(
            queue_t *fromByteQ,
            std::string fromByteQName,
            queue_t *toQ,
            std::string toQName,
            Packetizer *packetizer
    ) {

        uint8_t byte;

        //We're going to depend on the application container loop here so that we don't block here a bunch.
        //We will add one byte at a time to the packetizer.

        bool haveByte = queue_try_remove(fromByteQ, &byte);
        if (haveByte) {
            critical_section_enter_blocking(&packetizerCs);
                packetizer->addByte(byte);
                if (packetizer->isPacketComplete()) {
                    //fromCarQPacketizer.writeState("SingleCoreDmaManagerISR_fromCarQ", logger);
                    writePacketToQ(
                            data::IbusPacket(packetizer->getPacketBytes()),
                            toQ,
                            toQName
                    );
                    packetizer->reset();
                }
            critical_section_exit(&packetizerCs);
        }
    }

Packets to Observer Registry

The packet queues (FromCarQ and FromPiQ) are flushed together into the ObserverRegistry. Each Packet has a PacketSource set on it so that the observers can know where the packet originated from.

There is one observer whose sole job is to forward packets FromCar to the Pi: it uses this property to only forward packets with PacketSource::FROM_CAR and ignore PacketSource::FROM_PI packets so that it doesn’t create an infinite loop (when the Pi is in a heartbeat request/response cycle).

void SingleCoreDmaManager::flushFromCarQToLogic() {
        readIncomingQ_toLogic(&fromCarQ, "fromCarQ");
    }

    void SingleCoreDmaManager::flushFromPiQToLogic() {
        readIncomingQ_toLogic(&fromPiQ, "fromPiQ");
    }

    void SingleCoreDmaManager::readIncomingQ_toLogic(queue_t *queue, std::string queue_name) {

        bool log_dispatch_trace = false;

        std::array<uint8_t, 255> buffer = std::array<uint8_t , 255>();
        buffer.fill(0); //Fill the buffer with zeros so the .data() below dereferences validly.

        bool havePacket = queue_try_remove(queue, (void*) buffer.data());
        if (havePacket) {
            //A packet came into the pico for processing.
            if (log_dispatch_trace) {
                logger->d("SingleCoreDmaManager",
                          fmt::format("Dispatching packet from Q {} to cpu0 observers", queue_name));
            }

            data::IbusPacket packetFromArray = data::IbusPacket(buffer);

            if (queue == &fromCarQ) {
                packetFromArray.setPacketSource(data::PacketSource::FROM_CAR);
            }

            if (queue == &fromPiQ) {
                packetFromArray.setPacketSource(data::PacketSource::FROM_PI);
            }

            observerRegistry->dispatchMessageToAllObservers(packetFromArray);

            if (log_dispatch_trace) {
                logger->d("SingleCoreDmaManager",
                          fmt::format("Finished dispatching packet from Q {} to cpu0 observers", queue_name));
            }
        }

    }

Outgoing Packets to UARTs

When a packet is scheduled for write (by a Writer), it is added to a Queue of Packets within the DMAmanager.

The flush routines flushToPiQToUart(), and flushToCarQToUart(); are run in a tight loop on CPU 0 to dump out the packet contents to the UART in a blocking manner:

void SingleCoreDmaManager::writeOutgoingQ_toUart(
            queue_t *movePacketFrom,
            uart_inst_t *uart,
            std::string fromName,
            std::string uartName) {

        std::array<uint8_t, 255> outgoingPacketBuffer = std::array<uint8_t , 255>();
        outgoingPacketBuffer.fill(0);

        if (queue_try_remove(movePacketFrom, (void*)outgoingPacketBuffer.data())) {


            if (busTopologyManager->getBusToplogy() == topology::BusTopology::SLED_NO_PI) {
                //Guard to prevent infinite logging loop on topologies that output log messages over ibus.
                logger->d("DmaManager",
                          fmt::format("We have a packet from {} to write to uart {}", fromName, uartName));
            }

            //TODO what if we don't construct the packet all over again? What if we just copy out the buffer dumbly?
            uart_write_blocking(uart, outgoingPacketBuffer.data(), outgoingPacketBuffer[1] + 2);
        }
    }

The fixed size array might be alarming, but note that the length field on an IBUS packet is one byte long, so a packet can at most be 255 + 1 bytes long. In practise, this never happens.

To find out how many bytes to write out to the UART (IBUS packets are of variable length), rather than reparsing the packet just to use an accessor on it to find the length, I assume that the queue of packets contains only well-formed packets, which means that the second byte of each packet is the length field.

Return to Top