#![no_std]
#![allow(clippy::unusual_byte_groupings)]
#![allow(clippy::upper_case_acronyms)]
pub use arrayvec::ArrayVec;
use core::convert::TryFrom;
use num_enum::TryFromPrimitive;
pub const RP2040_MAX_PROGRAM_SIZE: usize = 32;
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum JmpCondition {
Always = 0b000,
XIsZero = 0b001,
XDecNonZero = 0b010,
YIsZero = 0b011,
YDecNonZero = 0b100,
XNotEqualY = 0b101,
PinHigh = 0b110,
OutputShiftRegisterNotEmpty = 0b111,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive, PartialEq, Eq)]
pub enum WaitSource {
GPIO = 0b00,
PIN = 0b01,
IRQ = 0b10,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum InSource {
PINS = 0b000,
X = 0b001,
Y = 0b010,
NULL = 0b011,
ISR = 0b110,
OSR = 0b111,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum OutDestination {
PINS = 0b000,
X = 0b001,
Y = 0b010,
NULL = 0b011,
PINDIRS = 0b100,
PC = 0b101,
ISR = 0b110,
EXEC = 0b111,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum MovDestination {
PINS = 0b000,
X = 0b001,
Y = 0b010,
EXEC = 0b100,
PC = 0b101,
ISR = 0b110,
OSR = 0b111,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum MovOperation {
None = 0b00,
Invert = 0b01,
BitReverse = 0b10,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum MovSource {
PINS = 0b000,
X = 0b001,
Y = 0b010,
NULL = 0b011,
STATUS = 0b101,
ISR = 0b110,
OSR = 0b111,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
pub enum SetDestination {
PINS = 0b000,
X = 0b001,
Y = 0b010,
PINDIRS = 0b100,
}
#[derive(Debug)]
pub enum InstructionOperands {
JMP {
condition: JmpCondition,
address: u8,
},
WAIT {
polarity: u8,
source: WaitSource,
index: u8,
relative: bool,
},
IN {
source: InSource,
bit_count: u8,
},
OUT {
destination: OutDestination,
bit_count: u8,
},
PUSH {
if_full: bool,
block: bool,
},
PULL {
if_empty: bool,
block: bool,
},
MOV {
destination: MovDestination,
op: MovOperation,
source: MovSource,
},
IRQ {
clear: bool,
wait: bool,
index: u8,
relative: bool,
},
SET {
destination: SetDestination,
data: u8,
},
}
impl InstructionOperands {
const fn discrim(&self) -> u16 {
match self {
InstructionOperands::JMP { .. } => 0b000,
InstructionOperands::WAIT { .. } => 0b001,
InstructionOperands::IN { .. } => 0b010,
InstructionOperands::OUT { .. } => 0b011,
InstructionOperands::PUSH { .. } => 0b100,
InstructionOperands::PULL { .. } => 0b100,
InstructionOperands::MOV { .. } => 0b101,
InstructionOperands::IRQ { .. } => 0b110,
InstructionOperands::SET { .. } => 0b111,
}
}
const fn operands(&self) -> (u8, u8) {
match self {
InstructionOperands::JMP { condition, address } => (*condition as u8, *address),
InstructionOperands::WAIT {
polarity,
source,
index,
relative,
} => {
if *relative && !matches!(*source, WaitSource::IRQ) {
panic!("relative flag should only be used with WaitSource::IRQ");
}
if matches!(*source, WaitSource::IRQ) && *index > 7 {
panic!("Index for WaitSource::IRQ should be in range 0..=7");
}
(
(*polarity) << 2 | (*source as u8),
*index | (if *relative { 0b10000 } else { 0 }),
)
}
InstructionOperands::IN { source, bit_count } => (*source as u8, *bit_count),
InstructionOperands::OUT {
destination,
bit_count,
} => (*destination as u8, *bit_count & 0b11111),
InstructionOperands::PUSH { if_full, block } => {
((*if_full as u8) << 1 | (*block as u8), 0)
}
InstructionOperands::PULL { if_empty, block } => {
(1 << 2 | (*if_empty as u8) << 1 | (*block as u8), 0)
}
InstructionOperands::MOV {
destination,
op,
source,
} => (*destination as u8, (*op as u8) << 3 | (*source as u8)),
InstructionOperands::IRQ {
clear,
wait,
index,
relative,
} => {
if *index > 7 {
panic!("invalid interrupt flags");
}
(
(*clear as u8) << 1 | (*wait as u8),
*index | (if *relative { 0b10000 } else { 0 }),
)
}
InstructionOperands::SET { destination, data } => (*destination as u8, *data),
}
}
pub const fn encode(&self) -> u16 {
let mut data: u16 = 0;
data |= self.discrim() << 13;
let (o0, o1) = self.operands();
data |= (o0 as u16) << 5;
data |= o1 as u16;
data
}
pub fn decode(instruction: u16) -> Option<Self> {
let discrim = instruction >> 13;
let o0 = ((instruction >> 5) & 0b111) as u8;
let o1 = (instruction & 0b11111) as u8;
match discrim {
0b000 => JmpCondition::try_from(o0)
.ok()
.map(|condition| InstructionOperands::JMP {
condition,
address: o1,
}),
0b001 => {
WaitSource::try_from(o0 & 0b011)
.ok()
.map(|source| InstructionOperands::WAIT {
polarity: o0 >> 2,
source,
index: if source == WaitSource::IRQ {
o1 & 0b00111
} else {
o1
},
relative: source == WaitSource::IRQ && (o1 & 0b10000) != 0,
})
}
0b010 => InSource::try_from(o0)
.ok()
.map(|source| InstructionOperands::IN {
source,
bit_count: o1,
}),
0b011 => {
OutDestination::try_from(o0)
.ok()
.map(|destination| InstructionOperands::OUT {
destination,
bit_count: o1,
})
}
0b100 => {
let if_flag = o0 & 0b010 != 0;
let block = o0 & 0b001 != 0;
if o1 != 0 {
None
} else if o0 & 0b100 == 0 {
Some(InstructionOperands::PUSH {
if_full: if_flag,
block,
})
} else {
Some(InstructionOperands::PULL {
if_empty: if_flag,
block,
})
}
}
0b101 => match (
MovDestination::try_from(o0).ok(),
MovOperation::try_from((o1 >> 3) & 0b11).ok(),
MovSource::try_from(o1 & 0b111).ok(),
) {
(Some(destination), Some(op), Some(source)) => Some(InstructionOperands::MOV {
destination,
op,
source,
}),
_ => None,
},
0b110 => {
if o0 & 0b100 == 0 {
Some(InstructionOperands::IRQ {
clear: o0 & 0b010 != 0,
wait: o0 & 0b001 != 0,
index: o1 & 0b01111,
relative: o1 & 0b10000 != 0,
})
} else {
None
}
}
0b111 => {
SetDestination::try_from(o0)
.ok()
.map(|destination| InstructionOperands::SET {
destination,
data: o1,
})
}
_ => None,
}
}
}
#[derive(Debug)]
pub struct Instruction {
pub operands: InstructionOperands,
pub delay: u8,
pub side_set: Option<u8>,
}
impl Instruction {
pub fn encode(&self, side_set: SideSet) -> u16 {
let delay_max = (1 << (5 - side_set.bits)) - 1;
let mut data = self.operands.encode();
if self.delay > delay_max {
panic!(
"delay of {} is greater than limit {}",
self.delay, delay_max
);
}
let side_set = if let Some(s) = self.side_set {
if s > side_set.max {
panic!("'side' set must be >=0 and <={}", side_set.max);
}
let s = (s as u16) << (5 - side_set.bits);
if side_set.opt {
s | 0b10000
} else {
s
}
} else if side_set.bits > 0 && !side_set.opt {
panic!("instruction requires 'side' set");
} else {
0
};
data |= ((self.delay as u16) | side_set) << 8;
data
}
pub fn decode(instruction: u16, side_set: SideSet) -> Option<Instruction> {
InstructionOperands::decode(instruction).map(|operands| {
let data = ((instruction >> 8) & 0b11111) as u8;
let delay = data & ((1 << (5 - side_set.bits)) - 1);
let has_side_set = side_set.bits > 0 && (!side_set.opt || data & 0b10000 > 0);
let side_set_data =
(data & if side_set.opt { 0b01111 } else { 0b11111 }) >> (5 - side_set.bits);
let side_set = if has_side_set {
Some(side_set_data)
} else {
None
};
Instruction {
operands,
delay,
side_set,
}
})
}
}
#[derive(Debug)]
enum LabelState {
Unbound(u8),
Bound(u8),
}
#[derive(Debug)]
pub struct Label {
state: LabelState,
}
impl Drop for Label {
fn drop(&mut self) {
if let LabelState::Unbound(_) = self.state {
panic!("label was not bound");
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SideSet {
opt: bool,
bits: u8,
max: u8,
pindirs: bool,
}
impl SideSet {
pub const fn new(opt: bool, bits: u8, pindirs: bool) -> SideSet {
SideSet {
opt,
bits: bits + opt as u8,
max: (1 << bits) - 1,
pindirs,
}
}
#[doc(hidden)]
pub fn new_from_proc_macro(opt: bool, bits: u8, pindirs: bool) -> SideSet {
SideSet {
opt,
bits,
max: (1 << bits) - 1,
pindirs,
}
}
pub fn optional(&self) -> bool {
self.opt
}
pub fn bits(&self) -> u8 {
self.bits
}
pub fn pindirs(&self) -> bool {
self.pindirs
}
}
impl Default for SideSet {
fn default() -> Self {
SideSet::new(false, 0, false)
}
}
#[derive(Debug)]
pub struct Assembler<const PROGRAM_SIZE: usize> {
#[doc(hidden)]
pub instructions: ArrayVec<Instruction, PROGRAM_SIZE>,
#[doc(hidden)]
pub side_set: SideSet,
}
impl<const PROGRAM_SIZE: usize> Assembler<PROGRAM_SIZE> {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Assembler::new_with_side_set(SideSet::default())
}
#[allow(clippy::new_without_default)]
pub fn new_with_side_set(side_set: SideSet) -> Self {
Assembler {
instructions: ArrayVec::new(),
side_set,
}
}
pub fn assemble(self) -> ArrayVec<u16, PROGRAM_SIZE> {
self.instructions
.iter()
.map(|i| i.encode(self.side_set))
.collect()
}
pub fn assemble_program(self) -> Program<PROGRAM_SIZE> {
let side_set = self.side_set;
let code = self.assemble();
let wrap = Wrap {
source: (code.len() - 1) as u8,
target: 0,
};
Program {
code,
origin: None,
side_set,
wrap,
}
}
pub fn assemble_with_wrap(self, source: Label, target: Label) -> Program<PROGRAM_SIZE> {
let source = self.label_offset(&source) - 1;
let target = self.label_offset(&target);
self.assemble_program().set_wrap(Wrap { source, target })
}
pub fn label_offset(&self, label: &Label) -> u8 {
match &label.state {
LabelState::Bound(offset) => *offset,
LabelState::Unbound(_) => panic!("can't get offset for unbound label"),
}
}
}
impl<const PROGRAM_SIZE: usize> Assembler<PROGRAM_SIZE> {
pub fn label(&mut self) -> Label {
Label {
state: LabelState::Unbound(core::u8::MAX),
}
}
pub fn label_at_offset(&mut self, offset: u8) -> Label {
Label {
state: LabelState::Bound(offset),
}
}
pub fn bind(&mut self, label: &mut Label) {
match label.state {
LabelState::Bound(_) => panic!("cannot bind label twice"),
LabelState::Unbound(mut patch) => {
let resolved_address = self.instructions.len() as u8;
while patch != core::u8::MAX {
let instr = unsafe { self.instructions.get_unchecked_mut(patch as usize) };
if let InstructionOperands::JMP { address, .. } = &mut instr.operands {
patch = *address;
*address = resolved_address;
} else {
unreachable!();
}
}
label.state = LabelState::Bound(resolved_address);
}
}
}
}
macro_rules! instr_impl {
( $(#[$inner:ident $($args:tt)*])* $name:ident ( $self:ident $(, $( $arg_name:ident : $arg_ty:ty ),*)? ) $body:expr, $delay:expr, $side_set:expr ) => {
$(#[$inner $($args)*])*
pub fn $name(
&mut $self
$(, $( $arg_name : $arg_ty , )*)?
) {
$self.instructions.push(Instruction {
operands: $body,
delay: $delay,
side_set: $side_set,
})
}
}
}
macro_rules! instr {
( $(#[$inner:ident $($args:tt)*])* $name:ident ( $self:ident $(, $($arg_name:ident : $arg_ty:ty ),*)? ) $body:expr ) => {
instr_impl!($(#[$inner $($args)*])* $name ( $self $(, $( $arg_name: $arg_ty ),*)? ) $body, 0, None );
paste::paste! {
instr_impl!($(#[$inner $($args)*])* [< $name _with_delay >] ( $self $(, $( $arg_name: $arg_ty ),*)? , delay: u8 ) $body, delay, None );
instr_impl!($(#[$inner $($args)*])* [< $name _with_side_set >] ( $self $(, $( $arg_name: $arg_ty ),*)? , side_set: u8 ) $body, 0, Some(side_set) );
instr_impl!($(#[$inner $($args)*])* [< $name _with_delay_and_side_set >] ( $self $(, $( $arg_name: $arg_ty ),*)? , delay: u8, side_set: u8 ) $body, delay, Some(side_set) );
}
}
}
impl<const PROGRAM_SIZE: usize> Assembler<PROGRAM_SIZE> {
instr!(
jmp(self, condition: JmpCondition, label: &mut Label) {
let address = match label.state {
LabelState::Unbound(a) => {
label.state = LabelState::Unbound(self.instructions.len() as u8);
a
}
LabelState::Bound(a) => a,
};
InstructionOperands::JMP {
condition,
address,
}
}
);
instr!(
wait(self, polarity: u8, source: WaitSource, index: u8, relative: bool) {
InstructionOperands::WAIT {
polarity,
source,
index,
relative,
}
}
);
instr!(
r#in(self, source: InSource, bit_count: u8) {
InstructionOperands::IN { source, bit_count }
}
);
instr!(
out(self, destination: OutDestination, bit_count: u8) {
InstructionOperands::OUT {
destination,
bit_count,
}
}
);
instr!(
push(self, if_full: bool, block: bool) {
InstructionOperands::PUSH {
if_full,
block,
}
}
);
instr!(
pull(self, if_empty: bool, block: bool) {
InstructionOperands::PULL {
if_empty,
block,
}
}
);
instr!(
mov(self, destination: MovDestination, op: MovOperation, source: MovSource) {
InstructionOperands::MOV {
destination,
op,
source,
}
}
);
instr!(
irq(self, clear: bool, wait: bool, index: u8, relative: bool) {
InstructionOperands::IRQ {
clear,
wait,
index,
relative,
}
}
);
instr!(
set(self, destination: SetDestination, data: u8) {
InstructionOperands::SET {
destination,
data,
}
}
);
instr!(
nop(self) {
InstructionOperands::MOV {
destination: MovDestination::Y,
op: MovOperation::None,
source: MovSource::Y
}
}
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Wrap {
pub source: u8,
pub target: u8,
}
#[derive(Debug)]
pub struct Program<const PROGRAM_SIZE: usize> {
pub code: ArrayVec<u16, PROGRAM_SIZE>,
pub origin: Option<u8>,
pub wrap: Wrap,
pub side_set: SideSet,
}
impl<const PROGRAM_SIZE: usize> Program<PROGRAM_SIZE> {
pub fn set_origin(self, origin: Option<u8>) -> Self {
Self { origin, ..self }
}
pub fn set_wrap(self, wrap: Wrap) -> Self {
assert!((wrap.source as usize) < self.code.len());
assert!((wrap.target as usize) < self.code.len());
Self { wrap, ..self }
}
}
pub struct ProgramWithDefines<PublicDefines, const PROGRAM_SIZE: usize> {
pub program: Program<PROGRAM_SIZE>,
pub public_defines: PublicDefines,
}
#[test]
fn test_jump_1() {
let mut a = Assembler::<32>::new();
let mut l = a.label();
a.set(SetDestination::X, 0);
a.bind(&mut l);
a.set(SetDestination::X, 1);
a.jmp(JmpCondition::Always, &mut l);
assert_eq!(
a.assemble().as_slice(),
&[
0b111_00000_001_00000, 0b111_00000_001_00001, 0b000_00000_000_00001, ]
);
}
#[test]
fn test_jump_2() {
let mut a = Assembler::<32>::new();
let mut top = a.label();
let mut bottom = a.label();
a.bind(&mut top);
a.set(SetDestination::Y, 0);
a.jmp(JmpCondition::YIsZero, &mut bottom);
a.jmp(JmpCondition::Always, &mut top);
a.bind(&mut bottom);
a.set(SetDestination::Y, 1);
assert_eq!(
a.assemble().as_slice(),
&[
0b111_00000_010_00000, 0b000_00000_011_00011, 0b000_00000_000_00000, 0b111_00000_010_00001, ]
);
}
#[test]
fn test_assemble_with_wrap() {
let mut a = Assembler::<32>::new();
let mut source = a.label();
let mut target = a.label();
a.set(SetDestination::PINDIRS, 0);
a.bind(&mut target);
a.r#in(InSource::NULL, 1);
a.push(false, false);
a.bind(&mut source);
a.jmp(JmpCondition::Always, &mut target);
assert_eq!(
a.assemble_with_wrap(source, target).wrap,
Wrap {
source: 2,
target: 1,
}
);
}
#[test]
fn test_assemble_program_default_wrap() {
let mut a = Assembler::<32>::new();
a.set(SetDestination::PINDIRS, 0);
a.r#in(InSource::NULL, 1);
a.push(false, false);
assert_eq!(
a.assemble_program().wrap,
Wrap {
source: 2,
target: 0,
}
);
}
macro_rules! instr_test {
($name:ident ( $( $v:expr ),* ) , $expected:expr, $side_set:expr) => {
paste::paste! {
#[test]
fn [< test _ $name _ $expected >]() {
let expected = $expected;
let mut a = Assembler::<32>::new_with_side_set($side_set);
a.$name(
$( $v ),*
);
let instr = a.assemble()[0];
if instr != expected {
panic!("assertion failure: (left == right)\nleft: {:#016b}\nright: {:#016b}", instr, expected);
}
let decoded = Instruction::decode(instr, $side_set).unwrap();
let encoded = decoded.encode($side_set);
if encoded != expected {
panic!("assertion failure: (left == right)\nleft: {:#016b}\nright: {:#016b}", encoded, expected);
}
}
}
};
($name:ident ( $( $v:expr ),* ) , $b:expr) => {
instr_test!( $name ( $( $v ),* ), $b, SideSet::new(false, 0, false) );
};
}
instr_test!(wait(0, WaitSource::IRQ, 2, false), 0b001_00000_010_00010);
instr_test!(wait(1, WaitSource::IRQ, 7, false), 0b001_00000_110_00111);
instr_test!(wait(1, WaitSource::GPIO, 16, false), 0b001_00000_100_10000);
instr_test!(
wait_with_delay(0, WaitSource::IRQ, 2, false, 30),
0b001_11110_010_00010
);
instr_test!(
wait_with_side_set(0, WaitSource::IRQ, 2, false, 0b10101),
0b001_10101_010_00010,
SideSet::new(false, 5, false)
);
instr_test!(wait(0, WaitSource::IRQ, 2, true), 0b001_00000_010_10010);
#[test]
#[should_panic]
fn test_wait_relative_not_used_on_irq() {
let mut a = Assembler::<32>::new();
a.wait(0, WaitSource::PIN, 10, true);
a.assemble_program();
}
instr_test!(r#in(InSource::Y, 10), 0b010_00000_010_01010);
instr_test!(out(OutDestination::Y, 10), 0b011_00000_010_01010);
instr_test!(push(true, false), 0b100_00000_010_00000);
instr_test!(push(false, true), 0b100_00000_001_00000);
instr_test!(pull(true, false), 0b100_00000_110_00000);
instr_test!(pull(false, true), 0b100_00000_101_00000);
instr_test!(
mov(
MovDestination::Y,
MovOperation::BitReverse,
MovSource::STATUS
),
0b101_00000_010_10101
);
instr_test!(irq(true, false, 0b11, false), 0b110_00000_010_00011);
instr_test!(irq(false, true, 0b111, true), 0b110_00000_001_10111);
instr_test!(set(SetDestination::Y, 10), 0b111_00000_010_01010);
#[cfg(doctest)]
mod test_readme {
macro_rules! external_doc_test {
($x:expr) => {
#[doc = $x]
extern "C" {}
};
}
external_doc_test!(include_str!("../README.md"));
}