Embedded Systems: Getting your hands on the Hardware

 This article will tell you how to read a datasheet, get to know a new processor, unravel a schematic, have a debugging toolbox and test the hardware.

The first section will describe the integration between the hardware and the software. A product starts out as an idea or a need to be filled.


These are based on features, cost and time to market and a schedule is made for the milestones and activities. The following diagram below enumerates a sample project sprint plan.




The hardware team then chooses components while consulting embedded software team, usually purchasing stuff for the riskiest parts of the system. The hardware team creates schematics. The software team works on the development kit. Most of the time the hardware team goes through the datasheets. The software team finds a compiler and debugger, and ideal peripherals as well. 


The schematics are entered using a CAD schematic capture program, and are expensive to use, mainly used for the processor. Good news is that the hardware engineers will (usually) generate an I/O map and make a header file from this map. Then the board gets fabricated and the PCBs are built, and an assembled board is called a PCBA  (printed circuit board assembly).

The software team then gets hardware tests written while the boards are being created. Ask the engineer which parts are the riskiest, and these tests also help make the systems more stable for development. The hardware engineer verifies everything and the board-bring up, which is discussed in the next paragraph, starts. 

Since Electrical Engineers can't compile many codes, the board may have tons of bugs. Don't be afraid to ask questions, since these engineers will answer questions. Be willing to give time to unravel a problem. Consider lectures if absolutely necessary. 

For tests, I first need to make each individual component testable. Make sure each underlying test modules are the same for these components. Then make the tests readable and reproducible and write the hardware verification code, which requires an in-depth knowledge of peripherals, leading to how to read a datasheet. 

Consider each chip as a software library and read through each library in detail and know each module, and take notes of each modules. Datasheets are very similar to API calls for peripherals, and describe many hardware components.


Datasheets are mainly written for electrical engineers, and think of it as a technological conversation. The top of the datasheet shows the overview. This is for experienced engineers, not newcomers. 

Functional diagrams are often in the first page, and the place to start in is the description. Read the description out loud, you can use a teddy bear to read the description.


 In the case that things go wrong, there are certain datasheet sections that I need. When I'm under pressure, I shouldn't go through operating conditions, electrical characteristics, packaging, layout, and mechanical considerations. They are all safe to ignore in the first pass. 

I need to know if the output pin is an active high or low, especially if there is a modifier near the name denoted by --Name, nName, etc. I also need to know the pin description. Some samples of pin descriptions are VDD, VSS, HND1, GND, HORN, to name a few. I should also look at performance characteristics, which include the temperature, voltage, and the range and sample schematics, which is basically and electrical engineering version of the driver code. 

Read any transitions from tables/graph to text/diagrams. These are usually important. These include communication, initialization, and timing protocols/other requirements.



Let's say there's a triceratops, with horns and hands, and it has 2 stages a normal stage and a stage after charge. This will take  a long time to draw. schematicwise.

Hand 1 and foot 2 are in sync. 


So the steps to successful development can follow this trajectory:
1. Write the driver
2. Rereads parts in the description, read through the summary again.
3. Write the interface
4. Write the problem

When there is an underlying issue, use datasheets first. Then, look into notes, manual and forums, but only if you evaluate the datasheets first. 

Timing diagrams can also help software developers. Timing diagrams shows the relationship between 2 transitions. For example, if one hand moves shortly after the other is down, this may be effectively shown as a transition in the state diagram. We should look for dashed, critical lines with arrows, and we need to evaluate the component first and talk to certain vendors. 

Now it's time to look at the initial skipped sections and electrical characteristics. Here, we look for datasheets that meet our high standards. 

We evaluate the first choice. Is it fast? Is the amount of noise acceptable? Does it fit another  narrow application besides the application we are seeking? These are all questions that we need to answer. Choose the best 2 component designs for flexibility since the initial criteria may be too strict. 

After which we prototype the parts and type some code for real, finding the "best fit" and getting a quick feel of what each parameter does. 


We can use a rating chart to help drive decision making. We look at Vin to GND (V) the input (mA), the max junction temperature/operating temperature range and storage temperature range (C) and animation period(Mga) as well as charge speed (kph).

Learning a processor has around the same level of difficulty as simply learning a new programming language. After you learn the first language, picking up new ones is easier and easier. 

The software is talking to the processor through registers, which is very similar to keywords in a language, with many different types. The amount of documentation scales with processor complexity.

You can try to first look at the user manual for the processor. A getting started guide for the development kit usually has excellent documentation for setting systems up from scratch, and shows how to connect all the cables. Slides are good in evaluating a processor in a project. Wikis might have valuable links to forums to search for problems. Try to network with the vendor in order to get help later. For processors, skip the datasheet and go to the user manual. Always look at example code, the vast majority of the time they are a good place to start with. 

After this, do your homework and read the entire part of the manual that describes the issue on hand, even if there is already sample code. After this, I can work on reading the schematic. Some schematics have blocks of textual data at the corner of a page which is basically comments of code. Start by looking at boxes with lots of connections, and try to look for the larguest boxes and the processor can be one of these many sheets. 




If above the box, then you got a component on the printed circuit board. Look at the values and the extra letters of the processor describe how the processor is packaged. (AT9140008-66AI).


Memory usually has many connections, 8 or 16 bits. Some processors don't need RAM or FLASH independently. Components are labelled as "U" whereas connection lines are labelled as "J". If a connector has many signals, it can indicate a daughter board. Names are intended to give certain hints. You can also look for Sensor or ADC names. LEDs, switches and buttons are connected to the processor. Pull-ups pull the voltage up towards the power, which means when a processor isn't driving a line it is supposed to carry a high voltage. Pull ups are resistors to ground, which means the logic level is low whenever a processor isn't driving it.


Touching hardware can give an electrostatic zap and ruin it, so keep your board safe, always, always ALWAYS carry it around the bag it came in. Get an anti-static mat, it's like $20. Get wires glued down as well as soldered, and have the hardware should be connected to a power strip that can be turned off. Always ask for a spare board or two. There is a high probability of damaging the first board. 

Also get Needle nose pliers, tweezers, box cutters, digital multimeters, electrical tape, and sharpies, as well as safety glasses, screwdrivers, flashlights and cable ties. 

In the digital Multimeter you need a voltage mode with aroud 0.01V ganuarity. Then a resistance check mode with ohm symbol. DON'T DO RESISTANCE MODE WITH A POWERED BOARD UNLESS YOU KNOW WHAT YOU ARE DOING. 

Finally, we want current mode to measure up to the milliamp scale. If you have enough money try to get an oscilloscope, which measures analog signals, and a Logic analyzer which measures digital signals. Logical analyzers will help determine what you send with the communication, aka SPI, bus. The next paragraph analyzes how to set up an oscilloscope. 




First, determine which signals help to figure out a problem. Attach the scope's clip to the ground, and attach the probes to the signals of interest. Many hardware engineers put test points on their board.

Time varies through the x axis and voltage/current/other symboles vary throughout the y axis with the scale knobs at the bottom. There is also a knob to make a timescale change. Try each channel in order to see which knobs fit which scalewise, or translatewise. Set the zero point of the timescale to the middle, and make sure the scope is in run mode using the run/stop button. The single button waits for a trigger and puts the system into stop mode. 




How do we set up a trigger? We look for a trigger knob, which will put up a horizontal line to show where the trigger voltage level is. We want the trigger to be on the channel at the change of an interesting even, and when the trigger is activated. Don't put the scope in AC mode unless you know what you are doing and look at the probe 1x and 10x marker. The trigger signals says where to start the scope measurement at. 

Bandwidth is the maximum frequency range the scope can measure. You should have bandwidth around 5x the max frequency. It's recommended the signal's rise time be 5x the scope's listed rise time. We should have 5x sample rate of expected highest frequency. There should be as much resolution as possible. So bandwidth, signal rise, sample rate, and resolution. 

The next section is on testing the software. We need to build software in order to test the hardware effectively, in a manner conducive to easy debugging. There are 3 tests that are seen for embedded systems, which I will discuss all of them below. 

POST, or power-on self tests runs every time you boot the system, even after the code is released, verifying that all the hardware components are there to run the system safely. The more POST tests, the longer the boot times.

The second test refers should verify the software and hardware will work as expected, and these are unit tests. These are automated test codes that verifies a unit of source code is ready for use. Some developers want the tests to cover all possible paths through the code. Unit tests should:

1. test the basic functionality of a program
2. test the corner tests that are likely to occur. 

Some unit tests are external to the hardware, otherwise leave them in the production code and run them upon some special set of criteria.  They could be a first line check to see whether the hardware is acting up oddly. The final tests are created during the build-up of the unit tests. The final sorts of tests are created during bring up, especially if a subsystem is not functioning as intended and can be often used as throwaway checks. We want to build systems by testing subsystems, and deleting is fine because you can always recover when you realize that you need the test again. 

The code to control peripherals is often written when the schematic is being completed. However, you might have to write drivers to integrate both the software and hardware. The code to the control peripherals is often written while the schematic is being completed. For the system connected to flash memory, we need to verify:

1. IO lines under software control
2. The SPI
3. The Flash that can be read or written. 

You use a digital multimeter, logic analyzer and debug subsystems to analyze all of these, respectively. Maybe you run all inclusive test first, then run them separately. 

Flash is nonvolatile, so it doesn't get cleared when the power is turned off. Other forms of this memory are ROM(read-only memory) and EEPROM(electrically erasable programmable read-only memory). Flash usually has more space than EEPROM and is less power hungry. 

For POST, we shouldn't modify the flash system, but we might be able to set some constraints for the unit tests. 

These flash tests takes 3 parameters, the goal address, a pointer to the memory, and the memory length. There is an "umbrella" tests that runs the other tests, a test that reads from the flash, and a test that writes to the flash.

int FlashTest(unint32 address, unint8t *memory, uint16_t memLength);
uint16_t FlashRead(uint32_t addr, uint8_t *data, uint16_t dataLen);
uint16_t FlashWrite(uint32_t addr, uint8_t *data, uint16_t dataLen);

There are several tests that you can write for this. 

The first test is to read the existing data. This verifies that the IO lines are configured to work as the SPI input. First do a read and then check if the data is valid. 

//Test 1: Read the existing data in a block to make sure it is possible to read teh data. 
dataLen = FlashRead(startAddress, memory, memLength) {
    if(dataLen != memLength) {
        Log(LogUnitTest, LogLevelError, "Flash test: truncation on byte read");
        memLength = dataLen;
        error++;
    }

The next test erases data in the sector. It fills the flash with data, and we want to make sure that this is effective. This would be called/denoted the Byte access test. 

FlashEraseSector(startAddress);
addValue = 0x55;
for(i = 0; i < memLength; i++) {
    value = i + addValue; 
    dataLen = FlashWrite(startAddress + i, &value, i); 
    if(dataLen != 1) {
        Log(LogUnitTest, LogLevelError, "Flash test: byte write error.");
        error++;
    }
}

Test 3 is block access, which the flash contains our newly written junk to confirms that the block access works by putting back the original data. Then verify data on the byte-by-byte read. 

Here's the code: 

FlashEraseSector(startAddress);
dataLen = FlashWrite(startAddress, memory, memLength);
if(dataLen != memLength) {
    LogWithNum(LogUnitTest, LogLevelError, "Flash test: block write error, len ", dataLen);
    error++;
}

If the board passes these 3 tests, you can be confident that both the hardware and software work. 

So there's a standard method to test everything.

1. Read the original data
2. Write some changing junk
3. Verify the junk
4. Rewrite the original
5. Verify the original. 

We need to figure out what test will give the confidence that is working reliably. Designing these tests can be difficult. 

Let's say that you create test functions for each piece of the hardware. If one test fails, this is the test that you should focus on. You should also use a jump-handling table to send commands through C. Therefore, we need a small interpreter around the command table. The next thing that we want to get into is creating the command. 


The version of the code outputs the version information. Testing the flash runs the flash unit test, printing out number of errors. Blink LED sets the LED to blink at a given frequency and the help lists available commands with a one-line description. 

C is much often much faster vs. other languages like C++, so that's what I'll try to implement things in. 

typedef void(*functionPointerType) (void);
struct commandStruct {
    char const *name;
    functionPointerType execute;
    char const *help;
};

const struct commandStruct commands[] = {
    {"ver", &CmdVersion, "Display firmware version"},
    {"flashTest", &CmdFlashTest, "Runs the flash unit test, prints number of errors upon completion"}.
    {"blinkLed", &CmdBlinkLed, "Sets the LED to blink at a desired rate (parameter: frequency(Hz))"}.
    {"",o,""} //end of table indicator, MUST BE LAST!!!!
};


The help command is a macro command that prints out the name and help strings of all the items on the table. 

To see what function you want to run, use a switch case: 

switch(algorithm) {
    case eFIRFilter:
        return fir(data, dataLen); 
    case eIIRFilter:
        return iir(data, dataLen);

However, in object-oriented languages, you can make a reference to the inference. So we can declare functions that can do the same thing through polymorphism, and in C you would need a prototype of the function. 

You can this change the argument only when you need to change the function pointer. 

filter = &fir;
*filter(data, dataLen);

The con is that this (and several other commands such as switch-case statement) will usually interrupt branch prediction. 

To invoke the command, go to the table, and look for the string that matches and call the particular function pointer. However easy it sounds, the embedded systems have tight constraints in hidden dependencies. This limits the complexity to its own space. Each command performs action or calls a function to perform the action. A smart command implements the receiver which is the function. Usually, though it is good practice to separate the command and the receiver. We want to decouple the command from the actual action to be taken. If we don't know the contents of the request, we need to consider the following. 

In the case of the client, we want to map the receiver onto the commands. These clients create the association between receivers and command objects. Invokers determine when the command needs to run and executes it and doesn't know anything about the receiver. The command executes the operation in a C++ class, Java interface or C structure and Receiver services the request, and runs the receiver code. 

We can also add a macro command that implements multiple commands, and the client creates the list of basic commands to be combined as a result. We just call the execute functions, which goes as a better form of decoupling. Here are the diagrams.




The next thing I want to discuss is the longevity of code. At some point, your code will fail and there are 2 ways to handle errors as a result. You can perform graceful degradation where the software tries to the best it can. But you should also attempt to think about error-handling in the design. 


Functions should attempt to handle errors in the best way they can. If a variable is out of range, the range should be fixed, and the error should be logged as appropriate. Functions can allow a caller to deal with the problem by returning errors. The caller might deal with the error, which means passing it further upstream in the multilayered applications, and you want to return diagnostic code which is not only used for testing. ASSERT() is implemented in embedded systems but is straightforward to implement. This might be printing a result to the system log or a breakpoint statement, or even and LED that toggles an error condition. Error return codes, overall, should be standardized over the code base. Some error codes are no error (returns 0), unknown error, bad parameter, bad index, uninitialized variable, or catastrophic failure. There should be a minimum number of errors  so that the application can interpret them, and generalizations make error handling and usage easier, and hand developers important information. You can also call an error-handling library instead of this function:

error = FunctionFoo()
if(error != NO_ERROR) {
    ErrorSet (&globalErrorCode, error)
}

we call this function:

ErrorSet(&globalErrorCode, FunctionFoo())

The four functions are ErrorSet, ErrorGet, ErrorPrint, ErrorClear. ErrorPrint may change from writing out a log of information to a small function that just toggles an I/O line, and this will be given to the developer. 

The next thing to discuss is the debugging of timer errors. Here, the serial output such as logging or printf can change the timing of the code. A timing problem will appear depending on where the output statements are or breakpoints are located, and the debugging becomes difficult now. Using the error library, you can store the data locally in RAM. You can fill the buffer in trigger points such as ASSERT or ErrorSet and then dump the buffer, when the code exits the time-sensitive area. Circular Buffers are an example of this. Also use space processor I/Os to be available to the board and accessible showing the system status in testing and profiling processor cycles. 

Now for an interview question.

"Tell me about a project that you worked on that was successful? What was not successul about it"?

The question wants to describe the processes that lead to success, and that heavily depends on knowledge information and communication. Talk about how you seek out information, then talk subsequently about the big picture. Talk about other factors of a project other than the technical part. Then when what went wrong is asked, tell about how the applicant evaluated factors involved in trying for a successful project, and understand what a candidate has to do technically will give a good impression to the recruiter. 

Comments

Popular Posts