[][atenl: add package]

[Description]
Add atenl package, a userspace daemon for mt76 testmode.
atenl acts as an intermediate for HQADLL command and mt76 testmode
(implemented with NL80211_CMD_TESTMODE), which provides transparency for
the usage of QA-tool and Litepoint on mt76.

[Release-log]
N/A

Change-Id: If11e67b36dd7c3ef9629e824bc26ed4f16f34dca
Reviewed-on: https://gerrit.mediatek.inc/c/openwrt/feeds/mtk_openwrt_feeds/+/5553443
diff --git a/feed/atenl/Makefile b/feed/atenl/Makefile
new file mode 100644
index 0000000..d1c9d36
--- /dev/null
+++ b/feed/atenl/Makefile
@@ -0,0 +1,35 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=atenl
+PKG_RELEASE=1
+
+PKG_LICENSE:=GPLv2
+PKG_LICENSE_FILES:=
+
+PKG_MAINTAINER:=Shayne Chen <shayne.chen@mediatek.com>
+PKG_BUILD_PARALLEL:=1
+
+include $(INCLUDE_DIR)/package.mk
+include $(INCLUDE_DIR)/cmake.mk
+
+CMAKE_SOURCE_DIR:=$(PKG_BUILD_DIR)
+CMAKE_BINARY_DIR:=$(PKG_BUILD_DIR)
+
+define Package/atenl
+  SECTION:=MTK Properties
+  CATEGORY:=MTK Properties
+  TITLE:=testmode daemon for nl80211
+  SUBMENU:=Applications
+  DEPENDS:=+libnl-tiny
+endef
+
+TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include/libnl-tiny
+
+define Package/atenl/install
+	mkdir -p $(1)/usr/sbin
+	$(INSTALL_BIN) $(PKG_BUILD_DIR)/atenl $(1)/usr/sbin
+	$(INSTALL_BIN) ./files/ated.sh $(1)/usr/sbin/ated
+	$(INSTALL_BIN) ./files/iwpriv.sh $(1)/usr/sbin/iwpriv
+endef
+
+$(eval $(call BuildPackage,atenl))
diff --git a/feed/atenl/files/ated.sh b/feed/atenl/files/ated.sh
new file mode 100755
index 0000000..13786d6
--- /dev/null
+++ b/feed/atenl/files/ated.sh
@@ -0,0 +1,27 @@
+#!/bin/ash
+# This script is used for wrapping atenl daemon to ated
+
+# 0 is normal mode, 1 is used for specific commands
+mode="0"
+add_quote="0"
+cmd="atenl"
+
+for i in "$@"
+do
+    if [ "$i" = "-c" ]; then
+        cmd="${cmd} -c"
+        mode="1"
+        add_quote="1"
+    elif [ "${add_quote}" = "1" ]; then
+        cmd="${cmd} \"${i}\""
+        add_quote="0"
+    else
+        cmd="${cmd} ${i}"
+    fi
+done
+
+if [ "$mode" = "0" ]; then
+    killall atenl
+fi
+
+eval "${cmd}"
diff --git a/feed/atenl/files/iwpriv.sh b/feed/atenl/files/iwpriv.sh
new file mode 100755
index 0000000..be5a2ac
--- /dev/null
+++ b/feed/atenl/files/iwpriv.sh
@@ -0,0 +1,329 @@
+#!/bin/ash
+
+interface=$1
+cmd_type=$2
+full_cmd=$3
+
+work_mode="RUN" # RUN/PRINT/DEBUG
+tmp_file="$HOME/.tmp_ate_config"
+phy_idx=$(echo ${interface} | tr -dc '0-9')
+
+cmd=$(echo ${full_cmd} | sed s/=/' '/g | cut -d " " -f 1)
+param=$(echo ${full_cmd} | sed s/=/' '/g | cut -d " " -f 2)
+
+function do_cmd() {
+    case ${work_mode} in
+        "RUN")
+            eval "$1"
+            ;;
+        "PRINT")
+            echo "$1"
+            ;;
+        "DEBUG")
+            eval "$1"
+            echo "$1"
+            ;;
+    esac
+}
+
+function record_config() {
+    if [ -f ${tmp_file} ]; then
+        if grep -q $1 ${tmp_file}; then
+            sed -i "/$1/c\\$1=$2" ${tmp_file}
+        else
+            echo "$1=$2" >> ${tmp_file}
+        fi
+    else
+        echo "$1=$2" >> ${tmp_file}
+    fi
+}
+
+function get_config() {
+    echo "$(cat ${tmp_file} | grep $1 | sed s/=/' '/g | cut -d " " -f 2)"
+}
+
+function do_ate_work() {
+    local ate_cmd=$1
+
+    case ${ate_cmd} in
+        "ATESTART")
+            local if_str=$(ifconfig | grep mon${phy_idx})
+
+            if [ ! -z "${if_str}" -a "${if_str}" != " " ]; then
+                echo "ATE already starts."
+            else
+                do_cmd "iw phy ${interface} interface add mon${phy_idx} type monitor"
+                do_cmd "iw dev wlan${phy_idx} del"
+                do_cmd "ifconfig mon${phy_idx} up"
+            fi
+            ;;
+        "ATESTOP")
+            local if_str=$(ifconfig | grep mon${phy_idx})
+
+            if [ -z "${if_str}" -a "${if_str}" != " " ]; then
+                echo "ATE does not start."
+            else
+                do_cmd "mt76-test ${interface} set state=off"
+                do_cmd "iw dev mon${phy_idx} del"
+                do_cmd "iw phy ${interface} interface add wlan${phy_idx} type managed"
+                do_cmd "ifconfig wlan${phy_idx} up"
+            fi
+            ;;
+        "TXFRAME")
+            do_cmd "mt76-test ${interface} set state=tx_frames"
+            ;;
+        "TXSTOP")
+            do_cmd "mt76-test ${interface} set state=idle"
+            ;;
+        "RXFRAME")
+            do_cmd "mt76-test ${interface} set state=rx_frames"
+            ;;
+        "RXSTOP")
+            do_cmd "mt76-test ${interface} set state=idle"
+            ;;
+        "TXCONT")
+            do_cmd "mt76-test ${interface} set state=tx_cont"
+            ;;
+    esac
+}
+
+function simple_convert() {
+    if [ "$1" = "ATETXCNT" ]; then
+        echo "tx_count"
+    elif [ "$1" = "ATETXLEN" ]; then
+        echo "tx_length"
+    elif [ "$1" = "ATETXMCS" ]; then
+        echo "tx_rate_idx"
+    elif [ "$1" = "ATEVHTNSS" ]; then
+        echo "tx_rate_nss"
+    elif [ "$1" = "ATETXLDPC" ]; then
+        echo "tx_rate_ldpc"
+    elif [ "$1" = "ATETXSTBC" ]; then
+        echo "tx_rate_stbc"
+    elif [ "$1" = "ATEPKTTXTIME" ]; then
+        echo "tx_time"
+    elif [ "$1" = "ATEIPG" ]; then
+        echo "tx_ipg"
+    elif [ "$1" = "ATEDUTYCYCLE" ]; then
+        echo "tx_duty_cycle"
+    elif [ "$1" = "ATETXFREQOFFSET" ]; then
+        echo "freq_offset"
+    else
+        echo "unknown"
+    fi
+}
+
+function convert_tx_mode() {
+    if [ "$1" = "0" ]; then
+        echo "cck"
+    elif [ "$1" = "1" ]; then
+        echo "ofdm"
+    elif [ "$1" = "2" ]; then
+        echo "ht"
+    elif [ "$1" = "4" ]; then
+        echo "vht"
+    elif [ "$1" = "8" ]; then
+        echo "he_su"
+    elif [ "$1" = "9" ]; then
+        echo "he_er"
+    elif [ "$1" = "10" ]; then
+        echo "he_tb"
+    elif [ "$1" = "11" ]; then
+        echo "he_mu"
+    else
+        echo "unknown"
+    fi
+}
+
+function convert_gi {
+    local tx_mode=$1
+    local val=$2
+    local sgi="0"
+    local he_ltf="0"
+
+    case ${tx_mode} in
+        "ht"|"vht")
+            sgi=${val}
+            ;;
+        "he_su"|"he_er")
+            case ${val} in
+                "0")
+                    ;;
+                "1")
+                    he_ltf="1"
+                    ;;
+                "2")
+                    sgi="1"
+                    he_ltf="1"
+                    ;;
+                "3")
+                    sgi="2"
+                    he_ltf="2"
+                    ;;
+                "4")
+                    he_ltf="2"
+                    ;;
+                *)
+                    echo "unknown gi"
+            esac
+            ;;
+        "he_mu")
+            case ${val} in
+                "0")
+                    he_ltf="2"
+                    ;;
+                "1")
+                    he_ltf="1"
+                    ;;
+                "2")
+                    sgi="1"
+                    he_ltf="1"
+                    ;;
+                "3")
+                    sgi="2"
+                    he_ltf="2"
+                    ;;
+                *)
+                    echo "unknown gi"
+            esac
+            ;;
+        "he_tb")
+            case ${val} in
+                "0")
+                    sgi="1"
+                    ;;
+                "1")
+                    sgi="1"
+                    he_ltf="1"
+                    ;;
+                "2")
+                    sgi="2"
+                    he_ltf="2"
+                    ;;
+                *)
+                    echo "unknown gi"
+            esac
+            ;;
+        *)
+            echo "unknown tx_rate_mode, can't transform gi"
+    esac
+
+    do_cmd "mt76-test ${interface} set tx_rate_sgi=${sgi} tx_ltf=${he_ltf}"
+}
+
+function convert_channel {
+    local band=$(echo $1 | sed s/:/' '/g | cut -d " " -f 2)
+    local ch=$(echo $1 | sed s/:/' '/g | cut -d " " -f 1)
+    local bw=$(get_config "ATETXBW" | cut -d ":" -f 1)
+
+    if [ "${band}" = "0" ]; then
+        case ${bw} in
+            "1")
+                if [ "${ch}" -ge "1" ] && [ "${ch}" -le "7" ]; then
+                    local bw_str="HT40+"
+                else
+                    local bw_str="HT40-"
+                fi
+                ;;
+            "0")
+                local bw_str="HT20"
+                ;;
+        esac
+    else
+        case ${bw} in
+            "2")
+                local bw_str="80MHz"
+                ;;
+            "1")
+                if [ "${ch}" == "36" ] || [ "${ch}" == "44" ] || [ "${ch}" == "52" ] || [ "${ch}" == "60" ] || \
+                   [ "${ch}" == "100" ] || [ "${ch}" == "108" ] || [ "${ch}" == "116" ] || [ "${ch}" == "124" ] || \
+                   [ "${ch}" == "132" ] || [ "${ch}" == "132" ] || [ "${ch}" == "140" ] || [ "${ch}" == "149" ] || \
+                   [ "${ch}" == "157" ]
+                then
+                    local bw_str="HT40+"
+                else
+                    local bw_str="HT40-"
+                fi
+                ;;
+            "0")
+                local bw_str="HT20"
+                ;;
+        esac
+    fi
+
+    do_cmd "iw dev mon${phy_idx} set channel ${ch} ${bw_str}"
+}
+
+if [ "${cmd_type}" = "set" ]; then
+    skip=0
+    use_ated=0
+    case ${cmd} in
+        "ATE")
+            do_ate_work ${param}
+
+            skip=1
+            ;;
+        "ATETXCNT"|"ATETXLEN"|"ATETXMCS"|"ATEVHTNSS"|"ATETXLDPC"|"ATETXSTBC"| \
+        "ATEPKTTXTIME"|"ATEIPG"|"ATEDUTYCYCLE"|"ATETXFREQOFFSET")
+            cmd_new=$(simple_convert ${cmd})
+            param_new=${param}
+            ;;
+        "ATETXANT"|"ATERXANT")
+            cmd_new="tx_antenna"
+            param_new=${param}
+            ;;
+        "ATETXGI")
+            tx_mode=$(convert_tx_mode $(get_config "ATETXMODE"))
+            convert_gi ${tx_mode} ${param}
+            skip=1
+            ;;
+        "ATETXMODE")
+            cmd_new="tx_rate_mode"
+            param_new=$(convert_tx_mode ${param})
+            record_config ${cmd} ${param}
+            ;;
+        "ATETXPOW0"|"ATETXPOW1"|"ATETXPOW2"|"ATETXPOW3")
+            cmd_new="tx_power"
+            param_new="${param},0,0,0"
+            ;;
+        "ATETXBW")
+            record_config ${cmd} ${param}
+            skip=1
+            ;;
+        "ATECHANNEL")
+            convert_channel ${param}
+            skip=1
+            ;;
+        "ATECTRLBANDIDX")
+            echo "Unused command, please use phy0/phy1 to switch"
+            skip=1
+            ;;
+        "bufferMode")
+            if [ "${param}" = "2" ]; then
+                do_cmd "ated -i ${interface} -c \"eeprom update\""
+            fi
+            skip=1
+            ;;
+        *)
+            echo "Unknown command to set"
+            skip=1
+    esac
+
+    if [ "${skip}" != "1" ]; then
+        do_cmd "mt76-test ${interface} set ${cmd_new}=${param_new}"
+    fi
+elif [ "${cmd_type}" = "show" ]; then
+    do_cmd "mt76-test ${interface} dump"
+    do_cmd "mt76-test ${interface} dump stats"
+elif [ "${cmd_type}" = "e2p" ]; then
+    v1=$(do_cmd "ated -i ${interface} -c \"eeprom read ${param}\"")
+    v1=$(echo "${v1}" | grep "val =" | cut -d '(' -f 2 | grep -o -E '[0-9]+')
+
+    param2=$(expr ${param} + "1")
+    v2=$(do_cmd "ated -i ${interface} -c \"eeprom read ${param2}\"")
+    v2=$(echo "${v2}" | grep "val =" | cut -d '(' -f 2 | grep -o -E '[0-9]+')
+    printf "[0x%04x]:0x%02x%02x\n" ${param} ${v2} ${v1}
+else
+    echo "Unknown command"
+fi
+
diff --git a/feed/atenl/src/CMakeLists.txt b/feed/atenl/src/CMakeLists.txt
new file mode 100644
index 0000000..cc91b05
--- /dev/null
+++ b/feed/atenl/src/CMakeLists.txt
@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 2.8)
+
+PROJECT(atenl C)
+ADD_DEFINITIONS(-Os -Wall --std=gnu99 -g3)
+
+ADD_EXECUTABLE(atenl main.c eth.c hqa.c nl.c eeprom.c util.c)
+TARGET_LINK_LIBRARIES(atenl nl-tiny)
+
+SET(CMAKE_INSTALL_PREFIX /usr)
+
+INSTALL(TARGETS atenl
+	RUNTIME DESTINATION sbin
+)
diff --git a/feed/atenl/src/atenl.h b/feed/atenl/src/atenl.h
new file mode 100644
index 0000000..ea8e4a0
--- /dev/null
+++ b/feed/atenl/src/atenl.h
@@ -0,0 +1,397 @@
+/* Copyright (C) 2021-2022 Mediatek Inc. */
+#ifndef __ATENL_H
+#define __ATENL_H
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/nl80211.h>
+#include <net/if.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "nl.h"
+#include "util.h"
+
+/* #define CONFIG_ATENL_DEBUG     1 */
+/* #define CONFIG_ATENL_DEBUG_VERBOSE     1 */
+#define BRIDGE_NAME	"br-lan"
+#define ETH_P_RACFG	0x2880
+#define RACFG_PKT_MAX_SIZE	1600
+#define RACFG_HLEN	12
+#define RACFG_MAGIC_NO	0x18142880
+
+#define RACFG_CMD_TYPE_MASK	GENMASK(14, 0)
+#define RACFG_CMD_TYPE_ETHREQ	BIT(3)
+#define RACFG_CMD_TYPE_PLATFORM_MODULE	GENMASK(4, 3)
+
+#define atenl_info(fmt, ...)	printf(fmt, __VA_ARGS__)
+#define atenl_err(fmt, ...)	fprintf(stderr, fmt, __VA_ARGS__)
+#ifdef CONFIG_ATENL_DEBUG
+#define atenl_dbg(fmt, ...)	atenl_info(fmt, __VA_ARGS__)
+#else
+#define atenl_dbg(fmt, ...)
+#endif
+
+#define set_band_val(_an, _band, _field, _val)	\
+	_an->anb[_band]._field = (_val)
+#define get_band_val(_an, _band, _field)	\
+	(_an->anb[_band]._field)
+
+enum atenl_rf_mode {
+	ATENL_RF_MODE_NORMAL,
+	ATENL_RF_MODE_TEST,
+	ATENL_RF_MODE_ICAP,
+	ATENL_RF_MODE_ICAP_OVERLAP,
+
+	__ATENL_RF_MODE_MAX,
+};
+
+struct atenl_rx_stat {
+	u64 total;
+	u64 ok_cnt;
+	u64 err_cnt;
+	u64 len_mismatch;
+};
+
+struct atenl_band {
+	bool valid;
+	u8 phy_idx;
+	u8 cap;
+	u8 chainmask;
+
+	enum mt76_testmode_state cur_state;
+	s8 tx_power;
+	enum atenl_rf_mode rf_mode;
+
+	bool use_tx_time;
+
+	bool reset_tx_cnt;
+	bool reset_rx_cnt;
+
+	/* history */
+	struct atenl_rx_stat rx_stat;
+};
+
+#define MAX_BAND_NUM	4
+
+struct atenl {
+	struct atenl_band anb[MAX_BAND_NUM];
+	u16 chip_id;
+
+	u8 cur_band;
+
+	u8 mac_addr[ETH_ALEN];
+	bool unicast;
+	int sock_eth;
+	int pipefd[2];
+	int child_pid;
+
+	const char *mtd_part;
+	u32 mtd_offset;
+	u8 *eeprom_data;
+	int eeprom_fd;
+	u16 eeprom_size;
+	bool eeprom_exist;
+
+	bool cmd_mode;
+};
+
+struct atenl_cmd_hdr {
+	__be32 magic_no;
+	__be16 cmd_type;
+	__be16 cmd_id;
+	__be16 len;
+	__be16 seq;
+	u8 data[2048];
+} __attribute__((packed));
+
+enum atenl_cmd {
+	HQA_CMD_UNKNOWN,
+	HQA_CMD_LEGACY,	/* legacy or deprecated */
+
+	HQA_CMD_OPEN_ADAPTER,
+	HQA_CMD_CLOSE_ADAPTER,
+	HQA_CMD_GET_CHIP_ID,
+	HQA_CMD_GET_SUB_CHIP_ID,
+	HQA_CMD_SET_TX_BW,
+	HQA_CMD_SET_TX_PKT_BW,
+	HQA_CMD_SET_TX_PRI_BW,
+	HQA_CMD_GET_TX_INFO,
+	HQA_CMD_SET_TX_PATH,
+	HQA_CMD_SET_TX_POWER,
+	HQA_CMD_SET_TX_POWER_MANUAL,
+	HQA_CMD_SET_RF_MODE,
+	HQA_CMD_SET_RX_PATH,
+	HQA_CMD_SET_RX_PKT_LEN,
+	HQA_CMD_SET_FREQ_OFFSET,
+	HQA_CMD_SET_TSSI,
+	HQA_CMD_SET_CFG,
+	HQA_CMD_SET_RU,
+	HQA_CMD_SET_BAND,
+	HQA_CMD_READ_MAC_BBP_REG,
+	HQA_CMD_READ_RF_REG,
+	HQA_CMD_READ_EEPROM_BULK,
+	HQA_CMD_READ_TEMPERATURE,
+	HQA_CMD_WRITE_MAC_BBP_REG,
+	HQA_CMD_WRITE_RF_REG,
+	HQA_CMD_WRITE_EEPROM_BULK,
+	HQA_CMD_WRITE_BUFFER_DONE,
+	HQA_CMD_GET_BAND,
+	HQA_CMD_GET_CFG,
+	HQA_CMD_GET_TX_POWER,
+	HQA_CMD_GET_TX_TONE_POWER,
+	HQA_CMD_GET_EFUSE_FREE_BLOCK,
+	HQA_CMD_GET_FREQ_OFFSET,
+	HQA_CMD_GET_FW_INFO,
+	HQA_CMD_GET_RX_INFO,
+	HQA_CMD_GET_RF_CAP,
+	HQA_CMD_CHECK_EFUSE_MODE,
+	HQA_CMD_CHECK_EFUSE_MODE_TYPE,
+	HQA_CMD_CHECK_EFUSE_MODE_NATIVE,
+	HQA_CMD_ANT_SWAP_CAP,
+	HQA_CMD_RESET_TX_RX_COUNTER,
+	HQA_CMD_CONTINUOUS_TX,
+
+	HQA_CMD_EXT,
+	HQA_CMD_ERR,
+
+	__HQA_CMD_MAX_NUM,
+};
+
+enum atenl_ext_cmd {
+	HQA_EXT_CMD_UNSPEC,
+
+	HQA_EXT_CMD_SET_CHANNEL,
+	HQA_EXT_CMD_SET_TX,
+	HQA_EXT_CMD_START_TX,
+	HQA_EXT_CMD_START_RX,
+	HQA_EXT_CMD_STOP_TX,
+	HQA_EXT_CMD_STOP_RX,
+	HQA_EXT_CMD_SET_TX_TIME_OPT,
+
+	HQA_EXT_CMD_OFF_CH_SCAN,
+
+	HQA_EXT_CMD_IBF_SET_VAL,
+	HQA_EXT_CMD_IBF_GET_STATUS,
+	HQA_EXT_CMD_IBF_PROF_UPDATE_ALL,
+
+	HQA_EXT_CMD_ERR,
+
+	__HQA_EXT_CMD_MAX_NUM,
+};
+
+struct atenl_data {
+	u8 buf[RACFG_PKT_MAX_SIZE];
+	int len;
+	enum atenl_cmd cmd;
+	u32 ext_id;
+	enum atenl_ext_cmd ext_cmd;
+};
+
+struct atenl_cmd_ops {
+	u16 resp_len;
+	int (*ops)(struct atenl *an, struct atenl_data *data);
+};
+
+static inline struct atenl_cmd_hdr * atenl_hdr(struct atenl_data *data)
+{
+	u8 *hqa_data = (u8 *)data->buf + ETH_HLEN;
+
+	return (struct atenl_cmd_hdr *)hqa_data;
+}
+
+static inline void
+atenl_dbg_print_data(struct atenl_data *data, const char *func_name, u32 len)
+{
+#ifdef CONFIG_ATENL_DEBUG_VERBOSE
+	u32 *tmp = (u32 *)data->buf;
+	int i;
+
+	for (i = 0; i < DIV_ROUND_UP(len, 4); i++)
+		atenl_dbg("%s: [%d] = 0x%08x\n", func_name, i, tmp[i]);
+#endif
+}
+
+enum atenl_phy_type {
+	ATENL_PHY_TYPE_CCK,
+	ATENL_PHY_TYPE_OFDM,
+	ATENL_PHY_TYPE_HT,
+	ATENL_PHY_TYPE_HT_GF,
+	ATENL_PHY_TYPE_VHT,
+	ATENL_PHY_TYPE_HE_SU = 8,
+	ATENL_PHY_TYPE_HE_EXT_SU,
+	ATENL_PHY_TYPE_HE_TB,
+	ATENL_PHY_TYPE_HE_MU,
+};
+
+enum atenl_e2p_mode {
+	E2P_EFUSE_MODE = 1,
+	E2P_FLASH_MODE,
+	E2P_EEPROM_MODE,
+	E2P_BIN_MODE,
+};
+
+enum atenl_band_type {
+	BAND_TYPE_UNUSE,
+	BAND_TYPE_2G,
+	BAND_TYPE_5G,
+	BAND_TYPE_2G_5G,
+	BAND_TYPE_6G,
+	BAND_TYPE_2G_6G,
+	BAND_TYPE_5G_6G,
+	BAND_TYPE_2G_5G_6G,
+};
+
+enum atenl_ch_band {
+	CH_BAND_2GHZ,
+	CH_BAND_5GHZ,
+	CH_BAND_6GHZ,
+};
+
+/* for mt7915 */
+enum {
+	MT_EE_BAND_SEL_DEFAULT,
+	MT_EE_BAND_SEL_5GHZ,
+	MT_EE_BAND_SEL_2GHZ,
+	MT_EE_BAND_SEL_DUAL,
+};
+
+/* for mt7916/mt7986 */
+enum {
+	MT_EE_BAND_SEL_2G,
+	MT_EE_BAND_SEL_5G,
+	MT_EE_BAND_SEL_6G,
+	MT_EE_BAND_SEL_5G_6G,
+};
+
+#define MT_EE_WIFI_CONF				0x190
+#define MT_EE_WIFI_CONF0_BAND_SEL		GENMASK(7, 6)
+
+enum {
+	MT7976_ONE_ADIE_DBDC		= 0x7,
+	MT7975_ONE_ADIE_SINGLE_BAND	= 0x8, /* AX7800 */
+	MT7976_ONE_ADIE_SINGLE_BAND	= 0xa, /* AX7800 */
+	MT7975_DUAL_ADIE_DBDC		= 0xd, /* AX6000 */
+	MT7976_DUAL_ADIE_DBDC		= 0xf, /* AX6000 */
+};
+
+enum {
+	TEST_CBW_20MHZ,
+	TEST_CBW_40MHZ,
+	TEST_CBW_80MHZ,
+	TEST_CBW_10MHZ,
+	TEST_CBW_5MHZ,
+	TEST_CBW_160MHZ,
+	TEST_CBW_8080MHZ,
+
+	TEST_CBW_MAX = TEST_CBW_8080MHZ - 1,
+};
+
+struct atenl_rx_info_hdr {
+	__be32 type;
+	__be32 ver;
+	__be32 val;
+	__be32 len;
+} __attribute__((packed));
+
+struct atenl_rx_info_band {
+	__be32 mac_rx_fcs_err_cnt;
+	__be32 mac_rx_mdrdy_cnt;
+	__be32 mac_rx_len_mismatch;
+	__be32 mac_rx_fcs_ok_cnt;
+	__be32 phy_rx_fcs_err_cnt_cck;
+	__be32 phy_rx_fcs_err_cnt_ofdm;
+	__be32 phy_rx_pd_cck;
+	__be32 phy_rx_pd_ofdm;
+	__be32 phy_rx_sig_err_cck;
+	__be32 phy_rx_sfd_err_cck;
+	__be32 phy_rx_sig_err_ofdm;
+	__be32 phy_rx_tag_err_ofdm;
+	__be32 phy_rx_mdrdy_cnt_cck;
+	__be32 phy_rx_mdrdy_cnt_ofdm;
+} __attribute__((packed));
+
+struct atenl_rx_info_path {
+	__be32 rcpi;
+	__be32 rssi;
+	__be32 fagc_ib_rssi;
+	__be32 fagc_wb_rssi;
+	__be32 inst_ib_rssi;
+	__be32 inst_wb_rssi;
+} __attribute__((packed));
+
+struct atenl_rx_info_user {
+	__be32 freq_offset;
+	__be32 snr;
+	__be32 fcs_error_cnt;
+} __attribute__((packed));
+
+struct atenl_rx_info_comm {
+	__be32 rx_fifo_full;
+	__be32 aci_hit_low;
+	__be32 aci_hit_high;
+	__be32 mu_pkt_count;
+	__be32 sig_mcs;
+	__be32 sinr;
+	__be32 driver_rx_count;
+} __attribute__((packed));
+
+enum atenl_ibf_action {
+	TXBF_ACT_INIT = 1,
+	TXBF_ACT_CHANNEL,
+	TXBF_ACT_MCS,
+	TXBF_ACT_POWER,
+	TXBF_ACT_TX_ANT,
+	TXBF_ACT_RX_START,
+	TXBF_ACT_RX_ANT,
+	TXBF_ACT_LNA_GAIN,
+	TXBF_ACT_IBF_PHASE_COMP,
+	TXBF_ACT_TX_PKT,
+	TXBF_ACT_IBF_PROF_UPDATE,
+	TXBF_ACT_EBF_PROF_UPDATE,
+	TXBF_ACT_IBF_PHASE_CAL,
+	TXBF_ACT_IBF_PHASE_E2P_UPDATE = 16,
+};
+
+static inline bool is_mt7915(struct atenl *an)
+{
+	return an->chip_id == 0x7915;
+}
+
+static inline bool is_mt7916(struct atenl *an)
+{
+	return (an->chip_id == 0x7916) || (an->chip_id == 0x7906);
+}
+
+static inline bool is_mt7986(struct atenl *an)
+{
+	return an->chip_id == 0x7986;
+}
+
+int atenl_eth_init(struct atenl *an);
+int atenl_eth_recv(struct atenl *an, struct atenl_data *data);
+int atenl_eth_send(struct atenl *an, struct atenl_data *data);
+int atenl_hqa_recv(struct atenl *an, struct atenl_data *data);
+int atenl_hqa_proc_cmd(struct atenl *an, struct atenl_data *data);
+int atenl_nl_process(struct atenl *an, struct atenl_data *data);
+int atenl_nl_process_many(struct atenl *an, struct atenl_data *data);
+int atenl_nl_check_mtd(struct atenl *an);
+int atenl_nl_write_eeprom(struct atenl *an, u32 offset, u8 *val, int len);
+int atenl_nl_write_efuse_all(struct atenl *an, struct atenl_data *data);
+int atenl_nl_update_buffer_mode(struct atenl *an);
+int atenl_nl_set_state(struct atenl *an, u8 band,
+		       enum mt76_testmode_state state);
+int atenl_eeprom_init(struct atenl *an, u8 phy_idx);
+void atenl_eeprom_close(struct atenl *an);
+int atenl_eeprom_write_mtd(struct atenl *an);
+int atenl_eeprom_read_from_driver(struct atenl *an, u32 offset, int len);
+void atenl_eeprom_cmd_handler(struct atenl *an, u8 phy_idx, char *cmd);
+u16 atenl_get_center_channel(u8 bw, u8 ch_band, u16 ctrl_ch);
+int atenl_reg_read(struct atenl *an, u32 offset, u32 *res);
+int atenl_reg_write(struct atenl *an, u32 offset, u32 val);
+
+#endif
diff --git a/feed/atenl/src/eeprom.c b/feed/atenl/src/eeprom.c
new file mode 100644
index 0000000..2a46971
--- /dev/null
+++ b/feed/atenl/src/eeprom.c
@@ -0,0 +1,456 @@
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "atenl.h"
+
+#define EEPROM_PART_SIZE 20480
+#define EEPROM_FILE	"/tmp/atenl-eeprom"
+
+static FILE *mtd_open(const char *mtd)
+{
+	char line[128], name[64];
+	FILE *fp;
+	int i;
+
+	fp = fopen("/proc/mtd", "r");
+	if (!fp)
+		return NULL;
+
+	snprintf(name, sizeof(name), "\"%s\"", mtd);
+	while (fgets(line, sizeof(line), fp)) {
+		if (!sscanf(line, "mtd%d:", &i) || !strstr(line, name))
+			continue;
+
+		snprintf(line, sizeof(line), "/dev/mtd%d", i);
+		fclose(fp);
+		return fopen(line, "r");
+	}
+	fclose(fp);
+
+	return NULL;
+}
+
+static int
+atenl_flash_create_file(struct atenl *an)
+{
+	char buf[1024];
+	ssize_t len;
+	FILE *f;
+	int fd, ret;
+
+	f = mtd_open(an->mtd_part);
+	if (!f) {
+		fprintf(stderr, "Failed to open MTD device\n");
+		return -1;
+	}
+
+	fd = open(EEPROM_FILE, O_RDWR | O_CREAT | O_EXCL, 00644);
+	if (fd < 0)
+		goto out;
+
+	while ((len = fread(buf, 1, sizeof(buf), f)) > 0) {
+		ssize_t w;
+
+retry:
+		w = write(fd, buf, len);
+		if (w > 0)
+			continue;
+
+		if (errno == EINTR)
+			goto retry;
+
+		perror("write");
+		unlink(EEPROM_FILE);
+		close(fd);
+		fd = -1;
+		goto out;
+	}
+
+	ret = lseek(fd, 0, SEEK_SET);
+	if (ret) {
+		fclose(f);
+		close(fd);
+		return ret;
+	}
+
+out:
+	fclose(f);
+	return fd;
+}
+
+static int
+atenl_efuse_create_file(struct atenl *an)
+{
+	char fname[64], buf[1024];
+	ssize_t len;
+	int fd_ori, fd, ret;
+
+	snprintf(fname, sizeof(fname),
+		"/sys/kernel/debug/ieee80211/phy%d/mt76/eeprom", get_band_val(an, 0, phy_idx));
+	fd_ori = open(fname, O_RDONLY);
+	if (fd_ori < 0)
+		return -1;
+
+	fd = open(EEPROM_FILE, O_RDWR | O_CREAT | O_EXCL, 00644);
+	if (fd < 0)
+		goto out;
+
+	while ((len = read(fd_ori, buf, sizeof(buf))) > 0) {
+		ssize_t w;
+
+retry:
+		w = write(fd, buf, len);
+		if (w > 0)
+			continue;
+
+		if (errno == EINTR)
+			goto retry;
+
+		perror("write");
+		unlink(EEPROM_FILE);
+		close(fd);
+		fd = -1;
+		goto out;
+	}
+
+	ret = lseek(fd, 0, SEEK_SET);
+	if (ret) {
+		close(fd_ori);
+		close(fd);
+		return ret;
+	}
+
+out:
+	close(fd_ori);
+	return fd;
+}
+
+static bool
+atenl_eeprom_file_exists(void)
+{
+	struct stat st;
+
+	return stat(EEPROM_FILE, &st) == 0;
+}
+
+static int
+atenl_eeprom_init_file(struct atenl *an, bool flash_mode)
+{
+	int fd;
+
+	if (!atenl_eeprom_file_exists()) {
+		if (flash_mode)
+			atenl_dbg("[%d]%s: init eeprom with flash mode\n", getpid(), __func__);
+		else
+			atenl_dbg("[%d]%s: init eeprom with efuse mode\n", getpid(), __func__);
+
+		if (flash_mode)
+			return atenl_flash_create_file(an);
+
+		return atenl_efuse_create_file(an);
+	}
+
+	fd = open(EEPROM_FILE, O_RDWR);
+	if (fd < 0)
+		perror("open");
+
+	an->eeprom_exist = true;
+
+	return fd;
+}
+
+static void
+atenl_eeprom_init_max_size(struct atenl *an)
+{
+	switch (an->chip_id) {
+	case 0x7915:
+		an->eeprom_size = 3584;
+		break;
+	case 0x7906:
+	case 0x7916:
+	case 0x7986:
+		an->eeprom_size = 4096;
+		break;
+	default:
+		break;
+	}
+}
+
+static void
+atenl_eeprom_init_band_cap(struct atenl *an)
+{
+	u8 *eeprom = an->eeprom_data;
+
+	if (is_mt7915(an)) {
+		u8 val = eeprom[MT_EE_WIFI_CONF];
+		u8 band_sel = FIELD_GET(MT_EE_WIFI_CONF0_BAND_SEL, val);
+		struct atenl_band *anb = &an->anb[0];
+
+		/* MT7915A */
+		if (band_sel == MT_EE_BAND_SEL_DEFAULT) {
+			anb->valid = true;
+			anb->cap = BAND_TYPE_2G_5G;
+			return;
+		}
+
+		/* MT7915D */
+		if (band_sel == MT_EE_BAND_SEL_2GHZ) {
+			anb->valid = true;
+			anb->cap = BAND_TYPE_2G;
+		}
+
+		val = eeprom[MT_EE_WIFI_CONF + 1];
+		band_sel = FIELD_GET(MT_EE_WIFI_CONF0_BAND_SEL, val);
+		anb++;
+
+		if (band_sel == MT_EE_BAND_SEL_5GHZ) {
+			anb->valid = true;
+			anb->cap = BAND_TYPE_5G;
+		}
+	} else if (is_mt7916(an) || is_mt7986(an)) {
+		struct atenl_band *anb;
+		u8 val, band_sel;
+		int i;
+
+		for (i = 0; i < 2; i++) {
+			val = eeprom[MT_EE_WIFI_CONF + i];
+			band_sel = FIELD_GET(MT_EE_WIFI_CONF0_BAND_SEL, val);
+			anb = &an->anb[i];
+
+			anb->valid = true;
+			switch (band_sel) {
+			case MT_EE_BAND_SEL_2G:
+				anb->cap = BAND_TYPE_2G;
+				break;
+			case MT_EE_BAND_SEL_5G:
+				anb->cap = BAND_TYPE_5G;
+				break;
+			case MT_EE_BAND_SEL_6G:
+				anb->cap = BAND_TYPE_6G;
+				break;
+			case MT_EE_BAND_SEL_5G_6G:
+				anb->cap = BAND_TYPE_5G_6G;
+				break;
+			default:
+				break;
+			}
+		}
+	}
+}
+
+static void
+atenl_eeprom_init_antenna_cap(struct atenl *an)
+{
+	if (is_mt7915(an)) {
+		if (an->anb[0].cap == BAND_TYPE_2G_5G)
+			an->anb[0].chainmask = 0xf;
+		else {
+			an->anb[0].chainmask = 0x3;
+			an->anb[1].chainmask = 0xc;
+		}
+	} else if (is_mt7916(an)) {
+		an->anb[0].chainmask = 0x3;
+		an->anb[1].chainmask = 0x3;
+	} else if (is_mt7986(an)) {
+		an->anb[0].chainmask = 0xf;
+		an->anb[1].chainmask = 0xf;
+	}
+}
+
+int atenl_eeprom_init(struct atenl *an, u8 phy_idx)
+{
+	bool flash_mode;
+	int eeprom_fd;
+
+	set_band_val(an, 0, phy_idx, phy_idx);
+
+	atenl_nl_check_mtd(an);
+	flash_mode = an->mtd_part != NULL;
+
+	eeprom_fd = atenl_eeprom_init_file(an, flash_mode);
+	if (eeprom_fd < 0)
+		return -1;
+
+	an->eeprom_data = mmap(NULL, EEPROM_PART_SIZE, PROT_READ | PROT_WRITE,
+			       MAP_SHARED, eeprom_fd, an->mtd_offset);
+	if (!an->eeprom_data) {
+		perror("mmap");
+		close(eeprom_fd);
+		return -1;
+	}
+
+	an->eeprom_fd = eeprom_fd;
+	an->chip_id = *(u16 *)an->eeprom_data;
+	atenl_eeprom_init_max_size(an);
+	atenl_eeprom_init_band_cap(an);
+	atenl_eeprom_init_antenna_cap(an);
+
+	if (get_band_val(an, 1, valid))
+		set_band_val(an, 1, phy_idx, phy_idx + 1);
+
+	return 0;
+}
+
+void atenl_eeprom_close(struct atenl *an)
+{
+	msync(an->eeprom_data, EEPROM_PART_SIZE, MS_SYNC);
+	munmap(an->eeprom_data, EEPROM_PART_SIZE);
+	close(an->eeprom_fd);
+
+	if (!an->eeprom_exist && (an->child_pid || an->cmd_mode))
+		if (remove(EEPROM_FILE))
+			perror("remove");
+}
+
+int atenl_eeprom_write_mtd(struct atenl *an)
+{
+	bool flash_mode = an->mtd_part != NULL;
+	pid_t pid;
+
+	if (!flash_mode)
+		return 0;
+
+	pid = fork();
+	if (pid < 0) {
+		perror("Fork");
+		return EXIT_FAILURE;
+	} else if (pid == 0) {
+		char *part = strdup(an->mtd_part);
+		char *cmd[] = {"mtd", "write", EEPROM_FILE, part, NULL};
+		int ret;
+
+		ret = execvp("mtd", cmd);
+		if (ret < 0) {
+			fprintf(stderr, "%s: execl error\n", __func__);
+			exit(0);
+		}
+	} else {
+		wait(&pid);
+	}
+
+	return 0;
+}
+
+/* Directly read some value from driver's eeprom.
+ * It's usally used to get calibrated data from driver.
+ */
+int atenl_eeprom_read_from_driver(struct atenl *an, u32 offset, int len)
+{
+	u8 *eeprom_data = an->eeprom_data + offset;
+	char fname[64], buf[1024];
+	int fd_ori, ret;
+	ssize_t rd;
+
+	snprintf(fname, sizeof(fname),
+		"/sys/kernel/debug/ieee80211/phy%d/mt76/eeprom",
+		get_band_val(an, 0, phy_idx));
+	fd_ori = open(fname, O_RDONLY);
+	if (fd_ori < 0)
+		return -1;
+
+	ret = lseek(fd_ori, offset, SEEK_SET);
+	if (ret < 0)
+		goto out;
+
+	while ((rd = read(fd_ori, buf, sizeof(buf))) > 0 && len) {
+		if (len < rd) {
+			memcpy(eeprom_data, buf, len);
+			break;
+		} else {
+			memcpy(eeprom_data, buf, rd);
+			eeprom_data += rd;
+			len -= rd;
+		}
+	}
+
+	ret = 0;
+out:
+	close(fd_ori);
+	return ret;
+}
+
+/* Update all eeprom values to driver before writing efuse */
+static void
+atenl_eeprom_sync_to_driver(struct atenl *an)
+{
+	int i;
+
+	for (i = 0; i < 3584; i += 16)
+		atenl_nl_write_eeprom(an, i, &an->eeprom_data[i], 16);
+}
+
+void atenl_eeprom_cmd_handler(struct atenl *an, u8 phy_idx, char *cmd)
+{
+	bool flash_mode;
+
+	an->cmd_mode = true;
+
+	atenl_eeprom_init(an, phy_idx);
+	flash_mode = an->mtd_part != NULL;
+
+	if (!strncmp(cmd, "sync eeprom all", 15)) {
+		atenl_eeprom_write_mtd(an);
+	} else if (!strncmp(cmd, "eeprom", 6)) {
+		char *s = strchr(cmd, ' ');
+
+		if (!s) {
+			fprintf(stderr, "eeprom: please type a correct command\n");
+			return;
+		}
+
+		s++;
+		if (!strncmp(s, "reset", 5)) {
+			unlink(EEPROM_FILE);
+		} else if (!strncmp(s, "file", 4)) {
+			atenl_info("%s\n", EEPROM_FILE);
+			atenl_info("Flash mode: %d\n", flash_mode);
+		} else if (!strncmp(s, "set", 3)) {
+			u32 offset, val;
+
+			s = strchr(s, ' ');
+			if (!s)
+				return;
+			s++;
+
+			if (!sscanf(s, "%x=%x", &offset, &val) ||
+			    offset > EEPROM_PART_SIZE)
+				return;
+
+			an->eeprom_data[offset] = val;
+			atenl_info("set offset 0x%x to 0x%x\n", offset, val);
+		} else if (!strncmp(s, "update", 6)) {
+			atenl_nl_update_buffer_mode(an);
+		} else if (!strncmp(s, "write", 5)) {
+			s = strchr(s, ' ');
+			if (!s)
+				return;
+			s++;
+
+			if (!strncmp(s, "flash", 5)) {
+				atenl_eeprom_write_mtd(an);
+			} else if (!strncmp(s, "efuse", 5)) {
+				atenl_eeprom_sync_to_driver(an);
+				atenl_nl_write_efuse_all(an, NULL);
+			}
+		} else if (!strncmp(s, "read", 4)) {
+			u32 offset;
+
+			s = strchr(s, ' ');
+			if (!s)
+				return;
+			s++;
+
+			if (!sscanf(s, "%x", &offset) ||
+			    offset > EEPROM_PART_SIZE)
+				return;
+
+			atenl_info("val = 0x%x (%u)\n", an->eeprom_data[offset],
+							an->eeprom_data[offset]);
+		}
+	} else {
+		atenl_err("Unknown command: %s\n", cmd);
+	}
+}
diff --git a/feed/atenl/src/eth.c b/feed/atenl/src/eth.c
new file mode 100644
index 0000000..a58add3
--- /dev/null
+++ b/feed/atenl/src/eth.c
@@ -0,0 +1,109 @@
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+
+#include "atenl.h"
+
+int atenl_eth_init(struct atenl *an)
+{
+	struct sockaddr_ll addr = {};
+	struct ifreq ifr = {};
+	int ret;
+ 
+	memcpy(ifr.ifr_name, BRIDGE_NAME, strlen(BRIDGE_NAME));
+	ret = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_RACFG));
+	if (ret < 0) {
+		perror("socket");
+		goto out;
+	}
+	an->sock_eth = ret;
+
+	addr.sll_family = AF_PACKET;
+	addr.sll_ifindex = if_nametoindex(BRIDGE_NAME);
+
+	ret = bind(an->sock_eth, (struct sockaddr *)&addr, sizeof(addr));
+	if (ret < 0) {
+		perror("bind");
+		goto out;
+	}
+
+	ret = ioctl(an->sock_eth, SIOCGIFHWADDR, &ifr);
+	if (ret < 0) {
+		perror("ioctl(SIOCGIFHWADDR)");
+		goto out;
+	}
+
+	memcpy(an->mac_addr, ifr.ifr_hwaddr.sa_data, ETH_ALEN);
+	atenl_info("Open Ethernet socket success on %s, mac addr = " MACSTR "\n",
+		   BRIDGE_NAME, MAC2STR(an->mac_addr));
+
+	ret = 0;
+out:
+	return ret;
+}
+
+int atenl_eth_recv(struct atenl *an, struct atenl_data *data)
+{
+	char buf[RACFG_PKT_MAX_SIZE];
+	int len = recvfrom(an->sock_eth, buf, sizeof(buf), 0, NULL, NULL);
+	struct ethhdr *hdr = (struct ethhdr *)buf;
+
+	atenl_dbg("[%d]%s: recv len = %d\n", getpid(), __func__, len);
+
+	if (len >= ETH_HLEN + RACFG_HLEN) {
+		if (hdr->h_proto == htons(ETH_P_RACFG) &&
+		    (ether_addr_equal(an->mac_addr, hdr->h_dest) ||
+		     is_broadcast_ether_addr(hdr->h_dest))) {
+			data->len = len;
+			memcpy(data->buf, buf, len);
+
+			return 0;
+		}
+	}
+
+	atenl_err("%s: packet len is too short\n", __func__);
+	return -EINVAL;
+}
+
+int atenl_eth_send(struct atenl *an, struct atenl_data *data)
+{
+	struct ethhdr *ehdr = (struct ethhdr *)data->buf;
+	struct sockaddr_ll addr = {};
+	int ret, len = data->len;
+
+	if (an->unicast)
+		ether_addr_copy(ehdr->h_dest, ehdr->h_source);
+	else
+		eth_broadcast_addr(ehdr->h_dest);
+
+	ether_addr_copy(ehdr->h_source, an->mac_addr);
+	ehdr->h_proto = htons(ETH_P_RACFG);
+
+	if (len < 60)
+		len = 60;
+	else if (len > 1514) {
+		atenl_err("%s: response ethernet length is too long\n", __func__);
+		return -1;
+	}
+
+	atenl_dbg_print_data(data, __func__, len);
+
+	addr.sll_family = PF_PACKET;
+	addr.sll_protocol = htons(ETH_P_RACFG);
+	addr.sll_ifindex = if_nametoindex(BRIDGE_NAME);
+	addr.sll_pkttype = PACKET_BROADCAST;
+	addr.sll_hatype = ARPHRD_ETHER;
+	addr.sll_halen = ETH_ALEN;
+	memset(addr.sll_addr, 0, 8);
+	eth_broadcast_addr(addr.sll_addr);
+
+	ret = sendto(an->sock_eth, data->buf, len, 0,
+		     (struct sockaddr *)&addr, sizeof(addr));
+	if (ret < 0) {
+		perror("sendto");
+		return ret;
+	}
+	
+	atenl_dbg("[%d]%s: send length = %d\n", getpid(), __func__, len);
+
+	return 0;
+}
diff --git a/feed/atenl/src/hqa.c b/feed/atenl/src/hqa.c
new file mode 100644
index 0000000..24a6e9b
--- /dev/null
+++ b/feed/atenl/src/hqa.c
@@ -0,0 +1,1034 @@
+/* Copyright (C) 2021-2022 Mediatek Inc. */
+#include "atenl.h"
+
+#define CHAN(_ch, _freq, _ch_80, _ch_160, ...) {	\
+	.ch = _ch,				\
+	.freq = _freq,				\
+	.ch_80 = _ch_80,			\
+	.ch_160 = _ch_160,			\
+	__VA_ARGS__				\
+}
+
+struct atenl_channel {
+	/* ctrl ch */
+	u16 ch;
+	u16 freq;
+	/* center ch */
+	u16 ch_80;
+	u16 ch_160;
+	/* only use for channels that don't have 80 but has 40 */
+	u16 ch_40;
+};
+
+static const struct atenl_channel atenl_channels_5ghz[] = {
+	CHAN(8, 5040, 0, 0),
+	CHAN(12, 5060, 0, 0),
+	CHAN(16, 5080, 0, 0),
+
+	CHAN(36, 5180, 42, 50),
+	CHAN(40, 5200, 42, 50),
+	CHAN(44, 5220, 42, 50),
+	CHAN(48, 5240, 42, 50),
+
+	CHAN(52, 5260, 58, 50),
+	CHAN(56, 5280, 58, 50),
+	CHAN(60, 5300, 58, 50),
+	CHAN(64, 5320, 58, 50),
+
+	CHAN(68, 5340, 0, 0),
+	CHAN(80, 5400, 0, 0),
+	CHAN(84, 5420, 0, 0),
+	CHAN(88, 5440, 0, 0),
+	CHAN(92, 5460, 0, 0),
+	CHAN(96, 5480, 0, 0),
+
+	CHAN(100, 5500, 106, 114),
+	CHAN(104, 5520, 106, 114),
+	CHAN(108, 5540, 106, 114),
+	CHAN(112, 5560, 106, 114),
+	CHAN(116, 5580, 122, 114),
+	CHAN(120, 5600, 122, 114),
+	CHAN(124, 5620, 122, 114),
+	CHAN(128, 5640, 122, 114),
+
+	CHAN(132, 5660, 138, 0),
+	CHAN(136, 5680, 138, 0),
+	CHAN(140, 5700, 138, 0),
+	CHAN(144, 5720, 138, 0),
+
+	CHAN(149, 5745, 155, 0),
+	CHAN(153, 5765, 155, 0),
+	CHAN(157, 5785, 155, 0),
+	CHAN(161, 5805, 155, 0),
+	CHAN(165, 5825, 0, 0, .ch_40 = 167),
+	CHAN(169, 5845, 0, 0, .ch_40 = 167),
+	CHAN(173, 5865, 0, 0),
+
+	CHAN(184, 4920, 0, 0),
+	CHAN(188, 4940, 0, 0),
+	CHAN(192, 4960, 0, 0),
+	CHAN(196, 4980, 0, 0),
+};
+
+static const struct atenl_channel atenl_channels_6ghz[] = {
+	/* UNII-5 */
+	CHAN(1, 5955, 7, 15),
+	CHAN(5, 5975, 7, 15),
+	CHAN(9, 5995, 7, 15),
+	CHAN(13, 6015, 7, 15),
+	CHAN(17, 6035, 23, 15),
+	CHAN(21, 6055, 23, 15),
+	CHAN(25, 6075, 23, 15),
+	CHAN(29, 6095, 23, 15),
+	CHAN(33, 6115, 39, 47),
+	CHAN(37, 6135, 39, 47),
+	CHAN(41, 6155, 39, 47),
+	CHAN(45, 6175, 39, 47),
+	CHAN(49, 6195, 55, 47),
+	CHAN(53, 6215, 55, 47),
+	CHAN(57, 6235, 55, 47),
+	CHAN(61, 6255, 55, 47),
+	CHAN(65, 6275, 71, 79),
+	CHAN(69, 6295, 71, 79),
+	CHAN(73, 6315, 71, 79),
+	CHAN(77, 6335, 71, 79),
+	CHAN(81, 6355, 87, 79),
+	CHAN(85, 6375, 87, 79),
+	CHAN(89, 6395, 87, 79),
+	CHAN(93, 6415, 87, 79),
+	/* UNII-6 */
+	CHAN(97, 6435, 103, 111),
+	CHAN(101, 6455, 103, 111),
+	CHAN(105, 6475, 103, 111),
+	CHAN(109, 6495, 103, 111),
+	CHAN(113, 6515, 119, 111),
+	CHAN(117, 6535, 119, 111),
+	/* UNII-7 */
+	CHAN(121, 6555, 119, 111),
+	CHAN(125, 6575, 119, 111),
+	CHAN(129, 6595, 135, 143),
+	CHAN(133, 6615, 135, 143),
+	CHAN(137, 6635, 135, 143),
+	CHAN(141, 6655, 135, 143),
+	CHAN(145, 6675, 151, 143),
+	CHAN(149, 6695, 151, 143),
+	CHAN(153, 6715, 151, 143),
+	CHAN(157, 6735, 151, 143),
+	CHAN(161, 6755, 167, 175),
+	CHAN(165, 6775, 167, 175),
+	CHAN(169, 6795, 167, 175),
+	CHAN(173, 6815, 167, 175),
+	CHAN(177, 6835, 183, 175),
+	CHAN(181, 6855, 183, 175),
+	CHAN(185, 6875, 183, 175),
+	/* UNII-8 */
+	CHAN(189, 6895, 183, 175),
+	CHAN(193, 6915, 199, 207),
+	CHAN(197, 6935, 199, 207),
+	CHAN(201, 6955, 199, 207),
+	CHAN(205, 6975, 199, 207),
+	CHAN(209, 6995, 215, 207),
+	CHAN(213, 7015, 215, 207),
+	CHAN(217, 7035, 215, 207),
+	CHAN(221, 7055, 215, 207),
+	CHAN(225, 7075, 0, 0, .ch_40 = 227),
+	CHAN(229, 7095, 0, 0, .ch_40 = 227),
+	CHAN(233, 7115, 0, 0),
+};
+
+static int
+atenl_hqa_adapter(struct atenl *an, struct atenl_data *data)
+{
+	char cmd[64];
+	u8 i;
+
+	for (i = 0; i < MAX_BAND_NUM; i++) {
+		u8 phy = get_band_val(an, i, phy_idx);
+
+		if (!get_band_val(an, i, valid))
+			continue;
+
+		if (data->cmd == HQA_CMD_OPEN_ADAPTER) {
+			sprintf(cmd, "iw phy phy%u interface add mon%u type monitor", phy, phy);
+			system(cmd);
+			sprintf(cmd, "iw dev wlan%u del", phy);
+			system(cmd);
+			sprintf(cmd, "ifconfig mon%u up", phy);
+			system(cmd);
+			/* set a special-defined country */
+			sprintf(cmd, "iw reg set VV");
+			system(cmd);
+			atenl_nl_set_state(an, i, MT76_TM_STATE_IDLE);
+		} else {
+			atenl_nl_set_state(an, i, MT76_TM_STATE_OFF);
+			sprintf(cmd, "iw reg set 00");
+			system(cmd);
+			sprintf(cmd, "iw dev mon%u del", phy);
+			system(cmd);
+			sprintf(cmd, "iw phy phy%u interface add wlan%u type managed", phy, phy);
+			system(cmd);
+		}
+	}
+
+	return 0;
+}
+
+static int
+atenl_hqa_set_rf_mode(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+
+	/* The testmode rf mode change applies to all bands */
+	set_band_val(an, 0, rf_mode, ntohl(*(u32 *)hdr->data));
+	set_band_val(an, 1, rf_mode, ntohl(*(u32 *)hdr->data));
+
+	return 0;
+}
+
+static int
+atenl_hqa_get_chip_id(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+
+	*(u32 *)(hdr->data + 2) = htonl(an->chip_id);
+
+	return 0;
+}
+
+static int
+atenl_hqa_get_sub_chip_id(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+
+	if (is_mt7986(an)) {
+		u32 sub_id, val;
+		int ret;
+
+		ret = atenl_reg_read(an, 0x18050000, &val);
+		if (ret)
+			return ret;
+
+		switch (val & 0xf) {
+		case MT7975_ONE_ADIE_SINGLE_BAND:
+		case MT7976_ONE_ADIE_SINGLE_BAND:
+			sub_id = htonl(0xa);
+			break;
+		case MT7976_ONE_ADIE_DBDC:
+			sub_id = htonl(0x7);
+			break;
+		case MT7975_DUAL_ADIE_DBDC:
+		case MT7976_DUAL_ADIE_DBDC:
+		default:
+			sub_id = htonl(0xf);
+			break;
+		}
+
+		memcpy(hdr->data + 2, &sub_id, 4);
+	}
+
+	return 0;
+}
+
+static int
+atenl_hqa_get_rf_cap(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	u32 band = ntohl(*(u32 *)hdr->data);
+	struct atenl_band *anb;
+
+	if (band >= MAX_BAND_NUM)
+		return -EINVAL;
+
+	anb = &an->anb[band];
+	/* fill tx and rx ant */
+	*(u32 *)(hdr->data + 2) = htonl(__builtin_popcount(anb->chainmask));
+	*(u32 *)(hdr->data + 2 + 4) = *(u32 *)(hdr->data + 2);
+
+	return 0;
+}
+
+static int
+atenl_hqa_reset_counter(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_band *anb = &an->anb[an->cur_band];
+
+	anb->reset_tx_cnt = true;
+	anb->reset_rx_cnt = true;
+
+	memset(&anb->rx_stat, 0, sizeof(anb->rx_stat));
+
+	return 0;
+}
+
+static int
+atenl_hqa_mac_bbp_reg(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	enum atenl_cmd cmd = data->cmd;
+	u32 *v = (u32 *)hdr->data;
+	u32 offset = ntohl(v[0]);
+	int ret;
+
+	if (cmd == HQA_CMD_READ_MAC_BBP_REG) {
+		u16 num = ntohs(*(u16 *)(hdr->data + 4));
+		u32 *ptr = (u32 *)(hdr->data + 2);
+		u32 res;
+		int i;
+
+		if (num > SHRT_MAX) {
+			ret = -EINVAL;
+			goto out;
+		}
+
+		hdr->len = htons(2 + num * 4);
+		for (i = 0; i < num && i < sizeof(hdr->data) / 4; i++) {
+			ret = atenl_reg_read(an, offset + i * 4, &res);
+			if (ret)
+				goto out;
+
+			res = htonl(res);
+			memcpy(ptr + i, &res, 4);
+		}
+	} else {
+		u32 val = ntohl(v[1]);
+
+		ret = atenl_reg_write(an, offset, val);
+		if (ret)
+			goto out;
+	}
+
+	ret = 0;
+out:
+	memset(hdr->data, 0, 2);
+
+	return ret;
+}
+
+static int
+atenl_hqa_eeprom_bulk(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	enum atenl_cmd cmd = data->cmd;
+
+	if (cmd == HQA_CMD_WRITE_BUFFER_DONE) {
+		u32 buf_mode = ntohl(*(u32 *)hdr->data);
+
+		switch (buf_mode) {
+		case E2P_EFUSE_MODE:
+			atenl_nl_write_efuse_all(an, data);
+			break;
+		default:
+			break;
+		}
+	} else {
+		u16 *v = (u16 *)hdr->data;
+		u16 offset = ntohs(v[0]), len = ntohs(v[1]);
+		u16 val;
+		size_t i;
+
+		if (offset >= an->eeprom_size || (len > sizeof(hdr->data) - 2))
+			return -EINVAL;
+
+		if (cmd == HQA_CMD_READ_EEPROM_BULK) {
+			hdr->len = htons(len + 2);
+			for (i = 0; i < len; i += 2) {
+				if (offset + i >= an->eeprom_size)
+					val = 0;
+				else
+					val = ntohs(*(u16 *)(an->eeprom_data + offset + i));
+				*(u16 *)(hdr->data + 2 + i) = val;
+			}
+		} else { /* write eeprom */
+			u16 offset_a;
+
+			for (i = 0; i < DIV_ROUND_UP(len, 2); i++) {
+				val = ntohs(v[i + 2]);
+				memcpy(&an->eeprom_data[offset + i * 2], &val, 2);
+			}
+
+			offset_a = offset - (offset % 16);
+			len += (offset - offset_a);
+			for (i = offset_a; i < offset_a + len; i += 16)
+				atenl_nl_write_eeprom(an, i, &an->eeprom_data[i], 16);
+
+			atenl_eeprom_write_mtd(an);
+		}
+	}
+
+	return 0;
+}
+
+static int
+atenl_hqa_get_efuse_free_block(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	u32 free_block = htonl(0x14);
+
+	/* TODO */
+	*(u32 *)(hdr->data + 2) = free_block;
+
+	return 0;
+}
+
+static int
+atenl_hqa_get_band(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	u32 band = ntohl(*(u32 *)hdr->data);
+
+	if (band >= MAX_BAND_NUM)
+		return -EINVAL;
+
+	*(u32 *)(hdr->data + 2) = htonl(an->anb[band].cap);
+
+	return 0;
+}
+
+static int
+atenl_hqa_get_tx_power(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	u32 tx_power = htonl(28);
+
+	memcpy(hdr->data + 6, &tx_power, 4);
+
+	return 0;
+}
+
+static int
+atenl_hqa_get_freq_offset(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	u32 freq_offset = htonl(10);
+
+	/* TODO */
+	memcpy(hdr->data + 2, &freq_offset, 4);
+
+	return 0;
+}
+
+static int
+atenl_hqa_get_cfg(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	u32 val = htonl(1);
+
+	/* TODO */
+	memcpy(hdr->data + 2, &val, 4);
+
+	return 0;
+}
+
+static int
+atenl_hqa_read_temperature(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	char buf[64], *str;
+	int fd, ret;
+	u32 temp;
+	u8 phy_idx = get_band_val(an, an->cur_band, phy_idx);
+
+	ret = snprintf(buf, sizeof(buf),
+		       "/sys/class/ieee80211/phy%u/hwmon%u/temp1_input",
+		       phy_idx, phy_idx);
+	if (snprintf_error(sizeof(buf), ret))
+		return -1;
+
+	fd = open(buf, O_RDONLY);
+	if (fd < 0)
+		return fd;
+
+	ret = read(fd, buf, sizeof(buf) - 1);
+	if (ret < 0)
+		goto out;
+	buf[ret] = 0;
+
+	str = strchr(buf, ':');
+	str += 2;
+	temp = strtol(str, NULL, 10);
+	/* unit conversion */
+	*(u32 *)(hdr->data + 2) = htonl(temp / 1000);
+
+	ret = 0;
+out:
+	close(fd);
+
+	return ret;
+}
+
+static int
+atenl_hqa_skip(struct atenl *an, struct atenl_data *data)
+{
+	return 0;
+}
+
+static int
+atenl_hqa_check_efuse_mode(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	bool flash_mode = an->mtd_part != NULL;
+	enum atenl_cmd cmd = data->cmd;
+	u32 mode;
+
+	switch (cmd) {
+	case HQA_CMD_CHECK_EFUSE_MODE:
+		mode = flash_mode ? 0 : 1;
+		break;
+	case HQA_CMD_CHECK_EFUSE_MODE_TYPE:
+		mode = flash_mode ? E2P_FLASH_MODE : E2P_BIN_MODE;
+		break;
+	case HQA_CMD_CHECK_EFUSE_MODE_NATIVE:
+		mode = flash_mode ? E2P_FLASH_MODE : E2P_EFUSE_MODE;
+		break;
+	default:
+		mode = E2P_BIN_MODE;
+		break;
+	}
+
+	*(u32 *)(hdr->data + 2) = htonl(mode);
+
+	return 0;
+}
+
+static inline u16
+atenl_get_freq_by_channel(u8 ch_band, u16 ch)
+{
+	u16 base;
+
+	if (ch_band == CH_BAND_6GHZ) {
+		base = 5950;
+	} else if (ch_band == CH_BAND_5GHZ) {
+		if (ch >= 184)
+			return 4920 + (ch - 184) * 5;
+
+		base = 5000;
+	} else {
+		base = 2407;
+	}
+
+	return base + ch * 5;
+}
+
+u16 atenl_get_center_channel(u8 bw, u8 ch_band, u16 ctrl_ch)
+{
+	const struct atenl_channel *chan = NULL;
+	const struct atenl_channel *ch_list;
+	u16 center_ch;
+	u8 ch_num;
+	int i;
+
+	if (ch_band == CH_BAND_2GHZ || bw <= TEST_CBW_40MHZ)
+		return 0;
+
+	if (ch_band == CH_BAND_6GHZ) {
+		ch_list = atenl_channels_6ghz;
+		ch_num = ARRAY_SIZE(atenl_channels_6ghz);
+	} else {
+		ch_list = atenl_channels_5ghz;
+		ch_num = ARRAY_SIZE(atenl_channels_5ghz);
+	}
+
+	for (i = 0; i < ch_num; i++) {
+		if (ctrl_ch == ch_list[i].ch) {
+			chan = &ch_list[i];
+			break;
+		}
+	}
+
+	if (!chan)
+		return 0;
+
+	switch (bw) {
+	case TEST_CBW_160MHZ:
+		center_ch = chan->ch_160;
+		break;
+	case TEST_CBW_80MHZ:
+		center_ch = chan->ch_80;
+		break;
+	default:
+		center_ch = 0;
+		break;
+	}
+
+	return center_ch;
+}
+
+static void atenl_get_bw_string(u8 bw, char *buf)
+{
+	switch (bw) {
+	case TEST_CBW_160MHZ:
+		sprintf(buf, "160");
+		break;
+	case TEST_CBW_80MHZ:
+		sprintf(buf, "80");
+		break;
+	case TEST_CBW_40MHZ:
+		sprintf(buf, "40");
+		break;
+	default:
+		sprintf(buf, "20");
+		break;
+	}
+}
+
+void atenl_set_channel(struct atenl *an, u8 bw, u8 ch_band,
+		       u16 ch, u16 center_ch1, u16 center_ch2)
+{
+	char bw_str[8] = {};
+	char cmd[128];
+	u16 freq = atenl_get_freq_by_channel(ch_band, ch);
+	u16 freq_center1 = atenl_get_freq_by_channel(ch_band, center_ch1);
+	int ret;
+
+	if (bw > TEST_CBW_MAX)
+		return;
+
+	atenl_get_bw_string(bw, bw_str);
+
+	if (bw == TEST_CBW_20MHZ)
+		ret = snprintf(cmd, sizeof(cmd), "iw dev mon%d set freq %u %s",
+						 get_band_val(an, an->cur_band, phy_idx),
+						 freq, bw_str);
+	else
+		ret = snprintf(cmd, sizeof(cmd), "iw dev mon%d set freq %u %s %u",
+						 get_band_val(an, an->cur_band, phy_idx),
+						 freq, bw_str, freq_center1);
+	if (snprintf_error(sizeof(cmd), ret))
+		return;
+
+	atenl_dbg("[%d]%s: cmd: %s\n", getpid(), __func__, cmd);
+
+	system(cmd);
+}
+
+static int
+atenl_hqa_set_channel(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	u32 *v = (u32 *)hdr->data;
+	u8 band = ntohl(v[2]);
+	u16 ch1 = ntohl(v[3]);	/* center */
+	u16 ch2 = ntohl(v[4]);
+	u8 bw = ntohl(v[5]);
+	u8 pri_sel = ntohl(v[7]);
+	u8 ch_band = ntohl(v[9]);
+	u16 ctrl_ch = 0;
+
+	if (band >= MAX_BAND_NUM)
+		return -EINVAL;
+
+	if ((bw == TEST_CBW_160MHZ && pri_sel > 7) ||
+	    (bw == TEST_CBW_80MHZ && pri_sel > 3) ||
+	    (bw == TEST_CBW_40MHZ && pri_sel > 1)) {
+		atenl_err("%s: ctrl channel select error\n", __func__);
+		return -EINVAL;
+	}
+
+	an->cur_band = band;
+
+	if (ch_band == CH_BAND_2GHZ) {
+		ctrl_ch = ch1;
+		switch (bw) {
+		case TEST_CBW_40MHZ:
+			if (pri_sel == 1)
+				ctrl_ch += 2;
+			else
+				ctrl_ch -= 2;
+			break;
+		default:
+			break;
+		}
+
+		atenl_set_channel(an, bw, CH_BAND_2GHZ, ctrl_ch, ch1, 0);
+	} else {
+		const struct atenl_channel *chan = NULL;
+		const struct atenl_channel *ch_list;
+		u8 ch_num;
+		int i;
+
+		if (ch_band == CH_BAND_6GHZ) {
+			ch_list = atenl_channels_6ghz;
+			ch_num = ARRAY_SIZE(atenl_channels_6ghz);
+		} else {
+			ch_list = atenl_channels_5ghz;
+			ch_num = ARRAY_SIZE(atenl_channels_5ghz);
+		}
+
+		if (bw == TEST_CBW_160MHZ) {
+			for (i = 0; i < ch_num; i++) {
+				if (ch1 == ch_list[i].ch_160) {
+					chan = &ch_list[i];
+					break;
+				} else if (ch1 < ch_list[i].ch_160) {
+					chan = &ch_list[i - 1];
+					break;
+				}
+			}
+		} else if (bw == TEST_CBW_80MHZ) {
+			for (i = 0; i < ch_num; i++) {
+				if (ch1 == ch_list[i].ch_80) {
+					chan = &ch_list[i];
+					break;
+				} else if (ch1 < ch_list[i].ch_80) {
+					chan = &ch_list[i - 1];
+					break;
+				}
+			}
+		} else {
+			for (i = 0; i < ch_num; i++) {
+				if (ch1 <= ch_list[i].ch) {
+					if (ch1 == ch_list[i].ch)
+						chan = &ch_list[i];
+					else
+						chan = &ch_list[i - 1];
+					break;
+				}
+			}
+		}
+
+		if (!chan)
+			return -EINVAL;
+
+		if (bw != TEST_CBW_20MHZ) {
+			chan += pri_sel;
+			if (chan > &ch_list[ch_num - 1])
+				return -EINVAL;
+		}
+		ctrl_ch = chan->ch;
+
+		atenl_set_channel(an, bw, ch_band, ctrl_ch, ch1, ch2);
+	}
+
+	*(u32 *)(hdr->data + 2) = data->ext_id;
+
+	return 0;
+}
+
+static int
+atenl_hqa_tx_time_option(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	u32 *v = (u32 *)hdr->data;
+	u8 band = ntohl(v[1]);
+	u32 option = ntohl(v[2]);
+
+	if (band >= MAX_BAND_NUM)
+		return -EINVAL;
+
+	set_band_val(an, band, use_tx_time, !!option);
+	*(u32 *)(hdr->data + 2) = data->ext_id;
+
+	return 0;
+}
+
+static inline enum atenl_cmd atenl_get_cmd_by_id(u16 cmd_idx)
+{
+#define CMD_ID_GROUP	GENMASK(15, 8)
+	u8 group = FIELD_GET(CMD_ID_GROUP, cmd_idx);
+
+	if (cmd_idx == 0x1600)
+		return HQA_CMD_EXT;
+
+	if (group == 0x10) {
+		switch (cmd_idx) {
+		case 0x1000:
+			return HQA_CMD_OPEN_ADAPTER;
+		case 0x1001:
+			return HQA_CMD_CLOSE_ADAPTER;
+		case 0x100b:
+			return HQA_CMD_SET_TX_PATH;
+		case 0x100c:
+			return HQA_CMD_SET_RX_PATH;
+		case 0x1011:
+			return HQA_CMD_SET_TX_POWER;
+		case 0x1018:
+			return HQA_CMD_SET_TX_POWER_MANUAL;
+		case 0x100d:
+			return HQA_CMD_LEGACY;
+		default:
+			break;
+		}
+	} else if (group == 0x11) {
+		switch (cmd_idx) {
+		case 0x1104:
+			return HQA_CMD_SET_TX_BW;
+		case 0x1105:
+			return HQA_CMD_SET_TX_PKT_BW;
+		case 0x1106:
+			return HQA_CMD_SET_TX_PRI_BW;
+		case 0x1107:
+			return HQA_CMD_SET_FREQ_OFFSET;
+		case 0x1109:
+			return HQA_CMD_SET_TSSI;
+		case 0x110d:
+			return HQA_CMD_ANT_SWAP_CAP;
+		case 0x1101:
+		case 0x1102:
+			return HQA_CMD_LEGACY;
+		default:
+			break;
+		}
+	} else if (group == 0x12) {
+		switch (cmd_idx) {
+		case 0x1200:
+			return HQA_CMD_RESET_TX_RX_COUNTER;
+		default:
+			break;
+		}
+	} else if (group == 0x13) {
+		switch (cmd_idx) {
+		case 0x1301:
+			return HQA_CMD_WRITE_MAC_BBP_REG;
+		case 0x1302:
+			return HQA_CMD_READ_MAC_BBP_REG;
+		case 0x1307:
+			return HQA_CMD_READ_EEPROM_BULK;
+		case 0x1306:
+		case 0x1308:
+			return HQA_CMD_WRITE_EEPROM_BULK;
+		case 0x1309:
+			return HQA_CMD_CHECK_EFUSE_MODE;
+		case 0x130a:
+			return HQA_CMD_GET_EFUSE_FREE_BLOCK;
+		case 0x130d:
+			return HQA_CMD_GET_TX_POWER;
+		case 0x130e:
+			return HQA_CMD_SET_CFG;
+		case 0x130f:
+			return HQA_CMD_GET_FREQ_OFFSET;
+		case 0x1311:
+			return HQA_CMD_CONTINUOUS_TX;
+		case 0x1312:
+			return HQA_CMD_SET_RX_PKT_LEN;
+		case 0x1313:
+			return HQA_CMD_GET_TX_INFO;
+		case 0x1314:
+			return HQA_CMD_GET_CFG;
+		case 0x131f:
+			return HQA_CMD_UNKNOWN;
+		case 0x131a:
+			return HQA_CMD_GET_TX_TONE_POWER;
+		default:
+			break;
+		}
+	} else if (group == 0x14) {
+		switch (cmd_idx) {
+		case 0x1401:
+			return HQA_CMD_READ_TEMPERATURE;
+		default:
+			break;
+		}
+	} else if (group == 0x15) {
+		switch (cmd_idx) {
+		case 0x1500:
+			return HQA_CMD_GET_FW_INFO;
+		case 0x1505:
+			return HQA_CMD_SET_TSSI;
+		case 0x1509:
+			return HQA_CMD_SET_RF_MODE;
+		case 0x1511:
+			return HQA_CMD_WRITE_BUFFER_DONE;
+		case 0x1514:
+			return HQA_CMD_GET_CHIP_ID;
+		case 0x151b:
+			return HQA_CMD_GET_SUB_CHIP_ID;
+		case 0x151c:
+			return HQA_CMD_GET_RX_INFO;
+		case 0x151e:
+			return HQA_CMD_GET_RF_CAP;
+		case 0x1522:
+			return HQA_CMD_CHECK_EFUSE_MODE_TYPE;
+		case 0x1523:
+			return HQA_CMD_CHECK_EFUSE_MODE_NATIVE;
+		case 0x152d:
+			return HQA_CMD_GET_BAND;
+		case 0x1594:
+			return HQA_CMD_SET_RU;
+		case 0x1502:
+		case 0x150b:
+			return HQA_CMD_LEGACY;
+		default:
+			break;
+		}
+	}
+
+	return HQA_CMD_ERR;
+}
+
+static inline enum atenl_ext_cmd atenl_get_ext_cmd(u16 ext_cmd_idx)
+{
+#define EXT_CMD_ID_GROUP	GENMASK(7, 4)
+	u8 ext_group = FIELD_GET(EXT_CMD_ID_GROUP, ext_cmd_idx);
+
+	if (ext_group == 0) {
+		switch (ext_cmd_idx) {
+		case 0x1:
+			return HQA_EXT_CMD_SET_CHANNEL;
+		case 0x2:
+			return HQA_EXT_CMD_SET_TX;
+		case 0x3:
+			return HQA_EXT_CMD_START_TX;
+		case 0x4:
+			return HQA_EXT_CMD_START_RX;
+		case 0x5:
+			return HQA_EXT_CMD_STOP_TX;
+		case 0x6:
+			return HQA_EXT_CMD_STOP_RX;
+		case 0x8:
+			return HQA_EXT_CMD_IBF_SET_VAL;
+		case 0x9:
+			return HQA_EXT_CMD_IBF_GET_STATUS;
+		case 0xc:
+			return HQA_EXT_CMD_IBF_PROF_UPDATE_ALL;
+		default:
+			break;
+		}
+	} else if (ext_group == 1) {
+	} else if (ext_group == 2) {
+		switch (ext_cmd_idx) {
+		case 0x26:
+			return HQA_EXT_CMD_SET_TX_TIME_OPT;
+		case 0x27:
+			return HQA_EXT_CMD_OFF_CH_SCAN;
+		default:
+			break;
+		}
+	}
+
+	return HQA_EXT_CMD_UNSPEC;
+}
+
+#define ATENL_GROUP(_cmd, _resp_len, _ops)	\
+	[HQA_CMD_##_cmd] = { .resp_len=_resp_len, .ops=_ops }
+static const struct atenl_cmd_ops atenl_ops[] = {
+	ATENL_GROUP(OPEN_ADAPTER, 2, atenl_hqa_adapter),
+	ATENL_GROUP(CLOSE_ADAPTER, 2, atenl_hqa_adapter),
+	ATENL_GROUP(SET_TX_PATH, 2, atenl_nl_process),
+	ATENL_GROUP(SET_RX_PATH, 2, atenl_nl_process),
+	ATENL_GROUP(SET_TX_POWER, 2, atenl_nl_process),
+	ATENL_GROUP(SET_TX_POWER_MANUAL, 2, atenl_hqa_skip),
+	ATENL_GROUP(SET_TX_BW, 2, atenl_hqa_skip),
+	ATENL_GROUP(SET_TX_PKT_BW, 2, atenl_hqa_skip),
+	ATENL_GROUP(SET_TX_PRI_BW, 2, atenl_hqa_skip),
+	ATENL_GROUP(SET_FREQ_OFFSET, 2, atenl_nl_process),
+	ATENL_GROUP(ANT_SWAP_CAP, 6, atenl_hqa_skip),
+	ATENL_GROUP(RESET_TX_RX_COUNTER, 2, atenl_hqa_reset_counter),
+	ATENL_GROUP(WRITE_MAC_BBP_REG, 2, atenl_hqa_mac_bbp_reg),
+	ATENL_GROUP(READ_MAC_BBP_REG, 0, atenl_hqa_mac_bbp_reg),
+	ATENL_GROUP(READ_EEPROM_BULK, 0, atenl_hqa_eeprom_bulk),
+	ATENL_GROUP(WRITE_EEPROM_BULK, 2, atenl_hqa_eeprom_bulk),
+	ATENL_GROUP(CHECK_EFUSE_MODE, 6, atenl_hqa_check_efuse_mode),
+	ATENL_GROUP(GET_EFUSE_FREE_BLOCK, 6, atenl_hqa_get_efuse_free_block),
+	ATENL_GROUP(GET_TX_POWER, 10, atenl_hqa_get_tx_power),
+	ATENL_GROUP(GET_FREQ_OFFSET, 6, atenl_hqa_get_freq_offset), /*TODO: MCU CMD, read eeprom?*/
+	ATENL_GROUP(CONTINUOUS_TX, 6, atenl_nl_process),
+	ATENL_GROUP(SET_RX_PKT_LEN, 2, atenl_hqa_skip),
+	ATENL_GROUP(GET_TX_INFO, 10, atenl_nl_process),
+	ATENL_GROUP(GET_CFG, 6, atenl_hqa_get_cfg), /*TODO*/
+	ATENL_GROUP(GET_TX_TONE_POWER, 6, atenl_hqa_skip),
+	ATENL_GROUP(SET_CFG, 2, atenl_nl_process),
+	ATENL_GROUP(READ_TEMPERATURE, 6, atenl_hqa_read_temperature),
+	ATENL_GROUP(GET_FW_INFO, 32, atenl_hqa_skip), /* TODO: check format */
+	ATENL_GROUP(SET_TSSI, 2, atenl_nl_process),
+	ATENL_GROUP(SET_RF_MODE, 2, atenl_hqa_set_rf_mode),
+	ATENL_GROUP(WRITE_BUFFER_DONE, 2, atenl_hqa_eeprom_bulk),
+	ATENL_GROUP(GET_CHIP_ID, 6, atenl_hqa_get_chip_id),
+	ATENL_GROUP(GET_SUB_CHIP_ID, 6, atenl_hqa_get_sub_chip_id),
+	ATENL_GROUP(GET_RX_INFO, 0, atenl_nl_process),
+	ATENL_GROUP(GET_RF_CAP, 10, atenl_hqa_get_rf_cap),
+	ATENL_GROUP(CHECK_EFUSE_MODE_TYPE, 6, atenl_hqa_check_efuse_mode),
+	ATENL_GROUP(CHECK_EFUSE_MODE_NATIVE, 6, atenl_hqa_check_efuse_mode),
+	ATENL_GROUP(GET_BAND, 6, atenl_hqa_get_band),
+	ATENL_GROUP(SET_RU, 2, atenl_nl_process_many),
+
+	ATENL_GROUP(LEGACY, 2, atenl_hqa_skip),
+	ATENL_GROUP(UNKNOWN, 1024, atenl_hqa_skip),
+};
+#undef ATENL_GROUP
+
+#define ATENL_EXT(_cmd, _resp_len, _ops)	\
+	[HQA_EXT_CMD_##_cmd] = { .resp_len=_resp_len, .ops=_ops }
+static const struct atenl_cmd_ops atenl_ext_ops[] = {
+	ATENL_EXT(SET_CHANNEL, 6, atenl_hqa_set_channel),
+	ATENL_EXT(SET_TX, 6, atenl_nl_process),
+	ATENL_EXT(START_TX, 6, atenl_nl_process),
+	ATENL_EXT(STOP_TX, 6, atenl_nl_process),
+	ATENL_EXT(START_RX, 6, atenl_nl_process),
+	ATENL_EXT(STOP_RX, 6, atenl_nl_process),
+	ATENL_EXT(SET_TX_TIME_OPT, 6, atenl_hqa_tx_time_option),
+	ATENL_EXT(OFF_CH_SCAN, 6, atenl_nl_process),
+	ATENL_EXT(IBF_SET_VAL, 6, atenl_nl_process),
+	ATENL_EXT(IBF_GET_STATUS, 10, atenl_nl_process),
+	ATENL_EXT(IBF_PROF_UPDATE_ALL, 6, atenl_nl_process_many),
+};
+#undef ATENL_EXT
+
+int atenl_hqa_recv(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	u16 cmd_type = ntohs(hdr->cmd_type);
+	int fd = an->pipefd[PIPE_WRITE];
+	int ret;
+
+	if (ntohl(hdr->magic_no) != RACFG_MAGIC_NO)
+		return -EINVAL;
+
+	if (FIELD_GET(RACFG_CMD_TYPE_MASK, cmd_type) != RACFG_CMD_TYPE_ETHREQ &&
+	    FIELD_GET(RACFG_CMD_TYPE_MASK, cmd_type) != RACFG_CMD_TYPE_PLATFORM_MODULE) {
+		atenl_err("[%d]%s: cmd type error = 0x%x\n", getpid(), __func__, cmd_type);
+		return -EINVAL;
+	}
+
+	atenl_dbg("[%d]%s: recv cmd type = 0x%x, id = 0x%x\n",
+		  getpid(), __func__, cmd_type, ntohs(hdr->cmd_id));
+
+	ret = write(fd, data, data->len);
+	if (ret < 0) {
+		perror("pipe write");
+		return ret;
+	}
+
+	return 0;
+}
+
+int atenl_hqa_proc_cmd(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	const struct atenl_cmd_ops *ops;
+	u16 cmd_id = ntohs(hdr->cmd_id);
+	u16 status = 0;
+
+	data->cmd = atenl_get_cmd_by_id(cmd_id);
+	if (data->cmd == HQA_CMD_ERR) {
+		atenl_err("Unknown command id: 0x%04x\n", cmd_id);
+		goto done;
+	}
+
+	if (data->cmd == HQA_CMD_EXT) {
+		data->ext_id = ntohl(*(u32 *)hdr->data);
+		data->ext_cmd = atenl_get_ext_cmd(data->ext_id);
+		if (data->ext_cmd == HQA_EXT_CMD_UNSPEC) {
+			atenl_err("Unknown ext command id: 0x%04x\n", data->ext_id);
+			goto done;
+		}
+
+		ops = &atenl_ext_ops[data->ext_cmd];
+	} else {
+		ops = &atenl_ops[data->cmd];
+	}
+
+	atenl_dbg_print_data(data, __func__,
+			     ntohs(hdr->len) + ETH_HLEN + RACFG_HLEN);
+	if (ops->ops)
+		status = htons(ops->ops(an, data));
+	if (ops->resp_len)
+		hdr->len = htons(ops->resp_len);
+
+	*(u16 *)hdr->data = status;
+
+done:
+	data->len = ntohs(hdr->len) + ETH_HLEN + RACFG_HLEN;
+	hdr->cmd_type |= ~htons(RACFG_CMD_TYPE_MASK);
+
+	return 0;
+}
diff --git a/feed/atenl/src/main.c b/feed/atenl/src/main.c
new file mode 100644
index 0000000..ec0a320
--- /dev/null
+++ b/feed/atenl/src/main.c
@@ -0,0 +1,274 @@
+/* Copyright (C) 2021-2022 Mediatek Inc. */
+
+#include <signal.h>
+#include <sys/select.h>
+#include <sys/wait.h>
+#include "atenl.h"
+
+static const char *progname;
+bool atenl_enable;
+
+void sig_handler(int signum)
+{
+	atenl_enable = false;
+}
+
+void atenl_init_signals()
+{
+	if (signal(SIGINT, sig_handler) == SIG_ERR)
+		goto out;
+	if (signal(SIGTERM, sig_handler) == SIG_ERR)
+		goto out;
+	if (signal(SIGABRT, sig_handler) == SIG_ERR)
+		goto out;
+	if (signal(SIGUSR1, sig_handler) == SIG_ERR)
+		goto out;
+	if (signal(SIGUSR2, sig_handler) == SIG_ERR)
+		goto out;
+
+	return;
+out:
+	perror("signal");
+}
+
+static int phy_lookup_idx(const char *name)
+{
+	char buf[128];
+	FILE *f;
+	size_t len;
+	int ret;
+
+	ret = snprintf(buf, sizeof(buf), "/sys/class/ieee80211/%s/index", name);
+	if (snprintf_error(sizeof(buf), ret))
+		return -1;
+
+	f = fopen(buf, "r");
+	if (!f)
+		return -1;
+
+	len = fread(buf, 1, sizeof(buf) - 1, f);
+	fclose(f);
+
+	if (!len)
+		return -1;
+
+	buf[len] = 0;
+	return atoi(buf);
+}
+
+static void usage(void)
+{
+	fprintf(stderr, "Usage:\n");
+	printf("  %s [-u] [-i phyX]\n", progname);
+	printf("options:\n"
+	       "  -h = show help text\n"
+	       "  -i = phy name of driver interface, please use first phy for dbdc\n"
+	       "  -u = use unicast to respond to HQADLL\n");
+	printf("examples:\n"
+	       "  %s -u -i phy0\n", progname);
+
+	exit(EXIT_FAILURE);
+}
+
+static int atenl_parent_work(struct atenl *an)
+{
+	int sock_eth = an->sock_eth;
+	int count, ret = 0;
+	fd_set readfds;
+
+	atenl_info("[%d]%s: start for receiving HQA commands\n", getpid(), __func__);
+
+	while (atenl_enable) {
+		FD_ZERO(&readfds);
+		FD_SET(sock_eth, &readfds);
+		count = select(sock_eth + 1, &readfds, NULL, NULL, NULL);
+
+		if (count < 0) {
+			atenl_err("%s: select failed, %s\n", __func__, strerror(errno));
+			continue;
+		} else if (count == 0) {
+			usleep(1000);
+			continue;
+		} else {
+			if (FD_ISSET(sock_eth, &readfds)) {
+				struct atenl_data *data = calloc(1, sizeof(struct atenl_data));
+
+				ret = atenl_eth_recv(an, data);
+				if (ret) {
+					kill(an->child_pid, SIGUSR1);
+					return ret;
+				}
+
+				ret = atenl_hqa_recv(an, data);
+				if (ret < 0) {
+					kill(an->child_pid, SIGUSR1);
+					return ret;
+				}
+
+				free(data);
+			}
+		}
+	}
+
+	atenl_info("[%d]%s: parent work end\n", getpid(), __func__);
+
+	return ret;
+}
+
+static int atenl_child_work(struct atenl *an)
+{
+	int rfd = an->pipefd[PIPE_READ], count;
+	int ret = 0;
+	fd_set readfds;
+
+	atenl_info("[%d]%s: start for sending back results\n", getpid(), __func__);
+
+	while (atenl_enable) {
+		struct atenl_data *data = calloc(1, sizeof(struct atenl_data));
+
+		FD_ZERO(&readfds);
+		FD_SET(rfd, &readfds);
+
+		count = select(FD_SETSIZE, &readfds, NULL, NULL, NULL);
+
+		if (count < 0) {
+			atenl_err("%s: select failed, %s\n", __func__, strerror(errno));
+			continue;
+		} else if (count == 0) {
+			usleep(1000);
+			continue;
+		} else {
+			if (FD_ISSET(rfd, &readfds)) {
+				count = read(rfd, data->buf, RACFG_PKT_MAX_SIZE);
+				atenl_dbg("[%d]PIPE Read %d bytes\n", getpid(), count);
+
+				if (count < 0) {
+					atenl_info("%s: %s\n", __func__, strerror(errno));
+				} else if (count == 0) {
+					continue;
+				} else {
+					int ret;
+
+					ret = atenl_hqa_proc_cmd(an, data);
+					if (ret) {
+						kill(getppid(), SIGUSR2);
+						goto out;
+					}
+
+					ret = atenl_eth_send(an, data);
+					if (ret) {
+						kill(getppid(), SIGUSR2);
+						goto out;
+					}
+				}
+			}
+		}
+	}
+
+out:
+	atenl_info("[%d]%s: child work end\n", getpid(), __func__);
+
+	return ret;
+}
+
+int main(int argc, char **argv)
+{
+	int opt, phy_idx, ret = 0;
+	char *phy = "phy0", *cmd = NULL;
+	struct atenl *an;
+
+	progname = argv[0];
+
+	an = calloc(1, sizeof(struct atenl));
+	if (!an)
+		return -ENOMEM;
+
+	while(1) {
+		opt = getopt(argc, argv, "hi:uc:");
+		if (opt == -1)
+			break;
+
+		switch (opt) {
+			case 'h':
+				usage();
+				free(an);
+				return 0;
+			case 'i':
+				phy = optarg;
+				break;
+			case 'u':
+				an->unicast = true;
+				printf("Opt: use unicast to send response\n");
+				break;
+			case 'c':
+				cmd = optarg;
+				break;
+			default:
+				fprintf(stderr, "Not supported option\n");
+				break;
+		}
+	}
+
+	phy_idx = phy_lookup_idx(phy);
+	if (phy_idx < 0 || phy_idx > UCHAR_MAX) {
+		fprintf(stderr, "Could not find phy '%s'\n", phy);
+		free(an);
+		return 2;
+	}
+
+	if (cmd) {
+		atenl_eeprom_cmd_handler(an, phy_idx, cmd);
+		goto out;
+	}
+
+	atenl_enable = true;
+	atenl_init_signals();
+
+	/* background ourself */
+	if (!fork()) {
+		pid_t pid;
+
+		ret = atenl_eeprom_init(an, phy_idx);
+		if (ret)
+			goto out;
+
+		ret = atenl_eth_init(an);
+		if (ret)
+			goto out;
+
+		ret = pipe(an->pipefd);
+		if (ret) {
+			perror("Pipe");
+			goto out;
+		}
+
+		pid = fork();
+		an->child_pid = pid;
+		if (pid < 0) {
+			perror("Fork");
+			ret = pid;
+			goto out;
+		} else if (pid == 0) {
+			close(an->pipefd[PIPE_WRITE]);
+			atenl_child_work(an);
+		} else {
+			int status;
+
+			close(an->pipefd[PIPE_READ]);
+			atenl_parent_work(an);
+			waitpid(pid, &status, 0);
+		}
+	} else {
+		usleep(800000);
+	}
+
+	ret = 0;
+out:
+	if (an->sock_eth)
+		close(an->sock_eth);
+	if (an->eeprom_fd || an->eeprom_data)
+		atenl_eeprom_close(an);
+	free(an);
+
+	return ret;
+}
diff --git a/feed/atenl/src/nl.c b/feed/atenl/src/nl.c
new file mode 100644
index 0000000..47e3d76
--- /dev/null
+++ b/feed/atenl/src/nl.c
@@ -0,0 +1,1175 @@
+/* Copyright (C) 2021-2022 Mediatek Inc. */
+#define _GNU_SOURCE
+
+#include <unl.h>
+
+#include "atenl.h"
+
+#define to_rssi(_rcpi)	((_rcpi - 220) / 2)
+
+struct atenl_nl_priv {
+	struct atenl *an;
+	struct unl unl;
+	struct nl_msg *msg;
+	int attr;
+	void *res;
+};
+
+struct atenl_nl_ops {
+	int set;
+	int dump;
+	int (*ops)(struct atenl *an, struct atenl_data *data,
+		   struct atenl_nl_priv *nl_priv);
+};
+
+static struct nla_policy testdata_policy[NUM_MT76_TM_ATTRS] = {
+	[MT76_TM_ATTR_STATE] = { .type = NLA_U8 },
+	[MT76_TM_ATTR_MTD_PART] = { .type = NLA_STRING },
+	[MT76_TM_ATTR_MTD_OFFSET] = { .type = NLA_U32 },
+	[MT76_TM_ATTR_TX_COUNT] = { .type = NLA_U32 },
+	[MT76_TM_ATTR_TX_LENGTH] = { .type = NLA_U32 },
+	[MT76_TM_ATTR_TX_RATE_MODE] = { .type = NLA_U8 },
+	[MT76_TM_ATTR_TX_RATE_NSS] = { .type = NLA_U8 },
+	[MT76_TM_ATTR_TX_RATE_IDX] = { .type = NLA_U8 },
+	[MT76_TM_ATTR_TX_RATE_SGI] = { .type = NLA_U8 },
+	[MT76_TM_ATTR_TX_RATE_LDPC] = { .type = NLA_U8 },
+	[MT76_TM_ATTR_TX_RATE_STBC] = { .type = NLA_U8 },
+	[MT76_TM_ATTR_TX_LTF] = { .type = NLA_U8 },
+	[MT76_TM_ATTR_TX_POWER_CONTROL] = { .type = NLA_U8 },
+	[MT76_TM_ATTR_TX_ANTENNA] = { .type = NLA_U8 },
+	[MT76_TM_ATTR_FREQ_OFFSET] = { .type = NLA_U32 },
+	[MT76_TM_ATTR_STATS] = { .type = NLA_NESTED },
+};
+
+static struct nla_policy stats_policy[NUM_MT76_TM_STATS_ATTRS] = {
+	[MT76_TM_STATS_ATTR_TX_PENDING] = { .type = NLA_U32 },
+	[MT76_TM_STATS_ATTR_TX_QUEUED] = { .type = NLA_U32 },
+	[MT76_TM_STATS_ATTR_TX_DONE] = { .type = NLA_U32 },
+	[MT76_TM_STATS_ATTR_RX_PACKETS] = { .type = NLA_U64 },
+	[MT76_TM_STATS_ATTR_RX_FCS_ERROR] = { .type = NLA_U64 },
+};
+
+static struct nla_policy rx_policy[NUM_MT76_TM_RX_ATTRS] = {
+	[MT76_TM_RX_ATTR_FREQ_OFFSET] = { .type = NLA_U32 },
+	[MT76_TM_RX_ATTR_RCPI] = { .type = NLA_NESTED },
+	[MT76_TM_RX_ATTR_IB_RSSI] = { .type = NLA_NESTED },
+	[MT76_TM_RX_ATTR_WB_RSSI] = { .type = NLA_NESTED },
+	[MT76_TM_RX_ATTR_SNR] = { .type = NLA_U8 },
+};
+
+struct he_sgi {
+	enum mt76_testmode_tx_mode tx_mode;
+	u8 sgi;
+	u8 tx_ltf;
+};
+
+#define HE_SGI_GROUP(_tx_mode, _sgi, _tx_ltf)	\
+	{ .tx_mode = MT76_TM_TX_MODE_##_tx_mode, .sgi = _sgi, .tx_ltf = _tx_ltf }
+static const struct he_sgi he_sgi_groups[] = {
+	HE_SGI_GROUP(HE_SU, 0, 0),
+	HE_SGI_GROUP(HE_SU, 0, 1),
+	HE_SGI_GROUP(HE_SU, 1, 1),
+	HE_SGI_GROUP(HE_SU, 2, 2),
+	HE_SGI_GROUP(HE_SU, 0, 2),
+	HE_SGI_GROUP(HE_EXT_SU, 0, 0),
+	HE_SGI_GROUP(HE_EXT_SU, 0, 1),
+	HE_SGI_GROUP(HE_EXT_SU, 1, 1),
+	HE_SGI_GROUP(HE_EXT_SU, 2, 2),
+	HE_SGI_GROUP(HE_EXT_SU, 0, 2),
+	HE_SGI_GROUP(HE_TB, 1, 0),
+	HE_SGI_GROUP(HE_TB, 1, 1),
+	HE_SGI_GROUP(HE_TB, 2, 2),
+	HE_SGI_GROUP(HE_MU, 0, 2),
+	HE_SGI_GROUP(HE_MU, 0, 1),
+	HE_SGI_GROUP(HE_MU, 1, 1),
+	HE_SGI_GROUP(HE_MU, 2, 2),
+};
+#undef HE_SGI_LTF_GROUP
+
+static u8 phy_type_to_attr(u8 phy_type)
+{
+	static const u8 phy_type_to_attr[] = {
+		[ATENL_PHY_TYPE_CCK] = MT76_TM_TX_MODE_CCK,
+		[ATENL_PHY_TYPE_OFDM] = MT76_TM_TX_MODE_OFDM,
+		[ATENL_PHY_TYPE_HT] = MT76_TM_TX_MODE_HT,
+		[ATENL_PHY_TYPE_HT_GF] = MT76_TM_TX_MODE_HT,
+		[ATENL_PHY_TYPE_VHT] = MT76_TM_TX_MODE_VHT,
+		[ATENL_PHY_TYPE_HE_SU] = MT76_TM_TX_MODE_HE_SU,
+		[ATENL_PHY_TYPE_HE_EXT_SU] = MT76_TM_TX_MODE_HE_EXT_SU,
+		[ATENL_PHY_TYPE_HE_TB] = MT76_TM_TX_MODE_HE_TB,
+		[ATENL_PHY_TYPE_HE_MU] = MT76_TM_TX_MODE_HE_MU,
+	};
+
+	if (phy_type >= ARRAY_SIZE(phy_type_to_attr))
+		return 0;
+
+	return phy_type_to_attr[phy_type];
+}
+
+static void
+atenl_set_attr_state(struct atenl *an, struct nl_msg *msg,
+		     u8 band, enum mt76_testmode_state state)
+{
+	if (get_band_val(an, band, cur_state) == state)
+		return;
+
+	nla_put_u8(msg, MT76_TM_ATTR_STATE, state);
+	set_band_val(an, band, cur_state, state);
+}
+
+static void
+atenl_set_attr_antenna(struct atenl *an, struct nl_msg *msg, u8 tx_antenna)
+{
+	if (!tx_antenna)
+		return;
+
+	if (is_mt7915(an))
+		nla_put_u8(msg, MT76_TM_ATTR_TX_ANTENNA,
+			   tx_antenna << (2 * an->cur_band));
+	else if (is_mt7916(an) || is_mt7986(an))
+		nla_put_u8(msg, MT76_TM_ATTR_TX_ANTENNA, tx_antenna);
+}
+
+static int
+atenl_nl_set_attr(struct atenl *an, struct atenl_data *data,
+		  struct atenl_nl_priv *nl_priv)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct nl_msg *msg = nl_priv->msg;
+	u32 val = ntohl(*(u32 *)hdr->data);
+	int attr = nl_priv->attr;
+	void *ptr, *a;
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	switch (attr) {
+	case MT76_TM_ATTR_TX_ANTENNA:
+		atenl_set_attr_antenna(an, msg, val);
+		break;
+	case MT76_TM_ATTR_FREQ_OFFSET:
+		nla_put_u32(msg, attr, val);
+		break;
+	case MT76_TM_ATTR_TX_POWER:
+		a = nla_nest_start(msg, MT76_TM_ATTR_TX_POWER);
+		if (!a)
+			return -ENOMEM;
+		nla_put_u8(msg, 0, val);
+		nla_nest_end(msg, a);
+		break;
+	default:
+		nla_put_u8(msg, attr, val);
+		break;
+	}
+
+	nla_nest_end(msg, ptr);
+
+	return unl_genl_request(&nl_priv->unl, msg, NULL, NULL);
+}
+
+static int
+atenl_nl_set_cfg(struct atenl *an, struct atenl_data *data,
+		 struct atenl_nl_priv *nl_priv)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct nl_msg *msg = nl_priv->msg;
+	enum atenl_cmd cmd = data->cmd;
+	u32 *v = (u32 *)hdr->data;
+	u8 type = ntohl(v[0]);
+	u8 enable = ntohl(v[1]);
+	void *ptr, *cfg;
+
+	if (cmd == HQA_CMD_SET_TSSI) {
+		type = 0;
+		enable = 1;
+	}
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	cfg = nla_nest_start(msg, MT76_TM_ATTR_CFG);
+	if (!cfg)
+		return -ENOMEM;
+
+	if (nla_put_u8(msg, 0, type) ||
+	    nla_put_u8(msg, 1, enable))
+		return -EINVAL;
+
+	nla_nest_end(msg, cfg);
+
+	nla_nest_end(msg, ptr);
+
+	return unl_genl_request(&nl_priv->unl, msg, NULL, NULL);
+}
+
+static int
+atenl_nl_set_tx(struct atenl *an, struct atenl_data *data,
+		struct atenl_nl_priv *nl_priv)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct nl_msg *msg = nl_priv->msg;
+	u32 *v = (u32 *)hdr->data;
+	u8 *addr1 = hdr->data + 36;
+	u8 *addr2 = addr1 + ETH_ALEN;
+	u8 *addr3 = addr2 + ETH_ALEN;
+	u8 default_addr[ETH_ALEN] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
+	void *ptr, *a;
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	if (get_band_val(an, an->cur_band, use_tx_time))
+		nla_put_u32(msg, MT76_TM_ATTR_TX_TIME, ntohl(v[7]));
+	else
+		nla_put_u32(msg, MT76_TM_ATTR_TX_LENGTH, ntohl(v[7]));
+
+	a = nla_nest_start(msg, MT76_TM_ATTR_MAC_ADDRS);
+	if (!a)
+		return -ENOMEM;
+
+	if (is_multicast_ether_addr(addr1))
+		nla_put(msg, 0, ETH_ALEN, default_addr);
+	else
+		nla_put(msg, 0, ETH_ALEN, addr1);
+	nla_put(msg, 1, ETH_ALEN, addr2);
+	nla_put(msg, 2, ETH_ALEN, addr3);
+
+	nla_nest_end(msg, a);
+
+	nla_nest_end(msg, ptr);
+
+	*(u32 *)(hdr->data + 2) = data->ext_id;
+
+	return unl_genl_request(&nl_priv->unl, msg, NULL, NULL);
+}
+
+static int
+atenl_nl_tx(struct atenl *an, struct atenl_data *data, struct atenl_nl_priv *nl_priv)
+{
+#define USE_SPE_IDX	BIT(31)
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct nl_msg *msg = nl_priv->msg;
+	u32 *v = (u32 *)hdr->data;
+	u8 band = ntohl(v[2]);
+	void *ptr;
+	int ret;
+
+	if (band >= MAX_BAND_NUM)
+		return -EINVAL;
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	if (data->ext_cmd == HQA_EXT_CMD_STOP_TX) {
+		atenl_set_attr_state(an, msg, band, MT76_TM_STATE_IDLE);
+	} else {
+		u32 tx_count = ntohl(v[3]);
+		u8 tx_rate_mode = phy_type_to_attr(ntohl(v[4]));
+		u8 aid = ntohl(v[11]);
+		u8 sgi = ntohl(v[13]);
+		u32 tx_antenna = ntohl(v[14]);
+		void *a;
+
+		if (sgi > 5)
+			return -EINVAL;
+
+		if (!tx_count)
+			tx_count = 10000000;
+
+		nla_put_u32(msg, MT76_TM_ATTR_TX_COUNT, tx_count);
+		nla_put_u32(msg, MT76_TM_ATTR_TX_IPG, ntohl(v[12]));
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_MODE, tx_rate_mode);
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_IDX, ntohl(v[5]));
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_STBC, ntohl(v[7]));
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_LDPC, ntohl(v[8]));
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_NSS, ntohl(v[15]));
+
+		/* for chips after 7915, tx need to use at least wcid = 1 */
+		if (!is_mt7915(an) && !aid)
+			aid = 1;
+		nla_put_u8(msg, MT76_TM_ATTR_AID, aid);
+
+		if (tx_antenna & USE_SPE_IDX) {
+			nla_put_u8(msg, MT76_TM_ATTR_TX_SPE_IDX,
+				   tx_antenna & ~USE_SPE_IDX);
+		} else {
+			nla_put_u8(msg, MT76_TM_ATTR_TX_SPE_IDX, 0);
+			atenl_set_attr_antenna(an, msg, tx_antenna);
+		}
+
+		if (tx_rate_mode >= MT76_TM_TX_MODE_HE_SU) {
+			u8 ofs = sgi;
+			size_t i;
+
+			for (i = 0; i < ARRAY_SIZE(he_sgi_groups); i++)
+				if (he_sgi_groups[i].tx_mode == tx_rate_mode)
+					break;
+
+			if ((i + ofs) >= ARRAY_SIZE(he_sgi_groups))
+				return -EINVAL;
+
+			sgi = he_sgi_groups[i + ofs].sgi;
+			nla_put_u8(msg, MT76_TM_ATTR_TX_LTF,
+				   he_sgi_groups[i + ofs].tx_ltf);
+		}
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_SGI, sgi);
+
+		a = nla_nest_start(msg, MT76_TM_ATTR_TX_POWER);
+		if (!a)
+			return -ENOMEM;
+		nla_put_u8(msg, 0, ntohl(v[6]));
+		nla_nest_end(msg, a);
+
+		atenl_set_attr_state(an, msg, band, MT76_TM_STATE_TX_FRAMES);
+	}
+
+	nla_nest_end(msg, ptr);
+
+	ret = unl_genl_request(&nl_priv->unl, msg, NULL, NULL);
+	if (ret)
+		return ret;
+
+	*(u32 *)(hdr->data + 2) = data->ext_id;
+
+	return 0;
+}
+
+static int
+atenl_nl_rx(struct atenl *an, struct atenl_data *data, struct atenl_nl_priv *nl_priv)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct atenl_band *anb = &an->anb[an->cur_band];
+	struct nl_msg *msg = nl_priv->msg;
+	u32 *v = (u32 *)hdr->data;
+	u8 band = ntohl(v[2]);
+	void *ptr;
+
+	if (band >= MAX_BAND_NUM)
+		return -EINVAL;
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	if (data->ext_cmd == HQA_EXT_CMD_STOP_RX) {
+		atenl_set_attr_state(an, msg, band, MT76_TM_STATE_IDLE);
+	} else {
+		v = (u32 *)(hdr->data + 18);
+
+		atenl_set_attr_antenna(an, msg, ntohl(v[0]));
+		nla_put_u8(msg, MT76_TM_ATTR_AID, ntohl(v[1]));
+		atenl_set_attr_state(an, msg, band, MT76_TM_STATE_RX_FRAMES);
+
+		anb->reset_rx_cnt = false;
+
+		/* clear history buffer */
+		memset(&anb->rx_stat, 0, sizeof(anb->rx_stat));
+	}
+
+	nla_nest_end(msg, ptr);
+
+	*(u32 *)(hdr->data + 2) = data->ext_id;
+
+	return unl_genl_request(&nl_priv->unl, msg, NULL, NULL);
+}
+
+static int
+atenl_off_ch_scan(struct atenl *an, struct atenl_data *data,
+		  struct atenl_nl_priv *nl_priv)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct nl_msg *msg = nl_priv->msg;
+	u32 *v = (u32 *)hdr->data;
+	u8 ch = ntohl(v[2]);
+	u8 bw = ntohl(v[4]);
+	u8 tx_path = ntohl(v[5]);
+	u8 status = ntohl(v[6]);
+	void *ptr;
+
+	if (!status)
+		ch = 0; /* stop */
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	nla_put_u8(msg, MT76_TM_ATTR_OFF_CH_SCAN_CH, ch);
+	nla_put_u8(msg, MT76_TM_ATTR_OFF_CH_SCAN_CENTER_CH,
+		   atenl_get_center_channel(bw, CH_BAND_5GHZ, ch));
+	nla_put_u8(msg, MT76_TM_ATTR_OFF_CH_SCAN_BW, bw);
+	nla_put_u8(msg, MT76_TM_ATTR_OFF_CH_SCAN_PATH, tx_path);
+
+	nla_nest_end(msg, ptr);
+
+	memcpy(hdr->data + 2, &data->ext_id, 4);
+
+	return 0;
+}
+
+static int atenl_nl_dump_cb(struct nl_msg *msg, void *arg)
+{
+	struct atenl_nl_priv *nl_priv = (struct atenl_nl_priv *)arg;
+	struct nlattr *tb1[NUM_MT76_TM_ATTRS];
+	struct nlattr *tb2[NUM_MT76_TM_STATS_ATTRS];
+	struct nlattr *nl_attr;
+	int attr = nl_priv->attr;
+	u64 *res = nl_priv->res;
+
+	nl_attr = unl_find_attr(&nl_priv->unl, msg, NL80211_ATTR_TESTDATA);
+	if (!nl_attr) {
+		fprintf(stderr, "Testdata attribute not found\n");
+		return NL_SKIP;
+	}
+
+	nla_parse_nested(tb1, MT76_TM_ATTR_MAX, nl_attr, testdata_policy);
+	nla_parse_nested(tb2, MT76_TM_STATS_ATTR_MAX,
+			 tb1[MT76_TM_ATTR_STATS], stats_policy);
+
+	if (attr == MT76_TM_STATS_ATTR_TX_DONE)
+		*res = nla_get_u32(tb2[MT76_TM_STATS_ATTR_TX_DONE]);
+
+	return NL_SKIP;
+}
+
+static int
+atenl_nl_dump_attr(struct atenl *an, struct atenl_data *data,
+		   struct atenl_nl_priv *nl_priv)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct nl_msg *msg = nl_priv->msg;
+	void *ptr;
+	u64 res = 0;
+
+	nl_priv->res = (void *)&res;
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+	nla_put_flag(msg, MT76_TM_ATTR_STATS);
+	nla_nest_end(msg, ptr);
+
+	unl_genl_request(&nl_priv->unl, msg, atenl_nl_dump_cb, (void *)nl_priv);
+
+	if (nl_priv->attr == MT76_TM_STATS_ATTR_TX_DONE)
+		*(u32 *)(hdr->data + 2 + 4 * an->cur_band) = htonl(res);
+
+	return 0;
+}
+
+static int atenl_nl_continuous_tx(struct atenl *an,
+				  struct atenl_data *data,
+				  struct atenl_nl_priv *nl_priv)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct nl_msg *msg = nl_priv->msg;
+	u32 *v = (u32 *)hdr->data;
+	u8 band = ntohl(v[0]);
+	bool enable = ntohl(v[1]);
+	void *ptr;
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	if (band >= MAX_BAND_NUM)
+		return -EINVAL;
+
+	if (!enable) {
+		int phy = get_band_val(an, band, phy_idx);
+		char cmd[64];
+
+		atenl_set_attr_state(an, msg, band, MT76_TM_STATE_IDLE);
+		nla_nest_end(msg, ptr);
+		unl_genl_request(&nl_priv->unl, msg, NULL, NULL);
+
+		sprintf(cmd, "iw dev mon%d del", phy);
+		system(cmd);
+		sprintf(cmd, "iw phy phy%d interface add mon%d type monitor", phy, phy);
+		system(cmd);
+		sprintf(cmd, "ifconfig mon%d up", phy);
+		system(cmd);
+
+		return 0;
+	}
+
+	if (get_band_val(an, band, rf_mode) != ATENL_RF_MODE_TEST)
+		return 0;
+
+	nla_put_u8(msg, MT76_TM_ATTR_TX_ANTENNA, ntohl(v[2]));
+	nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_MODE, phy_type_to_attr(ntohl(v[3])));
+	nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_IDX, ntohl(v[6]));
+
+	atenl_dbg("%s: enable = %d, ant=%u, tx_rate_mode=%u, rate_idx=%u\n",
+		  __func__, enable, ntohl(v[2]), ntohl(v[3]), ntohl(v[6]));
+
+	atenl_set_attr_state(an, msg, band, MT76_TM_STATE_TX_CONT);
+
+	nla_nest_end(msg, ptr);
+
+	return unl_genl_request(&nl_priv->unl, msg, NULL, NULL);
+}
+
+static int atenl_nl_get_rx_info_cb(struct nl_msg *msg, void *arg)
+{
+	struct atenl_nl_priv *nl_priv = (struct atenl_nl_priv *)arg;
+	struct atenl *an = nl_priv->an;
+	struct atenl_band *anb = &an->anb[an->cur_band];
+	struct atenl_data *data = nl_priv->res;
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct atenl_rx_info_hdr *rx_hdr;
+	struct atenl_rx_info_band *rx_band;
+	struct atenl_rx_info_user *rx_user;
+	struct atenl_rx_info_path *rx_path;
+	struct atenl_rx_info_comm *rx_comm;
+	struct nlattr *tb1[NUM_MT76_TM_ATTRS];
+	struct nlattr *tb2[NUM_MT76_TM_STATS_ATTRS];
+	struct nlattr *tb3[NUM_MT76_TM_RX_ATTRS];
+	struct nlattr *nl_attr, *cur;
+	struct atenl_rx_stat rx_cur, rx_diff = {};
+	u32 rcpi[4] = {};
+	u32 type_num = htonl(4);
+	s32 ib_rssi[4] = {}, wb_rssi[4] = {};
+	u8 path = an->anb[an->cur_band].chainmask;
+	u8 path_num = __builtin_popcount(path);
+	u8 *buf = hdr->data + 2;
+	int i, rem;
+
+	*(u32 *)buf = type_num;
+	buf += sizeof(type_num);
+
+#define RX_PUT_HDR(_hdr, _type, _val, _size) do {	\
+		_hdr->type = htonl(_type);		\
+		_hdr->val = htonl(_val);		\
+		_hdr->len = htonl(_size);		\
+		buf += sizeof(*_hdr);			\
+	} while (0)
+
+	rx_hdr = (struct atenl_rx_info_hdr *)buf;
+	RX_PUT_HDR(rx_hdr, 0, BIT(an->cur_band), sizeof(*rx_band));
+	rx_band = (struct atenl_rx_info_band *)buf;
+	buf += sizeof(*rx_band);
+
+	rx_hdr = (struct atenl_rx_info_hdr *)buf;
+	RX_PUT_HDR(rx_hdr, 1, path, path_num * sizeof(*rx_path));
+	rx_path = (struct atenl_rx_info_path *)buf;
+	buf += path_num * sizeof(*rx_path);
+
+	rx_hdr = (struct atenl_rx_info_hdr *)buf;
+	RX_PUT_HDR(rx_hdr, 2, GENMASK(15, 0), 16 * sizeof(*rx_user));
+	rx_user = (struct atenl_rx_info_user *)buf;
+	buf += 16 * sizeof(*rx_user);
+
+	rx_hdr = (struct atenl_rx_info_hdr *)buf;
+	RX_PUT_HDR(rx_hdr, 3, BIT(0), sizeof(*rx_comm));
+	rx_comm = (struct atenl_rx_info_comm *)buf;
+	buf += sizeof(*rx_comm);
+
+	hdr->len = htons(buf - hdr->data);
+
+	nl_attr = unl_find_attr(&nl_priv->unl, msg, NL80211_ATTR_TESTDATA);
+	if (!nl_attr) {
+		fprintf(stderr, "Testdata attribute not found\n");
+		return NL_SKIP;
+	}
+
+	nla_parse_nested(tb1, MT76_TM_ATTR_MAX, nl_attr, testdata_policy);
+	nla_parse_nested(tb2, MT76_TM_STATS_ATTR_MAX,
+			 tb1[MT76_TM_ATTR_STATS], stats_policy);
+
+	rx_cur.total = nla_get_u64(tb2[MT76_TM_STATS_ATTR_RX_PACKETS]);
+	rx_cur.err_cnt = nla_get_u64(tb2[MT76_TM_STATS_ATTR_RX_FCS_ERROR]);
+	rx_cur.len_mismatch = nla_get_u64(tb2[MT76_TM_STATS_ATTR_RX_LEN_MISMATCH]);
+	rx_cur.ok_cnt = rx_cur.total - rx_cur.err_cnt - rx_cur.len_mismatch;
+
+	if (!anb->reset_rx_cnt) {
+#define RX_COUNT_DIFF(_field)	\
+	rx_diff._field = (rx_cur._field) - (anb->rx_stat._field)
+		RX_COUNT_DIFF(total);
+		RX_COUNT_DIFF(err_cnt);
+		RX_COUNT_DIFF(len_mismatch);
+		RX_COUNT_DIFF(ok_cnt);
+#undef RX_COUNT_DIFF
+
+		memcpy(&anb->rx_stat, &rx_cur, sizeof(anb->rx_stat));
+	}
+
+	rx_band->mac_rx_mdrdy_cnt = htonl((u32)rx_diff.total);
+	rx_band->mac_rx_fcs_err_cnt = htonl((u32)rx_diff.err_cnt);
+	rx_band->mac_rx_fcs_ok_cnt = htonl((u32)rx_diff.ok_cnt);
+	rx_band->mac_rx_len_mismatch = htonl((u32)rx_diff.len_mismatch);
+	rx_user->fcs_error_cnt = htonl((u32)rx_diff.err_cnt);
+
+	nla_parse_nested(tb3, MT76_TM_RX_ATTR_MAX,
+			 tb2[MT76_TM_STATS_ATTR_LAST_RX], rx_policy);
+
+	rx_user->freq_offset = htonl(nla_get_u32(tb3[MT76_TM_RX_ATTR_FREQ_OFFSET]));
+	rx_user->snr = htonl(nla_get_u8(tb3[MT76_TM_RX_ATTR_SNR]));
+
+	i = 0;
+	nla_for_each_nested(cur, tb3[MT76_TM_RX_ATTR_RCPI], rem) {
+		if (nla_len(cur) != 1 || i >= 4)
+			break;
+
+		rcpi[i++] = nla_get_u8(cur);
+	}
+
+	i = 0;
+	nla_for_each_nested(cur, tb3[MT76_TM_RX_ATTR_IB_RSSI], rem) {
+		if (nla_len(cur) != 1 || i >= 4)
+			break;
+
+		ib_rssi[i++] = (s8)nla_get_u8(cur);
+	}
+
+	i = 0;
+	nla_for_each_nested(cur, tb3[MT76_TM_RX_ATTR_WB_RSSI], rem) {
+		if (nla_len(cur) != 1 || i >= 4)
+			break;
+
+		wb_rssi[i++] = (s8)nla_get_u8(cur);
+	}
+
+	for (i = 0; i < 4; i++) {
+		struct atenl_rx_info_path *path = &rx_path[i];
+
+		path->rcpi = htonl(rcpi[i]);
+		path->rssi = htonl(to_rssi((u8)rcpi[i]));
+		path->fagc_ib_rssi = htonl(ib_rssi[i]);
+		path->fagc_wb_rssi = htonl(wb_rssi[i]);
+	}
+
+	return NL_SKIP;
+}
+
+static int atenl_nl_get_rx_info(struct atenl *an, struct atenl_data *data,
+				struct atenl_nl_priv *nl_priv)
+{
+	struct nl_msg *msg = nl_priv->msg;
+	void *ptr;
+
+	nl_priv->an = an;
+	nl_priv->res = (void *)data;
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	nla_put_flag(msg, MT76_TM_ATTR_STATS);
+
+	nla_nest_end(msg, ptr);
+
+	return unl_genl_request(&nl_priv->unl, msg, atenl_nl_get_rx_info_cb,
+				(void *)nl_priv);
+}
+
+static int
+atenl_nl_set_ru(struct atenl *an, struct atenl_data *data,
+		struct atenl_nl_priv *nl_priv)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct nl_msg *msg;
+	u32 *v = (u32 *)(hdr->data + 4);
+	u32 seg0_num = ntohl(v[0]);	/* v[1] seg1_num unused */
+	void *ptr;
+	int i, ret;
+
+	if (seg0_num > 8)
+		return -EINVAL;
+
+	for (i = 0, v = &v[2]; i < seg0_num; i++, v += 11) {
+		u32 ru_alloc = ntohl(v[1]);
+		u32 aid = ntohl(v[2]);
+		u32 ru_idx = ntohl(v[3]);
+		u32 mcs = ntohl(v[4]);
+		u32 ldpc = ntohl(v[5]);
+		u32 nss = ntohl(v[6]);
+		u32 tx_length = ntohl(v[8]);
+		char buf[10];
+
+		if (unl_genl_init(&nl_priv->unl, "nl80211") < 0) {
+			fprintf(stderr, "Failed to connect to nl80211\n");
+			return 2;
+		}
+
+		msg = unl_genl_msg(&nl_priv->unl, NL80211_CMD_TESTMODE, false);
+		nla_put_u32(msg, NL80211_ATTR_WIPHY, get_band_val(an, an->cur_band, phy_idx));
+
+		ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+		if (!ptr)
+			return -ENOMEM;
+
+		if (i == 0)
+			atenl_set_attr_state(an, msg, an->cur_band, MT76_TM_STATE_IDLE);
+
+		nla_put_u8(msg, MT76_TM_ATTR_AID, aid);
+		nla_put_u8(msg, MT76_TM_ATTR_RU_IDX, ru_idx);
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_IDX, mcs);
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_LDPC, ldpc);
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_NSS, nss);
+		nla_put_u32(msg, MT76_TM_ATTR_TX_LENGTH, tx_length);
+
+		ret = snprintf(buf, sizeof(buf), "%x", ru_alloc);
+		if (snprintf_error(sizeof(buf), ret))
+			return -EINVAL;
+
+		nla_put_u8(msg, MT76_TM_ATTR_RU_ALLOC, strtol(buf, NULL, 2));
+
+		nla_nest_end(msg, ptr);
+
+		unl_genl_request(&nl_priv->unl, msg, NULL, NULL);
+
+		unl_free(&nl_priv->unl);
+	}
+
+	return 0;
+}
+
+static int atenl_nl_ibf_set_val(struct atenl *an, struct atenl_data *data,
+				struct atenl_nl_priv *nl_priv)
+{
+#define MT_IBF(_act)	MT76_TM_TXBF_ACT_##_act
+	static const u8 bf_act_map[] = {
+		[TXBF_ACT_IBF_PHASE_COMP] = MT_IBF(PHASE_COMP),
+		[TXBF_ACT_IBF_PROF_UPDATE] = MT_IBF(IBF_PROF_UPDATE),
+		[TXBF_ACT_EBF_PROF_UPDATE] = MT_IBF(EBF_PROF_UPDATE),
+		[TXBF_ACT_IBF_PHASE_CAL] = MT_IBF(PHASE_CAL),
+	};
+#undef MT_IBF
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct nl_msg *msg = nl_priv->msg;
+	u32 *v = (u32 *)(hdr->data + 4);
+	u32 action = ntohl(v[0]);
+	u16 val[8];
+	void *ptr, *a;
+	char cmd[64];
+	int i;
+
+	for (i = 0; i < 8; i++)
+		val[i] = ntohl(v[i + 1]);
+
+	atenl_dbg("%s: action = %u, val = %u, %u, %u, %u, %u\n",
+		  __func__, action, val[0], val[1], val[2], val[3], val[4]);
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	switch (action) {
+	case TXBF_ACT_INIT:
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_MODE, MT76_TM_TX_MODE_HT);
+		nla_put_u8(msg, MT76_TM_ATTR_AID, 1);
+		nla_put_u8(msg, MT76_TM_ATTR_TXBF_ACT, MT76_TM_TXBF_ACT_INIT);
+
+		a = nla_nest_start(msg, MT76_TM_ATTR_TXBF_PARAM);
+		if (!a)
+			return -ENOMEM;
+		nla_put_u16(msg, 0, 1);
+		if (!val[0])
+			nla_put_u16(msg, 1, 1);	/* init */
+		nla_nest_end(msg, a);
+		break;
+	case TXBF_ACT_CHANNEL:
+		sprintf(cmd, "iw dev mon%d set channel %u HT20",
+			get_band_val(an, an->cur_band, phy_idx), val[0]);
+		system(cmd);
+		printf("%s: %s", __func__, cmd);
+		nla_put_u8(msg, MT76_TM_ATTR_TXBF_ACT, MT76_TM_TXBF_ACT_UPDATE_CH);
+		a = nla_nest_start(msg, MT76_TM_ATTR_TXBF_PARAM);
+		if (!a)
+			return -ENOMEM;
+		nla_put_u16(msg, 0, 0);
+		nla_nest_end(msg, a);
+		break;
+	case TXBF_ACT_MCS:
+		nla_put_u8(msg, MT76_TM_ATTR_TX_RATE_IDX, val[0]);
+		nla_put_u8(msg, MT76_TM_ATTR_TX_ANTENNA, (1 << DIV_ROUND_UP(val[0], 8)) - 1);
+		break;
+	case TXBF_ACT_POWER:
+		break;
+	case TXBF_ACT_TX_ANT:
+		nla_put_u8(msg, MT76_TM_ATTR_TX_ANTENNA, val[0]);
+		break;
+	case TXBF_ACT_RX_START:
+		atenl_set_attr_state(an, msg, an->cur_band, MT76_TM_STATE_RX_FRAMES);
+		break;
+	case TXBF_ACT_RX_ANT:
+		nla_put_u8(msg, MT76_TM_ATTR_TX_ANTENNA, val[0]);
+		break;
+	case TXBF_ACT_TX_PKT:
+		nla_put_u8(msg, MT76_TM_ATTR_AID, val[1]);
+		nla_put_u32(msg, MT76_TM_ATTR_TX_COUNT, 10000000);
+		nla_put_u8(msg, MT76_TM_ATTR_TXBF_ACT, MT76_TM_TXBF_ACT_TX_PREP);
+		a = nla_nest_start(msg, MT76_TM_ATTR_TXBF_PARAM);
+		if (!a)
+			return -ENOMEM;
+
+		for (i = 0; i < 5; i++)
+			nla_put_u16(msg, i, val[i]);
+		nla_nest_end(msg, a);
+
+		atenl_set_attr_state(an, msg, an->cur_band, MT76_TM_STATE_TX_FRAMES);
+		break;
+	case TXBF_ACT_IBF_PHASE_COMP:
+	case TXBF_ACT_IBF_PROF_UPDATE:
+	case TXBF_ACT_EBF_PROF_UPDATE:
+	case TXBF_ACT_IBF_PHASE_CAL:
+		nla_put_u8(msg, MT76_TM_ATTR_TXBF_ACT, bf_act_map[action]);
+		a = nla_nest_start(msg, MT76_TM_ATTR_TXBF_PARAM);
+		if (!a)
+			return -ENOMEM;
+
+		for (i = 0; i < 5; i++)
+			nla_put_u16(msg, i, val[i]);
+		nla_nest_end(msg, a);
+		break;
+	case TXBF_ACT_IBF_PHASE_E2P_UPDATE:
+		atenl_eeprom_read_from_driver(an, 0x651, 0x28 * 9);
+		atenl_eeprom_write_mtd(an);
+
+		nla_put_u8(msg, MT76_TM_ATTR_AID, 0);
+		nla_put_u8(msg, MT76_TM_ATTR_TXBF_ACT, MT76_TM_TXBF_ACT_INIT);
+
+		a = nla_nest_start(msg, MT76_TM_ATTR_TXBF_PARAM);
+		if (!a)
+			return -ENOMEM;
+		nla_put_u16(msg, 0, 0);
+		nla_nest_end(msg, a);
+		break;
+	default:
+		break;
+	}
+
+	nla_nest_end(msg, ptr);
+
+	memcpy(hdr->data + 2, &data->ext_id, 4);
+
+	return unl_genl_request(&nl_priv->unl, msg, NULL, NULL);
+}
+
+static int
+atenl_nl_ibf_get_status(struct atenl *an, struct atenl_data *data,
+			struct atenl_nl_priv *nl_priv)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	u32 status = htonl(1);
+
+	memcpy(hdr->data + 2, &data->ext_id, 4);
+	memcpy(hdr->data + 6, &status, 4);
+
+	return 0;
+}
+
+static int
+atenl_nl_ibf_profile_update_all(struct atenl *an, struct atenl_data *data,
+				struct atenl_nl_priv *nl_priv)
+{
+	struct atenl_cmd_hdr *hdr = atenl_hdr(data);
+	struct nl_msg *msg;
+	void *ptr, *a;
+	u32 *v = (u32 *)(hdr->data + 4);
+	u16 pfmu_idx = ntohl(v[0]);
+	int i;
+
+	for (i = 0, v = &v[5]; i < 64; i++, v += 5) {
+		int j;
+
+		if (unl_genl_init(&nl_priv->unl, "nl80211") < 0) {
+			fprintf(stderr, "Failed to connect to nl80211\n");
+			return 2;
+		}
+
+		msg = unl_genl_msg(&nl_priv->unl, NL80211_CMD_TESTMODE, false);
+		nla_put_u32(msg, NL80211_ATTR_WIPHY,
+			    get_band_val(an, an->cur_band, phy_idx));
+
+		ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+		if (!ptr)
+			return -ENOMEM;
+
+		nla_put_u8(msg, MT76_TM_ATTR_TXBF_ACT, MT76_TM_TXBF_ACT_PROF_UPDATE_ALL);
+		a = nla_nest_start(msg, MT76_TM_ATTR_TXBF_PARAM);
+		if (!a)
+			return -ENOMEM;
+		nla_put_u16(msg, 0, pfmu_idx);
+
+		for (j = 0; j < 5; j++)
+			nla_put_u16(msg, j + 1, ntohl(v[j]));
+		nla_nest_end(msg, a);
+
+		nla_nest_end(msg, ptr);
+
+		unl_genl_request(&nl_priv->unl, msg, NULL, NULL);
+
+		unl_free(&nl_priv->unl);
+	}
+
+	memcpy(hdr->data + 2, &data->ext_id, 4);
+
+	return 0;
+}
+
+#define NL_OPS_GROUP(cmd, ...)	[HQA_CMD_##cmd] = { __VA_ARGS__ }
+static const struct atenl_nl_ops nl_ops[] = {
+	NL_OPS_GROUP(SET_TX_PATH, .set=MT76_TM_ATTR_TX_ANTENNA),
+	NL_OPS_GROUP(SET_TX_POWER, .set=MT76_TM_ATTR_TX_POWER),
+	NL_OPS_GROUP(SET_RX_PATH, .set=MT76_TM_ATTR_TX_ANTENNA),
+	NL_OPS_GROUP(SET_FREQ_OFFSET, .set=MT76_TM_ATTR_FREQ_OFFSET),
+	NL_OPS_GROUP(SET_CFG, .ops=atenl_nl_set_cfg),
+	NL_OPS_GROUP(SET_TSSI, .ops=atenl_nl_set_cfg),
+	NL_OPS_GROUP(CONTINUOUS_TX, .ops=atenl_nl_continuous_tx),
+	NL_OPS_GROUP(GET_TX_INFO, .dump=MT76_TM_STATS_ATTR_TX_DONE),
+	NL_OPS_GROUP(GET_RX_INFO, .ops=atenl_nl_get_rx_info, .dump=true),
+	NL_OPS_GROUP(SET_RU, .ops=atenl_nl_set_ru),
+};
+#undef NL_OPS_GROUP
+
+#define NL_OPS_EXT(cmd, ...)	[HQA_EXT_CMD_##cmd] = { __VA_ARGS__ }
+static const struct atenl_nl_ops nl_ops_ext[] = {
+	NL_OPS_EXT(SET_TX, .ops=atenl_nl_set_tx),
+	NL_OPS_EXT(START_TX, .ops=atenl_nl_tx),
+	NL_OPS_EXT(STOP_TX, .ops=atenl_nl_tx),
+	NL_OPS_EXT(START_RX, .ops=atenl_nl_rx),
+	NL_OPS_EXT(STOP_RX, .ops=atenl_nl_rx),
+	NL_OPS_EXT(OFF_CH_SCAN, .ops=atenl_off_ch_scan),
+	NL_OPS_EXT(IBF_SET_VAL, .ops=atenl_nl_ibf_set_val),
+	NL_OPS_EXT(IBF_GET_STATUS, .ops=atenl_nl_ibf_get_status),
+	NL_OPS_EXT(IBF_PROF_UPDATE_ALL, .ops=atenl_nl_ibf_profile_update_all),
+};
+#undef NL_OPS_EXT
+
+int atenl_nl_process(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_nl_priv nl_priv = {};
+	const struct atenl_nl_ops *ops;
+	struct nl_msg *msg;
+	int ret = 0;
+
+	if (data->ext_cmd != 0)
+		ops = &nl_ops_ext[data->ext_cmd];
+	else
+		ops = &nl_ops[data->cmd];
+
+	if (unl_genl_init(&nl_priv.unl, "nl80211") < 0) {
+		fprintf(stderr, "Failed to connect to nl80211\n");
+		return -1;
+	}
+
+	msg = unl_genl_msg(&nl_priv.unl, NL80211_CMD_TESTMODE, !!ops->dump);
+	nla_put_u32(msg, NL80211_ATTR_WIPHY, get_band_val(an, an->cur_band, phy_idx));
+	nl_priv.msg = msg;
+
+	if (ops->ops) {
+		ret = ops->ops(an, data, &nl_priv);
+	} else if (ops->dump) {
+		nl_priv.attr = ops->dump;
+		ret = atenl_nl_dump_attr(an, data, &nl_priv);
+	} else {
+		nl_priv.attr = ops->set;
+		ret = atenl_nl_set_attr(an, data, &nl_priv);
+	}
+
+	if (ret)
+		atenl_err("command process error: %d (%d)\n", data->cmd, data->ext_cmd);
+
+	unl_free(&nl_priv.unl);
+
+	return ret;
+}
+
+int atenl_nl_process_many(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_nl_priv nl_priv = {};
+	const struct atenl_nl_ops *ops;
+	int ret = 0;
+
+	if (data->ext_cmd != 0)
+		ops = &nl_ops_ext[data->ext_cmd];
+	else
+		ops = &nl_ops[data->cmd];
+
+	if (ops->ops)
+		ret = ops->ops(an, data, &nl_priv);
+
+	return ret;
+}
+
+int atenl_nl_set_state(struct atenl *an, u8 band,
+		       enum mt76_testmode_state state)
+{
+	struct atenl_nl_priv nl_priv = {};
+	struct nl_msg *msg;
+	void *ptr;
+
+	if (unl_genl_init(&nl_priv.unl, "nl80211") < 0) {
+		fprintf(stderr, "Failed to connect to nl80211\n");
+		return 2;
+	}
+
+	msg = unl_genl_msg(&nl_priv.unl, NL80211_CMD_TESTMODE, false);
+	nla_put_u32(msg, NL80211_ATTR_WIPHY, get_band_val(an, band, phy_idx));
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	atenl_set_attr_state(an, msg, band, state);
+
+	nla_nest_end(msg, ptr);
+
+	unl_genl_request(&nl_priv.unl, msg, NULL, NULL);
+
+	unl_free(&nl_priv.unl);
+
+	return 0;
+}
+
+static int atenl_nl_check_mtd_cb(struct nl_msg *msg, void *arg)
+{
+	struct atenl_nl_priv *nl_priv = (struct atenl_nl_priv *)arg;
+	struct atenl *an = nl_priv->an;
+	struct nlattr *tb[NUM_MT76_TM_ATTRS];
+	struct nlattr *attr;
+
+	attr = unl_find_attr(&nl_priv->unl, msg, NL80211_ATTR_TESTDATA);
+	if (!attr)
+		return NL_SKIP;
+
+	nla_parse_nested(tb, MT76_TM_ATTR_MAX, attr, testdata_policy);
+	if (!tb[MT76_TM_ATTR_MTD_PART] || !tb[MT76_TM_ATTR_MTD_OFFSET])
+		return NL_SKIP;
+
+	an->mtd_part = strdup(nla_get_string(tb[MT76_TM_ATTR_MTD_PART]));
+	an->mtd_offset = nla_get_u32(tb[MT76_TM_ATTR_MTD_OFFSET]);
+
+	return NL_SKIP;
+}
+
+int atenl_nl_check_mtd(struct atenl *an)
+{
+	struct atenl_nl_priv nl_priv = { .an = an };
+	struct nl_msg *msg;
+
+	if (unl_genl_init(&nl_priv.unl, "nl80211") < 0) {
+		fprintf(stderr, "Failed to connect to nl80211\n");
+		return 2;
+	}
+
+	msg = unl_genl_msg(&nl_priv.unl, NL80211_CMD_TESTMODE, true);
+	nla_put_u32(msg, NL80211_ATTR_WIPHY, get_band_val(an, 0, phy_idx));
+	unl_genl_request(&nl_priv.unl, msg, atenl_nl_check_mtd_cb, (void *)&nl_priv);
+
+	unl_free(&nl_priv.unl);
+
+	return 0;
+}
+
+int atenl_nl_write_eeprom(struct atenl *an, u32 offset, u8 *val, int len)
+{
+	struct atenl_nl_priv nl_priv = {};
+	struct nl_msg *msg;
+	void *ptr, *a;
+	int i;
+
+	if (unl_genl_init(&nl_priv.unl, "nl80211") < 0) {
+		fprintf(stderr, "Failed to connect to nl80211\n");
+		return 2;
+	}
+
+	if (len > 16)
+		return -EINVAL;
+
+	msg = unl_genl_msg(&nl_priv.unl, NL80211_CMD_TESTMODE, false);
+	nla_put_u32(msg, NL80211_ATTR_WIPHY, get_band_val(an, 0, phy_idx));
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	nla_put_u8(msg, MT76_TM_ATTR_EEPROM_ACTION,
+		   MT76_TM_EEPROM_ACTION_UPDATE_DATA);
+	nla_put_u32(msg, MT76_TM_ATTR_EEPROM_OFFSET, offset);
+
+	a = nla_nest_start(msg, MT76_TM_ATTR_EEPROM_VAL);
+	if (!a)
+		return -ENOMEM;
+
+	for (i = 0; i < len; i++)
+		if (nla_put_u8(msg, i, val[i]))
+			goto out;
+
+	nla_nest_end(msg, a);
+
+	nla_nest_end(msg, ptr);
+
+	unl_genl_request(&nl_priv.unl, msg, NULL, NULL);
+
+	unl_free(&nl_priv.unl);
+
+out:
+	return 0;
+}
+
+int atenl_nl_write_efuse_all(struct atenl *an, struct atenl_data *data)
+{
+	struct atenl_nl_priv nl_priv = {};
+	struct nl_msg *msg;
+	void *ptr;
+
+	if (unl_genl_init(&nl_priv.unl, "nl80211") < 0) {
+		fprintf(stderr, "Failed to connect to nl80211\n");
+		return 2;
+	}
+
+	msg = unl_genl_msg(&nl_priv.unl, NL80211_CMD_TESTMODE, false);
+	nla_put_u32(msg, NL80211_ATTR_WIPHY, get_band_val(an, 0, phy_idx));
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	nla_put_u8(msg, MT76_TM_ATTR_EEPROM_ACTION,
+		   MT76_TM_EEPROM_ACTION_WRITE_TO_EFUSE);
+
+	nla_nest_end(msg, ptr);
+
+	unl_genl_request(&nl_priv.unl, msg, NULL, NULL);
+
+	unl_free(&nl_priv.unl);
+
+	return 0;
+}
+
+int atenl_nl_update_buffer_mode(struct atenl *an)
+{
+	struct atenl_nl_priv nl_priv = {};
+	struct nl_msg *msg;
+	void *ptr;
+
+	if (unl_genl_init(&nl_priv.unl, "nl80211") < 0) {
+		fprintf(stderr, "Failed to connect to nl80211\n");
+		return 2;
+	}
+
+	msg = unl_genl_msg(&nl_priv.unl, NL80211_CMD_TESTMODE, false);
+	nla_put_u32(msg, NL80211_ATTR_WIPHY, get_band_val(an, 0, phy_idx));
+
+	ptr = nla_nest_start(msg, NL80211_ATTR_TESTDATA);
+	if (!ptr)
+		return -ENOMEM;
+
+	nla_put_u8(msg, MT76_TM_ATTR_EEPROM_ACTION,
+		   MT76_TM_EEPROM_ACTION_UPDATE_BUFFER_MODE);
+
+	nla_nest_end(msg, ptr);
+
+	unl_genl_request(&nl_priv.unl, msg, NULL, NULL);
+
+	unl_free(&nl_priv.unl);
+
+	return 0;
+}
+
diff --git a/feed/atenl/src/nl.h b/feed/atenl/src/nl.h
new file mode 100644
index 0000000..2a0685d
--- /dev/null
+++ b/feed/atenl/src/nl.h
@@ -0,0 +1,242 @@
+/* Copyright (C) 2021-2022 Mediatek Inc. */
+#ifndef __ATENL_NL_H
+#define __ATENL_NL_H
+
+/* This is copied from mt76/testmode.h */
+
+/**
+ * enum mt76_testmode_attr - testmode attributes inside NL80211_ATTR_TESTDATA
+ *
+ * @MT76_TM_ATTR_UNSPEC: (invalid attribute)
+ *
+ * @MT76_TM_ATTR_RESET: reset parameters to default (flag)
+ * @MT76_TM_ATTR_STATE: test state (u32), see &enum mt76_testmode_state
+ *
+ * @MT76_TM_ATTR_MTD_PART: mtd partition used for eeprom data (string)
+ * @MT76_TM_ATTR_MTD_OFFSET: offset of eeprom data within the partition (u32)
+ *
+ * @MT76_TM_ATTR_TX_COUNT: configured number of frames to send when setting
+ *	state to MT76_TM_STATE_TX_FRAMES (u32)
+ * @MT76_TM_ATTR_TX_PENDING: pending frames during MT76_TM_STATE_TX_FRAMES (u32)
+ * @MT76_TM_ATTR_TX_LENGTH: packet tx msdu length (u32)
+ * @MT76_TM_ATTR_TX_RATE_MODE: packet tx mode (u8, see &enum mt76_testmode_tx_mode)
+ * @MT76_TM_ATTR_TX_RATE_NSS: packet tx number of spatial streams (u8)
+ * @MT76_TM_ATTR_TX_RATE_IDX: packet tx rate/MCS index (u8)
+ * @MT76_TM_ATTR_TX_RATE_SGI: packet tx use short guard interval (u8)
+ * @MT76_TM_ATTR_TX_RATE_LDPC: packet tx enable LDPC (u8)
+ * @MT76_TM_ATTR_TX_RATE_STBC: packet tx enable STBC (u8)
+ * @MT76_TM_ATTR_TX_LTF: packet tx LTF, set 0 to 2 for 1x, 2x, and 4x LTF (u8)
+ *
+ * @MT76_TM_ATTR_TX_ANTENNA: tx antenna mask (u8)
+ * @MT76_TM_ATTR_TX_POWER_CONTROL: enable tx power control (u8)
+ * @MT76_TM_ATTR_TX_POWER: per-antenna tx power array (nested, u8 attrs)
+ *
+ * @MT76_TM_ATTR_FREQ_OFFSET: RF frequency offset (u32)
+ *
+ * @MT76_TM_ATTR_STATS: statistics (nested, see &enum mt76_testmode_stats_attr)
+ *
+ * @MT76_TM_ATTR_TX_SPE_IDX: tx spatial extension index (u8)
+ *
+ * @MT76_TM_ATTR_TX_DUTY_CYCLE: packet tx duty cycle (u8)
+ * @MT76_TM_ATTR_TX_IPG: tx inter-packet gap, in unit of us (u32)
+ * @MT76_TM_ATTR_TX_TIME: packet transmission time, in unit of us (u32)
+ *
+ */
+enum mt76_testmode_attr {
+	MT76_TM_ATTR_UNSPEC,
+
+	MT76_TM_ATTR_RESET,
+	MT76_TM_ATTR_STATE,
+
+	MT76_TM_ATTR_MTD_PART,
+	MT76_TM_ATTR_MTD_OFFSET,
+
+	MT76_TM_ATTR_TX_COUNT,
+	MT76_TM_ATTR_TX_LENGTH,
+	MT76_TM_ATTR_TX_RATE_MODE,
+	MT76_TM_ATTR_TX_RATE_NSS,
+	MT76_TM_ATTR_TX_RATE_IDX,
+	MT76_TM_ATTR_TX_RATE_SGI,
+	MT76_TM_ATTR_TX_RATE_LDPC,
+	MT76_TM_ATTR_TX_RATE_STBC,
+	MT76_TM_ATTR_TX_LTF,
+
+	MT76_TM_ATTR_TX_ANTENNA,
+	MT76_TM_ATTR_TX_POWER_CONTROL,
+	MT76_TM_ATTR_TX_POWER,
+
+	MT76_TM_ATTR_FREQ_OFFSET,
+
+	MT76_TM_ATTR_STATS,
+
+	MT76_TM_ATTR_TX_SPE_IDX,
+
+	MT76_TM_ATTR_TX_DUTY_CYCLE,
+	MT76_TM_ATTR_TX_IPG,
+	MT76_TM_ATTR_TX_TIME,
+
+	MT76_TM_ATTR_DRV_DATA,
+
+	MT76_TM_ATTR_MAC_ADDRS,
+
+	MT76_TM_ATTR_EEPROM_ACTION,
+	MT76_TM_ATTR_EEPROM_OFFSET,
+	MT76_TM_ATTR_EEPROM_VAL,
+
+	MT76_TM_ATTR_CFG,
+
+	MT76_TM_ATTR_OFF_CH_SCAN_CH,
+	MT76_TM_ATTR_OFF_CH_SCAN_CENTER_CH,
+	MT76_TM_ATTR_OFF_CH_SCAN_BW,
+	MT76_TM_ATTR_OFF_CH_SCAN_PATH,
+
+	MT76_TM_ATTR_AID,
+	MT76_TM_ATTR_RU_ALLOC,
+	MT76_TM_ATTR_RU_IDX,
+
+	MT76_TM_ATTR_TXBF_ACT,
+	MT76_TM_ATTR_TXBF_PARAM,
+
+	/* keep last */
+	NUM_MT76_TM_ATTRS,
+	MT76_TM_ATTR_MAX = NUM_MT76_TM_ATTRS - 1,
+};
+
+/**
+ * enum mt76_testmode_state - statistics attributes
+ *
+ * @MT76_TM_STATS_ATTR_TX_PENDING: pending tx frames (u32)
+ * @MT76_TM_STATS_ATTR_TX_QUEUED: queued tx frames (u32)
+ * @MT76_TM_STATS_ATTR_TX_QUEUED: completed tx frames (u32)
+ *
+ * @MT76_TM_STATS_ATTR_RX_PACKETS: number of rx packets (u64)
+ * @MT76_TM_STATS_ATTR_RX_FCS_ERROR: number of rx packets with FCS error (u64)
+ * @MT76_TM_STATS_ATTR_LAST_RX: information about the last received packet
+ *	see &enum mt76_testmode_rx_attr
+ */
+enum mt76_testmode_stats_attr {
+	MT76_TM_STATS_ATTR_UNSPEC,
+	MT76_TM_STATS_ATTR_PAD,
+
+	MT76_TM_STATS_ATTR_TX_PENDING,
+	MT76_TM_STATS_ATTR_TX_QUEUED,
+	MT76_TM_STATS_ATTR_TX_DONE,
+
+	MT76_TM_STATS_ATTR_RX_PACKETS,
+	MT76_TM_STATS_ATTR_RX_FCS_ERROR,
+	MT76_TM_STATS_ATTR_LAST_RX,
+	MT76_TM_STATS_ATTR_RX_LEN_MISMATCH,
+
+	/* keep last */
+	NUM_MT76_TM_STATS_ATTRS,
+	MT76_TM_STATS_ATTR_MAX = NUM_MT76_TM_STATS_ATTRS - 1,
+};
+
+
+/**
+ * enum mt76_testmode_rx_attr - packet rx information
+ *
+ * @MT76_TM_RX_ATTR_FREQ_OFFSET: frequency offset (s32)
+ * @MT76_TM_RX_ATTR_RCPI: received channel power indicator (array, u8)
+ * @MT76_TM_RX_ATTR_IB_RSSI: internal inband RSSI (array, s8)
+ * @MT76_TM_RX_ATTR_WB_RSSI: internal wideband RSSI (array, s8)
+ * @MT76_TM_RX_ATTR_SNR: signal-to-noise ratio (u8)
+ */
+enum mt76_testmode_rx_attr {
+	MT76_TM_RX_ATTR_UNSPEC,
+
+	MT76_TM_RX_ATTR_FREQ_OFFSET,
+	MT76_TM_RX_ATTR_RCPI,
+	MT76_TM_RX_ATTR_IB_RSSI,
+	MT76_TM_RX_ATTR_WB_RSSI,
+	MT76_TM_RX_ATTR_SNR,
+
+	/* keep last */
+	NUM_MT76_TM_RX_ATTRS,
+	MT76_TM_RX_ATTR_MAX = NUM_MT76_TM_RX_ATTRS - 1,
+};
+
+/**
+ * enum mt76_testmode_state - phy test state
+ *
+ * @MT76_TM_STATE_OFF: test mode disabled (normal operation)
+ * @MT76_TM_STATE_IDLE: test mode enabled, but idle
+ * @MT76_TM_STATE_TX_FRAMES: send a fixed number of test frames
+ * @MT76_TM_STATE_RX_FRAMES: receive packets and keep statistics
+ * @MT76_TM_STATE_TX_CONT: waveform tx without time gap
+ * @MT76_TM_STATE_ON: test mode enabled used in offload firmware
+ */
+enum mt76_testmode_state {
+	MT76_TM_STATE_OFF,
+	MT76_TM_STATE_IDLE,
+	MT76_TM_STATE_TX_FRAMES,
+	MT76_TM_STATE_RX_FRAMES,
+	MT76_TM_STATE_TX_CONT,
+	MT76_TM_STATE_ON,
+
+	/* keep last */
+	NUM_MT76_TM_STATES,
+	MT76_TM_STATE_MAX = NUM_MT76_TM_STATES - 1,
+};
+
+/**
+ * enum mt76_testmode_tx_mode - packet tx phy mode
+ *
+ * @MT76_TM_TX_MODE_CCK: legacy CCK mode
+ * @MT76_TM_TX_MODE_OFDM: legacy OFDM mode
+ * @MT76_TM_TX_MODE_HT: 802.11n MCS
+ * @MT76_TM_TX_MODE_VHT: 802.11ac MCS
+ * @MT76_TM_TX_MODE_HE_SU: 802.11ax single-user MIMO
+ * @MT76_TM_TX_MODE_HE_EXT_SU: 802.11ax extended-range SU
+ * @MT76_TM_TX_MODE_HE_TB: 802.11ax trigger-based
+ * @MT76_TM_TX_MODE_HE_MU: 802.11ax multi-user MIMO
+ */
+enum mt76_testmode_tx_mode {
+	MT76_TM_TX_MODE_CCK,
+	MT76_TM_TX_MODE_OFDM,
+	MT76_TM_TX_MODE_HT,
+	MT76_TM_TX_MODE_VHT,
+	MT76_TM_TX_MODE_HE_SU,
+	MT76_TM_TX_MODE_HE_EXT_SU,
+	MT76_TM_TX_MODE_HE_TB,
+	MT76_TM_TX_MODE_HE_MU,
+
+	/* keep last */
+	NUM_MT76_TM_TX_MODES,
+	MT76_TM_TX_MODE_MAX = NUM_MT76_TM_TX_MODES - 1,
+};
+
+/**
+ * enum mt76_testmode_eeprom_action - eeprom setting actions
+ *
+ * @MT76_TM_EEPROM_ACTION_UPDATE_DATA: update rf values to specific
+ * 	eeprom data block
+ * @MT76_TM_EEPROM_ACTION_UPDATE_BUFFER_MODE: send updated eeprom data to fw
+ * @MT76_TM_EEPROM_ACTION_WRITE_TO_EFUSE: write eeprom data back to efuse
+ */
+enum mt76_testmode_eeprom_action {
+	MT76_TM_EEPROM_ACTION_UPDATE_DATA,
+	MT76_TM_EEPROM_ACTION_UPDATE_BUFFER_MODE,
+	MT76_TM_EEPROM_ACTION_WRITE_TO_EFUSE,
+
+	/* keep last */
+	NUM_MT76_TM_EEPROM_ACTION,
+	MT76_TM_EEPROM_ACTION_MAX = NUM_MT76_TM_EEPROM_ACTION - 1,
+};
+
+enum mt76_testmode_txbf_act {
+	MT76_TM_TXBF_ACT_INIT,
+	MT76_TM_TXBF_ACT_UPDATE_CH,
+	MT76_TM_TXBF_ACT_PHASE_COMP,
+	MT76_TM_TXBF_ACT_TX_PREP,
+	MT76_TM_TXBF_ACT_IBF_PROF_UPDATE,
+	MT76_TM_TXBF_ACT_EBF_PROF_UPDATE,
+	MT76_TM_TXBF_ACT_PHASE_CAL,
+	MT76_TM_TXBF_ACT_PROF_UPDATE_ALL,
+
+	/* keep last */
+	NUM_MT76_TM_TXBF_ACT,
+	MT76_TM_TXBF_ACT_MAX = NUM_MT76_TM_TXBF_ACT - 1,
+};
+
+#endif
diff --git a/feed/atenl/src/util.c b/feed/atenl/src/util.c
new file mode 100644
index 0000000..1b10663
--- /dev/null
+++ b/feed/atenl/src/util.c
@@ -0,0 +1,105 @@
+/* Copyright (C) 2021-2022 Mediatek Inc. */
+
+#include "atenl.h"
+
+int atenl_reg_read(struct atenl *an, u32 offset, u32 *res)
+{
+	char dir[64], buf[16];
+	unsigned long val;
+	int fd, ret;
+
+	/* write offset into regidx */
+	ret = snprintf(dir, sizeof(dir),
+		       "/sys/kernel/debug/ieee80211/phy%d/mt76/regidx",
+		       get_band_val(an, 0, phy_idx));
+	if (snprintf_error(sizeof(dir), ret))
+		return ret;
+
+	fd = open(dir, O_WRONLY);
+	if (fd < 0)
+		return fd;
+
+	ret = snprintf(buf, sizeof(buf), "0x%x", offset);
+	if (snprintf_error(sizeof(buf), ret))
+		goto out;
+
+	lseek(fd, 0, SEEK_SET);
+	write(fd, buf, sizeof(buf));
+	close(fd);
+
+	/* read value from regval */
+	ret = snprintf(dir, sizeof(dir),
+		       "/sys/kernel/debug/ieee80211/phy%d/mt76/regval",
+		       get_band_val(an, 0, phy_idx));
+	if (snprintf_error(sizeof(dir), ret))
+		return ret;
+
+	fd = open(dir, O_RDONLY);
+	if (fd < 0)
+		return fd;
+
+	ret = read(fd, buf, sizeof(buf) - 1);
+	if (ret < 0)
+		goto out;
+	buf[ret] = 0;
+
+	val = strtoul(buf, NULL, 16);
+	if (val > (u32) -1)
+		return -EINVAL;
+
+	*res = val;
+	ret = 0;
+out:
+	close(fd);
+
+	return ret;
+}
+
+int atenl_reg_write(struct atenl *an, u32 offset, u32 val)
+{
+	char dir[64], buf[16];
+	int fd, ret;
+
+	/* write offset into regidx */
+	ret = snprintf(dir, sizeof(dir),
+		       "/sys/kernel/debug/ieee80211/phy%d/mt76/regidx",
+		       get_band_val(an, 0, phy_idx));
+	if (snprintf_error(sizeof(dir), ret))
+		return ret;
+
+	fd = open(dir, O_WRONLY);
+	if (fd < 0)
+		return fd;
+
+	ret = snprintf(buf, sizeof(buf), "0x%x", offset);
+	if (snprintf_error(sizeof(buf), ret))
+		goto out;
+
+	lseek(fd, 0, SEEK_SET);
+	write(fd, buf, sizeof(buf));
+	close(fd);
+
+	/* write value into regval */
+	ret = snprintf(dir, sizeof(dir),
+		       "/sys/kernel/debug/ieee80211/phy%d/mt76/regval",
+		       get_band_val(an, 0, phy_idx));
+	if (snprintf_error(sizeof(dir), ret))
+		return ret;
+
+	fd = open(dir, O_WRONLY);
+	if (fd < 0)
+		return fd;
+
+	ret = snprintf(buf, sizeof(buf), "0x%x", val);
+	if (snprintf_error(sizeof(buf), ret))
+		goto out;
+	buf[ret] = 0;
+
+	lseek(fd, 0, SEEK_SET);
+	write(fd, buf, sizeof(buf));
+	ret = 0;
+out:
+	close(fd);
+
+	return ret;
+}
diff --git a/feed/atenl/src/util.h b/feed/atenl/src/util.h
new file mode 100644
index 0000000..a02b956
--- /dev/null
+++ b/feed/atenl/src/util.h
@@ -0,0 +1,101 @@
+/* Copyright (C) 2021-2022 Mediatek Inc. */
+#ifndef __ATENL_UTIL_H
+#define __ATENL_UTIL_H
+
+#include <linux/const.h>
+#include <linux/if_arp.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <stdint.h>
+#include <string.h>
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+typedef uint64_t u64;
+typedef int8_t s8;
+typedef int16_t s16;
+typedef int32_t s32;
+typedef int64_t s64, ktime_t;
+
+#ifndef __WORDSIZE
+#define __WORDSIZE (__SIZEOF_LONG__ * 8)
+#endif
+
+#ifndef BITS_PER_LONG
+#define BITS_PER_LONG __WORDSIZE
+#endif
+
+#define UL(x)		(_UL(x))
+#define ULL(x)		(_ULL(x))
+
+#define BIT(nr)		(1UL << (nr))
+
+#define GENMASK_INPUT_CHECK(h, l) 0
+#define __GENMASK(h, l) \
+	(((~UL(0)) - (UL(1) << (l)) + 1) & \
+	 (~UL(0) >> (BITS_PER_LONG - 1 - (h))))
+#define GENMASK(h, l) \
+	(GENMASK_INPUT_CHECK(h, l) + __GENMASK(h, l))
+
+#define __bf_shf(x) (__builtin_ffsll(x) - 1)
+#define FIELD_GET(_mask, _reg)						\
+	({								\
+		(typeof(_mask))(((_reg) & (_mask)) >> __bf_shf(_mask));	\
+	})
+
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
+#endif
+
+#ifndef DIV_ROUND_UP
+#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
+#endif
+
+#define PIPE_READ 0
+#define PIPE_WRITE 1
+
+#define MAC2STR(a) (a)[0], (a)[1], (a)[2], (a)[3], (a)[4], (a)[5]
+#define MACSTR "%02x:%02x:%02x:%02x:%02x:%02x"
+
+static inline bool ether_addr_equal(const u8 *addr1, const u8 *addr2)
+{
+	const u16 *a = (const u16 *)addr1;
+	const u16 *b = (const u16 *)addr2;
+
+	return ((a[0] ^ b[0]) | (a[1] ^ b[1]) | (a[2] ^ b[2])) == 0;
+}
+
+static inline bool is_broadcast_ether_addr(const u8 *addr)
+{
+	return (*(const u16 *)(addr + 0) &
+		*(const u16 *)(addr + 2) &
+		*(const u16 *)(addr + 4)) == 0xffff;
+}
+
+static inline bool is_multicast_ether_addr(const u8 *addr)
+{
+	return 0x01 & addr[0];
+}
+
+static inline void eth_broadcast_addr(u8 *addr)
+{
+	memset(addr, 0xff, ETH_ALEN);
+}
+
+static inline void ether_addr_copy(u8 *dst, const u8 *src)
+{
+	u16 *a = (u16 *)dst;
+	const u16 *b = (const u16 *)src;
+
+	a[0] = b[0];
+	a[1] = b[1];
+	a[2] = b[2];
+}
+
+static inline int snprintf_error(size_t size, int res)
+{
+	return res < 0 || (unsigned int) res >= size;
+}
+
+#endif