An architecture for designing reusable embedded systems software, Part 3 - Embedded.com

An architecture for designing reusable embedded systems software, Part 3

As discussed in Part 1 and Part 2 in this series, the ECU_HSIS.H hardware/software-interface specification contains references to three external files that are used to further define the microcontroller architecture: Compiler.H , Microports.H , and Vectors.H . Other essential elements of this architecture include the microcontroller memory map, system-clock definitions, microcontroller peripheral definitions, and hardware/software-interface specification definitions.

COMPILER.H
The information in this file is found in the compiler's handbook and would consist of the following:

• Reassignment of standard C-types

• Compiler numerical size limitations.

• Compiler-specific commands unique to the microprocessor.

Reassignment of standard C types
The assignment of types is dependent on the compiler and/or microcontroller. Reassigning the standard types and using them in the software will speed up the process of switching to another compiler/microcontroller, if the types are different. In one case, an unsigned long integer might take on the shape of 32-bits while in another case the same unsigned integer type might be 16-bits. Defining the bit length in the redefined type eliminates ambiguity. U16 translates to an unsigned data type of 16-bit length.

typedef unsigned char   U8;typedef signed char     S8;typedef unsigned short  U16;typedef signed short    S16;typedef unsigned long   U32;typedef signed long     S32;   

Compiler numerical size limitations
Variable size limitations define the sizes of the standard data types that are being used in your compiler/microcontroller architecture. The sizes are based upon the new assignment of types and modified if the standard type(s) are changed.

#define MAX_UCHAR       255#define POS_CHAR_MAX    127#define NEG_CHAR_MIN    (-128)#define MAX_USHORT      (0xFFFF)#define POS_SHORT_MAX   32767#define NEG_SHORT_MIN   (-32768)#define MAX_ULONG       (0xFFFFFFFF)   

Compiler-specific commands
These commands are specific to your compiler and implemented to provide access to special microcontroller features. For example, each compiler may have explicit ways to handle the definition of interrupt subroutines. One compiler implementation might require the “@” symbol followed by the word “interrupt” before a function statement to define that function as being an Interrupt Subroutine (ISR). The implementations may vary greatly between compiler providers and should be explicitly defined in this header file. Below are some examples of compiler specific commands. These commands are an extension to the programming language standard and provide the software engineer with access to specific embedded microcontroller features.

#define INTERRUPT @interrupt //define interrupt subroutine#define EEPROM	@eeprom//define eeprom variable storage#define ASM @asm//define assembly language inline code   

MICROPORTS.H
Information in the microports.h file would contain bit definitions for the configuration registers and I/O ports. Microcontroller suppliers provide most of this information and in many cases they might even supply a predefined header file for use with the microcontroller. In the example, PORT A is an 8-bit port with bits defined Pax, where x is the bit number from 0 to 7, and a configuration data direction register (DDR) bits DDAx. BITx is defined in a top-level header file where all of the bits in the data type are set to zero except for the x th bit.

// I/O PORT A REGISTER#define PA7    BIT7#define PA6    BIT6#define PA5    BIT5#define PA4    BIT4#define PA3    BIT3#define PA2    BIT2#define PA1    BIT1#define PA0    BIT0 // I/O PORT A DATA DIRECTION REGISTER#define DDA7    BIT7#define DDA6    BIT6#define DDA5    BIT5#define DDA4    BIT4#define DDA3    BIT3#define DDA2    BIT2#define DDA1    BIT1#define DDA0    BIT0   

The following is a truncated structure definition for the ports in a microcontroller. U8 stands for unsigned data type 8-bits long and is a reassignment of the standard C type defined in the COMPILER.H file.

