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
} ;
Pay attention when checking the reference manual RM0360 list of interrupts, it is misleading.
There is only one reserved position between TIM6 and TIM14, the table shows two lines with entry 19 and two lines with address 0x00000084. I am using Rev 4 of RM0360, the latest version I could find online.
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 ;
-
kputc()
write intxbuf[]
whileUSART1_Handler()
only read from it. -
txbufin
is the index of the position wherekputc()
will insert a character, it’s written bykputc()
and read byUSART1_Handler()
. -
txbufout
is the index of the position whereUSART1_Handler()
will fetch a character, it’s written byUSART1_Handler()
and read bykputc()
. The value of txbufout will change under interrupt, so it is marked asvolatile
to make sure the compiler will not optimize the code in a conflicting way. -
As the index calculation in the Round Robin buffer uses integer division, it is best that the size of the buffer be a power of two on Cortex-M0 chipset. This way the compiler will optimize
(idx+1) % size
into(idx+1) & (size-1)
, which is more efficient on chipset with no support for integer division. -
I kept the buffer size small to be sure this code is well tested. With a value of 8, this means the buffer can hold up to 7 characters.
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 ;
}
-
kputc()
enables the interrupt generation after a new character is inserted in the buffer. -
USART1_Handler()
disables the interrupt generation when the buffer is empty. -
The conversion of LF to CR LF is done by the interrupt handler.
-
kputc()
now yields when the buffer is full.
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
text data bss dec hex filename
2096 0 20 2116 844 f030f4.elf
f030f4.hex
f030f4.bin
Checking the map and lst files I can verify that
-
This code has grown by 128 bytes due to the extra interrupt handlers.
-
Previous implementation of
kputc()
was using 4 bytes of bss to hold lastchar (1 byte). The new version uses 12 bytes to hold the round robin buffer (8 bytes), its in and out indexes (2 bytes) and lastchar (1 byte). -
The compiler optimizes the modulo instruction in
% size
to bit masking& (size – 1)
as the size 8 is a power of 2.
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-2021 Renaud Fivet