Skip to content

Commit b9a3901

Browse files
Merge pull request brainelectronics#75 from wpyoga/fix-timing
Fix timing issues with UART communication
2 parents fa6430c + b5416a9 commit b9a3901

File tree

3 files changed

+74
-36
lines changed

3 files changed

+74
-36
lines changed

changelog.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
<!-- ## [Unreleased] -->
1616

1717
## Released
18+
## [2.3.5] - 2023-07-01
19+
### Fixed
20+
- Time between RS485 control pin raise and UART transmission reduced by 80% from 1000us to 200us
21+
- The RS485 control pin is lowered as fast as possible by using `time.sleep_us()` instead of `machine.idle()` which uses an IRQ on the order of milliseconds. This kept the control pin active longer than necessary, causing the response message to be missed at higher baud rates. This applies only to MicroPython firmwares below v1.20.0
22+
- The following fixes were provided by @wpyoga
23+
- RS485 control pin handling fixed by using UART `flush` function, see #68
24+
- Invalid CRC while reading multiple coils and fixed, see #50 and #52
25+
1826
## [2.3.4] - 2023-03-20
1927
### Added
2028
- `package.json` for `mip` installation with MicroPython v1.19.1 or newer
@@ -282,8 +290,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
282290
- PEP8 style issues on all files of [`lib/uModbus`](lib/uModbus)
283291

284292
<!-- Links -->
285-
[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.4...develop
293+
[Unreleased]: https://github.com/brainelectronics/micropython-modbus/compare/2.3.5...develop
286294

295+
[2.3.5]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.5
287296
[2.3.4]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.4
288297
[2.3.3]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.3
289298
[2.3.2]: https://github.com/brainelectronics/micropython-modbus/tree/2.3.2

fakes/machine.py

+4
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,9 @@ def sendbreak(self) -> None:
425425
"""Send a break condition on the bus"""
426426
raise MachineError('Not yet implemented')
427427

428+
'''
429+
# flush introduced in MicroPython v1.20.0
430+
# use manual timing calculation for testing
428431
def flush(self) -> None:
429432
"""
430433
Waits until all data has been sent
@@ -434,6 +437,7 @@ def flush(self) -> None:
434437
Only available with newer versions than 1.19
435438
"""
436439
raise MachineError('Not yet implemented')
440+
'''
437441

438442
def txdone(self) -> bool:
439443
"""

umodbus/serial.py

+60-35
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from machine import Pin
1414
import struct
1515
import time
16-
import machine
1716

1817
# custom packages
1918
from . import const as Const
@@ -96,6 +95,8 @@ def __init__(self,
9695
:param ctrl_pin: The control pin
9796
:type ctrl_pin: int
9897
"""
98+
# UART flush function is introduced in Micropython v1.20.0
99+
self._has_uart_flush = callable(getattr(UART, "flush", None))
99100
self._uart = UART(uart_id,
100101
baudrate=baudrate,
101102
bits=data_bits,
@@ -112,12 +113,16 @@ def __init__(self,
112113
else:
113114
self._ctrlPin = None
114115

116+
# timing of 1 character in microseconds (us)
115117
self._t1char = (1000000 * (data_bits + stop_bits + 2)) // baudrate
118+
119+
# inter-frame delay in microseconds (us)
120+
# - <= 19200 bps: 3.5x timing of 1 character
121+
# - > 19200 bps: 1750 us
116122
if baudrate <= 19200:
117-
# 4010us (approx. 4ms) @ 9600 baud
118-
self._t35chars = (3500000 * (data_bits + stop_bits + 2)) // baudrate
123+
self._inter_frame_delay = (self._t1char * 3500) // 1000
119124
else:
120-
self._t35chars = 1750 # 1750us (approx. 1.75ms)
125+
self._inter_frame_delay = 1750
121126

122127
def _calculate_crc16(self, data: bytearray) -> bytes:
123128
"""
@@ -143,31 +148,35 @@ def _exit_read(self, response: bytearray) -> bool:
143148
:param response: The response
144149
:type response: bytearray
145150
146-
:returns: State of basic read response evaluation
151+
:returns: State of basic read response evaluation,
152+
True if entire response has been read
147153
:rtype: bool
148154
"""
149-
if response[1] >= Const.ERROR_BIAS:
150-
if len(response) < Const.ERROR_RESP_LEN:
155+
response_len = len(response)
156+
if response_len >= 2 and response[1] >= Const.ERROR_BIAS:
157+
if response_len < Const.ERROR_RESP_LEN:
151158
return False
152-
elif (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER):
159+
elif response_len >= 3 and (Const.READ_COILS <= response[1] <= Const.READ_INPUT_REGISTER):
153160
expected_len = Const.RESPONSE_HDR_LENGTH + 1 + response[2] + Const.CRC_LENGTH
154-
if len(response) < expected_len:
161+
if response_len < expected_len:
155162
return False
156-
elif len(response) < Const.FIXED_RESP_LEN:
163+
elif response_len < Const.FIXED_RESP_LEN:
157164
return False
158165

159166
return True
160167

161168
def _uart_read(self) -> bytearray:
162169
"""
163-
Read up to 40 bytes from UART
170+
Read incoming slave response from UART
164171
165172
:returns: Read content
166173
:rtype: bytearray
167174
"""
168175
response = bytearray()
169176

170-
for x in range(1, 40):
177+
# TODO: use some kind of hint or user-configurable delay
178+
# to determine this loop counter
179+
for x in range(1, 120):
171180
if self._uart.any():
172181
# WiPy only
173182
# response.extend(self._uart.readall())
@@ -178,7 +187,7 @@ def _uart_read(self) -> bytearray:
178187
break
179188

180189
# wait for the maximum time between two frames
181-
time.sleep_us(self._t35chars)
190+
time.sleep_us(self._inter_frame_delay)
182191

183192
return response
184193

@@ -194,10 +203,9 @@ def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray:
194203
"""
195204
received_bytes = bytearray()
196205

197-
# set timeout to at least twice the time between two frames in case the
198-
# timeout was set to zero or None
206+
# set default timeout to at twice the inter-frame delay
199207
if timeout == 0 or timeout is None:
200-
timeout = 2 * self._t35chars # in milliseconds
208+
timeout = 2 * self._inter_frame_delay # in microseconds
201209

202210
start_us = time.ticks_us()
203211

@@ -210,13 +218,13 @@ def _uart_read_frame(self, timeout: Optional[int] = None) -> bytearray:
210218

211219
# do not stop reading and appending the result to the buffer
212220
# until the time between two frames elapsed
213-
while time.ticks_diff(time.ticks_us(), last_byte_ts) <= self._t35chars:
221+
while time.ticks_diff(time.ticks_us(), last_byte_ts) <= self._inter_frame_delay:
214222
# WiPy only
215223
# r = self._uart.readall()
216224
r = self._uart.read()
217225

218226
# if something has been read after the first iteration of
219-
# this inner while loop (during self._t35chars time)
227+
# this inner while loop (within self._inter_frame_delay)
220228
if r is not None:
221229
# append the new read stuff to the buffer
222230
received_bytes.extend(r)
@@ -235,32 +243,49 @@ def _send(self, modbus_pdu: bytes, slave_addr: int) -> None:
235243
"""
236244
Send Modbus frame via UART
237245
238-
If a flow control pin has been setup, it will be controller accordingly
246+
If a flow control pin has been setup, it will be controlled accordingly
239247
240248
:param modbus_pdu: The modbus Protocol Data Unit
241249
:type modbus_pdu: bytes
242250
:param slave_addr: The slave address
243251
:type slave_addr: int
244252
"""
245-
serial_pdu = bytearray()
246-
serial_pdu.append(slave_addr)
247-
serial_pdu.extend(modbus_pdu)
248-
249-
crc = self._calculate_crc16(serial_pdu)
250-
serial_pdu.extend(crc)
253+
# modbus_adu: Modbus Application Data Unit
254+
# consists of the Modbus PDU, with slave address prepended and checksum appended
255+
modbus_adu = bytearray()
256+
modbus_adu.append(slave_addr)
257+
modbus_adu.extend(modbus_pdu)
258+
modbus_adu.extend(self._calculate_crc16(modbus_adu))
251259

252260
if self._ctrlPin:
253-
self._ctrlPin(1)
254-
time.sleep_us(1000) # wait until the control pin really changed
255-
send_start_time = time.ticks_us()
256-
257-
self._uart.write(serial_pdu)
261+
self._ctrlPin.on()
262+
# wait until the control pin really changed
263+
# 85-95us (ESP32 @ 160/240MHz)
264+
time.sleep_us(200)
265+
266+
# the timing of this part is critical:
267+
# - if we disable output too early,
268+
# the command will not be received in full
269+
# - if we disable output too late,
270+
# the incoming response will lose some data at the beginning
271+
# easiest to just wait for the bytes to be sent out on the wire
272+
273+
send_start_time = time.ticks_us()
274+
# 360-400us @ 9600-115200 baud (measured) (ESP32 @ 160/240MHz)
275+
self._uart.write(modbus_adu)
276+
send_finish_time = time.ticks_us()
277+
if self._has_uart_flush:
278+
self._uart.flush()
279+
else:
280+
sleep_time_us = (
281+
self._t1char * len(modbus_adu) - # total frame time in us
282+
time.ticks_diff(send_finish_time, send_start_time) +
283+
100 # only required at baudrates above 57600, but hey 100us
284+
)
285+
time.sleep_us(sleep_time_us)
258286

259287
if self._ctrlPin:
260-
total_frame_time_us = self._t1char * len(serial_pdu)
261-
while time.ticks_us() <= send_start_time + total_frame_time_us:
262-
machine.idle()
263-
self._ctrlPin(0)
288+
self._ctrlPin.off()
264289

265290
def _send_receive(self,
266291
modbus_pdu: bytes,
@@ -279,7 +304,7 @@ def _send_receive(self,
279304
:returns: Validated response content
280305
:rtype: bytes
281306
"""
282-
# flush the Rx FIFO
307+
# flush the Rx FIFO buffer
283308
self._uart.read()
284309

285310
self._send(modbus_pdu=modbus_pdu, slave_addr=slave_addr)

0 commit comments

Comments
 (0)