Showing posts with label gcc. Show all posts
Showing posts with label gcc. Show all posts

Monday, March 4, 2013

STM32F4 and “Most” of What You Ever Wanted to Learn about Its Analog Digital Converter (ADC)



When developing a lab for an upcoming course, I finally got the opportunity to work with the STM32F4 ADC. The capabilities of the MCU and its ADC almost qualify it as digital signal processor (DSP). Not surprisingly the ADC ended up a highly-configurable but arguably complex beast. The novelty of the F4 series makes documented sample code on the web sparse. 
In this post, I summarize and exemplify the different operating modes of a single ADC that comes with the STM32F4 microcontroller (MCU) from ST microelectronics. Dual ADC modes are not discussed (therefore "most" and not everything).
This post is targeted at engineers, hobbyists, students, and engineers with working knowledge of see and some basic exposure to the STM32 peripheral libraries (any of their STM32Fxxx series). I am explaining and exemplifying the different operating modes of the ADC and show where to look for the right information at each step.

Overview of Operating Modes

STMs Application Note AN3116 explains the different operating modes quite nicely but fails to address any practical considerations of their implementation. For an overview, I am just reviewing the properties shortly. The ADC can perform a single conversion, or continuously convert values. The conversion can be performed on a single channel or multiple channels. The latter is referred to as scan configuration. The permutation of these modes gives the different operating modes.
  1. Single-channel & single-conversion
  2. Multi-channel (scan) & multiple-conversion
  3. Single-channel & continuous operation
  4. Multi-channel (scan) & continuous operation
Each of the four modes can be triggered by modifying a memory-mapped register or an external trigger such as a timer. In any mode the sample time per channel can be specified as number of ADC clock steps. Furthermore, the STM32F4 contains an analog watchdog that triggers an interrupt when values fall outside a specified range. In any case interrupts can be used to signal the completion of a conversion.
The first mode performs a single conversion on a single channel when triggered and then stops. This can be used to measure a calibration value or perform checks between different program states.
The second mode performs a scan conversion, reading one channel after the other (of up to 16 channels in a single scan). This is very useful to capture sensor data at discrete points in time such as positions.
The third mode converts a single channel continuously. This can be useful to get data on a critical analog input.
The last mode is just scan conversion performed continuously.

Setup Description

Being an ARM processor, peripherals need to be clocked to be switched on. This applies to all components that have anything to do with the clock configuration. Again the STM32 being high-end MCU from STM makes the clock three a bit of a Maze (at the time of writing to be found in Reference Manual 90, Figure 9).
Use their clock configuration utility that actually generates you the initialisation code; or, be brave, print the page and determines the register value yourself. Once the clock itself is set up, the individual peripherals can be enabled with the RCC_xxx functions, depending on the bus they are connected to.
In this example we use ADC1, which is connected to Advanced Peripheral Bus 2 (APB2), which is a subdivision of the ARM AMBA high-performance bus (AHB). In this example it is assumed that the main clock is configured at 168 MHz and that APB2 runs at 84 MHz. Furthermore to constrain the scope just to the ADC we use the internal reference channels VREF_int and Temperature as input for our experiments.

Single-Conversion Mode


Based on the setup-description we describe in this chapter how to perform a single-channel conversion.
The initialisation looks as follows:

/* Define ADC init structures */
 ADC_InitTypeDef       ADC_InitStructure;
 ADC_CommonInitTypeDef ADC_CommonInitStructure;

 /* IMPORTANT: populates structures with reset values */
 ADC_StructInit(&ADC_InitStructure);
 ADC_CommonStructInit(&ADC_CommonInitStructure);

 /* enable ADC clock */
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

 /* init ADCs in independent mode, div clock by two */
 ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
 ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
 ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
 ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
 ADC_CommonInit(&ADC_CommonInitStructure);

 /* init ADC1: 12bit, single-conversion */
 ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
 ADC_InitStructure.ADC_ScanConvMode = DISABLE;
 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
 ADC_InitStructure.ADC_ExternalTrigConvEdge = 0;
 ADC_InitStructure.ADC_ExternalTrigConv = 0;
 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
 ADC_InitStructure.ADC_NbrOfConversion = 1;
 ADC_Init(ADC1, &ADC_InitStructure);

 /* Enable VREF_INT & Temperature channel */
 ADC_TempSensorVrefintCmd(ENABLE);

 /* Enable ADC1 **************************************************************/
 ADC_Cmd(ADC1, ENABLE);
The most important take home is to always initialise the structures of the STM32 peripheral libraries. Since those structures are allocated from the stack the values are unpredictable. Using the InitStruct methods of the peripheral library, the structures will always reflect the reset state of the peripheral.
In addition, even though we are just using a single ADC channel on a single ADC, we have to initialise the entire ADC block. The ADC block can operate in interleaved or independent mode. To use a single channel only the ADC block has to be configured in independent mode.
Furthermore, for a single conversion the modes Scan and Continuous are disabled. The internal VREF_INT and TEMP channels have to be explicitly enabled. They are disabled by default to conserve power. Finally the ADC has to be enabled for further usage.

The read function is relatively uneventful. After the ADC has been initialised, all we have to specify is the ADC channel, the sampling time, and wait for the conversion to complete.

