FreeBSD on the Lenovo Thinkpad X201
After many many years of running Linux on my Thinkpad X201 I decided to move over to FreeBSD.
Unlike a Linux install where everything auto-configured and worked out of the box, installing FreeBSD on a laptop in 2024 is very much like installing Linux back in the late 2000's. The basics are there but you need some tweaks to make it actually usable. So I created this page to document what needs to be done, both for my future reference and so that anyone else coming across these issues doesn't have to debug everything from scratch.
Some of these bits of information are also generic rather than specific to the X201 (or laptops in general) in order to assist long time Linux users to move to FreeBSD. While they have their similarities they also have their differences, which I will document here in relation to the X201.
Note that I don't cover the basics like installing packages, etc... The assumption is that you are comfortable with Unix operating systems already and for FreeBSD basics I can recommend the FreeBSD Handbook.
The install
As this is a laptop I take outside all the time the chances it gets lost or stolen are higher than a desktop, I went for a ZFS Encrypted zpool.
The install itself was flawless, and after about 20 mins (mostly downloading packages) I had a bootable FreeBSD OS.
I would however recommend you set your swap space to be larger than your RAM. By default it is set to 50% of RAM and I suspect this is why my laptop cannot suspend to disk. suspend-to-disk usually requires the OS to dump the entire RAM state to swap, so your swap should ideally be as large or larger than your RAM.
Setting up X11
This also proved to be simple. In fact it was so simple all I had to do was install "xinit" and run "startx". For the purposes of being explicit I did generate an xorg.conf file with the "X -configure" and saved it to /etc/X11/xorg.conf, but it is not required.
With that done I had a basic X11 session running. I went a bit further and installed fluxbox (my preferred window manager on small screens), configured my .xinitrc and went on my merry way.
Setting up suspend/resume
The second thing I tried to use was suspend/resume. On FreeBSD you type "acpiconf -s 3" on the command line to suspend the machine to RAM (if you want to suspend to disk, you run "acpiconf -s 4"). This bit worked fine, however when the machine resumed the screen remained off. The machine worked fine and I could SSH into it, but without the screen on I could not actually use it.
After quite a bit of research and a lot of help from the folks over at forums.freebsd.org I discovered that you have to load the Intel Graphics driver in order for this to work. By default FreeBSD has the VESA driver configured which is why my X11 session worked without any special driver install, but for more advanced things like suspend/resume you need the official driver.
Interestingly the FreeBSD community decided that instead of writing their own drivers all over again, they would implement a shim for the Linux DRM interface. This allows you to use Linux graphics drivers in FreeBSD.
To make use of this you have to install the "drm-kmod" package, once that is done you add your driver to the kld_list variable in /etc/rc.conf, as so:
kld_list="i915kms"
You can manually load up the driver using "kldload i915kms". If it all works as intended your console will change its graphics. You will go from the more traditional 80 column large font UNIX console to much smaller font console, akin to the Linux framebuffer console.
Interestingly I did not need to alter the xorg.conf. Its default Driver is "modesetting", which will automatically use the best available graphics driver. All I had to do was "startx" again to get my X session running.
Now suspend/resume worked perfectly.
Setting suspend on lid-close
If you are like me and like your laptop to suspend to RAM when you close the lid, you need to set the sysctl, as so:
root@Io~# sysctl hw.acpi.lid_switch_state=S3 hw.acpi.lid_switch_state: NONE -> S3
To make it permanent you put a line "hw.acpi.lid_switch_state=S3" in your /etc/sysctl.conf file.
Now when you close/open the lid the machine goes to sleep/resume. It is nice that this is configurable in sysctl on FreeBSD because it allows you to dynamically change the setting. So if by default you set it to suspend on lid close, but on one occasion you want to disable this, just change the above back to "NONE" (as root). It will persist until changed again or you reboot, after which whatever you set in /etc/sysctl.conf is set again.
Suspending using XScreensaver.App
If on the other hand you like to control whether your machine goes to sleep directly rather than based on events like lid open/close, you can have a look at XScreensaver.App (at least for fluxbox and windowmaker). For example if you set up the following in your startup file:
/usr/local/bin/XScreenSaver.App -l "xscreensaver-command -lock" -r "xscreensaver-command -lock && acpiconf -s 3" &
Then left clicking locks your screen, and right clicking will lock the screen and suspend. It is not implicit that your screen will lock on suspend (like people are used to on Windows) so if you want that behaviour you have to explicitly define it.
Hibernate
To go into hibernate you need to run "acpiconf -s4". Unfortunately it doesn't currently work on my X201. The machine powers off but when you power on again it is a clean instance, you've lost all your state. This is inconvenient for me as on Linux the X201 would auto-hibernate when battery was below 5%, allowing me to swap batteries and quickly resume my work.
Normally hibernate works by dumping the entire state to swap space, then restoring on next power on. I've noticed that the install by default gave me a swap volume of 2GB, which is 50% of my RAM (4GB). This might be why hibernate fails but I can't do anything without re-installing. I've made a note on "Install" above for people to make a note of changing this during the install.
Power and battery
To get information on your battery and its current state, you use "acpiconf -i $batt_number", where $batt_number which battery you want information from (on the X201 you only have one battery, so this is '0', some of the larger thinkpads had an option of two batteries).
#$ acpiconf -i 0 Design capacity: 73260 mWh Last full capacity: 38820 mWh Technology: secondary (rechargeable) Design voltage: 11100 mV Capacity (warn): 1941 mWh Capacity (low): 200 mWh Low/warn granularity: 1 mWh Warn/full granularity: 1 mWh Model number: 08K8193 Serial number: 15 Type: LION OEM info: JingYi State: discharging Remaining capacity: 94% Remaining time: 1:58 Present rate: 18569 mW Present voltage: 11760 mV
As you can see from above my "Design capacity" and "Last full capacity" are very different, my battery only has about 50% of its original capacity which makes sense considering that it is very old at this point. Still even at 50% capacity I get around 2h of use with the backlight at 100% (which is the largest power draw on the laptop).
Backlight control
On Linux the backlight was easily controlled by the fn+HOME/END keys as this was actually handled by the BIOS and not Linux
On FreeBSD however it does not work. It looks like the FreeBSD actually takes control of these keys from the BIOS so they no longer work.
First thing was to find out how to set the backlight in software, so that I can at least control it. I found out that there is an aptly named program called "backlight", which does the trick:
root@Io:~ # backlight brightness: 29 root@Io:~ # backlight -h Usage: backlight [-q] [-f device] backlight [-q] [-f device] -i backlight [-f device] value backlight [-f device] incr|+ value backlight [-f device] decr|- value
Simple enough and does the job. As an added bonus it does not require root. From what I can see FreeBSD exposes the backlight control in the "/dev/backlight" folder, allowing the backlight program to control brightness on the console or X11 as any user.
Binding the brightness keys
While having backlight control again is nice it is purely software controlled. This means I can't make use of the buttons without some configuration. First thing I need to do is enable the buttons so that they are registered by the OS. In order to do this you need to load the "acpi_video" module. A simple "kldload acpi_video" will suffice.
If it worked you would get no errors, and sysctl will now have some acpi_video options, as so:
dev.acpi_video.0.%parent: vgapci0 dev.acpi_video.0.%pnpinfo: dev.acpi_video.0.%location: dev.acpi_video.0.%driver: acpi_video dev.acpi_video.0.%desc: ACPI video extension dev.acpi_video.%parent:
Now you should have working keys. You can test by running "xev". If all goes as planned, when you hit fnCTRL + KeyUP/Down you should get responses corresponding to the below:
KeyPress event, serial 33, synthetic NO, window 0xc00001,
root 0x1cf, subw 0x0, time 42811377, (123,94), root:(945,120),
state 0x0, keycode 233 (keysym 0x1008ff02, XF86MonBrightnessUp), same_screen YES,
XLookupString gives 0 bytes:
XmbLookupString gives 0 bytes:
XFilterEvent returns: False
KeyRelease event, serial 33, synthetic NO, window 0xc00001,
root 0x1cf, subw 0x0, time 42811377, (123,94), root:(945,120),
state 0x0, keycode 233 (keysym 0x1008ff02, XF86MonBrightnessUp), same_screen YES,
XLookupString gives 0 bytes:
XFilterEvent returns: False
KeyPress event, serial 36, synthetic NO, window 0xc00001,
root 0x1cf, subw 0x0, time 42811846, (123,94), root:(945,120),
state 0x0, keycode 232 (keysym 0x1008ff03, XF86MonBrightnessDown), same_screen YES,
XLookupString gives 0 bytes:
XmbLookupString gives 0 bytes:
XFilterEvent returns: False
KeyRelease event, serial 36, synthetic NO, window 0xc00001,
root 0x1cf, subw 0x0, time 42811846, (123,94), root:(945,120),
state 0x0, keycode 232 (keysym 0x1008ff03, XF86MonBrightnessDown), same_screen YES,
XLookupString gives 0 bytes:
XFilterEvent returns: False
If you prefer to use xbindkeys, you can run "xbindkeys -k" and get the following output:
"(Scheme function)"
m:0x0 + c:233
XF86MonBrightnessUp
"(Scheme function)"
m:0x0 + c:232
XF86MonBrightnessDown
Whichever tool you use, you can see the key code combinations you need to set. If you use xbindkeys then its easy, just replace "(Scheme function)" lines above with "brightness + 5" and "brightness - 5" respectively (if you want 5 step change of brightness on button press, otherwise change to taste).
As I am using fluxbox on this machine it has its own keybinding server. To make use of that to control brightness, we have to alter the $HOME/.fluxbox/keys file, to add the following lines:
232 :Exec backlight - 5 233 :Exec backlight + 5
Then reload your fluxbox config (no need to restart your session), and test the buttons. In my case it worked, so success! I went with a 5% step each button press. This gives me 20 steps between 0 and 100% which for me is a good balance. You can set any valid value you like, the larger the value the fewer steps you have and the greater the variance between steps.
Other IBM/Lenovo thinkpad features
There is one other module available, called "acpi_ibm". This one exposes more features of the laptop to userspace control. To load it you "kldload acpi_ibm", and once done you get the following new sysctl fields:
dev.acpi_ibm.0.handlerevents: NONE dev.acpi_ibm.0.mic_led: 0 dev.acpi_ibm.0.fan: 1 dev.acpi_ibm.0.fan_level: 0 dev.acpi_ibm.0.fan_speed: 2592 dev.acpi_ibm.0.wlan: 1 dev.acpi_ibm.0.bluetooth: 1 dev.acpi_ibm.0.thinklight: 1 dev.acpi_ibm.0.mute: 1 dev.acpi_ibm.0.volume: 7 dev.acpi_ibm.0.lcd_brightness: 0 dev.acpi_ibm.0.hotkey: 2486 dev.acpi_ibm.0.eventmask: 134217727 dev.acpi_ibm.0.events: 1 dev.acpi_ibm.0.availmask: 134217727 dev.acpi_ibm.0.initialmask: 2060 dev.acpi_ibm.0.%parent: acpi0 dev.acpi_ibm.0.%pnpinfo: _HID=IBM0068 _UID=0 _CID=none dev.acpi_ibm.0.%location: handle=\_SB_.PCI0.LPC_.EC__.HKEY dev.acpi_ibm.0.%driver: acpi_ibm dev.acpi_ibm.0.%desc: ThinkPad ACPI Extras
Some of these (like dev.acpi_ibm.0.fan_speed) are read only, but can be useful to for monitoring. Others like dev.acpi_ibm.0.fan can be set (in this case, you can disable or enable your fan). These override the bios, so for example if you set dev.acpi_ibm.0.fan=0 your fan will turn off and it won't turn on no matter how hot the machine gets. Beyond 100°C your CPU will just shut off.
This means some of these options can damage your laptop and should only be used if you know what you are doing. In my case I quite like the ability to script the turning on/off of the thinklight using sysctl. I can incorporate it as an indicator in some of my scripts.
Current settings
So with all of the above, this is how my current settings look to run FreeBSD on the X201:
# In /etc/rc.conf: kld_list="i915kms acpi_video acpi_ibm"
# In /etc/sysctl.conf # Make the laptop suspend to RAM on lid close (switch to S4 to suspend to disk) hw.acpi.lid_switch_state=S3 # Make the machine more responsive (as it is a workstation rather than a server) # by increasing the context switches (it lowers computation throughput however) # see https://forums.freebsd.org/threads/what-is-sysctl-kern-sched-preempt_thresh.85601/ kern.sched.preempt_thresh=160 kern.eventtimer.timer=HPET
Beyond that I did one more thing. I set set the setuid bit on sysctl, as so:
chmod +s /sbin/sysctl
What this does is tell the OS that any user that calls that binary will have the binary run as the executable owner (in this case root). This allows me to issue sysctl commands as a non-root user, without resorting to things like "sudo" or similar. On a shared machine giving all normal users the ability to alter sysctl could be a security risk, however I am the only user of this machine. If any nefarious intruder got as far as accessing my encrypted volume and logging into FreeBSD, then the machine is most likely physically compromised already, so the setuid bit will be of little concern.
The advantage for setting it setuid is that I can run sysctl from my non-root user account, including in scripts, without dealing with privilege escalation.
Sound
So one surprise I found was that sound did not work "out of the box" on FreeBSD. Looking on the forums people mentioned that sound works only through the headphone jack, no luck with the internal speaker. So I decided to investigate. First thing I did was check what sound cards I have:
#$ cat /dev/sndstat Installed devices: pcm0:(play/rec) default pcm1: (play/rec) pcm2: (play) No devices installed from user space.
First surprise was that I actually had three built in sound cards. I then used the "beep" utility to test each one out. I discovered that each sound card is for a specific output, details in the comments:
#$ beep -g 100 -d /dev/dsp0.0 # Headphones / Dock stereo speakers #$ beep -g 100 -d /dev/dsp1.0 # Headphones / Internal speaker (mono) #$ beep -g 100 -d /dev/dsp2.0 # HDMI output
I also found out that the behaviour of the above is a bit hard to understand. So I tried to explain my understanding as best as I could:
- If the laptop is not docked but the headphones are plugged in the internal speaker is muted. Sending audio to pcm1 will go to internal speaker or headphones depending.
- If the laptop is docked then pcm0 is the sound card you should send audio to as that outputs to the dock stereo speakers. Inserting headphones in the laptop jack mutes both the internal speaker and dock speakers. Inserting headphones in the dock's headphone jack (at the rear) does the same. The docks headphone jack seems to have an internal amplifier as the audio was louder at the same volume level when I switched from the laptop headphone jack to the dock.
- I also discovered that because these appear as separate sound cards you can simultaneously send three different audio streams: one to the HDMI output, one to the internal speaker and one to the dock speakers. I confirmed this by sending three different audio streams at the same time. If you do this with headphones plugged into the jack then the two Conexant cards (pcm0/pcm1) will have their sound mixed into the jack and the HDMI will remain separate.
- The HDMI output seems to be completely isolated. Whether the headphones are plugged in or not makes no difference, but it only works when docked as the dock has the HDMI port (the laptop just has a VGA built in).
While interesting I can't think of any benefit to sending different audio to the three sound cards simultaneously so I will configure the laptop so that it uses the internal speaker if unlocked, dock speakers if docked and headphones if they are plugged in (regardless of dock status). This means setting the default sound card to pcm1 when the laptop is not docked and pcm0 when it is.
We need a way to change the default sound card depending on whether the laptop is docked or not. Luckily for us FreeBSD provides us with sysctl "dev.acpi_dock.0.status". It gives us a "1" if we are docked and a "0" otherwise.
FreeBSD also helpfully provides us with devd, a daemon to trigger user programs based on kernel events. This is already installed and configured to handle general system control so we add the following to the /etc/devd.conf file:
# Switch sound cards based on whether we are docked or not
# 0x00 = undocked, 0x01 = docked
notify 0 {
match "system" "ACPI";
match "subsystem" "Dock";
match "notify" "0x00" ;
action "sysctl hw.snd.default_unit=1";
};
notify 0 {
match "system" "ACPI";
match "subsystem" "Dock";
match "notify" "0x01" ;
# The below redirects to dock speakers/Headphone jack.
# Set to "2" if you want to switch to HDMI audio output when docked
action "sysctl hw.snd.default_unit=0";
};
Then you restart the udevd service. Now you will see when you dock/undock that the value returned by "sysctl hw.snd.default_unit" will change. Note that anything that has currently got the sound card open will not auto-switch. The audio program has to close and re-open the sound card. This means that if you are playing an audio stream through the dock speakers and you undock the laptop, the audio will not seamlessly be redirected to the internal speaker (instead you will hear nothing). You would have to stop and start your audio stream for it to make use of the new default.
With that I now have sound set up the way I wanted. FreeBSD's audio subsystem is impressive (it uses OSS4). It is powerful, flexible and logically designed. Linux audio is far more messy and dysfunctional but because of its popularity there are loads of people who already did all the work above so audio works "out of the box".
Physical audio buttons
In addition the acpi_ibm kernel module gives you the following sound controls:
dev.acpi_ibm.0.mute: 1 # Boolean for mute/unmute dev.acpi_ibm.0.volume: 14 # Volume range is from 0 to 14
The "mute" is controlled by the physical mute button on the keyboard. It acts only on the speakers. The HDMI and headphone jack are unaffected. The physical volume buttons have been taken over by the operating system so out of the box they do nothing. They are visible on xev as so:
KeyPress event, serial 36, synthetic NO, window 0x3800001,
root 0x1cf, subw 0x0, time 19824318, (665,480), root:(667,506),
state 0x0, keycode 122 (keysym 0x1008ff11, XF86AudioLowerVolume), same_screen YES,
XLookupString gives 0 bytes:
XmbLookupString gives 0 bytes:
XFilterEvent returns: False
KeyRelease event, serial 36, synthetic NO, window 0x3800001,
root 0x1cf, subw 0x0, time 19824446, (665,480), root:(667,506),
state 0x0, keycode 122 (keysym 0x1008ff11, XF86AudioLowerVolume), same_screen YES,
XLookupString gives 0 bytes:
XFilterEvent returns: False
KeyPress event, serial 36, synthetic NO, window 0x3800001,
root 0x1cf, subw 0x0, time 19825195, (665,480), root:(667,506),
state 0x0, keycode 123 (keysym 0x1008ff13, XF86AudioRaiseVolume), same_screen YES,
XLookupString gives 0 bytes:
XmbLookupString gives 0 bytes:
XFilterEvent returns: False
KeyRelease event, serial 36, synthetic NO, window 0x3800001,
root 0x1cf, subw 0x0, time 19825354, (665,480), root:(667,506),
state 0x0, keycode 123 (keysym 0x1008ff13, XF86AudioRaiseVolume), same_screen YES,
XLookupString gives 0 bytes:
XFilterEvent returns: False
So once again we can bind these to do what we want. To get it to work on the volume in sysctl as before will need some script work. So I wrote this quick perl script:
#!/usr/bin/env perl
$cmd = shift;
if($cmd eq "up") {
setVol(getVol() + 1);
} elsif ($cmd eq "down") {
setVol(getVol() - 1);
} else {
die("Unknown cmd $cmd\n");
}
sub getVol(){
return `sysctl -n dev.acpi_ibm.0.volume`;
}
sub setVol() {
if ($_[0] < 0) { return; } # Can't go below 0
die("Failed to set volume: $!\n") if system(
"sysctl", "dev.acpi_ibm.0.volume=$_[0]"
);
}
I named the script "volume", put it in my $PATH and set it executable. Then in my fluxbox/.keys file I put
122 :Exec volume down 123 :Exec volume up
And with that the volume keys do what they should. For the level of complexity it may be easier to see if you can get FreeBSD to not take over these keys, as then I suspect they will operate the volume directly. This however does give you the flexibility to customise what the buttons do.
Some useful command examples
Below are the commands I ran while testing to give you an idea of the syntax in case you need to do similar testing yourself:
# Examples send audio to pcm0 beep -g 100 -d /dev/dsp0.0 # Sends a simple beep at full volume # The below streams the audio to the requested device, the '//' is not a typo mpv --audio-device=oss//dev/dsp0 -vo null $audio_or_video_file
To test the ability to stream to all three sound cards simultaneously I just ran three mpv instances with different audio files.
(Pseudo) Transparency on urxvt/rxvt
On my Linux box setting transparency on my terminals was just a matter of passing "-tr -sh 35" when I called urxvt. This worked "out of the box" but on FreeBSD it doesn't. Once I got all the major bits working I thought this would be an easy thing to fix.
However I was wrong. For reasons beyond me most people nowadays want what they call "real" transparency, therefore a lot of online documentation and forum answers talk about how to enable this with a compositor. I find this result utterly crap because the whole reason I want to enable transparency is to see the background wallpaper I set, not whatever application is under my terminal.
I mean, have you ever tried to read terminal text when you can see the text of the terminal or application below it? It is headache inducing and the only way to make the text you want readable is to set the transparency value so low that you might as well not bother (an alternative nowadays is for your compositor to blur the text/application under your terminal, but then you still can't see your nice wallpaper).
As such finding out how to actually set pseudo-transparency took more research effort than it should have, especially when you just want it for your terminal emulator rather than all applications. So in the interest of making it easier to find this setting: what you need to do for (u)rxvt is set the following in your ~/.Xdefaults
Rxvt*Transparent: True
That is it, you don't even have to restart anything. This applies to Linux and FreeBSD equally, but i don't remember ever having to set it on Linux. Once I wrote that file the next time I ran urxvt with "-tr" it worked perfectly. Another advantage is that if you have a multi-monitor setup with multiple X displays (i.e. multihead without Xinerama) such as I have this works across all the X displays immediately.
In any case I have not really used ~/.Xdefaults before, so I found this link quite useful, you can set a lot of different things in here on a per-application basis.
Auto switching display when docked
After using the system for a bit I found that it would be nice to be able to have it automatically switch from LVDS to HDMI when I dock the laptop. In order to do this we need to:
- Detect when we have docked/undocked the laptop
- Detect whether we have a HDMI display connected
The reason for the second item is because my dock is connected to a KVM, so sometimes I dock the laptop and use it directly (meaning the LVDS should keep running) and other times I will use it via the KVM, meaning the HDMI port should be used.
The problem is that when docked and connected to the KVM xrandr claims HDMI is "active" even if I am not using it, meaning I can't use that to decide whether to switch to HDMI or not. Instead I found that when the KVM is active xrandr marks HDMI as "connected" and "disconnected" otherwise".
As such I had to write a perl script that handles the xrandr detection and settings.