Как-то раз программисты сидели и писали очередной температурный сенсор и программы с кнопочками. И вдруг оказалось, что этот сенсор хочет себе один небольшой производитель телефонов в будущей модели. Так образовалась задача поддержать I2C/GPIO сенсор на уровне Android OS, так как сенсор обещает быть неотъемлимой частью самого телефона.
Будучи глубоким субподрядом, надежды на быстрый и регулярный отклик от конечного заказчика не было, решили потренироваться на кошках и засунуть нашу железяку в какое-нибудь доступное устройство с Android.
Задача выглядела не очень сложно и подразумевала найти планшет, схему к нему, припаяться куда надо, ничего не сломав, и написать некоего кода, который благотворно скажется на присутствии нашего сенсора в конечной операционной системе.
Смотрим по порядку что тут есть:
- Введение
- Как железно подключиться к реальному девайсу типа планшет
- Как подрубиться к дебажному UART в аудио выходе и обнаружить, что он не работает
- Как написать несложный драйвер ядра с I2C, GPIO, прерываниями и фоновыми задачами
- Как сделать абстракцию своей железки в Android middleware или использовать существующую
- Как дописать свой системный сервис и куда чего добавить, чтобы он включился и нашёлся
- Как прорваться через SEAndroid/SELinux дописав свои правила
- Как проверить — напишем простой апп
- Как это всё собрать
- Как понять, что в предыдущих пунктах что-то не так
Введение
Дело, как обычно, развивалось таким образом, что доказать состоятельность и выполнимость задачи нужно было как можно раньше, поэтому образовалось несколько частей работы, не все из которых будут описаны, но приведу их для целостности впечатления.
В поисках планшета выбор остановился на Nexus 7 по ряду произаческих причин: он был у знакомого и был ему не нужен, так как был достаточно сильно побит молью (моль разбила сенсорный экран и приходилось пользоваться мышью), но всё же это Nexus, а значит, по нему было больше информации и исходников на гугловских сайтах. Так как браться сразу за планшет было боязно, первым под замес попал Raspberry Pi3. Большая часть отладки произошла на нём. Далее рассказ не будет поминать Raspberry Pi 3, но в уме можно держать, что бOльшая часть программных проблем порешалась именно на нём.
Как железно подключиться к реальному девайсу типа планшет
Подключаться к такому навороченному устройству как планшет без схемы — слабоумие и отвага дело неблагодарное. Поэтому вначале была схема. Вообще говоря, электрические схемы современных телефонов и планшетов — не самая открытая вещь, но если постараться и не бояться обилия китайского языка в окне браузера, то можно что-то и найти. Мы нашли её достаточно быстро где-то здесь. Дальнейшие зарисовки с использованием схемотехники являются вырезками из этой схемы-документа.
По идее, в планшете должно быть достаточно много шин I2C и на порядки больше всяких GPIO, надо только найти нужные, припаяться и притянуться к нужному уровню. К нашему счастью, в планшетах Nexus 7 отсутствует задняя камера, которая как раз использует I2C для управления и два пина (питание и ресет). А нам и надо I2C и 2 GPIO (один для вкл/выкл спящего режима, а второй для прерывания на счёт нового измерения температуры).
Соотнесение реальных внутренностей и схемы показало, что всё не так просто, как в названии планшета. А именно, Существует минимум три версии Nexus 7:
- Версия 2013 года не подходит к найденной нами схеме, так как имеет в себе другие процессоры и кучу отличающихся мелких деталей
- Версии 2012 года .1 имеет распаянное посадочное место для задней камеры и всё в ней хорошо
- Версии 2012 года .2 не имеет распаянного места и припаяться туда значительно сложнее.
У нас был планшет 2012 года, где не было готового разъема, и вдобавок разбитый touch и мышь в комплекте, что порою сильно доставляло. В итоге, после некоторых плясок с бубном вокруг да около, было решено купить другой такой же с распаянным разъемом. Новых Nexus 7 давно нет, поэтому искали на «базарах», что позволило заглянуть под крышку и выбрать нужный с распаянным местом под камеру.
Номер правильной шины I2C мы нашли простым перебором с помощью простецкой программы с использованием NDK. Для этого пришлось поставить рутованный Android и chmod колдовством через adb отпустить на волю все I2C шины. В процессе перебора пришлось немного поиграть с адресами на шине, так как некоторые из них были уже зарезервированы и мы получали отлуп при попытке коммуникации. В итоге, оказалось, что на целевой шине больше никого не было.
А что GPIO?
На первой же странице нашей схемы в общем обозрении видно, что есть камера под названием «Rear camera module OV5650».
Там же написовано, что она напрямую подключена к tegra T30L (т.е. главный SoC). Рядом есть линии I2C_CAM_… Поищем…
На странице 9 находится то, что нам нужно. Почти вся страница посвящена фронтальной и задней камерам. Там же есть упоминание, что у камеры есть два пина CAM_RST_5M и PWDN_5M, которые уходят в SoC на GPIO_PBB0 и GPIO_PBB5 соответственно. Кажется — это то, что нам надо. Только найти как туда припаяться, поэтому продолжаем искать…
Ну вот и всё. На этой странице описание FFC разъёма, куда включается камера, в том числе и искомые пины. На нашем изначальном планшете разъём не распаян. Но впоследствии мы найдем другой планшет с разъемом, дабы не мучаться.
Далее след найденных пинов возобновится уже в коде платформы и про это написано в части про драйвер…
Как подрубиться к дебажному UART в аудио выходе и обнаружить, что он не работает
Когда пишешь драйвера и всякое низкоуровневое ПO под линукс (крайне) желательно видеть лог загрузки ядра/системы, так как там загружается в том числе и наш драйвер. И как только что-то идет не так, всё прекращается и почему неизвестно.
Поэтому, покурив интернеты, мы разузнали, что Nexus устройства имеют дебажный UART выведенным через аудио разъём. И работает оно типа само безо всяких программных настроек таким образом:
- В аудиоразъеме по каналу MIC установлен компаратор, который реагирует на уровень более 3В.
- В обычном режиме, напряжение на MIC составляет 1.8В-2.9В.
- Как только уровень превышен, состояние передается на пин, который прерыванием говорит ядру, что на аудио разъеме теперь рулит дебаг.
- После этого левый и правый каналы становятся RX и TX соответственно, хотя и остаются на уровне 1.8В — потребуется преобразователь.
На радостях был сделан переходник 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» являются реализованы в закрытой части ядра, которая поставляется в бинарном виде). Посмотрим, что нам нужно там поменять
// ...
// Здесь небольшая таблица инициализации пинов
// Несмотря на то, что коммент ниже уговаривает нас не тратить время зря,
// код не выглядит нерабочим, поэтому добавим инициализацию нужных нам пинов
/* 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 содержит регистрацию всяких разных устройств и наиболее общего уровня функции для них (например, управление питанием). Здесь же нам нужно добавить структуру для регистрации нашего устройства драйвером, который будет загружен как часть ядра. Как-то так:
// ...
// Вот эту структуру получит драйвер после загрузки и будет уже
// иметь абстрагированное от конкретного номера 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_DOWNn");
}
else {
ret = gpio_direction_output(TEGRA_GPIO_PBB5, true);
if (ret < 0) {
pr_err("Tricky: Error: cannot set GPIO_PWR_DOWN as outputn");
}
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_NIRQn");
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 inputn");
}
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 <linux/init.h> // Macros used to mark up functions e.g. __init __exit
#include <linux/module.h> // Core header for loading LKMs into the kernel
#include <linux/device.h> // Header to support the kernel Driver Model
#include <linux/kernel.h> // Contains types, macros, functions for the kernel
#include <linux/fs.h> // Header for the Linux file system support
#include <linux/i2c.h> // main sensor communication protocol
#include <linux/gpio.h> // sensor`s wake/sleep and new data interrupts are processed via two pins
#include <linux/interrupt.h> // Support GPIO IRQ handler
#include <asm/uaccess.h> // copy_to_user and copy_from_user functions
#include <asm/io.h> // Access to memset()
#include <linux/workqueue.h> // Make IRQ event into deferred handler task
#include <linux/mutex.h> // Sync data buffer usage between IRQ-work and outer read requests
#include <linux/delay.h> // 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 numbern");
return majorNumber;
}
printk(KERN_INFO "Tricky: registered correctly with major number %dn", 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 classn");
err = PTR_ERR(trickyClass); // Correct way to return an error on a pointer
goto err_char_dev;
}
printk(KERN_INFO "Tricky: device class registered correctlyn");
// 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 devicen");
err = PTR_ERR(trickyDevice);
goto err_class;
}
printk(KERN_INFO "Tricky: device class created correctlyn"); // 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=%dn", __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=%dn", __func__, err);
goto err_drv;
}
// настраиваем очередь для чтения данных в фоновом потоке по приходу IRQ
wq = create_singlethread_workqueue("tricky_work");
mutex_init(&read_data_mutex);
printk(KERN_INFO "Tricky: initialization completedn");
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: Goodbyen");
}
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 applyn");
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=%dn", 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=%dn", 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 = %dn", ret);
}
}
// используем системные макросы, чтобы указать точки загрузки и выгрузки драйвера
module_init(tricky_temperature_init);
module_exit(tricky_temperature_exit);
Отдельно надо упомянуть файлы для сборки: KConfig и Makefile.
В KConfig допишем вот такой абзац, который по имени TRICKY_SENSOR (без префикса CONFIG_), созданному в Makefile, учтёт его при сборке. Также, наш драйвер станет виден при использовании make menuconfig.
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"
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 не специфицирует как именно должна выглядеть реализация и интерфейсы на этом уровне, поэтому без оглядки на большого брата можно начинать сеять доброе.
#ifndef ANDROID_TRICKY_INTERFACE_H
#define ANDROID_TRICKY_INTERFACE_H
#include <stdint.h>
#include <sys/cdefs.h>
#include <sys/types.h>
#include <hardware/hardware.h>
__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
#include <errno.h>
#include <cutils/log.h>
#include <cutils/sockets.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/i2c.h>
#include <hardware/sensor_tricky_temperature.h>
#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 файл, где написано такое:
# устанавливаем путь для сборки
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 модуля.
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. Дабы не выбиваться из ряда, напишем еще один. Сервис наш будет учавствовать в следующих файлах:
frameworksbasecorejavaandroidappContextImpl.java
frameworksbasecorejavaandroidcontentContext.java
frameworksbasecorejavaandroidhardwaretemperatureITrickyService.aidl
frameworksbasecorejavaandroidhardwaretemperatureTrickyTemperatureData.aidl
frameworksbasecorejavaandroidhardwaretemperatureTrickyTemperatureData.java
frameworksbasecorejavaandroidhardwaretemperatureTrickyManager.java
frameworksbaseservicesjavacomandroidservertemperatureTrickyService.java
frameworksbaseservicesjavacomandroidserverSystemServer.java
frameworksbaseservicesjniAndroid.mk
frameworksbaseservicesjnicom_android_server_temperature_TrickyService.cpp
frameworksbaseservicesjnionload.cpp
frameworksbaseAndroid.mk
Как видно из исходников, мы еще не разобрались с уровнем native и подключаться к HAL модулю нужно через JNI. Заодно запилим свой ссылочный тип, который надо будет определить через AIDL, а потом прокинуть из C++ в Java.
// после переопределения LOG_TAG сообщения в логе будут тегированы как нам надо
#define LOG_TAG "TRICKY"
#include "jni.h"
#include "JNIHelp.h"
#include "android_runtime/AndroidRuntime.h"
#include <utils/misc.h>
#include <utils/Log.h>
#include <hardware/hardware.h>
#include <hardware/sensor_tricky_temperature.h>
#include <stdio.h>
// да, всё действие в пределах этого пространства имен, так как
// мы не абы кто, а часть 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, "<init>", "()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 части тех сервисов, которым это надо. В том числе, и наш.
// ...
#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, что и показано в листинге дальше:
package android.hardware.temperature;
parcelable TrickyTemperatureData;
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<TrickyTemperatureData> CREATOR = new Parcelable.Creator<TrickyTemperatureData>() {
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 = in.readInt();
ntc2Temperature = in.readInt();
ntc3Temperature = in.readInt();
}
@Override
public int describeContents() {
return 0;
}
}
Сам сервис и его менеджер крайне просты и незатейливы, не буду их тут приводить, потом в ссылках будёт всё, чтобы посмотреть или потыкать.
Теперь нужно добавить константу с именем сервиса, с помощью его сервис можно будет найти через context.getSytemService. Замечу, что в комментарии должна стоять пометка hide, иначе сборка не пройдёт, выйдя с сообщением, что такие имена нужно либо зарегистрировать официально в API, либо заныкать.
// frameworksbasecorejavaandroidcontentContext.java
/**
* @hide
*/
public static final String TRICKY_SERVICE = "android.service.tricky.ITrickyService";
Чтобы сервис заработал, его надо включить в ServiceManager через SystemServer вот тут.
// frameworksbaseservicesjavacomandroidserverSystemServer.java
// initAndLoop ...
try {
Slog.e(TAG, "Tricky Service");
trickyService = new TrickyService(context);
ServiceManager.addService(Context.TRICKY_SERVICE, trickyService);
} catch (Throwable e) {
Slog.e(TAG, "Failure starting TrickyService", e);
}
Чтобы сделать сервис доступным на стороне приложения, его менеджер надо добавить в context (в блок статической загрузки).
//frameworksbasecorejavaandroidappContextImpl.java
registerService(TRICKY_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
IBinder iBinder = ServiceManager.getService(Context.TRICKY_SERVICE);
return new TrickyManager(ITrickyService.Stub.asInterface(iBinder));
}});
Сам registerService выглядит в Android 4.4.4 так:
private static int sNextPerContextServiceCacheIndex = 0;
// т.е. просто добавляем fetcher с севисом в общий map
private static void registerService(String serviceName, ServiceFetcher fetcher) {
if (!(fetcher instanceof StaticServiceFetcher)) {
fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
}
SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
}
// Далее, всем известная getSystemService просто ищет сервис по имени
// ...
@Override
public Object getSystemService(String name) {
ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
return fetcher == null ? null : fetcher.getService(this);
}
Как прорваться через SEAndroid/SELinux дописав свои правила
Ну вот вроде весь код и написан, но только даже после успешной сборки работать ничего не станет, так как драйвер безраздельно принадлежит пользователю root, сервис не запускается, извергая в логи сообщения об отсутствии прав на запуск, а потом и на чтение запись чего-либо и, в итоге, профита нет. Необходимые права и правила надо прописать в части sepolicy.
Платформенно-зависимая часть скрипта init, который обрабатывает почти самый ранний запуск всей системы.
#(device/asus/grouper/init.grouper.rc)
# ...
on post-fs-data
# ...
# tricky temperature sensor
# разрешим чтение/запись и поставим владельцем пользователя system
# так как с root`ом иметь дело не представляется возможным
chmod 0660 /sys/class/tricky/tricky_temperature/dev
chown system system /sys/class/tricky/tricky_temperature/dev
Пропишем также права и разрешения самого файла устройства в ueventd.
# device/asus/grouper/ueventd.grouper.rc
/dev/tricky_temperature 0660 system system
А вот дальше… надо написать всяких SELinux правил, чтобы наш сервис мог быть загружен системным сервером (куда мы его уже добавили в коде), а также правила, позволяющие нашему сервису читать и писать символьное устройство, коим является наш драйвер. Я основывался, в основном, на примерах из Brillo. Не уверен, что я проникся и понял всё, но попробуем по порядку:
#####################################
# Сначала надо сделать свой домен, в пределах которого будет работать наш сервис
# и будут написаны все правила о доступе и связе кого либо с кем либо.
# Сначала опишем, что такое этот наш домен.
# (te_macros)
# tricky_service_domain(domain)
# Allow a base set of permissions common across Android daemons.
define(`tricky_service_domain', `
init_daemon_domain($1)
# Allow using binder and performing IPC to system services.
binder_use($1)
binder_service($1)
# Allow access to files in /proc.
# Fixes denials like:
# avc: denied { read } for pid=1267 comm="peripheralman" name="misc" dev="proc"
# ino=4026531967 scontext=u:r:peripheralman:s0
# tcontext=u:object_r:proc:s0 tclass=file permissive=0
allow $1 proc:file r_file_perms;
allow $1 tricky_service:service_manager find;
# Cut down on spam.
dontaudit $1 kernel:system module_request;
')
#####################################
# Далее создадим этот домен и исполняемый тип для него, так как у нас не пассивный файлик,
# а исполняемый код
# (tricky_service.te)
type tricky_service, domain;
type tricky_service_exec, exec_type, file_type;
tricky_service_domain(tricky_service)
#####################################
# Укажем, что наш сервис относится к system manager
# (service.te)
type tricky_service, service_manager_type;
# Далее уже конкретно указываем, на какое имя в системе указывает наш абстрактный тип
# "tricky_service" и его правила.
# Подробнее о синтаксисе SELinux можно почитать тут https://source.android.com/security/selinux/
#####################################
# (service_contexts)
android.service.jdtstemperature.IJdstsService u:object_r:tricky_service:s0
#####################################
# С сервисом всё. Далее надо написать правила для драйвера и что до него могут доступаться
# системные сервисы и приложения. Сначала создадим тип для драйвера
# (device.te)
type tricky_device, dev_type, mlstrustedobject;
#####################################
# Свяжем этот тип с файлом нашего драйвера
# (file_contexts)
/dev/tricky_temperature u:object_r:tricky_device:s0
#####################################
# Разрешим системе при загрузке обращаться к нашему устройству (это нужно, как я понял,
# потому что при загрузке сервисы стартуют в другом домене "bootanim" и требуют прав на
# использование файлов)
#(bootanim.te)
allow bootanim tricky_device:chr_file rw_file_perms;
#####################################
# Наконец, укажем, что доступ к нашему устройству разрешен для SystemServer и system apps
# (system_server.te)
allow system_server tricky_device:chr_file rw_file_perms;
#####################################
# (system_app.te)
allow system_app tricky_device:chr_file rw_file_perms;
Здесь мы создали новый домен для нашего сервиса, определили наше устройство и показали, что наш сервис имеет права на чтение и запись нашего драйвера. Конечно, написать это всё получилось далеко не с первого раза. После всего этого загрузка система, наконец, избавилась от сообщений о заблокированном сервисе, а в adb shell стало видно, что драйвер записан на имя пользователя system и открыт для мира.
Как проверить — напишем простой апп
Надо бы еще как-то проверить, что оно всё будет работать. Можно, конечно, и через adb shell пялиться в logcat, но не все почему-то рады такому результату, поэтому добавим в кастомную OC еще и встроенное приложение. Конечно, встроенное. Кому оно нужно, кроме этого планшета. В исходниках положим в packages/apps/TrickyDemo, а также в build/target/product/core.mk укажем его с списке предустановленных.
package com.android.trickydemo;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.Switch;
import android.widget.TextView;
import android.hardware.temperature.*;
public class MainActivity extends Activity {
private final String TAG = "TrickyDemo";
private final int POLLING_PERIOD_MS = 200;
private TrickyManager mServiceManager = null;
private TrickyTemperatureData mSensorData = null;
private GaugeView mGaugeObj;
private GaugeView mGaugeNtc1;
private GaugeView mGaugeNtc2;
private GaugeView mGaugeNtc3;
private TextView mTextSynchro;
private ImageView mIrqImage;
private TextView mTextObj;
private TextView mTextNtc1;
private TextView mTextNtc2;
private TextView mTextNtc3;
// all temperatures are .2 points precision values in degrees Celsius
final private Object mDataSync = new Object();
private boolean mMeasModeUpdateRequired;
// set up when user switches between measurement modes and queues I2C expander command
// to switch the mode
private boolean mIsContinuousMode;
// continuous mode (power is always on, no control)
// burst mode (every cycle power on, read and power off required)
private boolean mPowerState;
private boolean mPowerUpdateRequired;
private Thread mCommThread = null;
private boolean mIsRunning = true; // the communication thread goes on unless onDestroy method is called
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// enforce the sensor to switch into continuous mode on startup
mPowerUpdateRequired = true;
mPowerState = true;
mMeasModeUpdateRequired = true;
mIsContinuousMode = true;
mIrqImage = (ImageView) findViewById(R.id.image_led_irq);
mGaugeObj = (GaugeView) findViewById(R.id.gauge_view_obj);
mGaugeNtc1 = (GaugeView) findViewById(R.id.gauge_view_ntc1);
mGaugeNtc2 = (GaugeView) findViewById(R.id.gauge_view_ntc2);
mGaugeNtc3 = (GaugeView) findViewById(R.id.gauge_view_ntc3);
mTextSynchro = (TextView) findViewById(R.id.text_synchro);
mTextObj = (TextView) findViewById(R.id.text_obj);
mTextNtc1 = (TextView) findViewById(R.id.text_ntc1);
mTextNtc2 = (TextView) findViewById(R.id.text_ntc2);
mTextNtc3 = (TextView) findViewById(R.id.text_ntc3);
Switch switch_mode = (Switch) findViewById(R.id.switch1);
switch_mode.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
synchronized (mDataSync) {
mIsContinuousMode = isChecked;
mMeasModeUpdateRequired = true;
}
}
});
Switch switch_power = (Switch) findViewById(R.id.switch_power);
switch_power.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
synchronized (mDataSync) {
mPowerState = isChecked;
mPowerUpdateRequired = true;
}
}
});
switch_power.setChecked(true); // power is on by default
mServiceManager = (TrickyManager) getSystemService(TRICKY_SERVICE);
mCommThread = new Thread() {
@Override
public void run() {
while(mIsRunning) {
synchronized (mDataSync) {
if (mPowerUpdateRequired) {
if (mServiceManager.activate(mPowerState)) {
mPowerUpdateRequired = false;
} else {
Log.w(TAG, "Cannot update power state");
}
}
if (mMeasModeUpdateRequired) {
if (mServiceManager.setMode(mIsContinuousMode)) {
mMeasModeUpdateRequired = false;
} else {
Log.w(TAG, "Cannot update measurement mode");
}
}
}
mSensorData = mServiceManager.readSample();
if (mSensorData != null) {
updateUI();
} else {
updateNonIRQUI();
}
try {
Thread.sleep(POLLING_PERIOD_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
mCommThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
e.printStackTrace();
}
});
mCommThread.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
try {
mCommThread.join(POLLING_PERIOD_MS * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void updateUI() {
runOnUiThread(new Runnable() {
public void run() {
float obj_temp = mSensorData.objectTemperature / 100.F;
float ntc1_temp = mSensorData.ntc1Temperature / 100.F;
float ntc2_temp = mSensorData.ntc2Temperature / 100.F;
float ntc3_temp = mSensorData.ntc3Temperature / 100.F;
String s_obj = String.format("%.2f °C", obj_temp);
String s_ntc1 = String.format("%.2f °C", ntc1_temp);
String s_ntc2 = String.format("%.2f °C", ntc2_temp);
String s_ntc3 = String.format("%.2f °C", ntc3_temp);
String s_synchro = String.format("Synchro = %d", mSensorData.synchro);
mGaugeObj.setTargetValue(obj_temp);
mTextObj.setText(s_obj);
mGaugeNtc1.setTargetValue(ntc1_temp);
mTextNtc1.setText(s_ntc1);
mGaugeNtc2.setTargetValue(ntc2_temp);
mTextNtc2.setText(s_ntc2);
mGaugeNtc3.setTargetValue(ntc3_temp);
mTextNtc3.setText(s_ntc3);
mTextSynchro.setText(s_synchro);
mIrqImage.setImageDrawable(getResources().getDrawable(R.drawable.led_green_hi));
Log.d(TAG,
s_synchro
+ "Obj = " + s_obj
+ " NTC1 = " + s_ntc1
+ " NTC2 = " + s_ntc2
+ " NTC3 = " + s_ntc3);
}
});
}
private void updateNonIRQUI() {
runOnUiThread(new Runnable() {
public void run() {
mIrqImage.setImageDrawable(getResources().getDrawable(R.drawable.led_green_md));
}
});
}
}
Для душевной простоты приложение написано в Android Studio (не забыть правильно выставить sdk — мы собираем под 4.4.4), а далее оторвано всё лишнее. А вот для сборки снова используется Android.mk, который у меня выглядит так.
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_RESOURCE_FILES := $(addprefix $(LOCAL_PATH)/, res)
LOCAL_PACKAGE_NAME := TrickyDemo
LOCAL_CERTIFICATE := platform
LOCAL_STATIC_JAVA_LIBRARIES := android-support-core-utils-api24
include $(BUILD_PACKAGE)
Когда при сборке возникают ошибки неправильных библиотек или нехватки каких-то из них, смотрим в out/target/common/obj/SHARED_LIBRARIES и ищем подходящие имена.
Как это всё собрать
Теперь осталось только всё это собрать. Итак, параметры целевого устройства такие:
HW: Nexus 7 (2012 grouper)
OS: Android Kitkat 4.4.4 KTU84P
Kernel: tegra3_android_defconfig 3.1.10-gle42d16
Сначала собираем ядро.
Нужные исходники ядра тут:
git clone https://android.googlesource.com/kernel/tegra.git -b android-tegra3-grouper-3.1-kitkat-mr2
Скачиваем нужный инструмент для сборки ядра отсюда:
mkdir arm-eabi-4.6
cd arm-eabi-4.6
git init
git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6/
Как собрать ядро (находясь в корневой папке ядра):
ARCH=arm SUBARCH=arm CROSS_COMPILE=<path_to_arm_eabi-4.6>/arm-eabi-4.6/bin/arm-eabi- make tegra3_android_defconfig
ARCH=arm SUBARCH=arm CROSS_COMPILE=<path_to_arm_eabi-4.6>/arm-eabi-4.6/bin/arm-eabi- make menuconfig
ARCH=arm SUBARCH=arm CROSS_COMPILE=<path_to_arm_eabi-4.6>/arm-eabi-4.6/bin/arm-eabi- make -j4 zImage
После копирования кастомных изменений в make menuconfig в разделе device drivers должен быть выбранный драйвер для Tricky temperature sensor (смотрим эту информацию с помощью menuconfig — команда выше). Собирается ядро быстро. После сборки полученный образ лежит в kernel/tegra/arch/arm/boot/zImage.
Дальше Android. Для сборки Android OS из исходников надо иметь достаточно могучий компьютер, чтобы не заскучать надолго, и много места на диске (вот тут подробно). В моем случае, сборка проходила на Ubuntu 14.04 LTS x64 (сборка на Windows не поддерживается вообще, если что).
Процесс установки необходимых пакетов хорошо описан здесь, так что на нём не останавливаюсь. Единственное, надо помнить, что для сборки разных версий ОС используются разные версии Java (для Android 7 это OpenJDK Java 8, для Nexus 7 и Android 4.x это Oracle Java 6).
Для настройки окружения перед сборкой Android читаем вот это.
Для скачивания исходников из репозиториев используем Repo — нахлобучка над Git, позволяющая работать сразу с множеством git-репозиториев (подробности установки тут). После установки Repo переходим в папку с будущими исходниками и выполняем это:
repo init -u https://android.googlesource.com/platform/manifest -b android-4.4.4_r2
cd .repo
repo sync
Процесс скачивания довольно долгий, так как скачано будет около 50Гб.
Далее скачиваем дополнительные бинарники от производителя (в корень папки с исходниками Android OS) для версии 4.4.4 KTU84P в нашем Nexus`е:
https://dl.google.com/dl/android/aosp/asus-grouper-ktu84p-b12ce5f7.tgz
https://dl.google.com/dl/android/aosp/broadcom-grouper-ktu84p-646d5a68.tgz
https://dl.google.com/dl/android/aosp/elan-grouper-ktu84p-742223b3.tgz
https://dl.google.com/dl/android/aosp/invensense-grouper-ktu84p-724c855a.tgz
https://dl.google.com/dl/android/aosp/nvidia-grouper-ktu84p-e6d581dc.tgz
https://dl.google.com/dl/android/aosp/nxp-grouper-ktu84p-27abae08.tgz
https://dl.google.com/dl/android/aosp/widevine-grouper-ktu84p-57b01f77.tgz
Распаковываем и извлекаем содержимое бинарников:
tar -xvf asus-grouper-ktu84p-b12ce5f7.tgz
tar -xvf broadcom-grouper-ktu84p-646d5a68.tgz
tar -xvf elan-grouper-ktu84p-742223b3.tgz
tar -xvf invensense-grouper-ktu84p-724c855a.tgz
tar -xvf nvidia-grouper-ktu84p-e6d581dc.tgz
tar -xvf nxp-grouper-ktu84p-27abae08.tgz
tar -xvf widevine-grouper-ktu84p-57b01f77.tgz
rm *.tgz
./extract-asus-grouper.sh
./extract-broadcom-grouper.sh
./extract-elan-grouper.sh
./extract-invensense-grouper.sh
./extract-nvidia-grouper.sh
./extract-nxp-grouper.sh
./extract-widevine-grouper.sh
Ну и собираем:
mkdir nexus
cd nexus
make clobber (очистит всё что было до этого)
. build/envsetup.sh
lunch aosp_grouper-userdebug
make -j4
Удалить предыдущую сборку можно командой:
make clobber
После сборки конечные образы будут находиться в папке out/target/product/grouper (system.img, recovery.img, ramdisk.img, userdata.img). Apk-файл приложения находится в out/target/product/grouper/obj/APPS/Jdts160demo_intermediates/package.apk.
Создание и заливка образа через fastboot. Создаем .zip-архив с образами, которые нужно залить во FLASH планшета (по умолчанию это boot.img, system.img, recovery.img, userdata.img, ramdisk.img), а также файлом android-info.txt с содержанием:
require board=grouper
require version-bootloader=4.23
Если версия загрузчика по умолчанию не та, то можно скачать готовый Factory image образ отсюда и выполнить скрипт flash_all.sh из него. Его же можно использовать как базовый образ для заливки своих изменений.
Для обновления boot.img нужно установить тулзу abootimg:
sudo apt-get install abootimg
abootimg --create boot.img -k zImage -r ramdisk.img
В команде для abootimg можно еще указать параметры загрузки, в т.ч. и консоль, но она всё равно не работает, поэтому хрен на неё.
Переходим в режим fastboot. Варианты:
adb reboot bootloader (если планшет включен, подключен по usb и авторизован adb)
если выключен, то включить, удерживая кнопки питания и громкость.
Проверяем доступность fastboot (usb подключен и usb debugging включен).
fastboot devices
Далее выполняем список команд. Совсем необязательно удалять всё, можно только изменяемые части:
fastboot oem unlock
fastboot erase boot
fastboot erase cache
fastboot erase recovery
fastboot erase system
fastboot erase userdata
fastboot -2 update image.zip
Как понять, что в предыдущих пунктах что-то не так
На самом деле, в процессе выполнения задачи особо не было времени разбираться в деталях глубоко и честно и путь был пройден по камушкам, найденным на просторах сети. Уже после того как задача была закрыта начался анализ и дочитывание того что и как и где можно было сделать иначе. Получился небольшой список:
- Изменения в ОС рекомендуется делать в папке devices/asus/grouper/..., написанием множества overlays, а не напрямую в структуре ядра и ОС, которые будут учтены при сборке. Я так до конца и не выснил, можно ли там городить ту же структуру, что в основных исходниках, или есть какие-то определенные требования.
- Консольный выход через audio-jack так и не заработал. Дальше после всего была перечитана статья, которая снова заронила ядро сомнения, что что-то было сделано не так. А именно:
- На схемотехнике упоминается uart-1 & uart-4 для debug, а в таблице pinmux соответствующие GPIO были в таблице not_used/disabled.
- Google не использует usb-uart FTDI для подачи нужного уровня на канал микрофона, так как там ограничение по току 50мА, чего может быть недостаточно.
- В make menuconfig можно включить uart отладку на любой из четырех uart`ов. Мы это делали, но не в купе с остальными пунктами.
- Еще были fastboot и bootloader опции и command line аргументы для консоли. Мы, конечно, попробовали не все варианты.
- Не удалось создать своего пользователя в системе, чтобы назначить ему доступ к драйверу и сервису. Поэтому пришлось использовать system
Автор: oxodff