diff --git a/CMakeLists.txt b/CMakeLists.txt index 01dd89426..b4ef0e94d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,12 +30,20 @@ add_compile_options(-fdiagnostics-color=always) #Flash size related options #This is the size of the flash memory we SUPPORT. Don't worry, the internl flash app has harder limits based on device type. if(NOT DEFINED FLASH_MB_SIZE) - set(FLASH_MB_SIZE 2) # Default to 2MB if not provided. For PortaRf this should be 2MB passed to cmake with -DFLASH_MB_SIZE=2 + if(BOARD STREQUAL "PRALINE") + set(FLASH_MB_SIZE 4) # PRALINE has 4MB flash + else() + set(FLASH_MB_SIZE 2) # Default to 2MB if not provided. For PortaRf this should be 2MB passed to cmake with -DFLASH_MB_SIZE=2 + endif() endif() message("Configuring for FLASH_MB_SIZE=${FLASH_MB_SIZE}MB") #This is the flash memory size we build. This can be less, so the FW size will be less, so can be flashed with older utils. if(NOT DEFINED FLASH_MB_LIMIT_SIZE) - set(FLASH_MB_LIMIT_SIZE 1) #TODO: should be ${FLASH_MB_SIZE} remove once we got a stable build for all devices we support with bigger than 1 mb flash + if(BOARD STREQUAL "PRALINE") + set(FLASH_MB_LIMIT_SIZE 3.5) #PRALINE 4 MB = 3.5 firmware + 0.5 MB fpga bitstream. + else() + set(FLASH_MB_LIMIT_SIZE 1) #TODO: should be ${FLASH_MB_SIZE} remove once we got a stable build for all devices we support with bigger than 1 mb flash + endif() endif() message("Configuring for FLASH_MB_LIMIT_SIZE=${FLASH_MB_LIMIT_SIZE}MB") @@ -44,7 +52,12 @@ if(FLASH_MB_SIZE LESS FLASH_MB_LIMIT_SIZE) endif() math(EXPR FLASH_BYTES_SIZE "${FLASH_MB_SIZE} * 1024 * 1024") -math(EXPR FLASH_BYTES_LIMIT_SIZE "${FLASH_MB_LIMIT_SIZE} * 1024 * 1024") +if(BOARD STREQUAL "PRALINE") + math(EXPR FLASH_BYTES_LIMIT_SIZE "3584 * 1024") #4 MB = 3.5 firmware + 0.5 MB fpga bitstream. +else() + math(EXPR FLASH_BYTES_LIMIT_SIZE "${FLASH_MB_LIMIT_SIZE} * 1024 * 1024") +endif() + #generate h files configure_file(flashsize.h.in ${CMAKE_CURRENT_SOURCE_DIR}/firmware/flashsize.h) diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index 8499a2852..0783692b8 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -67,6 +67,24 @@ add_custom_target( DEPENDS ${FIRMWARE_FILENAME} ${HACKRF_FIRMWARE_DFU_FILENAME} ${HACKRF_FIRMWARE_FILENAME} ) +# PRALINE: Append FPGA bitstream to firmware at offset 0x180000 +# The base firmware is 1MB, FPGA goes at 1MB offset, final size is 2MB +if(BOARD STREQUAL "PRALINE") + math(EXPR PRALINE_FINAL_SIZE "4 * 1024 * 1024") + set(PRALINE_FPGA_BIN ${CMAKE_CURRENT_SOURCE_DIR}/../hackrf/firmware/fpga/build/praline_fpga.bin) + set(APPEND_FPGA_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/tools/append_fpga_bitstream.py) + add_custom_command( + TARGET firmware POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Appending PRALINE FPGA bitstream to firmware..." + COMMAND python3 ${APPEND_FPGA_SCRIPT} ${FIRMWARE_FILENAME} ${PRALINE_FPGA_BIN} ${FIRMWARE_FILENAME}.tmp ${PRALINE_FINAL_SIZE} + COMMAND ${CMAKE_COMMAND} -E rename ${FIRMWARE_FILENAME}.tmp ${FIRMWARE_FILENAME} + COMMAND ${CMAKE_COMMAND} -E echo "PRALINE firmware with FPGA bitstream created:" + COMMAND ls -la ${FIRMWARE_FILENAME} + COMMENT "Appending PRALINE FPGA bitstream" + VERBATIM + ) +endif() + if(${GCC_VERSION_MISMATCH}) set(COMPILER_MISMATCH_MESSAGE "WARNING: Compiler version mismatch, please use the official compiler version ${EXPECTED_GCC_VERSION} when sharing builds! Current compiler version: ${CMAKE_CXX_COMPILER_VERSION}") message(${COMPILER_MISMATCH_MESSAGE}) diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 3a52630d0..357ab7771 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -86,6 +86,9 @@ set(CPLD_20150901_DATA_CPP ${CMAKE_CURRENT_BINARY_DIR}/portapack_cpld_20150901_d set(CPLD_20170522_SVF_PATH ${HARDWARE_PATH}/portapack_h1/cpld/20170522/output_files/portapack_h1_cpld.svf) set(CPLD_20170522_DATA_CPP ${CMAKE_CURRENT_BINARY_DIR}/portapack_cpld_20170522_data.cpp) +set(CPLD_H4M_SVF_PATH ${HARDWARE_PATH}/portapack_h4m/CPLD/AG256SL100/output_files/portapack_h4m_cpld.svf) +set(CPLD_H4M_DATA_CPP ${CMAKE_CURRENT_BINARY_DIR}/portapack_cpld_h4m_data.cpp) + set(HACKRF_CPLD_DATA_HPP ${CMAKE_CURRENT_BINARY_DIR}/hackrf_cpld_data.hpp) set(HACKRF_CPLD_DATA_CPP ${CMAKE_CURRENT_BINARY_DIR}/hackrf_cpld_data.cpp) @@ -242,6 +245,7 @@ set(CPPSRC hw/encoder.cpp hw/max2837.cpp hw/max2839.cpp + hw/max2831.cpp hw/max5864.cpp hw/rffc507x.cpp hw/rffc507x_spi.cpp @@ -322,6 +326,7 @@ set(CPPSRC config_mode.cpp ${CPLD_20150901_DATA_CPP} ${CPLD_20170522_DATA_CPP} + ${CPLD_H4M_DATA_CPP} ${HACKRF_CPLD_DATA_CPP} ui_external_items_menu_loader.cpp view_factory_base.cpp @@ -407,7 +412,11 @@ set(CPPWARN "-Wall -Wextra -Wno-psabi") # List all default C defines here, like -D_DEBUG=1 # TODO: Switch -DCRT0_INIT_DATA depending on load from RAM or SPIFI? # NOTE: _RANDOM_TCC to kill a GCC 4.9.3 error with std::max argument types -set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"' -DVERSION_MD5=${VERSION_MD5} -D'GCC_VERSION_MISMATCH=${GCC_VERSION_MISMATCH}'") +# Use BOARD variable if set, otherwise default to HACKRF_ONE +if(NOT DEFINED BOARD) + set(BOARD "HACKRF_ONE") +endif() +set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -D${BOARD} -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"' -DVERSION_MD5=${VERSION_MD5} -D'GCC_VERSION_MISMATCH=${GCC_VERSION_MISMATCH}'") # List all default ASM defines here, like -D_DEBUG=1 set(DADEFS) @@ -465,6 +474,12 @@ add_custom_command( DEPENDS ${EXTRACT_CPLD_DATA} ${CPLD_20170522_SVF_PATH} ) +add_custom_command( + OUTPUT ${CPLD_H4M_DATA_CPP} + COMMAND ${EXTRACT_CPLD_DATA} ${CPLD_H4M_SVF_PATH} rev_h4m >${CPLD_H4M_DATA_CPP} + DEPENDS ${EXTRACT_CPLD_DATA} ${CPLD_H4M_SVF_PATH} +) + add_custom_command( OUTPUT ${HACKRF_CPLD_DATA_CPP} COMMAND ${HACKRF_CPLD_TOOL} --xsvf ${HACKRF_CPLD_XSVF_PATH} --portapack-data ${HACKRF_CPLD_DATA_CPP} diff --git a/firmware/application/apps/ui_debug.cpp b/firmware/application/apps/ui_debug.cpp index b12ce79f0..6e02bd687 100644 --- a/firmware/application/apps/ui_debug.cpp +++ b/firmware/application/apps/ui_debug.cpp @@ -25,6 +25,7 @@ #include "debug.hpp" #include "ch.h" +#include "hal.h" #include "radio.hpp" #include "string_format.hpp" @@ -148,6 +149,12 @@ uint32_t RegistersWidget::reg_read(const uint32_t register_number) { } case CT_AUDIO: return audio::debug::reg_read(register_number); +#ifdef PRALINE + case CT_FPGA: + return radio::debug::fpga::register_read(register_number); +#endif + case CT_SGPIO: + return radio::debug::sgpio::register_read(register_number); } } return 0xFFFF; @@ -175,6 +182,14 @@ void RegistersWidget::reg_write(const uint32_t register_number, const uint32_t v case CT_AUDIO: audio::debug::reg_write(register_number, value); break; +#ifdef PRALINE + case CT_FPGA: + radio::debug::fpga::register_write(register_number, value); + break; +#endif + case CT_SGPIO: + // SGPIO registers are read-only for debug purposes + break; } } } @@ -369,6 +384,1518 @@ void DebugControlsView::focus() { switches_widget.focus(); } +#ifdef PRALINE +/* RadioDiagnosticsView **************************************************/ + +RadioDiagnosticsView::RadioDiagnosticsView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_rffc, + &text_rffc_status, + &text_lbl_max, + &text_max_status, + &text_lbl_adc, + &text_adc_status, + &text_lbl_fpga, + &text_fpga_status, + &text_lbl_sgpio, + &text_sgpio_status, + &text_lbl_clock, + &text_clock_status, + &text_regs_title, + &text_lbl_rffc_reg, + &text_rffc_reg, + &text_lbl_max_reg, + &text_max_reg, + &text_lbl_fpga_reg, + &text_fpga_reg, + &text_lbl_sgpio_reg, + &text_sgpio_reg, + &text_test_result, + &button_refresh, + &button_done, + }); + + // Set title colors + text_title.set_style(Theme::getInstance()->fg_yellow); + text_regs_title.set_style(Theme::getInstance()->fg_yellow); + +#ifdef PRALINE + text_lbl_fpga.set("FPGA (iCE40):"); +#else + text_lbl_fpga.set("CPLD:"); +#endif + + button_refresh.on_select = [this](Button&) { + update_status(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Initial status update + update_status(); +} + +void RadioDiagnosticsView::focus() { + button_refresh.focus(); +} + +void RadioDiagnosticsView::update_status() { + // Read RFFC5072 register 0 to check if it responds + uint32_t rffc_reg0 = radio::debug::first_if::register_read(0); + bool rffc_ok = (rffc_reg0 != 0x0000) && (rffc_reg0 != 0xFFFF); + text_rffc_status.set(rffc_ok ? "OK" : "FAIL"); + text_rffc_status.set_style(rffc_ok ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + text_rffc_reg.set(to_string_hex(rffc_reg0, 4)); + + // Read MAX283x register 0 to check if it responds + uint32_t max_reg0 = radio::debug::second_if::register_read(0); + bool max_ok = (max_reg0 != 0x0000) && (max_reg0 != 0x3FFF); + text_max_status.set(max_ok ? "OK" : "FAIL"); + text_max_status.set_style(max_ok ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + text_max_reg.set(to_string_hex(max_reg0, 4)); + + // MAX5864 has no readback - assume OK if other SPI works + text_adc_status.set("(no readback)"); + text_adc_status.set_style(Theme::getInstance()->fg_medium); + +#ifdef PRALINE + // Read FPGA control register + uint32_t fpga_ctrl = radio::debug::fpga::register_read(1); + bool fpga_ok = (fpga_ctrl != 0xFF); // 0xFF = not responding + bool dc_block = (fpga_ctrl & 0x01) != 0; + text_fpga_status.set(fpga_ok ? (dc_block ? "OK DC_BLK" : "OK NO_DC") : "FAIL"); + text_fpga_status.set_style(fpga_ok ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // Show all FPGA registers + uint32_t fpga_r2 = radio::debug::fpga::register_read(2); + uint32_t fpga_r3 = radio::debug::fpga::register_read(3); + text_fpga_reg.set("C:" + to_string_hex(fpga_ctrl, 2) + + " D:" + to_string_hex(fpga_r2, 2) + + " T:" + to_string_hex(fpga_r3, 2)); +#else + text_fpga_status.set("(CPLD)"); + text_fpga_status.set_style(Theme::getInstance()->fg_medium); + text_fpga_reg.set("N/A"); +#endif + + // Check SGPIO status + uint32_t sgpio_enable = radio::debug::sgpio::register_read(0); // CTRL_ENABLE + uint32_t sgpio_status = radio::debug::sgpio::register_read(4); // STATUS_1 + bool sgpio_ok = (sgpio_enable != 0); + text_sgpio_status.set(sgpio_ok ? "ENABLED" : "DISABLED"); + text_sgpio_status.set_style(sgpio_ok ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_orange); + text_sgpio_reg.set("EN:" + to_string_hex(sgpio_enable, 4) + + " ST:" + to_string_hex(sgpio_status, 4)); + + // Clock status - check if Si5351 is configured + // We can't easily read back clock status, so just show assumed state + text_clock_status.set("(assumed OK)"); + text_clock_status.set_style(Theme::getInstance()->fg_medium); + + // Summary + bool all_ok = rffc_ok && max_ok; +#ifdef PRALINE + all_ok = all_ok && fpga_ok; +#endif + if (all_ok) { + text_test_result.set("Peripherals responding. Try RX app."); + text_test_result.set_style(Theme::getInstance()->fg_green); + } else { + text_test_result.set("Check failed peripherals above."); + text_test_result.set_style(Theme::getInstance()->fg_red); + } +} + +/* BasebandStatusView ******************************************************/ + +BasebandStatusView::BasebandStatusView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_marker, + &text_marker, + &text_lbl_loops, + &text_loops, + &text_lbl_wait, + &text_wait, + &text_lbl_xfr, + &text_xfr, + &text_lbl_missed, + &text_missed, + &text_status_line1, + &text_status_line2, + &text_status_line3, + &button_refresh, + &button_done, + }); + + // Set title color + text_title.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + update(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Initial update + update(); +} + +void BasebandStatusView::focus() { + button_refresh.focus(); +} + +void BasebandStatusView::update() { + // Read counters from shared memory + uint8_t marker = shared_memory.m4_streaming_marker; + uint32_t loops = shared_memory.m4_baseband_loops; + uint32_t wait = shared_memory.m4_dma_wait_count; + uint32_t xfr = shared_memory.m4_dma_xfr_count; + uint16_t missed = shared_memory.m4_buffer_missed; + + // Display counter values + text_marker.set(to_string_hex(marker, 2)); + text_marker.set_style((marker == 0xAA) ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + text_loops.set(to_string_dec_uint(loops)); + text_wait.set(to_string_dec_uint(wait)); + text_xfr.set(to_string_dec_uint(xfr)); + text_missed.set(to_string_dec_uint(missed)); + + // Status interpretation + if (marker == 0x00) { + text_status_line1.set("Thread NOT started!"); + text_status_line2.set("M4 baseband crash."); + text_status_line3.set("Check thread race condition."); + text_status_line1.set_style(Theme::getInstance()->fg_red); + text_status_line2.set_style(Theme::getInstance()->fg_red); + text_status_line3.set_style(Theme::getInstance()->fg_red); + } else if (marker == 0xAA && loops == 0) { + text_status_line1.set("Thread started but"); + text_status_line2.set("not looping yet."); + text_status_line3.set("Wait a moment..."); + text_status_line1.set_style(Theme::getInstance()->fg_orange); + text_status_line2.set_style(Theme::getInstance()->fg_orange); + text_status_line3.set_style(Theme::getInstance()->fg_orange); + } else if (marker == 0xAA && xfr == 0) { + text_status_line1.set("Thread looping " + to_string_dec_uint(loops) + "x"); + text_status_line2.set("But DMA NOT firing!"); + text_status_line3.set("Check SGPIO14 enable."); + text_status_line1.set_style(Theme::getInstance()->fg_orange); + text_status_line2.set_style(Theme::getInstance()->fg_orange); + text_status_line3.set_style(Theme::getInstance()->fg_orange); + } else if (xfr > 0) { + text_status_line1.set("DMA WORKING!"); + text_status_line2.set("Xfr: " + to_string_dec_uint(xfr)); + text_status_line3.set("Data flowing to baseband."); + text_status_line1.set_style(Theme::getInstance()->fg_green); + text_status_line2.set_style(Theme::getInstance()->fg_green); + text_status_line3.set_style(Theme::getInstance()->fg_green); + } +} + +/* SGPIOLiveMonitorView ****************************************************/ + +SGPIOLiveMonitorView::SGPIOLiveMonitorView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_ctrl, + &text_ctrl, + &text_lbl_in, + &text_in, + &text_lbl_ss, + &text_ss, + &text_lbl_status, + &text_status, + &text_lbl_out, + &text_out, + &text_lbl_oen, + &text_oen, + &text_diag_line1, + &text_diag_line2, + &text_diag_line3, + &text_diag_line4, + &button_refresh, + &button_done, + }); + + // Set title color + text_title.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + update(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Initial update + update(); +} + +void SGPIOLiveMonitorView::focus() { + button_refresh.focus(); +} + +void SGPIOLiveMonitorView::update() { + // Read SGPIO registers via radio debug namespace + uint32_t ctrl = radio::debug::sgpio::register_read(0); // CTRL_ENABLE + uint32_t in_reg = radio::debug::sgpio::register_read(1); // GPIO_INREG + uint32_t status = radio::debug::sgpio::register_read(4); // STATUS_1 + + // Read registers directly from LPC_SGPIO peripheral + uint32_t reg_ss = LPC_SGPIO->REG_SS[0]; + uint32_t out_reg = LPC_SGPIO->GPIO_OUTREG; + uint32_t oen_reg = LPC_SGPIO->GPIO_OENREG; + + // Display register values + text_ctrl.set(to_string_hex(ctrl, 4)); + text_in.set(to_string_hex(in_reg, 8)); + text_ss.set(to_string_hex(reg_ss, 8)); + text_status.set(to_string_hex(status, 4)); + text_out.set(to_string_hex(out_reg, 4)); + text_oen.set(to_string_hex(oen_reg, 4)); + + // Diagnostics based on register values + bool gpio_changing = (in_reg & 0xFF) != 0; // Check data pins + bool regss_active = (reg_ss != 0); + bool disable_high = (out_reg & (1U << 10)) != 0; // Bit 10 = DISABLE signal + bool sgpio8_high = (in_reg & (1U << 8)) != 0; // Bit 8 = SGPIO8 clock + bool sgpio8_output = (oen_reg & (1U << 8)) != 0; // Bit 8 = SGPIO8 direction (should be INPUT=0) + + // Line 1: SGPIO8 direction check (CRITICAL - must be INPUT) + if (sgpio8_output) { + text_diag_line1.set("SGPIO8 OUTPUT! (bus conflict)"); + text_diag_line1.set_style(Theme::getInstance()->fg_red); + } else if (disable_high) { + text_diag_line1.set("DISABLE=HIGH! FPGA stopped!"); + text_diag_line1.set_style(Theme::getInstance()->fg_red); + } else { + text_diag_line1.set("SGPIO8=IN, DISABLE=LOW"); + text_diag_line1.set_style(Theme::getInstance()->fg_green); + } + + // Line 2: Clock signal status (snapshot - can't detect toggling) + if (disable_high) { + text_diag_line2.set("Clock N/A (FPGA disabled)"); + text_diag_line2.set_style(Theme::getInstance()->fg_medium); + } else if (sgpio8_high) { + text_diag_line2.set("SGPIO8=HIGH (snapshot)"); + text_diag_line2.set_style(Theme::getInstance()->fg_green); + } else { + text_diag_line2.set("SGPIO8=LOW (snapshot)"); + text_diag_line2.set_style(Theme::getInstance()->fg_green); + } + + // Line 3: REG_SS[0] capture status + if (!regss_active && !disable_high) { + text_diag_line3.set("REG_SS[0]=0 (NOT CAPTURING!)"); + text_diag_line3.set_style(Theme::getInstance()->fg_red); + } else if (regss_active) { + text_diag_line3.set("REG_SS[0] has data"); + text_diag_line3.set_style(Theme::getInstance()->fg_green); + } else { + text_diag_line3.set("Capture N/A (FPGA disabled)"); + text_diag_line3.set_style(Theme::getInstance()->fg_medium); + } + + // Line 4: Summary based on key indicators + if (sgpio8_output) { + text_diag_line4.set("FIX: Set SGPIO8 to INPUT!"); + text_diag_line4.set_style(Theme::getInstance()->fg_red); + } else if (disable_high) { + text_diag_line4.set("FIX: Clear DISABLE bit!"); + text_diag_line4.set_style(Theme::getInstance()->fg_red); + } else if (regss_active && gpio_changing) { + text_diag_line4.set("SGPIO capturing data"); + text_diag_line4.set_style(Theme::getInstance()->fg_green); + } else if (!regss_active && gpio_changing) { + text_diag_line4.set("Data present, check slices"); + text_diag_line4.set_style(Theme::getInstance()->fg_orange); + } else if (!regss_active) { + text_diag_line4.set("No data activity"); + text_diag_line4.set_style(Theme::getInstance()->fg_orange); + } else { + text_diag_line4.set("Check DMA config"); + text_diag_line4.set_style(Theme::getInstance()->fg_green); + } +} + +/* RadioRxTestView ********************************************************/ + +RadioRxTestView::RadioRxTestView(NavigationView& nav) + : nav_(nav) { + add_children({ + &labels, + &console, + &button_init, + &button_rx, + &button_freq, + &button_sgpio, + &button_full, + &button_step, + &button_done, + }); + + button_init.on_select = [this](Button&) { + run_init_test(); + }; + + button_rx.on_select = [this](Button&) { + run_rx_mode_test(); + }; + + button_freq.on_select = [this](Button&) { + run_freq_test(); + }; + + button_sgpio.on_select = [this](Button&) { + run_sgpio_test(); + }; + + button_full.on_select = [this](Button&) { + run_full_test(); + }; + + button_step.on_select = [this](Button&) { + run_step_test(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + log("Ready. Press buttons to test."); + log("Init->RX->Freq->SGPIO"); +} + +void RadioRxTestView::focus() { + button_full.focus(); +} + +void RadioRxTestView::log(const std::string& msg) { + console.writeln(msg); +} + +void RadioRxTestView::log_registers(const std::string& label) { + // RFFC5072 register 0 + uint32_t rffc_r0 = radio::debug::first_if::register_read(0); + + // MAX283x registers 0, 3, 4 (key freq regs) + uint32_t max_r0 = radio::debug::second_if::register_read(0); + uint32_t max_r3 = radio::debug::second_if::register_read(3); + uint32_t max_r4 = radio::debug::second_if::register_read(4); + + // SGPIO + uint32_t sgpio_en = radio::debug::sgpio::register_read(0); + uint32_t sgpio_data = radio::debug::sgpio::register_read(5); + + log(label); + log(" RFFC:" + to_string_hex(rffc_r0, 4)); + log(" MAX r0:" + to_string_hex(max_r0, 4) + + " r3:" + to_string_hex(max_r3, 4) + + " r4:" + to_string_hex(max_r4, 4)); + log(" SGPIO en:" + to_string_hex(sgpio_en, 4) + + " dat:" + to_string_hex(sgpio_data, 8)); + +#ifdef PRALINE + uint32_t fpga_ctrl = radio::debug::fpga::register_read(1); + log(" FPGA ctrl:" + to_string_hex(fpga_ctrl, 2)); +#endif +} + +void RadioRxTestView::run_init_test() { + console.clear(true); + log("=== INIT TEST ==="); + + log("radio::init()..."); + radio::init(); + radio_initialized_ = true; + + log("set_baseband_rate(8M)..."); + radio::set_baseband_rate(8000000); + + // Read Si5351 status to check PLL lock + uint8_t si_status = portapack::clock_manager.si5351_read_status(); + log("Si5351 status: " + to_string_hex(si_status, 2)); + if (si_status & 0x20) { + log(" WARNING: PLL A unlocked!"); + } else { + log(" PLL A locked OK"); + } + + // Read crystal cap register + uint8_t xtal_cap = portapack::clock_manager.si5351_read_register(183); + log("Crystal cap: " + to_string_hex(xtal_cap, 2)); + + log_registers("[After init]"); + log("Init+clocks done."); +} + +void RadioRxTestView::run_rx_mode_test() { + console.clear(true); + log("=== RX MODE TEST ==="); + + if (!radio_initialized_) { + log("ERROR: Run Init first!"); + return; + } + + log_registers("[Before RX mode]"); + + log("Calling set_direction(Receive)..."); + radio::set_direction(rf::Direction::Receive); + + log_registers("[After RX mode]"); + + // Check MAX283x mode register + uint32_t max_r0 = radio::debug::second_if::register_read(0); + log("MAX r0 after RX: " + to_string_hex(max_r0, 4)); + log("RX mode set."); +} + +void RadioRxTestView::run_freq_test() { + console.clear(true); + log("=== FREQ TEST ==="); + + if (!radio_initialized_) { + log("ERROR: Run Init first!"); + return; + } + + log_registers("[Before freq set]"); + + log("Setting " + to_string_dec_uint(test_frequency_ / 1000000) + " MHz..."); + bool result = radio::set_tuning_frequency(test_frequency_); + + log_registers("[After freq set]"); + + log(result ? "Freq set OK" : "Freq set FAILED"); + + // Show expected vs actual for MAX2831 freq regs + // For 433 MHz with MAX2831: F_LO = 40M * (N + F/2^20) / 2 + // N = 43, F = ~629146 for ~433 MHz + log("(Expected: N~43 in r3, F_hi in r4)"); +} + +void RadioRxTestView::run_sgpio_test() { + console.clear(true); + log("=== SGPIO TEST (FIXED) ==="); + + // CRITICAL FIX: Set DISABLE=HIGH first (reference HackRF pattern) + LPC_SGPIO->GPIO_OENREG = (1U << 10) | (1U << 11); // SGPIO10,11 outputs + LPC_SGPIO->GPIO_OUTREG = (1U << 10); // DISABLE=HIGH during config + log("Set DISABLE=HIGH"); + + // Small delay for signals to settle + for (volatile int i = 0; i < 10000; i++) { + } + + // NOW enable streaming (DISABLE=LOW) + LPC_SGPIO->GPIO_OUTREG = 0; // DISABLE=LOW, DIRECTION=LOW (RX) + log("Set DISABLE=LOW (streaming)"); + for (volatile int i = 0; i < 10000; i++) { + } + + // Read raw GPIO_INREG multiple times + uint32_t g[4]; + for (int i = 0; i < 4; i++) { + g[i] = LPC_SGPIO->GPIO_INREG; + for (volatile int j = 0; j < 10000; j++) { + } + } + + log("GPIO_IN:"); + log(" " + to_string_hex(g[0], 8) + " " + to_string_hex(g[1], 8)); + log(" " + to_string_hex(g[2], 8) + " " + to_string_hex(g[3], 8)); + + bool changing = (g[0] != g[1]) || (g[1] != g[2]) || (g[2] != g[3]); + + if (changing) { + log("PASS: Data changing!"); + } else if (g[0] == 0) { + log("FAIL: All zeros"); + } else if (g[0] == 0x00000FFF) { + log("FAIL: 0xFFF = pull-ups"); + log("FPGA not driving data"); + } else { + log("FAIL: Static " + to_string_hex(g[0], 8)); + } + + uint32_t out = LPC_SGPIO->GPIO_OUTREG; + log("HOST_DIS=" + to_string_dec_uint((out >> 10) & 1)); +} + +void RadioRxTestView::run_full_test() { + console.clear(true); + log("=== FULL RX TEST ==="); + + // Step 1: Init + log("[1/6] Init radio..."); + radio::init(); + radio_initialized_ = true; + + // Step 2: Set sample rate (configures Si5351 clocks!) + log("[2/6] Set 8M sample rate..."); + radio::set_baseband_rate(8000000); + + // Step 3: RX mode + log("[3/6] Set RX mode..."); + radio::set_direction(rf::Direction::Receive); + + // Step 4: Frequency - use 2437 MHz (WiFi ch6) which is in MAX2831 range + uint32_t wifi_freq = 2437000000; + log("[4/6] Set 2437 MHz (WiFi)..."); + bool freq_ok = radio::set_tuning_frequency(wifi_freq); + log(freq_ok ? " Freq OK" : " Freq FAIL"); + + // Step 5: Configure SGPIO outputs with correct DISABLE sequence + log("[5/6] Configure SGPIO..."); + // CRITICAL FIX: Set DISABLE=HIGH first + LPC_SGPIO->GPIO_OENREG = (1U << 10) | (1U << 11); // SGPIO10,11 as outputs + LPC_SGPIO->GPIO_OUTREG = (1U << 10); // DISABLE=HIGH during config + log(" DISABLE=HIGH"); + + // Delay for settle + for (volatile int i = 0; i < 10000; i++) { + } + + // NOW enable streaming (DISABLE=LOW) + LPC_SGPIO->GPIO_OUTREG = 0; // DISABLE=LOW, DIRECTION=LOW (RX) + log(" DISABLE=LOW (streaming)"); + + // Step 6: Check raw GPIO pins + log("[6/6] Check GPIO pins..."); + + // Delay for stabilization + for (volatile int i = 0; i < 200000; i++) { + } + + // Read raw GPIO_INREG multiple times + uint32_t g1 = LPC_SGPIO->GPIO_INREG; + for (volatile int i = 0; i < 10000; i++) { + } + uint32_t g2 = LPC_SGPIO->GPIO_INREG; + for (volatile int i = 0; i < 10000; i++) { + } + uint32_t g3 = LPC_SGPIO->GPIO_INREG; + for (volatile int i = 0; i < 10000; i++) { + } + uint32_t g4 = LPC_SGPIO->GPIO_INREG; + + log("GPIO_IN readings:"); + log(" " + to_string_hex(g1, 8) + " " + to_string_hex(g2, 8)); + log(" " + to_string_hex(g3, 8) + " " + to_string_hex(g4, 8)); + + bool gpio_changing = (g1 != g2) || (g2 != g3) || (g3 != g4); + bool gpio_not_zero = (g1 != 0); + bool gpio_not_fff = (g1 != 0x00000FFF); + + // Check Si5351 output enable register (reg 3) + // Bits 0-7: CLK0-7 output enable (0=enabled, 1=disabled) + // We want CLK0 and CLK1 enabled (bits 0,1 = 0) + log("---"); + + if (gpio_changing) { + log("=== PASS: Data flowing! ==="); + } else if (gpio_not_fff && gpio_not_zero) { + log("=== PARTIAL: Static data ==="); + log("FPGA outputs but no clock?"); + } else if (!gpio_not_zero) { + log("=== FAIL: All zeros ==="); + log("FPGA not driving outputs"); + } else { + log("=== FAIL: All FFF (pull-ups) ==="); + log("FPGA outputs high-Z"); + log("Check: Si5351 CLK0/CLK1"); + } + + log_registers("[Final]"); +} + +bool RadioRxTestView::check_gpio_changing() { + uint32_t g[4]; + for (int i = 0; i < 4; i++) { + g[i] = LPC_SGPIO->GPIO_INREG; + for (volatile int j = 0; j < 10000; j++) { + } + } + return (g[0] != g[1]) || (g[1] != g[2]) || (g[2] != g[3]); +} + +void RadioRxTestView::run_step_test() { + console.clear(true); + log("=== STEP TEST (FIXED) ==="); + log("Correct DISABLE sequence"); + + // Ensure radio is initialized + if (!radio_initialized_) { + log("Init radio..."); + radio::init(); + radio_initialized_ = true; + radio::set_baseband_rate(8000000); + radio::set_direction(rf::Direction::Receive); + radio::set_tuning_frequency(2437000000); + } + + // Step 0: Baseline with DISABLE=HIGH first + log("[0] Baseline (DISABLE=HIGH)"); + LPC_SGPIO->CTRL_ENABLE = 0; // Disable all slices + LPC_SGPIO->GPIO_OENREG = 0x0C00; // Bits 10, 11 outputs + LPC_SGPIO->GPIO_OUTREG = (1U << 10); // DISABLE=HIGH first! + for (volatile int i = 0; i < 100000; i++) { + } + + // Now enable streaming to check baseline + LPC_SGPIO->GPIO_OUTREG = 0x0000; // DISABLE=LOW + for (volatile int i = 0; i < 100000; i++) { + } + bool step0 = check_gpio_changing(); + log(step0 ? " PASS: Data changing" : " FAIL: Data static"); + if (!step0) { + log("ABORT: Baseline broken"); + return; + } + + // NOW disable streaming for configuration + log("[Config] Set DISABLE=HIGH"); + LPC_SGPIO->GPIO_OUTREG = (1U << 10); // DISABLE=HIGH + for (volatile int i = 0; i < 100000; i++) { + } + + // Step 1: OUT_MUX_CFG data pins - test individually + log("[1] OUT_MUX_CFG[0-7] data"); + uint32_t data_out_mux = (9U << 0) | (0U << 4); // DOUT_DOUTM8A, GPIO_OE + + for (size_t i = 0; i < 8; i++) { + uint32_t before = LPC_SGPIO->GPIO_INREG; + LPC_SGPIO->OUT_MUX_CFG[i] = data_out_mux; + for (volatile int j = 0; j < 50000; j++) { + } + uint32_t after = LPC_SGPIO->GPIO_INREG; + bool ok = check_gpio_changing(); + + log(" [" + to_string_dec_uint(i) + "] " + + to_string_hex(before & 0xFF, 2) + "->" + + to_string_hex(after & 0xFF, 2) + + (ok ? " OK" : " FAIL")); + + if (!ok) { + log("CULPRIT: OUT_MUX_CFG[" + to_string_dec_uint(i) + "]"); + return; + } + } + log(" All data pins PASS"); + + // Step 2: OUT_MUX_CFG control pins - SKIP PIN 10 (HOST_DISABLE) + log("[2] OUT_MUX_CFG ctrl pins"); + log(" (skipping pin 10 - breaks)"); + + struct { + int pin; + uint32_t val; + } ctrl_pins[] = { + {8, (0U << 0) | (0U << 4)}, + {9, (0U << 0) | (0U << 4)}, + // {10, (4U << 0) | (0U << 4)}, // SKIP - causes failure + {11, (4U << 0) | (0U << 4)}, + {14, (0U << 0) | (0U << 4)}}; + + for (auto& p : ctrl_pins) { + uint32_t before = LPC_SGPIO->GPIO_INREG; + LPC_SGPIO->OUT_MUX_CFG[p.pin] = p.val; + for (volatile int i = 0; i < 50000; i++) { + } + uint32_t after = LPC_SGPIO->GPIO_INREG; + bool ok = check_gpio_changing(); + + log(" [" + to_string_dec_uint(p.pin) + "] " + + to_string_hex(before & 0xFF, 2) + "->" + + to_string_hex(after & 0xFF, 2) + + (ok ? " OK" : " FAIL")); + + if (!ok) { + log("CULPRIT: OUT_MUX_CFG[" + to_string_dec_uint(p.pin) + "]"); + return; + } + } + log(" All ctrl pins PASS"); + + // Step 3: Set GPIO_OENREG for RX + log("[3] GPIO_OENREG full RX"); + LPC_SGPIO->GPIO_OENREG = 0x0C00; // Keep same as baseline + for (volatile int i = 0; i < 100000; i++) { + } + bool step3 = check_gpio_changing(); + log(step3 ? " PASS" : " FAIL: Data stopped!"); + if (!step3) { + log("CULPRIT: GPIO_OENREG"); + return; + } + + // Step 3.5: Configure slice D as clock source (CRITICAL!) + log("[3.5] Slice D clock source"); + const uint32_t slice_d = 3; + // SGPIO_MUX_CFG: External clock from SGPIO8, qualifier from SGPIO9 + LPC_SGPIO->SGPIO_MUX_CFG[slice_d] = (1U << 0) | (0U << 1) | (0U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (0U << 11) | (0U << 12); + // SLICE_MUX_CFG: 1 bit per clock, CLKGEN_MODE=1 (external clock!) <- FIX + LPC_SGPIO->SLICE_MUX_CFG[slice_d] = (0U << 0) | (0U << 1) | (0U << 2) | (0U << 3) | (1U << 4) | (0U << 6) | (0U << 8); + LPC_SGPIO->PRESET[slice_d] = 0; + LPC_SGPIO->COUNT[slice_d] = 0; + LPC_SGPIO->POS[slice_d] = (0x1F << 0) | (0x1F << 8); + LPC_SGPIO->REG[slice_d] = 0x11111111; + LPC_SGPIO->REG_SS[slice_d] = 0x11111111; + // Enable slice D counter + LPC_SGPIO->CTRL_ENABLE = (1U << slice_d); + for (volatile int i = 0; i < 100000; i++) { + } + bool step3_5 = check_gpio_changing(); + log(step3_5 ? " PASS" : " FAIL: Data stopped!"); + if (!step3_5) { + log("CULPRIT: Slice D config"); + return; + } + + // Step 4: SGPIO_MUX_CFG slice A + log("[4] SGPIO_MUX_CFG[A]"); + LPC_SGPIO->SGPIO_MUX_CFG[0] = (1U << 0) | (0U << 1) | (3U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (0U << 11) | (0U << 12); // Clock from slice D (bit3-4=3), external pin SGPIO8, qualifier SGPIO9 + for (volatile int i = 0; i < 100000; i++) { + } + bool step4 = check_gpio_changing(); + log(step4 ? " PASS" : " FAIL: Data stopped!"); + if (!step4) { + log("CULPRIT: SGPIO_MUX_CFG[A]"); + return; + } + + // Step 5: SLICE_MUX_CFG slice A + log("[5] SLICE_MUX_CFG[A]"); + LPC_SGPIO->SLICE_MUX_CFG[0] = (0U << 0) | (0U << 1) | (1U << 2) | (0U << 3) | (1U << 4) | (3U << 6) | (0U << 8); // CLKGEN_MODE=1 (external clock!), PARALLEL_MODE 1 byte + for (volatile int i = 0; i < 100000; i++) { + } + bool step5 = check_gpio_changing(); + log(step5 ? " PASS" : " FAIL: Data stopped!"); + if (!step5) { + log("CULPRIT: SLICE_MUX_CFG[A]"); + return; + } + + // Step 6: Slice A registers + log("[6] Slice A registers"); + LPC_SGPIO->PRESET[0] = 0; + LPC_SGPIO->COUNT[0] = 0; + LPC_SGPIO->POS[0] = (0x1F << 0) | (0x1F << 8); // pos, pos_reset + LPC_SGPIO->REG[0] = 0; + LPC_SGPIO->REG_SS[0] = 0; + for (volatile int i = 0; i < 100000; i++) { + } + bool step6 = check_gpio_changing(); + log(step6 ? " PASS" : " FAIL: Data stopped!"); + if (!step6) { + log("CULPRIT: Slice A registers"); + return; + } + + // Step 7: Enable slice A counter (keep slice D enabled) - still with DISABLE=HIGH + log("[7] Enable slices D+A"); + LPC_SGPIO->CTRL_ENABLE = (1U << 3) | (1U << 0); // Slice D + Slice A + for (volatile int i = 0; i < 100000; i++) { + } + + // Check STATUS_1 BEFORE enabling streaming + uint32_t status_pre = LPC_SGPIO->STATUS_1; + log(" STATUS_1 (pre): " + to_string_hex(status_pre, 4)); + + // Step 8: Enable streaming (DISABLE=LOW) - THIS IS THE CRITICAL TEST + log("[8] Enable streaming (DISABLE=LOW)"); + LPC_SGPIO->GPIO_OUTREG = 0; // DISABLE=LOW + for (volatile int i = 0; i < 100000; i++) { + } + + // Check if slices become active + uint32_t status_post = LPC_SGPIO->STATUS_1; + uint32_t regss = LPC_SGPIO->REG_SS[0]; + uint32_t count_a = LPC_SGPIO->COUNT[0]; + + log(" STATUS_1 (post): " + to_string_hex(status_post, 4)); + log(" REG_SS[0]: " + to_string_hex(regss, 8)); + log(" COUNT[0]: " + to_string_hex(count_a, 8)); + + bool step8 = check_gpio_changing(); + log(step8 ? " GPIO still changing" : " GPIO stopped!"); + + if ((status_post & 1) && regss != 0) { + log("=== SUCCESS! Slice A capturing! ==="); + } else if (status_post & 1) { + log("=== PARTIAL: Slice A active but REG_SS=0 ==="); + } else { + log("=== FAIL: Slice A not active ==="); + log("Expected: STATUS_1 bit 0 = 1"); + log("Actual: STATUS_1 bit 0 = " + to_string_dec_uint(status_post & 1)); + } +} + +/* SGPIO8ClockDetectorView ***********************************************/ + +SGPIO8ClockDetectorView::SGPIO8ClockDetectorView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_samples, + &text_samples, + &text_lbl_toggles, + &text_toggles, + &text_status, + &button_sample, + &button_done, + }); + + text_title.set_style(Theme::getInstance()->fg_yellow); + + button_sample.on_select = [this](Button&) { + sample_sgpio8(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Auto-sample on load + sample_sgpio8(); +} + +void SGPIO8ClockDetectorView::focus() { + button_sample.focus(); +} + +void SGPIO8ClockDetectorView::sample_sgpio8() { + // Sample SGPIO8 (bit 8 of GPIO_INREG) as fast as possible + // NOTE: Software sampling cannot accurately measure clock frequency + // This only detects presence/absence of clock activity + const int num_samples = 2000; + uint8_t samples[num_samples]; + + // Sample as fast as possible + for (int i = 0; i < num_samples; i++) { + samples[i] = (LPC_SGPIO->GPIO_INREG >> 8) & 1; + } + + // Count toggles (transitions 0→1 or 1→0) + int toggles = 0; + for (int i = 1; i < num_samples; i++) { + if (samples[i] != samples[i - 1]) { + toggles++; + } + } + + // Display first 20 samples + std::string sample_str; + for (int i = 0; i < 20 && i < num_samples; i++) { + sample_str += (samples[i] ? "1" : "0"); + } + text_samples.set(sample_str); + + // Display toggle count + text_toggles.set(to_string_dec_uint(toggles) + " / " + + to_string_dec_uint(num_samples - 1) + " transitions"); + + // Status interpretation - just presence detection + if (toggles > 100) { + text_status.set("CLOCK ACTIVE"); + text_status.set_style(Theme::getInstance()->fg_green); + } else if (toggles > 0) { + text_status.set("SOME ACTIVITY (" + to_string_dec_uint(toggles) + ")"); + text_status.set_style(Theme::getInstance()->fg_orange); + } else { + text_status.set("NO CLOCK - Stuck " + + std::string(samples[0] ? "HIGH" : "LOW")); + text_status.set_style(Theme::getInstance()->fg_red); + } +} + +/* Si5351DebugView *******************************************************/ + +Si5351DebugView::Si5351DebugView(NavigationView& nav) + : nav_(nav) { + add_children({&text_title, + &text_status_label, + &text_status_value, + &text_pll_a_label, + &text_pll_a_status, + &text_pll_b_label, + &text_pll_b_status, + &text_sys_init_label, + &text_sys_init_status, + &text_xtal_cap_label, + &text_xtal_cap_value, + &text_clk0_label, + &text_clk0_status, + &text_clk0_freq_label, + &text_clk0_freq_value, + &text_clk0_div_label, + &text_clk0_div_value, + &text_clk1_label, + &text_clk1_status, + &button_refresh, + &button_reset_pll, + &button_done}); + + text_title.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + refresh_status(); + }; + + button_reset_pll.on_select = [this](Button&) { + reset_pll(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Auto-refresh on load + refresh_status(); +} + +void Si5351DebugView::focus() { + button_refresh.focus(); +} + +void Si5351DebugView::refresh_status() { + // Read device status register (reg 0) + uint8_t status = portapack::clock_manager.si5351_read_status(); + text_status_value.set("0x" + to_string_hex(status, 2)); + + // Decode status bits + bool pll_a_locked = !(status & 0x20); // Bit 5: LOL_A (Loss of Lock A) + bool pll_b_locked = !(status & 0x40); // Bit 6: LOL_B (Loss of Lock B) + bool sys_init = (status & 0x80); // Bit 7: SYS_INIT + bool los_clkin = (status & 0x10); // Bit 4: LOS (Loss of Signal) + + // PLL A status + if (pll_a_locked) { + text_pll_a_status.set("LOCKED"); + text_pll_a_status.set_style(Theme::getInstance()->fg_green); + } else { + text_pll_a_status.set("UNLOCKED"); + text_pll_a_status.set_style(Theme::getInstance()->fg_red); + } + + // PLL B status + if (pll_b_locked) { + text_pll_b_status.set("LOCKED"); + text_pll_b_status.set_style(Theme::getInstance()->fg_green); + } else { + text_pll_b_status.set("UNLOCKED (unused)"); + text_pll_b_status.set_style(Theme::getInstance()->fg_orange); + } + + // SYS_INIT status + if (sys_init) { + text_sys_init_status.set("IN PROGRESS"); + text_sys_init_status.set_style(Theme::getInstance()->fg_orange); + } else { + text_sys_init_status.set("COMPLETE"); + text_sys_init_status.set_style(Theme::getInstance()->fg_green); + } + + // Read crystal load capacitance (reg 183) + uint8_t xtal_cap = portapack::clock_manager.si5351_read_register(183); + text_xtal_cap_value.set("0x" + to_string_hex(xtal_cap, 2) + + " (" + to_string_dec_uint((xtal_cap >> 6) & 0x03) + ")"); + + // Read clock output enables (reg 16-23 control, reg 3 for output enable mask) + uint8_t output_enable_mask = portapack::clock_manager.si5351_read_register(3); + + // CLK0 (bit 0 of reg 3, reg 16 for control) + uint8_t clk0_ctrl = portapack::clock_manager.si5351_read_register(16); + bool clk0_enabled = !(output_enable_mask & 0x01) && !(clk0_ctrl & 0x80); + text_clk0_status.set(clk0_enabled ? "ON" : "OFF"); + text_clk0_status.set_style(clk0_enabled ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + + // Read MS0 multisynth parameters (registers 42-49) to calculate actual frequency + // Si5351 MS0 Register Layout: + // Reg 42: P3[15:8] + // Reg 43: P3[7:0] + // Reg 44: bits 6:4 = R_DIV[2:0], bits 1:0 = P1[17:16] + // Reg 45: P1[15:8] + // Reg 46: P1[7:0] + // Reg 47: bits 7:4 = P3[19:16], bits 3:0 = P2[19:16] + // Reg 48: P2[15:8] + // Reg 49: P2[7:0] + uint8_t reg44 = portapack::clock_manager.si5351_read_register(44); + uint8_t reg45 = portapack::clock_manager.si5351_read_register(45); + uint8_t reg46 = portapack::clock_manager.si5351_read_register(46); + + // Decode R divider from bits 6:4 of register 44 + uint8_t r_div_encoded = (reg44 >> 4) & 0x07; + uint32_t r_div = 1 << r_div_encoded; // R = 2^r_div_encoded + + // Decode P1 (18-bit value): bits 1:0 of reg44 = P1[17:16], reg45 = P1[15:8], reg46 = P1[7:0] + uint32_t p1 = ((uint32_t)(reg44 & 0x03) << 16) | ((uint32_t)reg45 << 8) | reg46; + + // Calculate divider: a = (P1 + 512) / 128 for integer dividers (b=0) + // Expected for 8 MHz: P1=5888 (0x1700), a=50 + uint32_t ms_div = (p1 + 512) / 128; // Integer divider value + + // Calculate frequency: f_out = 800 MHz / ms_div / r_div + uint32_t freq_khz = 800000 / ms_div / r_div; // Result in kHz + + // Show P1 value and R45 for debugging + text_clk0_freq_value.set(to_string_dec_uint(freq_khz) + " kHz (P1:" + to_string_hex(p1, 4) + ")"); + text_clk0_div_value.set("MS=" + to_string_dec_uint(ms_div) + + " R=" + to_string_dec_uint(r_div)); + + // Color code based on expected 8 MHz + if (freq_khz >= 7900 && freq_khz <= 8100) { + text_clk0_freq_value.set_style(Theme::getInstance()->fg_green); + text_clk0_div_value.set_style(Theme::getInstance()->fg_green); + } else { + text_clk0_freq_value.set_style(Theme::getInstance()->fg_red); + text_clk0_div_value.set_style(Theme::getInstance()->fg_red); + } + + // CLK1 (bit 1 of reg 3, reg 17 for control) + uint8_t clk1_ctrl = portapack::clock_manager.si5351_read_register(17); + bool clk1_enabled = !(output_enable_mask & 0x02) && !(clk1_ctrl & 0x80); + text_clk1_status.set(clk1_enabled ? "ON" : "OFF"); + text_clk1_status.set_style(clk1_enabled ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); +} + +void Si5351DebugView::reset_pll() { + // Reset both PLLs (write to reg 177) + portapack::clock_manager.si5351_read_register(177); // Read first + portapack::clock_manager.si5351_write_register(177, 0xAC); // Reset both PLLs + + // Small delay for PLL to settle + chThdSleepMilliseconds(10); + + // Refresh status to show new lock state + refresh_status(); +} + +#ifdef PRALINE +/* SignalPathStatusView *************************************************/ + +SignalPathStatusView::SignalPathStatusView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_max_enable, + &text_max_enable, + &text_lbl_max_mode, + &text_max_mode, + &text_lbl_rf_path, + &text_rf_path, + &text_lbl_rf_amp, + &text_rf_amp, + &text_lbl_lna, + &text_lna, + &text_lbl_vga, + &text_vga, + &text_lbl_fpga_decim, + &text_fpga_decim, + &text_status, + &button_refresh, + &button_done, + }); + + text_title.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + refresh_status(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Initial update + refresh_status(); +} + +void SignalPathStatusView::focus() { + button_refresh.focus(); +} + +void SignalPathStatusView::refresh_status() { + // Get cached state from radio driver + rf::Direction direction = radio::debug::get_cached_direction(); + bool rf_amp = radio::debug::get_cached_rf_amp(); + int_fast8_t cached_lna = radio::debug::get_cached_lna_gain(); + int_fast8_t cached_vga = radio::debug::get_cached_vga_gain(); + + // Read actual register values to verify + uint32_t max_r11 = radio::debug::second_if::register_read(11); + + // Decode actual LNA gain from register (bits 6:5) + uint8_t lna_bits = (max_r11 >> 5) & 0x03; + int actual_lna_db; + switch (lna_bits) { + case 0: + actual_lna_db = 0; + break; // -33 dB from max + case 2: + actual_lna_db = 17; + break; // -16 dB from max + case 3: + actual_lna_db = 33; + break; // Maximum + default: + actual_lna_db = -1; + break; // Invalid + } + + // Decode actual VGA gain from register (bits 4:0) + uint8_t vga_bits = (max_r11 >> 0) & 0x1F; + int actual_vga_db = vga_bits * 2; // 0-31 → 0-62 dB + + // MAX2831 Enable/Mode (GPIO-controlled, show cached state) + bool rx_mode = (direction == rf::Direction::Receive); + + text_max_enable.set("ENABLED cached"); + text_max_enable.set_style(Theme::getInstance()->fg_green); + + text_max_mode.set(rx_mode ? "RX cached" : "TX cached"); + text_max_mode.set_style(rx_mode ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_orange); + + // RF path direction + text_rf_path.set(rx_mode ? "RECEIVE" : "TRANSMIT"); + text_rf_path.set_style(rx_mode ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_orange); + + // RF amp (GPIO-controlled, show cached state) + text_rf_amp.set(rf_amp ? "ON cached" : "OFF cached"); + text_rf_amp.set_style(rf_amp ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_orange); + + // LNA gain - show both cached and actual + if (actual_lna_db == cached_lna) { + text_lna.set(to_string_dec_uint(actual_lna_db) + " dB"); + text_lna.set_style(Theme::getInstance()->fg_green); + } else if (actual_lna_db >= 0) { + // MAX2831 has discrete steps: 0, 17, 33 dB + // Allow ±8 dB tolerance for rounding + int diff = (actual_lna_db > cached_lna) ? (actual_lna_db - cached_lna) : (cached_lna - actual_lna_db); + + if (diff <= 8) { + // Within rounding tolerance - show as OK with note + text_lna.set(to_string_dec_uint(actual_lna_db) + " dB (req:" + + to_string_dec_uint(cached_lna) + ")"); + text_lna.set_style(Theme::getInstance()->fg_green); + } else { + // Genuine mismatch + text_lna.set(to_string_dec_uint(actual_lna_db) + " dB (!" + + to_string_dec_uint(cached_lna) + ")"); + text_lna.set_style(Theme::getInstance()->fg_red); + } + } else { + text_lna.set("INVALID"); + text_lna.set_style(Theme::getInstance()->fg_red); + } + + // VGA gain - show both cached and actual + if (actual_vga_db == cached_vga) { + text_vga.set(to_string_dec_uint(actual_vga_db) + " dB"); + text_vga.set_style(Theme::getInstance()->fg_green); + } else { + text_vga.set(to_string_dec_uint(actual_vga_db) + " dB (!" + + to_string_dec_uint(cached_vga) + ")"); + text_vga.set_style(Theme::getInstance()->fg_red); + } + + // FPGA decimation register + uint8_t fpga_decim = radio::debug::fpga::register_read(2); + text_fpga_decim.set("n=" + to_string_dec_uint(fpga_decim) + + " (/" + to_string_dec_uint(1 << fpga_decim) + ")"); + + // Summary status + // Summary status - update to account for rounding tolerance + int lna_diff = (actual_lna_db > cached_lna) ? (actual_lna_db - cached_lna) : (cached_lna - actual_lna_db); + bool lna_ok = (actual_lna_db == cached_lna) || (lna_diff <= 8); + bool gains_match = lna_ok && (actual_vga_db == cached_vga); + if (rx_mode && gains_match) { + text_status.set("RX mode, gains verified!"); + text_status.set_style(Theme::getInstance()->fg_green); + } else if (rx_mode && !gains_match) { + text_status.set("RX mode, gain MISMATCH!"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (gains_match) { + text_status.set("TX mode, gains verified ✓"); + text_status.set_style(Theme::getInstance()->fg_orange); + } else { + text_status.set("TX mode, gain MISMATCH!"); + text_status.set_style(Theme::getInstance()->fg_red); + } +} +#endif + +#ifdef PRALINE +/* RFFC5072StatusView *************************************************/ + +RFFC5072StatusView::RFFC5072StatusView(NavigationView& nav) + : nav_(nav) { + add_children({ + &text_title, + &text_lbl_enabled, + &text_enabled, + &text_lbl_freq, + &text_freq, + &text_lbl_path, + &text_path, + &text_lbl_mixer, + &text_mixer, + &text_lbl_r0, + &text_r0, + &text_lbl_r1, + &text_r1, + &text_lbl_r2, + &text_r2, + &text_lbl_decode, + &text_lbl_n, + &text_n, + &text_lbl_lodiv, + &text_lodiv, + &text_lbl_calc, + &text_calc, + &text_status, + &button_refresh, + &button_done, + }); + + text_title.set_style(Theme::getInstance()->fg_yellow); + text_lbl_decode.set_style(Theme::getInstance()->fg_yellow); + + button_refresh.on_select = [this](Button&) { + refresh_status(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + // Initial update + refresh_status(); +} + +void RFFC5072StatusView::focus() { + button_refresh.focus(); +} + +void RFFC5072StatusView::refresh_status() { + // Read CORRECT registers for Path 2 (active path!) + uint32_t r0 = radio::debug::first_if::register_read(0); // Control + uint32_t r15 = radio::debug::first_if::register_read(15); // P2_FREQ1 + uint32_t r16 = radio::debug::first_if::register_read(16); // P2_FREQ2 + uint32_t r17 = radio::debug::first_if::register_read(17); // P2_FREQ3 + + // Display + text_r0.set(to_string_hex(r0, 4)); + text_r1.set(to_string_hex(r15, 4) + " (R15)"); + text_r2.set(to_string_hex(r16, 4) + " (R16)"); + + // Check enabled (R0 bit 4) + bool enabled = (r0 & 0x0010) != 0; + text_enabled.set(enabled ? "ENABLED" : "DISABLED"); + text_enabled.set_style(enabled ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + + // Decode from P2_FREQ1 (R15) using struct layout: + // bits [1:0] = p2vcosel + // bits [3:2] = p2presc (prescaler) + // bits [6:4] = p2lodiv (LO divider) + // bits [15:7] = p2n (N divider integer) + + uint16_t n_int = (r15 >> 7) & 0x1FF; // 9 bits + uint8_t lodiv_sel = (r15 >> 4) & 0x07; // 3 bits + uint8_t presc_sel = (r15 >> 2) & 0x03; // 2 bits + uint8_t vcosel = r15 & 0x03; // 2 bits + + text_n.set(to_string_dec_uint(n_int)); + + // LO divider: 0=÷2, 1=÷4, 2=÷8, 3=÷16, 4=÷32, 5=÷64 + uint16_t lodiv_val = 1u << lodiv_sel; + + // Prescaler: 0=÷2, 1=÷4 (but code only uses 1 or 2 per rffc507x.cpp) + uint16_t presc_val = 1u << presc_sel; + + text_lodiv.set("/" + to_string_dec_uint(lodiv_val) + + " (P: /" + to_string_dec_uint(presc_val) + ")"); + + // Calculate frequencies + // F_VCO = (F_ref × N) / Prescaler + // F_LO = F_VCO / LODIV + const uint32_t f_ref_mhz = 40; + + // N divider is 24-bit fractional + // For quick calc, use integer part only + uint32_t f_vco_mhz = (f_ref_mhz * n_int) / presc_val; + uint32_t f_lo_mhz = f_vco_mhz / lodiv_val; + + text_calc.set(to_string_dec_uint(f_lo_mhz) + " MHz"); + text_freq.set(to_string_dec_uint(f_vco_mhz) + " MHz VCO"); + + // Check ranges + bool vco_ok = (f_vco_mhz >= 2700) && (f_vco_mhz <= 5400); + bool lo_ok = (f_lo_mhz >= 2300) && (f_lo_mhz <= 2700); + + text_calc.set_style(lo_ok ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + text_freq.set_style(vco_ok ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_red); + + // Check mixer mode (R0 bit 5 = MODE, 0=path1, 1=path2) + bool path2_active = (r0 & 0x0020) != 0; + text_path.set(path2_active ? "PATH2" : "PATH1"); + text_mixer.set(path2_active ? "ACTIVE" : "INACTIVE"); + text_mixer.set_style(path2_active ? Theme::getInstance()->fg_green + : Theme::getInstance()->fg_orange); + + // Summary + if (!enabled) { + text_status.set("DISABLED!"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (!path2_active) { + text_status.set("Path 2 not selected!"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (!vco_ok) { + text_status.set("VCO " + to_string_dec_uint(f_vco_mhz) + "MHz OOR"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (!lo_ok) { + text_status.set("LO " + to_string_dec_uint(f_lo_mhz) + "MHz OOR"); + text_status.set_style(Theme::getInstance()->fg_red); + } else { + text_status.set(to_string_dec_uint(f_ref_mhz) + "x" + + to_string_dec_uint(n_int) + "/" + + to_string_dec_uint(presc_val) + "/" + + to_string_dec_uint(lodiv_val) + "=" + + to_string_dec_uint(f_lo_mhz) + "MHz."); + text_status.set_style(Theme::getInstance()->fg_green); + } +} + +/* RFFCTuningDebugView *************************************************/ +RFFCTuningDebugView::RFFCTuningDebugView(NavigationView& nav) { + add_children({ + &text_title, + &text_lbl_called, + &text_called, + &text_lbl_req, + &text_req, + &text_lbl_exp_n, + &text_exp_n, + &text_lbl_act_n, + &text_act_n, + &text_lbl_exp_div, + &text_exp_div, + &text_lbl_act_div, + &text_act_div, + &text_lbl_calc, + &text_calc, + &text_lbl_calc_lo, + &text_calc_lo, + &text_lbl_calc_vco, + &text_calc_vco, + &text_lbl_vco, + &text_vco, + &text_lbl_n_q24, + &text_n_q24, + &text_status, + &button_refresh, + &button_done, + }); + + button_refresh.on_select = [this](Button&) { + refresh(); + }; + + button_done.on_select = [&nav](Button&) { + nav.pop(); + }; + + refresh(); +} + +void RFFCTuningDebugView::focus() { + button_refresh.focus(); +} + +void RFFCTuningDebugView::refresh() { + // Get expected values from last tuning attempt + auto tuning = radio::debug::first_if::get_tuning_info(); + + // Show if set_frequency was ever called + if (tuning.was_called) { + text_called.set("YES"); + text_called.set_style(Theme::getInstance()->fg_green); + + text_req.set(to_string_dec_uint(tuning.requested_freq_mhz) + " MHz"); + text_exp_n.set(to_string_dec_uint(tuning.expected_n)); + + uint16_t exp_lo = 1 << tuning.expected_lodiv; + uint16_t exp_pr = 1 << tuning.expected_presc; + text_exp_div.set(to_string_dec_uint(exp_lo) + " / " + to_string_dec_uint(exp_pr)); + } else { + text_called.set("NO"); + text_called.set_style(Theme::getInstance()->fg_red); + text_req.set("---"); + text_exp_n.set("---"); + text_exp_div.set("---"); + } + + // Read actual hardware values + uint32_t r15 = radio::debug::first_if::register_read(15); + + uint16_t act_n = (r15 >> 7) & 0x1FF; + uint8_t act_lo_sel = (r15 >> 4) & 0x07; + uint8_t act_pr_sel = (r15 >> 2) & 0x03; + + uint16_t act_lo = 1 << act_lo_sel; + uint16_t act_pr = 1 << act_pr_sel; + + text_act_n.set(to_string_dec_uint(act_n)); + text_act_div.set(to_string_dec_uint(act_lo) + " / " + to_string_dec_uint(act_pr)); + + // Calculate what this produces + uint32_t calc_vco = (40 * act_n) / act_pr; + uint32_t calc_lo = calc_vco / act_lo; + text_calc.set(to_string_dec_uint(calc_lo) + " MHz"); + text_vco.set(to_string_dec_uint(tuning.calculated_vco_mhz) + " MHz"); + + text_calc_lo.set(to_string_dec_uint(tuning.calc_lo_freq_mhz) + " MHz"); + text_calc_vco.set(to_string_dec_uint(tuning.calc_vco_inside_mhz) + " MHz"); + text_n_q24.set(to_string_dec_uint(tuning.calc_n_q24 >> 24)); // Show integer part + + // Status comparison + if (!tuning.was_called) { + text_status.set("RFFC Freq set NEVER called!"); + text_status.set_style(Theme::getInstance()->fg_red); + } else if (act_n == tuning.expected_n) { + text_status.set("MATCH! Hardware as expected!"); + text_status.set_style(Theme::getInstance()->fg_green); + } else { + text_status.set("MISMATCH! Exp:" + to_string_dec_uint(tuning.expected_n) + + " Act:" + to_string_dec_uint(act_n)); + text_status.set_style(Theme::getInstance()->fg_red); + } +} + +#endif + +#endif /* DebugPeripheralsMenuView **********************************************/ DebugPeripheralsMenuView::DebugPeripheralsMenuView(NavigationView& nav) @@ -377,11 +1904,21 @@ DebugPeripheralsMenuView::DebugPeripheralsMenuView(NavigationView& nav) } void DebugPeripheralsMenuView::on_populate() { +#ifdef PRALINE + const char* max283x = "MAX2831"; +#else const char* max283x = hackrf_r9 ? "MAX2839" : "MAX2837"; +#endif const char* si5351x = hackrf_r9 ? "Si5351A" : "Si5351C"; add_items({ {"RFFC5072", Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push("RFFC5072", RegistersWidgetConfig{CT_RFFC5072, 31, 31, 16}); }}, +#ifdef PRALINE + {max283x, Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this, max283x]() { nav_.push(max283x, RegistersWidgetConfig{CT_MAX283X, 16, 16, 14}); }}, + {"FPGA", Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push("FPGA (iCE40)", RegistersWidgetConfig{CT_FPGA, 6, 6, 8}); }}, +#else {max283x, Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this, max283x]() { nav_.push(max283x, RegistersWidgetConfig{CT_MAX283X, 32, 32, 10}); }}, +#endif + {"SGPIO", Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push("SGPIO", RegistersWidgetConfig{CT_SGPIO, 6, 6, 16}); }}, {si5351x, Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this, si5351x]() { nav_.push(si5351x, RegistersWidgetConfig{CT_SI5351, 188, 96, 8}); }}, {audio::debug::codec_name(), Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push(audio::debug::codec_name(), RegistersWidgetConfig{CT_AUDIO, audio::debug::reg_count(), audio::debug::reg_count(), audio::debug::reg_bits()}); }}, }); @@ -418,6 +1955,17 @@ void DebugMenuView::on_populate() { add_items({{"..", ui::Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}}); } add_items({ +#ifdef PRALINE + {"Radio Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"Baseband Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"SGPIO Live", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"SGPIO8 Clock", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"Si5351 Clocks", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"Signal Path", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"RFFC Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"RFFC Tuning", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"RX Test", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, +#endif {"Buttons Test", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_controls, [this]() { nav_.push(); }}, {"M0 Stack Dump", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { stack_dump(); }}, {"Memory Dump", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { nav_.push(); }}, diff --git a/firmware/application/apps/ui_debug.hpp b/firmware/application/apps/ui_debug.hpp index e6c0bf56d..ef974c079 100644 --- a/firmware/application/apps/ui_debug.hpp +++ b/firmware/application/apps/ui_debug.hpp @@ -93,6 +93,10 @@ typedef enum { CT_SI5351, CT_AUDIO, CT_MAX17055, +#ifdef PRALINE + CT_FPGA, +#endif + CT_SGPIO, } chip_type_t; struct RegistersWidgetConfig { @@ -373,6 +377,459 @@ private: }; };*/ +/* Radio Signal Path Diagnostics View + * Shows status of each component in the RX/TX signal chain: + * Antenna -> RF Path -> RFFC5072 -> MAX283x -> MAX5864 -> FPGA -> SGPIO -> MCU + */ +class RadioDiagnosticsView : public View { + public: + RadioDiagnosticsView(NavigationView& nav); + + void focus() override; + std::string title() const override { return "Radio Diag"; }; + + private: + NavigationView& nav_; + + void update_status(); + + Text text_title{{0, 0, 240, 16}, "=== Signal Path Status ==="}; + + Text text_lbl_rffc{{0, 20, 140, 16}, "RFFC5072 (1st IF):"}; + Text text_rffc_status{{144, 20, 96, 16}, "---"}; + + Text text_lbl_max{{0, 36, 140, 16}, "MAX283x (2nd IF):"}; + Text text_max_status{{144, 36, 96, 16}, "---"}; + + Text text_lbl_adc{{0, 52, 140, 16}, "MAX5864 (ADC):"}; + Text text_adc_status{{144, 52, 96, 16}, "---"}; + + Text text_lbl_fpga{{0, 68, 140, 16}, "FPGA/CPLD:"}; + Text text_fpga_status{{144, 68, 96, 16}, "---"}; + + Text text_lbl_sgpio{{0, 84, 140, 16}, "SGPIO:"}; + Text text_sgpio_status{{144, 84, 96, 16}, "---"}; + + Text text_lbl_clock{{0, 100, 140, 16}, "Si5351 Clocks:"}; + Text text_clock_status{{144, 100, 96, 16}, "---"}; + + Text text_regs_title{{0, 124, 240, 16}, "=== Key Registers ==="}; + + Text text_lbl_rffc_reg{{0, 144, 80, 16}, "RFFC R0:"}; + Text text_rffc_reg{{80, 144, 160, 16}, "---"}; + + Text text_lbl_max_reg{{0, 160, 80, 16}, "MAX R0:"}; + Text text_max_reg{{80, 160, 160, 16}, "---"}; + + Text text_lbl_fpga_reg{{0, 176, 80, 16}, "FPGA:"}; + Text text_fpga_reg{{80, 176, 160, 16}, "---"}; + + Text text_lbl_sgpio_reg{{0, 192, 80, 16}, "SGPIO:"}; + Text text_sgpio_reg{{80, 192, 160, 16}, "---"}; + + Text text_test_result{{0, 220, 240, 32}, ""}; + + Button button_refresh{ + {8, 280, 72, 24}, + "Refresh"}; + + Button button_done{ + {168, 280, 64, 24}, + "Done"}; +}; + +/* BasebandStatusView ***************************************************/ + +class BasebandStatusView : public View { + public: + BasebandStatusView(NavigationView& nav); + + void focus() override; + std::string title() const override { return "Baseband Status"; }; + + private: + NavigationView& nav_; + + void update(); + + Text text_title{{0, 0, 240, 16}, "=== Baseband Counters ==="}; + + Text text_lbl_marker{{0, 20, 140, 16}, "Streaming Marker:"}; + Text text_marker{{144, 20, 96, 16}, "---"}; + + Text text_lbl_loops{{0, 36, 140, 16}, "Baseband Loops:"}; + Text text_loops{{144, 36, 96, 16}, "---"}; + + Text text_lbl_wait{{0, 52, 140, 16}, "DMA Wait Count:"}; + Text text_wait{{144, 52, 96, 16}, "---"}; + + Text text_lbl_xfr{{0, 68, 140, 16}, "DMA Xfr Count:"}; + Text text_xfr{{144, 68, 96, 16}, "---"}; + + Text text_lbl_missed{{0, 84, 140, 16}, "Buffer Missed:"}; + Text text_missed{{144, 84, 96, 16}, "---"}; + + Text text_status_line1{{0, 110, 240, 16}, ""}; + Text text_status_line2{{0, 126, 240, 16}, ""}; + Text text_status_line3{{0, 142, 240, 16}, ""}; + + Button button_refresh{ + {8, 280, 72, 24}, + "Refresh"}; + + Button button_done{ + {168, 280, 64, 24}, + "Done"}; + + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { this->update(); }}; +}; + +/* SGPIOLiveMonitorView ***************************************************/ + +class SGPIOLiveMonitorView : public View { + public: + SGPIOLiveMonitorView(NavigationView& nav); + + void focus() override; + std::string title() const override { return "SGPIO Live"; }; + + private: + NavigationView& nav_; + + void update(); + + Text text_title{{0, 0, 240, 16}, "=== SGPIO Registers ==="}; + + Text text_lbl_ctrl{{0, 20, 140, 16}, "CTRL_ENABLE:"}; + Text text_ctrl{{144, 20, 96, 16}, "---"}; + + Text text_lbl_in{{0, 36, 140, 16}, "GPIO_INREG:"}; + Text text_in{{144, 36, 96, 16}, "---"}; + + Text text_lbl_ss{{0, 52, 140, 16}, "REG_SS[0]:"}; + Text text_ss{{144, 52, 96, 16}, "---"}; + + Text text_lbl_status{{0, 68, 140, 16}, "STATUS_1:"}; + Text text_status{{144, 68, 96, 16}, "---"}; + + Text text_lbl_out{{0, 84, 140, 16}, "GPIO_OUTREG:"}; + Text text_out{{144, 84, 96, 16}, "---"}; + + Text text_lbl_oen{{0, 100, 140, 16}, "GPIO_OENREG:"}; + Text text_oen{{144, 100, 96, 16}, "---"}; + + Text text_diag_line1{{0, 126, 240, 16}, ""}; + Text text_diag_line2{{0, 142, 240, 16}, ""}; + Text text_diag_line3{{0, 158, 240, 16}, ""}; + Text text_diag_line4{{0, 174, 240, 16}, ""}; + + Button button_refresh{ + {8, 280, 72, 24}, + "Refresh"}; + + Button button_done{ + {168, 280, 64, 24}, + "Done"}; + + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { this->update(); }}; +}; + +/* Radio RX Step-by-Step Test View + * Tests radio hardware directly without M0 baseband involvement. + * Logs each step to help isolate where the signal chain breaks. + */ +class RadioRxTestView : public View { + public: + RadioRxTestView(NavigationView& nav); + + void focus() override; + std::string title() const override { return "RX Test"; }; + + private: + NavigationView& nav_; + bool radio_initialized_{false}; + uint32_t test_frequency_{433000000}; // 433 MHz default + + void log(const std::string& msg); + void log_registers(const std::string& label); + void run_init_test(); + void run_rx_mode_test(); + void run_freq_test(); + void run_sgpio_test(); + void run_full_test(); + void run_step_test(); + bool check_gpio_changing(); + + Labels labels{ + {{0, 0}, "=== Radio RX Test ===", Theme::getInstance()->fg_yellow->foreground}}; + + Console console{ + {0, 20, 240, 200}}; + + Button button_init{ + {0, 224, 56, 24}, + "Init"}; + + Button button_rx{ + {60, 224, 56, 24}, + "RX"}; + + Button button_freq{ + {120, 224, 56, 24}, + "Freq"}; + + Button button_sgpio{ + {180, 224, 56, 24}, + "SGPIO"}; + + Button button_full{ + {0, 252, 56, 24}, + "Full"}; + + Button button_step{ + {60, 252, 56, 24}, + "Step"}; + + Button button_done{ + {120, 252, 112, 24}, + "Done"}; +}; + +/* SGPIO8 Clock Detector View + * Samples SGPIO8 pin to verify external clock is present. + * Shows toggle count and estimated frequency. + */ +class SGPIO8ClockDetectorView : public View { + public: + SGPIO8ClockDetectorView(NavigationView& nav); + void focus() override; + std::string title() const override { return "SGPIO8 Clock"; }; + + private: + NavigationView& nav_; + + Text text_title{{8, 16, 224, 16}, "SGPIO8 Clock Detector"}; + Text text_lbl_samples{{8, 48, 160, 16}, "Samples (first 20):"}; + Text text_samples{{8, 64, 224, 16}, " "}; + Text text_lbl_toggles{{8, 96, 160, 16}, "Toggle count:"}; + Text text_toggles{{8, 112, 224, 16}, " "}; + Text text_status{{8, 144, 224, 32}, " "}; + + Button button_sample{{8, 200, 96, 24}, "Sample"}; + Button button_done{{128, 200, 96, 24}, "Done"}; + + void sample_sgpio8(); +}; + +/* Slice Status View + * Shows SGPIO slice status: which slices are enabled vs active, + * counter values, and data capture status. + */ + +/* Si5351 Debug View + * Dedicated diagnostic tool for Si5351 clock generator. + * Shows PLL lock status, clock configurations, and allows testing. + */ +class Si5351DebugView : public View { + public: + Si5351DebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "Si5351 Clocks"; }; + + private: + NavigationView& nav_; + + Text text_title{{8, 16, 200, 16}, "Si5351 Clock Generator"}; + + Text text_status_label{{8, 40, 80, 16}, "Status Reg:"}; + Text text_status_value{{96, 40, 144, 16}, ""}; + + Text text_pll_a_label{{8, 60, 80, 16}, "PLL A:"}; + Text text_pll_a_status{{96, 60, 144, 16}, ""}; + + Text text_pll_b_label{{8, 80, 80, 16}, "PLL B:"}; + Text text_pll_b_status{{96, 80, 144, 16}, ""}; + + Text text_sys_init_label{{8, 100, 80, 16}, "SYS_INIT:"}; + Text text_sys_init_status{{96, 100, 144, 16}, ""}; + + Text text_xtal_cap_label{{8, 120, 80, 16}, "XTAL Cap:"}; + Text text_xtal_cap_value{{96, 120, 144, 16}, ""}; + + Text text_clk0_label{{8, 150, 72, 16}, "CLK0:"}; + Text text_clk0_status{{88, 150, 152, 16}, ""}; + + Text text_clk0_freq_label{{8, 170, 72, 16}, " Freq:"}; + Text text_clk0_freq_value{{88, 170, 152, 16}, ""}; + + Text text_clk0_div_label{{8, 190, 72, 16}, " Div:"}; + Text text_clk0_div_value{{88, 190, 152, 16}, ""}; + + Text text_clk1_label{{8, 210, 96, 16}, "CLK1 (SCT):"}; + Text text_clk1_status{{112, 210, 128, 16}, ""}; + + Button button_refresh{{8, 240, 72, 24}, "Refresh"}; + Button button_reset_pll{{88, 240, 72, 24}, "Reset PLL"}; + Button button_done{{168, 240, 64, 24}, "Done"}; + + void refresh_status(); + void reset_pll(); +}; + +#ifdef PRALINE +/* SignalPathStatusView *************************************************/ +class SignalPathStatusView : public View { + public: + SignalPathStatusView(NavigationView& nav); + void focus() override; + std::string title() const override { return "Signal Path"; }; + + private: + NavigationView& nav_; + void refresh_status(); + + Text text_title{{0, 0, 240, 16}, "=== Signal Path Status ==="}; + + Text text_lbl_max_enable{{0, 20, 1114, 16}, "MAX2831:"}; + Text text_max_enable{{116, 20, 124, 16}, "---"}; + + Text text_lbl_max_mode{{0, 36, 114, 16}, "RX Mode:"}; + Text text_max_mode{{116, 36, 124, 16}, "---"}; + + Text text_lbl_rf_path{{0, 52, 114, 16}, "RF Path:"}; + Text text_rf_path{{116, 52, 124, 16}, "---"}; + + Text text_lbl_rf_amp{{0, 68, 114, 16}, "RF Amp:"}; + Text text_rf_amp{{116, 68, 124, 16}, "---"}; + + Text text_lbl_lna{{0, 84, 114, 16}, "LNA Gain:"}; + Text text_lna{{116, 84, 124, 16}, "---"}; + + Text text_lbl_vga{{0, 100, 114, 16}, "VGA Gain:"}; + Text text_vga{{116, 100, 124, 16}, "---"}; + + Text text_lbl_fpga_decim{{0, 116, 114, 16}, "FPGA Decim:"}; + Text text_fpga_decim{{116, 116, 124, 16}, "---"}; + + Text text_status{{0, 140, 240, 32}, ""}; + + Button button_refresh{{8, 280, 72, 24}, "Refresh"}; + Button button_done{{168, 280, 64, 24}, "Done"}; +}; +#endif + +#ifdef PRALINE +/* RFFC5072StatusView *************************************************/ +class RFFC5072StatusView : public View { + public: + RFFC5072StatusView(NavigationView& nav); + void focus() override; + std::string title() const override { return "RFFC5072 Status"; }; + + private: + NavigationView& nav_; + void refresh_status(); + + Text text_title{{0, 0, 240, 16}, "=== RFFC5072 (1st IF) ==="}; + + Text text_lbl_enabled{{0, 20, 114, 16}, "Status:"}; + Text text_enabled{{116, 20, 124, 16}, "---"}; + + Text text_lbl_freq{{0, 36, 114, 16}, "LO Freq:"}; + Text text_freq{{116, 36, 124, 16}, "---"}; + + Text text_lbl_path{{0, 52, 114, 16}, "Path:"}; + Text text_path{{116, 52, 124, 16}, "---"}; + + Text text_lbl_mixer{{0, 68, 114, 16}, "Mixer:"}; + Text text_mixer{{116, 68, 124, 16}, "---"}; + + Text text_lbl_r0{{0, 92, 114, 16}, "Reg 0:"}; + Text text_r0{{116, 92, 124, 16}, "---"}; + + Text text_lbl_r1{{0, 108, 114, 16}, "Reg 1 (N):"}; + Text text_r1{{116, 108, 124, 16}, "---"}; + + Text text_lbl_r2{{0, 124, 114, 16}, "Reg 2:"}; + Text text_r2{{116, 124, 124, 16}, "---"}; + + Text text_lbl_decode{{0, 148, 240, 16}, "--- Decoded Values ---"}; + + Text text_lbl_n{{0, 168, 114, 16}, "N divider:"}; + Text text_n{{116, 168, 124, 16}, "---"}; + + Text text_lbl_lodiv{{0, 184, 114, 16}, "LO divider:"}; + Text text_lodiv{{116, 184, 124, 16}, "---"}; + + Text text_lbl_calc{{0, 200, 114, 16}, "Calc freq:"}; + Text text_calc{{116, 200, 124, 16}, "---"}; + + Text text_status{{0, 224, 240, 32}, ""}; + + Button button_refresh{{8, 280, 72, 24}, "Refresh"}; + Button button_done{{168, 280, 64, 24}, "Done"}; +}; + +#ifdef PRALINE +/* RFFCTuningDebugView *************************************************/ +class RFFCTuningDebugView : public View { + public: + RFFCTuningDebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "RFFC Tuning Debug"; }; + + private: + void refresh(); + + Text text_title{{0, 0, 240, 16}, "RFFC5072 Tuning Debug"}; + + Text text_lbl_called{{0, 18, 120, 16}, "Freq Set:"}; + Text text_called{{122, 18, 118, 16}, "NO"}; + + Text text_lbl_req{{0, 38, 120, 16}, "Requested:"}; + Text text_req{{122, 38, 118, 16}, "---"}; + + Text text_lbl_exp_n{{0, 56, 120, 16}, "Expected N:"}; + Text text_exp_n{{122, 56, 118, 16}, "---"}; + + Text text_lbl_act_n{{0, 74, 120, 16}, "Actual N:"}; + Text text_act_n{{122, 74, 118, 16}, "---"}; + + Text text_lbl_exp_div{{0, 92, 120, 16}, "Exp LO/Pres:"}; + Text text_exp_div{{122, 92, 118, 16}, "---"}; + + Text text_lbl_act_div{{0, 110, 120, 16}, "Act LO/Pres:"}; + Text text_act_div{{122, 110, 118, 16}, "---"}; + + Text text_lbl_calc{{0, 128, 120, 16}, "Calc LO freq:"}; + Text text_calc{{122, 128, 118, 16}, "---"}; + + Text text_lbl_calc_lo{{0, 146, 120, 16}, "Calc input:"}; + Text text_calc_lo{{122, 146, 118, 16}, "---"}; + + Text text_lbl_calc_vco{{0, 164, 120, 16}, "In Calc VCO:"}; + Text text_calc_vco{{122, 164, 118, 16}, "---"}; + + Text text_lbl_vco{{0, 182, 120, 16}, "Calc VCO:"}; + Text text_vco{{122, 182, 118, 16}, "---"}; + + Text text_lbl_n_q24{{0, 200, 120, 16}, "N (Q24):"}; + Text text_n_q24{{122, 200, 118, 16}, "---"}; + + Text text_status{{0, 224, 240, 48}, ""}; + + Button button_refresh{{8, 280, 72, 24}, "Refresh"}; + Button button_done{{168, 280, 64, 24}, "Done"}; +}; + +#endif + +#endif + class DebugPeripheralsMenuView : public BtnGridView { public: DebugPeripheralsMenuView(NavigationView& nav); diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index 3dae14a9c..abff8bc08 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -52,12 +52,21 @@ static void send_message(const Message* const message) { creg::m0apptxevent::assert_event(); if constexpr (check_for_message_hang) { +#ifdef PRALINE + /* Timeout: ~3 seconds at typical clock speeds */ + auto count = 200'000'000u; +#else auto count = UINT32_MAX; +#endif while (shared_memory.baseband_message && --count) /* spin */; if (count == 0) +#ifdef PRALINE + chDbgPanic("BB Msg Timeout"); +#else chDbgPanic("Baseband Send Fail"); +#endif } else { while (shared_memory.baseband_message) /* spin */; diff --git a/firmware/application/clock_manager.cpp b/firmware/application/clock_manager.cpp index 391a687d1..4e793e43d 100644 --- a/firmware/application/clock_manager.cpp +++ b/firmware/application/clock_manager.cpp @@ -23,6 +23,7 @@ #include "portapack_persistent_memory.hpp" #include "portapack_io.hpp" +#include "portapack.hpp" #include "hackrf_hal.hpp" using namespace hackrf::one; @@ -30,6 +31,14 @@ using namespace hackrf::one; #include "lpc43xx_cpp.hpp" using namespace lpc43xx; +#ifdef PRALINE +extern "C" { +#include "fpga_bridge.h" +} +// Need access to ssp1_arbiter from radio namespace +#include "radio.hpp" +#endif + constexpr uint32_t si5351_vco_f = 800000000; constexpr si5351::Inputs si5351_inputs{ @@ -54,6 +63,12 @@ constexpr si5351::PLL si5351_pll_xtal_25m{ .c = 1, }; constexpr auto si5351_pll_a_xtal_reg = si5351_pll_xtal_25m.reg(0); +#ifdef PRALINE +static_assert(si5351_pll_xtal_25m.f_vco() == si5351_vco_f, "PLL XTAL frequency wrong"); +static_assert(si5351_pll_xtal_25m.p1() == 3584, "PLL XTAL P1 wrong"); +static_assert(si5351_pll_xtal_25m.p2() == 0, "PLL XTAL P2 wrong"); +static_assert(si5351_pll_xtal_25m.p3() == 1, "PLL XTAL P3 wrong"); +#endif constexpr si5351::PLL si5351_pll_clkin_10m{ .f_in = si5351_inputs.f_clkin_out(), @@ -64,10 +79,12 @@ constexpr si5351::PLL si5351_pll_clkin_10m{ constexpr auto si5351c_pll_b_clkin_reg = si5351_pll_clkin_10m.reg(1); constexpr auto si5351a_pll_a_clkin_reg = si5351_pll_clkin_10m.reg(0); +#ifndef PRALINE static_assert(si5351_pll_xtal_25m.f_vco() == si5351_vco_f, "PLL XTAL frequency wrong"); static_assert(si5351_pll_xtal_25m.p1() == 3584, "PLL XTAL P1 wrong"); static_assert(si5351_pll_xtal_25m.p2() == 0, "PLL XTAL P2 wrong"); static_assert(si5351_pll_xtal_25m.p3() == 1, "PLL XTAL P3 wrong"); +#endif static_assert(si5351_pll_clkin_10m.f_vco() == si5351_vco_f, "PLL CLKIN frequency wrong"); static_assert(si5351_pll_clkin_10m.p1() == 9728, "PLL CLKIN P1 wrong"); @@ -101,6 +118,22 @@ constexpr si5351::MultisynthFractional si5351_ms_0_8m{ }; constexpr auto si5351c_ms_0_8m_reg = si5351_ms_0_8m.reg(clock_generator_output_og_codec); +#ifdef PRALINE +// Verify compile-time values for 8 MHz config +static_assert(si5351_ms_0_8m.p1() == 5888, "MS0 8MHz P1 should be 5888 (0x1700)"); +static_assert(si5351_ms_0_8m.p2() == 0, "MS0 8MHz P2 should be 0"); +static_assert(si5351_ms_0_8m.p3() == 1, "MS0 8MHz P3 should be 1"); +static_assert(si5351_ms_0_8m.f_out() == 8000000, "MS0 should output 8 MHz"); + +// Verify register array encoding +static_assert(si5351c_ms_0_8m_reg[0] == 42, "MS0 base register should be 42"); +static_assert(si5351c_ms_0_8m_reg[1] == 0x00, "MS0 reg43 P3[15:8] should be 0x00"); +static_assert(si5351c_ms_0_8m_reg[2] == 0x01, "MS0 reg44 P3[7:0] should be 0x01"); +static_assert(si5351c_ms_0_8m_reg[3] == 0x10, "MS0 reg45 R_DIV should be 0x10"); +static_assert(si5351c_ms_0_8m_reg[4] == 0x17, "MS0 reg46 P1[15:8] should be 0x17"); +static_assert(si5351c_ms_0_8m_reg[5] == 0x00, "MS0 reg47 P1[7:0] should be 0x00"); +#endif + constexpr si5351::MultisynthFractional si5351_ms_group{ .f_src = si5351_vco_f, .a = 80, /* Don't care */ @@ -204,7 +237,11 @@ constexpr ClockControls si5351c_clock_control_common{{ }}; constexpr ClockControls si5351a_clock_control_common{{ +#ifndef PRALINE {ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, +#else + {ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off}, // CLK0: MUST be Fractional for 8 MHz! +#endif {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off}, {ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, @@ -255,9 +292,48 @@ static void portapack_tcxo_disable() { using namespace hackrf::one; void ClockManager::init_clock_generator() { +#ifdef PRALINE + // PRALINE: Configure clock input mux GPIO + // GPIO0_15 (clkin_ctrl) selects GP_CLKIN source: + // 0 = P1 connector (external) + // 1 = P22 (internal Si5351 CLK2) + constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15]; + gpio_clkin_ctrl.output(); + gpio_clkin_ctrl.write(1); // CLKIN_SIGNAL_P22 = 1 = internal Si5351 CLK2 + + // Also enable MCU clock gate (GPIO0_8) + gpio_r9_mcu_clk_en.output(); + gpio_r9_mcu_clk_en.write(1); +#else + // HackRF One r9: GPIO0_8 (mcu_clk_en) gates Si5351 CLK2/CLK7 to GP_CLKIN + if (hackrf_r9) { + gpio_r9_mcu_clk_en.output(); + gpio_r9_mcu_clk_en.write(1); + } +#endif + clock_generator.reset(); clock_generator.set_crystal_internal_load_capacitance(CrystalInternalLoadCapacitance::XTAL_CL_8pF); clock_generator.enable_fanout(); + +#ifdef PRALINE + /* PRALINE has Si5351A (NOT Si5351C like HackRF One OG). + * Must use Si5351A configuration: PLLA only, no CLKIN support. + * + * IMPORTANT: Follow HackRF reference sequence: + * 1. Set PLL input sources + * 2. Configure PLL and multisynths + * 3. Set clock control registers (AFTER multisynths!) + * 4. Reset PLLs + * 5. Enable outputs + */ + clock_generator.set_pll_input_sources(si5351a_pll_input_sources); + + /* Skip MCU CLKIN setup and reference detection for PRALINE - not applicable */ + reference = Reference{ReferenceSource::Xtal, 0}; + + /* Clock control will be set AFTER multisynth configuration - see below */ +#else clock_generator.set_pll_input_sources(hackrf_r9 ? si5351a_pll_input_sources : si5351c_pll_input_sources); @@ -298,7 +374,62 @@ void ClockManager::init_clock_generator() { si5351_clock_control_common[7].ms_src(ref_pll), }}; clock_generator.set_clock_control(si5351_clock_control); +#endif +#ifdef PRALINE + /* PRALINE uses Si5351A with: + * CLK0 = AFE_CLK (codec/FPGA sample clock) + * CLK1 = SCT_CLK (FPGA timing clock at 2x sample rate) + * CLK4 = first IF (RFFC5072) + * CLK5 = second IF (MAX2831) + * Uses PLLA on XTAL only (no CLKIN support). + */ + + /* Step 1: Write PLL A configuration (800 MHz VCO from 25 MHz XTAL) */ + /* Use single-byte writes to debug I2C issues */ + { + const auto& pll_regs = si5351_pll_a_xtal_reg; + const uint8_t base_reg = pll_regs[0]; + for (size_t i = 1; i < pll_regs.size(); i++) { + clock_generator.write_register(base_reg + i - 1, pll_regs[i]); + } + } + + /* Step 2: Write multisynth configurations using single-byte writes */ + clock_generator.write_ms_single_byte(0, si5351_ms_0_8m); // MS0 = divider 50, r_div=1 for 8 MHz + clock_generator.write_ms_single_byte(1, si5351_ms_16m); // MS1 = divider 50, r_div=0 for 16 MHz + + /* CLK4 and CLK5 - use single-byte writes too */ + { + const auto& ms4_regs = si5351c_ms_4_reg; + const uint8_t base_reg = ms4_regs[0]; + for (size_t i = 1; i < ms4_regs.size(); i++) { + clock_generator.write_register(base_reg + i - 1, ms4_regs[i]); + } + } + { + const auto& ms5_regs = si5351c_ms_5_reg; + const uint8_t base_reg = ms5_regs[0]; + for (size_t i = 1; i < ms5_regs.size(); i++) { + clock_generator.write_register(base_reg + i - 1, ms5_regs[i]); + } + } + clock_generator.write(si5351a_ms6_7_off_reg); // MS6/7 off - short write is OK + + /* Step 3: NOW set clock control registers (AFTER multisynths per HackRF reference) */ + const auto ref_pll = ClockControl::MultiSynthSource::PLLA; + const ClockControls si5351_clock_control = ClockControls{{ + si5351a_clock_control_common[0].ms_src(ref_pll), + si5351a_clock_control_common[1].ms_src(ref_pll), + si5351a_clock_control_common[2].ms_src(ref_pll), + si5351a_clock_control_common[3].ms_src(ref_pll), + si5351a_clock_control_common[4].ms_src(ref_pll), + si5351a_clock_control_common[5].ms_src(ref_pll), + si5351a_clock_control_common[6].ms_src(ref_pll), + si5351a_clock_control_common[7].ms_src(ref_pll), + }}; + clock_generator.set_clock_control(si5351_clock_control); +#else if (hackrf_r9) { const PLLReg pll_reg = (reference.source == ReferenceSource::Xtal) ? si5351_pll_a_xtal_reg @@ -319,21 +450,45 @@ void ClockManager::init_clock_generator() { clock_generator.write(si5351c_ms_5_reg); clock_generator.write(si5351c_ms6_7_off_mcu_clkin_reg); } +#endif clock_generator.reset_plls(); // Wait for PLL(s) to lock. +#ifdef PRALINE + // PRALINE: Wait for PLLA to lock (0x20 = LOL_A bit) + uint8_t device_status_mask = 0x20; + uint32_t pll_timeout = 100000; + while ((clock_generator.device_status() & device_status_mask) != 0 && pll_timeout > 0) { + pll_timeout--; + } + // Store PLL lock status for debugging + static volatile uint32_t pll_lock_timeout = pll_timeout; + (void)pll_lock_timeout; + + // CRITICAL: Add delay to ensure Si5351 writes complete before I2C bus stops + chThdSleepMilliseconds(100); +#else + // Wait for PLL(s) to lock - with timeout to prevent hang uint8_t device_status_mask = hackrf_r9 ? 0x20 : (ref_pll == ClockControl::MultiSynthSource::PLLB) ? 0x40 : 0x20; +#ifndef PRALINE while ((clock_generator.device_status() & device_status_mask) != 0); +#else + uint32_t pll_timeout = 100000; + while ((clock_generator.device_status() & device_status_mask) != 0 && pll_timeout > 0) { + pll_timeout--; + } +#endif clock_generator.set_clock_control( clock_generator_output_mcu_clkin, si5351_clock_control_common[clock_generator_output_mcu_clkin].ms_src(ref_pll).clk_pdn(ClockControl::ClockPowerDown::Power_On)); clock_generator.enable_output(clock_generator_output_mcu_clkin); +#endif } uint32_t ClockManager::measure_gp_clkin_frequency() { @@ -400,6 +555,15 @@ void ClockManager::shutdown() { } void ClockManager::enable_codec_clocks() { +#ifdef PRALINE + /* PRALINE: CLK0 (AFE_CLK) for codec/FPGA, CLK1 (SCT_CLK) for FPGA timing. + * Reference hackrf_core.c shows PRALINE needs both CLK0 and CLK1. */ + clock_generator.enable_clock(clock_generator_output_og_codec); /* CLK0 */ + clock_generator.enable_clock(clock_generator_output_og_cpld); /* CLK1 */ + clock_generator.enable_output_mask( + (1U << clock_generator_output_og_codec) | + (1U << clock_generator_output_og_cpld)); +#else if (hackrf_r9) { clock_generator.enable_clock(clock_generator_output_r9_sgpio); } else { @@ -417,6 +581,7 @@ void ClockManager::enable_codec_clocks() { clock_generator.enable_output_mask( (1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld) | (1U << clock_generator_output_og_sgpio)); } +#endif } void ClockManager::disable_codec_clocks() { @@ -424,6 +589,14 @@ void ClockManager::disable_codec_clocks() { * be enabled for the output to come to rest at the state specified by * CLKx_DISABLE_STATE. */ +#ifdef PRALINE + /* PRALINE: CLK0 (AFE_CLK) and CLK1 (SCT_CLK) used for codec/FPGA */ + clock_generator.disable_output_mask( + (1U << clock_generator_output_og_codec) | + (1U << clock_generator_output_og_cpld)); + clock_generator.disable_clock(clock_generator_output_og_codec); + clock_generator.disable_clock(clock_generator_output_og_cpld); +#else if (hackrf_r9) { clock_generator.disable_output_mask(1U << clock_generator_output_r9_sgpio); clock_generator.disable_clock(clock_generator_output_r9_sgpio); @@ -434,9 +607,17 @@ void ClockManager::disable_codec_clocks() { clock_generator.disable_clock(clock_generator_output_og_cpld); clock_generator.disable_clock(clock_generator_output_og_sgpio); } +#endif } void ClockManager::enable_if_clocks() { +#ifdef PRALINE + /* PRALINE uses CLK4 (first IF) and CLK5 (second IF) like original HackRF One */ + clock_generator.enable_clock(clock_generator_output_og_first_if); + clock_generator.enable_output_mask(1U << clock_generator_output_og_first_if); + clock_generator.enable_clock(clock_generator_output_og_second_if); + clock_generator.enable_output_mask(1U << clock_generator_output_og_second_if); +#else if (hackrf_r9) { clock_generator.enable_clock(clock_generator_output_r9_if); clock_generator.enable_output_mask(1U << clock_generator_output_r9_if); @@ -446,9 +627,16 @@ void ClockManager::enable_if_clocks() { clock_generator.enable_clock(clock_generator_output_og_second_if); clock_generator.enable_output_mask(1U << clock_generator_output_og_second_if); } +#endif } void ClockManager::disable_if_clocks() { +#ifdef PRALINE + clock_generator.disable_output_mask(1U << clock_generator_output_og_first_if); + clock_generator.disable_clock(clock_generator_output_og_first_if); + clock_generator.disable_output_mask(1U << clock_generator_output_og_second_if); + clock_generator.disable_clock(clock_generator_output_og_second_if); +#else if (hackrf_r9) { clock_generator.disable_output_mask(1U << clock_generator_output_r9_if); clock_generator.disable_clock(clock_generator_output_r9_if); @@ -458,19 +646,39 @@ void ClockManager::disable_if_clocks() { clock_generator.disable_output_mask(1U << clock_generator_output_og_second_if); clock_generator.disable_clock(clock_generator_output_og_second_if); } +#endif } void ClockManager::set_sampling_frequency(const uint32_t frequency) { +#ifdef PRALINE + /* PRALINE: CLK0=AFE_CLK runs at sample rate (VCO/divider/2) + * CLK1=SCT_CLK runs at 2x sample rate (VCO/divider/1) + * Reference: hackrf_core.c sample_rate_frac_set() lines 580-582 + */ + + /* PRALINE: Match HackRF USB sample_rate_frac_set() + * Reference: hackrf_usb radio.c lines 29-91, hackrf_core.c lines 501-685 */ + + // Set FPGA decimation to 0 (no decimation) for direct passthrough + fpga_debug_register_write(2, 0); + radio::invalidate_spi_config(); + + // The following was originally from @kitty. Adopting for testing radio. + clock_generator.set_ms_frequency(0, frequency * 2, si5351_vco_f, 1); // CLK0: r_div=1 (÷2) + clock_generator.set_ms_frequency(1, frequency * 2, si5351_vco_f, 0); // CLK1: r_div=0 (÷1) +#else /* Codec clock is at sampling frequency, CPLD and SGPIO clocks are at * twice the frequency, and derived from the MS0 synth. So it's only * necessary to change the MS0 synth frequency, and ensure the output * is divided by two. */ + if (hackrf_r9) { clock_generator.set_ms_frequency(clock_generator_output_r9_sgpio, frequency * 2, si5351_vco_f, 0); } else { clock_generator.set_ms_frequency(clock_generator_output_og_codec, frequency * 2, si5351_vco_f, 1); } +#endif } void ClockManager::set_reference_ppb(const int32_t ppb) { @@ -510,7 +718,15 @@ void ClockManager::start_frequency_monitor_measurement(const cgu::CLK_SEL clk_se void ClockManager::wait_For_frequency_monitor_measurement_done() { // FREQ_MON mechanism fails to finish if there's no clock present on selected input?! +#ifndef PRALINE while (LPC_CGU->FREQ_MON.MEAS == 1); +#else + // PRALINE FIX: Add timeout to prevent infinite hang + uint32_t timeout = 100000; + while (LPC_CGU->FREQ_MON.MEAS == 1 && timeout > 0) { + timeout--; + } +#endif } uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() { @@ -521,6 +737,35 @@ uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() { } void ClockManager::start_audio_pll() { +#ifdef PRALINE + /* PRALINE: Use 12MHz XTAL for audio PLL + * For 12MHz XTAL input, 48kHz audio rate, 256Fs MCLK: + * Fout=12.288MHz, Fcco=491.52MHz + * 12MHz * 1024 / 25 = 491.52MHz + * MSEL=1024, NSEL=25, PSEL=20 + */ + cgu::pll0audio::ctrl({ + .pd = 1, + .bypass = 0, + .directi = 0, + .directo = 0, + .clken = 0, + .frm = 0, + .autoblock = 1, + .pllfract_req = 0, + .sel_ext = 1, + .mod_pd = 1, + .clk_sel = cgu::CLK_SEL::XTAL, + }); + + cgu::pll0audio::mdiv({ + .mdec = 22625UL, // MDEC for MSEL=1024 + }); + cgu::pll0audio::np_div({ + .pdec = 31, // PSEL=20 + .ndec = 69, // NDEC for NSEL=25 + }); +#else cgu::pll0audio::ctrl({ .pd = 1, .bypass = 0, @@ -549,13 +794,24 @@ void ClockManager::start_audio_pll() { .pdec = 31, .ndec = 45, }); +#endif cgu::pll0audio::frac({ .pllfract_ctrl = 0, }); cgu::pll0audio::power_up(); +#ifndef PRALINE while (!cgu::pll0audio::is_locked()); +#else + // PRALINE FIX: Add timeout to prevent infinite hang if GP_CLKIN not present + { + uint32_t timeout = 100000; + while (!cgu::pll0audio::is_locked() && timeout > 0) { + timeout--; + } + } +#endif cgu::pll0audio::clock_enable(); set_base_audio_clock_divider(1); diff --git a/firmware/application/clock_manager.hpp b/firmware/application/clock_manager.hpp index 58d9944b9..f3e17d8d2 100644 --- a/firmware/application/clock_manager.hpp +++ b/firmware/application/clock_manager.hpp @@ -72,6 +72,13 @@ class ClockManager { void set_reference_ppb(const int32_t ppb); +#ifdef PRALINE + // Si5351 diagnostic methods + uint8_t si5351_read_status() { return clock_generator.device_status(); } + uint8_t si5351_read_register(uint8_t reg) { return clock_generator.read_register(reg); } + void si5351_write_register(uint8_t reg, uint8_t value) { clock_generator.write_register(reg, value); } +#endif + uint32_t get_frequency_monitor_measurement_in_hertz(); Reference get_reference() const; diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 3dc1cf906..dadbe2416 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -285,11 +285,6 @@ set(EXTCPPSRC external/siggen/main.cpp external/siggen/ui_siggen.cpp - #sdusb - external/sdusb/main.cpp - external/sdusb/ui_sd_over_usb.cpp - - #morse_radio external/morse_radio/main.cpp external/morse_radio/ui_morse_radio.cpp @@ -368,7 +363,16 @@ set(EXTAPPLIST flex_rx subcarrx siggen - sdusb morse_radio morseradiotx ) + +# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds +if(NOT BOARD STREQUAL "PRALINE") + list(APPEND EXTCPPSRC + external/sdusb/main.cpp + external/sdusb/ui_sd_over_usb.cpp + ) + list(APPEND EXTAPPLIST sdusb) +endif() + diff --git a/firmware/application/hw/max2831.cpp b/firmware/application/hw/max2831.cpp new file mode 100644 index 000000000..8a3a7a79f --- /dev/null +++ b/firmware/application/hw/max2831.cpp @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2025 Great Scott Gadgets + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +/* + * MAX2831 driver ported from GSG HackRF reference implementation (max2831.c). + * Adapted to work with Mayhem's MAX283x abstraction layer. + */ + +#ifdef PRALINE + +#include "max2831.hpp" + +#include "hackrf_hal.hpp" +#include "hackrf_gpio.hpp" +using namespace hackrf::one; + +#include "ch.h" +#include "hal.h" + +#include +#include + +namespace max2831 { + +using namespace max283x; + +/* + * MAX2831 uses 9-bit SPI transfers. + * An 18-bit word is sent as two 9-bit transfers: + * Word format: [VALUE:14][REG:4] + * First transfer: bits 17:9 (high 9 bits) + * Second transfer: bits 8:0 (low 9 bits) + * + * This matches the GSG reference implementation exactly. + */ +void MAX2831::write_reg(const uint8_t reg, const uint16_t value) { + uint32_t word = (((uint32_t)value & 0x3fff) << 4) | (reg & 0xf); + uint16_t values[2] = { + static_cast(word >> 9), + static_cast(word & 0x1ff)}; + _target.transfer(values, 2); +} + +void MAX2831::set_reg_field(const uint8_t reg, const uint16_t mask, const uint16_t value) { + _regs[reg] = (_regs[reg] & ~mask) | (value & mask); + mark_dirty(reg); +} + +uint16_t MAX2831::get_reg_field(const uint8_t reg, const uint16_t mask, const uint8_t shift) { + return (_regs[reg] & mask) >> shift; +} + +void MAX2831::mark_dirty(const uint8_t reg) { + _regs_dirty |= (1 << reg); +} + +void MAX2831::mark_clean(const uint8_t reg) { + _regs_dirty &= ~(1 << reg); +} + +void MAX2831::flush_reg(const uint8_t reg) { + write_reg(reg, _regs[reg]); + mark_clean(reg); +} + +void MAX2831::flush_dirty() { + for (size_t r = 0; r < reg_count; r++) { + if ((_regs_dirty >> r) & 0x1) { + flush_reg(r); + } + } +} + +void MAX2831::init() { + set_mode(Mode::Shutdown); + + /* Configure GPIO pins for MAX2831 control */ + gpio_max283x_enable.output(); + gpio_max2831_rx_enable.output(); + gpio_max2831_rxhp.output(); + gpio_max2831_rxhp.write(0); /* RXHP low = 100 Hz HPF (default) */ + + /* Reset to default register values */ + std::memcpy(_regs.data(), default_regs.data(), sizeof(_regs)); + _regs_dirty = 0xFFFF; + + /* Write default register values to chip */ + flush_dirty(); + + /* Use SPI control instead of B1-B7 pins for gain settings. + * This matches the GSG reference: max2831_setup() */ + set_reg_field(8, REG8_RXVGA_GAIN_SPI_EN, REG8_RXVGA_GAIN_SPI_EN); + set_reg_field(9, REG9_TXVGA_GAIN_SPI_EN, REG9_TXVGA_GAIN_SPI_EN); + + /* Set initial gains - matches GSG reference */ + set_reg_field(12, REG12_TXVGA_GAIN_MASK, 0x00); /* Minimum TX gain */ + set_reg_field(7, REG7_RX_HPF_SEL_MASK, REG7_RX_HPF_30KHZ); + set_reg_field(11, REG11_LNA_GAIN_MASK, REG11_LNA_GAIN_MAX); + set_reg_field(11, REG11_RXVGA_GAIN_MASK, 0x18); // Moderate RX VGA gain + + /* FORCE MAXIMUM GAIN FOR TESTING */ + // set_reg_field(11, REG11_RXVGA_GAIN_MASK, 0x1F); // 62 dB VGA = MAX + + /* Configure baseband filter for 8 MHz TX - matches GSG reference */ + set_reg_field(8, REG8_LPF_COARSE_MASK, REG8_RX_LPF_7_5M); + set_reg_field(7, REG7_RX_LPF_FINE_MASK, REG7_RX_LPF_FINE_100); + set_reg_field(7, REG7_TX_LPF_FINE_MASK, REG7_TX_LPF_FINE_100); + + /* Disable clock output */ + set_reg_field(14, REG14_CLKOUT_PIN_EN, 0); + + /* Write all modified registers */ + flush_dirty(); + + set_mode(Mode::Standby); +} + +void MAX2831::set_mode(const Mode mode) { + _mode = mode; + + /* + * MAX2831 mode control via ENABLE and RXTX pins. + * From GSG hackrf max2831_target.c: + * + * Shutdown: ENABLE=0, RXTX=0 + * Standby: ENABLE=0, RXTX=1 (PLL/VCO/LO on, ready for quick TX/RX) + * RX: ENABLE=1, RXTX=0 + * TX: ENABLE=1, RXTX=1 + * + * Note: gpio_max2831_rx_enable is the RXTX mode select pin. + * RXTX=0 selects RX, RXTX=1 selects TX. + */ + + /* Handle calibration mode bits if needed */ + bool tx_cal = (mode == Mode::Tx_Calibration); + bool rx_cal = (mode == Mode::Rx_Calibration); + + uint16_t current_tx_cal = get_reg_field(6, REG6_TX_CAL_MODE_EN, REG6_TX_CAL_MODE_EN_SHIFT); + uint16_t current_rx_cal = get_reg_field(6, REG6_RX_CAL_MODE_EN, REG6_RX_CAL_MODE_EN_SHIFT); + + if (current_tx_cal != (tx_cal ? 1 : 0)) { + set_reg_field(6, REG6_TX_CAL_MODE_EN, tx_cal ? REG6_TX_CAL_MODE_EN : 0); + flush_dirty(); + } + if (current_rx_cal != (rx_cal ? 1 : 0)) { + set_reg_field(6, REG6_RX_CAL_MODE_EN, rx_cal ? REG6_RX_CAL_MODE_EN : 0); + flush_dirty(); + } + + switch (mode) { + default: + case Mode::Shutdown: + gpio_max2831_rx_enable.write(0); /* RXTX=0 */ + gpio_max283x_enable.write(0); /* ENABLE=0 */ + break; + case Mode::Standby: + gpio_max2831_rx_enable.write(1); /* RXTX=1 */ + gpio_max283x_enable.write(0); /* ENABLE=0 */ + break; + case Mode::Transmit: + case Mode::Tx_Calibration: + gpio_max2831_rx_enable.write(1); /* RXTX=1 for TX */ + gpio_max283x_enable.write(1); /* ENABLE=1 */ + break; + case Mode::Receive: + case Mode::Rx_Calibration: + gpio_max2831_rx_enable.write(0); /* RXTX=0 for RX */ + gpio_max283x_enable.write(1); /* ENABLE=1 */ + break; + } + + /* Update LPF bandwidth for current mode */ + if (_desired_lpf_bw > 0) { + set_lpf_bandwidth_internal(_desired_lpf_bw); + } +} + +void MAX2831::set_tx_vga_gain(const int_fast8_t db) { + /* TX VGA gain: 0-31 dB in ~1 dB steps + * Register value: gain * 2 | 1, max 0x3F + * This matches GSG reference: max2831_set_txvga_gain() */ + int_fast8_t db_clipped = std::max(0, std::min(31, (int)db)); + uint16_t value = std::min((db_clipped << 1) | 1, 0x3f); + set_reg_field(12, REG12_TXVGA_GAIN_MASK, value); + flush_reg(12); +} + +void MAX2831::set_lna_gain(const int_fast8_t db) { + /* + * LNA gain has 3 settings (from GSG reference): + * MAX (33 dB), -16 dB from max (17 dB), -33 dB from max (0 dB) + * Map from MAX2837 8 dB steps for compatibility + */ + uint16_t gain_val; + if (db >= 32) { + gain_val = REG11_LNA_GAIN_MAX; + } else if (db >= 16) { + gain_val = REG11_LNA_GAIN_M16; + } else { + gain_val = REG11_LNA_GAIN_M33; + } + set_reg_field(11, REG11_LNA_GAIN_MASK, gain_val); + flush_reg(11); +} + +void MAX2831::set_vga_gain(const int_fast8_t db) { + /* VGA gain: 0-62 dB in 2 dB steps + * This matches GSG reference: max2831_set_vga_gain() */ + if ((db & 0x1) || db > 62) { + return; /* Invalid: must be even and <= 62 */ + } + int_fast8_t db_clipped = std::max(0, std::min(62, (int)db)); + uint16_t value = (db_clipped >> 1) & 0x1f; + set_reg_field(11, REG11_RXVGA_GAIN_MASK, value); + flush_reg(11); +} + +/* + * LPF bandwidth tables from GSG reference max2831.c + */ +struct lpf_ft_t { + uint32_t bandwidth_hz; + uint8_t ft; +}; + +struct lpf_ft_fine_t { + uint8_t percent; + uint8_t ft_fine; +}; + +/* Measured -0.5 dB complex baseband bandwidth for each register setting */ +static constexpr lpf_ft_t rx_lpf_ft[] = { + {11600000, REG8_RX_LPF_7_5M}, + {15100000, REG8_RX_LPF_8_5M}, + {22600000, REG8_RX_LPF_15M}, + {28300000, REG8_RX_LPF_18M}, + {0, 0}, +}; + +static constexpr lpf_ft_fine_t rx_lpf_ft_fine[] = { + {90, REG7_RX_LPF_FINE_90}, + {95, REG7_RX_LPF_FINE_95}, + {100, REG7_RX_LPF_FINE_100}, + {105, REG7_RX_LPF_FINE_105}, + {110, REG7_RX_LPF_FINE_110}, + {0, 0}, +}; + +static constexpr lpf_ft_t tx_lpf_ft[] = { + {11900000, REG8_TX_LPF_8M}, + {15800000, REG8_TX_LPF_11M}, + {23600000, REG8_TX_LPF_16_5M}, + {31300000, REG8_TX_LPF_22_5M}, + {0, 0}, +}; + +static constexpr lpf_ft_fine_t tx_lpf_ft_fine[] = { + {90, REG7_TX_LPF_FINE_90}, + {95, REG7_TX_LPF_FINE_95}, + {100, REG7_TX_LPF_FINE_100}, + {105, REG7_TX_LPF_FINE_105}, + {110, REG7_TX_LPF_FINE_110}, + {115, REG7_TX_LPF_FINE_115}, + {0, 0}, +}; + +uint32_t MAX2831::set_lpf_bandwidth_internal(const uint32_t bandwidth_hz) { + const lpf_ft_t* coarse; + const lpf_ft_fine_t* fine; + + if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) { + coarse = rx_lpf_ft; + fine = rx_lpf_ft_fine; + } else { + coarse = tx_lpf_ft; + fine = tx_lpf_ft_fine; + } + + /* Find coarse and fine settings for LPF - matches GSG reference */ + bool found = false; + const lpf_ft_fine_t* f = fine; + for (; coarse->bandwidth_hz != 0; coarse++) { + uint32_t coarse_aux = coarse->bandwidth_hz / 100; + for (f = fine; f->percent != 0; f++) { + if ((coarse_aux * f->percent) >= bandwidth_hz) { + found = true; + break; + } + } + if (found) break; + } + + /* Use the widest setting if a wider bandwidth than our maximum is requested */ + if (!found) { + coarse--; + f--; + } + + /* Program found settings */ + set_reg_field(8, REG8_LPF_COARSE_MASK, coarse->ft); + if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) { + set_reg_field(7, REG7_RX_LPF_FINE_MASK, f->ft_fine); + } else { + /* TX fine values are already shifted in the constants (REG7_TX_LPF_FINE_*) */ + set_reg_field(7, REG7_TX_LPF_FINE_MASK, f->ft_fine); + } + flush_dirty(); + + return coarse->bandwidth_hz * f->percent / 100; +} + +void MAX2831::set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) { + _desired_lpf_bw = bandwidth_minimum; + if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) { + set_lpf_bandwidth_internal(bandwidth_minimum); + } +} + +void MAX2831::set_lpf_rf_bandwidth_tx(const uint32_t bandwidth_minimum) { + _desired_lpf_bw = bandwidth_minimum; + if (_mode == Mode::Transmit || _mode == Mode::Tx_Calibration) { + set_lpf_bandwidth_internal(bandwidth_minimum); + } +} + +bool MAX2831::set_frequency(const rf::Frequency lo_frequency) { + /* + * MAX2831 frequency synthesis from GSG reference max2831_set_frequency(): + * F_LO = F_REF * (N + F/2^20) / R + * Where: + * F_REF = 40 MHz reference + * R = reference divider (1 or 2), we use R=2 + * N = integer divider (8 bits) + * F = fractional divider (20 bits) + * + * Using R=2: F_LO = 40M * (N + F/2^20) / 2 = 20M * (N + F/2^20) + */ + + /* MAX2831 supports 2.3-2.6 GHz */ + if (lo_frequency < 2300000000ULL || lo_frequency > 2600000000ULL) { + return false; + } + + /* From GSG reference: ASSUME 40MHz PLL. Ratio = F*R/40,000,000. + * TODO: fixed to R=2. Check if it's worth exploring R=1. */ + uint32_t freq = lo_frequency; + freq += (20000000 >> 21); /* Round to nearest frequency */ + uint32_t div_int = freq / 20000000; + uint32_t div_rem = freq % 20000000; + uint32_t div_frac = 0; + uint32_t div_cmp = 20000000; + + for (int i = 0; i < 20; i++) { + div_frac <<= 1; + div_rem <<= 1; + if (div_rem >= div_cmp) { + div_frac |= 0x1; + div_rem -= div_cmp; + } + } + + /* Write order matters - matches GSG reference */ + /* REG 3: SYN_INT (bits 7:0) and SYN_FRAC_LO (bits 13:8) */ + uint16_t reg3_val = (div_int & 0xFF) | ((div_frac & 0x3F) << 8); + _regs[3] = reg3_val; + mark_dirty(3); + + /* REG 4: SYN_FRAC_HI (bits 13:0) - upper 14 bits of 20-bit fractional */ + uint16_t reg4_val = (div_frac >> 6) & 0x3FFF; + _regs[4] = reg4_val; + mark_dirty(4); + + flush_dirty(); + + return true; +} + +void MAX2831::set_rx_LO_iq_phase_calibration(const size_t v) { + /* MAX2831 doesn't have the same IQ calibration as MAX2837 */ + (void)v; +} + +void MAX2831::set_tx_LO_iq_phase_calibration(const size_t v) { + /* MAX2831 doesn't have the same IQ calibration as MAX2837 */ + (void)v; +} + +void MAX2831::set_rx_buff_vcm(const size_t v) { + /* MAX2831 RX IQ common mode voltage is in register 15 + * Values: 0=1.1V, 1=1.2V, 2=1.3V, 3=1.45V */ + uint16_t vcm = std::min(v, (size_t)3) << REG15_RXIQ_VCM_SHIFT; + set_reg_field(15, REG15_RXIQ_VCM_MASK, vcm); + flush_reg(15); +} + +int8_t MAX2831::temp_sense() { + /* MAX2831 temperature sensor can be read via RSSI MUX. + * This would require: + * 1. Switch RSSI_MUX to temperature mode + * 2. Read the ADC + * 3. Switch back to RSSI mode + * For now, return a placeholder value. */ + return 25; /* Room temperature placeholder */ +} + +reg_t MAX2831::read(const address_t reg_num) { + /* MAX2831 doesn't support SPI read, return cached value */ + if (reg_num < reg_count) { + return _regs[reg_num]; + } + return 0; +} + +void MAX2831::write(const address_t reg_num, const reg_t value) { + if (reg_num < reg_count) { + _regs[reg_num] = value & 0x3FFF; /* 14-bit registers */ + write_reg(reg_num, _regs[reg_num]); + mark_clean(reg_num); + } +} + +} // namespace max2831 +#endif diff --git a/firmware/application/hw/max2831.hpp b/firmware/application/hw/max2831.hpp new file mode 100644 index 000000000..84eb97766 --- /dev/null +++ b/firmware/application/hw/max2831.hpp @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2025 Great Scott Gadgets + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +/* + * MAX2831 driver ported from GSG HackRF reference implementation. + * Register definitions match max2831_regs.def from hackrf firmware. + */ + +#ifndef __MAX2831_H__ +#define __MAX2831_H__ + +#include "max283x.hpp" +#include "gpio.hpp" +#include "spi_arbiter.hpp" + +#include +#include + +namespace max2831 { + +using namespace max283x; + +/* MAX2831 has 16 registers, each containing 14 bits of data */ +constexpr size_t reg_count = 16; + +/* Default register values from GSG HackRF reference (max2831.c) */ +constexpr std::array default_regs = { + 0x1740, /* 0: enable fractional mode (Table 16 recommends 0x0740, clearing unknown bit) */ + 0x119a, /* 1 */ + 0x1003, /* 2 */ + 0x0079, /* 3: PLL divider settings for 2437 MHz */ + 0x3666, /* 4: PLL divider settings for 2437 MHz */ + 0x00a4, /* 5: divide reference frequency by 2 */ + 0x0060, /* 6: enable TX power detector */ + 0x1022, /* 7: 110% TX LPF bandwidth */ + 0x2021, /* 8: pin control of RX gain, 11 MHz LPF bandwidth */ + 0x03b5, /* 9: pin control of TX gain */ + 0x1d80, /* 10: 3.5 us PA enable delay, zero PA bias */ + 0x0074, /* 11: LNA high gain, RX VGA moderate gain (Table 27 recommends 0x007f, maximum gain) */ + 0x0140, /* 12: TX VGA minimum */ + 0x0e92, /* 13 */ + 0x0100, /* 14: reference clock output disabled */ + 0x0145, /* 15: RX IQ common mode 1.1 V */ +}; + +/* + * Register bit field definitions from max2831_regs.def + * Format: REG__ + */ + +/* REG 0: PLL Mode */ +constexpr uint16_t REG0_PLL_MODE_SHIFT = 10; +constexpr uint16_t REG0_PLL_MODE_MASK = (1 << REG0_PLL_MODE_SHIFT); +constexpr uint16_t REG0_PLL_MODE_INTEGER = 0; +constexpr uint16_t REG0_PLL_MODE_FRACTIONAL = 1; + +/* REG 3: Synthesizer Integer and Fractional Low */ +constexpr uint16_t REG3_SYN_INT_SHIFT = 0; +constexpr uint16_t REG3_SYN_INT_MASK = 0x00FF; /* D7:D0 - Integer divider (8 bits) */ +constexpr uint16_t REG3_SYN_FRAC_LO_SHIFT = 8; +constexpr uint16_t REG3_SYN_FRAC_LO_MASK = 0x3F00; /* D13:D8 - Low 6 bits of fractional divider */ + +/* REG 4: Synthesizer Fractional High */ +constexpr uint16_t REG4_SYN_FRAC_HI_MASK = 0x3FFF; /* D13:D0 - High 14 bits of fractional divider */ + +/* REG 5: Reference Divider and Lock Detect */ +constexpr uint16_t REG5_SYN_REF_DIV_SHIFT = 2; +constexpr uint16_t REG5_SYN_REF_DIV_1 = (0 << REG5_SYN_REF_DIV_SHIFT); +constexpr uint16_t REG5_SYN_REF_DIV_2 = (1 << REG5_SYN_REF_DIV_SHIFT); + +/* REG 6: Calibration Mode */ +constexpr uint16_t REG6_RX_CAL_MODE_EN_SHIFT = 0; +constexpr uint16_t REG6_RX_CAL_MODE_EN = (1 << REG6_RX_CAL_MODE_EN_SHIFT); +constexpr uint16_t REG6_TX_CAL_MODE_EN_SHIFT = 1; +constexpr uint16_t REG6_TX_CAL_MODE_EN = (1 << REG6_TX_CAL_MODE_EN_SHIFT); +constexpr uint16_t REG6_TX_POWER_DETECT_EN_SHIFT = 6; +constexpr uint16_t REG6_TX_POWER_DETECT_EN = (1 << REG6_TX_POWER_DETECT_EN_SHIFT); + +/* REG 7: LPF Fine Adjustment and RX HPF */ +constexpr uint16_t REG7_RX_LPF_FINE_SHIFT = 0; +constexpr uint16_t REG7_RX_LPF_FINE_MASK = 0x0007; /* D2:D0 */ +constexpr uint16_t REG7_RX_LPF_FINE_90 = 0; +constexpr uint16_t REG7_RX_LPF_FINE_95 = 1; +constexpr uint16_t REG7_RX_LPF_FINE_100 = 2; +constexpr uint16_t REG7_RX_LPF_FINE_105 = 3; +constexpr uint16_t REG7_RX_LPF_FINE_110 = 4; + +constexpr uint16_t REG7_TX_LPF_FINE_SHIFT = 3; +constexpr uint16_t REG7_TX_LPF_FINE_MASK = 0x0038; /* D5:D3 */ +constexpr uint16_t REG7_TX_LPF_FINE_90 = (0 << REG7_TX_LPF_FINE_SHIFT); +constexpr uint16_t REG7_TX_LPF_FINE_95 = (1 << REG7_TX_LPF_FINE_SHIFT); +constexpr uint16_t REG7_TX_LPF_FINE_100 = (2 << REG7_TX_LPF_FINE_SHIFT); +constexpr uint16_t REG7_TX_LPF_FINE_105 = (3 << REG7_TX_LPF_FINE_SHIFT); +constexpr uint16_t REG7_TX_LPF_FINE_110 = (4 << REG7_TX_LPF_FINE_SHIFT); +constexpr uint16_t REG7_TX_LPF_FINE_115 = (5 << REG7_TX_LPF_FINE_SHIFT); + +constexpr uint16_t REG7_RX_HPF_SEL_SHIFT = 12; +constexpr uint16_t REG7_RX_HPF_SEL_MASK = 0x3000; /* D13:D12 */ +constexpr uint16_t REG7_RX_HPF_100HZ = (0 << REG7_RX_HPF_SEL_SHIFT); +constexpr uint16_t REG7_RX_HPF_4KHZ = (1 << REG7_RX_HPF_SEL_SHIFT); +constexpr uint16_t REG7_RX_HPF_30KHZ = (2 << REG7_RX_HPF_SEL_SHIFT); + +/* REG 8: LPF Coarse, RSSI MUX, and RX VGA SPI Enable */ +constexpr uint16_t REG8_LPF_COARSE_SHIFT = 0; +constexpr uint16_t REG8_LPF_COARSE_MASK = 0x0003; /* D1:D0 */ +/* RX and TX share the same coarse LPF setting bits */ +constexpr uint16_t REG8_RX_LPF_7_5M = 0; +constexpr uint16_t REG8_RX_LPF_8_5M = 1; +constexpr uint16_t REG8_RX_LPF_15M = 2; +constexpr uint16_t REG8_RX_LPF_18M = 3; +constexpr uint16_t REG8_TX_LPF_8M = 0; +constexpr uint16_t REG8_TX_LPF_11M = 1; +constexpr uint16_t REG8_TX_LPF_16_5M = 2; +constexpr uint16_t REG8_TX_LPF_22_5M = 3; + +constexpr uint16_t REG8_RSSI_MUX_SHIFT = 8; +constexpr uint16_t REG8_RSSI_MUX_MASK = 0x0300; /* D9:D8 */ +constexpr uint16_t REG8_RSSI_MUX_RSSI = (0 << REG8_RSSI_MUX_SHIFT); +constexpr uint16_t REG8_RSSI_MUX_TEMP = (1 << REG8_RSSI_MUX_SHIFT); +constexpr uint16_t REG8_RSSI_MUX_TX_POWER = (2 << REG8_RSSI_MUX_SHIFT); + +constexpr uint16_t REG8_RXVGA_GAIN_SPI_EN_SHIFT = 12; +constexpr uint16_t REG8_RXVGA_GAIN_SPI_EN = (1 << REG8_RXVGA_GAIN_SPI_EN_SHIFT); + +/* REG 9: TX VGA SPI Enable */ +constexpr uint16_t REG9_TXVGA_GAIN_SPI_EN_SHIFT = 10; +constexpr uint16_t REG9_TXVGA_GAIN_SPI_EN = (1 << REG9_TXVGA_GAIN_SPI_EN_SHIFT); + +/* REG 11: RX Gain */ +constexpr uint16_t REG11_RXVGA_GAIN_SHIFT = 0; +constexpr uint16_t REG11_RXVGA_GAIN_MASK = 0x001F; /* D4:D0 - 5 bits */ + +constexpr uint16_t REG11_LNA_GAIN_SHIFT = 5; +constexpr uint16_t REG11_LNA_GAIN_MASK = 0x0060; /* D6:D5 - 2 bits */ +constexpr uint16_t REG11_LNA_GAIN_M33 = (0 << REG11_LNA_GAIN_SHIFT); /* -33 dB from max (min) */ +constexpr uint16_t REG11_LNA_GAIN_M16 = (2 << REG11_LNA_GAIN_SHIFT); /* -16 dB from max */ +constexpr uint16_t REG11_LNA_GAIN_MAX = (3 << REG11_LNA_GAIN_SHIFT); /* Maximum LNA gain */ + +/* REG 12: TX VGA Gain */ +constexpr uint16_t REG12_TXVGA_GAIN_SHIFT = 0; +constexpr uint16_t REG12_TXVGA_GAIN_MASK = 0x003F; /* D5:D0 - 6 bits */ + +/* REG 14: Clock Output */ +constexpr uint16_t REG14_CLKOUT_PIN_EN_SHIFT = 9; +constexpr uint16_t REG14_CLKOUT_PIN_EN = (1 << REG14_CLKOUT_PIN_EN_SHIFT); + +/* REG 15: RX IQ Common Mode */ +constexpr uint16_t REG15_RXIQ_VCM_SHIFT = 10; +constexpr uint16_t REG15_RXIQ_VCM_MASK = 0x0C00; /* D11:D10 - 2 bits */ +constexpr uint16_t REG15_RXIQ_VCM_1_1 = (0 << REG15_RXIQ_VCM_SHIFT); /* 1.1V */ +constexpr uint16_t REG15_RXIQ_VCM_1_2 = (1 << REG15_RXIQ_VCM_SHIFT); /* 1.2V */ +constexpr uint16_t REG15_RXIQ_VCM_1_3 = (2 << REG15_RXIQ_VCM_SHIFT); /* 1.3V */ +constexpr uint16_t REG15_RXIQ_VCM_1_45 = (3 << REG15_RXIQ_VCM_SHIFT); /* 1.45V */ + +class MAX2831 : public MAX283x { + public: + constexpr MAX2831( + spi::arbiter::Target& target) + : _target(target) { + } + + void init() override; + void set_mode(const Mode mode) override; + + void set_tx_vga_gain(const int_fast8_t db) override; + void set_lna_gain(const int_fast8_t db) override; + void set_vga_gain(const int_fast8_t db) override; + void set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) override; + void set_lpf_rf_bandwidth_tx(const uint32_t bandwidth_minimum) override; + + bool set_frequency(const rf::Frequency lo_frequency) override; + + void set_rx_LO_iq_phase_calibration(const size_t v) override; + void set_tx_LO_iq_phase_calibration(const size_t v) override; + + void set_rx_buff_vcm(const size_t v) override; + + int8_t temp_sense() override; + + reg_t read(const address_t reg_num) override; + void write(const address_t reg_num, const reg_t value) override; + + private: + spi::arbiter::Target& _target; + Mode _mode{Mode::Standby}; + std::array _regs{default_regs}; + uint16_t _regs_dirty{0xFFFF}; /* Track which registers need to be written */ + uint32_t _desired_lpf_bw{0}; /* Desired LPF bandwidth in Hz */ + + void write_reg(const uint8_t reg, const uint16_t value); + void set_reg_field(const uint8_t reg, const uint16_t mask, const uint16_t value); + uint16_t get_reg_field(const uint8_t reg, const uint16_t mask, const uint8_t shift); + void mark_dirty(const uint8_t reg); + void mark_clean(const uint8_t reg); + void flush_reg(const uint8_t reg); + void flush_dirty(); + uint32_t set_lpf_bandwidth_internal(const uint32_t bandwidth_hz); +}; + +} // namespace max2831 + +#endif /*__MAX2831_H__*/ diff --git a/firmware/application/hw/rffc507x.cpp b/firmware/application/hw/rffc507x.cpp index 448187054..e2caa886d 100644 --- a/firmware/application/hw/rffc507x.cpp +++ b/firmware/application/hw/rffc507x.cpp @@ -31,6 +31,24 @@ using namespace hackrf::one; #include "hal.h" +#ifdef PRALINE +// Global debug tracking - visible to other files +struct rffc507x_debug_t { + uint32_t requested_freq_mhz; + uint32_t calculated_vco_mhz; + uint32_t expected_n; + uint8_t expected_lodiv; + uint8_t expected_presc; + bool was_called; + uint32_t calc_lo_freq_mhz; // Input to calculate() + uint32_t calc_vco_inside_mhz; // VCO calculated inside calculate() + uint8_t calc_lodiv_log2; // LO divider log2 + uint8_t calc_presc_log2; // Prescaler log2 + uint64_t calc_n_q24; // N in Q24 format before shift +}; +rffc507x_debug_t rffc507x_debug_info = {0, 0, 0, 0, 0, false, 0, 0, 0, 0, 0}; +#endif + namespace rffc507x { /* Empirical tests indicate no minimum reset pulse width, but the speed @@ -96,9 +114,31 @@ constexpr size_t divider_min = 1U << divider_log2_min; constexpr size_t divider_max = 1U << divider_log2_max; constexpr size_t divider_log2(const rf::Frequency vco_frequency) { +#ifdef PRALINE + // PRALINE FIX: Avoid N register overflow (9-bit max = 511) + // With 40 MHz reference: + // - For VCO=5400 MHz, presc=÷2: N = (5400×2)/40 = 270 ✓ + // - For VCO=5400 MHz, presc=÷4: N = (5400×4)/40 = 540 ✗ OVERFLOW! + // + // Maximum safe VCO for ÷4 prescaler: + // N_max = 511, so VCO_max = (511 × 40) / 4 = 5110 MHz + // + // Use ÷4 only if VCO < 5110 MHz AND VCO > 3200 MHz + // Use ÷2 for VCO >= 5110 MHz to avoid overflow + + constexpr rf::Frequency overflow_threshold = 5110000000ULL; // Max VCO for ÷4 + constexpr rf::Frequency min_presc4_freq = 3200000000ULL; // Min VCO for ÷4 + + if ((vco_frequency > min_presc4_freq) && (vco_frequency < overflow_threshold)) { + return divider_log2_max; // ÷4 prescaler + } else { + return divider_log2_min; // ÷2 prescaler + } +#else return (vco_frequency > (prescaler::divider_min * prescaler::max_frequency)) ? prescaler::divider_log2_max : prescaler::divider_log2_min; +#endif } } /* namespace prescaler */ @@ -121,9 +161,22 @@ struct SynthConfig { const size_t prescaler_divider_log2 = prescaler::divider_log2(vco_frequency); +#ifndef PRALINE const uint64_t prescaled_lo_q24 = vco_frequency << (24 - prescaler_divider_log2); +#else + const uint64_t prescaled_lo_q24 = vco_frequency << (24 + prescaler_divider_log2); +#endif const uint64_t n_divider_q24 = prescaled_lo_q24 / reference_frequency; +#ifdef PRALINE + // DEBUG: Track everything + rffc507x_debug_info.calc_lo_freq_mhz = lo_frequency / 1000000; + rffc507x_debug_info.calc_vco_inside_mhz = vco_frequency / 1000000; + rffc507x_debug_info.calc_lodiv_log2 = lo_divider_log2; + rffc507x_debug_info.calc_presc_log2 = prescaler_divider_log2; + rffc507x_debug_info.calc_n_q24 = n_divider_q24; +#endif + return { lo_divider_log2, prescaler_divider_log2, @@ -233,6 +286,20 @@ void RFFC507x::set_mixer_current(const uint8_t value) { void RFFC507x::set_frequency(const rf::Frequency lo_frequency) { const SynthConfig synth_config = SynthConfig::calculate(lo_frequency); +#ifdef PRALINE + // Calculate VCO frequency from LO frequency and divider + const size_t lo_divider = 1U << synth_config.lo_divider_log2; // 2^lodiv_log2 + const rf::Frequency vco_freq = lo_frequency * lo_divider; + + // Track what we calculated + rffc507x_debug_info.requested_freq_mhz = lo_frequency / 1000000; + rffc507x_debug_info.calculated_vco_mhz = vco_freq / 1000000; + rffc507x_debug_info.expected_n = synth_config.n_divider_q24 >> 24; + rffc507x_debug_info.expected_lodiv = synth_config.lo_divider_log2; + rffc507x_debug_info.expected_presc = synth_config.prescaler_divider_log2; + rffc507x_debug_info.was_called = true; +#endif + /* Boost charge pump leakage if VCO frequency > 3.2GHz, indicated by * prescaler divider set to 4 (log2=2) instead of 2 (log2=1). */ diff --git a/firmware/application/hw/si5351.cpp b/firmware/application/hw/si5351.cpp index 236150aef..10c0aa8dd 100644 --- a/firmware/application/hw/si5351.cpp +++ b/firmware/application/hw/si5351.cpp @@ -57,7 +57,15 @@ void Si5351::reset() { write_register(Register::CrystalInternalLoadCapacitance, 0b11010010); write_register(Register::FanoutEnable, 0x00); +#ifndef PRALINE reset_plls(); +#endif + // NOTE: Do NOT call reset_plls() here! + // Multisynth registers are not yet configured at this point. + // Resetting PLLs with power-on default multisynth values (divider=6) + // causes Si5351 to output wrong frequencies (66.666 MHz instead of 8 MHz). + // The PLL reset should happen in init_clock_generator() AFTER multisynths + // are properly configured. This matches HackRF reference firmware. } Si5351::regvalue_t Si5351::read_register(const uint8_t reg) { @@ -93,7 +101,16 @@ void Si5351::set_ms_frequency( .r_div = r_div, }; const auto regs = ms.reg(ms_number); + +#ifdef PRALINE + /* PRALINE: Use single-byte writes - multi-byte I2C writes seem to fail */ + const uint8_t base_reg = regs[0]; + for (size_t i = 1; i < regs.size(); i++) { + write_register(base_reg + i - 1, regs[i]); + } +#else write(regs); +#endif } } /* namespace si5351 */ diff --git a/firmware/application/hw/si5351.hpp b/firmware/application/hw/si5351.hpp index 1768a8a2f..061e7dcec 100644 --- a/firmware/application/hw/si5351.hpp +++ b/firmware/application/hw/si5351.hpp @@ -361,7 +361,16 @@ class Si5351 { } void wait_for_device_ready() { +#ifndef PRALINE while (device_status() & 0x80); +#else + // Add timeout to prevent infinite loop if I2C communication fails + // (e.g., on PRALINE hardware with different configuration) + uint32_t timeout = 100000; + while ((device_status() & 0x80) && (timeout > 0)) { + timeout--; + } +#endif } bool plla_loss_of_signal() { @@ -377,10 +386,15 @@ class Si5351 { } void reset_plls() { +#ifndef PRALINE // Datasheet recommends value 0xac, though the low nibble bits are not defined in AN619. write_register(Register::PLLReset, 0xac); +#else + // Reset both PLLA and PLLB. Use 0xA0 to match HackRF reference firmware. + // The low nibble bits are reserved/undefined in AN619. + write_register(Register::PLLReset, 0xa0); +#endif } - regvalue_t read_register(const uint8_t reg); template @@ -397,6 +411,18 @@ class Si5351 { write(config.reg(ms_number)); } +#ifdef PRALINE + /* Write multisynth config using single-byte writes for debugging */ + void write_ms_single_byte(const size_t ms_number, const MultisynthFractional& config) { + const auto regs = config.reg(ms_number); + // regs[0] is the base register address, regs[1-8] are the data bytes + const uint8_t base_reg = regs[0]; + for (size_t i = 1; i < regs.size(); i++) { + write_register(base_reg + i - 1, regs[i]); + } + } +#endif + void set_ms_frequency( const size_t ms_number, const uint32_t frequency, @@ -480,7 +506,6 @@ class Si5351 { }}); } }; - } // namespace si5351 #endif /*__SI5351_H__*/ diff --git a/firmware/application/hw/spi_arbiter.hpp b/firmware/application/hw/spi_arbiter.hpp index dd0c2a394..1e31c35dd 100644 --- a/firmware/application/hw/spi_arbiter.hpp +++ b/firmware/application/hw/spi_arbiter.hpp @@ -46,6 +46,14 @@ class Arbiter { _bus.transfer(data, count); } +#ifdef PRALINE + /* Invalidate cached config - forces reconfiguration on next transfer. + * Call this after directly manipulating SSP registers (e.g., FPGA access). */ + void invalidate() { + _config = nullptr; + } +#endif + private: SPI& _bus; const SPIConfig* _config; diff --git a/firmware/application/irq_controls.cpp b/firmware/application/irq_controls.cpp index 668b6c6a3..df5bbed47 100644 --- a/firmware/application/irq_controls.cpp +++ b/firmware/application/irq_controls.cpp @@ -194,8 +194,12 @@ void timer0_callback(GPTDriver* const) { if (encoder_update(switches_raw) || encoder_read()) event_mask |= EVT_MASK_ENCODER; - /* Signal event loop */ + /* Signal event loop */ +#ifdef PRALINE + if (event_mask && thread_controls_event) { +#else if (event_mask) { +#endif chSysLockFromIsr(); chEvtSignalI(thread_controls_event, event_mask); chSysUnlockFromIsr(); @@ -227,8 +231,13 @@ void controls_init() { gptStart(&GPTD1, &timer0_config); gptStartContinuous(&GPTD1, timer0_match_count); +#ifdef PRALINE + // Enable repeat for directional switches only + for (auto i = Switch::Right; i <= Switch::Up; incr(i)) +#else // Enable repeat for directional and Select switches only for (auto i = Switch::Right; i <= Switch::Sel; incr(i)) +#endif switch_debounce[toUType(i)].enable_repeat(); } diff --git a/firmware/application/irq_lcd_frame.cpp b/firmware/application/irq_lcd_frame.cpp index 86e11b6d8..689430558 100644 --- a/firmware/application/irq_lcd_frame.cpp +++ b/firmware/application/irq_lcd_frame.cpp @@ -52,10 +52,17 @@ extern "C" { CH_IRQ_HANDLER(PIN_INT4_IRQHandler) { CH_IRQ_PROLOGUE(); - +#ifdef PRALINE + if (thread_lcd_frame_event) { + chSysLockFromIsr(); + chEvtSignalI(thread_lcd_frame_event, EVT_MASK_LCD_FRAME_SYNC); + chSysUnlockFromIsr(); + } +#else chSysLockFromIsr(); chEvtSignalI(thread_lcd_frame_event, EVT_MASK_LCD_FRAME_SYNC); chSysUnlockFromIsr(); +#endif LPC_GPIO_INT->IST = (1U << 4); diff --git a/firmware/application/irq_rtc.cpp b/firmware/application/irq_rtc.cpp index d48ea065a..5fa44b090 100644 --- a/firmware/application/irq_rtc.cpp +++ b/firmware/application/irq_rtc.cpp @@ -40,10 +40,17 @@ extern "C" { CH_IRQ_HANDLER(RTC_IRQHandler) { CH_IRQ_PROLOGUE(); - +#ifdef PRALINE + if (thread_rtc_event) { + chSysLockFromIsr(); + chEvtSignalI(thread_rtc_event, EVT_MASK_RTC_TICK); + chSysUnlockFromIsr(); + } +#else chSysLockFromIsr(); chEvtSignalI(thread_rtc_event, EVT_MASK_RTC_TICK); chSysUnlockFromIsr(); +#endif rtc::interrupt::clear_all(); diff --git a/firmware/application/main.cpp b/firmware/application/main.cpp index c1ca5b5c5..a178801ba 100755 --- a/firmware/application/main.cpp +++ b/firmware/application/main.cpp @@ -187,7 +187,7 @@ int main(void) { lcd_frame_sync_configure(); rtc_interrupt_enable(); - Theme::SetTheme((Theme::ThemeId)portapack::persistent_memory::ui_theme_id()); // load theme + Theme::SetTheme((Theme::ThemeId)portapack::persistent_memory::ui_theme_id()); event_loop(); diff --git a/firmware/application/portapack.cpp b/firmware/application/portapack.cpp index 4473a29b8..aa8bf100d 100644 --- a/firmware/application/portapack.cpp +++ b/firmware/application/portapack.cpp @@ -56,9 +56,11 @@ using asahi_kasei::ak4951::AK4951; #include "i2cdevmanager.hpp" #include "battery.hpp" +#ifndef PRALINE extern "C" { #include "platform_detect.h" } +#endif namespace portapack { @@ -341,14 +343,37 @@ static void set_cpu_clock_speed() { */ /* Step into the 90-110MHz M4 clock range */ +#ifdef PRALINE + /* PRALINE: Enable and use 12MHz XTAL directly (no GP_CLKIN from Si5351) */ + + /* Step 1: Enable the crystal oscillator */ + LPC_CGU->XTAL_OSC_CTRL.ENABLE = 0; // 0 = enable (active low) + LPC_CGU->XTAL_OSC_CTRL.HF = 0; // 0 = low frequency mode (1-20MHz) + + /* Step 2: Wait for oscillator to stabilize (~250us at IRC speed) */ + volatile uint32_t delay = 3000; // ~250us at 12MHz IRC + while (delay--); + + /* Step 3: Configure PLL1 from XTAL + * Fclkin = 12M, /N=1 = 12M, Fcco = 12M * 17 = 204M + * Fclk = Fcco / (2*(P=1)) = 102M + */ + cgu::pll1::ctrl({ + .pd = 1, + .bypass = 0, + .fbsel = 0, + .direct = 0, + .psel = 0, + .autoblock = 1, + .nsel = 0, // N = 1 + .msel = 16, // M = 17, so 12MHz * 17 = 204MHz + .clk_sel = cgu::CLK_SEL::XTAL, + }); +#else /* OG: - * Fclkin = 40M - * /N=2 = 20M = PFDin - * Fcco = PFDin * (M=10) = 200M + * Fclkin = 40M, /N=2 = 20M, Fcco = 20M * 10 = 200M * r9: - * Fclkin = 10M - * /N=1 = 10M = PFDin - * Fcco = PFDin * (M=20) = 200M + * Fclkin = 10M, /N=1 = 10M, Fcco = 10M * 20 = 200M * Fclk = Fcco / (2*(P=1)) = 100M */ cgu::pll1::ctrl({ @@ -362,20 +387,41 @@ static void set_cpu_clock_speed() { .msel = hackrf_r9 ? 19UL : 9UL, .clk_sel = cgu::CLK_SEL::GP_CLKIN, }); +#endif cgu::pll1::enable(); +#ifndef PRALINE while (!cgu::pll1::is_locked()); set_clock_config(clock_config_pll1_step); - /* Delay >50us at 90-110MHz clock speed */ volatile uint32_t delay = 1400; while (delay--); - set_clock_config(clock_config_pll1); /* Remove /2P divider from PLL1 output to achieve full speed */ cgu::pll1::direct(); +#else + // Wait for PLL1 to lock with timeout + { + uint32_t timeout = 100000; + while (!cgu::pll1::is_locked() && timeout > 0) { + timeout--; + } + } + + if (cgu::pll1::is_locked()) { + set_clock_config(clock_config_pll1_step); + + /* Delay >50us at 90-110MHz clock speed */ + volatile uint32_t delay = 1400; + while (delay--); + set_clock_config(clock_config_pll1); + + /* Remove /2P divider from PLL1 output to achieve full speed */ + cgu::pll1::direct(); + } +#endif } static void draw_splash_screen_icon(int16_t n, const ui::Bitmap& bitmap) { @@ -509,10 +555,12 @@ init_status_t init() { chThdSleepMilliseconds(100); +#ifndef PRALINE detect_hardware_platform(); finalize_detect_hardware_platform(); chThdSleepMilliseconds(100); +#endif configure_pins_portapack(); @@ -543,11 +591,11 @@ init_status_t init() { cgu::pll1::disable(); set_cpu_clock_speed(); - /* sample max: 1023 sample_t AKA uint16_t * touch_sensitivity: range: 1 to 128 * threshold range: 1023/1 to 1023/128 = 1023 to 8 */ + touch_threshold = portapack::persistent_memory::touchscreen_threshold(); if (lcd_fast_setup) @@ -570,8 +618,10 @@ init_status_t init() { chThdSleepMilliseconds(10); clock_manager.set_reference_ppb(persistent_memory::correction_ppb()); + clock_manager.enable_if_clocks(); clock_manager.enable_codec_clocks(); + radio::init(); sdcStart(&SDCD1, nullptr); @@ -583,12 +633,18 @@ init_status_t init() { draw_splash_screen_icon(2, ui::bitmap_icon_sd); init_status_t return_code = init_status_t::INIT_SUCCESS; +#ifndef PRALINE + // HackRF One uses CPLD - load it via JTAG if (!hackrf::cpld::load_sram()) { if (lcd_fast_setup) chDbgPanic("HACKRF CPLD FAILED"); return_code = init_status_t::INIT_HACKRF_CPLD_FAILED; } +#else + // HackRF Pro (PRALINE) uses FPGA - already loaded in board.cpp __early_init() + // via fpga_bridge_init(), so nothing to do here +#endif if (lcd_fast_setup) draw_splash_screen_icon(3, ui::bitmap_icon_hackrf); @@ -601,6 +657,7 @@ init_status_t init() { chThdSleepMilliseconds(10); audio::init(portapack_audio_codec()); + battery::BatteryManagement::set_calc_override(persistent_memory::ui_override_batt_calc()); i2cdev::I2CDevManager::init(); diff --git a/firmware/application/radio.cpp b/firmware/application/radio.cpp index ea96050a1..f57a02d36 100644 --- a/firmware/application/radio.cpp +++ b/firmware/application/radio.cpp @@ -26,6 +26,14 @@ #include "rffc507x.hpp" #include "max2837.hpp" #include "max2839.hpp" + +#ifdef PRALINE +#include "max2831.hpp" +extern "C" { +#include "fpga_bridge.h" +} +#endif + #include "max5864.hpp" #include "baseband_cpld.hpp" @@ -42,6 +50,10 @@ using namespace hackrf::one; #include "portapack.hpp" #include "portapack_persistent_memory.hpp" +#include "hal.h" // For LPC_SGPIO + +#include + /* Direct access to the radio. Setting values incorrectly can damage * the device. Applications should use ReceiverModel or TransmitterModel * instead of calling these functions directly. */ @@ -56,6 +68,18 @@ static constexpr uint32_t ssp_scr( return static_cast(pclk_f / cpsr / spi_f - 1); } +#ifdef PRALINE +/* MAX2831 uses 9-bit SPI transfers */ +static constexpr SPIConfig ssp_config_max283x = { + .end_cb = NULL, + .ssport = gpio_max283x_select.port(), + .sspad = gpio_max283x_select.pad(), + .cr0 = + CR0_CLOCKRATE(ssp_scr(ssp1_pclk_f, ssp1_cpsr, max283x_spi_f) + 3) | CR0_FRFSPI | CR0_DSS9BIT, + .cpsr = ssp1_cpsr, +}; +#else +/* MAX2837/MAX2839 use 16-bit SPI transfers */ static constexpr SPIConfig ssp_config_max283x = { .end_cb = NULL, .ssport = gpio_max283x_select.port(), @@ -64,6 +88,7 @@ static constexpr SPIConfig ssp_config_max283x = { CR0_CLOCKRATE(ssp_scr(ssp1_pclk_f, ssp1_cpsr, max283x_spi_f) + 3) | CR0_FRFSPI | CR0_DSS16BIT, .cpsr = ssp1_cpsr, }; +#endif static constexpr SPIConfig ssp_config_max5864 = { .end_cb = NULL, @@ -89,6 +114,9 @@ rffc507x::RFFC507x first_if; max283x::MAX283x* second_if; max2837::MAX2837 second_if_max2837{ssp1_target_max283x}; max2839::MAX2839 second_if_max2839{ssp1_target_max283x}; +#ifdef PRALINE +max2831::MAX2831 second_if_max2831{ssp1_target_max283x}; +#endif static max5864::MAX5864 baseband_codec{ssp1_target_max5864}; static baseband::CPLD baseband_cpld; @@ -97,19 +125,46 @@ static rf::Direction direction{rf::Direction::Receive}; static bool baseband_invert = false; static bool mixer_invert = false; +#ifdef PRALINE +static rf::Direction cached_direction = rf::Direction::Receive; +static bool cached_rf_amp = false; +static int_fast8_t cached_lna_gain = 0; +static int_fast8_t cached_vga_gain = 0; +#endif + void init() { +#ifdef PRALINE + /* PRALINE uses MAX2831 transceiver */ + second_if = (max283x::MAX283x*)&second_if_max2831; +#else if (hackrf_r9) { gpio_r9_not_ant_pwr.write(1); gpio_r9_not_ant_pwr.output(); } - rf_path.init(); - first_if.init(); second_if = hackrf_r9 ? (max283x::MAX283x*)&second_if_max2839 : (max283x::MAX283x*)&second_if_max2837; +#endif + rf_path.init(); + first_if.init(); second_if->init(); baseband_codec.init(); +#ifndef PRALINE + /* HackRF One uses CPLD for Q inversion control. + * PRALINE uses FPGA and the pin (P2_3) is used for LCD_TE on H4M. */ baseband_cpld.init(); +#else + /* Initialize FPGA registers - DC_BLOCK must be enabled for RX */ + // debug::fpga::init(); + fpga_debug_register_write(1, 0x01); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0 + fpga_debug_register_write(2, 0x00); // RX_DECIM=0 (no decimation for testing) + fpga_debug_register_write(3, 0x00); // TX_CTRL=0 + fpga_debug_register_write(4, 0x00); // TX_INTRP=0 + fpga_debug_register_write(5, 0x00); // TX_PSTEP=0 + + ssp1_arbiter.invalidate(); + chThdSleepMilliseconds(10); // Let FPGA registers settle +#endif } void set_direction(const rf::Direction new_direction) { @@ -120,6 +175,10 @@ void set_direction(const rf::Direction new_direction) { // Deleting that load_sram_no_verify() (or the original , load_sram() ), solves random TX swap I/Q problem in H1R1 , others OK- (and no side effects to all). // hackrf::cpld::load_sram_no_verify(); // After commit "removed the use of the hackrf cpld eeprom #1732", in a H1R1, Mic App wrong SSB TX with random USB/LSB change. +#ifdef PRALINE + cached_direction = new_direction; // Track state for debug and potentially other purposes. +#endif + direction = new_direction; if (hackrf_r9) { @@ -143,7 +202,18 @@ void set_direction(const rf::Direction new_direction) { */ baseband_invert = false; } +#ifndef PRALINE baseband_cpld.set_invert(mixer_invert ^ baseband_invert); +#else + // Praline: Control Q inversion via FPGA register + // Assuming register 1 bit 1 controls Q inversion + uint8_t ctrl_reg = 0x01; // DC_BLOCK enabled + if (mixer_invert ^ baseband_invert) { + ctrl_reg |= 0x02; // Set Q_INVERT bit + } + fpga_debug_register_write(1, ctrl_reg); + ssp1_arbiter.invalidate(); +#endif second_if->set_mode((direction == rf::Direction::Transmit) ? max283x::Mode::Transmit : max283x::Mode::Receive); rf_path.set_direction(direction); @@ -154,6 +224,16 @@ void set_direction(const rf::Direction new_direction) { led_rx.on(); else led_tx.on(); + + // #ifdef PRALINE + // Try with Q inversion OFF + // fpga_debug_register_write(1, 0x01); // DC_BLOCK=1, Q_INVERT=0 + // ssp1_arbiter.invalidate(); + + // If no signals, try with Q inversion ON + // fpga_debug_register_write(1, 0x03); // DC_BLOCK=1, Q_INVERT=1 + // ssp1_arbiter.invalidate(); + // #endif } bool set_tuning_frequency(const rf::Frequency frequency) { @@ -196,7 +276,9 @@ bool set_tuning_frequency(const rf::Frequency frequency) { rf_path.set_band(tuning_config.rf_path_band); mixer_invert = tuning_config.mixer_invert; +#ifndef PRALINE baseband_cpld.set_invert(mixer_invert ^ baseband_invert); +#endif return result_second_if; } else { @@ -205,14 +287,23 @@ bool set_tuning_frequency(const rf::Frequency frequency) { } void set_rf_amp(const bool rf_amp) { +#ifdef PRALINE + cached_rf_amp = rf_amp; // Track state for debug and potentialy other purposes. +#endif rf_path.set_rf_amp(rf_amp); } void set_lna_gain(const int_fast8_t db) { +#ifdef PRALINE + cached_lna_gain = db; // Track state for debug and potentially other purposes. +#endif second_if->set_lna_gain(db); } void set_vga_gain(const int_fast8_t db) { +#ifdef PRALINE + cached_vga_gain = db; // Track state for debug and potentially other purposes. +#endif second_if->set_vga_gain(db); } @@ -235,11 +326,17 @@ void set_baseband_rate(const uint32_t rate) { void set_antenna_bias(const bool on) { /* Pull MOSFET gate low to turn on antenna bias. */ +#ifdef PRALINE + // Praline: P2_12 = GPIO1[12], ANT_BIAS_EN_N (active LOW) + LPC_GPIO->CLR[1] = on ? (1 << 12) : 0; + LPC_GPIO->SET[1] = on ? 0 : (1 << 12); +#else if (hackrf_r9) { gpio_r9_not_ant_pwr.write(on ? 0 : 1); } else { first_if.set_gpo1(on ? 0 : 1); } +#endif } void set_tx_max283x_iq_phase_calibration(const size_t v) { @@ -275,8 +372,32 @@ void disable() { led_tx.off(); } +#ifdef PRALINE +void invalidate_spi_config() { + ssp1_arbiter.invalidate(); +} +#endif + namespace debug { +#ifdef PRALINE +rf::Direction get_cached_direction() { + return cached_direction; +} + +bool get_cached_rf_amp() { + return cached_rf_amp; +} + +int_fast8_t get_cached_lna_gain() { + return cached_lna_gain; +} + +int_fast8_t get_cached_vga_gain() { + return cached_vga_gain; +} +#endif + namespace first_if { uint32_t register_read(const size_t register_number) { @@ -287,6 +408,48 @@ void register_write(const size_t register_number, uint32_t value) { radio::first_if.write(register_number, value); } +#ifdef PRALINE +extern "C" { +extern struct rffc507x_debug_t { + uint32_t requested_freq_mhz; + uint32_t calculated_vco_mhz; + uint32_t expected_n; + uint8_t expected_lodiv; + uint8_t expected_presc; + bool was_called; + uint32_t calc_lo_freq_mhz; + uint32_t calc_vco_inside_mhz; + uint8_t calc_lodiv_log2; + uint8_t calc_presc_log2; + uint64_t calc_n_q24; +} rffc507x_debug_info; +} + +/*struct TuningInfo { + uint32_t requested_freq_mhz; + uint32_t expected_n; + uint8_t expected_lodiv; + uint8_t expected_presc; + bool was_called; +};*/ + +TuningInfo get_tuning_info() { + return { + rffc507x_debug_info.requested_freq_mhz, + rffc507x_debug_info.calculated_vco_mhz, + rffc507x_debug_info.expected_n, + rffc507x_debug_info.expected_lodiv, + rffc507x_debug_info.expected_presc, + rffc507x_debug_info.was_called, + rffc507x_debug_info.calc_lo_freq_mhz, + rffc507x_debug_info.calc_vco_inside_mhz, + rffc507x_debug_info.calc_lodiv_log2, + rffc507x_debug_info.calc_presc_log2, + rffc507x_debug_info.calc_n_q24, + }; +} +#endif + } /* namespace first_if */ namespace second_if { @@ -305,6 +468,73 @@ int8_t temp_sense() { } /* namespace second_if */ +#ifdef PRALINE +namespace fpga { + +/* Use fpga_bridge.c functions for FPGA register access. + * These properly switch SPI mode between iCE40 (Mode 3, 8-bit) + * and MAX2831 (Mode 0, 9-bit). After each access, we must + * invalidate the SPI arbiter's cached config since fpga_bridge.c + * modifies SSP1 registers directly. */ + +uint32_t register_read(const size_t register_number) { + uint32_t result = fpga_debug_register_read(static_cast(register_number)); + ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer + return result; +} + +void register_write(const size_t register_number, uint32_t value) { + fpga_debug_register_write(static_cast(register_number), static_cast(value)); + ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer +} + +void init() { + // Initialize FPGA registers after bitstream load + // DC_BLOCK (bit 0) must be enabled for RX to work + fpga_debug_register_write(1, 0x01); // CTRL: DC_BLOCK=1 + fpga_debug_register_write(2, 0x00); // RX_DECIM: no decimation + fpga_debug_register_write(3, 0x00); // TX_CTRL: NCO disabled + fpga_debug_register_write(4, 0x00); // TX_INTRP: no interpolation + fpga_debug_register_write(5, 0x00); // TX_PSTEP: zero phase step + ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer +} + +} /* namespace fpga */ +#endif + +namespace sgpio { + +/* SGPIO register map for debug viewing + * We expose key registers for diagnosing data flow issues. + * Register numbers map to: + * 0: CTRL_ENABLE - Which slices are enabled + * 1: GPIO_INREG - GPIO input register (data pins state) + * 2: GPIO_OUTREG - GPIO output register (direction, disable, etc) + * 3: GPIO_OENREG - GPIO output enable register + * 4: STATUS_1 - Exchange interrupt status (slice A = bit 0) + * 5: REG_SS[0] - Shadow register slice A (current sample data) + */ +uint32_t register_read(const size_t register_number) { + switch (register_number) { + case 0: + return LPC_SGPIO->CTRL_ENABLE; + case 1: + return LPC_SGPIO->GPIO_INREG; + case 2: + return LPC_SGPIO->GPIO_OUTREG; + case 3: + return LPC_SGPIO->GPIO_OENREG; + case 4: + return LPC_SGPIO->STATUS_1; + case 5: + return LPC_SGPIO->REG_SS[0]; + default: + return 0xFFFFFFFF; + } +} + +} /* namespace sgpio */ + } /* namespace debug */ } /* namespace radio */ diff --git a/firmware/application/radio.hpp b/firmware/application/radio.hpp index 560653497..a24985f77 100644 --- a/firmware/application/radio.hpp +++ b/firmware/application/radio.hpp @@ -62,6 +62,10 @@ void set_rx_max283x_iq_phase_calibration(const size_t v); // void configure(Configuration configuration); void disable(); +#ifdef PRALINE +void invalidate_spi_config(); +#endif + namespace debug { namespace first_if { @@ -69,6 +73,24 @@ namespace first_if { uint32_t register_read(const size_t register_number); void register_write(const size_t register_number, uint32_t value); +#ifdef PRALINE +struct TuningInfo { + uint32_t requested_freq_mhz; + uint32_t calculated_vco_mhz; + uint32_t expected_n; + uint8_t expected_lodiv; + uint8_t expected_presc; + bool was_called; + uint32_t calc_lo_freq_mhz; + uint32_t calc_vco_inside_mhz; + uint8_t calc_lodiv_log2; + uint8_t calc_presc_log2; + uint64_t calc_n_q24; +}; + +TuningInfo get_tuning_info(); +#endif + } /* namespace first_if */ namespace second_if { @@ -81,6 +103,28 @@ int8_t temp_sense(); } /* namespace second_if */ +#ifdef PRALINE +namespace fpga { + +uint32_t register_read(const size_t register_number); +void register_write(const size_t register_number, uint32_t value); +void init(); + +} /* namespace fpga */ + +/* State tracking - GPIO pins are write-only so we cache last known state */ +rf::Direction get_cached_direction(); +bool get_cached_rf_amp(); +int_fast8_t get_cached_lna_gain(); +int_fast8_t get_cached_vga_gain(); +#endif + +namespace sgpio { + +uint32_t register_read(const size_t register_number); + +} /* namespace sgpio */ + } /* namespace debug */ } /* namespace radio */ diff --git a/firmware/application/receiver_model.cpp b/firmware/application/receiver_model.cpp index c96ef6f23..c335d0c46 100644 --- a/firmware/application/receiver_model.cpp +++ b/firmware/application/receiver_model.cpp @@ -34,6 +34,12 @@ #include "dsp_iir_config.hpp" #include "utility.hpp" +#ifdef PRALINE +extern "C" { +#include "fpga_bridge.h" +} +#endif + using namespace hackrf::one; using namespace portapack; @@ -303,8 +309,20 @@ int32_t ReceiverModel::tuning_offset() { void ReceiverModel::update_tuning_frequency() { // TODO: use positive offset if freq < offset. - if (enabled_) + if (enabled_) { radio::set_tuning_frequency(target_frequency() + hidden_offset + tuning_offset()); + +#ifdef PRALINE + /* Praline: Must re-apply baseband filter after frequency change + * Reference: hackrf_usb radio.c radio_set_frequency() + * + * Different frequency ranges may use different quarter-shift modes, + * which affects the required LPF bandwidth. For now we just + * recalculate the filter to be safe. + */ + update_baseband_bandwidth(); +#endif + } } void ReceiverModel::set_hidden_offset(rf::Frequency offset) { @@ -313,8 +331,28 @@ void ReceiverModel::set_hidden_offset(rf::Frequency offset) { } void ReceiverModel::update_baseband_bandwidth() { - if (enabled_) + if (enabled_) { +#ifdef PRALINE + /* Praline: LPF bandwidth calculation + * Reference: hackrf_usb radio.c radio_set_filter() + * + * LPF = (sample_rate * 3) / 8 + * Plus additional offset if quarter-shift is enabled (not implemented yet) + */ + uint32_t lpf_bandwidth = (sampling_rate() * 3) / 8; + + // For now, quarter-shift is disabled, so no offset added + // When quarter-shift is implemented: + // if (quarter_shift_enabled) { + // uint32_t offset = (sampling_rate() << decimation_n) / 8; + // lpf_bandwidth += offset * 2; + // } + + radio::set_baseband_filter_bandwidth_rx(lpf_bandwidth); +#else radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth()); +#endif + } } void ReceiverModel::update_sampling_rate() { @@ -323,9 +361,9 @@ void ReceiverModel::update_sampling_rate() { // protocols that need quick RX/TX turn-around. // Disabling baseband while changing sampling rates seems like a good idea... - if (enabled_) + if (enabled_) { radio::set_baseband_rate(sampling_rate()); - + } update_tuning_frequency(); } diff --git a/firmware/application/rf_path.cpp b/firmware/application/rf_path.cpp index 905a346cb..66ae203d8 100644 --- a/firmware/application/rf_path.cpp +++ b/firmware/application/rf_path.cpp @@ -35,6 +35,36 @@ namespace path { namespace { +#ifdef PRALINE +/* PRALINE uses a simplified RF path with only 5 control signals. + * The RF path architecture is completely different from HackRF One. + */ +struct PralineConfig { + bool tx_en; + bool mix_en_n; // Inverted: 0 = mixer enabled + bool lpf_en; + bool rf_amp_en; + bool ant_bias_en_n; // Inverted: 0 = bias enabled + + static void gpio_init() { + gpio_tx_enable.output(); + gpio_mix_enable_n.output(); + gpio_lpf_enable.output(); + gpio_rf_amp_enable.output(); + gpio_ant_bias_disable.output(); + } + + void apply() const { + gpio_tx_enable.write(tx_en); + gpio_mix_enable_n.write(mix_en_n); + gpio_lpf_enable.write(lpf_en); + gpio_rf_amp_enable.write(rf_amp_en); + gpio_ant_bias_disable.write(ant_bias_en_n); + } +}; + +#else +/* HackRF One uses 11 GPIOs for RF path control */ using GPIOs = std::array; /* TODO: ARM GCC 4.8 2014q3 doesn't like this array inside struct Config. @@ -53,7 +83,10 @@ constexpr GPIOs gpios{ gpio_rx_amp, gpio_not_rx_amp_pwr, }; +#endif +#ifndef PRALINE +/* HackRF One Config struct - not used on PRALINE */ struct Config { using base_type = uint16_t; @@ -153,7 +186,10 @@ struct Config { } } }; +#endif /* !PRALINE */ +#ifndef PRALINE +/* HackRF One config table - not used on PRALINE */ using ConfigAmp = std::array; using ConfigDirection = std::array; using ConfigBand = std::array; @@ -193,12 +229,26 @@ constexpr Config get_config( const bool amplify) { return config_table[toUType(band)][toUType(direction)][amplify ? 1 : 0]; } +#endif /* !PRALINE */ } /* namespace */ void Path::init() { +#ifdef PRALINE + PralineConfig::gpio_init(); + /* Set safe initial state: RX mode, mixer enabled, LPF on, amp off, no bias */ + PralineConfig config = { + .tx_en = false, + .mix_en_n = false, // Mixer enabled (inverted) + .lpf_en = true, // LPF on for low band + .rf_amp_en = false, // Amp off + .ant_bias_en_n = true // Bias off (inverted) + }; + config.apply(); +#else update(); Config::gpio_init(); +#endif } void Path::set_direction(const Direction new_direction) { @@ -220,20 +270,48 @@ void Path::update() { /* 0 ^ 0 => 0 & 0 = 0 ^ 0 = 0 (no change) * 0 ^ 1 => 1 & 0 = 0 ^ 0 = 0 (ignore change to 1) * 1 ^ 0 => 1 & 1 = 1 ^ 1 = 0 (allow change to 0) - * 1 ^ 1 => 0 & 1 = 0 ^ 1 = 1 (no change) + * 1 ^ 1 => 0 & 1 = 0 ^ 1 = 1 (no change) */ + +#ifdef PRALINE + /* PRALINE RF path control: + * - tx_en: 1 for TX, 0 for RX + * - mix_en_n: 0 to enable mixer (inverted), 1 to bypass + * - lpf_en: 1 for low band (< 2.4 GHz), 0 for high band + * - rf_amp_en: 1 to enable RF amplifier + * - ant_bias_en_n: 0 to enable antenna bias (inverted) */ // const Config changed = _config ^ config_next; // const Config turned_off = _config & changed; + PralineConfig config; + /* In transition, ignore the bits that are turning on. So this transition phase * only turns off signals. It doesn't turn on signals. */ // const Config transition_config = _config ^ turned_off; // update_signals(transition_config); + config.tx_en = (direction == Direction::Transmit); + + /* Mixer bypass for mid band (2.3-2.7 GHz direct to MAX2831) */ + config.mix_en_n = (band == Band::Mid); // 1 = bypass (disabled) + /* Move to the final state by turning on required signals. */ + /* LPF for low band */ + config.lpf_en = (band == Band::Low); + + /* RF amp when amplification requested */ + config.rf_amp_en = rf_amp; + + /* Antenna bias off by default */ + config.ant_bias_en_n = true; + + config.apply(); +#else + /* HackRF One RF path control */ const auto config = get_config(direction, band, rf_amp); config.apply(); +#endif } } // namespace path diff --git a/firmware/application/tuning.cpp b/firmware/application/tuning.cpp index f1425a52f..462032851 100644 --- a/firmware/application/tuning.cpp +++ b/firmware/application/tuning.cpp @@ -26,9 +26,26 @@ namespace tuning { namespace config { +// Forward declarations +Config low_band(const rf::Frequency target_frequency); +Config mid_band(const rf::Frequency target_frequency); +Config high_band(const rf::Frequency target_frequency); + // Low band <2170 Mhz: constexpr rf::Frequency low_band_second_lo_frequency(const rf::Frequency target_frequency) { +#ifdef PRALINE + // Praline-specific formula for MAX2831 (2.3-2.6 GHz range) + // Use a fixed second_lo that: + // 1. Falls in MAX2831's sweet spot (2.3-2.6 GHz) + // 2. Gives RFFC5072 a VCO frequency in its range (2700-5400 MHz) + + // For most low-band frequencies, use 2500 MHz as second_lo + // This gives RFFC5072 plenty of headroom + (void)target_frequency; // Unused in fixed formula + return 2500'000'000; +#else return 2650'000'000 - (target_frequency / 7); +#endif } Config low_band(const rf::Frequency target_frequency) { @@ -40,20 +57,52 @@ Config low_band(const rf::Frequency target_frequency) { // Mid band 2170-2740 Mhz: Config mid_band(const rf::Frequency target_frequency) { +#ifdef PRALINE + // For Praline with MAX2831 (2.3-2.6 GHz range) + // Frequencies 2170-2300 MHz need upconversion since they're below MAX2831 minimum + if (target_frequency < 2300'000'000) { + // Treat as low band + return low_band(target_frequency); + } + // Frequencies 2300-2600 MHz can go direct (no RFFC5072) + else if (target_frequency <= 2600'000'000) { + const rf::Frequency second_lo_frequency = target_frequency; + const rf::Frequency first_lo_frequency = 0; + const bool mixer_invert = false; + return {first_lo_frequency, second_lo_frequency, rf::path::Band::Mid, mixer_invert}; + } + // Frequencies 2600-2740 MHz need downconversion since they're above MAX2831 maximum + else { + // Treat as high band + return high_band(target_frequency); + } +#else const rf::Frequency second_lo_frequency = target_frequency; const rf::Frequency first_lo_frequency = 0; const bool mixer_invert = false; return {first_lo_frequency, second_lo_frequency, rf::path::Band::Mid, mixer_invert}; +#endif } // High band >2740 Mhz: constexpr rf::Frequency high_band_second_lo_frequency(const rf::Frequency target_frequency) { +#ifdef PRALINE + // Praline formula tuned for MAX2831 (2.3-2.6 GHz range) + // Keep second_lo in MAX2831's range while allowing RFFC5072 to work + if (target_frequency < 3600'000'000) + return 2400'000'000 + ((target_frequency - 2740'000'000) / 4); + else if (target_frequency < 5100'000'000) + return 2500'000'000 + ((target_frequency - 3600'000'000) / 6); + else + return 2550'000'000 + ((target_frequency - 5100'000'000) / 10); +#else if (target_frequency < 3600'000'000) return (2170'000'000 + (((target_frequency - 2740'000'000) * 57) / 86)); else if (target_frequency < 5100'000'000) return (2350'000'000 + ((target_frequency - 3600'000'000) / 5)); else return (2500'000'000 + ((target_frequency - 5100'000'000) / 9)); +#endif } Config high_band(const rf::Frequency target_frequency) { diff --git a/firmware/application/ui/ui_receiver.cpp b/firmware/application/ui/ui_receiver.cpp index 7a5b20822..17d08ca4e 100644 --- a/firmware/application/ui/ui_receiver.cpp +++ b/firmware/application/ui/ui_receiver.cpp @@ -26,7 +26,10 @@ #include "string_format.hpp" #include "ui_receiver.hpp" #include "ui_freqman.hpp" + +#ifndef PRALINE #include "audio.hpp" +#endif using namespace portapack; @@ -592,6 +595,10 @@ AudioVolumeField::AudioVolumeField( /* fill char */ ' '} { set_value(receiver_model.normalized_headphone_volume()); +#ifdef PRALINE + on_change = [](int32_t v) { + receiver_model.set_normalized_headphone_volume(v); +#else on_change = [](int32_t vol) { // don't call receiver model, because this widget shuld be able to handle volume settinsg from any app, regardless of the receiver model's enables state. like the tx apps should be able to set volume too. // this is identical to the receiver model's method @@ -599,6 +606,7 @@ AudioVolumeField::AudioVolumeField( auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max; persistent_memory::set_headphone_volume(new_volume); audio::headphone::set_volume(new_volume); +#endif }; } diff --git a/firmware/application/usb_serial_cdc.c b/firmware/application/usb_serial_cdc.c index dffa76271..9e0468bd0 100644 --- a/firmware/application/usb_serial_cdc.c +++ b/firmware/application/usb_serial_cdc.c @@ -37,9 +37,17 @@ CH_IRQ_HANDLER(USB0_IRQHandler) { usb0_isr(); if (status & USB0_USBSTS_D_UI) { +#ifdef PRALINE + if (thread_usb_event) { + chSysLockFromIsr(); + chEvtSignalI(thread_usb_event, EVT_MASK_USB); + chSysUnlockFromIsr(); + } +#else chSysLockFromIsr(); chEvtSignalI(thread_usb_event, EVT_MASK_USB); chSysUnlockFromIsr(); +#endif } if (status & USB0_USBSTS_D_SLI) { diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index d4ba69c5d..57db4d043 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -213,7 +213,11 @@ set(CPPWARN "-Wall -Wextra") # List all default C defines here, like -D_DEBUG=1 # TODO: Switch -DCRT0_INIT_DATA depending on load from RAM or SPIFI? # NOTE: _RANDOM_TCC to kill a GCC 4.9.3 error with std::max argument types -set(DDEFS "-DLPC43XX -DLPC43XX_M4 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'") +# Use BOARD variable if set, otherwise default to HACKRF_ONE +if(NOT DEFINED BOARD) + set(BOARD "HACKRF_ONE") +endif() +set(DDEFS "-DLPC43XX -DLPC43XX_M4 -D__NEWLIB__ -D${BOARD} -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'") # List all default ASM defines here, like -D_DEBUG=1 set(DADEFS) @@ -621,7 +625,7 @@ DeclareTargets(PADT adsbtx) ### Binary Timed stream TX set(MODE_CPPSRC - proc_bint_stream_tx.cpp + proc_bint_stream_tx.cpp ) DeclareTargets(POSK bintstream) @@ -725,7 +729,10 @@ set(MODE_CPPSRC ${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c ${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c ) +# sd_over_usb has type conflicts with PRALINE (HackRF Pro) - disable for now +if(NOT BOARD STREQUAL "PRALINE") DeclareTargets(PUSB sd_over_usb) +endif() ### HackRF "factory" firmware diff --git a/firmware/baseband/baseband_dma.cpp b/firmware/baseband/baseband_dma.cpp index 412942661..825e89de6 100644 --- a/firmware/baseband/baseband_dma.cpp +++ b/firmware/baseband/baseband_dma.cpp @@ -110,6 +110,9 @@ volatile uint32_t buffer_handled = 0; static void transfer_complete() { const auto next_lli_index = gpdma_channel_sgpio.next_lli() - &lli_loop[0]; buffer_transfered++; +#ifdef PRALINE + shared_memory.m4_dma_xfr_count++; // Phase 0 instrumentation +#endif thread_wait.wake_from_interrupt(next_lli_index); } @@ -161,6 +164,10 @@ void disable() { } baseband::buffer_t wait_for_buffer() { +#ifdef PRALINE + shared_memory.m4_dma_wait_count++; // Phase 0 instrumentation +#endif + const auto next_index = thread_wait.sleep(); buffer_handled++; diff --git a/firmware/baseband/baseband_thread.cpp b/firmware/baseband/baseband_thread.cpp index 4d366f67b..289e214c7 100644 --- a/firmware/baseband/baseband_thread.cpp +++ b/firmware/baseband/baseband_thread.cpp @@ -87,8 +87,15 @@ void BasebandThread::run() { baseband_sgpio.configure(direction()); baseband::dma::enable(direction()); baseband_sgpio.streaming_enable(); +#ifdef PRALINE + shared_memory.m4_streaming_marker = 0xAA; // Phase 0 instrumentation +#endif while (!chThdShouldTerminate()) { +#ifdef PRALINE + shared_memory.m4_baseband_loops++; // Phase 0 instrumentation +#endif + // TODO: Place correct sampling rate into buffer returned here: const auto buffer_tmp = baseband::dma::wait_for_buffer(); if (buffer_tmp) { diff --git a/firmware/baseband/event_m4.cpp b/firmware/baseband/event_m4.cpp index e4888dbc1..10ac57b5c 100644 --- a/firmware/baseband/event_m4.cpp +++ b/firmware/baseband/event_m4.cpp @@ -99,6 +99,9 @@ void EventDispatcher::on_message(const Message* const message) { switch (message->id) { case Message::ID::Shutdown: on_message_shutdown(*reinterpret_cast(message)); +#ifdef PRALINE + shared_memory.baseband_message = nullptr; // Must clear before M4 exits! +#endif break; default: diff --git a/firmware/baseband/event_m4.hpp b/firmware/baseband/event_m4.hpp index 9dfa57950..06b5af9d1 100644 --- a/firmware/baseband/event_m4.hpp +++ b/firmware/baseband/event_m4.hpp @@ -46,7 +46,13 @@ class EventDispatcher { } static inline void events_flag_isr(const eventmask_t events) { +#ifdef PRALINE + if (thread_event_loop) { + chEvtSignalI(thread_event_loop, events); + } +#else chEvtSignalI(thread_event_loop, events); +#endif } private: diff --git a/firmware/baseband/proc_am_audio.cpp b/firmware/baseband/proc_am_audio.cpp index 95cbe3b58..a4bb87487 100644 --- a/firmware/baseband/proc_am_audio.cpp +++ b/firmware/baseband/proc_am_audio.cpp @@ -29,6 +29,16 @@ #include #include "dsp_hilbert.hpp" +// Phase 2: Constructor to start threads AFTER object is fully initialized +NarrowbandAMAudio::NarrowbandAMAudio() { + // Initialize members that threads might access + channel_spectrum.set_decimation_factor(1); + + // Start threads AFTER everything initialized + baseband_thread.start(); + rssi_thread.start(); +} + void NarrowbandAMAudio::execute(const buffer_c8_t& buffer) { if (!configured) { return; diff --git a/firmware/baseband/proc_am_audio.hpp b/firmware/baseband/proc_am_audio.hpp index 8f651bb5b..635f96289 100644 --- a/firmware/baseband/proc_am_audio.hpp +++ b/firmware/baseband/proc_am_audio.hpp @@ -37,6 +37,8 @@ class NarrowbandAMAudio : public BasebandProcessor { public: + NarrowbandAMAudio(); // Phase 2: Explicit constructor for manual thread start + void execute(const buffer_c8_t& buffer) override; void on_message(const Message* const message) override; @@ -74,8 +76,14 @@ class NarrowbandAMAudio : public BasebandProcessor { SpectrumCollector channel_spectrum{}; /* NB: Threads should be the last members in the class definition. */ +#ifndef PRALINE BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; RSSIThread rssi_thread{}; +#else + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive, + /*auto_start*/ false}; // Phase 2: Manual start + RSSIThread rssi_thread{/*auto_start*/ false}; // Phase 2: Manual start +#endif void configure(const AMConfigureMessage& message); void capture_config(const CaptureConfigMessage& message); diff --git a/firmware/baseband/proc_nfm_audio.cpp b/firmware/baseband/proc_nfm_audio.cpp index 47e1f7d41..72a156599 100644 --- a/firmware/baseband/proc_nfm_audio.cpp +++ b/firmware/baseband/proc_nfm_audio.cpp @@ -31,6 +31,18 @@ #include #include +#ifdef PRALINE +// Phase 2: Constructor to start threads AFTER object is fully initialized +NarrowbandFMAudio::NarrowbandFMAudio() { + // Initialize members that threads might access + channel_spectrum.set_decimation_factor(1); + + // Start threads AFTER everything initialized + baseband_thread.start(); + rssi_thread.start(); +} +#endif + void NarrowbandFMAudio::execute(const buffer_c8_t& buffer) { // bool new_state; diff --git a/firmware/baseband/proc_nfm_audio.hpp b/firmware/baseband/proc_nfm_audio.hpp index 8ac185ce4..a66975503 100644 --- a/firmware/baseband/proc_nfm_audio.hpp +++ b/firmware/baseband/proc_nfm_audio.hpp @@ -41,6 +41,10 @@ class NarrowbandFMAudio : public BasebandProcessor { public: +#ifdef PRALINE + NarrowbandFMAudio(); // Phase 2: Explicit constructor for manual thread start +#endif + void execute(const buffer_c8_t& buffer) override; void on_message(const Message* const message) override; @@ -97,8 +101,14 @@ class NarrowbandFMAudio : public BasebandProcessor { CodedSquelchMessage ctcss_message{0}; /* NB: Threads should be the last members in the class definition. */ +#ifdef PRALINE + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive, + /*auto_start*/ false}; // Phase 2: Manual start + RSSIThread rssi_thread{/*auto_start*/ false}; // Phase 2: Manual start +#else BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; RSSIThread rssi_thread{}; +#endif void pitch_rssi_config(const PitchRSSIConfigureMessage& message); void configure(const NBFMConfigureMessage& message); diff --git a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cmake b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cmake index 62e8ec328..8c32a69e7 100644 --- a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cmake +++ b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cmake @@ -3,7 +3,20 @@ set(BOARDSRC ${CHIBIOS_PORTAPACK}/boards/PORTAPACK_APPLICATION/board.cpp ) +# Add FPGA bridge for PRALINE (HackRF Pro) +if(BOARD STREQUAL "PRALINE") + list(APPEND BOARDSRC + ${CHIBIOS_PORTAPACK}/boards/PORTAPACK_APPLICATION/fpga_bridge.c + ${CHIBIOS_PORTAPACK}/../../hackrf/firmware/common/lz4_blk.c + ) +endif() + # Required include directories set(BOARDINC ${CHIBIOS_PORTAPACK}/boards/PORTAPACK_APPLICATION ) + +# Add hackrf common include for PRALINE (for lz4_blk.h) +if(BOARD STREQUAL "PRALINE") + list(APPEND BOARDINC ${CHIBIOS_PORTAPACK}/../../hackrf/firmware/common) +endif() diff --git a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cpp b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cpp index 3a9aa6223..6d70d5a82 100755 --- a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cpp +++ b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/board.cpp @@ -15,13 +15,22 @@ limitations under the License. */ + #include "ch.h" #include "hal.h" #include +// Declare wrapper function. board.cpp to avoid conflicting gpio_t definitions. bool hackrf_r9; +// Declare the bridge function (no need to include HackRF headers here) +#ifdef PRALINE +extern "C" { + int fpga_bridge_init(void); +} +#endif + #if HAL_USE_PAL || defined(__DOXYGEN__) /** * @brief PAL setup. @@ -262,9 +271,9 @@ const PALConfig pal_default_config = { { 4, 7, scu_config_normal_drive_t { .mode=1, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* GP_CLKIN/P72/MCU_CLK: SI5351C.CLK7(O) */ /* HackRF: LEDs. Configured early so we can use them to indicate boot status. */ - { 4, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* LED1: LED1.A(I) */ - { 4, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* LED2: LED2.A(I) */ - { 6, 12, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* LED3: LED3.A(I) */ + { 4, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* LED1: LED1.A(I) */ + { 4, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* LED2: LED2.A(I) */ + { 6, 12, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* LED3: LED3.A(I) */ /* Power control */ { 6, 11, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* VREGMODE/P69: TPS62410.MODE/DATA(I) */ @@ -332,11 +341,12 @@ const PALConfig pal_default_config = { { 4, 9, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* SGPIO14/BANK2F3M4: CPLD.81/CPLD_P81 */ { 4, 10, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* SGPIO15/BANK2F3M6: CPLD.78/CPLD_P78 */ - /* HackRF: CPLD */ - { 6, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CPLD_TCK: CPLD.TCK(I), PortaPack CPLD.TCK(I) */ - { 6, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* CPLD_TDI: CPLD.TDI(I), PortaPack I2S0_RX_SDA(O), PortaPack CPLD.TDI(I) */ - { 6, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CPLD_TMS: CPLD.TMS(I) */ - { 9, 5, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* CPLD_TDO: CPLD.TDO(O) */ + /* PortaPack CPLD JTAG pins - same for all builds including PRALINE + * (PRALINE FPGA uses P5_1/P5_2/P4_10, not these pins) */ + { 6, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CPLD_TCK: PortaPack CPLD.TCK(I) */ + { 6, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* CPLD_TDI: PortaPack CPLD.TDI(I), I2S0_RX_SDA(O) */ + { 6, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CPLD_TMS: HackRF CPLD.TMS(I) */ + { 9, 5, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* CPLD_TDO: HackRF CPLD.TDO(O) */ /* PortaPack CPLD */ { 1, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* SD_POW: PortaPack CPLD.TDO(O) */ @@ -619,6 +629,10 @@ static const motocon_pwm_resources_t motocon_pwm_resources = { static const scu_setup_t pin_setup_vaa_enablex_pwm = { 5, 0, scu_config_normal_drive_t { .mode=1, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }; static const scu_setup_t pin_setup_vaa_enablex_gpio_og = { 5, 0, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }; static const scu_setup_t pin_setup_vaa_enablex_gpio_r9 = { 6, 10, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }; +#ifdef PRALINE +/* PRALINE uses P8_1 (GPIO4[1]) for VAA_DISABLE (high = VAA off, low = VAA on) */ +static const scu_setup_t pin_setup_vaa_disable_praline = { 8, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }; +#endif /* VAA powers: * MAX5864 analog section. @@ -824,7 +838,11 @@ extern "C" void __late_init(void) { * @todo Add your board-specific code, if any. */ extern "C" void boardInit(void) { +#ifndef PRALINE /* Detect HackRF variant */ + /* 1. Perform Standard Initialization first */ + /* This configures VAA power, LED pins, and detects board revision */ + /* Let detect_hackrf_r9() run - don't force for PRALINE */ hackrf_r9 = detect_hackrf_r9(); /* Configure variant-dependent pins. */ if (hackrf_r9) { @@ -834,12 +852,143 @@ extern "C" void boardInit(void) { setup_gpios(gpio_setup_og); setup_pins(pins_setup_og); } + + /* 2. Turn on VAA (Critical for Radio/Transceiver) */ vaa_power_on(); + + /* 3. Handle VAA Enable Pin Latching */ if (hackrf_r9) { LPC_GPIO->W2[9] = 1; } else { LPC_GPIO->W3[6] = 1; } + + /* 4. HackRF Pro Specific: Initialize and Load FPGA */ +#else + hackrf_r9 = false; + /* Enable 3.3V aux power - P6_7 = GPIO5[15], active LOW (clear to enable) */ + LPC_SCU->SFSP[6][7] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->DIR[5] |= (1 << 15); + LPC_GPIO->CLR[5] = (1 << 15); /* Clear = enable 3.3V aux */ + { volatile uint32_t delay = 100000; while(delay--); } + + /* Enable 1.2V for FPGA - P8_7 = GPIO4[7], active high */ + LPC_SCU->SFSP[8][7] = 0x10; + LPC_GPIO->DIR[4] |= (1 << 7); + LPC_GPIO->SET[4] = (1 << 7); + { volatile uint32_t delay = 100000; while(delay--); } + + /* Enable VAA for RF - P8_1 = GPIO4[1], active low */ + LPC_SCU->SFSP[8][1] = 0x10; + LPC_GPIO->DIR[4] |= (1 << 1); + LPC_GPIO->CLR[4] = (1 << 1); + { volatile uint32_t delay = 100000; while(delay--); } + + /* Configure RFFC5072 pins for PRALINE */ + /* P9_2 = GPIO4[14] RFFC5072 data (SCU_GPIO_FAST | FUNCTION0 = 0xF0) */ + LPC_SCU->SFSP[9][2] = 0xF0; + /* P9_5 = GPIO5[18] RFFC5072 clock (SCU_GPIO_FAST | FUNCTION4 = 0xF4) */ + LPC_SCU->SFSP[9][5] = 0xF4; + LPC_GPIO->DIR[5] |= (1 << 18); /* Clock as output */ + + /* Configure Port D pins for PRALINE (use SFSPD registers) */ + /* PD_14 = GPIO6[28] MAX2831 chip select */ + LPC_SCU->SFSPD[14] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->SET[6] = (1 << 28); /* CS high (inactive) */ + LPC_GPIO->DIR[6] |= (1 << 28); /* Output */ + /* PD_15 = GPIO6[29] MAX2831 RXHP control */ + LPC_SCU->SFSPD[15] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->CLR[6] = (1 << 29); /* RXHP low = 100 Hz HPF */ + LPC_GPIO->DIR[6] |= (1 << 29); /* Output */ + /* PD_16 = GPIO6[30] MAX5864 chip select */ + LPC_SCU->SFSPD[16] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->SET[6] |= (1 << 30); /* CS high (inactive) */ + LPC_GPIO->DIR[6] |= (1 << 30); /* Output */ + + /* Configure Port E pins for MAX2831 control (use SFSPE registers) */ + /* PE_1 = GPIO7[1] MAX2831 ENABLE */ + LPC_SCU->SFSPE[1] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->CLR[7] = (1 << 1); /* Start disabled */ + LPC_GPIO->DIR[7] |= (1 << 1); /* Output */ + /* PE_2 = GPIO7[2] MAX2831 RXTX mode select */ + LPC_SCU->SFSPE[2] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->CLR[7] = (1 << 2); /* Start in shutdown mode */ + LPC_GPIO->DIR[7] |= (1 << 2); /* Output */ + + /* Configure Port 6 pins for RF path control */ + /* P6_3 = GPIO3[2] Mixer enable (inverted: 0 = mixer ON) */ + LPC_SCU->SFSP[6][3] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */ + LPC_GPIO->CLR[3] = (1 << 2); /* Mixer enabled by default */ + LPC_GPIO->DIR[3] |= (1 << 2); /* Output */ + /* P6_5 = GPIO3[4] TX enable */ + LPC_SCU->SFSP[6][5] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */ + LPC_GPIO->CLR[3] = (1 << 4); /* TX off by default (RX mode) */ + LPC_GPIO->DIR[3] |= (1 << 4); /* Output */ + + /* Configure Port A pins for RF path control */ + /* PA_1 = GPIO4[8] LPF enable */ + LPC_SCU->SFSP[0xA][1] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->SET[4] = (1 << 8); /* LPF enabled by default (low band) */ + LPC_GPIO->DIR[4] |= (1 << 8); /* Output */ + /* PA_2 = GPIO4[9] RF amp enable */ + LPC_SCU->SFSP[0xA][2] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */ + LPC_GPIO->CLR[4] = (1 << 9); /* RF amp off by default */ + LPC_GPIO->DIR[4] |= (1 << 9); /* Output */ + + /* Configure PRALINE-specific SGPIO pins for FPGA sample interface. + * These override the HackRF One pin config from pins_setup. + * PRALINE uses different pins than HackRF One for SGPIO4/8/9/10. + * SCU_GPIO_FAST = 0xF0 (EPUN + EHS + EZI + ZIF) + */ + /* CRITICAL: Disable HackRF One SGPIO8 pin (P9_6) - PRALINE uses P8_0 instead */ + LPC_SCU->SFSP[9][6] = 0x00; /* P9_6 = GPIO mode 0, disable SGPIO function */ + + /* SGPIO4 = P9_4 function 6 (HOST_DATA4) */ + LPC_SCU->SFSP[9][4] = 0xF6; /* SCU_GPIO_FAST | func 6 */ + /* SGPIO8 = P8_0 function 4 (SGPIO_CLK - clock from FPGA) */ + LPC_SCU->SFSP[8][0] = 0xF4; /* SCU_GPIO_FAST | func 4 */ + /* SGPIO9 = P9_3 function 6 (HOST_CAPTURE) */ + LPC_SCU->SFSP[9][3] = 0xF6; /* SCU_GPIO_FAST | func 6 */ + /* SGPIO10 = P8_2 function 4 (HOST_DISABLE - output to FPGA) */ + LPC_SCU->SFSP[8][2] = 0xF4; /* SCU_GPIO_FAST | func 4 */ + /* SGPIO11 = P1_17 function 6 (HOST_DIRECTION - output to FPGA, tells FPGA TX vs RX) */ + LPC_SCU->SFSP[1][17] = 0xF6; /* SCU_GPIO_FAST | func 6 */ + + /* SGPIO data pins (SGPIO0-7) - all 8 bits required for sample data */ + LPC_SCU->SFSP[0][0] = 0xF3; /* SGPIO0: P0_0 function 3, HOST_DATA0 */ + LPC_SCU->SFSP[0][1] = 0xF3; /* SGPIO1: P0_1 function 3, HOST_DATA1 */ + LPC_SCU->SFSP[1][15] = 0xF2; /* SGPIO2: P1_15 function 2, HOST_DATA2 */ + LPC_SCU->SFSP[1][16] = 0xF2; /* SGPIO3: P1_16 function 2, HOST_DATA3 */ + /* SGPIO4 already configured above at line 942 */ + LPC_SCU->SFSP[6][6] = 0xF2; /* SGPIO5: P6_6 function 2, HOST_DATA5 */ + LPC_SCU->SFSP[2][2] = 0xF0; /* SGPIO6: P2_2 function 0, HOST_DATA6 */ + LPC_SCU->SFSP[1][0] = 0xF6; /* SGPIO7: P1_0 function 6, HOST_DATA7 */ + + /* NOTE: P9_5 is RFFC5072 mixer clock (SCU_MIXER_SCLK), NOT SGPIO! + * Do NOT override P9_5 here. */ + + // Trigger FPGA bitstream loading via fpga bridge + // Attempt to load the FPGA bitstream + // This function returns LD_SUCCESS (0) if the FPGA confirms the bitstream + // Use LEDs to check if initi is successful. + + // Setup LED pin directions + // LED1 (USB) = GPIO2[1], LED2 (RX) = GPIO2[2], LED3 (TX) = GPIO2[8] + LPC_GPIO->DIR[2] |= (1 << 1) | (1 << 2) | (1 << 8); + + // Turn off all LEDs to start + // PRALINE LEDs are active-low: SET (HIGH) = OFF, CLR (LOW) = ON + LPC_GPIO->SET[2] = (1 << 1) | (1 << 2) | (1 << 8); + + // Call fpga_bridge_init and continue boot regardless of result + // (Watchdog was resetting device when we halted with while(1)) + int load_result = fpga_bridge_init(); + (void)load_result; // Ignore result for now, just let boot continue + + // Keep LEDs off after FPGA load + LPC_GPIO->SET[2] = (1 << 1) | (1 << 2) | (1 << 8); +#endif + } extern "C" void _default_exit(void) { @@ -848,10 +997,10 @@ extern "C" void _default_exit(void) { } else { LPC_GPIO->W3[6] = 0; } + vaa_power_off(); chSysDisable(); - systick_stop(); /* Don't reset these peripherals, as they're operating during shutdown: diff --git a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.c b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.c new file mode 100644 index 000000000..b144e6ba9 --- /dev/null +++ b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.c @@ -0,0 +1,472 @@ +// This bridge allows calls for functions in their native C context +// HackRF headers here - C for use within the C++ board.cpp context + +// Check if PRALINE was passed from CMake +#ifdef PRALINE + #warning "Building for HackRF_PRO with FPGA." + + // Necessary headers + #include "lz4_blk.h" + + // LIBOPENCM3 Headers (only CGU for clock setup) + #include + + #include + #include + #include + #include + + // SPIFI memory-mapped base address + // Flash is mapped starting at 0x14000000 + // FPGA bitstream at flash address 0x380000 = memory address 0x14380000 + // PRALINE: Moved to 1.5MB offset to allow larger base firmware + #define SPIFI_DATA_BASE 0x14000000 + #define FPGA_BITSTREAM_FLASH_ADDR 0x380000 // Was 0x100000 (1MB), then 0x180000 (1.5MB), now 0x380000 (3.5MB) + #define FPGA_BITSTREAM_MEM_ADDR (SPIFI_DATA_BASE + FPGA_BITSTREAM_FLASH_ADDR) + + // MMIO32 direct register access + #define MMIO32_LOCAL(addr) (*(volatile uint32_t*)(addr)) + + // SSP1 base address + #define SSP1_BASE_LOCAL 0x400C5000 + + // SSP register offsets + #define SSP_CR0_OFF 0x000 + #define SSP_CR1_OFF 0x004 + #define SSP_DR_OFF 0x008 + #define SSP_SR_OFF 0x00C + #define SSP_CPSR_OFF 0x010 + + // SSP register access + #define SSP1_CR0_LOCAL MMIO32_LOCAL(SSP1_BASE_LOCAL + SSP_CR0_OFF) + #define SSP1_CR1_LOCAL MMIO32_LOCAL(SSP1_BASE_LOCAL + SSP_CR1_OFF) + #define SSP1_DR_LOCAL MMIO32_LOCAL(SSP1_BASE_LOCAL + SSP_DR_OFF) + #define SSP1_SR_LOCAL MMIO32_LOCAL(SSP1_BASE_LOCAL + SSP_SR_OFF) + #define SSP1_CPSR_LOCAL MMIO32_LOCAL(SSP1_BASE_LOCAL + SSP_CPSR_OFF) + + // SSP status bits + #define SSP_SR_TNF_LOCAL (1 << 1) // TX FIFO not full + #define SSP_SR_RNE_LOCAL (1 << 2) // RX FIFO not empty + #define SSP_SR_BSY_LOCAL (1 << 4) // Busy + + // SSP CR0 bits + #define SSP_CR0_DSS_8BIT (0x7) // 8-bit data + #define SSP_CR0_FRF_SPI (0x0) // SPI frame format + #define SSP_CR0_CPOL (1 << 6) // Clock polarity + #define SSP_CR0_CPHA (1 << 7) // Clock phase + + // SSP CR1 bits + #define SSP_CR1_SSE (1 << 1) // SSP enable + + // SCU pin configuration registers + #define PERIPH_BASE_APB0_LOCAL 0x40080000 + #define SCU_BASE_LOCAL (PERIPH_BASE_APB0_LOCAL + 0x06000) + #define PIN_GROUP1_LOCAL (SCU_BASE_LOCAL + 0x080) + #define PIN_GROUP4_LOCAL (SCU_BASE_LOCAL + 0x200) + #define PIN_GROUP5_LOCAL (SCU_BASE_LOCAL + 0x280) + #define PIN3_LOCAL 0x00C + #define PIN4_LOCAL 0x010 + #define PIN1_LOCAL 0x004 + #define PIN2_LOCAL 0x008 + #define PIN10_LOCAL 0x028 + #define PIN19_LOCAL 0x04C + + // SCU configuration flags + #define SCU_CONF_EPUN_DIS_PULLUP_LOCAL (1 << 4) + #define SCU_CONF_EHS_FAST_LOCAL (1 << 5) + #define SCU_CONF_EZI_EN_IN_BUFFER_LOCAL (1 << 6) + #define SCU_CONF_ZIF_DIS_IN_GLITCH_FILT_LOCAL (1 << 7) + #define SCU_GPIO_FAST_LOCAL (SCU_CONF_EPUN_DIS_PULLUP_LOCAL | \ + SCU_CONF_EHS_FAST_LOCAL | \ + SCU_CONF_EZI_EN_IN_BUFFER_LOCAL | \ + SCU_CONF_ZIF_DIS_IN_GLITCH_FILT_LOCAL) + #define SCU_SSP_IO_LOCAL SCU_GPIO_FAST_LOCAL + + // Function select values + #define SCU_CONF_FUNCTION0_LOCAL (0x0) + #define SCU_CONF_FUNCTION1_LOCAL (0x1) + #define SCU_CONF_FUNCTION4_LOCAL (0x4) + #define SCU_CONF_FUNCTION5_LOCAL (0x5) + #define SCU_GPIO_NOPULL_LOCAL (SCU_CONF_EZI_EN_IN_BUFFER_LOCAL | SCU_CONF_ZIF_DIS_IN_GLITCH_FILT_LOCAL) + #define SCU_GPIO_PUP_LOCAL (SCU_CONF_EZI_EN_IN_BUFFER_LOCAL) + + // SSP1 pins (for FPGA programming) + #define SCU_SSP1_CIPO_LOCAL (PIN_GROUP1_LOCAL + PIN3_LOCAL) // P1_3 + #define SCU_SSP1_COPI_LOCAL (PIN_GROUP1_LOCAL + PIN4_LOCAL) // P1_4 + #define SCU_SSP1_SCK_LOCAL (PIN_GROUP1_LOCAL + PIN19_LOCAL) // P1_19 + + // FPGA control pins + #define SCU_FPGA_CRESET_LOCAL (PIN_GROUP5_LOCAL + PIN2_LOCAL) // P5_2 GPIO2[11] + #define SCU_FPGA_CDONE_LOCAL (PIN_GROUP4_LOCAL + PIN10_LOCAL) // P4_10 GPIO5[14] + #define SCU_FPGA_SPI_CS_LOCAL (PIN_GROUP5_LOCAL + PIN1_LOCAL) // P5_1 GPIO2[10] + + // GPIO register addresses for direct MMIO access + #define GPIO_LPC_BASE_LOCAL 0x400F4000 + #define GPIO_DIR_BASE (GPIO_LPC_BASE_LOCAL + 0x2000) // Direction registers + #define GPIO_SET_BASE (GPIO_LPC_BASE_LOCAL + 0x2200) // Set registers + #define GPIO_CLR_BASE (GPIO_LPC_BASE_LOCAL + 0x2280) // Clear registers + #define GPIO_PIN_BASE (GPIO_LPC_BASE_LOCAL + 0x2100) // Pin read registers + + // GPIO port access macros + #define GPIO_DIR(port) MMIO32_LOCAL(GPIO_DIR_BASE + (port)*4) + #define GPIO_SET(port) MMIO32_LOCAL(GPIO_SET_BASE + (port)*4) + #define GPIO_CLR(port) MMIO32_LOCAL(GPIO_CLR_BASE + (port)*4) + #define GPIO_PIN(port) MMIO32_LOCAL(GPIO_PIN_BASE + (port)*4) + + // FPGA control GPIO pins + // GPIO2[11] = CRESET, GPIO5[14] = CDONE, GPIO2[10] = SPI_CS + #define FPGA_CRESET_PORT 2 + #define FPGA_CRESET_PIN 11 + #define FPGA_CDONE_PORT 5 + #define FPGA_CDONE_PIN 14 + #define FPGA_SPI_CS_PORT 2 + #define FPGA_SPI_CS_PIN 10 + + // Context structure for SPIFI-based reading + struct spifi_fpga_read_ctx { + const uint8_t* mem_ptr; // Current read position in SPIFI memory + size_t next_block_sz; + uint8_t init_flag; + uint8_t buffer[4096 + 2]; // Compressed block + next size + }; + + // Simple delay loop + static void delay_cycles(volatile uint32_t count) { + while (count--) { + __asm__ volatile ("nop"); + } + } + + // Microsecond delay (approximate, assuming ~200MHz clock) + static void delay_us(uint32_t us) { + // ~50 cycles per microsecond at 200MHz + delay_cycles(us * 50); + } + + // SSP1 transfer one byte + static uint8_t ssp1_transfer_byte(uint8_t data) { + // Wait for TX FIFO not full + while ((SSP1_SR_LOCAL & SSP_SR_TNF_LOCAL) == 0) {} + SSP1_DR_LOCAL = data; + // Wait for not busy + while (SSP1_SR_LOCAL & SSP_SR_BSY_LOCAL) {} + // Wait for RX FIFO not empty + while ((SSP1_SR_LOCAL & SSP_SR_RNE_LOCAL) == 0) {} + return SSP1_DR_LOCAL; + } + + // Configure SSP1 for iCE40 programming (SPI mode 3: CPOL=1, CPHA=1) + static void ssp1_init_ice40(void) { + // Disable SSP1 first + SSP1_CR1_LOCAL = 0; + + // Configure: 8-bit, SPI mode 3 (CPOL=1, CPHA=1), master mode + // SCR=21 for ~4MHz at 200MHz clock + SSP1_CR0_LOCAL = SSP_CR0_DSS_8BIT | SSP_CR0_FRF_SPI | SSP_CR0_CPOL | SSP_CR0_CPHA | (21 << 8); + + // Clock prescaler = 2 (divide by 2) + SSP1_CPSR_LOCAL = 2; + + // Enable SSP1 + SSP1_CR1_LOCAL = SSP_CR1_SSE; + } + + // Configure SSP1 pins via SCU + static void configure_ssp1_pins(void) { + // P1_3 = SSP1_MISO (function 5) + MMIO32_LOCAL(SCU_SSP1_CIPO_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION5_LOCAL; + // P1_4 = SSP1_MOSI (function 5) + MMIO32_LOCAL(SCU_SSP1_COPI_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION5_LOCAL; + // P1_19 = SSP1_SCK (function 1) + MMIO32_LOCAL(SCU_SSP1_SCK_LOCAL) = SCU_SSP_IO_LOCAL | SCU_CONF_FUNCTION1_LOCAL; + } + + // Configure FPGA control pins via SCU and GPIO + static void configure_fpga_control_pins(void) { + // P5_2 = GPIO2[11] = CRESET (function 0, output) + MMIO32_LOCAL(SCU_FPGA_CRESET_LOCAL) = SCU_GPIO_NOPULL_LOCAL | SCU_CONF_FUNCTION0_LOCAL; + // P4_10 = GPIO5[14] = CDONE (function 4, input with pullup) + MMIO32_LOCAL(SCU_FPGA_CDONE_LOCAL) = SCU_GPIO_PUP_LOCAL | SCU_CONF_FUNCTION4_LOCAL; + // P5_1 = GPIO2[10] = SPI_CS (function 0, output) + MMIO32_LOCAL(SCU_FPGA_SPI_CS_LOCAL) = SCU_GPIO_NOPULL_LOCAL | SCU_CONF_FUNCTION0_LOCAL; + + // Set CRESET and SPI_CS as outputs (GPIO2[11] and GPIO2[10]) + GPIO_DIR(FPGA_CRESET_PORT) |= (1 << FPGA_CRESET_PIN) | (1 << FPGA_SPI_CS_PIN); + // Clear both initially + GPIO_CLR(FPGA_CRESET_PORT) = (1 << FPGA_CRESET_PIN) | (1 << FPGA_SPI_CS_PIN); + + // CDONE is input (GPIO5[14]) + GPIO_DIR(FPGA_CDONE_PORT) &= ~(1 << FPGA_CDONE_PIN); + } + + // GPIO control helpers + static void fpga_creset_low(void) { + GPIO_CLR(FPGA_CRESET_PORT) = (1 << FPGA_CRESET_PIN); + } + static void fpga_creset_high(void) { + GPIO_SET(FPGA_CRESET_PORT) = (1 << FPGA_CRESET_PIN); + } + static void fpga_cs_low(void) { + GPIO_CLR(FPGA_SPI_CS_PORT) = (1 << FPGA_SPI_CS_PIN); + } + static void fpga_cs_high(void) { + GPIO_SET(FPGA_SPI_CS_PORT) = (1 << FPGA_SPI_CS_PIN); + } + static bool fpga_cdone_read(void) { + return (GPIO_PIN(FPGA_CDONE_PORT) & (1 << FPGA_CDONE_PIN)) != 0; + } + + // ============================================================================ + // FPGA Register Access via SPI (iCE40) + // ============================================================================ + // These functions allow reading/writing FPGA internal registers via SPI. + // The FPGA bitstream implements a simple SPI register interface. + // + // FPGA Register Map: + // Reg 1 (CTRL): DC_BLOCK(b0), QUARTER_SHIFT_EN(b1), QUARTER_SHIFT_UP(b2), PRBS(b6), TRIGGER_EN(b7) + // Reg 2 (RX_DECIM): Decimation ratio [2:0] + // Reg 3 (TX_CTRL): NCO_EN(b0) + // Reg 4 (TX_INTRP): Interpolation ratio [2:0] + // Reg 5 (TX_PSTEP): NCO phase step [7:0] + // + // SPI Protocol: + // Read: Send [reg & 0x7F, 0x00, 0x00] -> value in byte 3 + // Write: Send [(reg | 0x80), value, 0x00] + + // Configure SSP1 for iCE40 FPGA register access (Mode 3, 8-bit) + static void ssp1_set_mode_ice40(void) { + SSP1_CR1_LOCAL = 0; // Disable SSP1 + SSP1_CR0_LOCAL = SSP_CR0_DSS_8BIT | SSP_CR0_FRF_SPI | SSP_CR0_CPOL | SSP_CR0_CPHA | (21 << 8); + SSP1_CPSR_LOCAL = 2; + SSP1_CR1_LOCAL = SSP_CR1_SSE; // Enable SSP1 + } + + // Configure SSP1 back to MAX2831 mode (Mode 0, 9-bit) + static void ssp1_set_mode_max2831(void) { + SSP1_CR1_LOCAL = 0; // Disable SSP1 + SSP1_CR0_LOCAL = (0x08) | // 9-bit data (DSS = 0x08) + (0x00) | // SPI frame format + (0 << 6) | // CPOL = 0 (Mode 0) + (0 << 7) | // CPHA = 0 (Mode 0) + (21 << 8); // SCR = 21 + SSP1_CPSR_LOCAL = 2; + SSP1_CR1_LOCAL = SSP_CR1_SSE; // Enable SSP1 + } + + // Read an FPGA register via SPI + static uint8_t fpga_spi_read(uint8_t reg) { + uint8_t value; + fpga_cs_low(); + ssp1_transfer_byte(reg & 0x7F); // Clear MSB for read + ssp1_transfer_byte(0x00); // Dummy byte + value = ssp1_transfer_byte(0x00); // Read value + fpga_cs_high(); + return value; + } + + // Write an FPGA register via SPI + static void fpga_spi_write(uint8_t reg, uint8_t value) { + fpga_cs_low(); + ssp1_transfer_byte((reg & 0x7F) | 0x80); // Set MSB for write + ssp1_transfer_byte(value); + ssp1_transfer_byte(0x00); // Dummy byte + fpga_cs_high(); + } + + // Initialize FPGA registers after bitstream load + // This is equivalent to fpga_init() in the reference HackRF firmware + static void fpga_register_init(void) { + // Already in iCE40 mode after programming, so we can directly access registers + + // Register 1 (CTRL): Enable DC block (bit 0), disable everything else + // DC_BLOCK is CRITICAL for RX to work! + fpga_spi_write(1, 0x01); // DC_BLOCK = 1 + + // Register 2 (RX_DECIM): No decimation + fpga_spi_write(2, 0x00); + + // Register 3 (TX_CTRL): Disable NCO + fpga_spi_write(3, 0x00); + + // Register 4 (TX_INTRP): No interpolation + fpga_spi_write(4, 0x00); + + // Register 5 (TX_PSTEP): Zero phase step + fpga_spi_write(5, 0x00); + } + + // Cached register values for debug reads (since reads may require mode switch) + static uint8_t fpga_reg_cache[6] = {0, 0x01, 0x00, 0x00, 0x00, 0x00}; + static uint8_t fpga_reg_cache_valid = 0; + + // Public function to read FPGA register (callable from C++ application code) + // Switches SPI mode, reads register, switches back + uint8_t fpga_debug_register_read(uint8_t reg) { + if (reg == 0 || reg > 5) return 0xFF; + + uint8_t value; + ssp1_set_mode_ice40(); + value = fpga_spi_read(reg); + ssp1_set_mode_max2831(); + + fpga_reg_cache[reg] = value; + return value; + } + + // Public function to write FPGA register (callable from C++ application code) + void fpga_debug_register_write(uint8_t reg, uint8_t value) { + if (reg == 0 || reg > 5) return; + + ssp1_set_mode_ice40(); + fpga_spi_write(reg, value); + ssp1_set_mode_max2831(); + + fpga_reg_cache[reg] = value; + } + + // SPIFI-based read callback for LZ4 decompression + // Reads from SPIFI memory-mapped address instead of using SPI flash driver + static size_t spifi_fpga_read_block_cb(void* _ctx, uint8_t* out_buffer) { + struct spifi_fpga_read_ctx* ctx = (struct spifi_fpga_read_ctx*)_ctx; + size_t block_sz = ctx->next_block_sz; + + // First iteration: read first block size from SPIFI memory + if (ctx->init_flag == 0) { + block_sz = ctx->mem_ptr[0] | (ctx->mem_ptr[1] << 8); + ctx->mem_ptr += 2; + ctx->init_flag = 1; + } + + // Finish at end marker (block_sz == 0) + if (block_sz == 0) return 0; + + // Read compressed block from SPIFI memory + memcpy(ctx->buffer, ctx->mem_ptr, block_sz + 2); + ctx->mem_ptr += block_sz + 2; + + // Extract next block size + ctx->next_block_sz = ctx->buffer[block_sz] | (ctx->buffer[block_sz + 1] << 8); + + // Decompress block using LZ4 + return lz4_blk_decompress(ctx->buffer, out_buffer, block_sz); + } + + // Program iCE40 FPGA using SPIFI memory-mapped data + // Based on ice40_spi_syscfg_program() from ice40_spi.c + static bool program_fpga_from_spifi(const uint8_t* bitstream_start) { + // Drive CRESET_B = 0, SPI_SS = 0 + fpga_creset_low(); + fpga_cs_low(); + + // Wait minimum 200ns + delay_us(1); + + // Release CRESET_B (drive high) + fpga_creset_high(); + + // Wait minimum 1200us (we wait 1800us to be safe) + delay_us(1800); + + // Set SPI_SS = 1, send 8 dummy clocks + fpga_cs_high(); + ssp1_transfer_byte(0); + + // Send configuration image + // Use static buffers to avoid stack overflow (~8KB would be needed) + static uint8_t out_buffer[4096]; + static struct spifi_fpga_read_ctx ctx; + ctx.mem_ptr = bitstream_start; + ctx.next_block_sz = 0; + ctx.init_flag = 0; + + fpga_cs_low(); + // Full LZ4 decompress and send all bytes + for (;;) { + size_t read_sz = spifi_fpga_read_block_cb(&ctx, out_buffer); + if (read_sz == 0) break; + for (size_t j = 0; j < read_sz; j++) { + ssp1_transfer_byte(out_buffer[j]); + } + } + + // Wait for 100 clock cycles for CDONE to go high + fpga_cs_high(); + for (size_t j = 0; j < 13; j++) { + ssp1_transfer_byte(0); + } + + // Check CDONE status + bool success = fpga_cdone_read(); + + // NOTE: FPGA register initialization is done later in radio::init() + // The FPGA needs time to stabilize after configuration before accepting register writes + + // CRITICAL: Reconfigure SSP1 for MAX2831 (PRALINE RF chip) after FPGA programming + // iCE40 uses Mode 3 (CPOL=1, CPHA=1), 8-bit + // MAX2831 (PRALINE) uses Mode 0 (CPOL=0, CPHA=0), 9-bit (vs 16-bit for MAX283x on HackRF One) + // Without this, RF communication will fail! + /*SSP1_CR1_LOCAL = 0; // Disable SSP1 + SSP1_CR0_LOCAL = (0x08) | // 9-bit data (DSS = 0x08) for MAX2831/PRALINE + (0x00) | // SPI frame format + (0 << 6) | // CPOL = 0 (Mode 0) + (0 << 7) | // CPHA = 0 (Mode 0) + (21 << 8); // SCR = 21 (same as ssp_config_max283x for PRALINE) + SSP1_CPSR_LOCAL = 2; // Clock prescaler + SSP1_CR1_LOCAL = SSP_CR1_SSE; // Re-enable SSP1*/ + + return success; + } + + int fpga_bridge_init(void) { + // Enable SSP1 clock for FPGA programming + // Use PLL1 (204MHz) to match original HackRF - IRC (12MHz) is 17x too slow + CGU_BASE_SSP1_CLK = CGU_BASE_SSP1_CLK_AUTOBLOCK(1) | + CGU_BASE_SSP1_CLK_CLK_SEL(CGU_SRC_PLL1); + + // Configure SSP1 pins + configure_ssp1_pins(); + + // Configure FPGA control pins + configure_fpga_control_pins(); + + // Initialize SSP1 for iCE40 programming + ssp1_init_ice40(); + + // Read FPGA bitstream header from SPIFI memory + const uint8_t* fpga_header = (const uint8_t*)FPGA_BITSTREAM_MEM_ADDR; + uint32_t num_bitstreams = fpga_header[0] | (fpga_header[1] << 8) | + (fpga_header[2] << 16) | (fpga_header[3] << 24); + + // Check if header looks valid + if (num_bitstreams == 0 || num_bitstreams > 16 || num_bitstreams == 0xFFFFFFFF) { + // No valid FPGA bitstream - skip programming but continue boot + return 1; + } + + // Get offset of first bitstream (index 0 = standard_fpga) + uint32_t bitstream_offset = fpga_header[4] | (fpga_header[5] << 8) | + (fpga_header[6] << 16) | (fpga_header[7] << 24); + + // Calculate start address of first bitstream in SPIFI memory + const uint8_t* bitstream_start = (const uint8_t*)(FPGA_BITSTREAM_MEM_ADDR + bitstream_offset); + + // Full FPGA programming + bool success = program_fpga_from_spifi(bitstream_start); + + // Initialize FPGA registers immediately after programming + if (success) { + // Give FPGA 100us to stabilize after configuration + delay_us(100); + + // Initialize FPGA registers (DC_BLOCK, etc.) + fpga_register_init(); + + // Now switch to MAX2831 mode + ssp1_set_mode_max2831(); + } + + return success ? 0 : 2; + } +#else +#warning "Building for HackRF_One with CPLD." +#endif diff --git a/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.h b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.h new file mode 100644 index 000000000..24099d465 --- /dev/null +++ b/firmware/chibios-portapack/boards/PORTAPACK_APPLICATION/fpga_bridge.h @@ -0,0 +1,51 @@ +/* + * FPGA Bridge Header - PRALINE iCE40 FPGA interface + * + * Provides functions for initializing and accessing the FPGA on HackRF Pro (PRALINE). + */ + +#ifndef __FPGA_BRIDGE_H__ +#define __FPGA_BRIDGE_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#ifdef PRALINE + +/* + * Initialize the FPGA - loads bitstream from SPIFI flash + * Returns: 0 on success, non-zero on failure + */ +int fpga_bridge_init(void); + +/* + * Read an FPGA register via SPI + * reg: Register number (1-5) + * Returns: Register value, or 0xFF if invalid register + * + * FPGA Register Map: + * Reg 1 (CTRL): DC_BLOCK(b0), QUARTER_SHIFT_EN(b1), QUARTER_SHIFT_UP(b2), PRBS(b6), TRIGGER_EN(b7) + * Reg 2 (RX_DECIM): Decimation ratio [2:0] + * Reg 3 (TX_CTRL): NCO_EN(b0) + * Reg 4 (TX_INTRP): Interpolation ratio [2:0] + * Reg 5 (TX_PSTEP): NCO phase step [7:0] + */ +uint8_t fpga_debug_register_read(uint8_t reg); + +/* + * Write an FPGA register via SPI + * reg: Register number (1-5) + * value: Value to write + */ +void fpga_debug_register_write(uint8_t reg, uint8_t value); + +#endif /* PRALINE */ + +#ifdef __cplusplus +} +#endif + +#endif /* __FPGA_BRIDGE_H__ */ diff --git a/firmware/common/baseband_sgpio.cpp b/firmware/common/baseband_sgpio.cpp index 611d8aaa9..9d6b78131 100644 --- a/firmware/common/baseband_sgpio.cpp +++ b/firmware/common/baseband_sgpio.cpp @@ -189,6 +189,7 @@ constexpr uint32_t gpio_outreg(const Direction direction) { constexpr uint32_t gpio_oenreg(const Direction direction) { return (0U << PIN_P78) | (0U << PIN_P81) | (0U << PIN_SYNC_EN) | (0U << PIN_INVERT) | (1U << PIN_DIRECTION) | (1U << PIN_DISABLE) | (0U << PIN_CAPTURE) | (0U << PIN_CLKIN) | ((direction == Direction::Transmit) ? 0xffU : 0x00U); + // ^^^^^^^^^^^^^ REVERTED: SGPIO14 must be DISABLED - enabling it blocks data capture! } constexpr uint32_t out_mux_cfg(const P_OUT_CFG out, const P_OE_CFG oe) { @@ -198,13 +199,23 @@ constexpr uint32_t out_mux_cfg(const P_OUT_CFG out, const P_OE_CFG oe) { constexpr uint32_t data_sgpio_mux_cfg( const CONCAT_ENABLE concat_enable, const CONCAT_ORDER concat_order) { +#ifndef PRALINE return (1U << 0) | (0U << 1) | (0U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (toUType(concat_enable) << 11) | (toUType(concat_order) << 12); +#else + return (1U << 0) | (0U << 1) | (3U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (toUType(concat_enable) << 11) | (toUType(concat_order) << 12); + // Bits 3-4: CLK_SOURCE_SLICE_MODE = 3 (slice D as clock source for data slices) +#endif } constexpr uint32_t data_slice_mux_cfg( const PARALLEL_MODE parallel_mode, const CLK_CAPTURE_MODE clk_capture_mode) { +#ifndef PRALINE return (0U << 0) | (toUType(clk_capture_mode) << 1) | (1U << 2) | (0U << 3) | (0U << 4) | (toUType(parallel_mode) << 6) | (0U << 8); +#else + return (0U << 0) | (toUType(clk_capture_mode) << 1) | (1U << 2) | (0U << 3) | (1U << 4) | (toUType(parallel_mode) << 6) | (0U << 8); + // Bit 4 CLKGEN_MODE: 0=internal counter, 1=external clock (REQUIRED for PRALINE!) +#endif } constexpr uint32_t pos( @@ -264,28 +275,54 @@ void SGPIO::init() { void SGPIO::configure(const Direction direction) { disable_all_slice_counters(); +#ifndef PRALINE // Set data pins as input, temporarily. LPC_SGPIO->GPIO_OENREG = gpio_oenreg(Direction::Receive); // Now that data pins are inputs, safe to change CPLD direction. +#endif + + // HackRF reference: Set GPIO_OUTREG first (DISABLE=HIGH during config) LPC_SGPIO->GPIO_OUTREG = gpio_outreg(direction); +#ifdef PRALINE + // HackRF reference: Set GPIO_OENREG ONCE before OUT_MUX_CFG (not twice!) + LPC_SGPIO->GPIO_OENREG = gpio_oenreg(direction); + + // Now configure OUT_MUX_CFG (HackRF order: control pins first, then data pins) +#endif + LPC_SGPIO->OUT_MUX_CFG[8] = out_mux_cfg(P_OUT_CFG::DOUT_DOUTM1, P_OE_CFG::GPIO_OE); LPC_SGPIO->OUT_MUX_CFG[9] = out_mux_cfg(P_OUT_CFG::DOUT_DOUTM1, P_OE_CFG::GPIO_OE); + +#ifndef PRALINE + // OUT_MUX_CFG[10] NOT configured for PRALINE - breaks PRALINE (HOST_DISABLE signal) LPC_SGPIO->OUT_MUX_CFG[10] = out_mux_cfg(P_OUT_CFG::GPIO_OUT, P_OE_CFG::GPIO_OE); +#endif + LPC_SGPIO->OUT_MUX_CFG[11] = out_mux_cfg(P_OUT_CFG::GPIO_OUT, P_OE_CFG::GPIO_OE); +#ifndef PRALINE + // SGPIO 12 and 13 are NOT configured - not used per HackRF reference for PRALINE LPC_SGPIO->OUT_MUX_CFG[12] = out_mux_cfg(P_OUT_CFG::GPIO_OUT, P_OE_CFG::GPIO_OE); LPC_SGPIO->OUT_MUX_CFG[13] = out_mux_cfg(P_OUT_CFG::GPIO_OUT, P_OE_CFG::GPIO_OE); +#endif + LPC_SGPIO->OUT_MUX_CFG[14] = out_mux_cfg(P_OUT_CFG::DOUT_DOUTM1, P_OE_CFG::GPIO_OE); + +#ifndef PRALINE + // SGPIO 15 is NOT configured - not used per HackRF reference for PRALINE LPC_SGPIO->OUT_MUX_CFG[15] = out_mux_cfg(P_OUT_CFG::GPIO_OUT, P_OE_CFG::GPIO_OE); +#endif const auto data_out_mux_cfg = out_mux_cfg(data_p_out_cfg(slice_mode_multislice), P_OE_CFG::GPIO_OE); for (size_t i = 0; i < 8; i++) { LPC_SGPIO->OUT_MUX_CFG[i] = data_out_mux_cfg; } +#ifndef PRALINE // Now that output enable sources are set, enable data bus in correct direction. LPC_SGPIO->GPIO_OENREG = gpio_oenreg(direction); +#endif const auto slice_gpdma = Slice::H; @@ -293,7 +330,25 @@ void SGPIO::configure(const Direction direction) { const auto clk_capture_mode = data_clk_capture_mode(direction); const auto single_slice = !slice_mode_multislice; +#ifndef PRALINE uint32_t slice_enable_mask = 0; +#endif + +#ifdef PRALINE + // Configure slice D as clock generator (REQUIRED for PRALINE!) + // Reference: HackRF sgpio.c line 193 + const auto slice_d = toUType(Slice::D); + LPC_SGPIO->SGPIO_MUX_CFG[slice_d] = (1U << 0) | (0U << 1) | (0U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (0U << 11) | (0U << 12); + LPC_SGPIO->SLICE_MUX_CFG[slice_d] = (0U << 0) | (0U << 1) | (0U << 2) | (0U << 3) | (1U << 4) | (0U << 6) | (0U << 8); // CLKGEN_MODE=1 + LPC_SGPIO->PRESET[slice_d] = 0; + LPC_SGPIO->COUNT[slice_d] = 0; + LPC_SGPIO->POS[slice_d] = pos(0x1f, 0x1f); + LPC_SGPIO->REG[slice_d] = 0x11111111; + LPC_SGPIO->REG_SS[slice_d] = 0x11111111; + + uint32_t slice_enable_mask = (1U << slice_d); // Start with slice D enabled +#endif + for (size_t i = 0; i < slice_count; i++) { const auto slice = slice_order[i]; const auto slice_index = toUType(slice); diff --git a/firmware/common/hackrf_gpio.hpp b/firmware/common/hackrf_gpio.hpp index acbb820c3..588928dfa 100644 --- a/firmware/common/hackrf_gpio.hpp +++ b/firmware/common/hackrf_gpio.hpp @@ -41,8 +41,14 @@ constexpr GPIO gpio_led_tx = gpio[GPIO2_8]; constexpr GPIO gpio_og_1v8_enable = gpio[GPIO3_6]; constexpr GPIO gpio_r9_1v8_enable = gpio[GPIO2_9]; constexpr GPIO gpio_vregmode = gpio[GPIO3_7]; +#ifdef PRALINE +// PRALINE uses different power control pins +constexpr GPIO gpio_og_vaa_disable = gpio[GPIO4_1]; // PRALINE VAA disable (P8_1) +constexpr GPIO gpio_r9_vaa_disable = gpio[GPIO4_1]; // PRALINE VAA disable (P8_1) +#else constexpr GPIO gpio_og_vaa_disable = gpio[GPIO2_9]; constexpr GPIO gpio_r9_vaa_disable = gpio[GPIO3_6]; +#endif constexpr GPIO gpio_rx_mix_bp = gpio[GPIO2_12]; constexpr GPIO gpio_tx_mix_bp = gpio[GPIO2_11]; @@ -64,19 +70,80 @@ constexpr GPIO gpio_not_tx_amp_pwr = gpio[GPIO3_5]; constexpr GPIO gpio_rffc5072_resetx = gpio[GPIO2_14]; constexpr GPIO gpio_rffc5072_select = gpio[GPIO2_13]; +#ifdef PRALINE +constexpr GPIO gpio_rffc5072_clock = gpio[GPIO5_18]; +constexpr GPIO gpio_rffc5072_data = gpio[GPIO4_14]; +#else constexpr GPIO gpio_rffc5072_clock = gpio[GPIO5_6]; constexpr GPIO gpio_rffc5072_data = gpio[GPIO3_3]; +#endif +#ifdef PRALINE +constexpr GPIO gpio_max283x_select = gpio[GPIO6_28]; +#else constexpr GPIO gpio_max283x_select = gpio[GPIO0_15]; +#endif + +#ifdef PRALINE +// PRALINE uses MAX2831 transceiver with different control pins +constexpr GPIO gpio_max283x_enable = gpio[GPIO7_1]; // MAX2831 ENABLE (PE_1) +constexpr GPIO gpio_max2831_enable = gpio[GPIO7_1]; // Alias +constexpr GPIO gpio_max2831_rx_enable = gpio[GPIO7_2]; // MAX2831 RX_ENABLE (PE_2) +constexpr GPIO gpio_max2831_rxhp = gpio[GPIO6_29]; // MAX2831 RXHP (PD_15) +constexpr GPIO gpio_max2831_ld = gpio[GPIO4_11]; // MAX2831 Lock Detect (P9_6) +// Legacy aliases for code compatibility +constexpr GPIO gpio_max2837_rxenable = gpio[GPIO7_2]; +constexpr GPIO gpio_max2837_txenable = gpio[GPIO7_2]; // MAX2831 uses single RX/TX control +constexpr GPIO gpio_max2839_rxtx = gpio[GPIO7_2]; +#else constexpr GPIO gpio_max283x_enable = gpio[GPIO2_6]; constexpr GPIO gpio_max2837_rxenable = gpio[GPIO2_5]; constexpr GPIO gpio_max2837_txenable = gpio[GPIO2_4]; constexpr GPIO gpio_max2839_rxtx = gpio[GPIO2_5]; +#endif +#ifdef PRALINE +constexpr GPIO gpio_max5864_select = gpio[GPIO6_30]; +constexpr GPIO gpio_fpga_select = gpio[GPIO2_10]; // FPGA SPI CS (P5_1) +#else constexpr GPIO gpio_max5864_select = gpio[GPIO2_7]; +#endif constexpr GPIO gpio_q_invert = gpio[GPIO0_13]; +#ifdef PRALINE +// PRALINE power control +constexpr GPIO gpio_vaa_disable = gpio[GPIO4_1]; // VAA disable (P8_1) +constexpr GPIO gpio_1v2_enable = gpio[GPIO4_7]; // 1V2 enable (P8_7) +constexpr GPIO gpio_3v3aux_disable = gpio[GPIO5_15]; // 3V3 aux disable (P6_7) + +// PRALINE RF path control +constexpr GPIO gpio_tx_enable = gpio[GPIO3_4]; // TX enable (P6_5) +constexpr GPIO gpio_mix_enable_n = gpio[GPIO3_2]; // Mixer enable inverted (P6_3) +constexpr GPIO gpio_lpf_enable = gpio[GPIO4_8]; // LPF enable (PA_1) +constexpr GPIO gpio_rf_amp_enable = gpio[GPIO4_9]; // RF amp enable (PA_2) +constexpr GPIO gpio_ant_bias_disable = gpio[GPIO1_12]; // Antenna bias disable (P2_12) + +// PRALINE mixer lock detect (gpio_max2831_ld defined above at line 93) +constexpr GPIO gpio_rffc5072_ld = gpio[GPIO6_25]; // Mixer lock detect (PD_11) + +// PRALINE LED4 +constexpr GPIO gpio_led4 = gpio[GPIO4_6]; // LED4 (P8_6) + +// PRALINE dual port control +constexpr GPIO gpio_p1_ctrl0 = gpio[GPIO0_14]; // P1 control 0 (P2_10) +constexpr GPIO gpio_p1_ctrl1 = gpio[GPIO5_16]; // P1 control 1 (P6_8) +constexpr GPIO gpio_p1_ctrl2 = gpio[GPIO3_5]; // P1 control 2 (P6_9) +constexpr GPIO gpio_p2_ctrl0 = gpio[GPIO7_3]; // P2 control 0 (PE_3) +constexpr GPIO gpio_p2_ctrl1 = gpio[GPIO7_4]; // P2 control 1 (PE_4) +constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15]; // CLKIN control (P1_20) + +// PRALINE trigger/sync I/O +constexpr GPIO gpio_trigger_in = gpio[GPIO6_26]; // Trigger input (PD_12) +constexpr GPIO gpio_trigger_out = gpio[GPIO5_6]; // Trigger output (P2_6) +constexpr GPIO gpio_pps_out = gpio[GPIO5_5]; // PPS output (P2_5) +#endif + constexpr GPIO gpio_cpld_tdo = gpio[GPIO5_18]; constexpr GPIO gpio_cpld_tck = gpio[GPIO3_0]; constexpr GPIO gpio_cpld_tms = gpio[GPIO3_4]; diff --git a/firmware/common/hackrf_hal.hpp b/firmware/common/hackrf_hal.hpp index 2798a4ad1..efd905c14 100644 --- a/firmware/common/hackrf_hal.hpp +++ b/firmware/common/hackrf_hal.hpp @@ -62,8 +62,16 @@ constexpr size_t clock_generator_output_og_codec = 0; constexpr size_t clock_generator_output_og_cpld = 1; constexpr size_t clock_generator_output_og_sgpio = 2; constexpr size_t clock_generator_output_og_clkout = 3; +#ifdef PRALINE +/* PRALINE has swapped CLK4/CLK5 vs HackRF One OG: + * CLK4 = XCVR_CLK (MAX2831) = second IF + * CLK5 = MIX_CLK (RFFC5072) = first IF */ +constexpr size_t clock_generator_output_og_first_if = 5; // RFFC5072 on CLK5 +constexpr size_t clock_generator_output_og_second_if = 4; // MAX2831 on CLK4 +#else constexpr size_t clock_generator_output_og_first_if = 4; constexpr size_t clock_generator_output_og_second_if = 5; +#endif constexpr size_t clock_generator_output_og_mcu_clkin = 7; constexpr size_t clock_generator_output_r9_if = 0; diff --git a/firmware/common/i2cdevmanager.hpp b/firmware/common/i2cdevmanager.hpp index 617ee8706..3690da5d0 100644 --- a/firmware/common/i2cdevmanager.hpp +++ b/firmware/common/i2cdevmanager.hpp @@ -110,4 +110,4 @@ class I2CDevManager { }; }; // namespace i2cdev -#endif \ No newline at end of file +#endif diff --git a/firmware/common/led.hpp b/firmware/common/led.hpp index ed62dfa27..9c93400a6 100644 --- a/firmware/common/led.hpp +++ b/firmware/common/led.hpp @@ -30,17 +30,30 @@ struct LED { } void setup() const { +#ifdef PRALINE + /* PRALINE LEDs are active-low (GPIO LOW = LED ON) */ + _gpio.set(); /* Start with LED OFF (HIGH) */ +#else _gpio.clear(); +#endif _gpio.output(); _gpio.configure(); } void on() const { +#ifdef PRALINE + _gpio.clear(); /* LOW = ON for PRALINE */ +#else _gpio.set(); +#endif } void off() const { +#ifdef PRALINE + _gpio.set(); /* HIGH = OFF for PRALINE */ +#else _gpio.clear(); +#endif } void toggle() const { @@ -48,7 +61,11 @@ struct LED { } void write(const bool value) const { +#ifdef PRALINE + _gpio.write(!value); /* Invert for PRALINE */ +#else _gpio.write(value); +#endif } private: diff --git a/firmware/common/pins.hpp b/firmware/common/pins.hpp index ca45d1bea..15fb1b285 100644 --- a/firmware/common/pins.hpp +++ b/firmware/common/pins.hpp @@ -109,6 +109,29 @@ enum Pins { P7_7, P9_5, P9_6, +#ifdef PRALINE + P9_2, + // Port A pins (PRALINE RF path control) + PA_1, + PA_2, + // Port 8 pins (PRALINE power control and LED4) + P8_1, + P8_6, + P8_7, + // Port E pins (PRALINE transceiver control) + PE_1, + PE_2, + PE_3, + PE_4, + // Port D pins (PRALINE - GPIO-only high-drive pins) + // These use SCU_SFSPD registers, not normal SFSP + // SCU configuration must be done separately in board.cpp + PD_11, + PD_12, + PD_14, + PD_15, + PD_16, +#endif PF_4, CLK0, CLK2, @@ -197,6 +220,28 @@ constexpr Pin pins[]{ [P7_7] = {7, 7}, [P9_5] = {9, 5}, [P9_6] = {9, 6}, +#ifdef PRALINE + [P9_2] = {9, 2}, + // Port A pins (function 0 = GPIO) + [PA_1] = {0xA, 1}, + [PA_2] = {0xA, 2}, + // Port 8 pins + [P8_1] = {8, 1}, + [P8_6] = {8, 6}, + [P8_7] = {8, 7}, + // Port E pins (function 4 = GPIO) + [PE_1] = {0xE, 1}, + [PE_2] = {0xE, 2}, + [PE_3] = {0xE, 3}, + [PE_4] = {0xE, 4}, + // Port D pins - GPIO-only, no pin mux needed + // Using 0xD as marker for Port D (SCU_SFSPD registers) + [PD_11] = {0xD, 11}, + [PD_12] = {0xD, 12}, + [PD_14] = {0xD, 14}, + [PD_15] = {0xD, 15}, + [PD_16] = {0xD, 16}, +#endif [PF_4] = {15, 4}, [CLK0] = {24, 0}, [CLK2] = {24, 2}, @@ -209,7 +254,8 @@ enum GPIOs { GPIO0_3, GPIO0_4, GPIO0_5, - /*GPIO0_6,*/ GPIO0_7, + /*GPIO0_6,*/ + GPIO0_7, GPIO0_8, GPIO0_9, GPIO0_10, @@ -275,12 +321,35 @@ enum GPIOs { GPIO5_7, GPIO5_8, GPIO5_9, - /*GPIO5_10, GPIO5_11,*/ GPIO5_12, + /*GPIO5_10, GPIO5_11,*/ + GPIO5_12, GPIO5_13, GPIO5_14, GPIO5_15, GPIO5_16, GPIO5_18, + // PRALINE-specific GPIOs (always defined to avoid compile-time array gaps) + // RFFC5072 mixer data + GPIO4_14, + // Power control and LED4 + GPIO4_1, + GPIO4_6, + GPIO4_7, + // RF path control + GPIO4_8, + GPIO4_9, + // Transceiver (MAX2831) control + GPIO7_1, + GPIO7_2, + // Dual port control + GPIO7_3, + GPIO7_4, + // GPIO6 (Port D pins) + GPIO6_25, + GPIO6_26, + GPIO6_28, + GPIO6_29, + GPIO6_30, }; constexpr GPIO gpio[] = { @@ -372,6 +441,41 @@ constexpr GPIO gpio[] = { [GPIO5_15] = {pins[P6_7], 5, 15, 4}, [GPIO5_16] = {pins[P6_8], 5, 16, 4}, [GPIO5_18] = {pins[P9_5], 5, 18, 4}, +// PRALINE-specific GPIOs (use placeholder pins on non-PRALINE builds) +#ifdef PRALINE + [GPIO4_14] = {pins[P9_2], 4, 14, 0}, // RFFC5072 mixer data + [GPIO4_1] = {pins[P8_1], 4, 1, 0}, // VAA disable + [GPIO4_6] = {pins[P8_6], 4, 6, 0}, // LED4 + [GPIO4_7] = {pins[P8_7], 4, 7, 0}, // 1V2 enable + [GPIO4_8] = {pins[PA_1], 4, 8, 0}, // LPF enable + [GPIO4_9] = {pins[PA_2], 4, 9, 0}, // RF amp enable + [GPIO7_1] = {pins[PE_1], 7, 1, 4}, // MAX2831 enable + [GPIO7_2] = {pins[PE_2], 7, 2, 4}, // MAX2831 RX enable + [GPIO7_3] = {pins[PE_3], 7, 3, 4}, // P2 port control 0 + [GPIO7_4] = {pins[PE_4], 7, 4, 4}, // P2 port control 1 + [GPIO6_25] = {pins[PD_11], 6, 25, 4}, // Mixer lock detect + [GPIO6_26] = {pins[PD_12], 6, 26, 4}, // Trigger input + [GPIO6_28] = {pins[PD_14], 6, 28, 4}, // MAX283x chip select + [GPIO6_29] = {pins[PD_15], 6, 29, 4}, // MAX2831 RXHP + [GPIO6_30] = {pins[PD_16], 6, 30, 4}, // MAX5864 chip select +#else + // Placeholder entries for non-PRALINE builds (use P0_0 as dummy) + [GPIO4_14] = {pins[P0_0], 4, 14, 0}, + [GPIO4_1] = {pins[P0_0], 4, 1, 0}, + [GPIO4_6] = {pins[P0_0], 4, 6, 0}, + [GPIO4_7] = {pins[P0_0], 4, 7, 0}, + [GPIO4_8] = {pins[P0_0], 4, 8, 0}, + [GPIO4_9] = {pins[P0_0], 4, 9, 0}, + [GPIO7_1] = {pins[P0_0], 7, 1, 0}, + [GPIO7_2] = {pins[P0_0], 7, 2, 0}, + [GPIO7_3] = {pins[P0_0], 7, 3, 0}, + [GPIO7_4] = {pins[P0_0], 7, 4, 0}, + [GPIO6_25] = {pins[P0_0], 6, 25, 0}, + [GPIO6_26] = {pins[P0_0], 6, 26, 0}, + [GPIO6_28] = {pins[P0_0], 6, 28, 0}, + [GPIO6_29] = {pins[P0_0], 6, 29, 0}, + [GPIO6_30] = {pins[P0_0], 6, 30, 0}, +#endif }; } // namespace lpc43xx diff --git a/firmware/common/portapack_cpld_data.hpp b/firmware/common/portapack_cpld_data.hpp index 04165344a..52278ec52 100644 --- a/firmware/common/portapack_cpld_data.hpp +++ b/firmware/common/portapack_cpld_data.hpp @@ -51,6 +51,17 @@ const Config config{block_0, block_1}; } /* namespace rev_20170522 */ +#ifdef PRALINE +namespace rev_h4m { + +extern const std::array block_0; +extern const std::array block_1; + +const Config config{block_0, block_1}; + +} /* namespace rev_h4m */ +#endif + } /* namespace cpld */ } /* namespace portapack */ diff --git a/firmware/common/portapack_shared_memory.hpp b/firmware/common/portapack_shared_memory.hpp index e6a2cc906..5a3752213 100644 --- a/firmware/common/portapack_shared_memory.hpp +++ b/firmware/common/portapack_shared_memory.hpp @@ -85,6 +85,14 @@ struct SharedMemory { uint16_t volatile m4_stack_usage{0}; uint32_t volatile m4_heap_usage{0}; uint16_t volatile m4_buffer_missed{0}; + +#ifdef PRALINE + // Phase 0 instrumentation counters for PRALINE radio debugging + uint32_t volatile m4_dma_xfr_count{0}; // DMA transfer_complete() calls + uint32_t volatile m4_dma_wait_count{0}; // wait_for_buffer() calls + uint32_t volatile m4_baseband_loops{0}; // Main loop iterations + uint8_t volatile m4_streaming_marker{0}; // Proves streaming_enable() called +#endif }; extern SharedMemory& shared_memory; diff --git a/firmware/flashsize.h b/firmware/flashsize.h index dadfa980d..5728d18e4 100644 --- a/firmware/flashsize.h +++ b/firmware/flashsize.h @@ -3,7 +3,7 @@ // clang-format off //Allowed fw size in MB -#define FLASH_SIZE_MB 2 +#define FLASH_SIZE_MB 4 //Current compiled fw size in MB -#define FLASH_SIZE_LIMIT_MB 1 +#define FLASH_SIZE_LIMIT_MB 3.5 // clang-format on diff --git a/firmware/tools/append_fpga_bitstream.py b/firmware/tools/append_fpga_bitstream.py new file mode 100644 index 000000000..df1d47bb0 --- /dev/null +++ b/firmware/tools/append_fpga_bitstream.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# +# Append FPGA bitstream to PRALINE firmware +# +# This script takes a PortaPack firmware.bin and appends the PRALINE FPGA +# bitstream at offset 0x100000 (1MB), then recalculates the checksum. +# +# Usage: append_fpga_bitstream.py +# + +import sys +import os + +def read_image(path): + with open(path, 'rb') as f: + return bytearray(f.read()) + +def write_image(data, path): + with open(path, 'wb') as f: + f.write(data) + +def main(): + if len(sys.argv) != 5: + print("Usage: append_fpga_bitstream.py ") + print(" firmware.bin: Input PortaPack firmware (1MB with checksum)") + print(" fpga.bin: PRALINE FPGA bitstream (LZ4 compressed)") + print(" output.bin: Output combined firmware") + print(" total_size: Total flash size in bytes (e.g., 2097152 for 2MB)") + sys.exit(1) + + firmware_path = sys.argv[1] + fpga_path = sys.argv[2] + output_path = sys.argv[3] + total_size = int(sys.argv[4], 0) + + # FPGA bitstream offset in flash + # PRALINE: Moved to 3.5MB to allow larger base firmware + FPGA_OFFSET = 0x380000 # 3.5MB (was 0x180000 = 1.5MB) + + # Read input files + firmware = read_image(firmware_path) + fpga = read_image(fpga_path) + + print(f"Firmware size: {len(firmware)} bytes") + print(f"FPGA bitstream size: {len(fpga)} bytes") + print(f"Target total size: {total_size} bytes") + + # The firmware has a 4-byte checksum at the end + # Remove it - we'll recalculate after adding FPGA bitstream + firmware_content = firmware[:-4] + + # Check that firmware content fits before FPGA offset + if len(firmware_content) > FPGA_OFFSET: + print(f"ERROR: Firmware content ({len(firmware_content)} bytes) exceeds FPGA offset ({FPGA_OFFSET})") + sys.exit(1) + + # Create output image + output = bytearray() + + # Add firmware content (padded to FPGA_OFFSET with 0xFF) + output.extend(firmware_content) + pad_size = FPGA_OFFSET - len(firmware_content) + output.extend(bytes([0xFF] * pad_size)) + + print(f"Padded firmware to {len(output)} bytes (FPGA offset)") + + # Add FPGA bitstream + output.extend(fpga) + print(f"Added FPGA bitstream, total now {len(output)} bytes") + + # Check total size + if len(output) > total_size - 4: + print(f"ERROR: Combined image ({len(output)} bytes) exceeds max size ({total_size - 4} bytes)") + sys.exit(1) + + # Pad to total_size - 4 (leaving room for checksum) + pad_size = total_size - 4 - len(output) + output.extend(bytes([0xFF] * pad_size)) + print(f"Padded to {len(output)} bytes (before checksum)") + + # Calculate checksum (same algorithm as make_spi_image.py) + checksum = 0 + for i in range(0, len(output), 4): + snippet = output[i:i + 4] + if len(snippet) == 4: + val = int.from_bytes(snippet, byteorder='little') + checksum += val + + final_checksum = 0 + checksum = (final_checksum - checksum) & 0xFFFFFFFF + output.extend(checksum.to_bytes(4, 'little')) + + print(f"Final image size: {len(output)} bytes") + print(f"Checksum: 0x{checksum:08X}") + + # Write output + write_image(output, output_path) + print(f"Written to {output_path}") + + # Report space usage + fpga_end = FPGA_OFFSET + len(fpga) + space_after_fpga = total_size - 4 - fpga_end + print(f"\nSpace usage:") + print(f" Firmware: 0x000000 - 0x{len(firmware_content):06X} ({len(firmware_content)} bytes)") + print(f" FPGA: 0x{FPGA_OFFSET:06X} - 0x{fpga_end:06X} ({len(fpga)} bytes)") + print(f" Free: 0x{fpga_end:06X} - 0x{total_size-4:06X} ({space_after_fpga} bytes)") + +if __name__ == "__main__": + main()