"Вручную" заводим LCD controller stm8l на примере Discovery.


В статье попытаемся “приручить” LCD контроллер МК STM8L152C6T6, который установлен в STM8L-Discovery. Разберёмся с его инициализацией и всеми последующими манипуляциями, необходимыми для оживления дефолтного дисплея. По ходу всего этого дела запилим собственную простую библиотеку с поддержкой кириллицы.

Итак, уважаемые джедаи ЛУТа и кодинга, здравствуйте!

Успели вдоволь налюбоваться на демопрошивку вашей дискавери, а мигающие светодиоды уже говорят с вами во сне? )))
Что же, первый шаг сделан, надо двигаться дальше. Возникает вопрос «куда?»…
Я тоже над этим думал: ведь сразу браться за что-то серьезное на основе нового, для меня, МК не очень хотелось, как говорится: «на Дерибасовскую надо входить постепенно».
В общем, решил я, что надо начинать поближе знакомиться с периферией. И начать с того, что позволит нашим дальнейшим подвигам заявить о себе окружающему миру, а именно дисплея. Во многих наших будущих поделках он будет кстати, да и нередко возникают в процессе написания кода ситуации, когда возможность вывести на дисплей какую-то инфу, например помониторить содержимое переменной в реальном времени, или вести иные долгосрочные наблюдения, чтобы убедиться в догадках или даже исследовать доселе неведомое, приходится очень кстати. Не знаю кому как, а мне почти всегда наличие хоть какого-то экранчика в устройстве значительно упрощает жизнь на этапе проектирования и отладки, даже если в конечном итоге он и не понадобится.
В общем, много лирики – пора переходить к делу.

Для начала пойдем на сайт ST и скачаем Application note AN3114How to use the STM8L152x and STM8L162x LCD controllers»).
Сходу там идёт овер9000 некоторое количество теории и графиков, объясняющих основополагающие принципы работы жидких кристаллов и про “расово верное” управление дисплеями на их основе. Большинство из этого нам не интересно – такие знания вам потребуются в случае, если надумаете сами писать программный драйвер для управления ЖКИ-дисплеем, как когда-то я.
Всю эту теорию стоит, конечно, просмотреть, но на эту тему информации предостаточно и в более удобоваримом виде. Самое интересное начинается с 18-й странички, где размещены блок-схемы, иллюстрирующие устройство LCD-контроллера в МК линейки STM8L medium класса и отдельной диаграммой medium+ и high классов, которые включают в себя старших представителей данного семейства и имеют более навороченный LCD-контроллер (8х44).
Наш STM8L152C6T6, который установлен на дискавери, относится к medium классу, поэтому внимательно изучаем соответствующую диаграмму:

LCD controller block diagram

Как известно, ЖК требуется для управления переменное напряжение, иначе они быстро деградируют и срок их службы значительно сокращается, более того, даже определённая асимметрия управляющих напряжений приводит к появлению постоянной составляющей, также нежелательной для ЖК. Обычно предельные значения этой составляющей и рекомендуемая частота указываются производителем дисплея. Но в любом случае величина постоянной составляющей не должна превышать 30-50 мВ, а частота выбирается в пределах от 30 Гц до 100 Гц. Очевидно, что при частоте ниже указанной будет появляться визуально наблюдаемое мерцание, но и завышать частоту тоже не стоит, потому что ЖК свойственна существенная инерционность (для переориентирования кристаллов требуется некоторое время).
Если над первым нам можно не заморачиваться, поскольку на то он и драйвер, чтоб формировать «правильные» по форме сигналы, то для получения соответствующей частоты нам придется правильно сконфигурировать блок, именуемый Frequency generator, состоящий из 16-тиразрядного предделителя и делителя.
Но для начала нам надо определиться с источником тактирования. Для этого давайте заглянем в Reference manual RM0031, раздел Clock control на страницу 87, где приведена общая структурная схема системы тактирования контроллера:

Clock structure

Из рисунка видно, что тут у нас есть 4 варианта: HSE, HSI, LSI, LSE.

HSE (high speed external) – внешний источник тактирования высокой частоты (кварц).
HSI (high speed internal) – внутренний RC-генератор высокой частоты.
LSI (low speed internal) – внутренний низкочастотный RC-генератор.
LSE (low speed external) – внешний низкочастотный источник тактирования, например часовой кварц.

Вообще система тактирования у стм-ок устроена достаточно круто, но сейчас мы не будем вдаваться в детали.
Нас интересует тактирование для LCD-драйвера. Если мы посмотрим на приведенный рисунок, то увидим, что “схема сфазирована” таким образом, что цепи тактирования LCD связаны с тактированием RTC (real time counter) и источник тактирования у них должен быть общий. Кроме того, можно заметить, что на приведенной диаграмме формируется два источника тактов, подающихся на LCD-драйвер. Один из них – это RTCCLK/2 и LCDCLK. На основе одного, как можно узреть на первой картинке, формируются управляющие напруги для стекляшки, а второй используется для тактирования самого LCD controller-а, а точнее чтения/записи регистров. Но пока нам это не особо интересно, давайте, наконец, затактируем наш драйвер:

//Подаем тактирование на LCD, RTC:
 CLK_PCKENR2 |= (1<<3)|(1<<2);
//Выбираем внешний кварц (LSE) для RTC:
 CLK_CRTCR_bit.RTCSEL3 = 1;
 //CLK_CRTCR_bit.RTCSEL2 = 0;
 //CLK_CRTCR_bit.RTCSEL1 = 0;
 //CLK_CRTCR_bit.RTCSEL0 = 0;
//Ожидание стабилизации внешнего кварца:
 while(CLK_ECKR_bit.LSERDY==0);


все очень элементарно: за тактирование периферии у нас отвечают регистры CLK_PCKENRX, а конкретно за наш LCDCLK_PCKENR2, где мы и устанавливаем соостветствующие биты: 2 — отвечающий за такты RTC и 3 — отвечающий за LCD. Далее, в соответствующем регистре определяем источник тактов для RTC. Думаю из комментариев и так все понятно: источником этих тактов у нас будет внешний низкочастотный кварц.
Выполнив эти операции, рекомендуется дождаться стабильной генерации с кварца. Это конечно не особо критично, но нарушать рекомендации официальной доки рекомендуется лишь любителям решения увлекательных квестов с элементами головоломки (а порою даже триллера), выплывшими в будущем (особенно весело, когда уже в процессе производства).
Итак, тактирование подано, идем дальше:

/*.............Настройка LCD.............*/
//устанавливаем скважность:
 LCD_CR1_bit.DUTY1 = 1;/****фактически ****/
 LCD_CR1_bit.DUTY0 = 1;/*количество COM-ов*/
 
 LCD_CR2_bit.PON0 = 1;
 LCD_CR2_bit.PON1 = 1;
 LCD_CR2_bit.PON2 = 1;
 
//прескаллер n = 2 (коэфициэнт деления будет 2^n, F=(F_RTC/2)/2^n)
 //LCD_FRQ_bit.PS3 = 0;/****После прескаллера (32768/2)/4=4096****/
 //LCD_FRQ_bit.PS2 = 0;/**и дальше проходит через делитель на 16**/
 LCD_FRQ_bit.PS1 = 1;/******что и даст в результате 256 Гц******/
 //LCD_FRQ_bit.PS0 = 0;/******частота кадра же равна 256/4 Гц*****/
//включение LCD:
 LCD_CR3_bit.LCDEN=1;
   
//включим 24 ноги для управления сегментами (SEG00 -- SEG23):
  LCD_PM0 = (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7);
  LCD_PM1 = (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7);
  LCD_PM2 = (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7);


Здесь мы в регистре LCD_CR1 устанавливаем комбинацию битов DUTY, которая соответствует количеству выводов COM нашего индикатора, что описано в подразделе 17.6.1 на странице 267 Reference manual. Стекляшка, установленная на дискавери, имеет 4 «общих», соответсвенно — "11".
Биты PON, находящиеся в регистре LCD_CR2, насколько я понял, определяют задержку между изменением уровней на выводах индикатора, как говорит Reference manual "длительность". Уменьшение этого значения приводит к уменьшению энергопотребления, но для дисплеев с большим внутренним сопротивлением может потребоваться увеличить это значение (видимо время на переориентацию кристаллов?). На практике, с исследуемой стекляшкой лучший результат получился при максимальном значении, уменьшение же этой числа, записываемого в соотв. биты, приводит к появлению незначительной паразитной засветки сегментов.
Дальше мы устанавливаем коэффициенты деления предделителя и делителя, чтобы получить нужное значение частоты для управления дисплеем. Предделитель делит частоту на число, получаемое из двойки, возведенной в степень, которую мы в двоичном виде задаем битами PS[3:0]. На выходе прескаллера, в результате, получим 4096 Гц, которые дальше поступают на основной делитель. По умолчанию биты DIV[3:0], отвечающие за его коэффициент деления равны "000", и это соответствует минимальному коэффициенту деления 16, который равен 16+x, где x=DIV[3:0].Таким образом получаем 256 Гц. Это будет частота изменения состояний на выводах COM, но так как их у нас 4, то частота кадра (частота, с которой будет происходить полное обновление дисплея) равна 256/4=64 Гц.
Дальше, врубив сам ЖКИ контроллер, установкой бита LCDEN в регистре LCD_CR3, накладываем маску портов для определения задействованных в управлени стекляхой пинов. Те, которые не используются — можно юзать в своих целях. Подробнее про это в том же референс мануале начиная со страницы 270.
Вобщем, я думаю, что никаких сложностей тут не должно возникнуть.
Отдельной функцией было решено написать установку контраста, дабы иметь возможность менять его на лету:
void LCD_Contrast(unsigned char cntr)
{
//установка контрастности:
 LCD_CR2 |= ((cntr&0x07)<<1);
}

Контрастность у нас регулируется выбором величины напряжения, с которого и формируются управляющие уровни. Комбинация битов управляет внутренним делителем, откуда и получает нужный уровень:

В качестве опорного напряжения мы можем выбрать внутренний или внешний источник. Про это, а также многие другие фичи можно подробно прочитать в разделе 17, советую если не полностью, то хотя бы покурить структуру регистров. Там много интересного.
Таким образом, это все, что нам нужно, чтобы завести дисплейчик. Теперь допишем саму библиотеку, чтобы работать с индкатором без напряга, как все нормальные потсоны:

Начнем с определения символьных констант
//массив символов цифр
const unsigned int number_mask[] = 
{
  //0
  (1<<A_)|(1<<B_)|(1<<C_)|(1<<D_)|(1<<E_)|(1<<F_)|(0<<G_)|(0<<H_)|(0<<J_)|(0<<K_)|(0<<M_)|(0<<N_)|(0<<P_)|(0<<Q_)|(0<<DP)|(0<<COL), 
  //1
  (0<<A_)|(1<<B_)|(1<<C_)|(0<<D_)|(0<<E_)|(0<<F_)|(0<<G_)|(0<<H_)|(0<<J_)|(0<<K_)|(0<<M_)|(0<<N_)|(0<<P_)|(0<<Q_)|(0<<DP)|(0<<COL), 
  //2
  (1<<A_)|(1<<B_)|(0<<C_)|(1<<D_)|(1<<E_)|(0<<F_)|(1<<G_)|(0<<H_)|(0<<J_)|(0<<K_)|(1<<M_)|(0<<N_)|(0<<P_)|(0<<Q_)|(0<<DP)|(0<<COL),  
  //3
  (1<<A_)|(1<<B_)|(1<<C_)|(1<<D_)|(0<<E_)|(0<<F_)|(1<<G_)|(0<<H_)|(0<<J_)|(0<<K_)|(1<<M_)|(0<<N_)|(0<<P_)|(0<<Q_)|(0<<DP)|(0<<COL), 
  //4
  (0<<A_)|(1<<B_)|(1<<C_)|(0<<D_)|(0<<E_)|(1<<F_)|(1<<G_)|(0<<H_)|(0<<J_)|(0<<K_)|(1<<M_)|(0<<N_)|(0<<P_)|(0<<Q_)|(0<<DP)|(0<<COL), 
  //5
  (1<<A_)|(0<<B_)|(1<<C_)|(1<<D_)|(0<<E_)|(1<<F_)|(1<<G_)|(0<<H_)|(0<<J_)|(0<<K_)|(1<<M_)|(0<<N_)|(0<<P_)|(0<<Q_)|(0<<DP)|(0<<COL), 
  //6
  (1<<A_)|(0<<B_)|(1<<C_)|(1<<D_)|(1<<E_)|(1<<F_)|(1<<G_)|(0<<H_)|(0<<J_)|(0<<K_)|(1<<M_)|(0<<N_)|(0<<P_)|(0<<Q_)|(0<<DP)|(0<<COL), 
  //7
  (1<<A_)|(1<<B_)|(1<<C_)|(0<<D_)|(0<<E_)|(0<<F_)|(0<<G_)|(0<<H_)|(0<<J_)|(0<<K_)|(0<<M_)|(0<<N_)|(0<<P_)|(0<<Q_)|(0<<DP)|(0<<COL), 
  //8
  (1<<A_)|(1<<B_)|(1<<C_)|(1<<D_)|(1<<E_)|(1<<F_)|(1<<G_)|(0<<H_)|(0<<J_)|(0<<K_)|(1<<M_)|(0<<N_)|(0<<P_)|(0<<Q_)|(0<<DP)|(0<<COL), 
  //9
  (1<<A_)|(1<<B_)|(1<<C_)|(1<<D_)|(0<<E_)|(1<<F_)|(1<<G_)|(0<<H_)|(0<<J_)|(0<<K_)|(1<<M_)|(0<<N_)|(0<<P_)|(0<<Q_)|(0<<DP)|(0<<COL)
};

здесь показан пример для цифр, в таком же дуже описаны символы латинского, кириллического алфавита и спецсимволы, чего нет возможности привести полностью в силу объемности этого куска кода.
Записываем функцию, которая осуществляет поиск соответвующей маски для символа:

static unsigned int LCD_Char_Find(unsigned char* symb)
{
  unsigned int cur_char;
//если цифра:
      if ( (*symb <= 0x39) && (*symb >= 0x30) )
      {
        cur_char = number_mask[*symb-'0'];
      }
//если буква латинского алфавита:
      /* Если символ введён в верхнем регистре*/
      else{if ((*symb <= 0x5A) && (*symb >= 0x41))
      {
        cur_char = char_mask[*symb-'A'];
      }
      /* Если символ введён в нижнем регистре*/
      else{if ((*symb <= 0x7A) && ( *symb >= 0x61))    
      {
        cur_char = char_mask[*symb-'a'];
      }
//если буква кириллицы:
      /* Если символ введён в верхнем регистре*/
      else{if ((*symb <= 0xDF) && ( *symb >= 0xC0))
      {
        cur_char = rus_char_mask[*symb-'А'];
      }
      /* Если символ введён в нижнем регистре*/
      else{if (*symb >= 0xE0)
      {
        cur_char = rus_char_mask[*symb-'а'];
      }
      else
      {
        switch (*symb)
        {
//если спецсимвол:            
          case '$':
            cur_char = spec_char_mask[0];
            break;
            
          case '(':
            cur_char = spec_char_mask[1];
            break;
            
          case ')':
            cur_char = spec_char_mask[2];
            break;
            
          case '*':
            cur_char = spec_char_mask[3];
            break;
            
          case '+':
            cur_char = spec_char_mask[4];
            break;
            
          case '-':
            cur_char = spec_char_mask[5];
            break;
            
          case '=':
            cur_char = spec_char_mask[6];
            break;
            
          default:
            cur_char = 0x00;
            break;
        };
      };
      };
      };
      };
      };
      
      if(*symb=='m') {cur_char = spec_char_mask[7];}
      if(*symb=='u') {cur_char = spec_char_mask[8];}
      if(*symb=='n') {cur_char = spec_char_mask[9];}
      
  return(cur_char);
}

и функцию, которая непосредственно выводит полученный символ в соответствующее знакоместо индкатора:
void LCD_Write_Char(unsigned char* ch, bool point, bool column, unsigned char position)
{
  unsigned int cur_char; /*контейнер для хранения маски символа*/
  unsigned char temp[2];
  
/* Определение соответствующей символу константы*/
  cur_char = LCD_Char_Find(ch);
  if(position<5)/* В пятом, шестом знакоместе BAR */
  {
    /* Установка состояния десятичной точки */
    if(point){cur_char |= (1<<DP);};
    /* Установка состояния двоеточия */
    if(column){cur_char |= (1<<COL);};
  };

temp[0]=(unsigned char)cur_char;
temp[1]=(unsigned char)(cur_char>>8);

/*Запись маски символа в биты LCD_RAM, соответствующие текущей позиции*/
switch (position)
  {
    /* Позиция 1 */
    case 1:
      /*Очистка 1M, 1E*/            /*Запись нового значения 1M, 1E*/
      LCD_RAM0 &= ~((1<<1)|(1<<0)); LCD_RAM0 |= temp[0] & ((1<<1)|(1<<0));
      /*Очистка 1G, 1B*/            /*Запись нового значения 1G, 1B*/
      LCD_RAM2 &= ~((1<<7)|(1<<6)); LCD_RAM2 |= (temp[0]<<4) & ((1<<7)|(1<<6));
      /*Очистка 1C, 1D*/            /*Запись нового значения 1C, 1D*/
      LCD_RAM3 &= ~((1<<5)|(1<<4)); LCD_RAM3 |= temp[0] & ((1<<5)|(1<<4));
      /*Очистка 1F, 1A*/            /*Запись нового значения 1F, 1A*/
      LCD_RAM6 &= ~((1<<3)|(1<<2)); LCD_RAM6 |= (temp[0]>>4) & ((1<<3)|(1<<2));
      /*Очистка 1COL, 1P*/          /*Запись нового значения 1COL, 1P*/
      LCD_RAM7 &= ~((1<<1)|(1<<0)); LCD_RAM7 |= temp[1] & ((1<<1)|(1<<0));
      /*Очистка 1Q, 1K*/            /*Запись нового значения 1Q, 1K*/
      LCD_RAM9 &= ~((1<<7)|(1<<6)); LCD_RAM9 |= (temp[1]<<4) & ((1<<7)|(1<<6));
      /*Очистка 1DP, 1N*/           /*Запись нового значения 1DP, 1N*/
      LCD_RAM10 &= ~((1<<5)|(1<<4)); LCD_RAM10 |= temp[1] & ((1<<5)|(1<<4));
      /*Очистка 1H, 1J*/            /*Запись нового значения 1H, 1J*/
      LCD_RAM13 &= ~((1<<3)|(1<<2)); LCD_RAM13 |= (temp[1]>>4) & ((1<<3)|(1<<2));
      break;
    
    /* Позиция 2 */
    case 2:
      /*Очистка 2M, 2E*/            /*Запись нового значения 2M, 2E*/
      LCD_RAM0 &= ~((1<<3)|(1<<2)); LCD_RAM0 |= (temp[0]<<2) & ((1<<3)|(1<<2));
      /*Очистка 2G, 2B*/            /*Запись нового значения 2G, 2B*/
      LCD_RAM2 &= ~((1<<5)|(1<<4)); LCD_RAM2 |= (temp[0]<<2) & ((1<<5)|(1<<4));
      /*Очистка 2C, 2D*/            /*Запись нового значения 2C, 2D*/
      LCD_RAM3 &= ~((1<<7)|(1<<6)); LCD_RAM3 |= (temp[0]<<2) & ((1<<7)|(1<<6));
      /*Очистка 2F, 2A*/            /*Запись нового значения 2F, 2A*/
      LCD_RAM6 &= ~((1<<1)|(1<<0)); LCD_RAM6 |= (temp[0]>>6) & ((1<<1)|(1<<0));
      /*Очистка 2COL, 2P*/          /*Запись нового значения 2COL, 2P*/
      LCD_RAM7 &= ~((1<<3)|(1<<2)); LCD_RAM7 |= (temp[1]<<2) & ((1<<3)|(1<<2));
      /*Очистка 2Q, 2K*/            /*Запись нового значения 2Q, 2K*/
      LCD_RAM9 &= ~((1<<5)|(1<<4)); LCD_RAM9 |= (temp[1]<<2) & ((1<<5)|(1<<4));
      /*Очистка 2DP, 2N*/           /*Запись нового значения 2DP, 2N*/
      LCD_RAM10 &= ~((1<<7)|(1<<6)); LCD_RAM10 |= (temp[1]<<2) & ((1<<7)|(1<<6));
      /*Очистка 2H, 2J*/            /*Запись нового значения 2H, 2J*/
      LCD_RAM13 &= ~((1<<1)|(1<<0)); LCD_RAM13 |= (temp[1]>>6) & ((1<<1)|(1<<0));
      break;
    
    /* Позиция 3 */
    case 3:
      /*Очистка 3M, 3E*/            /*Запись нового значения 3M, 3E*/
      LCD_RAM0 &= ~((1<<5)|(1<<4)); LCD_RAM0 |= (temp[0]<<4) & ((1<<5)|(1<<4));
      /*Очистка 3G, 3B*/            /*Запись нового значения 3G, 3B*/
      LCD_RAM2 &= ~((1<<3)|(1<<2)); LCD_RAM2 |= temp[0] & ((1<<3)|(1<<2));
      /*Очистка 3C, 3D*/            /*Запись нового значения 3C, 3D*/
      LCD_RAM4 &= ~((1<<1)|(1<<0)); LCD_RAM4 |= (temp[0]>>4) & ((1<<1)|(1<<0));
      /*Очистка 3F, 3A*/            /*Запись нового значения 3F, 3A*/
      LCD_RAM5 &= ~((1<<7)|(1<<6)); LCD_RAM5 |= temp[0] & ((1<<7)|(1<<6));
      /*Очистка 3COL, 3P*/          /*Запись нового значения 3COL, 3P*/
      LCD_RAM7 &= ~((1<<5)|(1<<4)); LCD_RAM7 |= (temp[1]<<4) & ((1<<5)|(1<<4));
      /*Очистка 3Q, 3K*/            /*Запись нового значения 3Q, 3K*/
      LCD_RAM9 &= ~((1<<3)|(1<<2)); LCD_RAM9 |= temp[1] & ((1<<3)|(1<<2));
      /*Очистка 3DP, 3N*/           /*Запись нового значения 3DP, 3N*/
      LCD_RAM11 &= ~((1<<1)|(1<<0)); LCD_RAM11 |= (temp[1]>>4) & ((1<<1)|(1<<0));
      /*Очистка 3H, 3J*/            /*Запись нового значения 3H, 3J*/
      LCD_RAM12 &= ~((1<<7)|(1<<6)); LCD_RAM12 |= temp[1] & ((1<<7)|(1<<6));
      break;
    
    /* Позиция 4 */
    case 4:
      /*Очистка 4M, 4E*/            /*Запись нового значения 4M, 4E*/
      LCD_RAM0 &= ~((1<<7)|(1<<6)); LCD_RAM0 |= (temp[0]<<6) & ((1<<7)|(1<<6));
      /*Очистка 4G, 4B*/            /*Запись нового значения 4G, 4B*/
      LCD_RAM2 &= ~((1<<1)|(1<<0)); LCD_RAM2 |= (temp[0]>>2) & ((1<<1)|(1<<0));
      /*Очистка 4C, 4D*/            /*Запись нового значения 4C, 4D*/
      LCD_RAM4 &= ~((1<<3)|(1<<2)); LCD_RAM4 |= (temp[0]>>2) & ((1<<3)|(1<<2));
      /*Очистка 4F, 4A*/            /*Запись нового значения 4F, 4A*/
      LCD_RAM5 &= ~((1<<5)|(1<<4)); LCD_RAM5 |= (temp[0]>>2) & ((1<<5)|(1<<4));
      /*Очистка 4COL, 4P*/          /*Запись нового значения 4COL, 4P*/
      LCD_RAM7 &= ~((1<<7)|(1<<6)); LCD_RAM7 |= (temp[1]<<6) & ((1<<7)|(1<<6));
      /*Очистка 4Q, 4K*/            /*Запись нового значения 4Q, 4K*/
      LCD_RAM9 &= ~((1<<1)|(1<<0)); LCD_RAM9 |= (temp[1]>>2) & ((1<<1)|(1<<0));
      /*Очистка 4DP, 4N*/           /*Запись нового значения 4DP, 4N*/
      LCD_RAM11 &= ~((1<<3)|(1<<2)); LCD_RAM11 |= (temp[1]>>2) & ((1<<3)|(1<<2));
      /*Очистка 4H, 4J*/            /*Запись нового значения 4H, 4J*/
      LCD_RAM12 &= ~((1<<5)|(1<<4)); LCD_RAM12 |= (temp[1]>>2) & ((1<<5)|(1<<4));
      break;
    
    /* позиция 5 */
    case 5:
      /*Очистка 5M, 5E*/            /*Запись нового значения 5M, 5E*/
      LCD_RAM1 &= ~((1<<1)|(1<<0)); LCD_RAM1 |= temp[0] & ((1<<1)|(1<<0));
      /*Очистка 5G, 5B*/            /*Запись нового значения 5G, 5B*/
      LCD_RAM1 &= ~((1<<7)|(1<<6)); LCD_RAM1 |= (temp[0]<<4) & ((1<<7)|(1<<6));
      /*Очистка 5C, 5D*/            /*Запись нового значения 5C, 5D*/
      LCD_RAM4 &= ~((1<<5)|(1<<4)); LCD_RAM4 |= temp[0] & ((1<<5)|(1<<4));
      /*Очистка 5F, 5A*/            /*Запись нового значения 5F, 5A*/
      LCD_RAM5 &= ~((1<<3)|(1<<2)); LCD_RAM5 |= (temp[0]>>4) & ((1<<3)|(1<<2));
      /*Очистка 5COL, 5P*/          /*Запись нового значения 5COL, 5P*/
      LCD_RAM8 &= ~((1<<1)|(1<<0)); LCD_RAM8 |= temp[1] & ((1<<1)|(1<<0));
      /*Очистка 5Q, 5K*/            /*Запись нового значения 5Q, 5K*/
      LCD_RAM8 &= ~((1<<7)|(1<<6)); LCD_RAM8 |= (temp[1]<<4) & ((1<<7)|(1<<6));
      /*Очистка 5DP, 5N*/           /*Запись нового значения 5DP, 5N*/
      LCD_RAM11 &= ~((1<<5)|(1<<4)); LCD_RAM11 |= temp[1] & ((1<<5)|(1<<4));
      /*Очистка 5H, 5J*/            /*Запись нового значения 5H, 5J*/
      LCD_RAM12 &= ~((1<<3)|(1<<2)); LCD_RAM12 |= (temp[1]>>4) & ((1<<3)|(1<<2));
      break;
    
    /* Позиция 6 */
    case 6:
      /*Очистка 6M, 6E*/            /*Запись нового значения 6M, 6E*/
      LCD_RAM1 &= ~((1<<3)|(1<<2)); LCD_RAM1 |= (temp[0]<<2) & ((1<<3)|(1<<2));
      /*Очистка 6G, 6B*/            /*Запись нового значения 6G, 6B*/
      LCD_RAM1 &= ~((1<<5)|(1<<4)); LCD_RAM1 |= (temp[0]<<2) & ((1<<5)|(1<<4));
      /*Очистка 6C, 6D*/            /*Запись нового значения 6C, 6D*/
      LCD_RAM4 &= ~((1<<7)|(1<<6)); LCD_RAM4 |= (temp[0]<<2) & ((1<<7)|(1<<6));
      /*Очистка 6F, 6A*/            /*Запись нового значения 6F, 6A*/
      LCD_RAM5 &= ~((1<<1)|(1<<0)); LCD_RAM5 |= (temp[0]>>6) & ((1<<1)|(1<<0));
      /*Очистка 6COL, 6P*/          /*Запись нового значения 6COL, 6P*/
      LCD_RAM8 &= ~((1<<3)|(1<<2)); LCD_RAM8 |= (temp[1]<<2) & ((1<<3)|(1<<2));
      /*Очистка 6Q, 6K*/            /*Запись нового значения 6Q, 6K*/
      LCD_RAM8 &= ~((1<<5)|(1<<4)); LCD_RAM8 |= (temp[1]<<2) & ((1<<5)|(1<<4));
      /*Очистка 6DP, 6N*/           /*Запись нового значения 6DP, 6N*/
      LCD_RAM11 &= ~((1<<7)|(1<<6)); LCD_RAM11 |= (temp[1]<<2) & ((1<<7)|(1<<6));
      /*Очистка 6H, 6J*/            /*Запись нового значения 6H, 6J*/
      LCD_RAM12 &= ~((1<<1)|(1<<0)); LCD_RAM12 |= (temp[1]>>6) & ((1<<1)|(1<<0));
      break;
    /* выходим во всех остальных случаях (некорректный номер позиции)*/
      default:
      break;
  }
}

Вот и все. Осталось только ещё немного облегчить себе жизнь, организовав вывод строки:
void LCD_Write_String(unsigned char* str)
{
  unsigned char i = 1;

  /* Пока не дошли до конца строки
     или не вывели 6 символов*/
  while ((*str != 0) && (i < 7))
  {
    /* Выводим символ в текущее знакоместо */
    LCD_Write_Char(str, 0, 0, i);

    if(*str == '.')
    {
      i--;
      LCD_Write_Char(str-1, 1, 0, i);
    };
    if(*str == ':')
    {
      i--;
      LCD_Write_Char(str-1, 0, 1, i);
    };
    
    /* перевод указателя на след. символ */
    str++;

    /* Инкремент номера знакоместа */
    i++;
  };
}


Теперь у нас уже совсем все готово. И можно радоваться результатам.

Запилим тестовый проект в IARе

#include "iostm8l152c6.h"
#include "LCD.h"

unsigned long i;
int main( void )
{
 CLK_CKDIVR_bit.CKM=0;
 LCD_Init();
 LCD_Contrast(5);

 
 
 while(1)
 {
   LCD_Write_String("Hello ");
  for(i=0; i<200000; i++){asm("nop");};
  LCD_Write_String(" world");
  for(i=0; i<200000; i++){asm("nop");};
 }; 
}

Компилим, грузим в Discovery и радуемся счастью.
Короткометражная документалка прилагается:



ВНИМАНИЕ! Некоторые кириллическе символы будут выглядеть достаточно специфично в силу невозможности полноценного их отображения на таком дисплее. Во всяком случае фантазии автора не хватило, чтобы придумать что-то адекватней. Но подобных «Щ» литер немного и, в целом, на мой взгляд, эта безымянная секляшка вполне неплохо канает подойдет для многих задач. Еще дописать сюда разных эффектов, и прочих свистелок, типа блек-джека и шлюх мигания знакомест и т.д. — и можно юзать)

З.Ы. Прошу простить за сумбурность изложения. Просто статья начата была ещё пару месяцев назад и валялась в виде полуфабката долгое время. Пришлось дописать её хоть как-то или убить. Я выбрал первое. А вот насколько это правильно решать вам.
Проект в ИАРе, естественно, прилагается.
Если это интересно, то в ближайшем будущем возможно появление подобного на тему RTC, с написанием либы и использованием все того же LCD-индикатора. Что скажете?
Благодарю за внимание.
З.З.Ы. Я далеко не программист, поэтому код далек от идеала, а возле оптимальности он, скорее всего, и не валялся, в связи с чем с благодарностью принимаются все корректные замечания и советы, спасибо.
  • +3
  • 01 марта 2012, 01:29
  • DOOMSDAY
  • 1
Файлы в топике: lcd.zip

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

RSS свернуть / развернуть
З.З.Ы. Я далеко не программист, поэтому код далек от идеала
Сам пишу далеко не идеальный код, но раз замечания и советы принимаются, сделаю парочку.

