В этой статье (перевод документации [1]) приведен пример обмена с внешними устройствами по шине SPI на слое приложения. Архив с кодом можно скачать с Google-диска Архив с кодом можно скачать с Google-диска (см. ссылку на Code.zip в оригинальной документации [1]).
[1. Подсистема SPI]
В операционной системе Linux подсистема SPI представляет собой ключевой фреймворк драйверов, используемый для управления и контроля различных внешних устройств, подключенных через шину SPI. Для получения более подробной информации о подсистеме SPI можно обратиться к каталогу /Documentation/spi.
Ключевые компоненты подсистемы SPI:
• Интерфейс sysfs: подсистема SPI представлена набором файлов и директорий через файловую систему sysfs, предназначенных для конфигурирования и обслуживания шин SPI и устройств SPI. Эти файлы и директории обычно находятся в каталогах /sys/class/spi_master и /sys/bus/spi/devices, давая пользователям возможность просматривать и модифицировать свойства устройств SPI.
[root@luckfox ]# ls /sys/class/spi_master
spi2
[root@luckfox ]# ls /sys/bus/spi/devices
spi2.0
• Device Nodes: каждое подключенное устройство SPI создает device в директории /dev, разрешая обмен с устройством через программы рабочего пространства пользователя (user-space) с использования стандартных файловых операций ввода/вывода. Обычно эти device nodes имеют имена наподобие /dev/spidevX.Y, где X представляет номер шины SPI, а Y представляет номер устройства SPI.
[2. Просмотр SPI через шелл]
2.1. Цоколевка выводов платы. На платах разработчика обычно по умолчанию разрешен интерфейс SPI0. Вы можете определить соответствующие выводы интерфейса по картинкам, приведенным ниже. Например, у LuckFox Pico интерфейс SPI0 соответствует номерам выводов 48, 49, 50, 51 и 58 (вывод 58 изначально запрещен).
Цоколевка LuckFox Pico:
Цоколевка LuckFox Pico Mini A/B:
Цоколевка LuckFox Pico Plus:
Цоколевка LuckFox Pico Pro/Max:
Цоколевка LuckFox Pico Ultra/Ultra W:
2.2. Просмотр устройств. В директории /sys/bus/spi/devices у каждого SPI-устройства есть своя отдельная папка. Эти папки в своем имени обычно содержат "spi" и номер устройства. Например, /sys/bus/spi/devices/spi0.0 представляет устройство 0 на SPI-шине 0. Для просмотра доступных в системе шин SPI можно использовать команду ls:
# ls /sys/bus/spi/devices/
spi2.0 spi0.0
[3. SPI-обмен в программе Python]
3.1. В следующей программе реализован обмен данными через SPI.
3.2. Открытие устройства SPI. Показанный выше код использует класс SpiDev из библиотеки spidev для создания объекта SPI. В вызове метода open указывается номер шины SPI и номер устройства, в этом примере SPI bus 0 и device 0. Максимальная скорость обмена SPI установлена в значение 1000000 Гц, т. е. 1 МГц. На этом шаге происходит конфигурирование базовых параметров устройства SPI, необходимое для последующего обмена данными.
3.3. Прием и передача данных. Код приведенного выше примера использует метод xfer2 для передачи данных через SPI. Здесь tx_buffer это отправляемые данные, а в rx_buffer будут сохраняться принимаемые данные. Обратите внимание, что оригинальное значение в tx_buffer остается неизмененным, в xfer2 передается копия, т. е. tx_buffer[:]. И наконец, оператор print выводит в лог отправленные и принятые данные как строки, чтобы пользователь мог с ними ознакомиться.
3.4. Запуск программы. Используйте текстовый редактор vi (или nano), чтобы скопировать текст программы и вставить его в файл. После этого сохраните программу как файл i2c.py.
# nano i2c.py
Запуск программы осуществляется командой:
# python3 i2c.py
Соедините друг с другом выводы MOSI и MISO интерфейса SPI и запустите программу. Вы увидите такой лог:
4.1. Функция ioctl. При написании программы приложения функция ioctl используется для конфигурирования настроек SPI. Прототип этой функции следующий:
#include
intioctl(intfd,unsignedlongrequest,...);
Когда функция ioctl используется для обмена через SPI, обычно применяют следующие параметры запроса:
SPI_IOC_RD_MODE: используется для чтения настроек текущего режима обмена SPI. Этот параметр запроса читает информацию режима в целочисленную переменную для проверки текущих настроек полярности и фазы SPI (CPOL и CPHA).
SPI_IOC_WR_MODE: используется для установки режима обмена SPI. Вам нужно предоставить целочисленное значение, обычно составленное из двух двоичных цифр для представления полярности и фазы обмена SPI.
SPI_IOC_RD_BITS_PER_WORD: используется для чтения количества бит в слове данных. Прочитанное значение будет сохранено в предоставленную целочисленную переменную.
SPI_IOC_WR_BITS_PER_WORD: используется для установки количества бит в слове данных. Вам нужно предоставить целочисленное значение, чтобы указать размер передаваемого и принимаемого слова в битах.
SPI_IOC_RD_MAX_SPEED_HZ: используется для чтения максимальной скорости данных (частоты тактов) шины SPI. Этот параметр запроса считывает информацию скорости в целочисленную переменную.
SPI_IOC_WR_MAX_SPEED_HZ: используется для установки максимальной скорости шины SPI. Вам нужно предоставить целочисленное значение для указания частоты тактов шины SPI.
SPI_IOC_MESSAGE(N): используется для выполнения операций чтения и записи по шине SPI. Этот параметр запроса требует указателя на массив элементов структур spi_ioc_transfer, где каждый элемент описывает операцию транзакции SPI, благодаря чему можно выполнить несколько операций.
4.2. Следующая программа демонстрирует обмен данными SPI на языке C.
// Открытие устройства SPI: if((spi_file=open(SPI_DEVICE_PATH,O_RDWR))<0){ perror("Failed to open SPI device"); return-1; }
// Конфигурирование режима SPI и количества бит в слове данных: uint8_tmode=SPI_MODE_0; uint8_tbits=8; if(ioctl(spi_file,SPI_IOC_WR_MODE,&mode)<0){ perror("Failed to set SPI mode"); close(spi_file); return-1; } if(ioctl(spi_file,SPI_IOC_WR_BITS_PER_WORD,&bits)<0){ perror("Failed to set SPI bits per word"); close(spi_file); return-1; }
// Подготовка транзакции SPI: structspi_ioc_transfertransfer={ .tx_buf=(unsignedlong)tx_buffer, .rx_buf=(unsignedlong)rx_buffer, .len=sizeof(tx_buffer), .delay_usecs=0, .speed_hz=1000000,// скорость SPI в Гц .bits_per_word=8,};
if(ioctl(spi_file,SPI_IOC_MESSAGE(1),&transfer)<0){ perror("Failed to perform SPI transfer"); close(spi_file); return-1; }
// Вывод содержимого буферов передачи и приема: printf("\rtx_buffer: \n %s\n ",tx_buffer); printf("\rrx_buffer: \n %s\n ",rx_buffer);
// Закрытие устройства SPI: close(spi_file); return0;
}
4.3. Файловые пути. В следующей строке кода определен макрос, используемый для сохранение пути к файлу устройства SPI.
#define SPI_DEVICE_PATH "/dev/spidev0.0"
4.4. Открытие устройства SPI. Следующая секция кода делает попытку открыть указанный файл устройства SPI.
if((spi_file=open(SPI_DEVICE_PATH,O_RDWR))<0){ perror("Failed to open SPI device"); return-1;
}
4.5. Конфигурирование SPI. Этот код используется для конфигурирования режима обмена SPI Mode 0 (полярность тактов 0, фаза тактов 0) и установки размера слова данных 8 бит. Это обеспечивает необходимые параметры корректного обмена данными SPI.
uint8_tmode=SPI_MODE_0; uint8_tbits=8;
if(ioctl(spi_file,SPI_IOC_WR_MODE,&mode)<0){ perror("Failed to set SPI mode"); close(spi_file); return-1;
} if(ioctl(spi_file,SPI_IOC_WR_BITS_PER_WORD,&bits)<0){ perror("Failed to set SPI bits per word"); close(spi_file); return-1;
}
В переменной структуры типа spi_ioc_transfer с именем transfer задаются параметры транзакции SPI. Указываются адреса буферов передачи и приема, длина транзакции, задержка в микросекундах, частота тактов SPI в Герцах и размер слова данных. Эта структура будет передана в SPI_IOC_MESSAGE функции ioctl, чтобы выполнилась транзакция SPI.
structspi_ioc_transfertransfer={ .tx_buf=(unsignedlong)tx_buffer, .rx_buf=(unsignedlong)rx_buffer, .len=sizeof(tx_buffer), .delay_usecs=0, .speed_hz=1000000,// скорость SPI в Гц .bits_per_word=8,
};
4.6. Отправка данных. Следующий код использует функцию ioctl для выполнения транзакции SPI. Указывается количество транзакций числом 1 с помощью макроса SPI_IOC_MESSAGE(1), с третьим аргументом, указывающим на адрес переменной структуры spi_ioc_transfer. Если транзакция SPI оказалась неудачной, то печатается сообщение об ошибке, и закрывается дескриптор файла устройства SPI.
if(ioctl(spi_file,SPI_IOC_MESSAGE(1),&transfer)<0){ perror("Failed to perform SPI transfer"); close(spi_file); return-1;
}
4.7. Кросс-компиляция. Процесс компиляции происходит на хосте, где установлен SDK LuckFox [].
1. Подготовка инструментария для компиляции (Cross-Compilation Tool).
Сначала вам нужно добавить путь до утилит кросс-компиляции в переменную окружения PATH, чтобы можно было запускать эти утилиты из любой папки на диске. Вы можете сделать это, добавив следующую строку с файл конфигурации вашего шелла (обычно ~/.bashrc, или ~/.bash_profile, или ~/.zshrc, в зависимости от того, какой шелл используется), либо подготовив скрипт, который делает экспорт модифицированной переменной окружения PATH. Обатите внимание, что путь до инструментов кросс-компиляции должен быть вставлен сразу после PATH=.
Предположим, что SDK находится в профиле текущего пользователя, в папке по абсолютному пути /home/username/luckfox-pico, тогда строка экспорта должна выглядеть так:
После успешного завершения компиляции полученный исполняемый код в файле spi можно запустить на вашей целевой плате LuckFox.
# ls
spi spi.c
4.8. Запуск программы на плате LuckFox.
1. Передача файла. Сначала нам нужно передать файл скомпилированной программы в память платы LuckFox. Передать файл можно через TFTP или ADB. Ниже показан пример команды для передачи файла spi в каталог платы /oem/spitest с помощью ADB:
$ adb push ./spi /oem/spitest
2. Запуск программы. Для запуска программы запустите шелл на плате, поменяйте права доступа к файлу и запустите программу:
Интерфейс SPI изначально разрешен по умолчанию в файлах дерева устройств SDK, так что вы можете использовать их непосредственно. Однако вы можете изменить конфигурацию выводов SPI, как описано далее. Когда через SPI подключается несколько устройств, вы можете выбрать использование вывода CS как порт GPIO, и независимо управлять уровнем сигнала выборки CS. Например, вы можете сконфигурировать GPIO1_C0_d как обычный порт ввода-вывода (general-purpose IO, GPIO).
5.1. Модификация файла дерева устройств.
1. Файлы конфигурации устройств всех плат находятся в каталоге:
/project/cfg/BoardConfig_IPC/
Каждый *.mk файл в этом каталоге описывает конфигурационные параметры отдельной модели платы Luckfox Pico, такие как целевая архитектура (target architecture), загрузочный носитель (boot medium), используемый загрузчик (Uboot), ядро (kernel) и настройки таблицы разделов (partition settings).
Переменная RK_KERNEL_DTS файла конфигурации платы указывает файл дерева устройств (Device Tree Source, DTS) для ядра. Если взять в качестве примера Luckfox Pico и открыть его файл конфигурации BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk, то мы увидим, что переменная RK_KERNEL_DTS указывает на файл rv1103g-luckfox-pico.dts.
Основываясь на значении переменной RK_KERNEL_DTS, путь файла дерева устройств платы Luckfox Pico определен следующим образом:
2. Определение GPIO. Определение выводов GPIO обычно требует добавление двух сегментов кода в файл дерева устройств. Ниже приведен пример определения вывода GPIO1_C0_d:
3. Комментирование функции периферийного устройства для вывода. В соответствии с цоколевкой выводов вашей платы (см. выше картинки в разделе "2.1. Цоколевка выводов платы") вы можете обнаружить, что для вывода GPIO1_C0_d по умолчанию назначена функция PWM. Но в своей программе мы хотим сконфигурировать этот вывод как GPIO, так что требуется закомментировать функцию периферийного устройства этого вывода. Ниже показан пример, как запретить функцию PWM вывода GPIO1_C0_d в файле дерева устройств:
4. Конфигурирование SPI. Интерфейс SPI0 по умолчанию разрешен в файлах дерева устройств SDK. Здесь мы не будем настраивать GPIO1_C0_d в качестве вывода CS для SPI0 (см. комментарий к строке 167). Ниже приведен пример настройки SPI в дереве устройств:
Конфигурирование компиляции осуществляется выбором в меню скрипта ./build.sh lunch одной из конфигураций для плат LuckFox Pico, LuckFox Pico Mini A, LuckFox Pico Mini B, LuckFox Pico Plus и LuckFox Pico Pro/Max:
~/luckfox-pico$ ./build.sh lunch
ls: cannot access 'BoardConfig*.mk': No such file or directory
You're building on Linux
Lunch menu...pick a combo:
BoardConfig-*.mk naming rules:
BoardConfig-"Boot Media"-"Power Plan"-"Hardware Version"-"Application Scenario".mk
BoardConfig-"boot medium"-"power solution"-"hardware version"-"applicaton".mk
----------------------------------------------------------------
0. BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
boot medium: EMMC
power solution (power plan): NONE
hardware version: RV1103_Luckfox_Pico
applicaton (scenario): IPC
----------------------------------------------------------------
----------------------------------------------------------------
1. BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
boot medium: EMMC
power solution (power plan): NONE
hardware version: RV1103_Luckfox_Pico_Mini_A
applicaton (scenario): IPC
----------------------------------------------------------------
----------------------------------------------------------------
2. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
boot medium: SPI_NAND
power solution (power plan): NONE
hardware version: RV1103_Luckfox_Pico_Mini_B
applicaton (scenario): IPC
----------------------------------------------------------------
----------------------------------------------------------------
3. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
boot medium: SPI_NAND
power solution (power plan): NONE
hardware version: RV1103_Luckfox_Pico_Plus
applicaton (scenario): IPC
----------------------------------------------------------------
----------------------------------------------------------------
4. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
boot medium: SPI_NAND
power solution (power plan): NONE
hardware version: RV1106_Luckfox_Pico_Pro_Max
applicaton (scenario): IPC
----------------------------------------------------------------
Which would you like? [0]: 0
[build.sh:info] switching to board: /home/luckfox/luckfox-pico/project/cfg/BoardConfig_IPC/ BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
[build.sh:info] Running build_select_board succeeded.
В этом примере был выбран вариант 0, т. е. плата Luckfox Pico.
2. Перекомпилируйте ядро командой:
~/luckfox-pico$ ./build.sh kernel
5.3. Повторная прошивка firmware.
1. После того, как ядро было успешно скомпилировано, сгенерированные файлы будут находиться в директории /output/image:
// Определение аргументов командной строки с помощью argtable structarg_str*arg_dev=arg_str0(NULL,"dev","","SPI device path"); structarg_str*arg_text=arg_str0(NULL,"text","","Text to send"); structarg_int*arg_cpol=arg_int0(NULL,"cpol",NULL,"SPI clock polarity (0 or 1)"); structarg_int*arg_cpha=arg_int0(NULL,"cpha",NULL,"SPI clock phase (0 or 1)"); structarg_int*arg_speed=arg_int0(NULL,"speed",NULL,"SPI clock speed in Hz"); structarg_int*arg_bits=arg_int0(NULL,"bits",NULL,"Bits per word"); structarg_lit*arg_help=arg_lit0("h","help","Show help"); structarg_end*arg_end_ptr=arg_end(20); void*argtable[]={ arg_dev,arg_text,arg_cpol,arg_cpha,arg_speed,arg_bits,arg_help,arg_end_ptr };
if(arg_text->count>0){ constchar*input_text=arg_text->sval[0]; if(input_text==NULL||input_text[0]=='\0'){ fprintf(stderr,"Error: Text cannot be empty\n"); arg_freetable(argtable,sizeof(argtable)/sizeof(argtable[0])); return1; } if(strlen(input_text)>=MAX_TEXT_LENGTH){ fprintf(stderr,"Error: Text too long (max %d characters)\n",MAX_TEXT_LENGTH-1); arg_freetable(argtable,sizeof(argtable)/sizeof(argtable[0])); return1; } strncpy(tx_text,input_text,MAX_TEXT_LENGTH-1); tx_text[MAX_TEXT_LENGTH-1]='\0';// Ensure null termination }
if(arg_cpol->count>0){ intcpol=arg_cpol->ival[0]; if(cpol<0||cpol>1){ fprintf(stderr,"Error: CPOL must be 0 or 1\n"); arg_freetable(argtable,sizeof(argtable)/sizeof(argtable[0])); return1; } mode=(mode&~SPI_CPOL)|(cpol?SPI_CPOL:0); }
if(arg_cpha->count>0){ intcpha=arg_cpha->ival[0]; if(cpha<0||cpha>1){ fprintf(stderr,"Error: CPHA must be 0 or 1\n"); arg_freetable(argtable,sizeof(argtable)/sizeof(argtable[0])); return1; } mode=(mode&~SPI_CPHA)|(cpha?SPI_CPHA:0); }
if(arg_speed->count>0){ speed=arg_speed->ival[0]; if(speed< =0){ fprintf(stderr,"Error: Speed must be positive\n"); arg_freetable(argtable,sizeof(argtable)/sizeof(argtable[0])); return1; } }
if(arg_bits->count>0){ bits=arg_bits->ival[0]; if(bits<1||bits>32){ fprintf(stderr,"Error: Bits must be between 1 and 32\n"); arg_freetable(argtable,sizeof(argtable)/sizeof(argtable[0])); return1; } }
// Освобождение памяти argtable arg_freetable(argtable,sizeof(argtable)/sizeof(argtable[0]));
// Основная логика программы intspi_file; uint8_ttx_buffer[MAX_TEXT_LENGTH]; uint8_trx_buffer[MAX_TEXT_LENGTH]={0};
// Копируем текст в буфер передачи size_ttext_length=strlen(tx_text); memcpy(tx_buffer,tx_text,text_length+1);// +1 для нулевого терминатора printf("SPI Configuration:\n"); printf(" Device: %s\n",device_path); printf(" Mode: 0x%02X (CPOL=%d, CPHA=%d)\n",mode, (mode&SPI_CPOL)?1:0, (mode&SPI_CPHA)?1:0); printf(" Speed: %d Hz\n",speed); printf(" Bits per word: %d\n",bits); printf(" Text to send: \"%s\"\n",tx_text); printf(" Text length: %zu bytes\n",text_length);
// Открытие устройства SPI if((spi_file=open(device_path,O_RDWR))<0){ perror("Failed to open SPI device"); return-1; }
// Конфигурирование режима SPI if(ioctl(spi_file,SPI_IOC_WR_MODE,&mode)<0){ perror("Failed to set SPI mode"); close(spi_file); return-1; }
// Конфигурирование количества бит в слове данных if(ioctl(spi_file,SPI_IOC_WR_BITS_PER_WORD,&bits)<0){ perror("Failed to set SPI bits per word"); close(spi_file); return-1; }
// Конфигурирование скорости SPI if(ioctl(spi_file,SPI_IOC_WR_MAX_SPEED_HZ,&speed)<0){ perror("Failed to set SPI speed"); close(spi_file); return-1; }
// Вывод содержимого буферов передачи и приема printf("TX buffer: %s\n",tx_buffer); printf("RX buffer: %s\n",rx_buffer);
// Закрытие устройства SPI close(spi_file);
return0;
}
Без опций программа посылает текст "hello world" через устройство /dev/spidev0.0 и отображает принятые данные. Таким образом, если соединить MOSI и MISO, то будет выведен вот такой результат:
[root@luckfox spitest]# ./spi
SPI Configuration:
Device: /dev/spidev0.0
Mode: 0x00 (CPOL=0, CPHA=0)
Speed: 1000000 Hz
Bits per word: 8
Text to send: "hello world!"
Text length: 12 bytes
TX buffer: hello world!
RX buffer: hello world!
Программа также позволяет настраивать передаваемый текст и параметры соединения SPI:
[root@luckfox spitest]# ./spi --help
Usage: ./spi [OPTIONS]
Options:
--dev SPI device path (default: /dev/spidev0.0)
--text Text to send (default: "hello world!")
--cpol SPI clock polarity (default: 0)
--cpha SPI clock phase (default: 0)
--speed SPI clock speed in Hz (default: 1000000)
--bits Bits per word (default: 8)
--help Show this help message
К сожалению, в качестве параметра --bits можно указывать только значения 8 или 16.
Чтобы скомпилировать программу, необходим установленный SDK LuckFox и настроенная переменная окружения PATH, указывающая путь до тулчейна. Для копирования исполняемого файла spi на плату LuckFox Pico Mini необходим установленный adb.
Содержимое каталога проекта:
argtable3 библиотека arctable3 build.sh скрипт для сборки и копирования бинарника exp.sh скрипт, настраивающий переменную PATH makefile скрипт для команды make readme.txt этот текст spi.c исходный код этой программы
[Устройство /dev/spidev0.0]
По умолчанию устройство /dev/spidev0.0 на плате LuckFox Pico Mini запрещено. Чтобы его разрешить, нужно исправить файл дерева устройств sysdrv/source/kernel/arch/arm/boot/dts/rv1103g-luckfox-pico-mini.dts (находится в корне SDK):
После внесения этих изменений перекомпилируйте SDK в режиме Buildroot (с использованием файла конфигурации BoardConfig-SPI_NAND-Buildroot-RV1103_Luckfox_Pico_Mini-IPC.mk, он находится в папке luckfox-pico/project/cfg/BoardConfig_IPC/ каталога установки SDK). Затем перепрошейте плату файлом update.img (из папки luckfox-pico/output/image/).
Сигналы SPI будут доступны на контактах 6 .. 9 платы LuckFox Pico Mini A/B (см. выше цоколевку):
Почему устройство SPI ничего не передает? Вероятная причина ошибки в том, что содержимое буфера передачи не настраивается до вызова ioctl(spi_file, SPI_IOC_MESSAGE(1), &transfer).