2.9 Interrupt Driven Transmission

It’s time to revise the implementation of kputc(), remove the recursive call to handle CR LF transmission and avoid the busy wait loop. USART1 can trigger an interrupt when the Transmit Data Register (TDR) is empty which is all I need to implement interrupt driven transmission.

Extending the interrupt vector

I need to add the device specific interrupts to the interrupt vector. So far I have only mapped the initial Stack pointer and the 15 Core System Exceptions. The STF0x0 chipsets have 32 device specific interrupts which are listed in the Reference Manual RM0360.

I make a copy of startup.c into startup.txeie.c to do the changes. The name txeie refers to Transmit Data Register Empty Interrupt Enabled.

/* Stubs for System Exception Handler */
void Default_Handler( void) ;
#define dflt_hndlr( fun) void fun##_Handler( void) \
                                __attribute__((weak,alias("Default_Handler")))
dflt_hndlr( NMI) ;
dflt_hndlr( HardFault) ;
dflt_hndlr( SVCall) ;
dflt_hndlr( PendSV) ;
dflt_hndlr( SysTick) ;

dflt_hndlr( WWDG) ;
dflt_hndlr( RTC) ;
dflt_hndlr( FLASH) ;
dflt_hndlr( RCC) ;
dflt_hndlr( EXTI0_1) ;
dflt_hndlr( EXTI2_3) ;
dflt_hndlr( EXTI4_15) ;
dflt_hndlr( DMA_CH1) ;
dflt_hndlr( DMA_CH2_3) ;
dflt_hndlr( DMA_CH4_5) ;
dflt_hndlr( ADC) ;
dflt_hndlr( TIM1_BRK_UP_TRG_COM) ;
dflt_hndlr( TIM1_CC) ;
dflt_hndlr( TIM3) ;
dflt_hndlr( TIM6) ;
dflt_hndlr( TIM14) ;
dflt_hndlr( TIM15) ;
dflt_hndlr( TIM16) ;
dflt_hndlr( TIM17) ;
dflt_hndlr( I2C1) ;
dflt_hndlr( I2C2) ;
dflt_hndlr( SPI1) ;
dflt_hndlr( SPI2) ;
dflt_hndlr( USART1) ;
dflt_hndlr( USART2) ;
dflt_hndlr( USART3_4_5_6) ;
dflt_hndlr( USB) ;

/* Interrupt vector table:
 * 1  Stack Pointer reset value
 * 15 System Exceptions
 * 32 Device specific Interrupts
 */
typedef void (*isr_p)( void) ;
isr_p const isr_vector[ 16 + 32] __attribute__((section(".isr_vector"))) = {
    (isr_p) &__StackTop,
/* System Exceptions */
    Reset_Handler,
    NMI_Handler,
    HardFault_Handler,
    0,  0,  0,  0,  0,  0,  0,
    SVCall_Handler,
    0,  0,
    PendSV_Handler,
    SysTick_Handler,
/* STM32F030xx specific Interrupts cf RM0360 */
    WWDG_Handler,
    0,
    RTC_Handler,
    FLASH_Handler,
    RCC_Handler,
    EXTI0_1_Handler,
    EXTI2_3_Handler,
    EXTI4_15_Handler,
    0,
    DMA_CH1_Handler,
    DMA_CH2_3_Handler,
    DMA_CH4_5_Handler,
    ADC_Handler,
    TIM1_BRK_UP_TRG_COM_Handler,
    TIM1_CC_Handler,
    0,
    TIM3_Handler,
    TIM6_Handler,
    0,
    TIM14_Handler,
    TIM15_Handler,
    TIM16_Handler,
    TIM17_Handler,
    I2C1_Handler,
    I2C2_Handler,
    SPI1_Handler,
    SPI2_Handler,
    USART1_Handler,
    USART2_Handler,
    USART3_4_5_6_Handler,
    0,
    USB_Handler
} ;

kputc() and USART1_Handler()

I make a copy of clocks.c into txeie.c to make the changes to my system layer.

I add the description of the TX Empty Interrupt Enable bit in the Configuration Register of USART1:

#define USART_CR1_TXEIE (1 << 7)    /* 7: TDR Empty Interrupt Enable */
I use a Round Robin buffer to synchronize kputc() and USART1_Handler() making sure they don’t write to the same location.
static unsigned char txbuf[ 8] ; // best if size is a power of 2 for cortex-M0
#define TXBUF_SIZE (sizeof txbuf / sizeof txbuf[ 0])
static unsigned char            txbufin ;
static volatile unsigned char   txbufout ;
void USART1_Handler( void) {
    if( txbufout == txbufin) {
    /* Empty buffer => Disable TXEIE */
        USART1[ CR1] &= ~USART_CR1_TXEIE ;
    } else {
        static unsigned char lastc ;
        unsigned char c ;

        c = txbuf[ txbufout] ;
        if( c == '\n' && lastc != '\r')
            c = '\r' ;
        else
            txbufout = (txbufout + 1) % TXBUF_SIZE ;

        USART1[ TDR] = c ;
        lastc = c ;
    }
}

void kputc( unsigned char c) {  /* character output */
    int nextidx ;

/* Wait if buffer full */
    nextidx = (txbufin + 1) % TXBUF_SIZE ;
    while( nextidx == txbufout)
        yield() ;

    txbuf[ txbufin] = c ;
    txbufin = nextidx ;
/* Trigger transmission by enabling interrupt */
    USART1[ CR1] |= USART_CR1_TXEIE ;
}

Unmasking USART1 interrupt

I have configured USART1 peripheral to generate an interrupt when the transmit data register is empty, now I have to tell the Core to pay attention to USART1 specific interrupt line.

The 32 device specific interrupts are enabled through the Nested Vectored Interrupt Controller (NVIC). NVIC is a core peripheral so its description is in the Programming Manual. Enabling is done through the Interrupt Set-Enable Register (ISER).

Set-Enable means writing a 1 enables while writing a 0 does nothing. Reading reports the current settings. There is a corresponding Clear-Enable register, to disable interrupts.

#define NVIC                    ((volatile long *) 0xE000E100)
#define NVIC_ISER               NVIC[ 0]
#define unmask_irq( idx)        NVIC_ISER = 1 << idx
#define USART1_IRQ_IDX          27
I add a call to the macro unmask_irq() after USART1 initialization.
/* Unmask USART1 irq */
    unmask_irq( USART1_IRQ_IDX) ;

Build and test

I add the composition into Makefile
SRCS = startup.txeie.c txeie.c uptime.c
Build completes successfully
$ make
f030f4.elf from startup.txeie.o txeie.o uptime.o
   text    data     bss     dec     hex filename
   2097       0      20    2117     845 f030f4.elf
f030f4.hex
f030f4.bin
Checking the map and lst files I can verify that Flashing a device with the new executable, uptime works as the previous version.

Checkpoint

There is no obvious benefit in doing transmission under interrupt at this stage, my most complex application, uptime, prints one line every second, there is plenty of idle time.

Next, I will use an external sensor to do some measurement.


© 2020-2024 Renaud Fivet