[Из песочницы] Как запихнуть свой сенсор в Android OS

5cc51f5c5f144d08bd8e7d5afb2254f6.png

Как-то раз программисты сидели и писали очередной температурный сенсор и программы с кнопочками. И вдруг оказалось, что этот сенсор хочет себе один небольшой производитель телефонов в будущей модели. Так образовалась задача поддержать I2C/GPIO сенсор на уровне Android OS, так как сенсор обещает быть неотъемлимой частью самого телефона.

Будучи глубоким субподрядом, надежды на быстрый и регулярный отклик от конечного заказчика не было, решили потренироваться на кошках и засунуть нашу железяку в какое-нибудь доступное устройство с Android.

Задача выглядела не очень сложно и подразумевала найти планшет, схему к нему, припаяться куда надо, ничего не сломав, и написать некоего кода, который благотворно скажется на присутствии нашего сенсора в конечной операционной системе.

Смотрим по порядку что тут есть:

  1. Введение
  2. Как железно подключиться к реальному девайсу типа планшет
  3. Как подрубиться к дебажному UART в аудио выходе и обнаружить, что он не работает
  4. Как написать несложный драйвер ядра с I2C, GPIO, прерываниями и фоновыми задачами
  5. Как сделать абстракцию своей железки в Android middleware или использовать существующую
  6. Как дописать свой системный сервис и куда чего добавить, чтобы он включился и нашёлся
  7. Как прорваться через SEAndroid/SELinux дописав свои правила
  8. Как проверить — напишем простой апп
  9. Как это всё собрать
  10. Как понять, что в предыдущих пунктах что-то не так

Введение

Дело, как обычно, развивалось таким образом, что доказать состоятельность и выполнимость задачи нужно было как можно раньше, поэтому образовалось несколько частей работы, не все из которых будут описаны, но приведу их для целостности впечатления.

В поисках планшета выбор остановился на Nexus 7 по ряду произаческих причин: он был у знакомого и был ему не нужен, так как был достаточно сильно побит молью (моль разбила сенсорный экран и приходилось пользоваться мышью), но всё же это Nexus, а значит, по нему было больше информации и исходников на гугловских сайтах. Так как браться сразу за планшет было боязно, первым под замес попал Raspberry Pi3. Большая часть отладки произошла на нём. Далее рассказ не будет поминать Raspberry Pi 3, но в уме можно держать, что бOльшая часть программных проблем порешалась именно на нём.

Как железно подключиться к реальному девайсу типа планшет

Подключаться к такому навороченному устройству как планшет без схемы — слабоумие и отвага дело неблагодарное. Поэтому вначале была схема. Вообще говоря, электрические схемы современных телефонов и планшетов — не самая открытая вещь, но если постараться и не бояться обилия китайского языка в окне браузера, то можно что-то и найти. Мы нашли её достаточно быстро где-то здесь. Дальнейшие зарисовки с использованием схемотехники являются вырезками из этой схемы-документа.

По идее, в планшете должно быть достаточно много шин I2C и на порядки больше всяких GPIO, надо только найти нужные, припаяться и притянуться к нужному уровню. К нашему счастью, в планшетах Nexus 7 отсутствует задняя камера, которая как раз использует I2C для управления и два пина (питание и ресет). А нам и надо I2C и 2 GPIO (один для вкл/выкл спящего режима, а второй для прерывания на счёт нового измерения температуры).

Соотнесение реальных внутренностей и схемы показало, что всё не так просто, как в названии планшета. А именно, Существует минимум три версии Nexus 7:

  1. Версия 2013 года не подходит к найденной нами схеме, так как имеет в себе другие процессоры и кучу отличающихся мелких деталей
  2. Версии 2012 года .1 имеет распаянное посадочное место для задней камеры и всё в ней хорошо
  3. Версии 2012 года .2 не имеет распаянного места и припаяться туда значительно сложнее.

У нас был планшет 2012 года, где не было готового разъема, и вдобавок разбитый touch и мышь в комплекте, что порою сильно доставляло. В итоге, после некоторых плясок с бубном вокруг да около, было решено купить другой такой же с распаянным разъемом. Новых Nexus 7 давно нет, поэтому искали на «базарах», что позволило заглянуть под крышку и выбрать нужный с распаянным местом под камеру.

Номер правильной шины I2C мы нашли простым перебором с помощью простецкой программы с использованием NDK. Для этого пришлось поставить рутованный Android и chmod колдовством через adb отпустить на волю все I2C шины. В процессе перебора пришлось немного поиграть с адресами на шине, так как некоторые из них были уже зарезервированы и мы получали отлуп при попытке коммуникации. В итоге, оказалось, что на целевой шине больше никого не было.

Лирическое
Интересной деталью стало то, что не все версии Android могут быть одинаково полезны после рутования: в нашем случае, самой последней официальной версией был Android 5.1.1. После его установки и рутования всё вроде было без проблем, вот только доступа у нашей программы к папке /dev всё равно не было. Насильственное изменение прав доступа с использованием adb shell и chmod эффекта не дало. После недолгих раздумий решили откатиться до Android 4.4.4. Повторение такого же процесса рутования сразу дало программе доступ к /dev. Ещё можно отметить, что в adb shell папка /dev на версии 4.4.4 была доступна для чтения и без перехода в super user, в то время как в Android 5.1.1 нет. Вероятнее всего, причина кроется в достаточно больших изменениях по части безопасности ОС при переходе от Android 4 к Android 5 и далее (возможно, это третий пункт по ссылке).

А что GPIO?

На первой же странице нашей схемы в общем обозрении видно, что есть камера под названием «Rear camera module OV5650».

7e0b83b60c024da3a5b8e1019427ac28.png

Там же написовано, что она напрямую подключена к tegra T30L (т.е. главный SoC). Рядом есть линии I2C_CAM_… Поищем…

f97e745ea4f24c83b409efefefe0a76b.png

На странице 9 находится то, что нам нужно. Почти вся страница посвящена фронтальной и задней камерам. Там же есть упоминание, что у камеры есть два пина CAM_RST_5M и PWDN_5M, которые уходят в SoC на GPIO_PBB0 и GPIO_PBB5 соответственно. Кажется — это то, что нам надо. Только найти как туда припаяться, поэтому продолжаем искать…