else{if (*symb >= 0xE0)

форматирование выбрано несколько не удачно. Если ву обрабатываете несколько веток конструкцией else-if, записывая её в строку, то не используйте «лишнюю» фигурную скобку { Неудачность такого форматирования становится ясна в конце блока, где у вас идет столбик закрывающих скобок. Довольно просто ошибится в их количестве. Да и их роль не понятна при просмотре, ведь открывающую скобку не видно из-за слитного написания.
Если же фигурные скобки хочется писать, дабы быть уверенным в интерпретации кода (бывает полезно), то стоит комбинировать их с форматированием отступами
if(a) {
 ...
else {
 if(a) {
  ...
 } else {
  //..
 }
}


Мторой момент:
static unsigned int LCD_Char_Find(unsigned char* symb) {
 ... *symb ... *symb ... *symb ... *symb
}

Вы не изменяете значение, хранящееся по указателю, весь код содержит разадресацию и более того размер самой величины 1 байт. В таком случае гораздо лучше будет применить передачу по значению:
static unsigned int LCD_Char_Find(unsigned char symb) {
 ... symb ... symb ... symb ... symb
}

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

Ну остальное как и у меня, в принципе.
0
благодарю. весьма ценные замечания.
правда, с конструкциями вида «if-else» обычно как раз отступами разделываюсь и все весьма читабельно. это на сей раз как-то немного оплошал. а вобще редакторы, автоматом подсвечивающие пару скобке, рядом с которой расположен курсор — очень помогают. особенно копать чужой код)
0
Копаюсь в чужом коде и предпочитаю использовать обычный листер из командера. Никакой подсветки, ничего. и порой пропускаю подобные конструкции (многострочный комментарий в тексте без отступов, фигурные скобки упущены и мн. др.), но всё равно на мой взгляд это лучшее средство для разбора чужого кода.

Раз уж коммент написал, то отмечу ещё один неприятный момент
unsigned long i;
int main( void )
{ ...
 for(i=0; i<200000; i++){asm("nop");};

не желательно переменный цикла делать глобальными, тем более такие как i j k. В идеале их объявлять следует в самом цикле (если у вас не чистый С конечно). Косяк будет если в другой функции (вызываемой внутри цикла) вы напишите такой же цикл и забудите описать свою переменную.
0
само-собой. это же просто пример «для поиграться». естественно в рабочем коде никто в здравом уме не додумается лепить такое)) во всяком случае надеюсь на это.)
0
Спасибо за статью!
Успели вдоволь налюбоваться на демопрошивку вашей дискавери, а мигающие светодиоды уже говорят с вами во сне? )))
Это про меня) Теперь вот с дисплеем поиграю)
0
  • avatar
  • Dmi
  • 01 марта 2012, 11:04
Спасибо за статью.
Тоже наигрался вдоволь с этим ЖК, но только я переписал под себя кусок кода из примера WaveGenerator-а. Первым делом тоже добавил русские буквы.
Использовал ЖК-дисплей для вывода отладочной информации, или просто всякой фигни.
Потом попытался подключить к STM8L-Descovery небольшую гирлянду из светодиодов (для валентинки) и понял, что из 48 ножек МК из-за этого LCD на плате свободно всего 3 ножки, и то если не впаивать внешний кварц высокочастотный (тогда вообще 1 нога =). Ещё 3 ножки можно освободить, если выпаять перемычки связывающие с цепью измерения тока потребления. Отключать светодиоды (2 вывода), кнопку (1 вывод) и кварц на 32кГц (ещё 2 вывода) не хотелось.
Всего 6 свободных ножек на 48-ми ногом кристалле — меня жутко поразило (а ещё кто то сетует на отсутствие STM8 в маленьких корпусах). В связи с тем, что у нас сложно достать по нормальной цене такие стекляшки, решил что встроенный LCD контролер совсем мне не нужен, лучше я подключу символьный ЖК-дисплей 16х2 (который легко у нас достать).
Ещё раз спасибо за статью, жду статью про RTC с нетерпением!
0
Такие ЖК у нас легко достать из китайских гаджетов: часы, термометры, брелки сигналок,…
Так можно стандартную китайскую штамповку научить своим желаниям, и получится нестандартный гаджет.
P.S.: Дожили… Клонируем Китай… :)
0
а если на ибее сей дисплей заказать? 1 в 1 допустим. интересовался кто нить?
0
  • avatar
  • Dmi
  • 01 марта 2012, 17:34
А можно ссылку на ebay с подобными дисплеями?
0
Так я и просил ссылку. неправильно поняли =)
0
Спасибо за ссылки!
0
это все хорошо, но немного не то. хотелось бы именно 16-сегментник или что-то подобное, а то на семь сегментов кроме цифр мало что можно вывести…
0
Спасибо за статейку. Вот бы такую же ждя STM32L152… Будем ждать
0
Вопрос… так, что б разобраться…
char_mask[*symb-'A']
функцию этой строчки знаю, а вот как из *symb-'A' получаем нужный символ!? Буду благодарен...)
0
  • avatar
  • Onic
  • 29 марта 2012, 10:11
'A' == 0x41
'B' == 0x42

Т.е из полученного ASCII кода вычитаем «смещение» и получаем порядковый номер символа относительно 'A'
0
*symb — это, в данном случае, указатель на место в оперативе, где хранится код символа (в ОЗУ будет автоматом помещена вся передаваемая в функцию строка (вобще в реальных применениях так делать не стоит. намного разумнее разместить выводимые строки во флеше и передавать указатель на начало такой строки в качестве аргумента при выводе)) так вот: символы латинского алфавита, как Вам уже писали, в таблице ASCII расположены последовательно, начиная с номера 0x41, а в нашем массиве масок символов char_mask[] они располагаются в таком же порядке, но адресуются начиная с нулевого элемента массива. Соответственно символу «А», ASCII код которого 0x41, в нашем массиве констант соответствует элемент char_mask[0], символу B — char_mask[1] и т.д…
Таким образом мы видим, что адреса масок символов нашем флеш-массиве отличаются от их ASCII кодов как раз на 0x41 или на «A». Соответственно, мы компенсируем такое постоянное смещение путем преобразования «адреса» символа в таблице ASCII в адрес соответствующей ему маски в нашем массиве.
Если бы перед нами стояли задачи вывода на более серьезный дисплей, то можно было бы не жалеть флеш, а разместить всю таблицу целиком в памяти и не заморачиваться с адресацией вобще. Но в данном применении это особого смысла не имеет, ведь хранить символы, которые на наш экранчик мы никогда вывести не сможем — это как-то неразумно.
0
попытался завести LCD контроллер на STM8L152R6 вроде всё тоже, а сигналов на выходе com и seg нету
что это может быть???
0
А Вы использовали код из этой статьи?
0
да
но пробовал ещё и из других, на discovery всё нормально, как перехожу на свою — затык, хотя светодиодом мой мигает хорошо
0
Ну не знаю, схемы уже сравнивали (дискавери и свою)?
0
да сравнивал, с поправкой на другой корпус, питание только 3.1V а так всё вроде тоже самое.
я сравнил инициализацию LCD — вроде тоже самое, может с CLK что-то не так?
такое впечатление, что инициализация не проходит com и seg висят как после сброса
вообще контроллер очень навороченный, трудно разбираться (а с английским у меня плохо)
0
да сравнивал, с поправкой на другой корпус, питание только 3.1V а так всё вроде тоже самое.
я сравнил инициализацию LCD — вроде тоже самое, может с CLK что-то не так?
такое впечатление, что инициализация не проходит com и seg висят как после сброса
вообще контроллер очень навороченный, трудно разбираться (а с английским у меня плохо)

