Samsung Series 7 Keyboard Backlight Controls Investigations


I did a clean install of Windows 10 on my Samsung Series 7 (740U3E) laptop when I found out about Microsoft’s clean install tool which restores the OS to a pristine state, free from preinstalled vendor bloat-/software, but ended up not using the tool but completely reinstalling from an ISO instead (btw, if you visit the cleaning tool’s webpage while using Linux it will redirect to a page where you can download clean Windows 10 Home/Pro install ISO’s instead of the tool…).

Everything seemed to work fine after the reinstall with just drivers from Windows Update. Everything but some of the hotkeys, and the keyboard backlight that seemed to have a life of its own.

In order not to have to install Samsung Update and Samsung Settings and all services and things that that brings I thought that there must exist some kind of standard method to control the LEDs and enable the hotkeys and tools to do it with. And that was the start of way to many hours of analyzing and testing and getting to learn a little (more) about (U)EFI,  ACPI, disassembly, and debugging, memory layout and management… etc..

I couldn’t find any general tools or even information about how the LED’s were controlled. Besides the linux kernel’s samsung-laptop-module source code which hinted at something called SABI (Samsung Advanced BIOS Interface(?)) which could be used for controlling stuff like the ones one can control with the Samsung Settings application: USB-power in sleep mode, battery life saving mode, silent mode, WiFi and Bluetooth state and keyboard backlight level and eventual timeout. It seemed like a good place to start. I never could figure out how to make SABI calls when EFI-booting though, or if it is even possible, so it was a pretty time consuming detour, I’ve put my notes about it at the end.

So I had to install Samsung Settings anyways in order to get it to work (which was no trivial task in itself as Samsung does not seem to provide any software on their product support pages, but finally found a link to Samsung Update on Samsung’s american site in an old news post, through which other Samsung software and drivers could be installed.).

Attempt at debugging

This didn’t satisfy my curiosity though, so a couple of months later I thought that this question would easily be answered by debugging the controlling Samsung software.

Which led to many frustrating hours with the Windows Debugger (WinDbg) and IDA PRO disassembler. I expected to be able to kindof (relatively) quickly find a simple write to some memory address(es) that triggered the changes, and then be able to repeat them. But I didn’t.

Don’t know if it was because of intentional obfuscation reasons or something else, but for some reason the 32-bit EasySettingsCmdServer.exe through a stack of calls though SUS.dll (SetKeyboardBacklitLevel())and WSABI.dll (CallBIOSInterface())

makes a call to 64-bit code through ntdll!Wow64Transition/ntdll!Wow64SystemServiceCall, where 32-bit debuggers/disassemblers cannot follow. My very limited experience and understanding of these topics put a stop to  my investigations here after attempting to debug with WinDbg Preview which seemed to handle the switch (but I’m not sure) but led too deep into debuggers and assembly and windows internals for me to follow, one example trace of an SUS.dll!SetKeyboardBacklitLevel invocation:

Tracing SUS!SetKeyboardBacklitLevel to return address 739bc345
    7     0 [  0] SUS!SetKeyboardBacklitLevel
    4     0 [  1]   SUS!SetMaxPerformanceStatus
    3     0 [  2]     SUS!SetMaxPerformanceStatus
   17     0 [  3]       SUS!SetMaxPerformanceStatus
   14    17 [  2]     SUS!SetMaxPerformanceStatus
   10     0 [  3]       SUS!SetMaxPerformanceStatus
   16     0 [  4]         ntdll!RtlEnterCriticalSection
   17    16 [  3]       SUS!SetMaxPerformanceStatus
    5     0 [  4]         KERNEL32!TlsGetValueStub
   11     0 [  4]         KERNELBASE!TlsGetValue
   25    32 [  3]       SUS!SetMaxPerformanceStatus
   23     0 [  4]         ntdll!RtlLeaveCriticalSection
   32    55 [  3]       SUS!SetMaxPerformanceStatus
   19   104 [  2]     SUS!SetMaxPerformanceStatus
   11     0 [  3]       SUS!SetMaxPerformanceStatus
   20   115 [  2]     SUS!SetMaxPerformanceStatus
   12   135 [  1]   SUS!SetMaxPerformanceStatus
    3     0 [  2]     SUS!SetMaxPerformanceStatus
   18     0 [  3]       SUS!SetMaxPerformanceStatus
    9    18 [  2]     SUS!SetMaxPerformanceStatus
   11     0 [  3]       SUS!SetMaxPerformanceStatus
   10    29 [  2]     SUS!SetMaxPerformanceStatus
   15   174 [  1]   SUS!SetMaxPerformanceStatus
   10   189 [  0] SUS!SetKeyboardBacklitLevel
    6     0 [  1]   SUS!SetMaxPerformanceStatus
    3     0 [  2]     SUS!SetMaxPerformanceStatus
    3     0 [  3]       SUS!SetMaxPerformanceStatus
   17     0 [  4]         SUS!SetMaxPerformanceStatus
   14    17 [  3]       SUS!SetMaxPerformanceStatus
   10     0 [  4]         SUS!SetMaxPerformanceStatus
   16     0 [  5]           ntdll!RtlEnterCriticalSection
   17    16 [  4]         SUS!SetMaxPerformanceStatus
    5     0 [  5]           KERNEL32!TlsGetValueStub
   11     0 [  5]           KERNELBASE!TlsGetValue
   25    32 [  4]         SUS!SetMaxPerformanceStatus
   23     0 [  5]           ntdll!RtlLeaveCriticalSection
   32    55 [  4]         SUS!SetMaxPerformanceStatus
   19   104 [  3]       SUS!SetMaxPerformanceStatus
   11     0 [  4]         SUS!SetMaxPerformanceStatus
   20   115 [  3]       SUS!SetMaxPerformanceStatus
   11   135 [  2]     SUS!SetMaxPerformanceStatus
    3     0 [  3]       SUS!SetMaxPerformanceStatus
   18     0 [  4]         SUS!SetMaxPerformanceStatus
    9    18 [  3]       SUS!SetMaxPerformanceStatus
   11     0 [  4]         SUS!SetMaxPerformanceStatus
   10    29 [  3]       SUS!SetMaxPerformanceStatus
   14   174 [  2]     SUS!SetMaxPerformanceStatus
   15   188 [  1]   SUS!SetMaxPerformanceStatus
    5     0 [  2]     KERNEL32!ActivateActCtxStub
    4     0 [  2]     KERNELBASE!ActivateActCtx
   11     0 [  3]       KERNELBASE!IsActivateActCtxWorkerPresent
    8    11 [  2]     KERNELBASE!ActivateActCtx
    9     0 [  2]     KERNEL32!ActivateActCtxWorker
   17     0 [  3]       ntdll!RtlActivateActivationContext
   29     0 [  4]         ntdll!RtlActivateActivationContextEx
   52     0 [  5]           ntdll!RtlpAllocateActivationContextStackFrame
    3     0 [  6]             ntdll!__security_check_cookie
   55     3 [  5]           ntdll!RtlpAllocateActivationContextStackFrame
   79    58 [  4]         ntdll!RtlActivateActivationContextEx
   22   137 [  3]       ntdll!RtlActivateActivationContext
   15   159 [  2]     KERNEL32!ActivateActCtxWorker
   21   386 [  1]   SUS!SetMaxPerformanceStatus
   15   596 [  0] SUS!SetKeyboardBacklitLevel
   17     0 [  1]   SUS
    5     0 [  2]     advapi32!RegOpenKeyExWStub
   11     0 [  2]     KERNELBASE!RegOpenKeyExW
   54     0 [  3]       KERNELBASE!RegOpenKeyExInternalW
   27     0 [  4]         KERNELBASE!MapPredefinedHandleInternal
   32     0 [  5]           ntdll!RtlGetCurrentTransaction
   30    32 [  4]         KERNELBASE!MapPredefinedHandleInternal
   34     0 [  5]           ntdll!RtlSetCurrentTransaction
   32    66 [  4]         KERNELBASE!MapPredefinedHandleInternal
   16     0 [  5]           ntdll!RtlAcquireSRWLockExclusive
   49    82 [  4]         KERNELBASE!MapPredefinedHandleInternal
   14     0 [  5]           ntdll!RtlReleaseSRWLockExclusive
   52    96 [  4]         KERNELBASE!MapPredefinedHandleInternal
   34     0 [  5]           ntdll!RtlSetCurrentTransaction
   67   130 [  4]         KERNELBASE!MapPredefinedHandleInternal
    3     0 [  5]           KERNELBASE!__security_check_cookie
   70   133 [  4]         KERNELBASE!MapPredefinedHandleInternal
   62   203 [  3]       KERNELBASE!RegOpenKeyExInternalW
  201     0 [  4]         ntdll!RtlInitUnicodeStringEx
   77   404 [  3]       KERNELBASE!RegOpenKeyExInternalW
   73     0 [  4]         KERNELBASE!LocalBaseRegOpenKey
    3     0 [  5]           ntdll!NtOpenKeyEx
    1     0 [  6]             ntdll!Wow64SystemServiceCall
    1     0 [  6]             0x65117000
