Семисегментный дисплей

Первым делом нам нужно получить какое-то средство отладки – чтобы можно было искать ошибки (а они неизбежно появятся) и получать какой-то отзыв от контроллера.
Поэтому займемся сперва дисплеем на семисегментных индикаторах.

Рассмотрим сначала, как работает отдельный семисегментный индикатор:


Рисунок 1. Схема семисегментного индикатора с точкой.

Вот так схематично выглядит семисегментный индикатор с точкой.
Устроен он очень просто: есть восемь выводов (по одному на каждый сегмент + точка) и один (или два, которые объединяются вместе перемычкой – на рисунке это 3 и 8) общий вывод, который подключается либо на ноль (семисегментный индикатор с общим катодом), либо к питанию (семисегментный индикатор с общим анодом).
Вот так выглядит внутренняя схема индикатора:


Рисунок 2. Внутреннее устройство семисегментного индикатора с точкой.

Вот как располагаются сегменты:


Рисунок 3. Расположение сегментов.

Иными словами – семисегментный индикатор – не что иное, как семь (восемь, если есть точка) светодиодов, у которых один из выводов подключен к общей точке. В зависимости от того, катоды объединены в общую точку, или аноды, выделяют индикаторы, соответственно, с общим катодом или общим анодом. Общая точка подключается либо к «-» (общий катод), либо к «+» (общий анод).

Чтобы загорелся какой-то из сегментов, нужно подать напряжение между общей точкой и соответствующим выводом. Таким образом, и мы подаём напряжение на выводы A, B и C, то на индикаторе загорится "7".

Отдельные семисегментные индикаторы, как видим, подключаются достаточно просто.
Теперь рассмотрим, как подключить несколько индикаторов.
Здесь есть варианты:

Во-первых, можно найти семисегментные индикаторы на 2-3 и больше разрядов, которые будут выглядеть примерно вот так:


Рисунок 4. Схема трёхразрядного семисегментного дисплея.

У таких дисплеев есть восемь выводов для определения индикации (назовем их шиной данных), и несколько (по количеству разрядов) выводов для выбора разряда (шина адреса). Выводы для выбора разряда являются общими катодами или анодами.
Например, у нас трехразрядный индикатор. Попробуем вывести на нём "123"

  1. Чтобы вывести "1" в старшем (крайнем левом) разряде, нам нужно сначала погасить индикатор (отключаем все общие точки), затем выставить на шине данных нужную комбинацию – в данном случае, выводы B и C подключаем к "+", если индикатор с общим катодом, или к "-", если индикатор с общим анодом - и только потом подключить соответствующую общую точку. После этого необходимо сделать маленькую паузу, чтобы светодиоды успели зажечься.
  2. Для того, чтобы вывести "2", необходимо снова погасить индикатор; выставить на выводах данных обозначение "2" (выводы A, B, D, E, G), а затем подключить нужную общую точку - вывод CA2. И снова пауза.
  3. Аналогично для "3".
  4. И так до бесконечности

Если мы будем достаточно быстро циклически выполнять 1) - 3), то будет сложно заметить мерцание (выключили все разряды – включили первый – маленькая пауза – выключили все разряды – включили второй – маленькая пауза – выключили все разряды – включили третий – маленькая пауза – выключили все разряды – включили первый - …) – будет казаться, что весь дисплей горит ровно.
Вариант второй – у дисплея есть отдельные выводы a-g и DP для каждого разряда – или мы сами составляем дисплей из отдельных индикаторов.
Наша задача – по сути, реализовать первый вариант из подручных средств: будем использовать общие для всех разрядов восемь выводов данных и выводы адреса для выбора разряда.

Теперь о подключении - у нас индикаторы с общим анодом (на шине данных светодиоды включаются "минусом", а на шине адресации разряд выбирается "плюсом"):


Рисунок 5. Схема подключения нескольких отдельных семисегментных индикаторов.

