Lekcja 13- I2C

Wstęp

W tej lekcji zapoznamy się z obsługą magistrali I2C z poziomu modułu kernelowego. Wykorzystamy tutaj wiedzę z poprzednich lekcji- a w szczególności z lekcji o sterownikach platformowych i device-tree.

Implementacja

W tej lekcji zaimplementujemy sterownik dla zegara RTC DS3231. Na pierwszy rzut oka kod wydaje się być całkiem długi, ma on ponad 400 linii, ale funkcje, które czytają lub zapisują poszczególne atrybuty są w gruncie rzeczy do siebie bardzo podobne dlatego też nie będziemy szczegółowo omawiać każdej z nich. Implementacja wygląda następująco:

#include <linux/module.h>
#include <linux/i2c.h>

#define UPPER_NIBBLE(x) ((x & 0xF0) >> 4)
#define LOWER_NIBBLE(x) (x & 0x0F)

/* DS3231 register defines */
#define SECOND_REG 0x00
#define MINUTE_REG 0x01
#define HOUR_REG 0x02
#define DAY_REG 0x03
#define DATE_REG 0x04
#define MONTH_REG 0x05
#define YEAR_REG 0x06

/* write functions */
static void write_reg(struct i2c_client *client, char reg, char val)
{
	char buf_wr[2];
	struct i2c_msg msg[] =
	{
		{
			.addr = client->addr,
			.flags = 0x00,
			.len = 2,
			.buf = buf_wr,
		},
	};
	buf_wr[0] = reg;
	buf_wr[1] = val;
	i2c_transfer(client->adapter, msg, 1);
}

static char read_reg(struct i2c_client *client, char reg)
{
	char buf_rd[1];
	char buf_wr[1] = {reg};
	struct i2c_msg msg[] =
	{
		{
			.addr = client->addr,
			.flags = 0x00,
			.len = 1,
			.buf = buf_wr,
		},
		{
			.addr = client->addr,
			.flags = I2C_M_RD,
			.len = 1,
			.buf = buf_rd,
		},
	};
	i2c_transfer(client->adapter, msg, 2);
	return buf_rd[0];
}

static ssize_t sec_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
	return sprintf(buf, "%X", read_reg(client, SECOND_REG));
}

static ssize_t sec_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int sec;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	sscanf(buf, "%X", &sec);
	if(UPPER_NIBBLE(sec) > 5 || LOWER_NIBBLE(sec) > 9)
		return -EINVAL;

	write_reg(client, SECOND_REG, (char)sec);
	return count;
}

static ssize_t min_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
	return sprintf(buf, "%X", read_reg(client, MINUTE_REG));
}

static ssize_t min_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int min;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	sscanf(buf, "%X", &min);
	if(UPPER_NIBBLE(min) > 5 || LOWER_NIBBLE(min) > 9)
		return -EINVAL;

	write_reg(client, MINUTE_REG, (char)min);
	return count;
}

static ssize_t hour_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
	return sprintf(buf, "%X", read_reg(client, HOUR_REG));
}

static ssize_t hour_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int hour;
	char upper, lower;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	sscanf(buf, "%X", &hour);
	upper = UPPER_NIBBLE(hour);
	lower = LOWER_NIBBLE(hour);
	if(upper > 2 || (upper == 2 && lower > 4) || lower > 9)
		return -EINVAL;

	write_reg(client, HOUR_REG, (char)hour);
	return count;
}

static ssize_t week_day_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	char day;
	char day_name[16];
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	day = read_reg(client, DAY_REG);
	switch(day)
	{
		case 1:
			strcpy(day_name, "Monday");
			break;
		case 2:
			strcpy(day_name, "Tuseday");
			break;
		case 3:
			strcpy(day_name, "Wednesday");
			break;
		case 4:
			strcpy(day_name, "Thursday");
			break;
		case 5:
			strcpy(day_name, "Friday");
			break;
		case 6:
			strcpy(day_name, "Saturday");
			break;
		case 7:
			strcpy(day_name, "Sunday");
			break;
		default:
			strcpy(day_name, "Unkown");
	}
	return sprintf(buf, "%s", day_name);
}

static ssize_t week_day_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	char day_buf[count];
	int i, day;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	if(buf[count - 1] == '\n') {
		strncpy(day_buf, buf, count - 1);
		day_buf[count - 1] = '\0';
	}
	else
		strcpy(day_buf, buf);

	/*to lower case */
	for(i = 0; i < count; i++)
		if(day_buf[i] >= 65 && day_buf[i] <= 90)day_buf[i] += 32;

	if(strcmp(day_buf, "monday") == 0)
		day = 1;
	else if(strcmp(day_buf, "tuseday") == 0)
		day = 2;
	else if(strcmp(day_buf, "wednesday") == 0)
		day = 3;
	else if(strcmp(day_buf, "thursday") == 0)
		day = 4;
	else if(strcmp(day_buf, "friday") == 0)
		day = 5;
	else if(strcmp(day_buf, "saturday") == 0)
		day = 6;
	else if(strcmp(day_buf, "sunday") == 0)
		day = 7;
	else
		return -EINVAL;

	write_reg(client, DAY_REG, (char)day);
	return count;
}

static ssize_t month_day_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
	return sprintf(buf, "%X", read_reg(client, DATE_REG));
}

static ssize_t month_day_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int date;
	char upper, lower;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	sscanf(buf, "%X", &date);
	upper = UPPER_NIBBLE(date);
	lower = LOWER_NIBBLE(date);
	if(upper > 3 || (upper == 3 && lower > 1) || lower > 9 || date == 0)
		return -EINVAL;

	write_reg(client, DATE_REG, (char)date);
	return count;
}

static ssize_t month_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	char month;
	char month_name[16];
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	month = read_reg(client, MONTH_REG);
	switch(month)
	{
		case 0x01:
			strcpy(month_name, "January");
			break;
		case 0x02:
			strcpy(month_name, "February");
			break;
		case 0x03:
			strcpy(month_name, "March");
			break;
		case 0x04:
			strcpy(month_name, "April");
			break;
		case 0x05:
			strcpy(month_name, "May");
			break;
		case 0x06:
			strcpy(month_name, "June");
			break;
		case 0x07:
			strcpy(month_name, "July");
			break;
		case 0x08:
			strcpy(month_name, "August");
			break;
		case 0x09:
			strcpy(month_name, "September");
			break;
		case 0x10:
			strcpy(month_name, "October");
			break;
		case 0x11:
			strcpy(month_name, "November");
			break;
		case 0x12:
			strcpy(month_name, "December");
			break;
		default:
			strcpy(month_name, "Unkown");
	}
	return sprintf(buf, "%s", month_name);
}

static ssize_t month_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	char month_buf[count];
	int i, month;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	if(buf[count - 1] == '\n') {
		strncpy(month_buf, buf, count - 1);
		month_buf[count - 1] = '\0';
	}
	else
		strcpy(month_buf, buf);

	/*to lower case */
	for(i = 0; i < count; i++)
		if(month_buf[i] >= 65 && month_buf[i] <= 90)month_buf[i] += 32;

	if(strcmp(month_buf, "januart") == 0)
		month = 0x01;
	else if(strcmp(month_buf, "february") == 0)
		month = 0x02;
	else if(strcmp(month_buf, "march") == 0)
		month = 0x03;
	else if(strcmp(month_buf, "april") == 0)
		month = 0x04;
	else if(strcmp(month_buf, "may") == 0)
		month = 0x05;
	else if(strcmp(month_buf, "june") == 0)
		month = 0x06;
	else if(strcmp(month_buf, "july") == 0)
		month = 0x07;
	else if(strcmp(month_buf, "august") == 0)
		month = 0x08;
	else if(strcmp(month_buf, "september") == 0)
		month = 0x09;
	else if(strcmp(month_buf, "october") == 0)
		month = 0x10;
	else if(strcmp(month_buf, "november") == 0)
		month = 0x11;
	else if(strcmp(month_buf, "december") == 0)
		month = 0x12;
	else
		return -EINVAL;

	write_reg(client, MONTH_REG, (char)month);
	return count;
}

static ssize_t year_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
	return sprintf(buf, "%X", read_reg(client, YEAR_REG));
}

static ssize_t year_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
	int year;
	struct i2c_client *client = container_of(dev, struct i2c_client, dev);

	sscanf(buf, "%X", &year);
	if(UPPER_NIBBLE(year) > 9 || LOWER_NIBBLE(year) > 9)
		return -EINVAL;

	write_reg(client, YEAR_REG, (char)year);
	return count;
}


static DEVICE_ATTR(second, 0644, sec_show, sec_store);
static DEVICE_ATTR(minute, 0644, min_show, min_store);
static DEVICE_ATTR(hour, 0644, hour_show, hour_store);
static DEVICE_ATTR(week_day, 0644, week_day_show, week_day_store);
static DEVICE_ATTR(month_day, 0644, month_day_show, month_day_store);
static DEVICE_ATTR(month, 0644, month_show, month_store);
static DEVICE_ATTR(year, 0644, year_show, year_store);

static int ds3231_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	if(device_create_file(&client->dev, &dev_attr_second) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_second;
	}

	if(device_create_file(&client->dev, &dev_attr_minute) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_minute;
	}

	if(device_create_file(&client->dev, &dev_attr_hour) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_hour;
	}

	if(device_create_file(&client->dev, &dev_attr_week_day) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_week_day;
	}

	if(device_create_file(&client->dev, &dev_attr_month_day) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_month_day;
	}

	if(device_create_file(&client->dev, &dev_attr_month) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_month;
	}

	if(device_create_file(&client->dev, &dev_attr_year) != 0) {
		pr_err("Cannot create sysfs entry!!!\n");
		goto err_year;
	}

	return 0;

err_year:
	device_remove_file(&client->dev, &dev_attr_month);
err_month:
	device_remove_file(&client->dev, &dev_attr_month_day);
err_month_day:
	device_remove_file(&client->dev, &dev_attr_week_day);
err_week_day:
	device_remove_file(&client->dev, &dev_attr_hour);
err_hour:
	device_remove_file(&client->dev, &dev_attr_minute);
err_minute:
	device_remove_file(&client->dev, &dev_attr_second);
err_second:
	return -1;
}

static int ds3231_remove(struct i2c_client *client)
{
	device_remove_file(&client->dev, &dev_attr_year);
	device_remove_file(&client->dev, &dev_attr_month);
	device_remove_file(&client->dev, &dev_attr_month_day);
	device_remove_file(&client->dev, &dev_attr_week_day);
	device_remove_file(&client->dev, &dev_attr_hour);
	device_remove_file(&client->dev, &dev_attr_minute);
	device_remove_file(&client->dev, &dev_attr_second);
	return 0;
}

static const struct i2c_device_id ds3231_id[] = {
	{"ds3231sample", 0},
	{},
};

static const struct of_device_id ds3231_match[] = {
	{ .compatible = "unknown,ds3231sample", },
	{ },
};

MODULE_DEVICE_TABLE(i2c, ds3231_id);

static struct i2c_driver ds3231_i2c_driver = {
	.driver = {
		.owner = THIS_MODULE,
		.name = "ds3231sample",
		.of_match_table = of_match_ptr(ds3231_match),
	},
	.probe = ds3231_probe,
	.remove = ds3231_remove,
	.id_table = ds3231_id,
};

module_i2c_driver(ds3231_i2c_driver);

MODULE_LICENSE("GPL");

Choć kod wydaje się długawy to duża jego część powinna być dla Ciebie zrozumiała.

Zacznijmy od tego co znasz na pewno czyli funkcji probe oraz remove. W funkcji probe są tworzone atrybuty naszego urządzenia, a w funkcji remove są one usuwane. Na atrybuty w tym przypadku składają się poszczególne miary czasu tj. godzina, dzień, miesiąc, rok itd., które odpowiadają poszczególnym rejestrom urządzenia.

Funkcje show i store również powinny być tobie znane, jedyną nowością w nich jest zapis i odczyt danych po magistrali I2C.

Aby zapis i odczyt danych był możliwie prosty opakowaliśmy standardową funkcję i2c_transfer w funkcje write_reg oraz read_reg. Funkcje te wypełniają odpowiednio strukturę i2c_msg tak aby można było odczytać lub zapisać dane przy pomocy funkcji i2c_transfer.

Kolejną nowością, ale tylko pozorną, są struktura i2c_driver oraz makro module_i2c_driver. Nie jest to nic więcej jak odpowiedniki struktury platform_driver oraz makra module_platform_driver z tą różnicą, że są one dedykowane urządzeniom komunikującym się po I2C. Makro module_i2c_driver przygotowuje standardową funkcję init i exit, które oprócz tego, że rejestrują sterownik platformowy to rejestruje również urządzenie I2C.

Tak więc wydawało się, że jest sporo kodu, a tak naprawdę poznaliśmy wcale nie tak dużo nowych rzeczy.

Makefile wygląda następująco:

obj-m += 13_i2c.o
all:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) modules
clean:
	make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) clean

Przebuduj moduł dla swojej płytki:

# BBB
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# RPi4
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8

Device-tree

Wpis dla BBB oraz RPi będą do siebie łudząco podobne, będą się jedynie różnić magistralą do której są podłączone.

Nasz wpis będzie zawierał dwie właściwości- compatible oraz reg. Zapewne już wiesz czym jest compatible, a reg to adres urządzenia na magistrali I2C.

BeagleBone Black

W przypadku BBB w pliku arch/arm/boot/dts/am335x-boneblack-uboot-univ.dts dodaj następujący wpis:

&i2c2 {
     ds3231sample: ds3231sample@68 {
         compatible = "unknown,ds3231sample";
         reg = <0x68>;
     };
};

Device-tree przebuduj za pomocą komendy make tak jak jest to opisane w lekcji 10 i wgraj je na swoją płytkę.

Raspberry Pi 4

W przypadku RPi w pliku arch/arm/boot/dts/bcm2711-rpi-4-b.dts dodaj następujący wpis:

&i2c1 {
     ds3231sample: ds3231sample@68 {
         compatible = "unknown,ds3231sample";
         reg = <0x68>;
     };
};

Testowanie sterownika

Prześlij zbudowane moduły na swoją płytkę np. za pomocą scp. Załaduj moduł:

sudo insmod 13_i2c.ko

I przejdź do katalogu urządzenia:

# BBB
cd /sys/bus/i2c/devices/2-0068
# RPi
cd /sys/bus/i2c/devices/1-0068

Powinieneś zobaczyć kilka plików w tym atrybuty, które utworzyłeś w module. Każdy atrybut możesz odczytać oraz zapisać:

cat second
echo 10 > second

Inne podsystemy

W kernelu będą dostępne również analogiczne interfejsy do innych magistrali jak np. SPI, ale są również dostępne interfejsy nieco wyższego poziomu jak np. RTC, który to udostępnia interfejs do zegarów bez względu na używany interfejs komunikacyjny.

Ten wpis został opublikowany w kategorii Kurs pisania sterowników. Dodaj zakładkę do bezpośredniego odnośnika.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *