Русский
Русский
English
Статистика
Реклама

Spi

Подключаем дисплей SPI LCD ILI9341 к одноплатному компьютеру Banana Pi BPI-M64 или любому другому на ОС Armbian

15.03.2021 20:13:27 | Автор: admin
LCD SPI ILI9341 Banana Pi BPI-M64

Пост содержит инструкцию как подключить TFT-LCD дисплей на популярном контроллере ILI9341 к одноплатному компьютеру на ОС Armbian с помощью дерева устройств (Device Tree overlays) без танцев с бубном. В сети Интернет много материала как подключать различные LCD экраны к Raspberry Pi. Но что если у вас нет Raspberry Pi, а хочется подключить недорогой LCD экран на SPI интерфейсе? Все что вам необходимо, это любая плата с поддержкой ОС Armbian. В каталог поддерживаемых плат ОС Armbian входят платы: Asus, Pine64, Hardkernel, Orange Pi, Banana Pi, и т.д. На данный момент в каталоге более 114 моделей плат, объявлена поддержка различного оборудования из коробки. Доступны для подключения: 4G/LTE модемы, USB Wi-Fi, USB Bluetooth, USB Ethernet, сканеры DVB-тюнеры и т.д. К всем этим платам можно легко подключить SPI LCD дисплей ILI9341, как это реализовать прошу под кат.

Преамбула


Многие одноплатные компьютеры снабжены HDMI выходом, но подключение полноценного дисплея с поддержкой HDMI входа достаточно дорогое удовольствие для небольшого проекта. В особенности, если необходимо реализовать минимальный функционал взаимодействия с пользователем, терминал распечатки документов, вывод текущего статуса работающего приложения. Для подобных задач можно использовать символьные дисплеи LCD HD44780 на интерфейсе I2C, они достаточно дешевы и удобны. Но в тоже время сильно ограничены в функциональности, на эти экраны невозможно вывести консоль Linux и нативный UI приложения, вдобавок площадь LCD экрана нельзя использовать как панель ввода информации. Для решения этих задач прекрасно подойдут LCD экраны на SPI интерфейсе, дисплей диагональю 3.5 дюйма с резистивным слоем можно приобрести за 9.57$ (включая доставку). На LCD экран можно выводить консоль Linux и подсистему X11. Таким образом, использование SPI LCD является лучшим вариантом по соотношению функциональности к стоимости.

Дисплей ILI9341 2.2 inch 2.2" SPI TFT


Контроллер ILI9341 предназначен для управления TFT панелью. Под контроллер ILI9341 поставляются панели диагональю от 2.2 до 3.2 дюймов, разрешение 240x320, к некоторым LCD добавляют резистивный слой.
К одноплатному компьютеру Banana Pi BPI-M64 будем подключать модуль SPI LCD ILI9341 диагональю 2.4 дюйма без резистивного слоя.

Рассмотрим характеристики и распиновку SPI LCD ILI9341 2.4 inch

  • 2,4-дюймовый цветной экран, поддержка 65K цветов
  • разрешение 320X240
  • интерфейс подключения SPI
  • доступен слот для SD-карты
  • питание модуля 3.3V~5V
  • Напряжение управления логикой 3.3V(TTL)

Контакты подключения LCD
Number Pin Label Description
1 VCC 5V/3.3V power input
2 GND Ground
3 CS LCD chip select signal, low level enable
4 RESET LCD reset signal, low level reset
5 DC/RS LCD register / data selection signal,high level: register, low level: data
6 SDI(MOSI) SPI bus write data signal
7 SCK SPI bus clock signal
8 LED Backlight control, high level lighting,if not controlled, connect 3.3V always bright
9 SDO(MISO) SPI bus read data signal, if you do not need to the read function, you can not connect it

Для управления подсветки используется контакт номер 8 LED. Максимальное напряжение 3.3V соответствует максимальной яркости от общего питания VCC. Если необходимо задать 50% яркости экрана, то на LED необходимо подать напряжение в 1.65V. Для программного управления яркости подсветки необходимо контакт LED подключать к аналоговому выходу GPIO на одноплатном компьютере. В случае наличия только цифровых выходов, доступна лишь возможности включить или полностью выключить подсветку экрана.

Исходя из характеристик LCD экрана предъявляются следующие требования к одноплатному компьютеру:
  • наличие SPI интерфейса
  • напряжение логики на контактах 3.3V (большинство плат)
  • потребуется еще два (RESET, DC/RS) свободных контактов GPIO

Что ты за зверь Armbian и какой одноплатный компьютер необходим


На странице armbian.com/download представлено большое количество разнообразных одноплатных компьютеров. С точки зрения удобства подключения, лучше выбирать плату с 40-контактным разъемом GPIO совместимым с Raspberry Pi 3. Например, если подключать SPI LCD ILI9341 2.4 inch к плате Banana Pi BPI-M64 и Orange Pi PC, то номера физически подключаемых контактов GPIO будут совпадать (не путать с названиями контактов процессора, они будут различны, далее потребуется для конфигурирования). В случае если одноплатный компьютер будет построена не на процессоре Allwinner, то возможно потребуется изменять больше параметров в файле: sun50i-a64-spi-ili9341-led-always-on.dts (будет далее по тексту).

Armbian это самый популярный дистрибутив Linux, предназначенный для одноплатных компьютеров построенных на ARM процессоре, список поддерживаемых плат огромен: Orange Pi, Banana Pi, Odroid, Olimex, Cubieboard, Roseapple Pi, Pine64, NanoPi и др. Дистрибутив Armbain основан на Debian и Ubuntu.

После явления миру Raspberry Pi, китайские производители решили тоже влиться в движение Open Hardware Source, и сделали много разнообразных плат. Но программная поддержка была крайне слабой, для решения данной проблемы зародился проект Armbian. На данный момент Armbian уже исполнилось 7 лет, поддерживается 114 моделей плат, объявлена поддержка различного оборудования из коробки. Доступны для подключения: 4G/LTE модемы, USB Wi-Fi, USB Bluetooth, USB Ethernet, сканеры DVB-тюнеры и т.д.

Для запуска Armbian на одноплатном компьютере необходимо загрузить образ с сайта, затем скопировать его на microSD карту, с которой в последствие нужно будет загрузиться. Если на плате размещена eMMC память достаточного объема, то через утилиту armbian-config, операционная система легко переносится с microSD карты на eMMC память вместе с загрузчиком.

Создание IoT-проекта с использованием Armbian в отличие от Raspberry Pi, позволяет выбирать платы различающие по производительности, и набора периферии. Например, на всех версиях Raspberry Pi размещен только один Ethernet порт. Но если требуется сделать маршрутизатор с несколькими Ethernet портами, то из списка поддерживаемых плат Armbian подойдут модели: Helios64, Espressobin, Bananapi R2, и т.д.

Поддерживаемые SoC
  • Allwinner A10, A20, A31, H2+, H3, H5, H6, A64
  • Amlogic S805 and S905 (Odroid boards), S802/S812, S805, S905, S905X and S912 (fork by @balbes150)
  • Actionsemi S500
  • Freescale / NXP iMx6
  • Marvell Armada A380
  • Rockchip RK3288/RK3328/RK3399
  • Samsung Exynos 5422

Схема подключения SPI LCD ILI9341 2.4 inch к Banana Pi BPI-M64 (порт GPIO Raspberry Pi 3)


SPI интерфейс LCD экрана подключаем к SPI1 на Banana Pi BPI-M64. Контакты CS, RESET, DC/RS можно подключать к любым цифровым выводам.

Таблица контактов подключения:
Номер LCD Метка LCD Номер контакта на Banana Pi BPI-M64 (порт GPIO Raspberry Pi 3)
1 VCC 1 или 2 (если необходима максимальная яркость, то контакт 2 на 5V)
2 GND 39, или любой другой Ground
3 CS 24
4 RESET 18
5 DC/RS 22
6 SDI(MOSI) 19
7 SCK 23
8 LED 1 или любой свободный GPIO на 3.3V
9 SDO(MISO) 21

Если контакт LED подключать к цифровому выводу GPIO, то для включения подсветки вручную потребуется подавать логическую 1 для включения или 0 для выключения экрана.

Схема подключения SPI LCD ILI9341:
LCD SPI ILI9341 Banana Pi BPI-M64

Одноплатный компьютер Banana Pi BPI-M64


Banana Pi BPI-M64 это 64-битный четырехъядерный мини-одноплатный компьютер, поставляемый как решение с открытым исходном кодом. Ядром системы является процессор Allwinner A64 с 4-мя ядрами Cortex-A53 с частотой 1.2 ГГц. На плате размещено 2 ГБ DDR3 SDRAM 733МГц оперативной памяти и 8 ГБ eMMC.

Самое главное для успешного подключения SPI LCD необходимо знать название контактов для SPI интерфейса, их номер и название зависит от модели процессора. Для решения этой задачи необходим Allwinner A64 Datasheet. На Wiki-странице Banana Pi BPI-M64 представлена распиновка 40-контактного разъема GPIO, из которого мы узнаем название контактов: PD2, PD3, и т.д.
40 PIN GPIO of Banana pi BPI-M64
GPIO Pin Name Default Function Function2GPIO Function3
CON2-P18 PD4 PD4
CON2-P19 SPI1-MOSI PD2 UART4-TX
CON2-P21 SPI1-MISO PD3 UART4-RX
CON2-P22 PC0 PC0
CON2-P23 SPI1-CLK PD1 UART3-RX
CON2-P24 SPI1-CS PD0 UART3-TX

Помимо название контакта, необходимо узнать порядковый номер этого контакта на ножке процессора, легко вычисляется по формуле: (позиция буквы в алфавите 1) * 32 + позиция вывода. Рассчитаем номер ножки для контакта PD2. Первая буква не учитывается т.к. P PORT, позиция буквы D в алфавите = 4, получаем (4-1) * 32 + 2 = 98. Контакту с меткой PD2 соответствует 98 ножка на процессоре, далее потребуется для конфигурирования дерева устройств.

Дерево устройств (Device Tree, DT) в Linux


Дерево устройств (Device Tree, DT) это структура данных в системе Linux, состоящая из именованных узлов и свойств, описывающих оборудование, которое невозможно обнаружить путем опроса оборудования. Дерево должно включать имя базового процессора, конфигурацию его памяти и любые периферийные устройства (внутренние и внешние). DT не используется для описания программного обеспечения, хотя перечисление аппаратных модулей вызывает загрузку модулей драйверов.

Сердцем любой отладочной платы или одноплатного компьютера является SoC. SoC имеет множество контактов (ног) для подключения линий электропитания и различных устройств.

Allwinner V3s

Контакты могут быть объедены вместе для формирования интерфейса, например MIPI DSI(MIPI Display Serial Interface). Интерфейс MIPI DSI предназначен для подключения LCD панелей, активно используется в смартфонах и планшетах. Но если к устройству не планируется подключать дисплей по MIPI DSI, то эти линии можно использовать для других целей, путем изменения DT. В отличие от архитектуры x86 в системах построенных на SoC нет возможности произвести полностью опознание всех устройств в режиме Plug and Play. Поэтому необходимо явное декларирование какие контакты используются для интерфейсов и какие именно устройства подключены к этим интерфейсам.

До появления DT информация об устройствах в Linux являлась неотъемлемой частью ядра, и в случае изменения состава периферийных устройств требовалось пересобрать образ системы. Это было крайне неудобно, и поэтому описание периферийных устройств перенесли в конфигурационные файлы, которые собираются на логическом уровне в дерево. Где ветвь устройство с указанием драйвера необходимого для работы этого устройства.

После использования DT отпала необходимость в формирование индивидуального образа для каждого набора периферии устройств. Теперь достаточно сформировать один образ, включить в него набор драйверов для различных устройств, и для каждого устройства сформировать свой DT.

Наложения дерева устройств (Device Tree overlays)


Device Tree overlays (наложения дерева устройств) добавление к DT принципа наложения слоев устройств. Если конфигурация описывает интерфейс UART к которому был подключен Bluetooth, и необходимо Bluetooth заменить на GPS модуль, то можно не удалять существующие настройки Bluetooth а добавить дополнительный слой для GPS модуля который переопределит предыдущие настройки.

Для работы с DT используются следующие термины:
DT Дерево устройств
DTB (*.dtb) Бинарный файл дерева устройств
DTBO (*.dtbo) Бинарный файл дерева устройств для наложения
DTC Компилятор дерева устройств
DTO Наложения дерева устройств
DTS (*.dts) Исходный файл для дерева устройств
FDT Flattened Device Tree, двоичный формат, содержащийся в файле .dtb

Аппаратная конфигурация описывается в файлах исходниках DT ( .dts ) затем они компилируется в бинарные файлы DT ( .dtb ) уже для конечного использования в системе. Так же можно выполнить обратную процедуру декомпиляции файлов *. dtb в *. dts, компилятор/декомпилятор присутствует в системе.

Linux device tree bootloader

Реализация DTO включает разделение дерева устройств, построение, разбиение на разделы и исполнение.

Разделение DT


DT разделяются на две части:
  • Main DT (основное дерево устройств). Предоставляет разработчик SoC и является настройкой по умолчанию. В данном случае предоставляет компания Allwinner разработчик процессора Allwinner A64.
  • Overlay DT (Накладываемое дерево устройств). Специфическая конфигурация производителя платы, включает периферийные устройства которые размещены на плате. Для платыBanana Pi BPI-M64 предоставляет компания SinoVoip Co.,

Тема Device Tree overlays в Linux достаточно большая, чтобы не превращать пост в многотомное произведение Ленина, более детально можно почитать в публикации Работа с GPIO на примере Banana Pi BPI-M64. Часть 2. Device Tree overlays.

Формирование DTS для SPI LCD ILI9341 2.4 inch


Тестирование производилось на образе Armbian_20.08.2_Bananapim64_bionic_current_5.8.6_minimal.img.xz, на основе Ubuntu 18.04.5 LTS (Bionic Beaver), ядро Linux 5.8.6. uname: Linux bananapim64 5.8.6-sunxi64 #20.08.2 SMP Fri Sep 4 08:52:31 CEST 2020 aarch64 aarch64 aarch64 GNU/Linux.

В Armbian уже есть драйвер для ILI9341, поэтому все что требуется, это создать файл описания устройства в формате DTS, скомпилировать его в формат DTBO, и перезагрузить одноплатный компьютер. Как говорится Easy!

Для формирования файла DTS необходимо узнать ссылку на gpiochip в котором находится SPI интерфейс, для этого откроем терминал Armbian и выполним команду cat /sys/kernel/debug/gpio:

root@bananapim64:~# cat /sys/kernel/debug/gpiogpiochip1: GPIOs 0-255, parent: platform/1c20800.pinctrl, 1c20800.pinctrl: gpio-120 (                    |bananapi-m64:red:pwr) out hi gpio-142 (                    |bananapi-m64:green:u) out lo gpio-143 (                    |bananapi-m64:blue:us) out lo gpio-166 (                    |cd                  ) in  lo ACTIVE LOW gpio-233 (                    |usb0_id_det         ) in  hi IRQgpiochip0: GPIOs 352-383, parent: platform/1f02c00.pinctrl, 1f02c00.pinctrl: gpio-354 (                    |reset               ) out hi ACTIVE LOW gpio-356 (                    |shutdown            ) out hi gpio-357 (                    |host-wakeup         ) in  lo gpio-358 (                    |device-wakeup       ) out higpiochip2: GPIOs 510-511, parent: platform/axp20x-gpio, axp20x-gpio, can sleep:

Данная команда выведет все доступные устройства gpiochip и номера задействованных контактов в операционной системе. В предыдущем разделе для контакта SPI1-MOSI, название контакта PD2, определили номер ножки процессора 98. Исходя из полученного результата номер 98 приходится на диапазон GPIOs 0-255, который соответствует чипу gpiochip1: GPIOs 0-255, parent: platform/1c20800.pinctrl, 1c20800.pinctrl. Далее для формирования файла DTS потребуется узнать ссылку на 1c20800.pinctrl.

Создадим файл DTS с названием: sun50i-a64-spi-ili9341-led-always-on.dts (в основе dts файл для платы Orange Pi PC):
/dts-v1/;/plugin/;/ {compatible = "allwinner,sun8i-h3";  fragment@0 {    target = <&pio>;    __overlay__ {      ili9341_pins: ili9341_pins {        pins = "PD4", "PC0"; /*RESET, DC_RS*/        function = "gpio_out", "gpio_out" ;      };    };  };    fragment@1 {    target = <&spi1>;    __overlay__ {      status = "okay";            cs-gpios = <&pio 3 0 0>; /* PD0 */      ili9341: ili9341@0 {        compatible = "ilitek,ili9341";        reg = <0>;        pinctrl-names = "default";        pinctrl-0 = <&ili9341_pins>;        spi-max-frequency = <16000000>;        rotate = <90>;        bgr;        fps = <25>;        buswidth = <8>;        reset-gpios = <&pio 3 4 1>; /*RESET=PD4*/        dc-gpios = <&pio 2 0 0>; /*DC_RS=PC0*/        /*led-gpios = <&pio 2 4 0>; LED=PC4*/        debug = <0>;      };    };  };};

