Zephyr添加旋转编码器驱动

Creative Commons
本作品采用知识共享署名

本文说明如何为Zephyr添加增量型旋转编码器的驱动。

增量型旋转编码器作为输入器件广泛用于各种设备,例如汽车音响的音量调节,收音机频率调节,示波器上的旋钮。但遗憾的是在Zephyr中并没有增量型旋转编码器的驱动,本文将基于现有的Sensor API, 说明如何添加增量型旋转编码器的驱动,本文不对驱动操作硬件的实现细节进行说明。

增量型旋转编码器硬件要点

本文使用的是KY-040旋转编码器,详细信息见文末参考

  • button引脚是一个对地的开关,按压时接地
  • 旋转时A/B输出有相差的正交脉冲。
  • 旋转一圈产生固定数量的脉冲
  • 旋转时一个脉冲内旋转轴可以有多个停留位置,例如1,2,4.

驱动

驱动API选择

比较好的做法是为旋转编码器抽象新的驱动API,但新的API要进入Zephyr的主分支过程是非常漫长的,同时旋转编码器抽象API需要涵盖众多类型。因此我选用了现有的Senser API来对增类型旋转编码器的API。
增类型旋转编码器的按压就是一个简单的button,用gpio就可以处理,因此旋转编码器的驱动就只处理旋转。 编码器的旋转理解为是一个角度的传感器,正反转为转动方向,转动的距离就是角度,这里使用sensor API的SENSOR_CHAN_ROTATION来对其进行操控。

设备树绑定

设备树绑定是对旋转编码器的硬件进行抽象,一个增量式旋转编码器与旋转相关的的硬件特性有如下信息:

  • 输入引脚A/B
  • 旋转一圈产生的脉冲
  • 一个脉冲周期的稳妥数量
    创建dts/bindings/sensor/rotary-encoder.yaml内容如下
    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
    description: |
    Sensor driver for the relative-axis rotary encoder

    compatible: "rotary-encoder"

    properties:
    label:
    type: string
    required: true
    a-gpios:
    type: phandle-array
    required: true
    description: A pin for the encoder
    b-gpios:
    type: phandle-array
    required: true
    description: B pin for the encoder
    ppr:
    type: int
    description: Pulse Per Revolution
    required: false
    spp:
    type: int
    description: |
    Number of steps (stable states) per period
    1: Full-period mode (default)
    2: Half-period mode
    4: Quarter-period mode
    required: false

驱动代码

从设备树中获取硬件信息

创建管理数据变量和读取硬件信息

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
struct encoder_config {
const char *a_label;
const uint8_t a_pin;
const uint8_t a_flags;

const char *b_label;
const uint8_t b_pin;
const uint8_t b_flags;

const uint8_t ppr;
const uint8_t spp;
};

//创建管理数据和配置数据的宏
#define ENCODER_INST(n) \
struct encoder_data encoder_data_##n; \
const struct encoder_config encoder_cfg_##n = { \
.a_label = DT_INST_GPIO_LABEL(n, a_gpios), \
.a_pin = DT_INST_GPIO_PIN(n, a_gpios), \
.a_flags = DT_INST_GPIO_FLAGS(n, a_gpios), \
.b_label = DT_INST_GPIO_LABEL(n, b_gpios), \
.b_pin = DT_INST_GPIO_PIN(n, b_gpios), \
.b_flags = DT_INST_GPIO_FLAGS(n, b_gpios), \
COND_CODE_0(DT_INST_NODE_HAS_PROP(n, ppr), (1), (DT_INST_PROP(n, ppr))), \
COND_CODE_0(DT_INST_NODE_HAS_PROP(n, spp), (SPP_FULL), (DT_INST_PROP(n, spp))), \
};

//根据设备树对node进行初始化,会从设备树中读取硬件信息放在struct encoder_config变量中
DT_INST_FOREACH_STATUS_OKAY(ENCODER_INST) \

驱动初始化

在启动的POST_KERNEL阶段会调用encoder_init对驱动进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
int encoder_init(const struct device *dev)
{
// GPIO的配置
// GPIO中断安装
// 旋转编码器GPIO初始化状态读取
// 驱动初始化状态设置
// 驱动线程创建
// 使能中断
}

//注册驱动
DEVICE_AND_API_INIT(encoder_##n, DT_INST_LABEL(n), encoder_init, &encoder_data_##n, &encoder_cfg_##n, \
POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, &encoder_driver_api);

驱动流程

旋转编码器依靠脉冲出发GPIO中断,中断通知thread进行处理

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
static void encoder_a_gpio_callback(const struct device *dev, struct gpio_callback *cb,
uint32_t pins)
{
struct encoder_data *drv_data = CONTAINER_OF(cb, struct encoder_data, a_gpio_cb);

enable_int(drv_data->dev, false);
drv_data->intpin = 0b10;
//通知发生中断
k_sem_give(&drv_data->gpio_sem);

}

static void encoder_b_gpio_callback(const struct device *dev, struct gpio_callback *cb,
uint32_t pins)
{
struct encoder_data *drv_data = CONTAINER_OF(cb, struct encoder_data, b_gpio_cb);

enable_int(drv_data->dev, false);
drv_data->intpin = 0b01;
//通知发生中断
k_sem_give(&drv_data->gpio_sem);

}

static void encoder_thread(void *dev_ptr, void *p2, void *p3)
{
while (1) {
//等待中断通知
k_sem_take(&drv_data->gpio_sem, K_FOREVER);

//根据A/B GPIO level情况判断正反旋转
//更新旋转数据

//通过trigger handle通过应用层
if (drv_data->handler) {
drv_data->handler(dev, drv_data->trigger);
}

//使能中断
enable_int(dev, true);
}
}

驱动接口实现

sensor的接口有5个, 详细参考旋转编码器只用实现其中的2个既可以.
旋转编码器是主动输出型设备,无需软件触发,因此可以不必实现channel_fetch,只用实现trigger_set用于注册触发时的callback,实现channel_get用于在callback时从driver获取旋转的角度既可以。

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
static int encoder_trigger_set(const struct device *dev, const struct sensor_trigger *trig,
sensor_trigger_handler_t handler)
{
struct encoder_data *drv_data = dev->data;

enable_int(dev, false);

drv_data->trigger = trig;
drv_data->handler = handler;

enable_int(dev, true);

return 0;
}

static int encoder_channel_get(const struct device *dev, enum sensor_channel chan,
struct sensor_value *val)
{
struct encoder_data *drv_data = dev->data;
const struct encoder_config *drv_cfg = dev->config;
int32_t acc;

if (chan != SENSOR_CHAN_ROTATION) {
return -ENOTSUP;
}
acc = drv_data->pulses;

val->val1 = acc * FULL_ANGLE / (drv_cfg->ppr * drv_cfg->spp);
val->val2 = acc * FULL_ANGLE - val->val1 * (drv_cfg->ppr * drv_cfg->spp);
if (val->val2) {
val->val2 *= 1000000;
val->val2 /= (drv_cfg->ppr * drv_cfg->spp);
}

return 0;
}

static const struct sensor_driver_api encoder_driver_api = {
.trigger_set = encoder_trigger_set,
.channel_get = encoder_channel_get,
};

驱动使用

添加设备树节点

在板子的dts中添加旋转编码器的设备树节点:
gpio1.22和gpio1.23是旋转编码器连接旋转编码器的A/B引脚。旋转编码器旋转一圈有15个脉冲,每个脉冲下有2个稳定状态。

1
2
3
4
5
6
7
8
9
input_encoder: rotary_encoder {
compatible = "rotary-encoder";
status = "okay";
label = "INPUT_ENCODER";
a-gpios = <&gpio1 22 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
b-gpios = <&gpio1 23 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
ppr = <15>;
spp = <2>;
};

使用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void encoder_callback(const struct device *dev,
struct sensor_trigger *trigger)
{
struct sensor_value val;
//旋转编码器旋转发生,从驱动读出旋转过的角度
sensor_channel_get(dev, SENSOR_CHAN_ROTATION, &val);
printk("current %d.%d\r\n", val.val1, val.val2);
}

void main(void)
{
struct device *dev;

//获取旋转编码器device
dev = device_get_binding("INPUT_ENCODER");

//注册trigger callback,当旋转发生时将调用encoder_callback
sensor_trigger_set(dev, NULL, encoder_callback);
}

以上测试测序编译完后跑起来的效果

1
2
3
4
5
6
7
8
9
10
11
12
current 12.0
current 24.0
current 36.0
current 48.0
current 36.0
current 24.0
current 12.0
current 0.0
current -12.0
current -24.0
current -36.0
current -48.0

参考

https://zh.wikipedia.org/wiki/%E6%97%8B%E8%BD%89%E7%B7%A8%E7%A2%BC%E5%99%A8
https://elixir.bootlin.com/linux/latest/source/Documentation/devicetree/bindings/input/rotary-encoder.txt
https://www.epitran.it/ebayDrive/datasheet/25.pdf