Интерфейс управления GPIO

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

Чтобы последний успевал делать хоть что-то, разработчики МК поместили на кристалл отдельную схемотехнику для самых распространенных протоколов: SPI I2C UART.
Каждая из этих аппаратных реализаций имеет свой входной FIFO буфер. Размеры отличаются в зависимости от протокола и камня - от двух до нескольких десятков байт.
В этом случае CPU берет данные из RAM, кладет их в специальный регистр и запускает тот же SPI, записав пару флагов в его спец регистры. Как только SPI закончил, может произойти два варианта - он поставит спец флаг означающий "буфер пустой" или дернет прерывание которое CPU обработает.
Представим теперь, что нам нужно затолкать буфер в OLED 128х64 дисплей - 1024 байта, на каждый из которых процессор должен мотаться перекладывая данные внутри RAM.
Чтобы этого избежать, инженеры ввели специальный аппаратный модуль - DMA.
Direct Memory Access нужен как раз для такой тупой работы. CPU скажет (а говорит от всегда через запись каких-то битов в какие-то регистры) "клади 1024 байта начиная с 0xXXXXXXXX в регистр данных SPI (у него есть адрес в памяти)".

Чтобы он понимал с какой скоростью заталкивать туда биты (а он делает это вообще говоря быстрее чем периферия отрабатывает), вся периферия подпаяна "проводочком" DREQ с модулем DMA. Считайте что это некоторая упрощенная версия прерывания. У каждого такого проводочка есть свой числовой ID (лежат в hardware/regs/dreq.h).

#define DREQ_SPI0_TX 16
#define DREQ_SPI0_RX 17
//...
#define DREQ_UART0_TX 20
#define DREQ_UART0_RX 21

Когда мы конфигурируем DMA - мы складываем эту чиселку в его управляющий регистр. Во время работы контроллер будет ожидать сигналы от DREQ данной указанной периферии.

DMA на rp2040 имеет 12 каналов, которые гоняются арбитром по Round-Robin. Канал это ресурс - при написании кода с ними нужно быть осторожными. Например библиотека "oled_lib_by_vasek1337.h" могла захардкодить себе несколько каналов. Pico SDK предоставляет следующие методы:

int my_dma_channel = dma_claim_unused_channel(true); // (bool panic)
dma_channel_unclaim(my_dma_channel);

Специфика i2c на rp2ххх

У аппаратного I2C в RP2040 используется 16-битный регистр IC_DATA_CMD. Чтобы отправить байт по I2C, вы обязаны записать туда 16-битное слово - 8 бит на данные и 8 бит на настройки. Причем, последний байт передаваемых данных обязательно должен содержать stop бит.

Это усложняет работу с DMA. На выбор у вас есть три решения:

  • Хранить framebuffer дисплея в памяти не как массив uint8_t (1024 байта), а как массив uint16_t (2048 байт). Соответствующим образом подготовив биты.
  • Один канал DMA кидает данные, а второй (контрольный) канал по завершении первого просыпается и дозаписывает команду STOP в регистр управления I2C.
  • официальном репозитории pico-examples есть пример I2C реализованного через PIO.

Лабораторная 2

Собрать физический интерфейс управления GPIO для rp2040. Используем OLED и ротационный энкодер

Условия:

Использование сторонних библиотек запрещено.

Задачи:

  • Подключить дисплей из набора через аппаратный i2c, используя модуль DMA. Понять и объяснить проблему и обосновать решение.
  • Подключить и обосновать способ обработки ротационного энкодера.
  • Придумать интерфейс для отображения GPIO пинов:
    • Отображение текущего состояния
    • Режим и его смена (input/output/pwm)
  • Так как дисплей миниатюрный, продуманность и качество интерфейса оценивается выше, чем количество управляемых пинов. Уместите сколько сможете не отходя от вашей задумки, но хочется видеть хотя бы 8.

Допзадачи:

  • Применить pio для обработки энкодера.
  • Наивный подход к обновлению экрана - обновить framebuffer - затолкать framebuffer. sd13xx в стандартном режиме принимает данные "полосками", но ширина полоски может быть сконфигурирована - так мы будем обновлять только тот квадрат, что изменился (Dirty Rectangles). Нужно предложить удачную реализацию такой оптимизации.