uint16_t adc_read(ADC_TypeDef* ADCx, uint8_t channel, uint8_t ADC_SampleTime) {
 /* Configure Channel */
 ADC_RegularChannelConfig(ADCx, channel, 1, ADC_SampleTime);

 /* check if conversion was started, if not start */
 ADC_SoftwareStartConv(ADCx);

 /* wait for end of conversion */
 while((ADC_GetFlagStatus(ADCx, ADC_FLAG_EOC) == RESET));

 return ADC_GetConversionValue(ADCx);
}
The temperature channel and the voltage can then be read individually as follows:
uint16_t temp = adc_read(ADC1, ADC_Channel_16, ADC_SampleTime_480Cycles);
uint16_t vrefint = adc_read(ADC1, ADC_Channel_17, ADC_SampleTime_480Cycles);
The temperature sensor is wired to ADC channel 16 and VREF_INT is wired to ADC channel 17. In both cases a sample time of 480 ADC clock cycles (approx 11.4uS) and 12 cycles (0.3uS) ramp up time. The respective STM32F4 datasheet gives the conversion values for the temperature and the voltage. Some examples for the STM32F4Discovery are shown below.
#define ADC_TEMPERATURE_V25       760  /* mV */
#define ADC_TEMPERATURE_AVG_SLOPE 2500 /* mV/C */

int32_t adc_value_to_temp(const uint16_t value, const uint16_t steps_per_volt) {
 /* convert reading to millivolts */
 int32_t mv = ((uint32_t)value * 1000)/steps_per_volt;
 return (mv - ADC_TEMPERATURE_V25) / 25 + 25;
}

uint16_t adc_steps_per_volt(const uint16_t vref_value) {
 return (vref_value * 10) / 12; /* assume 1.2V internal voltage */
}
The example shown so far can be used for simple ADC experiments, in which one has to programmatically read an analog channel non-periodically. This mode can be useful for the calibration of sensors or for testing environmental conditions, when entering a different program state.

ADC Scan-Mode with Single-Conversion Interrupt


After having demonstrated how to read individual channels programmatically, we will describe how the scan-mode of the ADC works.
Since the ADC only has a single register that stores the information of the last conversion, there are only two ways to retrieve the values that are sampled through the scan.

  • Configure an interrupt to trigger after each channel is sampled,
  • Configure the DMA controller to copy the data of the channels into a defined memory location.
In this section we will illustrate the former and describe the latter mode for continuous-mode operation later on. Since this example uses interrupts, the nested vectored interrupt controller (NVIC) needs to be initialised as well. The code for the initialisation is shown below:
/* Unchanged: Define ADC init structures */
        ADC_InitTypeDef       ADC_InitStructure;
        ADC_CommonInitTypeDef ADC_CommonInitStructure;
        NVIC_InitTypeDef NVIC_InitStructure;

        /* Unchanged: populate default values before use */
        ADC_StructInit(&ADC_InitStructure);
        ADC_CommonStructInit(&ADC_CommonInitStructure);

        /* Unchanged: enable ADC peripheral */
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

        /* Unchanged: init ADC */
        ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
        ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
        ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
        ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
        ADC_CommonInit(&ADC_CommonInitStructure);

        /* Changed: Enabled scan mode conversion*/
        ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
        ADC_InitStructure.ADC_ScanConvMode = ENABLE; 
        ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; 
        ADC_InitStructure.ADC_DataAlign= ADC_DataAlign_Right; 
        ADC_InitStructure.ADC_ExternalTrigConv= 0; 
        ADC_InitStructure.ADC_ExternalTrigConvEdge= 0; 
        ADC_InitStructure.ADC_NbrOfConversion= 2; 

        ADC_Init(ADC1, &ADC_InitStructure);

        /* Enable Vref & Temperature channel */
        ADC_TempSensorVrefintCmd(ENABLE);

        /* Configure channels */
        /* Temp sensor */
        ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_480Cycles);
        /* VREF_int (2nd) */
        ADC_RegularChannelConfig(ADC1, ADC_Channel_17, 2, ADC_SampleTime_480Cycles);



        ADC_EOCOnEachRegularChannelCmd(ADC1, ENABLE);

        /* Enable ADC interrupts */
        ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);

        /* Configure NVIC */
        NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;
        NVIC_Init(&NVIC_InitStructure);

        /* Enable ADC1 **************************************************************/
        ADC_Cmd(ADC1, ENABLE);
In essence what changed is that now the ADC_ScanConvMode is set enabled, the channel initialisation has been moved into the initialisation routine, the NVIC configuration has been added.
The order of the channel conversion is controlled by assigning priorities (1...16) as third parameter to ADC_RegularChannelConfig. The total number of channels to be converted in sequence is specified by  ADC_InitStructure.ADC_NbrOfConversion.
As said before since the data register of the ADC only holds the last converted value, we have to trigger the interrupt on every conversion. This is done by calling ADC_EOCOnEachRegularChannelCmd(ADC1, ENABLE). This call enables the end-of-conversion flag after each channel, which triggers the end-of-conversion interrupt every time this flag is set.
In the interrupt service routine (ISR) the converted value can be copied to a global buffer and the ISR has to acknowledge the interrupt once the value is received. A global counter can be used to identify the channel that was converted.

uint16_t temp = 0;
uint16_t vref = 0;
uint16_t counter = 0;

void ADC_IRQHandler() {
        /* acknowledge interrupt */
        uint16_t value;
        ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);

        value = ADC_GetConversionValue(ADC1);
        if(counter % 2 == 0) {
                temp = value;
        } else {
                vref = value;
        }
        counter++;
}
It should be strongly noted that in the main program the scan is started by ADC_SoftwareStartConv(ADC1). The counter value can be checked against to determine when to read out the values.
        ADC_SoftwareStartConv(ADC1);
        while(counter <= 2);
        if(counter>= 2) {
                uint16_t v1, v2;
                v1 = temp;
                v2 = vref;
                /* … */
        }
