AVRでEEPROMに書き込む
NC旋盤のファームウェア開発を少しずつ進めております。
今回は機械の設定を保存しておく為にEEPROMを使ってみます。
使用するマイコンは毎度お馴染みATmega88です。今回のプログラムはEEPROMの値を読み出してLCDに表示し、変更した値をEEPROMに書き込むという事をしています。
EEPROMとは
EEPROMとはElectrically Erasable Programmable Read-Only Memoryというものだそうです。まあ難しい話は置いといて、電源を落としても記録したデータが残せるメモリです。
ここに機械の設定値を保存しておいてこの値を演算に使ったり、ハードの変更が合った時に設定画面から値を変更したりします。
配線図
前回のこの回路に追加でスイッチをPB2(16番ピン)に接続します。私が使用したロータリーエンコーダは軸が押しボタンスイッチになっているのでそれを使いました。
コード
最終目的のNC旋盤のファームウェア開発の為にちょこちょことファイルを分割しています。長ったらしくなりますがご了承ください。全てを説明するととても長くなるのでざっくりとだけにしておきます。
素人の作ったコードなので無駄や間違いがあるかもしれませんがとりあえずは動きました。
ロータリーエンコーダ
#ifndef ENCODER_H_
#define ENCODER_H_
#include <avr/io.h>
#include <avr/sfr_defs.h>
/* ロータリーエンコーダのA端子をPB0にB端子をPB1に接続する */
#define ENCODER_PIN_A PINB
#define ENCODER_PIN_B PINB
#define TERMINAL_A PB0
#define TERMINAL_B PB1
/*
引 数 なし
戻り値 右回転だと1、左回転だと-1、動いていないと0を返す
機 能 ロータリーエンコーダの回転方向を得る
*/
int encoder(void);
#endif /* ENCODER_H_ */
8〜11行目のPINBやPB0を書き換えればロータリーエンコーダの入力ピンを変更出来ます。
#include "encoder.h"
int encoder(void)
{
if (bit_is_clear(ENCODER_PIN_A, TERMINAL_A)) // A相がLになった時
{
while (bit_is_clear(ENCODER_PIN_A, TERMINAL_A)) // A相がLの間ループ、Hになるまで待機
{}
if (bit_is_clear(ENCODER_PIN_B, TERMINAL_B)) // B相がLの場合(右回転)
return 1;
else
return -1;
}
return 0;
}
シンプルにする為回転方向によって戻り値が変化する関数を作ってみました。この関数をタイマ割り込みの中で使います。
LCDモジュール
#include <avr/io.h>
#include <util/delay.h>
#ifndef LCD_H_
#define LCD_H_
/* LCDのデータビットに使うポートの指定(PD4,5,6,7) */
#define LCD_PORT PORTD
/* レジスター・セレクト・シグナルは何番ピンを使用するのか決める */
#define LCD_RS PD1
/* イネーブル・シグナルは何番ピンを使用するのか決める */
#define LCD_E PD2
// 4ビット送信
void lcd_out(char code, char rs);
// コマンド送信
void lcd_cmd(char cmd);
// データ送信
void lcd_data(char asci);
/*
引 数 行, 列
戻り値 なし
機 能 カーソル位置を変更する
*/
void lcd_pos(char line, char col);
/*
引 数 なし
戻り値 なし
機 能 LCDの表示をすべて消去する
*/
void lcd_clear(void);
/*
引 数 文字列
戻り値 なし
機 能 文字列をLCDに表示する
*/
void lcd_str(char *str);
/*
引 数 なし
戻り値 なし
機 能 LCDモジュール使用するための初期化関数
*/
void lcd_init(void);
/*
引 数 数値, 桁数, 単位(mmなど), 小数点(何桁まで整数か)
戻り値 なし
機 能 数値を数字に変換してLCDに表示する
*/
void lcd_inttoa(unsigned int i,int digit, char *unit, int decimal_point);
#endif /* LCD_H_ */
以前のものより説明が多めになりました。それとlcd_inttoa( )という関数を追加しました。数値を数字としてLCDに表示する関数です。
例えばaという変数に12345が代入されていた場合、lcd_inttoa(a, 5, " mm", 3);
とすればLCDに「123.45 mm」と表示されます。aが1だと「000.01 mm」と表示されます。
#include "lcd4bit.h"
void lcd_out(char code, char rs) {
LCD_PORT = (code & 0xF0) | (LCD_PORT & 0x0F);
if (rs == 0)
LCD_PORT = LCD_PORT & ~LCD_RS;
else
LCD_PORT = LCD_PORT | LCD_RS;
_delay_ms(1);
LCD_PORT = LCD_PORT | LCD_E;
_delay_ms(1);
LCD_PORT = LCD_PORT & ~LCD_E;
}
void lcd_cmd(char cmd) {
lcd_out(cmd, 0);
lcd_out(cmd<<4, 0);
_delay_ms(2);
}
void lcd_data(char asci) {
lcd_out(asci, 1);
lcd_out(asci<<4, 1);
_delay_ms(0.05);
}
void lcd_pos(char line, char col) {
if (line == 1)
lcd_cmd(0x80 + col - 1);
else if (line == 2)
lcd_cmd(0xC0 + col -1);
}
void lcd_clear(void) {
lcd_cmd(0x01);
}
void lcd_str(char *str) {
while(*str != '¥0') {
lcd_data(*str);
str++;
}
}
void lcd_init(void) {
_delay_ms(15);
lcd_out(0x30, 0);
_delay_ms(5);
lcd_out(0x30, 0);
_delay_ms(1);
lcd_out(0x30, 0);
_delay_ms(1);
lcd_out(0x20, 0); // 4ビットモードに設定
_delay_ms(1);
lcd_cmd(0x28); // 2行表示設定, 液晶2行, フォント5x7ドット
lcd_cmd(0x0C); // 画面表示ON, カーソルOFF, カーソル位置で点滅0FF
lcd_cmd(0x06); // カーソル移動右(インクリメントモード), 表示をシフトOFF
lcd_cmd(0x01); // 画面クリア
}
void lcd_inttoa(unsigned int i,int digit, char *unit, int decimal_point)
{
char numbers[15];
int j;
for (j = digit - 1; j >= 0; j--)
{
numbers[j] = ((i) % 10) + 0x30; // 数値を数字に変換
i /= 10;
}
for (i = 0; i < decimal_point ; i++)
{
lcd_data(numbers[i]);
}
if (decimal_point)
lcd_str("."); // 小数点を付ける
for (i = decimal_point; i < digit ; i++)
{
lcd_data(numbers[i]);
}
if (unit != 0)
lcd_str(unit); // 単位を付ける 例)mm
}
追加した関数は62〜88行の部分です。
main.c
EEPROMの関数を使う為に<avr/eeprom.h>と<stdint.h>をインクルードしました。stdint.hは「uint16_t」という型を使う為にインクルードしました。
#define F_CPU 1000000UL // 1MHz
/* ロータリーエンコーダ */
#define RE_PUSH PB2 // ロータリーエンコーダのプッシュスイッチ
#define RE_PUSH_PIN PINB
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include "lcd4bit.h"
#include "encoder.h"
#include <avr/eeprom.h>
#include <stdint.h>
#include <avr/sfr_defs.h>
/* 設定値 */
uint16_t xBacklash, zBacklash, xmmPerStep, zmmPerStep, centerOffset1, centerOffset2, xOrigin, zOrigin, numericalValue, coefficient;
uint16_t* eepromAddress;
/* EEPROMアドレス */
static uint16_t EEMEM EEPROM_X_BACKLASH;
static uint16_t EEMEM EEPROM_Z_BACKLASH;
static uint16_t EEMEM EEPROM_X_MM_PER_STEP;
static uint16_t EEMEM EEPROM_Z_MM_PER_STEP;
static uint16_t EEMEM EEPROM_CENTER_OFFSET_1;
static uint16_t EEMEM EEPROM_CENTER_OFFSET_2;
static uint16_t EEMEM EEPROM_X_ORIGIN;
static uint16_t EEMEM EEPROM_Z_ORIGIN;
volatile struct
{
// int position;
short moved;
} Encoder;
volatile struct
{
unsigned short directory; // 階層1
unsigned short index; // 階層2
unsigned short selectValue; // 値を変更
} Menu;
/* EEPROM読み込み */
void conf_init(void)
{
eeprom_busy_wait();
xBacklash = eeprom_read_word(&EEPROM_X_BACKLASH);
eeprom_busy_wait();
zBacklash = eeprom_read_word(&EEPROM_Z_BACKLASH);
eeprom_busy_wait();
xmmPerStep = eeprom_read_word(&EEPROM_X_MM_PER_STEP);
eeprom_busy_wait();
zmmPerStep = eeprom_read_word(&EEPROM_Z_MM_PER_STEP);
eeprom_busy_wait();
centerOffset1 = eeprom_read_word(&EEPROM_CENTER_OFFSET_1);
eeprom_busy_wait();
centerOffset2 = eeprom_read_word(&EEPROM_CENTER_OFFSET_2);
eeprom_busy_wait();
xOrigin = eeprom_read_word(&EEPROM_X_ORIGIN);
eeprom_busy_wait();
zOrigin = eeprom_read_word(&EEPROM_Z_ORIGIN);
}
void lcd_menu(void)
{
/* メニュー階層1 */
if (Menu.directory < 15) // 上位4ビットが0の場合
{
switch (Menu.directory) {
/*
>Menu
Machine Config
*/
case 0x00:
lcd_clear();
lcd_str(">Menu");
lcd_pos(2,1);
lcd_str("Machine Config");
break;
/*
Menu
>Machine Config
*/
case 0x01:
lcd_clear();
lcd_str("Menu");
lcd_pos(2,1);
lcd_str(">Machine Config");
break;
default:
Menu.directory = 0x00;
break;
}
}
/* メニュー階層2 */
if (Menu.index)
{
switch (Menu.directory) {
/*
[Machine Config]
>Back
*/
case 0x10:
lcd_clear();
lcd_str("[Machine Config]");
lcd_pos(2,1);
lcd_str(">Back");
break;
/*
X-Axis Backlash
00.000 mm
*/
case 0x11:
eepromAddress = &EEPROM_X_BACKLASH; // EEPROMのアドレスを代入
numericalValue = xBacklash; // 現在値を代入
lcd_clear();
lcd_str("X-Axis Backlash");
lcd_pos(2,2);
lcd_inttoa(xBacklash, 5, " mm", 2);
break;
/*
Z-Axis Backlash
00.000 mm
*/
case 0x12:
eepromAddress = &EEPROM_Z_BACKLASH;
numericalValue = zBacklash;
lcd_clear();
lcd_str("Z-Axis Backlash");
lcd_pos(2,2);
lcd_inttoa(zBacklash, 5, " mm", 2);
break;
/*
X-Axis mm/step
00.000 mm/step
*/
case 0x13:
eepromAddress = &EEPROM_X_MM_PER_STEP;
numericalValue = xmmPerStep;
lcd_clear();
lcd_str("X-Axis mm/step");
lcd_pos(2,2);
lcd_inttoa(xmmPerStep, 5, " mm/step", 2);
break;
/*
Z-Axis mm/step
00.000 mm/step
*/
case 0x14:
eepromAddress = &EEPROM_Z_MM_PER_STEP;
numericalValue = zmmPerStep;
lcd_clear();
lcd_str("Z-Axis mm/step");
lcd_pos(2,2);
lcd_inttoa(zmmPerStep, 5, " mm/step", 2);
break;
/*
Center Offset 1
000.00 mm
*/
case 0x15:
eepromAddress = &EEPROM_CENTER_OFFSET_1;
numericalValue = centerOffset1;
lcd_clear();
lcd_str("Center Offset 1");
lcd_pos(2,2);
lcd_inttoa(centerOffset1, 5, " mm", 3);
break;
/*
Center Offset 2
-000.00 mm
*/
case 0x16:
eepromAddress = &EEPROM_CENTER_OFFSET_2;
numericalValue = centerOffset2;
lcd_clear();
lcd_str("Center Offset 2");
lcd_pos(2,1);
lcd_str("-");
lcd_inttoa(centerOffset2, 5, " mm", 3);
break;
/*
X Machine Origin
-00.000 mm
*/
case 0x17:
eepromAddress = &EEPROM_X_ORIGIN;
numericalValue = xOrigin;
lcd_clear();
lcd_str("X Machine Origin");
lcd_pos(2,1);
lcd_str("-");
lcd_inttoa(xOrigin, 5, " mm", 2);
break;
/*
Z Machine Origin
-00.000 mm
*/
case 0x18:
eepromAddress = &EEPROM_Z_ORIGIN;
numericalValue = zOrigin;
lcd_clear();
lcd_str("Z Machine Origin");
lcd_pos(2,1);
lcd_str("-");
lcd_inttoa(zOrigin, 5, " mm", 2);
break;
default:
Menu.directory = 0x10;
break;
}
}
}
/* タイマー割り込み */
ISR(TIMER0_COMPA_vect)
{
static uint8_t holdDown;
static uint8_t digit = 4;
int i, value;
Encoder.moved = encoder();
if (Menu.selectValue)
{
if ((numericalValue == 0) && (Encoder.moved == -1))
Encoder.moved = 0;
else
{
if (Encoder.moved)
{
value = Encoder.moved;
for (i = digit;i > 0 ; i--)
{
value *=10;
}
numericalValue += value;
Encoder.moved = 0;
} // if (Encoder.moved)
switch (Menu.directory)
{
case 0x15: // Center Offset1は 000.00で表示している
case 0x16: // Center Offset2は-000.00で表示している
if (digit > 1)
{
lcd_pos(1, 5 - digit);
lcd_str(" _"); // 0x5Fは"_"
}
else
{
lcd_pos(1, 5 - digit);
lcd_str(" _");
}
lcd_pos(2, 2);
lcd_inttoa(numericalValue, 5, "", 3);
break;
default:
if (digit > 2) // 変更箇所の上に"_"
{
lcd_pos(1, 5 - digit);
lcd_str(" _");
}
else
{
lcd_pos(1, 5 - digit);
lcd_str(" _");
}
lcd_pos(2, 2);
lcd_inttoa(numericalValue, 5, "", 2);
break;
} // switch (Menu.directory)
} // else ((numericalValue == 0) && (Encoder.moved == -1))
} // if (Menu.selectValue)
else if (Encoder.moved && !Menu.selectValue)
{
if ((Encoder.moved == -1) && !(Menu.directory & 0x0F)) // movedが-1かつMenu.directoryの下位4ビットが0の場合
Encoder.moved = 0;
else
{
Menu.directory += Encoder.moved;
Encoder.moved = 0;
lcd_menu();
}
}
/* PB2が押されていたら */
if (bit_is_clear(RE_PUSH_PIN, RE_PUSH))
{
holdDown++;
if (holdDown > 5)
{
if (!Menu.index)
{
Menu.directory = Menu.directory << 4; // 0x01の場合0x10になる
Menu.index = 1;
lcd_menu();
}
else
{
if (Menu.selectValue)
{
if (digit == 0)
{
// EEPROM書き換え
eeprom_busy_wait();
eeprom_update_word(eepromAddress, numericalValue);
Menu.selectValue = 0;
digit = 4;
lcd_clear();
lcd_str("EEPROM Update");
_delay_ms(1000);
conf_init();
lcd_menu();
}
else
{
digit--;
}
}
else
{
if (Menu.directory & 0x0F) // 設定値変更
{
Menu.selectValue = 1;
lcd_pos(1,1);
lcd_str(" ");
}
else // Menu.directoryの下位4ビットが0の場合
{
Menu.directory = Menu.directory >> 4; // 0x10の場合0x01になる
Menu.index = 0;
lcd_menu();
}
} // else (Menu.selectValue == 0)
} // else (Menu.index == 0)
holdDown = 0;
} // if (holdDown > 10)
} // if (bit_is_clear(RE_PUSH_PIN, RE_PUSH))
else
holdDown = 0;
}
int main(void)
{
/* LCD設定 */
DDRD = 0b11111111; // LCDピンに使用
PORTD = 0b00000000; // ポートD初期化
lcd_init();
/* ロータリーエンコーダ */
DDRB = 0b00000000;
PORTB = 0b00000111;
/* タイマー割り込み設定 */
TCCR0A = 0b00000010; // CTC
TCCR0B = 0b00000101; // 1MHz/1024 = 約977Hz→約1kHz
OCR0A = 10; // 10msで割り込み
TIMSK0 = 0b00000010; // COMPA割り込み
conf_init();
lcd_str("EEPROM-TEST");
sei(); // 全体の割り込み許可
/* Replace with your application code */
while (1)
{
}
}
ざっと見てもらうと分かるように長いです。
とりあえずEEPROMに関するのは20〜27、44〜62、311、312行目といったところだと思います。他所のサイトを参考にしているのでEEPROMに関する細かい事はよく分かりませんが、読み込みにはeeprom_read_word( )、書き込みにはeeprom_update_word( )という関数を使用しました。wordの部分が16bitを表しており他にもbyte(8bit)、dword(32bit)などがあります。
eeprom_busy_wait( )というのは読み書き可能になるまで待つ関数だそうです。
今後演算に使用する予定なので44行目の関数でEEPROMのデータを読み出して変数に代入しています。
lcd_menu( )という64〜216行目までの関数はMenu.directoryという変数の値によってLCDの表示を変えています。とても長いですがコメント部分も結構あります。ほとんどswitch文なので内容は簡単です。0x10だとこれを表示して、0x11だとこれを表示するといった内容です。
ピン・チェンジ・インタラプトだと誤作動が多かったので今回はタイマ割り込みの中にボタンが押されていた時の処理を書いています。292〜346行目までです。割り込みが発生する度にボタンが押されているのか確認してカウントアップしていき既定の値になったら処理を実行するという風にしました。これをチャタリング対策としました。カウントする値を増やせば長押しなどに応用できると思います。途中で離すとカウントは0になります。if文の条件によってボタンが押された時の状況を判断しています。
まとめ
ロータリーエンコーダを回すとどこの値が変化するのか分かりにくかったので、数字を点滅させようとしましたがあまり上手く行かなかったので数値の入力箇所の上に「_」を表示するようにしました。
動画の方が分かりやすいかもしれません。
次は加工する為の値の入力画面を作って、Gコードを生成するプログラムを作ろうと思います。
NC旋盤とNCフライス盤だとGコードが微妙に違い(メーカーによっても違いますが)戸惑っています。まあ自分が使うだけなのでGコードに変換しなくても良いかもしれませんが、一から考えるより既存のものを参考にした方がトラブルも少なく早く完成しそうではあります。grblを使おうと思ったのですが、これを使ってNC旋盤を作られている方が少なかったので情報収集に時間を消費するより自分で作る方が良いかなと思った次第です。