пооробуй посмотреть что находится в регистрах контроллера жки перед самым выводом информаии, а также попробуй external источник питания для жки, у меня помогло
0
Пытаюсь залить проект в плату, IAR ругается вот так:
Error[Lp025]: absolute section .near.noinit (LCD.o #27) ([0x005406-0x005406]) overlaps with absolute section .near.noinit (LCD.o #25) ([0x005405-0x005406])

Ошибка пропадает, если при включении ног для управления сегментами (в LCD.с) убрать вторую или третью строку:
//включим 24 ноги для управления сегментами (SEG00 -- SEG23):
  LCD_PM0 = (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7);
  // LCD_PM1 = (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7);
  LCD_PM2 = (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7);

При этом прошивка заливается, но треть экрана не работает.
В проекте менял только источник тактирования на LSI:
//Подаем тактирование на LCD, RTC:
 CLK_PCKENR2 |= (1<<3)|(1<<2);
//Выбираем внутренний н/ч генератор (LSI) для RTC:
 //CLK_CRTCR_bit.RTCSEL3 = 0;
 //CLK_CRTCR_bit.RTCSEL2 = 0;
 CLK_CRTCR_bit.RTCSEL1 = 1;
 //CLK_CRTCR_bit.RTCSEL0 = 0;
//Ожидание стабилизации внутреннего генератора:
 while(CLK_ICKCR_bit.LSIRDY==0);

Что бы это значило?
0
Это в дискавери заливаете или контроллер другой?
Какая версия ИАРа?
Проект используете готовый или создавали свой?
0
В дискавери, ИАР 1.40.1, проект готовый, изменен только источник тактирования на LSI (как выше написано)
Проект здесь: yadi.sk/d/JZ0vYekEG3jjb
0
Проект, насколько помню, создавался в IAR 1.30.
А вы проект полностью пересобирали?
0
Попробуйте еще создать проект с нуля и потом подключить туда исходники.
0
Печальбеда, не помогло
0
Взял инициализацию из другого проекта, теперь работает
0
Это из-за опечатки в заголовочном файле 'iostm8l152c6.h', который 'идет в комплекте' с IAR 1.40.1, там в объявлении структуры __BITS_LCD_PM1 закралась лишняя строка:
/* LCD Port mask register 1 */
#ifdef __IAR_SYSTEMS_ICC__
typedef struct
{
  unsigned char SEG08       : 1;
  unsigned char SEG09       : 1;
  unsigned char SEG10       : 1;
  unsigned char SEG11       : 1;
  unsigned char SEG12       : 1;
  unsigned char SEG13       : 1;
  unsigned char SEG14       : 1;
//  unsigned char SEG07       : 1; <- вот эту строчку закомментировал
  unsigned char SEG15       : 1;
} __BITS_LCD_PM1;
#endif
__IO_REG8_BIT(LCD_PM1,     0x5405, __READ_WRITE, __BITS_LCD_PM1);

можно её удалить или закомментировать, у меня эта строчка номер 7699.
0
Хорошая статья. А можно подробнее про генерацию символов?
0
А где можно купить такие LCD?
Уже два месяца ищу нечто подобное, но в Москве таких миниатюрных нет (
0
А такие вообще продаются? Ни разу не видел голых стекляшек в продаже. На фото — из комплекта дискавери, и отдельно, увы, не встречается.
0
Для тех, кто захочет использовать кириллицу в связке с SPD, порядок битов, отвечающих за сегменты немного другой:


/*  =========================================================================
                                 LCD MAPPING
    =========================================================================
	    A
     _  ----------
COL |_| |\   |J  /|
       F| H  |  K |B
     _  |  \ | /  |
COL |_| --G-- --M--
        |   /| \  |
       E|  Q |  N |C
     _  | /  |P  \|   
DP  |_| -----------  
	    D         

 An LCD character coding is based on the following matrix:
      { E , D , P   , J  }
      { M , C , COL , H  }
      { B , A , K   , N  }
      { G , F , Q   , DP }

 The character 'A' for example is:
  -------------------------------
LSB   { 1 , 0 , 0 , 0   }
      { 1 , 1 , 0 , 0   }
      { 1 , 1 , 0 , 0   }
MSB   { 1 , 1 , 0 , 0   }
      -------------------
  'A' =  F    E   0   0 hexa

*/

#define A_	10
#define B_	14
#define C_	9
#define D_	8
#define E_	12
#define F_	11
#define G_	15
#define H_	1
#define J_	0
#define K_	6
#define M_	13
#define N_	2
#define P_	4
#define Q_	7
#define DP	3
#define COL	2
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.