Working serial connection with handshake & props

This commit is contained in:
asonix 2021-06-05 11:13:12 -05:00
commit 81f76202f7
9 changed files with 481 additions and 0 deletions

40
.cargo/config Normal file
View file

@ -0,0 +1,40 @@
[target.thumbv7m-none-eabi]
# uncomment this to make `cargo run` execute programs on QEMU
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# uncomment ONE of these three option to make `cargo run` start a GDB session
# which option to pick depends on your system
# runner = "arm-none-eabi-gdb -q -x openocd.gdb"
runner = "gdb-multiarch -q -x openocd.gdb"
# runner = "gdb -q -x openocd.gdb"
rustflags = [
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
# LLD (shipped with the Rust toolchain) is used as the default linker
"-C", "link-arg=-Tlink.x",
# if you run into problems with LLD switch to the GNU linker by commenting out
# this line
# "-C", "linker=arm-none-eabi-ld",
# if you need to link to pre-compiled C libraries provided by a C toolchain
# use GCC as the linker by commenting out both lines above and then
# uncommenting the three lines below
# "-C", "linker=arm-none-eabi-gcc",
# "-C", "link-arg=-Wl,-Tlink.x",
# "-C", "link-arg=-nostartfiles",
]
[build]
# Pick ONE of these compilation targets
target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
# target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
# target = "thumbv8m.base-none-eabi" # Cortex-M23
# target = "thumbv8m.main-none-eabi" # Cortex-M33 (no FPU)
# target = "thumbv8m.main-none-eabihf" # Cortex-M33 (with FPU)

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
**/*.rs.bk
.#*
.gdb_history
Cargo.lock
target/
# editor files
.vscode/*
!.vscode/*.md
!.vscode/*.svd
!.vscode/launch.json
!.vscode/tasks.json
!.vscode/extensions.json

28
Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
[package]
authors = ["asonix <asonix@asonix.dog>"]
edition = "2018"
readme = "README.md"
name = "trinket-streamdeck"
version = "0.1.0"
[dependencies]
cortex-m = "0.6.0"
cortex-m-rt = "0.6.10"
cortex-m-semihosting = "0.3.3"
once_cell = { version = "1.7.2", default-features = false }
panic-halt = "0.2.0"
smart-leds = "0.3"
trinket_m0 = { version = "0.9.0", features = ["unproven", "usb"] }
usb-device = "0.2"
usbd-serial = "0.1"
# this lets you use `cargo fix`!
[[bin]]
name = "trinket-streamdeck"
test = false
bench = false
[profile.release]
codegen-units = 1 # better optimizations
debug = true # symbols are nice and they don't increase the size on Flash
lto = true # better optimizations

31
build.rs Normal file
View file

@ -0,0 +1,31 @@
//! This build script copies the `memory.x` file from the crate root into
//! a directory where the linker can always find it at build time.
//! For many projects this is optional, as the linker always searches the
//! project root directory -- wherever `Cargo.toml` is. However, if you
//! are using a workspace or have a more complicated build setup, this
//! build script becomes required. Additionally, by requesting that
//! Cargo re-run the build script whenever `memory.x` is changed,
//! updating `memory.x` ensures a rebuild of the application with the
//! new memory settings.
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
// Put `memory.x` in our output directory and ensure it's
// on the linker search path.
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
// By default, Cargo will re-run a build script whenever
// any file in the project changes. By specifying `memory.x`
// here, we ensure the build script is only re-run when
// `memory.x` is changed.
println!("cargo:rerun-if-changed=memory.x");
}

36
deploy.sh Executable file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env bash
PROJECT=${1:-trinket-streamdeck}
function bossac() {
$HOME/snap/arduino/current/.arduino15/packages/arduino/tools/bossac/1.8.0-48-gb176eee/bossac "${@:1}"
}
set -e
cargo build --release
arm-none-eabi-objcopy -O binary \
target/thumbv6m-none-eabi/release/$PROJECT \
target/thumbv6m-none-eabi/release/$PROJECT.bin
ports="$(ls /dev/ | grep 'ttyACM')"
for port in $ports; do
if [ "$port" == "" ]; then
echo "Port missing"
exit 1
fi
device="/dev/$port"
printf "connecting to $device"
while ! $(/bin/stty -F $device ospeed 1200 2> /dev/null); do
printf "."
sleep 1
done
echo
bossac -i -d \
--port "$port" -U -e -w -v -o 0x2000 \
target/thumbv6m-none-eabi/release/$PROJECT.bin -R
done

34
memory.x Normal file
View file

@ -0,0 +1,34 @@
MEMORY
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
/* TODO Adjust these memory regions to match your device memory layout */
/* These values correspond to the LM3S6965, one of the few devices QEMU can emulate */
FLASH : ORIGIN = 0x00000000 + 8K, LENGTH = 256K - 8K
RAM : ORIGIN = 0x20000000, LENGTH = 32K
}
/* This is where the call stack will be allocated. */
/* The stack is of the full descending type. */
/* You may want to use this variable to locate the call stack and static
variables in different memory regions. Below is shown the default value */
/* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */
/* You can use this symbol to customize the location of the .text section */
/* If omitted the .text section will be placed right after the .vector_table
section */
/* This is required only on microcontrollers that store some configuration right
after the vector table */
/* _stext = ORIGIN(FLASH) + 0x400; */
/* Example of putting non-initialized variables into custom RAM locations. */
/* This assumes you have defined a region RAM2 above, and in the Rust
sources added the attribute `#[link_section = ".ram2bss"]` to the data
you want to place there. */
/* Note that the section will not be zero-initialized by the runtime! */
/* SECTIONS {
.ram2bss (NOLOAD) : ALIGN(4) {
*(.ram2bss);
. = ALIGN(4);
} > RAM2
} INSERT AFTER .bss;
*/

79
src/handshake.rs Normal file
View file

@ -0,0 +1,79 @@
use crate::hal::usb::UsbBus;
use cortex_m::peripheral::SYST;
use usbd_serial::SerialPort;
pub(crate) struct Handshake {
step_1_complete: bool,
step_2_complete: bool,
buffer: [u8; 16],
timestamp: u32,
}
impl Handshake {
pub(crate) const fn new() -> Self {
Handshake {
step_1_complete: false,
step_2_complete: false,
buffer: [0; 16],
timestamp: 0,
}
}
pub(crate) fn is_complete(&self) -> bool {
self.step_1_complete && self.step_2_complete
}
pub(crate) fn perform(
&mut self,
input: &[u8; 32],
serial: &mut SerialPort<UsbBus>,
) -> Option<bool> {
if self.is_complete() {
self.step_1_complete = false;
self.step_2_complete = false;
}
if !self.step_1_complete {
self.timestamp = SYST::get_current();
self.step_1(input, serial)?;
self.step_1_complete = true;
return Some(true);
} else if !self.step_2_complete {
self.step_2_complete = self.step_2(input);
if !self.step_2_complete {
self.step_1_complete = false;
}
return Some(true);
}
if self.step_1_complete
&& !self.step_2_complete
&& self.timestamp > SYST::get_current() + 100
{
self.step_1_complete = false;
}
Some(false)
}
fn step_1(&mut self, input: &[u8; 32], serial: &mut SerialPort<UsbBus>) -> Option<()> {
for (i, (c1, c2)) in input[..16].iter().zip(&input[16..]).enumerate() {
self.buffer[i] = c1 ^ c2;
}
serial.write(&self.buffer).ok()?;
serial.flush().ok()?;
Some(())
}
fn step_2(&self, input: &[u8; 32]) -> bool {
let mut succeeded = true;
for ((c1, c2), c3) in input[..16].iter().zip(&input[16..]).zip(&self.buffer) {
succeeded = succeeded || *c1 == *c2 && *c1 == *c3;
}
succeeded
}
}

207
src/main.rs Normal file
View file

