]> git.ipfire.org Git - thirdparty/kernel/linux.git/commit
clockevents/drivers/i8253: Fix stop sequence for timer 0
authorDavid Woodhouse <dwmw@amazon.co.uk>
Fri, 2 Aug 2024 13:55:55 +0000 (14:55 +0100)
committerThomas Gleixner <tglx@linutronix.de>
Fri, 2 Aug 2024 16:27:05 +0000 (18:27 +0200)
commit531b2ca0a940ac9db03f246c8b77c4201de72b00
treeb7e4728d24ae0ddf74f2bac22f186e6127a4c36f
parent70e6b7d9ae3c63df90a7bba7700e8d5c300c3c60
clockevents/drivers/i8253: Fix stop sequence for timer 0

According to the data sheet, writing the MODE register should stop the
counter (and thus the interrupts). This appears to work on real hardware,
at least modern Intel and AMD systems. It should also work on Hyper-V.

However, on some buggy virtual machines the mode change doesn't have any
effect until the counter is subsequently loaded (or perhaps when the IRQ
next fires).

So, set MODE 0 and then load the counter, to ensure that those buggy VMs
do the right thing and the interrupts stop. And then write MODE 0 *again*
to stop the counter on compliant implementations too.

Apparently, Hyper-V keeps firing the IRQ *repeatedly* even in mode zero
when it should only happen once, but the second MODE write stops that too.

Userspace test program (mostly written by tglx):
=====
 #include <stdio.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <stdint.h>
 #include <sys/io.h>

static __always_inline void __out##bwl(type value, uint16_t port) \
{ \
asm volatile("out" #bwl " %" #bw "0, %w1" \
     : : "a"(value), "Nd"(port)); \
} \
\
static __always_inline type __in##bwl(uint16_t port) \
{ \
type value; \
asm volatile("in" #bwl " %w1, %" #bw "0" \
     : "=a"(value) : "Nd"(port)); \
return value; \
}

BUILDIO(b, b, uint8_t)

 #define inb __inb
 #define outb __outb

 #define PIT_MODE 0x43
 #define PIT_CH0 0x40
 #define PIT_CH2 0x42

static int is8254;

static void dump_pit(void)
{
if (is8254) {
// Latch and output counter and status
outb(0xC2, PIT_MODE);
printf("%02x %02x %02x\n", inb(PIT_CH0), inb(PIT_CH0), inb(PIT_CH0));
} else {
// Latch and output counter
outb(0x0, PIT_MODE);
printf("%02x %02x\n", inb(PIT_CH0), inb(PIT_CH0));
}
}

int main(int argc, char* argv[])
{
int nr_counts = 2;

if (argc > 1)
nr_counts = atoi(argv[1]);

if (argc > 2)
is8254 = 1;

if (ioperm(0x40, 4, 1) != 0)
return 1;

dump_pit();

printf("Set oneshot\n");
outb(0x38, PIT_MODE);
outb(0x00, PIT_CH0);
outb(0x0F, PIT_CH0);

dump_pit();
usleep(1000);
dump_pit();

printf("Set periodic\n");
outb(0x34, PIT_MODE);
outb(0x00, PIT_CH0);
outb(0x0F, PIT_CH0);

dump_pit();
usleep(1000);
dump_pit();
dump_pit();
usleep(100000);
dump_pit();
usleep(100000);
dump_pit();

printf("Set stop (%d counter writes)\n", nr_counts);
outb(0x30, PIT_MODE);
while (nr_counts--)
outb(0xFF, PIT_CH0);

dump_pit();
usleep(100000);
dump_pit();
usleep(100000);
dump_pit();

printf("Set MODE 0\n");
outb(0x30, PIT_MODE);

dump_pit();
usleep(100000);
dump_pit();
usleep(100000);
dump_pit();

return 0;
}
=====

Suggested-by: Sean Christopherson <seanjc@google.com>
Co-developed-by: Li RongQing <lirongqing@baidu.com>
Signed-off-by: Li RongQing <lirongqing@baidu.com>
Signed-off-by: David Woodhouse <dwmw@amazon.co.uk>
Signed-off-by: Thomas Gleixner <tglx@linutronix.de>
Tested-by: Michael Kelley <mhkelley@outlook.com>
Link: https://lore.kernel.org/all/20240802135555.564941-2-dwmw2@infradead.org
arch/x86/kernel/cpu/mshyperv.c
drivers/clocksource/i8253.c
include/linux/i8253.h