This is an automated email from the ASF dual-hosted git repository.

acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git

commit d1375b123e0516c02708947a6ef702f39473cea6
Author: Alan Carvalho de Assis <[email protected]>
AuthorDate: Sat Oct 17 17:54:48 2026 -0300

    drivers/wireless: add Morse Micro Wi-Fi HaLow driver
    
    Add a network driver for the Morse Micro MM6108/MM8108 Wi-Fi HaLow
    (IEEE 802.11ah) transceivers connected through SPI, like the Quectel
    FGH100M module.
    
    The 802.11ah UMAC, the WPA3-SAE/OWE supplicant and the chip transport
    are provided by libmorse, the Morse Micro host library also used by the
    FreeRTOS and Zephyr ports. The driver implements the interfaces needed
    by libmorse on NuttX:
    
    - morse_mmosal.c: OS abstraction (kthreads, mutexes, semaphores, queues
      and timers on the work queue).
    - morse_mmhal.c: hardware abstraction (SPI, control GPIOs and IRQs
      through a board lower half, chip firmware and BCF embedded in the
      image).
    - morse_netdev.c: network interface using the netdev lower half and its
      wireless operations (scan, essid, psk, country, sense, ...), so it can
      be controlled with wapi.
    
    Only the needed MM-IoT-SDK files (about 3 MB: headers, the pre-built
    libmorse.a, the chip firmware and two Apache-2.0 helper sources) are
    downloaded at build time. libmorse is merged into libdrivers.a with
    PREBUILT_LIBS (Make) or added with nuttx_add_extra_library() (CMake).
    The Quectel FGH100M-H BCF is downloaded from the morse-firmware
    repository and converted to mbin.
    
    libmorse is built against newlib: the driver selects ARCH_SETJMP_H and
    LIBC_CTYPE_TABLE. WPA3-SAE needs a random device.
    
    The WAKE and BUSY lines are optional: without them the chip is kept
    always awake, as required by the Seeed Studio XIAO HaLow module where
    these signals are not connected.
    
    Tested with a WeAct STM32H743 and a Quectel FGH100M-H: scan, WPA3-SAE
    association, DHCP and ping.
    
    Signed-off-by: Alan Carvalho de Assis <[email protected]>
    Assisted-by: Claude Code Opus 5.5
---
 drivers/wireless/ieee80211/Kconfig              |    1 +
 drivers/wireless/ieee80211/Make.defs            |    1 +
 drivers/wireless/ieee80211/morse/.gitignore     |    3 +
 drivers/wireless/ieee80211/morse/CMakeLists.txt |  174 ++++
 drivers/wireless/ieee80211/morse/Kconfig        |  157 +++
 drivers/wireless/ieee80211/morse/Make.defs      |  160 +++
 drivers/wireless/ieee80211/morse/mmport.h       |   43 +
 drivers/wireless/ieee80211/morse/morse.h        |   70 ++
 drivers/wireless/ieee80211/morse/morse_mmhal.c  |  433 ++++++++
 drivers/wireless/ieee80211/morse/morse_mmosal.c |  706 ++++++++++++++
 drivers/wireless/ieee80211/morse/morse_netdev.c | 1194 +++++++++++++++++++++++
 include/nuttx/wireless/morse.h                  |  153 +++
 12 files changed, 3095 insertions(+)

diff --git a/drivers/wireless/ieee80211/Kconfig 
b/drivers/wireless/ieee80211/Kconfig
index 7a8411fa260..484314768a9 100644
--- a/drivers/wireless/ieee80211/Kconfig
+++ b/drivers/wireless/ieee80211/Kconfig
@@ -6,5 +6,6 @@
 if DRIVERS_IEEE80211
 
 source "drivers/wireless/ieee80211/bcm43xxx/Kconfig"
+source "drivers/wireless/ieee80211/morse/Kconfig"
 
 endif # DRIVERS_IEEE80211
diff --git a/drivers/wireless/ieee80211/Make.defs 
b/drivers/wireless/ieee80211/Make.defs
index f13ddc03be6..da2b4d3b933 100644
--- a/drivers/wireless/ieee80211/Make.defs
+++ b/drivers/wireless/ieee80211/Make.defs
@@ -29,5 +29,6 @@ ifeq ($(CONFIG_DRIVERS_IEEE80211),y)
 # Include IEEE 802.11 drivers into the build
 
 include wireless/ieee80211/bcm43xxx/Make.defs
+include wireless/ieee80211/morse/Make.defs
 
 endif # CONFIG_DRIVERS_IEEE80211