Упрощенная схема представлена на рисунке.
Так, мы просто объединяем выводы a-g, dp для всех индикаторов; «общими» выводами же управляем выбором конкретного индикатора – а дальше можем работать по алгоритму выше.

Работать будет, но дисплей будет светиться тускловато – выводы микроконтроллера, связанные с общим входом, обеспечивают довольно большой ток, когда подключены к «земле» (логический 0), и маленький при подтяжке к питанию (логическая 1) - поэтому добавим в схему транзисторы, которые будут работать в режиме насыщения (он же режим ключа):


Рисунок 6. Схема подключения нескольких отдельных семисегментных индикаторов с общим анодом с помощью транзисторов.

Так, теперь можно приступать к программе.
Рассмотрим, что должен уметь «кирпичик» работы с дисплеем, что он получает на вход и на выход.
Во-первых, «кирпичик» должен знать выводы, связанные с дисплеем: нам нужно 8 выводов для шины данных (будем использовать порт D) и 6 выводов для шины адреса (выводы B0 – B5) – требуется функция для инициализации дисплея. При этом нужно учитывать, что наш дисплей – индикаторы с общим анодом.
На вход должна поступать строка, которую мы сможем перевести в символы для семисегментного дисплея; соответственно, нужна функция записи строки в «память дисплея» (в роли памяти будет выступать массив на шесть байтовых элементов).
Ещё мы помним, что для того, чтобы наш дисплей постоянно показывал какую-либо строку, нам нужно часто-часто отображать каждый разряд дисплея – значит, нам потребуется функция, которая будет постоянно вызываться в прерывании.

Для начала попробуем просто выводить какие-то символы на дисплей. При этом у нас и шина данных, и шина адресации «включаются» нулем: шина данных потому, что индикаторы с общим анодом, а шина адресации – из-за транзисторов. Шина данных будет размещаться на выводах порта D, адресная шина – на выводах B0-B5; при этом к выводу B0 подключен самый старший разряд, к выводу B5 – самый младший.
Нужно понять, что «единица», «символ единицы» и «код единицы» - три совершенно разные вещи. Если мы проводим какие-то математические вычисления, то мы используем «единицу» - число или цифру; если мы работаем со строкой – то используем «символ единицы» - некий код из таблицы ACSII; а вот «код единицы» - это то значение, которое мы должны подать на шину данных, чтобы на семисегментном индикаторе загорелся символ единицы (хо-хо, мы снова всех запутаем!)

Вот код для самой простой программки, которая будет просто отображать «012345» на дисплее (используем среду IAR):

//выводим на экран "012345"
#include "ioavr.h"
#include "inavr.h"
#include "stdint.h"
 
//ddr и port для работы с уже выбранным индикатором
#define DispDdrDatabus DDRD
#define DispPortDatabus PORTD
//ddr и port для выбора индикатора
#define DispDdrChoose DDRB
#define DispPortChoose PORTB
#define DISP_CHOOSE_MASK 0x3F
#define DISP_SEGMENT_COUNT 6
 
uint8_t DispCurrSegment, DispCurrSegmentMask;                                   //переменные для хранения для номера текущего индикатора и его маски
uint8_t DispMemory[DISP_SEGMENT_COUNT] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92};  //непосредственно сама память дисплея
 
//функция для перерисовки дисплея
void RedrawDisplay(void)
{
    DispPortChoose |= DISP_CHOOSE_MASK;                                         //не выбираем ни одного индикатора
    DispPortDatabus = DispMemory[DispCurrSegment];                              //настраиваем шину данных
    DispPortChoose = (DispPortChoose | DISP_CHOOSE_MASK) & (˜DispCurrSegmentMask);  //выбираем нужный индикатор
                                             
    //определяем следующий сегмент и его маску
    if (DispCurrSegment < DISP_SEGMENT_COUNT - 1)
    {
        DispCurrSegment++;
        DispCurrSegmentMask = DispCurrSegmentMask << 1;
    }
    else                                                                        //если переходим на первый сегмент - обнуляем счётчик и меняем маску
    {
        DispCurrSegment = 0; 
        DispCurrSegmentMask = 1;                                                                                                  
    }
}
 
