| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Test Timebase |
| * |
| * Copyright 2024 Nicholas Piggin, IBM Corp. |
| * |
| * This contains tests of timebase facility, TB, DEC, etc. |
| */ |
| #include <libcflat.h> |
| #include <util.h> |
| #include <migrate.h> |
| #include <alloc.h> |
| #include <asm/handlers.h> |
| #include <devicetree.h> |
| #include <asm/hcall.h> |
| #include <asm/processor.h> |
| #include <asm/time.h> |
| #include <asm/barrier.h> |
| |
| static int dec_bits = 0; |
| |
| static void cpu_dec_bits(int fdtnode, u64 regval __unused, void *arg __unused) |
| { |
| const struct fdt_property *prop; |
| int plen; |
| |
| prop = fdt_get_property(dt_fdt(), fdtnode, "ibm,dec-bits", &plen); |
| if (!prop) { |
| dec_bits = 32; |
| return; |
| } |
| |
| /* Sanity check for the property layout (first two bytes are header) */ |
| assert(plen == 4); |
| |
| /* Check all CPU nodes have the same value of dec-bits */ |
| if (dec_bits) |
| assert(dec_bits == fdt32_to_cpu(*(uint32_t *)prop->data)); |
| else |
| dec_bits = fdt32_to_cpu(*(uint32_t *)prop->data); |
| } |
| |
| /* Check amount of CPUs nodes that have the TM flag */ |
| static int find_dec_bits(void) |
| { |
| int ret; |
| |
| ret = dt_for_each_cpu_node(cpu_dec_bits, NULL); |
| if (ret < 0) |
| return ret; |
| |
| return dec_bits; |
| } |
| |
| |
| static bool do_migrate = false; |
| static volatile bool got_interrupt; |
| static volatile struct pt_regs recorded_regs; |
| |
| static uint64_t dec_max; |
| static uint64_t dec_min; |
| |
| static void test_tb(int argc, char **argv) |
| { |
| uint64_t tb; |
| int i; |
| |
| tb = get_tb(); |
| report(get_tb() >= tb, "timebase is not going backwards"); |
| if (do_migrate) { |
| tb = get_tb(); |
| migrate(); |
| report(get_tb() >= tb, |
| "timebase is not going backwards over migration"); |
| } |
| |
| for (i = 0; i < 100; i++) { |
| if (get_tb() > tb) |
| break; |
| } |
| report(get_tb() > tb, "timebase is incrementing"); |
| } |
| |
| static void dec_stop_handler(struct pt_regs *regs, void *data) |
| { |
| mtspr(SPR_DEC, dec_max); |
| } |
| |
| static void dec_handler(struct pt_regs *regs, void *data) |
| { |
| got_interrupt = true; |
| memcpy((void *)&recorded_regs, regs, sizeof(struct pt_regs)); |
| regs->msr &= ~MSR_EE; |
| } |
| |
| static void test_dec(int argc, char **argv) |
| { |
| uint64_t tb1, tb2, dec; |
| int i; |
| |
| handle_exception(0x900, &dec_handler, NULL); |
| |
| for (i = 0; i < 100; i++) { |
| tb1 = get_tb(); |
| mtspr(SPR_DEC, dec_max); |
| dec = mfspr(SPR_DEC); |
| tb2 = get_tb(); |
| if (tb2 - tb1 < dec_max - dec) |
| break; |
| } |
| /* POWER CPUs can have a slight (few ticks) variation here */ |
| report_kfail(!host_is_tcg, tb2 - tb1 >= dec_max - dec, |
| "decrementer remains within TB after mtDEC"); |
| |
| tb1 = get_tb(); |
| mtspr(SPR_DEC, dec_max); |
| mdelay(1000); |
| dec = mfspr(SPR_DEC); |
| tb2 = get_tb(); |
| report(tb2 - tb1 >= dec_max - dec, |
| "decrementer remains within TB after 1s"); |
| |
| mtspr(SPR_DEC, dec_max); |
| local_irq_enable(); |
| local_irq_disable(); |
| if (mfspr(SPR_DEC) <= dec_max) { |
| report(!got_interrupt, |
| "no interrupt on decrementer positive"); |
| } |
| got_interrupt = false; |
| |
| mtspr(SPR_DEC, 1); |
| mdelay(100); /* Give the timer a chance to run */ |
| if (do_migrate) |
| migrate(); |
| local_irq_enable(); |
| local_irq_disable(); |
| report(got_interrupt, "interrupt on decrementer underflow"); |
| got_interrupt = false; |
| |
| if (do_migrate) |
| migrate(); |
| local_irq_enable(); |
| local_irq_disable(); |
| report(got_interrupt, "interrupt on decrementer still underflown"); |
| got_interrupt = false; |
| |
| mtspr(SPR_DEC, 0); |
| mdelay(100); /* Give the timer a chance to run */ |
| if (do_migrate) |
| migrate(); |
| local_irq_enable(); |
| local_irq_disable(); |
| report(got_interrupt, "DEC deal with set to 0"); |
| got_interrupt = false; |
| |
| /* Test for level-triggered decrementer */ |
| mtspr(SPR_DEC, -1ULL); |
| if (do_migrate) |
| migrate(); |
| local_irq_enable(); |
| local_irq_disable(); |
| report(got_interrupt, "interrupt on decrementer write MSB"); |
| got_interrupt = false; |
| |
| mtspr(SPR_DEC, dec_max); |
| local_irq_enable(); |
| if (do_migrate) |
| migrate(); |
| mtspr(SPR_DEC, -1); |
| local_irq_disable(); |
| report(got_interrupt, "interrupt on decrementer write MSB with irqs on"); |
| got_interrupt = false; |
| |
| mtspr(SPR_DEC, dec_min + 1); |
| mdelay(100); |
| local_irq_enable(); |
| local_irq_disable(); |
| /* TCG does not model this correctly */ |
| report_kfail(host_is_tcg, !got_interrupt, |
| "no interrupt after wrap to positive"); |
| got_interrupt = false; |
| |
| handle_exception(0x900, NULL, NULL); |
| } |
| |
| static void test_hdec(int argc, char **argv) |
| { |
| uint64_t tb1, tb2, hdec; |
| |
| if (!machine_is_powernv()) { |
| report_skip("test reqiures powernv machine"); |
| return; |
| } |
| |
| handle_exception(0x900, &dec_stop_handler, NULL); |
| handle_exception(0x980, &dec_handler, NULL); |
| |
| mtspr(SPR_HDEC, dec_max); |
| mtspr(SPR_LPCR, mfspr(SPR_LPCR) | LPCR_HDICE); |
| |
| tb1 = get_tb(); |
| mtspr(SPR_HDEC, dec_max); |
| hdec = mfspr(SPR_HDEC); |
| tb2 = get_tb(); |
| report(tb2 - tb1 >= dec_max - hdec, "hdecrementer remains within TB"); |
| |
| tb1 = get_tb(); |
| mtspr(SPR_HDEC, dec_max); |
| mdelay(1000); |
| hdec = mfspr(SPR_HDEC); |
| tb2 = get_tb(); |
| report(tb2 - tb1 >= dec_max - hdec, "hdecrementer remains within TB after 1s"); |
| |
| mtspr(SPR_HDEC, dec_max); |
| local_irq_enable(); |
| local_irq_disable(); |
| if (mfspr(SPR_HDEC) <= dec_max) { |
| report(!got_interrupt, "no interrupt on decrementer positive"); |
| } |
| got_interrupt = false; |
| |
| mtspr(SPR_HDEC, 1); |
| mdelay(100); /* Give the timer a chance to run */ |
| if (do_migrate) |
| migrate(); |
| /* HDEC is edge triggered so ensure it still fires */ |
| mtspr(SPR_HDEC, dec_max); |
| local_irq_enable(); |
| local_irq_disable(); |
| report(got_interrupt, "interrupt on hdecrementer underflow"); |
| got_interrupt = false; |
| |
| if (do_migrate) |
| migrate(); |
| local_irq_enable(); |
| local_irq_disable(); |
| report(!got_interrupt, "no interrupt on hdecrementer still underflown"); |
| got_interrupt = false; |
| |
| mtspr(SPR_HDEC, -1ULL); |
| if (do_migrate) |
| migrate(); |
| local_irq_enable(); |
| local_irq_disable(); |
| report(got_interrupt, "no interrupt on hdecrementer underflown write MSB"); |
| got_interrupt = false; |
| |
| mtspr(SPR_HDEC, 0); |
| mdelay(100); /* Give the timer a chance to run */ |
| if (do_migrate) |
| migrate(); |
| /* HDEC is edge triggered so ensure it still fires */ |
| mtspr(SPR_HDEC, dec_max); |
| local_irq_enable(); |
| local_irq_disable(); |
| report(got_interrupt, "HDEC deal with set to 0"); |
| got_interrupt = false; |
| |
| mtspr(SPR_HDEC, dec_max); |
| local_irq_enable(); |
| if (do_migrate) |
| migrate(); |
| mtspr(SPR_HDEC, -1ULL); |
| local_irq_disable(); |
| report(got_interrupt, "interrupt on hdecrementer write MSB with irqs on"); |
| got_interrupt = false; |
| |
| mtspr(SPR_HDEC, dec_max); |
| got_interrupt = false; |
| mtspr(SPR_HDEC, dec_min + 1); |
| if (do_migrate) |
| migrate(); |
| mdelay(100); |
| local_irq_enable(); |
| local_irq_disable(); |
| report(got_interrupt, "got interrupt after wrap to positive"); |
| got_interrupt = false; |
| |
| mtspr(SPR_HDEC, -1ULL); |
| local_irq_enable(); |
| local_irq_disable(); |
| got_interrupt = false; |
| mtspr(SPR_HDEC, dec_min + 1000000); |
| if (do_migrate) |
| migrate(); |
| mdelay(100); |
| mtspr(SPR_HDEC, -1ULL); |
| local_irq_enable(); |
| local_irq_disable(); |
| report(got_interrupt, "edge re-armed after wrap to positive"); |
| got_interrupt = false; |
| |
| mtspr(SPR_LPCR, mfspr(SPR_LPCR) & ~LPCR_HDICE); |
| |
| handle_exception(0x900, NULL, NULL); |
| handle_exception(0x980, NULL, NULL); |
| } |
| |
| struct { |
| const char *name; |
| void (*func)(int argc, char **argv); |
| } hctests[] = { |
| { "tb", test_tb }, |
| { "dec", test_dec }, |
| { "hdec", test_hdec }, |
| { NULL, NULL } |
| }; |
| |
| int main(int argc, char **argv) |
| { |
| bool all; |
| int i; |
| |
| all = argc == 1 || !strcmp(argv[1], "all"); |
| |
| for (i = 1; i < argc; i++) { |
| if (!strcmp(argv[i], "-w")) { |
| do_migrate = true; |
| if (!all && argc == 2) |
| all = true; |
| } |
| } |
| |
| find_dec_bits(); |
| dec_max = (1ULL << (dec_bits - 1)) - 1; |
| dec_min = (1ULL << (dec_bits - 1)); |
| |
| if (machine_is_powernv() && dec_bits > 32) { |
| mtspr(SPR_LPCR, mfspr(SPR_LPCR) | LPCR_LD); |
| } |
| |
| report_prefix_push("timebase"); |
| |
| for (i = 0; hctests[i].name != NULL; i++) { |
| if (all || strcmp(argv[1], hctests[i].name) == 0) { |
| report_prefix_push(hctests[i].name); |
| hctests[i].func(argc, argv); |
| report_prefix_pop(); |
| } |
| } |
| |
| report_prefix_pop(); |
| |
| if (machine_is_powernv() && dec_bits > 32) { |
| mtspr(SPR_LPCR, mfspr(SPR_LPCR) & ~LPCR_LD); |
| } |
| |
| return report_summary(); |
| } |