Pages

Monday 27 January 2014

Standalone Project: Multi functional Clock

I would like to share my project with you. It's an RTC clock with thermometer powered by atmega328p.

Clock, displaying current time, date & day of week.
It took me a while to finish this project since I wanted to polish the software. I wanted to implement a couple of features which will make my clock more interesting than any other similar device like that build before. I'll try to go through all the details now so, if you find this project interesting you can build it yourself (everything. including the software, is available for free).

Hardware 

The hardware itself is pretty simple. I used a ds18b20 digital One-Wire temperature sensor as a thermometer, a DS1307 RTC module which comes with a battery and a classic hd44780 LCD 16x2 display. Everything is pretty generic in that matter. There's a diode as well used to indicate that the clock is running and a couple of buttons. Besides that I use a couple of transistors in order to be able to control the LCD's brightness and contrast through software, using PWM timer. This is the schematics:

Clock schematics


I've taken some pictures during the building:

All elements populated, making the connections on the perf board.

Cutting the connections on the universal board.

View from the side.

Making the connections.

Completed device.

Software

As usual, the software is written purely in C - WITHOUT any 3rd party libraries. The code is 100 % mine - that includes the I2C driver, LCD driver, Software One Wire driver - all available in the libpca library. The software itself is pretty complicated as for a project like this it contains two state machines; the main one and the temperature measurement dedicated, handles two interrupt sources (INT0 from RTC, as well as Timer overflow interrupt) a fully independent menu system which can be used in any other project and some other additional routines.

Features:
  • Event driven state machine - buttons, interrupts, FSM itself, generate events
  • PWM controlled LCD backlight brightness
  • PWM controlled LCD contrast
  • LCD backlight fade-in/fade-out on keypress (handled in interrupt)
  • Multiple information screens (scrolling from one to another)
  • Independent scrolling strings
  • Animated transitions between the "screens"
  • The clock maintains all the data like current time and temperature measurements (maximum temperature / minimum temperature) without the external power (stored in RTC)
  • Displays a proverbs for every day of year
  • Displays a unique nameday for every day of year
  • Software "integration" key de-bouncing algorithm
The software is 31 kB is size - it takes almost whole available FLASH space. The code itself is somewhere around 15 kB, but since I wanted to display an occasional proverbs and name-days information those additional assets consume the remaining space (it's quite a lot considering the fact that you need at least one (let's say 16 bytes long only) for every day of the year 16 * 365 ~ 6 kB !!!.


Software Functional Block Diagram.

Although the software may look over complicated it is acutally very simple. Let's talk about the interrupts first. I use the RTC's square wave output as an external interrupt (INT0) source. Thanks to that I don't have to constantly poll the RTC in order to update the time - since it will only change once every second. The rest of the time I can use the CPU for any different task. This 1 Hz interrupt is crutial to the system. It generates 1 Hz event in order to update the time, decrements the internal timers - so for example I can program a variable to a value and I know it will be decremented with every second - this can be used to generate timeouts and I use it to generate screen timeouts. For example, once the finite state machine enters Time Display mode the timer is programmed to i.e. 30 seconds - this timer is decremented in the 1 Hz interrupt handler. Once zero a TIMEOUT event will be generate which will force the FSM to transite into another screen (like temperature display or any other). It is used as well to control the backlight on time. If the backlight time is configured to a fixed value (like 30 seconds) the backlight timer is decremented in the 1 Hz interrupt handler if there is no keypress. The 1 Hz interrupt triggers the temperature measurement as well.

Temperature measurement

Temperature measurement is kind of tricky, since in order to get the results I need to wait for around 800 ms in for the sensors to finish the conversion. There were two solutions really - do it with every second - it will synchronous with the clock or do it asynchronously - I wanted to try the second one. This method is slightly more complicated and requires the second Timer interrupt to come into play. In order to synchronize two interrupts and a main loop a small finite state machine dedicated to temperature measurement had to be created. Bellow diagram depicts the process of triggering the temperature sensor and obtaining the results.

Temperature measurement.

Timer interrupt & Screen Transition