@ -0,0 +1,207 @@
#![no_std]
#![no_main]
use panic_halt as _; // you can put a breakpoint on `rust_begin_unwind` to catch panics
use trinket_m0 as hal;
use core::cell::RefCell;
use cortex_m::{asm::delay as cycle_delay, interrupt::Mutex, peripheral::NVIC};
use hal::{
clock::GenericClockController,
entry,
pac::{interrupt, CorePeripherals, Peripherals},
prelude::_atsamd_hal_embedded_hal_digital_v2_OutputPin,
timer::SpinTimer,
usb::UsbBus,
Dotstar,
};
use once_cell::unsync::OnceCell;
use smart_leds::{SmartLedsWrite, RGB8};
use usb_device::{bus::UsbBusAllocator, prelude::*};
use usbd_serial::{SerialPort, USB_CLASS_CDC};
mod handshake;
mod unsafe_sync;
use handshake::Handshake;
use unsafe_sync::UnsafeSync;
static NAME: &'static [u8] = b"Streamdeck";
static AUTHOR: &'static [u8] = b"asonix";
static USB_ALLOCATOR: UnsafeSync<OnceCell<UsbBusAllocator<UsbBus>>> =
UnsafeSync::new(OnceCell::new());
static USB_BUS: Mutex<RefCell<Option<UsbDevice<UsbBus>>>> = Mutex::new(RefCell::new(None));
static USB_SERIAL: Mutex<RefCell<Option<SerialPort<UsbBus>>>> = Mutex::new(RefCell::new(None));
static HANDSHAKE: Mutex<RefCell<Handshake>> = Mutex::new(RefCell::new(Handshake::new()));
// section 9.3.3 of datasheet for SAMD21
fn serial_number(id: &mut [u8; 16]) {
let generator: [*const [u8; 4]; 4] = [
0x0080A00C as *const [u8; 4],
0x0080A040 as *const [u8; 4],
0x0080A044 as *const [u8; 4],
0x0080A048 as *const [u8; 4],
];
for i in 0..4 {
id[i * 4 + 0] = unsafe { (*generator[i])[3] };
id[i * 4 + 1] = unsafe { (*generator[i])[2] };
id[i * 4 + 2] = unsafe { (*generator[i])[1] };
id[i * 4 + 3] = unsafe { (*generator[i])[0] };
}
}
#[entry]
fn main() -> ! {
let mut peripherals = Peripherals::take().unwrap();
let mut core = CorePeripherals::take().unwrap();
let mut clocks = GenericClockController::with_internal_32kosc(
peripherals.GCLK,
&mut peripherals.PM,
&mut peripherals.SYSCTRL,
&mut peripherals.NVMCTRL,
);
let mut pins = hal::Pins::new(peripherals.PORT);
let mut red_led = pins.d13.into_open_drain_output(&mut pins.port);
let usb_allocator = hal::usb_allocator(
peripherals.USB,
&mut clocks,
&mut peripherals.PM,
pins.usb_dm,
pins.usb_dp,
&mut pins.port,
);
// SAFETY: No interrupt will access USB_ALLOCATOR before this point, since it is only
// referenced from inside the USB_SERIAL mutex and the USB_BUS mutex, which have not been set
// yet.
//
// SAFETY: This is the only line of the program that modifies USB_ALLOCATOR
let allocator = unsafe {
USB_ALLOCATOR.get().set(usb_allocator).ok().unwrap();
USB_ALLOCATOR.get().get().unwrap()
};
cortex_m::interrupt::free(|cs| {
let serial = USB_SERIAL.borrow(cs);
let mut serial = serial.borrow_mut();
*serial = Some(SerialPort::new(allocator));
});
cortex_m::interrupt::free(|cs| {
let bus = USB_BUS.borrow(cs);
let mut bus = bus.borrow_mut();
*bus = Some(
UsbDeviceBuilder::new(allocator, UsbVidPid(0x16c0, 0x27dd))
.manufacturer("Aode Lion")
.product("Streamdeck")
.serial_number("AAAA")
.device_class(USB_CLASS_CDC)
.build(),
);
});
unsafe {
core.NVIC.set_priority(interrupt::USB, 1);
NVIC::unmask(interrupt::USB);
}
let mut rgb = Dotstar {
ci: pins.dotstar_ci,
di: pins.dotstar_di,
nc: pins.dotstar_nc,
}
.init(SpinTimer::new(12), &mut pins.port);
let off = RGB8 { r: 0, g: 0, b: 0 };
rgb.write([off].iter().cloned()).unwrap();
red_led.set_low().unwrap();
loop {
cycle_delay(15 * 1024 * 1024);
}
}
fn poll_usb() -> Option<()> {
let ready = cortex_m::interrupt::free(|cs1| {
cortex_m::interrupt::free(|cs2| {
let mut usb_dev = USB_BUS.borrow(cs1).borrow_mut();
let usb_dev = usb_dev.as_mut()?;
let mut serial = USB_SERIAL.borrow(cs2).borrow_mut();
let serial = serial.as_mut()?;
Some(usb_dev.poll(&mut [&mut *serial]))
})
})?;
if !ready {
return Some(());
}
let mut buf = [0u8; 32];
let count = cortex_m::interrupt::free(|cs| {
USB_SERIAL
.borrow(cs)
.borrow_mut()
.as_mut()?
.read(&mut buf)
.ok()
})?;
if count == 32 {
let res = cortex_m::interrupt::free(|cs| {
cortex_m::interrupt::free(|cs2| {
let mut serial = USB_SERIAL.borrow(cs2).borrow_mut();
let serial = serial.as_mut()?;
HANDSHAKE.borrow(cs).borrow_mut().perform(&buf, serial)
})
});
if res? {
return Some(());
}
}
let is_complete = cortex_m::interrupt::free(|cs| HANDSHAKE.borrow(cs).borrow().is_complete());
if !is_complete {
return None;
}
if count == 5 && buf.starts_with(b"ident") {
let mut sn = [0u8; 16];
serial_number(&mut sn);
write_length_delim(&sn)?;
} else if count == 4 && buf.starts_with(b"name") {
write_length_delim(NAME)?;
} else if count == 6 && buf.starts_with(b"author") {
write_length_delim(AUTHOR)?;
} else {
write_length_delim(&buf[..count])?;
}
cortex_m::interrupt::free(|cs| {
let mut serial = USB_SERIAL.borrow(cs).borrow_mut();
let serial = serial.as_mut()?;
serial.flush().ok()?;
Some(())
})?;
Some(())
}
fn write_length_delim(bytes: &[u8]) -> Option<()> {
cortex_m::interrupt::free(|cs| {
let mut serial = USB_SERIAL.borrow(cs).borrow_mut();
let serial = serial.as_mut()?;
serial.write(&[bytes.len() as u8]).ok()?;
serial.write(bytes).ok()?;
Some(())
})
}
#[interrupt]
fn USB() {
poll_usb();
}

13
src/unsafe_sync.rs Normal file
View file

@ -0,0 +1,13 @@
pub(crate) struct UnsafeSync<T>(T);
impl<T> UnsafeSync<T> {
pub(crate) const fn new(item: T) -> Self {
UnsafeSync(item)
}
pub(crate) unsafe fn get(&self) -> &T {
&self.0
}
}
unsafe impl<T> Sync for UnsafeSync<T> where T: Send {}