1.Обзор устройства
Люксметр собран на популярном микроконтроллере семейства AVR — ATmega8A. В качестве чувствительного элемента применен цифровой I2C измеритель освещенности — микросхема BH1750 от ROHM Semiconductor с шестнадцатиразрадным выводом результата измерения. В последствии, потому что в местных магазинах я не смог отдельно купить микросхему BH1750 (на Ali) был куплен готовый модуль GY-302, который представляет собой небольшую платку с микросхемой BH1750 и стабилизатором напряжения на 3.3В для питания данной микросхемы. Для вывода результатов был взят символьный жидкокристаллический дисплей WH1602.
Принципиальная схема:
Характеристики:
- Напряжение питания 5В
- Потребляемый ток 150…300мА
- Оптический диапазон 450…670нм (по уровню -3дБ)
- Пределы измерения 0…64000Лк
Печатная плата:
Печатная плата для устройства нарисована в ПО Sprint-Layout 5. Внешний вид со стороны деталей (для ЛУТ зеркалить не нужно).
2.Прошивка
Для начала рассмотрим работу с дисплеем WH1602
Данный дисплей имеет 2 режима работы, первый использует все выводы порта данных (D0..D7) и общается с дисплеем по 8 битной шине, второй режим позволяет работать с дисплеем по 4 битной шине (D4..D7), что позволяет использовать меньшее число выводов контроллера. Управление дисплеем осуществляется посредством 8 битных команд, поэтому в первом режиме команда передается за 1 такт, во втором режиме команда делится на 2 полубайта, которые отправляются по очереди, причем сначала отправляется старший полубайт, а далее младший.
Вывод RW определяет направление данных . Высокий уровень — чтение данных дисплея, низкий — запись данных. Так как мы будем лишь отправлять дисплею данные, то этот вывод можно сразу подтянуть на землю.
Вывод RS определяет, что в данный момент будет передаваться по шине данных, если на данный вывод подать высокий уровень, то дисплей будет ожидать передачу символа, а если низкий, то — передачу команды.
Вывод E (Enable) служит для стробирования дисплея. В момент поступления импульса на данный вход дисплей, в зависимости от состояния вывода RS либо считывает символ, либо команду.
Основные команды:
0b001XYZ00 — выбор режима, числа строк, размера символа
X: 1 — 8бит, 0 — 4бит
Y: 1 — две строки, 0 — одна строка
Z: 1 — большие символы, 0 — маленькие
0b000XY00 — Режим сдвига
X: 1 — сдвиг экрана, 0 — сдвиг курсора
Y: 1 — сдвиг вправо, 0 — сдвиг влево
0b000001XY — Настройки сдвига
X: 1 — инкремент символов, 0 — декремент
Y: 1 — сдвиг окна экрана, 0 — экран неподвижен
0b00001XYZ — режим отображения
X: 1 — дисплей вкл , 0 — дисплей выкл
Y: 1 — курсор вкл , 0 — курсор выкл
Z: 1 — курсор в виде квадрата вкл , 0 — курсор в виде квадрата выкл
0b00000001 — очистка дисплея + сброс сдвигов
0b00000010 — сброс сдвигов
0b1XXXXXXX — установка курсора
XXXXXXX — адрес ячейки памяти
Адреса первой строки лежат в районе 0x00…0x27, а второй 0x40…0x67. Иными словами число ячеек памяти под строку значительно больше числа символов в строке, если дисплей не движется, то для работы нам будет достаточно адресов 0x00..0x0F для первой строки и 0x40..0x4F для второй. Таким образом если я хочу выводить текст во вторую (счет начинается с нуля) ячейку второй строки, то ее адрес будет 0x42, а соответствующий код команды 0b11000010, что равно 0xC2. Иными словами что бы переключится к i-му символу строки необходимо послать команду с кодом:
0x80+X+i , где X=0x00 для первой строки, 0x40 — для второй.
Процедура инициализации дисплея:
1) 3 команды запуска в 8ми битном режиме (start)
2) Выбор режима, числа строк, размера символов
3) Включение дисплея, настройка курсора
4) Очистка дисплея
5) Настройка сдвига
Так как свободных выводов МК в нашем случае достаточно, то мы будем работать с дисплеем в 8 битном режиме, что несколько проще, нежели в 4 битном.
Введем некоторые константы:
#define LCD_START 0b00110000
#define LCD_8BIT_2LINE 0b00111000
#define LCD_DISPLAY_ON 0b00001100
#define LCD_DISPLAY_OFF 0b00001000
#define LCD_INCREMENT_ON_CURSOR_OFF 0b00000110
#define LCD_CLEAR 0b00000001
#define LCD_SECOND_LINE 0b11000000
#define LCD_FIRST_LINE 0b10000000
#define LCD_DATA_PORT PORTD
#define LCD_DATA_DDR DDRD
#define LCD_CTRL_PORT PORTB
#define LCD_CTRL_DDR DDRB
#define LCD_EN (1<<7)
#define LCD_RS (1<<6)
Отправка команды:
void LCDcomand(char cmd){
LCD_CTRL_PORT|=LCD_EN;// поднимаем строб
LCD_DATA_PORT=cmd; // подаем команду
_delay_us(5); // ждем, что бы дисплей успел среагировать
LCD_CTRL_PORT&=~LCD_EN;//опускаем строб
_delay_ms(1);
}
Отправка символа:
void LCDprint(char symbol){
LCD_CTRL_PORT|=LCD_EN|LCD_RS // поднимаем строб, ставим режим символа (EN=1,RS=1)
LCD_DATA_PORT=symbol; // подаем символ
_delay_us(5); // ждем, что бы дисплей успел среагировать
LCD_CTRL_PORT&=~(LCD_EN|LCD_RS);//опускаем строб, отключаем режим символа (EN=0,RS=0)
_delay_ms(1);
}
Инициализация дисплея:
void LCDinit(){
_delay_ms(40);
LCDcomand(LCD_START);
_delay_ms(5);
LCDcomand(LCD_START);
_delay_us(120);
LCDcomand(LCD_START);
_delay_ms(1);
LCDcomand(LCD_8BIT_2LINE);
_delay_ms(5);
LCDcomand(LCD_DISPLAY_ON);
_delay_ms(5);
LCDcomand(LCD_CLEAR);
_delay_ms(5);
LCDcomand(LCD_INCREMENT_ON_CURSOR_OFF);
}
Используя функцию отправки символа сформируем функцию вывода строки:
void LCDprintLine(char* line,char count){
_delay_ms(5);
for(int i = 0;i<count;i++) LCDprint(line[i]);
}
Шина I2C:
Так как микросхема BH1750 работает по интерфейсу I2C, то разберем работу этого протокола. Название I2C запатентовано, поэтому компания Atmel в своих микроконтроллерах (к коим относится и Atmega8) использует другое имя для этого же протокола — TWI (Two wire interface). Данное название, к слову как нельзя лучше характеризует данный протокол. Он подразумевает, что два или более устройств общаются посредством 2-хпроводной шины, по первой линии осуществляется подача тактового сигнала, данную линию называют линией тактирования (SCL), по второй линии передаются данный (SDA). Согласно стандарта обе линии должны быть подтянуты резисторами (10к) к шине +5В. Следует отметить, что данный интерфейс подразумевает наличие ведущего (master) и ведомого (slave) устройств. Причем одно и то же устройство может быть ведущим и ведомым в разные моменты времени.
События:
Старт — падающий фронт на линии SDA при высоком уровне на линии SCL (наличие тактового импульса)
Стоп — возрастающий фронт на линии SDA при высоком уровне на линии SCL
Отправка данных:
1) Мастер генерирует событие «Старт»
2) Мастер отправляет первый пакет, в котором первые 7бит — адрес ведомого, а восьмой — режим (0 — запись)
3) Ведомый отправляет бит подтверждения (NACK)
4) Мастер отправляет байт данных
5) Ведомый отправляет бит подтверждения (NACK)
6) Пункты 4-5 повторяются, пока все данные не будут переданы.
7) Мастер генерирует событие «Стоп»
Чтение данных:
1) Мастер генерирует событие «Старт»
2) Мастер отправляет первый пакет, в котором первые 7бит — адрес ведомого, а восьмой — режим (1 — чтение)
3) Ведомый отправляет бит подтверждения (NACK)
4) Ведомый отправляет байт данных
5) Мастер отправляет бит подтверждения (NACK)
6) Пункты 4-5 повторяются, пока все данные не будут переданы.
7) Мастер генерирует событие «Стоп»
Между несколькими процедурами общения событие «Стоп» может быть упущено, вместо него может следовать повторное событие «Старт»
В микроконтроллере Atmega8 за работу TWI отвечают следующие регистры:
TWBR — регистр скорости передачи, его значение задает скорость работы шины (частоту тактирования) и определяется формулой:
(F_CPU/F_I2C-16) / (2*4^TWPS)
где TWPS — предделитель частоты SCL, два младших бита регистра TWSR, если выбрать эти биты равные нулю (предделитель отключен), то выражение упростится до вида:
(F_CPU/F_I2C-16) / 2
TWDR — регистр данных, сюда записывают данные для передачи, сюда же попадают принятые данные.
TWAR — регистр адреса, сюда записывают адрес микроконтроллера, когда он выполняет роль ведомого устройства.
TWSR — регистр статуса. Старше 5 бит этого регистра определяют код состояния шины TWI, а младшие 2 бита являются предделителем частоты.
TWCR — регистр управления. Описание битов данного регистра приведено в таблице.
бит
название
описание
0
TWIE
Разрешение прерывания
1
—
—
2
TWEN
Включение модуля TWI
3
TWWC
Флаг конфликта записи
4
TWSTO
Флаг состояния стоп
5
TWSTA
Флаг состояния старт
6
TWEA
Разрешение бита подтверждения
7
TWINT
Флаг прерывания
Особое внимание следует уделить биту TWINT. Этот бит автоматически устанавливается в 1 при завершении любой операции выполняемой модулем TWI и не сбрасывается в 0 автоматически, сброс данного бита осуществляется программно путем записи в него единицы, а до момента его сброса модуль не работает и удерживает низкий уровень на линии SCL.
Итак вооружившись теорией перейдем к практике, для начала объявим константы:
#define F_CPU 2000000UL
#define F_I2C 12500UL
#define TWBR_V (((F_CPU)/(F_I2C)-16)/2)
Инициализируем модуль:
void I2C_Init (void){
TWBR=TWBR_V; // задаем скорость передачи
TWSR = 0; // сбрасываем регистр состояния
}
Функции генерации событий работают следующим образом:
1) в регистре TWCR устанавливаются флаг соответствующего события ( TWSTA — start, TWSTO — stop)
2) включается модуль TWI (TWEN=1)
3) сбрасывается флаг прерывания (в TWINT записывается единица)
4) в случае генерации события «старт» дожидаемся завершения операции (установка флага прерывания TWINT)
void I2C_Start(void){// отправка СТАРТ
TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN);
while (!(TWCR & (1<<TWINT)));
}
void I2C_Stop(void){// отправка СТОП
TWCR = (1<<TWINT)|(1<<TWSTO)|(1<<TWEN);
}
Все хорошо осталось ещё чуть-чуть — передавать и принимать данные.
Начнем с передачи байта, данная процедура выглядит примерно следующим образом:
1) Записываем данные в регистр данных (TWDR)
2) Включаем модуль (TWEN=1)
3) Сбрасываем флаг прерывания (в TWINT записывается единица)
4) Ждем завершения передачи (установки флага прерывания TWINT)
void I2C_SendByte(unsigned char c){
TWDR = c;
TWCR = (1<<TWINT)|(1<<TWEN);
while (!(TWCR & (1<<TWINT)));
}
Теперь получим ответ, данная процедура не чуть не сложнее
1) Включаем модуль (TWEN=1)
2) Включаем отправку NACK (TWEA=1)
3) Сбрасываем флаг прерывания (в TWINT записывается единица)
4) Ждем завершения передачи (установки флага прерывания TWINT)
5) Получаем результат из регистра данных (TWDR)
char I2C_GetByte(){
TWCR = (1<<TWINT)|(1<<TWEA)|(1<<TWEN);
while (!(TWCR & (1<<TWINT)));
return TWDR
}
Обьеденим все рассмотренное выше и реализуем работу нашего устройства:
#define F_CPU 2000000UL //тактовая частота
//——КОНСТАНТЫ ДЛЯ LCD
#define LCD_START 0b00110000
#define LCD_8BIT_2LINE 0b00111000
#define LCD_DISPLAY_ON 0b00001100
#define LCD_DISPLAY_OFF 0b00001000
#define LCD_INCREMENT_ON_CURSOR_OFF 0b00000110
#define LCD_CLEAR 0b00000001
#define LCD_SECOND_LINE 0b11000000
#define LCD_FIRST_LINE 0b10000000
#define LCD_DATA_PORT PORTD
#define LCD_DATA_DDR DDRD
#define LCD_CTRL_PORT PORTB
#define LCD_CTRL_DDR DDRB
#define LCD_EN (1<<7)
#define LCD_RS (1<<6)
//——КОНСТАНТЫ ДЛЯ TWI
#define SLA_R 0b01000111 //адрес устройства + бит RW в режим чтения(1)
#define SLA_W 0b01000110 //адрес устройства + бит RW в режим запись(0)
#define F_I2C 12500UL //частота шины i2c
#define TWBR_V (((F_CPU)/(F_I2C)-16)/2)
#include <avr/io.h>
#include <util/delay.h>
//————————————————————-i2c
// отправка СТАРТ
void I2C_Start(void){
TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN);
while (!(TWCR & (1<<TWINT)));
}
// отправка СТОП
void I2C_Stop(void){
TWCR = (1<<TWINT)|(1<<TWSTO)|(1<<TWEN);
}
//отправка байта
void I2C_SendByte(unsigned char c){
TWDR = c;
TWCR = (1<<TWINT)|(1<<TWEN);
while (!(TWCR & (1<<TWINT)));
}
//получение байта
char I2C_GetByte(){
TWCR = (1<<TWINT)|(1<<TWEA)|(1<<TWEN);
while (!(TWCR & (1<<TWINT)));
return TWDR
}
//инициализация I2C как передатчика
void I2C_Init (void){
TWBR=TWBR_V;
TWSR = 0;
}
//——————————————————————————LCD
//отправка команды
void LCDcomand(char cmd){
LCD_CTRL_PORT|=LCD_EN // поднимаем строб (EN=1)
LCD_DATA_PORT=cmd; // подаем команду
_delay_us(20); // ждем, что бы дисплей успел среагировать
LCD_CTRL_PORT&=~LCD_EN;//опускаем строб
_delay_ms(4);
}
//отправка символа
void LCDprint(char symbol){
LCD_CTRL_PORT|=LCD_EN|LCD_RS // поднимаем строб, ставим режим символа (EN=1,RS=1)
LCD_DATA_PORT=symbol; // подаем символ
_delay_us(20); // ждем
LCD_CTRL_PORT&=~(LCD_EN|LCD_RS);//опускаем строб, отключаем режим символа (EN=0,RS=0)
_delay_ms(4);
}
//инициализация дисплея
void LCDinit(){
_delay_ms(160);
LCDcomand(LCD_START);
_delay_ms(20);
LCDcomand(LCD_START);
_delay_us(480);
LCDcomand(LCD_START);
_delay_ms(4);
LCDcomand(LCD_8BIT_2LINE);
_delay_ms(20);
LCDcomand(LCD_DISPLAY_ON);
_delay_ms(20);
LCDcomand(LCD_CLEAR);
_delay_ms(20);
LCDcomand(LCD_INCREMENT_ON_CURSOR_OFF);
}
//вывод строки
void LCDprintLine(char* line,char size){
_delay_ms(20);
for(int i = 0;i<size;i++) LCDprint(line[i]);
}
//инициализация портов
void port_init(){
LCD_CTRL_DDR=0xFF; // порт на выход
LCD_DATA_DDR=0xFF; // порт на выход
}
int main(void){
char h_byte,l_byte;//старший и младший байт результата измерения
I2C_Init(); //инициализируем i2с
port_init();//настраиваем порты
LCDinit(); //инициализация дисплея
LCDprintLine(» BESTSCHEMES.RU»,12); //вывод приветствия
LCDcomand(LCD_SECOND_LINE); //переход на вторую линию
LCDprintLine(«LUX METR»,8); //вывод текста на второй линии
_delay_ms(5000); //задержка 5 секунд
LCDcomand(LCD_CLEAR);//очистка дисплея
LCDprintLine(«LIGHT, lux:»,11); //вывод первой строки
while(1){
//———————-Получение освещенности от датчика
I2C_Start();//запуск шины i2c
I2C_SendByte(SLA_W);//отправляем адресс устройства + флаг «запись»
I2C_SendByte(0b00010000);//отправляем команду «начать измерение»
I2C_Stop();//останавливаем шину i2c
_delay_ms(250);//ждем пока устройство померяет (время взято из даташита + пару милисекунд)
I2C_Start();//запуск шины i2c
I2C_SendByte(SLA_R);//отправляем адресс устройства + флаг «чтение»
h_byte = I2C_GetByte();//читаем старший байт результата
l_byte=I2C_GetByte();//читаем младший байт результата
I2C_Stop();//останавливаем шину i2c
//——————-Пересчитываем результат
unsigned int a=((h_byte<<8)|l_byte)/1.2; //формула из даташита на BH1750
//——переводим число в строку
char I[16]=» «;//строка для вывода
for(char i=0;i<7;i++){
I[15-i]=a%10+48;//48-смещиние цыфры е ее коду
a=a/10;
}
//———————-Вывод данных
LCDcomand(LCD_SECOND_LINE);//переводим дисплей на вторую линию
LCDprintLine(I,16);//выводим строку с результатом
_delay_ms(1000);//пауза 1 секунда
}
}
В приложении к статье находится печатная плата и hex-файл прошивки, при прошивке контроллера фьюзы оставить дефолтные
Прикрепленные файлы:
- Архив.zip (12 Кб)