92dd1def24ad4a0b9189150c455cd483.png

Ну вот и всё. На этой странице описание FFC разъёма, куда включается камера, в том числе и искомые пины. На нашем изначальном планшете разъём не распаян. Но впоследствии мы найдем другой планшет с разъемом, дабы не мучаться.

Далее след найденных пинов возобновится уже в коде платформы и про это написано в части про драйвер…

Как подрубиться к дебажному UART в аудио выходе и обнаружить, что он не работает

Когда пишешь драйвера и всякое низкоуровневое ПO под линукс (крайне) желательно видеть лог загрузки ядра/системы, так как там загружается в том числе и наш драйвер. И как только что-то идет не так, всё прекращается и почему неизвестно.

Поэтому, покурив интернеты, мы разузнали, что Nexus устройства имеют дебажный UART выведенным через аудио разъём. И работает оно типа само безо всяких программных настроек таким образом:

  • В аудиоразъеме по каналу MIC установлен компаратор, который реагирует на уровень более 3В.
  • В обычном режиме, напряжение на MIC составляет 1.8В-2.9В.
  • Как только уровень превышен, состояние передается на пин, который прерыванием говорит ядру, что на аудио разъеме теперь рулит дебаг.
  • После этого левый и правый каналы становятся RX и TX соответственно, хотя и остаются на уровне 1.8В — потребуется преобразователь.

ae26fb6e960e47a38db4a6bf634f0348.png
a925ac66b4a74b1487eaac7ac7d4f6c7.png

На радостях был сделан переходник USB-UART → Audio. Мы его воткнули, включили в консоли Ubuntu minicom, загрузили планшет и… ничего. Вообще. То есть, совсем. Дальнейшие натурные поиски показали только то, что так или иначе, debug uart не включился, так как линии левого и правого каналов не вышли на нулевой уровень напряжения RX/TX. Также пробовали множество команд из fastboot, но ничего не помогло. Единственное, что успокоило нас в конце этой затеи — только информация, что еще один человек пробовал разные Nexus`ы, и на всех, кроме точно такого же планшета UART завёлся, а на нашем — нет. Но было интересно.

Еще более фундаментельным способом было подключение дебажного интерфейса процессора, но в эту сторону не пошли, хотя схема устройства показывала такую возможность.

В итоге, нашим спасением стало предварительное использование Raspberry Pi для вместилища Android. Там дебажный порт работал, это позволило отловить все ошибки и дальше на Nexus было понятно что менять, если ядро не грузится. Статистика показала, что больше всего затяжек по времени было из-за непропаянных пинов GPIO, а также недокументированных особенностей tegra3 в плане разрешения работы с GPIO.

Кстати, для отладки интересно видеть полный лог загрузки, его можно получить c помощью adb bugreport.

Как написать несложный драйвер ядра с I2C, GPIO, прерываниями и фоновыми задачами

Итак, нужно было написать драйвер ядра, который будет рулить устройством через I2C и GPIO, а также отсвечивать в папке /dev каким-нибудь оригинальным именем, так что потом Android middleware сможет обратиться к этому файлу/драйверу и что-нибудь прочитать или записать.

Немного общих особенностей при написании драйвера:

  • Драйвера загружаются в ядро цепочкой — устройства верхнего уровня (платформа, шины) загружают другие устройства (конкретные устройства и алгоритмы работы с ними).
  • Цепочка и порядок загрузки определяются device tree или Си кодом загрузки, если device tree отключено при сборке ядра или не поддерживается ввиду старой версии ядра. Наш случай с tegra3 — второй.
  • Для того чтобы подхватить структуру i2c клиента, через которую будет идти работа с I2C коммуникацией, нужно написать функцию probe, которая будет вызвана, если будет установлено соответствие устройства, описанного в коде начальной загрузки платформы и списке зарегистрированных драйверов, добавление которого мы вызовем с помощью функции i2c_add_driver.

Но сначала о предпосылках для загрузки драйвера. т.е. о коде инициализации платформы.

Nexus 7 2012 построен на процессоре Tegra3. Ядро на нем использовано не новое (3.1.ч.ч) и без device tree. А это значит, что всё железо описано Си кодом и находится оно в /kernel/tegra/arch/arm/mach-tegra/

Файл board-grouper-pinmux.c описывает железные конфигурации всех пинов SoC, а также содержит общие функции для их инициализации в закрытой части ядра от nVidia (все функции, начинающиеся словом «tegra» являются реализованы в закрытой части ядра, которая поставляется в бинарном виде). Посмотрим, что нам нужно там поменять

board-grouper-pinmux.c
// ...

// Здесь небольшая таблица инициализации пинов
// Несмотря на то, что коммент ниже уговаривает нас не тратить время зря,
// код не выглядит нерабочим, поэтому добавим инициализацию нужных нам пинов

/* We are disabling this code for now. */
#define GPIO_INIT_PIN_MODE(_gpio, _is_input, _value)  \
   {              \
      .gpio_nr = _gpio, \
      .is_input   = _is_input,   \
      .value      = _value,   \
   }

static struct gpio_init_pin_info init_gpio_mode_grouper_common[] = {
   GPIO_INIT_PIN_MODE(TEGRA_GPIO_PDD7, false, 0),
   GPIO_INIT_PIN_MODE(TEGRA_GPIO_PCC6, false, 0),
   GPIO_INIT_PIN_MODE(TEGRA_GPIO_PR0, false, 0),

   // вот тут наши пины. Почему так - написано в таблице ниже :)
   GPIO_INIT_PIN_MODE(TEGRA_GPIO_PBB0, true, 0),
   GPIO_INIT_PIN_MODE(TEGRA_GPIO_PBB5, false, 0),
};

// 
static __initdata struct tegra_pingroup_config grouper_pinmux_common[] = {

// ...

/*
На найденном ранее расположении пинов на схеме мы узнали, что искомые пины имеют имена
GPIO_PBB0 и GPIO_PBB5. Oни здесь, в блоке для камеры, которой нет. Немного поменяем их конфиг
*/
/* CAMERA */
DEFAULT_PINMUX(CAM_MCLK,        VI_ALT2,         PULL_DOWN, NORMAL,     INPUT),
DEFAULT_PINMUX(GPIO_PCC1,       RSVD1,           NORMAL,    NORMAL,     INPUT),
// было
//DEFAULT_PINMUX(GPIO_PBB0,       RSVD1,           NORMAL,    NORMAL,     INPUT),   
// стало: пин для прерывания ставим на вход и подтягиваем вверх, так как у нас nIRQ
DEFAULT_PINMUX(GPIO_PBB0,       RSVD1,           PULL_UP,    NORMAL,     INPUT),
DEFAULT_PINMUX(GPIO_PBB3,       VGP3,            NORMAL,    NORMAL,     INPUT),
//DEFAULT_PINMUX(GPIO_PBB5,       VGP5,            NORMAL,    NORMAL,     INPUT),   // было
// стало: пин для управления питанием оставляем на выход и притягиваем вниз, чтобы сенсор был выключен по умолчанию
DEFAULT_PINMUX(GPIO_PBB5,       VGP5,            PULL_DOWN,    NORMAL,     OUTPUT),

// ...

};

// ...

// Эта функция инициализации вызывается из следующей и применяет
// таблицу поменьше, что выше в этой вырезке кода
static void __init grouper_gpio_init_configure(void)
{
   int len;
   int i;
   struct gpio_init_pin_info *pins_info;
   u32 project_info = grouper_get_project_id();

   if (project_info == GROUPER_PROJECT_NAKASI_3G) {
      len = ARRAY_SIZE(init_gpio_mode_grouper3g);
      pins_info = init_gpio_mode_grouper3g;
   } else {
      // вот это оно - проект у нас не 3g, так как в планшете этом 3G нету
      len = ARRAY_SIZE(init_gpio_mode_grouper_common);
      pins_info = init_gpio_mode_grouper_common;
   }

   for (i = 0; i < len; ++i) {
      tegra_gpio_init_configure(pins_info->gpio_nr,
         pins_info->is_input, pins_info->value);
      pins_info++;
   }
}

// Это одна из функций инициализации ядра, где наши pinmux`ы уйдут в закрытую часть
// кода nVidia
int __init grouper_pinmux_init(void)
{
   struct board_info board_info;
   u32 project_info = grouper_get_project_id();

   tegra_get_board_info(&board_info);
   BUG_ON(board_info.board_id != BOARD_E1565);
   grouper_gpio_init_configure();

   // вот тут
   tegra_pinmux_config_table(grouper_pinmux_common, ARRAY_SIZE(grouper_pinmux_common));
   tegra_drive_pinmux_config_table(grouper_drive_pinmux,
               ARRAY_SIZE(grouper_drive_pinmux));

   if (project_info == GROUPER_PROJECT_NAKASI_3G) {
      tegra_pinmux_config_table(pinmux_grouper3g,
            ARRAY_SIZE(pinmux_grouper3g));
   }

   tegra_pinmux_config_table(unused_pins_lowpower,
      ARRAY_SIZE(unused_pins_lowpower));
   grouper_pinmux_audio_init();

   return 0;
}

