ci: trigger
ci: rerun ci: rerun ci: rerun3 ci: rerun4 ci: rerun5 ci: rerun6 Update ci.yml Update ci.yml Update ci.yml №1 ci.yml Update ci.yml
This commit is contained in:
parent
ab8bdeeb2f
commit
a1c6b1a30f
7 changed files with 285 additions and 153 deletions
108
.github/workflows/ci.yml
vendored
Normal file
108
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build in devcontainer
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build devcontainer image with cache
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: .devcontainer/Dockerfile
|
||||
tags: tft-devcontainer-ci:latest
|
||||
load: true
|
||||
cache-from: type=gha,scope=tft-devcontainer
|
||||
cache-to: type=gha,mode=max,scope=tft-devcontainer
|
||||
|
||||
- name: Show tool versions in container
|
||||
run: |
|
||||
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace tft-devcontainer-ci:latest bash -lc 'just --version && cmake --version && ninja --version && arm-none-eabi-gcc --version | head -n 1 && uv --version'
|
||||
|
||||
- name: Sync host Python tools inside container
|
||||
run: |
|
||||
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace tft-devcontainer-ci:latest bash -lc 'cd tools/host && uv sync'
|
||||
|
||||
- name: Run CI build inside container
|
||||
run: |
|
||||
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace tft-devcontainer-ci:latest bash -lc 'just ci::build'
|
||||
|
||||
- name: Upload build directory
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-tree
|
||||
path: build/
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
test:
|
||||
name: Host tests in devcontainer
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build devcontainer image with cache
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: .devcontainer/Dockerfile
|
||||
tags: tft-devcontainer-ci:latest
|
||||
load: true
|
||||
cache-from: type=gha,scope=tft-devcontainer
|
||||
cache-to: type=gha,mode=max,scope=tft-devcontainer
|
||||
|
||||
- name: Download build directory artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-tree
|
||||
path: build/
|
||||
|
||||
- name: Sync host Python tools inside container
|
||||
run: |
|
||||
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace tft-devcontainer-ci:latest bash -lc 'cd tools/host && uv sync'
|
||||
|
||||
- name: Run host tests inside container
|
||||
run: |
|
||||
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace tft-devcontainer-ci:latest bash -lc 'just ci::test'
|
||||
|
||||
- name: Upload test logs and build directory
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-artifacts
|
||||
path: |
|
||||
build/
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
|
@ -1,4 +1 @@
|
|||
---
|
||||
# Vendored / сгенерированный код — не анализируем вообще
|
||||
InheritParentConfig: false
|
||||
Checks: "-*"
|
||||
|
|
@ -186,6 +186,8 @@
|
|||
kFLEXSPI_Command_READ_SDR, kFLEXSPI_4PAD, 0x04U), \
|
||||
}
|
||||
|
||||
static const uint8_t BYTE_MASK = 0xFFU;
|
||||
|
||||
/** @brief LUT для W25Q64/128 — стандартные 3-byte opcodes. */
|
||||
static const uint32_t K_LUT_3B[LUT_TOTAL_WORDS] =
|
||||
BUILD_LUT(0x20U, 0x52U, 0xD8U, 0x32U, 0x6BU, ADDR_BITS_24);
|
||||
|
|
@ -204,11 +206,11 @@ static uint32_t g_s_flash_size = 0U; /**< Размер Flash в байтах.
|
|||
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
|
||||
AT_QUICKACCESS_SECTION_CODE(static uint32_t qspi_irq_lock(void))
|
||||
{
|
||||
const uint32_t primask = __get_PRIMASK();
|
||||
const uint32_t PRIMASK = __get_PRIMASK();
|
||||
__disable_irq();
|
||||
__DSB();
|
||||
__ISB();
|
||||
return primask;
|
||||
return PRIMASK;
|
||||
}
|
||||
|
||||
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
|
||||
|
|
@ -309,15 +311,15 @@ AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_chunk(uint8_t *p_dst, uint
|
|||
} while ((intr & (uint32_t) kFLEXSPI_IpRxFifoWatermarkAvailableFlag) == 0U);
|
||||
|
||||
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
|
||||
for (uint32_t w = 0U; w < wm_words; w++)
|
||||
for (uint32_t wm_word = 0U; wm_word < wm_words; wm_word++)
|
||||
{
|
||||
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
|
||||
const uint32_t WORD = QSPI_BASE->RFDR[w];
|
||||
const uint32_t BASE = w * QSPI_RFDR_WORD_BYTES;
|
||||
p_dst[BASE + 0U] = (uint8_t) (WORD & 0xFFU);
|
||||
p_dst[BASE + 1U] = (uint8_t) ((WORD >> 8U) & 0xFFU);
|
||||
p_dst[BASE + 2U] = (uint8_t) ((WORD >> 16U) & 0xFFU);
|
||||
p_dst[BASE + 3U] = (uint8_t) ((WORD >> 24U) & 0xFFU);
|
||||
const uint32_t WORD = QSPI_BASE->RFDR[wm_word];
|
||||
const uint32_t BASE = wm_word * QSPI_RFDR_WORD_BYTES;
|
||||
p_dst[BASE + 0U] = (uint8_t) (WORD & BYTE_MASK);
|
||||
p_dst[BASE + 1U] = (uint8_t) ((WORD >> 8U) & BYTE_MASK);
|
||||
p_dst[BASE + 2U] = (uint8_t) ((WORD >> 16U) & BYTE_MASK);
|
||||
p_dst[BASE + 3U] = (uint8_t) ((WORD >> 24U) & BYTE_MASK);
|
||||
}
|
||||
QSPI_BASE->INTR = (uint32_t) kFLEXSPI_IpRxFifoWatermarkAvailableFlag;
|
||||
return kStatus_Success;
|
||||
|
|
@ -353,16 +355,16 @@ AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_tail(uint8_t *p_dst, uint3
|
|||
}
|
||||
|
||||
uint32_t bytes_left = remain;
|
||||
for (uint32_t w = 0U; w < WORDS_NEEDED; w++)
|
||||
for (uint32_t word = 0U; word < WORDS_NEEDED; word++)
|
||||
{
|
||||
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
|
||||
const uint32_t WORD = QSPI_BASE->RFDR[w];
|
||||
const uint32_t WORD = QSPI_BASE->RFDR[word];
|
||||
const uint32_t BYTES =
|
||||
(bytes_left < QSPI_RFDR_WORD_BYTES) ? bytes_left : QSPI_RFDR_WORD_BYTES;
|
||||
const uint32_t BASE = w * QSPI_RFDR_WORD_BYTES;
|
||||
for (uint32_t b = 0U; b < BYTES; b++)
|
||||
const uint32_t BASE = word * QSPI_RFDR_WORD_BYTES;
|
||||
for (uint32_t byte = 0U; byte < BYTES; byte++)
|
||||
{
|
||||
p_dst[BASE + b] = (uint8_t) ((WORD >> (8U * b)) & 0xFFU);
|
||||
p_dst[BASE + byte] = (uint8_t) ((WORD >> (8U * byte)) & BYTE_MASK);
|
||||
}
|
||||
bytes_left -= BYTES;
|
||||
}
|
||||
|
|
@ -373,22 +375,22 @@ AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_tail(uint8_t *p_dst, uint3
|
|||
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
|
||||
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_fifo(uint8_t *p_buf, uint32_t len))
|
||||
{
|
||||
const uint32_t wm_units =
|
||||
const uint32_t WM_UNITS =
|
||||
((QSPI_BASE->IPRXFCR & FLEXSPI_IPRXFCR_RXWMRK_MASK) >> FLEXSPI_IPRXFCR_RXWMRK_SHIFT) + 1U;
|
||||
const uint32_t wm_words = wm_units * QSPI_WM_UNIT_WORDS;
|
||||
const uint32_t wm_bytes = wm_words * QSPI_RFDR_WORD_BYTES;
|
||||
const uint32_t WM_WORDS = WM_UNITS * QSPI_WM_UNIT_WORDS;
|
||||
const uint32_t WM_BYTES = WM_WORDS * QSPI_RFDR_WORD_BYTES;
|
||||
|
||||
uint32_t offset = 0U;
|
||||
|
||||
while ((len - offset) >= wm_bytes)
|
||||
while ((len - offset) >= WM_BYTES)
|
||||
{
|
||||
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) */
|
||||
const status_t ERR = qspi_read_chunk(p_buf + offset, wm_words);
|
||||
const status_t ERR = qspi_read_chunk(p_buf + offset, WM_WORDS);
|
||||
if (ERR != kStatus_Success)
|
||||
{
|
||||
return ERR;
|
||||
}
|
||||
offset += wm_bytes;
|
||||
offset += WM_BYTES;
|
||||
}
|
||||
|
||||
if (offset < len)
|
||||
|
|
@ -407,10 +409,10 @@ AT_QUICKACCESS_SECTION_CODE(static status_t qspi_write_fifo(const uint8_t *p_buf
|
|||
{
|
||||
const uint8_t *p = p_buf;
|
||||
uint32_t remain = len;
|
||||
const uint32_t wm_units =
|
||||
const uint32_t WM_UNITS =
|
||||
((QSPI_BASE->IPTXFCR & FLEXSPI_IPTXFCR_TXWMRK_MASK) >> FLEXSPI_IPTXFCR_TXWMRK_SHIFT) + 1U;
|
||||
const uint32_t wm_words = wm_units * QSPI_WM_UNIT_WORDS;
|
||||
const uint32_t wm_bytes = wm_words * QSPI_RFDR_WORD_BYTES;
|
||||
const uint32_t WM_WORDS = WM_UNITS * QSPI_WM_UNIT_WORDS;
|
||||
const uint32_t WM_BYTES = WM_WORDS * QSPI_RFDR_WORD_BYTES;
|
||||
|
||||
while (remain > 0U)
|
||||
{
|
||||
|
|
@ -426,26 +428,26 @@ AT_QUICKACCESS_SECTION_CODE(static status_t qspi_write_fifo(const uint8_t *p_buf
|
|||
}
|
||||
} while ((intr & (uint32_t) kFLEXSPI_IpTxFifoWatermarkEmptyFlag) == 0U);
|
||||
|
||||
const uint32_t CHUNK = (remain > wm_bytes) ? wm_bytes : remain;
|
||||
const uint32_t CHUNK = (remain > WM_BYTES) ? WM_BYTES : remain;
|
||||
const uint32_t WR_WORDS = (CHUNK + QSPI_RFDR_WORD_BYTES - 1U) / QSPI_RFDR_WORD_BYTES;
|
||||
if (WR_WORDS > wm_words)
|
||||
if (WR_WORDS > WM_WORDS)
|
||||
{
|
||||
return kStatus_FLEXSPI_IpCommandSequenceError;
|
||||
}
|
||||
|
||||
for (uint32_t w = 0U; w < WR_WORDS; w++)
|
||||
for (uint32_t wr_word = 0U; wr_word < WR_WORDS; wr_word++)
|
||||
{
|
||||
const uint32_t OFF = w * QSPI_RFDR_WORD_BYTES;
|
||||
const uint32_t OFF = wr_word * QSPI_RFDR_WORD_BYTES;
|
||||
const uint32_t BYTES =
|
||||
((CHUNK - OFF) < QSPI_RFDR_WORD_BYTES) ? (CHUNK - OFF) : QSPI_RFDR_WORD_BYTES;
|
||||
uint32_t word = 0U;
|
||||
for (uint32_t b = 0U; b < BYTES; b++)
|
||||
for (uint32_t byte = 0U; byte < BYTES; byte++)
|
||||
{
|
||||
word |= ((uint32_t) *p) << (8U * b);
|
||||
word |= ((uint32_t) *p) << (8U * byte);
|
||||
p++;
|
||||
}
|
||||
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
|
||||
QSPI_BASE->TFDR[w] = word;
|
||||
QSPI_BASE->TFDR[wr_word] = word;
|
||||
}
|
||||
remain -= CHUNK;
|
||||
QSPI_BASE->INTR = (uint32_t) kFLEXSPI_IpTxFifoWatermarkEmptyFlag;
|
||||
|
|
@ -662,7 +664,7 @@ AT_QUICKACCESS_SECTION_CODE(static bsp_status_t qspi_do_erase(uint32_t seq_idx,
|
|||
{
|
||||
return BSP_ERR_HW;
|
||||
}
|
||||
const uint32_t primask = qspi_irq_lock();
|
||||
const uint32_t PRIMASK = qspi_irq_lock();
|
||||
qspi_ahb_disable();
|
||||
|
||||
status_t result = qspi_write_enable(addr);
|
||||
|
|
@ -677,7 +679,7 @@ AT_QUICKACCESS_SECTION_CODE(static bsp_status_t qspi_do_erase(uint32_t seq_idx,
|
|||
|
||||
qspi_sw_reset();
|
||||
qspi_ahb_enable();
|
||||
qspi_irq_unlock(primask);
|
||||
qspi_irq_unlock(PRIMASK);
|
||||
return to_bsp(result);
|
||||
}
|
||||
|
||||
|
|
@ -748,7 +750,7 @@ AT_QUICKACCESS_SECTION_CODE(bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t
|
|||
{
|
||||
return BSP_ERR_HW;
|
||||
}
|
||||
const uint32_t primask = qspi_irq_lock();
|
||||
const uint32_t PRIMASK = qspi_irq_lock();
|
||||
qspi_ahb_disable();
|
||||
|
||||
uint8_t buf[SR_READ_LEN] = { 0U };
|
||||
|
|
@ -756,7 +758,7 @@ AT_QUICKACCESS_SECTION_CODE(bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t
|
|||
|
||||
qspi_sw_reset();
|
||||
qspi_ahb_enable();
|
||||
qspi_irq_unlock(primask);
|
||||
qspi_irq_unlock(PRIMASK);
|
||||
|
||||
if (RESULT != kStatus_Success)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,12 +17,14 @@ add_executable(
|
|||
|
||||
target_include_directories(firmware_test PRIVATE src/)
|
||||
|
||||
# __STARTUP_INITIALIZE_RAMFUNCTION - очистка секции .ram_function и копирование
|
||||
# туда данных из __ram_function_flash_start; __STARTUP_INITIALIZE_NONCACHEDATA -
|
||||
# инициализация некешируемое секции нулями
|
||||
target_compile_definitions(
|
||||
${TARGET_NAME}
|
||||
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512
|
||||
BOARD_MPU_SDRAM=1
|
||||
__STARTUP_INITIALIZE_RAMFUNCTION
|
||||
__STARTUP_CLEAR_BSS)
|
||||
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512 BOARD_MPU_SDRAM=1
|
||||
__STARTUP_INITIALIZE_RAMFUNCTION __STARTUP_CLEAR_BSS
|
||||
__STARTUP_INITIALIZE_NONCACHEDATA)
|
||||
#
|
||||
# -----------------------------------------------------------------------------
|
||||
# Зависимости — только то что нужно для входного контроля bsp_board транзитивно
|
||||
|
|
|
|||
|
|
@ -3,120 +3,63 @@
|
|||
* @file main.c
|
||||
* @brief firmware_test — точка входа.
|
||||
*
|
||||
* Минимальный smoke-тест QSPI в XIP-конфигурации:
|
||||
* 1) Инициализация QSPI.
|
||||
* 2) Чтение JEDEC ID.
|
||||
* 3) Erase одного сектора в конце Flash.
|
||||
* 4) Запись одной страницы и чтение назад.
|
||||
* 5) Сравнение буферов и LED-индикация PASS/FAIL.
|
||||
* Bare-metal входной контроль платы.
|
||||
* Единственный канал хост↔таргет: USB CDC ACM (J2).
|
||||
* Протокол: JSON-lines через cli.c.
|
||||
*
|
||||
* Последовательность старта:
|
||||
* 1. board_hw_init() — тактирование, MPU, кэш, пины
|
||||
* 2. bsp_tick_init() — SysTick 1 мс
|
||||
* 3. bsp_led_init() — оба LED выключены
|
||||
* 4. bsp_usb_cdc_init() — PHY + стек + NVIC
|
||||
* 5. Ожидание CDC ready — LED_HEARTBEAT мигает
|
||||
* 6. cli_init() — сброс буфера
|
||||
* 7. READY → хост — JSON сигнал готовности
|
||||
* 8. Главный цикл — poll + cli_process
|
||||
*/
|
||||
#include "board.h"
|
||||
#include "bsp/led.h"
|
||||
#include "bsp/qspi_flash.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "cli.h"
|
||||
#include "protocol.h"
|
||||
#include "test_runner.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static volatile uint32_t g_qspi_test_step = 0U;
|
||||
static volatile uint32_t g_qspi_test_addr = 0U;
|
||||
static volatile uint32_t g_qspi_test_mismatch_index = 0xFFFFFFFFUL;
|
||||
static volatile bsp_qspi_jedec_t g_qspi_test_jedec = { 0U, 0U };
|
||||
|
||||
static bool bytes_equal(const uint8_t *p_lhs, const uint8_t *p_rhs, size_t size)
|
||||
{
|
||||
for (size_t i = 0U; i < size; i++)
|
||||
{
|
||||
if (p_lhs[i] != p_rhs[i])
|
||||
{
|
||||
g_qspi_test_mismatch_index = (uint32_t) i;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const uint32_t ERROR_BLINK_MS = 80U;
|
||||
const uint32_t PASS_BLINK_MS = 350U;
|
||||
|
||||
const uint32_t CONNECT_BLINK_MS = 200U;
|
||||
const uint32_t ERROR_BLINK_MS = 250;
|
||||
board_hw_init();
|
||||
|
||||
bsp_led_init();
|
||||
bsp_tick_init();
|
||||
|
||||
g_qspi_test_step = 1U;
|
||||
if (bsp_qspi_init() != BSP_OK)
|
||||
{
|
||||
goto FAIL;
|
||||
}
|
||||
|
||||
g_qspi_test_step = 2U;
|
||||
if (bsp_qspi_read_jedec_id((bsp_qspi_jedec_t *) &g_qspi_test_jedec) != BSP_OK)
|
||||
{
|
||||
goto FAIL;
|
||||
}
|
||||
|
||||
if (g_qspi_test_jedec.manufacturer_id != BSP_QSPI_MFR_WINBOND)
|
||||
{
|
||||
goto FAIL;
|
||||
}
|
||||
|
||||
g_qspi_test_step = 3U;
|
||||
const uint32_t flash_size = bsp_qspi_flash_size();
|
||||
if (flash_size < BSP_QSPI_SECTOR_SIZE)
|
||||
{
|
||||
goto FAIL;
|
||||
}
|
||||
g_qspi_test_addr = flash_size - BSP_QSPI_SECTOR_SIZE;
|
||||
|
||||
uint8_t tx[BSP_QSPI_PAGE_SIZE];
|
||||
uint8_t rx[BSP_QSPI_PAGE_SIZE];
|
||||
|
||||
for (uint32_t i = 0U; i < BSP_QSPI_PAGE_SIZE; i++)
|
||||
{
|
||||
tx[i] = (uint8_t) (0xA5U ^ i ^ g_qspi_test_jedec.manufacturer_id ^
|
||||
(uint8_t) g_qspi_test_jedec.device_id);
|
||||
rx[i] = 0U;
|
||||
}
|
||||
|
||||
g_qspi_test_step = 4U;
|
||||
if (bsp_qspi_erase_sector(g_qspi_test_addr) != BSP_OK)
|
||||
{
|
||||
goto FAIL;
|
||||
}
|
||||
|
||||
g_qspi_test_step = 5U;
|
||||
if (bsp_qspi_write_page(g_qspi_test_addr, tx) != BSP_OK)
|
||||
{
|
||||
goto FAIL;
|
||||
}
|
||||
|
||||
g_qspi_test_step = 6U;
|
||||
if (bsp_qspi_read(g_qspi_test_addr, rx, sizeof(rx)) != BSP_OK)
|
||||
{
|
||||
goto FAIL;
|
||||
}
|
||||
|
||||
g_qspi_test_step = 7U;
|
||||
if (!bytes_equal(tx, rx, sizeof(rx)))
|
||||
{
|
||||
goto FAIL;
|
||||
}
|
||||
|
||||
g_qspi_test_step = 8U;
|
||||
bsp_led_on(LED_APP);
|
||||
while (1)
|
||||
{
|
||||
bsp_led_toggle(LED_HEARTBEAT);
|
||||
bsp_delay(PASS_BLINK_MS);
|
||||
}
|
||||
|
||||
FAIL:
|
||||
g_qspi_test_step |= 0x80000000UL;
|
||||
while (1)
|
||||
if (bsp_usb_cdc_init() != BSP_OK)
|
||||
{
|
||||
bsp_led_toggle(LED_HEARTBEAT);
|
||||
bsp_delay(ERROR_BLINK_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ожидать подключения хоста. LED_HEARTBEAT мигает — прошивка жива. */
|
||||
while (!bsp_usb_cdc_is_ready())
|
||||
{
|
||||
bsp_usb_cdc_poll();
|
||||
bsp_led_toggle(LED_HEARTBEAT);
|
||||
bsp_delay(CONNECT_BLINK_MS);
|
||||
}
|
||||
|
||||
bsp_led_on(LED_APP);
|
||||
|
||||
cli_init();
|
||||
test_runner_init();
|
||||
protocol_send_session_start();
|
||||
while (1)
|
||||
{
|
||||
bsp_usb_cdc_poll();
|
||||
cli_process();
|
||||
test_runner_process();
|
||||
}
|
||||
}
|
||||
21
just/ci.just
21
just/ci.just
|
|
@ -2,22 +2,20 @@
|
|||
# ci.just — CI/CD пайплайны
|
||||
# Выполняется в CI-окружении (GitHub Actions, GitLab CI, Jenkins)
|
||||
#
|
||||
# Предполагается что CI работает внутри devcontainer.
|
||||
# Stage 1: только reproducible шаги без железа:
|
||||
# - сборка
|
||||
# - host unit-тесты
|
||||
# lint/release будут включаться по мере стабилизации пайплайна.
|
||||
# =============================================================================
|
||||
# === Импорт общих переменных ===
|
||||
|
||||
BUILD_DIR := env('BUILD_DIR', justfile_directory() / 'build')
|
||||
|
||||
set working-directory := '..'
|
||||
|
||||
# =============================================================================
|
||||
# ГРУППА: ci — полные пайплайны
|
||||
# =============================================================================
|
||||
|
||||
[doc('Полный CI pipeline: сборка + тесты + линтеры')]
|
||||
[doc('Минимальный CI pipeline: сборка + host-тесты')]
|
||||
[group('ci')]
|
||||
pipeline: build test lint
|
||||
@echo " ✅ CI pipeline complete"
|
||||
pipeline: build test
|
||||
@echo " ✅ Minimal CI pipeline complete"
|
||||
|
||||
[doc('Собрать все проекты в Release')]
|
||||
[group('ci')]
|
||||
|
|
@ -32,10 +30,7 @@ test:
|
|||
[doc('Запустить линтеры и статический анализ')]
|
||||
[group('ci')]
|
||||
lint:
|
||||
echo "TODO"
|
||||
|
||||
#just build::lint
|
||||
#just build::check
|
||||
@echo "TODO: enable just build::lint / just build::check after local stabilization"
|
||||
|
||||
[doc('Собрать HAB Release-артефакты для выкладки')]
|
||||
[group('ci')]
|
||||
|
|
|
|||
85
just/ci_workflow.md
Normal file
85
just/ci_workflow.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Текущее состояние CI/CD workflow для `tft_manufacture_test`
|
||||
|
||||
## Обзор
|
||||
|
||||
В репозитории настроен рабочий GitHub Actions pipeline, который успешно запускается на событиях `push`, `pull_request` и при ручном запуске через `workflow_dispatch`.[cite:23][cite:99] Пайплайн специально привязан к реальному окружению разработки проекта: сборка и тесты выполняются внутри того же devcontainer-образа, который описан в `.devcontainer/Dockerfile`, а не в вручную собранной среде на `ubuntu-latest`.[cite:125]
|
||||
|
||||
Такой подход уже устранил основные проблемы, которые проявились при первичном поднятии CI: отсутствие ARM toolchain, отсутствие `ninja`, установка неправильного `just` и несовместимость прав доступа при работе с bind-mounted workspace внутри Docker.[cite:125][cite:119][cite:127]
|
||||
|
||||
## Текущая архитектура
|
||||
|
||||
Workflow разделён на две job’ы: `build` и `test`.[cite:156] Такое разделение делает пайплайн проще для сопровождения, позволяет отдельно анализировать результаты стадии сборки и создаёт хороший фундамент для следующих этапов — `lint`, coverage, release packaging и аппаратных проверок.[cite:156][cite:147]
|
||||
|
||||
Среда выполнения строится из Dockerfile devcontainer-а проекта, в котором уже определены ARM GCC toolchain в `/opt/arm-toolchain`, обновлённый `PATH`, а также установлены `cmake`, `ninja-build`, `clang-17`, `uv` и `just`.[cite:125] Поскольку все ключевые зависимости уже зафиксированы именно там, использование этого же образа в CI делает поведение раннера максимально близким к локальной разработке.[cite:125]
|
||||
|
||||
## Как работает workflow
|
||||
|
||||
Workflow реагирует на три типа событий: `push`, `pull_request` и `workflow_dispatch`.[cite:23][cite:99] Это даёт удобный баланс между автоматической проверкой обычных коммитов и возможностью вручную перезапускать pipeline для отладки инфраструктурных или нестабильных падений без обязательного нового изменения в коде.[cite:99][cite:106]
|
||||
|
||||
Внутри job используется Docker Buildx и `docker/build-push-action`, а кэширование слоёв контейнера подключено через backend GitHub Actions cache с помощью `cache-from: type=gha` и `cache-to: type=gha`.[cite:142][cite:143][cite:146] За счёт этого повторные прогоны не пересобирают devcontainer с нуля, а переиспользуют уже собранные Docker-слои, что заметно ускоряет пайплайн после первого успешного заполнения кэша.[cite:142][cite:143]
|
||||
|
||||
## Что делает job `build`
|
||||
|
||||
Job `build` выполняет checkout репозитория, инициализирует Buildx, собирает devcontainer image с поддержкой кэша, проверяет версии инструментов внутри контейнера, синхронизирует Python tooling в `tools/host` через `uv sync`, а затем запускает `just ci::build` внутри контейнера.[cite:142][cite:143][cite:125] После успешной сборки workflow выгружает директорию `build/` как GitHub artifact, чтобы результаты можно было сохранить и использовать на следующих стадиях.[cite:156][cite:153]
|
||||
|
||||
Важная техническая деталь — команды внутри контейнера запускаются с `--user root`.[cite:127][cite:135] Это требуется из-за того, что `GITHUB_WORKSPACE` подключается в контейнер как bind mount, а в GitHub Actions non-root пользователь внутри Docker часто не получает права на запись в такую директорию; ранее это как раз ломало создание `.venv` во время `uv sync`.[cite:127][cite:129][cite:135]
|
||||
|
||||
## Что делает job `test`
|
||||
|
||||
Job `test` зависит от `build`, скачивает artifact с директорией `build/`, заново поднимает тот же devcontainer image с использованием cached layers, синхронизирует `tools/host` и запускает `just ci::test` внутри контейнера.[cite:156][cite:142][cite:143] На практике это означает, что host unit-тесты работают в той же программной среде, что и стадия сборки, но при этом выделены в отдельный CI-этап.[cite:125][cite:156]
|
||||
|
||||
Так как GitHub-hosted runner’ы эфемерны, сам Docker image не передаётся напрямую между job’ами.[cite:143] Поэтому обмен между `build` и `test` организован двумя способами: ускорение повторной сборки образа идёт через Docker layer cache, а результаты проекта передаются через GitHub artifacts.[cite:143][cite:153][cite:156]
|
||||
|
||||
## Почему эта схема хорошо подходит проекту
|
||||
|
||||
Этот репозиторий нельзя считать обычным desktop C-проектом: он завязан на фиксированное расположение embedded toolchain и на специально подготовленный devcontainer.[cite:125] Ранние попытки выполнять pipeline прямо на runner’е падали, потому что проект ожидал наличие `/opt/arm-toolchain`, установленный `Ninja` и современный бинарник `just`, который понимает атрибуты вроде `[doc(...)]`.[cite:125][cite:97][cite:119]
|
||||
|
||||
Перенос CI внутрь devcontainer image устраняет этот класс расхождений и делает Dockerfile единым источником истины для окружения, версий и путей.[cite:125] Это упрощает дальнейшее сопровождение: при изменении инструментария достаточно обновить Dockerfile, и эти же изменения автоматически начнут действовать как локально, так и в CI.[cite:125]
|
||||
|
||||
## Чего workflow пока не делает
|
||||
|
||||
Текущий pipeline пока не включает обязательную стадию `lint` и статический анализ, потому что в `just/ci.just` для `lint` пока ещё оставлена заглушка, а не полноценный вызов `clang-format` и `clang-tidy`.[cite:1] Он также пока не формирует release/HAB artifacts в CI, хотя в репозитории уже есть соответствующие рецепты `just ci::release` и связанные сборочные шаги.[cite:1]
|
||||
|
||||
Также pipeline пока не запускает HIL-сценарии.[cite:1] Это ожидаемо и правильно для текущего этапа: hardware-in-the-loop проверки требуют физического оборудования и в дальнейшем должны выполняться отдельно на self-hosted runner рядом с bench-стендом, а не на GitHub-hosted машинах.[cite:1]
|
||||
|
||||
## Сильные стороны текущего решения
|
||||
|
||||
У текущей реализации уже есть несколько сильных сторон:
|
||||
|
||||
- Она воспроизводима, потому что сборка и тесты выполняются в том же образе, что и локальная разработка.[cite:125]
|
||||
- Она ускоряется на повторных прогонах за счёт Docker layer caching через GitHub Actions cache backend.[cite:142][cite:143]
|
||||
- Она модульна, потому что `build` и `test` вынесены в отдельные job’ы, связанные артефактами.[cite:156][cite:153]
|
||||
- Она удобна для отладки, потому что build outputs сохраняются как artifacts, а workflow можно запускать вручную через `workflow_dispatch`.[cite:99][cite:156]
|
||||
- Она хорошо вписана в структуру проекта, потому что использует уже существующие `just`-точки входа, а не дублирует build-логику в YAML.[cite:1]
|
||||
|
||||
## Текущие ограничения
|
||||
|
||||
Главное ограничение сейчас состоит в том, что pipeline проверяет собираемость и host unit-тесты, но ещё не закрывает style gate, static analysis, coverage и release packaging.[cite:1] Второе ограничение — Docker image пересобирается в каждой job, поэтому даже при наличии кэша остаётся неизбежный накладной расход по времени по сравнению с вариантом, где используется заранее опубликованный образ из registry.[cite:143]
|
||||
|
||||
Есть и архитектурное ограничение GitHub-hosted runner’ов для аппаратной части.[cite:127][cite:1] Прошивка через USB, pyOCD-сценарии и управление стендом должны в будущем быть вынесены в отдельную hardware lane на self-hosted runner.[cite:1]
|
||||
|
||||
## Рекомендуемые следующие шаги
|
||||
|
||||
### Шаг 1 — добавить `lint` job
|
||||
|
||||
Самое логичное следующее улучшение — реализовать полноценную стадию `lint` в `just/ci.just` и подключить отдельную job в workflow.[cite:1] В эту стадию стоит включить `clang-format --dry-run --Werror`, `clang-tidy` и необходимые исключения для generated-кода или vendor-зависимостей, чтобы избежать лишнего шума в CI.[cite:1]
|
||||
|
||||
### Шаг 2 — добавить coverage
|
||||
|
||||
После стабилизации `lint` полезно подключить экспорт coverage для host-тестов.[cite:1] В `ci.just` уже существует закрытый рецепт `_coverage`, и его можно развить до генерации XML-отчёта, выгрузки артефактов и последующей интеграции с внешним coverage-сервисом, если это будет нужно.[cite:1]
|
||||
|
||||
### Шаг 3 — выделить release workflow
|
||||
|
||||
Release packaging лучше оформлять отдельным workflow или отдельной gated job, запускаемой только по тегам, на `main` или вручную через `workflow_dispatch`.[cite:1] Это позволит не замедлять обычный PR-цикл, но при этом использовать `just ci::release` и публикацию HAB-артефактов тогда, когда это действительно нужно.[cite:1]
|
||||
|
||||
### Шаг 4 — публиковать devcontainer image в GHCR
|
||||
|
||||
Следующий сильный шаг по оптимизации — публиковать devcontainer image в GHCR и затем запускать CI уже на базе заранее собранного образа, а не пересобирать его в каждой job.[cite:142][cite:146] Это ещё сильнее сократит время старта pipeline и сделает масштабирование на `lint`, `coverage` и `release` заметно проще.[cite:142][cite:143]
|
||||
|
||||
### Шаг 5 — добавить self-hosted HIL lane
|
||||
|
||||
Финальное крупное направление развития — выделенный аппаратный workflow на self-hosted runner с доступом к MCU-Link, target board и M5StampPLC.[cite:1] Такую lane лучше запускать вручную, по расписанию или по label-триггеру, а не делать обязательной для каждого PR, поскольку аппаратные проверки медленнее, менее стабильны и по природе отличаются от быстрых software regression checks.[cite:1]
|
||||
|
||||
## Целевое состояние
|
||||
|
||||
Зрелая версия этого CI/CD контура, вероятно, будет состоять из четырёх независимых линий: быстрый PR-pipeline (`build`, `test`, `lint`), optional coverage reporting, отдельный release workflow и отдельный self-hosted HIL pipeline.[cite:1][cite:156] Такая структура сохранит короткий feedback loop для обычной разработки и одновременно покроет полный жизненный цикл embedded-проекта: от изменений в исходниках до production artifacts и аппаратной валидации на стенде.[cite:1]
|
||||
Loading…
Reference in a new issue