Микроконтроллеры: Arduino, порты, прерывания
Устройство микроконтроллера, работа с портами ввода-вывода, чтение аналоговых датчиков, ШИМ, прерывания и таймеры, опрос против прерываний, типовая схема стенда.
Курсовые по автоматизации и микроконтроллерам чаще всего делают на Arduino: дешёвая плата, простая среда и наглядный результат, который можно принести на защиту в собранном виде.
Что внутри микроконтроллера
| Блок | Назначение | Пример ATmega328P |
|---|---|---|
| Ядро | Выполняет команды | 8-разрядное AVR, 16 МГц |
| Flash | Программа | 32 КБ |
| SRAM | Переменные во время работы | 2 КБ |
| EEPROM | Данные, сохраняемые при отключении | 1 КБ |
| GPIO | Цифровые входы-выходы | 23 линии |
| АЦП | Измерение напряжения | 6 каналов, 10 разрядов |
| Таймеры | Отсчёт времени, ШИМ | 3 таймера |
| Интерфейсы | Связь с внешними устройствами | UART, I²C, SPI |
Цифровые входы и выходы
const uint8_t LED_PIN = 13;
const uint8_t BTN_PIN = 2;
void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BTN_PIN, INPUT_PULLUP); // встроенная подтяжка к питанию
Serial.begin(9600);
}
void loop() {
// При INPUT_PULLUP кнопка, соединяющая вход с землёй, даёт LOW
bool pressed = (digitalRead(BTN_PIN) == LOW);
digitalWrite(LED_PIN, pressed ? HIGH : LOW);
}
Без подтягивающего резистора вход «висит в воздухе» и ловит наводки — показания случайны. Режим INPUT_PULLUP включает резистор внутри контроллера, поэтому внешний не нужен: кнопку достаточно соединить с землёй.
Дребезг контактов
unsigned long lastPress = 0;
const unsigned long DEBOUNCE_MS = 50;
bool lastState = HIGH;
void loop() {
bool state = digitalRead(BTN_PIN);
if (state != lastState && millis() - lastPress > DEBOUNCE_MS) {
lastPress = millis();
lastState = state;
if (state == LOW) counter++; // сработало один раз на нажатие
}
}
Аналоговые измерения
АЦП на 10 разрядов: 0…1023 отсчёта на диапазон 0…5 В Напряжение: U = отсчёт · 5,0 / 1023 Разрешение: 5 В / 1024 ≈ 4,9 мВ на единицу Пример с делителем напряжения для измерения до 15 В: R1 = 20 кОм, R2 = 10 кОм → коэффициент деления 3 U_вх = (отсчёт · 5,0 / 1023) · 3
const uint8_t SENSOR = A0;
float readVoltage() {
// Усреднение подавляет шум измерения
uint32_t sum = 0;
for (uint8_t i = 0; i < 16; i++) {
sum += analogRead(SENSOR);
delayMicroseconds(200);
}
float code = sum / 16.0;
return code * 5.0 / 1023.0;
}
// Термистор или LM35: 10 мВ на градус
float readTemperature() {
return readVoltage() * 100.0;
}
ШИМ
Широтно-импульсная модуляция: среднее напряжение задаётся долей времени включённого состояния U_ср = U_пит · D, где D = t_вкл / T — коэффициент заполнения analogWrite(pin, 0…255): 128 соответствует D = 0,5, то есть 2,5 В при 5 В питания Частота ШИМ на Arduino Uno ≈ 490 Гц (или 980 Гц на выводах 5 и 6)
const uint8_t MOTOR = 9; // вывод с поддержкой ШИМ
void setMotorSpeed(uint8_t percent) {
analogWrite(MOTOR, map(percent, 0, 100, 0, 255));
}
// Плавный разгон
for (uint8_t p = 0; p <= 100; p += 5) {
setMotorSpeed(p);
delay(100);
}
Управлять двигателем напрямую с вывода нельзя: линия отдаёт максимум 40 мА, а мотору нужны сотни. Между ними ставят транзистор или драйвер (L298N, IRF540) — это обязательный элемент схемы в курсовой.
Прерывания
volatile uint32_t pulseCount = 0; // volatile обязательно!
void countPulse() { // обработчик прерывания
pulseCount++; // только самое необходимое
}
void setup() {
pinMode(2, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(2), countPulse, FALLING);
}
void loop() {
noInterrupts();
uint32_t count = pulseCount; // атомарное чтение копии
interrupts();
Serial.println(count);
delay(1000);
}
- Переменная, изменяемая в обработчике, обязана быть volatile — иначе компилятор закэширует её в регистре и основной код не увидит изменений.
- В обработчике нельзя вызывать delay() и Serial.print() — они сами используют прерывания.
- Обработчик должен быть максимально коротким: увеличить счётчик, поднять флаг. Вся обработка — в основном цикле.
- Чтение многобайтной переменной нужно защищать от прерывания, иначе можно поймать половину старого и половину нового значения.
Опрос или прерывания
| Опрос (polling) | Прерывания | |
|---|---|---|
| Реакция | Зависит от длительности цикла | Немедленная |
| Сложность | Простая | Выше, нужны volatile и защита |
| Пропуск событий | Возможен при долгом цикле | Практически исключён |
| Где применять | Кнопки, медленные датчики | Энкодеры, счётчики импульсов, аварийные сигналы |
Работа без delay
// delay() блокирует всё: за это время контроллер ничего не делает
unsigned long lastRead = 0, lastBlink = 0;
bool ledState = false;
void loop() {
unsigned long now = millis();
if (now - lastRead >= 500) { // датчик — раз в 500 мс
lastRead = now;
Serial.println(readTemperature());
}
if (now - lastBlink >= 1000) { // индикатор — раз в секунду
lastBlink = now;
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
handleButton(); // кнопка опрашивается постоянно
}
Что включить в курсовую
- Схема электрическая принципиальная с указанием номиналов и типов элементов.
- Расчёт токоограничивающих резисторов и делителей напряжения.
- Алгоритм работы: блок-схема или диаграмма состояний.
- Листинг с комментариями, в приложение — полностью.
- Таблица испытаний: воздействие, ожидаемая реакция, фактическая.
- Фотографии собранного стенда и осциллограммы, если требуется.
- Оценка потребления и выбор источника питания.
Частые вопросы
Почему нужен резистор последовательно со светодиодом?
Светодиод почти не ограничивает ток сам: без резистора он мгновенно выйдет из строя вместе с выводом контроллера. Номинал считают как R = (U_пит − U_св)/I_св, обычно получается 220-330 Ом.
Arduino или чистый AVR для курсовой?
Arduino быстрее даёт результат, но некоторые кафедры требуют программирование регистров напрямую. Компромисс: сделать проект на Arduino, а один-два узла реализовать через регистры, показав, что понимаете, как работает библиотека.
Что делать, если не хватает памяти?
Убрать float там, где хватает целых чисел, вынести строки во Flash через F(), уменьшить буферы, отказаться от String в пользу массивов char. Строковый класс String к тому же фрагментирует память и приводит к зависаниям.