ШИМ-регулятор на Atmega с сохранением настроек скорости в EEPROM
Введение
Недавно у меня возникла необходимость в создании ШИМ-регулятора с точной настройкой скорости. Обычно такие регуляторы управляются с помощью переменного резистора, который изменяет аналоговый сигнал. Однако этот подход оказался недостаточно надёжным. Переменные резисторы подвержены воздействию множества внешних факторов: температура, влажность, износ со временем — всё это может влиять на точность регулировки и стабильность работы устройства. Это привело меня к мысли, что нужно искать альтернативное решение.
Я решил, что лучше реализовать управление скоростью с помощью кнопок. Такой подход сразу решает несколько проблем. Во-первых, кнопки обеспечивают точную цифровую настройку, что исключает ошибки и неточности, присущие аналоговому управлению. Во-вторых, кнопочное управление не зависит от внешних условий и имеет гораздо большую долговечность. Это особенно важно для устройств, которые должны работать стабильно и предсказуемо на протяжении длительного времени. Дополнительно, настройки скорости, установленные с помощью кнопок, могут сохраняться в энергонезависимой памяти (EEPROM). Это означает, что они остаются неизменными даже после отключения питания, что значительно повышает удобство и надёжность работы устройства.
Преимущества
Основным преимуществом данного ШИМ-генератора по сравнению с другими является то, что изменение скорости происходит не через аналоговый вход, а с помощью физического нажатия кнопок. Это не только обеспечивает более точную и стабильную настройку, но и позволяет сохранять результаты в энергонезависимой памяти (EEPROM). Благодаря этому настройка скорости сохраняется даже после отключения питания, что делает устройство более удобным и надёжным в использовании.
Схема устройства
Назначение пинов микроконтроллера
Описание работы схемы
Кнопки управления
К выходам 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);