Jump to content
 

N9WXU

Member
  • Content Count

    70
  • Joined

  • Last visited

  • Days Won

    22

N9WXU last won the day on September 21

N9WXU had the most liked content!

Community Reputation

36 Excellent

1 Follower

About N9WXU

  • Rank
    Contributor

Recent Profile Visitors

440 profile views
  1. Sometimes I get the sad impression that embedded FW engineers only understand 1 data container, the array. The array is a powerful thing and there are many good reasons to use it but it also has some serious problems. For instance, a number of TCP/IP libraries use arrays to hold a static list of sockets. When you wish to create a new socket the library takes one of the unused sockets from the array and returns a reference (a pointer or index) so the socket can be used as a parameter for the rest of the API. It turns out that sockets are somewhat heavy things (use lots of memory) so you always want to optimize the system to have the smallest number of sockets necessary. Unfortunately, you must "pick" a reasonable number of sockets early in the development process. If you run out of sockets you must go back and recompile the library to utilize the new socket count. Now there is a dependency that is not obvious, only fails at run time and links the feature count of the product with the underlying library. You will see bugs like, "when I am using the app I no longer get notification e-mails". It turns out that this problem can be easily solved with a dynamic container. i.e. one that grows at runtime as you need it to. A brute force method would perhaps be to rely upon the heap to reallocate the array at runtime and simply give the library a pointer to an array. That will work but it inserts a heavy copy operation and the library has to be paused while the old array is migrated to the new array. I propose that you should consider a Linked List. I get a number of concerns from other engineers when I have made this suggestion so just hang tight just one moment. Concerns Allocating the memory requires the heap and my application cannot do that. Traversing the list is complicated and requires recursion. We cannot afford the stack space. A linked list library is a lot of code to solve this problem when a simple array can manage it. The linking pointers use more memory. If you have a new concern, post it below. I will talk about these first. Concern #1, Memory allocation I would argue that a heap is NOT required for a linked list. It is simply the way computer science often teaches the topic. Allocate a block of memory for the data. place the data in the block of memory. Data is often supplied as function parameters. insert the block into the list in the correct place. Computer science courses often teach linked lists and sorting algorithms at the same time so this process forms a powerful association. However, what if the process worked a little differently.j Library Code -> Define a suitable data structure for the user data. Include a pointer for the linked list. User Code -> Create a static instance of the library data structure. Fill it with data. User Code -> Pass a reference to the data structure to the library. Library Code -> insert the data structure into the linked list. If you follow this pattern, the user code can have as many sockets or timers or other widgets as it has memory for. The library will manage the list and operate on the elements. When you delete an element you are simply telling the library to forget but the memory is always owned by the user application. That fixes the data count dependency of the array. Concern #2, Traversing the list is complex and recursive. First, Recursion is always a choice. Just avoid it if that is a rule of your system. Every recursive algorithm can be converted to a loop. .Second, Traversing the list is not much different than an array. The pointer data type is larger so it does take a little longer. struct object_data { int mass; struct object_data *nextObject; }; int findTheMassOfTheObjects(struct object_data *objectList) { thisObject = objectList; while(thisObject) { totalMass += thisObject->mass; thisObject = thisObject->nextObject; } printf("The mass of all the objects is %d grams\n", totalMass); return totalMass; } So here is a quick example. It does have the potential of running across memory if the last object in the list does NOT point at NULL. So that is a potential pitfall. Concern #3, A linked list library is a lot of code Yes it is. Don't do that. A generic library can be done and is a great academic exercise but most of the time the additional next pointers and a few functions to insert and remove objects are sufficient. The "library" should be a collection of code snippets that your developers can copy and paste into the code. This will provide reuse but break the dependency on a common library allowing data types to change, or modifications to be made. Concern #4, A linked list will use more memory It is true that the linked list adds a pointer element to the container data structure. However, this additional memory is probably much smaller than the "just in case" additional memory of unused array elements. It is probably also MUCH better than going back and recompiling an underlying library late in the program and adding a lot more memory so the last bug will not happen again. A little history The linked list was invented by Allen Newell, Cliff Shaw and Herbert Simon. These men were developing IPL (Information Processing Language) and decided that lists were the most suitable solution for containers for IPL. They were eventually awarded a Turing Award for making basic contributions to AI, Psychology of Human Cognition and list processing. Interestingly IPL was developed for a computer called JOHNIAC which had a grand total of 16 kbytes of RAM. Even with only 16KB IPL was very successful and linked lists were determined to be the most suitable design for that problem set. Most of our modern microcontrollers have many times that memory and we are falling back on arrays more and more often. If you are going to insist on an array where a linked list is a better choice, you can rest easy knowing that CACHE memory works MUCH better with arrays simply because you can guarantee that all the data is in close proximity so the entire array is likely living in the cache. Good Luck P.S. - The timeout driver and the TCP library from Microchip both run on 8-bit machines with less than 4KB of RAM and they both use linked lists. Check out the code in MCC for details.
  2. One important rule for good software is Do not Repeat Yourself. This rule is often referred to under the acronym DRY. Most of the time, your code can be made DRY by refactoring repeating blocks of code into a function. This is a good practice but it can lead to a lot of small functions. You undoubtedly will keep your API's tidy by making these functions static and keeping them close to where they are needed but I have recently been working on a lot of C++ code and I have a new tool in my programming toolbox that I would like to share with you. The LAMBDA function. Essentially a lambda is a function that you define inside of another function. This lambda function has function scope so it can only be used in the function that defines it. This can be very useful and will do two things to help keep your code maintainable. It keeps your code readable by forcing you to define a function close to where it is needed. It encourages you to keep the function short & sweet because you will not be tempted to make it a "general purpose solution". Here is an example. I was tasked to implement a serializer where data would be sent on the serial port. This was a binary protocol and it included a special character for start of frame (SOF) and end of frame (EOF). Because the SOF and EOF characters could appear in the actual data, there was an additional data link escape (DLC) character sequence that would expand into the SOF, EOF and DLC. For added fun, there is a checksum that is generated BEFORE the character padding. <SOF><DATA><CHECKSUM><EOF> Here is a function that can decode this message. #define MAXIMUM_MESSAGE_SIZE 255 void dataLink::receiveData(char b) { const char SOF = 0x1A; const char EOF = 0x1B; const char DLC = 0x1C; static enum {findSOF, getData} theState; static int messageIndex=0; static char checksum = 0; static char receivedMessage[MAXIMUM_MESSAGE_SIZE]; switch(theState) { case findSOF: if(b == SOF) { theState = getData; messageIndex = 0; checksum = 0; memset(receivedMessage,0,sizeof(receivedMessage)); } break; case getData: { static bool dlc_last = false; if(dlc_last) { dlc_last = false; switch(b) { case 1: receivedMessage[messageIndex++] = 0x1A; checksum += 0x1A; break; case 2: receivedMessage[messageIndex++] = 0x1B; checksum += 0x1B; break; case 3: receivedMessage[messageIndex++] = 0x1C; checksum += 0x1C; break; } } else { switch(b) { case EOF: theState = findSOF; if(checksum == 0) { //********************* // Do something with the new message //********************* } break; case DLC: dlc_last = true; break; default: receivedMessage[messageIndex++] = b; checksum += b; break; } } break; } } } This function receives a byte and using a few states, creates a checksum validated array of decoded bytes representing the message. I will not explain each line as the details of this function are really not very important. As my code reviewer you should instantly notice that there are 4 sections of nearly identical code that are repeated. In other words, this is not DRY. My first inclination would be to attempt to reorder the decisions so the update of the message array and checksum was done once. This method of course works quite well in this case but I wanted a simple contrived example to show off the lambda function. #define MAXIMUM_MESSAGE_SIZE 255 void dataLink::receiveData(char b) { const char SOF = 0x1A; const char EOF = 0x1B; const char DLC = 0x1C; static enum {findSOF, getData} theState; static int messageIndex=0; static char checksum = 0; static char receivedMessage[MAXIMUM_MESSAGE_SIZE]; auto output_byte = [&](char b) { receivedMessage[messageIndex++] = b; checksum += b; }; switch(theState) { case findSOF: if(b == SOF) { theState = getData; messageIndex = 0; checksum = 0; memset(receivedMessage,0,sizeof(receivedMessage)); } break; case getData: { static bool dlc_last = false; switch(b) { case EOF: theState = findSOF; if(checksum == 0) { //********************* // Do something with the new message //********************* } break; case DLC: dlc_last = true; break; default: if(dlc_last) { dlc_last = false; switch(b) case 1: output_byte(0x1A); break; case 2: output_byte(0x1B); break; case 3: output_byte(0x1C); break; } else { output_byte(b); } break; } break; } } } Now you can see that I moved the work of inserting the byte into the array and updating the checksum into a function called output_byte. This function is defined inside the receiveData function. The syntax has a square bracket followed by parenthesis. The Ampersand inside the brackets indicates that the function has access to all the variables inside receiveData. This makes the function very simple and easy to verify by inspection. Of course you could have made the output_byte function a private member function of the class. But you would have needed to add the checksum and the index variables to the class as well. That increases the complexity of the class. By using the lambda, the function can be made DRY, and readable, and the function does not leak information or variables into any other code. This makes the function much simpler to maintain or potentially refactor in the future. BTW, I tested this function by building the project in the Arduino environment on a SAMD21. The actual Arduino IDE is very limited but when you use VIsual Studio Code and PlatformIO you get full debug on an ATMEL ICE with the Arduino frameworks. This makes developing interesting applications VERY fast. lambda_demo.zip
  3. N9WXU

    How to abstract

    I have seen lots of code that is tightly tied to specific hardware or to specific frameworks. This code is OK because it generally satisfies rule #1 (it must work) but as soon as the HW or framework changes this code becomes very difficult to adapt to the new system. Developers often state that they are abstracted from the hardware by the framework but this is generally never the case because the framework was provided by the hardware vendor. So what is a developer to do? Step #1 Ask the right question. Instead of asking HOW do I do a thing (how do I send bytes over the UART). The developer should ask WHAT do I need to do. Ideally the developer should answer this WHAT question at a pretty high level. WHAT do I need to do? I need to send a packet over RS485. Step #2 Define an API that satisfies the answers to the WHAT questions. If I must send a packet over RS485, then perhaps I need a SendPacket(myPacket) function. In the context of my application this function will be 100% clear to my partner developers. Step #3 Implement a trial of my new API that runs on my PC. This is sufficiently abstract that running my application on my development PC should be trivial. I can access a file, or the network, or the COM ports, or the STDIO and still satisfy the API. Get my partners to kick it around a bit. Repeat #1,#2 & #3 until the API is as clear as possible for THIS application. Step #4 Implement the new API on my HW or framework. This may seem like contributing to Lasagna code.... i.e. just another layer. But in fact this is the true definition of the hardware abstraction layer. ALL details of the HW (or framework) that are not required for THIS application are hidden away and no longer contribute to developer confusion. 100% of what is left is EXACTLY what your application needs. Now you have a chance at producing that mythical self documenting code. You will also find that unit testing the business logic can be more easily accomplished because you will MOCK all functions at this new API layer. Hardware NEVER has to be involved. Good Luck.
  4. I have been working on "STEM" activities with kids of all ages for quite some time. Usually it is with my own kids but often it has been with kids at schools or in the neighborhood. This activity has been very rewarding but there are a few challenges that can quickly make the experience less interesting for the kids and an exercise in frustration for you, the mentor. 1) Don't be spontaneous (but fake it well) - My daughter and I wired a display to a nano and wrote the code to count 0 to 9. This was a perfect bite sized project because I was able to write enough 7-segment abstraction (struct digit { int a:1; int b:1; etc...}; ) to quickly stick a number on the display and I left enough missing code to have her "help" by identifying which segments needed to be active to draw each number. This was a ton of fun and she was suitably engaged. However, on previous occasions we took on too much and the "library" that needed to be thrown together to bring the complexity into reach by a 7 year old was more that I could deliver inside her attention span. So you do need to be prepared for when the kids are motivated to play with electronics... but some of that preparedness might be a stock of ready to go code modules that you can tap into service. 2) Be Prepared with stuff. - I like to keep a pretty well stocked assortment of parts, tools and ingredients for many projects. With prices for components so cheap, I always buy a few extra's for the stock pile to enable a kid with a sudden itch to do something cool. Unfortunately, there are often a few unintended hurdles. For example: I have a large collection of 7-segment displays and a small pile of Arduino Nano's. 3) 3D printers are fun and interesting.... but laser cutters are better and scissors are best. We all like to show off the amazing things you can do with a 3d printer and I have 3 of them. Unfortunately, using a printer requires a few things. a) patience, b) learning to 3d model, c) patience. My kids are quite good at alerting me when my print has turned into a ball of yarn. But none of the kids has developed any interest in 3d modeling for the 3d printer. I also have a fairly large laser cutter. This is FAR more fun and the absolute best tool I have put into my garage. My laster cutter is 130W and cuts about 1.5meters x 1meter or so. We have cut the usual plywood and acrylic. We also cut gingerbread, fabric, paper, and cardboard. (Laser cut gingerbread houses taste bad) I can convert a pencil sketch into a DXF file in a few minutes....BUT the scissors are better for that quick and dirty experiment. which leads to.... 4) Fail Fast and with ZERO effort.... Kids HATE TO WASTE THEIR TIME. Of course what they consider wasted time and what you and I consider wasted time is a different discussion. For example: folding laundry is a waste of time because you will just unfold the laundry to wear the clothes. So it is better to jam everything under the bed. Taking 2 hours to design a 3d model for the laser cutter or 3d printer is a waste of time if the parts don't work when you are done. However, if you can quickly cut something out of cardboard with scissors or a knife, then the time cost is minimal and if it doesn't work out, they are not sad. I have often had a sad kid after an experiment that took a large amount of effort. I thought the experiment worked well and we all learned something but the "wasted effort" was a problem. I have also seen grownups ride a project down in flames because it was "too big to fail" or "we will have wasted all that money if we quit now".. This is the same problem on a grand scale as the kids. So teach them to fail fast and learn from each iteration. As the project develops, the cool tools will be used but not in the first pass. 5) Pick your battles. Guide your charges with small projects that can be "completed" in 30 minutes or so. DO NOT nag them to "finish" if it is not done on the first outing. If the kid finds that project fun, they will hound you to work on it with them. As they develop skills, they will work on parts themselves while you are not around. (watch out for super glue and soldering irons). This is the ideal situation. So you need to do teasers and have fun. They will come back to the fun projects and steer clear of boring ones. So what has worked for me? 1) Rockets. I have bought 12 packs of rockets as classroom kits. I keep a few on stock pile. Once you have a field to fly them you can always get an entire group of kids ready to fly small rockets in an hour or so and they are fun for all ages. 2) Paper Airplanes. Adults and kids can easily burn an afternoon with paper airplanes. Kids by themselves will quickly tire of it so teach them to fold a basic airplane, how to throw and add a little competition. Don't forget to include spectacular crashes as a competition goal because that will keep their spirits up when problems occur. 3) VEX Robotics. I have done FIRST robots, Lego League and VEX robotics. My favorite is VEX IQ because the budget can be reached by a small group of families and the field fits on the back porch. I did have to bribe one daughter who was doing the code with cookies. This started a tradition of "cookies and code". Each task completed earns a cookie. Each bug fixed is a cookie. The rewards are fantastic! 4) Robotics at Home. Robotics are good for kids because they incorporate so many aspects of engineering (Mechanical, Electrical, Software) into one package. You can easily fill in any of these elements while the child explores their interest. One of my daughters likes to build robots. Another likes to program them. I simply remove any technical obstacles, hopefully before they notice them coming. This allows them to keep living in the moment and solving the problems at their level. 5) SCIENCE!. Be prepared to take ANY question they have and turn it into a project. We launched baking soda & vinegar rockets. I did 3d print them so I had to plan ahead. We have also recreated Galileo's gravity experiments in our stairwell. We recorded the difference in the impact of different objects by connecting a microphone to an oscilloscope. We placed the microphone under a piece of wood so the objects would make a sharp noise. We then spent the time trying to release objects at exactly the same time. We used a lever to lift a car!. The child was 5. The lever was a 3 meter steel tube. The car was a small Jeep. We did not lift it very far and we bent the lever but the lesson was learned and will never be forgotten. 6) Change the Oil! Or any other advanced chore. Involve the child in activities that are beyond them but don't leave them stranded. I make sure my new drivers have changed the oil and a tire. I try to involve the younger kids just because they will be underfoot anyway. A child with engineering interests will be make their desires known. In the end you are providing an enriching experience for a child. Keep the experience short & sweet. The objective is to walk away THIS happy. If the experience is positive the child will come back for more. A future lesson can teach ohms law, or software abstraction. The first experiences are just to have fun and do something interesting. Please share your kid project ideas! Include pictures! Good Luck
  5. 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
  6. 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
  7. 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.
  8. 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.
  9. 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
  10. 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
  11. I totally cheat. https://cdecl.org Of course the right to left helps when you don't have the internet.
  12. 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.
  13. 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
  14. 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.
  15. 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.
×
×
  • Create New...