Проекты для микроконтроллеров / ШИМ-регулятор на Atmega с сохранением настроек скорости в EEPROM

ШИМ-регулятор на Atmega с сохранением настроек скорости в EEPROM

Микроконтроллер Atmega8
Поделится:

Введение

Недавно у меня возникла необходимость в создании ШИМ-регулятора с точной настройкой скорости. Обычно такие регуляторы управляются с помощью переменного резистора, который изменяет аналоговый сигнал. Однако этот подход оказался недостаточно надёжным. Переменные резисторы подвержены воздействию множества внешних факторов: температура, влажность, износ со временем — всё это может влиять на точность регулировки и стабильность работы устройства. Это привело меня к мысли, что нужно искать альтернативное решение.

Я решил, что лучше реализовать управление скоростью с помощью кнопок. Такой подход сразу решает несколько проблем. Во-первых, кнопки обеспечивают точную цифровую настройку, что исключает ошибки и неточности, присущие аналоговому управлению. Во-вторых, кнопочное управление не зависит от внешних условий и имеет гораздо большую долговечность. Это особенно важно для устройств, которые должны работать стабильно и предсказуемо на протяжении длительного времени. Дополнительно, настройки скорости, установленные с помощью кнопок, могут сохраняться в энергонезависимой памяти (EEPROM). Это означает, что они остаются неизменными даже после отключения питания, что значительно повышает удобство и надёжность работы устройства.

Преимущества

Основным преимуществом данного ШИМ-генератора по сравнению с другими является то, что изменение скорости происходит не через аналоговый вход, а с помощью физического нажатия кнопок. Это не только обеспечивает более точную и стабильную настройку, но и позволяет сохранять результаты в энергонезависимой памяти (EEPROM). Благодаря этому настройка скорости сохраняется даже после отключения питания, что делает устройство более удобным и надёжным в использовании.

Схема устройства

Схема ШИМ-регулятора на Atmega с сохранением настроек скорости в EEPROM

Назначение пинов микроконтроллера

Назначение пинов микроконтроллера Atmega 8

Описание работы схемы

Кнопки управления

К выходам PC0 и PC1 подключаются кнопки, которые используются для управления скоростью. Кнопка, подключённая к PC0, отвечает за увеличение скорости, а кнопка, подключённая к PC1, соответственно, за её уменьшение.

Семисегментный индикатор

В качестве индикатора скорости в устройстве используется семисегментный 4-разрядный индикатор с общим катодом. Для подключения сегментов этого индикатора используются выводы группы PD микроконтроллера, что позволяет управлять отображением цифр на каждом разряде. Управление разрядами индикатора осуществляется через базы транзисторов, которые подключены к пинам PB4, PB5, PB6 и PB7. Эти транзисторы выполняют роль коммутаторов для включения нужного разряда, что позволяет последовательно отображать цифры на всех четырёх разрядах индикатора.

Для обеспечения чёткого и стабильного отображения цифр используется мультиплексирование, при котором разряды переключаются с частотой 488 Гц. Эта частота достаточно высокая, чтобы человеческий глаз воспринимал изображение как стабильное, без мерцания.

Выход сигнала ШИМ

Выходной сигнал ШИМ формируется на пине PB1, согласно настройкам таймера

TCCR1A = (1 << COM1A1) | (1 << WGM11); и TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10);

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

Код проекта и его описание

#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

// Прототипы функций для работы с EEPROM
void EEPROM_write(unsigned int uiAddress, unsigned char ucData);
unsigned char EEPROM_read(unsigned int uiAddress);

// Таблица сегментов для отображения цифр 0-9 на семисегментном индикаторе
uint8_t digit[] = {
	0b00111111,  // 0
	0b00000110,  // 1
	0b01011011,  // 2
	0b01001111,  // 3
	0b01100110,  // 4
	0b01101101,  // 5
	0b01111101,  // 6
	0b00000111,  // 7
	0b01111111,  // 8
	0b01101111   // 9
};

uint8_t buf[4] = {0, 0, 0, 0};  // Буфер для 4-разрядного числа
uint8_t status = 0;  // Переменная для хранения текущего разряда


// Обработчик прерывания по переполнению таймера.
ISR(TIMER0_OVF_vect) {
	switch (status) {
		case 0: // Нулевой разряд
		PORTB &= ~(1 << PB7); // Отключить предыдущий разряд
		PORTD = buf[0];       // Загрузить символ из буфера в порт
		PORTB |= (1 << PB4);  // Включить текущий разряд
		status = 1;           // Переход к следующему разряду
		break;
		
		case 1: // Первый разряд
		PORTB &= ~(1 << PB4);
		PORTD = buf[1];
		PORTB |= (1 << PB5);
		status = 2;
		break;
		
		case 2: // Второй разряд
		PORTB &= ~(1 << PB5);
		PORTD = buf[2];
		PORTB |= (1 << PB6);
		status = 3;
		break;
		
		case 3: // Третий разряд
		PORTB &= ~(1 << PB6);
		PORTD = buf[3];
		PORTB |= (1 << PB7);
		status = 0;
		break;
	}
}

// Функция отображения 16-битного значения с точкой на индикаторе
void disp16(uint16_t n, uint8_t dot) {
	// Вычисление единиц, десятков и заполнение буфера
	for (uint8_t i = 0; i < 4; i++) {
		buf[i] = digit[n % 10];
		n /= 10;
	}
	// Обработка точки
	if (dot) {
		buf[dot - 1] |= (1 << 7); // Включение точки в нужном разряде
	}
}

// Функция записи в EEPROM
void EEPROM_write(unsigned int uiAddress, unsigned char ucData) {
	// Ожидание завершения предыдущей записи
	while (EECR & (1 << EEWE));
	
	// Настройка адресного и регистра данных
	EEAR = uiAddress;
	EEDR = ucData;
	
	// Установка логической единицы в бит EEMWE
	EECR |= (1 << EEMWE);
	
	// Запуск записи в EEPROM установкой бита EEWE
	EECR |= (1 << EEWE);
}

// Функция чтения из EEPROM
unsigned char EEPROM_read(unsigned int uiAddress) {
	// Ожидание завершения предыдущей записи
	while (EECR & (1 << EEWE));
	
	// Настройка регистра адреса
	EEAR = uiAddress;
	
	// Запуск чтения из EEPROM установкой бита EERE
	EECR |= (1 << EERE);
	
	// Возврат данных из регистра данных
	return EEDR;
}

int main(void) {
	// Настройка таймера 0 для создания прерываний с частотой 488 Гц
	TCCR0 |= (1 << CS01); // Делитель частоты 8
	TIMSK |= (1 << TOIE0); // Разрешить прерывание по переполнению

	// Настройка пинов для управления индикатором
	DDRD = 0xFF; // Порт D на выход
	DDRB |= (1 << PB4) | (1 << PB5) | (1 << PB6) | (1 << PB7); // Порты PB4-PB7 на выход

	// Адрес ячейки для записи скорости в память
	uint8_t eeprom_address = 0x04;

	// Настройка Timer1 в режиме Fast PWM с TOP = 100
	TCCR1A = (1 << COM1A1) | (1 << WGM11); // Fast PWM, clear OC1A on compare match, set OC1A at BOTTOM
	TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10); // WGM13:12 = 1:1 for Fast PWM with ICR1 as TOP, prescaler = 1
	
	// Установка значения TOP для достижения значения OCR1A = 100
	ICR1 = 100; // TOP value = 100

	// Установка начального коэффициента заполнения ШИМ на 10%
	if (EEPROM_read(eeprom_address) != 0xFF) {
		OCR1A = EEPROM_read(eeprom_address);
		} else {
		EEPROM_write(eeprom_address, 0x32); // Если при первом запуске данные в памяти стерты, устанавливаем значение 50
		OCR1A = 0x32;
	}

	// Порт для выхода сигнала ШИМ
	DDRB |= (1 << PB1);
	PORTB &= ~(1 << PB1);

	// Настройка пинов кнопок
	DDRC &= ~((1 << PC0) | (1 << PC1)); // Настройка PC0 и PC1 как входов
	PORTC |= (1 << PC0) | (1 << PC1);   // Включаем подтяжку для PC0 и PC1

	sei(); // Глобально разрешить прерывания

	while (1) {
		// Увеличение заполнения
		if (!(PINC & (1 << PC0)) && OCR1A < 100) {
			OCR1A += 1;  // Увеличиваем значение OCR1A на 1
			_delay_ms(50);  // Задержка для управления скоростью изменения

			// Запись значения в EEPROM, если оно изменилось
			if (EEPROM_read(eeprom_address) != (uint8_t)(OCR1A & 0xFF)) {
				EEPROM_write(eeprom_address, (uint8_t)(OCR1A & 0xFF));
			}
		}

		// Уменьшение заполнения
		if (!(PINC & (1 << PC1)) && OCR1A > 0) {
			OCR1A -= 1;  // Уменьшаем коэффициент заполнения на 1
			_delay_ms(50);  // Задержка для управления скоростью изменения

			// Запись значения в EEPROM, если оно изменилось
			if (EEPROM_read(eeprom_address) != (uint8_t)(OCR1A & 0xFF)) {
				EEPROM_write(eeprom_address, (uint8_t)(OCR1A & 0xFF));
			}
		}
		
		// Отображение значения OCR1A на индикаторе
		disp16(OCR1A, 0);
	}
}

Настройку микроконтроллера и его пинов для генерации сигнала ШИМ

Этот фрагмент кода выполняет настройку микроконтроллера для генерации сигнала ШИМ с использованием таймера 1, а также настраивает работу кнопок для управления скоростью.

Настройка Timer1 в режиме Fast PWM

Режим работы таймера: Timer1 настраивается в режим Fast PWM, где значение таймера увеличивается до значения, установленного в ICR1, после чего оно обнуляется. Это обеспечивает генерацию ШИМ сигнала с определенной частотой.

Настройка выходного сигнала: Биты COM1A1 задают режим работы выхода OC1A (пин PB1), который будет сбрасываться при совпадении с значением OCR1A и устанавливаться внизу.

TCCR1A = (1 << COM1A1) | (1 << WGM11); // Fast PWM, сброс OC1A при совпадении, установка OC1A на дне
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10); // WGM13:12 = 1:1 для Fast PWM с ICR1 как TOP, предделитель = 1

Установка значения TOP для ШИМ

ICR1 устанавливает максимальное значение счётчика (TOP), что задает период ШИМ. В данном случае значение TOP равно 100, что обеспечивает точное управление длительностью импульса.

ICR1 = 100; // Значение TOP = 100

Установка начального коэффициента заполнения ШИМ

Чтение из EEPROM: Этот блок проверяет, есть ли сохранённое значение коэффициента заполнения ШИМ в энергонезависимой памяти (EEPROM).

Установка начального значения: Если данные в памяти стерты (значение 0xFF), то коэффициент заполнения устанавливается на 50% (значение 0x32 в шестнадцатеричной системе).

if (EEPROM_read(eeprom_address) != 0xFF) {
    OCR1A = EEPROM_read(eeprom_address);
} else {
    EEPROM_write(eeprom_address, 0x32); // Если при первом запуске данные стерты, устанавливаем значение 50
    OCR1A = 0x32;
}

Настройка порта для выхода сигнала ШИМ

Пин PB1 настраивается как выход для сигнала ШИМ. На старте значение на этом пине устанавливается в низкий уровень.

DDRB |= (1 << PB1);
PORTB &= ~(1 << PB1);

Семисегментный индикатор и его настройка

Этот код отвечает за индикацию чисел на 4-разрядном семисегментном индикаторе и управление процессом отображения чисел.

Настройка таймера и пинов

Здесь происходит настройка таймера 0 для генерации прерываний с частотой 488 Гц (при делителе частоты 8). Эти прерывания необходимы для правильного мультиплексирования разрядов. Также настроены пины портов PD и PB для управления сегментами и разрядами индикатора.

TCCR0 |= (1 << CS01); // Делитель частоты 8
TIMSK |= (1 << TOIE0); // Разрешить прерывание по переполнению

// Настройка пинов для управления индикатором
DDRD = 0xFF; // Порт D на выход
DDRB |= (1 << PB4) | (1 << PB5) | (1 << PB6) | (1 << PB7); // Порты PB4-PB7 на выход

Таблица сегментов для отображения цифр (0-9)

Эта таблица содержит значения для каждого сегмента, которые необходимы для отображения цифр от 0 до 9 на семисегментном индикаторе. Каждое число представлено в виде двоичного кода, где каждый бит управляет состоянием одного из сегментов индикатора.

uint8_t digit[] = {
    0b00111111,  // 0
    0b00000110,  // 1
    0b01011011,  // 2
    0b01001111,  // 3
    0b01100110,  // 4
    0b01101101,  // 5
    0b01111101,  // 6
    0b00000111,  // 7
    0b01111111,  // 8
    0b01101111   // 9
};

Буфер и статус

Буфер buf используется для хранения значений четырёх разрядов, которые нужно отобразить на индикаторе. Переменная status отслеживает текущий активный разряд индикатора.

uint8_t buf[4] = {0, 0, 0, 0};  // Буфер для 4-разрядного числа
uint8_t status = 0;  // Переменная для хранения текущего разряда

Обработчик прерывания по переполнению таймера (ISR)

Этот обработчик прерываний выполняется каждый раз, когда таймер 0 переполняется. Он отвечает за последовательное включение и выключение разрядов индикатора, а также за загрузку соответствующего значения из буфера buf в порт PORTD. Таким образом, с помощью мультиплексирования поочередно отображаются четыре цифры, создавая иллюзию одновременного отображения всех цифр на индикаторе.

ISR(TIMER0_OVF_vect) {
    switch (status) {
        case 0: // Нулевой разряд
            PORTB &= ~(1 << PB7); // Отключить предыдущий разряд
            PORTD = buf[0];       // Загрузить символ из буфера в порт
            PORTB |= (1 << PB4);  // Включить текущий разряд
            status = 1;           // Переход к следующему разряду
            break;
        
        case 1: // Первый разряд
            PORTB &= ~(1 << PB4);
            PORTD = buf[1];
            PORTB |= (1 << PB5);
            status = 2;
            break;
        
        case 2: // Второй разряд
            PORTB &= ~(1 << PB5);
            PORTD = buf[2];
            PORTB |= (1 << PB6);
            status = 3;
            break;
        
        case 3: // Третий разряд
            PORTB &= ~(1 << PB6);
            PORTD = buf[3];
            PORTB |= (1 << PB7);
            status = 0;
            break;
    }
}

Функция отображения числа (disp16)

Функция disp16 принимает 16-битное число n и отображает его на 4-разрядном индикаторе. Число разбивается на цифры и сохраняется в буфере buf.

void disp16(uint16_t n, uint8_t dot) {
    // Вычисление единиц, десятков и заполнение буфера
    for (uint8_t i = 0; i < 4; i++) {
        buf[i] = digit[n % 10];
        n /= 10;
    }
    // Обработка точки
    if (dot) {
        buf[dot - 1] |= (1 << 7); // Включение точки в нужном разряде
    }
}

Функции записи и чтения из EEPROM

Этот код предоставляет две функции для работы с энергонезависимой памятью (EEPROM) на микроконтроллере: одну для записи данных в EEPROM и другую для чтения данных из неё.

Функция записи в EEPROM

Функция записи в EEPROM начинает с того, что проверяет, завершена ли предыдущая операция записи. Для этого используется бит EEWE в регистре управления EECR. Пока этот бит установлен, микроконтроллер занят записью, и необходимо дождаться её завершения. После этого функция устанавливает адрес памяти в регистр EEAR и данные, которые нужно записать, в регистр EEDR. Чтобы начать процесс записи, сначала активируется бит EEMWE, а затем устанавливается бит EEWE, что и инициирует саму запись данных в указанную ячейку памяти.

void EEPROM_write(unsigned int uiAddress, unsigned char ucData) {
    // Ожидание завершения предыдущей записи
    while (EECR & (1 << EEWE));
    
    // Настройка адресного и регистра данных
    EEAR = uiAddress;
    EEDR = ucData;
    
    // Установка логической единицы в бит EEMWE
    EECR |= (1 << EEMWE);
    
    // Запуск записи в EEPROM установкой бита EEWE
    EECR |= (1 << EEWE);
}

Функция чтения из EEPROM

Функция чтения из EEPROM также начинается с ожидания завершения возможной текущей операции записи, что проверяется по биту EEWE. После этого функция устанавливает адрес нужной ячейки памяти в регистре EEAR и запускает процесс чтения, устанавливая бит EERE в регистре EECR. Данные, считанные из EEPROM, попадают в регистр EEDR, откуда они возвращаются как результат функции.

unsigned char EEPROM_read(unsigned int uiAddress) {
    // Ожидание завершения предыдущей записи
    while (EECR & (1 << EEWE));
    
    // Настройка регистра адреса
    EEAR = uiAddress;
    
    // Запуск чтения из EEPROM установкой бита EERE
    EECR |= (1 << EERE);
    
    // Возврат данных из регистра данных
    return EEDR;
}

Рабочий цикл

Этот фрагмент кода отвечает за управление коэффициентом заполнения ШИМ-сигнала с помощью кнопок и его отображение на индикаторе. Код реализует две основные функции: увеличение и уменьшение коэффициента заполнения, а также сохранение его текущего значения в энергонезависимой памяти EEPROM, если оно изменилось.

При нажатии кнопки, подключенной к пину PC0, значение регистра OCR1A, отвечающего за коэффициент заполнения, увеличивается на единицу, при условии, что текущее значение меньше 100. После каждого изменения значения делается короткая задержка в 20 миллисекунд, чтобы контролировать скорость изменения. Если новое значение отличается от того, что уже сохранено в EEPROM, происходит запись обновленного значения в память.

Аналогичным образом работает блок кода, отвечающий за уменьшение коэффициента заполнения. Если нажата кнопка, подключенная к пину PC1, и текущее значение OCR1A больше нуля, коэффициент заполнения уменьшается на единицу, также с задержкой и последующей записью в EEPROM при изменении значения.

В конце каждого цикла новое значение OCR1A отображается на 4-разрядном семисегментном индикаторе с помощью функции disp16, что позволяет пользователю визуально контролировать текущий коэффициент заполнения ШИМ-сигнала.

// Увеличение заполнения
		if (!(PINC & (1 << PC0)) && OCR1A < 100) {
			OCR1A += 1;  // Увеличиваем значение OCR1A на 1
			_delay_ms(20);  // Задержка для управления скоростью изменения

			// Запись значения в EEPROM, если оно изменилось
			if (EEPROM_read(eeprom_address) != (uint8_t)(OCR1A & 0xFF)) {
				EEPROM_write(eeprom_address, (uint8_t)(OCR1A & 0xFF));
			}
		}

		// Уменьшение заполнения
		if (!(PINC & (1 << PC1)) && OCR1A > 0) {
			OCR1A -= 1;  // Уменьшаем коэффициент заполнения на 1
			_delay_ms(20);  // Задержка для управления скоростью изменения

			// Запись значения в EEPROM, если оно изменилось
			if (EEPROM_read(eeprom_address) != (uint8_t)(OCR1A & 0xFF)) {
				EEPROM_write(eeprom_address, (uint8_t)(OCR1A & 0xFF));
			}
		}
		
		// Отображение значения OCR1A на индикаторе
		disp16(OCR1A, 0);
  • 01.09.2024