Programming the Make Controller Kit
A practical guide to writing your own programs on the Make Controller.
Overview
What we're trying to do, and how to get set up to do it.
The Make Controller is built on the very powerful SAM7X, which offers a ton of flexibility. This, of course, translates into some complexity as well. We've done our best to create a balance that wraps up the unnecessary complexity, while not making it too hard to get down and dirty with the internals if you need to.The SAM7X
Toolchain
To create code for the Make Controller, you'll need to download an appropriate toolchain. This will get you a compiler, linker, and assembler that will be able to turn your code into instructions that the Make Controller understands.- On Windows, see the Build Firmware on Windows how-to.
- On OS X, download the appropriate GnuArm toolchain from the Downloads page. Just run the installer and you're all set.
- On Linux and other platforms, there are a variety of options - check www.gnuarm.org for a recent source package.
IDE (Integrated Development Environment)
The next concern is where you're actually going to be doing your work. The programs you use to edit and compile your code can make a big difference to your overall workflow, so it's worth spending a little time checking out the options. There are a couple options for- The simplest, of course, is using your favorite text editor and the trusty old command line.
- A good, free, open source option is Eclipse - check the Build Firmware with Eclipse how-to for instructions on how to set it up.
- On OS X, you can use Xcode, a nice IDE that comes free with OS X. Check the Build Firmware with Xcode how-to for instructions on getting set up.
If you have another favorite IDE, please let me know and I'd be happy to add it.
Getting the Source Code
Starting from the existing source code is almost definitely the way to go. To do this, you can- Grab a source package from the Downloads page.
- Pull the latest source from the MakingThings Subversion repository.
Project Layout
How projects are organized, and what the important pieces are.
When you download the firmware package, you'll see a handful of directories in there. The main directories we're concerned with are the core and libraries. Core contains the basic Make Controller code, and libraries contains modules that can be added/removed from projects as needed.
The doc folder contains the API documentation for the Make Controller. Inside the projects directory are some example projects. The heavy directory, for example, contains the project used to create the main heavy.bin. To create your own project, it's easiest to make a copy of one of the projects, rename it for your project, and then start making changes.
- firmware
|
+-core
|
+-libraries
|
+-doc
|
+-projects
Relevant Files
Each project, at the very least, has two relevant files - make.c and config.h.
config.h
This file contains configuration options for your program. You can choose which systems you'd like to include - Ethernet, USB, OSC, etc. Most of these systems take up lots of resources on the board so if you don't need them, you should comment them out and regain that space. You can also set the amount of memory available to your project - this depends on a number of things and is discussed in further detail in the memory section of this tutorial. By default, this is set to a value that should generally work for the heavy firmware.
make.c
This is where you actually start to write your program. If you've started off by copying heavy, you can probably start to comment things out as heavy uses just about everything in the firmware API. The Run( ) task in make.c gets called at startup and is a good place to fire up any initialization for your project. In heavy this is where OSC systems are registered, the LED blink task is started and the Network and USB systems are turned on.
RTOS
A description of the operating system that runs on the Make Controller.
Often, when you're programming a microcontroller you're constrained by the fact that the processor can only do one thing at a time. This can make programming difficult when you want your board to be doing more than one thing at a time. Enter the operating system.
The operating system, or RTOS (real-time operating system) as they're often called in the microcontroller world, is a system that can switch back and forth between different programs seemingly simultaneously, to give the illusion that more than one is running at a time. This means you can write each of the elements of your programs separately, making it much easier to create more sophisticated programs.
Do I want to use an RTOS?
Not always. Sometimes you need more precise control than an operating system can offer.
The base element of time in the RTOS is called the tick. Each time a tick rolls around, the RTOS checks to see if there's a task with higher priority than the current task and then switches to that task. If a task blocks (sleeps), then the RTOS gives processor time to a lower priority task.
By default on the Make Controller, the RTOS tick is 1 millisecond. For most communications and control applications, this is just fine. However, in some sensing applications you need microsecond timing. So why not just make the RTOS tick shorter? Well, you can definitely do that but it takes the processor some small amount of time to switch between tasks. Once you get down much shorter than a millisecond, you're just spending most of your time switching between tasks, and not as much time running tasks.
So, ultimately it depends on your application whether or not you want to use the RTOS. Currently, most of the Make Controller API is based on the assumption that an RTOS is being used.
Tasks
The most important part of the RTOS is a task. You can have many tasks happening simultaneously although, as you might imagine, it's best to be as efficient as possible with your use of them. A task is just a function, really, except it allows you to sit inside it forever, sleeping when you need to wait for something. It's important to always sleep at some point - otherwise, the processor will never give time to any of the other tasks. All tasks have the same signature - they look like:
void MyTask( void* parameter );
When you create a task, you need to specify a couple of things:
- its name (arbitrary)
- its priority - how important this task is relative to other tasks.
- the amount of memory to allocate it - this is a little touchy-feely, and is discussed in the Memory section of this tutorial.
TaskCreate( MyTask, "Mine", 1000, 0, 1 ); // create the task, called "Mine, with
void MyTask( void* parameter )
{
// initialization here
while( 1 ) // now loop forever
{
// do your task
}
}
Check the Task documentation for more details.
Priority
When tasks are created, they are assigned a priority - from 0 to 5. Higher priority tasks will always run instead of tasks with lower priorities. And tasks that have the same priority take turns in a round robin process. The only time lower priority tasks get to run in a system with tasks with different priorities is when the higher priority tasks either sleep or are waiting for input from something.
It is a common source of problems to create a high priority task and then forget to sleep, allowing lower priority tasks to run. In Heavy the Blink task is run with the lowest priority, so if the Controller status LED stops blinking it might be an indication that some task is hogging the system.
Communication between tasks
When all your code is in one task, you never have to worry about different tasks accessing data and resources simultaneously. When you have multiple tasks, however, you do. The problem comes when you do an operation that has multiple parts. Task A starts working on the operation, but runs out of time. The operating system switches over to the other task, which then starts working on the same data. When the operating system switches back to the first task, it proceeds unaware of what might have happened. Disaster frequently results. Tasks sometimes need to communicate with each other. This is done with message queues and semaphores.
Message queues are a feature that allows tasks to safely send and receive data amongst themselves. A task can sit waiting for a message, using no processor time at all. Another task can post a message to the queue and the first will be woken up and will be given the message.
Semaphores are a way to serialize access to a resource in a thread-safe way.
Check the Semaphores and Queues sections for details on how to use these.
Memory
Important concepts about how memory is used on the Make Controller.
The Make Controller has quite a bit of memory onboard (at least in the microcontroller world) - 256K of Flash and 64K of RAM. For those of us coming from an 8-bit microcontroller world, this sounds like more than you could ever know what to do with. Of course, from the perspective of the desktop, this is squat.It turns out to be a pretty reasonable amount - you don't have to spend hours assembly-optimizing your code, but you do need to be aware of your memory usage as you create your programs.
Memory allocation
Memory has been the cause of many a bug in programs on any platform. If you don't need to do dynamic memory allocation, the best advice is generally not to use it - just allocate whatever you need in your code and at compile time the linker will make sure you aren't overstepping your bounds. In the cases where dynamic memory allocation makes sense, the Make Controller can do dynamic memory allocation in a way that will be familiar to C programmers - using Malloc and Free. This requires some understanding about pointers so if those aren't your strong suit beware.Malloc will allocate a certain number of bytes for you, if they're available, when you ask it:
int bufferSize = 1024;When you're done with this memory, you should release it using Free:
char *bufferPtr = Malloc( bufferSize );
if( bufferPtr != NULL )
// then we allocated successfully
Free( bufferPtr );The memory manager in FreeRTOS does not defragment any of the heap after memory is freed.
Malloc also has to pause the processor while it allocates memory, so if you're Mallocing and Freeing all the time, it will take a toll on performance.
Balancing the heap
Malloc allocates memory from the heap. In your config.h you can specify how large the heap is by adjusting the CONTROLLER_HEAPSIZE constant. You need to be aware of how much memory the rest of your program is using, then you can basically use the remaining memory for your heap. For instance, the whole networking system on the Make Controller takes up a ton of memory. If you're not using it in your project, you can reclaim all that memory for your heap to be used in your calls to Malloc.To get a sense of how much memory you're using, you can take a look at the map file produced during compilation.
When you create new tasks, these are also allocated from the heap. If you're going to be creating lots of tasks that require lots of memory, be sure to set your heap size large enough.
Memory (stack) for tasks
When you create a new task, you need to specify how much memory to allocate it. This memory comes from the heap. One of the most frequent issues I have personally when writing programs is running out of stack on one of my tasks. This can be very frustrating because there's nothing that says, "Hey I've run out of memory I'm quitting now". The memory just runs over into some place it shouldn't and things go haywire, seemingly with no explanation. This is where you need to just take a deep breath, and allocate more memory to your task.It's hard to really know in advance how many bytes you're going to need for your task. But there are some things that will let us make educated guesses without going to all the trouble of caluclating the size of every piece of data we might use.
Local (or automatic) variables are created on the stack of a task. So, if you all of sudden say something like
char buffer[1024];then you have to make sure you have more than 1024 bytes allocated to your task. Some data structures are big and it can be easy to forget that when you're not actually writing out how many bytes they are, as in the buffer example above. If you're calling into any other systems, be aware that they will almost certainly have some local variables that you'll need to account for.
Usually it's best to just give yourself a big hunk of memory to start with, and then start to whittle it down once your program is taking shape.
Big memory consumers
There are a few players in the Make Controller codebase that are notoriously big consumers of memory. This isn't necessarily a problem - it's just good to be aware of them so that you can be an informed debugger!- The network system. By far the biggest consumer of memory.
- The web server.
- printf and snprintf. Compiled under newlib, these functions end up requiring quite a bit of memory. Use them with caution.
Networking
Important concepts in using the Make Controller's Ethernet capabilities.
The Make Controller makes it easy to get your project connected on your local network or on the internet via a standard sockets-based system, familiar to desktop programmers. It provides a full TCP/IP network stack based on lwIP (lightweight IP), an open source project. The work to integrate it into FreeRTOS was done by the FreeRTOS project.There are two main kinds of communication that programs tend to use: UDP and TCP. These are both supported on the Make Controller. TCP is built on top of UDP and ensures that messages get where they're going, although it adds some slight overhead to do that. If you don't know whether to use UDP or TCP, that's ok. A couple of general guidelines can get you through most questions:
- If you're connecting to any kind of webserver or web application, you're almost for sure going to use TCP
- If you're communicating via OSC, chances are you'll want to use UDP
- If you absolutely need to know that your data gets where it's going, use TCP
- If you're sending lots of data and don't want the overhead of using TCP, use UDP
Debugging
How to debug your projects.
There are two ways to go about debugging programs running on your Make Controller:
- Use the simple Debug system in which the Make Controller will send out debug messages via OSC over USB and/or Ethernet. This option is simple to use, but not the most powerful option.
- Set up to do breakpoint debugging. This option is more complicated, but much more powerful.
Debug via OSC
A debugging function called "Debug" is defined by the Controller Library. This function allows you to send debug data to another machine via UDP or via USB.
The Debug( ) function is declared as follows:
int Debug( int level, char* string, ... );
The idea is that the function can be called with parameters like printf( ) or sprintf( ). The first parameter specifies the debug level. This helps categorize messages according to severity. Here are some examples:
Debug( DEBUG_ALWAYS, "Hello" );
Debug( DEBUG_ERROR, "Oh no! No memory" );
Debug( DEBUG_MESSAGE, "Done parsing. Length %d", length );
There is a debug subsystem in the firmware API which can be accessed via OSC. You can control the debug level and whether USB and or UDP channels get the Debug messages. By default the system debug level is set to DEBUG_MESSAGE - which means that all messages will be sent.
Breakpoint Debugging
No matter which way you go about it, you'll need a piece of JTAG hardware for breakpoint debugging. If you've purchased Rowley's CrossWorks IDE, it's pretty easy to set this up - basically, just plug in and then check their documentation if you have any trouble. If you want to go the open source route, you can do breakpoint debugging with OpenOCD (Open On-Chip Debugger). Check the Debug With OpenOCD tutorial for details on how to do that.IO Sharing
An overview of the mechanism used on the Make Controller to prevent double-use of the IO lines.
The Application Board has eight 1A outputs that can be used for a variety of different purposes. They can be used as single channel outputs, in pairs to drive motors or in fours to drive up to two stepper motors. This flexibility creates a problem - how to share all these lines in the easiest way possible?
The solution we arrived at was to have an arrangement where subsystems like Motor that need to use two different IO lines for each motor need to lock those lines before they'll run successfully. Let's say that we wanted to use Motor 0. The two lines that Motor 0 uses are the Digital Output lines 0 and 1. We'd like it if code requesting to use Digital Out 0 or 1 while the Motor is being used would be denied.
The most obvious way to do this is to force all users of all subsystems to Open( ) or Lock( ) the subsystem before use and Close( ) or Unlock( ) the subsystem afterwards. But this is adds an additional level of complexity that we didn't think was convenient. Especially since in the majority of cases an IO is used only once and requiring an additional step for the rare case where an IO is put to multiple different uses would be frustrating.
What we decided to do instead was to permit calls to subsystems to attempt to lock the relevent IO's up upon first use. If another subsystem attempts to use an IO that has been previously locked, that second subsystem is denied access. All the user of a shared IO needs to do is to remember to call subsystem_SetActive( 0 ) to deactivate a subsystem. When there is no sharing, there are no special considerations at all. Just use it.
Here are a couple of examples. The first shows how to use non-conflicting subsystems.
// use a digital out - set digital out to 1
DigitalOut_SetValue( 0, 1 );
// use a non conflicting motor - motor 1 uses digital outs 2 & 3
Motor_SetSpeed( 1, 1023 );
There is no need to worry about opening or closing a device since there are no conflicts.
The second example shows how to use conflicting subsystems.
// use a digital out - set digital out to 1Had we not called SetActive( 0 ) on the output device, the Motor subsystem would not have allowed access to motor 0, and an error would have been returned by the SetSpeed( ) call.
DigitalOut_SetValue( 0, 1 );
// Now we're done with using DigitalOut 0 as a regular output,
// deactivate it
DigitalOut_SetActive( 0, 0 );
// use a conflicting motor - motor 0 uses digital outs 0 & 1
Motor_SetSpeed( 0, 1023 );