Рассмотрим содержимое:

  • fragment@0 является наложением на блок узла /soc/pinctrl@1c20800, ссылка этого узла &pio. Описывает название задействованных контактов GPIO PD4", PC0 и определяет функцию gpio_out для контактов.
  • &pio это ссылка в дереве устройств для контактов GPIO на узел /soc/pinctrl@1c20800, название определяли выше. Ссылка берется из файла в Armbian по пути: /boot/dtb-5.8.6-sunxi64/allwinner/sun50i-a64-bananapi-m64.dtb.Для компиляции в формат DTS, выполнить команду: $ dtc -I dtb -O dts sun50i-a64-bananapi-m64.dtb -o sun50i-a64-bananapi-m64.dts
  • fragment@1 является наложением на блок узла /soc/spi@1c69000, ссылка этого узла &spi1.
  • status = okay задействует SPI1 интерфейс на плате для подключения устройств
  • cs-gpios = <&pio 3 0 0>; /* PD0 */ номер контакта для CS интерфейса SPI1.
  • <&pio 3 0 0> параметры контакта, где &pio ссылка на gpioiochip1 в основном дереве устройств, буква P не учитывается, означает PORT, буква D кодируется в цифру 3 (формула: порядковый номер буквы в алфавите 1), 0 после цифры 3 это позиция вывода, 0 из PD0, и последний 0 означает полярность, по умолчанию всегда 0 (полярность 0 на логический ноль, выдается напряжение 0; полярность 1 на логический ноль, выдается напряжение 1).
  • compatible = ilitek,ili9341 идентификатор драйвера для данного устройства
  • pinctrl-0 = <&ili9341_pins> ссылка на используемые контакты из fragment@0
  • spi-max-frequency = <16000000> частота работы SPI интерфейса
  • rotate = <90> ориентация изображения, поворот на 90 градусов, в зависимости как необходимо расположить дисплей.
  • fps = <25> кадров в секунду
  • reset-gpios = <&pio 3 4 1> контакт RESET=PD4
  • dc-gpios = <&pio 2 0 0> контакт DC_RS=PC0

Разместим файл по пути /boot/dtb/allwinner/overlay. Затем компилирует файл .dts в .dtbo:

$ dtc -O dtb -o sun50i-a64-spi-ili9341-led-always-on.dtbo sun50i-a64-spi-ili9341-led-always-on.dts

Запустим утилиту конфигурирования платы: $ armbian-config. Перейдем по меню: System > Hardware, и включим слой (overlay): spi-ili9341-led-always-on. После перезагрузки платы, консоль Linux будет на SPI LCD экране:

LCD SPI ILI9341 Banana Pi BPI-M64


Midnight Commander и Htop на SPI LCD
Midnight Commander
LCD SPI ILI9341 Banana Pi BPI-M64

Htop
LCD SPI ILI9341 Banana Pi BPI-M64


Название контактов
Для всех процессоров Allwinner формат записи контакта, соответствует виду cs-gpios = <&pio 3 0 0>, для других процессоров формат записи контакта будет отличаться.

Решение проблем


Если изображение не появилось на LCD, выполните команду для проверки: $ dmesg | grep -E 'ili9341'.

В консоли должна быть следующая информация:
root@bananapim64:/boot/dtb-5.8.6-sunxi64/allwinner# dmesg | grep -E 'ili9341'[    5.733989] fb_ili9341: module is from the staging directory, the quality is unknown, you have been warned.[    5.734718] fb_ili9341 spi0.0: fbtft_property_value: buswidth = 8[    5.734731] fb_ili9341 spi0.0: fbtft_property_value: debug = 0[    5.734737] fb_ili9341 spi0.0: fbtft_property_value: rotate = 90[    5.734744] fb_ili9341 spi0.0: fbtft_property_value: fps = 25[    6.119287] graphics fb0: fb_ili9341 frame buffer, 320x240, 150 KiB video memory, 16 KiB buffer memory, fps=25, spi0.0 at 16 MHz


Настройка SPI LCD для графического интерфейса Xfce и подсистемы X11


Для вывода консоли Linux достаточно добавить файл DTS и все, но для вывода графики этого недостаточно.

1) Установим XORG и XFCE:

$ sudo apt-get update$ sudo apt-get install xorg$ sudo apt-get install xfce4

2) Для процессора Allwinner необходимо дополнительно устанавливать GPU драйвер fbdev:

$ sudo apt-get install xserver-xorg-video-fbdev

3) Удалить все конфигурационные файлы по пути /etc/X11/xorg.conf.d (если нет файлов, то отлично)

4) Создать конфигурационный файл по пути /usr/share/X11/xorg.conf.d/99-fbdev.conf и разместить в нем следующий фрагмент:

Section "Device"    Identifier "myfb"  Driver "fbdev"  Option "fbdev" "/dev/fb0"EndSection

Где /dev/fb0 устройство SPI LCD. Если к плате подключена HDMI панель, то может быть два устройства /dev/fb0 и /dev/fb1.

Запускаем графический интерфейс командой: startx или startxfсe4:

LCD SPI ILI9341 Banana Pi BPI-M64


Если необходимо сразу переходить в графический интерфейс, то необходимо дополнительно установить пакеты:
$ sudo apt-get remove tasksel$ sudo apt-get remove xubuntu-desktop

Для возвращения запуска только консоли необходимо отключить автозапуск службы display-manager.service

$ sudo systemctl disable display-manager.service

Решение проблем


Если графический интерфейс не запускается то для решение проблем необходимо посмотреть журнал событий X11, командой:

$ cat /var/log/Xorg.0.log


Итог


Установка и настройка SPI LCD не требует никаких компиляций модулей из исходного текста, что существенно упрощает установку. Главное внимательно выставить контакты и все заработает из коробки. Поставленные цели успешно решены.

RoadMap


  1. Подключение дисплея большего размера 3.5 дюйма на контроллере ILI9488.
  2. Настройка Touch интерфейса для Xfсe.
  3. Вывод на SPI LCD только одного графического приложения используя подсистему X11 из Docker контейнера (решение для публичных терминалов, киосков, POS-терминалов).

Файл sun50i-a64-spi-ili9341-led-always-on.dts и другие файлы наложения дерева доступны в каталоге GitHub Banana-Pi-BPI-M64/dt-overlays/
Подробнее..

Делаем блок SPI to AVALON_MM для USB-устройства на базе FX3

24.02.2021 16:18:54 | Автор: admin
В предыдущей статье мы научились подавать Vendor команды в устройство USB3.0 на базе контроллера FX3 и реализовали программную шину SPI. Сегодня мы продолжим начатое и сделаем компонент SPI to Avalon_MM. Может возникнуть вопрос: мы же уже умеем работать с шиной Avalon_MM через JTAG средствами TCL-скриптов, зачем нам что-то ещё?

Дело в том, что когда мы работаем на чистом TCL, как делали это здесь и здесь, всё замечательно. Но для задач, гоняющих десятки или даже сотни мегабайт, этот вариант слишком медленный. Поэтому мы вынуждены добавить программу на С++, работающую через USB 3.0.

Вариант с TCL-сервером, к которому обращается плюсовая программа, рассмотренный в этой статье, требует сложной ручной подготовки при каждом запуске. Надо обязательно запустить среду исполнения (для Windows и Linux они разные), запустить серверный скрипт, а затем в программе синхронизировать работу с данными по USB и с командами через TCP. Не люблю такие сложности. Оставим те варианты под случаи, когда они не создают трудностей. Здесь же у нас есть USB-устройство, мы всё равно с ним работаем, вот и будем обращаться к шине AVALON_MM через него. Приступаем.



Предыдущие статьи цикла:

  1. Начинаем опыты с интерфейсом USB 3.0 через контроллер семейства FX3 фирмы Cypress
  2. Дорабатываем прошивку USB 3.0, используя анализатор SignalTap, встроенный в среду разработки Quartus
  3. Учимся работать с USB-устройством и испытываем систему, сделанную на базе контроллера FX3
  4. Боремся с таймаутами при использовании USB 3.0 через контроллер FX3, возникающими при определенных условиях
  5. Добавляем поддержку Vendor-команд к USB3.0 устройству на базе FX3

Введение


Перед тем как заняться разработкой своего блока, я попытался найти что-то готовое. Да, работа с TCL как напрямую, так и через сеть создаёт ряд неудобств для пользователя. Но нельзя ли достучаться до JTAG-адаптера напрямую? Ну, или хотя бы подключиться к JTAG-серверу, как это делают штатные компоненты системы? Я мучил Гугля всё более и более мудрёными запросами. Увы. Есть вариант, когда сервер реализуется на самой плате (только в нашем варианте с платы выкинут процессор, не на чем его там запустить), но нет примеров, как подключиться к JTAG-серверу, запущенному на PC. Были статьи про существующий сервер для проекта Марсоход, запускаемый на Малине, но насколько я понял, там надо подменять DLL. Было ещё несколько статей с явно нужными ключевыми словами, но все они были удалены, а в кэше Гугля лежало что-то, совершенно нечитаемое.

Я даже выдвинул гипотезу, что дело в ядрах с ограниченными правами использования. Тех, которые работают только тогда, когда подключены к PC с запущенным сервером. Возможно, кто поймёт принцип управления JTAG-сервером, тот сможет их всколоть, а правообладатели этого не хотят, поэтому тщательно скрывают протоколы. У нас нет задачи что-то всколоть, а у меня нет желания писать статью, которую быстро удалят. Поэтому я решил просто сделать свой блок. Какой? Решение выплывает из моей текущей рабочей загрузки. Я играю в среду Litex. Там используется шина AXI-Lite или Wishbone. Я работаю со второй из них и вижу в системе массу переходников. Там есть и SPI to Wishbone, и UART to Wishbone и всё, что угодно to Wishbone. Поэтому я и решил сделать переходник SPI to Avalon-MM.

