I2C драйвер в Linux

С I2C в Linux вполне можно работать из пространства пользователя. Тем не менее это не очень удобно. Весьма удобнее и правильнее сделать модуль ядра.

Для создания драйвера в ядре Linux предусмотрены вспомогательные макросы. Если Вам не нужно ничего выполнять при инициализации модуля, можно воспользоваться макросом module_i2c_driver.

#define module_i2c_driver(__i2c_driver) \
        module_driver(__i2c_driver, i2c_add_driver, \
                        i2c_del_driver)

В качестве параметра ему передается структура i2c_driver, в которой указывается имя устройства и указатели на функции probe, remove.

Макрос module_driver в свою очередь определен так:

#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
        return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
static void __exit __driver##_exit(void) \
{ \
        __unregister(&(__driver) , ##__VA_ARGS__); \
} \
module_exit(__driver##_exit);

Как видно, в нем определены функции инициализации и деинициализации модуля ядра. Из которых вызываются функции i2c_register_driver(THIS_MODULE, driver) (она же макрос i2c_add_driver(driver)) и i2c_del_driver(struct i2c_driver *) соответственно.
Что использовать решать Вам, я воспользуюсь макросом module_i2c_driver. Как мы помним у меня в качестве подопытного часы ds1307. В Linux уже есть драйвер для них, но мы же не ищем легких путей!?

Параметром макросу передаем структуру:

static struct i2c_driver ds1307_driver = {
    .driver = {
        .name   = "blabla_ds1307",
    },
    .probe          = ds1307_probe,
    .remove         = ds1307_remove,
    .id_table       = ds1307_id,
};

Указываем имя драйвера, функцию probe и remove, таблицу идентификаторов поддерживаемых устройсв.

static const struct i2c_device_id ds1307_id[] = {
    { "blabla_ds1307", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, ds1307_id);

Имя устройства используется при поиске подходящего драйвера, второй элемент структуры — параметр, который можно потом достать из этой таблицы при необходимости.
При успешном поиске драйвера вызывается его функция probe. В ней Вы должно проинициализировать устройство или же вычитать его конфиг и дать команду начала работы с ним (запустить поток или еще чего). Мы такого делать не будем, вычитаем данные из часов и выведем их в лог.

Для работы с шиной I2C в ядре предусмотрены функции:

int i2c_master_send(const struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(const struct i2c_client *client, char *buf, int count);

Следует отметить что count должен быть менее 64К, т.к. в структуре сообщения i2c в недрах ядра длина 16 битная.
Для хранения данных я сделал такую структуру:

struct ds1307 {
    u8 sec;
    u8 min;
    u8 hour;
    u8 day;
    u8 date;
    u8 month;
    u8 year;
};

static int ds1307_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    int ret;
    const char wr_buf[1] = {0};
    struct ds1307 *buffer = kmalloc(sizeof(struct ds1307), GFP_KERNEL);
 
    printk(KERN_NOTICE "Blablai2c probing!\n" ); 

    ret = i2c_master_send(client, wr_buf, 1);
    if (ret < 0)
        return ret;
 
    ret = i2c_master_recv(client, (char *)buffer, sizeof(struct ds1307));
    if (ret < 0)
        return ret;
 
 
    print_hex_dump(KERN_DEBUG, "i2c buffer: ", DUMP_PREFIX_OFFSET, 16, 1, buffer, sizeof(struct ds1307), true);
    return 0;
}

В функции probe выделяем память для данных, которые будем читать из устройства. Конечно же выводим сообщение в лог ядра. Шлем ноль из wr_buf, чтобы установить указатель записи/чтения в часах. И читаем в выделенную при помощи kmalloc память данные по i2c шине. Если функция вернула число меньше 0 значит что-то пошло не так, в случае успеха будет возвращено количество отправленных байт.
Если считали успешно, выводим в лог при помощи функции print_hex_dump.

void print_hex_dump(const char *level, const char *prefix_str, int prefix_type,
                    int rowsize, int groupsize,
                    const void *buf, size_t len, bool ascii)

Очень удобная функция. Параметры:

  • level — уровень сообщения, аналогично printk.
  • prefix_str — строка, которая будет выведена перед буфером.
  • prefix_type — флаг вывода префикса смещения — DUMP_PREFIX_OFFSET, адреса — DUMP_PREFIX_ADDRESS, вывода без префикса — DUMP_PREFIX_NONE.
  • rowsize — количество символов выводимых в строку.
  • groupsize — вывод по 1,2,4,8 байт.
  • buf — данные для вывода.
  • len — количество байт в буфере.
  • ascii — выводить данные в виде строки ascii после дампа данных.

Теперь скормите новое устройство ядру.
Overlay для устройсва:

/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2708";

    fragment@0 {
            target = <&i2c1>;

            __overlay__ {
                    #address-cells = <1>;
                    #size-cells = <0>;
                    status = "okay";

                    blabla_ds1307@68 {
                            compatible = "blabla_ds1307";
                            reg = <0x68>;
                            status = "okay";
                    };
            };
      };
};

Получается вот так:

Исходники на GitHub

 

Похожий код:

Фото аватара
Алексей Петров

Программист, разработчик с 5 летним опытом работы. Учусь на разработчика игр на Unity и разработчика VR&AR реальности (виртуальной реальности). Основные языки программирования: C#, C++.

Оцените автора
Бла, бла код
Добавить комментарий

  1. DowJhob

    Как включить постоянную частоту на определенном выводе процессора в linux, allwinner V3S?

    Ответить