mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-23 10:51:30 +11:00
Add module for interrupt request (IRQ) handling
This commit is contained in:
parent
dc2127b2ce
commit
0d654032bb
56
crt0.s
56
crt0.s
|
@ -6,6 +6,11 @@ __start:
|
|||
.fill 188, 1, 0
|
||||
|
||||
.Linit:
|
||||
@ Set address of user IRQ handler
|
||||
ldr r0, =MainIrqHandler
|
||||
ldr r1, =0x03FFFFFC
|
||||
str r0, [r1]
|
||||
|
||||
@ set IRQ stack pointer
|
||||
mov r0, #0x12
|
||||
msr CPSR_cf, r0
|
||||
|
@ -31,4 +36,55 @@ __start:
|
|||
@ jump to user code
|
||||
ldr r0, =main
|
||||
bx r0
|
||||
|
||||
.arm
|
||||
.global MainIrqHandler
|
||||
.align 4, 0
|
||||
MainIrqHandler:
|
||||
@ Load base I/O register address
|
||||
mov r2, #0x04000000
|
||||
add r2, r2, #0x200
|
||||
|
||||
@ Save IRQ stack pointer and IME
|
||||
mrs r0, spsr
|
||||
ldrh r1, [r2, #8]
|
||||
stmdb sp!, {r0-r2,lr}
|
||||
|
||||
@ Disable all interrupts by writing to IME
|
||||
mov r0, #0
|
||||
strh r0, [r2, #8]
|
||||
|
||||
@ Acknowledge all received interrupts that were enabled in IE
|
||||
ldr r3, [r2, #0]
|
||||
and r0, r3, r3, lsr #16
|
||||
strh r0, [r2, #2]
|
||||
|
||||
@ Switch to system mode
|
||||
mrs r2, cpsr
|
||||
bic r2, r2, #0x1F
|
||||
orr r2, r2, #0x1F
|
||||
msr cpsr_cf, r2
|
||||
|
||||
@ Jump to user specified IRQ handler
|
||||
ldr r2, =__IRQ_HANDLER
|
||||
ldr r1, [r2]
|
||||
stmdb sp!, {lr}
|
||||
adr lr, MainIrqHandler_Return
|
||||
bx r1
|
||||
MainIrqHandler_Return:
|
||||
ldmia sp!, {lr}
|
||||
|
||||
@ Switch to IRQ mode
|
||||
mrs r2, cpsr
|
||||
bic r2, r2, #0x1F
|
||||
orr r2, r2, #0x92
|
||||
msr cpsr_cf, r2
|
||||
|
||||
@ Restore IRQ stack pointer and IME
|
||||
ldmia sp!, {r0-r2,lr}
|
||||
strh r1, [r2, #8]
|
||||
msr spsr_cf, r0
|
||||
|
||||
@ Return to BIOS IRQ handler
|
||||
bx lr
|
||||
.pool
|
||||
|
|
146
examples/irq.rs
Normal file
146
examples/irq.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
#![no_std]
|
||||
#![feature(start)]
|
||||
|
||||
use gba::{
|
||||
io::{
|
||||
display::{DisplayControlSetting, DisplayMode, DisplayStatusSetting, DISPCNT, DISPSTAT},
|
||||
irq::{self, IrqEnableSetting, IrqFlags, BIOS_IF, IE, IME},
|
||||
keypad::read_key_input,
|
||||
timers::{TimerControlSetting, TimerTickRate, TM0CNT_H, TM0CNT_L, TM1CNT_H, TM1CNT_L},
|
||||
},
|
||||
vram::bitmap::Mode3,
|
||||
Color,
|
||||
};
|
||||
|
||||
const BLACK: Color = Color::from_rgb(0, 0, 0);
|
||||
const RED: Color = Color::from_rgb(31, 0, 0);
|
||||
const GREEN: Color = Color::from_rgb(0, 31, 0);
|
||||
const BLUE: Color = Color::from_rgb(0, 0, 31);
|
||||
const YELLOW: Color = Color::from_rgb(31, 31, 0);
|
||||
const PINK: Color = Color::from_rgb(31, 0, 31);
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
fn start_timers() {
|
||||
let init_val: u16 = u32::wrapping_sub(0x1_0000, 64) as u16;
|
||||
const TIMER_SETTINGS: TimerControlSetting = TimerControlSetting::new().with_overflow_irq(true).with_enabled(true);
|
||||
|
||||
TM0CNT_L.write(init_val);
|
||||
TM0CNT_H.write(TIMER_SETTINGS.with_tick_rate(TimerTickRate::CPU1024));
|
||||
TM1CNT_L.write(init_val);
|
||||
TM1CNT_H.write(TIMER_SETTINGS.with_tick_rate(TimerTickRate::CPU64));
|
||||
}
|
||||
|
||||
#[start]
|
||||
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||
DISPCNT.write(DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true));
|
||||
Mode3::clear_to(BLACK);
|
||||
|
||||
// Set the IRQ handler to use.
|
||||
irq::set_irq_handler(irq_handler);
|
||||
|
||||
// Enable all interrupts that are set in the IE register.
|
||||
IME.write(IrqEnableSetting::UseIE);
|
||||
|
||||
// Request that VBlank, HBlank and VCount will generate IRQs.
|
||||
const DISPLAY_SETTINGS: DisplayStatusSetting = DisplayStatusSetting::new()
|
||||
.with_vblank_irq_enable(true)
|
||||
.with_hblank_irq_enable(true)
|
||||
.with_vcounter_irq_enable(true);
|
||||
DISPSTAT.write(DISPLAY_SETTINGS);
|
||||
|
||||
// Start two timers with overflow IRQ generation.
|
||||
start_timers();
|
||||
|
||||
loop {
|
||||
let this_frame_keys = read_key_input();
|
||||
|
||||
// The VBlank IRQ must be enabled at minimum, or else the CPU will halt
|
||||
// at the call to vblank_interrupt_wait() as the VBlank IRQ will never
|
||||
// be triggered.
|
||||
let mut flags = IrqFlags::new().with_vblank(true);
|
||||
|
||||
// Enable interrupts based on key input.
|
||||
if this_frame_keys.a() {
|
||||
flags = flags.with_hblank(true);
|
||||
}
|
||||
if this_frame_keys.b() {
|
||||
flags = flags.with_vcounter(true);
|
||||
}
|
||||
if this_frame_keys.l() {
|
||||
flags = flags.with_timer0(true);
|
||||
}
|
||||
if this_frame_keys.r() {
|
||||
flags = flags.with_timer1(true);
|
||||
}
|
||||
|
||||
IE.write(flags);
|
||||
|
||||
// Puts the CPU into low power mode until a VBlank IRQ is received. This
|
||||
// will yield considerably better power efficiency as opposed to spin
|
||||
// waiting.
|
||||
gba::bios::vblank_interrupt_wait();
|
||||
}
|
||||
}
|
||||
|
||||
static mut PIXEL: usize = 0;
|
||||
|
||||
fn write_pixel(color: Color) {
|
||||
unsafe {
|
||||
Mode3::write_pixel(PIXEL, 0, color);
|
||||
PIXEL = (PIXEL + 1) % Mode3::SCREEN_PIXEL_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn irq_handler(flags: IrqFlags) {
|
||||
if flags.vblank() {
|
||||
vblank_handler();
|
||||
}
|
||||
if flags.hblank() {
|
||||
hblank_handler();
|
||||
}
|
||||
if flags.vcounter() {
|
||||
vcounter_handler();
|
||||
}
|
||||
if flags.timer0() {
|
||||
timer0_handler();
|
||||
}
|
||||
if flags.timer1() {
|
||||
timer1_handler();
|
||||
}
|
||||
}
|
||||
|
||||
fn vblank_handler() {
|
||||
write_pixel(BLUE);
|
||||
|
||||
// When using `interrupt_wait()` or `vblank_interrupt_wait()`, IRQ handlers must acknowledge
|
||||
// the IRQ on the BIOS Interrupt Flags register.
|
||||
BIOS_IF.write(BIOS_IF.read().with_vblank(true));
|
||||
}
|
||||
|
||||
fn hblank_handler() {
|
||||
write_pixel(GREEN);
|
||||
|
||||
BIOS_IF.write(BIOS_IF.read().with_hblank(true));
|
||||
}
|
||||
|
||||
fn vcounter_handler() {
|
||||
write_pixel(RED);
|
||||
|
||||
BIOS_IF.write(BIOS_IF.read().with_vcounter(true));
|
||||
}
|
||||
|
||||
fn timer0_handler() {
|
||||
write_pixel(YELLOW);
|
||||
|
||||
BIOS_IF.write(BIOS_IF.read().with_timer0(true));
|
||||
}
|
||||
|
||||
fn timer1_handler() {
|
||||
write_pixel(PINK);
|
||||
|
||||
BIOS_IF.write(BIOS_IF.read().with_timer1(true));
|
||||
}
|
17
src/bios.rs
17
src/bios.rs
|
@ -11,6 +11,7 @@
|
|||
#![cfg_attr(not(all(target_vendor = "nintendo", target_env = "agb")), allow(unused_variables))]
|
||||
|
||||
use super::*;
|
||||
use io::irq::IrqFlags;
|
||||
|
||||
//TODO: ALL functions in this module should have `if cfg!(test)` blocks. The
|
||||
//functions that never return must panic, the functions that return nothing
|
||||
|
@ -184,16 +185,16 @@ pub fn stop() {
|
|||
/// * The first argument controls if you want to ignore all current flags and
|
||||
/// wait until a new flag is set.
|
||||
/// * The second argument is what flags you're waiting on (same format as the
|
||||
/// IE/IF registers).
|
||||
/// [`IE`](io::irq::IE)/[`IF`](io::irq::IF) registers).
|
||||
///
|
||||
/// If you're trying to handle more than one interrupt at once this has less
|
||||
/// overhead than calling `halt` over and over.
|
||||
///
|
||||
/// When using this routing your interrupt handler MUST update the BIOS
|
||||
/// Interrupt Flags `0x300_7FF8` in addition to the usual interrupt
|
||||
/// acknowledgement.
|
||||
/// Interrupt Flags at [`BIOS_IF`](io::irq::BIOS_IF) in addition to
|
||||
/// the usual interrupt acknowledgement.
|
||||
#[inline(always)]
|
||||
pub fn interrupt_wait(ignore_current_flags: bool, target_flags: u16) {
|
||||
pub fn interrupt_wait(ignore_current_flags: bool, target_flags: IrqFlags) {
|
||||
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))]
|
||||
{
|
||||
unimplemented!()
|
||||
|
@ -203,19 +204,19 @@ pub fn interrupt_wait(ignore_current_flags: bool, target_flags: u16) {
|
|||
unsafe {
|
||||
asm!(/* ASM */ "swi 0x04"
|
||||
:/* OUT */ // none
|
||||
:/* INP */ "{r0}"(ignore_current_flags), "{r1}"(target_flags)
|
||||
:/* INP */ "{r0}"(ignore_current_flags), "{r1}"(target_flags.0)
|
||||
:/* CLO */ // none
|
||||
:/* OPT */ "volatile"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO(lokathor): newtype this flag business.
|
||||
|
||||
/// (`swi 0x05`) "VBlankIntrWait", VBlank Interrupt Wait.
|
||||
///
|
||||
/// This is as per `interrupt_wait(true, 1)` (aka "wait for a new vblank"). You
|
||||
/// must follow the same guidelines that `interrupt_wait` outlines.
|
||||
/// This is as per `interrupt_wait(true, IrqFlags::new().with_vblank(true))`
|
||||
/// (aka "wait for a new vblank"). You must follow the same guidelines that
|
||||
/// [`interrupt_wait`](interrupt_wait) outlines.
|
||||
#[inline(always)]
|
||||
pub fn vblank_interrupt_wait() {
|
||||
#[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))]
|
||||
|
|
|
@ -12,6 +12,7 @@ pub mod background;
|
|||
pub mod color_blend;
|
||||
pub mod display;
|
||||
pub mod dma;
|
||||
pub mod irq;
|
||||
pub mod keypad;
|
||||
pub mod sound;
|
||||
pub mod timers;
|
||||
|
|
|
@ -133,14 +133,28 @@ pub fn vcount() -> u16 {
|
|||
}
|
||||
|
||||
/// Performs a busy loop until VBlank starts.
|
||||
///
|
||||
/// NOTE: This method isn't very power efficient, since it is equivalent to
|
||||
/// calling "halt" repeatedly. The recommended way to wait for a VBlank or VDraw
|
||||
/// is to set an IRQ handler with
|
||||
/// [`io::irq::set_irq_handler`](`io::irq::set_irq_handler`) and using
|
||||
/// [`bios::vblank_intr_wait`](bios::vblank_interrupt_wait) to sleep the CPU
|
||||
/// until a VBlank IRQ is generated. See the [`io::irq`](io::irq) module for
|
||||
/// more details.
|
||||
pub fn spin_until_vblank() {
|
||||
// TODO: make this the better version with BIOS and interrupts and such.
|
||||
while vcount() < VBLANK_SCANLINE {}
|
||||
}
|
||||
|
||||
/// Performs a busy loop until VDraw starts.
|
||||
///
|
||||
/// NOTE: This method isn't very power efficient, since it is equivalent to
|
||||
/// calling "halt" repeatedly. The recommended way to wait for a VBlank or VDraw
|
||||
/// is to set an IRQ handler with
|
||||
/// [`io::irq::set_irq_handler`](`io::irq::set_irq_handler`) and using
|
||||
/// [`bios::vblank_intr_wait`](bios::vblank_interrupt_wait) to sleep the CPU
|
||||
/// until a VBlank IRQ is generated. See the [`io::irq`](io::irq) module for
|
||||
/// more details.
|
||||
pub fn spin_until_vdraw() {
|
||||
// TODO: make this the better version with BIOS and interrupts and such.
|
||||
while vcount() >= VBLANK_SCANLINE {}
|
||||
}
|
||||
|
||||
|
|
180
src/io/irq.rs
Normal file
180
src/io/irq.rs
Normal file
|
@ -0,0 +1,180 @@
|
|||
//! Module containing a wrapper for interrupt request (IRQ) handling.
|
||||
//!
|
||||
//! When an interrupt is executed, the CPU will be set to IRQ mode and code
|
||||
//! execution will jump to the physical interrupt vector, located in BIOS. The
|
||||
//! BIOS interrupt handler will then save several registers to the IRQ stack
|
||||
//! pointer and execution will jump to the user interrupt handler starting at
|
||||
//! `0x0300_7FFC`, in ARM mode.
|
||||
//!
|
||||
//! Currently, the user interrupt handler is defined in `crt0.s`. It is set up
|
||||
//! to execute a user-specified interrupt handler after saving some registers.
|
||||
//! This handler is declared as a static function pointer on the Rust side, and
|
||||
//! can be set by using [`set_irq_handler`](irq::set_irq_handler).
|
||||
//!
|
||||
//! ## Notes
|
||||
//! * The interrupt will only be triggered if [`IME`](irq::IME) is enabled, the
|
||||
//! flag corresponding to the interrupt is enabled on the [`IE`](irq::IE)
|
||||
//! register, and the "IRQ Enable" flag is set on the register related to the
|
||||
//! interrupt, which varies. For example, to enable interrupts on VBlank you
|
||||
//! would set the
|
||||
//! [`vblank_irq_enable`](io::display::DisplayStatusSetting::vblank_irq_enable)
|
||||
//! flag on the [`DISPSTAT`](io::display::DISPCNT) register.
|
||||
//! * If you intend to use [`interrupt_wait`](bios::interrupt_wait) or
|
||||
//! [`vblank_interrupt_wait`](bios::vblank_interrupt_wait) to wait for an
|
||||
//! interrupt, your interrupt handler MUST update the BIOS Interrupt Flags at
|
||||
//! [`BIOS_IF`](irq::BIOS_IF) in addition to the usual interrupt
|
||||
//! acknowledgement (which is handled for you by the user interrupt handler).
|
||||
//! This is done by setting the corresponding IRQ flag on
|
||||
//! [`BIOS_IF`](irq::BIOS_IF) at the end of the interrupt handler.
|
||||
//! * You can change the low-level details of the interrupt handler by editing
|
||||
//! the `MainIrqHandler` routine in `crt0.s`. For example, you could declare
|
||||
//! an external static variable in Rust holding a table of interrupt function
|
||||
//! pointers and jump directly into one of them in assembly, without the need
|
||||
//! to write the branching logic in Rust. However, note that the main
|
||||
//! interrupt handler MUST acknowledge all interrupts received by setting
|
||||
//! their corresponding bits to `1` in the [`IF`](irq::IF) register.
|
||||
//! * If you wait on one or more interrupts, be sure at least one of them is
|
||||
//! able to be triggered or the call to wait will never return.
|
||||
//! * If you wait on multiple interrupts and those interrupts fire too quickly,
|
||||
//! it is possible that the call to wait will never return as interrupts will
|
||||
//! be constantly received before control is returned to the caller. This
|
||||
//! usually only happens when waiting on multiple timer interrupts with very
|
||||
//! fast overflow rates.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! extern "C" fn irq_handler(flags: IrqFlags) {
|
||||
//! if flags.vblank() {
|
||||
//! // Run drawing logic here.
|
||||
//!
|
||||
//! // Acknowledge the IRQ on the BIOS Interrupt Flags register.
|
||||
//! BIOS_IF.write(BIOS_IF.read().with_vblank(true));
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn main_loop() {
|
||||
//! // Set the IRQ handler to use.
|
||||
//! irq::set_irq_handler(irq_handler);
|
||||
//!
|
||||
//! // Handle only the VBlank interrupt.
|
||||
//! const FLAGS: IrqFlags = IrqFlags::new().with_vblank(true);
|
||||
//! IE.write(flags);
|
||||
//!
|
||||
//! // Enable all interrupts that are set in the IE register.
|
||||
//! IME.write(IrqEnableSetting::UseIE);
|
||||
//!
|
||||
//! // Enable IRQ generation during VBlank.
|
||||
//! const DISPLAY_SETTINGS: DisplayStatusSetting = DisplayStatusSetting::new()
|
||||
//! .with_vblank_irq_enable(true);
|
||||
//! DISPSTAT.write(DISPLAY_SETTINGS);
|
||||
//!
|
||||
//! loop {
|
||||
//! // Sleep the CPU until a VBlank IRQ is generated.
|
||||
//! bios::vblank_interrupt_wait();
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Implementation Details
|
||||
//!
|
||||
//! This is the setup the provided user interrupt handler in `crt0.s` will do
|
||||
//! when an interrupt is received, in order. It is based on the _Recommended
|
||||
//! User Interrupt Handling_ portion of the GBATEK reference.
|
||||
//!
|
||||
//! 1. Save the status of [`IME`](irq::IME).
|
||||
//! 2. Save the IRQ stack pointer and change to system mode to use the user
|
||||
//! stack instead of the IRQ stack (to prevent stack overflow).
|
||||
//! 3. Disable interrupts by setting [`IME`](irq::IME) to 0, so other interrupts
|
||||
//! will not preempt the main interrupt handler.
|
||||
//! 4. Acknowledge all IRQs that occurred and were enabled in the
|
||||
//! [`IE`](irq::IE) register by writing the bits to the [`IF`](irq::IF)
|
||||
//! register.
|
||||
//! 5. Save the user stack pointer, switch to Thumb mode and jump to the
|
||||
//! user-specified interrupt handler. The IRQ flags that were set are passed
|
||||
//! as an argument in `r0`.
|
||||
//! 6. When the handler returns, restore the user stack pointer and switch back
|
||||
//! to IRQ mode.
|
||||
//! 7. Restore the IRQ stack pointer and the status of [`IME`](irq::IME).
|
||||
//! 8. Return to the BIOS interrupt handler.
|
||||
|
||||
use super::*;
|
||||
|
||||
newtype!(
|
||||
/// A newtype over all interrupt flags.
|
||||
IrqFlags, pub u16
|
||||
);
|
||||
|
||||
impl IrqFlags {
|
||||
phantom_fields! {
|
||||
self.0: u16,
|
||||
vblank: 0,
|
||||
hblank: 1,
|
||||
vcounter: 2,
|
||||
timer0: 3,
|
||||
timer1: 4,
|
||||
timer2: 5,
|
||||
timer3: 6,
|
||||
serial: 7,
|
||||
dma0: 8,
|
||||
dma1: 9,
|
||||
dma2: 10,
|
||||
dma3: 11,
|
||||
keypad: 12,
|
||||
game_pak: 13,
|
||||
}
|
||||
}
|
||||
|
||||
/// Interrupt Enable Register. Read/Write.
|
||||
///
|
||||
/// After setting up interrupt handlers, set the flags on this register type corresponding to the
|
||||
/// IRQs you want to handle.
|
||||
pub const IE: VolAddress<IrqFlags> = unsafe { VolAddress::new(0x400_0200) };
|
||||
|
||||
/// Interrupt Request Flags / IRQ Acknowledge. Read/Write.
|
||||
///
|
||||
/// The main user interrupt handler will acknowledge the interrupt that was set
|
||||
/// by writing to this register, so there is usually no need to modify it.
|
||||
/// However, if the main interrupt handler in `crt0.s` is changed, then the
|
||||
/// handler must write a `1` bit to all bits that are enabled on this register
|
||||
/// when it is called.
|
||||
pub const IF: VolAddress<IrqFlags> = unsafe { VolAddress::new(0x400_0200) };
|
||||
|
||||
newtype_enum! {
|
||||
/// Setting to control whether interrupts are enabled.
|
||||
IrqEnableSetting = u32,
|
||||
/// Disable all interrupts.
|
||||
DisableAll = 0,
|
||||
/// Enable interrupts according to the flags set in the [`IE`](irq::IE) register.
|
||||
UseIE = 1,
|
||||
}
|
||||
|
||||
/// Interrupt Master Enable Register. Read/Write.
|
||||
pub const IME: VolAddress<IrqEnableSetting> = unsafe { VolAddress::new(0x400_0208) };
|
||||
|
||||
/// BIOS Interrupt Flags. Read/Write.
|
||||
///
|
||||
/// When using either [`interrupt_wait`](bios::interrupt_wait) or
|
||||
/// [`vblank_interrupt_wait`](bios::vblank_interrupt_wait), the corresponding
|
||||
/// interrupt handler MUST set the flag of the interrupt it has handled on this
|
||||
/// register in addition to the usual interrupt acknowledgement.
|
||||
pub const BIOS_IF: VolAddress<IrqFlags> = unsafe { VolAddress::new(0x0300_7FF8) };
|
||||
|
||||
/// A function pointer for use as an interrupt handler.
|
||||
pub type IrqHandler = extern "C" fn(IrqFlags);
|
||||
|
||||
/// Sets the function to run when an interrupt is executed. The function will
|
||||
/// receive the interrupts that were acknowledged by the main interrupt handler
|
||||
/// as an argument.
|
||||
pub fn set_irq_handler(handler: IrqHandler) {
|
||||
unsafe {
|
||||
__IRQ_HANDLER = handler;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn default_handler(_flags: IrqFlags) {}
|
||||
|
||||
// Inner definition of the interrupt handler. It is referenced in `crt0.s`.
|
||||
#[doc(hidden)]
|
||||
#[no_mangle]
|
||||
static mut __IRQ_HANDLER: IrqHandler = default_handler;
|
|
@ -101,8 +101,9 @@ newtype! {
|
|||
/// of the interrupt firing.
|
||||
///
|
||||
/// NOTE: This _only_ configures the operation of when keypad interrupts can
|
||||
/// fire. You must still set the `IME` to have interrupts at all, and you must
|
||||
/// further set `IE` for keypad interrupts to be possible.
|
||||
/// fire. You must still set the [`IME`](irq::IME) to have interrupts at all,
|
||||
/// and you must further set [`IE`](irq::IE) for keypad interrupts to be
|
||||
/// possible.
|
||||
KeyInterruptSetting, u16
|
||||
}
|
||||
#[allow(missing_docs)]
|
||||
|
|
Loading…
Reference in a new issue