LED maticové displeje jsou široce používány v aplikacích, jako jsou vývěsní štíty, hodiny a zobrazení počasí. Jsou to zařízení s nízkým rozlišením, navržená tak, aby byla pozorována z dálky. Mnoho z těchto zařízení je založeno na sériovém osmimístném displeji s integrovaným obvodem Maxim MAX7219. Tyto integrované obvody mohou ovládat 64 nezávislých LED, elektricky uspořádaných do mřížkového vzoru. Tradičně by LED diody byly nezávislé pruhy sedmisegmentového číselného displeje, ale lze také vytvořit přímou čtvercovou matici LED.

Předpřipravené LED matice založené na MAX7219 jsou široce dostupné ve velikostech 8x8, 32x8 a větších. Větší displeje jsou obvykle rozděleny do polonezávislých modulů 8x8. Každý modul má svůj vlastní MAX7219 a integrované obvody jsou zřetězeny, takže jediná sada řídicích linek může ovládat všechny moduly společně.

Druh zařízení, kterého se tento článek většinou týká, je znázorněn na fotografii níže. Tato zařízení jsou široce dostupná od online dodavatelů a v Čechách stojí v rozmezí 163 až 177 Kč. Ten zobrazený na fotografii je sestaven ze čtyř modulů 8x8 a má připojení na obou koncích, takže jednotky mohou být zřetězeny, aby poskytly větší displeje. Displeje však nejsou reverzibilní — jak vysvětlím, existuje "vstup" a "výstup".

pico7219 breadboard

Je také možné získat sedmisegmentové zobrazovací moduly založené na MAX7219. Článek popisující tyto moduly zde

sedmisegmentovka 8z sedmisegmentovka 8z chip

LED displej - 7 segmentový, 8 znaků MAX7219 - červený (43 Kč) — čip je čínský klon bez označení, nikoliv originální MAX7219

Ty fungují v zásadě stejným způsobem jako mřížka LED, s výjimkou jiného rozložení LED a jinak vypadají. Protože jsou sedmisegmentové displeje (nebo alespoň byly) tak běžné, má obvod MAX7219 vestavěnou logiku pro dekódování číselných dat do specifických vzorů pruhů LED. Tuto logiku v tomto článku popisovat nebudu; většina základních principů zůstává stejná, ať už máte co do činění s maticí LED nebo 7místným polem displeje.

Poznámka:
Možná si všimnete, že k připojení Pico k modulu displeje nepoužívám řadič úrovně.
Modul je navržen tak, aby běžel na 5V nebo tak, a napájím ho z USB připojení.
Datasheet říká, že "logicky vysoká" úroveň signalizace je alespoň 3,5 V a GPIO Pico toho nedosáhne.
V praxi to ale vypadá, že to funguje.
Na rozdíl od I2C jsou linky SPI schopné dodávat proud a já bych k GPIO Pico nepřipojoval 5V výstup.
Jsme zde v bezpečí, protože MAX7219 nemá žádné výstupy.

SPI rozhraní

MAX7219 používá třívodičové sériové rozhraní. Toto rozhraní používá hodinovou linku, datovou linku a "výběr čipu" (chip select). V datovém listu jsou tyto čáry rozhraní označeny CLK, DIN (data in) a LOAD. Účelem linky výběru čipu je indikovat, kdy jsou do modulu dodávána data. Alternativou k vytvoření velmi dlouhého řetězce LED modulů je jejich paralelní připojení a použití různých linek výběru čipu k označení, který konkrétní modul je zapsán. Ve skutečnosti může být kombinace řetězení a používání jednotlivých linek výběr čip užitečná při práci s velmi velkými displeji.

Pro uložení jednoho bitu dat nastaví mikrokontrolér datový bit na lince DIN a poté pulsuje CLK high. Datové bity jsou seskupeny do paketů podle řádku LOAD (výběr čipu). Tato linka je aktivní nízká, což znamená, že ji regulátor nastaví na začátku přenosu dat na nízkou, odešle data a poté ji znovu nastaví na vysokou. Když je LOAD vysoká, data se zablokují a MAX7219 je interpretuje. Čip přijímá 16bitové datové pakety, i když se používá pouze 12 bitů. Pokud linka výběru čipu zůstane vysoká, MAX7219 ignoruje, co se děje na datových a hodinových linkách – právě to umožňuje paralelní použití modulů.

