内核版本5.4
在使用spi总线接上了一个小网卡,实现了我们开发板对网络的访问之后,我还想接一个小的spi屏幕 1.44寸款,来画一只小企鹅,顺便显示一些系统的调试信息。但是由于我这个开发板向外暴露出来的spi接口就两个,而且有一个已经因为串口的设置而不能使用。所以我们只能让这个小屏幕和enc28j60共用一个spi外设。

内核配置

直接make menuconfig,进入Device Drivers,打开SPI,打开ST7735R的驱动。保存,再make -j16.

接线与修改设备树

我打算让enc28j60使用spi自己的cs作为片选线,然后另外找一个GPIO作为spi屏幕的片选。
那这样的话又得改设备树。我们这个spi屏幕的驱动器芯片是"st7735s"。但是linux有st7735r,这俩
是兼容的,可以直接用。

/ {model = "Lichee Pi Nano";compatible = "licheepi,licheepi-nano", "allwinner,suniv-f1c100s";aliases {serial1 = &uart1;};chosen {stdout-path = "serial1:115200n8";/delete-node/ framebuffer@0;};reg_vcc3v3: vcc3v3 {compatible = "regulator-fixed";regulator-name = "vcc3v3";regulator-min-microvolt = ;regulator-max-microvolt = ;};backlight: backlight {compatible = "gpio-backlight";gpios = ;default-on;};};&spi1{status = "okay";pinctrl-names = "default";pinctrl-0 = ;cs-gpios = ,;enc28j60: ethernet@0 {compatible = "microchip,enc28j60";pinctrl-names = "default";pinctrl-0 = ;reg = ;interrupt-parent = ;interrupts = ;spi-max-frequency = ;};display@1{compatible = "okaya,rh128128t", "sitronix,st7735r";reg = ;status = "okay";spi-max-frequency = ;spi-cpol;spi-cpha;bgr;rotate = ;fps = ;buswidth = ;dc-gpios = ;reset-gpios = ;backlight = ;};};

我们在spi节点下增加了一个display设备,进入linux主线的设备的DTC绑定信息都是在内核源码中可以找得到的。例如我们要找"sitronix,st7735r"的设备树绑定信息,我们直接打开内核根目录,一搜,就可以在Documentation/devicetree/bindings/display/sitronix,st7735r.yaml找到我们要的信息。

这个yaml给的是最新的drm驱动匹配方式,不过我们目前还是使用比较简单,资料比较多的framebuffer TFT驱动比较好。下一次可以尝试用这个drm驱动。

我们这个spi屏幕由于和enc28j60共用一个spi,所以我们需要指定其片选。我们可以在spi节点下直接设置cs-gpios = ,。这是什么意思呢?第一个代表了对于第一个spi设备,我们使用spi默认的片选引脚。第二个,代表的是我们指定第二个spi设备的片选引脚为PE5,并且低电平有效。GPIO_ACTIVE_LOW的意思是低电平有效,这样做可以在内核中配置不同设备的正负逻辑。

引脚正负逻辑与代码分析

fbtft设备在申请GPIO时,其实用的是gpio子系统的api。这些api用来用去最后是调用了

struct gpio_desc *of_find_gpio(struct device_node *np, const char *con_id,       unsigned int idx, unsigned long *flags)

它调用了

desc = of_get_named_gpiod_flags(np, prop_name, idx, &of_flags);*flags = of_convert_gpio_flags(of_flags);

来获取设备树下gpio属性的flag。在设置gpio的输出值时,即调用gpiod_set_value时,会调用

static void gpiod_set_value_nocheck(struct gpio_desc *desc, int value){if (test_bit(FLAG_ACTIVE_LOW, &desc->flags))value = !value;if (test_bit(FLAG_OPEN_DRAIN, &desc->flags))gpio_set_open_drain_value_commit(desc, value);else if (test_bit(FLAG_OPEN_SOURCE, &desc->flags))gpio_set_open_source_value_commit(desc, value);elsegpiod_set_raw_value_commit(desc, value);}

所以我们在设备树中配置引脚的正负逻辑,会在这里被处理。如果是正逻辑,直接输出,如果是负逻辑,则需要取反。

配置引脚正负逻辑

为了知道我们到底应该在哪些引脚配置我们的正负逻辑,我们应该直接看源代码,因为压根就没有文档说这些。

static void fbtft_reset(struct fbtft_par *par){if (!par->gpio.reset)return;fbtft_par_dbg(DEBUG_RESET, par, "%s()\n", __func__);gpiod_set_value_cansleep(par->gpio.reset, 1);usleep_range(20, 40);gpiod_set_value_cansleep(par->gpio.reset, 0);msleep(120);gpiod_set_value_cansleep(par->gpio.cs, 1);  /* Activate chip */}

RESET引脚在低电平的时候将会导致屏幕复位,Chip-Select在低电平时将会使能设备。所以这两个引脚都是负逻辑。我们应当在他们设备树的GPIO后边加一个GPIO_ACTIVE_LOW

上电测试

我上电后,屏幕一直白屏,调试了很久,代码改来改去,TF卡不知道插拔了多少次,也找不到原因。

最后插上逻辑分析仪,逻辑分析仪真的是电子工程师的眼睛,以前屏幕从来就没调通过,这次插了逻辑分析仪,能够分析逻辑后,调试就变得很容易了。一直看波形,发现RESET引脚一直被拉低。才发现原来linux的gpio子系统有这么一套正负逻辑的区别。

配置好之后,屏幕时好时坏。有多时好时坏呢,一插上逻辑分析仪,屏幕就成功初始化。一拔掉逻辑分析仪,这个屏幕又白屏了。最后一联想,就想到逻辑分析仪不是自带上拉吗?是不是因为我的RESET,CS,MOSI,CLK这些引脚都没有上拉?找了两个上拉电阻,一试,屏幕跑起来了!小企鹅出现了!

背光驱动的修改

其实我一开始把PE4的一个引脚设置为背光引脚(这样做似乎不太对,应该用mosfet或者三极管来驱动这个tft,因为电流大了会把f1c100s的引脚给烧了)。但是这个屏幕不知道为什么,就是不亮,亮都不亮一下。于是我就把这根线拔了,直接把TFT的LED引脚连了3V3。但是我觉得这样不够优雅,一定要让linux自己能够设置这个背光。

backlight: backlight {    compatible = "gpio-backlight";    gpios = ;    default-on;};

这个backlight会注册一个class,位置在/sys/class/backlight/backlight,这个目录下有一个文件叫brightness,即亮度,可以通过echo 1 > brightness,打开我们的背光。

这里我们把gpio配置为正逻辑,这样1就对应着打开背光,符合人类的操作逻辑。

但是为什么,这个背光不工作呢?设置成1或者0,在调试后,发现

static int gpio_backlight_update_status(struct backlight_device *bl){struct gpio_backlight *gbl = bl_get_data(bl);int brightness = bl->props.brightness;if (bl->props.power != FB_BLANK_UNBLANK ||    bl->props.fb_blank != FB_BLANK_UNBLANK ||    bl->props.state & (BL_CORE_SUSPENDED | BL_CORE_FBBLANK))brightness = 0;gpiod_set_value_cansleep(gbl->gpiod, brightness);return 0;}

是它判断这些power fb_blakn state的时候,直接把brightness设置成0了。我们直接去掉这几行代码。但是它调用gpiod_set_value_cansleep(gbl->gpiod, brightness);,我们的gpio也没有输出。

最后看了几遍代码,发现原来是它没有配置好gpio为输出。

static int gpio_backlight_probe_dt(struct platform_device *pdev,   struct gpio_backlight *gbl){struct device *dev = &pdev->dev;int ret;gbl->def_value = device_property_read_bool(dev, "default-on");gbl->gpiod = devm_gpiod_get(dev, NULL, GPIOD_ASIS);if (IS_ERR(gbl->gpiod)) {ret = PTR_ERR(gbl->gpiod);if (ret != -EPROBE_DEFER) {dev_err(dev,"Error: The gpios parameter is missing or invalid.\n");}return ret;}return 0;}

问题就在gbl->gpiod = devm_gpiod_get(dev, NULL, GPIOD_ASIS);的这个GPIOD_ASIS

/** * enum gpiod_flags - Optional flags that can be passed to one of gpiod_* to *                    configure direction and output value. These values *                    cannot be OR'd. * * @GPIOD_ASIS:Don't change anything * @GPIOD_IN:Set lines to input mode * @GPIOD_OUT_LOW:Set lines to output and drive them low * @GPIOD_OUT_HIGH:Set lines to output and drive them high * @GPIOD_OUT_LOW_OPEN_DRAIN:Set lines to open-drain output and drive them low * @GPIOD_OUT_HIGH_OPEN_DRAIN:Set lines to open-drain output and drive them high */enum gpiod_flags {GPIOD_ASIS= 0,GPIOD_IN= GPIOD_FLAGS_BIT_DIR_SET,GPIOD_OUT_LOW= GPIOD_FLAGS_BIT_DIR_SET | GPIOD_FLAGS_BIT_DIR_OUT,GPIOD_OUT_HIGH= GPIOD_FLAGS_BIT_DIR_SET | GPIOD_FLAGS_BIT_DIR_OUT |  GPIOD_FLAGS_BIT_DIR_VAL,GPIOD_OUT_LOW_OPEN_DRAIN = GPIOD_OUT_LOW | GPIOD_FLAGS_BIT_OPEN_DRAIN,GPIOD_OUT_HIGH_OPEN_DRAIN = GPIOD_OUT_HIGH | GPIOD_FLAGS_BIT_OPEN_DRAIN,};

我们改成GPIOD_OUT_LOW,即成功解决问题。当然,最新的驱动已经修复了这些问题。