Clock Multiplier & Divider を作る
概要
HAGIWOの作品にクロック マルチプル/ディバイダーというのがある。一定のパルスを発生するclock generator に加えて、そのパルスを逓倍(マルチブル)したり、分周(ディバイダー)したりできる。これを作ろうと、ブレッドボードに回路を組んで動かしているうちに、自分なりに手を加えたくなった。
あれこれソフトをいじってしまって、パルス波形に乱れが出るようになってしまった。おそらく、ソフトの処理早さと抵抗やコンデンサの値が微妙に影響しているような気がする。機能を追加するにも、ソフトは変数や条件分岐が多岐に渡っており、それを理解するのが大変た。
このソフトに手を出して、あれこれ変更(改悪だと思う)やバグ取りを繰り返すよりも、自分流のクロック マルチプル/ディバイダー を作ることにした。
機能・仕様
ハード構成
- 電源:+12V (基板上でLODにより +5V を生成)
- マイコン:Atmega328P(16MHzのセラロックをつけ、単体使用)
入力
- BPM設定
- デューティ比設定
- 分周比設定
- Multi/Divモード選択用トグルスイッチ
- AND/OR ロジック選択用トグルスイッチ
- 外部クロック:任意波形
出力
- OUTPUT1:マスタークロック(内部クロック、あるいは外部クロック)
- 外部クロックが4秒以上入らなければ、内部クロックを出力する。
- 内部クロック:60BPM から600BPMまで可変。Duty 10% から90%まで可変。
- 外部クロック:OUTPUT3を取り込んで出力する。周期4秒以下の任意波形。
- OUTPUT2:マスタークロックから、マルチプル/ディバイダーを生成
- マルチプル:x1, 2, 3, 4, 6, 8, 16
- ディバイダー:/1, /2, /3, /4, /6, /8, /16
- Multi/Divモード選択式式
- OUTPUT3:任意波形の外部クロックをdigitalRead()して、パルスのクロック波形を整形する。非出力。(出力ポートなし)
- OUTPUT4:OUTPUT1 とOUTPUT2 の論理積(AND)あるいは論理和(OR)
- AND/OR ロジック選択式
ピン設定
ピン名 | ピン番号 | 説明 |
---|---|---|
divisionPin |
A3 | 分周比設定 |
ratePin |
A4 | BPM設定 |
dutyPin |
A1 | デューティ比設定 |
logikalPin |
11 | AND/OR選択ピン |
selectPin |
13 | Multiply/Divideモード選択ピン |
outputPin1 |
7 | OUTPUT1 |
outputPin2 |
8 | OUTPUT2 |
outputPin3 |
2 | OUTPUT3 (割り込み0) |
outputPin4 |
6 | OUTPUT4 |
externalClockPin |
3 | 外部クロック入力ピン (割り込み1) |
debouncePin |
A0 | デバウンス用ポテンショメータ (ソフト設計時の調整用) |
回路図
入出力回路の各素子は、クロック マルチプル/ディバイダーからのフルコピー。
ソフト(スケッチ)
起動時にDelay
ソフトの起動時、発振気味の波形乱れがあったが、その原因を見つけることができず、しかたなく強制的に出力を止めている。
- 起動後
STARTUP_DELAY
(4000 ms)経過前は、全てのOUTPUTはLOW。
同期処理
最も苦労したのは、OUTPUT1 とOUTPUT2を同期させる方法だった。試行錯誤の結果、次の方法でやっとうまくいった。
タイマーの割り込みを使用してOUTPUT1とOUTPUT2を同期させる: 既存のコードのタイマー設定を利用し、分周比が変化した際にOUTPUT1とOUTPUT2の状態をリセットする方法。
具体的には、分周比が変化した際に、
divisionIndex
が変化した際に、lastDivisionIndex
と比較して変化を検知し、lastDivisionIndex
を更新する。変化が検知された場合、OUTPUT1とOUTPUT2のトグルタイミングをリセットする。lastToggle1
とlastToggle2
を0にリセットし、output1State
とoutput2State
をLOWに設定します。また、digitalWriteを使用して、OUTPUT1とOUTPUT2をLOWに設定する。
if (divisionIndex != lastDivisionIndex) {
// 分周比が変わったときにOUTPUT1とOUTPUT2を初期化
noInterrupts();
lastToggle1 = 0;
lastToggle2 = 0;
output1State = LOW;
output2State = LOW;
digitalWrite(outputPin1, LOW);
digitalWrite(outputPin2, LOW);
lastDivisionIndex = divisionIndex;
interrupts();
}
外部クロック処理、High/Low時間の計測
割り込みpin3に入力される外部クロック信号をdijitalread()で読み込みパルス波(OUTPUT3)に整形している。また、そのパルス波のHigh時間とLow時間を測定し、外部クロックをマスタークロックとするときの逓倍/分周波形の周波数に換算している。
OUTPUT3の信号は外部クロックがある場合にOUTPUT1から出力されるので、OUTPUT3用の出力ポートはない。
void output3ISR() {
unsigned long currentTime = micros();
if (digitalRead(outputPin3) == HIGH) {
lastRisingEdgeTime = currentTime;
if (lastFallingEdgeTime != 0) {
lowDuration = lastRisingEdgeTime - lastFallingEdgeTime;
}
risingEdgeCaptured = true;
} else {
if (risingEdgeCaptured) {
lastFallingEdgeTime = currentTime;
highDuration = lastFallingEdgeTime - lastRisingEdgeTime;
risingEdgeCaptured = false;
}
}
}
また、時間計測でのオーバーフロー対策として、ありえない値は無視するようにしている。
#define MIN_VALID_DURATION 5000 // 5000 μs 未満の値は無視する。外部クロックの周期測定時
分周比設定KNOBの目盛調整
逓倍/分周比用のKNOBは300度を6等分した目盛を府っている。この目盛と逓倍/分周比が一致するように実験的に求めた閾値をスケッチに書き込んだ。
const int thresholds[] = {86, 269, 449, 619, 806, 966}; // 閾値の配列
// 省略
int divisionIndex = 0;
for (int i = 0; i < 6; i++) {
if (pot1Value >= thresholds[i]) {
divisionIndex = i + 1;
} else {
break;
}
}
if (selectValue == HIGH) { // Multiply mode
divisionIndex = 12 - divisionIndex;
}
完全なコード
#include
#define UART_TRACE (0) // alternative
#define STARTUP_DELAY 4000 // (ms)起動時の遅延時間および、外部クロック有無判定の待ち時間
#define MIN_VALID_DURATION 5000 // 5000 μs 未満の値は無視する。外部クロックの周期測定時
const int debouncePin = A0; // デバウンス調整用ポテンショメータ
const int dutyPin = A1; // デューティ比
const int divisionPin = A3; // 分周比(乗算/除算)
const int ratePin = A4; // レート(BPM)
const int externalClockPin = 3; // 外部クロック入力ピン(デジタル)
const int logicalPin = 11; // 論理選択ピン
const int outputPin1 = 7; // OUTPUT1
const int outputPin2 = 8; // OUTPUT2
const int outputPin3 = 2; // OUTPUT3
const int outputPin4 = 6; // OUTPUT4
const int selectPin = 13; // ディバイダ/マルティプル選択ピン
int min_BPM = 60;
int max_BPM = 600;
const int divisions[] = {1, 2, 3, 4, 6, 8, 16}; // 分周比の配列
const int thresholds[] = {86, 269, 449, 619, 806, 966}; // 閾値の配列
volatile unsigned long output1OnTime = 0;
volatile unsigned long output1OffTime = 0;
volatile unsigned long output2OnTime = 0;
volatile unsigned long output2OffTime = 0;
volatile bool output1State = LOW;
volatile bool output2State = LOW;
volatile bool output3State = LOW;
unsigned long lastToggle1 = 0;
unsigned long lastToggle2 = 0;
int lastDivisionIndex = -1; // 最後のdivisionIndexを保持
volatile unsigned long lastExternalClockTime = 0;
volatile bool externalClockPresent = false;
volatile unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50; // (us) デバウンス用遅延時間
volatile unsigned long lastRisingEdgeTime = 0;
volatile unsigned long lastFallingEdgeTime = 0;
volatile unsigned long highDuration = 0; // us
volatile unsigned long lowDuration = 0; // us
volatile bool risingEdgeCaptured = false;
unsigned long waitTime = 0; // 起動時刻を記録する変数
volatile bool waitedComplete = false; // 起動完了フラグ
void setup() {
#if (UART_TRACE)
Serial.begin(9600);
#endif
pinMode(outputPin1, OUTPUT);
pinMode(outputPin2, OUTPUT);
pinMode(outputPin3, OUTPUT);
pinMode(outputPin4, OUTPUT);
pinMode(selectPin, INPUT_PULLUP);
pinMode(logicalPin, INPUT_PULLUP);
pinMode(externalClockPin, INPUT);
pinMode(debouncePin, INPUT);
waitTime = millis(); // 起動時刻を記録
// Timer1の設定
cli();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 249; // 1msごとに割り込み
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS11) | (1 << CS10);
TIMSK1 |= (1 << OCIE1A);
sei();
attachInterrupt(digitalPinToInterrupt(externalClockPin), externalClockISR, CHANGE);
attachInterrupt(digitalPinToInterrupt(outputPin3), output3ISR, CHANGE);
}
void loop() {
// STARTUP_DELAYが経過したら起動完了フラグを立てる
if (!waitedComplete && (millis() - waitTime > STARTUP_DELAY)) {
waitedComplete = true;
}
// ここに他のループ処理を追加できます
int pot1Value = analogRead(divisionPin);
int pot2Value = analogRead(ratePin);
int pot3Value = analogRead(dutyPin);
int selectValue = digitalRead(selectPin);
int divisionIndex = 0;
for (int i = 0; i < 6; i++) {
if (pot1Value >= thresholds[i]) {
divisionIndex = i + 1;
} else {
break;
}
}
if (selectValue == HIGH) { // Multiply mode
divisionIndex = 12 - divisionIndex;
}
if (divisionIndex != lastDivisionIndex) {
// 分周比が変わったときにOUTPUT1とOUTPUT2を初期化 <<同期のための処理>>
noInterrupts();
lastToggle1 = 0;
lastToggle2 = 0;
output1State = LOW;
output2State = LOW;
digitalWrite(outputPin1, LOW);
digitalWrite(outputPin2, LOW);
digitalWrite(outputPin3, LOW);
digitalWrite(outputPin4, LOW);
lastDivisionIndex = divisionIndex;
interrupts();
}
int division = selectValue == HIGH ? divisions[12 - divisionIndex] : divisions[divisionIndex];
int BPM = map(pot2Value, 0, 1023, min_BPM, max_BPM);
int dutyCycle = map(pot3Value, 0, 1023, 1, 9) * 10;
unsigned long cycleTime = 60000UL / BPM;
noInterrupts();
// 外部クロックがない場合
if (!externalClockPresent) {
output1OnTime = cycleTime * (100 - dutyCycle) / 100;
output1OffTime = cycleTime - output1OnTime;
} else { // 外部クロックがある場合
output1OnTime = lowDuration / 1000;
output1OffTime = highDuration / 1000;
}
if (selectValue == HIGH) { // Multiply mode
output2OnTime = output1OnTime / division;
output2OffTime = output1OffTime / division;
} else { // Divide mode
output2OnTime = output1OnTime * division;
output2OffTime = output1OffTime * division;
}
interrupts();
// 外部クロックの有無をチェック
noInterrupts();
if (millis() - lastExternalClockTime > STARTUP_DELAY) {
externalClockPresent = false;
} else {
externalClockPresent = true;
}
interrupts();
#if (UART_TRACE)
// シリアル出力
Serial.print("BPM: ");
Serial.print(BPM);
Serial.print(" | POT: ");
Serial.print(pot1Value);
Serial.print(" | Div: ");
Serial.print(selectValue == HIGH ? "" : "/" );
Serial.print(division);
Serial.print(" | DI: ");
Serial.print(divisionIndex);
Serial.print(" | DC: ");
Serial.print(dutyCycle);
Serial.print("% | On1: ");
Serial.print(output1OnTime);
Serial.print(" | Off1: ");
Serial.print(output1OffTime);
Serial.print(" | On2: ");
Serial.print(output2OnTime);
Serial.print(" | Off2: ");
Serial.print(output2OffTime);
Serial.print(" | T1: ");
Serial.print(lastToggle1 / 1000); // ms
Serial.print(" | T2: ");
Serial.print(lastToggle2 / 1000); // ms
Serial.print(" | ExtClk: ");
Serial.print(externalClockPresent);
Serial.print(" | Debounce: ");
Serial.print(debounceDelay); // us
Serial.print(" | HD: ");
Serial.print(highDuration); // ms
Serial.print(" | LD: ");
Serial.print(lowDuration); // ms
Serial.println();
#endif
}
ISR(TIMER1_COMPA_vect) {
unsigned long currentTime = micros();
// 起動完了前は出力しない
if (!waitedComplete) {
return;
}
// OUTPUT1のトグル処理
if (currentTime - lastToggle1 >= (output1State ? output1OffTime * 1000 : output1OnTime * 1000)) {
output1State = !output1State;
digitalWrite(outputPin1, output1State ? HIGH : LOW);
lastToggle1 = currentTime;
}
// OUTPUT2のトグル処理
if (currentTime - lastToggle2 >= (output2State ? output2OffTime * 1000 : output2OnTime * 1000)) {
output2State = !output2State;
digitalWrite(outputPin2, output2State ? HIGH : LOW);
lastToggle2 = currentTime;
}
// OUTPUT3の処理
if (!externalClockPresent) {
digitalWrite(outputPin3, output1State ? HIGH : LOW);
}
// OUTPUT4の処理
bool output4State;
if (digitalRead(logicalPin) == HIGH) { // AND logic
output4State = digitalRead(outputPin1) && digitalRead(outputPin2);
} else { // OR logic
output4State = digitalRead(outputPin1) || digitalRead(outputPin2);
}
digitalWrite(outputPin4, output4State ? HIGH : LOW);
}
void externalClockISR() {
unsigned long currentTime = micros();
if (currentTime - lastDebounceTime < debounceDelay) {
return;
}
if (digitalRead(externalClockPin) == HIGH) {
lastRisingEdgeTime = currentTime;
if (lastFallingEdgeTime != 0) {
lowDuration = lastRisingEdgeTime - lastFallingEdgeTime;
}
risingEdgeCaptured = true;
} else {
if (risingEdgeCaptured) {
lastFallingEdgeTime = currentTime;
highDuration = lastFallingEdgeTime - lastRisingEdgeTime;
risingEdgeCaptured = false;
}
}
lastDebounceTime = currentTime;
lastExternalClockTime = millis();
externalClockPresent = true;
}
void output3ISR() {
unsigned long currentTime = micros();
if (digitalRead(outputPin3) == HIGH) {
lastRisingEdgeTime = currentTime;
if (lastFallingEdgeTime != 0) {
lowDuration = lastRisingEdgeTime - lastFallingEdgeTime;
}
risingEdgeCaptured = true;
} else {
if (risingEdgeCaptured) {
lastFallingEdgeTime = currentTime;
highDuration = lastFallingEdgeTime - lastRisingEdgeTime;
risingEdgeCaptured = false;
}
}
}