Desktop vs. Target - Linux Embedded systems

The target where an application runs is remarkably similar to your desktop machine: it has a file system that is about the same and roughly the same set of device nodes. These similarities make it possible to use your desktop machine as a reasonable emulator for your target board. Yes, the processor is different; but between the kernel and the development language, the difference apparent to you is small to nonexistent, depending on the nature of the application. There are some key differences, however, which fall into two large categories: language and system related. Language-related differences appear as a result of the development language used to build the code; system-related differences have to do with the Linux kernel and root file system.

The target is slower than, has less memory than, has smaller fixed storage than, and probably doesn’t have the same input and output features as the desktop. However, the interface to those resources is identical to what’s on a desktop system. Because the interfaces are the same, the software can reasonably be run on both systems with similar results.

The Linux kernel provides a uniform interface to resources that stays the same even if the kernel is running on a different processor. Similarly, when programming in C, you use the same function calls for printing, reading input, interprocess communication, and process control. Compiling the C code with across-compiler outputs different object code, which, when run on the target, should work the same as the code run on the development host.

Coding for Portability

C isn’t absolutely uniform between a desktop system and an embedded target. Its creator referred to the language as a framework for creating assembler code; when you view it in that light, C leaves many differences exposed, not because of flaws in the language but because that’s how it was designed. Most users don’t encounter these problems unless they’re programming at a low level, but you should nonetheless understand them for the times they become an issue. For example, a system that transmits data over a TCP network must translate this data to big endian (endian is described in the next paragraph); if the target machine is big endian, this could be inadvertently skipped and the code would still run, but it would break on a little endian machine. There are, however, some factors that have to be taken into account almost every time we code portably in C, and on occasions, in other programming languages.

  • Endianess: This concept describes how multibyte data is stored. In a big-endian system, the highest byte is stored first, with low-order bytes stored in the memory locations following in memory. Little endian is the opposite: the lowest byte is stored first, and high order bytes follow. Arabic numbers are big endian: 78,123,456 puts the highest byte first (78 millions) following by the bytes 123 and 456. If Arabic numbering was little endian, this number would be represented 456,123,78. If the code is written to get the first byte, the results are very different on big- versus little-endian machines, but the code is formally correct and compiles without complaint.
  • Word alignment: This is a way of storing data so that bytes are stored in memory locations evenly divisible by the size of the word on the system. In most systems, a word is 4 bytes, meaning that addresses fetched from memory must be evenly divisible by 4. This allows the processor designer to optimize the design of the processor so that it can access memory more quickly. In order to make data structures align nicely, the compiler inserts some padding into data structures so they line up nicely. No standard exists, however, to set up padding.; it’s possible to have code that once worked fail from an overrun or underrun bug that now corrupts data. During an overrun, code writes outside the upper limits of its memory; an underrun happens when code accesses memory that’s below the lower limits.
  • Capacities: An integer is two byte long, and that byte represent numbers of up to 56535; but as the numbers get larger, there’s no standard as to how they’re stored.A PowerPC system may store long integers with 12 bytes, but an x86 system may use 16 bytes. Developing on an x86 system may result in code that fails on a PowerPC because you may need to store a value larger that what’s supported.
  • Symbol locations: This is less of a problem, but sometimes toolchains put symbols in different files; during compilation, errors are generated. These problems are easy to find.

When you’re doing application development, the best strategy is to compile the code for execution on your development host and work out coding problems that aren’t architecture-specific before you test the code on the target. In order to do this, you must do some work to compile the code so it runs on the host and the target. Although the code won’t be running on the target right away, it’s a good habit to regularly compile it for the target so that any problems the compiler can spot can be fixed as you go instead of having to fix them all at once.

Higher-level languages don’t have these incompatibilities because they run on a virtual machine or interpreter (written in C) that hides these differences. The fact that C has these differences isn’t a flaw or feature but rather an artifact of the design philosophy of the language itself.

System Differences

These relate primary to the differences in the target’s hardware and the software that serves as the interface. The primary way a userland program (that is, an application) interacts with the Linux kernel is through device nodes, which operate with the same set of rules as a file.

Device nodes can be anywhere on a system. The practice is to put them in the /dev directory. Writing data into the device node passes that data into the waiting device driver running in the kernel, which can then react to the input. Reading from a device node results in the device driver supplying data; the exact sort of data depends on the device driver.

Other device drivers supply an interface via system calls (also known as syscalls). Frequently, syscalls are supplied as well as a file interface. These are functions that register with the kernel to provide an interface that’s a function call. When you make a system call, the parameters passed into the function are passed to the kernel, which then passes them along to the device driver that registered the syscall. After the device driver’s call completes, any return data is passed back to you. Many device drivers that have a syscall type of interface supply an executable program that makes executing syscalls more convenient for you.

A syscall interface and a device-node interface are both acceptable ways to provide access to system resources. What matters as an application developer is what’s necessary to make those resources available on the development host. Many times, the development host can be populated with the same device drivers as the target machine. If that’s not possible, you can construct the application code with a wrapper around the bits of code that read from and write to the device driver or make syscalls. This extra bit of code introduces additional overhead and code size, but the amount is inconsequential.

FIFO

It’s worth mentioning FIFOs, which are handy ways of emulating /dev/$(something) files that report the state of some hardware. As you recall from your data structures class, a FIFO is a First In First Out queue of data. In real life, it resembles a line at the bank: the order which the data is entered is the order in which it comes out the other side of the queue.In Linux, a FIFO is a special sort of internal data structure that has a file interface. Write to the FIFO, and it accumulates data so that when the reader asks for data, the queue is reduced. Create a FIFO by doing the following (you don’t need root access to do this):

$ mkfifo ~/test-fifo

You open the file with a program called tail that prints out the contents of the file as soon as data becomes available. Call this terminal one:

$ tail -f ~/test-fifo

In another terminal window (terminal two), you can write to this file by doing the following:

$ echo something > $/test-fifo

On terminal one, “something” appears. You can pass as much data as desired; for example:

$ ls / > $/test-fifo

The FIFO has a limited amount of resources to store the data after it’s been written but before it’s read. The current limit is one page size, or about 4KB.

The wonderful thought about FIFO is how it lets you read from it from the command line. This makes creating a simple fake device easy. A real-life example of this feature’s usefulness is a board with a device driver for some digital IO buttons and a potentiometer. The buttons have a device driver that updates a file /dev/buttons with a new line of data when the button changes state. The device has six buttons, and the contents of /dev/buttons when none have been clicked is

0 0 0 0 0 0 <newline>

The corresponding 0 changes to a 1 while you hold down the button:

0 1 0 0 0 0 <newline>

It changes back to a 0 when you release the button. The code that reads from this device driver does so in a loop with scanf(). To test the code on a desktop machine, I created a FIFO /dev/buttons and ran a script that wrote into the FIFO to simulate the buttons being clicked:

echo "0 0 1 0 0 0" > /dev/buttons That’s all the code necessary to emulate the device on a desktop. This interface also makes it much easier to test the code before deployment. When you’re thinking about how a device will interact with other parts of the system, put some thought into using what Linux already has in terms of interfaces, especially a file-type interface doing so lets you leverage a host of other features.

All rights reserved © 2020 Wisdom IT Services India Pvt. Ltd DMCA.com Protection Status

Linux Embedded systems Topics