diff --git a/.github/actions/setup_esp32/action.yml b/.github/actions/setup_esp32/action.yml new file mode 100644 index 0000000000000..42c44cf76ce2e --- /dev/null +++ b/.github/actions/setup_esp32/action.yml @@ -0,0 +1,47 @@ +name: Setup ESP-IDF for CI +description: Install ESP-IDF +inputs: + idf_ver: + required: true + type: string + ccache_key: + required: true + type: string + +runs: + using: "composite" + + steps: + - id: python_ver + name: Read the Python version + run: echo PYTHON_VER=py$(python --version | cut -d' ' -f2) | tee "${GITHUB_OUTPUT}" + shell: bash + + - name: Cached ESP-IDF install + id: cache_esp_idf + uses: actions/cache@v5 + with: + path: | + ./esp-idf/ + ~/.espressif/ + !~/.espressif/dist/ + ~/.cache/pip/ + # Cache is keyed on both IDF version (from the job) and Python version (from the runner) + key: esp-idf-${{ inputs.idf_ver }}-${{ steps.python_ver.outputs.PYTHON_VER }} + + - name: Install ESP-IDF packages + if: steps.cache_esp_idf.outputs.cache-hit != 'true' + env: + IDF_VER: ${{ inputs.idf_ver }} + run: tools/ci.sh esp32_idf_setup + shell: bash + + - name: ccache + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: esp32-${{ inputs.idf_ver }}-${{ inputs.ccache_key }} + + - name: Enable CCache for ESP-IDF + run: echo "IDF_CCACHE_ENABLE=1" >> ${GITHUB_ENV} + shell: bash + diff --git a/.github/workflows/code_size.yml b/.github/workflows/code_size.yml index 42e7183c4f410..f3238a85d9acf 100644 --- a/.github/workflows/code_size.yml +++ b/.github/workflows/code_size.yml @@ -32,28 +32,17 @@ jobs: - name: Install packages run: tools/ci.sh code_size_setup - # Needs to be kept in synch with ports_esp32.yml - - id: idf_ver - name: Read the ESP-IDF version (including Python version) and set outputs.IDF_VER - run: tools/ci.sh esp32_idf_ver | tee "${GITHUB_OUTPUT}" - - name: Cached ESP-IDF install - id: cache_esp_idf - uses: actions/cache@v5 - with: - path: | - ./esp-idf/ - ~/.espressif/ - !~/.espressif/dist/ - ~/.cache/pip/ - key: esp-idf-${{ steps.idf_ver.outputs.IDF_VER }} - - name: Install ESP-IDF packages - if: steps.cache_esp_idf.outputs.cache-hit != 'true' - run: tools/ci.sh esp32_idf_setup + - name: Find IDF_NEWEST_VER + id: idf_ver + run: | + echo "IDF_VER="$(yq .env.IDF_NEWEST_VER < .github/workflows/ports_esp32.yml) \ + | tee "${GITHUB_OUTPUT}" - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 + - name: Setup ESP-IDF + uses: ./.github/actions/setup_esp32 with: - key: code_size + idf_ver: ${{ steps.idf_ver.outputs.IDF_VER }} + ccache_key: code_size - name: Build run: tools/ci.sh code_size_build diff --git a/.github/workflows/ports_esp32.yml b/.github/workflows/ports_esp32.yml index 87ab6dbe35543..56dce3b69d989 100644 --- a/.github/workflows/ports_esp32.yml +++ b/.github/workflows/ports_esp32.yml @@ -12,54 +12,52 @@ on: - 'lib/**' - 'drivers/**' - 'ports/esp32/**' - schedule: - # Scheduled run exists to keep master branch ESP-IDF cache entry hot - # and prevent creating many redundant per-branch cache entries instead. - - cron: "20 0 * * *" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + # Oldest and newest supported ESP-IDF versions, should match ports/esp32/README.md + IDF_OLDEST_VER: &oldest "v5.3" + IDF_NEWEST_VER: &newest "v5.5.1" + jobs: build_idf: strategy: fail-fast: false matrix: + idf_ver: + - *oldest + - *newest ci_func: # names are functions in ci.sh - esp32_build_cmod_spiram_s2 - esp32_build_s3_c3 - esp32_build_c2_c5_c6 - esp32_build_p4 + exclude: + # Exclude some jobs on the oldest IDF version, to save resources + - idf_ver: *oldest + ci_func: esp32_build_c2_c5_c6 + - idf_ver: *oldest + ci_func: esp32_build_p4 runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - # Needs to be kept in synch with code_size.yml - - id: idf_ver - name: Read the ESP-IDF version (including Python version) and set outputs.IDF_VER - run: tools/ci.sh esp32_idf_ver | tee "${GITHUB_OUTPUT}" + # Only the newest IDF version will build the ESP-IDF lockfiles correctly, + # so we need to disable MICROPY_MAINTAINER_BUILD on older versions. + - name: Disable extra checks for older ESP-IDF + id: check_newest_ver + if: ${{ matrix.idf_ver != env.IDF_NEWEST_VER }} + run: echo "MICROPY_MAINTAINER_BUILD=0" >> ${GITHUB_ENV} - - name: Cached ESP-IDF install - id: cache_esp_idf - uses: actions/cache@v5 + - name: Setup ESP-IDF + uses: ./.github/actions/setup_esp32 with: - path: | - ./esp-idf/ - ~/.espressif/ - !~/.espressif/dist/ - ~/.cache/pip/ - key: esp-idf-${{ steps.idf_ver.outputs.IDF_VER }} - - - name: Install ESP-IDF packages - if: steps.cache_esp_idf.outputs.cache-hit != 'true' - run: tools/ci.sh esp32_idf_setup + idf_ver: ${{ matrix.idf_ver }} + ccache_key: ${{ matrix.ci_func }} - # Needs to be kept in synch with code_size.yml - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 - with: - key: esp32-${{ matrix.ci_func }} - - name: Build ci_${{matrix.ci_func }} + - name: Build ci_${{matrix.ci_func }} on ESP-IDF ${{ matrix.idf_ver }} run: tools/ci.sh ${{ matrix.ci_func }} diff --git a/.github/workflows/ports_zephyr.yml b/.github/workflows/ports_zephyr.yml index 571a443e903f8..330121d1de648 100644 --- a/.github/workflows/ports_zephyr.yml +++ b/.github/workflows/ports_zephyr.yml @@ -12,10 +12,6 @@ on: - 'lib/**' - 'ports/zephyr/**' - 'tests/**' - schedule: - # Scheduled run exists to keep master branch Zephyr cache entry hot - # and prevent creating many redundant per-branch cache entries instead. - - cron: "40 4 * * *" concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/LICENSE b/LICENSE index 929a2e97de7bf..28b5239e5fe59 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2025 Damien P. George +Copyright (c) 2013-2026 Damien P. George Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/conf.py b/docs/conf.py index 603543aa18c7e..f80ca97edcaf9 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -72,7 +72,7 @@ # General information about the project. project = "MicroPython" -copyright = "- The MicroPython Documentation is Copyright © 2014-2025, " + micropy_authors +copyright = "- The MicroPython Documentation is Copyright © 2014-2026, " + micropy_authors # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/library/index.rst b/docs/library/index.rst index 0459814527692..fca9cbb53713f 100644 --- a/docs/library/index.rst +++ b/docs/library/index.rst @@ -82,6 +82,7 @@ library. struct.rst sys.rst time.rst + weakref.rst zlib.rst _thread.rst diff --git a/docs/library/machine.CAN.rst b/docs/library/machine.CAN.rst new file mode 100644 index 0000000000000..0da26b90167d6 --- /dev/null +++ b/docs/library/machine.CAN.rst @@ -0,0 +1,614 @@ +.. currentmodule:: machine +.. _machine.CAN: + +class CAN -- Controller Area Network protocol +============================================= + +CAN is a two-wire serial protocol used for reliable real-time message delivery +between one or more nodes connected to a common bus. CAN 2.0 was standardised in +ISO-11898, and is now also known as CAN Classic. + +There is also a newer, backwards compatible, protocol named CAN FD (CAN with +Flexible Data-Rate). *The machine.CAN driver does not currently support CAN FD +features, use `pyb.CAN` on stm32 if you need CAN FD*. + +CAN support requires a controller (often an internal microcontroller +peripheral), and an external transceiver to level-shift the signals onto the CAN +bus. + +The ``machine.CAN`` interface is a *low level basic* CAN messaging interface +that abstracts a CAN controller as an outgoing priority queue for sending +messages, an incoming queue for receiving messages, and mechanisms for reporting +errors. + +.. note:: The planned ``can`` and ``aiocan`` micropython-lib modules will be the + recommended way to use CAN with MicroPython. + +Constructor +----------- + +.. class:: CAN(id, *args, **kwargs) + + Construct a CAN controller object of the given id: + + - ``id`` identifies a particular CAN controller object; it is board and port specific. + - All other arguments are passed to :func:`CAN.init`. At least one argument (``bitrate``) + must be provided. + + Future versions of this class may also accept port-specific keyword arguments + here which configure the hardware. Currently no such keyword arguments are + implemented. + + Example + ^^^^^^^ + + Construct and initialise CAN controller 1 with bitrate 500kbps:: + + from machine import CAN + can = CAN(1, 500_000) + +.. Add a table of port-specific keyword arguments here, once they exist + +Methods +------- + +.. method:: CAN.init(bitrate, mode=CAN.MODE_NORMAL, sample_point=75, sjw=1, tseg1=None, tseg2=None) + + Initialise the CAN bus with the given parameters: + + - *bitrate* is the desired bus bit rate in bits per second. + - *mode* is one of the values shown under `can-modes`, indicating the + desired mode of operation. The default is "normal" operation on the bus. + + The next parameters are optional and relate to CAN bit timings. In most cases + you can leave these parameters set to the default values: + + - *sample_point* is an integer percentage of the data bit time. It + specifies the position of the bit sample with respect to the whole nominal + bit time. The CAN driver will calculate parameters accordingly. This + parameter is ignored if *tseg1* and *tseg2* are set. + - *sjw* is the resynchronisation jump width in units of time quanta for + nominal bits; it can be a value between 1 and 4 inclusive for classic CAN. + - *tseg1* defines the location of the sample point in units of time quanta + for nominal bits; it can be a value between 1 and 16 inclusive for classic + CAN. This is the sum of the ``Prop_Seg`` and ``Phase_Seg1`` phases as + defined in the ISO-11898 standard. If this value is set then *tseg2* + must also be set and *sample_point* is ignored. + - *tseg2* defines the location of the transmit point in units of the time + quanta for nominal bits; it can be a value between 1 and 8 inclusive for + classic CAN. This corresponds to ``Phase_Seg2`` in the ISO-11898 standard. + If this value is set then *tseg1* must also be set. + + If these arguments are specified then the CAN controller is configured + correctly for the desired *bitrate* and the specified total number of time + quanta per bit. The *tseg1* and *tseg2* values override the + *sample_point* argument if all of these are supplied. + + .. note:: Individual controller hardware may have additional restrictions on + valid values for these parameters, and will raise a ``ValueError`` + if a given value is not supported. + + .. note:: Specific controller hardware may accept additional optional + keyword parameters for hardware-specific features such as oversampling. + +.. method:: CAN.set_filters(filters) + + Set receive filters in the CAN controller. *filters* can be: + + - ``None`` to accept all incoming messages, or + - ``[]`` or ``()`` to disable all message receiving, or + - An iterable of one or more items defining the filter criteria. Each item + should be a tuple or list with three elements: + + - ``identifier`` is a CAN identifier (int). + - ``bit_mask`` is a bit mask for bits in the CAN identifier field (int). + - ``flags`` is an integer with zero or more of the bits defined in + `can-flags` set. This specifies properties that the incoming message needs + to match. Not all controllers support filtering on all flags, a + ``ValueError`` is raised if an unsupported flag is requested. + + Incoming messages are accepted if the bits masked in ``bit_mask`` match between + the message identifier and the filter ``identifier`` value, and flags set in the + filter match the incoming message. + + If the `CAN.FLAG_EXT_ID` bit is set in flags, the filter matches Extended + CAN IDs only. If the `CAN.FLAG_EXT_ID` bit is not set, the filter matches + Standard CAN IDs only. + + All filters are ORed together in the controller. Passing an empty list or + tuple for the filters argument means that no messages will be received. + + Some CAN controllers require each filter to be associated with only one + receive FIFO. In these cases, the filter items in the argument are allocated + round-robin to the available FIFOs. This driver does not distinguish between + FIFOs in the receive IRQ. + + .. note:: If the caller passes an iterable with more items than + :data:`CAN.FILTERS_MAX`, ``ValueError`` will be raised. + + .. note:: If either the ``identifier`` or the ``bit_mask`` is out of range + for the specified ID type, a ``ValueError`` with reason "invalid id" + will be raised. + + Examples + ^^^^^^^^ + + Receive all incoming messages:: + + can.set_filters(None) + + Receive messages with Standard ID values 0x301 and 0x700 only:: + + can.set_filters(((0x301, 0x7FF, 0), + (0x700, 0x7FF, 0))) + + Receive messages with Standard ID values in range 0x300-0x3FF, and Extended + ID value 0x50700 only:: + + can.set_filters(((0x300, 0x700, 0), + (0x50700, 0x1FFF_FFFF, CAN.FLAG_EXT_ID))) + +.. data:: CAN.FILTERS_MAX + + Constant value that reads the maximum number of supported receive filters + for this hardware controller. + + Note that some controllers may have more complex hardware restrictions on the + number of filters in use (for example, counting Standard and Extended ID + filters independently.) In these cases `CAN.set_filters` may raise a + ``ValueError`` even when the ``FILTERS_MAX`` limit is not exceeded. + +.. method:: CAN.send(id, data, flags=0) + + Copy a new CAN message into the controller's hardware transmit queue to be + sent onto the bus. The transmit queue is a priority queue sorted on CAN + identifier priority (lower numeric identifiers have higher priority). + + - *id* is an integer CAN identifier value. + - *data* is a bytes object (or similar) containing the CAN message data, + or describing a Remote Transmission Request (see below). + - *flags* is an integer with zero or more of the bits defined in + `can-flags` set, specifying properties of the outgoing CAN message + (Extended ID, Remote Transmission Request, etc.) + + If the message is successfully queued for transmit onto the bus, the function + returns an integer in the range ``0`` to `CAN.TX_QUEUE_LEN` (exclusive). This + value is the transmit buffer index where the message is queued to send, and + can be used by the `CAN.cancel_send` function and in `CAN.IRQ_TX` events. + + If the queue is full then the send will fail and ``None`` is returned. + + The send can also fail and return ``None`` if the provided *id* value has + equal priority to an existing message in the transmit queue and the CAN + controller hardware cannot guarantee that messages with the same ID will be + sent onto the bus in the same order they were added to the queue. To queue + the message anyway, pass the value :data:`CAN.FLAG_UNORDERED` flag in + the *flags* argument. This flag indicates that it's OK to send messages with + the same CAN ID onto the bus in any order. + + If the controller is in the "Bus Off" error state or disabled then calling + this function will raise an ``OSError``. + + .. note:: This intentionally low-level implementation is designed so the + caller can establish a software queue of outgoing messages. + + .. important:: The CAN "transmit queue" is not a FIFO queue, it is priority + ordered, and although it can hold up to `CAN.TX_QUEUE_LEN` + items there may be other hardware restrictions on messages + which can be queued at the same time. + + Remote Transmission Requests + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + If the bit `CAN.FLAG_RTR` is set in the *flags* argument then the controller + will send a Remote Transmission Request instead of a message. In this case + the contents of the *data* argument is ignored. The controller will send + a request where the ``DLC`` length field is equal to the length of the + *data* argument. + + Examples + ^^^^^^^^ + + Attempt to send a message with three byte payload ``0a0b0c`` and Standard ID 0x200:: + + can.send(0x200, b"\x0a\x0b\x0c", 0) + + Attempt to send a message with an empty payload and Extended ID 0x180008. + Indicate that the controller can send messages with this ID in any order, in + case other messages are already queued to send with the same ID:: + + can.send(0x180008, b"", can.FLAG_EXT_ID | can.FLAG_UNORDERED) + + Attempt to send a Remote Transmission Request with length 8 bytes and Standard ID 0x555:: + + can.send(0x555, b" " * 8, can.FLAG_RTR) + +.. method:: CAN.recv(arg=None) + + Return a CAN message that has been received by the controller, according to + filters set by :func:`CAN.set_filters`. + + This function takes a single optional argument, if provided then it must be a + list of at least 4 elements where the second element is a `memoryview` object + that refers to a `bytearray` or similar object that has enough capacity to hold + any received CAN message (8 bytes for CAN Classic, 64 bytes for CAN FD). The + provided list will be returned as a successful result, and avoids memory + allocation inside the function. + + If no messages have been received by the CAN controller, this function + returns ``None``. + + .. note:: `CAN.set_filters` must be called before any messages can be received by + the controller. To receive all messages, call ``set_filters(None)``. + + If a message has been received by the CAN controller, this function returns a + list with 4 elements: + + - Index 0 is the CAN ID of the received message, as an integer. + - Index 1 is a memoryview that provides access to the received message data. + + - If *arg* is not provided then this is a `memoryview` holding the bytes + which were received. This `memoryview` is backed by a newly allocated + `bytearray` large enough to hold any received CAN message. This allows + the result to be safely reused as a future *arg*, to save memory allocations. + - If *arg* is provided then the provided `memoryview` will be resized to + hold exactly the bytes which were received. The caller is responsible for + making sure the backing object for the `memoryview` can hold a CAN + message of any length. + + - Index 2 is an integer with zero or more of the bits defined in + `can-flags` set. It indicates metadata about the received message. + - Index 3 is an integer with zero or more of the bits defined in + `can-recv-errors` set. Any non-zero value indicates potential issues when + receiving CAN messages. These flags are reset inside the controller each + time this function returns. + + Remote Transmission Requests + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + If a Remote Transmission Request is received then the bit `CAN.FLAG_RTR` + will be set in Index 2 and the memoryview at Index 1 will contain all + zeroes, with a length equal to the ``DLC`` field of the received request. + + Example + ^^^^^^^ + :: + + can.set_filters(None) # receive all + while True: + res = can.recv() + if res: + can_id, data, flags, errs = res + print("Received", hex(can_id), data.hex(), hex(flags), hex(errs)) + else: + time.sleep_ms(1) # not a good pattern, use the irq instead! + + +.. method:: CAN.irq(handler=None, trigger=0, hard=False) + + Sets an interrupt *handler* function to be called when one or more of the + events flagged in *trigger* has occurred. + + - *handler* is a function to be called when the interrupt event + triggers. The handler must take exactly one argument which is the + :class:`CAN` instance. + + - *trigger* configures the event(s) which can generate an interrupt. + Possible values are a mask of one or more of the following: + + - `CAN.IRQ_RX` event occurs after the CAN controller has received + at least one message into its RX FIFO (meaning that :func:`CAN.recv()` + will return successfully). + - `CAN.IRQ_TX` event occurs after the CAN controller has either + successfully sent a message onto the CAN bus or failed to send a + message. This trigger has additional requirements for the handler, see + `machine_can_irq_flags` for details. + - `CAN.IRQ_STATE` event occurs when the CAN controller has transitioned + into a more severe error state. Call :func:`CAN.state()` to get the + updated state. + + - *hard* if True, a hard interrupt is used. This reduces the delay between + the CAN controller event and the handler being called. Hard interrupt + handlers may not allocate memory; see :ref:`isr_rules`. + + Returns an irq object. If called with no arguments then a previously-configured + irq object is returned. + + See `machine_can_irq_flags` for an example. + +.. method:: CAN.cancel_send(index) + + Request the CAN controller to cancel sending a message onto the bus. + + Argument *index* identifies a single transmit buffer. It should be an + integer in the range ``0`` to ``CAN.TX_QUEUE_LEN`` (exclusive). Generally + this will be a value previously returned by :func:`CAN.send()`. + + The result is ``True`` if a message was pending transmission in this buffer + and transmission was cancelled. + + The result is ``False`` otherwise (either no message was pending transmission + in this buffer, or transmission succeeded already). + + The IRQ event `CAN.IRQ_TX` should be used to determine if a message + was definitely sent or not, but note there are potential race conditions if a + transmission is cancelled and then the same buffer is used to send another + message (especially if the CAN controller IRQ is not "hard"). + +.. method:: CAN.state() + + Returns an integer value indicating the current state of the controller. + The value will be one of the values defined in `can-states`. + + Lower severity error states may automatically clear if the bus recovers, but + the `CAN.STATE_BUS_OFF` state can only be recovered by calling + :func:`CAN.restart()`. + +.. method:: CAN.get_counters(list=None /) + + Returns controller's error counter values. The result is a list of eight + values. If the optional *list* parameter is specified then the provided + list object is updated and returned as the result, to avoid an allocation. + + The list items are: + + - TEC (Transmit Error Counter) value + - REC (Receive Error Counter) value + - Number of times the controller entered the Warning state from the Active state. + - Number of times the controller entered the Error Passive state from the Warning state. + - Number of times the controller entered the Bus Off state from the Error Passive state. + - Total number of pending TX messages in the hardware queue. + - Total number of pending RX messages in the hardware queue. + - Number of times an RX overrun occurred. + + .. note:: Depending on the controller, these values may overflow back to 0 after + a certain value. + + .. note:: If a controller doesn't support a particular counter, it will return + ``None`` for that list element. + +.. method:: CAN.get_timings(list=None /) + + Returns a list of elements indicating the current timings configured in the + CAN controller. This can be used to verify timings for debugging purposes. + The result is a list of six values. If the optional *list* parameter is + specified then the provided list object is updated and returned as the + result, to avoid an allocation. + + The list items are: + + - Exact bitrate used by the controller. May vary from *bitrate* argument + passed to :func:`CAN.init()` due to quantisation to meet hardware + constraints. + - Resynchronisation jump width (SJW) in units of time quanta for nominal + bits. Has the same meaning as the *sjw* parameter of :func:`CAN.init()`. + - Location of the sample point in units of time quanta for nominal bits. Has + the same meaning as the *tseg1* parameter of :func:`CAN.init()`. + - Location of the transmit point in units of time quanta for nominal bits. + Has the same meaning as the *tseg2* parameter of :func:`CAN.init()`. + - CAN FD timing information. ``None`` for controllers which don't support CAN + FD, or if CAN FD is not initialised. Otherwise, a nested list of four + elements corresponding to the items above but applicable to the CAN FD BRS + feature. + - Optional controller-specific timing information. Depending on the + controller this will either be ``None`` if controller doesn't report any, + or it will be a constant length list whose elements are specific to a + particular hardware controller. + + .. note:: If :func:`CAN.init()` has not been called then this function + still returns a result, but the result depends on the controller + internals and may not be accurate. + +.. method:: CAN.restart() + + Causes the controller to exit `STATE_BUS_OFF` without clearing any other + internal state. Also clears some of the error counters (always the number + of times each error state has been entered, possibly TEC and REC depending + on the controller.) + + Calling this function also cancels any messages waiting to be sent. No + `IRQ_TX` interrupts are delivered for these messages. + + Note that this function may or may not cause the controller to exit the + "Error Passive" state, depending whether the controller hardware zeroes + TEC and REC or not. + +.. method:: CAN.deinit() + + De-initialises a previously active CAN instance. All pending messages + (transmit and receive) are dropped and the controller stops interacting on + the bus. To use this instance again, call :func:`CAN.init()`. + + No `IRQ_TX` or `IRQ_RX` interrupts are called in response to calling this + function. + + See also :func:`CAN.restart()`. + +Constants +--------- + +.. data:: CAN.TX_QUEUE_LEN + + Maximum number of CAN messages which can be queued in the outgoing hardware + message queue of the controller. The "transmit buffer indexes" used by + :func:`CAN.send()`, :func:`CAN.cancel_send()` and `machine_can_irq_flags` + will be in this range. + +.. _can-modes: + +Modes +^^^^^ + +These values represent controller modes of operation, as passed to `CAN.init()`. Not all controllers may support all modes. + +Changing the mode of a running controller requires calling `CAN.deinit()` and then calling `CAN.init()` again with the new mode. + +.. data:: CAN.MODE_NORMAL + + The controller is active as a standard CAN network node (will acknowledge + valid messages and may transmit errors depending on its current `State + `). + +.. data:: CAN.MODE_SLEEP + + CAN controller is asleep in a low power mode. Depending on the controller, + this may support waking the controller and transitioning to `CAN.MODE_NORMAL` + if CAN traffic is received. + +.. data:: CAN.MODE_LOOPBACK + + A testing mode. The CAN controller is still connected to the external bus, + but will also receive its own transmitted messages and ignore any ACK errors. + +.. data:: CAN.MODE_SILENT + + CAN controller receives messages but does not interact with the CAN bus + (including sending ACKs, errors, etc.) + +.. data:: CAN.MODE_SILENT_LOOPBACK + + A testing mode that does not require a CAN transceiver to be connected at + all. The CAN controller receives its own transmitted messages without + interacting with the CAN bus at all. The CAN TX and RX pins remain idle. + +.. _can-states: + +States +^^^^^^ + +These values are returned by :func:`CAN.state()` and reflect the error state of the CAN controller: + +.. data:: CAN.STATE_STOPPED + + The controller has not been initialised. + +.. data:: CAN.STATE_ACTIVE + + The controller is active and ``TEC`` and ``REC`` error counters are both below the + warning threshold of 96. See :func:`CAN.get_counters()`. + +.. data:: CAN.STATE_WARNING + + The controller is active but at last one of the ``TEC`` and ``REC`` error counters + are between 96 and 127. See :func:`CAN.get_counters()`. + +.. data:: CAN.STATE_PASSIVE + + The controller is in the "Error Passive" state meaning it no longer transmits active + errors to the bus, but it is otherwise functional. This state is entered when at + least one of the ``TEC`` and ``REC`` error counters is 128 or greater, but + ``TEC`` is less than 255. See :func:`CAN.get_counters()`. + +.. data:: CAN.STATE_BUS_OFF + + The controller is in the Bus-Off state, meaning ``TEC`` error counter is + greater than 255. The CAN controller will not interact with the bus in this + state, and needs to be restarted via :func:`CAN.restart()` to continue. + +.. _can-flags: + +Message Flags +^^^^^^^^^^^^^ + + These values represent metadata about a CAN message. Functions + :func:`CAN.send()`, :func:`CAN.recv()`, and :func:`CAN.set_filters()` either + accept or return an integer value made up of zero or more of these flags bitwise + ORed together. + +.. data:: CAN.FLAG_RTR + + Indicates a message is a remote transmission request. + +.. data:: CAN.FLAG_EXT_ID + + If set, indicates a Message identifier is Extended (29-bit). If not set, + indicates a message identifier is Standard (11-bit). + +.. data:: CAN.FLAG_UNORDERED + + If set in the ``flags`` argument of :func:`CAN.send`, indicates that it's + OK if messages with the same CAN ID are sent in any order onto the bus. + + Otherwise trying to queue multiple messages with the same ID may result in + :func:`CAN.send` failing if the controller hardware can't enforce ordering. + + This flag is never set on received messages, and is ignored by + `CAN.set_filters()`. + +.. _can-recv-errors: + +Receive Error Flags +^^^^^^^^^^^^^^^^^^^ + +The result of :func:`CAN.recv()` includes an integer value made up of zero or +more of these flags bitwise ORed together. If set, these flags indicate +potential general issues with receiving CAN messages. + +.. data:: CAN.RECV_ERR_FULL + +The hardware FIFO where this message was received is full, and additional incoming messages may be lost. + +.. data:: CAN.RECV_ERR_OVERRUN + +The hardware FIFO where this message was received is full, and one or more incoming messages has been lost. + +IRQ values +^^^^^^^^^^ + +.. data:: CAN.IRQ_RX + CAN.IRQ_TX + CAN.IRQ_STATE + + IRQ event triggers. Used with :func:`CAN.irq()` and `machine_can_irq_flags`. + +.. data:: CAN.IRQ_TX_FAILED + CAN.IRQ_TX_IDX_SHIFT + CAN.IRQ_TX_IDX_MASK + + Additional IRQ event flags for `CAN.IRQ_TX`. See `machine_can_irq_flags`. + +.. _machine_can_irq_flags: + +IRQ flags +--------- + +Calling :func:`CAN.irq()` registers an interrupt handler with one or more of the +triggers `CAN.IRQ_RX`, `CAN.IRQ_TX` and `CAN.IRQ_STATE`. + +The function returns an IRQ object, and calling the ``flags()`` function on this +object returns an integer indicating which trigger event(s) triggered the +interrupt. A CAN IRQ handler should call the ``flags()`` function repeatedly +until it returns ``0``. + +When the ``flags()`` function returns with `CAN.IRQ_TX` bit set, the +handler can also check the following flag bits in the result for additional +information about the TX event: + +* ``CAN.IRQ_TX_FAILED`` bit is set if the transmit failed. Usually this will + only happen if :func:`CAN.cancel_send()` was called, although it may also + happen if the controller enters an error state. +* ``CAN.IRQ_TX_MASK << CAN.IRQ_TX_SHIFT`` is a bitmasked region of the flags + value that holds the index of the transmit buffer which generated the event. + This will be an integer in the range ``0`` to `CAN.TX_QUEUE_LEN` (exclusive), + and will match the result of a previous call to `CAN.send()`. + +IRQ_TX Example +^^^^^^^^^^^^^^ +:: + + from machine import CAN + + def irq_send(can): + while flags := can.irq().flags(): + if flags & can.IRQ_TX: + idx = (flags >> can.IRQ_TX_IDX_SHIFT) & can.IRQ_TX_IDX_MASK + success = not (flags & can.IRQ_TX_FAILED) + print("irq_send", idx, success) + + can = CAN(1, 500_000) + can.irq(irq_send, trigger=can.IRQ_TX, hard=True) + +.. important:: If the `CAN.IRQ_TX` trigger is set then the handler **must** + call ``flags()`` repeatedly until it returns ``0``, as shown in + this example. Otherwise, CAN interrupts may not be correctly + re-enabled. diff --git a/docs/library/machine.PWM.rst b/docs/library/machine.PWM.rst index c2b606affd675..fdf7d7f4e0d77 100644 --- a/docs/library/machine.PWM.rst +++ b/docs/library/machine.PWM.rst @@ -40,7 +40,7 @@ Constructors Setting *freq* may affect other PWM objects if the objects share the same underlying PWM generator (this is hardware specific). Only one of *duty_u16* and *duty_ns* should be specified at a time. - *invert* is available only on the esp32, mimxrt, nrf, rp2, samd and zephyr ports. + *invert* is available only on the alif, esp32, mimxrt, nrf, rp2, samd, stm32 and zephyr ports. Methods ------- @@ -84,6 +84,22 @@ Methods Specific PWM class implementations ---------------------------------- +On the alif port there are 11 independent PWM blocks with independent +frequencies, and they have 2 outputs each. The underlying counter is +32-bits wide for all 11 PWM blocks. + +On the rp2 port there are 8 independent PWM blocks on RP2040 and 12 on +RP2350, each with independent frequencies, and each with 2 outputs. +The underlying counter is 16-bits wide for all PWM blocks. + +On the stm32 port the number of independent PWM blocks depends on the MCU +and can range between 4 and 19. TIM2 and TIM5 blocks (also TIM3 and TIM4 +blocks on STM32U5 and STM32N6) are 32-bits wide, and the others are +16-bits wide. All MCUs supported by MicroPython have at least one 32-bit +block available, and most have two. MCUs will have pins PA0 through PA3 +assigned to a 32-bit PWM block (except STM32N6 which has a 16-bit PWM +block on PA3). PWM blocks have up to 4 outputs each. + The following concrete class(es) implement enhancements to the PWM class. | :ref:`pyb.Timer for PyBoard ` diff --git a/docs/library/machine.rst b/docs/library/machine.rst index 31acb74920c49..481820defc16e 100644 --- a/docs/library/machine.rst +++ b/docs/library/machine.rst @@ -259,6 +259,7 @@ Classes machine.I2C.rst machine.I2CTarget.rst machine.I2S.rst + machine.CAN.rst machine.RTC.rst machine.Timer.rst machine.Counter.rst diff --git a/docs/library/pyb.CAN.rst b/docs/library/pyb.CAN.rst index eb21d8223f26e..139b54f5b1140 100644 --- a/docs/library/pyb.CAN.rst +++ b/docs/library/pyb.CAN.rst @@ -237,7 +237,7 @@ Methods - *list* is an optional list object to be used as the return value - *timeout* is the timeout in milliseconds to wait for the receive. - Return value: A tuple containing five values. + Return value: A list containing five values. - The id of the message. - A boolean that indicates if the message ID is standard or extended. @@ -245,8 +245,8 @@ Methods - The FMI (Filter Match Index) value. - An array containing the data. - If *list* is ``None`` then a new tuple will be allocated, as well as a new - bytes object to contain the data (as the fifth element in the tuple). + If *list* is ``None`` then a new list will be allocated, as well as a new + bytes object to contain the data (as the fifth element in the list). If *list* is not ``None`` then it should be a list object with a least five elements. The fifth element should be a memoryview object which is created diff --git a/docs/library/re.rst b/docs/library/re.rst index b8aeefd90cfa4..47f623c5600c8 100644 --- a/docs/library/re.rst +++ b/docs/library/re.rst @@ -54,6 +54,10 @@ Supported operators and special sequences are: Grouping. Each group is capturing (a substring it captures can be accessed with `match.group()` method). +``(?:...)`` + Non-capturing grouping. Each group is matched using the same rules as + regular grouping, but will not be part of the match object. + ``\d`` Matches digit. Equivalent to ``[0-9]``. @@ -87,7 +91,6 @@ Supported operators and special sequences are: * counted repetitions (``{m,n}``) * named groups (``(?P...)``) -* non-capturing groups (``(?:...)``) * more advanced assertions (``\b``, ``\B``) * special character escapes like ``\r``, ``\n`` - use Python's own escaping instead diff --git a/docs/library/weakref.rst b/docs/library/weakref.rst new file mode 100644 index 0000000000000..ffebc91277beb --- /dev/null +++ b/docs/library/weakref.rst @@ -0,0 +1,78 @@ +:mod:`weakref` -- Python object lifetime management +=================================================== + +.. module:: weakref + :synopsis: Create weak references to Python objects + +|see_cpython_module| :mod:`python:weakref`. + +This module allows creation of weak references to Python objects. A weak reference +is a non-traceable reference to a heap-allocated Python object, so the garbage +collector can still reclaim the object even though the weak reference refers to it. + +Python callbacks can be registered to be called when an object is reclaimed by the +garbage collector. This provides a safe way to clean up when objects are no longer +needed. + +**Availability:** the weakref module requires ``MICROPY_PY_WEAKREF`` to be enabled +at compile time. It is enabled on the unix coverage variant and the webassembly +pyscript variant. + +ref objects +----------- + +A ref object is the simplest way to make a weak reference. + +.. class:: ref(object [, callback], /) + + Return a weak reference to the given *object*. + + If *callback* is given and is not ``None`` then, when *object* is reclaimed + by the garbage collector and if the weak reference object is still alive, the + *callback* will be called. The *callback* will be passed the weak reference + object as its single argument. + +.. method:: ref.__call__() + + Calling the weak reference object will return its referenced object if that + object is still alive. Otherwise ``None`` will be returned. + +finalize objects +---------------- + +A finalize object is an extended version of a ref object that is more convenient to +use, and allows more control over the callback. + +.. class:: finalize(object, callback, /, *args, **kwargs) + + Return a weak reference to the given *object*. In contrast to *weakref.ref* + objects, finalize objects are held onto internally and will not be collected until + *object* is collected. + + A finalize object starts off alive. It transitions to the dead state when the + finalize object is called, either explicitly or when *object* is collected. It also + transitions to dead if the `finalize.detach()` method is called. + + When *object* is reclaimed by the garbage collector (or the finalize object is + explicitly called by user code) and the finalize object is still in the alive state, + the *callback* will be called. The *callback* will be passed arguments as: + ``callback(*args, **kwargs)``. + +.. method:: finalize.__call__() + + If the finalize object is alive then it transitions to the dead state and returns + the value of ``callback(*args, **kwargs)``. Otherwise ``None`` will be returned. + +.. method:: finalize.alive + + Read-only boolean attribute that indicates if the finalizer is in the alive state. + +.. method:: finalize.peek() + + If the finalize object is alive then return ``(object, callback, args, kwargs)``. + Otherwise return ``None``. + +.. method:: finalize.detach() + + If the finalize object is alive then it transitions to the dead state and returns + ``(object, callback, args, kwargs)``. Otherwise ``None`` will be returned. diff --git a/docs/reference/speed_python.rst b/docs/reference/speed_python.rst index 9360fd6108027..6382394bd507b 100644 --- a/docs/reference/speed_python.rst +++ b/docs/reference/speed_python.rst @@ -243,12 +243,10 @@ no adaptation (but see below). It is invoked by means of a function decorator: There are certain limitations in the current implementation of the native code emitter. -* Context managers are not supported (the ``with`` statement). -* Generators are not supported. * If ``raise`` is used an argument must be supplied. * The background scheduler (see `micropython.schedule`) is not run during execution of native code. -* On targets with thrteading and the GIL, the GIL is not released during +* On targets with threading and the GIL, the GIL is not released during execution of native code. To mitigate the last two points, long running native functions should call diff --git a/extmod/extmod.cmake b/extmod/extmod.cmake index 9ca869582e026..40ae717a54f6a 100644 --- a/extmod/extmod.cmake +++ b/extmod/extmod.cmake @@ -10,6 +10,7 @@ set(MICROPY_SOURCE_EXTMOD ${MICROPY_EXTMOD_DIR}/machine_adc.c ${MICROPY_EXTMOD_DIR}/machine_adc_block.c ${MICROPY_EXTMOD_DIR}/machine_bitstream.c + ${MICROPY_EXTMOD_DIR}/machine_can.c ${MICROPY_EXTMOD_DIR}/machine_i2c.c ${MICROPY_EXTMOD_DIR}/machine_i2c_target.c ${MICROPY_EXTMOD_DIR}/machine_i2s.c diff --git a/extmod/extmod.mk b/extmod/extmod.mk index 37151ad120e68..961699f089e6b 100644 --- a/extmod/extmod.mk +++ b/extmod/extmod.mk @@ -5,6 +5,7 @@ SRC_EXTMOD_C += \ extmod/machine_adc.c \ extmod/machine_adc_block.c \ extmod/machine_bitstream.c \ + extmod/machine_can.c \ extmod/machine_i2c.c \ extmod/machine_i2c_target.c \ extmod/machine_i2s.c \ diff --git a/extmod/machine_can.c b/extmod/machine_can.c new file mode 100644 index 0000000000000..11577d9da9407 --- /dev/null +++ b/extmod/machine_can.c @@ -0,0 +1,749 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2024-2026 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "py/objstr.h" +#include "py/runtime.h" + +#if MICROPY_PY_MACHINE_CAN + +#include +#include +#include "extmod/modmachine.h" +#include "extmod/machine_can.h" +#include "extmod/machine_can_port.h" + +#ifndef MICROPY_HW_NUM_CAN +#error "Port must provide a definition of MICROPY_HW_NUM_CAN" +#endif + +#define CAN_MSG_RTR (1 << 0) +#define CAN_MSG_EXTENDED_ID (1 << 1) +#define CAN_MSG_FD_F (1 << 2) +#define CAN_MSG_BRS (1 << 3) + +#define CAN_CLASSIC_MAX_LEN 8 + +// The port provides implementations of the static machine_can_port.h functions in this file. +#include MICROPY_PY_MACHINE_CAN_INCLUDEFILE + +// Note: not all possible return values of flags() are allowed as triggers +#define CAN_IRQ_ALLOWED_TRIGGERS (MP_CAN_IRQ_TX | MP_CAN_IRQ_RX | MP_CAN_IRQ_STATE) + +// Port can override any of these limits if necessary +#ifndef CAN_TSEG1_MIN +#define CAN_TSEG1_MIN 1 +#endif +#ifndef CAN_TSEG1_MAX +#define CAN_TSEG1_MAX 16 +#endif +#ifndef CAN_TSEG2_MIN +#define CAN_TSEG2_MIN 1 +#endif +#ifndef CAN_TSEG2_MAX +#define CAN_TSEG2_MAX 8 +#endif +#ifndef CAN_SJW_MIN +#define CAN_SJW_MIN 1 +#endif +#ifndef CAN_SJW_MAX +#define CAN_SJW_MAX 4 +#endif + +#if MICROPY_HW_ENABLE_FDCAN +// CAN-FD BRS (Baud Rate Switch) default limits +#ifndef CAN_FD_BRS_TSEG1_MIN +#define CAN_FD_BRS_TSEG1_MIN 1 +#endif +#ifndef CAN_FD_BRS_TSEG1_MAX +#define CAN_FD_BRS_TSEG1_MAX 32 +#endif +#ifndef CAN_FD_BRS_TSEG2_MIN +#define CAN_FD_BRS_TSEG2_MIN 1 +#endif +#ifndef CAN_FD_BRS_TSEG2_MAX +#define CAN_FD_BRS_TSEG2_MAX 16 +#endif +#ifndef CAN_FD_BRS_SJW_MIN +#define CAN_FD_BRS_SJW_MIN 1 +#endif +#ifndef CAN_FD_BRS_SJW_MAX +#define CAN_FD_BRS_SJW_MAX 16 +#endif +#endif // MICROPY_HW_ENABLE_FDCAN + +#ifndef CAN_FILTERS_STD_EXT_SEPARATE +// Set if the hardware maintains separate filter indexes for Standard vs Extended IDs +#define CAN_FILTERS_STD_EXT_SEPARATE 0 +#endif +#ifndef CAN_PORT_PRINT_FUNCTION +// If this is set then the port should define its own function machine_can_print() +#define CAN_PORT_PRINT_FUNCTION 0 +#endif + +// Non port-specific functions follow + +// Calculate baud rate prescaler, and if necessary also find tseg1, tseg2 +// to satisfy sample_point +// +static int calculate_brp(int bitrate_nom, int f_clock, int *tseg1, int *tseg2, int sample_point, bool is_fd_brs) { + bool find_tseg = (*tseg1 == -1); // We need to calculate tseg1, tseg2 + + // Set min/max limits for Classic CAN + int brp_min = CAN_BRP_MIN; + int brp_max = CAN_BRP_MAX; + int tseg1_min = CAN_TSEG1_MIN; + int tseg1_max = CAN_TSEG1_MAX; + int tseg2_min = CAN_TSEG2_MIN; + int tseg2_max = CAN_TSEG2_MAX; + #if MICROPY_HW_ENABLE_FDCAN + // If CAN-FD controller, set min/max limits for CAN-FD BRS + if (is_fd_brs) { + brp_min = CAN_FD_BRS_BRP_MIN; + brp_max = CAN_FD_BRS_BRP_MAX; + tseg1_min = CAN_FD_BRS_TSEG1_MIN; + tseg1_max = CAN_FD_BRS_TSEG1_MAX; + tseg2_min = CAN_FD_BRS_TSEG2_MIN; + tseg2_max = CAN_FD_BRS_TSEG2_MAX; + } + #else + (void)is_fd_brs; + #endif + + int brp_best = brp_min; + int best_bitrate_err = bitrate_nom; // best case deviation from bitrate_nom, start at max + int best_sample_err = 10000; // Units of .01%, start at max + + for (int brp = brp_max; brp >= brp_min; brp--) { + unsigned scaled_clock = f_clock / brp; + + if (find_tseg) { + // Find the total number of time quanta that gets closest to the nominal bitrate + int ts_total = (scaled_clock + bitrate_nom / 2) / bitrate_nom; + + if (ts_total < tseg1_min + tseg2_min + 1 + || ts_total > tseg1_max + tseg2_max + 1) { + // The total time quanta doesn't fit in the allowed range + continue; + } + + int bitrate_err = abs((int)bitrate_nom - (int)(scaled_clock / ts_total)); + + if (bitrate_err > best_bitrate_err) { + // This result is worse than our current best bitrate + continue; + } + + // Look for tseg1 & tseg2 that come closest to the sample point + for (int ts1 = tseg1_min; ts1 <= tseg1_max; ts1++) { + int ts2 = ts_total - 1 - ts1; + if (ts2 >= tseg2_min && ts2 <= tseg2_max) { + int try_sample = 10000 * (1 + ts1) / (1 + ts1 + ts2); // sample point, units of .01% + int try_err = abs(try_sample - sample_point * 100); + // Priorities for selecting the best: + // 1. Smallest bitrate error. + // 2. Smallest sample point error. + // 3. Shorter (i.e. more total) time quanta (meaning if all else is equal, choose lower brp). + if (bitrate_err < best_bitrate_err || try_err <= best_sample_err) { + *tseg1 = ts1; + *tseg2 = ts2; + best_sample_err = try_err; + brp_best = brp; + best_bitrate_err = bitrate_err; + } + } + } + } else { + // tseg1 and tseg2 already set, find brp with the lowest bitrate error + int ts_total = *tseg1 + *tseg2 + 1; + int bitrate_err = abs((int)bitrate_nom - (int)(scaled_clock / ts_total)); + if (bitrate_err <= best_bitrate_err) { + brp_best = brp; + best_bitrate_err = bitrate_err; + } + } + } + + if (best_bitrate_err == bitrate_nom) { + // Didn't find any eligible bitrates + mp_raise_ValueError(MP_ERROR_TEXT("unable to calculate BRP for baudrate")); + } + + return brp_best; +} + +// Check a CAN Message ID value is within the valid range, based on supplied flags +static void id_range_check(mp_uint_t can_id, mp_uint_t flags) { + mp_uint_t max = (flags & CAN_MSG_FLAG_EXT_ID) ? (1 << 29) : (1 << 11); + if (can_id >= max) { + mp_raise_ValueError(MP_ERROR_TEXT("invalid id")); + } +} + +static int machine_can_get_actual_bitrate(machine_can_obj_t *self) { + int f_clock = machine_can_port_f_clock(self); + return f_clock / self->brp / (1 + self->tseg1 + self->tseg2); +} + +static mp_obj_t machine_can_deinit(mp_obj_t self_in) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + // Each CAN instance should have at most one peripheral object + assert(self == MP_STATE_PORT(machine_can_objs)[self->can_idx]); + self->mp_irq_trigger = 0; + self->mp_irq_obj = NULL; + if (self->port) { + machine_can_update_irqs(self); + machine_can_port_deinit(self); + self->port = NULL; + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(machine_can_deinit_obj, machine_can_deinit); + +static void machine_can_init_helper(machine_can_obj_t *self, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_bitrate, ARG_mode, ARG_sample_point, ARG_sjw, ARG_tseg1, ARG_tseg2}; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_bitrate, MP_ARG_INT | MP_ARG_REQUIRED }, + { MP_QSTR_mode, MP_ARG_INT, {.u_int = MP_CAN_MODE_NORMAL} }, + { MP_QSTR_sample_point, MP_ARG_INT, {.u_int = 75} }, + { MP_QSTR_sjw, MP_ARG_INT, {.u_int = CAN_SJW_MIN } }, + { MP_QSTR_tseg1, MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_tseg2, MP_ARG_INT, {.u_int = -1} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Verify arguments + + int bitrate_nom = args[ARG_bitrate].u_int; + + machine_can_mode_t mode = args[ARG_mode].u_int; + if (mode >= MP_CAN_MODE_MAX || !machine_can_port_supports_mode(self, mode)) { + mp_raise_ValueError(MP_ERROR_TEXT("mode")); + } + + int sjw = args[ARG_sjw].u_int; + if (sjw < CAN_SJW_MIN || sjw > CAN_SJW_MAX) { + mp_raise_ValueError(MP_ERROR_TEXT("sjw")); + } + + int tseg1 = args[ARG_tseg1].u_int; + int tseg2 = args[ARG_tseg2].u_int; + if ((tseg1 != -1 && (tseg1 < CAN_TSEG1_MIN || tseg1 > CAN_TSEG1_MAX)) + || (tseg1 == -1 && tseg2 != -1)) { + mp_raise_ValueError(MP_ERROR_TEXT("tseg1")); + } + if ((tseg2 != -1 && (tseg2 < CAN_TSEG2_MIN || tseg2 > CAN_TSEG2_MAX)) + || (tseg2 == -1 && tseg1 != -1)) { + mp_raise_ValueError(MP_ERROR_TEXT("tseg2")); + } + + int sample_point = args[ARG_sample_point].u_int; + if (sample_point <= 0 || sample_point >= 100) { + // Probably can make these values more restrictive + mp_raise_ValueError(MP_ERROR_TEXT("sample_point")); + } + + int f_clock = machine_can_port_f_clock(self); + + // This function will also set tseg1 and tseg2 if they are -1 + int brp = calculate_brp(bitrate_nom, f_clock, &tseg1, &tseg2, sample_point, false); + + // Set up the hardware + self->tseg1 = tseg1; + self->tseg2 = tseg2; + self->brp = brp; + self->sjw = sjw; + self->mode = mode; + memset(&self->counters, 0, sizeof(self->counters)); + + if (self->port != NULL) { + machine_can_port_deinit(self); + } + machine_can_port_init(self); +} + +// Raise an exception if the CAN controller is not initialised +static void machine_can_check_initialised(machine_can_obj_t *self) { + // Use self->port being allocated as indicator that controller is initialised + if (self->port == NULL) { + mp_raise_OSError(MP_EINVAL); + } +} + +mp_uint_t machine_can_get_index(mp_obj_t identifier) { + mp_uint_t can_num; + if (mp_obj_is_str(identifier)) { + const char *port = mp_obj_str_get_str(identifier); + if (0) { + #ifdef MICROPY_HW_CAN1_NAME + } else if (strcmp(port, MICROPY_HW_CAN1_NAME) == 0) { + can_num = 1; + #endif + #ifdef MICROPY_HW_CAN2_NAME + } else if (strcmp(port, MICROPY_HW_CAN2_NAME) == 0) { + can_num = 2; + #endif + #ifdef MICROPY_HW_CAN3_NAME + } else if (strcmp(port, MICROPY_HW_CAN3_NAME) == 0) { + can_num = 3; + #endif + } else { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%s) doesn't exist"), port); + } + } else { + can_num = mp_obj_get_int(identifier); + } + if (can_num < 1 || can_num > MICROPY_HW_NUM_CAN) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%d) doesn't exist"), can_num); + } + + // check if the CAN is reserved for system use or not + if (MICROPY_HW_CAN_IS_RESERVED(can_num)) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%d) is reserved"), can_num); + } + + return can_num - 1; +} + +static mp_obj_t machine_can_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { + // check arguments + mp_arg_check_num(n_args, n_kw, 1, MP_OBJ_FUN_ARGS_MAX, true); + + // work out port + mp_uint_t can_idx = machine_can_get_index(args[0]); + + machine_can_obj_t *self = MP_STATE_PORT(machine_can_objs)[can_idx]; + if (self == NULL) { + self = mp_obj_malloc(machine_can_obj_t, &machine_can_type); + self->can_idx = can_idx; + MP_STATE_PORT(machine_can_objs)[can_idx] = self; + } + + mp_map_t kw_args; + mp_map_init_fixed_table(&kw_args, n_kw, args + n_args); + machine_can_init_helper(self, n_args, args, &kw_args); + + return MP_OBJ_FROM_PTR(self); +} + +static mp_obj_t machine_can_init(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + machine_can_init_helper(self, n_args, pos_args, kw_args); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(machine_can_init_obj, 1, machine_can_init); + +void machine_can_deinit_all(void) { + for (int i = 0; i < MICROPY_HW_NUM_CAN; i++) { + mp_obj_t can = MP_OBJ_FROM_PTR(MP_STATE_PORT(machine_can_objs)[i]); + if (can) { + machine_can_deinit(can); + MP_STATE_PORT(machine_can_objs)[i] = NULL; + } + } +} + +static mp_uint_t can_irq_trigger(mp_obj_t self_in, mp_uint_t new_trigger) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + machine_can_check_initialised(self); + self->mp_irq_trigger = new_trigger; + // Call into port layer to update hardware IRQ config + machine_can_update_irqs(self); + return 0; +} + +static mp_uint_t can_irq_info(mp_obj_t self_in, mp_uint_t info_type) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + machine_can_check_initialised(self); + if (info_type == MP_IRQ_INFO_FLAGS) { + // Call into port layer to get current irq flags + return machine_can_port_irq_flags(self); + } else if (info_type == MP_IRQ_INFO_TRIGGERS) { + return self->mp_irq_trigger; + } + return 0; +} + +static const mp_irq_methods_t can_irq_methods = { + .trigger = can_irq_trigger, + .info = can_irq_info, +}; + +static mp_obj_t mp_machine_can_irq(machine_can_obj_t *self, bool any_args, mp_arg_val_t *args) { + machine_can_check_initialised(self); + if (self->mp_irq_obj == NULL) { + self->mp_irq_trigger = 0; + self->mp_irq_obj = mp_irq_new(&can_irq_methods, MP_OBJ_FROM_PTR(self)); + } + + if (any_args) { + // TODO: refactor this into a helper to save some code size + mp_uint_t trigger = args[MP_IRQ_ARG_INIT_trigger].u_int; + mp_uint_t not_supported = trigger & ~CAN_IRQ_ALLOWED_TRIGGERS; + if (not_supported) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("trigger 0x%08x unsupported"), not_supported); + } + + mp_obj_t handler = args[MP_IRQ_ARG_INIT_handler].u_obj; + if (handler != mp_const_none && !mp_obj_is_callable(handler)) { + mp_raise_ValueError(MP_ERROR_TEXT("handler must be None or callable")); + } + + self->mp_irq_obj->handler = handler; + self->mp_irq_obj->ishard = args[MP_IRQ_ARG_INIT_hard].u_bool; + + can_irq_trigger(MP_OBJ_FROM_PTR(self), trigger); + } + + return MP_OBJ_FROM_PTR(self->mp_irq_obj); +} + +static mp_obj_t machine_can_send(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + machine_can_check_initialised(self); + + enum { ARG_id, ARG_data, ARG_flags, /*ARG_context*/ }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_id, MP_ARG_REQUIRED | MP_ARG_INT, {} }, + { MP_QSTR_data, MP_ARG_REQUIRED | MP_ARG_OBJ, {} }, + { MP_QSTR_flags, MP_ARG_INT, {.u_int = 0} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, + MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_uint_t can_id = args[ARG_id].u_int; + mp_uint_t flags = args[ARG_flags].u_int; + + mp_buffer_info_t data; + mp_get_buffer_raise(args[ARG_data].u_obj, &data, MP_BUFFER_READ); + + if (data.len > machine_can_port_max_data_len(flags)) { + mp_raise_msg(&mp_type_OverflowError, MP_ERROR_TEXT("data too long")); + } + id_range_check(can_id, flags); + + mp_int_t res = machine_can_port_send(self, can_id, data.buf, data.len, flags); + + return res >= 0 ? MP_OBJ_NEW_SMALL_INT(res) : mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(machine_can_send_obj, 3, machine_can_send); + +static mp_obj_t machine_can_cancel_send(mp_obj_t self_in, mp_obj_t arg) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + machine_can_check_initialised(self); + mp_uint_t idx = mp_obj_get_uint(arg); + if (idx >= CAN_TX_QUEUE_LEN) { + mp_raise_type(&mp_type_IndexError); + } + bool res = machine_can_port_cancel_send(self, mp_obj_get_uint(arg)); + return mp_obj_new_bool(res); +} +static MP_DEFINE_CONST_FUN_OBJ_2(machine_can_cancel_send_obj, machine_can_cancel_send); + +static mp_obj_t machine_can_recv(size_t n_args, const mp_obj_t *args) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(args[0]); + machine_can_check_initialised(self); + + uint8_t data[MP_CAN_MAX_LEN]; + size_t dlen = sizeof(data); + mp_uint_t id = 0; + mp_uint_t flags = 0; + mp_uint_t errors = 0; + mp_obj_array_t *data_memoryview; + bool use_arg_as_result = n_args > 1 && args[1] != mp_const_none; + mp_obj_list_t *result = mp_obj_list_optional_arg(use_arg_as_result ? args[1] : mp_const_none, RECV_ARG_LEN); + mp_obj_t *items = result->items; + + // Validate the memoryview item if list passed in arg + if (use_arg_as_result) { + if (!mp_obj_is_type(items[RECV_ARG_DATA], &mp_type_memoryview)) { + mp_raise_TypeError(NULL); + } + } + + // Call the port-specific receive function + if (!machine_can_port_recv(self, data, &dlen, &id, &flags, &errors)) { + return mp_const_none; // Nothing to return + } + + // Create memoryview for the result list, if not passed in + if (!use_arg_as_result) { + // Make the result memoryview large enough for the entire result buffer, not the recv result + // ... it will be resized further down + // (Potentially wasteful, but means the result can be safely reused as arg to a subsequent call) + void *backing_buf = m_malloc(MP_CAN_MAX_LEN); + items[RECV_ARG_DATA] = mp_obj_new_memoryview('B', MP_CAN_MAX_LEN, backing_buf); + } + // TODO: length check any memoryview 'result' that was passed in as arg? + // (may not be possible as I don't think memoryview records the size of its backing buffer) + + data_memoryview = MP_OBJ_TO_PTR(items[RECV_ARG_DATA]); // type of obj was checked already + + if ((flags & CAN_MSG_FLAG_RTR) == 0) { + memcpy(data_memoryview->items, data, dlen); + } else { + // Remote request, return a memoryview of 0s with correct length + memset(data_memoryview->items, 0, dlen); + } + data_memoryview->len = dlen; + + items[RECV_ARG_ID] = MP_OBJ_NEW_SMALL_INT(id); + items[RECV_ARG_FLAGS] = MP_OBJ_NEW_SMALL_INT(flags); + items[RECV_ARG_ERRORS] = MP_OBJ_NEW_SMALL_INT(errors); + + return MP_OBJ_FROM_PTR(result); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(machine_can_recv_obj, 1, 2, machine_can_recv); + +static mp_obj_t machine_can_set_filters(mp_obj_t self_in, mp_obj_t filters) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + machine_can_check_initialised(self); + mp_obj_iter_buf_t iter_buf; + int idx = 0; + #if CAN_FILTERS_STD_EXT_SEPARATE + int idx_ext = 0; + #endif + mp_obj_t filter; + + machine_can_port_clear_filters(self); + + if (filters == mp_const_none) { + // Passing 'None' is shortcut for "allow all" - pass standard IDs + // via filter 0 and extended IDs via filter 1. + machine_can_port_set_filter(self, 0, 0, 0, 0); + machine_can_port_set_filter(self, 1, 0, 0, CAN_MSG_FLAG_EXT_ID); + } else { + // Walk the iterable argument and call machine_can_port_set_filter() + // for each item + filters = mp_getiter(filters, &iter_buf); + while ((filter = mp_iternext(filters)) != MP_OBJ_STOP_ITERATION) { + mp_obj_t *args; + size_t arg_len; + mp_obj_get_array(filter, &arg_len, &args); + if (arg_len != 3) { + mp_raise_ValueError(MP_ERROR_TEXT("filter items must have length 3")); + } + mp_uint_t id = mp_obj_get_int(args[0]); + mp_uint_t mask = mp_obj_get_int(args[1]); + mp_uint_t flags = mp_obj_get_int(args[2]); + + id_range_check(id, flags); + id_range_check(mask, flags); + + // This function is expected to raise an exception if filter cannot be set + machine_can_port_set_filter(self, + #if CAN_FILTERS_STD_EXT_SEPARATE + (flags & CAN_MSG_FLAG_EXT_ID) ? idx_ext++ : idx++, + #else + idx++, + #endif + id, + mask, + flags + ); + } + } + + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_2(machine_can_set_filters_obj, machine_can_set_filters); + +static mp_obj_t machine_can_state(mp_obj_t self_in) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (self->port == NULL) { + return MP_OBJ_NEW_SMALL_INT(MP_CAN_STATE_STOPPED); + } + return MP_OBJ_NEW_SMALL_INT(machine_can_port_get_state(self)); +} +static MP_DEFINE_CONST_FUN_OBJ_1(machine_can_state_obj, machine_can_state); + +static mp_obj_t machine_can_get_counters(size_t n_args, const mp_obj_t *args) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(args[0]); + machine_can_counters_t *counters = &self->counters; + mp_obj_list_t *list = mp_obj_list_optional_arg(n_args > 1 ? args[1] : mp_const_none, 8); + machine_can_check_initialised(self); + machine_can_port_update_counters(self); + + // Note: the members of 'counters' are laid out in the same order, + // so compiler should be able to infer some kind of loop here... + list->items[0] = MP_OBJ_NEW_SMALL_INT(counters->tec); + list->items[1] = MP_OBJ_NEW_SMALL_INT(counters->rec); + list->items[2] = MP_OBJ_NEW_SMALL_INT(counters->num_warning); + list->items[3] = MP_OBJ_NEW_SMALL_INT(counters->num_passive); + list->items[4] = MP_OBJ_NEW_SMALL_INT(counters->num_bus_off); + list->items[5] = MP_OBJ_NEW_SMALL_INT(counters->tx_pending); + list->items[6] = MP_OBJ_NEW_SMALL_INT(counters->rx_pending); + list->items[7] = MP_OBJ_NEW_SMALL_INT(counters->rx_overruns); + + return MP_OBJ_FROM_PTR(list); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(machine_can_get_counters_obj, 1, 2, machine_can_get_counters); + +static mp_obj_t machine_can_get_timings(size_t n_args, const mp_obj_t *args) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(args[0]); + mp_obj_list_t *list = mp_obj_list_optional_arg(n_args > 1 ? args[1] : mp_const_none, 6); + + list->items[0] = MP_OBJ_NEW_SMALL_INT(machine_can_get_actual_bitrate(self)); + list->items[1] = MP_OBJ_NEW_SMALL_INT(self->sjw); + list->items[2] = MP_OBJ_NEW_SMALL_INT(self->tseg1); + list->items[3] = MP_OBJ_NEW_SMALL_INT(self->tseg2); + #if MICROPY_HW_ENABLE_FDCAN + mp_obj_list_t *fd_list = mp_obj_list_optional_arg(list->items[4], 4); + fd_list->items[0] = mp_const_none; // TODO: CAN-FD timings support + fd_list->items[1] = mp_const_none; + fd_list->items[2] = mp_const_none; + fd_list->items[3] = mp_const_none; + list->items[4] = MP_OBJ_FROM_PTR(fd_list); + #else + list->items[4] = mp_const_none; + #endif + list->items[5] = machine_can_port_get_additional_timings(self, n_args > 1 ? list->items[5] : mp_const_none); + + return MP_OBJ_FROM_PTR(list); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(machine_can_get_timings_obj, 1, 2, machine_can_get_timings); + + +static mp_obj_t machine_can_restart(mp_obj_t self_in) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + machine_can_check_initialised(self); + machine_can_port_restart(self); + memset(&self->counters, 0, sizeof(self->counters)); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(machine_can_restart_obj, machine_can_restart); + +#if !CAN_PORT_PRINT_FUNCTION +static void machine_can_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_can_obj_t *self = MP_OBJ_TO_PTR(self_in); + // We don't store the bitrate argument, instead print the real achieved bitrate + int f_clock = machine_can_port_f_clock(self); + int actual_bitrate = machine_can_get_actual_bitrate(self); + + qstr mode; + switch (self->mode) { + case MP_CAN_MODE_NORMAL: + mode = MP_QSTR_MODE_NORMAL; + break; + case MP_CAN_MODE_SLEEP: + mode = MP_QSTR_MODE_SLEEP; + break; + case MP_CAN_MODE_LOOPBACK: + mode = MP_QSTR_MODE_LOOPBACK; + break; + case MP_CAN_MODE_SILENT: + mode = MP_QSTR_MODE_SILENT; + break; + case MP_CAN_MODE_SILENT_LOOPBACK: + default: + mode = MP_QSTR_MODE_SILENT_LOOPBACK; + break; + } + + mp_printf(print, "CAN(%d, bitrate=%u, mode=CAN.%q, sjw=%u, tseg1=%u, tseg2=%u, f_clock=%u)", + self->can_idx + 1, + actual_bitrate, + mode, + self->sjw, + self->tseg1, + self->tseg2, + f_clock); +} +#endif + +// CAN.irq(handler, trigger, hard) +static mp_obj_t machine_can_irq(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + mp_arg_val_t args[MP_IRQ_ARG_INIT_NUM_ARGS]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_IRQ_ARG_INIT_NUM_ARGS, mp_irq_init_args, args); + machine_can_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + bool any_args = n_args > 1 || kw_args->used != 0; + return MP_OBJ_FROM_PTR(mp_machine_can_irq(self, any_args, args)); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(machine_can_irq_obj, 1, machine_can_irq); + +static const mp_rom_map_elem_t machine_can_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&machine_can_init_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&machine_can_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_irq), MP_ROM_PTR(&machine_can_irq_obj) }, + { MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&machine_can_send_obj) }, + { MP_ROM_QSTR(MP_QSTR_cancel_send), MP_ROM_PTR(&machine_can_cancel_send_obj) }, + { MP_ROM_QSTR(MP_QSTR_recv), MP_ROM_PTR(&machine_can_recv_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_filters), MP_ROM_PTR(&machine_can_set_filters_obj) }, + { MP_ROM_QSTR(MP_QSTR_state), MP_ROM_PTR(&machine_can_state_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_counters), MP_ROM_PTR(&machine_can_get_counters_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_timings), MP_ROM_PTR(&machine_can_get_timings_obj) }, + { MP_ROM_QSTR(MP_QSTR_restart), MP_ROM_PTR(&machine_can_restart_obj) }, + + // Mode enum constants + { MP_ROM_QSTR(MP_QSTR_MODE_NORMAL), MP_ROM_INT(MP_CAN_MODE_NORMAL) }, + { MP_ROM_QSTR(MP_QSTR_MODE_SLEEP), MP_ROM_INT(MP_CAN_MODE_SLEEP) }, + { MP_ROM_QSTR(MP_QSTR_MODE_LOOPBACK), MP_ROM_INT(MP_CAN_MODE_LOOPBACK) }, + { MP_ROM_QSTR(MP_QSTR_MODE_SILENT), MP_ROM_INT(MP_CAN_MODE_SILENT) }, + { MP_ROM_QSTR(MP_QSTR_MODE_SILENT_LOOPBACK), MP_ROM_INT(MP_CAN_MODE_SILENT_LOOPBACK) }, + + // State enum constants + { MP_ROM_QSTR(MP_QSTR_STATE_STOPPED), MP_ROM_INT(MP_CAN_STATE_STOPPED) }, + { MP_ROM_QSTR(MP_QSTR_STATE_ACTIVE), MP_ROM_INT(MP_CAN_STATE_ACTIVE) }, + { MP_ROM_QSTR(MP_QSTR_STATE_WARNING), MP_ROM_INT(MP_CAN_STATE_WARNING) }, + { MP_ROM_QSTR(MP_QSTR_STATE_PASSIVE), MP_ROM_INT(MP_CAN_STATE_PASSIVE) }, + { MP_ROM_QSTR(MP_QSTR_STATE_BUS_OFF), MP_ROM_INT(MP_CAN_STATE_BUS_OFF) }, + + // Message Flag enum constants + { MP_ROM_QSTR(MP_QSTR_FLAG_RTR), MP_ROM_INT(CAN_MSG_FLAG_RTR) }, + { MP_ROM_QSTR(MP_QSTR_FLAG_EXT_ID), MP_ROM_INT(CAN_MSG_FLAG_EXT_ID) }, + { MP_ROM_QSTR(MP_QSTR_FLAG_UNORDERED), MP_ROM_INT(CAN_MSG_FLAG_UNORDERED) }, + + // Receive Error Flag enum constants + { MP_ROM_QSTR(MP_QSTR_RECV_ERR_FULL), MP_ROM_INT(CAN_RECV_ERR_FULL) }, + { MP_ROM_QSTR(MP_QSTR_RECV_ERR_OVERRUN), MP_ROM_INT(CAN_RECV_ERR_OVERRUN) }, + + // IRQ enum constants + { MP_ROM_QSTR(MP_QSTR_IRQ_RX), MP_ROM_INT(MP_CAN_IRQ_RX) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_TX), MP_ROM_INT(MP_CAN_IRQ_TX) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_STATE), MP_ROM_INT(MP_CAN_IRQ_STATE) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_TX_FAILED), MP_ROM_INT(MP_CAN_IRQ_TX_FAILED) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_TX_IDX_SHIFT), MP_ROM_INT(MP_CAN_IRQ_IDX_SHIFT) }, + { MP_ROM_QSTR(MP_QSTR_IRQ_TX_IDX_MASK), MP_ROM_INT(MP_CAN_IRQ_IDX_MASK) }, + + // Other constants + { MP_ROM_QSTR(MP_QSTR_TX_QUEUE_LEN), MP_ROM_INT(CAN_TX_QUEUE_LEN) }, + { MP_ROM_QSTR(MP_QSTR_FILTERS_MAX), MP_ROM_INT(CAN_HW_MAX_FILTER) }, +}; +static MP_DEFINE_CONST_DICT(machine_can_locals_dict, machine_can_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + machine_can_type, + MP_QSTR_CAN, + 0, + make_new, machine_can_make_new, + print, machine_can_print, + locals_dict, &machine_can_locals_dict + ); + +MP_REGISTER_ROOT_POINTER(struct _machine_can_obj_t *machine_can_objs[MICROPY_HW_NUM_CAN]); + +#endif // MICROPY_PY_MACHINE_CAN diff --git a/extmod/machine_can.h b/extmod/machine_can.h new file mode 100644 index 0000000000000..6f2d310367083 --- /dev/null +++ b/extmod/machine_can.h @@ -0,0 +1,41 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2024-2026 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_H +#define MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_H + +#include "py/obj.h" + +// machine.CAN support APIs that are called from port-level C code + +// Return the 0-based index of the CAN peripheral based on the name or the +// (1-based) number. +// +// Raises an exception if the identifier is invalid, doesn't exist, or is reserved. +mp_uint_t machine_can_get_index(mp_obj_t identifier); + +void machine_can_deinit_all(void); + +#endif // MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_H diff --git a/extmod/machine_can_port.h b/extmod/machine_can_port.h new file mode 100644 index 0000000000000..58eaca0f5c8d2 --- /dev/null +++ b/extmod/machine_can_port.h @@ -0,0 +1,188 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2024-2026 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_PORT_H +#define MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_PORT_H + +#include "py/obj.h" +#include "py/objarray.h" +#include "py/runtime.h" +#include "shared/runtime/mpirq.h" + +// This header is included into both extmod/machine_can.c and port-specific +// machine_can.c implementations and provides shared (static) function +// declarations to both. +// +// In a MicroPython build including this header from port-specific machine_can.c +// include is a no-op (as the file is included directly into +// extmod/machine_can.c). However, including it anyway means that Language +// Servers and IDEs can correctly analyse the machine_can.c file while the +// developer is writing it. + +typedef enum { + MP_CAN_STATE_STOPPED, + MP_CAN_STATE_ACTIVE, + MP_CAN_STATE_WARNING, + MP_CAN_STATE_PASSIVE, + MP_CAN_STATE_BUS_OFF, +} machine_can_state_t; + +typedef enum { + MP_CAN_MODE_NORMAL, + MP_CAN_MODE_SLEEP, + MP_CAN_MODE_LOOPBACK, + MP_CAN_MODE_SILENT, + MP_CAN_MODE_SILENT_LOOPBACK, + MP_CAN_MODE_MAX, +} machine_can_mode_t; + +// CAN IRQ Flags +// (currently the same for all ports) +#define MP_CAN_IRQ_TX (1 << 0) +#define MP_CAN_IRQ_RX (1 << 1) +#define MP_CAN_IRQ_TX_FAILED (1 << 2) +#define MP_CAN_IRQ_STATE (1 << 3) + +// Transmit buffer index is encoded into the irq().flags() response for MP_CAN_IRQ_TX +#define MP_CAN_IRQ_IDX_SHIFT 16 +#define MP_CAN_IRQ_IDX_MASK 0xFF + +#if MICROPY_HW_ENABLE_FDCAN +#define MP_CAN_MAX_LEN 64 +#else +#define MP_CAN_MAX_LEN 8 +#endif + +struct machine_can_port; + +// These values appear in the same order as the result of CAN.get_counters() +typedef struct { + mp_uint_t tec; + mp_uint_t rec; + mp_uint_t num_warning; // Number of "Error Warning" transitions + mp_uint_t num_passive; // Number of "Error Passive" transitions + mp_uint_t num_bus_off; // Number of "Bus-Off" transitions + mp_uint_t tx_pending; + mp_uint_t rx_pending; + mp_uint_t rx_overruns; +} machine_can_counters_t; + +typedef struct _machine_can_obj_t { + mp_obj_base_t base; + mp_uint_t can_idx; + + // Timing register settings + byte tseg1; + byte tseg2; + byte brp; + byte sjw; + + machine_can_mode_t mode; + + mp_irq_obj_t *mp_irq_obj; + uint16_t mp_irq_trigger; + mp_uint_t rx_error_flags; + + // Assumed some of these counters are updated from different port ISRs, etc. and some + // are updated by calling machine_can_port_update_counters() + machine_can_counters_t counters; + + struct machine_can_port *port; +} machine_can_obj_t; + +// Indexes for recv result list +typedef enum { + RECV_ARG_ID, + RECV_ARG_DATA, + RECV_ARG_FLAGS, + RECV_ARG_ERRORS, + RECV_ARG_LEN, // Overall length, not an index +} recv_arg_idx_t; + +#define CAN_STD_ID_MASK 0x7FF +#define CAN_EXT_ID_MASK 0x1fffffff + +// CAN Message Flags +#define CAN_MSG_FLAG_RTR (1 << 0) +#define CAN_MSG_FLAG_EXT_ID (1 << 1) +#define CAN_MSG_FLAG_FD_F (1 << 2) +#define CAN_MSG_FLAG_BRS (1 << 3) +#define CAN_MSG_FLAG_UNORDERED (1 << 4) + +// CAN recv() Error Flags +#define CAN_RECV_ERR_FULL (1 << 0) +#define CAN_RECV_ERR_OVERRUN (1 << 1) +#define CAN_RECV_ERR_ESI (1 << 2) + +// The port must provide implementations of these low-level CAN functions +static int machine_can_port_f_clock(const machine_can_obj_t *self); + +static bool machine_can_port_supports_mode(const machine_can_obj_t *self, machine_can_mode_t mode); + +static void machine_can_port_clear_filters(machine_can_obj_t *self); + +static mp_uint_t machine_can_port_max_data_len(mp_uint_t flags); + +// The extmod layer calls this function in a loop with incrementing filter_idx +// values. It's up to the port how to apply the filters from here, and to raise +// an exception if there are too many. +// +// If the CAN_FILTERS_STD_EXT_SEPARATE flag is set to 1, filter_idx will +// enumerate standard id filters separately to extended id filters (the +// CAN_MSG_FLAG_EXT_ID bit in 'flags' differentiates the type). +static void machine_can_port_set_filter(machine_can_obj_t *self, int filter_idx, mp_uint_t can_id, mp_uint_t mask, mp_uint_t flags); + +// Update interrupt configuration based on the new contents of 'self' +static void machine_can_update_irqs(machine_can_obj_t *self); + +// Return the irq().flags() result. Calling this function may also update the hardware state machine. +static mp_uint_t machine_can_port_irq_flags(machine_can_obj_t *self); + +static void machine_can_port_init(machine_can_obj_t *self); + +static void machine_can_port_deinit(machine_can_obj_t *self); + +static mp_int_t machine_can_port_send(machine_can_obj_t *self, mp_uint_t id, const byte *data, size_t data_len, mp_uint_t flags); + +static bool machine_can_port_cancel_send(machine_can_obj_t *self, mp_uint_t idx); + +static bool machine_can_port_recv(machine_can_obj_t *self, void *data, size_t *dlen, mp_uint_t *id, mp_uint_t *flags, mp_uint_t *errors); + +static machine_can_state_t machine_can_port_get_state(machine_can_obj_t *self); + +static void machine_can_port_restart(machine_can_obj_t *self); + +// Updates values in self->counters (which counters are updated by this function versus from ISRs and the like +// is port specific +static void machine_can_port_update_counters(machine_can_obj_t *self); + +// Hook for port to fill in the final item of the get_timings() result list with controller-specific values +static mp_obj_t machine_can_port_get_additional_timings(machine_can_obj_t *self, mp_obj_t optional_arg); + +// This function is only optionally defined by the port. If macro CAN_PORT_PRINT_FUNCTION is not set +// then a default machine_can_print function will be used. +static void machine_can_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind); + +#endif // MICROPY_INCLUDED_EXTMOD_MACHINE_CAN_PORT_H diff --git a/extmod/machine_pwm.c b/extmod/machine_pwm.c index 0c1834886c4df..eb68bd9671c64 100644 --- a/extmod/machine_pwm.c +++ b/extmod/machine_pwm.c @@ -50,7 +50,7 @@ static void mp_machine_pwm_duty_set_ns(machine_pwm_obj_t *self, mp_int_t duty_ns #include MICROPY_PY_MACHINE_PWM_INCLUDEFILE static mp_obj_t machine_pwm_init(size_t n_args, const mp_obj_t *args, mp_map_t *kw_args) { - mp_machine_pwm_init_helper(args[0], n_args - 1, args + 1, kw_args); + mp_machine_pwm_init_helper(MP_OBJ_TO_PTR(args[0]), n_args - 1, args + 1, kw_args); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(machine_pwm_init_obj, 1, machine_pwm_init); diff --git a/extmod/modlwip.c b/extmod/modlwip.c index 4a72a05d84b5a..0e2df8f32903a 100644 --- a/extmod/modlwip.c +++ b/extmod/modlwip.c @@ -384,7 +384,7 @@ static void lwip_socket_free_incoming(lwip_socket_obj_t *socket, bool free_queue pbuf_free(socket->incoming.tcp.pbuf); socket->incoming.tcp.pbuf = NULL; } - } else { + } else if (socket->incoming.udp_raw.array != NULL) { for (size_t i = 0; i < LWIP_INCOMING_PACKET_QUEUE_LEN; ++i) { lwip_incoming_packet_t *slot = &socket->incoming.udp_raw.array[i]; if (slot->pbuf != NULL) { @@ -463,6 +463,8 @@ static void udp_raw_incoming(lwip_socket_obj_t *socket, struct pbuf *p, const ip slot->peer_addr = *addr; slot->peer_port = port; socket->incoming.udp_raw.iput = (socket->incoming.udp_raw.iput + 1) % LWIP_INCOMING_PACKET_QUEUE_LEN; + // Notify user callback of the new packet + exec_user_callback(socket); } } @@ -938,7 +940,12 @@ static void lwip_socket_print(const mp_print_t *print, mp_obj_t self_in, mp_prin static mp_obj_t lwip_socket_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { mp_arg_check_num(n_args, n_kw, 0, 4, false); + // Once the socket is allocated it must be in a valid state to be finalised: + // - `incoming.udp_raw.array` is NULL or a valid heap pointer + // - `pcb` is NULL or a valid lwIP PCB that has been fully initialised lwip_socket_obj_t *socket = mp_obj_malloc_with_finaliser(lwip_socket_obj_t, &lwip_socket_type); + socket->pcb.tcp = NULL; + socket->incoming.udp_raw.array = NULL; socket->timeout = -1; socket->recv_offset = 0; socket->domain = MOD_NETWORK_AF_INET; @@ -946,10 +953,16 @@ static mp_obj_t lwip_socket_make_new(const mp_obj_type_t *type, size_t n_args, s socket->callback = MP_OBJ_NULL; socket->state = STATE_NEW; + // Parse given arguments. + uint8_t socket_proto = 0; + (void)socket_proto; if (n_args >= 1) { socket->domain = mp_obj_get_int(args[0]); if (n_args >= 2) { socket->type = mp_obj_get_int(args[1]); + if (n_args >= 3) { + socket_proto = mp_obj_get_int(args[2]); + } } } @@ -963,18 +976,17 @@ static mp_obj_t lwip_socket_make_new(const mp_obj_type_t *type, size_t n_args, s #if MICROPY_PY_LWIP_SOCK_RAW case MOD_NETWORK_SOCK_RAW: #endif + socket->incoming.udp_raw.array = m_new0(lwip_incoming_packet_t, LWIP_INCOMING_PACKET_QUEUE_LEN); if (socket->type == MOD_NETWORK_SOCK_DGRAM) { socket->pcb.udp = udp_new(); } #if MICROPY_PY_LWIP_SOCK_RAW else { - mp_int_t proto = n_args <= 2 ? 0 : mp_obj_get_int(args[2]); - socket->pcb.raw = raw_new(proto); + socket->pcb.raw = raw_new(socket_proto); } #endif socket->incoming.udp_raw.iget = 0; socket->incoming.udp_raw.iput = 0; - socket->incoming.udp_raw.array = m_new0(lwip_incoming_packet_t, LWIP_INCOMING_PACKET_QUEUE_LEN); break; default: mp_raise_OSError(MP_EINVAL); diff --git a/extmod/modmachine.c b/extmod/modmachine.c index 28b60683b1e58..60107a9020cd7 100644 --- a/extmod/modmachine.c +++ b/extmod/modmachine.c @@ -213,6 +213,9 @@ static const mp_rom_map_elem_t machine_module_globals_table[] = { #if MICROPY_PY_MACHINE_ADC_BLOCK { MP_ROM_QSTR(MP_QSTR_ADCBlock), MP_ROM_PTR(&machine_adc_block_type) }, #endif + #if MICROPY_PY_MACHINE_CAN + { MP_ROM_QSTR(MP_QSTR_CAN), MP_ROM_PTR(&machine_can_type) }, + #endif #if MICROPY_PY_MACHINE_DAC { MP_ROM_QSTR(MP_QSTR_DAC), MP_ROM_PTR(&machine_dac_type) }, #endif diff --git a/extmod/modmachine.h b/extmod/modmachine.h index 53660a7b7ab64..18ee229dc79bd 100644 --- a/extmod/modmachine.h +++ b/extmod/modmachine.h @@ -203,6 +203,7 @@ extern const machine_mem_obj_t machine_mem32_obj; // is provided by a port. extern const mp_obj_type_t machine_adc_type; extern const mp_obj_type_t machine_adc_block_type; +extern const mp_obj_type_t machine_can_type; extern const mp_obj_type_t machine_i2c_type; extern const mp_obj_type_t machine_i2c_target_type; extern const mp_obj_type_t machine_i2s_type; diff --git a/extmod/nimble/modbluetooth_nimble.c b/extmod/nimble/modbluetooth_nimble.c index 5e7030e36fab4..6b000f77f8a53 100644 --- a/extmod/nimble/modbluetooth_nimble.c +++ b/extmod/nimble/modbluetooth_nimble.c @@ -618,7 +618,17 @@ int mp_bluetooth_init(void) { // Initialise NimBLE memory and data structures. DEBUG_printf("mp_bluetooth_init: nimble_port_init\n"); + + // On some ports `nimble_port_init` may return an error code indicating a + // failed initialisation. + #if MICROPY_BLUETOOTH_NIMBLE_PORT_INIT_RETURNS_ERROR + if (nimble_port_init() < 0) { + mp_bluetooth_deinit(); + return MP_EIO; + } + #else nimble_port_init(); + #endif ble_hs_cfg.reset_cb = reset_cb; ble_hs_cfg.sync_cb = sync_cb; diff --git a/lib/cyw43-driver b/lib/cyw43-driver index dd7568229f3bf..055d64274b014 160000 --- a/lib/cyw43-driver +++ b/lib/cyw43-driver @@ -1 +1 @@ -Subproject commit dd7568229f3bf7a37737b9e1ef250c26efe75b23 +Subproject commit 055d64274b014dd7b1c2fc94d26e8a18face7124 diff --git a/lib/micropython-lib b/lib/micropython-lib index 6ae440a8a1442..8380c7bb8f9e5 160000 --- a/lib/micropython-lib +++ b/lib/micropython-lib @@ -1 +1 @@ -Subproject commit 6ae440a8a144233e6e703f6759b7e7a0afaa37a4 +Subproject commit 8380c7bb8f9e5e5260e9539156742925e00366b2 diff --git a/ports/alif/irq.h b/ports/alif/irq.h index 86b739795c173..0aa9774748578 100644 --- a/ports/alif/irq.h +++ b/ports/alif/irq.h @@ -43,11 +43,14 @@ #define IRQ_PRI_MHU NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 0, 0) #define IRQ_PRI_QUIET_TIMING NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 1, 0) #define IRQ_PRI_UART_REPL NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 1, 0) +#define IRQ_PRI_DMA NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 2, 0) #define IRQ_PRI_ADC NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 3, 0) #define IRQ_PRI_CSI NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 5, 0) #define IRQ_PRI_USB NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 7, 0) #define IRQ_PRI_HWSEM NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 8, 0) +#define IRQ_PRI_NPU NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 9, 0) #define IRQ_PRI_GPU NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 10, 0) +#define IRQ_PRI_PDM NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 10, 0) #define IRQ_PRI_GPIO NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 50, 0) #define IRQ_PRI_I2C NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 60, 0) #define IRQ_PRI_RTC NVIC_EncodePriority(NVIC_PRIORITYGROUP_7, 100, 0) diff --git a/ports/esp32/CMakeLists.txt b/ports/esp32/CMakeLists.txt index ee062be203d40..c739368c55027 100644 --- a/ports/esp32/CMakeLists.txt +++ b/ports/esp32/CMakeLists.txt @@ -63,19 +63,20 @@ set(SDKCONFIG_DEFAULTS ${CMAKE_BINARY_DIR}/sdkconfig.combined) include($ENV{IDF_PATH}/tools/cmake/project.cmake) # Generate individual dependencies.lock files based on chip target -idf_build_set_property(DEPENDENCIES_LOCK lockfiles/dependencies.lock.${IDF_TARGET}) +set(LOCKFILE_PATH lockfiles/dependencies.lock.${IDF_TARGET}) +idf_build_set_property(DEPENDENCIES_LOCK ${LOCKFILE_PATH}) # Define the project. project(micropython) # Check for lockfile changes and either warn or error depending on build type message("Checking lockfile contents...") -execute_process(COMMAND git diff --exit-code lockfiles/ WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} +execute_process(COMMAND git diff --exit-code ${LOCKFILE_PATH} WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} RESULT_VARIABLE RES) if (RES) # Maintainer builds (CI or autobuild runs) should fail if this has happened if($ENV{MICROPY_MAINTAINER_BUILD}) - message(FATAL_ERROR "Failing build as lockfiles are dirty (see above). Check that ESP-IDF versions match.") + message(FATAL_ERROR "Failing build as lockfile is dirty (see above). Check that ESP-IDF versions match.") else() message(WARNING "Component lockfile contents have changed (see above). This may be due to building with a different ESP-IDF version. Please mention this output if reporting an issue with MicroPython.") endif() diff --git a/ports/esp32/README.md b/ports/esp32/README.md index b5cd1c2a8c6bb..85e13904054d1 100644 --- a/ports/esp32/README.md +++ b/ports/esp32/README.md @@ -53,7 +53,13 @@ build environment and toolchains needed to build the firmware. The ESP-IDF changes quickly and MicroPython only supports certain versions. The current recommended version of ESP-IDF for MicroPython is v5.5.1. MicroPython -also supports v5.2, v5.2.2, v5.3, v5.4, v5.4.1 and v5.4.2. +also supports v5.3, v5.4, v5.4.1 and v5.4.2. + + To install the ESP-IDF the full instructions can be found at the [Espressif Getting Started guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html#installation-step-by-step). diff --git a/ports/esp32/boards/SEEED_XIAO_ESP32C6/board.json b/ports/esp32/boards/SEEED_XIAO_ESP32C6/board.json new file mode 100644 index 0000000000000..81e15d4a3e6cf --- /dev/null +++ b/ports/esp32/boards/SEEED_XIAO_ESP32C6/board.json @@ -0,0 +1,24 @@ +{ + "deploy": [ + "../deploy_nativeusb.md" + ], + "deploy_options": { + "flash_offset": "0" + }, + "docs": "", + "features": [ + "Battery Charging", + "BLE", + "External Flash", + "WiFi", + "USB", + "USB-C" + ], + "images": [ + "seeed_xiao_esp32c6.jpg" + ], + "mcu": "esp32c6", + "product": "XIAO ESP32C6", + "url": "https://www.seeedstudio.com/Seeed-Studio-XIAO-ESP32C6-p-5884.html", + "vendor": "Seeed Studio" +} diff --git a/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.cmake b/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.cmake new file mode 100644 index 0000000000000..48946f7094530 --- /dev/null +++ b/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.cmake @@ -0,0 +1,8 @@ +set(IDF_TARGET esp32c6) + +set(SDKCONFIG_DEFAULTS + boards/sdkconfig.base + boards/sdkconfig.riscv + boards/sdkconfig.c6 + boards/sdkconfig.ble +) diff --git a/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.h b/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.h new file mode 100644 index 0000000000000..a85f13899868c --- /dev/null +++ b/ports/esp32/boards/SEEED_XIAO_ESP32C6/mpconfigboard.h @@ -0,0 +1,9 @@ +#define MICROPY_HW_BOARD_NAME "Seeed XIAO ESP32C6" +#define MICROPY_HW_MCU_NAME "ESP32C6" + +#define MICROPY_HW_I2C0_SCL (23) +#define MICROPY_HW_I2C0_SDA (22) + +#define MICROPY_HW_SPI1_MOSI (18) +#define MICROPY_HW_SPI1_MISO (20) +#define MICROPY_HW_SPI1_SCK (19) diff --git a/ports/esp32/boards/SEEED_XIAO_ESP32C6/pins.csv b/ports/esp32/boards/SEEED_XIAO_ESP32C6/pins.csv new file mode 100644 index 0000000000000..aca4bd9948919 --- /dev/null +++ b/ports/esp32/boards/SEEED_XIAO_ESP32C6/pins.csv @@ -0,0 +1,22 @@ +D0,GPIO0 +D1,GPIO1 +D2,GPIO2 +D3,GPIO21 +D4,GPIO22 +D5,GPIO23 +D6,GPIO16 +D7,GPIO17 +D8,GPIO19 +D9,GPIO20 +D10,GPIO18 +A0,GPIO0 +A1,GPIO1 +A2,GPIO2 +LED,GPIO15 +MTDO,GPIO7 +MTDI,GPIO5 +MTCK,GPIO6 +MTMS,GPIO4 +BOOT,GPIO9 +RF_SEL,GPIO14 +RF_POWER,GPIO3 diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/board.json b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/board.json new file mode 100644 index 0000000000000..2be28659b9e0b --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/board.json @@ -0,0 +1,29 @@ +{ + "deploy": [ + "../deploy.md" + ], + "deploy_options": { + "flash_offset": "0x2000" + }, + "docs": "", + "features": [ + "BLE", + "Battery Charging", + "External Flash", + "External RAM", + "Feather", + "JST-SH", + "RGB LED", + "USB-C", + "WiFi", + "microSD" + ], + "images": [ + "30678-Thing-Plus-ESP32-C5-Feature.jpg" + ], + "mcu": "esp32c5", + "product": "Thing Plus ESP32-C5", + "thumbnail": "", + "url": "https://www.sparkfun.com/sparkfun-thing-plus-esp32-c5.html", + "vendor": "SparkFun" +} diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/manifest.py b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/manifest.py new file mode 100644 index 0000000000000..73446ecac9892 --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/manifest.py @@ -0,0 +1,2 @@ +include("$(PORT_DIR)/boards/manifest.py") +require("sdcard") diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.cmake b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.cmake new file mode 100644 index 0000000000000..216cd664dbe70 --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.cmake @@ -0,0 +1,13 @@ +set(IDF_TARGET esp32c5) + +set(SDKCONFIG_DEFAULTS + boards/sdkconfig.base + boards/sdkconfig.riscv + boards/sdkconfig.ble + boards/sdkconfig.240mhz + boards/sdkconfig.free_ram + boards/sdkconfig.spiram + boards/SPARKFUN_THINGPLUS_ESP32C5/sdkconfig.board +) + +set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py) diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.h b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.h new file mode 100644 index 0000000000000..7c892399da94f --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/mpconfigboard.h @@ -0,0 +1,11 @@ +// Board specific definitions for the SparkFun Thing Plus ESP32-C5. + +#define MICROPY_HW_BOARD_NAME "SparkFun Thing Plus ESP32-C5" +#define MICROPY_HW_MCU_NAME "ESP32C5" + +#define MICROPY_HW_I2C0_SCL (24) +#define MICROPY_HW_I2C0_SDA (23) + +#define MICROPY_HW_SPI1_SCK (10) +#define MICROPY_HW_SPI1_MOSI (8) +#define MICROPY_HW_SPI1_MISO (9) diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/pins.csv b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/pins.csv new file mode 100644 index 0000000000000..aec57725e78c0 --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/pins.csv @@ -0,0 +1,18 @@ +ALERT,GPIO0 +BAT_STAT,GPIO6 +SD_DET,GPIO7 +MOSI,GPIO8 +PICO,GPIO8 +MISO,GPIO9 +POCI,GPIO9 +SCK,GPIO10 +TX,GPIO11 +RX,GPIO12 +SDA,GPIO23 +SCL,GPIO24 +CS,GPIO25 +LP,GPIO26 +LED,GPIO27 +RGB_LED,GPIO27 +NEOPIXEL,GPIO27 +BUTTON,GPIO28 diff --git a/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/sdkconfig.board b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/sdkconfig.board new file mode 100644 index 0000000000000..369330682f91a --- /dev/null +++ b/ports/esp32/boards/SPARKFUN_THINGPLUS_ESP32C5/sdkconfig.board @@ -0,0 +1,2 @@ +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y diff --git a/ports/esp32/esp32_common.cmake b/ports/esp32/esp32_common.cmake index d6a4aedb85c68..86ea1aaf4ab27 100644 --- a/ports/esp32/esp32_common.cmake +++ b/ports/esp32/esp32_common.cmake @@ -169,7 +169,6 @@ list(APPEND IDF_COMPONENTS esp_app_format esp_mm esp_common - esp_driver_touch_sens esp_eth esp_event esp_hw_support @@ -198,6 +197,11 @@ list(APPEND IDF_COMPONENTS vfs ) +if($ENV{IDF_VERSION} VERSION_GREATER_EQUAL "5.4") + list(APPEND IDF_COMPONENTS + esp_driver_touch_sens) +endif() + # Provide the default LD fragment if not set if (MICROPY_USER_LDFRAGMENTS) set(MICROPY_LDFRAGMENTS ${MICROPY_USER_LDFRAGMENTS}) @@ -277,14 +281,6 @@ target_compile_options(${MICROPY_TARGET} PUBLIC target_include_directories(${MICROPY_TARGET} PUBLIC ${IDF_PATH}/components/bt/host/nimble/nimble ) -if (IDF_VERSION VERSION_LESS "5.3") -# Additional include directories needed for private RMT header. -# IDF 5.x versions before 5.3.1 - message(STATUS "Using private rmt headers for ${IDF_VERSION}") - target_include_directories(${MICROPY_TARGET} PRIVATE - ${IDF_PATH}/components/driver/rmt - ) -endif() # Add additional extmod and usermod components. if (MICROPY_PY_BTREE) diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32 b/ports/esp32/lockfiles/dependencies.lock.esp32 index 8ba25c77011c2..e242478083e3d 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32 @@ -30,6 +30,6 @@ direct_dependencies: - espressif/lan867x - espressif/mdns - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32c2 b/ports/esp32/lockfiles/dependencies.lock.esp32c2 index 8a366af342369..a6bbf8d61c837 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32c2 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32c2 @@ -16,6 +16,6 @@ dependencies: direct_dependencies: - espressif/mdns - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32c2 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32c3 b/ports/esp32/lockfiles/dependencies.lock.esp32c3 index 3aa99692d9f35..4f99727f4e8f0 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32c3 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32c3 @@ -16,6 +16,6 @@ dependencies: direct_dependencies: - espressif/mdns - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32c3 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32c5 b/ports/esp32/lockfiles/dependencies.lock.esp32c5 index 2fb130b8e5282..ac6b1d99d915d 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32c5 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32c5 @@ -16,6 +16,6 @@ dependencies: direct_dependencies: - espressif/mdns - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32c5 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32c6 b/ports/esp32/lockfiles/dependencies.lock.esp32c6 index c81806909a631..8dfb4d77bc26e 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32c6 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32c6 @@ -16,6 +16,6 @@ dependencies: direct_dependencies: - espressif/mdns - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32c6 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32p4 b/ports/esp32/lockfiles/dependencies.lock.esp32p4 index aea6ec2cc3607..4582830b5b902 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32p4 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32p4 @@ -88,6 +88,6 @@ direct_dependencies: - espressif/mdns - espressif/tinyusb - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32p4 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32s2 b/ports/esp32/lockfiles/dependencies.lock.esp32s2 index 8717181c10ac7..417b999f85a86 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32s2 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32s2 @@ -32,6 +32,6 @@ direct_dependencies: - espressif/mdns - espressif/tinyusb - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32s2 version: 2.0.0 diff --git a/ports/esp32/lockfiles/dependencies.lock.esp32s3 b/ports/esp32/lockfiles/dependencies.lock.esp32s3 index 0b8b8e92bd57b..f0376c9a8d60a 100644 --- a/ports/esp32/lockfiles/dependencies.lock.esp32s3 +++ b/ports/esp32/lockfiles/dependencies.lock.esp32s3 @@ -32,6 +32,6 @@ direct_dependencies: - espressif/mdns - espressif/tinyusb - idf -manifest_hash: 482087bc40f0e187795a9ef9ad08ef15a585b4cdabc296c715a9d19284622150 +manifest_hash: 40b684ab14058130e675aab422296e4ad9d87ee39c5aa46d7b3df55c245e14f5 target: esp32s3 version: 2.0.0 diff --git a/ports/esp32/machine_bitstream.c b/ports/esp32/machine_bitstream.c index 60addcc15b635..d44f9e71a40ae 100644 --- a/ports/esp32/machine_bitstream.c +++ b/ports/esp32/machine_bitstream.c @@ -95,9 +95,6 @@ static void IRAM_ATTR machine_bitstream_high_low_bitbang(mp_hal_pin_obj_t pin, u /******************************************************************************/ // RMT implementation -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) -#include "rmt_private.h" -#endif #include "driver/rmt_tx.h" #include "driver/rmt_encoder.h" @@ -159,11 +156,7 @@ static bool machine_bitstream_high_low_rmt(mp_hal_pin_obj_t pin, uint32_t *timin // Disable and release channel. check_esp_err(rmt_del_encoder(encoder)); rmt_disable(channel); - #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) - channel->del(channel); - #else rmt_del_channel(channel); - #endif // Cancel RMT output to GPIO pin. esp_rom_gpio_connect_out_signal(pin, SIG_GPIO_OUT_IDX, false, false); diff --git a/ports/esp32/machine_timer.c b/ports/esp32/machine_timer.c index b9cd80f48efed..d953f324b9cd7 100644 --- a/ports/esp32/machine_timer.c +++ b/ports/esp32/machine_timer.c @@ -183,7 +183,15 @@ void machine_timer_enable(machine_timer_obj_t *self) { } timer_ll_enable_counter(self->hal_context.dev, self->index, false); + + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) esp_clk_tree_enable_src(TIMER_CLK_SRC, true); + #elif TIMER_CLK_SRC != SOC_MOD_CLK_APB + // esp_clk_tree_enable_src() is only required on some newer chips where timer + // source clock may not be enabled by default + #error "This chip requires ESP-IDF v5.4 or newer for working Timer." + #endif + #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0) timer_ll_set_clock_source(self->hal_context.dev, self->index, TIMER_CLK_SRC); timer_ll_enable_clock(self->hal_context.dev, self->index, true); diff --git a/ports/esp32/machine_touchpad.c b/ports/esp32/machine_touchpad.c index 88b34d64ff070..61a3bcf83db89 100644 --- a/ports/esp32/machine_touchpad.c +++ b/ports/esp32/machine_touchpad.c @@ -31,14 +31,6 @@ #if SOC_TOUCH_SENSOR_SUPPORTED -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) -#if SOC_TOUCH_VERSION_1 -#define SOC_TOUCH_SENSOR_VERSION (1) -#elif SOC_TOUCH_VERSION_2 -#define SOC_TOUCH_SENSOR_VERSION (2) -#endif -#endif - #if SOC_TOUCH_SENSOR_VERSION == 1 // ESP32 only #include "driver/touch_pad.h" #elif SOC_TOUCH_SENSOR_VERSION == 2 // most ESP32 diff --git a/ports/esp32/main/idf_component.yml b/ports/esp32/main/idf_component.yml index 176e29c3c4862..47ae737b39cc0 100644 --- a/ports/esp32/main/idf_component.yml +++ b/ports/esp32/main/idf_component.yml @@ -19,6 +19,5 @@ dependencies: version: "~1.0.0" rules: - if: "target == esp32" - - if: "idf_version >=5.3" idf: - version: ">=5.2.0" + version: ">=5.3.0" diff --git a/ports/esp32/modespnow.c b/ports/esp32/modespnow.c index 725374ea77c55..c7a9c6eb1b913 100644 --- a/ports/esp32/modespnow.c +++ b/ports/esp32/modespnow.c @@ -423,10 +423,7 @@ static mp_obj_t espnow_recvinto(size_t n_args, const mp_obj_t *args) { mp_int_t timeout_ms = ((n_args > 2 && args[2] != mp_const_none) ? mp_obj_get_int(args[2]) : self->recv_timeout_ms); - mp_obj_list_t *list = MP_OBJ_TO_PTR(args[1]); - if (!mp_obj_is_type(list, &mp_type_list) || list->len < 2) { - mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recvinto(): Invalid argument")); - } + mp_obj_list_t *list = mp_obj_list_ensure(args[1], 2); mp_obj_array_t *msg = MP_OBJ_TO_PTR(list->items[1]); if (mp_obj_is_type(msg, &mp_type_bytearray)) { msg->len += msg->free; // Make all the space in msg array available diff --git a/ports/esp32/modnetwork.h b/ports/esp32/modnetwork.h index a68db41a3e353..68260dd19a3bb 100644 --- a/ports/esp32/modnetwork.h +++ b/ports/esp32/modnetwork.h @@ -29,8 +29,8 @@ #include "esp_wifi_types.h" #include "esp_netif.h" -// lan867x component requires newer IDF version -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) && CONFIG_IDF_TARGET_ESP32 +// lan867x component requires Original ESP32 +#if CONFIG_IDF_TARGET_ESP32 #define PHY_LAN867X_ENABLED (1) #else #define PHY_LAN867X_ENABLED (0) diff --git a/ports/esp32/mpconfigport.h b/ports/esp32/mpconfigport.h index dd8f89c4bc410..97c44c8076f47 100644 --- a/ports/esp32/mpconfigport.h +++ b/ports/esp32/mpconfigport.h @@ -108,6 +108,7 @@ #define MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING (1) #define MICROPY_BLUETOOTH_NIMBLE (1) #define MICROPY_BLUETOOTH_NIMBLE_BINDINGS_ONLY (1) +#define MICROPY_BLUETOOTH_NIMBLE_PORT_INIT_RETURNS_ERROR (1) #endif // MICROPY_PY_BLUETOOTH #define MICROPY_PY_RANDOM_SEED_INIT_FUNC (esp_random()) @@ -140,8 +141,8 @@ #define MICROPY_PY_MACHINE_I2C (1) #define MICROPY_PY_MACHINE_I2C_TRANSFER_WRITE1 (1) #ifndef MICROPY_PY_MACHINE_I2C_TARGET -// I2C target hardware is limited on ESP32 (eg read event comes after the read) so we only support newer SoCs. -#define MICROPY_PY_MACHINE_I2C_TARGET (SOC_I2C_SUPPORT_SLAVE && !CONFIG_IDF_TARGET_ESP32) +// I2C target hardware is limited on ESP32 (eg read event comes after the read) so we only support newer SoCs & ESP-IDF v5.4 or newer +#define MICROPY_PY_MACHINE_I2C_TARGET (SOC_I2C_SUPPORT_SLAVE && !CONFIG_IDF_TARGET_ESP32 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)) #define MICROPY_PY_MACHINE_I2C_TARGET_INCLUDEFILE "ports/esp32/machine_i2c_target.c" #define MICROPY_PY_MACHINE_I2C_TARGET_MAX (2) #endif diff --git a/ports/esp32/network_wlan.c b/ports/esp32/network_wlan.c index 07b16e91cb941..5433bf862f109 100644 --- a/ports/esp32/network_wlan.c +++ b/ports/esp32/network_wlan.c @@ -769,9 +769,7 @@ static const mp_rom_map_elem_t wlan_if_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_SEC_WPA3_ENT_192), MP_ROM_INT(WIFI_AUTH_WPA3_ENT_192) }, { MP_ROM_QSTR(MP_QSTR_SEC_WPA3_EXT_PSK), MP_ROM_INT(WIFI_AUTH_WPA3_EXT_PSK) }, { MP_ROM_QSTR(MP_QSTR_SEC_WPA3_EXT_PSK_MIXED_MODE), MP_ROM_INT(WIFI_AUTH_WPA3_EXT_PSK_MIXED_MODE) }, - #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) { MP_ROM_QSTR(MP_QSTR_SEC_DPP), MP_ROM_INT(WIFI_AUTH_DPP) }, - #endif #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) { MP_ROM_QSTR(MP_QSTR_SEC_WPA3_ENT), MP_ROM_INT(WIFI_AUTH_WPA3_ENTERPRISE) }, { MP_ROM_QSTR(MP_QSTR_SEC_WPA2_WPA3_ENT), MP_ROM_INT(WIFI_AUTH_WPA2_WPA3_ENTERPRISE) }, @@ -797,8 +795,6 @@ _Static_assert(WIFI_AUTH_MAX == 17, "Synchronize WIFI_AUTH_XXX constants with th _Static_assert(WIFI_AUTH_MAX == 16, "Synchronize WIFI_AUTH_XXX constants with the ESP-IDF. Look at esp-idf/components/esp_wifi/include/esp_wifi_types_generic.h"); #elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) _Static_assert(WIFI_AUTH_MAX == 14, "Synchronize WIFI_AUTH_XXX constants with the ESP-IDF. Look at esp-idf/components/esp_wifi/include/esp_wifi_types_generic.h"); -#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0) -_Static_assert(WIFI_AUTH_MAX == 13, "Synchronize WIFI_AUTH_XXX constants with the ESP-IDF. Look at esp-idf/components/esp_wifi/include/esp_wifi_types.h"); #else #error "Error in macro logic, all supported versions should be covered." #endif diff --git a/ports/esp32/usb_serial_jtag.c b/ports/esp32/usb_serial_jtag.c index 2df7e20086283..86c89385fae81 100644 --- a/ports/esp32/usb_serial_jtag.c +++ b/ports/esp32/usb_serial_jtag.c @@ -36,9 +36,7 @@ #include "freertos/portmacro.h" // Number of bytes in the input buffer, and number of bytes for output chunking. -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) #define USB_SERIAL_JTAG_PACKET_SZ_BYTES (64) -#endif static DRAM_ATTR portMUX_TYPE rx_mux = portMUX_INITIALIZER_UNLOCKED; static uint8_t rx_buf[USB_SERIAL_JTAG_PACKET_SZ_BYTES]; diff --git a/ports/esp8266/modespnow.c b/ports/esp8266/modespnow.c index 91510a3f9df14..cbe9cdc4d4494 100644 --- a/ports/esp8266/modespnow.c +++ b/ports/esp8266/modespnow.c @@ -304,10 +304,7 @@ static mp_obj_t espnow_recvinto(size_t n_args, const mp_obj_t *args) { size_t timeout_ms = ((n_args > 2 && args[2] != mp_const_none) ? mp_obj_get_int(args[2]) : self->recv_timeout_ms); - mp_obj_list_t *list = MP_OBJ_TO_PTR(args[1]); - if (!mp_obj_is_type(list, &mp_type_list) || list->len < 2) { - mp_raise_ValueError(MP_ERROR_TEXT("ESPNow.recvinto(): Invalid argument")); - } + mp_obj_list_t *list = mp_obj_list_ensure(args[1], 2); mp_obj_array_t *msg = MP_OBJ_TO_PTR(list->items[1]); size_t msg_size = msg->len + msg->free; if (mp_obj_is_type(msg, &mp_type_bytearray)) { diff --git a/ports/rp2/boards/SEEED_XIAO_RP2040/board.json b/ports/rp2/boards/SEEED_XIAO_RP2040/board.json new file mode 100644 index 0000000000000..ae7e31be1d5cc --- /dev/null +++ b/ports/rp2/boards/SEEED_XIAO_RP2040/board.json @@ -0,0 +1,20 @@ +{ + "deploy": [ + "../deploy.md" + ], + "docs": "", + "features": [ + "Dual-core", + "External Flash", + "RGB LED", + "USB", + "USB-C" + ], + "images": [ + "seeedstudio_xiao_rp2040.jpg" + ], + "mcu": "rp2040", + "product": "XIAO RP2040", + "url": "https://www.seeedstudio.com/XIAO-RP2040-v1-0-p-5026.html", + "vendor": "Seeed Studio" +} diff --git a/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.cmake b/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.cmake new file mode 100644 index 0000000000000..a069458867d2e --- /dev/null +++ b/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.cmake @@ -0,0 +1,3 @@ +# cmake file for Seeed Studio XIAO RP204 + +set(PICO_BOARD "seeed_xiao_rp2040") diff --git a/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.h b/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.h new file mode 100644 index 0000000000000..4291f8c00c3f6 --- /dev/null +++ b/ports/rp2/boards/SEEED_XIAO_RP2040/mpconfigboard.h @@ -0,0 +1,19 @@ +// https://wiki.seeedstudio.com/XIAO-RP2040/ + +#define MICROPY_HW_BOARD_NAME "Seeed Studio XIAO RP2040" +#define MICROPY_HW_FLASH_STORAGE_BYTES (1408 * 1024) + +// No VID/PID defined for the Seeed XIAO RP2040 +// #define MICROPY_HW_USB_VID (0x) +// #define MICROPY_HW_USB_PID (0x) + +// I2C0 +#define MICROPY_HW_I2C0_SCL (7) +#define MICROPY_HW_I2C0_SDA (6) + +// SPI0 +#define MICROPY_HW_SPI0_SCK (2) +#define MICROPY_HW_SPI0_MOSI (3) +#define MICROPY_HW_SPI0_MISO (4) + +// UART0 is, by default, assigned the correct pins (TX=0, RX=1) diff --git a/ports/rp2/boards/SEEED_XIAO_RP2040/pins.csv b/ports/rp2/boards/SEEED_XIAO_RP2040/pins.csv new file mode 100644 index 0000000000000..7ae0ca50bb050 --- /dev/null +++ b/ports/rp2/boards/SEEED_XIAO_RP2040/pins.csv @@ -0,0 +1,21 @@ +D0,GPIO26 +D1,GPIO27 +D2,GPIO28 +D3,GPIO29 +D4,GPIO6 +D5,GPIO7 +D6,GPIO0 +D7,GPIO1 +D8,GPIO2 +D9,GPIO4 +D10,GPIO3 +A0,GPIO26 +A1,GPIO27 +A2,GPIO28 +A3,GPIO29 +NEOPIXEL_POWER,GPIO11 +NEOPIXEL,GPIO12 +LED_R,GPIO17 +LED_G,GPIO16 +LED_B,GPIO25 +LED,GPIO25 diff --git a/ports/rp2/rp2_dma.c b/ports/rp2/rp2_dma.c index 471cf24ea2ce5..bb935f3b4846c 100644 --- a/ports/rp2/rp2_dma.c +++ b/ports/rp2/rp2_dma.c @@ -427,6 +427,9 @@ static mp_obj_t rp2_dma_close(mp_obj_t self_in) { uint8_t channel = self->channel; if (channel != CHANNEL_CLOSED) { + // Disable channel IRQ + dma_channel_set_irq0_enabled(channel, false); + // Reset this channel's registers to their default values (zeros). dma_channel_config config = { .ctrl = 0 }; dma_channel_configure(channel, &config, NULL, NULL, 0, false); @@ -441,7 +444,6 @@ static mp_obj_t rp2_dma_close(mp_obj_t self_in) { if (irq) { irq->parent = MP_OBJ_NULL; irq->handler = MP_OBJ_NULL; - dma_channel_set_irq0_enabled(channel, false); } dma_channel_unclaim(channel); self->channel = CHANNEL_CLOSED; @@ -479,7 +481,8 @@ void rp2_dma_init(void) { } void rp2_dma_deinit(void) { - // Remove our interrupt handler. + // Disable and remove our interrupt handler. + irq_set_enabled(DMA_IRQ_0, false); irq_remove_handler(DMA_IRQ_0, rp2_dma_irq_handler); } diff --git a/ports/stm32/boards/ESPRUINO_PICO/mpconfigboard.h b/ports/stm32/boards/ESPRUINO_PICO/mpconfigboard.h index cfc46491e0763..e38f8e6f61d1d 100644 --- a/ports/stm32/boards/ESPRUINO_PICO/mpconfigboard.h +++ b/ports/stm32/boards/ESPRUINO_PICO/mpconfigboard.h @@ -3,6 +3,7 @@ #define MICROPY_EMIT_THUMB (0) #define MICROPY_EMIT_INLINE_THUMB (0) +#define MICROPY_OPT_COMPUTED_GOTO (0) #define MICROPY_PY_BUILTINS_COMPLEX (0) #define MICROPY_PY_SOCKET (0) #define MICROPY_PY_NETWORK (0) diff --git a/ports/stm32/boards/NUCLEO_G474RE/mpconfigboard.h b/ports/stm32/boards/NUCLEO_G474RE/mpconfigboard.h index 21619d6d0d1cb..6f4fd6df48e5a 100644 --- a/ports/stm32/boards/NUCLEO_G474RE/mpconfigboard.h +++ b/ports/stm32/boards/NUCLEO_G474RE/mpconfigboard.h @@ -86,3 +86,9 @@ #define MICROPY_HW_CAN1_NAME "FDCAN1" #define MICROPY_HW_CAN1_TX (pin_A12) // A12, B9, D1 #define MICROPY_HW_CAN1_RX (pin_A11) // A11, B8, D0 + +#define MICROPY_HW_CAN2_NAME "FDCAN2" +#define MICROPY_HW_CAN2_TX (pin_B13) // B13, B6 +#define MICROPY_HW_CAN2_RX (pin_B12) // B12, B5 + +// Note: This MCU has an FDCAN3 peripheral, but not currently supported in MicroPython diff --git a/ports/stm32/boards/NUCLEO_L432KC/mpconfigboard.h b/ports/stm32/boards/NUCLEO_L432KC/mpconfigboard.h index 0daf4877ff6c2..f38601c5a7d40 100644 --- a/ports/stm32/boards/NUCLEO_L432KC/mpconfigboard.h +++ b/ports/stm32/boards/NUCLEO_L432KC/mpconfigboard.h @@ -12,6 +12,7 @@ #define MICROPY_PY_STM (0) #define MICROPY_PY_PYB_LEGACY (0) #define MICROPY_PY_HEAPQ (0) +#define MICROPY_PY_FRAMEBUF (0) #define MICROPY_HW_ENABLE_RTC (1) #define MICROPY_HW_ENABLE_ADC (1) diff --git a/ports/stm32/boards/PYBD_SF2/f722_qspi.ld b/ports/stm32/boards/PYBD_SF2/f722_qspi.ld index 354c1919b41a3..8ada037d6e050 100644 --- a/ports/stm32/boards/PYBD_SF2/f722_qspi.ld +++ b/ports/stm32/boards/PYBD_SF2/f722_qspi.ld @@ -50,6 +50,7 @@ SECTIONS .text_ext : { . = ALIGN(4); + *py/emit*(.text.emit_inline_thumb_* .rodata.emit_inline_thumb_*) *lib/btstack/*(.text* .rodata*) *lib/mbedtls/*(.text* .rodata*) *lib/mynewt-nimble/*(.text* .rodata*) diff --git a/ports/stm32/boards/stm32f091_af.csv b/ports/stm32/boards/stm32f091_af.csv index 5e44697776b13..b8fba765e18b7 100644 --- a/ports/stm32/boards/stm32f091_af.csv +++ b/ports/stm32/boards/stm32f091_af.csv @@ -1,6 +1,6 @@ Port ,Pin ,AF0 ,AF1 ,AF2 ,AF3 ,AF4 ,AF5 ,AF6 ,AF7 ,,,,,,,,,ADC , ,AF0 ,AF1 ,AF2 ,AF3 ,AF4 ,AF5 ,AF6 ,AF7 ,,,,,,,,,ADC -PortA,PA0 , ,USART2_CTS ,TIM2_CH1_ETR ,TSC_G1_IO1,USART4_TX , , ,COMP1_OUT,,,,,,,,,ADC1_IN0 +PortA,PA0 , ,USART2_CTS ,TIM2_CH1/TIM2_ETR ,TSC_G1_IO1,USART4_TX , , ,COMP1_OUT,,,,,,,,,ADC1_IN0 PortA,PA1 ,EVENTOUT ,USART2_RTS ,TIM2_CH2 ,TSC_G1_IO2,USART4_RX ,TIM15_CH1N , , ,,,,,,,,,ADC1_IN1 PortA,PA2 ,TIM15_CH1 ,USART2_TX ,TIM2_CH3 ,TSC_G1_IO3, , , ,COMP2_OUT,,,,,,,,,ADC1_IN2 PortA,PA3 ,TIM15_CH2 ,USART2_RX ,TIM2_CH4 ,TSC_G1_IO4, , , , ,,,,,,,,,ADC1_IN3 diff --git a/ports/stm32/boards/stm32n657_af.csv b/ports/stm32/boards/stm32n657_af.csv index c02c658bd7330..b269d0497c646 100644 --- a/ports/stm32/boards/stm32n657_af.csv +++ b/ports/stm32/boards/stm32n657_af.csv @@ -1,7 +1,9 @@ Port ,Pin ,AF0 ,AF1 ,AF2 ,AF3 ,AF4 ,AF5 ,AF6 ,AF7 ,AF8 ,AF9 ,AF10 ,AF11 ,AF12 ,AF13 ,AF14 ,AF15 ,ADC , ,SYS ,LPTIM1/TIM1/2/16/17,LPTIM3/PDM_SAI1/TIM3/4/5/12/15,I3C1/LPTIM2/3/LPUART1/OCTOSPI/TIM1/8,CEC/DCMI/I2C1/2/3/4/LPTIM1/2/SPI1/I2S1/TIM15/USART1,CEC/I3C1/LPTIM1/SPI1/I2S1/SPI2/I2S2/SPI3/I2S3/SPI4/5/6,I2C4/OCTOSPI/SAI1/SPI3/I2S3/SPI4/UART4/12/USART10/USB_PD,SDMMC1/SPI2/I2S2/SPI3/I2S3/SPI6/UART7/8/12/USART1/2/3/6/10/11,LPUART1/SAI2/SDMMC1/SPI6/UART4/5/8,FDCAN1/2/FMC[NAND16]/FMC[NORmux]/FMC[NOR_RAM]/OCTOSPI/SDMMC2/TIM13/14,CRS/FMC[NAND16]/OCTOSPI/SAI2/SDMMC2/TIM8/USB_,ETH[MII/RMII]/FMC[NAND16]/OCTOSPI/SDMMC2/UART7/9/USB_PD,FMC[NAND16]/FMC[NORmux]/FMC[NOR_RAM]/FMC[SDRAM_16bit]/SDMMC1,DCMI/FMC[NAND16]/FMC[NORmux]/FMC[NOR_RAM]/LPTIM5,LPTIM3/4/5/6/TIM2/UART5,SYS ,ADC -PortA,PA0 , , , , , , , , , , , ,SDMMC2_CMD , , , , ,ADC12_INP0/ADC12_INN1 -PortA,PA3 , , , , , ,SPI5_NSS , , , , , , , , , , , +PortA,PA0 , ,TIM2_CH1 ,TIM5_CH1 ,TIM9_CH1 ,TIM15_BKIN , , , , , , ,SDMMC2_CMD , , , , ,ADC12_INP0/ADC12_INN1 +PortA,PA1 , ,TIM2_CH2 ,TIM5_CH2 ,LPTIM3_IN1 ,TIM15_CH1N , , , , , , , , , , , ,ADC12_INP1 +PortA,PA2 , ,TIM2_CH3 ,TIM5_CH3 ,LPTIM3_IN2 ,TIM15_CH1 , , , , , , , , , , , ,ADC12_INP14 +PortA,PA3 , ,TIM16_CH1 , , , ,SPI5_NSS , , , , , , , , , , , PortA,PA5 , , , , , , , , , , , , , , , , ,ADC2_INP18 PortA,PA8 , , , , , , , , , , , , , , , , ,ADC12_INP5 PortA,PA9 , , , , , , , , , , , , , , , , ,ADC12_INP10 diff --git a/ports/stm32/can.c b/ports/stm32/can.c index d43d73ad42d94..fd03e895f46a3 100644 --- a/ports/stm32/can.c +++ b/ports/stm32/can.c @@ -35,9 +35,81 @@ #if !MICROPY_HW_ENABLE_FDCAN -bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart) { +#if defined(MICROPY_HW_CAN3_TX) +#define NUM_CAN 3 +#elif defined(MICROPY_HW_CAN2_TX) +#define NUM_CAN 2 +#else +#define NUM_CAN 1 +#endif + +static int get_inst_index(CAN_HandleTypeDef *hcan) { + #if defined(MICROPY_HW_CAN1_TX) + if (hcan->Instance == CAN1) { + return 0; + } + #endif + #if defined(MICROPY_HW_CAN2_TX) + if (hcan->Instance == CAN2) { + return 1; + } + #endif + #if defined(MICROPY_HW_CAN3_TX) + if (hcan->Instance == CAN3) { + return 2; + } + #endif + assert(0); // Invalid hcan argument + return 0; +} + +static uint32_t get_tx_irqn(int can_id) { + switch (can_id) { + #if defined(MICROPY_HW_CAN1_TX) + case PYB_CAN_1: + return CAN1_TX_IRQn; + #endif + #if defined(MICROPY_HW_CAN2_TX) + case PYB_CAN_2: + return CAN2_TX_IRQn; + #endif + #if defined(MICROPY_HW_CAN3_TX) + case PYB_CAN_3: + return CAN3_TX_IRQn; + #endif + default: + return -1; + } +} + +int can_get_transmit_finished(CAN_HandleTypeDef *hcan, bool *is_success) { + CAN_TypeDef *instance = hcan->Instance; + uint32_t tsr = instance->TSR; + int result = -1; + + if (tsr & CAN_TSR_RQCP0) { + *is_success = tsr & CAN_TSR_TXOK0; + instance->TSR = CAN_TSR_RQCP0; // This also resets TXOK0, ALST0, TERR0 + result = 0; + } else if (tsr & CAN_TSR_RQCP1) { + *is_success = tsr & CAN_TSR_TXOK1; + instance->TSR = CAN_TSR_RQCP1; // This also resets TXOK1, ALST1, TERR1 + result = 1; + } else if (tsr & CAN_TSR_RQCP2) { + *is_success = tsr & CAN_TSR_TXOK2; + instance->TSR = CAN_TSR_RQCP2; // This also resets TXOK2, ALST2, TERR2 + result = 2; + } + + // Re-enable interrupts, to fire again if any transmit events outstanding + HAL_NVIC_EnableIRQ(get_tx_irqn(get_inst_index(hcan) + 1)); + + return result; +} + +bool can_init(CAN_HandleTypeDef *can, int can_id, can_tx_mode_t tx_mode, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart) { CAN_InitTypeDef *init = &can->Init; - init->Mode = mode << 4; // shift-left so modes fit in a small-int + init->Mode = mode; init->Prescaler = prescaler; init->SJW = ((sjw - 1) & 3) << 24; init->BS1 = ((bs1 - 1) & 0xf) << 16; @@ -49,8 +121,11 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca init->RFLM = DISABLE; init->TXFP = DISABLE; + (void)tx_mode; // This parameter is important for initialising FDCAN variant, but not bxCAN + CAN_TypeDef *CANx = NULL; - uint32_t sce_irq = 0; + uint32_t sce_irq; + uint32_t tx_irq = get_tx_irqn(can_id); const machine_pin_obj_t *pins[2]; switch (can_id) { @@ -100,13 +175,18 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca // init CANx can->Instance = CANx; - HAL_CAN_Init(can); + if (HAL_CAN_Init(can) != HAL_OK) { + return false; + } __HAL_CAN_ENABLE_IT(can, CAN_IT_ERR | CAN_IT_BOF | CAN_IT_EPV | CAN_IT_EWG); NVIC_SetPriority(sce_irq, IRQ_PRI_CAN); HAL_NVIC_EnableIRQ(sce_irq); + NVIC_SetPriority(tx_irq, IRQ_PRI_CAN); + HAL_NVIC_EnableIRQ(tx_irq); + return true; } @@ -140,6 +220,10 @@ void can_deinit(CAN_HandleTypeDef *can) { } } +uint32_t can_get_source_freq(void) { + return HAL_RCC_GetPCLK1Freq(); +} + void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { __HAL_CAN_DISABLE_IT(can, ((fifo == CAN_RX_FIFO0) ? (CAN_IT_FMP0 | CAN_IT_FF0 | CAN_IT_FOV0) : @@ -147,12 +231,29 @@ void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { } void can_enable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, bool enable_msg_received) { + uint32_t irq = 0; + if (can->Instance == CAN1) { + irq = (fifo == CAN_RX_FIFO0) ? CAN1_RX0_IRQn : CAN1_RX1_IRQn; + } + #if defined(CAN2) + else if (can->Instance == CAN2) { + irq = (fifo == CAN_RX_FIFO0) ? CAN2_RX0_IRQn : CAN2_RX1_IRQn; + } + #endif + #if defined(CAN3) + else { + irq = (fifo == CAN_RX_FIFO0) ? CAN3_RX0_IRQn : CAN3_RX1_IRQn; + } + #endif + NVIC_SetPriority(irq, IRQ_PRI_CAN); + HAL_NVIC_EnableIRQ(irq); + __HAL_CAN_ENABLE_IT(can, ((fifo == CAN_RX_FIFO0) ? ((enable_msg_received ? CAN_IT_FMP0 : 0) | CAN_IT_FF0 | CAN_IT_FOV0) : ((enable_msg_received ? CAN_IT_FMP1 : 0) | CAN_IT_FF1 | CAN_IT_FOV1))); } -void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, uint8_t bank) { +void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, uint8_t can2_start_bank) { CAN_FilterConfTypeDef filter; filter.FilterIdHigh = 0; @@ -164,7 +265,7 @@ void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, uint8_t bank) filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_16BIT; filter.FilterActivation = DISABLE; - filter.BankNumber = bank; + filter.BankNumber = can2_start_bank; HAL_CAN_ConfigFilter(can, &filter); } @@ -214,12 +315,46 @@ int can_receive(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, CanRxMsgTypeDef *msg return 0; // success } -// Lightly modified version of HAL CAN_Transmit to handle Timeout=0 correctly +static HAL_StatusTypeDef can_transmit_common(CAN_HandleTypeDef *hcan, int index, CanTxMsgTypeDef *txmsg, const uint8_t *data) { + hcan->pTxMsg = txmsg; + (void)data; // Not needed here, caller has set it up as &tx_msg->Data + + // Set up the Id + hcan->Instance->sTxMailBox[index].TIR &= CAN_TI0R_TXRQ; + if (hcan->pTxMsg->IDE == CAN_ID_STD) { + assert_param(IS_CAN_STDID(hcan->pTxMsg->StdId)); + hcan->Instance->sTxMailBox[index].TIR |= ((hcan->pTxMsg->StdId << 21) | \ + hcan->pTxMsg->RTR); + } else { + assert_param(IS_CAN_EXTID(hcan->pTxMsg->ExtId)); + hcan->Instance->sTxMailBox[index].TIR |= ((hcan->pTxMsg->ExtId << 3) | \ + hcan->pTxMsg->IDE | \ + hcan->pTxMsg->RTR); + } + + // Set up the DLC + hcan->pTxMsg->DLC &= (uint8_t)0x0000000F; + hcan->Instance->sTxMailBox[index].TDTR &= (uint32_t)0xFFFFFFF0; + hcan->Instance->sTxMailBox[index].TDTR |= hcan->pTxMsg->DLC; + + // Set up the data field + hcan->Instance->sTxMailBox[index].TDLR = (((uint32_t)hcan->pTxMsg->Data[3] << 24) | + ((uint32_t)hcan->pTxMsg->Data[2] << 16) | + ((uint32_t)hcan->pTxMsg->Data[1] << 8) | + ((uint32_t)hcan->pTxMsg->Data[0])); + hcan->Instance->sTxMailBox[index].TDHR = (((uint32_t)hcan->pTxMsg->Data[7] << 24) | + ((uint32_t)hcan->pTxMsg->Data[6] << 16) | + ((uint32_t)hcan->pTxMsg->Data[5] << 8) | + ((uint32_t)hcan->pTxMsg->Data[4])); + + // Request transmit + hcan->Instance->sTxMailBox[index].TIR |= CAN_TI0R_TXRQ; + return HAL_OK; +} + HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *hcan, CanTxMsgTypeDef *txmsg, uint8_t *data, uint32_t Timeout) { uint32_t transmitmailbox; uint32_t tickstart; - uint32_t rqcpflag = 0; - uint32_t txokflag = 0; hcan->pTxMsg = txmsg; (void)data; // Not needed here, caller has set it up as &tx_msg->Data @@ -232,81 +367,130 @@ HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *hcan, CanTxMsgTypeDef *txmsg, // Select one empty transmit mailbox if ((hcan->Instance->TSR & CAN_TSR_TME0) == CAN_TSR_TME0) { transmitmailbox = CAN_TXMAILBOX_0; - rqcpflag = CAN_FLAG_RQCP0; - txokflag = CAN_FLAG_TXOK0; } else if ((hcan->Instance->TSR & CAN_TSR_TME1) == CAN_TSR_TME1) { transmitmailbox = CAN_TXMAILBOX_1; - rqcpflag = CAN_FLAG_RQCP1; - txokflag = CAN_FLAG_TXOK1; } else if ((hcan->Instance->TSR & CAN_TSR_TME2) == CAN_TSR_TME2) { transmitmailbox = CAN_TXMAILBOX_2; - rqcpflag = CAN_FLAG_RQCP2; - txokflag = CAN_FLAG_TXOK2; } else { - transmitmailbox = CAN_TXSTATUS_NOMAILBOX; - } - - if (transmitmailbox != CAN_TXSTATUS_NOMAILBOX) { - // Set up the Id - hcan->Instance->sTxMailBox[transmitmailbox].TIR &= CAN_TI0R_TXRQ; - if (hcan->pTxMsg->IDE == CAN_ID_STD) { - assert_param(IS_CAN_STDID(hcan->pTxMsg->StdId)); - hcan->Instance->sTxMailBox[transmitmailbox].TIR |= ((hcan->pTxMsg->StdId << 21) | \ - hcan->pTxMsg->RTR); - } else { - assert_param(IS_CAN_EXTID(hcan->pTxMsg->ExtId)); - hcan->Instance->sTxMailBox[transmitmailbox].TIR |= ((hcan->pTxMsg->ExtId << 3) | \ - hcan->pTxMsg->IDE | \ - hcan->pTxMsg->RTR); - } + return HAL_BUSY; + } - // Set up the DLC - hcan->pTxMsg->DLC &= (uint8_t)0x0000000F; - hcan->Instance->sTxMailBox[transmitmailbox].TDTR &= (uint32_t)0xFFFFFFF0; - hcan->Instance->sTxMailBox[transmitmailbox].TDTR |= hcan->pTxMsg->DLC; - - // Set up the data field - hcan->Instance->sTxMailBox[transmitmailbox].TDLR = (((uint32_t)hcan->pTxMsg->Data[3] << 24) | - ((uint32_t)hcan->pTxMsg->Data[2] << 16) | - ((uint32_t)hcan->pTxMsg->Data[1] << 8) | - ((uint32_t)hcan->pTxMsg->Data[0])); - hcan->Instance->sTxMailBox[transmitmailbox].TDHR = (((uint32_t)hcan->pTxMsg->Data[7] << 24) | - ((uint32_t)hcan->pTxMsg->Data[6] << 16) | - ((uint32_t)hcan->pTxMsg->Data[5] << 8) | - ((uint32_t)hcan->pTxMsg->Data[4])); - // Request transmission - hcan->Instance->sTxMailBox[transmitmailbox].TIR |= CAN_TI0R_TXRQ; - - if (Timeout == 0) { - return HAL_OK; - } + HAL_StatusTypeDef err = can_transmit_common(hcan, transmitmailbox, txmsg, data); + if (err != HAL_OK) { + return err; + } - // Get tick - tickstart = HAL_GetTick(); - // Check End of transmission flag - while (!(__HAL_CAN_TRANSMIT_STATUS(hcan, transmitmailbox))) { - // Check for the Timeout - if (Timeout != HAL_MAX_DELAY) { - if ((HAL_GetTick() - tickstart) > Timeout) { - // When the timeout expires, we try to abort the transmission of the packet - __HAL_CAN_CANCEL_TRANSMIT(hcan, transmitmailbox); - while (!__HAL_CAN_GET_FLAG(hcan, rqcpflag)) { - } - if (__HAL_CAN_GET_FLAG(hcan, txokflag)) { - // The abort attempt failed and the message was sent properly - return HAL_OK; - } else { - return HAL_TIMEOUT; - } + if (Timeout == 0) { + return HAL_OK; + } + + // Get tick + tickstart = HAL_GetTick(); + // Check End of transmission flag + while (!(__HAL_CAN_TRANSMIT_STATUS(hcan, transmitmailbox))) { + // Check for the Timeout + if (Timeout != HAL_MAX_DELAY) { + if ((HAL_GetTick() - tickstart) > Timeout) { + // When the timeout expires, we try to abort the transmission of the packet + bool was_transmitting = can_cancel_transmit(hcan, transmitmailbox); + // Note: there is a small race here where a message that transmits exactly as + // we call can_cancel_transmit() will still look like it failed + if (!was_transmitting) { + // The abort attempt failed and the message was sent properly + return HAL_OK; + } else { + return HAL_TIMEOUT; } } } - return HAL_OK; + } + return HAL_OK; +} + +HAL_StatusTypeDef can_transmit_buf_index(CAN_HandleTypeDef *hcan, int index, CanTxMsgTypeDef *txmsg, const uint8_t *data) { + __HAL_CAN_ENABLE_IT(hcan, CAN_IT_TME); + return can_transmit_common(hcan, index, txmsg, data); +} + + +bool can_cancel_transmit(CAN_HandleTypeDef *hcan, int index) { + uint32_t empty_flag, mailbox; + bool result = false; + switch (index) { + case 0: + empty_flag = CAN_FLAG_TME0; + mailbox = CAN_TXMAILBOX_0; + break; + case 1: + empty_flag = CAN_FLAG_TME1; + mailbox = CAN_TXMAILBOX_1; + break; + default: + empty_flag = CAN_FLAG_TME2; + mailbox = CAN_TXMAILBOX_2; + break; + } + if (__HAL_CAN_GET_FLAG(hcan, empty_flag) == 0) { + result = true; + __HAL_CAN_CANCEL_TRANSMIT(hcan, mailbox); + mp_uint_t start = mp_hal_ticks_us(); + while (__HAL_CAN_GET_FLAG(hcan, empty_flag) == 0) { + // we don't expect this to take longer than a few clock cycles, if + // it does then it probably indicates a bug in the driver. However, + // either way we don't want to end up stuck here + mp_uint_t elapsed = mp_hal_ticks_us() - start; + assert(elapsed < 2000); + if (elapsed >= 2000) { + break; + } + } + } + + return result; +} + +can_state_t can_get_state(CAN_HandleTypeDef *can) { + uint32_t esr; + + if (can->State == HAL_CAN_STATE_RESET) { + return CAN_STATE_STOPPED; + } + + esr = can->Instance->ESR; + if (esr & CAN_ESR_BOFF) { + return CAN_STATE_BUS_OFF; + } else if (esr & CAN_ESR_EPVF) { + return CAN_STATE_ERROR_PASSIVE; + } else if (esr & CAN_ESR_EWGF) { + return CAN_STATE_ERROR_WARNING; } else { - return HAL_BUSY; + return CAN_STATE_ERROR_ACTIVE; } } +void can_get_counters(CAN_HandleTypeDef *can, can_counters_t *counters) { + CAN_TypeDef *inst = can->Instance; + uint32_t esr = inst->ESR; + counters->tec = esr >> CAN_ESR_TEC_Pos & 0xff; + counters->rec = esr >> CAN_ESR_REC_Pos & 0xff; + counters->tx_pending = 0x01121223 >> ((inst->TSR >> CAN_TSR_TME_Pos & 7) << 2) & 0xf; + counters->rx_fifo0_pending = inst->RF0R >> CAN_RF0R_FMP0_Pos & 3; + counters->rx_fifo1_pending = inst->RF1R >> CAN_RF1R_FMP1_Pos & 3; +} + +// Compatibility shim: call both the pyb.CAN and machine.CAN handlers if necessary, +// allow them to decide which is initialised. +static inline void call_can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo) { + #if MICROPY_PY_MACHINE_CAN + machine_can_irq_handler(can_id, interrupt); + #endif + if (interrupt != CAN_INT_TX_COMPLETE) { + pyb_can_irq_handler(can_id, interrupt, fifo); + } + // TODO: Need to do something to clear the transmit state if pyb.CAN is in use, I think + // (check usage of can_get_transmit_finished() from pyb CAN code) +} + // Workaround for the __HAL_CAN macros expecting a CAN_HandleTypeDef which we // don't have in the ISR. Using this "fake" struct instead of CAN_HandleTypeDef // so it's not possible to accidentally call an API that uses one of the other @@ -317,6 +501,7 @@ typedef struct { static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t fifo) { uint32_t full_flag, full_int, overrun_flag, overrun_int, pending_int; + bool msg_received; const fake_handle_t handle = { .Instance = instance, @@ -328,12 +513,14 @@ static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t overrun_flag = CAN_FLAG_FOV0; overrun_int = CAN_IT_FOV0; pending_int = CAN_IT_FMP0; + msg_received = __HAL_CAN_MSG_PENDING(&handle, CAN_FIFO0); } else { full_flag = CAN_FLAG_FF1; full_int = CAN_IT_FF1; overrun_flag = CAN_FLAG_FOV1; overrun_int = CAN_IT_FOV1; pending_int = CAN_IT_FMP1; + msg_received = __HAL_CAN_MSG_PENDING(&handle, CAN_FIFO1); } bool full = __HAL_CAN_GET_FLAG(&handle, full_flag); @@ -342,39 +529,67 @@ static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t // Note: receive interrupt bits are disabled below, and re-enabled by the // higher layer after calling can_receive() + // Only leave msg_received set if the interrupt is enabled, + // otherwise an CAN_INT_MESSAGE_RECEIVED interrupt is already pending + // and this ISR is being called for another reason + msg_received = msg_received && (handle.Instance->IER & pending_int); + if (full) { __HAL_CAN_DISABLE_IT(&handle, full_int); __HAL_CAN_CLEAR_FLAG(&handle, full_flag); if (!overrun) { - can_irq_handler(can_id, CAN_INT_FIFO_FULL, fifo); + call_can_irq_handler(can_id, CAN_INT_FIFO_FULL, fifo); } } if (overrun) { __HAL_CAN_DISABLE_IT(&handle, overrun_int); __HAL_CAN_CLEAR_FLAG(&handle, overrun_flag); - can_irq_handler(can_id, CAN_INT_FIFO_OVERFLOW, fifo); + call_can_irq_handler(can_id, CAN_INT_FIFO_OVERFLOW, fifo); } - if (!(full || overrun)) { - // Process of elimination, if neither of the above - // FIFO status flags are set then message pending interrupt is what fired. + if (msg_received) { __HAL_CAN_DISABLE_IT(&handle, pending_int); - can_irq_handler(can_id, CAN_INT_MESSAGE_RECEIVED, fifo); + call_can_irq_handler(can_id, CAN_INT_MESSAGE_RECEIVED, fifo); } } -static void can_sce_irq_handler(uint can_id, CAN_TypeDef *instance) { +static void can_sce_irq_handler(int can_id, CAN_TypeDef *instance) { instance->MSR = CAN_MSR_ERRI; // Write to clear ERRIE interrupt uint32_t esr = instance->ESR; if (esr & CAN_ESR_BOFF) { - can_irq_handler(can_id, CAN_INT_ERR_BUS_OFF, 0); + call_can_irq_handler(can_id, CAN_INT_ERR_BUS_OFF, 0); } else if (esr & CAN_ESR_EPVF) { - can_irq_handler(can_id, CAN_INT_ERR_PASSIVE, 0); + call_can_irq_handler(can_id, CAN_INT_ERR_PASSIVE, 0); } else if (esr & CAN_ESR_EWGF) { - can_irq_handler(can_id, CAN_INT_ERR_WARNING, 0); + call_can_irq_handler(can_id, CAN_INT_ERR_WARNING, 0); + } +} + +void can_disable_tx_interrupts(CAN_HandleTypeDef *can) { + __HAL_CAN_DISABLE_IT(can, CAN_IT_TME); +} + +void can_restart(CAN_HandleTypeDef *can) { + CAN_TypeDef *instance = can->Instance; + // This sequence puts the hardware in and out of initialisation mode, + // which is the manual way to leave Bus-Off mode (see RM0090 CAN_MCR bit ABOM) + instance->MCR |= CAN_MCR_INRQ; + while ((instance->MSR & CAN_MSR_INAK) == 0) { + } + instance->MCR &= ~CAN_MCR_INRQ; + while ((instance->MSR & CAN_MSR_INAK)) { } } +static void can_tx_irq_handler(int can_id, CAN_TypeDef *instance) { + // Update mailbox tx state based on any RQCPx flags which are set, + // and then clear the RQCPx flags. + + // TX IRQ is re-enabled by higher layer + HAL_NVIC_DisableIRQ(get_tx_irqn(can_id)); + call_can_irq_handler(can_id, CAN_INT_TX_COMPLETE, 0); +} + #if defined(MICROPY_HW_CAN1_TX) void CAN1_RX0_IRQHandler(void) { IRQ_ENTER(CAN1_RX0_IRQn); @@ -393,6 +608,12 @@ void CAN1_SCE_IRQHandler(void) { can_sce_irq_handler(PYB_CAN_1, CAN1); IRQ_EXIT(CAN1_SCE_IRQn); } + +void CAN1_TX_IRQHandler(void) { + IRQ_ENTER(CAN1_TX_IRQn); + can_tx_irq_handler(PYB_CAN_1, CAN1); + IRQ_EXIT(CAN1_TX_IRQn); +} #endif #if defined(MICROPY_HW_CAN2_TX) @@ -413,6 +634,12 @@ void CAN2_SCE_IRQHandler(void) { can_sce_irq_handler(PYB_CAN_2, CAN2); IRQ_EXIT(CAN2_SCE_IRQn); } + +void CAN2_TX_IRQHandler(void) { + IRQ_ENTER(CAN2_TX_IRQn); + can_tx_irq_handler(PYB_CAN_2, CAN2); + IRQ_EXIT(CAN2_TX_IRQn); +} #endif #if defined(MICROPY_HW_CAN3_TX) @@ -433,6 +660,12 @@ void CAN3_SCE_IRQHandler(void) { can_sce_irq_handler(PYB_CAN_3, CAN3); IRQ_EXIT(CAN3_SCE_IRQn); } + +void CAN3_TX_IRQHandler(void) { + IRQ_ENTER(CAN3_TX_IRQn); + can_tx_irq_handler(PYB_CAN_3, CAN3); + IRQ_EXIT(CAN3_TX_IRQn); +} #endif #endif // !MICROPY_HW_ENABLE_FDCAN diff --git a/ports/stm32/can.h b/ports/stm32/can.h index 3422f4180d6df..ff73e1a74d2b4 100644 --- a/ports/stm32/can.h +++ b/ports/stm32/can.h @@ -47,19 +47,48 @@ #define LIST32 (3) #if MICROPY_HW_ENABLE_FDCAN +// Interface compatibility for the classic CAN controller HAL #define CAN_TypeDef FDCAN_GlobalTypeDef #define CAN_HandleTypeDef FDCAN_HandleTypeDef #define CanTxMsgTypeDef FDCAN_TxHeaderTypeDef #define CanRxMsgTypeDef FDCAN_RxHeaderTypeDef + +#define CAN_MODE_NORMAL FDCAN_MODE_NORMAL +#define CAN_MODE_LOOPBACK FDCAN_MODE_EXTERNAL_LOOPBACK +#define CAN_MODE_SILENT FDCAN_MODE_BUS_MONITORING +#define CAN_MODE_SILENT_LOOPBACK FDCAN_MODE_INTERNAL_LOOPBACK + +// FDCAN peripheral has independent indexes for standard id vs extended id filters +#if defined(STM32G4) +#define CAN_HW_MAX_STD_FILTER 28 +#define CAN_HW_MAX_EXT_FILTER 8 +#elif defined(STM32H7) +// The RAM filtering section is configured for 64 x 1 word elements for 11-bit standard +// identifiers, and 31 x 2 words elements for 29-bit extended identifiers. +// The total number of words reserved for the filtering per FDCAN instance is 126 words. +#define CAN_HW_MAX_STD_FILTER 64 +#define CAN_HW_MAX_EXT_FILTER 31 +#endif + +// Value reported via machine.CAN.FILTER_MAX, somewhat optimistic as requires using +// the exact numbers of each type of filter. +#define CAN_HW_MAX_FILTER (CAN_HW_MAX_STD_FILTER + CAN_HW_MAX_EXT_FILTER) + +#else + +// CAN1 & CAN2 have 28 filters which can be arbitrarily split, but machine.CAN +// implementation hard-codes to 14/14. CAN3 has 14 independent filters. +#define CAN_HW_MAX_FILTER 14 + #endif -enum { +typedef enum { CAN_STATE_STOPPED, CAN_STATE_ERROR_ACTIVE, CAN_STATE_ERROR_WARNING, CAN_STATE_ERROR_PASSIVE, CAN_STATE_BUS_OFF, -}; +} can_state_t; typedef enum _rx_state_t { RX_STATE_FIFO_EMPTY = 0, @@ -76,8 +105,31 @@ typedef enum { CAN_INT_ERR_BUS_OFF, CAN_INT_ERR_PASSIVE, CAN_INT_ERR_WARNING, + + CAN_INT_TX_COMPLETE, } can_int_t; +typedef enum { + CAN_TX_FIFO, + CAN_TX_QUEUE, +} can_tx_mode_t; + +// Counter data as used by pyb.CAN.info() and machine.CAN.get_counters() +typedef struct { + unsigned tec; + unsigned rec; + unsigned tx_pending; + unsigned rx_fifo0_pending; + unsigned rx_fifo1_pending; +} can_counters_t; + +#if defined(STM32H7) +#define CAN_TX_QUEUE_LEN 16 +#else +// FDCAN STM32G4, bxCAN +#define CAN_TX_QUEUE_LEN 3 +#endif + // RX FIFO numbering // // Note: For traditional CAN peripheral, the values of CAN_FIFO0 and CAN_FIFO1 are the same @@ -87,13 +139,29 @@ typedef enum { CAN_RX_FIFO1, } can_rx_fifo_t; -bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart); +bool can_init(CAN_HandleTypeDef *can, int can_id, can_tx_mode_t tx_mode, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart); void can_deinit(CAN_HandleTypeDef *can); -void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, uint8_t bank); +uint32_t can_get_source_freq(void); + int can_receive(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, CanRxMsgTypeDef *msg, uint8_t *data, uint32_t timeout_ms); + +// Transmit a CAN frame (callee to choose the transmit slot). Used by pyb.CAN only, does not enable TX interrupt +// On FDCAN this function invalidates the 'txmsg' structure if successful. HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *hcan, CanTxMsgTypeDef *txmsg, uint8_t *data, uint32_t Timeout); +// Tell the controller to copy a CAN frame copied to 'index' and start transmitting +// On FDCAN this function invalidates the 'txmsg' structure if successful. +HAL_StatusTypeDef can_transmit_buf_index(CAN_HandleTypeDef *hcan, int index, CanTxMsgTypeDef *txmsg, const uint8_t *data); + +// Cancel the pending transmission in the specified buffer index. Returns after buffer stops transmitting. +// Result is true if buffer was transmitting, false if not transmitting (or finished transmitting before cancellation) +bool can_cancel_transmit(CAN_HandleTypeDef *hcan, int index); + +// Get the lowest index of a buffer in FAILED or SUCCEEDED state, or -1 if none exists +// Calling this function also re-enables the TX done IRQ for this peripheral +int can_get_transmit_finished(CAN_HandleTypeDef *hcan, bool *is_success); + // Disable all CAN receive interrupts for a FIFO void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo); @@ -102,21 +170,38 @@ void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo); // Interrupt for CAN_INT_MESSAGE_RECEIVED is only enabled if enable_msg_received is set. void can_enable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, bool enable_msg_received); +// Disable all pending TX interrupts (ahead of restart or deinit). Will re-enable n next transmit +void can_disable_tx_interrupts(CAN_HandleTypeDef *can); + +can_state_t can_get_state(CAN_HandleTypeDef *can); + +void can_get_counters(CAN_HandleTypeDef *can, can_counters_t *counters); + +// Restart controller (clears error states). Caller expected to check controller initialised already. +void can_restart(CAN_HandleTypeDef *can); + // Implemented in pyb_can.c, called from lower layer -extern void can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo); +extern void pyb_can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo); + +// Implemented in machine_can.c, called from lower layer +extern void machine_can_irq_handler(uint can_id, can_int_t interrupt); #if MICROPY_HW_ENABLE_FDCAN -static inline unsigned can_rx_pending(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { +static inline unsigned can_is_rx_pending(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { return HAL_FDCAN_GetRxFifoFillLevel(can, fifo == CAN_RX_FIFO0 ? FDCAN_RX_FIFO0 : FDCAN_RX_FIFO1); } +void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, bool is_extid); + #else -static inline unsigned can_rx_pending(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { +static inline unsigned can_is_rx_pending(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { return __HAL_CAN_MSG_PENDING(can, fifo == CAN_RX_FIFO0 ? CAN_FIFO0 : CAN_FIFO1); } +void can_clearfilter(CAN_HandleTypeDef *can, uint32_t filter_num, uint8_t can2_start_bank); + #endif // MICROPY_HW_ENABLE_FDCAN #endif // MICROPY_HW_ENABLE_CAN diff --git a/ports/stm32/fdcan.c b/ports/stm32/fdcan.c index 3034dd38ef272..a7fcf7330b19c 100644 --- a/ports/stm32/fdcan.c +++ b/ports/stm32/fdcan.c @@ -59,17 +59,68 @@ #define FDCAN_IT_GROUP_BIT_LINE_ERROR (FDCAN_ILS_EPE | FDCAN_ILS_ELOE) #define FDCAN_IT_GROUP_PROTOCOL_ERROR (FDCAN_ILS_ARAE | FDCAN_ILS_PEDE | FDCAN_ILS_PEAE | FDCAN_ILS_WDIE | FDCAN_ILS_BOE | FDCAN_ILS_EWE) #define FDCAN_IT_GROUP_RX_FIFO1 (FDCAN_ILS_RF1NL | FDCAN_ILS_RF1FL | FDCAN_ILS_RF1LL) -#endif // The dedicated Message RAM should be 2560 words, but the way it's defined in stm32h7xx_hal_fdcan.c // as (SRAMCAN_BASE + FDCAN_MESSAGE_RAM_SIZE - 0x4U) limits the usable number of words to 2559 words. #define FDCAN_MESSAGE_RAM_SIZE (2560 - 1) +#endif // STM32H7 + +#if defined(STM32G4) +// These HAL APIs are not implemented for STM32G4, so we implement them here... +static HAL_StatusTypeDef HAL_FDCAN_AddMessageToTxBuffer(FDCAN_HandleTypeDef *hfdcan, FDCAN_TxHeaderTypeDef *pTxHeader, uint8_t *pTxData, uint32_t BufferIndex); +static HAL_StatusTypeDef HAL_FDCAN_EnableTxBufferRequest(FDCAN_HandleTypeDef *hfdcan, uint32_t BufferIndex); +#endif // STM32G4 // also defined in _hal_fdcan.c, but not able to declare extern and reach the variable -const uint8_t DLCtoBytes[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64}; +static const uint8_t DLCtoBytes[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64}; + +#if defined(MICROPY_HW_CAN3_TX) +#error "Support is not yet added for FDCAN CAN3" +#elif defined(MICROPY_HW_CAN2_TX) +#define NUM_CAN 2 +#else +#define NUM_CAN 1 +#endif + +int can_get_transmit_finished(CAN_HandleTypeDef *hcan, bool *is_success) { + // Note: No HAL API available for the below regs, unless we use the HAL's IRQ handler + FDCAN_GlobalTypeDef *instance = hcan->Instance; -bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart) { - (void)auto_restart; + uint32_t enabled_tx = instance->TXBTIE; // Which buffers have TX ints enabled? + uint32_t tx_success = instance->TXBTO & enabled_tx; // Which TX buffers succeeded? + uint32_t tx_cancel = instance->TXBCF & enabled_tx; // Which TX buffers cancelled? + int result = -1; + + for (int i = 0; i < CAN_TX_QUEUE_LEN; i++) { + if (tx_success & (1U << i)) { + *is_success = true; + result = i; + break; + } + if (tx_cancel & (1U << i)) { + *is_success = false; + result = i; + break; + } + } + + if (result != -1) { + // Clear the TX interrupts for this buffer, will re-enable + // when next sending + instance->TXBTIE &= ~(1U << result); + instance->TXBCIE &= ~(1U << result); + } + + // Re-enable transmit interrupts + instance->IE |= (FDCAN_IT_TX_COMPLETE | FDCAN_IT_TX_ABORT_COMPLETE); + + return result; +} + +bool can_init(CAN_HandleTypeDef *can, int can_id, can_tx_mode_t tx_mode, uint32_t mode, uint32_t prescaler, uint32_t sjw, uint32_t bs1, uint32_t bs2, bool auto_restart) { + (void)auto_restart; // FDCAN peripheral doesn't support automatic exit of Bus-Off + + uint32_t fifo_queue_mode = (tx_mode == CAN_TX_FIFO) ? FDCAN_TX_FIFO_OPERATION : FDCAN_TX_QUEUE_OPERATION; FDCAN_InitTypeDef *init = &can->Init; // Configure FDCAN with FD frame and BRS support. @@ -85,15 +136,16 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca init->TransmitPause = DISABLE; init->ProtocolException = ENABLE; + init->StdFiltersNbr = CAN_HW_MAX_STD_FILTER; + init->ExtFiltersNbr = CAN_HW_MAX_EXT_FILTER; + #if defined(STM32G4) init->ClockDivider = FDCAN_CLOCK_DIV1; init->DataPrescaler = 1; init->DataSyncJumpWidth = 1; init->DataTimeSeg1 = 1; init->DataTimeSeg2 = 1; - init->StdFiltersNbr = 28; - init->ExtFiltersNbr = 8; - init->TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; + init->TxFifoQueueMode = fifo_queue_mode; #elif defined(STM32H7) // The dedicated FDCAN RAM is 2560 32-bit words and shared between the FDCAN instances. // To support 2 FDCAN instances simultaneously, the Message RAM is divided in half by @@ -108,26 +160,24 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca // data field and the specific transmission or reception bits field for control. // The following code configures the different Message RAM sections per FDCAN instance. - // The RAM filtering section is configured for 64 x 1 word elements for 11-bit standard - // identifiers, and 31 x 2 words elements for 29-bit extended identifiers. - // The total number of words reserved for the filtering per FDCAN instance is 126 words. - init->StdFiltersNbr = 64; - init->ExtFiltersNbr = 31; - // The Tx event FIFO is used to store the message ID and the timestamp of successfully // transmitted elements. The Tx event FIFO can store a maximum of 32 (2 words) elements. // NOTE: Events are stored in Tx event FIFO only if tx_msg.TxEventFifoControl is enabled. init->TxEventsNbr = 0; - // Transmission section is configured in FIFO mode operation, with no dedicated Tx buffers. - // The Tx FIFO can store a maximum of 32 elements (or 576 words), each element is 18 words + // The Tx FIFO or Queue can store a maximum of 32 elements (or 576 words), each element is 18 words // long (to support a maximum of 64 bytes data field): - // 2 words header + 16 words data field (to support up to 64 bytes of data). + // 2 words header + 16 words data field (to support up to 64 bytes of data). // The total number of words reserved for the Tx FIFO per FDCAN instance is 288 words. - init->TxBuffersNbr = 0; - init->TxFifoQueueElmtsNbr = 16; + if (tx_mode == CAN_TX_FIFO) { + init->TxBuffersNbr = 0; + init->TxFifoQueueElmtsNbr = CAN_TX_QUEUE_LEN; + } else { + init->TxBuffersNbr = CAN_TX_QUEUE_LEN; + init->TxFifoQueueElmtsNbr = 0; + } + init->TxFifoQueueMode = fifo_queue_mode; init->TxElmtSize = FDCAN_DATA_BYTES_64; - init->TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; // Reception section is configured to use Rx FIFO 0 and Rx FIFO1, with no dedicated Rx buffers. // Each Rx FIFO can store a maximum of 64 elements (1152 words), each element is 18 words @@ -139,15 +189,14 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca init->RxFifo0ElmtSize = FDCAN_DATA_BYTES_64; init->RxFifo1ElmtsNbr = 24; init->RxFifo1ElmtSize = FDCAN_DATA_BYTES_64; - #endif + #endif // STM32H7 - FDCAN_GlobalTypeDef *CANx = NULL; const machine_pin_obj_t *pins[2]; switch (can_id) { #if defined(MICROPY_HW_CAN1_TX) case PYB_CAN_1: - CANx = FDCAN1; + can->Instance = FDCAN1; pins[0] = MICROPY_HW_CAN1_TX; pins[1] = MICROPY_HW_CAN1_RX; break; @@ -155,7 +204,7 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca #if defined(MICROPY_HW_CAN2_TX) case PYB_CAN_2: - CANx = FDCAN2; + can->Instance = FDCAN2; pins[0] = MICROPY_HW_CAN2_TX; pins[1] = MICROPY_HW_CAN2_RX; break; @@ -177,9 +226,7 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca } } - // init CANx - can->Instance = CANx; - // catch bad configuration errors. + // initialise hardware, catching bad configuration errors. if (HAL_FDCAN_Init(can) != HAL_OK) { return false; } @@ -188,7 +235,9 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca HAL_FDCAN_ConfigGlobalFilter(can, FDCAN_REJECT, FDCAN_REJECT, DISABLE, DISABLE); // The configuration registers are locked after CAN is started. - HAL_FDCAN_Start(can); + if (HAL_FDCAN_Start(can) != HAL_OK) { + return false; + } // Reset all filters for (int f = 0; f < init->StdFiltersNbr; ++f) { @@ -222,44 +271,79 @@ bool can_init(CAN_HandleTypeDef *can, int can_id, uint32_t mode, uint32_t presca // FDCAN IT 1 HAL_FDCAN_ConfigInterruptLines(can, FDCAN_IT_GROUP_RX_FIFO1, FDCAN_INTERRUPT_LINE1); - // Enable error interrupts. RX-related interrupts are enabled via can_enable_rx_interrupts() - HAL_FDCAN_ActivateNotification(can, FDCAN_IT_BUS_OFF | FDCAN_IT_ERROR_WARNING | FDCAN_IT_ERROR_PASSIVE, 0); + // Enable error interrupts for all queue positions. + // (RX-related interrupts are enabled via can_enable_rx_interrupts(), + // and TX-related interrupts are enabled during TX + HAL_FDCAN_ActivateNotification(can, + FDCAN_IT_BUS_OFF | FDCAN_IT_ERROR_WARNING | FDCAN_IT_ERROR_PASSIVE, 0); return true; } void can_deinit(FDCAN_HandleTypeDef *can) { + bool any_enabled = false; HAL_FDCAN_DeInit(can); + if (can->Instance == FDCAN1) { HAL_NVIC_DisableIRQ(FDCAN1_IT0_IRQn); HAL_NVIC_DisableIRQ(FDCAN1_IT1_IRQn); - // TODO check if FDCAN2 is used. - __HAL_RCC_FDCAN_FORCE_RESET(); - __HAL_RCC_FDCAN_RELEASE_RESET(); - __HAL_RCC_FDCAN_CLK_DISABLE(); + #if defined(MICROPY_HW_CAN2_TX) + any_enabled = NVIC_GetEnableIRQ(FDCAN2_IT0_IRQn); + #endif #if defined(MICROPY_HW_CAN2_TX) } else if (can->Instance == FDCAN2) { HAL_NVIC_DisableIRQ(FDCAN2_IT0_IRQn); HAL_NVIC_DisableIRQ(FDCAN2_IT1_IRQn); - // TODO check if FDCAN2 is used. + any_enabled = NVIC_GetEnableIRQ(FDCAN1_IT0_IRQn); + #endif + } + + if (!any_enabled) { + // Only reset FDCAN block and disable clock if all FDCAN units are disabled __HAL_RCC_FDCAN_FORCE_RESET(); __HAL_RCC_FDCAN_RELEASE_RESET(); __HAL_RCC_FDCAN_CLK_DISABLE(); - #endif } } -void can_clearfilter(FDCAN_HandleTypeDef *can, uint32_t f, uint8_t extid) { - FDCAN_FilterTypeDef filter = {0}; - if (extid == 1) { - filter.IdType = FDCAN_EXTENDED_ID; - } else { - filter.IdType = FDCAN_STANDARD_ID; - } - filter.FilterIndex = f; - filter.FilterConfig = FDCAN_FILTER_DISABLE; +void can_clearfilter(FDCAN_HandleTypeDef *can, uint32_t f, bool is_extid) { + FDCAN_FilterTypeDef filter = { + .IdType = is_extid ? FDCAN_EXTENDED_ID : FDCAN_STANDARD_ID, + .FilterIndex = f, + .FilterConfig = FDCAN_FILTER_DISABLE, + }; HAL_FDCAN_ConfigFilter(can, &filter); } +uint32_t can_get_source_freq(void) { + // Find CAN kernel clock + #if defined(STM32H7) + switch (__HAL_RCC_GET_FDCAN_SOURCE()) { + case RCC_FDCANCLKSOURCE_HSE: + return HSE_VALUE; + case RCC_FDCANCLKSOURCE_PLL: { + PLL1_ClocksTypeDef pll1_clocks; + HAL_RCCEx_GetPLL1ClockFreq(&pll1_clocks); + return pll1_clocks.PLL1_Q_Frequency; + } + case RCC_FDCANCLKSOURCE_PLL2: { + PLL2_ClocksTypeDef pll2_clocks; + HAL_RCCEx_GetPLL2ClockFreq(&pll2_clocks); + return pll2_clocks.PLL2_Q_Frequency; + } + default: + abort(); // Should be unreachable, macro should return one of the above + } + #elif defined(STM32G4) + // STM32G4 CAN clock from reset is HSE, unchanged by MicroPython + return HSE_VALUE; + #else // G0, and assume other MCUs too. + // CAN1/CAN2/CAN3 on APB1 use GetPCLK1Freq, alternatively use the following: + // can_kern_clk = ((HSE_VALUE / osc_config.PLL.PLLM ) * osc_config.PLL.PLLN) / + // (osc_config.PLL.PLLQ * clk_init.AHBCLKDivider * clk_init.APB1CLKDivider); + return HAL_RCC_GetPCLK1Freq(); + #endif +} + void can_disable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo) { HAL_FDCAN_DeactivateNotification(can, (fifo == CAN_RX_FIFO0) ? FDCAN_IT_RX_FIFO0_MASK : FDCAN_IT_RX_FIFO1_MASK); } @@ -272,6 +356,19 @@ void can_enable_rx_interrupts(CAN_HandleTypeDef *can, can_rx_fifo_t fifo, bool e HAL_FDCAN_ActivateNotification(can, ints, 0); } +// Fixup the DataLength field from a byte count to a valid DLC value index (rounding up) +static void encode_datalength(CanTxMsgTypeDef *txmsg) { + // Roundup DataLength to next DLC size and encode to DLC. + size_t len_bytes = txmsg->DataLength; + for (mp_uint_t i = 0; i < MP_ARRAY_SIZE(DLCtoBytes); i++) { + if (len_bytes <= DLCtoBytes[i]) { + txmsg->DataLength = (i << 16); + return; + } + } + assert(0); // DataLength value is invalid +} + HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *can, CanTxMsgTypeDef *txmsg, uint8_t *data, uint32_t timeout_ms) { uint32_t start = HAL_GetTick(); while (HAL_FDCAN_GetTxFifoFreeLevel(can) == 0) { @@ -286,9 +383,58 @@ HAL_StatusTypeDef can_transmit(CAN_HandleTypeDef *can, CanTxMsgTypeDef *txmsg, u } mp_event_wait_ms(1); } + // Note: this function doesn't set up TX interrupts, because it's only used by pyb.CAN which + // doesn't care about this - machine.CAN calls can_transmit_buf_index() + encode_datalength(txmsg); + return HAL_FDCAN_AddMessageToTxFifoQ(can, txmsg, data); } +HAL_StatusTypeDef can_transmit_buf_index(CAN_HandleTypeDef *hcan, int index, CanTxMsgTypeDef *txmsg, const uint8_t *data) { + uint32_t tx_loc = 1U << index; + + encode_datalength(txmsg); + + HAL_StatusTypeDef err = HAL_FDCAN_ActivateNotification(hcan, FDCAN_IT_TX_COMPLETE | FDCAN_IT_TX_ABORT_COMPLETE, tx_loc); + if (err == HAL_OK) { + // Note: casting away const from data, the HAL implementation still treats 'data' as const + err = HAL_FDCAN_AddMessageToTxBuffer(hcan, txmsg, (void *)data, tx_loc); + } + if (err == HAL_OK) { + err = HAL_FDCAN_EnableTxBufferRequest(hcan, tx_loc); + } + return err; +} + +bool can_cancel_transmit(CAN_HandleTypeDef *hcan, int index) { + FDCAN_GlobalTypeDef *instance = hcan->Instance; + bool result = false; + + if (instance->TXBRP & (1U << index)) { + result = true; + HAL_StatusTypeDef err = HAL_FDCAN_AbortTxRequest(hcan, 1U << index); + assert(err == HAL_OK); // Should only fail if controller not started + if (err != HAL_OK) { + return false; + } + mp_uint_t start = mp_hal_ticks_us(); + + // Wait for the TX buffer to be marked as no longer pending + while ((instance->TXBRP & (1U << index)) != 0) { + // we don't expect this to take longer than a few clock cycles, if + // it does then it probably indicates a bug in the driver. However, + // either way we don't want to end up stuck here + mp_uint_t elapsed = mp_hal_ticks_us() - start; + assert(elapsed < 1000); + if (elapsed >= 1000) { + break; + } + } + } + + return result; +} + int can_receive(FDCAN_HandleTypeDef *can, can_rx_fifo_t fifo, FDCAN_RxHeaderTypeDef *hdr, uint8_t *data, uint32_t timeout_ms) { volatile uint32_t *rxf, *rxa; uint32_t fl; @@ -362,8 +508,72 @@ int can_receive(FDCAN_HandleTypeDef *can, can_rx_fifo_t fifo, FDCAN_RxHeaderType return 0; // success } -static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t fifo) { - uint32_t ints, rx_fifo_ints, error_ints; +can_state_t can_get_state(CAN_HandleTypeDef *can) { + uint32_t psr; + + if (can->State != HAL_FDCAN_STATE_BUSY) { + // The HAL states which map to "stopped" are: + // HAL_FDCAN_STATE_RESET - peripheral never initialised + // HAL_FDCAN_STATE_READY - CAN is stopped (i.e. HAL_FDCAN_CAN_Stop() called) + // HAL_FDCAN_STATE_ERROR - Internal transition timeout, mostly relates to + // low-power states we currently don't use + return CAN_STATE_STOPPED; + } + + psr = can->Instance->PSR; + if (psr & FDCAN_PSR_BO) { + return CAN_STATE_BUS_OFF; + } else if (psr & FDCAN_PSR_EP) { + return CAN_STATE_ERROR_PASSIVE; + } else if (psr & FDCAN_PSR_EW) { + return CAN_STATE_ERROR_WARNING; + } else { + return CAN_STATE_ERROR_ACTIVE; + } +} + +void can_get_counters(CAN_HandleTypeDef *can, can_counters_t *counters) { + FDCAN_GlobalTypeDef *inst = can->Instance; + uint32_t esr = inst->ECR; + counters->tec = (esr & FDCAN_ECR_TEC_Msk) >> FDCAN_ECR_TEC_Pos; + if (esr & FDCAN_ECR_RP) { + counters->rec = 128; // "at least 128" + } else { + counters->rec = (esr & FDCAN_ECR_REC_Msk) >> FDCAN_ECR_REC_Pos; + } + if (can->Init.TxFifoQueueMode == FDCAN_TX_FIFO_OPERATION) { + counters->tx_pending = inst->TXEFS & 0x7; + } else { + counters->tx_pending = mp_popcount(inst->TXBRP); + } + counters->rx_fifo0_pending = (inst->RXF0S & FDCAN_RXF0S_F0FL_Msk) >> FDCAN_RXF0S_F0FL_Pos; + counters->rx_fifo1_pending = (inst->RXF1S & FDCAN_RXF1S_F1FL_Msk) >> FDCAN_RXF1S_F1FL_Pos; +} + +void can_disable_tx_interrupts(CAN_HandleTypeDef *can) { + HAL_FDCAN_DeactivateNotification(can, FDCAN_IT_TX_COMPLETE | FDCAN_IT_TX_ABORT_COMPLETE); +} + +void can_restart(CAN_HandleTypeDef *can) { + HAL_FDCAN_Stop(can); + HAL_FDCAN_Start(can); +} + +// Compatibility shim: call both the pyb.CAN and machine.CAN handlers if necessary, +// allow them to decide which is initialised. +static inline void call_can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo) { + #if MICROPY_PY_MACHINE_CAN + machine_can_irq_handler(can_id, interrupt); + #endif + if (interrupt != CAN_INT_TX_COMPLETE) { + pyb_can_irq_handler(can_id, interrupt, fifo); + } + // TODO: Need to do something to clear the transmit state if pyb.CAN is in use, I think + // (check usage of can_get_transmit_finished() from pyb CAN code) +} + +static void can_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t fifo) { + uint32_t ints, rx_fifo_ints, error_ints, tx_complete_int; ints = instance->IR & instance->IE; @@ -374,54 +584,57 @@ static void can_rx_irq_handler(uint can_id, CAN_TypeDef *instance, can_rx_fifo_t } error_ints = ints & FDCAN_IT_ERROR_STATUS_MASK; + tx_complete_int = ints & (FDCAN_IT_TX_COMPLETE | FDCAN_IT_TX_ABORT_COMPLETE); + // Disable receive interrupts, re-enabled by higher layer after calling can_receive() + // (Note: can't use __HAL_CAN API as only have a CAN_TypeDef, not CAN_HandleTypeDef) instance->IE &= ~rx_fifo_ints; - instance->IR = rx_fifo_ints | error_ints; + instance->IR = rx_fifo_ints | error_ints | tx_complete_int; if (rx_fifo_ints) { - if (rx_fifo_ints & FDCAN_IT_RX_NEW_MESSAGE_MASK) { - can_irq_handler(can_id, CAN_INT_MESSAGE_RECEIVED, fifo); - } if (rx_fifo_ints & FDCAN_IT_RX_FULL_MASK) { - can_irq_handler(can_id, CAN_INT_FIFO_FULL, fifo); + call_can_irq_handler(can_id, CAN_INT_FIFO_FULL, fifo); } if (rx_fifo_ints & FDCAN_IT_RX_MESSAGE_LOST_MASK) { - can_irq_handler(can_id, CAN_INT_FIFO_OVERFLOW, fifo); + call_can_irq_handler(can_id, CAN_INT_FIFO_OVERFLOW, fifo); + } + if (rx_fifo_ints & FDCAN_IT_RX_NEW_MESSAGE_MASK) { + call_can_irq_handler(can_id, CAN_INT_MESSAGE_RECEIVED, fifo); } } if (error_ints) { uint32_t Psr = instance->PSR; - if (error_ints & FDCAN_IT_ERROR_WARNING) { - if (Psr & FDCAN_PSR_EW) { - can_irq_handler(can_id, CAN_INT_ERR_WARNING, 0); - } - } - if (error_ints & FDCAN_IT_ERROR_PASSIVE) { - if (Psr & FDCAN_PSR_EP) { - can_irq_handler(can_id, CAN_INT_ERR_PASSIVE, 0); - } - } - if (error_ints & FDCAN_IT_BUS_OFF) { + if (error_ints & (FDCAN_IT_ERROR_WARNING | FDCAN_IT_ERROR_PASSIVE | FDCAN_IT_BUS_OFF)) { if (Psr & FDCAN_PSR_BO) { - can_irq_handler(can_id, CAN_INT_ERR_BUS_OFF, 0); + call_can_irq_handler(can_id, CAN_INT_ERR_BUS_OFF, 0); + } else if (Psr & FDCAN_PSR_EP) { + call_can_irq_handler(can_id, CAN_INT_ERR_PASSIVE, 0); + } else if (Psr & FDCAN_PSR_EW) { + call_can_irq_handler(can_id, CAN_INT_ERR_WARNING, 0); } } } + + if (tx_complete_int) { + // Disable TX interrupts until we process these ones + instance->IE &= ~tx_complete_int; + call_can_irq_handler(can_id, CAN_INT_TX_COMPLETE, 0); + } } #if defined(MICROPY_HW_CAN1_TX) void FDCAN1_IT0_IRQHandler(void) { IRQ_ENTER(FDCAN1_IT0_IRQn); - can_rx_irq_handler(PYB_CAN_1, FDCAN1, CAN_RX_FIFO0); + can_irq_handler(PYB_CAN_1, FDCAN1, CAN_RX_FIFO0); IRQ_EXIT(FDCAN1_IT0_IRQn); } void FDCAN1_IT1_IRQHandler(void) { IRQ_ENTER(FDCAN1_IT1_IRQn); - can_rx_irq_handler(PYB_CAN_1, FDCAN1, CAN_RX_FIFO1); + can_irq_handler(PYB_CAN_1, FDCAN1, CAN_RX_FIFO1); IRQ_EXIT(FDCAN1_IT1_IRQn); } #endif @@ -429,15 +642,159 @@ void FDCAN1_IT1_IRQHandler(void) { #if defined(MICROPY_HW_CAN2_TX) void FDCAN2_IT0_IRQHandler(void) { IRQ_ENTER(FDCAN2_IT0_IRQn); - can_rx_irq_handler(PYB_CAN_2, FDCAN2, CAN_RX_FIFO0); + can_irq_handler(PYB_CAN_2, FDCAN2, CAN_RX_FIFO0); IRQ_EXIT(FDCAN2_IT0_IRQn); } void FDCAN2_IT1_IRQHandler(void) { IRQ_ENTER(FDCAN2_IT1_IRQn); - can_rx_irq_handler(PYB_CAN_2, FDCAN2, CAN_RX_FIFO1); + can_irq_handler(PYB_CAN_2, FDCAN2, CAN_RX_FIFO1); IRQ_EXIT(FDCAN2_IT1_IRQn); } #endif +#if defined(STM32G4) +// These implementations are copied from stm32h7xx_hal_fdcan.c with modifications for different G4 registers & code formatting + +// *FORMAT-OFF* +// ^^^ Keep original STM HAL code style for easier comparison + +static void FDCAN_CopyMessageToRAM(FDCAN_HandleTypeDef *hfdcan, FDCAN_TxHeaderTypeDef *pTxHeader, uint8_t *pTxData, uint32_t BufferIndex); + +static HAL_StatusTypeDef HAL_FDCAN_AddMessageToTxBuffer(FDCAN_HandleTypeDef *hfdcan, FDCAN_TxHeaderTypeDef *pTxHeader, uint8_t *pTxData, uint32_t BufferIndex) +{ + HAL_FDCAN_StateTypeDef state = hfdcan->State; + + /* Check function parameters */ + assert_param(IS_FDCAN_ID_TYPE(pTxHeader->IdType)); + if (pTxHeader->IdType == FDCAN_STANDARD_ID) + { + assert_param(IS_FDCAN_MAX_VALUE(pTxHeader->Identifier, 0x7FFU)); + } + else /* pTxHeader->IdType == FDCAN_EXTENDED_ID */ + { + assert_param(IS_FDCAN_MAX_VALUE(pTxHeader->Identifier, 0x1FFFFFFFU)); + } + assert_param(IS_FDCAN_FRAME_TYPE(pTxHeader->TxFrameType)); + assert_param(IS_FDCAN_DLC(pTxHeader->DataLength)); + assert_param(IS_FDCAN_ESI(pTxHeader->ErrorStateIndicator)); + assert_param(IS_FDCAN_BRS(pTxHeader->BitRateSwitch)); + assert_param(IS_FDCAN_FDF(pTxHeader->FDFormat)); + assert_param(IS_FDCAN_EFC(pTxHeader->TxEventFifoControl)); + assert_param(IS_FDCAN_MAX_VALUE(pTxHeader->MessageMarker, 0xFFU)); + assert_param(IS_FDCAN_TX_LOCATION(BufferIndex)); + + if ((state == HAL_FDCAN_STATE_READY) || (state == HAL_FDCAN_STATE_BUSY)) + { + /* Check that the selected buffer has an allocated area into the RAM */ + if (POSITION_VAL(BufferIndex) >= CAN_TX_QUEUE_LEN) // Note: Modified for G4 here + { + /* Update error code */ + hfdcan->ErrorCode |= HAL_FDCAN_ERROR_PARAM; + + return HAL_ERROR; + } + + /* Check that there is no transmission request pending for the selected buffer */ + if ((hfdcan->Instance->TXBRP & BufferIndex) != 0U) + { + /* Update error code */ + hfdcan->ErrorCode |= HAL_FDCAN_ERROR_PENDING; + + return HAL_ERROR; + } + else + { + /* Add the message to the Tx buffer */ + FDCAN_CopyMessageToRAM(hfdcan, pTxHeader, pTxData, POSITION_VAL(BufferIndex)); + } + + /* Return function status */ + return HAL_OK; + } + else + { + /* Update error code */ + hfdcan->ErrorCode |= HAL_FDCAN_ERROR_NOT_INITIALIZED; + + return HAL_ERROR; + } +} + +static HAL_StatusTypeDef HAL_FDCAN_EnableTxBufferRequest(FDCAN_HandleTypeDef *hfdcan, uint32_t BufferIndex) +{ + if (hfdcan->State == HAL_FDCAN_STATE_BUSY) + { + /* Add transmission request */ + hfdcan->Instance->TXBAR = BufferIndex; + + /* Return function status */ + return HAL_OK; + } + else + { + /* Update error code */ + hfdcan->ErrorCode |= HAL_FDCAN_ERROR_NOT_STARTED; + + return HAL_ERROR; + } +} + +#define SRAMCAN_TFQ_SIZE (18U * 4U) /* TX FIFO/Queue Elements Size in bytes */ + +// This function is copied 100% as-is from stm32g4xx_hal_fdcan.c, unfortunately +static void FDCAN_CopyMessageToRAM(FDCAN_HandleTypeDef *hfdcan, FDCAN_TxHeaderTypeDef *pTxHeader, uint8_t *pTxData, + uint32_t BufferIndex) +{ + uint32_t TxElementW1; + uint32_t TxElementW2; + uint32_t *TxAddress; + uint32_t ByteCounter; + + /* Build first word of Tx header element */ + if (pTxHeader->IdType == FDCAN_STANDARD_ID) + { + TxElementW1 = (pTxHeader->ErrorStateIndicator | + FDCAN_STANDARD_ID | + pTxHeader->TxFrameType | + (pTxHeader->Identifier << 18U)); + } + else /* pTxHeader->IdType == FDCAN_EXTENDED_ID */ + { + TxElementW1 = (pTxHeader->ErrorStateIndicator | + FDCAN_EXTENDED_ID | + pTxHeader->TxFrameType | + pTxHeader->Identifier); + } + + /* Build second word of Tx header element */ + TxElementW2 = ((pTxHeader->MessageMarker << 24U) | + pTxHeader->TxEventFifoControl | + pTxHeader->FDFormat | + pTxHeader->BitRateSwitch | + pTxHeader->DataLength); + + /* Calculate Tx element address */ + TxAddress = (uint32_t *)(hfdcan->msgRam.TxFIFOQSA + (BufferIndex * SRAMCAN_TFQ_SIZE)); + + /* Write Tx element header to the message RAM */ + *TxAddress = TxElementW1; + TxAddress++; + *TxAddress = TxElementW2; + TxAddress++; + + /* Write Tx payload to the message RAM */ + for (ByteCounter = 0; ByteCounter < DLCtoBytes[pTxHeader->DataLength >> 16U]; ByteCounter += 4U) + { + *TxAddress = (((uint32_t)pTxData[ByteCounter + 3U] << 24U) | + ((uint32_t)pTxData[ByteCounter + 2U] << 16U) | + ((uint32_t)pTxData[ByteCounter + 1U] << 8U) | + (uint32_t)pTxData[ByteCounter]); + TxAddress++; + } +} + +#endif // STM32G4 + +// *FORMAT-ON* #endif // MICROPY_HW_ENABLE_CAN && MICROPY_HW_ENABLE_FDCAN diff --git a/ports/stm32/machine_can.c b/ports/stm32/machine_can.c new file mode 100644 index 0000000000000..74821b88d5bca --- /dev/null +++ b/ports/stm32/machine_can.c @@ -0,0 +1,489 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2024-2026 Angus Gratton + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/machine_can.c via MICROPY_PY_MACHINE_CAN_INCLUDEFILE. +#include +#include "extmod/machine_can_port.h" +#include "can.h" +#include "py/runtime.h" +#include "py/mperrno.h" +#include "py/mphal.h" +#include "py/gc.h" + +#if MICROPY_HW_ENABLE_FDCAN +#define CAN_BRP_MIN 1 +#define CAN_BRP_MAX 512 +#define CAN_FD_BRS_BRP_MIN 1 +#define CAN_FD_BRS_BRP_MAX 32 +#define CAN_FILTERS_STD_EXT_SEPARATE 1 + +#else // Classic bxCAN +#define CAN_BRP_MIN 1 +#define CAN_BRP_MAX 1024 +#define CAN_FILTERS_STD_EXT_SEPARATE 0 +#endif + +#define TX_EMPTY UINT32_MAX + +struct machine_can_port { + CAN_HandleTypeDef h; + uint32_t tx[CAN_TX_QUEUE_LEN]; // ID stored in each hardware tx buffer, or TX_EMPTY if empty + bool irq_state_pending; + bool error_passive; +}; + +// Convert the port agnostic CAN mode to the ST mode +static uint32_t can_port_mode(machine_can_mode_t mode) { + switch (mode) { + case MP_CAN_MODE_NORMAL: + return CAN_MODE_NORMAL; + case MP_CAN_MODE_SLEEP: + return CAN_MODE_SILENT; // Sleep is not an operating mode for ST's peripheral + case MP_CAN_MODE_LOOPBACK: + return CAN_MODE_LOOPBACK; + case MP_CAN_MODE_SILENT: + return CAN_MODE_SILENT; + case MP_CAN_MODE_SILENT_LOOPBACK: + return CAN_MODE_SILENT_LOOPBACK; + default: + assert(0); // Mode should have been checked already + return CAN_MODE_NORMAL; + } +} + +static int machine_can_port_f_clock(const machine_can_obj_t *self) { + return (int)can_get_source_freq(); +} + +static bool machine_can_port_supports_mode(const machine_can_obj_t *self, machine_can_mode_t mode) { + return mode < MP_CAN_MODE_MAX; +} + +static mp_uint_t machine_can_port_max_data_len(mp_uint_t flags) { + #if MICROPY_HW_ENABLE_FDCAN + if (flags & CAN_MSG_FLAG_FD_F) { + return 64; + } + #endif + return 8; +} + +static void machine_can_port_init(machine_can_obj_t *self) { + if (!self->port) { + self->port = m_new(struct machine_can_port, 1); + } + memset(self->port, 0, sizeof(struct machine_can_port)); + for (int i = 0; i < CAN_TX_QUEUE_LEN; i++) { + self->port->tx[i] = TX_EMPTY; + } + + bool res = can_init(&self->port->h, + self->can_idx + 1, // Convert 0-based index to 1-based 'can_id' for lower layer + CAN_TX_QUEUE, + can_port_mode(self->mode), + self->brp, + self->sjw, + self->tseg1, + self->tseg2, + false); // auto_restart not currently exposed + + if (!res) { + mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("CAN init failed")); + } +} + +static void machine_can_port_cancel_all_tx(machine_can_obj_t *self) { + struct machine_can_port *port = self->port; + can_disable_tx_interrupts(&port->h); + for (int i = 0; i < CAN_TX_QUEUE_LEN; i++) { + can_cancel_transmit(&port->h, i); + port->tx[i] = TX_EMPTY; + } +} + +static void machine_can_port_deinit(machine_can_obj_t *self) { + machine_can_port_cancel_all_tx(self); + can_deinit(&self->port->h); +} + +static mp_int_t machine_can_port_send(machine_can_obj_t *self, mp_uint_t id, const byte *data, size_t data_len, mp_uint_t flags) { + int idx_empty = -1; // Empty transmit buffer, where no later index has the same ID message in it + + // Scan through the current transmit queue to find an eligible buffer for transmit + for (int i = 0; i < CAN_TX_QUEUE_LEN; i++) { + uint32_t tx_id = self->port->tx[i]; + if (tx_id == TX_EMPTY) { + // This slot is empty + if (idx_empty == -1) { + // Still have to keep scanning as we might see a later message with the same ID, + idx_empty = i; + } + } else if (tx_id == id && !(flags & CAN_MSG_FLAG_UNORDERED)) { + // Can't queue a second message with the same ID and guarantee order + + // (Undocumented hardware limitation - CANFD reference suggests + // messages with the same ID are sent in buffer index order but + // testing shows not always the case at least on STM32H7! Unsure if + // also a limitation of bxCAN or STM32G4, but these only have 3 TX + // buffers so inserting in buffer index order is likely to run out + // of buffers relatively quickly anyway...) + + // Note: currently the driver considers a Standard and an Extended + // ID with the same numeric value to be the same ID... could fix + // this, although it's a relatively uncommon case. + return -1; + } + } + + if (idx_empty == -1) { + // No space in transmit queue + return -1; + } + + CanTxMsgTypeDef tx = { + #if MICROPY_HW_ENABLE_FDCAN + .MessageMarker = 0, + .ErrorStateIndicator = FDCAN_ESI_ACTIVE, + .TxEventFifoControl = FDCAN_NO_TX_EVENTS, + .Identifier = id, // Range checked by caller + .IdType = (flags & CAN_MSG_FLAG_EXT_ID) ? FDCAN_EXTENDED_ID : FDCAN_STANDARD_ID, + .TxFrameType = (flags & CAN_MSG_FLAG_RTR) ? FDCAN_REMOTE_FRAME : FDCAN_DATA_FRAME, + .FDFormat = (flags & CAN_MSG_FLAG_FD_F) ? FDCAN_FD_CAN : FDCAN_CLASSIC_CAN, + .BitRateSwitch = (flags & CAN_MSG_FLAG_BRS) ? FDCAN_BRS_ON : FDCAN_BRS_OFF, + .DataLength = data_len, // Converted inside can_transmit_buf_index + #else // Classic + .StdId = (flags & CAN_MSG_FLAG_EXT_ID) ? 0 : id, + .ExtId = (flags & CAN_MSG_FLAG_EXT_ID) ? id : 0, + .IDE = (flags & CAN_MSG_FLAG_EXT_ID) ? CAN_ID_EXT : CAN_ID_STD, + .RTR = (flags & CAN_MSG_FLAG_RTR) ? CAN_RTR_REMOTE : CAN_RTR_DATA, + .DLC = data_len, + #endif + }; + #if !MICROPY_HW_ENABLE_FDCAN + assert(data_len <= sizeof(tx.Data)); // Also checked by caller + memcpy(tx.Data, data, data_len); + #endif + + HAL_StatusTypeDef err = can_transmit_buf_index(&self->port->h, idx_empty, &tx, data); + if (err != HAL_OK) { + return -1; + } + self->port->tx[idx_empty] = id; + + return idx_empty; +} + +static bool machine_can_port_cancel_send(machine_can_obj_t *self, mp_uint_t idx) { + return can_cancel_transmit(&self->port->h, idx); +} + +static bool machine_can_port_recv(machine_can_obj_t *self, void *data, size_t *dlen, mp_uint_t *id, mp_uint_t *flags, mp_uint_t *errors) { + CAN_HandleTypeDef *can = &self->port->h; + CanRxMsgTypeDef msg; + + for (can_rx_fifo_t fifo = CAN_RX_FIFO0; fifo <= CAN_RX_FIFO1; fifo++) { + if (can_receive(can, fifo, &msg, data, 0) == 0) { + // CanRxMsgTypeDef is different for Classic vs FD + #if MICROPY_HW_ENABLE_FDCAN + *flags = ((msg.IdType == FDCAN_EXTENDED_ID) ? CAN_MSG_FLAG_EXT_ID : 0) | + ((msg.RxFrameType == FDCAN_REMOTE_FRAME) ? CAN_MSG_FLAG_RTR : 0); + *id = msg.Identifier; + *dlen = msg.DataLength; // Lower layer has converted to bytes already + #else + *flags = (msg.IDE ? CAN_MSG_FLAG_EXT_ID : 0) | + (msg.RTR ? CAN_MSG_FLAG_RTR : 0); + *id = msg.IDE ? msg.ExtId : msg.StdId; + *dlen = msg.DLC; + #endif + + *errors = self->rx_error_flags; + self->rx_error_flags = 0; + + // Re-enable any interrupts that were disabled in RX IRQ handlers + can_enable_rx_interrupts(can, fifo, self->mp_irq_trigger & MP_CAN_IRQ_RX); + + return true; + } + } + return false; +} + +static void machine_can_update_irqs(machine_can_obj_t *self) { + uint16_t triggers = self->mp_irq_trigger; + + for (can_rx_fifo_t fifo = CAN_RX_FIFO0; fifo <= CAN_RX_FIFO1; fifo++) { + if (triggers & MP_CAN_IRQ_RX) { + can_enable_rx_interrupts(&self->port->h, fifo, true); + } else { + can_disable_rx_interrupts(&self->port->h, fifo); + } + } + + // Note: TX complete interrupt is always enabled to manage the internal queue state +} + +static void machine_can_port_clear_filters(machine_can_obj_t *self) { + #if MICROPY_HW_ENABLE_FDCAN + for (int f = 0; f < CAN_HW_MAX_STD_FILTER; f++) { + can_clearfilter(&self->port->h, f, false); + } + for (int f = 0; f < CAN_HW_MAX_EXT_FILTER; f++) { + can_clearfilter(&self->port->h, f, true); + } + #else + int bank_offs = (self->can_idx == 1) ? CAN_HW_MAX_FILTER : 0; // CAN2 filters index after CAN1 + for (int f = 0; f < CAN_HW_MAX_FILTER; f++) { + can_clearfilter(&self->port->h, f + bank_offs, CAN_HW_MAX_FILTER); + } + #endif +} + +#if MICROPY_HW_ENABLE_FDCAN +static void machine_can_port_set_filter(machine_can_obj_t *self, int filter_idx, mp_uint_t can_id, mp_uint_t mask, mp_uint_t flags) { + int max_idx = (flags & CAN_MSG_FLAG_EXT_ID) ? CAN_HW_MAX_EXT_FILTER : CAN_HW_MAX_STD_FILTER; + if (filter_idx >= max_idx) { + mp_raise_ValueError(MP_ERROR_TEXT("too many filters for this ID type")); + } + if (flags & ~CAN_MSG_FLAG_EXT_ID) { + mp_raise_ValueError(MP_ERROR_TEXT("flags")); // Only supported flag is for extended ID + } + + FDCAN_FilterTypeDef filter = { + .IdType = (flags & CAN_MSG_FLAG_EXT_ID) ? FDCAN_EXTENDED_ID : FDCAN_STANDARD_ID, + // FDCAN counts standard and extended id filters separately, but this is + // already accounted for in filter_idx due to CAN_FILTERS_STD_EXT_SEPARATE. + .FilterIndex = filter_idx, + .FilterType = FDCAN_FILTER_MASK, + // Round-robin between FIFO1 and FIFO0 + .FilterConfig = (filter_idx & 1) ? FDCAN_FILTER_TO_RXFIFO1 : FDCAN_FILTER_TO_RXFIFO0, + .FilterID1 = can_id, + .FilterID2 = mask, + }; + + int r = HAL_FDCAN_ConfigFilter(&self->port->h, &filter); + assert(r == HAL_OK); + (void)r; +} +#else +static void machine_can_port_set_filter(machine_can_obj_t *self, int filter_idx, mp_uint_t can_id, mp_uint_t mask, mp_uint_t flags) { + if (filter_idx >= CAN_HW_MAX_FILTER) { + mp_raise_ValueError(MP_ERROR_TEXT("too many filters")); + } + if (flags & ~CAN_MSG_FLAG_EXT_ID) { + mp_raise_ValueError(MP_ERROR_TEXT("flags")); // Only supported flag is for extended ID + } + + if (self->can_idx == 1) { + filter_idx += CAN_HW_MAX_FILTER; // CAN2 filters index after CAN1 + } + + CAN_FilterConfTypeDef filter = { + .FilterActivation = ENABLE, + .FilterScale = CAN_FILTERSCALE_32BIT, + .FilterMode = CAN_FILTERMODE_IDMASK, + .FilterNumber = filter_idx, + // Apply the filters round-robin to each FIFO, as each filter in bxCAN is + // associated with only one FIFO. + .FilterFIFOAssignment = filter_idx % 2, + .BankNumber = CAN_HW_MAX_FILTER, // Assign same number of filters to CAN2 as CAN1 + }; + + // This somewhat corresponds to STM32 RM Figure 342 "Filter bank scale + // configuration", although the Reference Manual makes 32-bit mask filters look + // a lot more complex than they are, then the ST HAL makes it even more + // complex by only supporting filter configuration via 16-bit halfwords + // which are re-assembled to full words inside the HAL... + if (flags & CAN_MSG_FLAG_EXT_ID) { + filter.FilterIdLow = (can_id << 3) | CAN_ID_EXT; + filter.FilterIdHigh = can_id >> 13; + filter.FilterMaskIdLow = (mask << 3) | CAN_ID_EXT; + filter.FilterMaskIdHigh = mask >> 13; + } else { + filter.FilterIdLow = 0; + filter.FilterIdHigh = can_id << 5; + filter.FilterMaskIdLow = CAN_ID_EXT; // Set to require CAN_ID_EXT unset in message + filter.FilterMaskIdHigh = mask << 5; + } + + int r = HAL_CAN_ConfigFilter(&self->port->h, &filter); + assert(r == HAL_OK); // Params should be verified before passing to HAL + (void)r; +} +#endif // MICROPY_HW_ENABLE_FDCAN + +static machine_can_state_t machine_can_port_get_state(machine_can_obj_t *self) { + // machine_can_port.h defines MP_CAN_STATE_xxx enums, verify they all match + // numerically with stm32 can.h CAN_STATE_xxx enums + MP_STATIC_ASSERT((int)MP_CAN_STATE_STOPPED == (int)CAN_STATE_STOPPED); + MP_STATIC_ASSERT((int)MP_CAN_STATE_ACTIVE == (int)CAN_STATE_ERROR_ACTIVE); + MP_STATIC_ASSERT((int)MP_CAN_STATE_WARNING == (int)CAN_STATE_ERROR_WARNING); + MP_STATIC_ASSERT((int)MP_CAN_STATE_PASSIVE == (int)CAN_STATE_ERROR_PASSIVE); + MP_STATIC_ASSERT((int)MP_CAN_STATE_BUS_OFF == (int)CAN_STATE_BUS_OFF); + return (machine_can_state_t)can_get_state(&self->port->h); +} + +static void machine_can_port_update_counters(machine_can_obj_t *self) { + can_counters_t hw_counters; + struct machine_can_port *port = self->port; + machine_can_counters_t *counters = &self->counters; + + can_get_counters(&port->h, &hw_counters); + + counters->tec = hw_counters.tec; + counters->rec = hw_counters.rec; + counters->tx_pending = hw_counters.tx_pending; + counters->rx_pending = hw_counters.rx_fifo0_pending + hw_counters.rx_fifo1_pending; + + // Other fields in 'counters' are updated from ISR directly +} + +static mp_obj_t machine_can_port_get_additional_timings(machine_can_obj_t *self, mp_obj_t optional_arg) { + return mp_const_none; +} + +static void machine_can_port_restart(machine_can_obj_t *self) { + // extmod layer has already checked CAN is initialised + struct machine_can_port *port = self->port; + machine_can_port_cancel_all_tx(self); + can_restart(&port->h); + port->irq_state_pending = false; +} + +static bool clear_complete_transfer(machine_can_obj_t *self, int *index, bool *is_success) { + *index = can_get_transmit_finished(&self->port->h, is_success); + if (*index == -1) { + return false; + } + self->port->tx[*index] = TX_EMPTY; + + return true; +} + +static mp_uint_t machine_can_port_irq_flags(machine_can_obj_t *self) { + mp_uint_t flags = 0; + CAN_HandleTypeDef *can = &self->port->h; + + if (self->mp_irq_trigger & MP_CAN_IRQ_STATE && self->port->irq_state_pending) { + flags |= MP_CAN_IRQ_STATE; + self->port->irq_state_pending = false; + } + + // Check for RX + if (self->mp_irq_trigger & MP_CAN_IRQ_RX) { + for (can_rx_fifo_t fifo = CAN_RX_FIFO0; fifo <= CAN_RX_FIFO1; fifo++) { + if (can_is_rx_pending(can, fifo)) { + flags |= MP_CAN_IRQ_RX; + } + } + } + + // Check for TX done + if (self->mp_irq_trigger & MP_CAN_IRQ_TX) { + bool is_success = false; + int index; + if (clear_complete_transfer(self, &index, &is_success)) { + flags |= (mp_uint_t)(index << MP_CAN_IRQ_IDX_SHIFT) | MP_CAN_IRQ_TX; + if (!is_success) { + flags |= MP_CAN_IRQ_TX_FAILED; + } + } + } + + return flags; +} + +void machine_can_irq_handler(uint can_id, can_int_t interrupt) { + assert(can_id > 0); + machine_can_obj_t *self = MP_STATE_PORT(machine_can_objs)[can_id - 1]; + if (self == NULL) { + return; // Should only hit this code path if pyb.CAN has enabled interrupt + } + struct machine_can_port *port = self->port; + machine_can_counters_t *counters = &self->counters; + bool call_irq = false; + bool irq_state = false; + + switch (interrupt) { + // RX + case CAN_INT_FIFO_FULL: + self->rx_error_flags |= CAN_RECV_ERR_FULL; + break; + case CAN_INT_FIFO_OVERFLOW: + self->rx_error_flags |= CAN_RECV_ERR_OVERRUN; + counters->rx_overruns++; + break; + case CAN_INT_MESSAGE_RECEIVED: + call_irq = call_irq || (self->mp_irq_trigger & MP_CAN_IRQ_RX); + break; + + // Error states + case CAN_INT_ERR_WARNING: + if (!port->error_passive) { + // Only count entering warning state, not leaving it + counters->num_warning++; + irq_state = true; + } + port->error_passive = false; + break; + case CAN_INT_ERR_PASSIVE: + counters->num_passive++; + port->error_passive = true; + irq_state = true; + break; + case CAN_INT_ERR_BUS_OFF: + counters->num_bus_off++; + irq_state = true; + port->error_passive = false; + break; + + // TX + case CAN_INT_TX_COMPLETE: + if (!(self->mp_irq_trigger & MP_CAN_IRQ_TX)) { + // No TX IRQ, so mark this buffer as free and move on + int index; + bool is_success = false; + clear_complete_transfer(self, &index, &is_success); + } else { + // Otherwise, the slot is marked empty after the irq calls flags() + call_irq = true; + } + break; + + default: + assert(0); // Should be unreachable + } + + if (irq_state && (self->mp_irq_trigger & MP_CAN_IRQ_STATE)) { + self->port->irq_state_pending = true; + call_irq = true; + } + + if (call_irq) { + assert(self->mp_irq_obj != NULL); // Can't set mp_irq_trigger otherwise + mp_irq_handler(self->mp_irq_obj); + } +} diff --git a/ports/stm32/machine_pwm.c b/ports/stm32/machine_pwm.c new file mode 100644 index 0000000000000..2560e0caf1d5f --- /dev/null +++ b/ports/stm32/machine_pwm.c @@ -0,0 +1,685 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * Copyright (c) 2026 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// This file is never compiled standalone, it's included directly from +// extmod/machine_pwm.c via MICROPY_PY_MACHINE_PWM_INCLUDEFILE. + +#include "py/mphal.h" +#include "timer.h" + +// tim is a TIMx instance, ch is one of PWM_CHx +#define CCRx(tim, ch) ((&(tim)->CCR1)[(ch)]) + +enum { + #if defined(TIM1) + TIM1_ENABLED, + #endif + #if defined(TIM2) + TIM2_ENABLED, + #endif + #if defined(TIM3) + TIM3_ENABLED, + #endif + #if defined(TIM4) + TIM4_ENABLED, + #endif + #if defined(TIM5) + TIM5_ENABLED, + #endif + // TIM6, TIM7 have no output channels so aren't used for PWM. + #if defined(TIM8) + TIM8_ENABLED, + #endif + #if defined(TIM9) + TIM9_ENABLED, + #endif + #if defined(TIM10) + TIM10_ENABLED, + #endif + #if defined(TIM11) + TIM11_ENABLED, + #endif + #if defined(TIM12) + TIM12_ENABLED, + #endif + #if defined(TIM13) + TIM13_ENABLED, + #endif + #if defined(TIM14) + TIM14_ENABLED, + #endif + #if defined(TIM15) + TIM15_ENABLED, + #endif + #if defined(TIM16) + TIM16_ENABLED, + #endif + #if defined(TIM17) + TIM17_ENABLED, + #endif + #if defined(TIM18) + TIM18_ENABLED, + #endif + #if defined(TIM19) + TIM19_ENABLED, + #endif + #if defined(TIM20) + TIM20_ENABLED, + #endif + #if defined(TIM21) + TIM21_ENABLED, + #endif + #if defined(TIM22) + TIM22_ENABLED, + #endif + NUM_TIMERS, +}; + +enum { + PWM_CH1 = 0, + PWM_CH2 = 1, + PWM_CH3 = 2, + PWM_CH4 = 3, + NUM_CHANNELS_PER_TIMER, +}; + +enum { + PWM_POLARITY_NORMAL, + PWM_POLARITY_INVERTED, +}; + +typedef struct _pwm_t { + uint8_t tim_id; // TIMx id + uint8_t channel; // one of PWM_CH1-PWM_CH4 +} pwm_t; + +static void pwm_init(pwm_t *pwm) { + // The following code assumes each channel has 8 bits in CCMR1/2. + MP_STATIC_ASSERT(TIM_CCMR1_CC1S_Pos + 8 == TIM_CCMR1_CC2S_Pos); + MP_STATIC_ASSERT(TIM_CCMR2_CC3S_Pos + 8 == TIM_CCMR2_CC4S_Pos); + + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + + // Initialise TIM if it's not already running. + if (!(tim->CR1 & TIM_CR1_CEN)) { + timer_clock_enable(pwm->tim_id); + + // Initialise with: clock division 1, up counter mode, auto-reload buffered. + // ARPE allows to smoothly change the frequency of the timer, and prevents + // long silent periods when a 32-bit timer is used and CNT goes beyond ARR. + tim->CR1 = TIM_CR1_ARPE; + + // Invalidate the frequency so `pwm_freq_is_valid()` works. + tim->ARR = 0; + + #if defined(IS_TIM_REPETITION_COUNTER_INSTANCE) + if (IS_TIM_REPETITION_COUNTER_INSTANCE(tim)) { + tim->RCR = 0; + } + #endif + } + + // Configure PWM mode. + uint32_t reg = 6 << TIM_CCMR1_OC1M_Pos // PWM1 mode + | 1 << TIM_CCMR1_OC1PE_Pos // preload enabled + | 0 << TIM_CCMR1_CC1S_Pos // output mode + ; + uint32_t shift = 8 * (pwm->channel & 1); + if (pwm->channel == PWM_CH1 || pwm->channel == PWM_CH2) { + tim->CCMR1 = (tim->CCMR1 & ~(0xff << shift)) | reg << shift; + } else { + tim->CCMR2 = (tim->CCMR2 & ~(0xff << shift)) | reg << shift; + } + + #if defined(IS_TIM_BREAK_INSTANCE) + // Enable master output if needed. + if (IS_TIM_BREAK_INSTANCE(tim)) { + tim->BDTR |= TIM_BDTR_MOE; + } + #endif +} + +static void pwm_set_polarity(pwm_t *pwm, unsigned int polarity) { + // The following code assumes each channel has 4 bits in CCER. + MP_STATIC_ASSERT(TIM_CCER_CC1P_Pos + 4 == TIM_CCER_CC2P_Pos); + + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + unsigned int shift = 4 * pwm->channel; + if (polarity == PWM_POLARITY_NORMAL) { + tim->CCER &= ~(TIM_CCER_CC1P << shift); + } else { + tim->CCER |= TIM_CCER_CC1P << shift; + } +} + +static void pwm_deinit(pwm_t *pwm) { + // The following code assumes each channel has 4 bits in CCER. + MP_STATIC_ASSERT(TIM_CCER_CC1E_Pos + 4 == TIM_CCER_CC2E_Pos); + + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + + // Disable the normal output. + tim->CCER &= ~(TIM_CCER_CC1E << (4 * pwm->channel)); + + // Disable the TIM peripheral if all channels are disabled. + uint32_t ccer_out_mask = TIM_CCER_CCxE_MASK; + #if defined(TIM_CCER_CCxNE_MASK) + ccer_out_mask |= TIM_CCER_CCxNE_MASK; + #endif + if ((tim->CCER & ccer_out_mask) == 0) { + #if defined(IS_TIM_BREAK_INSTANCE) + if (IS_TIM_BREAK_INSTANCE(tim)) { + tim->BDTR &= ~TIM_BDTR_MOE; + } + #endif + tim->CR1 &= ~TIM_CR1_CEN; + } +} + +static bool pwm_freq_is_valid(pwm_t *pwm) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + return tim->ARR != 0; +} + +// The ARR needs to be configured (eg `pwm_set_freq()`) before this function is called. +static void pwm_start(pwm_t *pwm) { + // The following code assumes each channel has 4 bits in CCER. + MP_STATIC_ASSERT(TIM_CCER_CC1E_Pos + 4 == TIM_CCER_CC2E_Pos); + + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + if (!(tim->CR1 & TIM_CR1_CEN)) { + // Reinitialise the counter and update the registers. + tim->EGR = TIM_EGR_UG; + + // Enable the timer if it's not already running. + tim->CR1 |= TIM_CR1_CEN; + } + + // Enable output on pin. + unsigned int shift = 4 * pwm->channel; + tim->CCER |= TIM_CCER_CC1E << shift; +} + +static uint32_t pwm_get_freq(pwm_t *pwm) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + uint32_t prescaler = tim->PSC; + uint32_t period = tim->ARR; + uint32_t freq = timer_get_source_freq(pwm->tim_id) / ((prescaler + 1) * (period + 1)); + return freq; +} + +// Input freq_hz must be > 0. +// Returns false if freq_hz is too large. +static bool pwm_set_freq(pwm_t *pwm, uint32_t freq_hz) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + + uint32_t source_freq = timer_get_source_freq(pwm->tim_id); + + // Find optimal prescaler and period values for the given frequency: + // - For 32-bit TIM, the prescaler is always 1 to get maximum precision. + // - For 16-bit TIM, find the smallest prescaler with a period that fits 16-bits. + uint32_t prescaler = 1; + uint32_t period = (source_freq + freq_hz / 2) / freq_hz; + if (!IS_TIM_32B_COUNTER_INSTANCE(tim)) { + while (period > 0xffff) { + // If we can divide exactly, do that first. + if (period % 5 == 0) { + prescaler *= 5; + period /= 5; + } else if (period % 3 == 0) { + prescaler *= 3; + period /= 3; + } else { + // May not divide exactly, but loses minimal precision. + prescaler <<= 1; + period >>= 1; + } + } + } + + if (period <= 1) { + // Requested frequency too high. + return false; + } + + // Set the prescaler and period registers. + tim->PSC = prescaler - 1; + tim->ARR = period - 1; + + return true; +} + +static uint16_t pwm_get_duty_u16(pwm_t *pwm) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + uint32_t top = tim->ARR + 1; + uint32_t cc = CCRx(tim, pwm->channel); + return (cc * 65535ULL + top / 2U) / top; +} + +static void pwm_set_duty_u16(pwm_t *pwm, uint32_t duty_u16) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + uint32_t top = tim->ARR + 1; + uint32_t cc = ((uint64_t)duty_u16 * top + 65535ULL / 2) / 65535U; + if (cc > top) { + cc = top; + } + CCRx(tim, pwm->channel) = cc; +} + +static uint32_t pwm_get_duty_ns(pwm_t *pwm) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + uint32_t source_freq = timer_get_source_freq(pwm->tim_id) / (tim->PSC + 1); + uint32_t cc = CCRx(tim, pwm->channel); + return ((uint64_t)cc * 1000000000ULL + source_freq / 2U) / source_freq; +} + +static void pwm_set_duty_ns(pwm_t *pwm, uint32_t duty_ns) { + TIM_TypeDef *tim = timer_id_to_reg(pwm->tim_id); + uint32_t source_freq = timer_get_source_freq(pwm->tim_id) / (tim->PSC + 1); + uint32_t top = tim->ARR + 1; + uint32_t cc = ((uint64_t)duty_ns * source_freq + 1000000000ULL / 2U) / 1000000000ULL; + if (cc > top) { + cc = top; + } + CCRx(tim, pwm->channel) = cc; +} + +/******************************************************************************/ +// Pin helper + +// The heuristic to search for a TIMx_CHy alt configuration depends on the MCU. +// Some use "first available" and others use "second available". The scheme is +// chosen to give the best overall assignment of TIMx_CHy to pins which is: +// - deterministic (doesn't depend on order of allocation); +// - maximises the number of unique assignments; +// - guarantees that PA0-PA3 are on a 32-bit timer. +#if defined(STM32G0) || defined(STM32L432xx) || defined(STM32L452xx) +#define TIM_SEARCH_FOR_SECOND (0) +#else +#define TIM_SEARCH_FOR_SECOND (1) +#endif + +typedef struct _pwm_pin_config_t { + uint8_t timer_id; + uint8_t timer_channel; + uint8_t alt; +} pwm_pin_config_t; + +static bool pin_find_af_for_pwm(mp_hal_pin_obj_t pin, pwm_pin_config_t *cfg) { + const pin_af_obj_t *pin_af = NULL; + for (size_t i = 0; i < pin->num_af; ++i) { + if (pin->af[i].fn == AF_FN_TIM && pin->af[i].type >= AF_PIN_TYPE_TIM_CH1 && pin->af[i].type <= AF_PIN_TYPE_TIM_CH4) { + #if TIM_SEARCH_FOR_SECOND + // Get the second TIMx_CHy configuration (or first if there's only one). + if (pin_af != NULL) { + pin_af = &pin->af[i]; + break; + } + pin_af = &pin->af[i]; + #else + // Get the first TIMx_CHy configuration. + pin_af = &pin->af[i]; + break; + #endif + } + } + if (pin_af != NULL) { + cfg->timer_id = pin_af->unit; + cfg->timer_channel = pin_af->type - AF_PIN_TYPE_TIM_CH1; + cfg->alt = pin_af->idx; + return true; + } + return false; +} + +/******************************************************************************/ +// MicroPython bindings for machine.PWM + +enum { + DUTY_NOT_SET = 0, + DUTY_U16, + DUTY_NS +}; + +typedef struct _machine_pwm_obj_t { + mp_obj_base_t base; + pwm_t pwm; +} machine_pwm_obj_t; + +typedef struct _machine_pwm_state_t { + uint8_t duty_type; + mp_int_t duty; +} machine_pwm_state_t; + +static uint32_t timer_pwm_active = 0; + +static const machine_pwm_obj_t machine_pwm_obj[NUM_TIMERS * NUM_CHANNELS_PER_TIMER] = { + #if defined(TIM1) + {{&machine_pwm_type}, {1, PWM_CH1}}, + {{&machine_pwm_type}, {1, PWM_CH2}}, + {{&machine_pwm_type}, {1, PWM_CH3}}, + {{&machine_pwm_type}, {1, PWM_CH4}}, + #endif + #if defined(TIM2) + {{&machine_pwm_type}, {2, PWM_CH1}}, + {{&machine_pwm_type}, {2, PWM_CH2}}, + {{&machine_pwm_type}, {2, PWM_CH3}}, + {{&machine_pwm_type}, {2, PWM_CH4}}, + #endif + #if defined(TIM3) + {{&machine_pwm_type}, {3, PWM_CH1}}, + {{&machine_pwm_type}, {3, PWM_CH2}}, + {{&machine_pwm_type}, {3, PWM_CH3}}, + {{&machine_pwm_type}, {3, PWM_CH4}}, + #endif + #if defined(TIM4) + {{&machine_pwm_type}, {4, PWM_CH1}}, + {{&machine_pwm_type}, {4, PWM_CH2}}, + {{&machine_pwm_type}, {4, PWM_CH3}}, + {{&machine_pwm_type}, {4, PWM_CH4}}, + #endif + #if defined(TIM5) + {{&machine_pwm_type}, {5, PWM_CH1}}, + {{&machine_pwm_type}, {5, PWM_CH2}}, + {{&machine_pwm_type}, {5, PWM_CH3}}, + {{&machine_pwm_type}, {5, PWM_CH4}}, + #endif + // TIM6, TIM7 have no output channels so aren't used for PWM. + #if defined(TIM8) + {{&machine_pwm_type}, {8, PWM_CH1}}, + {{&machine_pwm_type}, {8, PWM_CH2}}, + {{&machine_pwm_type}, {8, PWM_CH3}}, + {{&machine_pwm_type}, {8, PWM_CH4}}, + #endif + #if defined(TIM9) + {{&machine_pwm_type}, {9, PWM_CH1}}, + {{&machine_pwm_type}, {9, PWM_CH2}}, + {{&machine_pwm_type}, {9, PWM_CH3}}, + {{&machine_pwm_type}, {9, PWM_CH4}}, + #endif + #if defined(TIM10) + {{&machine_pwm_type}, {10, PWM_CH1}}, + {{&machine_pwm_type}, {10, PWM_CH2}}, + {{&machine_pwm_type}, {10, PWM_CH3}}, + {{&machine_pwm_type}, {10, PWM_CH4}}, + #endif + #if defined(TIM11) + {{&machine_pwm_type}, {11, PWM_CH1}}, + {{&machine_pwm_type}, {11, PWM_CH2}}, + {{&machine_pwm_type}, {11, PWM_CH3}}, + {{&machine_pwm_type}, {11, PWM_CH4}}, + #endif + #if defined(TIM12) + {{&machine_pwm_type}, {12, PWM_CH1}}, + {{&machine_pwm_type}, {12, PWM_CH2}}, + {{&machine_pwm_type}, {12, PWM_CH3}}, + {{&machine_pwm_type}, {12, PWM_CH4}}, + #endif + #if defined(TIM13) + {{&machine_pwm_type}, {13, PWM_CH1}}, + {{&machine_pwm_type}, {13, PWM_CH2}}, + {{&machine_pwm_type}, {13, PWM_CH3}}, + {{&machine_pwm_type}, {13, PWM_CH4}}, + #endif + #if defined(TIM14) + {{&machine_pwm_type}, {14, PWM_CH1}}, + {{&machine_pwm_type}, {14, PWM_CH2}}, + {{&machine_pwm_type}, {14, PWM_CH3}}, + {{&machine_pwm_type}, {14, PWM_CH4}}, + #endif + #if defined(TIM15) + {{&machine_pwm_type}, {15, PWM_CH1}}, + {{&machine_pwm_type}, {15, PWM_CH2}}, + {{&machine_pwm_type}, {15, PWM_CH3}}, + {{&machine_pwm_type}, {15, PWM_CH4}}, + #endif + #if defined(TIM16) + {{&machine_pwm_type}, {16, PWM_CH1}}, + {{&machine_pwm_type}, {16, PWM_CH2}}, + {{&machine_pwm_type}, {16, PWM_CH3}}, + {{&machine_pwm_type}, {16, PWM_CH4}}, + #endif + #if defined(TIM17) + {{&machine_pwm_type}, {17, PWM_CH1}}, + {{&machine_pwm_type}, {17, PWM_CH2}}, + {{&machine_pwm_type}, {17, PWM_CH3}}, + {{&machine_pwm_type}, {17, PWM_CH4}}, + #endif + #if defined(TIM18) + {{&machine_pwm_type}, {18, PWM_CH1}}, + {{&machine_pwm_type}, {18, PWM_CH2}}, + {{&machine_pwm_type}, {18, PWM_CH3}}, + {{&machine_pwm_type}, {18, PWM_CH4}}, + #endif + #if defined(TIM19) + {{&machine_pwm_type}, {19, PWM_CH1}}, + {{&machine_pwm_type}, {19, PWM_CH2}}, + {{&machine_pwm_type}, {19, PWM_CH3}}, + {{&machine_pwm_type}, {19, PWM_CH4}}, + #endif + #if defined(TIM20) + {{&machine_pwm_type}, {20, PWM_CH1}}, + {{&machine_pwm_type}, {20, PWM_CH2}}, + {{&machine_pwm_type}, {20, PWM_CH3}}, + {{&machine_pwm_type}, {20, PWM_CH4}}, + #endif + #if defined(TIM21) + {{&machine_pwm_type}, {21, PWM_CH1}}, + {{&machine_pwm_type}, {21, PWM_CH2}}, + {{&machine_pwm_type}, {21, PWM_CH3}}, + {{&machine_pwm_type}, {21, PWM_CH4}}, + #endif + #if defined(TIM22) + {{&machine_pwm_type}, {22, PWM_CH1}}, + {{&machine_pwm_type}, {22, PWM_CH2}}, + {{&machine_pwm_type}, {22, PWM_CH3}}, + {{&machine_pwm_type}, {22, PWM_CH4}}, + #endif +}; + +static machine_pwm_state_t machine_pwm_state[NUM_TIMERS * NUM_CHANNELS_PER_TIMER]; + +static inline machine_pwm_state_t *get_state(machine_pwm_obj_t *pwm) { + size_t pwm_index = pwm - &machine_pwm_obj[0]; + return &machine_pwm_state[pwm_index]; +} + +static void mp_machine_pwm_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + machine_pwm_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "", self->pwm.tim_id, self->pwm.channel + 1); +} + +static void mp_machine_pwm_init_helper(machine_pwm_obj_t *self, + size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_freq, ARG_duty_u16, ARG_duty_ns, ARG_invert }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_freq, MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_duty_u16, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_duty_ns, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} }, + { MP_QSTR_invert, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = -1} }, + }; + + // Parse the arguments. + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + if (args[ARG_invert].u_int != -1) { + pwm_set_polarity(&self->pwm, args[ARG_invert].u_int ? PWM_POLARITY_INVERTED : PWM_POLARITY_NORMAL); + } + if (args[ARG_freq].u_int != -1) { + mp_machine_pwm_freq_set(self, args[ARG_freq].u_int); + } + if (args[ARG_duty_u16].u_int != -1) { + mp_machine_pwm_duty_set_u16(self, args[ARG_duty_u16].u_int); + } + if (args[ARG_duty_ns].u_int != -1) { + mp_machine_pwm_duty_set_ns(self, args[ARG_duty_ns].u_int); + } + if (pwm_freq_is_valid(&self->pwm)) { + pwm_start(&self->pwm); + } +} + +// PWM(pin [, args]) +static mp_obj_t mp_machine_pwm_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + // Check number of arguments + mp_arg_check_num(n_args, n_kw, 1, MP_OBJ_FUN_ARGS_MAX, true); + + // Get pin to connect to PWM. + mp_hal_pin_obj_t pin = mp_hal_get_pin_obj(all_args[0]); + + // Search the given pin's alternate functions for a TIMx_CHy function. + machine_pwm_obj_t *self = NULL; + pwm_pin_config_t cfg; + if (pin_find_af_for_pwm(pin, &cfg)) { + for (size_t i = 0; i < MP_ARRAY_SIZE(machine_pwm_obj); ++i) { + if (machine_pwm_obj[i].pwm.tim_id == cfg.timer_id && machine_pwm_obj[i].pwm.channel == cfg.timer_channel) { + self = (machine_pwm_obj_t *)&machine_pwm_obj[i]; + break; + } + } + } + + if (self == NULL) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("Pin(%q) doesn't have PWM capabilities"), pin->name); + } + + // If inactive, clear out the state (may be set from a previous soft reset cycle). + if (!(timer_pwm_active & (1U << self->pwm.tim_id))) { + timer_pwm_active |= 1U << self->pwm.tim_id; + size_t pwm_base_index = (self - &machine_pwm_obj[0]) & ~(NUM_CHANNELS_PER_TIMER - 1); + for (size_t i = 0; i < 4; ++i) { + machine_pwm_state_t *state = &machine_pwm_state[pwm_base_index + i]; + state->duty_type = DUTY_NOT_SET; + } + } + + // Initialise the TIM and the output driver. + pwm_init(&self->pwm); + if (n_args > 1 || n_kw > 0) { + // Arguments given, so reset the state. + machine_pwm_state_t *state = get_state(self); + state->duty_type = DUTY_NOT_SET; + pwm_set_polarity(&self->pwm, PWM_POLARITY_NORMAL); + } + + // Process the remaining parameters. + mp_map_t kw_args; + mp_map_init_fixed_table(&kw_args, n_kw, all_args + n_args); + mp_machine_pwm_init_helper(self, n_args - 1, all_args + 1, &kw_args); + + // Select PWM function for given pin. + mp_hal_pin_config(pin, MP_HAL_PIN_MODE_ALT, MP_HAL_PIN_PULL_NONE, cfg.alt); + + return MP_OBJ_FROM_PTR(self); +} + +void machine_pwm_deinit_all(void) { + for (size_t i = 0; i < MP_ARRAY_SIZE(machine_pwm_obj); ++i) { + machine_pwm_state_t *state = &machine_pwm_state[i]; + if (state->duty_type != DUTY_NOT_SET) { + state->duty_type = DUTY_NOT_SET; + mp_machine_pwm_deinit((machine_pwm_obj_t *)&machine_pwm_obj[i]); + } + } + timer_pwm_active = 0; +} + +static void mp_machine_pwm_deinit(machine_pwm_obj_t *self) { + pwm_deinit(&self->pwm); +} + +static mp_obj_t mp_machine_pwm_freq_get(machine_pwm_obj_t *self) { + if (pwm_freq_is_valid(&self->pwm)) { + return MP_OBJ_NEW_SMALL_INT(pwm_get_freq(&self->pwm)); + } else { + return MP_OBJ_NEW_SMALL_INT(0); + } +} + +static void mp_machine_pwm_freq_set(machine_pwm_obj_t *self, mp_int_t freq) { + // Check validity and change the frequency of the TIM peripheral. + if (freq <= 0) { + mp_raise_ValueError(MP_ERROR_TEXT("freq too small")); + } + if (!pwm_set_freq(&self->pwm, freq)) { + mp_raise_ValueError(MP_ERROR_TEXT("freq too large")); + } + + // Update the duty cycle of all active channels that use this TIM. + size_t pwm_base_index = (self - &machine_pwm_obj[0]) & ~(NUM_CHANNELS_PER_TIMER - 1); + for (size_t i = 0; i < NUM_CHANNELS_PER_TIMER; ++i) { + machine_pwm_obj_t *obj = (machine_pwm_obj_t *)&machine_pwm_obj[pwm_base_index + i]; + machine_pwm_state_t *state = &machine_pwm_state[pwm_base_index + i]; + if (state->duty_type == DUTY_U16) { + mp_machine_pwm_duty_set_u16(obj, state->duty); + } else if (state->duty_type == DUTY_NS) { + mp_machine_pwm_duty_set_ns(obj, state->duty); + } + } +} + +static mp_obj_t mp_machine_pwm_duty_get_u16(machine_pwm_obj_t *self) { + machine_pwm_state_t *state = get_state(self); + if (state->duty_type != DUTY_NOT_SET && pwm_freq_is_valid(&self->pwm)) { + uint32_t duty_u16 = pwm_get_duty_u16(&self->pwm); + return MP_OBJ_NEW_SMALL_INT(duty_u16); + } else { + return MP_OBJ_NEW_SMALL_INT(0); + } +} + +static void mp_machine_pwm_duty_set_u16(machine_pwm_obj_t *self, mp_int_t duty_u16) { + machine_pwm_state_t *state = get_state(self); + state->duty = duty_u16; + state->duty_type = DUTY_U16; + + if (pwm_freq_is_valid(&self->pwm)) { + pwm_set_duty_u16(&self->pwm, duty_u16); + pwm_start(&self->pwm); + } +} + +static mp_obj_t mp_machine_pwm_duty_get_ns(machine_pwm_obj_t *self) { + machine_pwm_state_t *state = get_state(self); + if (state->duty_type != DUTY_NOT_SET && pwm_freq_is_valid(&self->pwm)) { + return mp_obj_new_int_from_uint(pwm_get_duty_ns(&self->pwm)); + } else { + return MP_OBJ_NEW_SMALL_INT(0); + } +} + +static void mp_machine_pwm_duty_set_ns(machine_pwm_obj_t *self, mp_int_t duty_ns) { + machine_pwm_state_t *state = get_state(self); + state->duty = duty_ns; + state->duty_type = DUTY_NS; + + if (pwm_freq_is_valid(&self->pwm)) { + pwm_set_duty_ns(&self->pwm, duty_ns); + pwm_start(&self->pwm); + } +} diff --git a/ports/stm32/main.c b/ports/stm32/main.c index 6ae8061c4135f..17111c6df983e 100644 --- a/ports/stm32/main.c +++ b/ports/stm32/main.c @@ -41,6 +41,7 @@ #include "lib/littlefs/lfs2_util.h" #include "extmod/modmachine.h" #include "extmod/modnetwork.h" +#include "extmod/machine_can.h" #include "extmod/vfs.h" #include "extmod/vfs_fat.h" #include "extmod/vfs_lfs.h" @@ -763,10 +764,16 @@ void stm32_main(uint32_t reset_mode) { #endif #if MICROPY_HW_ENABLE_CAN pyb_can_deinit_all(); + #if MICROPY_PY_MACHINE_CAN + machine_can_deinit_all(); #endif + #endif // MICROPY_HW_ENABLE_CAN #if MICROPY_HW_ENABLE_DAC dac_deinit_all(); #endif + #if MICROPY_PY_MACHINE_PWM + machine_pwm_deinit_all(); + #endif #if MICROPY_PY_MACHINE machine_deinit(); #endif diff --git a/ports/stm32/modmachine.h b/ports/stm32/modmachine.h index 899a29be8ebfe..7e5fa42861e08 100644 --- a/ports/stm32/modmachine.h +++ b/ports/stm32/modmachine.h @@ -31,6 +31,7 @@ void machine_init(void); void machine_deinit(void); void machine_i2s_init0(); +void machine_pwm_deinit_all(void); MP_DECLARE_CONST_FUN_OBJ_VAR_BETWEEN(machine_info_obj); diff --git a/ports/stm32/mpconfigport.h b/ports/stm32/mpconfigport.h index 4ffe5752be978..bc9e94902ffd3 100644 --- a/ports/stm32/mpconfigport.h +++ b/ports/stm32/mpconfigport.h @@ -122,6 +122,14 @@ #ifndef MICROPY_PY_MACHINE_BITSTREAM #define MICROPY_PY_MACHINE_BITSTREAM (1) #endif +#ifndef MICROPY_PY_MACHINE_CAN +#ifdef MICROPY_HW_CAN1_TX +#define MICROPY_PY_MACHINE_CAN (1) +#else +#define MICROPY_PY_MACHINE_CAN (0) +#endif +#endif +#define MICROPY_PY_MACHINE_CAN_INCLUDEFILE "ports/stm32/machine_can.c" #define MICROPY_PY_MACHINE_DHT_READINTO (1) #define MICROPY_PY_MACHINE_PULSE (1) #define MICROPY_PY_MACHINE_PIN_MAKE_NEW mp_pin_make_new @@ -135,6 +143,8 @@ #define MICROPY_PY_MACHINE_I2S_CONSTANT_RX (I2S_MODE_MASTER_RX) #define MICROPY_PY_MACHINE_I2S_CONSTANT_TX (I2S_MODE_MASTER_TX) #define MICROPY_PY_MACHINE_I2S_RING_BUF (1) +#define MICROPY_PY_MACHINE_PWM (1) +#define MICROPY_PY_MACHINE_PWM_INCLUDEFILE "ports/stm32/machine_pwm.c" #define MICROPY_PY_MACHINE_SPI (1) #define MICROPY_PY_MACHINE_SPI_MSB (SPI_FIRSTBIT_MSB) #define MICROPY_PY_MACHINE_SPI_LSB (SPI_FIRSTBIT_LSB) @@ -207,6 +217,17 @@ extern const struct _mp_obj_type_t network_lan_type; #define MICROPY_HW_NIC_ETH #endif +// Provide a port-level default of MICROPY_HW_NUM_CAN based on pin definitions +#ifndef MICROPY_HW_NUM_CAN +#if defined(MICROPY_HW_CAN3_TX) +#define MICROPY_HW_NUM_CAN 3 +#elif defined(MICROPY_HW_CAN2_TX) +#define MICROPY_HW_NUM_CAN 2 +#elif defined(MICROPY_HW_CAN1_TX) +#define MICROPY_HW_NUM_CAN 1 +#endif +#endif // MICROPY_HW_NUM_CAN + // extra constants #define MICROPY_PORT_CONSTANTS \ MACHINE_BUILTIN_MODULE_CONSTANTS \ diff --git a/ports/stm32/mpthreadport.c b/ports/stm32/mpthreadport.c index 621b4311bfc34..369171067e50e 100644 --- a/ports/stm32/mpthreadport.c +++ b/ports/stm32/mpthreadport.c @@ -61,8 +61,8 @@ mp_uint_t mp_thread_get_id(void) { mp_uint_t mp_thread_create(void *(*entry)(void *), void *arg, size_t *stack_size) { if (*stack_size == 0) { *stack_size = 4096; // default stack size - } else if (*stack_size < 2048) { - *stack_size = 2048; // minimum stack size + } else if (*stack_size < 2560) { + *stack_size = 2560; // minimum stack size } // round stack size to a multiple of the word size diff --git a/ports/stm32/pyb_can.c b/ports/stm32/pyb_can.c index 0d004ecfcb5e4..2d676b23d20f5 100644 --- a/ports/stm32/pyb_can.c +++ b/ports/stm32/pyb_can.c @@ -39,6 +39,8 @@ #include "pyb_can.h" #include "can.h" #include "irq.h" +// For some non-port-specific utility functions +#include "extmod/machine_can.h" #if MICROPY_HW_ENABLE_CAN @@ -70,19 +72,12 @@ #define CAN_MAXIMUM_DBS1 (32) #define CAN_MAXIMUM_DBS2 (16) -#define CAN_MODE_NORMAL FDCAN_MODE_NORMAL -#define CAN_MODE_LOOPBACK FDCAN_MODE_EXTERNAL_LOOPBACK -#define CAN_MODE_SILENT FDCAN_MODE_BUS_MONITORING -#define CAN_MODE_SILENT_LOOPBACK FDCAN_MODE_INTERNAL_LOOPBACK - #define CAN1_RX0_IRQn FDCAN1_IT0_IRQn #define CAN1_RX1_IRQn FDCAN1_IT1_IRQn #if defined(CAN2) #define CAN2_RX0_IRQn FDCAN2_IT0_IRQn #define CAN2_RX1_IRQn FDCAN2_IT1_IRQn #endif - -extern const uint8_t DLCtoBytes[16]; #else #define CAN_MAX_FILTER (28) @@ -115,6 +110,7 @@ void pyb_can_deinit_all(void) { pyb_can_obj_t *can_obj = MP_STATE_PORT(pyb_can_obj_all)[i]; if (can_obj != NULL) { pyb_can_deinit(MP_OBJ_FROM_PTR(can_obj)); + MP_STATE_PORT(pyb_can_obj_all)[i] = NULL; } } } @@ -144,7 +140,7 @@ static void pyb_can_print(const mp_print_t *print, mp_obj_t self_in, mp_print_ki self->can_id, mode, #if MICROPY_HW_ENABLE_FDCAN - (self->can.Instance->CCCR & FDCAN_CCCR_DAR) ? MP_QSTR_True : MP_QSTR_False + MP_QSTR_False // auto_restart not supported on FDCAN hardware #else (self->can.Instance->MCR & CAN_MCR_ABOM) ? MP_QSTR_True : MP_QSTR_False #endif @@ -152,45 +148,10 @@ static void pyb_can_print(const mp_print_t *print, mp_obj_t self_in, mp_print_ki } } -static uint32_t pyb_can_get_source_freq() { - uint32_t can_kern_clk = 0; - - // Find CAN kernel clock - #if defined(STM32H7) - switch (__HAL_RCC_GET_FDCAN_SOURCE()) { - case RCC_FDCANCLKSOURCE_HSE: - can_kern_clk = HSE_VALUE; - break; - case RCC_FDCANCLKSOURCE_PLL: { - PLL1_ClocksTypeDef pll1_clocks; - HAL_RCCEx_GetPLL1ClockFreq(&pll1_clocks); - can_kern_clk = pll1_clocks.PLL1_Q_Frequency; - break; - } - case RCC_FDCANCLKSOURCE_PLL2: { - PLL2_ClocksTypeDef pll2_clocks; - HAL_RCCEx_GetPLL2ClockFreq(&pll2_clocks); - can_kern_clk = pll2_clocks.PLL2_Q_Frequency; - break; - } - } - #elif defined(STM32G4) - // STM32G4 CAN clock from reset is HSE, unchanged by MicroPython - can_kern_clk = HSE_VALUE; - #else // G0, F4, F7 and assume other MCUs too. - // CAN1/CAN2/CAN3 on APB1 use GetPCLK1Freq, alternatively use the following: - // can_kern_clk = ((HSE_VALUE / osc_config.PLL.PLLM ) * osc_config.PLL.PLLN) / - // (osc_config.PLL.PLLQ * clk_init.AHBCLKDivider * clk_init.APB1CLKDivider); - can_kern_clk = HAL_RCC_GetPCLK1Freq(); - #endif - - return can_kern_clk; -} - static void pyb_can_get_bit_timing(mp_uint_t baudrate, mp_uint_t sample_point, uint32_t max_brp, uint32_t max_bs1, uint32_t max_bs2, uint32_t min_tseg, mp_int_t *bs1_out, mp_int_t *bs2_out, mp_int_t *prescaler_out) { - uint32_t can_kern_clk = pyb_can_get_source_freq(); + uint32_t can_kern_clk = can_get_source_freq(); mp_uint_t max_baud_error = baudrate / 1000; // Allow .1% deviation const mp_uint_t MAX_SAMPLE_ERROR = 5; // round to nearest 1%, which is the param resolution sample_point *= 10; @@ -273,12 +234,18 @@ static mp_obj_t pyb_can_init_helper(pyb_can_obj_t *self, size_t n_args, const mp #else // Init filter banks for classic CAN. can2_start_bank = args[ARG_num_filter_banks].u_int; + int bank_offs = (self->can_id == 2) ? can2_start_bank : 0; for (int f = 0; f < CAN_MAX_FILTER; f++) { - can_clearfilter(&self->can, f, can2_start_bank); + can_clearfilter(&self->can, f + bank_offs, can2_start_bank); } #endif - if (!can_init(&self->can, self->can_id, args[ARG_mode].u_int, args[ARG_prescaler].u_int, args[ARG_sjw].u_int, + mp_uint_t mode = args[ARG_mode].u_int; + #if !MICROPY_HW_ENABLE_FDCAN + mode = mode << 4; // Undo the '>> 4' set when defining the bxCAN Python constants further down in this file + #endif + + if (!can_init(&self->can, self->can_id, CAN_TX_FIFO, mode, args[ARG_prescaler].u_int, args[ARG_sjw].u_int, args[ARG_bs1].u_int, args[ARG_bs2].u_int, args[ARG_auto_restart].u_bool)) { mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%d) init failure"), self->can_id); } @@ -297,45 +264,16 @@ static mp_obj_t pyb_can_make_new(const mp_obj_type_t *type, size_t n_args, size_ mp_arg_check_num(n_args, n_kw, 1, MP_OBJ_FUN_ARGS_MAX, true); // work out port - mp_uint_t can_idx; - if (mp_obj_is_str(args[0])) { - const char *port = mp_obj_str_get_str(args[0]); - if (0) { - #ifdef MICROPY_HW_CAN1_NAME - } else if (strcmp(port, MICROPY_HW_CAN1_NAME) == 0) { - can_idx = PYB_CAN_1; - #endif - #ifdef MICROPY_HW_CAN2_NAME - } else if (strcmp(port, MICROPY_HW_CAN2_NAME) == 0) { - can_idx = PYB_CAN_2; - #endif - #ifdef MICROPY_HW_CAN3_NAME - } else if (strcmp(port, MICROPY_HW_CAN3_NAME) == 0) { - can_idx = PYB_CAN_3; - #endif - } else { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%s) doesn't exist"), port); - } - } else { - can_idx = mp_obj_get_int(args[0]); - } - if (can_idx < 1 || can_idx > MP_ARRAY_SIZE(MP_STATE_PORT(pyb_can_obj_all))) { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%d) doesn't exist"), can_idx); - } - - // check if the CAN is reserved for system use or not - if (MICROPY_HW_CAN_IS_RESERVED(can_idx)) { - mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("CAN(%d) is reserved"), can_idx); - } + mp_uint_t can_idx = machine_can_get_index(args[0]); // 0-based index pyb_can_obj_t *self; - if (MP_STATE_PORT(pyb_can_obj_all)[can_idx - 1] == NULL) { + if (MP_STATE_PORT(pyb_can_obj_all)[can_idx] == NULL) { self = mp_obj_malloc(pyb_can_obj_t, &pyb_can_type); - self->can_id = can_idx; + self->can_id = can_idx + 1; // ID is 1-based self->is_enabled = false; - MP_STATE_PORT(pyb_can_obj_all)[can_idx - 1] = self; + MP_STATE_PORT(pyb_can_obj_all)[can_idx] = self; } else { - self = MP_STATE_PORT(pyb_can_obj_all)[can_idx - 1]; + self = MP_STATE_PORT(pyb_can_obj_all)[can_idx]; } if (!self->is_enabled || n_args > 1) { @@ -381,25 +319,7 @@ static mp_obj_t pyb_can_restart(mp_obj_t self_in) { if (!self->is_enabled) { mp_raise_ValueError(NULL); } - CAN_TypeDef *can = self->can.Instance; - #if MICROPY_HW_ENABLE_FDCAN - can->CCCR |= FDCAN_CCCR_INIT; - while ((can->CCCR & FDCAN_CCCR_INIT) == 0) { - } - can->CCCR |= FDCAN_CCCR_CCE; - while ((can->CCCR & FDCAN_CCCR_CCE) == 0) { - } - can->CCCR &= ~FDCAN_CCCR_INIT; - while ((can->CCCR & FDCAN_CCCR_INIT)) { - } - #else - can->MCR |= CAN_MCR_INRQ; - while ((can->MSR & CAN_MSR_INAK) == 0) { - } - can->MCR &= ~CAN_MCR_INRQ; - while ((can->MSR & CAN_MSR_INAK)) { - } - #endif + can_restart(&self->can); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_1(pyb_can_restart_obj, pyb_can_restart); @@ -407,77 +327,30 @@ static MP_DEFINE_CONST_FUN_OBJ_1(pyb_can_restart_obj, pyb_can_restart); // Get the state of the controller static mp_obj_t pyb_can_state(mp_obj_t self_in) { pyb_can_obj_t *self = MP_OBJ_TO_PTR(self_in); - mp_int_t state = CAN_STATE_STOPPED; if (self->is_enabled) { - CAN_TypeDef *can = self->can.Instance; - #if MICROPY_HW_ENABLE_FDCAN - uint32_t psr = can->PSR; - if (psr & FDCAN_PSR_BO) { - state = CAN_STATE_BUS_OFF; - } else if (psr & FDCAN_PSR_EP) { - state = CAN_STATE_ERROR_PASSIVE; - } else if (psr & FDCAN_PSR_EW) { - state = CAN_STATE_ERROR_WARNING; - } else { - state = CAN_STATE_ERROR_ACTIVE; - } - #else - if (can->ESR & CAN_ESR_BOFF) { - state = CAN_STATE_BUS_OFF; - } else if (can->ESR & CAN_ESR_EPVF) { - state = CAN_STATE_ERROR_PASSIVE; - } else if (can->ESR & CAN_ESR_EWGF) { - state = CAN_STATE_ERROR_WARNING; - } else { - state = CAN_STATE_ERROR_ACTIVE; - } - #endif + return MP_OBJ_NEW_SMALL_INT(can_get_state(&self->can)); + } else { + return MP_OBJ_NEW_SMALL_INT(CAN_STATE_STOPPED); } - return MP_OBJ_NEW_SMALL_INT(state); } static MP_DEFINE_CONST_FUN_OBJ_1(pyb_can_state_obj, pyb_can_state); // Get info about error states and TX/RX buffers static mp_obj_t pyb_can_info(size_t n_args, const mp_obj_t *args) { pyb_can_obj_t *self = MP_OBJ_TO_PTR(args[0]); - mp_obj_list_t *list; - if (n_args == 1) { - list = MP_OBJ_TO_PTR(mp_obj_new_list(8, NULL)); - } else { - if (!mp_obj_is_type(args[1], &mp_type_list)) { - mp_raise_TypeError(NULL); - } - list = MP_OBJ_TO_PTR(args[1]); - if (list->len < 8) { - mp_raise_ValueError(NULL); - } - } + mp_obj_list_t *list = mp_obj_list_optional_arg(n_args > 1 ? args[1] : mp_const_none, 8); + can_counters_t hw_counters; - #if MICROPY_HW_ENABLE_FDCAN - FDCAN_GlobalTypeDef *can = self->can.Instance; - uint32_t esr = can->ECR; - list->items[0] = MP_OBJ_NEW_SMALL_INT((esr & FDCAN_ECR_TEC_Msk) >> FDCAN_ECR_TEC_Pos); - list->items[1] = MP_OBJ_NEW_SMALL_INT((esr & FDCAN_ECR_REC_Msk) >> FDCAN_ECR_REC_Pos); - list->items[2] = MP_OBJ_NEW_SMALL_INT(self->num_error_warning); - list->items[3] = MP_OBJ_NEW_SMALL_INT(self->num_error_passive); - list->items[4] = MP_OBJ_NEW_SMALL_INT(self->num_bus_off); - uint32_t TXEFS = can->TXEFS; - list->items[5] = MP_OBJ_NEW_SMALL_INT(TXEFS & 0x7); - list->items[6] = MP_OBJ_NEW_SMALL_INT((can->RXF0S & FDCAN_RXF0S_F0FL_Msk) >> FDCAN_RXF0S_F0FL_Pos); - list->items[7] = MP_OBJ_NEW_SMALL_INT((can->RXF1S & FDCAN_RXF1S_F1FL_Msk) >> FDCAN_RXF1S_F1FL_Pos); - #else - CAN_TypeDef *can = self->can.Instance; - uint32_t esr = can->ESR; - list->items[0] = MP_OBJ_NEW_SMALL_INT(esr >> CAN_ESR_TEC_Pos & 0xff); - list->items[1] = MP_OBJ_NEW_SMALL_INT(esr >> CAN_ESR_REC_Pos & 0xff); + can_get_counters(&self->can, &hw_counters); + + list->items[0] = MP_OBJ_NEW_SMALL_INT(hw_counters.tec); + list->items[1] = MP_OBJ_NEW_SMALL_INT(hw_counters.rec); list->items[2] = MP_OBJ_NEW_SMALL_INT(self->num_error_warning); list->items[3] = MP_OBJ_NEW_SMALL_INT(self->num_error_passive); list->items[4] = MP_OBJ_NEW_SMALL_INT(self->num_bus_off); - int n_tx_pending = 0x01121223 >> ((can->TSR >> CAN_TSR_TME_Pos & 7) << 2) & 0xf; - list->items[5] = MP_OBJ_NEW_SMALL_INT(n_tx_pending); - list->items[6] = MP_OBJ_NEW_SMALL_INT(can->RF0R >> CAN_RF0R_FMP0_Pos & 3); - list->items[7] = MP_OBJ_NEW_SMALL_INT(can->RF1R >> CAN_RF1R_FMP1_Pos & 3); - #endif + list->items[5] = MP_OBJ_NEW_SMALL_INT(hw_counters.tx_pending); + list->items[6] = MP_OBJ_NEW_SMALL_INT(hw_counters.rx_fifo0_pending); + list->items[7] = MP_OBJ_NEW_SMALL_INT(hw_counters.rx_fifo1_pending); return MP_OBJ_FROM_PTR(list); } @@ -487,7 +360,7 @@ static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(pyb_can_info_obj, 1, 2, pyb_can_info) static mp_obj_t pyb_can_any(mp_obj_t self_in, mp_obj_t fifo_in) { pyb_can_obj_t *self = MP_OBJ_TO_PTR(self_in); can_rx_fifo_t fifo = mp_obj_get_int(fifo_in); - return mp_obj_new_bool(can_rx_pending(&self->can, fifo) != 0); + return mp_obj_new_bool(can_is_rx_pending(&self->can, fifo) != 0); } static MP_DEFINE_CONST_FUN_OBJ_2(pyb_can_any_obj, pyb_can_any); @@ -554,13 +427,7 @@ static mp_obj_t pyb_can_send(size_t n_args, const mp_obj_t *pos_args, mp_map_t * } else { tx_msg.BitRateSwitch = FDCAN_BRS_ON; } - // Roundup DataLength to next DLC size and encode to DLC. - for (mp_uint_t i = 0; i < MP_ARRAY_SIZE(DLCtoBytes); i++) { - if (bufinfo.len <= DLCtoBytes[i]) { - tx_msg.DataLength = (i << 16); - break; - } - } + tx_msg.DataLength = bufinfo.len; // Converted to DLC encoding inside can_transmit #else tx_msg.DLC = bufinfo.len; uint8_t *tx_data = tx_msg.Data; // Data is uint32_t but holds only 1 byte @@ -634,7 +501,7 @@ static mp_obj_t pyb_can_recv(size_t n_args, const mp_obj_t *pos_args, mp_map_t * // Manage the rx state machine if ((fifo == CAN_RX_FIFO0 && self->rxcallback0 != mp_const_none) || (fifo == CAN_RX_FIFO1 && self->rxcallback1 != mp_const_none)) { - bool fifo_empty = can_rx_pending(&self->can, fifo) == 0; + bool fifo_empty = can_is_rx_pending(&self->can, fifo) == 0; byte *state = (fifo == CAN_RX_FIFO0) ? &self->rx_state0 : &self->rx_state1; switch (*state) { case RX_STATE_FIFO_EMPTY: @@ -656,24 +523,14 @@ static mp_obj_t pyb_can_recv(size_t n_args, const mp_obj_t *pos_args, mp_map_t * can_enable_rx_interrupts(&self->can, fifo, fifo_empty); } - // Create the tuple, or get the list, that will hold the return values + // Create or get the list, that will hold the return values // Also populate the fifth element, either a new bytes or reuse existing memoryview - mp_obj_t ret_obj = args[ARG_list].u_obj; - mp_obj_t *items; - if (ret_obj == mp_const_none) { - ret_obj = mp_obj_new_tuple(5, NULL); - items = ((mp_obj_tuple_t *)MP_OBJ_TO_PTR(ret_obj))->items; + mp_obj_list_t *list = mp_obj_list_optional_arg(args[ARG_list].u_obj, 5); + mp_obj_t *items = list->items; + if (MP_OBJ_FROM_PTR(list) != args[ARG_list].u_obj) { + // If newly allocated, create item 4 items[4] = mp_obj_new_bytes(rx_data, rx_dlc); } else { - // User should provide a list of length at least 5 to hold the values - if (!mp_obj_is_type(ret_obj, &mp_type_list)) { - mp_raise_TypeError(NULL); - } - mp_obj_list_t *list = MP_OBJ_TO_PTR(ret_obj); - if (list->len < 5) { - mp_raise_ValueError(NULL); - } - items = list->items; // Fifth element must be a memoryview which we assume points to a // byte-like array which is large enough, and then we resize it inplace if (!mp_obj_is_type(items[4], &mp_type_memoryview)) { @@ -702,7 +559,7 @@ static mp_obj_t pyb_can_recv(size_t n_args, const mp_obj_t *pos_args, mp_map_t * #endif // Return the result - return ret_obj; + return MP_OBJ_FROM_PTR(list); } static MP_DEFINE_CONST_FUN_OBJ_KW(pyb_can_recv_obj, 1, pyb_can_recv); @@ -921,20 +778,6 @@ static mp_obj_t pyb_can_rxcallback(mp_obj_t self_in, mp_obj_t fifo_in, mp_obj_t *callback = callback_in; } else if (mp_obj_is_callable(callback_in)) { *callback = callback_in; - uint32_t irq = 0; - if (self->can_id == PYB_CAN_1) { - irq = (fifo == CAN_RX_FIFO0) ? CAN1_RX0_IRQn : CAN1_RX1_IRQn; - #if defined(CAN2) - } else if (self->can_id == PYB_CAN_2) { - irq = (fifo == CAN_RX_FIFO0) ? CAN2_RX0_IRQn : CAN2_RX1_IRQn; - #endif - #if defined(CAN3) - } else { - irq = (fifo == CAN_RX_FIFO0) ? CAN3_RX0_IRQn : CAN3_RX1_IRQn; - #endif - } - NVIC_SetPriority(irq, IRQ_PRI_CAN); - HAL_NVIC_EnableIRQ(irq); can_enable_rx_interrupts(&self->can, fifo, true); } return mp_const_none; @@ -995,8 +838,8 @@ static mp_uint_t can_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, i uintptr_t flags = arg; ret = 0; if ((flags & MP_STREAM_POLL_RD) - && ((can_rx_pending(&self->can, 0) != 0) - || (can_rx_pending(&self->can, 1) != 0))) { + && ((can_is_rx_pending(&self->can, 0) != 0) + || (can_is_rx_pending(&self->can, 1) != 0))) { ret |= MP_STREAM_POLL_RD; } #if MICROPY_HW_ENABLE_FDCAN @@ -1016,12 +859,15 @@ static mp_uint_t can_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, i // IRQ handler, called from lower layer can.c or fdcan.c in ISR context -void can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo) { +void pyb_can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo) { mp_obj_t callback; pyb_can_obj_t *self; byte *state; self = MP_STATE_PORT(pyb_can_obj_all)[can_id - 1]; + if (self == NULL) { + return; // Should only hit this code path if machine.CAN has enabled interrupt + } if (fifo == CAN_RX_FIFO0) { callback = self->rxcallback0; @@ -1056,7 +902,7 @@ void can_irq_handler(uint can_id, can_int_t interrupt, can_rx_fifo_t fifo) { return; default: - return; // Should be unreachable + return; // CAN_INT_TX_COMPLETE is ignored by pyb.CAN } // Run the callback diff --git a/ports/stm32/pyb_can.h b/ports/stm32/pyb_can.h index a82043d7863ed..c548bc9367d35 100644 --- a/ports/stm32/pyb_can.h +++ b/ports/stm32/pyb_can.h @@ -52,7 +52,5 @@ extern const mp_obj_type_t pyb_can_type; void pyb_can_deinit_all(void); void pyb_can_init0(void); -void pyb_can_irq_handler(uint can_id, can_rx_fifo_t fifo, can_int_t interrupt); - #endif #endif diff --git a/ports/stm32/timer.c b/ports/stm32/timer.c index a0db596307970..c05c4716d475d 100644 --- a/ports/stm32/timer.c +++ b/ports/stm32/timer.c @@ -149,6 +149,8 @@ TIM_HandleTypeDef TIM6_Handle; #define PYB_TIMER_OBJ_ALL_NUM MP_ARRAY_SIZE(MP_STATE_PORT(pyb_timer_obj_all)) +static const uint32_t tim_instance_table[MICROPY_HW_MAX_TIMER]; + static mp_obj_t pyb_timer_deinit(mp_obj_t self_in); static mp_obj_t pyb_timer_callback(mp_obj_t self_in, mp_obj_t callback); static mp_obj_t pyb_timer_channel_callback(mp_obj_t self_in, mp_obj_t callback); @@ -229,6 +231,124 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { #endif } +TIM_TypeDef *timer_id_to_reg(uint32_t tim_id) { + return (TIM_TypeDef *)(tim_instance_table[tim_id - 1] & 0xffffff00); +} + +void timer_clock_enable(size_t tim_id) { + // enable TIM clock + switch (tim_id) { + #if defined(TIM1) + case 1: + __HAL_RCC_TIM1_CLK_ENABLE(); + break; + #endif + case 2: + __HAL_RCC_TIM2_CLK_ENABLE(); + break; + #if defined(TIM3) + case 3: + __HAL_RCC_TIM3_CLK_ENABLE(); + break; + #endif + #if defined(TIM4) + case 4: + __HAL_RCC_TIM4_CLK_ENABLE(); + break; + #endif + #if defined(TIM5) + case 5: + __HAL_RCC_TIM5_CLK_ENABLE(); + break; + #endif + #if defined(TIM6) + case 6: + __HAL_RCC_TIM6_CLK_ENABLE(); + break; + #endif + #if defined(TIM7) + case 7: + __HAL_RCC_TIM7_CLK_ENABLE(); + break; + #endif + #if defined(TIM8) + case 8: + __HAL_RCC_TIM8_CLK_ENABLE(); + break; + #endif + #if defined(TIM9) + case 9: + __HAL_RCC_TIM9_CLK_ENABLE(); + break; + #endif + #if defined(TIM10) + case 10: + __HAL_RCC_TIM10_CLK_ENABLE(); + break; + #endif + #if defined(TIM11) + case 11: + __HAL_RCC_TIM11_CLK_ENABLE(); + break; + #endif + #if defined(TIM12) + case 12: + __HAL_RCC_TIM12_CLK_ENABLE(); + break; + #endif + #if defined(TIM13) + case 13: + __HAL_RCC_TIM13_CLK_ENABLE(); + break; + #endif + #if defined(TIM14) + case 14: + __HAL_RCC_TIM14_CLK_ENABLE(); + break; + #endif + #if defined(TIM15) + case 15: + __HAL_RCC_TIM15_CLK_ENABLE(); + break; + #endif + #if defined(TIM16) + case 16: + __HAL_RCC_TIM16_CLK_ENABLE(); + break; + #endif + #if defined(TIM17) + case 17: + __HAL_RCC_TIM17_CLK_ENABLE(); + break; + #endif + #if defined(TIM18) + case 18: + __HAL_RCC_TIM18_CLK_ENABLE(); + break; + #endif + #if defined(TIM19) + case 19: + __HAL_RCC_TIM19_CLK_ENABLE(); + break; + #endif + #if defined(TIM20) + case 20: + __HAL_RCC_TIM20_CLK_ENABLE(); + break; + #endif + #if defined(TIM21) + case 21: + __HAL_RCC_TIM21_CLK_ENABLE(); + break; + #endif + #if defined(TIM22) + case 22: + __HAL_RCC_TIM22_CLK_ENABLE(); + break; + #endif + } +} + // Get the frequency (in Hz) of the source clock for the given timer. // On STM32F405/407/415/417 there are 2 cases for how the clock freq is set. // If the APB prescaler is 1, then the timer clock is equal to its respective @@ -694,117 +814,7 @@ static mp_obj_t pyb_timer_init_helper(pyb_timer_obj_t *self, size_t n_args, cons init->RepetitionCounter = 0; #endif - // enable TIM clock - switch (self->tim_id) { - #if defined(TIM1) - case 1: - __HAL_RCC_TIM1_CLK_ENABLE(); - break; - #endif - case 2: - __HAL_RCC_TIM2_CLK_ENABLE(); - break; - #if defined(TIM3) - case 3: - __HAL_RCC_TIM3_CLK_ENABLE(); - break; - #endif - #if defined(TIM4) - case 4: - __HAL_RCC_TIM4_CLK_ENABLE(); - break; - #endif - #if defined(TIM5) - case 5: - __HAL_RCC_TIM5_CLK_ENABLE(); - break; - #endif - #if defined(TIM6) - case 6: - __HAL_RCC_TIM6_CLK_ENABLE(); - break; - #endif - #if defined(TIM7) - case 7: - __HAL_RCC_TIM7_CLK_ENABLE(); - break; - #endif - #if defined(TIM8) - case 8: - __HAL_RCC_TIM8_CLK_ENABLE(); - break; - #endif - #if defined(TIM9) - case 9: - __HAL_RCC_TIM9_CLK_ENABLE(); - break; - #endif - #if defined(TIM10) - case 10: - __HAL_RCC_TIM10_CLK_ENABLE(); - break; - #endif - #if defined(TIM11) - case 11: - __HAL_RCC_TIM11_CLK_ENABLE(); - break; - #endif - #if defined(TIM12) - case 12: - __HAL_RCC_TIM12_CLK_ENABLE(); - break; - #endif - #if defined(TIM13) - case 13: - __HAL_RCC_TIM13_CLK_ENABLE(); - break; - #endif - #if defined(TIM14) - case 14: - __HAL_RCC_TIM14_CLK_ENABLE(); - break; - #endif - #if defined(TIM15) - case 15: - __HAL_RCC_TIM15_CLK_ENABLE(); - break; - #endif - #if defined(TIM16) - case 16: - __HAL_RCC_TIM16_CLK_ENABLE(); - break; - #endif - #if defined(TIM17) - case 17: - __HAL_RCC_TIM17_CLK_ENABLE(); - break; - #endif - #if defined(TIM18) - case 18: - __HAL_RCC_TIM18_CLK_ENABLE(); - break; - #endif - #if defined(TIM19) - case 19: - __HAL_RCC_TIM19_CLK_ENABLE(); - break; - #endif - #if defined(TIM20) - case 20: - __HAL_RCC_TIM20_CLK_ENABLE(); - break; - #endif - #if defined(TIM21) - case 21: - __HAL_RCC_TIM21_CLK_ENABLE(); - break; - #endif - #if defined(TIM22) - case 22: - __HAL_RCC_TIM22_CLK_ENABLE(); - break; - #endif - } + timer_clock_enable(self->tim_id); // set IRQ priority (if not a special timer) if (self->tim_id != 5) { @@ -1026,15 +1036,11 @@ static mp_obj_t pyb_timer_make_new(const mp_obj_type_t *type, size_t n_args, siz memset(tim, 0, sizeof(*tim)); tim->base.type = &pyb_timer_type; tim->tim_id = tim_id; - #if defined(STM32L1) - tim->is_32bit = tim_id == 5; - #else - tim->is_32bit = tim_id == 2 || tim_id == 5; - #endif tim->callback = mp_const_none; uint32_t ti = tim_instance_table[tim_id - 1]; tim->tim.Instance = (TIM_TypeDef *)(ti & 0xffffff00); tim->irqn = ti & 0xff; + tim->is_32bit = IS_TIM_32B_COUNTER_INSTANCE(tim->tim.Instance); MP_STATE_PORT(pyb_timer_obj_all)[tim_id - 1] = tim; } else { // reference existing Timer object diff --git a/ports/stm32/timer.h b/ports/stm32/timer.h index 2ba91cf158d96..10aa81bd52c73 100644 --- a/ports/stm32/timer.h +++ b/ports/stm32/timer.h @@ -26,6 +26,11 @@ #ifndef MICROPY_INCLUDED_STM32_TIMER_H #define MICROPY_INCLUDED_STM32_TIMER_H +// Define this helper macro for MCUs that the HAL misses. +#if defined(STM32L0) +#define IS_TIM_32B_COUNTER_INSTANCE(tim) (false) +#endif + extern TIM_HandleTypeDef TIM5_Handle; extern const mp_obj_type_t pyb_timer_type; @@ -34,6 +39,8 @@ void timer_init0(void); void timer_tim5_init(void); TIM_HandleTypeDef *timer_tim6_init(uint freq); void timer_deinit(void); +TIM_TypeDef *timer_id_to_reg(uint32_t tim_id); +void timer_clock_enable(size_t tim_id); uint32_t timer_get_source_freq(uint32_t tim_id); void timer_irq_handler(uint tim_id); diff --git a/ports/unix/coverage.c b/ports/unix/coverage.c index f5f3657aca8ba..79ec07f45e1aa 100644 --- a/ports/unix/coverage.c +++ b/ports/unix/coverage.c @@ -548,6 +548,45 @@ static mp_obj_t extra_coverage(void) { mp_printf(&mp_plat_print, "%x%08x\n", (uint32_t)(value_ll >> 32), (uint32_t)value_ll); } + // list argument helpers + { + mp_printf(&mp_plat_print, "# list argument helpers\n"); + + // Create a list to test with + mp_obj_t list_items[] = { mp_const_none, MP_OBJ_NEW_SMALL_INT(77), mp_obj_new_str_from_cstr("hello") }; + size_t list_len = MP_ARRAY_SIZE(list_items); + mp_obj_t list = mp_obj_new_list(list_len, list_items); + + // mp_obj_list_ensure + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_obj_list_ensure(MP_OBJ_NEW_SMALL_INT(-1), 5); // Not a list + nlr_pop(); + } else { + mp_obj_print_exception(&mp_plat_print, MP_OBJ_FROM_PTR(nlr.ret_val)); + } + + if (nlr_push(&nlr) == 0) { + mp_obj_list_ensure(list, list_len + 2); // List shorter than minimum length + nlr_pop(); + } else { + mp_obj_print_exception(&mp_plat_print, MP_OBJ_FROM_PTR(nlr.ret_val)); + } + + mp_obj_list_t *as_ptr = mp_obj_list_ensure(list, list_len); // Acceptable! + mp_printf(&mp_plat_print, "mp_obj_list_ensure same list? %d\n", MP_OBJ_TO_PTR(list) == as_ptr); + + // mp_obj_list_optional_arg() + as_ptr = mp_obj_list_optional_arg(list, list_len); + mp_printf(&mp_plat_print, "mp_obj_list_optional_arg same list? %d\n", MP_OBJ_TO_PTR(list) == as_ptr); + + as_ptr = mp_obj_list_optional_arg(mp_const_none, list_len); + mp_printf(&mp_plat_print, "mp_obj_list_optional_arg new list len %d\n", as_ptr->len); + + as_ptr = mp_obj_list_optional_arg(MP_OBJ_NULL, list_len); + mp_printf(&mp_plat_print, "mp_obj_list_optional_arg new list from NULL len %d\n", as_ptr->len); + } + // runtime utils { mp_printf(&mp_plat_print, "# runtime utils\n"); diff --git a/ports/webassembly/Makefile b/ports/webassembly/Makefile index 9a673b757b29c..3dbe24188786b 100644 --- a/ports/webassembly/Makefile +++ b/ports/webassembly/Makefile @@ -130,7 +130,7 @@ OBJ += $(addprefix $(BUILD)/, $(SRC_C:.c=.o)) ################################################################################ # Main targets. -.PHONY: all repl min test test_min +.PHONY: all repl min test test//% test_min all: $(BUILD)/micropython.mjs @@ -150,6 +150,9 @@ min: $(BUILD)/micropython.min.mjs test: $(BUILD)/micropython.mjs $(TOP)/tests/run-tests.py cd $(TOP)/tests && MICROPY_MICROPYTHON_MJS=../ports/webassembly/$< ./run-tests.py -t webassembly +test//%: $(BUILD)/micropython.mjs $(TOP)/tests/run-tests.py + cd $(TOP)/tests && MICROPY_MICROPYTHON_MJS=../ports/webassembly/$< ./run-tests.py -t webassembly -i "$*" + test_min: $(BUILD)/micropython.min.mjs $(TOP)/tests/run-tests.py cd $(TOP)/tests && MICROPY_MICROPYTHON_MJS=../ports/webassembly/$< ./run-tests.py -t webassembly diff --git a/ports/webassembly/variants/pyscript/mpconfigvariant.h b/ports/webassembly/variants/pyscript/mpconfigvariant.h index ed8e812803533..0b77efc4b32bf 100644 --- a/ports/webassembly/variants/pyscript/mpconfigvariant.h +++ b/ports/webassembly/variants/pyscript/mpconfigvariant.h @@ -1,3 +1,4 @@ #define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_FULL_FEATURES) #define MICROPY_GC_SPLIT_HEAP (1) #define MICROPY_GC_SPLIT_HEAP_AUTO (1) +#define MICROPY_PY_WEAKREF (1) diff --git a/py/gc.c b/py/gc.c index 0d4d19ce9e74c..5fe26ef8905c5 100644 --- a/py/gc.c +++ b/py/gc.c @@ -104,14 +104,21 @@ #if MICROPY_ENABLE_FINALISER // FTB = finaliser table byte // if set, then the corresponding block may have a finaliser - #define BLOCKS_PER_FTB (8) - #define FTB_GET(area, block) ((area->gc_finaliser_table_start[(block) / BLOCKS_PER_FTB] >> ((block) & 7)) & 1) #define FTB_SET(area, block) do { area->gc_finaliser_table_start[(block) / BLOCKS_PER_FTB] |= (1 << ((block) & 7)); } while (0) #define FTB_CLEAR(area, block) do { area->gc_finaliser_table_start[(block) / BLOCKS_PER_FTB] &= (~(1 << ((block) & 7))); } while (0) #endif +#if MICROPY_PY_WEAKREF +// WTB = weakref table byte +// if set, then the corresponding block may have a weakref in MP_STATE_VM(mp_weakref_map). +#define BLOCKS_PER_WTB (8) +#define WTB_GET(area, block) ((area->gc_weakref_table_start[(block) / BLOCKS_PER_WTB] >> ((block) & 7)) & 1) +#define WTB_SET(area, block) do { area->gc_weakref_table_start[(block) / BLOCKS_PER_WTB] |= (1 << ((block) & 7)); } while (0) +#define WTB_CLEAR(area, block) do { area->gc_weakref_table_start[(block) / BLOCKS_PER_WTB] &= (~(1 << ((block) & 7))); } while (0) +#endif + #if MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL #define GC_MUTEX_INIT() mp_thread_recursive_mutex_init(&MP_STATE_MEM(gc_mutex)) #define GC_ENTER() mp_thread_recursive_mutex_lock(&MP_STATE_MEM(gc_mutex), 1) @@ -138,17 +145,23 @@ static void gc_sweep_free_blocks(void); // TODO waste less memory; currently requires that all entries in alloc_table have a corresponding block in pool static void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) { // calculate parameters for GC (T=total, A=alloc table, F=finaliser table, P=pool; all in bytes): - // T = A + F + P + // T = A + F + W + P // F = A * BLOCKS_PER_ATB / BLOCKS_PER_FTB + // W = A * BLOCKS_PER_ATB / BLOCKS_PER_WTB // P = A * BLOCKS_PER_ATB * BYTES_PER_BLOCK - // => T = A * (1 + BLOCKS_PER_ATB / BLOCKS_PER_FTB + BLOCKS_PER_ATB * BYTES_PER_BLOCK) + // => T = A * (1 + BLOCKS_PER_ATB / BLOCKS_PER_FTB + BLOCKS_PER_ATB / BLOCKS_PER_WTB + BLOCKS_PER_ATB * BYTES_PER_BLOCK) size_t total_byte_len = (byte *)end - (byte *)start; - #if MICROPY_ENABLE_FINALISER + #if MICROPY_ENABLE_FINALISER || MICROPY_PY_WEAKREF area->gc_alloc_table_byte_len = (total_byte_len - ALLOC_TABLE_GAP_BYTE) * MP_BITS_PER_BYTE / ( MP_BITS_PER_BYTE + #if MICROPY_ENABLE_FINALISER + MP_BITS_PER_BYTE * BLOCKS_PER_ATB / BLOCKS_PER_FTB + #endif + #if MICROPY_PY_WEAKREF + + MP_BITS_PER_BYTE * BLOCKS_PER_ATB / BLOCKS_PER_WTB + #endif + MP_BITS_PER_BYTE * BLOCKS_PER_ATB * BYTES_PER_BLOCK ); #else @@ -157,26 +170,36 @@ static void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) { area->gc_alloc_table_start = (byte *)start; + // Allocate FTB and WTB blocks if they are enabled. + byte *next_table = area->gc_alloc_table_start + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE; + (void)next_table; #if MICROPY_ENABLE_FINALISER size_t gc_finaliser_table_byte_len = (area->gc_alloc_table_byte_len * BLOCKS_PER_ATB + BLOCKS_PER_FTB - 1) / BLOCKS_PER_FTB; - area->gc_finaliser_table_start = area->gc_alloc_table_start + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE; + area->gc_finaliser_table_start = next_table; + next_table += gc_finaliser_table_byte_len; + #endif + #if MICROPY_PY_WEAKREF + size_t gc_weakref_table_byte_len = (area->gc_alloc_table_byte_len * BLOCKS_PER_ATB + BLOCKS_PER_WTB - 1) / BLOCKS_PER_WTB; + area->gc_weakref_table_start = next_table; + next_table += gc_weakref_table_byte_len; #endif + // Allocate the GC pool of heap blocks. size_t gc_pool_block_len = area->gc_alloc_table_byte_len * BLOCKS_PER_ATB; area->gc_pool_start = (byte *)end - gc_pool_block_len * BYTES_PER_BLOCK; area->gc_pool_end = end; + assert(area->gc_pool_start >= next_table); - #if MICROPY_ENABLE_FINALISER - assert(area->gc_pool_start >= area->gc_finaliser_table_start + gc_finaliser_table_byte_len); - #endif - - #if MICROPY_ENABLE_FINALISER - // clear ATB's and FTB's - memset(area->gc_alloc_table_start, 0, gc_finaliser_table_byte_len + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE); - #else - // clear ATB's - memset(area->gc_alloc_table_start, 0, area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE); - #endif + // Clear ATB's, and FTB's and WTB's if they are enabled. + memset(area->gc_alloc_table_start, 0, + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE + #if MICROPY_ENABLE_FINALISER + + gc_finaliser_table_byte_len + #endif + #if MICROPY_PY_WEAKREF + + gc_weakref_table_byte_len + #endif + ); area->gc_last_free_atb_index = 0; area->gc_last_used_block = 0; @@ -196,6 +219,12 @@ static void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) { gc_finaliser_table_byte_len, gc_finaliser_table_byte_len * BLOCKS_PER_FTB); #endif + #if MICROPY_PY_WEAKREF + DEBUG_printf(" weakref table at %p, length " UINT_FMT " bytes, " + UINT_FMT " blocks\n", area->gc_weakref_table_start, + gc_weakref_table_byte_len, + gc_weakref_table_byte_len * BLOCKS_PER_WTB); + #endif DEBUG_printf(" pool at %p, length " UINT_FMT " bytes, " UINT_FMT " blocks\n", area->gc_pool_start, gc_pool_block_len * BYTES_PER_BLOCK, gc_pool_block_len); @@ -310,6 +339,9 @@ static bool gc_try_add_heap(size_t failed_alloc) { #if MICROPY_ENABLE_FINALISER + total_blocks / BLOCKS_PER_FTB #endif + #if MICROPY_PY_WEAKREF + + total_blocks / BLOCKS_PER_WTB + #endif + total_blocks * BYTES_PER_BLOCK + ALLOC_TABLE_GAP_BYTE + sizeof(mp_state_mem_area_t); @@ -556,6 +588,9 @@ void gc_collect_end(void) { } MP_STATE_THREAD(gc_lock_depth) &= ~GC_COLLECT_FLAG; GC_EXIT(); + #if MICROPY_PY_WEAKREF + gc_weakref_sweep(); + #endif } static void gc_deal_with_stack_overflow(void) { @@ -581,12 +616,16 @@ static void gc_deal_with_stack_overflow(void) { // Run finalisers for all to-be-freed blocks static void gc_sweep_run_finalisers(void) { - #if MICROPY_ENABLE_FINALISER + #if MICROPY_ENABLE_FINALISER || MICROPY_PY_WEAKREF + #if MICROPY_ENABLE_FINALISER && MICROPY_PY_WEAKREF + MP_STATIC_ASSERT(BLOCKS_PER_FTB == BLOCKS_PER_WTB); + #endif for (const mp_state_mem_area_t *area = &MP_STATE_MEM(area); area != NULL; area = NEXT_AREA(area)) { assert(area->gc_last_used_block <= area->gc_alloc_table_byte_len * BLOCKS_PER_ATB); // Small speed optimisation: skip over empty FTB blocks size_t ftb_end = area->gc_last_used_block / BLOCKS_PER_FTB; // index is inclusive for (size_t ftb_idx = 0; ftb_idx <= ftb_end; ftb_idx++) { + #if MICROPY_ENABLE_FINALISER byte ftb = area->gc_finaliser_table_start[ftb_idx]; size_t block = ftb_idx * BLOCKS_PER_FTB; while (ftb) { @@ -616,9 +655,26 @@ static void gc_sweep_run_finalisers(void) { ftb >>= 1; block++; } + #endif + #if MICROPY_PY_WEAKREF + byte wtb = area->gc_weakref_table_start[ftb_idx]; + block = ftb_idx * BLOCKS_PER_WTB; + while (wtb) { + MICROPY_GC_HOOK_LOOP(block); + if (wtb & 1) { // WTB_GET(area, block) shortcut + if (ATB_GET_KIND(area, block) == AT_HEAD) { + mp_obj_base_t *obj = (mp_obj_base_t *)PTR_FROM_BLOCK(area, block); + gc_weakref_about_to_be_freed(obj); + WTB_CLEAR(area, block); + } + } + wtb >>= 1; + block++; + } + #endif } } - #endif // MICROPY_ENABLE_FINALISER + #endif // MICROPY_ENABLE_FINALISER || MICROPY_PY_WEAKREF } // Free unmarked heads and their tails @@ -769,6 +825,25 @@ void gc_info(gc_info_t *info) { GC_EXIT(); } +#if MICROPY_PY_WEAKREF +// Mark the GC heap pointer as having a weakref. +void gc_weakref_mark(void *ptr) { + mp_state_mem_area_t *area; + #if MICROPY_GC_SPLIT_HEAP + area = gc_get_ptr_area(ptr); + assert(area); + #else + assert(VERIFY_PTR(ptr)); + area = &MP_STATE_MEM(area); + #endif + + size_t block = BLOCK_FROM_PTR(area, ptr); + assert(ATB_GET_KIND(area, block) == AT_HEAD); + + WTB_SET(area, block); +} +#endif + void *gc_alloc(size_t n_bytes, unsigned int alloc_flags) { bool has_finaliser = alloc_flags & GC_ALLOC_FLAG_HAS_FINALISER; size_t n_blocks = ((n_bytes + BYTES_PER_BLOCK - 1) & (~(BYTES_PER_BLOCK - 1))) / BYTES_PER_BLOCK; @@ -967,6 +1042,11 @@ void gc_free(void *ptr) { FTB_CLEAR(area, block); #endif + #if MICROPY_PY_WEAKREF + // Objects that have a weak reference should not be explicitly freed. + assert(!WTB_GET(area, block)); + #endif + #if MICROPY_GC_SPLIT_HEAP if (MP_STATE_MEM(gc_last_free_area) != area) { // We freed something but it isn't the current area. Reset the diff --git a/py/gc.h b/py/gc.h index 36177633062b2..4679d6dc8632f 100644 --- a/py/gc.h +++ b/py/gc.h @@ -58,6 +58,11 @@ void gc_collect_end(void); // Use this function to sweep the whole heap and run all finalisers void gc_sweep_all(void); +// These functions are used to manage weakrefs. +void gc_weakref_mark(void *ptr); +void gc_weakref_about_to_be_freed(void *ptr); +void gc_weakref_sweep(void); + enum { GC_ALLOC_FLAG_HAS_FINALISER = 1, }; diff --git a/py/modweakref.c b/py/modweakref.c new file mode 100644 index 0000000000000..3360a4e20950d --- /dev/null +++ b/py/modweakref.c @@ -0,0 +1,314 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "py/gc.h" +#include "py/runtime.h" + +#if MICROPY_PY_WEAKREF + +// Macros to obfuscate a heap pointer as a small integer object. +#define PTR_TO_INT_OBJ(ptr) (MP_OBJ_NEW_SMALL_INT(((uintptr_t)ptr) >> 1)) +#define PTR_FROM_INT_OBJ(obj) ((void *)(MP_OBJ_SMALL_INT_VALUE((obj)) << 1)) + +// Macros to convert between a weak reference and a heap pointer. +#define WEAK_REFERENCE_FROM_HEAP_PTR(ptr) PTR_TO_INT_OBJ(ptr) +#define WEAK_REFERENCE_TO_HEAP_PTR(weak_ref) PTR_FROM_INT_OBJ(weak_ref) + +// Macros to manage ref-finalizer linked-list pointers. +// - mp_obj_ref_t is obfuscated as a small integer object so it's not traced by the GC. +// - mp_obj_finalize_t is stored as-is so it is traced by the GC. +#define REF_FIN_LIST_OBJ_IS_FIN(r) (!mp_obj_is_small_int((r))) +#define REF_FIN_LIST_OBJ_TO_PTR(r) ((mp_obj_ref_t *)(mp_obj_is_small_int((r)) ? PTR_FROM_INT_OBJ((r)) : MP_OBJ_TO_PTR((r)))) +#define REF_FIN_LIST_OBJ_FROM_REF(r) (PTR_TO_INT_OBJ((r))) +#define REF_FIN_LIST_OBJ_FROM_FIN(r) (MP_OBJ_FROM_PTR((r))) +#define REF_FIN_LIST_OBJ_TAIL (PTR_TO_INT_OBJ(NULL)) + +// weakref.ref() instance. +typedef struct _mp_obj_ref_t { + mp_obj_base_t base; + mp_obj_t ref_fin_next; + mp_obj_t obj_weak_ref; + mp_obj_t callback; +} mp_obj_ref_t; + +// weakref.finalize() instance. +// This is an extension of weakref.ref() and shares a lot of code with it. +typedef struct _mp_obj_finalize_t { + mp_obj_ref_t base; + size_t n_args; + size_t n_kw; + mp_obj_t *args; +} mp_obj_finalize_t; + +static const mp_obj_type_t mp_type_ref; +static const mp_obj_type_t mp_type_finalize; + +static mp_obj_t ref___del__(mp_obj_t self_in); + +void gc_weakref_about_to_be_freed(void *ptr) { + mp_obj_t idx = WEAK_REFERENCE_FROM_HEAP_PTR(ptr); + mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), idx, MP_MAP_LOOKUP); + if (elem != NULL) { + // Mark element as being freed. + elem->key = mp_const_none; + } +} + +void gc_weakref_sweep(void) { + mp_map_t *map = &MP_STATE_VM(mp_weakref_map); + for (size_t i = 0; i < map->alloc; i++) { + if (map->table[i].key == mp_const_none) { + // Element was just freed, so call all the registered callbacks. + --map->used; + map->table[i].key = MP_OBJ_SENTINEL; + mp_obj_ref_t *ref = REF_FIN_LIST_OBJ_TO_PTR(map->table[i].value); + map->table[i].value = MP_OBJ_NULL; + while (ref != NULL) { + // Invalidate the weak reference. + assert(ref->obj_weak_ref != mp_const_none); + ref->obj_weak_ref = mp_const_none; + + // Call any registered callbacks. + if (ref->callback != mp_const_none) { + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + if (ref->base.type == &mp_type_ref) { + // weakref.ref() type. + mp_call_function_1(ref->callback, MP_OBJ_FROM_PTR(ref)); + } else { + // weakref.finalize() type. + mp_obj_finalize_t *fin = (mp_obj_finalize_t *)ref; + mp_call_function_n_kw(fin->base.callback, fin->n_args, fin->n_kw, fin->args); + } + nlr_pop(); + } else { + mp_printf(MICROPY_ERROR_PRINTER, "Unhandled exception in weakref callback:\n"); + mp_obj_print_exception(MICROPY_ERROR_PRINTER, MP_OBJ_FROM_PTR(nlr.ret_val)); + } + } + + // Unlink the node. + mp_obj_ref_t *ref_fin_next = REF_FIN_LIST_OBJ_TO_PTR(ref->ref_fin_next); + ref->ref_fin_next = REF_FIN_LIST_OBJ_TAIL; + ref = ref_fin_next; + } + } + } +} + +static mp_obj_t mp_obj_ref_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { + if (type == &mp_type_ref) { + // weakref.ref() type. + mp_arg_check_num(n_args, n_kw, 1, 2, false); + } else { + // weakref.finalize() type. + mp_arg_check_num(n_args, n_kw, 2, MP_OBJ_FUN_ARGS_MAX, true); + } + + // Validate the input object can have a weakref. + void *ptr = NULL; + if (mp_obj_is_obj(args[0])) { + ptr = MP_OBJ_TO_PTR(args[0]); + if (gc_nbytes(ptr) == 0) { + ptr = NULL; + } + } + if (ptr == NULL) { + mp_raise_TypeError(MP_ERROR_TEXT("not a heap object")); + } + + // Create or get the entry in mp_weakref_map corresponding to this object. + mp_obj_t obj_weak_reference = WEAK_REFERENCE_FROM_HEAP_PTR(ptr); + mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), obj_weak_reference, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND); + if (elem->value == MP_OBJ_NULL) { + // This heap object does not have any existing weakref's, so initialise it. + elem->value = REF_FIN_LIST_OBJ_TAIL; + gc_weakref_mark(ptr); + } + + mp_obj_ref_t *self; + if (type == &mp_type_ref) { + // Create a new weakref.ref() object. + self = mp_obj_malloc_with_finaliser(mp_obj_ref_t, type); + // Link this new ref into the list of all refs/finalizers pointing to this object. + // To ensure it will *NOT* be traced by the GC (the user must manually hold onto it), + // store an integer version of the object after any weakref.finalize() objects (so + // the weakref.finalize() objects continue to be traced by the GC). + mp_obj_t *link = &elem->value; + while (REF_FIN_LIST_OBJ_IS_FIN(*link)) { + link = &REF_FIN_LIST_OBJ_TO_PTR(*link)->ref_fin_next; + } + self->ref_fin_next = *link; + *link = REF_FIN_LIST_OBJ_FROM_REF(self); + } else { + // Create a new weakref.finalize() object. + mp_obj_finalize_t *self_fin = mp_obj_malloc(mp_obj_finalize_t, type); + self_fin->n_args = n_args - 2; + self_fin->n_kw = n_kw; + size_t n_args_kw = self_fin->n_args + self_fin->n_kw * 2; + if (n_args_kw == 0) { + self_fin->args = NULL; + } else { + self_fin->args = m_new(mp_obj_t, n_args_kw); + memcpy(self_fin->args, args + 2, n_args_kw * sizeof(mp_obj_t)); + } + self = &self_fin->base; + // Link this new finalizer into the list of all refs/finalizers pointing to this object. + // To ensure it will be traced by the GC, store its pointer at the start of the list. + self->ref_fin_next = elem->value; + elem->value = REF_FIN_LIST_OBJ_FROM_FIN(self_fin); + } + + // Populate the object weak reference, and the callback. + self->obj_weak_ref = obj_weak_reference; + if (n_args > 1) { + self->callback = args[1]; + } else { + self->callback = mp_const_none; + } + + return MP_OBJ_FROM_PTR(self); +} + +static mp_obj_t mp_obj_ref_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) { + mp_obj_ref_t *self = MP_OBJ_TO_PTR(self_in); + if (self->obj_weak_ref == mp_const_none) { + return mp_const_none; + } + if (self->base.type == &mp_type_ref) { + // weakref.ref() type. + return MP_OBJ_FROM_PTR(WEAK_REFERENCE_TO_HEAP_PTR(self->obj_weak_ref)); + } else { + // weakref.finalize() type. + mp_obj_finalize_t *self_fin = MP_OBJ_TO_PTR(self_in); + ref___del__(self_in); + return mp_call_function_n_kw(self_fin->base.callback, self_fin->n_args, self_fin->n_kw, self_fin->args); + } +} + +static mp_obj_t ref___del__(mp_obj_t self_in) { + mp_obj_ref_t *self = MP_OBJ_TO_PTR(self_in); + mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), self->obj_weak_ref, MP_MAP_LOOKUP); + if (elem != NULL) { + for (mp_obj_t *link = &elem->value; REF_FIN_LIST_OBJ_TO_PTR(*link) != NULL; link = &REF_FIN_LIST_OBJ_TO_PTR(*link)->ref_fin_next) { + if (self == REF_FIN_LIST_OBJ_TO_PTR(*link)) { + // Unlink and clear this node. + *link = self->ref_fin_next; + self->ref_fin_next = REF_FIN_LIST_OBJ_TAIL; + self->obj_weak_ref = mp_const_none; + break; + } + } + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(ref___del___obj, ref___del__); + +static mp_obj_t finalize_peek_detach_helper(mp_obj_t self_in, bool detach) { + mp_obj_finalize_t *self = MP_OBJ_TO_PTR(self_in); + if (self->base.obj_weak_ref == mp_const_none) { + return mp_const_none; + } + mp_obj_t tuple[4] = { + MP_OBJ_FROM_PTR(WEAK_REFERENCE_TO_HEAP_PTR(self->base.obj_weak_ref)), + self->base.callback, + mp_obj_new_tuple(self->n_args, self->args), + mp_obj_dict_make_new(&mp_type_dict, 0, self->n_kw, self->args + self->n_args), + }; + if (detach) { + ref___del__(self_in); + } + return mp_obj_new_tuple(MP_ARRAY_SIZE(tuple), tuple); +} + +static mp_obj_t finalize_peek(mp_obj_t self_in) { + return finalize_peek_detach_helper(self_in, false); +} +static MP_DEFINE_CONST_FUN_OBJ_1(finalize_peek_obj, finalize_peek); + +static mp_obj_t finalize_detach(mp_obj_t self_in) { + return finalize_peek_detach_helper(self_in, true); +} +static MP_DEFINE_CONST_FUN_OBJ_1(finalize_detach_obj, finalize_detach); + +static void mp_obj_finalize_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + if (dest[0] != MP_OBJ_NULL) { + // Store/delete attribute, unsupported. + return; + } + + if (attr == MP_QSTR_alive) { + mp_obj_finalize_t *self = MP_OBJ_TO_PTR(self_in); + dest[0] = mp_obj_new_bool(self->base.obj_weak_ref != mp_const_none); + return; + } else if (attr == MP_QSTR_peek) { + dest[0] = MP_OBJ_FROM_PTR(&finalize_peek_obj); + dest[1] = self_in; + } else if (attr == MP_QSTR_detach) { + dest[0] = MP_OBJ_FROM_PTR(&finalize_detach_obj); + dest[1] = self_in; + } +} + +static const mp_rom_map_elem_t ref_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&ref___del___obj) }, +}; +static MP_DEFINE_CONST_DICT(ref_locals_dict, ref_locals_dict_table); + +static MP_DEFINE_CONST_OBJ_TYPE( + mp_type_ref, + MP_QSTR_ref, + MP_TYPE_FLAG_NONE, + make_new, mp_obj_ref_make_new, + call, mp_obj_ref_call, + locals_dict, &ref_locals_dict + ); + +static MP_DEFINE_CONST_OBJ_TYPE( + mp_type_finalize, + MP_QSTR_finalize, + MP_TYPE_FLAG_NONE, + make_new, mp_obj_ref_make_new, + call, mp_obj_ref_call, + attr, mp_obj_finalize_attr + ); + +static const mp_rom_map_elem_t mp_module_weakref_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_weakref) }, + { MP_ROM_QSTR(MP_QSTR_ref), MP_ROM_PTR(&mp_type_ref) }, + { MP_ROM_QSTR(MP_QSTR_finalize), MP_ROM_PTR(&mp_type_finalize) }, +}; +static MP_DEFINE_CONST_DICT(mp_module_weakref_globals, mp_module_weakref_globals_table); + +const mp_obj_module_t mp_module_weakref = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&mp_module_weakref_globals, +}; + +MP_REGISTER_ROOT_POINTER(mp_map_t mp_weakref_map); +MP_REGISTER_MODULE(MP_QSTR_weakref, mp_module_weakref); + +#endif // MICROPY_PY_WEAKREF diff --git a/py/mpconfig.h b/py/mpconfig.h index d0150ec7b9995..0951651e7d6a3 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -1914,6 +1914,11 @@ typedef time_t mp_timestamp_t; #define MICROPY_PY_THREAD_RECURSIVE_MUTEX (MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL) #endif +// Whether to provide the "weakref" module. +#ifndef MICROPY_PY_WEAKREF +#define MICROPY_PY_WEAKREF (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EVERYTHING) +#endif + // Extended modules #ifndef MICROPY_PY_ASYNCIO diff --git a/py/mpstate.h b/py/mpstate.h index 325c12217521f..32d1adb13ed74 100644 --- a/py/mpstate.h +++ b/py/mpstate.h @@ -107,6 +107,9 @@ typedef struct _mp_state_mem_area_t { #if MICROPY_ENABLE_FINALISER byte *gc_finaliser_table_start; #endif + #if MICROPY_PY_WEAKREF + byte *gc_weakref_table_start; + #endif byte *gc_pool_start; byte *gc_pool_end; diff --git a/py/objlist.c b/py/objlist.c index d5aca03396034..41c920511d2d4 100644 --- a/py/objlist.c +++ b/py/objlist.c @@ -520,3 +520,22 @@ mp_obj_t mp_obj_new_list_iterator(mp_obj_t list, size_t cur, mp_obj_iter_buf_t * o->cur = cur; return MP_OBJ_FROM_PTR(o); } + +mp_obj_list_t *mp_obj_list_optional_arg(mp_obj_t arg_in, size_t min_len) { + if (arg_in == MP_OBJ_NULL || arg_in == mp_const_none) { + return MP_OBJ_TO_PTR(mp_obj_new_list(min_len, NULL)); + } else { + return mp_obj_list_ensure(arg_in, min_len); + } +} + +mp_obj_list_t *mp_obj_list_ensure(mp_obj_t in, size_t min_len) { + if (!mp_obj_is_type(in, &mp_type_list)) { + mp_raise_TypeError(NULL); + } + mp_obj_list_t *list = MP_OBJ_TO_PTR(in); + if (list->len < min_len) { + mp_raise_ValueError(NULL); + } + return list; +} diff --git a/py/objlist.h b/py/objlist.h index 1f4c70504c8d9..e72778266a3ba 100644 --- a/py/objlist.h +++ b/py/objlist.h @@ -60,4 +60,11 @@ static inline void mp_obj_list_store(mp_obj_t self_in, mp_obj_t index, mp_obj_t self->items[i] = value; } +// Helper function for pattern of an optional argument which can be a list of a specified size, and is +// allocated on-demand otherwise +mp_obj_list_t *mp_obj_list_optional_arg(mp_obj_t arg_in, size_t min_len); + +// Ensure provided object is a list of minimum length min_len. Raises TypeError & ValueError otherwise. +mp_obj_list_t *mp_obj_list_ensure(mp_obj_t in, size_t min_len); + #endif // MICROPY_INCLUDED_PY_OBJLIST_H diff --git a/py/objtemplate.c b/py/objtemplate.c index 0fc51a78d9a19..86451350a2143 100644 --- a/py/objtemplate.c +++ b/py/objtemplate.c @@ -152,9 +152,9 @@ static mp_obj_t mp_obj_template_make_new(const mp_obj_type_t *type, size_t n_arg static void mp_obj_template_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { (void)kind; mp_obj_template_t *self = MP_OBJ_TO_PTR(self_in); - mp_printf(print, "%q(%q=", MP_QSTR_Template, MP_QSTR_strings); + mp_printf(print, "%q(%q=", (qstr)MP_QSTR_Template, (qstr)MP_QSTR_strings); mp_obj_print_helper(print, self->strings, PRINT_REPR); - mp_printf(print, ", %q=", MP_QSTR_interpolations); + mp_printf(print, ", %q=", (qstr)MP_QSTR_interpolations); mp_obj_print_helper(print, self->interpolations, PRINT_REPR); mp_print_str(print, ")"); } @@ -346,7 +346,7 @@ static mp_obj_t mp_obj_interpolation_make_new(const mp_obj_type_t *type, size_t static void mp_obj_interpolation_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { (void)kind; mp_obj_interpolation_t *self = MP_OBJ_TO_PTR(self_in); - mp_printf(print, "%q(", MP_QSTR_Interpolation); + mp_printf(print, "%q(", (qstr)MP_QSTR_Interpolation); mp_obj_print_helper(print, self->value, PRINT_REPR); mp_print_str(print, ", "); mp_obj_print_helper(print, self->expression, PRINT_REPR); diff --git a/py/py.cmake b/py/py.cmake index 8b3c857ef4885..c2efab556c475 100644 --- a/py/py.cmake +++ b/py/py.cmake @@ -54,6 +54,7 @@ set(MICROPY_SOURCE_PY ${MICROPY_PY_DIR}/modsys.c ${MICROPY_PY_DIR}/modthread.c ${MICROPY_PY_DIR}/moderrno.c + ${MICROPY_PY_DIR}/modweakref.c ${MICROPY_PY_DIR}/mpprint.c ${MICROPY_PY_DIR}/mpstate.c ${MICROPY_PY_DIR}/mpz.c diff --git a/py/py.mk b/py/py.mk index a8b50b8d24b7e..932c47ef1773f 100644 --- a/py/py.mk +++ b/py/py.mk @@ -204,6 +204,7 @@ PY_CORE_O_BASENAME = $(addprefix py/,\ modsys.o \ moderrno.o \ modthread.o \ + modweakref.o \ vm.o \ bc.o \ showbc.o \ diff --git a/py/runtime.c b/py/runtime.c index d35cf4025f757..618e9b5ae41cf 100644 --- a/py/runtime.c +++ b/py/runtime.c @@ -179,6 +179,10 @@ void mp_init(void) { MP_STATE_VM(usbd) = MP_OBJ_NULL; #endif + #if MICROPY_PY_WEAKREF + mp_map_init(&MP_STATE_VM(mp_weakref_map), 0); + #endif + #if MICROPY_PY_THREAD_GIL mp_thread_mutex_init(&MP_STATE_VM(gil_mutex)); #endif diff --git a/tests/basics/weakref_callback_exception.py b/tests/basics/weakref_callback_exception.py new file mode 100644 index 0000000000000..df8e5129803f6 --- /dev/null +++ b/tests/basics/weakref_callback_exception.py @@ -0,0 +1,42 @@ +# Test weakref ref/finalize raising an exception within the callback. +# +# This test has different output to CPython due to the way that MicroPython +# prints the exception. + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +import gc + + +class A: + def __str__(self): + return "" + + +def callback(*args): + raise ValueError("weakref callback", args) + + +def test(): + print("test ref with exception in the callback") + a = A() + r = weakref.ref(a, callback) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("collect done") + + print("test finalize with exception in the callback") + a = A() + weakref.finalize(a, callback) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("collect done") + + +test() diff --git a/tests/basics/weakref_callback_exception.py.exp b/tests/basics/weakref_callback_exception.py.exp new file mode 100644 index 0000000000000..c2f7796310c2b --- /dev/null +++ b/tests/basics/weakref_callback_exception.py.exp @@ -0,0 +1,12 @@ +test ref with exception in the callback +Unhandled exception in weakref callback: +Traceback (most recent call last): + File "\.\+weakref_callback_exception.py", line 21, in callback +ValueError: ('weakref callback', (,)) +collect done +test finalize with exception in the callback +Unhandled exception in weakref callback: +Traceback (most recent call last): + File "\.\+weakref_callback_exception.py", line 21, in callback +ValueError: ('weakref callback', ()) +collect done diff --git a/tests/basics/weakref_callback_exception.py.native.exp b/tests/basics/weakref_callback_exception.py.native.exp new file mode 100644 index 0000000000000..a06a35c3b7e94 --- /dev/null +++ b/tests/basics/weakref_callback_exception.py.native.exp @@ -0,0 +1,8 @@ +test ref with exception in the callback +Unhandled exception in weakref callback: +ValueError: ('weakref callback', (,)) +collect done +test finalize with exception in the callback +Unhandled exception in weakref callback: +ValueError: ('weakref callback', ()) +collect done diff --git a/tests/basics/weakref_finalize_basic.py b/tests/basics/weakref_finalize_basic.py new file mode 100644 index 0000000000000..792cffacb1385 --- /dev/null +++ b/tests/basics/weakref_finalize_basic.py @@ -0,0 +1,58 @@ +# Test weakref.finalize() functionality that doesn't require gc.collect(). + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# Cannot reference non-heap objects. +for value in (None, False, True, Ellipsis, 0, "", ()): + try: + weakref.finalize(value, lambda: None) + except TypeError: + print(value, "TypeError") + + +# Convert (obj, func, args, kwargs) so CPython and MicroPython have a chance to match. +def convert_4_tuple(values): + if values is None: + return None + return (type(values[0]).__name__, type(values[1]), values[2], values[3]) + + +class A: + def __str__(self): + return "" + + +print("test alive, peek, detach") +a = A() +f = weakref.finalize(a, lambda: None, 1, 2, kwarg=3) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("detach", convert_4_tuple(f.detach())) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("detach", convert_4_tuple(f.detach())) +print("call", f()) +a = None + +print("test alive, peek, call") +a = A() +f = weakref.finalize(a, lambda *args, **kwargs: (args, kwargs), 1, 2, kwarg=3) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("call", f()) +print("alive", f.alive) +print("peek", convert_4_tuple(f.peek())) +print("call", f()) +print("detach", convert_4_tuple(f.detach())) + +print("test call which raises exception") +a = A() +f = weakref.finalize(a, lambda: 1 / 0) +try: + f() +except ZeroDivisionError as er: + print("call ZeroDivisionError") diff --git a/tests/basics/weakref_finalize_collect.py b/tests/basics/weakref_finalize_collect.py new file mode 100644 index 0000000000000..f6e7c14843e01 --- /dev/null +++ b/tests/basics/weakref_finalize_collect.py @@ -0,0 +1,76 @@ +# Test weakref.finalize() functionality requiring gc.collect(). +# Should be kept in sync with tests/ports/webassembly/weakref_finalize_collect.py. + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# gc module must be available if weakref is. +import gc + + +class A: + def __str__(self): + return "" + + +def callback(*args, **kwargs): + print("callback({}, {})".format(args, kwargs)) + return 42 + + +def test(): + print("test basic use of finalize() with a simple callback") + a = A() + f = weakref.finalize(a, callback) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) + + print("test that a callback is passed the correct values") + a = A() + f = weakref.finalize(a, callback, 1, 2, kwarg=3) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) + + print("test that calling the finalizer cancels the finalizer") + a = A() + f = weakref.finalize(a, callback) + print(f()) + print(a) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + print("test that calling detach cancels the finalizer") + a = A() + f = weakref.finalize(a, callback) + print(len(f.detach())) + print(a) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + print("test that finalize does not get collected before its ref does") + a = A() + weakref.finalize(a, callback) + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print("free a") + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + +test() diff --git a/tests/basics/weakref_multiple_refs.py b/tests/basics/weakref_multiple_refs.py new file mode 100644 index 0000000000000..400e03a17c7f8 --- /dev/null +++ b/tests/basics/weakref_multiple_refs.py @@ -0,0 +1,36 @@ +# Test weakref when multiple weak references are active. +# +# This test has different output to CPython due to the order that MicroPython +# executes weak reference callbacks. + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# gc module must be available if weakref is. +import gc + + +class A: + def __str__(self): + return "" + + +def test(): + global r1, r2 # needed for webassembly port to retain references to them + + print("test having multiple ref and finalize objects referencing the same thing") + a = A() + r1 = weakref.ref(a, lambda r: print("ref1", r())) + f1 = weakref.finalize(a, lambda: print("finalize1")) + r2 = weakref.ref(a, lambda r: print("ref2", r())) + f2 = weakref.finalize(a, lambda: print("finalize2")) + print(r1(), f1.alive, r2(), f2.alive) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + + +test() diff --git a/tests/basics/weakref_multiple_refs.py.exp b/tests/basics/weakref_multiple_refs.py.exp new file mode 100644 index 0000000000000..1f2d366f776fa --- /dev/null +++ b/tests/basics/weakref_multiple_refs.py.exp @@ -0,0 +1,6 @@ +test having multiple ref and finalize objects referencing the same thing + True True +finalize2 +finalize1 +ref2 None +ref1 None diff --git a/tests/basics/weakref_ref_basic.py b/tests/basics/weakref_ref_basic.py new file mode 100644 index 0000000000000..058045f6c33b3 --- /dev/null +++ b/tests/basics/weakref_ref_basic.py @@ -0,0 +1,14 @@ +# Test weakref.ref() functionality that doesn't require gc.collect(). + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# Cannot reference non-heap objects. +for value in (None, False, True, Ellipsis, 0, "", ()): + try: + weakref.ref(value) + except TypeError: + print(value, "TypeError") diff --git a/tests/basics/weakref_ref_collect.py b/tests/basics/weakref_ref_collect.py new file mode 100644 index 0000000000000..0e8db977d7764 --- /dev/null +++ b/tests/basics/weakref_ref_collect.py @@ -0,0 +1,69 @@ +# Test weakref.ref() functionality requiring gc.collect(). +# Should be kept in sync with tests/ports/webassembly/weakref_ref_collect.py. + +try: + import weakref +except ImportError: + print("SKIP") + raise SystemExit + +# gc module must be available if weakref is. +import gc + +# Cannot reference non-heap objects. +for value in (None, False, True, Ellipsis, 0, "", ()): + try: + weakref.ref(value) + except TypeError: + print(value, "TypeError") + + +class A: + def __str__(self): + return "" + + +def callback(r): + print("callback", r()) + + +def test(): + print("test basic use of ref() with only one argument") + a = A() + r = weakref.ref(a) + print(r()) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print(r()) + + print("test use of ref() with a callback") + a = A() + r = weakref.ref(a, callback) + print(r()) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print(r()) + + print("test when weakref gets collected before the object it refs") + a = A() + r = weakref.ref(a, callback) + print(r()) + r = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + a = None + + print("test a double reference") + a = A() + r1 = weakref.ref(a, callback) + r2 = weakref.ref(a, callback) + print(r1(), r2()) + a = None + clean_the_stack = [0, 0, 0, 0] + gc.collect() + print(r1(), r2()) + + +test() diff --git a/tests/extmod/socket_badconstructor.py b/tests/extmod/socket_badconstructor.py index 4a9d2668c7f1a..1ea5d750b3e65 100644 --- a/tests/extmod/socket_badconstructor.py +++ b/tests/extmod/socket_badconstructor.py @@ -16,6 +16,11 @@ except TypeError: print("TypeError") +try: + s = socket.socket(socket.AF_INET, 123456) +except OSError: + print("OSError") + try: s = socket.socket(socket.AF_INET, socket.SOCK_RAW, None) except TypeError: diff --git a/tests/extmod_hardware/machine_can2.py b/tests/extmod_hardware/machine_can2.py new file mode 100644 index 0000000000000..0ecced82865c0 --- /dev/null +++ b/tests/extmod_hardware/machine_can2.py @@ -0,0 +1,44 @@ +# Test machine.CAN(1) and machine.CAN(2) using loopback +# +# Single device test, assumes support for loopback and no connections to the CAN pins +# +# This test is ported from tests/ports/stm32/pyb_can2.py + +try: + from machine import CAN + + CAN(2, 125_000) +except (ImportError, ValueError): + print("SKIP") + raise SystemExit + +import time + +# Setting up each CAN peripheral independently is deliberate here, to catch +# catch cases where initialising CAN2 breaks CAN1 + +can1 = CAN(1, 125_000, mode=CAN.MODE_LOOPBACK) +can1.set_filters([(0x100, 0x700, 0)]) + +can2 = CAN(2, 125_000, mode=CAN.MODE_LOOPBACK) +can2.set_filters([(0x000, 0x7F0, 0)]) + +# Drain any old messages in RX FIFOs +for can in (can1, can2): + while can.recv(): + pass + +for id, can in ((1, can1), (2, can2)): + print("testing", id) + # message1 should only receive on can1, message2 on can2 + can.send(0x123, b"message1", 0) + can.send(0x003, "message2", 0) + time.sleep_ms(10) + did_recv = False + while res := can.recv(): + did_recv = True + print(hex(res[0]), bytes(res[1]), res[2], res[3]) + if not did_recv: + print("no rx!") + +print("done") diff --git a/tests/extmod_hardware/machine_can2.py.exp b/tests/extmod_hardware/machine_can2.py.exp new file mode 100644 index 0000000000000..bfb6a5088babd --- /dev/null +++ b/tests/extmod_hardware/machine_can2.py.exp @@ -0,0 +1,5 @@ +testing 1 +0x123 b'message1' 0 0 +testing 2 +0x3 b'message2' 0 0 +done diff --git a/tests/extmod_hardware/machine_can_timings.py b/tests/extmod_hardware/machine_can_timings.py new file mode 100644 index 0000000000000..441059f5da546 --- /dev/null +++ b/tests/extmod_hardware/machine_can_timings.py @@ -0,0 +1,60 @@ +# Test machine.CAN timings results +# +# Single device test, assumes no connections to the CAN pins + +try: + from machine import CAN +except ImportError: + print("SKIP") + raise SystemExit + +import unittest + +from target_wiring import can_args, can_kwargs + + +class TestTimings(unittest.TestCase): + def test_bitrate(self): + for bitrate in (125_000, 250_000, 500_000, 1_000_000): + can = CAN(*can_args, bitrate=bitrate, **can_kwargs) + print(can) + timings = can.get_timings() + print(timings) + # Actual bitrate may not be exactly equal to requested rate + self.assertAlmostEqual(timings[0], bitrate, delta=1_000) + can.deinit() + + def test_sample_point(self): + # Verify that tseg1 and tseg2 are set correctly from the sample_point argument + for sample_point in (66, 75, 95): + can = CAN(*can_args, bitrate=500_000, sample_point=sample_point, **can_kwargs) + _bitrate, _sjw, tseg1, tseg2, _fd, _port = can.get_timings() + print(f"sample_point={sample_point}, tseg1={tseg1}, tseg2={tseg2}") + self.assertAlmostEqual(sample_point / 100, tseg1 / (tseg1 + tseg2), delta=0.05) + can.deinit() + + def test_tseg_args(self): + # Verify that tseg1 and tseg2 are set correctly and sample_point is ignored if these are provided + for tseg1, tseg2 in ((5, 2), (16, 8), (16, 5), (15, 5)): + print(f"tseg1={tseg1} tseg2={tseg2}") + can = CAN( + *can_args, bitrate=250_000, tseg1=tseg1, tseg2=tseg2, sample_point=99, **can_kwargs + ) + bitrate, _sjw, ret_tseg1, ret_tseg2, _fd, _port = can.get_timings() + self.assertEqual(ret_tseg1, tseg1) + self.assertEqual(ret_tseg2, tseg2) + + def test_invalid_timing_args(self): + # Test various kwargs out of their allowed value ranges + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=250_000, tseg1=55, **can_kwargs) + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=500_000, tseg2=9, **can_kwargs) + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=-1, **can_kwargs) + with self.assertRaises(ValueError): + CAN(*can_args, bitrate=500_000, sample_point=101, **can_kwargs) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod_hardware/machine_pwm.py b/tests/extmod_hardware/machine_pwm.py index a756493f0e566..7d4f82fd2fe74 100644 --- a/tests/extmod_hardware/machine_pwm.py +++ b/tests/extmod_hardware/machine_pwm.py @@ -1,10 +1,7 @@ # Test machine.PWM, frequency and duty cycle (using machine.time_pulse_us). # # IMPORTANT: This test requires hardware connections: the PWM-output and pulse-input -# pins must be wired together (see the variable `pwm_pulse_pins`). - -import sys -import time +# pins must be wired together (see the variable `pwm_loopback_pins`). try: from machine import time_pulse_us, Pin, PWM @@ -12,46 +9,34 @@ print("SKIP") raise SystemExit -import unittest +import machine, sys, time, unittest +from target_wiring import pwm_loopback_pins pwm_freq_limit = 1000000 freq_margin_per_thousand = 0 duty_margin_per_thousand = 0 timing_margin_us = 5 -# Configure pins based on the target. -if "alif" in sys.platform: - pwm_pulse_pins = (("P0_4", "P0_5"),) -elif "esp32" in sys.platform: - pwm_pulse_pins = ((4, 5),) +# Slow MCUs cannot capture short pulses using `time_pulse_us` so limit the maximum PWM +# frequency tested on such targets. +if hasattr(machine, "freq"): + f = machine.freq() + if isinstance(f, tuple): + f = f[0] + if f <= 48_000_000: + pwm_freq_limit = 2_000 + elif f <= 64_000_000: + pwm_freq_limit = 5_000 + +# Tune test parameters based on the target. +if "esp32" in sys.platform: freq_margin_per_thousand = 2 duty_margin_per_thousand = 1 timing_margin_us = 20 elif "esp8266" in sys.platform: - pwm_pulse_pins = ((4, 5),) pwm_freq_limit = 1_000 duty_margin_per_thousand = 3 timing_margin_us = 50 -elif "mimxrt" in sys.platform: - if "Teensy" in sys.implementation._machine: - # Teensy 4.x - pwm_pulse_pins = ( - ("D0", "D1"), # FLEXPWM X and UART 1 - ("D2", "D3"), # FLEXPWM A/B - ("D11", "D12"), # QTMR and MOSI/MISO of SPI 0 - ) - else: - pwm_pulse_pins = (("D0", "D1"),) -elif "rp2" in sys.platform: - pwm_pulse_pins = (("GPIO0", "GPIO1"),) -elif "samd" in sys.platform: - pwm_pulse_pins = (("D0", "D1"),) - if "SAMD21" in sys.implementation._machine: - # MCU is too slow to capture short pulses. - pwm_freq_limit = 2_000 -else: - print("Please add support for this test on this platform.") - raise SystemExit # Test a specific frequency and duty cycle. @@ -68,8 +53,8 @@ def _test_freq_duty(self, pulse_in, pwm, freq, duty_u16): self.assertLessEqual(duty_error, duty_margin_per_thousand) # Calculate expected timing. - expected_total_us = 1_000_000 // freq - expected_high_us = expected_total_us * duty_u16 // 65535 + expected_total_us = (1_000_000 + freq // 2) // freq + expected_high_us = (expected_total_us * duty_u16 + 65535 // 2) // 65535 expected_low_us = expected_total_us - expected_high_us expected_us = (expected_low_us, expected_high_us) timeout = 2 * expected_total_us @@ -158,7 +143,7 @@ def test_freq_10000(self): # Generate test classes, one for each set of pins to test. -for pwm, pulse in pwm_pulse_pins: +for pwm, pulse in pwm_loopback_pins: cls_name = "Test_{}_{}".format(pwm, pulse) globals()[cls_name] = type( cls_name, (TestBase, unittest.TestCase), {"pwm_pin": pwm, "pulse_pin": pulse} diff --git a/tests/feature_check/tstring.py b/tests/feature_check/tstring.py index e1d428b4e7b2b..05322b2ae612d 100644 --- a/tests/feature_check/tstring.py +++ b/tests/feature_check/tstring.py @@ -1,8 +1,5 @@ # check whether t-strings (PEP-750) are supported -# TODO remove this check when micropython-lib's string extends ustring -from string.templatelib import Template, Interpolation - a = 1 t = t"a={a}" print("tstring") diff --git a/tests/multi_extmod/machine_can_01_rxtx_simple.py b/tests/multi_extmod/machine_can_01_rxtx_simple.py new file mode 100644 index 0000000000000..fbc7692926d53 --- /dev/null +++ b/tests/multi_extmod/machine_can_01_rxtx_simple.py @@ -0,0 +1,35 @@ +from machine import CAN +import time + +ID_0 = 0x50 +ID_1 = 0x50333 +FLAGS_1 = CAN.FLAG_EXT_ID # ID_1 is an extended CAN ID + +can = CAN(1, 500_000) +can.set_filters(None) # receive all + + +def print_rx(can_id, data, flags, errors): + print(hex(can_id), bytes(data), hex(flags), hex(errors)) + + +def instance0(): + multitest.next() + + # receive from instance1 first + while not (rx := can.recv()): + time.sleep(0) + print_rx(*rx) + + # now send one + can.send(ID_0, b"1234") + + +def instance1(): + multitest.next() + + can.send(ID_1, b"ABCD", FLAGS_1) + + while not (rx := can.recv()): + time.sleep(0) + print_rx(*rx) diff --git a/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp b/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp new file mode 100644 index 0000000000000..479b6809490ca --- /dev/null +++ b/tests/multi_extmod/machine_can_01_rxtx_simple.py.exp @@ -0,0 +1,4 @@ +--- instance0 --- +0x50333 b'ABCD' 0x2 0x0 +--- instance1 --- +0x50 b'1234' 0x0 0x0 diff --git a/tests/multi_extmod/machine_can_02_rx_callback.py b/tests/multi_extmod/machine_can_02_rx_callback.py new file mode 100644 index 0000000000000..80b13dcd622d6 --- /dev/null +++ b/tests/multi_extmod/machine_can_02_rx_callback.py @@ -0,0 +1,122 @@ +from machine import CAN +import time + +# Test the CAN.IRQ_RX irq handler, including overflow + +rx_overflow = False +rx_full = False +received = [] + +# CAN IDs +ID_SPAM = 0x345 # messages spammed into the receive FIFO +ID_ACK_OFLOW = 0x055 # message the receiver sends after it's seen an overflow +ID_AFTER = 0x100 # message the sender sends after the ACK + +can = CAN(1, 500_000) + + +# A very basic "soft" receiver handler that stores received messages into a global list +def receiver_irq_recv(can): + global rx_overflow, rx_full + + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + + can_id, data, _flags, errors = can.recv() + + received.append((can_id, None)) + + # The FIFO is expected not to overflow by itself, wait until 40 messages + # have been received and then block the receive handler to induce an overflow + if len(received) == 40: + assert not rx_overflow # shouldn't have already happened, either + time.sleep_ms(500) + + if not rx_overflow and (errors & CAN.RECV_ERR_OVERRUN): + # expected this should happen on the very next message after + # the one where we slept for 500ms + print("irq_recv overrun", len(received)) + received.clear() # check we still get some messages, see rx_spam print line below + rx_overflow = True + + # also expect the FIFO to be FULL again immediately after overrunning and rx_overflow event + if rx_overflow and (errors & CAN.RECV_ERR_OVERRUN | CAN.RECV_ERR_FULL) == CAN.RECV_ERR_FULL: + rx_full = True + + +# Receiver +def instance0(): + can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False) + + can.set_filters(None) # receive all + + multitest.next() + + while not rx_overflow: + pass # Resume ASAP after FIFO0 overflows + + can.send(ID_ACK_OFLOW, b"overflow") + + # at least one ID_SPAM message should have been received + # *after* we overflowed and 'received' was clear in the irq handler + print("rx_spam", any(r[0] == ID_SPAM for r in received)) + + # wait until the "after" message is received + for n in range(100): + if any(r[0] == ID_AFTER for r in received): + break + time.sleep_ms(10) + + can.irq(None) # disable the IRQ + received.clear() + + # at some point while waiting for ID_AFTER the FIFO should have gotten + # full again + print("rx_full", rx_full) + + # now IRQ is disabled, no new messages should be received + time.sleep_ms(250) + print("len", len(received)) + + +received_ack = False + +# reusing the result buffer so sender_irq_recv can be 'hard' +sender_irq_result = [None, memoryview(bytearray(64)), None, None] + + +def sender_irq_recv(can): + global received_ack + + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + + can_id, data, _flags, _errors = can.recv(sender_irq_result) + print("sender_irq_recv", can_id, len(data)) # should be ID_ACK_OFLOW and "overflow" payload + received_ack = True + + +# Sender +def instance1(): + can.irq(sender_irq_recv, CAN.IRQ_RX, hard=True) + + can.set_filters(None) + + multitest.next() + + # Spam out messages until the receiver tells us its RX FIFO is full. + # + # The RX FIFO on the receiver can vary from 3 deep (BXCAN) to 25 deep (STM32H7), + # so we keep sending to it until we see a CAN message on ID_ACK_OFLOW indicating + # the receiver's FIFO has overflowed + while not received_ack: + for i in range(255): + while can.send(ID_SPAM, bytes([i] * 8)) is None and not received_ack: + # Don't overflow the TX FIFO + time.sleep_ms(1) + if received_ack: + break + + # give the receiver some time to make space in the FIFO + time.sleep_ms(200) + + # send the final message, the receiver should get this one + can.send(ID_AFTER, b"aaaaa") diff --git a/tests/multi_extmod/machine_can_02_rx_callback.py.exp b/tests/multi_extmod/machine_can_02_rx_callback.py.exp new file mode 100644 index 0000000000000..8e4e5dc64750a --- /dev/null +++ b/tests/multi_extmod/machine_can_02_rx_callback.py.exp @@ -0,0 +1,7 @@ +--- instance0 --- +irq_recv overrun 41 +rx_spam True +rx_full True +len 0 +--- instance1 --- +sender_irq_recv 85 8 diff --git a/tests/multi_extmod/machine_can_03_rx_filters.py b/tests/multi_extmod/machine_can_03_rx_filters.py new file mode 100644 index 0000000000000..a56acbbe91eff --- /dev/null +++ b/tests/multi_extmod/machine_can_03_rx_filters.py @@ -0,0 +1,103 @@ +from machine import CAN +import time + +# Test for filtering capabilities + +can = CAN(1, 500_000) + +# IDs and filter phases used for the 'single id' part of the test +SINGLE_EXT_ID = (0x1234_5678, 0x1FFF_FFFF, CAN.FLAG_EXT_ID) +SINGLE_STD_ID = (0x505, 0x7FF, 0) +SINGLE_ID_PHASES = [ + ("single ext id", [SINGLE_EXT_ID]), + ("single std id", [SINGLE_STD_ID]), + ("ext+std ids", [SINGLE_EXT_ID, SINGLE_STD_ID]), + ("std+ext ids", [SINGLE_STD_ID, SINGLE_EXT_ID]), # these two should be equivalent + ("accept none", []), + ("accept all", None), + ("accept none again", ()), +] + + +# Receiver +def receiver_irq_recv(can): + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + can_id, data, _flags, _errors = can.recv() + print("recv", hex(can_id), data.hex()) + + +def instance0(): + can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False) + + multitest.next() + + # Configure to receive standard frames (in a range), and + # extended frames (in a range). + can.set_filters([(0x300, 0x300, 0), (0x3000, 0x3000, CAN.FLAG_EXT_ID)]) + multitest.broadcast("ready id ranges") + + # Run through the phases of filtering for individual IDs + for phase, filters in SINGLE_ID_PHASES: + multitest.wait("configure " + phase) + if filters and len(filters) > CAN.FILTERS_MAX: + # this check really exists to add test coverage for the FILTERS_MAX constant + print("Warning: Too many filters for hardware!") + can.set_filters(filters) + print("receiver configured " + phase) + multitest.broadcast("ready " + phase) + + multitest.wait("Sender done") + + +def send_messages(messages): + for can_id, payload, flags in messages: + r = can.send(can_id, payload, flags) + if r is None: + print("Failed to send:", hex(can_id), payload.hex()) + time.sleep_ms(5) # avoid flooding either our or the receiver's FIFO + + +# Sender +def instance1(): + multitest.next() + multitest.wait("ready id ranges") + + print("Sending ID ranges...") + for i in range(3): + send_messages( + [ + (0x345, bytes([i, 0xFF] * (i + 1)), 0), + (0x3700 + i, bytes([0xEE] * (i + 1)), CAN.FLAG_EXT_ID), + (0x123, b"abcdef", 0), # matches no filter, expect ACKed but not received + ] + ) + + # Now move on to single ID filtering + + single_id_messages = [ + (0x1234_5678, b"\x01\x02\x03\x04\x05", CAN.FLAG_EXT_ID), # matches ext id + (0x0234_5678, b"\x00\x00", CAN.FLAG_EXT_ID), # no match + (0x678, b"\x00\x01", 0), # no match + (0x505, b"\x06\x07\x08\x09\x0a\x0b", 0), # matches standard id + (0x345, b"\x00\x02", 0), # no match (in prev filter) + (0x1234_5679, b"\x00\x03", CAN.FLAG_EXT_ID), # no match + (0x3705, b"\x00\x04", CAN.FLAG_EXT_ID), # no match (in prev filter) + (0x1234_5678, b"\x01\x02\x03", CAN.FLAG_EXT_ID), # matches ext id + (0x505, b"\x04\x05\x06", 0), # matches standard id + (0x505, b"\x00\x05", CAN.FLAG_EXT_ID), # no match (is ext id) + (0x507, b"\x00\x06", 0), # no match + (0x1334_5678, b"\x00\x07", CAN.FLAG_EXT_ID), # no match + (0x1234_5670, b"\x00\x08", CAN.FLAG_EXT_ID), # no match + ] + + # Send the same list of messages for each phase of the test. + # The receiver will have configured different filters, and the .exp + # file is what selects which messages should be received or not. + for phase, _ in SINGLE_ID_PHASES: + multitest.broadcast("configure " + phase) + multitest.wait("ready " + phase) + print("Sending for " + phase + "...") + send_messages(single_id_messages) + + print("Sender done") + multitest.broadcast("Sender done") diff --git a/tests/multi_extmod/machine_can_03_rx_filters.py.exp b/tests/multi_extmod/machine_can_03_rx_filters.py.exp new file mode 100644 index 0000000000000..d55c0c97d159a --- /dev/null +++ b/tests/multi_extmod/machine_can_03_rx_filters.py.exp @@ -0,0 +1,49 @@ +--- instance0 --- +recv 0x345 00ff +recv 0x3700 ee +recv 0x345 01ff01ff +recv 0x3701 eeee +recv 0x345 02ff02ff02ff +recv 0x3702 eeeeee +receiver configured single ext id +recv 0x12345678 0102030405 +recv 0x12345678 010203 +receiver configured single std id +recv 0x505 060708090a0b +recv 0x505 040506 +receiver configured ext+std ids +recv 0x12345678 0102030405 +recv 0x505 060708090a0b +recv 0x12345678 010203 +recv 0x505 040506 +receiver configured std+ext ids +recv 0x12345678 0102030405 +recv 0x505 060708090a0b +recv 0x12345678 010203 +recv 0x505 040506 +receiver configured accept none +receiver configured accept all +recv 0x12345678 0102030405 +recv 0x2345678 0000 +recv 0x678 0001 +recv 0x505 060708090a0b +recv 0x345 0002 +recv 0x12345679 0003 +recv 0x3705 0004 +recv 0x12345678 010203 +recv 0x505 040506 +recv 0x505 0005 +recv 0x507 0006 +recv 0x13345678 0007 +recv 0x12345670 0008 +receiver configured accept none again +--- instance1 --- +Sending ID ranges... +Sending for single ext id... +Sending for single std id... +Sending for ext+std ids... +Sending for std+ext ids... +Sending for accept none... +Sending for accept all... +Sending for accept none again... +Sender done diff --git a/tests/multi_extmod/machine_can_04_tx_order.py b/tests/multi_extmod/machine_can_04_tx_order.py new file mode 100644 index 0000000000000..204bdafd59da8 --- /dev/null +++ b/tests/multi_extmod/machine_can_04_tx_order.py @@ -0,0 +1,170 @@ +from machine import CAN +import time +from random import seed, randrange + +import micropython + +micropython.alloc_emergency_exception_buf(256) +seed(0) + +# Testing that transmit order obeys the priority ordering + +ID_LOW = 0x500 +ID_HIGH = 0x200 + +NUM_MSGS = 255 + +MSG_LEN = 4 + +can = CAN(1, 500_000) + + +def check_sequence(items, label): + # The full range of NUM_MSGS values should have been received or sent, in + # order, without duplicates + if len(items) != NUM_MSGS: + print(label, "wrong count", len(items), "vs", NUM_MSGS) + if items == list(range(NUM_MSGS)): + print(label, "OK") + else: + print(label, "error:") + print(items) + + +# Receiver + +# lists of received messages, one list per ID +received_low = [] +received_high = [] + + +def irq_recv(can): + while can.irq().flags() & can.IRQ_RX: + can_id, data, flags, _errors = can.recv() + + if can_id == ID_LOW and len(data) == MSG_LEN: + received_low.append(data[0]) + elif can_id == ID_HIGH and len(data) == MSG_LEN: + received_high.append(data[0]) + else: + print("unexpected recv", can_id, data, flags) + + +def instance0(): + can.irq(irq_recv, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + multitest.next() + + multitest.wait("sender done") + check_sequence(received_low, "Low prio received") + check_sequence(received_high, "High prio received") + + +# Sender + +## Messages pending to send +pending_low = list(range(NUM_MSGS)) +pending_high = list(range(NUM_MSGS)) + +# List of the messages currently queued to send +tx_queue = [None] * CAN.TX_QUEUE_LEN + +# Messages sent, recorded in order as [high_prio, val] +sent = [] +for _ in range(NUM_MSGS * 2): + sent.append([None, None]) +num_sent = 0 + + +def irq_send(can): + global num_sent + + while flags := can.irq().flags(): + assert flags & can.IRQ_TX # the only enabled IRQ + + idx = (flags >> can.IRQ_TX_IDX_SHIFT) & can.IRQ_TX_IDX_MASK + success = not (flags & can.IRQ_TX_FAILED) + + if not success: + return # We don't worry about failures here + + if not tx_queue[idx]: + print("bad done", idx, success) + return + + was_high, val = tx_queue[idx] + tx_queue[idx] = None + sent[num_sent][0] = was_high + sent[num_sent][1] = val + num_sent += 1 + + +def instance1(): + # note: this test can pass with hard=True, but in a debug build + # the completion IRQ may race ahead of setting tx_queue[idx], below + can.irq(irq_send, trigger=can.IRQ_TX, hard=False) + data = bytearray(MSG_LEN) + + multitest.next() + + while pending_low or pending_high: + if pending_high: + val = pending_high.pop(0) + data[0] = val + data[1] = 1 + while True: + idx = can.send(ID_HIGH, data) + if idx is None: + continue # keep trying until a queue spot opens up + old = tx_queue[idx] + tx_queue[idx] = (True, val) + if old: + print("error high priority queue race", idx, val, old) + break + + for _ in range(randrange(4)): + # Try and queue many low priority messages (expecting most will fail) + if pending_low: + val = pending_low[0] + data[0] = val + data[1] = 0 + idx = can.send(ID_LOW, data) + if idx is None: + # don't retry indefinitely for low priority messages + continue + + old = tx_queue[idx] + tx_queue[idx] = (False, val) + pending_low.pop(0) + if old is not None: + print("error low priority queue race", idx, val, old) + + print("waiting for tx queue to empty...") + while any(x is not None for x in tx_queue): + pass + + multitest.broadcast("sender done") + + # Check we sent the right number of messages + if num_sent != 2 * NUM_MSGS: + print("Sent %d expected %d" % (num_sent, 2 * NUM_MSGS)) + else: + print("Sent right number of messages") + + # Check the low and high priority messages all arrived in order + sent_low = [val for (prio, val) in sent[:num_sent] if prio == False] + sent_high = [val for (prio, val) in sent[:num_sent] if prio == True] + check_sequence(sent_low, "Low prio sent") + check_sequence(sent_high, "High prio sent") + + # check that high priority message queue items always stayed ahead of the low priority + high_val = -1 + for idx, (prio, val) in enumerate(sent): + if prio: + high_val = val + elif high_val <= val and val < NUM_MSGS - 1: + print( + "Low priority message %d overtook high priority %d at index %d" + % (val, high_val, idx) + ) diff --git a/tests/multi_extmod/machine_can_04_tx_order.py.exp b/tests/multi_extmod/machine_can_04_tx_order.py.exp new file mode 100644 index 0000000000000..a2de6ee27063d --- /dev/null +++ b/tests/multi_extmod/machine_can_04_tx_order.py.exp @@ -0,0 +1,8 @@ +--- instance0 --- +Low prio received OK +High prio received OK +--- instance1 --- +waiting for tx queue to empty... +Sent right number of messages +Low prio sent OK +High prio sent OK diff --git a/tests/multi_extmod/machine_can_05_tx_prio_cancel.py b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py new file mode 100644 index 0000000000000..64756a1a1af2e --- /dev/null +++ b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py @@ -0,0 +1,118 @@ +from machine import CAN +import time + +# Check that cancelling a low priority outgoing message and replacing it with a +# high priority message causes it to be transmitted successfully onto a busy bus + +recv = [] + +ITERS = 5 + +can = CAN(1, 500_000) + + +def irq_recv(can): + global recv_std_id + while can.irq().flags() & can.IRQ_RX: + can_id, data, flags, _errors = can.recv() + assert flags & CAN.FLAG_EXT_ID # test uses all extended IDs + if len(recv) < ITERS: + recv.append(can_id) + + +def instance0(): + can.irq(irq_recv, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + multitest.next() + + # "Babble" medium priority messages onto the bus to prevent + # instance1() from sending anything lower priority than this + while len(recv) < ITERS: + for id in range(0x5000, 0x6000): + can.send(id, b"BABBLE", CAN.FLAG_EXT_ID) + if len(recv) >= ITERS: + break + + print("received", ITERS, "messages") + for can_id in recv: + print(hex(can_id)) # should be the high priority messages from instance1, only + + multitest.wait("sender done") + print("done") + + +last_idx = 0 +total_cancels = 0 +total_sent = 0 + + +def irq_send(can): + global total_cancels, total_sent + + while flags := can.irq().flags(): + assert flags & can.IRQ_TX # the only enabled IRQ + + idx = (flags >> can.IRQ_TX_IDX_SHIFT) & can.IRQ_TX_IDX_MASK + + if flags & can.IRQ_TX_FAILED: + # we should only see failed transmits due to cancels in buffer 'last_idx' + assert idx == last_idx + total_cancels += 1 + else: + # this includes the messages we explicitly send, plus queued low + # priority messages once the receiver stops 'babbling' on the bus + total_sent += 1 + + +def instance1(): + global last_idx + can.irq(irq_send, trigger=can.IRQ_TX, hard=True) + + multitest.next() + + for i in range(ITERS): + # Fill the transmit queue with low priority messages (all extended IDs) + last_idx = 0 + if i < 3: + # For the first 3 iterations, send unique message IDs + id_range = range(0x7000, 0x7FFF) + flags = CAN.FLAG_EXT_ID + else: + # For the last iterations, repeat the same ID but tell controller to ignore + # ordering (allows it to queue more than one despite hardware limitations) + id_range = [0x50000 + i] * CAN.TX_QUEUE_LEN + flags = CAN.FLAG_EXT_ID | CAN.FLAG_UNORDERED + + for id in id_range: + idx = can.send(id, b"LOWPRIO", flags) + if idx is None: + break # send queue is full, stop trying to send + last_idx = idx + + time.sleep_ms(50) # the send queue shouldn't empty as instance0 is "babbling" + + # try and cancel the last message we queued + res = can.cancel_send(last_idx) + print(i, "cancel result", res) + + # send a high priority message, that we expect to go out + idx = can.send(0x500 + i, b"HIPRIO", CAN.FLAG_EXT_ID) + print(i, "send result", idx is not None) + + # make sure this message is sent onto the bus + time.sleep_ms(1) + + multitest.broadcast("sender done") + + # let the entire transmit queue drain, now instance0 should have gone quiet + time.sleep_ms(50) + + print("total cancels", total_cancels) # should equal ITERS + + if total_sent == CAN.TX_QUEUE_LEN - 1 + ITERS: + # expect we send one message for each of ITERS, plus all low priority + # queued messages once instance0 stops babbling on the bus + print("total sent OK") + else: + print("total sent", total_sent, CAN.TX_QUEUE_LEN - 1 + ITERS) diff --git a/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp new file mode 100644 index 0000000000000..26bab097c4471 --- /dev/null +++ b/tests/multi_extmod/machine_can_05_tx_prio_cancel.py.exp @@ -0,0 +1,21 @@ +--- instance0 --- +received 5 messages +0x500 +0x501 +0x502 +0x503 +0x504 +done +--- instance1 --- +0 cancel result True +0 send result True +1 cancel result True +1 send result True +2 cancel result True +2 send result True +3 cancel result True +3 send result True +4 cancel result True +4 send result True +total cancels 5 +total sent OK diff --git a/tests/multi_extmod/machine_can_06_remote_req.py b/tests/multi_extmod/machine_can_06_remote_req.py new file mode 100644 index 0000000000000..4a1414c0946ef --- /dev/null +++ b/tests/multi_extmod/machine_can_06_remote_req.py @@ -0,0 +1,70 @@ +from machine import CAN +import time + +# Test CAN remote transmission requests + +ID = 0x750 + +FILTER_ID = 0x101 +FILTER_MASK = 0x7FF + +can = CAN(1, 500_000) + + +def receiver_irq_recv(can): + assert can.irq().flags() & can.IRQ_RX # the only enabled IRQ + can_id, data, flags, _errors = can.recv() + is_rtr = flags == CAN.FLAG_RTR + print( + "recv", + hex(can_id), + is_rtr, + len(data) if is_rtr else bytes(data), + ) + if is_rtr: + # The 'data' response of a remote request should be all zeroes + assert bytes(data) == b"\x00" * len(data) + + +# Receiver +def instance0(): + can.irq(receiver_irq_recv, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + multitest.next() + + multitest.wait("enable filter") + can.set_filters([(FILTER_ID, FILTER_MASK, 0)]) + multitest.broadcast("filter set") + + multitest.wait("done") + + +# Sender +def instance1(): + multitest.next() + + can.send(ID, b"abc", CAN.FLAG_RTR) # length==3 remote request + time.sleep_ms(5) + can.send(ID, b"abc", 0) # regular message using the same ID + time.sleep_ms(5) + can.send(ID, b"abcde", CAN.FLAG_RTR) # length==5 remote request + time.sleep_ms(5) + + multitest.broadcast("enable filter") + multitest.wait("filter set") + + # these two messages should be filtered out + can.send(ID, b"abc", CAN.FLAG_RTR) # length==3 remote request + time.sleep_ms(5) + can.send(ID, b"abc", 0) # regular message using the same ID + time.sleep_ms(5) + + # these messages should be filtered in + can.send(FILTER_ID, b"def", CAN.FLAG_RTR) # length==3 remote request + time.sleep_ms(5) + can.send(FILTER_ID, b"hij", 0) # regular message using the same ID + time.sleep_ms(5) + + multitest.broadcast("done") + print("done") diff --git a/tests/multi_extmod/machine_can_06_remote_req.py.exp b/tests/multi_extmod/machine_can_06_remote_req.py.exp new file mode 100644 index 0000000000000..fe94508ba3da2 --- /dev/null +++ b/tests/multi_extmod/machine_can_06_remote_req.py.exp @@ -0,0 +1,8 @@ +--- instance0 --- +recv 0x750 True 3 +recv 0x750 False b'abc' +recv 0x750 True 5 +recv 0x101 True 3 +recv 0x101 False b'hij' +--- instance1 --- +done diff --git a/tests/multi_extmod/machine_can_07_error_states.py b/tests/multi_extmod/machine_can_07_error_states.py new file mode 100644 index 0000000000000..e7eec513ee169 --- /dev/null +++ b/tests/multi_extmod/machine_can_07_error_states.py @@ -0,0 +1,205 @@ +from machine import CAN +from micropython import const +import time + +# Test that without a second CAN node on the network the controller will +# correctly go into the correct error states, and can then recover. +# +# Note this test depends on no other CAN node being active on the network apart +# from the two test instances. (Although it's OK for extra nodes to be in a +# "listen only" mode where they won't ACK messages.) + +rx_overflow = False +rx_full = False +received = [] + +# CAN IDs +_ID = const(0x100) + +# can.state() result list indexes, as constants +_IDX_TEC = const(0) +_IDX_REC = const(1) +_IDX_NUM_WARNING = const(2) +_IDX_NUM_PASSIVE = const(3) +_IDX_NUM_BUS_OFF = const(4) +_IDX_PEND_TX = const(5) + +can = CAN(1, 500_000) + + +def state_name(state): + for name in dir(CAN): + if name.startswith("STATE_") and state == getattr(CAN, name): + return name + return f"UNKNOWN-{state}" + + +def irq_recv(can): + while can.irq().flags() & can.IRQ_RX: + can_id, data, _flags, errors = can.recv() + print("recv", hex(can_id), data.hex()) + + +# Receiver +def instance0(): + can.irq(irq_recv, trigger=can.IRQ_RX, hard=False) + + can.set_filters(None) # receive all + + multitest.next() + + # Receive at least one CAN message before sender asks us to disable the controller + + multitest.wait("disable receiver") + can.deinit() + print("can deinit()") + multitest.broadcast("receiver disabled") + + # Wait for the sender to tell us to re-enable + + multitest.wait("enable receiver") + # note the irq is no longer active after deinit() + can.init(500_000) + print("can init()") + multitest.broadcast("receiver enabled") + + # Receive CAN messages until the sender asks us to switch to an invalid baud rate + + multitest.wait("switch baud") + can.init(125_000) + print("can switch baud") + multitest.broadcast("switched baud") + + time.sleep_ms(1) + + print("sending bad msg") + # trying to send this frame should introduce more bus errors + idx_bad = can.send(_ID, b"BADBAUD") + + multitest.wait("fix baud") + print("sending cancelling") + print("cancelled", can.cancel_send(idx_bad)) + print("re-init") + can.init(500_000) + multitest.broadcast("fixed baud") + + # Should be receiving CAN messages OK again + + print("done") + + +def irq_sender(can): + while flags := can.irq().flags(): + if flags & can.IRQ_STATE: + print("irq state", can.state()) + if flags & can.IRQ_TX: + print("irq sent", not (flags & can.IRQ_TX_FAILED)) + + +# Sender +def instance1(): + can.irq(irq_sender, CAN.IRQ_STATE | CAN.IRQ_TX, hard=False) + + can.set_filters(None) + + multitest.next() + + print("started", state_name(can.state())) # should be ERROR_ACTIVE + + # Send a single message to the receiver, to verify it's working + can.send(_ID, b"PAYLOAD") + active_counters = can.get_counters() + # print(active_counters) # DEBUG + + multitest.broadcast("disable receiver") + multitest.wait("receiver disabled") + + # Now the receiver shouldn't be ACKing our frames, queue will stay full + # ... we will get the ISR for ERROR_WARNING but it'll go from ERROR_WARNING to ERROR_PASSIVE + # very quickly as all messages are failing + can.send(_ID, b"MORE") + while can.state() in (CAN.STATE_ACTIVE, CAN.STATE_WARNING): + pass + + print(state_name(can.state())) # should be ERROR_PASSIVE now + passive_counters = can.get_counters() + # print(passive_counters) # DEBUG + print("tec increased", passive_counters[_IDX_TEC] > active_counters[_IDX_TEC]) + print("tec over thresh", passive_counters[_IDX_TEC] >= 128) + # we should have counted exactly one ERROR_WARNING and ERROR_PASSIVE transition + print( + "counted warning", + passive_counters[_IDX_NUM_WARNING] == active_counters[_IDX_NUM_WARNING] + 1, + ) + print( + "counted passive", + passive_counters[_IDX_NUM_PASSIVE] == active_counters[_IDX_NUM_PASSIVE] + 1, + ) + print("some pending tx", passive_counters[_IDX_PEND_TX] > 0) + + # Re-enable the receiver which should allow us to go from ERROR_PASSIVE to ERROR_WARNING + multitest.broadcast("enable receiver") + multitest.wait("receiver enabled") + + can.send(_ID, b"MORE") + while can.state() == CAN.STATE_PASSIVE: + pass + + print(state_name(can.state())) # should be ERROR_WARNING now + warning_counters = can.get_counters() + # print(warning_counters) # DEBUG + print("tec decreased", warning_counters[_IDX_TEC] < passive_counters[_IDX_TEC]) + print( + "tec below thresh", warning_counters[_IDX_TEC] < 128 + ) # and should be more than error passive threshold + # error warning count should stay the same, as we went "down" in severity not up + print( + "no new warning", warning_counters[_IDX_NUM_WARNING] == passive_counters[_IDX_NUM_WARNING] + ) + print( + "no new passive", warning_counters[_IDX_NUM_PASSIVE] == passive_counters[_IDX_NUM_PASSIVE] + ) + + # Tell the receiver to change to the wrong baud rate, which should create both RX and TX errorxs + multitest.broadcast("switch baud") + multitest.wait("switched baud") + + # queue another message. This will keep trying to send until we revert back to ERROR_PASSIVE + idx = can.send(_ID, b"YETMORE") + print("queued yetmore", idx is not None) + while can.state() != CAN.STATE_PASSIVE: + pass + + print(state_name(can.state())) # should be ERROR_PASSIVE again + passive_counters = can.get_counters() + # print(passive_counters) # DEBUG + # we can't say for sure which error counter will hit the ERROR_PASSIVE threshold first + print( + "one over thresh", passive_counters[_IDX_TEC] >= 128 or passive_counters[_IDX_REC] >= 128 + ) + print( + "no new warning", passive_counters[_IDX_NUM_WARNING] == warning_counters[_IDX_NUM_WARNING] + ) + print( + "counted passive", + passive_counters[_IDX_NUM_PASSIVE] == warning_counters[_IDX_NUM_PASSIVE] + 1, + ) + + # Note that we can't get all the way to the most severe BUS_OFF error state + # with this test setup, as Bus Off requires more than just "normal" frame + # transmit errors. + + # restarting the controller may cause it to leave its error state, or not, depending + # on the implementation - but it shouldn't cause any recovery issues. Also cancels all pending TX + # (note: have to do this before 'fix baud' or we create a race condition for pending tx) + can.restart() + + # tell the receiver to go back to a valid baud rate + multitest.broadcast("fix baud") + multitest.wait("fixed baud") + + idx_more = can.send(_ID, b"MOREMORE") + time.sleep_ms(50) # irq_sender should fire during this window + print("queued moremore", idx_more is not None) + + print("done") diff --git a/tests/multi_extmod/machine_can_07_error_states.py.exp b/tests/multi_extmod/machine_can_07_error_states.py.exp new file mode 100644 index 0000000000000..158ae63bfbc7a --- /dev/null +++ b/tests/multi_extmod/machine_can_07_error_states.py.exp @@ -0,0 +1,37 @@ +--- instance0 --- +recv 0x100 5041594c4f4144 +can deinit() +can init() +can switch baud +sending bad msg +sending cancelling +cancelled True +re-init +done +--- instance1 --- +started STATE_ACTIVE +irq sent True +irq state 2 +irq state 3 +STATE_PASSIVE +tec increased True +tec over thresh True +counted warning True +counted passive True +some pending tx True +irq sent True +irq sent True +STATE_WARNING +tec decreased True +tec below thresh True +no new warning True +no new passive True +irq state 3 +queued yetmore True +STATE_PASSIVE +one over thresh True +no new warning True +counted passive True +irq sent True +queued moremore True +done diff --git a/tests/multi_extmod/machine_can_08_init_mode.py b/tests/multi_extmod/machine_can_08_init_mode.py new file mode 100644 index 0000000000000..459e6180ab62d --- /dev/null +++ b/tests/multi_extmod/machine_can_08_init_mode.py @@ -0,0 +1,102 @@ +from machine import CAN +from micropython import const +import time + +# instance0 transitions through various modes, instance1 +# listens for various messages (or not) +# +# Note this test assumes no other CAN nodes are connected apart from the test +# instances (or if they are connected they must be in silent mode.) +# +# TODO: This test needs to eventually support the case where modes aren't supported +# on a controller, maybe by printing fake output if the mode switch fails? + +# MODE_NORMAL, MODE_SLEEP, MODE_LOOPBACK, MODE_SILENT, MODE_SILENT_LOOPBACK +can = CAN(1, 500_000, mode=CAN.MODE_NORMAL) + +# While instance0 is in Silent mode, instance1 sends a message with this ID +# that will be retried for 100ms (as instance0 won't ACK). So don't print every one. +_SILENT_RX_ID = const(0x53) +silent_rx_count = 0 + + +def irq_print(can): + global silent_rx_count + while flags := can.irq().flags(): + if flags & can.IRQ_RX: + can_id, data, _flags, errors = can.recv() + if can_id != _SILENT_RX_ID: + print("recv", hex(can_id), bytes(data)) + else: + silent_rx_count += 1 + if flags & can.IRQ_TX: # note: only enabled on instance1 to avoid race conditions + print("send", "failed" if flags & can.IRQ_TX_FAILED else "ok") + + +def reinit_with_mode(mode): + can.deinit() + can.init(bitrate=500_000, mode=mode) + can.irq(irq_print, trigger=can.IRQ_RX, hard=False) + can.set_filters(None) # receive all + + +def instance0(): + multitest.next() + multitest.wait("instance1 ready") + + reinit_with_mode(can.MODE_NORMAL) + print("Normal", "MODE_NORMAL" in str(can)) + can.send(0x50, b"Normal") + time.sleep_ms(100) + + # Skipping MODE_SLEEP as means different things on different hardware + + reinit_with_mode(can.MODE_LOOPBACK) + print("Loopback", "MODE_LOOPBACK" in str(can)) + + # This message should go out to the bus, but will also be received by instance0 itself + can.send(0x51, b"Loopback") + time.sleep_ms(100) + + reinit_with_mode(can.MODE_SILENT) + print("Silent", "MODE_SLIENT" in str(can)) + + # This message shouldn't go out onto the bus + idx = can.send(0x52, b"Silent") + multitest.broadcast("silent") + multitest.wait("silent done") + # we should have received the message from instance1 many times, as instance0 won't have ACKed it + print("silent_rx_count", silent_rx_count > 5) + can.cancel_send(idx) + + reinit_with_mode(can.MODE_SILENT_LOOPBACK) + print("Silent Loopback", "MODE_SILENT_LOOPBACK" in str(can)) + + # This message should be received by instance0 only + idx = can.send(0x54, b"SiLoop") + time.sleep_ms(50) + + reinit_with_mode(can.MODE_NORMAL) + print("Normal again", "MODE_NORMAL" in str(can)) + can.send(0x55, b"Normal2") # should be received by instance1 only, again + multitest.broadcast("normal done") + + +# Receiver +def instance1(): + can.irq(irq_print, trigger=can.IRQ_RX | can.IRQ_TX, hard=False) + can.set_filters(None) # receive all + multitest.next() + + multitest.broadcast("instance1 ready") + + # The IRQ does most of the work on this instance + + multitest.wait("silent") + # Sending this message back, it should fail to send as Silent mode won't ACK it + idx = can.send(0x53, b"Silent2") + time.sleep_ms(20) + can.cancel_send(idx) + multitest.broadcast("silent done") + + multitest.wait("normal done") diff --git a/tests/multi_extmod/machine_can_08_init_mode.py.exp b/tests/multi_extmod/machine_can_08_init_mode.py.exp new file mode 100644 index 0000000000000..b9f0f11ae1065 --- /dev/null +++ b/tests/multi_extmod/machine_can_08_init_mode.py.exp @@ -0,0 +1,14 @@ +--- instance0 --- +Normal True +Loopback True +recv 0x51 b'Loopback' +Silent False +silent_rx_count True +Silent Loopback True +recv 0x54 b'SiLoop' +Normal again True +--- instance1 --- +recv 0x50 b'Normal' +recv 0x51 b'Loopback' +send failed +recv 0x55 b'Normal2' diff --git a/tests/multi_pyb_can/rx_callback.py.exp b/tests/multi_pyb_can/rx_callback.py.exp index 3ab197cc10319..2126a3fccb02f 100644 --- a/tests/multi_pyb_can/rx_callback.py.exp +++ b/tests/multi_pyb_can/rx_callback.py.exp @@ -2,8 +2,8 @@ rx0 reason full rx0 reason overflow rxed_spam True -(256, False, False, 0, b'aaaaa') +[256, False, False, 0, b'aaaaa'] any False --- instance1 --- -(85, False, False, 0, b'overflow') +[85, False, False, 0, b'overflow'] any False diff --git a/tests/multi_pyb_can/rx_filters.py.exp b/tests/multi_pyb_can/rx_filters.py.exp index 65fbdf4b1b22b..8016f641524a7 100644 --- a/tests/multi_pyb_can/rx_filters.py.exp +++ b/tests/multi_pyb_can/rx_filters.py.exp @@ -1,13 +1,13 @@ --- instance0 --- 0 -fifo0 (837, False, False, 0, b'') -fifo1 (14080, True, False, 0, b'') +fifo0 [837, False, False, 0, b''] +fifo1 [14080, True, False, 0, b''] 1 -fifo0 (837, False, False, 0, b'\x01\x03') -fifo1 (14081, True, False, 0, b'\xee') +fifo0 [837, False, False, 0, b'\x01\x03'] +fifo1 [14081, True, False, 0, b'\xee'] 2 -fifo0 (837, False, False, 0, b'\x02\x03\x02\x03') -fifo1 (14082, True, False, 0, b'\xee\xee') +fifo0 [837, False, False, 0, b'\x02\x03\x02\x03'] +fifo1 [14082, True, False, 0, b'\xee\xee'] Timed out as expected --- instance1 --- 0 diff --git a/tests/ports/stm32/can.py.exp b/tests/ports/stm32/can.py.exp deleted file mode 100644 index bd8f6d60b609e..0000000000000 --- a/tests/ports/stm32/can.py.exp +++ /dev/null @@ -1,74 +0,0 @@ -ValueError -1 -ValueError 0 -CAN 1 -ValueError 3 -CAN(1) -True -CAN(1, CAN.LOOPBACK, auto_restart=False) -False -True -[0, 0, 0, 0, 0, 0, 0, 0] -True [0, 0, 0, 0, 0, 0, 1, 0] -(123, False, False, 0, b'abcd') -(2047, False, False, 0, b'abcd') -(0, False, False, 0, b'abcd') -passed -[42, False, False, 0, ] 0 bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -[42, False, False, 0, ] 4 bytearray(b'1234\x00\x00\x00\x00\x00\x00') -[42, False, False, 0, ] 8 bytearray(b'01234567\x00\x00') -[42, False, False, 0, ] 3 bytearray(b'abc34567\x00\x00') -b'abc' -b'def' -TypeError -ValueError -TypeError -ValueError -ValueError -==== TEST extframe=True ==== -CAN(1, CAN.LOOPBACK, auto_restart=False) -passed -('0x8', '0x1c', '0xa', True, b'ok') -('0x800', '0x1c00', '0xa00', True, b'ok') -('0x80000', '0x1c0000', '0xa0000', True, b'ok') -('0x8000000', '0x1c000000', '0xa000000', True, b'ok') -==== TEST rx callbacks ==== -cb0 -pending -cb0 -full -cb0a -overflow -cb1 -pending -cb1 -full -cb1a -overflow -(1, False, False, 0, b'11111111') -(2, False, False, 1, b'22222222') -(4, False, False, 3, b'44444444') -(5, False, False, 0, b'55555555') -(6, False, False, 1, b'66666666') -(8, False, False, 3, b'88888888') -cb0a -pending -cb1a -pending -(1, False, False, 0, b'11111111') -(5, False, False, 0, b'55555555') -==== TEST async send ==== -False -(1, False, False, 0, b'abcde') -passed -(2, False, False, 0, b'abcde') -(3, False, False, 0, b'abcde') -(4, False, False, 0, b'abcde') -==== TEST rtr messages ==== -False -(5, False, True, 4, b'') -(6, False, True, 5, b'') -(7, False, True, 6, b'') -False -(32, False, True, 9, b'') -==== TEST errors ==== -OSError(110,) diff --git a/tests/ports/stm32/can2.py b/tests/ports/stm32/can2.py deleted file mode 100644 index 2ce438f1af971..0000000000000 --- a/tests/ports/stm32/can2.py +++ /dev/null @@ -1,25 +0,0 @@ -try: - from pyb import CAN - - CAN(2) -except (ImportError, ValueError): - print("SKIP") - raise SystemExit - -# Testing rtr messages -bus2 = CAN(2, CAN.LOOPBACK) -while bus2.any(0): - bus2.recv(0) -bus2.setfilter(0, CAN.LIST32, 0, (1, 2), rtr=(True, True), extframe=True) -bus2.setfilter(1, CAN.LIST32, 0, (3, 4), rtr=(True, False), extframe=True) -bus2.setfilter(2, CAN.MASK32, 0, (16, 16), rtr=(False,), extframe=True) -bus2.setfilter(2, CAN.MASK32, 0, (32, 32), rtr=(True,), extframe=True) - -bus2.send("", 1, rtr=True, extframe=True) -print(bus2.recv(0)) -bus2.send("", 2, rtr=True, extframe=True) -print(bus2.recv(0)) -bus2.send("", 3, rtr=True, extframe=True) -print(bus2.recv(0)) -bus2.send("", 4, rtr=True, extframe=True) -print(bus2.any(0)) diff --git a/tests/ports/stm32/can2.py.exp b/tests/ports/stm32/can2.py.exp deleted file mode 100644 index 3339e5cbecaaf..0000000000000 --- a/tests/ports/stm32/can2.py.exp +++ /dev/null @@ -1,4 +0,0 @@ -(1, True, True, 0, b'') -(2, True, True, 1, b'') -(3, True, True, 2, b'') -False diff --git a/tests/ports/stm32/can.py b/tests/ports/stm32/pyb_can.py similarity index 51% rename from tests/ports/stm32/can.py rename to tests/ports/stm32/pyb_can.py index 020efae0531ac..8178d91fe74f1 100644 --- a/tests/ports/stm32/can.py +++ b/tests/ports/stm32/pyb_can.py @@ -7,6 +7,15 @@ from array import array import micropython import pyb +import sys + +# Classic CAN (aka bxCAN) hardware has a different filter API +# and some different behaviours to newer FDCAN hardware +IS_CLASSIC = hasattr(CAN, "MASK16") + +# STM32H7 series has a gold-plated FDCAN peripheral with much deeper TX Queue +# than all other parts +IS_H7 = (not IS_CLASSIC) and "STM32H7" in str(sys.implementation) # test we can correctly create by id (2 handled in can2.py test) for bus in (-1, 0, 1, 3): @@ -25,22 +34,26 @@ can.init(CAN.LOOPBACK, num_filter_banks=14) print(can) -print(can.any(0)) +print("any", can.any(0)) # Test state when freshly created -print(can.state() == can.ERROR_ACTIVE) +print("error_active", can.state() == can.ERROR_ACTIVE) # Test that restart can be called can.restart() # Test info returns a sensible value -print(can.info()) +print("info", can.info()) -# Catch all filter -can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +# Catch all filter (standard IDs) +if IS_CLASSIC: + can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +else: + can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=False) can.send("abcd", 123, timeout=5000) -print(can.any(0), can.info()) +pyb.delay(10) # For FDCAN, needs some time to send +print("any+info", can.any(0), can.info()) print(can.recv(0)) can.send("abcd", -1, timeout=5000) @@ -51,11 +64,16 @@ # Test too long message try: - can.send("abcdefghi", 0x7FF, timeout=5000) + if IS_CLASSIC: + payload = "abcdefghi" # 9 bytes long + else: + # the pyb.CAN API for FDCAN always accepts messages up to 64 bytes and sends as FDCAN + payload = b"x" * 65 + can.send(payload, 0x7FF, timeout=5000) except ValueError: - print("passed") + print("overlong passed") else: - print("failed") + print("overlong failed") # Test that recv can work without allocating memory on the heap @@ -135,7 +153,13 @@ can = CAN(1, CAN.LOOPBACK) # Catch all filter, but only for extframe's -can.setfilter(0, CAN.MASK32, 0, (0, 0), extframe=True) +if IS_CLASSIC: + can.setfilter(0, CAN.MASK32, 0, (0, 0), extframe=True) +else: + # FDCAN manages standard and extframe IDs independently, so need to + # clear the standard ID filter and then set the extended ID filter + can.clearfilter(0, extframe=False) + can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=True) print(can) @@ -144,11 +168,12 @@ except ValueError: print("failed") else: + pyb.delay(10) r = can.recv(0) if r[0] == 0x7FF + 1 and r[4] == b"abcde": - print("passed") + print("extframe passed") else: - print("failed, wrong data received") + print("failed, wrong data received", r) # Test filters for n in [0, 8, 16, 24]: @@ -158,109 +183,40 @@ id_fail = 0b00011010 << n can.clearfilter(0, extframe=True) - can.setfilter(0, pyb.CAN.MASK32, 0, (filter_id, filter_mask), extframe=True) + if IS_CLASSIC: + can.setfilter(0, CAN.MASK32, 0, (filter_id, filter_mask), extframe=True) + else: + can.setfilter(0, CAN.MASK, 0, (filter_id, filter_mask), extframe=True) - can.send("ok", id_ok, timeout=3, extframe=True) + can.send("ok", id_ok, timeout=5, extframe=True) + pyb.delay(10) if can.any(0): msg = can.recv(0) print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4])) - can.send("fail", id_fail, timeout=3, extframe=True) + can.send("fail", id_fail, timeout=5, extframe=True) + pyb.delay(10) if can.any(0): msg = can.recv(0) print((hex(filter_id), hex(filter_mask), hex(msg[0]), msg[1], msg[4])) del can -# Test RxCallbacks -print("==== TEST rx callbacks ====") - -can = CAN(1, CAN.LOOPBACK) -can.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) -can.setfilter(1, CAN.LIST16, 1, (5, 6, 7, 8)) - - -def cb0(bus, reason): - print("cb0") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -def cb1(bus, reason): - print("cb1") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -def cb0a(bus, reason): - print("cb0a") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -def cb1a(bus, reason): - print("cb1a") - if reason == 0: - print("pending") - if reason == 1: - print("full") - if reason == 2: - print("overflow") - - -can.rxcallback(0, cb0) -can.rxcallback(1, cb1) - -can.send("11111111", 1, timeout=5000) -can.send("22222222", 2, timeout=5000) -can.send("33333333", 3, timeout=5000) -can.rxcallback(0, cb0a) -can.send("44444444", 4, timeout=5000) - -can.send("55555555", 5, timeout=5000) -can.send("66666666", 6, timeout=5000) -can.send("77777777", 7, timeout=5000) -can.rxcallback(1, cb1a) -can.send("88888888", 8, timeout=5000) - -print(can.recv(0)) -print(can.recv(0)) -print(can.recv(0)) -print(can.recv(1)) -print(can.recv(1)) -print(can.recv(1)) - -can.send("11111111", 1, timeout=5000) -can.send("55555555", 5, timeout=5000) - -print(can.recv(0)) -print(can.recv(1)) - -del can - # Testing asynchronous send print("==== TEST async send ====") can = CAN(1, CAN.LOOPBACK) -can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +# Catch all filter (standard IDs) +if IS_CLASSIC: + can.setfilter(0, CAN.MASK16, 0, (0, 0, 0, 0)) +else: + can.setfilter(0, CAN.MASK, 0, (0, 0), extframe=False) while can.any(0): can.recv(0) can.send("abcde", 1, timeout=0) -print(can.any(0)) +print("any", can.any(0)) while not can.any(0): pass @@ -270,45 +226,82 @@ def cb1a(bus, reason): can.send("abcde", 2, timeout=0) can.send("abcde", 3, timeout=0) can.send("abcde", 4, timeout=0) - can.send("abcde", 5, timeout=0) + if not IS_H7: + can.send("abcde", 5, timeout=0) + else: + # Hack around the STM32H7's deeper transmit queue by pretending this call failed + # (STM32G4 will fail here, using otherwise the same code, so there is still some test coverage.) + print("send fail ok") except OSError as e: - if str(e) == "16": - print("passed") + # When send() fails Classic CAN raises OSError(MP_EBUSY) (16), FDCAN raises OSError(MP_ETIMEDOUT) (110) + if e.errno == (16 if IS_CLASSIC else 110): + print("send fail ok") else: - print("failed") + print("send fail not ok", e) pyb.delay(500) while can.any(0): print(can.recv(0)) +del can + # Testing rtr messages print("==== TEST rtr messages ====") bus1 = CAN(1, CAN.LOOPBACK) while bus1.any(0): bus1.recv(0) -bus1.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) -bus1.setfilter(1, CAN.LIST16, 0, (5, 6, 7, 8), rtr=(True, True, True, True)) -bus1.setfilter(2, CAN.MASK16, 0, (64, 64, 32, 32), rtr=(False, True)) + +if IS_CLASSIC: + # pyb.CAN Classic API allows distinguishing between RTR in the filter + bus1.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) + bus1.setfilter(1, CAN.LIST16, 0, (5, 6, 7, 8), rtr=(True, True, True, True)) + bus1.setfilter(2, CAN.MASK16, 0, (64, 64, 32, 32), rtr=(False, True)) +else: + # pyb.CAN FDCAN API does not allow distinguishing RTR in filter args, so + # instead we'll only filter the message IDs where Classic CAN equivalent is + # setting the RTR flag (meaning we're verifying RTR is received correctly, but + # not verifying the missing filter behaviour.) + bus1.setfilter(0, CAN.RANGE, 0, (5, 8)) + bus1.setfilter(1, CAN.MASK, 0, (32, 32)) + + +def print_rtr(msg): + if msg: + # Skip printing msg[3] as this is the filter match index, and the value + # is different between Classic and FDCAN implementations + print(msg[0], msg[1], msg[2], msg[4]) + else: + print(msg) + bus1.send("", 1, rtr=True) -print(bus1.any(0)) +print("any", bus1.any(0)) bus1.send("", 5, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) bus1.send("", 6, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) bus1.send("", 7, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) bus1.send("", 16, rtr=True) -print(bus1.any(0)) +print("any", bus1.any(0)) bus1.send("", 32, rtr=True) -print(bus1.recv(0)) +print_rtr(bus1.recv(0)) + +del bus1 # test HAL error, timeout print("==== TEST errors ====") +# Note: this test requires no other CAN node is attached to the CAN peripheral can = pyb.CAN(1, pyb.CAN.NORMAL) try: - can.send("1", 1, timeout=50) + if IS_CLASSIC: + can.send("1", 1, timeout=50) + else: + # Difference between pyb.CAN on Classic vs FDCAN - Classic waits until the message is sent to the bus, + # FDCAN only times out if the TX queue is full + while True: + can.send("1", 1, timeout=50) except OSError as e: - print(repr(e)) + print("timeout", repr(e)) diff --git a/tests/ports/stm32/pyb_can.py.exp b/tests/ports/stm32/pyb_can.py.exp new file mode 100644 index 0000000000000..80f44981231ea --- /dev/null +++ b/tests/ports/stm32/pyb_can.py.exp @@ -0,0 +1,49 @@ +ValueError -1 +ValueError 0 +CAN 1 +ValueError 3 +CAN(1) +True +CAN(1, CAN.LOOPBACK, auto_restart=False) +any False +error_active True +info [0, 0, 0, 0, 0, 0, 0, 0] +any+info True [0, 0, 0, 0, 0, 0, 1, 0] +[123, False, False, 0, b'abcd'] +[2047, False, False, 0, b'abcd'] +[0, False, False, 0, b'abcd'] +overlong passed +[42, False, False, 0, ] 0 bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') +[42, False, False, 0, ] 4 bytearray(b'1234\x00\x00\x00\x00\x00\x00') +[42, False, False, 0, ] 8 bytearray(b'01234567\x00\x00') +[42, False, False, 0, ] 3 bytearray(b'abc34567\x00\x00') +b'abc' +b'def' +TypeError +ValueError +TypeError +ValueError +ValueError +==== TEST extframe=True ==== +CAN(1, CAN.LOOPBACK, auto_restart=False) +extframe passed +('0x8', '0x1c', '0xa', True, b'ok') +('0x800', '0x1c00', '0xa00', True, b'ok') +('0x80000', '0x1c0000', '0xa0000', True, b'ok') +('0x8000000', '0x1c000000', '0xa000000', True, b'ok') +==== TEST async send ==== +any False +[1, False, False, 0, b'abcde'] +send fail ok +[2, False, False, 0, b'abcde'] +[3, False, False, 0, b'abcde'] +[4, False, False, 0, b'abcde'] +==== TEST rtr messages ==== +any False +5 False True b'' +6 False True b'' +7 False True b'' +any False +32 False True b'' +==== TEST errors ==== +timeout OSError(110,) diff --git a/tests/ports/stm32/pyb_can2.py b/tests/ports/stm32/pyb_can2.py new file mode 100644 index 0000000000000..62ae935357c7e --- /dev/null +++ b/tests/ports/stm32/pyb_can2.py @@ -0,0 +1,50 @@ +try: + from pyb import CAN + + CAN(2) +except (ImportError, ValueError): + print("SKIP") + raise SystemExit + +# Classic CAN (aka bxCAN) hardware has a different filter API +# and some different behaviours to newer FDCAN hardware +IS_CLASSIC = hasattr(CAN, "MASK16") + +# Setting up each CAN peripheral independently is deliberate here, to catch +# catch cases where initialising CAN2 breaks CAN1 + +can1 = CAN(1, CAN.LOOPBACK) +if IS_CLASSIC: + can1.setfilter(0, CAN.LIST16, 0, (123, 124, 125, 126)) +else: + can1.setfilter(0, CAN.RANGE, 0, (123, 126)) + +can2 = CAN(2, CAN.LOOPBACK) +if IS_CLASSIC: + can2.setfilter(0, CAN.LIST16, 0, (3, 4, 5, 6)) +else: + can2.setfilter(0, CAN.RANGE, 0, (3, 6)) + +# Drain any old messages in RX FIFOs +for can in (can1, can2): + while can.any(0): + can.recv(0) + +for id, can in ((1, can1), (2, can2)): + print("testing", id) + # message1 should only receive on can1, message2 on can2 + can.send("message1", 123) + can.send("message2", 3) + did_recv = False + try: + while True: + res = can.recv(0, timeout=50) + # not printing all of 'res' as the filter index result is different + # on Classic vs FD-CAN + print("rx", res[0], res[4]) + did_recv = True + except OSError: + if not did_recv: + print("no rx!") + +print("done") diff --git a/tests/ports/stm32/pyb_can2.py.exp b/tests/ports/stm32/pyb_can2.py.exp new file mode 100644 index 0000000000000..9696f2fa0108f --- /dev/null +++ b/tests/ports/stm32/pyb_can2.py.exp @@ -0,0 +1,5 @@ +testing 1 +rx 123 b'message1' +testing 2 +rx 3 b'message2' +done diff --git a/tests/ports/stm32/pyb_can_classic_rtr_filter.py b/tests/ports/stm32/pyb_can_classic_rtr_filter.py new file mode 100644 index 0000000000000..90ae28ca9be38 --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rtr_filter.py @@ -0,0 +1,30 @@ +try: + from pyb import CAN +except ImportError: + print("SKIP") + raise SystemExit + +if not hasattr(CAN, "LIST16"): + # This test relies on the RTR-aware filters of Classic CAN, + # so needs to be skipped on FDCAN hardware + print("SKIP") + raise SystemExit + +can = CAN(1, CAN.LOOPBACK) +while can.any(0): + can.recv(0) +can.setfilter(0, CAN.LIST32, 0, (1, 2), rtr=(True, True), extframe=True) +can.setfilter(1, CAN.LIST32, 0, (3, 4), rtr=(True, False), extframe=True) +can.setfilter(2, CAN.MASK32, 0, (16, 16), rtr=(False,), extframe=True) +can.setfilter(2, CAN.MASK32, 0, (32, 32), rtr=(True,), extframe=True) + +can.send("", 1, rtr=True, extframe=True) +print(can.recv(0)) +can.send("", 2, rtr=True, extframe=True) +print(can.recv(0)) +can.send("", 3, rtr=True, extframe=True) +print(can.recv(0)) +can.send("", 4, rtr=True, extframe=True) +print(can.any(0)) + +del can diff --git a/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp b/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp new file mode 100644 index 0000000000000..b970a0509feba --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rtr_filter.py.exp @@ -0,0 +1,4 @@ +[1, True, True, 0, b''] +[2, True, True, 1, b''] +[3, True, True, 2, b''] +False diff --git a/tests/ports/stm32/pyb_can_classic_rx.py b/tests/ports/stm32/pyb_can_classic_rx.py new file mode 100644 index 0000000000000..39a5eadca54f0 --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rx.py @@ -0,0 +1,89 @@ +try: + from pyb import CAN +except ImportError: + print("SKIP") + raise SystemExit + +if not hasattr(CAN, "LIST16"): + # This test relies on some specific behaviours of the Classic CAN RX FIFO + # interrupt, so needs to be skipped on FDCAN hardware + print("SKIP") + raise SystemExit + +# Test RxCallbacks +print("==== TEST rx callbacks ====") + +can = CAN(1, CAN.LOOPBACK) +can.setfilter(0, CAN.LIST16, 0, (1, 2, 3, 4)) +can.setfilter(1, CAN.LIST16, 1, (5, 6, 7, 8)) + + +def cb0(bus, reason): + print("cb0") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +def cb1(bus, reason): + print("cb1") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +def cb0a(bus, reason): + print("cb0a") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +def cb1a(bus, reason): + print("cb1a") + if reason == 0: + print("pending") + if reason == 1: + print("full") + if reason == 2: + print("overflow") + + +can.rxcallback(0, cb0) +can.rxcallback(1, cb1) + +can.send("11111111", 1, timeout=5000) +can.send("22222222", 2, timeout=5000) +can.send("33333333", 3, timeout=5000) +can.rxcallback(0, cb0a) +can.send("44444444", 4, timeout=5000) + +can.send("55555555", 5, timeout=5000) +can.send("66666666", 6, timeout=5000) +can.send("77777777", 7, timeout=5000) +can.rxcallback(1, cb1a) +can.send("88888888", 8, timeout=5000) + +print(can.recv(0)) +print(can.recv(0)) +print(can.recv(0)) +print(can.recv(1)) +print(can.recv(1)) +print(can.recv(1)) + +can.send("11111111", 1, timeout=5000) +can.send("55555555", 5, timeout=5000) + +print(can.recv(0)) +print(can.recv(1)) + +del can diff --git a/tests/ports/stm32/pyb_can_classic_rx.py.exp b/tests/ports/stm32/pyb_can_classic_rx.py.exp new file mode 100644 index 0000000000000..c1c743c42b5be --- /dev/null +++ b/tests/ports/stm32/pyb_can_classic_rx.py.exp @@ -0,0 +1,25 @@ +==== TEST rx callbacks ==== +cb0 +pending +cb0 +full +cb0a +overflow +cb1 +pending +cb1 +full +cb1a +overflow +[1, False, False, 0, b'11111111'] +[2, False, False, 1, b'22222222'] +[4, False, False, 3, b'44444444'] +[5, False, False, 0, b'55555555'] +[6, False, False, 1, b'66666666'] +[8, False, False, 3, b'88888888'] +cb0a +pending +cb1a +pending +[1, False, False, 0, b'11111111'] +[5, False, False, 0, b'55555555'] diff --git a/tests/ports/unix/extra_coverage.py.exp b/tests/ports/unix/extra_coverage.py.exp index 3a46994c23d8d..1c3fe1558f239 100644 --- a/tests/ports/unix/extra_coverage.py.exp +++ b/tests/ports/unix/extra_coverage.py.exp @@ -75,7 +75,7 @@ json machine marshal math os platform random re select socket string struct sys termios time tls -uctypes vfs websocket +uctypes vfs weakref websocket me micropython machine marshal math @@ -115,6 +115,13 @@ deadbeef 0deadbeef c0ffee 000c0ffee +# list argument helpers +TypeError: +ValueError: +mp_obj_list_ensure same list? 1 +mp_obj_list_optional_arg same list? 1 +mp_obj_list_optional_arg new list len 3 +mp_obj_list_optional_arg new list from NULL len 3 # runtime utils TypeError: unsupported type for __abs__: 'str' TypeError: unsupported types for __divmod__: 'str', 'str' diff --git a/tests/ports/webassembly/heap_expand.mjs.exp b/tests/ports/webassembly/heap_expand.mjs.exp index 67ebe98e7fe02..4161fc7eaed83 100644 --- a/tests/ports/webassembly/heap_expand.mjs.exp +++ b/tests/ports/webassembly/heap_expand.mjs.exp @@ -1,27 +1,27 @@ -135241312 -135241280 -135241248 -135241216 -135241168 -135241120 -135241040 -135240896 -135240592 -135240064 -135239024 -135236960 +135233568 +135233536 +135233504 +135233472 +135233424 +135233376 +135233296 +135233152 135232848 -135224640 -135208240 -135175456 -135109840 -134978752 -134716592 -135216800 -136217168 -138217984 -142219568 -150222816 +135232320 +135231280 +135229216 +135225104 +135216896 +135200496 +135167712 +135102096 +134971008 +134708848 +135201312 +136186256 +138156160 +142095984 +149975648 1 2 4 diff --git a/tests/ports/webassembly/weakref_finalize_collect.mjs b/tests/ports/webassembly/weakref_finalize_collect.mjs new file mode 100644 index 0000000000000..1e0bc951350db --- /dev/null +++ b/tests/ports/webassembly/weakref_finalize_collect.mjs @@ -0,0 +1,86 @@ +// Test weakref.finalize() functionality requiring gc.collect(). +// Should be kept in sync with tests/basics/weakref_finalize_collect.py. +// +// This needs custom testing on the webassembly port since the GC can only +// run when Python code returns to JavaScript. + +const mp = await (await import(process.argv[2])).loadMicroPython(); + +// Set up. +mp.runPython(` +import gc, weakref + +class A: + def __str__(self): + return "" + +def callback(*args, **kwargs): + print("callback({}, {})".format(args, kwargs)) + return 42 +`); + +console.log("test basic use of finalize() with a simple callback"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) +`); + +console.log("test that a callback is passed the correct values"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback, 1, 2, kwarg=3) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print("alive", f.alive) + print("peek", f.peek()) + print("detach", f.detach()) + print("call", f()) +`); + +console.log("test that calling the finalizer cancels the finalizer"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback) + print(f()) + print(a) + a = None + gc.collect() +`); +console.log("(outside Python)"); + +console.log("test that calling detach cancels the finalizer"); +mp.runPython(` + a = A() + f = weakref.finalize(a, callback) + print(len(f.detach())) + print(a) + a = None + gc.collect() +`); +console.log("(outside Python)"); + +console.log("test that finalize does not get collected before its ref does"); +mp.runPython(` + a = A() + weakref.finalize(a, callback) + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print("free a") + a = None + gc.collect() +`); +console.log("(outside Python)"); diff --git a/tests/ports/webassembly/weakref_finalize_collect.mjs.exp b/tests/ports/webassembly/weakref_finalize_collect.mjs.exp new file mode 100644 index 0000000000000..e8087a4ae9bd9 --- /dev/null +++ b/tests/ports/webassembly/weakref_finalize_collect.mjs.exp @@ -0,0 +1,28 @@ +test basic use of finalize() with a simple callback +callback((), {}) +(outside Python) +alive False +peek None +detach None +call None +test that a callback is passed the correct values +callback((1, 2), {'kwarg': 3}) +(outside Python) +alive False +peek None +detach None +call None +test that calling the finalizer cancels the finalizer +callback((), {}) +42 + +(outside Python) +test that calling detach cancels the finalizer +4 + +(outside Python) +test that finalize does not get collected before its ref does +(outside Python) +free a +callback((), {}) +(outside Python) diff --git a/tests/ports/webassembly/weakref_ref_collect.mjs b/tests/ports/webassembly/weakref_ref_collect.mjs new file mode 100644 index 0000000000000..546a851f0ac09 --- /dev/null +++ b/tests/ports/webassembly/weakref_ref_collect.mjs @@ -0,0 +1,69 @@ +// Test weakref.ref() functionality requiring gc.collect(). +// Should be kept in sync with tests/basics/weakref_ref_collect.py. +// +// This needs custom testing on the webassembly port since the GC can only +// run when Python code returns to JavaScript. + +const mp = await (await import(process.argv[2])).loadMicroPython(); + +// Set up. +mp.runPython(` +import gc, weakref + +class A: + def __str__(self): + return "" + +def callback(r): + print("callback", r()) +`); + +console.log("test basic use of ref() with only one argument"); +mp.runPython(` + a = A() + r = weakref.ref(a) + print(r()) + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) +`); + +console.log("test use of ref() with a callback"); +mp.runPython(` + a = A() + r = weakref.ref(a, callback) + print(r()) + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) + a = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + print(r()) +`); + +console.log("test when weakref gets collected before the object it refs"); +mp.runPython(` + a = A() + r = weakref.ref(a, callback) + print(r()) + r = None + gc.collect() +`); +console.log("(outside Python)"); +mp.runPython(` + a = None + gc.collect() +`); diff --git a/tests/ports/webassembly/weakref_ref_collect.mjs.exp b/tests/ports/webassembly/weakref_ref_collect.mjs.exp new file mode 100644 index 0000000000000..f903d41702815 --- /dev/null +++ b/tests/ports/webassembly/weakref_ref_collect.mjs.exp @@ -0,0 +1,16 @@ +test basic use of ref() with only one argument + +(outside Python) + +(outside Python) +None +test use of ref() with a callback + +(outside Python) + +callback None +(outside Python) +None +test when weakref gets collected before the object it refs + +(outside Python) diff --git a/tests/run-tests.py b/tests/run-tests.py index 84daf4cbbf8d3..153424f65f72c 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -158,6 +158,9 @@ "webassembly": ( "basics/string_format_modulo.py", # can't print nulls to stdout "basics/string_strip.py", # can't print nulls to stdout + "basics/weakref_callback_exception.py", # has different exception printing output + "basics/weakref_ref_collect.py", # requires custom test due to GC behaviour + "basics/weakref_finalize_collect.py", # requires custom test due to GC behaviour "extmod/asyncio_basic2.py", "extmod/asyncio_cancel_self.py", "extmod/asyncio_current_task.py", @@ -289,7 +292,9 @@ "extmod/machine_spi_rate.py", "extmod/machine_uart_irq_txidle.py", "extmod/machine_uart_tx.py", + "extmod_hardware/machine_can_timings.py", "extmod_hardware/machine_encoder.py", + "extmod_hardware/machine_pwm.py", "extmod_hardware/machine_uart_irq_break.py", "extmod_hardware/machine_uart_irq_rx.py", "extmod_hardware/machine_uart_irq_rxidle.py", @@ -431,6 +436,7 @@ def detect_target_wiring_script(pyb, args): "micropython/meminfo.py", "basics/bytes_compare3.py", "basics/builtin_help.py", + "basics/weakref_callback_exception.py", "misc/sys_settrace_cov.py", "net_inet/tls_text_errors.py", "thread/thread_exc2.py", diff --git a/tests/target_wiring/NUCLEO_WB55.py b/tests/target_wiring/NUCLEO_WB55.py index 166f6f8d35ff9..e40d35e225b61 100644 --- a/tests/target_wiring/NUCLEO_WB55.py +++ b/tests/target_wiring/NUCLEO_WB55.py @@ -8,3 +8,5 @@ uart_loopback_kwargs = {} spi_standalone_args_list = [(1,), (2,)] + +pwm_loopback_pins = [("D1", "D0")] diff --git a/tests/target_wiring/PYBx.py b/tests/target_wiring/PYBx.py index a419320d90279..b36e342e944ea 100644 --- a/tests/target_wiring/PYBx.py +++ b/tests/target_wiring/PYBx.py @@ -8,3 +8,9 @@ uart_loopback_kwargs = {} spi_standalone_args_list = [(1,), (2,)] + +# CAN args assume no connection for single device tests +can_args = (1,) +can_kwargs = {} + +pwm_loopback_pins = [("X1", "X2")] diff --git a/tests/target_wiring/README.md b/tests/target_wiring/README.md index 96c98da8dff7f..c3a7038bb9111 100644 --- a/tests/target_wiring/README.md +++ b/tests/target_wiring/README.md @@ -48,6 +48,20 @@ SPI instances will be created using: for spi_args in spi_standalone_args_list: machine.SPI(*spi_args) +### PWM tests + +PWM tests require a PWM output to be connected in loopback mode to another GPIO pin +that will use `machine.time_pulse_us()` to time the PWM output signal. The variables +are: + + pwm_loopback_pins: list[tuple[Any, Any]] + +The PWM and input Pin instances will be created using: + + for pwm_pin, pulse_pin in pwm_loopback_pins: + machine.PWM(pwm_pin) + machine.Pin(pulse_pin, Pin.IN) + ### Encoder tests Encoder tests require one encoder to be connected in loopback mode to two other GPIO diff --git a/tests/target_wiring/alif.py b/tests/target_wiring/alif.py index cb62ea407200a..708b4591846cb 100644 --- a/tests/target_wiring/alif.py +++ b/tests/target_wiring/alif.py @@ -7,3 +7,5 @@ uart_loopback_kwargs = {} spi_standalone_args_list = [(0,)] + +pwm_loopback_pins = [("P0_4", "P0_5")] diff --git a/tests/target_wiring/esp32.py b/tests/target_wiring/esp32.py index a3d39b5111f44..d94a6f607596f 100644 --- a/tests/target_wiring/esp32.py +++ b/tests/target_wiring/esp32.py @@ -14,6 +14,8 @@ else: spi_standalone_args_list = [(1,), (2,)] +pwm_loopback_pins = [(4, 5)] + encoder_loopback_id = 0 encoder_loopback_out_pins = (4, 12) encoder_loopback_in_pins = (5, 13) diff --git a/tests/target_wiring/esp8266.py b/tests/target_wiring/esp8266.py index ba9bca01c864b..ee0c3020bfbe1 100644 --- a/tests/target_wiring/esp8266.py +++ b/tests/target_wiring/esp8266.py @@ -7,3 +7,5 @@ uart_loopback_kwargs = {} spi_standalone_args_list = [(1,)] + +pwm_loopback_pins = [(4, 5)] diff --git a/tests/target_wiring/mimxrt.py b/tests/target_wiring/mimxrt.py index e1e51ea143df3..2836d88ab9d9f 100644 --- a/tests/target_wiring/mimxrt.py +++ b/tests/target_wiring/mimxrt.py @@ -3,11 +3,23 @@ # Connect: # - UART1 TX and RX, usually D0 and D1 +import sys + uart_loopback_args = (1,) uart_loopback_kwargs = {} spi_standalone_args_list = [()] +if "Teensy" in sys.implementation._machine: + # Teensy 4.x + pwm_loopback_pins = [ + ("D0", "D1"), # FLEXPWM X and UART 1 + ("D2", "D3"), # FLEXPWM A/B + ("D11", "D12"), # QTMR and MOSI/MISO of SPI 0 + ] +else: + pwm_loopback_pins = [("D0", "D1")] + encoder_loopback_id = 0 encoder_loopback_out_pins = ("D0", "D2") encoder_loopback_in_pins = ("D1", "D3") diff --git a/tests/target_wiring/rp2.py b/tests/target_wiring/rp2.py index 4024eb20884c2..c11cbd469049e 100644 --- a/tests/target_wiring/rp2.py +++ b/tests/target_wiring/rp2.py @@ -7,3 +7,5 @@ uart_loopback_kwargs = {"tx": "GPIO0", "rx": "GPIO1"} spi_standalone_args_list = [(0,), (1,)] + +pwm_loopback_pins = [("GPIO0", "GPIO1")] diff --git a/tests/target_wiring/samd.py b/tests/target_wiring/samd.py index 1ee67e8e74940..50c7b087e5119 100644 --- a/tests/target_wiring/samd.py +++ b/tests/target_wiring/samd.py @@ -7,3 +7,5 @@ uart_loopback_kwargs = {"tx": "D1", "rx": "D0"} spi_standalone_args_list = [()] + +pwm_loopback_pins = [("D0", "D1")] diff --git a/tests/target_wiring/stm32.py b/tests/target_wiring/stm32.py new file mode 100644 index 0000000000000..533431e694795 --- /dev/null +++ b/tests/target_wiring/stm32.py @@ -0,0 +1,12 @@ +# Target wiring for non-PyBoard stm32 boards +# +# See PYBx.py for PyBoards +# +# Connect: +# - D0 to D1 (Arduino labels) + +# CAN args assume no connection for single device tests +can_args = (1,) +can_kwargs = {} + +pwm_loopback_pins = [("D0", "D1")] diff --git a/tools/ci.sh b/tools/ci.sh index 6bc74bd54ce48..056f6a6102d96 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -10,7 +10,9 @@ fi ulimit -n 1024 # Fail on some things which are warnings otherwise -export MICROPY_MAINTAINER_BUILD=1 +if [ -z "$MICROPY_MAINTAINER_BUILD" ]; then + export MICROPY_MAINTAINER_BUILD=1 +fi ######################################################################################## # general helper functions @@ -206,20 +208,11 @@ function ci_embedding_build { ######################################################################################## # ports/esp32 -# GitHub tag of ESP-IDF to use for CI, extracted from the esp32 dependency lockfile -# This should end up as a tag name like vX.Y.Z -# (note: This hacky parsing can be replaced with 'yq' once Ubuntu >=24.04 is in use) -IDF_VER=v$(grep -A10 "idf:" ports/esp32/lockfiles/dependencies.lock.esp32 | grep "version:" | head -n1 | sed -E 's/ +version: //') -PYTHON=$(command -v python3 2> /dev/null) -PYTHON_VER=$(${PYTHON:-python} --version | cut -d' ' -f2) - -export IDF_CCACHE_ENABLE=1 - -function ci_esp32_idf_ver { - echo "IDF_VER=${IDF_VER}-py${PYTHON_VER}" -} - function ci_esp32_idf_setup { + if [ -z "$IDF_VER" ]; then + echo "IDF_VER environment variable must be set before running" + return 1 + fi echo "Using ESP-IDF version $IDF_VER" git clone --depth 1 --branch $IDF_VER https://github.com/espressif/esp-idf.git # doing a treeless clone isn't quite as good as --shallow-submodules, but it @@ -945,9 +938,9 @@ function ci_unix_qemu_arm_build { function ci_unix_qemu_arm_run_tests { # Issues with ARM tests: - # - thread/stress_aes.py takes around 70 seconds + # - thread/stress_aes.py takes around 90 seconds file ./ports/unix/build-coverage/micropython - (cd tests && MICROPY_MICROPYTHON=../ports/unix/build-coverage/micropython MICROPY_TEST_TIMEOUT=90 ./run-tests.py) + (cd tests && MICROPY_MICROPYTHON=../ports/unix/build-coverage/micropython MICROPY_TEST_TIMEOUT=120 ./run-tests.py) } function ci_unix_qemu_riscv64_setup { diff --git a/tools/mpremote/mpremote/commands.py b/tools/mpremote/mpremote/commands.py index 4974c71e2e9c3..3f57f67347516 100644 --- a/tools/mpremote/mpremote/commands.py +++ b/tools/mpremote/mpremote/commands.py @@ -4,10 +4,10 @@ import os import sys import tempfile -import zlib import serial.tools.list_ports +from .compression_utils import compress_chunk, DEFLATE_WBITS from .transport import TransportError, TransportExecError, stdout_write_bytes from .transport_serial import SerialTransport from .romfs import make_romfs, VfsRomWriter @@ -682,18 +682,20 @@ def _do_romfs_deploy(state, args): chunk_size = max(chunk_size, rom_min_write) # Detect capabilities of the device to use the fastest method of transfer. - has_bytes_fromhex = transport.eval("hasattr(bytes,'fromhex')") - try: + caps = transport.detect_encoding_capabilities() + has_deflate_io = caps.get("deflate", False) + has_a2b_base64 = caps.get("base64", False) + has_bytes_fromhex = caps.get("fromhex", False) + + # Import encoding modules on device (detection uses hasattr, not exec). + if has_deflate_io: + transport.exec( + "from binascii import a2b_base64\n" + "from io import BytesIO\n" + "from deflate import DeflateIO,RAW" + ) + elif has_a2b_base64: transport.exec("from binascii import a2b_base64") - has_a2b_base64 = True - except TransportExecError: - has_a2b_base64 = False - try: - transport.exec("from io import BytesIO") - transport.exec("from deflate import DeflateIO,RAW") - has_deflate_io = True - except TransportExecError: - has_deflate_io = False # Deploy the ROMFS filesystem image to the device. for offset in range(0, len(romfs), chunk_size): @@ -701,14 +703,12 @@ def _do_romfs_deploy(state, args): romfs_chunk += bytes(chunk_size - len(romfs_chunk)) if has_deflate_io: # Needs: binascii.a2b_base64, io.BytesIO, deflate.DeflateIO. - compressor = zlib.compressobj(wbits=-9) - romfs_chunk_compressed = compressor.compress(romfs_chunk) - romfs_chunk_compressed += compressor.flush() + romfs_chunk_compressed = compress_chunk(romfs_chunk) buf = binascii.b2a_base64(romfs_chunk_compressed).strip() - transport.exec(f"buf=DeflateIO(BytesIO(a2b_base64({buf})),RAW,9).read()") + transport.exec(f"buf=DeflateIO(BytesIO(a2b_base64({buf})),RAW,{DEFLATE_WBITS}).read()") elif has_a2b_base64: # Needs: binascii.a2b_base64. - buf = binascii.b2a_base64(romfs_chunk) + buf = binascii.b2a_base64(romfs_chunk).strip() transport.exec(f"buf=a2b_base64({buf})") elif has_bytes_fromhex: # Needs: bytes.fromhex. diff --git a/tools/mpremote/mpremote/compression_utils.py b/tools/mpremote/mpremote/compression_utils.py new file mode 100644 index 0000000000000..35a3a17e95c56 --- /dev/null +++ b/tools/mpremote/mpremote/compression_utils.py @@ -0,0 +1,85 @@ +# +# This file is part of the MicroPython project, http://micropython.org/ +# +# The MIT License (MIT) +# +# Copyright (c) 2024 Andrew Leech +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import zlib + +# Minimum file size to attempt compression testing (bytes). +# Below this, decompression setup overhead on device isn't worth it. +MIN_COMPRESS_SIZE = 256 + +# Compression ratio threshold: use deflate only if compressed/original < this. +# 0.80 means require at least 20% size reduction. +MIN_COMPRESS_RATIO = 0.80 + +# Chunk sizes for each encoding method. +# Larger chunks = fewer exec() round trips over serial. +DEFLATE_CHUNK_SIZE = 4096 # Compressed chunks are smaller on wire +BASE64_CHUNK_SIZE = 2048 # Still benefits from fewer round trips + +# Device-side decompression window size (512 bytes, minimal RAM). +DEFLATE_WBITS = 9 +# Host-side compression: negative = raw deflate (no zlib header). +DEFAULT_WBITS = -DEFLATE_WBITS + +# Sample size for compression ratio testing. +COMPRESS_SAMPLE_SIZE = 4096 + + +def compress_chunk(data, wbits=DEFAULT_WBITS): + """Compress a single chunk using raw deflate. + + Each chunk is independently compressed/decompressible, which is required + for per-chunk transfer where each exec() call decompresses one chunk. + + Args: + data: Bytes to compress. + wbits: Window bits for deflate. Negative = raw deflate (no header). + Default -9 = 512-byte window, minimal device RAM usage. + + Returns: + Compressed bytes in raw deflate format. + """ + compressor = zlib.compressobj(wbits=wbits) + return compressor.compress(data) + compressor.flush() + + +def test_compression_ratio(data, sample_size=COMPRESS_SAMPLE_SIZE): + """Test compression ratio on a data sample. + + Compresses a prefix of the data to estimate overall compressibility + without processing the entire file. + + Args: + data: The full data to test. + sample_size: Max bytes to sample from start of data. + + Returns: + Ratio of compressed/original size (0.0-1.0+). Lower = better compression. + """ + sample = data[:sample_size] + if not sample: + return 1.0 + compressed = compress_chunk(sample) + return len(compressed) / len(sample) diff --git a/tools/mpremote/mpremote/transport.py b/tools/mpremote/mpremote/transport.py index d7568b281b1be..446d3311a745c 100644 --- a/tools/mpremote/mpremote/transport.py +++ b/tools/mpremote/mpremote/transport.py @@ -24,8 +24,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import ast, errno, hashlib, os, re, sys +import ast, binascii, errno, hashlib, os, re, sys from collections import namedtuple +from .compression_utils import ( + compress_chunk, + test_compression_ratio, + MIN_COMPRESS_SIZE, + MIN_COMPRESS_RATIO, + DEFLATE_CHUNK_SIZE, + DEFLATE_WBITS, + BASE64_CHUNK_SIZE, +) from .mp_errno import MP_ERRNO_TABLE @@ -151,16 +160,127 @@ def fs_readfile(self, src, chunk_size=256, progress_callback=None): return contents - def fs_writefile(self, dest, data, chunk_size=256, progress_callback=None): + def detect_encoding_capabilities(self): + """Detect available encoding methods on device. Cached after first call.""" + if hasattr(self, "_fs_encoding_caps"): + return self._fs_encoding_caps + + # Two separate eval() calls so that a missing deflate module (which + # raises ImportError inside the dict expression) doesn't prevent + # base64/bytesio from being detected. + try: + caps = self.eval( + "{" + "'bytesio':hasattr(__import__('io'),'BytesIO')," + "'base64':hasattr(__import__('binascii'),'a2b_base64')," + "'fromhex':hasattr(bytes,'fromhex')," + "}" + ) + except TransportExecError: + caps = {} + + try: + has_deflate = self.eval("hasattr(__import__('deflate'),'DeflateIO')") + except TransportExecError: + has_deflate = False + + self._fs_encoding_caps = { + "deflate": has_deflate and caps.get("bytesio") and caps.get("base64"), + "base64": caps.get("base64", False), + "fromhex": caps.get("fromhex", False), + } + + return self._fs_encoding_caps + + def _choose_encoding_for_data(self, data): + """Choose best encoding based on device capabilities and data compressibility. + + Three-tier fallback: + 1. deflate+base64 - if device has deflate/io/binascii and data compresses well + 2. base64 - if device has binascii.a2b_base64 + 3. repr - universal fallback + + Returns: (encoding, ratio) where encoding is 'deflate', 'base64', or 'repr', + and ratio is the compression ratio (float) for deflate, or None otherwise. + """ + caps = self.detect_encoding_capabilities() + + if caps.get("deflate") and len(data) > MIN_COMPRESS_SIZE: + ratio = test_compression_ratio(data) + if ratio < MIN_COMPRESS_RATIO: + return "deflate", ratio + + if caps.get("base64"): + return "base64", None + + return "repr", None + + _VALID_ENCODINGS = ("deflate", "base64", "repr") + + def fs_writefile(self, dest, data, chunk_size=None, progress_callback=None, encoding=None): + """Write data to a file on the device. + + Automatically selects the best encoding based on device capabilities and + data compressibility, with dynamic chunk sizing per encoding method. + + Args: + dest: Destination path on device + data: Bytes to write + chunk_size: Chunk size in bytes, or None for encoding-appropriate default. + progress_callback: Optional callback(written, total) + encoding: Force encoding: 'deflate', 'base64', 'repr', or None (auto-select) + """ if progress_callback: src_size = len(data) written = 0 + # Auto-select encoding based on data compressibility + if encoding is None: + encoding, _ratio = self._choose_encoding_for_data(data) + + if encoding not in self._VALID_ENCODINGS: + raise ValueError( + "encoding must be one of %s, got %r" % (self._VALID_ENCODINGS, encoding) + ) + + # Dynamic chunk sizing: larger chunks = fewer exec() round trips. + # Only auto-size when caller didn't specify. + if chunk_size is None: + if encoding == "deflate": + chunk_size = DEFLATE_CHUNK_SIZE + elif encoding == "base64": + chunk_size = BASE64_CHUNK_SIZE + else: + chunk_size = 256 + try: - self.exec("f=open('%s','wb')\nw=f.write" % dest) + # Setup imports and file handle on device + if encoding == "deflate": + self.exec( + "from binascii import a2b_base64\n" + "from io import BytesIO\n" + "from deflate import DeflateIO,RAW\n" + "f=open('%s','wb')\nw=f.write" % dest + ) + elif encoding == "base64": + self.exec("from binascii import a2b_base64\nf=open('%s','wb')\nw=f.write" % dest) + else: + self.exec("f=open('%s','wb')\nw=f.write" % dest) + while data: chunk = data[:chunk_size] - self.exec("w(" + repr(chunk) + ")") + if encoding == "deflate": + compressed = compress_chunk(chunk) + b64 = binascii.b2a_base64(compressed).strip() + self.exec( + "w(DeflateIO(BytesIO(a2b_base64(%s)),RAW,%d).read())" + % (b64, DEFLATE_WBITS) + ) + elif encoding == "base64": + b64 = binascii.b2a_base64(chunk).strip() + self.exec("w(a2b_base64(%s))" % b64) + else: + self.exec("w(" + repr(chunk) + ")") data = data[len(chunk) :] if progress_callback: written += len(chunk)