Загрузка данных
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));
}
}