Chapter
5: Interrupts, DAC, and Sound Generation
Jonathan Valvano and Ramesh Yerraballi
An embedded system uses its input/output devices to interact with the external world. Input devices allow the computer to gather information, and output devices can display information. Output devices also allow the computer to manipulate its environment. The tight-coupling between the computer and external world distinguishes an embedded system from a regular computer system. The challenge is under most situations the software executes much faster than the hardware. E.g., it might take the software only 1 us to ask the hardware to clear the LCD, but the hardware might take 1 ms to complete the command. During this time, the software could execute tens of thousands of instructions. Therefore, the synchronization between the executing software and its external environment is critical for the success of an embedded system. This chapter presents general concepts about interrupts, and specific details for the Cortex™-M microcontroller. We will then use periodic interrupts to cause a software task to be executed on a periodic basis. If a GPIO pin is configured as an input, it can also be armed to invoke an interrupt on falling edges, rising edges or both falling and rising edges. Using interrupts allows the software to respond quickly to changes in the external environment.
As we have seen throughout this class, an embedded system uses its input/output devices to interact with the external world. Input devices allow the system to gather information about the world, and output devices can affect visual, mechanical, chemical, auditory, and biologic processes in the world. In this chapter we will literally “Shape The World”. We present a technique for the system to generate an analog output using a digital to analog converter (DAC). Together with periodic interrupts the system can generate waveforms, which are analog voltages that vary in time and in amplitude. We will then connect the waveform to a speaker and generate sound.
Table of Contents:
Return to book table of contents
Video 5.0a. Introduction to Interrupts
Video 5.0b. Introduction to Sound
An interrupt is the automatic transfer of software execution in response to a hardware event that is asynchronous with the current software execution. This hardware event is called a trigger. The hardware event can either be a busy to ready transition in an external I/O device (like the UART input/output) or an internal event (like bus fault, memory fault, or a periodic timer). When the hardware needs service, signified by a busy to ready state transition, it will request an interrupt by setting its trigger flag. A thread is defined as the path of action of software as it executes. The execution of the interrupt service routine is called a background thread. This thread is created by the hardware interrupt request and is killed when the interrupt service routine returns from interrupt (e.g., by executing a BX LR). A new thread is created for each interrupt request. It is important to consider each individual request as a separate thread because local variables and registers used in the interrupt service routine are unique and separate from one interrupt event to the next interrupt. In a multi-threaded system, we consider the threads as cooperating to perform an overall task. Consequently we will develop ways for the threads to communicate (e.g., FIFO) and to synchronize with each other. Most embedded systems have a single common overall goal. On the other hand, general-purpose computers can have multiple unrelated functions to perform. A process is also defined as the action of software as it executes. Processes do not necessarily cooperate towards a common shared goal. Threads share access to I/O devices, system resources, and global variables, while processes have separate global variables and system resources. Processes do not share I/O devices.
Figure 5.1.1. Multithreading using interrupts.
Video 5.1.2. Interrupt Processing - The Context Switch
There are no standard definitions for the terms mask, enable, and arm in the professional, Computer Science, or Computer Engineering communities. Nevertheless, in this class we will adhere to the following specific meanings. To arm a device means to allow the hardware trigger to interrupt. Conversely, to disarm a device means to shut off or disconnect the hardware trigger from the interrupts. Each potential interrupting trigger has a separate arm bit. One arms a trigger if one is interested in interrupts from this source. Conversely, one disarms a trigger if one is not interested in interrupts from this source. To enable means to allow interrupts at this time. Conversely, to disable means to postpone interrupts until a later time. On the ARM Cortex-M processor there is one interrupt enable bit for the entire interrupt system. We disable interrupts if it is currently not convenient to accept interrupts. In particular, to disable interrupts we set the I bit in PRIMASK. In C, we enable and disable interrupts by calling the functions __enable_irq() and __disable_irq() respectively.
The software has dynamic control over some aspects of the interrupt request sequence. First, each potential interrupt trigger has a separate arm bit that the software can activate or deactivate. The software will set the arm bits for those devices from which it wishes to accept interrupts, and will deactivate the arm bits within those devices from which interrupts are not to be allowed. In other words it uses the arm bits to individually select which devices will and which devices will not request interrupts. For most devices there is a enable bit in the NVIC that must be set (periodic SysTick interrupts are an exception, having no NVIC enable). The third aspect that the software controls is the interrupt enable bit. Specifically, bit 0 of the special register PRIMASK is the interrupt mask bit, I. If this bit is 1 most interrupts and exceptions are not allowed, which we will define as disabled. If the bit is 0, then interrupts are allowed, which we will define as enabled. The fourth aspect is priority. The BASEPRI register prevents interrupts with lower priority interrupts, but allows higher priority interrupts. The MSPM0 supports four priority levels. Priority level 0 is the highest or most important. Priority level 3 is the lowers or least important. For example if the software sets the BASEPRI to 2, then requests with level 0 and 1 can interrupt, while requests at levels 2 or 3 will be postponed. The software can also specify the priority level of each interrupt request. If BASEPRI is zero, then the priority feature is disabled and all interrupts are allowed. The fifth aspect is the external hardware trigger. One example of a hardware trigger is the Count flag in the SysTick->CTRL register which is set periodically by SysTick. Another example of hardware triggers are bits in the GPIOB->CPU_INT.RIS register that are set on rising or falling edges of digital input pins. Five conditions must be true for an interrupt to be generated:
1) device arm,
2) NVIC enable,
3) global enable,
4) interrupt priority level must be higher than current level executing,
and
5) hardware event trigger.
For an interrupt to occur, these five conditions must be simultaneously true but can occur in any order.
An interrupt causes the following sequence of five events. First, the current instruction is finished. Second, the execution of the currently running program is suspended, pushing eight registers on the stack (R0, R1, R2, R3, R12, LR, PC, and PSR with the R0 on top). If the floating point unit on a Cortex-M4 is active, an additional 18 words will be pushed on the stack representing the floating point state, making a total of 26 words. Third, the LR is set to a specific value signifying an interrupt service routine (ISR) is being run (bits [31:4] to 0xFFFFFFF, and bits [3:0] specify the type of interrupt return to perform). In our examples we will see LR is set to 0xFFFFFFF9. If the floating point registers were pushed, the LR will be 0xFFFFFFE9. Fourth, the IPSR is set to the interrupt number being processed. Lastly, the PC is loaded with the address of the ISR (vector).
1) Current instruction is finished,
2) Eight registers are pushed on the stack,
3) LR is set to 0xFFFFFFF9,
4) IPSR is set to the interrupt number,
5) PC is loaded with the interrupt vector
These five steps, called a context switch, occur automatically in hardware as the context is switched from a foreground thread to a background thread. We can also have a context switch from a lower priority ISR to a higher priority ISR. Next, the software executes the ISR.
If a trigger flag is set, but the interrupts are disabled (I=1), the interrupt level is not high enough, or the flag is disarmed, the request is not dismissed. Rather the request is held pending, postponed until a later time, when the system deems it convenient to handle the requests. In other words, once the trigger flag is set, under most cases it remains set until the software clears it. The five necessary events (device arm, NVIC enable, global enable, level, and trigger) can occur in any order. For example, the software can set the I bit to prevent interrupts, run some code that needs to run to completion, and then clear the I bit. A trigger occurring while running with I=1 is postponed until the time the I bit is cleared again.
Clearing a trigger flag is called acknowledgement, which occurs only by specific software action. Each trigger flag has a specific action software must perform to clear that flag. We will pay special attention to these enable/disable software actions. The SysTick periodic interrupt will be the only example of an automatic acknowledgement. For SysTick, the periodic timer requests an interrupt, but the trigger flag will be automatically cleared when the ISR runs. For all the other trigger flags, the ISR must explicitly execute code that clears the flag.
The interrupt service routine (ISR) is the software module that is executed when the hardware requests an interrupt. There may be one large ISR that handles all requests (polled interrupts), or many small ISRs specific for each potential source of interrupt (vectored interrupts). The design of the interrupt service routine requires careful consideration of many factors. Except for the SysTick interrupt, the ISR software must explicitly clear the trigger flag that caused the interrupt (acknowledge). After the ISR provides the necessary service, it will execute BX LR. Because LR contains a special value (e.g., 0xFFFFFFF9), this instruction pops the 8 registers from the stack, which returns control to the main program. If the LR is 0xFFFFFFE9, then 26 registers (R0-R3,R12,LR,PC,PSR, and 18 floating point registers) will be popped by BX LR. There are two stack pointers: PSP and MSP. The software in this class will exclusively use the MSP. It is imperative that the ISR software balance the stack before exiting. Execution of the previous thread will then continue with the exact stack and register values that existed before the interrupt. Although interrupt handlers can create and use local variables, parameter passing between threads must be implemented using shared global memory variables. A private global variable can be used if an interrupt thread wishes to pass information to itself, e.g., from one interrupt instance to another. The execution of the main program is called the foreground thread, and the executions of the various interrupt service routines are called background threads.
An axiom with interrupt synchronization is that the ISR should execute as fast as possible. The interrupt should occur when it is time to perform a needed function, and the interrupt service routine should perform that function, and return right away. Placing backward branches (busy-wait loops, iterations) in the interrupt software should be avoided if possible. The percentage of time spent executing interrupt software should be small when compared to the time between interrupt triggers.
Performance measures: latency and bandwidth. For an input device, the interface latency is the time between when new input is available, and the time when the software reads the input data. We can also define device latency as the response time of the external I/O device. For example, if we request that a certain sector be read from a disk, then the device latency is the time it take to find the correct track and spin the disk (seek) so the proper sector is positioned under the read head. For an output device, the interface latency is the time between when the output device is idle, and the time when the software writes new data. A real-time system is one that can guarantee a worst case interface latency. Bandwidth is defined as the amount of data/sec being processed.
Many factors should be considered when deciding the most appropriate mechanism to synchronize hardware and software. One should not always use busy wait because one is too lazy to implement the complexities of interrupts. On the other hand, one should not always use interrupts because they are fun and exciting. Busy-wait synchronization is appropriate when the I/O timing is predictable and when the I/O structure is simple and fixed. Busy wait should be used for dedicated single thread systems where there is nothing else to do while the I/O is busy. Interrupt synchronization is appropriate when the I/O timing is variable, and when the I/O structure is complex. In particular, interrupts are efficient when there are I/O devices with different speeds. Interrupts allow for quick response times to important events. In particular, using interrupts is one mechanism to design real-time systems, where the interface latency must be short and bounded. Bounded means it is always less than a specified value. Short means the specified value is acceptable to our consumers.
Interrupts can also be used for infrequent but critical events like power failure, memory faults, and machine errors. Periodic interrupts will be useful for real-time clocks, data acquisition systems, and control systems. For extremely high bandwidth and low latency interfaces, direct memory access (DMA) should be used. We use DMA transfer data directly between and I/O device and RAM. DMA is beyond the scope of this introductory class, although the MSPM0G3507 does provide DMA functionality.
An atomic operation is a sequence that once started will always finish, and cannot be interrupted. All instructions on the ARM® Cortex™-M processor are atomic except store and load multiple, STM LDM PUSH POP. If we wish to make a section of code atomic, we can run that code with I=1. In this way, interrupts will not be able to break apart the sequence. Again, requested interrupts that are triggered while I=1 are not dismissed, but simply postponed until I=0. In particular, to implement an atomic operation we will 1) save the current value of the PRIMASK, 2) disable interrupts, 3) execute the operation that needs to run atomically, and 4) restore the PRIMASK back to its previous value.
: What five conditions must be true for an interrupt to occur?
: How do you enable interrupts?
: What are the steps that occur when an interrupt is processed?
As you develop experience using interrupts, you will come to notice a few common aspects that most computers share. The following paragraphs outline three essential mechanisms that are needed to utilize interrupts. Although every computer that uses interrupts includes all three mechanisms, how the mechanisms operate will vary from one computer to another.
All interrupting systems must have the ability for the hardware to request action from computer. In general, the interrupt requests can be generated using a separate connection to the processor for each device. The TM4C microcontrollers use separate connections to request interrupts.
All interrupting systems must have the ability for the computer to determine the source. A vectored interrupt system employs separate connections for each device so that the computer can give automatic resolution. You can recognize a vectored system because each device has a separate interrupt vector address. With a polled interrupt system, the interrupt software must poll each device, looking for the device that requested the interrupt. Most interrupts on the MSPM0 microcontrollers are vectored, but there are some triggers that share the same vector. For these interrupts the ISR must poll to see which trigger caused the interrupt. For example, all input pins on one GPIO port can trigger an interrupt, but the trigger flags share the same vector. So if multiple pins on one GPIO port are armed, the shared ISR must poll to determine which one(s) requested service.
The third necessary component of the interface is the ability for the computer to acknowledge the interrupt. Normally there is a trigger flag in the interface that is set on the busy to ready state transition. In essence, this trigger flag is the cause of the interrupt. Acknowledging the interrupt involves clearing this flag. It is important to shut off the request, so that the computer will not mistakenly request a second (and inappropriate) interrupt service for the same condition. Except for periodic SysTick interrupts, TM4C microcontrollers use software acknowledge. So when designing an interrupting interface, it will be important to know exactly what hardware condition sets the trigger flag (and request an interrupt) and how the software will clear it (acknowledge) in the ISR.
Common Error: The system will crash if the interrupt service routine doesn’t either acknowledge or disarm the device requesting the interrupt.
Common Error: The ISR software should not disable interrupts at the beginning nor should it reenable interrupts at the end. Which interrupts are allowed to run is automatically controlled by the priority set in the NVIC.
For regular function calls we use the registers and stack to pass parameters, but interrupt threads have logically separate registers and stack. More specifically, registers are automatically saved by the processor as it switches from main program (foreground thread) to interrupt service routine (background thread). Exiting an ISR will restore the registers back to their previous values. Thus, all parameter passing must occur through global memory. One cannot pass data from the main program to the interrupt service routine using registers or the stack.
In this chapter, multi-threading means one main program (foreground thread) and multiple ISRs (background threads). An operating system allows multiple foreground threads. For more information on operating systems see Embedded Systems: Real-Time Operating Systems for ARM® Cortex™-M Microcontrollers. Synchronizing threads is a critical task affecting efficiency and effectiveness of systems using interrupts. In this section, we will present in general form three constructs to synchronize threads: binary semaphore, mailbox, and FIFO queue.
Video 5.2.1. Inter-Thread Communication and Synchronization
A binary semaphore is simply a shared flag, as described in Figure 5.2.1. There are two operations one can perform on a semaphore. Signal is the action that sets the flag. Wait is the action that checks the flag, and if the flag is set, the flag is cleared and important stuff is performed. This flag must exist as a private global variable with restricted access to only these two code pieces. In C, we add the qualifier static to a global variable to restrict access to software within the same file. In order to reduce complexity of the system, it will be important to limit the access to this flag to as few modules as possible.
Figure 5.2.1. A semaphore can be used to synchronize threads.
A flag of course has two states: 0 and 1. However, it is good design to assign a meaning to this flag. For example, 0 might mean the switch has not been pressed, and 1 might mean the switch has been pressed. Figure 5.2.1 shows two examples of the binary semaphore. The big arrows in this figure signify synchronization links between the threads. In the example on the left, the ISR signals the semaphore and the main program waits on the semaphore. Notice the “important stuff” is run in the foreground once per execution of the ISR. In the example on the right, the main program signals the semaphore and the ISR waits. It is good design to have NO backwards jumps in an ISR. In this particular application, if the ISR is running and the semaphore is 0, the action is just skipped and the computer returns from the interrupt.
The second inter-thread synchronization scheme is the mailbox. The mailbox is a binary semaphore with associated data variable. Interactive Tool 5.2.1 illustrates an input device interfaced using interrupt synchronization and uses a mailbox to send data from ISR to the main program. The mailbox structure is implemented with two shared global variables. Mail contains data, and Status is a semaphore flag specifying whether the mailbox is full or empty.
Figure 5.2.2. A mailbox can be used to pass data between threads.
Code Blocks, A, B, C and D refer to the blocks labelled (lower-case) in Figure 5.2.2. The interrupt is requested when its trigger flag is set, signifying new data are ready from the input device. The ISR will read the data from the input device and store it in the shared global variable Mail, then update its Status to full. The main program will perform other calculations, while occasionally checking the status of the mailbox. When the mailbox has data, the main program will process it. This approach is adequate for situations where the input bandwidth is slow compared to the software processing speed. One way to visualize the interrupt synchronization is to draw a state versus time plot of the activities of the hardware, the mailbox, and the two software threads .
Use the following tool to how the foreground and background thread communicate using a "mailbox"
Using the tool demonstrates that during execution of block A, the mailbox is empty, the input device is idle and the main program is performing other tasks, because mailbox is empty. When new input data are ready, the trigger flag will be set, and an interrupt will be requested. In block B the ISR reads data from input device and saves it in Mail, and then it sets Status to full. The main program recognizes Status is full in Block C. In Block D, the main program processes data from Mail, sets Status to empty. Notice that even though there are two threads, only one is active at a time. The interrupt hardware switches the processor from the main program to the ISR, and the return from interrupt switches the processor back.
The third synchronization technique is the FIFO queue. The use of a FIFO is similar to the mailbox, but allows buffering, which is storing data in a first come first served manner. For an input device, an interrupt occurs when new input data are available, the ISR reads the data from the input device, and puts the data in the FIFO. Whenever the main program is idle, it will attempt to get data from the FIFO. If data were to exist, that data will be processed. The big arrows in Figures 5.2.3 and 5.2.4 signify the communication and synchronization link between the background and foreground.
Figure 5.2.3. For an input device we can use a FIFO to pass data from the ISR to the main program.
For an output device, the main program puts data into the FIFO whenever it wishes to perform output. This data is buffered, and if the system is properly configured, the FIFO never becomes full and the main program never actually waits for the output to occur. An interrupt occurs when the output device is idle, the ISR gets from the FIFO and write the data to the output device. Whenever the ISR sees the FIFO is empty, it could cause the output device to become idle. The direction of the big arrows in Figures 5.2.3 and 5.2.4 signify the direction of data flow in these buffered I/O examples.
Figure 5.2.4. For an output device we can use a FIFO to pass data from the main program to the ISR.
There are other types of interrupt that are not an input or output. For example we will configure the computer to request an interrupt on a periodic basis. This means an interrupt handler will be executed at fixed time intervals. This periodic interrupt will be essential for the implementation of real-time data acquisition and real-time control systems. For example if we are implementing a digital controller that executes a control algorithm 100 times a second, then we will set up the internal timer hardware to request an interrupt every 10 ms. The interrupt service routine will execute the digital control algorithm and then return to the main thread. In a similar fashion, we will use periodic interrupts to perform analog input and/or analog output. For example if we wish to sample the ADC 100 times a second, then we will set up the internal timer hardware to request an interrupt every 10 ms. The interrupt service routine will sample the ADC, process (or save) the data, and then return to the main thread.
Performance Tip: It is poor design to employ backward jumps in an ISR, because they may affect the latency of other interrupt requests. Whenever you are thinking about using a backward jump, consider redesigning the system with more or different triggers to reduce the number of backward jumps.
On the ARM Cortex-M processor, exceptions include resets, software interrupts and hardware interrupts. Interrupts on the Cortex-M are controlled by the Nested Vectored Interrupt Controller (NVIC). Each exception has an associated 32-bit vector that points to the memory location where the ISR that handles the exception is located. Vectors are stored in ROM at the beginning of memory. Program 5.3.1 shows the first few vectors as defined in the startup_mspm0g3507_ticlang.c file. DCD is an assembler pseudo-op that defines a 32-bit constant. ROM location 0x0000.0000 has the initial stack pointer, and location 0x0000.0004 contains the initial program counter, which is called the reset vector. It points to a function called the reset handler, which is the first thing executed following reset. There are up to 240 possible interrupt sources and their 32-bit vectors are listed in order starting with location 0x0000.0008. From a programming perspective, we can attach ISRs to interrupts by writing the ISRs as regular assembly subroutines or C functions with no input or output parameters and editing the startup_mspm0g3507_ticlang.c file to specify those functions for the appropriate interrupt. For example, if we wrote a Port A edge-triggered interrupt service routine named GROUP0ISR, then we would replace GROUP0_IRQHandler with GROUP0ISR. In this class, we will write our ISRs using standard function names so that the startup_mspm0g3507_ticlang.c file need not be edited. I.e., we will simply name the ISR for edge-triggered interrupts on Port A as GROUP0_IRQHandler. The ISR for this interrupt is a 32-bit pointer located at ROM address 0x0000.0040. Because the vectors are in ROM, this linkage is defined at compile time and not at run time. For more details see the startup_mspm0g3507_ticlang.c files within the interrupt examples posted on the book web site.
void (* const interruptVectors[])(void) = {
(void (*)(void))((uint32_t)&__STACK_END), /* initial SP */
Reset_Handler, /* The reset handler */
NMI_Handler, /* The NMI handler */
HardFault_Handler, /* The hard fault handler */
0, /* Reserved */
0, /* Reserved */
0, /* Reserved */
0, /* Reserved */
0, /* Reserved */
0, /* Reserved */
0, /* Reserved */
SVC_Handler, /* SVCall handler */
0, /* Reserved */
0, /* Reserved */
PendSV_Handler, /* The PendSV handler */
SysTick_Handler, /* SysTick handler */
GROUP0_IRQHandler, /* GROUP0 interrupt handler */
GROUP1_IRQHandler, /* GROUP1 interrupt handler */
TIMG8_IRQHandler, /* TIMG8 interrupt handler */
UART3_IRQHandler, /* UART3 interrupt handler */
ADC0_IRQHandler, /* ADC0 interrupt handler */
ADC1_IRQHandler, /* ADC1 interrupt handler */
CANFD0_IRQHandler, /* CANFD0 interrupt handler */
DAC0_IRQHandler, /* DAC0 interrupt handler */
0, /* Reserved */
SPI0_IRQHandler, /* SPI0 interrupt handler */
SPI1_IRQHandler, /* SPI1 interrupt handler */
0, /* Reserved */
0, /* Reserved */
UART1_IRQHandler, /* UART1 interrupt handler */
UART2_IRQHandler, /* UART2 interrupt handler */
UART0_IRQHandler, /* UART0 interrupt handler */
TIMG0_IRQHandler, /* TIMG0 interrupt handler */
TIMG6_IRQHandler, /* TIMG6 interrupt handler */
TIMA0_IRQHandler, /* TIMA0 interrupt handler */
TIMA1_IRQHandler, /* TIMA1 interrupt handler */
TIMG7_IRQHandler, /* TIMG7 interrupt handler */
TIMG12_IRQHandler, /* TIMG12 interrupt handler */
Program 5.3.1. Software syntax to set the interrupt vectors for the MSPM0 (only some vectors are shown, see the startup_mspm0g3507_ticlang.c file for a complete list).
Program 5.3.2 shows that the syntax for an ISR looks like a function with no parameters. Notice that each ISR (except for SysTick) must acknowledge the interrupt in software by clearing the flag that caused the interrupt. In Program 5.3.2, we assume the interrupt was caused by an edge on PB21 input, so writing to the ICLR register will clear trigger flag 21.
void GROUP1_IRQHandler(void){
GPIOB->CPU_INT.ICLR = 0x00200000; // ack, clear interrupt flag21
// stuff
}
Program 5.3.2. Typical interrupt service routine.
Vector address | Number | IRQ | ISR name | Usage |
0x0000002C | 11 | -5 | SVC_Handler | Software interrupt |
0x00000038 | 14 | -2 | PendSV_Handler | Software interrupt to OS |
0x0000003C | 15 | -1 | SysTick_Handler | Periodic timer |
0x00000040 | 16 | 0 | GROUP0_IRQHandler | Port A edge triggered |
0x00000044 | 17 | 1 | GROUP1_IRQHandler | Port B edge triggered |
0x00000048 | 18 | 2 | TIMG8_IRQHandler | Timer |
0x0000004C | 19 | 3 | UART3_IRQHandler | Asynchronous serial I/O |
0x00000050 | 20 | 4 | ADC0_IRQHandler | Analog to digital |
0x00000054 | 21 | 5 | ADC1_IRQHandler | Analog to digital |
0x00000058 | 22 | 6 | CANFD0_IRQHandler | Controller area network |
0x0000005C | 23 | 7 | DAC0_IRQHandler | Digital to analog |
0x00000064 | 25 | 9 | SPI0_IRQHandler | Synchronous serial I/O |
0x00000068 | 26 | 10 | SPI1_IRQHandler | Synchronous serial I/O |
0x00000074 | 29 | 13 | UART1_IRQHandler | Asynchronous serial I/O |
0x00000078 | 30 | 14 | UART2_IRQHandler | Asynchronous serial I/O |
0x0000007C | 31 | 15 | UART0_IRQHandler | Asynchronous serial I/O |
0x00000080 | 32 | 16 | TIMG0_IRQHandler | Timer |
0x00000084 | 33 | 17 | TIMG6_IRQHandler | Timer |
0x00000088 | 34 | 18 | TIMA0_IRQHandler | Timer |
0x0000008C | 35 | 19 | TIMA1_IRQHandler | Timer |
0x00000090 | 36 | 20 | TIMG7_IRQHandler | Timer |
0x00000094 | 37 | 21 | TIMG12_IRQHandler | Timer |
Table 5.3.1. Some of the interrupt vectors for the MSPM0.
: Where is the vector for SysTick? What should you call the name of software function which is the SysTick interrupt service routine?
To activate an interrupt source we need to set its priority and enable that source in the NVIC. This activation is in addition to the arm and enable steps. Table 5.3.1 lists some of the interrupt sources available on the MSPM0 family of microcontrollers. Interrupt numbers 0 to 15 contain the faults, software interrupt and SysTick. SysTick does not require access to NVIC->ISER to enable. The SysTick priority is set in bits 31 and 30 of SCB->SHP[1].
Table 5.3.2 shows some of the priority registers on the NVIC. Each register contains an 8-bit priority field for four devices. On the MSPM0 microcontrollers, only the top two bits of the 8-bit field are used. This allows us to specify the interrupt priority level for each device from 0 to 3, with 0 being the highest priority. The interrupt number (number column in Table 5.3.1) is loaded into the IPSR register. The servicing of interrupts does not set the I bit in the PRIMASK, so a higher priority interrupt can suspend the execution of a lower priority ISR. If a request of equal or lower priority is generated while an ISR is being executed, that request is postponed until the ISR is completed. In particular, those devices that need prompt service should be given high priority.
ISR name | NVIC priority | Priority bits | NVIC enable | Enable bit |
PendSV_Handler | SCB->SHP[1] | 23 – 22 | -- | -- |
SysTick_Handler | SCB->SHP[1] | 31 – 30 | -- | -- |
GROUP0_IRQHandler | NVIC->IP[0] | 7 – 6 | NVIC->ISER[0] | 0 |
GROUP1_IRQHandler | NVIC->IP[0] | 15 – 14 | NVIC->ISER[0] | 1 |
TIMG8_IRQHandler | NVIC->IP[0] | 23 – 22 | NVIC->ISER[0] | 2 |
UART3_IRQHandler | NVIC->IP[0] | 31 – 30 | NVIC->ISER[0] | 3 |
ADC0_IRQHandler | NVIC->IP[1] | 7 – 6 | NVIC->ISER[0] | 4 |
ADC1_IRQHandler | NVIC->IP[1] | 15 – 14 | NVIC->ISER[0] | 5 |
CANFD0_IRQHandler | NVIC->IP[1] | 23 – 22 | NVIC->ISER[0] | 6 |
DAC0_IRQHandler | NVIC->IP[1] | 31 – 30 | NVIC->ISER[0] | 7 |
SPI0_IRQHandler | NVIC->IP[2] | 15 – 14 | NVIC->ISER[0] | 9 |
SPI1_IRQHandler | NVIC->IP[2] | 23 – 22 | NVIC->ISER[0] | 10 |
UART1_IRQHandler | NVIC->IP[3] | 15 – 14 | NVIC->ISER[0] | 13 |
UART2_IRQHandler | NVIC->IP[3] | 23 – 22 | NVIC->ISER[0] | 14 |
UART0_IRQHandler | NVIC->IP[3] | 31 – 30 | NVIC->ISER[0] | 15 |
TIMG0_IRQHandler | NVIC->IP[4] | 7 – 6 | NVIC->ISER[0] | 16 |
TIMG6_IRQHandler | NVIC->IP[4] | 15 – 14 | NVIC->ISER[0] | 17 |
TIMA0_IRQHandler | NVIC->IP[4] | 23 – 22 | NVIC->ISER[0] | 18 |
TIMA1_IRQHandler | NVIC->IP[4] | 31 – 30 | NVIC->ISER[0] | 19 |
TIMG7_IRQHandler | NVIC->IP[5] | 7 – 6 | NVIC->ISER[0] | 20 |
TIMG12_IRQHandler | NVIC->IP[5] | 15 – 14 | NVIC->ISER[0] | 21 |
Table 5.3.2. The MSPM0 NVIC registers. Each register is 32 bits wide. Bits not shown are zero.
The NVIC->ISER[0] register contains one bit for each IRQ number from 0 to 31. We write one to the corresponding bit of NVIC->ISER[0] to enable that IRQ. Writing zeros to NVIC->ISER[0] has no affect. So NVIC->ISER[0] = 1; will enable GROUP0 (IRQ=0), without affecting the other 31 interrupts. The NVIC->ICER[0] register also contains one bit for each IRQ number from 0 to 31. We write one to the corresponding bit of NVIC->ICER[0] to disable that IRQ, without affecting the other 31 interrupts. Writing zeros to NVIC->ISER[0] has no affect. So NVIC->ISER[0] = 1; will disable GROUP0 (IRQ=0).
: Consider using TimerG0 for a periodic interrupt. How would you set its priority to 1?
: Consider using TimerG0 for a periodic interrupt. How would you enable TimerG0 in the NVIC?
: What does this code do? SCB->SHP[1]=(SCB->SHP[1]&(~0xC0000000))|(2<<30);
Interactive tool 5.4.1 shows the context switch from executing in the foreground to running a periodic SysTick ISR. Before the interrupt occurs, the I bit in the PRIMASK is 0 signifying interrupts are enabled, and the interrupt number (ISRNUM) in the IPSR register is 0, meaning we are running in Thread mode (i.e., the main program, and not an ISR). Handler mode is signified by a nonzero value in IPSR. When BASEPRI register is zero, all interrupts are allowed and the BASEPRI register is not active.
When the SysTick counter goes from 1 to 0, the Count flag in the SysTick->CTRL register is set, triggering an interrupt. The current instruction is finished. (a) Eight registers are pushed on the stack with R0 on top. These registers are pushed onto the stack . (b) The vector address is loaded into the PC (“Vector address” column in Table 5.3.1). (c) The IPSR register is set to 15 (“Number” column in Table 5.3.1) (d) The top 24 bits of LR are set to 0xFFFFFF, signifying the processor is executing an ISR. The bottom eight bits specify how to return from interrupt.
0xE1 Return to Handler mode MSP (using floating point state on a Cortex M4)
0xE9 Return to Thread mode MSP (using floating point state on a Cortex M4)
0xED Return to Thread mode PSP (using floating point state on a Cortex M4)
0xF1 Return to Handler mode MSP
0xF9 Return to Thread mode MSP ← in this class we will always be using this one
0xFD Return to Thread mode PSP
After pushing the registers, the processor always uses the main stack pointer (MSP) during the execution of the ISR. Events 2, 3, and 4 can occur simultaneously
Use the following tool to see the steps involved in a context switch from the executing the foreground thread to switching to the background thread (ISR) when the Systick interrupt occurs.
To return from an interrupt, the ISR executes the typical function return BX LR. However, since the top 24 bits of LR are 0xFFFFFF, it knows to return from interrupt by popping the eight registers off the stack. Since the bottom eight bits of LR in this case are 0b11111001, it returns to thread mode using the MSP as its stack pointer. Since the IPSR is part of the PSR that is popped, it is automatically reset its previous state.
A nested interrupt occurs when a higher priority interrupt suspends an ISR. The lower priority interrupt will finish after the higher priority ISR completes. When one interrupt preempts another, the LR is set to 0xFFFFFFF1, so it knows to return to handler mode. Tail chaining occurs when one ISR executes immediately after another. Optimization occurs because the eight registers need not be popped only to be pushed once again. If an interrupt is triggered and is in the process of stacking registers when a higher priority interrupt is requested, this late arrival interrupt will be executed first.
Priority determines the order of service when two or more requests are made simultaneously. Priority also allows a higher priority request to suspend a lower priority request currently being processed. Usually, if two requests have the same priority, we do not allow them to interrupt each other. NVIC assigns a priority level to each interrupt trigger. This mechanism allows a higher priority trigger to interrupt the ISR of a lower priority request. Conversely, if a lower priority request occurs while running an ISR of a higher priority trigger, it will be postponed until the higher priority service is complete.
Observation: There are many interrupt sources, but an effective system will use only a few.
Program 5.4.1 gives the definitions that allow the software to enable and disable interrupts. These functions are callable from either assembly or C code. The wait for interrupt can be used to place the processor in low-power sleep mode while it waits for an interrupt.
//*********** DisableInterrupts ***************
// disable interrupts
// inputs: none
// outputs: none
__disable_irq: CPSID I // set I=1
BX LR
//*********** EnableInterrupts ***************
// enable interrupts
// inputs: none
// outputs: none
__enable_irq: CPSIE I // set I=0
BX LR
//*********** WaitForInterrupt ************************
// go to low power mode while waiting for the next interrupt
// inputs: none
// outputs: none
__WFI:
WFI
BX LR
Program 5.4.1. Assembly functions needed for interrupt enabling and disabling.
One application of periodic interrupts is called “intermittent polling” or “periodic polling”. Figure 5.5.1 shows busy wait side by side with periodic polling. In busy-wait synchronization, the main program polls the I/O devices continuously. With periodic polling, the I/O devices are polled on a regular basis (established by the periodic interrupt.) If no device needs service, then the interrupt simply returns.
Figure 5.5.1. On the left is busy-wait, and on the right is periodic polling.
If the polling period is Δt, then on average the interface latency will be ½Δt, and the worst case latency will be Δt. Periodic polling is appropriate for low bandwidth devices where real-time response is not necessary. This method frees the main program to perform other functions. We use periodic polling if the following two conditions apply:
1. The I/O hardware cannot generate interrupts directly
2. We wish to perform the I/O functions in the background
For a data acquisition system, it is important to establish an accurate sampling rate. The time in between ADC samples must be equal and known in order for the digital signal processing to function properly. Similarly for microcontroller-based control systems, it is important to maintain a periodic rate for reading data from the sensors and outputing commands to the actuators.
The SysTick timer is a simple way to create periodic interrupts. A periodic interrupt is one that is requested on a fixed time basis. This interfacing technique is required for data acquisition and control systems, because software servicing must be performed at accurate time intervals.
Table 5.5.1 shows the SysTick registers used to create a periodic interrupt. SysTick has a 24-bit counter that decrements at the bus clock frequency. Let fBUS be the frequency of the bus clock, and let n be the value of the SysTick->LOAD register. The frequency of the periodic interrupt will be fBUS/(n+1). First, we clear the ENABLE bit to turn off SysTick during initialization. Second, we set the SysTick->LOAD register. Third, we write any value to SysTick->VAL to clear the counter. Lastly, we write the desired mode to the control register, SysTick->CTRL. We set CLK_SRC=1, selecting the 80 MHz bus clock to decrement VAL. We set INTEN to enable interrupts. We establish the priority of the SysTick interrupts using the TICK field in the SCB->SHP[1] register. We need to set the ENABLE bit so the counter will run. When the VAL value counts down from 1 to 0, the COUNT flag is set. On the next clock, the VAL is loaded with the LOAD value. In this way, the SysTick counter (VAL) is continuously decrementing. If the LOAD value is n, then the SysTick counter operates at modulo n+1 (…n, n-1, n-2 … 1, 0, n, n-1, …). In other words, it rolls over every n+1 counts. Thus, the COUNT flag will be set every n+1 counts. Program 5.5.1 shows a simple example of SysTick. SysTick is the only interrupt on the MSPM0 that has an automatic acknowledge. Running the SysTick ISR will clear trigger flag, COUNT, automatically. Notice there is no explicit software step in the ISR to clear the COUNT flag.
Address |
31-24 |
23-17 |
16 |
15-3 |
2 |
1 |
0 |
Name |
0xE000E010 |
0 |
0 |
COUNT |
0 |
CLK_SRC |
INTEN |
ENABLE |
SysTick->CTRL |
0xE000E014 |
0 |
24-bit reload value |
SysTick->LOAD |
|||||
0xE000E018 |
0 |
24-bit current value of SysTick counter |
SysTick->VAL |
Address |
31-30 |
29-24 |
23-22 |
21-8 |
7-6 |
5-0 |
Name |
0xE000ED20 |
TICK |
0 |
PENDSV |
0 |
DEBUG |
0 |
SCB->SHP[1] |
Table 5.5.1. SysTick registers. TICK is the 2-bit priority field for SysTick.
volatile uint32_t Counts=0; // elapsed time in ms
void SysTick_Init(uint32_t period){
SysTick->CTRL = 0; // disable SysTick during setup
SysTick->LOAD = period-1; // reload value
SysTick->VAR = 0; // any write to VAL clears COUNT and sets VAL equal to LOAD
SCB->SHP[1] = (SCB->SHP[1]&(~0xC0000000))|0x40000000; // priority 2
SysTick->CTRL = 0x07; // enable SysTick with 80 MHz bus clock and interrupts
// enable interrupts after all initialization is finished
}
void SysTick_Handler(void){
// COUNT is set when VAL counts from 1,0
// After VAL is 0, it then becomes LOAD and continues to count down
// COUNT is the trigger flag, so an interrupt occurs every (LOAD+1)*12.5ns
// COUNT is automatically cleared by running this ISR
GPIOB->DOUT31_0 ^= 1<<22; // toggle PB22
Counts = Counts + 1;
}
int
main(void){
Clock_Init80MHz(0); // running at 80 MHz
LaunchPad_Init(); // PB22 is an output
Counts = 0;
SysTick_Init(80000); //
initialize SysTick timer, every 1ms
__enable_irq(); // enable after
everything initialized
while(1){
//
interrupts every 1ms, 500 Hz flash
WaitForInterrupt();
}
}
Program 5.5.1. Implementation of a periodic interrupt using SysTick that toggles PB22 every 1ms.
Video 5.5.1. PeriodicSysTickInts Code Demo, ***needs recording***
Example 5.1. Design an interface 32 Ω speaker and use it to generate a soft 1 kHz sound.
Solution: To make sound we need to create an oscillating wave. In this example, the wave will be a simple square wave. At 3.3V, a 32 Ω speaker will require a current of about 100 mA. The maximum the MSPM0 can produce on an output pin is 6 mA. If we place a resistor in series with the head phones, then the current will only be 3.3V/(1500+32Ω) = 2.2mA. To generate the 1 kHz sound we need a 1 kHz square wave. There are many good methods to generate square waves. In this example we will implement one of the simplest methods: period interrupt and toggle an output pin in the ISR. To generate a 1 kHz wave we will toggle the PB0 pin at 2 kHz, or every 500 µs. We will assume the PLL is active and the system is running at 80 MHz. We wish to initialize the SysTick to interrupt with a period of 500 µs. The correct value for LOAD is 39999 ((500µs/12.5ns)-1). Alternatively, we could have calculated LOAD by ((80MHz/2kHz)-1). If the bus frequency were to be 16 MHz, we would set the LOAD value to be 7999 ((500µs/62.5ns)-1). Since this sound wave output is a real time signal, we set its priority to highest level, which is 0. See Program 5.5.2.
Figure 5.5.2. A squarewave output connected to a speaker will generate sound.
void Sound_Init(void){
LaunchPad_Init(); // reset and activate ports A B
IOMUX->SECCFG.PINCM[PB0INDEX] = 0x00000081; // output
GPIOB->DOE31_0 |= 0x01; // make PB0 out
SysTick->CTRL = 0x00; // disable SysTick during setup
SysTick->LOAD = 39999; // reload value for 500us (assuming 80MHz)
SysTick->VAR = 0; // any write to current clears it
SCB->SHP[1] = SCB->SHP[1]&(~0xC0000000); // set priority = 0 (bits 31,30)
SysTick->CTRL = 0x00000007; // enable with core clock and interrupts
__enable_irq(); // normally we set I=0 in the main
}
void SysTick_Handler(void){
GPIOB->DOUT31_0 ^= 0x01; // toggle PB0
}
Program 5.5.2. Sound output using a periodic interrupt .
We choose interrupt priority according to the importance of the software task. Our ears can distiguish variations in time much better than our eyes. Furthermore, our eyes are more time-sensitive than our fingers.
: How could we make the sound quieter?
: How would we change Program 5.5.2 to output 523 Hz (middle C)?
The DC motor has a frame that remains motionless (called the stator), and an armature that moves (called the rotor). A brushed DC motor has an electromagnetic coil as well, located on the rotor, and the rotor is positioned inside the stator. In Figure 5.6.1, North and South refer to a permanent magnet, generating a constant B field from left to right. In this case, the rotor moves in a circular manner. When current flows through the coil, a magnetic force is created causing a rotation of the shaft. A brushed DC motor uses commutators to flip the direction of the current in the coil. In this way, the coil on the right always has an up force, and the one on the left always has a down force. Hence, a constant current generates a continuous rotation of the shaft. When the current is removed, the magnetic force stops, and the shaft is free to rotate. In a pulse-width modulated DC motor, the computer activates the coil with a current of fixed magnitude but varies the duty cycle in order to adjust the power delivered to the motor.
Video 5.6.1. Working of a DC Motor
Figure 5.6.1. A brushed DC motor uses a commutator to flip the coil current.
In the LED interface the microcontroller was able to control electrical power to the LED in a binary fashion: either all on or all off. Sometimes it is desirable for the microcontroller to be able to vary the delivered power in a variable manner. One effective way to do this is to use pulse width modulation (PWM). The basic idea of PWM is to create a digital output wave of fixed frequency, but allow the microcontroller to vary its duty cycle. The system is designed in such a way that High+Low is constant (meaning the frequency is fixed). The duty cycle is defined as the fraction of time the signal is high:
Hence, duty cycle varies from 0 to 1. We interface this digital output wave to an external actuator (like a DC motor), such that power is applied to the motor when the signal is high, and no power is applied when the signal is low. We purposely select a frequency high enough so the DC motor does not start/stop with each individual pulse, but rather responds to the overall average value of the wave. The average value of a PWM signal is linearly related to its duty cycle and is independent of its frequency. Let P (P=V*I) be the power to the DC motor, shown in Figure 5.6.2, when the PA0 signal is high. Under conditions of constant speed and constant load, the delivered power to the motor is linearly related to duty cycle.
Delivered Power =
Unfortunately, as speed and torque vary, the developed emf will affect delivered power. Nevertheless, PWM is a very effective mechanism, allowing the microcontroller to adjust delivered power.
The resistance in the coil (R) comes from the long wire that goes from the + terminal to the – terminal of the motor, see Figure 5.6.2. The inductance in the coil (L) arises from the fact that the wire is wound into coils to create the electromagnetics. The coil itself can generate its own voltage (emf) because of the interaction between the electric and magnetic fields. If the coil is a DC motor, then the emf is a function of both the speed of the motor and the developed torque (which in turn is a function of the applied load on the motor.) Because of the internal emf of the coil, the current will depend on the mechanical load. For example, a DC motor running with no load might draw 100 mA, but under load (friction) the current may jump to 1 A.
Figure 5.6.2. The system uses a transistor to turn the motor on and off.
There are lots of motor driver chips, but we will use an NPN Darlington transistor, e.g., TIP120, to allow the software to turn a motor on and off. If the port output is low, no current can flow into the base, so the TIP120 transistor is off, and the collector current, IC, will be zero. If the port output is high, current does flow into the base and VBE goes above VBEsat turning on the TIP120 transistor. The transistor is in the linear range if VBE ≤ VBEsat and Ic = hfe·Ib. The transistor is in the saturated mode if VBE ≥ VBEsat, VCE = 0.7V and Ic < hfe·Ib. We select the value of the Rb resistor to operate the transistor in its saturated mode. We start with the desired motor current, which will be the collector current Ic. The voltage across the coil will be the V - VCE. Next, we calculate the needed base current (Ib) given the current gain of the NPN
Ib = Ic /hfe
knowing the current gain of the NPN (hfe), see Table 5.6.1. Finally, given the output high voltage of the microcontroller (VOH is about 3.3 V) and base-emitter voltage of the NPN (VBEsat) needed to activate the transistor, we can calculate the desired interface resistor.
Rb ≤ (VOH - VBEsat)/ Ib = hfe *(VOH - VBEsat)/ Ic
The inequality means we can choose a smaller resistor, creating a larger Ib. Because the hfe of the transistors can vary a lot, it is a good design practice to make the Rb resistor 0.1 to 0.5 times the value shown in the above equation. Since the transistor is saturated, the increased base current produces the same VCE and thus the same coil current.
Parameter |
TIP120 (at ICE=200mA) |
hfe |
900 |
VBEsat |
1.3 V |
VCE |
0.7 V |
ICE |
Up to 5A |
Table 5.6.1. Design parameters for the TIP120.
Example 5.6.1. Design an interface for an 8-V 200-mA geared DC motor. SW1 will make the motor spin faster and SW2 will make it spin slower.
Solution: We will use the TIP120 circuit as shown in Figure 5.6.2 because the TIP120 can sink many times the current needed for this motor. We select a +8.4V battery supply and connect it to the positive side of the motor. Just like the stepper motor, when designing with motors we will not use the 3.3V or +5V from the LaunchPad. Although the current is only 200 mA when spinning unloaded, it will increase to 1 A if the motor experiences mechanical friction. The needed base current is
Ib = Icoil /hfe = 200mA/900 = 0.22mA
The desired interface resistor.
Rb ≤ (VOH - Vbe)/ Ib = (3.3-1.3)/0.22mA = 9 kΩ
To cover the variability in hfe, we will use a 1 kΩ resistor instead of the 9 kΩ. The actual voltage across the motor when active will be +8.4-0.7 = 7.7V. Motors and transistors vary a lot, so it is appropriate to experimentally verify the design by measuring the voltages and currents.
Program 5.6.1 is used to control the motor. Interrupts are triggered on the falling edges of PF4 and PF0. The period of the PWM output is chosen to be about 10 times shorter than the time constant of the motor. The electronic driver will turn on and off every 1ms, but the motor only responds to the average level. The software sets the duty cycle of the PWM to adjust the delivered power. When active, the interface will drive +7.65 V across the motor. The actual current will be a function of the friction applied to the shaft. The software maintains H+L to be 80,000 (Figure 5.6.3); this will set the period on PA0 to be a fixed value of 1ms. The duty cycle, L/(H+L), will specify the amount of electrical power delivered to the motor.
Figure 5.6.3. The duty cycle of the PWM output sets the power to the motor.
Video 5.6.2. Duty Cycle Flowchart and Timing
The interface for one motor is shown in Figure 5.6.4. If the robot has two motors then a second copy of the TIP120 circuit will be needed. Edge-triggered interrupts will change the duty cycle by ±10% depending on whether the operator touches S1 (up) or S2 (down). Since the three ISRs pass data (H,L passed from GROUP0_IRQHandler and GROUP1_IRQHandler to SysTick_Handler), we will set the interrupt priorities to be equal. With equal priorities no ISR will interrupt another. This way, the global variables H,L will always be in a consistent state.
Figure 5.6.4. DC motor interface (bold lines show path of the large current).
uint32_t H,L; // shared global variables
void Motor_Init(void){
H = 8000; L = 72000; // 10%
IOMUX->SECCFG.PINCM[PA0INDEX] = 0x00000081; // output
GPIOA->DOE31_0 |= 0x01; // make PA0 out
GPIOA->DOUT31_0 &= ~0x01; // make PA0 low
SysTick->CTRL = 0x00; // disable SysTick during setup
SysTick->LOAD = L-1; // first reload value doesn't matter
SysTick->VAR = 0; // any write to current clears it
SCB->SHP[1] = (SCB->SHP[1]&0x00FFFFFF)|0x40000000; // priority 2
SysTick->CTRL = 0x00000007; // enable with core clock and interrupts
}
void SysTick_Handler(void){
if(GPIOA->DOUT31_0&0x01){ // toggle PA0
GPIOA->DOUT31_0 &= ~0x01; // make PA0 low
SysTick->LOAD = L-1; // reload value for low phase
} else{
GPIOA->DOUT31_0 |= 0x01; // make PA0 high
SysTick->LOAD = H-1; // reload value for high phase
}
}
void Switch_Init(void){
// LaunchPad_Init will make PA18 an input, and PB21 an input with internal pullup.
GPIOA->POLARITY31_16 = 0x00000010; // rising
GPIOA->CPU_INT.ICLR = 0x00040000; // clear bit 18
GPIOA->CPU_INT.IMASK = 0x00040000; // arm PA18
NVIC->IP[0] = (NVIC->IP[0]&(~0x0000FF00))|2<<6; // set priority (bits 15,14) IRQ 0
NVIC->ISER[0] = 1 << 0; // Group0 interrupt
GPIOB->POLARITY31_16 = 0x00000800; // falling
GPIOB->CPU_INT.ICLR = 0x00200000; // clear bit 21
GPIOB->CPU_INT.IMASK = 0x00200000; // arm PB21
NVIC->IP[0] = (NVIC->IP[0]&(~0x0000FF00))|2<<14; // set priority (bits 15,14) IRQ 1
NVIC->ISER[0] = 1 << 1; // Group1 interrupt
GPIOA->CPU_INT.ICLR = 0x00040000; // clear bit 18
GPIOB->CPU_INT.ICLR = 0x00200000; // clear bit 21
}
// L range: 8000,16000,24000,32000,40000,48000,56000,64000,72000
// power: 10% 20% 30% 40% 50% 60% 70% 80% 90%
void GROUP0_IRQHandler(void){ // called on touch of S1
GPIOA->CPU_INT.ICLR = 0x00040000; // clear bit 18
if(L<72000) L = L+8000; // speed up
H = 80000-L; // constant period of 1ms, variable duty cycle
}
void GROUP1_IRQHandler(void){ // called on touch of S2
GPIOB->CPU_INT.ICLR = 0x00200000; // clear bit 21
if(L>8000) L = L-8000; // slow down
H = 80000-L; // constant period of 1ms, variable duty cycle
}
int main(void){
__disable_irq(); // disable interrupts while initializing
Clock_Init80MHz(0); // bus clock at 80 MHz
LaunchPad_Init(); // reset and activate ports A and B
Motor_Init(); // output from PA0, SysTick interrupts
Switch_Init(); // arm edge interrupts
__enable_irq(); // enable after all initialization are done
while(1){ // main program is free to perform other tasks
__WFI(); // low power mode
}
}
Program 5.6.1. Motor output using a periodic interrupt. Switches are used to adjust the power.
Video 5.6.3. PWM Software control of DC Motor
An analog signal is one that is continuous in both amplitude and time. Neglecting quantum physics, most signals in the world exist as continuous functions of time in an analog fashion (e.g., voltage, current, position, angle, speed, force, pressure, temperature, and flow etc.) In other words, the signal has amplitude that can vary over time, but the value cannot instantaneously change. To represent a signal in the digital domain we must approximate it in two ways: amplitude quantizing and time quantizing (or Sampling). From an amplitude perspective, we will first place limits on the signal restricting it to exist between a minimum and maximum value (e.g., 0 to +3.3V), and second, we will divide this amplitude range into a finite set of discrete values. The range of the system is the maximum minus the minimum value. The precision of the system defines the number of values from which the amplitude of the digital signal is selected. Precision can be given in number of alternatives, binary bits, or decimal digits. The resolution is the smallest change in value that is significant. Furthermore, with respect to time one considers analog signals to exist from time equals minus infinity to plus infinity. Because memory is finite, when representing signals on a digital computer, we will restrict signal to a finite time, or we could have a finite set of data that are repeated over and over.
|
|
Problems playing this file? See media help. |
Figure 5.7.1 shows a temperature waveform (solid line), with a corresponding digital representation sampled at 1 Hz and stored as a 5-bit integer number with a range of 0 to 31 oC. Because it is digitized in both amplitude and time, the digital samples (individual dots) in Figure 5.7.1 must exist at an intersection of grey lines. Because it is a time-varying signal (mathematically, this is called a function), we have one amplitude for each time, but it is possible for there to be 0, 1, or more times for each amplitude.
Figure 5.7.1. An analog signal is represented in the digital domain as discrete samples
Video 5.7.1. Fundamentals of Digitization
The second approximation occurs in the time domain. Time quantizing is caused by the finite sampling interval. For example, the data are sampled every 1 second in Figure 5.7.1. In practice we will use a periodic timer to trigger an analog to digital converter (ADC) to digitize information, converting from the analog to the digital domain. Similarly, if we are converting from the digital to the analog domain, we use the periodic timer to output new data to a digital to analog converter (DAC). The Nyquist Theorem states that if the signal is sampled with a frequency of fs, then the digital samples only contain frequency components from 0 to ½ fs. Conversely, if the analog signal does contain frequency components larger than ½ fs, then there will be an aliasing error during the sampling process (performed with a frequency of fs). Aliasing is when the digital signal appears to have a different frequency than the original analog signal. Also note, the digital data has 11 values at times 0 to 10, but no information before time=0, and no information after time=10.
: Why can’t the digital samples represent the little wiggles in the analog signal?
: Why can’t the digital samples represent temperatures above 31 oC?
: What range of frequencies is represented in the digital samples when the ADC is sampled once every millisecond?
: If I wanted to create an analog output wave with frequency components from 0 to 11 kHz, what is the slowest rate at which I could output to the DAC?
Proving the Nyquist Theorem mathematically is beyond the scope of this course, but we can present a couple examples to support the basic idea of the Nyquist Theorem: we must sample at a rate faster than twice the rate at which signal itself is oscillating.
Video 5.7.2. The Nyquist Theorem Illustrated
Example 1) There is a long distance race with runners circling around an oval track and it is your job to count the number of times a particular runner circles the track. Assume the fastest time a runner can make a lap is 60 seconds. You want to read a book and occasionally look up to see where your runner is. How often to you have to look at your runner to guarantee you properly count the laps? If you look at a period faster than every 30 seconds you will see the runner at least twice per lap and properly count the laps. If you look at a period slower than every 30 seconds, you may only see the runner once per lap and not know if the runner is going very fast or very slow. In this case, the runner oscillates at most 1 lap per minute and thus you must observe the runner at a rate faster than twice per minute.
Example 2) You live on an island and want to take the boat back to the mainland as soon as possible. There is a boat that arrives at the island once a day, waits at the dock for 12 hours and then it sets sail to the mainland. Because of weather conditions, the exact time of arrival is unknown, but the boat will always wait at the dock for 12 hours before it leaves. How often do you need to walk down to the dock to see if the boat is there? If the boat is at the dock, you get on the boat and take the next trip back to the mainland. If you walk down to the dock every 13 hours, it is possible to miss the boat. However, if you walk down to the dock every 12 hours or less, you’ll never miss the boat. In this case, the boat frequency is once/day and you must sample it (go to the dock) two times/day.
Discover the Nyquist Theorem. A Continuous waveform like Figure 5.7.1 is shown, V = 1.5 + 1*sin(2pi50t)+0.5*cos(2pi 200t). You may select the sampling rate and the precision (in bits) to see the signal captured. Notice that at sampling rates above 100 Hz you capture the essence of the 50Hz periodic wave, and above 400 Hz you capture the essence of both the 50 and 200 Hz waves. To “capture the essence” means the analog and digital signal go up and down at the same rate.
A DAC converts digital signals into analog form as illustrated in Figure 5.8.1. Although one can interface a DAC to a regular output port, most DACs are interfaced using high-speed synchronous protocols, like the SPI. The DAC output can be current or voltage. Additional analog processing may be required to filter, amplify, or modulate the signal. We can also use DACs to design variable gain or variable offset analog circuits.
The DAC precision is the number of distinguishable DAC outputs (e.g., 16 alternatives, 4 bits). The DAC range is the maximum and minimum DAC output (volts, amps). The DAC resolution is the smallest distinguishable change in output. The units of resolution are in volts or amps depending on whether the output is voltage or current. The resolution is the change in output that occurs when the digital input changes by 1. For most DACs there is a simple relationship between range precision and resolution.
Range(volts) = Precision(alternatives) • Resolution(volts)
Figure 5.8.1. A 4-bit DAC provides analog output.
Video 5.8.1. Design of a DAC circuit
Learn about DAC precision using this tool. The analog output is fixed in the range 0 to 3.3V. You may adjust the number of bits from 1 to 10 to see how close the fit is to the straight line.
1 10 Number of DAC bits = 0The DAC accuracy is (Actual - Ideal) / Ideal where Ideal is referred to the National Institute of Standards and Technology (NIST). One can choose the full scale range of the DAC to simplify the use of fixed-point math. For example, if an 8-bit DAC had a full scale range of 0 to 2.55 volts, then the resolution would be exactly 10 mV. This means that if the DAC digital input were 123, then the DAC output voltage would be 1.23 volts.
: 10-bit DAC has a range of 0 to 2.5V, what is the approximate resolution?
: You need a DAC with a range of 0 to 2V, and a resolution of 1 mV. What is the smallest number of bits could you use for the DAC?
Example 5.8.1. Design a 2-bit binary-weighted DAC with a range of 0 to +3.3V using resistors.
Solution: We begin by specifying the desired input/output relationship of the 2-bit DAC. The design specifications are shown in Table 5.8.1.
N |
Q1 Q0 |
Vout (V) |
0 |
0 0 |
0.0 |
1 |
0 3.3 |
1.1 |
2 |
3.3 0 |
2.2 |
3 |
3.3 3.3 |
3.3 |
Table 5.8.1. Specifications of the 2-bit binary-weighted DAC.
Assume the output high voltage (VOH) of the microcontroller is 3.3 V, and its output low voltage (VOL) is 0. With a binary-weighted DAC, we choose the resistor ratio to be 2/1 so Q1 bit is twice as significant as the Q0 bit, as shown in Figure 5.8.2. Considering the circuit on the left (no headphones), if both Q1 and Q0 are 0, the output Vout is zero. If Q1 is 0 and Q0 is +3.3V, the output Vout is determined by the resistor divider network
Note the total impedance from 3.3V to ground in the above circuit is 18kΩ. Using Ohm's Law, with the voltage divider equation, we can calculate Vout to be 3.3V*6kΩ/18kΩ, which is 1.1V. If Q1 is +3.3V and Q0 is 0, the output Vout is determined by the network
Again notice the total impedance from 3.3V to ground in this second circuit is 18kΩ. But this time we calculate Vout to be 3.3V*12kΩ/18kΩ, which is 2.2V. If both Q1 and Q0 are +3.3V, the output Vout is +3.3V. The output impedance of this DAC is approximately 12 kΩ, which means it cannot source or sink much current.
Figure 5.8.2. A 2-bit binary-weighted DAC.
If we connect headphones to this DAC, as shown in the right side of Figure 5.8.2, we could hear sounds generated by software writing a sequence of data to the DAC. However, since the impedance of the headphones is much smaller than the impedance of the DAC, the output voltages will be very small, but we could calculate the currents into the headphones. Considering the circuit on the right (with headphones), if both Q1 and Q0 are 0, the output current is zero. If Q1 is 0 and Q0 is +3.3V, the output current, Iout, is 3.3V divided by 12.032kΩ which is 0.275mA. If Q0 is 0 and Q1 is +3.3V, the output current, Iout, is 3.3V divided by 6.032kΩ which is 0.550mA. And finally, if both Q1 and Q0 are 3,3V, Iout is the sum of 0.275+0.550 (Kirkhoff's Current Law), which is about 0.825mA. Notice the current into the headphones is linearly related to the digital value. In other words, digital values of 0,1,2,3 map to currents of 0,0.275,0.550,0.825mA.
Shows how to compute Vout for the two scenarios bit1=0;bit0=1 and bit1=1;bit0=0. Using superposition, the scenario bit1=1;bit0=1 is simply the sum of the two cases: bit1=0;bit0=1 and bit1=1;bit0=0.
bit 0: bit 1:
You can realistically build a 6-bit DAC using the binary-weighted method.
: How do you build a 3-bit binary-weighted DAC using this method?
: How do you build a 3-bit binary-weighted DAC using multiple 20 kΩ resistors?
Before we generate sound, let’s look at how a typical speaker works. Sound exists as varying pressure waves that are created when a physical object moves, vibrating the air next to it. These air pressure waves travel through the air in all directions at about 343 m/sec. Sound can also be generated in water, where it travels at 1484 m/sec. Our ears can sense sounds from 20 Hz to about 20 kHz. In other words, the pressure wave must be oscillating faster than 20 Hz and slower than 20 kHz for us to hear it. See Figure 5.9.1.
Figure 5.9.1. Sound waves exist as pressure waves in media such as air, water, and non-porous solids. For more information on sound, see http://www.mediacollege.com/audio/01/sound-waves.html
There are two
magnets in a speaker. There is a large permanent magnetic that creates
a static magnetic field oriented in the direction the speaker is
facing, and there is a dynamic electromagnet created by a spiral-wound
coil oriented in the same direction. Figure 5.9.2 shows a magnetic
field generated by a cylindrically-wound coil.
Figure 5.9.2. The magnetic field produced by a coil is very strong oriented in the direction of the cylinder. For more information on magnetic fields produced by a coil, see http://www.ndt-ed.org/EducationResources/CommunityCollege/MagParticle/Physics/CoilField.htm
The strength and direction of the magnetic field are related to the strength and direction of the electrical current conducted through the coiled wire. Figure 5.9.3 shows a cut away of a typical speaker. The alternating magnetic field generated by the coil interacts with the constant magnetic field produced by the permanent magnet. To generate sound we will create an oscillating current through the coil, this will create an oscillating magnetic field, and will vibrate the voice coil. The diaphragm and spider hold the voice coil to the suspension allowing it to vibrate up and down. When the voice coil vibrates up and down it creates sound waves. The frequency and amplitude of the sound is directly related to the frequency and amplitude of the current passing through the coil. The resistance of the coil in a typical headphone is 32 Ω.
Figure 5.9.3. A speaker can generate sound by vibrating the voice coil using an electromagnet. For more information on speakers, see http://www.howstuffworks.com/speaker6.htm or http://www.audiocircuit.com/DIY/Dynamic-Speakers/Article:How-dynamic-loudspeakers-work
: Some speakers are heavier than others. These heavy speakers have larger permanent magnets. Why would we want a permanent magnet that can create a larger magnetic field?
Video 5.9.1. how the speaker works
Most digital music devices rely on high-speed DACs to create the analog waveforms required to produce high-quality sound. In this section, we will discuss a very simple sound generation system that illustrates this application of the DAC. The hardware consists of a DAC and a speaker interface. You can drive headphones directly from a DAC output, but to drive a regular speaker, you will need to add an audio amplifier, as illustrated in Figure 5.10.1.
Figure 5.10.1. DAC allows the software to create music. For more information on the audio amplifier, refer to the data sheet of the MC34119. http://users.ece.utexas.edu/~valvano/Datasheets/MC34119.pdf
Video 5.10.1. Sound as an analog signal: Loudness, pitch and shape
The quality of the music will depend on both hardware and software factors. The precision of the DAC, external noise, and the dynamic range of the speaker are some of the hardware factors. Software factors include the DAC output rate and the complexity of the stored sound data. If you output a sequence of numbers to the DAC that form a sine wave, then you will hear a continuous tone on the speaker, as shown in Figure 5.10.2. The loudness of the tone is determined by the amplitude of the wave. The pitch is defined as the frequency of the wave. Table 5.10.1 contains frequency values for the notes in one octave. The frequency of the wave, fsin, will be determined by the frequency of the interrupt, fint, divided by the size of the table n. The size of the table in Program 5.10.1 is n=16.
fsin = fint /n
Figure 5.10.2. The loudness and pitch are controlled by the amplitude and frequency.
The frequency of each musical note can be calculated by multiplying the previous frequency by. You can use this method to determine the frequencies of additional notes above and below the ones in Table 5.10.1. There are twelve notes in an octave, therefore moving up one octave doubles the frequency.
Note |
frequency |
C |
523 Hz |
B |
494 Hz |
Bb |
466 Hz |
A |
440 Hz |
Ab |
415 Hz |
G |
392 Hz |
Gb |
370 Hz |
F |
349 Hz |
E |
330 Hz |
Eb |
311 Hz |
D |
294 Hz |
Db |
277 Hz |
C |
262 Hz |
Table 5.10.1. Fundamental frequencies of standard musical notes. The frequency for ‘A’ is exact.
Figure 5.10.3 illustrates the concept of instrument. You can define the type of sound by the shape of the voltage versus time waveform. Brass instruments have a very large first harmonic frequency.
Figure 5.10.3. A waveform shape that generates a trumpet sound.
The tempo of the music defines the speed of the song. In 2/4 3/4 or 4/4 music, a beat is defined as a quarter note. A moderate tempo is 120 beats/min, which means a quarter note has a duration of ½ second. A sequence of notes can be separated by pauses (silences) so that each note is heard separately. The envelope of the note defines the amplitude versus time relationship. A very simple envelope is illustrated in Figure 5.10.4. The Cortex™-M processor has plenty of processing power to create these types of waves.
Figure 5.10.4. You can control the amplitude, frequency and duration of each note (not drawn to scale).
The smooth-shaped envelope, as illustrated in Figure 5.10.5, causes a less staccato and more melodic sound. This type of sound generation is possible to produce in real time on the Cortex™-M microcontroller.
Figure 5.10.5. The amplitude of a plucked string drops exponentially in time.
A chord is created by playing multiple notes simultaneously. When two piano keys are struck simultaneously both notes are created, and the sounds are mixed arithmetically. You can create the same effect by adding two waves together in software, before sending the wave to the DAC. Figure 5.10.6 plots the mathematical addition of a 262 Hz (low C) and a 392 Hz sine wave (G), creating a simple chord.
Figure 5.10.6. A simple chord mixing the notes C and G.
Example 5.10.1. Design a 3-bit R-2R DAC and use it to create a 100 Hz sine wave.
Video 5.10.2. Design of a System that produces a 100Hz Sine Wave.
Video 5.10.3. Construction of the DAC Circuit
Solution: We begin by specifying the desired input/output relationship of the 3-bit DAC. Table 5.10.2 shows the design specification. It will be able to generate 8 different current outputs with a resolution of about 12.5 μA. The circuit is presented in Figure 5.10.6.
N |
Q2 Q1 Q0 |
Iout (μA) |
0 |
0 0 0 |
0.0 |
1 |
0 0 3.3 |
12.5 |
2 |
0 3.3 0 |
25.0 |
3 |
0 3.3 3.3 |
37.5 |
4 |
3.3 0 0 |
50.0 |
5 |
3.3 0 3.3 |
62.5 |
6 |
3.3 3.3 0 |
75.0 |
7 |
3.3 3.3 3.3 |
87.5 |
Table 5.10.2. Specifications of the 3-bit R-2R DAC.
We assume the output high voltage (VOH) of the microcontroller is 3.3 V, and its output low voltage (VOL) is 0. A 3-bit R-2R DAC will use two R resistors (11kΩ) and five 2R resistors (22kΩ), as shown in Figure 5.10.7. The maximum current output will be (1/2 + 1/4 + 1/8)*3.3V/33kΩ = 87.5 μA.
Figure 5.10.7. A 3-bit R-2R DAC.
The way to analyze an R-2R DAC is to employ the Law of Superposition. We will consider the three basis elements (1, 2, and 4). If these three cases are demonstrated, then the Law of Superposition guarantees the other five will work. When one of the digital inputs is true then Vref (3.3V) is connected to the R-2R ladder, and when the digital input is false, then the connection is grounded. See Figure 5.10.8. Since 22,000 is much bigger than 32, we can neglect the 32-Ω resistance when calculating current.
Figure 5.10.8. Analysis of the three basis elements {001, 010, 100} of the 3-bit unsigned R-2R DAC.
Video 5.10.4. Analysis of a R-2R Ladder circuit
In each of the three test cases, the current across the active switch is I0=Vref / (3R). This current is divided by 2 at each branch point. I.e., I1 = I0/2, and I2 = I1/2. Current injected from the lower bits will be divided more times. Since each stage divides by two, the exponential behavior is produced. An actual DAC is implemented with a current switch rather than a voltage switch. Nevertheless, this simple circuit illustrates the operation of the R-2R ladder function. When the input is 001, Vref is presented to the left. The effective impedance to ground is 3R, so the current injected into the R-2R ladder is I0=Vref / (3R). The current is divided in half three times, and I001=Vref / (24R).
When the input is 010, Vref is presented in the middle. The effective impedance to ground is still 3R, so the current injected into the R-2R ladder is I0=Vref / (3R). The current is divided in half twice, and I010=Vref / (12R).
When the input is 100, Vref is presented on the right. The effective impedance to ground is once again 3R, so the current injected into the R-2R ladder is I0=Vref / (3R). The current is divided in half once, and I100=Vref / (6R).
Using the Law of Superposition, the output voltage is a linear combination of the three digital inputs, Iout=(4b2 + 2b1 + b0)Vref / (24R). A current to voltage circuit is used to create a voltage output. To increase the precision one simply adds more stages to the R-2R ladder.
To generate sound we need a table of data and a periodic interrupt. Program 5.10.1 shows C code that defines a 16-element 3-bit sine wave. The frequency of the sound will be the interrupt frequency divided by 16 (size of the table). So, to create a 100 Hz wave we need SysTick to interrupt at 16*100 Hz, 1600 Hz. If the bus clock is 80 MHz, then the initialization should be called with an input parameter of value 50000. The const modifier will place the data in ROM. The interrupt software will output one value to the DAC. See Figure 5.10.9. In this example, the 3-bit DAC is interfaced to output pins PB2-0. To output to this DAC we simply write to Port B. In order to create the sound, it is necessary to output just one number to the DAC upon each interrupt. The DAC range is 0 to 87.5 μA.
Figure 5.10.9. A DAC and a periodic interrupt are used to create sound. Output is discrete in time and voltage.
const uint8_t SineWave[16] = {4,5,6,7,7,7,6,5,4,3,2,1,1,1,2,3};
uint32_t Index=0; // Index varies from 0 to 15
// **************DAC_Init*********************
// Initialize 3-bit DAC
// Input: none
// Output: none
void DAC_Init(void){
IOMUX->SECCFG.PINCM[PB0INDEX] = 0x00000081; // PB2-0 outputs
IOMUX->SECCFG.PINCM[PB1INDEX] = 0x00000081;
IOMUX->SECCFG.PINCM[PB2INDEX] = 0x00000081;
GPIOB->DOE31_0 |= 0x07;
}
// **************Sound_Init*********************
// Initialize Systick periodic interrupts
// Input: interrupt period
// Units of period are 12.5ns
// Maximum is 2^24-1
// Minimum is determined by length of ISR
// Output: none
void Sound_Init(uint32_t period){
DAC_Init(); // Port B is DAC
Index = 0;
SysTick->CTRL = 0; // disable SysTick during setup
SysTick->LOAD = period-1; // reload value
SysTick->VAL = 0; // any write to current clears it
SCB->SHP[1] = SCB->SHP[1]&(~0xC0000000)|0x40000000; // set priority = 1
SysTick->CTRL = 0x0007; // enable SysTick with core clock and interrupts
}
// **************DAC_Out*********************
// output to DAC
// Input: 3-bit data, 0 to 7
// Output: none
void DAC_Out(uint32_t data){
GPIOB->DOUT31_0 = (GPIOB->DOUT31_0&(~0x07))|data;
}
// the sound frequency will be (interrupt frequency)/(size of the table)
void SysTick_Handler(void){
Index = (Index+1)&0x0F; // 4,5,6,7,7,7,6,5,4,3,2,1,1,1,2,3,...
DAC_Out(SineWave[Index]); // output one value each interrupt
}
void main(void){
__disable_irq(); // disable interrupts while initializing
Clock_Init80MHz(0);
// bus clock at 80 MHz
LaunchPad_Init(); // reset and activate Port B
Sound_Init(50000);
// initialize SysTick timer, 1.6kHz
__enable_irq(); // enable after all initialization are done
while(1){
}
}
Program 5.10.1. The periodic interrupt outputs one value to the DAC.
Video 5.10.5. Program walkthrough
Video 5.10.6. Demonstration of the Solution using a Scope
to do***
Go to Chapter 6: Local variables and LCD output
This material was created to teach ECE319K at the University of Texas at Austin
Reprinted with approval from Introduction to Embedded Systems Using the MSPM0+, ISBN: 979-8852536594
Embedded
Systems - Shape the World by Jonathan Valvano and Ramesh Yerraballi is
licensed under a Creative
Commons
Attribution-NonCommercial-NoDerivatives 4.0 International License.
Based on a work at http://users.ece.utexas.edu/~valvano/mspm0/