#![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; }); #[global_allocator] static HEAP: Heap = Heap::empty(); static LOG_CH: Channel, 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>, 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 = 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 { 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; } } } }