Debugging Large Embedded Memories in Embedded Programming

It is common in the embedded space to have small, simple storage devices accessed address-wise.

Chips can be as small as a few kB, leaving insufficient space for a file system. On the other hand, we may encounter situations where large storage devices are used in the design, but no file system is included because it was not necessary for the intended purpose.

Software testing validation and debugging in embedded systems can be long and complicated in such cases. For instance, we worked on a product which would generate a few hundred bytes of storage a few times a week. The design would have to be tested when the chip is full, which by design should take some weeks

Bermondsey Electronics Logo

Challenges in Software Testing Validation and Debugging in Embedded Systems That We Overcame

The first solution is to abstract away data accesses through a dummy file. It , which doesn’t do much. Its true purpose is to act as a barrier between the rest of the code and the memory accesses.

When running code for the MCU target, we compile the code which uses its access methods. However, when running on the desktop, the code interacts with file system accesses. For example, in a system that reads a single byte from a specific address, the access method will differ depending on whether the code is executed on the MCU or the desktop environment.

We could directly code the register access sequence into the method, or we could have a
HAL function which passes it through to another. Both signatures look identical:
int HAL_ReadMemory(uint8_t * buffer, uint32_t address, uint32_t
count)

It doesn’t matter to the function calling it if the underlying memory access is through an MCU or the file system. It wants a memory area to hold the result, a return code, a start address, and the number of bytes to read.

When we need to know on the desktop that we can read it back, we can use f_read
to read from a file. For many systems, it doesn’t even need to be that complicated – we can store the whole thing in a uint8_t array.

And it’s fast. It’s RAM, after all!

We can run pass/fail tests much more quickly than if we need to wait for an embedded
erase cycle, or for the product to generate the required amount of data naturally.

Embedded Programming Key Methods

Think back to our system that generates a few hundred bytes per week. How do we test whether the memory accesses are good?

Debugging Large Embedded Memories

Debugging Outcomes in Small Embedded OS for Large Memory Devices

Here, we have presented a way of speeding debugging large memory devices in small embedded operating systems. 

Sometimes it means we expand our test coverage cheaply. Sometimes we use it to expose integration test failures in the final product.

Either way, we learn something new about our product and how it behaves under stressful conditions.

Don’t just take our word for it. Hear from our customers.

An Alternative Solution

The second answer is a little more devious. What if we could do all of that, but on the real target?

After all, the desktop doesn’t have to deal with the same embedded memory limitations. It doesn’t have to deal with the same sector sizes or erase times. It is best to do that on the target. But how?

The answer here lies in integration test commands. We require a back channel onto the target. We send the target a specially crafted message, and it runs its “store some data” method in a loop. We have to deal with things like the power gulp during sector erase. 

We can’t hide from erase times on the chip. If there’s a problem in the layout, and it interferes with the address lines, this is one way to find it.

Maybe interrupt priorities matter?  Maybe you can expose a race condition?

How to Debug the Target

Here is one way to debug it. If we have a set of integration tests running on the target, we can design tests to expose timing dependencies. We can link as much of the application as we want. With an appropriate bidirectional communication link, we can collect results and treat the runtime like a unit testing environment.

For example, referring back to the original 128MB storage IC, filling this memory “naturally” would have taken ~21 months. Using a desktop-centric unit testing approach, the test runs in around 13 seconds.

As an integration test on the real target, the memory fills in around 4 ¾ minutes. We must wait for the memory to be written to the chip in real-time, with various pages write times. We must wait for the chip to carry out sector erases in real-time. All of this happens in the background while the device executes its normal scheduler, so it is dependent on ISRs and conflicts just like anything else.

Modern Embedded Programming Tools

As an aside, we now find  tools are rarely the problem. Yes, historically we used to see toolchain problems – the compiler wouldn’t always generate the same code, and copious compiler-specific directives and quirks of the devices worked around in unclear ways.

Modern tools (particularly Cortex tools) are reliable. What you get on the embedded target is almost always what happens on the desktop. The argument is that there are significant differences between the desktop and embedded runtimes is largely now discredited.

Beware the vendors pointing out all the clever ways their tools are “good with C” – there’s probably a reason the marketing department chooses that wording.

Bermondsey Electronics

Contact Us

At Bermondsey, we are specialists in a range of areas, including embedded software and firmware, embedded IoT and embedded Linux. Our expertise extends to software testing validation and debugging in embedded systems, ensuring that your products perform optimally under all conditions. If you have questions about how we can help your business, please complete the form below, and we will be in touch shortly. 

Alternatively, please call us on +44 (0)208 0650 162

Email : [email protected]

FAQs on embedded programming and debugging

What is debugging in an embedded system?

Debugging is a vital part of embedded systems development. Since these systems handle critical tasks, even a small error or malfunction can lead to serious consequences. Effective debugging allows us to identify and correct issues, and ensures the system operates as it’s intended.

Common issues include:

  • Memory leak – a program that allocates memory but does not release it when it’s no longer needed
  • Fragmentation – when memory is allocated and freed in a way that creates unusable gaps, making it harder to use the available memory efficiently
  • Crosstalk – crosstalk is interference caused when the electric or magnetic fields of one telecommunication signal disrupt a signal in a nearby circuit