Not able to set GPIO state in Linux Kernel Module

Hi, guys

I am writing a Linux Kernel Module for a TFT display, ILI9341. However, I am having problems even setting GPIO states.

It should turn on the GPIOs when I configure them, but it doesn’t.

Here is what I tried

  • Monitor for error messages in through dmesg → No error shown
  • Verify the GPIO configurations through duo-pinmux → All pins set to GPIO

Here is the code. I would love any suggestions

#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio/consumer.h>

enum ILI9341_ERROR {
    ILI9341_ERROR_NONE,
    ILI9341_ERROR_DC_PIN,
    ILI9341_ERROR_CS_PIN,
    ILI9341_ERROR_RST_PIN,
};


struct display {
    struct {
        int pin;
        struct gpio_desc *desc;
    } dc, cs, rst;
};
1

#define GPIOA_OFFSET 480

static struct display display = {
    .dc = {.pin=GPIOA_OFFSET + 23},
    .cs = {.pin=GPIOA_OFFSET + 24},
    .rst = {.pin=GPIOA_OFFSET + 25},
};


static inline
int ili9341_config_pins(void)
{
    int status = 0;
    display.dc.desc = gpio_to_desc(display.dc.pin);
    if (!display.dc.desc) {
        status = ILI9341_ERROR_DC_PIN;
        goto exit;
    }

    display.cs.desc = gpio_to_desc(display.cs.pin);
    if (!display.cs.desc) {
        status = ILI9341_ERROR_CS_PIN;
        goto error_cs;
    }

    display.rst.desc = gpio_to_desc(display.rst.pin);
    if (!display.rst.desc) {
        status = ILI9341_ERROR_RST_PIN;
        goto error_rst;
    }

error_rst:
    gpiod_put(display.cs.desc);
    display.cs.desc = NULL;
error_cs:
    gpiod_put(display.dc.desc);
    display.dc.desc = NULL;
exit:
    return status;
}


int ili9341_init(void)
{
    int status = 0;

    status = ili9341_config_pins();
    if (status) {
        goto exit;
    }

    gpiod_direction_output(display.dc.desc, 0);
    gpiod_direction_output(display.cs.desc, 0);
    gpiod_direction_output(display.rst.desc, 0);

// Here, they should turn on, but they don't
    gpiod_set_value(display.dc.desc, 1);
    gpiod_set_value(display.cs.desc, 1);
    gpiod_set_value(display.rst.desc, 1);
exit:
    return status;
}


void ili9341_deinit(void)
{
    gpiod_set_value(display.dc.desc, 0);
    gpiod_set_value(display.cs.desc, 0);
    gpiod_set_value(display.rst.desc, 0);

    gpiod_put(display.dc.desc);
    gpiod_put(display.cs.desc);
    gpiod_put(display.rst.desc);

    display.dc.desc = NULL;
    display.cs.desc = NULL;
    display.rst.desc = NULL;
}
MODULE_LICENSE("GPL");


int __init initialize(void) 
{
    int status = 0;

    pr_info("Initializing display\n");

    status = ili9341_init();
    if (status) {
        pr_err("Error initializing display: %d\n", status);
        goto exit;
    }

exit:
    return status;
}


void __exit terminate(void)
{
    pr_info("Deinitializing display\n");
    ili9341_deinit();
}


module_init(initialize);
module_exit(terminate);

Here is the makefile

PROJECT:=my_project
ARCH?=riscv
KDIR?=~/Documents/duo-buildroot-sdk/linux_5.10
CROSS_COMPILE?=~/.local/bin/gcc/riscv64-linux-x86_64/bin/riscv64-unknown-linux-gnu-
BUILD_DIR?=$(PROJECT_DIR)/build

PROJECT_DIR:=$(patsubst %/,%, $(dir $(realpath $(lastword $(MAKEFILE_LIST)))))
PWD:= $(shell pwd)

obj-m+=$(PROJECT).o
CODE_DIR:=$(PROJECT_DIR)/src
OUT_DIR:=$(PROJECT_DIR)/build
C_FILES:=$(patsubst $(PROJECT_DIR)/%,%,$(foreach d,$(CODE_DIR),$(shell find $(d) -type f -name '*.c')))
O_FILES:=$(patsubst %.c,%.o,$(C_FILES))

$(PROJECT)-objs+=$(O_FILES)

all:
	make -C $(KDIR) M=$(PROJECT_DIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) V=0 -I$(KDIR)/include modules 
	@mkdir -p $(OUT_DIR)
	@mv $(PROJECT_DIR)/*.ko $(PROJECT_DIR)/*.mod $(PROJECT_DIR)/*mod.c $(PROJECT_DIR)/*.o \
		$(PROJECT_DIR)/modules.order $(PROJECT_DIR)/Module.symvers $(PROJECT_DIR)/.*.cmd $(OUT_DIR)

clean:
	make -C $(KDIR) M=$(PROJECT_DIR) clean
	rm -rf $(OUT_DIR) 2>/dev/null || true


.PHONY: all clean insmod

Is there any other point I am missing?

I am using the MilkV Duo 64M.

Edit:

I didn’t even get to the part for configuring the SPI. I am still trying to set up the GPIOs for DC, RST, and CS.

I try to write to it using gpiod_set_value(…), but it doesn’t work (I have LEDs attached to the pins). The only pin that works is the XGPIOA_25 (the DC pin). The others don’t.

I’ve not got/used that particular TFT, but for ST7735 I changed the device tree and verified the pins were configured as SPI in the pinmux board init.

They are not that different.

Anyway, the board init is correct

I didn’t even get to the part for configuring the SPI.

I am trying yet to set up the GPIOs for DC, RST and CS.

I try to write to it with gpiod_set_value(…) but it doesn’t work (I have leds attached to the pins). The only pin that works is the XGPIOA_25 (the DC pin). The others don’t.

Edit: typos

If it helps, these are the pins I used.

PINMUX_CONFIG(SD0_PWR_EN, XGPIOA_14);    // Duo Pin 19 // BL
PINMUX_CONFIG(SPK_EN, XGPIOA_15);        // Duo Pin 20 // RES
PINMUX_CONFIG(SPINOR_MISO, XGPIOA_23);   // Duo Pin 21 // DC/AO
PINMUX_CONFIG(SD1_CLK, SPI2_SCK); // SCK
PINMUX_CONFIG(SD1_CMD, SPI2_SDO); // SDA
PINMUX_CONFIG(SD1_D3, SPI2_CS_X); // CS

I later changed IO23 to IO9 to free up SPINOR (but sacrificed I2C for that).

I found the problem. It was really dumb. I missed the “goto exit“ after the configuration.:sweat_smile:

Now, when setting up the SPI, I am getting the error

[ 1832.441977] dw_spi_mmio 41a0000.spi2: chipselect 0 already in use

How did you get around that?

What exactly do you mean by setting up the SPI? Do you have your TFT in the device tree so it can ‘take’ the SPI pins? Before this you will delete the spidev@0 node since that uses spi2 otherwise.

I have an example DT overlay for setting up the pins for a SPI TFT display (st7789) here:

I never found a way to set the GPIO pins in kernel driver code other than manually writing registers, you can look at the aic8800 drivers and USB mode switching scripts in that repo to see how.