Управление MAX7219 при помощи STM32. Часть первая - введение.

Добрый день.
На предыдущем занятии мы поняли как можно работать с семисегментным индикатором.
Но что, если нам потребуется выводить не цифры от 0 до 9, а числа, предположим десятизначные!
Нам в этом случае потребуется использовать очень много выводов, а их столько у нас нет.
Существуют микросхемы, которые специально спроектированы, что бы управлять семисегментными индикаторами.
Но возникает другая сложность - как этой микросхеме объяснить что именно требуется отобразить.

Для решения этой задачи человечество придумало связывать устройства при помощи цифровых линий связи.
Сегодня мы познакомимся с одной из их разновидностей.
Представляю Вашему вниманию микросхему MAX7219, которая заботливо припаяна китайцами на небольшой модуль, и управляет одновременно восемью! индикаторами.
Если открыть даташит на эту микросхему, то можно увидеть, что она с микроконтроллером можем связываться при помощи трёх интерфейсов - SPI QSPI и MICROWIRE.
На самом деле - QSPI И MICROWIRE - разновидности SPI интерфейса. А так как интерфейс SPI более распространён - в первую очередь будем разбираться как работать с ним.

Для начала - совсем чуть чуть теории.
SPI - последовательный полнодуплексный четырёхпроводной синхронный интерфейс.

Теперь немного подробнее всё таки.
тут обязательно надо нарисовать анимацию.
Четырёхпроводной - для связи используется 4 провода.
Синхронный - при передаче данных используются синхроимпульсы.
Последовательный - данные передаются последовательно один за другим.
Полнодуплексный - данные могут передаваться одновременно как от ведущего к ведомому, так и в обратном направлении.

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

Ну и как обычно - задача на занятие.
У нас есть две кнопки.
при нажатии верхней - необходимо увеличивать число на индикаторе
при нажатии нижней - уменьшать.
если одна из кнопок зажата более чем на 1 секунду - изменять числа необходимо десятками.
Если зажата более чем на 5 секунд - сотнями.
Если нажаты обе вместе - сбросить счетчик в ноль, и не изменять его, пока не будут отпущены обе кнопки.

Приступаем.
сохраняем предыдущий проект.
git commit.
git push.
Создаём новый репозиторий.
Клонируем.
CubeMX.
Отладка, кнопки, подтяжка.

И наконец что-то интересное.
Раскрываем подменю Connectivity, и видим там SPI1. Выбираем его.
И начинаем базовую настройку.
Mode - какой режим передачи будет использоваться. В нашем случае, мы отравляем данные в одну сторону, и в ответ ничего не собираемся принимать, так же мы на себя берём генерацию синхроимпульсов
по этому выбираем Transmit Only Master.

Hardware NSS Signal - аппаратный режим работы модуля генерирующего сигнал Chip Select.
Пока не будем его использовать.
Далее - Basic Parameters
Frame Format - Нам доступен только 1 Motorola, выбираем его.
Для тех, кто всё таки почитал википедию про SPI - это выбор режима CPHA
Motorola - CPHA равен единице. TI - нулю.

Data Size - идём смотреть даташит на нашу микросхему.
В разделе Serial-Addressing Modes находим - данные отправляются в 16 битных пакетах.
Отлично, устанавливаем Data Size в 16 Bits.

First Bit - какой бит необходимо передавать первым. Старший или младший.
Возвращаемся к даташиту, и смотрим на Timing Diagram.
В ней видно, что сначала передаётся D15, потом D14, и так до D0.
Получается нам необходимо передавать данные в режиме старший бит - первый, или MSB. Выбираем.
Дальше - тайминги.
Prescaler - на сколько необходимо поделить частоту тактирования, для работы с этим устройством.
Видим, что сейчас выбрано значение 2.
Мы пока что еще не касались настроек и режимов работы тактирования, но можно уже косвенно сделать
вывод, что микроконтроллер работает на частоте 8 Мегагерц.
Но нам сейчас надо тут что-то выбрать, а не отвлекаться.
По этому - опять же идём в даташит.
Поднимаемся в самый верх, и видим жирным шрифтом 10MHz Serial Interface.
Отлично, мы не можем поставить значение ниже 2, следовательно максимально доступные
сейчас для нас 4 мегабита устраивают.
Как можно сравнить Мегагерцы и Мегабиты - очень просто. За 1 такт передаётся 1 символ.
Получается при 10 мегагерцовом тактировании будет передаваться информация со скоростью 1- Мегабит.

Clock Polarity (CPOL). Придётся всё таки еще раз сходить на википедию, и сравнить картинку там,
с Timing Diagram в даташите.
Из даташита мы видим, что в режиме ожидания, уровень сигнала Clock - низкий.
На картинке из вики - этому соответствует CPOL = 0. Выбираем Low.

Clock Phase - на каком фронте тактового сигнала будет находиться середина времени передачи бита данных.
На первом, или на втором.
Глядя на Timing Diagram - Видим что на первом.

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

Наконец то всё, даём проекту имя, тулчейн, и генерируем код.
VSCode, импорт проекта.
Подготовка - завершена.

Осталось понять какие данные и как необходимо передавать.
Пока что мы только знаем что пакет данных должен содержать 16 бит. То есть 2 байта.

Идём читать даташит.
в разделе Serial-Addressing Modes можно прочитать:
После передачи - данные фиксируются либо в цифровом, либо в управляющем регистре по нарастающему фронту сигнала LOAD/CS.

Недоработка, мы не выбирали ни какую ножку микроконтроллера для управления выводом Chip Select.
Возвращаемся в CubeMX, устанавливаем для пина PA6 режим Output, и даём ему имя CS.
Перегенерируем проект.
Изучаем этот раздел даташита дальше.

биты D8 - D11 содержат адрес регистра.
биты D0 - D7 - данные.
биты D12- D15 - не используемые.
Далее видим, что таблица 2 описывает 14 адресуемых цифровых и контрольных регистров.
Ищем таблицу 2.

Отлично, мы теперь знаем что мы можем положить в биты D8-D11.
И очень к стати, мы видим главу Initial Power-Up.
Из которой следует, что после включения все регистры сброшены, индикаторы - выключены, и микросхема находится в режиме выключения. Нам советуют настроить микросхему перед использованием. В противном случае - будет выводиться 1 цифра (тут это называется scan mode), не будет декодирования данных, а интенсивность будет установлена на минимум.

Отлично, уже появилось понимание что нужно сделать.
получается требуется следующая последовательность действий:

  1. вывести микросхему из "выключенного" режима.
  2. заставить её выводить все 8 цифр
  3. установить яркость хотя бы на половину.
  4. во все регистры цифр записать данные. Пока не будем разбираться что за декодирование. Просто попытаемся наобум записать туда цифры от 1 до 8.

так же, можно заметить, что в таблице 2, которую мы нашли, есть регистр Display Test.
Для проверки корректности подключения - идеальный кандидат.
наш первоначальный алгоритм сокращается до 2 шагов:

  1. вывести микросхему из "выключенного" режима.
  2. запустить тест дисплея.

Хорошо. Смотрим дальше, на форматы этих регистров.
Регистр выключения, адрес 0xXC, где X - произвольное значение.
Нам надо режим Normal Mode. следовательно нам надо в этот регистр положить 1 бит, в D0 единицу.

Регистр тестирования дисплея. Адрес 0xF, так же всего 1 бит. нам надо туда тоже положить единицу.

Запишем то, что мы нашли прямо в main.c в виде комментария.
сначала - побитно.
В калькуляторе, в режиме программиста посмотрим, что 0xC - это вот такая последовательность битов

Наконец то разобрались что передавать. Теперь осталось последнее - как.
Для передачи данных по SPI есть функция HAL_SPI_Transmit.
она на вход ожидает 4 аргумента:
указатель на конфигурационную информацию.
указатель на буфер данных
размер этого буфера
и таймаут по истечении которого она прекратит попытки отправки, в случае ошибки.
получается нам надо переменная для буфера. Хорошо, объявляем. Создадим массив из 1 элемента,
uint16_t;

далее - указатель на конфигурационную информацию.
его мы можем найти просто пролистав main чуть ниже.
видим комментарий SPI1 parameter configuration.
И помня что мы в кубе настраивали SPI1 делаем вывод что необходимый нам указатель - это указатель на hspi1.
С аргументами разобрались.

Кладём в наш массив первые 2 байта, для включения обычного режима.
Хотя как кладём. Давайте на калькуляторе считать что надо записать в эту переменную.
0xC сдвигаем на 8 разрядов в лево, и добавляем 1. Получилось 0xC01. Отлично, запомним это, что бы не считать на калькуляторе каждое значение.

Дальше вроде всё просто.
Отправляем данные.
Вспоминаем, что нам необходимо после передачи данных подать высокий уровень на вывод Chip Select.
Добавляем после отправки. А раз нам необходимо именно что бы было изменение уровня, значит перед отправкой необходимо подать низкий уровень.

Собираем вторые два байта
Опять отправляем.
Дёргаем Chip Select

Запускаем программу, и смотрим что же получилось.
Новостей две, как обычно плохая и хорошая.
Хорошая - все сегменты на всех индикаторах включились!
Значит мы на верном пути.
Плохая - мы получили предупреждение компилятора что мы используем переменную длинной 16 бит, там, где подразумевается восьмибитное число.

Пока что не будем вдаваться в дебри устройства ХАЛ, просто сделаем явное приведение типов.

давайте теперь изменим нашу программу, в соответствии с нашим первоначальным алгоритмом:

  1. вывести микросхему из "выключенного" режима.
  2. заставить её выводить все 8 цифр
  3. установить яркость хотя бы на половину.
  4. во все регистры цифр записать данные. Записать туда цифры от 1 до 8.


TODO написать это прямо в комментарий в VSCode.
первое - мы умеем
второе - идём искать в даташит как это сделать. Нам надо регистр 0xB.
что бы задействовать все цифры - надо записать туда 7.
третье - регистра 0xA туда записать 8
ну и последнее - это регистры с первого по восьмой включительно.
пошли писать...

Отключим проверку дисплея

немного оптимизируем запись использованием цикла.
Эх, ещё бы без ошибок оптимизировать....

Можно проверять что получилось.

К сожалению ничего хорошего не получилось, но мы уже близко.
У нас остался последний регистр, который мы не разобрали. Это регистр декодирования.
Идём смотреть даташит. Самое главное там для нас - это если туда записать 0 - декодирование не будет
производиться. Если записать FF - будет.
Попробуем записать 0, перед отправкой данных.

Прошиваем, проверяем. Нет, не оно. Хорошо. Значит записываем туда 0xFF.
И опять ничего. Внимательно смотрим на написанный код, и приходит осознание того, что цикл последней итерацией захватывает 9 регистр, и перезаписывает его. Окей, исправляем, прошиваем, проверяем.

Мы передали цифры в регистры данных, и мы увидели цифры на индикаторе!

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

Важное дополнение.
Самое главное - эту микросхему не стоит использовать с 3,3 вольтовой логикой. Она всё таки рассчитана на работу с 5 вольтами.
Из неописанных в даташите багов - скорее всего из-за того, что работаю с пониженным напряжением питания - после подачи питания необходимо подождать 0,5 секунды, прежде чем передавать данные.
Иначе часть входящих данных может быть пропущена.
Так же очень желательно добавить внешний электролитический конденсатор.

А на это всё, спасибо за внимание.