Reflow oven controller firmware
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

970 lines
21 KiB

/* menu.c */
//#include <stdio.h>
#include <stddef.h>
#include <string.h>
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <util/atomic.h>
#include "common.h"
#include "menu.h"
#include "display.h"
#include "timer.h"
#include "therm.h"
#include "config.h"
#include "beep.h"
#include "fields.h"
#include "profile.h"
#include "control.h"
uint8_t menu_current;
uint8_t menu_row;
uint8_t field_row;
uint8_t menu_next;
uint8_t menu_redraw;
bool edit_dirty;
#define REDRAW_NONE 0
#define REDRAW_FIELDS 1
#define REDRAW_ALL 2
uint8_t run_status;
bool menu_editing;
uint8_t menu_editing_field;
menu_t *menu_current_p;
const char str_b[] PROGMEM = "B";
const char str_e[] PROGMEM = "E";
const char str_j[] PROGMEM = "J";
const char str_k[] PROGMEM = "K";
const char str_n[] PROGMEM = "N";
const char str_r[] PROGMEM = "R";
const char str_s[] PROGMEM = "S";
const char str_t[] PROGMEM = "T";
PGM_P const enum_thermocouple_types[] PROGMEM = {
str_b,
str_e,
str_j,
str_k,
str_n,
str_r,
str_s,
str_t
};
const char str_negative[] PROGMEM = "-";
const char str_positive[] PROGMEM = "+";
PGM_P const enum_posneg[] PROGMEM = {
str_negative,
str_positive
};
const char str_off[] PROGMEM = "Off";
const char str_on[] PROGMEM = "On";
PGM_P const enum_boolean[] PROGMEM = {
str_off,
str_on
};
const char str_50hz[] PROGMEM = "50Hz";
const char str_60hz[] PROGMEM = "60Hz";
PGM_P const enum_frequency[] PROGMEM = {
str_50hz,
str_60hz
};
const char str_degc[] PROGMEM = "\047C";
const char str_degf[] PROGMEM = "\047F";
/* str_k already defined */
const char str_degde[] PROGMEM = "\047De";
const char str_degn[] PROGMEM = "\047N";
const char str_degr[] PROGMEM = "\047R";
const char str_degre[] PROGMEM = "\047Re";
const char str_degro[] PROGMEM = "\047Ro";
const char str_mev[] PROGMEM = "meV";
#define TEMPERATURE_CELSIUS 0
#define TEMPERATURE_FAHRENHEIT 1
#define TEMPERATURE_KELVIN 2
#define TEMPERATURE_DELISLE 3
#define TEMPERATURE_NEWTON 4
#define TEMPERATURE_RANKINE 5
#define TEMPERATURE_REAUMUR 6
#define TEMPERATURE_ROMER 7
#define TEMPERATURE_MEV 8
PGM_P const enum_units[] PROGMEM = {
str_degc,
str_degf,
str_k,
str_degde,
str_degn,
str_degr,
str_degre,
str_degro,
str_mev
};
const char str_seconds[] PROGMEM = "s";
const char str_minutes[] PROGMEM = "m";
const char str_hours[] PROGMEM = "h";
PGM_P const enum_time_units[] PROGMEM = {
str_seconds,
str_minutes,
str_hours
};
const char str_0[] PROGMEM = "0";
const char str_1[] PROGMEM = "1";
const char str_2[] PROGMEM = "2";
const char str_3[] PROGMEM = "3";
const char str_4[] PROGMEM = "4";
const char str_5[] PROGMEM = "5";
const char str_6[] PROGMEM = "6";
const char str_7[] PROGMEM = "7";
const char str_8[] PROGMEM = "8";
const char str_9[] PROGMEM = "9";
PGM_P const enum_digits[] PROGMEM = {
str_0,
str_1,
str_2,
str_3,
str_4,
str_5,
str_6,
str_7,
str_8,
str_9,
};
const char str_start[] PROGMEM = "Start?";
const char str_running[] PROGMEM = "Run";
const char str_finished[] PROGMEM = "Done";
//const char str_error[] PROGMEM = "Error";
PGM_P const enum_status[] PROGMEM = {
str_start,
str_running,
str_finished
// str_error
};
#define RUN_STATUS_READY 0
#define RUN_STATUS_RUNNING 1
#define RUN_STATUS_FINISHED 2
#define RUN_STATUS_ERROR 3
const char str_empty[] PROGMEM = "";
const char str_fault[] PROGMEM = "FAULT";
//const char str_open[] PROGMEM = "Connect thermocouple";
//const char str_chiprange[] PROGMEM = "Cold junction range";
//const char str_temprange[] PROGMEM = "Thermocouple range";
PGM_P const enum_faultstatus[] PROGMEM = {
str_empty,
str_fault
// str_open,
// str_chiprange,
// str_temprange
};
const char str_run_profile[] PROGMEM = "Run profile";
const char str_edit_profile[] PROGMEM = "Edit profile";
const char str_configure[] PROGMEM = "Configure";
const char str_faultstatus[] PROGMEM = "\x0a";
const char str_oven[] PROGMEM = "Oven \x08\x02\x02\x02\x02.\x02\x04\x04\x04";
const char str_ambient[] PROGMEM = "Ambient \x08\x02\x02\x02\x02.\x02\x04\x04\x04";
PGM_P const menu_main[] PROGMEM = {
str_run_profile,
str_edit_profile,
str_configure,
NULL,
str_faultstatus,
NULL,
str_oven,
str_ambient
};
const char str_placeholder[] PROGMEM = "Placeholder";
const char str_selecting_profile[] PROGMEM = "Selecting profile";
const char str_str[] PROGMEM = "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01";
const char str_editstr[] PROGMEM = "\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11";
PGM_P const menu_select_profile[] PROGMEM = {
str_str,
str_str,
str_str,
str_str,
str_str,
str_str,
str_str,
str_str
};
const char str_editing_profile[] PROGMEM = "Editing profile";
const char str_name_field[] PROGMEM = "> Name field <";
const char str_start_temperature[] PROGMEM = "Start temp \x18\x12\x12\x12\x12\x04<<";
const char str_edit_field[] PROGMEM = "\x02\x02.\x02\x02/ \x12\x12\x12\x12\x13 \x18\x12\x12\x12\x12\x04<<";
PGM_P const menu_edit_profile[] PROGMEM = {
str_editstr,
str_start_temperature,
str_edit_field,
str_edit_field,
str_edit_field,
str_edit_field,
str_edit_field,
str_edit_field,
};
const char str_contrast[] PROGMEM = "Contrast \x12\x12\x12";
const char str_beep[] PROGMEM = "Beep \x15>>";
const char str_thermocouple[] PROGMEM = "Thermocouple \x16";
const char str_frequency[] PROGMEM = "Frequency \x17>>>";
const char str_units[] PROGMEM = "Units \x14>>";
const char str_p[] PROGMEM = "P \x18\x12.\x12\x12";
const char str_i[] PROGMEM = "I \x18\x12.\x12\x12";
const char str_d[] PROGMEM = "D \x18\x12.\x12\x12";
PGM_P const menu_configuration[] PROGMEM = {
str_contrast,
str_beep,
str_thermocouple,
str_frequency,
str_units,
str_p,
str_i,
str_d
};
const char str_running_profile[] PROGMEM = "\x09\x09\x09\x09\x09\x09\x12\x12\x12\x12\x13\x08\x02\x02\x02\x02.\x02\x04<<";
PGM_P const menu_run_profile[] PROGMEM = {
str_running_profile,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL
};
static void main_menu_fields(uint8_t row);
static void configuration_fields(uint8_t row);
static void save_configuration_fields(uint8_t row);
static void select_profile_fields(uint8_t row);
static void edit_profile_fields(uint8_t row);
static void save_profile_fields(uint8_t row);
static void update_run_status(uint8_t row);
menu_t menus[] = {
{ menu_main, 0, 2, main_menu_fields, NULL },
{ menu_select_profile, 0, 7, select_profile_fields, NULL },
{ menu_configuration, 0, 7, configuration_fields, save_configuration_fields },
{ menu_edit_profile, 0, 7, edit_profile_fields, save_profile_fields },
{ menu_run_profile, 7, 7, update_run_status, NULL }
};
#define TEMPERATURE_UNIT 0x1000L
int8_t temp_n[] = {1, 9, 1, -3, 33, 9, 4, 21, 10};
int8_t temp_d[] = {1, 5, 1, 2, 100, 5, 5, 40, 116};
int32_t temp_off[] = {0, 32*TEMPERATURE_UNIT, 1118822, -614400, 0, 2013880, 0, 30720, 96450};
int32_t temperature_from_kelvin(temp_t temp)
{
uint8_t units = config.units;
int32_t temp32 = temp;
int32_t source = ((temp32-2732)<<12) / 10;
int32_t result;
result = source * temp_n[units] / temp_d[units] + temp_off[units];
// printf("temp = %d, source = %d (%x), result = %d (%x)\n", temp, source, source, result, result);
return result;
}
temp_t temperature_to_kelvin(int32_t source)
{
uint8_t units = config.units;
int32_t result = (source - temp_off[units]) * temp_d[units] / temp_n[units];
return therm_reduce(result);
}
static void update_run_status(uint8_t row)
{
temp_t temp = therm_temp();
write_field_enum(0, run_status);
write_field_uint16(1, control_now());
write_field_enum(2, 0);
write_field_temperature(3, TRUE, temp);
}
static void main_menu_fields(uint8_t row)
{
temp_t temp;
switch (row) {
case 4:
write_field_enum(0, therm_fault()?1:0);
return;
case 6:
temp = therm_temp();
break;
case 7:
temp = therm_coldtemp();
break;
default:
return;
}
write_field_temperature(0, TRUE, temp);
}
static void configuration_fields(uint8_t row)
{
if (row == 0) {
write_field_uint8(0, config.contrast);
return;
}
if (row > 4) {
int16_t value16;
int32_t val;
switch (row) {
case 5:
value16 = config.p;
break;
case 6:
value16 = config.i;
break;
case 7:
value16 = config.d;
break;
default:
return;
}
val = (int32_t)value16 << 5;
write_field_fracint(0, TRUE, TRUE, val);
} else {
uint8_t value8;
switch (row) {
case 1:
value8 = config.beep;
break;
case 2:
value8 = config.thermocouple_type;
break;
case 3:
value8 = config.frequency;
break;
case 4:
value8 = config.units;
break;
}
write_field_enum(0, value8);
}
}
static void save_configuration_fields(uint8_t row)
{
if (row == 0) {
config.contrast = read_field_uint8(0);
return;
}
if (row > 4) {
int16_t value16 = read_field_fracint(0, TRUE) >> 5;
switch (row) {
case 5:
config.p = value16;
break;
case 6:
config.i = value16;
break;
case 7:
config.d = value16;
break;
}
} else {
uint8_t value8 = read_field_enum(0);
switch (row) {
case 1:
config.beep = value8;
break;
case 2:
config.thermocouple_type = value8;
break;
case 3:
config.frequency = value8;
break;
case 4:
config.units = value8;
break;
}
}
}
static void select_profile_fields(uint8_t row)
{
profile_select(row);
memcpy(field_values, &profile_p->name, PROFILE_NAME_LENGTH);
}
static void save_profile_fields(uint8_t row)
{
int32_t temp;
temp_t k_temp;
uint8_t n;
if (row == 0) {
memcpy(profile_p->name, field_values, PROFILE_NAME_LENGTH);
return;
}
n = find_field_number(find_editable_field(0, FALSE));
temp = read_field_fracint(n+((row == 1)?0:2), FALSE);
k_temp = temperature_to_kelvin(temp);
set_profile_temperature(k_temp);
if (row == 1)
return;
set_profile_time(row-2, read_field_uint16(n), read_field_enum(n+1));
}
static void edit_profile_fields(uint8_t row)
{
int32_t unit_temp, prev_temp, step;
temp_t temp;
uint16_t time;
uint8_t time_units;
if (row == 0) {
memcpy(field_values, profile_p->name, PROFILE_NAME_LENGTH);
return;
}
temp = get_profile_temperature(row);
write_field_temperature(((row == 1)?0:4), FALSE, temp);
if (row == 1)
return;
unit_temp = temperature_from_kelvin(temp);
prev_temp = temperature_from_kelvin(get_profile_temperature(row-1));
time = profile_get_time(row-2);
time_units = profile_get_time_units(row-2);
if (time == 0)
step = 0;
else
step = (unit_temp - prev_temp) / time;
write_field_fracint(0, FALSE, TRUE, step);
write_field_uint16(2, time);
write_field_enum(3, time_units);
}
#define MENU_MAIN 0
#define MENU_SELECT_PROFILE 1
#define MENU_CONFIGURATION 2
#define MENU_EDIT_PROFILE 3
#define MENU_RUN_PROFILE 4
char menu_getchar(uint8_t col)
{
PGM_P ptr = get_string(&menu_current_p->text[field_row]);
if (!ptr)
return '\0';
if (col > 20)
return '\0';
return pgm_read_byte(ptr + col);
}
field_t fields[] = {
{NULL /* ASCII */, 127-32, 1},
{enum_digits, ARRAY_SIZE(enum_digits), 1},
{enum_time_units, ARRAY_SIZE(enum_time_units), 1},
{enum_units, ARRAY_SIZE(enum_units), 3},
{enum_boolean, ARRAY_SIZE(enum_boolean), 3},
{enum_thermocouple_types, ARRAY_SIZE(enum_thermocouple_types), 1},
{enum_frequency, ARRAY_SIZE(enum_frequency), 4},
{enum_posneg, ARRAY_SIZE(enum_posneg), 1},
{enum_status, ARRAY_SIZE(enum_status), 6},
{enum_faultstatus, ARRAY_SIZE(enum_faultstatus), 21}
};
static void display_field(char c, uint8_t col)
{
uint8_t i;
if (field_is_text(c)) {
if ((field_values[col] >= 32) && (field_values[col] < 32+field_display_entries(c)))
display_putchar(field_values[col]);
else
display_putchar('?');
return;
}
if (field_values[col] >= field_display_entries(c)) {
for (i = 0; i < field_length(c); i++)
display_putchar('?');
} else {
PGM_P str = field_text(c, field_values[col]);
i = 0;
if (field_length(c) > 1) {
i = field_length(c) - strlen_P(str);
if (menu_getchar(col+1) == '>')
for (; i; i--)
display_putchar(' ');
}
display_putstr_P(str);
for (; i; i--)
display_putchar(' ');
}
}
#define menu_draw_current_row() menu_draw_row(menu_row)
static void menu_set_fields(uint8_t row)
{
menu_t *menu = menu_current_p;
field_row = row;
if ((!menu_editing) && menu->get_field)
menu->get_field(row);
}
static void menu_draw_row(uint8_t row)
{
uint8_t col;
display_settextpos(0, row);
display_setinverse((row == menu_row) && !menu_editing);
if (get_string(&menu_current_p->text[row])) {
char c;
menu_set_fields(row);
col = 0;
while ((c = menu_getchar(col)) != '\0') {
if (menu_editing)
display_setinverse(col == menu_editing_field);
if (is_field(c)) {
display_field(c, col);
col += field_length(c);
} else {
display_putchar(c);
col++;
}
}
}
display_clearline();
}
#if 1
static void display_menu(bool all)
{
uint8_t row;
for (row = 0; row < 8; row++) {
field_row = row;
if (all || (find_field(0) != 255))
menu_draw_row(row);
}
}
#endif
void set_menu(uint8_t new_menu)
{
if (new_menu == menu_current)
return;
menu_current = new_menu;
menu_current_p = &menus[new_menu];
menu_redraw = TRUE;
// memcpy_P(&menu_row, &menu_current_p->first_line, sizeof(menu_row));
menu_row = 0;
menu_editing = FALSE;
}
void menu_init(void)
{
/* Make sure set_menu does its work */
menu_current = MENU_MAIN+1;
set_menu(MENU_MAIN);
display_init();
}
#define BUTTON_UP 1
#define BUTTON_DOWN 2
#define BUTTON_LEFT 4
#define BUTTON_RIGHT 3
static void menu_end_edit(void)
{
menu_t *menu = menu_current_p;
menu_editing = FALSE;
if (menu->put_field)
menu->put_field(menu_row);
menu_redraw = TRUE;
}
static void menu_edit_new_field(uint8_t field, bool left)
{
field = find_editable_field(field, left);
menu_editing_field = field;
menu_editing = TRUE;
edit_dirty = FALSE;
if (field == 255)
menu_end_edit();
menu_draw_row(menu_row);
}
static void increment_value(int8_t inc)
{
uint8_t val = field_values[menu_editing_field];
uint8_t c = menu_getchar(menu_editing_field);
uint8_t min = 0;
uint8_t max = field_display_entries(c) - 1;
if (field_is_text(c)) {
min += 32;
max += 32;
}
if (inc < 0 && val == min)
val = max;
else if (inc > 0 && val == max)
val = min;
else
val += inc;
field_values[menu_editing_field] = val;
edit_dirty = TRUE;
}
static void menu_navigate_up(void)
{
if (menu_row == menu_current_p->first_line)
return;
menu_row--;
menu_draw_row(menu_row+1);
menu_draw_row(menu_row);
}
static void menu_navigate_down(void)
{
if (menu_row == menu_current_p->last_line)
return;
menu_row++;
menu_draw_row(menu_row-1);
menu_draw_row(menu_row);
}
static void menu_navigate_into(void)
{
uint8_t menu = menu_current;
switch (menu) {
case MENU_MAIN:
switch (menu_row) {
case 0:
menu = MENU_SELECT_PROFILE;
menu_next = MENU_RUN_PROFILE;
break;
case 1:
menu = MENU_SELECT_PROFILE;
menu_next = MENU_EDIT_PROFILE;
break;
case 2:
menu = MENU_CONFIGURATION;
break;
}
break;
case MENU_SELECT_PROFILE:
menu = menu_next;
profile_select(menu_row);
break;
case MENU_RUN_PROFILE:
control_start();
run_status = RUN_STATUS_RUNNING;
break;
default:
menu_edit_new_field(21, TRUE);
break;
}
set_menu(menu);
}
static void menu_navigate_back(void)
{
uint8_t menu = menu_current;
switch (menu) {
case MENU_EDIT_PROFILE:
profile_save();
case MENU_RUN_PROFILE:
menu = MENU_SELECT_PROFILE;
break;
default:
menu = MENU_MAIN;
break;
}
set_menu(menu);
}
static void menu_process_leftright(bool left)
{
int8_t increment = left?(-1):1;
if (menu_editing)
menu_edit_new_field(menu_editing_field+increment, left);
else {
if (left)
menu_navigate_back();
else
menu_navigate_into();
}
}
static void menu_process_updown(bool up)
{
int8_t increment = up?1:(-1);
if (menu_editing) {
increment_value(increment);
menu_draw_row(menu_row);
} else {
if (up)
menu_navigate_up();
else
menu_navigate_down();
}
}
#define GRAPH_POINTS 128
//#define GRAPH_POINTS 128
//#define GRAPH_POINTS 12
#define GRAPH_START_X 0
#define GRAPH_HEIGHT (GRAPH_PAGES * 8)
#define GRAPH_PAGES 7
#define GRAPH_START_Y 1
//uint8_t graph_desired[GRAPH_POINTS+1];
//uint8_t graph_actual[GRAPH_POINTS+1];
uint8_t graph_actual[4];
static void minmax_mark(uint8_t *min_p, uint8_t *max_p, uint8_t mid)
{
uint8_t min = *min_p;
uint8_t max = *max_p;
if (mid == 255) {
*min_p = 255;
return;
}
if (min == 255)
min = mid;
if (max == 255)
max = mid;
if (max < min) {
uint8_t tmp = min;
min = max;
max = tmp;
}
*min_p = mid - (mid-min)/2;
*max_p = (max-mid)/2 + mid;
}
temp_t graph_temp_min, graph_temp_range;
uint32_t time_per_unit;
uint8_t graph_cursor;
static uint8_t graph_value(temp_t temp);
static uint8_t graph_at(uint8_t col)
{
return graph_value(temperature_at_time(col*time_per_unit));
}
static void graphics_draw_column(uint8_t col)
{
uint8_t data;
uint8_t pr_mid, pr_min, pr_max;
uint8_t act_mid, act_min, act_max;
uint8_t i;
uint8_t page;
uint8_t act_col;
if (col >= GRAPH_POINTS)
return;
if (col == graph_cursor)
act_col = 1;
else
act_col = 0;
pr_mid = graph_at(col);
pr_min = graph_at(col-1);
pr_max = graph_at(col+1);
minmax_mark(&pr_min, &pr_max, pr_mid);
act_min = graph_actual[act_col];
act_mid = graph_actual[act_col+1];
act_max = graph_actual[act_col+2];
minmax_mark(&act_min, &act_max, act_mid);
if (col == 0) {
pr_min = 0;
pr_max = GRAPH_HEIGHT-1;
}
page = GRAPH_PAGES-1;
for (i = 0; i < GRAPH_HEIGHT; i++) {
data <<= 1;
if ((i >= pr_min && i <= pr_max) ||
(i >= act_min && i <= act_max) ||
(i == 0)) {
data |= 1;
}
if ((i & 0x7) == 7) {
if (graph_cursor == col)
data |= 0x55;
display_setposition(col, GRAPH_START_Y + page);
display_data(1, &data);
page--;
}
}
}
static void graphics_draw_screen(void)
{
int i;
/* initialise profile */
for (i = 0; i < GRAPH_POINTS; i++)
graphics_draw_column(i);
}
static uint8_t __attribute__ ((noinline)) graph_value(temp_t temp)
{
int32_t v;
if (temp == INVALID_TEMPERATURE)
v = 255;
else
v = ((int32_t)temp - graph_temp_min) * GRAPH_HEIGHT / graph_temp_range;
return v;
}
static void graphics_init(void)
{
uint8_t i;
uint32_t total_time;
temp_t temp_min, temp_max;
uint8_t *graph_actual_p;
run_status = RUN_STATUS_READY;
temp_min = temp_max = get_profile_temperature(1);
total_time = 0;
for (i = 0; i < 6; i++) {
temp_t temp = get_profile_temperature(i+2);
uint32_t time = profile_time(i);
if (time == 0)
continue;
total_time += time;
if (temp > temp_max)
temp_max = temp;
if (temp < temp_min)
temp_min = temp;
}
time_per_unit = (total_time + GRAPH_POINTS - 1) / GRAPH_POINTS;
temp_min -= 10 * 10;
temp_max += 10 * 10;
graph_temp_range = temp_max - temp_min;
graph_temp_min = temp_min;
graph_cursor = 255;
graph_actual_p = &graph_actual[0];
for (i = 0; i < 4; i++)
*(graph_actual_p++) = 255;
}
static void graphics_poll(void)
{
if (menu_redraw) {
graphics_init();
graphics_draw_screen();
}
if (run_status == RUN_STATUS_RUNNING) {
uint8_t col = control_now() / time_per_unit;
if (col <= GRAPH_POINTS) {
if (col != graph_cursor) {
graph_actual[0] = graph_actual[1];
graph_actual[1] = graph_actual[2];
}
graph_actual[2] = graph_value(therm_temp());
} else {
run_status = RUN_STATUS_FINISHED;
}
graph_cursor = col;
graphics_draw_column(col-1);
graphics_draw_column(col);
}
}
void menu_poll(void)
{
uint8_t button_pressed;
ATOMIC_BLOCK(ATOMIC_FORCEON) {
button_pressed = button;
button = 0;
}
if (button_pressed == BUTTON_LEFT || button_pressed == BUTTON_RIGHT)
menu_process_leftright(button_pressed == BUTTON_LEFT);
if (button_pressed == BUTTON_UP || button_pressed == BUTTON_DOWN)
menu_process_updown(button_pressed == BUTTON_UP);
if (menu_redraw || (menu_current == MENU_RUN_PROFILE) || (menu_current == MENU_MAIN)) {
display_menu(menu_redraw);
menu_set_fields(menu_row);
}
if (menu_current == MENU_RUN_PROFILE) {
graphics_poll();
}
menu_redraw = FALSE;
}