diff options
-rw-r--r-- | .cargo/config | 33 | ||||
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | Cargo.toml | 36 | ||||
-rw-r--r-- | memory.x | 5 | ||||
-rw-r--r-- | openocd.cfg | 2 | ||||
-rw-r--r-- | openocd.gdb | 40 | ||||
-rw-r--r-- | src/app.rs | 362 | ||||
-rw-r--r-- | src/build.rs | 18 |
8 files changed, 501 insertions, 0 deletions
diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..fa909f5 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,33 @@ +[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 = [ + # 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) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59a4524 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +**/*.rs.bk +.#* +.gdb_history +Cargo.lock +target/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..17b3be0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,36 @@ +[package] +authors = ["Determinant <[email protected]>"] +edition = "2018" +readme = "README.md" +name = "cessna-knobox" +version = "0.1.0" + +[dependencies] +cortex-m = "0.6" +cortex-m-rt = "0.6" +cortex-m-semihosting = "0.3" +cortex-m-rtic = "0.5" +panic-halt = "0.2.0" +panic-semihosting = "0.5.3" +nb = "0.1.2" +embedded-hal = "0.2.3" +heapless = "0.5.1" +rand_core = "0.4" +usb-device = "0.2.4" + +[dependencies.stm32f1] +version = "0.8.0" +features = ["stm32f103", "rt"] + +[dependencies.stm32f1xx-hal] +version = "0.6.1" +features = ["stm32f103", "rt", "stm32-usbd"] + +[[bin]] +name ="knobox" +path = "src/app.rs" + +[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 diff --git a/memory.x b/memory.x new file mode 100644 index 0000000..e20ff2b --- /dev/null +++ b/memory.x @@ -0,0 +1,5 @@ +MEMORY +{ + FLASH : ORIGIN = 0x08000000, LENGTH = 128K + RAM : ORIGIN = 0x20000000, LENGTH = 20K +} diff --git a/openocd.cfg b/openocd.cfg new file mode 100644 index 0000000..b00e2bf --- /dev/null +++ b/openocd.cfg @@ -0,0 +1,2 @@ +source [find interface/stlink-v2.cfg] +source [find target/stm32f1x.cfg] diff --git a/openocd.gdb b/openocd.gdb new file mode 100644 index 0000000..7795319 --- /dev/null +++ b/openocd.gdb @@ -0,0 +1,40 @@ +target extended-remote :3333 + +# print demangled symbols +set print asm-demangle on + +# set backtrace limit to not have infinite backtrace loops +set backtrace limit 32 + +# detect unhandled exceptions, hard faults and panics +break DefaultHandler +break HardFault +break rust_begin_unwind +# # run the next few lines so the panic message is printed immediately +# # the number needs to be adjusted for your panic handler +# commands $bpnum +# next 4 +# end + +# *try* to stop at the user entry point (it might be gone due to inlining) +break main + +monitor arm semihosting enable + +# # send captured ITM to the file itm.fifo +# # (the microcontroller SWO pin must be connected to the programmer SWO pin) +# # 8000000 must match the core clock frequency +# monitor tpiu config internal itm.txt uart off 8000000 + +# # OR: make the microcontroller SWO pin output compatible with UART (8N1) +# # 8000000 must match the core clock frequency +# # 2000000 is the frequency of the SWO pin +# monitor tpiu config external uart off 8000000 2000000 + +# # enable ITM port 0 +# monitor itm port 0 on + +load + +# start the process but immediately halt the processor +stepi diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..1a4a424 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,362 @@ +#![no_std] +#![no_main] + +extern crate panic_semihosting; + +use cortex_m::{asm::delay, peripheral::DWT}; +use embedded_hal::digital::v2::{InputPin, OutputPin}; +use rtic::cyccnt::{Instant, U32Ext as _}; +use stm32f1xx_hal::usb::{Peripheral, UsbBus, UsbBusType}; +use stm32f1xx_hal::{adc, gpio, prelude::*, stm32::ADC1}; +use usb_device::bus; +use usb_device::prelude::*; + +pub struct PanelState { + throttle0: u16, + throttle1: u16, + sw: u8, +} + +#[allow(unused)] +pub mod hid { + + use usb_device::class_prelude::*; + use usb_device::Result; + + const USB_CLASS_HID: u8 = 0x03; + + const USB_SUBCLASS_NONE: u8 = 0x00; + const USB_SUBCLASS_BOOT: u8 = 0x01; + + const USB_INTERFACE_NONE: u8 = 0x00; + const USB_INTERFACE_KEYBOARD: u8 = 0x01; + const USB_INTERFACE_MOUSE: u8 = 0x02; + + const REQ_GET_REPORT: u8 = 0x01; + const REQ_GET_IDLE: u8 = 0x02; + const REQ_GET_PROTOCOL: u8 = 0x03; + const REQ_SET_REPORT: u8 = 0x09; + const REQ_SET_IDLE: u8 = 0x0a; + const REQ_SET_PROTOCOL: u8 = 0x0b; + + const REPORT_DESCR: &[u8] = &[ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x04, // USAGE (Joystick) + 0xa1, 0x01, // COLLECTION (Application) + 0x05, 0x02, // USAGE_PAGE (Simulation) + 0x09, 0xbb, // USAGE (Throttle) + 0x16, 0x00, 0x80, /* Logical Minimum (-32768) */ + 0x26, 0xff, 0x7f, /* Logical Maximum (32767) */ + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x10, // REPORT_SIZE (16) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x32, // USAGE (Z) + 0x16, 0x00, 0x80, /* Logical Minimum (-32768) */ + 0x26, 0xff, 0x7f, /* Logical Maximum (32767) */ + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x10, // REPORT_SIZE (16) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM(Button 1) + 0x29, 0x05, // USAGE_MAXIMUM(Button 5) + 0x95, 0x05, // REPORT_COUNT (5) + 0x75, 0x01, // REPORT_SIZE (1) + 0x15, 0x00, /* Logical Minimum (0) */ + 0x25, 0x01, /* Logical Maximum (1) */ + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x95, 0x03, // REPORT_COUNT (3) + 0x75, 0x01, // REPORT_SIZE (1) + 0x81, 0x01, // INPUT (Cnst,Arr,Abs) + 0xc0, // END_COLLECTION + ]; + + pub fn report(s: super::PanelState) -> [u8; 5] { + [ + s.throttle1 as u8, // throttle 0 + (s.throttle1 >> 8) as u8, + s.throttle0 as u8, // throttle 1 + (s.throttle0 >> 8) as u8, + s.sw, + ] + } + + pub struct HIDClass<'a, B: UsbBus> { + report_if: InterfaceNumber, + report_ep: EndpointIn<'a, B>, + } + + impl<B: UsbBus> HIDClass<'_, B> { + /// Creates a new HIDClass with the provided UsbBus and max_packet_size in bytes. For + /// full-speed devices, max_packet_size has to be one of 8, 16, 32 or 64. + pub fn new(alloc: &UsbBusAllocator<B>) -> HIDClass<'_, B> { + HIDClass { + report_if: alloc.interface(), + report_ep: alloc.interrupt(8, 10), + } + } + + pub fn write(&mut self, data: &[u8]) { + self.report_ep.write(data).ok(); + } + } + + impl<B: UsbBus> UsbClass<B> for HIDClass<'_, B> { + fn get_configuration_descriptors(&self, writer: &mut DescriptorWriter) -> Result<()> { + writer.interface( + self.report_if, + USB_CLASS_HID, + USB_SUBCLASS_NONE, + USB_INTERFACE_MOUSE, + )?; + + let descr_len: u16 = REPORT_DESCR.len() as u16; + writer.write( + 0x21, + &[ + 0x01, // bcdHID + 0x01, // bcdHID + 0x00, // bContryCode + 0x01, // bNumDescriptors + 0x22, // bDescriptorType + descr_len as u8, // wDescriptorLength + (descr_len >> 8) as u8, // wDescriptorLength + ], + )?; + + writer.endpoint(&self.report_ep)?; + + Ok(()) + } + + fn control_in(&mut self, xfer: ControlIn<B>) { + let req = xfer.request(); + + if req.request_type == control::RequestType::Standard { + match (req.recipient, req.request) { + (control::Recipient::Interface, control::Request::GET_DESCRIPTOR) => { + let (dtype, _index) = req.descriptor_type_index(); + if dtype == 0x21 { + // HID descriptor + cortex_m::asm::bkpt(); + let descr_len: u16 = REPORT_DESCR.len() as u16; + + // HID descriptor + let descr = &[ + 0x09, // length + 0x21, // descriptor type + 0x01, // bcdHID + 0x01, // bcdHID + 0x00, // bCountryCode + 0x01, // bNumDescriptors + 0x22, // bDescriptorType + descr_len as u8, // wDescriptorLength + (descr_len >> 8) as u8, // wDescriptorLength + ]; + + xfer.accept_with(descr).ok(); + return; + } else if dtype == 0x22 { + // Report descriptor + xfer.accept_with(REPORT_DESCR).ok(); + return; + } + } + _ => { + return; + } + }; + } + + if !(req.request_type == control::RequestType::Class + && req.recipient == control::Recipient::Interface + && req.index == u8::from(self.report_if) as u16) + { + return; + } + + match req.request { + REQ_GET_REPORT => { + // USB host requests for report + // I'm not sure what should we do here, so just send empty report + xfer.accept_with(&report(super::PanelState { + throttle0: 0, + throttle1: 0, + sw: 0, + })) + .ok(); + } + _ => { + xfer.reject().ok(); + } + } + } + + fn control_out(&mut self, xfer: ControlOut<B>) { + let req = xfer.request(); + + if !(req.request_type == control::RequestType::Class + && req.recipient == control::Recipient::Interface + && req.index == u8::from(self.report_if) as u16) + { + return; + } + + xfer.reject().ok(); + } + } +} + +use hid::HIDClass; + +const PERIOD: u32 = 80_000; + +#[rtic::app(device = stm32f1xx_hal::stm32, peripherals = true, monotonic = rtic::cyccnt::CYCCNT)] +const APP: () = { + struct Resources { + usb_dev: UsbDevice<'static, UsbBusType>, + hid: HIDClass<'static, UsbBusType>, + adc1: adc::Adc<ADC1>, + th0: gpio::gpioa::PA1<gpio::Analog>, + th1: gpio::gpioa::PA2<gpio::Analog>, + sw0: gpio::gpioa::PA8<gpio::Input<gpio::PullUp>>, + sw1: gpio::gpioa::PA9<gpio::Input<gpio::PullUp>>, + sw2: gpio::gpioa::PA10<gpio::Input<gpio::PullUp>>, + sw3: gpio::gpiob::PB14<gpio::Input<gpio::PullUp>>, + sw4: gpio::gpiob::PB15<gpio::Input<gpio::PullUp>>, + } + + #[init(schedule = [on_tick])] + fn init(mut cx: init::Context) -> init::LateResources { + static mut USB_BUS: Option<bus::UsbBusAllocator<UsbBusType>> = None; + + cx.core.DCB.enable_trace(); + DWT::unlock(); + cx.core.DWT.enable_cycle_counter(); + + let mut flash = cx.device.FLASH.constrain(); + let mut rcc = cx.device.RCC.constrain(); + + let clocks = rcc + .cfgr + .use_hse(8.mhz()) + .sysclk(48.mhz()) + .pclk1(24.mhz()) + .adcclk(2.mhz()) + .freeze(&mut flash.acr); + + assert!(clocks.usbclk_valid()); + + let mut gpioa = cx.device.GPIOA.split(&mut rcc.apb2); + let mut gpiob = cx.device.GPIOB.split(&mut rcc.apb2); + + // BluePill board has a pull-up resistor on the D+ line. + // Pull the D+ pin down to send a RESET condition to the USB bus. + let mut usb_dp = gpioa.pa12.into_push_pull_output(&mut gpioa.crh); + usb_dp.set_low().ok(); + delay(clocks.sysclk().0 / 100); + + let usb_dm = gpioa.pa11; + let usb_dp = usb_dp.into_floating_input(&mut gpioa.crh); + + let usb = Peripheral { + usb: cx.device.USB, + pin_dm: usb_dm, + pin_dp: usb_dp, + }; + + *USB_BUS = Some(UsbBus::new(usb)); + + let hid = HIDClass::new(USB_BUS.as_ref().unwrap()); + let adc1 = adc::Adc::adc1(cx.device.ADC1, &mut rcc.apb2, clocks); + let th0 = gpioa.pa1.into_analog(&mut gpioa.crl); + let th1 = gpioa.pa2.into_analog(&mut gpioa.crl); + let sw0 = gpioa.pa8.into_pull_up_input(&mut gpioa.crh); + let sw1 = gpioa.pa9.into_pull_up_input(&mut gpioa.crh); + let sw2 = gpioa.pa10.into_pull_up_input(&mut gpioa.crh); + let sw3 = gpiob.pb14.into_pull_up_input(&mut gpiob.crh); + let sw4 = gpiob.pb15.into_pull_up_input(&mut gpiob.crh); + + let usb_dev = UsbDeviceBuilder::new(USB_BUS.as_ref().unwrap(), UsbVidPid(0x0483, 0x002a)) + .manufacturer("Ted's") + .product("Cessna Knobox") + .serial_number("001") + .device_class(0) + .build(); + + cx.schedule.on_tick(cx.start + PERIOD.cycles()).ok(); + + init::LateResources { + usb_dev, + hid, + adc1, + th0, + th1, + sw0, + sw1, + sw2, + sw3, + sw4, + } + } + + #[task(schedule = [on_tick], resources = [hid, adc1, th0, th1, sw0, sw1, sw2, sw3, sw4])] + fn on_tick(mut cx: on_tick::Context) { + cx.schedule.on_tick(Instant::now() + PERIOD.cycles()).ok(); + + let hid = &mut cx.resources.hid; + let adc1 = &mut cx.resources.adc1; + let raw0: u32 = adc1.read(cx.resources.th0).unwrap(); + let raw1: u32 = adc1.read(cx.resources.th1).unwrap(); + let throttle0 = (raw0 * 32767 / 4095) as u16; + let throttle1 = (raw1 * 32767 / 4095) as u16; + hid.write(&hid::report(PanelState { + throttle0, + throttle1, + sw: if cx.resources.sw0.is_low().unwrap() { + 1 + } else { + 0 + } | (if cx.resources.sw1.is_low().unwrap() { + 1 + } else { + 0 + }) << 1 + | (if cx.resources.sw2.is_low().unwrap() { + 1 + } else { + 0 + }) << 2 + | (if cx.resources.sw3.is_low().unwrap() { + 1 + } else { + 0 + }) << 3 + | (if cx.resources.sw4.is_low().unwrap() { + 1 + } else { + 0 + }) << 4, + })); + } + + #[task(binds=USB_HP_CAN_TX, resources = [usb_dev, hid])] + fn usb_tx(mut cx: usb_tx::Context) { + usb_poll(&mut cx.resources.usb_dev, &mut cx.resources.hid); + } + + #[task(binds=USB_LP_CAN_RX0, resources = [usb_dev, hid])] + fn usb_rx(mut cx: usb_rx::Context) { + usb_poll(&mut cx.resources.usb_dev, &mut cx.resources.hid); + } + + extern "C" { + fn EXTI0(); + } +}; + +fn usb_poll<B: bus::UsbBus>(usb_dev: &mut UsbDevice<'static, B>, hid: &mut HIDClass<'static, B>) { + if !usb_dev.poll(&mut [hid]) { + return; + } +} diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..98f603e --- /dev/null +++ b/src/build.rs @@ -0,0 +1,18 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put the linker script somewhere the linker can find it + 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()); + + // Only re-run the build script when memory.x is changed, + // instead of when any part of the source code changes. + println!("cargo:rerun-if-changed=memory.x"); +} |