此为PLCT Lab BJ71实习内容,我的工作是为RT-Thread完善Milk-v Duo开发板支持。

GPIO

感受之前已经详细记录过了,在此便不再赘述。这里只写一些开发流程和最终代码框架展示。

开发流程

10.13—概述

是什么

首先了解一下gpio是什么。

芯片上的引脚一般分为 4 类:电源、时钟、控制与 I/O,I/O 口在使用模式上又分为 General Purpose Input Output(通用输入 / 输出),简称 GPIO,与功能复用 I/O(如 SPI/I2C/UART 等)。

https://blog.csdn.net/m0_56694518/article/details/131207367

GPIO是英文General Purpose Input/Output的缩写,中文翻译为通用输入/输出。它是一种在数字电子系统中常见的接口类型,用于与外部设备进行通信和控制。

GPIO接口可以作为输入或输出引脚使用。作为输入引脚时,GPIO可以接收来自外部设备的电信号,并将其转换为数字信号,供系统内部使用。作为输出引脚时,GPIO可以将数字信号发送到外部设备,从而实现对其的控制。

  1. 引脚:GPIO接口通常由一组引脚组成,每个引脚都可以用作输入或输出。每个引脚都有一个唯一的标识符,如GPIO0、GPIO1等。

  2. 输入模式:当GPIO引脚配置为输入模式时,它可以接收外部设备发送的电信号。通常,输入引脚可以读取高电平(1)或低电平(0)状态,或者在某些系统中可以读取模拟信号。

  3. 输出模式:当GPIO引脚配置为输出模式时,它可以向外部设备发送数字信号。输出引脚可以设置为高电平(1)或低电平(0),以控制连接的设备的状态。

  4. 状态和电平:GPIO引脚的状态表示当前引脚的输入或输出电平。高电平通常表示逻辑1,低电平表示逻辑0。在某些系统中,还可以使用其他状态,如浮空、上拉和下拉等。

  5. 控制寄存器:为了配置和控制GPIO引脚的功能,通常需要通过写入特定的控制寄存器来设置引脚的模式、状态和电平。这些寄存器的具体配置取决于所使用的硬件平台和操作系统。【这个估计就是我们驱动要干的】

也就是意思就是,这个gpio相当于一个接口,能够把系统的信号传给硬件,也能把硬件信号传给系统。所以有时候我们可以通过读写其寄存器来与硬件交互。比如, GPIO每个引脚连接着一个led,那么我们通过读写gpio的控制寄存器,控制每个引脚输出1/0,就可以控制led灯亮or灭。

GPIO的实际应用非常广泛,以下是一些常见的示例:

  1. 控制LED:将GPIO引脚配置为输出模式,可以通过设置引脚的高低电平状态来控制LED的亮灭。

  2. 按钮输入:将GPIO引脚配置为输入模式,可以连接按钮或开关,并通过读取引脚的电平状态来检测按钮是否被按下或开关是否打开。

  3. 传感器接口:通过GPIO引脚,可以连接各种传感器,如温度传感器、湿度传感器、光照传感器等。传感器的输出信号可以通过读取GPIO引脚的状态来获取。

  4. 驱动电机:通过GPIO引脚,可以连接电机驱动器,并通过设置引脚的高低电平状态来控制电机的运行方向和速度。

  5. 与外部设备通信:通过GPIO引脚,可以与其他外部设备进行通信,如显示器、LCD屏幕、数码管等。通过设置引脚的状态和电平,可以发送数据或控制命令

  6. 脉冲宽度调制(PWM)输出:一些GPIO引脚支持PWM功能,可以生成模拟信号,用于控制电机速度、调节LED亮度等需要模拟输出的应用。

  7. 扩展IO功能:通过使用扩展芯片或GPIO扩展板,可以增加系统的GPIO引脚数量,从而实现更多外部设备的控制和通信。

寄存器

4 个 32 位 配 置 寄 存 器

  1. GPIOx_MODER 模式寄存器

    用于配置GPIO引脚的模式(输入或输出)。每个引脚通常使用两个位表示模式,例如00表示输入模式,01表示输出模式。

  2. GPIOx_OTYPER 输出模式寄存器

    用于配置GPIO引脚的输出类型。每个引脚通常使用一个位表示输出类型,例如0表示推挽输出,1表示开漏输出。

  3. GPIOx_ OSPEEDR 输出速度寄存器

    用于配置GPIO引脚的输出速度。每个引脚通常使用两个位表示输出速度,例如00表示低速,11表示高速。

  4. GPIOx_PUPDR 上拉下拉寄存器

    用于配置GPIO引脚的上拉或下拉电阻。每个引脚通常使用两个位表示上拉/下拉配置,例如00表示无上拉/下拉,01表示上拉,10表示下拉。

2 个 32 位数据寄存器
GPIOx_IDR 输入数据寄存器
GPIOx_ODR 输出数据寄存器

每个位对应一个引脚,读取/写入该位。

1个 32 位置位 / 复位寄存器
GPIOx_BSRR 置位 / 复位寄存器

用于通过设置或复位位来控制GPIO引脚的输出电平。每个引脚通常使用两个位,一个位用于置位(设置为1),另一个位用于复位(设置为0)。

2 个 32 位复用功能寄存器
GPIOx_AFRH
GPIOx_AFRL

用于配置GPIO引脚的复用功能,例如将引脚用作特定的外设功能(如UART、SPI等)。这些寄存器通常将32位分为两个部分,每个部分对应一组引脚。

模式

https://zhuanlan.zhihu.com/p/612333717?utm_id=0

v2-9ece20d4c5fb58bd9c9736a14e952403_1440w

原来上下拉是这个意思啊,就是缺省值呗。

一般来说,开漏输出连接上拉输入或浮空输入的外部元件,推挽输出连接下拉输入的外部元件。

当引脚具有复用功能时,它可以在不同的工作模式下切换为不同的功能,而无需切换整个 GPIO 模式。

找到测试程序

感觉其实思路还是比较清晰。看这个:

https://milkv.io/zh/docs/duo/application-development/wiringx

https://github.com/milkv-duo/duo-examples/blob/main/README-zh.md

也即我最后gpio的效果就是能够运行它给的这个blink example就行。等下回去试下用linux和rtt试试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int gpio_test(void)
{
int DUO_LED = 25;

rt_pin_mode(DUO_LED, PIN_MODE_OUTPUT);
rt_pin_write(DUO_LED, PIN_LOW);

for (int i = 0; i < 10; i ++) { // 闪烁十次
rt_kprintf("Duo LED GPIO (wiringX) %d: High\n", DUO_LED);
rt_pin_write(DUO_LED, PIN_HIGH);
rt_thread_delay(RT_TICK_PER_SECOND);
rt_kprintf("Duo LED GPIO (wiringX) %d: Low\n", DUO_LED);
rt_pin_write(DUO_LED, PIN_LOW);
rt_thread_delay(RT_TICK_PER_SECOND);
}

return 0;
}

总之明天试着开始写吧,我也不知道怎么办了该。可以先问下老师确认led是哪个。我看文档写的GPIOC24但是压根没那东西。

10.14—找到型号

感觉可以从相同型号的gpio入手。查了下compatible,这东西好像是什么海思研发的什么the synopsys DW gpio。

/home/xiunian/rt-thread/milkv-duo-buildroot-sdk/linux_5.10/drivers/gpio/gpio-dwapb.c

https://lkml.org/lkml/2020/8/22/19 commit patch

Milk-V Duo开发板免费体验 GPIO分析

DesignWare_APB_GPIO模块DUT&Testbench仿真

RK3399之8250串口驱动

linux驱动 内核层适配485驱动控制引脚

我现在发现了milkv的gpio型号是dwapb,找到了它对应的驱动手册【DW_apb_gpio_databook 浅看了下,里面至少有介绍寄存器是在干什么】,还有linux【drivers/gpio/gpio-dwapb.c drivers/gpio/gpio-pl061.c】和rtt【bsp/ft2004/libraries/bsp/ft_gpio/ft_gpio.c】同一型号的驱动代码,我准备对照着这几个参考弄下

然后各个回调的介绍可以看documentation/device/pin/pin.md

总之,不就是看寄存器都是啥东西,然后对应实现read write嘛!概念上是不难的,好好研究,相信可以!

接下来的思路就是,看下那几个寄存器具体的description,理解下两个参考的代码,就可以慢慢开始写了。测试可能需要再花点心思因为毕竟那个还跑步起来。。。总之方向算是比较明确了(至少比前几天明确)加油捏。

10.15—寄存器了解

这东西甚至是一个bit一个bit控制的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#define GPIO_SWPORTA_DR		0x00 //A 组端口输出寄存器
写入该寄存器的值独立控制端口A中对应数据位的方向。0是input,1是output
#define GPIO_SWPORTA_DDR 0x04 //A 组端口方向控制寄存器
#define GPIO_SWPORTB_DR 0x0c //B 组端口输出寄存器
#define GPIO_SWPORTB_DDR 0x10 //B 组端口方向控制寄存器
#define GPIO_SWPORTC_DR 0x18 //C 组端口输出寄存器
#define GPIO_SWPORTC_DDR 0x1c //C 组端口方向控制寄存器
#define GPIO_SWPORTD_DR 0x24 //D 组端口输出寄存器
#define GPIO_SWPORTD_DDR 0x28 //D 组端口方向控制寄存器

1. 这个东西可以用来控制每个bit的开关中断
2. 默认关中断
3. Output时关中断
#define GPIO_INTEN 0x30 //A 组端口中断使能寄存器
mask为1表示屏蔽
#define GPIO_INTMASK 0x34 //A 组端口中断屏蔽寄存器
控制是电平敏感中断还是边缘敏感
#define GPIO_INTTYPE_LEVEL 0x38 //A 组端口中断等级寄存器
极性
#define GPIO_INT_POLARITY 0x3c //A 组端口中断极性寄存器
只读
#define GPIO_INTSTATUS 0x40 //A 组端口中断状态寄存器
是否需要消抖【是不是很熟悉hhh】
#define GPIO_PORTA_DEBOUNCE 0x48 //A 组端口防反跳配置寄存器
清除A/所有中断
#define GPIO_PORTA_EOI 0x4c //A 组端口中断清除寄存器

只读,表示A端口连接的数据信号(也即input信号)
#define GPIO_EXT_PORTA 0x50 //A 组端口输入寄存器
#define GPIO_EXT_PORTB 0x54 //B 组端口输入寄存器
#define GPIO_EXT_PORTC 0x58 //C 组端口输入寄存器
#define GPIO_EXT_PORTD 0x5c //D 组端口输入寄存器

#define DWAPB_DRIVER_NAME "gpio-dwapb"
#define DWAPB_MAX_PORTS 4

// 步长。可以看到如它所言,确实GPIO_EXT_PORT每个差4,GPIO_SWPORTA_DR和GPIO_SWPORTA_DDR每个差12
#define GPIO_EXT_PORT_STRIDE 0x04 /* register stride 32 bits */
#define GPIO_SWPORT_DR_STRIDE 0x0c /* register stride 3*32 bits */
#define GPIO_SWPORT_DDR_STRIDE 0x0c /* register stride 3*32 bits */

#define GPIO_REG_OFFSET_V2 1

#define GPIO_INTMASK_V2 0x44
#define GPIO_INTTYPE_LEVEL_V2 0x34
#define GPIO_INT_POLARITY_V2 0x38
#define GPIO_INTSTATUS_V2 0x3c
#define GPIO_PORTA_EOI_V2 0x40

#define DWAPB_NR_CLOCKS 2

#define DWAPB_GPIO_BASE 0x03020000

难道说,我有一个猜想,就是这个实际表示是说GPIO Port A/C的第16/17、9/10位吗。。。。

image-20231027203332238

所以现在很需要这个pin和port的转换。。。

一个是内核传过来的pin是什么数,还有它上面那个是不是端口和位的意思,这两点需要弄清楚

总之先向来是确认下我的想法:那个图的灰色块和引脚号是同一个东西,GPIOA17这样的字符表示GPIO的A端口寄存器的第16个bit。

如果是这样的话,那么思路就很自然了。我们可以通过图中引脚和GPIO这种的分布,从而得知当前是要写入什么端口什么寄存器,这样就能编写代码了。Good。

10.17—整亮led

也许可以搜一下schematic该怎么看,说不定就能知道哪位是引脚号了。

现在先假设GPIO C 24表示GPIO C端口的第24个bit,也即 (1<<23)吧。所以我们在读取和写入的时候,都是先读出原来的值,然后再把对应位设为1or0。但是为啥qemu是一字节一字节写。。。算了相信自己,总之先试试

然后就去研究了下linux中的gpio……发现了其内部映射。

1
2
~# cat /sys/class/gpio/gpiochip
gpiochip352/ gpiochip384/ gpiochip416/ gpiochip448/ gpiochip480/

384是GPIOD,416是C,448是B,480是A,每个间距32。而LED对应的是GPIO C 24,所以号是416+24 = 440。

416+24 = 440,也就是说,GPIOC24是port C的第24号,也即第25位。

然后慢慢整理问题……

整理一下目前的问题。

首先,我是不是想错了???

按照刚刚在Linux下看到的结果:

1
2
~$ cat /sys/class/gpio/gpiochip
gpiochip352/ gpiochip384/ gpiochip416/ gpiochip448/ gpiochip480/

