Пишем модуль ядра Linux: I2C

Автор: admin от 14-06-2018, 19:55, посмотрело: 29

Хабр, привет!



Данная статья посвящена разработке I2C (Inter-Integrated Circuit) модуля ядра Linux. Далее описан процесс реализация базовой структуры I2C драйвера, в которую можно легко добавить реализацию необходимого функционала.



Опишем входные данные: I2C блок для нового процессора «зашитый» на ПЛИС, запущенный Linux версии 3.18.19 и периферийные устройства (EEPROM AT24C64 и BME280).

Принцип работы I2C достаточно прост, но если нужно освежить знания, то можно почитать тут.



Пишем модуль ядра Linux: I2C
Рисунок 1. Временная диаграмма сигналов шины I2C

тут.



Шаг первый



Для начала познакомимся с утилитой i2cdetect. Результат работы i2cdetect выглядит следующим образом:

./i2cdetect -y 0
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: — — — — — — — — — — — — — 
10: — — — — — — — — — — — — — — — — 
20: — — — — — — — — — — — — — — — — 
30: — — — — — — — — — — — — — — — — 
40: — — — — — — — — — — — — — — — — 
50: 50 — — — — — — — — — — — — — — — 
60: — — — — — — — — — — — — — — — — 
70: — — — — — — — — 


Утилита последовательно выставляет на I2C шину адреса устройств и при получении положительного ответа (в данном случае положительным ответом является ACK) выводит в консоль номер адреса устройства на шине.



Напишем небольшую программку, которая считывает уникальный ID датчика температуры, и выведем результат ее работы в консоль. Выглядит очень просто:



#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>

#define I2C_ADAPTER     "/dev/i2c-0"
 
int read_buffer(int fd)
{
        struct i2c_rdwr_ioctl_data data;
        struct i2c_msg messages[2];
        unsigned char write_buf[1] = {0xD0}, read_buf[1] = {0x00};
        unsigned char write[200];

        /* 
         * .addr - Адрес устройства (датчика)
         * .flags - операция чтения или записи (0 - w, 1 - r)
         * .len - кол-во передаваемых/принимаемых сообщений
         * .buf - буфер на чтение или запись
         */
        messages[0].addr = 0x50;
        messages[0].flags = 0;
        messages[0].len = 1;
        messages[0].buf = write_buf;

        messages[1].addr = 0x50;
        messages[1].flags = 1;
        messages[1].len = 1;
        messages[1].buf = read_buf;
 
        data.msgs = messages;
        data.nmsgs = 2;
 
        if (ioctl(fd, I2C_RDWR, &data) < 0)
                printf("Cant send data!n");
        else
                printf("ID = 0x%xn", read_buf[0]);

}

int main(int argc, char **argv)
{
        int fd;
 
         /*
          * Open I2C file descriptor.
          */
         fd = open(I2C_ADAPTER, O_RDWR);
 
         if (fd < 0) {
                 printf("Unable to open i2c filen");
                 return 0;
         }
 
         read_buffer(fd);
 
         return 0;
 }


Становятся понятно что модуль ядра принимает данные в виде полей сообщения i2c_rdwr_ioctl_data. Структура содержит такие поля как i2c_msg и nmsgs, которые используется для передачи:




  • .addr — адреса устройства;

  • .flags — типа операции (чтение или запись);

  • .len — длины текущего сообщения;

  • .buf- буфера обмена.



Шаг второй



Теперь, не углубляюсь во внутренности, познакомимся с одним вариантом работы I2C драйвера.

Как уже было установлено, модуль ядра получает сообщения в виде структуры. Для примера рассмотрим алгоритм работы драйвера при выполнении операции записи (аппаратно-зависимая часть):




  • Сначала заполняется TX FIFO: первым идет адрес устройства, а после оставшиеся данные на передачу;

  • Очищается статусный регистр прерывания ISR и разрешаются прерывания в регистре IER (в данном случае прерывание возникающее при отсутствии данных в TX FIFO);

  • Разрешается передача данных и устанавливается старт бит на шине.



Весь последующей обмен данными будет происходить в обработчике прерывания.

Драйвера, которые работают по данному алгоритму, можно найти тут. Также у контроллера может не быть FIFO, а только единственный регистр на передачу, но это частный случай с размером FIFO равным одному.



Шаг третий



Добавим модуль ядра в сборку и опишем аппаратную часть устройств в device tree:



1. Создадим source файл в следующей директории:



cd drivers/i2c/busses/
vim i2c-skel.c
:wq


В результате появится файл:



drivers/i2c/busses/i2c-skel.c




2. Добавим конфигурацию драйвера в drivers/i2c/busses/Kconfig:



config I2C_SKEL
        tristate "I2C adapter"
	help
	  If you say yes to this option, support will be included for the
	  I2C interface.


3. Добавим в сборку драйвер drivers/i2c/busses/Makefile:



obj-$(CONFIG_I2C_SKEL)	+= i2c-skel.o


4. Добавим в devicetree (*.dts) описание I2C блока, а также сразу поддержу eeprom устройства:



		i2c: i2c@f8f01d00 {
			compatible = "skel,skel-i2c";
			#address-cells = <1>;
			#size-cells = <0>;
			reg = <0x43c00000 0x100>;
			interrupt-parent = <&ps7_scugic_0>;
			interrupts = <0 29 4>;
			clock-names = "skel-i2c";
			clocks = <&clkc 38>;
			clock-frequency = <100000>;

			24c64@50 {
				compatible = "at,24c64";
				pagesize = <32>;
				reg = <0x50>;
			};
		} ;


Подробно рассматриваться выше перечисленные шаги не будут, но любопытным читателям можно заглянуть сюда.



Шаг четвертый



После ознакомления с принципом работы драйвера приступим к реализации.

Сначала подключим заголовочные файлы, опишем «виртуальную» регистровую карту, а также представление драйвера I2C.



/* i2c-skel.c: I2C bus driver.
 *
 * Name Surname <name@surname.ru>
 *
 * This file is licensed under the terms of the GNU General Public License
 * version 2. This program is licensed "as is" without any warranty of any
 * kind, whether express or implied.
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/time.h>
#include <linux/delay.h>
#include <linux/device.h>

/*
 * Registers description.
 */
#define SKEL_I2C_ID	0x00				/* Core Identifier register */
#define SKEL_I2C_ISR	0x14				/* Interrupt Status Register */
#define		SKEL_I2C_ISR_DNE		BIT(0)	/* One byte transaction done */
#define		SKEL_I2C_ISR_ARB		BIT(1)	/* Arbitration lost */
#define		SKEL_I2C_ISR_TXE		BIT(2)	/* RX FIFO nearly full */
#define		SKEL_I2C_ISR_NACK		BIT(3)	/* No ACK */
#define SKEL_I2C_IER	0x18				/* Interrupt Enable Register */
#define		SKEL_I2C_IER_DNE		BIT(0)	/* Enable DNE IRQ */
#define		SKEL_I2C_IER_ARB		BIT(1)	/* Enable ARB LOSR IRQ */
#define		SKEL_I2C_IER_TXE		BIT(2)	/* Enable TX FIFO EPMTY IRQ */
#define		SKEL_I2C_IER_NACK		BIT(3)	/* Enable NACK IRQ */
#define SKEL_I2C_CTRL	0x1C				/* Control Register */
#define		SKEL_I2C_CTRL_EN		BIT(0)	/* Enable I2C controller */
#define		SKEL_I2C_CTRL_START		BIT(1)	/* Send START condition */
#define		SKEL_I2C_CTRL_R			BIT(2)	/* Read command */
#define		SKEL_I2C_CTRL_W			BIT(3)	/* Write command */
#define		SKEL_I2C_CTRL_STOP		BIT(4)	/* Send STOP cindition */
#define SKEL_I2C_TX		0x20			/* TX FIFO */
#define SKEL_I2C_RX		0x24			/* RX FIFO */
#define SKEL_I2C_CLK	0x28			/* Clock Prescale Register*/

#define SKEL_I2C_TIMEOUT		100000
#define SKEL_I2C_XFER_TIMEOUT	(msecs_to_jiffies(500))

#define FIFO_SIZE_TX	1024
#define FIFO_SIZE_RX	1024

int presc = -1;

module_param(presc, int, S_IRUGO | S_IWUSR);

/*
 * skel_i2c - I2C device context
 * @base: pointer to register struct
 * @msg: pointer to current message
 * @mlen: number of bytes transferred in msg
 * @dev: device reference
 * @adap: i2c core abstraction
 * @msg_complete: xfer completion object
 * @clk: reference for i2c input clock
 * @err: error occured
 * @buf: ptr to msg buffer
 * @bus_clock: current i2c bus clock rate
 * @lock: spinlock for IRQ synchronization
 */
struct skel_i2c {
	void __iomem *base;
	struct i2c_msg *msg;
	size_t mlen;
	struct device *dev;
	struct i2c_adapter adap;
	struct completion msg_complete;
	struct clk *clk;
	u32 bus_clock;
	int err;
	u32 addr;
	u8 *buf;
	spinlock_t lock;
};


Главными управляющими регистрами контроллера являются:




  • Control Register (CTRL) — регистр управления;

  • Interrupt Status Register (ISR) — статусный регистр прерывания;

  • Interrupt Enable Register (IER) — регистр маски прерывания.



Сердцем драйвера является структура skel_i2c, которая содержит такие поля как:




  • .base — указатель на начало регистровой карты;

  • .msg — указатель на текущее сообщение;

  • .adap — I2C абстракция (клик).



Перейдем к более практической части, опишем типы поддерживаемых драйвером устройств,

функционал I2C адаптера и интерфейс передачи I2C сообщений:



static const struct of_device_id skel_i2c_match[] = {
	{
		.compatible = "skel,skel-i2c",
	},
	{
		.compatible = "at,24c64",
	},
	{},
};

static u32 skel_i2c_func(struct i2c_adapter *adap)
{
	return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
}

static const struct i2c_algorithm skel_i2c_algo = {
	.master_xfer = skel_i2c_xfer,
	.functionality = skel_i2c_func,
};

static struct platform_driver skel_i2c_driver = {
	.probe = skel_i2c_probe,
	.remove = skel_i2c_remove,
	.driver = {
		.name = "skel-i2c",
		.of_match_table = skel_i2c_match,
	},
};

module_platform_driver(skel_i2c_driver);

MODULE_AUTHOR("Name Surname");
MODULE_DESCRIPTION("I2C bus driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:skel-i2c");


Из названий структур и функций очевидно их назначение, опишем только главную структуру из представленных выше:




  • skel_i2c_driver — описывает имя драйвера, таблицу поддерживаемых устройств и функций, которые вызываются в момент загрузки или удаления модуля ядра из системы.



Пора зарегистрировать драйвер в системе, а значит реализовать функцию инициализации контроллера, а также описать skel_i2c_probe (вызывается в момент загрузки драйвера в систему) и skel_i2c_remove (вызывается в момент удаления драйвера из системы).



static int skel_i2c_init(struct skel_i2c *rdev)
{
	u32 bus_clk_khz = rdevbus_clock / 1000;
	u32 clk_khz  = clk_get_rate(rdevclk) / 1000;
	int prescale;
	int diff;

	prescale = clk_khz / (5 * bus_clk_khz) - 1;
	prescale = clamp(prescale, 0, 0xFFFF);

	diff = clk_khz / (5 * (prescale  1)) - bus_clk_khz;

	if (abs(diff) > bus_clk_khz / 10) {
		dev_err(rdevdev,
			"Unsupported clock settings: clk: %d KHz, bus: %d KHzn",
			clk_khz, bus_clk_khz);
		return -EINVAL;
	}

	if (presc != -1)
		i2c_write(presc, rdevbase, SKEL_I2C_CLK);
	else
		i2c_write(prescale, rdevbase, SKEL_I2C_CLK);

	return 0;
}

static int skel_i2c_probe(struct platform_device *pdev)
{
	struct skel_i2c *rdev = NULL;
	struct resource *res;
	int irq, ret;
	u32 val;

	rdev = devm_kzalloc(&pdevdev, sizeof(*rdev), GFP_KERNEL);

	if (!rdev)
		return -ENOMEM;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	rdevbase = devm_ioremap_resource(&pdevdev, res);

	if (IS_ERR(rdevbase))
		return PTR_ERR(rdevbase);

	irq = platform_get_irq(pdev, 0);

	if (irq < 0) {
		dev_err(&pdevdev, "Missing interrupt resourcen");
		return irq;
	}

	rdevclk =  devm_clk_get(&pdevdev, NULL);

	if (IS_ERR(rdevclk)) {
		dev_err(&pdevdev, "Missing clockn");
		return PTR_ERR(rdevclk);
	}

	rdevdev = &pdevdev;
	init_completion(&rdevmsg_complete);
	spin_lock_init(&rdevlock);

	val =  of_property_read_u32(pdevdev.of_node, "clock-frequency",
				       &rdevbus_clock);

	if (val) {
		dev_err(&pdevdev, "Default to 100kHzn");
		rdevbus_clock = 100000;	/* default clock rate */
	}

	if (rdevbus_clock > 400000) {
		dev_err(&pdevdev, "Invalid clock-frequency %dn",
			rdevbus_clock);
		return -EINVAL;
	}

	ret = devm_request_irq(&pdevdev, irq, skel_i2c_isr, 0,
				pdevname, rdev);

	if (ret) {
		dev_err(&pdevdev, "Failed to claim IRQ %dn", irq);
		return ret;
	}

	ret = clk_prepare_enable(rdevclk);

	if (ret) {
		dev_err(&pdevdev, "Failed to enable clockn");
		return ret;
	}

	skel_i2c_init(rdev);

	i2c_set_adapdata(&rdevadap, rdev);
	strlcpy(rdevadap.name, pdevname, sizeof(rdevadap.name));

	rdevadap.owner = THIS_MODULE;
	rdevadap.algo = &skel_i2c_algo;
	rdevadap.dev.parent = &pdevdev;
	rdevadap.dev.of_node  = pdevdev.of_node;

	platform_set_drvdata(pdev, rdev);

	ret = i2c_add_adapter(&rdevadap);

	if (ret) {
		clk_disable_unprepare(rdevclk);
		return ret;
	}

	dev_info(&pdevdev, "I2C probe completen");

	return 0;
}

static int skel_i2c_remove(struct platform_device *pdev)
{
	struct skel_i2c *rdev = platform_get_drvdata(pdev);

	clk_disable_unprepare(rdevclk);
	i2c_del_adapter(&rdevadap);

	return 0;
}


Наиболее простой функцией является skel_i2c_remove, которая отключает источник тактовой частоты и освобождает используемую память. Функция skel_i2c_init выполняет первичную инициализацию I2C контроллера.



Как упоминалось ранее skel_i2c_probe регистрирует драйвер в системе. Последовательность действий, условно, можно разделить на два этапа:




  • Получение системных ресурсов и регистрацию обработчика прерывания skel_i2c_isr;

  • Заполнение полей структуры и вызов процедуры добавления нового I2C адаптера.



После того как драйвер зарегистрирован в системе, можно реализовать логику передачи сообщений по интерфейсу:



static inline void i2c_write(uint32_t value, void *base, uint32_t addr)
{
	writel(value, base  addr);

#if defined DEBUG
	dev_dbg(rdevdev, "iowrite32(0x%x, base  0x%x);n", value, addr);
#endif
}

static inline uint32_t i2c_read(void *base, uint32_t addr)
{
	uint32_t reg =  readl(base  addr);

#if defined DEBUG
	dev_dbg(rdevdev, "/* ioread32(base  0x%x) == 0x%x */n", addr, reg);
#endif
	return reg;
}

static irqreturn_t skel_i2c_isr(int irq, void *dev)
{

	if (unlikely(int_stat & skel_I2C_ISR_ARB)) {
		
	} else if (unlikely(int_stat & skel_I2C_ISR_NACK)) {
		
	}

	if (read)
		fill_rx_fifo(rdev);
	else
		fill_tx_fifo(rdev);

	complete(&rdevmsg_complete);

	return IRQ_HANDLED;
}

static int skel_i2c_xfer_msg(struct skel_i2c *rdev, struct i2c_msg *msg)
{
	unsigned long time;

	rdevmsg = msg;
	rdevmlen = msglen;
	rdevaddr = msgaddr;
	rdevbuf = msgbuf;
	rdeverr = 0;

	reinit_completion(&rdevmsg_complete);

	skel_i2c_start_trans(rdev, msg);

	time = wait_for_completion_timeout(&rdevmsg_complete,
					skel_I2C_XFER_TIMEOUT);

	if (time == 0)
		rdeverr = -ETIMEDOUT;

	rdevcurr;

	return	rdeverr;
}

static int
skel_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
	struct skel_i2c *rdev = i2c_get_adapdata(adap);
	int i, ret = 0;

	for (i = 0; (ret == 0) && (i < num); i)
		ret = skel_i2c_xfer_msg(rdev, msgs);

	skel_i2c_snd_stop(rdev);

	return ret ? : num;
}


В первом шаге было описано взаимодействие user space приложения с модулем ядра системы. После того как мы реализовали внутренности драйвера легко увидеть интерфейс через который происходит обмен. В общем случае передача сообщений происходит следующем образом:




  • skel_i2c_xfer — функция напрямую получает сообщения на передачу и последовательно передает каждое сообщение в skel_i2c_xfer_msg. Если во время передачи данных произошла ошибка, то передача данных останавливается;

  • skel_i2c_xfer_msg — функция устанавливает все необходимые поля драйвера и инициирует начало передачи сообщений;

  • skel_i2c_isr — процедура обработки прерывания. Здесь происходит обработка ошибок, а также обмен данными по шине. Если все данные отправлены/приняты устанавливается флаг done с помощью вызова функции complete, которая сигнализирует о завершении передачи сообщения.



В статье не описаны некоторые тонкости работы. Например, последовательность действий передачи сообщений, так как реализация данного алгоритма является аппаратно зависимой. Мы же сосредоточились на реализации общей части драйвера вне зависимости от аппаратных особенностей контроллера.



Полный скелет драйвера прикреплен ниже. Пожалуйста, если вы нашли ошибки/неточности, или вам есть что добавить — напишите в ЛС или в комментарии.





Спасибо за ваше внимание!

Источник: Хабр / Интересные публикации

Теги: c linux i2c

Категория: Linux

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *