Zephyr: Make a driver for the Nunchuk Joystick Zephyr Make a driver for the Nunchuk Joystick


This post is the fifth blog post in our series of blog posts about zephyr. You can find the previous consequences below:

  1. First steps with Zephyr
  2. Understand Zephyr’s flashing example
  3. Zephyr: Implementing a device driver for a sensor
  4. Integration of ST7789H2 Display support on STM32L562E DK with Zephyr: A step-by-step instructions

In this 5th blog post we go to Implement a driver for the Nunchuk Joystick by Nintendo. The Nunchuk is a simple joystick that has the advantage of having a very simple interface based on the I²C bus.

Connect and test the Nunchuk

As shown in His short “data sheet”The Nunchuk uses a certain connector with six pens:

Zephyr: Make a driver for the Nunchuk Joystick nunchuk pins

To use them, we use This small adapter from OlimexThis simply makes it easier to connect the Nunchuk signals to every development committee. As soon as we have connected everything, we can communicate with the I²C bus with the Nunchuk.

We will first test the Nunchuk with a simple application before we write a real driver that implemented the input -API.

To communicate with the Nunchuk, we have to get that struct device of the I²C bus that we use. For this development we use the STM32M562E Discovery Kit From st. On this board we will connect the Nunchuk to the i2C1 bus, which is as described i2c1 in the device tree of this card. For another hardware platform, the I2C bus can of course be different. Therefore, check the data sheet of your card. As soon as the I2C bus number is identified, we can call up the corresponding zephyr struct device Use the following snippet of C code:


const struct device *i2c_bus = DEVICE_DT_GET(DT_NODELABEL(i2c1));

Note the struct device Here the I2C bus represents, not the nunchuk itself. In order to address the Nunchuk, we have to know its I²C address: According to the previously referred Nunchuk data sheet, the address is 0x52So we can define:


#define NUNCHUK_ADRESS 0x52

In order to communicate with the Nunchuk, and before we read the conditions of the buttons or the joystick, we have to initialize it. To do this, we have to send two initialization sequences to the Nunchuk: First, 0xf0 0x55followed by 0xfb 0x00.


uint8_t init_seq_1(2) = 0xf0, 0x55;
uint8_t init_seq_2(2) = 0xfb, 0x00;
int ret;


ret = i2c_write(i2c_bus, init_seq_1, sizeof(init_seq_1), NUNCHUK_ADRESS);
if (ret < 0) 
    printf("I2C write failed.\n");


k_msleep(1);

ret = i2c_write(i2c_bus, init_seq_2, sizeof(init_seq_2), NUNCHUK_ADRESS);
if (ret < 0) 
    printf("I2C write failed.\n");


The Nunchuk data sheet also describes how to call up the status of the Nunchuk buttons and the joystick: Send one 0x0 Byte via I2C enables us to read a data structure of 6 bytes from the nunchuk. Here is a small code neckline that does exactly this, and logs the status of the Z and C buttons as well as the X and Y coordinates of the joystick:


uint8_t value = 0;
uint8_t buffer(6);
bool z_pressed;
bool c_pressed;

while (1) 
    i2c_write(i2c_bus, &value, 1, NUNCHUK_ADRESS);

    k_msleep(10);

    i2c_read(i2c_bus, buffer, 6, NUNCHUK_ADRESS);

    z_pressed = buffer(5) & 1;
    c_pressed = buffer(5) & 2;

    printf("Z : %d\n", z_pressed);
    printf("C : %d\n", c_pressed);
    printf("X joystick: %d\n",buffer(0));
    printf("Y joystick: %d\n\n",buffer(1));

    k_msleep(100);


When you run this program, you will see the status of the keys and joystick displayed in your terminal. If you move the joystick, update the numbers while the keys are displayed when pressing 0 and 1 when you are released.


Z : 1
C : 1
X joystick: 132
Y joystick: 126

Z : 1
C : 1
X joystick: 132
Y joystick: 255

Z : 1
C : 1
X joystick: 114
Y joystick: 255

Z : 1
C : 1
X joystick: 77
Y joystick: 255

Z : 1
C : 1
X joystick: 132
Y joystick: 126

Write the driver

After we understand how to use the Nunchuk, we have to create a real Zephyr driver. In view of the type of device that is the nunchuk, we will use them Entry -subsystem made of Zephyr Report events to the users.

First we have to add the Nunchuk to describe the device tree hardware. To do this, we have to create a device tree binding (in dts/bindings/input/nintendo,nunchuk.yaml):


description: Nintendo Nunchuk joystick through I2C

compatible: "nintendo,nunchuk"

include: i2c-device.yaml

properties:
  polling-interval-ms:
    type: int
    default: 50
    description: |
      Interval between two reads, in ms. The interval must be greater than 21 ms. Default to 50 ms.

We only have one property polling-interval-msWhat is used to tell the driver how often he has to read the nunchuk registers. In fact, the Nunchuk has no interrupt signal, so the choice is the only option to call up the status of the keys and the joystick. We will discuss the reason for the minimum quantity interval of 21 ms later in this blog post.

Next we can add the Nunchuk to our device tree by picking up the following code in ours app.overlay File:


&i2c1 
    nunchuk: nunchuk@52 
        reg = <0x52>;
        compatible = "nintendo,nunchuk";
    ;
;

Before we write a code, we have to integrate our future driver into the Zephyr configuration system (based on Kconfig) and build system (based on Cmake). In the drivers/input We have to create folders first Kconfig.nunchuk:


config INPUT_NUNCHUK
    bool "Nintendo Nunchuk joystick"
    default y
    depends on DT_HAS_NINTENDO_NUNCHUK_ENABLED
    select I2C
    help
      This option enable the driver for the Nintendo Nunchuk joystick.

Then in drivers/input/Kconfig We close this again Kconfig.nunchuk:


source "drivers/input/Kconfig.nunchuk"

And in drivers/input/CMakeLists.txtWe say the Zephyr -Build system that he should build input_nunchuk.c If the CONFIG_INPUT_NUNCHUK Option is activated:


zephyr_library_sources_ifdef(CONFIG_INPUT_NUNCHUK input_nunchuk.c)

We can finally start writing our driver.

First we have to write the standard boiler plate driver code, with which Zephyr can correctly initialize our driver:


#define DT_DRV_COMPAT nintendo_nunchuk

#include <zephyr/device.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/logging/log.h>
#include <zephyr/input/input.h>
#include <zephyr/kernel.h>
#include <zephyr/timing/timing.h>

LOG_MODULE_REGISTER(input_nunchuk, CONFIG_INPUT_LOG_LEVEL);

struct nunchuk_config 
    struct i2c_dt_spec i2c_bus;
    int polling_interval_ms;
;

struct nunchuk_data 

;


static int nunchuk_init(const struct device *dev)




#define NUNCHUK_INIT(inst)                                                                         \
        static const struct nunchuk_config nunchuk_config_##inst =                                \
                .i2c_bus = I2C_DT_SPEC_INST_GET(inst),                                             \
                .polling_interval_ms = DT_INST_PROP(inst, polling_interval_ms),                    \
        ;                                                                                         \
        BUILD_ASSERT(DT_INST_PROP(inst, polling_interval_ms) > 20);                                \
                                                                                                   \
        static struct nunchuk_data nunchuk_data_##inst;                                            \
                                                                                                   \
        DEVICE_DT_INST_DEFINE(inst, &nunchuk_init, NULL, &nunchuk_data_##inst,                     \
                      &nunchuk_config_##inst, POST_KERNEL, CONFIG_INPUT_INIT_PRIORITY,             \
                      NULL);

DT_INST_FOREACH_STATUS_OKAY(NUNCHUK_INIT)

If you don’t understand this code, do not hesitate to read Our previous blog postWhich explains in detail what each of these lines does.

This line: BUILD_ASSERT(DT_INST_PROP(inst, polling_interval_ms) > 20); Make sure that the value passed in the device tree is valid.

In the nunchuk_init() Function, we will include the code that is required to initialize the nunchuk. We will also save the result of a first reading in order to compare it later with subsequent reading processes and to recognize changes. To avoid a repetition, we will create a function called nunchuk_read_registers().


#define NUNCHUK_DELAY_MS     10
#define NUNCHUK_READ_SIZE 6

struct nunchuk_data 
    uint8_t joystick_x;
    uint8_t joystick_y;
    bool button_c;
    bool button_z;
;

static int nunchuk_read_registers(const struct device *dev, uint8_t *buffer)

    const struct nunchuk_config *cfg = dev->config;
    int ret;
    uint8_t value = 0;

    ret = i2c_write_dt(&cfg->i2c_bus, &value, sizeof(value));
    if (ret < 0) 
        return ret;
    

    k_msleep(NUNCHUK_DELAY_MS);
    ret = i2c_read_dt(&cfg->i2c_bus, buffer, NUNCHUK_READ_SIZE);
    if (ret < 0) 
        return ret;
    

    return 0;


static int nunchuk_init(const struct device *dev)

    const struct nunchuk_config *cfg = dev->config;
    struct nunchuk_data *data = dev->data;
    int ret;

    uint8_t init_seq_1(2) = 0xf0, 0x55;
    uint8_t init_seq_2(2) = 0xfb, 0x00;
    uint8_t buffer(NUNCHUK_READ_SIZE);

    if (!i2c_is_ready_dt(&cfg->i2c_bus)) 
        LOG_ERR("Bus device is not ready");
        return -ENODEV;
    

    /* Send the unencrypted init sequence */
    ret = i2c_write_dt(&cfg->i2c_bus, init_seq_1, sizeof(init_seq_1));
    if (ret < 0) 
        LOG_ERR("I2C write failed (%d).", ret);
        return ret;
    

    k_msleep(1);
    ret = i2c_write_dt(&cfg->i2c_bus, init_seq_2, sizeof(init_seq_2));
    if (ret < 0) 
        return ret;
    

    k_msleep(1);
    ret = nunchuk_read_registers(dev, buffer);
    if (ret < 0) 
        return ret;
    

    /* Sometimes, the first read gives unexpected results, so we make another one. */
    k_msleep(1);
    ret = nunchuk_read_registers(dev, buffer);
    if (ret < 0) 
        return ret;
    

    data->joystick_x = buffer(0);
    data->joystick_y = buffer(1);
    data->button_z = buffer(5) & BIT(0);
    data->button_c = buffer(5) & BIT(1);


This code carries out the same steps as the applications that we have created beforehand, but it treats errors and easily uses different functions for the I²C access to the advantages of using the struct i2c_dt_spec What we created in the NUNCHUK_INIT Macro with the device tree.

Use of workers

Now we have to set up the surveys. We will use a Zephyr mechanism that is mentioned Queues. Our goal is to regularly call up a function (which we will call nunchuk_poll()) everything polling_interval_ms Milliseconds.

We will use A struct k_work_delayableWhat we will add to our struct nunchuk_data:


struct nunchuk_data 
    ...
    struct k_work_delayable work;


We will now explain our handler. The prototype is void nunchuk_poll(struct k_work *work). To access our device, we have to call them up struct k_work_delayableAnd then we can use the macro CONTAINER_OF To get that struct nunchuk_data. To get ours struct deviceWe will add a pointer to this nunchuk_data.


struct nunchuk_data 
    ...
    const struct device *dev;
    struct k_work_delayable work;



static void nunchuk_poll(struct k_work *work)

    struct k_work_delayable *dwork = k_work_delayable_from_work(work);
    struct nunchuk_data *data = CONTAINER_OF(dwork, struct nunchuk_data, work);
    const struct device *dev = data->dev;
    
    ...


As soon as our handler is ready, we have to use it k_work_reschedule. Ideally, Zephyr would have a mechanism to plan a certain function in a regular descent. Unfortunately the Work -bearing queue The mechanism does not allow this, and we can only shift a function relative to the current time stamp. To approach our nunchuk_poll() Handler everyone Poll interval Milliseconds, as defined in the device tree, we are planning the next iteration at the end of nunchuk_poll()Taking into account an approach to the execution time of the function. Since it contains a delay of 10 ms and a few code, we approach 11 ms, which is why we move the next iteration Survey interval-MS-11 milliseconds later. As the Nunchuk tells us, we should wait at least 10 ms between two messages in the I2C bus, we need at least 21 milliseconds as a value of Poll interval Milliseconds. Although all this is not ideal, it works and was accepted by the Zephyr community, which is located upstream. In the nunchuk_init() Function, we will carry out and initialize this calculation and then plan our work for the first time.


struct nunchuk_data 
    ...
    const struct device *dev;
    struct k_work_delayable work;
    k_timeout_t interval_ms;


static void nunchuk_poll(struct k_work *work)

    struct k_work_delayable *dwork = k_work_delayable_from_work(work);
    struct nunchuk_data *data = CONTAINER_OF(dwork, struct nunchuk_data, work);
    const struct device *dev = data->dev;
    
    ...

    k_work_reschedule(dwork, data->interval_ms);



static int nunchuk_init(const struct device *dev)

    ...

    data->dev = dev;
    data->interval_ms = K_MSEC(cfg->polling_interval_ms - 11);

    k_work_init_delayable(&data->work, nunchuk_poll);
    ret = k_work_reschedule(&data->work, data->interval_ms);

    return ret;


The remaining tasks are now easy The input subsystem. There are several types of events supported by the input -subsystem: We will use for the Nunchuk INPUT_EV_KEY For the buttons and INPUT_EV_ABS For the joystick. There is a subtlety: synchronization. This flag must be determined when the device has reached a stable state. We can simply set it for the buttons true Every time, but for the joystick, it is a bit more complex: if both y And x Axes change, we must not be synchronized true Until both events were sent.

The final nunchuk_poll Function looks like this:


static void nunchuk_poll(struct k_work *work)

    struct k_work_delayable *dwork = k_work_delayable_from_work(work);
    struct nunchuk_data *data = CONTAINER_OF(dwork, struct nunchuk_data, work);
    const struct device *dev = data->dev;
    uint8_t buffer(NUNCHUK_READ_SIZE);
    uint8_t joystick_x, joystick_y;
    bool button_c, button_z;
    bool y_changed;
    bool sync_flag;
    int ret;

    nunchuk_read_registers(dev, buffer);

    joystick_x = buffer(0);
    joystick_y = buffer(1);
    y_changed = (joystick_y != data->joystick_y);

    if (joystick_x != data->joystick_x) 
        data->joystick_x = joystick_x;
        sync_flag = !y_changed;
        ret = input_report_abs(dev, INPUT_ABS_X, data->joystick_x, sync_flag, K_FOREVER);
    

    if (y_changed) 
        data->joystick_y = joystick_y;
        ret = input_report_abs(dev, INPUT_ABS_Y, data->joystick_y, true, K_FOREVER);
    

    button_z = buffer(5) & BIT(0);
    if (button_z != data->button_z) 
        data->button_z = button_z;
        ret = input_report_key(dev, INPUT_KEY_Z, !data->button_z, true, K_FOREVER);
    

    button_c = buffer(5) & BIT(1);
    if (button_c != data->button_c) 
        data->button_c = button_c;
        ret = input_report_key(dev, INPUT_KEY_C, !data->button_c, true, K_FOREVER);
    

    k_work_reschedule(dwork, data->interval_ms);


The driver should work now. To test it, we can use the sample Input committee:


*** Booting Zephyr OS build v4.0.0-2760-g5a8671e7edb6 ***
Input sample started
I: input event: dev=nunchuk@52       SYN type= 1 code= 44 value=1
I: input event: dev=nunchuk@52       SYN type= 1 code= 44 value=0
I: input event: dev=nunchuk@52       SYN type= 1 code= 46 value=1
I: input event: dev=nunchuk@52       SYN type= 1 code= 46 value=0
I: input event: dev=nunchuk@52       SYN type= 3 code=  1 value=156
I: input event: dev=nunchuk@52           type= 3 code=  0 value=100
I: input event: dev=nunchuk@52       SYN type= 3 code=  1 value=221
I: input event: dev=nunchuk@52           type= 3 code=  0 value=78
I: input event: dev=nunchuk@52       SYN type= 3 code=  1 value=255

You can see the events sent by the driver on the terminal that are sent when pressing the buttons (type= 1) or move the joystick type= 3.

The code was Submitted to Zephyr And after a few reviews, it was merged. You will find the final version in Driver/input/input_nunchuk.c. It is part of the Recently published 4.1 publication. This means that you now use a nunchuk in every project you have with Zephyr!

We hope that this post has helped you to write a driver for Zephyr with the input -subsystem, and you may even have inspired you to write one yourself. Be looking forward to our next blog posts about Zephyr!



Source link