五个gpio,bank name分别为porte、d、c、b、a。那么问题来了,这个port跟我们在dw_apb手册里看到的gpio的abcd四个port是一个概念吗??

已知,dwapb gpio有ABCD四个Port,因而有四组寄存器。有一点值得注意,到底是一个字节控制一个设备,还是一个bit?

那么我感觉的是,板上有5个gpio,每个gpio都各自有4个port和4K地址空间,每个port都有这样一堆寄存器:

image-20231027203540506

而GPIOC24的意思就是,它是第四个gpio?port夺少?说实话我真的很迷惑。。。

而且我那个写入为什么一点效果没有。。。好痛苦我的天

好,刚刚终于成功把led整亮了。所以是这样的,它那个GPIOABCD并不是一个GPIO的Port ABCD的意思,应该就是纯纯有多个GPIO芯片。然后GPIOC24的意思就是第三块GPIO的A组寄存器第32位,为什么是A不是别的我也不大懂,反正我看了Linux的驱动代码,好像一般也是只用A组端口。

10.18/19—中断

很好,目前已经基本完成了数据的读写这部分,接下来要做的就是中断了,中断还是先看看数据手册在做。中断的话,决定先研究下wiringX的那个example,然后来看看中断怎么进行有效性验证。

很好,把测试程序搬了过来,还有学习了下qemu的架构也搬过来了,现在剩下的任务就是看寄存器了

目前是这样,dwapb_pin_attach_irqdwapb_pin_detach_irq负责注册回调函数,dwapb_pin_irq_enable负责开关中断。然后,我们通过rt_hw_interrupt_install(DWAPB_GPIOE_IRQNUM, rt_hw_gpio_isr, &gpio_idx, "gpio");注册中断函数,这样一来每次DWAPB_GPIOE_IRQNUM中断时就会触发该函数,从而执行rt_hw_gpio_isr中遍历所有hdr并执行一遍的流程了。

整理一下它目前需要有哪些寄存器。首先是rt_hw_gpio_isr,需要有一个pending功能;然后是dwapb_pin_attach_irq,需要能设置是电平/边缘触发以及触发的极性;最后是dwapb_pin_irq_enable,需要有个能开关中断的寄存器。

目前是这样,理论上,我通过rt_pin_irq_enable向INTEN写入数据,这b对应的GPIO引脚就会产生一个中断,然后将中断传递给CPU,CPU通过其中断号从而调用中断处理函数,然后就吊我们在it_install中安装的那个回调。然而不知怎的没调。我觉得原因可能两个,一个寄存器看错了,另一个安装没装好。明天对照Linux源码看下吧。

应该不是,还是得靠外界输入才能触发中断。明天问问老师怎么做。

新进展,发现C9C10互连,然后C10间隔输出就行23333这样就可以触发中断,perfect。

不过我目前的问题似乎是,触发了中断之后,不知道为什么没有进入pending处理,导致中断一直未处理从而寄。

牛逼,成了!!!明天改下码风,多写几个测试,应该就结了。好像目前是both有点问题。明天把这个修下。

image-20231027203706048

BOTH_EDGE有点问题,好像那啥玩意没效果,依然保持着上次设置的lowedge。现在去学习下linux的both edge怎么写的。

很好,bug已除。

代码框架

GPIO实际上就是一个单纯的在板子和外设之间搬运字节的东西,所以本质上只需做好寄存器配置,以及引脚映射即可。接下来将从几个方面拆解我的代码。

整体架构

在rtt中,提供了一系列回调函数用于实现gpio:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct rt_pin_ops
{
// 设置gpio的模式(读/写)
void (*pin_mode)(struct rt_device *device, rt_base_t pin, rt_uint8_t mode);
// 读写gpio
void (*pin_write)(struct rt_device *device, rt_base_t pin, rt_uint8_t value);
rt_int8_t (*pin_read)(struct rt_device *device, rt_base_t pin);
// rtt特有中断回调,mode可以设置为电平/边缘触发,以及触发极性
rt_err_t (*pin_attach_irq)(struct rt_device *device, rt_base_t pin,
rt_uint8_t mode, void (*hdr)(void *args), void *args);
rt_err_t (*pin_detach_irq)(struct rt_device *device, rt_base_t pin);
rt_err_t (*pin_irq_enable)(struct rt_device *device, rt_base_t pin, rt_uint8_t enabled);
// 引脚映射
rt_base_t (*pin_get)(const char *name);
};

milkv上有ABCDE五个gpio组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gpio@03020000 {
bank-name = "porta";
};

gpio@03021000 {
bank-name = "portb";
};

gpio@03022000 {
bank-name = "portc";
};