KBD_backlit_Auto On

And after reading articles like these I realized that it was probably nothing that I quickly could learn to a sufficient degree without significant additional time investment:

It also seems to make WMI-calls and do something related to ACPI0008 (driver name: Light Sensor), but I’m too unskilled with the debugger to tell which are related and not.


Instead, while trying to find out how to make a SABI call I found out about this tool FWTS (“Firmware Test Suite (FWTS) is a test suite that performs sanity checks on firmware. It is intended to identify BIOS, UEFI, ACPI and many other errors and if appropriate it will try to explain the errors and give advice to help workaround or fix firmware bugs. It is primarily intended to be a Linux-centric firmware troubleshooting tool.”). Running it provided an wealth of interesting hardware information.

Somehow this led me to the ACPI tables, the decompilation of them from “ACPI Machine Language” to “ACPI Source Language”) (fwts dumpacpi, iasl -d), and while reading through the DSDT (Differentiated System Description Table) table’s decompiled source code I happened upon some interestingly named variables (See e.g. Arch Linux DSDT wiki article for more information):

OperationRegion (SNVS, SystemMemory, 0xC9F67D98, 0x0100)
Field (SNVS, AnyAcc, Lock, Preserve)
    OSTP,   16, 
    SMIS,   8,     // Offset 0x02
    DB00,   8, 
    DW00,   16, 
    KBTO,   8,     // Offset 0x77
    Offset (0x7C), 
    KBST,   8,     // Offset 0x7C
    KBLL,   8,     // Offset 0x7D

Which gave me a memory address (0xC9F67D98) and offsets to KBTO, KBST and KBLL, which I guessed was KeyBoard TimeOut, STatus and LightLevel or something like that.

I wrote getter and setter shell scripts that reads/writes from/to these addresses though /dev/mem:

for i in KBLL,$((0xC9F67D98 + 0x7d)) \
        KBST,$((0xC9F67D98 + 0x7c)) \
        KBTO,$((0xC9F67D98 + 0x77))
    IFS=',' read k v <<< "${i}"
    echo -n "${k}: "
    dd if=/dev/mem skip=${v} bs=1 count=1 2>/dev/null | hexdump -e '"%x"'