This example shows how to read out multiple channels of an ADC in a single-scan. Interrupts are needed to identify the converted values of the individual channels. This mode is useful, when data points from multiple sources need to be taken at the same time.

ADC Continous-Scan Mode with DMA


After having discussed how to sample multiple values in a single sweep, we modify the example to execute the ADC continuously. Furthermore, because the interrupt after each channel conversion created substantial overhead, we use DMA in this example to copy the channel values to a cyclic buffer and just interrupt after the conversion of all channels is complete.
volatile uint16_t ADCBuffer[] = {0xAAAA, 0xAAAA, 0xAAAA};

...

        /* Define ADC init structures */
        ADC_InitTypeDef       ADC_InitStructure;
        ADC_CommonInitTypeDef ADC_CommonInitStructure;
        NVIC_InitTypeDef NVIC_InitStructure;
        DMA_InitTypeDef DMA_InitStructure;

        /* Initialise DMA */
        DMA_StructInit(&DMA_InitStructure);

        /* Enable clock on DMA1 */
        /* Enable DMA2, thats where ADC is hooked on -> see Tab 20 (RM00090) */
        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);               
        /* config of DMAC */
        DMA_InitStructure.DMA_Channel = DMA_Channel_0;                     
        DMA_InitStructure.DMA_BufferSize = 2;                       /* 2 * memsize */
        DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;     /* direction */
        DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;      /* no FIFO */
        DMA_InitStructure.DMA_FIFOThreshold = 0;
        DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
        DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
        DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;            /* circular buffer */
        DMA_InitStructure.DMA_Priority = DMA_Priority_High;        /* high priority */
        /* config of memory */
        DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)ADCBuffer; /* target address */
        DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; /* 16 bit */
        DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; /* increment after wrt */
        /* config of peripheral */
        DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
        DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
        DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
        DMA_Init(DMA2_Stream0, &DMA_InitStructure); /* See Table 20 for mapping */
        DMA_Cmd(DMA2_Stream0, ENABLE);

        ADC_StructInit(&ADC_InitStructure);
        ADC_CommonStructInit(&ADC_CommonInitStructure);

        /* init ADC clock */
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

        /* init ADC */
        ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
        ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
        ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
        ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
        ADC_CommonInit(&ADC_CommonInitStructure);

        ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
        ADC_InitStructure.ADC_ScanConvMode = ENABLE;
        ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
        ADC_InitStructure.ADC_ExternalTrigConvEdge = 0;
        ADC_InitStructure.ADC_ExternalTrigConv = 0;
        ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
        ADC_InitStructure.ADC_NbrOfConversion = 2; /* 2 channels in total */
        ADC_Init(ADC1, &ADC_InitStructure);

        /* Enable Vref & Temperature channel */
        ADC_TempSensorVrefintCmd(ENABLE);

        /* Configure channels */
        /* Temp sensor */
        ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_480Cycles);
        /* VREF_int (2nd) */
        ADC_RegularChannelConfig(ADC1, ADC_Channel_17, 2, ADC_SampleTime_480Cycles);

        /* Enable ADC interrupts */
        ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);

        /* Enable DMA request after last transfer (Single-ADC mode) */
        ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE);

        /* Enable ADC3 DMA */
        ADC_DMACmd(ADC1, ENABLE);
        /* Configure NVIC */
        NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;
        NVIC_Init(&NVIC_InitStructure);

        /* Enable ADC1 **************************************************************/
        ADC_Cmd(ADC1, ENABLE);
The major change to the previous configurations is that now scan mode and continuous modes are enabled. Furthermore the DMA is configured to copy the buffer off the ADC register into the target location. It should be strongly noted that individual DMA peripherals and their channels are multiplexed among multiple components. The mapping is explained in Table 20 in RM00090. Furthermore, it should be noted that the DMA performs after the EOC interrupt triggers. A reliable exchange protocol would be to copy the previous buffer value of the DMA buffer in the scan conversion for the main control program or to configure DMA interrupts (which is beyond the scope of this article).
void ADC_IRQHandler() {
        /* acknowledge interrupt */
        ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);

        /* nothing to do here except for possibly copying DMA buffers */
}

This example shows how to convert ADC channels continuously and store them in a global buffer using DMA. The ADC interrupt can be used to implement a synchronization scheme with the main program. This mode is useful for applications, in which a stream of continuous data is needed. An alternative to the cyclic buffer described in the example would be to use a linear buffer and use a counter in the ADC to abort the sampling after a fixed number of samples have been converted.

ADC Continous-Scan Mode with DMA and Additional GPIO Channels


So far we have just discussed examples that read off the on-chip channels. We modify the previous example to add three additional external pins as input to the ADC. We have chosen ADC_IN 10 ... 12, which are mapped to PORTC 0 ... 2. Note the pin mapping can be obtained from the datasheet of your STM32F4 chip. For the STM32F4Discovery the mapping is highlighted in the above picture.
The modified initialisation looks as follows:

