Учебный курс. Семисегментный индикатор. Динамическая индикация

   Введение 

Для отображения цифровой информации в системах на базе микроконтроллеров  используются светодиодные семисегментные индикаторы. Они просты в управлении, имеет высокую яркость, широкий диапазон рабочих температур и низкую стоимость. К недостаткам светодиодных индикаторов относятся – высокое энергопотребление,  отсутствие управляющего контроллера и скудные возможности по выводу буквенной информации.
  Светодиодный семисегментный индикатор представляет собой группу светодиодов   расположенных в определенном порядке и объединенных конструктивно. Зажигая одновременно несколько светодиодов можно формировать на индикаторе символы цифр. Индикаторы различаются по типу соединения светодиодов – общий анод, общий катод, по количеству отображаемых разрядов – однораразрядные, двух разрядные и т.д. и по цвету –  красные, зеленые, желтые и т.д.



    Семисегментным индикатором можно управлять статически или динамически. При статическом управлении разряды индикатора подключены к микроконтроллеру независимо друг от друга и информация на них выводится постоянно. Этот способ управления проще динамического, но без использования дополнительных элементов, как-то сдвиговые регистры, подключить многоразрядный семисегментный индикатор к микроконтроллеру будет проблематично - может не хватить выводов.



     Динамическое управление (динамическая индикация) подразумевает поочередное зажигание разрядов индикатора с частотой, не воспринимаемой человеческим глазом. Схема подключения индикатора в этом случае на порядок экономичнее благодаря тому, что одинаковые сегменты разрядов индикатора объединены.


Эксперименты с семисегментным индикатором

   Рассмотрим простейший случай управления индикатором – вывод одной цифры.  Схема для наших экспериментов приведена ниже. 

  Чтобы зажечь на индикаторе какую-то цифру нужно настроить порты, к которым подключен индикатор, в режим выхода, “открыть” транзистор (в данном случае подать на базу “единицу”) и установить в порту микроконтроллера её код.
  В зависимости от того, куда подключены сегменты индикатора – коды могут быть разные. Для нашего случая коды цифр будут выглядеть так.

unsigned char number[] =
{
  0x3f, //0
  0x06, //1
  0x5b, //2
  0x4f, //3  
  0x66, //4
  0x6d, //5
  0x7d, //6
  0x07, //7  
  0x7f, //8
  0x6f  //9   
};

   Используя десятичные цифры от 0 до 9 в качестве индекса массива, легко выводить в порт нужные коды.

Пример 1. Вывод цифр от 0 до 9

#include <ioavr.h>
#include <intrinsics.h>

unsigned char number[] =
{
  0x3f, //0
  0x06, //1
  0x5b, //2
  0x4f, //3  
  0x66, //4
  0x6d, //5
  0x7d, //6
  0x07, //7  
  0x7f, //8
  0x6f  //9   
};

unsigned char count = 0;
int main( void )
{
  //порт, к которому подкл. сегменты
  PORTB = 0xff;
  DDRB = 0xff;
 
  //вывод, к которому подкл. катод
  PORTD |= (1<<0);
  DDRD |= (1<<0);
 
  while(1){
    PORTB = number[count];
    count++;
    if (count == 10) count = 0;
    __delay_cycles(8000000);   
  }
  return 0;
}

Эта программа каждую секунду выводит значение переменной count на семисегментный индикатор. Индикация в данном случае - статическая.
 
   Идем дальше – вывод двух цифр. Настройка портов и коды цифр остаются без изменений, но теперь нам нужно добавить кусок кода, который будет зажигать разряды индикатора по очереди. Частота смены разрядов должна быть достаточно высокой, чтобы свечение индикатора воспринималось без мерцания.

Пример2. Вывод цифр от 0 до 99

#include <ioavr.h>
#include <intrinsics.h>

unsigned char number[] =
{
  0x3f, //0
  0x06, //1
  0x5b, //2
  0x4f, //3  
  0x66, //4
  0x6d, //5
  0x7d, //6
  0x07, //7  
  0x7f, //8
  0x6f  //9   
};

unsigned char
count = 0;

//числа для вывода на индикатор
unsigned char data1 = 2;
unsigned char data2 = 5;

int main( void )
{
  //порт, к которому подкл. сегменты
  PORTB = 0xff;
  DDRB = 0xff;
 
  //порт, к которому подкл. катод
  PORTD = 0;
  DDRD = (1<<1)|(1<<0);
 
  while(1){

    //гасим оба разряда
    PORTD &= ~((1<<1)| (1<<0));

    //выводим в порт код цифры
    //и зажигаем следующий разряд

    if (count == 0) {
      PORTB = number[data2];
      PORTD |= (1<<1);
    }
    if (count == 1) {
      PORTB = number[data1];
      PORTD |= (1<<0);
    }
   
    count++;
    if (count == 2) count = 0;

    //частота смены разрядов будет 100 Гц при кварце 8МГц

    __delay_cycles(800000);   
  } 
  return 0;
}

   Эта программа просто выводит любое поразрядно заданное число от 0 до 99.
   Частота смены разрядов семисегментного индикатора задается с помощью программной задержки __delay_cycles(). Это не самое удачное решение, потому что добавление каких-нибудь других задач в цикл while будет мешать выводу на индикатор. Давайте организуем смену разрядов индикатора с помощью аппаратного таймера/счетчика Т0