The timer interrupt is needed for other purposes. I use to for transitions between screens and for dimming in/out the backlight of the screen (change the PWM duty cycle from/to programmed one from/to zero).

In order to realize the transitions between screens I use a nice property of the hd44780 display. Although it is 16x2 characters it has a lot more DISPLAY RAM it is actually 2 x 40 characters. What I do is during the transition generate the content of for example time display screen to the display RAM at zeroth character and generate the second screen at character 17th (currently outside of the display frame), during the transition I simply move the display frame from 0 to 17 changing the screen content in result


Screen Transition

Menu system

Menu system has been implemented as a separate module. With a small adjustments it's possible to use it in any different project. The menu definition looks the following way:

struct menu_item {
const char *name;

// 1 - inactive/active
// 0 - cb/submenu
uint8_t config;

union {
menu_callback_t cb;
struct menu *submenu;
} ptr;
};

Each item has a name, and respective callback pointer called once the item has been selected. The menu definition for the clock looks the following way:

static struct menu_item items[] = {
{ "Set Time", MENU_ITEM_OWNER, { menu_set_time } }, 
{ "Set Date", MENU_ITEM_OWNER, { menu_set_date } }, 
{ "Time Mode",MENU_ITEM_DEFAULT, { menu_set_time_mode } }, 
{ "LCD Brightness", MENU_ITEM_DEFAULT, { menu_set_lcd_brightness } }, 
{ "LCD Contrast", MENU_ITEM_DEFAULT, { menu_set_lcd_contrast } },
{ "LCD Backlight Time", MENU_ITEM_DEFAULT, { menu_set_lcd_backlight } }, 
{ "Reset temperature", MENU_ITEM_DEFAULT, { menu_reset_temperature } },
{ "Temperature Display Time", MENU_ITEM_DEFAULT, { menu_set_temp_disp_time } },
{ "Time Display Time", MENU_ITEM_DEFAULT, { menu_set_time_disp_time } },
{ "Nameday Display Time", MENU_ITEM_DEFAULT, { menu_set_nameday_disp_time } },
{ "Words Of Wisdom Display Time", MENU_ITEM_DEFAULT, { menu_set_wow_disp_time } },
{ "Save Settings to EEPROM", MENU_ITEM_DEFAULT, { menu_save_settings } }
};

Of course if any string is longer than the the display line, the string will be scrolled. The implementation for that is generic and modular as well so can be taken straight away.

Overview

Clock displaying nameday.

Picture bellow shows the transition in progress, from the Time Screen to the Settings Menu. It also depicts how long does it take to switch a character in this LCD, since the new character is overlaying on the remnants of the old one.

Transition from Menu Screen to Time Screen

Menu Screen

More options in the Menu Screen

Long Option name is being scrolled in the menu screen.

Transition in progress.

Simple progress bar implemented for the menu.

Brightness regulation.

Video demonstration:



As usual, the source code as well as all the documentation is available on my github account:

git clone git@github.com:dagon666/avr_Libpca pca
git clone git@github.com:dagon666/avr_ArduinoProjects projects

Look for subdirectory projects/clk.

Wednesday 1 January 2014

Simple Serial Port Command Line Interface (CLI)

It's often very useful to have some sort of interface through which a basic management can be done of our application. Whether you're designing a giant Marquee with a LED display or a plotter or you simply need to get some diagnostics from your device occasionally, a simple CLI system can come very handy. 

Of course, designing something similar to "bash" or any other unix shell is completely out of scope since those applications are huge and are simply an overkill for our needs. It's pretty simple though to create some basic, yet flexible & easilly extensible CLI system.

First thing needed is a command type definition. This will bind a keyword with an actual underlying routine executed for that keyword typed in the CLI.


typedef struct _t_cmd {
const char *cmd;
void (*fh)(void*);
} t_cmd;


The command type is pretty simple. There's the CLI command/keyword pointer, it holds a pointer to the execution function and that's it.

OK, so far so good. Now we need to define a set of commands since a single command CLI isn't much useful. Let's create a CLI context type for that which will hold entire CLI state - this has two advantages, first - it aggregates all the data in one place, second - if we'll write our functions to depend only on the context (which I'm going to do), then it will be possible to declare a couple of independent CLIs on different interfaces for example running concurrently.


