Загрузка данных
#include "motion.h"
#include "config.h"
#include "pinout.h"
#include "gcode_program.h"
#include "state_manager.h"
#include <Arduino.h>
// Размер кольцевой ленты времени (8192 тика = 40.96 миллисекунд подушки безопасности ОЗУ)
#define STEP_BUFFER_SIZE 4096
// Структура одной микросекундной ячейки времени
struct StepCmd {
uint32_t stepX : 1; // 1 = импульс STEP HIGH на нечетном тике таймера, 0 = пустой такт
uint32_t dirX : 1; // 1 = HIGH, 0 = LOW
uint32_t stepY : 1;
uint32_t dirY : 1;
uint32_t stepZ : 1;
uint32_t dirZ : 1;
uint32_t valid : 1; // Флаг готовности такта времени к чтению из ISR прерывания
};
// Статический массив кольцевого буфера тактов времени
static StepCmd stepRingBuffer[STEP_BUFFER_SIZE];
// Локальные внутренние указатели кольца (намертво заперты внутри модуля, не видны извне)
static volatile uint32_t bufferHead = 0;
static volatile uint32_t bufferTail = 0;
// Статический флаг для блокировки триггера концевиков в ISR во время поиска баз
static volatile bool isHomingActive = false;
// Честные физические счетчики шагов станка (изменяются ИСКЛЮЧИТЕЛЬНО в прерывании таймера)
volatile long currentStepsX = 0;
volatile long currentStepsY = 0;
volatile long currentStepsZ = 0;
// Единый флаг активности движения для автомата gcode_program и main.cpp
volatile bool isVectorMoving = false;
volatile bool isPlannerBusy = false;
// Абсолютная сквозная математическая координатная база планировщика ЧПУ (в шагах)
volatile long plannerStepsX = 0;
volatile long plannerStepsY = 0;
volatile long plannerStepsZ = 0;
// Массив точек оптической лазерной карты коррекции провиса балки фермы (до 250 точек)
static CorrectionPoint curvatureMap[250];
// Объявление структуры-посредника неблокирующего обмена кадрами между Ядром 0 и Ядром 1
volatile InputSegment nextSeg = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, false};
// Динамический программный Override общей подачи со слайдера PyQt6 (1.0 = 100%)
float pythonFeedrateOverride = 1.0f;
// ============================================================================
// СТРУКТУРНЫЙ БЛОК: ИНИЦИАЛИЗАЦИЯ И ЛАЗЕРНАЯ КОРРЕКЦИЯ
// ============================================================================
void initMotion() {
isVectorMoving = false;
isPlannerBusy = false;
currentStepsX = 0; currentStepsY = 0; currentStepsZ = 0;
plannerStepsX = 0; plannerStepsY = 0; plannerStepsZ = 0;
bufferHead = 0;
bufferTail = 0;
// Безопасное зануление всей лазерной карты при старте контроллера
for (int i = 0; i < 250; i++) {
curvatureMap[i].y_offset = 0.0f;
curvatureMap[i].z_offset = 0.0f;
}
}
// Запись точки лазерного сканирования из Python-интерфейса в ОЗУ станка
void updateCurvaturePoint(int index, float y_off, float z_off) {
if (index >= 0 && index < cfg.laserMapSize) {
curvatureMap[index].y_offset = y_off;
curvatureMap[index].z_offset = z_off;
}
}
// Линейная микронная интерполяция провиса фермы между двумя ближайшими точками сканирования
CorrectionPoint getCorrection(float currentX) {
if (currentX < 0.0f) currentX = 0.0f;
// Ограничиваем максимальную длину рабочей зоны по карте лазера
float max_len = (cfg.laserMapSize - 1) * cfg.laserGridStep;
if (currentX > max_len) currentX = max_len;
// Находим индекс левой опорной точки
int index = (int)(currentX / cfg.laserGridStep);
if (index >= cfg.laserMapSize - 1) return curvatureMap[cfg.laserMapSize - 1];
// Вычисляем коэффициент положения (от 0.0 до 1.0) между точками
float t = (currentX - (index * cfg.laserGridStep)) / cfg.laserGridStep;
CorrectionPoint corr;
// Находим промежуточное смещение по Y и Z
corr.y_offset = curvatureMap[index].y_offset + t * (curvatureMap[index+1].y_offset - curvatureMap[index].y_offset);
corr.z_offset = curvatureMap[index].z_offset + t * (curvatureMap[index+1].z_offset - curvatureMap[index].z_offset);
return corr;
}
// ============================================================================
// СТРУКТУРНЫЙ БЛОК: НЕБЛОКИРУЮЩАЯ ПЕРЕДАЧА КАДРА МЕЖДУ ЯДРАМИ (Core 0)
// ============================================================================
void prepareVectorSegment(float newX, float newY, float newZ, float feedRateMM_Sec,
float accelMM_Sec2, float vStartMM_Sec, float vEndMM_Sec) {
// Блокировка от затирания: ждем, пока Ядро 1 заберет предыдущий пакет
while (nextSeg.hasNewData) {
delay(1);
}
// Перекладываем геометрию и скорости кадра в структуру межъядерного обмена
nextSeg.x = newX;
nextSeg.y = newY;
nextSeg.z = newZ;
nextSeg.accel = accelMM_Sec2;
// Подача кадра мм/секунду для физических формул Ядра 1
nextSeg.v_frame = feedRateMM_Sec;
// Принимаем готовые Look-Ahead скорости сопряжения от Python (они уже в мм/сек)
nextSeg.v_start = vStartMM_Sec;
nextSeg.v_end = vEndMM_Sec;
// Атомарная отмашка для потока Ядра 1 и взвод флага движения для main.cpp
nextSeg.hasNewData = true;
isPlannerBusy = true;
isVectorMoving = true;
}
// ============================================================================
// СТРУКТУРНЫЙ БЛОК: ЯДРО ПЛАНИРОВЩИКА ДВИЖЕНИЯ ЧПУ (Core 1)
// ============================================================================
void motionTask(void * parameter) {
// Внутренняя фиксированная частота работы нашего двухфазного таймера (200 кГц)
const float TIMER_FREQ = 200000.0f;
// Локальные переменные потока для отслеживания шагов внутри кадра
long localStepsX = 0;
long localStepsY = 0;
long localStepsZ = 0;
while (true) {
// --- 4.1.1. ОПРОС И БЛОКИРОВКА МЕЖЪЯДЕРНОЙ ОЧЕРЕДИ ---
if (!nextSeg.hasNewData) {
// Если в кольцевом буфере физически кончились шаги, сигнализируем Ядру 0 в main.cpp
if (bufferTail == bufferHead) {
isPlannerBusy = false;
}
vTaskDelay(pdMS_TO_TICKS(1)); // Неблокирующее ожидание FreeRTOS (1 мс)
continue;
}
// --- 4.1.2. ИЗВЛЕЧЕНИЕ ПАРАМЕТРОВ КАДРА И СКОРОСТЕЙ (в мм/сек) ---
float nX = nextSeg.x;
float nY = nextSeg.y;
float nZ = nextSeg.z;
float accel = nextSeg.accel;
float v_max = nextSeg.v_frame; // Ограничение подачи кадра от CAM
float v_start = nextSeg.v_start; // Скорость входа от Look-Ahead Python
float v_end = nextSeg.v_end; // Скорость выхода на стыке угла от Look-Ahead Python
nextSeg.hasNewData = false; // Мгновенный Handshake: освобождаем ячейку для Ядра 0
// --- 4.1.3. ОПТИЧЕСКАЯ ЛАЗЕРНАЯ КОМПЕНСАЦИЯ ПРОВИСА ФЕРМЫ ---
CorrectionPoint corr = getCorrection(nX);
float realMachineY = nY + corr.y_offset;
float realMachineZ = nZ + corr.z_offset;
// --- 4.1.4. ПЕРЕВОД МИЛЛИМЕТРОВ В ЦЕЛОЧИСЛЕННЫЕ НАПРАВЛЕНИЯ ЧПУ ---
// Рассчитываем точные целевые шаги, которые требует CAM-модель
long targetStepsX = nX * cfg.stepsPerMmX;
long targetStepsY = realMachineY * cfg.stepsPerMmY;
long targetStepsZ = realMachineZ * cfg.stepsPerMmZ;
// Вычисляем чистые дельты перемещения строго от идеальной математической базы прошлого кадра
long totalDeltaX = targetStepsX - plannerStepsX;
long totalDeltaY = targetStepsY - plannerStepsY;
long totalDeltaZ = targetStepsZ - plannerStepsZ;
long absDeltaX = labs(totalDeltaX);
long absDeltaY = labs(totalDeltaY);
long absDeltaZ = labs(totalDeltaZ);
// Находим ведущую ось (максимальное дискретное перемещение в импульсах внутри этого кадра)
long maxSegmentSteps = absDeltaX;
if (absDeltaY > maxSegmentSteps) maxSegmentSteps = absDeltaY;
if (absDeltaZ > maxSegmentSteps) maxSegmentSteps = absDeltaZ;
// Защита от пустых кадров (команды шпинделя, охлаждения и т.д.)
if (maxSegmentSteps == 0) {
continue;
}
// ====================================================================
// СТРУКТУРНЫЙ БЛОК: MULTI-SEGMENT VECTOR SPLITTING & VALIDATION (Core 1)
// ====================================================================
// Идеальные длины участков в мм
float s_accel_ideal = (v_max * v_max - v_start * v_start) / (2.0f * accel);
float s_decel_ideal = (v_max * v_max - v_end * v_end) / (2.0f * accel);
float dx_mm = (float)totalDeltaX / cfg.stepsPerMmX;
float dy_mm = (float)totalDeltaY / cfg.stepsPerMmY;
float dz_mm = (float)totalDeltaZ / cfg.stepsPerMmZ;
float totalLength_mm = sqrt(dx_mm * dx_mm + dy_mm * dy_mm + dz_mm * dz_mm);
// Объявляем массивы целочисленных дельт для каждого из трех сегментов
long dX[3] = {0, 0, 0}; // [0] - разгон, [1] - марш, [2] - торможение
long dY[3] = {0, 0, 0};
long dZ[3] = {0, 0, 0};
// Процентные доли сегментов в общем векторе
float pct_accel = 0.0f;
float pct_cruise = 0.0f;
if (s_accel_ideal + s_decel_ideal > totalLength_mm) {
// --- СЦЕНАРИЙ А: КОРОТКИЙ ОТРЕЗК (ТРЕУГОЛЬНИК, 2 сегмента) ---
float v_peak = sqrt((2.0f * accel * totalLength_mm + v_start * v_start + v_end * v_end) / 2.0f);
v_max = v_peak;
float s_accel_actual = (v_peak * v_peak - v_start * v_start) / (2.0f * accel);
pct_accel = s_accel_actual / totalLength_mm;
pct_cruise = 0.0f; // Марша нет
// Целочисленные дельты разгона
dX[0] = (long)(totalDeltaX * pct_accel);
dY[0] = (long)(totalDeltaY * pct_accel);
dZ[0] = (long)(totalDeltaZ * pct_accel);
// Марш равен нулю
dX[1] = 0; dY[1] = 0; dZ[1] = 0;
// Торможение — это строго остаток от исходного вектора
dX[2] = totalDeltaX - dX[0];
dY[2] = totalDeltaY - dY[0];
dZ[2] = totalDeltaZ - dZ[0];
}
else {
// --- СЦЕНАРИЙ Б: КЛАССИЧЕСКАЯ ТРАПЕЦИЯ (3 сегмента) ---
pct_accel = s_accel_ideal / totalLength_mm;
float s_cruise_ideal = totalLength_mm - (s_accel_ideal + s_decel_ideal);
pct_cruise = s_cruise_ideal / totalLength_mm;
// Дельты разгона
dX[0] = (long)(totalDeltaX * pct_accel);
dY[0] = (long)(totalDeltaY * pct_accel);
dZ[0] = (long)(totalDeltaZ * pct_accel);
// Дельты маршевого круиза
dX[1] = (long)(totalDeltaX * pct_cruise);
dY[1] = (long)(totalDeltaY * pct_cruise);
dZ[1] = (long)(totalDeltaZ * pct_cruise);
// Дельты торможения — строго вычитанием из общего баланса, поглощая округления float
dX[2] = totalDeltaX - dX[0] - dX[1];
dY[2] = totalDeltaY - dY[0] - dY[1];
dZ[2] = totalDeltaZ - dZ[0] - dZ[1];
}
// --- 4.2.3. ЖЕСТКАЯ ЦЕЛОЧИСЛЕННАЯ ВАЛИДАЦИЯ БАЛАНСА ШАГОВ ---
// Проверяем, что сумма всех дельт сегментов математически тождественна общему вектору
// (Это защита на уровне компилятора. Если баланс сошелся — мы гарантированно не пропустим микрошаг)
long checkSumX = dX[0] + dX[1] + dX[2];
long checkSumY = dY[0] + dY[1] + dY[2];
long checkSumZ = dZ[0] + dZ[1] + dZ[2];
if (checkSumX != totalDeltaX || checkSumY != totalDeltaY || checkSumZ != totalDeltaZ) {
// Если из-за краевых эффектов округления где-то потерялся 1 микрошаг,
// принудительно корректируем его в сегменте торможения [2]
if (checkSumX != totalDeltaX) dX[2] += (totalDeltaX - checkSumX);
if (checkSumY != totalDeltaY) dY[2] += (totalDeltaY - checkSumY);
if (checkSumZ != totalDeltaZ) dZ[2] += (totalDeltaZ - checkSumZ);
}
// ====================================================================
// ТЕПЕРЬ У НАС ЕСТЬ ЖЕСТКИЕ ЦЕЛОЧИСЛЕННЫЕ ЗАДАЧИ ДЛЯ БРЕЗЕНХЕМА:
// Сегмент 0 (Разгон): сделать шаги dX[0], dY[0], dZ[0]
// Сегмент 1 (Марш): сделать шаги dX[1], dY[1], dZ[1]
// Сегмент 2 (Торможение): сделать шаги dX[2], dY[2], dZ[2]
// СУММА ДЕЛЬТ СЕГМЕНТОВ ВСЕГДА СТРОГО РАВНА ИСХОДНОМУ ВЕКТОРУ!
// ====================================================================
// ====================================================================
// СТРУКТУРНЫЙ БЛОК: ЧИСТАЯ ПОТАКТОВАЯ ОТРАБОТКА ПОДВЕКТОРОВ (Core 1)
// ====================================================================
// Базовая ЧПУ-сетка частоты планирования — 100 кГц (1 такт = 10 микросекунд)
const float BASE_CNC_FREQ = 100000.0f;
// Текущая физическая скорость вектора в начале кадра (мм/сек)
float currentVelocity = v_start;
localStepsX = plannerStepsX;
localStepsY = plannerStepsY;
localStepsZ = plannerStepsZ;
// Поочередно прогоняем 3 рассчитанных подвектора (Разгон, Марш, Торможение)
for (uint8_t segIdx = 0; segIdx < 3; segIdx++) {
long segDeltaX = dX[segIdx];
long segDeltaY = dY[segIdx];
long segDeltaZ = dZ[segIdx];
long segAbsX = labs(segDeltaX);
long segAbsY = labs(segDeltaY);
long segAbsZ = labs(segDeltaZ);
// Определяем, какая физическая ось является Мастер-Осью для текущего подвектора
long totalSegSteps = segAbsX;
float masterStepsPerMm = cfg.stepsPerMmX; // Цена шага Мастер-Оси
if (segAbsY > totalSegSteps) {
totalSegSteps = segAbsY;
masterStepsPerMm = cfg.stepsPerMmY;
}
if (segAbsZ > totalSegSteps) {
totalSegSteps = segAbsZ;
masterStepsPerMm = cfg.stepsPerMmZ;
}
// Если сегмент пустой (например, марш в треугольнике), просто идем дальше
if (totalSegSteps == 0) continue;
// Инициализация накопителей ошибки Брезенхема для этого подвектора
long errX = totalSegSteps / 2;
long errY = totalSegSteps / 2;
long errZ = totalSegSteps / 2;
uint8_t dirBitX = (segDeltaX >= 0) ? 1 : 0;
uint8_t dirBitY = (segDeltaY >= 0) ? 1 : 0;
uint8_t dirBitZ = (segDeltaZ >= 0) ? 1 : 0;
// --- ХОДОВОЙ ЦИКЛ ГЕНЕРАЦИИ ТАКТОВ ПОДВЕКТОРА ---
// Цикл итерируется строго по физическому количеству импульсов, которые нужно выдать
for (long step = 0; step < totalSegSteps; step++) {
// Вычисляем истинное количество шагов Master-оси на 1 мм пространственного пути.
// totalDistanceMm — полная длина 3D вектора в мм (уже посчитана при приеме кадра).
// totalSegSteps — полное количество шагов Master-оси в текущем сегменте/кадре.
float effectiveMasterStepsPerMm = (float)totalSegSteps / totalLength_mm;
// Защита от околонулевых значений (если кадр пустой)
if (effectiveMasterStepsPerMm < 0.001f) effectiveMasterStepsPerMm = 1.0f;
// Считаем частоту Master-оси на основе её реального вклада в пространственную траекторию:
float masterFrequency = currentVelocity * effectiveMasterStepsPerMm;
// Защита от деления на ноль: частота не может быть ниже 1 Гц (1 шаг в секунду)
if (masterFrequency < 1.0f) masterFrequency = 1.0f;
// 2. ПЕРЕВОД ФИЗИЧЕСКОЙ ЧАСТОТЫ В ДИСКРЕТНЫЕ КВАНТЫ ЧПУ-ЛЕНТЫ
// Делим базовую частоту планирования сетки (BASE_CNC_FREQ = 100 000 Гц) на частоту шагов,
// дополнительно умноженную на глобальный множитель подачи pythonFeedrateOverride.
// Пример при 100% подаче (1.0f): 100 000 / (10 000 * 1.0) = 10 тактов паузы на этот шаг.
uint16_t delay100kHz = (uint16_t)(BASE_CNC_FREQ / (masterFrequency * pythonFeedrateOverride));
// Аппаратная защита таймера: задержка не может быть меньше 1 такта сетки (10 микросекунд).
// Это ограничивает максимальную частоту импульсов на отметке 100 кГц, предотвращая зависание ISR.
if (delay100kHz < 1) delay100kHz = 1;
// 3. РАСЧЕТ РЕАЛЬНОГО ФИЗИЧЕСКОГО ВРЕМЕНИ ДЛИТЕЛЬНОСТИ ТЕКУЩЕГО ШАГА
// Переводим полученные такты обратно во float-секунды для физических формул разгона/торможения.
// Пример: 10 тактов / 100 000 Гц = 0.0001 секунды (100 микросекунд) длится этот конкретный шаг.
float stepDurationSec = (float)delay100kHz / BASE_CNC_FREQ;
// ====================================================================
// ДИНАМИЧЕСКИЙ ПЕРЕСЧЕТ СКОРОСТИ И АДАПТИВНЫЙ БАРЬЕР БЕЗОПАСНОСТИ
// ====================================================================
// --- СЦЕНАРИЙ А: СЕГМЕНТ РАЗГОНА (segIdx == 0) ---
if (segIdx == 0) {
// Вычисляем теоретический прирост скорости за время текущего шага по формуле: V = a * t
// Пример: 500 мм/сек2 (ускорение) * 0.0001 сек (время шага) = 0.05 мм/сек прироста
float deltaV = accel * stepDurationSec;
// РАСЧЕТ ПРОГРАММНОГО БАРЬЕРА БЕЗОПАСНОСТИ (Anti-Jerk Guard):
// Прирост скорости за ОДИН дискретный шаг не имеет права превысить половину
// от всей разницы между скоростью входа в кадр и маршевой (круизной) скоростью.
// Это защищает тяжелый портал от ударного срыва роторов на сверхвысоких частотах таймера.
float maxSafeDeltaV = (v_max - v_start) / 2.0f;
// Нижний барьер-предохранитель, чтобы рампа не застряла на околонулевых скоростях
if (maxSafeDeltaV < 0.1f) maxSafeDeltaV = 0.1f;
// Если математический расчет выдал слишком агрессивный прыжок скорости — зажимаем его по лимиту
if (deltaV > maxSafeDeltaV) deltaV = maxSafeDeltaV;
// Физически наращиваем текущую скорость вектора на безопасную величину дельты
currentVelocity += deltaV;
// Срезаем скорость по верхней границе маршевой подачи кадра (ограничение G-кода / CAM)
if (currentVelocity > v_max) currentVelocity = v_max;
}
// --- СЦЕНАРИЙ Б: СЕГМЕНТ ТОРМОЖЕНИЯ (segIdx == 2) ---
else if (segIdx == 2) {
// Вычисляем теоретическое падение скорости за время шага: V = a * t
float deltaV = accel * stepDurationSec;
// Симметричный барьер безопасности для торможения перед стыком со следующим кадром.
// Защищает от мгновенного удара и «втыкания» в угол при переходе на новую траекторию.
float maxSafeDeltaV = (v_max - v_end) / 2.0f;
if (maxSafeDeltaV < 0.1f) maxSafeDeltaV = 0.1f;
if (deltaV > maxSafeDeltaV) deltaV = maxSafeDeltaV;
// Физически гасим текущую скорость вектора
currentVelocity -= deltaV;
// Срезаем скорость по нижней границе, заданной Python Look-Ahead планировщиком (скорость выхода)
if (currentVelocity < v_end) currentVelocity = v_end;
}
// --- СЦЕНАРИЙ В: СЕГМЕНТ МАРШЕВОГО КРУИЗА (segIdx == 1) ---
else {
// На маршевом участке скорость станка строго стабильна и равна максимальной подаче кадра
currentVelocity = v_max;
}
// ====================================================================
// БЛОК 3: ИЗОЛИРОВАННЫЙ ЦЕЛОЧИСЛЕННЫЙ АЛГОРИТМ БРЕЗЕНХЕМА (Core 1)
// ====================================================================
// Объявляем локальные битовые флаги-триггеры для этого конкретного шага.
// 1 = в этот такт нужно физически выдать импульс STEP, 0 = ось стоит на месте.
uint8_t issueX = 0;
uint8_t issueY = 0;
uint8_t issueZ = 0;
// --- ЛИНЕЙНАЯ ИНТЕРПОЛЯЦИЯ ОСИ X ---
// Вычитаем из накопителя ошибки Брезенхема (errX) модуль перемещения текущей оси (segAbsX)
errX -= segAbsX;
// Если ошибка упала ниже нуля — пороговое значение пересечено, пора делать физический шаг!
if (errX < 0) {
errX += totalSegSteps; // Возвращаем накопитель ошибки в рабочую зону ведущей оси
issueX = 1; // Взводим флаг генерации импульса STEP для прерывания
// Обновляем виртуальную координату планировщика в ОЗУ в зависимости от знака направления
localStepsX += (dirBitX == 1) ? 1 : -1;
}
// --- ЛИНЕЙНАЯ ИНТЕРПОЛЯЦИЯ ОСИ Y ---
// Математика полностью идентична оси X, но выполняется параллельно на независимых регистрах
errY -= segAbsY;
if (errY < 0) {
errY += totalSegSteps;
issueY = 1;
localStepsY += (dirBitY == 1) ? 1 : -1;
}
// --- ЛИНЕЙНАЯ ИНТЕРПОЛЯЦИЯ ОСИ Z ---
errZ -= segAbsZ;
if (errZ < 0) {
errZ += totalSegSteps;
issueZ = 1;
localStepsZ += (dirBitZ == 1) ? 1 : -1;
}
// ====================================================================
// БЛОК 4: ЗАПОЛНЕНИЕ БУФЕРА СЕТКИ ВРЕМЕНИ (Внутренний цикл паузы)
// ====================================================================
// Цикл крутится ровно delay100kHz раз, нарезая 10-микросекундные кванты времени.
// Если delay100kHz равен 10, то мы запишем 1 такт с импульсом и 9 тактов чистой паузы.
for (uint16_t t = 0; t < delay100kHz; t++) {
// 1. КОНТРОЛЬ ПЕРЕПОЛНЕНИЯ И ЗАЩИТА ОЧЕРЕДИ FREE ROOT
// Вычисляем виртуальный следующий индекс для головы кольцевого буфера
uint32_t nextHead = (bufferHead + 1) % STEP_BUFFER_SIZE;
// Если голова догнала хвост (буфер ОЗУ забит на все 40.96 мс вперед)
while (nextHead == bufferTail) {
// Уступаем квант времени операционной системе FreeRTOS на 1 мс.
// Прерывание таймера на Core 1 продолжит читать буфер и освободит место.
vTaskDelay(pdMS_TO_TICKS(1));
}
// 2. ИНИЦИАЛИЗАЦИЯ ЧИСТОЙ БИНАРНОЙ КОМАНДЫ ВРЕМЕНИ
// Создаем пустую микроструктуру (по умолчанию все шаги равны 0, valid = 0)
StepCmd cmd = { 0, 0, 0, 0, 0, 0, 0 };
// 3. ПОТАКТОВОЕ ФОРМИРОВАНИЕ ПЕРЕДНЕГО ФРОНТА (Только в первый квант)
// Физический импульс шага и биты направлений закладываются СТРОГО в самый первый такт (t == 0)
if (t == 0) {
// Подмешиваем флаги и направления, которые рассчитал Брезенхем в Блоке 3
if (issueX) { cmd.stepX = 1; cmd.dirX = dirBitX; }
if (issueY) { cmd.stepY = 1; cmd.dirY = dirBitY; }
if (issueZ) { cmd.stepZ = 1; cmd.dirZ = dirBitZ; }
}
// Во всех остальных итерациях (при t > 0) поля cmd.stepX/Y/Z остаются равными 0.
// Это и формирует необходимую временную паузу между шагами моторов!
// Взводим флаг готовности ячейки. Аппаратный таймер теперь имеет право её прочесть.
cmd.valid = 1;
// Физически записываем структуру в оперативную память кольцевого буфера
stepRingBuffer[bufferHead] = cmd;
// Сдвигаем индекс головы кольца вперед
bufferHead = nextHead;
}
} // Конец цикла по шагам текущего подвектора
} // Конец цикла по 3-м подвекторам
isVectorMoving = true; // Жестко взводим флаг физического старта движения!
// Фиксируем эталонную базу
plannerStepsX = targetStepsX;
plannerStepsY = targetStepsY;
plannerStepsZ = targetStepsZ;
vTaskDelay(pdMS_TO_TICKS(1));
}
}
// ============================================================================
// СТРУКТУРНЫЙ БЛОК: ВЫСОКОСКОРОСТНОЕ ДВУХФАЗНОЕ ПРЕРЫВАНИЕ ТАЙМЕРА (200 кГц)
// ============================================================================
// Флаг фазы таймера: true = нечетный такт (HIGH), false = четный такт (LOW)
static volatile bool timerPhaseHigh = true;
void IRAM_ATTR onTimerInterrupt() {
// Проверяем критические аварии, которые должны сработать ВСЕГДА
bool criticalAlarm = (digitalRead(ESTOP_PIN) == HIGH || digitalRead(MOTORS_ALARM) == HIGH);
// Линию концевиков подмешиваем к аварии ТОЛЬКО если сейчас не идет процедура хоуминга
if (!isHomingActive && digitalRead(ENDSTOPS_PIN) == HIGH) {
criticalAlarm = true;
}
if (criticalAlarm) {
digitalWrite(MOTORS_ENABLE, HIGH); // Немедленно обесточить драйверы Leadshine
// Жестко сбрасываем буфер времени и останавливаем станок
bufferTail = bufferHead;
isVectorMoving = false; // Мгновенно гасим физический статус движения
isPlannerBusy = false; // Освобождаем конвейер данных
timerPhaseHigh = true; // Сбрасываем фазу
// Переводим станок в аварийный стейт ALARM на Ядре 0
changeState(STATE_ALARM);
return;
}
// --- 5.2. ФАЗА 2: ЧЕТНЫЙ ТАКТ ТАЙМЕРА (СПАД ИМПУЛЬСА STEP) ---
if (!timerPhaseHigh) {
// Вслепую, за наносекунды опускаем все пины STEP в LOW
// Процессор не висит в delayMicroseconds, освобождая 20% мощности ядра!
digitalWrite(X_STEP_PIN, LOW);
digitalWrite(Y_STEP_PIN, LOW);
digitalWrite(Z_STEP_PIN, LOW);
timerPhaseHigh = true; // Переключаем триггер на следующую фазу HIGH
return; // Мгновенно выходим из прерывания
}
// --- 5.3. ФАЗА 1: НЕЧЕТНЫЙ ТАКТ ТАЙМЕРА (ФОРМИРОВАНИЕ ФРОНТА STEP) ---
// Мы зашли сюда строго раз в 10 микросекунд (эквивалент базовой частоты ЧПУ 100 кГц)
timerPhaseHigh = false; // Следующий тик таймера гарантированно пойдет в фазу LOW
// Если в кольцевом буфере есть готовые такты времени от планировщика
if (bufferTail != bufferHead) {
StepCmd &cmd = stepRingBuffer[bufferTail];
if (cmd.valid) {
bool stepIssued = false;
// --- Ось X ---
if (cmd.stepX) {
digitalWrite(X_DIR_PIN, cmd.dirX ? HIGH : LOW);
digitalWrite(X_STEP_PIN, HIGH);
currentStepsX += cmd.dirX ? 1 : -1; // Честный целочисленный инкремент ЧПУ
stepIssued = true;
}
// --- Ось Y ---
if (cmd.stepY) {
digitalWrite(Y_DIR_PIN, cmd.dirY ? HIGH : LOW);
digitalWrite(Y_STEP_PIN, HIGH);
currentStepsY += cmd.dirY ? 1 : -1;
stepIssued = true;
}
// --- Ось Z ---
if (cmd.stepZ) {
digitalWrite(Z_DIR_PIN, cmd.dirZ ? HIGH : LOW);
digitalWrite(Z_STEP_PIN, HIGH);
currentStepsZ += cmd.dirZ ? 1 : -1;
stepIssued = true;
}
// Теперь эта ячейка считается пустой и планировщик на Ядре 1 сможет записать сюда новый такт
cmd.valid = 0;
// Такт времени (10 мкс) успешно отработан — сдвигаем указатель чтения кольца!
bufferTail = (bufferTail + 1) % STEP_BUFFER_SIZE;
} else {
// Защита от битых или неполных ячеек
bufferTail = (bufferTail + 1) % STEP_BUFFER_SIZE;
}
} else {
// Если буфер пуст (станок приехал в цель и замер в IDLE)
isVectorMoving = false;
}
}
// ============================================================================
// СТРУКТУРНЫЙ БЛОК: АВТОМАТИЧЕСКИЙ ПОИСК БАЗ СТАНКА (HOMING)
// ============================================================================
// Функция поиска базы для одной конкретной оси через кольцевой буфер времени
bool homeAxis(int stepPin, int dirPin, int dirSign, volatile long &axisSteps, int endstopPin, long pullOffSteps) {
const uint16_t HOMING_DELAY_TICKS = 50; // Жесткая задержка скорости хоуминга (эквивалент ~2 кГц)
isHomingActive = true;
// Шаг 1: Едем в сторону датчика до физического срабатывания
while (digitalRead(endstopPin) == LOW) {
// Если оператор нажал E-STOP во время поиска баз — немедленно выходим
if (digitalRead(ESTOP_PIN) == HIGH) return false;
uint32_t nextHead = (bufferHead + 1) % STEP_BUFFER_SIZE;
while (nextHead == bufferTail) { vTaskDelay(pdMS_TO_TICKS(1)); }
// Формируем потактовую пачку времени для одного шага хоуминга
for (uint16_t t = 0; t < HOMING_DELAY_TICKS; t++) {
StepCmd cmd = {0, 0, 0, 0, 0, 0, 0};
if (t == 0) {
if (stepPin == X_STEP_PIN) { cmd.stepX = 1; cmd.dirX = (dirSign > 0) ? 1 : 0; }
if (stepPin == Y_STEP_PIN) { cmd.stepY = 1; cmd.dirY = (dirSign > 0) ? 1 : 0; }
if (stepPin == Z_STEP_PIN) { cmd.stepZ = 1; cmd.dirZ = (dirSign > 0) ? 1 : 0; }
}
cmd.valid = 1;
stepRingBuffer[bufferHead] = cmd;
bufferHead = (bufferHead + 1) % STEP_BUFFER_SIZE;
}
}
// Ждем, пока таймер полностью выгребет остатки шагов из буфера и мотор физически замрет
while (bufferTail != bufferHead) { vTaskDelay(pdMS_TO_TICKS(1)); }
vTaskDelay(pdMS_TO_TICKS(100)); // Короткая пауза для гашения инерции фермы
// Шаг 2: Обязательный целочисленный отъезд назад (Pull-off) для освобождения датчика
for (long i = 0; i < pullOffSteps; i++) {
uint32_t nextHead = (bufferHead + 1) % STEP_BUFFER_SIZE;
while (nextHead == bufferTail) { vTaskDelay(pdMS_TO_TICKS(1)); }
for (uint16_t t = 0; t < HOMING_DELAY_TICKS; t++) {
StepCmd cmd = {0, 0, 0, 0, 0, 0, 0};
if (t == 0) {
// Едем строго в противоположную сторону (разворачиваем dirSign)
if (stepPin == X_STEP_PIN) { cmd.stepX = 1; cmd.dirX = (dirSign > 0) ? 0 : 1; }
if (stepPin == Y_STEP_PIN) { cmd.stepY = 1; cmd.dirY = (dirSign > 0) ? 0 : 1; }
if (stepPin == Z_STEP_PIN) { cmd.stepZ = 1; cmd.dirZ = (dirSign > 0) ? 0 : 1; }
}
cmd.valid = 1;
stepRingBuffer[bufferHead] = cmd;
bufferHead = (bufferHead + 1) % STEP_BUFFER_SIZE;
}
}
// Снова ждем полной физической остановки
while (bufferTail != bufferHead) { vTaskDelay(pdMS_TO_TICKS(1)); }
// Жестко зануляем физический и математический счетчик оси! База официально найдена.
axisSteps = 0;
isHomingActive = false;
return true;
}
// Главный последовательный автомат поиска баз ЧПУ (Вызывается из handleIdleCommands на Core 0)
void runFullHoming() {
changeState(STATE_HOMING);
isHomed = false;
// 1. Первой всегда строго вверх уходит безопасная ось Z, чтобы не сломать фрезу об прижимы
if (!homeAxis(Z_STEP_PIN, Z_DIR_PIN, cfg.homingDirZ, currentStepsZ, ENDSTOPS_PIN, cfg.pullOffZ)) {
changeState(STATE_ALARM); return;
}
plannerStepsZ = 0; // Синхронизируем базу планировщика
// 2. Затем в базовый угол синхронно или последовательно уходят оси X и Y
if (!homeAxis(X_STEP_PIN, X_DIR_PIN, cfg.homingDirX, currentStepsX, ENDSTOPS_PIN, cfg.pullOffX)) {
changeState(STATE_ALARM); return;
}
plannerStepsX = 0;
if (!homeAxis(Y_STEP_PIN, Y_DIR_PIN, cfg.homingDirY, currentStepsY, ENDSTOPS_PIN, cfg.pullOffY)) {
changeState(STATE_ALARM); return;
}
plannerStepsY = 0;
// Все базы успешно привязаны
isHomed = true;
changeState(STATE_IDLE);
Serial.println("STATUS: Homing cycle completed successfully. Machine unlocked.");
}
// ============================================================================
// СТРУКТУРНЫЙ БЛОК: ПЛАВНЫЙ FEEDRATE OVERRIDE И РАМПА ПАУЗЫ (Core 0)
// ============================================================================
void updateFeedrateFading() {
static unsigned long lastFadeTime = 0;
unsigned long now = millis();
// Выдерживаем шаг изменения рампы раз в 5 миллисекунд
if (now - lastFadeTime < 5) return;
lastFadeTime = now;
// Шаг изменения коэффициента за 5 мс, чтобы вся рампа от 0% до 100% длилась ровно 300 мс
const float FADE_STEP = 5.0f / 300.0f; // ~0.0166 на один шаг
// СЦЕНАРИЙ А: Нажата кнопка Паузы (HOLD). Плавно гасим скорость до полного нуля
if (currentMachineState == STATE_HOLD) {
if (pythonFeedrateOverride > 0.0f) {
pythonFeedrateOverride -= FADE_STEP;
if (pythonFeedrateOverride < 0.0f) {
pythonFeedrateOverride = 0.0f; // Станок полностью замер, но буфер и траектория сохранены!
}
}
}
// СЦЕНАРИЙ Б: Работает автоматическая программа. Плавно возвращаем скорость к 100% (или значению слайдера)
else if (currentMachineState == STATE_RUNNING) {
// Здесь целевое значение берется из глобальных настроек ползунка (по умолчанию 1.0f)
float targetOverride = cfg.feedMultiplier;
if (pythonFeedrateOverride < targetOverride) {
pythonFeedrateOverride += FADE_STEP;
if (pythonFeedrateOverride > targetOverride) pythonFeedrateOverride = targetOverride;
}
else if (pythonFeedrateOverride > targetOverride) {
pythonFeedrateOverride -= FADE_STEP;
if (pythonFeedrateOverride < targetOverride) pythonFeedrateOverride = targetOverride;
}
}
}
void startMotionTask() {
// Создаем изолированную задачу для Ядра 1 с наивысшим приоритетом 24
xTaskCreatePinnedToCore(
motionTask, // Функция задачи
"motionTask", // Имя задачи
8192, // Размер стека (выделяем с запасом под float-вычисления)
NULL, // Параметры
24, // Приоритет
NULL, // Хэндл
1 // Жесткая привязка к Core 1
);
}