// ...


Файл board-grouper-sensors.c содержит регистрацию всяких разных устройств и наиболее общего уровня функции для них (например, управление питанием). Здесь же нам нужно добавить структуру для регистрации нашего устройства драйвером, который будет загружен как часть ядра. Как-то так:
board-grouper-sensors.c
// ...

// Вот эту структуру получит драйвер после загрузки и будет уже
// иметь абстрагированное от конкретного номера GPIO значение nIRQ
// По честноку, управление питанием надо бы реализовать в этом же файле 
// и передать указатели на функции, но лень, поэтому оно является 
// частью драйвера, который знает номер GPIO для PWRD
static const struct i2c_board_info tricky_sensor_board_info[] = {
   {
      I2C_BOARD_INFO("tricky",0x55),
      .irq = TEGRA_GPIO_TO_IRQ(TEGRA_GPIO_PBB0)
   },
};

// вот тут мы настраиваем 2 наших GPIO (отдельно, для порядка).
// ХЗ как тамреализована работа в закрытой части кода, поэтому
// скажем какими хотим видеть наши пины более привычным и уже
// абстрагированным через linux/gpio путём
static int grouper_tricky_init(void)
{
   // хотя и тут не обходится без магии вызова функций tegra_gpio_enable,
   // так как иначе пины работать не будут

   int ret = 0;

   ret = gpio_request(TEGRA_GPIO_PBB5, "tricky_npwd");
   if (ret < 0) {
     pr_err("Tricky: Error: cannot register GPIO_PWR_DOWN\n");
   }
   else {
      ret = gpio_direction_output(TEGRA_GPIO_PBB5, true);
      if (ret < 0) {
        pr_err("Tricky: Error: cannot set GPIO_PWR_DOWN as output\n");
      } 
      else {
         tegra_gpio_enable(TEGRA_GPIO_PBB5);
      }
   }

   ret = gpio_request(TEGRA_GPIO_PBB0, "tricky_nirq");
   if (ret < 0) {
      pr_err("Tricky: Error: cannot register GPIO_NIRQ\n");
      return ret;
   }

   ret = gpio_direction_input(TEGRA_GPIO_PBB0);
   if (ret < 0) {
      gpio_free(TEGRA_GPIO_PBB0);
      pr_err("Tricky: Error: cannot set GPIO_NIRQ as input\n");
   }
   else {
      tegra_gpio_enable(TEGRA_GPIO_PBB0);
   }

   printk("%s: Tricky OK", __FUNCTION__);

   return ret;
}

// ...

