7 ms·
Interesting how author switches to assembly language for more precise reading, but keeps the "read_data" method as a separate non-assembly function. That introd
by theamk 3y ago
Interesting how author switches to assembly language for more precise reading, but keeps the "read_data" method as a separate non-assembly function. That introduces lots of branching in the code which is busy-loop-based and branch prediction is not what I'd want for consistent timing. It also introduces un-needed dependency: what if the next version of compiler changes the code? All timings (which are based on cycle-counting) will be off.
That said, Teensy 4.0 is 600 MHz ARM cpu, so there are 1000 cycles even between the shortest transitions.. some overhead is fine, the project is not exactly cpu-starved.
I also wonder if author has considered using a peripherals for precise signal capture? Something like timer in capture mode feeding into DMA buffer would allow hardware signal capture with very high precision and without any dependencies on exact instructions emitted.
- naitgacem 3y agoI think judging by the title and the mention of bit banging, the aim of this isn't to get a robust reliable thing going on. I find hacking things together like this to be really fun and doesn't feel like a job. sort of entertainment. but that's just me perhaps.
- theamk 3y agoSo do I, but that's more of the reason to keep things robust, no? In my work projects, I can use the dangerous code like this - because we have compilers and libraries frozen, unit and integration tests, a complex testing process. We can do all the right efforts to ensure the things work, even if solution is intrinsically unreliable. In my personal projects I write some stuff and start using it, the testing is minimal, and toolchain versions is "whatever platformio decided to pull up today". I'd hate for my project to break just because I rebuilt it to add the new feature and meanwhile my compiler got upgraded. So I'd definitely abuse SPI port or something to get things reliable.
- chasd00 3y ago> Teensy 4.0 is 600 MHz ARM cpu, so there are 1000 cycles even between the shortest transitions. can you explain this a bit more? When you say "transition" are you talking about an individual transistor moving from on to off or vice versa?
- theamk 3y ago"transition" is signal changing, from high to low or from low to high. As described in the page, there are multiple signals changing when operating floppy ("track 0", "write gate", "data", etc..). Of them the fastest one is "data", so that's what I am going to focus on. The 2nd page of writeup [0] says: A short transition (S) will nominally have 2us between bits, and represents 0b10 A medium transition (M) will nominally have 3us between bits, and represents 0b100 A long transition (L) will nominally have 4us between bits, and represents 0b1000 So we are looking at 3, 4 or 5 microseconds between bits. To get this in CPU cycles, you multiply this by clock frequency - google can help you with units, searching for "2 microseconds * 600 megahertz" [1] shows the answer, 1200, right away. I've rounded this down to 1000, as there are two transitions per pulse and it is all very approximate anyway. And then you use your embedded knowledge to assign meaning to the number: the CPU is ARM, so 1 instruction/cycle is a good approximation (it could be more due to dual-issue or less due to jumps). So you have like 1000 instructions. Each function call in language like C or C++ might be a 5-20 instructions overhead, and you probably want to read that pin at least 10 times to detect both transitions. The tightest loop is also going to be a dozen instructions or less (read gpio, mask, compare, maybe jump out, increase, compare timeout, loop) So.. you can do it in C/C++ easily if your main loop involves no function calls (and you have no interrupts). If you use functions to read, your timing is going to be tight and those functions are better be super-optimized, you will be asking your compiler for a lot. Higher level languages like lua/micropython are out of the question (at least for that loop). And as I learned from reading this, rust is also out of the question, although I wonder if there are some unsafe primitives which do not do any checking. (and yes, there are transistors changing in the background all the time throughout the process, but I really don't care much about them, they are on too low of the abstraction level) [0] https://floppy.cafe/mfm.html https://floppy.cafe/mfm.html [1] https://www.google.com/search?q=2+microseconds+*+600+megahertz https://www.google.com/search?q=2+microseconds+*+600+megaher...
- dragontamer 3y ago> And then you use your embedded knowledge to assign meaning to the number: the CPU is ARM, so 1 instruction/cycle is a good approximation (it could be more due to dual-issue or less due to jumps). So you have like 1000 instructions. Each function call in language like C or C++ might be a 5-20 instructions overhead, and you probably want to read that pin at least 10 times to detect both transitions. The tightest loop is also going to be a dozen instructions or less (read gpio, mask, compare, maybe jump out, increase, compare timeout, loop) Nit: That's definitely the wrong approach though IMO. So you want to accomplish two things: 1. Clock recovery -- Figuring out the timing of a signal 2. Decoding -- Figuring out what that signal means These are two separate steps and should be done separately, be it in code or hardware. Though advanced protocols combine both into a single step, the older protocols (UART / Floppy / etc. etc.) had these two concepts separated into two different steps. You won't have 2us between bits: but instead 2.01us or 1.99us between bits, etc. etc. Clock-recovery mechanisms means that even in the face of worst-case timing differences, your code remains resilient. Decoding is the step you've done here, but it should be done after clock-recovery. ------------- Traditional clock recovery methods are phase-locked-loops (in hardware), or various XOR-loops (in software) to try and figure out the timing of the clock from the 0-1 and 1-0 transitions. ---- The traditional UART (ex: 9600 baud or 115200 baud) is ~16-ticks per bit. (IE: a 9600 baud UART needs to look at the signal 153600 times per second. A 115200 baud UART needs to look at the signal 1843200 times per second). The 16-times per bit helps you "center your aim" for the transition. You then typically aim at the center-3 timeslots (ex: count number 7, 8, and 9) for when to send and/or read the signal. -------- That being said, your analysis for "how many instructions you have per timeslice to read the data" is correct. I just feel like adding that the clock-recovery portion needs to be definitely addressed.
- dfox 3y agoIt would be a good match for some kind of input capture peripheral (the MCU does not have input capture per-se but it is an Cortex M7 so you can certainly build that out of interrupt matrix and some clever configuration of DMA engine) or for abusing SPI USRT if it can support such extremely long frames (which it probably can).
- LegionMammal978 3y ago> That introduces lots of branching in the code which is busy-loop-based and branch prediction is not what I'd want for consistent timing. I don't see how so basic a call would create conditional branches that would have to be predicted. Calling the function is an unconditional branch-and-link, after which it should just be doing a load and returning. (It's LLVM with the equivalent of -O2, it's not going to be doing anything weird.) Unless the return address isn't cached in these processors?