//функция инициализации дисплея
void DisplayInit()
{
  DispDdrChoose |= DISP_CHOOSE_MASK;                                            //все выводы, относящиеся к дисплею - на выход
    DispPortChoose = 0xFF;                                                      //не выбираем ни одного индикатора
    DispDdrDatabus = 0xFF;
  DispPortDatabus = 0xFF;                                                       //и не выводим ничего на шину данных                                                          
    DispCurrSegment = 0;                                                        //определяем, что текущий элемент - нулевой
    DispCurrSegmentMask = 1;                                                    //соответственно, маска для нулевого элемента 
}
 
void main( void )
{
  DDRB = 0;
  PORTB = 0;
  DDRD = 0;
  PORTD = 0;
  DDRC = 0;
  PORTC = 0;
   
  DisplayInit();
  while(1)
  {
    RedrawDisplay();
  }
}

Итак, у нас есть память – массив DispMemory; в нем по умолчанию записана строка с кодами символов 012345 (не путать с самими символами!). Чтобы получить эти коды, можно просто вычислять значения с помощью «распиновки» - см. рисунок … - или можно, например, использовать простенькие программки типа приложения «Seven Segment Editor» в среде MikroC.
Также есть переменные DispCurrSegment – показывает номер текущего работающего индикатора (он же номер текущего отображаемого элемента в массиве DispMemory), и DispCurrSegmentMask – соответственно, маска для текущего индикатора для шины адресации.
Для работы используются две функции – функция инициализации (ну, тут все понятно) и функция перерисовки дисплея. Последняя делает следующие действия:

  • выключает дисплей через шину адресации
  • выставляет на шине данных нужное значение
  • включает нужный индикатор
  • переходит на следующий индикатор и изменяет маску

В принципе, все как мы и планировали выше.
Но есть несколько проблем:
Самое главное – это то, что нельзя вызывать функцию перерисовки дисплея в главной функции, так как в main-е в дальнейшем у нас будет вызываться множество функций, а перерисовка дисплея критична ко времени (чтобы убедиться в этом, просто добавьте задержку после вызова функции RedrawDisplay – на 5 мс изображение заметно дрожит, на 10 – раздражающе перемигивает)
Что ж, делать нечего – используем прерывание!

//добавляем прерывание
//выводим на экран "012345"
#include "ioavr.h"
#include "inavr.h"
#include "stdint.h"
 
//ddr и port для работы с уже выбранным индикатором
#define DispDdrDatabus DDRD
#define DispPortDatabus PORTD
//ddr и port для выбора индикатора
#define DispDdrChoose DDRB
#define DispPortChoose PORTB
#define DISP_CHOOSE_MASK 0x3F
#define DISP_SEGMENT_COUNT 6
 
uint8_t DispCurrSegment, DispCurrSegmentMask;                          			//переменные для хранения для номера текущего индикатора и его маски
uint8_t DispMemory[DISP_SEGMENT_COUNT] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92};  	//непосредственно сама память дисплея
 
//прерывание для перерисовки дисплея
void RedrawDispInterrupt(void)
{
    DispPortChoose |= DISP_CHOOSE_MASK;                                         	//не выбираем ни одного индикатора
    DispPortDatabus = DispMemory[DispCurrSegment];                              	//настраиваем шину данных
    DispPortChoose = (DispPortChoose | DISP_CHOOSE_MASK) & (~DispCurrSegmentMask); //выбираем нужный индикатор
                                             
    //определяем следующий сегмент и его маску
    if (DispCurrSegment < DISP_SEGMENT_COUNT - 1)
    {
        DispCurrSegment++;
        DispCurrSegmentMask = DispCurrSegmentMask << 1;
    }
    else                                                                        	//если переходим на первый сегмент - обнуляем счётчик и меняем маску
    {
        DispCurrSegment = 0; 
        DispCurrSegmentMask = 1;                                                                                                  
    }
}
 
