# Samsung Series 7 Keyboard Backlight Controls Investigations

##### Background

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
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
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
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:

https://reverseengineering.stackexchange.com/questions/16200/how-to-investigate-windows-32-64bit-wow64-transition

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.

##### ACPI

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:

#!/bin/bash
for i in KBLL,$((0xC9F67D98 + 0x7d)) \ KBST,$((0xC9F67D98 + 0x7c)) \
KBTO,$((0xC9F67D98 + 0x77)) do IFS=',' read k v <<< "${i}"
echo -n "${k}: " dd if=/dev/mem skip=${v} bs=1 count=1 2>/dev/null | hexdump -e '"%x"'
echo
done

#!/bin/bash
if [[ $# -ne 2 ]]; then echo "Usage setkbd {LL,ST,TO} hexval." exit fi case "${1^^}" in
KBLL | LL)
addr=$((0xC9F67D98 + 0x7d)) ;; KBST | ST) addr=$((0xC9F67D98 + 0x7c))
;;
KBTO | TO)
addr=$((0xC9F67D98 + 0x77)) ;; *) echo "Unknown argument$1"
echo "Usage setkbd {LL,ST,TO} hexval."
exit
;;
esac

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):

##### SABI
/*
* 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,
}

SwSmi@[port:2][iface:1][en_mem:1][re_mem:1][data_offset:2][data_segment:2]

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_COMPLETE		0x04
#define SABI_IFACE_DATA			0x05

/.../

/* enable memory to be able to write to it */

/* 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);
}

/* write protect memory to make it safe */

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

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

[main_funtion:2][command:2][complete:1][d0:4][d1:4][d2:2][d3:1]

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.