Jump to content
 

N9WXU

Member
  • Content Count

    66
  • Joined

  • Last visited

  • Days Won

    18

N9WXU last won the day on May 18

N9WXU had the most liked content!

Community Reputation

31 Excellent

1 Follower

About N9WXU

  • Rank
    Contributor

Recent Profile Visitors

311 profile views
  1. It has been said that software is the most complicated system humanity has created and like all complicated things we manage this complexity by breaking the problem into small problems. Each small problem is then solved and each solution is assembled to create larger and larger solutions. In other fields of work, humans created standardized solutions to common problems. For example, nails and screws are common solutions to the problem of fastening wood together. Very few carpenters worry about the details of building nails and screws, they simply use them as needed. This practice of creating common solutions to typical problems is also done in software. Software Libraries can easily be used to provide drivers, and advanced functions saving a developer many hours of effort. To make a software library useful, the library developer must create an abstraction of the problem solved by the library. This abstraction must interact with the library user in a simple way and hide the specialist details of the problem. For example, if your task is to convert RGB color values into CMYK color values, you would get a library that had a function such as this one: struct cmyk { float cyan; float magenta; float yellow; float black; }; struct rgb { float red; float green; float blue; }; struct cmyk make_CMYK_from_RGB(struct rgb); This seems very simple and it would absolutely be simple to use. But, if you had to implement such a function yourself you may quickly find your self immersed in color profiles and the behavior of the human eye. All of this complexity is hidden behind a simple function. In the embedded world we often work with hardware and we are very used to silicon vendors providing hardware abstraction layers. These hardware abstraction layers are an attempt to simplify the use use of a vendors hardware and to make it more complicated to switch the hardware to a competing system. Let us go into a little more detail. Here is a typical software layer cake as drawn by a silicon vendor. Often they will provide the bottom 3 layers and even a few demo applications. The hope is you will create your application using their libraries. The benefit for you is a significant time savings (you don't need to make your nails and screws). The benefit to the silicon vendor is getting you locked into a proprietary solution. Here is a short story about the early "dark ages" of computing before PC's had reasonable device drivers (hardware abstraction). In the early days of PC gaming all PC games run in MSDOS. This was to improve game performance be removing any software that was not specifically required. The only sound these early PC had was a simple buzzer so a large number of companies developed a spectacular number of sound cards. There were so many sound cards that PC games could not keep up adding sound card support. Each game had a setup menu where the sound card was selected along with its I/O memory, IRQ, and other esoteric parameters. We had to write the HW configuration down on a cheat sheet and each time we upgraded we had to juggle the physical configuration of our PC (with jumpers) so everything ran without conflict. Eventually, the sound blaster card became the "standard" card and all other vendors either designed their HW to be compatible or wrote their drivers to look just like the sound blaster drivers and achieve compatibility in software. Hardware abstraction has the goal of creating a Hardware interface definition that allows the hardware to present the needed capabilities to the running application. The hardware can have many additional features and capabilities but these are not important to the application so they are not part of the interface. So abstraction provides a simplification by hiding the stuff the application does not care about. The simplification comes from focusing just on the features the application does care about. So if the silicon vendors are providing these abstractions, life can be only good!... We should look a little more closely. Silicon is pretty cheap to make but expensive to design. So each micro controller has a large number of features on each peripheral in the hopes that it will find a home in a large number of applications. Many of these features are mutually exclusive such as synchronous vs asynchronous mode in the EUSART on a PIC micro controller. These features are all well documented in the data sheets but at some point it was decided across the entire industry that if there were functions tied to each feature they would be easier to use. Here is an example from MCC's MSSP driver in SPI mode: void SPI2_ClearWriteCollisionStatus(void) { SSP2CON1bits.WCOL = 0; } Now it may be useful to have a more readable name for the WCOL flag and perhaps ClearWriteCollisionStatus does make the code easier to use. The argument is that making this function call will be more intuitive than clearing the WCOL bit. As you go through many of the HAL layers you find hundreds of examples of very simple functions setting or clearing a few bits. In a few cases you will find an example where all the functions are working together to create a specific abstraction. Most cases, you simply find the HW flags hidden behind more verbosely named functions. Simply renaming the bits is NOT a hardware abstraction. In fact, if the C compiler does not automatically inline these functions they are simply creating overhead. Sadly there is another problem in this mess. The data sheets are very precisely written documents that accurately describe the features of the hardware. Typically these datasheets are written with references to registers and bits. If the vendor provides a comprehensive function interface to the hardware, the data sheet will need to be recreated with function calls and function use examples rather than the bits and registers. In my opinion the term HAL (Hardware Abstraction Layer) has been hijacked to represent a function call interface to all the peripheral functions. What about Board Support Package (BSP)? Generally the BSP is inserted in the layer cake to provide a place for all the code that enables the vendor demo code to run on the "HAL". Arguably, the BSP is what the purist would call the HAL. Enough of the ranting....How does this topic affect you the hapless developer who is likely using vendor code. Silicon Vendors will continue to provide HAL's to interface the hardware, Middleware to provide customers with high function libraries and board support packages to link everything to their proprietary demo boards. As customers, we can evaluate their offering on their systems but we can expect to write our own BSP's to get the rest of their software running on our final product hardware. Software Vendors will continue to provide advanced libraries, RTOS's and other forms of middleware for us to buy to speed our development. The ability to adapt this software to our systems largely depends upon how well the software vendor defines the expected interfaces that we need to provide. Sometimes these vendors can be contracted to get their software running on our hardware and and get us going. FW engineers will continue to spend a significant part of the the project nudging all these pieces into one cohesive system so we can layer our secret sauce on top. One parting comment. Software layers are invented to allow large systems to take advantage of the single responsibility principle. This is great, but if you use too many layers you end up with a new problem called Lasagna code. If you use too few layers you end up with Spaghetti code. One day I would love to know why Italian food is used to name two of the big software smells. Good Luck
  2. Here I am on a clear cool evening, by the fire outside with my laptop. Tonight I will talk about a new peripheral, the timer/counter. Periodically I will be interrupted to put another log on my fire, but it should not slow me down too much. Timers and counters are almost the same peripheral with only the difference of what is causing the counter to count. If the counter is incrementing from a clock source, it becomes a timer because each count registers the passage of a precise unit of time. If the counter is incrementing from an unknown signal (perhaps not even a regular signal), it is simply a counter. Clearly, the difference between these is a matter of the use-case and not a matter of design. Through there are some technical details related to clocking any peripheral from an external "unknown" clock that is not synchronized with the clocks inside the microcontroller. We will happy ignore those details because the designers have done a good job of sorting them out. Let us take a peek at a very simple timer on the PIC16F18446. Turn to page 348 of your PIC16F18446 data sheet and take a look at figure 25-1. (shown below) This is the basic anatomy of a pretty vanilla timer. Of course most timers have many more features so this article is simply an introduction. On the left side of this image there are a number of clock sources entering a symbol that represents a multiplexer. A multiplexer is simply a device that can select one input and pass it to its output. The T0CS<2:0> signal below the multiplexer is shorthand for a 3-bit signal named T0CS. The slash 3 also indicates that that is a 3-bit signal. Each of the possible 3-bit codes is inside the multiplexer next to one of the inputs. This indicates the input you will select if you apply that code on the signal T0CS. Pretty simple. The inputs are from top to bottom (ignoring the reserved ones) SOSC (Secondary Oscillator), LFINTOSC (Low Frequency Internal Oscillator), HFINTOSC (High Frequency Internal Oscillator) Fosc/4 (The CPU Instruction Clock) an input pin (T0CKI) inverted or not-inverted. Let us cover each section of this peripheral in a little more detail. Of course you should go to the data sheet to read all the information. SOSC The secondary oscillator is a second crystal oscillator on 2 I/O pins. A crystal oscillator is a type of clock that uses a piece of quartz crystal to produce a very accurate frequency. The secondary oscillator is designed to operate at 32.768kHz which by some coincidence is 2^15 counts per second. This makes keeping accurate track of seconds very easy and very low power. You could configure the hardware to wake up the CPU every second and spend most of your time in a low power sleep mode. LFINTOSC and HFINTOSC There are two internal oscillator in the PIC16F18446. The LFINTOSC is approximately 31kHz and is intended for low power low speed operation but not very accurate timing. The HFINTOSC is adjustable from 1-32MHz and is better than 5% accurate so it is often sufficient for most applications. Because these two oscillators are directly available to the timer, the CPU can be operating at a completely different frequency allowing high resolution timing of some events, while running the CPU at a completely different frequency. FOSC/4 This option is the simplest option to select because most of the math you are doing for other peripherals is already at this frequency. If you are porting software for a previous PIC16 MCU, the timer may already be assumed to be at this frequency. Due to historical reasons, a PIC16 is often clocked at 4MHz. This makes the instruction clock 1MHz and each timer tick is 1us. Having a 1us tick makes many timing calculations trivial. If you were clocking at 32MHz, each tick would be 31ns which is much smaller but does not divide as nicely into base 10 values. T0CKI This option allows your system to measure time based upon an external clock. You might connect the timing wheel of an engine to this input pin and compute the RPM with a separate timer. Prescaler After the input multiplexer, there is an input pre-scaler. The goal of the pre-scaler is to reduce the input clock frequency to a slower frequency that may be more suitable for the application. The most prescallers are implemented as a chain of 'T' flip-flops. A T flip-flop simply changes its output (high to low or low to high) on each rising edge of an input signal. That makes a T Flip-Flop a divide by 2 circuit for a clock. If you have a chain of these and you use a multiplexer to decide which T flip flop to listen to, you get a very simple divider that can divide by some power of 2. i.e. 2, 4, 8, 16... with each frequency 1/2 of the previous one. Synchronizer The synchronizer ensures that input pulses that are NOT sourced by an oscillator related to FOSC are synchronized to FOSC. This synchronization ensures reliable pulses for counting or for any peripherals that are attached to the counter. However, synchronization requires the FOSC/4 clock source to be operating and that condition is not true in when the CPU is saving power in sleep. If you are building an alarm clock that must run on a tiny battery, you will want the timer to operate while the CPU is sleeping and to produce a wakeup interrupt at appropriate intervals. To do this, you disable synchronization. Once the CPU has been awakened, it is a good idea to activate synchronization or to avoid interacting with the counter while it is running. TMR0 Body The TMR0 body used to be a simple counter, but in more recent years it has gained 2 options. Either, the timer can be a 16-bit counter, or it can be an 8-bit counter with an 8-bit compare. The 8-bit compare allows the timer to be reset to zero on any 8-bit value. The 16-bit counter allows it to count for a longer period of time before an overflow. The output from the TMR0 body depends upon the module. In the 8-bit compare mode, the output will be set each time there is a compare match. In the 16-bit mode, the output will be set each time the counter rolls from 0xFFFF to 0x0000. Output The output from the core can be directed to other peripherals such as the CLC's, it can also be sent through a postscaler for further division and then create an interrupt or toggle an output on an I/O pin. The postscaler is different than the prescaler because it is not limited to powers of two. It is a counting divider and it can divide by any value between 1 and 16. We shall use that feature in the example. Using the Timer Timers can be used for a great number of things but one common thing is to produce a precise timing interval that does not depend upon your code. For instance, 2 lessons ago, we generated a PWM signal. The one way to do this was to set and clear the GPIO pin every so many instruction cycles. Unfortunately, as we added code to execute the PWM would get slower and slower. Additionally, it could get less reliable because the program could take different paths through the code. Using the PWM peripheral was the perfect solution, but another solution would be to use a timer. For instance, you could configure the timer to set the output after an interval. After that interval had elapsed, you could set a different interval to clear the output. By switching back and forth between the set interval and the clear interval, you would get a PWM output. Still more work than the PWM peripheral, but MUCH better than the pure software approach. For this exercise we will use the timer to force our main loop to execute at a fixed time interval. We will instrument this loop and show that even as we add work to the loop, it still executes at the same time interval. This type of structure is called an exec loop and it is often used in embedded programming because it ensures that all the timing operations can be simple software counting in multiples of the loop period. And here is the program. void main(void) { TRISAbits.TRISA2 = 0; // Configure the TRISA2 as an output (the LED) T0CON1bits.ASYNC = 0; // Make sure the timer is synchronized T0CON1bits.CKPS = 5; // Configure the prescaler to divide by 32 T0CON1bits.CS = 2; // use the FOSC/4 clock for the input // the TMR0 clock should now be 250kHz TMR0H = 250; // Set the counter to reset to 0 when it reaches 250 (1ms) TMR0L = 0; // Clear the counter T0CON0bits.T0OUTPS = 9; // Configure the postscaler to divide by 10 T0CON0bits.T0EN = 1; // turn the timer on // the timer output should be every 10ms while(1) { LATAbits.LATA2 = 0; // Turn on the LED... this allows us to measure CPU time __delay_ms(5); // do some work... could be anything. LATAbits.LATA2 = 1; // Turn off the LED... Any extra time will be while the LED is off. while(! PIR0bits.TMR0IF ); // burn off the unused CPU time. This time remaining could be used as a CPU load indicator. PIR0bits.TMR0IF = 0; // clear the overflow flag so we can detect the next interval. } } I chose to use a delay macro to represent doing useful work. In a "real" application, this area would be filled with all the various functions that need to be executed every 10 milliseconds. If you needed something run every 20 milliseconds you would execute that function every other time. In this way, many different rates can be easily accommodated so long as the total execution time does not exceed 10 milliseconds because that will stretch a executive cycle into the next interval and break the regular timing. Special Considerations One interesting effect in timers is they are often the first example of "concurrency issues" that many programmers encounter. Concurrency issues arise when two different systems access the same resource at the same time. Quite often you get unexpected results which can be seen as "random" behavior. In the code above I configured the timer in 8-bit mode and took advantage of the hardware compare feature so I never needed to look at the timer counter again. But let us imagine a slightly different scenario. Imagine that we needed to measure the lap time of a race car. When the car started the race we would start the timer. As the car crossed the start line, we would read the timer BUT WE WOULD NOT STOP IT. When the car finished the last lap, we could stop the timer and see the total time. IN this way we would have a record for every lap in the race. Simply by subtracting the time of completion for each lap, we would have each lap interval which would be valuable information for the race driver. Each time we read the timer without stopping it, we have an opportunity for a concurrency issue. For an 8-bit timer we can read the entire value with one instruction and there are no issues. However, the race is likely to last longer than we can count on 8-bits so we need a bigger timer. With a 16-bit timer we must perform 2 reads to get the entire value and now we encounter our problem. In the picture above I have shown two scenarios where TMR0 in 16-bit mode is counting 1 count per instruction cycle. This is done to demonstrate the problem. Slowing down the counting rate does not really solve the problem but it can affect the frequency of the issue. In this example the blue cell indicates the first read while the red cell indicates the second read to get all 16-bits. When the counter was 251, the reads are successful, however when the counter is 255, the actual value we will read will be 511 which is about 2x the actual value. If we reverse the read order we have the same problem. One solution is to read the high, then the low and finally, read the high a second time. With these three data points and some math, it is possible to reconstruct the exact value at the time of the first read. Another solution is in hardware. In the data sheet we see that there is some additional circuitry surrounding TMR0H. With this circuitry, the TMR0 will automatically read TMR0H from the counter into a holding register when TMR0L is read. So if you read TMR0L first and then TMR0H you will NEVER have the issue. Now consider the following line of C. timerValue = TMR0 It is not clear from just this line of code which byte of TMR0 is read first. If it is the low byte this line is finished and perfect. However, if it is the high byte, then we still have a problem. One way to be perfectly clear in the code is the following: timerValue = TMR0L; timerValue |= TMR0H << 8; This code is guaranteed to read the registers in the correct order and should be no less efficient. The left shift by 8 will probably not happen explicitly because the compiler is smart enough to simply read the value of TMR0H and store it in the high byte of timerValue. These concurrency issues can appear in many areas of computer programming. If your program is using interrupts then it is possible to see variables partially updated when an interrupt occurs causing the same concurrency issues. Some bigger computers use real-time operating systems to provide multi-tasking. Sharing variables between the tasks is another opportunity for concurrency issues. There are many solutions, for now just be aware that these exist and they will affect your future code. Timer 0 is probably the easiest timer on a PICmicrocontroller. It has always been very basic and its simplicity makes it the best timer to play with as you learn how they work. Once you feel you have mastered timer 0, spend some time with timer 1 and see what additional features it has. Once again, the project is in the attached files. Good Luck. exercise_6.zip
  3. That is covered!!! As explained in the article and demonstrated in the 5'th listing, printf is setup by the putch function. Of course I did not do something as mundane as Hello, world!, but I did produce floating point values for the graph.
  4. Remember, this timer counts up.and you get the interrupt when it rolls over. To interrupt at a precise interval, you must compute the number of "counts" required for that interval and then subtract from 65535 to determine the timer load value. void setTimer(unsigned int intervalCounts) { TMR1ON = 0; TMR1 = 65535 - intervalCounts; TMR1ON = 1; } By turning the timer off and then setting the counts and restoring the timer, you can be sure that you will not get unexpected behavior if the timer value is written in an unexpected order. I will cover this topic in the next blog post on timers.
  5. It has been a busy few weeks but finally I can sit down and write another installment to this series on embedded programming. Today's project will be another peripheral and a visualization tool for our development machines. We are going to learn about the UART. Then we are going to write enough code to attach the UART to a basic C printing library (printf) and finally we are going to use a program called processing to visualize data sent over the UART. This should be a fun adventure so let us get started. UART (USART?) The word UART is an acronym that stands for Universal Asynchronous Receiver Transmitter. Some UART's have an additional feature so they are Universal Synchronous Asynchronous Receiver Transmitter or USART. We are not going to bother with the synchronous part so let us focus on the asynchronous part. The purpose of the UART is to send and receive data between our micro controller and some other device. Most frequently the UART is used to provide some form of user interface or simply debugging information live on a console. The UART signaling has special features that allow it to send data at an agreed upon rate (the baud rate) and an agreed upon data format (most typically 8 data bits, no parity and 1 stop bit). I am not going to go into detail on the UART signaling but you can easily learn about it by looking in chapter 36 of the PIC16F18446 datasheet. Specifically look at figure 36-3 if you are interested. UARTs are a device that transfer bytes of data (8-bits in a byte) one bit at a time across a single wire (one wire for transmit and one for receive) and a matching UART puts the bits back into a byte. Each bit is present on the wire for a brief but adjustable amount of time. Sending the data slowly is suitable for long noisy wires while sending the data fast is good for impatient engineers. A common baud rate for user interfaces is 9600 baud which is sends 1 letter every millisecond (0.001 seconds). Many years ago, before the internet, I had a 300 baud modem for communicating with other computers. When I read my e-mail the letters would arrive slightly slower than I could read. Later, after the internet was invented and we used modems to dial into the internet, I had a 56,600 baud modem so I could view web-pages and pictures more quickly. Today I use UARTS to send text data to my laptop and with the help of the processing program we can make a nice data viewer. Enough history... let us figure out the UART. Configuring the UART The UART is a peripheral with many options so we need to configure the UART for transmitting and receiving our data correctly. To setup the UART we shall turn to page 571 of our PIC16F18446 data sheet. The section labeled 36.1.1.7 describes the settings required to transmit asynchronous data. And now for some code. Here is the outline of our program. void init_uart(void) { } int getch(void) { } void putch(char c) { } void main(void) { init_uart(); while(1) { putch(getch()+1); } } This is a common pattern for designing a program. I start by writing my "main" function simply using simple functions to do the tricky bits. That way I can think about the problem and decide how each function needs to work. What sort of parameters each function will require and what sort of values each function will return. This program will be very simple. After initializing the UART to do its work, the program will read values from the UART, add 1 to each value and send it back out the UART. Since every letter in a computer is a simple value it will return the next letter in each series that I type. If I type 'A' it will return 'B' and so on. I like this test program for UART testing because it will demonstrate the UART is working and not simply shorting the receive pin to the transmit pin. Now that we have an idea for each function. Let us write them one a time. init_uart() This function will simply write all the needed values into the special function registers to activate the UART. To do that, we shall follow the checklist in the data sheet. We need to configure both the transmit and the receive function. Step 1 is to configure the baud rate. We shall use the BRG16 because that mode has the simplest baud rate formula. Of course, it is even easier when you use the supplied tables. I always figure that if you are not cheating, you are not trying so lets just find the 32MHz column in the SYNC=0, BRGH=0, BRG16 = 1 table. I like 115.2k baud because that makes the characters send much faster. So the value we need for the SPBRG is 16. Step 2 is to configure the pins. We shall configure TX and RX. To do that, we need the schematics. The key portion of the schematics is this bit. The confusing part is, the details of the TX and RX pin. If I had a $ for every time I got these backwards, I would have retired already. Fortunately the team at Microchip who drew these schematics were very careful to label the CDC TX connecting to the UART RX and the CDC RX connecting to the UART TX. They also labeled the UART TX as RB4 and UART RX as RB6. This looks pretty simple. Now we need to steer these signals to the correct pins via the PPS peripheral. NOTE: The PPS stands for Peripheral Pin Select. This feature allows the peripherals to be steered to almost any I/O pin on the device. This makes laying out your printed circuit boards MUCH easier as you can move the signals around to make all the connections straight through. You can also steer signals to more than one output pin enabling debugging by steering signals to your LED's or a test point. After steering the functions to the correct pins, it is time to clear the ANSEL for RB6 (making the pin digital) and clear TRIS for RB4 (making the pin an output). The rest of the initialization steps involve putting the UART in the correct mode. Let us see the code. void init_uart(void) { // STEP 1 - Set the Baud Rate BAUD1CONbits.BRG16 = 1; TX1STAbits.BRGH = 0; SPBRG = 16; // STEP 2 - Configure the PPS // PPS unlock sequence // this should be in assembly because the hardware counts the instruction cycles // and will not unlock unless it is exactly right. The C language cannot promise to // make the instruction cycles exactly what is below. asm("banksel PPSLOCK"); asm("movlw 0x55"); asm("movwf PPSLOCK"); asm("movlw 0xAA"); asm("movwf PPSLOCK"); asm("bcf PPSLOCK,0"); RX1PPSbits.PORT = 1; RX1PPSbits.PIN = 6; RB4PPS = 0x0F; // Step 2.5 - Configure the pin direction and digital mode ANSELBbits.ANSB6 = 0; TRISBbits.TRISB4 = 0; // Step 3 TX1STAbits.SYNC = 0; RC1STAbits.SPEN = 1; // Step 4 TX1STAbits.TX9 = 0; // Step 5 - No needed // Step 1 from RX section (36.1.2.1) RC1STAbits.CREN = 1; // Step 6 TX1STAbits.TXEN = 1; // Step 7 - 9 are not needed because we are not using interrupts // Step 10 is in the putchar step. } To send a character we simply need to wait for room in the transmitter and then add another character. void putch(char c) { while(!TX1IF); // sit here until there is room to send a byte TX1REG = c; } And to receive a character we can just wait until a character is present and retrieve it. int getch(void) { while(!RC1IF); // sit here until there is a byte in the receiver return RC1REG; } And that completes our program. Here is the test run. Every time I typed a letter or number, the following character is echoed. Typing a few characters is interesting and all but I did promise graphing. We shall make a simple program that will stream a series of numbers one number per row. Then we will introduce processing to convert this stream into a graph. The easiest way to produce a stream of numbers is to use printf. This function will produce a formatted line of text and insert the value of variables where you want them. The syntax is as follows: value = 5; printf("This is a message with a value %d\n",value); To get access to printf you must do two things. 1) You must include the correct header file. So add the following line near the top of your program. (by the xc.h line) #include <stdio.h> 2) You must provide a function named putch that will output a character. We just did that. So here is our printing program. #include <stdio.h> #include <math.h> /// Lots of stuff we already did void main(void) { double theta = 0; init_uart(); while(1) { printf("%f\r\n",sin(theta)); theta += 0.01; if(theta >= 2 * 3.1416) theta = 0; } } Pictures or it didn't happen. And now for processing. Go to http://www.processing.org and download a suitable version for your PC. Attached is a processing sketch that will graph whatever appears in the serial port from the PIC16. And here is a picture of it working. I hope you found this exercise interesting. As always post your questions & comments below. Quite a lot was covered in this session and your questions will guide our future work. Good Luck exercise_5a.zip exercise_5.zip graph_data.pde
  6. Today I am going to introduce a peripheral. So far we have interacted with the I/O pins on the micro controller. This can get you pretty far and if you can manipulate the pins quickly enough you can produce nearly any signal. However, there are times when the signal must be generated continuously and your MCU must perform other tasks. Today we will be generating a PWM signal. PWM stands for Pulse Width Modulation. The purpose of PWM is to produce a fixed frequency square wave but the width of the high and low pulses can be adjusted. By adjusting the percentage of the time the signal is high versus low, you can change the amount of energy in a system. In a motor control application, changing the energy will change the speed. In an LED application, changing the energy will change the brightness. We will use the PWM to change the brightness of the LED on our demo board. Here is a sample of a PWM signal. The PWM frequency is 467.3hz and the duty cycle is 35%. One way to produce such a signal is to execute a bit of code such as this one. do_pwm macro banksel pwm_counter movlw pwm_reload decfsz pwm_counter,f movf pwm_counter,w movwf pwm_counter banksel pwm subwf pwm,w banksel LATA movf LATA,w andlw 0xFB btfss STATUS,C iorlw 0x04 movwf LATA endm This code does not help you if you are writing in C but it will serve to show the challenges of this technique. This is a simple macro that must be executed numerous times to produce the entire PWM cycle. The PWM frequency is determined by how often this macro can be called. So one option is quite simply this: loop do_pwm goto loop But if you do this the MCU will not be able to perform any other functions. loop do_pwm banksel reload incfsz reload goto loop banksel pwm incf pwm,f goto loop Here is one that updates the PWM duty cycle every time the reload variable overflows. This is not a lot of additional work but the MCU is still 100% consumed toggling a pin and deciding when to update the duty cycle. Surely there is a better way. Enter a Peripheral MCU Peripherals are special hardware functions that can be controlled by the software but do some tasks automatically. One common peripheral is the PWM peripheral. Different MCU have different peripherals that can produce a PWM signal. For instance, older PICmicrocontrollers have the CCP (Capture, Compare, PWM) peripheral and newer PICmicrocontrollers have dedicated PWM's. To demonstrate the use of this simple peripheral I will switch to C and give some code. The good side to using a peripheral is the work it takes on. The down side is the additional effort required to setup the additional hardware. Microchip provides a tool call MCC to do this work but in this example, we will do the work ourselves. Fortunately, Microchip provided a section in the data sheet that provides an 8 step checklist for configuring the PWM. Time for doing the steps // Steps from Chapter 30.9 of the datasheet // Step 1 TRISAbits.TRISA2 = 1; // Step 2 PWM6CON = 0; // Step 3 T2PR = 0xFF; // Step 4 PWM6DC = 358 << 6; // Step 5 T2CLKCONbits.CS = 1; T2CONbits.CKPS = 0; T2CONbits.ON = 1; // Step 6 // Step 7 TRISAbits.TRISA2 = 0; RA2PPS = 0x0D; // Step 8 PWM6CONbits.EN = 1; And here is 35% just like before, except last time the period of the entire wave was 2.14ms and now it is 0.254ms. That is about 10x faster than before. This time the main loop is doing absolutely nothing making it possible to do all kinds of neat things like implement a flicker to make the LED look like a candle. while(1) { __delay_ms(5); PWM6DC = rand() % 1024 << 6; } So here is a candle. Through honestly it is not the best looking candle. Perhaps you can do a better job. Peripherals can be a huge time saver and can certainly provide more CPU in your application for the real "secret sauce". Most of the data sheet covers peripherals so we will go through a few of them in the next few weeks. Good Luck Step_4.zip step_4_asm.zip
  7. I totally cheat. https://cdecl.org Of course the right to left helps when you don't have the internet.
  8. It is now time for my favorite introductory lab. This lab is my favorite because it.... nope I will not spoil the surprise! Today we will do two labs and we will learn about digital inputs, and pushbuttons. First the digital input. The PIC16F18446 has 20 pins spread across 3 ports. PORTA, PORTB and PORTC. Last time we did not learn about the digital I/O pins but we did learn how to drive a pin as an output. It turns out that making a pin an output is pretty simple... Just clear the appropriate TRIS bit to zero and that pin is an output. It turns out that all the pins are inputs by default when the chip starts up. Additionally, many of the pins have an alternate analog function and the pin is configured as an analog input by default in order to keep the port in the lowest power state that cannot conflict with outside circuitry. To get a digital input, we need to leave the digital input function enabled (TRIS is 1) and turn the analog function off (ANSEL is 0). If we leave the analog function enabled, the digital function will always read a zero. Here is the port diagram from the data sheet. You can see in the diagram that if the ANSELx signal is set to a 1 the leading to the data bus AND gate will always output a 0. (Remember, both inputs to an AND gate must be 1 for the output to be 1 and the little circle on the ANSELx signal inverts 1's to 0's). From this diagram we learn that ANSELx must be 0 and TRISx must be 1 or the output buffer connected to the TRISx signal will drive the pin. It is time for some code. We will make a simple program that lights the LED when the button on our Curiosity is pressed and when the button is released, the LED will turn off. A quick peak at the schematic shows the button is on PORTC bit 2. The Program Please: void main(void) { TRISAbits.TRISA2 = 0; ANSELCbits.ANSC2 = 0; WPUCbits.WPUC2 = 1; while(1) { if(PORTCbits.RC2) { LATAbits.LATA2 = 1; } else { LATAbits.LATA2 = 0; } } } A quick surprise, the WPUC register is the weak pull up control register for PORTC. This is not shown in the generic diagram but the explanation is simple. The pushbutton will connect the RC2 pin to ground which will create a 0 on the input. The weak pull up is required to make a 1 when the button is NOT pressed. Setting the WPUC2 bit will enable the weak pull up for RC2. Programming.... Testing... Viola! it works, But this is pretty boring after all, we could replace the Curiosity with a wire and save some money. It is time to make that computer earn its place in the design. We are going to make a small change to the program so it will toggle the LED each time the button is pressed. void main(void) { TRISAbits.TRISA2 = 0; ANSELCbits.ANSC2 = 0; WPUCbits.WPUC2 = 1; while(1) { if(PORTCbits.RC2) { if(LATAbits.LATA2 == 1) LATAbits.LATA2 = 0; else LATAbits.LATA2 = 1; } } } Well that is strange.... it seems like it works when I press the button. AH HA, RC2 is a 1 when the button is NOT pressed.... stand by... void main(void) { TRISAbits.TRISA2 = 0; ANSELCbits.ANSC2 = 0; WPUCbits.WPUC2 = 1; while(1) { if(PORTCbits.RC2==0) { if(LATAbits.LATA2 == 1) LATAbits.LATA2 = 0; else LATAbits.LATA2 = 1; } } } Well darn, it almost works again. It seems like it is dim when I hold the button and when I release it, the LED is randomly on or off. AH HA!, It is toggling the LED as long as I hold the pin. We need to add a variable and only trigger the LED change when the pin changes state. __bit oldRC2 = 0; void main(void) { TRISAbits.TRISA2 = 0; ANSELCbits.ANSC2 = 0; WPUCbits.WPUC2 = 1; while(1) { if(PORTCbits.RC2==0 && oldRC2 == 1) { if(LATAbits.LATA2 == 1) LATAbits.LATA2 = 0; else LATAbits.LATA2 = 1; } oldRC2 = PORTCbits.RC2; } } I added a variable called oldRC2. I used the compiler built-in type __bit to represent a single bit of data (matching the data size of a single pin) and the LED is triggered when the button is pressed (RC2 == 0) AND the button was previously NOT pressed (oldRC2 == 1). The value of oldRC2 is set to be RC2 after the testing. Well Heck... It is getting closer but something is still not quite right. I press the button and the LED changes state... usually... sometimes... I see the problem. The pin RC2 is sampled twice. Once at the beginning where it is compared to the oldRC2 and once a bit later to make a new copy for oldRC2. What if the value on RC2 changed between these two samplings. That would mean that we could miss an edge. The solution is simple. __bit oldRC2 = 0; __bit newRC2 = 0; void main(void) { TRISAbits.TRISA2 = 0; ANSELCbits.ANSC2 = 0; WPUCbits.WPUC2 = 1; while(1) { newRC2 = PORTCbits.RC2; if(newRC2==0 && oldRC2 == 1) { if(LATAbits.LATA2 == 1) LATAbits.LATA2 = 0; else LATAbits.LATA2 = 1; } oldRC2 = newRC2; } } We will just create a new variable and sample RC2 once into the variable newRC2. Then we will use that for the comparison and the assignment. Ah, this seems to be pretty good. Time for some more extensive testing... This is strange. If I press this many times, every so often it looks like a button press was skipped. Let us put the logic analyzer on this problem. Ok, the top trace is RC2(the button) and the bottom trace is RA2 (the LED). Every falling edge of RC2 the LED changes state (on to off, or off to on). But look right in the middle. It appears that the LED changed state on the rising edge as well as the falling edge. Perhaps we should look at that spot more closely. Look at that. An extra transition on the button. It turns out that buttons are made of two pieces of metal that are pressed together. Sometimes the metal will bounce and break the connection. If we measure this bounce we find that it is nearly 20useconds wide ( 0.000020 seconds). That is pretty fast but the PIC16F18446 detected the bounce and changed the LED state. If you research button bouncing you will find that this phenomenon is on ALL buttons, but some buttons are much worse than others. This button is actually pretty good and it took a large number of tries before I was caught by it. This button is very good. So I will do the simplest button debouncer I have ever done. __bit oldRC2 = 0; __bit newRC2 = 0; void main(void) { TRISAbits.TRISA2 = 0; ANSELCbits.ANSC2 = 0; WPUCbits.WPUC2 = 1; while(1) { newRC2 = PORTCbits.RC2 && PORTCbits.RC2; if(newRC2==0 && oldRC2 == 1) { if(LATAbits.LATA2 == 1) LATAbits.LATA2 = 0; else LATAbits.LATA2 = 1; } oldRC2 = newRC2; } } I will simply read PORTC twice and let it read a one if it reads high on BOTH reads. Note the AND function between the two reads of RC2. That is all for today. Your homework is to do some research on different ways to solve the debounce problem. Next week we will introduce our first peripheral.
  9. Assembly language may no longer be the mainstream way to write code for embedded systems, however it is the best way to learn how a specific CPU works without actually building one. Assembly language is simply the raw instruction set of a specific CPU broken into easy to remember pneumonics with a very basic syntax. This enables you full control of everything the CPU does without any translation provided by a compiler. Sometimes this is the only reasonable way to do something that cannot be represented by a higher level language. Here is an example from a project I was working on today. Today I wanted to create a 128-bit integer (16 bytes). That means I will need to add, subtract, multiply, etc. on my new 128-bit datatype. I was writing for a 32-bit CPU so this would require 4 32-bit values concatenated together to form the 128-bit value. If we consider the trivial problem of adding two of these numbers together, lets consider the following imaginary code. int128_t foo = 432123421234; int128_t bar = 9873827438282; int128_t sum = foo + bar; But my 32-bit CPU does not understand int128_t so I must fake it. How about this idea. int32_t foo[] = {0x00112233, 0x44556677, 0x8899AABB, 0xCCDDEEFF}; int32_t bar[] = {0xFFEEDDCC, 0xBBAA9988, 0x77665544, 0x33221100}; int32_t sum[4]; sum[0] = foo[0] + bar[0]; sum[1] = foo[1] + bar[1]; sum[2] = foo[2] + bar[2]; sum[3] = foo[3] + bar[3]; But back in grade school I learned about the 10's place and how I needed to carry a 1 when the sum of the one's place exceeded 10. It seems that it is possible that FOO[0] + BAR[0] could exceed the maximum value that can be stored in an int32_t so there will be a carry from that add. How do I add carry into the next digit? In C I would need to rely upon some math tricks to determine if there was a carry. But the hardware already has a carry flag and there are instructions to use it. We could easily incorporate some assembly language and do this function in the most efficient way possible. So enough rambling. Let us see some code. First, we need to configure MPLAB to create an ASM project. Create a project in the normal way, but when you get to select a compiler you will select MPASM. Now you are ready to get the basic source file up and running. Here is a template to cut/paste. #include "p16f18446.inc" ; CONFIG1 ; __config 0xFFFF __CONFIG _CONFIG1, _FEXTOSC_ECH & _RSTOSC_EXT1X & _CLKOUTEN_OFF & _CSWEN_ON & _FCMEN_ON ; CONFIG2 ; __config 0xFFFF __CONFIG _CONFIG2, _MCLRE_ON & _PWRTS_OFF & _LPBOREN_OFF & _BOREN_ON & _BORV_LO & _ZCD_OFF & _PPS1WAY_ON & _STVREN_ON ; CONFIG3 ; __config 0xFF9F __CONFIG _CONFIG3, _WDTCPS_WDTCPS_31 & _WDTE_OFF & _WDTCWS_WDTCWS_7 & _WDTCCS_SC ; CONFIG4 ; __config 0xFFFF __CONFIG _CONFIG4, _BBSIZE_BB512 & _BBEN_OFF & _SAFEN_OFF & _WRTAPP_OFF & _WRTB_OFF & _WRTC_OFF & _WRTD_OFF & _WRTSAF_OFF & _LVP_ON ; CONFIG5 ; __config 0xFFFF __CONFIG _CONFIG5, _CP_OFF ; GPR_VAR UDATA Variable RES 1 SHR_VAR UDATA_SHR Variable2 RES 1 ;******************************************************************************* ; Reset Vector ;******************************************************************************* RES_VECT CODE 0x0000 ; processor reset vector pagesel START ; the location of START could go beyond 2k GOTO START ; go to beginning of program ISR CODE 0x0004 ; interrupt vector location ; add Interrupt code here RETFIE ;******************************************************************************* ; MAIN PROGRAM ;******************************************************************************* MAIN_PROG CODE ; let linker place main program START ; initialize the CPU LOOP ; do the work GOTO LOOP END The first thing you will notice is the formatting is very different than C. In assembly language programs the first column in your file is for a label, the second column is for instructions and the third column is for the parameters for the instructions. In this code RES_VECT, ISR, MAIN_PROG, START and LOOP are all labels. In fact, Variable and Variable2 are also simply labels. The keyword CODE tells the compiler to place code at the address following the keyword. So the RES_VECT (reset vector) is at address zero. We informed the compiler to place the instructions pagesel and GOTO at address 0. Now when the CPU comes out of reset it will be at the reset vector (address 0) and start executing these instructions. Pagesel is a macro that creates a MOVLP instruction with the bits <15:11> of the address of START. Goto is a CPU instruction for an unconditional branch that will direct the program to the address provided. The original PIC16 had 35 instructions plus another 50 or so special keywords for the assembler. The PIC16F1xxx family (like the PIC16F18446) raises that number to about 49 instructions. You can find the instructions in the instruction set portion of the data sheet documented like this: The documentation shows the syntax, the valid range of each operand, the status bits that are affected and the work performed by the instruction. In order to make full use of this information, you need one more piece of information. That is the Programmers Model. Even C has a programmers model but it does not always match the underlying CPU. In ASM programming the programmers model is even more critical. You can also find this information in the data sheet. In the case of the PIC16F18446 it can be found in chapter 7 labeled Memory Organization. This chapter is required reading for any aspiring ASM programmers. Before I wrap up we shall modify the program template above to have a real program. START banksel TRISA clrf TRISA banksel LATA loop bsf LATA,2 nop bcf LATA,2 GOTO loop ; loop forever END This program changes to the memory bank that contains TRISA and clears TRISA making all of PORT A an output. Next is changes to the memory bank that contains the LATCH register for PORT A and enters the loop. BSF is the pneumonic for Bit Set File and it allows us to set bit 2 of the LATA register. NOP is for No OPeration and just lets the bit set settle. BCF is for Bit Clear File and allows us to clear bit 2 and finally we have a branch to loop to do this all over again. Because this is in assembly we can easily count up the instruction cycles for each instruction and determine how fast this will run. Here is the neat thing about PIC's. EVERY instruction that does not branch takes 1 instruction cycle (4 clock cycles) to execute. So this loop is 5 cycles long. We can easily add instructions if we need to produce EXACTLY a specific waveform. I hope this has provided some basic getting started information for assembly language programming. It can be rewarding and will definitely provide a deeper understanding on how these machines work. Good Luck
  10. Standby for a introduction to embedded... the ASM version. But suffice to say, the PIC assembler (MPASM) does understand #define for creating macro's just like in C. Setting breakpoints works the same way as in C but it is a bit more clear because you can set the breakpoint on exactly the instruction you want to stop at. The biggest hassle with ASM programming is the lack of any kind of support anything not supported by the ISA.
  11. Fantastic. I "unfortunately" remember those olden days of writing DLL's for use in Windows 3.1. One of my first JAVA experiences was writing a DLL to call a JAVA program to access hardware for an embedded system. There is a long list of things that don't "quite" work the same in C++ and C, never mind the differences between the different revision.
  12. Here is the advanced course version in assembly language bank0 udata delayLow res 1 delayHigh res 1 RES_VECT CODE 0x0000 ; processor reset vector GOTO START ; go to beginning of program MAIN_PROG CODE ; let linker place main program #define delayValue 10000 go_slow banksel delayLow movlw low delayValue movwf delayLow movlw high delayValue movwf delayHigh slow_loop decfsz delayLow goto slow_loop decfsz delayHigh goto slow_loop return START BANKSEL TRISA CLRF TRISA loop BANKSEL PORTA BSF PORTA,2 call go_slow BANKSEL PORTA BCF PORTA,2 call go_slow GOTO loop END It works exactly the same way as the C version.
  13. The defaults worked just fine! Of course there may have been a watchdog timer occurring someplace.
  14. Time for part 2! Last time, I gave you the homework of downloading and installing MPLAB and finding a Curiosity Nano DM164144 . Once you have done your homework, it is time for STEP 3, get that first project running. Normally my advice would be to breakout Mplab Code Configurator and get the initialization code up and running, but I did not assign that for homework! So we will go old school and code straight to the metal. Fortunately, our first task is to blink an LED. Step 1: Find the pin with the LED. A quick check of the schematic finds this section on page 3. This section reveals that the LED is attached to PORT A bit 2. With the knowledge of the LED location, we can get to work at blinking the LED. The first step is to configure the LED pin as an output. This is done by clearing bits in the TRIS register. I will cheat and simply clear ALL the bits in this register. Next we go into a loop and repeatedly set and clear the the PORT A bit 2. #include <xc.h> void main(void) { TRISA = 0; while(1) { PORTA = 0; PORTA = 0x04; } return; } Let us put this together with MPLAB and get it into the device. First we will make a new project: Second, we will create our first source file by selecting New File and then follow the Microchip Embedded -> XC8 Compiler -> main.c give your file a name (I chose main.c) And you are ready to enter the program above. And this is what it looks like typed into MPLAB. But does it work? Plug in your shiny demo board and press this button: And Voila!, the LED is lit... but wait, my code should turn the LED ON and OFF... Why is my LED simply on? To answer that question I will break out my trusty logic analyzer. That is my Saleae Logic Pro 16. This device can quickly measure the voltage on the pins and draw a picture of what is happening. One nice feature of this device is it can show both a simple digital view of the voltage and an analog view. So here are the two views at the same time. Note the LED is on for 3.02µs (microseconds for all of you 7'th graders). That is 0.00000302 seconds. The LED is off for nearly 2µs. That means the LED is blinking at 201.3kHz. (201 thousand times per second). That might explain why I can't see it. We need to add a big delay to our program and slow it down so humans can see it. One way would be to make a big loop and just do nothing for a few thousand instructions. Let us make a function that can do that. Here is the new program. #include <xc.h> void go_slow(void) { for(int x=0;x<10000;x++) { NOP(); } } void main(void) { TRISA = 0; while(1) { PORTA = 0; go_slow(); PORTA = 0x04; go_slow(); } return; } Note the new function go_slow(). This simply executes a NOP (No Operation) 10,000 times. I called this function after turning the LED OFF and again after turning the LED ON. The LED is now blinking at a nice rate. If we attach the saleae to it, we can measure the new blink. Now is is going at 2.797 times per second. By adjusting the loop from 10,000 to some other value, we could make the blink anything we want. To help you make fast progress, please notice the complete project Step_3.zip attached to this post. Next time we will be exploring the button on this circuit board. For your homework, see if you can make your LED blink useful patterns like morse code. Good Luck Step_3.zip
  15. It seems that in 2.15 they changed how the command line options are interpreted. in 2.10, there was a -fgnu89-inline option that ensured the inlines operated in a particular way (the GNU89 way). In 2.15 it seems this option is being quietly ignored. You can test by removing the option from the command line when you build in 2.10 and then you should see the same error.
×
×
  • Create New...