Tento druh datového rozhraní je široce kompatibilní se schématem SPI ("sériové periferní rozhraní"), které podporuje většina mikrokontrolérů. Zařízení, jako je MAX7219, je dokonale možné ovládat nastavením specifických pinů GPIO na potřebné logické úrovně v časech řízených softwarem, což je proces známý hovorově jako „bit bang“. SPI je však tak populární, že většina mikrokontrolérů má k tomu vestavěný hardware a Pico není výjimkou. Pico C SDK má specifické funkce pro odesílání a přijímání dat přes SPI, jak vysvětlím později.

Ačkoli rozhraní SPI mají ustanovení pro odesílání i přijímání, v této aplikaci Pico pouze odesílá. Protože SPI je poměrně obecné rozhraní, uvidíte, že pro signalizační linky kompatibilní s SPI se používají různé názvy. Myslím, že nejoblíbenější jsou MOSI (master out, slave in), MISO (master in, slave out) a SCK (sériové hodiny) a CS (výběr čipu — chip select). Datový list Pico používá štítky TX, RX, SCK a CS. Vztah mezi těmito různými schématy pojmenování, který musíme mít na paměti při zapojování displeje, je uveden v tabulce níže.

funkce SPI Pico MAX7219

výstup s řadiče

MOSI

TX

DIN

vstup do řadiče

MISO

RX

CLOCK

výběr čipu

CS

CS

LOAD

Zobrazovací modul, který používám, označuje svá připojení „DIN“, „SCK“ a „CS“, takže zapojení by mělo být relativně jasné (pokud tomu tak není, viz konkrétní příklad později).

Jak jsou zařízení s čipem MAX7219 zřetězena

Toto je část schématu rozhraní, která pravděpodobně způsobí zmatek, pokud vůbec nějaký je. Níže uvedené schéma zapojení ukazuje, jak jsou zařízení MAX7219 obvykle uspořádána ve vícemodulovém designu. Každý modul má společné připojení k SCK, CS a napájecím linkám. Pouze první modul v řetězci má přímé připojení k MOSI; moduly jsou zřetězeny připojením pinu DOUT (datový výstup) jednoho MAX7219 k pinu DIN dalšího zařízení v řetězci.

Řetězení zařízení

pico7219 circuit

Každé zařízení MAX7219 v řetězci vyžaduje 16bitový datový paket (jehož formát popíšu později). Pokud všechna zařízení sdílejí společné linky SCK a CS, jak můžeme oslovit jednotlivá zařízení v řetězci?

No, nemůžeme — vlastně ne. Co však můžeme udělat, je poslat více 16bitových datových paketů, jeden paket pro každé zařízení MAX7219, takže každé zařízení dostane 16 bitů z celkového přenosu.

Na začátku přenosu dat jsou data čtena prvním MAX7219 v řetězci, dokud nepřečte 16 bitů. Poté, pokaždé, když je taktován nový bit, data v prvním MAX7219 přetečou jeho posuvný registr a dřívější data se objeví na DOUT, a tedy na DIN dalšího zařízení. Takže po odeslání 32 bitů jsme zaplnili posuvné registry dvou zařízení. Po 64 bitech jsme zaplnili čtyři zařízení a tak dále.

Potenciálně matoucím aspektem této operace je, že první odeslaný datový bit ve skutečnosti skončí v zařízení MAX7219 na konci řetězce, nikoli na začátku. Při psaní softwaru to může vést ke značnému poškrábání hlavy.

Z předchozí diskuse by mělo být jasné, že pro změnu stavu LED v modulu nejblíže řadiči jsme mohli v zásadě poslat pouze 16 bitů. Pro úpravu modulu nejvzdálenějšího od regulátoru však musíme poslat 4 x 16 = 64 bitů a musíme zajistit, že nastavíme správné hodnoty i pro LED v modulu, které měnit nechceme. Protože neexistuje způsob, jak číst stav LED diod, software musí sledovat stav každé LED v matici. Vzhledem k tomu, že stav každé LED vyžaduje pouze bit úložiště, není to pro moderní mikrokontrolér zatěžující.

Datový paket pro MAX7219

MAX7219 očekává 16bitový datový paket, ale je použito pouze 12 bitů. Důvodem pro použití 16bitového paketu je to, že mikrokontroléry pravděpodobně posílají data bajt po bajtu. Většina ve skutečnosti nebude schopna dělat nic jiného.

Můžeme si tedy představit datový paket jako dva bajty, z nichž první bajt používá pouze spodní čtyři bity. Tyto čtyři bity mají dvojí účel: když jsou nastaveny na číslo mezi 1 a 8, indikují, že bajt, který následuje, je bitový vzor pro konkrétní řadu LED (řádky jsou číslovány 1-8; zápis "řádek 0" nemá žádný vliv). Když jsou spodní čtyři bity prvního bajtu číslo mezi 10 a 15, je toto číslo interpretováno jako příkaz a druhý bajt je interpretován jako argument příkazu.

Takže například sekvence bajtů {3, 0xAA} indikuje, že LED diody v řadě tři by měly být nastaveny podle hexadecimálního čísla 0xAA (to je binární 10101010, takže LED bude nastavena střídavě on-off). Na druhou stranu odeslání {10, 3} nastaví jas LED na 3 (z 15 možných úrovní). V datasheetu je samozřejmě seznam kódů příkazů. V praxi bude příkaz jasu pravděpodobně jediný, který je třeba po nastavení zařízení změnit.

Jak budeme programovat SPI na Picu

Pico C SDK závisí, v dobrém i ve zlém, na použití CMake. V následujícím popíšu, co je potřeba nastavit v CMake. Normálně CMake nepoužívám a nechci se do toho dostat dál, než musím. Takže nutně nevím, co tato nastavení dělají na úrovni sestavení. Pokud to chcete vědět, hodně štěstí při hledání. Pokud vím, lidé z Raspberry Pi nechtějí proces sestavování dokumentovat podrobněji než „použít CMake“.

Chcete-li používat knihovnu SPI, musíte ji povolit v souboru CMake CMakeLists.txt takto:

target_link_libraries (my_binary hardware_spi hardware_gpio ...)

Pocom céčkový kód bude vypadat takto:

#include "hardware/spi.h"
#include "hardware/gpio.h"

Piny GPIO v Pico jsou multifunkční. Přestože jsou zde vestavěna pouze dvě rozhraní SPI, každé je lze přiřadit ke dvěma párům pinů GPIO. To znamená, že nastavení je dvoufázový proces: nastavení samotného rozhraní SPI a přiřazení příslušných pinů SPI. Budete se muset podívat na pinoutový graf Pico, abyste viděli, které kolíky jsou k dispozici pro který kanál SPI. V tomto článku předpokládám, že chceme použít SPI0 na pinech 17 (CS, chip-select), 18 (SCK, hodiny) a 19 (MOSI nebo TX).

Zde je základní kód pro nastavení rozhraní SPI:

#define MOSI 19
#define SCK 18
#define CS 17
#define SPI spi0
#define BAUD (1000*1000)

spi_init (SPI, BAUD);

gpio_set_function (MOSI, GPIO_FUNC_SPI);
gpio_set_function (SCK, GPIO_FUNC_SPI);

gpio_init (CS);
gpio_set_dir (CS, GPIO_OUT);
gpio_put (CS, 1);
Poznámka:
Je těžké dát obecnou radu ohledně přenosové rychlosti -- v mých testech 1 000 kbaud funguje dostatečně dobře
s krátkými připojeními v nehostinném prostředí.
Podle datasheetu MAX7219 by zařízení mělo být schopné alespoň dvojnásobku této rychlosti, možná o něco více.
To by však bylo za ideálních podmínek. Neznám lepší způsob, jak nastavit přenosovou rychlost než testováním.

Všimněte si, že ačkoli je linka CS součástí skupiny pinů SPI, rozhraní SPI API ji neřídí. To proto, že neví, kolik bajtů tvoří přenos dat. Takže, abychom zapsali dva bajty dat do prvního MAX7219 v řetězci, v zásadě bychom udělali toto:

gpio_put (CS, 0); // Set CS low to start the transfer

uint8_t buf[] = {first_byte, second_byte};
spi_write_blocking (self->spi, buf, 2);

gpio_put (CS, 1); // Set CS high at the end

To znamená, že vymezujeme výstup dat nízkým stavem CS linky.

V praxi tento kód nebude na Pico fungovat spolehlivě, protože je příliš rychlý. Linka CS se musí usadit ve svém novém stavu na několik hodinových cyklů, než odešle jakákoli data. Příklady SDK používají tento přístup ke generování velmi krátkého zpoždění:

gpio_put (CS, 0);
asm volatile ("nop \n nop \n nop");
...
gpio_put (CS, 1);
asm volatile ("nop \n nop \n nop");

Tyto NOP instrukce nedělají nic - to je celý účel: pouze absorbují několik hodinových cyklů.

Zbývajícím kouskem skládačky je, jak oslovit více zařízení v řetězci. Jak jsem řekl výše, nemůžeme to udělat. Musíme odeslat data do každého modulu až po modul, který má být adresován.

Zvažme odeslání bitového vzoru konkrétního řádku do třetího modulu v řetězci. Mohli bychom to udělat takto:

gpio_put (CS, 0);

uint8_t row = //.. select row
uint8_t pattern = //.. determine bit pattern for row

uint8_t buf[] = {0, 0, 0, 0, row, pattern};
spi_write_blocking (self->spi, buf, 6);

gpio_put (CS, 1);

To, co jsme zde udělali, je napsat dva 16bitové bloky „nedělat nic“ před instrukcí k zápisu řádku. Celý tento 48bitový vzor je zapsán v jedné dávce s nízkou úrovní CS linky – tak zařízení vědí, že data jsou pro řetězec, nejen pro první modul.

Tento přístup samozřejmě trochu plýtvá zdroji SPI – posíláme spoustu dat, která neslouží žádnému užitečnému účelu. Protože stejně musíme zaznamenávat všechny jednotlivé stavy LED, můžeme také zapsat všechny hodnoty LED v určité oblasti displeje v jedné operaci. Zda je dotyčný „region“ řádek, část řádku nebo celé zobrazení, závisí na aplikaci.

Závěrečné poznámky

Takže to je základní myšlenka: inicializujte SPI a přidělte mu piny; posílat data ve skupinách bitů, které jsou dostatečně dlouhé, aby se dostaly k modulu, který by je měl zpracovat, a v případě potřeby doplňte přenos nulami.

Ďábel je jako vždy v detailech. Ačkoli je rozhraní SPI téměř triviální, komplikací je softwarově sledovat, jaké LED diody změnily stav, a generovat nejúčinnější sekvenci bitů, které budou odrážet změnu stavu v hardwaru. Téměř jistě bude vyžadováno určité žonglování s bity, aby se přizpůsobilo způsobu, jakým konkrétní výrobce připojil čip MAX7219 k LED diodám. Nebudu zde zabíhat do těchto detailů - není to problém s SPI nebo rozhraním, ale obecný problém vývoje C.

Pro konkrétní implementaci najdete kompletní, zdokumentovaný příklad v mém úložišti GitHub.

pico7219.h — rozhraní knihovny
/*=========================================================================

  Pico7219

  pico7219.h

  A low-level library for controlling LED matrixes based on chained
  8x8 modules including MAX7219 ICs.

  See the accompanying test.c to see how this library should be used.

  The functions in this library that turn on and turn off LEDs all
  have a "flush" argument. If flush is TRUE, then changes are written
  to the hardware immediately. As it may be necessary to write an
  entire row to change one LED, it's much more efficient to group
  changes and called flush() at the end.

  Note the row and column numbers in all functions are zero-based. The
  "bottom-left" corner is (0,0) although, of course, the display modules
  can be rotated so this might be the top right in some installations.

  At present, this library doesn't have a way to set the display module to
  stand-by mode, except by calling pico7219_destroy() to tidy it up.
  Since entering stand-by requires reinitializing the hardware, this
  is probably (but not definitely) the right approach.

  The library supports the notion of a "virtual" chain of display
  modules. When LEDs are turned on and off, they are written to this
  virtual chain, which can be much longer than the real display --
  as long as memory allows. Only the part of the virtual chain that
  fits on the physical display chain will be shown when the LEDs are
  first set, but content that won't fit can be scrolled into view by
  calling pico7219_scroll repeatedly.

  Copyright (c)2021 Kevin Boone, GPL v3.0

  =========================================================================*/
#pragma once

#include <stdint.h>
#if PICO_ON_DEVICE
#include "hardware/spi.h"
#endif

#ifndef BOOL
typedef uint8_t BOOL;
#endif

#ifndef TRUE
#define TRUE 1
#endif

#ifndef FALSE
#define FALSE 0
#endif

// Maximum devices in chain. This can usefully be increased, at the expense
//  of using a little more memory.
#define PICO7219_MAX_CHAIN 8

// Number of LEDs in each row of column. This is a feature of the
//   MAX7219, and can't usefull be changed
#define PICO7219_ROWS 8
#define PICO7219_COLS 8

// An enum to denote the SPI channel to use. This is to avoid exposing
//   client classes to the low-level API provided by the Pico SDK
enum PicoSpiNum
  {
  PICO_SPI_0 = 0,
  PICO_SPI_1
  };

struct Pico7219;

#ifdef __cplusplus
extern "C" {
#endif

/** pico7219_create() -- create the Pico7219 structure, storing references
    to the hardware parameters. Then initialize the hardware, and set the
    MAX2719 to "running" mode.  The Pico SDK provides no way to tell whether
    initialization succeeded or not, so the only way this function can fail
    is to run out of memory. In tha case, it returns NULL. If it doesn't
    return NULL, use pico7219_destroy() to tidy up. */
extern struct Pico7219 *pico7219_create (enum PicoSpiNum spi_num,
                           int32_t baud, uint8_t mosi,
			   uint8_t sck, uint8_t cs, uint8_t chain_len,
			   BOOL reverse_bits);

/** Clean up the library. If "deinit" is TRUE, the corresponding SPI
    channel in the Pico is deinitialized. In either case, set the
    display hardware to the low-power standby mode. */
extern void             pico7219_destroy (struct Pico7219 *self, BOOL deinit);

/** Write a whole row in one operation. The bits[] argument is an array
    of bytes, where each byte represents a set of on/off states in
    specific columns. bits[0] represents the module at the end of
    the chain nearest the input, however long the chain is. The LSB
    of bits[0] is the either the LSB of the 7219 outputs or the
    MSB, dependending on whether the object was created with
    bit-reverse mode or not. This is a low-level function, intended
    for clients for which the build-in set_output() and clear_output()
    methods are not fast enough. It does not change the internal state
    of the library at all. */
extern void             pico7219_set_row_bits (const struct Pico7219 *self,
                          uint8_t row,
			  const uint8_t bits[PICO7219_MAX_CHAIN]);

/** Turn on the LED at a particular row and column. If flush is TRUE,
    changes are written immediately to the hardware. Otherwise they are
    buffered for a later call to flush(). */
extern void             pico7219_switch_on (struct Pico7219 *self,
                          uint8_t row, uint8_t col, BOOL flush);

/** Turn off the LED at a particular row and column. If flush is TRUE,
    changes are written immediately to the hardware. Otherwise they are
    buffered for a later call to flush(). */
extern void             pico7219_switch_off (struct Pico7219 *self,
                          uint8_t row, uint8_t col, BOOL flush);

/** Turn off all the LEDs in a row. If flush is TRUE,
    changes are written immediately to the hardware. Otherwise they are
    buffered for a later call to flush(). */
extern void pico7219_switch_off_row (struct Pico7219 *self, uint8_t row,
                          BOOL flush);

/** Turn off all the LEDs in the display. If flush is TRUE,
    changes are written immediately to the hardware. Otherwise they are
    buffered for a later call to flush(). */
extern void pico7219_switch_off_all (struct Pico7219 *self, BOOL flush);

/** Turn on all the LEDs in a row. If flush is TRUE,
    changes are written immediately to the hardware. Otherwise they are
    buffered for a later call to flush(). */
extern void pico7219_switch_on_row (struct Pico7219 *self, uint8_t row,
                          BOOL flush);

/** Turn on all the LEDs in the display. If the module is powered by
    USB, the supply might not be adequate to switch on all LEDs at
    high intensity. */
extern void pico7219_switch_on_all (struct Pico7219 *self, BOOL flush);

/** Write buffered LED state changes to the hardware. */
extern void pico7219_flush (struct Pico7219 *self);

/** Set the LED brightness in the range 0-15. Default is 1. Note that
 * there is no "off" setting -- even 0 has some illumination. */
extern void pico7219_set_intensity (struct Pico7219 *self, uint8_t intensity);

/** Scroll the virtual module chain one pixel (LED) to the left. The part
      of the virtual chain that fits on the display will be shown. If
      wrap is TRUE, pixels that are scrolled off the display are redrawn
      on the end (of the virtual chain) and may eventually be scrolled back
      into view.
    This function is design to be used alone. Writing new data to the module
      while scrolling will have odd results. Calling flush() will restore
      the non-scrolled state.
    Note that the display will scroll even if the text will fit on the module.
      Don't ask for it if you don't want it. */
extern void pico7219_scroll (struct Pico7219 *self, BOOL wrap);

/** Set the number of "virtual modules" in the display chain. This can be
      any length (subject to memory), but it makes little sense to set
      this smaller than the actual display. The purpose of setting the
      virtual length is to be able to write content that will not fit
      onto the physical display, and then call scroll() to bring it
      into view. By default, the virtual chain length is the same as
      the predefined maximum physical chain length, that is, 8 modules.
      If you don't plan to use the scrolling function, you can save a
      little memory by setting the virtual chain length to the actual
      chain length. But we're talking bytes here. */
extern void pico7219_set_virtual_chain_length (struct Pico7219 *self,
   int chain_len);

#ifdef __cplusplus
}
#endif
pico7219.c — implementace knihovny
/*=========================================================================

  Pico7219

  pico7219.c

  A low-level driver for chained display modules based on the MAX7219.
  See the corresponding header file for a description of how the
  functions are used. If this library is built in "host" mode, the
  actual GPIO operations are replaced by printouts of the pins of the
  operations that would be carried out.

  Copyright (c)2021 Kevin Boone, GPL v3.0

  =========================================================================*/
