peterhinch / micropython-async

Application of uasyncio to hardware interfaces. Tutorial and code.
MIT License
744 stars 169 forks source link

auart_hd issue #85

Closed skylin008 closed 2 years ago

skylin008 commented 2 years ago

Thank you peterhinch to shared this impressive project. I want to modified auart_hd to fit the modbus bus driver. I found used the async mode, the function response so slow.As the code follow. How to solve the slow issue. Thanks! `

The Serial's send_command() method sends a command and waits for a number of

lines from the device. The end of the process is signified by a timeout, when

a list of lines is returned. This allows line-by-line processing.

A special test mode demonstrates the behaviour with a non-responding device. If

None is passed, no commend is sent. The master waits for a response which never

arrives and returns an empty list.

import logging from machine import UART from machine import Pin import uasyncio as asyncio import aswitch

import functions as functions import const as Const import struct

logging.level_name(level = logging.INFO) log = logging.Logger('modbus_driver')

class Serial(): def init(self, uart_no, ctrl_pin = None, stime = 3, debug = False): self.uart = UART(uart_no, 19200) if ctrl_pin is not None: self._ctrlPin = Pin(ctrl_pin, mode = Pin.OUT) # Send data , ctrl_pin must be High else: self._ctrlPin = None self._timeout = stime self._swriter = asyncio.StreamWriter(self.uart, {}) self._sreader = asyncio.StreamReader(self.uart) self._delay = aswitch.Delay_ms() self._response = [] self._debug = debug

    loop = asyncio.get_event_loop()
    loop.create_task(self._recv())

async def _recv(self):
    while True:
        self._ctrlPin(0)
        res = await self._sreader.read(9)            
        self._response.append(res)  # Append to list of lines
        self._ctrlPin(1)
        self._delay.trigger(self._timeout)  # Got something, retrigger timer

async def send_command(self, command):
    self._response = []  # Discard any pending messages
    if command is None:
        print('Timeout test.')
    else:
        self._ctrlPin(1)
        await self._swriter.awrite(command)
        await self._swriter.drain()
        if self._debug:            
            log.info('Command sent:', command)
        self._ctrlPin(0)
    self._delay.trigger(self._timeout)  # Re-initialise timer
    while self._delay.running():
        await asyncio.sleep_ms(self._timeout)  # Wait for 5ms after last msg received        
    return self._response

@micropython.native
def _calculate_crc16(self, data):
    crc = 0xFFFF
    for char in data:
        crc = (crc >> 8) ^ Const.CRC16_TABLE[((crc) ^ char) & 0xFF]

    return struct.pack('<H',crc)

@micropython.native
def _to_short(self, byte_array, signed=True):
    response_quantity = int(len(byte_array) / 2)
    fmt = '>' + (('h' if signed else 'H') * response_quantity)

    return struct.unpack(fmt, byte_array)

@micropython.native
def _validate_resp_hdr(self, response, slave_addr, function_code, count):

    if len(response) == 0:
        #raise OSError('no data received from slave')
        return None

    resp_crc = response[-Const.CRC_LENGTH:]
    expected_crc = self._calculate_crc16(response[0:len(response) - Const.CRC_LENGTH])
    if (resp_crc[0] != expected_crc[0]) or (resp_crc[1] != expected_crc[1]):
        #raise OSError('invalid response CRC')
        return None

    if (response[0] != slave_addr):
        #raise ValueError('wrong slave address')
        return None

    if (response[1] == (function_code + Const.ERROR_BIAS)):
        #raise ValueError('slave returned exception code: {:d}'.format(response[2]))
        return None

    hdr_length = (Const.RESPONSE_HDR_LENGTH + 1) if count else Const.RESPONSE_HDR_LENGTH

    return response[hdr_length : len(response) - Const.CRC_LENGTH]

async def read_holding_registers(self, slave_addr, starting_addr, register_qty, signed = False):

    modbus_pdu = functions.read_holding_registers(starting_addr, register_qty)

    serial_pdu = bytearray()
    serial_pdu.append(slave_addr)
    serial_pdu.extend(modbus_pdu)

    crc = self._calculate_crc16(serial_pdu)
    serial_pdu.extend(crc)

    response = await self.send_command(serial_pdu)

    if len(response):
        b = bytearray()
        for d in response:
            b.extend(d)            
        resp_data = self._validate_resp_hdr(b, slave_addr, modbus_pdu[0], True)

        if resp_data is not None:
            register_value = self._to_short(resp_data, signed)

            return register_value
        else:
            return None

if name == 'main':

async def write():
    buf = [ ]
    modbus = Serial(2, ctrl_pin = '485_TR1')        
    while True:
        import utime
        old = utime.ticks_ms()
        for i in range(1,7):               
            res = await modbus.read_holding_registers(i, 0x94, 0x02, signed = False)                
            if res is not None:
                log.info(res)
        delta = utime.ticks_diff(utime.ticks_ms(), old)                
        print("delta_time:",delta)
        await asyncio.sleep_ms(10)

async def main():
    asyncio.create_task(write())
    while True:
        await asyncio.sleep_ms(10)

def test():
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print('Interrrupt')

    finally:
        asyncio.new_event_loop()
        print('run again')

test()

`

peterhinch commented 2 years ago

I'm really not in a position to do a full analysis of this substantial chunk of code. I would use one of three approaches.

  1. Try to produce a minimal code sample - a stripped down version of the application which exhibits the problem. This often makes the cause apparent.
  2. Put in print statements at various points in the code which output timing information.
  3. If you have a logic analyser: the approach I would actually use is to employ my monitor application to identify where time is being spent.

As a general comment your approach looks good. One thing that did stand out is

    while self._delay.running():
        await asyncio.sleep_ms(self._timeout)  # Wait for 5ms after last msg received        

A delay of 3ms (according to my reading of the code) is unlikely actually to happen in uasyncio because of competition from other tasks. In any event it's an inefficient way to wait on a timer. Try:

    await self._delay.wait()

Internally this waits on an Event. During the waiting period there is no load on the scheduler.

By the same token main() can be simplified:

async def main():
    await write()

I don't think either of these tweaks will fix the issue, but I would review any instances of short sleeps in the light of possible competition from other tasks.

Good luck.