/* Define ADC init structures */
        ADC_InitTypeDef       ADC_InitStructure;
        ADC_CommonInitTypeDef ADC_CommonInitStructure;
        NVIC_InitTypeDef NVIC_InitStructure;
        DMA_InitTypeDef DMA_InitStructure;
        GPIO_InitTypeDef GPIO_InitStructure;

        /* Enable clock on DMA1 & GPIOC */
        /* Enable DMA2, thats where ADC is hooked on -> see Tab 20 (RM00090) */
        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_DMA2, ENABLE);

        /* Initialise GPIOs C0 (ADC123_IN10), C1 (ADC123_IN11), C2 (ADC123_IN12)*/
        GPIO_StructInit(&GPIO_InitStructure);
        GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2;
        GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AIN;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_NOPULL;
        GPIO_Init(GPIOC, &GPIO_InitStructure);

        /* Initialise DMA */
        DMA_StructInit(&DMA_InitStructure);

        /* config of DMAC */
        DMA_InitStructure.DMA_Channel = DMA_Channel_0; /* See Tab 20 */
        DMA_InitStructure.DMA_BufferSize = 5; /* 5 * memsize */
        DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;/* direction */
        DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;/* no FIFO */
        DMA_InitStructure.DMA_FIFOThreshold = 0;
        DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
        DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
        DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; /* circular buffer */
        DMA_InitStructure.DMA_Priority = DMA_Priority_High;
        /* config of memory */
        DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)ADCBuffer;/* target addr.  */
        DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; /* 16 bit */
       /* config of peripheral */
        DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
        DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
        DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
        DMA_Init(DMA2_Stream0, &DMA_InitStructure); /* See Table 20 for mapping */
        DMA_Cmd(DMA2_Stream0, ENABLE);

        ADC_StructInit(&ADC_InitStructure);
        ADC_CommonStructInit(&ADC_CommonInitStructure);

        /* init ADC clock */
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

        /* init ADC */
        ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
        ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
        ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
        ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
        ADC_CommonInit(&ADC_CommonInitStructure);

        /* ADC1 Init: this is mostly done with ADC1->CR */
        ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
        ADC_InitStructure.ADC_ScanConvMode = ENABLE;
        ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
        ADC_InitStructure.ADC_ExternalTrigConvEdge = 0;
        ADC_InitStructure.ADC_ExternalTrigConv = 0;
        ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
        ADC_InitStructure.ADC_NbrOfConversion = 5; /* 5 channels in total */
        ADC_Init(ADC1, &ADC_InitStructure);

        /* Enable Vref & Temperature channel */
        ADC_TempSensorVrefintCmd(ENABLE);

        /* Configure channels */
        /* Temp sensor */
        ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_480Cycles);        
        /* VREF_int (2nd) */
        ADC_RegularChannelConfig(ADC1, ADC_Channel_17, 2, ADC_SampleTime_480Cycles);         
        /* PC0 */
        ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 3, ADC_SampleTime_480Cycles);
        /* PC1 */
        ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 4, ADC_SampleTime_480Cycles);
        /* PC2 */
        ADC_RegularChannelConfig(ADC1, ADC_Channel_12, 5, ADC_SampleTime_480Cycles);

        /* Enable ADC interrupts */
        ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);

        /* Enable DMA request after last transfer (Single-ADC mode) */
        ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE);

        /* Enable ADC3 DMA */
        ADC_DMACmd(ADC1, ENABLE);

        /* Configure NVIC */
        NVIC_InitStructure.NVIC_IRQChannel = ADC_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;
        NVIC_Init(&NVIC_InitStructure);

        /* Enable ADC1 **************************************************************/
        ADC_Cmd(ADC1, ENABLE);

The main difference to the previous example is that now a GPIO initialization has been added to the initialization, that the total number of conversions changed, and that the number and priorities of the input channels changed.
The GPIOs used as inputs need to be configured as analog inputs (AIN) and not be pulled up or down. The configured port speed is not relevant for the AIN mode. Furthermore, the clock (RCC) for the GPIO also needs to be enabled for the block to function properly.
The outcomes of this experiment are similar to the previous example, except that now three external pins are added to the buffer that contains the analog conversions.

Time-Triggered ADC-Scan Mode with DMA and Additional GPIO Channels

So far we have discussed the different operating modes of the ADC that are triggered by software. In addition to those modes the ADC can also be triggered by an external pin or a timer source. In this example, we will demonstrate how to modify the code of the previous example to be triggered from a timer.
In this case we enable Timer2 to provide the clock for the ADC. To synchronize the transfer we enable the interrupt of timer two. Note any timer is likely connected to a different part of the AHB bus structure than the ADC; it is always wise to look at the clock tree and validate the clock configuration.
The code is shown below:

 /* Define ADC init structures */
        ADC_InitTypeDef       ADC_InitStructure;
        ADC_CommonInitTypeDef ADC_CommonInitStructure;
        NVIC_InitTypeDef NVIC_InitStructure;
        DMA_InitTypeDef DMA_InitStructure;
        GPIO_InitTypeDef GPIO_InitStructure;
        TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

        /* Enable timer (timer runs at 21 MHz)*/
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
        TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
        TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
        TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
        TIM_TimeBaseStructure.TIM_Period = 1999;
        TIM_TimeBaseStructure.TIM_Prescaler = 17999;
        TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
        TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_Update);
        TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

        NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;
        NVIC_Init(&NVIC_InitStructure);

        /* Enable clock on DMA1 & GPIOC */
        /* Enable DMA2, thats where ADC is hooked on -> see Tab 20 (RM00090) */
        RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_DMA2, ENABLE);

        /* Initialise GPIOs C0 (ADC123_IN10), C1 (ADC123_IN11), C2 (ADC123_IN12)*/
        GPIO_StructInit(&GPIO_InitStructure);
        GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2;
        GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AIN;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_NOPULL;
        GPIO_Init(GPIOC, &GPIO_InitStructure);

        /* Initialise DMA */
        DMA_StructInit(&DMA_InitStructure);

        /* config of DMAC */
        DMA_InitStructure.DMA_Channel = DMA_Channel_0; /* See Tab 20 */
        DMA_InitStructure.DMA_BufferSize = 5; /* 5 * memsize */
        DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; /* direction */
        DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; /* no FIFO */
        DMA_InitStructure.DMA_FIFOThreshold = 0;
        DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
        DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
        DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; /* circular buffer */
        DMA_InitStructure.DMA_Priority = DMA_Priority_High; /* high priority */
        /* config of memory */
        DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)ADCBuffer; /* target addr */
        DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; /* 16 bit */
        DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
        DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
        DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
        DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
        DMA_Init(DMA2_Stream0, &DMA_InitStructure); /* See Table 20 for mapping */
        DMA_Cmd(DMA2_Stream0, ENABLE);

        /* IMPORTANT: populate default values before use */
        ADC_StructInit(&ADC_InitStructure);
        ADC_CommonStructInit(&ADC_CommonInitStructure);

        /* reset configuration if needed, could be used for previous init */
        ADC_Cmd(ADC1, DISABLE);
        ADC_DeInit();

        /* init ADC clock */
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

        /* init ADC */
        ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
        ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
        ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
        ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
        ADC_CommonInit(&ADC_CommonInitStructure);

        /* ADC1 Init: this is mostly done with ADC1->CR */
        ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
        ADC_InitStructure.ADC_ScanConvMode = ENABLE;
        ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
        ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising;
        ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO;
        ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
        ADC_InitStructure.ADC_NbrOfConversion = 5; /* 5 channels in total */
        ADC_Init(ADC1, &ADC_InitStructure);

        /* Enable Vref & Temperature channel */
        ADC_TempSensorVrefintCmd(ENABLE);

        /* Configure channels */
        ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_480Cycles);         
        ADC_RegularChannelConfig(ADC1, ADC_Channel_17, 2, ADC_SampleTime_480Cycles); 
        ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 3, ADC_SampleTime_480Cycles);
        ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 4, ADC_SampleTime_480Cycles);
        ADC_RegularChannelConfig(ADC1, ADC_Channel_12, 5, ADC_SampleTime_480Cycles);

        /* Enable DMA request after last transfer (Single-ADC mode) */
        ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE);

        /* Enable ADC3 DMA */
        ADC_DMACmd(ADC1, ENABLE);

        /* Enable ADC1 **************************************************************/
        ADC_Cmd(ADC1, ENABLE);

        … /* in main */ 
        TIM_Cmd(TIM2, ENABLE);
As you can see the timer is enabled to feed a signal into the ADC. Upon expiry of the timer period an interrupt is triggered that initiates the ADC. During the conversion the DMA copies the samples into the cyclic buffer. The timer ISR can be used to synchronize the transfer to the main program, by copying the “previous” sample into a global buffer.
void TIM2_IRQHandler() {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        
        /* nothing to do here except for possibly copying DMA buffers of previous conversion */
}
This example shows how to use a timer as clock source to pace the ADC. This configuration is useful for applications that require a data points at periodic intervals. In this post we demonstrated the four basic operating modes of the ADC. We furthermore demonstrated practical modifications of these scenarios that involve an external trigger and data coming from an external pin. We have left out specific concepts such as injected channels or the interleaved operation of channels, which will be discussed in different posts.

References

Monday, February 8, 2010

On Designing Boot Loaders and Grey-box-Testing Firmware (Part 2/2)

In the past tutorial, we have established how to integrate two pieces of code together, exemplifying a boot-loader and firmware interaction. The difference to the previous scenario is that in a test-suite, you actually need to maintain a symmetric interaction among those two different pieces of code. There are several approaches how this can be achieved.

  • Explicitly pin each data-structure to specific locations in the memory. So each of the code pieces knows where to look for the others code and data.
  • Pin an entry point of the firmware to a specific memory section that registers tests in another shared memory section.
  • Pin an entry point of the firmware to a specific memory section that registers tests in a data-structure provided by the test-suite.

Looking at the different options it becomes apparent, why we talk about “grey-box testing”. In all cases we need to know some memory sections within the other pieces of code. The approaches differ by the number of memory sections required to be pinned. In addition, you might want to load tests dynamically through the boot loader requiring additional pinned sections.

Step One: How to Customize the Locations of Code and Data

In principle you want to place individual data structures and code into labelled memory sections, defining the location and label in the linker script and referencing label in GCC for the linker. The attribute-section paradigm allows us to do so. Suppose we want to load a function called test into a block of memory at an absolute address. First, we need to define this memory section in the linker script as follows:

MEMORY
{
ram : ORIGIN = 0x10200000, LENGTH = 1M
}

SECTIONS
{
.text :
{
*(.text)
*(.rodata*)
} > ram

.data :
{
*(.data)
} > ram

.bss :
{
*(.bss)
} > ram

__TESTS__ 0x10300000:
{
*(__TESTS__)
}
}

Second, we need to reference this section in the code. This is done by declaring an attribute in the function’s specification as follows:

void __attribute__ ((section ("__TEST_INIT__"))) init_tests() {
...
}

The listing shows that the function is indeed stored at that particular location.

Sections:
Idx Name Size VMA LMA File off Algn
...
3 __TEST_INIT__ 00000030 10400000 10400000 00006000 2**1
...
Disassembly of section __TEST_INIT__:

10400000 :
...

Likewise global data structures and variables can be stored at such particular locations.



typedef struct {
void (*test1_fun)();
} TestFixture;

TestFixture __attribute__ ((section("__TESTS__"))) tests;

Step Two: Designing the Tests

We proposed three options for the test integration in the introduction.

Option One: (Naïve) Tell everything

The first naïve approach is to actually pin each testable primitive and global data structure to a particular memory region. In this scenario, all entry points to these primitives and global data structures are declared in the linker script of the test code. The linker script of the firmware declares all of those sections and the primitives are pinned to these sections using the attribute-section paradigm. This approach reduces the overhead of implementing tests vastly, since all locations are defined and no futher registration of the firmware with the tests is needed. Expected results can be directly checked against the data structures. However, maintaining the linker scripts in-sync, handling fragmentation of the firmware code (i.e., huge gaps between the declared sections) and changing the firmware code incur at significant overhead.

Option Two: Let the Firmware Register with a Global Data Structure

This approach is geared to minimize the overhead of maintaining the memory locations. In this scenario the firmware voluntarily registers with the test code, placing the information into a shared data-structure. In this scenario, two locations need to be shared across the firmware and the test-code.

  • The location of the registration routine that is to be implemented by the firmware code.
  • The location of the global data structure that contains the test information.

In addition the specification of the test data structure as well as the registration interface need to be shared among the two pieces of code. This can be achieved by sharing a common header file.

This scenario is useful when the test information is known beforehand. It also enables testing slightly modified versions of the firmware because the test code does not need to be aware of the location of the primitives or data structures. The firmware voluntarily provides this information through the registration routine. Both linker scripts define the section of the global data structure. Both linker scripts define the sections of the global data structure and registration routine. In order to avoid adverse effects the test code may prevent explicit writing to the registration routine. Here the test linker script:

MEMORY
{
sram : ORIGIN = 0x10200000, LENGTH = 1M
}

__FIRMWARE_ = 0x10100000;

/* firmware's test registration routine */
__REGISTER_TEST__ = 0x10300000;

SECTIONS
{
...
/* shared test data goes here */
__TEST_DATA__ 0x10400000:
{
*(__TEST_DATA__)
}
}

And the firmware linker script looks like this:

MEMORY
{
sram : ORIGIN = 0x10100000, LENGTH = 1M
}

SECTIONS
{
...
/* firmware's test registration routine */
__REGISTER_TEST__ 0x10300000:
{
*(__REGISTER_TEST__)
}

/* shared test data goes here */
__TEST_DATA__ 0x10400000:
{
*(__TEST_DATA__)
}
}

The shared header among the firmware and the test code defining the structure of the shared data structure and the registration interface:

typedef struct {
void (*firmware_fun)();
} TestFixture;

TestFixture __attribute__ ((section("__TEST_DATA__"))) gTestFixture;

extern void __REGISTER_TEST__();

The test code inside the bootloader simply registers with the firmware and invokes the required primitives of the firmware:

int main(void)
{
debug_puts("Inside boot-loader test suite!\r\n");

/* register test structure */
__REGISTER_TEST__();
/* execute a firmware function to test */
gTestFixture.firmware_fun();

debug_puts("Inside boot-loader again!\r\n");

return 0;
}

The location of the registration code inside the firmware is pinned as follows:

void  __attribute__ ((section ("__REGISTER_TEST__"))) test_register() {
gTestFixture.firmware_fun = myprimitive;
}

Merging the test suite with the firmware and executing it in the coldfire simulator, as described in Part one of this tutorial, yields the following output. The test code can be obtained from option2.zip (see attachments below).

Use CTRL-C (SIGINT) to cause autovector interrupt 7 (return to monitor)
Loading memory modules...
Loading board configuration...
Opened [/usr/local/coldfire/share/coldfire/cjdesign-5307.board]
Board ID: CJDesign
CPU: 5307 (Motorola Coldfire 5307)
unimplemented instructions: CPUSHL PULSE WDDATA WDEBUG
69 instructions registered
building instruction cache... done.
Memory segments: dram timer0 timer1 uart0(on port 5206)
uart1(on port 5207) sim flash sram

!!! Remember to telnet to the above ports if you want to see any output!
Hard Reset...
Initializing monitor...
Enter 'help' for help.
dBug> dl merged.s19
Downloading S-Record...
Done downloading S-Record.
dBug> go 0x10200000
... telnet on uart0
Inside boot-loader test suite!
Inside firmware primitive!
Inside boot-loader again!

Option Three: Only Register with the Firmware

This option obviates the use of a shared data structure and may be used in the case where the test code has access to enough memory to allocate its own data structures. Usually, the constraints on boot loaders and such testers are relatively low that this is not an option. In this case we only have to share parts of the test data structure and the specification of the registration interface. The only difference to the previous example is that now the registration routine takes a pointer to the test structure provided by the test code. In addition only the prefix of the structure needs to be identical across the two pieces of code. For example the test code may choose to store test results in the structure that are hidden from the firmware. Let’s look at an example. As follows the specification of the test structure for the bootloader. Notice the removal of the pinned global variable and the additional value.

typedef struct {
void (*firmware_fun)();
int someTestingValue;
} TestFixture;

extern void __REGISTER_TEST__(TestFixture *tests);

And here the specification of the test structure for the firmware:

typedef struct {
void (*firmware_fun)();
} TestFixture;

This time the test definition structure is allocated by the bootloader and passed in as parameter to the firmware. The example code is included in option3.zip (see attachments below). The interaction with the simulator is identical to the previous example.

Step Three: Dynamic Tests

In many cases the space constraints for the test code are limited. So flashing an entire precompiled suite of tests may be impossible or undesirable. In order to overcome this issue you may want to consider dynamic tests. In this scenario only individual tests are uploaded through the boot loader and executed against the firmware. This approach can be combined with all of the above methods. In addition to the specified memory sections required by the test procedure (see Step two) you also need to define a section that holds the dynamic code. In the boot loader this section is referenced as array to store and replace the code and as function pointer to execute the test. The following example shows this with an already written array that is stored at the location of the test-code. The example code of the test is shown as follows:



/* dynamically loaded structure */
unsigned char __attribute__ ((section("__TEST_CODE__")))code [] = {
...
};
...
extern int __TEST_CODE__();
extern unsigned char * code;

...

int main(void)
{
debug_puts("Inside boot-loader test suite!\r\n");

/* perform test from loaded array */
__TEST_CODE__();

debug_puts("Inside boot-loader again!\r\n");

return 0;
}

To build the test code, you link it using a script that places the text, data and bss segment at the location of the test-code; or pin the test function explicitly to the section of the test code of the boot loader. The former option is useful, when the tests consist of several subroutine calls. The latter is useful for unit tests consisting of a single function call, having no global data structures. In addition, you might want to consider allocating a separate stack inside the test routine to avoid corruption.

The array containing the test cases can be created from the SREC-S19 file of the compiled test-case. The python script srec_to_c.py included in the sample code performs that conversion for continous S19 files.

Discussion

In this tutorial, we have shown how to leverage the boot-loader-firmware-paradigm introduced in the previous part of this tutorial to perform dynamic firmware testing. It is up to the software engineer to select the degree to which the firmware has to interact with the tests to register with the test-suite.

In addition to the procedures shown, you may want to consider using your host systems timers to check the progress of executed tests. If the firmware does not register with the tests properly or a test becomes stalled, the boot loader can be able to recover itself using a timer interrupt.

A substantial risk using these approaches is that the firmware and the bootloader still share the same address space. You may want to consider introducing explicit checks that ensure that the firmware does not touch boot loader code (i.e., through heap operations) and vice-versa.

The srec_to_c.py script performs the transformation of the test-case’s SREC/S19 files to the array. You can modify this script to create binary images that are uploaded through your devices interface.

Sample Code:

Saturday, February 6, 2010

On Designing Boot Loaders and Grey-box-Testing Firmware (Part 1/2)

I am currently TAing SE350. The students’ deliverable is a small real-time executive kernel (RTX) that runs on a Freescale Coldfire chip. We got the idea of building an automated embedded test suite for the students’ term projects. However, instead of having to compile the students’ code from scratch we would only want to take their firmware binary directly and test it. This testing would involve injecting several test processes in the students’ OS. These tests would stress their implementation and dump the results to a serial port of the actual Coldfire board. Having worked in the embedded field this problem is similar to integrating boot-loaders with firmware. In the project, the boot-loader is the testing code and the actual firmware is the code to be tested.
You end up with two pieces of binary code that will be programmed into your device. So the challenge is to make them talk to each other. In the case of the boot-loader, you have the boot-loader invoking the firmware, and in the case of the testing code the testing code invokes the RTX.
In the following sections, I describe the steps for…
  • Building a tool-chain,
  • Developing the boot-loader-, firmware-code,
  • And integrating the different SREC/S19 files
In the second part I will describe how to leverage the established framework to design a native testing suite.
Step 1: What tools do I need?
In order to run stuff on bare (i.e., no existing OS) chips you need to have a tool chain that translates your source code into ELF files (ELF = Executable and linkable format) and SREC/S19 files for flashing it onto the device. We need:
Step 2: Where to put my firmware code?
If you are going to integrate two pieces of code, you need to make sure they do not overlap in flash and do not access themselves in an undesired fashion. Since you are developing on the bare hardware, you actually have complete control over the former property and can enforce the latter by a careful code design. Your generated ELF file will consist of three major sections, as follows:
Note that the BSS segment exists for some historic reason and in almost all OS lectures it is implied by the data segment (i.e. data := data + BSS). By convention the text segment starts at a lower address than data and BSS segments. When your program is executed the data the values of the data and BSS segments are copied into the main memory. However the size is not established at runtime. The program’s stack for function calls and local variables resides on the heap which is by convention allocated after the BSS segment and grows dynamically. The GNU tool-chain you just build includes the GNU Linker that allows specifying these locations explicitly by linker scripts (i.e., LD files). GNU LD files have a simple structure describing:
  • The memory banks and locations,
  • How to spread your code across those locations.
The following simple example file describes an embedded system (i.e. in my case: CJDesign’s MCF5307 board). Most evaluation boards, like mine, come with a huge SRAM and actually have a ROM that allows you to load stuff in main memory. As such, we will dump all code into SRAM for testing purposes. The following example specifies the assignment 1 MB at address 0x10100000 to SRAM and dumps all sections of the code into that segment. Hint the space after the section names is required to ensure the uniqueness of the names. The actual code will execute from the SRAM start address, which is 0x10100000.

/* firmware.ld */
MEMORY
{
  sram        : ORIGIN = 0x10100000, LENGTH = 1M
}