#include <stdlib.h>
#include <string.h>

#if PICO_ON_DEVICE
#include "hardware/spi.h"
#include "hardware/gpio.h"
#else
#include <stdio.h> // For printf(). Don't need this in the Pico build
#endif

#include "pico7219/pico7219.h"

#define PICO7219_INTENSITY_REG 0x0A
#define PICO7219_SHUTDOWN_REG 0x0C

// An opaque data structure that holds the information relevant to the
//   library. Users of the library do not see this, or need to.

struct Pico7219
  {
  uint8_t spi_num; // 0 or 1
  uint8_t cs; // Chip select GPIO pin
  uint8_t chain_len; // Number of chained devices
  BOOL reverse_bits; // TRUE is we must reverse output->layout order
#if PICO_ON_DEVICE
  spi_inst_t* spi; // The Pico-specific SPI device
#endif
  // data is an array of bits that represents the states of the
  //   individual bits. They are packed into 8-bit chunks, which is
  //   how the need to be written to the hardware, as well as saving
  //   space.
  uint8_t data[PICO7219_ROWS][PICO7219_MAX_CHAIN];
  uint8_t row_dirty [PICO7219_ROWS]; // TRUE for each row to be flushed
  uint8_t *vdata;
  // Length of the "virtual chain" of modules
  int vchain_len;
  };

/** Change the state of the chip-select line, allowing a very short
    time for it to settle. */
static void pico7219_cs (const struct Pico7219 *self, uint8_t select)
  {
#if PICO_ON_DEVICE
  asm volatile("nop \n nop \n nop");
  gpio_put (self->cs, select);
  asm volatile("nop \n nop \n nop");
#else
  printf ("Set GPIO %d = %d\n", self->cs, select);
#endif
  }

/** write_word_to_chain() outputs the same 16-bit word as many times
    as there are modules in the chain. This is mostly used for
    initialization -- each module will be initialized with the same
    values, so we must repeat the data output enough times that each
    module gets a copy. */
static void pico7219_write_word_to_chain (const struct Pico7219 *self,
        uint8_t hi, uint8_t lo)
  {
  pico7219_cs (self, 0);
#if PICO_ON_DEVICE
  uint8_t buf[] = {hi, lo};
  for (int i = 0; i < self->chain_len; i++)
    spi_write_blocking (self->spi, buf, 2);
#else
  (void)hi; (void)lo;
#endif
  pico7219_cs (self, 1);
  }

/* init() sends the same set of initialization values to all modules
   in the chain. We write zero to all the row buffers, and set
   reasonable values for the control registers. */
static void pico7219_init (const struct Pico7219 *self)
  {
  // Rows
  pico7219_write_word_to_chain (self, 0x00, 0x00);
  pico7219_write_word_to_chain (self, 0x01, 0x00);
  pico7219_write_word_to_chain (self, 0x02, 0x00);
  pico7219_write_word_to_chain (self, 0x03, 0x00);
  pico7219_write_word_to_chain (self, 0x04, 0x00);
  pico7219_write_word_to_chain (self, 0x05, 0x00);
  pico7219_write_word_to_chain (self, 0x06, 0x00);
  pico7219_write_word_to_chain (self, 0x07, 0x00);
  pico7219_write_word_to_chain (self, 0x08, 0x00);
  // Control registers
  pico7219_write_word_to_chain (self, 0x09, 0x00); // Decode mode
  pico7219_write_word_to_chain (self, PICO7219_INTENSITY_REG, 0x01);
  pico7219_write_word_to_chain (self, 0x0b, 0x07); // scan limit = full
  pico7219_write_word_to_chain (self, PICO7219_SHUTDOWN_REG, 0x01); // Run
  pico7219_write_word_to_chain (self, 0x0f, 0x00); // Display test = off
  }

/** pico7219_create_vdata(). Create enough space for a "virtual"
    chain of 8x8 displays, whose size is */