Пример3. Вывод цифр от 0 до 99. Смена разрядов выполняется в прерывании таймера

#include <ioavr.h>
#include <intrinsics.h>

unsigned char number[] =
{
  0x3f, //0
  0x06, //1
  0x5b, //2
  0x4f, //3  
  0x66, //4
  0x6d, //5
  0x7d, //6
  0x07, //7  
  0x7f, //8
  0x6f  //9   
};

//числа для вывода на индикатор
volatile unsigned char data1 = 0;
volatile unsigned char data2 = 0;

int main( void )
{
  //порт, к которому подкл. сегменты
  PORTB = 0xff;
  DDRB = 0xff;
 
  //порт, к которому подкл. катод
  PORTD = 0;
  DDRD |= (1<<1)|(1<<0);
 
  //инициализация таймера Т0
  //частота прерываний 100Гц при кварце 8МГц

  TIMSK = (1<<TOIE0);
  TCCR0 = (1<<CS02)|(0<<CS01)|(1<<CS00);
  TCNT0 =  0xb2;
   
  __enable_interrupt();
  while(1){
    //программный счетчик секунд
    data1++;
    if (data1 == 10) {
        data1 = 0;
        data2++;
        if (data2 == 10) data2 = 0;
    }
    __delay_cycles(8000000);   
  } 
  return 0;
}

//прерывания таймера Т0 - вывод на индикатор
#pragma vector = TIMER0_OVF_vect
__interrupt void Timer0_Ovf(void)
{
  static unsigned char count = 0;
  TCNT0 = 0xb2;
 
   //гасим оба разряда
   PORTD &= ~((1<<1)|(1<<0));

    //выводим в порт код цифры
    //и зажигаем следующий разряд

   if (count == 0) {
      PORTB = number[data2];
      PORTD |= (1<<1);
   }
   if (count == 1) {
      PORTB = number[data1];
      PORTD |= (1<<0);
   }
   
    count++;
    if (count == 2) count = 0;
}

   Переменные data1, data2  объявлены с ключевым словом volatile, потому что они используются и в основном коде и в прерывании.  В проекте  под GCC я забыл поставить его поставить, и компилятор выкинул обе переменные, посчитав их ненужными!

   Прерывания таймера происходят параллельно выполнению цикла while. Это позволяет выполнять в цикле какую-нибудь полезную задачу. В данном примере с помощью двух переменных  в цикле организован программный счетчик от 0 до 99.
   Использовать две восьмиразрядные переменные для организации счетчика от 0 до 99 неудобно и расточительно, ведь такой счетчик можно сделать и на одной переменной типа unsigned char. Хорошо, счетчик мы сделаем, а как вывести его значение на семисегментный индикатор? Нужен код “разбивающий” десятичное число на отдельные разряды и вот как он выглядит:

    //программный счетчик
    unsigned char counterProg = 35;
   
    //”разбиваем” значение счетчика на отдельные разряды
    data1 = counterProg % 10;
    data2 = counterProg/10;

data1 = counterProg % 10 – это операция деления по модулю 10 (деление с остатком). Результатом этого выражения будет остаток от деления переменной counterProg на 10, то есть для нашего случая  5.

counterProg/10 – это целочисленное деление на 10. Результатом этого выражения будет число 3.

Таким образом, в переменные data2, data1 будут записаны числа 3 и 5 соответственно, значение счетчика counterProg при этом не изменится.

Пример 4. Вывод цифр от 0 до 99.
Преобразование двоичных чисел в двоично-десятичные (BCD)


#include <ioavr.h>
#include <intrinsics.h>

unsigned char number[] =
{
  0x3f, //0
  0x06, //1
  0x5b, //2
  0x4f, //3  
  0x66, //4
  0x6d, //5
  0x7d, //6
  0x07, //7  
  0x7f, //8
  0x6f  //9   
};

//числа для вывода на индикатор
volatile  unsigned char data1 = 0;
volatile  unsigned char data2 = 0;

//программный счетчик секунд
unsigned char counterProg = 0;

int main( void )
{
  //порт, к которому подкл. сегменты
  PORTB = 0xff;
  DDRB = 0xff;
 
  //порт, к которому подкл. катод
  PORTD = 0;
  DDRD |= (1<<1)|(1<<0);
 
  //инициализация таймера Т0
  TIMSK = (1<<TOIE0);
  TCCR0 = (1<<CS02)|(0<<CS01)|(1<<CS00);
  TCNT0 =  0xb2;
   
  __enable_interrupt();
  while(1){
    //программный счетчик секунд
    counterProg++;
    if (counterProg == 100) counterProg = 0;
    data1 = counterProg % 10;
    data2 = counterProg/10;
    __delay_cycles(8000000);   
  } 
  return 0;
}

//прерывания таймера Т0 - вывод на индикатор
#pragma vector = TIMER0_OVF_vect
__interrupt void Timer0_Ovf(void)
{
  static unsigned char count = 0;
  TCNT0 = 0xb2;
 
   //гасим оба разряда
   PORTD &= ~((1<<1)|(1<<0));
  
   //выводим код цифры в порт
   //и зажигаем следующий разряд

   if (count == 0) {
      PORTB = number[data2];
      PORTD |= (1<<1);
   }
   if (count == 1) {
      PORTB = number[data1];
      PORTD |= (1<<0);
   }
   
    count++;
    if (count == 2) count = 0;
}

   Следующий этап работы над программой – выделение кода обслуживающего светодиодный семисегментный индикатор в отдельные функции. Какой минимальный набор функций нам необходим? Функция инициализации, функция вывода на индикатор и функция преобразования чисел и записи их в буфер.

Функция инициализации

#define PORT_IND PORTB
#define DDR_IND DDRB
#define PORT_K PORTD
#define DDR_K DDRD
#define KAT1 0
#define KAT2 1

volatile  unsigned char data[2];

void IND_Init(void)
{
  //порт к которому подкл. сегменты
  PORT_IND = 0xff;
  DDR_IND = 0xff;
 
  //порт, к которому подкл. катоды
  PORT_K &= ~((1<<KAT2)|(1<<KAT1));
  DDR_K |= (1<<KAT2)|(1<<KAT1);

   //очистка буфера
   data[0] = 0;
   data[1] = 0;
}

Порты, к которым подключен семисегментный индикатор, определены с помощью директивы #define – в будущем это позволит быстро править код. Вместо двух переменных data1, data2 удобнее использовать массив unsigned char data[2].

Функция преобразования


void IND_Conv(unsigned char value)
{
  unsigned char tmp;
  tmp = value % 10;
  data[0] =  number[tmp];
  tmp = value/10;
  data[1] =  number[tmp];
}

Процедура преобразования чисел аналогична описанной выше. Единственное отличие – в буфере (data[]) мы теперь сохраняем не результат преобразования, а коды цифр. Зачем делать в прерывании то, что можно сделать в основном цикле программы?

Функция вывода на индикатор

void IND_Update(void)
{
   static unsigned char count = 0;
  
   //гасим оба разряда
   PORT_K &= ~((1<<KAT2)|(1<<KAT1));

   //выводим в порт код цифры
   PORT_IND = data[count];  
  
   //зажигаем нужный разряд
   if (count == 0) PORT_K |= (1<<KAT1);
   if (count == 1) PORT_K |= (1<<KAT2);
     
    count++;
    if (count == 2) count = 0;
}

Эта функция будет вызываться в прерывании таймера. В принципе, для экономии ресурсов ее можно было бы сделать встраиваемой.

Пример 5. Код программы с использованием этих функций
//программный счетчик секунд
unsigned char counterProg = 0;

int main( void )
{
  IND_Init();
 
  //инициализация таймера Т0
  TIMSK = (1<<TOIE0);
  TCCR0 = (1<<CS02)|(0<<CS01)|(1<<CS00);
  TCNT0 =  0xb2;
   
  __enable_interrupt();
  while(1){
    counterProg++;
    if (counterProg == 100) counterProg = 0;
    IND_Conv(counterProg);
    __delay_cycles(8000000);   
  } 
  return 0;
}

//прерывания таймера Т0 – вывод на индикатор
#pragma vector = TIMER0_OVF_vect
__interrupt void Timer0_Ovf(void)
{
  TCNT0 = 0xb2;
  IND_Update();
}

И, наконец – разбиение программы на модули.

Создаем два файла – indicator.h и indicator.c. Сохраняем их в папке проекта.
В хидер файле у нас будут директивы условной компиляции, макроопределения и прототипы функций.

#ifndef INDICATOR_H
#define INDICATOR_H

#include <ioavr.h>

#define PORT_IND PORTB
#define DDR_IND DDRB
#define PORT_K PORTD
#define DDR_K DDRD
#define KAT1 0
#define KAT2 1

void
IND_Init(void);
void IND_Conv(unsigned char value);
void IND_Update(void);

#endif //INDICATOR_H

В сишном файле – #include “indicator.h”,  объявления переменных  и реализация трех функций. Функции описаны выше, поэтому здесь их не привожу.

Подключаем файл indicator.c к проекту.
В файл main.c инклюдим заголовочный файл нашего драйвера семисегментного индикатора  - #include “indicator.h”

Все. Теперь мы можем использовать функции для работы с индикатором в файле main.
Полный код драйвера смотрите в проектах.

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

Файлы

Семисегментный индикатор. Проекты для IARа
Семисегментный индикатор. Проекты для WinAvr
Семисегментный индикатор. Проекты для CodeVision
Отладочный проект для Proteus`a

Добавить комментарий

При добавлении в комментарий Си кода, заключайте его между тегами [code] [/code]. Иначе он будет отображаться некорректно.


Защитный код
Обновить