├── .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 | 
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 |
--------------------------------------------------------------------------------