Ну что же, я пойду ещё немного дальше: буду использовать ещё один переменный резистор для установки длительности нарастания яркости. Также я буду использовать таблицу со значениями яркостей ввиду того, что кривая чувствительности глаза напоминает логарифмическую зависимость. Ну а т.к. это заготовка под сетевой диммер, то для ШИМа светодиода она требует корректировки. Но здесь я хочу показать принцип, а не изготовить конечное изделие. МК я буду использовать attiny841 (441). У него практически любой вход может быть аналоговым, ШИМ сигнал также можно вывести на практически любую ногу. Также в нем присутствуют два 16-ти битных таймера. Один из них уйдёт под генерацию ШИМ, на другом будем отмерять "системное" время. Тактироваться буду от внутреннего RC осциллятора с включенным fuse CKDIV8, что в итоге даст системную частоту 1 МГц.
Калибровочная таблица со вспомогательными функциями:
#include <avr/pgmspace.h>
const uint16_t light_list[] PROGMEM = // Массив со значениями яркостей
{
11000, // Нулевое значение далеко в стороне от пересечения нуля
9782,
9781,
9780,
9778,
9775,
9769,
9761,
9752,
9740,
9726,
9710,
9692,
9672,
9650,
9626,
9600,
9572,
9542,
9510,
9476,
9440,
9402,
9362,
9319,
9275,
9229,
9181,
9131,
9078,
9024,
8968,
8909,
8849,
8786,
8722,
8656,
8587,
8517,
8444,
8370,
8293,
8215,
8134,
8051,
7967,
7880,
7791,
7701,
7608,
7513,
7416,
7318,
7217,
7114,
7009,
6902,
6793,
6682,
6569,
6455,
6338,
6219,
6097,
5974,
5849,
5722,
5593,
5462,
5329,
5194,
5056,
4917,
4776,
4633,
4487,
4340,
4191,
4039,
3886,
3731,
3573,
3414,
3252,
3089,
2923,
2756,
2586,
2415,
2241,
2065,
1888,
1708,
1526,
1343,
1157,
969,
779,
588,
394,
198
};
uint16_t Get_Phase (uint8_t light)
{
return pgm_read_word(&(light_list[light]));
}
Этот код помещён в файл light.c и для него создан заголовочный файл light.h:
#ifndef LIGHT_H_
#define LIGHT_H_
uint16_t Get_Phase (uint8_t light);
#endif /* LIGHT_H_ */
Теперь определимся со схемотехникой. У меня пин PA1 - это выход ШИМ, PA4 - это пин управления яркостью (точнее задатчик яркости), PA5 - пин регулировки скорости изменения яркости:
-----------
VCC | | GND
(PB0) -> | | <- (PA0)
(PB1) -> | | -> (PA1) ШИМ выход (OC1B)
RESET (PB3) -> | | -> (PA2)
(PB2) -> | | <- (PA3)
(PA7) <- |SS | <- (PA4) ADC4D, 00 0100, пин управления яркостью
(PA6) <- | | <- (PA5) ADC5D, 00 0101, пин управления скоростью изменения яркости
|_________|
Подключаем необходимые заголовки, дефайним константы, создаём необходимые переменные
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include "Light.h"
#define Dimmer_DDR DDRA
#define Dimmer_OUT PINA1 // Пин c ШИМ сигналом
#define Light_pin 0b000100 // Значение регистра мультиплексора входов АЦП
#define Speed_pin 0b000101
#define ADC_pause 100 // Необходимая пауза между переключениями входов мультиплексора
#define icr1 10000 // Значение TOP для таймера 1
#define Prescaler_1 (1 << CS10) // Делитель тактов
#define Prescaler_8 (1 << CS11)
#define Prescaler_64 (1 << CS10) | (1 << CS11)
#define Prescaler_256 (1 << CS12)
#define Prescaler_1024 (1 << CS10) | (1 << CS12)
#define Set_Prescaler1 Prescaler_1 // Установка предделителя для таймера 1
#define Set_Prescaler2 Prescaler_1024 // Установка предделителя для таймера 2
#define ClearPrescaler GTCCR = (1 << PSR) // Сброс предделителя
#define Timer1_Start() TCCR1B |= Set_Prescaler1; ClearPrescaler // Пуск таймера1 с очисткой делителя
#define Timer2_Start() TCCR2B |= Set_Prescaler2 // Пуск таймера2
// Структуры
struct Phase_t
{
uint8_t End; // Индекс целевой (конечной) фазы ШИМ сигнала
int8_t Current; // Индекс текущей фазы ШИМ сигнала
uint8_t Save; // Сохраненное значение индекса
uint8_t Change; // Признак регулирования фазы (индекса)
uint16_t Speed; // Скорость изменния индекса
};
// Глобальные переменные
struct Phase_t Phase;
ADC_pause необходима для того, чтобы дать мультиплексору время на переключение входов. Даташит говорит про необходимую паузу в 10мс. Я же задаю около 100мс - мне торопиться некуда.
icr1 - это значение счетчика таймера1, достигнув которого таймер1 сбросится. Таким образом формируется частота ШИМ. В нашем случае она равна 100Гц.
Структура Phase_t содержит все необходимые поля с переменными. Здесь я не использую поле Save - это артефакт)
Теперь запишем функции работы с АЦП.
inline void ADC_Init(void)
{
DIDR0 = (1<<ADC4D) | (1<<ADC5D); // Отключаем триггер Шмитта от сигнальной ноги
// ADMUXB = (0<<REFS2) | (0<<REFS1) | (0<<REFS0) | (0<<GSEL1) | (0<<GSEL0); // Reference = Vcc, усиление = 1
// ADCSRB = (0<<ADLAR) | (0<<ADTS1) | (0<<ADTS0); // Запуск вручную
ADCSRA = (1<<ADEN) | // Разрешаем работу АЦП
(1<<ADIE) | // Разрешаем прерывания
(0<<ADATE) | // Триггер выключен, ручной старт
(1<<ADPS2) | (0<<ADPS1) | (0<<ADPS0); // Прескалер = 16
}
void ADC_Set_channel (uint8_t Analog_input)
{
ADMUXA = Analog_input;
}
void ADC_Start_Conversion (void)
{
ADCSRA = (1<<ADEN) |
(1<<ADIE) |
(0<<ADATE) |
(1<<ADPS2) | (0<<ADPS1) | (0<<ADPS0) |
(1<<ADSC); // Команда на запуск преобразования
}
В регистр DIDR0 записываем пины, которые будут аналоговыми входами. Это необходимо для того, чтобы входной триггер Шмитта не "щелкал", когда входной сигнал пересекает точки переключения логических уровней, что несколько снизит помехи. Системная частота у нас 1МГц, но дополнительно я включаю прескалер (делитель) на 16, что в итоге даст частоту тактирования АЦП 62,5 кГц. Мне торопиться некуда)
Раз мы разрешили прерывания у АЦП, то обязательно пишем обработчик, иначе по прерыванию уйдем на ресет:
ISR (ADC_vect)
{
}
Инициализируем таймеры:
//... Инициализация таймера с ШИМ
inline void T1_Init(void)
{
TCCR1A = (1 << COM1B1) | (1 << COM1B0) | (1 << WGM11) | (0 << WGM10); // При совпадении значения таймера с блоком сравнения COM1B формируется передний фронт импульса ШИМ
TCCR1B = (1 << WGM13) | (1 << WGM12); // При достижении таймером значения ICR1 формируется задний фронт импульса ШИМ
TOCPMSA0 = (1 << TOCC0S0); // Привязка блока сравнения COM1B к пину с подключенным симистором
TOCPMCOE = (1 << TOCC0OE); // Разрешаем работу пина от блока сравнения
ICR1 = icr1;
// TIMSK1 = (1 << OCIE1A) | (1 << OCIE1B); // Установка прерывания от компаратора 1A и 1B
}
//... Инициализация таймера скорости изменения яркости
inline void T2_Init(void)
{
// TIMSK2 = (1 << OCIE2A);
}
У attiny841(441) недостаточно разрешить работу компаратора таймера для выдачи ШИМ сигнала наружу. Необходимо также настроить выходной коммутатор. Что где разрешаем должно быть понятно из комментария в коде.
Функция инициализации таймера2 пустая, но оставлена - вдруг что туда ещё записать)
Запускать измерение АЦП я буду при остановленном CPU, поэтому запишем ещё одну функцию:
void Sleep_Init(uint8_t Sleep_mode)
{
set_sleep_mode(Sleep_mode); // Выбираем режим сна
sleep_enable(); // Разрешаем сон
}
Здесь мы определяем режим сна и разрешаем сон (только разрешаем!, а не уходим в него!)
Теперь функция main:
int main(void)
{
uint16_t Old_time, Current_time, Old_time_ADC;
uint8_t light_measur;
Dimmer_DDR = (1 << Dimmer_OUT); // Конфигурируем пин ШИМ-а на выход
PRR = (1 << PRTWI) | (1 << PRUSART1) | (1 << PRUSART0); // Запрещаем работу ненужных блоков
T1_Init(); // Инициализация таймеров
T2_Init();
Phase.Current = 0;
Phase.Speed = 0;
Phase.End = 0;
Timer1_Start(); // Запускаем таймеры (на самом деле подключаем
Timer2_Start(); // тактовый вход таймера к одному из выходов предедлителя)
ADC_Init(); // Инициализация АЦП
Old_time = TCNT2; // Фиксируем текущее время
Old_time_ADC = TCNT2;
ADC_Set_channel(Light_pin); // Переключаем мультиплексор на вход задатчика яркости
light_measur = 1; // Взводим признак того. что мулитплексор переключен на вход
// задатчика яркости
Sleep_Init(SLEEP_MODE_IDLE);
sei(); // Разрешаем прерывания
while (1)
{
Current_time = TCNT2; // Фиксируем текущее время
if ((Current_time - Old_time) > Phase.Speed) // Если настало время для изменения яркости к целевому значению
{
Set_Phase();
OCR1B = Get_Phase(Phase.Current); // Пишем в компаратор ШИМ таймера новое значение фазы (скважности)
Old_time = TCNT2; // Фиксируем текущее время
}
if ((Current_time - Old_time_ADC) > ADC_pause) // А не пора ли снять показания с АЦП?
{
ADC_Start_Conversion(); // Команда на запуск преобразования АЦП
sleep_cpu(); // Останавливаем ЦПУ
if (light_measur) // Если мультиплексор на входе задатчика яркости
{
Phase.End = (ADC >> 3); // Делим значение с АЦП на 4
if (Phase.End > 100) {Phase.End = 100;} // Если индекс больше 100, то корректируем
light_measur = 0;
ADC_Set_channel(Speed_pin); // Переключаем мультиплексор на вход управления скоростью изменеия яркостью
}
else
{
Phase.Speed = ADC; // Запоминаем значение скорости изменения яркостью
light_measur = 1;
ADC_Set_channel(Light_pin); // Переключаемся на задатчик яркости
}
Old_time_ADC = TCNT2; // Фиксируем текущее время
}
}
}
Запускаю АЦП на измерение и сразу увожу МК в режим сна IDLE. Почему не SLEEP_MODE_ADC? Всё просто - в этом режиме останавливается тактирование таймеров, что негативно отразится на ШИМ сигнале.
Последняя функция: установка текущей фазы (скважности) ШИМ сигнала. Я через определённые промежутки времени (задаваемые вторым потенциометром) инкрементирую или декрементирую (в зависимости от того, с какой "стороны" нахожусь от целевого значения) текущий индекс, который определяет уровень яркости (см. выше light.c):
//... Установка скважности ШИМ сигнала
void Set_Phase(void)
{
if (Phase.Change)
{
if (Phase.End > Phase.Current)
{
Phase.Current++;
}
else
{
Phase.Current--;
}
}
if (Phase.End == Phase.Current)
{
Phase.Change = 0;
}
else
{
Phase.Change = 1;
}
}
Скомпилированный с оптимизацией -O3 код занимает:
Program Memory Usage : 688 bytes 8,4 % Full
Data Memory Usage : 6 bytes 1,2 % Full