Compare commits

...

10 commits

Author SHA1 Message Date
Sebastian d878b71eec Cleaned up cargo.toml 2023-05-28 16:14:18 +02:00
Sebastian af62ec315d Updated to latest embassy version 2023-05-28 16:02:12 +02:00
Sebastian 06f4ba549b Added slop to AZ and EL movement 2022-09-05 19:22:37 +02:00
Sebastian 29e3d21996 Added activity indicator 2022-09-03 22:58:23 +02:00
Sebastian 045eada9d3 Added comments to movement.rs 2022-08-28 22:03:17 +02:00
Sebastian ce049b81d2 Added timeouts to sends in movement.rs 2022-08-28 00:39:08 +02:00
Sebastian 5f3f5e63b2 Stop rotor when USB connections break 2022-08-28 00:13:22 +02:00
Sebastian e7c9f5c5cc Cleanup and comments 2022-08-28 00:03:06 +02:00
Sebastian 5935541ab2 Added CI 2022-08-27 22:39:32 +02:00
Sebastian e0dd01ad17 Added AZ and EL position readouts 2022-08-24 20:08:51 +02:00
7 changed files with 507 additions and 672 deletions

8
.woodpecker.yml Normal file
View file

@ -0,0 +1,8 @@
pipeline:
build:
image: rust
commands:
- rustup override set nightly
- rustup target add thumbv7m-none-eabi
- cargo install flip-link
- cargo build --release

