├── .gitignore ├── scheme.png ├── Cargo.toml ├── LICENSE ├── README.md ├── memory.x ├── .cargo └── config └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | TODO.todo 2 | target 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /scheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alttch/stm32f1-modbus-example/HEAD/scheme.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Sergei S. "] 3 | edition = "2018" 4 | readme = "README.md" 5 | name = "app" 6 | version = "0.1.0" 7 | 8 | [dependencies] 9 | cortex-m = "^0.6.3" 10 | cortex-m-rt = "^0.6.12" 11 | embedded-hal = "^0.2.4" 12 | panic-halt = "^0.2.0" 13 | rmodbus = { version = "0.3.9", features = ["nostd", "smallcontext", "single"] } 14 | fixedvec = "0.2.4" 15 | stm32f1 = { version = "0.6.0", features = ["stm32f103", "rt"] } 16 | 17 | [dependencies.stm32f1xx-hal] 18 | features = ["stm32f103", "rt", "medium"] 19 | version = "^0.6.1" 20 | 21 | [[bin]] 22 | name = "app" 23 | test = false 24 | bench = false 25 | 26 | [profile.release] 27 | codegen-units = 1 28 | features = ["stm32f103", "rt", "medium"] 29 | lto = true 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Altertech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Modbus context example with stm32 (stm32f103c8t6) & RS485 2 | 3 | Very simple example of Modbus context on stm32 via RS485/RTU. 4 | 5 | ## Hardware 6 | 7 | * stm32f103c8t6 8 | 9 | * UART TTL to RS485 (MAX485) 10 | 11 | * ST-LINK/V2 (for flashing) 12 | 13 | * any USB RS485 whistle (for testing from PC) 14 | 15 | ## Wiring 16 | 17 | ![Connection scheme](scheme.png?raw=true "Connection scheme") 18 | 19 | ## Software 20 | 21 | * any Modbus client (e.g. https://github.com/favalex/modbus-cli for tests) 22 | 23 | ## Libraries 24 | 25 | * https://github.com/stm32-rs/stm32f1xx-hal/ for STM32F1 HAL 26 | 27 | * https://github.com/alttch/rmodbus for Modbus 28 | 29 | ## Flashing 30 | 31 | ```shell 32 | cargo install cargo-flash # if not installed yed 33 | cargo flash --chip stm32f103C8 --release 34 | ``` 35 | 36 | ## What can it do 37 | 38 | Just the demo. You can read/write Modbus context with any Modbus/RTU client, 39 | get/set any Modbus registers. 40 | 41 | Input registers 0 and 1 contain processed frame counter (big-endian u32). 42 | 43 | Rmodbus used with "smallcontext" feature, so only registers 0-999 (all types) 44 | are accessible. 45 | 46 | Enjoy! 47 | -------------------------------------------------------------------------------- /memory.x: -------------------------------------------------------------------------------- 1 | MEMORY 2 | { 3 | /* NOTE 1 K = 1 KiBi = 1024 bytes */ 4 | /* TODO Adjust these memory regions to match your device memory layout */ 5 | /* These values correspond to the LM3S6965, one of the few devices QEMU can emulate */ 6 | FLASH : ORIGIN = 0x08000000, LENGTH = 64K 7 | RAM : ORIGIN = 0x20000000, LENGTH = 20K 8 | } 9 | 10 | /* This is where the call stack will be allocated. */ 11 | /* The stack is of the full descending type. */ 12 | /* You may want to use this variable to locate the call stack and static 13 | variables in different memory regions. Below is shown the default value */ 14 | /* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */ 15 | 16 | /* You can use this symbol to customize the location of the .text section */ 17 | /* If omitted the .text section will be placed right after the .vector_table 18 | section */ 19 | /* This is required only on microcontrollers that store some configuration right 20 | after the vector table */ 21 | /* _stext = ORIGIN(FLASH) + 0x400; */ 22 | 23 | /* Example of putting non-initialized variables into custom RAM locations. */ 24 | /* This assumes you have defined a region RAM2 above, and in the Rust 25 | sources added the attribute `#[link_section = ".ram2bss"]` to the data 26 | you want to place there. */ 27 | /* Note that the section will not be zero-initialized by the runtime! */ 28 | /* SECTIONS { 29 | .ram2bss (NOLOAD) : ALIGN(4) { 30 | *(.ram2bss); 31 | . = ALIGN(4); 32 | } > RAM2 33 | } INSERT AFTER .bss; 34 | */ 35 | -------------------------------------------------------------------------------- /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.thumbv7m-none-eabi] 2 | #runner = "cargo flash --chip stm32f103C8 --release" 3 | 4 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 5 | # uncomment ONE of these three option to make `cargo run` start a GDB session 6 | # which option to pick depends on your system 7 | # runner = "arm-none-eabi-gdb -q -x openocd.gdb" 8 | # runner = "gdb-multiarch -q -x openocd.gdb" 9 | # runner = "gdb -q -x openocd.gdb" 10 | 11 | rustflags = [ 12 | # This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x 13 | # See https://github.com/rust-embedded/cortex-m-quickstart/pull/95 14 | "-C", "link-arg=--nmagic", 15 | 16 | # LLD (shipped with the Rust toolchain) is used as the default linker 17 | "-C", "link-arg=-Tlink.x", 18 | 19 | # if you run into problems with LLD switch to the GNU linker by commenting out 20 | # this line 21 | # "-C", "linker=arm-none-eabi-ld", 22 | 23 | # if you need to link to pre-compiled C libraries provided by a C toolchain 24 | # use GCC as the linker by commenting out both lines above and then 25 | # uncommenting the three lines below 26 | # "-C", "linker=arm-none-eabi-gcc", 27 | # "-C", "link-arg=-Wl,-Tlink.x", 28 | # "-C", "link-arg=-nostartfiles", 29 | ] 30 | 31 | [build] 32 | # Pick ONE of these compilation targets 33 | # target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+ 34 | target = "thumbv7m-none-eabi" # Cortex-M3 35 | # target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU) 36 | # target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU) 37 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | #[macro_use] 5 | extern crate fixedvec; 6 | use core::ptr; 7 | use core::sync::atomic::{self, Ordering}; 8 | 9 | use fixedvec::FixedVec; 10 | 11 | use cortex_m_rt::entry; 12 | use embedded_hal::digital::v2::OutputPin; 13 | #[allow(unused_imports)] 14 | use panic_halt; 15 | use stm32f1xx_hal::{ 16 | delay::Delay, 17 | pac, 18 | prelude::*, 19 | serial::{Config, Serial}, 20 | }; 21 | 22 | use rmodbus::server::{context, guess_frame_len, process_frame, ModbusFrame, ModbusProto}; 23 | 24 | #[entry] 25 | fn main() -> ! { 26 | let dp = pac::Peripherals::take().unwrap(); 27 | let cp = cortex_m::Peripherals::take().unwrap(); 28 | let mut rcc = dp.RCC.constrain(); 29 | let mut gpioc = dp.GPIOC.split(&mut rcc.apb2); 30 | let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); // will blink on frame rcv 31 | let mut flash = dp.FLASH.constrain(); 32 | let clocks = rcc.cfgr.sysclk(14.mhz()).freeze(&mut flash.acr); 33 | let mut delay = Delay::new(cp.SYST, clocks); 34 | 35 | let mut afio = dp.AFIO.constrain(&mut rcc.apb2); 36 | let mut gpiob = dp.GPIOB.split(&mut rcc.apb2); 37 | let tx = gpiob.pb10.into_alternate_push_pull(&mut gpiob.crh); 38 | let rx = gpiob.pb11; 39 | let mut direction = gpiob.pb12.into_push_pull_output(&mut gpiob.crh); // dir ctrl 4 half duplex 40 | let serial = Serial::usart3( 41 | // B10=DI, B11=R0, B12=DE+RE 42 | dp.USART3, 43 | (tx, rx), 44 | &mut afio.mapr, 45 | Config::default() 46 | .baudrate(9600.bps()) 47 | .parity_none() 48 | .stopbits(stm32f1xx_hal::serial::StopBits::STOP1), 49 | clocks, 50 | &mut rcc.apb1, 51 | ); 52 | let (mut _tx, mut _rx) = serial.split(); 53 | let channels = dp.DMA1.split(&mut rcc.ahb); 54 | let mut rx = _rx.with_dma(channels.3); 55 | let mut tx = _tx.with_dma(channels.2); 56 | direction.set_low().ok(); 57 | // pre-setup DMA channels 58 | // working with DMA directly because stm32f1xx-hal read/write with DMA are limited to args 59 | rx.channel.ch().cr.modify(|_, w| { 60 | w.mem2mem() 61 | .clear_bit() 62 | .pl() 63 | .medium() 64 | .msize() 65 | .bits8() 66 | .psize() 67 | .bits8() 68 | .circ() 69 | .clear_bit() 70 | .dir() 71 | .clear_bit() 72 | }); 73 | rx.channel.set_peripheral_address( 74 | unsafe { &(*pac::USART3::ptr()).dr as *const _ as u32 }, 75 | false, 76 | ); 77 | tx.channel.ch().cr.modify(|_, w| { 78 | w.mem2mem() 79 | .clear_bit() 80 | .pl() 81 | .medium() 82 | .msize() 83 | .bits8() 84 | .psize() 85 | .bits8() 86 | .circ() 87 | .clear_bit() 88 | .dir() 89 | .set_bit() 90 | }); 91 | tx.channel.set_peripheral_address( 92 | unsafe { &(*pac::USART3::ptr()).dr as *const _ as u32 }, 93 | false, 94 | ); 95 | let mut c: u32 = 0; // frame calc 96 | loop { 97 | macro_rules! exec_channel { 98 | ($ch:expr, $obj:expr, $len:expr, $nto:expr) => { 99 | $ch.set_memory_address($obj.as_ptr() as u32, true); 100 | $ch.set_transfer_length($len); 101 | atomic::compiler_fence(Ordering::Release); 102 | $ch.start(); 103 | let mut to = 0; 104 | let timeout = $len * 2; 105 | while $ch.in_progress() { 106 | delay.delay_ms(1_u16); 107 | if $nto { 108 | let remaining = $ch.get_ndtr() as usize; 109 | if remaining < $len { 110 | to = to + 1; 111 | if to > timeout { 112 | break; 113 | } 114 | } 115 | } 116 | } 117 | atomic::compiler_fence(Ordering::Acquire); 118 | $ch.stop(); 119 | unsafe { 120 | ptr::read_volatile(&0); 121 | } 122 | atomic::compiler_fence(Ordering::Acquire); 123 | }; 124 | } 125 | led.set_high().unwrap(); 126 | let frame: ModbusFrame = [0; 256]; 127 | exec_channel!(rx.channel, frame, 8, true); 128 | let len = guess_frame_len(&frame, ModbusProto::Rtu).unwrap(); 129 | if len > 8 { 130 | exec_channel!(rx.channel, frame[8..], (len - 8) as usize, true); 131 | } 132 | led.set_low().unwrap(); 133 | c = c + 1; 134 | if c == u32::MAX { 135 | c = 0; 136 | } 137 | context::input_set_u32(0, c).unwrap(); // input registers 0-1 = frame calc (u32 big endian) 138 | let mut preallocated_space = alloc_stack!([u8; 256]); 139 | let mut response: FixedVec = FixedVec::new(&mut preallocated_space); 140 | if process_frame(1, &frame, ModbusProto::Rtu, &mut response).is_ok() && !response.is_empty() 141 | { 142 | direction.set_high().ok(); 143 | exec_channel!(tx.channel, response.as_slice(), response.len(), false); 144 | delay.delay_ms(response.len() as u16); // make sure frame is sent before switching dir 145 | direction.set_low().ok(); 146 | } 147 | } 148 | } 149 | --------------------------------------------------------------------------------