As discussed in Part 1 in this series, the linchpin in making this reusable embedded systems software architecture work is the software interface layer, which consists of three components:
1) Microcontroller specification (ECU_HSIS.H ).
2) I/O signals interface specification (I/O Signal #1, #2, #n ).
3) I/O interface macros (Interface.h , Interface.c ).
The ECU_HSIS.H file would contain references to three external files that are used to further define the microcontroller architecture, as shown in Figure 1. The base ECU_HSIS would define the I/O parameters from the microcontroller pins out a wiring harness used to interface to the sensors and drivers. Each one of the subheader files is specific to the internal workings of the CPU and will be discussed in detail later in this series.
I/O signal specification
For the I/O signals interface specification, define a file called SIGNALS.H . This file contains, almost verbatim, the specification of the sensors/actuators in the system as defined by the sensor/actuator supplier. For example, a system that uses an acceleration sensor supplied by company X has given the specification (shown in Table 1) that defines the signal.
In addition, defining a fixed-point scaling will make it easier to debug the variables since all of the parameters are in one location. In the accelerometer example, the scalar used is 210 , which translates into a resolution of the accelerometer sensor of 1/1,024 m/s/s.
#define VOLTS_PER_G ( 1.0 ) // Sensor Specification (V/g)#define ONE_G (9.812 * COUNTS_PER_M_PER_S2)#define VOLTS_PER_MPS2 ( 0.101916 ) // Sensor Specification (V/m/s^2)#define ACCEL_ZERO_PT_PROP ( 0.5 ) // proportion of Vss for 0 m/s/s
Define the scaling for fixed-point processors. In this case, 1mps^2 = 1,024 units, which stated differently defines the software resolution of the signal to 1/1024 mps^2 = 1 Least Significant Bit (LSB).
#define MPS2_ACCEL_SCALE (1024.) // 1 m/s/s = 1024 units#define ACCEL_RESOLUTION (1.0 / MPS2_ACCEL_SCALE // m/s^2#define ACCEL_SCALE (MPS2_ACCEL_SCALE / COUNTS_PER_M_PER_S2) // = 49#define ACCEL_ZERO_VALUE (HW_ADC_STEPS * ACCEL_ZERO_PT_PROP)
The ACCEL_SCALE parameter is calculated and used to derive the value of the fixed-point scaled sensor signal by multiplying it to the raw A to D count value. Given the defined parameters, the formula for determining the fixed point, scaled acceleration input signal is:
Accel (mps^2) = ACCEL_SCALE (mps^2/counts) * (A2D_PortValue (counts) - ACCEL_ZERO_VALUE (counts) )
The parameters for VOLTS_PER_MPS2 and HW_ADC_STEPS are defined in the hardware definitions header file. Optionally, it's possible to define the signal resolution in real units instead of voltage. The resolution of the sensor at the software input level is 1/1,024 volts, which can translate the volts into either mps^2 or Gs. These additional constants can be used throughout the software product for the specification of the signal resolution in real units.
#define M_PER_S2_PER_COUNT ( HW_ADC_VREFHI / VOLTS_PER_MPS2 / HW_ADC_STEPS )#define COUNTS_PER_M_PER_S2 ( 1.0 / M_PER_S2_PER_COUNT )#define COUNTS_PER_G ( COUNTS_PER_M_PER_S2 * 9.812 )
One of the specifications for the sensor is the start-up time. The start-up time is the time that the sensor signal is valid after the power-supply voltage is applied to the sensor. LOOP_TIME_SEC is defined in another file and in this example is set to 10 ms. The ACCEL_SENS_POWER_UP_TIME is a counter of the number of main operating-system loop counts that equates to 1 second (i.e., 100 loops = 1 sec).
#define ACCEL_SENS_POWER_UP_TIME ( 1. / LOOP_TIME_SEC ) // LATA_SENS_POWER_UP_TIME=1 sec
It is also beneficial to define the filter scaling in this file if the signal is being software filtered for noise. In this case, a first order low-pass filter is used so we define the Beta constants for the filter. The real units are used (i.e., 0 < Beta < 1.0) and converted to a fixed point by multiplying it by a predefined filter scale.
// FIRST ORDER FILTER CONSTANTS FOR Z// ACCELERATION SIGNALS #define ACCEL_BETA (0.04321 * FILTER_SCALE) // 50 Hz filter
This would be done for each of the I/O signals in the system. In summary, the SIGNALS.H file consists of information about the I/O signals and should contain, if applicable, the following information:
1. Sensor specifications.
2. Fixed-point scaling.
3. Real unit conversions.
4. Filter parameters.
The SIGNALS.H file almost takes on the form of a data dictionary for the I/O signals since all the information about the signals is defined in this header file.
I/O interface macros
The final step in designing a highly portable architecture is to define the interface layer that contains the macros for importing/exporting variables to/from the core software. Consider the following example:
#define ssGet_sensor_signal()(signed short int) (RAW_ANALOG_INPUT.SENSOR1)#define ssGet_nvram_data () (NVRAM_STRUCTURE.DATA_REGISTER)#define ssPut_nvram_data ( x ) (NVRAM_STRUCTURE.DATA_PORT = (x) )
This interface is defined by Get and Put macros whose definitions are hardware, machine and/or architecture dependent. In this previous section of code, the ssGet_sensor_signal macro looks for a signed short data structure (RAW_ANALOG _INPUT ) that is memory mapped to the A/D converters and picks out the SENSOR1 port of the A/D converter. By developing a set of import/export interface macros, the algorithm developer can code the core algorithm without being concerned about the origin for the signal. This method also lends itself easily to unit testing. When engineering is unit testing a software module on a PC, the interface for the signal sources are modified to reflect the simulated or real signals available for the test. Once the module is unit tested, the interface is written for the actual terminators of the system. The means, which is encapsulated by the macro, is of little interest once the interface is designed. What's important is the value of the sensor signal at a given time. Therefore, the interface in the main code would consist of Get and Put statements. When the hardware architecture changes, only the macro definition is modified to correspond to the change. The core software is completely void of any interface code with the exception of an interface header that links the outside real world to the inside micro-world.
The main types of macros defined in this interface header file consist of:
1. Reading input signals
#define Get_Veh_Lat_Accel()( GET_ACCELERATION ( adtREG.ADR03, UW_Y_ACCEL_ZERO_POINT) )
#define Get_RC_HT_SENS()( GET_HEIGHT (adtREG.ADR16, UW_HS_ZERO_POINT) )#define Get_BatteryVoltage()( GET_VOLTAGE (adtREG.ADR04, UW_VBAT_SCALE, UW_VBAT_ZERO_PT) )
In this case, the macro is used as a preprocessor definition that calls a function (i.e., GET_ACCELERATION() , GET_HEIGHT , or GET_VOLTAGE ). These functions filter the input signal and remove any zero-point bias offsets. They're written specific to the requirements for processing the signals. The user interfaces to the signal in the software via the get_ macro . If a signal requires no processing, the microcontroller port may be accessed directly as in the following example:
#define Get_Accel_A2D_V() ( adtREG.ADR00 >> 6 ) // 10bit A/D MSB justified
In this case, the voltage is directly read from the 10-bit analog to digital converter (i.e., register ADR00 in the A to D converter), which uses a 16-bit data type that is most-significant-bit justified and requires to be shifted right by six bits. The next example interrogates a digital input to determine if the ignition state is on by masking out a bit in the Port B register.
#define Get_IgnitionState() ( FLAGS.IGNITION_OFF = ~( PORTB & PB0 ) )
If the register is changed, the macro is updated and the rest of the software is left untouched.
2. Driving/writing to outputs
#define Put_TurnOnAccelSensPower()( PORTA |= PA7 )#define Put_TurnOffAccelSensPower()( PORTA &= ~PA7 )#define Put_TurnOffECUPowerSupply()( PORTB &= ~PB7 )#define Put_TurnOnECUPowerSupply()( PORTB |= PB7 )
In the examples above, the put statements define a bit function on a microcontroller port. The put macro is used to write/drive an output to a desired state in the core software. The put macro can take several forms including calling a function or writing a variable to a memory mapped I/O register.
3. Defining internal microcontroller interfaces
#define MAIN_LOOP_TIME_INTERRUPT (!(TIMER_FLAG & BIT1))
In this example, the I/O never reaches the outside world of the microcontroller. It's specific to an I/O signal within the microcontroller. The goal of the macro is to determine when the timer interrupt has occurred, which signals the start of a main loop timer. Another example would be to create a macro specific to the compiler for generating an assembly language op-code that would disable all interrupts.
#define Put_DisableAllInterrupts() (_asm("sei")) // inhibit all maskable interrupts
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 firstname.lastname@example.org