In this lab, you are going to be using interrupts to improve your UART driver along with building a simple scheduler based on a periodic SysTick. Additionally, you will be working with the real-time clock to timestamp data from the sensors.
A periodic interrupt is a common need within an embedded system. Given this fact, ARM has included a simple timer as part of the processor core that greatly simplifies the setup of a periodic interrupt.
The initialization for the systick timer is very straightforward. You will pass the system clock divided by the number of clocks per second to set the rate. For this lab, you need to generate 100 interrupts per second (100Hz or 10mS between interrupts).
void ds_systick_init(void) {
SysTick_Config(SystemCoreClock/100);
}
It is important that the interrupt handler is named exactly the same the definition that is placed into the vector table in the file: /l/arm2/STM32F3-Discovery_FW_V1.1.0/Libraries/CMSIS/Device/ST/STM32F30x/Source/Templates/gcc_ride7/startup_stm32f30x.s. You will be adding to this handler as the lab progresses. However, for now we are going to toggle a pin to allow confirmation that the interrupt is operating at the correct rate.
void SysTick_Handler(void) {
static state = 0;
state ^= 1;
if (state) {
GPIOE->BSRR = 0x8000;
}
else {
GPIOE->BRR = 0x8000;
}
}
Use a Saleae logic probe to confirm that PE15 is toggling at the correct rate. Make sure that no other code is writing to PE15 …
In this section, you will be improving your code in ds_uart.c to make it interrupt driven. You should start with your ds_uart code from the earlier labs.
In this section, you will be creating TX and RX buffer data structures, initializing the UART to generate interrupts, and designing an interrupt handler.
The interrupt routine will need circular buffers to store characters that have been received and characters that are waiting to be transmitted.
You should test your queue functions in isolation to ensure that the work properly.
You will need to configure the UART to generate interrupts. This involves enabling the interrupts at the UART and additionally enabling the interrupts at the NVIC (nested vectored interrupt controller).
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x08;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x08;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
Now that the NVIC has been setup. The RX interrupt can be initialized using USART_ITConfig.
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
You might be wondering why the transmitter interrupt is not enabled at the same time. The reason is that the uart transmitter will generate an interrupt when it is empty to let the system know that it can accept another interrupt. If the interrupt were enabled at this point, the handler would be immediately even though there are no characters to send. The transmit interrupt should only be used when there is something to send and should be disabled once the transmit queue is empty.
The USART1 module has a single handler for all of the potential interrupt sources. We only need to consider the RXNE and TXE interrupt sources as there are the only sources that we are going to enable. In your handler you will need to check for the following flags:
USART_GetFlagStatus(USART1, USART_FLAG_RXNE)
If the RXNE is set, the character should be read from the receiver:
ch = USART_ReceiveData(USART1);
This should look very familiar from the polled implementation. However, in this case we know that either RXNE or TXE or we would not be in the handler.
enqueue(&rxbuf,ch);
The character need to be placed on the receiver's queue. However, the issue of what to do if the queue is full needs to be considered. At this point, we are going to just throw away the character if there is no space available. We will consider more graceful alternatives in later labs. However, we must not wait in the handler.
if (USART_GetFlagStatus(USART1,USART_FLAG_TXE)) {
ch = dequeue(&txbuf);
if (ch) {
USART_SendData(USART1,ch);
}
else {
USART_ITConfig(USART1,USART_IT_TXE,DISABLE);
}
}
}
The transmitter requires a bit more logic to handle. If the transmitter is empty, the transmitter's queue needs to be checked to see if there are any characters that are available to send. If there are, then it can grab the character and put it into the transmitter.
However, if there are not any characters to send, the transmitter interrupt needs to be disabled. If this is not done, the UART will continually generate interrupts to let the system know that the tranmitter is empty. This is not very useful information unless we have something to send.
Periodically or explicitly, the system needs enable the transmitter interrupt to clear out the buffer. Turning on the interrupt will immediately cause a UART interrupt and the first character will be sent.
void flush_uart(void) {
USART_ITConfig(USART1,USART_IT_TXE,ENABLE);
}
It is best to periodically check to see if there are any characters in the transmitter and call this function. A good place to perform this check is in the Systick handler. You should go ahead and add this to Systick.
Now that characters are being buffered in the interrupt handler, the getchar function will talk to the queue to get data rather than the receiver.
int getchar(void) {
int ch;
while (!(ch=dequeue(&rxbuf)));
return (ch);
}
Notice that if the queue is empty, then the function will wait until a character is available. You should update your getchar_nb function as well to use the rx buffer.
Update your putchar function to use the transmit buffer. This function should block until it has room to add the buffer.
At this point, you should be able to receive characters from the console along with printing using purchar and printf. Verify that the console interface is working using:
The real-time clock is an internal peripheral on the STM32F series processor. In this lab, we are going to be initializing the clock and then reading it periodically to use as a timestamp for our sensor data.
The RTC follows the same initialization pattern as the other system peripherals in using a structure that is initialized and then passed to an init function. In this case of the RTC, there are three different sections that need to be addressed.
Declare the initialization structures
RTC_TimeTypeDef RTC_TimeStructure;
RTC_DateTypeDef RTC_DateStructure;
RTC_InitTypeDef RTC_InitStructure;
To prevent an errant write from corrupting the clock, the RTC needs to be unlocked prior to writing. These commands enable the peripheral clock, enable access, and setup the oscillator that will be used for timekeeping.
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
PWR_BackupAccessCmd(ENABLE);
RCC_BackupResetCmd(ENABLE);
RCC_BackupResetCmd(DISABLE);
PWR_BackupAccessCmd(ENABLE);
RCC_LSICmd(ENABLE);
while(RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
Base setup for the clock.
RTC_StructInit(&RTC_InitStructure);
RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24;
RTC_InitStructure.RTC_AsynchPrediv = 88;
RTC_InitStructure.RTC_SynchPrediv = 470;
RTC_Init(&RTC_InitStructure);
This structure initialization sets up the date.
RTC_DateStructure.RTC_Year = 13;
RTC_DateStructure.RTC_Month = RTC_Month_February;
RTC_DateStructure.RTC_Date = 8;
RTC_DateStructure.RTC_WeekDay = RTC_Weekday_Friday;
RTC_SetDate(RTC_Format_BCD, &RTC_DateStructure);
Similar to the date, this structure initialized the date.
RTC_TimeStructure.RTC_H12 = RTC_H12_AM;
RTC_TimeStructure.RTC_Hours = 0x09;
RTC_TimeStructure.RTC_Minutes = 0x05;
RTC_TimeStructure.RTC_Seconds = 0x00;
RTC_SetTime(RTC_Format_BIN, &RTC_TimeStructure);
You can read the time or date from the clock at any time using RTC_GetTime and RTC_GetDate. The call to the function would typically look like the following.
RTC_GetTime(RTC_Format_BIN, &RTC_TimeStructure);
RTC_GetDate(RTC_Format_BIN, &RTC_DateStructure);
With a regular interrupt from Systick, a scheduler can be designed with counters to run functions at integer multiples of the base period of 10mS. The following is an example of how this might be accomplished.
void scheduler(void) {
static int counter_100mS = 0;
if (counter_100mS++==10) {
counter_100mS=0;
// tasks to accomplish at 100mS intervals
}
}
When considering where to run this function, it is tempting to call it directly from the Systick handler. However, this is typically not a good idea given that the scheduler might contain functions that could block like printf. It is better to set a flag in the systick handler. This flag can be tested and cleared in the main function.
if (systick_flag) {
systick_flag=0;
schedule();
}
This assignment is due by midnight on 2/14. At midnight, your AI will pull the current state of your repository to review your code. In the following lab session, you will be asked to demonstrate your solution. Independently, you will also write a lab report describing in approximately 300 words or less the following:
You will turn in this lab report in the oncourse assignments section.