gpio@03023000 {
bank-name = "portd";
};

gpio@05021000 {
bank-name = "porte";
};

但是,在实现中,这五个gpio组件会被注册为同一个设备,而不是像uart那样有多少个就注册多少个:

1
rt_device_pin_register("gpio", &_dwapb_ops, RT_NULL);

外界交互

应用程序会通过引脚名,使用rt_pin_get获取驱动内部自定义的引脚号,然后就可以通过引脚号对其进行模式设置和读写:

1
2
3
int DUO_LED = rt_pin_get("C24");
rt_pin_mode(DUO_LED, PIN_MODE_OUTPUT);
rt_pin_write(DUO_LED, PIN_LOW);

寄存器

配置

这坨宏我是从linux里搬过来的。各个寄存器的含义可以详见data book,在此不多赘述。

访问

在rtt中,外设地址也是统一编址到内存地址空间的。由其它gpio实现可知,当开启RT_USING_LWP(轻量级进程支持)时,外设地址不再是一一映射,需要我们手动在init中调用ioremap进行映射:

1
2
3
4
5
6
7
8
9
10
#ifdef RT_USING_LWP
#define BSP_IOREMAP_GPIO_DEVICE(no) \
rt_ioremap((void *)(DWAPB_GPIOA_BASE + (no) * DWAPB_GPIO_SIZE), DWAPB_GPIO_SIZE);

dwapb_gpio_base = (rt_size_t)BSP_IOREMAP_GPIO_DEVICE(0);
BSP_IOREMAP_GPIO_DEVICE(1);
BSP_IOREMAP_GPIO_DEVICE(2);
BSP_IOREMAP_GPIO_DEVICE(3);
dwapb_gpio_base_e = (rt_size_t)rt_ioremap((void *)DWAPB_GPIOE_BASE, DWAPB_GPIO_SIZE);
#endif

然后,之后就可以在各个方法中借助这两个函数传入寄存器地址直接进行读写了:

1
2
3
4
5
6
7
8
9
10
rt_inline rt_uint32_t dwapb_read32(rt_ubase_t addr)
{
return HWREG32(addr);
}

rt_inline void dwapb_write32(rt_ubase_t addr, rt_uint32_t value)
{
HWREG32(addr) = value;
}
#define HWREG32(x) (*((volatile rt_uint32_t *)(x)))

引脚映射

摸索了半天,最终还是猜测这个引脚映射规定应该是自己决定的,于是我就从别的bsp那边把规则搬了过来:

1
2
3
4
5
6
// port && no -> pin
#define PIN_NUM(port, no) (((((port) & 0xFu) << 8) | ((no) & 0xFFu)))
// pin -> port
#define PIN_PORT(pin) ((uint8_t)(((pin) >> 8) & 0xFu))
// pin -> no
#define PIN_NO(pin) ((uint8_t)((pin) & 0xFFu))

例如,“C24”就可转化为((((('C' - 'A') & 0xFu) << 8) | ((24) & 0xFFu)))

根据此规则实现get_pin回调即可。具体板子上哪个引脚是哪个port,详见milkv的schematic book。

中断

回调执行

rtt提供了很好用的中断回调。参考别的bsp以及以前的经验,很容易知道attach_irqdetach_irq就是注册和注销回调函数。那么,回调是什么时候被调用的呢?查阅其他bsp,也可得知它这是采取了一个非常巧妙的委托:在rtt给的rt_hw_interrupt_install(DWAPB_GPIOE_IRQNUM, rt_hw_gpio_isr, RT_NULL, "gpio");注册的rt_hw_gpio_isr中做即可。这个层层外包的思想让我不禁想起Linux的调度类机制和用户态调度框架的实现原理,实在是牛逼至极。

为了记录所有回调签名,我们需要为每个gpio组件整一个数据结构:

1
2
3
4
5
6
static struct dwapb_event
{
void (*(hdr[DWAPB_GPIO_NR]))(void *args);
void *args[DWAPB_GPIO_NR];
rt_uint8_t is_both_edge[DWAPB_GPIO_NR];
} _dwapb_events[DWAPB_GPIO_PORT_NR];

rt_hw_gpio_isr中:

  1. 根据硬件寄存器判断是否发生中断
  2. 调用相应回调
  3. 清除中断位表明完成中断处理

即可。

both-edge实现

这个是抄自linux。本质逻辑就是先设个上升沿触发,然后在rt_hw_gpio_isr执行完回调后再改成反方向也即下降沿触发,以此类推。不得不说确实帅。

I2C

Wait todo…