knurling-rs / probe-run

Run embedded programs just like native ones
Apache License 2.0
643 stars 75 forks source link

App-specific communication over probe-run's connection? #261

Open lynaghk opened 3 years ago

lynaghk commented 3 years ago

Thanks for making probe-run and the related machinery (defmt, rtt), the dev experience is amazing compared to the alternatives =D

This is a design / "would this PR be welcome?" question rather than bug report (let me know if I should move it elsewhere): How should I build a laptop-to-microcontroller communication channel while retaining probe-run's logging?

Say I'm prototyping a stepper motor; I'd like to:

In production one would use the microcontroller's USB or USART channel for app-specific messages. But for prototyping and one-offs, being able to send application data over the probe-run link would be very handy.

Presumably I could fork probe-run and add my own messaging logic, but is there a more composable solution that can be reused by the community? How can app-specific RTT communication channels coexist with probe-run's logging?

Urhengulas commented 3 years ago

Hi,

I think what you want are "rtt down channels", for host to target communication. This currently isn't implemented in either defmt-rtt or probe-run, but I don't see why we shouldn't do it. rtt_target support them, but this can't be used together with defmt-rtt.

lynaghk commented 3 years ago

I forked probe-run and have an RTT-based solution working for myself. Not sure the best abstraction for general usage yet; once I have some firsthand experience I'll update this thread with ideas for discussion.

In the mean time, for anyone who has a similar need, here's what I did:

On the target

Cargo deps should be:

defmt = "0.2"
rtt-target = { version = "0.3", features = ["cortex-m"] }

rather than defmt-rtt, which explicitly disallows downchannels. You can basically copy/paste defmt-rtt impl and modify it to take a channel at initialization time. (Or just use this crate, which does basically that: https://github.com/akiles/defmt-rtt-target)

Then initialize your RTT channels:

let channels = rtt_target::rtt_init! {
    up: {
        0: { // channel number
            size: 1024 // buffer size in bytes
            mode: NoBlockSkip // mode (optional, default: NoBlockSkip, see enum ChannelMode)
            name: "defmt" // gotta call it this so that probe run logs it.
        }
    }
    down: {
        0: {
            size: 64
            name: "Controls"
        }
    }
};

log::init(channels.up.0);

On the host

Fork probe-run and copy/paste setup_logging_channel to open your downchannel. Do whatever app-specific logic you need in there. Here's mine, where I'm reading from stdin and sending app specific message structs:

fn setup_my_downchannel(rtt_buffer_address: u32, sess: Arc<Mutex<Session>>) -> anyhow::Result<()> {
    let scan_region = ScanRegion::Exact(rtt_buffer_address);

    match Rtt::attach_region(sess.clone(), &scan_region) {
        Ok(mut rtt) => {
            log::info!("Successfully attached my downchannel RTT");

            let mut channel = rtt
                .down_channels()
                .take(0)
                .ok_or_else(|| anyhow!("RTT down channel 0 not found"))?;
            let mut buf = [0u8; 256];
            std::thread::spawn(move || loop {
                use interface::Msg;
                if let Some(msg) = match read_line().unwrap().as_str() {
                    "start" => Some(Msg::Start),
                    "stop" => Some(Msg::Stop),
                    "status" => Some(Msg::Status),
                    "tick" => Some(Msg::Tick),
                    _ => None,
                } {
                    channel.write_all(msg.serialize(&mut buf).unwrap()).unwrap();
                }
            });
        }

        Err(probe_rs_rtt::Error::ControlBlockNotFound) => {
            log::trace!("Could not attach because the target's RTT control block isn't initialized (yet). retrying");
        }

        Err(e) => {
            return Err(anyhow!(e));
        }
    }

    Ok(())
}

I ran this setup in the same place probe_run sets up logging: https://github.com/knurling-rs/probe-run/blob/fd606d6f90b863190c2a0170bd7262150595727c/src/main.rs#L204

I ran into substantial (> 1 second) latency writing to the downchannel and realized this was due to lock contention with probe run trying to read log messages from the upchannel. Adding std::thread::sleep_ms(1); in this loop https://github.com/knurling-rs/probe-run/blob/fd606d6f90b863190c2a0170bd7262150595727c/src/main.rs#L230 is my YOLO fix =D

Urhengulas commented 3 years ago

@lynaghk Good job!

Not sure the best abstraction for general usage yet

That's also what we discussed internally. Looking forward to your ideas!