387 lines
13 KiB
Rust
387 lines
13 KiB
Rust
#![no_std]
|
|
#![no_main]
|
|
extern crate alloc;
|
|
|
|
use crate::hal::peripherals::CAN1;
|
|
use canapi::id::{plant_id, MessageKind, IDENTIFY_CMD_OFFSET, MOISTURE_DATA_OFFSET};
|
|
use canapi::SensorSlot;
|
|
use ch32_hal::adc::{Adc, SampleTime, ADC_MAX};
|
|
use ch32_hal::can;
|
|
use ch32_hal::can::{Can, CanFifo, CanFilter, CanFrame, CanMode};
|
|
use ch32_hal::gpio::{Flex, Level, Output, Pull, Speed};
|
|
use ch32_hal::mode::NonBlocking;
|
|
use ch32_hal::peripherals::USBD;
|
|
use core::fmt::Write as _;
|
|
use embassy_executor::{task, Spawner};
|
|
use embassy_futures::yield_now;
|
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
|
use embassy_sync::channel::Channel;
|
|
use embassy_time::{Delay, Duration, Instant, Timer};
|
|
use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
|
|
use embassy_usb::{Builder, UsbDevice};
|
|
use embedded_can::{Id, StandardId};
|
|
use hal::bind_interrupts;
|
|
use hal::usbd::Driver;
|
|
use {ch32_hal as hal, panic_halt as _};
|
|
use embedded_alloc::LlffHeap as Heap;
|
|
use embedded_can::nb::Can as nb_can;
|
|
|
|
macro_rules! mk_static {
|
|
($t:ty,$val:expr) => {{
|
|
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
|
|
#[deny(unused_attributes)]
|
|
let x = STATIC_CELL.uninit().write(($val));
|
|
x
|
|
}};
|
|
}
|
|
|
|
bind_interrupts!(struct Irqs {
|
|
USB_LP_CAN1_RX0 => hal::usbd::InterruptHandler<hal::peripherals::USBD>;
|
|
});
|
|
|
|
#[global_allocator]
|
|
static HEAP: Heap = Heap::empty();
|
|
static LOG_CH: Channel<CriticalSectionRawMutex, heapless::String<128>, 8> = Channel::new();
|
|
|
|
#[embassy_executor::main(entry = "qingke_rt::entry")]
|
|
async fn main(spawner: Spawner) {
|
|
ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2));
|
|
|
|
unsafe {
|
|
static mut HEAP_SPACE: [u8; 4096] = [0; 4096]; // 4 KiB heap, adjust as needed
|
|
HEAP.init(HEAP_SPACE.as_ptr() as usize, HEAP_SPACE.len());
|
|
}
|
|
|
|
let p = hal::init(hal::Config {
|
|
rcc: hal::rcc::Config::SYSCLK_FREQ_144MHZ_HSI,
|
|
..Default::default()
|
|
});
|
|
|
|
// Build driver and USB stack using 'static buffers
|
|
let driver = Driver::new(p.USBD, Irqs, p.PA12, p.PA11);
|
|
|
|
|
|
|
|
let mut probe_gnd = Flex::new(p.PB1);
|
|
probe_gnd.set_as_input(Pull::None);
|
|
|
|
|
|
// Create GPIO for 555 Q output (PB0)
|
|
let q_out = Output::new(p.PB0, Level::Low, Speed::Low);
|
|
// Built-in LED on PB2 mirrors Q state
|
|
let mut info = Output::new(p.PB2, Level::Low, Speed::Low);
|
|
|
|
// Read configuration switches on PB3..PB7 at startup with floating detection
|
|
// PB3: Sensor A/B selector (Low=A, High=B)
|
|
// PB4..PB7: address bits (1,2,4,8)
|
|
let mut pb3 = Flex::new(p.PB3);
|
|
let mut pb4 = Flex::new(p.PB4);
|
|
let mut pb5 = Flex::new(p.PB5);
|
|
let mut pb6 = Flex::new(p.PB6);
|
|
let mut pb7 = Flex::new(p.PB7);
|
|
|
|
// Validate all config pins; if any is floating, stay in an error loop until fixed
|
|
// Try read PB3..PB7
|
|
let res_pb3 = detect_stable_pin(&mut pb3).await;
|
|
let res_pb4 = detect_stable_pin(&mut pb4).await;
|
|
let res_pb5 = detect_stable_pin(&mut pb5).await;
|
|
let res_pb6 = detect_stable_pin(&mut pb6).await;
|
|
let res_pb7 = detect_stable_pin(&mut pb7).await;
|
|
|
|
let slot = if res_pb3.unwrap_or(false) { SensorSlot::B } else { SensorSlot::A };
|
|
let mut addr: u8 = 0;
|
|
if res_pb4.unwrap_or(false) { addr |= 1; }
|
|
if res_pb5.unwrap_or(false) { addr |= 2; }
|
|
if res_pb6.unwrap_or(false) { addr |= 4; }
|
|
if res_pb7.unwrap_or(false) { addr |= 8; }
|
|
let moisture_id = plant_id(MOISTURE_DATA_OFFSET, slot, addr as u16);
|
|
let identify_id = plant_id(IDENTIFY_CMD_OFFSET, slot, addr as u16);
|
|
|
|
let invalid_config = res_pb3.is_none() || res_pb4.is_none() || res_pb5.is_none() || res_pb6.is_none() || res_pb7.is_none();
|
|
|
|
let mut config = embassy_usb::Config::new(0xC0DE, 0xCAFE);
|
|
config.manufacturer = Some("Can Sensor v0.2");
|
|
let msg = mk_static!(heapless::String<128>, heapless::String::new());;
|
|
let _ = core::fmt::Write::write_fmt(msg, format_args!("Sensor {:?} plant {}", slot, addr));
|
|
config.product = Some(msg.as_str());
|
|
config.serial_number = Some("12345678");
|
|
config.max_power = 100;
|
|
config.max_packet_size_0 = 64;
|
|
|
|
// Windows compatibility requires these; CDC-ACM
|
|
config.device_class = 0x02;
|
|
config.device_sub_class = 0x02;
|
|
config.device_protocol = 0x00;
|
|
config.composite_with_iads = false;
|
|
|
|
let mut builder = Builder::new(
|
|
driver,
|
|
config,
|
|
mk_static!([u8; 256], [0; 256]),
|
|
mk_static!([u8; 256], [0; 256]),
|
|
&mut [], // no msos descriptors
|
|
mk_static!([u8; 64], [0; 64]),
|
|
);
|
|
// Initialize CDC state and create CDC-ACM class
|
|
let class = mk_static!(
|
|
CdcAcmClass<'static, Driver<'static, hal::peripherals::USBD>>,
|
|
CdcAcmClass::new(&mut builder, mk_static!(State, State::new()), 64)
|
|
);
|
|
|
|
// Build USB device
|
|
let usb = mk_static!(UsbDevice<Driver<USBD>>, builder.build());
|
|
|
|
|
|
|
|
if invalid_config {
|
|
// At least one floating: report and blink code for the first one found.
|
|
let mut msg: heapless::String<128> = heapless::String::new();
|
|
let code = if res_pb4.is_none() { 1 } else if res_pb5.is_none() { 2 } else if res_pb6.is_none() { 3 } else if res_pb7.is_none() { 4 } else { 5 }; // PB3 -> 5
|
|
let which = match code { 1 => "PB4", 2 => "PB5", 3 => "PB6", 4 => "PB7", _ => "PB3 (A/B)" };
|
|
let _ = core::fmt::Write::write_fmt(&mut msg, format_args!("Config pin floating detected on {} -> blinking code {}. Fix jumpers.\r\n", which, code));
|
|
log(msg);
|
|
blink_error(&mut info, code).await;
|
|
};
|
|
|
|
|
|
// Log startup configuration and derived CAN IDs
|
|
|
|
{
|
|
let mut msg: heapless::String<128> = heapless::String::new();
|
|
let slot_chr = match slot { SensorSlot::A => 'a', SensorSlot::B => 'b' };
|
|
let _ = core::fmt::Write::write_fmt(&mut msg, format_args!(
|
|
"Startup: slot={} addr={} moisture_id=0x{:03X} identity_id=0x{:03X}\r\n",
|
|
slot_chr, addr, moisture_id, identify_id
|
|
));
|
|
log(msg);
|
|
}
|
|
|
|
// Create ADC on ADC1 and use PA1 as analog input (Threshold/Trigger)
|
|
let adc = Adc::new(p.ADC1, Default::default());
|
|
let ain = p.PA1;
|
|
let config = can::can::Config::default();
|
|
let can: Can<CAN1, NonBlocking> = Can::new_nb(
|
|
p.CAN1,
|
|
p.PB8,
|
|
p.PB9,
|
|
CanFifo::Fifo0,
|
|
CanMode::Normal,
|
|
125_000,
|
|
config,
|
|
)
|
|
.expect("Valid");
|
|
ch32_hal::pac::AFIO.pcfr1().write(|w| w.set_can1_rm(2));
|
|
|
|
spawner.spawn(usb_task(usb)).unwrap();
|
|
spawner.spawn(usb_writer(class)).unwrap();
|
|
// move Q output, LED, ADC and analog input into worker task
|
|
spawner.spawn(worker(probe_gnd, q_out, info, adc, ain, can, StandardId::new(moisture_id).unwrap(), StandardId::new(identify_id).unwrap())).unwrap();
|
|
|
|
// Prevent main from exiting
|
|
core::future::pending::<()>().await;
|
|
}
|
|
|
|
|
|
// Helper closure: detect stable pin by comparing readings under Pull::Down and Pull::Up
|
|
async fn detect_stable_pin(pin: &mut Flex<'static>) -> Option<bool> {
|
|
pin.set_as_input(Pull::Down);
|
|
Timer::after_millis(2).await;
|
|
let low_read = pin.is_high();
|
|
pin.set_as_input(Pull::Up);
|
|
Timer::after_millis(2).await;
|
|
let high_read = pin.is_high();
|
|
if low_read == high_read { Some(high_read) } else { None }
|
|
}
|
|
async fn blink_error(mut info_led: &mut Output<'static>, code: u8) -> !{
|
|
loop {
|
|
// code: 1-4 for PB4..PB7, 5 for PB3 (A/B)
|
|
for _ in 0..code {
|
|
info_led.set_high();
|
|
Timer::after_millis(200).await;
|
|
info_led.set_low();
|
|
Timer::after_millis(200).await;
|
|
}
|
|
// Pause between sequences
|
|
Timer::after_secs(2).await;
|
|
}
|
|
}
|
|
|
|
|
|
#[task]
|
|
async fn worker(
|
|
mut probe_gnd: Flex<'static>,
|
|
mut q: Output<'static>,
|
|
mut info: Output<'static>,
|
|
mut adc: Adc<'static, hal::peripherals::ADC1>,
|
|
mut ain: hal::peripherals::PA1,
|
|
mut can: Can<'static, CAN1, NonBlocking>,
|
|
moisture_id: StandardId,
|
|
identify_id: StandardId
|
|
) {
|
|
// 555 emulation state: Q initially Low
|
|
let mut q_high = false;
|
|
let low_th: u16 = (ADC_MAX as u16) / 3; // ~1/3 Vref
|
|
let high_th: u16 = ((ADC_MAX as u32 * 2) / 3) as u16; // ~2/3 Vref
|
|
|
|
let mut filter = CanFilter::new_id_list();
|
|
|
|
filter
|
|
.get(0)
|
|
.unwrap()
|
|
.set(identify_id.into(), Default::default());
|
|
|
|
//can.add_filter(filter);
|
|
can.add_filter(CanFilter::accept_all());
|
|
|
|
loop {
|
|
// Count rising edges of Q in a 100 ms window
|
|
let start = Instant::now();
|
|
let mut pulses: u32 = 0;
|
|
let mut last_q = q_high;
|
|
|
|
probe_gnd.set_as_output(Speed::Low);
|
|
probe_gnd.set_low();
|
|
let probe_duration = Duration::from_millis(1000);
|
|
while Instant::now()
|
|
.checked_duration_since(start)
|
|
.unwrap_or(Duration::from_millis(0))
|
|
< probe_duration
|
|
{
|
|
// Sample the analog input (Threshold/Trigger on A1)
|
|
let val: u16 = adc.convert(&mut ain, SampleTime::CYCLES28_5);
|
|
|
|
// 555 core behavior:
|
|
// - If input <= 1/3 Vref => set Q high (trigger)
|
|
// - If input >= 2/3 Vref => set Q low (threshold)
|
|
// - Otherwise keep previous Q state (hysteresis)
|
|
if val <= low_th {
|
|
q_high = true;
|
|
} else if val >= high_th {
|
|
q_high = false;
|
|
}
|
|
|
|
// Drive output pin accordingly
|
|
if q_high {
|
|
q.set_high();
|
|
} else {
|
|
q.set_low();
|
|
}
|
|
|
|
// Count rising edges
|
|
if !last_q && q_high {
|
|
pulses = pulses.saturating_add(1);
|
|
}
|
|
last_q = q_high;
|
|
|
|
// Yield to allow USB and other tasks to run
|
|
yield_now().await;
|
|
}
|
|
probe_gnd.set_as_input(Pull::None);
|
|
|
|
// Compute frequency from 100 ms window
|
|
let freq_hz = pulses; // pulses per 0.1s => Hz
|
|
|
|
let mut msg: heapless::String<128> = heapless::String::new();
|
|
let _ = write!(
|
|
&mut msg,
|
|
"555 window={}ms pulses={} freq={} Hz (A1->Q on PB0)\r\n", probe_duration.as_millis(),
|
|
pulses, freq_hz
|
|
);
|
|
log(msg);
|
|
|
|
let mut moisture = CanFrame::new(moisture_id, &(freq_hz as u16).to_be_bytes()).unwrap();
|
|
match can.transmit(&mut moisture) {
|
|
Ok(..) => {
|
|
let mut msg: heapless::String<128> = heapless::String::new();
|
|
let _ = write!(&mut msg, "Send to canbus");
|
|
log(msg);
|
|
}
|
|
Err(err) => {
|
|
for _ in 0..3 {
|
|
info.set_high();
|
|
Timer::after_millis(100).await;
|
|
info.set_low();
|
|
Timer::after_millis(100).await;
|
|
}
|
|
let mut msg: heapless::String<128> = heapless::String::new();
|
|
let _ = write!(&mut msg, "err {:?}", err);
|
|
log(msg);
|
|
}
|
|
}
|
|
|
|
loop {
|
|
let mut msg: heapless::String<128> = heapless::String::new();
|
|
let _ = write!(
|
|
&mut msg,
|
|
"Check identity addr received: {:#x} \r\n",
|
|
identify_id.as_raw()
|
|
);
|
|
log(msg);
|
|
|
|
|
|
yield_now().await;
|
|
match can.receive() {
|
|
Ok(frame) => match frame.id() {
|
|
Id::Standard(s_frame) => {
|
|
let mut msg: heapless::String<128> = heapless::String::new();
|
|
let _ = write!(
|
|
&mut msg,
|
|
"Received from canbus: {:?} ident is {:?} \r\n",
|
|
s_frame.as_raw(),
|
|
identify_id.as_raw()
|
|
);
|
|
log(msg);
|
|
if s_frame.as_raw() == identify_id.as_raw() {
|
|
for _ in 0..10 {
|
|
Timer::after_millis(250).await;
|
|
info.toggle();
|
|
}
|
|
info.set_low();
|
|
}
|
|
}
|
|
Id::Extended(_) => {}
|
|
},
|
|
|
|
Err(err) => {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn log(message: heapless::String<128>) {
|
|
match LOG_CH.try_send(message) {
|
|
Ok(_) => {}
|
|
Err(_) => {}
|
|
}
|
|
}
|
|
|
|
#[task]
|
|
async fn usb_task(usb: &'static mut UsbDevice<'static, Driver<'static, hal::peripherals::USBD>>) {
|
|
usb.run().await;
|
|
}
|
|
|
|
#[task]
|
|
async fn usb_writer(
|
|
class: &'static mut CdcAcmClass<'static, Driver<'static, hal::peripherals::USBD>>,
|
|
) {
|
|
loop {
|
|
class.wait_connection().await;
|
|
printer(class).await;
|
|
}
|
|
}
|
|
|
|
async fn printer(class: &mut CdcAcmClass<'static, Driver<'static, USBD>>) {
|
|
loop {
|
|
let msg = LOG_CH.receive().await;
|
|
match class.write_packet(msg.as_bytes()).await {
|
|
Ok(_) => {}
|
|
Err(_) => {
|
|
// Disconnected or endpoint disabled
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|