typedef  struct {         volatile   U8 PORTA;         // port A         volatile   U8 PORTB;         // port B         volatile   U8 DDRA;         // data direction port A         volatile   U8 DDRB;         // data direction port B         volatile   U8 Reserved1;         volatile   U8 Reserved2;         volatile   U8 Reserved3;         volatile   U8 Reserved4;         volatile   U8 PORTE;         // port E         volatile   U8 DDRE;          // data direction port E         volatile   U8 PEAR;          // port E assignment register         volatile   U8 MODE;          // mode register         volatile   U8 PUCR;          // pull-up control register         volatile   U8 RDRIV;         // reduced drive of I/O lines         volatile   U8 Reserved8;         volatile   U8 Reserved9;                    .    .    .                    .    .    .                    .    .    .               } D60Regs_t;   

VECTORS
A separate file would define the interrupt subroutine vector table. In the following example, the isr_00 label jumps to a routine that has, as its only function, a return from subroutine. The isr_00 label is included at every location in the vector table that doesn't have an associated interrupt subroutine. If for some reason, an interrupt occurs on a channel that is not defined, function is immediately returned to the main processing. The microcontroller supplier usually provides a template of this file.

// INTERRUPT VECTOR TABLE: LOCATED AT $FFC2 // - $FFFF FOR HC12d60 const _vectab[]={    isr_00, // CGM lock and limp home     CAN_transmit_isr, // MSCAN tranmit     CAN_receive_isr, // MSCAN receive     CAN_error_isr,  // MSCAN errors     isr_00, // Pulse Accumulator B overflow     isr_00, // Modulus down counter underflow     isr_00, // Key wake-up G or H     CAN_wakeup_isr, // MSCAN wake-up     isr_00, // ATD 0 or 1 interrupt     isr_00, // SCI 1 interrupt     isr_00, // SCI 0 interrupt     isr_00, // SPI complete     isr_00, // Pulse accumulator input     isr_00, // Pulse accumulator overflow     isr_00, // Timer overflow interrupt     isr_00, // Timer channel 7 interrupt     ISR_THREE, // Timer channel 6 interrupt USED FOR MAIN // LOOP CLOCK     isr_00, // Timer channel 5 interrupt     isr_00, // Timer channel 4 interrupt     isr_00, // Timer channel 3 interrupt     isr_00, // Timer channel 2 interrupt     isr_00, // Timer channel 1 interrupt     ISR_TWO, // Timer channel 0 interrupt 2ms BACKGROUND // LOOP CLOCK     isr_00, // Real time interrupt     ISR_ONE, // INTCR IRQ     isr_00, // External interrupt request     isr_00, // Software interrupt     isr_00, // Unimplemented Instruction Trap     isr_00, // COP failure reset     isr_00, // Clock monitor fail reset     _stext, // reset }  

The rest of the ECU_HSIS.H would define the hardware to software interface. This file contains the following components:

1. The microcontroller's memory map.

2. System clock frequency defines.

3. Microcontroller peripheral definitions.

MICROCONTROLLER MEMORY MAP
Define the microcontrollers memory map upfront. This will help to quickly redefine the memory map when changing microcontrollers.

#define START_OF_REG_RAM       (0x0800)#define LENGTH_OF_REG_RAM      (512) // Register bytes#define START_OF_BASE_RAM      (0x0000)#define LENGTH_OF_BASE_RAM     (2048) // RAM bytes #define START_OF_EEPROM        (0x0c00)#define LENGTH_OF_EEPROM       (1024) // EEprom bytes #define END_OF_EEPROM          (START_OF_EEPROM+LENGTH_OF_EEPROM-1) #define START_OF_ROM           (0x1000)#define START_OF_VECTOR_TABLE  (0xFF00) // ISR vector table #define LENGTH_OF_VECTOR_TABLE (0xFF)   

System clock definitions
The heartbeat of the entire system is the external system clock that is usually implemented by using a crystal or resonator oscillator circuit. The microcontroller vendor usually recommends a range of allowable frequencies. All of the timing constraints, counters, pulse-width modulator, input capture/output compare modules, and communications interfaces are dependent on the external system clock frequency. Define the external clock frequency and the internal system clock (which may differ from the external clock) and use it to define other time-based variables. A modification to the system clock requires only an update of the system clock definition.

#define EXTERNAL_CLOCK_FREQ    16e6#define INTERNAL_SYSCLK        (EXTERNAL_CLOCK_FREQ /2)   

All clock prescalers and internal timers are based on the system clock parameters. The prescalers are defined by your microcontroller architecture. In the example, it is used to set the modulus down counter and the main timer clock.

#define MOD_CLOCK_PRESCALER    16#define TIMER_CLOCK_RESCALER   16#define MOD_DOWN_CTR_CLK       (INTERNAL_SYSCLK /         MOD_CLOCK_PRESCALER)#define MAIN_TIMER_CLK         (INTERNAL_SYSCLK /         TIMER_CLOCK_PRESCALER)   

If you are using a custom operating system, which is common in many automotive applications, the main loop timing variables are a function of the external clock frequency for the micro-controller and are based on the main timer definitions. The constants MAIN_LOOP_TIME , IO_LOOP_TIME , and ISR_LOOP_TIME are defined in real units of seconds. These constants should be part of a top-level operating system header file. For example, below we are given the following:

MAIN_LOOP_TIME is set to 0.010 seconds (or 10ms), an input/output processing time, IO_MS_TIME_TIME set to 0.001 seconds and an interrupt subroutine time, ISR_TIMER set to 0.002 seconds. If the clock frequency changes, the software engineer changes the EXTERNAL_CLOCK_FREQ define and that value is propagated through out the other timer defines.

#define MAIN_LOOP_TIME_TMR    (MAIN_LOOP_TIME / MAIN_TIMER_CLK)#define IO_MS_TIME_TMR        (IO_MS_ISR_TIME / MAIN_TIMER_CLK)#define ISR_TIMER1            (ISR_TIME / MAIN_TIMER_CLK)   

Since the external clock frequency is 16 MHz, the internal system clock is half of the external clock (i.e., 8 MHz) and the timer prescaler is 16 therefore the main timer is running at 500 KHz or one tic every 2 uSec (e.g., =1/(8,000,000/16) ). In the example, MAIN_TIMER_CLK is set to 0.000002 sec or 2 microseconds. Therefore the number of clock tics for 10 ms is equal to 10ms/2uSec or 5000 tics. We normally don't think about 10ms being 5000 clock tics, but the processor does. Having defined time values in this manner, we can continue to work in the real units domain, specifically seconds or milliseconds and let the pre-processor do the substitutions to convert to clock tics.

We can continue defining specific units of time based on the system clock definitions. For example, we can define a constant of ONE_SECOND equal to 1.0/MAIN_TIMER_CLOCK . The preprocessor calculates the tic required for a one second unit of time. When ONE_SECOND is used in the code, the meaning is clear (as opposed to using 500,000 counts or tic of the clock). The core software is not modified when the system clock frequency is changed, only the define statements in the ECU_HSIS.H file are updated to reflect the new system clock frequency.

Microcontroller peripheral definitions
In like manner, one would set up all of the microcontroller hardware peripherals within the ECU_HSIS.H header file. All defines would be equated to some real unit of measurement and then tied to a base architecture parameter. Any parameter specific to the electronic control module hardware and I/O processing is defined in this header file. For example, When setting up a pulse width modulated (PWM) output, define the PWM frequency and when controlling a current with the PWM signal, the duty cycle at the maximum allowable current limit would be defined.

#define PWM_FREQUENCY           (500)     // Hz #define DTYCYL_AT_MAX_DAMPER_I  ( 0.75 )  // 75% duty cycle=Max Amps   

Below is an example of a configuration for a 10-bit A to D converter for a 16-bit microcontroller. These values are derived from your microcontroller's reference manual (assuming that your analog to digital converter is an on chip peripheral).

// 10 Bit A2D resolution #define HW_ADC_STEPS    (1024) // Reference Voltage High #define HW_ADC_VREFHI   (5.0) // Max voltage input to A2D#define A2D_VIN_MAX     (5.0) // 204.8 counts = 1 volt#define COUNTS_PER_VOLT (HW_ADC_STEPS / HW_ADC_VREFHI) // 0.004883 Volts per count #define VOLTS_PER_COUNT (HW_ADC_VREFHI / HW_ADC_STEPS)   

Hardware/software-interface specifications definitions
Other parameters specific to hardware, which are not tied to the micro-controller, would also be defined here. An example would be constants that are used to do a Proportional-Integral-Derivative (PID) closed loop current control. The hardware team would design a voltage feedback circuit and specify the shunt resistor value that is in series with the output load. The shunt resistor would drop a voltage across it that is proportional to the amount of current through it. The formula used is Voltage = Current * Resistance. The hardware-software interface specification would define the shunt sense resistance and the typical impedance of the load. Also defined is the average voltage across the load, the fly-back voltage clamp, and the PWM drive voltage.

#define PWM_DRIVE_VOLTAGE        (13.5 ) // Volts#define FLYBACK_CLAMP_VOLTAGE    ( 0.7 ) // Volts#define SOLENOID_IMPEDENCE       ( 4.1 ) // Ohms #define SENS_RESISTOR            ( 0.2 ) // Ohms #define LOAD_SEN_OHM             (SENS_RESISTOR + SOLENOID_IMPEDENCE) // Ohms  

Dividends
The goal of the architecture presented in this series is to reduce development time and simplify maintainability of the software by the following methods:

1. Define highly reuseable modules that can be used on multiple programs, and

2. Localize the software modifications required when terminators external to the core functions are changed in the system.

The ideas presented here are simply a framework that one can build upon and modify to fit within their software architecture. The key is to wrap the core software with an interface layer thereby isolating the core software from modifications that occur outside of the interface layer. The components of the interface layer are reusable for systems that have identical terminators and are easily modified for new terminators that are introduced in the system.

Designing your software to be reusable takes more time up front but will pay long-term dividends of increased software quality, reduced development time, and increased maintainability. A company can maintain an entire library of signal.h files for every sensor input designed into their products. A similar library can be maintained for the ecu_hsis.h of every microcontroller/DSP that was ever used and for the interface.h of every conceivable interface ever developed. When it comes to a new design, the software engineer picks which modules to use for the interface layer and puts them together like lego blocks that completely surrounds the core software.

Dinu P. Madau is a Software Technical Fellow with Visteon. He has been developing software for embedded systems for over 22 years. He has an MSE in computer and electrical control systems engineering from Wayne State University and a BSE in computer engineering. Dinu has developed safety-critical software for anti-lock brakes, vehicle stability control, and suspension controls and is currently working in Advanced Global Technologies at Visteon developing driver awareness systems leveraging vision and radar technologies. He can be reached by e-mail at dmadau@visteon.com

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.