// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright ( c ) 2025 , Qualcomm Innovation Center , Inc . All rights reserved .
*/
#define pr_fmt(fmt) "tlmm-test: " fmt
#include <kunit/test.h>
#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/pinctrl/consumer.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
/*
* This TLMM test module serves the purpose of validating that the TLMM driver
* ( pinctrl - msm ) delivers expected number of interrupts in response to changing
* GPIO state .
*
* To achieve this without external equipment the test takes a module parameter
* " gpio " , which the tester is expected to specify an unused and non - connected
* pin . The GPIO state is then driven by adjusting the bias of the pin , at
* suitable times through the different test cases .
*
* Upon execution , the test initialization will find the TLMM node ( subject to
* tlmm_of_match [ ] allow listing ) and create the necessary references
* dynamically , rather then relying on e . g . Devicetree and phandles .
*/
#define MSM_PULL_MASK GENMASK(2 , 0 )
#define MSM_PULL_DOWN 1
#define MSM_PULL_UP 3
#define TLMM_REG_SIZE 0 x1000
static int tlmm_test_gpio = -1 ;
static char *tlmm_reg_name = "default_region" ;
module_param_named(gpio, tlmm_test_gpio, int , 0600 );
module_param_named(name, tlmm_reg_name, charp, 0600 );
static struct {
void __iomem *base;
void __iomem *reg;
int irq;
u32 low_val;
u32 high_val;
} tlmm_suite;
/**
* struct tlmm_test_priv - Per - test context
* @ intr_count : number of times hard handler was hit with TLMM_TEST_COUNT op set
* @ thread_count : number of times thread handler was hit with TLMM_TEST_COUNT op set
* @ intr_op : operations to be performed by the hard IRQ handler
* @ intr_op_remain : number of times the TLMM_TEST_THEN_ * operations should be
* performed by the hard IRQ handler
* @ thread_op : operations to be performed by the threaded IRQ handler
* @ thread_op_remain : number of times the TLMM_TEST_THEN_ * operations should
* be performed by the threaded IRQ handler
*/
struct tlmm_test_priv {
atomic_t intr_count;
atomic_t thread_count;
unsigned int intr_op;
atomic_t intr_op_remain;
unsigned int thread_op;
atomic_t thread_op_remain;
};
/* Operation masks for @intr_op and @thread_op */
#define TLMM_TEST_COUNT BIT(0 )
#define TLMM_TEST_OUTPUT_LOW BIT(1 )
#define TLMM_TEST_OUTPUT_HIGH BIT(2 )
#define TLMM_TEST_THEN_HIGH BIT(3 )
#define TLMM_TEST_THEN_LOW BIT(4 )
#define TLMM_TEST_WAKE_THREAD BIT(5 )
static void tlmm_output_low(void )
{
writel(tlmm_suite.low_val, tlmm_suite.reg);
readl(tlmm_suite.reg);
}
static void tlmm_output_high(void )
{
writel(tlmm_suite.high_val, tlmm_suite.reg);
readl(tlmm_suite.reg);
}
static irqreturn_t tlmm_test_intr_fn(int irq, void *dev_id)
{
struct tlmm_test_priv *priv = dev_id;
if (priv->intr_op & TLMM_TEST_COUNT)
atomic_inc(&priv->intr_count);
if (priv->intr_op & TLMM_TEST_OUTPUT_LOW)
tlmm_output_low();
if (priv->intr_op & TLMM_TEST_OUTPUT_HIGH)
tlmm_output_high();
if (atomic_dec_if_positive(&priv->intr_op_remain) > 0 ) {
udelay(1 );
if (priv->intr_op & TLMM_TEST_THEN_LOW)
tlmm_output_low();
if (priv->intr_op & TLMM_TEST_THEN_HIGH)
tlmm_output_high();
}
return priv->intr_op & TLMM_TEST_WAKE_THREAD ? IRQ_WAKE_THREAD : IRQ_HANDLED;
}
static irqreturn_t tlmm_test_intr_thread_fn(int irq, void *dev_id)
{
struct tlmm_test_priv *priv = dev_id;
if (priv->thread_op & TLMM_TEST_COUNT)
atomic_inc(&priv->thread_count);
if (priv->thread_op & TLMM_TEST_OUTPUT_LOW)
tlmm_output_low();
if (priv->thread_op & TLMM_TEST_OUTPUT_HIGH)
tlmm_output_high();
if (atomic_dec_if_positive(&priv->thread_op_remain) > 0 ) {
udelay(1 );
if (priv->thread_op & TLMM_TEST_THEN_LOW)
tlmm_output_low();
if (priv->thread_op & TLMM_TEST_THEN_HIGH)
tlmm_output_high();
}
return IRQ_HANDLED;
}
static void tlmm_test_request_hard_irq(struct kunit *test, unsigned long irqflags)
{
struct tlmm_test_priv *priv = test->priv;
int ret;
ret = request_irq(tlmm_suite.irq, tlmm_test_intr_fn, irqflags, test->name, priv);
KUNIT_EXPECT_EQ(test, ret, 0 );
}
static void tlmm_test_request_threaded_irq(struct kunit *test, unsigned long irqflags)
{
struct tlmm_test_priv *priv = test->priv;
int ret;
ret = request_threaded_irq(tlmm_suite.irq,
tlmm_test_intr_fn, tlmm_test_intr_thread_fn,
irqflags, test->name, priv);
KUNIT_EXPECT_EQ(test, ret, 0 );
}
static void tlmm_test_silent(struct kunit *test, unsigned long irqflags)
{
struct tlmm_test_priv *priv = test->priv;
priv->intr_op = TLMM_TEST_COUNT;
/* GPIO line at non-triggering level */
if (irqflags == IRQF_TRIGGER_LOW || irqflags == IRQF_TRIGGER_FALLING)
tlmm_output_high();
else
tlmm_output_low();
tlmm_test_request_hard_irq(test, irqflags);
msleep(100 );
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 0 );
}
/*
* Test that no RISING interrupts are triggered on a silent pin
*/
static void tlmm_test_silent_rising(struct kunit *test)
{
tlmm_test_silent(test, IRQF_TRIGGER_RISING);
}
/*
* Test that no FALLING interrupts are triggered on a silent pin
*/
static void tlmm_test_silent_falling(struct kunit *test)
{
tlmm_test_silent(test, IRQF_TRIGGER_FALLING);
}
/*
* Test that no LOW interrupts are triggered on a silent pin
*/
static void tlmm_test_silent_low(struct kunit *test)
{
tlmm_test_silent(test, IRQF_TRIGGER_LOW);
}
/*
* Test that no HIGH interrupts are triggered on a silent pin
*/
static void tlmm_test_silent_high(struct kunit *test)
{
tlmm_test_silent(test, IRQF_TRIGGER_HIGH);
}
/*
* Square wave with 10 high pulses , assert that we get 10 rising interrupts
*/
static void tlmm_test_rising(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
int i;
priv->intr_op = TLMM_TEST_COUNT;
tlmm_output_low();
tlmm_test_request_hard_irq(test, IRQF_TRIGGER_RISING);
for (i = 0 ; i < 10 ; i++) {
tlmm_output_low();
msleep(20 );
tlmm_output_high();
msleep(20 );
}
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
}
/*
* Square wave with 10 low pulses , assert that we get 10 falling interrupts
*/
static void tlmm_test_falling(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
int i;
priv->intr_op = TLMM_TEST_COUNT;
tlmm_output_high();
tlmm_test_request_hard_irq(test, IRQF_TRIGGER_FALLING);
for (i = 0 ; i < 10 ; i++) {
tlmm_output_high();
msleep(20 );
tlmm_output_low();
msleep(20 );
}
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
}
/*
* Drive line low 10 times , handler drives it high to " clear the interrupt
* source " , assert we get 10 interrupts
*/
static void tlmm_test_low(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
int i;
priv->intr_op = TLMM_TEST_COUNT | TLMM_TEST_OUTPUT_HIGH;
atomic_set(&priv->intr_op_remain, 9 );
tlmm_output_high();
tlmm_test_request_hard_irq(test, IRQF_TRIGGER_LOW);
for (i = 0 ; i < 10 ; i++) {
msleep(20 );
tlmm_output_low();
}
msleep(100 );
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
}
/*
* Drive line high 10 times , handler drives it low to " clear the interrupt
* source " , assert we get 10 interrupts
*/
static void tlmm_test_high(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
int i;
priv->intr_op = TLMM_TEST_COUNT | TLMM_TEST_OUTPUT_LOW;
atomic_set(&priv->intr_op_remain, 9 );
tlmm_output_low();
tlmm_test_request_hard_irq(test, IRQF_TRIGGER_HIGH);
for (i = 0 ; i < 10 ; i++) {
msleep(20 );
tlmm_output_high();
}
msleep(100 );
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
}
/*
* Handler drives GPIO high to " clear the interrupt source " , then low to
* simulate a new interrupt , repeated 10 times , assert we get 10 interrupts
*/
static void tlmm_test_falling_in_handler(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
priv->intr_op = TLMM_TEST_COUNT | TLMM_TEST_OUTPUT_HIGH | TLMM_TEST_THEN_LOW;
atomic_set(&priv->intr_op_remain, 10 );
tlmm_output_high();
tlmm_test_request_hard_irq(test, IRQF_TRIGGER_FALLING);
msleep(20 );
tlmm_output_low();
msleep(100 );
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
}
/*
* Handler drives GPIO low to " clear the interrupt source " , then high to
* simulate a new interrupt , repeated 10 times , assert we get 10 interrupts
*/
static void tlmm_test_rising_in_handler(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
priv->intr_op = TLMM_TEST_COUNT | TLMM_TEST_OUTPUT_LOW | TLMM_TEST_THEN_HIGH;
atomic_set(&priv->intr_op_remain, 10 );
tlmm_output_low();
tlmm_test_request_hard_irq(test, IRQF_TRIGGER_RISING);
msleep(20 );
tlmm_output_high();
msleep(100 );
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
}
/*
* Square wave with 10 high pulses , assert that we get 10 rising hard and
* 10 threaded interrupts
*/
static void tlmm_test_thread_rising(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
int i;
priv->intr_op = TLMM_TEST_COUNT | TLMM_TEST_WAKE_THREAD;
priv->thread_op = TLMM_TEST_COUNT;
tlmm_output_low();
tlmm_test_request_threaded_irq(test, IRQF_TRIGGER_RISING);
for (i = 0 ; i < 10 ; i++) {
tlmm_output_low();
msleep(20 );
tlmm_output_high();
msleep(20 );
}
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
KUNIT_ASSERT_EQ(test, atomic_read(&priv->thread_count), 10 );
}
/*
* Square wave with 10 low pulses , assert that we get 10 falling interrupts
*/
static void tlmm_test_thread_falling(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
int i;
priv->intr_op = TLMM_TEST_COUNT | TLMM_TEST_WAKE_THREAD;
priv->thread_op = TLMM_TEST_COUNT;
tlmm_output_high();
tlmm_test_request_threaded_irq(test, IRQF_TRIGGER_FALLING);
for (i = 0 ; i < 10 ; i++) {
tlmm_output_high();
msleep(20 );
tlmm_output_low();
msleep(20 );
}
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
KUNIT_ASSERT_EQ(test, atomic_read(&priv->thread_count), 10 );
}
/*
* Drive line high 10 times , threaded handler drives it low to " clear the
* interrupt source " , assert we get 10 interrupts
*/
static void tlmm_test_thread_high(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
int i;
priv->intr_op = TLMM_TEST_COUNT | TLMM_TEST_WAKE_THREAD;
priv->thread_op = TLMM_TEST_COUNT | TLMM_TEST_OUTPUT_LOW;
tlmm_output_low();
tlmm_test_request_threaded_irq(test, IRQF_TRIGGER_HIGH | IRQF_ONESHOT);
for (i = 0 ; i < 10 ; i++) {
tlmm_output_high();
msleep(20 );
}
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
KUNIT_ASSERT_EQ(test, atomic_read(&priv->thread_count), 10 );
}
/*
* Drive line low 10 times , threaded handler drives it high to " clear the
* interrupt source " , assert we get 10 interrupts
*/
static void tlmm_test_thread_low(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
int i;
priv->intr_op = TLMM_TEST_COUNT | TLMM_TEST_WAKE_THREAD;
priv->thread_op = TLMM_TEST_COUNT | TLMM_TEST_OUTPUT_HIGH;
tlmm_output_high();
tlmm_test_request_threaded_irq(test, IRQF_TRIGGER_LOW | IRQF_ONESHOT);
for (i = 0 ; i < 10 ; i++) {
tlmm_output_low();
msleep(20 );
}
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
KUNIT_ASSERT_EQ(test, atomic_read(&priv->thread_count), 10 );
}
/*
* Handler drives GPIO low to " clear the interrupt source " , then high in the
* threaded handler to simulate a new interrupt , repeated 10 times , assert we
* get 10 interrupts
*/
static void tlmm_test_thread_rising_in_handler(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
priv->intr_op = TLMM_TEST_COUNT | TLMM_TEST_OUTPUT_LOW | TLMM_TEST_WAKE_THREAD;
priv->thread_op = TLMM_TEST_COUNT | TLMM_TEST_THEN_HIGH;
atomic_set(&priv->thread_op_remain, 10 );
tlmm_output_low();
tlmm_test_request_threaded_irq(test, IRQF_TRIGGER_RISING);
msleep(20 );
tlmm_output_high();
msleep(100 );
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
KUNIT_ASSERT_EQ(test, atomic_read(&priv->thread_count), 10 );
}
/*
* Handler drives GPIO high to " clear the interrupt source " , then low in the
* threaded handler to simulate a new interrupt , repeated 10 times , assert we
* get 10 interrupts
*/
static void tlmm_test_thread_falling_in_handler(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
priv->intr_op = TLMM_TEST_COUNT | TLMM_TEST_OUTPUT_HIGH | TLMM_TEST_WAKE_THREAD;
priv->thread_op = TLMM_TEST_COUNT | TLMM_TEST_THEN_LOW;
atomic_set(&priv->thread_op_remain, 10 );
tlmm_output_high();
tlmm_test_request_threaded_irq(test, IRQF_TRIGGER_FALLING);
msleep(20 );
tlmm_output_low();
msleep(100 );
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 10 );
KUNIT_ASSERT_EQ(test, atomic_read(&priv->thread_count), 10 );
}
/*
* Validate that edge interrupts occurring while irq is disabled is delivered
* once the interrupt is reenabled .
*/
static void tlmm_test_rising_while_disabled(struct kunit *test)
{
struct tlmm_test_priv *priv = test->priv;
unsigned int after_edge;
unsigned int before_edge;
priv->intr_op = TLMM_TEST_COUNT;
atomic_set(&priv->thread_op_remain, 10 );
tlmm_output_low();
tlmm_test_request_hard_irq(test, IRQF_TRIGGER_RISING);
msleep(20 );
disable_irq(tlmm_suite.irq);
before_edge = atomic_read(&priv->intr_count);
tlmm_output_high();
msleep(20 );
after_edge = atomic_read(&priv->intr_count);
msleep(20 );
enable_irq(tlmm_suite.irq);
msleep(20 );
free_irq(tlmm_suite.irq, priv);
KUNIT_ASSERT_EQ(test, before_edge, 0 );
KUNIT_ASSERT_EQ(test, after_edge, 0 );
KUNIT_ASSERT_EQ(test, atomic_read(&priv->intr_count), 1 );
}
static int tlmm_test_init(struct kunit *test)
{
struct tlmm_test_priv *priv;
priv = kunit_kzalloc(test, sizeof (*priv), GFP_KERNEL);
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, priv);
atomic_set(&priv->intr_count, 0 );
atomic_set(&priv->thread_count, 0 );
atomic_set(&priv->intr_op_remain, 0 );
atomic_set(&priv->thread_op_remain, 0 );
test->priv = priv;
return 0 ;
}
/*
* NOTE : When adding compatibles to this list , ensure that TLMM_REG_SIZE and
* pull configuration values are supported and correct .
*/
static const struct of_device_id tlmm_of_match[] = {
{ .compatible = "qcom,sc8280xp-tlmm" },
{ .compatible = "qcom,x1e80100-tlmm" },
{}
};
static int tlmm_reg_base(struct device_node *tlmm, struct resource *res)
{
const char **reg_names;
int count;
int ret;
int i;
count = of_property_count_strings(tlmm, "reg-names" );
if (count <= 0 ) {
pr_err("failed to find tlmm reg name\n" );
return count;
}
reg_names = kcalloc(count, sizeof (char *), GFP_KERNEL);
if (!reg_names)
return -ENOMEM;
ret = of_property_read_string_array(tlmm, "reg-names" , reg_names, count);
if (ret != count) {
kfree(reg_names);
return -EINVAL;
}
if (!strcmp(tlmm_reg_name, "default_region" )) {
ret = of_address_to_resource(tlmm, 0 , res);
} else {
for (i = 0 ; i < count; i++) {
if (!strcmp(reg_names[i], tlmm_reg_name)) {
ret = of_address_to_resource(tlmm, i, res);
break ;
}
}
if (i == count)
ret = -EINVAL;
}
kfree(reg_names);
return ret;
}
static int tlmm_test_init_suite(struct kunit_suite *suite)
{
struct of_phandle_args args = {};
struct resource res;
int ret;
u32 val;
if (tlmm_test_gpio < 0 ) {
pr_err("use the tlmm-test.gpio module parameter to specify which GPIO to use\n" );
return -EINVAL;
}
struct device_node *tlmm __free(device_node) = of_find_matching_node(NULL, tlmm_of_match);
if (!tlmm) {
pr_err("failed to find tlmm node\n" );
return -EINVAL;
}
ret = tlmm_reg_base(tlmm, &res);
if (ret < 0 )
return ret;
tlmm_suite.base = ioremap(res.start, resource_size(&res));
if (!tlmm_suite.base)
return -ENOMEM;
args.np = tlmm;
args.args_count = 2 ;
args.args[0 ] = tlmm_test_gpio;
args.args[1 ] = 0 ;
tlmm_suite.irq = irq_create_of_mapping(&args);
if (!tlmm_suite.irq) {
pr_err("failed to map TLMM irq %d\n" , args.args[0 ]);
goto err_unmap;
}
tlmm_suite.reg = tlmm_suite.base + tlmm_test_gpio * TLMM_REG_SIZE;
val = readl(tlmm_suite.reg) & ~MSM_PULL_MASK;
tlmm_suite.low_val = val | MSM_PULL_DOWN;
tlmm_suite.high_val = val | MSM_PULL_UP;
return 0 ;
err_unmap:
iounmap(tlmm_suite.base);
tlmm_suite.base = NULL;
return -EINVAL;
}
static void tlmm_test_exit_suite(struct kunit_suite *suite)
{
irq_dispose_mapping(tlmm_suite.irq);
iounmap(tlmm_suite.base);
tlmm_suite.base = NULL;
tlmm_suite.irq = -1 ;
}
static struct kunit_case tlmm_test_cases[] = {
KUNIT_CASE(tlmm_test_silent_rising),
KUNIT_CASE(tlmm_test_silent_falling),
KUNIT_CASE(tlmm_test_silent_low),
KUNIT_CASE(tlmm_test_silent_high),
KUNIT_CASE(tlmm_test_rising),
KUNIT_CASE(tlmm_test_falling),
KUNIT_CASE(tlmm_test_high),
KUNIT_CASE(tlmm_test_low),
KUNIT_CASE(tlmm_test_rising_in_handler),
KUNIT_CASE(tlmm_test_falling_in_handler),
KUNIT_CASE(tlmm_test_thread_rising),
KUNIT_CASE(tlmm_test_thread_falling),
KUNIT_CASE(tlmm_test_thread_high),
KUNIT_CASE(tlmm_test_thread_low),
KUNIT_CASE(tlmm_test_thread_rising_in_handler),
KUNIT_CASE(tlmm_test_thread_falling_in_handler),
KUNIT_CASE(tlmm_test_rising_while_disabled),
{}
};
static struct kunit_suite tlmm_test_suite = {
.name = "tlmm-test" ,
.init = tlmm_test_init,
.suite_init = tlmm_test_init_suite,
.suite_exit = tlmm_test_exit_suite,
.test_cases = tlmm_test_cases,
};
kunit_test_suites(&tlmm_test_suite);
MODULE_DESCRIPTION("Qualcomm TLMM test" );
MODULE_LICENSE("GPL" );
Messung V0.5 in Prozent C=97 H=99 G=97
¤ Dauer der Verarbeitung: 0.15 Sekunden
(vorverarbeitet am 2026-09-28)
¤
*© Formatika GbR, Deutschland