Control a Servo from Your PC with the Atmel SAM4S
Learn how to communicate between your PC and the SAM4S using the Atmel Software Framework’s USB module and simple ASCII commands.
Learn how to communicate between your PC and the SAM4S using the Atmel Software Framework’s USB module and simple ASCII commands.
Supporting Information
- Intro to Project Development with the Atmel SAM4S Xplained Pro
- Pulse Width Modulation
- Turn Your PWM into a DAC
- Low-Pass Filter a PWM Signal into an Analog Voltage
- Pulse-Width Modulation with the SAM4S Xplained Pro
Required Hardware/Software
- SAM4S Xplained Pro evaluation kit
- PROTO1 Xplained Pro extension board (not strictly necessary, but very helpful)
- Atmel Studio
- TowerPro SG92R servo (or any equivalent servo)
- AC/DC wall-mount transformer, 5 V (or any other 5 V supply with adequate current capacity)
- 2 receptacle-to-plug jumper wires
- 1 plug-to-plug jumper wire
- Realterm (not necessary, but recommended)
Previous Article
The Stack
In the previous article, we set up our hardware and implemented a basic framework for controlling a servo using a pulse-width-modulated signal. Now it’s time to make this project a little more interesting by incorporating PC-to-SAM4S USB communications along with a simple ASCII command interface. The first thing we need to do is properly configure and initialize the USB stack. Just in case you’re not familiar with this terminology, “stack” in this context refers to a software framework that makes it easy (or at least easier) for an ordinary user to accomplish a specific programming task. Atmel’s USB stack, then, is a collection of software that 1) takes care of numerous complicated details involved in USB communication and 2) provides various high-level functions that allow the user to implement USB functionality while knowing nothing about endpoints, packet types, enumeration, and so forth.
The Atmel Software Framework (ASF) covers several flavors of USB. As discussed in the previous article, we will use the CDC (communications device class) module:
This module makes it surprisingly painless to establish a virtual COM port connection with the PC. Once we have a virtual COM port connection, we can send and receive USB messages with nothing more than a terminal program. I will be using Realterm, which is free, reliable, and loaded with useful features. (If you see a strange error message when you open Realterm, try using “Run as administrator” from the right-click menu.) If you prefer some other means of talking to a COM port, by all means use it. If you have no other means but don’t feel like downloading a new program, you can use the “serial port control panel” included in Atmel Studio 7.0 (Tools → Data Visualizer, then click on “Configuration” on the left and under “Modules” select External Connection → Serial Port).
USB Config
We need to modify the “conf_usb.h” file before we can initialize and use the stack. There’s quite a bit going on in this file, and I will not attempt to cover everything. Instead, I’ll simply give you the code and supplement Atmel’s comments with some helpful information:
#ifndef _CONF_USB_H_
#define _CONF_USB_H_
#include "compiler.h"
//#warning You must refill the following definitions with a correct values
/**
* USB Device Configuration
* @{
*/
//! Device definition (mandatory)
#define USB_DEVICE_VENDOR_ID USB_VID_ATMEL
#define USB_DEVICE_PRODUCT_ID USB_PID_ATMEL_ASF_CDC
#define USB_DEVICE_MAJOR_VERSION 1
#define USB_DEVICE_MINOR_VERSION 0
#define USB_DEVICE_POWER 100 // Consumption on Vbus line (mA)
#define USB_DEVICE_ATTR \
(USB_CONFIG_ATTR_SELF_POWERED)
// (USB_CONFIG_ATTR_BUS_POWERED)
// (USB_CONFIG_ATTR_REMOTE_WAKEUP|USB_CONFIG_ATTR_SELF_POWERED)
// (USB_CONFIG_ATTR_REMOTE_WAKEUP|USB_CONFIG_ATTR_BUS_POWERED)
//! USB Device string definitions (Optional)
#define USB_DEVICE_MANUFACTURE_NAME "Atmel"
#define USB_DEVICE_PRODUCT_NAME "CDC Virtual COM Port"
// #define USB_DEVICE_SERIAL_NAME "12...EF"
//! Number of communication port used (1 to 3)
#define UDI_CDC_PORT_NB 1
//! Interface callback definition
#define UDI_CDC_TX_EMPTY_NOTIFY(port)
#define UDI_CDC_SET_CODING_EXT(port,cfg)
#define UDI_CDC_SET_DTR_EXT(port,set)
#define UDI_CDC_SET_RTS_EXT(port,set)
#define UDI_CDC_ENABLE_EXT(port) my_callback_cdc_enable()
extern bool my_callback_cdc_enable(void);
#define UDI_CDC_DISABLE_EXT(port) my_callback_cdc_disable()
extern void my_callback_cdc_disable(void);
#define UDI_CDC_RX_NOTIFY(port) my_callback_rx_notify(port)
extern void my_callback_rx_notify(uint8_t port);
// #define UDI_CDC_TX_EMPTY_NOTIFY(port) my_callback_tx_empty_notify(port)
// extern void my_callback_tx_empty_notify(uint8_t port);
// #define UDI_CDC_SET_CODING_EXT(port,cfg) my_callback_config(port,cfg)
// extern void my_callback_config(uint8_t port, usb_cdc_line_coding_t * cfg);
// #define UDI_CDC_SET_DTR_EXT(port,set) my_callback_cdc_set_dtr(port,set)
// extern void my_callback_cdc_set_dtr(uint8_t port, bool b_enable);
// #define UDI_CDC_SET_RTS_EXT(port,set) my_callback_cdc_set_rts(port,set)
// extern void my_callback_cdc_set_rts(uint8_t port, bool b_enable);
//! Define it when the transfer CDC Device to Host is a low rate (<512000 bauds)
//! to reduce CDC buffers size
#define UDI_CDC_LOW_RATE
//! Default configuration of communication port
#define UDI_CDC_DEFAULT_RATE 115200
#define UDI_CDC_DEFAULT_STOPBITS CDC_STOP_BITS_1
#define UDI_CDC_DEFAULT_PARITY CDC_PAR_NONE
#define UDI_CDC_DEFAULT_DATABITS 8
//@}
//@}
//! The includes of classes and other headers must be done at the end of this file to avoid compile error
#include "udi_cdc_conf.h"
#endif // _CONF_USB_H_
Note that I have removed some comments and commented-out code; if you want to clearly see all the modifications, you can use a diff tool (e.g., Diffchecker) to compare my file to the one originally generated by Atmel Studio when you added the ASF module. The most confusing part, in my opinion, is this:
//! Interface callback definition #define UDI_CDC_TX_EMPTY_NOTIFY(port) #define UDI_CDC_SET_CODING_EXT(port,cfg) #define UDI_CDC_SET_DTR_EXT(port,set) #define UDI_CDC_SET_RTS_EXT(port,set) #define UDI_CDC_ENABLE_EXT(port) my_callback_cdc_enable() extern bool my_callback_cdc_enable(void); #define UDI_CDC_DISABLE_EXT(port) my_callback_cdc_disable() extern void my_callback_cdc_disable(void); #define UDI_CDC_RX_NOTIFY(port) my_callback_rx_notify(port) extern void my_callback_rx_notify(uint8_t port); // #define UDI_CDC_TX_EMPTY_NOTIFY(port) my_callback_tx_empty_notify(port) // extern void my_callback_tx_empty_notify(uint8_t port); // #define UDI_CDC_SET_CODING_EXT(port,cfg) my_callback_config(port,cfg) // extern void my_callback_config(uint8_t port, usb_cdc_line_coding_t * cfg); // #define UDI_CDC_SET_DTR_EXT(port,set) my_callback_cdc_set_dtr(port,set) // extern void my_callback_cdc_set_dtr(uint8_t port, bool b_enable); // #define UDI_CDC_SET_RTS_EXT(port,set) my_callback_cdc_set_rts(port,set) // extern void my_callback_cdc_set_rts(uint8_t port, bool b_enable);
What you see here are various USB-related events to which you might want to attach a callback function. A callback function is like an interrupt service routine in that it is invoked in response to a meaningful event. The difference is that an interrupt service routine executes as a direct result of a hardware interrupt, whereas a callback function is called by the stack as a way of allowing the main application to deal with various events that are handled by the stack. As you can see, we are using only three of the seven events listed in the above code excerpt. In this particular application, I don’t need to be notified when a buffer of data is transmitted to the PC, so I did not uncomment the following lines:
// #define UDI_CDC_TX_EMPTY_NOTIFY(port) my_callback_tx_empty_notify(port) // extern void my_callback_tx_empty_notify(uint8_t port);
I do, however, want to be notified 1) when the CDC interface is ready for data transfers, 2) when the CDC interface has lost the ability to perform data transfers, and 3) when bytes received from the PC are ready to be processed. Thus, I uncommented the following preprocessor definitions and function prototypes:
#define UDI_CDC_ENABLE_EXT(port) my_callback_cdc_enable() extern bool my_callback_cdc_enable(void); #define UDI_CDC_DISABLE_EXT(port) my_callback_cdc_disable() extern void my_callback_cdc_disable(void); #define UDI_CDC_RX_NOTIFY(port) my_callback_rx_notify(port) extern void my_callback_rx_notify(uint8_t port)
ASCII Commands: If Not Broken, Don’t Fix
This project gives us an opportunity to employ a simple, reliable, effective (though not exactly sophisticated) command interface that stretches way back into the good old days of RS-232. We will transfer all data in the form of ASCII codes—letters and numbers, along with the space and carriage return characters. Each command has three components: a two-letter string that indicates the type of command, one or more arguments, and a carriage return to unambiguously identify the end of the command. I say “unambiguously” because all data, including numerical data, is encoded as ASCII characters, and thus we know that the carriage return (corresponding to the number 13) will never appear anywhere except at the end of the message—even if we have to send the number 13, this will be encoded as 0x31 (ASCII ‘1’) followed by 0x33 (ASCII ‘3’). The same applies to any other white-space characters we might wish to use, such as space, line feed, or tab.
For this project we will implement just two command types: “set position” and “move to position”; the corresponding command strings are SP and MP. The SP command is used to move the servo shaft as quickly as possible to a specified angular position; it takes one argument. The MP command moves the servo shaft to a specified angular position in a specified amount of time; it takes two arguments. The space character is used between the command string and the first argument and, if applicable, between the first argument and the second argument. The final character is always a carriage return. The following table gives the details for each command:
Command |
Command String |
Argument 1 |
Argument 2 |
set position |
SP |
‘L’ for extreme left, ‘C’ for center, ‘R’ for extreme right, or 0–180 for angular position in degrees |
n/a |
move to position |
MP |
0–180 for angular position in degrees |
1–9 for travel time in seconds |
For example, let’s say we want to do the following: move the shaft to extreme left as quickly as possible, then to 45° as quickly as possible, then to 135° as quickly as possible, then to 0° in 2 seconds, then to 180° in 9 seconds. Here is the corresponding sequence of commands (\r represents the carriage-return byte):
- SP L\r
- SP 45\r
- SP 135\r
- MP 0 2\r
- MP 180 9\r
Firmware
You can use the following link to download all the source and project files:
ServoControl_via_USB_Part2.zip
There’s quite a bit of code involved here, so I can’t cover every detail; you should be able to understand most of it with the help of the comments and descriptive identifiers. Mainly I want to discuss the USB code, since USB communication is the focus of this article.
After you have properly modified the USB configuration file (“conf_usb.h”), you need three function calls to make the USB interface ready for action:
/*Initialize interrupt vectors and enable interrupts globally.
These statements are necessary because the USB stack
uses interrupts.*/
irq_initialize_vectors();
cpu_irq_enable();
//make the USB stack active
udc_start();
After performing any other initialization and configuration tasks, you need to wait until the USB interface is ready to send and receive data. This can be accomplished as follows:
/*When a connection is established between the host and
the device, the USB stack calls my_callback_cdc_enable().
In this callback, CDC_TRANSFERS_AUTHORIZED is set to "true".*/
while(CDC_TRANSFERS_AUTHORIZED == false);
Important note: The CDC_TRANSFERS_AUTHORIZED flag must be declared as “volatile”:
//must be volatile
volatile bool CDC_TRANSFERS_AUTHORIZED = false;
The volatile keyword tells the compiler that this variable can be modified in an “unexpected” way—i.e., at any time and without any apparent connection to nearby code. (In this case, the “unexpected” modification occurs in the my_callback_cdc_enable() function.) If you don’t tell the compiler that this variable is volatile, the assembly code that it generates for the while statement will fail because it will be based on an incorrect assumption, namely, that there is no way for CDC_TRANSFERS_AUTHORIZED to change once program execution enters the while statement.
As mentioned above, we are using three USB callbacks:
/*This function is called by the stack when the CDC interface is ready for data transfers.*/
bool my_callback_cdc_enable(void)
{
CDC_TRANSFERS_AUTHORIZED = true;
return true;
}
/*This function is called by the stack when data can no longer be transferred
because the USB device was unplugged or reset by the USB host.*/
void my_callback_cdc_disable(void)
{
CDC_TRANSFERS_AUTHORIZED = false;
}
/*This function is called by the stack when received data is available.*/
void my_callback_rx_notify(uint8_t port)
{
iram_size_t Number_of_Rcvd_Bytes;
Number_of_Rcvd_Bytes = udi_cdc_get_nb_received_data();
udi_cdc_read_buf(USB_Rcv_Buffer, Number_of_Rcvd_Bytes);
//if the packet ends with a carriage return, we assume it is valid
if (USB_Rcv_Buffer[Number_of_Rcvd_Bytes - 1] == ASCII_CR)
{
PROCESS_RCVD_PACKET = true;
}
}
Actually, we don’t really need my_callback_cdc_disable() because this application only responds to commands from the host, and it’s safe to assume that the USB interface is active if we just received a message via USB. If you develop a USB project in which the microcontroller sends USB messages without first receiving something from the host, you can confirm that the USB connection is still active by checking the CDC_TRANSFERS_AUTHORIZED flag.
After performing the action specified by a command, the firmware responds as follows:
//send the response indicating "command accepted"
udi_cdc_putc('1'); udi_cdc_putc(ASCII_CR);
Here’s an important detail: As you can see, we send a two-byte message by making two calls to the udi_cdc_putc() function. The ASF CDC interface includes a function for sending multiple bytes, namely, udi_cdc_write_buf()—why use udi_cdc_putc() instead? Simple: As far as I can tell, udi_cdc_write_buf() doesn’t work. Actually, it caused my firmware to crash, and it even interfered with the terminal program running on the PC. A Google search indicates that there may be a known bug with udi_cdc_write_buf(), though presumably the problem is limited to specific situations (otherwise Atmel would have fixed it by now, it seems to me). So if you try udi_cdc_write_buf() and notice strange behavior, switch over to udi_cdc_putc().
Results
You can check the Device Manager to determine the COM port number to use when connecting to the SAM4S:
The following screenshot gives you an example of how to use Realterm for sending servo commands:
And here is a video that shows the servo action generated by the example command sequence given above, repeated here for your convenience:
- SP L\r
- SP 45\r
- SP 135\r
- MP 0 2\r
- MP 180 9\r
Give this project a try for yourself! Get the BOM.
Thanks, It is very interesting and helpful! I would like to know more knowledge and examples for ATMEL SAM based chip applications. If you have other publishes related with SAM chips, please let me known.
I am working on SAME70 family now for my projects, and wish to find more examples for those chip’s applications.
Thanks again!