diff --git a/drivers/wireless/ieee80211/morse/.gitignore 
b/drivers/wireless/ieee80211/morse/.gitignore
new file mode 100644
index 00000000000..fa8a8f70dbb
--- /dev/null
+++ b/drivers/wireless/ieee80211/morse/.gitignore
@@ -0,0 +1,3 @@
+/mm-iot-sdk-*
+/bcf_*.bin
+/bcf_*.mbin
diff --git a/drivers/wireless/ieee80211/morse/CMakeLists.txt 
b/drivers/wireless/ieee80211/morse/CMakeLists.txt
new file mode 100644
index 00000000000..fa413cdf633
--- /dev/null
+++ b/drivers/wireless/ieee80211/morse/CMakeLists.txt
@@ -0,0 +1,174 @@
+# 
##############################################################################
+# drivers/wireless/ieee80211/morse/CMakeLists.txt
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more 
contributor
+# license agreements.  See the NOTICE file distributed with this work for
+# additional information regarding copyright ownership.  The ASF licenses this
+# file to you under the Apache License, Version 2.0 (the "License"); you may 
not
+# use this file except in compliance with the License.  You may obtain a copy 
of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+# License for the specific language governing permissions and limitations under
+# the License.
+#
+# 
##############################################################################
+
+if(CONFIG_IEEE80211_MORSE)
+
+  if(CONFIG_ARCH_CORTEXM7)
+    set(MORSE_CORE arm-cortex-m7f)
+  elseif(CONFIG_ARCH_CORTEXM33)
+    set(MORSE_CORE arm-cortex-m33f)
+  else()
+    set(MORSE_CORE arm-cortex-m4f)
+  endif()
+
+  if(CONFIG_IEEE80211_MORSE_MM8108)
+    set(MORSE_CHIP mm8108)
+    set(MORSE_FW_NAME mm8108b2-rl.mbin)
+  else()
+    set(MORSE_CHIP mm6108)
+    set(MORSE_FW_NAME mm6108.mbin)
+  endif()
+
+  # MM-IoT-SDK files: only the files used by the driver are downloaded from the
+  # SDK repository (about 3 MB), see Make.defs.
+
+  set(MORSE_SDK_HEADERS
+      emmet.h
+      mbin.h
+      mm_mbedtls_config.h
+      mmhal.h
+      mmhal_app.h
+      mmhal_core.h
+      mmhal_flash.h
+      mmhal_os.h
+      mmhal_uart.h
+      mmhal_wlan.h
+      mmlog.h
+      mmosal.h
+      mmpkt.h
+      mmpkt_list.h
+      mmtrace.h
+      mmversion.h
+      mmwlan.h
+      mmwlan_stats.h)
+
+  list(TRANSFORM MORSE_SDK_HEADERS PREPEND framework/morselib/include/
+                                           OUTPUT_VARIABLE MORSE_SDK_FILES)
+  list(
+    APPEND
+    MORSE_SDK_FILES
+    framework/morselib/lib/${MORSE_CORE}/libmorse.a
+    framework/src/mmregdb/mmregdb.c
+    framework/src/mmregdb/mmregdb.h
+    framework/src/mmpktmem/mmpktmem_heap.c
+    framework/src/mmutils/mmutils.h
+    framework/morsefirmware/${MORSE_FW_NAME}
+    LICENSES/Apache-2.0.txt
+    LICENSES/LicenseRef-MorseMicroBDL.txt)
+
+  if(CONFIG_IEEE80211_MORSE_BCF_QUECTEL_FGH100MH)
+    list(APPEND MORSE_SDK_FILES
+         framework/tools/buildsystem/convert-bin-to-mbin.py)
+  elseif(CONFIG_IEEE80211_MORSE_BCF_SDK)
+    list(
+      APPEND
+      MORSE_SDK_FILES
+      
framework/morsefirmware/${MORSE_CHIP}/bcfs/${CONFIG_IEEE80211_MORSE_BCF_NAME}
+    )
+  endif()
+
+  # Files already present are not downloaded again: to build without network
+  # access copy them in MORSE_SDK beforehand.
+
+  set(MORSE_SDK
+      
${CMAKE_CURRENT_LIST_DIR}/mm-iot-sdk-${CONFIG_IEEE80211_MORSE_SDK_VERSION}
+  )
+  set(MORSE_SDK_URL
+      
https://raw.githubusercontent.com/MorseMicro/mm-iot-sdk/${CONFIG_IEEE80211_MORSE_SDK_VERSION}
+  )
+  foreach(file ${MORSE_SDK_FILES})
+    if(NOT EXISTS ${MORSE_SDK}/${file})
+      message(STATUS "Downloading: MM-IoT-SDK ${file}")
+      file(DOWNLOAD ${MORSE_SDK_URL}/${file} ${MORSE_SDK}/${file}.tmp
+           STATUS MORSE_DL_STATUS)
+      list(GET MORSE_DL_STATUS 0 MORSE_DL_ERROR)
+      if(MORSE_DL_ERROR)
+        file(REMOVE ${MORSE_SDK}/${file}.tmp)
+        message(FATAL_ERROR "Failed to download ${MORSE_SDK_URL}/${file}")
+      endif()
+      file(RENAME ${MORSE_SDK}/${file}.tmp ${MORSE_SDK}/${file})
+    endif()
+  endforeach()
+
+  set(MORSE_FRAMEWORK ${MORSE_SDK}/framework)
+  set(MORSE_FW ${MORSE_FRAMEWORK}/morsefirmware/${MORSE_FW_NAME})
+
+  # Board configuration file
+
+  if(CONFIG_IEEE80211_MORSE_BCF_QUECTEL_FGH100MH)
+    set(MORSE_BCF_COMMIT 919f27a6dc2d46a988682d6ff64dd3fdb9ffc465)
+    set(MORSE_BCF_ELF ${CMAKE_CURRENT_LIST_DIR}/bcf_fgh100mhaamd.bin)
+    set(MORSE_BCF ${CMAKE_CURRENT_LIST_DIR}/bcf_fgh100mhaamd.mbin)
+    if(NOT EXISTS ${MORSE_BCF})
+      file(
+        DOWNLOAD
+        
https://raw.githubusercontent.com/MorseMicro/morse-firmware/${MORSE_BCF_COMMIT}/bcf/quectel/bcf_fgh100mhaamd.bin
+        ${MORSE_BCF_ELF}
+        STATUS MORSE_BCF_STATUS)
+      list(GET MORSE_BCF_STATUS 0 MORSE_DL_ERROR)
+      if(MORSE_DL_ERROR)
+        message(FATAL_ERROR "Failed to download the FGH100M-H BCF")
+      endif()
+
+      # The BCF is distributed in ELF format: convert it to mbin (requires
+      # python3 with pyelftools)
+
+      execute_process(
+        COMMAND
+          python3 -I
+          ${MORSE_FRAMEWORK}/tools/buildsystem/convert-bin-to-mbin.py -o
+          ${MORSE_BCF} ${MORSE_BCF_ELF} COMMAND_ERROR_IS_FATAL ANY)
+    endif()
+  elseif(CONFIG_IEEE80211_MORSE_BCF_SDK)
+    set(MORSE_BCF
+        
${MORSE_FRAMEWORK}/morsefirmware/${MORSE_CHIP}/bcfs/${CONFIG_IEEE80211_MORSE_BCF_NAME}
+    )
+  else()
+    set(MORSE_BCF ${CONFIG_IEEE80211_MORSE_BCF_PATH})
+  endif()
+
+  # Pre-built libmorse
+
+  nuttx_add_extra_library(
+    ${MORSE_FRAMEWORK}/morselib/lib/${MORSE_CORE}/libmorse.a)
+
+  set(SRCS
+      morse_mmosal.c morse_mmhal.c morse_netdev.c
+      ${MORSE_FRAMEWORK}/src/mmregdb/mmregdb.c
+      ${MORSE_FRAMEWORK}/src/mmpktmem/mmpktmem_heap.c)
+
+  set_property(
+    SOURCE morse_mmhal.c
+    APPEND
+    PROPERTY OBJECT_DEPENDS ${MORSE_FW} ${MORSE_BCF})
+
+  target_compile_definitions(drivers PRIVATE MORSE_FW_FILE=${MORSE_FW}
+                                             MORSE_BCF_FILE=${MORSE_BCF})
+
+  target_include_directories(
+    drivers
+    PRIVATE ${CMAKE_CURRENT_LIST_DIR} ${MORSE_FRAMEWORK}/morselib/include
+            ${MORSE_FRAMEWORK}/src/mmregdb ${MORSE_FRAMEWORK}/src/mmutils)
+
+  target_sources(drivers PRIVATE ${SRCS})
+
+endif()
diff --git a/drivers/wireless/ieee80211/morse/Kconfig 
b/drivers/wireless/ieee80211/morse/Kconfig
new file mode 100644
index 00000000000..f081f03da2d
--- /dev/null
+++ b/drivers/wireless/ieee80211/morse/Kconfig
@@ -0,0 +1,157 @@
+#
+# For a description of the syntax of this configuration file,
+# see the file kconfig-language.txt in the NuttX tools repository.
+#
+
+config IEEE80211_MORSE
+       bool "Morse Micro Wi-Fi HaLow (IEEE 802.11ah) support"
+       default n
+       depends on SPI && NET && NETDEV_WIRELESS_HANDLER
+       depends on ARCH_CORTEXM4 || ARCH_CORTEXM7 || ARCH_CORTEXM33
+       depends on ARCH_FPU
+       depends on !LIBC_LOCALTIME
+       depends on ARCH_HAVE_SETJMP
+       depends on DEV_RANDOM || DEV_URANDOM
+       select ARCH_SETJMP_H
+       select LIBC_CTYPE_TABLE
+       select SCHED_WORKQUEUE
+       select SCHED_HPWORK
+       ---help---
+               Driver for the Morse Micro MM6108/MM8108 Wi-Fi HaLow 
transceivers
+               (e.g. the Quectel FGH100M module) connected through SPI.
+
+               The 802.11ah MAC, the WPA3-SAE/OWE supplicant and the 
transceiver
+               transport are provided by the Morse Micro host library 
(libmorse,
+               part of the MM-IoT-SDK).  The needed SDK files (about 3 MB) are
+               downloaded at build time and libmorse is linked as a pre-built
+               library distributed under the Morse Micro Binary Distribution
+               License.  The chip firmware and the board configuration file 
(BCF)
+               are embedded in the NuttX image.
+
+               See Documentation/os/drivers/special/wireless/morse.rst.
+
+               libmorse is built against newlib: it needs setjmp() and the
+               newlib compatible _ctype_ table.  The NuttX localtime support
+               (LIBC_LOCALTIME) is not compatible because its mktime() writes 
the
+               NuttX specific tm_gmtoff/tm_zone fields of struct tm.
+
+               WPA3-SAE needs a good entropy source: a random device
+               (DEV_RANDOM, ideally backed by a hardware RNG, or DEV_URANDOM) 
is
+               required.
+
+if IEEE80211_MORSE
+
+choice
+       prompt "Transceiver"
+       default IEEE80211_MORSE_MM6108
+
+config IEEE80211_MORSE_MM6108
+       bool "MM6108"
+
+config IEEE80211_MORSE_MM8108
+       bool "MM8108"
+
+endchoice # Transceiver
+
+config IEEE80211_MORSE_SDK_VERSION
+       string "MM-IoT-SDK version"
+       default "2.13.1"
+       ---help---
+               Version (git tag) of the Morse Micro MM-IoT-SDK the files are
+               downloaded from (https://github.com/MorseMicro/mm-iot-sdk).  The
+               driver is tested with the default version.
+
+choice
+       prompt "Board configuration file (BCF)"
+       default IEEE80211_MORSE_BCF_QUECTEL_FGH100MH
+
+config IEEE80211_MORSE_BCF_QUECTEL_FGH100MH
+       bool "Quectel FGH100M-H (FGH100MHAAMD)"
+       depends on IEEE80211_MORSE_MM6108
+       ---help---
+               BCF of the Quectel FGH100M-H module (used e.g. on the Seeed 
Studio
+               Wi-Fi HaLow module for XIAO).  It is downloaded from the
+               MorseMicro/morse-firmware repository and converted to the mbin
+               format with the SDK tool (requires python3 with pyelftools).
+
+config IEEE80211_MORSE_BCF_SDK
+       bool "BCF from the SDK"
+       ---help---
+               Use one of the BCF files distributed with the SDK, in
+               framework/morsefirmware/<chip>/bcfs.
+
+config IEEE80211_MORSE_BCF_CUSTOM
+       bool "Custom BCF file"
+
+endchoice # BCF
+
+config IEEE80211_MORSE_BCF_NAME
+       string "SDK BCF file name"
+       default "bcf_mf16858.mbin"
+       depends on IEEE80211_MORSE_BCF_SDK
+
+config IEEE80211_MORSE_BCF_PATH
+       string "Custom BCF file path"
+       default ""
+       depends on IEEE80211_MORSE_BCF_CUSTOM
+       ---help---
+               Absolute path of the BCF file, in mbin format.
+
+config IEEE80211_MORSE_COUNTRY
+       string "Default country code"
+       default "US"
+       ---help---
+               Two letter country code selecting the regulatory domain (channel
+               list and transmit power limits).  It can be changed at run time
+               while disconnected, e.g. "wapi country wlan0 AU".
+
+config IEEE80211_MORSE_SPI_FREQUENCY
+       int "SPI frequency (Hz)"
+       default 20000000
+       ---help---
+               Default SPI clock frequency, used when the board lower half does
+               not provide one.  The SPI driver selects the closest frequency
+               that is not higher.
+
+config IEEE80211_MORSE_TASK_PRIORITY
+       int "Driver task priority"
+       default 110
+       ---help---
+               Priority of the libmorse "normal" priority tasks and of the 
network
+               RX thread.  The other libmorse task priorities are spread around
+               this value.
+
+config IEEE80211_MORSE_CONNECT_TIMEOUT
+       int "Connection timeout (s)"
+       default 30
+       ---help---
+               Maximum time the connect request (e.g. "wapi essid") waits for 
the
+               link to come up.  On timeout the driver keeps on trying in the
+               background.
+
+config IEEE80211_MORSE_TX_TIMEOUT
+       int "TX flow control timeout (ms)"
+       default 100
+       ---help---
+               Maximum time to wait for the transceiver TX queues before 
dropping
+               a packet.
+
+config IEEE80211_MORSE_TX_QUOTA
+       int "TX packet quota"
+       default 8
+
+config IEEE80211_MORSE_RX_QUOTA
+       int "RX packet quota"
+       default 8
+
+config IEEE80211_MORSE_SCAN_BUFSIZE
+       int "Scan result buffer size"
+       default 4096
+
+config IEEE80211_MORSE_LIBLOG
+       bool "Enable libmorse log output"
+       default n
+       ---help---
+               Route the libmorse log messages to the syslog.
+
+endif # IEEE80211_MORSE
diff --git a/drivers/wireless/ieee80211/morse/Make.defs 
b/drivers/wireless/ieee80211/morse/Make.defs
new file mode 100644
index 00000000000..344cc33020f
--- /dev/null
+++ b/drivers/wireless/ieee80211/morse/Make.defs
@@ -0,0 +1,160 @@
+############################################################################
+# drivers/wireless/ieee80211/morse/Make.defs
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+############################################################################
+
+ifeq ($(CONFIG_IEEE80211_MORSE),y)
+
+CSRCS += morse_mmosal.c morse_mmhal.c morse_netdev.c
+
+# Location of the MM-IoT-SDK files, downloaded in the driver directory.
+# MORSE_SDK_REL is the same location relative to the drivers/ directory,
+# used to build the SDK sources (VPATH).
+
+MORSE_SDK_VERSION := $(patsubst "%",%,$(strip 
$(CONFIG_IEEE80211_MORSE_SDK_VERSION)))
+
+MORSE_DIR       = 
$(TOPDIR)$(DELIM)drivers$(DELIM)wireless$(DELIM)ieee80211$(DELIM)morse
+MORSE_SDK       = $(MORSE_DIR)$(DELIM)mm-iot-sdk-$(MORSE_SDK_VERSION)
+MORSE_SDK_REL   = 
wireless$(DELIM)ieee80211$(DELIM)morse$(DELIM)mm-iot-sdk-$(MORSE_SDK_VERSION)
+MORSE_FRAMEWORK = $(MORSE_SDK)$(DELIM)framework
+
+ifeq ($(CONFIG_ARCH_CORTEXM7),y)
+  MORSE_CORE = arm-cortex-m7f
+else ifeq ($(CONFIG_ARCH_CORTEXM33),y)
+  MORSE_CORE = arm-cortex-m33f
+else
+  MORSE_CORE = arm-cortex-m4f
+endif
+
+# Apache-2.0 helpers from the SDK: regulatory database and packet memory
+
+CSRCS += mmregdb.c mmpktmem_heap.c
+
+DEPPATH += --dep-path 
$(MORSE_SDK_REL)$(DELIM)framework$(DELIM)src$(DELIM)mmregdb
+DEPPATH += --dep-path 
$(MORSE_SDK_REL)$(DELIM)framework$(DELIM)src$(DELIM)mmpktmem
+VPATH += :$(MORSE_SDK_REL)$(DELIM)framework$(DELIM)src$(DELIM)mmregdb
+VPATH += :$(MORSE_SDK_REL)$(DELIM)framework$(DELIM)src$(DELIM)mmpktmem
+
+# The pre-built libmorse is merged into libdrivers.a
+
+PREBUILT_LIBS += 
$(MORSE_FRAMEWORK)$(DELIM)morselib$(DELIM)lib$(DELIM)$(MORSE_CORE)$(DELIM)libmorse.a
+
+# MM-IoT-SDK files
+#
+# Only the files used by the driver are downloaded from the SDK repository
+# (about 3 MB): the libmorse headers and pre-built library, the chip
+# firmware, two Apache-2.0 helper sources and the license texts.
+
+MORSE_SDK_URL = 
https://raw.githubusercontent.com/MorseMicro/mm-iot-sdk/$(MORSE_SDK_VERSION)
+
+MORSE_SDK_HEADERS  = emmet.h mbin.h mm_mbedtls_config.h mmhal.h mmhal_app.h
+MORSE_SDK_HEADERS += mmhal_core.h mmhal_flash.h mmhal_os.h mmhal_uart.h
+MORSE_SDK_HEADERS += mmhal_wlan.h mmlog.h mmosal.h mmpkt.h mmpkt_list.h
+MORSE_SDK_HEADERS += mmtrace.h mmversion.h mmwlan.h mmwlan_stats.h
+
+MORSE_SDK_FILES  = $(addprefix 
framework/morselib/include/,$(MORSE_SDK_HEADERS))
+MORSE_SDK_FILES += framework/morselib/lib/$(MORSE_CORE)/libmorse.a
+MORSE_SDK_FILES += framework/src/mmregdb/mmregdb.c
+MORSE_SDK_FILES += framework/src/mmregdb/mmregdb.h
+MORSE_SDK_FILES += framework/src/mmpktmem/mmpktmem_heap.c
+MORSE_SDK_FILES += framework/src/mmutils/mmutils.h
+MORSE_SDK_FILES += LICENSES/Apache-2.0.txt
+MORSE_SDK_FILES += LICENSES/LicenseRef-MorseMicroBDL.txt
+
+# Chip firmware
+
+ifeq ($(CONFIG_IEEE80211_MORSE_MM8108),y)
+  MORSE_CHIP    = mm8108
+  MORSE_FW_NAME = mm8108b2-rl.mbin
+else
+  MORSE_CHIP    = mm6108
+  MORSE_FW_NAME = mm6108.mbin
+endif
+
+MORSE_FW = $(MORSE_FRAMEWORK)$(DELIM)morsefirmware$(DELIM)$(MORSE_FW_NAME)
+MORSE_SDK_FILES += framework/morsefirmware/$(MORSE_FW_NAME)
+
+# Board configuration file
+
+ifeq ($(CONFIG_IEEE80211_MORSE_BCF_QUECTEL_FGH100MH),y)
+
+MORSE_BCF_COMMIT = 919f27a6dc2d46a988682d6ff64dd3fdb9ffc465
+MORSE_BCF_URL    = 
https://raw.githubusercontent.com/MorseMicro/morse-firmware/$(MORSE_BCF_COMMIT)/bcf/quectel
+MORSE_BCF_ELF    = $(MORSE_DIR)$(DELIM)bcf_fgh100mhaamd.bin
+MORSE_BCF        = $(MORSE_DIR)$(DELIM)bcf_fgh100mhaamd.mbin
+
+$(MORSE_BCF_ELF):
+       @echo "Downloading: Quectel FGH100M-H BCF"
+       $(Q) curl -L -f -s -S -o $@ $(MORSE_BCF_URL)/bcf_fgh100mhaamd.bin
+
+# The BCF is distributed in ELF format: convert it to mbin with the SDK
+# tool (requires python3 with pyelftools)
+
+MORSE_BCF_TOOL   = 
$(MORSE_FRAMEWORK)$(DELIM)tools$(DELIM)buildsystem$(DELIM)convert-bin-to-mbin.py
+MORSE_SDK_FILES += framework/tools/buildsystem/convert-bin-to-mbin.py
+
+$(MORSE_BCF): $(MORSE_BCF_ELF) $(MORSE_BCF_TOOL)
+       @echo "Converting: $(notdir $(MORSE_BCF_ELF)) -> $(notdir $@)"
+       $(Q) python3 -c "import elftools" 2>/dev/null || \
+            { echo "ERROR: python3 pyelftools is required (pip install 
pyelftools)"; exit 1; }
+       $(Q) python3 -I $(MORSE_BCF_TOOL) -o $@ $(MORSE_BCF_ELF)
+
+else ifeq ($(CONFIG_IEEE80211_MORSE_BCF_SDK),y)
+  MORSE_BCF_NAME   = $(patsubst "%",%,$(CONFIG_IEEE80211_MORSE_BCF_NAME))
+  MORSE_BCF        = 
$(MORSE_FRAMEWORK)$(DELIM)morsefirmware$(DELIM)$(MORSE_CHIP)$(DELIM)bcfs$(DELIM)$(MORSE_BCF_NAME)
+  MORSE_SDK_FILES += 
framework/morsefirmware/$(MORSE_CHIP)/bcfs/$(MORSE_BCF_NAME)
+else
+  MORSE_BCF = $(patsubst "%",%,$(CONFIG_IEEE80211_MORSE_BCF_PATH))
+endif
+
+# Files already present are not downloaded again: to build without network
+# access copy them in $(MORSE_SDK) beforehand.
+
+$(MORSE_SDK)$(DELIM)%:
+       @echo "Downloading: MM-IoT-SDK $(MORSE_SDK_VERSION) $*"
+       $(Q) curl -L -f -s -S --create-dirs -o [email protected] $(MORSE_SDK_URL)/$*
+       $(Q) mv [email protected] $@
+
+context:: $(addprefix $(MORSE_SDK)$(DELIM),$(MORSE_SDK_FILES))
+
+ifeq ($(CONFIG_IEEE80211_MORSE_BCF_QUECTEL_FGH100MH),y)
+context:: $(MORSE_BCF)
+endif
+
+# The firmware and the BCF are embedded with .incbin
+
+morse_mmhal$(OBJEXT): $(MORSE_FW) $(MORSE_BCF)
+
+CFLAGS += -DMORSE_FW_FILE=$(MORSE_FW) -DMORSE_BCF_FILE=$(MORSE_BCF)
+
+CFLAGS += ${INCDIR_PREFIX}$(MORSE_DIR)
+CFLAGS += ${INCDIR_PREFIX}$(MORSE_FRAMEWORK)$(DELIM)morselib$(DELIM)include
+CFLAGS += ${INCDIR_PREFIX}$(MORSE_FRAMEWORK)$(DELIM)src$(DELIM)mmregdb
+CFLAGS += ${INCDIR_PREFIX}$(MORSE_FRAMEWORK)$(DELIM)src$(DELIM)mmutils
+
+DEPPATH += --dep-path wireless$(DELIM)ieee80211$(DELIM)morse
+VPATH += :wireless$(DELIM)ieee80211$(DELIM)morse
+
+distclean::
+       $(call DELDIR, $(MORSE_DIR)$(DELIM)mm-iot-sdk-$(MORSE_SDK_VERSION))
+       $(call DELFILE, $(MORSE_DIR)$(DELIM)bcf_fgh100mhaamd.bin)
+       $(call DELFILE, $(MORSE_DIR)$(DELIM)bcf_fgh100mhaamd.mbin)
+
+endif # CONFIG_IEEE80211_MORSE
diff --git a/drivers/wireless/ieee80211/morse/mmport.h 
b/drivers/wireless/ieee80211/morse/mmport.h
new file mode 100644
index 00000000000..fd097715efe
--- /dev/null
+++ b/drivers/wireless/ieee80211/morse/mmport.h
@@ -0,0 +1,43 @@
+/****************************************************************************
+ * drivers/wireless/ieee80211/morse/mmport.h
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/* Port definitions required by the Morse Micro mmosal.h header */
+
+#ifndef __DRIVERS_WIRELESS_IEEE80211_MORSE_MMPORT_H
+#define __DRIVERS_WIRELESS_IEEE80211_MORSE_MMPORT_H
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/compiler.h>
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+#define MMPORT_BREAKPOINT()  __asm volatile ("bkpt 0\n\t")
+#define MMPORT_GET_LR()      return_address(0)
+#define MMPORT_GET_PC(_a)    __asm volatile ("mov %0, pc" : "=r" (_a))
+#define MMPORT_MEM_SYNC()    __sync_synchronize()
+
+#endif /* __DRIVERS_WIRELESS_IEEE80211_MORSE_MMPORT_H */
diff --git a/drivers/wireless/ieee80211/morse/morse.h 
b/drivers/wireless/ieee80211/morse/morse.h
new file mode 100644
index 00000000000..7cd923331ba
--- /dev/null
+++ b/drivers/wireless/ieee80211/morse/morse.h
@@ -0,0 +1,70 @@
+/****************************************************************************
+ * drivers/wireless/ieee80211/morse/morse.h
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+#ifndef __DRIVERS_WIRELESS_IEEE80211_MORSE_MORSE_H
+#define __DRIVERS_WIRELESS_IEEE80211_MORSE_MORSE_H
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <nuttx/spi/spi.h>
+#include <nuttx/wireless/morse.h>
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+#ifndef CONFIG_IEEE80211_MORSE_SPI_FREQUENCY
+#  define CONFIG_IEEE80211_MORSE_SPI_FREQUENCY 20000000
+#endif
+
+/****************************************************************************
+ * Public Function Prototypes
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: morse_hal_initialize
+ *
+ * Description:
+ *   Bind the mmhal implementation (used by libmorse) to the SPI bus and to
+ *   the board lower half.  Must be called before mmwlan_init().
+ *
+ ****************************************************************************/
+
+int morse_hal_initialize(FAR struct spi_dev_s *spi,
+                         FAR const struct morse_lower_s *lower);
+
+/****************************************************************************
+ * Name: morse_hal_has_powersave
+ *
+ * Description:
+ *   Return true if the board connects both the WAKE and the BUSY lines,
+ *   which are required to put the chip in power save.
+ *
+ ****************************************************************************/
+
+bool morse_hal_has_powersave(void);
+
+#endif /* __DRIVERS_WIRELESS_IEEE80211_MORSE_MORSE_H */
diff --git a/drivers/wireless/ieee80211/morse/morse_mmhal.c 
b/drivers/wireless/ieee80211/morse/morse_mmhal.c
new file mode 100644
index 00000000000..afab9dc708c
--- /dev/null
+++ b/drivers/wireless/ieee80211/morse/morse_mmhal.c
@@ -0,0 +1,433 @@
+/****************************************************************************
+ * drivers/wireless/ieee80211/morse/morse_mmhal.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/* Implementation of the Morse Micro Hardware Abstraction Layer (mmhal) used
+ * by libmorse: SPI transport, control GPIOs, interrupts and access to the
+ * chip firmware and board configuration file (BCF).
+ */
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <assert.h>
+#include <debug.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/param.h>
+
+#include <nuttx/irq.h>
+#include <nuttx/macro.h>
+#include <nuttx/spi/spi.h>
+#include <nuttx/wireless/morse.h>
+
+#include "mmhal_core.h"
+#include "mmhal_wlan.h"
+#include "mmosal.h"
+#include "morse.h"
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+/* Number of 0xff bytes clocked with CS high to put the chip SPI interface
+ * in a known state (>= 74 clocks, SD specification 6.4.1.1).
+ */
+
+#define MORSE_TRAINING_BYTES  16
+
+/****************************************************************************
+ * Private Types
+ ****************************************************************************/
+
+struct morse_hal_s
+{
+  FAR struct spi_dev_s *spi;
+  FAR const struct morse_lower_s *lower;
+  uint32_t frequency;
+  mmhal_irq_handler_t spi_handler;
+  mmhal_irq_handler_t busy_handler;
+};
+
+/****************************************************************************
+ * Private Data
+ ****************************************************************************/
+
+static struct morse_hal_s g_morse_hal;
+
+/* Chip firmware and BCF embedded in the image (mbin format) */
+
+extern const uint8_t g_morse_fw[];
+extern const uint8_t g_morse_fw_end[];
+extern const uint8_t g_morse_bcf[];
+extern const uint8_t g_morse_bcf_end[];
+
+__asm__(".section .rodata.morse_fw, \"a\", %progbits\n"
+        ".balign 4\n"
+        ".globl g_morse_fw\n"
+        "g_morse_fw:\n"
+        ".incbin \"" STRINGIFY(MORSE_FW_FILE) "\"\n"
+        ".globl g_morse_fw_end\n"
+        "g_morse_fw_end:\n"
+        ".balign 4\n"
+        ".globl g_morse_bcf\n"
+        "g_morse_bcf:\n"
+        ".incbin \"" STRINGIFY(MORSE_BCF_FILE) "\"\n"
+        ".globl g_morse_bcf_end\n"
+        "g_morse_bcf_end:\n"
+        ".previous\n");
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static int morse_spi_isr(int irq, FAR void *context, FAR void *arg)
+{
+  if (g_morse_hal.spi_handler != NULL)
+    {
+      g_morse_hal.spi_handler();
+    }
+
+  return OK;
+}
+
+static int morse_busy_isr(int irq, FAR void *context, FAR void *arg)
+{
+  if (g_morse_hal.busy_handler != NULL)
+    {
+      g_morse_hal.busy_handler();
+    }
+
+  return OK;
+}
+
+static void morse_read_blob(FAR const uint8_t *start,
+                            FAR const uint8_t *end, uint32_t offset,
+                            uint32_t requested_len,
+                            FAR struct mmhal_robuf *robuf)
+{
+  size_t size = end - start;
+
+  robuf->buf      = NULL;
+  robuf->len      = 0;
+  robuf->free_cb  = NULL;
+  robuf->free_arg = NULL;
+
+  if (offset > size)
+    {
+      wlerr("ERROR: read beyond the end of the file (%" PRIu32 ")\n",
+            offset);
+      return;
+    }
+
+  robuf->buf = start + offset;
+  robuf->len = MIN(size - offset, requested_len);
+}
+
+static void morse_spi_configure(FAR struct spi_dev_s *spi)
+{
+  SPI_SETMODE(spi, SPIDEV_MODE0);
+  SPI_SETBITS(spi, 8);
+  SPI_HWFEATURES(spi, 0);
+  SPI_SETFREQUENCY(spi, g_morse_hal.frequency);
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+int morse_hal_initialize(FAR struct spi_dev_s *spi,
+                         FAR const struct morse_lower_s *lower)
+{
+  int ret;
+
+  DEBUGASSERT(spi != NULL && lower != NULL);
+  DEBUGASSERT(lower->reset != NULL && lower->irq_attach != NULL &&
+              lower->irq_enable != NULL && lower->irq_asserted != NULL);
+
+  g_morse_hal.spi       = spi;
+  g_morse_hal.lower     = lower;
+  g_morse_hal.frequency = lower->frequency != 0 ? lower->frequency :
+                          CONFIG_IEEE80211_MORSE_SPI_FREQUENCY;
+
+  wlinfo("FW %zu bytes, BCF %zu bytes, SPI %" PRIu32 " Hz\n",
+         (size_t)(g_morse_fw_end - g_morse_fw),
+         (size_t)(g_morse_bcf_end - g_morse_bcf), g_morse_hal.frequency);
+
+  lower->irq_enable(lower, false);
+  ret = lower->irq_attach(lower, morse_spi_isr, NULL);
+  if (ret < 0)
+    {
+      return ret;
+    }
+
+  if (lower->busy_attach != NULL && lower->busy_enable != NULL)
+    {
+      lower->busy_enable(lower, false);
+      ret = lower->busy_attach(lower, morse_busy_isr, NULL);
+    }
+
+  return ret;
+}
+
+/****************************************************************************
+ * Name: morse_hal_has_powersave
+ *
+ * Description:
+ *   Return true if the board connects both the WAKE and the BUSY lines,
+ *   which are required to put the chip in power save.
+ *
+ ****************************************************************************/
+
+bool morse_hal_has_powersave(void)
+{
+  return g_morse_hal.lower->wake != NULL && g_morse_hal.lower->busy != NULL;
+}
+
+/* Core *********************************************************************/
+
+uint32_t mmhal_random_u32(uint32_t min, uint32_t max)
+{
+  uint32_t value;
+
+  arc4random_buf(&value, sizeof(value));
+
+  if (min == 0 && max == UINT32_MAX)
+    {
+      return value;
+    }
+
+  return min + value % (max - min + 1);
+}
+
+void mmhal_set_deep_sleep_veto(uint8_t veto_id)
+{
+  UNUSED(veto_id);
+}
+
+void mmhal_clear_deep_sleep_veto(uint8_t veto_id)
+{
+  UNUSED(veto_id);
+}
+
+/* WLAN *********************************************************************/
+
+void mmhal_wlan_init(void)
+{
+  mmhal_wlan_assert_reset(false);
+}
+
+void mmhal_wlan_deinit(void)
+{
+  mmhal_wlan_assert_reset(true);
+}
+
+const struct mmhal_chip *mmhal_get_chip(void)
+{
+#ifdef CONFIG_IEEE80211_MORSE_MM8108
+  return &mmhal_mm8108;
+#else
+  return &mmhal_mm6108;
+#endif
+}
+
+void mmhal_read_mac_addr(FAR uint8_t *mac_addr)
+{
+  FAR const struct morse_lower_s *lower = g_morse_hal.lower;
+  uint8_t mac[MMWLAN_MAC_ADDR_LEN];
+
+  /* A board provided address takes precedence over the chip OTP one.  If
+   * neither is valid the address is left zeroed and libmorse generates a
+   * random one.
+   */
+
+  if (lower->getmac != NULL && lower->getmac(lower, mac))
+    {
+      memcpy(mac_addr, mac, sizeof(mac));
+    }
+}
+
+void mmhal_wlan_hard_reset(void)
+{
+  mmhal_wlan_assert_reset(true);
+  mmosal_task_sleep(5);
+  mmhal_wlan_assert_reset(false);
+  mmosal_task_sleep(20);
+}
+
+void mmhal_wlan_assert_reset(bool assert_reset)
+{
+  g_morse_hal.lower->reset(g_morse_hal.lower, assert_reset);
+}
+
+void mmhal_wlan_wake_assert(void)
+{
+  if (g_morse_hal.lower->wake != NULL)
+    {
+      g_morse_hal.lower->wake(g_morse_hal.lower, true);
+    }
+}
+
+void mmhal_wlan_wake_deassert(void)
+{
+  if (g_morse_hal.lower->wake != NULL)
+    {
+      g_morse_hal.lower->wake(g_morse_hal.lower, false);
+    }
+}
+
+bool mmhal_wlan_busy_is_asserted(void)
+{
+  /* Without a BUSY line the chip cannot tell when it wants to be woken
+   * up.  Reporting it as always busy prevents libmorse from suspending the
+   * host interface (see morse_hal_has_powersave()).
+   */
+
+  if (g_morse_hal.lower->busy == NULL)
+    {
+      return true;
+    }
+
+  return g_morse_hal.lower->busy(g_morse_hal.lower);
+}
+
+void mmhal_wlan_register_busy_irq_handler(mmhal_irq_handler_t handler)
+{
+  g_morse_hal.busy_handler = handler;
+}
+
+void mmhal_wlan_set_busy_irq_enabled(bool enabled)
+{
+  if (g_morse_hal.lower->busy_enable != NULL)
+    {
+      g_morse_hal.lower->busy_enable(g_morse_hal.lower, enabled);
+    }
+}
+
+void mmhal_wlan_register_spi_irq_handler(mmhal_irq_handler_t handler)
+{
+  g_morse_hal.spi_handler = handler;
+}
+
+bool mmhal_wlan_spi_irq_is_asserted(void)
+{
+  return g_morse_hal.lower->irq_asserted(g_morse_hal.lower);
+}
+
+void mmhal_wlan_set_spi_irq_enabled(bool enabled)
+{
+  FAR const struct morse_lower_s *lower = g_morse_hal.lower;
+
+  if (enabled)
+    {
+      /* The chip holds the IRQ line low while it has pending events.  The
+       * interrupt is edge triggered, so check the line level to not miss
+       * an event that was signaled while the interrupt was disabled.
+       */
+
+      if (lower->irq_asserted(lower) && g_morse_hal.spi_handler != NULL)
+        {
+          g_morse_hal.spi_handler();
+        }
+      else
+        {
+          lower->irq_enable(lower, true);
+        }
+    }
+  else
+    {
+      lower->irq_enable(lower, false);
+    }
+}
+
+void mmhal_wlan_read_bcf_file(uint32_t offset, uint32_t requested_len,
+                              FAR struct mmhal_robuf *robuf)
+{
+  morse_read_blob(g_morse_bcf, g_morse_bcf_end, offset, requested_len,
+                  robuf);
+}
+
+void mmhal_wlan_read_fw_file(uint32_t offset, uint32_t requested_len,
+                             FAR struct mmhal_robuf *robuf)
+{
+  morse_read_blob(g_morse_fw, g_morse_fw_end, offset, requested_len,
+                  robuf);
+}
+
+/* SPI **********************************************************************/
+
+void mmhal_wlan_spi_cs_assert(void)
+{
+  FAR struct spi_dev_s *spi = g_morse_hal.spi;
+
+  SPI_LOCK(spi, true);
+  morse_spi_configure(spi);
+  SPI_SELECT(spi, g_morse_hal.lower->devid, true);
+}
+
+void mmhal_wlan_spi_cs_deassert(void)
+{
+  FAR struct spi_dev_s *spi = g_morse_hal.spi;
+
+  SPI_SELECT(spi, g_morse_hal.lower->devid, false);
+  SPI_LOCK(spi, false);
+}
+
+uint8_t mmhal_wlan_spi_rw(uint8_t data)
+{
+  return (uint8_t)SPI_SEND(g_morse_hal.spi, data);
+}
+
+void mmhal_wlan_spi_read_buf(FAR uint8_t *buf, unsigned len)
+{
+  /* The SPI driver clocks out 0xff when receiving */
+
+  SPI_RECVBLOCK(g_morse_hal.spi, buf, len);
+}
+
+void mmhal_wlan_spi_write_buf(FAR const uint8_t *buf, unsigned len)
+{
+  SPI_SNDBLOCK(g_morse_hal.spi, buf, len);
+}
+
+void mmhal_wlan_send_training_seq(void)
+{
+  FAR struct spi_dev_s *spi = g_morse_hal.spi;
+  int i;
+
+  /* Clock the bus with CS de-asserted and MOSI high */
+
+  SPI_LOCK(spi, true);
+  morse_spi_configure(spi);
+  SPI_SELECT(spi, g_morse_hal.lower->devid, false);
+
+  for (i = 0; i < MORSE_TRAINING_BYTES; i++)
+    {
+      SPI_SEND(spi, 0xff);
+    }
+
+  SPI_LOCK(spi, false);
+}
diff --git a/drivers/wireless/ieee80211/morse/morse_mmosal.c 
b/drivers/wireless/ieee80211/morse/morse_mmosal.c
new file mode 100644
index 00000000000..57fbfeb7cc8
--- /dev/null
+++ b/drivers/wireless/ieee80211/morse/morse_mmosal.c
@@ -0,0 +1,706 @@
+/****************************************************************************
+ * drivers/wireless/ieee80211/morse/morse_mmosal.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/* Implementation of the Morse Micro Operating System Abstraction Layer
+ * (mmosal) on top of the NuttX kernel services.  This is the OS interface
+ * used by the Morse Micro host library (libmorse).
+ */
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <assert.h>
+#include <debug.h>
+#include <sched.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+
+#include <nuttx/arch.h>
+#include <nuttx/clock.h>
+#include <nuttx/irq.h>
+#include <nuttx/kmalloc.h>
+#include <nuttx/kthread.h>
+#include <nuttx/mutex.h>
+#include <nuttx/sched.h>
+#include <nuttx/semaphore.h>
+#include <nuttx/wqueue.h>
+
+#include "mmosal.h"
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+/* Timers run on the low priority work queue if available */
+
+#ifdef CONFIG_SCHED_LPWORK
+#  define MORSE_TIMER_WORK  LPWORK
+#else
+#  define MORSE_TIMER_WORK  HPWORK
+#endif
+
+/* Extra stack added to every mmosal task: the stack sizes requested by
+ * libmorse are tuned for FreeRTOS and do not account for the NuttX
+ * syslog/exception overhead.
+ */
+
+#define MORSE_TASK_STACK_EXTRA  2048
+
+#define MORSE_TASK_PRIO_STEP    10
+
+/****************************************************************************
+ * Private Types
+ ****************************************************************************/
+
+struct mmosal_task
+{
+  mmosal_task_fn_t fn;
+  FAR void *arg;
+};
+
+struct mmosal_mutex
+{
+  mutex_t lock;
+};
+
+struct mmosal_sem
+{
+  sem_t sem;
+  unsigned int max;
+};
+
+struct mmosal_semb
+{
+  sem_t sem;
+};
+
+struct mmosal_queue
+{
+  sem_t items;              /* Number of items in the queue */
+  sem_t spaces;             /* Number of free slots */
+  size_t itemsize;
+  size_t nitems;
+  size_t head;
+  size_t tail;
+  uint8_t buf[1];
+};
+
+struct mmosal_timer
+{
+  struct work_s work;
+  timer_callback_t callback;
+  FAR void *arg;
+  uint32_t period;          /* Timer period in ms */
+  bool reload;              /* Auto reload */
+  volatile bool active;
+};
+
+/****************************************************************************
+ * Private Data
+ ****************************************************************************/
+
+static irqstate_t g_critical_flags;
+static int g_critical_nest;
+static irqstate_t g_irq_flags;
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static uint32_t ms2tick(uint32_t ms)
+{
+  return (uint32_t)MSEC2TICK((clock_t)ms);
+}
+
+/* Wait on a NuttX semaphore with an mmosal style timeout.  Returns true if
+ * the semaphore was taken.
+ */
+
+static bool sem_wait_ms(FAR sem_t *sem, uint32_t timeout_ms)
+{
+  int ret;
+
+  if (timeout_ms == UINT32_MAX)
+    {
+      ret = nxsem_wait_uninterruptible(sem);
+    }
+  else if (timeout_ms == 0)
+    {
+      ret = nxsem_trywait(sem);
+    }
+  else
+    {
+      ret = nxsem_tickwait_uninterruptible(sem, ms2tick(timeout_ms));
+    }
+
+  return ret >= 0;
+}
+
+/* Post a semaphore only if its count is below 'max'.  Safe to be called
+ * from interrupt context.
+ */
+
+static bool sem_post_max(FAR sem_t *sem, int max)
+{
+  irqstate_t flags;
+  bool ret = false;
+  int value;
+
+  flags = enter_critical_section();
+  if (nxsem_get_value(sem, &value) >= 0 && value < max)
+    {
+      ret = nxsem_post(sem) >= 0;
+    }
+
+  leave_critical_section(flags);
+  return ret;
+}
+
+static int morse_task_main(int argc, FAR char *argv[])
+{
+  FAR struct mmosal_task *task;
+
+  DEBUGASSERT(argc == 2);
+  task = (FAR struct mmosal_task *)((uintptr_t)strtoul(argv[1], NULL, 16));
+
+  task->fn(task->arg);
+
+  kmm_free(task);
+  return 0;
+}
+
+static void morse_timer_worker(FAR void *arg)
+{
+  FAR struct mmosal_timer *timer = arg;
+
+  if (!timer->active)
+    {
+      return;
+    }
+
+  if (timer->reload)
+    {
+      work_queue(MORSE_TIMER_WORK, &timer->work, morse_timer_worker, timer,
+                 ms2tick(timer->period));
+    }
+  else
+    {
+      timer->active = false;
+    }
+
+  timer->callback(timer);
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/* Memory management ********************************************************/
+
+FAR void *mmosal_malloc_(size_t size)
+{
+  return kmm_malloc(size);
+}
+
+FAR void *mmosal_calloc_(size_t nitems, size_t size)
+{
+  return kmm_calloc(nitems, size);
+}
+
+FAR void *mmosal_realloc_(FAR void *ptr, size_t size)
+{
+  return kmm_realloc(ptr, size);
+}
+
+void mmosal_free(FAR void *p)
+{
+  kmm_free(p);
+}
+
+FAR void *mmosal_malloc_dbg(size_t size, FAR const char *name,
+                            unsigned line_number)
+{
+  return kmm_malloc(size);
+}
+
+FAR void *mmosal_calloc_dbg(size_t nitems, size_t size,
+                            FAR const char *name, unsigned line_number)
+{
+  return kmm_calloc(nitems, size);
+}
+
+FAR void *mmosal_realloc_dbg(FAR void *ptr, size_t size,
+                             FAR const char *name, unsigned line_number)
+{
+  return kmm_realloc(ptr, size);
+}
+
+/* Tasks ********************************************************************/
+
+FAR struct mmosal_task *
+mmosal_task_create(mmosal_task_fn_t task_fn, FAR void *argument,
+                   enum mmosal_task_priority priority,
+                   unsigned stack_size_u32, FAR const char *name)
+{
+  FAR struct mmosal_task *task;
+  FAR char *argv[2];
+  char arg[2 + 2 * sizeof(uintptr_t) + 1];
+  int prio;
+  int pid;
+
+  task = kmm_malloc(sizeof(*task));
+  if (task == NULL)
+    {
+      return NULL;
+    }
+
+  task->fn  = task_fn;
+  task->arg = argument;
+
+  /* Map the mmosal priorities around the configured base priority */
+
+  prio = CONFIG_IEEE80211_MORSE_TASK_PRIORITY +
+         ((int)priority - (int)MMOSAL_TASK_PRI_NORM) * MORSE_TASK_PRIO_STEP;
+  if (prio < SCHED_PRIORITY_MIN + 1)
+    {
+      prio = SCHED_PRIORITY_MIN + 1;
+    }
+  else if (prio > SCHED_PRIORITY_MAX)
+    {
+      prio = SCHED_PRIORITY_MAX;
+    }
+
+  snprintf(arg, sizeof(arg), "%" PRIxPTR, (uintptr_t)task);
+  argv[0] = arg;
+  argv[1] = NULL;
+
+  pid = kthread_create(name, prio,
+                       stack_size_u32 * sizeof(uint32_t) +
+                       MORSE_TASK_STACK_EXTRA,
+                       morse_task_main, argv);
+  if (pid < 0)
+    {
+      wlerr("ERROR: failed to create task %s: %d\n", name, pid);
+      kmm_free(task);
+      return NULL;
+    }
+
+  /* The task handle is the PID of the kernel thread */
+
+  return (FAR struct mmosal_task *)(uintptr_t)pid;
+}
+
+void mmosal_task_delete(FAR struct mmosal_task *task)
+{
+  kthread_delete((pid_t)(uintptr_t)task);
+}
+
+FAR struct mmosal_task *mmosal_task_get_active(void)
+{
+  return (FAR struct mmosal_task *)(uintptr_t)nxsched_gettid();
+}
+
+void mmosal_task_yield(void)
+{
+  sched_yield();
+}
+
+void mmosal_task_sleep(uint32_t duration_ms)
+{
+  nxsched_usleep((useconds_t)duration_ms * USEC_PER_MSEC);
+}
+
+void mmosal_task_enter_critical(void)
+{
+  irqstate_t flags = enter_critical_section();
+
+  if (g_critical_nest++ == 0)
+    {
+      g_critical_flags = flags;
+    }
+}
+
+void mmosal_task_exit_critical(void)
+{
+  DEBUGASSERT(g_critical_nest > 0);
+
+  if (--g_critical_nest == 0)
+    {
+      leave_critical_section(g_critical_flags);
+    }
+}
+
+void mmosal_disable_interrupts(void)
+{
+  g_irq_flags = up_irq_save();
+}
+
+void mmosal_enable_interrupts(void)
+{
+  up_irq_restore(g_irq_flags);
+}
+
+FAR const char *mmosal_task_name(void)
+{
+#if CONFIG_TASK_NAME_SIZE > 0
+  return nxsched_self()->name;
+#else
+  return "";
+#endif
+}
+
+/* Mutexes ******************************************************************/
+
+FAR struct mmosal_mutex *mmosal_mutex_create(FAR const char *name)
+{
+  FAR struct mmosal_mutex *mutex = kmm_malloc(sizeof(*mutex));
+
+  if (mutex != NULL)
+    {
+      nxmutex_init(&mutex->lock);
+    }
+
+  return mutex;
+}
+
+void mmosal_mutex_delete(FAR struct mmosal_mutex *mutex)
+{
+  if (mutex != NULL)
+    {
+      nxmutex_destroy(&mutex->lock);
+      kmm_free(mutex);
+    }
+}
+
+bool mmosal_mutex_get(FAR struct mmosal_mutex *mutex, uint32_t timeout_ms)
+{
+  if (timeout_ms == UINT32_MAX)
+    {
+      return nxmutex_lock(&mutex->lock) >= 0;
+    }
+  else if (timeout_ms == 0)
+    {
+      return nxmutex_trylock(&mutex->lock) >= 0;
+    }
+
+  return nxmutex_timedlock(&mutex->lock, timeout_ms) >= 0;
+}
+
+bool mmosal_mutex_release(FAR struct mmosal_mutex *mutex)
+{
+  return nxmutex_unlock(&mutex->lock) >= 0;
+}
+
+bool mmosal_mutex_is_held_by_active_task(FAR struct mmosal_mutex *mutex)
+{
+  return nxmutex_is_hold(&mutex->lock);
+}
+
+/* Counting semaphores ******************************************************/
+
+FAR struct mmosal_sem *mmosal_sem_create(unsigned max_count,
+                                         unsigned initial_count,
+                                         FAR const char *name)
+{
+  FAR struct mmosal_sem *sem = kmm_malloc(sizeof(*sem));
+
+  if (sem != NULL)
+    {
+      nxsem_init(&sem->sem, 0, initial_count);
+      sem->max = max_count;
+    }
+
+  return sem;
+}
+
+void mmosal_sem_delete(FAR struct mmosal_sem *sem)
+{
+  if (sem != NULL)
+    {
+      nxsem_destroy(&sem->sem);
+      kmm_free(sem);
+    }
+}
+
+bool mmosal_sem_give(FAR struct mmosal_sem *sem)
+{
+  return sem_post_max(&sem->sem, sem->max);
+}
+
+bool mmosal_sem_give_from_isr(FAR struct mmosal_sem *sem)
+{
+  return sem_post_max(&sem->sem, sem->max);
+}
+
+bool mmosal_sem_wait(FAR struct mmosal_sem *sem, uint32_t timeout_ms)
+{
+  return sem_wait_ms(&sem->sem, timeout_ms);
+}
+
+uint32_t mmosal_sem_get_count(FAR struct mmosal_sem *sem)
+{
+  int value = 0;
+
+  nxsem_get_value(&sem->sem, &value);
+  return value > 0 ? value : 0;
+}
+
+/* Binary semaphores ********************************************************/
+
+FAR struct mmosal_semb *mmosal_semb_create(FAR const char *name)
+{
+  FAR struct mmosal_semb *semb = kmm_malloc(sizeof(*semb));
+
+  if (semb != NULL)
+    {
+      nxsem_init(&semb->sem, 0, 0);
+    }
+
+  return semb;
+}
+
+void mmosal_semb_delete(FAR struct mmosal_semb *semb)
+{
+  if (semb != NULL)
+    {
+      nxsem_destroy(&semb->sem);
+      kmm_free(semb);
+    }
+}
+
+bool mmosal_semb_give(FAR struct mmosal_semb *semb)
+{
+  return sem_post_max(&semb->sem, 1);
+}
+
+bool mmosal_semb_give_from_isr(FAR struct mmosal_semb *semb)
+{
+  return sem_post_max(&semb->sem, 1);
+}
+
+bool mmosal_semb_wait(FAR struct mmosal_semb *semb, uint32_t timeout_ms)
+{
+  return sem_wait_ms(&semb->sem, timeout_ms);
+}
+
+/* Queues *******************************************************************/
+
+FAR struct mmosal_queue *mmosal_queue_create(size_t num_items,
+                                             size_t item_size,
+                                             FAR const char *name)
+{
+  FAR struct mmosal_queue *queue;
+
+  queue = kmm_zalloc(sizeof(*queue) + num_items * item_size);
+  if (queue != NULL)
+    {
+      nxsem_init(&queue->items, 0, 0);
+      nxsem_init(&queue->spaces, 0, num_items);
+      queue->itemsize = item_size;
+      queue->nitems   = num_items;
+    }
+
+  return queue;
+}
+
+void mmosal_queue_delete(FAR struct mmosal_queue *queue)
+{
+  if (queue != NULL)
+    {
+      nxsem_destroy(&queue->items);
+      nxsem_destroy(&queue->spaces);
+      kmm_free(queue);
+    }
+}
+
+static void queue_put(FAR struct mmosal_queue *queue, FAR const void *item)
+{
+  irqstate_t flags = enter_critical_section();
+
+  memcpy(&queue->buf[queue->head * queue->itemsize], item,
+         queue->itemsize);
+  queue->head = (queue->head + 1) % queue->nitems;
+  leave_critical_section(flags);
+  nxsem_post(&queue->items);
+}
+
+static void queue_get(FAR struct mmosal_queue *queue, FAR void *item)
+{
+  irqstate_t flags = enter_critical_section();
+
+  memcpy(item, &queue->buf[queue->tail * queue->itemsize],
+         queue->itemsize);
+  queue->tail = (queue->tail + 1) % queue->nitems;
+  leave_critical_section(flags);
+  nxsem_post(&queue->spaces);
+}
+
+bool mmosal_queue_push(FAR struct mmosal_queue *queue, FAR const void *item,
+                       uint32_t timeout_ms)
+{
+  if (!sem_wait_ms(&queue->spaces, timeout_ms))
+    {
+      return false;
+    }
+
+  queue_put(queue, item);
+  return true;
+}
+
+bool mmosal_queue_pop(FAR struct mmosal_queue *queue, FAR void *item,
+                      uint32_t timeout_ms)
+{
+  if (!sem_wait_ms(&queue->items, timeout_ms))
+    {
+      return false;
+    }
+
+  queue_get(queue, item);
+  return true;
+}
+
+bool mmosal_queue_push_from_isr(FAR struct mmosal_queue *queue,
+                                FAR const void *item)
+{
+  return mmosal_queue_push(queue, item, 0);
+}
+
+bool mmosal_queue_pop_from_isr(FAR struct mmosal_queue *queue,
+                               FAR void *item)
+{
+  return mmosal_queue_pop(queue, item, 0);
+}
+
+/* Time *********************************************************************/
+
+uint32_t mmosal_get_time_ms(void)
+{
+  return (uint32_t)TICK2MSEC(clock_systime_ticks());
+}
+
+uint32_t mmosal_get_time_ticks(void)
+{
+  return (uint32_t)clock_systime_ticks();
+}
+
+uint32_t mmosal_ticks_per_second(void)
+{
+  return TICK_PER_SEC;
+}
+
+/* Timers *******************************************************************/
+
+FAR struct mmosal_timer *mmosal_timer_create(FAR const char *name,
+                                             uint32_t timer_period_ms,
+                                             bool auto_reload,
+                                             FAR void *arg,
+                                             timer_callback_t callback)
+{
+  FAR struct mmosal_timer *timer = kmm_zalloc(sizeof(*timer));
+
+  if (timer != NULL)
+    {
+      timer->callback = callback;
+      timer->arg      = arg;
+      timer->period   = timer_period_ms;
+      timer->reload   = auto_reload;
+    }
+
+  return timer;
+}
+
+void mmosal_timer_delete(FAR struct mmosal_timer *timer)
+{
+  if (timer != NULL)
+    {
+      mmosal_timer_stop(timer);
+      kmm_free(timer);
+    }
+}
+
+bool mmosal_timer_start(FAR struct mmosal_timer *timer)
+{
+  timer->active = true;
+  return work_queue(MORSE_TIMER_WORK, &timer->work, morse_timer_worker,
+                    timer, ms2tick(timer->period)) >= 0;
+}
+
+bool mmosal_timer_stop(FAR struct mmosal_timer *timer)
+{
+  timer->active = false;
+  work_cancel(MORSE_TIMER_WORK, &timer->work);
+  return true;
+}
+
+bool mmosal_timer_change_period(FAR struct mmosal_timer *timer,
+                                uint32_t new_period)
+{
+  /* Like FreeRTOS xTimerChangePeriod(), this also (re)starts the timer */
+
+  timer->period = new_period;
+  return mmosal_timer_start(timer);
+}
+
+FAR void *mmosal_timer_get_arg(FAR struct mmosal_timer *timer)
+{
+  return timer->arg;
+}
+
+bool mmosal_is_timer_active(FAR struct mmosal_timer *timer)
+{
+  return timer->active;
+}
+
+/* Debug and assertions *****************************************************/
+
+int mmosal_printf(FAR const char *format, ...)
+{
+#ifdef CONFIG_IEEE80211_MORSE_LIBLOG
+  va_list ap;
+
+  va_start(ap, format);
+  vsyslog(LOG_INFO, format, ap);
+  va_end(ap);
+#endif
+
+  return 0;
+}
+
+void mmosal_log_failure_info(FAR const struct mmosal_failure_info *info)
+{
+  syslog(LOG_EMERG, "morse: failure at pc 0x%08" PRIx32 " lr 0x%08" PRIx32
+         " file 0x%08" PRIx32 " line %" PRIu32 " (0x%08" PRIx32 ")\n",
+         info->pc, info->lr, info->fileid, info->line,
+         info->platform_info[0]);
+}
+
+void mmosal_impl_assert(void)
+{
+  PANIC();
+}
diff --git a/drivers/wireless/ieee80211/morse/morse_netdev.c 
b/drivers/wireless/ieee80211/morse/morse_netdev.c
new file mode 100644
index 00000000000..8ff052c00d0
--- /dev/null
+++ b/drivers/wireless/ieee80211/morse/morse_netdev.c
@@ -0,0 +1,1194 @@
+/****************************************************************************
+ * drivers/wireless/ieee80211/morse/morse_netdev.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/* Network interface for the Morse Micro MM6108/MM8108 Wi-Fi HaLow
+ * (IEEE 802.11ah) transceivers.
+ *
+ * The 802.11ah UMAC, the supplicant (WPA3-SAE/OWE) and the chip transport
+ * are provided by the Morse Micro host library (libmorse).  This file binds
+ * the libmorse WLAN API to the NuttX lower half network driver interface,
+ * including the wireless extensions used by tools like wapi.
+ *
+ * The libmorse datapath works with 802.3 frames, so the interface is
+ * registered as an Ethernet-like IEEE 802.11 link.
+ */
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <assert.h>
+#include <debug.h>
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/param.h>
+
+#include <net/if.h>
+#include <net/if_arp.h>
+
+#include <nuttx/kmalloc.h>
+#include <nuttx/mutex.h>
+#include <nuttx/semaphore.h>
+#include <nuttx/spinlock.h>
+#include <nuttx/wqueue.h>
+#include <nuttx/net/netdev_lowerhalf.h>
+#include <nuttx/wireless/wireless.h>
+#include <nuttx/wireless/morse.h>
+
+#include "mmwlan.h"
+#include "mmpkt.h"
+#include "mmregdb.h"
+#include "morse.h"
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+#ifndef CONFIG_SCHED_LPWORK
+#  define LPWORK HPWORK
+#endif
+
+#define MORSE_IW_EVENT_SIZE(field) \
+  (offsetof(struct iw_event, u) + sizeof(((union iwreq_data *)0)->field))
+
+/* 802.11 capability information: privacy bit */
+
+#define MORSE_CAPAB_PRIVACY   0x0010
+
+/* Frequencies are reported as m * 10^e Hz.  An exponent of zero means a
+ * channel number in the wireless extensions, so use kHz units (all the S1G
+ * channel centres are multiple of 500 kHz).
+ */
+
+/****************************************************************************
+ * Private Types
+ ****************************************************************************/
+
+enum morse_scan_state_e
+{
+  MORSE_SCAN_IDLE = 0,
+  MORSE_SCAN_RUNNING,
+  MORSE_SCAN_DONE,
+};
+
+struct morse_dev_s
+{
+  struct netdev_lowerhalf_s dev;    /* Must be first */
+
+  mutex_t lock;                     /* Serializes the wireless operations */
+  netpkt_queue_t rxq;               /* Received packets */
+  spinlock_t rxlock;
+  struct work_s linkwork;           /* Carrier on/off notification */
+  sem_t linksem;                    /* Posted when the link goes up */
+  volatile bool linkup;
+  bool sta_enabled;
+
+  /* Station configuration */
+
+  uint8_t ssid[MMWLAN_SSID_MAXLEN];
+  uint16_t ssid_len;
+  uint8_t bssid[MMWLAN_MAC_ADDR_LEN];
+  char passphrase[MMWLAN_PASSPHRASE_MAXLEN + 1];
+  uint16_t passphrase_len;
+  enum mmwlan_security_type security;
+  char country[MMWLAN_COUNTRY_CODE_LEN];
+  FAR const struct mmwlan_s1g_channel_list *channels;
+  int txpower;                      /* Max TX power (dBm), 0 = default */
+
+  /* Scan results, stored as a stream of struct iw_event */
+
+  volatile uint8_t scan_state;
+  FAR uint8_t *scan_buf;
+  size_t scan_len;
+  bool scan_full;
+};
+
+/****************************************************************************
+ * Private Function Prototypes
+ ****************************************************************************/
+
+/* Network device operations */
+
+static int morse_ifup(FAR struct netdev_lowerhalf_s *dev);
+static int morse_ifdown(FAR struct netdev_lowerhalf_s *dev);
+static int morse_transmit(FAR struct netdev_lowerhalf_s *dev,
+                          FAR netpkt_t *pkt);
+static FAR netpkt_t *morse_receive(FAR struct netdev_lowerhalf_s *dev);
+
+/* Wireless operations */
+
+static int morse_connect(FAR struct netdev_lowerhalf_s *dev);
+static int morse_disconnect(FAR struct netdev_lowerhalf_s *dev);
+static int morse_essid(FAR struct netdev_lowerhalf_s *dev,
+                       FAR struct iwreq *iwr, bool set);
+static int morse_bssid(FAR struct netdev_lowerhalf_s *dev,
+                       FAR struct iwreq *iwr, bool set);
+static int morse_passwd(FAR struct netdev_lowerhalf_s *dev,
+                        FAR struct iwreq *iwr, bool set);
+static int morse_mode(FAR struct netdev_lowerhalf_s *dev,
+                      FAR struct iwreq *iwr, bool set);
+static int morse_auth(FAR struct netdev_lowerhalf_s *dev,
+                      FAR struct iwreq *iwr, bool set);
+static int morse_freq(FAR struct netdev_lowerhalf_s *dev,
+                      FAR struct iwreq *iwr, bool set);
+static int morse_txpower(FAR struct netdev_lowerhalf_s *dev,
+                         FAR struct iwreq *iwr, bool set);
+static int morse_country(FAR struct netdev_lowerhalf_s *dev,
+                         FAR struct iwreq *iwr, bool set);
+static int morse_sensitivity(FAR struct netdev_lowerhalf_s *dev,
+                             FAR struct iwreq *iwr, bool set);
+static int morse_scan(FAR struct netdev_lowerhalf_s *dev,
+                      FAR struct iwreq *iwr, bool set);
+
+/****************************************************************************
+ * Private Data
+ ****************************************************************************/
+
+static const struct netdev_ops_s g_morse_netdev_ops =
+{
+  .ifup     = morse_ifup,
+  .ifdown   = morse_ifdown,
+  .transmit = morse_transmit,
+  .receive  = morse_receive,
+};
+
+static const struct wireless_ops_s g_morse_wireless_ops =
+{
+  .connect     = morse_connect,
+  .disconnect  = morse_disconnect,
+  .essid       = morse_essid,
+  .bssid       = morse_bssid,
+  .passwd      = morse_passwd,
+  .mode        = morse_mode,
+  .auth        = morse_auth,
+  .freq        = morse_freq,
+  .txpower     = morse_txpower,
+  .country     = morse_country,
+  .sensitivity = morse_sensitivity,
+  .scan        = morse_scan,
+};
+
+/* libmorse supports a single WLAN interface; the callbacks without an
+ * argument use this reference.
+ */
+
+static FAR struct morse_dev_s *g_morse_dev;
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static int morse_status2errno(enum mmwlan_status status)
+{
+  switch (status)
+    {
+      case MMWLAN_SUCCESS:
+        return OK;
+
+      case MMWLAN_INVALID_ARGUMENT:
+      case MMWLAN_CHANNEL_INVALID:
+        return -EINVAL;
+
+      case MMWLAN_UNAVAILABLE:
+      case MMWLAN_SHUTDOWN_BLOCKED:
+        return -EBUSY;
+
+      case MMWLAN_NO_MEM:
+        return -ENOMEM;
+
+      case MMWLAN_TIMED_OUT:
+        return -ETIMEDOUT;
+
+      case MMWLAN_NOT_FOUND:
+        return -ENOENT;
+
+      case MMWLAN_CHANNEL_LIST_NOT_SET:
+      case MMWLAN_NOT_RUNNING:
+      case MMWLAN_NOT_INITIALIZED:
+        return -ENETDOWN;
+
+      default:
+        return -EIO;
+    }
+}
+
+/* Look up the centre frequency of an S1G channel in the active
+ * regulatory domain.  Returns 0 if not found.
+ */
+
+static uint32_t morse_chan2freq(FAR struct morse_dev_s *priv,
+                                unsigned int chan)
+{
+  unsigned int i;
+
+  if (priv->channels == NULL)
+    {
+      return 0;
+    }
+
+  for (i = 0; i < priv->channels->num_channels; i++)
+    {
+      if (priv->channels->channels[i].s1g_chan_num == chan)
+        {
+          return priv->channels->channels[i].centre_freq_hz;
+        }
+    }
+
+  return 0;
+}
+
+static int morse_set_country(FAR struct morse_dev_s *priv,
+                             FAR const char *code, bool boot)
+{
+  FAR const struct mmwlan_s1g_channel_list *list;
+  enum mmwlan_status status;
+
+  list = mmwlan_lookup_regulatory_domain(get_regulatory_db(), code);
+  if (list == NULL)
+    {
+      wlerr("ERROR: unsupported country code %.2s\n", code);
+      return -EINVAL;
+    }
+
+  /* The channel list cannot be changed while the chip is booted: shut it
+   * down (no effect if not booted), apply the new list and boot it again.
+   */
+
+  mmwlan_shutdown();
+  status = mmwlan_set_channel_list(list);
+  if (status == MMWLAN_SUCCESS && boot)
+    {
+      status = mmwlan_boot(NULL);
+    }
+
+  if (status != MMWLAN_SUCCESS)
+    {
+      return morse_status2errno(status);
+    }
+
+  priv->channels   = list;
+  priv->country[0] = code[0];
+  priv->country[1] = code[1];
+  priv->country[2] = '\0';
+  return OK;
+}
+
+/* Datapath callbacks *******************************************************/
+
+static void morse_rx_callback(FAR struct mmpkt *mmpkt, FAR void *arg)
+{
+  FAR struct morse_dev_s *priv = arg;
+  FAR struct mmpktview *view;
+  FAR netpkt_t *pkt;
+  irqstate_t flags;
+  uint32_t len;
+  int ret;
+
+  view = mmpkt_open(mmpkt);
+  len  = mmpkt_get_data_length(view);
+
+  pkt = netpkt_alloc(&priv->dev, NETPKT_RX);
+  if (pkt == NULL)
+    {
+      wlwarn("WARNING: no RX buffer, packet dropped\n");
+      goto out;
+    }
+
+  ret = netpkt_copyin(&priv->dev, pkt, mmpkt_get_data_start(view), len, 0);
+  if (ret < 0)
+    {
+      netpkt_free(&priv->dev, pkt, NETPKT_RX);
+      goto out;
+    }
+
+  flags = spin_lock_irqsave(&priv->rxlock);
+  ret = netpkt_tryadd_queue(pkt, &priv->rxq);
+  spin_unlock_irqrestore(&priv->rxlock, flags);
+  if (ret < 0)
+    {
+      netpkt_free(&priv->dev, pkt, NETPKT_RX);
+      goto out;
+    }
+
+  netdev_lower_rxready(&priv->dev);
+
+out:
+  mmpkt_close(&view);
+  mmpkt_release(mmpkt);
+}
+
+static void morse_link_worker(FAR void *arg)
+{
+  FAR struct morse_dev_s *priv = arg;
+
+  if (priv->linkup)
+    {
+      netdev_lower_carrier_on(&priv->dev);
+    }
+  else
+    {
+      netdev_lower_carrier_off(&priv->dev);
+    }
+}
+
+static void morse_link_callback(enum mmwlan_link_state link_state,
+                                FAR void *arg)
+{
+  FAR struct morse_dev_s *priv = arg;
+
+  /* Must not block here: defer the carrier change to the work queue */
+
+  priv->linkup = (link_state == MMWLAN_LINK_UP);
+  wlinfo("link %s\n", priv->linkup ? "up" : "down");
+
+  work_queue(LPWORK, &priv->linkwork, morse_link_worker, priv, 0);
+
+  if (priv->linkup)
+    {
+      nxsem_post(&priv->linksem);
+    }
+}
+
+static void morse_sta_status_callback(enum mmwlan_sta_state sta_state)
+{
+  wlinfo("STA state %d\n", sta_state);
+  UNUSED(sta_state);
+}
+
+/* Scan callbacks ***********************************************************/
+
+static FAR struct iw_event *morse_scan_event(FAR struct morse_dev_s *priv,
+                                             size_t len)
+{
+  FAR struct iw_event *iwe;
+
+  if (priv->scan_len + len > CONFIG_IEEE80211_MORSE_SCAN_BUFSIZE)
+    {
+      priv->scan_full = true;
+      return NULL;
+    }
+
+  iwe = (FAR struct iw_event *)&priv->scan_buf[priv->scan_len];
+  iwe->len = len;
+  priv->scan_len += len;
+  return iwe;
+}
+
+static void morse_scan_rx_callback(FAR const struct mmwlan_scan_result *res,
+                                   FAR void *arg)
+{
+  FAR struct morse_dev_s *priv = arg;
+  FAR struct iw_event *iwe;
+  size_t ssid_len;
+  size_t needed;
+
+  /* Only add complete records */
+
+  ssid_len = MIN(res->ssid_len, IW_ESSID_MAX_SIZE);
+  needed   = MORSE_IW_EVENT_SIZE(ap_addr) +
+             MORSE_IW_EVENT_SIZE(essid) + ((ssid_len + 3) & ~3) +
+             MORSE_IW_EVENT_SIZE(qual) + MORSE_IW_EVENT_SIZE(mode) +
+             MORSE_IW_EVENT_SIZE(data) + MORSE_IW_EVENT_SIZE(freq);
+
+  if (priv->scan_buf == NULL ||
+      priv->scan_len + needed > CONFIG_IEEE80211_MORSE_SCAN_BUFSIZE)
+    {
+      priv->scan_full = true;
+      return;
+    }
+
+  iwe = morse_scan_event(priv, MORSE_IW_EVENT_SIZE(ap_addr));
+  iwe->cmd = SIOCGIWAP;
+  iwe->u.ap_addr.sa_family = ARPHRD_ETHER;
+  memcpy(iwe->u.ap_addr.sa_data, res->bssid, MMWLAN_MAC_ADDR_LEN);
+
+  iwe = morse_scan_event(priv, MORSE_IW_EVENT_SIZE(essid) +
+                               ((ssid_len + 3) & ~3));
+  iwe->cmd = SIOCGIWESSID;
+  iwe->u.essid.flags  = 0;
+  iwe->u.essid.length = ssid_len;
+
+  /* The iw_point pointer field holds the offset to the data */
+
+  iwe->u.essid.pointer = (FAR void *)sizeof(iwe->u.essid);
+  memcpy(&iwe->u.essid + 1, res->ssid, ssid_len);
+
+  iwe = morse_scan_event(priv, MORSE_IW_EVENT_SIZE(qual));
+  iwe->cmd = IWEVQUAL;
+  iwe->u.qual.qual    = 0;
+  iwe->u.qual.level   = (uint8_t)res->rssi;
+  iwe->u.qual.noise   = (uint8_t)res->noise_dbm;
+  iwe->u.qual.updated = IW_QUAL_DBM | IW_QUAL_ALL_UPDATED;
+
+  iwe = morse_scan_event(priv, MORSE_IW_EVENT_SIZE(mode));
+  iwe->cmd    = SIOCGIWMODE;
+  iwe->u.mode = IW_MODE_MASTER;
+
+  iwe = morse_scan_event(priv, MORSE_IW_EVENT_SIZE(data));
+  iwe->cmd = SIOCGIWENCODE;
+  iwe->u.data.flags   = (res->capability_info & MORSE_CAPAB_PRIVACY) ?
+                        IW_ENCODE_ENABLED | IW_ENCODE_NOKEY :
+                        IW_ENCODE_DISABLED;
+  iwe->u.data.length  = 0;
+  iwe->u.data.pointer = NULL;
+
+  iwe = morse_scan_event(priv, MORSE_IW_EVENT_SIZE(freq));
+  iwe->cmd      = SIOCGIWFREQ;
+  iwe->u.freq.m = res->channel_freq_hz / 1000;
+  iwe->u.freq.e = 3;
+  iwe->u.freq.i = 0;
+  iwe->u.freq.flags = IW_FREQ_FIXED;
+
+  wlinfo("scan: %.*s rssi %d freq %" PRIu32 "\n", (int)ssid_len,
+         res->ssid, res->rssi, res->channel_freq_hz);
+}
+
+static void morse_scan_complete_callback(enum mmwlan_scan_state state,
+                                         FAR void *arg)
+{
+  FAR struct morse_dev_s *priv = arg;
+
+  wlinfo("scan complete (%d), %zu bytes%s\n", state, priv->scan_len,
+         priv->scan_full ? ", results truncated" : "");
+  priv->scan_state = MORSE_SCAN_DONE;
+}
+
+/* Network device operations ************************************************/
+
+static int morse_ifup(FAR struct netdev_lowerhalf_s *dev)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  irqstate_t flags;
+
+  flags = spin_lock_irqsave(&priv->rxlock);
+  netpkt_free_queue(&priv->rxq);
+  spin_unlock_irqrestore(&priv->rxlock, flags);
+
+  /* The carrier follows the WLAN link state */
+
+  if (priv->linkup)
+    {
+      netdev_lower_carrier_on(dev);
+    }
+  else
+    {
+      netdev_lower_carrier_off(dev);
+    }
+
+  return OK;
+}
+
+static int morse_ifdown(FAR struct netdev_lowerhalf_s *dev)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  irqstate_t flags;
+
+  morse_disconnect(dev);
+
+  flags = spin_lock_irqsave(&priv->rxlock);
+  netpkt_free_queue(&priv->rxq);
+  spin_unlock_irqrestore(&priv->rxlock, flags);
+
+  return OK;
+}
+
+static int morse_transmit(FAR struct netdev_lowerhalf_s *dev,
+                          FAR netpkt_t *pkt)
+{
+  unsigned int len = netpkt_getdatalen(dev, pkt);
+  struct mmwlan_tx_metadata metadata = MMWLAN_TX_METADATA_INIT;
+  FAR struct mmpktview *view;
+  FAR struct mmpkt *mmpkt;
+  enum mmwlan_status status;
+  int ret;
+
+  /* The upper half drops the packet on error, so give the chip a short
+   * time to drain its queues when TX is flow controlled.
+   */
+
+  status = mmwlan_tx_wait_until_ready(CONFIG_IEEE80211_MORSE_TX_TIMEOUT);
+  if (status != MMWLAN_SUCCESS)
+    {
+      return morse_status2errno(status);
+    }
+
+  mmpkt = mmwlan_alloc_mmpkt_for_tx(len, metadata.tid);
+  if (mmpkt == NULL)
+    {
+      return -ENOMEM;
+    }
+
+  view = mmpkt_open(mmpkt);
+  ret  = netpkt_copyout(dev, mmpkt_append(view, len), pkt, len, 0);
+  mmpkt_close(&view);
+  if (ret < 0)
+    {
+      mmpkt_release(mmpkt);
+      return ret;
+    }
+
+  /* The mmpkt is consumed by mmwlan_tx_pkt() in all cases */
+
+  status = mmwlan_tx_pkt(mmpkt, &metadata);
+  if (status != MMWLAN_SUCCESS)
+    {
+      return morse_status2errno(status);
+    }
+
+  netpkt_free(dev, pkt, NETPKT_TX);
+  netdev_lower_txdone(dev);
+  return OK;
+}
+
+static FAR netpkt_t *morse_receive(FAR struct netdev_lowerhalf_s *dev)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  FAR netpkt_t *pkt;
+  irqstate_t flags;
+
+  flags = spin_lock_irqsave(&priv->rxlock);
+  pkt = netpkt_remove_queue(&priv->rxq);
+  spin_unlock_irqrestore(&priv->rxlock, flags);
+
+  return pkt;
+}
+
+/* Wireless operations ******************************************************/
+
+static int morse_connect(FAR struct netdev_lowerhalf_s *dev)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  struct mmwlan_sta_args args = MMWLAN_STA_ARGS_INIT;
+  enum mmwlan_status status;
+  int ret;
+
+  nxmutex_lock(&priv->lock);
+
+  if (priv->ssid_len == 0)
+    {
+      ret = -EINVAL;
+      goto out;
+    }
+
+  /* Reconfigure: drop the current association first */
+
+  if (priv->sta_enabled)
+    {
+      mmwlan_sta_disable();
+      priv->sta_enabled = false;
+    }
+
+  memcpy(args.ssid, priv->ssid, priv->ssid_len);
+  args.ssid_len = priv->ssid_len;
+  memcpy(args.bssid, priv->bssid, sizeof(args.bssid));
+  args.security_type = priv->security;
+  if (priv->security == MMWLAN_SAE)
+    {
+      memcpy(args.passphrase, priv->passphrase, priv->passphrase_len);
+      args.passphrase_len = priv->passphrase_len;
+    }
+
+  if (priv->security == MMWLAN_OPEN)
+    {
+      args.pmf_mode = MMWLAN_PMF_DISABLED;
+    }
+
+  while (nxsem_trywait(&priv->linksem) >= 0);
+
+  /* Without the WAKE and BUSY lines the host cannot wake the chip from
+   * power save: keep it always awake.
+   */
+
+  if (!morse_hal_has_powersave())
+    {
+      mmwlan_set_power_save_mode(MMWLAN_PS_DISABLED);
+    }
+
+  wlinfo("connecting to %.*s (security %d)\n", priv->ssid_len, priv->ssid,
+         priv->security);
+
+  status = mmwlan_sta_enable(&args, morse_sta_status_callback);
+  if (status != MMWLAN_SUCCESS)
+    {
+      wlerr("ERROR: mmwlan_sta_enable failed: %d\n", status);
+      ret = morse_status2errno(status);
+      goto out;
+    }
+
+  priv->sta_enabled = true;
+
+  /* libmorse keeps on trying in the background, so on timeout the link can
+   * still come up later.
+   */
+
+  ret = nxsem_tickwait_uninterruptible(&priv->linksem,
+                       SEC2TICK(CONFIG_IEEE80211_MORSE_CONNECT_TIMEOUT));
+  if (ret < 0)
+    {
+      wlwarn("WARNING: link not up after %d s\n",
+             CONFIG_IEEE80211_MORSE_CONNECT_TIMEOUT);
+      ret = -ETIMEDOUT;
+    }
+
+out:
+  nxmutex_unlock(&priv->lock);
+  return ret;
+}
+
+static int morse_disconnect(FAR struct netdev_lowerhalf_s *dev)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  enum mmwlan_status status = MMWLAN_SUCCESS;
+
+  nxmutex_lock(&priv->lock);
+
+  if (priv->sta_enabled)
+    {
+      status = mmwlan_sta_disable();
+      priv->sta_enabled = false;
+      if (status == MMWLAN_SHUTDOWN_BLOCKED)
+        {
+          status = MMWLAN_SUCCESS;
+        }
+    }
+
+  nxmutex_unlock(&priv->lock);
+  return morse_status2errno(status);
+}
+
+static int morse_essid(FAR struct netdev_lowerhalf_s *dev,
+                       FAR struct iwreq *iwr, bool set)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  int ret = OK;
+
+  nxmutex_lock(&priv->lock);
+
+  if (set)
+    {
+      if (iwr->u.essid.length > MMWLAN_SSID_MAXLEN ||
+          iwr->u.essid.pointer == NULL)
+        {
+          ret = -EINVAL;
+        }
+      else
+        {
+          memcpy(priv->ssid, iwr->u.essid.pointer, iwr->u.essid.length);
+          priv->ssid_len = iwr->u.essid.length;
+        }
+    }
+  else
+    {
+      if (iwr->u.essid.length < priv->ssid_len)
+        {
+          ret = -E2BIG;
+        }
+      else
+        {
+          memcpy(iwr->u.essid.pointer, priv->ssid, priv->ssid_len);
+          iwr->u.essid.length = priv->ssid_len;
+          iwr->u.essid.flags  = priv->linkup ? IW_ESSID_ON : IW_ESSID_OFF;
+        }
+    }
+
+  nxmutex_unlock(&priv->lock);
+  return ret;
+}
+
+static int morse_bssid(FAR struct netdev_lowerhalf_s *dev,
+                       FAR struct iwreq *iwr, bool set)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+
+  if (set)
+    {
+      memcpy(priv->bssid, iwr->u.ap_addr.sa_data, MMWLAN_MAC_ADDR_LEN);
+      return OK;
+    }
+
+  iwr->u.ap_addr.sa_family = ARPHRD_ETHER;
+  if (!priv->linkup ||
+      mmwlan_get_bssid((FAR uint8_t *)iwr->u.ap_addr.sa_data) !=
+      MMWLAN_SUCCESS)
+    {
+      memset(iwr->u.ap_addr.sa_data, 0, MMWLAN_MAC_ADDR_LEN);
+    }
+
+  return OK;
+}
+
+static int morse_passwd(FAR struct netdev_lowerhalf_s *dev,
+                        FAR struct iwreq *iwr, bool set)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  FAR struct iw_encode_ext *ext = iwr->u.encoding.pointer;
+  int ret = OK;
+
+  if (ext == NULL)
+    {
+      return -EINVAL;
+    }
+
+  nxmutex_lock(&priv->lock);
+
+  if (set)
+    {
+      /* Wi-Fi HaLow only allows open, OWE and WPA3-SAE networks: a
+       * passphrase selects SAE.
+       */
+
+      if (ext->alg == IW_ENCODE_ALG_NONE || ext->key_len == 0)
+        {
+          priv->passphrase_len = 0;
+          if (priv->security == MMWLAN_SAE)
+            {
+              priv->security = MMWLAN_OPEN;
+            }
+        }
+      else if (ext->key_len > MMWLAN_PASSPHRASE_MAXLEN)
+        {
+          ret = -EINVAL;
+        }
+      else
+        {
+          memcpy(priv->passphrase, ext->key, ext->key_len);
+          priv->passphrase[ext->key_len] = '\0';
+          priv->passphrase_len = ext->key_len;
+          priv->security = MMWLAN_SAE;
+        }
+    }
+  else
+    {
+      if (iwr->u.encoding.length < sizeof(*ext) + priv->passphrase_len)
+        {
+          ret = -E2BIG;
+        }
+      else
+        {
+          ext->alg     = priv->security == MMWLAN_SAE ?
+                         IW_ENCODE_ALG_CCMP : IW_ENCODE_ALG_NONE;
+          ext->key_len = priv->passphrase_len;
+          memcpy(ext->key, priv->passphrase, priv->passphrase_len);
+        }
+    }
+
+  nxmutex_unlock(&priv->lock);
+  return ret;
+}
+
+static int morse_mode(FAR struct netdev_lowerhalf_s *dev,
+                      FAR struct iwreq *iwr, bool set)
+{
+  if (set)
+    {
+      return iwr->u.mode == IW_MODE_INFRA ? OK : -ENOSYS;
+    }
+
+  iwr->u.mode = IW_MODE_INFRA;
+  return OK;
+}
+
+static int morse_auth(FAR struct netdev_lowerhalf_s *dev,
+                      FAR struct iwreq *iwr, bool set)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  int flag = iwr->u.param.flags & IW_AUTH_INDEX;
+  int ret = OK;
+
+  nxmutex_lock(&priv->lock);
+
+  if (set)
+    {
+      if (flag == IW_AUTH_WPA_VERSION)
+        {
+          if (iwr->u.param.value & IW_AUTH_WPA_VERSION_DISABLED)
+            {
+              priv->security = MMWLAN_OPEN;
+            }
+          else if (iwr->u.param.value & IW_AUTH_WPA_VERSION_WPA3)
+            {
+              priv->security = priv->passphrase_len > 0 ? MMWLAN_SAE :
+                                                          MMWLAN_OWE;
+            }
+          else
+            {
+              /* WPA/WPA2 are not allowed by the 802.11ah standard: use
+               * WPA3-SAE instead (requires a passphrase).
+               */
+
+              wlwarn("WARNING: HaLow requires WPA3, using SAE\n");
+              priv->security = MMWLAN_SAE;
+            }
+        }
+    }
+  else
+    {
+      if (flag == IW_AUTH_WPA_VERSION)
+        {
+          iwr->u.param.value = priv->security == MMWLAN_OPEN ?
+                               IW_AUTH_WPA_VERSION_DISABLED :
+                               IW_AUTH_WPA_VERSION_WPA3;
+        }
+      else if (flag == IW_AUTH_CIPHER_PAIRWISE ||
+               flag == IW_AUTH_CIPHER_GROUP)
+        {
+          iwr->u.param.value = priv->security == MMWLAN_OPEN ?
+                               IW_AUTH_CIPHER_NONE : IW_AUTH_CIPHER_CCMP;
+        }
+      else
+        {
+          ret = -ENOSYS;
+        }
+    }
+
+  nxmutex_unlock(&priv->lock);
+  return ret;
+}
+
+static int morse_freq(FAR struct netdev_lowerhalf_s *dev,
+                      FAR struct iwreq *iwr, bool set)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  struct mmwlan_vif_channel_info info;
+
+  /* The channel is selected by the access point */
+
+  if (set)
+    {
+      return -ENOSYS;
+    }
+
+  if (!priv->linkup ||
+      mmwlan_get_vif_channel_info(MMWLAN_VIF_STA, &info) != MMWLAN_SUCCESS)
+    {
+      return -ENOTCONN;
+    }
+
+  iwr->u.freq.m     = morse_chan2freq(priv, info.s1g_chan_num) / 1000;
+  iwr->u.freq.e     = 3;
+  iwr->u.freq.i     = 0;
+  iwr->u.freq.flags = IW_FREQ_FIXED;
+  return OK;
+}
+
+static int morse_txpower(FAR struct netdev_lowerhalf_s *dev,
+                         FAR struct iwreq *iwr, bool set)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  enum mmwlan_status status;
+
+  if (set)
+    {
+      if (iwr->u.txpower.flags != IW_TXPOW_DBM ||
+          iwr->u.txpower.value < 0)
+        {
+          return -EINVAL;
+        }
+
+      status = mmwlan_override_max_tx_power(iwr->u.txpower.value);
+      if (status == MMWLAN_SUCCESS)
+        {
+          priv->txpower = iwr->u.txpower.value;
+        }
+
+      return morse_status2errno(status);
+    }
+
+  iwr->u.txpower.value    = priv->txpower;
+  iwr->u.txpower.fixed    = priv->txpower != 0;
+  iwr->u.txpower.disabled = 0;
+  iwr->u.txpower.flags    = IW_TXPOW_DBM;
+  return OK;
+}
+
+static int morse_country(FAR struct netdev_lowerhalf_s *dev,
+                         FAR struct iwreq *iwr, bool set)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  FAR char *code = iwr->u.data.pointer;
+  int ret = OK;
+
+  if (code == NULL)
+    {
+      return -EINVAL;
+    }
+
+  nxmutex_lock(&priv->lock);
+
+  if (set)
+    {
+      /* The channel list can only be changed while inactive */
+
+      if (priv->sta_enabled)
+        {
+          ret = -EBUSY;
+        }
+      else
+        {
+          ret = morse_set_country(priv, code, true);
+        }
+    }
+  else if (iwr->u.data.length < 3)
+    {
+      ret = -E2BIG;
+    }
+  else
+    {
+      strlcpy(code, priv->country, iwr->u.data.length);
+      iwr->u.data.length = strlen(priv->country);
+    }
+
+  nxmutex_unlock(&priv->lock);
+  return ret;
+}
+
+static int morse_sensitivity(FAR struct netdev_lowerhalf_s *dev,
+                             FAR struct iwreq *iwr, bool set)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+
+  /* Report the RSSI of the connection, like the other NuttX drivers */
+
+  if (set)
+    {
+      return -ENOSYS;
+    }
+
+  if (!priv->linkup)
+    {
+      return -ENOTCONN;
+    }
+
+  iwr->u.sens.value = -mmwlan_get_rssi();
+  return OK;
+}
+
+static int morse_scan(FAR struct netdev_lowerhalf_s *dev,
+                      FAR struct iwreq *iwr, bool set)
+{
+  FAR struct morse_dev_s *priv = (FAR struct morse_dev_s *)dev;
+  struct mmwlan_scan_req req = MMWLAN_SCAN_REQ_INIT;
+  FAR struct iw_scan_req *scanreq;
+  enum mmwlan_status status;
+  int ret = OK;
+
+  nxmutex_lock(&priv->lock);
+
+  if (set)
+    {
+      if (priv->scan_state == MORSE_SCAN_RUNNING)
+        {
+          ret = -EBUSY;
+          goto out;
+        }
+
+      if (priv->scan_buf == NULL)
+        {
+          priv->scan_buf = kmm_malloc(CONFIG_IEEE80211_MORSE_SCAN_BUFSIZE);
+          if (priv->scan_buf == NULL)
+            {
+              ret = -ENOMEM;
+              goto out;
+            }
+        }
+
+      priv->scan_len  = 0;
+      priv->scan_full = false;
+
+      /* Optional directed scan for a given ESSID */
+
+      scanreq = (FAR struct iw_scan_req *)iwr->u.data.pointer;
+      if (scanreq != NULL && iwr->u.data.length >= sizeof(*scanreq) &&
+          (iwr->u.data.flags & IW_SCAN_THIS_ESSID) != 0 &&
+          scanreq->essid_len <= MMWLAN_SSID_MAXLEN)
+        {
+          memcpy(req.args.ssid, scanreq->essid, scanreq->essid_len);
+          req.args.ssid_len = scanreq->essid_len;
+        }
+
+      req.scan_rx_cb       = morse_scan_rx_callback;
+      req.scan_complete_cb = morse_scan_complete_callback;
+      req.scan_cb_arg      = priv;
+
+      priv->scan_state = MORSE_SCAN_RUNNING;
+      status = mmwlan_scan_request(&req);
+      if (status != MMWLAN_SUCCESS)
+        {
+          wlerr("ERROR: mmwlan_scan_request failed: %d\n", status);
+          priv->scan_state = MORSE_SCAN_IDLE;
+          ret = morse_status2errno(status);
+        }
+
+      goto out;
+    }
+
+  /* Get the scan results */
+
+  if (priv->scan_state == MORSE_SCAN_RUNNING)
+    {
+      ret = -EAGAIN;
+    }
+  else if (priv->scan_state == MORSE_SCAN_IDLE)
+    {
+      ret = -EINVAL;
+    }
+  else if (iwr->u.data.pointer == NULL ||
+           iwr->u.data.length < priv->scan_len)
+    {
+      /* Tell the caller the size of the result buffer needed */
+
+      iwr->u.data.length = priv->scan_len;
+      ret = -E2BIG;
+    }
+  else
+    {
+      memcpy(iwr->u.data.pointer, priv->scan_buf, priv->scan_len);
+      iwr->u.data.length = priv->scan_len;
+
+      /* The results are consumed: further reads return an empty list
+       * until the next scan.
+       */
+
+      kmm_free(priv->scan_buf);
+      priv->scan_buf = NULL;
+      priv->scan_len = 0;
+    }
+
+out:
+  nxmutex_unlock(&priv->lock);
+  return ret;
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: morse_register
+ *
+ * Description:
+ *   Initialize the Morse Micro HaLow driver and register the network
+ *   interface.  See include/nuttx/wireless/morse.h.
+ *
+ ****************************************************************************/
+
+int morse_register(FAR struct spi_dev_s *spi,
+                   FAR const struct morse_lower_s *lower)
+{
+  FAR struct morse_dev_s *priv;
+  struct mmwlan_version version;
+  enum mmwlan_status status;
+  uint8_t mac[MMWLAN_MAC_ADDR_LEN];
+  int ret;
+
+  DEBUGASSERT(spi != NULL && lower != NULL);
+
+  if (g_morse_dev != NULL)
+    {
+      return -EBUSY;
+    }
+
+  priv = kmm_zalloc(sizeof(*priv));
+  if (priv == NULL)
+    {
+      return -ENOMEM;
+    }
+
+  nxmutex_init(&priv->lock);
+  nxsem_init(&priv->linksem, 0, 0);
+  spin_lock_init(&priv->rxlock);
+  priv->security = MMWLAN_OPEN;
+
+  ret = morse_hal_initialize(spi, lower);
+  if (ret < 0)
+    {
+      goto errout;
+    }
+
+  mmwlan_init();
+
+  ret = morse_set_country(priv, CONFIG_IEEE80211_MORSE_COUNTRY, false);
+  if (ret < 0)
+    {
+      goto errout_deinit;
+    }
+
+  mmwlan_register_rx_pkt_cb(morse_rx_callback, priv);
+  mmwlan_register_link_state_cb(morse_link_callback, priv);
+
+  /* Boot the chip to read its version and MAC address */
+
+  status = mmwlan_boot(NULL);
+  if (status != MMWLAN_SUCCESS)
+    {
+      wlerr("ERROR: failed to boot the HaLow chip: %d\n", status);
+      ret = morse_status2errno(status);
+      goto errout_deinit;
+    }
+
+  if (mmwlan_get_version(&version) == MMWLAN_SUCCESS)
+    {
+      syslog(LOG_INFO, "morse: chip 0x%04" PRIx32 " firmware %s "
+             "morselib %s\n", version.morse_chip_id,
+             version.morse_fw_version, version.morselib_version);
+    }
+
+  status = mmwlan_get_vif_mac_addr(MMWLAN_VIF_STA, mac);
+  if (status != MMWLAN_SUCCESS)
+    {
+      ret = morse_status2errno(status);
+      goto errout_shutdown;
+    }
+
+  memcpy(priv->dev.netdev.d_mac.ether.ether_addr_octet, mac, sizeof(mac));
+
+  priv->dev.ops              = &g_morse_netdev_ops;
+  priv->dev.iw_ops           = &g_morse_wireless_ops;
+  priv->dev.quota[NETPKT_TX] = CONFIG_IEEE80211_MORSE_TX_QUOTA;
+  priv->dev.quota[NETPKT_RX] = CONFIG_IEEE80211_MORSE_RX_QUOTA;
+  priv->dev.rxtype           = NETDEV_RX_THREAD;
+  priv->dev.priority         = CONFIG_IEEE80211_MORSE_TASK_PRIORITY;
+
+  g_morse_dev = priv;
+
+  ret = netdev_lower_register(&priv->dev, NET_LL_IEEE80211);
+  if (ret < 0)
+    {
+      g_morse_dev = NULL;
+      goto errout_shutdown;
+    }
+
+  return OK;
+
+errout_shutdown:
+  mmwlan_shutdown();
+errout_deinit:
+  mmwlan_deinit();
+errout:
+  nxsem_destroy(&priv->linksem);
+  nxmutex_destroy(&priv->lock);
+  kmm_free(priv);
+  return ret;
+}
diff --git a/include/nuttx/wireless/morse.h b/include/nuttx/wireless/morse.h
new file mode 100644
index 00000000000..a3cc87527c1
--- /dev/null
+++ b/include/nuttx/wireless/morse.h
@@ -0,0 +1,153 @@
+/****************************************************************************
+ * include/nuttx/wireless/morse.h
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.  The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+#ifndef __INCLUDE_NUTTX_WIRELESS_MORSE_H
+#define __INCLUDE_NUTTX_WIRELESS_MORSE_H
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <nuttx/irq.h>
+#include <nuttx/spi/spi.h>
+
+#ifdef CONFIG_IEEE80211_MORSE
+
+/****************************************************************************
+ * Public Types
+ ****************************************************************************/
+
+/* The Morse Micro MM6108/MM8108 Wi-Fi HaLow (IEEE 802.11ah) transceiver is
+ * connected to the host through an SPI bus plus a few GPIOs:
+ *
+ *   RESET_N  - output, active low, the chip is held in reset at boot
+ *   WAKE     - output, active high, wakes the chip from deep sleep
+ *   SPI_IRQ  - input, active low, the chip has pending events
+ *   BUSY     - input, active high, used by the chip power save logic
+ *
+ * The board logic provides an instance of this structure describing how to
+ * drive these lines.  The chip select must be driven by the board's
+ * <spi>select() function for the device ID 'devid'.
+ *
+ * The lower half callbacks may be called from interrupt context unless
+ * noted otherwise.
+ */
+
+struct morse_lower_s
+{
+  uint32_t devid;      /* SPI device ID used for chip select */
+  uint32_t frequency;  /* SPI clock frequency (Hz), 0 means driver default */
+
+  /* Control the RESET_N line: true = hold the chip in reset */
+
+  CODE void (*reset)(FAR const struct morse_lower_s *lower, bool assert);
+
+  /* Control the WAKE line: true = wake the chip.
+   *
+   * Read the BUSY line: true = BUSY asserted.
+   *
+   * WAKE and BUSY are needed for the chip power save.  They are optional:
+   * if any of them is NULL (line not connected) the chip is kept always
+   * awake.
+   */
+
+  CODE void (*wake)(FAR const struct morse_lower_s *lower, bool assert);
+  CODE bool (*busy)(FAR const struct morse_lower_s *lower);
+
+  /* Read the SPI_IRQ line: true = IRQ asserted (line low) */
+
+  CODE bool (*irq_asserted)(FAR const struct morse_lower_s *lower);
+
+  /* Attach the SPI_IRQ handler (falling edge interrupt) and enable or
+   * disable that interrupt.
+   */
+
+  CODE int  (*irq_attach)(FAR const struct morse_lower_s *lower,
+                          xcpt_t handler, FAR void *arg);
+  CODE void (*irq_enable)(FAR const struct morse_lower_s *lower,
+                          bool enable);
+
+  /* Attach the BUSY handler (rising edge interrupt) and enable or disable
+   * that interrupt.  These are optional and may be NULL.
+   */
+
+  CODE int  (*busy_attach)(FAR const struct morse_lower_s *lower,
+                           xcpt_t handler, FAR void *arg);
+  CODE void (*busy_enable)(FAR const struct morse_lower_s *lower,
+                           bool enable);
+
+  /* Optional: provide a board specific MAC address.  Return true if
+   * 'mac' was filled with a valid address.  May be NULL, in which case the
+   * address programmed in the chip OTP is used.  Not called from interrupt
+   * context.
+   */
+
+  CODE bool (*getmac)(FAR const struct morse_lower_s *lower,
+                      FAR uint8_t *mac);
+};
+
+/****************************************************************************
+ * Public Function Prototypes
+ ****************************************************************************/
+
+#ifdef __cplusplus
+#define EXTERN extern "C"
+extern "C"
+{
+#else
+#define EXTERN extern
+#endif
+
+/****************************************************************************
+ * Name: morse_register
+ *
+ * Description:
+ *   Initialize the Morse Micro HaLow driver, boot the transceiver and
+ *   register the network interface (wlanN).  The interface is then
+ *   controlled with the standard wireless IOCTLs (for example with the
+ *   wapi tool).
+ *
+ * Input Parameters:
+ *   spi   - SPI bus the transceiver is connected to
+ *   lower - Board specific lower half.  It must persist for the lifetime
+ *           of the driver.
+ *
+ * Returned Value:
+ *   OK on success; a negated errno value on failure.
+ *
+ ****************************************************************************/
+
+int morse_register(FAR struct spi_dev_s *spi,
+                   FAR const struct morse_lower_s *lower);
+
+#undef EXTERN
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* CONFIG_IEEE80211_MORSE */
+#endif /* __INCLUDE_NUTTX_WIRELESS_MORSE_H */

Reply via email to