Изначально поставка задачи была такой. У меня была в наличии плата TM1638. Нужно было научиться с ней работать (ну и проверить работоспособность самой платы) для того, чтобы использовать её в одном интересном проекте (о нём в другой раз). Под рукой оказалась платка Arduino Nano. Хотелось быстро отделаться проверить работу самой платы при помощи ардуиновской библиотеки SPI.h – не получилось. В результате проделанный объём работы вылился в эту заметку.

Для начала скажу, что покупала я эту плату для выполнения заданий Школы Синтеза. И когда заказывала, я не смотрела на то, что она из себя представляет (у Школы есть готовые скрипты для подключения железа, что очень удобно при старте обучения с нуля). Когда она пришла, я поняла, что без скриптов я её подключить не могу, потому что у неё интерфейс SPI, а я с ним прежде никогда не работала даже в микроконтроллерах (вот уж не знаю как так вышло, что он обошёл меня стороной).

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

В классическом варианте SPI использует для передачи четыре провода: SCLK для передачи тактового сигнала, MOSI и MISO – для передачи данных от мастера (ведущего) к ведомому и обратно и SS для выбора ведомого. Подробнее о самом SPI и о подключении устройств к шине можно почитать в интернете (например, тут описано, на мой взгляд, хорошо и с картинками).

Модуль же TM1638 имеет пять пинов, два из них это земля и питание, а вот остальные три для обмена данными. Один для передачи ему тактового сигнала от мастера (CLK), один для управления процессом передачи (STB) и один для передачи самих данных (DIO). Т.е. данная реализация SPI является полудуплексной — для передачи и приёма данных используется один и тот же провод.

Для управления модулем есть несколько команд: управление яркостью (и включение/выключение) (Display Control), задание адресации (Address Command) и выбор чтения/записи (Data Command). 

Для настройки яркости используются следующие команды (переведённая и сокращённая мной таблица из документации):

Для выбора направления передачи и способа адресации есть следующие команды:

Для передачи адреса используются команды, начинающиеся с 0xC:

Для тех, кто не в курсе на всякий случай (ну вдруг) поясню, что если число, начинается с 0x или заканчивается на h, то это значит, что оно записано в шестнадцатеричной системе счисления.

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

Немного модифицированная мной табличка из документации:

Из неё понятно (ну или на самом деле не очень), что чётные адреса с 00h по 0Eh отвечают за подключение семисегментных индикаторов. Нечётные адреса же – за подключение кнопок и светодиодов. Причём светодиоды подключены к SEG10, а кнопки к SEG9.

Т.е. чтобы вывести цифру на первый слева индикатор, нужно записать данные по адресу 00h. Для того, чтобы зажечь первый справа светодиод, нужно записать число 2 по адресу 0fh (чтобы погасить, нужно записать ноль).

Собственно про цифры. В документации имеется распиновка индикатора и схема его подключения к самому чипу. 

Т.е. сегмент a — это младший бит, а dp — соответственно, старший. Для того, чтобы вывести цифру ноль нужно зажечь все сегменты, кроме g и dp — это 0x3f. Чтобы вывести единицу, нужно зажечь сегменты b и c — это 0x06 и т.д. В таблице приведены двоичные и шестнадцатеричные коды для всех цифр.

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

Прочитав документацию по диагонали, и ещё пару статей в интернете, я стала подключать модуль TM1638 к Arduino Nano. Для этого мне понадобилось заглянуть в распиновку адруины и выбрать там пины аппаратного SPI. Получились такие: пин 13 — SCK, пин 11 — MOSI, пин 10 — SS (ножек три, так как у нас полудуплекс). 

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

Ножки при этом я выбрала другие, но на самом деле это абсолютно неважно. Передача команды у меня выглядела так:

void transfer (uint8_t data){
 digitalWrite (SS,LOW);
 shiftOut (DATA_PIN, CLK, LSBFIRST, data);
 digitalWrite (SS,HIGH);
 _delay_ms(100);
}

Где SS — это пин, к которому подключен вход STB модуля TM1638, CLK — вход SCLK модуля, DATA_PIN — вход DIO. У меня это были пины 10, 12 и 11 (PB2, PB4 и PB3). Писать функцию digitalWrite через регистры я тоже не стала, ибо в данной ситуации мне это ни к чему. Функция shiftOut осуществляет сдвиг данных, т.е. выдаёт биты по одному на ножку DATA_PIN, сопровождая при этом передачу тактовыми импульсами на ножке CLK. Параметр LSBFIRST указывает, что передача будет осуществляться старшим битом вперёд (да, в документации указано, что передача должна осуществляться именно так).

Выдав с помощью этой функции команду включения и настройки яркости, команду настройки инкремента адреса (в данном случае подходит только запись с фиксированным адресом, т.к. адреса расположены не по порядку), сам адрес и какие-то данные, я ожидала, что всё заработает. Экран зажегся (уже прогресс) и вывел всякий мусор. 

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

Вообще, это всё описано в документации и даже с картинками, но я до туда не дочитала (как всегда).

Вот, собственно, временные диаграммы передачи данных из документации:

Где:

  • Command1:  установить режим записи с фиксированным адресом

  • Command2: установить адрес

  • Data1: Отправка данных по адресу, указанному в Command2

  • Command3: установить адрес

  • Data2: отправка данных по адресу, указанному в Command3 и т.д.

Т.е. для отправки команд настройки, нужно сначала опустить в ноль STB, отправить восемь бит команды (не забывая передавать импульсы по CLK), поднять STB (установить логическую единицу) и сделать паузу. Для того, чтобы передать данные, нужно опустить STB, отправить восемь бит адреса, затем восемь бит данных и только потом поднять STB. 

Исходя из этой информации, я дописала вторую функцию для передачи данных.

 void transferData(uint8_t addr, uint8_t data)
{
	digitalWrite(SS,LOW);
	shiftOut(DATA_PIN, CLK, LSBFIRST, addr);
  shiftOut(DATA_PIN, CLK, LSBFIRST, data);
  digitalWrite(SS,HIGH);
  _delay_ms(100);
}

В общем, после всех мытарств, оно заработало и вывело красивые циферки.

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

Полный код из двадцати строк здесь.

Буду рада, если кому-то это оказалось полезным.

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


  1. GennPen
    29.04.2024 06:47
    +3

    Использовать ногодрыганье когда есть аппаратный SPI - такое себе.

    Вот тут посмотрите как реализована работа по SPI с этим чипом: https://github.com/i2make/TM1638_using_hardwareSPI/blob/91d131355ccdd123a5a64fbfb2d4ce565ab7c0c8/src/TM16XX.cpp#L141


    1. kass_andra_go Автор
      29.04.2024 06:47

      посмотрю, спасибо


  1. kalapanga
    29.04.2024 06:47

    "Всё украдено написано до нас" :)

    https://www.arduino.cc/reference/en/libraries/tm1638/


    1. GennPen
      29.04.2024 06:47
      +1

      Самому учиться программировать интересней. Но ... В этой библиотеке используется стандартная функция shiftOut() в которой по сути используется то же самое ногодрыганье: https://github.com/arduino/ArduinoCore-avr/blob/63092126a406402022f943ac048fa195ed7e944b/cores/arduino/wiring_shift.c#L40
      Я не спорю хорошо это или плохо, но не лучше ли сразу учиться использовать нужные интерфейсы правильно?


      1. kass_andra_go Автор
        29.04.2024 06:47
        +1

        Изначально мне нужно было проверить работоспособность купленной платы, но потом я решила воспользоваться ситуацией и детальней разобраться с тем как устроен и работает SPI. Я сейчас пытаюсь реализовать аппаратный spi на fpga, опять же исключительно в целях самообразования. И «ногодрыганье» мне помогло это получше в голове уложить.

        Честно говоря, я долго думала, делиться своим «велосипедом» или нет,но если бы не поделилась, то не получила бы столько ценных советов и ссылок


        1. GennPen
          29.04.2024 06:47

          Все нормально. Если знаний в голове прибавилось - значит время потрачено не зря. =)


  1. AVKinc
    29.04.2024 06:47
    +1

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

    Я писал полностью свою реализацию отображения на нем. У меня волосы зашевелились на голове когда я прочитал описание адресации сегментов )))))


    1. Yuri0128
      29.04.2024 06:47

       есть вариант хитрого подключения

      Так а в чем проблема? Не используйте индикаторы с общим анодом, для индикаторов с общим катодом там все норм. А если вдруг надо - то перекодировать буфер, ортогонально развернув индикацию, - ну несколько минут для написания программы и немного тактов для его обслуживания.

      А вот написать полудуплекс для чтения клавиатуры совсестно с выводом индикации на TM1638 при работе на пристойных скоростях SPI (>2 MHz, ну для Ардуино пристойных) - ну задачка уже поинтереснее.


    1. kass_andra_go Автор
      29.04.2024 06:47

      Я когда стала читать на неё описание, не могла никак принять, почему именно так в ней всё странно расположено


  1. NutsUnderline
    29.04.2024 06:47

    может и велосипед, но мне нравится что детально разжевано


    1. kass_andra_go Автор
      29.04.2024 06:47
      +1

      спасибо


  1. Vedga
    29.04.2024 06:47

    Раз уж влезла в программирование мк, не пожалей 7-10т.р. на простенький осциллограф. На пару каналов, хотя бы. Тогда вопросы "почему не работает/работает не так" будут решаться быстрее.

    Или, как вариант, макетка FPGA с включённым анализатором сигналов. Особенно если потом в область verilog залезть захочется.


    1. kass_andra_go Автор
      29.04.2024 06:47
      +1

      У меня есть возможность воспользоваться осциллографом, но почему-то конкретно в этом случае мне такая мысль в голову не пришла

      За логический анализатор на FPGA отдельное спасибо! это интересно, обязательно попробую воспользоваться


    1. sim2q
      29.04.2024 06:47

      В подавляющем большинстве случаев для начала достаточно логического анализатора на cy7c68013a за 400руб.


  1. fiego
    29.04.2024 06:47

    Параметр LSBFIRST указывает, что передача будет осуществляться старшим битом вперёд (да, в документации указано, что передача должна осуществляться именно так).

    Показания документации на shiftOut() расходятся с вашими:

    MSBFIRST or LSBFIRST. (Most Significant Bit First, or, Least Significant Bit First).


    1. kass_andra_go Автор
      29.04.2024 06:47
      +1

      ой! надо всё перепроверить


  1. sim2q
    29.04.2024 06:47

    Но вообще-то он не SPI, а I2C без адреса, отправляется на него по I2C нормально, а вот чтение несколько сложнее.