MicroPython for Microcontrollers Module 7
Memory limits, speed, native and viper code interview questions
Interview questions on memory limits, speed, native and viper code: warm-ups, then what each harder question tests and how a good answer starts.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher
Why does gc.mem_free() not go up after del buf?
Answered in the lesson The heap, the garbage collector and mem_info
-
Fresher
Why use time.ticks_diff() instead of subtracting two ticks_us() values?
-
Fresher
What is the difference between @micropython.native and @micropython.viper?
Answered in the lesson @micropython.native and @micropython.viper
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Fresher to mid
A Pico samples a sensor every millisecond and logs gc.mem_free() once a second. The log falls by about 16,000 bytes a second, from 180,000 to 20,000, jumps back to 180,000 and repeats, and a few samples go missing at every jump. What does the log show, and how would you stop the missed samples?
- What it tests
- Reading a falling and jumping mem_free as garbage made and then collected, linking the jump to a collection pause, and choosing between less garbage, a threshold and collecting at a chosen moment.
- First step
- Read the curve before touching code. The slope is the rate of garbage: 16,000 bytes a second is 16 bytes, one block, per sample, a float or a small object made on every pass. Each jump is an automatic collection when a request found no room, about every ten seconds, and its pause lands wherever the loop happens to be. Then find what the sampling path allocates.
- Follow-ups
-
- What does the sampling loop allocate per sample?
- Would gc.threshold() alone fix it?
- How do you check the fix on the board?
From the lesson The heap, the garbage collector and mem_info
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
You are starting a Pico W product that keeps 12 KB of sensor buffers and uploads a batch over HTTPS every five minutes. Before the app is written, how do you budget its RAM, and what do you measure on the first prototype?
- What it tests
- Pricing buffers in 16-byte blocks, knowing that TLS memory comes from the heap on the rp2 port and needs one long run, measuring imports on the board, and planning the order of allocations.
- First step
- List what lives for the whole run and price it in blocks: each buffer rounded up, plus its 16-byte object. Then the TLS connection, whose buffers come from the MicroPython heap on the rp2 port and need one run of at least 1,026 blocks for the incoming records. Plan to measure gc.mem_free() after the imports and max free sz before every upload on the prototype.
- Follow-ups
-
- What changes when the upload starts while the buffers are full?
- How do you keep a run of 1,026 blocks free after days of uptime?
- What would you freeze into the firmware, and why?
From the lesson The heap, the garbage collector and mem_info
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
A Pico logger has run for two days when bytearray(1024) fails with "memory allocation failed, allocating 1024 bytes". A gc.mem_free() call just before it returned 7,488. How can that be, and what do you change?
- What it tests
- That an allocation needs one free run, that collections never move live objects, reading max free sz, and fixing the order of allocations.
- First step
- Log micropython.mem_info() right before the failing line. The GC line gives the free total, and max free sz the longest free run in blocks. If it is below 64, the 1,024 bytes of items fit nowhere, however much is free. Then look at what the program keeps between runs, because those objects decide where the holes are.
- Follow-ups
-
- Why did it work for two days?
- Would gc.collect() before the call help?
- How would you have caught it before it failed?
From the lesson Fragmentation and zero-allocation loops
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
To keep room for an upload buffer, a teammate makes reserve = bytearray(1100) at start-up, deletes it right before each upload and makes it again right after. Uploads fail with MemoryError from the second one on. How do you find out why, and what do you change?
- What it tests
- Debugging allocation order on a first-fit heap, reading block positions in mem_info(1), and knowing what a collection does and does not do.
- First step
- Measure instead of guessing: print max free sz after the reserve is deleted, after the upload and after the reserve is made again, and the block map of mem_info(1) at each point. The question is whether the run the reserve held is still in one piece when the next request arrives, and if not, which object split it.
- Follow-ups
-
- Why does the first upload work?
- What is the smallest fix?
- When would you choose another strategy?
From the lesson Fragmentation and zero-allocation loops
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Your parser for a 1 kHz sensor stream must never allocate. How do you prove that it does not, and what can a passing proof still miss?
- What it tests
- Using heap_lock() as a test, covering every branch, reading code for allocating constructs, and knowing what a locked run cannot show.
- First step
- Turn the claim into a test: run the parser under micropython.heap_lock(), on the unix port or the board, and make sure the heap is unlocked again however the run ends. Then decide which inputs the test must feed it: good packets, damaged ones and every edge case the protocol allows.
- Follow-ups
-
- Where do the unlock and the reporting go, and why?
- Which allocations can the proof miss?
- How do you keep the proof true as the code changes?
From the lesson Fragmentation and zero-allocation loops
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
A colleague timed a filter function at 40 µs on their laptop and planned a 10 kHz control loop on a Pico around that number. What is wrong with the plan, and how would you measure the function properly?
- What it tests
- That a timing belongs to its interpreter and machine, how to time on the board with ticks_us(), ticks_diff() and several runs, and what the budget of a 10 kHz loop is.
- First step
- Start from the budget: 10 kHz leaves 100 µs per pass for everything, the filter and the rest. Then time the filter where it will run: on the Pico, with ticks_us() and ticks_diff(), gc.collect() before each of several runs, the best and the slowest run reported. The laptop's 40 µs comes from CPython on a different processor.
- Follow-ups
-
- Why not scale the laptop's number by the clock speeds?
- What should the harness print beside the times?
- What if the best run fits and the slowest does not?
From the lesson Measuring and improving speed
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A Pico's 1 kHz loop needs 1,200 µs per sample, and the budget is 1,000. Walk through how you would make it fit, in order, and say what you would measure at each step.
- What it tests
- Measuring before changing code, using the hot spot's share to choose the work, and ordering the changes: less work per sample, no allocation, integer maths, local names, const(), then machine code.
- First step
- Measure before touching anything: time one sample on the board with the best of several runs, then time its parts, so you know what share the slowest part takes. The share decides what any speed-up of that part can win for the whole loop, and whether the plan can reach 1,000 µs at all.
- Follow-ups
-
- How much does a 2 times faster filter buy if it takes 80 % of the sample?
- Where do allocations hide in such a loop?
- When do you reach for native or viper code?
From the lesson Measuring and improving speed
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A driver module defines 60 register addresses as REG_NAME = const(0x..). A teammate says the constants waste RAM. Is that right, how much is it, and what do you change without breaking the modules that use the driver?
- What it tests
- What const() puts into the bytecode, what a public name still costs in the module's globals, private names, and keeping an API stable while saving RAM.
- First step
- Separate the two effects of const(). Every use of a constant is folded into the bytecode, so reads cost nothing in any case. What the teammate means is the name itself: a public const name stays in the module's globals so other modules can read it. Count which of the 60 names other modules actually use.
- Follow-ups
-
- How much RAM do the 60 public names take at least?
- How do you keep the few names other modules need?
- What else in the driver could const() remove?
From the lesson Measuring and improving speed
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
A viper checksum over a file's bytes matches the CPython reference in every unit test, yet a Pico reports a different value for large files. What is the likely cause, how do you confirm it, and how do you make the tests catch it?
- What it tests
- That viper integers are machine words that wrap, why tests on CPython or a 64-bit build cannot see it, and how to fix the algorithm and the tests.
- First step
- Suspect the word size before the code. Viper integers are 32-bit words on the RP2040, while CPython's never wrap, so a sum that passes 2³¹ − 1 differs only on the board. Confirm it by computing the reference result and checking whether it fits a signed 32-bit word, for the file sizes that fail.
- Follow-ups
-
- Why did a test on the unix x64 build pass too?
- How do you change the algorithm?
- What check belongs in the host tests from now on?
From the lesson @micropython.native and @micropython.viper
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
Your CI runs MicroPython's unix port on an Apple Silicon runner, and every module with a viper function fails at import with SyntaxError: invalid micropython decorator. What is going on, and how do you test viper code in CI?
- What it tests
- Which builds have native emitters, why the error comes at compile time, and a test plan that covers both the logic and the board's word size.
- First step
- Read the error as a fact about the build, not the code: decorators are handled by the compiler, and the unix port has no native emitter for 64-bit ARM, so the module cannot compile there at all. The fix belongs in CI: a build with an emitter, or tests that do not need one.
- Follow-ups
-
- Which CI runners can run viper code?
- What can you test without any emitter?
- What still needs a real board?
From the lesson @micropython.native and @micropython.viper
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Senior to staff
A Pico must checksum each 64 KB block it reads from flash within 20 ms, and the bytecode version takes 60 ms. How do you choose between native, viper and the inline assembler, and what evidence do you collect before you decide?
- What it tests
- Measuring on the board, the hot spot's share, what each emitter gains and costs, viper's word-size risks, and when assembler is worth its maintenance.
- First step
- Measure first: time the checksum alone and the whole block on the board with the best of several runs, so you know whether the checksum is most of the 60 ms. Then try the cheapest change that could reach 20 ms, measure again, and move to a harder one only if the numbers say so.
- Follow-ups
-
- What would you expect from @micropython.native here?
- What makes viper a good fit, and what must you check?
- When would you write assembler instead?
From the lesson @micropython.native and @micropython.viper
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
An inline assembler routine that ran on a Pyboard fails on a Pico at import with SyntaxError: unsupported Thumb instruction 'movwt' with 2 arguments. Why, and how do you port it?
- What it tests
- The difference between the ARMv7-M and ARMv6-M instruction sets, why the error comes at compile time, and the usual rewrites of constants, division, bit counting and conditional blocks.
- First step
- Read the error as a statement about the processor. The Pyboard's chip runs the full Thumb-2 set of ARMv7-M, while the RP2040's Cortex-M0+ implements ARMv6-M, and MicroPython's assembler refuses ARMv7-M instructions when it builds for the Pico. List every instruction of the routine that is ARMv7-M only before changing any.
- Follow-ups
-
- How do you replace movwt?
- What about udiv, clz and it blocks?
- How do you prove the port gives the same results?
From the lesson Inline assembler: asm_thumb and asm_rv32
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
A teammate wants to rewrite a viper filter in inline assembler for a 20 % gain on the filter. How do you decide, and what must come with the change if you accept it?
- What it tests
- Turning a local gain into a whole-program gain, weighing maintenance and portability, and the tests, fallbacks and measurements that make assembler safe to keep.
- First step
- Ask for the numbers first: the filter's share of the loop and the loop's target, both measured on the board. A 20 % gain on a part that takes a third of the time saves under 7 % of the whole, so the change is worth considering only if that closes a gap the product actually has.
- Follow-ups
-
- What if the loop already meets its target?
- What must the pull request contain?
- How does the answer change for a product with Pico and ESP32 boards?
From the lesson Inline assembler: asm_thumb and asm_rv32
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
Estimate how long a Thumb loop that sums 64 KB of bytes takes on a Pico at its default clock, and say what the estimate leaves out.
- What it tests
- Counting instructions and cycles from the datasheet's table, using the clock rate, and knowing why a cycle count is a floor rather than a measurement.
- First step
- Write the loop down and count one pass: a byte load, an addition, a pointer step, a count down and a branch back. Take each instruction's cycles from the RP2040 datasheet's table, multiply by 65,536 bytes, and divide by the rp2 port's default 125 MHz.
- Follow-ups
-
- How much does unrolling by four save?
- What does the estimate leave out?
- How do you check it on the board?
From the lesson Inline assembler: asm_thumb and asm_rv32
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
All MicroPython for Microcontrollers interview questions · The MicroPython for Microcontrollers track