Introduction: AVRSH: a Command Interpreter Shell for Arduino/AVR.
Ever wanted to be "logged in" to your AVR microcontroller? Ever thought it would be cool to "cat" a register to see its contents? Have you always wanted a way to power up and power down individual peripheral sub-systems of your AVR or Arduino in *real time* ? Me, too, so I wrote the AVR Shell, a UNIX-like shell.
It's UNIX-like because it's reminiscent of the shell account you went out and bought to run your irc nick collision bots on, as well as having a command or two in common. It also has a filesystem that resembles UNIX extfs, using an external EEPROM, but that's become a project unto itself so I'll be releasing that module separately under a different instructable when it's production-ready.
Here's a list of the things you can currently do with the AVR Shell:
It's UNIX-like because it's reminiscent of the shell account you went out and bought to run your irc nick collision bots on, as well as having a command or two in common. It also has a filesystem that resembles UNIX extfs, using an external EEPROM, but that's become a project unto itself so I'll be releasing that module separately under a different instructable when it's production-ready.
Here's a list of the things you can currently do with the AVR Shell:
- Read all your Data Direction Registers (DDRn), ports, and pins in real-time
- Write to all your DDRn's, ports, and pins to turn on motors, LED's, or read sensors in real-time
- List all known registers on the system
- Create and store values in user-defined variables backed up by EEPROM.
- Create a root password and authenticate against it (used for telnet access)
- Read the configured CPU clock speed
- Change your CPU clock speed by setting a prescaler
- Start and stop 16-bit timers for timing of various things
- Power up and/or power down peripheral sub-systems: Analog to Digital Converters (ADC), Serial Peripheral Interface (SPI), Two-wire Interface (TWI/I2C), UART/USART. Useful for when you want to reduce power consumption of the microcontroller or to enable certain functions.
- Written in C++ with reusable objects.
Step 1: What You'll Need
This instructable doesn't require much except that you:
Note: The PROGMEM attribute is broken in the current AVR GCC implementation for C++ and this is a known bug. If you compile it, expect to get many warning messages saying "warning: only initialized variables can be placed into program memory area." Besides being annoying to see, this warning is harmless. As C++ on the embedded platform isn't high on the AVR GCC priorities list, it is unknown when this will be fixed. If you check out the code, you will see where I have made work arounds to reduce this warning by implementing my own attribute statements.
Pretty simple. Download and install anything that you might need to then flip the page and let's get crackin'.
- Have an Arduino or ATmega328P. Other AVR's could work, but you may need to modify the code to list any registers that are unique to your MCU. The names only need to match what is listed in the <avr/io*.h> header file unique to your MCU. Many of the register names are the same between AVRs, so your mileage may vary when porting.
- Have a way to connect to the serial USART of your Arduino/AVR. The system has been tested most extensively with the AVR Terminal, a Windows app that makes a serial connection via your USB or COM port. Works with Arduinos using the USB connection and any AVR using the USB-BUB from Moderndevice.com. Other terminal options include: Putty, minicom (Linux and FreeBSD), screen (Linux/FreeBSD), Hyperterminal, Teraterm. I've found putty and teraterm send some garbage when connecting so your first command may be garbled.
- Have the AVR Shell firmware installed and running, which you can download from these pages, or always get the latest version at BattleDroids.net.
Note: The PROGMEM attribute is broken in the current AVR GCC implementation for C++ and this is a known bug. If you compile it, expect to get many warning messages saying "warning: only initialized variables can be placed into program memory area." Besides being annoying to see, this warning is harmless. As C++ on the embedded platform isn't high on the AVR GCC priorities list, it is unknown when this will be fixed. If you check out the code, you will see where I have made work arounds to reduce this warning by implementing my own attribute statements.
Pretty simple. Download and install anything that you might need to then flip the page and let's get crackin'.
Step 2: Reading and Writing Registers
The AVR Shell was written primarily to access some sensors that I had connected to my AVR. It started with a simple LED then moved to light sensors, temperature sensors, and finally to two ultrasonic transducers. avrsh can set the digital components of these sensors by writing to the registers that control them.
Manipulating AVR registers while running
To get a list of all known registers on your Arduino, type:
By setting bits like this you can change the functioning of your AVR on the fly. For instance, by changing the CTC timer match value found in OCR1A. It also lets you peek into particular settings that you would have to programmatically check in your code, such as the UBBR value for your baud rate.
Working with DDRn, PORTn, and PINn
The I/O pins are also assigned to registers and can be set in exactly the same way, but a special syntax has been created to work with these types of registers.
In code, there is a normal process for, say, turning on an LED or other device that requires a digital high or low. It requires setting the Data Direction Register to indicate the pin is for output, and then writing a 1 or 0 to the particular bit in the correct port. Assuming we have an LED connected to digital pin 13 (PB5) and we want to turn it on, here's how to do that while your AVR is running:
Manipulating AVR registers while running
To get a list of all known registers on your Arduino, type:
print registersand you'll get a printout looking like this...
I know about the following registers: TIFR0 PORTC TIFR1 PORTD TIFR2 DDRD PCIFR DDRB EIFR DDRC EIMSK PINB EECR PINC EEDR PIND SREG EEARL GPIOR0 EEARH GPIOR1 GTCCR GPIOR2 TCCR0A TCCR0B TCNT0 OCR0A OCR0B SPCR SPDR ACSR SMCR MCUSR MCUCR SPMCSR WDTCSR CLKPR PRR OSCCAL PCICR EICRA PCMSK0 PCMSK1 TIMSK0 TIMSK1 TIMSK2 ADCL ADCH ADCSRA ADCSRB ADMUX DIDR0 DIDR1 TCCR1A TCCR1B TCCR1C TCNT1L TCNT1H ICR1L ICR1H OCR1AL OCR1AH OCR1BL OCR1BH TCCR2A TCCR2B TCNT2 OCR2A OCR2B ASSR TWBR TWSR TWAR TWDR TWCR TWAMR UCSR0A UCSR0B UCSR0C UBRR0L UBRR0H UDR0 PORTB root@ATmega328p>To see how the individual bits are set in any register, use the cat or echo command:
cat %GPIOR0Here I'm asking the command interpreter to display, or echo, the contents of the General Purpose I/O Register #0. Note the percent sign (%) in front of the register name. You need this to indicate to the shell that this is a reserved keyword identifying a register. The typical output from an echo command looks like this:
GPIOR0(0x0) set to [00000000]The output shows the name of the register, the hexadecimal value found in the register and the binary representation of the register (showing each bit as a 1 or 0). To set a particular bit in any register, use the "index of" operator []. For example, let's say I want the 3rd bit to a 1.
%GPIOR0[3] = 1and the shell will give you a response indicating it's action and the result:
GPIOR0(0x0) set to [00000000] --> (0x8) set to [00001000]Dont' forget the percent sign to tell the shell you're working with a register. Also note that by setting the 3rd bit, that's 4 bits in because our AVR's use a zero-based index. In other words, counting to the 3rd bit you count 0, 1, 2, 3, which is the 4th place, but the 3rd bit. You can clear a bit in the same way by setting a bit to zero.
By setting bits like this you can change the functioning of your AVR on the fly. For instance, by changing the CTC timer match value found in OCR1A. It also lets you peek into particular settings that you would have to programmatically check in your code, such as the UBBR value for your baud rate.
Working with DDRn, PORTn, and PINn
The I/O pins are also assigned to registers and can be set in exactly the same way, but a special syntax has been created to work with these types of registers.
In code, there is a normal process for, say, turning on an LED or other device that requires a digital high or low. It requires setting the Data Direction Register to indicate the pin is for output, and then writing a 1 or 0 to the particular bit in the correct port. Assuming we have an LED connected to digital pin 13 (PB5) and we want to turn it on, here's how to do that while your AVR is running:
set pin pb5 outputwrite pin pb5 highThe output, besides being able to see your LED come on, would look like this:
root@ATmega328p> set pin pb5 outputSet pb5 for outputroot@ATmega328p> write pin pb5 highWrote logic high to pin pb5The "root@ATmega328p>" is the shell's prompt that indicates it is ready to accept commands from you. To turn the LED off, you would simply write a low to the pin. If you want to read the digital input from a pin, use the read command. Using our above example:
root@ATmega328p> read pin pb5Pin: pb5 is HIGHAlternatively, just echo the pin register that controls that pin port. For example, if we have dip switches connected to digital pin 7 and 8 (PD7 and PD8), you could send the command:
echo %PINDand the shell would then display the contents of that register, showing you all of the input/output states of connected devices and whether the state of the switch was on or off.
Step 3: Reading and Writing Fuses
Fuses are special types of registers. They control everything from the clock speed of your microcontroller to what programming methods are available to write-protecting EEPROM. Sometimes you will need to change these settings, especially if you're creating a stand-alone AVR system. I'm not sure you should change your fuse settings on Arduino. Be careful with your fuses; you can lock yourself out if you set them incorrectly.
In a previous instructable, I demonstrated how you can read and set your fuses using your programmer and avrdude. Here, I'll show you how to read back your fuses at run time to see how your MCU has actually set them. Note, that this isn't the compile-time setting that you get from the definitions in <avr/io*.h> but the actual fuses as the MCU reads them at run time.
From Table 27-9 in the ATmega328P datasheet (databook, more like it) the bits of the Fuse Low Byte are as follows:
The same procedure is used for checking the High Fuse Byte, Extended Fuse Byte, and Lock fuses. The calibration and signature fuse bytes have been disabled in the code with an #if 0 preprocessor directive, which you can change if you feel scrappy.
In a previous instructable, I demonstrated how you can read and set your fuses using your programmer and avrdude. Here, I'll show you how to read back your fuses at run time to see how your MCU has actually set them. Note, that this isn't the compile-time setting that you get from the definitions in <avr/io*.h> but the actual fuses as the MCU reads them at run time.
From Table 27-9 in the ATmega328P datasheet (databook, more like it) the bits of the Fuse Low Byte are as follows:
CKDIV8 CKOUT SUT1 SUT0 CKSEL3 CKSEL2 CKSEL1 CKSEL0An interesting thing to note is that with fuses, 0 means programmed and a 1 means that that particular bit is unprogrammed. Somewhat counter-intuitive, but once you know it you know it.
- CKDIV8 sets your CPU clock to be divided by 8. The ATmega328P comes from the factory programmed to use its internal oscillator at 8MHz with CKDIV8 programmed (ie set to 0) giving you a final F_CPU or CPU frequency of 1MHz. On Arduino's, this is changed since they are configured to use an external oscillator at 16MHz.
- CKOUT when programmed will output your CPU clock on PB0, which is digital pin 8 on Arduinos.
- SUT[1..0] specifies the startup time for your AVR.
- CKSEL[3..0] sets the clock source, such as the internal RC oscillator, external oscillator, etc.
root@ATmega328p> read lfuseLower Fuse: 0xffSo, all bits are set to 1. I did the same procedure on an Arduino clone and got the same value. Checking one of my stand-alone AVR systems, I got 0xDA which is the value I had set some time back when configuring the chip.
The same procedure is used for checking the High Fuse Byte, Extended Fuse Byte, and Lock fuses. The calibration and signature fuse bytes have been disabled in the code with an #if 0 preprocessor directive, which you can change if you feel scrappy.
Step 4: Other Commands
There are several other commands that the default command interpreter understands that you may find useful. You can see all the implemented and future-release commands by issuing help or menu at the prompt. I'll quickly cover them here as they are mostly self-explanatory.
CPU Clock Frequency Settings
You can find out what your firmware has been configured to use as the CPU clock settings with the fcpu command:
Powering Up and Powering Down Peripheral Sub-Systems
On the same note as reducing power consumption mentioned earlier, you may want to further reduce power by shutting down some of the on-board peripherals that you are not using. The command interpreter and shell can currently power up and power down the following peripherals:
Starting and Stopping Timers
The shell has a built-in 16-bit timer that is available for use. You start the timer with the timer command:
Authentication
The shell can store an 8-character password into EEPROM. This password mechanism was created to support the telnet login capabilities, but could be expanded to protect other things. For example, you could require certain commands, like changing register values, through the authentication mechanism.
Set the password with the password command:
Variables
The shell understands the notion of user-defined variables. The code limits this to 20, but you can change that if you like by changing the define MAX_VARIABLES in script.h. You can save any 16-bit value (that is, any number up to 65,536) to a variable to be recalled later. The syntax is similar to registers except a dollar sign ($) is used to denote variables to the shell. List all your variables with the print variables command.
CPU Clock Frequency Settings
You can find out what your firmware has been configured to use as the CPU clock settings with the fcpu command:
root@ATmega328p> fcpuCPU Freq: 16000000That's 16 million, or 16 million herz, more commonly known as 16 MHz. You can change this on the fly, for whatever reason, with the clock command. This command takes one argument: the prescaler to use when dividing your clock speed. The clock command understands these prescaler values:
- ckdiv2
- ckdiv4
- ckdiv8
- ckdiv16
- ckdiv32
- ckdiv64
- ckdiv128
- ckdiv256
clock ckdiv2when your cpu speed is 16MHz would result in your clock speed being changed to 8MHz. Using a prescaler of ckdiv64 with an initial clock speed of 16MHz will result in a final clock speed of 250 KHz. Why on Earth would you want to make your MCU slower? Well, for one, a lower clock speed consumes less power and if you have your MCU running off of a battery in a project enclosure you may not need it to run at top speed, and could therefore, lower the speed and reduce it's power consumption, increasing the battery life. Also, if you are using the clock for any sort of timing issues with another MCU, say, implementing a software UART or some such thing, you may want to set it to a particular value that is easy to get a nice even baud rate with lower error rates.
Powering Up and Powering Down Peripheral Sub-Systems
On the same note as reducing power consumption mentioned earlier, you may want to further reduce power by shutting down some of the on-board peripherals that you are not using. The command interpreter and shell can currently power up and power down the following peripherals:
- Analog-to-Digital Converter (ADC). This peripheral is used when you have an analog sensor providing data (like temperature, light, acceleration, etc) and need to convert it to a digital value.
- Serial Peripheral Interface (SPI). The SPI bus is used to communicate with other SPI-enabled devices, like external memories, LED drivers, external ADC's, etc. Parts of the SPI are used for ISP programming, or at least the pins are, so be careful when shutting this down if you are programming via ISP.
- Two-Wire Interface. Some external devices use the I2C bus to communicate, although these are rapidly being replaced by SPI-enabled devices as SPI has a greater throughput.
- USART. This is your serial interface. You probably don't want to turn this off if you are connected to the AVR via the serial connection! However, I added this in here as a skeleton for porting to devices that have multiple USART's like the ATmega162 or ATmega644P.
- all. This argument to the powerup or powerdown command turns on all of the peripherals mentioned or turns them all off with one command. Again, use this command wisely.
root@ATmega328p> powerdown twiPowerdown of twi complete.root@ATmega328p> powerup twiPowerup of twi complete.
Starting and Stopping Timers
The shell has a built-in 16-bit timer that is available for use. You start the timer with the timer command:
timer startand stop the timer with the stop argument:
timer stopThis timer will not conflict with the internal USART timer. See the code for the implementation details of the USART timer, if that sort of gory detail interests you.
root@ATmega328p> timer startStarted timer.root@ATmega328p> timer stopElapsed time: ~ 157 seconds
Authentication
The shell can store an 8-character password into EEPROM. This password mechanism was created to support the telnet login capabilities, but could be expanded to protect other things. For example, you could require certain commands, like changing register values, through the authentication mechanism.
Set the password with the password command:
root@ATmega328p> passwd blahWrote root password to EEPROMAuthorize against he password (or require authorization programatically through the code) with the auth command. Note, that if you attempt to change the root password and there is already a root password set, you must authorize yourself against the old password before being allowed to change it to a new password.
root@ATmega328p> passwd blinkyYou must authorize yourself first.root@ATmega328p> auth blahAuthorized.root@ATmega328p> passwd blinkyWrote NEW root password to EEPROMOf course, you will need to load the avrsh.eep file if you erase the firmware to have your old values and variables restored. The Makefile will create the EEPROM file for you.
Variables
The shell understands the notion of user-defined variables. The code limits this to 20, but you can change that if you like by changing the define MAX_VARIABLES in script.h. You can save any 16-bit value (that is, any number up to 65,536) to a variable to be recalled later. The syntax is similar to registers except a dollar sign ($) is used to denote variables to the shell. List all your variables with the print variables command.
print variablesUser-defined variables:Index Name -> Value(01): $FREE$ -> 0(02): $FREE$ -> 0(03): $FREE$ -> 0(04): $FREE$ -> 0(05): $FREE$ -> 0(06): $FREE$ -> 0(07): $FREE$ -> 0(08): $FREE$ -> 0(09): $FREE$ -> 0(10): $FREE$ -> 0(11): $FREE$ -> 0(12): $FREE$ -> 0(13): $FREE$ -> 0(14): $FREE$ -> 0(15): $FREE$ -> 0(16): $FREE$ -> 0(17): $FREE$ -> 0(18): $FREE$ -> 0(19): $FREE$ -> 0(20): $FREE$ -> 0Complete.Set a variable:
$newvar = 25$timeout = 23245Get the value of a given variable:
root@ATmega328p> echo $newvar$ newvar --> 25You can see what all variables you've currently instantiated with the print command that you already know.
User-defined variables:Index Name -> Value(01): newvar -> 25(02): timeout -> 23245(03): $FREE$ -> 0(04): $FREE$ -> 0(05): $FREE$ -> 0(06): $FREE$ -> 0(07): $FREE$ -> 0(08): $FREE$ -> 0(09): $FREE$ -> 0(10): $FREE$ -> 0(11): $FREE$ -> 0(12): $FREE$ -> 0(13): $FREE$ -> 0(14): $FREE$ -> 0(15): $FREE$ -> 0(16): $FREE$ -> 0(17): $FREE$ -> 0(18): $FREE$ -> 0(19): $FREE$ -> 0(20): $FREE$ -> 0Complete.The $FREE$ name just indicates that that variable location is free and has not been assigned a variable name yet.
Step 5: Customizing the Shell
You are free to hack at the code and customize it to your own needs, if you like. If I had known I'd be releasing this code, I would have made a separate command interpreter class and command structure and simply iterated through this calling a function pointer. It would reduce the amount of code, but as it stands the shell parses the command line and calls the appropriate shell method.
To add in your own custom commands, do the following:
1. Add your command to the parse list
The command parser will parse the command line and give you the command and any arguments separately. The arguments are passed as pointers to pointers, or an array of pointers, however you like to work with them. This is found in shell.cpp. Open up shell.cpp and find the ExecCmd method of the AVRShell class.
You may wish to add the command to program memory. If you do, add the command in progmem.h and progmem.cpp. You can add the command to program memory directly using the PSTR() macro, but you'll generate another warning of the type mentioned earlier. Again, this is a known bug working with C++, but you can get around this by adding the command directly in the progmem.* files, as I have done. If you don't mind adding to your SRAM usage, you can add the command as I have illustrated with the "clock" command.
Say you wanted to add a new command called "newcmd." Go to AVRShell::ExecCmd and find a convenient place to insert the following code:
2. Write your custom command code
In the same file, add your custom command code. This is the method definition. You will still want to add the declaration to shell.h. Just append it to the other commands. In the previous example, the code might look something like this:
The WriteLine and WriteRAM are global functions that return the UART's methods of the same name. The 2nd argument to this function is implicit. If you pass nothing, a command prompt will be written afterwards. If yo pass a 0 as the 2nd argument, a prompt will not be written. This is useful when you want to write several separate strings to output before the command prompt is returned to the user.
3. Have the shell execute the command code
You have already told the shell executor to execute the method cmdNewCmd when you setup the new command, but add it to the shell.h file to have it understood by the shell object. Just add it below the last command or in front of the first command, or anywhere in there.
And that's it. Recompile and upload the firmware to your Arduino and your new command is available from the shell at the prompt.
To add in your own custom commands, do the following:
1. Add your command to the parse list
The command parser will parse the command line and give you the command and any arguments separately. The arguments are passed as pointers to pointers, or an array of pointers, however you like to work with them. This is found in shell.cpp. Open up shell.cpp and find the ExecCmd method of the AVRShell class.
You may wish to add the command to program memory. If you do, add the command in progmem.h and progmem.cpp. You can add the command to program memory directly using the PSTR() macro, but you'll generate another warning of the type mentioned earlier. Again, this is a known bug working with C++, but you can get around this by adding the command directly in the progmem.* files, as I have done. If you don't mind adding to your SRAM usage, you can add the command as I have illustrated with the "clock" command.
Say you wanted to add a new command called "newcmd." Go to AVRShell::ExecCmd and find a convenient place to insert the following code:
else if (!strcmp(c,"newcmd")) cmdNewCmd(args);This will add your command and call the cmdNewCmd method that you will write in the next step.
2. Write your custom command code
In the same file, add your custom command code. This is the method definition. You will still want to add the declaration to shell.h. Just append it to the other commands. In the previous example, the code might look something like this:
voidAVRShell::cmdNewCmd(char ** args){ sprintf_P(buff,PSTR("Your command is %s\r\n",args[0]); WriteRAM(buff);}There are several things here. First, "buff" is a 40-character array buffer provided in the code for your use. We use the program memory version of sprintf since we're passing it a PSTR. You can use the regular version if you like, but ensure you don't pass the format in a PSTR. Also, the arguments are in the args array. If you typed "newcmd arg1 arg2" you can get at these arguments with the args[0] and args[1] subscripts. You can pass a maximum of MAX_ARGS arguments, as defined in the code. Feel free to change that value when you recompile if you need many more arguments to be passed at once.
The WriteLine and WriteRAM are global functions that return the UART's methods of the same name. The 2nd argument to this function is implicit. If you pass nothing, a command prompt will be written afterwards. If yo pass a 0 as the 2nd argument, a prompt will not be written. This is useful when you want to write several separate strings to output before the command prompt is returned to the user.
3. Have the shell execute the command code
You have already told the shell executor to execute the method cmdNewCmd when you setup the new command, but add it to the shell.h file to have it understood by the shell object. Just add it below the last command or in front of the first command, or anywhere in there.
And that's it. Recompile and upload the firmware to your Arduino and your new command is available from the shell at the prompt.
Step 6: Summary
You should know how to install and connect to your AVR/Arduino and get a live prompt on your running microcontroller. You know several commands that will pull runtime data from the MCU or set values into the MCU on the fly. You have also been shown how to add your own custom code to create your own unique commands to the shell to further customize it for your own needs. You can even gut the command interpreter to have it only contain your custom commands, if that suits your needs.
I hope you've enjoyed this instructable and that the AVR Shell can be useful for you, either as a real-time command interpreter or as a learning process in implementing your own.
As always, I look forward to any comments or suggestions on how this instructable can be improved!
Have fun with your AVR!
I hope you've enjoyed this instructable and that the AVR Shell can be useful for you, either as a real-time command interpreter or as a learning process in implementing your own.
As always, I look forward to any comments or suggestions on how this instructable can be improved!
Have fun with your AVR!