Драйвер OLED-дисплея, директивы предкомпилятору и т.д
Есть у меня такой дисплей:
https://www.chipdip.ru/product/0.96inch-oled-a
… драйвер к которому можно взять тут (Adafruit_SSD1306-master):
https://www.chipdip.ru/product0/9000318627
Для работы ещё потребуется Adafruit-GFX-Library-master, потому что там наследуемый класс (сейчас не важно, что это).
На этот раз я не буду портить текст сокращениями, потому что такие программы как раз пишутся под возможно большее число вариантов использования. В тех файлах именно это во всей красе.
Но пока про дисплей. Смотрю файл Adafruit_SSD1306.h и вижу там три способа создания объекта для работы с дисплеем (три конструктора объекта):
class Adafruit_SSD1306 : public Adafruit_GFX {
public:
Adafruit_SSD1306(int8_t SID, int8_t SCLK, int8_t DC, int8_t RST, int8_t CS);
Adafruit_SSD1306(int8_t DC, int8_t RST, int8_t CS);
Adafruit_SSD1306(int8_t RST = -1);
Смотрю текст с часами и вижу там такое:
Adafruit_SSD1306 display(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
.. т.е выбран первый вариант конструктора, в котором можно задать полный список PIN-ов для SPI подключения.
Пример с часами тоже в каком-то смысле - объект. У него много чего, чем можно управлять. Там и дисплей, и часы, и пищалка. Потому у него тоже есть файл с расширением *.H. Смотрю этот clock.h и вижу у нем самую стандартную директиву тому самому предкомпилятору define (чаще всего используемую):
#define OLED_MOSI 9
#define OLED_CLK 10
#define OLED_DC 11
#define OLED_CS 12
#define OLED_RESET 13
… которая даёт мне возможность писать в программе не ничего незначащие цифры – номера PIN-ов а их назначения большими буквами. Так проще и писать и читать текст программы. Вот только машине это все не надо. Даже помешало бы захламление памяти, если бы я решила завести переменные с такими названиями. Потому возник компромиссный механизм – предкомпиляция. До компиляции во всём тексте удобные для человека названия (определенные в define) будут заменены в данном случае на просто цифры – номера PIN-ов.
Вернусь к дисплею и попытаюсь понять, кто тут кто:
… потому что комбинация не совсем совпадает с тем, что должно быть для SPI:
https://akostina76.ucoz.ru/blog/2022-01-25-7444
.. а, с другой стороны, PIN D/C (данные или команда по описанию) как раз совпадает с протоколом передачи в LCD дисплея:
https://akostina76.ucoz.ru/blog/2022-01-29-7450
Ещё я вижу, что в сокращённый вариант (второй конструктор) передаются только DC, RST и CS. Видимо в этом случае MOSI и CLK должны быть подключены к PIN-ам по умолчанию (MOSI=11, CLK=SCK=clock = часы = 13).
Тексты всех трёх конструкторов, благо они короткие
Adafruit_SSD1306::Adafruit_SSD1306(int8_t SID, int8_t SCLK, int8_t DC, int8_t RST, int8_t CS) : Adafruit_GFX(SSD1306_LCDWIDTH, SSD1306_LCDHEIGHT) {
cs = CS;
rst = RST;
dc = DC;
sclk = SCLK;
sid = SID;
hwSPI = false;
}
// constructor for hardware SPI - we indicate DataCommand, ChipSelect, Reset
Adafruit_SSD1306::Adafruit_SSD1306(int8_t DC, int8_t RST, int8_t CS) : Adafruit_GFX(SSD1306_LCDWIDTH, SSD1306_LCDHEIGHT) {
dc = DC;
rst = RST;
cs = CS;
hwSPI = true;
}
// initializer for I2C - we only indicate the reset pin!
Adafruit_SSD1306::Adafruit_SSD1306(int8_t reset) :
Adafruit_GFX(SSD1306_LCDWIDTH, SSD1306_LCDHEIGHT) {
sclk = dc = cs = sid = -1;
rst = reset;
}
… потому что в них не делается ничего кроме заполнения внутренних переменных объекта номерами PIN-ов. Точнее, почти ничего. Последний вариант записывает в номера PIN-ов для SPI значения -1. Дальше такие значения будут означать, что передача ведётся по I2C протоколу. А второй конструктор (в котором мало PIN-ов - параметров) присваивает значение «истина» внутренней переменной hwSPI. А это дальше будет означать, что для передачи будет использован стандартный SPI (со стандартными MOSI и CLK).
Стандартное использование дисплея такое:
Adafruit_SSD1306 display(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
void setup() {
display.begin(SSD1306_SWITCHCAPVCC);
display.drawPixel(10, 10, WHITE);
display.display();
}
.. т.е после создания объекта полагается быть функции begin(). Вот её начало :
void Adafruit_SSD1306::begin(uint8_t vccstate, uint8_t i2caddr, bool reset) {
_vccstate = vccstate;
_i2caddr = i2caddr; // Адрес I2C устройства
// set pin directions
if (sid != -1
{ // Не I2C
pinMode(dc, OUTPUT);
pinMode(cs, OUTPUT);
#ifdef HAVE_PORTREG
csport = portOutputRegister(digitalPinToPort(cs));
cspinmask = digitalPinToBitMask(cs);
dcport = portOutputRegister(digitalPinToPort(dc));
dcpinmask = digitalPinToBitMask(dc);
#endif
/// Выше было для всех типов SPI передачи
if (!hwSPI){ // Переданы все SPI пины
// set pins for software-SPI
pinMode(sid, OUTPUT);
pinMode(sclk, OUTPUT);
#ifdef HAVE_PORTREG
clkport = portOutputRegister(digitalPinToPort(sclk));
clkpinmask = digitalPinToBitMask(sclk);
mosiport = portOutputRegister(digitalPinToPort(sid));
mosipinmask = digitalPinToBitMask(sid);
#endif
} // Конец «Переданы все SPI пины»
if (hwSPI){ // Передана часть SPI пинов
SPI.begin();
#ifdef SPI_HAS_TRANSACTION
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
#else
SPI.setClockDivider (4);
#endif
} // Конец “// Передана часть SPI пинов»
} // Конец “Не I2C»
else
{
// I2C Init
Wire.begin();
#ifdef __SAM3X8E__
// Force 400 KHz I2C, rawr! (Uses pins 20, 21 for SDA, SCL)
TWI1->TWI_CWGR = 0;
TWI1->TWI_CWGR = ((VARIANT_MCK / (2 * 400000)) - 4) * 0x101;
#endif
} // конец “I2C Init»
if ((reset) && (rst >= 0)) {
// Setup reset pin direction (used by both SPI and I2C)
pinMode(rst, OUTPUT);
digitalWrite(rst, HIGH);
// VDD (3.3V) goes high at start, lets just chill for a ms
delay(1);
// bring reset low
digitalWrite(rst, LOW);
// wait 10ms
delay(10);
// bring out of reset
digitalWrite(rst, HIGH);
// turn on VCC (9V?)
}
// Команды дисплею:
ssd1306_command(SSD1306_DISPLAYOFF); // 0xAE
ssd1306_command(SSD1306_SETDISPLAYCLOCKDIV); // 0xD5
ssd1306_command(0x80); // the suggested ratio 0x80
Первое, что я тут вижу - все три варианты (два SPI и I2C) засунуты в одну функцию. Разделены они стандартными if… else. Так обычно и делают.
Второе, что вижу - то, что меня интересовало. Похоже, что любой пин можно назначить портом для SPI. Для этого надо написать ему что-то типа
clkport = portOutputRegister(digitalPinToPort(sclk));
… И, наконец я вижу как работают стандартные инструменты передачи (на стандартных пинах). Здесь они инициализируются функциями SPI.begin(); Wire.begin();, дальше передача байта потребует тоже по одной функции. Быстро, просто и коротка. Но на стандартных пинах.
А ещё я тут вижу #ifdef #endif. Это тоже директивы предкомпилятору. Если переменная условия не определена, текст перед компиляцией будет просто изъят. Появилось, скорее всего, тогда же когда и define, но в обычной жизни используется довольно редко. Язык Си был уже на больших машинах. И, конечно, на персональных компьютерах он тоже был. Только чуть разный. Отличие, правда, не в языке, а в функциях и библиотеках. Но какой-нибудь чисто расчётный текст вообще мог быть запущен где угодно. А если не расчётный, а что-то более сложное хочется запускать в принципиально разных средах? Так и появились тексты из таких кусков для разных сред использования. Достаточно поменять что-то в начале текста и можно использовать в другой среде.
Пора вспомнить, что я читаю текст драйвера даже не дисплея, а его микросхемы – контроллера SSD1306. К этому SSD1306 могут быть подключены разные дисплеи. 128*64 точек или 128*32 точки, например. Программа требуется одна и та же. Только в одном тексте будет где-то 64. а в другом 32. С этим отличием успешно справится даже директива define. А ещё сам управляющий контроллер у меня может быть каким угодно. Они тоже чуть по-разному работают. Для учёта разной работы контроллеров тут обычно и используются эти #ifdef #endif., которые на больших компьютерах нужны для использования и компиляции в разных операционных системах.
Текст функции – command, передающей байт команды дисплею:
void Adafruit_SSD1306::ssd1306_command(uint8_t c) {
if (sid != -1)
{
// SPI
#ifdef HAVE_PORTREG
*csport |= cspinmask;
*dcport &= ~dcpinmask;
*csport &= ~cspinmask;
#else
digitalWrite(cs, HIGH);
digitalWrite(dc, LOW);
digitalWrite(cs, LOW);
#endif
fastSPIwrite(c);
#ifdef HAVE_PORTREG
*csport |= cspinmask;
#else
digitalWrite(cs, HIGH);
#endif
}
else
{
// I2C
uint8_t control = 0x00; // Co = 0, D/C = 0
Wire.beginTransmission(_i2caddr);
Wire.write(control);
Wire.write(c);
Wire.endTransmission();
}
}
В случае LCD дисплея была запись байта в пины и тик часов. Здесь вызовы функций fastSPIwrite(c) для SPI и Wire.write(c) для I2C. Отмечу только, что пин DC (данные или команда) устанавливается для команд в 0.
Запись SPI:
inline void Adafruit_SSD1306::fastSPIwrite(uint8_t d) {
if(hwSPI) {
(void)SPI.transfer(d);
} else {
for(uint8_t bit = 0x80; bit; bit >>= 1) {
#ifdef HAVE_PORTREG
*clkport &= ~clkpinmask;
if(d & bit) *mosiport |= mosipinmask;
else *mosiport &= ~mosipinmask;
*clkport |= clkpinmask;
#else
digitalWrite(sclk, LOW);
if(d & bit) digitalWrite(sid, HIGH);
else digitalWrite(sid, LOW);
digitalWrite(sclk, HIGH);
#endif
}
}
}
В случае стандартного SPI одна функция (SPI.transfer()). А в самопальном варианте SPI передачи обращает на себя внимание отсутствие задержек в микросекундах (выключаются часы, загружается бит из MOSI, включаются часы).
Про самопальную передачу I2C по любым пинам, похоже, тут:
https://www.instructables.com/id/Interfacing-LCD-With-Arduino-Using-Only-3-Pins/
Не очень хорошая штука у этого дисплея с передачей картинки. Если LCD дисплею я вначале командой передаю место в памяти, а потом букву, которую туда надо записать (после чего буква отображается на экране), то здесь я вначале «рисую» всё в специальном выделенном блоке памяти, а потом передаю микросхеме весь экран командой display(). Для маленького экрана это 128*64 бита. Для большого при такой организации работы:
https://akostina76.ucoz.ru/blog/2022-01-28-7448
… будет 1280 * 1024 бит. Понятно, что все сейчас быстро работает. Но, наверное, стоит учитывать и реальные потребности, особенно если будет использован такой метод передачи.
|