06-04-2026 04:57 PM - edited 06-04-2026 05:06 PM
@JÞB wrote:
The millisecond Wait based on the msec timer waits up to msec tick counts on non RT targets. The new Wait routine based on the high resolution timer exit logic is different. Why?
Why does it wait up to ms tick? It can't guarantee anything like that. It waits at least <interval - 1> ms and at most <interval + some whatever the Windows system allows the current process/thread to react to>. ms. The interval - 1 is a feature of the Windows API used. It is what it is and trying to "fix" that only can make the function even more unreliable since the Windows API has changed behaviour quite a bit over the course of time, from having a 15.6 ms resulation, to 1ms through the trick with using the multimedia timer functions, which came at the cost of much higher interrupt load in the system. then Windows XP or 7 reduced that to 10 ms and somewhere between Windows 7 and 10 they changed it to 1ms by default but if an old application used the multimedia timer trick the actual timing interval varied even more wildly as it interfered with the new implementation.
The Wait on RT isn't fundamentally different. It only is more accurate but also waits at least the specified time and can take longer if the system load is high. Just because it is RT does not mean that it can guarantee 1ms accuracy in user land on heavily loaded systems.
Edit: I'm pretty sure "borking" is an autocorrected feature. (To block a judicial appointment) the kernel works fine. The BIOS is designed for USERS. Which is why we have real time targets where Wait exit behavior differs. If you want pi from a counter you are irrational.
Borking was intended. What I meant is that you would need to use a kernel driver that goes deeply into the kernel and tries to do its own timer implementation if you want to get a higher accuracy than what Windows does and that only could make the Windows system more unstable. What BIOS and user has to do I'm not sure. It is in fact even below the kernel although modern computers don't use a traditional BIOS anymore but rather UEFI which is quite a different type of firmware to a classical BIOS.
06-05-2026 04:25 AM - edited 06-05-2026 04:28 AM
@JÞB wrote:The millisecond Wait based on the msec timer waits up to msec tick counts on non RT targets. The new Wait routine based on the high resolution timer exit logic is different. Why?
If you are still referring to the Wait (ms) Function and the High Resolution Polling Wait VI, then the answer was already given in my previous long comment — they use completely different underlying mechanisms: the Sleep(ms) function versus QueryPerformanceCounter(). To be exact, the High Resolution Polling Wait combines both approaches, but this has already been discussed, so there is no need to repeat it again. The latter method consumes CPU resources in a spinning loop on last two milliseconds, but it allows for higher timing precision within certain limits.
This behavior is inherent to how PCs and the Windows operating system are designed.
You are clearly a very experienced LabVIEW user (being an NI Knight says a lot), but I know many brilliant LabVIEW developers — much more knowledgeable than I am — who simply don’t concern themselves with such low-level details because they simply don’t need to. That’s absolutely fine; we’re all human, and no one can know everything. You wouldn’t believe the “blank spots” I still have myself in my brain.
However, you’ve touched on this area where this kind of knowledge becomes essential. I don’t know your exact background or skill set — my suggestion is based only on your questions, answers, and the terminology you use in this thread — but I would like to recommend very politely and kindly to grab and read the following books:
Andrew S. Tanenbaum — Modern Operating Systems (5th Edition, 2022)
Mark E. Russinovich — Windows Internals, Parts 1 & 2 (7th Edition, 2017)
Daniel Kusswurm — Modern x86 Assembly Language (3rd Edition, 2023)
You can read these in parallel. For example, when you are on Chapter 2 (Processes and Threads) in Modern Operating Systems (which is more abstract), you can then move to the corresponding chapters (3 and 4) in Windows Internals (Processes, Jobs, and Threads), which focus specifically on Windows. This approach helps connect the concepts and build a more complete understanding to get this puzzle complete.
Also, try the suggested experiments and exercises — you already have everything you need in your hands: a PC, Windows, and LabVIEW + some other tools.
The last book does not need to be studied in full. The sections on general-purpose registers and basic assembly instructions are sufficient to understand CPU basics, although I would like to recommend going as far as AVX2 if possible — modern CPUs are truly amazing in their design and capabilities, and understanding them at this level reveals just how fascinating and powerful they really are, a beautifully engineered world in my humble opinion.
In total, these three books amount to roughly 3000+ pages, which could take probably about half a year to complete. However, if you revisit this topic afterward, you will likely have a completely different perspective — and be able to answer your original question: “Why is the exit logic different?”
06-05-2026 09:34 AM - edited 06-05-2026 10:33 AM
@rolfk wrote: you would need to use a kernel driver that goes deeply into the kernel and tries to do its own timer implementation if you want to get a higher accuracy than what Windows does and that only could make the Windows system more unstable...
You might be surprised, but even in a kernel driver you don’t have many options when it comes to implementing delays. If we are talking about delays, then the only available function is KeStallExecutionProcessor(), which is “a processor-dependent routine that busy-waits for at least the specified number of microseconds, but not significantly longer.” At least the input parameter allows you to specify the delay in microseconds.
Well, it’s not that complicated nowadays. In my career, I have written Windows drivers twice, but let’s go through this exercise anyway, my humble attempt number three.
Visual Studio 2026 v18.6.2; Windows 11 SDK v28000.2114 and WDK 28000.1761.
In general, the whole driver consists of a single C file, shown below:
#include <ntddk.h>
// Device + symbolic link
#define DEVICE_NAME L"\\Device\\StallDriver"
#define SYMLINK_NAME L"\\DosDevices\\StallDriver"
// IOCTL code
#define IOCTL_STALL \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
// Input structure
typedef struct _STALL_REQUEST {
ULONG Microseconds;
} STALL_REQUEST, * PSTALL_REQUEST;
// Forward declarations
DRIVER_UNLOAD DriverUnload;
DRIVER_DISPATCH DispatchCreateClose;
DRIVER_DISPATCH DispatchDeviceControl;
// ------------------------
// DriverEntry
// ------------------------
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
NTSTATUS status;
PDEVICE_OBJECT deviceObject = NULL;
UNICODE_STRING devName, symLink;
RtlInitUnicodeString(&devName, DEVICE_NAME);
RtlInitUnicodeString(&symLink, SYMLINK_NAME);
status = IoCreateDevice(
DriverObject,
0,
&devName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&deviceObject
);
if (!NT_SUCCESS(status))
return status;
status = IoCreateSymbolicLink(&symLink, &devName);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(deviceObject);
return status;
}
// Setup dispatch table
DriverObject->MajorFunction[IRP_MJ_CREATE] =
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] =
DispatchDeviceControl;
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
// ------------------------
// Unload
// ------------------------
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING symLink;
RtlInitUnicodeString(&symLink, SYMLINK_NAME);
IoDeleteSymbolicLink(&symLink);
IoDeleteDevice(DriverObject->DeviceObject);
}
// ------------------------
// Create / Close
// ------------------------
NTSTATUS DispatchCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
// ------------------------
// IOCTL handler
// ------------------------
NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
ULONG_PTR info = 0;
if (stack->Parameters.DeviceIoControl.IoControlCode == IOCTL_STALL)
{
if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(STALL_REQUEST))
{
status = STATUS_BUFFER_TOO_SMALL;
}
else
{
PSTALL_REQUEST req = (PSTALL_REQUEST)Irp->AssociatedIrp.SystemBuffer;
// Busy wait
KeStallExecutionProcessor(req->Microseconds);
status = STATUS_SUCCESS;
}
}
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = info;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}Actually delay is implemented here:
// ------------------------
// IOCTL handler
//
NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
... // Busy wait
KeStallExecutionProcessor(req->Microseconds);
...
You will also need an INF file:
;
; stall_driver.inf
;
[Version]
Signature = "$WINDOWS NT$"
Class = System ; TODO: specify appropriate Class
ClassGuid = {4d36e97d-e325-11ce-bfc1-08002be10318} ; TODO: specify appropriate ClassGuid
Provider = %ManufacturerName%
CatalogFile = stall_driver.cat
DriverVer = 06/01/2026,1.0.0.0
PnpLockdown = 1
[DestinationDirs]
DefaultDestDir = 12 ; was 13
[SourceDisksNames]
1 = %DiskName%,,,""
[SourceDisksFiles]
stall_driver.sys = 1,,
;*****************************************
; Install Section
;*****************************************
[Manufacturer]
%ManufacturerName% = Standard,NT$ARCH$.10.0...16299 ; %13% support introduced in build 16299
[Standard.NT$ARCH$.10.0...16299]
%stall_driver.DeviceDesc% = stall_driver_Device, Root\stall_driver ; TODO: edit hw-id
[stall_driver_Device.NT]
CopyFiles = File_Copy
[File_Copy]
stall_driver.sys
;-------------- Service installation
[stall_driver_Device.NT.Services]
AddService = stall_driver,%SPSVCINST_ASSOCSERVICE%, stall_driver_Service_Inst
; -------------- stall_driver driver install sections
[stall_driver_Service_Inst]
DisplayName = %stall_driver.SVCDESC%
ServiceType = 1 ; SERVICE_KERNEL_DRIVER
StartType = 3 ; SERVICE_DEMAND_START
ErrorControl = 1 ; SERVICE_ERROR_NORMAL
ServiceBinary = %13%\stall_driver.sys
[stall_driver_Device.NT.Wdf]
KmdfService = stall_driver, stall_driver_wdfsect
[stall_driver_wdfsect]
KmdfLibraryVersion = $KMDFVERSION$
[Strings]
SPSVCINST_ASSOCSERVICE = 0x00000002
ManufacturerName = "Example" ;TODO: Replace with your manufacturer name
DiskName = "stall_driver Installation Disk"
stall_driver.DeviceDesc = "stall_driver Device"
stall_driver.SVCDESC = "stall_driver Service"You also need a properly installed and configured Visual Studio environment as described here. A freshly built driver can be started with:
sc create StallDriver type= kernel start= demand binPath=<Path to>\stall_driver.sys
sc start StallDriver
Of course, this is an unsigned driver, so Secure Boot must be OFF, and don’t forget:
bcdedit.exe -set TESTSIGNING ON
You should see something like this:
SERVICE_NAME: StallDriver
TYPE : 1 KERNEL_DRIVER
STATE : 4 RUNNING
(STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
...
A minimal C application that performs the delay is shown below:
#include <windows.h>
#include <stdio.h>
#define IOCTL_STALL \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
typedef struct _STALL_REQUEST {
ULONG Microseconds;
} STALL_REQUEST;
int main()
{
HANDLE hDevice = CreateFileA(
"\\\\.\\StallDriver",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL
);
if (hDevice == INVALID_HANDLE_VALUE)
{
printf("Failed to open device (%d)\n", GetLastError());
return 1;
}
STALL_REQUEST req;
req.Microseconds = 50; // requested delay
DWORD bytesReturned;
LARGE_INTEGER freq, start, end;
// Get high-resolution timer frequency
QueryPerformanceFrequency(&freq);
// Start timing
QueryPerformanceCounter(&start);
BOOL result = DeviceIoControl(
hDevice,
IOCTL_STALL,
&req,
sizeof(req),
NULL,
0,
&bytesReturned,
NULL
);
// Stop timing
QueryPerformanceCounter(&end);
if (!result)
{
printf("IOCTL failed (%d)\n", GetLastError());
}
else
{
// Calculate elapsed time in microseconds
double elapsed_us =
(double)(end.QuadPart - start.QuadPart) * 1e6 / freq.QuadPart;
printf("Requested delay: %lu us\n", req.Microseconds);
printf("Measured time : %.2f us\n", elapsed_us);
}
CloseHandle(hDevice);
return 0;
}And the output is:
Requested delay: 50 us
Measured time : 51.80 us
So, as you can see, there are only two functions involved — CreateFileA and DeviceIoControl.
Hold your breath — it’s the same in LabVIEW:
1 ms delay "stability":
However, I would never use this approach in production. Never ever.
06-05-2026 01:24 PM - edited 06-05-2026 01:36 PM
@rolfk wrote:How you get the frequency is of course another interesting thing. This is in the MSR 0xCE register but that is only accessible from Ring 0 aka kernel space.
However there are still many other potential bears hiding under this,
That was actually your question from the other topic ("Re: Wait for less than 1 ms"), but let’s keep it here in one thread. Now that I’ve got a working driver in my hands, I can finally give you an answer back.:
static ULONG64 ReadTscFrequency()
{
const ULONG64 MSR_PLATFORM_INFO = 0xCE; // MSR 0xCE register
ULONG64 value = __readmsr(MSR_PLATFORM_INFO);
ULONG64 ratio = (value >> 8) & 0xFF; // Bits 15:8
return ratio * 100000000ULL; // 100 MHz base clock
}
A bit disappointed that we still have to rely on the FSB multiplier, but that’s how it is. Full source code as is:
#include <ntddk.h>
// Device + symbolic link
#define DEVICE_NAME L"\\Device\\StallDriver"
#define SYMLINK_NAME L"\\DosDevices\\StallDriver"
// IOCTL code
#define IOCTL_STALL \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
// Input structure
typedef struct _STALL_REQUEST {
ULONG Microseconds;
} STALL_REQUEST, * PSTALL_REQUEST;
//For Frequency
#define IOCTL_GET_TSC_FREQ \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
typedef struct _TSC_FREQ_INFO {
ULONG64 TscFrequencyHz;
} TSC_FREQ_INFO, * PTSC_FREQ_INFO;
// Forward declarations
DRIVER_UNLOAD DriverUnload;
DRIVER_DISPATCH DispatchCreateClose;
DRIVER_DISPATCH DispatchDeviceControl;
//Freq
static ULONG64 ReadTscFrequency()
{
const ULONG64 MSR_PLATFORM_INFO = 0xCE;
ULONG64 value = __readmsr(MSR_PLATFORM_INFO);
ULONG64 ratio = (value >> 8) & 0xFF; // Bits 15:8
if (ratio == 0)
return 0;
return ratio * 100000000ULL; // 100 MHz base clock
}
// ------------------------
// DriverEntry
// ------------------------
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
NTSTATUS status;
PDEVICE_OBJECT deviceObject = NULL;
UNICODE_STRING devName, symLink;
RtlInitUnicodeString(&devName, DEVICE_NAME);
RtlInitUnicodeString(&symLink, SYMLINK_NAME);
status = IoCreateDevice(
DriverObject,
0,
&devName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&deviceObject
);
if (!NT_SUCCESS(status))
return status;
status = IoCreateSymbolicLink(&symLink, &devName);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(deviceObject);
return status;
}
// Setup dispatch table
DriverObject->MajorFunction[IRP_MJ_CREATE] =
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] =
DispatchDeviceControl;
DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
// ------------------------
// Unload
// ------------------------
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING symLink;
RtlInitUnicodeString(&symLink, SYMLINK_NAME);
IoDeleteSymbolicLink(&symLink);
IoDeleteDevice(DriverObject->DeviceObject);
}
// ------------------------
// Create / Close
// ------------------------
NTSTATUS DispatchCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
// ------------------------
// IOCTL handler
// ------------------------
NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
ULONG_PTR info = 0;
if (stack->Parameters.DeviceIoControl.IoControlCode == IOCTL_STALL)
{
if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(STALL_REQUEST))
{
status = STATUS_BUFFER_TOO_SMALL;
}
else
{
PSTALL_REQUEST req = (PSTALL_REQUEST)Irp->AssociatedIrp.SystemBuffer;
// ⚠️ Busy wait
KeStallExecutionProcessor(req->Microseconds);
status = STATUS_SUCCESS;
}
}
if (stack->Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_TSC_FREQ)
{
if (stack->Parameters.DeviceIoControl.OutputBufferLength < sizeof(TSC_FREQ_INFO))
{
status = STATUS_BUFFER_TOO_SMALL;
}
else
{
PTSC_FREQ_INFO out = (PTSC_FREQ_INFO)Irp->AssociatedIrp.SystemBuffer;
ULONG64 freq = ReadTscFrequency();
if (freq == 0)
{
// Fallback: derive from system clock increment
ULONG inc = KeQueryTimeIncrement(); // 100ns units
freq = 10000000ULL / inc; // rough estimate
}
out->TscFrequencyHz = freq;
info = sizeof(TSC_FREQ_INFO);
status = STATUS_SUCCESS;
}
}
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = info;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}And LabVIEW (this is 2.7 GHz CPU):
Sorry, I just can’t stop playing with this new toy. Maybe the snippets above will help someone, but since an unsigned driver is involved, they’re probably not very practical, almost useless.
06-08-2026 11:19 AM
Just one more thing related to the Windows timer — I was wrong in my previous suggestion that the increased timer resolution is “hard-coded.” It is not; it’s optional. The hidden key option is called useDefaultTimer:
useDefaultTimer=true
If you add this to LabVIEW.ini, LabVIEW will no longer set the resolution to 1 ms. Instead, it will leave it at the default, and Wait (ms) will wait from 1 ms to 16 ms or more, as expected.
This may be slightly controversial in the context of the discussion on “how to make it more reliable,” but this is how most Windows applications operate.
06-12-2026 09:06 AM
It’s me again. Friday is a good day for exercises — I’ve done two today and would like to report them and share the results (they had been on my “pending” list for a long time; now I’m done).
First, I converted an old HP Z440 workstation with an Intel Xeon CPU E5-1620 v3 @ 3.50 GHz into an NI Real-Time target. It works, but not with the onboard network — you have to insert an additional Intel PRO/1000 PCIe adapter. Also, turn off Hyper-Threading.
Then, in MAX, I created a USB stick, booted from it, and installed LabVIEW Real-Time 26.1f0 following NI instructions (sorry for the blurry photo of the monitor):
After that, I used the Hardware Configuration Utility to install software (don't forget cRIO Server, otherwise LabVIEW from host PC will not connect).
This is how it appeared in MAX — as an NI cRIO-903x:
Project in LabVIEW looks like this, same as "traditional" cRIO:
Now the Timed Loop is truly a timed loop (I set up 1 MHz clock):
And it is very stable (not as stable as on cRIO with FPGA, but good):
The High Resolution Polling Wait is also available:
Inside, it looks different from Windows, but it is still a kind of combination of Wait (ms) and "clock_nanosleep":
(Full PNG image — see attachment; the Block Diagram is not locked.)
It is also stable — 1.0018…1.0029 ms range, real-time is truly real-time (I i think I forgot to enable Multi CPU, running on single core), but anyway:
This experiment is interesting as well, of course:
And the result:
Roughly 1.01…1.09 ms sleep time, but no spikes up to 2 ms as seen on Windows. It may use a similar mechanism that does not “eat” CPU resources aggressively.
---
The second experiment was more fun. I created EFI software using the Rust UEFI crate. In general, this is a normal program that does not require an OS at all and runs on a “bare” CPU. You can compile and build it under Windows using x86_64-unknown-uefi target, then put BOOTX64.EFI into \EFI\BOOT Folder on the FAT32 formatted USB Stick, that is all (dont' forget to turn off legacy boot mode in BIOS). Can be tested under QEMU.
Technically, the entry point is called by the BIOS, which provides a System Table — a structure with pointers to functions (to print something on the monitor, etc.):
// =============================
// Entry point
// =============================
#[entry]
fn main(_handle: Handle, mut st: SystemTable<Boot>) -> Status { ... }
And you can do a lot inside (almost anything)!
For example, you can use assembly instructions directly in Rust code and enable CR4.PCE to get RDPMC working:
// =============================
// Enable RDPMC in CR4
//
#[inline(always)]
fn enable_rdpmc() {
unsafe {
let mut val: u64;
core::arch::asm!(
"mov {}, cr4",
out(reg) val
);
val |= 1 << 8; // CR4.PCE
core::arch::asm!(
"mov cr4, {}",
in(reg) val
);
}
}
Then you can read all counters.
Or you can access the interrupt controller registers:
// =============================
// LAPIC timer (one-shot)
// =============================
unsafe fn lapic_timer_start(count: u32) {
unsafe {
// Divide by 16
mmio_write32(LAPIC_BASE + 0x3E0, 0b0011);
// Vector 0x20, one-shot mode
mmio_write32(LAPIC_BASE + 0x320, 0x20);
// Start timer
mmio_write32(LAPIC_BASE + 0x380, count);
}
}
Then issue the CPU HALT instruction:
// =============================
// CPU halt
// =============================
#[inline(always)]
fn cpu_halt() {
unsafe { core::arch::asm!("hlt") }
}
And work with interrupts — when the CPU wakes up from the halt state via the timer:
// ✅ wait ~1 second via 20 LAPIC intervals
for _ in 0..20 {
unsafe {
lapic_timer_start(50_000_000); // 50 M x 20 -> 1 s
}
cpu_halt(); // sleep until interrupt
unsafe {
lapic_eoi(); // acknowledge interrupt
}
}
Full code:
#![no_main]
#![no_std]
use core::fmt::Write;
use core::panic::PanicInfo;
use uefi::prelude::*;
// =============================
// LTO padding
// =============================
#[used]
#[unsafe(no_mangle)]
static PADDING: [u8; 512] = [0; 512];
// =============================
// LAPIC constants
// =============================
const LAPIC_BASE: usize = 0xFEE0_0000;
// =============================
// MMIO helpers
// =============================
#[inline(always)]
unsafe fn mmio_write32(addr: usize, val: u32) {
unsafe {core::ptr::write_volatile(addr as *mut u32, val);}
}
// =============================
// LAPIC EOI
// =============================
#[inline(always)]
unsafe fn lapic_eoi() {
unsafe {mmio_write32(LAPIC_BASE + 0xB0, 0);}
}
// =============================
// LAPIC timer (one-shot)
// =============================
unsafe fn lapic_timer_start(count: u32) {
unsafe {
// Divide by 16
mmio_write32(LAPIC_BASE + 0x3E0, 0b0011);
// Vector 0x20, one-shot mode
mmio_write32(LAPIC_BASE + 0x320, 0x20);
// Start timer
mmio_write32(LAPIC_BASE + 0x380, count);
}
}
// =============================
// CPU halt
// =============================
#[inline(always)]
fn cpu_halt() {
unsafe { core::arch::asm!("hlt") }
}
// =============================
// rdtsc
// =============================
#[inline(always)]
fn rdtsc() -> u64 {
let lo: u32;
let hi: u32;
unsafe {
core::arch::asm!(
"rdtsc",
out("eax") lo,
out("edx") hi
);
}
((hi as u64) << 32) | lo as u64
}
// =============================
// Serialize
// =============================
#[inline(always)]
fn serialize() {
core::arch::x86_64::__cpuid(0);
}
// =============================
// Measure TSC frequency (~1s)
// =============================
fn measure_tsc_hz(st: &SystemTable<Boot>) -> u64 {
let start = rdtsc();
st.boot_services().stall(1_000_000); // 1 second
let end = rdtsc();
end - start
}
// =============================
// UTF-16 printer
// =============================
fn print_utf16(out: &mut impl Write, mut ptr: *const u16) {
unsafe {
while !ptr.is_null() {
let val = *ptr;
if val == 0 {
break;
}
let ch = if val < 128 { val as u8 as char } else { '?' };
let _ = out.write_char(ch);
ptr = ptr.add(1);
}
}
}
// =============================
// Entry point
// =============================
#[entry]
fn main(_handle: Handle, mut st: SystemTable<Boot>) -> Status {
// Enable interrupts (critical for HLT wake)
unsafe {
core::arch::asm!("sti");
}
let vendor_ptr = st.firmware_vendor().as_ptr() as *const u16;
let revision = st.firmware_revision();
let _ = st.stdout().reset(false);
{
let mut out = st.stdout();
let _ = out.write_str("LAPIC + HLT ~1s delay demo\r\n");
let _ = out.write_str("Firmware Vendor: ");
print_utf16(&mut out, vendor_ptr);
let _ = out.write_str("\r\n");
let _ = writeln!(out, "Firmware Revision: {:#x}", revision);
let _ = out.write_str("Calibrating TSC...\r\n");
}
let tsc_per_sec = measure_tsc_hz(&st);
{
let out = st.stdout();
let _ = writeln!(out, "TSC frequency: {} cycles/sec\r\n", tsc_per_sec);
}
// =============================
// Main loop (~1 second delay)
// =============================
loop {
serialize();
let start = rdtsc();
// ✅ wait ~1 second via 20 LAPIC intervals
for _ in 0..20 {
unsafe {
lapic_timer_start(50_000_000); // 50 M x 20 -> 1 s
}
cpu_halt(); // sleep until interrupt
unsafe {
lapic_eoi(); // acknowledge interrupt
}
}
serialize();
let end = rdtsc();
let elapsed = end - start;
let out = st.stdout();
let _ = writeln!(
out,
"Elapsed cycles: {} (~{:.3} sec)",
elapsed,
(elapsed as f64) / (tsc_per_sec as f64)
);
}}
// =============================
// Panic handler
// =============================
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
Of course, you can learn a lot about the CPU — such as measuring cache and instruction latency, branch prediction and misprediction, and much more — all without “pollution” from other OS threads, since no operating system is involved.
I can recommend this to everyone who wants to dive deep into this amazing real-time world — it’s quite interesting.
06-14-2026 06:25 AM
I got a cRIO 9038 image running in VmWare for a long time. Was mainly an exercise to see if and how it can be done. Not very difficult to do and if you have a single board x64 based hardware board, there are high chances that it can be done there too, if you are a bit savy with Linux. The problem is that as long as you only install NI Linux RT, you are legally ok but it is useless as a target for LabVIEW (you still can talk to it like how the Hobbyist Toolkit does as an example, but you can't deploy LabVIEW programs directly to it).
But since NI has not created a legal way to obtain a license for the NI part of what makes NI Linux RT a valid target for LabVIEW (LabVIEW runtime, NI-RIO, NI-VISA and many of the other drivers they have for NI Linux RT) using such a system as direct LabVIEW target is illegal.
Even most ARM Cortex-A boards have a good change to run the ARM version of NI Linux RT and the LabVIEW runtime. The difficulty is that most current ARM boards run a 64-bit variant of Linux with Neon hardware FPU support, while the ARM version of NI Linux RT is compiled as 32-bit version with software FPU emulation and the according LabVIEW runtime and drivers too. So there is no way to just install the LabVIEW runtime on them as they are binary incompatible. You need to install the entire NI Linux RT and that comes with some difficulties. While not impossible it requires some tinkering and as far as I found out recompiling of the Linux RT kernel and sometimes adding support for extra hardware components into the kernel. Then you need to try to get the LabVIEW runtime system on that thing. This is were I run into quite some trouble on a Home Assistant Yellow (basically it's a Raspberry Pi Compute Module 4 with network interface and NVMe SSD memory, but the Home Assistant Green should be likely possible too) and eventually lost interest to pursue it, also due to the fact that even if it would eventually work, it's nothing more than a fun show off, as legally it's not something you could ever use until NI decides to provide a way to get a license for it.