Skip to content

Repository files navigation

RT2570 WiFi Hard Block Fix

Symptom: Built-in WiFi card is completely dead. rfkill says Hard blocked: yes. No physical switch exists. Toggling rfkill does nothing. WiFi works fine under Windows.

This guide explains what causes the problem, how it was diagnosed, and how to fix it permanently — on both Gentoo Linux (OpenRC) and Debian 12 (systemd).

Quick install

Clone the repo and run the installer as root. It detects your init system (OpenRC or systemd) and installs the right files automatically.

git clone https://github.com/huppiflupp/rt2570-rfkill-fix.git
cd rt2570-rfkill-fix
sudo ./install.sh

No git? Download all files with curl instead:

mkdir rt2570-rfkill-fix && cd rt2570-rfkill-fix
for f in rt2570-unlock-rfkill rt2570-rfkill.service rt2570-rfkill.rules rt2570-rfkill.start install.sh; do
  curl -sO https://raw.githubusercontent.com/huppiflupp/rt2570-rfkill-fix/main/$f
done
chmod +x install.sh
sudo ./install.sh

To uninstall:

sudo ./install.sh uninstall

The manual step-by-step instructions are in section 6 if you prefer not to use the installer.


Table of Contents

  1. What is rfkill and what does "hard blocked" mean?
  2. The hardware
  3. How the problem was found
  4. What is actually wrong
  5. The fix — one command
  6. Making the fix survive a reboot
  7. Files in this repository
  8. Technical deep-dive

1. What is rfkill and what does "hard blocked" mean?

Modern computers have a system called rfkill that can turn wireless radios (WiFi, Bluetooth, mobile data) on and off in software. Think of it as a master power switch for radio signals.

rfkill has two kinds of blocks:

Block type What sets it Can software clear it?
Soft block The operating system Yes — rfkill unblock wifi
Hard block A hardware signal from the chip itself Normally no

When rfkill list shows:

1: phy0: Wireless LAN
    Soft blocked: no
    Hard blocked: yes

…it means the WiFi chip itself is telling Linux "I am disabled". Linux trusts this and refuses to use the radio. This is by design — hard blocks are meant to honour physical kill switches (like on old laptops).

The problem here is that the chip is lying. It is asserting the "I am disabled" signal not because of a physical switch, but because a tiny register inside the chip was never set correctly by the Linux driver.


2. The hardware

  • WiFi chip: Ralink RT2570 (USB, built-in, not a dongle)
  • USB IDs: vendor 148f, product 2570
  • Linux driver: rt2500usb
  • Kernel interface name: wlp0s29f7u6u6 (may differ on your system)

The RT2570 is connected internally to a USB port on the motherboard — it looks like an external USB device to the operating system, but there is no dongle.


3. How the problem was found

Step 1 — Confirm the block is "hard"

rfkill list all

Output showed Hard blocked: yes with hard_block_reasons: 0x1, which means the chip itself is asserting a physical kill signal — not a user setting.

Step 2 — Rule out external hardware

The motherboard's GPIO (general-purpose input/output) pins were checked. These are tiny programmable signals used to control hardware. On this system the CPU chipset is an Intel ICH6, which exposes its GPIO registers at a known address.

All ICH6 GPIO output pins were read and the ones that were LOW (off) were identified as candidates for the kill signal. After toggling them, the rfkill state did not change — so the kill was not coming from the motherboard.

Step 3 — Read the Linux driver source

The driver file rt2500usb.c contains this function:

static int rt2500usb_rfkill_poll(struct rt2x00_dev *rt2x00dev)
{
    u16 reg;
    reg = rt2500usb_register_read(rt2x00dev, MAC_CSR19);
    return rt2x00_get_field16(reg, MAC_CSR19_VAL7);
}

This function runs periodically to decide whether to report a hard block. It reads a register called MAC_CSR19 from the chip and checks bit 7.

  • If bit 7 is 1 → radio is enabled
  • If bit 7 is 0 → hard block is reported

Step 4 — Read the register directly

Using the kernel's debug filesystem (debugfs), the current value of MAC_CSR19 was read directly from the chip:

MAC_CSR19 = 0xFE00

In binary: 1111 1110 0000 0000

  • Bits 9–15 (1111111) = GPIO pins 1–7 are configured as outputs
  • Bits 0–7 (00000000) = all output values are zero (LOW)

Bit 7 is 0 → the driver reports "hard blocked". The GPIO7 output is driving LOW, which the rfkill poll reads as "radio disabled".

Step 5 — Understand what Windows does differently

The Windows driver sets GPIO7 to output-HIGH during startup. The Linux driver never does this — it sets up other GPIO pins but leaves GPIO7 at zero.

Because Linux never sets bit 7 high, the chip always appears to be in the hardware-kill state.


4. What is actually wrong

Inside the RT2570 chip there is a register called MAC_CSR19. It controls eight GPIO (general-purpose input/output) pins on the chip itself.

GPIO7 (bit 7 of MAC_CSR19) is used as the hardware radio kill signal. When this bit is 0, the Linux driver believes a hardware kill switch is asserting "radio OFF". When it is 1, the radio is allowed to run.

The Linux rt2500usb driver initialises GPIO7 as an output but never sets it high. So bit 7 stays at 0 forever, and rfkill permanently reports Hard blocked: yes.

The Windows driver sets bit 7 high at startup, which is why WiFi works in Windows.

This is a bug in the Linux driver. The fix is to write the correct value to MAC_CSR19 so that bit 7 is 1.

The register address in the chip is 0x0426. The value we need to write is 0xFE80:

Bits Value Meaning
15–9 1111111 GPIO 1–7 configured as outputs (unchanged)
8 0 GPIO 0 configured as input (unchanged)
7 1 GPIO7 output HIGH — releases the kill block
6–0 0000000 Other GPIO values (unchanged)

5. The fix — one command

The Linux kernel exposes a debug interface that lets you read and write chip registers directly. To use it, find the correct path on your system:

ls /sys/kernel/debug/ieee80211/

You will see something like phy0 or phy1. Use whichever one exists. Then:

# Replace phy0 with the correct name on your system
REG=/sys/kernel/debug/ieee80211/phy0/rt2500usb/register

echo 19     > $REG/csr_offset
echo 0xFE80 > $REG/csr_value

Why 19? The register address 0x0426 minus the chip's base address 0x0400 equals 0x26 = 38 bytes = 19 two-byte words. The debug interface uses word numbering, not byte addresses.

After running these two commands, check the result:

rfkill list all

You should see:

1: phy0: Wireless LAN
    Soft blocked: no
    Hard blocked: no

WiFi is now unblocked. You can bring the interface up normally.


6. Making the fix survive a reboot

The register is reset every time the system boots, so the fix needs to run automatically at startup. There are two methods depending on your Linux distribution.


Gentoo / OpenRC

OpenRC is the init system used by Gentoo Linux. It has a simple mechanism called local.d — any script placed in /etc/local.d/ with a .start extension is run automatically at boot.

Install the unlock script:

cp rt2570-unlock-rfkill /usr/local/sbin/
chmod +x /usr/local/sbin/rt2570-unlock-rfkill

Install the boot hook:

cp rt2570-rfkill.start /etc/local.d/
chmod +x /etc/local.d/rt2570-rfkill.start

Verify the local service is active:

rc-update show | grep local

You should see local listed under the default runlevel. If not:

rc-update add local default

That is all. On the next boot the script will run automatically, find the debugfs path, write the correct register value, and release the hard block before your network manager tries to connect.


Debian 12 / systemd

Debian 12 uses systemd as its init system and udev to load kernel modules. Because the rt2500usb module is loaded by udev rather than by systemd-modules-load.service, a plain systemd service that runs at multi-user.target starts before the USB driver has bound to the device. The fix is a udev rule that triggers the service at exactly the right moment — when the RT2570 USB device is bound to its driver.

Install the dependency:

sudo apt install python3-usb

The unlock script is written in Python and sends the register write directly over USB — no CONFIG_RT2X00_DEBUG or debugfs access required. Debian's stock kernel has that option disabled, so the debugfs path never appears.

Install the unlock script:

sudo cp rt2570-unlock-rfkill /usr/local/sbin/
sudo chmod +x /usr/local/sbin/rt2570-unlock-rfkill

Install the udev rule:

sudo cp rt2570-rfkill.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules

Install the systemd service:

sudo cp rt2570-rfkill.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable rt2570-rfkill.service

The service is enabled so systemd knows about it, but on Debian 12 it is started by the udev rule, not by the normal boot target order. The enable step just prevents systemd from complaining about an unknown unit.

Test it immediately without rebooting:

# Simulate what udev does at boot: rebind the USB device
echo 0 | sudo tee /sys/bus/usb/devices/<BUS>-<PORT>/authorized
echo 1 | sudo tee /sys/bus/usb/devices/<BUS>-<PORT>/authorized

Replace <BUS>-<PORT> with your device's path — find it with:

lsusb -d 148f:2570
# e.g. "Bus 001 Device 003" → path is likely 1-6 or similar
ls /sys/bus/usb/devices/ | xargs -I{} sh -c \
  'grep -q 148f $(ls /sys/bus/usb/devices/{}/idVendor 2>/dev/null) && echo {}'

Or simply reboot to confirm. After the reboot:

sudo systemctl status rt2570-rfkill.service
rfkill list all

You should see the service as active (exited) and Hard blocked: no.


7. Files in this repository

File What it does
rt2570-unlock-rfkill The unlock script (Python). Sends a USB vendor control transfer to write 0xFE80 to MAC_CSR19. Does not require CONFIG_RT2X00_DEBUG or debugfs. Requires python3-usb.
rt2570-rfkill.start Boot hook for Gentoo / OpenRC. Place in /etc/local.d/.
rt2570-rfkill.service systemd service unit for Debian 12. Place in /etc/systemd/system/.
rt2570-rfkill.rules udev rule for Debian 12. Triggers the service when the RT2570 USB device binds. Place in /etc/udev/rules.d/.
install.sh Installer script. Detects OpenRC or systemd and installs the correct files. Run as root: sudo ./install.sh. Supports uninstall too.

8. Technical deep-dive

This section is for readers who want to understand the full diagnostic process.

The chip: Ralink RT2570

The RT2570 is a USB 802.11g WiFi chip from Ralink Technology (now part of MediaTek). It connects to the host computer via USB and communicates using vendor-specific USB control transfers — essentially USB messages with a custom format that the Linux driver knows how to send and receive.

How the Linux driver talks to the chip

The driver sends USB control transfers with these parameters:

  • bmRequestType 0x40 (vendor request, device-to-host for writes)
  • bRequest 6 (USB_MULTI_WRITE) for register writes
  • bRequest 7 (USB_MULTI_READ) for register reads
  • wIndex = register byte address (e.g. 0x0426 for MAC_CSR19)
  • data = 2-byte value to write

How rfkill polling works

The rt2500usb driver registers an rfkill poll function with the kernel:

static int rt2500usb_rfkill_poll(struct rt2x00_dev *rt2x00dev)
{
    u16 reg = rt2500usb_register_read(rt2x00dev, MAC_CSR19);
    return rt2x00_get_field16(reg, MAC_CSR19_VAL7);  // bit 7
}

The kernel calls this function periodically. If it returns 0, the kernel marks the radio as hard-blocked. If it returns 1, the block is cleared.

Why the debugfs interface works

The rt2x00 driver framework (which rt2500usb is built on) exposes a debug interface under:

/sys/kernel/debug/ieee80211/<phy>/rt2500usb/register/

Writing a word index to csr_offset and a value to csr_value causes the kernel to send the corresponding USB control transfer to the chip — exactly as if the driver itself had written the register.

The word index is computed as:

index = (byte_address - CSR_REG_BASE) / word_size
      = (0x0426 - 0x0400) / 2
      = 19

Why the udev rule is needed on Debian 12

On Gentoo (OpenRC), /etc/local.d/ scripts run late in the boot sequence, after all drivers have been loaded and USB devices have been enumerated. The timing is naturally correct.

On Debian 12, systemd services with After=systemd-modules-load.service start before udev has finished binding drivers to USB devices. The rt2500usb module is loaded by udev on device detection, not by systemd-modules-load.service. This means the unlock script must not run until the USB device is actually bound.

Why the Debian 12 unlock script uses USB directly instead of debugfs

Debian's stock 6.1 kernel has CONFIG_RT2X00_DEBUG disabled. The original fix relied on writing to:

/sys/kernel/debug/ieee80211/phy0/rt2500usb/register/

But that directory is only created by the driver when CONFIG_RT2X00_DEBUG is compiled in. On Debian 12 the directory is empty and no register files exist.

The replacement approach sends the identical USB vendor control transfer that the kernel driver uses internally, directly from Python using python3-usb:

dev.ctrl_transfer(0x40, 0x06, 0, 0x0426, struct.pack('<H', 0xFE80))
  • 0x40 — bmRequestType: vendor request, host-to-device
  • 0x06 — bRequest: USB_MULTI_WRITE (rt2500usb vendor command)
  • 0 — wValue: unused
  • 0x0426 — wIndex: MAC_CSR19 byte address
  • 0xFE80 — data: the register value (little-endian)

This does not require detaching the kernel driver and works while rt2500usb is running normally.

The udev rule rt2570-rfkill.rules uses ACTION=="bind" on the RT2570's USB IDs to fire the service at the correct moment:

ACTION=="bind", SUBSYSTEM=="usb", ATTR{idVendor}=="148f", ATTR{idProduct}=="2570", \
    TAG+="systemd", ENV{SYSTEMD_WANTS}+="rt2570-rfkill.service"

TAG+="systemd" tells udevd to track this device as a systemd device unit. ENV{SYSTEMD_WANTS} causes systemd to start rt2570-rfkill.service as a dependency of that device unit — which happens immediately after the driver probe() function returns and the bind event is sent.

Why ndiswrapper is not the answer

Ndiswrapper loads the Windows driver binary into the Linux kernel to control hardware that has no Linux driver. In this case:

  1. The RT2570 already has a Linux driver (rt2500usb) — the driver works, it just initialises one register wrong.
  2. Ndiswrapper is incompatible with modern Linux kernels (4.x and above) on most hardware.
  3. Ndiswrapper requires NDIS (Windows driver model) support, which has been removed from most distributions.

The correct fix is to patch the one register value, not replace the driver.

Why ICH6 GPIO was not the cause

Early in the diagnosis, the Intel ICH6 chipset GPIO registers were inspected. The ICH6 has a GPIO base address at 0x0480 (read from PCI config space of device 00:1f.0, offset 0x48). The GPIO level register (GP_LVL) at 0x048C showed two output pins held LOW (GPIO19 and GPIO23). These were toggled HIGH, but the rfkill state did not change. The kill signal originates inside the RT2570 chip, not from external motherboard GPIO lines.

The ACPI DSDT

The system's ACPI DSDT (firmware table that describes hardware to the OS) defines named fields for the ICH6 GPIO pins (GO10–GO1C, GO20–GO2B) but never calls any method that uses them. This confirmed that the firmware does not control the WiFi kill — the Windows driver handles it directly by writing to the chip register over USB.


Tested on: Gentoo Linux, kernel 6.12.58, Pentium 4 / ICH6, RT2570 USB WiFi (148f:2570), driver rt2500usb 2.3.0

Debian 12 fix (udev rule + Python USB) verified on kernel 6.1.0-42-amd64 / systemd 252

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages