Привет, мир! Меня зовут Василий, я работаю инженером-программистом в научно-исследовательском институте. Последние лет пять занимаюсь внедрением регистрирующего оборудования на базе микрокомпьютера Raspberry Pi, хочу поделиться опытом разработки. Работа еще не завершена, но материала накопилось много, думаю, он будет полезен тем, кто работает с Linux-микрокомпьютерами и учится писать драйвера для подключения разных железок. Буду также крайне признателен, если отпишутся знающие люди, поделятся советом и укажут на мои ошибки.

ПЛАН

  1. Какое было техническое задание?

  2. Особенности работы с микросхемой АЦП ADS1256

  3. Реализация модуля ядра Linux

  4. Два прерывания по цене одного?

  5. Как костыли натыкаются на подводные камни (О проблемах)

Какое было техническое задание?

Требования к системе регистрации были следующие:

  • Вместо одного многоканального регистратора – массив одноканальных. Это требование продиктовано чисто рационалистическими соображениями. Т.к. наблюдения происходят в полевых условиях, есть немалая вероятность выхода из строя оборудования, оно должно быть легко заменяемым. К тому же, пункты наблюдения разнесены друг от друга на 150-500 м, некоторые из них автономны – с питанием от солнечных панелей и связью по беспроводному каналу Wi-Fi – по той причине, что до них сложно проложить кабель.

  • Высокая частота дискретизации не требуется, достаточно 50-100 SPS (сэмплов в секунду).

  • Разрядность АЦП не менее 24 бит, чтобы за счет широкого динамического диапазона наблюдать едва заметные сигнальчики.

  • Наконец, должна быть возможность тактирования АЦП от сигналов точного времени (например, от импульсного сигнала PPS в GNSS-приемнике). Только так можно быть уверенным, что разрозненные датчики работают синхронно.

Так я пришел к платам АЦП на основе микросхемы Texas Instruments ADS1256. И только потом выбрал master-устройство, которое всем заправляет – собственно, Raspberry Pi. Думаю, в целом, я не прогадал, работать с малинками мне нравится благодаря операционной системе на основе Linux. В перспективе возможен переход на любой подобный микрокомпьютер.

Схема соединений между отдельными устройствами внутри регистратора
Схема соединений между отдельными устройствами внутри регистратора

Особенности работы с микросхемой АЦП ADS1256

Немного фактов про пациента. Это 24-битный АЦП с обратной связью – это означает, что есть базовая частота дискретизации, она же самая высокая (30 кГц), и для того, чтобы получить результат на другой, маленькой, частоте, АЦП усредняет результат нескольких преобразований на базовой частоте. Для частоты 50 Гц нужно сделать 600 таких усреднений. Да, есть список из 16 частот дискретизации, которые доступны для установки, если вместо значения из списка поставить что-то своё – работа не гарантируется. Нет отдельной настройки на работу от внешнего триггера, что очень триггерило моего руководителя. Выяснилось это, конечно же, постфактум, после покупки.

АЦП имеет на борту 8 аналоговых входов, которые можно сконфигурировать как 8 несимметричных входов или 4 дифференциальных. В зависимости от подключаемого датчика, мне нужен один несимметричный / дифференциальных вход, хотя идея подключать несколько разных датчиков к одной плате всё еще рассматривается.

Общение с микросхемой происходит по цифровому интерфейсу SPI: документацией определен список команд для управления преобразованием, внесения информации в регистры и т.п. Да, предусмотрено несколько регистров, которые можно читать или перезаписывать – задавать программное усиление и калибровочную информацию, менять частоту дискретизации и т.д. А вот регистр, в котором хранится результат преобразования, для непосредственного чтения недоступен, извольте по MOSI отправить команду RDATA или RDATAC (read data / read data continuously), когда флаг готовности данных DRDY сменит свой логический уровень, и только после этого получите необходимые 3 байта. Инициировать преобразование также можно и системой команд, только и останется что за состоянием DRDY следить, и в такой системе возможность синхронизации внешним сигналом выглядит рудиментом. Но, нам нужны точные измерения, поэтому, продолжим.

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

Задаем строб SYNC, это должен быть сигнал отрицательной полярности (1 → 0 → 1) длительностью не менее 4 периодов тактовой частоты или 520 нс. Но сильно долго держать SYNC в LOW тоже нельзя, всё это время преобразование не идет, оно начнется только на нарастающем фронте SYNC-сигнала. Поэтому я обычно задаю длительность 1 мкс. После этого микросхема практически мгновенно устанавливает флаг DRDY с нуля в "1", само преобразование, к сожалению, длится чуть больше заявленного периода дискретизации (например, 20.19 мс вместо 20 мс), что немного странно. Понятно, что в это время включены различные издержки, но ведь можно было уменьшить число усреднений, чтобы издержки уместились в мои 20 мс.

Так и получается, что задать частоту дискретизации 50 Гц и частоту строба 50 Гц – нельзя, дискретизация должна быть выше частоты синхросигнала. Иначе DRDY постоянно висит в "1", и нет возможности забрать результаты преобразования. Да, обратите внимание, что вход SYNC микросхемы толерантен к входному сигналу величиной до +6В, а на плате расширения разработчик предусмотрел соединение SYNC с одним из GPIO малинки, который рассчитан максимум на 3.3В. При превышении напруги GPIO может банально выгореть, поэтому, БУДЬТЕ ОСТРОЖНЫ с подключением каких-либо внешних сигналов к вашим устройствам!

В первом случае частота дискретизации больше частоты тактирования, поэтому после спадающего фронта на DRDY есть возможность забрать данные. Во втором случае АЦП настроен на частоту 50 Гц, но, поскольку время преобразования на самом деле она чуть больше, АЦП не успевает выполнить преобразование до следующего строба, и DRDY вообще не опускается в "0". Обратите внимание на множество коротких импульсов на верхнем рисунке, они повторяются с периодом дискретизации и сигнализируют о том, что данные предыдущей конверсии не извлекли. Как только их забираешь – начнется новое преобразование
В первом случае частота дискретизации больше частоты тактирования, поэтому после спадающего фронта на DRDY есть возможность забрать данные. Во втором случае АЦП настроен на частоту 50 Гц, но, поскольку время преобразования на самом деле она чуть больше, АЦП не успевает выполнить преобразование до следующего строба, и DRDY вообще не опускается в "0". Обратите внимание на множество коротких импульсов на верхнем рисунке, они повторяются с периодом дискретизации и сигнализируют о том, что данные предыдущей конверсии не извлекли. Как только их забираешь – начнется новое преобразование

Возвращаясь к периоду дискретизации. Как дождаться срабатывания флага DRDY? Известное дело, в пользовательской программе (Python, C и т.п.) это можно сделать только методом опроса GPIO, с которым соединен выход DRDY, т.н. polling. Поскольку мы собираем устройство для точных измерений, нас такой расклад не устраивает. Благо, Linux-микрокомпьютеры дают возможность непосредственно влезть в ядро операционной системы и дергать оттуда аппаратные прерывания.

Реализация модуля ядра Linux

Начну с того, что мне необычайно понравился пример с подключением кнопки (стр 109) к Raspberry Pi, где при нажатии работа операционной системы прерывается до тех пор, пока не будет выполнен некий важный код – обработчик прерывания, реакция на внешнее событие происходит значительно быстрее. Разумеется, в обычной C-программе такая роскошь недоступна, нужно писать модуль ядра.

Об особенностях такой разработки долго не хочу распинаться, об этом много где написано. По терминологии скажу пару слов, в ядре можно написать крайне простенький код, который ничего не делает, кроме как выводит некий текст в системный лог командой printk. Тем не менее, если код написан по правилам, его можно загрузить в ядро и выгрузить оттуда, значит это – полноценный модуль ядра, часть операционной системы. Хоть и на время. Мой модуль несколько сложнее, он работает с железом и предполагает обмен данными с другим программным обеспечением. Внешняя программа, написанная, например, на Python, может выкачивать из ядра буфер измеренных значений и разрешать/запрещать прерывания, примерная схема, как это работает, приведена ниже. И это дает основание называть данный модуль целым, не побоюсь этого слова, драйвером! Кстати, устройства SPI в Linux обслуживает штатный драйвер spidev. А свой драйвер по аналогии я назвал spirev! Где "rev" – это революционный, revolution :) Кхм, таким образом, каждый драйвер – это модуль, но не каждый модуль – драйвер, запомните.

Код spirev v.1.0
#include <linux/module.h>			//
#include <linux/kernel.h>			//
#include <linux/init.h>				// 
#include <linux/cdev.h>				// register / unregister chrdev
#include <linux/device.h>			// to add struct class
#include <linux/fs.h>				// file operations
#include <linux/uaccess.h>			// copy_to_user
#include <linux/gpio.h>				//
#include <linux/interrupt.h>		//
#include <linux/spi/spi.h>			//
#include <linux/workqueue.h>		//
#include <linux/delay.h>			//

// Prototypes
static int spirev_open(struct inode *, struct file *);
static int spirev_release(struct inode *, struct file *);
static ssize_t spirev_read(struct file *, char __user *, size_t, loff_t *);
static ssize_t spirev_write(struct file *, const char __user *, size_t, loff_t *);

#define DEVICE_NAME "spirev"
#define DRDY 17
#define BUS_NUM 0
#define NN 2000
#define N 1000
#define n32 4

// Array processing variables
static int count = 0;
static u32 data;
static u32 adata[NN];	// data array
// ADC register config
static u8 mux_single = 0x08;	// 0000 1000 AIN0
static u8 mux_diff = 0x10;		// 0001 0000 AIN1-AIN0
static u8 adc_1 = 0x20;			// no amp
static u8 adc_2 = 0x21;			// x2
static u8 drt_100 = 0x82;		// for correct work with 50PPS
// SPI data transfer
static u8 command_read[] = {0x01};
static u8 rxx[] = {0,0,0};
// New device & device class
static int major;
static struct class *cls;
static struct spi_device *ads1256;
// Interrupt & bottom half
static int map_to_irq = -1;
static u8 irq_status = 0;
static struct workqueue_struct *wqs;
static struct work_struct drdy_work;

static void drdy_handler(struct work_struct *work){
	spi_write(ads1256, &command_read, 1);
	spi_read(ads1256, &rxx, 3);
	data = rxx[0]<<16;
	data |= rxx[1]<<8;
	data |= rxx[2];
	if(data & 0x800000){
		data |= 0xFF000000;
	}
	//printk("%d, %d\n", count, data);
	adata[count] = data;
	if (count<NN-1){
		count++;
	}
	else {
		count = 0;
	}
}

static irqreturn_t drdy_interrupt(int irq, void *data){
	if (!irq_status){
		return IRQ_HANDLED;
	}
	schedule_work(&drdy_work);
	return IRQ_HANDLED;
}

static struct file_operations spirev_ops = {
	.open = spirev_open,
	.release = spirev_release,
	.read = spirev_read,
	.write = spirev_write,
};

static int __init spirev_init(void){
	int ret = 0;
	u8 mux[] = {0x51, 0x00, mux_diff};
	u8 adc[] = {0x52, 0x00, adc_1};
	u8 drt[] = {0x53, 0x00, drt_100};
	struct spi_master *master;
	struct spi_board_info ads1256_info = {
		.modalias = "ads1256",
		.max_speed_hz = 3000000,//976000,
		.bus_num = BUS_NUM,
		.chip_select = 0,
		.mode = 1,
	};
	// Device registration
	major = register_chrdev(0, DEVICE_NAME, &spirev_ops);
	if (major < 0){
		pr_alert("Registering char device failed with %d.\n", major);
		return major;
	}
	pr_info("Get major number %d.\n", major);
	cls = class_create(THIS_MODULE, DEVICE_NAME);
	device_create(cls, NULL, MKDEV(major,0), NULL, DEVICE_NAME);
	// DRDY interrupt config
	ret = gpio_request_one(DRDY, GPIOF_IN, NULL);
	if(ret){
		pr_err("Unable to request GPIO: %d.\n", ret);
		return ret;
	}
	ret = gpio_to_irq(DRDY);
	map_to_irq = ret;
	ret = request_irq(map_to_irq, drdy_interrupt, IRQF_TRIGGER_FALLING, "DRDY#inter", NULL);
	if(ret<0){
		pr_err("Unable to request IRQ: %d.\n", ret);
		gpio_free(DRDY);
	}
	// SPI-connection config
	master = spi_busnum_to_master(BUS_NUM);
	if(!master){
		pr_err("There is no spi bus with No %d.\n", BUS_NUM);
		return -1;
	}
	ads1256 = spi_new_device(master, &ads1256_info);
	if(!ads1256){
		pr_err("Could not create device.\n");
		return -1;
	}
	ads1256 -> bits_per_word = 8;
	if(spi_setup(ads1256)!=0){
		pr_err("Could not change bus setup.\n");
		spi_unregister_device(ads1256);
		return -1;
	}
	// ADC registers config
	spi_write(ads1256, mux, 3);
	udelay(10);
	spi_write(ads1256, adc, 3);
	udelay(10);
	spi_write(ads1256, drt, 3);
	// Workqueue config
	wqs = alloc_workqueue("SPIREV-WQ", WQ_UNBOUND | WQ_HIGHPRI, 0);
	INIT_WORK(&drdy_work, drdy_handler);
	return 0;
}

static void __exit spirev_exit(void){
	flush_workqueue(wqs);
	destroy_workqueue(wqs);
	free_irq(map_to_irq, NULL);
	gpio_free(DRDY);
	spi_unregister_device(ads1256);
	device_destroy(cls, MKDEV(major,0));
	class_destroy(cls);
	unregister_chrdev(major, DEVICE_NAME);
}

static int spirev_open(struct inode *inode, struct file *file){
	try_module_get(THIS_MODULE);
	return 0;
}

static int spirev_release(struct inode *inode, struct file *file){
	module_put(THIS_MODULE);
	return 0;
}

static ssize_t spirev_read(struct file *file, char __user *buffer, size_t length, loff_t *offset){
	int *ptr;
	int start, end;
	ssize_t ret = 0;
	if (count<N){
		start = N;
		end = NN;
	}
	else {
		start = 0;
		end = N;
	}
	for (ptr = adata+start; ptr<adata+end; ptr++){
		if (copy_to_user(buffer+*offset, ptr, n32)){
			return -EFAULT;
		}
		else {
			ret+=n32;
			*offset+=n32;
		}
	}
	return ret;
}

static ssize_t spirev_write(struct file *file, const char __user *buffer, size_t len, loff_t *offset){
	ssize_t ret = 0;
	if (*offset>=sizeof(irq_status)){
		return -ENOSPC;
	}
	if (get_user(irq_status, buffer)) {
		return -EFAULT;
	}
	else {
		ret+=1;
	}
	*offset = 0;
	return ret;
}

module_init(spirev_init);
module_exit(spirev_exit);
MODULE_LICENSE("GPL");
Драйвер spirev
Драйвер spirev

Совсем кратенько поясню схему и код. При загрузке модуля первой активируется функция spirev_init, в ней происходят предварительные подготовки и настройки. Соответственно, spirev_exit отменяет все внесенные изменения. Поскольку наш проект предполагает общение с железякой и накопление буфера в памяти ядра, создается символьное устройство (char dev), хотя, в теории, оно может быть и блочным. Его можно будет найти в директории /dev и обратиться словно к какой-нибудь флэшке или видеокарте, открыть-закрыть-прочитать и т.п., но об этом чуть позже. Регистрация чардева подразумевает, что у операционной системы необходимо выпросить уникальный идентификатор устройства – старший номер (major). Мне, например, система практически всегда выдавала номер 239. Разумеется, устройство не создается само по себе, оно принадлежит некому классу, который нужно прописать отдельно. Очередной чардев /dev/spirev материализуется после выполнения device_create.

Список непотребств, которые можно выполнять с чардевом, называется file operations и определен в заголовочном файле /include/linux/fs.h. Чтобы компилятор не ругался, используемые функции нужно в коде модуля поместить в структуру типа file_operations. Кроме того, если для нашего модуля мы не пишем отдельно заголовочный файл (header, .h), то все символьные функции желательно объявить как прототипы в шапке C-файла. Когда стороннее приложение открывает символьное устройство, вызывается spirev_open, и функция не дает другим приложениям использовать чардев в данный момент, spirev_release отменяет это ограничение. spirev_read передает некоторую информацию из ядра пользователю, spirev_write делает наоборот, но я еще вернусь к этим функциям.

Далее, необходимо получить номер прерывания для нашего пина GPIO, на этом этапе пин определяется как вход (флаг GPIOF_IN), а в функции request_irq указывается обработчик прерывания drdy_interrupt и условие, при котором возникает прерывание – на пине DRDY должен произойти переход импульсного сигнала из "1" в ноль, т.е. спадающий фронт. Таким образом, прерывание происходит в момент, когда АЦП в очередной раз заканчивает преобразование и готово передать его результат, соответственно, в обработчике необходимо отправить команду на чтение command_read, забрать эти данные и поместить новое значение в буфер.

О, точно, буфер! Я пришел к тому, что для меня оптимальный размер одного буфера это 1000 значений типа int (3 байта само измерение + 1 байт на знак) или 4000 байт. Или 20 секунд записи (частота 50 SPS). Буферов надо два, поскольку, пока один наполняется, другой должен быть готов к отправке, итого 8 тыс байт. В ядре, разумеется, нежелательно объявлять большие массивы, однако предупреждение при загрузке модуля типа:

realloc(): invalid next size
Aborted

возникает, когда пытаешься выделить 40 тыс байт и больше, так что переполнений быть не должно, тем более что буферы попеременно перезаписываются. И это именно статическое выделение памяти, как в обычной C-программе, а не динамическое, как в случае с функциями вроде kmalloc. Как видите, оно допустимо, хоть и с ограничениями. Буферы можно реализовать в виде отдельных массивов, но я сделал один массив, поделенный на две части, переключение между частями одного массива выглядит не так громоздко в коде.

Да, описанный чуть выше алгоритм для drdy_interrupt слишком сложен, из-за него компьютер просто зависнет. Не менее десятка раз пришлось заруинить систему, прежде чем пришло понимание, что основную часть обработки придется перенести на т.н. нижнюю половину прерывания, которая вызывается, ну, чуть позже. Самое главное, чтобы прерывание сработало вовремя, остальное уже не так важно, частота дискретизации маленькая и времени для манипуляции данными, казалось бы, достаточно. Существует несколько механизмов постобработки прерывания – тасклеты, очереди заданий и т.п., я как раз остановился на этой самой очереди, workqueue.

По всей видимости, очереди работают следующим образом. Объявляя в шапке модуля структуру workqueue_struct, мы провоцируем создание нового рабочего потока в ядре, помимо уже существующих. После чего необходимо обозначить список заданий, которые будут выполняться в этом потоке, каждое задание представляет собой структуру work_struct, и его можно задекларировать в самом начале с помощью макроса DECLARE_WORK, либо, например, по ходу выполнения функции spirev_init путем макроса INIT_WORK, как в примере выше. Тем самым я несколько упрощаю схему, предложенную в книжке Роберта Лава на стр 192.

Работа с очередью заданий (workqueue)
Работа с очередью заданий (workqueue)

Касательно SPI, инструментарий ядра для настройки интерфейса выглядит максимально гибким, и реализовать подключение, думаю, можно разными способами. А, да, важная ремарка:

Информация ниже актуальна для версии ядра 5.10, в более новых версиях модуль может не работать. Например, функция spi_busnum_to_master в версии 6.6 уже отсутствует.

linux kernel 6.6
В версии ядра 6.6 даже синтаксис функции create_class изменился, я уж глубже не стал копать проблему совместимости
В версии ядра 6.6 даже синтаксис функции create_class изменился, я уж глубже не стал копать проблему совместимости

Чтобы зарегистрировать новое подключение, нужно заполнить структуру spi_board_info. Понятное дело, что у Raspberry Pi Model 3b, которые я использую, шина SPI одна, поэтому и BUS_NUM = 0, chip_select можно поставить 0 или 1, в данном случае без разницы. А вот значение mode я честно взял у китайцев в коде, поскольку в даташите на микросхему о режиме синхронизации информации нет. Очевидно, режим зависит скорее от master-устройства, т.е. от малинки… в общем, при работе с цифровым интерфейсом нужно уточнять такие моменты, поскольку в другом режиме соединение может не заработать.

Для отправки и приема сообщений используются spi_write и spi_read. В хидере /include/linux/spi/spi.h, где они описаны, они являются, скажем так, функциями более высокого уровня, потому что и в том, и в другом случае основную работу выполняет spi_sync_transfer, для которой заполняется структура spi_transfer, но нам, как видите, этого делать не нужно. Обратите внимание, что в описании к функциям разработчик указывает контекст, например "can sleep" означает, что функция может использоваться только в контексте процесса, в обработчике прерывания ее использовать нельзя.

Допустим, мы написали модуль, а при попытке его собрать нам сообщается: chipselect 0 already in use. Как же так? А помните, в начале главы я писал про spidev? Штатный драйвер оккупирует нашу единственную шину, и создает на ней два устройства, /dev/spidev0.0 и /dev/spidev0.1. Я не нашел информации, можно ли отключить драйвер на этапе загрузки системы, но можно принудительно исключить устройства из дерева устройств (Device-tree). Сложная тема, вообще не хочу в нее вдаваться, просто скажу, что способ, описанный здесь пользователем Samt43, рабочий. Я даже немного доработал его код в spidev_disabler.dts, так, чтобы убрать из /dev сразу оба устройства:

spidev_disabler.dts
/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2708";

    fragment@0 {
        target = <&spidev0>;
        __overlay__ {
            status = "disabled";
        };
    };
    fragment@1 {
        target = <&spidev1>;
        __overlay__ {
            status = "disabled";
        };
    };
};

После компиляции создается файл .dtbo, который можно запускать при загрузке системы через службу rc.local, то есть команду dtoverlay можно поместить в файл /etc/rc.local перед строкой "exit 0". Главное, убедиться, что служба автозагрузки вообще включена:

systemctl status rc.local

Пример регистрации исполняемого кода в автозагрузке в Raspberry Pi

Вернемся к spirev_read и spirev_write, как и обещал. За передачу какой-либо информации между пространствами ядра и пользователя отвечают пары функций copy_to_user / put_user и copy_from_user / get_user, которые доступны в /include/linux/uaccess.h. Put и get позволяют передать только один байт (переменную типа char), с помощью copy-функции можно отправить довольно большой объем информации, например, весь буфер значений сразу! Выглядит это так:

copy_to_user example
// N32 = 4000
// data1 is a separate array
if (copy_to_user(buffer, &data1, N32)){
	return -EFAULT;
}
else {
	ret+=N32;
}

Очевидно, работать это будет в том случае, если каждый буфер пишется в свой массив. Мне же приходится в цикле перебирать индексы массива в пределах от start до end, и каждую итерацию цикла в buffer складывается только одно 32-битное значение. Кроме того, нужно контролировать, чтобы объем передачи совпадал как на стороне пользователя, так и в ядре. И если вы запрограммировали ядро на передачу 4000 байт, то, будьте добры, укажите то же число и пользовательской программе:

python reader example
dev = open('/dev/spirev', 'rb')
data = dev.read(4000)
dev.close()

Самое крутое, что пользователь читает 4000 байт, но на стороне ядра этот объем заполняется как угодно, можно применять copy_to_user и put_user в любой последовательности: 2000 байт (например) отправляется с помощью copy, остальное – побайтово put-ом, главное следить и уметь манипулировать значениями buffer и *offset. Это удобно, когда помимо буфера значений нужно передать какую-либо служебную информацию, например, так на ранних этапах я пробовал вместе с данными отправить значение счетчика count, чтобы проконтролировать, что пропусков в данных нет и все прерывания отрабатывают корректно. Разумеется, в таком случае пользователь должен быть готов принять уже 4004 байта.

some proprietary info example
// ‘previous’ is declared at the beginning of module
static ssize_t spirev_read(struct file *flip, char __user *buffer, size_t length, loff_t *offset){
	int i;
	ssize_t ret;
	int present;
	union {
		int c;
		char s[4];
	} count2;
	present = count;
	pr_info("prev = %d, pres = %d\n",previous, present);
	count2.c = present - previous;
	// put some proprietary information to user
	for (i=0; i<4; i++){
		if (put_user(count2.s[i], buffer++)!=0){
			return -EFAULT;
		}
		else {
			ret++;
		}
	}
	// put data array entirely to user 
	if (copy_to_user(buffer, &data1, N32)){
		return -EFAULT;
	}
	else {
		ret+=N32;
	}
	previous = present;
	return ret;
}

А, маленький нюанс! put_user передает только 1 байт за раз, значит, чтобы передать значение из нескольких байт, нужно представить его в виде массива типа char. Чтобы не плодить в памяти копии копий и обращаться к той же области памяти, используйте объединение (union), как в примере. И последнее, обе функции, spirev_read и spirev_write, должны вернуть число переданных и отправленных байт. Такие правила.

Два прерывания по цене одного?

В предыдущей главе я разбираю код spirev.c (v.1.0), эта версия работала без нареканий при тактировании как от генератора, так и от GNSS. Единственное, АЦП нужно было настроить на частоту дискретизации 100 SPS, и тогда есть возможность корректно и вовремя забирать данные преобразования, я писал об этом выше. Каково же было мое удивление, когда после долгого простоя я догадался посмотреть, как ведут себя сигналы в логическом анализаторе.

Нееет, так быть не должно!!1
Нееет, так быть не должно!!1

Одно из ключевых правил документации – АЦП прерывает текущее преобразование, если наступает новый строб синхросигнала, и здесь мы видим, что данное правило не выполняется. Устройство как будто находится в состоянии ожидания до наступления строба, приходит строб, и только после этого преобразование выполняется так, как следует, с периодом 10 мс. В итоге, я получаю результат, но какому моменту времени он принадлежит? Я хочу, чтобы этот процесс протекал максимально прозрачно и последовательно: строб → вычисление значения → получение значения.

А правило, на самом деле, выполняется, но только в том случае, если не извлекать данные перед новым стробом. Получается, частоту дискретизации нужно поднять еще на один шаг, до 500 сэмплов, тогда на очередном стробе:

  • АЦП сам начинает новое преобразование,

  • когда оно закончится - извлекаем данные,

  • перманентно начинается следующее преобразование,

  • а после него данные не извлекаются...

и тогда АЦП корректно отреагирует на новый строб. Звучит здорово, но это значит, что придется усложнить модуль, объявить в нем два прерывания, по SYNC и по DRDY. Следовательно, должно быть два постобработчика этих прерываний. А еще не забудем, что если не извлечь данные, то АЦП вредительски будет сигналить об этом мусорными импульсами на DRDY. В общем, держите доработанный код:

spirev v.2.0
#include <linux/module.h>				//
#include <linux/kernel.h>				//
#include <linux/init.h>					// 
#include <linux/cdev.h>					// register / unregister chrdev
#include <linux/device.h>				// to add struct class
#include <linux/fs.h>					// file operations
#include <linux/uaccess.h>				// copy_to_user
#include <linux/gpio.h>					//
#include <linux/interrupt.h>			//
#include <linux/spi/spi.h>				//
#include <linux/workqueue.h>			//
#include <linux/delay.h>				//

// Prototypes
static int spirev_open(struct inode *, struct file *);
static int spirev_release(struct inode *, struct file *);
static ssize_t spirev_read(struct file *, char __user *, size_t, loff_t *);
static ssize_t spirev_write(struct file *, const char __user *, size_t, loff_t *);
static void drdy_handler(struct work_struct *work);
static void sync_handler(struct work_struct *work);

#define DEVICE_NAME "spirev"
#define DRDY 17
#define SYNC 27
#define BUS_NUM 0
#define NN 2000
#define N 1000
#define n32 4
#define n16 2

// Array processing variables
static int count = 0;
static u32 data;
static u32 adata[NN];				// data array
// Time array variables
static u32 tcount;
static u32 tdata[NN];
static struct timespec64 ts;
// ADC register config
static u8 mux_single = 0x08;		// 0000 1000 AIN0
static u8 mux_diff = 0x10;			// 0001 0000 AIN1-AIN0
static u8 adc_1 = 0x20;				// no amp
static u8 adc_2 = 0x21;				// x2
static u8 drt_500 = 0x92;			// for correct work with 50PPS
// SPI data transfer
static u8 command_read[] = {0x01};
static u8 rxx[] = {0,0,0};
// New device & device class
static int major;
static struct class *cls;
static struct spi_device *ads1256;
// Interrupt & bottom half
static u8 irq_status = 0;
static int map_drdy = -1;
static int map_sync = -1;
static struct workqueue_struct *wqs;
DECLARE_WORK(drdy_work, drdy_handler);
DECLARE_WORK(sync_work, sync_handler);

static void drdy_handler(struct work_struct *work){
	disable_irq(map_drdy);
	spi_write(ads1256, &command_read, 1);
		udelay(7);
	spi_read(ads1256, &rxx, 3);
	data = rxx[0]<<16;
	data |= rxx[1]<<8;
	data |= rxx[2];
	if(data & 0x800000){
		data |= 0xFF000000;
	}
	printk("%d, %d\n", count, data);
	adata[count] = data;
	tdata[count] = tcount;
	if (count<NN-1){
		count++;
	}
	else {
		count = 0;
	}
}

static irqreturn_t drdy_interrupt(int irq, void *data){
	if (!irq_status){
		return IRQ_HANDLED;
	}
	queue_work(wqs, &drdy_work);
	return IRQ_HANDLED;
}

static void sync_handler(struct work_struct *work){
	struct irq_data *drdy_data = irq_get_irq_data(map_drdy);
	if(irqd_irq_disabled(drdy_data)){
		enable_irq(map_drdy);
	}
	ktime_get_real_ts64(&ts);
	tcount = ts.tv_nsec;
}

static irqreturn_t sync_interrupt(int irq, void *data){
	if (!irq_status){
		return IRQ_HANDLED;
	}
	queue_work(wqs, &sync_work);
	return IRQ_HANDLED;
}

static struct file_operations spirev_ops = {
	.open = spirev_open,
	.release = spirev_release,
	.read = spirev_read,
	.write = spirev_write,
};

static int prepare_gpio_to_irq(int gpio){
	int ret = gpio_request_one(gpio, GPIOF_IN, NULL);
	if(ret){
		pr_err("Unable to request GPIO: %d.\n", ret);
		return ret;
	}
	ret = gpio_to_irq(gpio);
	return ret;	
}

static int __init spirev_init(void){
	int ret = 0;
	u8 mux[] = {0x51, 0x00, mux_single};
	u8 adc[] = {0x52, 0x00, adc_1};
	u8 drt[] = {0x53, 0x00, drt_500};
	struct spi_master *master;
	struct spi_board_info ads1256_info = {
		.modalias = "ads1256",
		.max_speed_hz = 3000000, //976000,
		.bus_num = BUS_NUM,
		.chip_select = 0,
		.mode = 1,
	};
	// Device registration
	major = register_chrdev(0, DEVICE_NAME, &spirev_ops);
	if (major < 0){
		pr_alert("Registering char device failed with %d.\n", major);
		return major;
	}
	pr_info("Get major number %d.\n", major);
	cls = class_create(THIS_MODULE, DEVICE_NAME);
	device_create(cls, NULL, MKDEV(major,0), NULL, DEVICE_NAME);
	// DRDY interrupt config
	map_drdy = prepare_gpio_to_irq(DRDY);
	ret = request_irq(map_drdy, drdy_interrupt, IRQF_TRIGGER_FALLING, "DRDY#inter", NULL);
	if(ret<0){
		pr_err("Unable to request IRQ: %d.\n", ret);
		gpio_free(DRDY);
	}
	disable_irq(map_drdy);
	// SYNC interrupt config
	map_sync = prepare_gpio_to_irq(SYNC);
	ret = request_irq(map_sync, sync_interrupt, IRQF_TRIGGER_FALLING | IRQF_PERCPU | IRQF_NOBALANCING | IRQF_FORCE_RESUME | IRQF_NO_THREAD, "SYNC#inter", NULL);
	if(ret<0){
		pr_err("Unable to request IRQ: %d.\n", ret);
		gpio_free(SYNC);
	}
	// SPI-connection config
	master = spi_busnum_to_master(BUS_NUM);
	if(!master){
		pr_err("There is no spi bus with No %d.\n", BUS_NUM);
		return -1;
	}
	ads1256 = spi_new_device(master, &ads1256_info);
	if(!ads1256){
		pr_err("Could not create device.\n");
		return -1;
	}
	ads1256 -> bits_per_word = 8;
	if(spi_setup(ads1256)!=0){
		pr_err("Could not change bus setup.\n");
		spi_unregister_device(ads1256);
		return -1;
	}
	// ADC registers config
	spi_write(ads1256, mux, 3);
	udelay(10);
	spi_write(ads1256, adc, 3);
	udelay(10);
	spi_write(ads1256, drt, 3);
	// Workqueue config
	wqs = alloc_workqueue("SPIREV-WQ", WQ_UNBOUND | WQ_HIGHPRI, 0);
	return 0;
}

static void __exit spirev_exit(void){
	flush_workqueue(wqs);
	destroy_workqueue(wqs);
	free_irq(map_drdy, NULL);
	gpio_free(DRDY);
	free_irq(map_sync, NULL);
	gpio_free(SYNC);
	spi_unregister_device(ads1256);
	device_destroy(cls, MKDEV(major,0));
	class_destroy(cls);
	unregister_chrdev(major, DEVICE_NAME);
}

static int spirev_open(struct inode *inode, struct file *file){
	try_module_get(THIS_MODULE);
	return 0;
}

static int spirev_release(struct inode *inode, struct file *file){
	module_put(THIS_MODULE);
	return 0;
}

static ssize_t spirev_read(struct file *file, char __user *buffer, size_t length, loff_t *offset){
	u32 *ptr;
	u32 *pptr;
	int start, end;
	ssize_t ret = 0;
	if (count<N){
		start = N;
		end = NN;
	}
	else {
		start = 0;
		end = N;
	}
	for (ptr = adata+start; ptr<adata+end; ptr++){
		if (copy_to_user(buffer+*offset, ptr, n32)){
			return -EFAULT;
		}
		else {
			ret+=n32;
			*offset+=n32;
		}
	}
	for (pptr = tdata+start; pptr<tdata+end; pptr++){
		if (copy_to_user(buffer+*offset, pptr, n32)){
			return -EFAULT;
		}
		else {
			ret+=n32;
			*offset+=n32;
		}
	}
	return ret;
}

static ssize_t spirev_write(struct file *file, const char __user *buffer, size_t len, loff_t *offset){
	ssize_t ret = 0;
	if (*offset>=sizeof(irq_status)){
		return -ENOSPC;
	}
	if (get_user(irq_status, buffer)) {
		return -EFAULT;
	}
	else {
		ret+=1;
	}
	*offset = 0;
	return ret;
}

module_init(spirev_init);
module_exit(spirev_exit);
MODULE_LICENSE("GPL");
Здесь уже более очевидно, что каждый новый сэмпл был вычислен сразу после того, как пришел синхроимпульс. Захвачено в приложении Saleae Logic
Здесь уже более очевидно, что каждый новый сэмпл был вычислен сразу после того, как пришел синхроимпульс. Захвачено в приложении Saleae Logic

Получается такой странный сигнал. Но это уже реальный успех! На этот раз в spirev_init мы объявляем прерывание на DRDY и тут же его запрещаем с помощью disable_irq. Прерывание на SYNC не запрещаем, его обработчики в конечном итоге должны выполнить enable_irq до смены логического уровня на DRDY. А, перед этим нужно обязательно выполнять проверку, что прерывание на DRDY точно запрещено, в моей версии ядра это делается вводом новой структуры irq_data, а проверяется структура в irqd_irq_disabled (в других версиях это может выполняться по-другому). Особенность функций разрешения и запрета в том, что что они сгруппированы, если disable_irq вызвана один раз – enable_irq вызывается один раз, если первая вызвана два раза подряд – вторая тоже вызывается дважды, и только после этого прерывания будут разрешены. Очень легко допустить здесь ошибку и заруинить систему, для этого и пришлось разобраться с проверкой. С помощью DECLARE_WORK я объявляю два задания для постобработки, которые, тем не менее, выполняются в одной очереди wqs. В обработчике второго задания, drdy_handler, я снова запрещаю прерывания на DRDY, и мусорные импульсы становятся нипочем, пока не придет новый строб SYNC.

Как костыли натыкаются на подводные камни (О проблемах)

Отлично, всё работает! И первые несколько минут работы не выявляют каких-то скрытых проблем. А что по поводу часа, суток? Нужно каким-то образом проанализировать длительную запись. Я сделал следующее: аналоговые входы АЦП подключил к генератору, оцифровывал синусоиду или пилу с частотой 1 Гц. Очевидно, каждую секунду (или 50 сэмплов) записанный сигнал должен повторяться, и каждый такой фрагмент должен в идеале быть максимально похожим на предыдущий, а степень их похожести можно оценить с помощью критерия Пирсона. Сигнал с генератора писал около суток, иногда меньше.

Оценивание соседних фрагментов по критерию Пирсона с помощью функции scipy.stats.pearsonr, когда фрагменты совпадают – значение близко к ''1''
Оценивание соседних фрагментов по критерию Пирсона с помощью функции scipy.stats.pearsonr, когда фрагменты совпадают – значение близко к ''1''

И что бы вы думали, в Пирсоне возникли скачки! Чтобы с этим бороться, пришлось в буфер вместе с амплитудой класть еще и время системы. С дробным величинами в ядре работать нельзя, поэтому в Linux есть структура timespec64, которая содержит два значения – количество целых секунд и отдельно число наносекунд (0…999,999,999 – максимум девять знаков, да) системного времени, вот наносекунды я и пишу в массив tdata. Если между соседними отсчетами времени разница всегда примерно 20 мс – всё гуд.

При тестировании системы в лабораторных условиях наблюдались ''скачки'' в Пирсоне
При тестировании системы в лабораторных условиях наблюдались ''скачки'' в Пирсоне

Кстати, эксплуатация в реальных, а не лабораторных, условиях показывает, что никаких ''лишних'' сэмплов на самом деле не возникает, ни на одном канале ничего такого не было. А что это было? Я грешу на плохой контакт при сборке лабораторного стенда. И теперь, получается, буфер времени не особо нужен… из своего кода я эту часть со временем уберу, но для Хабра, так уж и быть, оставлю.

Тестирование в полевых условиях подтвердило хорошую согласованность между раздельными каналами. Теперь победа?! Нет, к сожалению, возникла еще одна неприятность, с которой я так пока и не разобрался. Большие по амплитуде сигналы (3 вольта пик-пик, например) АЦП оцифровывает нормально, но маленькие сигналы съедает странная помеха. Она вроде и случайная, если рассмотреть ее на малом интервале времени, но при длительных измерениях видно, что образуется ''лесенка'' строго определенной амплитуды. И, самое главное, помеха появляется в случайные моменты времени, из-за чего фильтровать ее становится почти нереально.

Цифруем меандр 200…205 мВ частотой 1 Гц. Во втором случае работа с АЦП осуществляется в python, без модулей ядра: посылается команда RDATAC, после чего микросхема входит в режим бесконечной генерации новых значений, остается также в python следить за флагом DRDY и вовремя забирать данные. SYNC просто подтянут к питанию
Цифруем меандр 200…205 мВ частотой 1 Гц. Во втором случае работа с АЦП осуществляется в python, без модулей ядра: посылается команда RDATAC, после чего микросхема входит в режим бесконечной генерации новых значений, остается также в python следить за флагом DRDY и вовремя забирать данные. SYNC просто подтянут к питанию

Код работы с АЦП ''без тактирования'', считаю, не заслуживает особого внимания, похожий код можно найти здесь. На картинках показан меандр с размахом по амплитуде 5 мВ – слабый сигнал, но, очевидно, это не предел для АЦП, ведь его разрешающая способность 10В / 2^24 = 0.6 мкВ (входной сигнал -5В…+5В, поэтому в формуле 10) в режиме без программного усиления (PGA), а с усилением – еще круче. Тем не менее, вот: если не трогать SYNC, то сигнал чистый. Кроме этого, можно видеть, что два меандра смещены по амплитуде по-разному (генератор честно выдает нижний предел 200 мВ), но на такие загадки природы у меня тем более нет ответа.

Помеха появляется независимо от источника, тактирующего вход SYNC (подключал лабораторный генератор, выход PPS GNSS-приемника), независимо от источника аналогового сигнала, независимо от питания АЦП и малинки. Также это не связано с программным усилением (пробовал PGA1-2 – без разницы). Есть подозрение, что дело в неправильной эксплуатации АЦП, в нарушении внутренних алгоритмов работы. И есть у меня как минимум одна идея, как можно с этим бороться: в spirev v.2.0 я уже более гибко управляю прерываниями, а, значит, могу до наступления следующего строба SYNC получить несколько результатов преобразования на частоте 500 или 1000 SPS, а потом их просто усреднить. Но это погружение в еще более костыльные дебри, возможно, задача решается гораздо проще.

Итог

Противоречивый пост вышел. Вроде, хорошая получилась штуковина, подходит для создания распределенного в пространстве массива регистраторов, может помогать ученым мониторить какие-либо геофизические процессы. Дешевая, $200 за один комплект (это если брать дорогой приемник NEO-M8T). Точно срабатывает, но очень шумная, увы. Надеюсь довести ее до ума, тем более, если сообщество мне поможет.

Хочу поблагодарить пользователей @Bright_Translate и @Lampus за циклы статей по программированию в ядре Линукс. Материалы других авторов на других ресурсах, которые очень выручали на ранних этапах разработки:

  1. Собственный SPI драйвер для Linux на Raspberry Pi

  2. Linux - Доступ к шине SPI из пространства пользователя

  3. SPI Device Driver Tutorial – Linux Device Driver Tutorial Part 47

  4. Модули ядра Linux: пример символьного устройства

  5. Взаимодействие с файлами устройств

Особая благодарность Роману В., Максиму А. и Александру П. за то, что направляли и поддерживали. Ваньку привет!

Материалы, на которые ссылаюсь в посте

  1. Waveshare High Precision AD-DA board

  2. ADS1256 datasheet

  3. Пособие по программированию модулей ядра Linux

  4. Роберт Лав "Ядро Linux. Описание процесса разработки"

  5. Код от разработчиков Waveshare

  6. Ошибка "chipselect 0 already in use" Raspberry Pi

Собственно, ознакомиться с проектом можно ЗДЕСЬ.

Комментарии (10)


  1. Bright_Translate
    31.10.2025 12:53

    Хороший проект. Успехов!