void pico7219_set_virtual_chain_length (struct Pico7219 *self, int chain_len)
  {
  if (self->vdata) free (self->vdata);
  self->vdata = malloc (PICO7219_ROWS * chain_len);
  memset (self->vdata, 0, PICO7219_ROWS * chain_len);
  self->vchain_len = chain_len;
  }

/** pico7219_create() */
struct Pico7219 *pico7219_create (enum PicoSpiNum spi_num, int32_t baud,
         uint8_t mosi, uint8_t sck, uint8_t cs, uint8_t chain_len,
	 BOOL reverse_bits)
  {
  struct Pico7219 *self = malloc (sizeof (struct Pico7219));
  if (self)
    {
    self->chain_len = chain_len;
    self->cs = cs;
    self->spi_num = spi_num;
    self->reverse_bits = reverse_bits;
    self->vdata = NULL;
    self->vchain_len = 0;
    // Start with the virtual chain length the same as the maximum
    //  physical chain length
    pico7219_set_virtual_chain_length (self, PICO7219_MAX_CHAIN);
    // Set data buffer to all "off", as that's how the LEDs power up
    memset (self->data, 0, sizeof (self->data));
    // Set all data clean
    memset (self->row_dirty, 0, sizeof (self->row_dirty));
#if PICO_ON_DEVICE
    switch (spi_num)
      {
      case PICO_SPI_0:
        self->spi = spi0;
	break;
      case PICO_SPI_1:
        self->spi = spi1;
	break;
      }

    // Initialize the SPI and GPIO

    spi_init (self->spi, baud);

    gpio_set_function(mosi, GPIO_FUNC_SPI);
    gpio_set_function(sck, GPIO_FUNC_SPI);

    gpio_init (self->cs);
    gpio_set_dir (self->cs, GPIO_OUT);
    gpio_put (self->cs, 1);
#else
printf ("Init SPI %d at %d baud, mosi=%d, sck=%d, cs=%d\n",
     self->spi_num, baud, mosi, sck, self->cs);
#endif

    // Initialize the hardware
    pico7219_init (self);
    }
  return self;
  }

/** pico7219_destroy() */
void pico7219_destroy (struct Pico7219 *self, BOOL deinit)
  {
  if (self)
    {
    if (self->vdata) free (self->vdata);
    pico7219_write_word_to_chain (self, PICO7219_SHUTDOWN_REG, 0x00); // off
    if (deinit)
      {
#if PICO_ON_DEVICE
      spi_deinit (self->spi);
#endif
      }
    free (self);
    }
  }

/** reverse_bits() reverses the order of bits in a specific byte. */
static uint8_t pico7219_reverse_bits (uint8_t b)
  {
  b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
  b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
  b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
  return b;
  }

/** pico7219_set_row_bits(). */
void pico7219_set_row_bits (const struct Pico7219 *self, uint8_t row,
        const uint8_t bits[PICO7219_MAX_CHAIN])
  {
  pico7219_cs (self, 0);
  int chain_len = self->chain_len;
  for (int i = 0; i < chain_len; i++)
    {
    uint8_t v = bits[chain_len - i - 1];
    if (self->reverse_bits)
      v = pico7219_reverse_bits (v);
    uint8_t buf[] = {row + 1, v};
#if PICO_ON_DEVICE
    spi_write_blocking (self->spi, buf, 2);
#else
    printf ("SPI write %02x %02x\n", buf[0], buf[1]);
#endif
    }
  pico7219_cs (self, 1);
  }

/** pico7219_switch_off_row() */
void pico7219_switch_off_row (struct Pico7219 *self, uint8_t row, BOOL flush)
  {
  self->row_dirty[row] = TRUE;
  //fprintf (stderr, "row = %d len=%d\n", row, self->vchain_len);
  memset (self->vdata + row * self->vchain_len, 0x0, self->vchain_len);
  if (flush) pico7219_flush (self);
  }

/** pico7219_switch_off_all() */
void pico7219_switch_off_all (struct Pico7219 *self, BOOL flush)
  {
  for (int i = 0; i < PICO7219_ROWS; i++)
    pico7219_switch_off_row (self, i, FALSE);
  if (flush) pico7219_flush (self);
  }

/** pico7219_switch_on_row() */
void pico7219_switch_on_row (struct Pico7219 *self, uint8_t row, BOOL flush)
  {
  self->row_dirty[row] = TRUE;
  memset (self->vdata + row * self->vchain_len, 0xFF, self->vchain_len);
  if (flush) pico7219_flush (self);
  }