typedef struct _t_cli_ctx {
// command set
t_cmd *cmds;

// cmd
char cmd[CLI_CMD_BUFFER_SIZE];
unsigned char cpos;

} t_cli_ctx;


OK, the context is pretty simple. It contains the command set pointer and current command buffer "cmd" - this buffer will hold everything you type before hitting ENTER and interpreting the command. It contains current cursor position as well. Once again, at the moment the context is pretty simple - but it will be enhanced later on to implement some more features.


static t_cmd g_cmds[] = {

{ "hw", fh_hello },
{ "hi", fh_hi }

// null
{ {0x00}, 0x00, 0x00  }
};


Let's prepare an initialization function for the CLI context definition in order to prepare it for execution.


void cli_init(t_cli_ctx *a_ctx, t_cmd *a_cmds) {
memset(a_ctx, 0x00, sizeof(t_cli_ctx));
a_ctx->cmds = a_cmds;
}


Believe it or not but more or less that's it. We can now declare some commands and proceed with the implementation of the CLI interpreter. We haven't yet used or done anything hardware specific so, the code can be compiled on a usual x86 machine for tests. The only platform specific code will be IO related. We need to create it now in order to proceed. For the AVR it will be quite simple. Our CLI system needs to be fed one key at a time. We can use a function from libpca:

unsigned char serial_getc(unsigned char *a_data);

It returns one if there is a character available and place it in the pointer provided. In order to perform some offline tests a similar function for x86 Linux terminal is needed. Unfortunately such function doesn't exist. There are two options


  1. Use ncurses which implements getch() function - to retrieve a single character.
  2. Disabled so called "canonical mode" for a standard linux terminal and use getchar() in non blocking mode.


Let's focus on the second approach. Ncurses is a bit of an overkill as far as I'm concerned though using it would be platform independent - but it's only a test application so, it's not that important. Let's have a look on the test program bellow.


I implemented here two functions cm_off & cm_on in order to turn the "canonical" mode on & off. When this mode is turned off - I'm able to get a direct input using the getchar() routine without having to press ENTER after every key. Our final linux_getc() along with linux_putc() routine will look the following way:


Let's implement the CLI interpreter itself now.


It doesn't do much at the moment. In fact it doesn't even run the commands. All it does is handle some basic input. We echo back the incoming characters, interpret backspace key and the ENTER key, printing some fancy prompt:

$ ./a.out 

#> command

#> some other cmd 

#> aaa

#> 

That is not very exciting in particular. Let's implement a command interpretation first:


I'll place that function in the KEY_CODE_ENTER switch case:


case KEY_CODE_ENTER: // new line
a_ctx->cmd[POSINC(a_ctx->cpos)] = '\0';
CLI_IO_OUTPUT("\r\n", 2);

res = _cli_interpret_cmd(a_ctx);

a_ctx->cpos = 0;
memset(a_ctx->cmd, 0x00, CLI_CMD_BUFFER_SIZE);
_cli_prompt(a_ctx, 1);
break;


I need some dummy commands implementation as well:

static void fh_hw(void *a_data) {
CLI_IO_OUTPUT("hello_world", 11);
}

static void fh_hi(void *a_data) {
CLI_IO_OUTPUT("hi", 2);
}

Let's do the test

./a.out 
#> hw
hello_world
#> hi
hi
#> test

#> 

Looks good, although there are is a problem - there is no info that a particular command is unknown. This must be fixed. Let's create another function and place it just after _cli_interpret_cmd in the switch case for ENTER key.


The switch case for ENTER will look the following way now:

case KEY_CODE_ENTER: // new line
a_ctx->cmd[POSINC(a_ctx->cpos)] = '\0';
CLI_IO_OUTPUT("\r\n", 2);

res = _cli_interpret_cmd(a_ctx);
_cli_reinterpret_cmd(a_ctx, res);