SECTIONS
{
  .text :
  {
    *(.text)
    *(.rodata*)
  } > sram

  .data :
  {
    *(.data)
  } > sram

  .bss :
  {
    *(.bss)
  } > sram
}
A note for SE350 students: Guys please do not attempt to hack the linker file provided by the course. You may run into serious trouble by using my linker script or hacking the existing one!
Step 3: Building your firmware
To compile and link your source (firmware.c) with this file, use the following command.
m68k-elf-gcc -Tfirmware.ld -Map=firmware.map –o firmware.elf firmware.c
You may want to generate a listing of the file to see that everything is at the expected location, as follows:
m68k-elf-objdump -xdC firmware.bin > firmware.lst
In order to flash or deliver this file to the customer we actually need to convert it into the Motorola S19/SREC file as follows.
m68k-elf-objcopy --output-format=srec firmware.bin firmware.s19
Step 4: Building the other piece of code
What’s left to build is the boot-loader. In order to ensure distinct flash and memory regions you need to provide another linker script that puts all the boot-loader code into a different location than the other code. A wise choice is to put this code as far away from the actual firmware as possible, possibly at the end of the available memory. The following code offsets the memory bank by 1MB and dumps the code there.
/* bootloader.ld */
MEMORY
{
  sram        : ORIGIN = 0x10200000, LENGTH = 1M
}

__FIRMWARE__ = 0x10100000;

SECTIONS
{
  .text :
  {
    *(.text)
    *(.rodata*)
  } > sram

  .data :
  {
    *(.data)
  } > sram

  .bss :
  {
    *(.bss)
  } > sram
}
In order to invoke the firmware we need to put a symbol inside the linker script that identifies the expected starting address of the firmware, which is in this case called firmware. This symbol can be used from the C-code directly as function call. In order to avoid any compilation warnings you should forward declare this function as external. The compilation and transformation into the S19 file is analog to creating the firmware code. You should end up with a bootloader.s19.
Step 5: Throwing things together
In practice when you build an embedded device. It should have the boot-loader and some firmware programmed in when it leaves assembly. In many cases the interface that the end-user has to the device (i.e. a USB connector) is different from what you have during assembly (e.g., an in-system flash tool). As such it is necessary to throw the boot-loader and the firmware together.
The S19 format is a simple ASCII data exchange format, originally developed by Motorola, for executable code. It is widely accepted by most programmers for Motorola-based embedded systems. The files are processed line by line; each line contains a control code, a record size, an address, an optional data sequence and a checksum. You find the details here.
GNU Object Copy usually outputs:
  • A block header (S0),
  • A sequence of data records (S1-S3)
  • And the start-address (S5-S9).
The block header usually contains the name of the file (e.g., firmware.s19 or bootloader.s19). Most ROM loaders on evaluation boards will actually processes start address record, which is in our case the declared origin of the SRAM, and fail to load if they do not find it, so it needs to be included.
So in order to put the boot-loader and the firmware together into one file you need to provide one header, the data of both programs and one starting address:
  • Header: any of the firmware/boot-loader, or a custom header (see below)
  • Data: concatenate the data records of both original programs
  • Starting address: the start address of the boot-loader
Step 5a: Composing your own header
Yes, geeky people like me actually like implementing checksum algorithms and brand their creation. In order to do so we need to dive into the checksum procedure used by S-records. According to Wikipedia the checksum is “[…]the least significant byte of ones' complement of the sum of the values represented by the two hex digit pairs for the byte count, address and data fields.” So guys, its time to dig out those algorithm-class-notes and figure that out, … oh wait …, found it:

  • Sum up all bytes starting from the byte count record.
  • Set: checksum = 0xFF – (0x00FF & sum)

Why the hell would anyone use such a check-summing algorithm? The answer is simple: It can be easily checked! While processing the S19 records, you can actually sum everything up, including the provided checksum and should get 0xFF. That is a simple compare operation and that can be evaluated in no time.
Step 6: Testing your Creation
If you build the Coldfire simulator according to my instructions you can invoke the simulator as follows…
coldfire --board cjdesign-5307.board
and load the code like this…
Use CTRL-C (SIGINT) to cause autovector interrupt 7 (return to monitor)
Loading memory modules...
Loading board configuration...
        Opened [/usr/local/coldfire/share/coldfire/cjdesign-5307.board]
Board ID: CJDesign
CPU: 5307 (Motorola Coldfire 5307)
        unimplemented instructions: CPUSHL PULSE WDDATA WDEBUG
        69 instructions registered
        building instruction cache... done.

Memory segments: dram  timer0  timer1  uart0(on port 5206)
                 uart1(on port 5207)  sim  flash  sram

!!! Remember to telnet to the above ports if you want to see any output!

Hard Reset...
Initializing monitor...
Enter 'help' for help.
dBug> dl merged.s19
Downloading S-Record...
Done downloading S-Record.

dBug> go 0x10100000 <- the actual firmware
... some garbage, because RTS returns to nowhere...
dBug> go 0x10200000 <- the boot-loader invoking the firmware
You should yield the following output on the terminal (telnet localhost 5206).
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

uart0
Inside firmware! <- 1st go of the firmware
Inside boot-loader! <- 2nd go inside firmware!
Inside firmware!
Back in boot-loader!
Discussion
In this part of the how-to I explained the basics of building two pieces of binary Coldfire code and integrating them into a single file that can be processed by most programmers and ROM-loaders. A popular application is the integration of boot-loader and firmware code for embedded system assembly. Another application is embedded grey box testing. In this technique, instead of a boot-loader a test-suite is evaluated against the firmware to check for potential defects. In the next post, I’ll describe how to design such a test framework.
You can find the sample code of this post here. The code will contain some modified linker scripts that deal with particular alignment problems of the simulator. Furthermore, the boot-loader and the firmware should have different stacks so some assembly files have been added to do so. The S19 merging is done by the python script merge.py.
References and Sample Code