Где черпаем вдохновение


Если вбить Гуглю запрос Avalon Memory-Mapped Master Templates, то мы попадём вот сюда:

Avalon Memory-Mapped Master Templates (intel.com)

Скачиваем имеющийся на этой странице zip-файл, там есть примеры мастеров для шины AVALON-MM практически на все случаи жизни. В документе приводятся примеры, как просто взять и положить файлы из этого архива, после чего начать работать с ними. Приведу пример рисунка для одного из направлений



Мы должны реализовать участок Control Logic и FIFO, после чего всё заработает само. Сначала я гипнотизировал эти примеры, мечтая пойти именно по этому пути. Первое, что не давало покоя: мастер чтения и мастер записи разные блоки, каждый из которых подключается к шине AVALON. Соединить их не так просто. Затем, начав практические опыты, я понял, что иерархия там тоже получается не самая лучшая. Мне бы пришлось сделать очень много транзитных верёвок. Система становилась слишком сложной, а выше я уже писал, что не люблю сложных систем.

Тогда я внимательно посмотрел на исходные тексты и понял, что на самом деле там реализуются действия, сложность которых не выше, чем у блоков AVALON_MM Slave, которые мы уже делаем, как семечки щёлкаем. Там нет ничего страшного. Нет никаких линий GNT, характерных для некоторых взрослых шин. Вообще ничего нет. Знай себе, ставь стробы и удерживай их, пока не получил подтверждения, что данные прокачались. Всё! Остальное за нас сделает логика, являющаяся внешней по отношению к нашему блоку (она спрятана от нас где-то внутри System Interconnect Fabric).

Некоторые небольшие трудности возникли бы при отладке пакетных передач, но я же не собираюсь их гонять! У меня очень медленная шина SPI (в прошлой статье мы видели, что на ней частота тактовых сигналов не превышает 4 МГц, а данных по ней пробегает 8 + 32 + 32 = 72 бита, итого предельная частота следования данных 55,(5) КГц). Так что получили запрос прогнали одно слово по Avalon, отпустили шину. Ждём следующий запрос. Не нужны тут пакеты!

Итого. Пишем свой модуль с нуля, но всё равно, черпая вдохновение в исходных кодах, скачанных с вышеуказанной странички. Собственно, если кому-то больше нравится работать не по примерам (пусть и фирменным), а по документам ссылка на спецификацию Avalon на той страничке тоже есть.

Главный автомат


В основу работы модуля положим конечный автомат. Причём сигналом сброса для него я выбрал положительный уровень линии SS. Давайте я покажу типичную посылку по шине SPI, взяв первую попавшуюся времянку с просторов сети:



На этом рисунке линия называется не SS (Slave Select), а CS (Chip Select). Но мы видим, что её высокий уровень можно трактовать как сброс шины. Это очень удобно. Не надо бояться, что произойдёт рассинхронизация. Мы почти уверены, что перед первым битом этот сигнал перейдёт из единицы в ноль. В своём коде для FX3 я сделаю так, чтобы быть уверенным не почти, а стопроцентно.

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



Когда значение bit_cnt достигло сорока (из-за особенностей языков Verilog, да и VHDL тоже, в коде используется константа 39), мы выходим на рабочий участок. Команд может быть две: чтение и запись. За это отвечает нулевой бит команды (из-за той же особенности языка в коде проверяется первый). Вот так выглядит обработчик этого состояния на SystemVerilog:

Смотерть текст.
always_ff @(posedge master_clk, posedge spi_ss)begin      // Это эквивалентно сбросу со стороны SPI      if (spi_ss == 1)      begin          bit_cnt <= 0;          state <= idle;      end else      begin          master_write <= 0;          master_read <= 0;          case (state)            idle: begin                 // Ура! У нас очередной перепад SCK!                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regCmdAndAddr <= {spi_mosi_reg,regCmdAndAddr[39:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 39)                    begin                       // Пишем                       if (regCmdAndAddr[1])                       begin                           state <= write1;                       end else                       // Читаем                       begin                           state <= read1;                       end                    end                 end            end


Чтобы хоть как-то оправдать восьмибитный регистр команды, я использую старшие 4 бита как BYTE_ENABLE для шины AVALON_MM, чтобы дать возможность писать не только по 32 бита. Для этого в конце текста есть такая строка:

assign master_byteenable = regCmdAndAddr [7:4];assign master_address = regCmdAndAddr [39:8];

Вторая строка в этой паре подключает наш регистр адреса к линиям адреса шины AVALON_MM.

Теперь пройдёмся по ветви чтения. Сначала надо считать данные из шины. Я исхожу из предположения, что шина SPI крайне медленная, поэтому не ввожу на неё никакого сигнала готовности. Считаем, что данные из AVALON_MM придут так быстро, что в SPI не успеет убежать ни одного лишнего бита. Нам потребуется два состояния. В первом мы взведём строб чтения и будем удерживать его, пока нам не придёт подтверждения, что данные пришли. Тогда мы защёлкнем эти данные и будем выдавать их в SPI на протяжении тридцати двух тактов. Собственно, всё. Потом мы на всякий случай сбросим счётчик битов (а вдруг начнётся передача новой команды без снятия SS?) и вновь перейдём в состояние idle, где будем копить новую команду. Состояние read1 выделено особым цветом, так как оно не зависит от положительного перепада на линии SCK шины SPI. Оно привязано только к тактовому сигналу шины AVALON_MM.



Вот так реализованы эти состояния в автомате.