/** pico7219_switch_on_all() */
void pico7219_switch_on_all (struct Pico7219 *self, BOOL flush)
  {
  for (int i = 0; i < PICO7219_ROWS; i++)
    pico7219_switch_on_row (self, i, FALSE);
  if (flush) pico7219_flush (self);
  }

/** pico7219_switch_on() */
void pico7219_switch_on (struct Pico7219 *self, uint8_t row,
       uint8_t col, BOOL flush)
  {
  if (row < PICO7219_ROWS && col < PICO7219_COLS * self->vchain_len)
    {
    int block = col / 8;
    int pos = col - 8 * block;
    uint8_t v = 1 << pos;
    self->vdata[row * self->vchain_len + block] |= v;
    self->row_dirty[row] = TRUE;
    if (flush) pico7219_flush (self);
    }
  }

/** pico7219_switch_off() */
void pico7219_switch_off (struct Pico7219 *self, uint8_t row,
       uint8_t col, BOOL flush)
  {
  if (row < PICO7219_ROWS && col < PICO7219_COLS * self->vchain_len)
    {
    int block = col / 8;
    int pos = col - 8 * block;
    uint8_t v = 1 << pos;
    self->vdata[row * self->vchain_len + block] &= ~v;
    self->row_dirty[row] = TRUE;
    if (flush) pico7219_flush (self);
    }
  }

/** Copy from the virtual chain to self->data, preparatory to
    writing to the device. This function will only write the start
    of the virtual chain, if it is longer than the physical chain. */
static void pico7219_vrow_to_row (struct Pico7219 *self, int row)
  {
  int target_mods = self->chain_len;
  if (target_mods > self->vchain_len) target_mods = self->vchain_len;
  int row_start = row * self->vchain_len;
  for (int i = 0; i < target_mods; i++)
    {
    self->data[row][i] = self->vdata[row_start + i];
    }
  }

/** Scroll one pixel left. */
void pico7219_scroll (struct Pico7219 *self, BOOL wrap)
  {
  int target_mods = self->chain_len;
  if (target_mods > self->vchain_len) target_mods = self->vchain_len;

  // Shift bits in vdata
  // This logic is twisted because the bits are in MSB-LSB order in the
  //   opposite order from the modules. So when we shift a bit rightwards
  //   off the end of one module, it appears as the MSB in the next, not
  //   the LSB.
  for (int row = 0; row < PICO7219_ROWS; row++)
    {
    int row_start = row * self->vchain_len;
    uint8_t carry = 0;
    int l = self->vchain_len - 1;
    for (int i = l; i >= 0; i--)
      {
      int row_offset = row_start + i;
      BOOL carry_next = FALSE;

      if (self->vdata[row_offset] & 0x01) carry_next = TRUE;
      self->vdata[row_offset] >>= 1;

      // If we're at position 0, and the shift would carry, we have to
      //   carry to position "-1" which, of course, does not exist. So,
      //   instead, we carry to position l, that is, to the far end of
      //   the chain.
      if (wrap)
        {
        if (i == 0 && carry_next)
          self->vdata[row_start + l] |= 0x80;
        }

      self->vdata[row_offset] |= carry;
      carry = 0;

      if (carry_next)
	carry = 0x80;
      }

    pico7219_set_row_bits (self, row, self->vdata + row * self->vchain_len);
    }
  }

/** pico7219_flush() */
void pico7219_flush (struct Pico7219 *self)
  {
  for (int i = 0; i < PICO7219_ROWS; i++)
    {
    pico7219_vrow_to_row (self, i);
    if (self->row_dirty[i])
      pico7219_set_row_bits (self, i, self->data[i]);
    self->row_dirty[i] = FALSE;
    }
  }

/** pico7219_set_intensity() */
void pico7219_set_intensity (struct Pico7219 *self, uint8_t intensity)
  {
  pico7219_write_word_to_chain (self, PICO7219_INTENSITY_REG, intensity);
  }

Úpravy a dodělávky

Font, který se používá není moc čitelný, upravil jsem ho podle fontu Spleen 8x5, který jsem použil u OLED displeje, bylo nutné ho zcela předefinovat protože tady je definice po sloupcích a u OLED dipleje po řádcích.

Upravena je stránka na definici fontů https://sspvc.lixis.cz/pmp/OLED1306/Custom_Character_Generator_for_OLED_displays.html , aby bylo možno font předefinovat.

Výsledky jsou zde pico7219.tar.gz

Bude k tomu ještě nějaký komentář.

Takto to funguje

Zdroje