Here’s a clean, practical way to send RTC time from an MCU (e.g., STM32) to an FPGA automatically from main.c. I’ll use UART because it’s the simplest to implement on both sides; I’ll also note SPI/I²C options at the end.
The plan (TL;DR)
- Wire MCU UART → FPGA UART (TX→RX, GND↔GND, same voltage, e.g., 3.3 V).
- Frame a tiny time packet: sync bytes + YY/MM/DD/HH/MM/SS + checksum.
- STM32: read RTC each second (RTC wakeup or timer) and auto-send packet from main.c (UART DMA or interrupt).
- FPGA: simple UART RX + small state machine to parse the packet and update time registers.
1) Hardware hookup
- MCU TX → FPGA RX
- MCU GND ↔ FPGA GND
- Voltage levels: keep both at 3.3 V (avoid 5 V unless level-shifted).
- Pick a common baud (e.g., 115200 8N1).
2) Minimal packet format (easy to parse)
Use a fixed 10-byte frame:
[0] 0xA5
[1] 0x5A
[2] sec (0–59)
[3] min (0–59)
[4] hour (0–23)
[5] day (1–31)
[6] month (1–12)
[7] year (0–99) // e.g., 25 for 2025
[8] csum // XOR of bytes [2..7]
[9] 0x0D // end marker
You can switch to BCD later if you prefer, but raw binary is fine.
3) STM32 (HAL) — “automatic” send from main.c
Below is a compact pattern that works on CubeMX-generated projects. It:
- Inits RTC + UART,
- Uses RTC Wakeup (1 Hz) or a hardware timer to trigger,
- Reads time and transmits via UART (DMA or blocking).
// main.c (STM32 HAL-style)
#include "main.h"
#include "usart.h"
#include "rtc.h"
static uint8_t frame[10];
static uint8_t make_checksum(uint8_t *p) {
uint8_t x = 0;
for (int i = 2; i <= 7; ++i) x ^= p[i];
return x;
}
static void build_time_frame(RTC_TimeTypeDef *t, RTC_DateTypeDef *d) {
frame[0] = 0xA5; frame[1] = 0x5A;
frame[2] = t->Seconds;
frame[3] = t->Minutes;
frame[4] = t->Hours;
frame[5] = d->Date;
frame[6] = d->Month;
frame[7] = d->Year; // 0..99 (RTC BCD config off -> values are binary)
frame[8] = make_checksum(frame);
frame[9] = 0x0D;
}
static void send_time_now(void) {
RTC_TimeTypeDef t; RTC_DateTypeDef d;
// Read TIME first, then DATE (RTC shadow regs)
HAL_RTC_GetTime(&hrtc, &t, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &d, RTC_FORMAT_BIN);
build_time_frame(&t, &d);
// non-blocking is nicer; blocking works too for 10 bytes
HAL_UART_Transmit(&huart1, frame, sizeof(frame), 10);
// or: HAL_UART_Transmit_DMA(&huart1, frame, sizeof(frame));
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_RTC_Init();
MX_USART1_UART_Init();
// Option A) RTC Wakeup @1 Hz
HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 0x0FFF, RTC_WAKEUPCLOCK_RTCCLK_DIV16); // tune for ~1 s on your part
while (1) {
// Option B) If not using Wakeup interrupt, poll every second:
// static uint32_t ms0; if (HAL_GetTick() - ms0 >= 1000) { ms0 += 1000; send_time_now(); }
// low-power friendly: __WFI();
}
}
// Called each second if using RTC wakeup IT
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc) {
send_time_now();
}
Notes
- In CubeMX, enable RTC (LSE/LSEBYP, etc.) and USART.
- If you prefer not to use the RTC wakeup interrupt, use a TIMx interrupt (1 Hz) to call send_time_now().
- For robust delivery, switch HAL_UART_Transmit to DMA and check HAL_UART_TxCpltCallback.
4) FPGA side — simple UART RX + parser (Verilog sketch)
UART RX (parameterize by FPGA clock and baud):
- If your FPGA clock is 50 MHz and baud is 115200, set CLKS_PER_BIT = 50_000_000 / 115200 ≈ 434.
- Most open-source “uart_rx.v” modules work fine; or use the following outline.
// uart_rx.sv (very compact outline)
module uart_rx #(parameter CLK_HZ=50_000_000, BAUD=115200) (
input logic clk, rstn,
input logic rx,
output logic [7:0] data,
output logic data_valid
);
localparam int CLKS_PER_BIT = CLK_HZ / BAUD; // 434 @50MHz
// ... implement standard 8N1 oversampling or mid-bit sampling FSM ...
// On each received byte, assert data_valid for one clk and present 'data'.
endmodule
Packet parser: wait for 0xA5, 0x5A, collect 8 payload bytes, verify checksum, then update registers.
module time_parser (
input logic clk, rstn,
input logic [7:0] rx_data,
input logic rx_strobe,
output logic [6:0] year, month, day, hour, minute, second,
output logic time_valid_pulse
);
typedef enum logic [2:0] {IDLE, S1, PAYLOAD, CSUM, END} state_t;
state_t st; logic [3:0] idx; logic [7:0] buf[0:7]; logic [7:0] xsum;
always_ff @(posedge clk or negedge rstn) begin
if (!rstn) begin st<=IDLE; idx<=0; xsum<=0; time_valid_pulse<=0; end
else begin
time_valid_pulse <= 1'b0;
if (rx_strobe) begin
unique case (st)
IDLE: st <= (rx_data==8'hA5) ? S1 : IDLE;
S1: st <= (rx_data==8'h5A) ? PAYLOAD : IDLE;
PAYLOAD: begin
buf[idx] <= rx_data; // [0]=sec, [1]=min, ... [5]=month, [6]=year
xsum <= (idx==0) ? rx_data : (xsum ^ rx_data);
if (idx==6) begin idx<=0; st<=CSUM; end
else idx <= idx + 1;
end
CSUM: st <= (rx_data==xsum) ? END : IDLE;
END: begin
if (rx_data==8'h0D) begin
second <= buf[0][6:0];
minute <= buf[1][6:0];
hour <= buf[2][6:0];
day <= buf[3][6:0];
month <= buf[4][6:0];
year <= buf[5][6:0];
time_valid_pulse <= 1'b1; // one-clock strobe
end
st <= IDLE;
end
endcase
end
end
end
endmodule
Wire uart_rx.data_valid → rx_strobe, and uart_rx.data → rx_data. On each time_valid_pulse, your design now has fresh YY/MM/DD/HH/MM/SS registers.
5) Alternatives (if UART isn’t your thing)
- SPI (MCU master → FPGA slave): fastest to implement in HDL. Define a fixed 8-byte register burst; use a chip-select and a “frame counter” in FPGA.
- I²C: MCU as master; FPGA as slave (more logic to implement). Use a small set of time registers at fixed addresses.
- Parallel GPIO: 8- or 16-bit bus + strobe if you already have spare pins.
6) Reliability tips
- Clock accuracy: UART tolerates small error. With 50 MHz and 115200, CLKS_PER_BIT=434 is fine (~0.006% error).
- Loss handling: Keep the sync bytes (0xA5, 0x5A) so the FPGA can re-lock if it ever desyncs.
- Triggering: Use RTC Wakeup/Alarm on the MCU for true 1 Hz sends.
- Validation: Blink an LED on time_valid_pulse and print the received time on an FPGA UART TX (loopback to a PC) during bring-up.

Top comments (0)