Учебный курс. Подключение LCD к микроконтроллеру. Разбиваем программу на модули

02/11/2009 - 21:00

Зачем разбивать программу на модули/файлы

Программа становиться более читабельной
   Чем больше программа, тем сложнее ее контролировать и держать в голове. Разбивая программу на функционально законченные части, мы делаем ее более читабельной и логичной.  Все функции для работы с дисплеем будут сосредоточены в одном месте.

Возможность повторного использования кода
   Если понадобиться использовать lcd дисплей в другом устройстве, нам не придется выдирать куски кода из старого проекта. Мы просто подключим к новому проекту библиотеку lcd_lib.

Возможность раздельной компиляции
   Когда программа становится большой, то увеличивается время ее компиляции. Даже если мы вносим небольшое изменение в код, компилятор будет перелопачивать весь файл от начала до конца. Если программа разбита на модули, можно компилировать только модуль, в котором были сделаны изменения.

Возможность совместной работы над проектом
   Разбиение программы на модули позволяет вести работу над проектом нескольким программистам сразу. Например, один программист делает основную программу, другие занимаются библиотеками.

Область видимости переменных/функций, находящихся в разных модулях

   Глобальные переменные одного модуля будут не видны другому, и наоборот. Чтобы использовать глобальную переменную из другого модуля, перед ней нужно написать ключевое слово extern.

extern unsigned char key;

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

extern unsigned char key = 0;  //неправильно. Вызовет ошибку!

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

static unsigned char
LCD_CheckBF(void)
{
    //тело функции
}

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

Как разбить программу на модули 

Ну ладно, достаточно теории, приступаем.
Открываем наш старый проект.
Создаем в IARe  два файла – lcd_lib.h и lcd_lib.c.
Сохраняем файлы в папке проекта.
Добавляем файл lcd_lib.c в наш проект. (Правой кнопкой мышки кликаем в workspace`e  и в открывшемся меню выбираем  Add > Add files…).
В lcd_lib.c с помощью директивы include подключаем заголовочный файл -

#include “lcd_lib.h”.

В lcd_lib.h добавляем строчки

#ifndef LCD_LIB_H
#define LCD_LIB_H

 #endif

#ifndef и #endif – директивы условной компиляции. Для чего они нужны? Допустим у нас большой проект, и наша библиотека подключается в нескольких местах. Может возникнуть ситуация когда, содержимое lcd_lib.h окажется включенным в какой-нибудь файл многократно. Тогда при компиляции проекта возникнет ошибка.
   Чтобы этого не происходило, возьмите за правило обрамлять содержимое заголовочного файла приведенной выше конструкцией. Когда препроцессор наткнется на эту запись, он проверит, определена ли константа LCD_LIB_H или нет. Если она не определена, он ее определит (#define LCD_LIB_H) и включит содержимое lcd_lib.h  в файл. Ну а если константа определена, то не включит.  

Теперь у нас есть своего рода заготовка и нам нужно ее наполнить содержимым.

lcd_lib.h – это заголовочный файл, интерфейсная часть нашей библиотеки. lcd_lib.c – файл реализации. Чтобы было понятней, приведу пример с телевизором. У него тоже есть интерфейс и реализация. Интерфейс – это кнопки на его корпусе, с помощью которых мы можем его включать, настраивать и выбирать каналы. Реализация – это совокупность плат, компонентов и соединений между ними, которая обеспечивает работу телевизора. Меня, как конечного пользователя,  не интересует, что у него внутри, главное чтобы он выполнял свои функции. Поэтому интерфейсная часть телевизора не должна содержать деталей его реализации. Это же применимо и к заголовочному файлу.

Содержимое заголовочного файла

Подключаемые библиотеки

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

Макроопределения портов

//порт к которому подключена шина данных ЖКД
#define PORT_DATA PORTD
#define PIN_DATA  PIND
#define DDRX_DATA DDRD

//порт к которому подключены управляющие выводы ЖКД
#define PORT_SIG PORTB
#define PIN_SIG  PINB
#define DDRX_SIG DDRB

//Номера выводов к которым подключены управляющие выводы ЖКД

#define RS 5
#define RW 6
#define EN 7

Макроопределение тактовой частоты микроконтроллера

#define F_CPU 8000000

Прототипы функций

void LCD_Init(void);  //инициализация портов и жкд
void LCD_WriteData(unsigned char data);   //выводит байт данных на жкд
void LCD_WriteCom(unsigned char data);   //посылает команду жкд

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

   Заметьте, я переименовал названия всех функций. Это один из стандартов программирования – добавлять к именам функций префикс с названием файла библиотеки. Наша библиотека называется lcd_lib, поэтому к названиям функций я добавил  LCD_. Это простое правило позволяет определять, в каком файле содержится реализация функции.   
   Кстати, в IARe это еще можно сделать так -  в окне редактора кода кликните на названии функции  правой кнопкой мыши  и выберете опцию Go to definition…, откроется файл, содержащий определение этой функции.

Содержимое файла lcd_lib.c

Макросы для работы с битами и макросы для программных задержек

#define ClearBit(reg, bit)       reg &= (~(1<<(bit)))
#define SetBit(reg, bit)           reg |= (1<<(bit))    
#define _delay_us(us)      __delay_cycles((F_CPU / 1000000) * (us));
#define _delay_ms(ms)     __delay_cycles((F_CPU / 1000) * (ms));

Определение всех функций

//функция записи команды
void LCD_WriteCom(unsigned char data)
{
  ClearBit(PORT_SIG, RS);           //установка RS в 0 - команды
  PORT_DATA = data;               //вывод данных на шину индикатора
  SetBit(PORT_SIG, EN);            //установка E в 1
  _delay_us(2);
  ClearBit(PORT_SIG, EN);           //установка E в 0 - записывающий фронт
  _delay_us(40);
}

//функция записи данных
void LCD_WriteData(unsigned char data)
{
  SetBit(PORT_SIG, RS);            //установка RS в 1 - данные
  PORT_DATA = data;               //вывод данных на шину индикатора
  SetBit(PORT_SIG, EN);            //установка E в 1
  _delay_us(2);
  ClearBit(PORT_SIG, EN);          //установка E в 0 - записывающий фронт
  _delay_us(40);
}

//функция инициализации
void LCD_Init(void)
{
  DDRX_DATA = 0xff;
  PORT_DATA = 0xff;    
  DDRX_SIG = 0xff;
  PORT_SIG |= (1<<RW)|(1<<RS)|(1<<EN);
  ClearBit(PORT_SIG, RW);

  _delay_ms(40);
  LCD_WriteCom(0x38); //0b00111000 - 8 разрядная шина, 2 строки
  LCD_WriteCom(0xf);  //0b00001111 - дисплей, курсор, мерцание включены
  LCD_WriteCom(0x1);  //0b00000001 - очистка дисплея
  _delay_ms(2);
  LCD_WriteCom(0x6);  //0b00000110 - курсор движется вправо, сдвига нет
}

Подключаем библиотеку к проекту

В принципе все готово.
Сохраняем оба файла.
Подключаем нашу библиотеку к файлу main.c.
 
#include "lcd_lib.h"

Убираем из него все лишнее. И добавляем вывод слова “Test.”
Вот что должно получиться.

#include <ioavr.h>
#include "lcd_lib.h"

int main( void )
{
  LCD_Init();
  LCD_WriteData('T');
  LCD_WriteData('e');
  LCD_WriteData('s');
  LCD_WriteData('t');
  LCD_WriteData('2');
  while(1);
  return 0;
}

Компилируем проект… У меня все прошло без ошибок. Надеюсь у вас тоже.

 Ну а это структура нашего нового проекта.


   ioavr.h и lcd_lib.h подключены к файлу main.c явно (в тексте программы). Остальные заголовочные файлы попали в main из файла ioavr.h. Можете полазить по нему, чтобы убедиться в этом.
   К lcd_lib.c мы подключали lcd_lib.h,  ioavr.h, intrinsics.h. Остальные подключились через ioavr.h.

  P.S.:
Если вы работаете в WINAVR и ваш проект состоит из нескольких файлов, компилятору нужно указывать их вручную. Делается это в Makefile – просто вписываем название файлов через пробел.

# List C source files here. (C dependencies are automatically generated.)
SRC = $(TARGET).c lcd_lib.c

Файлы

Проект для IARa. Проект для WINAVR.

 

Comments   

# Guest 2010-03-21 15:56
пишу так :
#define port_rs PORTB_PORTB5

соответственно :
port_rs = 0;
port_rs = 1;
( iar avr 5.40 )
# Pashgan 2010-03-21 18:41
ну да, в IARе так можно делать.
# Guest 2010-05-04 17:31
Подскажите начинающему "чайнику" в каком месте нужно объявлять глобальные переменные? Если я пишу перед main(), то их не видит добавленный файл.с и наоборот
пример объявления;
extern volatile unsigned char raz;
# Pashgan 2010-05-06 06:08
Допустим у нас main.c и модуль keyboard.c, keyboard.h. К main`у подключен заголовочный файл keyboard.h, а в файле keyboard.c объявлена переменная keyState. Чтобы эта переменная была доступна из функции main, нужно перед ней объявить эту переменную с ключевым словом extern:
extern unsigned char keyState;
# Guest 2010-06-23 14:39
Всё-таки надо определять не пины чипа, а
номера битов порта вывода. Т.е.
#define RS 4
#define RW 5
#define EN 6

либо править схему. Повнимательней, пожалуйста. Начинающим довольно трудно выцеплять такие баги.
А в целом - молодца!!! Хороший материал.
# BVV 2010-12-03 12:23
Такой "дубовый" вопрос:
Quote:
lcd_lib.h подключен к файлу main.c явно
Но в lcd_lib.h нет упоминания о файле lcd_lib.с. Как компилятор "узнает", что исходный текст в последнем и его надо обрабатывать?
# Pashgan 2010-12-05 16:19
Фишка в чем. Модули программы компилируются независимо друг от друга. Никакой информации о функциях, содержащихся в других модулях, компилятор на момент компиляции не обладает. Программист должен предоставить компилятору эту информацию. Для этого создается заголовочный файл модуля с прототипами функций и другими объявлениями и включается в соответствующие модули.
# Конст 2010-12-10 13:29
BVV прав. Фактически модуль *.c мы подключаем не используя хэдер, а используя интерфейс компилятора.

цитата:
Добавляем файл lcd_lib.c в наш проект. (Правой кнопкой мышки кликаем в workspace`e и в открывшемся меню выбираем Add > Add files…).


А непосредственно в самом хэдере мы о нем ничего не упоминаем. Так и надо? Или все же есть способ, без использования правой кнопки мыши?
# Pashgan 2010-12-12 19:33
Так и надо. В хедере содержаться объявления функций. Они нужны для компилятора. Модули компилируются независимо друг от друга. Если в main используются функции из других модулей, то компилятору нужно предоставить информацию о них. Это делается включением заголовочного файла.
# Конст 2010-12-13 05:17
Это то понятно. Просто человеку, смотрящему на main.c непонятно будет, откуда взялся тот или иной *.c модуль. *.h подключен, прототипы функций есть, а где искать саму реализацию непонятно. (если не смотреть на древовидную структуру слева, в IAR'е). В этом весь вопрос. Почему в хедер не подписывать после протипов функций еще и

Code:
#include "LCD.c"
# N1X 2011-01-06 17:50
Меня, как новичка, интересует тот же вопрос... Хочется таки увидеть ответ на него, посему UP )
# Pashgan 2011-01-08 09:26
Quote:
Просто человеку, смотрящему на main.c непонятно будет, откуда взялся тот или иной *.c модуль. *.h подключен, прототипы функций есть, а где искать саму реализацию непонятно. (если не смотреть на древовидную структуру слева, в IAR'е). В этом весь вопрос. Почему в хедер не подписывать после протипов функций еще и
Включать файл реализации в main.c нет никакого смысла. Это уже не деление на модули. Если вы не первый раз пользуетесь средой разработки, то вы будете знать,где искать файлы реализации. Файлы реализации обычно лежат в каталоге проекта.
# DIMA 2012-03-29 04:08
Пользуюсь AVRSTUDIO_5.1. Осваиваю СИ после Асмы. Так и не понял как к проекту подцепить файл lcd_lib.c. Я вписал в проект #include “lcd_lib.h” этот файл подцепился. А lcd_lib.c нет. Естественно когда вызываю функцию void LCD_Init(void); появляется ошибка, мол что это? Файл lcd_lib.c положил просто в каталог самого проекта. Так как сделать так чтоб этот файл lcd_lib.c, тоже был в проекте?
# Вася 2011-09-10 16:27
Глобальные переменные одного модуля будут не видны другому, и наоборот.
А в Codevision-е видны. Отличный от других?
# Pashgan 2011-09-21 20:42
Видимо он не следует стандарту.
# Виталий 2012-10-18 02:01
Зачем создавать два файла (.c и .h) Почему все функции нельзя прописать только в .h?
# Pashgan 2012-10-18 20:08
Можно, только тогда это не будет разделением программы на модули.
# Валера 2012-11-06 01:49
Спасибо! Все получилось.
# Pashgan 2012-11-06 21:10
Пожалуйста.
# radiolomaster 2013-03-10 20:46
А мы разбиваем на модули только для того чтоб сэкономить время компиляции? Т.е. если сделали изменения в одном модуле то можно только его перекомпилирова ть? А иначе можно просто в .h файл перенести весь код из .c файла, еще и прототипы функций убрать? На результат это не повлияет?
# Pashgan 2013-03-11 06:05
Нет. Основные преимущества, которые дает разбиение на модули это:
- упорядоченная структура программы,
- отделение интерфейса модуля от его реализации,
- возможность повторного использования кода.
Теоретически да, нужно перекомпилирова ть один модуль. Но потом нужно еще собрать весь проект (этим занимается линкер).
Можно перенести, но это равносильно написанию кода в одном файле.
# foxit 2013-05-05 08:06
Pashgan, давно задумываюсь об удобном разбиении программ на модули и ведении базы библиотек.

Ну примерно так
есть интерфейс i2c - для него написана библиотека, которая реализует функции для работы с ним.
есть микросхема DS1307 - для нее написана своя библиотека, которая реализует функции для работы с ней и из нее вызывается библиотека для i2c.
Также можно для spi, 1-wire.
# Pashgan 2013-05-05 17:51
Ну да, я тоже иногда над этим задумываюсь. Просто надо писать больше кода, и тогда такие библиотеки накопятся. Я не так много его пишу.

У вас недостаточно прав для комментирования.