第4章:GPIO驱动开发:GPIO控制器驱动、API实现、中断处理、实际案例:LED控制
各位同学,欢迎来到第四章。前面几章我们把Zephyr的外设框架聊了个大概,今天咱们来点实在的——GPIO驱动开发。
GPIO,说白了就是芯片的“手脚”。你要控制LED、读取按键、驱动蜂鸣器,都离不开它。在Zephyr里,GPIO驱动看似简单,但里面的门道可不少。我刚开始接触Zephyr时,以为GPIO就是调几个API,结果在中断处理上栽了个大跟头。嗯,今天咱们就把这些坑一个个填上。
4.1 GPIO控制器驱动:底层是怎么工作的?
先说说GPIO控制器驱动。在Zephyr里,每个GPIO外设都对应一个驱动实例。这个实例负责管理硬件寄存器、配置引脚方向、设置上下拉等。
我个人习惯把GPIO驱动分成三层:
- 硬件抽象层:直接操作寄存器,比如写GPIOx_MODER、GPIOx_ODR
- API实现层:封装成Zephyr标准的gpio_pin_configure()、gpio_pin_set()等
- 应用层:你写的业务代码,比如点个灯、读个按键
来看一个典型的GPIO控制器驱动初始化代码:
// 以STM32为例,GPIO控制器驱动初始化
static int gpio_stm32_init(const struct device *dev)
{
struct gpio_stm32_data *data = dev->data;
const struct gpio_stm32_config *cfg = dev->config;
// 使能GPIO时钟
clock_control_on(cfg->clock_dev, cfg->clock_subsys);
// 配置中断(如果有)
if (cfg->irq_num > 0) {
irq_connect_dynamic(cfg->irq_num, cfg->irq_prio,
gpio_stm32_isr, (void *)dev, 0);
irq_enable(cfg->irq_num);
}
return 0;
}
这里有个细节:时钟使能必须在配置引脚之前。我在项目中遇到过有人把顺序搞反,结果寄存器写不进去,查了半天才发现是时钟没开。
4.2 API实现:Zephyr标准接口怎么封装?
Zephyr定义了一套统一的GPIO API,所有芯片的驱动都要实现这些接口。你想想看,这样上层应用就不用关心底层是STM32还是NXP了,多方便。
核心API就这几个:
| API函数 | 功能 | 我踩过的坑 |
|---|---|---|
| gpio_pin_configure() | 配置引脚方向、上下拉、驱动强度 | 参数flags的位域容易搞混 |
| gpio_pin_set() | 输出高/低电平 | 注意逻辑电平与硬件电平的映射 |
| gpio_pin_get() | 读取引脚电平 | 开漏输出时读到的可能不准 |
| gpio_pin_interrupt_configure() | 配置中断触发方式 | 中断使能顺序有讲究 |
来看一个实际的API实现片段:
static int gpio_stm32_pin_configure(const struct device *dev,
gpio_pin_t pin,
gpio_flags_t flags)
{
struct gpio_stm32_data *data = dev->data;
const struct gpio_stm32_config *cfg = dev->config;
uint32_t mode = 0, otype = 0, pupd = 0, speed = 0;
// 解析flags,设置寄存器
if (flags & GPIO_OUTPUT) {
mode = 0x01; // 输出模式
} else {
mode = 0x00; // 输入模式
}
// 配置上下拉
if (flags & GPIO_PULL_UP) {
pupd = 0x01;
} else if (flags & GPIO_PULL_DOWN) {
pupd = 0x02;
}
// 写寄存器
MODIFY_REG(cfg->base->MODER, 0x03 << (pin * 2), mode << (pin * 2));
MODIFY_REG(cfg->base->PUPDR, 0x03 << (pin * 2), pupd << (pin * 2));
return 0;
}
这里要注意:flags参数是Zephyr定义的位掩码,不同芯片的寄存器映射可能不同。我曾经在移植驱动时,直接拿STM32的代码改给GD32用,结果上下拉配置反了——因为两家芯片的PUPDR寄存器编码不一样。
4.3 中断处理:别让中断把你搞懵
GPIO中断,是嵌入式开发里最容易出问题的地方之一。我记得刚入行时,写一个按键中断,结果一按下去系统就死机——后来发现是没清中断标志位。
Zephyr的GPIO中断处理流程是这样的:
- 硬件检测到电平变化,触发中断
- CPU跳转到中断向量表,执行ISR
- ISR里调用回调函数(如果有注册)
- 清中断标志位,退出
来看一个完整的中断配置示例:
// 定义回调函数
void button_callback(const struct device *dev,
struct gpio_callback *cb,
uint32_t pins)
{
printk("Button pressed! Pin mask: 0x%x\n", pins);
// 这里可以做消抖处理
}
// 配置中断
void setup_button_interrupt(void)
{
const struct device *gpio_dev;
struct gpio_callback button_cb_data;
gpio_dev = DEVICE_DT_GET(DT_NODELABEL(gpio0));
if (!device_is_ready(gpio_dev)) {
return;
}
// 配置引脚为输入,上拉
gpio_pin_configure(gpio_dev, 5, GPIO_INPUT | GPIO_PULL_UP);
// 配置中断:下降沿触发
gpio_pin_interrupt_configure(gpio_dev, 5, GPIO_INT_EDGE_FALLING);
// 注册回调
gpio_init_callback(&button_cb_data, button_callback, BIT(5));
gpio_add_callback(gpio_dev, &button_cb_data);
}
这里有几个关键点:
- 中断触发方式:边沿触发还是电平触发?按键一般用下降沿或低电平
- 消抖:机械按键有抖动,硬件消抖或软件延时都行
- 回调函数里别做耗时操作:ISR里尽量只设标志位,具体处理放到线程里
4.4 实际案例:用GPIO控制LED
好了,理论说完了,咱们来点实战。控制LED是GPIO最经典的应用,没有之一。
先看硬件连接:LED正极接GPIO引脚,负极通过电阻接地。这样GPIO输出高电平时LED亮,低电平时灭。
在Zephyr里,我们通常用设备树来定义LED引脚:
// dts文件中的LED节点
/ {
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = &gpio0, 13, GPIO_ACTIVE_HIGH;
label = "LED0";
};
};
};
然后代码里这样用:
#include <zephyr.h>
#include <device.h>
#include <devicetree.h>
#include <drivers/gpio.h>
// 获取LED对应的GPIO设备
#define LED0_NODE DT_ALIAS(led0)
#define LED0_GPIO_LABEL DT_GPIO_LABEL(LED0_NODE, gpios)
#define LED0_PIN DT_GPIO_PIN(LED0_NODE, gpios)
#define LED0_FLAGS DT_GPIO_FLAGS(LED0_NODE, gpios)
void main(void)
{
const struct device *dev;
int ret;
dev = device_get_binding(LED0_GPIO_LABEL);
if (!dev) {
printk("Failed to get GPIO device\n");
return;
}
// 配置为输出
ret = gpio_pin_configure(dev, LED0_PIN,
GPIO_OUTPUT_ACTIVE | LED0_FLAGS);
if (ret < 0) {
return;
}
// 开始闪烁
while (1) {
gpio_pin_set(dev, LED0_PIN, 1);
k_sleep(K_MSEC(500));
gpio_pin_set(dev, LED0_PIN, 0);
k_sleep(K_MSEC(500));
}
}
这段代码很简单,但有几个细节值得注意:
- GPIO_OUTPUT_ACTIVE:这个宏会根据设备树里的GPIO_ACTIVE_HIGH/LOW自动处理电平极性
- device_get_binding:获取设备实例,如果返回NULL,说明设备没初始化或不存在
- k_sleep:延时用内核API,别用空循环——那会浪费CPU
4.5 进阶:用GPIO模拟时序
最后聊个进阶话题。有时候你需要用GPIO模拟一些简单的通信协议,比如单总线、PWM(如果硬件不支持)。
我做过一个项目,需要驱动一个WS2812B灯带。这个灯带对时序要求很严格,必须用GPIO精确控制高低电平的持续时间。Zephyr的GPIO API本身不提供纳秒级延时,所以得用硬件定时器或者CPU空转。
这里给个思路:
// 用GPIO模拟WS2812B时序
#define T0H 3 // 0码高电平时间,单位:CPU周期
#define T0L 9 // 0码低电平时间
#define T1H 7 // 1码高电平时间
#define T1L 5 // 1码低电平时间
static inline void ws2812_send_bit(uint8_t bit)
{
if (bit) {
gpio_pin_set(dev, PIN, 1);
__asm__("nop"); // 延时T1H
gpio_pin_set(dev, PIN, 0);
__asm__("nop"); // 延时T1L
} else {
gpio_pin_set(dev, PIN, 1);
__asm__("nop"); // 延时T0H
gpio_pin_set(dev, PIN, 0);
__asm__("nop"); // 延时T0L
}
}
注意:这种用空指令延时的方法依赖CPU频率,换芯片就得重新调参数。我建议用硬件定时器或者DWT(数据观察点跟踪)来做精确延时,更可靠。
好了,这一章的内容就到这。GPIO驱动开发,说白了就是三板斧:配置、读写、中断。但每一板斧都有它的门道。下一章咱们聊I2C驱动,那个更复杂,但更有意思。
记住:写驱动不是写应用,你得时刻想着硬件是怎么工作的。多读芯片手册,多动手调试,慢慢就有感觉了。