ESP32, STM32 и Arduino: как заставить нейросеть писать прошивки, которые не виснут на первой же миллисекунде
Embedded-разработка — это мир, где ошибка в один такт может превратить ваш девайс в кирпич, а неверная настройка тактирования — в бесконечный HardFault_Handler. И вот парадокс: нейросети уверенно генерируют код для веб-приложений, но когда дело доходит до регистров STM32 или DMA-трансферов на ESP32, начинается лотерея. Модель может предложить инициализацию таймера, которая компилируется, но не работает, или пропустить критичный volatile в обработчике прерывания.
Проблема не в том, что AI «не умеет» embedded. Проблема в том, что большинство промтов написаны так, будто мы просим написать функцию сортировки массива. Embedded требует контекста: конкретный чип, версия SDK, частота тактирования, требования по энергопотреблению. Без этого нейросеть галлюцинирует регистры, путает HAL с LL, а иногда предлагает API, которого не существует в природе.
Ниже — подборка промтов, которые я собрал и отладил на реальных проектах: от прототипов на Arduino до промышленных задач на STM32 с FreeRTOS. Каждый промт проверен на практике, но помните: AI — это ускоритель, а не замена даташиту. Всегда сверяйтесь с reference manual вашего конкретного чипа.
Промт 1: Инициализация периферии STM32 без магии CubeMX
Задача: Когда нужно настроить периферию вручную или понять, что именно сгенерировал CubeMX.
Промт:
Ты — эксперт по STM32 HAL. Напиши инициализацию TIM2 в режиме PWM для STM32F407VG.
Требования:
- Тактирование APB1 = 84 МГц
- Частота PWM = 1 кГц
- Скважность 50%
- Канал 1, пин PA0 (AF1)
- Использовать только HAL, без LL
Объясни расчёт prescaler и period. Покажи код инициализации GPIO и TIM.
Что получите: Код с корректным расчётом Prescaler = (SystemCoreClock / (2 * APB1_Timer_Clock)) - 1 и Period = (APB1_Timer_Clock / PWM_Freq) - 1. Модель покажет, почему для STM32F4 таймеры на APB1 получают удвоенную частоту.
Пример вывода:
// APB1 Timer Clock = 84 MHz (при APB1 prescaler > 1)
htim2.Instance = TIM2;
htim2.Init.Prescaler = 84 - 1; // 84 MHz / 84 = 1 MHz
htim2.Init.Period = 1000 - 1; // 1 MHz / 1000 = 1 kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 50% duty
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
Совет: Если модель путает APB1_Timer_Clock, уточните: «Проверь, включён ли TIM2 на APB1 или APB2 в твоём чипе. У STM32F407 TIM2 на APB1». Это заставит её свериться с reference manual.
Промт 2: Обработчик прерываний, который не ломает систему
Задача: Написать ISR для кнопки с антидребезгом без блокировки main loop.
Промт:
Напиши обработчик внешнего прерывания (EXTI) для кнопки на PA1 (STM32F103, HAL).
Требования:
- Антидребезг через таймер (не delay в ISR)
- Флаг события для main loop (volatile)
- Учитывать, что ISR должна быть максимально короткой
- Показать, как сбросить EXTI pending bit
Почему это работает: Модель знает, что HAL_Delay в ISR — это ошибка. Она сгенерирует установку флага и запуск таймера, а не блокирующий код.
Пример:
volatile uint8_t button_pressed = 0;
void HAL_GPIO_EXTI_Callback(uint16_t pin) {
if (pin == GPIO_PIN_1) {
// Запускаем таймер антидребезга на 50 мс
__HAL_TIM_SET_COUNTER(&htim3, 0);
HAL_TIM_Base_Start_IT(&htim3);
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM3) {
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET) {
button_pressed = 1;
}
HAL_TIM_Base_Stop_IT(&htim3);
}
}
Важно: Всегда проверяйте, что HAL_GPIO_EXTI_Callback вызывается из правильного контекста. Нейросеть иногда забывает про __HAL_GPIO_EXTI_CLEAR_IT, но если попросить явно — добавит.
Промт 3: ESP32 + FreeRTOS — задачи, которые не конфликтуют
Задача: Создать две задачи с разными приоритетами и обменом данными через очередь.
Промт:
ESP-IDF v5.x, ESP32. Создай две задачи FreeRTOS:
1. Задача считывает данные с датчика DHT22 каждые 2 секунды (приоритет 5)
2. Задача отправляет данные по Wi-Fi раз в 10 секунд (приоритет 3)
Обмен через queue. Покажи создание задач, очередь, обработку ошибок.
Учти watchdog и не блокируй scheduler.
Что получите: Код с xQueueCreate, xTaskCreatePinnedToCore (важно для ESP32 — можно закрепить задачи за ядрами), корректными задержками через vTaskDelay.
Пример:
QueueHandle_t sensor_queue;
typedef struct {
float temperature;
float humidity;
} sensor_data_t;
void sensor_task(void *pvParameters) {
sensor_data_t data;
while (1) {
if (dht_read_data(&data) == ESP_OK) {
xQueueSend(sensor_queue, &data, portMAX_DELAY);
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void wifi_task(void *pvParameters) {
sensor_data_t received;
while (1) {
if (xQueueReceive(sensor_queue, &received, pdMS_TO_TICKS(10000))) {
send_to_server(&received);
}
}
}
void app_main() {
sensor_queue = xQueueCreate(5, sizeof(sensor_data_t));
xTaskCreatePinnedToCore(sensor_task, "sensor", 4096, NULL, 5, NULL, 0);
xTaskCreatePinnedToCore(wifi_task, "wifi", 4096, NULL, 3, NULL, 1);
}
Совет: Уточните в промте «Покажи, как избежать priority inversion». Модель добавит мьютекс или объяснит, почему очередь здесь безопасна.
Промт 4: Отладка HardFault на STM32
Задача: Когда плата уходит в HardFault, а вы не понимаете почему.
Промт:
STM32F4, HardFault_Handler срабатывает случайно. Напиши код для извлечения причины:
- Чтение регистров CFSR, HFSR, MMFAR, BFAR
- Вывод через SWO или UART
- Расшифровка битов (IMPRECISERR, PRECISERR, IBUSERR и т.д.)
Покажи, как встроить это в обработчик.
Пример вывода:
void HardFault_Handler(void) {
volatile uint32_t cfsr = SCB->CFSR;
volatile uint32_t hfsr = SCB->HFSR;
volatile uint32_t mmfar = SCB->MMFAR;
volatile uint32_t bfar = SCB->BFAR;
printf("CFSR: 0x%08lX\n", cfsr);
printf("HFSR: 0x%08lX\n", hfsr);
if (cfsr & (1 << 1)) printf("DACCVIOL: Data access violation\n");
if (cfsr & (1 << 9)) printf("PRECISERR: Precise data bus error at 0x%08lX\n", bfar);
while (1);
}
Кейс: На одном проекте модель помогла выявить, что HardFault возникал из-за доступа к неинициализированному указателю на структуру DMA. Расшифровка CFSR показала IMPRECISERR, что сузило поиск до работы с периферией.
Промт 5: Работа с I2C на Arduino без блокировок
Задача: Чтение нескольких датчиков по I2C без зависаний.
Промт:
Arduino (AVR, Wire library). Напиши неблокирующее чтение датчика BMP280 по I2C.
Требования:
- Проверка наличия устройства через Wire.endTransmission()
- Таймаут на чтение (если датчик не отвечает)
- Обработка ошибок без while(1)
Покажи код с комментариями.
Пример:
#include <Wire.h>
#define BMP280_ADDR 0x76
bool read_bmp280(float *temp) {
Wire.beginTransmission(BMP280_ADDR);
Wire.write(0xFA); // регистр температуры
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom(BMP280_ADDR, 3) != 3) return false;
uint32_t raw = (Wire.read() << 12)
| (Wire.read() << 4) | (Wire.read() >> 4);
*temp = raw / 100.0; // упрощённо, нужна калибровка
return true;
}
void loop() {
float temp;
if (read_bmp280(&temp)) {
Serial.println(temp);
}
delay(1000);
}
Совет: Нейросеть часто забывает про endTransmission(false) для repeated start. Уточните: «Используй repeated start, не завершай передачу перед чтением».
Промт 6: Энергосбережение на ESP32 — deep sleep с пробуждением по таймеру
Задача: Устройство на батарейке должно работать месяцами.
Промт:
ESP32, ESP-IDF. Настрой deep sleep с пробуждением по таймеру каждые 10 минут.
Требования:
- Отключение Wi-Fi перед сном
- Сохранение данных в RTC memory
- Пробуждение, отправка данных, снова сон
Покажи код с расчётом времени сна.
Пример:
RTC_DATA_ATTR int boot_count = 0;
void app_main() {
boot_count++;
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
if (cause == ESP_SLEEP_WAKEUP_TIMER) {
// Проснулись по таймеру — отправляем данные
wifi_init_and_send();
}
esp_wifi_stop();
esp_deep_sleep_enable_timer_wakeup(10 * 60 * 1000000ULL); // 10 минут
esp_deep_sleep_start();
}
Важно: RTC_DATA_ATTR сохраняет переменные между пробуждениями. Модель это знает, но иногда забывает про esp_wifi_stop() перед сном — уточните явно.
Промт 7: АЦП на STM32 с DMA и усреднением
Задача: Читать аналоговый сигнал без нагрузки на CPU.
Промт:
STM32F4, HAL. Настрой ADC1 в режиме continuous conversion с DMA.
Требования:
- Буфер на 100 значений
- Усреднение в main loop
- Тактирование ADC = 21 МГц (APB2 / 4)
Покажи инициализацию и обработку.
Пример:
#define ADC_BUFFER_SIZE 100
uint16_t adc_buffer[ADC_BUFFER_SIZE];
// В main:
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);
// Обработка:
uint32_t sum = 0;
for (int i = 0; i < ADC_BUFFER_SIZE; i++) {
sum += adc_buffer[i];
}
uint16_t avg = sum / ADC_BUFFER_SIZE;
Совет: Если модель путает HAL_ADC_Start_DMA с HAL_ADC_Start_IT, попросите: «Покажи разницу между DMA и IT режимами для ADC».
Промт 8: UART с кольцевым буфером на Arduino
Задача: Принимать данные без потерь на высокой скорости.
Промт:
Arduino Mega, UART на 115200 бод. Реализуй кольцевой буфер для приёма.
Требования:
- Размер буфера 64 байта
- Обработка переполнения
- Чтение в main loop без блокировки
Покажи код с ISR.
Пример:
#define BUF_SIZE 64
volatile uint8_t rx_buffer[BUF_SIZE];
volatile uint8_t rx_head = 0, rx_tail = 0;
ISR(USART0_RX_vect) {
uint8_t data = UDR0;
uint8_t next = (rx_head + 1) % BUF_SIZE;
if (next != rx_tail) {
rx_buffer[rx_head] = data;
rx_head = next;
}
}
bool uart_available() { return rx_head != rx_tail; }
uint8_t uart_read() {
uint8_t data = rx_buffer[rx_tail];
rx_tail = (rx_tail + 1) % BUF_SIZE;
return data;
}
Промт 9: Протокол Modbus RTU на STM32
Задача: Подключить промышленный датчик.
Промт:
STM32F103, HAL. Реализуй Modbus RTU slave на UART2.
Требования:
- Функция 0x03 (Read Holding Registers)
- CRC16 расчёт
- Таймаут 3.5 символа для определения конца кадра
Покажи код с обработкой.
Пример CRC:
uint16_t modbus_crc(uint8_t *buf, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= buf[i];
for (int j = 0; j < 8; j++) {
if (crc & 1) crc = (crc >> 1) ^ 0xA001;
else crc >>= 1;
}
}
return crc;
}
Кейс: На производстве модель сгенерировала код, который корректно обрабатывал запросы, но не учитывал таймаут. После уточнения промта добавилась проверка через HAL_GetTick().
Промт 10: Прошивка ESP32 по OTA
Задача: Обновлять устройство удалённо.
Промт:
ESP-IDF v5.x. Реализуй OTA-обновление через HTTPS.
Требования:
- Проверка версии
- Откат при ошибке
- Прогресс-бар через callback
Покажи код с esp_https_ota.
Пример:
esp_http_client_config_t config = {
.url = "https://example.com/firmware.bin",
.cert_pem = server_cert_pem,
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
esp_restart();
} else {
ESP_LOGE(TAG, "OTA failed");
}
Совет: Уточните «Добавь проверку подписи прошивки». Модель добавит esp_ota_get_app_description и проверку хеша.
Промт 11: Таймеры STM32 — Input Capture для измерения частоты
Задача: Измерить частоту внешнего сигнала.
Промт:
STM32F4, HAL. Настрой TIM1 в режиме Input Capture на канале 1 (PA8).
Требования:
- Измерение периода между фронтами
- Расчёт частоты
- Обработка переполнения таймера
Покажи код с обработчиком прерывания.
Пример:
volatile uint32_t capture1 = 0, capture2 = 0;
volatile float freq = 0;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
capture2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
if (capture2 > capture1) {
uint32_t diff = capture2 - capture1;
freq = 84000000.0 / diff; // тактовая частота таймера
}
capture1 = capture2;
}
}
Промт 12: Отладка через SWO на STM32
Задача: Логи без UART.
Промт:
STM32F4, SWO вывод через ITM. Настрой вывод printf в SWO.
Покажи инициализацию и функцию _write.
Пример:
int _write(int file, char *ptr, int len) {
for (int i = 0; i < len; i++) {
ITM_SendChar(*ptr++);
}
return len;
}
Совет: В STM32CubeIDE нужно включить SWO в настройках debug. Модель это знает, но лучше уточнить: «Покажи настройки в CubeIDE».
Промт 13: Питание от батареи — расчёт времени работы
Задача: Оценить, сколько проживёт устройство.
Промт:
ESP32, deep sleep 10 минут, активность 2 секунды, средний ток 80 мА в активном режиме, 10 мкА в сне.
Батарея 2000 мАч. Рассчитай время работы.
Покажи формулу и результат.
Пример расчёта:
Активная фаза: 2 сек * 80 мА = 160 мА*с
Сон: 598 сек * 0.01 мА = 5.98 мА*с
Итого за цикл: 165.98 мА*с = 0.046 мА*ч
Время работы: 2000 / 0.046 ≈ 43478 циклов ≈ 302 дней
Важно: Модель может ошибиться в единицах. Перепроверьте расчёт вручную.
Промт 14: Код для датчика с I2C на ESP32
Задача: Подключить BME280.
Промт:
ESP-IDF, ESP32. Напиши драйвер для BME280 по I2C.
Требования:
- Чтение температуры, давления, влажности
- Калибровочные коэффициенты
- Проверка ID (0x60)
Покажи код с i2c_master.
Пример:
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (BME280_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, 0xD0, true); // регистр ID
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (BME280_ADDR << 1) | I2C_MASTER_READ, true);
uint8_t id;
i2c_master_read_byte(cmd, &id, I2C_MASTER_NACK);
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_NUM_0, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
Совет: Нейросеть иногда путает i2c_master_read_byte с i2c_master_read. Уточните версию ESP-IDF.
Промт 15: Проверка кода на типичные ошибки embedded
Задача: Найти баги до прошивки.
Промт:
Проверь этот код для STM32 на типичные ошибки embedded:
- Отсутствие volatile в переменных, изменяемых в ISR
- Блокирующие вызовы в прерываниях
- Неверная инициализация периферии
- Утечки памяти (если есть malloc)
Вот код: [вставьте код]
Пример вывода: Модель укажет: «Переменная flag изменяется в ISR, но не объявлена volatile — компилятор может оптимизировать проверку в main loop».
Кейс: На одном проекте этот промт выявил, что HAL_UART_Transmit вызывается внутри HAL_UART_RxCpltCallback — это блокирует систему. Заменили на очередь.
Что важно помнить при работе с AI в embedded
- Всегда проверяйте регистры. Модель может выдумать бит, которого нет в вашем чипе. Сверяйтесь с reference manual.
- Указывайте версию SDK. ESP-IDF v4 и v5 имеют разные API. HAL для F1 и F4 тоже отличается.
- Тестируйте на железе. Код, который компилируется, не всегда работает. Особенно это касается таймингов и прерываний.
- Не доверяйте расчётам. Перепроверяйте prescaler, период, время работы от батареи.
- Используйте промты для отладки. HardFault, зависания, неверные данные — AI помогает сузить поиск.
Embedded — это область, где AI пока не заменит инженера, но может стать мощным ускорителем. Главное — давать контекст и проверять результат. Если вы только начинаете, начните с простых промтов: инициализация GPIO, UART, таймеров. Постепенно переходите к DMA, RTOS, OTA. И помните: лучший код — тот, который вы понимаете, а не тот, который сгенерировала нейросеть.
Попробуйте эти промты на своём следующем проекте. Начните с одного — например, с инициализации таймера. Сравните с тем, что генерирует CubeMX. Вы увидите, где AI ошибается, а где экономит часы работы. И тогда вы сможете использовать его осознанно, без выгорания и без кирпичей.
Комментарии