Шаг 4.2. Продолжаем мучать LCD

Итак, "низкий" уровень протокола общения с дисплеем мы реализовали - у нас есть функция, которая отсылает байт команды или данных. Посмотрим, как можно улучшить наш код. Для начала, очевидно, что нужно создать ещё одну функцию для обработки не символов, а целых строк. То же верно и для команд. Ещё один открытый вопрос - нужно ли разделять функцию для строк и для массивов команд? С одной стороны, мы получим в два раза больше кода, с другой - он будет намного понятней. Так как памяти у нас пока хватает, сделаем код понятным :) Начнем с программы на Си.

Так, и команды, и символы у нас можно определить типом "byte", а массивы или строки передать через указатели на нулевой элемент - "byte*".

Мы знаем, что строки в Си (в MikroPascal-е тоже так) всегда заканчиваются нулём - можно на этом сыграть!

//функция отправки дисплею строки, заканчивающейся нулём ('\0')
void LCD_SendStr(byte* Str)
{
  byte i;
  for (i = 0; Str[i] != 0; i++)                                                 //просто идём по строке и отправляем байты до тех пор, пока не встретим '\0'
    LCD_SendByte(Str[i], 1);
}

Выглядит совсем просто! Теперь подумаем о массивах команд: очень заманчиво точно так же дописывать на конце массива ноль и не запоминать, сколько у нас команд, - тем более, что в списке команд в документации нет команды, содержащей одни нули. Так что в принципе, почему бы и нет)

//функция отправки дисплею массива команд, заканчивающейся нулём
void LCD_SendCommands(byte* CommandArr)
{
  byte i;
  for (i = 0; CommandArr[i] != 0; i++)
    LCD_SendByte(CommandArr[i], 0);
}

Единственное, что может смущать - что обычно команды, или массивы команд, записываются как константы. И вот тут-то нас поджидает неприятный момент.

Дело в том, что наш микроконтроллер имеет гарвардскую архитектуру - слоны отдельно, мухи отдельно, то есть ОЗУ (переменные) отдельно, ПЗУ (константы) отдельно. Весело и задорно про память можно прочитать здесь, у наших товарищей. И поэтому нельзя просто так взять, и написать так:

//функция отправки дисплею массива команд, заканчивающейся нулём
void LCD_SendCommands(byte* CommandArr)
...
void main()
{
  //объявление переменных и констант
  const byte aInit[] = {0b00110011,                                                   //команды для инициализации модуля
                   0b00110010,
                   0b00101000,
                   0b00001111,
                   0b00000001,
                   0b00000110, 0};
  ...
  LCD_SendCommands(aInit);                                                    //инициализация дисплея

На такой код компилятор выдаст ошибку: Illegal pointer conversion, то есть недопустимое преобразование указателя.

Но решение есть - ОЗУ и ПЗУ могут воздействовать через указатели, а именно указатели на константы: const byte*.

В этом случае наш код будет работать:

//программа работает с LCD-дисплеем МТ-10S1; инициализирует его и выводит на экран приветствие "Hello"
//переименование типов
typedef unsigned char byte;
//определяем назначение портов ввода-вывода
#define portLCD PORTB
#define ddrLCD DDRB 

//константы для дисплея
const byte LCD_A0_OUT_NUMBER = 5;                                               //номер вывода, к которому подключён вывод A0 дисплея
const byte LCD_E_OUT_NUMBER = 4;                                                //номер вывода, к которому подключён E дисплея
const byte LCD_DATA_MASK = 0x0F;
const byte LCD_DATA_SHIFT = 0;

//задержки
const byte DL_LCD_SWITCH_ON_MS = 20;                                            //задержка после подачи питания на дисплей в мс
const byte DL_LCD_BETWEEN_E_CHANGE_MCS = 1;                                     //задержка перед изменением вывода Е дисплея (отправка полубайта команд) в мкс
const byte DL_LCD_DATA_SEND_MS = 2;                                             //задержка после отправки одного байта данных или команды в мс


void LCD_SendByte(byte DataByte, byte IsSymbol)
{
  portLCD.LCD_A0_OUT_NUMBER = IsSymbol;                                         //определяем, что отправляем - команду или данные
  portLCD.LCD_E_OUT_NUMBER = 1;                                                 //показываем, что сейчас будет устанавливать данные
  portLCD = portLCD & (˜LCD_DATA_MASK) | (DataByte >> 4);                       //устанавливаем на выводах DB7-DB4 значение старшего полубайта
  Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS);                                        //на всякий случай делаем задержку перед отправкой
  portLCD.LCD_E_OUT_NUMBER = 0;                                                 //отправляем значение старшего полубайта!
  Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS);                                        //на всякий случай делаем задержку перед изменением вывода Е; далее аналогично для младшего полубайта
  portLCD.LCD_E_OUT_NUMBER = 1;
  portLCD = portLCD & (˜LCD_DATA_MASK) | (DataByte & 0x0F);
  Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS);
  portLCD.LCD_E_OUT_NUMBER = 0;
  Delay_ms(DL_LCD_DATA_SEND_MS);                                                //делаем задержку для того, чтобы дисплей мог обработать команду или данные
}