// А вот это уже функция инициализации всех сенсоров планшета,
// где мы вызываем настройку своих пинов и регистрируем устройство
int __init grouper_sensors_init(void)
{
   int err;
   grouper_camera_init();

#ifdef CONFIG_VIDEO_OV2710
   i2c_register_board_info(2, grouper_i2c2_board_info,
    ARRAY_SIZE(grouper_i2c2_board_info));
#endif
   /* Front Camera mi1040 + */
   pr_info("mi1040 i2c_register_board_info");
   i2c_register_board_info(2, front_sensor_i2c2_board_info,
    ARRAY_SIZE(front_sensor_i2c2_board_info));

   err = grouper_nct1008_init();
   if (err)
      printk("[Error] Thermal: Configure GPIO_PCC2 as an irq fail!");
   i2c_register_board_info(4, grouper_i2c4_nct1008_board_info,
      ARRAY_SIZE(grouper_i2c4_nct1008_board_info));

   mpuirq_init();

   i2c_register_board_info(2, cardhu_i2c1_board_info_al3010,
      ARRAY_SIZE(cardhu_i2c1_board_info_al3010));

    if (GROUPER_PROJECT_BACH == grouper_get_project_id()) {
        i2c_register_board_info(2, cap1106_i2c1_board_info,
                ARRAY_SIZE(cap1106_i2c1_board_info));
    }

   // вот тут наша инициализация
   grouper_tricky_init();

   i2c_register_board_info(2/*это номер I2C шины, на которой сидит сенсор*/, tricky_sensor_board_info,
      ARRAY_SIZE(tricky_sensor_board_info));

   return 0;
}

// TBD (показать регистрацию устройства)


Код части драйвера с комментариями
#include            // Macros used to mark up functions e.g. __init __exit
#include          // Core header for loading LKMs into the kernel
#include          // Header to support the kernel Driver Model
#include          // Contains types, macros, functions for the kernel
#include              // Header for the Linux file system support
#include             // main sensor communication protocol
#include            // sensor`s wake/sleep and new data interrupts are processed via two pins
#include       // Support GPIO IRQ handler
#include           // copy_to_user and copy_from_user functions
#include                // Access to memset()
#include       // Make IRQ event into deferred handler task
#include           // Sync data buffer usage between IRQ-work and outer read requests
#include           // Access to mdelay

// Устройство будет доступно в ядре как /dev/tricky_temperature
#define  DEVICE_NAME "tricky_temperature"
// Имя символьного устройства
#define  CLASS_NAME  "tricky"

// ... всякие разные дефайны ...

// описание модуля
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Pavel Akimov");
MODULE_DESCRIPTION("Test Linux driver for tricky sensor");  ///< Описание доступно через команду modinfo
MODULE_VERSION("0.1");

// номер устройства и указатели на структуры, необходимые для регистрации драйвера
static int    majorNumber;
static struct class*  trickyClass  = NULL;
static struct device* trickyDevice = NULL;

// ... всякие разные конфиги и команды для сенсора ...

// массив для последних считанных данных 
static u8 sensor_data_buffer[I2C_DATA_SIZE] = { 0 };

// Через указатель на I2C клиент осуществляется доступ к коммуникации
struct i2c_client *tricky_i2c_client = NULL;

// Объявление файлового интерфейса драйвера
static int dev_open(struct inode *, struct file *);
static ssize_t dev_read(struct file *, char *, size_t, loff_t *);
static ssize_t dev_ioctl(struct file *file, unsigned int ioctl_num, unsigned long ioctl_param);

// Объявление I2C интерфейса для подключения драйвера
static int tricky_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id);
static int tricky_i2c_remove(struct i2c_client *i2c_client);

// Установка питания через пин
static int set_sensor_power(u8 enabled);
// Чтение данных через I2C
static int read_raw_temperatures(void);
// Обработчик прерывания о поступлении новых данных
static irq_handler_t tricky_data_irq_handler(unsigned int irq, void *dev_id, struct pt_regs *regs);
// Поточная функция для выполнения задачи при возникновении прерывания
static void read_data_work_handler(struct work_struct *w);

// Работа с фоновым потоком для чтения данных в ядре
static struct workqueue_struct *wq = NULL;
static DECLARE_DELAYED_WORK(read_data_work, read_data_work_handler);
static struct mutex read_data_mutex;

// файловый интерфейс драйвера
static struct file_operations fops =
{
   .open = dev_open,
   .read = dev_read,
   .unlocked_ioctl = dev_ioctl
};

// таблица устройств 
static const struct i2c_device_id tricky_i2c_id[] = {
   { CLASS_NAME, 0 },
   { }, // должна заканчиваться пустой записью, по которой ядро определит конец текущей таблицы
};
MODULE_DEVICE_TABLE(i2c, tricky_i2c_id);

// описание драйвера, который будет добавлен при инициализации модуля
static struct i2c_driver tricky_i2c_driver = {
   .driver = {
      .owner = THIS_MODULE,
      .name = CLASS_NAME,
   },
   
   .id_table = tricky_i2c_id,
   .probe = tricky_i2c_probe,
   .remove = tricky_i2c_remove
};

// после добавления i2c драйвера здесь мы получим указатель на i2c клиент, если мы зарегистрированы в списке железа
static int tricky_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
   tricky_i2c_client = client;
   return 0;
}

// 
static int tricky_i2c_remove(struct i2c_client *i2c_client)
{
   if (tricky_i2c_client != NULL) {
      i2c_unregister_device(tricky_i2c_client);
      tricky_i2c_client = NULL;
   }

   return 0;
}

