├── rust-toolchain.toml ├── src ├── flipper_serprog.icon └── main.rs ├── .github ├── dependabot.yml └── workflows │ ├── dependabot.yml │ └── build.yml ├── Cargo.toml ├── .gitignore ├── .cargo └── config ├── LICENSE └── README.md /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" -------------------------------------------------------------------------------- /src/flipper_serprog.icon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psychotropos/flipper_serprog/HEAD/src/flipper_serprog.icon -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flipper_serprog" 3 | version = "0.1.0" 4 | edition = "2021" 5 | autobins = false 6 | autoexamples = false 7 | autotests = false 8 | autobenches = false 9 | 10 | [[bin]] 11 | name = "flipper_serprog" 12 | bench = false 13 | test = false 14 | 15 | [dependencies] 16 | flipperzero = "0.15.0" 17 | flipperzero-alloc = "0.15.0" 18 | flipperzero-rt = "0.15.0" 19 | flipperzero-sys = "0.15.0" 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Generated by Cargo 4 | # will have compiled files and executables 5 | debug/ 6 | target/ 7 | 8 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 9 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 10 | Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | 15 | # MSVC Windows builds of rustc generate these, which store debugging information 16 | *.pdb 17 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Enable auto-merge for Dependabot PRs 14 | run: gh pr merge --auto --merge "$PR_URL" 15 | env: 16 | PR_URL: ${{github.event.pull_request.html_url}} 17 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 18 | 19 | -------------------------------------------------------------------------------- /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.thumbv7em-none-eabihf] 2 | rustflags = [ 3 | # CPU is Cortex-M4 (STM32WB55) 4 | "-C", "target-cpu=cortex-m4", 5 | 6 | # Size optimizations 7 | "-C", "panic=abort", 8 | "-C", "debuginfo=0", 9 | "-C", "opt-level=z", 10 | 11 | # LTO helps reduce binary size 12 | "-C", "embed-bitcode=yes", 13 | "-C", "lto=yes", 14 | 15 | # Linker flags for relocatable binary 16 | "-C", "link-args=--script=flipperzero-rt.ld --Bstatic --relocatable --discard-all --strip-all --lto-O3 --lto-whole-program-visibility", 17 | 18 | # Required to link with `lld` 19 | "-Z", "no-unique-section-names=yes", 20 | ] 21 | 22 | [build] 23 | target = "thumbv7em-none-eabihf" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Ioannis Profetis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flipper_serprog 2 | A serprog-based SPI programmer to be used in conjuction with flashrom for the Flipper Zero. 3 | 4 | # Wiring guide 5 | PA6 -> MISO 6 | PA7 -> MOSI 7 | PA4 -> CS 8 | PB3 -> SCK 9 | 3.3V -> VCC 10 | 11 | Note that depending on your SPI chip, additional pins might need to be held high (ideally via pull-up resistors, or bridged to the 3v3 rail). This applies to i.e. the /HOLD and /WP pins in Winbond chips. 12 | 13 | If the target device is attached to via a SOP8 clip, it is recommended that the device is attached and the connections verified while the Flipper Zero is turned off. Fluctuations in the power use of the 3v3 rail on the Flipper Zero might otherwise cause issues (such as the SD card interface being unavailable until the device is restarted). 14 | 15 | # Usage 16 | Use flashrom to interface with the Flipper Zero via USB. Note that flipper_serprog makes use of the secondary VCP channel by default, meaning that the serprog interface will appear under a different tty number on the Flipper. 17 | 18 | This behaviour can be amended by changing "USB_VCP_CHANNEL" in the source code to 0 and re-compiling. Do note that the standard Flipper RPC interface will be unavailable while flipper_serprog is running if this is done. 19 | 20 | Example: 21 | flashrom -p serprog:dev=/dev/tty.usbmodemflip_3 -r spi.bin 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build FAP 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | id: checkout 12 | uses: actions/checkout@v3 13 | 14 | - name: Install latest nightly 15 | uses: actions-rs/toolchain@v1 16 | id: install_latest_nightly 17 | with: 18 | toolchain: nightly 19 | override: true 20 | target: thumbv7em-none-eabihf 21 | 22 | - name: Build binary 23 | uses: actions-rs/cargo@v1 24 | id: build_library 25 | with: 26 | command: build 27 | args: --release 28 | 29 | - name: Tag Release 30 | id: tag_release 31 | uses: mathieudutour/github-tag-action@v6.1 32 | with: 33 | github_token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Create Release 36 | id: create_release 37 | uses: actions/create-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | tag_name: ${{ steps.tag_release.outputs.new_tag }} 42 | release_name: Release ${{ steps.tag_release.outputs.new_tag }} 43 | body: ${{ steps.tag_release.outputs.changelog }} 44 | draft: false 45 | prerelease: false 46 | 47 | - name: Upload Release Asset 48 | id: upload_release_asset 49 | uses: actions/upload-release-asset@v1 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | upload_url: ${{ steps.create_release.outputs.upload_url }} 54 | asset_path: ./target/thumbv7em-none-eabihf/release/flipper_serprog 55 | asset_name: flipper_serprog.fap 56 | asset_content_type: application/octet-stream 57 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![crate_type = "staticlib"] 2 | #![no_main] 3 | #![no_std] 4 | 5 | use core::ffi::*; 6 | use core::mem::*; 7 | use core::time::Duration; 8 | 9 | extern crate flipperzero_rt; 10 | extern crate flipperzero_alloc; 11 | extern crate alloc; 12 | 13 | use alloc::string::String; 14 | use alloc::vec::Vec; 15 | use flipperzero_sys::*; 16 | use flipperzero_rt::{entry, manifest}; 17 | 18 | manifest!(name = "Serial Flash Programmer", app_version = 1, has_icon = true, icon = "flipper_serprog.icon"); 19 | entry!(main); 20 | 21 | struct SerprogData { 22 | view_port: *mut ViewPort, 23 | event_queue: *mut FuriMessageQueue, 24 | worker_thread: *mut FuriThread, 25 | trx_thread: *mut FuriThread, 26 | rx_stream: *mut FuriStreamBuffer, 27 | tx_stream: *mut FuriStreamBuffer, 28 | prescaler_value: u32, 29 | running: bool, 30 | } 31 | 32 | const RECORD_GUI: *const c_char = "gui\0".as_ptr(); 33 | const RECORD_CLI_VCP: *const c_char = "cli_vcp\0".as_ptr(); 34 | 35 | #[allow(dead_code, non_camel_case_types)] 36 | const S_ACK: u8 = 0x06; 37 | const S_NAK: u8 = 0x15; 38 | 39 | const CDC_DATA_SZ: usize = 0x40; 40 | 41 | // From flashrom: 42 | const BUS_SPI: u8 = 1 << 3; 43 | 44 | const USB_VCP_CHANNEL: u8 = 1; 45 | 46 | const FURI_FLAG_WAIT_FOREVER: u32 = 0xFFFFFFFF; 47 | const FURI_FLAG_ERROR: u32 = 0x80000000; 48 | const FURI_FLAG_WAIT_ANY: u32 = 0; 49 | 50 | #[repr(u32)] 51 | #[allow(non_camel_case_types)] 52 | enum CR1Bits { 53 | SPI_CR1_BR = 0x0038, 54 | SPI_CR1_BR_0 = 0x0008, 55 | SPI_CR1_BR_1 = 0x0010, 56 | SPI_CR1_BR_2 = 0x0020, 57 | } 58 | 59 | #[repr(u32)] 60 | #[allow(dead_code, non_camel_case_types)] 61 | // External SPI is on APB2, so the base frequency is 64MHz. The comments reflect this. 62 | enum PrescalerValues { 63 | LL_SPI_BAUDRATEPRESCALER_DIV2 = 0, 64 | /* 32MHz */ 65 | LL_SPI_BAUDRATEPRESCALER_DIV4 = CR1Bits::SPI_CR1_BR_0 as u32, 66 | /* 16MHz */ 67 | LL_SPI_BAUDRATEPRESCALER_DIV8 = CR1Bits::SPI_CR1_BR_1 as u32, 68 | /* 8MHz */ 69 | LL_SPI_BAUDRATEPRESCALER_DIV16 = (CR1Bits::SPI_CR1_BR_1 as u32 | CR1Bits::SPI_CR1_BR_0 as u32), 70 | /* 4MHz */ 71 | LL_SPI_BAUDRATEPRESCALER_DIV32 = CR1Bits::SPI_CR1_BR_2 as u32, 72 | /* 2MHz */ 73 | LL_SPI_BAUDRATEPRESCALER_DIV64 = (CR1Bits::SPI_CR1_BR_2 as u32 | CR1Bits::SPI_CR1_BR_0 as u32), 74 | /* 1MHz */ 75 | LL_SPI_BAUDRATEPRESCALER_DIV128 = (CR1Bits::SPI_CR1_BR_2 as u32 | CR1Bits::SPI_CR1_BR_1 as u32), 76 | /* 500KHz */ 77 | LL_SPI_BAUDRATEPRESCALER_DIV256 = (CR1Bits::SPI_CR1_BR_2 as u32 78 | | CR1Bits::SPI_CR1_BR_1 as u32 79 | | CR1Bits::SPI_CR1_BR_0 as u32), 80 | /* 250KHz */ 81 | } 82 | 83 | #[repr(u32)] 84 | #[allow(dead_code, non_camel_case_types)] 85 | enum PrescalerValuesInHz { 86 | LL_SPI_BAUDRATEPRESCALER_DIV2 = 32000000, 87 | /* 32MHz */ 88 | LL_SPI_BAUDRATEPRESCALER_DIV4 = 16000000, 89 | /* 16MHz */ 90 | LL_SPI_BAUDRATEPRESCALER_DIV8 = 8000000, 91 | /* 8MHz */ 92 | LL_SPI_BAUDRATEPRESCALER_DIV16 = 4000000, 93 | /* 4MHz */ 94 | LL_SPI_BAUDRATEPRESCALER_DIV32 = 2000000, 95 | /* 2MHz */ 96 | LL_SPI_BAUDRATEPRESCALER_DIV64 = 1000000, 97 | /* 1MHz */ 98 | LL_SPI_BAUDRATEPRESCALER_DIV128 = 500000, 99 | /* 500KHz */ 100 | LL_SPI_BAUDRATEPRESCALER_DIV256 = 250000, 101 | /* 250KHz */ 102 | } 103 | 104 | #[repr(u8)] 105 | #[allow(dead_code, non_camel_case_types)] 106 | enum SerprogCommands { 107 | S_CMD_NOP = 0x00, 108 | S_CMD_Q_IFACE = 0x01, 109 | S_CMD_Q_CMDMAP = 0x02, 110 | S_CMD_Q_PGMNAME = 0x03, 111 | S_CMD_Q_SERBUF = 0x04, 112 | S_CMD_Q_BUSTYPE = 0x05, 113 | S_CMD_Q_CHIPSIZE = 0x06, 114 | S_CMD_Q_OPBUF = 0x07, 115 | S_CMD_Q_WRNMAXLEN = 0x08, 116 | S_CMD_R_BYTE = 0x09, 117 | S_CMD_R_NBYTES = 0x0A, 118 | S_CMD_O_INIT = 0x0B, 119 | S_CMD_O_WRITEB = 0x0C, 120 | S_CMD_O_WRITEN = 0x0D, 121 | S_CMD_O_DELAY = 0x0E, 122 | S_CMD_O_EXEC = 0x0F, 123 | S_CMD_SYNCNOP = 0x10, 124 | S_CMD_Q_RDNMAXLEN = 0x11, 125 | S_CMD_S_BUSTYPE = 0x12, 126 | S_CMD_O_SPIOP = 0x13, 127 | S_CMD_S_SPI_FREQ = 0x14, 128 | S_CMD_S_PIN_STATE = 0x15, 129 | } 130 | 131 | const SUPPORTED_COMMANDS: u32 = (1 << SerprogCommands::S_CMD_NOP as u32) 132 | | (1 << SerprogCommands::S_CMD_Q_IFACE as u32) 133 | | (1 << SerprogCommands::S_CMD_Q_CMDMAP as u32) 134 | | (1 << SerprogCommands::S_CMD_Q_PGMNAME as u32) 135 | | (1 << SerprogCommands::S_CMD_Q_SERBUF as u32) 136 | | (1 << SerprogCommands::S_CMD_Q_BUSTYPE as u32) 137 | | (1 << SerprogCommands::S_CMD_SYNCNOP as u32) 138 | | (1 << SerprogCommands::S_CMD_O_SPIOP as u32) 139 | | (1 << SerprogCommands::S_CMD_S_BUSTYPE as u32) 140 | | (1 << SerprogCommands::S_CMD_S_SPI_FREQ as u32); 141 | 142 | #[repr(u32)] 143 | enum WorkerEvents { 144 | CdcRx = (1 << 0), 145 | CdcStop = (1 << 1), 146 | CdcTxStream = (1 << 2), 147 | CdcTx = (1 << 3), 148 | } 149 | 150 | const WORKER_ALL_CDC_EVENTS: u32 = WorkerEvents::CdcRx as u32 151 | | WorkerEvents::CdcStop as u32 152 | | WorkerEvents::CdcTxStream as u32 153 | | WorkerEvents::CdcTx as u32; 154 | 155 | fn main(args: Option<&CStr>) -> i32 { 156 | unsafe { 157 | _entry(args) 158 | } 159 | } 160 | 161 | unsafe fn _entry(_args: Option<&CStr>) -> i32 { 162 | let mut state = SerprogData { 163 | view_port: view_port_alloc(), 164 | event_queue: furi_message_queue_alloc(8, size_of::() as u32), 165 | trx_thread: 0 as *mut FuriThread, 166 | worker_thread: 0 as *mut FuriThread, 167 | rx_stream: furi_stream_buffer_alloc(5 * CDC_DATA_SZ, 1), 168 | tx_stream: furi_stream_buffer_alloc(5 * CDC_DATA_SZ, 1), 169 | prescaler_value: PrescalerValues::LL_SPI_BAUDRATEPRESCALER_DIV32 as u32, 170 | running: true, 171 | }; 172 | 173 | let mut cdc_cb: CdcCallbacks = CdcCallbacks { 174 | tx_ep_callback: Some(vcp_on_cdc_tx_complete), 175 | rx_ep_callback: Some(vcp_on_cdc_rx), 176 | state_callback: Some(vcp_state_callback), 177 | ctrl_line_callback: Some(vcp_on_cdc_control_line), 178 | config_callback: Some(vcp_on_line_config), 179 | }; 180 | 181 | furi_hal_spi_bus_handle_init(&furi_hal_spi_bus_handle_external); 182 | furi_hal_usb_unlock(); 183 | 184 | if USB_VCP_CHANNEL == 0 { 185 | let cli_vcp = furi_record_open(RECORD_CLI_VCP) as *mut CliVcp; 186 | cli_vcp_disable(cli_vcp); 187 | furi_record_close(RECORD_CLI_VCP); 188 | } 189 | 190 | if !furi_hal_usb_set_config( 191 | if USB_VCP_CHANNEL == 0 { 192 | &mut usb_cdc_single as *mut FuriHalUsbInterface 193 | } else { 194 | &mut usb_cdc_dual as *mut FuriHalUsbInterface 195 | }, 196 | 0 as *mut c_void, 197 | ) { 198 | panic!("Failed to set USB config on init"); 199 | } 200 | 201 | furi_hal_cdc_set_callbacks( 202 | USB_VCP_CHANNEL, 203 | &mut cdc_cb as *mut CdcCallbacks, 204 | &mut state as *mut SerprogData as *mut c_void, 205 | ); 206 | 207 | state.worker_thread = furi_create_thread( 208 | "SerprogUsbProcThread\0".into(), 209 | 2048, 210 | &mut state as *mut SerprogData as *mut c_void, 211 | Some(usb_process_thread_callback), 212 | true, 213 | ); 214 | 215 | state.trx_thread = furi_create_thread( 216 | "SerprogUsbTRxThread\0".into(), 217 | 2048, 218 | &mut state as *mut SerprogData as *mut c_void, 219 | Some(usb_trx_thread_callback), 220 | false, 221 | ); 222 | 223 | furi_thread_flags_set( 224 | furi_thread_get_id(state.trx_thread), 225 | WorkerEvents::CdcRx as u32, 226 | ); 227 | 228 | furi_thread_start(state.trx_thread); 229 | 230 | view_port_draw_callback_set( 231 | state.view_port, 232 | Some(draw_callback), 233 | state.view_port as *mut c_void, 234 | ); 235 | view_port_input_callback_set(state.view_port, Some(input_callback), state.event_queue as *mut c_void); 236 | 237 | let gui: *mut Gui = furi_record_open(RECORD_GUI) as *mut Gui; 238 | gui_add_view_port(gui, state.view_port, GuiLayerFullscreen); 239 | 240 | let mut event: MaybeUninit = MaybeUninit::uninit(); 241 | 242 | while state.running { 243 | if furi_message_queue_get( 244 | state.event_queue, 245 | event.as_mut_ptr() as *mut c_void, 246 | 100, 247 | ) == FuriStatusOk 248 | { 249 | let event = event.assume_init(); 250 | if event.type_ == InputTypePress || event.type_ == InputTypeRepeat 251 | { 252 | #[allow(non_upper_case_globals)] 253 | match event.key { 254 | InputKeyBack => { 255 | state.running = false; 256 | break; 257 | } 258 | _ => (), 259 | } 260 | } 261 | } 262 | view_port_update(state.view_port); 263 | } 264 | 265 | furi_hal_cdc_set_callbacks(USB_VCP_CHANNEL, 0 as *mut CdcCallbacks, 0 as *mut c_void); 266 | 267 | furi_hal_usb_unlock(); 268 | if !furi_hal_usb_set_config( 269 | &mut usb_cdc_single as *mut FuriHalUsbInterface, 270 | 0 as *mut c_void, 271 | ) { 272 | panic!("Failed to reset USB config on destruction"); 273 | } 274 | 275 | if USB_VCP_CHANNEL == 0 { 276 | let cli = furi_record_open(RECORD_CLI_VCP) as *mut CliVcp; 277 | cli_vcp_enable(cli); 278 | furi_record_close(RECORD_CLI_VCP); 279 | } 280 | 281 | furi_thread_flags_set( 282 | furi_thread_get_id(state.trx_thread), 283 | WorkerEvents::CdcStop as u32, 284 | ); 285 | furi_thread_join(state.trx_thread); 286 | furi_thread_free(state.trx_thread); 287 | 288 | furi_thread_join(state.worker_thread); 289 | furi_thread_free(state.worker_thread); 290 | 291 | furi_stream_buffer_free(state.rx_stream); 292 | furi_stream_buffer_free(state.tx_stream); 293 | 294 | furi_hal_spi_bus_handle_deinit(&furi_hal_spi_bus_handle_external); 295 | 296 | view_port_enabled_set(state.view_port, false); 297 | gui_remove_view_port(gui, state.view_port); 298 | view_port_free(state.view_port); 299 | furi_message_queue_free(state.event_queue); 300 | 301 | furi_record_close(RECORD_GUI); 302 | 303 | 0 304 | } 305 | 306 | unsafe fn set_spi_baud_rate(handle: &FuriHalSpiBusHandle, prescaler_value: u32) { 307 | let bus = handle.bus.as_mut().unwrap(); 308 | let spi = bus.spi.as_mut().unwrap(); 309 | spi.CR1 = (spi.CR1 & !(CR1Bits::SPI_CR1_BR as u32)) | prescaler_value; 310 | } 311 | 312 | pub unsafe extern "C" fn input_callback( 313 | input_event: *mut InputEvent, 314 | event_queue: *mut c_void, 315 | ) { 316 | furi_message_queue_put( 317 | event_queue as *mut FuriMessageQueue, 318 | input_event as *mut c_void, 319 | FURI_FLAG_WAIT_FOREVER, 320 | ); 321 | } 322 | 323 | pub unsafe extern "C" fn draw_callback(canvas: *mut Canvas, _context: *mut c_void) { 324 | canvas_draw_str(canvas, 39, 31, "SPI Flasher\0".as_ptr()); 325 | } 326 | 327 | unsafe fn usb_process_packet(state: &mut SerprogData) { 328 | let mut data: [u8; CDC_DATA_SZ] = [0; CDC_DATA_SZ]; 329 | 330 | loop { 331 | let receive_length = 332 | furi_stream_buffer_receive(state.rx_stream, data.as_mut_ptr() as *mut c_void, 1, 0); 333 | if receive_length == 0 { 334 | // Nothing here for us - stop busy looping. 335 | // We will re-enter this once a CdcRx event is received by the processing thread anyway. 336 | return; 337 | } 338 | 339 | let _send_length = 0; 340 | 341 | let command: SerprogCommands = transmute(data[0]); 342 | 343 | let send_length: usize = (|| match command { 344 | SerprogCommands::S_CMD_NOP => { 345 | data[0] = S_ACK; 346 | return 1; 347 | } 348 | SerprogCommands::S_CMD_Q_IFACE => { 349 | data[0] = S_ACK; 350 | data[1] = 0x01; 351 | return 3; 352 | } 353 | SerprogCommands::S_CMD_Q_CMDMAP => { 354 | data[0] = S_ACK; 355 | *(data.as_mut_ptr().add(1) as *mut u32) = SUPPORTED_COMMANDS; 356 | return 33; 357 | } 358 | SerprogCommands::S_CMD_Q_PGMNAME => { 359 | data[0] = S_ACK; 360 | data[1] = b'f'; 361 | data[2] = b'u'; 362 | data[3] = b'r'; 363 | data[4] = b'i'; 364 | return 17; 365 | } 366 | SerprogCommands::S_CMD_Q_SERBUF => { 367 | data[0] = S_ACK; 368 | *(data.as_mut_ptr().add(1) as *mut u16) = 0xFFFF as u16; 369 | return 3; 370 | } 371 | SerprogCommands::S_CMD_Q_BUSTYPE => { 372 | data[0] = S_ACK; 373 | data[1] = BUS_SPI; 374 | return 2; 375 | } 376 | SerprogCommands::S_CMD_SYNCNOP => { 377 | data[0] = S_NAK; 378 | data[1] = S_ACK; 379 | return 2; 380 | } 381 | SerprogCommands::S_CMD_O_SPIOP => { 382 | if blocking_furi_stream_buffer_receive( 383 | state.rx_stream, 384 | data.as_mut_ptr() as *mut c_void, 385 | 6, 386 | 10, 387 | ) != 6 388 | { 389 | data[0] = S_NAK; 390 | return 1; 391 | } 392 | 393 | let write_length: usize = *(data.as_mut_ptr() as *const usize) & 0x00FFFFFF; 394 | let mut read_length: usize = 395 | *(data.as_mut_ptr().add(3) as *const usize) & 0x00FFFFFF; 396 | 397 | let mut read_buffer: Vec = Vec::with_capacity(read_length); 398 | read_buffer.set_len(read_length); 399 | 400 | let mut write_buffer: Vec = Vec::with_capacity(write_length); 401 | write_buffer.set_len(write_length); 402 | 403 | if blocking_furi_stream_buffer_receive( 404 | state.rx_stream, 405 | write_buffer.as_mut_ptr() as *mut c_void, 406 | write_length, 407 | 1000, 408 | ) != write_length 409 | { 410 | data[0] = S_NAK; 411 | return 1; 412 | } 413 | 414 | data[0] = S_ACK; 415 | signal_usb_cdc_send(&state, data.as_mut_ptr(), 1); 416 | 417 | furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external); 418 | 419 | if state.prescaler_value != PrescalerValues::LL_SPI_BAUDRATEPRESCALER_DIV32 as u32 { 420 | set_spi_baud_rate(&furi_hal_spi_bus_handle_external, state.prescaler_value); 421 | } 422 | 423 | furi_hal_spi_bus_tx( 424 | &furi_hal_spi_bus_handle_external, 425 | write_buffer.as_mut_ptr(), 426 | write_length, 427 | 5000, 428 | ); 429 | furi_hal_spi_bus_rx( 430 | &furi_hal_spi_bus_handle_external, 431 | read_buffer.as_mut_ptr(), 432 | read_length, 433 | 5000, 434 | ); 435 | furi_hal_spi_release( 436 | &furi_hal_spi_bus_handle_external, 437 | ); 438 | 439 | let mut read_index = 0; 440 | while read_length > 0 { 441 | let bytes_to_write = core::cmp::min(CDC_DATA_SZ, read_length); 442 | signal_usb_cdc_send( 443 | state, 444 | read_buffer[read_index..read_index + bytes_to_write].as_mut_ptr(), 445 | bytes_to_write, 446 | ); 447 | read_length -= bytes_to_write; 448 | read_index += bytes_to_write; 449 | } 450 | 451 | return 0; 452 | } 453 | SerprogCommands::S_CMD_S_BUSTYPE => { 454 | if blocking_furi_stream_buffer_receive( 455 | state.rx_stream, 456 | data.as_mut_ptr() as *mut c_void, 457 | 1, 458 | 10, 459 | ) != 1 460 | { 461 | data[0] = S_NAK; 462 | return 1; 463 | } 464 | 465 | data[0] = if (data[0] | BUS_SPI) == BUS_SPI { 466 | S_ACK 467 | } else { 468 | S_NAK 469 | }; 470 | return 1; 471 | } 472 | SerprogCommands::S_CMD_S_SPI_FREQ => { 473 | if blocking_furi_stream_buffer_receive( 474 | state.rx_stream, 475 | data.as_mut_ptr() as *mut c_void, 476 | 4, 477 | 10, 478 | ) != 4 479 | { 480 | data[0] = S_NAK; 481 | return 1; 482 | } 483 | 484 | let freq = *(data.as_ptr() as *const u32); 485 | if freq == 0 { 486 | data[0] = S_NAK; 487 | return 1; 488 | } else { 489 | let mut prescaler_value = PrescalerValues::LL_SPI_BAUDRATEPRESCALER_DIV256; 490 | let mut prescaler_value_in_hz = 491 | PrescalerValuesInHz::LL_SPI_BAUDRATEPRESCALER_DIV256; 492 | 493 | if freq >= 2000000 { 494 | prescaler_value = PrescalerValues::LL_SPI_BAUDRATEPRESCALER_DIV32; 495 | prescaler_value_in_hz = PrescalerValuesInHz::LL_SPI_BAUDRATEPRESCALER_DIV32; 496 | } else if freq >= 1000000 { 497 | prescaler_value = PrescalerValues::LL_SPI_BAUDRATEPRESCALER_DIV64; 498 | prescaler_value_in_hz = PrescalerValuesInHz::LL_SPI_BAUDRATEPRESCALER_DIV64; 499 | } else if freq >= 500000 { 500 | prescaler_value = PrescalerValues::LL_SPI_BAUDRATEPRESCALER_DIV128; 501 | prescaler_value_in_hz = 502 | PrescalerValuesInHz::LL_SPI_BAUDRATEPRESCALER_DIV128; 503 | } else if freq < 500000 { 504 | prescaler_value = PrescalerValues::LL_SPI_BAUDRATEPRESCALER_DIV256; 505 | prescaler_value_in_hz = 506 | PrescalerValuesInHz::LL_SPI_BAUDRATEPRESCALER_DIV256; 507 | } 508 | 509 | state.prescaler_value = prescaler_value as u32; 510 | data[0] = S_ACK; 511 | *(data.as_mut_ptr().add(1) as *mut u32) = prescaler_value_in_hz as u32; 512 | return 5; 513 | } 514 | } 515 | _ => { 516 | data[0] = S_NAK; 517 | return 1; 518 | } 519 | })(); 520 | 521 | if send_length > 0 { 522 | signal_usb_cdc_send(&state, data.as_mut_ptr(), send_length); 523 | } 524 | } 525 | } 526 | 527 | unsafe fn signal_usb_cdc_send(state: &SerprogData, data: *mut u8, length: usize) { 528 | furi_stream_buffer_send( 529 | state.tx_stream, 530 | data as *mut c_void, 531 | length, 532 | FURI_FLAG_WAIT_FOREVER, 533 | ); 534 | furi_thread_flags_set( 535 | furi_thread_get_id(state.trx_thread), 536 | WorkerEvents::CdcTxStream as u32, 537 | ); 538 | } 539 | 540 | unsafe fn blocking_furi_stream_buffer_receive( 541 | stream_buffer: *mut FuriStreamBuffer, 542 | data: *mut c_void, 543 | length: usize, 544 | timeout: usize, 545 | ) -> usize { 546 | let mut remaining = length; 547 | let mut index = 0; 548 | 549 | let timer = furi_hal_cortex_timer_get(Duration::from_millis(timeout as u64).as_micros() as u32); 550 | 551 | while remaining > 0 { 552 | let timer_clone: FuriHalCortexTimer = transmute_copy(&timer); 553 | if furi_hal_cortex_timer_is_expired(timer_clone) { 554 | furi_stream_buffer_reset(stream_buffer); 555 | return index; 556 | } 557 | 558 | let count = furi_stream_buffer_receive( 559 | stream_buffer, 560 | data.add(index), 561 | core::cmp::min(CDC_DATA_SZ, remaining), 562 | 0, 563 | ); 564 | remaining -= count; 565 | index += count; 566 | } 567 | 568 | length 569 | } 570 | 571 | pub unsafe extern "C" fn usb_process_thread_callback(context: *mut c_void) -> i32 { 572 | let state = (context as *mut SerprogData).as_mut().unwrap(); 573 | 574 | loop { 575 | let events = furi_thread_flags_wait( 576 | WORKER_ALL_CDC_EVENTS, 577 | FURI_FLAG_WAIT_ANY, 578 | FURI_FLAG_WAIT_FOREVER, 579 | ); 580 | if (events & FURI_FLAG_ERROR) > 0 { 581 | panic!("FuriFlagError set in usb_process_thread_callback's furi_thread_flags_wait"); 582 | } 583 | 584 | if (events & (WorkerEvents::CdcStop as u32)) > 0 { 585 | break; 586 | } 587 | 588 | if (events & (WorkerEvents::CdcRx as u32)) > 0 { 589 | usb_process_packet(state); 590 | } 591 | } 592 | 593 | 0 594 | } 595 | 596 | pub unsafe extern "C" fn usb_trx_thread_callback(context: *mut c_void) -> i32 { 597 | let mut data: [u8; CDC_DATA_SZ] = [0; CDC_DATA_SZ]; 598 | let mut tx_idle = true; 599 | let mut last_tx_pkt_length = 0; 600 | let state = (context as *mut SerprogData).as_ref().unwrap(); 601 | 602 | loop { 603 | let mut events = furi_thread_flags_wait( 604 | WORKER_ALL_CDC_EVENTS, 605 | FURI_FLAG_WAIT_ANY, 606 | FURI_FLAG_WAIT_FOREVER, 607 | ); 608 | if (events & FURI_FLAG_ERROR) > 0 { 609 | panic!("FuriFlagError set in usb_trx_thread_callback's furi_thread_flags_wait"); 610 | } 611 | 612 | if (events & (WorkerEvents::CdcStop as u32)) > 0 { 613 | furi_thread_flags_set( 614 | furi_thread_get_id(state.worker_thread), 615 | WorkerEvents::CdcStop as u32, 616 | ); 617 | break; 618 | } 619 | 620 | if (events & (WorkerEvents::CdcTxStream as u32)) > 0 { 621 | if tx_idle { 622 | events |= WorkerEvents::CdcTx as u32; 623 | } 624 | } 625 | 626 | if events & (WorkerEvents::CdcTx as u32) > 0 { 627 | let length = furi_stream_buffer_receive( 628 | state.tx_stream, 629 | data.as_mut_ptr() as *mut c_void, 630 | CDC_DATA_SZ, 631 | 0, 632 | ); 633 | if length > 0 { 634 | tx_idle = false; 635 | furi_hal_cdc_send(USB_VCP_CHANNEL, data.as_mut_ptr(), length as u16); 636 | last_tx_pkt_length = length; 637 | } else { 638 | if last_tx_pkt_length == 64 { 639 | furi_hal_cdc_send(USB_VCP_CHANNEL, 0 as *mut u8, 0); 640 | } else { 641 | tx_idle = true; 642 | } 643 | last_tx_pkt_length = 0; 644 | } 645 | } 646 | 647 | if (events & (WorkerEvents::CdcRx as u32)) > 0 { 648 | let length = 649 | furi_hal_cdc_receive(USB_VCP_CHANNEL, data.as_mut_ptr(), CDC_DATA_SZ as u16) 650 | as usize; 651 | if length > 0 { 652 | furi_stream_buffer_send( 653 | state.rx_stream, 654 | &data as *const u8 as *const c_void, 655 | length, 656 | FURI_FLAG_WAIT_FOREVER, 657 | ); 658 | 659 | furi_thread_flags_set( 660 | furi_thread_get_id(state.worker_thread), 661 | WorkerEvents::CdcRx as u32, 662 | ); 663 | } 664 | } 665 | } 666 | 667 | 0 668 | } 669 | 670 | pub unsafe extern "C" fn vcp_on_cdc_tx_complete(context: *mut c_void) { 671 | furi_thread_flags_set( 672 | furi_thread_get_id((*(context as *mut SerprogData)).trx_thread), 673 | WorkerEvents::CdcTx as u32, 674 | ); 675 | } 676 | 677 | pub unsafe extern "C" fn vcp_on_cdc_rx(context: *mut c_void) { 678 | furi_thread_flags_set( 679 | furi_thread_get_id((*(context as *mut SerprogData)).trx_thread), 680 | WorkerEvents::CdcRx as u32, 681 | ); 682 | } 683 | 684 | pub unsafe extern "C" fn vcp_state_callback(_context: *mut c_void, _state: CdcState) {} 685 | 686 | pub unsafe extern "C" fn vcp_on_cdc_control_line(_context: *mut c_void, _state: CdcCtrlLine) {} 687 | 688 | pub unsafe extern "C" fn vcp_on_line_config( 689 | _context: *mut c_void, 690 | _config: *mut usb_cdc_line_coding, 691 | ) {} 692 | 693 | unsafe fn furi_create_thread( 694 | thread_name: String, 695 | stack_size: usize, 696 | context: *mut c_void, 697 | callback: FuriThreadCallback, 698 | start_immediately: bool, 699 | ) -> *mut FuriThread { 700 | let c_thread_name: &CStr = &CStr::from_bytes_with_nul(thread_name.as_bytes()).unwrap(); 701 | let thread = furi_thread_alloc(); 702 | 703 | furi_thread_set_name(thread, c_thread_name.as_ptr()); 704 | furi_thread_set_stack_size(thread, stack_size); 705 | furi_thread_set_context(thread, context); 706 | furi_thread_set_callback(thread, callback); 707 | 708 | if start_immediately { 709 | furi_thread_start(thread); 710 | } 711 | 712 | thread 713 | } 714 | --------------------------------------------------------------------------------