Embedded Mastery Series · Volume 1 · Companion

Chip B Target Firmware

A single unified firmware for the second NUCLEO-H563ZI used in Volume 1's two-board labs. Flash it once. It runs as the I3C target for §S7 and as the FDCAN echo node for §S8.3. The bench harness on chip A talks to it across iterate.sh runs without manual reset thanks to a built-in independent watchdog.

What this firmware does

  • §S7.1 ENTDAA + private write target. Listens on PB8 (SCL) / PB9 (SDA) at AF3 with the ES0565 §2.15.3 pull-up workaround. Responds to the controller's ENTDAA broadcast with its 8-byte BCR/DCR/PID payload, accepts DA 0x32, then receives a 4-byte private write into tgt_rx[].
  • §S7.2 IBI source. The blue USER button (B1, on PC13) edge-triggers an empty-payload IBI to the controller via HAL_I3C_Tgt_IBIReq_IT. The controller's HAL_I3C_NotifyCallback reads the source DA from the CCC info struct, increments a counter, prints the DA. Auto-rearms its receive IT after the IBI completes (on EVENT_ID_IBIEND).
  • §S7.3 GETPID Direct CCC responder. The H5 I3C peripheral handles GETPID 0x8D entirely in hardware — no firmware path needed; the firmware just ensures DA is set and the bus is armed.
  • §S8.3 FDCAN echo node. FDCAN1 on PD0 (RX) / PD1 (TX), AF9, wired through an MCP2562FD pair to chip A's FDCAN1. Filter accepts every standard 11-bit ID into RX FIFO 0; the callback echoes each frame back at the source ID + 0x100, in the same FD/BRS / DLC the original came in.

Why a single firmware

Earlier drafts of the bench had separate projects for each two-board lab (an I3C-only target, an FDCAN-only echo). That meant the customer had to re-flash chip B between stages, which broke the cumulative-project tenet (flash once, run the rest of the book). The unified firmware here brings up every chip-B-side peripheral the book uses, so flashing chip B is a one-time setup at the start of §S7.

§S9 (USB CDC) is a one-board lab — the customer flashes ST's Ux_Device_HID_CDC_ACM example temporarily to whichever board has a free USB cable, runs the lab, then re-flashes the chip B target firmware here. Chip B doesn't need to do anything for §S9.

Bench autonomy (no manual reset between iterations)

The H5 I3C peripheral keeps its dynamic address in DEVR0 across firmware iterations on chip A — meaning a second HAL_I3C_Ctrl_DynAddrAssign_IT(... I3C_ONLY_ENTDAA) call from chip A would silently no-op (chip B already has DA 0x32 from the last session). The clean fix is on the controller side: chip A uses I3C_RSTDAA_THEN_ENTDAA instead of I3C_ONLY_ENTDAA, which broadcasts a Reset Dynamic Address before the new ENTDAA so targets drop and re-enroll fresh.

Chip B's firmware adds an Independent Watchdog (IWDG) with a 3-second timeout, kicked on every main-loop iteration. If the I3C state machine ever does end up stuck in a no-progress loop (e.g., a future protocol corner the firmware doesn't anticipate), the watchdog reboots chip B autonomously. DBGMCU.APB1FZR1's DBG_IWDG_STOP bit is set so the IWDG freezes when the CPU is paused at a breakpoint — debugger sessions don't trip it.

Build & flash

  1. Open STM32CubeMX. File → New Project, select NUCLEO-H563ZI board. Click No when asked whether to enable TrustZone (this firmware runs in non-TZ mode for simplicity).
  2. Configure I3C1 on PB8/PB9 in Target mode. The IOC settings can be left at CubeMX defaults — the firmware overrides the timing fields at runtime.
  3. Configure FDCAN1 in Normal mode at 500 kbps nominal / 1 Mbps data. Pin PD0 = FDCAN1_RX, PD1 = FDCAN1_TX.
  4. Configure USART1 as the VCP (NUCLEO-H563ZI's ST-LINK VCP is wired to USART3, but USART1 is fine for this firmware too — adjust the BSP_COM_Init line if you change pins).
  5. Enable HAL_IWDG_MODULE_ENABLED in stm32h5xx_hal_conf.h.
  6. Generate code, drop the main.c below into Core/Src/main.c (replacing the generated file inside the USER CODE blocks), build, and flash the board you've chosen as chip B.

On boot the VCP at 115200 8N1 prints:

I3C Target ready — waiting for ENTDAA
FDCAN echo node ready — listening on FDCAN1 RX FIFO0, echo with ID+0x100

Green LED on. Done. Leave chip B running for the rest of the book.

Source — Core/Src/main.c

This is the complete main.c from the NUCLEO_H563ZI_I3C_Target STM32CubeIDE project. Copy it into your CubeMX-generated project's Core/Src/main.c, preserving the USER CODE block markers.

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2026 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define RX_BUF_SIZE 16
#define TX_BUF_SIZE 16
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

COM_InitTypeDef BspCOMInit;

I3C_HandleTypeDef hi3c1;

/* USER CODE BEGIN PV */
uint8_t tgt_rx[RX_BUF_SIZE];
uint8_t tgt_tx[TX_BUF_SIZE] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04};
volatile uint8_t tgt_rx_done = 0;
volatile uint8_t tgt_tx_done = 0;
volatile uint8_t got_daa = 0;
volatile uint8_t pending_reset_chip = 0;   /* set in IRQ on RSTACT 0x02; main loop reboots */
volatile uint8_t pending_reset_i3c  = 0;   /* set in IRQ on RSTACT 0x01; main loop reinit's */
volatile uint8_t da_assigned  = 0;    /* latched: 1 once DA has ever been assigned */
volatile uint8_t ibi_in_flight = 0;   /* set when IBIReq submitted, cleared on EVENT_ID_IBIEND */
volatile uint8_t need_rearm_rx = 0;   /* main loop re-arms receive when this is set */
volatile uint32_t ibi_count   = 0;
I3C_XferTypeDef tgt_xfer;

/* S8.3 — FDCAN1 echo node. Replies to received frames with ID+0x100,
 * same payload. Bit timing matches the v3 controller (500 kbps nominal,
 * 1 Mbps data, FD_BRS, sample point 75% / 70%). */
FDCAN_HandleTypeDef hfdcan1;
IWDG_HandleTypeDef  hiwdg;
static volatile uint32_t echo_rx_count = 0;
static volatile uint32_t echo_tx_count = 0;
static volatile uint32_t echo_last_rx_id = 0;
static volatile uint32_t echo_last_tx_id = 0;
static volatile uint32_t echo_last_err = 0;
static FDCAN_RxHeaderTypeDef echo_rx_hdr;
static FDCAN_TxHeaderTypeDef echo_tx_hdr;
static uint8_t echo_payload[64];
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MPU_Config(void);
static void MX_GPIO_Init(void);
static void MX_I3C1_Init(void);
static void MX_ICACHE_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* MPU Configuration--------------------------------------------------------*/
  MPU_Config();

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_I3C1_Init();
  MX_ICACHE_Init();
  /* USER CODE BEGIN 2 */
  /* Initialize leds */
    BSP_LED_Init(LED_GREEN);
    BSP_LED_Init(LED_YELLOW);
    BSP_LED_Init(LED_RED);

    /* Initialize USER push-button, will be used to trigger an interrupt each time it's pressed.*/
    BSP_PB_Init(BUTTON_USER, BUTTON_MODE_EXTI);

    /* Initialize COM1 port (115200, 8 bits (7-bit data + 1 stop bit), no parity */
    BspCOMInit.BaudRate   = 115200;
    BspCOMInit.WordLength = COM_WORDLENGTH_8B;
    BspCOMInit.StopBits   = COM_STOPBITS_1;
    BspCOMInit.Parity     = COM_PARITY_NONE;
    BspCOMInit.HwFlowCtl  = COM_HWCONTROL_NONE;
    if (BSP_COM_Init(COM1, &BspCOMInit) != BSP_ERROR_NONE)
    {
      Error_Handler();
    }
  /* Enable internal pull-ups on I3C pins */
    MODIFY_REG(GPIOB->PUPDR,
        GPIO_PUPDR_PUPD8 | GPIO_PUPDR_PUPD9,
        GPIO_PUPDR_PUPD8_0 | GPIO_PUPDR_PUPD9_0);

    /* Activate target notifications. Per ST reference example
     * I3C_Target_ENTDAA_IT, we only enable DAUPDIE here — pre-arming
     * ERRIE/FCIE or HAL_I3C_Tgt_Receive_IT before DA is assigned puts the
     * peripheral in a half-state where it doesn't terminate ENTDAA cleanly,
     * causing the controller's FCF to never fire. The receive IT gets armed
     * later in the main loop after the DA-update event lands. */
    /* DAUPDIE = dynamic-address update; IBIEND = our IBI request finished
     * (so we know when it's safe to issue another IBI). */
    HAL_I3C_ActivateNotification(&hi3c1, NULL,
        HAL_I3C_IT_DAUPDIE | EVENT_ID_IBIEND | HAL_I3C_IT_RSTIE);

    /* Independent Watchdog — autonomous recovery if the I3C state machine
     * ever gets stuck in a no-progress loop (e.g., post-IBI BUSY state
     * that won't accept fresh ENTDAA). LSI runs at ~32 kHz; prescaler /256
     * gives a 125 Hz tick, reload 375 → 3 second timeout. Main loop kicks
     * the watchdog every iteration; if it doesn't kick for 3 s, chip B
     * reboots cleanly. Customer flashes once and the firmware self-heals
     * across all I3C lab interactions. */
    DBGMCU->APB1FZR1 |= DBGMCU_APB1FZR1_DBG_IWDG_STOP;   /* halt IWDG when CPU is at a breakpoint */
    hiwdg.Instance = IWDG;
    hiwdg.Init.Prescaler = IWDG_PRESCALER_256;
    hiwdg.Init.Window    = IWDG_WINDOW_DISABLE;
    hiwdg.Init.Reload    = 375;
    if (HAL_IWDG_Init(&hiwdg) != HAL_OK) {
        BSP_LED_On(LED_RED);
    }

    /* S8.3 — FDCAN1 echo node. Init peripheral with the same parameters
     * the v3 controller uses (FD_BRS, 500k nominal / 1M data, prescaler 1).
     * Filter accepts every standard 11-bit ID into RX FIFO 0; the
     * RxFifo0Callback below replies with ID+0x100 on each received frame. */
    hfdcan1.Instance = FDCAN1;
    hfdcan1.Init.ClockDivider       = FDCAN_CLOCK_DIV1;
    hfdcan1.Init.FrameFormat        = FDCAN_FRAME_FD_BRS;
    hfdcan1.Init.Mode               = FDCAN_MODE_NORMAL;
    hfdcan1.Init.AutoRetransmission = DISABLE;
    hfdcan1.Init.TransmitPause      = DISABLE;
    hfdcan1.Init.ProtocolException  = DISABLE;
    /* Bit timing must match the v3 controller exactly (sum + sample point):
     * v3 nominal = 1+13+2 = 16 quanta @ 87.5% sample, SJW=1.
     * v3 data    = 1+5+2  = 8  quanta @ 75% sample,   SJW=1. */
    hfdcan1.Init.NominalPrescaler     = 1;
    hfdcan1.Init.NominalSyncJumpWidth = 1;
    hfdcan1.Init.NominalTimeSeg1      = 13;
    hfdcan1.Init.NominalTimeSeg2      = 2;
    hfdcan1.Init.DataPrescaler        = 1;
    hfdcan1.Init.DataSyncJumpWidth    = 1;
    hfdcan1.Init.DataTimeSeg1         = 5;
    hfdcan1.Init.DataTimeSeg2         = 2;
    hfdcan1.Init.StdFiltersNbr      = 1;
    hfdcan1.Init.ExtFiltersNbr      = 0;
    hfdcan1.Init.TxFifoQueueMode    = FDCAN_TX_FIFO_OPERATION;
    if (HAL_FDCAN_Init(&hfdcan1) != HAL_OK) Error_Handler();

    {
        FDCAN_FilterTypeDef f = {0};
        f.IdType       = FDCAN_STANDARD_ID;
        f.FilterIndex  = 0;
        f.FilterType   = FDCAN_FILTER_RANGE;
        f.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
        f.FilterID1    = 0x000;
        f.FilterID2    = 0x7FF;
        HAL_FDCAN_ConfigFilter(&hfdcan1, &f);
        HAL_FDCAN_ConfigGlobalFilter(&hfdcan1, FDCAN_REJECT, FDCAN_REJECT,
                                      FDCAN_FILTER_REMOTE, FDCAN_FILTER_REMOTE);
        HAL_FDCAN_Start(&hfdcan1);
        HAL_FDCAN_ActivateNotification(&hfdcan1,
            FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0);
    }

    printf("I3C Target ready — waiting for ENTDAA\r\n");
    printf("FDCAN echo node ready — listening on FDCAN1 RX FIFO0, echo with ID+0x100\r\n");
    BSP_LED_On(LED_GREEN);
  /* USER CODE END 2 */



  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  if (got_daa)
	        {
	            got_daa = 0;
	            da_assigned = 1;
	            printf("DAA complete — dynamic address assigned\r\n");
	            BSP_LED_On(LED_YELLOW);

	            /* Now that we have a DA, arm receive IT for incoming private
	             * writes from the controller. Doing this BEFORE DA assignment
	             * keeps the peripheral in a state that breaks ENTDAA. */
	            memset(tgt_rx, 0, RX_BUF_SIZE);
	            tgt_xfer.RxBuf.pBuffer = tgt_rx;
	            /* H5 target HAL treats RxBuf.Size as EXPECTED bytes, not max.
	             * If the controller sends fewer it raises HAL_I3C_ERROR_SIZE
	             * even though the bytes arrived correctly. Match the
	             * controller's 4-byte private write exactly. */
	            tgt_xfer.RxBuf.Size = 4;
	            HAL_I3C_Tgt_Receive_IT(&hi3c1, &tgt_xfer);
	        }

	        if (tgt_rx_done)
	        {
	            tgt_rx_done = 0;
	            printf("RX: %02X %02X %02X %02X\r\n",
	                tgt_rx[0], tgt_rx[1], tgt_rx[2], tgt_rx[3]);
	            BSP_LED_Toggle(LED_GREEN);

	            /* Do NOT auto-rearm receive here. The H5 target HAL puts the
	             * peripheral in BUSY_RX while a receive IT is armed, which
	             * blocks IBI submission. Leave the peripheral in READY so
	             * the user button can fire IBIs. Re-arm happens after each
	             * IBI completes (need_rearm_rx branch above). */
	        }

	        if (tgt_tx_done)
	        {
	            tgt_tx_done = 0;
	            printf("TX complete\r\n");
	        }

	        /* Re-arm receive IT after an IBI completes (we aborted it before
	         * the IBI submit so the state machine would accept the IBI). */
	        if (need_rearm_rx) {
	            need_rearm_rx = 0;
	            memset(tgt_rx, 0, RX_BUF_SIZE);
	            tgt_xfer.RxBuf.pBuffer = tgt_rx;
	            tgt_xfer.RxBuf.Size    = 4;
	            HAL_I3C_Tgt_Receive_IT(&hi3c1, &tgt_xfer);
	            printf("RX re-armed after IBI\r\n");
	        }
	        /* IWDG keepalive — kicked every main-loop iteration so that if
	         * the I3C state machine ever gets stuck in a no-progress loop
	         * (e.g., post-IBI BUSY state that never clears), the watchdog
	         * fires after WATCHDOG_TIMEOUT and chip B reboots cleanly. */
	        HAL_IWDG_Refresh(&hiwdg);

	        /* S7.2 IBI: USER button (B1, PC13, active-low — asserted when LOW)
	         * triggers an empty-payload IBI to the controller. Edge-detected
	         * with debounce; only allowed once we have a DA AND no IBI is
	         * in flight. */
	        {
	            static GPIO_PinState last_btn = GPIO_PIN_SET;
	            GPIO_PinState btn = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13);
	            if (last_btn == GPIO_PIN_SET && btn == GPIO_PIN_RESET) {
	                HAL_Delay(20);  /* debounce */
	                if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET
	                    && da_assigned
	                    && ibi_in_flight == 0) {
	                    HAL_I3C_StateTypeDef st = HAL_I3C_GetState(&hi3c1);
	                    ibi_in_flight = 1;
	                    HAL_StatusTypeDef rc = HAL_I3C_Tgt_IBIReq_IT(&hi3c1, NULL, 0U);
	                    printf("IBI request submitted (rc=0x%02X, count=%lu, prev_state=0x%02X)\r\n",
	                        rc, ++ibi_count, st);
	                    if (rc != HAL_OK) ibi_in_flight = 0;
	                }
	            }
	            last_btn = btn;
	        }
	        {
	                  static uint32_t last_toggle = 0;
	                  if (HAL_GetTick() - last_toggle >= 500)
	                  {
	                      BSP_LED_Toggle(LED_RED);
	                      last_toggle = HAL_GetTick();
	                  }
	              }
	        HAL_Delay(10);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Configure the main internal regulator output voltage
  */
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE0);

  while(!__HAL_PWR_GET_FLAG(PWR_FLAG_VOSRDY)) {}

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_BYPASS;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLL1_SOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 4;
  RCC_OscInitStruct.PLL.PLLN = 250;
  RCC_OscInitStruct.PLL.PLLP = 2;
  RCC_OscInitStruct.PLL.PLLQ = 2;
  RCC_OscInitStruct.PLL.PLLR = 2;
  RCC_OscInitStruct.PLL.PLLRGE = RCC_PLL1_VCIRANGE_1;
  RCC_OscInitStruct.PLL.PLLVCOSEL = RCC_PLL1_VCORANGE_WIDE;
  RCC_OscInitStruct.PLL.PLLFRACN = 0;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2
                              |RCC_CLOCKTYPE_PCLK3;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB3CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure the programming delay
  */
  __HAL_FLASH_SET_PROGRAM_DELAY(FLASH_PROGRAMMING_DELAY_2);
}

/**
  * @brief I3C1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_I3C1_Init(void)
{

  /* USER CODE BEGIN I3C1_Init 0 */

  /* USER CODE END I3C1_Init 0 */

  I3C_FifoConfTypeDef sFifoConfig = {0};
  I3C_TgtConfTypeDef sTgtConfig = {0};

  /* USER CODE BEGIN I3C1_Init 1 */

  /* USER CODE END I3C1_Init 1 */
  hi3c1.Instance = I3C1;
  hi3c1.Mode = HAL_I3C_MODE_TARGET;
  hi3c1.Init.TgtBusCharacteristic.BusAvailableDuration = 0xf8;
  if (HAL_I3C_Init(&hi3c1) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure FIFO
  */
  sFifoConfig.RxFifoThreshold = HAL_I3C_RXFIFO_THRESHOLD_1_4;
  sFifoConfig.TxFifoThreshold = HAL_I3C_TXFIFO_THRESHOLD_1_4;
  sFifoConfig.ControlFifo = HAL_I3C_CONTROLFIFO_DISABLE;
  sFifoConfig.StatusFifo = HAL_I3C_STATUSFIFO_DISABLE;

  if (HAL_I3C_SetConfigFifo(&hi3c1, &sFifoConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Target
  */
  sTgtConfig.Identifier = 0;
  sTgtConfig.MIPIIdentifier = 0;
  sTgtConfig.CtrlRoleRequest = DISABLE;
  sTgtConfig.HotJoinRequest = DISABLE;
  sTgtConfig.IBIRequest = ENABLE;   /* S7.2: target may issue In-Band Interrupts */
  sTgtConfig.IBIPayload = DISABLE;  /* keep payload empty for the simplest demo */
  sTgtConfig.IBIPayloadSize = HAL_I3C_PAYLOAD_1_BYTE;
  sTgtConfig.MaxReadDataSize = 0;
  sTgtConfig.MaxWriteDataSize = 0;
  sTgtConfig.CtrlCapability = DISABLE;
  sTgtConfig.GroupAddrCapability = DISABLE;
  sTgtConfig.DataTurnAroundDuration = HAL_I3C_TURNAROUND_TIME_TSCO_LESS_12NS;
  sTgtConfig.MaxReadTurnAround = 0;
  sTgtConfig.MaxDataSpeed = HAL_I3C_GETMXDS_FORMAT_1;
  sTgtConfig.MaxSpeedLimitation = DISABLE;
  sTgtConfig.HandOffActivityState = HAL_I3C_HANDOFF_ACTIVITY_STATE_0;
  sTgtConfig.HandOffDelay = DISABLE;
  sTgtConfig.PendingReadMDB = DISABLE;
  if (HAL_I3C_Tgt_Config(&hi3c1, &sTgtConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN I3C1_Init 2 */

  /* USER CODE END I3C1_Init 2 */

}

/**
  * @brief ICACHE Initialization Function
  * @param None
  * @retval None
  */
static void MX_ICACHE_Init(void)
{

  /* USER CODE BEGIN ICACHE_Init 0 */

  /* USER CODE END ICACHE_Init 0 */

  /* USER CODE BEGIN ICACHE_Init 1 */

  /* USER CODE END ICACHE_Init 1 */

  /** Enable instruction cache in 1-way (direct mapped cache)
  */
  if (HAL_ICACHE_ConfigAssociativityMode(ICACHE_1WAY) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_ICACHE_Enable() != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN ICACHE_Init 2 */

  /* USER CODE END ICACHE_Init 2 */

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  /* USER CODE BEGIN MX_GPIO_Init_1 */

  /* USER CODE END MX_GPIO_Init_1 */

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOE_CLK_ENABLE();
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOF_CLK_ENABLE();
  __HAL_RCC_GPIOH_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOG_CLK_ENABLE();

  /*Configure GPIO pins : RMII_MDC_Pin RMII_RXD0_Pin RMII_RXD1_Pin */
  GPIO_InitStruct.Pin = RMII_MDC_Pin|RMII_RXD0_Pin|RMII_RXD1_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  /*Configure GPIO pins : RMII_REF_CLK_Pin RMII_MDIO_Pin RMII_CRS_DV_Pin */
  GPIO_InitStruct.Pin = RMII_REF_CLK_Pin|RMII_MDIO_Pin|RMII_CRS_DV_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /*Configure GPIO pin : VBUS_SENSE_Pin */
  GPIO_InitStruct.Pin = VBUS_SENSE_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(VBUS_SENSE_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pins : UCPD_CC1_Pin UCPD_CC2_Pin */
  GPIO_InitStruct.Pin = UCPD_CC1_Pin|UCPD_CC2_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  /*Configure GPIO pin : RMII_TXD1_Pin */
  GPIO_InitStruct.Pin = RMII_TXD1_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
  HAL_GPIO_Init(RMII_TXD1_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pin : UCPD_FLT_Pin */
  GPIO_InitStruct.Pin = UCPD_FLT_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(UCPD_FLT_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pins : USB_FS_N_Pin USB_FS_P_Pin */
  GPIO_InitStruct.Pin = USB_FS_N_Pin|USB_FS_P_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  GPIO_InitStruct.Alternate = GPIO_AF10_USB;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /*Configure GPIO pins : RMII_TXT_EN_Pin RMI_TXD0_Pin */
  GPIO_InitStruct.Pin = RMII_TXT_EN_Pin|RMI_TXD0_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
  HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);

  /*Configure GPIO pins : ARD_D1_TX_Pin ARD_D0_RX_Pin */
  GPIO_InitStruct.Pin = ARD_D1_TX_Pin|ARD_D0_RX_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  GPIO_InitStruct.Alternate = GPIO_AF8_LPUART1;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  /* USER CODE BEGIN MX_GPIO_Init_2 */

  /* USER CODE END MX_GPIO_Init_2 */
}

/* USER CODE BEGIN 4 */
void HAL_I3C_TgtRxCpltCallback(I3C_HandleTypeDef *hi3c)
{
    tgt_rx_done = 1;
}

void HAL_I3C_TgtTxCpltCallback(I3C_HandleTypeDef *hi3c)
{
    tgt_tx_done = 1;
}

void HAL_I3C_NotifyCallback(I3C_HandleTypeDef *hi3c, uint32_t eventId)
{
    if (eventId & EVENT_ID_DAU)
    {
        got_daa = 1;
    }
    if (eventId & EVENT_ID_IBIEND)
    {
        ibi_in_flight = 0;
        need_rearm_rx = 1;   /* main loop re-arms the receive IT */
    }
    if (eventId & EVENT_ID_RSTACT)
    {
        /* I3C v1.1 Reset Action CCC. Set a flag for the main loop to act
         * on — printf and NVIC_SystemReset are not safe to call from IRQ
         * context (newlib printf is non-reentrant; HAL_Delay spins on
         * SysTick which can't fire from inside another IRQ). */
        I3C_CCCInfoTypeDef info = {0};
        if (HAL_I3C_GetCCCInfo(hi3c, EVENT_ID_RSTACT, &info) == HAL_OK)
        {
            if (info.ResetAction == 0x02U) {
                pending_reset_chip = 1;
            } else if (info.ResetAction == 0x01U) {
                pending_reset_i3c = 1;
            }
        }
    }
}

void HAL_I3C_ErrorCallback(I3C_HandleTypeDef *hi3c)
{
    printf("I3C Tgt Err: 0x%08lX\r\n", HAL_I3C_GetError(hi3c));
    BSP_LED_On(LED_RED);
}

/* S8.3 — FDCAN MspInit override (weak in HAL). Configures FDCAN kernel
 * clock from HSE, enables peripheral clock, sets PD0/PD1 as AF9_FDCAN1,
 * arms the FDCAN1_IT0 NVIC. */
void HAL_FDCAN_MspInit(FDCAN_HandleTypeDef *hfdcan)
{
    if (hfdcan->Instance != FDCAN1) return;

    RCC_PeriphCLKInitTypeDef p = {0};
    p.PeriphClockSelection = RCC_PERIPHCLK_FDCAN;
    p.FdcanClockSelection  = RCC_FDCANCLKSOURCE_HSE;
    if (HAL_RCCEx_PeriphCLKConfig(&p) != HAL_OK) Error_Handler();

    __HAL_RCC_FDCAN_CLK_ENABLE();
    __HAL_RCC_GPIOD_CLK_ENABLE();

    GPIO_InitTypeDef g = {0};
    g.Pin       = GPIO_PIN_0 | GPIO_PIN_1;
    g.Mode      = GPIO_MODE_AF_PP;
    g.Pull      = GPIO_NOPULL;
    g.Speed     = GPIO_SPEED_FREQ_LOW;
    g.Alternate = GPIO_AF9_FDCAN1;
    HAL_GPIO_Init(GPIOD, &g);

    HAL_NVIC_SetPriority(FDCAN1_IT0_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(FDCAN1_IT0_IRQn);
}

/* S8.3 — RX FIFO0 callback. Pulls each received frame, transmits an echo
 * with Identifier += 0x100 and identical DLC/FDFormat/BRS/payload. The
 * +0x100 offset means a sender at ID 0x300 hears its own echo at 0x400,
 * which the controller-side test code uses to distinguish self-RX from
 * the echoed reply. */
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
    if (hfdcan->Instance != FDCAN1) return;
    if (!(RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE)) return;

    if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0,
                                &echo_rx_hdr, echo_payload) != HAL_OK) {
        echo_last_err = 0xE1;
    } else {
        echo_rx_count++;
        echo_last_rx_id = echo_rx_hdr.Identifier;

        echo_tx_hdr.Identifier          = echo_rx_hdr.Identifier + 0x100;
        echo_tx_hdr.IdType              = echo_rx_hdr.IdType;
        echo_tx_hdr.TxFrameType         = FDCAN_DATA_FRAME;
        echo_tx_hdr.DataLength          = echo_rx_hdr.DataLength;
        echo_tx_hdr.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
        echo_tx_hdr.BitRateSwitch       = echo_rx_hdr.BitRateSwitch;
        echo_tx_hdr.FDFormat            = echo_rx_hdr.FDFormat;
        echo_tx_hdr.TxEventFifoControl  = FDCAN_NO_TX_EVENTS;
        echo_tx_hdr.MessageMarker       = 0;

        if (HAL_FDCAN_AddMessageToTxFifoQ(hfdcan, &echo_tx_hdr, echo_payload) != HAL_OK) {
            echo_last_err = 0xE2;
        } else {
            echo_tx_count++;
            echo_last_tx_id = echo_tx_hdr.Identifier;
        }
    }

    HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0);
}

void FDCAN1_IT0_IRQHandler(void)
{
    HAL_FDCAN_IRQHandler(&hfdcan1);
}
/* USER CODE END 4 */

 /* MPU Configuration */

void MPU_Config(void)
{
  MPU_Region_InitTypeDef MPU_InitStruct = {0};
  MPU_Attributes_InitTypeDef MPU_AttributesInit = {0};

  /* Disables the MPU */
  HAL_MPU_Disable();

  /** Initializes and configures the Region 0 and the memory to be protected
  */
  MPU_InitStruct.Enable = MPU_REGION_ENABLE;
  MPU_InitStruct.Number = MPU_REGION_NUMBER0;
  MPU_InitStruct.BaseAddress = 0x08FFF000;
  MPU_InitStruct.LimitAddress = 0x08FFFFFF;
  MPU_InitStruct.AttributesIndex = MPU_ATTRIBUTES_NUMBER0;
  MPU_InitStruct.AccessPermission = MPU_REGION_ALL_RO;
  MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
  MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;

  HAL_MPU_ConfigRegion(&MPU_InitStruct);

  /** Initializes and configures the Attribute 0 and the memory to be protected
  */
  MPU_AttributesInit.Number = MPU_ATTRIBUTES_NUMBER0;
  MPU_AttributesInit.Attributes = INNER_OUTER(MPU_NOT_CACHEABLE);

  HAL_MPU_ConfigMemoryAttributes(&MPU_AttributesInit);
  /* Enables the MPU */
  HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

}

/**
  * @brief  Period elapsed callback in non blocking mode
  * @note   This function is called  when TIM6 interrupt took place, inside
  * HAL_TIM_IRQHandler(). It makes a direct call to HAL_IncTick() to increment
  * a global variable "uwTick" used as application time base.
  * @param  htim : TIM handle
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */

  /* USER CODE END Callback 0 */
  if (htim->Instance == TIM6)
  {
    HAL_IncTick();
  }
  /* USER CODE BEGIN Callback 1 */

  /* USER CODE END Callback 1 */
}

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
Volume 1 — STM32H5 · Chip B Target Firmware · Source rev v2.8 · Last revised 2026-05-03