// инициализации драйвера вызывается при загрузке ядра
static int __init tricky_temperature_init(void) {
   int err;

   // Try to dynamically allocate a major number for the device -- more difficult but worth it
   majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
   if (majorNumber<0){
      pr_err(KERN_ALERT "Tricky failed to register a major number\n");
      return majorNumber;
   }
   printk(KERN_INFO "Tricky: registered correctly with major number %d\n", majorNumber);
 
   // Register the device class
   trickyClass = class_create(THIS_MODULE, CLASS_NAME);
   if (IS_ERR(trickyClass)){                // Check for error and clean up if there is
      pr_err(KERN_ALERT "Failed to register device class\n");
      err = PTR_ERR(trickyClass);          // Correct way to return an error on a pointer
      goto err_char_dev;
   }
   printk(KERN_INFO "Tricky: device class registered correctly\n");
 
   // Register the device driver
   trickyDevice = device_create(trickyClass, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
   if (IS_ERR(trickyDevice)){               // Clean up if there is an error
      pr_err(KERN_ALERT "Failed to create the device\n");
      err = PTR_ERR(trickyDevice);
      goto err_class;
   }
   printk(KERN_INFO "Tricky: device class created correctly\n"); // Made it! device was initialized

   // добавляем новый I2С драйвер, после чего мы найдем устройство в списке 
   // зарегистрированных устройств (тот, что в board grouper sensors) и вызовем probe
   err = i2c_add_driver(&tricky_i2c_driver);
   if (err < 0) {
      pr_err("Tricky: Error: %s: driver registration failed, error=%d\n", __func__, err);
      goto err_dev;
   }

   // Всячески настраиваем сенсор по I2C ...

   // запрашиваем привязку IRQ к нашему callback`у с уникальной строковой меткой
   // отмечаем, что реакция должна быть на падающий фронт сигнала
   err = request_irq(
      i2c_client->irq,
      (irq_handler_t)tricky_data_irq_handler,
      IRQF_TRIGGER_FALLING,
      "tricky_gpio_handler",
      NULL); // no shared interrupt lines
   if (err < 0) {
      pr_err("Tricky: Error: %s: cannot register GPIO_NIRQ irq handler: Error=%d\n", __func__, err);
      goto err_drv;
   }

   // настраиваем очередь для чтения данных в фоновом потоке по приходу IRQ
   wq = create_singlethread_workqueue("tricky_work");
   mutex_init(&read_data_mutex);

   printk(KERN_INFO "Tricky: initialization completed\n");
   return 0;

err_irq:
   destroy_workqueue(wq);
   free_irq(i2c_client->irq, NULL);
err_drv:
   i2c_del_driver(&tricky_i2c_driver);
err_dev:
   device_destroy(trickyClass, MKDEV(majorNumber, 0));     // remove the device
   class_unregister(trickyClass);                          // unregister the device class
err_class:
   class_destroy(trickyClass);                             // remove the device class
err_char_dev:
   unregister_chrdev(majorNumber, DEVICE_NAME);             // unregister the major number

   return err;
}

// выгрузка при выключении
static void __exit tricky_temperature_exit(void) {

   if (delayed_work_pending(&read_data_work) != 0)
      cancel_delayed_work_sync(&read_data_work);
   destroy_workqueue(wq);

   free_irq(i2c_client->irq, NULL);
   i2c_del_driver(&tricky_i2c_driver);

   if (tricky_i2c_client != NULL) {
      i2c_unregister_device(tricky_i2c_client);
      tricky_i2c_client = NULL;
   }

   device_destroy(trickyClass, MKDEV(majorNumber, 0));
   class_unregister(trickyClass);
   class_destroy(trickyClass);
   unregister_chrdev(majorNumber, DEVICE_NAME);

   printk(KERN_INFO "Tricky: Goodbye\n");
}

static int dev_open(
    struct inode *node, 
    struct file *filep) {
   
   printk(KERN_INFO "Tricky: Open the LKM!\n");
   return 0;
}

static ssize_t dev_read(
    struct file *filep, 
    char *buffer, 
    size_t len, 
    loff_t *offset) {

   int ret;

   // да, уровень выше (HAL) знает, сколько ему можно читать байт
   if (!buffer || len != I2C_DATA_SIZE) {
      return -EINVAL;
   }

   mutex_lock(&read_data_mutex);
   ret = copy_to_user(buffer, sensor_data_buffer, I2C_DATA_SIZE);
   mutex_unlock(&read_data_mutex);

   if (ret != 0) {
      return -ENOMEM;
   }

   return 0;
}

static ssize_t dev_ioctl(
    struct file *file,
    unsigned int ioctl_num,
    unsigned long ioctl_param) {

  
    switch (ioctl_num) {
        case IOCTL_POWER:
            ret = set_sensor_power(ioctl_param != CMD_POWER_WAKEUP ? 1 : 0);
            if (ret < 0) {
                return ret;
            }
            break;

        case ... // more commands
        
        default:
            pr_err(KERN_INFO "Tricky: invalid command type to apply\n");
            return -EINVAL;
    }

    return 0;
}

static int set_sensor_power(u8 enabled) {
   gpio_set_value(GPIO_PWR_DOWN, enabled != 0);
   return 0;
}

// при чтении I2C используем два сообщения: записываем адрес (2 байта) 
// и считываем данные по этому адресу
static int read_raw_temperatures(void) {
   int ret;
   struct i2c_msg write_message;
   struct i2c_msg read_message;

   write_message.addr = I2C_SLAVE_ADDRESS;
   write_message.flags = 0; // plain write
   write_message.buf = (char*)i2c_read_temperatures_address;
   write_message.len = sizeof(i2c_read_temperatures_address);

   memset(sensor_data_buffer, 0, sizeof(sensor_data_buffer));
   read_message.addr = I2C_SLAVE_ADDRESS;
   read_message.flags = I2C_M_RD; // plain read
   read_message.buf = (char*)sensor_data_buffer;
   read_message.len = sizeof(sensor_data_buffer);

   // read out data
   ret = i2c_transfer(tricky_i2c_client->adapter, &write_message, 1);
   if (ret < 0) {
      pr_err(KERN_INFO "Tricky: Cannot write data address. Error=%d\n", ret);
      return ret;
   }

   ret = i2c_transfer(tricky_i2c_client->adapter, &read_message, 1);
   if (ret < 0) {
      pr_err(KERN_INFO "Tricky: Cannot read data from the sensor. Error=%d\n", ret);
      return ret;
   }

   return 0;
}

// в обработчике прерывания крайне нехорошо делать что-либо длительное
// не говоря уж о том, что запрос I2C там не пройдёт вообще, так как
// I2C сам использует прерывания в своей работе
// Поэтому по получении сигнала, что данные готовы, добавляем задачу в очередь,
// которая выполняется в фоновом потоке
static irq_handler_t tricky_data_irq_handler(unsigned int irq, void *dev_id, struct pt_regs *regs) {
   // заодно проверяем, что предыдущая задача завершилась
   if (delayed_work_pending(&read_data_work) == 0)
      queue_delayed_work(wq, &read_data_work, msecs_to_jiffies(1));

   return (irq_handler_t)IRQ_HANDLED;
}

// читаем данные в массив с использованием блокировки,
// так как при файловых запросах данные будут копироваться
// из этого же массива (в другом потоке, конечно)
static void read_data_work_handler(struct work_struct *w) {
   int ret;

   mutex_lock(&read_data_mutex);
   ret = read_raw_temperatures();
   mutex_unlock(&read_data_mutex);

   if (ret < 0) {
      printk(KERN_INFO "Tricky: read_data_work_handler. Ret = %d\n", ret);
   }
}
 
// используем системные макросы, чтобы указать точки загрузки и выгрузки драйвера
module_init(tricky_temperature_init);
module_exit(tricky_temperature_exit);


Отдельно надо упомянуть файлы для сборки: KConfig и Makefile.

В KConfig допишем вот такой абзац, который по имени TRICKY_SENSOR (без префикса CONFIG_), созданному в Makefile, учтёт его при сборке. Также, наш драйвер станет виден при использовании make menuconfig.

KConfig
menuconfig THERMAL
   tristate "Generic Thermal sysfs driver"
   help
     Generic Thermal Sysfs driver offers a generic mechanism for
     thermal management. Usually it's made up of one or more thermal
     zone and cooling device.
     Each thermal zone contains its own temperature, trip points,
     cooling devices.
     All platforms with ACPI thermal support can use this driver.
     If you want this support, you should say Y or M here.

config THERMAL_HWMON
   bool
   depends on THERMAL
   depends on HWMON=y || HWMON=THERMAL
   default y

config TRICKY_SENSOR
   default y
   bool
   prompt "Tricky temperature sensor support"


Makefile
obj-$(CONFIG_THERMAL)      += thermal_sys.o
obj-$(CONFIG_TRICKY_SENSOR)  += tricky_temperature.o 


В итоге, мы получаем следующие файлы для ядра:
kernel/tegra/arch/arm/mach-tegra/board-grouper-pinmux.c
kernel/tegra/arch/arm/mach-tegra/board-grouper-sensors.c
kernel/tegra/drivers/thermal/tricky_sensor.c
kernel/tegra/drivers/thermal/KConfig
kernel/tegra/drivers/thermal/Makefile

Как сделать абстракцию своей железки в Android middleware или использовать существующую

Теперь переходим на уровень выше. Драйвер написан и теперь мы перемещается в user space часть Android, где надо как-то привязаться к драйверу.

Для того чтобы работать со многими реализациями однотипной периферии в Android есть слой middleware (написанный на С/С++), который содержит различные интерфейсы железных абстракций (Hardware Abstraction Level — HAL). И для всяких температурных магнитных и т.п. сенсоров там есть место. Но ограничением этого HAL является то, что его API подразумевает только чтение — что разумно ввиду множества пользовательских программ, которые могут одновременно доступаться к этим устройствам. И если одна поменяет настройки под себя, то для другой это будет подставой. Всё это очень хорошо описано здесь.

И конкретно в части read-only режима работы с сенсорами вот эта цитата из ссылки выше:

Besides sampling frequency and maximum reporting latency, applications cannot configure sensor parameters. For example, imagine a physical sensor that can function both in «high accuracy» and «low power» modes. Only one of those two modes can be used on an Android device, because otherwise, an application could request the high accuracy mode, and another one a low power mode; there would be no way for the framework to satisfy both applications. The framework must always be able to satisfy all its clients, so this is not an option.

There is no mechanism to send data down from the applications to the sensors or their drivers. This ensures one application cannot modify the behavior of the sensors, breaking other applications.


А нам ну очень хочется управлять своим устройством (питание переключать и режим измерения, например) и так как наш сенсор недокументирован официально и работать с ним будет только наша программа, то напишем свой HAL. Вот тут доступным английским написаны основные вещи, так чтобы дальше было понятно что за структуры данных и почему.

Создадим свой модуль железки. Для этого нужно придумать ему ID и сделать структуру, содержащую hw_device_t с описанием модуля, ну и наши производные функции. Google не специфицирует как именно должна выглядеть реализация и интерфейсы на этом уровне, поэтому без оглядки на большого брата можно начинать сеять доброе.

sensor_tricky_temperature.h
#ifndef ANDROID_TRICKY_INTERFACE_H
#define ANDROID_TRICKY_INTERFACE_H

#include 
#include 
#include 

#include 

__BEGIN_DECLS

#define TRICKY_HARDWARE_MODULE_ID "tricky"

struct tricky_device_t {
    struct hw_device_t common;

    int (*read_sample)(unsigned short *psynchro, short *pobj_temp, short *pntc1_temp, short *pntc2_temp, short *pntc3_temp);
    int (*activate)(unsigned char enabled);
    int (*set_mode)(unsigned char is_continuous);
};

__END_DECLS

#endif // ANDROID_TRICKY_INTERFACE_H


sensor_tricky_temperature.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define     LOG_TAG  "TRICKY"
#define     DEVICE_NAME "/dev/tricky_temperature"

#define     TRICKY_MODE_0  0
#define     TRICKY_MODE_1  1

int fd = 0;

int read_sample(unsigned short *psynchro, short *pobj_temp, short *pntc1_temp, short *pntc2_temp, short *pntc3_temp)
{
    int ret = 0;
    unsigned char buffer[10];
    
    ALOGD("HAL -- read_sample() called");

    ret = read(fd, (char*)buffer, sizeof(buffer));    
    if (ret < 0) {
        ALOGE("HAL -- cannot read raw temperature data");
        return -1;
    }

    if (psynchro)   *psynchro   = (unsigned short)(buffer[3] << 8 | buffer[2]);
    if (pobj_temp)  *pobj_temp  = (short)(buffer[1] << 8 | buffer[0]);
    if (pntc1_temp) *pntc1_temp = (short)(buffer[5] << 8 | buffer[4]);
    if (pntc2_temp) *pntc2_temp = (short)(buffer[7] << 8 | buffer[6]);
    if (pntc3_temp) *pntc3_temp = (short)(buffer[9] << 8 | buffer[8]);

    ALOGD("HAL - sample read OK");
    return 0;
}

int activate(unsigned char enabled)
{
    int ret = 0;
    ALOGD("HAL - activate(%d) called", enabled);

    ret = ioctl(fd, 0, enabled ? TRICKY_MODE_0 : TRICKY_MODE_1);
    if (ret < 0) {
        ALOGE("HAL - cannot write activation state");
        return -1;
    }

    ALOGD("HAL - activation state written OK");
    return 0;
}

int set_mode(unsigned char is_continuous)
{
    int ret;
    ALOGD("HAL -- set_mode(%d) called", is_continuous);

    ret = ioctl(fd, 1, is_continuous ? TRICKY_MODE_0 : TRICKY_MODE_1);
    if (ret < 0) {
        ALOGE("HAL - cannot write mode state");
        return -1;
    }

    ALOGD("HAL - mode state written OK");
    return 0;
}

static int open_tricky(const struct hw_module_t* module, char const* name, struct hw_device_t** device)
{
    int ret = 0;

    struct tricky_device_t *dev = malloc(sizeof(struct tricky_device_t));
    if (dev == NULL) {
        ALOGE("HAL - cannot allocate memory for the device");
        return -ENOMEM;
    }
    else {
        memset(dev, 0, sizeof(*dev));
    }

    ALOGD("HAL - openHAL() called");

    dev->common.tag = HARDWARE_DEVICE_TAG;
    dev->common.version = 0;
    dev->common.module = (struct hw_module_t*)module;
    dev->read_sample = read_sample;
    dev->activate = activate;
    dev->set_mode = set_mode;

    *device = (struct hw_device_t*) dev;

    fd = open(DEVICE_NAME, O_RDWR);
    if (fd <= 0) {
        ALOGE("HAL - cannot open device driver");
        return -1;
    }

    ALOGD("HAL - has been initialized");
    return 0;
}

static struct hw_module_methods_t tricky_module_methods = {
    .open = open_tricky
};

struct hw_module_t HAL_MODULE_INFO_SYM = {
    .tag = HARDWARE_MODULE_TAG,
    .version_major = 1,
    .version_minor = 0,
    .id = TRICKY_HARDWARE_MODULE_ID,
    .name = "Tricky HAL Module",
    .author = "Pavel Akimov",
    .methods = &tricky_module_methods,
};


Для сборки модуля понадобится Android.mk файл, где написано такое:
Android.mk
# устанавливаем путь для сборки
LOCAL_PATH := $(call my-dir)

# сборки модулей следуют одна за другой в объединенном .mk файле, так что
# надо очищать настройки предыдущей сборки перед началом
# Да, LOCAL_PATH не убьётся
include $(CLEAR_VARS)

LOCAL_PRELINK_MODULE := false
# это то, где ему лежать в заруженной системе
LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)/hw
# что надо для сборки
LOCAL_SHARED_LIBRARIES := liblog libcutils libhardware
# все исходники, разделенные пробелами
LOCAL_SRC_FILES := sensor_tricky_temperature.c
# имя на выходе
LOCAL_MODULE := techartmsjdts.default
LOCAL_MODULE_TAGS := debug