Cмотреть текст.
            read1: begin                  master_read <= 1;                  if (master_readdatavalid)                  begin                      state <= read2;                      regData <= master_readdata;                  end            end            read2: begin                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regData <= {1'b0,regData[31:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 71)                    begin                       bit_cnt <= 0;                       state <= idle;                    end                 end            end


И вот так привязан выход регистра сдвига к сигналу MOSI шины SPI:

assign spi_miso = regData [0];

Ветка записи с точностью до наоборот. Сначала в состоянии write1 мы принимаем 32 бита данных, затем выставляем строб записи и висим в состоянии write2 (также не привязанным к SCK, поэтому имеющем особый цвет), пока нам не сообщат, что наши данные ушли.



Получаем такой код для реализации состояний.

Смотреть текст.
            // При записи, надо сначала допринимать данные из SPI            write1: begin                 // Ура! У нас очередной перепад SCK!                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regData <= {spi_mosi_reg,regData[31:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 71)                    begin                       state <= write2;                    end                 end            end            // Всё заполнено. Держим строб записи.             write2: begin               master_write <= 1;               // Если шина нас услышала - ну и прекрасно. Вышли               if (master_waitrequest == 0)               begin                   bit_cnt <= 0;                   state <= idle;               end            end


и такую строку для статического проецирования регистра данных на линии данных AVALON_MM:

assign master_writedata = regData;

Остальной текст модуля необходимая технологическая мишура. Нам надо продискретизировать линии SPI по тактовой частоте шины AVALON_MM, кроме того, надо получить задержанный на один авалоновский такт сигнал SCK шины SPI, чтобы иметь возможность ловить его перепад. Все эти действия мы имеем право выполнять только если уверены, что шина SPI достаточно медленная относительно AVALON_MM. Именно поэтому в прошлой статье я занимался оптимизацией, но не гнался за бешеными показателями.

reg spi_sck_reg, spi_sck_d;reg spi_mosi_reg;always @ (posedge master_clk)begin     spi_sck_reg <= spi_sck;     spi_mosi_reg <= spi_mosi;     spi_sck_d <= spi_sck_reg;end

Cобственно, всё. Модуль готов. Вот его полный текст для справки.

Смотреть полный текст модуля.
module spitoAvalon_mm (input           master_clk,input           master_reset,output  [31:0]    master_address,output  reg       master_write=0,output  [3:0]     master_byteenable,output  [31:0]    master_writedata,output reg        master_read,input             master_readdatavalid,input [31:0]    master_readdata,input             master_waitrequest,input           spi_sck,input           spi_mosi,output          spi_miso,input           spi_ss);// Чтобы не заводить SPI_SCK на линию GCK,// мы ориентируемся на то, что шина - медленная, поэтому// просто ловим перепады по основной тактовой частоте// Да и вообще, отдискретизируем SPI по тактовой. Во избежание...reg spi_sck_reg, spi_sck_d;reg spi_mosi_reg;always @ (posedge master_clk)begin     spi_sck_reg <= spi_sck;     spi_mosi_reg <= spi_mosi;     spi_sck_d <= spi_sck_reg;end// Число битов, принятых из SPIreg [7:0] bit_cnt;// Регистр команд/адреса. Итого 8 + 32 = 40 битreg [39:0] regCmdAndAddr = 0;reg [31:0] regData = 0;enum {idle,        read1, read2,       write1,write2     } state = idle;always_ff @(posedge master_clk, posedge spi_ss)begin      // Это эквивалентно сбросу со стороны SPI      if (spi_ss == 1)      begin          bit_cnt <= 0;          state <= idle;      end else      begin          master_write <= 0;          master_read <= 0;          case (state)            idle: begin                 // Ура! У нас очередной перепад SCK!                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regCmdAndAddr <= {spi_mosi_reg,regCmdAndAddr[39:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 39)                    begin                       // Пишем                       if (regCmdAndAddr[1])                       begin                           state <= write1;                       end else                       // Читаем                       begin                           state <= read1;                       end                    end                 end            end            // При записи, надо сначала допринимать данные из SPI            write1: begin                 // Ура! У нас очередной перепад SCK!                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regData <= {spi_mosi_reg,regData[31:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 71)                    begin                       state <= write2;                    end                 end            end            // Всё заполнено. Держим строб записи.             write2: begin               master_write <= 1;               // Если шина нас услышала - ну и прекрасно. Вышли               if (master_waitrequest == 0)               begin                   bit_cnt <= 0;                   state <= idle;               end            end            // При чтении - наоборот, сначала считали данные            // Для медленной шины, надо бы ещё готовность SPI добавить            // но в этом примере, мы ею пренебрежём            read1: begin                  master_read <= 1;                  if (master_readdatavalid)                  begin                      state <= read2;                      regData <= master_readdata;                  end            end            read2: begin                 if ((spi_sck_d == 0) && (spi_sck_reg == 1))                 begin                    regData <= {1'b0,regData[31:1]};                    bit_cnt <= bit_cnt + 1;                    // Особенность машинного языка - анализируем не новое,                     // а предыдущее значение                    if (bit_cnt == 71)                    begin                       bit_cnt <= 0;                       state <= idle;                    end                 end            end            default:  begin              state <= idle;            end          endcase                endendassign master_byteenable = regCmdAndAddr [7:4];assign master_address = regCmdAndAddr [39:8];assign master_writedata = regData;assign spi_miso = regData [0];endmodule


Можно внедрять его в проект. Как это делается мы детально рассматривали тут и потом постоянно занимались этим в цикле статей про Redd. Но сегодня править придётся намного больше вещей, чем обычно.

Внедряем модуль в проект


Обычно я детально рассказываю про процесс внедрения модуля. Но сегодня потребуется такое количество мелких правок, что мне кажется, рассказ не будет иметь никакого эффекта. Все дочитают до третьего абзаца, затем зевнут и перейдут к следующему разделу. Поэтому сегодня я дам TCL-скрипт, внедряющий новый модуль в систему, и расскажу, как его применить. Будет не так скучно, а главное это будет давать какие-то новые знания. Вот такой скрипт сделал Квартус после всех моих действий:

Смотреть текст Квартуса.
# TCL File Generated by Component Editor 17.1# Wed Dec 30 02:23:32 MSK 2020# DO NOT MODIFY# # SpiToAvalonMM "SpiToAvalonMM" v1.0#  2020.12.30.02:23:32# # # # request TCL package from ACDS 16.1# package require -exact qsys 16.1# # module SpiToAvalonMM# set_module_property DESCRIPTION ""set_module_property NAME SpiToAvalonMMset_module_property VERSION 1.0set_module_property INTERNAL falseset_module_property OPAQUE_ADDRESS_MAP trueset_module_property AUTHOR ""set_module_property DISPLAY_NAME SpiToAvalonMMset_module_property INSTANTIATE_IN_SYSTEM_MODULE trueset_module_property EDITABLE trueset_module_property REPORT_TO_TALKBACK falseset_module_property ALLOW_GREYBOX_GENERATION falseset_module_property REPORT_HIERARCHY false# # file sets# add_fileset QUARTUS_SYNTH QUARTUS_SYNTH "" ""set_fileset_property QUARTUS_SYNTH TOP_LEVEL spitoAvalon_mmset_fileset_property QUARTUS_SYNTH ENABLE_RELATIVE_INCLUDE_PATHS falseset_fileset_property QUARTUS_SYNTH ENABLE_FILE_OVERWRITE_MODE falseadd_fileset_file spitoAvalon_mm.sv SYSTEM_VERILOG PATH MyCores/spitoAvalon_mm.sv TOP_LEVEL_FILE# # parameters# # # display items# # # connection point conduit_end# add_interface conduit_end conduit endset_interface_property conduit_end associatedClock ""set_interface_property conduit_end associatedReset ""set_interface_property conduit_end ENABLED trueset_interface_property conduit_end EXPORT_OF ""set_interface_property conduit_end PORT_NAME_MAP ""set_interface_property conduit_end CMSIS_SVD_VARIABLES ""set_interface_property conduit_end SVD_ADDRESS_GROUP ""add_interface_port conduit_end spi_ss spi_ss Input 1add_interface_port conduit_end spi_sck spi_sck Input 1add_interface_port conduit_end spi_mosi spi_mosi Input 1add_interface_port conduit_end spi_miso spi_miso Output 1# # connection point avalon_master# add_interface avalon_master avalon startset_interface_property avalon_master addressUnits SYMBOLSset_interface_property avalon_master associatedClock clock_sinkset_interface_property avalon_master associatedReset reset_sinkset_interface_property avalon_master bitsPerSymbol 8set_interface_property avalon_master burstOnBurstBoundariesOnly falseset_interface_property avalon_master burstcountUnits WORDSset_interface_property avalon_master doStreamReads falseset_interface_property avalon_master doStreamWrites falseset_interface_property avalon_master holdTime 0set_interface_property avalon_master linewrapBursts falseset_interface_property avalon_master maximumPendingReadTransactions 0set_interface_property avalon_master maximumPendingWriteTransactions 0set_interface_property avalon_master readLatency 0set_interface_property avalon_master readWaitTime 0set_interface_property avalon_master setupTime 0set_interface_property avalon_master timingUnits Cyclesset_interface_property avalon_master writeWaitTime 0set_interface_property avalon_master ENABLED trueset_interface_property avalon_master EXPORT_OF ""set_interface_property avalon_master PORT_NAME_MAP ""set_interface_property avalon_master CMSIS_SVD_VARIABLES ""set_interface_property avalon_master SVD_ADDRESS_GROUP ""add_interface_port avalon_master master_address address Output 32add_interface_port avalon_master master_write write Output 1add_interface_port avalon_master master_byteenable byteenable Output 4add_interface_port avalon_master master_writedata writedata Output 32add_interface_port avalon_master master_read read Output 1add_interface_port avalon_master master_readdatavalid readdatavalid Input 1add_interface_port avalon_master master_readdata readdata Input 32add_interface_port avalon_master master_waitrequest waitrequest Input 1# # connection point clock_sink# add_interface clock_sink clock endset_interface_property clock_sink clockRate 0set_interface_property clock_sink ENABLED trueset_interface_property clock_sink EXPORT_OF ""set_interface_property clock_sink PORT_NAME_MAP ""set_interface_property clock_sink CMSIS_SVD_VARIABLES ""set_interface_property clock_sink SVD_ADDRESS_GROUP ""add_interface_port clock_sink master_clk clk Input 1# # connection point reset_sink# add_interface reset_sink reset endset_interface_property reset_sink associatedClock clock_sinkset_interface_property reset_sink synchronousEdges DEASSERTset_interface_property reset_sink ENABLED trueset_interface_property reset_sink EXPORT_OF ""set_interface_property reset_sink PORT_NAME_MAP ""set_interface_property reset_sink CMSIS_SVD_VARIABLES ""set_interface_property reset_sink SVD_ADDRESS_GROUP ""add_interface_port reset_sink master_reset reset Input 1


Итак. У меня в проекте традиционно имеется каталог MyCores. Туда я кладу СистемВерилоговский файл spitoAvalon_mm.sv.



А теперь на уровень проекта кладу файл SpiToAvalonMM_hw.tcl.



Прекрасно. Открываем Platform Designer и видим, что наш компонент сам запрыгнул в перечень доступных!



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



Обратите внимание, что всем линиям шины SPI пришлось дать осмысленные имена.

Теперь посмотрим настройки шины AVALON_MM



Я выставил адресацию с точностью до байта. Можно было бы переключить точность до слова, но раз у меня есть линии BYTE_ENABLE, то можно же сделать работу с байтами и WORDами. Так что до байта. И какая-то из латентностей, уже не помню какая, была установлена в единицу. Я заменил на ноль. Собственно, на рисунке выше все задержки и латентности равны нулю, так что не перепутаете. Единицу будет хорошо видно.

Добавляем тестовую систему


Собственно, как проверить шину? А давайте через неё подключим небольшую ОЗУшку и попробуем писать и читать данные. Добавляем в систему, доставшуюся нам в наследство от прошлых статей, два элемента: мост SpiToAvalonMM и On Chip Memory. Но ОЗУ мы настроим чуть более сложно, чем обычно. Дело в том, что при отладке зеркальных систем можно получить, что запись и чтение работают. Но если допущена идеологическая ошибка в обоих направлениях, оно неверно запишется, так же неверно считается, но считанные данные совпадут с записанными, и мы увидим ложную работу. Поэтому желательно, чтобы в ОЗУ были заранее известные данные. Сначала мы убедимся, что они читаются верно, а уже затем начнём проводить пару запись-чтение, чтобы проверить уже работу записи при заведомо работающем чтении. Для этого мы взводим флажок Enable non-default initialization file и начинаем разбираться, куда положить и как сделать файл onchip_mem.hex.



Кладём его на тот же уровень, где живут файлы *.qpf и *.qsf. А вот с содержимым придётся немного повеселиться. Генератор такого файла входит в состав NIOS II EDS, то есть, в состав Квартуса. Но увы, это elf2hex. А нам бы bin2hex. Я нашёл замечательный проект SRecord 1.64 (sourceforge.net), который умеет преобразовывать любые файлы в любые. Замечательный проект! Он стоит того, чтобы попасть в записные книжки разработчиков железа Но в классическом HEXе адресация идёт байтами, а Квартус хочет, чтобы шла DWORD-ами, поэтому пришлось писать генератор hex файла самому. Учитывая, что надо было экономить время на разработку, он получился таким.

Смотреть текст.
void SpiToAvalonDemo::on_m_btnCreateMemInitFile_clicked(){    // Create an output stream    QFile file ("onchip_mem.hex");    if (!file.open(QIODevice::WriteOnly))    {     return;    }    QTextStream out (&file);    uint32_t initData [4096/sizeof(uint32_t)];    memset (initData,0,sizeof(initData));    initData [0x00] = 0x12345678;    initData [0x01] = 0x11111111;    initData [0x02] = 0x22222222;    initData [0x03] = 0x33333333;    initData [0x04] = 0x44444444;    initData [0x05] = 0x55555555;    initData [0x06] = 0x66666666;    initData [0x07] = 0x77777777;    initData [0x08] = 0xffffffff;    // Адрес всегда нулевой    out << ":020000020000FC\r\n";    for (size_t i=0;i<sizeof(initData)/sizeof(initData[0]);i+=8)    {        uint8_t cs = 0x20;        out <<":20";        cs += (uint8_t) (i/0x100);        cs += (uint8_t) (i/0x1);        out << QString ("%1").arg(i,4,16,QChar('0'));        out << "00";        uint8_t* ptr = (uint8_t*)(initData+i);        for (int j=0;j<32;j++)        {            cs += ptr[j];            out << QString ("%1").arg(ptr[j],2,16,QChar('0'));        }        cs = -1 * cs;        out << QString ("%1\r\n").arg(cs,2,16,QChar('0'));    }    // EOF    out << ":00000001FF\r\n";//    out.writeRawData((const char*)initData,sizeof(initData));    file.close();}


Просто для справки: вот так выглядят вновь добавленные блоки в процессорной системе:



Conduit выход spi экспортирован наружу, единственная шина AVALON_MM соединяет наш мастер со slave-входом ОЗУ. А больше я даже не знаю, что сказать. Обычно у нас системы были позабористей. Тут всё просто.

Не забываем назначить новые выводы


Не забываем, что у нас появились новые выводы (SPI). Дело в том, что когда я повторял свои подвиги при написании этого текста, я реально забыл это сделать, и очень удивлялся, что постоянно читается FFFFFF, хотя система уже была проверена при черновой подготовке и не должна была дурить. Так что не забываем! Для моей аппаратуры получилось так:



Дорабатываем код FX3


В прошлый раз в прошивке FX3 мы сделали только запись в SPI. В этот раз надо добавить чтение. Для начала чтение бита данных. Если при записи было очень полезно работать с конкретными битами, так как вместо чтения-модификации-записи, можно было только писать, то при чтении такой режим не даёт никакого выигрыша. Всё равно надо принять данные и как-то выделить требуемый бит. Поэтому для чтения данных я сделал такой макрос:

#define GET_IO_BIT(nBit) ((GPIO->lpp_gpio_invalue1 >> (nBit-32))&1)

Из оптимизации там только то, что я заранее знаю, что нужные нам биты находятся в диапазоне 32-63, поэтому сразу обращаюсь к регистру GPIO->lpp_gpio_invalue1, не тратя такты процессора на проверку.

Само чтение тоже особо ничем не приметно. Но кто будет вглядываться в текст, тот заметит, что последовательность действий отличается от той, которая напрашивается сама собой. Я просто постарался раскидать работу, чтобы и в положительном, и в отрицательном полупериоде сигнала SCK задержку вносили какие-то полезные команды. Было бы обидно всё полезное расположить в одной половинке, а в другую добавлять бесполезную задержку при помощи NOPов. И так всё медленно работает!

Смотреть текст.
unsigned int SPI_Read (int nBits){   unsigned int data = 0;   SET_IO_BIT (MY_BIT_MOSI);   while (nBits)   {   data >>= 1;   nBits -= 1;   CLR_IO_BIT (MY_BIT_CLK);   data |= (GET_IO_BIT (MY_BIT_MISO) << 31);   SET_IO_BIT (MY_BIT_CLK);   }   CLR_IO_BIT (MY_BIT_CLK);   return data;}


И, наконец, обработчик Vendor-команды в итоге стал таким:

Смотреть текст.
    if (bType == CY_U3P_USB_VENDOR_RQT)    {    // Cut size if needif (wLength > sizeof(ep0_buffer)){wLength = sizeof (ep0_buffer);}    // Need send data to PC    if (bReqType & 0x80)    {            CLR_IO_BIT(MY_BIT_SS);            SPI_Write(bRequest,8);            SPI_Write(wValue,16);            SPI_Write(wIndex,16);            ep0_buffer [0] = SPI_Read (32);            SET_IO_BIT(MY_BIT_SS);    CyU3PUsbSendEP0Data (wLength, (uint8_t*)ep0_buffer);            isHandled = CyTrue;    } else    {    CyU3PUsbGetEP0Data (wLength, (uint8_t*)ep0_buffer, NULL);    ep0_buffer [wLength] = 0;// Null terminated String            CyU3PDebugPrint (4, (char*)ep0_buffer);            CLR_IO_BIT(MY_BIT_SS);            SPI_Write(bRequest,8);            SPI_Write(wValue,16);            SPI_Write(wIndex,16);            SPI_Write(ep0_buffer[0],32);            SET_IO_BIT(MY_BIT_SS);    CyU3PUsbAckSetup();            isHandled = CyTrue;    }    }


Черновая проверка


Как и в прошлый раз, начерно всё проверяем через какую-нибудь подавалку USB-команд. Лично я предпочитаю BusHound. Как через него выполнять подобные проверки, было рассказано в предыдущей статье. Вот я читаю адрес 0. Длину всегда задаю равную четырём. Читается 12345678, как раз то, что было записано в файле onchip_mem.hex.



С адреса 4 читается 11111111



Ну, и так далее. Теперь пробуем записать. Скажем, по адресу 0x40 значение 0x87654321.



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

Добавляем код для программной работы


Убедившись, что всё работает верно, я добавил в класс, общающийся с библиотекой LibUSB, две функции. Если быть совсем точным, то я создал класс CAvalonViaFX3, унаследовав его от уже известного по одной из прошлых статей CUsbTester, а уже в него добавил эти две функции. Но это уже детали реализации, для нас сейчас важен сам код. Вот он:

bool CAvalonViaFX3::WriteDword(uint32_t addr, uint32_t data){    int res = libusb_control_transfer(m_hUsb,                            0x40,0xf1,                            (uint16_t)addr,(uint16_t)(addr>>16),                            (unsigned char*)&data,4,100);    return (res == 4);}bool CAvalonViaFX3::ReadDword(uint32_t addr, uint32_t& data){    int res = libusb_control_transfer(m_hUsb,                            0xc0,0xf0,                            (uint16_t)addr,(uint16_t)(addr>>16),                            (unsigned char*)&data,4,100);    return (res == 4);}

Ну, и код, тестирующий память, размером 4 килобайта, выглядит так:

static const int memSize = 4096;void SpiToAvalonDemo::on_m_btnMemoryTest_clicked(){    QRandomGenerator genWrite (1234);    uint32_t data [memSize/sizeof(uint32_t)];    for (size_t i=0;i<memSize/sizeof(uint32_t);i++)    {        data[i] = genWrite.generate();    }    for (int i=0;i<memSize;i+=sizeof(uint32_t))    {        if (!m_tester.WriteDword(i,data[i/sizeof(uint32_t)]))        {            QString msg = QString ("Write Error at Addr: 0x%1\n").arg(i,8,16,QChar('0'));            qDebug() << msg;            return;        }    }    for (int i=0;i<memSize;i+=sizeof(uint32_t))    {        uint32_t rd;        if (!m_tester.ReadDword(i,rd))        {            QString msg = QString ("Read Error at Addr: 0x%1\n").arg(i,8,16,QChar('0'));            qDebug() << msg;            return;        }        if (rd!=data[i/sizeof(uint32_t)])        {            QString msg = QString ("Miscompare at Addr: 0x%1 Written: %2 Read: %3\n").arg(i,8,16,QChar('0')).                    arg(data[i],8,16,QChar('0')).arg(rd,8,16,QChar('0'));            qDebug() << msg;            return;        }    }    qDebug()<<"Test Finished\n";}

Внутренний буфер data пришлось завести для того, чтобы можно было выводить при ошибках ожидаемые значения. Правда, это не понадобилось, всё работает и так. Но если бы понадобилось это бы сильно помогло выяснить наиболее вероятную причину ошибки (сбой адреса, сбой данных, прочие сбои). Если делать на века, то, разумеется, можно было бы обойтись без буфера, пользуясь только генератором псевдослучайных чисел, порождённым от той же базовой константы, от которой был порождён генератор чисел, использованные при записи. Но это бы сделало текст менее читаемым, а для статей читаемость важнее, чем доведение эффективности из области и так неплохо в область идеала.

Заключение


Мы освоили методику разработки мастеров, читающих и пишущих в шину AVALON_MM. Набивая руку, мы сделали переходник SPI в AVALON_MM и проверили его работоспособность. При работе с контроллером FX3 это позволит обращаться (с не очень высокой производительностью) к шине без использования каких-либо сторонних средств, так как раньше пришлось бы работать с TCL-командами или скриптами.

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

Из песочницы Вывод текста на OLED дисплей с контроллером SH1106 по шине SPI через библиотеку HAL

23.08.2020 16:12:18 | Автор: admin

Здравствуйте, уважаемые читатели. В своих разработках на микроконтроллерах STM32, для вывода осмысленной информации, я пользуюсь OLED дисплеями на чипе SSD1306. В последний раз пришел ко мне 1,3" SPI модель по демократичной цене около 200руб. Первое, что бросилось в глаза надпись SH1106 вместо SSD1306, поиск в интернете прояснил, что это практически тоже самое, только оставлен единственный страничный режим адресации, да и тот ограничен одной строкой. Как с ним работать я и постараюсь объяснить вам в этой публикации.


Где-то с год назад мне стало не хватать возможностей синей пилюли (STM32F103) и была заказана китайская плата разработчика STM32F407VE. Для отладки, часто, двух светодиодов не хватает, поэтому в каждом проекте для вывода информации подключаю OLED SSD1306 по шине I2C, в который влюбился еще со времен Arduino. Так как графику я на него не вывожу, в основном числа и текст, а размер готовых библиотек и их содержание поражал мое воображение, была написана небольшая библиотечка, которую я немного адаптировал под SH1106 и хочу поделится с вами процессом ее написания. Дисплей приехал 7pin SPI:


sh1106

Плата разработчика у меня такая, но ничего вам не помешает подключить к другой, хоть на STM3F103, для чего HAL и был придуман (разве не так ?):


F407board


Выберем в CubeMX наш STM32F407VE кристалл, обязательно включим режим отладки, иначе потом перешивать придется через UART1. В Clock Configuration выберем резонаторы 8MHz и зададим частоту работы кристалла 168MHz от HSE. При желании можете сконфигурировать на выход ножки PA6 и PA7, к которым подключены светодиоды D2 и D3 (загораются подачей лог 0) для контроля прохождения проблемных точек кода:


RccDebug

На плате SPI1 выведен на колодку NRF24L01 и к нему у меня подключен ESP-PSRAM64H, для дисплея остался SPI3. Выберем его мастером на передачу в DMA режиме и активируем прерывания, настройки будут такие :


SpiAll

Теперь настроим ножки управляющих сигналов DC (данные/команда), RESET (аппаратный сброс) и CS (выбор дисплея) :


Gpio

Таблица соединений :


SH1106 STM32F407
  1. GND GND
  2. VDD 3V3
  3. SCK PC10
  4. SDA PC12
  5. RES PD0
  6. DC PC11
  7. CS PA15

Увеличим Heap и Stack в 2 раза и создадим проект для Atollic, и выберем его запуск. Сразу создадим библиотеку, которую потом будем подключать к своим проектам. В левом окне раскроем папку Src нашего проекта и в меню выберем File->Source File, введем имя нашей библиотеки spi1106.c аналогично создадим File->Header File с именем spi1106.h. Последний перенесем из папки Src в Inc и откроем для редактирования, между #define ic1306_H_ и #endif /* ic1306_H_ */ определим короткие команды для управлением сигналами CD, RESET и CS и функцию инициализации экрана:

#define SPI1106_H_#include "main.h"void sh1106Init (uint8_t contrast, uint8_t bright, uint8_t mirror);#define SH_Command HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_RESET)#define SH_Data HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET)#define SH_ResHi HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, GPIO_PIN_SET)#define SH_ResLo HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, GPIO_PIN_RESET)#define SH_CsHi HAL_GPIO_WritePin(GPIOA, CS_Pin, GPIO_PIN_SET)#define SH_CsLo HAL_GPIO_WritePin(GPIOA, CS_Pin, GPIO_PIN_RESET)#endif /* SPI1106_H_ */

В файле spi1106.c начнем создавать наши функции, вначале подключим main.h, хэдер нашей библиотеки и выбранного канала SPI дисплея:


#include "main.h"#include <spi1106.h>extern SPI_HandleTypeDef hspi3;

Напишем функции пересылки кода команды через SPI при помощи библиотеки HAL:


void SH1106_WC (uint8_t comm){uint8_t temp[1];SH_Command;SH_CsLo;temp[0]=comm;HAL_SPI_Transmit(&hspi3,&temp,1,1);SH_CsHi;}

Функция инициализации с минимальным набором команд:


void sh1106Init (uint8_t contrast, uint8_t bright,uint8_t mirror){SH_ResLo;HAL_Delay(1);SH_ResHi;HAL_Delay(1);SH1106_WC(0xAE); //display offSH1106_WC(0xA8); //--set multiplex ratio(1 to 64)SH1106_WC(0x3F); //SH1106_WC(0x81); //--set contrast control registerSH1106_WC(contrast);if (mirror) {SH1106_WC(0xA0);SH1106_WC(0xC0);}else {SH1106_WC(0xA1);SH1106_WC(0xC8); }SH1106_WC(0xDA);SH1106_WC(0x12);SH1106_WC(0xD3);SH1106_WC(0x00);SH1106_WC(0x40);SH1106_WC(0xD9); //--set pre-charge periodSH1106_WC(bright);SH1106_WC(0xAF); //--turn on SSD1306 panel}

С ней, надеюсь все понятно, первый параметр контрастность (0-255), второй яркость (разбита на два полубайта, комбинации 0xX0 0x0X недопустимы), третий ориентация дисплея (0/1). Для понимания работы дисплея и дальнейших функций вывода изображения советую почитать статью на Датагоре Визуализация для микроконтроллера. Часть 1. OLED дисплей 0.96 (128х64) на SSD1306" и переведенный русский даташит SSD1306.


До бесконечного цикла в main.c проведем инициализацию дисплея :


/* USER CODE BEGIN 2 */sh1106Init (40,0x22,0);/* USER CODE END 2 */

На экране увидите графические узоры типа таких :


noise

В файле spi1106.h напишем определение сразу еще трех функций очистки, печати мелким и средним шрифтом :


void sh1106Clear(uint8_t start, uint8_t stop);void sh1106SmallPrint(uint8_t posx, uint8_t posy, uint8_t *str);void sh1106MediumPrint(uint8_t posx, uint8_t posy,uint8_t *str);

В функции очистки в файле spi1106.c выбор страницы с 0-ой по 7-ю осуществляется командой 0xB0...0xB7 и будет выглядеть так :


void sh1106Clear(uint8_t start, uint8_t stop){ uint32_t *adrclear;uint32_t timep,timec;uint8_t dt[128];adrclear=(uint32_t *)dt;for(uint8_t i=0;i<32;i++) {*adrclear++=0x00;}for (uint8_t m = start; m <= stop; m++){SH1106_WC(0xB0+m);SH1106_WC(2);SH1106_WC(0x10);SH_Data;SH_CsLo;HAL_SPI_Transmit_DMA(&hspi3,dt,128);timec=HAL_GetTick();timep=timec+50;while ((HAL_SPI_GetState(&hspi3) != HAL_SPI_STATE_READY)&&(timec<timep)){timec=HAL_GetTick();}SH_CsHi;}}

До основного цикла в main.c, после инициализации, вставим очистку :


sh1106Clear(0,7);

Можете поиграться с очисткой, задав заполнение своим узором, мы же двигаем далее, вставим в файл spi1106.c ближе к началу сперва маленький шрифт из файла DefaultFonts.c (взял из какой-то ардуиновской библиотеки). Особенностью данного шрифта является, то что он вертикально ориентирован (как раз под структуру отображения байта дисплеями SSD1306/SH1106). В начале массива символов необходимо удалить ненужные нам 4-е служебных байта. Весь фонт вы найдете в архиве библиотеки, в качестве примера, приведу лишь начало:


SmallFont
const uint8_t SmallFont[] =  //  Шрифт  SmallFont{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //001)0x20=032пробел0x00, 0x00, 0x00, 0x2F, 0x00, 0x00, //002)0x21=033!0x00, 0x00, 0x07, 0x00, 0x07, 0x00, //003)0x22=034"0x00, 0x14, 0x7F, 0x14, 0x7F, 0x14, //004)0x23=035#0x00, 0x24, 0x2A, 0x7F, 0x2A, 0x12, //005)0x24=036$0x00, 0x23, 0x13, 0x08, 0x64, 0x62, //006)0x25=037%0x00, 0x36, 0x49, 0x55, 0x22, 0x50, //007)0x26=038&0x00, 0x00, 0x05, 0x03, 0x00, 0x00, //008)0x27=039'0x00, 0x00, 0x1C, 0x22, 0x41, 0x00, //009)0x28=040(0x00, 0x00, 0x41, 0x22, 0x1C, 0x00, //010)0x29=041)0x00, 0x14, 0x08, 0x3E, 0x08, 0x14, //011)0x2A=042*0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, //012)0x2B=043+0x00, 0x00, 0x00, 0xA0, 0x60, 0x00, //013)0x2C=044,0x00, 0x08, 0x08, 0x08, 0x08, 0x08, //014)0x2D=045-0x00, 0x00, 0x60, 0x60, 0x00, 0x00, //015)0x2E=046.0x00, 0x20, 0x10, 0x08, 0x04, 0x02, //016)0x2F=047///0x00, 0x3E, 0x51, 0x49, 0x45, 0x3E, //017)0x30=04800x00, 0x00, 0x42, 0x7F, 0x40, 0x00, //018)0x31=04910x00, 0x42, 0x61, 0x51, 0x49, 0x46, //019)0x32=05020x00, 0x21, 0x41, 0x45, 0x4B, 0x31, //020)0x33=05130x00, 0x18, 0x14, 0x12, 0x7F, 0x10, //021)0x34=05240x00, 0x27, 0x45, 0x45, 0x45, 0x39, //022)0x35=05350x00, 0x3C, 0x4A, 0x49, 0x49, 0x30, //023)0x36=05460x00, 0x01, 0x71, 0x09, 0x05, 0x03, //024)0x37=05570x00, 0x36, 0x49, 0x49, 0x49, 0x36, //025)0x38=05680x00, 0x06, 0x49, 0x49, 0x29, 0x1E, //026)0x39=05790x00, 0x00, 0x36, 0x36, 0x00, 0x00, //027)0x3A=058:0x00, 0x00, 0x56, 0x36, 0x00, 0x00, //028)0x3B=059;0x00, 0x08, 0x14, 0x22, 0x41, 0x00, //029)0x3C=060<0x00, 0x14, 0x14, 0x14, 0x14, 0x14, //030)0x3D=061 =0x00, 0x00, 0x41, 0x22, 0x14, 0x08, //031)0x3E=062>0x00, 0x02, 0x01, 0x51, 0x09, 0x06, //032)0x3F=063?//0x00, 0x32, 0x49, 0x59, 0x51, 0x3E, //033)0x40=064@0x00, 0x7C, 0x12, 0x11, 0x12, 0x7C, //034)0x41=065A0x00, 0x7F, 0x49, 0x49, 0x49, 0x36, //035)0x42=066B// ...}


Напишем функцию печати маленьким шрифтом высотой одна страница (ширина 6, высота 8 точек), где posx знакоместо, кратное 6 пикселям дисплея, а posy номер страницы (0 верхняя, 7 нижняя):


void sh1106SmallPrint(uint8_t posx, uint8_t posy, uint8_t *str){uint8_t dt[128];uint16_t posfont, posscr;uint32_t *adrclr;uint16_t *adrdst,*adrsrc;uint32_t timer,timec;adrclr=(uint32_t *)&dt;uint8_t code;code=*str++;for(uint8_t i=0;i<32;i++) { *adrclr++=0; }posscr=posx*6;while (code>31){if(code==32) {posscr+=2;}else{posfont=6*(code-32);adrdst=(uint16_t *)&dt[posscr];adrsrc=(uint16_t *)&SmallFont[posfont];*(adrdst++)=*(adrsrc++);*(adrdst++)=*(adrsrc++);*(adrdst++)=*(adrsrc++);posscr+=6;}code=*str++;if (posscr>122) break;}SH1106_WC(0xB0+posy);SH1106_WC(2);SH1106_WC(0x10);SH_Data;SH_CsLo;HAL_SPI_Transmit_DMA(&hspi3,dt,128);timec=HAL_GetTick();timer=timec+50;while ((HAL_SPI_GetState(&hspi3) != HAL_SPI_STATE_READY)&&(timec<timer)){timec=HAL_GetTick();}SH_CsHi;}

Затем, в библиотеке spi1106.c после мелкого шрифта вставим средний (высота 16, ширина 12 пикселей) из того же файла DefaultFonts.c (также удалите 4 служебных байта в начале массива) и напишем функцию печати средним шрифтом высотой две страницы, где posx знакоместо, кратное уже 12 пикселям дисплея, а posy аналогично предыдущей функции номер страницы:


void sh1106MediumPrint(uint8_t posx, uint8_t posy, uint8_t *str){uint8_t dt[256];uint16_t posfont, posscr;uint32_t *adrdst, *adrsrc;uint32_t timer,timec;adrdst=(uint32_t *)&dt;uint8_t code;code=*str++;for(uint8_t i=0;i<64;i++) { *adrdst++=0; }posscr=posx*12;while (code>31){posfont=24*(code-32);adrsrc=(uint32_t *)&MediumFont[posfont];adrdst=(uint32_t *)&dt[posscr];*(adrdst++)=*(adrsrc++);*(adrdst++)=*(adrsrc++);*(adrdst++)=*(adrsrc++);adrsrc=(uint32_t *)&MediumFont[posfont+12];adrdst=(uint32_t *)&dt[posscr+128];*(adrdst++)=*(adrsrc++);*(adrdst++)=*(adrsrc++);*(adrdst++)=*(adrsrc++);code=*str++;posscr+=12;if (posscr>116) break;}SH1106_WC(0xB0+posy);SH1106_WC(2);SH1106_WC(0x10);SH_Data;SH_CsLo;HAL_SPI_Transmit_DMA(&hspi3,dt,128);timec=HAL_GetTick();timer=timec+50;while ((HAL_SPI_GetState(&hspi3) != HAL_SPI_STATE_READY)&&(timec<timer)){timec=HAL_GetTick();}SH1106_WC(0xB0+posy+1);SH1106_WC(2);SH1106_WC(0x10);SH_Data;SH_CsLo;HAL_SPI_Transmit_DMA(&hspi3,dt+128,128);timec=HAL_GetTick();timer=timec+50;while ((HAL_SPI_GetState(&hspi3) != HAL_SPI_STATE_READY)&&(timec<timer)){timec=HAL_GetTick();}SH_CsHi;}

В main.c после очистки напечатаем приветствие :


sh1106SmallPrint(0,0,(uint8_t *) "Hello SH1106_1234567890");sh1106MediumPrint(0,1,(uint8_t *) "Hi SH1106");sh1106MediumPrint(0,3,(uint8_t *) "Hello SH1106");

hello

Как видно, библиотека корректно обрезает строку, не налезая на следующую и не вылетая в HardFault при выходе за выделенный буфер. Можно было написать еще функцию вывода больших символов и графические примитивы, но честно говоря, мне рассмотренных пока хватает, оставим это на домашнее задание для вас.


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


/* Infinite loop *//* USER CODE BEGIN WHILE */uint8_t buf[128*8];char str[32];uint16_t count;uint8_t x,y,b;uint32_t timep,timec;while (1){count++;b=count&0x07;x=(count>>3)&0x7f;y=(count>>10)&0x07;buf[y*128+x]=buf[y*128+x]|(1<<b);timec=HAL_GetTick();for (uint8_t m = 0; m < 7; m++){SH1106_WC(0xB0+m);SH1106_WC(2);SH1106_WC(0x10);SH_Data;SH_CsLo;HAL_SPI_Transmit_DMA(&hspi3,buf+m*128,128);while ((HAL_SPI_GetState(&hspi3) != HAL_SPI_STATE_READY)){__NOP();}SH_CsHi;}timep=HAL_GetTick();sprintf(str, "%d", timep-timec);sh1106SmallPrint(0,7,str);/* USER CODE END WHILE */

speed

По скорости заполнения примерно одна страница в секунду и мелькающим в нижней строке то "0" то "1" можно сказать, что достигнута скорость обновления семи строк экрана менее 1ms, т.е. около 1000fps. Конечно с учетом реальных задач построения изображения в буфере, будет медленнее, но результат впечатляет. Да и задержку ожидания можно вставить до пересылки по SPI и распаралелить задачи отрисовки МК и перекидывания по SPI в режиме DMA.


Надеюсь данная публикация будет полезна вам для изучения алгоритма работы с данным дисплеем.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru