Discussing the Complexity of Embedded Programming

The first step is to look at embedded issues from the perspective of PC programming; the second step is to learn to use embedded programming concepts; the third step is to combine PC and embedded thinking and apply it to actual projects. Many friends transition from PC programming to embedded programming. In China, few embedded programming professionals have graduated from computer science; most come from fields such as automatic control or electronics. These individuals have substantial practical experience but lack theoretical knowledge; a significant portion of computer science graduates pursue online games, web pages, and other higher-level applications independent of operating systems. They are often reluctant to engage in the embedded industry, as this path is challenging. They possess strong theoretical knowledge but lack knowledge in circuits and related areas, making it difficult to learn specific knowledge in embedded systems.

Although I have not conducted an industrial survey, from my observations and the personnel I have recruited, engineers in the embedded industry either lack theoretical knowledge or practical experience. Rarely do they possess both. The root cause lies in the issues of university education in China. I will not discuss this to avoid a war of words. I would like to present a few examples from my practice to raise awareness of certain issues when working on embedded projects.

The first issue:

A colleague was developing a serial port driver under uC/OS-II, and problems were discovered during testing both in the driver and the interface. A communication program was developed in the application, where the serial port driver provided a function to query the number of characters in the driver buffer: GetRxBuffCharNum(). The upper layer needs to receive a certain number of characters before it can parse the packet. The code written by a colleague can be represented in pseudocode as follows:

bExit = FALSE;

do {

if (GetRxBuffCharNum() >= 30)

bExit = ReadRxBuff(buff, GetRxBuffCharNum());

} while (!bExit);

This code checks if there are more than 30 characters in the current buffer and reads all characters from the buffer until successful. The logic is clear, and the thought process is straightforward. However, this code does not work properly. If it were on a PC, there would be no issues; it would operate normally. But in embedded systems, it is truly unpredictable. My colleague was frustrated and did not understand why. When he asked me to solve the problem, I looked at the code and asked him how GetRxBuffCharNum() was implemented. Upon inspection:

unsigned GetRxBuffCharNum(void)

{

cpu_register reg;

unsigned num;

reg = interrupt_disable();

num = gRxBuffCharNum;

interrupt_enable(reg);

return (num);

}

It is clear that within the loop, between interrupt_disable() and interrupt_enable() is a global critical section, ensuring the integrity of gRxBufCharNum. However, due to the frequent enabling and disabling of interrupts in the outer do { } while() loop, this time is very short. In reality, the CPU may not respond to the UART interrupt properly. Of course, this is related to the baud rate of the UART, the size of the hardware buffer, and the CPU speed. The baud rate we are using is very high, approximately 3Mbps. The start and stop signals of the UART account for one bit each. A byte takes about 10 cycles to transmit. At a baud rate of 3Mbps, it takes about 3.3us to transmit one byte. How many CPU instructions can be executed in 3.3us? At 100MHz ARM, about 150 instructions can be executed. How long does it take to disable interrupts? Generally, disabling interrupts on ARM requires more than 4 instructions, and re-enabling also requires more than 4 instructions. The code for receiving UART interrupts is actually more than 20 instructions. Therefore, it is possible to encounter bugs that lead to communication data loss, which manifests as instability in the communication system.

Modifying this code is actually quite simple; the easiest way is to modify it from the upper layer. That is:

bExit = FALSE;

do {

DelayUs(20); // Delay 20us, generally implemented using empty loop instructions

num = GetRxBuffCharNum();

if (num >= 30)

bExit = ReadRxBuff(buff, num);

} while (!bExit);

This allows the CPU time to execute the interrupt code, thereby avoiding the delays caused by frequently disabling interrupts, which could lead to information loss. In embedded systems, most RTOS applications do not include serial port drivers. When designing code, the integration of the code with the kernel is not adequately considered, resulting in deep-seated issues. An RTOS is called an RTOS because of its fast response to events; quick response to events relies on the CPU’s response speed to interrupts. Drivers in systems like Linux are highly integrated with the kernel, running in kernel mode. Although RTOS cannot replicate Linux’s structure, there are certain lessons to be learned.

From the above example, it is clear that embedded development requires developers to understand every aspect of the code.

The second example:

A colleague was driving a 14094 chip that converts serial to parallel. The serial signal is simulated using IO since there is no dedicated hardware. My colleague hastily wrote a driver, and after debugging for 3 or 4 days, problems still persisted. I couldn’t bear to watch any longer, so I went to take a look. The control of the parallel signal was sometimes normal and sometimes abnormal. I examined the code, which can be roughly represented in pseudocode as:

for (i = 0; i < 8; i++)

{

SetData((data >> i) & 0x1);

SetClockHigh();

for (j = 0; j < 5; j++);

SetClockLow();

}

This sends the 8 bits of data from bit0 to bit7 sequentially on each high clock level. It should work correctly. But where is the problem? I thought about it and checked the datasheet for the 14094, and understood. It turns out that the 14094 requires the clock’s high level to last for 10ns, and the low level must also last for 10ns. This code only has a delay for the high level and does not have a delay for the low level. If interrupts occur during the low level, this code might work. However, if the CPU does not execute during the low level, it will not work correctly. Hence, the output is inconsistent.

Modification is also relatively simple:

for (i = 0; i < 8; i++)

{

SetData((data >> i) & 0x1);

SetClockHigh();

for (j = 0; j < 5; j++);

SetClockLow();

for (j = 0; j < 5; j++);

}

Now it works perfectly. However, this code is not easily portable because if the compiler optimizes, it may eliminate these two delay loops. If they are lost, the requirement for the high and low levels to last for 10ns cannot be guaranteed, and it will not work correctly. Therefore, truly portable code should implement this loop as a nanosecond-level DelayNs(10);

Like Linux, at power-up, first measure how long the nop instruction takes to execute, how many nop instructions are needed for 10ns. Executing a certain number of nop instructions will suffice. Use compiler directives to prevent optimization of the delay loop, such as:

__volatile__ __asm__(“nop;\n”);

From this example, it is clear that writing good code requires a lot of supporting knowledge.

Discussing the Complexity of Embedded Programming

Leave a Comment