405 lines
14 KiB
Rust
405 lines
14 KiB
Rust
//! # Firmware for the handcrank project.
|
|
//!
|
|
//! Implements a voltage boost regulator. Measures the input and output voltages and output current
|
|
//! using an external ADC connected via SPI to pins GP2 to GP5. The switching transistor is
|
|
//! controlled directly via a PWM signal on pin GP10.
|
|
|
|
#![no_std]
|
|
#![no_main]
|
|
|
|
use embedded_hal::digital::v2::OutputPin;
|
|
|
|
// Ensure we halt the program on panic (if we don't mention this crate it won't
|
|
// be linked)
|
|
use panic_halt as _;
|
|
|
|
// Alias for our HAL crate
|
|
use rp2040_hal as hal;
|
|
|
|
// A shorter alias for the Peripheral Access Crate, which provides low-level
|
|
// register access
|
|
use hal::pac;
|
|
|
|
// SPI + DMA
|
|
use hal::dma::DMAExt;
|
|
|
|
// UART related types
|
|
use hal::uart::{DataBits, StopBits, UartConfig};
|
|
|
|
use heapless::String;
|
|
|
|
// Some traits we need
|
|
//use cortex_m::singleton;
|
|
use fugit::MicrosDurationU32;
|
|
use embedded_hal::PwmPin;
|
|
use fugit::RateExtU32;
|
|
use hal::clocks::Clock;
|
|
use hal::timer::Alarm;
|
|
use hal::timer::ScheduleAlarmError;
|
|
use pac::interrupt;
|
|
use core::cell::RefCell;
|
|
use critical_section::Mutex;
|
|
|
|
mod ext_adc;
|
|
mod logger;
|
|
mod switch_control;
|
|
|
|
use logger::UartLogger;
|
|
use logger::Logger;
|
|
|
|
/// The linker will place this boot block at the start of our program image. We
|
|
/// need this to help the ROM bootloader get our code up and running.
|
|
/// Note: This boot block is not necessary when using a rp-hal based BSP
|
|
/// as the BSPs already perform this step.
|
|
#[link_section = ".boot2"]
|
|
#[used]
|
|
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;
|
|
|
|
const SYSTICK_INTERVAL_US: MicrosDurationU32 = MicrosDurationU32::millis(1);
|
|
|
|
static mut SYSTICK_ALARM: Mutex<RefCell<Option<hal::timer::Alarm0>>> = Mutex::new(RefCell::new(None));
|
|
|
|
// Flag that is set by the alarm interrupt
|
|
static mut SYSTICK_FLAG: bool = false;
|
|
|
|
/// External high-speed crystal on the Raspberry Pi Pico board is 12 MHz. Adjust
|
|
/// if your board has a different frequency
|
|
const XTAL_FREQ_HZ: u32 = 12_000_000u32;
|
|
|
|
const SWITCH_PWM_MAX: i32 = 512;
|
|
const SWITCH_PWM_LIMIT: i32 = 93 * SWITCH_PWM_MAX / 100;
|
|
|
|
fn convert_adc_measurements(raw: &[u16; 4]) -> (i32, i32, i32)
|
|
{
|
|
let iout = (raw[0] as i32 - raw[1] as i32) * 3300 * 20 / 4096 / 28; // *20 = division by 50 mΩ shunt
|
|
let vout = raw[2] as i32 * 3300 * (220 + 10) / 10 / 4096;
|
|
let vin = raw[3] as i32 * 3300 * (100 + 10) / 10 / 4096;
|
|
|
|
(vin, vout, iout) /* units: mV, mV, mA */
|
|
}
|
|
|
|
/// Entry point to our bare-metal application.
|
|
///
|
|
/// The `#[rp2040_hal::entry]` macro ensures the Cortex-M start-up code calls this function
|
|
/// as soon as all global variables and the spinlock are initialised.
|
|
///
|
|
/// The function configures the RP2040 peripherals, then fades the LED in an
|
|
/// infinite loop.
|
|
#[rp2040_hal::entry]
|
|
fn main() -> ! {
|
|
// Grab our singleton objects
|
|
let mut pac = pac::Peripherals::take().unwrap();
|
|
|
|
// Set up the watchdog driver - needed by the clock setup code
|
|
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
|
|
|
|
// Configure the clocks
|
|
//
|
|
// The default is to generate a 125 MHz system clock
|
|
let clocks = hal::clocks::init_clocks_and_plls(
|
|
XTAL_FREQ_HZ,
|
|
pac.XOSC,
|
|
pac.CLOCKS,
|
|
pac.PLL_SYS,
|
|
pac.PLL_USB,
|
|
&mut pac.RESETS,
|
|
&mut watchdog,
|
|
)
|
|
.ok()
|
|
.unwrap();
|
|
|
|
// The single-cycle I/O block controls our GPIO pins
|
|
let sio = hal::Sio::new(pac.SIO);
|
|
|
|
// Set the pins up according to their function on this particular board
|
|
let pins = hal::gpio::Pins::new(
|
|
pac.IO_BANK0,
|
|
pac.PADS_BANK0,
|
|
sio.gpio_bank0,
|
|
&mut pac.RESETS,
|
|
);
|
|
|
|
// The delay object lets us wait for specified amounts of time (in
|
|
// milliseconds)
|
|
//let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
|
|
|
|
// UART
|
|
let uart_pins = (
|
|
// UART TX (characters sent from RP2040) on pin 1 (GPIO0)
|
|
pins.gpio0.into_mode::<hal::gpio::FunctionUart>(),
|
|
// UART RX (characters received by RP2040) on pin 2 (GPIO1)
|
|
pins.gpio1.into_mode::<hal::gpio::FunctionUart>(),
|
|
);
|
|
let uart = hal::uart::UartPeripheral::new(pac.UART0, uart_pins, &mut pac.RESETS)
|
|
.enable(
|
|
UartConfig::new(38400.Hz(), DataBits::Eight, None, StopBits::One),
|
|
clocks.peripheral_clock.freq(),
|
|
)
|
|
.unwrap();
|
|
|
|
// initialize the logger
|
|
let mut logger: UartLogger<_, _, 256> = UartLogger::new(uart);
|
|
logger.log(b"Logging initialized.\r\n").unwrap();
|
|
|
|
// Init PWMs
|
|
let mut pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
|
|
|
|
// Configure switch PWM
|
|
let pwm5 = &mut pwm_slices.pwm5;
|
|
pwm5.set_ph_correct();
|
|
pwm5.set_div_int(4);
|
|
pwm5.set_div_frac((88 << 4) / 100);
|
|
pwm5.set_top(SWITCH_PWM_MAX as u16);
|
|
pwm5.enable();
|
|
|
|
// set up switch PWM output
|
|
// note:
|
|
// duty cycle = 0 => output voltage = input voltage
|
|
// duty cycle = 512 (100%) => input short circuit
|
|
//
|
|
// general: D = duty cycle (0..1)
|
|
// => output voltage = 1/(1-D) * input voltage
|
|
let pwr_switch_ch = &mut pwm5.channel_a;
|
|
pwr_switch_ch.set_duty(0); // no output by default
|
|
pwr_switch_ch.output_to(pins.gpio10);
|
|
|
|
// LED pins
|
|
let mut pin_led_r = pins.gpio15.into_push_pull_output();
|
|
let mut pin_led_y = pins.gpio13.into_push_pull_output();
|
|
let mut pin_led_g = pins.gpio14.into_push_pull_output();
|
|
|
|
// off by default
|
|
pin_led_r.set_low().unwrap();
|
|
pin_led_y.set_low().unwrap();
|
|
pin_led_g.set_low().unwrap();
|
|
|
|
// SPI CS pin is controlled by software
|
|
let mut spi_cs = pins.gpio5.into_push_pull_output();
|
|
spi_cs.set_high().unwrap();
|
|
|
|
// These are implicitly used by the spi driver if they are in the correct mode
|
|
let _spi_sclk = pins.gpio2.into_mode::<hal::gpio::FunctionSpi>();
|
|
let _spi_mosi = pins.gpio3.into_mode::<hal::gpio::FunctionSpi>();
|
|
let _spi_miso = pins.gpio4.into_mode::<hal::gpio::FunctionSpi>();
|
|
|
|
// Initialize DMA.
|
|
let dma = pac.DMA.split(&mut pac.RESETS);
|
|
|
|
let mut adc_ctrl = ext_adc::AdcContext::init(
|
|
pac.SPI0,
|
|
clocks.peripheral_clock.freq(),
|
|
&mut pac.RESETS,
|
|
dma.ch0,
|
|
dma.ch1,
|
|
spi_cs
|
|
);
|
|
|
|
logger.log(b"Initialization complete!\r\n").unwrap();
|
|
|
|
// initialize the timer
|
|
let mut timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS);
|
|
|
|
// set up an alarm that is used for a periodic main loop execution
|
|
let mut next_systick_instant = timer.get_counter() + MicrosDurationU32::millis(1000);
|
|
|
|
let mut systick_alarm = timer.alarm_0().unwrap();
|
|
|
|
systick_alarm.schedule_at(next_systick_instant).unwrap();
|
|
systick_alarm.enable_interrupt();
|
|
|
|
unsafe {
|
|
// move the alarm object so the IRQ can access it
|
|
critical_section::with(|cs| {
|
|
SYSTICK_ALARM.borrow(cs).replace(Some(systick_alarm));
|
|
});
|
|
|
|
// Unmask the timer0 IRQ so that it will generate an interrupt
|
|
pac::NVIC::unmask(pac::Interrupt::TIMER_IRQ_0);
|
|
}
|
|
|
|
let mut switchctrl = switch_control::SwitchControl::<1>::new(12000, 100, SWITCH_PWM_LIMIT);
|
|
|
|
// track nano_joules (== nanoVoltAmpereSeconds) generated, for LED flashing
|
|
let mut nano_joules: u32 = 0;
|
|
let mut joules: u32 = 0; // total energy generated
|
|
let mut loopcnt_led_y_off = 0;
|
|
|
|
let mut loopcnt: u64 = 0;
|
|
|
|
pin_led_r.set_high().unwrap();
|
|
|
|
// main loop
|
|
loop {
|
|
// only execute the main loop if the systick has expired
|
|
let mut systick_received = false;
|
|
|
|
critical_section::with(|_cs| unsafe {
|
|
systick_received = core::ptr::read_volatile(&SYSTICK_FLAG);
|
|
});
|
|
|
|
if systick_received {
|
|
// re-shedule the systick alarm
|
|
critical_section::with(|_cs| unsafe {
|
|
core::ptr::write_volatile(&mut SYSTICK_FLAG, false);
|
|
});
|
|
|
|
next_systick_instant += SYSTICK_INTERVAL_US;
|
|
|
|
critical_section::with(|cs| {
|
|
unsafe {
|
|
if let Some(mut systick_alarm) = SYSTICK_ALARM.borrow(cs).take() {
|
|
let result = systick_alarm.schedule_at(next_systick_instant);
|
|
match result {
|
|
Ok(_) => {}, // just continue
|
|
Err(e) => {
|
|
match e {
|
|
ScheduleAlarmError::AlarmTooLate => logger.log_fatal(b"Systick: alarm too late.\r\n"),
|
|
};
|
|
}
|
|
}
|
|
|
|
SYSTICK_ALARM.borrow(cs).replace(Some(systick_alarm));
|
|
} else {
|
|
logger.log_fatal(b"Systick: object not set when it should be!\r\n");
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
let mut adc_value: [u16; 4] = [0; 4];
|
|
|
|
(adc_ctrl, adc_value[0]) = adc_ctrl.sample_adc_channel(0);
|
|
(adc_ctrl, adc_value[1]) = adc_ctrl.sample_adc_channel(1);
|
|
(adc_ctrl, adc_value[2]) = adc_ctrl.sample_adc_channel(2);
|
|
(adc_ctrl, adc_value[3]) = adc_ctrl.sample_adc_channel(3);
|
|
|
|
let (vin, vout, iout) = convert_adc_measurements(&adc_value);
|
|
|
|
// Update the PWM value for the switch
|
|
let pwmval = switchctrl.update(vin, vout, iout);
|
|
|
|
pwr_switch_ch.set_duty(pwmval as u16);
|
|
|
|
// Yellow LED flashes for 10 ms whenever 1 Joule has been generated.
|
|
// This means that it flashes once per second when output power is 1 Watt, and 3 times
|
|
// per second if output power is 3 Watt.
|
|
let nano_joules_generated = vout * iout; // * 1 millisecond, implicitely
|
|
if nano_joules_generated > 0 {
|
|
nano_joules += nano_joules_generated as u32;
|
|
}
|
|
|
|
// 1000 mV * 1000 mA * 1000 ms = 1 Joule
|
|
if nano_joules >= 1000000000 {
|
|
pin_led_y.set_high().unwrap();
|
|
loopcnt_led_y_off = loopcnt + 10;
|
|
nano_joules -= 1000000000;
|
|
joules += 1;
|
|
}
|
|
|
|
if loopcnt == loopcnt_led_y_off {
|
|
pin_led_y.set_low().unwrap();
|
|
}
|
|
|
|
// Green LED is on in constant-voltage mode and off in MPPT mode.
|
|
match switchctrl.get_control_mode() {
|
|
switch_control::ControlMode::ConstantVoltage => {pin_led_g.set_high().unwrap()},
|
|
switch_control::ControlMode::MPPT => {pin_led_g.set_low().unwrap()},
|
|
switch_control::ControlMode::ImpedanceControl => {pin_led_g.set_low().unwrap()},
|
|
}
|
|
|
|
// do not output status data every loop
|
|
match loopcnt % 500 {
|
|
1 => {
|
|
let mut data: String<16>;
|
|
|
|
logger.log(b"Vin: ").unwrap();
|
|
data = String::from(vin);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b"mV - Vout: ").unwrap();
|
|
data = String::from(vout);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b"mV - Iout: ").unwrap();
|
|
data = String::from(iout);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b"mA - Pout: ").unwrap();
|
|
data = String::from(vout * iout / 1000);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b"mW - E: ").unwrap();
|
|
data = String::from(joules);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b"J / ").unwrap();
|
|
data = String::from(joules/3600);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
|
|
logger.log(b"Wh - raw: [").unwrap();
|
|
|
|
for i in 0..adc_value.len() {
|
|
data = String::from(adc_value[i]);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
|
|
if i < 3 {
|
|
logger.log(b", ").unwrap();
|
|
}
|
|
}
|
|
|
|
logger.log(b"]; PWM: ").unwrap();
|
|
data = String::from(pwmval);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
|
|
logger.log(b"\r\n").unwrap();
|
|
},
|
|
2 => {
|
|
let mut data: String<16>;
|
|
|
|
logger.log(b"{\"vin\":").unwrap();
|
|
data = String::from(vin);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b",\"vout\":").unwrap();
|
|
data = String::from(vout);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b",\"iout\":").unwrap();
|
|
data = String::from(iout);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b",\"pout\":").unwrap();
|
|
data = String::from(vout * iout);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b",\"energy\":").unwrap();
|
|
data = String::from(joules);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b",\"pwm\"").unwrap();
|
|
data = String::from(pwmval);
|
|
logger.log(data.as_bytes()).unwrap();
|
|
logger.log(b"}\r\n").unwrap();
|
|
},
|
|
3 => {
|
|
switchctrl.log_status(&mut logger);
|
|
},
|
|
_ => {}
|
|
}
|
|
|
|
loopcnt += 1;
|
|
}
|
|
|
|
logger.process().unwrap();
|
|
|
|
// save some power as long as no interrupt occurs
|
|
// FIXME: not working yet for some reason
|
|
//cortex_m::asm::wfi();
|
|
}
|
|
}
|
|
|
|
#[interrupt]
|
|
fn TIMER_IRQ_0() {
|
|
critical_section::with(|cs| {
|
|
unsafe {
|
|
core::ptr::write_volatile(&mut SYSTICK_FLAG, true);
|
|
|
|
if let Some(mut systick_alarm) = SYSTICK_ALARM.borrow(cs).take() {
|
|
systick_alarm.clear_interrupt();
|
|
|
|
SYSTICK_ALARM.borrow(cs).replace(Some(systick_alarm));
|
|
}
|
|
}
|
|
});
|
|
}
|