Jump to content
 

N9WXU

Member
  • Content Count

    70
  • Joined

  • Last visited

  • Days Won

    22

Everything posted by N9WXU

  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.
  16. 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.
  17. The defaults worked just fine! Of course there may have been a watchdog timer occurring someplace.
  18. 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
  19. 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.
  20. I have often been asked "how do I get started" in embedded software. As I think about this question, I realize that the basic steps to get started with embedded software are nearly identical to the basic steps required to bring up a new MCU or new hardware. There is always a bootstrapping process and a logic progression of steps before you are "home-free" and building your product. So here is my bootstrapping process, but broken down into each step so the process is clear for those just starting out. Step 1 - Collect the tools. The tools of the trade for embedded engineering are quite simple. Development environment. Hardware to develop on. Measurement tools to check signals. Programming tool (sometimes built into a development kit) Serial Monitoring tool (sometimes built into a development kit) an LED (usually built into a development kit) Step 2 - Install the development environment This process can be quite simple, like installing MPLAB IDE, or it can be quite involved, like installing the ESP-IDF environment for the ESP32. You will be living in this environment for the duration of your project so get it right. Step 3 - That First Project. This first project is THE MOST IMPORTANT ONE. If it goes well, you are off to the races, but if it goes poorly, you will likely regret your choice of MCU and start hunting for a different one. The first project is to blink an LED. The actual code is trivial, but this project will ensure the entire development workflow is working and you can program your target. Step 4 - Building Out This is where you start exploring your new world. What are the peripherals? How do they work? What kinds of things do folks do with these peripherals? Step 5 - Techniques of the experts Now you probably know enough to bull your way to success. I have seen some amazing projects built by folks who simply did not know how to quit. But, it would sure be nice if you could stand on the shoulders of giants and make your program easy and effective. Back in university one of my professors told a story of his first program. He was a physicist and needed to run a simulation. The idea was to write a program for the departments new computer to perform the simulation. Like many simulation, this one involved a newtonian solver for some of the math. This sort of solver converges on the correct answer iteratively. So he started his work. After a few days, he was bugging his computer science friend on the syntax of fortran and how to declare variables. Eventually, he had it doing one pass through math. Finally, he figured he knew his tool chain (fortran) and plowed ahead. A week later he proudly showed off his new program to his friend. His CS friend was impressed that this new programmer has produced such a complex application and it was working. His friend asked to see the program and was shocked at the 1 meter high printout. Scanning through the printout, he quickly discovered that while the physicist had figured out how to setup the math, he never figured out how to write a loop. The entire program was the same set of functions RETYPED (he did not figure out cut/paste) hundreds of times until the math had run enough times to converge on the result. Brute force does work....but there is usually a better way. Enough talking. It is time to get started. If you want to follow along, download and install MPLAB IDE from www.microchip.com/mplab and find a Curiosity Nano Evaluation Kit DM164144 (https://new.microchipdirect.com/product/search/all/DM164144). Next time, we will install the IDE, setup MCC and start that all-important LED blink. Good Luck.
  21. Embedded applications are hard for a large number of reasons, but one of the main issues is memory. Today I want to talk about how our C variables get initialized and a few assumptions we make as we use C to write embedded software. Let us take a few simple declarations such as we might make every day. char *string1 = "string1"; const char *string2 = "string2"; char const *string3 = "string3"; char * const string4 = "string4"; char const * const string5 = "string5"; In C99 these will all compile just fine but they are very different. In C++11, 2 of these will have a warning. We shall discuss them in order. Duration & Scope The first thing to notice is these variables are all declared outside of a function. That affects them in the following ways: External Linkage - i.e. they are global. Static Storage Duration - i.e. they are always active. allocated and initialized before control goes to main() Of course, if the keyword static had be placed before one of the declarations, the external linkage would have changed to internal linkage, preventing the variable from being accessed outside the compilation unit. Because they have static storage class, they will be initialized. If no initializer is specified they will be initialized to 0 or NULL. In each case, these have an initializer. So what is being initialized? Each variable is some type of a character pointer. So the value being stored in the variable will be an address. The address will be the address of a string of characters. The image on the right shows a data segment with the initialized variables. C does not actually specify where these const char strings need to be located. In GCC, they will be placed in the data segment and the variable will get the address of the string. However, in XC8, they will be placed in the Text segment (in FLASH). In the Arduino (GCC) environment you can force a string into FLASH by adding PROGMEM or using the F (FLASH) macro like this : F("String"). If the string is NOT in flash memory, then CINIT will copy the string from the text segment into the data segment. Then the variables will be initialized with pointers to the strings. String 1 The declaration for string 1 is simply a char *. This is a pointer to character(s). It is permissible for this pointer to be changed at run-time. i.e. the following is legal: string1 = string2; Of course, you will get a warning because string2 is a const char *. It is also permissible to do this: string1[3] = 'a'; Which will change the original string into "striag1". However it is NOT permissible to do the following: string1 = string2; string1[3] = 'a'; If you DO do this, it is likely to compile but there could be a few problems because string2 is declared as a const string so it must not be modified. Here is my compiler reluctantly obeying me and then the program crashes on the write. Just imagine that the string is in FLASH so writing is impossible without specific write sequences which the compiler probably does not know. In some environments you can get away with this. For example, I was using a Cypress WiFi device that loads the entire FLASH into RAM and then executes it. This code will run and it will not crash. Be very careful in such circumstances because in a few years you will be tasked to port the program to something else and life will be made hard because you did not fix the warnings. It turns out that in section 6.7.3 paragraph 5 of the C99 standard the behavior of line 22 is undefined. Your environment can choose to do anything it wants. String 2 & 3 The declaration for string 2 is a const char * and string 3 is char const *. These are IDENTICAL. This is in section 6.7.3 paragraph 9 "the order of type qualifiers within a list of specifiers or qualifiers does not affect the specified type". So these are pointers to a constant character(s). In a nice compiler, these characters would be stored in FLASH memory and never copied into RAM. That would be most memory efficient. However, GCC will copy these from FLASH into RAM and then use the address of these strings to initialize the variables. String 4 This declaration is to a const pointer. That is, the pointer value cannot change but the data pointed to by the pointer CAN change. Note the ERROR on line 22. Line 21 is perfectly fine. The data pointed by the pointer is NOT const so it is allowed to change. In C++11, the original declaration will have a warning because a char * is being initialized to point to a const char *. Never mind that the pointer is const. String 5 This is a combination of both sorts of constants. A const pointer pointing at a const character(s). This can be initialized from a const string just fine. But you will not be allowed to change the pointer or the data pointed to. Both line 21 and line 22 have errors and not simply warnings. We will do more of these variable initializer posts. The language rules are very clear but there are a few constructions that we don't see very often. And even worse, the assumptions we make about the syntax work often enough that we end up with some very strange notions on what the language allows. A good resource for testing your knowledge about strange C declarations is this website: https://www.cdecl.org Good Luck
  22. Good job tracking this down folks. It sounds like the templates need a little help for the 8-bit case and probably for the 32-bit case (running this on a PIC32). Is there any news on getting the templates on a public repository (GITHUB) so we can contribute fixes and help out?
  23. N9WXU

    Hello

    War stories seem to be one of the best ways for engineers to learn. Let us share and avoid repeating bad experiences.
  24. I have found the textbook “introduction to algorithms” by Thomas Cormen et al to be very helpful.
  25. Today we are going to explore the weird and wonderful trie (try). We are actually going to brute force a trie and we will write a basic code generator to synthesize a static trie so it our trusty NMEA token tester can stay in flash memory and fit in the PICmcu. I promise this is going to be the LEAST memory efficient trie you ever will see, but it is quick and it is easy to understand so let us press on. First things first, what is a trie? Simply, the trie takes the if-else construct we did in pt 2 and turns it into a data structure. I love this concept because it means I can write the trie parser once and simply be feeding it different data sets, it can process any language. In fact, I try to make most of my programs data driven so the code can be well tested and the data can be focus of our attention. Our basic trie will be implemented as a 2D array 26 x N. N will be adjusted to be as small as possible. The 26 comes from the 26 unique characters that can make up my NMEA string set. We will ignore lower case to make this dataset as small as possible. Here is a picture showing what we plan to do. In the picture we are finding the NMEA string "GPGGA". Each row of our 2D array represents each letter. If a letter is not involved at that layer of lookup, we will fill it with a -1. Each time we match, we learn the row where the next match is. By matching to the row, GPGGA and GNGSA both share the first G by pointing to the second row. On the second row, The P and the N will point further into the structures. Here is the search code: int trie_search(const char *s,int length) { int t=0; int p; for(int i=0;i<length;i++) { p = s[i] - 'A'; if((t = trie[t][p].index)==-1) return -1; } return trie[t][p].key_index - 1; // -1 hold over from a previous version. Generator can post increment the key } This looks fantastic. Only one table lookup per letter so this should be blindly fast. Let us take a look at the data. STRNCMP IF-ELSE RAGEL -G2 Hash Search (Hash/Search) Hash GPERF Hash GPERF no compare Trie Search GNGSA 399 121 280 326 167/159 374 126 915 GPGSV 585 123 304 288 167/121 374 126 871 GLGSV 724 59 225 503 167/336 113 113 1395 GPRMC 899 83 299 536 167/369 374 126 984 GPGGA 283 113 298 440 167/273 374 126 821 Well Heck! that is certainly not what we expected to see. What is going on? At the core, this algorithm is wonderful because it only touches each letter once. This is very similar to the IF-ELSe so I would expect that this algorithm would be very similar to the IF-ELSE. We need to dig a bit deeper. As I look at the code, the only thing that stands out is the actual table lookup. We have a 2D table of structs, with each struct being 2 bytes. I wonder if this function is using a multiply to compute the table address. 9840 l1579: 9841 031A ;example5.c: 65: if((t = trie[t][p].index)==-1) 9842 movf (trie_search@p+1),w 9843 031A 082C movwf (??_trie_search+0)+0+1 9844 031B 00A1 movf (trie_search@p),w 9845 031C 082B movwf (??_trie_search+0)+0 9846 031D 00A0 lslf (??_trie_search+0)+0,f 9847 031E 35A0 rlf (??_trie_search+0)+1,f 9848 031F 0DA1 lslf (??_trie_search+0)+0,f 9849 0320 35A0 rlf (??_trie_search+0)+1,f 9850 0321 0DA1 movf (trie_search@t+1),w 9851 0322 082E movwf (___wmul@multiplier+1) 9852 0323 00F1 movf (trie_search@t),w 9853 0324 082D movwf (___wmul@multiplier) 9854 0325 00F0 movlw 068h 9855 0326 3068 movwf (___wmul@multiplicand) 9856 0327 00F2 movlw 0 9857 0328 3000 movwf ((___wmul@multiplicand))+1 9858 0329 00F3 fcall ___wmul Here is the list file of line 65 and sure enough, there it is. Lots of code to setup the multiply and then a brutal software multiply. We were doomed. This simple brute force approach needs to be a bit more elegance in its data structures. I will include the packaged project and the generator and you can duplicate the work. I still like this concept, I challenge the readers to run some experiments and post the fastest/smallest version they can devise. Good Luck example5.zip trie_builder.c trie_builder.h
×
×
  • Create New...