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: "-*"
|
Checks: "-*"
|
||||||
|
|
@ -186,6 +186,8 @@
|
||||||
kFLEXSPI_Command_READ_SDR, kFLEXSPI_4PAD, 0x04U), \
|
kFLEXSPI_Command_READ_SDR, kFLEXSPI_4PAD, 0x04U), \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const uint8_t BYTE_MASK = 0xFFU;
|
||||||
|
|
||||||
/** @brief LUT для W25Q64/128 — стандартные 3-byte opcodes. */
|
/** @brief LUT для W25Q64/128 — стандартные 3-byte opcodes. */
|
||||||
static const uint32_t K_LUT_3B[LUT_TOTAL_WORDS] =
|
static const uint32_t K_LUT_3B[LUT_TOTAL_WORDS] =
|
||||||
BUILD_LUT(0x20U, 0x52U, 0xD8U, 0x32U, 0x6BU, ADDR_BITS_24);
|
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) */
|
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
|
||||||
AT_QUICKACCESS_SECTION_CODE(static uint32_t qspi_irq_lock(void))
|
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();
|
__disable_irq();
|
||||||
__DSB();
|
__DSB();
|
||||||
__ISB();
|
__ISB();
|
||||||
return primask;
|
return PRIMASK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
|
/* 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);
|
} while ((intr & (uint32_t) kFLEXSPI_IpRxFifoWatermarkAvailableFlag) == 0U);
|
||||||
|
|
||||||
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
|
/* 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) */
|
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
|
||||||
const uint32_t WORD = QSPI_BASE->RFDR[w];
|
const uint32_t WORD = QSPI_BASE->RFDR[wm_word];
|
||||||
const uint32_t BASE = w * QSPI_RFDR_WORD_BYTES;
|
const uint32_t BASE = wm_word * QSPI_RFDR_WORD_BYTES;
|
||||||
p_dst[BASE + 0U] = (uint8_t) (WORD & 0xFFU);
|
p_dst[BASE + 0U] = (uint8_t) (WORD & BYTE_MASK);
|
||||||
p_dst[BASE + 1U] = (uint8_t) ((WORD >> 8U) & 0xFFU);
|
p_dst[BASE + 1U] = (uint8_t) ((WORD >> 8U) & BYTE_MASK);
|
||||||
p_dst[BASE + 2U] = (uint8_t) ((WORD >> 16U) & 0xFFU);
|
p_dst[BASE + 2U] = (uint8_t) ((WORD >> 16U) & BYTE_MASK);
|
||||||
p_dst[BASE + 3U] = (uint8_t) ((WORD >> 24U) & 0xFFU);
|
p_dst[BASE + 3U] = (uint8_t) ((WORD >> 24U) & BYTE_MASK);
|
||||||
}
|
}
|
||||||
QSPI_BASE->INTR = (uint32_t) kFLEXSPI_IpRxFifoWatermarkAvailableFlag;
|
QSPI_BASE->INTR = (uint32_t) kFLEXSPI_IpRxFifoWatermarkAvailableFlag;
|
||||||
return kStatus_Success;
|
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;
|
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) */
|
/* 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 =
|
const uint32_t BYTES =
|
||||||
(bytes_left < QSPI_RFDR_WORD_BYTES) ? bytes_left : QSPI_RFDR_WORD_BYTES;
|
(bytes_left < QSPI_RFDR_WORD_BYTES) ? bytes_left : QSPI_RFDR_WORD_BYTES;
|
||||||
const uint32_t BASE = w * QSPI_RFDR_WORD_BYTES;
|
const uint32_t BASE = word * QSPI_RFDR_WORD_BYTES;
|
||||||
for (uint32_t b = 0U; b < BYTES; b++)
|
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;
|
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) */
|
/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) */
|
||||||
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_fifo(uint8_t *p_buf, uint32_t len))
|
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;
|
((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_WORDS = WM_UNITS * QSPI_WM_UNIT_WORDS;
|
||||||
const uint32_t wm_bytes = wm_words * QSPI_RFDR_WORD_BYTES;
|
const uint32_t WM_BYTES = WM_WORDS * QSPI_RFDR_WORD_BYTES;
|
||||||
|
|
||||||
uint32_t offset = 0U;
|
uint32_t offset = 0U;
|
||||||
|
|
||||||
while ((len - offset) >= wm_bytes)
|
while ((len - offset) >= WM_BYTES)
|
||||||
{
|
{
|
||||||
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) */
|
/* 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)
|
if (ERR != kStatus_Success)
|
||||||
{
|
{
|
||||||
return ERR;
|
return ERR;
|
||||||
}
|
}
|
||||||
offset += wm_bytes;
|
offset += WM_BYTES;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset < len)
|
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;
|
const uint8_t *p = p_buf;
|
||||||
uint32_t remain = len;
|
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;
|
((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_WORDS = WM_UNITS * QSPI_WM_UNIT_WORDS;
|
||||||
const uint32_t wm_bytes = wm_words * QSPI_RFDR_WORD_BYTES;
|
const uint32_t WM_BYTES = WM_WORDS * QSPI_RFDR_WORD_BYTES;
|
||||||
|
|
||||||
while (remain > 0U)
|
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);
|
} 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;
|
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;
|
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 =
|
const uint32_t BYTES =
|
||||||
((CHUNK - OFF) < QSPI_RFDR_WORD_BYTES) ? (CHUNK - OFF) : QSPI_RFDR_WORD_BYTES;
|
((CHUNK - OFF) < QSPI_RFDR_WORD_BYTES) ? (CHUNK - OFF) : QSPI_RFDR_WORD_BYTES;
|
||||||
uint32_t word = 0U;
|
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++;
|
p++;
|
||||||
}
|
}
|
||||||
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
|
/* NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) */
|
||||||
QSPI_BASE->TFDR[w] = word;
|
QSPI_BASE->TFDR[wr_word] = word;
|
||||||
}
|
}
|
||||||
remain -= CHUNK;
|
remain -= CHUNK;
|
||||||
QSPI_BASE->INTR = (uint32_t) kFLEXSPI_IpTxFifoWatermarkEmptyFlag;
|
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;
|
return BSP_ERR_HW;
|
||||||
}
|
}
|
||||||
const uint32_t primask = qspi_irq_lock();
|
const uint32_t PRIMASK = qspi_irq_lock();
|
||||||
qspi_ahb_disable();
|
qspi_ahb_disable();
|
||||||
|
|
||||||
status_t result = qspi_write_enable(addr);
|
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_sw_reset();
|
||||||
qspi_ahb_enable();
|
qspi_ahb_enable();
|
||||||
qspi_irq_unlock(primask);
|
qspi_irq_unlock(PRIMASK);
|
||||||
return to_bsp(result);
|
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;
|
return BSP_ERR_HW;
|
||||||
}
|
}
|
||||||
const uint32_t primask = qspi_irq_lock();
|
const uint32_t PRIMASK = qspi_irq_lock();
|
||||||
qspi_ahb_disable();
|
qspi_ahb_disable();
|
||||||
|
|
||||||
uint8_t buf[SR_READ_LEN] = { 0U };
|
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_sw_reset();
|
||||||
qspi_ahb_enable();
|
qspi_ahb_enable();
|
||||||
qspi_irq_unlock(primask);
|
qspi_irq_unlock(PRIMASK);
|
||||||
|
|
||||||
if (RESULT != kStatus_Success)
|
if (RESULT != kStatus_Success)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,14 @@ add_executable(
|
||||||
|
|
||||||
target_include_directories(firmware_test PRIVATE src/)
|
target_include_directories(firmware_test PRIVATE src/)
|
||||||
|
|
||||||
|
# __STARTUP_INITIALIZE_RAMFUNCTION - очистка секции .ram_function и копирование
|
||||||
|
# туда данных из __ram_function_flash_start; __STARTUP_INITIALIZE_NONCACHEDATA -
|
||||||
|
# инициализация некешируемое секции нулями
|
||||||
target_compile_definitions(
|
target_compile_definitions(
|
||||||
${TARGET_NAME}
|
${TARGET_NAME}
|
||||||
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512
|
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512 BOARD_MPU_SDRAM=1
|
||||||
BOARD_MPU_SDRAM=1
|
__STARTUP_INITIALIZE_RAMFUNCTION __STARTUP_CLEAR_BSS
|
||||||
__STARTUP_INITIALIZE_RAMFUNCTION
|
__STARTUP_INITIALIZE_NONCACHEDATA)
|
||||||
__STARTUP_CLEAR_BSS)
|
|
||||||
#
|
#
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Зависимости — только то что нужно для входного контроля bsp_board транзитивно
|
# Зависимости — только то что нужно для входного контроля bsp_board транзитивно
|
||||||
|
|
|
||||||
|
|
@ -3,120 +3,63 @@
|
||||||
* @file main.c
|
* @file main.c
|
||||||
* @brief firmware_test — точка входа.
|
* @brief firmware_test — точка входа.
|
||||||
*
|
*
|
||||||
* Минимальный smoke-тест QSPI в XIP-конфигурации:
|
* Bare-metal входной контроль платы.
|
||||||
* 1) Инициализация QSPI.
|
* Единственный канал хост↔таргет: USB CDC ACM (J2).
|
||||||
* 2) Чтение JEDEC ID.
|
* Протокол: JSON-lines через cli.c.
|
||||||
* 3) Erase одного сектора в конце Flash.
|
*
|
||||||
* 4) Запись одной страницы и чтение назад.
|
* Последовательность старта:
|
||||||
* 5) Сравнение буферов и LED-индикация PASS/FAIL.
|
* 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 "board.h"
|
||||||
#include "bsp/led.h"
|
#include "bsp/led.h"
|
||||||
#include "bsp/qspi_flash.h"
|
|
||||||
#include "bsp/tick.h"
|
#include "bsp/tick.h"
|
||||||
|
#include "bsp/usb_cdc.h"
|
||||||
|
#include "cli.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "test_runner.h"
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.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)
|
int main(void)
|
||||||
{
|
{
|
||||||
const uint32_t ERROR_BLINK_MS = 80U;
|
const uint32_t CONNECT_BLINK_MS = 200U;
|
||||||
const uint32_t PASS_BLINK_MS = 350U;
|
const uint32_t ERROR_BLINK_MS = 250;
|
||||||
|
|
||||||
board_hw_init();
|
board_hw_init();
|
||||||
|
|
||||||
bsp_led_init();
|
bsp_led_init();
|
||||||
bsp_tick_init();
|
bsp_tick_init();
|
||||||
|
|
||||||
g_qspi_test_step = 1U;
|
if (bsp_usb_cdc_init() != BSP_OK)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
bsp_led_toggle(LED_HEARTBEAT);
|
bsp_led_toggle(LED_HEARTBEAT);
|
||||||
bsp_delay(ERROR_BLINK_MS);
|
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.just — CI/CD пайплайны
|
||||||
# Выполняется в CI-окружении (GitHub Actions, GitLab CI, Jenkins)
|
# Выполняется в CI-окружении (GitHub Actions, GitLab CI, Jenkins)
|
||||||
#
|
#
|
||||||
# Предполагается что CI работает внутри devcontainer.
|
# Stage 1: только reproducible шаги без железа:
|
||||||
|
# - сборка
|
||||||
|
# - host unit-тесты
|
||||||
|
# lint/release будут включаться по мере стабилизации пайплайна.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# === Импорт общих переменных ===
|
|
||||||
|
|
||||||
BUILD_DIR := env('BUILD_DIR', justfile_directory() / 'build')
|
BUILD_DIR := env('BUILD_DIR', justfile_directory() / 'build')
|
||||||
|
|
||||||
set working-directory := '..'
|
set working-directory := '..'
|
||||||
|
|
||||||
# =============================================================================
|
[doc('Минимальный CI pipeline: сборка + host-тесты')]
|
||||||
# ГРУППА: ci — полные пайплайны
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
[doc('Полный CI pipeline: сборка + тесты + линтеры')]
|
|
||||||
[group('ci')]
|
[group('ci')]
|
||||||
pipeline: build test lint
|
pipeline: build test
|
||||||
@echo " ✅ CI pipeline complete"
|
@echo " ✅ Minimal CI pipeline complete"
|
||||||
|
|
||||||
[doc('Собрать все проекты в Release')]
|
[doc('Собрать все проекты в Release')]
|
||||||
[group('ci')]
|
[group('ci')]
|
||||||
|
|
@ -32,10 +30,7 @@ test:
|
||||||
[doc('Запустить линтеры и статический анализ')]
|
[doc('Запустить линтеры и статический анализ')]
|
||||||
[group('ci')]
|
[group('ci')]
|
||||||
lint:
|
lint:
|
||||||
echo "TODO"
|
@echo "TODO: enable just build::lint / just build::check after local stabilization"
|
||||||
|
|
||||||
#just build::lint
|
|
||||||
#just build::check
|
|
||||||
|
|
||||||
[doc('Собрать HAB Release-артефакты для выкладки')]
|
[doc('Собрать HAB Release-артефакты для выкладки')]
|
||||||
[group('ci')]
|
[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