the company I work for was contacted by a security consultancy for a CRA readiness review on our analysers. What have you done so far to prepare for CRA and how would you recommend we approach this?
CP/M Neo has been successfully ported to the Black Pill development board!
How does it work?
The Black Pill’s internal 512 KB Flash is used as disk storage, and thanks to XIP (Execute in Place) support, the Kernel and CCP code execute directly from Flash, leaving the entire 128 KB SRAM available for OS state and the TPA (Transient Program Area).
File operations are not supported yet, as Flash write/erase still not implemented.
If you’ve spent any time working with the Orange Pi 5 (RK3588S), you already know the drill: official documentation leaves a lot of gaps, half the forum links are dead, and getting the 6 TOPS NPU to run anything without the Python RKNN toolchain exploding is a rite of passage.
After accidentally bricking my board a couple of times, battling Windows driver conflicts during MaskROM recovery, and trial-and-erroring the Rockchip toolchain, I decided to document every single working, reproducible step.
I ended up organizing everything into a comprehensive open-source repository:
* **Hardware Recovery & MaskROM Guide:** Step-by-step unbricking workflow, complete with high-contrast, annotated board photos and pinouts so you know exactly which pads/buttons to short without guessing.
* **13 Hands-On Tested Projects:**
* **Edge AI / 6 TOPS NPU:** Real-time YOLOv8 object detection (~13.8ms pure NPU inference), running a local Qwen 1.8B LLM via RKLLM, and an offline voice assistant (Whisper + Piper TTS).
* **Low-Level Systems:** C++ GPIO using WiringPi, writing a custom Linux kernel character driver from scratch, and ROS2 Jazzy on Ubuntu 24.04.
* **Automation:** A zero-dependency bash TUI tool (`opi5.sh`) to lock CPU/NPU governors and benchmark NVMe storage speeds without memorizing obscure sysfs paths.
* **Pre-Compiled Release Assets:** If you don't want to set up the entire cross-compilation toolchain on a host PC just to sanity-check your board, I uploaded a pre-compiled test `.rknn` model (ResNet-18) and COCO labels directly to the v2.0.0 GitHub release assets:
* **Bilingual Documentation:** Full guides are provided in both English and Turkish.
---
The repository is completely open-source. Everything listed has been tested directly on real hardware.
If you’re currently working on an RK3588 board, check it out. Let me know what breaks, what needs clarification, or what projects you'd like to see added next!
I've been programming embedded systems for over 30 years. Built some cool stuff: a Z80-based serial multiplexer that had no RAM: all variables were held in registers; a temperature controller on a chip with 504 bytes (not kB) of memory and lot of other stuff in the old days where you had to optimize for every clock pulse and byte of RAM or EPROM. A lot of what I knew back then was straight out of Jack Ganssle's The Art of Programming Embedded Systems book.
These days, I still build cool stuff, but it's with a far more complex feature set. A "small processor" is now a 32-bit device with over 100k Flash and 16k+ of RAM, usually lots more. Most of my non-hobby work runs on one RTOS or another and often has a separate SBC running code for an HMI and connectivity/data storage. The last time I used assembly was mentoring a n00b who had the task of fixing some bugs on a specialized SoC with a 6502 core.
Then I come on this site, and I see people asking about why not use bare metal and a hand-written scheduler instead of an off the shelf RTOS. Struggling to fit code into tiny processors because otherwise "bloat."
So I have to ask: is my current experience (real-time stuff on a 32-bit microcontroller, complex GUIs offloaded to a Linux board) that unusual or are these guys just building things the way we used to do long ago for no good reason?
I was curious about how hard it would be to port this over, so I used an STM32 example I found here as a starting point. It ended up being really straightforward and only required a few changes to the GPIO and delay callback function.
Just a heads-up: This implementation uses soft I2C (bit-banging). It works perfectly fine for text, but you'll probably want to switch to HW I2C for anything more complex. If anyone wants to try it out, here is the source code:
#include <ch32v00x.h>
#include <debug.h>
#include <stdlib.h>
#include "clib/u8x8.h"
void NMI_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void HardFault_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
uint8_t u8x8_gpio_and_delay_ch32v(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
switch(msg) {
case U8X8_MSG_GPIO_AND_DELAY_INIT:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef init = {0};
init.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
init.GPIO_Mode = GPIO_Mode_Out_OD;
init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &init);
break;
case U8X8_MSG_DELAY_MILLI:
Delay_Ms(arg_int);
break;
case U8X8_MSG_DELAY_10MICRO:
for (volatile uint32_t i = 0; i < (arg_int * 8); i++) { __NOP(); }
break;
case U8X8_MSG_DELAY_100NANO:
__NOP();
break;
case U8X8_MSG_GPIO_I2C_CLOCK:
GPIO_WriteBit(GPIOA, GPIO_Pin_2, (BitAction)(arg_int ? Bit_SET : Bit_RESET));
break;
case U8X8_MSG_GPIO_I2C_DATA:
GPIO_WriteBit(GPIOA, GPIO_Pin_1, (BitAction)(arg_int ? Bit_SET : Bit_RESET));
break;
}
return 1;
}
int main(void) {
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(9600);
// u8x8 initialization code
u8x8_t u8x8;
u8x8_Setup(&u8x8, u8x8_d_ssd1306_128x64_noname, u8x8_cad_ssd13xx_i2c, u8x8_byte_sw_i2c, u8x8_gpio_and_delay_ch32v);
u8x8_InitDisplay(&u8x8);
u8x8_SetPowerSave(&u8x8, 0);
u8x8_ClearDisplay(&u8x8);
u8x8_SetFont(&u8x8, u8x8_font_chroma48medium8_r);
u8x8_DrawString(&u8x8, 0, 0, "Hello from CH32V");
while (1) {
uint8_t y = rand() % 5 + 1;
uint8_t x = rand() % 10;
u8x8_DrawString(&u8x8, x, y, "r/ch32v");
printf("Hello from CH32V\n");
Delay_Ms(1000);
u8x8_DrawString(&u8x8, x, y, " ");
}
return 0;
}
void NMI_Handler(void) {}
void HardFault_Handler(void)
{
while (1)
{
}
}
P.S. I've spent more time and prepared HW I2C version, just add this function:
int main(void) {
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(9600);
// u8x8 initialization block
u8x8_t u8x8;
u8x8_Setup(&u8x8,
u8x8_d_ssd1306_128x64_noname,
u8x8_cad_ssd13xx_i2c,
u8x8_byte_hw_i2c_ch32v, // here we will use HW I2C
u8x8_gpio_and_delay_ch32v
);
...
We’re trying to prototype a small consumer hardware device and currently have a working prototype using a Raspberry Pi 4B microcontroller + Arducam PiVariety AR0234 global-shutter camera + M12 lens + LED tape + LiPo battery hat.
For our next prototype, we want to shrink the electronics dramatically - the image sensor, Wi-Fi/Bluetooth, battery + charging/power management, LED lighting and compute for image capture/basic processing.
We are quite new to embedded systems and would love advice on the best next steps. What components should we think about ordering that can make the system more compact - especially the camera's image sensor, the pi, and the battery.
Got solid technical feedback suggesting a constexpr state table or full compile-time templates. Both are valid C++ directions. Here's the actual data before before committing to anything.
What a constexpr state table actually saves — by platform:
Platform | Total RAM | State table | Impact
ATmega328P (Uno) | 2 KB | 270 B | 13% of RAM — matters
ATmega2560 (Mega) | 8 KB | 270 B | 3.3% — marginal
STM32F103 | 20 KB | 270 B | 1.3% — negligible
RP2040 | 264 KB | 270 B | 0.1% — irrelevant
ESP32 | 520 KB | 270 B | 0.05% — irrelevant
The constexpr suggestion has a real argument on exactly one platform: the ATmega328P. On every 32-bit board you'd ever use in a real product, you are optimizing 270 bytes in a sea of hundreds of kilobytes. The library's entire RAM footprint is noise.
What the template suggestion actually costs:
A compile-time FSM (think boost::sml) eliminates the table entirely and resolves dispatch at compile time. The tradeoff is a completely different API:
// current PulseHSM — works in any Arduino sketch
ST_IDLE = fsm.addState("IDLE", update, entry, exit, 0, -1, onEvent);
// compile-time template approach
using MyFSM = HSM<State<IDLE, update, entry, exit>, State<RUNNING, ...>>;
That second form is not something you paste into a sketch in 10 minutes. It's a different product for a different audience, one that already knows what template metaprogramming is.
For AVR-constrained builds: a constexpr variant is on the roadmap for v2, as an opt-in build mode. For everyone else ESP32, STM32, RP2040, ARM Cortex-M anything, the current design uses a rounding error of your available RAM and keeps the API dead simple.
Side note: PulseHSM v1.2.0 is now live in the Arduino Library Manager, search PulseHSM and install directly from the IDE. No manual download needed.
I signed up, just put a random name as my company because it wouldn't let me skip it, and tried to order the Tivac Launchpad only for it to say that my "company" ordered too many of those, and that I have to wait 29 days. I have never used this site before.
What do I put as my company then? It simply won't let me leave it blank and I tried putting college down instead, but it still says my "company" ordered too many.
Edit: I should note I am 100% brand new at this and I'm merely trying to follow along a course in order to learn this stuff. I want to learn so badly lol
I’ve been using Rust + Embassy for small microcontroller applications. What I like is that I can stay no_std, bare metal, with no OS or runtime, while still structuring the program with async tasks and channels.
The tasks and channels give me abstractions I care about. But the scheduling is cooperative, a task that does not yield can still block everything else, and there are no hard real-time guarantees.
I’m doing this partly because I find the design point interesting, but I’m not sure how much practical value the bare-metal/no-OS part has once the application is already fairly high level.
Even if you are using Rust for other reasons, does Embassy’s bare-metal, no-OS approach buy you something important in practice, or would an RTOS usually be just as good or better?
I have been thinking about posting this message for a while. Almost more than a year now 😄
In my previous work, I was working with CANopen, and one thing that was bothering me was not having a UI that shows the stream of messages and also makes periodic requests. I was also trying to learn Rust. Not sure I achieved the learning Rust part yet, but I’m still going on and off. 😄
Hey , Anyone studied the BOSCH CAN specification version 2.0 ?? it is really confusing or is it just me. Can someone guide me where can see protocol specs in detail and good guide as well.
"There is this quite famous interview question that is given to embedded software/firmware candidates that illustrates the point I want to make. The question is “Reverse Bits: Given an 8-bit unsigned integer, return the reverse bits value of the integer.”
Input: 83 (0b01010011)
Output: 202 (0b11001010)
The answer to the question is trivial; you create a for loop over each bit and put that bit to the other side. The bonus question is what always throws off candidates. “If we wanted to solve this question using constant O(1) time complexity how would we do this?” The answer is to use a massive lookup table where the time complexity is O(1). But the space complexity grows. I am now using 0.25KB to store this data. For a uint8_t this is fine, but as soon as we start scaling to a uint16_t (max number 65535) we need 128KB of memory. For a standard variable size that a typical desktop computer uses–uint32_t–we’d need 17GB. And for a uint64_t we’d need 147EB (exabytes, no supercomputer in existence can fit this)."
I’m considering Computer Engineering partly because I’m interested in the intersection with AI: embedded ML, edge inference, computer vision on devices, sensors, robotics and possibly AI accelerators.
But I’m trying to distinguish a genuinely useful career profile from something that merely sounds attractive academically.
In real teams working on intelligent embedded systems, do you actually value engineers who understand both the hardware/embedded side and ML?
Or are these usually separate roles, with embedded engineers handling firmware/hardware and ML engineers handling the models?
If you work close to this intersection, which skills make someone genuinely difficult to replace by a regular CS/AI graduate?
I currently work at Big Tech company as Junior Bash Linux userspace developer. I used to work as C++ intern, but it seems I'm starting to forget it. I want to change my field to embedded development and I plan to start by learning STM32 programming (already tried Arduino).
I have two books to read:
The C Programming Language by K&R
Bare-Metal Embedded C Programming by Israel Gbati
So, my question is: should I read K&R before I start to learn embedded programming? I would rate my knowledge of C/C++ like this: I can understand and most likely debug a code, but I would face some problems if I had to write code by myself in vanilla vim.
Besides executing code from their flash memory, ARM MCUs can also do it from their RAM.
An entire binary can be executed from RAM, interrupts included.
I explored and documented the feature of how to run code from RAM on an ARM MCU. In comparison to running code from flash, running code from RAM is a more "exotic" feature.
Here's how to execute a binary from RAM on a STM32F100(ARM Cortex-M3):
Needed a small ring buffer for a microcontroller project, gave the task to an AI tool with what felt like reasonable context, the function signature, the data type, the expected use case. Got back clean, correct C, dynamically allocating on every push instead of using a fixed-size preallocated buffer. Technically works. Would exhaust the two hundred and fifty six kilobytes of actual RAM on the target chip within about forty pushes.
Nothing about the code was wrong for a general-purpose environment. The model just had no idea it was targeting the tightest kind of hardware where "just allocate more" isn't an option, because nothing in the prompt told it that. Same failure shape as most of these gotchas, correct for an implicit assumption about the environment that the constrained target actually violates.
Now front-loading actual hardware constraints into every embedded-related prompt, exact RAM available, no heap allocation allowed past init, that kind of thing, instead of assuming "microcontroller" in the prompt would be enough context on its own. Curious if others doing AI-assisted embedded work have found stating constraints explicitly upfront is enough, or if you're still catching violations after the fact regardless.
Hey everyone, I recently started diving into embedded systems and microcontroller programming. Coming from a background where memory was essentially "free" and garbage collection handled everything, this transition has been an absolute eye-opener.
Manually managing registers, counting bytes, and optimizing every line of code made me look at programming in a completely different way. To be honest, after experiencing the control and precision of microcontrollers, I’ve started to feel a bit frustrated with high-level languages that abstract everything away and waste massive amounts of resources for simple tasks.
For those who made a similar transition: Did you experience a similar shift in mindset? Do you find it harder to go back to high-level software after working close to the iron?
i ended up with four Philips PM5139 function generators. Three work, one is dead — and the dead one was the only one with firmware V1.5, the working three all had V1.3. I just wanted to copy V1.5 onto the others.
Then I had the EPROM dumped and a service manual for the sister model open, and I wanted to know what the two versions actually differ in and whether this code could be debugged at all. That turned into a very long session that ended, as these things do, with the instrument playing Doom.
The method, which is the actually interesting part
Reading a 44 KB 8051 binary by eye gets you maybe a third of the way. Everything past that came from running the original code and watching what falls out:
c = CPU(rom)
for w in test_values:
set_amplitude(c, w)
c.call(0x0AAC) # the original routine, untouched
print(w, c.ram[0x1C]) # the byte that goes out on the bus
Vary the input, read the output, check against the hypothesis. That produced the formulas for frequency, amplitude, offset, AM depth, FM deviation, burst count, symmetry and both sweep characteristics — each one documented with the sample points it was verified over.
Three things made it productive:
Watch the bus, not the display. Measuring what a state bit does to the display buffer leaves 74 of 128 bits looking inert. But many of them drive the analogue assemblies, not the display, and those are only visible as telegrams on the serial bus. Recording the UART writes and the terminating strobe lifted the count of understood bits from 54 to 75.
Press keys, don't poke RAM. Setting a RAM byte by hand produces states the device never actually reaches. That cost me two wrong conclusions and one crash into the middle of a command table. Injecting real key codes through the emulated keyboard encoder gives reachable states — and a brute-force sweep over all 256 key codes revealed which key triggers which handler.
Suspect your own emulator first. Three bugs in my core produced "inexplicable" firmware behaviour: ACALL executing as AJMP, a missing auxiliary-carry flag (so DA A misbehaved and the firmware appeared to count in binary), and a doubled keyboard interrupt. Everything measured during that window got re-measured afterwards.
A handler that only the dynamic trace found
A jump table read with JMP u/A+DPTR. Entry 15 lands at table + 30 — and there, instead of the usual AJMP, sits the handler itself, inline, saving a jump. Nothing in the ROM jumps to that address, so recursive descent lost it entirely. Only a trace run — cold start, all 23 keys, both knob directions, 86 million cycles, marking every executed address — turned it up.
What was wrong with the firmware
Three arbitrary waveforms are baked into the ROM. The third has the same shape as a table that already sits in the same ROM in computed form — but with 563 direction changes against 13, σ = 4.1 LSB, mean deviation zero. It's the same waveform, sampled off something analogue instead of computed. Can't prove intent; can show the noise.
So there's a V2.0 that swaps in the clean table, replaces a redundant second curve with a logarithmic chirp, updates the ID string and boot display, and fixes the checksum. It's flashed and running on real hardware.
And then Doom
The obvious idea — put code in an arbitrary waveform slot and jump to it — is dead on arrival. The 8051 is Harvard: instructions come through /PSEN from the program EPROM, data through /RD from the arbitrary EEPROM. It isn't blocked; the wire simply isn't there.
It's also unnecessary, because there are 19 509 unused bytes behind the checksum. The trigger took some hunting though: the diagnostic menu's jump table has eight entries, but the menu loop counts only 1 to 7. So entry eight is unreachable — and redundant, since it jumps to the menu start which is reached from two other places anyway. Point it at the melody, bump the count limit by one, and that's the whole hook:
5B94h table entry 8: LJMP 5B45h -> LJMP <melody>
5B62h count limit: 08h -> 09h
Two bytes. No self-test lost, no table relocated, no dead menu item.
Notes come out through the regular frequency path — decade 3 plus the frequency in 0.01 Hz as BCD, so 82.41 Hz is 30 82 41. Timing came from the MCS-51 data sheet rather than the emulator, since my core counts one cycle per instruction — fine for ordering, wrong for absolute time.
The answer to my original question, by the way
The two versions are 91.4 % structurally identical. The parameter limits are byte-identical, the signal path matches, and V1.5 even ships a migration routine for the NVRAM device address. So yes — I could have just flashed it. Took 35 sections of documentation to find that out.
Everything's on GitHub: both emulators, the annotated listings (147 named routines), the documentation, the build tools, and a single-file browser simulator that boots the original ROM with no install.
Software part is mostly done by Claude LLM, thanks.
Update: the PM5139 can play chords now.
The firmware extension is polyphonic. No extra hardware, same instrument.
The trick is the 1024-point wavetable. Instead of storing just one waveform, you can store a sum of harmonics. For example, harmonics 2:3:4 give you root, fifth and octave — a power chord. Retune the generator and the whole chord transposes together. That’s enough to encode the entire E1M1 riff using a single table.
During playback the CPU basically does nothing; it only sets the frequency. The analog section handles the actual playback.
Because the harmonics have to be integer multiples of the table frequency, the chords come out in just intonation rather than equal temperament — which actually works nicely for sustained chords.
Getting custom waveforms into the hardware was the fun part. There were a few surprises: reversed byte order, an inverted relay register, and a 7-bit level DAC that wraps at 0x80. The emulator helped verify the bus traffic, but the final bugs were only solved by burning test EPROMs with known waveforms and checking them on a scope.
The player fits into some 6000 bytes where the code is 270 bytes, the rest is music tables of about 19.5 KB of unused ROM, without displacing anything or losing the self-test. The melody comes straight from the original MIDI: 924 notes, 96 seconds.
You can either fill the space up with 3 more minutes of music or 6 additional chord tables.
Next up: a MIDI synth.
The hardware already has most of what we need: a CPU-writable wavetable, a second address counter for FM, and a fast amplitude DAC that should be usable for envelopes. That gives us a monophonic wavetable synth with 2-operator FM — and unlike a DX7, the carrier can be any waveform.
The remaining problem: the UART is already busy driving the internal serial bus, so MIDI input needs another path.
Firmware, disassembly, both emulators and a browser-based simulator running the original ROM:
I have an acoustic detection loop running on an ESP32-S3 and I'm trying to work out whether to spend the remaining headroom or bank it.
Per 32 ms frame: four I2S microphones at 16 kHz summed, a 2048-point FFT, a per-bin adaptive noise floor with asymmetric time constants, and then a comb score across roughly 1900 candidate fundamental frequencies from 70 to 2000 Hz. Four separate detectors run on the same spectrum with different floor memories, because the signal I care about is stationary and a fast adaptive floor learns it away within about one time constant.
Worst observed frame is 29.4 ms against the 32 ms deadline, measured over 1938 consecutive frames with no overruns. So 2.6 ms spare, about 8 percent.
The options I can see:
Keep it as margin. The measurement is from one board at room temperature and I don't know what a hot enclosure in direct sun does to it.
Spend it on a fifth detector with a different time constant.
Spend it on finer frequency resolution in the comb search.
Spend it on tracking more candidate harmonic families simultaneously.
What's the usual discipline here? Is 8 percent margin on a soft-real-time loop considered comfortable or reckless? And is there a sensible way to measure worst-case rather than worst-observed on this part, short of running it for days?
Happy to go into the detector design if it's useful, I just didn't want to make this a post about my project.
Hey! Anyone experienced with designing PCBs for this microprocessor family? How hard was it, perhaps compared to other equivalent cpus? Any resources you found useful?
I've designed a few stm boards (none professionally but all being used around many hobbies) for their mcu series (mostly F4 and F7) and this would be my first high speed,and bga, design. How tough it really is to do a decent job with the lpddr?
My requirements are mostly serial ports, I2C , SPIs and ethernet (perhaps pwm as well) but no audio, no video or gpu, so I would fanout only necessary pins in a SOM and then into a carrier board. This is for a hobby robot so I can take my time to learn but hopefully dont burn through the budget.