855
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,12 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
embassy-util = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
embassy-executor = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt", "integrated-timers"] }
embassy-time = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt", "defmt-timestamp-uptime", "tick-32768hz"] }
embassy-stm32 = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["nightly", "defmt", "stm32f103c8", "unstable-pac", "memory-x", "time-driver-any"] }
embassy-sync = { version = "0.2.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
embassy-futures = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
embassy-executor = { version = "0.2.0", git = "https://github.com/embassy-rs/embassy.git", features = ["arch-cortex-m", "executor-thread", "defmt", "integrated-timers"] }
embassy-time = { version = "0.1.1", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt", "defmt-timestamp-uptime"] }
embassy-stm32 = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["nightly", "defmt", "stm32f103c8", "unstable-pac", "memory-x", "time-driver-any", "unstable-traits"] }
embassy-usb = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
embassy-usb-serial = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
defmt = "0.3.2"
defmt-rtt = "0.3.2"
@ -19,10 +19,8 @@ panic-probe = { version = "0.3.0"}
cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7.0"
embedded-hal = "0.2.6"
futures = { version = "0.3.17", default-features = false, features = ["async-await"] }
heapless = { version = "0.7.5", default-features = false, features = ['ufmt-impl']}
nb = "1.0.0"
ufmt = "0.2.0"
ssd1306 = "0.7.1"

View file

@ -1,10 +1,14 @@
use embassy_stm32::i2c;
use embassy_stm32::peripherals;
use embassy_stm32::bind_interrupts;
use embassy_stm32::dma::NoDma;
use embassy_stm32::time::Hertz;
use embassy_time::{Duration, Timer};
use embassy_util::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_util::channel::mpmc::Receiver;
use embassy_util::{select, Either};
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::channel::Receiver;
use embassy_futures::select::{select, Either};
use embedded_graphics::{
mono_font::{
@ -24,6 +28,10 @@ use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
use crate::{AzElPair, RotorState};
bind_interrupts!(struct Irqs {
I2C1_EV => i2c::InterruptHandler<peripherals::I2C1>;
});
#[embassy_executor::task]
pub async fn display_task(
i2c1: peripherals::I2C1,
@ -31,7 +39,7 @@ pub async fn display_task(
scl: peripherals::PB7,
cmd_receiver: Receiver<'static, ThreadModeRawMutex, RotorState, 1>,
) {
let i2c = i2c::I2c::new(i2c1, sda, scl, Hertz::hz(100_000), i2c::Config::default());
let i2c = i2c::I2c::new(i2c1, sda, scl, Irqs, NoDma, NoDma, Hertz(100_000), Default::default());
let interface = I2CDisplayInterface::new(i2c);
@ -50,9 +58,11 @@ pub async fn display_task(
let mut rotor_state = RotorState {
actual_pos: AzElPair { az: 0, el: 0 },
setpoint_pos: AzElPair { az: 0, el: 0 },
stopped: false,
stopped: true,
};
let mut activity_indicator = 0;
loop {
display.clear();
@ -72,23 +82,38 @@ pub async fn display_task(
.draw(&mut display)
.unwrap();
Rectangle::new(Point::new(0, 19), Size::new(128, 23))
.into_styled(style_filled)
.draw(&mut display)
.unwrap();
if !rotor_state.stopped {
Rectangle::new(Point::new(0, 19), Size::new(128, 23))
.into_styled(style_filled)
.draw(&mut display)
.unwrap();
};
let setpoint_style = if !rotor_state.stopped {
text_large_inv
} else {
text_large
};
tmp.clear();
uwrite!(tmp, "AZ: {}", rotor_state.setpoint_pos.az).unwrap();
Text::new(&tmp, Point::new(1, 30), text_large_inv)
Text::new(&tmp, Point::new(1, 30), setpoint_style)
.draw(&mut display)
.unwrap();
tmp.clear();
uwrite!(tmp, "EL: {}", rotor_state.setpoint_pos.el).unwrap();
Text::new(&tmp, Point::new(64, 30), text_large_inv)
Text::new(&tmp, Point::new(64, 30), setpoint_style)
.draw(&mut display)
.unwrap();
if rotor_state.stopped || activity_indicator < 19 {
display.set_pixel(127, activity_indicator, true);
} else {
display.set_pixel(127, activity_indicator, false);
}
activity_indicator = (activity_indicator + 1) % 32;
display.flush().unwrap();
let result = select(

View file

@ -2,9 +2,7 @@
#![no_main]
#![feature(type_alias_impl_trait)]
use core::fmt::Write;
use defmt::{panic, Format};
use defmt::Format;
use defmt_rtt as _;
use panic_probe as _;
@ -13,9 +11,8 @@ use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_stm32::time::Hertz;
use embassy_stm32::Config;
use embassy_time::{Duration, Timer};
use embassy_util::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_util::channel::mpmc::Channel;
use embassy_util::Forever;
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::channel::Channel;
mod display;
use display::display_task;
@ -74,10 +71,13 @@ async fn main(spawner: Spawner) {
spawner
.spawn(movement_task(
p.ADC1,
p.PA0,
p.PA1,
p.PA2,
p.PA3,
p.PA4,
p.PA5,
p.PA6,
CMD_CHAN.receiver(),
POS_CHAN.sender(),
STATE_CHAN.sender(),

View file

@ -1,91 +1,189 @@
use embassy_stm32::adc::Adc;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_stm32::peripherals;
use embassy_time::{Duration, Instant, Timer};
use embassy_util::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_util::channel::mpmc::{Receiver, Sender};
use embassy_util::{select, Either};
use futures::future::join;
use embassy_time::{with_timeout, Delay, Duration, Timer};
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::channel::{Receiver, Sender};
use embassy_futures::select::{select, Either};
use embassy_futures::join::join;
use heapless::Vec;
use crate::usb::Gs232Cmd;
use crate::{AzElPair, RotorState};
// ADC reading for azimuth 0°
const AZ_MIN_READING: f32 = 0.0;
// ADC reading for azimuth 360°
const AZ_MAX_READING: f32 = 4096.0;
// Range of motion for azimuth (0 to AZ_RANGE)
const AZ_RANGE: f32 = 360.0;
// Tolerance for the azimuth setpoint
const AZ_SLOP : i16 = 1;
// ADC reading for elevation 0°
const EL_MIN_READING: f32 = 0.0;
// ADC reading for elevation 360°
const EL_MAX_READING: f32 = 4096.0;
// Range of motion for elevantion (0 to EL_RANGE)
const EL_RANGE: f32 = 180.0;
// Tolerance for the elevation setpoint
const EL_SLOP : i16 = 1;
#[embassy_executor::task]
pub async fn movement_task(
cw_pin: peripherals::PA3,
ccw_pin: peripherals::PA4,
up_pin: peripherals::PA5,
down_pin: peripherals::PA6,
adc1: peripherals::ADC1,
mut az_pin: peripherals::PA0,
mut el_pin: peripherals::PA1,
cw_pin: peripherals::PA2,
ccw_pin: peripherals::PA3,
up_pin: peripherals::PA4,
down_pin: peripherals::PA5,
cmd_receiver: Receiver<'static, ThreadModeRawMutex, Gs232Cmd, 1>,
pos_sender: Sender<'static, ThreadModeRawMutex, AzElPair, 1>,
state_sender: Sender<'static, ThreadModeRawMutex, RotorState, 1>,
) {
// Initialize the rotor state
let mut rotor_state = RotorState {
actual_pos: AzElPair { az: 0, el: 0 },
setpoint_pos: AzElPair { az: 0, el: 0 },
stopped: false,
stopped: true,
};
// Setup output pins for moving the rotor
let mut cw_pin = Output::new(cw_pin, Level::Low, Speed::Low);
let mut ccw_pin = Output::new(ccw_pin, Level::Low, Speed::Low);
let mut up_pin = Output::new(up_pin, Level::Low, Speed::Low);
let mut down_pin = Output::new(down_pin, Level::Low, Speed::Low);
// Setup the ADC for reading the rotor positions
let mut adc = Adc::new(adc1, &mut Delay);
// Do an initial ADC reading to initialize the averages
let az_reading = adc.read(&mut az_pin) as f32;
let el_reading = adc.read(&mut el_pin) as f32;
let mut az_average = Average::new(az_reading);
let mut el_average = Average::new(el_reading);
loop {
// Wait until either a new command has been received or 100ms have elapsed
match select(
cmd_receiver.recv(),
Timer::after(Duration::from_millis(100)),
)
.await
{
// A new command has been received. This task only cares about MoveTo and Stop.
Either::First(cmd) => match cmd {
// Move to command. Update the setpoint pair in the rotor state
Gs232Cmd::MoveTo(pair) => {
rotor_state.setpoint_pos = pair;
rotor_state.stopped = false;
}
// Stop command. Set the stopped flag.
Gs232Cmd::Stop => {
rotor_state.stopped = true;
}
// Everthing elese is an noop.
_ => {}
},
// Second case of the select statement. Timer has elapsed.
Either::Second(_) => {
if !rotor_state.stopped && rotor_state.actual_pos.az < rotor_state.setpoint_pos.az {
rotor_state.actual_pos.az += 1;
// First read the current rotor position
let az_reading = adc.read(&mut az_pin) as f32;
let el_reading = adc.read(&mut el_pin) as f32;
// Apply the averaging filters
az_average.add(az_reading);
el_average.add(el_reading);
// Calculate the position in degreee
let az_actual = (az_average.average() - AZ_MIN_READING)
/ (AZ_MAX_READING - AZ_MIN_READING)
* AZ_RANGE;
let el_actual = (el_average.average() - EL_MIN_READING)
/ (EL_MAX_READING - EL_MIN_READING)
* EL_RANGE;
// Update the rotor state
rotor_state.actual_pos.az = az_actual as u16;
rotor_state.actual_pos.el = el_actual as u16;
let delta_az =
rotor_state.setpoint_pos.az as i16 - rotor_state.actual_pos.az as i16;
let delta_el =
rotor_state.setpoint_pos.el as i16 - rotor_state.actual_pos.el as i16;
if !rotor_state.stopped && delta_az > AZ_SLOP {
// Azimuth needs to move clockwise
cw_pin.set_high();
ccw_pin.set_low();
} else if !rotor_state.stopped
&& rotor_state.actual_pos.az > rotor_state.setpoint_pos.az
{
rotor_state.actual_pos.az -= 1;
} else if !rotor_state.stopped && delta_az < -AZ_SLOP {
// Azimuth needs to move counter clockwise
cw_pin.set_low();
ccw_pin.set_high();
} else {
// Either azimuth is on the setpoint or the rotor has beend stopped.
cw_pin.set_low();
ccw_pin.set_low();
}
if !rotor_state.stopped && rotor_state.actual_pos.el < rotor_state.setpoint_pos.el {
rotor_state.actual_pos.el += 1;
if !rotor_state.stopped && delta_el > EL_SLOP {
// Elevation needs to move up
up_pin.set_high();
down_pin.set_low();
} else if !rotor_state.stopped
&& rotor_state.actual_pos.el > rotor_state.setpoint_pos.el
{
rotor_state.actual_pos.el -= 1;
} else if !rotor_state.stopped && delta_el < -EL_SLOP {
// Elevation needs to move down
up_pin.set_low();
down_pin.set_high();
} else {
// Either elevation is on the setpoint or the rotor has beend stopped.
up_pin.set_low();
down_pin.set_low();
}
join(
pos_sender.send(rotor_state.actual_pos),
state_sender.send(rotor_state),
// Send the state to the display task and the position usb.
// Use timeouts to prevent blocking if display or usb task are unresponsive.
let _ = join(
with_timeout(
Duration::from_millis(100),
pos_sender.send(rotor_state.actual_pos),
),
with_timeout(Duration::from_millis(100), state_sender.send(rotor_state)),
)
.await;
//state_sender.send(rotor_state).await;
}
};
}
}
// Simple sliding average filter
struct Average {
pos: usize,
data: Vec<f32, 5>,
}
impl Average {
// Create a new filter and prefill the state using an initial value
fn new(initial: f32) -> Average {
let mut data: Vec<f32, 5> = Vec::new();
data.resize(5, initial).unwrap();
Average { pos: 0, data }
}
// Adds a new value to internal state
fn add(&mut self, sample: f32) {
self.data[self.pos] = sample;
self.pos = (self.pos + 1) % self.data.len();
}
// Calculate the average value from the internal state
fn average(&self) -> f32 {
let mut sum = 0.0;
for sample in &self.data {
sum += sample;
}
sum / self.data.len() as f32
}
}

View file

@ -1,21 +1,25 @@
use defmt::Format;
use embassy_stm32::interrupt;
use embassy_stm32::peripherals;
use embassy_stm32::{bind_interrupts, usb};
use embassy_stm32::usb::Driver;
use embassy_usb::Builder;
use embassy_usb_serial::{CdcAcmClass, State};
use embassy_util::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_util::channel::mpmc::{Receiver, Sender};
use embassy_util::{select, Either};
use futures::future::join;
use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::channel::{Receiver, Sender};
use embassy_futures::select::{select, Either};
use embassy_futures::join::join;
use heapless::String;
use ufmt::uwrite;
use crate::AzElPair;
bind_interrupts!(struct Irqs {
USB_LP_CAN1_RX0 => usb::InterruptHandler<peripherals::USB>;
});
#[embassy_executor::task]
pub async fn usb_task(
usb: peripherals::USB,
@ -24,12 +28,10 @@ pub async fn usb_task(
cmd_sender: Sender<'static, ThreadModeRawMutex, Gs232Cmd, 1>,
pos_receiver: Receiver<'static, ThreadModeRawMutex, AzElPair, 1>,
) {
let irq = interrupt::take!(USB_LP_CAN1_RX0);
let driver = Driver::new(usb, irq, dp_pin, dm_pin);
let driver = Driver::new(usb, Irqs, dp_pin, dm_pin);
// Create embassy-usb Config
let config = embassy_usb::Config::new(0xc0de, 0xcafe);
//config.max_packet_size_0 = 64;
// Create embassy-usb DeviceBuilder using the driver and config.
// It needs some buffers for building the descriptors.
@ -38,7 +40,7 @@ pub async fn usb_task(
let mut bos_descriptor = [0; 256];
let mut control_buf = [0; 7];
let mut state = State::new();
let mut usb_state = State::new();
let mut builder = Builder::new(
driver,
@ -46,41 +48,51 @@ pub async fn usb_task(
&mut device_descriptor,
&mut config_descriptor,
&mut bos_descriptor,
&mut control_buf,
None,
&mut control_buf
);
// Create classes on the builder.
let mut class = CdcAcmClass::new(&mut builder, &mut state, 64);
let mut class = CdcAcmClass::new(&mut builder, &mut usb_state, 64);
// Build the builder.
let mut usb = builder.build();
// Do stuff with the class!
// Create a future to handle incomming usb packets
let usb_handler_fut = async {
// Initialize the current position in case we get a B or C command,
// before we get the first the update via pos_receiver
let mut current_pos = AzElPair { az: 0, el: 0 };
loop {
// No much used doing anything until we have a usb connection
class.wait_connection().await;
defmt::info!("USB connected");
// Allocate a space for incomming usb data packets
let mut packet = [0; 64];
// Allocate a string to act as buffer to pares the packets linewise
let mut buffer: String<64> = String::new();
loop {
let n = match select(class.read_packet(&mut packet), pos_receiver.recv()).await {
// The read_packet furture returned either usb data or an error.
Either::First(res) => match res {
// In case of an error break the loop and treat it like an usb disconnect
Ok(n) => n,
// In case of an error break the loop and treat it like an usb disconnect
Err(err) => {
defmt::error!("Unable to read packet: {}", err);
break;
}
},
// The pos_receiver future returned a position update from moment task.
// Just update position and restart loop.
Either::Second(pair) => {
current_pos = pair;
continue;
}
};
// Append the data in the packet buffer to the buffer string
for byte in &packet[..n] {
if buffer.len() == 64 {
buffer.clear();
@ -88,91 +100,115 @@ pub async fn usb_task(
buffer.push(*byte as char).unwrap();
}
// Check if the buffer string contains a '\r'
let line_end = match buffer.rfind('\r') {
// Carriage return found, keep the index
Some(n) => n,
// No carriage return, wait for the next package
_ => continue,
};
defmt::info!("Line buffer: {:x}", buffer.as_bytes());
// The is a non-zero amount of characters before the carriage return
if line_end > 0 {
// Try the parse the slice leading up to linend as a GS323 command
let cmd = parse_command(&buffer.as_str()[..line_end]);
defmt::info!("Command: {}", cmd);
// Reverse some space for a respose to the command
let mut resp: String<16> = String::new();
match cmd {
Gs232Cmd::GetAl => {
// Get Azimuth command. Respond with last known azimuth
Gs232Cmd::GetAz => {
uwrite!(&mut resp, "AZ={}\r", current_pos.az).unwrap();
}
Gs232Cmd::GetEz => {
// Get Elevation comman. Respond with last known elevation
Gs232Cmd::GetEl => {
uwrite!(&mut resp, "EL={}\r", current_pos.el).unwrap();
}
Gs232Cmd::GetAlEz => {
// Get Azimuth and Elevation. Respond with last known pair
Gs232Cmd::GetAzEl => {
uwrite!(&mut resp, "AZ={} EL={}\r", current_pos.az, current_pos.el)
.unwrap();
}
// Move to command. Send to movement task. Respond with empty line.
Gs232Cmd::MoveTo(_) => {
cmd_sender.send(cmd).await;
resp.push_str("\r").unwrap();
}
// Stop command. Send to movement task. Respond with empty line.
Gs232Cmd::Stop => {
cmd_sender.send(cmd).await;
resp.push_str("\r").unwrap();
}
// Unknown command or parser error. Complain and do nothing.
_ => {
defmt::error!("Uknown command: {}", &buffer.as_str()[..line_end]);
resp.push_str("Unkown command!\r").unwrap();
}
}
// Write the response back via USB
match class.write_packet(resp.as_bytes()).await {
Ok(_) => {}
// Error treat like broken usb connection
Err(err) => {
defmt::error!("Unable to write packet: {}", err);
break;
}
};
}
// Drop the processed line from the buffer
buffer = String::from(&buffer.as_str()[line_end + 1..]);
}
defmt::info!("USB disconnected");
// USB connection is broken, so better stop the rotor.
cmd_sender.send(Gs232Cmd::Stop).await;
}
};
// Run the ubs and handler future both to completion.
// None of the ever completes, but they will still be polled continously.
join(usb.run(), usb_handler_fut).await;
}
// Enum for the GS232 commands
#[derive(Format, PartialEq)]
pub enum Gs232Cmd {
Unkown,
GetAl,
GetEz,
GetAlEz,
GetAz,
GetEl,
GetAzEl,
MoveTo(AzElPair),
Stop,
}
// Parse a GS232 commmand from a string slice
fn parse_command(data: &str) -> Gs232Cmd {
match data.chars().nth(0).unwrap() {
'B' => {
// Get Az command. Format 'B\r'
if data.len() == 1 {
Gs232Cmd::GetAl
Gs232Cmd::GetAz
} else {
Gs232Cmd::Unkown
}
}
'C' => {
// Get AZ and EL. Format 'C2\r'
if data.len() == 2 && data.chars().nth(1).unwrap() as char == '2' {
Gs232Cmd::GetAlEz
Gs232Cmd::GetAzEl
// Get EL only 'C\r'
} else if data.len() == 1 {
Gs232Cmd::GetEz
Gs232Cmd::GetEl
} else {
Gs232Cmd::Unkown
}
}
'W' => {
// Set position 'Waaa eee\r' with azimuth aaa and elevation eee.
// Fortunately rotcld will prepend zeros, so there will always be 3 digits per number.
if data.len() == 8 {
if let Ok(az) = data[1..4].parse::<u16>() {
if let Ok(el) = data[5..].parse::<u16>() {
@ -189,6 +225,7 @@ fn parse_command(data: &str) -> Gs232Cmd {
}
'S' => {
// Stop command. Format 'S\r'
if data.len() == 1 {
Gs232Cmd::Stop
} else {