use core::{convert::Infallible, mem};
use derive_more::From;
use gdbstub::{
conn::Connection,
stub::{
GdbStub, GdbStubBuilder, GdbStubError, MultiThreadStopReason, SingleThreadStopReason,
state_machine::GdbStubStateMachine,
},
};
use snafu::Snafu;
use spin::{Mutex, MutexGuard};
use static_cell::ConstStaticCell;
use vex_sdk::vexSystemExitRequest;
use zynq7000::devcfg;
use crate::{
Debugger,
exceptions::DebugEventContext,
gdb_target::{MonitorStatus, StopReason, V5Target},
sdk::stop_all_motors,
sys::{DebuggerSystem, System},
transport::{Transport, TransportError},
};
#[derive(Debug, Snafu)]
pub enum DebuggerError {
#[snafu(context(false))]
Io { source: TransportError },
#[snafu(context(false))]
GdbStub {
source: GdbStubError<Infallible, TransportError>,
},
}
#[derive(Debug, Default, Clone)]
pub struct DebuggerConfig {
pub stop_motors_on_break: bool,
}
pub struct V5Debugger<S: Transport> {
session: Mutex<DebugSession<'static, S>>,
config: DebuggerConfig,
}
impl<S: Transport> V5Debugger<S> {
#[must_use]
pub fn new(stream: S) -> Self {
const PACKET_BUFFER_SIZE: usize = 4096;
static PACKET_BUFFER: ConstStaticCell<[u8; PACKET_BUFFER_SIZE]> =
ConstStaticCell::new([0; PACKET_BUFFER_SIZE]);
let pkt_buffer = PACKET_BUFFER.take();
let target = V5Target::new(&mut unsafe { devcfg::Registers::new_mmio_fixed() });
Self {
session: Mutex::new(DebugSession {
stage: SessionStage::Uninitialized(
GdbStubBuilder::new(stream)
.with_packet_buffer(pkt_buffer)
.build()
.unwrap(),
),
target,
internal_breaks: None,
}),
config: DebuggerConfig::default(),
}
}
#[must_use]
pub fn with_config(mut self, config: DebuggerConfig) -> Self {
self.config = config;
self
}
#[must_use]
pub fn session<'a>(&'a self) -> MutexGuard<'a, DebugSession<'static, S>> {
self.session.lock()
}
}
unsafe impl<S: Transport + 'static> Debugger for V5Debugger<S> {
fn initialize(&self) {
let mut session = self.session();
session.register_internal_breakpoints();
System::initialize(&mut session.target);
crate::sdk::competition::install_override();
session.target.stop_motors_on_break = self.config.stop_motors_on_break;
log::debug!("Debugger initialized (config={:?})", self.config);
}
unsafe fn handle_debug_event(&self, ctx: &mut DebugEventContext) -> bool {
let mut session = self.session();
let stop_reason = session.target.enter_breakpoint(ctx);
if session.target.stop_motors_on_break {
log::debug!("Auto motor-stop triggered by breakpoint");
stop_all_motors();
}
let action = session.handle_stop(stop_reason);
if action == StopAction::EnterMonitor {
log::debug!("Starting debug console");
session.run_debug_console();
log::debug!("Debug console has exited");
}
session.target.leave_breakpoint(ctx)
}
fn poll(&self) {
if let Some(mut session) = self.session.try_lock()
&& let SessionStage::Active(gdb_state) = &mut session.stage
&& let GdbStubStateMachine::Running(gdb) = gdb_state
{
const CTRL_C: u8 = 0x03;
while let Ok(Some(byte)) = gdb.borrow_conn().try_read() {
if byte == CTRL_C {
session.target.request_interrupt();
break;
}
}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum StopAction {
Resume,
EnterMonitor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InternalBreakpoint {
SystemExitRequest,
}
pub struct DebugSession<'a, S>
where
S: Transport,
{
pub target: V5Target,
internal_breaks: Option<[(InternalBreakpoint, u32); 1]>,
stage: SessionStage<'a, S>,
}
#[derive(From)]
enum SessionStage<'a, C: Connection> {
Uninitialized(GdbStub<'a, V5Target, C>),
Active(GdbStubStateMachine<'a, V5Target, C>),
Transitioning,
}
impl<S> DebugSession<'_, S>
where
S: Transport,
{
fn has_client(&self) -> bool {
match &self.stage {
SessionStage::Active(GdbStubStateMachine::Disconnected(_)) => false,
SessionStage::Active(_) => true,
_ => false,
}
}
fn handle_stop(&mut self, reason: StopReason) -> StopAction {
let StopReason::TrackedSoftwareBreak { id } = reason else {
return StopAction::EnterMonitor;
};
let breakpoint = self
.target
.breakpoint(id)
.expect("bkpt deleted before it could be handled");
let internal_action = if breakpoint.reason.internal {
self.handle_internal_breakpoint()
} else {
StopAction::Resume
};
if breakpoint.reason.user {
StopAction::EnterMonitor
} else {
internal_action
}
}
fn handle_internal_breakpoint(&mut self) -> StopAction {
debug_assert!(self.target.breakpoints_ignored());
let pc = self.target.saved_ctx().program_counter;
let Some(&(id, addr)) = self
.internal_breaks
.iter()
.flatten()
.find(|&&(_id, addr)| addr == pc)
else {
return StopAction::Resume;
};
match id {
InternalBreakpoint::SystemExitRequest => {
self.target.remove_sw_breakpoint(addr, true);
if !self.has_client() {
return StopAction::Resume;
}
self.target.monitor_status = MonitorStatus::Exiting;
StopAction::EnterMonitor
}
}
}
fn register_internal_breakpoints(&mut self) {
assert!(self.internal_breaks.is_none());
let exit_func = vexSystemExitRequest as *const () as u32;
let is_thumb = (exit_func & 1) != 0;
log::debug!("Register pre-exit handler (thumb={is_thumb})");
let internal_breaks = [(InternalBreakpoint::SystemExitRequest, exit_func & !1)];
for (_id, addr) in internal_breaks {
unsafe {
self.target
.register_sw_breakpoint(addr, is_thumb, true)
.unwrap();
}
}
self.internal_breaks = Some(internal_breaks);
}
fn run_debug_console(&mut self) {
let stage = mem::replace(&mut self.stage, SessionStage::Transitioning);
match stage {
SessionStage::Uninitialized(gdb) => {
self.stage = gdb.run_state_machine(&mut self.target).unwrap().into();
self.run_debug_console();
}
SessionStage::Active(mut state) => {
while self.target.monitor_status != MonitorStatus::ResumingProgram {
unsafe {
vex_sdk::vexTasksRun();
}
state = Self::tick_state_machine(state, &mut self.target)
.expect("debugger encountered an error");
}
self.stage = state.into();
}
SessionStage::Transitioning => panic!("Cannot resume from transitioning state"),
}
}
fn tick_state_machine<'a>(
gdb: GdbStubStateMachine<'a, V5Target, S>,
target: &mut V5Target,
) -> Result<GdbStubStateMachine<'a, V5Target, S>, DebuggerError> {
match gdb {
GdbStubStateMachine::Idle(mut gdb) => {
if let Ok(byte) = gdb.borrow_conn().read() {
Ok(gdb.incoming_data(target, byte)?)
} else {
Ok(gdb.into())
}
}
GdbStubStateMachine::Running(gdb) => {
let reported_reason = target.gdb_stop_reason();
log::info!("Debugger Stop reason: {reported_reason:?}");
if matches!(reported_reason, MultiThreadStopReason::Exited(_)) {
target.monitor_status = MonitorStatus::ResumingProgram;
}
Ok(gdb.report_stop(target, reported_reason)?)
}
GdbStubStateMachine::CtrlCInterrupt(gdb) => {
log::warn!("Got Ctrl+C");
let stop_reason: Option<SingleThreadStopReason<_>> = None;
Ok(gdb.interrupt_handled(target, stop_reason)?)
}
GdbStubStateMachine::Disconnected(gdb) => {
if target.monitor_status == MonitorStatus::Exiting {
target.monitor_status = MonitorStatus::ResumingProgram;
}
Ok(gdb.return_to_idle())
}
}
}
}