//функция отправки дисплею строки, заканчивающейся нулём ('\0')
void LCD_SendStr(byte* Str)
{
  byte i;
  for (i = 0; Str[i] != 0; i++)                                                 //просто идём по строке и отправляем байты до тех пор, пока не встретим '\0'
    LCD_SendByte(Str[i], 1);
}
//функция отправки дисплею массива команд, заканчивающейся нулём
void LCD_SendCommands(const byte* CommandArr)
{
  byte i;
  for (i = 0; CommandArr[i] != 0; i++)
    LCD_SendByte(CommandArr[i], 0);
}

void main()
{
  //объявление переменных и констант
  const byte aInit[] = {0b00110011,                                                   //команды для инициализации модуля
                        0b00110010,
                        0b00101000,
                        0b00001111,
                        0b00000001,
                        0b00000110, 
                        0};
  //строка на вывод
  char sText[] = "Hello";

  //инициализация портов ввода-вывода
  DDRA = 0;
  PORTA = 0;
  DDRB = 0;
  PORTB = 0;
  DDRD = 0;
  PORTD = 0;
  //инициализация выводов для дисплея - на выход и на 0
  ddrLCD |= (1 << LCD_A0_OUT_NUMBER) | (1 << LCD_E_OUT_NUMBER) | LCD_DATA_MASK;
  
  Delay_ms(DL_LCD_SWITCH_ON_MS);                                                //задержка для дисплея после включения

  LCD_SendCommands(aInit);                                                      //инициализация дисплея набором константных команд
  LCD_SendStr(sText);                                                           //отправка текста

  while(1)
  {
   Delay_ms(1);
  }
}

Ещё одно маленькое лирическое отступление: не забываем, что в Си можно встретить следующие выражения:

  1. const int* a;
  2. int const* b;
  3. int *const c;

Первые два определения, по сути своей, являются одним и тем же: указателем на целое, которое нельзя менять. Сам же указатель может "двигаться", например, по массиву.

Третье же определение - int *const c; - является константным указателем на целое. Но есть значение этого указателя задается единожды, при инициализации, и потом поставить его на другой адрес не удастся. При этом указатель можно разыменовать и присвоить по адресу новое значение.

В общем, следите - не отстрелите себе ногу, будьте осторожны!

Будем считать, что с программой на Си в этом плане мы разобрались. Переходим к Паскалю:

//программа работает с LCD-дисплеем МТ-10S1; инициализирует его и выводит на экран приветствие "Hello"
program LCD;
const
  //константы для дисплея
  LCD_A0_OUT_NUMBER = 5;                                                        //номер вывода, к которому подключён вывод A0 дисплея
  LCD_E_OUT_NUMBER = 4;                                                         //номер вывода, к которому подключён E дисплея
  LCD_DATA_MASK = 0x0F;
  LCD_DATA_SHIFT = 0;
  //задержки
  DL_LCD_SWITCH_ON_MS = 20;                                                     //задержка после подачи питания на дисплей в мс
  DL_LCD_BETWEEN_E_CHANGE_MCS = 1;                                              //задержка перед изменением вывода Е дисплея (отправка полубайта команд) в мкс
  DL_LCD_DATA_SEND_MS = 2;                                                      //задержка после отправки одного байта данных или команды в мс
type
  TCommandArr = array [0.. 1] of Byte;											//тип для передачи массива констант в функцию (размер минимальный; MikroPascal не проверяет соответствие размерам)
