#include "ami_app.h" #include "ami_protocol.h" #include "fdcan.h" #include "usart.h" #include #include #include #include #define SCREEN_RX_DMA_SIZE 64U #define SCREEN_TX_SIZE 32U #define TASK_QUEUE_SIZE 4U #define DIRTY_SPEED (1UL << 0) #define DIRTY_TS_VOLTAGE (1UL << 1) #define DIRTY_TORQUE (1UL << 2) #define DIRTY_TS_CURRENT (1UL << 3) #define DIRTY_MOTOR_TEMP (1UL << 4) #define DIRTY_MCU_TEMP (1UL << 5) #define DIRTY_CELL_TEMP (1UL << 6) #define DIRTY_DEBUG(n) (1UL << (8U + (n))) #define DIRTY_DASH_ALL 0x000000FFUL #define DIRTY_COLOR(n) (1UL << (24U + (n))) #define DIRTY_COLOR_ALL 0xFF000000UL #define DIRTY_DEBUG_ALL 0x00FFFF00UL typedef struct { /* speed, HV V, torque, HV A, motor C, MCU C, max cell C, LV 0.01V. */ int32_t dash_value[8]; uint8_t dash_valid; uint8_t debug_state[16]; } AmiDisplayState; static volatile AmiDisplayState display_state; static volatile uint32_t display_dirty; static volatile uint32_t dash_last_rx_ms; static volatile uint32_t debug_last_rx_ms; static volatile uint32_t adc_last_rx_ms; static volatile uint32_t bms_status_last_rx_ms; static volatile uint32_t bms_temperature_last_rx_ms; static volatile uint32_t drivetrain_last_rx_ms; static volatile uint32_t vcu_status_last_rx_ms; /* Task 7 = unknown; lock/EBS 0 = unknown, 1 = green, 2 = red. */ static volatile uint8_t vcu_display_state[3] = {7U, 0U, 0U}; static volatile uint8_t vcu_display_dirty; static volatile bool vcu_status_received; /* Preserve the exact source units even where the HMI has integer controls. */ static volatile uint16_t sdc_adc_mv; static volatile uint16_t sdc_frequency_hz[4]; static volatile uint16_t sdc_gpio_state; static volatile uint16_t sdc_gpio_changed; static volatile uint32_t sdc_gpio_timestamp_ms; static volatile bool sdc_gpio_received; static volatile uint16_t bms_voltage_tenths; static volatile int32_t bms_current_tenths; static volatile uint32_t can_port_last_rx_ms[2]; static volatile uint32_t can_rx_error_count; static volatile uint32_t uart_rx_error_count; static uint8_t screen_rx_dma[SCREEN_RX_DMA_SIZE]; static uint8_t screen_tx_dma[SCREEN_TX_SIZE]; static volatile bool screen_tx_busy; static uint8_t parser_pos; static uint8_t parser_task_id; static volatile uint8_t task_queue[TASK_QUEUE_SIZE]; static volatile uint8_t task_head; static volatile uint8_t task_tail; static uint8_t task_sequence; static uint8_t task_tx_mask; static uint8_t task_target_mask; static volatile uint32_t task_drop_count; static GPIO_PinState LedInactiveLevel(void) { return (AMI_LED_ACTIVE_LEVEL == GPIO_PIN_SET) ? GPIO_PIN_RESET : GPIO_PIN_SET; } static uint32_t ReadBeU16(const uint8_t *data) { return ((uint32_t)data[0] << 8) | (uint32_t)data[1]; } static uint32_t ReadLeU16(const uint8_t *data) { return (uint32_t)data[0] | ((uint32_t)data[1] << 8); } static int32_t RoundTenths(int32_t value) { return (value >= 0) ? ((value + 5) / 10) : ((value - 5) / 10); } static int32_t ReadLeS16(const uint8_t *data) { uint32_t raw = ReadLeU16(data); return (raw & 0x8000U) ? ((int32_t)raw - 65536) : (int32_t)raw; } static size_t AppendUnsigned(char *dst, uint32_t value) { char reversed[10]; size_t count = 0U; size_t i; do { reversed[count++] = (char)('0' + (value % 10U)); value /= 10U; } while ((value != 0U) && (count < sizeof(reversed))); for (i = 0U; i < count; ++i) { dst[i] = reversed[count - 1U - i]; } return count; } static size_t AppendSigned(char *dst, int32_t value) { uint32_t magnitude; size_t count = 0U; if (value < 0) { dst[count++] = '-'; magnitude = (uint32_t)(-(value + 1)) + 1U; } else { magnitude = (uint32_t)value; } return count + AppendUnsigned(&dst[count], magnitude); } static bool StartScreenAssignment(const char *prefix, int32_t value) { size_t length = strlen(prefix); if (screen_tx_busy || (length + 14U > sizeof(screen_tx_dma))) { return false; } memcpy(screen_tx_dma, prefix, length); length += AppendSigned((char *)&screen_tx_dma[length], value); screen_tx_dma[length++] = 0xFFU; screen_tx_dma[length++] = 0xFFU; screen_tx_dma[length++] = 0xFFU; screen_tx_busy = true; if (HAL_UART_Transmit_DMA(&huart2, screen_tx_dma, (uint16_t)length) != HAL_OK) { screen_tx_busy = false; return false; } return true; } static void RestartScreenRx(void) { if (HAL_UARTEx_ReceiveToIdle_DMA(&huart2, screen_rx_dma, sizeof(screen_rx_dma)) == HAL_OK) { __HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT); } else { ++uart_rx_error_count; } } static void QueueTaskFromIsr(uint8_t task_id) { uint8_t next = (uint8_t)((task_head + 1U) % TASK_QUEUE_SIZE); if (next == task_tail) { ++task_drop_count; return; } task_queue[task_head] = task_id; task_head = next; } static void ParseScreenByte(uint8_t byte) { static const uint8_t fixed[6] = {0xAAU, 0x55U, 0x01U, 0x00U, 0x0DU, 0x0AU}; switch (parser_pos) { case 0U: parser_pos = (byte == fixed[0]) ? 1U : 0U; break; case 1U: parser_pos = (byte == fixed[1]) ? 2U : ((byte == fixed[0]) ? 1U : 0U); break; case 2U: parser_pos = (byte == fixed[2]) ? 3U : ((byte == fixed[0]) ? 1U : 0U); break; case 3U: if ((byte >= 1U) && (byte <= 6U)) { parser_task_id = byte; parser_pos = 4U; } else { parser_pos = (byte == fixed[0]) ? 1U : 0U; } break; case 4U: parser_pos = (byte == fixed[4]) ? 5U : ((byte == fixed[0]) ? 1U : 0U); break; case 5U: if (byte == fixed[5]) { QueueTaskFromIsr(parser_task_id); } parser_pos = (byte == fixed[0]) ? 1U : 0U; break; default: parser_pos = 0U; break; } } static void SetDashValid(uint32_t index, bool valid) { uint8_t mask = (uint8_t)(1U << index); bool previous = (display_state.dash_valid & mask) != 0U; if (valid != previous) { if (valid) { display_state.dash_valid |= mask; } else { display_state.dash_valid &= (uint8_t)~mask; } display_dirty |= DIRTY_COLOR(index); } } static void SetDashValue(uint32_t index, int32_t value) { if ((display_state.dash_value[index] != value) || ((display_state.dash_valid & (1U << index)) == 0U)) { display_state.dash_value[index] = value; display_dirty |= (1UL << index); } SetDashValid(index, true); } static void UpdateDebugFromSdcGpio(uint16_t levels) { /* PD order supplied by the wiring table; HMI dN order is different. */ static const uint8_t debug_index[15] = { 0U, 5U, 2U, 6U, 7U, 1U, 3U, 4U, 8U, 9U, 10U, 11U, 12U, 13U, 14U }; for (uint32_t bit = 0U; bit < 15U; ++bit) { uint32_t index = debug_index[bit]; /* All fifteen inputs: low = normal, high = fault. */ uint8_t state = (levels & (1U << bit)) ? 2U : 1U; if (display_state.debug_state[index] != state) { display_state.debug_state[index] = state; display_dirty |= DIRTY_DEBUG(index); } } /* d15 (12V_Control) has no source in the fifteen-input table. */ } static bool DecodeCan(uint32_t identifier, uint32_t id_type, uint32_t length, const uint8_t *data) { uint32_t now = HAL_GetTick(); if (id_type == FDCAN_STANDARD_ID) { if (identifier == AMI_CAN_ID_VCU_STATUS) { /* Custom VCU feedback; requests never update these actual states. */ if ((length != 8U) || (data[7] != 1U) || (data[0] > 6U) || (data[1] > 1U) || (data[2] > 1U) || ((data[3] & 0xF8U) != 0U) || (data[5] != 0U) || (data[6] != 0U)) { return false; } uint8_t states[3] = { (data[3] & 1U) ? data[0] : 7U, (data[3] & 2U) ? (data[1] ? 1U : 2U) : 0U, (data[3] & 4U) ? (data[2] ? 1U : 2U) : 0U }; for (uint32_t i = 0U; i < 3U; ++i) { if (vcu_display_state[i] != states[i]) { vcu_display_state[i] = states[i]; vcu_display_dirty |= (uint8_t)(1U << i); } } vcu_status_last_rx_ms = now; vcu_status_received = true; return true; } if (identifier == AMI_CAN_ID_DRIVETRAIN_DISPLAY) { /* Version 1 display input only; no motor actuation is performed. */ if ((length != 8U) || (data[7] != 1U) || ((data[6] & 0xF8U) != 0U)) { return false; } int32_t torque = ReadLeS16(data); if ((data[6] & 1U) && (torque >= -1000) && (torque <= 1000)) { SetDashValue(2U, torque); } else { SetDashValid(2U, false); } if (data[6] & 2U) { SetDashValue(4U, RoundTenths(ReadLeS16(&data[2]))); } else { SetDashValid(4U, false); } if (data[6] & 4U) { SetDashValue(5U, RoundTenths(ReadLeS16(&data[4]))); } else { SetDashValid(5U, false); } drivetrain_last_rx_ms = now; dash_last_rx_ms = now; return true; } if ((identifier == AMI_CAN_ID_SDC_ADC) && (length == 2U)) { sdc_adc_mv = (uint16_t)ReadLeU16(data); /* Xfloat lvbat has two decimals: mV -> hundredths of a volt. */ SetDashValue(7U, (int32_t)((sdc_adc_mv + 5U) / 10U)); adc_last_rx_ms = now; dash_last_rx_ms = now; return true; } if ((identifier == AMI_CAN_ID_SDC_HEARTBEAT) && (length == 8U) && (data[0] == 0xA1U) && (data[1] == 1U)) { debug_last_rx_ms = now; return true; } if ((identifier == AMI_CAN_ID_SDC_FREQUENCY) && (length == 8U)) { for (uint32_t i = 0U; i < 4U; ++i) { sdc_frequency_hz[i] = (uint16_t)ReadLeU16(&data[i * 2U]); } /* Hz is not km/h; wheel circumference and pulses/rev are undefined. */ return true; } if ((identifier == AMI_CAN_ID_SDC_GPIO) && (length == 8U)) { sdc_gpio_state = (uint16_t)(ReadLeU16(data) & 0x7FFFU); sdc_gpio_changed = (uint16_t)(ReadLeU16(&data[2]) & 0x7FFFU); sdc_gpio_timestamp_ms = (uint32_t)data[4] | ((uint32_t)data[5] << 8) | ((uint32_t)data[6] << 16) | ((uint32_t)data[7] << 24); sdc_gpio_received = true; debug_last_rx_ms = now; UpdateDebugFromSdcGpio(sdc_gpio_state); return true; } if ((identifier == AMI_CAN_ID_SDC_DAC_ACK) && (length == 4U)) { return true; } if ((identifier == AMI_CAN_ID_BMS_TEMPERATURE) && (length == 8U)) { int32_t maximum = -40; bool valid = false; for (uint32_t i = 0U; i < 8U; ++i) { if (data[i] != 0xFFU) { int32_t temperature = (int32_t)data[i] - 40; if (!valid || (temperature > maximum)) { maximum = temperature; } valid = true; } } if (valid) { SetDashValue(6U, maximum); } else { SetDashValid(6U, false); } bms_temperature_last_rx_ms = now; dash_last_rx_ms = now; return true; } } if (((id_type == FDCAN_STANDARD_ID) && (identifier == AMI_CAN_ID_BMS_STATUS)) || ((id_type == FDCAN_EXTENDED_ID) && (identifier == AMI_CAN_ID_BMS_STATUS_EXT))) { if (length != 8U) { return false; } bms_voltage_tenths = (uint16_t)ReadBeU16(data); bms_current_tenths = (int32_t)ReadBeU16(&data[2]) - 5000; SetDashValue(1U, RoundTenths(bms_voltage_tenths)); SetDashValue(3U, RoundTenths(bms_current_tenths)); bms_status_last_rx_ms = now; dash_last_rx_ms = now; return true; } if ((id_type == FDCAN_EXTENDED_ID) && (identifier == AMI_CAN_ID_BMS_EXTREMES_EXT) && (length == 8U)) { if (data[4] != 0xFFU) { SetDashValue(6U, (int32_t)data[4] - 40); } else { SetDashValid(6U, false); } bms_temperature_last_rx_ms = now; dash_last_rx_ms = now; /* BMS LV bytes 6..7 do not override the SDC ADC source. */ return true; } return false; } static void ConfigureCanPort(FDCAN_HandleTypeDef *hfdcan) { FDCAN_FilterTypeDef filter = {0}; filter.IdType = FDCAN_STANDARD_ID; filter.FilterIndex = 0U; filter.FilterType = FDCAN_FILTER_RANGE; filter.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; filter.FilterID1 = AMI_CAN_ID_SDC_HEARTBEAT; filter.FilterID2 = AMI_CAN_ID_SDC_GPIO; if (HAL_FDCAN_ConfigFilter(hfdcan, &filter) != HAL_OK) { Error_Handler(); } filter.FilterIndex = 1U; filter.FilterType = FDCAN_FILTER_MASK; filter.FilterID1 = AMI_CAN_ID_BMS_STATUS; filter.FilterID2 = 0x7FFU; if (HAL_FDCAN_ConfigFilter(hfdcan, &filter) != HAL_OK) { Error_Handler(); } filter.FilterIndex = 2U; filter.FilterID1 = AMI_CAN_ID_BMS_TEMPERATURE; if (HAL_FDCAN_ConfigFilter(hfdcan, &filter) != HAL_OK) { Error_Handler(); } filter.FilterIndex = 3U; filter.FilterType = FDCAN_FILTER_DUAL; filter.FilterID1 = AMI_CAN_ID_DRIVETRAIN_DISPLAY; filter.FilterID2 = AMI_CAN_ID_VCU_STATUS; if (HAL_FDCAN_ConfigFilter(hfdcan, &filter) != HAL_OK) { Error_Handler(); } filter.IdType = FDCAN_EXTENDED_ID; filter.FilterIndex = 0U; filter.FilterType = FDCAN_FILTER_DUAL; filter.FilterID1 = AMI_CAN_ID_BMS_STATUS_EXT; filter.FilterID2 = AMI_CAN_ID_BMS_EXTREMES_EXT; if (HAL_FDCAN_ConfigFilter(hfdcan, &filter) != HAL_OK) { Error_Handler(); } } static void ConfigureCan(void) { ConfigureCanPort(&hfdcan1); ConfigureCanPort(&hfdcan2); if ((HAL_FDCAN_ConfigGlobalFilter(&hfdcan1, FDCAN_REJECT, FDCAN_REJECT, FDCAN_REJECT_REMOTE, FDCAN_REJECT_REMOTE) != HAL_OK) || (HAL_FDCAN_ConfigGlobalFilter(&hfdcan2, FDCAN_REJECT, FDCAN_REJECT, FDCAN_REJECT_REMOTE, FDCAN_REJECT_REMOTE) != HAL_OK) || (HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0U) != HAL_OK) || (HAL_FDCAN_ActivateNotification(&hfdcan2, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0U) != HAL_OK) || (HAL_FDCAN_Start(&hfdcan1) != HAL_OK) || (HAL_FDCAN_Start(&hfdcan2) != HAL_OK)) { Error_Handler(); } } static void SendPendingTask(void) { FDCAN_TxHeaderTypeDef header = {0}; uint8_t data[8]; uint8_t task_id; uint32_t now; if (task_tail == task_head) { return; } task_id = task_queue[task_tail]; now = HAL_GetTick(); if (task_target_mask == 0U) { if ((can_port_last_rx_ms[0] != 0U) && ((now - can_port_last_rx_ms[0]) <= AMI_CAN_DATA_TIMEOUT_MS)) { task_target_mask |= 0x01U; } if ((can_port_last_rx_ms[1] != 0U) && ((now - can_port_last_rx_ms[1]) <= AMI_CAN_DATA_TIMEOUT_MS)) { task_target_mask |= 0x02U; } if (task_target_mask == 0U) { task_target_mask = 0x03U; } } data[0] = 1U; data[1] = task_id; data[2] = task_sequence; data[3] = 1U; data[4] = (uint8_t)now; data[5] = (uint8_t)(now >> 8); data[6] = (uint8_t)(now >> 16); data[7] = (uint8_t)(now >> 24); header.Identifier = AMI_CAN_ID_TASK_REQUEST; header.IdType = FDCAN_STANDARD_ID; header.TxFrameType = FDCAN_DATA_FRAME; header.DataLength = FDCAN_DLC_BYTES_8; header.ErrorStateIndicator = FDCAN_ESI_ACTIVE; header.BitRateSwitch = FDCAN_BRS_OFF; header.FDFormat = FDCAN_CLASSIC_CAN; header.TxEventFifoControl = FDCAN_NO_TX_EVENTS; header.MessageMarker = 0U; if (((task_target_mask & 0x01U) != 0U) && ((task_tx_mask & 0x01U) == 0U) && (HAL_FDCAN_GetTxFifoFreeLevel(&hfdcan1) > 0U) && (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &header, data) == HAL_OK)) { task_tx_mask |= 0x01U; } if (((task_target_mask & 0x02U) != 0U) && ((task_tx_mask & 0x02U) == 0U) && (HAL_FDCAN_GetTxFifoFreeLevel(&hfdcan2) > 0U) && (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan2, &header, data) == HAL_OK)) { task_tx_mask |= 0x02U; } if ((task_tx_mask & task_target_mask) == task_target_mask) { task_tail = (uint8_t)((task_tail + 1U) % TASK_QUEUE_SIZE); ++task_sequence; task_tx_mask = 0U; task_target_mask = 0U; } } static void SendHeartbeat(void) { static uint32_t last_tx_ms; static uint16_t sequence; uint32_t now = HAL_GetTick(); FDCAN_TxHeaderTypeDef header = {0}; uint8_t data[8]; if ((uint32_t)(now - last_tx_ms) < AMI_HEARTBEAT_PERIOD_MS) { return; } /* Skip missed periods instead of generating a catch-up burst. */ last_tx_ms = now; data[0] = 1U; /* Protocol version. */ data[1] = 0U; /* Reserved. */ data[2] = (uint8_t)sequence; data[3] = (uint8_t)(sequence >> 8); data[4] = (uint8_t)now; data[5] = (uint8_t)(now >> 8); data[6] = (uint8_t)(now >> 16); data[7] = (uint8_t)(now >> 24); ++sequence; header.Identifier = AMI_CAN_ID_HEARTBEAT; header.IdType = FDCAN_STANDARD_ID; header.TxFrameType = FDCAN_DATA_FRAME; header.DataLength = FDCAN_DLC_BYTES_8; header.ErrorStateIndicator = FDCAN_ESI_ACTIVE; header.BitRateSwitch = FDCAN_BRS_OFF; header.FDFormat = FDCAN_CLASSIC_CAN; header.TxEventFifoControl = FDCAN_NO_TX_EVENTS; /* CAN1 (500 kbit/s) only; reserve one FIFO slot for task requests. A busy port skips this period; no software backlog or blocking retry. */ if (HAL_FDCAN_GetTxFifoFreeLevel(&hfdcan1) > 1U) { (void)HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &header, data); } } static void UpdateTimeoutsAndLeds(void) { static uint32_t last_heartbeat_ms; static bool debug_timeout_applied; uint32_t now = HAL_GetTick(); bool dash_timeout = (dash_last_rx_ms == 0U) || ((now - dash_last_rx_ms) > AMI_CAN_DATA_TIMEOUT_MS); bool debug_timeout = (debug_last_rx_ms == 0U) || ((now - debug_last_rx_ms) > AMI_CAN_DATA_TIMEOUT_MS); if ((now - last_heartbeat_ms) >= 500U) { HAL_GPIO_TogglePin(STATUS_LED1_GPIO_Port, STATUS_LED1_Pin); last_heartbeat_ms = now; } HAL_GPIO_WritePin(STATUS_LED2_GPIO_Port, STATUS_LED2_Pin, (dash_timeout || debug_timeout) ? AMI_LED_ACTIVE_LEVEL : LedInactiveLevel()); /* Each physical source expires separately; other traffic cannot refresh it. */ __disable_irq(); /* Sample time after excluding RX IRQs: a newer timestamp must not underflow. */ now = HAL_GetTick(); if (vcu_status_received && ((now - vcu_status_last_rx_ms) > AMI_VCU_STATUS_TIMEOUT_MS)) { vcu_display_state[0] = 7U; vcu_display_state[1] = 0U; vcu_display_state[2] = 0U; vcu_display_dirty |= 7U; vcu_status_received = false; } if ((adc_last_rx_ms == 0U) || ((now - adc_last_rx_ms) > AMI_ADC_TIMEOUT_MS)) { SetDashValid(7U, false); } if ((bms_status_last_rx_ms == 0U) || ((now - bms_status_last_rx_ms) > AMI_BMS_TIMEOUT_MS)) { SetDashValid(1U, false); SetDashValid(3U, false); } if ((bms_temperature_last_rx_ms == 0U) || ((now - bms_temperature_last_rx_ms) > AMI_BMS_TIMEOUT_MS)) { SetDashValid(6U, false); } if ((drivetrain_last_rx_ms == 0U) || ((now - drivetrain_last_rx_ms) > AMI_DRIVETRAIN_TIMEOUT_MS)) { SetDashValid(2U, false); SetDashValid(4U, false); SetDashValid(5U, false); } /* Recheck within the RX critical section to avoid erasing a new snapshot. */ debug_timeout = (debug_last_rx_ms == 0U) || ((now - debug_last_rx_ms) > AMI_CAN_DATA_TIMEOUT_MS); if (debug_timeout && !debug_timeout_applied) { uint32_t i; for (i = 0U; i < 16U; ++i) { display_state.debug_state[i] = 0U; } display_dirty |= DIRTY_DEBUG_ALL; debug_timeout_applied = true; } else if (!debug_timeout) { debug_timeout_applied = false; } __enable_irq(); } static void SendNextDirtyValue(void) { static const char *const vcu_prefix[3] = { "current_task=", "lock_state=", "ebs_state=" }; static const char *const dash_prefix[8] = { "dash.speed.val=", "dash.ts.val=", "dash.mct.val=", "dash.dl.val=", "dash.djwd.val=", "dash.mcuwd.val=", "dash.dtwd.val=", "dash.lvbat.val=" }; static const char *const color_prefix[8] = { "dash.speed.pco=", "dash.ts.pco=", "dash.mct.pco=", "dash.dl.pco=", "dash.djwd.pco=", "dash.mcuwd.pco=", "dash.dtwd.pco=", "dash.lvbat.pco=" }; static uint32_t cursor; uint32_t dirty; uint8_t vcu_dirty; uint32_t bit; uint32_t index; int32_t value; const char *prefix; char debug_prefix[5] = {'d', '0', '=', '\0', '\0'}; if (screen_tx_busy || (HAL_GetTick() < AMI_SCREEN_BOOT_DELAY_MS)) { return; } __disable_irq(); dirty = display_dirty; vcu_dirty = vcu_display_dirty; __enable_irq(); if ((dirty == 0U) && (vcu_dirty == 0U)) { return; } /* Fair rotation prevents rapid source updates from starving other fields. */ for (uint32_t offset = 0U; offset < 35U; ++offset) { index = (cursor + offset) % 35U; if ((index < 32U) ? ((dirty & (1UL << index)) != 0U) : ((vcu_dirty & (1U << (index - 32U))) != 0U)) { break; } } bit = 1UL << ((index < 32U) ? index : index - 32U); cursor = (index + 1U) % 35U; __disable_irq(); if (index >= 32U) { value = vcu_display_state[index - 32U]; prefix = vcu_prefix[index - 32U]; vcu_display_dirty &= (uint8_t)~bit; } else if (index < 8U) { value = display_state.dash_value[index]; prefix = dash_prefix[index]; } else if (index >= 24U) { uint32_t dash_index = index - 24U; value = (display_state.dash_valid & (1U << dash_index)) ? 65535 : 16904; prefix = color_prefix[dash_index]; } else { uint32_t debug_index = index - 8U; value = display_state.debug_state[debug_index]; if (debug_index >= 10U) { debug_prefix[1] = '1'; debug_prefix[2] = (char)('0' + (debug_index - 10U)); debug_prefix[3] = '='; prefix = debug_prefix; } else { debug_prefix[1] = (char)('0' + debug_index); prefix = debug_prefix; } } if (index < 32U) { display_dirty &= ~bit; } __enable_irq(); if (!StartScreenAssignment(prefix, value)) { __disable_irq(); if (index < 32U) { display_dirty |= bit; } else { vcu_display_dirty |= (uint8_t)bit; } __enable_irq(); } } void AMI_App_Init(void) { HAL_GPIO_WritePin(STATUS_LED1_GPIO_Port, STATUS_LED1_Pin, LedInactiveLevel()); HAL_GPIO_WritePin(STATUS_LED2_GPIO_Port, STATUS_LED2_Pin, AMI_LED_ACTIVE_LEVEL); display_dirty = DIRTY_DASH_ALL | DIRTY_DEBUG_ALL | DIRTY_COLOR_ALL; vcu_display_dirty = 7U; ConfigureCan(); RestartScreenRx(); } void AMI_App_Service(void) { static uint32_t last_screen_sync_ms; uint32_t now = HAL_GetTick(); SendPendingTask(); SendHeartbeat(); UpdateTimeoutsAndLeds(); /* The HMI may reboot/reconnect after the MCU has already sent its state. */ if ((now - last_screen_sync_ms) >= AMI_SCREEN_RESYNC_MS) { __disable_irq(); display_dirty |= DIRTY_DASH_ALL | DIRTY_DEBUG_ALL | DIRTY_COLOR_ALL; vcu_display_dirty |= 7U; __enable_irq(); last_screen_sync_ms = now; } SendNextDirtyValue(); } void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t rx_fifo0_its) { FDCAN_RxHeaderTypeDef header; uint8_t data[64]; /* HAL copies payload before validation, including FD DLCs. */ if ((rx_fifo0_its & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) == 0U) { return; } while (HAL_FDCAN_GetRxFifoFillLevel(hfdcan, FDCAN_RX_FIFO0) > 0U) { if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &header, data) != HAL_OK) { ++can_rx_error_count; break; } if ((header.RxFrameType != FDCAN_DATA_FRAME) || (header.FDFormat != FDCAN_CLASSIC_CAN) || (header.DataLength > FDCAN_DLC_BYTES_8)) { continue; } if (DecodeCan(header.Identifier, header.IdType, header.DataLength, data)) { can_port_last_rx_ms[(hfdcan->Instance == FDCAN1) ? 0U : 1U] = HAL_GetTick(); } } } void HAL_FDCAN_ErrorCallback(FDCAN_HandleTypeDef *hfdcan) { (void)hfdcan; ++can_rx_error_count; } void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t size) { uint16_t i; if (huart->Instance != USART2) { return; } for (i = 0U; i < size; ++i) { ParseScreenByte(screen_rx_dma[i]); } RestartScreenRx(); } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { screen_tx_busy = false; } } void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { ++uart_rx_error_count; (void)HAL_UART_AbortReceive(huart); RestartScreenRx(); } }