include $(BUILD_SHARED_LIBRARY)


И еще один Android.mk файл уровнем выше, чтобы включить собранную библиотеку в libhardware. Добавляем по имени ID модуля.
Android.mk
hardware_modules := gralloc hwcomposer audio nfc nfc-nci local_time \
   power usbaudio audio_remote_submix camera consumerir tricky
include $(call all-named-subdir-makefiles,$(hardware_modules)) 


На выходе HAL имеем следующие файлы
hardware/libhardware/include/hardware/sensor_tricky_temperature.h
hardware/libhardware/modules/Android.mk
hardware/libhardware/modules/tricky/sensor_tricky_temperature.c
hardware/libhardware/modules/tricky/Android.mk

Как дописать свой системный сервис и куда чего добавить, чтобы он включился и нашёлся

Дальше надо чтобы наш HAL кто-то вызывал. В других частях OC такие вещи делаются с помощью системных сервисов и их менеджеров, которые написаны на Java. Дабы не выбиваться из ряда, напишем еще один. Сервис наш будет учавствовать в следующих файлах:

frameworks\base\core\java\android\app\ContextImpl.java
frameworks\base\core\java\android\content\Context.java
frameworks\base\core\java\android\hardware\temperature\ITrickyService.aidl
frameworks\base\core\java\android\hardware\temperature\TrickyTemperatureData.aidl
frameworks\base\core\java\android\hardware\temperature\TrickyTemperatureData.java
frameworks\base\core\java\android\hardware\temperature\TrickyManager.java
frameworks\base\services\java\com\android\server\temperature\TrickyService.java
frameworks\base\services\java\com\android\server\SystemServer.java
frameworks\base\services\jni\Android.mk
frameworks\base\services\jni\com_android_server_temperature_TrickyService.cpp
frameworks\base\services\jni\onload.cpp
frameworks\base\Android.mk