var
  //определяем назначение портов ввода-вывода
  portLCD: byte at PORTB;
  ddrLCD: byte at DDRB;

procedure LCD_SendByte(DataByte: byte; IsSymbol: byte);
begin
  portLCD.LCD_A0_OUT_NUMBER := IsSymbol;                                        //определяем, что отправляем - команду или данные
  portLCD.LCD_E_OUT_NUMBER := 1;                                                //показываем, что сейчас будет устанавливать данные
  portLCD := portLCD and (not LCD_DATA_MASK) or (DataByte shr 4);               //устанавливаем на выводах DB7-DB4 значение старшего полубайта
  Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS);                                        //на всякий случай делаем задержку перед отправкой
  portLCD.LCD_E_OUT_NUMBER := 0;                                                //отправляем значение старшего полубайта!
  Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS);                                        //на всякий случай делаем задержку перед изменением вывода Е; далее аналогично для младшего полубайта
  portLCD.LCD_E_OUT_NUMBER := 1;
  portLCD := portLCD and (not LCD_DATA_MASK) or (DataByte and 0x0F);
  Delay_us(DL_LCD_BETWEEN_E_CHANGE_MCS);
  portLCD.LCD_E_OUT_NUMBER := 0;
  Delay_ms(DL_LCD_DATA_SEND_MS);                                                //делаем задержку для того, чтобы дисплей мог обработать команду или данные
end;

//функция отправки дисплею строки, заканчивающейся нулём ('\0')
procedure LCD_SendStr(var Str: String);
var
  i: Integer;
begin
  i := 0;
  while (Str[i] <> 0) do                                                        //просто идём по строке и отправляем байты до тех пор, пока не встретим '\0'
  begin
    LCD_SendByte(Str[i], 1);
    inc(i);
  end;
end;

//функция отправки дисплею массива команд, заканчивающейся нулём; на вход указатель на массив констант
procedure LCD_SendCommands(const pCommandArr: ^TCommandArr);
var
  i: Integer;
begin
  i := 0;
  while (pCommandArr^[i] <> 0) do                                               //отправляем команды последовательно до тех пор, пока не встретим команду = 0
  begin
    LCD_SendByte(pCommandArr^[i], 0);
    inc(i);
  end;
end;

const
  Init: array[0.. 6] of Byte  = (%00110011,                                     //команды для инициализации модуля
                                 %00110010,
                                 %00101000,
                                 %00001111,
                                 %00000001,
                                 %00000110,
                                 0);
var
  sText: string[15];
begin
  sText := 'Hello';

  //инициализация портов ввода-вывода
  DDRA := 0;
  PORTA := 0;
  DDRB := 0;
  PORTB := 0;
  DDRD := 0;
  PORTD := 0;
  //инициализация выводов для дисплея - на выход и на 0
  ddrLCD := ddrLCD or (1 shl LCD_A0_OUT_NUMBER) or (1 shl LCD_E_OUT_NUMBER) or LCD_DATA_MASK;

  Delay_ms(DL_LCD_SWITCH_ON_MS);                                                //задержка для дисплея после включения
  LCD_SendCommands(@Init[0]);                                                   //инициализация дисплея
  LCD_SendStr(sText);                                                           //отправка текста
  while(1) do
    Delay_ms(1);
end.

Итак, для работы с массивами комманд мы использовали тут свой тип - TCommandArr = array [0.. 1] of Byte;. Связано появление этого типа с тем, что у MikroPascal-я есть некоторые сложности с передачей массивов в функции, тем более - массивов констант. Мы взяли минимально необходимый размер массива (одна команда и ноль) - но функция будет работать и с длинными массивами команд, так как MikroPascal не проверяет соответствие размеров (да-да, это можно называть костылём). При этом Паскаль не считает автоматически указатель началом массива, как это делает у нас Си, поэтому для получения i-той команды необходимо сначала разыменовать указатель - pCommandArr^ - а потом уже сослаться на i-ый элемент: pCommandArr^[i].

Для передачи строки мы используем var Str: String (используя ключевое слово var, мы работаем по ссылке со строкой напрямую, не создавая её локальную копию - и это позволяет избежать указания размера строки).

Автор - Moriam
Обсудить на форуме