if [[ $# -ne 2 ]]; then
    echo "Usage setkbd {LL,ST,TO} hexval."

case "${1^^}" in
    addr=$((0xC9F67D98 + 0x7d))
    addr=$((0xC9F67D98 + 0x7c))
    addr=$((0xC9F67D98 + 0x77))
    echo "Unknown argument $1"
    echo "Usage setkbd {LL,ST,TO} hexval."

printf "\x${2}" | dd of=/dev/mem seek=$addr bs=1

Just writing to the addresses had no effect though. I had however noticed that the backlight turned on when the computer awoke from sleep (sometimes, or always since I taped over my ambient light sensor). Only from sleep though, not at power on, so the lights would remain off until the computer first was put to sleep and awoken and then they would turn on. So i hypothesized that maybe that could be used as a trigger. Which it could.

So I could set KBLL to a value 0 – 4 (printf "\x04" | dd of=/dev/mem seek=$((0xC9F67D98 + 0x7D)) bs=1)and then put the computer to sleep (fwts s3 -, shutting the lid)and when it woke up it would use that brightness level.  At least I now got a (somewhat cumbersome) way to set the light level!

I booted Windows and had a peek at the addresses (using RW Everything) while I was changing timeout settings and light level using Samsung Settings and found that only KBLL seemed to change from 0 to 4 depending on level chosen. KBTO seemed to have nothing to do with timeout, KBLL just changed from a value > 0 to 0 at timeout and back to whatever it was again upon next user input. I’m guessing that the timeout stuff and hotkeys (that reports ordinary scancodes like ordinary keys) are handled by software under windows.

In the DSDT table there fortunately existed a method (it is a pretty strange and almost superfluous one and without it I would not have come to this solution) that is executed when the computer wakes from sleep that seemingly reads from the ambient light sensor and turn on the keyboard backlight if the reading is below a certain value and turn it off if above another value (unverified) if KBTO <> 0 (verified). This is the only place in the table that the KBxx values seemed to be referenced so searches for those symbols led there:

Method (_Q70, 0, NotSerialized)  // _Qxx: EC Query
    If (((ALSE == 0x02) && IGDS))
        Local0 = ^ALSD._ALI ()
        ^^^GFX0.AINT (Zero, Local0)
        Notify (ALSD, 0x80) // Status Change

    Local0 = LUXH /* \_SB_.PCI0.LPCB.H_EC.LUXH */
    Local0 = ((Local0 << 0x08) | LUXL) /* \_SB_.PCI0.LPCB.H_EC.LUXL */
    Local0 = ^ALSD.CLUX (Local0)
    If ((KBTO == Zero))
        If (((Local0 <= 0x14) && (KBST != One))) 
            SECS (0x9A) 
            KBST = One 
        ElseIf (((Local0 >= 0x32) && (KBST != Zero)))
            SECS (0x9B)
            KBST = Zero

(one annoying this is that most ACPI symbols are four letters long so it is not immediately obvious what they stand for… so by guessing CLUX(LUXH/LUXL) = some kind of normalized measurement from the light sensor (high/low byte)).

When status/state(?) is set to 1 the SECS() function is called with argument 0x9A, when to 0 with 0x9B. So maybe we could simulate those SECS()-calls to trigger a change. Jumping to the definition…

Method (SECS, 1, Serialized)
    Acquire (MSEC, 0xFFFF)
    Store (Arg0, SMIS) /* \SMIS */
    Store (SWCD, TRPS) /* \TRPS */
    Release (MSEC)

(SECS, guessing something about the EC (Embedded Controller), SMIS reminds of SwSmi, TRPS maybe TRigger something (Power State? 0xB2 (but now we’re getting ahead of ourselves) is sometimes refered to as the APM port?)? SWCD also reminicent of SwSmi…).

We can here see that the argument to SECS, 0x9A or 0x9B, is stored in SMIS and SWCD is written to TRPS.

Moving on to MSEC mutex revealed TRPS/SECR and led on onto SSMI:

Mutex (MSEC, 0x00)
OperationRegion (SECR, SystemIO, SSMI, 0x02)
Field (SECR, ByteAcc, NoLock, Preserve)
    TRPS,   8

SWCD and SSMI were constants:

Name (SSMI, 0xB2)
Name (SWCD, 0xA8)

And the SMIS variable was to be found in the same SNVS region as the KBxx ones at 0xC9F67D98, offset 0x02:

OperationRegion (SNVS, SystemMemory, 0xC9F67D98, 0x0100)
Field (SNVS, AnyAcc, Lock, Preserve)
    OSTP,   16, 
    SMIS,   8,     // Offset 0x02
    DB00,   8, 
    DW00,   16, 
    KBTO,   8,     // Offset 0x77
    Offset (0x7C), 
    KBST,   8,     // Offset 0x7C
    KBLL,   8,     // Offset 0x7D

Now I had all the information needed to make the calls by emulating this by writing  to /dev/mem and /dev/port:

# Write "command" 0x9A to enable or 0x9B to disable to SMIS (0xC9F67D98 + 0x02)
printf "\x9A" | dd of=/dev/mem seek=$((0xC9F67D98 + 0x02)) bs=1

# Write desired brightness level 0-4 (8 on some models) to KBLL (0xC9F67D98 + 0x7D)
printf "\x04" | dd of=/dev/mem seek=$((0xC9F67D98 + 0x7D)) bs=1

# Write "SWCD" (0xA8), to SMI Command IO Port "SSMI" (0xB2) to trigger change
printf "\xA8" | dd of=/dev/port seek=$((0xB2)) bs=1

And (to my surprise) it works. Writing 0x9A to SMIS will indeed enable the backlight and 0x9B will disable it. Writing 0 – 4 to KBLL will set light level when enabled. Writing A8 to port B2 is what will trigger the change.

This doesn’t align too well with the values of the commands and arguments as the BIOS SABI calls as per the kernel code. But works well enough. Now I had a way to control keyboard backlight without having to put the computer to sleep to trigger the change. Mission accomplished!

Looking through the (U)EFI driver names (given by UEFItool) one gets the impression that there are two ways of changing settings, the SABI way and the SMM (System Management Mode, google “SSM B2”) way (the existence of an EFI variable named LegacySabiBuffPtr also supports this). (However, SABI also make use of port 0xB2):

 * This driver is needed because a number of Samsung laptops do not hook
 * their control settings through ACPI.  So we have to poke around in the
 * BIOS to do things like brightness values, and "special" key controls.

 * We have 0 - 8 as valid brightness levels.  The specs say that level 0 should
 * be reserved by the BIOS (which really doesn't make much sense), we tell
 * userspace that the value is 0 - 7 and then just tell the hardware 1 - 8
#define MAX_BRIGHT	0x07

The kernel module source disclosed that SABI calls were made by writing commands and arguments to a specific memory addresses and output to a port.

The first thing that the module does is to check if we’re EFI-booting. If so initialization will fail and the module not load. The reason for this is that some Samsung models will brick themselves it EFI non-volatile variable storage memory becomes more than 50% full so the kernel devs did not think it safe to peek/poke around in memory then:

static int __init samsung_init(void)
    struct samsung_laptop *samsung;
    int ret;

    if (efi_enabled(EFI_BOOT))
        return -ENODEV;

The port, the values to output to the port and the pointer to the SABI call memory address is found by searching for the string “SwSmi@” in the memory segment where BIOS is mapped, 64kb under 1Mb, 0xf0000 to 0xfffff. The bytes following that string then gives us:

.header_offsets = {
    .port = 0x00,
    .iface_func = 0x02,
    .en_mem = 0x03,
    .re_mem = 0x04,
    .data_offset = 0x05,
    .data_segment = 0x07,


In a hexdump from that memory segment from my system I found:

“SwSmi@” b2 00 80 81 82 00 f0 90 d2

data_offset and data_segment makes up a pointer to SABI call interface memory area and is given given by the expression (data_segment << 4 | data_offset).

That makes port=0x00B2, iface=0x80, en_mem=0x81, re_mem=0x82 and pointer = (0xf000 << 4 | 0xd290) = 0x000fd290.

The source code for sabi_command() and some defines tells us how a SABI call is made:

#define SABI_IFACE_MAIN			0x00
#define SABI_IFACE_SUB			0x02
#define SABI_IFACE_DATA			0x05


/* enable memory to be able to write to it */
outb(readb(samsung->sabi + config->header_offsets.en_mem), port);

/* write out the command */
writew(config->main_function, samsung->sabi_iface + SABI_IFACE_MAIN);
writew(command, samsung->sabi_iface + SABI_IFACE_SUB);
writeb(0, samsung->sabi_iface + SABI_IFACE_COMPLETE);
if (in) {
    writel(in->d0, samsung->sabi_iface + SABI_IFACE_DATA);
    writel(in->d1, samsung->sabi_iface + SABI_IFACE_DATA + 4);
    writew(in->d2, samsung->sabi_iface + SABI_IFACE_DATA + 8);
    writeb(in->d3, samsung->sabi_iface + SABI_IFACE_DATA + 10);
outb(readb(samsung->sabi + config->header_offsets.iface_func), port);

/* write protect memory to make it safe */
outb(readb(samsung->sabi + config->header_offsets.re_mem), port);

/* see if the command actually succeeded */
complete = readb(samsung->sabi_iface + SABI_IFACE_COMPLETE);
iface_data = readb(samsung->sabi_iface + SABI_IFACE_DATA);

This means the call is made by writing following to in my case 0x000fd290:


Main function is always 0x5843, the command for changing the backlight 0x0078, and first parameter d0 = (0x82 | level << 8). The backlight first has to be enabled by issuing command 0xaabb though, all given by:

/* 0x81 to read, (0x82 | level << 8) to set, 0xaabb to enable */
u16 kbd_backlight;
kbd_backlight = 0x78

So we should be able to control the backlight by something like:

# Enable the bl
printf "\x81" | dd of=/dev/port seek=$((0xB2)) bs=1 2>/dev/null
printf "\x43\x58\x78\x00\x00\xbb\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" | dd of=/dev/mem seek=$((0xfd290)) bs=1 2>/dev/null
printf "\x80" | dd of=/dev/port seek=$((0xB2)) bs=1 2>/dev/null
printf "\x82" | dd of=/dev/port seek=$((0xB2)) bs=1 2>/dev/null
sleep 1
hexdump -C -s $((0xfd290)) -n 20 /dev/mem
# complete, written 0x00, should read back 0xaa, on error d0 low byte should read 0xff
# d0, written 0xaabb, should now be read back 0xccdd

sleep 10

# Set light level
printf "\x81" | dd of=/dev/port seek=$((0xB2)) bs=1 2>/dev/null
printf "\x43\x58\x78\x00\x00\x82\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" | dd of=/dev/mem seek=$((0xfd290)) bs=1 2>/dev/null
printf "\x80" | dd of=/dev/port seek=$((0xB2)) bs=1 2>/dev/null
printf "\x82" | dd of=/dev/port seek=$((0xB2)) bs=1 2>/dev/null
sleep 1
hexdump -C -s $((0xfd290)) -n 20 /dev/mem
# complete, written 0x00, should read back 0xaa, on error d0 low byte should read 0xff

(If you’re noticing that the order of bytes written seems a bit of fit is because of little-endianess. Where we have writeb() we can just printf the byte. Where we have writew() we have to write the word least-signifiant-byte-first, so main-function 0x5843 becomes printf “\x43\x58”. Same with writel(), 0x0000aabb becomes printf “\xbb\xaa\x00\x00”.)

This method does not work however. Nothing happens.

The “SwSmi@” string seems to me to be nowhere to be found in memory (where I’ve peeked) unless having the CSM (“The Compatibility Support Module (CSM) is a component of the UEFI firmware that provides legacy BIOS compatibility by emulating a BIOS environment, allowing legacy operating systems and some option ROMs that do not support UEFI to still be used.”…) enabled. And I’d prefer to not enable that. And even if I did the kernel module won’t initialize if EFI booting anyways, a check for this is made at startup and the module is not initialized (could easily be changed though).

(The reason for not wanting to enable the CSM is that it had certain unpredictable side effects on my stationary computers. Firstly, the option that enable/disable it is there named “Windows 8 Mode”. Not entirely obvious that enabling that option means disabling CSM. Having CSM enabled will in its turn limit the EFI framebuffer resolution to something like 1024×768 though, another not-too-obvious connection. It doesn’t matter too much on my media-server which has an Intel video card that has framebuffer driver support that can take over after the EFI framebuffer a bit later in the boot sequence and up the resolution to my TV’s 1920×1080. It was an annoyance on my desktop computer that has an NVidia 1060 card… The proprietary NVidia drivers does not have framebuffer support that can take over after the EFI one which meant that I was left with ugly low-res consoles instead of nice 2560×1440 ones. Or so I thought. Enabling “Windows 8 Mode”, disabling CSM, enabled much higher EFI-framebuffer resolutions. So now even grub is in monitor-native res. Took many years (and three NVidia cards) before I figured that one out.)

There exists at least two EFI variables which seems to be related. KBDBacklitLvl and LegacySabiBuffPtr. Writing to the Kbd-variable does nothing however and the the LegacySabiBuffPtr holds the same memory address that can be found by searching for “SwSmi@” with the CSM on, 0x000fd270.