Как видно из исходников, мы еще не разобрались с уровнем native и подключаться к HAL модулю нужно через JNI. Заодно запилим свой ссылочный тип, который надо будет определить через AIDL, а потом прокинуть из C++ в Java.
Код native части сервиса
// после переопределения LOG_TAG сообщения в логе будут тегированы как нам надо
#define LOG_TAG "TRICKY"

#include "jni.h"
#include "JNIHelp.h"
#include "android_runtime/AndroidRuntime.h"

#include 
#include 
#include 
#include 

#include 

// да, всё действие в пределах этого пространства имен, так как
// мы не абы кто, а часть Android
namespace android
{
    static jlong init_native(JNIEnv *env, jobject clazz)
    {
        int err;
        hw_module_t* module;
        tricky_device_t* dev = NULL;
        
        // найдем наш HAL
        // там внутри этой функции проверяется несколько путей, где hw модули могут
        // располагаться и должны имен структурированные имена, поэтому имя нашего
        // HAL заканчивается на ".default" - хотя не самый честный суффикс (честнее было бы
        // написать что это железо-зависимый HAL, но да ладно)
        err = hw_get_module(TRICKY_HARDWARE_MODULE_ID, (hw_module_t const**)&module);
        if (err == 0) {
            err = module->methods->open(module, "", ((hw_device_t**) &dev));
            if (err != 0) {
                ALOGE("init_native: cannot open device module: %d", err);
                return -1;
            }
        } else {
            ALOGE("init_native: cannot get device module: %d", err);
            return 0;
        }

        ALOGD("init_native: start ok");

        // этот указатель мы сохраним в Java части сервиса и будем передавать в другие методы
        return (jlong)dev;
    }

