From b816fbcfcccb423870424c4a0f66453ce4cdc660 Mon Sep 17 00:00:00 2001 From: Benjamin Koch <bbbsnowball@gmail.com> Date: Sun, 21 May 2023 00:46:37 +0200 Subject: [PATCH] add very simple Modbus communication --- firmware/rust1/Cargo.lock | 16 +++ firmware/rust1/Cargo.toml | 1 + firmware/rust1/src/rs485.rs | 240 +++++++++++++++++++++++++++++++++--- 3 files changed, 240 insertions(+), 17 deletions(-) diff --git a/firmware/rust1/Cargo.lock b/firmware/rust1/Cargo.lock index 4d809ce..1826c0d 100644 --- a/firmware/rust1/Cargo.lock +++ b/firmware/rust1/Cargo.lock @@ -259,6 +259,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + [[package]] name = "crc-any" version = "2.4.3" @@ -268,6 +277,12 @@ dependencies = [ "debug-helper", ] +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + [[package]] name = "critical-section" version = "1.1.1" @@ -1014,6 +1029,7 @@ dependencies = [ "byte-slice-cast 1.2.2", "cortex-m", "cortex-m-rt", + "crc", "defmt", "defmt-rtt", "display-interface", diff --git a/firmware/rust1/Cargo.toml b/firmware/rust1/Cargo.toml index 7268c1f..d2db0ff 100644 --- a/firmware/rust1/Cargo.toml +++ b/firmware/rust1/Cargo.toml @@ -55,6 +55,7 @@ pio-proc = "0.2" pio = "0.2.1" heapless = "0.7.16" +crc = "3.0.1" [profile.release] debug = true diff --git a/firmware/rust1/src/rs485.rs b/firmware/rust1/src/rs485.rs index 2b1919d..9c78b1e 100644 --- a/firmware/rust1/src/rs485.rs +++ b/firmware/rust1/src/rs485.rs @@ -13,6 +13,8 @@ use embassy_rp::Peripheral; use {defmt_rtt as _, panic_probe as _}; use fixed::traits::ToFixed; use fixed_macro::types::U56F8; +use heapless::Vec; +use crc::{Crc, CRC_16_MODBUS}; use crate::dont_abort::DontAbort; use crate::dont_abort::DontAbortMode::*; @@ -37,6 +39,7 @@ fn pin_io<P: gpio::Pin>(pin: &P) -> pac::io::Gpio { block.gpio(pin.pin() as _) } +#[allow(dead_code)] async fn debug_print_pio_addr(sm: pac::pio::StateMachine) { let mut prev = 42u8; loop { @@ -49,6 +52,146 @@ async fn debug_print_pio_addr(sm: pac::pio::StateMachine) { } } +#[repr(u8)] +pub enum ModbusErrorCode { + IllegalFunction = 1, + IllegalDataAddress = 2, + IllegalDataValue = 3, + ServerDeviceFailure = 4, + Acknowledge = 5, + ServerDeviceBusy = 6, + MemoryParityError = 7, + GatewayPathUnavailable = 0xa, + GatewayTargetDeviceFailedToRespond = 0xb, +} + +fn read_modbus_holding_register(addr: u16) -> Result<u16, ModbusErrorCode> { + if addr == 1 { + return Ok(42) + } + Err(ModbusErrorCode::IllegalDataAddress) +} + +fn read_modbus_input_register(addr: u16) -> Result<u16, ModbusErrorCode> { + if addr == 2 { + return Ok(42) + } + Err(ModbusErrorCode::IllegalDataAddress) +} + +fn write_modbus_register(addr: u16, value: u16) -> Result<(), ModbusErrorCode> { + Err(ModbusErrorCode::IllegalDataAddress) +} + +fn modbus_reply_error(rxbuf: &Vec<u8, 32>, txbuf: &mut Vec<u8, 32>, code: ModbusErrorCode) { + txbuf.clear(); + txbuf.push(rxbuf[0]).unwrap(); + txbuf.push(rxbuf[1] | 0x80).unwrap(); + txbuf.push(code as u8).unwrap(); +} + +fn handle_modbus_frame2(rxbuf: &Vec<u8, 32>, txbuf: &mut Vec<u8, 32>) -> Result<(), ModbusErrorCode> { + use ModbusErrorCode::*; + + info!("Modbus frame: {:?}", rxbuf.as_slice()); + + match rxbuf[1] { + 0x03 => { + // read holding registers + if rxbuf.len() != 8 { + // we shouldn't get here + return Err(ServerDeviceFailure); + } + let start = ((rxbuf[2] as u16) << 8) | rxbuf[3] as u16; + let quantity = ((rxbuf[4] as u16) << 8) | rxbuf[5] as u16; + if quantity as usize > (txbuf.capacity() - 5) / 2 || quantity >= 128 { + return Err(IllegalDataValue); // is that right? + } + txbuf.push(rxbuf[0]).or(Err(ServerDeviceFailure))?; + txbuf.push(rxbuf[1]).or(Err(ServerDeviceFailure))?; + txbuf.push((quantity*2) as u8).or(Err(ServerDeviceFailure))?; + for i in 0..quantity { + let value = read_modbus_holding_register(start + i)?; + txbuf.push((value >> 8) as u8).or(Err(ServerDeviceFailure))?; + txbuf.push((value & 0xff) as u8).or(Err(ServerDeviceFailure))?; + } + Ok(()) + }, + 0x04 => { + // read input registers + if rxbuf.len() != 8 { + // we shouldn't get here + return Err(ServerDeviceFailure); + } + let start = ((rxbuf[2] as u16) << 8) | rxbuf[3] as u16; + let quantity = ((rxbuf[4] as u16) << 8) | rxbuf[5] as u16; + if quantity as usize > (txbuf.capacity() - 5) / 2 || quantity >= 128 { + return Err(IllegalDataValue); // is that right? + } + txbuf.push(rxbuf[0]).or(Err(ServerDeviceFailure))?; + txbuf.push(rxbuf[1]).or(Err(ServerDeviceFailure))?; + txbuf.push((quantity*2) as u8).or(Err(ServerDeviceFailure))?; + for i in 0..quantity { + let value = read_modbus_input_register(start + i)?; + txbuf.push((value >> 8) as u8).or(Err(ServerDeviceFailure))?; + txbuf.push((value & 0xff) as u8).or(Err(ServerDeviceFailure))?; + } + Ok(()) + }, + 0x06 => { + // write register + Err(IllegalDataAddress) + }, + 0x10 => { + // write multiple registers + Err(IllegalDataAddress) + }, + _ => { + Err(IllegalFunction) + }, + } +} + +fn handle_modbus_frame(rxbuf: &Vec<u8, 32>, txbuf: &mut Vec<u8, 32>) { + match handle_modbus_frame2(rxbuf, txbuf) { + Ok(()) => { + if txbuf.capacity() - txbuf.len() < 2 { + // We don't have enough space for the CRC so reply with error instead. + modbus_reply_error(rxbuf, txbuf, ModbusErrorCode::ServerDeviceFailure); + } + }, + Err(code) => { + modbus_reply_error(rxbuf, txbuf, code); + } + } + + const CRC: Crc<u16> = Crc::<u16>::new(&CRC_16_MODBUS); + let x = CRC.checksum(txbuf.as_slice()); + txbuf.push((x & 0xff) as u8).unwrap(); + txbuf.push((x >> 8) as u8).unwrap(); +} + +enum ModbusFrameLength { + NeedMoreData(u16), + Length(u16), + Unknown, +} + +//FIXME This won't work if this is a response frame! +fn get_modbus_frame_length(rxbuf: &[u8]) -> ModbusFrameLength { + use ModbusFrameLength::*; + + if rxbuf.len() < 3 { + return NeedMoreData(3); + } + + match rxbuf[1] { + 0x01..=0x06 => Length(8), + 0x0f | 0x10 => if rxbuf.len() == 7 { Length(9 + rxbuf[6] as u16) } else { NeedMoreData(7) }, + _ => Unknown, + } +} + impl RS485 { pub fn new( uart: UART0, rx_pin: peripherals::PIN_17, tx_pin: peripherals::PIN_16, tx_en_pin: peripherals::PIN_15, @@ -246,25 +389,26 @@ impl RS485 { sm1.set_pin_dirs(embassy_rp::pio::Direction::Out, &[&tx_en_pin_pio, &tx_pin_pio]); sm1.set_enable(true); - let mut tx_data = [0, 'H' as u32, 'e' as u32, 'l' as u32, 'l' as u32, 'o' as u32, '\r' as u32, '\n' as u32]; - tx_data[0] = (tx_data.len() - 2) as u32; - for i in 1..tx_data.len() { - let x = tx_data[i] & 0xff; - let mut parity = 0; - for j in 0..8 { - parity ^= x>>j; - } - tx_data[i] = x | ((parity&1) << 8); - } + //FIXME Can we split Modbus parts from UART/RS485? + let mut rxbuf = Vec::<u8, 32>::new(); + const CRC: Crc<u16> = Crc::<u16>::new(&CRC_16_MODBUS); + let mut rxcrc = CRC.digest(); + let mut rx_expected_bytes = ModbusFrameLength::NeedMoreData(3); + let mut rx_received_bytes = 0u16; + let mut rx_char_prev: u8 = 0; + const TX_BUF_LENGTH: usize = 32; + let mut txbuf = Vec::<u8, TX_BUF_LENGTH>::new(); + let mut tx_data = [0; TX_BUF_LENGTH+1]; let mut dma_in_ref = self.dma_channel.into_ref(); let mut dma_tx_ref = self.tx_dma_channel.into_ref(); let mut din = [42u32; 9]; let mut bit_index = 0; - let mut rx_buf = [0; 1]; - let mut rx_future = DontAbort::new(self.rx.read(&mut rx_buf), PanicIfReused); - let mut tx_future = DontAbort::new(sm1.tx().dma_push(dma_tx_ref.reborrow(), &tx_data), HangIfReused); + let mut rx_buf_one = [0; 1]; + let mut rx_future = DontAbort::new(self.rx.read(&mut rx_buf_one), PanicIfReused); + // We transmit with length zero to have a dummy no-op future with the right type. This seems to work ok. + let mut tx_future = DontAbort::new(sm1.tx().dma_push(dma_tx_ref.reborrow(), &tx_data[0..0]), HangIfReused); loop { let x = select4( irq0.wait(), @@ -368,16 +512,78 @@ impl RS485 { drop(rx_future); match x { Result::Ok(()) => { - info!("RX {:?}", rx_buf); + info!("RX {:?}", rx_buf_one); + let rx_char = rx_buf_one[0]; - drop(tx_future); - tx_future = DontAbort::new(sm1.tx().dma_push(dma_tx_ref.reborrow(), &tx_data), HangIfReused); + rxcrc.update(&[rx_char]); + if !rxbuf.is_full() { + rxbuf.push(rx_char).unwrap_or_default(); + } + rx_received_bytes += 1; + + if let ModbusFrameLength::NeedMoreData(x) = rx_expected_bytes { + if x == rx_received_bytes { + rx_expected_bytes = get_modbus_frame_length(rxbuf.as_slice()); + match rx_expected_bytes { + ModbusFrameLength::Unknown => { + //FIXME Wait for pause. + }, + _ => {} + } + } + } + if let ModbusFrameLength::Length(x) = rx_expected_bytes { + if x == rx_received_bytes { + let received_crc = rx_char_prev as u16 | ((rx_char as u16) << 8); + let calculated_crc = rxcrc.finalize(); + rxcrc = CRC.digest(); + info!("received_crc: {:04x}, calculated_crc: {:04x}", received_crc, calculated_crc); + + //FIXME In case of CRC mismatch, wait for gap/idle of >=1.5 chars. + const CORRECT_CRC: u16 = 0; // because we include the CRC bytes in our calculation + const OUR_ADDRESS: u8 = 1; + if rxbuf[0] == OUR_ADDRESS && calculated_crc == CORRECT_CRC { + txbuf.clear(); + handle_modbus_frame(&rxbuf, &mut txbuf); + + if !txbuf.is_empty() { + drop(tx_future); + + tx_data[0] = (txbuf.len() - 1) as u32; + for i in 0..txbuf.len() { + let x = txbuf[i] & 0xff; + let mut parity = 0; + for j in 0..8 { + parity ^= x>>j; + } + tx_data[i + 1] = x as u32 | (((parity as u32) & 1) << 8); + } + + tx_future = DontAbort::new(sm1.tx().dma_push(dma_tx_ref.reborrow(), + &tx_data[0..(txbuf.len()+1)]), HangIfReused); + } + } + + rxbuf.clear(); + rxcrc = CRC.digest(); + rx_expected_bytes = ModbusFrameLength::NeedMoreData(3); + rx_received_bytes = 0; + } + } + + rx_char_prev = rx_char; }, Result::Err(e) => { info!("RX error {:?}", e); + + //FIXME wait for gap/idle of >=1.5 chars. + rxbuf.clear(); + rxcrc = CRC.digest(); + rx_expected_bytes = ModbusFrameLength::NeedMoreData(3); + rx_received_bytes = 0; }, } - rx_future = DontAbort::new(self.rx.read(&mut rx_buf), PanicIfReused); + rx_future = DontAbort::new(self.rx.read(&mut rx_buf_one), PanicIfReused); }, _ => { }, -- GitLab