Kernel compilation and driver systems programming in C++ for Ubuntu Linux

Kernel compilation and driver systems programming in C++ for Ubuntu Linux

February 13, 2024

In our study of the prolonged, targeted exposure of the human body to electromagnetic fields, we thoroughly examined the DisplayPort data interface. The experimental setup necessitated an environment free from extraneous electromagnetic interference to ensure accurate detection of the effects. To achieve this, we utilized an anechoic shielded room, equipped with radiation-absorbent material to block electromagnetic waves, ensuring first-class screening.

Inside this controlled environment, we placed a standard PC equipped with a DisplayPort interface. Adjacent to the monitor, a noise-immune DisplayPort cable was connected to a receiving antenna. Outside the room, a measuring receiver, attached to this aerial, was set up. The PC ran a program developed in G (the graphical programming language used in LabVIEW), which controlled the measuring receiver. To minimize potential external interference, the room was kept devoid of people during the experiment.

The implementation of the project involved Linux kernel programming. This is a quite common task for a linux development company. Throughout the study, the following parameters were identified:

01 Signal frequency Signal frequency.
02 Speedometer Electromagnetic field signal power.

The frequency of the signal in our study is determined by the measuring receiver, with assistance from an operator and a management program. Once the frequency is established, the power of the signal is measured.

Our project's primary objective was to test the noise immunity of the DisplayPort transmission channel, focusing on how its internal settings affect performance. Preliminary studies indicated that altering the transfer parameters of the DisplayPort interface can significantly influence both the noise immunity of the transmission channel and the signal strength. The DisplayPort interface's transmission channel, in simplified terms, consists of a chain: graphic processor–cable–monitor.

Экранированная камера
Room with first-class anechoic shielded screening

For our experiments, we used an anechoic shielded room, classified as first-class in terms of screening capabilities. DisplayPort, primarily designed for connecting video and audio equipment, supports the transmission of digital content up to 8K resolution. Version 2.1 of DisplayPort boasts a maximum bandwidth capacity of 77.37 Gbit/s. The base specification of DisplayPort was developed by the Video Electronics Standards Association (VESA) in 2006. Its interface includes three transmission channels: the main channel handles graphics rendering; an additional bidirectional channel facilitates the connection between the transmitting PC and the receiving monitor; and the third channel is a hot-plugging line (Hot Plug Detect), which records the monitor's power state.

Features of the interface:

01 Resolution 8К 8K resolution support.
02 Data encryption Data encryption.
03 Electromagnetic waves Low level of electromagnetic interference.
04 Playback bands Distribution of the bandwidth between audio and video.
05 High-speed channel High-speed auxiliary channel.

We were given a task of developing a low-level filter driver for the Display Port interface to address the issue of changing DisplayPort Configuration Data (DPCD) under Windows 10, specifically for DisplayPort 1.2. Not an usual custom linux software development task, isn't it? Additionally, we were to create a program to interact with the driver. For testing, we were provided with a Philips 242E1GAJ monitor and an MSI PRO DP21 11MA-210RU computer.

At first, the kernel development went well; we resolved issues related to registering the driver in the system, incorporating it into the monitor driver stack, and managing data transmission to and from an external program. However, challenges arose when we began testing the interaction with the DPCD itself. Despite following official documentation and utilizing the special DXGKDDI_DPAUXIOTRANSMISSION function, we were unable to extract any data from the DPCD, regardless of the parameters used.

Through a detailed study of WDDM files, we discovered that this function, along with many other DisplayPort functions, lacked implementation and served merely as a template for custom driver creation. Given the lack of time allocated for such an extensive task, we decided to switch the operating system to a Linux-based alternative thus offering our linux development services. We experimented with Debian 9, 10, and 11, but none of these versions successfully managed DisplayPort functionality 'out of the box' to transmit the image onto the monitor. Eventually, we transitioned to Ubuntu 22.04.1, which immediately established a working connection with the monitor via DisplayPort. This will be elaborated on further.

It's important to note that in Linux, there is no equivalent to a filter driver, nor are there layered drivers. However, Linux allows for the interception and manipulation of a driver's function calls. Essentially, we can redirect the call from the original function to our custom function. But this was not necessary for our current task. To develop a driver for Linux, you must install header files compatible with your kernel version.

sudo apt-get install linux-headers-$(uname -r)

In cases where the Linux kernel is compiled from source code, the linux-headers for your kernel version may not be available in the repository. However, this is not an issue as they are generated during the kernel compilation process.

Let's begin with driver creation. Direct interaction between an external program and a driver is not possible. Instead, a character device is created, to which a program can connect and execute data transfer calls to the driver. There are four available modes: read, write, simultaneous read and write, and no parameters. The device should be created at the time of driver registration and deleted alongside it. It's important to remember to delete not only the device itself but also the device class when deregistering the driver.

int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);

We encountered problems when creating the file_operations structure. The challenge involved defining a function to handle calls to the ioctl (Input/Output Control) device. In Linux kernel versions up to 3.x, defining the handler function was accomplished by setting the .ioctl parameter.

In more recent kernel versions, this parameter is no longer accessible and has been replaced by unlocked_ioctl for several reasons related to new architectural changes. More information on this can be found in discussions on the Big Kernel Lock.

static struct file_operations fops = 
{
    .unlocked_ioctl = etx_ioctl
};

Much of the documentation on developing drivers for Linux is based on older kernel versions, making it challenging to find information relevant to current versions. However, documentation for the current kernel version can be accessed here.

In the current kernel versions, all ioctl-calls are directed to the function assigned to unlocked_ioctl. The function, including its parameters, is defined as follows:

long etx_ioctl(struct file *file, unsigned int ioctl_num, unsigned long ioctl_param);

Our primary focus lies on the last two parameters: ioctl_num and ioctl_param. As the name suggests, ioctl_num is the command number, which must be predefined.

#define "ioctl name" __IOX("magic number", "command number", "argument type")

Where IOX should be:

  • IO — without parameters;
  • IOW— for writing parameters (copy_from_user);
  • IOR — for reading parameters (copy_to_user);
  • IOWR — for reading and writing parameters.

In the case of DPCD, we assumed that read and write commands would suffice. We will delve into more details later. Essentially, you need to know the address and the data that can be written. However, some DPCD fields span more than one address; this means that integral data must be logically divided across multiple addresses. If you aim to change any available field from the application, it is preferable to use the basic approach of specifying the address and data in separate commands. Consequently, a specific command was assigned to each field.

Now, let's delve into understanding DPCD. What exactly is it? As per the documentation, DPCD is an address space in the DisplayPort device that provides data for setting up and initializing channels. The documentation specifies which address corresponds to which parameter, so we will not elaborate on that here. Crucially, each address value consists of 8 bits. Notably, not every field consists of a single address. There is a function drm_dp_dpcd_readb for working with DPCD fields, which becomes available with linux-headers. Its prototype is in drm_dp_helper.h.

ssize_t drm_dp_dpcd_readb(struct drm_dp_aux *aux, unsigned int offset, u8 *valuep)

You might wonder how to obtain drm_dp_aux. Currently, there are no direct means to do so. However, the monitor functions and the video driver creates and utilizes it in some way. It's important to note that our PC uses an integrated Intel video card. If you're using an AMD or Nvidia video card, your approach may differ.

The Intel i915 driver is included with the Linux kernel. Given that the source code is open, why not retrieve the necessary parameter from there? We downloaded the kernel source code and navigated to drivers\gpu\drm\i915\display\intel_dp_aux.c. Then, we added the necessary functions to it.

static struct drm_dp_aux *static_dp_aux_ptr;

struct drm_dp_aux *intel_dp_aux_get_struct(void) {
    if (static_dp_aux_ptr == NULL)
        printk(KERN_INFO "Backlight: Could not init the aux_ptr!\n");
    return static_dp_aux_ptr;
}

EXPORT_SYMBOL(intel_dp_aux_get_struct);

static void intel_dp_aux_set_struct(struct drm_dp_aux *dp_aux) {
    static_dp_aux_ptr = dp_aux;
}

At the end of the function void intel_dp_aux_init(struct intel_dp *intel_dp), we add a call to our new function:

intel_dp_aux_set_struct(&intel_dp->aux);

So, what have we achieved? We have created a pointer to the structure necessary for reading DPCD, developed functions for writing and reading this structure, exported the reading function for use in other drivers (using EXPORT_SYMBOL), and added a function call for writing the structure into the initialization function. Then, we compile and install the kernel; through calling the function intel_dp_aux_get_struct, we obtain the structure. Don't forget to specify external in the function prototype.

Further tasks are trivial and will not be discussed here. Where necessary, you can read the data received in the ioctl function via copy_from_user and return the data on request via copy_to_user.

DPCD Menu

The next task was to keep the monitor in test mode. According to the specification, the mode activates when the last two bits of the DPCD TRAINING_PATTERN_SET field are set to any value other than 0. There are three test modes in total, with zero indicating the test is not running. We focused on the variant set by the value 01 in [1:0] bits of the training pattern 1 field. However, this setting is effective for a maximum of 10 milliseconds before deactivating. Therefore, in the control program, we dedicated a separate thread with a cycle that sets the 01 value in TRAINING_PATTERN_SET every millisecond.

This approach worked: rewriting the field with the current value of one had no effect. But once 0 (“testing is over”) is replaced, it triggers testing again. Alternatively, we considered modifying the driver by inserting automatic looping.

The test mode is used by the device itself at startup to set parameters. It is uncertain how it would behave if testing fails to finish within the 10 milliseconds stated in the specification. Furthermore, the driver previously had a looping capability, which was fixed. According to comments in the code, such cases were considered bugs.

But writing data into DPCD doesn't guarantee that a setting is applied. For example, changing the number of used channels in the LANE_COUNT_SET field does not affect the monitor's operation. Once the test mode starts, LANE_COUNT_SET is recalculated and rewritten. What we want is for the values written into DPCD to remain. Let's return to the i915 driver. In the intel_dp.c file, there is a function intel_dp_retrain_link. This function is called when test mode is enabled. In one of its cycles, the intel_dp_start_link_train function, which initiates the test, is called. During this cycle, a structure representing the real status of the entire system is created:

const struct intel_crtc_state *crtc_state = to_intel_crtc_state(crtc->base.state)

The values of this structure are used for the test mode. This means we have to modify it — first, remove the const in the definition. Then, set the necessary value (01, 10, or 11) in crtc_state->lane_count. However, we can't take the value from DPCD — the value will be set post-testing based on the test results. Once the testing mode is looped, the new value will be set. It appears that the value must be fetched differently. Here, EXPORT_SYMBOL comes in handy again. In intel_dp.c, we create a function that returns the number of channels. We then create a variable to store this value. When recording a value in LANE_COUNT_SET in one of the ioctl functions, we simultaneously call this new function.

For the forced entry to work only in test mode, once the test mode is disabled by an external program, we record a 0 value in the variable, which would be impossible under normal logic. We add a value check — if the number of channels in the variable is more than 0, we assign crtc_state->lane_count. Otherwise, we continue executing the function. A similar approach is possible for other fields (scrambling, amplitude scattering, etc.).

This concludes our discussion on the driver. We will briefly describe the functions of the user application. You can connect to the device using the following command: open(DEVICE_FILE_NAME, O_WRONLY), where DEVICE_FILE_NAME is the name of the character device created in the driver.

The function will return the device number, to which we will send settings via ioctl. It's also important to mention the last parameter — a pointer to the structure containing the sent and received data. Do not allocate memory for the data dynamically, as the driver may not have access, or you might receive erroneous data.

After enabling test mode and forcing a value in crtc_state->lane_count, it's essential to reset it to zero before the end of testing for normal operations to resume. Don't forget to add this reset to the application closing event.

info

As a result of development for Linux, we have created a driver and an application to control DPCD settings. We have also modified the i915 video driver for managing the actual state and not just displaying it.