    // при выходе не забываем выключить свет
    static void finalize_native(JNIEnv *env, jobject clazz, jlong ptr)
    {
        tricky_device_t* dev = (tricky_device_t*)ptr;

        if (dev == NULL) {
            ALOGE("finalize_native: invalid device pointer");
            return;
        }

        free(dev);
        ALOGD("finalize_native: finalized ok");
    }

    // тут читаем данные из HAL
    //  и возвращаем их из C++ в нашем типе TrickyTemperatureData
    static jobject read_sample_native(JNIEnv *env, jobject clazz, jlong ptr)
    {
        tricky_device_t* dev = (tricky_device_t*)ptr;
        int ret = 0;

        unsigned short synchro = 0;
        short obj_temp = 0;
        short ntc1_temp = 0;
        short ntc2_temp = 0;
        short ntc3_temp = 0;

        if (dev == NULL) {
            ALOGE("read_sample_native: invalid device pointer");
            return (jobject)NULL;
        }

        ret = dev->read_sample(&synchro, &obj_temp, &ntc1_temp, &ntc2_temp, &ntc3_temp);
        if (ret < 0) {
            ALOGE("read_sample_native: Cannot read TrickyTemperatureData");
            return (jobject)NULL;
        }

        // ищем тип, который мы определили как
        // android.hardware.temperature.TrickyTemperatureData
        jclass c = env->FindClass("android/hardware/temperature/TrickyTemperatureData");
        if (c == 0) {
            ALOGE("read_sample_native: Find Class TrickyTemperatureData Failed");
            return (jobject)NULL;
        }

        // находим конструктор (без аргументов)
        jmethodID cnstrctr = env->GetMethodID(c, "", "()V");
        if (cnstrctr == 0) {
            ALOGE("read_sample_native: Find constructor TrickyTemperatureData Failed");
            return (jobject)NULL;
        }

        // получаем ID полей. Да, полей, долго уже пишу, нет сил на getter`ы и setter`ы
        jfieldID synchroField = env->GetFieldID(c, "synchro", "I");
        jfieldID objTempField = env->GetFieldID(c, "objectTemperature", "I");
        jfieldID ntc1TempField = env->GetFieldID(c, "ntc1Temperature", "I");
        jfieldID ntc2TempField = env->GetFieldID(c, "ntc2Temperature", "I");
        jfieldID ntc3TempField = env->GetFieldID(c, "ntc3Temperature", "I");
        if (synchroField == 0 || objTempField == 0 ||
            ntc1TempField == 0 || ntc2TempField == 0 || ntc3TempField == 0) {
            ALOGE("read_sample_native: cannot get fields of resulting object");
            return (jobject)NULL;
        }

        // создаем объект и наполняем прочитанными данными
        jobject jdtsData = env->NewObject(c, cnstrctr);

        env->SetIntField(jdtsData, synchroField, (jint)synchro);
        env->SetIntField(jdtsData, objTempField, (jint)obj_temp);
        env->SetIntField(jdtsData, ntc1TempField, (jint)ntc1_temp);
        env->SetIntField(jdtsData, ntc2TempField, (jint)ntc2_temp);
        env->SetIntField(jdtsData, ntc3TempField, (jint)ntc3_temp);

        ALOGD("read_sample_native: read ok");
        return jdtsData;
    }

    // еще немного аналогичной писанины
    
   // объявляем таблицу методов для упрощения их поиска в терминах JNI
   static JNINativeMethod method_table[] = {
        { "init_native", "()J", (void*)init_native },
        { "finalize_native", "(J)V", (void*)finalize_native },
        { "read_sample_native", "(J)Landroid/hardware/temperature/TrickyTemperatureData;", (void*)read_sample_native },
        { "activate_native", "(JZ)Z", (void*)activate_native },
        { "set_mode_native", "(JZ)Z", (void*)set_mode_native},
    };

    // И вот эта функция будет вызвана при загрузке системы из onload.cpp, 
    // который вызывается при запуске system server службы
    int register_android_server_JdtsService(JNIEnv *env)
    {
        ALOGD("register_android_server_JdtsService");

        return jniRegisterNativeMethods(
            env, 
            "com/android/server/temperature/JdtsService",
            method_table,
            NELEM(method_table));
    };
};


Далее в onload.cpp загружаются все JNI части тех сервисов, которым это надо. В том числе, и наш.
onload.cpp
// ...

#include "JNIHelp.h"
#include "jni.h"
#include "utils/Log.h"
#include "utils/misc.h"

namespace android {

// ...

int register_android_server_JdtsService(JNIEnv* env);
};

using namespace android;

extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("GetEnv failed!");
        return result;
    }
    ALOG_ASSERT(env, "Could not retrieve the env!");

    // ...

    register_android_server_JdtsService(env);

    return JNI_VERSION_1_4;
} 


Традиционный Android.mk содержит информацию для сборки всех частей, и наш JNI кусок тоже там.

Наш ссылочный тип должен быть создан при помощи AIDL, так как этот язык является средством межпроцессной пересылки данных в Android, да и не только в нем. Так же, для того чтобы его пересылать он должен быть Parcelable, что и показано в листинге дальше:

TrickyTemperatureData.aidl
package android.hardware.temperature;

parcelable TrickyTemperatureData;


TrickyTemperatureData.java
package android.hardware.temperature;

import android.os.Parcel;
import android.os.Parcelable;

/** {@hide} */
public final class TrickyTemperatureData implements Parcelable {
   public int synchro;
    public int objectTemperature;
    public int ntc1Temperature;
    public int ntc2Temperature;
    public int ntc3Temperature;

    public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
        public TrickyTemperatureData createFromParcel(Parcel in) {
            return new TrickyTemperatureData(in);
        }

        public TrickyTemperatureData[] newArray(int size) {
            return new TrickyTemperatureData[size];
        }
    };

    public TrickyTemperatureData() {
    }

    private TrickyTemperatureData(Parcel in) {
        readFromParcel(in);
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(synchro);
        out.writeInt(objectTemperature);
        out.writeInt(ntc1Temperature);
        out.writeInt(ntc2Temperature);
        out.writeInt(ntc3Temperature);
    }

    public void readFromParcel(Parcel in) {
        synchro = in.readInt();
        objectTemperature = in.readInt();
        ntc1Temperature 
    
            

© Habrahabr.ru