//функция инициализирует дисплей
void DisplayInit()
{
  DispDdrChoose |= DISP_CHOOSE_MASK;                                            //все выводы, относящиеся к дисплею - на выход
    DispPortChoose = 0xFF;                                                      //не выбираем ни одного индикатора
    DispDdrDatabus = 0xFF;
  DispPortDatabus = 0xFF;                                                       //и не выводим ничего на шину данных                                                          
    DispCurrSegment = 0;                                                        //определяем, что текущий элемент - нулевой
    DispCurrSegmentMask = 1;                                                    //соответственно, маска для нулевого элемента 
}
 
//инициализация таймера Т0
void FastTimerInit()
{
    __disable_interrupt();
    TCCR0A = 0;
    TCCR0B = (1 << CS01);                                                       //таймер 0 включён с предделителем 8 в нормальном режиме;
    TIMSK0 |= (1 << TOIE0);                                                     //разрешили прерывание по переполнению Т0
    __enable_interrupt();
}
 
//прерывание таймера Т0
#pragma vector = TIMER0_OVF_vect
__interrupt void FastTimerInterrupt(void)
{
    RedrawDispInterrupt();                                                      //перерисовка дисплея
}
 
void main( void )
{
  DDRB = 0;
  PORTB = 0;
  DDRD = 0;
  PORTD = 0;
  DDRC = 0;
  PORTC = 0;
   
  DisplayInit();
  FastTimerInit();
  while(1)
  {
  }
}

Смотрим, что изменилось, а что нет: функция RedrawDisplay просто поменяла название и стала вызываться в прерывании. Добавились функции для таймера – инициализация (таймер 0 с предделителем 8 и прерыванием по переполнению) и, соответственно, сама функция прерывания.
Так, отлично. Теперь у нас есть возможность делать что-то в основном цикле, и это никак не будет влиять на работу дисплея.
Следующая задача – это писать на дисплее то, что нам хочется.
Есть несколько вариантов реализации: во-первых, можно сделать просто – вызываем функцию, передаём туда код для дисплея, записываем его в массив DispMemory. Но! Работать с кодами очень, очень неудобно – совершенно непонятно, что мы передаём в данный момент.
Гораздо удобней передавать в функцию строку того, что мы хотим отобразить, внутри функции перекодировать эту строку в код и переписывать массив DispMemory. Но тогда нам понадобится хранить «алфавит дисплея» - пары «символ-код».  Алфавит наш, конечно, будет несколько… своеобразным: часть букв будет, естественно, пропущена – не все можно отобразить через семисегментный дисплей – также будут присутствовать цифры, точка, пробел и так далее.
Так, пусть мы определили наш алфавит:

//массив "символ-код"
//по "договорённости" размещаем сначала цифры от 0 до F
#define DISP_SYMBOL_COUNT 37
uint8_t DispSymbolCodeArr[2][DISP_SYMBOL_COUNT] =  {{'0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',  '8',  '9',  'A',  'B',  'C',  'D',  'E',  'F',  ' ', '.',  'T',  'N',   '-',  'S',  'U',  'V',  'L',  'I',  'R',  'O',  'H',  'Y',  'N',  'R',  'P',  'I',  'c',  '<',  '>'},
													 {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E, 0xFF, 0x7F, 0x87, 0xAB, 0xBF, 0x92, 0xC1, 0xC1, 0xC7, 0xF9, 0xAF, 0xA3, 0x89, 0x91, 0xAB, 0x08, 0x8C, 0xFB, 0x46, 0x23, 0x1C}};

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


Рисунок 7. Алгоритм функции записи символов в память дисплея.

Вот код новой функции:

//массив "символ-код"
//по "договорённости" размещаем сначала цифры от 0 до F
#define DISP_SYMBOL_COUNT 37
uint8_t DispSymbolCodeArr[2][DISP_SYMBOL_COUNT] =  {{'0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',  '8',  '9',  'A',  'B',  'C',  'D',  'E',  'F',  ' ', '.',  'T',  'N',   '-',  'S',  'U',  'V',  'L',  'I',  'R',  'O',  'H',  'Y',  'N',  'R',  'P',  'I',  'c',  '<',  '>'},
													 {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E, 0xFF, 0x7F, 0x87, 0xAB, 0xBF, 0x92, 0xC1, 0xC1, 0xC7, 0xF9, 0xAF, 0xA3, 0x89, 0x91, 0xAB, 0x08, 0x8C, 0xFB, 0x46, 0x23, 0x1C}};

const uint8_t DispBlankSymbol = ' ';
const uint8_t DispBlankCodeIndex = 17;
const uint8_t DispPointSymbol = '.';
const uint8_t DispPointCodeIndex = 18;


…

//изменение строки, которая будет записываться на дисплей; строка должна иметь SegmentCount символов минимум или заканчиваться 0
//ключевое слово "__monitor" запрещает прерывания при выполнении функции
__monitor void DisplayShowStr(char* Str)
{
	int8_t currDispMemoryIndex, currSymbolIndex;
  uint8_t* currStrSymbol;
	currDispMemoryIndex = 0;																	
	if (Str)																		//если строка существует
	{
	  currStrSymbol = (uint8_t*) Str;												//определяем текущий символ - первый
		while ((currDispMemoryIndex < DISP_SEGMENT_COUNT) && (*currStrSymbol != 0))	//до конца строки или пока есть непереписанные сегменты
		{
      //ищем текущий символ в таблице соответствий "Символ-код"
      currSymbolIndex = 0;
      while ((*currStrSymbol != DispSymbolCodeArr[0][currSymbolIndex]) && (currSymbolIndex < DISP_SYMBOL_COUNT))
        currSymbolIndex++;
      if (currSymbolIndex < DISP_SYMBOL_COUNT)										//если нашли символ
        DispMemory[currDispMemoryIndex] = DispSymbolCodeArr[1][currSymbolIndex];
      else																			//если не нашли - записываем пробел
        DispMemory[currDispMemoryIndex] = DispSymbolCodeArr[1][DispBlankCodeIndex];
      currDispMemoryIndex++;														//перешли на следующее знакоместо
			currStrSymbol++;														//перешли на следующий записываемый символ
		}
	}
	//если строка оказалась меньше 6 символов - дописываем пробелы
	for (; currDispMemoryIndex < DISP_SEGMENT_COUNT; currDispMemoryIndex++)		//до конца памяти дисплея
		DispMemory[currDispMemoryIndex] = DispSymbolCodeArr[1][DispBlankCodeIndex];	//записываем пробел
}

void main( void )
{
  DDRB = 0;
  PORTB = 0;
  DDRD = 0;
  PORTD = 0;
  DDRC = 0;
  PORTC = 0;
  
  DisplayInit();
  FastTimerInit();
  while(1)
  {
    DisplayShowStr("123456.7");
    __delay_cycles(16000000);
    DisplayShowStr("AB.CD");
    __delay_cycles(16000000);
    
  }
}

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

Вот результат:


Рисунок 8. Отображение строки с отдельно стоящей точкой.

Получилась вот какая проблема: точка стала занимать отдельное место, хотя логичней будет приписать её к предыдущему символу.
Тогда нам добавить в функцию два случая: первый – это когда встречается точка в середине строки – тогда мы дописываем к предыдущему символу код точки (так, код символа «С» без точки – 0хС6 = 11000110; код символа «С» с точкой – 0x46 = 01000110) – иными словами, к предыдущему символу нужно нужно применить побитовое И с кодом точки 0x7F = 01111111.
Второй случай – когда точку нужно добавить в конце строки – то есть когда точка – символ № DISP_SYMBOL_COUNT + 1.

//изменение строки, которая будет записываться на дисплей; строка должна иметь SegmentCount символов минимум или заканчиваться 0
//ключевое слово "__monitor" запрещает прерывания при выполнении функции
__monitor void DisplayShowStr(char* Str)
{
	int8_t currDispMemoryIndex, currSymbolIndex;
  uint8_t* currStrSymbol;
	currDispMemoryIndex = 0;																	
	if (Str)																		//если строка существует
	{
	  currStrSymbol = (uint8_t*) Str;												//определяем текущий символ - первый
		while ((currDispMemoryIndex < DISP_SEGMENT_COUNT) && (*currStrSymbol != 0))	//до конца строки или пока есть непереписанные сегменты
		{
			//если наш символ - точка, и она стоит не в начале строки, то к предыдущему значению приписываем '.'
			if ((*currStrSymbol == DispPointSymbol) && (currStrSymbol != (uint8_t*)Str))
			{
				DispMemory[currDispMemoryIndex - 1] &= DispSymbolCodeArr[1][DispPointCodeIndex];
			}
			else																	//иначе ищем текущий символ в таблице соответствий "Символ-код"
			{
        //ищем текущий символ в таблице соответствий "Символ-код"
        currSymbolIndex = 0;
        while ((*currStrSymbol != DispSymbolCodeArr[0][currSymbolIndex]) && (currSymbolIndex < DISP_SYMBOL_COUNT))
          currSymbolIndex++;
        if (currSymbolIndex < DISP_SYMBOL_COUNT)									//если нашли символ
          DispMemory[currDispMemoryIndex] = DispSymbolCodeArr[1][currSymbolIndex];
        else																		//если не нашли - записываем пробел
          DispMemory[currDispMemoryIndex] = DispSymbolCodeArr[1][DispBlankCodeIndex];
        currDispMemoryIndex++;														//перешли на следующее знакоместо
			}
			currStrSymbol++;														//перешли на следующий записываемый символ
		}
	}
	//свободные сегменты вроде кончились, но вдруг нам нужно записать ещё только точку?
	if((*currStrSymbol == DispPointSymbol) && (currDispMemoryIndex == DISP_SEGMENT_COUNT))
		DispMemory[currDispMemoryIndex - 1] &= DispSymbolCodeArr[1][DispPointCodeIndex];

	//если строка оказалась меньше 6 символов - дописываем пробелы
	for (; currDispMemoryIndex < DISP_SEGMENT_COUNT; currDispMemoryIndex++)			//до конца памяти дисплея
		DispMemory[currDispMemoryIndex] = DispSymbolCodeArr[1][DispBlankCodeIndex];	//записываем пробел
}


Рисунок 9. Отображение строки с точкой, присоединяющейся к предыдущему символу.

Итак, теперь у нас уже есть дисплей, который будет отображать произвольные строки из символов алфавита!
Что нужно ещё для счастья?
Возвращаемся к нашему моделированию – шла речь о том, чтобы при настройке желаемой температуры или таймера значение на дисплее мигало.
Что есть мигание – это попеременное включение и выключение дисплея. Так, за включение-выключение и отображение информации на дисплее отвечает функция, вызываемая в прерывании – её-то и попробуем модифицировать!

Так, прерывание у нас вызывается очень часто – раз в 256 мкс, а мигание должно происходить так, чтобы это было не раздражающе для глаза. Ищем в прерывании место, которое вызывается реже всего:


Рисунок 10. Поиск наиболее редко вызываемого места в прерывании.

Итак, смена адреса текущего индикатора на нулевой (окончание очередной полной отрисовки дисплея) происходит реже всего, а именно:
КОЛИЧЕСТВО_ИНДИКАТОРОВ * ЧАСТОТА_ВЫЗОВА_ПРЕРЫВАНИЯ = 6 * 256 мкс = 1536 мкс ˜ 1.5 мс
Уже неплохо, но все ещё слишком часто для того, чтобы вставить в это место включение-выключение дисплея – при такой частоте мигание будет просто-напросто незаметно, дисплей будет просто тускло светиться.
Что же делать в случаях, когда таймер вызывается слишком часто? Счётчик в счётчике – введем переменную, которая будет увеличиваться каждый раз при попадании в переход на нулевой индикатор. По достижению максимума мы и будем работать с миганием.
Теперь посмотрим, как реализовать это самое мигание, то есть включать-выключать дисплей в нужный момент – и так, чтобы потратить на это минимум времени и изменить минимум кода в основной функции. Пляшем от того, что мы будем редактировать ту часть функции, где происходит переход на нулевой индикатор – именно это и определяет «включенность» дисплея! В этом месте мы задаём маску для текущего индикатора, а дальше весь цикл отображения памяти мы просто эту маску сдвигаем.
А теперь представим, что вместо маски для нулевого индикатора мы не выбираем индикатора вообще – тогда в дальнейшем все сдвиги будут «пустые», и дисплей будет выключен! Вот то, что нам нужно!


Рисунок 11. Алгоритм реализации мигания в прерывании.

Получили следующий код:

//переменные для работы с миганием: флаг включенности режима мигания, текущая фаза мигания (дисплей включён-выключен), счётчик для смены фазы мигания
uint8_t DispBlinkEnabled, DispBlinkCurrMode, dispBlinkModeSwitchCounter;
#define DISP_BLINK_MODE_SWITCH_MAX_COUNTER 255										//длительность мигания дисплея

//прерывание для перерисовки дисплея
void RedrawDispInterrupt(void)
{
  DispPortChoose |= DISP_CHOOSE_MASK;												//не выбираем ни одного индикатора
	DispPortDatabus = DispMemory[DispCurrSegment];									//настраиваем шину данных
	DispPortChoose = (DispPortChoose | DISP_CHOOSE_MASK) & (˜DispCurrSegmentMask);	//выбираем нужный индикатор
											
	//определяем следующий сегмент и его маску
	if (DispCurrSegment < DISP_SEGMENT_COUNT - 1)
	{
		DispCurrSegment++;
		DispCurrSegmentMask = DispCurrSegmentMask << 1;
	}
	else																			//если переходим на первый сегмент - обнуляем счётчик и меняем маску
	{
		DispCurrSegment = 0;
    if(DispBlinkEnabled)															//если включён режим мигания
		{
			dispBlinkModeSwitchCounter++;											//увеличиваем счётчик
			if (dispBlinkModeSwitchCounter == DISP_BLINK_MODE_SWITCH_MAX_COUNTER)	//если выждали время, когда дисплей был включен или выключен - пора интвертироваться
			{
				dispBlinkModeSwitchCounter = 0;
				DispBlinkCurrMode = !DispBlinkCurrMode;								//первый бит будет либо 1 (тогда ниже маска будет равняться первому сегменту), либо 0 - тогда будет пройден цикл без отображения на дисплей
			}
		  DispCurrSegmentMask = DispBlinkCurrMode & 1;
		}
		else
			DispCurrSegmentMask = 1;												//если выключен режим мигания, то маска просто для первого сегмента
	}
}

Отлично!
По сути своей, мы сделали то, что хотели – теперь осталось навести красоту: написать отдельные функции для включения-выключения мигания, также впоследствии мы напишем специальные функции для отображения температуры, времени, различных настроек и т.д.
Затем нужно будет вынести функции и переменные, связанные с дисплеем, в отдельную библиотеку (выделить файл заголовка ‘.h’ и файл ‘.c’).
Итак, вот версия с выделенной отдельно библиотекой дисплея – проект сделан в среде IAR Embedded Workbench IDE.

А вот отдельно прошивка.


Пример работы библиотеки

Как видно, мы перевернули последние два индикатора - это сделано для того, чтобы корректно отображать температуру, а именно точку для градуса Цельсия: "°C"; при этом сегменты подключены к основной шине таким образом, чтобы можно было выводить на них символы без искажений, как и для остальных индикаторов.


Наверх

Автор - Moriam  =ˆˆ=

Обсудить на форуме