a_ctx->cpos = 0;
memset(a_ctx->cmd, 0x00, CLI_CMD_BUFFER_SIZE);
_cli_prompt(a_ctx, 1);
break;

Let's test it:

./a.out

#> test

Command not found
#> other

Command not found
#> hw
hello_world
#>

That's starting to look very good. At the moment we have a functional very basic CLI system. Let's consider what else is missing. When pressing an arrow keys nothing happens - we are unable to correct any mistakes in the middle of the command. This can be implemented. But first, let's checkout the key codes produced by those keys. On my terminal those are:


  • Left: 1b 5b 44
  • Right: 1b 5b 43
  • Up: 1b 5b 41
  • Down: 1b 5b 42


They may be completely different on yours. It's all terminal dependent, things like character encoding and terminal settings have a major influence here. That complicates the situation slightly, since a single key produces 3 codes and the key itself can be distinguished only by the third one. Our present implementation is unable to cope with that kind of input. Let's define a structure for that kind of input representing a single key

#define MULTICODE_INPUT_MAX_LEN 5

typedef struct _t_multicode_input {
unsigned char buf[MULTICODE_INPUT_MAX_LEN];
unsigned char pos;
} t_multicode_input;


... and a related command:


typedef struct _t_multicode_cmd {
unsigned char pattern[MULTICODE_INPUT_MAX_LEN];
unsigned char len;
void (*fh)(void*);
} t_multicode_cmd;


OK. The "input code" can be up to 5 characters long. What I want to do is to simply read the whole multi-code input and compare it against known ones in order to trigger some actions. First the CLI context structure must be extended:


typedef struct _t_cli_ctx {
// command set
t_cmd *cmds;
t_multicode_cmd *mcmds;

// cmd
char cmd[CLI_CMD_BUFFER_SIZE];
unsigned char cpos;

// multicode input
t_multicode_input mchar;
} t_cli_ctx;


The interpreter must be extended as well:


What I'm doing here is simply matching a whole multi-code input sequence against the known ones and fire an action if there is a match. Still, there's no associated actions defined. Let's create a couple:

t_multicode_cmd g_mc_cmds[] = {

{ { 0x1b, 0x5b, 0x44 }, 3, _cli_key_left },
{ { 0x1b, 0x5b, 0x43 }, 3, _cli_key_right },

// null
{ {0x00}, 0, 0x00}
};

static void _cli_key_left(void *a_data) {
t_cli_ctx *ctx = (t_cli_ctx *)a_data;

if (ctx->cpos) {
ctx->cmd[ctx->cpos--] = '\0';
CLI_IO_OUTPUT("\b \b", 3);
}
}

static void _cli_key_right(void *a_data) {
}

Just to make things simple - the left arrow key will behave the same way as backspace key. The right key won't do anything. That's great we can now handle even more complex input type. Let's think what else is still missing ?

What happends when you hit an up arrow key in bash ? It brings back the previous command. Some simple history implementation would be very fancy and would make the CLI system very attractive. Let's implement it as well. First some additional data fields are needed in the CLI context. Let's have a look on it once again:

#define CLI_CMD_HISTORY_LEN 8

typedef struct _t_cli_ctx {
// command set
t_cmd *cmds;
t_multicode_cmd *mcmds;

// cmd
char cmd[CLI_CMD_BUFFER_SIZE];
unsigned char cpos;

// history
char history[CLI_CMD_HISTORY_LEN][CLI_CMD_BUFFER_SIZE];
unsigned char hpos;
unsigned char hhead, htail;

// multicode input
t_multicode_input mchar;
} t_cli_ctx;


The CLI will hold last 8 commands typed into it. After pressing ENTER we need to save current command line into the history. Let's do it in the _cli_reinterpret_cmd function to sift any junk input from contaminating the history:


Handlers for up/down arrow keys are needed to make this feature complete:


Now, those commands must be attached to our multicode_input command array.


t_multicode_cmd g_mc_cmds[] = {

{ { 0x1b, 0x5b, 0x44 }, 3, _cli_key_left },
{ { 0x1b, 0x5b, 0x43 }, 3, _cli_key_right },

{ { 0x1b, 0x5b, 0x41 }, 3, _cli_history_up },
{ { 0x1b, 0x5b, 0x42 }, 3, _cli_history_down },

// null
{ {0x00}, 0, 0x00}
};

And we're ready to go. Let's test it.

$ ./a.out

#> command1

Command not found
#> cmd2

Command not found
#> cmd3

Command not found
#>
(3/8)cmd3
(2/8)cmd2
(1/8)command1

Command not found
#>
(4/8)command1
(3/8)cmd3

Command not found
#>

That seems to work very well. Our CLI starts to look very attractive now. One thing is still missing though - the basic command argument support. I left it for the end deliberately. Let's not forget that the final product will run on a small micro-controller with not much RAM on board. The only generic useful information would be an argc - an integer defining how many space delimited words have been typed into the CLI. If the command really needs to parse arguments then it should do it itself, just to save resources. Let's modify the code for _cli_interpret_cmd first and add a call to the argument counting routine:

a_ctx->argc = _cli_count_arguments(a_ctx);

The routine itself:


Let's write a test command for that as well:

{ "argc", fh_argc },


static void fh_argc(void *a_data) {
char tmp[16] = { 0x00 };
t_cli_ctx *ctx = (t_cli_ctx *)a_data;
sprintf(tmp, "argc: %d", ctx->argc);
CLI_IO_OUTPUT(tmp, strlen(tmp));
}

and test it:

$ ./a.out

#> argc
argc: 1
#> argc a1 a2 a4
argc: 4
#>

That works fine as well. At this stage we are done with testing on the x86 platform and we can move the code to the MCU itself. It's a good point to summarize the complete code so far. I reorganized the whole thing and split the code into files



The test application can be compiled without a Makefile, the following way:

gcc main.c linux_io.c cli.c -I. -o cli

Moving the CLI to the MCU


There's not much to do actually since the CLI has been written with a hardware independent code. However there are some gotchas which will come out later on. First let's prepare the project, change the IO routines for the CLI, compile the project and try to run it. Of course, I'm going to use the serial routines implemented in libpca for the IO. The IO definitions look the following way:


/**
 * @brief IO input routine - change it accordingly to your implementation
 */
#define CLI_IO_INPUT(__data) \
serial_getc(__data)


/**
 * @brief IO output routine - change it accordingly to your implementation
 */
#define CLI_IO_OUTPUT(__data, __len) \
serial_poll_send(__data, __len)

After flashing the arduino and connecting a minicom terminal, first thing to notice is lack of any response for ENTER key. Minicom emulates VT102 terminal and sends only a 0x0d code for a new line character, the devinition for KEY_CODE_ENTER must be changed accordingly. Let's recompile and verify the rest of the keys. The arrow keys works. The backspace key and the delete key don't work though, again it's the matter of the incorrect keycodes. We can discover those by echoing them back after receiving:

sprintf(tmp, "%02x\n", i);
CLI_IO_OUTPUT(tmp,strlen(tmp));

Minicom on my machine sends 0x08 code for backspace and a set of codes for backspace key: 0x1b 0x5b 33 7e. The code needs to be adjusted slightly. First the value of KEY_CODE_BACKSPACE must be corrected. The DELETE key needs to be interpreted in "multicode" fashion. I'll attach the same action to it as for BACKSPACE and left arrow key:

{ { 0x1b, 0x5b, 0x33, 0x7e }, 4, _cli_delete }, // delete

Let's recompile and test once again. Everything seems to work now. The basic CLI code is complete. This code proves how much CLI systems and interpreters are terminal dependent. We can either try to be as much compliant and flexible with most of popular terminal emulators or simply require from the user to use only one specific type i.e. VT100 / VT102 or any other.

As usual a set of sources can be downloaded either from my google drive or from github. Snapshot, containing a version of libpca as well as the source code discussed in this post is available here

The code is available in my GitHub repositories. First you need libpca:

git clone git@github.com:dagon666/avr_Libpca pca

and my projects repository:

git clone git@github.com:dagon666/avr_ArduinoProjects projects

Navigate to projects/cli_mcu to find the source code.