// 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 #include #include #include #include #include #include #include #include #include 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(); }